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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章