Serverless的微服務持續交付案例

本文是GitChat《Serverless 微服務的持續交付》部分內容已做修改。文章聊天實錄請見:“顧宇:Serverless 微服務的持續交付解析

Serverless 風格微服務的持續交付(上):架構案例”中,我們介紹了一個無服務器風格的微服務的架構案例。這個案例中混合了各種風格的微服務。

架構圖如下:

在這個架構中,我們採用了前後端分離的技術。我們把 HTML,JS, CSS 等靜態內容部署在 S3 上,並通過 CloudFront 作爲 CDN 構成了整個架構的前端部分。我們把 Amazon API Gateway 作爲後端的整體接口連接後端的各種風格的微服務,無論是運行在 Lambda 上的函數,還是運行在 EC2 上的 Java 微服務,他們整體構成了這個應用的後端部分。

從這個架構圖上我們可以明顯的看到 前端(Frontend)和後端(Backend)的區分。

持續部署流水線的設計和實現

任何 DevOps 部署流水線都可以分爲三個階段:待測試,待發布,已發佈

由於我們的架構是前後端分離的,因此我們爲前端和後端分別構造了兩條流水線,使得前後端開發可以獨立。如下圖所示:

(整體流水線)

在這種情況下,前端團隊和後端團隊是兩個不同的團隊,可以獨立開發和部署,但在發佈的時候則有些不同。由於用戶是最後感知功能變化的。因此,爲了避免界面報錯找不到接口,在新增功能的場景下,後端先發布,前端後發佈。在刪除功能的場景下,前端先發布,後端後發佈。

我們採用 Jenkins 構建我們的流水線,Jenkins 中已經含有足夠的 AWS 插件可以幫助我們完成整個端到端的持續交付流水線。

前端流水線

前端持續交付流水線如下所示:

前端流水線的各步驟過程如下:

  1. 我們採用 BDD/ATDD 的方式進行前端開發。用 NightWatch.JS 框架做 端到端的測試,mochachai 用於做某些邏輯的驗證。
  2. 我們採用單代碼庫主幹(develop 分支)進行開發,用 master 分支作爲生產環境的部署。生產環境的發佈則是通過 Pull Request 合併的。在合併前,我們會合並提交。
  3. 前端採用 Webpack 進行構建,形成前端的交付產物。在構建之前,先進行一次全局測試。
  4. 由於 S3 不光可以作爲對象存儲服務,也可以作爲一個高可用、高性能而且成本低廉的靜態 Web 服務器。所以我們的前端靜態內容存儲在 S3 上。每一次部署都會在 S3 上以 build 號形成一個新的目錄,然後把 Webpack 構建出來的文件存儲進去。
  5. 我們採用 Cloudfront 作爲 CDN,這樣可以和 S3 相互集成。只需要把 S3 作爲 CDN 的源,在發佈時修改對應發佈的目錄就可以了。

由於我們做到了前後端分離。因此前端的數據和業務請求會通過 Ajax 的方式請求後端的 Rest API,而這個 Rest API 是由 Amazon API Gateway 通過 Swagger 配置生成的。前端只需要知道 這個 API Gateway,而無需知道API Gateway 的對應實現。

後端流水線

後端持續交付流水線如下所示:

後端流水線的各步驟過程如下:

  1. 我們採用“消費者驅動的契約測試”進行開發,先根據前端的 API 調用構建出相應的 Swagger API 規範文件和示例數據。然後,把這個規範上傳至 AWS API Gateway,AWS API Gateway 會根據這個文件生成對應的 REST API。前端的小夥伴就可以依據這個進行開發了。
  2. 之後我們再根據數據的規範和要求編寫後端的 Lambda 函數。我們採用 NodeJS 作爲 Lambda 函數的開發語言。並採用 Jest 作爲 Lambda 的 TDD 測試框架。
  3. 和前端一樣,對於後端我們也採用單代碼庫主幹(develop 分支)進行開發,用 master 分支作爲生產環境的部署。
  4. 由於 AWS Lambda 函數需要打包到 S3 上才能進行部署,所以我們先把對應的構建產物存儲在 S3 上,然後再部署 Lambda 函數。
  5. 我們採用版本化 Lambda 部署,部署後 Lambda 函數不會覆蓋已有的函數,而是生成新版本的函數。然後通過別名(Alias)區分不同前端所對應的函數版本。默認的 LATESTProdPreProd,uatfunc4func41func LATEST 別名指向 4 版本。別名 PreProd 和 UAT 指向 3 版本,別名 Prod 在 2 版本。
  6. 技術而 API 的部署則是修改 API Gateway 的配置,使其綁定到對應版本的函數上去。由於 API Gateway 支持多階段(Stage)的配置,我們可以採用和別名匹配的階段綁定不同的函數。
  7. 完成了 API Gateway 和 Lamdba 的綁定之後,還需要進行一輪端到端的測試以保證 API 輸入輸出正確。
  8. 測試完畢後,再修改 API Gateway 的生產環境配置就可以了。

部署的效果如下所示:

(API Gateway + Lambda 配置)

無服務器微服務的持續交付新挑戰

在實現以上的持續交付流水線的時候,我們踩了很多坑。但經過我們的反思,我們發現是雲計算顛覆了我們很多的認識,當雲計算把某些成本降低到趨近於 0 時。我們發現了以下幾個新的挑戰:

  1. 如果你要 Stub,有可能你走錯了路。
  2. 測試金字塔的倒置。
  3. 你不再需要多個運行環境,你需要一個多階段的生產環境 (Multi-Stage Production)。
  4. 函數的管理和 Nanoservice 反模式。

Stub ?別逗了

很多開發者最初都想在本地建立一套開發環境。由於 AWS 多半是通過 API 或者 CloudFormation 操作,因此開發者在本地開發的時候對於AWS 的外部依賴進行打樁(Stub) 進行測試,例如集成 DynamoDB(一種 NoSQL 數據庫),當然你也可以運行本地版的 DynamoDB,但組織自動化測試的額外代價極高。然而隨着微服務和函數規模的增加,這種管理打樁和構造打樁的虛擬雲資源的代價會越來越大,但收效卻沒有提升。另一方面,往往需要修改幾行代碼立即生效的事情,卻要執行很長時間的測試和部署流程,這個性價比並不是很高。

這時我們意識到一件事:如果某一個環節代價過大而價值不大,你就需要思考一下這個環節存在的必要性。

由於 AWS 提供了很好的配置隔離機制,於是爲了得到更快速的反饋,我們放棄了 Stub 或構建本地 DynamoDB,而是直接部署在 AWS 上進行集成測試。只在本地執行單元測試,由於單元測試是 NodeJS 的函數,所以非常好測試。

另外一方面,我們發現了一個有趣的事實,那就是:

測試金字塔的倒置

由於我們採用 ATDD 進行開發,然後不斷向下進行分解。在統計最後的測試代碼和測試工作量的的時候,我們有了很有趣的發現:

  • End-2-End (UI)的測試代碼佔30%左右,佔用了開發人員 30% 的時間(以小時作爲單位)開發和測試。
  • 集成測試(函數、服務和 API Gateway 的集成)代碼佔 45%左右,佔用了開發人員60% 的時間(以小時作爲單位)開發和測試。
  • 單元測試的測試代碼佔 25%左右,佔用了10%左右的時間開發和測試。

一開始我們以爲我們走入了“蛋筒冰激凌反模式”或者“紙杯蛋糕反模式”但實際上:

  1. 我們並沒有太多的手動測試,絕大部分自動化。除了驗證手機端的部署以外,幾乎沒有手工測試工作量。
  2. 我們的自動化測試都是必要的,且沒有重複。
  3. 我們的單元測試足夠,且不需要增加單元測試。

但爲什麼會造成這樣的結果呢,經過我們分析。是由於 AWS 供了很多功能組件,而這些組件你無需在單元測試中驗證(減少了很多 Stub 或者 Mock),只有通過集成測試的方式才能進行驗證。因此,Serverless 基礎設施大大降低了單元測試的投入,但把這些不同的組件組合起來則勞時費力 。如果你有多套不一致的環境,那你的持續交付流水線配置則是很困難的。因此我們意識到:

**你不再需要多個運行環境,你只需要一個多階段的生產環境 (Multi-Stage Production)
**

通常情況下,我們會有多個運行環境,分別面對不同的人羣:

  1. 面向開發者的本地開發環境
  2. 面向測試者的集成環境或測試環境(Test,QA 或 SIT)
  3. 面向業務部門的測試環境(UAT 環境)
  4. 面向最終用戶的生產環境(Production 環境)

然而多個環境帶來的最大問題是環境基礎配置的不一致性。加之應用部署的不一致性。帶來了很多不可重現問題。在 DevOps 運動,特別是基礎設施即代碼實踐的推廣下,這一問題得到了暫時的緩解。然而無服務器架構則把基礎設施即代碼推向了極致:只要能做到配置隔離和部署權限隔離,資源也可以做到同樣的隔離效果。

我們通過 DNS 配置指向了同一個的 API Gateway,這個 API Gateway 有着不同的 Stage:我們只有開發(Dev)和生產(Prod)兩套配置,只需修改配置以及對應 API 所指向的函數版本就可完成部署和發佈。

然而,多個函數的多版本管理增加了操作複雜性和配置性,使得整個持續交付流水線多了很多認爲操作導致持續交付並不高效。於是我們在思考:

對函數的管理和“ Nanoservices 反模式 ”

根據微服務的定義,AWS API Gateway 和 Lambda 的組合確實滿足 微服務的特徵,這看起來很美好。就像下圖一樣:

但當Lambda 函數多了,管理衆多的函數的發佈就成爲了很高的一件事。而且, 可能會變成“Nanoservice 反模式”:

Nanoservice is an antipattern where a service is too fine-grained. A nanoservice is a service whose overhead (communications, maintenance, and so on) outweighs its utility.

如何把握微服務的粒度和函數的數量,就變成了一個新的問題。而 Serverless Framework ,就是解決這樣的問題的。它認爲微服務是由一個多個函數和相關的資源所組成。因此,才滿足了微服務可獨立部署可獨立服務的屬性。它把微服務當做一個用於管理 Lambda 的單元。所有的 Lambda 要按照微服務的要求來組織。Serverless Framework 包含了三個部分:

  1. 一個 CLI 工具,用於創建和部署微服務。
  2. 一個配置文件,用於管理和配置 AWS 微服務所需要的所有資源。
  3. 一套函數模板,用於讓你快速啓動微服務的開發。

此外,這個工具由 AWS 自身推廣,所以兼容性很好。但是,我們得到了 Serverless 的衆多好處,卻難以擺脫對 AWS 的依賴。因爲 AWS 的這一套架構是和別的雲平臺不兼容的。

所以,這就又是一個“自由的代價”的問題。

CloudNative 的持續交付

在實施 Serverless 的微服務期間,發生了一件我認爲十分有意義的事情。我們客戶想增加一個很小的需求。我和兩個客戶方的開發人員,客戶的開發經理,以及客戶業務部門的兩個人要實現一個需求。當時我們 6 個人在會議室裏面討論了兩個小時。討論兩個小時之後我們不光和業務部門定下來了需求(這點多麼不容易),與此同時我們的前後端代碼已經寫完了,而且發佈到了生產環境並通過了業務部門的測試。由於客戶內部流程的關係,我們僅需要一個生產環境發佈的批准,就可以完成新需求的對外發布!

在這個過程中,由於我們沒有太多的環境要準備,並且和業務部門共同制定了驗收標準並完成了自動化測試的編寫。這全得益於 Serverless 相關技術帶來的便利性。

我相信在未來的環境,如果這個架構,如果在線 IDE 技術成熟的話(由於 Lambda 控制了代碼的規模,因此在線 IDE 足夠),那我們可以大量縮短我們需求確定之後到我功能上線的整體時間。

通過以上的經歷,我發現了 CloudNative 持續交付的幾個重點:

  1. 優先採用 SaaS 化的服務而不是自己搭建持續交付流水線。
  2. 開發是離不開基礎設施配置工作的。
  3. 狀態和過程分離,把狀態通過版本化的方式保存到配置管理工具中。

而在這種環境下,Ops工作就只剩下三件事:

  1. 設計整體的架構,除了基礎設施的架構以外,還要關注應用架構。以及優先採用的 SaaS 服務解決問題。
  2. 嚴格管理配置和權限並構建一個快速交付的持續交付流程。
  3. 監控生產環境。

剩下的事情,就全部交給雲平臺去做。


文/ThoughtWorks顧宇

更多精彩洞見,請關注微信公衆號:思特沃克

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