豔傑。擅長 Python 與 JAVA , 現任餓了麼物流團隊資深 Python 工程師,負責分流核心鏈路, 專注於系統業務分析及穩定性建設。
上次我們分享了我們團隊Java應用Docker化部署GC變長的踩坑經歷,發現還真的幫助很多同學解決了他們專案中同樣的問題。這對我們來說真的是很大的一個激勵,所以我們決定後面會不定期分享一些我們團隊的踩坑經歷,今天分享的是 Spring-RabbitMQ consumer 的兩個坑,希望能對大家有所幫助。
坑 No.1: consumer 假死,無真正的消費能力
背景
spring-rabbit 版本變更至 1.6.2.RELEASE
現象
consumer 數量正常,mq 控制皮膚的 prefetch
引數始終是1, 訊息無法正常 ack, 佇列處於假死狀態,
系統報異常 org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException
[2018-09-09 10:31:27.27]RuntimeException-org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException [ ERROR] [ Elog ]
Execution of Rabbit message listener failed. org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException: Listener threw exception
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.wrapToListenerExecutionFailedExceptionIfNeeded(AbstractMessageListenerContainer.java:870)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:780)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:700)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$001(SimpleMessageListenerContainer.java:95)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$1.invokeListener(SimpleMessageListenerContainer.java:187)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.invokeListener(SimpleMessageListenerContainer.java:1187)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:681)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:1165)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:1149)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$1100(SimpleMessageListenerContainer.java:95)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1312)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.springframework.amqp.AmqpIllegalStateException: No default listener method specified: Either specify a non-null value for the 'defaultListenerMethod' property or override the 'getListenerMethodName' method.
at org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter.onMessage(MessageListenerAdapter.java:291)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:777)
... 10 more
複製程式碼
異常原因
ListenerExecutionFailedException
根據官方文件說明,是 consumer 在訊息消費時發生了異常, 預設情況下,該訊息會被 reject, 並重新回到佇列中, 但如果異常發生在到達使用者程式碼之前的異常,此訊息將會一直投遞。
org.springframework.amqp.AmqpIllegalStateException
該異常丟擲,RabbitMQ無法收到訊息回應,將一直處於等待狀態。
No default listener method specified: Either specify a non-null value ...
找不到 consumer OnMessage
的方法, 猜測是類載入錯誤,於是重新開始檢查 consumer 類定義,最終發現
rabbit:listener
的 ID 和 真實 consumer 的 ID 衝突,導致真實的 consumer Bean 無效,找不到接受訊息的方法,而 rabbitmq 在與 client 端通訊過程中發生異常,會停止消費。
<bean id="consumer_1" class="me.ele.Consumer1"/>
<rabbit:listener-container connection-factory="galaxyConnectionFactory" acknowledge="manual" concurrency="16" >
<rabbit:listener queues="queue_1" ref="consumer_1" id="consumer_1" />
</rabbit:listener-container>
複製程式碼
解決辦法
刪除 rabbit:listener
的 ID 屬性
坑 No.2:consumer 數量異常
背景
原服務有 6 個佇列,每個佇列起 10 個 consumer, task-executor
執行緒池設定為 90, 後因資料量增加,發現消費能力不足,決定增加 consumer 數量,調整 listener-container 的 concurrency 為 20,重啟伺服器。
現象
部分佇列 consumer 數量不足,缺失項始終為 xml 中宣告在後的佇列
異常定位
回退 concurrency 引數為 10 異常消失,觀察異常現場,發現 consumer 消失佇列有先後順序之分,且 consumer 數量存在上限值為 90,猜測是收引數限制,檢查配置引數如下:
<bean class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"
id="taskExecutor">
<!--核心執行緒數 -->
<property name="corePoolSize" value="16"/>
<!--最大執行緒數 -->
<property name="maxPoolSize" value="16"/>
<property name="queueCapacity" value="500"/>
<!--執行緒池維護執行緒所允許的空閒時間 -->
<property name="keepAliveSeconds" value="60"/>
<!--執行緒池對拒絕任務(無執行緒可用)的處理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
</property>
<property name="WaitForTasksToCompleteOnShutdown" value="true"/>
</bean>
<rabbit:listener-container connection-factory="monitorConnectionFactory"
acknowledge="manual" task-executor="taskExecutor" prefetch="10" concurrency="20">
<rabbit:listener queues="queue_1" ref="consumer_1"/>
<rabbit:listener queues="queue_2" ref="consumer_2"/>
<rabbit:listener queues="queue_3" ref="consumer_3"/>
<rabbit:listener queues="queue_4" ref="consumer_4"/>
<rabbit:listener queues="queue_5" ref="consumer_5"/>
<rabbit:listener queues="queue_6" ref="consumer_6"/>
</rabbit:listener-container>
複製程式碼
異常原因
經排查對比,發現上限值與 task-executor ThreadPoolTaskExecutor 引數 corePoolSize
、maxPoolSize
極為接近,查詢相關資料發現,多個 queue 的 consumer 會共用 taskExecutor 的執行緒池數量,如果執行緒池數量不足,consumer 無法建立。
後發現官方文件已有明確說明.
解決辦法
增大 task-executor corePoolSize
和 maxPoolSize
的值為 200,重啟服務,解決。
總結
- 在 spring 容器管理中,ID 是一個物件的引用,必須僅且只有一個無重複的 ID, 包含 Bean 及其他自定義標籤(如
rabbit:listener
), 即使在不同檔案也需要注意。 - spring-rabbit task-exeutor 預設使用
SimpleAsyncTaskExecutor
作為非同步執行緒池,每次請求新開執行緒,沒有最大執行緒數設定. 其不是真的執行緒池,這個類不重用執行緒,每次呼叫都會建立一個新的執行緒。可改用ThreadPoolTaskExecutor
替代,但存在多個 queue 時需要ThreadPoolTaskExecutor
執行緒池足夠可用,尤其針對多個 listener-container 共用一個 task-executor 的情況。
閱讀部落格還不過癮?
歡迎大家掃二維碼通過新增群助手,加入交流群,討論和部落格有關的技術問題,還可以和博主有更多互動
部落格轉載、線下活動及合作等問題請郵件至 shadowfly_zyl@hotmail.com 進行溝通