前言
到目前为止我们已经支持了基本的RPC调用,也支持基于zk的服务注册和发现,还支持鉴权和熔断等等。虽然实现得都非常简单,但是这些功能都是基于可替换的接口实现的,所以我们后续可以很方便的替换成更加完善成熟的实现。
这次我们继续服务治理方面的功能,包括注册中心优化、限流的支持、链路追踪的支持,同时增加了一种路由策略。
支持多种数据源的注册中心
在上一篇文章里我们借助libkv实现了基于zookeeper的服务注册与发现,这次我们更进一步,将我们的ZookeeperRegistry改造成支持多种数据源的Registry。实际上的改造也比较简单,最重要的注册、发现以及通知等都已经完成了,我们只需要将底层的数据源类型改造为可配置的即可。代码如下:
//Registry的定义,就是从上一篇的ZookeeperRegistry改过来的
type KVRegistry struct {
AppKey string //KVRegistry
ServicePath string //数据存储的基本路径位置,比如/service/providers
UpdateInterval time.Duration //定时拉取数据的时间间隔
kv store.Store //store实例是一个封装过的客户端
providersMu sync.RWMutex
providers []registry.Provider //本地缓存的列表
watchersMu sync.Mutex
watchers []*Watcher //watcher列表
}
//初始化逻辑,根据backend参数的不同支持不同的底层数据源
func NewKVRegistry(backend store.Backend,addrs []string,AppKey string,cfg *store.Config,ServicePath string,updateInterval time.Duration) registry.Registry {
//libkv中需要显式初始化数据源
switch backend {
case store.ZK:
zookeeper.Register()
case store.ETCD:
etcd.Register()
case store.CONSUL:
consul.Register()
case store.BOLTDB:
boltdb.Register()
}
r := new(KVRegistry)
r.AppKey = AppKey
r.ServicePath = ServicePath
r.UpdateInterval = updateInterval
//生成实际的数据源
kv, err := libkv.NewStore(backend, addrs, cfg)
if err != nil {
log.Fatalf("cannot create kv registry: %v", err)
}
r.kv = kv
//省略了后面的初始化逻辑,因为和之前没有改动
return r
}
这里实际上是偷懒了,可以看出来这里完全就是对libkv的包装,所以能够支持的数据源也仅限libkv支持的几种,包括:boltdb、etcd、consul、zookeeper。后续如果要支持其他的注册中西比如eureka或者narcos,就得自己写接入代码了。
限流
当前的限流是基于Ticker实现的,同时支持服务端和客户端的限流
首先列举限流器接口的定义:
type RateLimiter interface {
//获取许可,会阻塞直到获得许可
Acquire()
//尝试获取许可,如果不成功会立即返回false,而不是一直阻塞
TryAcquire() bool
//获取许可,会阻塞直到获得许可或者超时,超时时会返回一个超时异常,成功时返回nil
AcquireWithTimeout(duration time.Duration) error
}
客户端的实现如下(基于Wrapper):
type RateLimitInterceptor struct {
//内嵌了defaultClientInterceptor,defaultClientInterceptor类实现了Wrapper的所有方法,我们只需要覆盖自己需要实现的方法即可
defaultClientInterceptor
Limit ratelimit.RateLimiter
}
var ErrRateLimited = errors.New("request limited")
func (r *RateLimitInterceptor) WrapCall(option *SGOption, callFunc CallFunc) CallFunc {
return func(ctx context.Context, ServiceMethod string, arg interface{}, reply interface{}) error {
if r.Limit != nil {
//进行尝试获取,获取失败时直接返回限流异常
if r.Limit.TryAcquire() {
return callFunc(ctx, ServiceMethod, arg, reply)
} else {
return ErrRateLimited
}
} else {//若限流器为nil则不进行限流
return callFunc(ctx, ServiceMethod, arg, reply)
}
}
}
func (r *RateLimitInterceptor) WrapGo(option *SGOption, goFunc GoFunc) GoFunc {
return func(ctx context.Context, ServiceMethod string, arg interface{}, reply interface{}, done chan *Call) *Call {
if r.Limit != nil {
//进行尝试获取,获取失败时直接返回限流异常
if r.Limit.TryAcquire() {
return goFunc(ctx, ServiceMethod, arg, reply, done)
} else {
call := &Call{
ServiceMethod: ServiceMethod,
Args:arg,
Reply: nil,
Error: ErrRateLimited,
Done: done,
}
done <- call
return call
}
} else {//若限流器为nil则不进行限流
return goFunc(ctx, ServiceMethod, arg, reply, done)
}
}
}
服务端的限流实现如下(基于Wrapper):
type RequestRateLimitInterceptor struct {
//这里内嵌了defaultServerInterceptor,defaultServerInterceptor类实现了Wrapper的所有方法,我们只需要覆盖自己需要实现的方法即可
defaultServerInterceptor
Limiter ratelimit.RateLimiter
}
func (rl *RequestRateLimitInterceptor) WrapHandleRequest(s *SGServer, requestFunc HandleRequestFunc) HandleRequestFunc {
return func(ctx context.Context, request *protocol.Message, response *protocol.Message, tr transport.Transport) {
if rl.Limiter != nil {
//进行尝试获取,获取失败时直接返回限流异常
if rl.Limiter.TryAcquire() {
requestFunc(ctx, request, response, tr)
} else {
s.writeErrorResponse(response, tr, "request limited")
}
} else {//如果限流器为nil则直接返回
requestFunc(ctx, request, response, tr)
}
}
}
可以看到这里的限流逻辑非常简单,只支持全局限流,没有区分各个方法,但要支持也很简单,在Wrapper里维护一个方法到限流器的map,在限流时根据具体的方法名获取不同的限流器进行限流判断即可;同时这里限流也是基于单机的,不支持集群限流,要支持集群级别的限流需要独立的数据源进行次数统计等等,这里暂时不涉及了。
链路追踪
链路追踪在大型分布式系统中可以有效地帮助我们进行故障排查、性能分析等等。链路追踪通常包括三部分工作:数据埋点、数据收集和数据展示,而到RPC框架这里实际上就只涉及数据埋点了。目前业界有许多链路追踪的产品,而他们各自的api和实现都不一样,要支持不同的产品需要做很多额外的兼容改造工作,于是就有了opentracing规范。opentracing旨在统一各个不同的追踪产品的api,提供标准的接入层。而我们这里就直接集成opentracing,用户可以在使用时绑定到不同的opentracing实现,比较主流的opentracing实现有zipkin和jaeger。
客户端链路追踪的实现(同样基于Wrapper):
//目前只做了同步调用支持
type OpenTracingInterceptor struct {
defaultClientInterceptor
}
func (*OpenTracingInterceptor) WrapCall(option *SGOption, callFunc CallFunc) CallFunc {
return func(ctx context.Context, ServiceMethod string, arg interface{}, reply interface{}) error {
var clientSpan opentracing.Span
if ServiceMethod != "" { //不是心跳的请求才进行追踪
//先从当前context获取已存在的追踪信息
var parentCtx opentracing.SpanContext
if parent := opentracing.SpanFromContext(ctx); parent != nil {
parentCtx = parent.Context()
}
//开始埋点
clientSpan := opentracing.StartSpan(
ServiceMethod,
opentracing.ChildOf(parentCtx),
ext.SpanKindRPCClient)
defer clientSpan.Finish()
meta := metadata.FromContext(ctx)
writer := &trace.MetaDataCarrier{&meta}
//将追踪信息注入到metadata中,通过rpc传递到下游
injectErr := opentracing.GlobalTracer().Inject(clientSpan.Context(), opentracing.TextMap, writer)
if injectErr != nil {
log.Printf("inject trace error: %v", injectErr)
}
ctx = metadata.WithMeta(ctx, meta)
}
err := callFunc(ctx, ServiceMethod, arg, reply)
if err != nil && clientSpan != nil {
clientSpan.LogFields(opentracingLog.String("error", err.Error()))
}
return err
}
}
服务端链路追踪的实现(同样基于Wrapper):
type OpenTracingInterceptor struct {
defaultServerInterceptor
}
func (*OpenTracingInterceptor) WrapHandleRequest(s *SGServer, requestFunc HandleRequestFunc) HandleRequestFunc {
return func(ctx context.Context, request *protocol.Message, response *protocol.Message, tr transport.Transport) {
if protocol.MessageTypeHeartbeat != request.MessageType {
meta := metadata.FromContext(ctx)
//从metadata中提取追踪信息
spanContext, err := opentracing.GlobalTracer().Extract(opentracing.TextMap, &trace.MetaDataCarrier{&meta})
if err != nil && err != opentracing.ErrSpanContextNotFound {
log.Printf("extract span from meta error: %v", err)
}
//开始服务端埋点
serverSpan := opentracing.StartSpan(
request.ServiceName + "." + request.MethodName,
ext.RPCServerOption(spanContext),
ext.SpanKindRPCServer)
defer serverSpan.Finish()
ctx = opentracing.ContextWithSpan(ctx, serverSpan)
}
requestFunc(ctx, request, response, tr)
}
}
可以看到我们实现链路追踪的逻辑主要就是两部分:
- 根据请求方法名等信息生成链路信息
- 通过rpc metadata传递追踪信息
前面也提到了,RPC框架的工作也仅限于数据埋点而已,剩下的数据收集和数据展示部分需要依赖具体的产品。用户需要在程序里设置具体的实现,类似这样:
//mocktracker是mock的追踪,只限于测试目的使用
opentracing.SetGlobalTracer(mocktracer.New())
//或者使用jaeger
import (
"github.com/uber/jaeger-client-go/config"
"github.com/uber/jaeger-lib/metrics/prometheus"
)
metricsFactory := prometheus.New()
tracer, closer, err := config.Configuration{
ServiceName: "your-service-name",
}.NewTracer(
config.Metrics(metricsFactory),
)
//设置tracer
opentracing.SetGlobalTracer(tracer)
基于标签的路由策略
最后我们来实现基于服务端元数据的规则路由,用户在实际使用过程中,肯定有一些特殊的路由要求,比如“我们的服务运行在不同的idc,我希望能够尽量保证同idc相互调用”,或者“我希望能够在运行时切断某个服务提供者的流量”,这些需求都可以抽象成基于标签的路由。我们给每个服务提供者都打上不同的标签,客户端在调用时会根据自己的需要过滤出符合某些标签的服务提供者。
而标签的具体实现就是将标签放到服务提供者的元数据里,这些元数据会被注册到注册中心,也会被客户端服务发现时获取到,客户端在调用前进行过滤即可。
代码实现:
//服务端注册时,将我们设置的tags作为元数据注册到注册中心
func (w *DefaultServerWrapper) WrapServe(s *SGServer, serveFunc ServeFunc) ServeFunc {
return func(network string, addr string, meta map[string]interface{}) error {
//省略注册shutdownHook的逻辑
...
if meta == nil {
meta = make(map[string]interface{})
}
//注入tags
if len(s.Option.Tags) > 0 {
meta["tags"] = s.Option.Tags
}
meta["services"] = s.Services()
provider := registry.Provider{
ProviderKey: network + "@" + addr,
Network: network,
Addr: addr,
Meta: meta,
}
r := s.Option.Registry
rOpt := s.Option.RegisterOption
r.Register(rOpt, provider)
log.Printf("registered provider %v for app %s", provider, rOpt)
return serveFunc(network, addr, meta)
}
}
//客户端实现,基于tags进行过滤
func TaggedProviderFilter(tags map[string]string) Filter {
return func(provider registry.Provider, ctx context.Context, ServiceMethod string, arg interface{}) bool {
if tags == nil {
return true
}
if provider.Meta == nil {
return false
}
providerTags, ok := provider.Meta["tags"].(map[string]string)
if !ok || len(providerTags) <= 0{
return false
}
for k, v := range tags {
if tag, ok := providerTags[k];ok {
if tag != v {
return false
}
} else {
return false
}
}
return true
}
}
这里的实现当中,服务端和客户端的标签在注册前就已经设置好了,只能满足比较简单的策略,后续再考虑实现运行时修改标签的支持了。
结语
今天的内容就到此为止了,实际上我们的很多功能都是基于最开始定义的Wrapper实现的拦截器来完成的。这样设计的好处就是能保证对扩展开放,对修改关闭,也就是开闭原则,我们在扩充时可以完全不影响之前的逻辑。但是这种基于高阶函数的实现有个不方便的地方就是debug时比较困难,不容易找到具体的实现逻辑,不知道有没有更好的解决方式。