與設(shè)計模式)
1. 項目概述為什么我們要深挖Apollo的scripts子模塊如果你正在研究百度Apollo自動駕駛平臺或者你的團(tuán)隊正在基于Apollo進(jìn)行二次開發(fā)那么你遲早會碰到一個看似不起眼實則至關(guān)重要的目錄scripts。這個文件夾里塞滿了各種以.sh、.py、.bat結(jié)尾的腳本文件從環(huán)境搭建、代碼編譯、容器管理到系統(tǒng)監(jiān)控幾乎無所不包。很多新手開發(fā)者甚至一些有經(jīng)驗的工程師往往只把它們當(dāng)作“黑盒”工具來用需要啟動Docker時就運(yùn)行./docker/scripts/dev_start.sh需要編譯時就敲./apollo.sh build。但很少有人停下來思考這些腳本是如何組織在一起的它們背后遵循著怎樣的設(shè)計邏輯為什么Apollo團(tuán)隊要選擇這樣的架構(gòu)這就是我們今天要深入剖析的“scripts子模塊軟件架構(gòu)”。理解它遠(yuǎn)不止是滿足技術(shù)好奇心。它能讓你在遇到環(huán)境配置失敗、編譯報錯、容器啟動異常時不再像個無頭蒼蠅一樣四處搜索而是能精準(zhǔn)定位問題根源。它能讓你在需要定制化開發(fā)流程、集成新的硬件或軟件棧時知道從哪里入手修改而不會破壞整個系統(tǒng)的穩(wěn)定性和可維護(hù)性。更進(jìn)一步這種通過腳本進(jìn)行復(fù)雜系統(tǒng)生命周期管理的模式本身就是一種值得學(xué)習(xí)的軟件工程實踐尤其適用于大型、異構(gòu)、依賴復(fù)雜的項目。簡單來說scripts子模塊是Apollo平臺的“神經(jīng)系統(tǒng)”和“自動化流水線”。它封裝了平臺底層環(huán)境的復(fù)雜性為上層應(yīng)用開發(fā)提供了統(tǒng)一、簡潔的入口。本次分析我們將像解構(gòu)一個精密的機(jī)械鐘表一樣層層拆解這個子模塊看看它的齒輪腳本是如何嚙合發(fā)條設(shè)計思想又是如何驅(qū)動的。2. 核心架構(gòu)思想與設(shè)計模式解析Apolloscripts子模塊的架構(gòu)并非一蹴而就它體現(xiàn)了在大型開源項目中管理復(fù)雜性的經(jīng)典智慧。其核心思想可以概括為“約定優(yōu)于配置分層解耦職責(zé)單一”。2.1 分層與模塊化設(shè)計這是最顯著的特征。scripts目錄不是一堆腳本的簡單堆積而是有清晰層次結(jié)構(gòu)的。第一層入口與分發(fā)層這一層由位于根目錄或scripts/頂級目錄下的少數(shù)幾個核心腳本構(gòu)成例如最著名的apollo.sh。這個腳本本身不干具體的“臟活累活”它更像一個總調(diào)度中心或命令行路由器。它的核心職責(zé)是參數(shù)解析識別用戶輸入的命令如build,clean,cyber_visualizer。環(huán)境檢查驗證當(dāng)前目錄、Docker環(huán)境、用戶權(quán)限等前置條件。任務(wù)分發(fā)根據(jù)解析出的命令將實際工作委托給下一層更專業(yè)的腳本去執(zhí)行。例如./apollo.sh build最終可能會調(diào)用scripts/apollo_build.sh。這種設(shè)計的好處是為用戶提供了一個穩(wěn)定、統(tǒng)一的交互界面。無論Apollo內(nèi)部如何迭代只要apollo.sh的接口不變用戶的使用習(xí)慣就不用改變。同時它將復(fù)雜的邏輯判斷集中在一處便于維護(hù)。第二層功能模塊層這一層根據(jù)功能領(lǐng)域進(jìn)行劃分通常以子目錄的形式組織。常見的模塊包括docker/scripts/所有與Docker容器生命周期管理相關(guān)的腳本如啟動(dev_start.sh)、進(jìn)入(dev_into.sh)、停止(dev_stop.sh)。canbus/,localization/,perception/等模塊目錄下的scripts/這些是模塊級腳本負(fù)責(zé)該模塊特定的測試、數(shù)據(jù)回放或工具啟動。它們體現(xiàn)了架構(gòu)的縱向解耦每個模塊可以獨立管理自己的輔助工具鏈。scripts/根下的功能腳本如apollo_build.sh編譯,apollo_config.sh配置管理,apollo_base.sh基礎(chǔ)函數(shù)庫等。這些是橫向的通用功能組件。第三層基礎(chǔ)庫與工具層這一層包含被其他腳本頻繁引用的公共函數(shù)和工具腳本。最典型的是apollo_base.sh。這個腳本定義了大量的Shell函數(shù)例如顏色輸出函數(shù) (info,warn,error,ok)用于在終端輸出帶顏色的、格式統(tǒng)一的信息提升可讀性。環(huán)境變量設(shè)置函數(shù)集中管理PYTHONPATH,LD_LIBRARY_PATH,CYBER_PATH等關(guān)鍵路徑。常用工具檢查函數(shù)檢查docker,nvidia-docker,git等必要工具是否存在且版本合適。錯誤處理與退出函數(shù)提供標(biāo)準(zhǔn)的錯誤退出流程。通過source apollo_base.sh其他腳本可以輕松復(fù)用這些功能保證了代碼的一致性和可維護(hù)性避免了“復(fù)制粘貼”編程。2.2 設(shè)計模式的應(yīng)用工廠方法模式 (Factory Method)apollo.sh根據(jù)不同的命令參數(shù)動態(tài)“生產(chǎn)”并執(zhí)行對應(yīng)的功能腳本。用戶無需關(guān)心具體是哪個腳本完成了build工作他們只與工廠apollo.sh交互。外觀模式 (Facade)整個scripts子模塊為Apollo復(fù)雜的構(gòu)建、部署、運(yùn)行系統(tǒng)提供了一個簡化的接口apollo.sh及其主要命令。它隱藏了背后涉及Docker、Bazel/Catkin、ROS/Cyber RT、各種依賴包的復(fù)雜性。模板方法模式 (Template Method)在基礎(chǔ)庫apollo_base.sh中定義算法骨架例如“啟動服務(wù)”的通用流程檢查環(huán)境-加載配置-啟動進(jìn)程-檢查狀態(tài)具體的步驟由子腳本或調(diào)用者填充。這在許多服務(wù)啟動腳本中能看到影子。職責(zé)鏈模式 (Chain of Responsibility)在環(huán)境檢查和初始化過程中體現(xiàn)明顯。一個腳本可能會依次檢查是否為Apollo根目錄-Docker是否安裝-Docker服務(wù)是否運(yùn)行-鏡像是否存在-容器狀態(tài)如何。每一步檢查都是一個“處理器”只有當(dāng)前一步通過責(zé)任鏈才會傳遞到下一步。2.3 配置與數(shù)據(jù)分離腳本邏輯本身與可配置的數(shù)據(jù)是分離的。例如容器鏡像的標(biāo)簽、版本號通常定義在單獨的.env文件或scripts/apollo_config.sh中。不同硬件平臺如NVIDIA Jetson vs. x86的差異化配置可能通過傳入不同的參數(shù)或讀取不同的配置文件來激活。用戶自定義的Docker鏡像倉庫地址、代理設(shè)置等也鼓勵通過環(huán)境變量或配置文件來設(shè)置而不是硬編碼在腳本里。這種分離使得定制和適配變得非常靈活也符合十二要素應(yīng)用開發(fā)方法論中的“配置存儲在環(huán)境中”的原則。注意理解這些設(shè)計模式不是為了生搬硬套概念而是為了給你一套分析工具。當(dāng)你在閱讀或修改一個陌生腳本時可以嘗試用這些模式去套一套往往能更快地理解作者的意圖和腳本的結(jié)構(gòu)。3. 關(guān)鍵腳本深度剖析與執(zhí)行流程讓我們深入到幾個最具代表性的腳本內(nèi)部看看它們是如何具體運(yùn)作的。我們將以一次典型的“從零開始構(gòu)建并啟動Apollo”的流程為主線。3.1 環(huán)境啟動的基石docker/scripts/dev_start.sh這是幾乎所有Apollo開發(fā)者的第一個命令。它的工作流程堪稱經(jīng)典引導(dǎo)與參數(shù)解析腳本開頭會source引用apollo_base.sh等基礎(chǔ)庫然后解析用戶傳入的參數(shù)如-l本地模式不使用GPU、-g使用GPU、-t指定鏡像標(biāo)簽、-p指定自定義參數(shù)傳遞給docker run。環(huán)境預(yù)檢調(diào)用基礎(chǔ)庫中的函數(shù)檢查Docker是否安裝、Docker服務(wù)是否運(yùn)行、用戶是否有權(quán)限、是否在Apollo根目錄下執(zhí)行。對于-g選項還會額外檢查NVIDIA Docker Runtime是否可用。鏡像管理拉取策略腳本會檢查本地是否存在指定的Docker鏡像。如果不存在則嘗試從默認(rèn)倉庫如apolloauto/apollo拉取。這里通常會有鏡像標(biāo)簽的拼接邏輯例如將用戶輸入的標(biāo)簽與基礎(chǔ)名稱組合。構(gòu)建策略在某些版本或分支中腳本可能支持-f選項強(qiáng)制從本地的Dockerfile重新構(gòu)建鏡像這對于深度定制開發(fā)非常有用。容器創(chuàng)建與啟動這是核心步驟。腳本會構(gòu)造一個非常長的docker run命令。這個命令包含了Apollo容器化的精髓資源限制設(shè)置CPU、內(nèi)存限制--cpus,--memory。設(shè)備映射通過--device映射GPU設(shè)備如果使用GPU通過--privileged或更細(xì)粒度的--cap-add來賦予容器必要的權(quán)限如訪問CAN卡、USB設(shè)備。文件系統(tǒng)映射這是實現(xiàn)“宿主機(jī)開發(fā)容器內(nèi)運(yùn)行”的關(guān)鍵。通過-v參數(shù)將宿主機(jī)上的Apollo代碼目錄、數(shù)據(jù)目錄、甚至用戶家目錄下的某些配置文件映射到容器內(nèi)的對應(yīng)路徑。特別注意這里通常使用$(pwd)來獲取當(dāng)前Apollo根目錄的絕對路徑確保映射準(zhǔn)確。網(wǎng)絡(luò)與IPC使用--net host讓容器共享宿主機(jī)的網(wǎng)絡(luò)命名空間簡化網(wǎng)絡(luò)通信特別是與外部硬件、其他ROS節(jié)點的通信。有時也會使用--ipchost共享IPC命名空間。環(huán)境變量注入通過-e設(shè)置容器內(nèi)的環(huán)境變量如DISPLAY用于GUI應(yīng)用、QT_X11_NO_MITSHM1解決某些圖形顯示問題。入口點通常設(shè)置為一個自定義的啟動腳本如/apollo/scripts/docker_start.sh該腳本在容器啟動后執(zhí)行負(fù)責(zé)容器內(nèi)部的進(jìn)一步初始化。狀態(tài)驗證與用戶提示容器啟動后腳本可能會執(zhí)行docker ps來驗證容器是否在運(yùn)行并打印出容器的ID和名稱。最后它會提示用戶使用./docker/scripts/dev_into.sh進(jìn)入容器。實操心得當(dāng)你因為端口占用、權(quán)限不足、鏡像拉取失敗導(dǎo)致dev_start.sh執(zhí)行失敗時不要慌。最有效的調(diào)試方法是在腳本中關(guān)鍵步驟后添加echo語句或者直接查看它最終拼接出的那個超長的docker run命令。你可以把腳本中構(gòu)造命令的那一行通常是docker run ...打印出來然后手動執(zhí)行這個命令的簡化版往往能發(fā)現(xiàn)環(huán)境變量錯誤、路徑不對、設(shè)備權(quán)限等具體問題。3.2 核心樞紐apollo.sh的調(diào)度邏輯apollo.sh是一個用Bash寫的簡單但強(qiáng)大的分發(fā)器。我們來看它的典型結(jié)構(gòu)#!/usr/bin/env bash source $(dirname ${BASH_SOURCE[0]})/scripts/apollo_base.sh function main() { local cmd$1 shift # 移除第一個參數(shù)cmd剩下的參數(shù)傳遞給子函數(shù) case $cmd in build) bash scripts/apollo_build.sh $ ;; build_gpu) bash scripts/apollo_build.sh --gpu $ ;; build_opt) bash scripts/apollo_build.sh --opt $ ;; build_no_perception) bash scripts/apollo_build.sh noperception $ ;; test) bash scripts/apollo_test.sh $ ;; clean) bash scripts/apollo_clean.sh $ ;; config) bash scripts/apollo_config.sh $ ;; # ... 其他很多命令如 release, version, format, lint 等 *) echo Unknown command: $cmd echo Try ./apollo.sh --help for more information. exit 1 ;; esac } main $它的設(shè)計巧妙之處在于極簡的維護(hù)要添加一個新命令只需在case語句中添加一個分支指向一個新的功能腳本即可。參數(shù)的透明傳遞使用shift和$可以將用戶輸入給apollo.sh的額外參數(shù)原封不動地傳遞給底層腳本。例如./apollo.sh build --jobs 8--jobs 8會被傳遞給apollo_build.sh。幫助信息生成很多版本的apollo.sh會通過解析case語句或單獨的幫助文本動態(tài)生成--help信息。3.3 構(gòu)建引擎scripts/apollo_build.sh構(gòu)建腳本是Apollo開發(fā)中的高頻操作。它主要封裝了底層構(gòu)建系統(tǒng)從早期的Catkin到現(xiàn)在的Bazel的調(diào)用。構(gòu)建類型選擇腳本通常支持多種構(gòu)建類型通過參數(shù)控制--gpu構(gòu)建GPU版本的模塊如感知模塊。--opt優(yōu)化編譯-O3用于發(fā)布。--dbg調(diào)試編譯-g用于開發(fā)。noperception跳過感知模塊的構(gòu)建常用于快速驗證其他模塊。環(huán)境準(zhǔn)備在容器內(nèi)它會再次確認(rèn)必要的環(huán)境變量如CYBER_PATH是否已設(shè)置。它可能會調(diào)用apollo_config.sh來加載當(dāng)前的硬件平臺配置。調(diào)用底層構(gòu)建命令核心就是執(zhí)行bazel build //modules/...或類似的命令。但腳本會做很多優(yōu)化工作并行控制通過--jobs參數(shù)控制并行編譯任務(wù)數(shù)充分利用多核CPU。緩存管理Bazel本身有強(qiáng)大的緩存腳本可能會在構(gòu)建前執(zhí)行bazel clean --expunge在apollo_clean.sh中或bazel sync來確保依賴正確。資源限制在容器環(huán)境中腳本可能需要根據(jù)容器分配的CPU和內(nèi)存資源動態(tài)調(diào)整Bazel的--local_resources參數(shù)防止構(gòu)建過程耗盡資源導(dǎo)致容器崩潰。輸出處理與錯誤處理腳本會捕獲bazel命令的輸出和退出碼。對于成功構(gòu)建它可能只摘要性提示“Build passed”。對于失敗構(gòu)建它會嘗試提取和打印關(guān)鍵的錯誤信息如編譯錯誤、鏈接錯誤并返回非零退出碼。常見問題排查構(gòu)建內(nèi)存不足如果你在構(gòu)建大型模塊如perception時遇到編譯器被kill通常是OOM你需要調(diào)整Docker容器的內(nèi)存限制在dev_start.sh的docker run命令中修改-m參數(shù)或者在apollo_build.sh中降低--jobs數(shù)。第三方依賴下載失敗Bazel構(gòu)建中依賴下載失敗很常見。腳本可能沒有完善的重試機(jī)制。此時你需要手動檢查網(wǎng)絡(luò)或查看bazel輸出中具體的下載URL嘗試手動下載并放置到Bazel的緩存目錄中。4. 高級主題擴(kuò)展性與定制化開發(fā)指南當(dāng)你不再滿足于使用現(xiàn)成的腳本而是需要為你的特定傳感器、算法或硬件平臺定制開發(fā)流程時理解如何擴(kuò)展scripts架構(gòu)就至關(guān)重要了。4.1 添加一個新的模塊級腳本假設(shè)你為modules/contribution目錄開發(fā)了一個新的算法模塊并希望為它添加一個一鍵測試腳本。創(chuàng)建腳本在modules/contribution/scripts/目錄下創(chuàng)建你的腳本例如run_my_algorithm_test.sh。遵循規(guī)范開頭source必要的公共庫如$(dirname ${BASH_SOURCE[0]})/../../scripts/apollo_base.sh注意相對路徑的跳轉(zhuǎn)。使用apollo_base.sh中定義的info、error等函數(shù)進(jìn)行輸出。做好參數(shù)解析可以使用getopts并提供--help信息。在腳本末尾根據(jù)執(zhí)行結(jié)果以正確的退出碼結(jié)束成功為0失敗為非0。集成到總?cè)肟诳蛇x但推薦如果你希望這個測試命令能通過./apollo.sh調(diào)用你需要修改apollo.sh。在case語句中添加一個新的分支contribution_test) bash modules/contribution/scripts/run_my_algorithm_test.sh $ ;;這樣用戶就可以通過./apollo.sh contribution_test來運(yùn)行你的測試了。4.2 定制Docker開發(fā)環(huán)境Apollo的默認(rèn)Docker鏡像包含了大部分通用依賴。但如果你需要安裝額外的系統(tǒng)包如特定的串口工具。預(yù)裝某個特定版本的Python庫。配置特殊的UDEV規(guī)則來識別你的定制硬件。你有兩種主要方式方式一修改Dockerfile并重建鏡像這是最徹底的方式。找到Apollo根目錄下的Dockerfile.*可能有多個版本在合適的位置例如在安裝系統(tǒng)包的部分添加你的RUN apt-get install -y your-package指令。然后修改docker/scripts/dev_start.sh使其在啟動時使用你本地構(gòu)建的鏡像通過-f選項或修改默認(rèn)鏡像名邏輯。方式二在容器啟動后腳本中安裝Apollo容器啟動后通常會執(zhí)行一個入口腳本如/apollo/scripts/docker_start.sh。你可以修改這個腳本在里面添加安裝命令。但要注意這會導(dǎo)致每次啟動容器都執(zhí)行一次安裝適合安裝輕量級或經(jīng)常變化的依賴。更優(yōu)雅的做法是將你的定制安裝步驟寫成一個單獨的腳本然后在你的本地啟動流程中在dev_into.sh之后手動執(zhí)行它。4.3 實現(xiàn)多環(huán)境配置管理Apollo需要適配不同的車輛和硬件。scripts/apollo_config.sh是這個機(jī)制的核心。它通常會讀取一個配置文件如scripts/apollo_config.xml或modules/common/data/global_flagfile.txt來設(shè)置一系列環(huán)境變量如APOLLO_GPU_ENABLED: 是否啟用GPU。APOLLO_PLATFORM: 平臺類型如x86_64,aarch64。各個模塊的啟動參數(shù)。定制化實踐你可以創(chuàng)建自己的配置文件例如my_vehicle_config.sh。在其中設(shè)置你獨有的環(huán)境變量如export MY_LIDAR_MODELHDL-64E。在apollo_base.sh或你的模塊腳本開頭source這個配置文件。在你的C或Python代碼中通過std::getenv()或os.environ來讀取這些環(huán)境變量從而實現(xiàn)條件編譯或運(yùn)行時配置。這種模式將配置從代碼中剝離使得同一套代碼可以輕松地在仿真環(huán)境、測試車、不同型號的實車上切換。5. 故障排查與調(diào)試技巧實錄即使理解了架構(gòu)在實際操作中仍會遇到各種問題。下面是一些常見問題的排查思路和“止血”技巧。5.1 Docker容器啟動失敗問題現(xiàn)象可能原因排查步驟與解決方案Error response from daemon: ... conflict: container name is already in use.已存在同名容器。docker ps -a查看所有容器用docker rm -f apollo_dev強(qiáng)制刪除舊容器后再啟動。docker: Error response from daemon: could not select device driver ... with capabilities: [[gpu]].NVIDIA Docker運(yùn)行時未安裝或未配置。運(yùn)行nvidia-smi驗證驅(qū)動運(yùn)行docker run --rm --gpus all nvidia/cuda:11.0-base nvidia-smi測試nvidia-docker。確保dev_start.sh使用了-g參數(shù)。Cannot connect to the Docker daemon at unix:///var/run/docker.sock.Docker服務(wù)未啟動或當(dāng)前用戶不在docker組。sudo systemctl start docker將用戶加入docker組sudo usermod -aG docker $USER需要重新登錄。啟動后容器立即退出 (Exited)。入口點腳本執(zhí)行失敗。docker logs apollo_dev查看容器日志。常見原因是映射的宿主機(jī)目錄不存在或權(quán)限不足。檢查dev_start.sh中的-v映射路徑。5.2 構(gòu)建過程中的典型錯誤問題現(xiàn)象可能原因排查步驟與解決方案Bazel BUILD file not found你不在Apollo根目錄或者Bazel工作空間未正確設(shè)置。確保在Apollo根目錄執(zhí)行。運(yùn)行bazel info workspace查看當(dāng)前工作空間。C compilation of rule //modules/... failed代碼語法錯誤、頭文件找不到、依賴缺失。仔細(xì)閱讀錯誤信息Bazel的錯誤輸出通常很詳細(xì)。關(guān)注第一個報錯??赡苁侨鄙倌硞€第三方庫需要在對應(yīng)的BUILD文件中添加deps。Downloading ... FAILED網(wǎng)絡(luò)問題無法下載依賴如glog, protobuf等。嘗試配置Bazel的代理。或者根據(jù)錯誤URL手動下載放入~/.cache/bazel目錄下對應(yīng)的位置??梢运阉鳌癰azel 離線編譯”尋找解決方案。Out of memory或編譯器進(jìn)程被殺死。編譯過程內(nèi)存不足。減少并行編譯任務(wù)./apollo.sh build --jobs 2。增加Docker容器內(nèi)存限制在dev_start.sh中修改-m參數(shù)例如-m 8g。5.3 運(yùn)行時腳本問題問題現(xiàn)象可能原因排查步驟與解決方案./apollo.sh: line X: syntax error near unexpected token腳本語法錯誤可能是換行符問題Windows編輯后傳到Linux。使用dos2unix script.sh轉(zhuǎn)換文件格式。使用cat -A script.sh檢查行尾是否為^M$。source: not found在非Bash shell如sh中執(zhí)行了source命令。確保腳本第一行是#!/usr/bin/env bash。執(zhí)行時用bash script.sh而非sh script.sh。function not found未成功source包含函數(shù)定義的公共庫文件。檢查apollo_base.sh等庫文件的路徑是否正確。使用絕對路徑或可靠的相對路徑進(jìn)行source。終極調(diào)試心法逐層剝離與日志追蹤當(dāng)遇到復(fù)雜問題時最有效的方法是將自動化腳本手動執(zhí)行一遍。剝離容器層如果懷疑是容器內(nèi)問題先用dev_into.sh進(jìn)入容器然后在容器內(nèi)手動執(zhí)行失敗的命令如bazel build觀察輸出。剝離腳本層如果懷疑是腳本邏輯問題在腳本的關(guān)鍵決策點如if,case語句后和命令執(zhí)行前添加set -x或echo語句打印出變量的值和即將執(zhí)行的命令。然后運(yùn)行腳本看實際執(zhí)行流程與預(yù)期是否一致。追蹤環(huán)境變量在腳本開頭和函數(shù)調(diào)用前后打印關(guān)鍵的環(huán)境變量如PATH,LD_LIBRARY_PATH,APOLLO_HOME確保它們被正確設(shè)置。理解Apolloscripts子模塊的架構(gòu)就像拿到了一張自動駕駛平臺的“電氣原理圖”。它不能讓你立刻成為感知或規(guī)劃專家但它能讓你在平臺層游刃有余高效地搭建環(huán)境、調(diào)試問題、定制流程。從被腳本“驅(qū)使”的開發(fā)者轉(zhuǎn)變?yōu)椤榜{馭”腳本的工程師這其中的提升對于深入?yún)⑴c任何大型開源項目都是無價的。下次當(dāng)你再運(yùn)行./apollo.sh時希望你能感受到背后那一整套精妙設(shè)計的自動化體系在為你工作。