Golang中間件【轉】

本章將對現在流行的 web 框架中的中間件技術原理進行分析,並介紹如何使用中間件技術將業務和非業務代碼功能進行解耦。

代碼泥潭

先來看一段代碼:

// middleware/hello.go
package main

func hello(wr http.ResponseWriter, r *http.Request) {
    wr.Write([]byte("hello"))
}

func main() {
    http.HandleFunc("/", hello)
    err := http.ListenAndServe(":8080", nil)
    ...
}

這是一個典型的 web 服務,掛載了一個簡單的路由。我們的線上服務一般也是從這樣簡單的服務開始逐漸拓展開去的。

現在突然來了一個新的需求,我們想要統計之前寫的 hello 服務的處理耗時,需求很簡單,我們對上面的程序進行少量修改:

// middleware/hello_with_time_elapse.go
var logger = log.New(os.Stdout, "", 0)

func hello(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("hello"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
}

這樣便可以在每次接收到 http 請求時,打印出當前請求所消耗的時間。

完成了這個需求之後,我們繼續進行業務開發,提供的 api 逐漸增加,現在我們的路由看起來是這個樣子:

// middleware/hello_with_more_routes.go
// 省略了一些相同的代碼
package main

func helloHandler(wr http.ResponseWriter, r *http.Request) {
    ...
}

func showInfoHandler(wr http.ResponseWriter, r *http.Request) {
    ...
}

func showEmailHandler(wr http.ResponseWriter, r *http.Request) {
    ...
}

func showFriendsHandler(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("your friends is tom and alex"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
}

func main() {
    http.HandleFunc("/", helloHandler)
    http.HandleFunc("/info/show", showInfoHandler)
    http.HandleFunc("/email/show", showEmailHandler)
    http.HandleFunc("/friends/show", showFriendsHandler)
    ...
}

每一個 handler 裏都有之前提到的記錄運行時間的代碼,每次增加新的路由我們也同樣需要把這些看起來長得差不多的代碼拷貝到我們需要的地方去。因爲代碼不太多,所以實施起來也沒有遇到什麼大問題。

漸漸的我們的系統增加到了 30 個路由和 handler 函數,每次增加新的 handler,我們的第一件工作就是把之前寫的所有和業務邏輯無關的周邊代碼先拷貝過來。

接下來系統安穩地運行了一段時間,突然有一天,老闆找到你,我們最近找人新開發了監控系統,爲了系統運行可以更加可控,需要把每個接口運行的耗時數據主動上報到我們的監控系統裏。給監控系統起個名字吧,叫 metrics。現在你需要修改代碼並把耗時通過 http post 的方式發給 metrics 了。我們來修改一下 helloHandler:

func helloHandler(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("hello"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
    // 新增耗時上報
    metrics.Upload("timeHandler", timeElapsed)
}

修改到這裏,本能地發現我們的開發工作開始陷入了泥潭。無論未來對我們的這個 web 系統有任何其它的非功能或統計需求,我們的修改必然牽一髮而動全身。只要增加一個非常簡單的非業務統計,我們就需要去幾十個 handler 裏增加這些業務無關的代碼。雖然一開始我們似乎並沒有做錯,但是顯然隨着業務的發展,我們的行事方式讓我們陷入了代碼的泥潭。

使用 middleware 剝離非業務邏輯

我們來分析一下,一開始在哪裏做錯了呢?我們只是一步一步地滿足需求,把我們需要的邏輯按照流程寫下去呀?

實際上,我們犯的最大的錯誤是把業務代碼和非業務代碼揉在了一起。對於大多數的場景來講,非業務的需求都是在 http 請求處理前做一些事情,或者/並且在響應完成之後做一些事情。我們有沒有辦法使用一些重構思路把這些公共的非業務功能代碼剝離出去呢?回到剛開頭的例子,我們需要給我們的 helloHandler 增加超時時間統計,我們可以使用一種叫 function adapter 的方法來對 helloHandler 進行包裝:


func hello(wr http.ResponseWriter, r *http.Request) {
    wr.Write([]byte("hello"))
}

func timeMiddleware(next http.Handler) http.Handler {
    return func(wr http.ResponseWriter, r *http.Request) {
        timeStart := time.Now()

        // next handler
        next.ServeHTTP(wr, r)

        timeElapsed := time.Since(timeStart)
        logger.Println(timeElapsed)
    }
}

func main() {
    http.HandleFunc("/", timeMiddleware(hello))
    err := http.ListenAndServe(":8080", nil)
    ...
}

這樣就非常輕鬆地實現了業務與非業務之間的剝離,魔法就在於這個 timeMiddleware。可以從代碼中看到,我們的 timeMiddleware 也是一個函數,其參數爲 http.Handler,http.Handler 的定義在 net/http 包中:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

任何方法實現了 ServeHTTP,即是一個合法的 http.Handler,讀到這裏你可能會有一些混亂,我們先來梳理一下 http 庫的 Handler,HandlerFunc 和 ServeHTTP 的關係:

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
     f(w, r)
}

實際上只要你的 handler 函數簽名是:

func (ResponseWriter, *Request)

那麼這個 handler 就是一個 HandlerFunc 類型了,也就相當於實現了 http.Handler 這個接口,這要歸功於 golang 的 duck typing 式的類型系統。在 http 庫需要調用你的 handler 函數來處理 http 請求時,會調用 HandlerFunc 的 ServeHTTP 函數,可見一個請求的基本調用鏈是這樣的:

h = getHandler() => h.ServeHTTP(w, r) => h(w, r)

知道 handler 是怎麼一回事,我們的中間件通過包裝 handler,再返回一個新的 handler 就好理解了。

總結一下,我們的中間件要做的事情就是通過一個或多個函數對 handler 進行包裝,返回一個包括了各個中間件邏輯的函數鏈。我們把上面的包裝再做得複雜一些:

customizedHandler = logger(timeout(ratelimit(helloHandler)))

這個函數鏈在執行過程中的上下文可以用下面這張圖來表示。

再直白一些,這個流程在進行請求處理的時候實際上就是不斷地進行函數壓棧再出棧,有一些類似於遞歸的執行流:

[exec of logger logic]           函數棧: []

[exec of timeout logic]          函數棧: [logger]

[exec of ratelimit logic]        函數棧: [timeout/logger]

[exec of helloHandler logic]     函數棧: [ratelimit/timeout/logger]

[exec of ratelimit logic part2]  函數棧: [timeout/logger]

[exec of timeout logic part2]    函數棧: [logger]

[exec of logger logic part2]     函數棧: []

功能實現了,但在上面的使用過程中我們也看到了,這種函數套函數的用法不是很美觀,同時也不具備什麼可讀性。

更優雅的 middleware 寫法

上一節中解決了業務功能代碼和非業務功能代碼的解耦,但也提到了,看起來並不美觀,如果需要修改這些函數的順序,或者增刪 middleware 還是有點費勁,本節我們來進行一些“寫法”上的優化。

看一個例子:

r = NewRouter()
r.Use(logger)
r.Use(timeout)
r.Use(ratelimit)
r.Add("/", helloHandler)

通過多步設置,我們擁有了和上一節差不多的執行函數鏈。勝在直觀易懂,如果我們要增加或者刪除 middleware,只要簡單地增加刪除對應的 Use 調用就可以了。非常方便。

從框架的角度來講,怎麼實現這樣的功能呢?也不復雜:

type middleware func(http.Handler) http.Handler

type Router struct {
    middlewareChain [] func(http.Handler) http.Handler
    mux map[string] http.Handler
}

func NewRouter() *Router{
    return &Router{}
}

func (r *Router) Use(m middleware) {
    r.middlewareChain = append(r.middlewareChain, m)
}

func (r *Router) Add(route string, h http.Handler) {
    var mergedHandler = h

    for i := len(r.middlewareChain) - 1; i >= 0; i-- {
        mergedHandler = r.middlewareChain[i](mergedHandler)
    }

    r.mux[route] = mergedHandler
}

注意代碼中的 middleware 數組遍歷順序,和用戶希望的調用順序應該是"相反"的。應該不難理解。

哪些事情適合在 middleware 中做

以較流行的開源 golang 框架 chi 爲例:

compress.go
  => 對 http 的 response body 進行壓縮處理
heartbeat.go
  => 設置一個特殊的路由,例如 /ping,/healthcheck,用來給 load balancer 一類的前置服務進行探活
logger.go
  => 打印 request 處理日誌,例如請求處理時間,請求路由
profiler.go
  => 掛載 pprof 需要的路由,如 /pprof、/pprof/trace 到系統中
realip.go
  => 從請求頭中讀取 X-Forwarded-For 和 X-Real-IP,將 http.Request 中的 RemoteAddr 修改爲得到的 RealIP 
requestid.go
  => 爲本次請求生成單獨的 requestid,可一路透傳,用來生成分佈式調用鏈路,也可用於在日誌中串連單次請求的所有邏輯
timeout.go
  => 用 context.Timeout 設置超時時間,並將其通過 http.Request 一路透傳下去
throttler.go
  => 通過定長大小的 channel 存儲 token,並通過這些 token 對接口進行限流

每一個 web 框架都會有對應的 middleware 組件,如果你有興趣,也可以向這些項目貢獻有用的 middleware,只要合理一般項目的維護人也願意合併你的 pull request。

比如開源界很火的 gin 這個框架,就專門爲用戶貢獻的 middleware 開了一個倉庫:

如果讀者去閱讀 gin 的源碼的話,可能會發現 gin 的 middleware 中處理的並不是 http.Handler,而是一個叫 gin.HandlerFunc 的函數類型,和本節中講解的 http.Handler 簽名並不一樣。不過實際上 gin 的 handler 也只是針對其框架的一種封裝,middleware 的原理與本節中的說明是一致的。

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