项目背景

在生产环境中,我们为了验证新版本的发布,一般会将部分新版本的服务交付生产环境,让它们与上个版本的服务共同为上层请求方服务。此时由于还未经过生产验证,所以可以通过部分引流将请求拿过来消费,如下图
基于go-micro做灰度发布解决方案 - 图1

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

      rpc流量染色

      如下图,我们在k8s中部署了srv三个实例,两个当前正服务的实例srv-old(v0.0.1),此时我们想发布一个灰度实例(v0.0.2)
      image.png
      我们可以在api这个上级应用的配置中心里添加热配置,如下:
      1. gray.switch = true #灰度发布开关
      2. gray.service = srv #灰度发布的服务
      3. gray.version = v0.0.2
      4. gray.option = mod #操作 对某个值取模mod , 或 判断包含
      5. gray.target = 100 #option为mod时:target标示mod基数,比如*mod10 ,option为contain时 ,target标示[pre,endWith,equal]
      6. gray.key = userId #分流的关键字段
      7. gray.result = 1,2,3 #操作对比的结果集

micro客户端插件实现:


//灰度发布-流量染色
type grayscaleWrapper struct {
    client.Client
}

func (w *grayscaleWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
    //判断当前是否是需要灰度发布的请求
    if apollo.C().GetByDefaultNamespace("gray.switch") == "true" && apollo.C().GetByDefaultNamespace("gray.service") == req.Service() {
        nOpts := append(opts, client.WithSelectOption(
            selector.WithFilter(w.filterChain(req)),
        ))
        return w.Client.Call(ctx, req, rsp, nOpts...)
    }

    return w.Client.Call(ctx, req, rsp, opts...)
}

func (w *grayscaleWrapper) filterChain(req client.Request) selector.Filter {
    return func(old []*registry.Service) []*registry.Service {
        defer func() {
            if r := recover(); r != nil {
                log.Error(r)
                return
            }
        }()
        var services []*registry.Service

        switch apollo.C().GetByDefaultNamespace("gray.option") {

        case "mod":
            {
                key := apollo.C().GetByDefaultNamespace("gray.key")
                if len(key) == 0 {
                    return old
                }
                gTarget := apollo.C().GetByDefaultNamespace("gray.target")
                if len(gTarget) == 0 {
                    return old
                }
                gTargetInt, err := strconv.Atoi(gTarget)
                if err != nil {
                    log.Error(err)
                    return old
                }
                value, ok := req.Body().(map[string]interface{})[key]
                if !ok {
                    log.Error("gray.key : " + key + " non-existent")
                    return old
                }
                vInt, err := strconv.Atoi(fmt.Sprintf("%d", value))
                if err != nil {
                    log.Error(err)
                    return old
                }
                t := vInt % gTargetInt
                //判断是否满足条件
                gResult := apollo.C().GetByDefaultNamespace("gray.result")
                if len(gResult) == 0 {
                    return old
                }
                gResultArr := strings.Split(gResult, ",")
                tV := strconv.Itoa(t)
                ok = false
                for _, v := range gResultArr {
                    if tV == v {
                        ok = true
                        break
                    }
                }
                if !ok {
                    return old
                }
                //挑选灰度版本
                gVersion := apollo.C().GetByDefaultNamespace("gray.version")
                for _, srv := range old {
                    if srv.Version == gVersion {
                        services = append(services, srv)
                    }
                }
            }
            break
        case "contain":
            {
                key := apollo.C().GetByDefaultNamespace("gray.key")
                if len(key) == 0 {
                    return old
                }
                gTarget := apollo.C().GetByDefaultNamespace("gray.target")  //pre endWith equal
                if len(gTarget) == 0 {
                    return old
                }

                vPara, ok := req.Body().(map[string]interface{})[key]
                if !ok {
                    log.Error("gray.key : " + key + " non-existent")
                    return old
                }
                value:=fmt.Sprintf("%s",vPara)

                //判断是否满足条件
                gResult := apollo.C().GetByDefaultNamespace("gray.result")
                if len(gResult) == 0 {
                    return old
                }
                gResultArr := strings.Split(gResult, ",")
                ok = false
                switch gTarget {
                case "pre":
                    {
                        for _, v := range gResultArr {
                            if strings.HasPrefix(value,v) {
                                ok = true
                                break
                            }
                        }
                    }
                    break
                case "endWith":
                    {
                        for _, v := range gResultArr {
                            if strings.HasSuffix(value,v) {
                                ok = true
                                break
                            }
                        }
                    }
                    break
                default:
                    {
                        for _, v := range gResultArr {
                            if value==v {
                                ok = true
                                break
                            }
                        }
                    }
                }

                if !ok {
                    return old
                }
                //挑选灰度版本
                gVersion := apollo.C().GetByDefaultNamespace("gray.version")
                for _, srv := range old {
                    if srv.Version == gVersion {
                        services = append(services, srv)
                    }
                }
            }
            break


        default:
            return old
        }

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

        return services
    }
}

func NewClientWrapper() client.Wrapper {
    return func(c client.Client) client.Client {
        return &grayscaleWrapper{
            Client: c,
        }
    }
}

客户端api添加
image.png