使用 caddy 作為微服務的 API gateway

背景

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

在最新版的Docker里,隨著 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 mainnnimport (nt"io"nt"log"nt"net/http"nt"os"n)nn// HelloServer the web servernfunc HelloServer(w http.ResponseWriter, req *http.Request) {nthostname, _ := os.Hostname()ntlog.Println(hostname)ntio.WriteString(w, "Hello, world! I am "+hostname+" :)n")n}nnfunc main() {nthttp.HandleFunc("/", HelloServer)ntlog.Fatal(http.ListenAndServe(":12345", nil))n}n

Docker 化

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

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

這是最新版本的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 {n proxy / app:12345n}n

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

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

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

運行服務端

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

docker-compose pullndocker stack deploy -c docker-compose.yml caddyn

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

docker stack ps caddyn

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

測試

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

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

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

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

感興趣的人可以看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的職責,不過這些就要你進行一些開發了。 你還有什麼更多的想法嗎?歡迎留言。


推薦閱讀:

是時候想想該怎麼刪代碼了
一次生產事故的優化經歷
英文版本的「弱者道之用」
詳解Serverless服務,它會顛覆你對雲的理解 | 硬創公開課
架構設計的0層邏輯

TAG:Go语言 | Docker | 软件架构 |