HttpClient异步调用引发的程序挂起问题排查及解决

      在搭建搭建分布式系统时,基础组件与框架的重要性不言而喻。但是如果组件出现bug,真的很要命。虽然我们通过各种单元测试,拼命找bug,但是总有一些问题被盲目自信蒙蔽了双眼,很多时候我们认为这段代码100%没有问题,但是我想说,没有100%没有问题的代码,只有你没想到的应用场景。下面就说一下最近技术组件出现的一次离奇的故障。

      开始之前,先看看这个服务的压力,大约每分钟3700左右的样子,折合成TPS也就不到100的样子。

image

      问题现象是,当服务程序重启后,系统一直没有结束的任务,并且线程持续增长,直到线程数增长到最大。

  image

      通过Dump分析发现,所有的线程都被一个线程阻塞,阻塞线程18.

image

      进一步分析18号线程的堆栈发现,SetDatabaseConfig方法创建了一个Task,一直等待没有返回。

    image

      OK,看到这里,立马看一下程序的代码吧。代码中一个web请求,并且没有超时时间。

image

      分析了这点代码后,简单整理了一下整个逻辑的调用过程。大体如下:

image

      说明一下,获取数据库连接时,有一个全局的程序锁,锁中的逻辑是通过HttpClient远程访问WebAPI获取连接信息,并缓存到本地,上面的源代码就是在请求配置数据。通过上图我们可以看出,在HttpClient返回前,所有线程池中执行的任务,如果与数据库访问有关,都会被挂起。并且此时RPC服务持续受到请求,收到请求很快会被RPC服务消费并创建线程后交给线程池。这样可以解释为什么TeldHost.exe的线程持续增长的原因了。

      分析到这里,还是毫无头绪,第一次调用加锁了,没有并发请求,按理说很快就可以返回了。百思不得其解,干脆写个模拟程序测测获取数据库连接的方法吧。

image[29]

      通过模拟程序测得,10并发时1s左右所有调用都完成了,100并发时需要9秒多,1000并发时需要350s。尼玛,果然有问题。通过review里面的代码,极度怀疑是HttpClient搞的鬼。通过翻看HttpClient的源代码,我们会发现,后台是通过Task实现的异步请求。看到这里恍然大悟,典型的HttpClient发起请求后,创建的task ,但是task拿不到线程的执行权引起的线程资源竞争。因为来自前端的压力一直很大,一直没有空闲线程,HttpClient一直在处理阻塞等待状态。

      果断把HttpClient异步调用改为同步调用,通过多次压测验证,问题得到解决。下面是修复后的代码。

image

posted @ 2017-01-17 12:15  凌晨三点半  阅读(6526)  评论(2编辑  收藏  举报