标签归档:Linux

Tmux应用

Tmux是一个键盘驱动的终端分屏工具,可以替代Linux下的screen。当然,如果是在Mac下使用的话,它的核心功能(例如window,分屏等)也是可以被iTerm2替代的,不过好在Tmux在*nix操作系统中足够通用,用包管理工具(apt-get, brew等)即可安装,所以了解并熟练使用它,还是能为平时终端下的工作节省不少时间的。我从几年前开始使用,但实际上也一直没有形成依赖,因为前面说过了它的可替代性。网上有大堆的所谓Tmux技巧基本上是来自于一本叫『tmux: Productive Mouse-Free Development』的小书,各大网盘也有其电子版下载,抽空通读了一遍,根据示例一步一步个性化Tmux的设置,了解了之前不知道的用法和配置细节。

首先是Tmux的用法简介。

一. session: 会话,Tmux是一个C/S架构的工具,一个会话可以认为是C端和S端一次交互的上下文。我们的所有操作都属于某个session,session可以长时间存在,也可以临时退出再重进。我们可用通过session来区分不同的工作空间,比如本地操作开一个session,远程SSH操作开一个session,又或者SSH生产环境机器开一个session,SSH测试环境机器开一个session。以下是针对session的一些常用操作。

1. open session

1
2
3
$ tmux new-session -s basic
或者
$ tmux new -s basic

-s参数表示session名称,如果不加-s参数,那么Tmux默认会新建一个以数字(下标从0开始)命名的session,并默认打开一个window。打开一个session后,后续的所有控制Tmux本身的快捷键都需要加前缀,默认是Ctrl+b,以下把前缀按键称为Prefix。

2. detach session
想要暂时离开Tmux,回到终端环境时,可以通过快捷键Prefix+d (d for detach)。要注意的时,即使是detach的状态,Tmux中在运行的程序还会继续运行。想要回到Tmux session时,只需执行:

1
$ tmux attach -t basic

-t参数可以指定要attach的session。

3. list session
终端中执行tmux ls (ls for list session)可以列出当前有多少个session。如果已经在session中,执行Prefix+s (s for session)可以列出当前有多少个session,并且可通过上、下键选择要进入的session。

4. kill session
要真正关闭一个session,可以在终端下执行命令tmux kill-session -t basic,其中-t参数表示session名称。

二. window
如果说session是个不可见的东西,那么window就是我们输入、执行命令的地方。一个session可以包含多个window。把window类比成iTerm2中的标签应该就理解了。

1. 创建window
在创建session的时候默认会创建一个以"数字下标+bash"命名的window,并且名称随着bash中执行的不同命令而变化。在新建session时可以通过-n参数指定默认打开的window名称,比如通过tmux new -s basic -n win命名一个win名称的window。也可以随时通过Prefix+,来修改window名称。

2. 切换window
类似标签,我们可以通过一些快捷键在同一个session下的多个window之间切换。比如:

Prefix+p (p for previous):切换到上一个window。
Prefix+n (n for next): 切换到下一个window。
Prefix+0: 切换到0号window,依次类推,1、2、3...
Prefix+w (w for windows): 列出当前session所有window,通过上、下键可以选择切换到指定window。

3. 关闭window
Prefix+&: 关闭当前window。

三. pane
一个window可以切割成多个pane,也就是所谓的分屏,算是Tmux的核心功能之一。

1. 分屏
Prefix+%: 垂直分屏,用一条垂线把当前窗口分成左右两屏。
Prefix+": 水平分屏,用一条水平线把当前窗口分成上下两屏。

2. 切换pane
默认情况下,被选中(激活状态下)的pane会被绿色边框高亮突显出来。
Prefix+o: 依次切换当前窗口下的各个pane。
Prefix+Up|Down|Left|Right: 根据按箭方向选择切换到某个pane。
Prefix+Space(空格键): 对当前窗口下的所有pane重新排列布局,每按一次,换一种样式。
Prefix+z: 最大化当前pane。再按一次后恢复。

3. 关闭pane
Prefix+x: 关闭当前使用中的pane。

关于Tmux的三个核心概念(session、window和pane)及其基本用法已经介绍完毕。接下来的是一些个性化配置和奇技淫巧,包括重新绑定快捷键、自定义快捷键、UI样式、鼠标支持、复制粘贴等等,这些可配置的高级功能也是Tmux受人推崇的原因。Tmux配置文件推荐放在~/.tmux.conf文件中,避免某个用户修改配置影响到其他用户,修改配置文件后要经过reload操作才会在已打开session中生效。

一. 重新绑定快捷键
Tmux的很多默认配置不够友好,需要个人重新定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unbind C-b
set -g prefix C-a
 
bind C-a send-prefix
 
bind r source-file ~/.tmux.conf \; display "tmux.conf reload!"
 
bind | split-window -h
bind - split-window -v
 
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
 
set -g base-index 1
set -g pane-base-index 1

第1-2行表示重新定义Prefix,把默认的Ctrl+b换成Ctrl+a,便于单手操作。
第4行重新定义Ctrl+a组合键,当Prefix + Ctrl+a按下后,等同于原先Ctrl+a功能,解决Ctrl+a被设置为Prefix后已有快捷键失效的问题,也就是说只要按下两次Ctrl+a,就能实现原先终端下回到行首的功能。
第6行定义新的快捷键Prefix+r,重新加载Tmux配置文件,避免每次要进入命令模式reload配置。
第8-9行重新定义分屏快捷键。使用Prefix+|代替Prefix+%实现垂直分屏,使用Prefix+-代替Prefix+"实现水平分屏。|和-的符号本身就可以表示分屏线形状,非常直观。
第11-14行重新定义上下左右方向键,遵循vi习惯。定义以后,任何需要上下左右方向键的场景都可以用hjkl替代。
第16行表示将window的起始下标设为1。因为标准键盘的0在9后面,Prefix + 0/1/2...切换不便。
第17行表示将pane的起始下标设为1。理由同上。

二. 鼠标支持

1
2
3
4
set-window-option -g mode-mouse on
set -g mouse-select-pane on
set -g mouse-resize-pane on
set -g mouse-select-window on

第1行表示启用鼠标。虽然Tmux推荐用键盘完成所有操作,但是对现代开发人员来说,纯键盘操作的习惯并非那么容易养成,因此启用鼠标配置成为标配。
第2行表示支持鼠标选择pane。
第3行表示支持鼠标调整pane大小。
第4行表示支持鼠标选择window。

三. UI样式调整

1
2
3
4
5
6
7
setw -g window-status-current-fg white
setw -g window-status-current-bg red
setw -g window-status-current-attr bright
 
set -g status-justify left
 
setw -g monitor-activity on

第1-3行表示状态栏中window标签的高亮样式,默认是绿底黑字,设置后当前window红底白字显示。
第5行表示状态栏中window列表左对齐排列。
第7行表示非当前window有内容更新时显示在状态栏。

四. 复制粘贴
默认情况下,按Prefix+[进入复制模式,按回车(Enter)退出复制模式。可以通过配置在复制模式中使用vi习惯操作:

1
setw -g mode-keys vi

在复制模式下,按空格键(Space)开始复制,按回车(Enter)完成复制,并退出模式,按Prefix+]粘贴。这些快捷键也可以通过以下配置进行修改,使操作更加靠近vi。

1
2
3
4
5
6
unbind [
bind Escape copy-mode
unbind p
bind p paste-buffer
bind -t vi-copy 'v' begin-selection
bind -t vi-copy 'y' copy-selection

第1-2行表示重新绑定Escape键,Prefix+Escape为进入复制模式。
第3-4行表示重新绑定p键,Prefix+p为粘贴。
第5行表示重新绑定v键,Prefix+v为开始复制。
第6行表示重新绑定y键,Prefix+y为完成复制。

要查看当前复制的内容,可以在Prefix+:后出现的命令行中输入show-buffer,输入list-buffers可以列出所有的复制历史内容。
关于复制粘贴,更深入的话题是Tmux和系统剪贴板之间的交互,Linux可以使用xclip,Mac可以使用tmux-MacOSX-pasteboard,不过我没有试验成功,暂时可以通过ALT + 鼠标复制内容到系统剪贴板。

五. 多屏操作
默认情况下,一个window上只有一个pane被激活,接收键盘交互。但是某些场景下需要在多个pane中执行相同的操作,比如同时修改两台或更多台远程机器的nginx配置,这时候可以在分屏后按Prefix+:进入命令模式,输入set synchronize-panes,即可进入批量操作模式,要退出批量操作模式,再次输入set synchronize-panes即可。

最后上图一张:
Tmux

--EOF--

aufs文件系统

aufs是一种实现了联合挂载(union mount)的文件系统,同unionfs类似,它能够将不同类型的文件系统透明地层叠在一起,实现一个高效的分层文件系统。说白了aufs就是能将不同的目录挂载到某一目录下,并将各个源目录下的内容联合到目标目录下,这里每个源目录对应aufs中的一层,用户在目标目录读写时,感觉不到此目录是联合而来的。aufs中的每一层都可以有不同的权限(只读,读写),这个特性使得它在很多开源项目中有应用,比如Knoppix,Live CD, Docker等等。

Linux上unionfs的实现依托了VFS设计的灵活性,从架构上看,它在VFS的下层,在具体文件系统(如ext3, ext4等)的上层。系统调用read()/write()落在VFS上,VFS找到待操作文件的inode(unionfs inode),经过unionfs的路由后找到真实文件的inode,执行操作。这是一个hook的过程。具体实现可以参考这两篇关于unionfs实现的文章:Part1, Part2

0x00: 环境准备

aufs默认没有合入内核,据说被拒绝多次,此生无望。debian下可以通过apt安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# apt-get install -y aufs-tools
# dpkg -l | grep aufs-tools
ii  aufs-tools    1:3.0+20120411-2    amd64    Tools to manage aufs filesystems
# pwd
/root/test
# mkdir a b uniondir
# echo 1 > a/1.txt
# echo 2 > b/2.txt
# tree .
.
├── a
│   └── 1.txt
├── b
│   └── 2.txt
└── uniondir

0x01: 挂载,文件操作

1
# mount -t aufs -o br=/root/test/a:/root/test/b none /root/test/uniondir

aufs里每个源目录都是一个branch,挂载过程就是对不同branch进行联合操作,当不同分支内容相同时,上层覆盖下层。mount命令中,-t表示目标文件系统类型,-o表示挂载参数, none表示挂载的不是设备文件。这些都是mount命令的参数。br是aufs的参数,表示分支,多个分支之间用冒号(:)分隔,由于分支之间在逻辑上是层叠的,因此声明在前的表示逻辑上层,声明在后的表示逻辑上层,示例中a目录覆盖b目录。mount命令执行后,当前文件系统状态如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# mount -v
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
......
none on /root/test/uniondir type aufs (rw,relatime,si=73990447ced17254)
# tree .
.
├── a
│   └── 1.txt
├── b
│   └── 2.txt
└── uniondir
    ├── 1.txt
    └── 2.txt

可见,a、b目录的内容都被联合到uniondir目录下,此时uniondir目录就是一个aufs文件系统类型的挂载点,从df命令也可以验证。

因为在mount br参数里没加读写权限,默认情况下最上层分支可读写,其他层只读,所以,在uniondir目录下创建3.txt,实际上会创建到最上层分支里(a目录):

1
2
3
4
5
6
7
8
9
10
11
12
# echo 3 > /root/test/uniondir/3.txt
# tree .
.
├── a
│   ├── 1.txt
│   └── 3.txt
├── b
│   └── 2.txt
└── uniondir
    ├── 1.txt
    ├── 2.txt
    └── 3.txt

因为读写权限的不同,修改1.txt和2.txt内容对a、b的影响也不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# echo 'add sth' >> /root/test/uniondir/1.txt
# echo 'add sth' >> /root/test/uniondir/2.txt
# tree .
.
├── a
│   ├── 1.txt
│   ├── 2.txt
│   └── 3.txt
├── b
│   └── 2.txt
└── uniondir
    ├── 1.txt
    ├── 2.txt
    └── 3.txt
# cat /root/test/a/1.txt
1
add sth
# cat /root/test/a/2.txt
2
add sth
# cat /root/test/b/2.txt
2

可见,对可读写分支(目录a),修改直接作用到分支上;而对于只读分支(目录b),修改操作会触发一次写时复制(COW, Copy On Write),即先将待修改文件从目录b复制到目录a,然后在目录a修改相应文件。注意写时复制机制可能在修改大文件时带来性能损耗,虽然每个文件最多只会复制一次。

最后验证下aufs文件系统的删除操作:

1
2
3
4
5
6
7
# rm /root/test/uniondir/*
# tree .
.
├── a
├── b
│   └── 2.txt
└── uniondir

所有的删除操作也都是在最上层的可读写层进行,目录b因为是只读层,所以不受影响。但是为什么最终的uniondir目录中不见2.txt文件呢?因为最上层对它进行了遮挡:

1
2
3
4
5
# ls -al /root/test/a
drwxr-xr-x 4 root root 4096 Jan 17 20:08 .
drwxr-xr-x 5 root root 4096 Jan 17 19:20 ..
-r--r--r-- 2 root root    0 Jan 17 19:27 .wh.2.txt
......

aufs通过在上层加“.wh.+文件/目录名”的特殊标记来遮挡下层的文件。

0x02: 高级参数

1. aufs在mount的时候可以指定每个分支的读写权限。比如:

1
# mount -t aufs -o br=a=rw:b=rw:c=ro none /root/test/uniondir

这个挂载选项指定了a分支和b分支为可读写,c目录为只读。每个分支的读写权限记录在/sys/fs/aufs/si_XXX/brNNN文件里,这里,XXX可从mount -v命令中得到,NNN从0开始,表示第一层,往上递增,依次类推。

1
2
3
4
5
6
# ls /sys/fs/aufs/si_73990447c8980254/
br0  br1  br2  xi_path
# cat /sys/fs/aufs/si_73990447c8980254/br*
/root/test/a=rw
/root/test/b=rw
/root/test/c=ro

当有两个及以上的读写层时,在aufs挂载目录中新建文件就会涉及到源目录的选择策略问题,aufs提供了create配置参数,可以指定各种策略(round-robin(rr), most−free−space(mfs), etc),默认情况下只会写到第一层可写层。另外,经过测试发现,aufs文件系统最上层一定是要可读写的,否则会造成此文件系统只读。

2. aufs支持udba参数,用于配置是否监听源目录(branch)的变化。如果需要源目录的修改实时反应到挂载目录,设置挂载参数-o udba=reval,如果对源目录的修改不需反应到挂载目录,设置挂载参数-o udba=none。默认为reval。

0x03: 限制

aufs出于某些原因会限制层叠的分支数量,debian里默认是127层(早期是42层)。首先,层数越多,效率肯定越低。另一方面,由于/proc和sysfs文件系统中有单个文件4k大小的限制,所以依赖于此的的aufs元信息写入就会受影响,无论层次太多还是分支名称过长,都很容易达到这个限制(brs参数为0时,aufs会将所有的分支信息写入到/proc/mount文件里)。要突破这个限制,可以选择重新编译aufs模块,enable CONFIG_SYSFS和设置brs参数为1(貌似debian下的aufs包默认就是开启此功能的),并且设置合适的编译参数(kconfig中CONFIG_AUFS_BRANCH_MAX_32767=y),这种绕过的方法未经测试,理论上应该可行。

拓展阅读:
1. aufs manpage. http://aufs.sourceforge.net/aufs3/man.html
2. aufs changelog. http://aufs.sourceforge.net/
3. aufs tutorial. http://www.thegeekstuff.com/2013/05/linux-aufs/

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

博客搬迁至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--

从free命令看Linux内存管理

free命令是Linux系统下用来查看内存使用情况的,例如:

$ free -h
             total       used       free     shared    buffers     cached
Mem:          7.8G       6.6G       1.3G         0B       600M       1.9G
-/+ buffers/cache:       4.1G       3.7G
Swap:         2.0G         0B       2.0G

第一行Mem是从操作系统层面看到的内存使用情况,total表示物理内存总量,used表示当前已使用量,free表示当前未分配内存,shared表示共享内存大小,可能已废弃,固定为0,buffers和cached表示被操作系统用来当做磁盘高速缓存的内存大小。这里total=used+free,并且buffers和cached包含在used里,因为在操作系统看来,用于缓存的内存也是属于被用掉的内存。

第二行-/+ buffers/cache从程序角度看到的内存使用情况,used表示被当前系统中所有进程用掉的物理内存大小,free表示当前程序可用的物理内存大小。从数值上看,used=Mem:used-buffers-cached,free=Mem:free+buffers+cached。这里涉及到Linux对内存的管理方式,buffers和cached是两种不同类型的磁盘高速缓存,分别称为块缓存(buffer cache)和页缓存(page cache)。当物理内存剩余充分的时候,操作系统可能会将非常多的空闲内存用作缓存,借以提升磁盘IO性能,这种机制造成的特有现象就是很多时候Mem:free值很低,而buffers和cached的值很高,这个时候并不意味着内存吃紧。当有应用程序需要分配内存时,操作系统会逐渐回收buffers和cached占用的内存空间,这个过程中,Mem:used和Mem:free值不会有大的变动,但是-/+ buffers/cache used和free会体现出变化,具体表现为free值减少,used值增加。所以,第二行的-/+ buffers/cache往往比第一行的Mem统计数据更有参考意义,因为第二行显示的才是当前操作系统真正可用的内存使用量和剩余量。

第三行Swap表示交换空间的使用情况,Swap存在于磁盘中,可以认为是内存的扩展,当应用程序继续吃进内存,操作系统已经没有多余buffers和cached可供回收时,就会将一部分内存页交换到Swap区,以便腾出可用的物理内存。假如此时某个进程关闭腾出了大块物理内存(used值降低,free值上升),操作系统不会把原先置换到Swap区的内存页交换回物理内存,空出来的物理内存会被用来做缓存(buffers+cached)。

为了有个更加直观的解释,特地从网上盗图一张,加深理解:
free

上文的描述都未对页缓存和块缓存进行具体区分,实际上,两者还是有一定区别的。简单来说,页缓存(page cache)为了提升文件内容的读写效率,属于文件系统级别的缓存;块缓存(buffer cache)为了提升块设备的读写效率,属于磁盘级别的缓存。

Linux下文件系统中的文件是由文件内容和元信息(inode)两部分组成的,其中文件内容是分散存储在磁盘的不同块中,通过元信息来组织和描述,这些磁盘上的块数据形成了一个逻辑意义上的文件。因此,磁盘高速缓存机制分成两个层次。当操作系统通过文件系统接口读出文件时,文件内容会被缓存在页缓存里;当文件系统继而将文件内容从磁盘中读出时,文件内容会被缓存在块缓存里。很显然,一次读文件操作会导致文件内容被页缓存和块缓存分别缓存一次,Linux 2.4版本之前就是这么设计的。2.6版本以后,这部分功能进行了优化,对于文件内容的读写,块缓存中的地址被指向到页缓存中,从此,内存中只有一份文件内容的缓存。但是,这并不意味着块缓存可以去除,因为有些场景是需要内核直接读写块设备的,这时候块缓存仍旧有着用武之地,比如读写inode,或者一些跳过文件系统直接操作块设备的命令,如dd等。

References:
[1] Linux Kernel: What is the major difference between the buffer cache and the page cache?
[2] linux cache and buffer

--EOF--