什么是流量染色

流量染色是指根据流量协议设置对应的流量染色规则,对指定的流量进行染色标记,并在整个调用链中携带该标记。通过染色流量可以对特定的流量进行跟踪和路由,所以流量染色功能常被用于灰度发布的场景。在业务系统迭代过程中会不断有新版本发布,在正式发布前,可以使用流量染色控制先进行小规模验证,通过收集使用体验的数据,对应用新版本的功能、性能、稳定性等指标进行评判,然后再全量升级。即使某个新版本出现问题,也只会影响已染色流量,不会将问题蔓延至整个系统,保证整个系统的正常运行。

同理,流量染色功能还可以用于大促前的性能压测。在线上压测的场景中,为了让压测数据和正式的线上数据实现隔离,常用的方法是对于消息队列,缓存,数据库使用影子的方式。这就需要流量染色的技术,带一个tag进去,说明这个请求是测试数据,还是真实数据。

此外,流量染色功能还可以用于多测试环境的治理。在大规模微服务场景下,不可能每个部门部署一套完整的环境,因为耗费的资源量实在是太大了。这时候就需要合理规划测试环境,可以建立一个基准测试环境,对应Master分支,里面部署全量的应用。每一个分支对应有更新的模块,比如说你修改了五个工程,测试的时候,不需要部署全量的应用,只需要把这五个工程去创建一个Delta测试环境就可以了。

当客户端进行测试的时候,通过流量染色标记不同的测试分支流量,将该流量路由至测试版本。当这五个服务之内相互调用的时候,微服务框架就会选择这五个服务的实例进行调用,如果需要调用五个服务之外的其他服务的时候,微服务框架会到Master环境里面,选择服务实例进行调用。有了流量染色的环境治理机制,测试环境数量会大大减少。

grpc通信模式流 grpc 流量控制_bc

 

染色信息的传递


给入站请求绑定上下文(如: http header), in-process 使用 context 传递,跨服务使用 metadata 传递在这个架构中每一个基础组件都能够理解染色信息,并且能够基于染色路由隔离流量


grpc通信模式流 grpc 流量控制_测试环境_02

 

grpc怎么实现流量染色

依赖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,流量染色整体的功能就实现了。