再见,micro: 迁移go-micro到纯gRPC框架

再见,micro: 迁移go-micro到纯gRPC框架

micro是基于golang的微服务框架,之前华尔街见闻架构升级中谈到了我们是基于go-micro的后端架构,随着我们对服务网格的调研、测试和实施,为了打通不同语言之间的服务调用,我们选择了gRPC作为服务内部的通用协议。

go-micro框架的架构非常具有拓展性,它拥有自己的RPC框架,通过抽象codec,transport,selector等微服务组件,你既可以使用官方实现的各种插件go-plugins进行组装,又可以根据实际的情况实现自己的组件。然而,我们打算利用服务网格的优势,将微服务的基础组件下沉到基础设施中去,将组件代码从代码库中剥离开来。

这样一来,我们相当于只需要最简的RPC框架,只需要服务之间有统一、稳定、高效的通信协议,由于micro在我们新架构中略显臃肿,于是我们选择逐渐剥除micro。还有一个重要原因,我们选择的服务网格方案是istio,它的代理原生支持gRPC,而micro只是将gRPC当做transport层,相当于复写了gRPC的服务路由逻辑,这样有损于istio的一些特性,譬如流量监控等功能。

出于这些考虑,第一步需要将micro改成纯gRPC模式,这里的改造部分我们考虑只应该去更改基础库的代码,而尽量不要使业务代码更改,减少对已有逻辑的影响,和一些软性的譬如开发人员的工作量。

服务发现模块

istio的服务发现支持etcd、consul等,我们需要将其改成使用kubernetes的服务名进行访问。通过实现istio selector的方式,我们将RPC调用的服务名与k8s的服务名端口做映射。

register.go
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
package istio

import (
"fmt"

"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/selector"
)

const (
svcPort = 10088
)

var (
serviceHostMapping = map[string]string{
"payment": "payment.common",
"content": "content.ns1",
"user": "user.ns1",
}
)

type istio struct{}

func (r *istio) Select(service string, opts ...selector.SelectOption) (selector.Next, error) {
if host, exist := serviceHostMapping[service]; exist {
node := &registry.Node{
Id: service,
Address: host,
Port: svcPort,
}
return func() (*registry.Node, error) {
return node, nil
}, nil
}
return nil, fmt.Errorf("service %s(%s) not found", service, svc)
}

...

这里注意的是由于服务之间调用需要指定service name和端口,所以在这里我们把端口设置了一个magic number。

传输模块

go-micro采用各种协议作为传输报文的工具,可以从transport中了解到,有http/grpc/tcp/utp等,我们曾先后使用过tcp、utp、gRPC,经过测试gRPC是其中最为稳定的。之前提到过gRPC只是作为传输信道,micro定义了自己的RPC接口,实现了RPC的路由、传输、重试等功能,通过自定义了protobuf的插件生成符合micro标准的proto文件。

为了向后兼容,在使用grpc替换原RPC时,我也需要根据protobuf文件生成新的golang代码,其中包括client端、server端代码的变更,实际上我的更新是针对protoc-gen-go,fork之后修改grpc/grpc.go部分,在func generateService()中对Client端代码进行micro的适配。

Client

1
func (c *someClient) DoSomething(ctx context.Context, in *SomeRequest, opts ...grpc.CallOption) (*SomeResponse, error)

Server

Server端代码的更新除了接口的适配,有一个关注点是micro的接口设计是

1
DoSomething(context.Context, *SomeRequest, *SomeResponse) error

而gRPC是

1
DoSomething(context.Context, *SomeRequest) (*SomeResponse, error)

micro接口设计的好处是支持服务端缓存的应用,当服务端handler触发时,cache interceptor可以将response缓存,当缓存命中时可以将其返回。而由于gRPC的版本将response放在了返回值中,运行时无法将譬如redis中字符串格式的response解码成SomeResponse,而micro版本由于将其放在了参数位置,所以可以通过

1
json.Unmarshall([]byte(""), &SomeResponse{})

从字符串恢复response。

我们的做法是不改变接口的设计,而是在自动生成的golang代码中,在interceptor运行之前将response object塞入context。

1
2
3
4
5
6
// Server
ctx = ctx.WithValue("IstioResponseBody", &SomeResponse{})

// Interceptor
obj := ctx.Value("IstioResponseBody")
json.Unmarshall([]byte(""), obj)

Interceptor模块

一些interceptor的迁移工作,例如logger、cache、recover等。

注意事项

  1. 向后兼容
    更新的过程开发人员实际上并不是特别感兴趣,我们SRE能做的是对他们基本上无改动,第一个版本不要让开发感知。虽然实际上测试时由于我们的变动,测试环境不断受到稳定性质疑。

  2. micro和gRPC的context传递不同
    gRPC是基于http2,metadata是基于HTTP header传递,Client利用metadata.NewOutgoingContext(ctx, MD)携带metadata,而Server端利用metadata.FromIncomingContext(ctx)提取metadata。而micro则用context.WithValue(metaKey{}, MD)传递。

  3. 使用环境变量做一些特殊处理
    我们通过环境变量在基础库里做了一些判断,在新老架构并存下进行一些特殊处理,譬如刚才说到的metadata获取逻辑。

  4. 关于proto自动生成
    我们通过在CI中配置istio版本镜像的编译打包逻辑和原有逻辑共存,在编译前我们运行proto编译工具,生成gRPC的golang文件,从而打出新的镜像。

总结

micro陪伴了我们一年多的时间,期间它的架构设计给了我们很大的弹性可以去适配一些我们的架构,可以选择很多好的开源工具,譬如zipkin、prometheus、etcd等,适合于刚上微服务不久的项目进行技术摸索和选择,而这一年的时间,我们的架构也逐渐明晰和稳定,我们更倾向于一个精简的基础库,并且由于在线项目的不断增加,运维成本可以通过下沉基础组件进行降低。

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×