最近项目上遇到好几个崩溃问题,解决过程有点曲折,在此记做个记录。
项目背景介绍:该项目为语音识别实时分析系统,整套系统架构如下:
接连几次崩溃的是中间的语音流接入系统,崩溃的情况如下:
1、打开文件过多报错,导致系统直接卡死。
2、打开线程过多,导致系统直接崩溃。
3、Jetty容器异步支持bug。
第一次崩溃:打开文件过多
首先在日志中大量的刷屏,因为我们的语音流接入系统只是一个中间转发的服务,这个服务当时是从实时语音分析服务中剥离出来的,当时剥离出来的主要目的是降低实时语音分析服务的带宽压力,所以当出现这个问题后,直接指向的是有网络连接没有释放。
既然确定了排查方向,使用lsof命令,好家伙,该进程直接占了六万多个文件句柄,其中eventpoll占了一万六千多个,打开的pipe有三万三千多个,就这两项就占了近五万个句柄。项目上部署的这套系统最高并发为预计的3000路通话,即使在最高通话并发的情况下,也不可能占用这么多句柄数,所以情况就是有连接没有释放,导致句柄泄漏,并逐渐累积到这个数目,验证这个情况,使用netstat,果然发现大量的连接一直没有释放。
好了,锁定了目标,接下来就是排查代码中没有正确释放的地方。
如最上,一通新通话进来时,我们的语音流接入系统会接入两个语音流并发送给语音识别服务进行识别,在这个过程中,语音流发送是一个持续的过程,并且我们要确保同一个语音流由同一台机器进行识别。所以在新语音流进来时,我们的接入系统与识别服务之间会创建一个session,当通话结束时销毁这个session,这个session在我们的语音流接入系统(以下简称接入系统)中是和语音流ID即streamId一一对应的,在一个流推送结束后我们要根据streamId进行session的关闭。结果在代码中有一个地方,本来应该是传streamId的,但是结果却传成了toString(这个错误很低级!),好了,找到这个地方修改后,项目重新上线。(可是幸福不会来的这样突然!)
第二次崩溃:打开线程过多
当上面以为问题解决后,第二天线上直接报出进程崩溃的问题,查看崩溃日志,里面大量的线程阻塞,一个进程居然有三万个线程。
遇到这个情况也只能结合代码去分析这些线程是在哪里起的了。因为这个接入系统只是一个中间商,所以起线程的地方只有三个,一个是接入语音流的地方,一个是接收识别结果的地方,剩下的就是推送识别结果。查看语音抓包系统并发数正常,而识别结果推送是同步的,但是我们在接收识别结果的时候采用的是异步接口,而每收到一个识别结果的时候都会当作一个任务加入线程池等待执行。那么这时,积压只能是在接收识别结果这里了。(说明:在前面通过打时间戳的方式已经确认过了接入系统和分析服务之间发送和接收速度不一致,因为分析服务拿到识别结果后还会有后续的模型、流程分析处理,所以这就是一个典型的快生产者慢消费者问题。)
针对上面的分析结果,确认是消费者过慢问题,那么快生产者就应该进行控制,查看代码,发现在处理接收的识别结果的时候,我们使用的线程池是newCachedThreadPool,所以因为这个原因,当分析服务这边接收过慢时,接入系统在接收识别服务的识别结果时就只能创建大量的线程去等待执行。针对这个情况,所以改为使用newFixedThreadPool。(还有就是如果消费者过慢的话,提高消费者处理能力才是正解,所以后面也有对分析服务的优化,提高响应时间。)
所以在使用生产者消费者模型的时候,可以有快生产者慢消费者存在,但是两者之间的处理速度不应该相差过大,更不能说是没有消费者(当分析服务崩溃或者阻塞就是这种情况。)
第三次崩溃:Jetty容器异步支持bug
再经过上面两次bug修复后,以为问题彻底解决了,但是还是同样的到项目上跑上一天后,又出现了崩溃问题。对于这一次从日志里面分析,还是文件句柄占用耗尽而崩溃,分析这些链接,发现还是我们的接入系统和识别服务之间有大量的连接没有释放。这让人很疑惑,经过最终的确认,所有的连接之间都有得到正确的释放。然后注意到了之前一直被忽略的一条错误日志:
最终确认jetty容器在抛出该异常后,会导致异步回调永远得不到调用,这样的话就会使得我们的接入系统和识别服务之间的连接可能因为异步回调没有得到调用而导致连接得不到释放。(当时使用的jetty版本是9.4.12)
线上的每一次崩溃都让我的小心脏跳动加速一倍,活着不易,且行且珍惜!