lighty 的生活

lighty 开发者博客

降低请求设置成本

回顾 mod-cml 的首次实现之时,我曾把请求设置成本视为万恶之源。那正是我希望通过 mod-cml 解决的问题。

但是,请求设置成本是什么呢?是什么影响了请求时间?你能从哪里影响它?

当你向浏览器发送请求时,它必须:

  1. 接收请求
  2. 解析请求
  3. 连接到后端
  4. 将请求发送到后端
  5. 等待响应
  6. 接收响应并发送给客户端

使用 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-libFCGI_accept() 调用结合起来。

lua-fastcgi-magnet 非常简单,它只做以下几件事:

  1. 创建一个全局 Lua 环境
  2. 调用 FCGI_accept()
  3. 从脚本缓存中加载脚本
  4. 为脚本创建一个空脚本环境
  5. 注册 print() 函数以使用 fastcgi stdio 封装器
  6. 执行脚本
  7. 再次进入 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 层面工作的脚本语言,例如