使用 caddy 作為微服務的 API gateway

Muninn發表於2019-02-16

背景

大家都知道,Docker這些年讓IT界產生了深刻的變革,
從開發到測試到運維,處處都有它的身影。
它同時也和微服務架構相互促進,並肩前行。

在最新版的 Docker(CE 17.03) 裡,隨著 swarm mode 的成熟,
在較簡單的場景裡已經可以不再需要專門的基礎設施管理
服務編排服務發現健康檢查負載均衡等等。

但是API gateway還是需要一個的。或許再加上一個日誌收集
你的微服務架構就五臟俱全了。
我們知道Nginx Plus是可以很好的勝任 API gateway 的工作的,
但它是商業軟體。Nginx我們不說認證啊限流啊統計啊之類的功能,
單就請求轉發這一點最基本的就出了問題。

我們知道Docker是用DNS的方式,均衡同一名稱的服務請求到不同的node,
但是Nginx為了速度,在反向代理的時候會有一個不可取消的 DNS Cache,
這樣我們Docker在根據容器的擴充套件或收縮動態的更新DNS,可Nginx卻不為所動,
堅持把請求往固定的IP上發,不說均衡,這個IP甚至可能已經失效了呢。

有一個配置檔案上的小Hack可以實現Nginx每次去查詢DNS,我本來準備寫一篇文章來著,
現在看來不用了,我們找到了更優雅的API gateway, Caddy
我上篇文章也寫了一個它的簡介。

接下來的所有程式碼,都在這個demo中,
你可以clone下來玩,也能在此基礎上做自己的實驗。

應用

我們先用golang寫一個最簡單的HTTP API,你可以用你會的任何語言寫出來,
它為GET請求返回 Hello World 加自己的 hostname .

package main

import (
    "io"
    "log"
    "net/http"
    "os"
)

// HelloServer the web server
func HelloServer(w http.ResponseWriter, req *http.Request) {
    hostname, _ := os.Hostname()
    log.Println(hostname)
    io.WriteString(w, "Hello, world! I am "+hostname+" :)
")
}

func main() {
    http.HandleFunc("/", HelloServer)
    log.Fatal(http.ListenAndServe(":12345", nil))
}

Docker 化

我們需要把上面的應用做成一個docker映象,暴露埠12345
接著才有可能使用Docker Swarm啟動成叢集。
本來做映象特別簡單,但我為了讓大家直接拉映象測試時快一點,用了兩步構建,
先編譯出應用,然後新增到比較小的alpine映象中。大家可以不必在意這些細節。
我們還是先來看看最終的docker-compose.yml編排檔案吧。

version: `3`
services:
    app:
        image: muninn/caddy-microservice:app
        deploy:
            replicas: 3
    gateway:
        image: muninn/caddy-microservice:gateway
        ports:
            - 2015:2015
        depends_on:
            - app
        deploy:
            replicas: 1
            placement:
                constraints: [node.role == manager]

這是最新版本的docker-compose檔案,不再由docker-compose命令啟動,而是要用docker stack deploy命令。
總之現在這個版本在編排方面還沒有完全整合好,有點暈,不過能用。現在我們看到編排中有兩個映象:

  • muninn/caddy-microservice:app 這是我們上一節說的app映象,我們將啟動3個例項,測試上層的負載均衡。

  • muninn/caddy-microservice:gateway 這是我們接下來要講的gateway了,它監聽2015埠並將請求轉發給app。

用 caddy 當作 gateway

為了讓caddy當作gateway,我們主要來看一下Caddyfile:

:2015 {
    proxy / app:12345
}

好吧,它太簡單了。它監聽本機的2015埠,將所有的請求都轉發到 app:12345 。
這個app,其實是一個域名,在docker swarm的網路中,它會被解析到這個名字服務隨機的一個例項。

將來如果有很多app,將不同的請求字首轉發到不同的app就好啦。
所以記得寫規範的時候讓一個app的endpoint字首儘量用一樣的。

然後caddy也需要被容器化,感興趣的可以看看Dockerfile.gateway .

執行服務端

理解了上面的內容,就可以開始執行服務端了。直接用我上傳到雲端的映象就可以。本文用到的三個映象下載時總計26M左右,不大。
clone我背景章節提到的庫進入專案目錄,或者僅僅複製上文提到的compose檔案存成docker-compose.yml,然後執行如下命令。

docker-compose pull
docker stack deploy -c docker-compose.yml caddy

啊,對了,第二個stack命令需要你已經將docker切到了swarm模式,如果沒有會自動出來提示,根據提示切換即可。
如果成功了,我們檢查下狀態:

docker stack ps caddy

如果沒問題,我們能看到已經啟動了3個app和一個gateway。然後我們來測試這個gateway是否能將請求分配到三個後端。

測試

我們是可以通過訪問http://{your-host-ip}:2015來測試服務是不是通的,用瀏覽器或者curl。
然後你會發現,怎麼重新整理內容都不變啊,並沒有像想象中的那樣會訪問到隨機的後端。

不要著急,這個現象並非因為caddy像nginx那樣快取了dns導致均衡失敗,而是另一個原因。
caddy為了反向代理的速度,會和後端保持一個連線池。當只有一個客戶端的時候,用到總是那第一個連線呢。
為了證明這一點,我們需要併發的訪問我們的服務,再看看是否符合我們的預期。

同樣的,測試我也為大家準備了映象,可以直接通過docker使用。

docker run --rm -it muninn/caddy-microservice:client

感興趣的人可以看client資料夾裡的程式碼,它同時發起了30個請求,並且列印出了3個後端被命中的次數。

另外我還做了一個shell版本,只需要sh test.sh就可以,不過只能看輸出拉,沒有自動檢查結果。

好了,現在我們可以知道,caddy可以很好的勝任微服務架構中的 API Gateway 了。

API Gateway

什麼?你說沒看出來這是個 API Gateway 啊。我們前邊只是解決了容器專案中 API Gateway 和DNS式服務發現配合的一個難題,
接下來就簡單了啊,我們寫n個app,每個app是一個微服務,在gateway中把不同的url路由到不同的app就好了啊。

進階

caddy還可以輕鬆的順便把認證中心做了,微服務建議用jwt做認證,將許可權攜帶在token中,caddy稍微配置下就可以。
我後續也會給出教程和demo 。auth2.0我認為並不適合微服務架構,但依然是有個複雜的架構方案的,這個主題改天再說。

caddy還可以做API狀態監控,快取,限流等API gateway的職責,不過這些就要你進行一些開發了。
你還有什麼更多的想法嗎?歡迎留言。

相關文章