ActiveMQ常見的問題和解決方法
1.先講嚴重的:服務掛掉。
這得從ActiveMQ的儲存機制說起。在通常的情況下,非持久化訊息是儲存在記憶體中的,持久化訊息是儲存在檔案中的,它們的最大限制在配置檔案的節點中配置。但是,在非持久化訊息堆積到一定程度,記憶體告急的時候,ActiveMQ會將記憶體中的非持久化訊息寫入臨時檔案中,以騰出記憶體。雖然都儲存到了檔案裡,但它和持久化訊息的區別是,重啟後持久化訊息會從檔案中恢復,非持久化的臨時檔案會直接刪除。
那如果檔案增大到達了配置中的最大限制的時候會發生什麼?我做了以下實驗:
設定2G左右的持久化檔案限制,大量生產持久化訊息直到檔案達到最大限制,此時生產者阻塞,但消費者可正常連線並消費訊息,等訊息消費掉一部分,檔案刪除又騰出空間之後,生產者又可繼續傳送訊息,服務自動恢復正常。
設定2G左右的臨時檔案限制,大量生產非持久化訊息並寫入臨時檔案,在達到最大限制時,生產者阻塞,消費者可正常連線但不能消費訊息,或者原本慢速消費的消費者,消費突然停止。整個系統可連線,但是無法提供服務,就這樣掛了。
具體原因不詳,解決方案:儘量不要用非持久化訊息,非要用的話,將臨時檔案限制儘可能的調大。
2.丟訊息。
這得從java的java.net.SocketException異常說起。簡單點說就是當網路傳送方傳送一堆資料,然後呼叫close關閉連線之後。這些傳送的資料都在接收者的快取裡,接收者如果呼叫read方法仍舊能從快取中讀取這些資料,儘管對方已經關閉了連線。但是當接收者嘗試傳送資料時,由於此時連線已關閉,所以會發生異常,這個很好理解。不過需要注意的是,當發生SocketException後,原本快取區中資料也作廢了,此時接收者再次呼叫read方法去讀取快取中的資料,就會報Software caused connection abort: recv failed錯誤。
通過抓包得知,ActiveMQ會每隔10秒傳送一個心跳包,這個心跳包是伺服器傳送給客戶端的,用來判斷客戶端死沒死。如果你看過上面第一條,就會知道非持久化訊息堆積到一定程度會寫到檔案裡,這個寫的過程會阻塞所有動作,而且會持續20到30秒,並且隨著記憶體的增大而增大。當客戶端發完訊息呼叫connection.close()時,會期待伺服器對於關閉連線的回答,如果超過15秒沒回答就直接呼叫socket層的close關閉tcp連線了。這時客戶端發出的訊息其實還在伺服器的快取裡等待處理,不過由於伺服器心跳包的設定,導致發生了java.net.SocketException異常,把快取裡的資料作廢了,沒處理的訊息全部丟失。
解決方案:用持久化訊息,或者非持久化訊息及時處理不要堆積,或者啟動事務,啟動事務後,commit()方法會負責任的等待伺服器的返回,也就不會關閉連線導致訊息丟失了。
3.持久化訊息非常慢。
預設的情況下,非持久化的訊息是非同步傳送的,持久化的訊息是同步傳送的,遇到慢一點的硬碟,傳送訊息的速度是無法忍受的。但是在開啟事務的情況下,訊息都是非同步傳送的,效率會有2個數量級的提升。所以在傳送持久化訊息時,請務必開啟事務模式。其實發送非持久化訊息時也建議開啟事務,因為根本不會影響效能。
4.訊息的不均勻消費。
有時在傳送一些訊息之後,開啟2個消費者去處理訊息。會發現一個消費者處理了所有的訊息,另一個消費者根本沒收到訊息。原因在於ActiveMQ的prefetch機制。當消費者去獲取訊息時,不會一條一條去獲取,而是一次性獲取一批,預設是1000條。這些預獲取的訊息,在還沒確認消費之前,在管理控制檯還是可以看見這些訊息的,但是不會再分配給其他消費者,此時這些訊息的狀態應該算作“已分配未消費”,如果訊息最後被消費,則會在伺服器端被刪除,如果消費者崩潰,則這些訊息會被重新分配給新的消費者。但是如果消費者既不消費確認,又不崩潰,那這些訊息就永遠躺在消費者的快取區裡無法處理。更通常的情況是,消費這些訊息非常耗時,你開了10個消費者去處理,結果發現只有一臺機器吭哧吭哧處理,另外9臺啥事不幹。
解決方案:將prefetch設為1,每次處理1條訊息,處理完再去取,這樣也慢不了多少。
5.死信佇列。
如果你想在訊息處理失敗後,不被伺服器刪除,還能被其他消費者處理或重試,可以關閉AUTO_ACKNOWLEDGE,將ack交由程式自己處理。那如果使用了AUTO_ACKNOWLEDGE,訊息是什麼時候被確認的,還有沒有阻止訊息確認的方法?有!
消費訊息有2種方法,一種是呼叫consumer.receive()方法,該方法將阻塞直到獲得並返回一條訊息。這種情況下,訊息返回給方法呼叫者之後就自動被確認了。另一種方法是採用listener回撥函式,在有訊息到達時,會呼叫listener介面的onMessage方法。在這種情況下,在onMessage方法執行完畢後,訊息才會被確認,此時只要在方法中丟擲異常,該訊息就不會被確認。那麼問題來了,如果一條訊息不能被處理,會被退回伺服器重新分配,如果只有一個消費者,該訊息又會重新被獲取,重新拋異常。就算有多個消費者,往往在一個伺服器上不能處理的訊息,在另外的伺服器上依然不能被處理。難道就這麼退回–獲取–報錯死迴圈了嗎?
在重試6次後,ActiveMQ認為這條訊息是“有毒”的,將會把訊息丟到死信佇列裡。如果你的訊息不見了,去ActiveMQ.DLQ裡找找,說不定就躺在那裡。