Share Notes

chundev

View the Project on GitHub latteouka/share-notes

INFINITIX AI-Stack 部署踩到的 Apache Camel JMS timing race — 為什麼「測試連線」永遠失敗

日期:2026-04-11 環境:INFINITIX AI-Stack 4.27.2 / aiscdeployment-RC-0.6.5.0205 Ansible installer / Ubuntu 24.04 / Docker 26.1 / Kubernetes 1.29 (kubeadm + cri-dockerd) / Apache Camel 2.23.1 / ActiveMQ 5.18.4 / Tomcat 9


TL;DR

用 INFINITIX 原廠 Ansible installer 部署 AI-Stack 4.27.2 單機 AIO,cluster-12-aistack-install.yml 跑完後 Ansible PLAY RECAP 0 failed,但登進管理介面按「測試連線」永遠失敗,dashboard widget 全紅,背景 scheduler 每幾秒吐 DirectConsumerNotAvailableException: No consumers available on endpoint: direct://SendCommandRoot cause 是 Apache Camel 的 JMS 橋接 route 在 tomcat 啟動階段註冊失敗 — ActiveMQ / skyport 那時候還沒 ready,Camel JmsConsumer init 失敗後不會重試,整條 route 就永遠不存在於 context 裡。修法是停掉所有 container → 按 activemq+postgres → skyport → tomcat 順序重啟,讓 JMS 依賴在 tomcat 啟動時已經 ready。


背景

INFINITIX AI-Stack 是一套企業級 AI / ML 協作管理平台,架構是 Tomcat + Spring + Angular SPA + PostgreSQL + ActiveMQ + 自家的 skyport 執行引擎,全部跑在 docker compose 上,可選整合到一個獨立部署的 Kubernetes 叢集來跑訓練 / 推論 workload。原廠提供 Ansible installer (aiscdeployment-RC-0.6.5.0205),32 個 playbook、48 個 role,從 kernel 升級、Harbor registry 自建、kubeadm init、AI-Stack Web 部署一路到 GPU driver 都做掉。

我們的場景是在乾淨的 Ubuntu 24.04 VM 上,用原廠 installer 跑完整 AIO 流程(master + aistack + harbor 全在同一台),驗證 offline 部署可行性。前 11 個 playbook(cluster-01 ~ cluster-11)都順利跑完,cluster-12 也 report 全綠(ok=12 changed=6 failed=0),但接下來的人工 Web UI 驗證就出事了。


症狀

表面層:登入 AI-Stack 管理介面 https://<ip>:8080/operation/ 後:

  1. 打開 Cloud Provider 編輯頁面,按 Test Connection 按鈕 → 顯示「連線失敗」
  2. Dashboard 上的 Cluster Nodes、Pod Count、GPU 使用率、Registry Health 等 widget 全部空白或紅色
  3. 不是 UI 層問題 —— Chrome DevTools 看 XHR 回應是 HTTP 500 + server error

背景層:tomcat 的 application log 每 5-10 秒就吐一組 stack trace:

org.apache.camel.component.direct.DirectConsumerNotAvailableException:
  No consumers available on endpoint: direct://SendCommand.
  Exchange[ID-6366b87e827e-1775867694367-0-393]
    at org.apache.camel.component.direct.DirectProducer.process(DirectProducer.java:69)
    at com.infinitiessoft.commons.commands.client.ClientCommandExecutor.asyncSend(...)
    at com.infinities.cloudfusion.mls.schedule.MlsJobStatusScannerScheduler.listPod(...)

Exchange ID 後面的序號從 -1 一路累加到 -500+,代表每次呼叫都是新的失敗 —— 不是 cached error。


除錯過程

❌ 嘗試 1:重啟 tomcat

直覺上像 container 啟動順序問題,docker compose restart ai-stack 試試。

結果:一樣爆同樣的錯。Exchange ID 的 1775867694367 前綴沒變(因為 docker-compose restart 只重啟主 process,container ID 不變),但 sequence 還是從 0 開始重新累加 —— 確定是 fresh failure,不是 stale cache。

❌ 嘗試 2:懷疑 Spring component-scan package 錯了

反編譯 commons-commands-3.1.0.jar,找到 com.infinitiessoft.commons.commands.routebuilder.ClientCommandRouteBuilder 這個 class,從 strings 看它就是要建立 direct:SendCommand → jms:queue:SendCommand 的橋接 route:

direct:SendCommand
jms:queue:SendCommand?requestTimeout=60000&disableTimeToLive=true

看了 applicationContext.xml

<context:component-scan base-package="com.infinities.cloudfusion" />

Scan 的 package 是 com.infinities.cloudfusion ClientCommandRouteBuildercom.infinitiessoft.commons.commands(拼法不一樣,多了 ssoft)—— 以為找到 smoking gun 了。

結果:這條線索是誤判。進一步反編譯 cloudfusion-camel-37.7.0.jarSpringConfigurationCamel.class 發現它是個 @Configuration class,用 @Bean 顯式建立 commandRouteBuilder() 回傳 ClientCommandRouteBuilder 物件。因為 SpringConfigurationCamel 本身在 com.infinities.cloudfusion.camel 下(scan 範圍內),ClientCommandRouteBuilder 是由 @Bean 建立而非 component-scan 撿到,package 拼法根本不相干。同個 @Configuration 的其他 @Bean(例如 EventPublishRouteBuilder)卻註冊成功了,代表 Spring bean init 有在跑,問題是特定這顆失敗。

❌ 嘗試 3:懷疑 ActiveMQ 認證

IssInitParams.properties 裡有:

broker.username = cloudfusion
broker.password = cloudfusion

以為 ActiveMQ 預設無認證但 client 硬塞帳密會被拒。

結果:看 tomcat 啟動 log,EventSubscribeRouteBuilder 成功 subscribe 了 10 幾個 ActiveMQ topic(activemq:topic:RemoteEvent-*),代表 JMS connection 本身是通的。認證不是問題。

❌ 嘗試 4:懷疑 _REPLACE_ME_ 加密失敗連帶破壞 Camel

Vendor 的 cluster-12 會自動 INSERT INTO userserviceaccessinfo VALUES (..., '_REPLACE_ME_', 'Kubernetes', ...)(預留 skyport key 給 cluster-21 填回)。tomcat 啟動後 StringEncryptConverter 試著解密這個 placeholder 會拋錯 unable to decrypt key, raw ->_REPLACE_ME_。猜測 Spring bean init 因此失敗。

結果StringEncryptConverter 的錯誤出現在 MlsJobStatusScannerScheduler 的 log context,不是 在 SpringConfigurationCamel init。這兩個錯誤互相獨立。把 _REPLACE_ME_ 換成真值之後垃圾 log 消失,但 DirectConsumerNotAvailable 還是照爆。

✅ 嘗試 5:比對 skyport container 的 route log

skyport 是獨立的 docker compose project(跟 ai-stack-web compose 是不同的 project / network),重啟 skyport 後看它印的 Camel route 清單:

Route: Server-SendCommand started and consuming from:
    jms://queue:SendCommand?disableTimeToLive=true&requestTimeout=60000
Route: Server-ExecuteCommand started and consuming from: direct://ExecuteCommand
Route: Server-ReturnCommand started and consuming from: direct://ReturnCommand
Route: publish:topic:RemoteEvent-EventID started and consuming from: direct://PublishEvent
Total 4 routes, of which 4 are started

所以架構是:

ai-stack (tomcat JVM)             skyport JVM
  REST layer                         [waiting]
     │
     ▼ .to("direct:SendCommand")
  [Camel bridge route ❌ missing]
     │ would → .to("jms:queue:SendCommand")
     ▼
 ActiveMQ broker ────────────────▶ jms:queue:SendCommand consumer ✅
                                    │
                                    ▼ Fabric8 Kubernetes client
                                  K8s API

關鍵觀察:skyport 重啟後印出 Total 4 routes, 4 started,但 skyport 重啟當下 tomcat 還在跑,tomcat 的 Camel context 並不會因為 skyport 重新上線就去補建 direct:SendCommand bridge route —— 因為 Camel 認為自己 context 已經 started 了。

✅ 嘗試 6:停 tomcat、讓 skyport + activemq 完全 ready 之後再起 tomcat

cd /opt/ai-stack-web && sudo docker-compose stop
cd /opt/skyport && sudo docker-compose stop

# 1. activemq + postgres 先起
cd /opt/ai-stack-web && sudo docker-compose up -d activemq postgresql
sleep 20

# 2. skyport 起來並確認 4/4 routes
cd /opt/skyport && sudo docker-compose up -d
sleep 60
sudo docker logs skyport_skyport_1 --since 1m | grep "Total.*routes"
# Total 4 routes, of which 4 are started   ← 必須看到

# 3. 最後起 tomcat
cd /opt/ai-stack-web && sudo docker-compose up -d ai-stack reverse-proxy
sleep 90

# Verify
sudo docker logs ai-stack-web_ai-stack_1 --since 2m | grep -c "DirectConsumerNotAvailable"
# 必須為 0

結果DirectConsumerNotAvailable 消失,skyport log 看到它正在處理 ExecuteCommand[RcsPodListing] 回傳真實的 RcsPod 物件(cadvisor, ingress-nginx, calico-node, kepler …),dashboard widget 全亮。問題解決。


根本原因

Apache Camel 的 direct: 是 in-process 同步 endpoint,producer(.to("direct:X"))跟 consumer(.from("direct:X").to(...)必須在同一個 Camel context。如果 context start 完成時沒有 consumer 註冊,producer 的 call 就會丟 DirectConsumerNotAvailableException —— Camel 不會自動補建 missing consumer

ClientCommandRouteBuilderconfigure() 是:

from("direct:SendCommand")
    .to("jms:queue:SendCommand?requestTimeout=60000&disableTimeToLive=true");

Camel 啟動這條 route 的時候,會先解析 jms:queue:SendCommand → 交給 JmsComponentJmsComponent 在內部透過 Spring 的 DefaultJmsMessageListenerContainer 建立 JMS connection。若 ActiveMQ broker 那時候還沒開 port(或者已經開了但 jms:queue:SendCommand 的 consumer 端還沒有任何 subscriber),某些 Camel 版本會讓這條 route 初始化靜默失敗但不擋整個 context 啟動 —— 結果是整個 Camel context 依然 state=Started,其他 route 也都正常運作,唯獨這一條不見了。

INFINITIX 的部署架構有三個 docker compose project:

/opt/ai-stack-web/docker-compose.yaml   ← tomcat + activemq + postgresql + reverse-proxy
/opt/skyport/docker-compose.yaml        ← skyport (獨立 compose project)
/opt/harbor/docker-compose.yml          ← harbor (獨立 compose project)

ai-stack-web 這個 compose 裡面,tomcat 的 depends_on: [activemq, postgresql] 只保證 container 啟動順序,不保證 activemq broker 已經 listen socket。Vendor installer 的 cluster-12-aistack-install.yml 首次 docker-compose up -d 時,tomcat 跑得比 ActiveMQ broker accept 連線還快,Camel 的 JMS 橋接 route init 就在那個時點撞上 race。

                       docker-compose up -d
                               
        ┌──────────────────────┼──────────────────────┐
                                                    
   postgresql             activemq              ai-stack (tomcat)
   ready at T+3s          ready at T+8s         Spring context  T+5s
                                                嘗試建 JMS route  fail
                                                Camel context started
                                                route 永遠不存在

為什麼 EventPublishRouteBuilder 不受影響?

同個 SpringConfigurationCamel @Configuration 裡,EventPublishRouteBuilder 的 configure 是 activemq:topic:RemoteEvent(publish-only,不需要 consumer 先存在),EventSubscribeRouteBuilder 會訂閱 topic(topic subscribe 是 lazy 的,先建一個 listener 之後 ActiveMQ 有 message 再派)。唯獨 ClientCommandRouteBuilderrequest-response pattern 的 JMS queue,初始化時會嘗試建立 consumer connection pool 來接 reply queue,時序要求最敏感,所以只有它中招。


學到的事


參考資料