雲原生與 Go 微服務實戰 - 一文讀懂微服務網關及其實現原理

在單體架構中,客戶端在向服務端發起請求時,會通過類似 Nginx 的負載均衡組件獲取到多個相同的應用程序實例中的一個。請求由該服務實例進行處理,服務端處理完之後返回響應給客戶端。

而在微服務架構下,原來的單體應用拆分成了多個業務微服務。

此時,直接對外暴露這些業務微服務,必然會存在一些問題。客戶端直接向每個微服務發送請求,其問題主要如下:

如上問題,解決的方案是使用微服務網關。網關在一個 API 架構中的作用是保護、增強和控制外部請求對於 API 服務的訪問。

什麼是微服務網關

在微服務架構中,網關位於接入層之下和業務服務層之上。微服務網關是微服務架構中的一個基礎服務,從面向對象設計的角度看,它與外觀模式類似。

微服務架構圖

微服務網關封裝了系統內部架構,爲每個客戶端提供一個定製的 API,用來保護、增強和控制對於微服務的訪問。換句話來講,微服務網關就是一個處於應用程序或服務之前的系統,用來管理授權、訪問控制和流量限制等,這樣微服務就會被微服務網關保護起來,對所有的調用者透明。因此,隱藏在微服務網關後面的業務系統就可以更加專注於業務本身。

微服務網關的功能特性

作爲連接服務消費方和服務提供方的中間件系統,微服務網關將各自業務系統的演進和發展做了天然的隔離,使業務系統更加專注於業務服務本身,同時微服務網關還可以爲服務提供和沉澱更多附加功能。

微服務網關的主要功能特性如下圖所示:

網關的功能特性示意圖

結合該圖,我們就來具體介紹下這四類功能

實戰案例:自己動手實現一個網關

API 網關最基礎的功能是對請求進行路由轉發,根據配置的轉發規則將請求動態地轉發到指定的服務實例。

動態是指與服務發現結合,如 Consul、ZooKeeper 等組件。

API 網關根據客戶端 HTTP 請求,動態查詢註冊中心的服務實例,通過反向代理實現對後臺服務的調用

API 網關將符合規則的請求路由調用對應的後端服務。這裏的規則可以有很多種,如 HTTP 請求的資源路徑、方法、頭部和參數等。這裏我們以最簡單的請求路徑爲例,規則爲 :/{serviceName}/#。即:路徑第一部分爲註冊中心服務實例名稱,其餘部分爲服務實例的 REST 路徑。如:

/cargo-service/cargos/

/cargo-service/locations

其中:

/cargo-service 爲服務名稱;

/locations 爲 cargo-service 服務提供的接口。

1. 實現思路

客戶端向網關發起請求,網關解析請求資源路徑中的信息,根據服務名稱查詢註冊中心的服務實例;然後使用反向代理技術把客戶端請求轉發至後端真實的服務實例,請求執行完畢後,再把響應信息返回客戶端。

自定義網關的調用請求示意圖

我們設計實現的網關的功能主要包含如下幾點:

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