go kit 是一个分布式的开发工具集,在大型的组织(业务)中可以用来构建微服务。其解决了分布式系统中的大多数常见问题,因此,使用者可以将精力集中在业务逻辑上。
Go kit首先解决了RPC消息模式。其使用了一个抽象的 endpoint 来为每一个RPC建立模型。
endpoint通过被一个server进行实现(implement),或是被一个client调用。这是很多 Go kit组件的基本构建代码块。
Circuitbreaker(回路断路器) 模块提供了很多流行的回路断路lib的端点(endpoint)适配器。回路断路器可以避免雪崩,并且提高了针对间歇性错误的弹性。每一个client的端点都应该封装(wrapped)在回路断路器中。
ratelimit模块提供了到限流器代码包的端点适配器。限流器对服务端(server-client)和客户端(client-side)同等生效。使用限流器可以强制进、出请求量在阈值上限以下。
transport 模块提供了将特定的序列化算法绑定到端点的辅助方法。当前,Go kit只针对JSON和HTTP提供了辅助方法。如果你的组织使用完整功能的传输层,典型的方案是使用Go在传输层提供的函数库,Go kit并不需要来做太多的事情。这些情况,可以查阅代码例子来理解如何为你的端点写一个适配器。目前,可以查看 addsvc的代码来理解Transport绑定是如何工作的。我们还提供了针对Thirft,gRPC,net/rpc,和http json的特殊例子。对JSON/RPC和Swagger的支持在计划中。
服务产生的日志是会被延迟消费(使用)的,或者是人或者是机器(来使用)。人可能会对调试错误、跟踪特殊的请求感兴趣。机器可能会对统计那些有趣的事件,或是对离线处理的结果进行聚合。这两种情况,日志消息的结构化和可操作性是很重要的。Go kit的 log 模块针对这些实践提供了最好的设计。
直到服务经过了跟踪计数、延迟、健康状况和其他的周期性的或针对每个请求信息的仪表盘化,才能被认为是“生产环境”完备的。Go kit 的 metric 模块为你的服务提供了通用并健壮的接口集合。可以绑定到常用的后端服务,比如 expvar 、statsd、Prometheus。
随着你的基础设施的增长,能够跟踪一个请求变得越来越重要,因为它可以在多个服务中进行穿梭并回到用户。Go kit的 tracing 模块提供了为端点和传输的增强性的绑定功能,以捕捉关于请求的信息,并把它们发送到跟踪系统中。(当前支持 Zipkin,计划支持Appdash
如果你的服务调用了其他的服务,需要知道如何找到它(另一个服务),并且应该智能的将负载在这些发现的实例上铺开(即,让被发现的实例智能的分担服务压力)。Go kit的loadbalancer模块提供了客户端端点的中间件来解决这类问题,无论你是使用的静态的主机名还是IP地址,或是 DNS的 SRV 记录,Consul,etcd 或是 Zookeeper。并且,如果你使用定制的系统,也可以非常容易的编写你自己的 Publisher,以使用 Go kit 提供的负载均衡策略。(目前,支持静态主机名、etcd、Consul、Zookeeper)
Go kit 是一个函数库,设计的目标是引入到二进制文件中。对于二进制软件包的作者来讲,Vendoring是目前用来确保软件可靠、可重新构建的最好的机制。因此,我们强烈的建议我们的用户使用vendoring机制来管理他们软件的依赖,包括Go kit。
为了避免兼容性和可用性的问题,Go kit没有vendor它自己的依赖,并且并不推荐使用第三方的引用代理。
有一些工具可以让vendor机制更简单,包括 gb、glide、gvt、 govendor 和 vendetta。另外,Go kit使用了一系列的持续集成的机制来确保在尽快地修复那些复杂问题。
标注有 ★ 的项目对 Go kit 的设计有着特别的影响 (反之亦然)
afex/hystrix-go, client-side latency and fault tolerance library
armon/go-metrics, library for exporting performance and runtime metrics to external metrics systems
codahale/lunk, structured logging in the style of Google’s Dapper or Twitter’s Zipkin
eapache/go-resiliency, resiliency patterns
sasbury/logging, a tagged style of logging
grpc/grpc-go, HTTP/2 based RPC
inconshreveable/log15, simple, powerful logging for Go ★
mailgun/vulcand, programmatic load balancer backed by etcd
mattheath/phosphor, distributed system tracing
pivotal-golang/lager, an opinionated logging library
rubyist/circuitbreaker, circuit breaker library
Sirupsen/logrus, structured, pluggable logging for Go ★
sourcegraph/appdash, application tracing system based on Google’s Dapper
spacemonkeygo/monitor, data collection, monitoring, instrumentation, and Zipkin client library
streadway/handy, net/http handler filters
vitess/rpcplus, package rpc + context.Context
gdamore/mangos, nanomsg implementation in pure Go
Revel (considered harmful)
Architecting for the Cloud — Netflix
Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Google
Your Server as a Function (PDF) — Twitter
发表于2016 年 5 月 28 日由chunshengster@gmail.com
服务(Service)是从业务逻辑开始的,在 Go kit 中,我们将服务以 interface 作为模型
1 2 3 4 5 | // StringService provides operations on strings. type StringService interface { Uppercase(string) (string, error) Count(string) int } |
这个 interface 需要有一个“实现”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 中,主要的消息模式是 RPC。因此,接口( interface )的每一个方法都会被模型化为远程过程调用(RPC)。对于每一个方法,我们都定义了请求和响应的结构体,捕获输入、输出各自的所有参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | type uppercaseRequest struct { S string `json:"s"` }
type uppercaseResponse struct { V string `json:"v"` Err string `json:"err,omitempty"` // errors don't define JSON marshaling }
type countRequest struct { S string `json:"s"` }
type countResponse struct { V int `json:"v"` } |
Go kit 通过 endpoint 提供了非常丰富的功能。
一个端点代表一个RPC,也就是我们服务接口中的一个函数。我们将编写简单的适配器,将我们的服务的每一个方法转换成端点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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支持多种开箱即用的 传输 方式。(Adding support for new ones is easy—just 对新方式的支持是非常简单的。参见 这里
针对我们现在的这个微型的服务例子,我们使用基于 HTTP 的 JSON。Go kit 中提供了一个辅助结构体,在 transport/http 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | import ( "encoding/json" "log" "net/http"
"golang.org/x/net/context"
httptransport "github.com/go-kit/kit/transport/http" )
func main() { ctx := context.Background() svc := stringService{}
uppercaseHandler := httptransport.NewServer( ctx, makeUppercaseEndpoint(svc), decodeUppercaseRequest, encodeResponse, )
countHandler := httptransport.NewServer( ctx, 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) } |
发表于2016 年 6 月 1 日由chunshengster@gmail.com
任何服务在日志和仪表化没有就绪的情况下,都不能说是生产环境就绪的。
任何需要日志记录的组件都需要将 logger 作为依赖,就像数据库连接一样。因此,我们在 main 函数中构造 logger 对象,然后将其传入需要使用它的组件中。我们始终不去使用一个全局的 logger 对象。
我们可以直接将 logger 传入到 stringService 的实现代码中,但是,还有一个更好的方式。我们可以使用 中间件 (middleware) ,也常常被称为 装饰者。
middleware 是一个函数,它接收一个 endpoint 作为参数,并且返回一个 endpoint。
1 | type Middleware func(Endpoint) Endpoint |
在函数中,它可以做任何事情。下面就让我们来创建一个基本的日志中间件。
1 2 3 4 5 6 7 8 9 | 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) } } } |
然后,我们将它加入到每一个处理函数中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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 的很多模块都被实现为端点中间件。
那么,在我们的应用中,应该如何记录日志呢?比如那些需要被传入的参数等。事实上,我们能够为我们的服务定义一个中间件,从而获得同样好的组合效果。由于我们的 StringService被定义为一个接口,我们只需要作出一个新的类型,来包装先有的 StringService,让它来执行扩充的记录日志的任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | type loggingMiddleware struct{ logger log.Logger 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.StringService.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.StringService.Count(s) return } |
然后,将新的类型引入到下面的代码中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import ( "os"
"github.com/go-kit/kit/log" httptransport "github.com/go-kit/kit/transport/http" )
func main() { logger := log.NewLogfmtLogger(os.Stderr)
svc := stringService{} svc = loggingMiddleware{logger, svc}
uppercaseHandler := httptransport.NewServer( // ... makeUppercaseEndpoint(svc), // ... )
countHandler := httptransport.NewServer( // ... makeCountEndpoint(svc), // ... ) } |
在传输环节使用端点中间件,比如回路断路器和速率限制。在业务环节使用服务中间件,比如日志和仪表化。
在 Go kit 中,仪表化意味着使用 包指标 来记录关于服务运行行为的状态。统计执行的任务的数量,在请求完成后记录消耗的时间,以及跟踪所有正在执行的操作的数量,都被认为是 仪表化。
我们可以使用同样的中间件模式,在记录日志的环节我们曾经用过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | type instrumentingMiddleware struct { requestCount metrics.Counter requestLatency metrics.TimeHistogram countResult metrics.Histogram 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.StringService.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.StringService.Count(s) return } |
然后将其引入到服务中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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{}))
svc := stringService{} svc = loggingMiddleware{logger, svc} svc = instrumentingMiddleware{requestCount, requestLatency, countResult, svc}
uppercaseHandler := httptransport.NewServer( // ... makeUppercaseEndpoint(svc), // ... )
countHandler := httptransport.NewServer( // ... makeCountEndpoint(svc), // ... )
http.Handle("/metrics", stdprometheus.Handler()) } |
目前位置,完整的服务是 stringsvc2.
1 2 3 | $ go get github.com/go-kit/kit/examples/stringsvc2 $ stringsvc2 msg=HTTP addr=:8080 |
1 2 3 4 | $ 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} |
1 2 | method=uppercase input="hello, world" output="HELLO, WORLD" err=null took=2.455µs method=count input="hello, world" n=12 took=743ns |
发表于2016 年 6 月 16 日由chunshengster@gmail.com
存在“真空”(即极其独立,与其他任何服务无互相调用的关系)中的服务是罕见的。而我们常见的是,我们需要调用其他的服务。这也是 Go kit 的闪光点 ,我们提供了 传输中间件机制来解决可能出现的很多问题。
下面我们将实现一个代理功能的中间件,作为一个服务中间件。在这里我们只代理一个方法,Uppercase。
1 2 3 4 5 6 7 8 | // proxymw implements StringService, forwarding Uppercase requests to the // provided endpoint, and serving all other (i.e. Count) requests via the // embedded StringService. type proxymw struct { context.Context StringService // Serve most requests via this embedded service... UppercaseEndpoint endpoint.Endpoint // ...except Uppercase, which gets served by this endpoint } |
我们已经有了一个跟我们所知道的完全相同的端点,但是我们将使用它来调用一个请求,而不是提供服务。按照这种方式来使用它的时候,我们称它为客户端端点。为了调用客户端端点,我们需要做一些简单的转换。
1 2 3 4 5 6 7 8 9 10 11 | func (mw proxymw) Uppercase(s string) (string, error) { response, err := mw.UppercaseEndpoint(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)函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 为不同的服务发现系统提供了适配器,为了获取最新的服务实例集合,暴露端点个体。这些适配器被称为 发布器(publishers)。
1 2 3 | type Publisher interface { Endpoints() ([]endpoint.Endpoint, error) } |
在发布器内部,它使用一个私有的工厂函数,将被发现的每一个 host:port 字符串 转换成一个可用的端点。
1 | type Factory func(instance string) (endpoint.Endpoint, error) |
目前,我们的工程方法,makeUppercaseEndpoint,只是直接请求 URL。但是,在工厂函数中加入一些安全的中间件方法是很重要的,比如 回路断路器 和 限流器。
1 2 3 4 5 6 7 8 9 | func factory(ctx context.Context, maxQPS int) loadbalancer.Factory { return func(instance string) (endpoint.Endpoint, error) { 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) return e, nil } } |
现在,我们已经有了一系列的端点,我们需要在其中选择一个。负载均衡器包装了 发布器,并且从端点集合中选择其中的某一个。Go kit 提供了一组基本的负载均衡器,并且,如果你希望更多的高级的算法,也可以很容易的自己来写一个。
1 2 3 | type LoadBalancer interface { Endpoint() (endpoint.Endpoint, error) } |
现在,我们可以根据一下算法来选择端点。我们能够使用它为消费器提供一个单一的、逻辑的可靠的端点。通过重试的策略包装负载均衡器,并且返回一个可用的端点。重试的策略可以将一个失败的请求进行重试,直到达到最大的可重试次数或是达到超时时间。
1 | func Retry(max int, timeout time.Duration, lb LoadBalancer) endpoint.Endpoint |
现在,让我们将最后的代理中间件加入到代码中。为了简洁,我们假设用户会为逗号(,)分隔的多个实例端点指定一个标记。
1 2 3 4 5 6 7 8 9 10 11 12 13 | func proxyingMiddleware(proxyList string, ctx context.Context, logger log.Logger) ServiceMiddleware { return func(next StringService) StringService { var ( qps = 100 // max to each instance publisher = static.NewPublisher(split(proxyList), factory(ctx, qps), logger) lb = loadbalancer.NewRoundRobin(publisher) maxAttempts = 3 maxTime = 100 * time.Millisecond endpoint = loadbalancer.Retry(maxAttempts, maxTime, lb) ) return proxymw{ctx, endpoint, next} } } |
截止目前,这个完整的服务是 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
1 2 3 4 | $ 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} |
1 2 3 4 5 6 | 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 |
上下文对象用来在单个请求中携带那些需要跨越概念性边界的信息。在我们的例子中,我们还没有在业务逻辑中使用线程上下文。但是,这种方式几乎会永远都是一个好方案。它允许我们在业务逻辑和中间件中传递请求范围内的信息,并且对于复杂的任务(比如,分布式系统的细粒度追踪信息)也是很必要的。
具体点来讲,这也意味着你的业务逻辑接口会这样:
1 2 3 4 5 | type MyService interface { Foo(context.Context, string, int) (string, error) Bar(context.Context, string) error Baz(context.Context) (int, error) } |
一旦你的基础设施增长超过一定的规模,在多个服务中跟踪请求是非常必要的,这样,你就能够分析并解决故障热点。参见 package tracing 获取更多信息。
使用 Go kit 为你的服务创建一个客户端软件包是很可能的事情,让你的服务能够很容易对其他的 Go 程序进行调用。实际上,你的客户端package会提供你的服务接口,这个接口会使用指定的传输方式来调用远程服务。参见 package addsvc/client 作为参考例子.
addsvc 是原来的一个例子。它公开 所有支持的传输方式 的系列操作。它完整地做了日志记录、仪表盘化,并且使用 Zipkin 来跟踪请求。同样,它也示范了如何创建和使用客户端package。它是一个非常棒的例子,为 Go kit 服务提供了完整的功能示例。
profilesvc 示范了如何使用 Go kit 来打造 REST 风格的微服务。
apigateway 示范了如何实现一个 API 网关模式,通过 Consul 作为服务发现系统。
shipping 是一个完整的,真实的应用,由多个微服务组成,基于领域驱动设计原则。