石墨文檔基於 K8S 的 Go 微服務實踐(上篇)
1、 架構演進
互聯網的 WEB 架構演進可以分爲三個階段:單體應用時期、垂直應用時期、微服務時期。
單體應用時期一般處於一個公司的創業初期,他的好處就是運維簡單、開發快速、能夠快速適應業務需求變化。但是當業務發展到一定程度後,會發現許多業務會存在一些莫名奇妙的耦合,例如你修改了一個支付模塊的函數,結果登錄功能掛了。爲了避免這種耦合,會將一些功能模塊做一個垂直拆分,進行業務隔離,彼此之間功能相互不影響。但是在業務發展過程中,會發現垂直應用架構有許多相同的功能,需要重複開發或者複製粘貼代碼。所以要解決以上覆用功能的問題,我們可以將同一個業務領域內功能抽出來作爲一個單獨的服務,服務之間使用 RPC 進行遠程調用,這就是我們常所說的微服務架構。
總的來說,我們可以將這三個階段總結爲以下幾點。單體應用架構快速、簡單,但耦合性強;垂直應用架構隔離性、穩定性好,但複製粘貼代碼會比較多;微服務架構可以說是兼顧了垂直應用架構的隔離性、穩定性,並且有很強的複用性能力。可以說微服務架構是公司發展壯大後,演進到某種階段的必然趨勢。
但微服務真的那麼美好嗎?我們可以看到一個單體架構和微服務架構的對比圖。在左圖我們可以看到一個業務可以通過 Nginx + 服務器 + 數據庫就能實現業務需求。但是在右圖微服務架構中,我們完成一個業務需要引入大量的組件,比如在中間這一塊我們會引入 DNS、HPA、ConfigMap 等、下面部分引入了存儲組件 Redis、MySQL、Mongo 等。以前單體應用時期我們可能直接上機器看日誌或上機器上查看資源負載監控,但是到了微服務階段,應用太多了,肯定不能這麼去操作,這個時候我們就需要引入 ELK、Prometheus、Grafana、Jaeger 等各種基礎設施,來更方便地對我們的服務進行觀測。
微服務的組件增多、架構複雜,使得我們運維變得更加複雜。對於大廠而言,人多維護起來肯定沒什麼太大問題,可以自建完整的基礎設施,但對於小廠而言,研發資源有限,想自建會相當困難。
不過微服務的基礎設施維護困難的問題在 Kubernetes 出現後逐漸出現了轉機。在 2014 年 6 月 Google 開源了 Kubernetes 後,經過這幾年的發展,已逐漸成爲容器編排領域的事實標準。同時 Kubernetes 已儼然成爲雲原生時代的超級操作系統,它使得基礎設施維護變得異常簡單。
在傳統模式下,我們不僅需要關注應用開發階段存在的問題,同時還需要關心應用的測試、編譯、部署、觀測等問題,例如程序是使用 systemd、supervisor 啓動、還是寫 bash 腳本啓動?日誌是如何記錄、如何採集、如何滾動?我們如何對服務進行觀測?Metrics 指標如何採集?採集後的指標如何展示?服務如何實現健康檢查、存活檢查?服務如何滾動更新?如何對流量進行治理,比如實現金絲雀發佈、流量鏡像?諸如此類的問題。我們業務代碼沒寫幾行,全在考慮和權衡基礎設施問題。然而使用 Kubernetes 後,可以發現大部分問題都已經被 Kubernetes 或周邊的生態工具解決了,我們僅僅只需要關心上層的應用開發和維護 Kubernetes 集羣即可。
Kubernetes 在微服務中的作用就如同建高樓的地基,做了很多基礎工作,統一了大量的基礎設施標準,以前我們要實現服務的啓動、配置、日誌採集、探活等功能需要寫很多中間件,現在我們只需要寫寫 yaml 文件,就可以享受這些基礎設施的能力。運維更加簡單這個也顯而易見,例如在以前出現流量高峯時研發提工單後增加副本數,運維處理工單,人肉擴縮容,現在我們可以根據實際應用的負載能力,合理的配置好副本 CPU、Mem 等資源及 HPA 規則,在流量高峯時由 Kubernetes 自動擴容、流量低谷時自動縮容,省去了大量人工操作。
同時在框架層面,傳統模式下基礎設施組件很多都是自研的,基本上沒有太多標準可言,框架需要做各種 switch case 對這種基礎設施組件的適配,並且框架經常會爲因爲基礎設施的改變,做一些不兼容的升級。現在只需要適配 Kubernetes 即可,大大簡化微服務的框架難度和開發成本。
2、 微服務的生命週期
剛纔我們講到 Kubernetes 的優勢非常明顯,在這裏會描述下我們自己研發的微服務框架 Ego 怎麼和 Kubernetes 結合起來的一些有趣實踐。
我們將微服務的生命週期分爲以下 6 個階段:開發、測試、部署、啓動、調用、治理。
2.1 開發階段
在開發階段我們最關注三個問題。如何配置、如何對接,如何調試。
2.1.1 配置驅動
大家在使用開源組件的時候,其實會發現每個開源組件的配置、調用方式、debug 方式、記錄日誌方式都不一樣,導致我們需要不停去查看組件的示例、文檔、源碼,才能使用好這個組件。我們只想開發一個功能,卻需要關心這麼多底層實現細節,這對我們而言是一個很大的心智負擔。
所以我們將配置、調用方式做了統一。可以看到上圖我們所有組件的地址都叫 addr,然後在下圖中我們調用 redis、gRPC、MySQL 的時候,只需要基於組件的配置 Key path 去 Load 對應的組件配置,通過 build 方法就可以構造一個組件實例。可以看到調用方式完全相同,就算你不懂這個組件,你只要初始化好了,就可以根據編輯器代碼提示,調用這個組件裏的 API,大大簡化我們的開發流程。
2.1.2 配置補齊
配置補齊這個功能,是源於我們在最開始使用一些組件庫的時候,很容易遺漏配置,例如使用gRPC
的客戶端,未設置連接錯誤、導致我們在阻塞模式下連接不上的時候,沒有報正確的錯誤提示;或者在使用 Redis、MySQL 沒有超時配置,導致線上的調用出現問題,產生雪崩效應。這些都是因爲我們對組件的不熟悉,纔會遺漏配置。框架要做的是在用戶不配置的情況下,默認補齊這些配置,並給出一個最佳實踐配置,讓業務方的服務更加穩定、高效。
2.1.3 配置工具
我們編寫完配置後,需要將配置發佈到測試環境,我們將配置中心 IDE 化,能夠非常方便的編寫配置,通過鼠標右鍵,就可以插入資源引用,鼠標懸停可以看到對應的配置信息。通過配置中心,使我們在對比配置版本,發佈,回滾,可以更加方便。
2.1.4 對接 - Proto 管理
我們內部系統全部統一採用gRPC
協議和protobuf
編解碼。統一的好處在於不需要在做任何協議、編解碼轉換,這樣就可以使我們所有業務採用同一個protobuf
倉庫,基於 CI/CD 工具實現許多自動化功能。
我們要求所有服務提供者提前在獨立的路徑下定義好接口和錯誤碼的 protobuf 文件,然後提交到 GitLab,我們通過 GitLab CI 的 check 階段對變更的 protobuf 文件做 format、lint、breaking 檢查。然後在 build 階段,會基於 protobuf 文件中的註釋自動產生文檔,並推送至內部的微服務管理系統接口平臺中,還會根據 protobuf 文件自動構建 Go/PHP/Node/Java 等多種語言的樁代碼和錯誤碼,並推送到指定對應的中心化倉庫。
2.1.5 對接 - 錯誤碼管理
有了以上比較好的 protobuf 生成流程後,我們可以進一步簡化業務錯誤狀態碼的對接工作。而我們採用了以下方式:
-
Generate:
-
編寫 protobuf error 的插件,生成我們想要的 error 代碼
-
根據 go 官方要求,實現 errors 的 interface,他的好處在於可以區分是我們自定義的 error 類型,方便斷言。
- 根據註解的 code 信,在錯誤碼中生成對應的 grpc status code,業務方使用的時候少寫一行代碼
-
確保錯誤碼唯一,後續在 API 層響應用戶數據確保唯一錯誤碼,例如: 下單失敗 (xxx)
-
errors 裏設置 with message,with metadata,攜帶更多的錯誤信息
-
Check:
-
gRPC 的 error 可以理解爲遠程 error,他是在另一個服務返回的,所以每次 error 在客戶端是反序列化,new 出來的。是無法通過 errors.Is 判斷其根因。
- 我們通過工具將 gRPC 的錯誤碼註冊到一起,然後客戶端通過 FromError 方法,從註冊的錯誤碼中,根據 Reason 的唯一性,取出對應的錯誤碼,這個時候我們可以使用 errors.Is 來判斷根因。
- 最後做到 errors.Is 的判斷: errors.Is(eerrors.FromError(err), UserErrNotFound())
2.1.6 對接 - 調試
對接中調試的第一步是閱讀文檔,我們之前通過 protobuf 的 ci 工具裏的 lint,可以強制讓我們寫好註釋,這可以幫助我們生成非常詳細的文檔。
基於 gRPC Reflection 方法,服務端獲得了暴露自身已註冊的元數據能力,第三方可以通過 Reflection 接口獲取服務端的 Service、Message 定義等數據。結合 Kubernetes API,用戶選擇集羣、應用、Pod 後,可直接在線進行 gRPC 接口測試。同時我們可以對測試用例進行存檔,方便其他人來調試該接口。
2.1.7 Debug - 調試信息
我們大部分的時候都是對接各種組件 API,如果我們能夠展示各種組件例如 gRPC、HTTP、MySQL、Redis、Kafka 的調試信息,我們就能夠快速的 debug。在這裏我們定義了一種規範,我們將配置名、請求 URL、請求參數、響應數據、耗時時間、執行行號稱爲 Debug 的六元組信息。
將這個 Debug 的六元組信息打印出來,如下圖所示。我們就可以看到我們的響應情況,數據結構是否正確,是否有錯誤。
2.1.8 Debug - 定位錯誤
Debug 裏面有個最重要的一點能夠快速定位錯誤問題,所以我們在實踐的過程中,會遵循 Fail Fast 理念。將框架中影響功能的核心錯誤全部設置爲 panic,讓程序儘快的報錯,並且將錯誤做好高亮,在錯誤信息裏顯示 Panic 的錯誤碼,組件、配置名、錯誤信息,儘快定位錯誤根因。這個圖裏面就是我們的錯誤示例,他會高亮的顯示出來,你的配置可能不存在,這個時候業務方在配置文件中需要找到server.grpc
這個配置,設置一下即可。
2.2 測試階段
2.2.1 測試類型
開發完成後,我們會進入到測試階段。我們測試可以分爲四種方式:單元測試、接口測試、性能測試、集成測試。
我們會通過 docker-compose 跑本地的一些單元測試,使用 GitLab CI 跑提交代碼的單元測試。我們接口測試則使用上文所述接口平臺裏的測試用例集。性能測試主要是分兩種,一類是 benchmark 使用 GitLab ci。另一類是全鏈路壓測就使用平臺工具。集成測試目前還做的不夠好,之前是用 GitLab ci 去拉取鏡像,通過 dind(Docker in Docker)跑整個流程,但之前我們沒有拓撲圖,所以需要人肉配置 yaml,非常繁瑣,目前我們正在結合配置中心的依賴拓撲圖,準備用 jekins 完成集成測試。
在這裏我主要介紹下單元測試。
2.2.2 工具生成測試用例
單元測試優勢大家都應該很清楚,能夠通過單測代碼保證代碼質量。但單測缺點其實也非常明顯,如果每個地方都寫單測,會消耗大家大量的精力。
所以我們首先定義了一個規範,業務代碼裏面不要出現基礎組件代碼,所有組件代碼下層到框架裏做單元測試。業務代碼裏只允許有 CRUD 的業務邏輯,可以大大簡化我們的測試用例數量。同時我們的業務代碼做好 gRPC,HTTP 服務接口級別的單元測試,可以更加簡單、高效。
然後我們可以通過開發 protobuf 工具的插件,拿到 gRPC 服務的描述信息,通過他結合我們的框架,使用指令自動生成測試代碼用例。在這裏我們框架使用了 gRPC 中的測試 bufconn 構造一個 listener,這樣就可以在測試中不關心 gRPC 服務的 ip port。
以下是我們通過工具生成的單元測試代碼,我們業務人員只需要在紅框內填寫好對應的斷言內容,就可以完成一個藉口的單測。
2.2.3 簡單高效做單元測試
目前單元測試大部分的玩法,都是在做解除依賴,例如以下的一些方式
-
面向接口編程
-
依賴注入、控制反轉
-
使用 Mock
不可否認,以上的方法確實可以使代碼變得更加優雅,更加方便測試。但是實現了以上的代碼,會讓我們的代碼變得更加複雜、增加更多的開發工作量,下班更晚。如果我們不方便解除依賴,我們是否可以讓基礎設施將所有依賴構建起來。基礎設施能做的事情,就不要讓研發用代碼去實現。
以下舉我們一個實際場景的 MySQL 單元測試例子。我們可以通過 docker-compose.yml,構建一個 mysql。然後通過 Ego 的應用執行 job。
-
創建數據庫的表./app --job=install
-
初始化數據庫表中的數據 ./app --job=intialize
-
執行 go test ./...
可以看到我們可以每次都在乾淨的環境裏,構建起服務的依賴項目,跑完全部的測試用例。詳細 example 請看 https://github.com/gotomicro/go-engineering。
2.3 部署階段
2.3.1 注入信息
編譯是微服務的重要環節。我們可以在編譯階段通過-ldflags
指令注入必要的信息,例如應用名稱、應用版本號、框架版本號、編譯機器 Host Name、編譯時間。該編譯腳本可以參考 https://github.com/gotomicro/ego/blob/master/scripts/build/gobuild.sh:
go build -o bin/hello -ldflags -X "github.com/gotomicro/ego/core/eapp.appName=hello -X github.com/gotomicro/ego/core/eapp.buildVersion=cbf03b73304d7349d3d681d3abd42a90b8ba72b0-dirty -X github.com/gotomicro/ego/core/eapp.buildAppVersion=cbf03b73304d7349d3d681d3abd42a90b8ba72b0-dirty -X github.com/gotomicro/ego/core/eapp.buildStatus=Modified -X github.com/gotomicro/ego/core/eapp.buildTag=v0.6.3-2-gcbf03b7 -X github.com/gotomicro/ego/core/eapp.buildUser=`whoami` -X github.com/gotomicro/ego/core/eapp.buildHost=`hostname -f` -X github.com/gotomicro/ego/core/eapp.buildTime=`date +%Y-%m-%d--%T`"
通過該方式注入後,編譯完成後,我們可以使用./hello --version ,查看該服務的基本情況,如下圖所示。
2.3.2 版本信息
微服務還有一個比較重要的就是能夠知道你的應用當前線上跑的是哪個框架版本。我們在程序運行時,使用 go 裏面的 debug 包,讀取到依賴版本信息,匹配到我們的框架,得到這個版本。
然後我們就可以在 prometheus 中或者二進制中看到我們框架的版本,如果框架某個版本真有什麼大 bug,可以查詢線上運行版本,然後找到對應的應用,讓他們升級。
2.3.3 發佈版本
發佈配置版本,我們在沒有 Kubernetes 的時候,不得不做個 agent,從遠端 ETCD 讀取配置,然後將文件放入到物理機裏,非常的繁瑣。而使用 Kubernetes 發佈配置,就會非常簡單。我們會在數據庫記錄配置版本信息,然後調用 Kubernetes API,將配置寫入到 config map 裏,然後再將配置掛載到應用裏。
發佈微服務應用版本,因爲有了 Kubernetes 就更加簡單,我們只需要發佈系統調用一下 deployment.yml 就能實現,應用的拉取鏡像、啓動服務、探活、滾動更新等功能。
2.4 啓動階段
2.4.1 啓動參數
EGO
內置很多環境變量,這樣可以很方便的通過基礎設施將公司內部規範的一些數據預設在Kubernetes
環境變量內,業務方就可以簡化很多啓動參數,在dockerfile
裏啓動項變爲非常簡單的命令行:CMD ["sh", "-c", "./${_APP_}"]
2.4.2 加載配置
我們通過 Kubernetes configmap 掛載到應用 pod,通過框架 watch 該配置。在這裏要提醒一點,Kubernetes 的配置是軟鏈模式,框架要想要監聽該配置,必須使用 filepath.EvalSymlinks(fp.path) 計算出真正的路徑。然後我們就可以通過配置中心更改配置,通過 configmap 傳遞到我們的框架內部,實現配置的實時更新。
2.4.3 探活
探活的概念:
-
livenessProbe:如果檢查失敗,將殺死容器,根據 Pod 的 restartPolicy 來操作
-
readinessProbe:如果檢查失敗,Kubernetes 會把 Pod 從 service endpoints 中剔除
轉換成我們常見的研發人話就是,liveness 通常是你服務 panic 了,進程沒了,檢測 ip port 不存在了,這個時候 Kubernetes 會殺掉你的容器。而 readinessProbe 則是你服務可能因爲負載問題不響應了,但是 ip port 還是可以連上的,這個時候 Kubernetes 會將你從 service endpoints 中剔除。
所以我們 liveness Probe 設置一個 tcp 檢測 ip port 即可,readness 我們需要根據 HTTP,gRPC 設置不同的探活策略。
當我們確保服務接口是 readness,這個時候流量就會導入進來。然後在結合我們的滾動更新,我們服務可以很優雅的啓動起來。(liveness、readness 必須同時設置,而且策略必須有差異,否則會帶來一些問題)
2.5 調用階段
我們在使用 Kubernetes 的時候,初期也使用最簡單的 dns 服務發現,他的好處就是簡單方便,gRPC 中直接內置。但是在實際的使用過程中,發現 gRPC DNS Resolver 還是存在一些問題。
gRPC DNS Resolver 使用了 rn 的 channel 傳遞事件。當客戶端發現連接有異常,都會執行 ResolveNow,觸發客戶端更新服務端副本的列表。但是當 K8S 增加服務端副本時,客戶端連接是無法及時感知的。
因爲 gRPC DNS Resolver 存在的問題,我們自己實現了 Kubernetes API Resolver。我們根據 Kubernetes 的 API,watch 服務的 endpoints 方式,實現服務發現。
我們再來梳理下微服務在 Kubernetes 的註冊與發現的流程,首先我們服務啓動後,探針會通過 ip port 檢測我們的端口查看我們是否是活的,如果是活的就說明我們的 pod 已經跑起來了,然後會通過探針訪問我們 gRPC 服務的 health 接口,如果是可用的,這個時候 Kubernetes 會將我們這個服務的 pod ip 註冊到 service endpoints,流量就會隨之導入進來。然後我們的客戶端會通過 Kubernetes API Watch 到 service endpoints 的節點變化,然後將該節點添加到它自己的服務列表裏,然後它就可以通過 Balancer 調用服務節點,完成 RPC 調用。
由於篇幅較多,以上介紹了微服務生命週期的一部分,下期我們在介紹微服務治理中的監控、日誌、鏈路、限流熔斷、報警、微服務管理等內容。以下是 ego 架構圖和研發生命週期的全景圖。
3、 資料鏈接
-
Ego 框架:https://github.com/gotomicro/ego
-
文檔:https://ego.gocn.vip
-
PPT:https://github.com/gopherchina/meetup/blob/master/XiAn/20210911/%E7%9F%B3%E5%A2%A8%E6%96%87%E6%A1%A3Go%E5%9C%A8K8S%E4%B8%8A%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%9A%84%E5%AE%9E%E8%B7%B5-%E5%BD%AD%E5%8F%8B%E9%A1%BA.pdf
-
編譯:https://ego.gocn.vip/micro/chapter1/build.html
-
鏈路:https://ego.gocn.vip/micro/chapter2/trace.html
-
限流:https://ego.gocn.vip/frame/client/sentinel.html
-
日誌:https://ego.gocn.vip/frame/core/logger.html
-
docker-compose 單元測試,protobuf 統一錯誤碼:https://github.com/gotomicro/go-engineering
-
proto 錯誤碼插件:https://github.com/gotomicro/ego/tree/master/cmd/protoc-gen-go-errors
-
proto 單元測試插件:https://github.com/gotomicro/ego/tree/master/cmd/protoc-gen-go-test
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KNTS8K3tpsqj3yz7pnF70Q