RussellLuo

让思想在文字间徜徉


  • 首页

  • 归档

  • 标签

图解 Go 新增的并发安全的字典 sync.Map

发表于 2017-06-03 | 分类于 技术 |

sync.Map 是并发安全的字典,于 2017 年 4 月 27 日合并到 Go 代码仓库的主分支:

Map is a concurrent map with amortized-constant-time loads, stores, and deletes.
It is safe for multiple goroutines to call a Map’s methods concurrently.

以下是对 sync.Map 的 Load/Store/Delete 等常用操作的图解:

go-sync-map

Elasticsearch 基于时间的索引

发表于 2017-05-16 | 分类于 技术 |

应用场景

对于数据量较大的业务功能(比如日志),如果使用单个 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

epoll 与 CLOSE_WAIT 连接

发表于 2016-11-18 | 分类于 技术 |

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

一、问题背景

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

内部实现上,该数据导出功能是通过 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'

Python Mocking 技巧

发表于 2016-07-15 | 分类于 技术 |

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 浅析

发表于 2016-07-10 | 分类于 技术 |

基本概念

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

Analyzer 主要用于两个地方:

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

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

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

更多说明参考 Analysis 和 Mapping -> Mapping parameters -> analyzer。

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

内置 Analyzers

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

例如,如果要使用内置的「标准 Analyzer」,则需要指定 type 为 standard。

自定义 Analyzers

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

例如:

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>"
}'

生活,不止于幸福

发表于 2015-09-30 | 分类于 翻译 |

原文作者: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 岁的美国人,问他们是否认为自己的生活过得有意义和/或幸福。通过测试这些人对意义、幸福,还有压力水平、开支模式和生小孩等其他因素的态度,研究者们发现,有意义的生活和幸福的生活之间在某种程度上是有重叠的,但它们终究还是不同。心理学家发现:喜欢过幸福的生活的人更倾向于做一个“索取者”,而愿意过有意义的生活的人更接近于是一个“给予者”。

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

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

(未完待续…)

使用 Hexo 搭建博客

发表于 2015-08-30 |

一、购买 VPS

我使用的是 DigitalOcean。购买合适的套餐后,你会得到一个预装了指定操作系统的虚拟服务器,以及一个虚拟服务器 IP(VPS-IP)。

二、安装软件

登录 VPS,安装一些依赖软件。

Node.js

  1. 安装 Node.js

    这里安装 Node.js v0.12,其中自带了 NPM。参考 nodesource/distributions。

    1
    2
    $ curl -sL https://deb.nodesource.com/setup_0.12 | sudo -E bash -
    $ sudo apt-get install -y nodejs
  2. 配置 NPM

    设置 NPM 的全局安装目录为用户主目录。参考 Install NPM into home directory with distribution nodejs package (Ubuntu)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # Create .npmrc
    NPM_PACKAAGES="$HOME/.npm-packages"
    mkdir -p "$NPM_PACKAAGES"

    echo "prefix = $NPM_PACKAAGES" >> $HOME/.npmrc

    # Add configurations into .bashrc (or .zshrc)
    echo '
    # NPM packages in homedir
    NPM_PACKAGES="$HOME/.npm-packages"

    # Tell our environment about user-installed node tools
    PATH="$NPM_PACKAGES/bin:$PATH"
    # Unset manpath so we can inherit from /etc/manpath via the `manpath` command
    unset MANPATH # delete if you already modified MANPATH elsewhere in your configuration
    MANPATH="$NPM_PACKAGES/share/man:$(manpath)"

    # Tell Node about these packages
    NODE_PATH="$NPM_PACKAGES/lib/node_modules:$NODE_PATH"
    ' >> $HOME/.bashrc

Git

1
2
$ sudo apt-get update
$ sudo apt-get -y install git

Supervisor

1
$ sudo apt-get install -y supervisor

Nginx

1
$ sudo apt-get -y install nginx

三、使用 Hexo

安装 Hexo

1
$ npm install -g hexo

初始化 Hexo

1
2
3
4
$ mkdir hexo && cd hexo
$ hexo init blog
$ cd blog
$ npm install

配置 Hexo

Hexo 有两份主要的配置文件(_config.yml),一份位于站点根目录下,另一份位于主题目录下。为了描述方便,在以下说明中,将前者称为 站点配置文件,后者称为 主题配置文件。

站点信息

编辑 站点配置文件:

1
2
3
4
5
6
title: RussellLuo
subtitle: 让思想在文字间徜徉
description:
author: RussellLuo
language: zh-Hans
timezone: Asia/Chongqing

绑定博客仓库

考虑到独立性和可维护性,我的博客文章都是放在 GitHub 的 blog 仓库 里的。

为了保证所有文章能被 Hexo 正确地识别和处理,blog 仓库会被克隆并绑定到 Hexo 中 post layout 所在的默认路径 source/_posts:

  1. 克隆 blog 仓库

    先从 GitHub 克隆 blog 仓库:

    1
    2
    $ mkdir github && cd github
    $ git clone https://github.com/RussellLuo/blog.git
  2. 绑定前的目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /home/user/
    hexo/ # Hexo 站点
    blog/
    source/
    _posts/
    github/ # GitHub 仓库
    blog/
    blog/
    2015/
    2015-08-30-hello.md
    ...
    ...
  3. 绑定 blog 仓库

    这里采用软链接的方式实现绑定,简单直接。

    1
    2
    $ cd hexo/blog/source/_posts
    $ ln -s ~/github/blog/blog blog
  4. 绑定后的目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /home/user/
    hexo/ # Hexo 站点
    blog/
    source/
    _posts/
    blog/
    2015/
    2015-08-30-hello.md
    ...
    ...
    github/ # GitHub 仓库
    blog/
    blog/
    2015/
    2015-08-30-hello.md
    ...
    ...

文章链接

基于上述绑定后的文章目录结构,我希望文章的链接格式形如 /yyyy/mm/title.html。以文章 blog/2015/2015-08-30-hello.md 为例,我希望它对应的链接是 /2015/08/hello.html。

为了到达上述效果,需要编辑 站点配置文件 如下:

1
2
3
root: /
permalink: :year/:month/:title.html
new_post_name: blog/:year/:year-:month-:day-:title.md

配置主题

我个人喜欢简洁优雅的风格,因此选择了 NexT 主题。

1. 下载 NexT 主题

1
2
$ cd blog
$ git clone https://github.com/iissnan/hexo-theme-next themes/next

2. 启用 NexT 主题

编辑 站点配置文件:

1
theme: next

3. 设置主题参数

启用 NexT 主题中的 Mist 主题:编辑 主题配置文件,将 #scheme: Mist 前面的 # 注释去掉。

4. 侧边栏

  1. 头像

    编辑 站点配置文件,新增字段 avatar,将值设置成头像的链接地址。

  2. 社交链接

    编辑 站点配置文件,新增字段 social,然后添加社交站点名称与地址即可。例如:

    1
    2
    3
    social:
    GitHub: https://github.com/RussellLuo
    豆瓣: http://douban.com/people/RussellLuo

5. 评论系统

虽然 Disqus 是国外最流行的第三方评论系统,但是在国内感觉使用 多说 更接地气。

NexT 主题内置支持 多说 评论系统,因此配置很简单:

  1. 创建站点

    登录 多说 后,在首页点击“我要安装”,创建站点,填写必要信息。

    其中,多说域名 一栏就是你的 duoshuo_shortname。

  2. 配置 duoshuo_shortname

    编辑 站点配置文件,新增 duoshuo_shortname:

    1
    duoshuo_shortname: russellluo

更新于 2017 年 5 月 18 日
多说在 2017 年 3 月 21 日宣布:将于 2017 年 6 月 1 日正式关停服务。Disqus 会被墙,畅言要备案,最后发现了 网易云跟帖。

切换到网易云跟帖的步骤:

  1. 注册网易云跟帖的账号
  2. 按提示填写相关信息(特别注意 “站点网址” 需要跟博客网址相同,比如 http://russellluo.com)
  3. 升级到最新版本的 NexT
  4. 编辑 站点配置文件,注释掉 duoshuo_shortname
  5. 编辑 主题配置文件,设置 gentie_productKey

更多详情,可以参考 这篇博客。

更新于 2017 年 9 月 26 日
继多说关闭之后,网易云跟帖在 2017 年 8 月 1 日也停止了服务:( 不打算折腾了,直接切换到 Disqus(墙内用户需要自备梯子)。

切换到 Disqus 的步骤:

  1. 注册 Disqus 的账号并登录
  2. 依次点击 “GET STARTED”、“I want to install Disqus on my site”
  3. 按提示填写站点信息(注意 “Website Name” 的内容就是 shortname,稍后会用到,比如 russellluo)
  4. 按提示 “1. Select Plan”(没钱就选择 Basic)、“2. Install Disqus”(NexT 主题的可以忽略)、“3. Configure Disqus”(填写 “Website URL” 后,点击 “Complete Setup” 即可)
  5. 编辑 主题配置文件,启用 Disqus:

    1
    2
    3
    4
    disqus:
    enable: true
    shortname: russellluo
    count: true

更多详情,可以参考 这篇博客。

四、正式部署

1. 使用 Supervisor 管理 Hexo 服务

使用 hexo server 启动的 Hexo 服务是非 Daemon 模式的。为了便于管理,这里使用 Supervisor。

创建 Supervisor 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ vi /etc/supervisor/conf.d/blog.conf

[program:blog]
command=/home/user/.npm-packages/bin/hexo server
directory=/home/user/hexo/blog
autostart=true
autorestart=true
startsecs=5
stopsignal=HUP
stopasgroup=true
stopwaitsecs=5
stdout_logfile_maxbytes=20MB
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stderr_logfile_maxbytes=20MB
stderr_logfile=/var/log/supervisor/%(program_name)s.log

启动 Supervisor 守护进程:

1
$ supervisord

查看 blog 程序(即 Hexo 服务)的状态:

1
2
$ supervisorctl status
blog RUNNING pid 28974, uptime 0:00:32

可以看出,blog 程序已经处于运行状态,监听端口为 hexo server 命令的默认端口 4000。在浏览器中访问 http://<VPS-IP>:4000 可以看到博客的运行效果。

2. 配置 Nginx 代理

作为一个对外公开的网站,使用 4000 端口显然是不合适的。可以直接改成 80 端口,但是这样直接把 Hexo 服务暴露给用户,并不恰当。更好的办法是使用 Nginx 做代理。

创建 Nginx 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ vi /etc/nginx/conf.d/blog.conf

server {
listen 80;
server_name <VPS-IP>;

location / {
proxy_pass http://localhost:4000;
}

access_log /var/log/nginx/blog.access.log;
error_log /var/log/nginx/blog.error.log;
}

重启 Nginx:

1
2
$ nginx -t
$ nginx -s reload

此时,在浏览器中访问 http://<VPS-IP>,就可以体验到高效、稳定的博客网站。

五、购买域名

让用户通过 http://<VPS-IP> 访问博客,显然是反 Web 的行为。作为一名专业的博主,我在 GoDaddy 购买了自己博客的域名。

启用了高大上的域名后,需要修改上述的 Nginx 配置,将 server_name 从 IP 改成域名:

1
2
3
4
5
6
7
$ vi /etc/nginx/conf.d/blog.conf

server {
...
server_name russellluo.com;
...
}

六、设置 DNS

对外开放的最后一步,是设置 DNS,让域名 russellluo.com 真正解析到我的虚拟服务器 IP。我用 DNSPod,快速、免费、稳定!

That’s all! 如果你在我的博客上看到了这篇文章,说明我已经成功了。

12
RussellLuo

RussellLuo

17 日志
2 分类
16 标签
GitHub 豆瓣
© 2025 RussellLuo
由 Hexo 强力驱动
主题 - NexT.Mist
访问人数 总访问量 次