Nginx+Fastcgi+Webpy惊险之旅

事件起因

自从去年11月份跳槽之后,就告别了熟悉的c#,涉险Python开发,顺便开始熟悉管理之道

最近一直忙于一个新产品的beta版上线公测,该产品涉及到好几个子系统,为了节约空间,这些子系统全部部署在一台64位Linux机器上,通过不同端口访问,惊险之旅自此开始…

项目情况大致如下

A子系统: nginx管理静态文件,动态请求基于jsonp方式访问不同域名下的网关应用

B子系统: nginx管理静态文件,动态请求通过nginx反向代理映射到fastcgi+webpy模块上,认证方式使用webpy内置的session机制

测试环境包括平时的开发测试和部署之后的内部测试,不久之前测试人员经常跟我反映:

部署在内网中的B子系统在用浏览器登录几分钟之后就出现频繁的404回应,根本无法使用,但在强制清除cookie之后又可以正常使用,不久之后又恢复不能访问现象…

分析

起初我很是奇怪,按理说只有当静态文件不存在时nginx才会返回404错误,而现在访问的是配置在nginx.conf中的一个动态路径,该动态路径请求通过fastcgi最终会映射到某个python class的GET或POST方法中,那为什么nginx会返回404呢?

查阅相关文档之后发现

fastcgi在遇到webpy或其他后端http模块处理极慢的情况下,也就是说超过nginx允许的应答时间,nginx就会对此动态路径请求做出404的应答

针对此情况,我开始着手准备一系列测试和实验

实验

实验一:剥离fastcgi,单独使用webpy,进行压力测试

这个实验很简单,只需要注释掉一行代码,便可以以纯webpy方式访问,基于webpy的python应用main入口一般都是这样:

1
2
3
4
<font color="#0000ff">if</font> __name__ == <font color="#0000ff">"__main__"</font>:
    web.wsgi.runwsgi <font color="#0000ff">=</font> <font color="#0000ff">lambda</font> func, addr<font color="#0000ff">=</font><font color="#808080">None</font>: web.wsgi.runfcgi(func,addr)
    app.add_processor(web.loadhook(session_hook))
    app.run()

只需要注释掉第二行即可,之后在终端下运行python <filename>.py,webpy会默认监听本地的8080端口,之后无论是通过浏览器还是其他方式访问相应地址即可

同时,还需要准备一份发起http请求的代码,python用来干这活最简单不过了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<font color="#0000ff">import</font> httplib
  
server <font color="#0000ff">=</font> <font color="#0000ff">"192.168.2.2:8080"</font>
url <font color="#0000ff">=</font> <font color="#0000ff">"/user/login?username=xxx@domain.com&password=111&rnd=33"</font>
  
<font color="#0000ff">class</font> perftest:
    <font color="#0000ff">def</font> __init__(<font color="#808080">self</font>):
        <font color="#0000ff">pass</font>
          
    <font color="#0000ff">def</font> run(<font color="#808080">self</font>):
        conn <font color="#0000ff">=</font> httplib.HTTPConnection(server)
        <font color="#0000ff">for</font> i <font color="#0000ff">in</font> <font color="#ff1493">range</font>(<font color="#009900">100</font>):
            <font color="#0000ff">try</font>:
                conn.request(<font color="#0000ff">"GET"</font>, url)
                rsp <font color="#0000ff">=</font> conn.getresponse()
  
                <font color="#0000ff">if</font> <font color="#009900">200</font> == rsp.status:
                    <font color="#ff1493">print</font> <font color="#0000ff">"headers"</font>, rsp.getheaders()
                    content <font color="#0000ff">=</font> rsp.read()
                    <font color="#ff1493">print</font> <font color="#0000ff">"content"</font>, content
            <font color="#0000ff">except</font> Exception, ex:
                <font color="#ff1493">print</font> <font color="#0000ff">"ERR:"</font>, ex
        conn.close()

跑了几次压力之后,webpy扛不住了…开始大面积抛异常,大致意思是python自带的logging模块出错,因为有频繁的文件打开关闭操作,某个操作导致文件句柄被非法访问…

在查看相关的log封装类之后发现,python的日志模块设计实在是有点不够内聚,也可以说不够彻底吧,不由得想起了之前用过的log4net,真是简洁啊

出错的原因在项目中开发同事封装的log模块多次执行了addHandler和removeHandler操作,据说在之前的开发过程中还出现过一行日志打印多遍的情况,而且随着程序的运行,相同的日志会越来越多…

顺手将该封装修改为单例类之后,故障解除

处理完之后接着跑压力,webpy还是会偶尔打印异常信息,不过已经变成了session访问异常,还好的是出现面积很小,于是开始增加压力测试的并发量,结果表明稳定度还是可以的,性能也在可以接受的范围之内

实验二:保持最终部署环境不变(nginx+fastcgi+webpy),继续压力测试

在上次测试解决完日志的问题之后,我开始以真实环境做相关的测试,要注意的是使用nginx之后,需要访问nginx.conf中配置绑定的相关端口,不再是webpy默认的8080端口

继续做上述的压力测试,性能也在可以接受的范围之内

实验三:模拟用户环境

根据测试MM所描述的,浏览器在B子系统出错过程中扮演了很关键的角色,于是我分别使用几个浏览器和纯粹的python代码分别访问相同的地址

几种组合的测试发现:

  1. 浏览器在只访问B子系统的情况下,一直表现正常
  2. 一旦浏览器访问过A子系统之后,再次访问B子系统即出现测试MM描述的404现象
  3. python代码一直访问正常

那么,我猜想问题原因应该在浏览器的某种特殊行为导致了该现象的出现,仔细想想,浏览器和python脚本的唯一区分在于浏览器有保存和发送cookie的行为,而python脚本没有,那么,可以抓包看看

Nginx+Fastcgi+Webpy惊险之旅

抓包发现,在只访问B子系统的情况下,http头的发送Cookie只有由webpy设置的set-cookie信息,即web_session_id字段,访问过A子系统之后,出现了其他的字段,即截图中的

  • loginUser=….;
  • xoxoyou@163.com=…

此时我才想起来A,B子系统部署在同一台机器上,而且代码都在同一个根目录下,于是A子系统的cookie信息便被浏览器发送到B子系统的webpy环境中解析

为了进一步证实我的想法,我在python脚本中直接使用socket方式,模拟了浏览器的http头信息,直接请求webpy的8080端口,注意,此时需要打开webpy的debug开关

果然,webpy马上报错了

Nginx+Fastcgi+Webpy惊险之旅

打开python库的相关代码,发现:

1
2
3
D:\Python27\Lib\Cookie.py
line <font color="#009900">247</font>
_LegalChars <font color="#0000ff">=</font> string.ascii_letters <font color="#0000ff">+</font> string.digits <font color="#0000ff">+</font> <font color="#0000ff">"!#$%&'*+-.^_`|~"</font>

合法的cookie key唯独就没有@符号…Nginx+Fastcgi+Webpy惊险之旅

总结

至此,bug的根源算是找到了,但我的心情却很是复杂,以往的代码review过程发现开发同事的开发习惯不是很理想,代码编写非常随意,结构比较混乱,由此带来的问题便是代码质量不高,bug频现,该怎么解决这个病瘤一直是我心中的痛…

同时,我们也发现,在服务器代码开发中一定要避免出现异常捕获,由此带来的性能问题和其他诡异的现象非常令人头痛,还有,谙熟相关标准显然也是非常重要的

相关推荐