ConcurrentLinkedQueue-1次乱加锁引发的血案

背景:一个跑的好好的祖传埋点接收系统,最近突然出了问题,CPU使用率飙高,磁盘使用率开始飙升。


系统架构.png

先理一下业务系统的逻辑,埋点上报服务上报的埋点分json消息和zip包两种,一般都放入服务端的内存队列,内存队列超过阈值1w条之后,改为丢入磁盘,等队列空闲之后慢慢消费。
就这,为啥突然出问题了呢?

先扒tomcat配置,好家伙,清楚地写着,server.tomcat.maxThread=20,限制了每秒接口并发最多20个线程。秉着对系统的信任,先将其改为100。
but,没啥卵用。

dump下线程栈,发现大量线程处于等待状态。
我们知道,ConcurrentLinkedQueue是非阻塞队列,怎么会引起线程的等待呢?原来,这个系统自己基于ConcurrentLinkedQueue写了个内部队列,大概是这么个形式:

class MQ {
        private ConcurrentLinkedQueue 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了,在生产者生产消息进队列时,队列满了给他堵住,上线后经过漫长的几个小时,磁盘里堆积的百万消息终于给他消费完了。

完结,撒花、

你可能感兴趣的:(ConcurrentLinkedQueue-1次乱加锁引发的血案)