1
头图

go-kit的基本介绍

go-kit 介绍

go-kit 是一个 Golang 编写的开发框架,可以帮助开发者更快捷地构建可伸缩的微服务架构。它提供了一系列模块化的组件,可以帮助开发者更轻松地构建和维护微服务。go-kit的设计理念是可组合的,它可以与各种服务发现系统进行集成,如etcd、consul和zookeeper等,并且可以轻松实现服务熔断和负载均衡。

另外,go-kit也提供了诸如监控、日志和链路追踪的功能,可以帮助开发者更好地理解和控制微服务架构。

go-kit 还提供了指标收集和分析功能,可以帮助开发者进行性能优化和故障诊断。它还允许用户使用自定义的协议,比如REST、gRPC和GraphQL等,来实现不同服务之间的通信。

设计哲学

go-kit 是一个符合 KISS 原则的框架,通过使用关注点分离,让开发者优先集中于业务逻辑的开发。在业务逻辑完成之后,再通过组合快速接入微服务的各种能力。

go-kit 主要可以划分为:

  • Service Layer —— 专注于业务逻辑,处理 request,返回 response。
  • Endpoint Layer —— 是 Service 的入口,对 Service 进行 wrapper,可以附加各种 rate-limit metrics 的 middleware,从而增强 Service。
  • Transport Layer —— 定义客户端和服务端应该如何通信,负责网络协议转换等,例如 gRPC、HTTP 等协议的处理。

在 go-kit 中,整个项目就像是一个洋葱,最内核是 Service,也就是业务逻辑。然后通过一层层middleware 进行包裹,为项目添加各种能力。

https://gokit.io/faq/onion.png

动手实践

Service

既然是业务优先,那么开发的顺序自然是应该从 Service 业务逻辑开始。

让我们从一个简单的用户服务开始吧!假设我们需要实现一个user-service,它需要处理用户的注册、登录的逻辑。基于面向接口编程的原则,我们可以设计一个Service如下:

type HelloRequest struct {
    Name string `json:"name"`
}

type HelloResponse struct {
    Message string `json:"message"`
}

type HelloService interface {
    Hello(ctx context.Context, name string) (HelloResponse, error)
}

type helloService struct{}

func (s *helloService) Hello(ctx context.Context, name string) (HelloResponse, error) {
    return HelloResponse{Message: "Hello, " + name}, nil
}

Endpoint

写完业务逻辑之后,我们需要对外提供这个接口,可以用Endpoint来包裹这个Service。在 go-kit 中,Endpoint 就是一个interface

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

它主要负责的是:接收外部的 request,交给 Service 处理之后,返回对应的 response。

那么,我们可以这样实现 HelloService 的 Endpoint:

func MakeHelloEndpoint(svc HelloService) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(HelloRequest)
        res, err := svc.Hello(ctx, req.Name)        // 调用实际的 Service 执行业务逻辑
        if err != nil {
            return nil, err
        }
        return res, nil
    }
}

Transport

最后,就是需要把这个服务暴露出来,对外提供服务了。在 go-kit 中,这也就是 Transport 需要做的事情,Transport 具体怎么写,取决于项目实际的网络方案。如果是 http,那么 Transport 就需要将 http 请求数据转换为 Service的请求参数。我们使用 http 做一个示例:

func decodeHelloRequest(_ context.Context, r *http.Request) (interface{}, error) {
    return HelloRequest{Name: r.FormValue("name")}, nil
}

func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
    return json.NewEncoder(w).Encode(response)
}

其中decode负责 http.Request --> HelloRequestencode 负责HelloResponse --> http.Response

Server

最后,将所有的组件装配起来,用一个 http server 来启动服务就好了:

func main() {
    svc := &helloService{}
    ep := MakeHelloEndpoint(svc)

    route := mux.NewRouter()
  // go-kit的 http 协议处理
    route.Methods("Get").Path("/hello").Handler(kithttp.NewServer(
        ep,
        decodeHelloRequest,
        encodeResponse,
    ))

    log.Fatal(http.ListenAndServe(":8080", route))
}

运行一下就可以看到结果:

❯ curl "http://localhost:8080/hello?name=j"
{"message":"Hello, j"}

其中最核心的一块就是执行kithttp.NewServer()这个函数,它会接受 endpoint、decode、encode几个参数。我们可以分别再看看这几个参数的作用:

  • endpoint —— 接受 request,调用 Service,返回 response
  • decode —— 将网络协议数据转换成 request
  • encode —— 将 response 转换成网络协议数据返回

也许,再看看 go-kit的源码会更加有助于理解整个链路是怎么样的。在 go-kit中,NewServer创建的对象最核心的逻辑就是:

func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    request, err := s.dec(ctx, r)
    if err != nil {
        // error handler
        return
    }

    response, err := s.e(ctx, request)
    if err != nil {
        // error handler
        return
    }
  
  if err := s.enc(ctx, w, response); err != nil {
        // error handler
        return
    }
}

完成的示例可以从 GitHub 查看。

引入微服务的能力

为什么需要流量控制

在微服务架构中,服务之间是通过网络调用来实现协作的。如果某个服务的负载高,其它服务请求这个服务时就会等待。这样会导致整个系统的瓶颈,影响整个系统的吞吐量和稳定性。因此,对服务进行流量控制是很有必要的。而 ratelimit 就是其中一种流量控制的实现方法。它可以限制一个服务在一段时间内能够接受的请求数量,从而避免一个服务的高负载导致整个系统的故障。

如何实现 ratelmit

在 go-kit 中,可以很方便地实现一个简单的 ratelimit。

在如果熟悉 OOP 的话,应该会听过装饰器模式。在 go-kit 中,就是使用了这个思想,用 Endpoint 包裹 Endpoint,从而添加各种不同的能力。例如,在我们的例子中,想要给微服务添加一个ratelimit能力的话,就可以这样创建一个装饰器:

type limitMiddleware struct {
    timer time.Duration
    burst int
}

func (l limitMiddleware) wrap(e endpoint.Endpoint) endpoint.Endpoint {
    e = ratelimit.NewErroringLimiter(rate.NewLimiter(rate.Every(l.timer), l.burst))(e)
    return e
}

limitMiddwware是一个限速器,timer 是一个时间周期,burst 是最大并发请求数量。wrap函数就是我们的装饰器,接受一个 Endpoint,返回一个 Endpoint,它就可以为 Endpoint 添加 ratelimit 的功能。

相应地,我们的 main程序就可以这样使用这个装饰器:

func main() {
    svc := &helloService{}
    ep := MakeHelloEndpoint(svc)

    // decorate ratelimit
    ratelimit := limitMiddleware{
        timer: 5 * time.Second,
        burst: 3,
    }
    ep = ratelimit.wrap(ep)
  
    route := mux.NewRouter()
    route.Methods("Get").Path("/hello").Handler(kithttp.NewServer(
        ep,
        decodeHelloRequest,
        encodeResponse,
    ))

    log.Fatal(http.ListenAndServe(":8080", route))
}

上面的例子就为这个服务创建了一个 ratelimit,如果在 5 秒钟内请求数超过 3 个的话,这个 ratelimit 就会拒绝请求。我们可以看看效果:

❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:27 CST 2023
{"message":"Hello, j"}
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:27 CST 2023
{"message":"Hello, j"}
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:28 CST 2023
{"message":"Hello, j"}
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:29 CST 2023
rate limit exceeded%                        # 触发了 ratelimit                                                                           
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:30 CST 2023
rate limit exceeded%                                                                                                   
❯ date && curl "http://localhost:8080/hello?name=j"
Wed Feb  8 15:25:33 CST 2023                # 恢复响应请求
{"message":"Hello, j"}

拓展一下

不同的算法

上面用到的限速器是基于令牌桶算法实现的,类似的还有很多其他的算法实现:

还有开源软件也有各自的实现,比如 Java 生态中的 Hystrixresillience4,或者是 Nginx 也有自己的实现。

全局限流

换个角度,这些ratelimit 都是单个服务的限流,如果要做全局限流的话,我们可以通过引入集中式的数据存储。将原本程序内存的请求计数器放到外部存储,所有服务共享一个计数器来实现。比如Redis 限流最佳实践

自适应限流

上面的 ratelimit 解决方案都有一个问题:静态的配置在实际的分布式环境中不好用。在大型的分布式系统中,并发数、系统负载、可用资源都是动态变化的,我们很难得到一个静态的值来限流,这就需要我们实现一种动态的限流算法:根据系统的情况,动态调整限流阈值。相应的有aws 限流算法netflix限流算法来实现自适应限流处理。

总之,还是那句话:

系统设计没有银弹,还是需要根据实际情况做 trade-off。

其他

这只是一个简单的示例,微服务开发中还有很多服务发现断路器负载均衡重试等等的常规功能。就留待之后再进行拓展吧。


stillfox
137 声望7 粉丝