在自己的进程中运行并用轻量的通信机制
已业务为中心进行构建
能够独立的自动化部署
使用最少的集中化管理
二、GoKit简介
GoKit其实是一个Go语言组件库,通过它可以构建稳定、可靠、易维护的微服务。GoKit的原作者为Peter Bourgon,其灵感主要还是来自Twitter公司基于Scala开发的RPC框架Finagle。Peter Bourgon认为Scala语言还是太复杂了,通常也是难以理解的,对比之下Go语言就比较易于理解,且拥有接近C语言的运行效率,但是当时Go语言还没有一个成熟的、详尽的分布式服务工具包。在这个背景下,GoKit诞生了。
Gokit的特点就是其标志的洋葱模型,如上图所示。可以看到模型分成了很多层,而这些其实可以归为三层:最里边这层可以称为Service层,这层是服务的基础,实现了所有的业务逻辑;中间的这层称之为Endpoint层,可以理解为安全层,有点类似controller层里的handler,在这层中每个方法都被抽象成endpoint.Endpoint方法;最外层的是Transport层,主要负责各种协议的解析和封装,例如HTTP、gRPC。
网络请求进入服务时先经过Transport层,一直流入到Service层,然后进行响应时路径相反。
三、为什么如此设计
很多开发者在学习GoKit的过程中,可能理解起来会比较困难,尤其是其中的抽象的Endpoint层。
为了方便理解,我们可以按照微服务的理论,尝试设计一套微服务的架构,通过这个过程由浅入深的去理解GoKit的设计理念。
3.1 简易服务搭建
例如,实现一个计算加法的计算服务,那么从业务出发,这个服务应该被定义成这样:即接受两个int型的变量,然后相加后返回。
type CalculateService interface {
sum(int, int) (int, error)
}
接着编写具体的结构体,去实现这个接口。
type Calculator struct {
A int
B int
}
func (c *Calculator) sum(a, b int) (int, error) {
return a + b, nil
}
定义Calculator的ServeHTTP方法,让这个结构体实现net/http包的Handler接口。这样http.ListenAndServe函数就可以接收相应的对象,并且能够启动这个服务了。
func (c *Calculator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
method := r.Form.Get("method")
var calculator Calculator
switch method {
case "sum":
reqA := r.FormValue("a")
reqB := r.FormValue("b")
if reqA == "" || reqB == "" {
return
}
paramA, err := strconv.Atoi(reqA)
if err != nil {
return
}
paramB, err := strconv.Atoi(reqB)
if err != nil {
return
}
res, err := calculator.sub(paramA, paramB)
if err != nil {
return
}
json.NewEncoder(w).Encode(res)
}
}
func main() {
mux := http.NewServeMux()
ps := new(Calculator)
mux.Handle("/calculate", ps)
http.ListenAndServe(":8081", mux)
}
3.2 完善服务
现在要完善这个服务,首先加入日志功能,那么ServeHTTP方法现在变成这样。
func (c *Calculator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
method := r.Form.Get("method")
var calculator Calculator
switch method {
case "sum":
reqA := r.FormValue("a")
reqB := r.FormValue("b")
if reqA == "" || reqB == "" {
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusBadRequest)
return
}
paramA, err := strconv.Atoi(reqA)
if err != nil {
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusBadRequest)
return
}
paramB, err := strconv.Atoi(reqB)
if err != nil {
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusBadRequest)
return
}
res, err := calculator.sub(paramA, paramB)
if err != nil {
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusInternalServerError)
return
}
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusOK)
json.NewEncoder(w).Encode(res)
}
}
如果要加入其他的管理功能呢,比如监控,下面以Prometheus举例。可以看到代码已经变得十分臃肿。
func (c *Calculator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
method := r.Form.Get("method")
var calculator Calculator
var begin = time.Now()
switch method {
case "sum":
reqA := r.FormValue("a")
reqB := r.FormValue("b")
if reqA == "" || reqB == "" {
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusBadRequest)
dur.WithLabelValues(r.Method, fmt.Sprint(http.StatusBadRequest)).Observe(time.Since(begin).Seconds())
return
}
paramA, err := strconv.Atoi(reqA)
if err != nil {
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusBadRequest)
dur.WithLabelValues(r.Method, fmt.Sprint(http.StatusBadRequest)).Observe(time.Since(begin).Seconds())
return
}
paramB, err := strconv.Atoi(reqB)
if err != nil {
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusBadRequest)
dur.WithLabelValues(r.Method, fmt.Sprint(http.StatusBadRequest)).Observe(time.Since(begin).Seconds())
return
}
res, err := calculator.sub(paramA, paramB)
if err != nil {
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusInternalServerError)
dur.WithLabelValues(r.Method, fmt.Sprint(http.StatusInternalServerError)).Observe(time.Since(begin).Seconds())
return
}
log.Printf("%s: %s: %s: %d", r.RemoteAddr, r.Method, r.URL, http.StatusOK)
dur.WithLabelValues(r.Method, fmt.Sprint(http.StatusOK)).Observe(time.Since(begin).Seconds())
json.NewEncoder(w).Encode(res)
}
}
假如继续加入其他功能,业务逻辑那层依然是简洁明了的,但是协议解析的这层代码将会越来越臃肿。
设想,能不能引入一种功能单一、可以互相组合使用的中间件,例如下面这样:
func logging() {
// log...
}
func rateLimiting() {
// limit rate...
}
func metrics() {
// metrics...
}
可以自然的想到使用装饰者模式,装饰者模式的好处是可以动态的扩展功能,需要哪些功能装饰一下就可以了。
为了实现装饰者模式,可以先抽象出一个函数类型,不管对这个类型装饰多少层,最后还是最初的类型。这个函数应该有输入和输出,还有一个报错信息,所以大概是下面所示:
type Endpoint func(request interface{}) (response interface{}, err error)
除此之外,在一些场景下,例如rpc,需要在洋葱模型之间传递错误信息。这里引入context来实现并发场景下对goroutine的控制。
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)
然后这里重新定义一个函数,用于返回一个Endpoint类型,这样就可以对返回的Endpoint类型进行各种中间件的包装了。
func makeSumEndpoint() Endpoint {
return func(ctx context.Context, request interface{}) (interface {}, error) {
calculator := request.(Calculator)
return calculator.sum(calculator.A, calculator.B)
}
}
func loggingMiddleware(e Endpoint) Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
// logging
begin := time.Now()
defer func() {
log.Printf("time cost: %d", time.Since(begin))
}()
return e(ctx, request)
}
}
func makeLoggingMiddleware(method string) Middleware {
return func(e Endpoint) Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
// logging
begin := time.Now()
defer func() {
log.Printf("%s :time cost: %d", method, time.Since(begin))
}()
return e(ctx, request)
}
}
}
type Handler struct {
endpoint Endpoint
}
func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
method := r.Form.Get("method")
switch method {
case "sum":
reqA := r.FormValue("a")
reqB := r.FormValue("b")
if reqA == "" || reqB == "" {
return
}
paramA, err := strconv.Atoi(reqA)
if err != nil {
return
}
paramB, err := strconv.Atoi(reqB)
if err != nil {
return
}
var calculator = Calculator{
A: paramA,
B: paramB,
}
res, err := handler.endpoint(context.Background(), calculator)
if err != nil {
return
}
json.NewEncoder(w).Encode(res)
}
}
可以看到协议层还在直接引用业务层的Calculator结构体,这里可以沿用之前的处理方法,Handler结构体增加两个函数类型:decodeRequest和encodeResponse,分别负责请求内容解析和响应内容封装。当调用Serve HTTP方法时,首先调用decodeRequest解析请求内容,然后调用endpoint方法,最后调用encodeResponse将响应内容进行封装。这其实就是GoKit的解决办法。
type Handler struct {
endpoint Endpoint
dec func(r *http.Request) (interface{}, error)
enc func(v interface{}, w http.ResponseWriter) error
}
func decodeRequest(r *http.Request) (interface{}, error) {
r.ParseForm()
reqA := r.FormValue("a")
reqB := r.FormValue("b")
if reqA == "" || reqB == "" {
return nil, errors.New("param err")
}
paramA, err := strconv.Atoi(reqA)
if err != nil {
return nil, err
}
paramB, err := strconv.Atoi(reqB)
if err != nil {
return nil, err
}
return Calculator{
A: paramA,
B: paramB,
},nil
}
func encodeResponse(v interface{}, w http.ResponseWriter) error {
return json.NewEncoder(w).Encode(v)
}
func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
method := r.Form.Get("method")
switch method {
case "sum":
calculator,err := handler.dec(r)
if err != nil {
return
}
res, err := handler.endpoint(context.Background(), calculator)
if err != nil {
return
}
err = handler.enc(res, w)
}
}
经过这样处理后,三层结构已经非常清晰了,这也是恰好对应着GoKit中的Transport、Endpoint和Service三层架构。
GoKit提供给了开发者现成的组件,开发者只需要编写对应的service、endpoint和encode/decode函数就可以了。下面是GoKit提供的限流器、熔断器、链路追踪等中间件。开发者还可以自己编写中间件。
func NewErroringLimiter(limit Allower) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
if !limit.Allow() {
return nil, ErrLimited
}
return next(ctx, request)
}
}
}
func Gobreaker(cb *gobreaker.CircuitBreaker) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
return cb.Execute(func() (interface{}, error) { return next(ctx, request) })
}
}
}
func TraceServer(tracer opentracing.Tracer, operationName string) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
serverSpan := opentracing.SpanFromContext(ctx)
if serverSpan == nil {
// All we can do is create a new root span.
serverSpan = tracer.StartSpan(operationName)
} else {
serverSpan.SetOperationName(operationName)
}
defer serverSpan.Finish()
otext.SpanKindRPCServer.Set(serverSpan)
ctx = opentracing.ContextWithSpan(ctx, serverSpan)
return next(ctx, request)
}
}
}
除了服务端,GoKit将客户端也抽象出了同样的Endpoint层,例如go-kit/kit/transport/http中NewClient函数,返回的Client结构体会含有一个Endpoint方法,这个方法就是获取对应的endpoint,内部逻辑实际就是发出具体的http请求。这样的好处就是一个同一个中间件可以同时对服务端和客户端进行装饰。
四、总结
GoKit套件这种架构达到了超低的耦合度。
Service层就只负责业务逻辑,哪怕哪天不想用GoKit这套东西了,可以直接把Serivce层的方法直接嵌到其他框架中;Transport层就只负责协议的解析与封装,GoKit目前支持多种通信方式,包括HTTP、gRPC、Thrift、RabbitMQ等等;而抽象出来的Enpoint中间层,可以利用GoKit提供的各种Middleware进行封装,实现服务发现、接口限流、负载均衡以及链路追踪等功能。
最后再细看GoKit在设计过程中体现的哲学思想:
S.O.L.I.D design面向对象编程和面向对象设计五大原则
DDD领域驱动设计
The Clean Architecture整洁架构理论
截止到2022年1月1日,GoKit 在github中的stars已经达到22.1k,也是从侧面反映了GoKit不失为一个优秀的微服务组件库。
微服务解决了大型单体应用带来的问题,但是却引入了一些新的问题:例如链路较长导致的测试困难、分布式事务等等。假如GoKit仅仅也是用来开发微服务应用的话,那这些问题也是不可避免的。好消息是尽管GoKit被设计成一个打造微服务的组件库,开发者依然可以参考GoKit的这种三层架构的思想,来建造一个稳定、易维护的系统。