一个局部变量导致的血案
今天接到一个临时任务,排查一个网站的诡异问题,是这样的,这个网站访问量很大,上了一个模块,在页面服务端发出一个http请求,读取另一个java网站提供的数据,上线之后发现一旦存在并发,或是比较多的访问,http请求就会失败,甚至在服务器上不能打开任何页面,但是服务器可以被ping通,也可以ping通其它网址(我没有看到真实的情况,只是听说有这样一个情况)。
对于一个高并发网站的服务端发起一个http请求我总是觉得很恐怖,在以前的项目中也从来没这么做过(宁愿让客户端ajax方式请求),凭感觉一开始我就怀疑是线程池的问题。因为我们知道,asp.net的请求由线程池中的工作线程处理,如果在这个工作线程中同步发起http请求,那么就好比把一个内存级的处理速度一下子拉到了网络级的处理速度(因为需要同步等待网络),我想会不会是网站本身访问量就很大了(比如线程池最大允许工作线程800,当前的并发已经需要占用200),那么这个速度下来之后(比如从100毫秒处理时间到1秒),一下子线程池就爆满了(200×10=1000>800)。于是,我建议开发使用异步页和异步的HttpWebRequest,最简单的方式是使用WebClient基于事件的异步模式DownloadStringAync()。在开发人员按照我的建议修改代码上线之后,问题还是没有解决。我也一时很迷茫,想不出还会是哪些原因(我自己在线下测试了一下,使用这种方式即使很大的请求量始终占用一个工作线程,而IOCP则占用了比较多)。
在拿到了线上服务器调试的机会之后,修改页面代码植入了定时器,定时输出线程池的可用工作线程和可用IOCP,结果请求过来之后也只占用了30个工作线程(我们的网站30个并发左右),IOCP不占用(因为后来代码回滚了,没有使用异步的Http请求)。后来我想看一下tcp连接情况吧,输入netstat –s后我傻了,看到4000多个tcp连接,当时第一反映就是访问量太大了。在之前写程序的时候记得好像要通过注册表修改才能为windows server启用6万多个tcp端口(默认端口范围好象是1000多到5000,正好符合之前看到的4000多个连接),还记得tcp的端口关闭之后默认需要等待4分钟才能继续使用,查了一下是MaxUserPort和TcpTimedWaitDelay两个参数,根据msdn把这2个值修改为最大的65534和最小的30(秒)。算了一下,如果每个访问网站的请求都是新连接,每个请求又要建立一次http请求,那么一次请求也就占用2个端口最多了,算它最大能建立6万个端口,30秒全部释放,也就是30秒可以处理3万个请求,那么我们的处理能力是1000请求/秒,但是我们的网站一般每台服务器的并发不超过50,远远够用了。修改了注册表重启机器之后发现,果然修改生效了,不过建立的tcp连接在压力上来之后很快达到了6万多个,服务器又出现不能访问外网的问题了,此时断定根源问题就是tcp连接过多,已经不能再建立连接了(当然也就不能建立http连接),但是已经打开的页面确实可以打开,新的页面不能打开,又忽然想到了http的keep alive。
有了这个方向之后修改代码,为httpwebrequest设置了keep alive(为了确认是不是加上了connection:keep alive的http头又费了不少周折),为目标的web服务器也启用了keep alive,乱七八糟设置了一堆ServicePointManager的属性,经过几个小时百般尝试之后还是不行,总是发现服务器很容易达到6万个连接(也就1分钟不到吧),cpu狂飙,然后就像断网一样,一直纠结在为什么没有keep alive,为什么没有重用tcp连接。后来又单独制作了一个网站,发现并没有占用这么多端口(难道有其它服务或是模块开了很多tcp端口?)。
突然想到,为什么不看看到底建立了哪些连接呢?输入netstat –an –p tcp > c:\a.txt,之后打开a.txt一看瞬间傻眼,99%的连接都是到memcached的服务器。第一反应,代码里大量用到了memcached?查了代码之后发现,虽然一个请求可能确实进行了10次左右的memcache存取(不能说少,但是也不至于并发大到这个程度),但是我们用的客户端有连接池,不可能并发大到一下子建立这么多连接,而且我们的连接池最大的tcp连接也就是500。于是继续查看代码,在看到MemcachedClient的初始化代码后我瞬间石化,居然是每次都声明一个局部变量,然后初始化MemcachedClient类,而不是使用static变量来保存MemcachedClient的实例。
在之前调研memcache客户端连接池bug问题的时候了解到,每一次实例化一个新的MemcachedClient对象都会新建一套连接池,所谓池就是具有一个最小连接,也就是在初始化池的时候就会建立这些最小连接,达到比较好的初始性能,这个值我们配置的是10(每一次请求都要新建10个tcp连接??)。写了一段循环测试了一下才运行100次循环,建立的tcp连接就到了1000(并且每一次初始化连接池都耗时300毫秒以上,cpu始终是100%,可见创建这么多连接很耗性能),和猜想完全一致,也想到当时发现页面刷新一下连接就加了十几个是这样原因啊。在把局部变量改为全局的静态变量之后,网站在100个并发的情况下依然运行良好,远远大于原先的期望(50个并发)。
从线程池怀疑到端口不够用,再怀疑到keep alive问题,最后定位最终的原因(和新加的httpwebrequest模块没关系),费了不少周折。那么之后的解决方案很简单了,排查项目中其它memcache的使用方式,然后修改memcache客户端(我们使用的是Enyim.Caching),修改MemcachedClient的构造方法为private的,并且提供singleton入口即可。
对于性能优化我的体会是,往往大多数的性能问题来自一到两个根源问题,如果能找到并解决那么或许可以有很大的效果。希望此文对大家有帮助。
![](https://img2020.cnblogs.com/blog/103060/202004/103060-20200402094642952-518549746.jpg)