协作开发与灰度发布是微服务框架在流量治理能力方面的两个体现,本文结合go-micro实践对流量进行染色,实现开发环境的多分支协作, 以及生产环境的灰度发布。

场景

开发环境多服务、多分支协作

micro-chain-dev

  • QA组测试v1.2v2.0链路
    • v2.0 + v1.2链路
  • v1.1组仅关注v1.1的版本开发
    • v1.1 + master链路
  • v1.2组在v1.1开发新版srv-2服务
    • v1.2 + v1.1 + master链路
  • v2.0组仅关注v2.0的版本开发
    • v2.0 + master链路

生产环境灰度发布

micro-chain-gray

  • 普通用户
    • Pro链路
  • 灰度测试用户
    • Gray + Pro链路
  • QA
    • Pre + Pro链路

流量染色

流量染色核心:

  • gateway对请求进行染色
    • 染色规则可以是hostheader字段、agent终端信息、用户筛选、流量比例等等
  • 染色信息在服务间传递
    • go-microhttp请求的header以及rpc请求的metadata
  • 服务调用时根据染色信息对服务进行筛选,实现调用链路的管控

我们基于go-micro实践的是实现多链路染色,染色链路带有优先级,如开发环境多服务、多分支协作v1.2组, 虽然v1.1v1.2都有srv-2服务,但我们在染色信息中v1.2在前优先选择,所以可以实现多分支同时染色(PS:如果两个分支中两个服务的优先级相反无法实现,需要设计更复杂的染色方案)

网关染色及client wrapper实现参考我实现的两个chain插件

染色

在网关对流量进行染色,基于mciro的插件,可以方便的实现,具体染色规则需要根据自身需求实现。

// 链路染色
api.Register(chain.New(chain.WithChainsFunc(func(r *http.Request) []string {
	return []string{"v2.0", "v1.2"}
})))

web.Register(chain.New(chain.WithChainsFunc(func(r *http.Request) []string {
	return []string{"v2.0"}
})))

调用链路管控

go-micro实现调用链路管控,最大的障碍就是网关APIWeb均不支持服务筛选,需要自己二次开发,相关问题也反馈给社区看后续计划#1003

网关服务筛选-坑

  • 此方法仅用于测试,具体原因 vtolstov 在社区提的PR#1388有 asim 的反馈
  • 自定义 Router 实现网关对服务筛选的支持,实现参考我 fork 的分支版本 hb-chen/micro/gateway

API网关仅尝试了api handler,修改相对简单,只需要增加ClientWrapper,并去掉指定的负载策略即可。如果使用其他handler需要逐个解决。

1. micro/main.go添加ClientWrapper

func main() {
	cmd.Init(
		// 链路染色
		micro.WrapClient(chain.NewClientWrapper()),
	)
}

2. go-micro/api/handler/api.go去掉strategyrpc handler类似

// create strategy
// so := selector.WithStrategy(strategy(service.Services))

// if err := c.Call(cx, req, rsp, client.WithSelectOption(so)); err != nil {
if err := c.Call(cx, req, rsp); err != nil {
	
}

Web网关相对麻烦,不能方便的使用microoptions,暂时测试在web模块实现filter

1. micro/web/web.go修改func (s *srv) proxy() http.Handler,增加SelectOption

func (s *srv) proxy() http.Handler {
	sel := s.service.Client().Options().Selector

	director := func(r *http.Request) {
		kill := func() {
			r.URL.Host = ""
			r.URL.Path = ""
			r.URL.Scheme = ""
			r.Host = ""
			r.RequestURI = ""
		}

		parts := strings.Split(r.URL.Path, "/")
		if len(parts) < 2 {
			kill()
			return
		}
		if !re.MatchString(parts[1]) {
			kill()
			return
		}

		// NOTE: 添加SelectOption
		val := r.Header.Get("X-Micro-Chain")
		chains := strings.Split(val, ";")

		next, err := sel.Select(Namespace+"."+parts[1], selector.WithFilter(filterChain(chains)))
		if err != nil {
			kill()
			return
		}

		s, err := next()
		if err != nil {
			kill()
			return
		}

		r.Header.Set(BasePathHeader, "/"+parts[1])
		r.URL.Host = s.Address
		r.URL.Path = "/" + strings.Join(parts[2:], "/")
		r.URL.Scheme = "http"
		r.Host = r.URL.Host
	}

	return &proxy{
		Default:  &httputil.ReverseProxy{Director: director},
		Director: director,
	}
}

// NOTE: SelectOption的filter实现
func filterChain(chains []string) selector.Filter {
	return func(old []*registry.Service) []*registry.Service {
		if len(chains) == 0 {
			return old
		}

		var services []*registry.Service

		chain := ""
		idx := 0
		for _, service := range old {
			serv := new(registry.Service)
			var nodes []*registry.Node

			for _, node := range service.Nodes {
				if node.Metadata == nil {
					continue
				}

				val := node.Metadata["chain"]
				if len(val) == 0 {
					continue
				}

				if len(chain) > 0 && idx == 0 {
					if chain == val {
						nodes = append(nodes, node)
					}
					continue
				}

				// chains按顺序优先匹配
				ok, i := inArray(val, chains)
				if ok && idx > i {
					// 出现优先链路,services清空,nodes清空
					idx = i
					services = services[:0]
					nodes = nodes[:0]
				}

				if ok {
					chain = val
					nodes = append(nodes, node)
				}
			}

			// only add service if there's some nodes
			if len(nodes) > 0 {
				// copy
				*serv = *service
				serv.Nodes = nodes
				services = append(services, serv)
			}
		}

		if len(services) == 0 {
			return old
		}

		return services
	}
}

func inArray(s string, d []string) (bool, int) {
	for k, v := range d {
		if s == v {
			return true, k
		}
	}
	return false, 0
}

服务筛选

普通服务的筛选框架支持没有什么问题,首先要为服务添加metadata信息,不赘述。其次要对Client进行包装,服务调用时添加SelectOption, 实现参考我的chain插件。

1. 添加metadata

md := make(map[string]string)
md["chain"] = "v2.0"

// New Service
service := micro.NewService(
	micro.Name("go.micro.api.xxx"),
	micro.Version("latest"),
	micro.Metadata(md),
)

2. Wrap Client

需要注意的是service初始过程,有类似micro new模板将service client注入到context的方法,需要分两次Init(),先WrapClient,再WrapHandler, 否则注入的client将是未被包装的。这也是micro比较常遇到的

// Initialise service
// NOTE: 注意有类似micro new模块将service client注入到context的方法,需要分两次Init(),否则注入的Client并未被包装
service.Init(
	micro.WrapClient(chain.NewClientWrapper()),
)
service.Init(
	// create wrap for the Example srv client
	micro.WrapHandler(client.AccountWrapper(service)),
)

Ref