雲原生與 Go 微服務實戰 - 一文讀懂微服務網關及其實現原理
在單體架構中,客戶端在向服務端發起請求時,會通過類似 Nginx 的負載均衡組件獲取到多個相同的應用程序實例中的一個。請求由該服務實例進行處理,服務端處理完之後返回響應給客戶端。
而在微服務架構下,原來的單體應用拆分成了多個業務微服務。
此時,直接對外暴露這些業務微服務,必然會存在一些問題。客戶端直接向每個微服務發送請求,其問題主要如下:
-
API 粒度的問題,客戶端需求和每個微服務暴露的細粒度可能存在 API 不匹配的情況。
-
微服務之間的調用可能不僅僅基於 HTTP 的方式,還有可能使用 Thrift、gRPC 和 AMQP 消息傳遞協議,這些 API 無法暴露出去。
-
直接對外暴露接口,使得微服務難以重構,特別是服務數量達到一個量級,這類重構就非常困難了。
如上問題,解決的方案是使用微服務網關。網關在一個 API 架構中的作用是保護、增強和控制外部請求對於 API 服務的訪問。
什麼是微服務網關
在微服務架構中,網關位於接入層之下和業務服務層之上。微服務網關是微服務架構中的一個基礎服務,從面向對象設計的角度看,它與外觀模式類似。
微服務架構圖
微服務網關封裝了系統內部架構,爲每個客戶端提供一個定製的 API,用來保護、增強和控制對於微服務的訪問。換句話來講,微服務網關就是一個處於應用程序或服務之前的系統,用來管理授權、訪問控制和流量限制等,這樣微服務就會被微服務網關保護起來,對所有的調用者透明。因此,隱藏在微服務網關後面的業務系統就可以更加專注於業務本身。
微服務網關的功能特性
作爲連接服務消費方和服務提供方的中間件系統,微服務網關將各自業務系統的演進和發展做了天然的隔離,使業務系統更加專注於業務服務本身,同時微服務網關還可以爲服務提供和沉澱更多附加功能。
微服務網關的主要功能特性如下圖所示:
網關的功能特性示意圖
結合該圖,我們就來具體介紹下這四類功能。
-
請求接入。管理所有接入請求,作爲所有 API 接口的請求入口。在生產環境中,爲了保護內部系統的安全性,往往內網與外網都是隔離的,服務端應用都是運行在內網環境中,爲了安全,一般不允許外部直接訪問。網關可以通過校驗規則和配置白名單,對外部請求進行初步過濾,這種方式更加動態靈活。
-
統一管理。可以提供統一的監控工具、配置管理和接口的 API 文檔管理等基礎設施。例如,統一配置日誌切面,並記錄對應的日誌文件。
-
解耦。可以使得微服務系統的各方能夠獨立、自由、高效、靈活地調整,而不用擔心給其他方面帶來影響。軟件系統的整個過程中包括不同的角色,有服務的開發提供方、服務的用戶、運維人員、安全管理人員等,每個角色的職責和關注點都不同。微服務網關可以很好地解耦各方的相互依賴關係,讓各個角色的用戶更加專注自己的目標。
-
攔截插件。服務網關層除了處理請求的路由轉發外,還需要負責認證鑑權、限流熔斷、監控和安全防範等,這些功能的實現方式,往往隨着業務的變化不斷調整。這就要求網關層提供一套機制,可以很好地支持這種動態擴展。攔截策略提供了一個擴展點,方便通過擴展機制對請求進行一系列加工和處理。同時還可以提供統一的安全、路由和流控等公共服務組件。
實戰案例:自己動手實現一個網關
API 網關最基礎的功能是對請求進行路由轉發,根據配置的轉發規則將請求動態地轉發到指定的服務實例。
動態是指與服務發現結合,如 Consul、ZooKeeper 等組件。
API 網關根據客戶端 HTTP 請求,動態查詢註冊中心的服務實例,通過反向代理實現對後臺服務的調用。
API 網關將符合規則的請求路由調用對應的後端服務。這裏的規則可以有很多種,如 HTTP 請求的資源路徑、方法、頭部和參數等。這裏我們以最簡單的請求路徑爲例,規則爲 :/{serviceName}/#。即:路徑第一部分爲註冊中心服務實例名稱,其餘部分爲服務實例的 REST 路徑。如:
/cargo-service/cargos/
/cargo-service/locations
其中:
/cargo-service 爲服務名稱;
/locations 爲 cargo-service 服務提供的接口。
1. 實現思路
客戶端向網關發起請求,網關解析請求資源路徑中的信息,根據服務名稱查詢註冊中心的服務實例;然後使用反向代理技術把客戶端請求轉發至後端真實的服務實例,請求執行完畢後,再把響應信息返回客戶端。
自定義網關的調用請求示意圖
我們設計實現的網關的功能主要包含如下幾點:
-
HTTP 請求的規則遵循 /{serviceName}/#,否則不予通過。
-
使用 Go 提供的反向代理包 httputil.ReverseProxy 實現一個簡單的反向代理,它能夠對請求實現負載均衡,隨機地把請求發送給服務實例。
-
使用 Consul 客戶端 API 動態查詢服務實例。
2. 編寫反向代理方法
創建目錄 gateway,然後新建 main.go 文件。NewReverseProxy 方法接受兩個參數:Consul 客戶端對象 api.Client 和日誌記錄工具 log.Logger,返回反向代理對象。
該方法的實現過程如下:
1,獲取請求路徑,檢查是否符合規則,不符合規則直接返回;
2,解析請求路徑,獲取服務名稱(請求路徑的第一部分);
3,使用 Consul 客戶端查詢服務實例,若查詢到結果,則隨機選擇一個作爲目標實例;
4,根據選定的目標實例,設置反向代理參數 Schema、Host 和 Path。
// 位於 section19/gateway/main.go
// NewReverseProxy 創建反向代理處理方法
func NewReverseProxy(client *api.Client, logger log.Logger) *httputil.ReverseProxy {
// 創建 Director
director := func(req *http.Request) {
// 查詢原始請求路徑,如:/arithmetic/calculate
reqPath := req.URL.Path
if reqPath == "" {
return
}
// 按照分隔符'/'對路徑進行分解,獲取服務名稱 serviceName
pathArray := strings.Split(reqPath, "/")
serviceName := pathArray[1]
// 調用 consul api 查詢 serviceName 的服務實例列表
result, _, err := client.Catalog().Service(serviceName, "", nil)
if err != nil {
logger.Log("ReverseProxy failed", "query service instace error", err.Error())
return
}
if len(result) == 0 {
logger.Log("ReverseProxy failed", "no such service instance", serviceName)
return
}
// 重新組織請求路徑,去掉服務名稱部分
destPath := strings.Join(pathArray[2:], "/")
// 隨機選擇一個服務實例
tgt := result[rand.Int()%len(result)]
logger.Log("service id", tgt.ServiceID)
// 設置代理服務地址信息
req.URL.Scheme = "http"
req.URL.Host = fmt.Sprintf("%s:%d", tgt.ServiceAddress, tgt.ServicePort)
req.URL.Path = "/" + destPath
}
return &httputil.ReverseProxy{Director: director}
}
在反向轉發處理的時候,我們只是根據請求中的服務名直接轉發,如果需要對外屏蔽服務名的話,這樣的路由轉發規則顯然是不夠的。爲了增加路由配置的多樣性,我們可以抽出路由配置層,根據指定的規則進行路由轉發,如根據配置名稱、頭部的信息、請求的參數、請求的 body 等規則轉發到指定的服務。
3. 編寫入口方法
main 方法的主要任務是創建 Consul 連接對象、創建日誌記錄對象和開啓反向代理 HTTP 服務。整個過程與前面課時創建用戶服務類似,代碼如下(爲了測試方便,直接指定了 Consul 服務地址信息):
// 位於 section19/gateway/main.go:65
func main() {
// 創建環境變量
var (
consulHost = flag.String("consul.host", "127.0.0.1", "consul server ip address")
consulPort = flag.String("consul.port", "8500", "consul server port")
)
flag.Parse()
// 創建日誌組件
var logger log.Logger
{
logger = log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
logger = log.With(logger, "caller", log.DefaultCaller)
}
// 創建 consul api 客戶端
consulConfig := api.DefaultConfig()
consulConfig.Address = "http://" + *consulHost + ":" + *consulPort
consulClient, err := api.NewClient(consulConfig)
if err != nil {
logger.Log("err", err)
os.Exit(1)
}
// 創建反向代理
proxy := NewReverseProxy(consulClient, logger)
errc := make(chan error)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
errc <- fmt.Errorf("%s", <-c)
}()
// 開始監聽
go func() {
logger.Log("transport", "HTTP", "addr", "9099")
errc <- http.ListenAndServe(":9099", proxy)
}()
// 開始運行,等待結束
logger.Log("exit", <-errc)
}
如上的代碼實現,爲了創建反向代理,需要先創建日誌組件和 Consul 連接對象。反向代理處理器一般還可以使用裝飾者模式封裝,如增加中間件 Hystrix 斷路器、鏈路追蹤 Tracer(Zipkin、Jaeger)組件等。
4. 運行貨運與網關服務
做好如上的準備步驟之後,我們開始運行貨運服務。爲了測試負載均衡效果,啓動兩個實例。這裏我們是在一臺主機上測試,所以需要使用不同的端口。
首先編譯貨運服務:
$ go build -o cmd/cargo cmd/main.go
在 cmd 目錄下生成了 cargo 可執行文件,下面我們就分別來啓動兩個貨運服務實例:
./cargo/cmd/cargo -consul.host localhost -consul.port 8500 -service.host 127.0.0.1 -service.port 8000
./cargo/cmd/cargo -consul.host localhost -consul.port 8500 -service.host 127.0.0.1 -service.port 8002
啓動成功並註冊到 Consul,控制檯輸出如下:
ts=2020-07-28T10:11:12.974789Z transport=http address=8000 msg=listening
ts=2020-07-28T10:11:13.006241Z service=cargo-service tags="[cargo-service aoho]" address=localhost action=register
再切換至目錄 gateway,執行 go build 完成編譯,最後啓動網關服務。
./gateway -consul.host localhost -consul.port 8500
ts=2020-07-28T10:11:37.662124Z caller=main.go:56 transport=HTTP addr=9099
5. 測試
網關服務和兩個貨運服務實例啓動好之後,我們通過命令行請求貨運服務的接口 /cargos,以獲取指定 Id 的貨運信息,請求如下:
$ curl -X POST \
http://localhost:9099/cargo-service/cargos/ \
-H 'Content-Type: application/json' \
-d '{
"Id": "ABC123"
}'
{
"cargo": {
"arrival_deadline": "2020-08-11T18:56:44.627+08:00",
"destination": "CNHKG",
"misrouted": false,
"origin": "SESTO",
"routed": false,
"tracking_id": "ABC123"
}
}
同時,在終端可以看到如下輸出,說明多次請求訪問了不同的服務實例:
ts=2020-07-28T10:11:51.108611Z caller=main.go:96 serviceid=cargo-service64ffdd53-9c66-43cb-9ada-0d48ebddc632
ts=2020-07-28T10:12:00.215364Z caller=main.go:96 serviceid=cargo-servicee8c53e6f-e4ff-4737-a3bd-f1b11b0b2e95
本案例我們使用反向代理技術,並結合註冊中心 Consul 實現了簡單的 API 網關。Go 提供了反向代理工具包,使得整個實現過程變得比較簡單。實際項目中使用的產品,如 Zuul、Nginx 等,還包含了限流、請求過濾、身份認證等功能。該網關雖然僅僅實現了請求的代理,但重點在於幫助你瞭解了網關實現的基本原理,從而爲後續網關功能的擴增打下基礎。
小結
本節我們首先介紹了微服務網關產生的背景及其相關概念,然後還介紹了微服務網關在微服務架構中的職能。作爲服務端的統一入口點,微服務網關主要用來實現接入請求、統一管理、解耦和配置攔截策略等功能。最後,爲便於你更加詳細地瞭解網關組件相關功能的實現原理,我們還自己動手實現了一個 Go 微服務網關,你可以跟着上手實操下。
學完節時,你可以結合自己的實踐經驗,思考下我們實現的簡易網關還需要承擔哪些微服務架構中的職責。
歡迎你關注【雲世】公衆號,在留言區積極發言和討論。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/d-ZPLkYqpb8fI43LLzkm5w