使用 go kit進行微服務開發

stillfox發表於2023-02-08

go-kit的基本介紹

go-kit 介紹

go-kit 是一個 Golang 編寫的開發框架,可以幫助開發者更快捷地構建可伸縮的微服務架構。它提供了一系列模組化的元件,可以幫助開發者更輕鬆地構建和維護微服務。go-kit的設計理念是可組合的,它可以與各種服務發現系統進行整合,如etcd、consul和zookeeper等,並且可以輕鬆實現服務熔斷和負載均衡。

另外,go-kit也提供了諸如監控、日誌和鏈路追蹤的功能,可以幫助開發者更好地理解和控制微服務架構。

go-kit 還提供了指標收集和分析功能,可以幫助開發者進行效能最佳化和故障診斷。它還允許使用者使用自定義的協議,比如REST、gRPC和GraphQL等,來實現不同服務之間的通訊。

設計哲學

go-kit 是一個符合 KISS 原則的框架,透過使用關注點分離,讓開發者優先集中於業務邏輯的開發。在業務邏輯完成之後,再透過組合快速接入微服務的各種能力。

go-kit 主要可以劃分為:

  • Service Layer —— 專注於業務邏輯,處理 request,返回 response。
  • Endpoint Layer —— 是 Service 的入口,對 Service 進行 wrapper,可以附加各種 rate-limit metrics 的 middleware,從而增強 Service。
  • Transport Layer —— 定義客戶端和服務端應該如何通訊,負責網路協議轉換等,例如 gRPC、HTTP 等協議的處理。

在 go-kit 中,整個專案就像是一個洋蔥,最核心是 Service,也就是業務邏輯。然後透過一層層middleware 進行包裹,為專案新增各種能力。

https://gokit.io/faq/onion.png

動手實踐

Service

既然是業務優先,那麼開發的順序自然是應該從 Service 業務邏輯開始。

讓我們從一個簡單的使用者服務開始吧!假設我們需要實現一個user-service,它需要處理使用者的註冊、登入的邏輯。基於面向介面程式設計的原則,我們可以設計一個Service如下:

type HelloRequest struct {
    Name string `json:"name"`
}

type HelloResponse struct {
    Message string `json:"message"`
}

type HelloService interface {
    Hello(ctx context.Context, name string) (HelloResponse, error)
}

type helloService struct{}

func (s *helloService) Hello(ctx context.Context, name string) (HelloResponse, error) {
    return HelloResponse{Message: "Hello, " + name}, nil
}

Endpoint

寫完業務邏輯之後,我們需要對外提供這個介面,可以用Endpoint來包裹這個Service。在 go-kit 中,Endpoint 就是一個interface

type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)

它主要負責的是:接收外部的 request,交給 Service 處理之後,返回對應的 response。

那麼,我們可以這樣實現 HelloService 的 Endpoint:

func MakeHelloEndpoint(svc HelloService) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(HelloRequest)
        res, err := svc.Hello(ctx, req.Name)        // 呼叫實際的 Service 執行業務邏輯
        if err != nil {
            return nil, err
        }
        return res, nil
    }
}

Transport

最後,就是需要把這個服務暴露出來,對外提供服務了。在 go-kit 中,這也就是 Transport 需要做的事情,Transport 具體怎麼寫,取決於專案實際的網路方案。如果是 http,那麼 Transport 就需要將 http 請求資料轉換為 Service的請求引數。我們使用 http 做一個示例:

func decodeHelloRequest(_ context.Context, r *http.Request) (interface{}, error) {
    return HelloRequest{Name: r.FormValue("name")}, nil
}

func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
    return json.NewEncoder(w).Encode(response)
}

其中decode負責 http.Request --> HelloRequestencode 負責HelloResponse --> http.Response

Server

最後,將所有的元件裝配起來,用一個 http server 來啟動服務就好了:

func main() {
    svc := &helloService{}
    ep := MakeHelloEndpoint(svc)

    route := mux.NewRouter()
  // go-kit的 http 協議處理
    route.Methods("Get").Path("/hello").Handler(kithttp.NewServer(
        ep,
        decodeHelloRequest,
        encodeResponse,
    ))

    log.Fatal(http.ListenAndServe(":8080", route))
}

執行一下就可以看到結果:

❯ curl "http://localhost:8080/hello?name=j"
{"message":"Hello, j"}

其中最核心的一塊就是執行kithttp.NewServer()這個函式,它會接受 endpoint、decode、encode幾個引數。我們可以分別再看看這幾個引數的作用:

  • endpoint —— 接受 request,呼叫 Service,返回 response
  • decode —— 將網路協議資料轉換成 request
  • encode —— 將 response 轉換成網路協議資料返回

也許,再看看 go-kit的原始碼會更加有助於理解整個鏈路是怎麼樣的。在 go-kit中,NewServer建立的物件最核心的邏輯就是:

func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    request, err := s.dec(ctx, r)
    if err != nil {
        // error handler
        return
    }

    response, err := s.e(ctx, request)
    if err != nil {
        // error handler
        return
    }
  
  if err := s.enc(ctx, w, response); err != nil {
        // error handler
        return
    }
}

完成的示例可以從 GitHub 檢視。

引入微服務的能力

為什麼需要流量控制

在微服務架構中,服務之間是透過網路呼叫來實現協作的。如果某個服務的負載高,其它服務請求這個服務時就會等待。這樣會導致整個系統的瓶頸,影響整個系統的吞吐量和穩定性。因此,對服務進行流量控制是很有必要的。而 ratelimit 就是其中一種流量控制的實現方法。它可以限制一個服務在一段時間內能夠接受的請求數量,從而避免一個服務的高負載導致整個系統的故障。

如何實現 ratelmit

在 go-kit 中,可以很方便地實現一個簡單的 ratelimit。

在如果熟悉 OOP 的話,應該會聽過裝飾器模式。在 go-kit 中,就是使用了這個思想,用 Endpoint 包裹 Endpoint,從而新增各種不同的能力。例如,在我們的例子中,想要給微服務新增一個ratelimit能力的話,就可以這樣建立一個裝飾器:

type limitMiddleware struct {
    timer time.Duration
    burst int
}

func (l limitMiddleware) wrap(e endpoint.Endpoint) endpoint.Endpoint {
    e = ratelimit.NewErroringLimiter(rate.NewLimiter(rate.Every(l.timer), l.burst))(e)
    return e
}

limitMiddwware是一個限速器,timer 是一個時間週期,burst 是最大併發請求數量。wrap函式就是我們的裝飾器,接受一個 Endpoint,返回一個 Endpoint,它就可以為 Endpoint 新增 ratelimit 的功能。

相應地,我們的 main程式就可以這樣使用這個裝飾器:

func main() {
    svc := &helloService{}
    ep := MakeHelloEndpoint(svc)

    // decorate ratelimit
    ratelimit := limitMiddleware{
        timer: 5 * time.Second,
        burst: 3,
    }
    ep = ratelimit.wrap(ep)
  
    route := mux.NewRouter()
    route.Methods("Get").Path("/hello").Handler(kithttp.NewServer(
        ep,
        decodeHelloRequest,
        encodeResponse,
    ))

    log.Fatal(http.ListenAndServe(":8080", route))
}

上面的例子就為這個服務建立了一個 ratelimit,如果在 5 秒鐘內請求數超過 3 個的話,這個 ratelimit 就會拒絕請求。我們可以看看效果:

❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:27 CST 2023
{"message":"Hello, j"}
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:27 CST 2023
{"message":"Hello, j"}
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:28 CST 2023
{"message":"Hello, j"}
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:29 CST 2023
rate limit exceeded%                        # 觸發了 ratelimit                                                                           
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:30 CST 2023
rate limit exceeded%                                                                                                   
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:33 CST 2023                # 恢復響應請求
{"message":"Hello, j"}

擴充一下

不同的演算法

上面用到的限速器是基於令牌桶演算法實現的,類似的還有很多其他的演算法實現:

還有開源軟體也有各自的實現,比如 Java 生態中的 Hystrixresillience4,或者是 Nginx 也有自己的實現。

全侷限流

換個角度,這些ratelimit 都是單個服務的限流,如果要做全侷限流的話,我們可以透過引入集中式的資料儲存。將原本程式記憶體的請求計數器放到外部儲存,所有服務共享一個計數器來實現。比如Redis 限流最佳實踐

自適應限流

上面的 ratelimit 解決方案都有一個問題:靜態的配置在實際的分散式環境中不好用。在大型的分散式系統中,併發數、系統負載、可用資源都是動態變化的,我們很難得到一個靜態的值來限流,這就需要我們實現一種動態的限流演算法:根據系統的情況,動態調整限流閾值。相應的有aws 限流演算法netflix限流演算法來實現自適應限流處理。

總之,還是那句話:

系統設計沒有銀彈,還是需要根據實際情況做 trade-off。

其他

這只是一個簡單的示例,微服務開發中還有很多服務發現斷路器負載均衡重試等等的常規功能。就留待之後再進行擴充吧。

相關文章