前言
相信看过了《实操篇》后,可以假设我们已经把某个模块 A 的 grpc server 与 client 都实现出来(既是所谓的“微服务化”),那么我们就来聊聊在上文结尾所提到的“可回滚的client”。
回滚方案
也许你会问,都已经整出了 client ,为什么还要这样多此一举?
其中最大的考量是,当这个微服务化后的业务模块出问题了,我们能怎么办?
别说什么“不可能出错”,我又不是高德纳老爷子(《计算机程序设计艺术》系列丛书的作者),我很清楚自己几斤几两。
假设这业务模块已经上线了,好巧不巧出问题了。
这时你的第一反应也许是:马上回滚到上一个版本,等修好后再更回来?
然而实际是,就这回滚的过程,从停止现版本所需的组件,准备上一版本的运行空间与所需组件到最后启动上一版本,需要一段时间。
这样的时间,不问长短,都能让我们坐立不安。
那么现在有这么一个 client ,组合了本地版本与微服务化版本,使用一个“开关”控制着流量。
这 client 平常跑着肯定是微服务化版本,如果出了问题了,只用拨动这个“开关”,就能让本地版本顶上来,换下微服务版本去“维修” 。
这么方便的 client ,不想试试吗?
构造方法
我们先来看看这样的 client 要如何构建
假设我们的“源文件”是这样的:
type PersonAction interface {
SayHello(anybody string)
SayGoodBye(anybody string)
}
type Person struct {
name string
gender string
age int
}
func NewPerson(name, gender string, age int) *Person {
return &Person{
name: name,
gender: gender,
age: age,
}
}
func (p *Person) SayHello(anybody string) {
fmt.Printf("%s say \"Hello\" to %s", p.name, anybody)
}
func (p *Person) SayGoodBye(anybody string) {
fmt.Printf("%s say \"Good bye\" to %s", p.name, anybody)
}
注意上面的 PersonAction ,我们这样构建本地客户端:
type PersonLocalAdapter struct {
gp GRPC_test.PersonAction
}
再根据这个 struct 实现出 grpc 生成 client 接口,像这样:
func (p *PersonLocalAdapter) SayHello(ctx context.Context, in *personv1.SayHelloRequest, opts ...grpc.CallOption) (*personv1.SayHelloResponse, error) {
p.gp.SayHello(in.GetAnybody())
return &personv1.SayHelloResponse{}, nil
}
func (p *PersonLocalAdapter) SayGoodBye(ctx context.Context, in *personv1.SayGoodByeRequest, opts ...grpc.CallOption) (*personv1.SayGoodByeResponse, error) {
p.gp.SayGoodBye(in.GetAnybody())
return &personv1.SayGoodByeResponse{}, nil
}
这个 PersonLocalAdapter 就是我们的本地客户端。
grpc 的客户端就是由 grpc 生成的,就是现成的东西,那么我们就该想想这个能控制流量,甚至支持回滚的开关要怎么实现?
我们不妨试试随机数 + 阈值组合,就是进来的调用带着一个随机数,当这个随机数小于 client 内部设定好的阈值,那么就走微服务调用,否则就走本地调用。
这是参考的实现:
type PersonGreyScaleClient struct {
// 本地
local *PersonLocalAdapter
// grpc
remote personv1.PersonActionClient
// 阈值
threshold *atomicx.Value[int32]
}
接着就是带随机数的调用:
func (p *PersonGreyScaleClient) client() personv1.PersonActionClient {
threshold := p.threshold.Load()
num := rand.Int31n(100)
if num < threshold {
return p.remote
}
return p.local
}
最后就是基于 PersonGreyScaleClient 带上 client() 方法把 grpc 的 client 给实现出来就好,就像这样:
func (p *PersonGreyScaleClient) SayHello(ctx context.Context, in *personv1.SayHelloRequest, opts ...grpc.CallOption) (*personv1.SayHelloResponse, error) {
return p.client().SayHello(ctx, in, opts...)
}
func (p *PersonGreyScaleClient) SayGoodBye(ctx context.Context, in *personv1.SayGoodByeRequest, opts ...grpc.CallOption) (*personv1.SayGoodByeResponse, error) {
return p.client().SayGoodBye(ctx, in, opts...)
}
结语
到这里,就可以针对 PersonGreyScaleClient 写测试,看看会不会有 bug ,以及在使用过程中观察其运行状态,如果运行良好的话,就可以渐渐把流量都调给微服务调用。
等哪天感觉万无一失了,就可以把 local *PersonLocalAdapter 给去掉,完成该模块的彻底微服务化。