博客 / 詳情

返回

使用 go kit進行微服務開發

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"}

拓展一下

不同的算法

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

  • Token bucket
  • Leaky bucket
  • Fixed window counter
  • Sliding window log
  • Sliding window counter

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

全侷限流

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

自適應限流

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

總之,還是那句話:

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

其他

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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.