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 。

你可能感兴趣的:(Go kit 翻译)