流量染色是指根据流量协议设置对应的流量染色规则,对指定的流量进行染色标记,并在整个调用链中携带该标记。通过染色流量可以对特定的流量进行跟踪和路由,所以流量染色功能常被用于灰度发布的场景。在业务系统迭代过程中会不断有新版本发布,在正式发布前,可以使用流量染色控制先进行小规模验证,通过收集使用体验的数据,对应用新版本的功能、性能、稳定性等指标进行评判,然后再全量升级。即使某个新版本出现问题,也只会影响已染色流量,不会将问题蔓延至整个系统,保证整个系统的正常运行。
同理,流量染色功能还可以用于大促前的性能压测。在线上压测的场景中,为了让压测数据和正式的线上数据实现隔离,常用的方法是对于消息队列,缓存,数据库使用影子的方式。这就需要流量染色的技术,带一个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,流量染色整体的功能就实现了。