Go kit 翻譯

Go kit 文檔

首要原則

創建一個小型Go kit 服務

你的業務邏輯

你的服務起始于業務邏輯.在Go kit 中,我們讓一個接口作為一個服務.

// StringService provides operations on strings.
type StringService interface {
    Uppercase(string) (string, error)
    Count(string) int
}

該接口將會被實現

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

請求和響應

在Go kit中,主要的通信方式是PRC.所以,你接口中的每個方法都會被遠程過程調用.對于每個方法,我們都定義了請求和響應結構體,來分別捕獲所有的入參合出參.

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

端點

Go kit 通過抽象出一個端點提供了大部分的功能

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

一個端點對應一個PRC.就是我們服務接口中的一個方法.我們將會寫簡單的適配器去把我們服務中的方法轉換成一個端點.每個適配器拿到一個 StringService,
同時返回一個方法中對應的端點.

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

傳輸

現在我們需要將你的服務暴露給外界調用,所以可以調用它.你的組織可能對服務如何交流有所了解了.也許你使用Thrift,或者通過HTTP自定義JSON,Go kit許多傳輸開箱即用.

對于小型服務,使用HTTP/JSON.Go kit 在transport/http提供了一個 helper 結構體.

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

以上完整的服務 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}

中間件

沒有日志和儀表盤的服務是不能用于生產環境的

傳輸日志

需要記錄的任何組件都應該像記錄器那樣像一個依賴關系,與數據庫連接相同.因此.我們在我們的func man中構建我們的記錄器,并將其傳遞給需要它的組件.我們從不使用全局范圍的記錄器.

我們可以直接將記錄器傳遞給我們的stringService實現,但是有一個更好的方法。我們來使用一個中間件,也稱為裝飾器。中間件是一個接收端點并返回端點的函數。

在中間件里,可以做任何事,讓我創建一個基本的記錄器中間件.

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

在你的每個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,
    // ...
)

事實證明,這種技術比僅僅打印日志有用的多.許多Go kit 組件都是一個端點的中間件.

應用日志

假如我們想在全局打印日志,是要傳遞參數給作用域嗎?應該給我們的服務定義一個中間件,配置化得同時可以達到相同的效果.由于你的服務定義了一個接口,我們僅僅需要定義一個類型包裹這個服務,執行額外的打印日志的功能.

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
}

同時在這里加上

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),
        // ...
    )
}

端點的中間件關注傳輸層,例如線路中斷和請求限制.服務的中間件關注業務層,例如日志打印儀表盤.話說儀表盤是什么…

應用儀表盤

在 Go kit 中,儀表盤是 用package 記錄你服務運行時行為的統計.工作進程數,
請求耗時,執行邏輯數都會被認為是儀表盤.

我們使用上面日志記錄相同的中間件模式

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
}

把它加到我們的服務中

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

以上服務完整的例子 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

調用其他服務

在真空中存在著很少的服務.通常.您需要調用其他服務。這是Go Kit厲害之處,我們提供傳輸層來解決這些問題.

假設我們要讓我們的字符串服務調用不同的字符串服務來滿足大寫方法。 實際上,將請求代理到另一個服務。 我們將代理中間件實現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
}

客戶端端點

我們已經有了我們了解的完全相同的端點,但是我們現在要調用它,而不是作為一個服務的請求,這樣的使用方式,我們稱之為一個客戶端端點.調用客戶端端點,我僅僅需要做一些轉換.

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
}

現在,我們構造其中一個代理的中間件,我們代理一個URL字符串到一個端點.假定我們使用HTTP/JSON格式,我們需要用到 一個來自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()
}

服務發現和負載均衡

假如我們只有一臺遠程服務器很好辦.但是實際上,我們有多臺服務器實例在運行,我們想通過某種服務器發現機制去發現這些服務器.然后把發現的服務廣播到其他服務上.假如其中任何一臺服務不可用了,也不會影響到服務的可用性.

Go kit 提供了可以發現不同服務系統的適配器,以獲取作為單個端點公開的最新實例集。這些適配器稱為訂閱.

type Subscriber interface {
    Endpoints() ([]endpoint.Endpoint, error)
}

在訂閱內部,訂閱會用提供的函數工廠把每一個被發現的實例(類型:host:port)轉化成一個端點.

type Factory func(instance string) (endpoint.Endpoint, error)

到目前為止,我們的函數工廠 makeUppercaseEndpoint 是直接訪問URL.一些關于訪問安全的中間件,比如 熔斷器和請求限制也應該加到你的工廠里去.

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

現在我們已經設置了一些端點,我需要從中選擇一個.我們要從端點中選擇一個使用負載均衡封裝訂閱.Go kit 提供了基本的負載均衡器,你也可以很容易地優化它.

type Balancer interface {
    Endpoint() (endpoint.Endpoint, error)
}

現在我們有能力編寫自定義的端點.我們可以使用它為消費者提供一個單一的,合乎邏輯的,穩健的端點.一個重發機制封裝均衡負載器,返回一個可用的端點.
這個重發機制會重新發送失敗的請求直到超過最大請求數或者超時.

func Retry(max int, timeout time.Duration, lb Balancer) endpoint.Endpoint

我們來連接我們的最終代理中間件,為了簡單起見,我們假設用戶將使用一個標志指定多個逗號分隔的實例端點.

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

目前完整的服務 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

優化建議

上下文的使用

上下文對象用于在單個請求的作用域內跨邊界攜帶信息.在我們的例子中.我們還沒有通過我們的業務邏輯來描述上下文.但這幾乎總是一個好主意.它允許您在業務邏輯和中間件之間傳遞請求作用域的信息.并且對于更復雜的任務(如粒度分布式跟蹤注釋)是必需的.

直觀地,這就意味著我們的業務邏輯接口看起來像這樣

type MyService interface {
    Foo(context.Context, string, int) (string, error)
    Bar(context.Context, string) error
    Baz(context.Context) (int, error)
}

跟蹤請求

一旦您的基礎架構超出了一定的規模,通過多個服務跟蹤請求變得非常重要,因此您可以識別和排除熱點問題.有關詳細信息,請參 tracing

創建一個客戶端包

可以使用Go Kit為您的服務創建客戶端包,以便從其他Go程序中更輕松地使用您的服務.實際上,您的客戶端軟件包將提供您的服務接口的實現,該接口使用特定的傳輸調用遠程服務實例.有關示例.請參閱package addsvc / client package profilesvc / client

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • Spring Boot 參考指南 介紹 轉載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,953評論 6 342
  • 簡介: go kit 是一個分布式的開發工具集,在大型的組織(業務)中可以用來構建微服務。其解決了分布式系統中常見...
    李小賤AA閱讀 2,943評論 1 2
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,284評論 25 708
  • 一直以來 不管是在微博 還是微信中 總會看到很多眾籌的項目 每一次都會有種沉重感 無論認識與否 我都會為其祈禱 是...
    木樂子閱讀 1,776評論 0 0