每次看到這份文件,就值得再 review 一次...
本文的貢獻者參與過數以百計的 Application 的開發和部署,並通過 Heroku 平台間接見證了數十萬 Application 的開發,運作以及擴展的過程
如今,Softare 通常會作為一種服務來交付,Softare 即服務(SaaS)。12-Factor 為構建如下的 SaaS Application提供了方法論:
特別關註於 Application 如何保持良性成長,開發者之間如何進行有效的代碼協作,以及如何 避免軟體汙染
- 使用標準化流程自動配置,從而使新的開發者花費最少的學習成本加入這個項目
- 和操作系統之間盡可能的劃清界限,在各個系統中提供最大的可移植性
- 適合部署在現代的雲計算平台,從而在 server 和系統管理方面節省資源
- 將開發環境和生產環境的差異降至最低,並使用持續交付實施敏捷開發
- 可以在工具、架構和開發流程不發生明顯變化的前提下實現擴展
- 這套理論適用於任意語言和後端服務( database 、消息隊列、緩存等)開發的 Application
讀者應該是哪些人? 任何 SaaS Application 的開發人員。部署和管理此類 Application 的運維工程師
12-Factor Application
- 通常會使用版本控制系統加以管理,一份用來跟蹤代碼所有修訂版本的 database 被稱作代碼庫(repo)
- 一份代碼庫對應多份部署
基準代碼和 Application 之間總是保持一一對應的關系:
- 一旦有多個基準代碼,就不能稱為一個 Application,而是一個分布式系統
- 分布式系統中的每一個組件都是一個 Application
- 每一個 Application 可以分別使用 12-Factor 進行開發
多個 Application 共享一份基準代碼是有悖於 12-Factor 原則的
- 解決方案是將共享的代碼拆分為獨立的類庫
- 然後使用「依賴管理」策略去加載它們
所有部署的基準代碼相同
- 但每份部署可以使用其不同的版本
- 比如,開發人員可能有一些提交還沒有同步至預發布環境
- 預發布環境也有一些提交沒有同步至生產環境
但它們都共享一份基準代碼
大多數編程語言都會提供一個打包系統,用來為各個類庫提供打包服務,就像 Perl 的 CPAN 或是 Ruby 的 Rubygems
12-Factor 規則下的 Application 程序不會隱式依賴系統級的類庫
- 它一定通過「依賴清單」,確切地聲明所有依賴項
- 此外,在運行過程中通過「依賴隔離」工具來確保程序不會調用系統中存在但清單中未聲明的依賴項
- 這一做法會統一 Application 到生產和開發環境
顯式聲明依賴的優點之一是
- 為新進開發者簡化了環境配置流程
- 新進開發者可以檢出 Application 程序的基準代碼,安裝編程語言環境和它對應的依賴管理工具
- 只需通過一個 構建命令 來安裝所有的依賴項,即可開始工作
12-Factor Application 同樣不會隱式依賴某些系統工具
- 如 ImageMagick 或是curl。即使這些工具存在於幾乎所有系統
- 但終究無法保證所有未來的系統都能支持 Application 順利運行,或是能夠和 Application 兼容
- 如果 Application 必須使用到某些系統工具,那麽這些工具應該被包含在 Application 之中
通常,Application 的「配置」在不同「部署」 (預發布、生產環境、開發環境等等)間會有很大差異
這其中包括:
- DB,Memcached,以及其他 後端服務 的配置
- 第三方服務的證書,如 Amazon S3、Twitter 等
- 每份部署特有的配置,如域名等
有些 Application
- 在代碼中使用常量保存配置,這與 12-Factor 所要求的代碼和配置嚴格分離顯然大相徑庭
- 配置文件在各部署間存在大幅差異,代碼卻完全一致
判斷一個 Application 是否正確地將配置排除在代碼之外,一個簡單的方法是
- 看該 Application 的基準代碼是否可以立刻開源,而不用擔心會暴露任何敏感的信息
需要指出的是
- 這裏定義的“配置”並不包括 Application 的內部配置
- 比如 Rails 的 config/routes.rb,或是使用 Spring 時 代碼模塊間的依賴註入關系
- 這類配置在不同部署間不存在差異,所以應該寫入代碼
另外一個解決方法是使用配置文件,但不把它們納入版本控制系統,yml
- 這相對於在代碼中使用常量已經是長足進步
- 但仍然有缺點:總是會不小心將配置文件簽入了代碼庫
- 配置文件的可能會分散在不同的目錄,並有著不同的格式
- 這讓找出一個地方來統一管理所有配置變的不太現實
- 更糟的是,這些格式通常是語言或框架特定的
12-Factor 推薦將 Application 的配置
- 存儲於 環境變量 中( env vars, env )
- 環境變量可以非常方便地在不同的部署間做修改,卻不動一行代碼
- 與配置文件不同,不小心把它們簽入代碼庫的概率微乎其微
- 與一些傳統的解決配置問題的機制(比如 Java 的屬性配置文件)相比,環境變量與語言和系統無關
12-Factor Application 中
- 環境變量的粒度要足夠小,且相對獨立
- 它們永遠也不會組合成一個所謂的“環境”
- 而是獨立存在於每個部署之中
- 當 Application 程序不斷擴展,需要更多種類的部署時,這種配置管理方式能夠做到平滑過渡
如 database (MySQL,CouchDB),消息/隊列系統(RabbitMQ,Beanstalkd),SMTP 郵件發送服務(Postfix),以及緩存系統(Memcached)
類似 database 的後端服務
- 通常由部署 Application 的系統管理員一起管理
- 除了本地服務之外, Application 有可能使用了第三方發布和管理的服務
- 示例包括 SMTP(例如 Postmark)
- data 收集服務(例如 New Relic 或 Loggly)
- data 存儲服務(如 Amazon S3)
- 以及使用 API 訪問的服務(例如 Twitter, Google Maps)
12-Factor Application 不會區別對待本地或第三方服務。 對 Application 而言
- 兩種都是附加資源 �- 通過一個 url 或是其他存儲在「配置」中的服務定位/服務證書來獲取 data
12-Factor Application 的任意「部署」,都應該可以
- 在不進行任何代碼改動的情況下,將本地 MySQL database 換成第三方服務(例如 Amazon RDS)
- 類似的,本地 SMTP 服務應該也可以和第三方 SMTP 服務(例如 Postmark )互換
- 上述 2 個例子中,僅需修改配置中的資源地址
每個不同的後端服務是一份 資源 。例如
- 一個 MySQL database 是一個資源
- 兩個 MySQL database (用來 data 分區)就被當作是 2 個不同的資源
- 12-Factor Application 將這些 database 都視作 附加資源
- 這些資源和它們附屬的部署保持松耦合
部署可以按需加載或卸載資源。例如
- 如果 Application 的 database 服務由於硬件問題出現異常
- 管理員可以從最近的備份中恢覆一個 database ,卸載當前的 database
- 然後加載新的 database – 整個過程都不需要修改代碼
基準代碼轉化為一份部署(非開發環境)需要以下三個階段:
- 構建階段
- 是指將代碼倉庫轉化為可執行包的過程。構建時會使用指定版本的代碼,獲取和打包 依賴項,編譯成二進制文件和資源文件
- 發布階段
- 會將構建的結果和當前部署所需 配置 相結合,並能夠立刻在運行環境中投入使用
- 運行階段 (或者說“運行時”)
- 是指針對選定的發布版本,在執行環境中啟動一系列 Application 程序進程
代碼被構建,然後和配置結合成為發布版本
12-factor Application
- 嚴格區分構建,發布,運行這三個步驟
- 舉例來說,直接修改處於運行狀態的代碼是非常不可取的做法
- 因為這些修改很難再同步回構建步驟
部署工具通常都提供了發布管理工具
- 最引人註目的功能是退回至較舊的發布版本
- 工具的 rollback 命令可以很容易地實現回退版本的功能
每一個發布版本
- 必須對應一個唯一的發布 ID,例如可以使用發布時的時間戳(2011-04-06-20:32:17)
- 亦或是一個增長的數字(v100)
- 發布的版本就像一本只能追加的賬本,一旦發布就不可修改
- 任何的變動都應該產生一個新的發布版本
新的代碼
- 在部署之前,需要開發人員觸發構建操作
- 但是,運行階段不一定需要人為觸發,而是可以自動進行
- 如 server 重啟,或是進程管理器重啟了一個崩潰的進程
因此,運行階段應該保持盡可能少的模塊
- 這樣假設半夜發生系統故障而開發人員又捉襟見肘也不會引起太大問題
構建階段是可以相對覆雜一些的
- 因為錯誤信息能夠立刻展示在開發人員面前,從而得到妥善處理
運行環境中
- Application 通常是以一個和多個 進程 運行的
12-Factor Application 的 process
- 必須無狀態且無共享
- 任何需要持久化的 data 都要存儲在「後端服務」內,比如 database
RAM or disk 可以作為
- process 在做某種事務型操作時的緩存
- 例如下載一個很大的文件,對其操作並將結果寫入 database 的過程
12-Factor Application
- 不用考慮這些緩存的內容是不是可以保留給之後的請求來使用
- 因為 Application 啟動了多種類型的 process,後續的請求多半會由其他 process 來服務
- 即使在只有一個 process 的情形下,先前保存的 data (內存或文件系統中)也會因為重啟(如代碼部署、配置更改、或運行環境將 process 調度至另一個物理區域執行)而丟失
一些互聯網系統依賴於 “stick session”
- 將用戶 session 中的 data 緩存至某 process 的內存中,並將同一用戶的後續請求路由到同一個 process
stick session 是 12-Factor 極力反對的
- Session 中的 data 應該保存在諸如 Memcached 或 Redis 這樣的帶有過期時間的緩存中
互聯網 Application 有時會運行於 server 的容器之中
- 如 Java 運行於 Tomcat
12-Factor Application
- 完全自我加載而不依賴於任何網絡 server 就可以創建一個面向網絡的服務
- 互聯網 Application 通過 port 綁定來提供服務 ,並監聽發送至該 port 的請求
本地環境中
- 開發人員通過類似 http://localhost:5000/ 的地址來訪問服務
在線上環境中
- 請求統一發送至公共域名而後路由至綁定了 port 的網絡 process
HTTP 並不是唯一一個可以由 port 綁定提供的服務。其實幾乎所有 server 軟體都可以通過 process 綁定port 來等待請求。例如,使用 XMPP 的 ejabberd , 以及使用 Redis 協議 的 Redis
還要指出的是
- port 綁定這種方式也意味著一個 Application 可以成為另外一個 Application的 後端服務
- 調用方將服務方提供的相應 URL 當作資源存入 配置 以備將來調用
任何計算機程序,一旦啟動,就會生成一個或多個 process
- 在 12-factor Application 中,process 是一等公民
- 12-Factor Application 的 process 主要借鑒於 unix 守護 process 模型
- 開發人員可以運用這個模型去設計 Application 架構,將不同的工作分配給不同的 process 類型
- 例如,HTTP 請求可以交給 web process 來處理
- 而常駐的後台工作則交由 worker process 負責
這並不包括個別較為特殊的 process
- 例如通過虛擬機的線程處理並發的內部運算
- 或是使用諸如 EventMachine, Twisted, Node.js 的異步/事件觸發模型
但一台獨立的虛擬機的擴展有瓶頸(垂直擴展),所以
- Application 必須可以在多台物理機器間跨 process 工作
上述 process 模型會在系統急需擴展時大放異彩
- 12-Factor Application 的 process 所具備的無共享,水平分區的特性
- 意味著添加並發會變得簡單而穩妥
12-Factor Application 的 process
- 不需要守護 process 或是寫入 PID 文件
相反的,應該借助操作系統的 process 管理器(例如 systemd ,分布式的 process 管理雲平台,或是類似 Foreman 的工具),來管理 輸出流,響應崩潰的 process
12-Factor Application 的 process 是
- 易處理(disposable)的
- 意思是說它們可以瞬間開啟或停止
這有利於快速、彈性的 scale Application,迅速部署變化的 代碼 或 配置,穩健的部署 Application。
process 應當追求
- 最小啟動時間
- 更少的啟動時間提供了更敏捷的 發布 以及擴展過程
- process 一旦接收 終止信號(SIGTERM)就會優雅的終止
- 就網絡 process 而言,優雅終止是指停止監聽服務的端口
- 即拒絕所有新的請求
- 並繼續執行當前已接收的請求
- 然後退出
- 就網絡 process 而言,優雅終止是指停止監聽服務的端口
此類型的 process 所隱含的要求是HTTP請求大多都很短(不會超過幾秒鐘),而在長時間輪詢中,客戶端在丟失連接後應該馬上嘗試重連
對於 worker process 來說
- 優雅終止是指將當前任務退回隊列。例如,RabbitMQ 中,worker 可以發送一個NACK信號
- 此類型的 process 所隱含的要求是,任務都應該 可重覆執行
Application 的所有部署,這其中包括開發、預發布以及線上環境,都應該使用同一個後端服務的相同版本
以經驗來看
- 開發環境(即開發人員的本地 部署)
- 線上環境(外部用戶訪問的真實部署)
之間存在著很多差異。這些差異表現在以下三個方面:
- 時間差異:開發人員正在編寫的代碼可能需要幾天,幾周,甚至幾個月才會上線
- 人員差異:開發人員編寫代碼,運維人員部署代碼
- 工具差異:開發人員或許使用 Nginx,SQLite,OS X,而線上環境使用 Apache,MySQL 以及 Linux
要做到「持續部署 CI」就必須縮小本地與線上差異。再回頭看上面所描述的三個差異:
- 縮小時間差異:開發人員可以幾小時,甚至幾分鐘就部署代碼
- 縮小人員差異:開發人員不只要編寫代碼,更應該密切參與部署過程以及代碼在線上的表現
- 縮小工具差異:盡量保證開發環境以及線上環境的一致性
12-Factor Application 的開發人員應該
- 反對在不同環境間使用不同的後端服務
因為
- 不同的後端服務意味著會突然出現的不兼容
- 從而導致測試、預發布都正常的代碼在線上出現問題
- 這些錯誤會給持續部署帶來阻力
- 從 Application 的生命周期來看,消除這種阻力需要花費很大的代價
在基於 server 的環境中
- log 通常被寫在 disk 的一個文件裏,但這只是一種輸出格式
- log 應該是 事件流 的匯總,將所有運行中進程和後端服務的輸出流按照時間順序收集起來
- 盡管在回溯問題時可能需要看很多行
- log 最原始的格式確實是一個事件一行
- log 沒有確定開始和結束,但隨著 Application在運行會持續的增加
12-factor Application
- 本身從不考慮存儲自己的輸出流
- 不應該試圖去寫或者管理log 文件
- 相反,每一個運行的進程都會直接的標準輸出(stdout)事件流
- 開發環境中,開發人員可以通過這些 data 流,實時在終端看到 Application 的活動
在預發布或線上部署中
- 每個進程的輸出流由運行環境截獲,並將其他輸出流整理在一起
- 然後一並發送給一個或多個最終的處理程序,用於查看或是長期存檔
- 這些存檔路徑對於 Application 來說不可見也不可配置
- 而是完全交給程序的運行環境管理。類似 Logplex 和 Fluentd 的開源工具可以達到這個目的
這些事件流
- 可以輸出至文件,或者在終端實時觀察
- 最重要的,輸出流可以發送到 Splunk 這樣的log 索引及分析系統,或 Hadoop/Hive
這些系統為查看 Application的歷史活動提供了強大而靈活的功能,包括:
- 找出過去一段時間特殊的事件
- 圖形化一個大規模的趨勢,比如每分鐘的請求量
- 根據用戶定義的條件實時觸發警報,比如每分鐘的報錯超過某個警戒線
process 構成(process formation) 0 是指用來處理 Application 的常規業務(比如處理 web 請求)的一組 process
- 與此不同,開發人員經常希望執行一些管理或維護 Application 的一次性任務
例如:
- 運行 data 移植
- 運行一個控制台,來執行一些代碼或是針對線上 database 做一些檢查
一次性管理 process
- 應該和正常的 常駐 process 使用同樣的環境
- 這些管理 process 和任何其他的 process 一樣使用相同的 代碼 和 配置,基於某個 發布版本 運行
- 後台管理代碼應該隨其他 Application 代碼一起發布,從而避免同步問題
12-factor 尤其青睞那些提供了 REPL shell 的語言
- 那會讓運行一次性腳本變得簡單
- 在本地部署中,開發人員直接在命令行使用 shell 命令調用一次性管理 process
- 在線上部署中,開發人員依舊可以使用ssh或是運行環境提供的其他機制來運行這樣的 process