技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> Apache --> 流量低峰也烦人-lighttpd耗时长问题追查

流量低峰也烦人-lighttpd耗时长问题追查

浏览:5229次  出处信息

结论

如果你用lighttpd1.5(以下lighttpd均指1.5)做静态文件服务器,或者你虽然用lighttpd处理php请求,但是用到$PHYSICAL作为mod_proxy_core的条件, 且某个时候你的单机流量很低(几个/s), 或许你也有类似的问题,但是影响程度或许不会引起你的注意!
1.Lighttpd的mod_proxy_core不建议用$PHYSICAL作为条件;
2.Lighttpd的stat cache机制没有节省任何开销;
3.Lighttpd子线程和主线程通过管道+epoll的通信机制,存在event丢失问题;

现象

用户反馈凌晨的时候访问百度某页面,某些模块的数据出不来;

其它依赖于我们的前端接口的产品线反馈访问时间有时候超过1s;

我们自己的QA环境偶尔也会出现请求超过1s的问题;

因此我们打开lighttpd的日志的%D配置,打印ms级别的处理时间,发现晚上1点到凌晨8点有很多处理时间超过1s的请求,500ms以上的也有很多,并且流量越低, 比例越大;

1点-8点是流量低峰时期,流量越低,性能越差

这个现象每到高峰时期就正常了,因为是流量低峰才会出现这样的慢请求,占总比例非常之少,对整体的性能和稳定性影响极小,所以性能和稳定性监控报表中没有发现这个问题。

追查过程

由于这个页面对性能要求相对比较严格,虽然性能和稳定性衡量数据已经非常好, 但是这个问题一直是一个阴影,不解决终归不爽,所以开始了下面的追查过程:

由于处理路径是lighttpd->php-cgi->框架+逻辑, 首先的怀疑是框架+逻辑问题,但是通过查看php的处理时间,流量低峰高峰都非常正常,极少超过100ms,所以排除是框架+逻辑问题;那究竟是php-cgi的问题还是lighttpd本身的问题呢?为了排除php-cgi的问题,我们尝试了从线下复现这个问题,看访问静态文件是否也有类似的问题。但是悲剧的是线下就是复现不了这个问题。那再对比和线上环境的不同,会不会是先要经过一段时间的大流量,然后再小流量才会出现这个问题呢?于是用ab 30qps压了2个小时后停止,然后再手动访问试了一下,果然如此!通过访问静态文件,发现静态文件也是如此,处理时间超过1s, 因此基本排除php-cgi本身的原因,问题应该是出在lighttpd本身

通过这个线下实验,我们还发现了如下规律:

1.前期用ab压的时间不定,有时候压2个小时后然后低流量访问还不会出现这个问题,有一定的随机性;

2.手动低流量访问的时候,并不是每次都慢,对同一个url, 紧接着的两次访问(访问第一次后马上访问第二次),第一次会慢,但第二次会很快,然后再过个1-2s钟再访问第三次,又会很慢;

3.手动请求的时候,如果慢,总是慢1s, 但是线上有慢1s的,也有不是慢1s的,最多1s;

4.重启lighttpd后,所有请求会恢复正常,需要重新压;

于是产生两个最大的疑问, lighttpd在公司使用这么广,处理静态资源和php请求的都有用到, 别的产品线为什么不报? 为什么是1s? 对于第一个疑问,觉得可能是因为这种情况影响的平均性能非常少,可能其他产品线不会这么敏感,或者是流量低峰的单机请求量也很高,没有频繁的触发这个问题,这个时候还对比了其它产品线的lighttpd.conf, 这个时候是没有发现有什么问题的。于是就从第二个问题开始着手追查:为什么是1s?

带着问题,开始读lighttpd的源代码了。。。

该页面lighttpd event-handler用的是linux-sysepoll;

首先发现lighttpd代码中有各个地方和1s有关的代码:

源文件server.c

第一个1000ms是lighttpd的epoll的超时时间,也就是如果没有任何句柄有事件发生,epoll最多等待1000ms后即会返回,如果有事件发生,epoll会马上返回有事件发生的所有句柄,然后lighttpd会处理joblist中已经准备好的connection,重新进入状态机;第二个1s是lighttpd有个一个trigger机制,每隔1s会触发一次SIGALRM, 然后lighttpd处理超时的请求,清理stat_cache等;因此,通过修改这两个1s, 发现当改成fdevent_poll(srv->ev, 500);  后,慢请求都变成500了, 所以慢请求是因为的那个connection已经放在joblist, 但是没有成功触发epoll返回, epoll只有等待超时后返回,该connection才会被处理,这也就解释了为什么流量高峰的时候没有这个问题,因为高峰的时候epoll返回得相当频繁,也可以解释为什么线上的慢请求慢100,200ms的都有,但是最多不超过1s了,线下手工访问的时候总是慢1s, 这也是因为每秒的请求量的原因, 这其实也是类似epoll这种异步事件处理模型所带来的通病,用延迟换吞吐量;

问题进一步,那为什么重启lighttpd后,就算流量低也没问题呢,所以进一步看代码,通过把lighttpd所有debug日志打开,发现这个问题和lighttpd­的stat cache机制有关, 为了避免反复的调用stat来获取文件信息,lighttpd用了一个全局的hash表保存了每个物理路径的所对应文件的stat结果,这个机制和server. stat-cache-engine这个配置有关,我们用的默认配置“simple”, cache结果会缓存1s, 如果没有命中或者失效,lighttpd会把这个stat任务放在一个队列里面, 然后告诉状态机HANDLER_WAIT_FOR_EVENT, 暂时退出状态机,由另外几个线程来异步处理这个stat任务,处理完这个任务后,会重新把这个任务关联的connection加到joblist_queue中,然后通过管道通知主线程,让epoll返回, 相关代码如下:

源文件joblist.c

上面的代码可以看出, lighttpd是通过判断一个全局的变量srv->did_wakeup,如果是0,就把它改成1,然后往这个管道发生一个空格字符串,触发主线程的epoll返回,如果这个变量不是0,就不会通知主进程。

下面的代码是主线程epoll返回后,和这个管道句柄对于的处理函数,可以看出主线程又把srv->did_wakeup初始化成0了, 这样下次还会wakeup主线程;

源文件server.c

这就引发一个思考,如果因为某种原因srv->did_wakeup被修改成1了,但是主线程由于某种原因没有收到这个write事件,导致srv->did_wakeup没有被改成0,那不是后面都不会通过管道通知主线程了,为了证明这个假设,我加了下trace代码,发现确实是这样的,设置srv->did_wakeup =1 做了2456次,但是设置srv->did_wakeup = 0只做了2455次,只差一次,并且后续都没有做这个操作了,另外还发现子线程每次write管道都是成功的,但是最后一次主线程没有收到这个事件,至于为什么没有收到,就没有继续查了。

但是,还是有个疑问,我访问的php请求,lighttpd应该把请求路径发给php-fpm,自己应该不关心物理路径的啊,就不用搞什么stat cache吧,这个时候想起了当时为了解决某扩展能够正确获取到PATH_INFO的问题,把mod_proxy_core的条件配置从$HTTP["url"] =~ “\.php$”改成了$PHYSICAL["existing-path"] =~ “\.php$”。马上修改配置,再测试,问题果然没有了, 通过查看lighttpd代码,发现如果配置成$PHYSICAL这种形式,会导致lighttpd去stat这个物理文件,这个操作在mod_proxy_core之前执行,如果用$HTTP[“url”]就不会引发这个问题,到此,一切都清楚了,我看到的其它老的产品线都是配的$HTTP[“url”], 只有少数的几个产品线不是用的$HTTP[“url”],也只是单机流量非常低的情况才会出现这个问题,很难会让人觉察到!

另外,在追查问题的过程中还发现lighttpd stat_cache机制的两个问题,第一个问题就是处理stat任务的子线程,在stat之后,并没有更新这个stat cache的状态为FINISHED, 下次来查的时候还是没有命中cache, 等于是白干了。如下代码所示:

源文件stat_cache.c

第二个问题是就是命中了stat cache, 其实还是需要调用stat判断改cache有没有过期, 所以觉得stat cache本身这个机制也是白搞了,比较没有节省stat的开销,还多搞了,如下代码所示:

源文件stat_cache.c

遗留问题

子线程和主线程通过管道+epoll的机制来通信,为什么会有一定的概率失败呢?write管道其实是成功的,由于精力有限,这个问题没有继续追查;

管道其实是成功的,由于精力有限,这个问题没有继续追查;

对lighttpd配置的建议

如果利用mod_proxy_core做php处理,还是用$HTTP[“url”]做条件吧,例如:

$HTTP["url"] =~ “\.php” {

proxy-core.balancer = “static”

proxy-core.protocol = “fastcgi”

proxy-core.allow-x-sendfile = “enable”

proxy-core.backends = ( “unix:/home/super/php/var/php-cgi.sock” )

proxy-core.rewrite-request = (

“_pathinfo” => ( “(/[^\?]*)/index\.php(/[^\?]*)” => “$2″ ),

“_scriptname” => ( “(.*\.php)” => “$1″ )

)

注意,为了让php-cgi取到正确的PATH_IFNO, 请注意添加“_pathinfo” rewrite规则!

对lighttpd代码优化的建议

1.在每隔1s的trigger操作中,新增一个操作:将srv->did_wakeup重置为0,防止这个变量变成1以后永远便不会0的情况发生;
2.stat_cache_thread处理完stat_job之后,要更新源stat_cache_entry的状态为FINISHED, 否则就白搞了;
3.命中stat cache后,不用再通过stat判断该cache是不是最新的,因为最多缓存1s钟;

by he_wei

建议继续学习:

  1. 说说lighttpd的fastcgi    (阅读:5632)
  2. lighttpd, web.py, spawning fcgi failed    (阅读:3625)
  3. Lighttpd mod_fastcgi源码分析    (阅读:2730)
  4. LIGHTTPD安装    (阅读:2235)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1