总结一下遇到过的网络同步IO导致服务阻塞的问题
在上家公司曾写过这样一个服务,用户通过我的应用(以下简称fri_svr)索取自己的好友信息,而fri_svr需要向第三方平台(如:人人,Facebook)通过http协议批量请求用户数据,由于用户数据可能很大(几k几十k的级别),所以整个req/rep的过程通常会很慢,平均大概会在 1s – 10s 之间,这样当瞬时请求量到一定级别后,就会造成fri_svr的内存暴涨且响应不了前端的请求,原因在于fri_svr会对前端的每个请求hash到(根据user_id)专门用于http请求的线程队列中(也即是one thread per queue模型),当前端向fri_svr的请求速率大于平台响应fri_svr的,那么就会造成fri_svr中队列的积攒,内存的暴涨,且无法在超时时间内响应前端请求。
上述这个场景的问题就在于同步IO,也即是我们的逻辑代码段在发起向第三方的请求后,必须阻塞到(除了超时)平台成功返回大批用户数据后才能继续执行之后的逻辑,如果可以的话,我们希望这个世界上所有的程序都是异步的,但往往又有许多理由和处境让我们在眼下无法使用异步(原因千奇百怪此处略去各种吐槽)。当时解决这个问题的方法就是简单粗暴地增加IO线程数,由于该服务几乎没有额外的cpu开销,只是简单的 组包 和 hash,所以线程数即使开到了1000的级别导致上下文切换增高,也不会影响整体的服务响应速度。
第二个要说的场景是目前这家公司遇到的一个问题,又是由于某种原因,项目某部分的数据服务和客户端的数据传输使用的是ICE(一种基于声明式语言的跨语言RPC网络通讯引擎)的同步方法,好了,问题又来了,虽然这里少了与第三方平台的交互,但是和客户端之间的同步响应必定会在请求量达到某个临界后出现响应缓慢或超时的问题,且该服务又承载着一些至关重要的cpu计算,如果像上一个场景一样单纯地调高线程数一定会顾此失彼。
在调试的过程中,我发现有些用户可以及时收到响应,有些用户却会出现几分钟收不到响应的情况,基于这一点很容易就想到是某个线程队列中的某个用户的任务阻塞了,所以导致该队列中后续其他用户的任务也被影响,而hash到其他线程队列的用户则不会被影响。
我的解决思路是改变线程队列的模型,采用单队列多线程(multi thread per queue)的任务驱动方式(在我的观念中这也即是最传统的ThreadPool实现方式),这样就能有效地解决在单个task执行意外阻塞时,后续的任务能被其他的 active 线程执行,唯一的问题就是如何保证任务执行的顺序性,因为很有可能队列中的两个任务是属于同一个用户的,而用户的任务基本都会有严格的执行顺序,虽然任务从queue中pop出的顺序是一致的,但被不同的线程取得后很可能被无序地执行。
首先我们先确定,在queue中的任务是顺序存放的,问题在于被各个线程取出之后线程的调度存在不确定性。
其次我们考虑,每个任务必定有一个owner,所以才会需要被有序的执行
基于以上两点,那么我们只要保证线程从queue中pop出的任务在当前时刻必定没有其他线程在执行拥有相同owner的任务
这样问题就能有效地解决了,只要封装一个数据结构(注意,这个数据结构必须是线程安全的),内含一个task queue(list/queue)和一个KV(hash table/map/bitmap),task queue中为存放的任务,而KV结构中的key为owner, val为true/false(表示这个owner的任务当前是否在执行),每次从该数据结构中pop出任务的同时,其内部会对KV设置owner的状态为正在执行(true),而当线程执行该任务完毕后,必须主动通知数据结构更新KV中的状态为false(这里必须小心处理,c++利用析构,java利用finally)。
这样一来我们pop任务的逻辑也会作相应调整,不再是以前那样依次pop head,而是遍历到所属owner的状态为false的那个任务
OK,这样所有问题就解决了,但记得它不是万能的,这种方案在性能上必定存在一些损失
其一是在于为了保证顺序,每次更新KV都会做相应的check/set操作
其二是某些multi thread per queue模型的实现在上下文切换上会比one thread per queue相对更高,而且更多的线程对锁的竞争也会更激烈
如果任务都是属于时间可控的cpu消耗性任务,那么应该优先考虑one thread per queue的模型来做
好,就到这, 希望对大家有帮助
我的个人博客地址:www.cppthinker.com
欢迎大家交流:)