流量染色是指根据流量协议设置对应的流量染色规则,对指定的流量进行染色标记,并在整个调用链中携带该标记。通过染色流量可以对特定的流量进行跟踪和路由,所以流量染色功能常被用于灰度发布的场景。在业务系统迭代过程中会不断有新版本发布,在正式发布前,可以使用流量染色控制先进行小规模验证,通过收集使用体验的数据,对应用新版本的功能、性能、稳定性等指标进行评判,然后再全量升级。即使某个新版本出现问题,也只会影响已染色流量,不会将问题蔓延至整个系统,保证整个系统的正常运行。
同理,流量染色功能还可以用于大促前的性能压测。在线上压测的场景中,为了让压测数据和正式的线上数据实现隔离,常用的方法是对于消息队列,缓存,数据库使用影子的方式。这就需要流量染色的技术,带一个tag进去,说明这个请求是测试数据,还是真实数据。
此外,流量染色功能还可以用于多测试环境的治理。在大规模微服务场景下,不可能每个部门部署一套完整的环境,因为耗费的资源量实在是太大了。这时候就需要合理规划测试环境,可以建立一个基准测试环境,对应Master分支,里面部署全量的应用。每一个分支对应有更新的模块,比如说你修改了五个工程,测试的时候,不需要部署全量的应用,只需要把这五个工程去创建一个Delta测试环境就可以了。
当客户端进行测试的时候,通过流量染色标记不同的测试分支流量,将该流量路由至测试版本。当这五个服务之内相互调用的时候,微服务框架就会选择这五个服务的实例进行调用,如果需要调用五个服务之外的其他服务的时候,微服务框架会到Master环境里面,选择服务实例进行调用。有了流量染色的环境治理机制,测试环境数量会大大减少。
给入站请求绑定上下文(如: http header), in-process 使用 context 传递,跨服务使用 metadata 传递在这个架构中每一个基础组件都能够理解染色信息,并且能够基于染色路由隔离流量
依赖resolver注册组件以及balancer负载均衡组件
一.首先需要将染色信息注册到etcd等注册发现中心
ip := "127.0.0.1" // NOTE: 必须拿到您实例节点的真实IP,
port := "9000" // NOTE: 必须拿到您实例grpc监听的真实端口
hn, _ := os.Hostname()
dis,err := etcd.New(&clientv3.Config{Endpoints:[]string{"127.0.0.1"}})
if err != nil {
panic(err)
}
ins := &naming.Instfszance{
AppID: "test",
Addrs: []string{
"grpc://" + ip + ":" + port,
},
Metadata : map[string]string{"color":env.Color},
}
cancel, err := etcd.Register(context.Background(), ins)
完成服务注册,如test服务,如果有不同测试分支,则数据有多条,同一个appid不用addr和color
二.实现resolver组件Build方法
addrs := make([]resolver.Address, 0, len(instances))
for _, ins := range instances {
var rpc string
for _, a := range ins.Addrs {
u, err := url.Parse(a)
if err == nil && u.Scheme == Scheme {
rpc = u.Host
}
}
addr := resolver.Address{
Addr: rpc,
Type: resolver.Backend,
ServerName: ins.AppID,
Metadata: wmeta.MD{Weight: uint64(weight), Color: ins.Metadata[naming.MetaColor]},
}
addrs = append(addrs, addr)
}
log.Info("resolver: finally get %d instances", len(addrs))
r.cc.NewAddress(addrs)
主要逻辑思想为从etcd发现信息,而后更新到grpc,resolver.Address里自此存在了color信息
三.实现balancer的Build和Pick方法,从中挑选出你想要的Conn
func (*p2cPickerBuilder) Build(readySCs map[resolver.Address]balancer.SubConn) balancer.Picker {
p := &p2cPicker{
colors: make(map[string]*p2cPicker),
r: rand.New(rand.NewSource(time.Now().UnixNano())),
}
for addr, sc := range readySCs {
meta, ok := addr.Metadata.(wmd.MD)
if !ok {
meta = wmd.MD{
Weight: 10,
}
}
subc := &subConn{
conn: sc,
addr: addr,
meta: meta,
svrCPU: 500,
lag: 0,
success: 1000,
inflight: 1,
}
if meta.Color == "" {
p.subConns = append(p.subConns, subc)
continue
}
// if color not empty, use color picker
cp, ok := p.colors[meta.Color]
if !ok {
cp = &p2cPicker{r: rand.New(rand.NewSource(time.Now().UnixNano()))}
p.colors[meta.Color] = cp
}
cp.subConns = append(cp.subConns, subc)
}
return p
}
type p2cPicker struct {
// subConns is the snapshot of the weighted-roundrobin balancer when this picker was
// created. The slice is immutable. Each Get() will do a round robin
// selection from it and return the selected SubConn.
subConns []*subConn
colors map[string]*p2cPicker
logTs int64
r *rand.Rand
lk sync.Mutex
}
func (p *p2cPicker) Pick(ctx context.Context, opts balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error) {
// FIXME refactor to unify the color logic
color := nmd.String(ctx, nmd.Color)
if color == "" && env.Color != "" {
color = env.Color
}
if color != "" {
if cp, ok := p.colors[color]; ok {
return cp.pick(ctx, opts)
}
}
return p.pick(ctx, opts)
}
自此则实现了根据染色信息进而路由的功能,那么最后从网关入口处根据不通的流量,标记不通的染色信息,带入go中的context,流量染色整体的功能就实现了。