chundev
日期: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
用 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://SendCommand。Root 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/ 後:
背景層: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。
直覺上像 container 啟動順序問題,docker compose restart ai-stack 試試。
結果:一樣爆同樣的錯。Exchange ID 的 1775867694367 前綴沒變(因為 docker-compose restart 只重啟主 process,container ID 不變),但 sequence 還是從 0 開始重新累加 —— 確定是 fresh failure,不是 stale cache。
反編譯 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,但 ClientCommandRouteBuilder 在 com.infinitiessoft.commons.commands(拼法不一樣,多了 ssoft)—— 以為找到 smoking gun 了。
結果:這條線索是誤判。進一步反編譯 cloudfusion-camel-37.7.0.jar 的 SpringConfigurationCamel.class 發現它是個 @Configuration class,用 @Bean 顯式建立 commandRouteBuilder() 回傳 ClientCommandRouteBuilder 物件。因為 SpringConfigurationCamel 本身在 com.infinities.cloudfusion.camel 下(scan 範圍內),ClientCommandRouteBuilder 是由 @Bean 建立而非 component-scan 撿到,package 拼法根本不相干。同個 @Configuration 的其他 @Bean(例如 EventPublishRouteBuilder)卻註冊成功了,代表 Spring bean init 有在跑,問題是特定這顆失敗。
IssInitParams.properties 裡有:
broker.username = cloudfusion
broker.password = cloudfusion
以為 ActiveMQ 預設無認證但 client 硬塞帳密會被拒。
結果:看 tomcat 啟動 log,EventSubscribeRouteBuilder 成功 subscribe 了 10 幾個 ActiveMQ topic(activemq:topic:RemoteEvent-*),代表 JMS connection 本身是通的。認證不是問題。
_REPLACE_ME_ 加密失敗連帶破壞 CamelVendor 的 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 還是照爆。
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 了。
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。
ClientCommandRouteBuilder 的 configure() 是:
from("direct:SendCommand")
.to("jms:queue:SendCommand?requestTimeout=60000&disableTimeToLive=true");
Camel 啟動這條 route 的時候,會先解析 jms:queue:SendCommand → 交給 JmsComponent → JmsComponent 在內部透過 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 再派)。唯獨 ClientCommandRouteBuilder 是 request-response pattern 的 JMS queue,初始化時會嘗試建立 consumer connection pool 來接 reply queue,時序要求最敏感,所以只有它中招。
docker-compose depends_on 不等於 readiness wait。即便加了 condition: service_healthy,docker-compose v1 也不支援(v2 才有)。複合系統裡跨 compose project 的 dependency(例如這裡的 ai-stack-web compose vs skyport compose)完全沒有任何 ordering 保證。正確作法是在 cluster 初次部署時手動分階段啟動:infra 服務 → 等 healthy → 業務服務。direct: endpoint 是最容易遇到「消費者不存在」陷阱的類型。Producer 跟 consumer 必須同一個 context,且 consumer 必須在 producer 被呼叫前就 start 完成。如果 consumer 是由一個依賴外部系統(JMS broker、DB)的橋接 route 提供的,外部系統的就緒時間直接決定 route 是否存活。sudo docker logs <container> --since 5m | grep -c "DirectConsumerNotAvailable"
# == 0 才算過
這個檢查值得寫進所有複雜多容器部署的 SOP。
@Autowired 強依賴。InfiniTIX 這裡的 commandRouteBuilder bean 顯然是 optional 依賴,所以 init 失敗時 Spring context 依然 started。EventPublishRouteBuilder 有成功 register 但 ClientCommandRouteBuilder 沒有 —— 同個 class 載入器、同個 Spring context、同個 Camel context —— 差別一定在 route 自己的 pattern,不是 infra 層問題。找到這個對照組就把搜尋空間縮了 90%。DefaultJmsMessageListenerContainer source — 這個 class 的 initialize() 行為決定 Camel JMS consumer 成敗depends_on 不等待健康狀態的討論 — 官方建議用 wait-for-it 之類的 script 顯式等 readiness