月度归档:2014年11月

Erlang运行时之Message Passing

本文试图分析Erlang消息传递机制,总结优缺点,大部分资料来源于论文『Characterizing the Scalability of Erlang VM on Many-core Processors』和stackoverflow,并且带有自己的理解,不当之处需多包涵。

Erlang进程间通信采用的策略是消息传递(Message Passing)。这里的传递是指传递消息副本,而不是传递消息指针或者引用。小消息的复制成本可以忽略,现在的DDR3 SDRAM内存带宽约为2000MT/s,64位数据总线下有近16GB/s的数据传输速度。从VM的角度看,消息传递的本质就是调度器将消息从发送者进程的堆中复制到接收者进程的堆中。基于复制的消息传递有一个好处,可以非常方便实现以进程为单位的GC,因为GC范围是进程私有堆,不必考虑堆中的数据是否被其他进程引用。但是并不是所有消息都是通过复制实现消息传递,大于64K的二进制数据是通过共享内存实现的,前文已有提及。消息太大时,进程GC频繁触发,并且大消息复制成本也无法忽略,这两点带来的性能损耗可能比共享对象引用计数造成的性能损失还要大,因此,Erlang权衡之下对二进制数据采用了不同的消息传递策略。

在多核时代,不同的Erlang进程被不同的调度器执行。当接收进程正被其他调度器执行,而它的堆空间不够或者正有其他消息往堆中发送消息时,发送进程会分配一块临时的Heap Fragment(参考『Erlang运行时之进程』中的图1),用于存储待发送消息的副本,这块Heap Fragment数据会在GC期间被合入接收进程的堆空间。发送进程把消息复制完毕后,生成一个该消息的元数据结构(data management structure),该结构体包含了指向消息副本(处在接收进程堆或者Heap Fragment里)的指针,最后将元数据结构体放入接受进程的消息队列(mailbox)里。如果此时接收进程处于suspended状态,那么它会被激活,处理该消息;如果本来就是运行状态,那么消息会被等待取走进行匹配。

接收进程的mailbox在逻辑上是一个消息队列,但在物理上是通过两个物理队列实现,分别为public queue和private queue。前者对应PCB中的ErlMessageInQueue,只有在支持SMP的erts才有,用于接收其他进程发送来的消息,发送进程操作此队列时需要加锁互斥访问,避免不同进程操作同一块内存;后者对应PCB中的ErlMessageQueue,是接收进程取消息的队列,这样接收进程就可以不必关心锁带来的额外开销了,否则接收进程必须通过获取锁才能安全地读取完整的消息数据。当private queue为空时,public queue的消息会被追加到private queue里。由此可见,mailbox(含public queue和private queue)本身处在PCB中,而不是在堆中。ErlMessageInQueue和ErlMessageQueue的定义(erlang/erts/emulator/beam/erl_message.h)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
    ErlMessage* first;
    ErlMessage** last;  /* point to the last next pointer */
    ErlMessage** save;
    int len;            /* queue length */
} ErlMessageQueue;
 
#ifdef ERTS_SMP
typedef struct {
    ErlMessage* first;
    ErlMessage** last;  /* point to the last next pointer */
    int len;            /* queue length */
} ErlMessageInQueue;
#endif

其中,ErlMessageQueue结构体有个save属性,这个save链表就是进程selective recieve时匹配失败后将消息暂存的save queue。

在支持SMP的Erlang虚拟机里,消息传递机制会引入一些额外消耗,这些消耗会在一定程度上影响到CPU核数在erts上的可扩展性:
1. 上文已有提及,分属不同调度器的发送进程向同一个接收进程发送消息时,元数据结构入public queue需要锁去同步。
2. 发送进程要从OS进程的堆空间分配内存供存储消息(或者Heap Fraigment)和元数据使用,这个分配内存的操作需要加锁。同理,内存释放操作也同样需要加锁。
3. 当大量进程并行执行时,他们的消息收发顺序相对于单核模式下是乱序的,虽然不影响最终接收进程的消息匹配,但是会带来损耗,原因是进程的消息接收采用selective receive机制,有些消息会被匹配多次。

Erlang的消息传递机制基于复制。我觉得,通过传递消息副本来避免锁不是Erlang高效的直接原因,因为有变量不变性(immutable variable)保障,即使传递引用也可以避免不必要的锁开销,况且,在支持SMP的运行时环境下,锁作为同步手段也经常使用。但是基于复制带来的好处是方便实现以进程为粒度的GC机制,从而在间接上提升了Erlang的并发性。

--EOF--

Erlang运行时之进程

本文试图从进程角度解释Erlang之所以高效的原因,大部分资料来源于论文『Characterizing the Scalability of Erlang VM on Many-core Processors』,并且带有自己的理解,不当之处请多包涵。

Erlang作为一门面向并发的语言(Concurrent Oriented Programming, COP),进程扮演着重要的作用,可以说Erlang就是一门面向进程的语言。归根到底,Erlang的核心概念无非就是进程、模式匹配、消息传递三大法宝。

目前主流的Erlang虚拟机是BEAM(Bogdan/Bjrn’s Erlang Abstract Machine),早期的JAM, old BEAM现都已经废弃不用。Erlang虚拟机是运行在操作系统中的一个多线程进程。Linux下,用POSIX线程库(pthread)实现,多线程共享进程(VM)的内存空间。一般来说,Erlang虚拟机会为每个CPU核分配两个线程,一个负责IO,一个作为调度器负责调度Erlang进程。

Erlang进程是虚拟机级别的进程,它非常轻量,初始化时只有2K左右,Erlang官方文档有给出测试初始进程占用内存大小的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Eshell V6.1  (abort with ^G)
1> Fun = fun() -> receive after infinity -> ok end end.
#Fun<erl_eval.20.90072148>
2> Pid = spawn(Fun).
<0.35.0>
3> {_,Bytes} = process_info(Pid, memory).
{memory,2680}
4> Bytes div erlang:system_info(wordsize).
335
5> erlang:process_info(Pid).
 ......
 {total_heap_size,233},
 {heap_size,233},
 {stack_size,9},
 ......

可以看到,一个进程包含堆栈在内只需2680B内存,其中堆(含栈)大小为233个字,64位系统下一个字等于8个字节,堆栈占用1864B。实际上,如果只计算PCB,大约只占300B,相比Linux PCB的1K也轻量不小。另外,Joe Armstrong在『Progamming Erlang』中也有过示范,物理内存充足的情况下,spawn一个进程只需花费微秒数量级的时间。因此,Erlang系统中允许同时存在成千上万的进程。

Erlang Process
图1

图1是Erlang进程的内部组成,每个进程都由独立的进程控制块(PCB, process control block)、栈和私有堆三部分组成。PCB包含的信息有进程ID、堆栈起始地址、mailbox、程序寄存器(PC)、参数寄存器等等,完整定义可以参考Erlang运行时(erts)源代码头文件erlang/erts/emulator/beam/erl_process.h中的process结构体,以下是几个主要字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct process {
    Eterm* htop;    /* Heap top */
    Eterm* stop;    /* Stack top */
    Eterm* heap;    /* Heap start */
    Eterm* hend;    /* Heap end */
    Uint heap_sz;   /* Size of heap in words */
    Uint min_heap_size; /* Minimum size of heap (in words). */
    Eterm* i;       /* Program counter for threaded code. */
    Uint32 status;  /* process STATE */
    Eterm id;       /* The pid of this process */
    Uint reds;      /* No of reductions for this process  */
    Process *next;  /* Pointer to next process in run queue */
    Process *prev;  /* Pointer to prev process in run queue */
    ErlMessageQueue msg;/* Message queue */
#ifdef ERTS_SMP
    ErlMessageInQueue msg_inq;
#endif
    ......
};

进程堆和栈共同占用一块连续的内存空间,堆空间由低地址向高地址增长,栈空间由高地址向低地址增长,当堆顶和栈顶一样时,可以判定堆栈空间已满,需要通过垃圾回收空间和或者增长空间。在Erlang进程看来,这块堆栈内存是独占的,进程间彼此隔离;在操作系统进程看来,所有Erlang进程的堆栈空间都在自己的堆空间里。Erlang进程里堆空间和栈空间存放数据类型有所区分,前者主要是一些复合数据,比如元组、列表和大数等,后者主要存放一些简单数据类型以及堆中复合数据的引用。

图2是以列表和元组为例展示了Erlang进程中堆栈的内存布局:

Heap Layout
图2

Erlang是动态类型语言,变量类型需要到运行时才能确定,因此,堆栈中每个数据都有一个Type标签表示其类型。元组在堆中是以Array的形式存储,有字段表示元组大小,并且在栈中有一个指针(引用)指向这块堆空间,因为是连续空间,要取出元组中的数据只需O(1)的复杂度计算内存偏移量即可。对于列表来说,列表元素在堆中是以链表形式存在的,由栈中的一个指针指向列表的第一个元素。相邻列表元素在内存中并不连续,也没有字段表示列表大小,因此要获取列表大小只能通过遍历,这个操作是个O(N)的时间复杂度。对于lists:append(ListB, ListA)这个操作,Erlang做的事情是先复制ListB,遍历ListB,找到列表尾部元素,将下一元素指针从NIL改为ListA第一个元素地址。由此可知,要提高性能,最好是将较长的列表追加到较短的列表上,以减少遍历时间。另外,也不要试图在列表尾部追加元素,原因同上,之前的一篇『Erlang列表操作性能分析』对此已做过分析。如果将ListC当做消息内容发送给其他进程,则整个ListC列表都会复制一份,即使往同一个节点发送多次,复制也会进行多次,这往往会导致消息接收进程占用的内存空间比发送进程大,因为对接收进程来说,每次接到的ListC会被当成不同的数据。

从图2还可以看出,ListA和ListC共享了部分数据,也就说,在一个Erlang进程内部,是存在内存共享的情况的。在Erlang里,变量拥有不变性,一次赋值(模式匹配)成功,它就不会再变,因此,ListA和ListC可以永远安全地共享这些数据。当然,Erlang中的内存共享在其他场景下也会出现,在图1中所示的有两块内存共享区域,一块是二进制数据共享区,用于存储大于64K的二进制数据;另一块是存储ETS表用的,ETS可以供每个进程访问,相比真正的共享内存有一些不同,它基于消息复制以记录为单位进行存取;相比数据库,它弱了很多,不支持事务机制。

当进程堆栈空间满时,会触发调度器对进程进行GC,如果GC结束堆栈空间仍然不足,则会分配新空间。Erlang的GC是以进程为单位,对某个进程GC不会影响其他进程的执行,虽然对单个进程来说,存在stop the world的现象,但是从全局来看,其他进程不会受影响,这个特性使得Erlang能够应付大规模高并发的业务场景,基于Elrang的业务系统可以达到软实时的级别。另外,一旦进程生命周期结束,GC可以非常方便地直接回收这个进程占用的所有内存。

综上,从进程的角度的来看,使得Erlang高效主要是以下方面:
1. 进程本身轻量。所以线程池的概念在Erlang语言层面根本不存在。
2. 变量不变性避免了很多无谓的数据复制。如List操作,直接通过修改指针实现append操作。
3. 以进程为单位进行GC。

References:
[1] Characterizing the Scalability of Erlang VM on Many-core Processors
[2] Erlang does have shared memory
[3] Erlang vs Java memory architecture
[4] Erlang - Programming the Parallel World

--EOF--

Linux OOM Killer问题

上周有一个线上程序挂了,由于早就知道服务器存在内存不足的情况,因此大概猜测问题应该出在Linux的OOM Killer上,grep了一把syslog,找到真凶,证实了我的猜测:

1
2
3
4
5
6
7
8
/var/log/messages:Nov 10 09:41:53 kernel: [8358409.054]java invoked oom-killer:
gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
/var/log/messages:Nov 10 09:41:53 kernel: [8358409.054] [<ffffffff810b799b>] ?
oom_kill_process+0x49/0x271
/var/log/messages.1:Nov  9 06:29:19 kernel: [8260455.112]ntpd invoked oom-killer:
gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
/var/log/messages.1:Nov  9 06:29:19 kernel: [8260455.112] [<ffffffff810b799b>] ?
oom_kill_process+0x49/0x271

Linux的内存管理存在超售现象,也就是Overcommit策略。举个栗子,当我们用C语言的malloc分配了内存之后,只要指定的内存大小不超过进程的虚拟内存大小,一般都会返回成功,只有程序在使用这块内存时,操作系统才会真正分配给它,比如调用memset()函数填充内存时。在32位操作系统下,进程的虚拟内存是4G,低3G为用户空间,高1G为内核空间,所以程序一次性可以malloc 3G以下的内存而不报错,即使当前的物理内存远远小于这个值。这样必然出现一种情况,当系统中所有进程占用的实际内存大于物理内存和swap分区之和时,Linux需要选择一个策略来处理这个问题,策略的选择就是修改Overcommit参数。

当Overcommit策略启用时,并且此时物理内存已经耗尽而又有程序需要分配内存时,Linux就会触发OOM Killer,它可以简单理解为操作系统选择一些进程杀掉,腾出内存空间。选择被杀进程的算法还是蛮讲究的,既要考虑进程重要性,比如是否root执行、是否系统进程、nice值等,又要兼顾回收效益,比如最好应该通过杀掉最少的进程来回收最多的内存。这方面,不同的内核版本也会有所区别,大概思路如2.4版源码注释所说:

1
2
3
4
5
6
7
1) we lose the minimum amount of work done
2) we recover a large amount of memory
3) we don't kill anything innocent of eating tons of memory
4) we want to kill the minimum amount of processes (one)
5) we try to kill the process the user expects us to kill, this
   algorithm has been meticulously tuned to meet the priniciple
   of least surprise ... (be careful when you change it)

注: 源码路径linux/mm/oom_kill.c,函数调用链为out_of_memory() -> oom_kill() -> select_bad_process() -> badness(),上述注释来自badness()函数。

选择被杀进程的难度,就好比社会学意义上判定一个人死刑一样,无论这个人有多少个该死的理由,但是总能找出一些这个人的存在价值。对应到应用程序,某个应用程序可能吃内存最多,但是同时它对某些业务来说最不可或缺,比如RabbitMQ,Redis,MySQL等等。于是Linux为每个进程都提供了一个可以免死的机会,就是/proc/<pid>/oom_score_adj参数。

每个进程都有一个oom_score值(位于/proc/<pid>/oom_score,总是大于等于0),这个值越大就越容易被OOM Killer杀掉,同时/proc/<pid>/oom_score_adj值(取值范围-1000~1000)可以对oom_score值进行调整,比如oom_score_adj值为-10,则会将最终的oom_score值减10。

做个实验吧,实验环境为:VPS,单核,RAM 128M, swap 256M, 32位debian6,kernel 2.6.32。

1
2
3
4
5
6
7
8
# vi oomscore.sh
#!/bin/bash
for proc in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+'); do
    printf "%2d %5d %s\n" \
        "$(cat $proc/oom_score)" \
        "$(basename $proc)" \
        "$(cat $proc/cmdline | tr '\0' ' ' | head -c 50)"
done 2>/dev/null | sort -nr | head -n 10

这个oomscore.sh脚本可以显示当前oom_score排名前10的进程oom_score,pid和进程名等,借鉴来辅助观察。

首先执行oomscore.sh脚本,得到当前omm_score排名前10的进程,以及内存使用情况:

1
2
3
4
5
6
7
8
9
10
# sh oomscore.sh 
67 17976 /usr/sbin/apache2 -k start 
40   695 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib
 7 18616 sshd: root@pts/6  
.......
# free -m
             total       used       free     shared    buffers     cached
Mem:           128         75         52          0          0         26
-/+ buffers/cache:         49         78
Swap:          256         15        240

然后跑以下测试程序alloc1mps.c,每秒分配1M内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
 
#define M 1024*1024
int main(){
        int count = 1;
        while(1){
                char* mem = (char*)malloc(sizeof(char) * M);
                if(!mem){
                        break;
                }
                memset(mem, 0, M);
                printf("alloc %d M\n", count++);
                sleep(1);
        }
        printf("mem exhausted!\n");
        return 0;
}

通过sh omm_score.sh,top,free -m,dmesg等命令观察内存分配和omm_score关系,可以得到以下现象和结论:
1. 首先从分配物理内存中分配空间,等物理内存分配得差不多了,-/+ buffers/cache free趋于0时,开始从swap分配内存。
2. 当swap分区内存接近耗尽时,此时alloc1mps进程的omm_score值非常大。
3. 内存(物理内存+swap)耗尽,触发omm killer回收内存,alloc1mps进程被杀,dmesg证实这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
# free -m
             total       used       free     shared    buffers     cached
Mem:           128        124          3          0          0          0
-/+ buffers/cache:        123          4
Swap:          256        249          6
# sh oom.sh 
791 19412 ./alloc1mps 
56 17976 /usr/sbin/apache2 -k start 
36   695 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib
......
# dmesg
[235669.855]Out of memory in UB 35782: OOM killed process 19412 (alloc1mps)
score 0 vm:318156kB, rss:119476kB, swap:196868kB

重跑测试程序,当alloc1mps开始运行后,调整其omm_score_adj参数,设为-800。

1
2
3
4
# Pid=` ps axu | grep alloc1mps | grep -v 'grep' | awk '{print $2}'`
# echo -800 > /proc/${Pid}/oom_score_adj
# cat  /proc/${Pid}/oom_score_adj
-800

继续观察,这次发现内存接近耗尽时,受oom_score_adj影响,alloc1mps进程的oom_score值低于apache2和mysqld进程,因此首先被OOM Killer杀掉的是apache2进程。apache2进程被kill后,alloc1mps继续执行,它的oom_score值也慢慢涨上来了,下一个被kill的就是它了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# free -m
             total       used       free     shared    buffers     cached
Mem:           128        122          5          0          0          0
-/+ buffers/cache:        121          6
Swap:          256        252          3
# sh oom.sh 
36   695 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib
33 21288 ./alloc1mps 
 2 22233 sh oom.sh 
......
# dmesg
[236432.377]Out of memory in UB 35782: OOM killed process 17976 (apache2)
score 0 vm:55760kB, rss:4kB, swap:22284kB
[236455.623]Out of memory in UB 35782: OOM killed process 21288 (alloc1mps)
score 0 vm:339744kB, rss:118540kB, swap:219132kB

了解了OOM Killer机制后,想要规避重要进程被kill的命运,就只能在进程运行时调整其omm_score_adj参数了,这种调整是临时的,当系统重启后会清零。所以,最根本的解决方法还是加内存。

--EOF--

『火影忍者』

『火影忍者』说起来,我也是一个老火影迷了,第一次接触到火影距今已有10多年的时间,那会儿是2003年,高二的教室里。多年来,我也曾有短暂的跟过,只是我是个不喜欢等待的人,慢慢地,也就粉转路人了。因此不能说火影伴随着我成长,那样显得太过矫情了,只不过是我成长的时候恰好火影在连载罢了。上个月看到了集英社发布『火影忍者』即将完结的新闻,这意味着我是时候从头到尾完整看一遍了。

『火影忍者』是否是一部伟大的作品无需评价,能够连载15年的漫画至少是得到大家认可和喜爱的。一千个人看『火影忍者』,相信一千个人都能从中体会到友情和梦想之于人生的意义。我作为读者之一,短时间内看完700话漫画,心中自然会有一些好的与坏的想法。尽管存在个人喜好差异,但我相信这些想法有一定的普适性的。

晓
首先是整部漫画的格局问题,也就是世界观。我必须承认,火影的世界观是引导着我不断往下看的原动力,仅仅前十话,岸本齐史就已经将火影的基本格局布置好了。对于鸣人这条线,出生卑微,体内被四代火影封印着九尾狐,资质平庸,却渴望当上火影。单单这些前提,就能引导出一系列的剧本,鸣人如何成长为火影?有火影,那么自然也有其他忍者村的影?有四代火影,那么还有一二三代火影?体内封印着九尾狐,那么还有其他数量的尾兽?对于佐助这条线,交代了他的宇智波一族同胞都被亲哥哥所杀,并且自己的终极梦想就是亲手杀死哥哥。那么自然会让人想到他哥哥为什么要杀害全族,以及佐助如何复仇等线索。此后,随着故事的发展,逐渐加入了晓组织、轮回眼、六道仙人等元素,这些线索足以构成一个宏大的故事框架,背后有说不完的故事。这也是火影于我最大的魅力。

接着是人物塑造方面,在『火影忍者』第一部,岸本成功地塑造出了十数位个性鲜明的经典人物形象。身为主角的鸣佐自不必说,配角形象也深入人心,比如卡卡西,自来也,纲手,大蛇丸,春野樱,宁次,雏田,小李,我爱罗,鹿丸,等等,甚至一些寥寥数笔带过的人物也令人印象深刻,比如油女志乃,犬牙冢等。正是前期的铺垫太长,导致第二部疾风传开始后,众多人物选择性消失令人感到不适,配角里除了三忍和鹿丸,其余人基本处于打酱油的状态,战场上能看见他们的身影,却发挥不了该有的作用。这也是格局铺得太大,鸣佐成长太快带来的副作用吧。从全局看来,我觉得个人形象塑造得较好的人里,正派有鸣人,佐助,自来也,鼬,我爱罗,小李和鹿丸,反派有佩恩和大蛇丸。

最后是作者的编剧能力,纵观火影全集,从故事主线来说,也就是前文提到的剧本是一流的。反派不断变强,晓被长门利用,长门被带土利用,带土被宇智波斑利用,宇智波斑被大筒木辉夜利用,写轮眼逐级进化,这些都做到了无缝衔接。但在在故事叙述方面,岸本有着头重脚轻的嫌疑,比如对前期的中忍考试和寻回佐助篇章,作者费劲笔墨描绘,到了忍界大战的后期鸣佐大战大筒木辉夜的章节时,却又草草封印了事,这种前后不一致的节奏造成了很多漫迷无比怀念『火影忍者』第一部,也就是截止到佐助出逃木叶为止。在我看来,火影最好看的部分是截止到佩恩死的时候,这之前节奏都还可控。在这之后,世界观瞬间变大,引出了混乱的五影会谈和忍界大战,死过一次的人通过秽土转生忍术重新复活,更加引出了数十年前的宇智波斑等一代人,事先未经铺垫的一干人混战令人双眼疲劳。加上后期反派战斗的模式逐渐单一、反派厌世理由无厘头化等要素,虽还不至于到“烂尾”的程度,但至少后半部的口碑是无法与前半部相匹敌的。另外鸣人频繁的口遁也凸显了岸本作品表达能力的不足,想想被鸣人口遁过的人员列表吧,正派有木叶丸,宁次,我爱罗,纲手,佐助等,反派有桃地再不斩,长门,带土,九尾等,所以说打败鸣人的方式只有一种,就是不要让他开口说话。当作品充斥这大量的雷同的说教词时,会让其本身的质量大打折扣。从更深一点的程度来说,就跟电影一样,当一个作者需要通过人物对话,甚至是演讲来突出主题时,那么作品的表达和反思能力是不足的。这一点对比手冢治虫和宫崎骏的反战作品就分出高低了。

例举完对火影的看法后,接下描述火影带给我印象最深刻的两个人物。

宇智波鼬第一个是宇智波鼬。无论从天赋、实力、智商、大局观和个人魅力上,鼬在火影里面都算得上是最接近完美的一个存在了。他年少成名,天赋异禀,10岁成为上忍,为了全村人利益牺牲自己和整个家族,即便受到包括弟弟在内的所有人误解,也独自忍受,几乎凭借一己之力控制全局。所以,与其说喜欢宇智波鼬,不如说是讨厌自己的无能和脆弱。

第二个是李洛克。这是一个毫无天赋可言的平凡人,没有血统,没有血继限界,没有贵人相助。李洛克在忍者的世界里不会忍术也算是个奇葩了,但他能凭借着努力和信念,专攻体术,在同级生里出类拔萃,站到可以与我爱罗和宁次分庭抗礼的舞台。要说火影里带给我感动最多的人,恐怕就是小李了。这世上大部分人其实跟小李是一样的,没有天赋,唯有努力才能保持与别人在同一水平线上,有时候即使努力过了,也远达不到别人的成就。所以,与其说喜欢小李,不如说是讨厌自己的平凡的资质和在困难面前的退缩。

『火影忍者』结束连载,这是一个里程碑式的结局,这也意味着有些遗憾成了永远。比如卡卡西的真面目,自来也是否还存在于世,等等。这是结束,也是另一个开始。

--EOF--

博客搬迁至DigitalOcean VPS

0x00: 起因

最近想把博客从国内的虚拟主机升级到VPS,原因有3点:
1. 虚拟主机空间有限,100M的空间大小(含数据库)捉襟见肘。
2. 灵活度太小。后期想新增几个博客,如果用VPS的话可以共用空间,降低成本。
3. 网络环境越来越差了,VPN已经成为必需品。

0x01: 选型

决定了购买国外的VPS以后,接下来的就是简单地评测工作。有两家待选:
1. Linode. 老牌VPS提供商,服务和售后服务稳定,其在东京的机房网络对国内用户非常友好。
2. DigtalOcean. VPS界的后起之秀,凭性价比取胜,它提供的VPS更接近云主机。对国内访问来说,有旧金山和新加坡两大机房可选,虽然物理上新加坡更近,但是种种资料表明旧金山的网络更加稳定,更有说法国内到新加坡的流量是经过旧金山的。

分别选取了两家最小规格VPS,Linode东京机房1024型(24G存储,1VCPU,2T流量),DigtalOcean旧金山机房和新加坡机房的512型(20G SSD存储,1VCPU, 1T流量)。用阿里测跑了下全网ping值,Linode稳定在80-100ms,DigtalOcean新加坡机房能保持在150ms以内,旧金山机房则在200-400ms不等。再在本地用ab跑了下Nginx静态页面测试,三个节点性能差不多,但是不知为何SSH到新加坡节点后操作异常卡顿,感觉其ping值名不副实,故首先排除DigtalOcean新加坡节点。

从测试情况来看,Linode VPS胜出,但是我却选择了DigtalOcean旧金山机房节点,因为它便宜了一半,5刀一个月,一年算下来360RMB。不是有句话说吗,选择恐惧症说到底还是因为穷。Linode给出的2TB流量对我这种个站来说完全多余,而且我相信DigtalOcean的SSD能在磁盘读写方面弥补一些网络延时造成的劣势。

0x02: 部署

1. 软件安装

部署采用广泛的LNMP架构(Linux+Nginx+MySQL+PHP),操作系统Debian7, 所有依赖软件安装用apt-get命令。

1
2
3
4
apt-get update
apt-get install nginx mysql-server-5.5
apt-get install php5 php5-mysql php5-cli php5-common
apt-get install php5-fpm php5-cgi

2. 数据导入

把原先博客的数据导出成sql文件,安装phpMyAdmin工具,phpMyAdmin是一个PHP编写的MySQL Web客户端管理软件,去官网下个最新版解压到Nginx vhost根目录下即可,安装成功后用http://ip/phpMyAdmin/index.php访问,根据提示导入sql数据。

3. 程序迁移
把原先博客的WordPress目录打包后解压至Nginx vhost根目录下,修改wp-config.php文件中的数据库用户名和密码,修改Nginx vhost配置后reload,理论上此时博客应该迁移成功了。Nginx的关键配置如下:

1
2
3
4
5
6
7
8
9
10
root /usr/share/nginx/www;
index index.php;
 
server_name fengchj.com *.fengchj.com localhost;
location ~ \.php$ {
  fastcgi_split_path_info ^(.+\.php)(/.+)$;
  fastcgi_pass unix:/var/run/php5-fpm.sock;
  fastcgi_index index.php;
  include fastcgi_params;
}

4. 修改DNS解析
去DNSPod上把域名的CNAME记录改成A记录,指到DigtalOcean VPS,保存后本地测试发现实时生效。

至此,博客迁移完毕。期间配置方面的问题可以查看Nginx日志(/var/log/nginx/access.log和error.log),比如Nginx worker进程跑在www-data用户下,而程序文件可能对其不可读写或执行时会报Permission Deny等等,这类问题通过Google很好解决。部署方面不想折腾的话可以使用LNMP一键安装包一键搞定必需软件的依赖和配置,不过我不喜欢这种定制化的方式,过程不可控。

0x03: 优化
1. 修改MySQL密码为强密码。混淆phpMyAdmin的访问路径,避免猜测攻击。
2. 安装七牛镜像存储WordPress插件。它支持博客静态文件CDN加速,免费的,绝对是WordPress必装插件之一。
3. 安装Disable Google Fonts插件。WordPress的Twenty Twelve主题在管理后台不知道什么原因依赖了Google的Open Sans字体,这在Google被禁的地方非常不友好,每次页面加载都被这个请求阻塞,安装这个插件后后台打开速度会提升不少。
4. 优化php-fpm和MySQL的内存使用策略,避免OOM Killer问题,参考『小内存VPS站点内存使用调优』一文。(2015-04-11更新)
以上就是本次博客迁移的全部过程,历时一天。按照惯例放出我的DigtalOcean邀请链接,点此连接注册可以获取10刀优惠,这可是相当于免费体验两个月的最小规格VPS了。

--EOF--