Warning
這篇文章提出來的 fix, 等於 patch ActiveMQ-5.8.0 的程式, 而不是 ActiveMQ-5.9.0 的程式.
Reference
Problem
ActiveMQ 從 5.5.1 升級到 5.9.0 之後過一個禮拜,
ActiveMQ 把一台 server 的八核 CPU 都吃滿, 達到 100% 使用率.
查 ActiveMQ-5.9.1 的 release note, 發現在 AMQ-4853 有在解相關的問題.
這個問題主要是說 ActiveMQ 在 5.9.0 有個修改. 這個修改的 comment 是:
這個問題主要是說 ActiveMQ 在 5.9.0 有個修改. 這個修改的 comment 是:
Just use a concurrent linked queue, makes the code much simpler and目的是為了讓程式變簡單, 理論上不影響效能. (不過看起來的確 hurt performance)
doesn't hurt performance overall
Reproduce
- 部屬 ActiveMQ 兩台 cluster (一台不容易重現)
- 執行程式去打 ActiveMQ
package test; import java.util.Enumeration; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.jms.Connection; import javax.jms.JMSException; import javax.jms.QueueBrowser; import javax.jms.Session; import javax.jms.TextMessage; import org.apache.activemq.ActiveMQConnectionFactory; import org.junit.Test; public class OccupyCPUTest { @Test public void test() throws JMSException, InterruptedException { final ActiveMQConnectionFactory f1 = new ActiveMQConnectionFactory("tcp://192.168.0.1:61616"); final ActiveMQConnectionFactory f2 = new ActiveMQConnectionFactory("tcp://192.168.0.2:61616"); ExecutorService e = Executors.newCachedThreadPool(); for ( int i = 0; i < 40; i++ ) { newRun(f1, e); } for ( int i = 0; i < 40; i++ ) { newRun(f2, e); } TimeUnit.DAYS.sleep(1); } protected void newRun(final ActiveMQConnectionFactory f, ExecutorService e) { e.execute(new Runnable(){ @Override public void run() { try { Connection c = f.createConnection("id", "pwd"); c.start(); Session session = c.createSession(false, Session.AUTO_ACKNOWLEDGE); while (true) { QueueBrowser b = session.createBrowser(session.createQueue("test"), "key='val'"); Enumeration<textmessage> enumeration = b.getEnumeration(); while (enumeration.hasMoreElements()) { enumeration.nextElement(); } b.close(); TimeUnit.MILLISECONDS.sleep(50); } } catch (Throwable ex) { ex.printStackTrace(); } }}); } }
- 用 jconsole 或 jvisualvm 監控兩台 ActiveMQ 的 CPU, 可以看到 CPU 慢慢往上升, 直到 100%
What Happened?
這個測試程式其實就是起 80個 thread, 每個 thread 每 50 millis 去 browse 一次 ActiveMQ.
而每次 browse message, ActiveMQ 都會建立內部的 consumer, browse 之後 ActiveMQ 又會 removeConsumer.
回到 AMQ-4853, 以前的 AdvisorySupport#removeConsumer 會從 ConcurrentSkipListMap put or remove ConsumerIdKey & ConsumerInfo. 注意 ConsumerIdKey 有保留 Consumer 進入的時間先後順序, 程式的 comment 提到:
意思是說這段程式的目的是要確保 consumer 的 advisory message 會照順序的被發出去.// replay consumer advisory messages in the order in which they arrive - allows duplicate suppression in// mesh networks with ttl>1
不過這樣的處理方式, 後來被覺得程式比較複雜, 所以直接將 ConcurrentSkipListMap 改為 ConcurrentLinkedQueue, 然後把一堆 ConsumerIdKey 的處理拿掉.
結果問題出現, 當 consumer 頻繁的上下線, 會造成 ConcurrentLinkedQueue#remove 被呼叫很多次, 造成 CPU 節節升高. 只要把 jvisualvm 打開 monitor 就可以發現 bottleneck 就在 #remove 這個 method 上.
當這個問題在 issue 上被提出來質疑 ConcurrentLinkedQueue 是個不好的做法時, 該 developer - Timothy Bish 認為 ActiveMQ 的使用者不該讓 consumer 太常上下線.
You should investigate why all the consumers are coming and going so often -Timothy Bish最後有個 user 研究後發現 ConcurrentLinkedQueue<ConsumerInfo> 的 ConsumerInfo 沒有實作 equals & hashCode, 這樣會讓 ConcurrentLinkedQueue#remove 變很慢, 所以 Tim 就把這個 improvement 放到 trunk & 5.9.1. 結束 AMQ-4853.
ActiveMQ Classpath
我們使用 ActiveMQ 的 batch 檔開 ActiveMQ.
ActiveMQ 使用 wrapper service 起動, 起動方式會先載入 activemq.jar, 裡面有個 main class, 這個 class 會去載相關的 library.
把這個 main class decompile 後看到
app.addClassPathList(System.getProperty("activemq.classpath"));看起來可以設定 activemq.classpath, 改了 AdvisoryBroker 後包成一個小 jar 檔, 然後在 wrapper.conf 裡面設定 -Dactivemq.classpath=${patch jar path} 就可以了.
Problem Fixed?
放上這個 issue 的 fix (就是實作 ConsumerInfo 的 equals & hashCode) 測一次, 效能比較好的那台機器似乎 CPU 維持平穩, 但比較差的那台機器就撐不過去.
我後來把 ConcurrentLinkedQueue 改為 ConcurrentHashMap 就可以通過測試.
Final
原本有點擔心 ConcurrentHashMap 的實作無法滿足 replay consumer advisory message 的需求, 不過查一下 ActiveMQ-5.8.0 的程式, 發現本來就是使用 ConcurrentHashMap. 看來 ConcurrentSkipListMap 與 ConcurrentLinkedQueue 都是在 ActiveMQ-5.9.0 加 & refactor 的.
在 ActiveMQ-5.9.0 把 ConcurrentHashMap 改為 ConcurrentSkipListMap & ConcurrentLinkedQueue 的原因是 AMQ-2327 - Consumers left hanging in the large network of brokers
在我們的 case 裡面, 還沒有那麼多台 ActiveMQ, 而且使用上也可以分成很多獨立的 ActiveMQ 使用, 看起來使用 ConcurrentHashMap 在我們的環境不會遇到 AMQ-2327 的 issue 才對. 所以最後我就選擇把 ConcurrentLinkedQueue 改回使用 ConcurrentHashMap 了.
在我們的 case 裡面, 還沒有那麼多台 ActiveMQ, 而且使用上也可以分成很多獨立的 ActiveMQ 使用, 看起來使用 ConcurrentHashMap 在我們的環境不會遇到 AMQ-2327 的 issue 才對. 所以最後我就選擇把 ConcurrentLinkedQueue 改回使用 ConcurrentHashMap 了.
沒有留言 :
張貼留言