RussellLuo

让思想在文字间徜徉

应用场景

对于数据量较大的业务功能(比如日志),如果使用单个 ES 索引来存储文档,与日俱增的数据量很快就会使得单个索引过大,因为无法水平扩展,最终会导致机器空间不足。这种大数据量的场景下,需要对数据进行切分,将数据分段存储在不同的索引中。

Sizing Elasticsearch 介绍了常用的几种数据切分方法,因为这两天在工作中刚好用到过,所以在这里重点总结下 “基于时间的索引” (time-based indices) 的管理技巧。

选择时间范围

根据数据增长速度的不同,可以选择按天索引(索引名称形如 2017-05-16),或者按月索引(索引名称形如 2017-05)等等。

设计索引模板

面对这么多不断新增的索引,如何管理它们的 settings 和 mappings 呢?一个一个地去手动维护,无疑是个噩梦。这时,就需要用到 ES 的 Index Templates 机制。

Index Templates 的基本原理是:首先预定义一个或多个 “索引模板”(index template,其中包括 settings 和 mappings 配置);然后在创建索引时,一旦索引名称匹配了某个 “索引模板”,ES 就会自动将该 “索引模板” 包含的配置(settings 和 mappings)应用到这个新创建的索引上面。

以日志为例,假设我们的 ES 索引需求如下:

  1. 按天索引(索引名称形如 log-2017-05-16)
  2. 每天的日志数据,只会进入当天的索引
  3. 搜索的时候,希望搜索范围是所有的索引(借助 alias)

基于上述索引需求,对应的 “索引模板” 可以设计为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ curl -XPUT http://localhost:9200/_template/log_template -d '{
"template": "log-*",
"settings": {
"number_of_shards": 1
},
"mappings": {
"log": {
"dynamic": false,
"properties": {
"content": {
"type": "string"
},
"created_at": {
"type": "date",
"format": "dateOptionalTime"
}
}
}
},
"aliases": {
"search-logs": {}
}
}'

两点说明:

  1. 创建索引时,如果索引名称的格式形如 “log-*”,ES 会自动将上述 settings 和 mappings 应用到该索引
  2. aliases 的配置,告诉 ES 在每次创建索引时,自动为该索引添加一个名为 “search-logs” 的 alias(别名)

索引与搜索

基于上述 “索引模板” 的设计,索引与搜索的策略就很直接了。

索引策略:每天的数据,只索引到当天对应的索引。比如,2017 年 5 月 16 日这天的数据,只索引到 log-2017-05-16 这个索引当中。

搜索策略:因为搜索需求是希望全量搜索,所以在搜索的时候,索引名称使用 “search-logs” 这个 alias 即可。

更多关于 “如何有效管理基于时间的索引” 的技巧,可以参考 Managing Elasticsearch time-based indices efficiently

本周在公司专项排查一个问题,最终问题被解决了,自己也感觉收获颇丰,特此总结一下。

一、问题背景

公司产品有一个数据导出功能,该功能一直以来饱受诟病,客户经常反馈说导出不了数据,于是就让客户支持人员帮忙手动导数据。客户体验差不说,客户支持人员也是苦不堪言。

内部实现上,该数据导出功能是通过 Celery 异步任务的方式来处理的。如果是因为导出数据量太大,导致任务超时被中途停掉,倒还可以理解。但大部分情况是,即使是很少量的数据,也无法导出。

二、排查分析

1. 查看 Celery 错误日志

当数据导出不了的时候,查看 Celery 错误日志,一般都是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[2016-11-15 14:17:09,478: ERROR/MainProcess] Exception during reset or similar
Traceback (most recent call last):
File "/data/app/eggs/SQLAlchemy-1.0.15-py2.7-linux-x86_64.egg/sqlalchemy/pool.py", line 636, in _finalize_fairy
fairy._reset(pool)
File "/data/app/eggs/SQLAlchemy-1.0.15-py2.7-linux-x86_64.egg/sqlalchemy/pool.py", line 774, in _reset
self._reset_agent.rollback()
File "/data/app/eggs/SQLAlchemy-1.0.15-py2.7-linux-x86_64.egg/sqlalchemy/engine/base.py", line 1563, in rollback
self._do_rollback()
File "/data/app/eggs/SQLAlchemy-1.0.15-py2.7-linux-x86_64.egg/sqlalchemy/engine/base.py", line 1601, in _do_rollback
self.connection._rollback_impl()
File "/data/app/eggs/SQLAlchemy-1.0.15-py2.7-linux-x86_64.egg/sqlalchemy/engine/base.py", line 670, in _rollback_impl
self._handle_dbapi_exception(e, None, None, None, None)
File "/data/app/eggs/SQLAlchemy-1.0.15-py2.7-linux-x86_64.egg/sqlalchemy/engine/base.py", line 1341, in _handle_dbapi_exception
exc_info
File "/data/app/eggs/SQLAlchemy-1.0.15-py2.7-linux-x86_64.egg/sqlalchemy/util/compat.py", line 202, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=cause)
File "/data/app/eggs/SQLAlchemy-1.0.15-py2.7-linux-x86_64.egg/sqlalchemy/engine/base.py", line 668, in _rollback_impl
self.engine.dialect.do_rollback(self.connection)
File "/data/app/eggs/SQLAlchemy-1.0.15-py2.7-linux-x86_64.egg/sqlalchemy/dialects/mysql/base.py", line 2542, in do_rollback
dbapi_connection.rollback()
File "/data/app/eggs/PyMySQL-0.7.6-py2.7.egg/pymysql/connections.py", line 772, in rollback
self._execute_command(COMMAND.COM_QUERY, "ROLLBACK")
File "/data/app/eggs/PyMySQL-0.7.6-py2.7.egg/pymysql/connections.py", line 1055, in _execute_command
self._write_bytes(packet)
File "/data/app/eggs/PyMySQL-0.7.6-py2.7.egg/pymysql/connections.py", line 1007, in _write_bytes
raise err.OperationalError(2006, "MySQL server has gone away (%r)" % (e,))
OperationalError: (pymysql.err.OperationalError) (2006, "MySQL server has gone away (error(32, 'Broken pipe'))")
[2016-11-15 14:17:09,478: ERROR/MainProcess] Hard time limit (1800s) exceeded for app.jobs.download.download_data[63c0d55e-76a5-42ef-8d65-7bd432bc0877]

分析上述日志:

  1. 似乎是 MySQL 报错了,但是 “MySQL server has gone away” 通常表明:client 端连接超时了,然后 server 端受不了了,所以强制 kill 掉了数据库连接(参考 Error 2006: MySQL server has gone away

  2. 导出任务执行超过了 1800 秒(30 分钟)!!然后被强制停掉了。。。

所有矛头都指向了一点:导出任务执行过慢。

2. 加日志作性能分析

导出任务执行过慢,最直接的判断就是:执行过程中有些步骤耗时过长。没啥好说的,加日志分别计算下每个步骤的执行时间呗。

这里特别提一点:对于使用了 SQLAlchemy 的代码,在对数据库查询作性能分析时,不必为每个查询语句都加日志,相关技巧请参考这篇官方文档 How can I profile a SQLAlchemy powered application?

加上日志后,(为了让修改后的代码生效)重启 celery-download,然后观察日志。然而。。。并没有发现异常,每个步骤的执行时间看起来都很合理。。。

3. 由 CPU 占用率引发的思考

事实证明,上面的排查手段并不奏效。重新整理思路后,决定不再过早地作判断,而是先搜集更多的有用信息。

很自然地,这一次注意到了 celery-download 的 CPU 占用率。一个 top 命令,发现 celery-download 的 CPU 占用率竟然接近 100%:

1
2
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND         
28912 tester 20 0 477576 116500 14600 S 99.3 8.5 71:07.62 [celeryd: celery_download...]

惊讶地同时,似乎也能解释得通了:这么高的 CPU 占用率,一定是哪里出现了死循环,导致整个 celery-download 进程几乎瘫痪,进而无法正常工作。

除此以外,经过一些尝试和观察,还注意到一个现象:如果重启 celery-download,CPU 占用率会瞬间降下来,并且维持一段时间的正常值,然后过一会儿 CPU 占用率又会飙到很高。这也解释了在上一步 加日志作性能分析 的时候,为什么没有发现问题了:刚刚重启后的一段时间内,一切都是正常的。

但是为何会出现上述这些现象,对此我毫无头绪。请教 Google 大神后,意外地搜到了这个 celery:issue#1845。仔细看完以后,简直可以用 “醍醐灌顶” 来形容。

4. 顺藤摸瓜找元凶

按照 celery:issue#1845 中给出的思路,进行一一排查:

1)strace 跟踪 celery-download 进程

1
2
3
4
5
6
7
8
9
10
11
$ strace -p 28912
...
epoll_wait(11, {{EPOLLIN|EPOLLOUT, {u32=30, u64=21474836725}}}, 130, 1) = 1
clock_gettime(CLOCK_MONOTONIC, {29956630, 774274775}) = 0
clock_gettime(CLOCK_MONOTONIC, {29956630, 774404883}) = 0
clock_gettime(CLOCK_MONOTONIC, {29956630, 774497018}) = 0
epoll_wait(11, {{EPOLLIN|EPOLLOUT, {u32=30, u64=21474836725}}}, 130, 1) = 1
clock_gettime(CLOCK_MONOTONIC, {29956630, 774643990}) = 0
clock_gettime(CLOCK_MONOTONIC, {29956630, 774770274}) = 0
clock_gettime(CLOCK_MONOTONIC, {29956630, 774861865}) = 0
...

可以看出,celery-download 进程一直在重复调用 epoll_wait 和 clock_gettime。参考 Linux 手册,我们知道 epoll_wait 返回 1 表示:有 1 个 fd(文件描述符)可用于读写。在这里,这个导致 epoll_wait 返回的 fd 就是 30。

再来看看这个 fd 有什么特别之处:

1
2
$ lsof -d 30|grep 28912
[celeryd: 28912 tester 30u IPv4 1026883657 0t0 TCP xx-celery-app0:3759->ip-10-10-10-10.xx:6379 (CLOSE_WAIT)

很明显地,”xx-celery-app0:3759->ip-10-10-10-10.xx:6379” 是一个指向 Redis 的 socket 连接,而且这个连接处于 CLOSE_WAIT 状态!正是这个 CLOSE_WAIT 状态的 Redis 连接,导致 epoll_wait 总是会立即返回,从而让 celery-download 进程陷入了不断调用 epoll_wait 的死循环中!!

而对于 “celery-download 重启后,CPU 占用率会恢复正常” 的现象,可以这样解释:因为进程结束时,会关闭它用到的所有文件描述符(包括 CLOSE_WAIT 连接);而对于新启动的进程,运行一段时间后,才会莫名其妙地产生 CLOSE_WAIT 连接 :-(

2)分析 CLOSE_WAIT 连接

那么上述 Redis 连接为什么会处于 CLOSE_WAIT 状态呢?

我们知道,Redis 有个 timeout 参数,参考 Redis 配置文件

1
2
# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0

如果 timeout 不为 0,当 client 端连接的空闲时间超过了 timeout 秒,server 端会主动关闭该连接(更多说明参考 Client timeouts)。这种 “server 端主动关闭超时的 client 端连接” 的机制,与之前提到的 MySQL 的机制,其实是类似的。

当然这里的 timeout 0 是官方的参考值,”ip-10-10-10-10.xx” 对应的 Redis 实例(注意:与 celery:issue#1845 中描述的情况不同,这个 Redis 实例不是用作 Celery 的 broker 或 result backend,而是用作普通的缓存),实际的 timeout 配置为 1200(秒)。

于是,我们可以大胆猜测:fd 为 30 的那个 Redis 连接,因为空闲时间超过了 1200 秒,进而被 Redis 的 server 端主动关闭了(发送 FIN 报文),但是因为 client 端没有正确关闭(即被动关闭的一方,也许响应了 ACK 报文,但是没有发送 FIN 报文),导致该连接一直处于 CLOSE_WAIT 状态。

到这里,尚存两点疑惑:

  1. 为什么 Redis 连接超过了 1200 秒,client 端还不主动关闭,非要等到 server 端关闭呢?
  2. 为什么 server 端关闭后,client 端不正确响应呢?

3)redis-py 的连接池机制

考虑到使用的 Redis 客户端库是 redis-py ,参考文档发现 redis-py 内部使用了 连接池机制,因此:一个连接用完后,不会被立即关闭,而会被释放到连接池中,等待下次取用。

查看 redis-py 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# redis-2.10.5-py2.7.egg/redis/client.py

class StrictRedis(object):

...

def execute_command(self, *args, **options):
"Execute a command and return a parsed response"
pool = self.connection_pool
command_name = args[0]
connection = pool.get_connection(command_name, **options)
try:
connection.send_command(*args)
return self.parse_response(connection, command_name, **options)
except (ConnectionError, TimeoutError) as e:
connection.disconnect()
if not connection.retry_on_timeout and isinstance(e, TimeoutError):
raise
connection.send_command(*args)
return self.parse_response(connection, command_name, **options)
finally:
pool.release(connection)

结合 redis-py:issue#306 的说法,可以进一步得知:redis-py 对于关闭连接的处理是被动的,只会在下一次使用该连接的时候,检测该连接的可用性;如果使用连接时,遇到报错 ConnectionError 或 TimeoutError,才会调用 disconnect() 关闭该连接。

综上所述,前面提到的两点疑惑,具体来讲,可以归结为一点:

  • 为什么 fd 为 30 的 Redis 连接,在 redis-py 中没有被再次使用过,进而导致 server 端关闭该连接后,redis-py 不能正确关闭该连接?

遗憾地是,目前为止,这个问题还没能得到解答。(因为 Celery 的并发使用了 gevent,所以怀疑过是 gevent 魔幻的 patch 处理redis-py 的连接池机制 产生了化学反应,然而这种猜测暂时无法得到验证。)

三、归纳总结

前面长篇大论地说了很多,关于这个问题,总结起来其实只有三点:

1. 根本原因

celery-download 进程,运行一段时间后,产生了处于 CLOSE_WAIT 状态的连接,这种连接会让系统调用 epoll_wait 立即返回,从而让进程陷入不断调用 epoll_wait 的死循环中,进而导致该进程无法正常工作。

2. 解决办法

将 Redis 的 timeout 配置修改为 0(即禁止 server 端主动关闭超时的 client 端连接),从源头上避免 CLOSE_WAIT 连接的产生。

3. 遗留问题

celery 3.1.24 + gevent 1.2a1 + redis-py 2.10.5 的组合,会出现这种问题:redis-py 的连接池机制无法复用某些连接,进而导致这些连接处于失控状态。

四、一些技巧

1. 问题复现

对于上述 epoll 与 CLOSE_WAIT 连接 的问题,用这个 简化示例 可以完美复现。

server 端的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# server.py

import SocketServer
import time

HOST = '127.0.0.1'
PORT = 9999

class ReusableTCPServer(SocketServer.TCPServer):
allow_reuse_address = True

class ServeAndCloseHandler(SocketServer.BaseRequestHandler):
"""
Sends some data and then closes, to test epoll behavior against.
"""

def handle(self):
for i in range(3):
self.request.sendall('data %d\n' % i)
time.sleep(.5)

if __name__ == '__main__':
server = ReusableTCPServer((HOST, PORT), ServeAndCloseHandler)
server.serve_forever()

client 端的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# client.py

import select
import socket

HOST = '127.0.0.1'
PORT = 9999

def main():
s = socket.socket()
s.connect((HOST, PORT))
epoll = select.epoll()
epoll.register(s, select.POLLIN)
while True:
epoll.poll()
data = s.recv(256)

if __name__ == '__main__':
exit(main())

复现步骤提示:

  1. 启动 server 端(python server.py)
  2. 启动 client 端(python client.py)
  3. 观察 client 端进程的 CPU 占用率(top)
  4. 跟踪 client 端进程的执行情况(strace)
  5. 观察 client 端进程的 CLOSE_WAIT 连接(lsof)

2. 关闭 CLOSE_WAIT 连接

我们知道,重启进程可以去掉 CLOSE_WAIT 连接。但是重启进程毕竟动作太大,有没有办法在进程运行的同时,去掉该进程中产生的 CLOSE_WAIT 连接呢?

答案是肯定的,借助 gdb 就可以做到。继续上面的例子,假设进程号为 28912、CLOSE_WAIT 连接的 fd 为 30,可以使用以下命令:

1
$ gdb -p 28912 -ex 'p close(30)' -ex 'set confirm off' -ex 'quit'

Mock 全局符号

这里的 符号 包括:模块、类、类的实例、函数等。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# example.py

import the_module
from module import TheClass, the_instance, the_func


def foo():
return the_module.constant


def bar():
return TheClass.class_method()


def wow():
return the_instance.attribute


def sigh():
return the_func()

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> from mock import MagicMock, patch
>>> from example import foo, bar, wow, sigh
>>> @patch('example.the_module', constant='Foo')
... def test_foo(mock_the_module):
... assert foo() == 'Foo'
...
>>> test_foo()
>>> @patch('example.TheClass', class_method=MagicMock(return_value='Bar'))
... def test_bar(mock_the_class):
... assert bar() == 'Bar'
... mock_the_class.method.assert_called_once()
...
>>> test_bar()
>>> @patch('example.the_instance', attribute='Wow')
... def test_wow(mock_the_instance):
... assert wow() == 'Wow'
...
>>> test_wow()
>>> @patch('example.the_func', return_value='Sigh')
... def test_sigh(mock_the_func):
... assert sigh() == 'Sigh'
... mock_the_func.assert_called_once()
...
>>> test_sigh()

Mock 局部实例化的类

代码:

1
2
3
4
5
6
7
# example.py

from module import TheClass


def foo():
return TheClass()

测试:

1
2
3
4
5
6
7
>>> from mock import patch
>>> from example import foo
>>> @patch('example.TheClass', return_value='Foo')
... def test(mock_class):
... assert foo() == 'Foo'
...
>>> test()

Mock 局部实例化的类的属性

代码:

1
2
3
4
5
6
7
8
# example.py

from module import TheClass


def foo():
the_class = TheClass()
return the_class.method()

测试:

1
2
3
4
5
6
7
>>> from mock import MagicMock, patch
>>> from example import foo
>>> @patch('example.TheClass', return_value=MagicMock(method=MagicMock(return_value='Foo')))
... def test(mock_class):
... assert foo() == 'Foo'
...
>>> test()

如果 method 的返回值比较复杂,这样写可读性更高:

1
2
3
4
5
6
7
8
9
10
>>> from mock import MagicMock, patch
>>> from example import foo
>>> @patch('example.TheClass')
... def test(mock_class):
... method = MagicMock(return_value={'value': 'Foo'})
... mock_class.return_value = MagicMock(method=method)
... result = foo()
... assert result['value'] == 'Foo'
...
>>> test()

Mock 类的属性

代码:

1
2
3
4
5
6
7
8
9
# example.py

class TheClass(object):

def foo(self):
return self.bar()

def bar(self):
return 'thing'

测试:

1
2
3
4
5
6
7
8
9
>>> from mock import patch
>>> from example import TheClass
>>> @patch.object(TheClass, 'bar', return_value='Foo')
... def test(mock_bar):
... the_class = TheClass()
... assert the_class.foo() == 'Foo'
... mock_bar.assert_called_once()
...
>>> test()

Mock 局部导入的模块

代码:

1
2
3
4
5
6
7
8
9
10
# example.py

def foo():
from module import constant
return constant


def bar():
import module
return module.constant

测试:

1
2
3
4
5
6
7
8
>>> from mock import MagicMock, patch
>>> from example import foo, bar
>>> @patch.dict('sys.modules', module=MagicMock(constant='Foo'))
... def test():
... assert foo() == 'Foo'
... assert bar() == 'Foo'
...
>>> test()

Mock 具有特殊属性的对象

通常在 Mock 一个简单对象的时候,我们会使用 类 Mock(或者它的 子类 MagicMock)。但是 类 Mock 本身带有一些 可选参数,如果待 Mock 对象恰好具有一个属性,该属性与某个可选参数同名,我们在此定义这样的属性为 特殊属性

具有上述 特殊属性 的对象,无法通过 类 Mock 直接创建出来。例如:

1
2
3
4
5
6
>>> from mock import Mock
>>> m = Mock(name='foo', value='bar')
>>> m.name
<Mock name='foo.name' id='4554622624'>
>>> m.value
'bar'

可以看出,name 符合上述对 特殊属性 的定义,创建对象 m 时:

  1. name 并没有被当做 m 的属性,因此 m.name 的值并不是预期的 foo
  2. value 被当做了 m 的属性,因此 m.value 的值是预期的 bar

想要 name 被当做 m 的属性,有两种方式:

  1. 直接赋值覆盖

    1
    2
    3
    4
    5
    6
    7
    >>> from mock import Mock
    >>> m = Mock(value='bar')
    >>> m.name = 'foo'
    >>> m.name
    'foo'
    >>> m.value
    'bar'
  2. 借助 configure_mock 方法(个人更倾向于这种方式)

    1
    2
    3
    4
    5
    6
    >>> m = Mock()
    >>> m.configure_mock(name='foo', value='bar')
    >>> m.name
    'foo'
    >>> m.value
    'bar'

基本概念

Elasticsearch Analyzer 由三部分构成:(零个或多个)character filters、(一个 )tokenizers、(零个或多个)token filters。

Analyzer 主要用于两个地方:

  1. 索引文档时,分析处理「文档字段」analyzed fields
  2. 搜索文档时,分析处理「查询字符串」query strings

Analyzer 中各个部分的工作顺序如下:

(Input) –> [Character Filters] –> [Tokenizers] –> [Token filters] –> (Tokens or Terms)

更多说明参考 AnalysisMapping -> Mapping parameters -> analyzer

另外,Elasticsearch Analyzer 的内部机制 这篇文章总结得也很到位。

内置 Analyzers

ElasticSearch 包括多种内置的 Analyzers。更多说明参考 Analysis -> Analyzers

例如,如果要使用内置的「标准 Analyzer」,则需要指定 typestandard

自定义 Analyzers

ElasticSearch 也支持自定义 Analyzers,这正是其强大之处。自定义 Analyzer 必须指定 typecustom

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ curl -XDELETE 'http://localhost:9200/test'

$ curl -XPUT 'http://localhost:9200/test' -d '
{
"index": {
"analysis": {
"analyzer": {
"my_analyzer": {
"type": "custom",
"tokenizer": "uax_url_email",
"filter": ["lowercase"],
"char_filter": ["html_strip"]
}
}
}
}
}'

$ curl -XGET 'http://localhost:9200/test/_analyze' -d '
{
"analyzer": "my_analyzer",
"text": "this is a test url <b>http://www.example.com</b>"
}'

如果只是想测试 Analyzer 是否工作,也可以不用指定索引。更多说明参考 Indices APIs -> Analyze

例如上述 Analyzer 可以这样测试:

1
2
3
4
5
6
7
$ curl -XGET 'http://localhost:9200/_analyze' -d '
{
"tokenizer": "uax_url_email",
"filter": ["lowercase"],
"char_filter": ["html_strip"],
"text": "this is a test url <b>http://www.example.com</b>"
}'

原文作者:Emily Esfahani Smith

原文网址:http://www.theatlantic.com/health/archive/2013/01/theres-more-to-life-than-being-happy/266805/

译者:RussellLuo

生活的意义,来自对高于幸福的事物的追求。

题图

恰恰是对幸福的追求本身阻碍了幸福。

1942 年 9 月,维克多·弗兰克,维也纳杰出的犹太族精神病学和神经病学专家,连同他的妻子和父母,一起被逮捕并遣送到了纳粹集中营。三年后,当他所在的集中营被解放的时候,他的亲人,包括他怀孕的妻子,都已经死去。但囚犯编号为 119104 的他,却活了下来。他 1946 年的畅销书 《人类对意义的追寻》,是一本他用 9 天时间完成的、关于他的集中营经历的书。在这本书中,弗兰克总结到,活着的人跟死去的人,本质的区别只有一个:意义,一种对生命的洞察力。在他还是一名 高中生 的时候,他的一个科学老师跟全班同学说:“生命就是一个燃烧的过程,一个氧化的过程,其他的什么都不是。”话音刚落,弗兰克就从椅子上跳了起来,问到:“老师,如果真是这样,那生命的意义是什么呢?”

就像他在集中营里所看到的,那些找到了生命意义的人,即使在最可怕的环境中,也远远比那些没有找到的人更能承受痛苦。弗兰克在《人类对意义的追寻》中写到:“一个人拥有的一切都可以被剥夺,但唯独有一样东西除外,那就是他最后的一点自由——在任何给定的环境中,他选择自己的态度和面对方式的自由。”

在集中营里,弗兰克的职务是治疗师。在他的书中,他给出了一个例子,那是他遇到的两个有自杀倾向的囚犯。像集中营里的其他人一样,这两个人很绝望,他们觉得生无可恋。弗兰克写到:“对这两个病例(的治疗),是一个让他们意识到生活仍然有盼头的问题,在现在或将来的生活中,仍然有一些东西对他们满怀期待。”对于其中一个人来说,那是他当时身居国外的年幼的小孩;对另一个人而言(他是一名科学家),那是他尚未完成的系列丛书。弗兰克在书中写到:

个体的唯一性和独特性,既区分了不同的个体,也赋予了一个个体存在的意义。这种唯一性和独特性,既与人类的爱有关,也与创造性的工作有关。当一个人意识到自己的不可替代性以后,他会产生持续的、强大的责任感。当他知道自己还承担着对一个深情等待着他的人、或者一项未完成的工作的责任后,他是绝不会放弃自己的生命的。他知道自己“为什么”而活,因此无论“怎样”(的境遇),他都能够承受。

维克多·弗兰克

在 1991 年,国会图书馆和每月读者俱乐部把《人类对意义的追寻》列为美国 最有影响力的十本书之一。这本书在全世界范围内卖出了数百万本。如今,二十多年过去了,这本书的精神——它对“意义”、“苦难的价值”和“对高于自我的事物的责任感”的强调——在大家更愿意追求个人幸福而不是寻找意义的今天,看起来与我们的文化格格不入。弗兰克写到:“在欧洲人看来,美国文化有一个特性,那就是人们一遍又一遍地被指挥和命令要求‘感到幸福’。但幸福是不能被追求到的;它必须是随之而来的。一个人必须要有一个‘感到幸福’的理由。”

根据盖洛普的民意调查,美国人的幸福水平创下了四年以来的新高——这个数字看起来跟标题中带有“幸福”字样的畅销书的数量一样多。在这篇文章中,盖洛普 同时报告称如今有接近 60% 的美国人都感到幸福,他们没有太多的压力和忧虑。另一方面,根据 美国疾病控制中心 的说法,10 个美国人当中有 4 个人还没有找到满意的生活目标。这 40% 的人,要么认为自己的生活没有清晰的目标,要么在自己的生活是否有目标这个问题上持中立态度。接近四分之一的美国人不知道,或者没有强烈意识到可以让他们的生活有意义的事物。调查显示,在生活中找到目标和意义,可以提高一个人的总体幸福感和生活满意度,改善他的身心健康,增强他的弹性和自尊,并减小他抑郁的可能性。除此以外,最近的研究 表明,一根筋地去追求幸福,往往会适得其反地让人感到不幸福。弗兰克懂得:“恰恰是对幸福的追求本身阻碍了幸福。”


这就是一些研究者反对纯粹追求幸福的原因。在今年即将发表在 《积极心理学期刊》 上的一份 最新研究 中,心理学家询问了接近 400 个年龄从 18 岁到 78 岁的美国人,问他们是否认为自己的生活过得有意义和/或幸福。通过测试这些人对意义、幸福,还有压力水平、开支模式和生小孩等其他因素的态度,研究者们发现,有意义的生活和幸福的生活之间在某种程度上是有重叠的,但它们终究还是不同。心理学家发现:喜欢过幸福的生活的人更倾向于做一个“索取者”,而愿意过有意义的生活的人更接近于是一个“给予者”。

研究者们写到:“缺乏意义的幸福,描绘了一种相对肤浅的、自我的,甚至是自私的生活。这种生活崇尚一切顺利,需求和欲望都能得到轻易地满足,却逃避困难和麻烦。”

那么幸福的生活和有意义的生活之间究竟有怎样的区别呢?幸福,与感觉良好有关。具体而言,研究者们发现,感到幸福的人常常会觉得他们的生活很轻松,自己身体健康,并且有能力购买自己想要的东西。如果没有足够的金钱会降低你对自己的生活的幸福感,那说明它对你的幸福程度有着巨大的影响。幸福的生活通常也没有压力和担忧。

(未完待续…)

0%