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 進行包裹,為專案新增各種能力。
動手實踐
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
--> HelloRequest
;encode
負責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 生態中的 Hystrix、resillience4,或者是 Nginx 也有自己的實現。
全侷限流
換個角度,這些ratelimit 都是單個服務的限流,如果要做全侷限流的話,我們可以透過引入集中式的資料儲存。將原本程式記憶體的請求計數器放到外部儲存,所有服務共享一個計數器來實現。比如Redis 限流最佳實踐。
自適應限流
上面的 ratelimit 解決方案都有一個問題:靜態的配置在實際的分散式環境中不好用。在大型的分散式系統中,併發數、系統負載、可用資源都是動態變化的,我們很難得到一個靜態的值來限流,這就需要我們實現一種動態的限流演算法:根據系統的情況,動態調整限流閾值。相應的有aws 限流演算法和netflix限流演算法來實現自適應限流處理。
總之,還是那句話:
系統設計沒有銀彈,還是需要根據實際情況做 trade-off。
其他
這只是一個簡單的示例,微服務開發中還有很多服務發現
、斷路器
、負載均衡
、重試
等等的常規功能。就留待之後再進行擴充吧。