微服務:使用Angular + Nodejs+spanner+AppEngine in GCP的一個微服務實戰

一、簡介

這不是一個微服務項目哦!不知道如何起名,只好濫用它了。
“微服務架構有一條重要規則:每個微服務必須擁有領域邏輯和數據。與完整的應用有邏輯和數據類似,在自治的生命週期內,微服務也有自己的邏輯和數據,並可針對每個微服務獨立部署。” 

本項目中所有的服務共用一個關係型數據庫,也沒有領域驅動設計,所以它不能算是一個微服務項目,但作爲一個開發人員遇到的絕大部分的項目都是重複造輪子,能遇到一個項目,它包含多個服務且已正式上線,這也勉強算是一次不錯的經歷吧。

本文將一個傳統的單體應用重構成一個現代化的多服務的雲端應用,以及使用微服務化的思想來解決問題。將一個已有的大單體應用,經過少量的重構後拆分成多個服務,每個服務都能獨立地開發、部署和擴展,它們將部署到谷歌的GCP雲上,最終作爲了一個整體爲應用程序提供服務。有以下幾點好處:

(1) 可維護性: 此種架構提供了長期敏捷性,這些服務通常擁有細粒度和獨立生命週期等特徵,這使得複雜的、大型的以及高度擴展的系統,擁有更好的可維護性;

(2)節約成本:每個服務都能獨立進行橫向擴展,這樣就能只擴展真正需要更多處理資源或網絡帶寬的功能,而不是一起將本不需要擴展的 其他功能區域也進行擴展。因爲使用的硬件更少,這也意味着可以節約成本。

二、單體應用

本是一個單體應用, 採用前後端分離的方式,前端使用angular框架,後端採用Nodejs+Mysql,這部分相對簡單,不是本文的重點。具體的架構圖如下:

具體的網頁如下圖所示

可以看出是一個非常普通的網站,在這裏我們試想一下,我們如何對一個大單體進行拆分,是否需要全面遷移和重構?

三、應用設計

微服務提供了強大優勢的同時也帶來了巨大的新挑戰,我們打算採用微服務的思想對它進行分析。

(一)、如何設計?

微服務架構模式爲創建微服務應用提供了基礎支持,它其實是某種領域驅動設計(DDD)模式外加容器編排理論。在實際項目中,前端已經被分離出去了,我們只需要對後端API按某種規則拆成一個個小的微服務,目前有大概20多個微服務,每個微服務都是一個單獨的Nodejs WebAPI應用,他們互不依賴,可以進行獨立的開發和部署,最後將這些微服務部署到GCP雲上的AppEngine裏,AppEngine本身包含對容器編排、服務自動橫向擴縮的能力。

  • 領域驅動設計:我們沒有使用領域驅動設計(如果說有的話,那就是所有服務共用一套通用領域模型,感覺怪怪的)
  • 容器編排:目前主流使用K8s,但我們不必要自己在本地搭建它,各類雲廠商已提供了很好的此類服務,比如Azure的AKS,GCP的GKE等,他們提供了強大的容器編排服務能力,但也提高了學習和使用成本,這裏我們使用更加集成化的PAAS服務AppEngine,它已經包含了容器編排的能力,我們只需要負責上傳代碼就可以了,其他的事全部交給雲服務去處理。
  • 服務間的通信(同步/異步):目前本項目中也沒有,下圖像少了個尾巴,但使用了其他的替代方案,後面將會完善。

部署在AppEngine中的服務大致如下圖所示, 在AppEngine的最佳實踐中可以看出,它天然支持多服務。紅色部分就是部署在AppEngine中的服務,如果想了解AppEngine的更多內容,猛戳這裏

 

(二)、如何拆分服務?

比較有技術學問的應該屬於這一塊了,服務拆分講究一個“度”,學過馬哲的小夥伴應該知道“度”是屬於哲學的一個範疇,哈哈,但有一些準則是可以測量它的。

微服務的大小不是重點,服務拆分粒度應該保證微服務具有業務的獨立性與完整性,儘可能少的存在服務依賴,鏈式調用,以便能獨立地開發、部署和擴展每個服務。從程序設計的簡單角度來說就是解耦,比如說你的團隊提交的代碼和其他團隊提交的代碼頻繁出現大量衝突或需要頻繁溝通的時候,你是否需要考慮把服務拆分一下呢?

拆分服務的核心是如何識別微服務的領域模型邊界,而微服務的理論本身就源自於領域驅動設計(DDD)的限界上下文(BC)模式,所在拆分服務的時候使用DDD 模式是一種好的選擇,它可以用來識別限界上下文。本質上,當我們對相關領域的瞭解越深入,就應該越能夠適配微服務的大小,找到正確的大小。

拆分服務出合適的微服務的大小,通常這個目標是無法一蹴而就的。在實際的開發過程,我們在設計之初可以將服務的粒度設計的大一些,並考慮其可擴展性,隨着業務的發展,再慢慢根據需要進一步地拆分。

服務拆分既可以通過業務能力拆分,也可以通過領域驅動設計(DDD)進行拆分。

服務拆分的內容很多,需要更多的文章才能講得明白,我這裏準備的一些鏈接,他們都講得太好了。

本項目中,到目前爲止拆分出了20多個微服務了,也是採用漸進的或者逐漸改進的方式來拆出這麼多的。一開始是簡單粗暴按菜單進行拆分,也可以理解爲業務邏輯拆分,後來又單獨拆出的部分服務如下:

  • 網站的訪問權限,拆分作爲一個服務
  • 網站的一個框架,即一個殼子(shell) 及基礎數據,拆分作爲了一個服務
  • 某一個查詢頁面,它包括不同維度的查詢,幾乎不涉及寫操作,拆分作爲了一個服務
  • 網站的提醒功能,拆分作爲了一個服務
  • CRON JOB用於定時跑任務的,拆分作爲了一個服務

可以看到,只要一個服務具有業務的獨立性與完整性,與其他服務不存在依賴,那它就可以被拆出來,拆服務之前你的DevOps一定要先弄好,這是微服務化的前提。
 

(三)、單個服務的架構

微服務是一種思想,或者說是一種邏輯架構,創建微服務並不要求必須使用某種技術,例如 Docker 容器就不是必需的。在本項目中就沒有使用Docker,類似於下圖所示,只需要有app.yaml這個配置文件, 就可以使用GCP SDK 將單個服務部署上去的,並沒有將其打包爲了一個Docker鏡像。

 (四)、爲什麼不使用多數據庫?

單個服務必須擁有自己的領域模型(數據 + 邏輯 + 行爲),這樣纔算一個完整的微服務,也就是說每個服務都獨立擁有自己的數據庫,這樣會帶來不小的挑戰:

  1. 如何拆分服務。數據庫如何設計幾乎與你的領域驅動模型相匹配,所以要先拆分服務。
  2. 如何創建從多個微服務獲取數據的查詢。通常一個服務是不能直接訪問另一個服務的數據庫的,比如:你需要生成一個Report,它需要從不同的數據庫的表中聚合數據。有兩種辦法:(1)調用它們的API將數據聚合起來。(2)使用 CQRS 來處理多數據庫,提前生成好Report的數據,即提前生成只讀表或視圖。 如果這種聚合的操作發生的很頻繁,那麼要考慮之前拆分服務是否拆錯了,是否需要合併服務。
  3. 多數據庫之間如何實現一致性。通常我們不使用強一致性,強一致性很難做到高可用和高可擴展。 更多的是使用最終一致性。辦法是:事件驅動、異步通信,常用的雲服務有Azure的Service Bus, GCP的Pub/Sub

從上可以看出,我們需要花額外的努力去解決這些的問題,而且不太容易解決。在本項目中並沒有使用多數據庫的方式,因爲項目不算大,沒有領域模型,項目中只使用了箇中心化的數據庫,是使用GCP的Spanner數據庫,它是一個分佈式的關係型數據庫,我們項目所有的服務共用這一個數據庫。

微服務是把雙刃劍!

如果你的項目比較複雜,一張表可能數十列,甚至上百列,而某些相對獨立的業務並不會用到所有的列,即不同的領域模型用得到字段是不一樣的,那麼就可以考慮是否改用多數據庫的方式。

如果你的項目不算複雜或者項目成員還沒有足夠的能力去使用微服務架構,往往單體應用或像本項目這樣的僞微服務的變體應用更加適合。

(五)、應用數據如何存儲?

可使用雲服務,各類雲廠商都提供了相應的對象存儲服務。

  • AWS: 可以使用S3存儲
  • Azure: 可以使用Service Account存儲
  • GCP: 可以使用Storage存儲
  • Ali: 可以使用OSS存儲

(六)、如何實現使用 API 網關的?

它主要是爲多個微服務提供單個入口的服務,可使用雲服務,各類雲廠商都提供了相應的API 網關服務。

  • AWS: 可以使用API Gateway
  • Azure: 可以使用Api Management

在本項目中,由於使用得是AppEngine服務,它本身內置了這種路由分發和負載均衡的功能,dispatch.yaml文件就定義了這種路由規則,想了解AppEngine的更多內容,猛戳這裏

(七)、服務通信與服務治理?

這些如果你想使用的話,其實也需要花不少的努力去處理這類問題的。如果你希望瞭解服務通信原理的話可以參考如下鏈接。

如果你希望瞭解服務治理方面的內容的話,建議你去看一下istio

但本項目使用AppEngine,它也內置了這些功能,想了解AppEngine的更多內容,猛戳這裏

 

(八)、關於部署,使用DevOps

本項目是使用Azure DevOps工具來實現CI/CD。

(1)Pipelines->Library主要是用於存放應用程序所需要的環境變量、參數、及文件等敏感信息,比如:密碼、數據庫連接字符串、密鑰文件等,通常來說源代碼裏不應該包含這些敏感數據,而是放在這裏。我們首先需要在Library裏給每個環境分別定義一個Variable group,它裏面定義應用程序運行所需要的環境變量。接着在Secure files存放GCP service account的json文件,它包含私鑰信息,有了它我們就可以通過GCP SDK將應用程序部署到GCP雲上。

 (2)Pipelines->Pipelines 是用於持續集成(CI),我們可以使用它將程序打包、運行UT及測試覆蓋率等,最終構建出我們需要的Artifact。本項目中是使用yaml文件來創建此pipeline的,它定義在源代碼中,點擊azure-pipelines.yml

(3)Pipelines->Releases是用於部署的(CD),將CI中生成好的Aritifact部署到相應的環境中去,比如:部署到虛擬機或某些PAAS服務裏,以便用可以通過IP或域名訪問到我們的應用程序。本項目中是將Artifact部署到GCP的AppEngine裏,下面主要列出bash腳本。

# 第一步:在release definition中選擇agent時你選擇要下載的artifact zip包

# 第二步:在release definition中創建一個task用於下載service account文件

# 第三步:解壓Artifact, 如果你不知道像這樣的參數$AGENT_RELEASEDIRECTORY的實際路徑是什麼,不妨先運行一個Release試試,在Initialize job階段它會列出所有參數及相應的值,或者參考官網,裏面有預先定義的參數。
if [ -f $AGENT_RELEASEDIRECTORY/Artifacts/release/api-gateway-$(ApiGateway_Srv_Name)-$(Release.Artifacts.Artifacts.BuildNumber).zip ]; then
    cd $AGENT_RELEASEDIRECTORY/Artifacts/release && unzip api-gateway-$(ApiGateway_Srv_Name)-$(Release.Artifacts.Artifacts.BuildNumber).zip
else
    echo "File not found to extract : $AGENT_RELEASEDIRECTORY/Artifacts/release/api-gateway-$(ApiGateway_Srv_Name)-$(Release.Artifacts.Artifacts.BuildNumber).zip"
    exit 1
fi

# 第四步:在release definition中創建一個task用於安裝GCP SDK

# 第五步:部署到AppEngine的時候是需要用app.yaml文件的,它裏定義了運行時和所需要的環境變量,所以我們需要將Library裏定義的環境變量寫入到app.yaml文件中,事先我們在app.yaml中給每個環境變量設置了placeholder,在這一步,我們需要將這些placeholder替換成真正的環境變量。比如:PROJECT_URL是library中定義的變量,$(PROJECT_URL)是它的值,[PROJECT_URL]是在app.yaml中設置的placeholder。

cd $AGENT_RELEASEDIRECTORY/Artifacts/release/dist_archive

ls -ail && pwd

echo "************ Replace PROJECT_URL ************"
sed -i -e "s/\[PROJECT_URL\]/$(PROJECT_URL)/g" "./app.yaml"
echo "************ Replace CLIENT_ID************"
sed -i -e "s/\[CLIENT_ID\]/$(CLIENT_ID)/g" "./app.yaml"


# 第六步:激活serviceAccount,然後部署artifact到appengine中。

cd $AGENT_RELEASEDIRECTORY/Artifacts/release/dist_archive

ls -ail && pwd

gcloud config set verbosity debug

cp $(Agent.TempDirectory)/$(GCP_CREDENTIAL_FILE) ./keyfile.json

# 激活serviceAccount
gcloud auth activate-service-account $(SR_ACCT_CLIENT_EMAIL) --key-file=./keyfile.json --project $(GCP_PROJECT_ID)

# 設置此serviceAccount爲當前的account
gcloud config set core/account $(SR_ACCT_CLIENT_EMAIL)

# $(SERVICE_FILE_PATH) 實際就是app.yaml文件, 你可以將它hard code在這。
SERVICE_FILE_PATH="$(SERVICE_FILE_PATH)"
if [ -z "$(CRON_SERVICE_FILE_PATH)" ]
then
    echo "SERVICE_FILE_PATH is: $SERVICE_FILE_PATH"
    # 部署artifact到appengine中,就這麼一行,前面所有的操作都爲它而準備
    gcloud app deploy $SERVICE_FILE_PATH  --quiet
else
    SERVICE_FILE_PATH="$SERVICE_FILE_PATH $(CRON_SERVICE_FILE_PATH)"
    echo "SERVICE_FILE_PATH is: $SERVICE_FILE_PATH"
    gcloud app deploy $SERVICE_FILE_PATH  --quiet
fi

如上三步操作定義瞭如何使用CI/CD一鍵部署我們的服務到AppEngine中,一個服務需要定義如上三步,如果你有20個服務,就需要定義20個這樣的三步操作。 對於第三步,裏面包含6個Task,你可以抽象出來把這6個Task放到一個Task group裏。

如上CD是使用GCP SDK來部署的,其實還可以使用Terraform,它是一個 IT 基礎架構自動化編排工具,將會在不久的將來介紹它。

 

(九)、如何處理認證和授權

本項目這裏使用的是Azure的Azure AD認證服務,戳這裏瞭解

四、代碼

本代碼是從實際項目中提煉出來,去掉了敏感信息,以及部分的改造讓它變得更通用, 以便下次可以重用。代碼的具體說明見代碼中的readme,包含的內容有:

  • DevOps中的CI部分
  • 如何在本地運行單個服務
  • 如何在本地運行所有服務
  • 如何對Azure AD的token進行驗證
  • 設置cors

 

五、討論與總結

1. 問題:假如後端的API被拆分成了20多個服務,前端網站必須依賴後端所有的API才能正常運行。那開發人員在本地開發的時候,豈不是要將這20多個服務全部運行起來?如果你是做.net程序開發的,每個服務對應一個WebApi Project,那這20多個WebApi Project如何同時運行?

答:對於本項目是使用Nodejs,單個項目既可以獨立運行又可以作爲子項目運行,所以很好地解決了這個問題,具體參考代碼裏的readme。那如果是其他非nodejs應用程序該如何解決呢?如果你有更好的解決辦法,歡迎評論

2.問題:我們一共使用了哪些雲服務?

答:(1)使用了AWS的S3來Host前端網站,包含S3+Lambda+CloudFront . (2) 使用了GCP裏的AppEngine來Host後端的所有服務,包含AppEngine+Google Storage+IAM+Log Viewer。 (3) 使用了Azure的Azure AD作了認證服務,以及Azure DevOps來託管代碼和持續集成。 可以看到,不知不覺我們已經使用了雲的許多服務,要想構建一個現代化的應用已離不開雲,它已經像水電煤一樣慢慢滲透在我們的工作和生活之中。

六、源碼

https://github.com/wucong60/nodejs-appengine-example

參考鏈接

基於微服務的容器化應用程序: eShopOnContainers

.net架構書籍系列

各種騷操作教你如何上雲

 

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