簡介
如今,軟件通常會作為一種服務來交付,它們被稱為網絡應用程序,或軟件即服務(SaaS)。12-Factor 為構建如下的 SaaS 應用提供了方法論:
使用標準化流程自動配置,從而使新的開發者花費最少的學習成本加入這個項目。
和操作系統之間盡可能的劃清界限,在各個系統中提供最大的可移植性。
適合部署在現代的云計算平臺,從而在服務器和系統管理方面節省資源。
將開發環境和生產環境的差異降至最低,并使用持續交付實施敏捷開發。
可以在工具、架構和開發流程不發生明顯變化的前提下實現擴展。
這套理論適用于任意語言和后端服務(數據庫、消息隊列、緩存等)開發的應用程序。
背景
本文的貢獻者參與過數以百計的應用程序的開發和部署,并通過?Heroku?平臺間接見證了數十萬應用程序的開發,運作以及擴展的過程。
本文綜合了我們關于 SaaS 應用幾乎所有的經驗和智慧,是開發此類應用的理想實踐標準,并特別關注于應用程序如何保持良性成長,開發者之間如何進行有效的代碼協作,以及如何?避免軟件污染?。
我們的初衷是分享在現代軟件開發過程中發現的一些系統性問題,并加深對這些問題的認識。我們提供了討論這些問題時所需的共享詞匯,同時使用相關術語給出一套針對這些問題的廣義解決方案。本文格式的靈感來自于 Martin Fowler 的書籍:?Patterns of Enterprise Application Architecture?,?Refactoring?。
讀者應該是哪些人?
任何 SaaS 應用的開發人員。部署和管理此類應用的運維工程師。
I. 基準代碼
一份基準代碼(Codebase),多份部署(deploy)
12-Factor應用(譯者注:應該是說一個使用本文概念來設計的應用,下同)通常會使用版本控制系統加以管理,如Git,?Mercurial,?Subversion。一份用來跟蹤代碼所有修訂版本的數據庫被稱作?代碼庫(code repository, code repo, repo)。
在類似 SVN 這樣的集中式版本控制系統中,基準代碼?就是指控制系統中的這一份代碼庫;而在 Git 那樣的分布式版本控制系統中,基準代碼?則是指最上游的那份代碼庫。
一份代碼庫對應多份部署
基準代碼和應用之間總是保持一一對應的關系:
一旦有多個基準代碼,就不能稱為一個應用,而是一個分布式系統。分布式系統中的每一個組件都是一個應用,每一個應用可以分別使用 12-Factor 進行開發。
多個應用共享一份基準代碼是有悖于 12-Factor 原則的。解決方案是將共享的代碼拆分為獨立的類庫,然后使用?依賴管理?策略去加載它們。
盡管每個應用只對應一份基準代碼,但可以同時存在多份部署。每份?部署?相當于運行了一個應用的實例。通常會有一個生產環境,一個或多個預發布環境。此外,每個開發人員都會在自己本地環境運行一個應用實例,這些都相當于一份部署。
所有部署的基準代碼相同,但每份部署可以使用其不同的版本。比如,開發人員可能有一些提交還沒有同步至預發布環境;預發布環境也有一些提交沒有同步至生產環境。但它們都共享一份基準代碼,我們就認為它們只是相同應用的不同部署而已。
II. 依賴
顯式聲明依賴關系(?dependency?)
大多數編程語言都會提供一個打包系統,用來為各個類庫提供打包服務,就像 Perl 的?CPAN?或是 Ruby 的?Rubygems?。通過打包系統安裝的類庫可以是系統級的(稱之為 “site packages”),或僅供某個應用程序使用,部署在相應的目錄中(稱之為 “vendoring” 或 “bunding”)。
12-Factor規則下的應用程序不會隱式依賴系統級的類庫。?它一定通過?依賴清單?,確切地聲明所有依賴項。此外,在運行過程中通過?依賴隔離?工具來確保程序不會調用系統中存在但清單中未聲明的依賴項。這一做法會統一應用到生產和開發環境。
例如, Ruby 的?Bundler?使用?Gemfile?作為依賴項聲明清單,使用?bundle exec?來進行依賴隔離。Python 中則可分別使用兩種工具 –?Pip?用作依賴聲明,?Virtualenv?用作依賴隔離。甚至 C 語言也有類似工具,?Autoconf?用作依賴聲明,靜態鏈接庫用作依賴隔離。無論用什么工具,依賴聲明和依賴隔離必須一起使用,否則無法滿足 12-Factor 規范。
顯式聲明依賴的優點之一是為新進開發者簡化了環境配置流程。新進開發者可以檢出應用程序的基準代碼,安裝編程語言環境和它對應的依賴管理工具,只需通過一個?構建命令?來安裝所有的依賴項,即可開始工作。例如,Ruby/Bundler 下使用?bundle install,而 Clojure/Leiningen?則是?lein deps。
12-Factor 應用同樣不會隱式依賴某些系統工具,如 ImageMagick 或是curl。即使這些工具存在于幾乎所有系統,但終究無法保證所有未來的系統都能支持應用順利運行,或是能夠和應用兼容。如果應用必須使用到某些系統工具,那么這些工具應該被包含在應用之中。
III. 配置
在環境中存儲配置
通常,應用的?配置?在不同?部署?(預發布、生產環境、開發環境等等)間會有很大差異。這其中包括:
數據庫,Memcached,以及其他?后端服務?的配置
第三方服務的證書,如 Amazon S3、Twitter 等
每份部署特有的配置,如域名等
有些應用在代碼中使用常量保存配置,這與 12-Factor 所要求的代碼和配置嚴格分離顯然大相徑庭。配置文件在各部署間存在大幅差異,代碼卻完全一致。
判斷一個應用是否正確地將配置排除在代碼之外,一個簡單的方法是看該應用的基準代碼是否可以立刻開源,而不用擔心會暴露任何敏感的信息。
需要指出的是,這里定義的“配置”并不包括應用的內部配置,比如 Rails 的?config/routes.rb,或是使用?Spring?時?代碼模塊間的依賴注入關系?。這類配置在不同部署間不存在差異,所以應該寫入代碼。
另外一個解決方法是使用配置文件,但不把它們納入版本控制系統,就像 Rails 的?config/database.yml?。這相對于在代碼中使用常量已經是長足進步,但仍然有缺點:總是會不小心將配置文件簽入了代碼庫;配置文件的可能會分散在不同的目錄,并有著不同的格式,這讓找出一個地方來統一管理所有配置變的不太現實。更糟的是,這些格式通常是語言或框架特定的。
12-Factor推薦將應用的配置存儲于 環境變量 中(?env vars,?env?)。環境變量可以非常方便地在不同的部署間做修改,卻不動一行代碼;與配置文件不同,不小心把它們簽入代碼庫的概率微乎其微;與一些傳統的解決配置問題的機制(比如 Java 的屬性配置文件)相比,環境變量與語言和系統無關。
配置管理的另一個方面是分組。有時應用會將配置按照特定部署進行分組(或叫做“環境”),例如Rails中的?development,test, 和?production?環境。這種方法無法輕易擴展:更多部署意味著更多新的環境,例如?staging或?qa?。 隨著項目的不斷深入,開發人員可能還會添加他們自己的環境,比如?joes-staging?,這將導致各種配置組合的激增,從而給管理部署增加了很多不確定因素。
12-Factor 應用中,環境變量的粒度要足夠小,且相對獨立。它們永遠也不會組合成一個所謂的“環境”,而是獨立存在于每個部署之中。當應用程序不斷擴展,需要更多種類的部署時,這種配置管理方式能夠做到平滑過渡。
IV. 后端服務
把后端服務(backing services)當作附加資源
后端服務是指程序運行所需要的通過網絡調用的各種服務,如數據庫(MySQL,CouchDB),消息/隊列系統(RabbitMQ,Beanstalkd),SMTP 郵件發送服務(Postfix),以及緩存系統(Memcached)。
類似數據庫的后端服務,通常由部署應用程序的系統管理員一起管理。除了本地服務之外,應用程序有可能使用了第三方發布和管理的服務。示例包括 SMTP(例如?Postmark),數據收集服務(例如?New Relic?或?Loggly),數據存儲服務(如?Amazon S3),以及使用 API 訪問的服務(例如?Twitter,?Google Maps,?Last.fm)。
12-Factor 應用不會區別對待本地或第三方服務。?對應用程序而言,兩種都是附加資源,通過一個 url 或是其他存儲在?配置?中的服務定位/服務證書來獲取數據。12-Factor 應用的任意?部署?,都應該可以在不進行任何代碼改動的情況下,將本地 MySQL 數據庫換成第三方服務(例如?Amazon RDS)。類似的,本地 SMTP 服務應該也可以和第三方 SMTP 服務(例如 Postmark )互換。上述 2 個例子中,僅需修改配置中的資源地址。
每個不同的后端服務是一份?資源?。例如,一個 MySQL 數據庫是一個資源,兩個 MySQL 數據庫(用來數據分區)就被當作是 2 個不同的資源。12-Factor 應用將這些數據庫都視作?附加資源?,這些資源和它們附屬的部署保持松耦合。
一種部署附加4個后端服務
部署可以按需加載或卸載資源。例如,如果應用的數據庫服務由于硬件問題出現異常,管理員可以從最近的備份中恢復一個數據庫,卸載當前的數據庫,然后加載新的數據庫 – 整個過程都不需要修改代碼。
V. 構建,發布,運行
嚴格分離構建和運行
基準代碼?轉化為一份部署(非開發環境)需要以下三個階段:
構建階段?是指將代碼倉庫轉化為可執行包的過程。構建時會使用指定版本的代碼,獲取和打包?依賴項,編譯成二進制文件和資源文件。
發布階段?會將構建的結果和當前部署所需?配置?相結合,并能夠立刻在運行環境中投入使用。
運行階段?(或者說“運行時”)是指針對選定的發布版本,在執行環境中啟動一系列應用程序?進程。
代碼被構建,然后和配置結合成為發布版本
12-factor 應用嚴格區分構建,發布,運行這三個步驟。?舉例來說,直接修改處于運行狀態的代碼是非常不可取的做法,因為這些修改很難再同步回構建步驟。
部署工具通常都提供了發布管理工具,最引人注目的功能是退回至較舊的發布版本。比如,?Capistrano?將所有發布版本都存儲在一個叫?releases?的子目錄中,當前的在線版本只需映射至對應的目錄即可。該工具的?rollback?命令可以很容易地實現回退版本的功能。
每一個發布版本必須對應一個唯一的發布 ID,例如可以使用發布時的時間戳(2011-04-06-20:32:17),亦或是一個增長的數字(v100)。發布的版本就像一本只能追加的賬本,一旦發布就不可修改,任何的變動都應該產生一個新的發布版本。
新的代碼在部署之前,需要開發人員觸發構建操作。但是,運行階段不一定需要人為觸發,而是可以自動進行。如服務器重啟,或是進程管理器重啟了一個崩潰的進程。因此,運行階段應該保持盡可能少的模塊,這樣假設半夜發生系統故障而開發人員又捉襟見肘也不會引起太大問題。構建階段是可以相對復雜一些的,因為錯誤信息能夠立刻展示在開發人員面前,從而得到妥善處理。
VI. 進程
以一個或多個無狀態進程運行應用
運行環境中,應用程序通常是以一個和多個?進程?運行的。
最簡單的場景中,代碼是一個獨立的腳本,運行環境是開發人員自己的筆記本電腦,進程由一條命令行(例如python my_script.py)。另外一個極端情況是,復雜的應用可能會使用很多?進程類型?,也就是零個或多個進程實例。
12-Factor 應用的進程必須無狀態且 無共享 。?任何需要持久化的數據都要存儲在?后端服務?內,比如數據庫。
內存區域或磁盤空間可以作為進程在做某種事務型操作時的緩存,例如下載一個很大的文件,對其操作并將結果寫入數據庫的過程。12-Factor應用根本不用考慮這些緩存的內容是不是可以保留給之后的請求來使用,這是因為應用啟動了多種類型的進程,將來的請求多半會由其他進程來服務。即使在只有一個進程的情形下,先前保存的數據(內存或文件系統中)也會因為重啟(如代碼部署、配置更改、或運行環境將進程調度至另一個物理區域執行)而丟失。
源文件打包工具(Jammit,?django-compressor) 使用文件系統來緩存編譯過的源文件。12-Factor 應用更傾向于在?構建步驟做此動作——正如?Rails資源管道?,而不是在運行階段。
一些互聯網系統依賴于 “粘性 session”, 這是指將用戶 session 中的數據緩存至某進程的內存中,并將同一用戶的后續請求路由到同一個進程。粘性 session 是 12-Factor 極力反對的。Session 中的數據應該保存在諸如?Memcached?或?Redis?這樣的帶有過期時間的緩存中。
VII. 端口綁定
通過端口綁定(Port binding)來提供服務
互聯網應用有時會運行于服務器的容器之中。例如 PHP 經常作為?Apache HTTPD?的一個模塊來運行,正如 Java 運行于?Tomcat?。
12-Factor 應用完全自我加載?而不依賴于任何網絡服務器就可以創建一個面向網絡的服務。互聯網應用?通過端口綁定來提供服務?,并監聽發送至該端口的請求。
本地環境中,開發人員通過類似http://localhost:5000/的地址來訪問服務。在線上環境中,請求統一發送至公共域名而后路由至綁定了端口的網絡進程。
通常的實現思路是,將網絡服務器類庫通過?依賴聲明?載入應用。例如,Python 的?Tornado, Ruby 的Thin?, Java 以及其他基于 JVM 語言的?Jetty。完全由?用戶端?,確切的說應該是應用的代碼,發起請求。和運行環境約定好綁定的端口即可處理這些請求。
HTTP 并不是唯一一個可以由端口綁定提供的服務。其實幾乎所有服務器軟件都可以通過進程綁定端口來等待請求。例如,使用?XMPP?的?ejabberd?, 以及使用?Redis 協議?的?Redis?。
還要指出的是,端口綁定這種方式也意味著一個應用可以成為另外一個應用的?后端服務,調用方將服務方提供的相應 URL 當作資源存入?配置?以備將來調用。
VIII. 并發
通過進程模型進行擴展
任何計算機程序,一旦啟動,就會生成一個或多個進程。互聯網應用采用多種進程運行方式。例如,PHP 進程作為 Apache 的子進程存在,隨請求按需啟動。Java 進程則采取了相反的方式,在程序啟動之初 JVM 就提供了一個超級進程儲備了大量的系統資源(CPU 和內存),并通過多線程實現內部的并發管理。上述 2 個例子中,進程是開發人員可以操作的最小單位。
擴展表現為運行中的進程,工作多樣性表現為進程類型。
在 12-factor 應用中,進程是一等公民。12-Factor 應用的進程主要借鑒于?unix 守護進程模型?。開發人員可以運用這個模型去設計應用架構,將不同的工作分配給不同的?進程類型?。例如,HTTP 請求可以交給 web 進程來處理,而常駐的后臺工作則交由 worker 進程負責。
這并不包括個別較為特殊的進程,例如通過虛擬機的線程處理并發的內部運算,或是使用諸如?EventMachine,?Twisted,?Node.js?的異步/事件觸發模型。但一臺獨立的虛擬機的擴展有瓶頸(垂直擴展),所以應用程序必須可以在多臺物理機器間跨進程工作。
上述進程模型會在系統急需擴展時大放異彩。?12-Factor 應用的進程所具備的無共享,水平分區的特性?意味著添加并發會變得簡單而穩妥。這些進程的類型以及每個類型中進程的數量就被稱作?進程構成?。
12-Factor 應用的進程?不需要守護進程?或是寫入 PID 文件。相反的,應該借助操作系統的進程管理器(例如?systemd?,分布式的進程管理云平臺,或是類似?Foreman?的工具),來管理?輸出流?,響應崩潰的進程,以及處理用戶觸發的重啟和關閉超級進程的請求。
IX. 易處理
快速啟動和優雅終止可最大化健壯性
12-Factor 應用的 進程 是 易處理(disposable)的,意思是說它們可以瞬間開啟或停止。?這有利于快速、彈性的伸縮應用,迅速部署變化的?代碼?或?配置?,穩健的部署應用。
進程應當追求?最小啟動時間?。 理想狀態下,進程從敲下命令到真正啟動并等待請求的時間應該只需很短的時間。更少的啟動時間提供了更敏捷的?發布?以及擴展過程,此外還增加了健壯性,因為進程管理器可以在授權情形下容易的將進程搬到新的物理機器上。
進程?一旦接收 終止信號(SIGTERM) 就會優雅的終止?。就網絡進程而言,優雅終止是指停止監聽服務的端口,即拒絕所有新的請求,并繼續執行當前已接收的請求,然后退出。此類型的進程所隱含的要求是HTTP請求大多都很短(不會超過幾秒鐘),而在長時間輪詢中,客戶端在丟失連接后應該馬上嘗試重連。
對于 worker 進程來說,優雅終止是指將當前任務退回隊列。例如,RabbitMQ?中,worker 可以發送一個NACK信號。?Beanstalkd?中,任務終止并退回隊列會在worker斷開時自動觸發。有鎖機制的系統諸如?Delayed Job?則需要確定釋放了系統資源。此類型的進程所隱含的要求是,任務都應該?可重復執行?, 這主要由將結果包裝進事務或是使重復操作?冪等?來實現。
進程還應當在面對突然死亡時保持健壯,例如底層硬件故障。雖然這種情況比起優雅終止來說少之又少,但終究有可能發生。一種推薦的方式是使用一個健壯的后端隊列,例如?Beanstalkd?,它可以在客戶端斷開或超時后自動退回任務。無論如何,12-Factor 應用都應該可以設計能夠應對意外的、不優雅的終結。Crash-only design?將這種概念轉化為?合乎邏輯的理論。
X. 開發環境與線上環境等價
盡可能的保持開發,預發布,線上環境相同
從以往經驗來看,開發環境(即開發人員的本地?部署)和線上環境(外部用戶訪問的真實部署)之間存在著很多差異。這些差異表現在以下三個方面:
時間差異:?開發人員正在編寫的代碼可能需要幾天,幾周,甚至幾個月才會上線。
人員差異:?開發人員編寫代碼,運維人員部署代碼。
工具差異:?開發人員或許使用 Nginx,SQLite,OS X,而線上環境使用 Apache,MySQL 以及 Linux。
12-Factor 應用想要做到 持續部署 就必須縮小本地與線上差異。?再回頭看上面所描述的三個差異:
縮小時間差異:開發人員可以幾小時,甚至幾分鐘就部署代碼。
縮小人員差異:開發人員不只要編寫代碼,更應該密切參與部署過程以及代碼在線上的表現。
縮小工具差異:盡量保證開發環境以及線上環境的一致性。
將上述總結變為一個表格如下:
傳統應用12-Factor 應用
每次部署間隔數周幾小時
開發人員 vs 運維人員不同的人相同的人
開發環境 vs 線上環境不同盡量接近
后端服務?是保持開發與線上等價的重要部分,例如數據庫,隊列系統,以及緩存。許多語言都提供了簡化獲取后端服務的類庫,例如不同類型服務的?適配器?。下列表格提供了一些例子。
類型語言類庫適配器
數據庫Ruby/RailsActiveRecordMySQL, PostgreSQL, SQLite
隊列Python/DjangoCeleryRabbitMQ, Beanstalkd, Redis
緩存Ruby/RailsActiveSupport::CacheMemory, filesystem, Memcached
開發人員有時會覺得在本地環境中使用輕量的后端服務具有很強的吸引力,而那些更重量級的健壯的后端服務應該使用在生產環境。例如,本地使用 SQLite 線上使用 PostgreSQL;又如本地緩存在進程內存中而線上存入 Memcached。
12-Factor 應用的開發人員應該反對在不同環境間使用不同的后端服務?,即使適配器已經可以幾乎消除使用上的差異。這是因為,不同的后端服務意味著會突然出現的不兼容,從而導致測試、預發布都正常的代碼在線上出現問題。這些錯誤會給持續部署帶來阻力。從應用程序的生命周期來看,消除這種阻力需要花費很大的代價。
與此同時,輕量的本地服務也不像以前那樣引人注目。借助于Homebrew,apt-get等現代的打包系統,諸如Memcached、PostgreSQL、RabbitMQ 等后端服務的安裝與運行也并不復雜。此外,使用類似?Chef?和?Puppet?的聲明式配置工具,結合像?Vagrant?這樣輕量的虛擬環境就可以使得開發人員的本地環境與線上環境無限接近。與同步環境和持續部署所帶來的益處相比,安裝這些系統顯然是值得的。
不同后端服務的適配器仍然是有用的,因為它們可以使移植后端服務變得簡單。但應用的所有部署,這其中包括開發、預發布以及線上環境,都應該使用同一個后端服務的相同版本。
XI. 日志
把日志當作事件流
日志?使得應用程序運行的動作變得透明。在基于服務器的環境中,日志通常被寫在硬盤的一個文件里,但這只是一種輸出格式。
日志應該是?事件流?的匯總,將所有運行中進程和后端服務的輸出流按照時間順序收集起來。盡管在回溯問題時可能需要看很多行,日志最原始的格式確實是一個事件一行。日志沒有確定開始和結束,但隨著應用在運行會持續的增加。
12-factor應用本身從不考慮存儲自己的輸出流。?不應該試圖去寫或者管理日志文件。相反,每一個運行的進程都會直接的標準輸出(stdout)事件流。開發環境中,開發人員可以通過這些數據流,實時在終端看到應用的活動。
在預發布或線上部署中,每個進程的輸出流由運行環境截獲,并將其他輸出流整理在一起,然后一并發送給一個或多個最終的處理程序,用于查看或是長期存檔。這些存檔路徑對于應用來說不可見也不可配置,而是完全交給程序的運行環境管理。類似?Logplex和?Fluentd?的開源工具可以達到這個目的。
這些事件流可以輸出至文件,或者在終端實時觀察。最重要的,輸出流可以發送到?Splunk?這樣的日志索引及分析系統,或?Hadoop/Hive?這樣的通用數據存儲系統。這些系統為查看應用的歷史活動提供了強大而靈活的功能,包括:
找出過去一段時間特殊的事件。
圖形化一個大規模的趨勢,比如每分鐘的請求量。
根據用戶定義的條件實時觸發警報,比如每分鐘的報錯超過某個警戒線。