背景:一个跑的好好的祖传埋点接收系统,最近突然出了问题,CPU使用率飙高,磁盘使用率开始飙升。
系统架构.png
先理一下业务系统的逻辑,埋点上报服务上报的埋点分json消息和zip包两种,一般都放入服务端的内存队列,内存队列超过阈值1w条之后,改为丢入磁盘,等队列空闲之后慢慢消费。
就这,为啥突然出问题了呢?
先扒tomcat配置,好家伙,清楚地写着,server.tomcat.maxThread=20,限制了每秒接口并发最多20个线程。秉着对系统的信任,先将其改为100。
but,没啥卵用。
dump下线程栈,发现大量线程处于等待状态。
我们知道,ConcurrentLinkedQueue是非阻塞队列,怎么会引起线程的等待呢?原来,这个系统自己基于ConcurrentLinkedQueue写了个内部队列,大概是这么个形式:
class MQ {
private ConcurrentLinkedQueue<Message> queue = new ConcurrentLinkedQueue<>();
private Lock lock = new ReentrantLock();
private static final int QUEUE_MAX_SIZE = 10_000;
public void putMessage(Message message) {
lock.lock();
try {
//队列大小到达门限时不再放入消息,转存磁盘
if(getMQSize() < QUEUE_MAX_SIZE) {
queue.add(message);
} else {
saveToDisk(message);
}
} finally {
lock.unlock();
}
}
public Message getMessage() {
Message message;
lock.lock();
try {
message = queue.poll();
} finally {
lock.unlock();
}
return message;
}
public int getMQSize() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
}
我天,直接加了个粗粒度的锁,将并发容器给堵住了。而且,每次放消息,都要查一次容器大小,该方法可要遍历整个容器的对象,这个性能可太低了啊。既然如此,那为什么不用BlockingQueue呢?
当时本着少改少错的原则,并没有直接上,而是先去掉锁,并在类内维护一个原子累加器,虽然不会特别精准,但这种场景下,内存队列放1w个对象和9500个对象差不了多少,毛估估的一个值,主要是怕把内存撑爆。
第一版就这么上了,然而,机器又挂了。这又是咋回事?
原来,从磁盘加载消息进内存队列的速度太快,应用启动瞬间,队列里放了几十万对象了,dump文件分析,就这个队列对象就占了1.2G了。失败!
第二版还是老老实实上BlockingQueue了,在生产者生产消息进队列时,队列满了给他堵住,上线后经过漫长的几个小时,磁盘里堆积的百万消息终于给他消费完了。
完结,撒花、