指南)
簡介面向部署數(shù)據(jù)庫遷移工具的開發(fā)與運維人員這份容器鏡像集合將遷移引擎、代理服務(wù)、監(jiān)控告警、可視化界面等組件打包為可一鍵編排的部署方案。包內(nèi)共十一個文件包含十個gz格式的容器鏡像包和一個YAML編排文件整體大小約七百三十八兆。鏡像覆蓋數(shù)據(jù)庫、消息代理、監(jiān)控、可視化等核心服務(wù)配合編排文件執(zhí)行單條命令即可批量啟動所有依賴組件省去手工配置網(wǎng)絡(luò)、卷掛載與環(huán)境變量的繁瑣步驟。對于需要快速搭建數(shù)據(jù)庫同步或遷移環(huán)境的技術(shù)團隊該資源能顯著縮短環(huán)境準備時間并借助監(jiān)控與可視化組件獲得清晰的運行狀態(tài)視圖。目前已有四十八人學習下載適合具備基礎(chǔ)容器使用經(jīng)驗、希望直接復用現(xiàn)成編排方案的讀者。1. 為什么說 DBmotion 的全量遷移先從一份容器集合開始做數(shù)據(jù)遷移的人應(yīng)該都有這種體會真正耗時間的往往不是遷移任務(wù)本身而是把源端、采集端、傳輸通道、目標端這一整套依賴環(huán)境湊齊。DBmotion 這類遷移工具雖然把同步邏輯封裝得很好但它的全量遷移鏈路里ZooKeeper、Kafka、etcd、采集與寫入服務(wù)一個都少不了。手動一個個裝、一個個調(diào)半天過去了還沒開始遷移數(shù)據(jù)。所謂“DBmotion 全量所需要容器集合包含可執(zhí)行的 docker-compose.yaml”就是把這條鏈路里的每個環(huán)節(jié)都做成容器用一份 docker-compose 編排文件一次性拉起來。適合兩種人一是想快速評估 DBmotion 遷移效果、不想在環(huán)境搭建上花太多時間的開發(fā)者二是要給客戶或內(nèi)部環(huán)境交付遷移方案需要一套可復現(xiàn)、可移植的部署模板。這篇文章按我實際搭建這套環(huán)境的順序把容器集合怎么設(shè)計、compose 文件怎么寫、參數(shù)怎么調(diào)、哪些坑必踩一次講清楚。2. 拆開 DBmotion 的容器集合每個容器解決遷移鏈路里的哪個環(huán)節(jié)2.1 一條全量遷移鏈路里哪些組件是不能省的DBmotion 做全量遷移簡單說就是“讀源庫 → 傳輸 → 寫目標庫”。但中間為了保證數(shù)據(jù)不丟、順序不亂、斷點能續(xù)引入了一組支撐組件。我最早做模擬項目X的時候就想著精簡只起源端和目標端兩個容器結(jié)果根本跑不通。后來把鏈路理清楚才發(fā)現(xiàn)下面這些角色各有各的活誰也替不了誰。源端數(shù)據(jù)庫容器承擔“數(shù)據(jù)從哪來”的角色。全量遷移要驗證數(shù)據(jù)一致性沒有真實的源庫實例后面所有環(huán)節(jié)都是空轉(zhuǎn)。我一般用 MySQL 或 PostgreSQL 的官方鏡像直接掛初始化腳本造數(shù)據(jù)。采集端容器是 DBmotion 里干累活的。它連上源庫把全量數(shù)據(jù)按表讀出來轉(zhuǎn)成統(tǒng)一的消息格式發(fā)到 Kafka。這個角色通常對應(yīng) DBmotion 的采集服務(wù)如果只是做功能驗證可以用輕量模擬程序代替但正式評估時必須用真實的采集節(jié)點。Kafka 是核心傳輸通道。全量數(shù)據(jù)量大采集端不可能直接往目標庫灌那樣源庫和目標庫會互相拖垮。Kafka 在這里相當于一個高速緩沖帶采集端只管往里面寫寫入端只管從里面讀兩者解耦。Kafka 本身依賴 ZooKeeper 做元數(shù)據(jù)管理所以 ZooKeeper 容器成了隱藏的必需項。etcd 管的是遷移任務(wù)的元數(shù)據(jù)。哪些表遷移完了、遷移到哪個偏移量了、任務(wù)當前什么狀態(tài)這些都存在 etcd 里。沒有它斷點續(xù)傳就是空話。最后是寫入端容器它消費 Kafka 里的數(shù)據(jù)把全量數(shù)據(jù)按目標庫的方言寫入同時往 etcd 上報進度。這么一拆就很清楚了源端、采集端、Kafka、ZooKeeper、etcd、寫入端、目標端七個角色缺一個鏈路就斷。我見過有人想把 ZooKeeper 省掉直接連 Kafka只能說那是給自己挖坑。2.2 為什么用 ZooKeeper Kafka etcd 三件套而不是單機版很多人第一次看到這套組合會問都是存元數(shù)據(jù)為什么 Kafka 要配 ZooKeeperDBmotion 自己又要一個 etcd這倆不能合并嗎答案是職責不同。Kafka 用 ZooKeeper 存的是 broker 列表、分區(qū)分配、消費者組偏移量這些“消息系統(tǒng)自己的狀態(tài)”。Kafka 的歷史版本把 ZooKeeper 當成硬依賴雖然新版本在做 KRaft 模式去 ZooKeeper但 DBmotion 官方鏈路默認還是走 ZooKeeper 的方式實盤驗證時扯皮最少。我自己試過 KRaft 模式不是說不行而是排查問題時社區(qū)資料少一旦出狀況很難判斷是 DBmotion 的問題還是 Kafka 自身的問題。etcd 管的是遷移任務(wù)級別的元數(shù)據(jù)比如哪個表遷移到哪條記錄了。這不是 Kafka 的分區(qū)偏移量是業(yè)務(wù)數(shù)據(jù)在遷移管道里的進度。如果硬塞進 Kafka 的 topic 里任務(wù)一旦重建歷史偏移量會污染新任務(wù)。所以這套“三件套”不是重復造輪子而是各自守住各自的邊界。ZooKeeper 守消息系統(tǒng)etcd 守遷移任務(wù)。明白了這一點編排容器時就能理解為什么每個服務(wù)都要獨立部署而不是圖省事塞進同一個容器里。進程混在一起日志、重啟、擴容全都互相干擾全量遷移跑到一半某個組件掛了排查起來想死的心都有。2.3 容器集合的拓撲與端口規(guī)劃我把整套容器集合的拓撲固定成了一張表每次搭新環(huán)境直接照著抄。拓撲上分為三層數(shù)據(jù)層、傳輸層、任務(wù)層。數(shù)據(jù)層是源端和目標端數(shù)據(jù)庫傳輸層是 Kafka 加 ZooKeeper任務(wù)層是采集端、寫入端和 etcd。DBmotion 本身的管理端如果有放在任務(wù)層最上面負責下發(fā)任務(wù)和查看狀態(tài)。服務(wù)鏡像類型默認端口數(shù)據(jù)持久化職責source-dbMySQL/PostgreSQL 官方鏡像3306/5432是初始化腳本掛載全量數(shù)據(jù)來源target-db目標數(shù)據(jù)庫官方鏡像3306/5432是全量數(shù)據(jù)寫入目標zookeeperZooKeeper 官方鏡像2181是dataDir 掛載Kafka 元數(shù)據(jù)存儲kafkaKafka 官方鏡像9092是log.dirs 掛載全量數(shù)據(jù)傳輸通道etcdetcd 官方鏡像2379, 2380是data 目錄掛載遷移任務(wù)進度/狀態(tài)dbmotion-collectorDBmotion 采集端鏡像無外部端口否讀源庫寫 Kafkadbmotion-writerDBmotion 寫入端鏡像無外部端口否讀 Kafka寫目標庫端口規(guī)劃有個原則只有需要被外部訪問的服務(wù)才映射到宿主機服務(wù)間通信用 Docker 內(nèi)部網(wǎng)絡(luò)。Kafka 比較特殊因為客戶端采集端、寫入端跑在別的容器里Kafka 自己的 advertised.listeners 必須配成容器網(wǎng)絡(luò)內(nèi)的地址否則會踩“容器里連不上 Kafka”的經(jīng)典大坑。這一點后面專門講。etcd 的 2380 是集群內(nèi)部 peer 通信用的單節(jié)點部署不需要映射到宿主機2379 看需要再決定要不要暴露。3. 寫一份可執(zhí)行的 docker-compose.yaml部署模型與關(guān)鍵參數(shù)3.1 先從 compose 文件結(jié)構(gòu)入手網(wǎng)絡(luò)、卷、服務(wù)三段式一份可執(zhí)行的 compose 文件結(jié)構(gòu)上分三塊networks 定義容器間通信網(wǎng)絡(luò)volumes 定義持久化存儲services 定義每個容器。全量遷移的場景里容器之間都是“服務(wù)發(fā)現(xiàn)”的關(guān)系——采集端通過 etcd 找任務(wù)通過 Kafka 發(fā)數(shù)據(jù)寫入端從 Kafka 拉數(shù)據(jù)。所以網(wǎng)絡(luò)必須用自定義 bridge 網(wǎng)絡(luò)而不是默認的 default 網(wǎng)絡(luò)這樣才能用服務(wù)名互相訪問。下面是我整理出來的精簡版文件框架。注意 source-db 和 target-db 的初始化腳本我用的是掛載目錄而不是鏡像內(nèi)嵌這樣換表結(jié)構(gòu)、換數(shù)據(jù)都不用重新 build 鏡像。version: 3.8 networks: dbmotion-net: driver: bridge ipam: config: - subnet: 172.28.0.0/16 volumes: zk-data: kafka-data: etcd-data: source-db-data: target-db-data: services: zookeeper: image: zookeeper:3.8 container_name: dbmotion-zookeeper networks: dbmotion-net: ipv4_address: 172.28.0.10 ports: - 2181:2181 volumes: - zk-data:/data - ./zk-conf:/conf environment: ZOO_MY_ID: 1 ZOO_SERVERS: server.1zookeeper:2888:3888;2181 healthcheck: test: [CMD, zkServer.sh, status] interval: 10s timeout: 5s retries: 5network 指定了固定子網(wǎng)和固定 IP。這個做法在 compose 里不是必須的但配合 healthcheck 和 depends_on 的 condition 時服務(wù)名解析穩(wěn)定排查網(wǎng)絡(luò)問題也少一層干擾。volumes 全部用 named volume而不是 bind mount 到宿主機目錄原因是遷移環(huán)境經(jīng)常要在不同機器之間搬named volume 隨 compose 項目走不會被宿主機路徑差異絆倒。zookeeper 這里指定了 ZOO_MY_ID 和 ZOO_SERVERS。單節(jié)點時 ZOO_MY_ID 寫 1 就行ZOO_SERVERS 的格式是 server.1主機名:2888:3888;21812888 是 Leader 選舉端口3888 是集群內(nèi)通信端口。單節(jié)點不涉及選舉但配全了以后想擴成三節(jié)點集群只需再追加 server.2 和 server.3不用改其他配置。healthcheck 用的是 zkServer.sh status這條命令在容器里返回 0 表示服務(wù)健康。3.2 中間件服務(wù)的參數(shù)陷阱Kafka 的 advertised.listeners 與健康檢查Kafka 是這套容器集合里最容易翻車的服務(wù)沒有之一。官方鏡像默認的 listener 配置只對宿主機訪問友好容器內(nèi)其他服務(wù)通過服務(wù)名訪問時Kafka 返回的 broker 地址是容器 ID客戶端拿著這個地址去連必然超時。這就是業(yè)界說的“Kafka 容器網(wǎng)絡(luò)黑匣子”問題。解決辦法是在 environment 里顯式指定三組 listenerPLAINTEXT 負責容器內(nèi)通信EXTERNAL 負責宿主機訪問CONTROLLER 是給新版本 Kafka 用的。advertised.listeners 必須寫成服務(wù)名 kafka:9092而不是 localhost 或宿主機 IP。如果寫 127.0.0.1采集容器里的客戶端能連上 broker但 broker 返回的元數(shù)據(jù)里寫的地址還是 127.0.0.1客戶端拿這個地址根本路由不到 Kafka 容器。kafka: image: bitnami/kafka:3.4 container_name: dbmotion-kafka networks: dbmotion-net: ipv4_address: 172.28.0.11 ports: - 9092:9092 volumes: - kafka-data:/bitnami/kafka environment: KAFKA_CFG_NODE_ID: 1 KAFKA_CFG_PROCESS_ROLES: broker,controller KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1kafka:9093 KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR: 1 ALLOW_PLAINTEXT_LISTENER: yes KAFKA_HEAP_OPTS: -Xmx512m -Xms256m healthcheck: test: [CMD, kafka-topics.sh, --bootstrap-server, localhost:9092, --list] interval: 15s timeout: 10s retries: 5 start_period: 30s這里用的是 bitnami 鏡像環(huán)境變量前綴 KAFKA_CFG_ 是 bitnami 的封裝規(guī)范和 Apache 官方鏡像的 KAFKA_ 前綴不一樣。選 bitnami 的原因是其健康檢查腳本更完整kafka-topics.sh --list 能真實反映 broker 是否對外可用而 Apache 鏡像默認的 healthcheck 經(jīng)常只是檢查進程在不在。KAFKA_CFG_PROCESS_ROLES 寫成 broker,controller 是讓單節(jié)點同時承擔 broker 和 controller 職責。全量遷移的最小環(huán)境不需要獨立的 KRaft controller 節(jié)點。副本因子全部設(shè)成 1因為單節(jié)點 Kafka 上不允許配大于 1 的副本因子這個參數(shù)不影響遷移正確性但能避免一堆 “Replication factor is larger than available brokers” 的報錯刷屏。KAFKA_HEAP_OPTS 限制 JVM 內(nèi)存后面避坑章節(jié)專門講內(nèi)存問題。etcd 的配置相對清爽主要注意 client 端口和 peer 端口的區(qū)分。etcd: image: quay.io/coreos/etcd:v3.5.5 container_name: dbmotion-etcd networks: dbmotion-net: ipv4_address: 172.28.0.12 ports: - 2379:2379 volumes: - etcd-data:/etcd-data command: - /usr/local/bin/etcd - --nameetcd0 - --data-dir/etcd-data - --advertise-client-urlshttp://etcd:2379 - --listen-client-urlshttp://0.0.0.0:2379 - --listen-peer-urlshttp://0.0.0.0:2380 - --initial-advertise-peer-urlshttp://etcd:2380 - --initial-clusteretcd0http://etcd:2380 - --initial-cluster-statenew healthcheck: test: [CMD, etcdctl, endpoint, health] interval: 10s timeout: 5s retries: 5advertise-client-urls 寫容器服務(wù)名 etcd 而不是 IP這樣 DBmotion 寫入端的容器不管調(diào)度到哪個網(wǎng)絡(luò)位置都能通過服務(wù)名穩(wěn)定訪問。initial-cluster-state 必須寫 new如果寫 existingetcd 會去找舊集群狀態(tài)單節(jié)點場景下會一直卡在等待集群就緒。3.3 DBmotion 相關(guān)服務(wù)的啟動順序與 depends_on 的正確用法容器編排里啟動順序是個老生常談但永遠有人踩坑的問題。DBmotion 采集端啟動時必須能連上 etcd 和 Kafka寫入端必須能連上 Kafka 和 etcd而 Kafka 又要等 ZooKeeper 就緒。這些依賴如果用默認的 depends_ondocker compose 只保證“先啟動”不保證“真就緒”采集端大概率在 Kafka 還在初始化時就嘗試連接然后崩潰退出。容器退出后 compose 不會自動重啟整套環(huán)境就癱在那里。正確做法是 depends_on 配 condition: service_healthy再配合每個中間件的 healthcheck。compose 會先啟動依賴服務(wù)等 healthcheck 通過后才啟動下游服務(wù)。dbmotion-collector: image: dbmotion-collector:latest container_name: dbmotion-collector networks: dbmotion-net: ipv4_address: 172.28.0.20 depends_on: kafka: condition: service_healthy etcd: condition: service_healthy environment: DBMOTION_ROLE: collector DBMOTION_ETCD_ENDPOINTS: http://etcd:2379 DBMOTION_KAFKA_BOOTSTRAP: kafka:9092 DBMOTION_SOURCE_DSN: root:sourcepasstcp(source-db:3306)/migration_db?charsetutf8mb4 command: [/app/dbmotion-collector, --config, /etc/dbmotion/collector.yaml] dbmotion-writer: image: dbmotion-writer:latest container_name: dbmotion-writer networks: dbmotion-net: ipv4_address: 172.28.0.21 depends_on: kafka: condition: service_healthy etcd: condition: service_healthy environment: DBMOTION_ROLE: writer DBMOTION_ETCD_ENDPOINTS: http://etcd:2379 DBMOTION_KAFKA_BOOTSTRAP: kafka:9092 DBMOTION_TARGET_DSN: root:targetpasstcp(target-db:3306)/migration_target?charsetutf8mb4 command: [/app/dbmotion-writer, --config, /etc/dbmotion/writer.yaml]DBMOTION_SOURCE_DSN 和 DBMOTION_TARGET_DSN 里的主機名全部寫服務(wù)名tcp(source-db:3306) 這種格式是 Go 語言數(shù)據(jù)庫驅(qū)動的標準寫法。這里有個容易忽略的點source-db 和 target-db 這兩個容器本身也要配 healthcheck否則 depends_on 里如果加了它們condition 會檢查不到健康狀態(tài)而一直等待。MySQL 鏡像的 healthcheck 建議用 mysqladmin pingPostgreSQL 用 pg_isready。啟動順序變長了以后整套環(huán)境冷啟動可能要等 1 到 2 分鐘這不是卡死是中間件在做初始化。后面加了一個 start_period 參數(shù)專門解決“啟動慢被 healthcheck 誤殺”的問題。采集端和寫入端的鏡像這里用 dbmotion-collector:latest 指代實際使用時換成你自己的鏡像名。4. 把容器集合跑起來從 docker compose up 到第一個遷移任務(wù)4.1 啟動前檢查端口沖突、內(nèi)存預(yù)算、鏡像源啟動之前有三次檢查能幫你省掉后面至少兩個小時的排查時間。第一次檢查端口沖突。2181、9092、2379 這三個端口是中間件默認端口本機如果已經(jīng)跑著別的 ZooKeeper、Kafka 或 etcdcompose 起服務(wù)時會直接報 bind: address already in use。檢查命令很簡單lsof -i :2181 -i :9092 -i :2379有輸出說明端口被占了。解決方式有兩個關(guān)掉宿主機的舊服務(wù)或者在 compose 文件里改 ports 映射的宿主機側(cè)端口比如把 Kafka 改成 19092:9092。要注意改宿主機側(cè)端口不影響容器間通過服務(wù)名訪問因為容器間走的是 9092 這個容器內(nèi)端口。第二次檢查內(nèi)存預(yù)算。Kafka 默認的 JVM 堆上限是 1G加上 ZooKeeper、etcd、兩個數(shù)據(jù)庫實例整套環(huán)境冷啟動大概需要 3 到 4G 可用內(nèi)存。如果機器只有 2GKafka 會頻繁 Full GC然后被 healthcheck 判死compose 就會反復重啟它日志里全是內(nèi)存溢出。建議在 compose 里給 Kafka 顯式限制 KAFKA_HEAP_OPTS給 MySQL/PostgreSQL 用 shm_size 控制共享內(nèi)存。第三次檢查鏡像源。DBmotion 相關(guān)鏡像如果是私有倉庫docker compose 不會自動登錄需要先手動 docker login。ZooKeeper、Kafka、etcd 這幾個中間件鏡像建議提前 docker pull 到本地免得 compose up 時才開始拉一旦網(wǎng)絡(luò)不穩(wěn)定整個環(huán)境起得斷斷續(xù)續(xù)。4.2 啟動整套環(huán)境與驗證每個容器狀態(tài)檢查做完就可以啟動了。我習慣用后臺模式啟動然后把日志輸出到文件方便后面排查。docker compose up -d這個命令會按依賴順序依次啟動所有容器。等第一次執(zhí)行完別急著配遷移任務(wù)先花 30 秒確認每個容器都進入了 healthy 狀態(tài)docker compose ps輸出里每個服務(wù)對應(yīng)一行STATUS 列應(yīng)該有 healthy 字樣。這一步非常重要因為采集端和寫入端的啟動依賴于 Kafka 和 etcd 健康如果中間件還處于 starting下游容器會卡在等待狀態(tài)??吹?unhealthy 的容器立即看它的日志docker compose logs -f kafka docker compose logs -f etcd日志出現(xiàn)的常見關(guān)鍵字也有套路。ZooKeeper 日志里出現(xiàn) bind to port 2181 說明監(jiān)聽成功Kafka 日志里出現(xiàn) [KafkaRaftServer nodeId1] Started 說明 KRaft 模式啟動完成etcd 日志出現(xiàn) serving insecure client requests on 0.0.0.0:2379 說明客戶端接口可用。如果等了兩分鐘這些關(guān)鍵字還沒出現(xiàn)基本可以斷定配置有問題而不是啟動慢。所有容器都 healthy 之后再驗證一下容器間的網(wǎng)絡(luò)連通性。我一般會進入采集端容器里用內(nèi)置工具測試到 Kafka 和 etcd 的連通性docker exec -it dbmotion-collector bash # 在容器內(nèi)執(zhí)行 curl http://etcd:2379/health # 期望輸出 {health:true} kafka-topics.sh --bootstrap-server kafka:9092 --list # 期望輸出不報錯哪怕沒有 topic這一步驗證的是服務(wù)名解析和端口連通。curl 輸出 health true 說明 etcd 正常kafka-topics.sh 能連上說明 Kafka 的 advertised.listeners 配置生效了。如果這兩步有一處失敗不要繼續(xù)往下配任務(wù)先回頭改配置。4.3 跑通一個最小全量遷移任務(wù)環(huán)境就緒后遷移任務(wù)本身怎么下發(fā)DBmotion 的做法是通過 etcd 寫入任務(wù)配置采集端和寫入端監(jiān)聽 etcd 的變化然后開始干活。所以我需要往 etcd 里寫入一個最小的遷移任務(wù)配置。任務(wù)配置的核心內(nèi)容包括源庫連接信息、目標庫連接信息、要遷移的表列表、Kafka topic 名稱。docker exec -it dbmotion-etcd etcdctl put /dbmotion/tasks/task001 { name: full_migration_demo, source: root:sourcepasstcp(source-db:3306)/migration_db, target: root:targetpasstcp(target-db:3306)/migration_target, tables: [users, orders], kafka_topic: dbmotion-full-001, mode: full, batch_size: 1000 }這段配置我一般在源庫和目標庫里都先建好同名表再填到 etcd。batch_size 控制每個批次寫入的記錄數(shù)設(shè) 1000 是保守值主要考慮目標庫寫入性能和 Kafka 消息大小。模式寫成 full表示只做全量遷移不做增量同步。字段名和結(jié)構(gòu)按實際 DBmotion 版本可能略有差異但核心信息就是這四類兩端連接、表清單、通道、批大小。任務(wù)寫進去之后采集端監(jiān)聽到配置變化會去源庫讀數(shù)據(jù)然后發(fā)到 Kafka寫入端從 Kafka 拉數(shù)據(jù)寫入目標庫。整個過程中可以通過 etcd 查看任務(wù)進度docker exec -it dbmotion-etcd etcdctl get /dbmotion/tasks/task001/status看到 status 字段變成 finished 或 done說明全量遷移跑完了。這時進目標庫查一下數(shù)據(jù)量docker exec -it dbmotion-target-db mysql -uroot -ptargetpass -e SELECT COUNT(*) FROM migration_target.users;如果和目標庫源表行數(shù)一致第一個全量遷移任務(wù)就算通了。第一次跑通的意義不在于遷移了多大數(shù)據(jù)量而在于驗證了從容器集合到任務(wù)下發(fā)的整條鏈路是通的后續(xù)換真實庫、加表、調(diào)并發(fā)都在這條鏈路上迭代。5. 容器集合落地避坑五個最容易翻車的點5.1 Kafka 容器起來了客戶端卻連不上現(xiàn)象docker compose ps 顯示 Kafka 是 healthy但采集端容器日志里反復報 connection refused 或者 timeoutKafka 的 topic 列表也拉取失敗。原因絕大多數(shù)情況是 advertised.listeners 配置不對。Kafka 容器對外廣播的地址寫成了 localhost 或 127.0.0.1容器內(nèi)的采集端拿到這個地址后嘗試連自己自然連不上。我見過有人排查了一下午最后發(fā)現(xiàn)是 KAFKA_CFG_ADVERTISED_LISTENERS 漏了 KAFKA_CFG_ 前綴導致配置根本沒生效。 解決把 advertised.listeners 顯式寫成 PLAINTEXT://kafka:9092注意主機名必須和 compose 服務(wù)名一致。改完之后重啟 Kafka 容器然后進入采集端容器里執(zhí)行 kafka-topics.sh --bootstrap-server kafka:9092 --list 驗證。改完配置不重啟、只看 docker inspect 里的 Environment 字段是不會生效的。5.2 depends_on 直接寫服務(wù)名下游容器還是崩潰退出現(xiàn)象dbmotion-collector 容器啟動后立即退出日志顯示連接 etcd 超時或 Kafka 未就緒但 Kafka 容器明明是 running。原因depends_on 只保證服務(wù)啟動先后順序不保證服務(wù)內(nèi)部就緒。Kafka 容器進程起來了但 broker 還在加載磁盤里的日志、注冊 controller這個窗口可能持續(xù)幾十秒。采集端這個時候去連必然失敗。容器退出之后 compose 不會自動拉起它所以整個環(huán)境就靜默地“半死”了。 解決depends_on 里加上 condition: service_healthy然后給 Kafka 和 etcd 都配上真正的 healthcheck。注意 Kafka 的 healthcheck 不要用 telnet 端口要用 kafka-topics.sh --list因為端口通了不代表 broker 完成注冊。這是一個很典型的“玄學問題”其實不是玄學是就緒語義沒搞清。5.3 etcd 容器啟動失敗名叫“permission denied”現(xiàn)象docker compose up 時 etcd 容器反復重啟日志里出現(xiàn) mkdir /etcd-data: permission denied。原因etcd 官方鏡像里的進程默認以非 root 用戶運行而掛載進來的 named volume 初始所有權(quán)是 root。容器用戶沒有寫權(quán)限etcd 建不了數(shù)據(jù)目錄。這個坑在大部分 Linux 發(fā)行版上都會踩macOS 上因為 Docker Desktop 的特殊權(quán)限模型可能不觸發(fā)反而增加了迷惑性。 解決最簡單的方式是在 compose 的 etcd 服務(wù)里加 user: 0:0 強制以 root 運行或者用初始化容器預(yù)先 chown 數(shù)據(jù)目錄。生產(chǎn)環(huán)境建議用初始化容器本地評估可以直接加 user。改完記得 docker compose down -v 把舊的卷刪掉重建否則權(quán)限修復不會套用到已創(chuàng)建的卷上。5.4 整套環(huán)境跑著跑著某個容器被系統(tǒng)殺掉現(xiàn)象遷移任務(wù)跑到一半Kafka 或兩個數(shù)據(jù)庫容器突然消失docker compose ps 里看不到系統(tǒng)日志里出現(xiàn) oom-kill 記錄。原因宿主機內(nèi)存被占滿。Kafka 默認的 JVM 堆內(nèi)存上限是 1G加上 ZooKeeper、etcd、兩個數(shù)據(jù)庫實例再算上 Docker 本身的開銷2G 內(nèi)存的機器根本扛不住。系統(tǒng) OOM killer 會優(yōu)先殺掉內(nèi)存占用最大的進程Kafka 首當其沖。 解決在 compose 里給每個服務(wù)加 mem_limitKafka 設(shè) 512mZooKeeper 設(shè) 256metcd 設(shè) 256m數(shù)據(jù)庫各自設(shè) 512m。同時給 Kafka 設(shè)置 KAFKA_HEAP_OPTS 為 -Xmx512m -Xms256m讓 JVM 在容器內(nèi)存限制之內(nèi)運行。壓內(nèi)存之前先用 docker stats 看實時占用確定是誰在吃內(nèi)存。5.5 數(shù)據(jù)“丟了”重啟后源庫或目標庫是空的現(xiàn)象整套環(huán)境重啟之后遷移任務(wù)的任務(wù)還在但目標庫里的數(shù)據(jù)沒了有時候源庫的初始化數(shù)據(jù)也沒了。原因數(shù)據(jù)庫容器雖然掛了 volumes但掛的是匿名卷或沒掛卷。compose down 的時候如果沒加 -v匿名卷會保留但 docker compose down -v 會把匿名卷一起刪掉。更隱蔽的場景是鏡像自帶的數(shù)據(jù)目錄路徑和你在 volumes 里掛載的容器內(nèi)路徑不一致比如 MySQL 鏡像的數(shù)據(jù)目錄是 /var/lib/mysql你掛成了 /var/lib/mysql/data卷里存了文件但 MySQL 根本不認識啟動時生成一個空的數(shù)據(jù)目錄看起來就是“數(shù)據(jù)丟了”。 解決先確認鏡像文檔里的數(shù)據(jù)目錄路徑。MySQL 官方鏡像是 /var/lib/mysqlPostgreSQL 是 /var/lib/postgresql/dataetcd 的路徑通過 --data-dir 參數(shù)自己指定。compose 里 volumes 的容器側(cè)路徑必須和實際數(shù)據(jù)目錄完全一致。養(yǎng)成一個好習慣改動環(huán)境之前先 docker compose down不要直接 docker compose down -v-v 是刪除數(shù)據(jù)卷的操作沒有后悔藥。6. 讓這套容器集合更順手參數(shù)化模板與多環(huán)境復用容器集合能跑通只是第一步第二步是讓它能復用。我最常用的是 .env 配合 compose 變量替換。把端口、密碼、數(shù)據(jù)目錄這些環(huán)境相關(guān)的值抽到 .env 文件里compose 文件本身保持一份不動。# .env 示例 SOURCE_DB_PORT3306 TARGET_DB_PORT3307 MYSQL_ROOT_PASSWORDsourcepass TARGET_MYSQL_ROOT_PASSWORDtargetpass KAFKA_PORT9092compose 文件里用 ${MYSQL_ROOT_PASSWORD} 引用這些變量。這樣同一份 compose 文件開發(fā)環(huán)境、測試環(huán)境、客戶現(xiàn)場只要改 .env 就能遷移不用動 YAML 本身。變量替換是 compose 原生的能力不需要額外工具。數(shù)據(jù)校驗是另一個值得養(yǎng)成的習慣。全量遷移結(jié)束只對比行數(shù)遠遠不夠。行數(shù)一樣但內(nèi)容對不上比如字符集編碼問題、時區(qū)轉(zhuǎn)換問題、浮點數(shù)精度問題都會讓遷移“成功但錯誤”。我一般用一個簡單的校驗?zāi)_本把源表和目標表按主鍵排序后做哈希對比docker exec source-db mysql -e SELECT CRC32(GROUP_CONCAT(col1, col2, col3 ORDER BY id)) FROM users; docker exec target-db mysql -e SELECT CRC32(GROUP_CONCAT(col1, col2, col3 ORDER BY id)) FROM users;兩邊的 CRC32 值一致說明這批數(shù)據(jù)內(nèi)容層面也對上了。不一致時把排序后的主鍵列表導出做 diff就能定位到具體哪一行差異。這個校驗方法簡單直接不用引入額外的工具鏈。這套容器集合做到現(xiàn)在這個程度我最大的教訓是不要因為追求“環(huán)境小”而閹割依賴組件。最早我為了省內(nèi)存把 ZooKeeper 換成嵌入式模式把 etcd 塞進采集端進程里結(jié)果每次排查問題都要先懷疑自己改過的地方反而浪費更多時間。按標準鏈路把容器集合完整拉起來雖然占資源多一點但每一步行為和線上行為一致驗證出來的結(jié)論才作數(shù)。希望這篇筆記能幫你把環(huán)境搭建這一步真正省下來把精力留給遷移任務(wù)本身。本文還有配套的精品資源點擊獲取