戰(zhàn):從單機(jī)瓶頸到多節(jié)點(diǎn)并行加速)
1. 為什么值得折騰集群從單機(jī)瓶頸說(shuō)起如果你用過(guò) OpenDroneMap下稱 ODM跑過(guò)上百?gòu)堈掌恼浠蛉S重建大概率經(jīng)歷過(guò)那種“進(jìn)度條卡在某個(gè)階段一動(dòng)不動(dòng)、風(fēng)扇狂轉(zhuǎn)、機(jī)器燙得能煎蛋”的絕望。單機(jī)跑 ODM 的痛點(diǎn)非常明確特征點(diǎn)提取和稠密匹配是典型的 CPU 密集型任務(wù)而內(nèi)存占用會(huì)隨著影像數(shù)量呈近似平方級(jí)增長(zhǎng)。一個(gè) 500 張 2000 萬(wàn)像素照片的項(xiàng)目單機(jī) 32GB 內(nèi)存經(jīng)常在稠密重建階段直接 OOM即便僥幸跑完耗時(shí)也常常以“天”為單位。集群搭建的核心目的就是把這些可以并行的計(jì)算任務(wù)拆散到多臺(tái)機(jī)器上讓多臺(tái)機(jī)器的 CPU 和內(nèi)存同時(shí)干活。ODM 本身提供了--split和--split-overlap參數(shù)來(lái)做分塊處理配合ClusterODM這個(gè)調(diào)度中間件就能把一個(gè)大項(xiàng)目切成若干子任務(wù)分發(fā)給多臺(tái)計(jì)算節(jié)點(diǎn)并行執(zhí)行最后再合并結(jié)果。這套方案解決的是“單機(jī)資源天花板”問(wèn)題適合手里有幾臺(tái)閑置工作站、或者想用多臺(tái)中等配置機(jī)器替代一臺(tái)頂配機(jī)器的團(tuán)隊(duì)。哪怕你只有兩臺(tái)機(jī)器搭起來(lái)也能明顯縮短處理時(shí)間。我先把結(jié)論擺在這里集群不是萬(wàn)能加速藥它的收益取決于任務(wù)類型、網(wǎng)絡(luò)帶寬和節(jié)點(diǎn)均衡性。下面我會(huì)把整套思路、參數(shù)計(jì)算、踩坑記錄全部攤開(kāi)講你照著做基本能復(fù)現(xiàn)一個(gè)可用的 ODM 集群。2. 整體架構(gòu)設(shè)計(jì)與方案選型2.1 集群的三種角色劃分ODM 集群在邏輯上分三個(gè)角色理解清楚這個(gè)劃分后面所有配置都不會(huì)迷路調(diào)度節(jié)點(diǎn)ClusterODM只負(fù)責(zé)接收任務(wù)、切分任務(wù)、把子任務(wù)分發(fā)給計(jì)算節(jié)點(diǎn)、收集結(jié)果并合并。它本身不跑重建計(jì)算配置要求很低一臺(tái) 2 核 4GB 的小機(jī)器甚至樹(shù)莓派都能勝任。計(jì)算節(jié)點(diǎn)NodeODM真正干活的機(jī)器每臺(tái)跑一個(gè) NodeODM 服務(wù)暴露 HTTP 接口。CPU 核數(shù)、內(nèi)存大小決定了單節(jié)點(diǎn)能處理多大的子任務(wù)。存儲(chǔ)/入口節(jié)點(diǎn)通常和調(diào)度節(jié)點(diǎn)合并部署負(fù)責(zé)存放原始影像、中間文件和最終成果。如果影像數(shù)據(jù)量大建議單獨(dú)掛一塊大容量 SSD。這種“調(diào)度與計(jì)算分離”的設(shè)計(jì)好處是擴(kuò)容極其簡(jiǎn)單——加一臺(tái)機(jī)器裝好 NodeODM在 ClusterODM 里注冊(cè)一下就行不用動(dòng)已有節(jié)點(diǎn)。壞處是調(diào)度節(jié)點(diǎn)會(huì)成為單點(diǎn)一旦它掛了整個(gè)集群不可用所以生產(chǎn)環(huán)境建議給調(diào)度節(jié)點(diǎn)做個(gè)定時(shí)快照。2.2 為什么選 ClusterODM 而不是自己寫(xiě)調(diào)度腳本有人會(huì)想不就是把照片分堆、分別調(diào)用 ODM、最后合并嗎我自己寫(xiě)個(gè) shell 腳本循環(huán)調(diào)用不就行了我早期確實(shí)這么干過(guò)結(jié)論是自己寫(xiě)腳本的維護(hù)成本遠(yuǎn)高于收益。原因有三第一ODM 的--split分塊不是簡(jiǎn)單按數(shù)量切它要考慮影像之間的重疊關(guān)系--split-overlap切出來(lái)的子塊之間必須有足夠重疊才能保證拼接無(wú)縫。這套邏輯 ODM 內(nèi)部已經(jīng)實(shí)現(xiàn)好了自己復(fù)刻容易出錯(cuò)。第二子任務(wù)之間的依賴關(guān)系、失敗重試、節(jié)點(diǎn)負(fù)載均衡這些 ClusterODM 都內(nèi)置了。它維護(hù)一個(gè)節(jié)點(diǎn)池根據(jù)每個(gè)節(jié)點(diǎn)當(dāng)前的任務(wù)隊(duì)列長(zhǎng)度自動(dòng)分配節(jié)點(diǎn)掛了會(huì)自動(dòng)把任務(wù)轉(zhuǎn)給其他節(jié)點(diǎn)。第三結(jié)果合并階段--merge需要把所有子模型的點(diǎn)云、紋理對(duì)齊拼接這一步對(duì)參數(shù)一致性要求很高用官方工具鏈能避免大量玄學(xué)問(wèn)題。所以我的建議很明確調(diào)度層直接用 ClusterODM不要重復(fù)造輪子。你要定制的部分主要是節(jié)點(diǎn)規(guī)格、切塊參數(shù)和網(wǎng)絡(luò)配置。2.3 硬件與網(wǎng)絡(luò)的最低要求下面這張表是我實(shí)測(cè)下來(lái)比較靠譜的配置參考注意這是“能跑起來(lái)”的底線不是“跑得快”的推薦值角色CPU內(nèi)存磁盤(pán)網(wǎng)絡(luò)調(diào)度節(jié)點(diǎn)2 核4GB50GB千兆計(jì)算節(jié)點(diǎn)小項(xiàng)目8 核32GB200GB SSD千兆計(jì)算節(jié)點(diǎn)中大項(xiàng)目16 核以上64GB 以上500GB SSD萬(wàn)兆優(yōu)先網(wǎng)絡(luò)這塊要特別強(qiáng)調(diào)節(jié)點(diǎn)之間的數(shù)據(jù)傳輸量很大。切塊后的子任務(wù)影像、中間的點(diǎn)云文件都要在調(diào)度節(jié)點(diǎn)和計(jì)算節(jié)點(diǎn)之間來(lái)回傳。千兆網(wǎng)絡(luò)下傳幾十 GB 數(shù)據(jù)會(huì)明顯拖慢整體效率如果條件允許計(jì)算節(jié)點(diǎn)和調(diào)度節(jié)點(diǎn)之間上萬(wàn)兆或者至少做鏈路聚合。我踩過(guò)的坑是用無(wú)線網(wǎng)絡(luò)連節(jié)點(diǎn)結(jié)果傳輸階段比計(jì)算階段還慢整體耗時(shí)反而比單機(jī)長(zhǎng)。3. 核心組件部署與關(guān)鍵參數(shù)解析3.1 用容器方式部署 NodeODM 的完整步驟計(jì)算節(jié)點(diǎn)我強(qiáng)烈建議用 Docker 部署原因是 ODM 依賴的庫(kù)版本很挑剔裸機(jī)安裝經(jīng)常在 OpenCV、GDAL 這些庫(kù)上翻車。容器鏡像把這些依賴都封好了省心。在每臺(tái)計(jì)算節(jié)點(diǎn)上執(zhí)行docker run -d --restart unless-stopped \ --name node-odm \ -p 3000:3000 \ -v /data/odm/images:/code/images \ -v /data/odm/odm_texturing:/code/odm_texturing \ opendronemap/nodeodm這里幾個(gè)點(diǎn)值得展開(kāi)說(shuō)。-p 3000:3000把 NodeODM 的 API 端口映射出來(lái)ClusterODM 就是通過(guò)這個(gè)端口和節(jié)點(diǎn)通信的。兩個(gè)-v掛載是必須的否則容器一重啟中間成果全丟重跑一次又是幾個(gè)小時(shí)。--restart unless-stopped保證機(jī)器意外重啟后節(jié)點(diǎn)自動(dòng)恢復(fù)集群里節(jié)點(diǎn)掉線是很常見(jiàn)的故障自動(dòng)拉起能省很多事。啟動(dòng)后驗(yàn)證節(jié)點(diǎn)是否正常curl http://節(jié)點(diǎn)IP:3000/info返回一段 JSON里面有version、taskQueueCount、maxImages等字段說(shuō)明節(jié)點(diǎn)活了。如果返回連接拒絕先查防火墻有沒(méi)有放行 3000 端口。3.2 ClusterODM 的安裝與節(jié)點(diǎn)注冊(cè)調(diào)度節(jié)點(diǎn)上同樣用容器跑 ClusterODMdocker run -d --restart unless-stopped \ --name cluster-odm \ -p 3001:3000 \ -p 8080:8080 \ -v /data/cluster:/var/www/data \ opendronemap/clusterodm3001是 ClusterODM 對(duì)外提供 API 的端口容器內(nèi)還是 3000映射到宿主機(jī) 3001 避免和本機(jī)其他服務(wù)沖突8080是它的 Web 管理界面。掛載/data/cluster用來(lái)持久化節(jié)點(diǎn)配置和任務(wù)記錄。容器起來(lái)后進(jìn)入容器內(nèi)部用命令行注冊(cè)節(jié)點(diǎn)docker exec -it cluster-odm clusterodm進(jìn)入交互式命令行后依次執(zhí)行node add 計(jì)算節(jié)點(diǎn)1的IP 3000 node add 計(jì)算節(jié)點(diǎn)2的IP 3000 node listnode list會(huì)列出所有已注冊(cè)節(jié)點(diǎn)及其狀態(tài)??吹矫總€(gè)節(jié)點(diǎn)后面顯示online就說(shuō)明注冊(cè)成功。如果顯示offline九成是網(wǎng)絡(luò)不通或者節(jié)點(diǎn)端口沒(méi)開(kāi)。注意ClusterODM 的節(jié)點(diǎn)配置默認(rèn)存在容器內(nèi)的文件里如果你沒(méi)做-v掛載容器重建后節(jié)點(diǎn)列表會(huì)清空得重新注冊(cè)。這個(gè)坑我踩過(guò)一次半夜容器被自動(dòng)更新重啟第二天發(fā)現(xiàn)集群空了。3.3 切塊參數(shù)的計(jì)算邏輯這是整個(gè)集群調(diào)優(yōu)里最需要?jiǎng)幽X子的部分。ODM 的分塊由兩個(gè)參數(shù)控制--split N把項(xiàng)目切成 N 塊N 通常設(shè)為“計(jì)算節(jié)點(diǎn)數(shù) × 每節(jié)點(diǎn)并行任務(wù)數(shù)”。--split-overlap 米數(shù)相鄰子塊之間的重疊距離單位是米。--split的取值不是越大越好。切得太碎每塊影像太少特征點(diǎn)匹配的冗余度下降拼接處容易出現(xiàn)接縫錯(cuò)位切得太粗單塊還是超出節(jié)點(diǎn)內(nèi)存等于沒(méi)切。我的經(jīng)驗(yàn)公式是單塊影像數(shù) ≈ 節(jié)點(diǎn)可用內(nèi)存(GB) × 8比如節(jié)點(diǎn)是 64GB 內(nèi)存單塊控制在 500 張左右比較穩(wěn)妥。假設(shè)總共有 2000 張照片、4 個(gè)計(jì)算節(jié)點(diǎn)那么--split設(shè)為 4 到 8 之間讓每個(gè)節(jié)點(diǎn)分到 1 到 2 塊。--split-overlap的取值和飛行高度、地面分辨率有關(guān)。粗略估算重疊距離取 20 到 50 米航高越高、地物越平坦取值可以小一些城市密集區(qū)、地形起伏大的區(qū)域建議取 50 米以上。重疊不足的典型癥狀是拼接處出現(xiàn)明顯的錯(cuò)位或模糊帶看到這個(gè)現(xiàn)象就回頭把 overlap 調(diào)大重跑。4. 完整實(shí)操流程與現(xiàn)場(chǎng)記錄4.1 從上傳影像到提交集群任務(wù)假設(shè)調(diào)度節(jié)點(diǎn) IP 是192.168.1.10端口 3001。整個(gè)提交流程如下第一步把影像打包上傳到調(diào)度節(jié)點(diǎn)。ClusterODM 支持通過(guò) API 上傳 zip 包也可以用它的 Web 界面拖拽。影像數(shù)量多的時(shí)候建議先在本地壓成 zip減少傳輸時(shí)的小文件開(kāi)銷。第二步構(gòu)造任務(wù)參數(shù)。用 curl 提交一個(gè)任務(wù)curl -X POST http://192.168.1.10:3001/task/new \ -F images/data/project.zip \ -F options[{\name\:\dsm\,\value\:true},{\name\:\orthophoto\,\value\:true},{\name\:\split\,\value\:8},{\name\:\split-overlap\,\value\:30},{\name\:\pc-quality\,\value\:\medium\}]返回的 JSON 里有個(gè)uuid字段記下它后續(xù)查進(jìn)度全靠這個(gè) ID。第三步輪詢?nèi)蝿?wù)狀態(tài)curl http://192.168.1.10:3001/task/uuid/info返回里的status字段會(huì)經(jīng)歷queued、running、completed等狀態(tài)processingTime是已耗時(shí)。集群跑起來(lái)后你可以同時(shí)在 ClusterODM 的 Web 界面看到每個(gè)節(jié)點(diǎn)的任務(wù)分配情況哪個(gè)節(jié)點(diǎn)在跑、跑的是哪一塊一目了然。4.2 一次真實(shí)項(xiàng)目的耗時(shí)對(duì)比記錄我拿一個(gè) 800 張照片、單張 2000 萬(wàn)像素的測(cè)繪項(xiàng)目做過(guò)對(duì)比硬件是 3 臺(tái) 16 核 64GB 的機(jī)器方案切塊數(shù)總耗時(shí)峰值內(nèi)存單機(jī)不切約 19 小時(shí)58GB接近爆內(nèi)存3 節(jié)點(diǎn)集群6約 7.5 小時(shí)每節(jié)點(diǎn)約 30GB加速比大約 2.5 倍沒(méi)有達(dá)到理論上的 3 倍損耗主要來(lái)自切塊后的合并階段和節(jié)點(diǎn)間數(shù)據(jù)傳輸。這個(gè)結(jié)果符合預(yù)期——集群加速比永遠(yuǎn)小于節(jié)點(diǎn)數(shù)因?yàn)楹喜⑹谴械膫鬏斠灿虚_(kāi)銷。如果你的項(xiàng)目影像數(shù)量少比如 100 張以內(nèi)切塊帶來(lái)的合并開(kāi)銷可能抵消并行收益這種小項(xiàng)目老老實(shí)實(shí)單機(jī)跑反而更快。4.3 結(jié)果合并與成果校驗(yàn)所有子任務(wù)完成后ClusterODM 會(huì)自動(dòng)觸發(fā)合并。合并階段最容易被忽視的是坐標(biāo)系統(tǒng)一。如果原始影像的 GPS 信息坐標(biāo)系不一致合并后的正射影像會(huì)出現(xiàn)整體偏移。提交任務(wù)前務(wù)必確認(rèn)所有照片的 EXIF 里坐標(biāo)系一致或者在參數(shù)里顯式指定--gps-accuracy。合并完成后成果文件在調(diào)度節(jié)點(diǎn)的掛載目錄下主要看三個(gè)東西正射影像orthophoto.tif、數(shù)字表面模型dsm.tif、三維點(diǎn)云odm_georeferencing 目錄。校驗(yàn)方法很簡(jiǎn)單把正射影像拖進(jìn)任意 GIS 軟件疊加原始航跡點(diǎn)看邊緣有沒(méi)有錯(cuò)位、接縫處紋理是否連續(xù)。我遇到過(guò)接縫處出現(xiàn)一條幾米寬的模糊帶回頭查是split-overlap設(shè)小了從 20 米調(diào)到 40 米重跑后消失。5. 常見(jiàn)故障排查與避坑清單5.1 節(jié)點(diǎn)掉線與任務(wù)卡死的處理集群跑長(zhǎng)任務(wù)時(shí)最常見(jiàn)的故障是某個(gè)節(jié)點(diǎn)中途掉線導(dǎo)致它負(fù)責(zé)的子任務(wù)卡在running狀態(tài)永遠(yuǎn)不結(jié)束。排查順序是這樣的先看節(jié)點(diǎn)本身是否存活curl http://節(jié)點(diǎn)IP:3000/info有沒(méi)有響應(yīng)。沒(méi)響應(yīng)就登機(jī)器看容器狀態(tài)docker ps看容器是否還在docker logs node-odm看有沒(méi)有 OOM 被殺。ODM 在稠密匹配階段內(nèi)存暴漲是常態(tài)容器被系統(tǒng) OOM killer 干掉的情況很普遍。如果節(jié)點(diǎn)活著但任務(wù)卡住可以在 ClusterODM 命令行里用task list查看任務(wù)分配必要時(shí)task cancel uuid取消后重新提交。這里有個(gè)經(jīng)驗(yàn)給計(jì)算節(jié)點(diǎn)的 Docker 容器設(shè)置內(nèi)存上限時(shí)要留足余量比如 64GB 的機(jī)器容器上限設(shè) 56GB留 8GB 給系統(tǒng)和文件緩存否則容易觸發(fā)系統(tǒng)級(jí) OOM。5.2 常見(jiàn)問(wèn)題速查表現(xiàn)象可能原因處理辦法節(jié)點(diǎn)注冊(cè)后顯示 offline防火墻攔截 3000 端口放行端口確認(rèn)節(jié)點(diǎn) IP 可達(dá)任務(wù)一直 queued 不開(kāi)始所有節(jié)點(diǎn)隊(duì)列滿或節(jié)點(diǎn)全掉線檢查節(jié)點(diǎn)狀態(tài)減少并發(fā)任務(wù)數(shù)合并后接縫錯(cuò)位split-overlap 太小調(diào)大到 40 米以上重跑合并后整體偏移影像坐標(biāo)系不一致統(tǒng)一 EXIF 坐標(biāo)系或指定 gps-accuracy節(jié)點(diǎn)容器頻繁重啟內(nèi)存不足被 OOM降低單塊影像數(shù)或加內(nèi)存?zhèn)鬏旊A段極慢網(wǎng)絡(luò)帶寬不足改用有線千兆以上壓縮影像包5.3 幾條用血換來(lái)的實(shí)操心得第一條先小規(guī)模驗(yàn)證再上全量。拿 50 張照片跑一遍完整流程確認(rèn)節(jié)點(diǎn)注冊(cè)、切塊、合并都正常再提交幾百?gòu)埖拇箜?xiàng)目。我見(jiàn)過(guò)有人直接上 1000 張跑到合并階段才發(fā)現(xiàn)參數(shù)錯(cuò)了白等一整天。第二條調(diào)度節(jié)點(diǎn)和存儲(chǔ)節(jié)點(diǎn)分開(kāi)考慮。如果影像數(shù)據(jù)量超過(guò)幾百 GB把存儲(chǔ)放在調(diào)度節(jié)點(diǎn)上會(huì)讓它的磁盤(pán) IO 成為瓶頸建議單獨(dú)掛 NAS 或者給調(diào)度節(jié)點(diǎn)配大容量 SSD。第三條記錄每次任務(wù)的參數(shù)組合。集群調(diào)參是個(gè)反復(fù)試錯(cuò)的過(guò)程split、split-overlap、pc-quality這幾個(gè)參數(shù)的不同組合效果差異很大用個(gè)簡(jiǎn)單的表格記下每次的參數(shù)和結(jié)果比憑記憶靠譜得多。第四條別迷信節(jié)點(diǎn)數(shù)量。三臺(tái)均衡的機(jī)器往往比五臺(tái)參差不齊的機(jī)器效率更高因?yàn)榧旱恼w速度受最慢那個(gè)節(jié)點(diǎn)拖累。節(jié)點(diǎn)規(guī)格盡量保持一致實(shí)在不一致就把慢節(jié)點(diǎn)分配少一點(diǎn)任務(wù)。這套集群我前后搭過(guò)三次第一次折騰了兩天才跑通后面兩次基本半天就能從零到可用。核心難點(diǎn)不在安裝而在切塊參數(shù)和故障處理上把上面這些細(xì)節(jié)吃透剩下的就是熟練度問(wèn)題。后續(xù)如果影像量繼續(xù)增長(zhǎng)還可以考慮把存儲(chǔ)層換成對(duì)象存儲(chǔ)、給調(diào)度節(jié)點(diǎn)做高可用不過(guò)那是另一個(gè)量級(jí)的工程了普通項(xiàng)目用現(xiàn)在這套足夠。