構(gòu)建Serverless風格的微服務(wù)架構(gòu)案例
一、一次微服務(wù)架構(gòu)的奇遇
2016年12月初,當時我正在以一名 DevOps 咨詢師的身份參與某客戶的 DevOps 轉(zhuǎn)型項目。這個項目是提升該部門在 AWS (Amazon Web Services)云計算平臺上的 DevOps 能力。
自助服務(wù)的應(yīng)用系統(tǒng)基于 Ruby on Rails 框架開發(fā),前端部分采用 AngularJS 1.0,但是沒有采用前后端分離的設(shè)計,頁面代碼仍然是通過 ERB 組合而成。移動端則采用 Cordova 開發(fā)。為了降低開發(fā)難度和工作量, 移動端的應(yīng)用內(nèi)容實際上是把 AngularJS 所生成的 Web 頁面通過響應(yīng)式樣式的方式嵌入到移動端。但因為經(jīng)常超時,所以這款 APP 體驗并不好。
整套 Rails 應(yīng)用部署在 AWS 上,并且通過網(wǎng)關(guān)和內(nèi)部業(yè)務(wù)系統(tǒng)隔離。BOSS 系統(tǒng)采用 SOAP 對外暴露服務(wù),并由另外一個部門負責。因此,云上的應(yīng)用所做的業(yè)務(wù)是給用戶展現(xiàn)一個使用友好的界面,并通過數(shù)據(jù)的轉(zhuǎn)化和內(nèi)部 BOSS 系統(tǒng)進行交互。系統(tǒng)架構(gòu)如下圖所示:
應(yīng)用的交互流程如下
- 瀏覽器或者移動端通過域名(由 AWS Route 53托管)轉(zhuǎn)向 CDN(采用 AWS Cloudfront)。
- CDN 根據(jù)請求的內(nèi)容類別進行區(qū)分,靜態(tài)文件(圖片,JS,CSS 樣式等),會轉(zhuǎn)向 AWS S3 存儲。動態(tài)請求會直接發(fā)給負載均衡器 (AWS Elastic Load Balancer)。
- 負載均衡器會根據(jù)各 EC2 計算實例的負載狀態(tài)將請求轉(zhuǎn)發(fā)到不同的實例上的 Ruby On Rails 應(yīng)用上。每一個應(yīng)用都是一個典型的 MVC Web 應(yīng)用。
- EC2 上的應(yīng)用會將一部分數(shù)據(jù)存儲在關(guān)系型數(shù)據(jù)服務(wù)(AWS RDS,Relational Database Service)上,一部分存儲在本地文件里。經(jīng)過應(yīng)用的處理,轉(zhuǎn)換成 SOAP 請求通過 網(wǎng)關(guān)發(fā)送給 BOSS 系統(tǒng)處理。BOSS 系統(tǒng)處理完成后會返回對應(yīng)的消息。
根據(jù)業(yè)務(wù)的需要,一部分數(shù)據(jù)會采用 AWS ElastiCache 的 Redis 服務(wù)作為緩存以優(yōu)化業(yè)務(wù)響應(yīng)速度。
二、團隊痛點
這個應(yīng)用經(jīng)歷了多年的開發(fā),前后已經(jīng)更換過很多技術(shù)人員。但是沒有人對這個應(yīng)用代碼庫有完整的的認識。因此,我們對整個團隊和產(chǎn)品進行了一次痛點總結(jié):
1. 組織結(jié)構(gòu)方面
運維團隊成為瓶頸,60 個人左右的開發(fā)團隊只有 4 名 Ops 支持。運維團隊除了日常的事務(wù)以外,還要給開發(fā)團隊提供各種支持。很多資源的使用權(quán)限被限制在這個團隊里,就導致各種問題的解決進度進一步拖延。
隨著業(yè)務(wù)的增長,需要基礎(chǔ)設(shè)施代碼庫提供各種各樣的能力。然而 Ops 團隊的任何更改都會導致所有的開發(fā)團隊停下手頭的進度去修復(fù)更新所帶來的各種問題。
2. 應(yīng)用架構(gòu)方面
應(yīng)用架構(gòu)并沒有達到前后端分離的效果,仍然需要同一個工程師編寫前后端代碼。這樣的技術(shù)棧對于對于開發(fā)人員的要求很高,然而市場上缺乏合適的 RoR 工程師,導致維護成本進一步上升。經(jīng)過了三個月,仍然很難招聘到合適的工程師。
多個團隊在一個代碼庫上工作,新舊功能之間存在各種依賴點。加上 Ruby 的語言特性,使得代碼中存在很多隱含的依賴點和類/方法覆蓋,導致了開發(fā)進度緩慢。我們一共有 4 個團隊在一個代碼庫上工作,3個團隊在開發(fā)新的功能。1 個團隊需要修復(fù) Bug 和清理技術(shù)債,這一切都要同時進行。
3. 技術(shù)債方面
代碼庫中有大量的重復(fù) Cucumber 自動化測試,但是缺乏正確的并行測試策略,導致自動化測試會隨機失敗,持續(xù)集成服務(wù)器 (Jenkins)的 slave 節(jié)點本地難以創(chuàng)建,導致失敗原因更加難以查找。如果走運的話,從提交代碼到新的版本發(fā)布至少需要 45 分鐘。如果不走運的話,兩三天都無法完成一次成功的構(gòu)建,真是依靠人品構(gòu)建。
基礎(chǔ)設(shè)施即代碼(Infrastructure as Code)建立在一個混合的遺留的 Ruby 代碼庫上。這個代碼庫用來封裝一些類似于 Packer 和 AWS CLI 這樣的命令行工具,包含一些 CloudFormation 的轉(zhuǎn)化能力。由于缺乏長期的規(guī)劃和編碼規(guī)范,加之人員變動十分頻繁,使得代碼庫難以維護。
此外,基礎(chǔ)設(shè)施代碼庫作為一個 gem 和應(yīng)用程序代碼庫耦合在一起,運維團隊有唯一的維護權(quán)限。因此很多基礎(chǔ)設(shè)施上的問題開發(fā)團隊無法解決,也不愿解決。
我參與過很多 Ruby 技術(shù)棧遺留系統(tǒng)的維護。在經(jīng)歷了這些 Ruby 項目之后,我發(fā)現(xiàn) Ruby 是一個開發(fā)起來很爽但是維護起來很痛苦的技術(shù)棧。大部分的維護更改是由于 Ruby 的版本 和 Gem 的版本更新導致的。此外,由于 Ruby 比較靈活,人們都有自己的想法和使用習慣,因此代碼庫很難維護。
雖然團隊已經(jīng)有比較好的持續(xù)交付流程,但是 Ops 能力缺乏和應(yīng)用架構(gòu)帶來的局限阻礙了整個產(chǎn)品的前進。因此,當務(wù)之急是能夠通過 DevOps 提升團隊的 Ops 能力,緩解 Ops 資源不足,削弱 DevOps 矛盾。
DevOps 組織轉(zhuǎn)型中一般有兩種方法:一種方法是提升 Dev 的 Ops 能力,另一種方法是降低 Ops 工作門檻。在時間資源很緊張的情況下,通過技術(shù)的改進,降低 Ops 的門檻是短期內(nèi)收益***的方法。
三、微服務(wù)觸發(fā)點:并購帶來的業(yè)務(wù)功能合并
在我加入項目之后,客戶收購了另外一家業(yè)務(wù)相關(guān)的企業(yè)。因此原有的系統(tǒng)要同時承載兩個業(yè)務(wù)。恰巧有個訂單查詢的業(yè)務(wù)需要讓當前的團隊完成這樣一個需求:通過現(xiàn)有的訂單查詢功能同時查詢兩個系統(tǒng)的業(yè)務(wù)訂單。
這個需求看起來很簡單,只需要在現(xiàn)有系統(tǒng)中增加一個數(shù)據(jù)源,然后把輸入的訂單號進行轉(zhuǎn)化就可以。但由于存在上述的痛點,完成這樣一個簡單的功能的代價是十分高昂的。幾乎 70% 的工作量都和功能開發(fā)本身沒有關(guān)系。
在開發(fā)的項目上進行 DevOps 轉(zhuǎn)型就像在行進的汽車上換車輪,一不留心就會讓所有團隊停止工作。因此我建議通過設(shè)立并行的新團隊來同時完成新功能的開發(fā)和 DevOps 轉(zhuǎn)型的試點。
這是一個功能拆分和新功能拆分需求,剛好訂單查詢是原系統(tǒng)中一個比較獨立和成熟的功能。為了避免影響原有各功能開發(fā)的進度。我們決定采用微服務(wù)架構(gòu)來完成這個功能。
四、構(gòu)建微服務(wù)的架構(gòu)的策略
我們并不想重蹈之前應(yīng)用架構(gòu)的覆轍,我們要做到前后端分離。使得比較小的開發(fā)團隊可以并行開發(fā),只要協(xié)商好了接口之間的契約(Contract),未來開發(fā)完成之后會很好集成。
這讓我想起了 Chris Richardson 提出了三種微服務(wù)架構(gòu)策略,分別是:停止挖坑,前后端分離和提取微服務(wù)。
停止挖坑的意思是說:如果發(fā)現(xiàn)自己掉坑里,馬上停止。
原先的單體應(yīng)用對我們來說就是一個焦油坑,因此我們要停止在原來的代碼庫上繼續(xù)工作。并且為新應(yīng)用單***建一個代碼庫。所以,我們拆分策略模式如下所示:
在我們的架構(gòu)里,實現(xiàn)新的需求就要變動老的應(yīng)用。我們的想法是:
- 構(gòu)建出新的業(yè)務(wù)頁面,生成微服務(wù)契約。
- 根據(jù) API 契約構(gòu)建出新的微服務(wù)。
- 部署 Web 前端到 S3 上,采用 S3 的 Static Web Hosting (靜態(tài) Web 服務(wù)) 發(fā)布。
- 部署后端微服務(wù)上線,并采用臨時的域名和 CDN 加載點進行測試。
- 通過更新 CDN 把原應(yīng)用的流量導向新的微服務(wù)。
- 刪除舊的服務(wù)代碼。
我們原本要在原有的應(yīng)用上增加一個 API 用來訪問以前應(yīng)用的邏輯。但想想這實際上也是一種挖坑。在評估了業(yè)務(wù)的復(fù)雜性之后。我們發(fā)現(xiàn)這個功能如果全新開發(fā)只需要 2人2周(一個人月)的時間,這僅僅占我們預(yù)估工作量的20%不到。因此我們放棄了對遺留代碼動工的念頭。最終通過微服務(wù)直接訪問后臺系統(tǒng),而不需要通過原有的應(yīng)用。
在我們拆微服務(wù)的部分十分簡單。對于后端來說說只需要修改 CDN 覆蓋原先的訪問源(Origin)以及保存在 route.rb 里的原功能訪問點,就可以完成微服務(wù)的集成。
1. 構(gòu)建出新的業(yè)務(wù)頁面,生成微服務(wù)契約
結(jié)合上面的應(yīng)用痛點和思路,在構(gòu)建微服務(wù)的技術(shù)選型時我們確定了以下方向:
- 前端框架要具備很好的 Responsive 擴展。
- 采用 Swagger 來描述 API 需要具備的行為。
- 過消費者驅(qū)動進行契約測試驅(qū)動微服務(wù)后端開發(fā)。
- 前端代碼庫和后端代碼庫分開。
- 前端代碼框架要對持續(xù)交付友好。
因此我們選擇了 React 作為前端技術(shù)棧并且用 yarn 管理依賴和任務(wù)。另外一個原因是我們能夠通過 React-native 為未來構(gòu)建新的應(yīng)用做好準備。此外,我們引入了 AWS SDK 的 nodejs 版本。用編寫一些常見的諸如構(gòu)建、部署、配置等 AWS 相關(guān)的操作。并且通過 swagger 描述后端 API 的行為。這樣,后端只需要滿足這個 API 規(guī)范,就很容易做前后端集成。
2. 部署前端部分到S3上
由于 AWS S3 服務(wù)自帶 Static Web Hosting (靜態(tài)頁面服務(wù)) 功能,這就大大減少了我們構(gòu)建基礎(chǔ)環(huán)境所花費的時間。如果你還想著用 Nginx 和 Apache 作為靜態(tài)內(nèi)容的 Web 服務(wù)器,那么你還不夠 CloudNative。
雖然AWS S3 服務(wù)曾經(jīng)發(fā)生過故障,但 SLA 也比我們自己構(gòu)建的 EC2 實例處理靜態(tài)內(nèi)容要強得多。此外還有以下優(yōu)點:
- 擁有獨立的 URL,很容易做很多 301 和 302 的重定向和改寫操作。
- 和 CDN(CloudFront)集成很好。
- 很容易和持續(xù)集成工具集成。
- ***的優(yōu)點:比 EC2 便宜。
3. 根據(jù) API 契約構(gòu)建出新的微服務(wù)
在構(gòu)建微服務(wù)的最初,我們當時有兩個選擇:
采用 Sinatra (一個用來構(gòu)建 API 的 Ruby gem) 構(gòu)建一個微服務(wù) ,這樣可以復(fù)用原先 Rails 代碼庫的很多組件。換句話說,只需要 copy 一些代碼,放到一個單獨的代碼庫里,就可以完成功能。但也同樣會面臨之前 Ruby 技術(shù)棧帶來的種種問題。
采用 Spring Boot 構(gòu)建一個微服務(wù),Java 作為成熟工程語言目前還是***的選擇,社區(qū)和實踐都非常成熟。可以復(fù)用后臺很多用來做 SOAP 處理的 JAR 包。另一方面是解決了 Ruby 技術(shù)棧帶來的問題。
然而,這兩個方案的都有一個共同的問題:需要通過 ruby 語言編寫的基礎(chǔ)設(shè)施工具構(gòu)建一套運行微服務(wù)的基礎(chǔ)設(shè)施。而這個基礎(chǔ)設(shè)施的搭建,前前后后估計得需要至少 1個月,這還是在運維團隊有人幫助的情況下的樂觀估計。
所以,要找到一種降低環(huán)境構(gòu)建和運維團隊阻塞的方式避開傳統(tǒng)的 EC2 搭建應(yīng)用的方式。
這,只有 Lambda 可以做到!
基于上面的種種考量,我們選擇了 Amazon API Gateway + Lambda 的組合。而 Amazon API Gateway + Lambda 還有額外好處:
- 支持用 Swagger 規(guī)范配置 API Gateway。也就是說,你只要導入前端的 Swagger 規(guī)范,就可以生成 API Gateway。
- 可以用數(shù)據(jù)構(gòu)建 Mock API,這樣就可以很大程度上實現(xiàn)消費者驅(qū)動契約開發(fā)。
- 通過 Amazon API Gateway 的 Stage 功能,我們無需構(gòu)建 QA 環(huán)境,UAT 環(huán)境和 Staging 環(huán)境。只需要指定不同的 Stage,就可以完成對應(yīng)的切換。
- Lambda 的發(fā)布生效時間很短,反饋很快。原先用 CloudFormation 構(gòu)建的 API 基礎(chǔ)設(shè)施需要至少 15 分鐘,而 Lambda 的生效只需要短短幾秒鐘。
- Lambda 的編寫很方便,可以采用在線的方式。雖然在線 IDE 并不很好用,但是真的也寫不了幾行代碼。
- Lambda 自動根據(jù)請求自擴展,無需考慮負載均衡。
雖然有這么多優(yōu)點,但不能忽略了關(guān)鍵性的問題:AWS Lambda 不一定適合你的應(yīng)用場景!
很多對同步和強一致性的業(yè)務(wù)需求是無法滿足的。所以,AWS Lambda 更適合能夠異步處理的業(yè)務(wù)場景。此外,AWS Lambda 對消耗存儲空間和 CPU 很多的場景支持不是很好,例如 AI 和 大數(shù)據(jù)。(PS: AWS 已經(jīng)有專門的 AI 和大數(shù)據(jù)服務(wù)了,所以不需要和自己過不去)
對于我們的應(yīng)用場景而言,上文中的 Ruby On Rails 應(yīng)用中的主要功能(至少60% 以上)實際上只是一個數(shù)據(jù)轉(zhuǎn)換適配器:把前端輸入的數(shù)據(jù)進行加工,轉(zhuǎn)換成對應(yīng)的 SOAP 調(diào)用。因此,對于這樣一個簡單的場景而言,Amazon API Gateway + Lambda 完全滿足需求!
4. 部署后端微服務(wù)
選擇了Amazon API Gateway + Lambda 后,后端的微服務(wù)部署看起來很簡單:
- 更新 Lambda 函數(shù)。
- 更新 API 規(guī)范,并要求 API 綁定對應(yīng) Lambda 函數(shù)處理請求。
但是,這卻不是很容易的一件事。我們將在下一篇文章《Serverless 風格微服務(wù)的持續(xù)交付》中對這方面踩過的坑詳細介紹。
5. 把原應(yīng)用的請求導向新的微服務(wù)
這時候在 CDN 上給新的微服務(wù)配置 API Gateway 作為一個新的源(Origin),覆蓋原先寫在 route.rb 和 nginx.conf 里的 API 訪問規(guī)則就可以了。CDN 會攔截訪問請求,使得請求在 nginx 處理之前就會把對應(yīng)的請求轉(zhuǎn)發(fā)到 API Gateway。
當然,如果你想做灰度發(fā)布的話,就不能按上面這種方式搞了。CloudFront 和 ELB 負載均衡 并不具備帶權(quán)轉(zhuǎn)發(fā)功能。因此你需要通過 nginx 配置,按訪問權(quán)重把 API Gateway 作為一個 upstream 里的一個 Server 就可以。
6. 刪除舊的服務(wù)代碼
不要留著無用的遺留代碼!
不要留著無用的遺留代碼!
不要留著無用的遺留代碼!
重要且最容易被忽略的事情要說三遍。斬草要除根,雖然我們可以保持代碼不動。但是清理不再使用的遺留代碼和自動化測試可以為其它團隊減少很多不必要的工作量。
7. 最終的架構(gòu)
經(jīng)過6個人兩個月的開發(fā)(原計劃8個人3個月),我們的 Serverless 微服務(wù)最終落地了。當然這中間有 60% 的時間是在探索全新的技術(shù)棧。如果熟練的話,估計 4 個人一個月就可以完成工作。
***的架構(gòu)如下圖所示:
在上圖中,請求仍然是先通過域名到 CDN (CloudFront),然后:
- CDN 根據(jù)請求點的不同,把頁面請求轉(zhuǎn)發(fā)至 S3 ,把 API 請求轉(zhuǎn)發(fā)到 API Gateway。
- 前端的內(nèi)容通過藍綠部署被放到了不同的 S3 Bucket 里面,只需要改變 CDN 設(shè)置就可以完成對應(yīng)內(nèi)容的部署。雖然對于部署來說藍綠 Bucket 乍看有一點多余,但這是為了能夠在生產(chǎn)環(huán)境下做集成在線測試準備的。這樣可以使環(huán)境不一致盡可能少。
- API Gateway 有自己作用的 VPC,很好的實現(xiàn)了網(wǎng)絡(luò)級別的隔離。
- 通過 API Gateway 轉(zhuǎn)發(fā)的 API 請求分成了三類,每一類都可以根據(jù)請求狀況自擴展。
- 身份驗證類:***次訪問會請求 ElastCache(Redis),如果 Token 失效或者不存在,則重新走一遍用戶驗證流程。
- 數(shù)據(jù)請求類:數(shù)據(jù)請求類會通過 Lambda 訪問由其他團隊開發(fā)的 Java 微服務(wù),這類微服務(wù)是后臺系統(tǒng)唯一的訪問點。
- 操作審計類:請求會記錄到 DynamoDB (一種時間序列數(shù)據(jù)庫)中,用來跟蹤異步請求的各種日志。
- API Gateway 自己有一些緩存,可以加速 API 的訪問。
- 消息返回后,再有三類不同的請求的結(jié)果統(tǒng)一通過 API Gateway 返回給客戶端。
五、Serverless 風格微服務(wù)架構(gòu)的優(yōu)點
由于沒有 EC2 設(shè)施初始化的時間,我們減少了至少一個月的工作量,分別是:
- 初始化網(wǎng)絡(luò)配置的時間。
- 構(gòu)建 EC2 配置的時間。
- 構(gòu)建反向代理和前端靜態(tài)內(nèi)容服務(wù)器的時間。
- 構(gòu)建后端 API 應(yīng)用基礎(chǔ)設(shè)施的時間。
- 構(gòu)建負載均衡的時間。
- 把上述內(nèi)容用 Ruby 進行基礎(chǔ)設(shè)施即代碼化的時間。
如果要把 API Gateway 算作是基礎(chǔ)設(shè)施初始化的時間來看。***次初始化 API Gateway 用了一天,以后 API Gateway 結(jié)合持續(xù)交付流程每次修改僅僅需要幾分鐘,Serverless 風格的微服務(wù)大大降低了基礎(chǔ)設(shè)施配置和運維門檻。
此外,對于團隊來說,Amazon API Gateway + Lambda 的微服務(wù)還帶來其它好處:
- 開發(fā)效率高,原先至少 45 分鐘的開發(fā)反饋周期縮短為 5 分鐘以內(nèi)。
- 無關(guān)的代碼量少,需要維護的代碼量少。除了專注業(yè)務(wù)本身。上游和 API Gateway 的集成以及下游和后端服務(wù)的集成代碼量很少。
- 應(yīng)用維護成本低。代碼僅僅幾十行,且都為函數(shù)式,很容易測試。避免了代碼庫內(nèi)部復(fù)雜性的增加。
此外,我們做了 Java 和 NodeJs 比較。在開發(fā)同樣的功能下,NodeJS 的開發(fā)效率更高,原因是 Java 要把請求的 json 轉(zhuǎn)化為對象,也要把返回的 json 轉(zhuǎn)化為對象,而不像 nodejs 直接處理 json。此外, Java 需要引入一些其它 JAR 包作為依賴。在 AWS 場景下開發(fā)同樣一個函數(shù)式微服務(wù),nodejs 有 4 倍于 java 的開發(fā)效率提升。
六、***
Serverless 風格的微服務(wù)雖然大大減少了開發(fā)工作量以及基礎(chǔ)設(shè)施的開發(fā)維護工作量。但也帶來了新的挑戰(zhàn):
- 大量函數(shù)的管理。
- SIT,UAT 環(huán)境的管理。
- 持續(xù)交付流水線的配置。
- 面對基礎(chǔ)設(shè)施集成帶來的測試。
這讓我們重新思考了 Serverless 架構(gòu)的微服務(wù)如何更好的進行持續(xù)交付。
【本文是51CTO專欄作者“ThoughtWorks”的原創(chuàng)稿件,微信公眾號:思特沃克,轉(zhuǎn)載請聯(lián)系原作者】