1. 程式人生 > >Cloud Native 與12-Factor

Cloud Native 與12-Factor

開發十年,就只剩下這套架構體系了! >>>   

12-Factor(twelve-factor),也稱為“十二要素”,是一套流行的應用程式開發原則。Cloud Native架構中使用12-Factor作為設計準則。

12-Factor 的目標在於:

  • 使用標準化流程自動配置,從而使新的開發者花費最少的學習成本加入專案中。
  • 和底層作業系統之間儘可能的劃清界限,在各個系統中提供最大的可移植性。
  • 適合部署在現代的雲端計算平臺,從而在伺服器和系統管理方面節省資源。
  • 將開發環境和生產環境的差異降至最低,並使用持續交付實施敏捷開發。
  • 可以在工具、架構和開發流程不發生明顯變化的前提下實現擴充套件。

12-Factor 可以適用於任意語言和後端服務(資料庫、訊息佇列、快取等)開發的應用程式,自然也適用於 Cloud Native。在構建 Cloud Native 應用時,也需要考慮這十二個方面的內容。

1 基準程式碼

程式碼是程式的根本,有什麼樣的程式碼最終會表現為怎麼樣的程式軟體。從原始碼到產品釋出中間會經歷多個環節,比如開發、編譯、測試、構建、部署等,這些環節可能都有自己的不同的部署環境,而不同的環境相應的責任人關注於產品的不同階段。比如,測試人員主要關注於測試的結果,而業務人員可能關注於生產環境的最終的部署結果。但不管是哪個環節,部署到怎麼的環境中,他們所依賴的程式碼是一致的,即所謂的“一份基準程式碼(Codebase),多份部署(Deploy)”。

現代的程式碼管理,往往需要進行版本的管理。即便是個人的工作,採用版本管理工具進行管理,對於方便查詢特定版本的內容,或者是回溯歷史的修改內容都是極其必要。版本控制系統就是幫助人們協調工作的工具,它能夠幫助我們和其他小組成員監測同一組檔案,比如說軟體原始碼,升級過程中所做的變更,也就是說,它可以幫助我們輕鬆地將工作進行融合。

版本控制工具發展到現在已經有幾十年了,簡單地可以將其分為四代:

  • 檔案式版本控制系統,比如 SCCS、RCS;
  • 樹狀版本控制系統—伺服器模式,比如 CVS;
  • 樹狀版本控制系統—雙伺服器模式,比如 Subversion;
  • 樹狀版本控制系統—分散式模式,比如 Bazaar、Mercurial、Git。

目前,在企業中廣泛採用伺服器模式的版本控制系統,但越來越多的企業開始傾向於採用分散式模式版本控制系統。

讀者如果對版本控制系統感興趣,可以參閱筆者所著的《分散式系統常用技術及案例分析》中的“第7章分散式版本控制系統”內容。本書“10.3 程式碼管理”章節部分,還會繼續深入探討 Git 的使用。

2 依賴

應該明確宣告應用程式依賴關係(Dpendency),這樣,所有的依賴關係都可以從工件的儲存庫中獲得,並且可以使用依賴管理器(例如 Apache Maven、Gradle)進行下載。

顯式宣告依賴的優點之一是為新進開發者簡化了環境配置流程。新進開發者可以檢出應用程式的基準程式碼,安裝程式語言環境和它對應的依賴管理工具,只需通過一個構建命令來安裝所有的依賴項,即可開始工作。

比如,專案組統一採用 Gradle 來進行依賴管理。那麼可以使用 Gradle Wrapper。Gradle Wrapper 免去了使用者在使用 Gradle 進行專案構建時需要安裝 Gradle 的繁瑣步驟。每個 Gradle Wrapper 都繫結到一個特定版本的 Gradle,所以當你第一次在給定 Gradle 版本下執行上面的命令之一時,它將下載相應的 Gradle 釋出包,並使用它來執行構建。預設,Gradle Wrapper 的釋出包是指向的官網的 Web 服務地址,相關配置記錄在了 gradle-wrapper.properties 檔案中。我們檢視下 Sring Boot 提供的這個 Gradle Wrapper 的配置,引數“distributionUrl”就是用於指定釋出包的位置。

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip

而這個 gradle-wrapper.properties 檔案是作為依賴項,而納入程式碼儲存庫中的。

3 配置

相同的應用,在不同的部署環境(如預釋出、生產環境、開發環境等等)下,可能有不同的配置內容。這其中包括:

  • 資料庫、Redis 以及其他後端服務的配置;
  • 第三方服務的證書;
  • 每份部署特有的配置,如域名等。

這些配置項不可能硬編碼在程式碼中,因為我們必須要保證同一份基準程式碼(Codebase)能夠多份部署。一種解決方法是使用配置檔案,但不把它們納入版本控制系統,就像 Rails 的 config/database.yml。這相對於在程式碼中硬編碼常量已經是長足進步,但仍然有缺點:

  • 不小心將配置檔案簽入了程式碼庫;
  • 配置檔案的可能會分散在不同的目錄,並有著不同的格式,不方便統一管理;
  • 這些格式通常是語言或框架特定的,不具備通用性。

所以,推薦的做法是將應用的配置儲存於環境變數中。好處在於:

  • 環境變數可以非常方便地在不同的部署間做修改,卻不動一行程式碼;
  • 與配置檔案不同,不小心把它們簽入程式碼庫的概率微乎其微;
  • 與一些傳統的解決配置問題的機制(比如 Java 的屬性配置檔案)相比,環境變數與語言和系統無關。

本書介紹了另外一種解決方案——集中化配置中心。通過配置中心來集中化管理各個環境的配置變數。配置中心的實現也是於具體語言和系統無關的。欲瞭解有關配置中心的內容,可以參閱本書“10.5 配置管理”章節的內容。

4 後端服務

後端服務(Backing Services)是指程式執行所需要的通過網路呼叫的各種服務,如資料庫(MySQL,CouchDB),訊息/佇列系統(RabbitMQ,Beanstalkd),SMTP 郵件傳送服務(Postfix),以及快取系統(Memcached,Redis)。

這些後端服務,通常由部署應用程式的系統管理員一起管理。除了本地服務之外,應用程式有可能使用了第三方釋出和管理的服務。示例包括 SMTP(例如 Postmark),資料收集服務(例如 New Relic 或 Loggly),資料儲存服務(如 Amazon S3),以及使用 API 訪問的服務(例如 Twitter、Google Maps 等等)。

12-Factor 應用不會區別對待本地或第三方服務。對應用程式而言,本地或第三方服務都是附加資源,都可以通過一個 URI 或是其他儲存在配置中的服務定位或服務證書來獲取資料。12-Factor 應用的任意部署,都應該可以在不進行任何程式碼改動的情況下,將本地 MySQL 資料庫換成第三方服務(例如 Amazon RDS)。類似的,本地 SMTP 服務應該也可以和第三方 SMTP 服務(例如 Postmark )互換。比如,在上述兩個例子中,僅需修改配置中的資源地址。

每個不同的後端服務都是一份資源 。例如,一個 MySQL 資料庫是一個資源,兩個 MySQL 資料庫(用來資料分割槽)就被當作是兩個不同的資源。12-Factor 應用將這些資料庫都視作附加資源,這些資源和它們附屬的部署保持鬆耦合。

使用後端服務的好處在於,部署可以按需載入或解除安裝資源。例如,如果應用的資料庫服務由於硬體問題出現異常,管理員可以從最近的備份中恢復一個數據庫,解除安裝當前的資料庫,然後載入新的資料庫,整個過程都不需要修改程式碼。

5 構建、釋出、執行

基準程式碼進行部署需要以下三個階段:

  • 構建階段:是指將程式碼倉庫轉化為可執行包的過程。構建時會使用指定版本的程式碼,獲取和打包依賴項,編譯成二進位制檔案和資原始檔。
  • 釋出階段:會將構建的結果和當前部署所需配置相結合,並能夠立刻在執行環境中投入使用。
  • 執行階段:是指標對選定的釋出版本,在執行環境中啟動一系列應用程式程序。

應用嚴格區分構建、釋出、執行這三個步驟。舉例來說,直接修改處於執行狀態的程式碼是非常不可取的做法,因為這些修改很難再同步回構建步驟。

部署工具通常都提供了釋出管理工具,在必要的時候還是可以退回至較舊的釋出版本。

每一個釋出版本必須對應一個唯一的釋出 ID,例如可以使用釋出時的時間戳(2011-04-06-20:32:17),亦或是一個增長的數字(v100)。釋出的版本就像一本只能追加的賬本,一旦釋出就不可修改,任何的變動都應該產生一個新的釋出版本。

新的程式碼在部署之前,需要開發人員觸發構建操作。但是,執行階段不一定需要人為觸發,而是可以自動進行。如伺服器重啟,或是程序管理器重啟了一個崩潰的程序。因此,執行階段應該保持儘可能少的模組,這樣假設半夜發生系統故障而開發人員又捉襟見肘也不會引起太大問題。構建階段是可以相對複雜一些的,因為錯誤資訊能夠立刻展示在開發人員面前,從而得到妥善處理。

6 程序

12-Factor 應用推薦以一個或多個無狀態程序執行應用。這裡的“無狀態”是與 REST 中的無狀態是一個意思,即程序的執行不依賴於上一個程序的執行。

舉例來說,記憶體區域或磁碟空間可以作為程序在做某種事務型操作時的快取,例如下載一個很大的檔案,對其操作並將結果寫入資料庫的過程。12-Factor 應用根本不用考慮這些快取的內容是不是可以保留給之後的請求來使用,這是因為應用啟動了多種型別的程序,將來的請求多半會由其他程序來服務。即使在只有一個程序的情形下,先前儲存的資料(記憶體或檔案系統中)也會因為重啟(如程式碼部署、配置更改、或執行環境將程序排程至另一個物理區域執行)而丟失。

一些網際網路應用依賴於“粘性 session”, 這是指將使用者 session 中的資料快取至某程序的記憶體中,並將同一使用者的後續請求路由到同一個程序。粘性 session 是 12-Factor 極力反對的。Session 中的資料應該儲存在諸如 Memcached 或 Redis 這樣的帶有過期時間的快取中。

相比於有狀態的應用而言,無狀態具有更好的可擴充套件性。

7 埠繫結

傳統的網際網路應用有時會運行於伺服器的容器之中。例如 PHP 經常作為 Apache HTTPD 的一個模組來執行,而 Java 應用往往會運行於 Tomcat 中。

12-Factor 應用完全具備自我載入的能力,而不依賴於任何網路伺服器就可以建立一個面向網路的服務。網際網路應用通過埠繫結(Port binding)來提供服務,並監聽傳送至該埠的請求。

舉例來說,Java 程式完全能夠內嵌一個 Tomcat 在程式中,從而自己就能啟動並提供服務,省去了將 Java 應用部署到 Tomcat 中的繁瑣過程。在這方面,Spring Boot 框架的佈道者 Josh Long 有句名言“Make JAR not WAR”,即 Java 應用程式應該被打包為可以獨立執行的 JAR 檔案,而不是傳統的 WAR 包。

以 Spring Boot 為例,構建一個具有內嵌容器的 Java 應用是非常簡單的,只需要引入以下依賴:

// 依賴關係
dependencies {

	// 該依賴用於編譯階段
	compile('org.springframework.boot:spring-boot-starter-web')

}

這樣,該 Spring Boot 應用就包含了內嵌 Tomcat 容器。

如果想使用其他容器,比如 Jetty、Undertow 等,只需要在依賴中加入相應 Servlet 容器的 Starter 就能實現預設容器的替換,比如:

  • spring-boot-starter-jetty:使用 Jetty 作為內嵌容器,可以替換 spring-boot-starter-tomcat;
  • spring-boot-starter-undertow:使用 Undertow 作為內嵌容器,可以替換 spring-boot-starter-tomcat。

可以使用 Spring Environment 屬性配置常見的 Servlet 容器的相關設定。通常您將在 application.properties 檔案中來定義屬性。

常見的 Servlet 容器設定包括:

  • 網路設定:監聽 HTTP 請求的埠(server.port)、繫結到 server.address 的介面地址等;
  • 會話設定:會話是否持久(server.session.persistence)、會話超時(server.session.timeout)、會話資料的位置(server.session.store-dir)和會話 cookie 配置(server.session.cookie.*);
  • 錯誤管理:錯誤頁面的位置(server.error.path)等;
  • SSL;
  • HTTP 壓縮。

Spring Boot 儘可能地嘗試公開這些常見公用設定,但也會有一些特殊的配置。對於這些例外的情況,Spring Boot 提供了專用名稱空間來對應特定於伺服器的配置(比如 server.tomcat 和 server.undertow)。

8 併發

在 12-factor 應用中,程序是一等公民。由於程序之間不會共享狀態,這意味著應用可以通過程序的擴充套件來實現併發。

類似於 unix 守護程序模型,開發人員可以運用這個模型去設計應用架構,將不同的工作分配給不同的程序。例如,HTTP 請求可以交給 web 程序來處理,而常駐的後臺工作則交由 worker 程序負責。

在 Java 語言中,往往通過多執行緒的方式來實現程式的併發。執行緒允許在同一個程序中同時存在多個執行緒控制流。執行緒會共享程序範圍內的資源,例如記憶體控制代碼和檔案控制代碼,但每個執行緒都有各自的程式計數器、棧以及區域性變數。執行緒還提供了一種直觀的分解模式來充分利用作業系統中的硬體並行性,而在同一個程式中的多個執行緒也可以被同時排程到多個CPU上執行。

毫無疑問,多執行緒程式設計使得程式任務併發成為了可能。而併發控制主要是為了解決多個執行緒之間資源爭奪等問題。併發一般發生在資料聚合的地方,只要有聚合,就有爭奪發生,傳統解決爭奪的方式採取執行緒鎖機制,這是強行對CPU管理執行緒進行人為干預,執行緒喚醒成本高,新的無鎖併發策略來源於非同步程式設計、非阻塞I/O等程式設計模型。

併發的使用並非沒有風險。多執行緒併發會帶來如下的問題:

  • 安全性問題。在沒有充足同步的情況下,多個執行緒中的操作執行順序是不可預測的,甚至會產生奇怪的結果。執行緒間的通訊主要是通過共享訪問欄位及其欄位所引用的物件來實現的。這種形式的通訊是非常有效的,但可能導致兩種錯誤:執行緒干擾(thread interference)和記憶體一致性錯誤(memory consistency errors)。
  • 活躍度問題。一個並行應用程式的及時執行能力被稱為它的活躍度(liveness)。安全性的含義是“永遠不發生糟糕的事情”,而活躍度則關注於另外一個目標,即“某件正確的事情最終會發生”。當某個操作無法繼續執行下去,就會發生活躍度問題。在序列程式中,活躍度問題形式之一就是無意中造成的無限迴圈(死迴圈)。而在多執行緒程式中,常見的活躍度問題主要有死鎖、飢餓以及活鎖。
  • 效能問題。在設計良好的併發應用程式中,執行緒能提升程式的效能,但無論如何,執行緒總是帶來某種程度的執行時開銷。而這種開銷主要是線上程排程器臨時關起活躍執行緒並轉而執行另外一個執行緒的上下文切換操作(Context Switch)上,因為執行上下文切換,需要儲存和恢復執行上下文,丟失區域性性,並且CPU時間將更多地花線上程排程而不執行緒執行上。當執行緒共享資料時,必須使用同步機制,而這些機制往往會抑制某些編譯器優化,使記憶體快取區中的資料無效,以及增加貢獻記憶體匯流排的同步流量。所以這些因素都會帶來額外的效能開銷。

9 易處理

12-Factor 應用的程序是易處理(Disposable)的,意味著它們可以瞬間啟動或停止。比如,Spring Boot 應用,它可以無需依賴容器,而採用內嵌容器的方式來實現自啟動。這有利於迅速部署變化的程式碼或配置,保障系統的可用性,並在系統負荷到來前,快速實現擴充套件。

程序應當追求最小啟動時間。 理想狀態下,程序從敲下命令到真正啟動並等待請求的時間應該只需很短的時間。更少的啟動時間提供了更敏捷的釋出以及擴充套件過程,此外還增加了健壯性,因為程序管理器可以在授權情形下容易的將程序搬到新的物理機器上。

程序一旦接收終止訊號(SIGTERM)就會優雅的終止。就網路程序而言,優雅終止是指停止監聽服務的埠,即拒絕所有新的請求,並繼續執行當前已接收的請求,然後退出。

對於 worker 程序來說,優雅終止是指將當前任務退回佇列。例如,RabbitMQ 中,worker 可以傳送一個 NACK 訊號。Beanstalkd 中,任務終止並退回佇列會在 worker 斷開時自動觸發。有鎖機制的系統諸如 Delayed Job 則需要確定釋放了系統資源。

10 開發環境與線上環境等價

我們期望一份基準程式碼可以部署到多個環境,但如果環境不一致,最終也可能導致執行程式的結果不一致。

比如,在開發環境,我們是採用了 MySQL 作為測試資料庫,而在線上生產環境,則是採用了 Oracle。雖然,MySQL 和 Oracle 都遵循相同的 SQL 標準,但兩者在很多語法上還是存在細微的差異。這些差異非常有可能導致兩者的執行結果不一致,甚至某些 SQL 語句在開發環境能夠正常執行,而在線上環境根本無法執行。這都給除錯增加了複雜性,同時,也無法保障最終的測試效果。

所以,一個好的指導意見是,不同的環境儘量保持一樣。開發環境、測試環境與線上環境設定成一樣,更早發現測試問題,而不至於在生產環境才暴露出問題。

11 日誌

在應用程式中打日誌是一個好習慣。日誌使得應用程式執行的動作變得透明。日誌是在系統出現故障時,排查問題的有力幫手。

日誌應該是事件流的彙總,將所有執行中程序和後端服務的輸出流按照時間順序收集起來。儘管在回溯問題時可能需要看很多行,日誌最原始的格式確實是一個事件一行。日誌沒有確定開始和結束,但隨著應用在執行會持續的增加。對於傳統的 Java EE 應用程式而言,有許多框架和庫可用於日誌記錄。Java Logging (JUL) 是 Java 自身所提供的現成選項。除此之外 Log4j、Logback 和 SLF4J 是其他一些流行的日誌框架。

對於傳統的單塊架構而言,日誌管理本身並不存在難點,畢竟所有的日誌檔案,都儲存在應用所部屬的主機上,獲取日誌檔案或者搜尋日誌內容都比較簡單。但在 Cloud Native 應用中, 情況則有非常大的不同。分散式系統,特別是微服務架構所帶來的部署應用方式的重大轉變,都使得微服務的日誌管理面臨很多新的挑戰。一方面隨著微服務例項的數量的增長,伴隨而來的就是日誌檔案的遞增。另一方面,日誌被散落在各自的例項所部署的主機上,不方面整合和回溯。

在這種情況下,將日誌進行集中化的管理變得意義重大。本書的“10.4 日誌管理”章節內容,會對 Cloud Native 的日誌集中化管理進行詳細的探討。

12 管理程序

開發人員經常希望執行一些管理或維護應用的一次性任務,例如:

  • 執行資料移植(Django 中的 manage.py migrate, Rails 中的 rake db:migrate)。
  • 執行一個控制檯(也被稱為 REPL shell),來執行一些程式碼或是針對線上資料庫做一些檢查。大多數語言都通過直譯器提供了一個 REPL 工具(python 或 perl),或是其他命令(Ruby 使用 irb, Rails 使用 rails console)。
  • 執行一些提交到程式碼倉庫的一次性指令碼。

一次性管理程序應該和正常的常駐程序使用同樣的環境。這些管理程序和任何其他的程序一樣使用相同的程式碼和配置,基於某個釋出版本執行。後臺管理程式碼應該隨其他應用程式程式碼一起釋出,從而避免同步問題。

所有程序型別應該使用同樣的依賴隔離技術。例如,如果 Rub y的 web 程序使用了命令 bundle exec thin start,那麼資料庫移植應使用 bundle exec rake db:migrate。同樣的,如果一個 Python 程式使用了 Virtualenv,則需要在執行 Tornado Web 伺服器和任何 manage.py 管理程序時引入 bin/python。

參考引用