使用 sendfile() 提升网络文件发送性能

偶见一好文,清楚地阐述了什么是零拷贝(Zero Copy)以及 sendfile 的由来,不复述下实感不快。
原文见:http://www.linuxjournal.com/article/6345

文章中列出了我们平时通过网络发送文件时会用到的两个系统调用:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);

调用过程示意图如下:

在用户空间调用 read() 读取文件时发生两次内存拷贝:

  1. DMA引擎将文件读取到内核的文件缓冲区
  2. 调用返回用户空间时将内核的文件缓冲区的数据复制到用户空间的缓冲区

接着调用 write() 把数据写入 socket 时,又发生了两次内存拷贝:

  1. 将用户空间的缓冲区的数据复制到内核的 socket 缓冲区
  2. 将内核 socket 缓冲区的数据复制到网络协议引擎

也就是说,在整个文件发送的过程中,发生了四次内存拷贝。
然后,数据读取到用户空间后并没有做过任何加工处理,因此通过网络发送文件时,根本没有必要把文件内容复制到用户空间。

于是引入了 mmap():
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

调用过程示意图:

  1. 调用 mmap() 时会将文件直接读取到内核缓冲区,并把内核缓冲区直接共享到用户空间
  2. 调用 write() 时,直接将内核缓冲区的数据复制到网络协议引擎

这样一来,就少了用户空间和内核空间之间的内存复制了。
这种方式会有个问题,当前进程在调用 write() 时,另一个进程把文件清空了,程序就会报出 SIGBUS 类型错误。

Linux Kernel 2.1 引进了 sendfile(),只需要一个系统调用来实现文件发送。
sendfile(socket, file, len);

调用过程示意图:

  1. 调用 sendfile() 时会直接在内核空间把文件读取到内核的文件缓冲区
  2. 将内核的文件缓冲区的数据复制到内核的 socket 缓冲区中
  3. 将内核的 socket 缓冲区的数据复制到网络协议引擎

从性能上看,这种方式只是少了一个系统调用而已,还是做了3次拷贝操作。

Linux Kernel 2.4 改进了 sendfile(),调用接口没有变化:
sendfile(socket, file, len);

调用过程示意图:

  1. 调用 sendfile() 时会直接在内核空间把文件读取到内核的文件缓冲区
  2. 内核的 socket 缓冲区中保存的是当前要发送的数据在内核的文件缓冲区中的位置和偏移量
  3. DMA gather copy 将内核的文件缓冲区的数据复制到网络协议引擎

这样就只剩下2次拷贝啦。

在许多 http server 中,都引入了 sendfile 的机制,如 nginx、lighttpd 等,它们正是利用 sendfile() 这个特性来实现高性能的文件发送的。

— EOF —

无废话安装 Redis PHP C 扩展 phpredis

[2011-07-10] 更新至最新版本:

# wget https://download.github.com/nicolasff-phpredis-2.1.3-0-g43bc590.tar.gz
# tar zxf nicolasff-phpredis-2.1.3-0-g43bc590.tar.gz
# cd nicolasff-phpredis-43bc590/
# /usr/local/php/bin/phpize
# ./configure –with-php-config=/usr/local/php/bin/php-config
# make && make install
# echo “extension = redis.so” >> /usr/local/php/etc/php.ini

以上扩展在 Redis 2.2.11 下测试正常。

接口操作手册:
https://github.com/nicolasff/phpredis

[2010-12-22] 更新至最新版本:

# wget –no-check-certificate https://github.com/owlient/phpredis/tarball/2.0.12
# tar zxf owlient-phpredis-2.0.12-0-g44f048c.tar.gz
# cd owlient-phpredis-44f048c/
# /usr/local/php/bin/phpize
# ./configure –with-php-config=/usr/local/php/bin/php-config
# make && make install
# echo “extension = redis.so” >> /usr/local/php/etc/php.ini

以上扩展在 Redis 2.0.2 下测试正常。

接口操作手册:
https://github.com/nicolasff/phpredis

— EOF —

Nginx+PHP 配置漏洞:静态文件都可以当作 PHP 解析

漏洞危险等级:毁灭性。

这个漏洞严格上说并不是 Nginx 和 PHP 本身的漏洞造成的,而是由配置造成的。在我之前写的许多配置中,都普遍存在这个漏洞。

简易检测方法:
打开 Nginx + PHP 服务器上的任意一张图片,如:
http://hily.me/test.png
如果在图片链接后加一串 /xxx.php (xxx为任意字符)后,如:
http://hily.me/test.png/xxx.php
图片还能访问的话,说明你的配置存在漏洞。

漏洞分析:

下面通过分析一个很常见的 Nginx 配置来解释下漏洞的成因:

server {
    listen       80;
    server_name  test.local;

    access_log  /work/www/logs/test.access.log  main;
    error_log  /work/www/logs/test.error.log;

    location / {
        root   /work/www/test;
        index  index.html index.htm index.php;
    }

    location ~ \.php$ {
        root           /work/www/test;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
        include        fastcgi_params;
        fastcgi_pass   unix:/tmp/php-fpm.sock;
    }
}

我们在 /work/www/test/ 目录下新建一个文件 test.png,内容如下:

那么访问 http://test.local/test.png 时,输出为文本内容:

但是当在后面加上 /xxx.php 时,即 http://test.local/test.png/xxx.php,可怕的事情发生了:

Array
(
    [HOSTNAME] => 
    [PATH] => /usr/local/bin:/usr/bin:/bin
    [TMP] => /tmp
    [TMPDIR] => /tmp
    [TEMP] => /tmp
    [OSTYPE] => 
    [MACHTYPE] => 
    [MALLOC_CHECK_] => 2
    [USER] => www
    [HOME] => /home/www
    [FCGI_ROLE] => RESPONDER
    [SCRIPT_FILENAME] => /work/www/test/test.png
    [QUERY_STRING] => 
    [REQUEST_METHOD] => GET
    [CONTENT_TYPE] => 
    [CONTENT_LENGTH] => 
    [SCRIPT_NAME] => /test.png/xxx.php
    [REQUEST_URI] => /test.png/xxx.php
    [DOCUMENT_URI] => /test.png/xxx.php
    [DOCUMENT_ROOT] => /work/www/test
    [SERVER_PROTOCOL] => HTTP/1.1
    [GATEWAY_INTERFACE] => CGI/1.1
    [SERVER_SOFTWARE] => nginx/0.7.62
    [REMOTE_ADDR] => 192.168.1.163
    [REMOTE_PORT] => 4080
    [SERVER_ADDR] => 192.168.1.12
    [SERVER_PORT] => 80
    [SERVER_NAME] => test.local
    [REDIRECT_STATUS] => 200
    [HTTP_ACCEPT] => image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/QVOD, application/QVOD, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
    [HTTP_ACCEPT_LANGUAGE] => zh-cn
    [HTTP_ACCEPT_ENCODING] => gzip, deflate
    [HTTP_USER_AGENT] => Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQPinyin 689; QQDownload 627; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; InfoPath.2; TheWorld)
    [HTTP_HOST] => test.local
    [HTTP_CONNECTION] => Keep-Alive
    [ORIG_SCRIPT_FILENAME] => /work/www/test/test.png/xxx.php
    [PATH_TRANSLATED] => /work/www/test
    [PHP_SELF] => /test.png/xxx.php
    [REQUEST_TIME] => 1274125615
)

环境变量中,SCRIPT_FILENAME 是 Nginx 传过来的:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

$fastcgi_script_name 变量说明请参考:
http://wiki.nginx.org/NginxHttpFcgiModule

Nginx 传给 PHP 的值为 /work/www/test/test.png/xxx.php,即 $_SERVER 中 ORIG_SCRIPT_FILENAME 的值,但是 $_SERVER 中 SCRIPT_FILENAME 却是 /work/www/test/test.png。

原因是,/work/www/test/test.png/xxx.php 并不存在,对于这些不存在的路径,PHP 会检查路径中存在的文件,并将多余的部分当作 PATH_INFO。
这里,/work/www/test/test.png 被 PHP 解析为 SCRIPT_FILENAME,/xxx.php 被 PHP 解析为 PATH_INFO 后被丢弃,因此并没有在 $_SERVER 中出现。

解决方法:

解决这个漏洞的方法很显然:关闭上面所述的解析即可。

这个解析可以在 PHP 的配置文件中设置,默认为开启。在这里我们需要将它关闭:

; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP’s
; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting
; this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting
; of zero causes PHP to behave as before. Default is 1. You should fix your scripts
; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
; http://php.net/cgi.fix-pathinfo
;cgi.fix_pathinfo=1
cgi.fix_pathinfo=0

其中 cgi.fix_pathinfo=0 为新增的配置行,表示关闭 PHP 的自动 PATH_INFO 检测。关闭后,该配置漏洞即可消除。

更好的解决方案?
以上方案并不是最完美的,如果你先前有用到 cgi.fix_pathinfo 这个特性,影响会很大,比如关闭后,我的 Blog(Wordpress)文章的 URL 目录形式就得用 rewrite 来实现了。
如果可以将 PHP 设置成只解析 .php 为扩展名的文件,那么这个问题解决起来会更合理。
不过我没找到相关的设置项,或许今后应该出现在 php-fpm 的配置文件中?

总结:
这类问题基本上是无法预料的,但是如果架构设计良好的话,即使存在这个问题,也不会影响安全性。这里给出架构上的安全建议:
* 尽可能使动静内容分离,所有的静态内容存在于静态内容服务器,静态内容服务器上不解析PHP,这样静态文件就永远不能被解析了。

— EOF —

无废话安装 dbcached

# 安装 qdbm 和 nmdb
dbcached 作分布式需要用到这两个

emerge libevent
emerge qdbm
wget http://blitiri.com.ar/p/nmdb/files/0.22/nmdb-0.22.tar.gz
tar zxvf nmdb-0.22.tar.gz
cd nmdb-0.22/
make BACKEND=qdbm ENABLE_TIPC=0 ENABLE_SCTP=0 install

# 安装 dbcached

wget http://dbcached.googlecode.com/files/dbcached-1.0.beta2.tar.gz
tar zxvf dbcached-1.0.beta2.tar.gz
cd dbcached-1.0.beta2/
./configure –prefix=/usr/local/dbcached –with-libevent=/usr
make && make install

# 启动 nmdb

/usr/local/bin/nmdb -d /var/dbcached.db -t 26010 -T 127.0.0.1 -u 26010 -U 127.0.0.1 -c 1024

# 启动 dbcached

/usr/local/dbcached/bin/memcached -d -m 16 -l 127.0.0.1 -p 11211 -c 512 -u nobody -x 127.0.0.1 -y 26010 -z 26010

— EOF —

RabbitMQ: 消息交换模块 Exchange

RabbitMQ 中的 Exchange 负责对消息进行路由。
当收到 Publisher 传递给它的消息后,Exchange 会根据路由键 routing key 决定将消息加入到哪些消息队列中。

Exchange 有三种类型:

1. 直接交换类型 Direct Exchange Type
使用一个固定字符串作为 routing key。
若 exchange 和 message queue 绑定的 key 和 routing key 一样时,消息会被路由到绑定的这个 message queue 中。

2. 扇形交换类型 Fanout Exchange Type
该类型不使用 routing key,进来什么就出去什么。
扇形结构,可用于负载均衡。

3. 主题交换类型 Topic Exchange Type
routing key 中可以包括通配符。
在与 message queue 绑定时,也可以使用通配符。
这种交换类型的起名应该和微博中的应用有关,比如 *.sometopic 代表 sometopic 主题下的所有项目。
通配符:* 表示一个词 # 表示0个或多个词

— EOF —