本文会描述在工作中遇到的一个多线程HTTP Server模型出现的重复投递现象的解决过程,希望能在给各位同行带来一些启迪和新的思路。
首先描述一下问题:这个Server是针对一个后台程序而做的,Server本身是一个成熟组件,经过考验的。现在出现的问题是,模拟了1000次请求,Server内部的handle模块收到了1000+的请求,经过跟踪,确定问题出在Server的监听和Handle调用之间,这里的Queue是一个无界队列,会不断扩充自己来处理接受到的请求,Executor也会首先启动10个core Thread来处理请求,当10个线程都忙的时候,会再次创建不超过10个线程来处理上述请求。如果20个线程全忙,那么任务会在等待keepAliveTimeout(这里是10)的时间以后,reject掉。
一下子,问题回到了原点。Server真的是可靠的么?
经过半夜的电话骚扰,Server从出生到现在,没有经历过我们这么高频次的折磨。我们的请求间隔是30ms,而Server的设计要求是能够处理每秒30次请求,于是我们就都囧了。
就差这几毫秒啊……
好吧,这里的多线程重复投递,也有了解释,无法处理的请求被拒绝,而发送方又重新抛了过来,这次被处理,又或者是被再次拒绝。但是,线程的世界里,是无法准确度量的,世界毕竟是测不准的,有那么几个请求实际已经处理了,Server还是标记为未处理状态,于是乎出现了重复投递。
解决了这个问题,在两者之间增加了中间层,自主校验请求是否被处理过,没处理就处理,否则直接返回上次处理结果。对,这可以算是一个cache,一个永远不expire的cache。
后续问题又出现了。。。请求没有被丢弃或者是重复处理,但是发现理论计算的结果,和实际处理结果出现了偏差,有一些处理结果是错的,是程序的初始值。那么,既然没有多投递,代码也执行了,而且根据人工分析,代码的确不会走这样的路径,那么,这一切到底是为什么?
简单的说,int i = 5;i += 4;return i; 应该是9,是吧。
但是我有几个请求获取到的是5.看似绝对不可能,但是实际已发生。
怎么解决,放在我面前的第一大问题。有的同学会说,快,快,加快处理速度,让程序跑的更快。我很负责任的说,这个思路第一时间就被我毙了,如果不是因为跑的太快了,又怎么会跑进岔路呢,呵呵。记得之前还遇到过一个问题,也是线程相关的,for循环启动线程,C++的代码。应该启动10个,但是每次都有那么几个,怎么都起不来,那次的解决方案给了我很深刻的印象,一个1986年开始写代码的高人,一语道破天机。至于是什么,稍后,稍后。
众所周知,线程在运行,有个运行状态,比如挂起之类的。从内存角度来说,线程调用一个方法,大体上是丢个入参进去,然后等待判断处理,最后ret或者是到指定内存去pop一个值。那么为什么有的是5,有的是9呢?这就涉及到一个很关键的问题,CPU时间片。
线程是需要CPU去调度的,而CPU不是神,处理能力是有限的。Linux和Windows在这一点上,处理逻辑基本是类似的,将CPU在某一个瞬间内的处理能力看成一个可用资源,也就是时间片。时间片是CPU分配给各个程序的时间,每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
回到我们具体的java代码里来。handle是被Server以多线程来调用的,那么也就存在一个问题,时间片争用。CPU分什么时间片给你,上层应用是不会知道的,上层应用只管在一定时间内,你必须给我结果。而有一些线程在这时候没忙完呢,甚至于没有轮到它来执行,或者执行了一半,都是有可能的。
那么不管执行了多少的percent,想得到9是没可能了。那么5是哪里来的?
int i = 5;这句话翻译到汇编里,毋庸置疑,肯定是开辟一个空间,然后丢个5进去。在调用方法之前,方法签名上会明确说明返回的是什么类型,在哪里搞这个东西。如果执行完了,那么返回的内存地址里是9.可是如果刚放了个5进去,就被要求返回了呢?
从高级语言的角度来说,没的想的,肯定是5.(当然,你可以volatile或者是synchronization一下,强制读取内存或者强制同步,尽量减少这个情况发生,但是只能是尽量,因为你在和CPU比快,在和ring 0的东西比谁胳臂粗,呵呵。)
但是我要保证基本不出现这个问题,怎么办?
揭晓答案。
Thread.sleep(200);//为多线程而暂停- -
想不到吧,不是要更快,而是要慢一些!
为什么要sleep,想不明白的可以参看我上面对于时间片的描述。出现方法没执行完就ret出来的情况,不是因为跑的不够快,而是你跑的太快了,快到抢占了别人的资源,明白不。。。
简单的举例,大家都拥挤着上Bus,估计极端情况下,等个十几年也上不去,一个不让一个,又都是一样壮,这还怎么搞。谁也打不过谁,也挤不动谁,等呗,等到不能等呗。。。
排个队不就好了。按顺序上,又快又好,最简单的顺序就是,FIFO。
这也同样适用于上面描述的场景。每个任务处理完毕以后,暂停一段时间,是自己强制CPU休眠,而现代CPU的时间片不可能长达200ms,那就要出事了。sleep指令翻译到汇编上,多数是用无数个nop代替,也就是机器码的90.CPU看到一坨90,是不会再搭理你的,只会让你睡吧,睡爽点,给贴个休眠中的标签就完事了。这么一来,不就可以让出资源给其他线程了?Executor调度线程是根据线程的status来调度,也就是sleep或者是running,或者其他。处理完了以后,主动强制自己进入休眠状态,让出资源给其他线程使用。而由于所有线程都会这么做,那么相对而言,只要thread的maxSize充足,是不会出现不够用的情况,也就不会出现争用。
那还担心出现执行一半代码就ret么?不用了吧。
所以,记得,写代码,不是越快越好,也要记得放慢脚步,看看风景。生活多美好,不要天天敲。