等等!這兩個 Spring-RabbitMQ 的坑我們已經替你踩了

餓了麼物流技術團隊發表於2018-10-15

豔傑。擅長 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 引數 corePoolSizemaxPoolSize 極為接近,查詢相關資料發現,多個 queue 的 consumer 會共用 taskExecutor 的執行緒池數量,如果執行緒池數量不足,consumer 無法建立。 後發現官方文件已有明確說明.

官方文件

解決辦法

增大 task-executor corePoolSizemaxPoolSize 的值為 200,重啟服務,解決。

總結

  1. 在 spring 容器管理中,ID 是一個物件的引用,必須僅且只有一個無重複的 ID, 包含 Bean 及其他自定義標籤(如 rabbit:listener), 即使在不同檔案也需要注意。
  2. spring-rabbit task-exeutor 預設使用 SimpleAsyncTaskExecutor 作為非同步執行緒池,每次請求新開執行緒,沒有最大執行緒數設定. 其不是真的執行緒池,這個類不重用執行緒,每次呼叫都會建立一個新的執行緒。可改用 ThreadPoolTaskExecutor 替代,但存在多個 queue 時需要 ThreadPoolTaskExecutor 執行緒池足夠可用,尤其針對多個 listener-container 共用一個 task-executor 的情況。




閱讀部落格還不過癮?

歡迎大家掃二維碼通過新增群助手,加入交流群,討論和部落格有關的技術問題,還可以和博主有更多互動

等等!這兩個 Spring-RabbitMQ 的坑我們已經替你踩了

部落格轉載、線下活動及合作等問題請郵件至 shadowfly_zyl@hotmail.com 進行溝通

相關文章