石墨測試

標題:

石墨文檔基於Kubernetes的Go微服務實踐(上篇)


作者 | 彭友順@石墨文檔


導語:

在2014年6月Google開源了Kubernetes後,經過這幾年的發展,已逐漸成爲容器編排領域的事實標準, 可以稱之爲雲原生時代的操作系統,它使得基礎設施維護變得異常簡單。在雲原生時代,微服務依賴於Kubernetes的優勢在哪,微服務的生命週期基於Kubernetes該如何實踐呢?本文整理自石墨文檔架構負責人彭友順在Gopher China Meetup西安站的主題演講《石墨文檔基於Kubernetes的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 等多種語言的樁代碼和錯誤碼,並推送到指定對應的中心化倉庫。



推送到倉庫後,我們就可以通過各語言的包管理工具拉取客戶端、服務端的gRPC和錯誤碼的依賴,不需要口頭約定對接數據的定義,也不需要通過 IM 工具傳遞對接數據的定義文件,極大的簡化了對接成本。

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

Plain Text
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 資料鏈接

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