Serverless的微服務架構案例

本文首發於GitChat《Serverless 風格微服務的持續交付(上):架構案例》,部分內容已做修改。文章聊天實錄請見:“顧宇:構建Serverless 風格微服務實戰解析(上)”

一次微服務架構的奇遇

2016年12月初,當時我正在以一名 DevOps 諮詢師的身份參與某客戶的 DevOps 轉型項目。這個項目是提升該部門在 AWS (Amazon Web Services)雲計算平臺上的 DevOps 能力。

自助服務的應用系統基於 Ruby on Rails 框架開發,前端部分採用 AngularJS 1.0,但是沒有采用前後端分離的設計,頁面代碼仍然是通過 ERB 組合而成。移動端則採用 Cordova 開發。為了降低開發難度和工作量, 移動端的應用內容實際上是把 AngularJS 所生成的 Web 頁面通過響應式樣式的方式嵌入到移動端。但因為經常超時,所以這款 APP 體驗並不好。

整套 Rails 應用部署在 AWS 上,並且通過網關和內部業務系統隔離。BOSS 系統採用 SOAP 對外暴露服務,並由另外一個部門負責。因此,雲上的應用所做的業務是給用戶展現一個使用友好的界面,並通過數據的轉化和內部 BOSS 系統進行交互。系統架構如下圖所示:

Serverless的微服務架構案例

應用的交互流程如下

  1. 瀏覽器或者移動端通過域名(由 AWS Route 53託管)轉向 CDN(採用 AWS Cloudfront)。

  2. CDN 根據請求的內容類別進行區分,靜態文件(圖片,JS,CSS 樣式等),會轉向 AWS S3 存儲。動態請求會直接發給負載均衡器 (AWS Elastic Load Balancer)。

  3. 負載均衡器會根據各 EC2 計算實例的負載狀態將請求轉發到不同的實例上的 Ruby On Rails 應用上。每一個應用都是一個典型的 MVC Web 應用。

  4. EC2 上的應用會將一部分數據存儲在關係型數據服務(AWS RDS,Relational Database Service)上,一部分存儲在本地文件裡。經過應用的處理,轉換成 SOAP 請求通過 網關發送給 BOSS 系統處理。BOSS 系統處理完成後會返回對應的消息。

根據業務的需要,一部分數據會採用 AWS ElastiCache 的 Redis 服務作為緩存以優化業務響應速度。

團隊痛點

這個應用經歷了多年的開發,前後已經更換過很多技術人員。但是沒有人對這個應用代碼庫有完整的的認識。因此,我們對整個團隊和產品進行了一次痛點總結:

組織結構方面

運維團隊成為瓶頸,60 個人左右的開發團隊只有 4 名 Ops 支持。運維團隊除了日常的事務以外,還要給開發團隊提供各種支持。很多資源的使用權限被限制在這個團隊裡,就導致各種問題的解決進度進一步拖延。

Serverless的微服務架構案例

隨著業務的增長,需要基礎設施代碼庫提供各種各樣的能力。然而 Ops 團隊的任何更改都會導致所有的開發團隊停下手頭的進度去修復更新所帶來的各種問題。

應用架構方面

應用架構並沒有達到前後端分離的效果,仍然需要同一個工程師編寫前後端代碼。這樣的技術棧對於對於開發人員的要求很高,然而市場上缺乏合適的 RoR 工程師,導致維護成本進一步上升。經過了三個月,仍然很難招聘到合適的工程師。

多個團隊在一個代碼庫上工作,新舊功能之間存在各種依賴點。加上 Ruby 的語言特性,使得代碼中存在很多隱含的依賴點和類/方法覆蓋,導致了開發進度緩慢。我們一共有 4 個團隊在一個代碼庫上工作,3個團隊在開發新的功能。1 個團隊需要修復 Bug 和清理技術債,這一切都要同時進行。

技術債方面

代碼庫中有大量的重複 Cucumber 自動化測試,但是缺乏正確的並行測試策略,導致自動化測試會隨機失敗,持續集成服務器 (Jenkins)的 slave 節點本地難以創建,導致失敗原因更加難以查找。如果走運的話,從提交代碼到新的版本發佈至少需要 45 分鐘。如果不走運的話,兩三天都無法完成一次成功的構建,真是依靠人品構建。

基礎設施即代碼(Infrastructure as Code)建立在一個混合的遺留的 Ruby 代碼庫上。這個代碼庫用來封裝一些類似於 Packer 和 AWS CLI 這樣的命令行工具,包含一些 CloudFormation 的轉化能力。由於缺乏長期的規劃和編碼規範,加之人員變動十分頻繁,使得代碼庫難以維護。

此外,基礎設施代碼庫作為一個 gem 和應用程序代碼庫耦合在一起,運維團隊有唯一的維護權限。因此很多基礎設施上的問題開發團隊無法解決,也不願解決。

我參與過很多 Ruby 技術棧遺留系統的維護。在經歷了這些 Ruby 項目之後,我發現 Ruby 是一個開發起來很爽但是維護起來很痛苦的技術棧。大部分的維護更改是由於 Ruby 的版本 和 Gem 的版本更新導致的。此外,由於 Ruby 比較靈活,人們都有自己的想法和使用習慣,因此代碼庫很難維護。

Serverless的微服務架構案例

雖然團隊已經有比較好的持續交付流程,但是 Ops 能力缺乏和應用架構帶來的侷限阻礙了整個產品的前進。因此,當務之急是能夠通過 DevOps 提升團隊的 Ops 能力,緩解 Ops 資源不足,削弱 DevOps 矛盾。

DevOps 組織轉型中一般有兩種方法:一種方法是提升 Dev 的 Ops 能力,另一種方法是降低 Ops 工作門檻。在時間資源很緊張的情況下,通過技術的改進,降低 Ops 的門檻是短期內收益最大的方法。

微服務觸發點:併購帶來的業務功能合併

在我加入項目之後,客戶收購了另外一家業務相關的企業。因此原有的系統要同時承載兩個業務。恰巧有個訂單查詢的業務需要讓當前的團隊完成這樣一個需求:通過現有的訂單查詢功能同時查詢兩個系統的業務訂單。

這個需求看起來很簡單,只需要在現有系統中增加一個數據源,然後把輸入的訂單號進行轉化就可以。但由於存在上述的痛點,完成這樣一個簡單的功能的代價是十分高昂的。幾乎 70% 的工作量都和功能開發本身沒有關係。

在開發的項目上進行 DevOps 轉型就像在行進的汽車上換車輪,一不留心就會讓所有團隊停止工作。因此我建議通過設立並行的新團隊來同時完成新功能的開發和 DevOps 轉型的試點。

這是一個功能拆分和新功能拆分需求,剛好訂單查詢是原系統中一個比較獨立和成熟的功能。為了避免影響原有各功能開發的進度。我們決定採用微服務架構來完成這個功能。

構建微服務的架構的策略

我們並不想重蹈之前應用架構的覆轍,我們要做到前後端分離。使得比較小的開發團隊可以並行開發,只要協商好了接口之間的契約(Contract),未來開發完成之後會很好集成。

這讓我想起了 Chris Richardson 提出了三種微服務架構策略,分別是:停止挖坑,前後端分離和提取微服務。

停止挖坑的意思是說:如果發現自己掉坑裡,馬上停止。

原先的單體應用對我們來說就是一個焦油坑,因此我們要停止在原來的代碼庫上繼續工作。並且為新應用單獨創建一個代碼庫。所以,我們拆分策略模式如下所示:

Serverless的微服務架構案例

在我們的架構裡,實現新的需求就要變動老的應用。我們的想法是:

  1. 構建出新的業務頁面,生成微服務契約。

  2. 根據 API 契約構建出新的微服務。

  3. 部署 Web 前端到 S3 上,採用 S3 的 Static Web Hosting (靜態 Web 服務) 發佈。

  4. 部署後端微服務上線,並採用臨時的域名和 CDN 加載點進行測試。

  5. 通過更新 CDN 把原應用的流量導向新的微服務。

  6. 刪除舊的服務代碼。

我們原本要在原有的應用上增加一個 API 用來訪問以前應用的邏輯。但想想這實際上也是一種挖坑。在評估了業務的複雜性之後。我們發現這個功能如果全新開發只需要 2人2周(一個人月)的時間,這僅僅佔我們預估工作量的20%不到。因此我們放棄了對遺留代碼動工的念頭。最終通過微服務直接訪問後臺系統,而不需要通過原有的應用。

在我們拆微服務的部分十分簡單。對於後端來說說只需要修改 CDN 覆蓋原先的訪問源(Origin)以及保存在 route.rb 裡的原功能訪問點,就可以完成微服務的集成。

構建出新的業務頁面,生成微服務契約

結合上面的應用痛點和思路,在構建微服務的技術選型時我們確定了以下方向:

  1. 前端框架要具備很好的 Responsive 擴展。

  2. 採用 Swagger 來描述 API 需要具備的行為。

  3. 過消費者驅動進行契約測試驅動微服務後端開發。

  4. 前端代碼庫和後端代碼庫分開。

  5. 前端代碼框架要對持續交付友好。

因此我們選擇了 React 作為前端技術棧並且用 yarn 管理依賴和任務。另外一個原因是我們能夠通過 React-native 為未來構建新的應用做好準備。此外,我們引入了 AWS SDK 的 nodejs 版本。用編寫一些常見的諸如構建、部署、配置等 AWS 相關的操作。並且通過 swagger 描述後端 API 的行為。這樣,後端只需要滿足這個 API 規範,就很容易做前後端集成。

Serverless的微服務架構案例

部署前端部分到 S3 上

由於 AWS S3 服務自帶 Static Web Hosting (靜態頁面服務) 功能,這就大大減少了我們構建基礎環境所花費的時間。如果你還想著用 Nginx 和 Apache 作為靜態內容的 Web 服務器,那麼你還不夠 CloudNative。

雖然AWS S3 服務曾經發生過故障,但 SLA 也比我們自己構建的 EC2 實例處理靜態內容要強得多。此外還有以下優點:

  1. 擁有獨立的 URL,很容易做很多 301 和 302 的重定向和改寫操作。

  2. 和 CDN(CloudFront)集成很好。

  3. 很容易和持續集成工具集成。

  4. 最大的優點:比 EC2 便宜。

根據 API 契約構建出新的微服務

在構建微服務的最初,我們當時有兩個選擇:

採用 Sinatra (一個用來構建 API 的 Ruby gem) 構建一個微服務 ,這樣可以複用原先 Rails 代碼庫的很多組件。換句話說,只需要 copy 一些代碼,放到一個單獨的代碼庫裡,就可以完成功能。但也同樣會面臨之前 Ruby 技術棧帶來的種種問題。

採用 Spring Boot 構建一個微服務,Java 作為成熟工程語言目前還是最好的選擇,社區和實踐都非常成熟。可以複用後臺很多用來做 SOAP 處理的 JAR 包。另一方面是解決了 Ruby 技術棧帶來的問題。

然而,這兩個方案的都有一個共同的問題:需要通過 ruby 語言編寫的基礎設施工具構建一套運行微服務的基礎設施。而這個基礎設施的搭建,前前後後估計得需要至少 1個月,這還是在運維團隊有人幫助的情況下的樂觀估計。

所以,要找到一種降低環境構建和運維團隊阻塞的方式避開傳統的 EC2 搭建應用的方式。

這,只有 Lambda 可以做到!

基於上面的種種考量,我們選擇了 Amazon API Gateway + Lambda 的組合。而 Amazon API Gateway + Lambda 還有額外好處:

  1. 支持用 Swagger 規範配置 API Gateway。也就是說,你只要導入前端的 Swagger 規範,就可以生成 API Gateway。

  2. 可以用數據構建 Mock API,這樣就可以很大程度上實現消費者驅動契約開發。

  3. 通過 Amazon API Gateway 的 Stage 功能,我們無需構建 QA 環境,UAT 環境和 Staging 環境。只需要指定不同的 Stage,就可以完成對應的切換。

  4. Lambda 的發佈生效時間很短,反饋很快。原先用 CloudFormation 構建的 API 基礎設施需要至少 15 分鐘,而 Lambda 的生效只需要短短几秒鐘。

  5. Lambda 的編寫很方便,可以採用在線的方式。雖然在線 IDE 並不很好用,但是真的也寫不了幾行代碼。

  6. Lambda 自動根據請求自擴展,無需考慮負載均衡。

雖然有這麼多優點,但不能忽略了關鍵性的問題:AWS Lambda 不一定適合你的應用場景!

很多對同步和強一致性的業務需求是無法滿足的。所以,AWS Lambda 更適合能夠異步處理的業務場景。此外,AWS Lambda 對消耗存儲空間和 CPU 很多的場景支持不是很好,例如 AI 和 大數據。(PS: AWS 已經有專門的 AI 和大數據服務了,所以不需要和自己過不去)

對於我們的應用場景而言,上文中的 Ruby On Rails 應用中的主要功能(至少60% 以上)實際上只是一個數據轉換適配器:把前端輸入的數據進行加工,轉換成對應的 SOAP 調用。因此,對於這樣一個簡單的場景而言,Amazon API Gateway + Lambda 完全滿足需求!

Serverless的微服務架構案例

部署後端微服務

選擇了Amazon API Gateway + Lambda 後,後端的微服務部署看起來很簡單:

  • 更新 Lambda 函數。

  • 更新 API 規範,並要求 API 綁定對應 Lambda 函數處理請求。

但是,這卻不是很容易的一件事。我們將在下一篇文章《Serverless 風格微服務的持續交付》中對這方面踩過的坑詳細介紹。

把原應用的請求導向新的微服務

這時候在 CDN 上給新的微服務配置 API Gateway 作為一個新的源(Origin),覆蓋原先寫在 route.rb 和 nginx.conf 裡的 API 訪問規則就可以了。CDN 會攔截訪問請求,使得請求在 nginx 處理之前就會把對應的請求轉發到 API Gateway。

當然,如果你想做灰度發佈的話,就不能按上面這種方式搞了。CloudFront 和 ELB 負載均衡 並不具備帶權轉發功能。因此你需要通過 nginx 配置,按訪問權重把 API Gateway 作為一個 upstream 裡的一個 Server 就可以。

刪除舊的服務代碼

不要留著無用的遺留代碼!

不要留著無用的遺留代碼!

不要留著無用的遺留代碼!

重要且最容易被忽略的事情要說三遍。斬草要除根,雖然我們可以保持代碼不動。但是清理不再使用的遺留代碼和自動化測試可以為其它團隊減少很多不必要的工作量。

最終的架構

經過6個人兩個月的開發(原計劃8個人3個月),我們的 Serverless 微服務最終落地了。當然這中間有 60% 的時間是在探索全新的技術棧。如果熟練的話,估計 4 個人一個月就可以完成工作。

最後的架構如下圖所示:

Serverless的微服務架構案例

在上圖中,請求仍然是先通過域名到 CDN (CloudFront),然後:

  1. CDN 根據請求點的不同,把頁面請求轉發至 S3 ,把 API 請求轉發到 API Gateway。

  2. 前端的內容通過藍綠部署被放到了不同的 S3 Bucket 裡面,只需要改變 CDN 設置就可以完成對應內容的部署。雖然對於部署來說藍綠 Bucket 乍看有一點多餘,但這是為了能夠在生產環境下做集成在線測試準備的。這樣可以使環境不一致儘可能少。

  3. API Gateway 有自己作用的 VPC,很好的實現了網絡級別的隔離。

  4. 通過 API Gateway 轉發的 API 請求分成了三類,每一類都可以根據請求狀況自擴展。

  5. 身份驗證類:第一次訪問會請求 ElastCache(Redis),如果 Token 失效或者不存在,則重新走一遍用戶驗證流程。

  6. 數據請求類:數據請求類會通過 Lambda 訪問由其他團隊開發的 Java 微服務,這類微服務是後臺系統唯一的訪問點。

  7. 操作審計類:請求會記錄到 DynamoDB (一種時間序列數據庫)中,用來跟蹤異步請求的各種日誌。

  8. API Gateway 自己有一些緩存,可以加速 API 的訪問。

  9. 消息返回後,再有三類不同的請求的結果統一通過 API Gateway 返回給客戶端。

Serverless 風格微服務架構的優點

由於沒有 EC2 設施初始化的時間,我們減少了至少一個月的工作量,分別是:

  1. 初始化網絡配置的時間。

  2. 構建 EC2 配置的時間。

  3. 構建反向代理和前端靜態內容服務器的時間。

  4. 構建後端 API 應用基礎設施的時間。

  5. 構建負載均衡的時間。

  6. 把上述內容用 Ruby 進行基礎設施即代碼化的時間。

如果要把 API Gateway 算作是基礎設施初始化的時間來看。第一次初始化 API Gateway 用了一天,以後 API Gateway 結合持續交付流程每次修改僅僅需要幾分鐘,Serverless 風格的微服務大大降低了基礎設施配置和運維門檻。

此外,對於團隊來說,Amazon API Gateway + Lambda 的微服務還帶來其它好處:

  1. 開發效率高,原先至少 45 分鐘的開發反饋週期縮短為 5 分鐘以內。

  2. 無關的代碼量少,需要維護的代碼量少。除了專注業務本身。上游和 API Gateway 的集成以及下游和後端服務的集成代碼量很少。

  3. 應用維護成本低。代碼僅僅幾十行,且都為函數式,很容易測試。避免了代碼庫內部複雜性的增加。

此外,我們做了 Java 和 NodeJs 比較。在開發同樣的功能下,NodeJS 的開發效率更高,原因是 Java 要把請求的 json 轉化為對象,也要把返回的 json 轉化為對象,而不像 nodejs 直接處理 json。此外, Java 需要引入一些其它 JAR 包作為依賴。在 AWS 場景下開發同樣一個函數式微服務,nodejs 有 4 倍於 java 的開發效率提升。

最後

Serverless 風格的微服務雖然大大減少了開發工作量以及基礎設施的開發維護工作量。但也帶來了新的挑戰:

  1. 大量函數的管理。

  2. SIT,UAT 環境的管理。

  3. 持續交付流水線的配置。

  4. 面對基礎設施集成帶來的測試。

這讓我們重新思考了 Serverless 架構的微服務如何更好的進行持續交付。


文/ThoughtWorks顧宇

原文鏈接:https://insights.thoughtworks.cn/serverless-microservices-architecture-case/


分享到:


相關文章: