回顾 mod-cml 的首次实现之时,我曾把请求设置成本视为万恶之源。那正是我希望通过 mod-cml 解决的问题。
但是,请求设置成本是什么呢?是什么影响了请求时间?你能从哪里影响它?
当你向浏览器发送请求时,它必须:
- 接收请求
- 解析请求
- 连接到后端
- 将请求发送到后端
- 等待响应
- 接收响应并发送给客户端
使用 ab 向后端发送相同的请求,我们应该能很好地命中缓存,并了解在缓存“热”时还剩下什么。
$ ab -n 100 -c 1 http://127.0.0.1:1025/123.php ... Time per request: 7.515 [ms] (mean) [strace] ...
ab 对在 strace 中运行的 lighty 1.4.13-r1385 进行了测试。我们将使用此设置运行所有测试。
$ strace -o costs.txt -tt -s 512 lighttpd -D -f ./lighttpd.conf ...:26.574274 ... (last syscall of the previous request) ...:26.575448 accept(...) = 8 ...:26.576006 read(8, ...) ...:26.576702 connect(9, ... ) ...:26.577239 writev(9, ... ) ...:26.579688 read(9, ...) ...:26.580459 writev(8, ... ) ...:26.581128 close(8)
- accept() 连接耗时 1.2ms
- 从客户端 读取 请求耗时 0.5ms。
- 通过 Unix 套接字 连接 到后端耗时 0.7ms
- 将请求数据 writev() 到后端耗时 0.5ms
- 等待后端并读取响应耗时 2.4ms
- 将响应 writev() 到客户端耗时 0.8ms
- 再次关闭连接耗时 0.5ms
(总计 6.8ms)
不使用 strace 时,响应时间为
Time per request: 2.946 [ms] (mean)
lighty 中的请求降至 0.5ms。后端中的 2.4ms 保持不变。
Keep-Alive
下一个尝试是使用 keep-alive 来摆脱末尾的 accept() 和 close() 调用。在 strace 计时中,执行这两个调用耗时 1.7ms。
$ ab -n 100 -c 1 -k http://127.0.0.1:1025/foo.php ... Time per request: 5.564 [ms] (mean) [strace] ...
strace 告诉我们
…:03.242201 … (the last syscall of the previous request)
…:03.242903 read(8, …)
…:03.243703 connect(9, …)
…:03.244144 writev(9, …)
…:03.246396 read(9, …)
…:03.246969 writev(8, …)
…:03.247261 … (last syscall of this request)
- 读取请求的 read() 耗时 0.5ms
- connect() 耗时 0.8ms
- writev() 到后端耗时 0.4ms
- 等待 + 读取响应耗时 2.2ms
- 将响应 writev() 到客户端耗时 0.7ms
(总计 4.6ms)
Time per request: 2.394 [ms] (mean)
这里看到的 2.2ms 耗费在后端,并且不受 strace 的影响。这是后端花费的时间。如果你将它们从计算中移除,你将得到
100 * (1 - ((2.4ms - 2.2ms) / (2.9ms - 2.5ms))) = 50% 100 * (1 - ((4.6ms - 2.2ms) / (6.8ms - 2.5ms))) = 44%
lighty 内部成本节省了 50%。但实际上,这 50% 的节省只相当于总请求时间的 10%,因为大部分请求时间都花在后端。
后端
限制低请求时间的因素是后端开销尽可能小。为了上述计时,我使用了
它正在执行脚本
<?php echo "123" ?>
PHP 在这 2.2-2.5ms 内做了什么?strace 将再次帮助我们。
...:58.351886 ... (last syscall of the previous request) ...:58.352020 accept(0, ...) = 4 ...:58.353110 read(4, ...) ...:58.353260 stat() ...:58.354431 open(...) ...:58.355489 read(5, ...) ...:58.357595 write(4, ...) ...:58.359911 close(4)
- accept() 耗时 0.2ms
- 读取整个 fastcgi 请求耗时 1.1ms
- stat() 检查文件是否存在耗时 0.1ms
- 打开文件耗时 1.2ms
- 读取文件耗时 1.0ms
- 执行脚本并发送响应耗时 2.1ms
- 关闭连接耗时 2.4ms
我们有两个选择
- 2.6ms 用于建立和关闭连接。FastCGI 可以使用 keep-alive。lighty 1.5.x 将支持此功能。
- 2.2ms 用于将脚本文件导入 PHP。
没有字节码缓存的 PHP 会读取两次
我很好奇,既然我在这里使用了 XCache 1.0.x 作为代码缓存,并且文件在缓存中并获得了缓存命中,为什么还会对文件进行 open()+read() 操作呢?
如果我移除 xcache,同样的 php 文件会被 PHP 读取两次
open("..../foo.php", O_RDONLY) = 4 fstat64(4, {st_mode=S_IFREG|0664, st_size=26, ...}) = 0 ... fstat64(4, {st_mode=S_IFREG|0664, st_size=26, ...}) = 0 read(4, "<?php\n print \"123\";\n?>\n", 4096) = 26 _llseek(4, 0, [0], SEEK_SET) = 0 read(4, "<?php\n print \"123\";\n?>\n", 8192) = 26 read(4, "", 4096) = 0 read(4, "", 8192) = 0 close(4) = 0
php 文件被 open(),stat() 两次 [为什么?] 并 read() 一次,总共 26 字节。
之后我们定位到开头 (_llseek()),再次 read() 26 字节 (与 fstat() 告诉我们的大小相同),并且必须调用 read() 两次才能真正确定确实没有更多数据了。
更新:在与 PHP 核心开发者讨论后,这是一个 FastCGI/CGI SAPI 的特性。由于旧的 CLI 脚本可以通过 CGI SAPI 执行,它必须支持 shell 脚本的“#!/usr/bin/php”序列。对于 Web 应用,这一行必须跳过。否则它会打印到输出中。
移除代码后我们得到
Time per request: 2.150 [ms] (mean)
追求极致
也许最好还是远离 PHP,转而使用一种不提供我们喜欢 PHP 的所有那些“好”功能的语言
- GET 参数的自动解析
- var[] 到数组的映射
- 后端的文件上传支持
- 输出缓冲、压缩
- 诸如 $PHP_SELF 等内部变量,…
既然我已为 mod-magnet 编写了一个字节码缓存,我便稍微扩展了下这个想法,将字节码缓存与来自 fastcgi-lib 的 FCGI_accept() 调用结合起来。
lua-fastcgi-magnet 非常简单,它只做以下几件事:
- 创建一个全局 Lua 环境
- 调用 FCGI_accept()
- 从脚本缓存中加载脚本
- 为脚本创建一个空脚本环境
- 注册 print() 函数以使用 fastcgi stdio 封装器
- 执行脚本
- 再次进入 FCGI_accept()
我想执行相同的脚本,看看现在的响应时间是怎样的
Time per request: 1.594 [ms] (mean)
我们节省了 0.8ms,或整个请求时间的 30%。
magnet 的 strace 简单得多
…:28.541361 … (last call of previous request)
…:28.541507 accept(0, …) = 3
…:28.541916 read(3, …)
…:28.542397 stat64(…)
…:28.542809 write(3, …)
…:28.544807 close(3)
- accept() 耗时 0.2ms
- read() 耗时 0.4ms
- stat() 耗时 0.4ms
- 执行脚本并写入响应耗时 0.5ms
- 等待最终数据包并关闭连接耗时 2.0ms
如果我们的 FastCGI 实现现在有了 keep-alive …
我们已经看到,脚本执行在实时环境中快了 0.8ms。如果我们再次将 strace 附加到 lighty 并运行 ab,我们将得到我们预期的结果
Time per request: 4.800 [ms] (mean) [strace]
在 strace 中,启用 Keep-Alive 的 PHP 耗时 5.6ms,这比之前增加了 0.8ms。
核心 Magnet
如果你真的想花费更少的时间,并且不必等待任何外部资源,你也可以尝试使用 mod-magnet 来执行脚本。
Time per request: 0.584 [ms] (mean)
由于我节省了连接后端和向其传输数据的时间,我可以极快地响应。
结论
让我们提出最后一个问题:我为什么要在意那 0.8ms 呢?
不使用 Keep-Alive 的 PHP | 2.9ms | 345 次请求/秒 |
---|---|---|
PHP | 2.4ms | 416 次请求/秒 |
补丁版 PHP | 2.1ms | 465 次请求/秒 |
lua-fastcgi | 1.6ms | 625 次请求/秒 |
mod-magnet | 0.6ms | 1700 次请求/秒 |
由于 print “123” 是唯一真正生成输出的最小脚本,这应该表明你能达到的最高请求数。在我的测试机器上,使用 PHP 你无法获得超过 416 次请求/秒的吞吐量
- AMD Duron 1.3GHz
- 640MB DDR RAM
- 磁盘和网络无关紧要,因为基准测试是基于 RAM 和回环接口的
如果你需要更高的吞吐量,可以使用 mod-magnet 来分担后端的一些工作。也许你可以缓存一些内容,并在 mod-magnet 中直接处理响应,只需在极少数缓存未命中的情况下才需要将请求上报给后端。
或者使用另一种允许你更直接地在 FastCGI 层面工作的脚本语言,例如
- Ruby
- Python
- Perl
- http://jan.kneschke.de/projects/lua/