1. 问题
最近发生一个问题,当节点主备复制开启的时候,客户端疯狂超时,集群会突然脑裂(心跳不通了),而且之前的压测都没有出现这种情况,所以要查看原因。
2. 测试
结果客户端多次压测,发现服务端突然自己就卡住了,很明显STW,连上VisualVM监控,发现压测过程中,堆内存直接线性增长了,并且GC日志(使用G1回收)出现了:
2019-03-04T15:16:45.918+0800: 43.851: [Full GC (Allocation Failure) 2019-03-04T15:16:45.918+0800: 43.851: [Heap Dump (before full gc): , 86.9621601 secs] 8191M->4362M(8192M), 100.6275291 secs]
明显Full GC的问题,实锤了。
这次很聪明的先在Full GC前dump了堆内存。
3. 检测
jhat
不好使,贼慢,就用了Eclipse MAT。
Histogram显示出最大项居然是Object[]
,WTF?
然后进行泄漏检测,发现1个ThreadPoolExecutor
居然占了接近2GB的内存,不可接受,点开一看就发现自己写了一个非常低级的错误,看红圈就知道了。
4. REVIEW: ThreadPoolExecutor
这东西很熟悉了,不过再回顾一下。
这玩意初始化时会有以下几个参数:
corePoolSize
maxPoolSize
keepAliveTime
&timeUnit
workQueue
threadFactory
rejectHandler
除了threadFactory
不那么重要外,其它都蛮重要的。
代码规范:不要用工厂方法创建,直接用构造函数
这些参数组合可以构建不同类型的线程池,如单一线程池、固定数量多线程池、缓冲线程池等等
往线程池中添加任务的流程大家都很了解了,这里再重新回顾以下:
1
2
3
4
5
6
7
8
9
10
if (corePool.availableThread() > 0) {
corePool.pickThread().exec(task);
} else if (workQueue.isFull() == false) {
workQueue.put(task); //threads in core pool & max pool will fetch tasks from the work queue
} else if (maxPool.availableThread() > 0) {
newThread = createThreadAndExec(task);
maxPool.put(newThread); // thread will be alive determined by the keepAliveTime
} else {
rejectHandler.reject(task);
}
5. 问题所在
根据之前的设计,请求先由sc-boss
线程接管,由于多路复用(可参考Netty的线程模型),分配到sc-worker
线程。由于请求需要耗时,所以必须将请求再独立分到其它线程池中,这里有:
- 对于普通请求,分配到
request-scheduler
- 对于复制请求,分配到
replication-scheduler
不这么做,耗时的请求将会一直占用sc-worker
线程,造成该池队列堆积,新请求无法及时处理,且两类请求互相抢占,很可能造成某类操作超时(超时指仍然能连接,但是publisher#subscribe
迟迟收不到应答,实测复制请求会严重超时)等其它问题。此外,还可以让调试和性能监测更加容易。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Flux<Resp> handleReq(Flux<Req> reqs) {
// in sc-worker
return Flux.create(emitter -> {
reqs.publishOn(reqScheduler)
.onBackpressureBuffer()
.subscribe(req -> {
// handle reqs in request-scheduler
// will be locked when exec blockingQueue.put()
// ...
}, throwable -> {
// handle error in sc-worker
// no-lock & wait free
}, () -> {
// handle other when reqs flux is terminated in request-scheduler
// ...
})
});
// The Flux will be returned immediately.
}
无关的话说完,其实修复这个问题很简单,只要给workerQueue
设置一个上限参数,然后这里使用Aborted
拒绝策略,抛出异常,以便于subscriber
处理就行了。
结果是没有Full GC,一切变得正常了。
而实际上,之前的压测是将请求分批(30个)发送给服务端的,而目前的压测是将请求一个一个发出去的。很明显,线程池中队列的元素数量和请求数量成正比,前者明显要少很多,所以掩盖了这个问题。
6. 小结
细节最重要,有时候知道大致原理不代表真的懂,要对每个参数了如指掌。(这种低级错误不应该)