Go kit 翻譯

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 。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,545評論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,268評論 6 342
  • 簡介: go kit 是一個(gè)分布式的開發(fā)工具集,在大型的組織(業(yè)務(wù))中可以用來構(gòu)建微服務(wù)。其解決了分布式系統(tǒng)中常見...
    李小賤AA閱讀 3,012評論 1 2
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,983評論 25 709
  • 一直以來 不管是在微博 還是微信中 總會看到很多眾籌的項(xiàng)目 每一次都會有種沉重感 無論認(rèn)識與否 我都會為其祈禱 是...
    木樂子閱讀 1,876評論 0 0

友情鏈接更多精彩內(nèi)容