标签归档:php

小内存VPS站点内存使用调优(续)

昨天在『小内存VPS站点内存使用调优』中介绍了在VPS中打开swap分区,解决物理内存不足导致的OOM问题。本以为有了1G swap分区(SSD)做后盾,php-fpm的pm.*参数可以调大一点:

1
2
3
4
5
6
pm = dynamic
pm.max_children = 10
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 5
pm.max_requests = 1000

结果今天DNSPod继续发来报警邮件,网站不可用,不过这次不可用的原因不是内存不足,而是IO太忙,Nginx提示504 Time-out。

先SSH到VPS上查看系统状态:
top-wa

上图是top命令的执行结果,异常表现在load很高(单核,10+),CPU使用率很低12.7%,大部分时间在等待IO,71.8% wa。

top命令看不出哪个进程在io wait。于是用apt-get安装iotop命令,它可以用来查看每个进程的IO情况:
iotop

通过左右箭头可以指定按某一列排序,按DISK READ列排序后,问题很直观了,清一色的php-fpm worker进程,并且都是99%左右的时间在swapin,所以问题定位在swap分区的使用不合理上。

swap分区本质是用磁盘模拟内存,它的IO特点是随机读写,即便是号称SSD,相比HDD延时低了不少,看来还是达不到系统使用要求。不知道DigitalOcean有没有对磁盘的IOPS进行Qos限制,从iotop上显示读速度62.85M/s来看,跟基准数据相比,处在偏下的水平。

定位出问题的原因之后,只能继续折腾php-fpm配置参数:

1
2
3
4
5
6
pm = dynamic
pm.max_children = 5
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 3
pm.max_requests = 1000

另外,调整内核vm.swappiness参数(取值0-100,默认60)。swappiness数值越大,表示越优先使用swap;swappiness数值越低,表示越优先使用物理内存。

1
2
# echo 5 > /proc/sys/vm/swappiness
# echo vm.swappiness=5 >> /etc/sysctl.conf

把swappiness参数设置为5,告诉系统大部分时间优先分配物理内存,这可以解决极端情况下物理内存耗尽时,用swap分区做下缓冲。

初步看来,Nginx提示504 Timeout的问题解决了。

--EOF--

小内存VPS站点内存使用调优

本站托管在DigitalOcean,VPS规格是最低的1VCPU,512M内存,20G SSD存储。即便是对于这种日均PV不过百余的小站点,LNMP架构再加上顺便在主机里装上的一系列Shadowsocks、squid、l2tp等代理和VPN软件,内存逐渐成为最大瓶颈。

终于这两天DNSPod不断发邮件报警网站无法访问,打开网站提示“数据库连接错误”,ssh到主机里,dmesg提示各种OOM Killer:

# dmesg | grep oom
[.] php5-fpm invoked oom-killer: gfp_mask=0x201da,order=0,oom_adj=0,oom_score_adj=0
[.]  [<ffffffff810b77f3>] ? oom_kill_process+0x49/0x271
[.] [ pid ]   uid  tgid total_vm      rss cpu oom_adj oom_score_adj name
[.] mysqld invoked oom-killer: gfp_mask=0x201da,order=0,oom_adj=0,oom_score_adj=0
[.]  [<ffffffff810b77f3>] ? oom_kill_process+0x49/0x271
[.] [ pid ]   uid  tgid total_vm      rss cpu oom_adj oom_score_adj name
[.] php5-fpm invoked oom-killer: gfp_mask=0x280da,order=0,oom_adj=0,oom_score_adj=0
[.]  [<ffffffff810b77f3>] ? oom_kill_process+0x49/0x271
[.] [ pid ]   uid  tgid total_vm      rss cpu oom_adj oom_score_adj name
[.] php5-fpm invoked oom-killer: gfp_mask=0x201da,order=0,oom_adj=0,oom_score_adj=0
[.]  [<ffffffff810b77f3>] ? oom_kill_process+0x49/0x271
[.] [ pid ]   uid  tgid total_vm      rss cpu oom_adj oom_score_adj name

php5-fpm和mysqld进程频繁被杀,php5-fpm进程实际上是fastcgi worker进程,即使被杀,也能被php-fpm master进程重启,因此不影响网站可用性,但是mysqld进程一旦被杀了,数据库就挂了,导致首页出现“数据库连接错误”提示。

低层次的内存使用优化可以从两方面入手:

1. 降低mysql的oom score权重,降低被OOM Killer选中的几率。

这点可参考『Linux OOM Killer问题』,将mysqld进程的oom_score_adj调整成最小值-1000:

1
2
# pid=$(cat /var/run/mysqld/mysqld.pid)
# echo -1000 > /proc/$pid/oom_score_adj

2. 减少php-fpm的内存使用量。

Nginx作为Web Server,本身不具备解释执行php的能力,我们能通过GET /index.php获得HTML响应的原因是Nginx后端对接了php解释程序,这个对接的协议就是fastcgi(早期使用cgi,因为fork-exec的请求分发执行方式效率太低,后来有了fastcgi,采用对象池模型,避免每次解析配置文件、初始化执行环境等),fastcgi与语言无关,php-fpm就是php语言环境下的一个fastcgi实现。关于cgi、fastcgi、php-fpm之类的讨论可以参考这两篇文章: 『FastCgi与PHP-fpm之间的关系』,『概念了解:CGI,FastCGI,PHP-CGI与PHP-FPM』。

默认情况下,php-fpm中关于进程池的配置(/etc/php5/fpm/pool.d/www.conf)如下:

1
2
3
pm = dynamic ;;表示worker进程动态调整。
pm.max_children = 20 ;;表示最大worker进程数量。
pm.max_requests = 0 ;;表示每个worker进程最多处理多少次请求后重启,0表示永不重启。

从中可以看出默认配置对于小内存(512M)的VPS的不合理之处,每个cgi worker进程的初始内存占用量约为10M,并且很容易就涨至50+M,当并发数稍稍上去一点,可能就会产生 20(workers) * 50M ~= 1G 的内存使用量。这还未考虑某些php第三方库存在内存泄露问题,当pm.max_requests参数为0,内存泄露会造成worker进程内存使用量越用越大,最终失控。

权衡之后,我把配置调整为:

1
2
3
4
5
6
pm = dynamic
pm.max_children = 3
pm.start_servers = 2 ;;启动worker数量
pm.min_spare_servers = 2 ;;最小空闲worker数量
pm.max_spare_servers = 3 ;;最大空闲worker数量
pm.max_requests = 500 ;;每个worker处理500次请求后重启

虽然保守了一点,但至少比较保险。

调整pm.*参数时建议taif syslog和php5-fpm.log,实时监控站点运行状况。tailf syslog目的是为了监控还有没有php-fpm进程被oom kill;tailf php5-fpm.log目的是为了查看fpm worker进程的启动和异常关闭情况。如果调整后出现Nginx “502 bad gateway”错误,或者php-fpm频繁的“WARNING: [pool www] child 10595 exited on signal 9 (SIGKILL) after 78.871109 seconds from start”日志,那肯定是pm.*参数调整不合理无疑了。

以上2种手段毕竟治标不治本,是以牺牲性能为前提的。实际上当内存不足时,最直观也是最简单的方法当然是加内存,既然物理内存没法加,那就加虚拟内存(swap)吧,DigtalOcean上的VPS主机默认没有开swap分区:

1
2
3
4
5
6
7
# free -h
             total       used       free     shared    buffers     cached
Mem:          497M       408M        88M         0B       456K        15M
-/+ buffers/cache:       393M       103M
Swap:            0          0          0
# swapon -s
Filename                                Type            Size    Used    Priority

DigitalOcean提供了SSD硬盘,所以开个2倍物理内存大小的swap分区也没什么问题,以下是使用磁盘文件作为swap分区的步骤:

1. 生成1G大小的磁盘文件

1
# dd if=/dev/zero of=/swapfile bs=1M count=1024

2. 将文件格式化为swap分区

1
# mkswap /swapfile

3. 启动swap分区

1
2
3
4
5
6
7
8
9
# swapon /swapfile
# free -h
             total       used       free     shared    buffers     cached
Mem:          497M       462M        34M         0B       252K        21M
-/+ buffers/cache:       440M        56M
Swap:         1.0G          0       1.0G
# swapon -s
Filename                                Type            Size    Used    Priority
/swapfile                               file            1048572 645840  -1

4. 将swap分区加到/etc/fstab,使重启后仍生效

1
# echo /swapfile swap swap defaults 0 0 >> /etc/fstab

有了1G的基于SSD的swap分区后,就可以把php-fpm参数调得任性一点了:

1
2
3
4
5
6
pm = dynamic
pm.max_children = 10
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 5
pm.max_requests = 1000

初步看来,小内存VPS站点内存耗尽问题解决了。

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

SimpleXML函数库在豆瓣API调用中的应用

SimpleXML是PHP5提供的一套用于处理XML的函数库,它通过simplexml_load_file和simplexml_load_string两个函数实现将XML对象转换为simplexml对象(SimpleXMLElement),前者用于处理XML文档,后者用于处理符合XML标准的字符串,程序员可以通过simplexml对象直接读写XML内容。

相比普通XML格式文件,调用豆瓣API返回的结果中有两个地方需要特殊处理:

1. 节点名称中含命名空间。例如<db:attribute />、<db:tag /> 、<gd:rating />等节点分别包含在db和gd的命名空间中,命名空间在XML文件会有定义。对于包含在命名空间的节点名的处理,SimpleXML函数库提供了children函数。通常children函数是用来获得SimpleXMLElement对象的子节点的值,其时,children函数还接受namespace的参数,用来获取当前节点内部指定命名空间中的子节点的值。因此,通过带namespace参数的children函数可以解析上述的<db:attribute />、<db:tag />和<gd:rating />子节点。

2. 节点名称内部包含属性值。例如<db:tag count="63706" name="青春" />,tag节点的内部属性count的值为“63706”,内部属性name的值为“青春”。内部属性的名称和值在一些场合下非常有用,甚至必需。SimpleXML函数库提供了attributes函数来解析节点的内部属性。对一个节点调用attributes函数,会返回一个关联数组,键为内部属性名,值为内部属性值,例如上述的tag节点会被解析为数组:Array ( [count] => 63706 [name] => 青春 ) )。

以我最近比较喜欢的电影『那些年,我们一起追的女孩』为例,这部电影相关信息的API地址为http://api.douban.com/movie/subject/4920528(缓存页面),通过SimpleXML函数库解析返回结果的PHP代码实现如下:

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
<?php
header("Content-type: text/html; charset=utf-8");//中文支持。
$url = 'http://api.douban.com/movie/subject/4920528';//影片对应的API地址。
$xml =  simplexml_load_file($url); //解析XML文件形式的返回结果。
 
print "id: " . $xml->id . "<br>";//影片ID,URL。
print "title: ".$xml->title . "<br>";//标题。
print "author: ".$xml->author->name . "<br>";//导演。
print "summary: " . $xml->summary . "<br>";//故事梗概。
 
//调用children函数获取db命名空间下的节点数组。
$db_xml = $xml->children('http://www.douban.com/xmlns/');
//var_dump($db_xml); //db命名空间只包含attribute节点和tag节点。
foreach($db_xml->attribute as $obj){//遍历所有<db:attribute />节点。
	print "db:attribute: ";
	$attrname = $obj->attributes();//调用attributes()函数获取内部属性数组。
	foreach($attrname as $key=>$value){//遍历当前节点的所有内部属性。
		print $key . "=>" . $value . " ";
	}
	print $obj . "<br>";
}
//<db:tag />节点的解析同<db:attribute />。省略。
 
//获取gd命名空间下的节点的方法同db命名空间。省略。
?>

上述程序的运行结果如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
id: http://api.douban.com/movie/subject/4920528
title: 那些年,我们一起追的女孩
author: 九把刀 Giddens
summary: 柯景腾(柯震东 饰)、老曹(敖犬 饰)、勃起(鄢胜宇 饰)、该边(蔡昌宪 饰),
、阿和(赦绍文 饰),从国中到高中,一直是不离不弃的死党。他们都对班花沈佳宜(陈妍希 饰)
有着一种纠结的感情。一方面,他们瞧不起这种只会用功读书的女生,另一方面他们又为她的美好
气质倾倒。因为学习成绩较差,柯景腾被老师安排坐在沈佳宜前面。因为他的一次英雄救美,她开始
用强制的方式帮他补习功课。此事令其他兄弟羡慕嫉妒恨,但是大家都未说破。毕业后,柯景腾和
沈佳宜在各自大学保持恋人般的联系。直到他举办比武大会,事情才出现了变化…… 本片根据导演
兼编剧九把刀在2007年的自传体小说改编。柯震东凭借本片获第48届金马奖最佳新演员。©豆瓣
db:attribute: name=>director 九把刀
db:attribute: lang=>zh_CN name=>aka 那些年,我们一起追的女孩
db:attribute: name=>movie_type 剧情
db:attribute: name=>movie_type 喜剧
db:attribute: name=>movie_type 爱情
db:attribute: name=>website www.appleofmyeye.com.tw
db:attribute: name=>movie_duration 110分钟(港澳台)
db:attribute: name=>movie_duration 100分钟(中国大陆)
db:attribute: name=>year 2011
db:attribute: name=>writer 九把刀
db:attribute: name=>language 汉语普通话
db:attribute: name=>imdb http://www.imdb.com/title/tt2036416/
db:attribute: name=>aka You Are the Apple of My Eye
db:attribute: name=>aka 나사년, 아문일기추적녀해
db:attribute: name=>aka あの頃、君を追いかけた
db:attribute: name=>aka 那些年,我們一起追的女孩
db:attribute: name=>aka 那些年,我们一起追的女孩
db:attribute: name=>title 那些年,我们一起追的女孩
db:attribute: name=>country 台湾
db:attribute: name=>pubdate 2011-08-19(台湾)
db:attribute: name=>pubdate 2011-10-20(香港)
db:attribute: name=>pubdate 2012-01-06(中国大陆)
db:attribute: name=>cast 柯震东
db:attribute: name=>cast 陈妍希
db:attribute: name=>cast 敖犬
db:attribute: name=>cast 郝邵文
db:attribute: name=>cast 蔡昌宪
db:attribute: name=>cast 鄢胜宇
db:attribute: name=>cast 弯弯
db:attribute: name=>cast 邱彦翔

更多的SimpleXML函数库用法可参见官方文档

--EOF--

短网址API调用实例(PHP实现)

短网址是随着微博的流行而出现,目前市面上的短网址服务林林总总不计其数,评价一个短网址服务好坏的最重要标准是其服务的稳定性。试想,短网址服务做的工作就是为原先的长网址加一层映射关系,而这种映射关系是握在短网址服务提供商手上的,如果这个环节出了问题,导致映射关系丢失,那也就意味着再也无法找回原先的长网址。

以上差不多都是废话。

国内外比较有实力的短网址提供商如goo.gl(Google)、dwz.cn(百度)、is.gd等都提供了API的方式供外部生成短网址,该方式只需模拟一个HTTP请求,以长网址作为参数向服务器发送该请求,如果请求成功,服务器就会返回缩短以后的短网址。

有些API要求将HTTP请求以POST方式提交,例如goo.gl和dwz.cn(demo),这时可以采用基于PHP的cURL库来完成HTTP请求的发送和接收。以goo.gl为例(更多细节可参考『Google URL Shortener API』):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$apiKey = 'APIKEY'; //google短网址所需。
//API key从http://code.google.com/apis/console/得到
$postData = array('longUrl' => "http://fengchj.com", 'key' => $apiKey);
$jsonData = json_encode($postData);
$co = curl_init();
curl_setopt($co, CURLOPT_URL, 'https://www.googleapis.com/urlshortener/v1/url');
curl_setopt($co, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($co, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($co, CURLOPT_HEADER, 0);
curl_setopt($co, CURLOPT_HTTPHEADER, array('Content-type:application/json'));
curl_setopt($co, CURLOPT_POST, 1);
curl_setopt($co, CURLOPT_POSTFIELDS, $jsonData);
$strResponse = curl_exec($co);
curl_close($co);
$arrResponse = json_decode($strResponse );
print $arrResponse->id; //得到http://goo.gl/xxxx类型的短网址。

有些短网址服务API接受以GET方式提交的长网址,例如is.gd,用户只需将长网址作为参数向提供商给出的URL发送请求即可,如:

1
print file_get_contents("http://is.gd/create.php?format=simple&url=http://fengchj.com");

实际上这里也可采用cURL库进行GET方式HTTP请求的封装,但是由于GET方式的幂等特性,用file_get_contents来请求给定URL页面更为方便。相对于cURL库,file_get_contents方式的缺点在于无法对HTTP请求过程进行错误处理。

调用外部API生成短网址的效果可参见http://fengchj.com/?page_id=1619

--EOF--