Go kit 文檔
首要原則
創(chuàng)建一個(gè)小型Go kit 服務(wù)
你的業(yè)務(wù)邏輯
你的服務(wù)起始于業(yè)務(wù)邏輯.在Go kit 中,我們讓一個(gè)接口作為一個(gè)服務(wù).
// StringService provides operations on strings.
type StringService interface {
Uppercase(string) (string, error)
Count(string) int
}
該接口將會被實(shí)現(xiàn)
type stringService struct{}
func (stringService) Uppercase(s string) (string, error) {
if s == "" {
return "", ErrEmpty
}
return strings.ToUpper(s), nil
}
func (stringService) Count(s string) int {
return len(s)
}
// ErrEmpty is returned when input string is empty
var ErrEmpty = errors.New("Empty string")
請求和響應(yīng)
在Go kit中,主要的通信方式是PRC.所以,你接口中的每個(gè)方法都會被遠(yuǎn)程過程調(diào)用.對于每個(gè)方法,我們都定義了請求和響應(yīng)結(jié)構(gòu)體,來分別捕獲所有的入?yún)⒑铣鰠?
type uppercaseRequest struct {
S string `json:"s"`
}
type uppercaseResponse struct {
V string `json:"v"`
Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}
type countRequest struct {
S string `json:"s"`
}
type countResponse struct {
V int `json:"v"`
}
端點(diǎn)
Go kit 通過抽象出一個(gè)端點(diǎn)提供了大部分的功能
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)
一個(gè)端點(diǎn)對應(yīng)一個(gè)PRC.就是我們服務(wù)接口中的一個(gè)方法.我們將會寫簡單的適配器去把我們服務(wù)中的方法轉(zhuǎn)換成一個(gè)端點(diǎn).每個(gè)適配器拿到一個(gè) StringService,
同時(shí)返回一個(gè)方法中對應(yīng)的端點(diǎn).
import (
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
)
func makeUppercaseEndpoint(svc StringService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(uppercaseRequest)
v, err := svc.Uppercase(req.S)
if err != nil {
return uppercaseResponse{v, err.Error()}, nil
}
return uppercaseResponse{v, ""}, nil
}
}
func makeCountEndpoint(svc StringService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(countRequest)
v := svc.Count(req.S)
return countResponse{v}, nil
}
}
傳輸
現(xiàn)在我們需要將你的服務(wù)暴露給外界調(diào)用,所以可以調(diào)用它.你的組織可能對服務(wù)如何交流有所了解了.也許你使用Thrift,或者通過HTTP自定義JSON,Go kit許多傳輸開箱即用.
對于小型服務(wù),使用HTTP/JSON.Go kit 在transport/http提供了一個(gè) helper 結(jié)構(gòu)體.
import (
"encoding/json"
"log"
"net/http"
"golang.org/x/net/context"
httptransport "github.com/go-kit/kit/transport/http"
)
func main() {
svc := stringService{}
uppercaseHandler := httptransport.NewServer(
makeUppercaseEndpoint(svc),
decodeUppercaseRequest,
encodeResponse,
)
countHandler := httptransport.NewServer(
makeCountEndpoint(svc),
decodeCountRequest,
encodeResponse,
)
http.Handle("/uppercase", uppercaseHandler)
http.Handle("/count", countHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func decodeUppercaseRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request uppercaseRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
func decodeCountRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request countRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
return json.NewEncoder(w).Encode(response)
}
stringsvc1
以上完整的服務(wù) stringsvc1
$ go get github.com/go-kit/kit/examples/stringsvc1
$ stringsvc1
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/uppercase
{"v":"HELLO, WORLD","err":null}
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/count
{"v":12}
中間件
沒有日志和儀表盤的服務(wù)是不能用于生產(chǎn)環(huán)境的
傳輸日志
需要記錄的任何組件都應(yīng)該像記錄器那樣像一個(gè)依賴關(guān)系,與數(shù)據(jù)庫連接相同.因此.我們在我們的func man中構(gòu)建我們的記錄器,并將其傳遞給需要它的組件.我們從不使用全局范圍的記錄器.
我們可以直接將記錄器傳遞給我們的stringService實(shí)現(xiàn),但是有一個(gè)更好的方法。我們來使用一個(gè)中間件,也稱為裝飾器。中間件是一個(gè)接收端點(diǎn)并返回端點(diǎn)的函數(shù)。
在中間件里,可以做任何事,讓我創(chuàng)建一個(gè)基本的記錄器中間件.
func loggingMiddleware(logger log.Logger) Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
logger.Log("msg", "calling endpoint")
defer logger.Log("msg", "called endpoint")
return next(ctx, request)
}
}
}
在你的每個(gè)Handler配置
logger := log.NewLogfmtLogger(os.Stderr)
svc := stringService{}
var uppercase endpoint.Endpoint
uppercase = makeUppercaseEndpoint(svc)
uppercase = loggingMiddleware(log.NewContext(logger).With("method", "uppercase"))(uppercase)
var count endpoint.Endpoint
count = makeCountEndpoint(svc)
count = loggingMiddleware(log.NewContext(logger).With("method", "count"))(count)
uppercaseHandler := httptransport.Server(
// ...
uppercase,
// ...
)
countHandler := httptransport.Server(
// ...
count,
// ...
)
事實(shí)證明,這種技術(shù)比僅僅打印日志有用的多.許多Go kit 組件都是一個(gè)端點(diǎn)的中間件.
應(yīng)用日志
假如我們想在全局打印日志,是要傳遞參數(shù)給作用域嗎?應(yīng)該給我們的服務(wù)定義一個(gè)中間件,配置化得同時(shí)可以達(dá)到相同的效果.由于你的服務(wù)定義了一個(gè)接口,我們僅僅需要定義一個(gè)類型包裹這個(gè)服務(wù),執(zhí)行額外的打印日志的功能.
type loggingMiddleware struct {
logger log.Logger
next StringService
}
func (mw loggingMiddleware) Uppercase(s string) (output string, err error) {
defer func(begin time.Time) {
mw.logger.Log(
"method", "uppercase",
"input", s,
"output", output,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
output, err = mw.next.Uppercase(s)
return
}
func (mw loggingMiddleware) Count(s string) (n int) {
defer func(begin time.Time) {
mw.logger.Log(
"method", "count",
"input", s,
"n", n,
"took", time.Since(begin),
)
}(time.Now())
n = mw.next.Count(s)
return
}
同時(shí)在這里加上
import (
"os"
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
)
func main() {
logger := log.NewLogfmtLogger(os.Stderr)
var svc StringService
svc = stringsvc{}
svc = loggingMiddleware{logger, svc}
// ...
uppercaseHandler := httptransport.NewServer(
// ...
makeUppercaseEndpoint(svc),
// ...
)
countHandler := httptransport.NewServer(
// ...
makeCountEndpoint(svc),
// ...
)
}
端點(diǎn)的中間件關(guān)注傳輸層,例如線路中斷和請求限制.服務(wù)的中間件關(guān)注業(yè)務(wù)層,例如日志打印儀表盤.話說儀表盤是什么…
應(yīng)用儀表盤
在 Go kit 中,儀表盤是 用package 記錄你服務(wù)運(yùn)行時(shí)行為的統(tǒng)計(jì).工作進(jìn)程數(shù),
請求耗時(shí),執(zhí)行邏輯數(shù)都會被認(rèn)為是儀表盤.
我們使用上面日志記錄相同的中間件模式
type instrumentingMiddleware struct {
requestCount metrics.Counter
requestLatency metrics.TimeHistogram
countResult metrics.Histogram
next StringService
}
func (mw instrumentingMiddleware) Uppercase(s string) (output string, err error) {
defer func(begin time.Time) {
methodField := metrics.Field{Key: "method", Value: "uppercase"}
errorField := metrics.Field{Key: "error", Value: fmt.Sprintf("%v", err)}
mw.requestCount.With(methodField).With(errorField).Add(1)
mw.requestLatency.With(methodField).With(errorField).Observe(time.Since(begin))
}(time.Now())
output, err = mw.next.Uppercase(s)
return
}
func (mw instrumentingMiddleware) Count(s string) (n int) {
defer func(begin time.Time) {
methodField := metrics.Field{Key: "method", Value: "count"}
errorField := metrics.Field{Key: "error", Value: fmt.Sprintf("%v", error(nil))}
mw.requestCount.With(methodField).With(errorField).Add(1)
mw.requestLatency.With(methodField).With(errorField).Observe(time.Since(begin))
mw.countResult.Observe(int64(n))
}(time.Now())
n = mw.next.Count(s)
return
}
把它加到我們的服務(wù)中
import (
stdprometheus "github.com/prometheus/client_golang/prometheus"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/go-kit/kit/metrics"
)
func main() {
logger := log.NewLogfmtLogger(os.Stderr)
fieldKeys := []string{"method", "error"}
requestCount := kitprometheus.NewCounter(stdprometheus.CounterOpts{
// ...
}, fieldKeys)
requestLatency := metrics.NewTimeHistogram(time.Microsecond, kitprometheus.NewSummary(stdprometheus.SummaryOpts{
// ...
}, fieldKeys))
countResult := kitprometheus.NewSummary(stdprometheus.SummaryOpts{
// ...
}, []string{}))
var svc StringService
svc = stringService{}
svc = loggingMiddleware{logger, svc}
svc = instrumentingMiddleware{requestCount, requestLatency, countResult, svc}
// ...
http.Handle("/metrics", stdprometheus.Handler())
}
stringsvc2
以上服務(wù)完整的例子 stringsvc2
$ go get github.com/go-kit/kit/examples/stringsvc2
$ stringsvc2
msg=HTTP addr=:8080
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/uppercase
{"v":"HELLO, WORLD","err":null}
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/count
{"v":12}
method=uppercase input="hello, world" output="HELLO, WORLD" err=null took=2.455μs
method=count input="hello, world" n=12 took=743ns
調(diào)用其他服務(wù)
在真空中存在著很少的服務(wù).通常.您需要調(diào)用其他服務(wù)。這是Go Kit厲害之處,我們提供傳輸層來解決這些問題.
假設(shè)我們要讓我們的字符串服務(wù)調(diào)用不同的字符串服務(wù)來滿足大寫方法。 實(shí)際上,將請求代理到另一個(gè)服務(wù)。 我們將代理中間件實(shí)現(xiàn)ServiceMiddleware,與日志記錄或儀表盤中間件相同。
func (mw proxymw) Uppercase(s string) (string, error) {
response, err := mw.uppercase(mw.Context, uppercaseRequest{S: s})
if err != nil {
return "", err
}
resp := response.(uppercaseResponse)
if resp.Err != "" {
return resp.V, errors.New(resp.Err)
}
return resp.V, nil
}
客戶端端點(diǎn)
我們已經(jīng)有了我們了解的完全相同的端點(diǎn),但是我們現(xiàn)在要調(diào)用它,而不是作為一個(gè)服務(wù)的請求,這樣的使用方式,我們稱之為一個(gè)客戶端端點(diǎn).調(diào)用客戶端端點(diǎn),我僅僅需要做一些轉(zhuǎn)換.
func (mw proxymw) Uppercase(s string) (string, error) {
response, err := mw.uppercase(mw.Context, uppercaseRequest{S: s})
if err != nil {
return "", err
}
resp := response.(uppercaseResponse)
if resp.Err != "" {
return resp.V, errors.New(resp.Err)
}
return resp.V, nil
}
現(xiàn)在,我們構(gòu)造其中一個(gè)代理的中間件,我們代理一個(gè)URL字符串到一個(gè)端點(diǎn).假定我們使用HTTP/JSON格式,我們需要用到 一個(gè)來自transport/http里helper.
import (
httptransport "github.com/go-kit/kit/transport/http"
)
func proxyingMiddleware(proxyURL string, ctx context.Context) ServiceMiddleware {
return func(next StringService) StringService {
return proxymw{ctx, next, makeUppercaseEndpoint(ctx, proxyURL)}
}
}
func makeUppercaseEndpoint(ctx context.Context, proxyURL string) endpoint.Endpoint {
return httptransport.NewClient(
"GET",
mustParseURL(proxyURL),
encodeUppercaseRequest,
decodeUppercaseResponse,
).Endpoint()
}
服務(wù)發(fā)現(xiàn)和負(fù)載均衡
假如我們只有一臺遠(yuǎn)程服務(wù)器很好辦.但是實(shí)際上,我們有多臺服務(wù)器實(shí)例在運(yùn)行,我們想通過某種服務(wù)器發(fā)現(xiàn)機(jī)制去發(fā)現(xiàn)這些服務(wù)器.然后把發(fā)現(xiàn)的服務(wù)廣播到其他服務(wù)上.假如其中任何一臺服務(wù)不可用了,也不會影響到服務(wù)的可用性.
Go kit 提供了可以發(fā)現(xiàn)不同服務(wù)系統(tǒng)的適配器,以獲取作為單個(gè)端點(diǎn)公開的最新實(shí)例集。這些適配器稱為訂閱.
type Subscriber interface {
Endpoints() ([]endpoint.Endpoint, error)
}
在訂閱內(nèi)部,訂閱會用提供的函數(shù)工廠把每一個(gè)被發(fā)現(xiàn)的實(shí)例(類型:host:port)轉(zhuǎn)化成一個(gè)端點(diǎn).
type Factory func(instance string) (endpoint.Endpoint, error)
到目前為止,我們的函數(shù)工廠 makeUppercaseEndpoint 是直接訪問URL.一些關(guān)于訪問安全的中間件,比如 熔斷器和請求限制也應(yīng)該加到你的工廠里去.
var e endpoint.Endpoint
e = makeUppercaseProxy(ctx, instance)
e = circuitbreaker.Gobreaker(gobreaker.NewCircuitBreaker(gobreaker.Settings{}))(e)
e = kitratelimit.NewTokenBucketLimiter(jujuratelimit.NewBucketWithRate(float64(maxQPS), int64(maxQPS)))(e)
}
現(xiàn)在我們已經(jīng)設(shè)置了一些端點(diǎn),我需要從中選擇一個(gè).我們要從端點(diǎn)中選擇一個(gè)使用負(fù)載均衡封裝訂閱.Go kit 提供了基本的負(fù)載均衡器,你也可以很容易地優(yōu)化它.
type Balancer interface {
Endpoint() (endpoint.Endpoint, error)
}
現(xiàn)在我們有能力編寫自定義的端點(diǎn).我們可以使用它為消費(fèi)者提供一個(gè)單一的,合乎邏輯的,穩(wěn)健的端點(diǎn).一個(gè)重發(fā)機(jī)制封裝均衡負(fù)載器,返回一個(gè)可用的端點(diǎn).
這個(gè)重發(fā)機(jī)制會重新發(fā)送失敗的請求直到超過最大請求數(shù)或者超時(shí).
func Retry(max int, timeout time.Duration, lb Balancer) endpoint.Endpoint
我們來連接我們的最終代理中間件,為了簡單起見,我們假設(shè)用戶將使用一個(gè)標(biāo)志指定多個(gè)逗號分隔的實(shí)例端點(diǎn).
func proxyingMiddleware(instances string, ctx context.Context, logger log.Logger) ServiceMiddleware {
// If instances is empty, don't proxy.
if instances == "" {
logger.Log("proxy_to", "none")
return func(next StringService) StringService { return next }
}
// Set some parameters for our client.
var (
qps = 100 // beyond which we will return an error
maxAttempts = 3 // per request, before giving up
maxTime = 250 * time.Millisecond // wallclock time, before giving up
)
// Otherwise, construct an endpoint for each instance in the list, and add
// it to a fixed set of endpoints. In a real service, rather than doing this
// by hand, you'd probably use package sd's support for your service
// discovery system.
var (
instanceList = split(instances)
subscriber sd.FixedSubscriber
)
logger.Log("proxy_to", fmt.Sprint(instanceList))
for _, instance := range instanceList {
var e endpoint.Endpoint
e = makeUppercaseProxy(ctx, instance)
e = circuitbreaker.Gobreaker(gobreaker.NewCircuitBreaker(gobreaker.Settings{}))(e)
e = kitratelimit.NewTokenBucketLimiter(jujuratelimit.NewBucketWithRate(float64(qps), int64(qps)))(e)
subscriber = append(subscriber, e)
}
// Now, build a single, retrying, load-balancing endpoint out of all of
// those individual endpoints.
balancer := lb.NewRoundRobin(subscriber)
retry := lb.Retry(maxAttempts, maxTime, balancer)
// And finally, return the ServiceMiddleware, implemented by proxymw.
return func(next StringService) StringService {
return proxymw{ctx, next, retry}
}
}
stringsvc3
目前完整的服務(wù) stringsvc3.
$ go get github.com/go-kit/kit/examples/stringsvc3
$ stringsvc3 -listen=:8001 &
listen=:8001 caller=proxying.go:25 proxy_to=none
listen=:8001 caller=main.go:72 msg=HTTP addr=:8001
$ stringsvc3 -listen=:8002 &
listen=:8002 caller=proxying.go:25 proxy_to=none
listen=:8002 caller=main.go:72 msg=HTTP addr=:8002
$ stringsvc3 -listen=:8003 &
listen=:8003 caller=proxying.go:25 proxy_to=none
listen=:8003 caller=main.go:72 msg=HTTP addr=:8003
$ stringsvc3 -listen=:8080 -proxy=localhost:8001,localhost:8002,localhost:8003
listen=:8080 caller=proxying.go:29 proxy_to="[localhost:8001 localhost:8002 localhost:8003]"
listen=:8080 caller=main.go:72 msg=HTTP addr=:8080
$ for s in foo bar baz ; do curl -d"{\"s\":\"$s\"}" localhost:8080/uppercase ; done
{"v":"FOO","err":null}
{"v":"BAR","err":null}
{"v":"BAZ","err":null}
listen=:8001 caller=logging.go:28 method=uppercase input=foo output=FOO err=null took=5.168μs
listen=:8080 caller=logging.go:28 method=uppercase input=foo output=FOO err=null took=4.39012ms
listen=:8002 caller=logging.go:28 method=uppercase input=bar output=BAR err=null took=5.445μs
listen=:8080 caller=logging.go:28 method=uppercase input=bar output=BAR err=null took=2.04831ms
listen=:8003 caller=logging.go:28 method=uppercase input=baz output=BAZ err=null took=3.285μs
listen=:8080 caller=logging.go:28 method=uppercase input=baz output=BAZ err=null took=1.388155ms
優(yōu)化建議
上下文的使用
上下文對象用于在單個(gè)請求的作用域內(nèi)跨邊界攜帶信息.在我們的例子中.我們還沒有通過我們的業(yè)務(wù)邏輯來描述上下文.但這幾乎總是一個(gè)好主意.它允許您在業(yè)務(wù)邏輯和中間件之間傳遞請求作用域的信息.并且對于更復(fù)雜的任務(wù)(如粒度分布式跟蹤注釋)是必需的.
直觀地,這就意味著我們的業(yè)務(wù)邏輯接口看起來像這樣
type MyService interface {
Foo(context.Context, string, int) (string, error)
Bar(context.Context, string) error
Baz(context.Context) (int, error)
}
跟蹤請求
一旦您的基礎(chǔ)架構(gòu)超出了一定的規(guī)模,通過多個(gè)服務(wù)跟蹤請求變得非常重要,因此您可以識別和排除熱點(diǎn)問題.有關(guān)詳細(xì)信息,請參 tracing 。
創(chuàng)建一個(gè)客戶端包
可以使用Go Kit為您的服務(wù)創(chuàng)建客戶端包,以便從其他Go程序中更輕松地使用您的服務(wù).實(shí)際上,您的客戶端軟件包將提供您的服務(wù)接口的實(shí)現(xiàn),該接口使用特定的傳輸調(diào)用遠(yuǎn)程服務(wù)實(shí)例.有關(guān)示例.請參閱package addsvc / client 或package profilesvc / client 。