龙空技术网

如何用20分钟就能获得同款企业级全链路灰度能力?

大数据工程师必读 483

前言:

现时姐妹们对“灰度发布怎么实现”大体比较关怀,你们都想要学习一些“灰度发布怎么实现”的相关内容。那么小编同时在网络上收集了一些关于“灰度发布怎么实现””的相关资讯,希望朋友们能喜欢,你们快快来了解一下吧!

简介:MSE 微服务引擎将推出服务治理专业版,提供开箱即用且完整专业的微服务治理解决方案,帮助企业更好地实现微服务治理能力。如果您的系统也可以像本文描述的那样,快速具备完整的全链路灰度能力,并基于该能力进行进一步的微服务治理实践,不仅可以节省客观的人力与成本,还可以让您的企业在微服务领域的探索更加有底气。

作者:十眠

审核&校对:望陶、亦盏

编辑&排版:雯燕

今年双 11,云原生中间件完成了开源、自研、商业化三位一体,全面升级到中间件云产品。MSE 微服务治理通过 Dubbo3.0 支撑了阿里集团核心业务双 11 的流量洪峰,截止目前集团内 50% 的用户已经习惯使用 MSE 微服务治理 HSF 和 Dubbo3.0 应用,今天我们来细聊一下 MSE 服务治理专业版中的全链路灰度能力,以及它在生产大规模实践的一些场景。

背景

微服务架构下,有一些需求开发,涉及到微服务调用链路上的多个微服务同时发生了改动,需要通过灰度发布方式来更好地控制新版本服务上线的风险和爆炸半径。通常每个微服务都会有灰度环境或分组来接受灰度流量,我们希望通过进入上游灰度环境的流量,也能进入下游灰度的环境中,确保 1 个请求始终在灰度环境中传递,即使这个调用链路上有一些微服务没有灰度环境,这些应用请求下游的时候依然能够回到灰度环境中。通过 MSE 提供的全链路灰度能力,可以在不需要修改任何您的业务代码的情况下,能够轻松实现上述能力。

MSE 微服务治理全链路灰度特点

全链路灰度作为 MSE 服务治理专业版中的拳头功能,具备以下六大特点

可通过定制规则引入精细化流量

除了简单地按照比例进行流量引入外,我们还支持 Spring Cloud 与 Dubbo 流量按规则引入,Spring Cloud 流量可根据请求的 cookie、header、param 参数或随机百分比引入流量,Dubbo 流量可按照服务、方法、参数来引入。

全链路隔离流量泳道

1) 通过设置流量规则对所需流量进行“染色,“染色流量会路由到灰度机器。

2) 灰度流量携带灰度标往下游传递,形成灰度专属环境流量泳道,无灰度环境应用会默认选择未打标的基线环境。

端到端的稳定基线环境

未打标的应用属于基线稳定版本的应用,即稳定的线上环境。当我们将发布对应的灰度版本代码,然后可以配置规则定向引入特定的线上流量,控制灰度代码的风险。

流量一键动态切流

流量规则定制后,可根据需求进行一键停启,增删改查,实时生效。灰度引流更便捷。

低成本接入,基于 Java Agent 技术实现无需修改一行业务代码

MSE 微服务治理能力基于 Java Agent 字节码增强的技术实现,无缝支持市面上近 5 年的所有 Spring Cloud 和 Dubbo 的版本,用户不用改一行代码就可以使用,不需要改变业务的现有架构,随时可上可下,没有绑定。只需开启 MSE 微服务治理专业版,在线配置,实时生效。

具备无损上下线能力,使得发布更加丝滑

应用开启 MSE 微服务治理后就具备无损上下线能力,大流量下的发布、回滚、扩容、缩容等场景,均能保证流量无损。

大规模生产实践的场景

本文主要介绍 MSE 微服务治理在支持大客户过程中总结抽象出来的常用的几个全链路灰度方案生产落地实践的场景。

场景一:对经过机器的流量进行自动染色,实现全链路灰度

进入带 tag 的节点后续调用优先选择带有相同 tag 的节点,即对经过 tag 节点的流量进行“染色”。有 tag 的调用链路上找不到相同 tag 的节点,则 fallback 到无 tag 的节点。

有 tag 的调用链路经过无 tag 的节点,如果链路后续调用有 tag 的节点,则恢复 tag 调用的模式。

场景二:通过给流量带上特定的 header 实现全链路灰度

客户端通过在请求中增加制定环境的标识,接入层根据表示进行转发至表示对应环境的网关,对应环境的网关通过隔离插件调用标识对应的项目隔离环境,请求在业务项目隔离环境中闭环。

场景三:通过自定义路由规则来进行全链路灰度

通过在灰度请求中增加指定的 header,且整条调用链路会将该 header 透传下去,只需在对应的应用配置该 header 相关的路由规则,带指定 header 的灰度请求进入灰度机器,即可按需实现全链路流量灰度。

全链路灰度的实践

我们如何快速获得上述同款全链路灰度的能力呢?下面我会带大家从 0 到 1 快速搭建我们的全链路灰度能力。

我们假设应用的架构由 Ingress-nginx 以及后端的微服务架构(Spring Cloud)来组成,后端调用链路有 3 跳,购物车(a),交易中心(b),库存中心(c),他们通过 Nacos 注册中心做服务发现,客户端通过客户端或者是 H5 页面来访问后端服务。

前提条件

安装 Ingress-nginx 组件

访问容器服务控制台,打开应用目录,搜索 ack-ingress-nginx ,选择命名空间 kube-system,点击创建,安装完成后,在 kube-system 命名空间中会看到一个 deployment ack-ingress-nginx-default-controller ,表明安装成功。

$ kubectl get deployment -n kube-systemNAME                                      READY   UP-TO-DATE   AVAILABLE   AGEack-ingress-nginx-default-controller      2/2     2            2           18h

开启 MSE 微服务治理专业版

点击开通 MSE 微服务治理专业版 以使用全链路灰度能力。

访问容器服务控制台,打开应用目录,搜索 ack-mse-pilot ,点击创建。

在 MSE 服务治理控制台,打开 K8s 集群列表,选择对应集群,对应命名空间,并打开微服务治理。

部署 Demo 应用程序

将下面的文件保存到 ingress-gray.yaml 中,并执行 kubectl apply -f ingress-gray.yaml 以部署应用,这里我们将要部署 A, B, C 三个应用,每个应用分别部署一个基线版本和一个灰度版本。

# A 应用 base 版本---apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: spring-cloud-a  name: spring-cloud-aspec:  replicas: 2  selector:    matchLabels:      app: spring-cloud-a  template:    metadata:      annotations:        msePilotCreateAppName: spring-cloud-a      labels:        app: spring-cloud-a    spec:      containers:      - env:        - name: LANG          value: C.UTF-8        - name: JAVA_HOME          value: /usr/lib/jvm/java-1.8-openjdk/jre        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-a:0.1-SNAPSHOT        imagePullPolicy: Always        name: spring-cloud-a        ports:        - containerPort: 20001          protocol: TCP        resources:          requests:            cpu: 250m            memory: 512Mi        livenessProbe:          tcpSocket:            port: 20001          initialDelaySeconds: 10          periodSeconds: 30# A 应用 gray 版本---            apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: spring-cloud-a-new  name: spring-cloud-a-newspec:  replicas: 2  selector:    matchLabels:      app: spring-cloud-a-new  strategy:  template:    metadata:      annotations:        alicloud.service.tag: gray        msePilotCreateAppName: spring-cloud-a      labels:        app: spring-cloud-a-new    spec:      containers:      - env:        - name: LANG          value: C.UTF-8        - name: JAVA_HOME          value: /usr/lib/jvm/java-1.8-openjdk/jre        - name: profiler.micro.service.tag.trace.enable          value: "true"        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-a:0.1-SNAPSHOT        imagePullPolicy: Always        name: spring-cloud-a-new        ports:        - containerPort: 20001          protocol: TCP        resources:          requests:            cpu: 250m            memory: 512Mi        livenessProbe:          tcpSocket:            port: 20001          initialDelaySeconds: 10          periodSeconds: 30# B 应用 base 版本---apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: spring-cloud-b  name: spring-cloud-bspec:  replicas: 2  selector:    matchLabels:      app: spring-cloud-b  strategy:  template:    metadata:      annotations:        msePilotCreateAppName: spring-cloud-b      labels:        app: spring-cloud-b    spec:      containers:      - env:        - name: LANG          value: C.UTF-8        - name: JAVA_HOME          value: /usr/lib/jvm/java-1.8-openjdk/jre        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-b:0.1-SNAPSHOT        imagePullPolicy: Always        name: spring-cloud-b        ports:        - containerPort: 8080          protocol: TCP        resources:          requests:            cpu: 250m            memory: 512Mi        livenessProbe:          tcpSocket:            port: 20002          initialDelaySeconds: 10          periodSeconds: 30# B 应用 gray 版本  ---apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: spring-cloud-b-new  name: spring-cloud-b-newspec:  replicas: 2  selector:    matchLabels:      app: spring-cloud-b-new  template:    metadata:      annotations:        alicloud.service.tag: gray        msePilotCreateAppName: spring-cloud-b      labels:        app: spring-cloud-b-new    spec:      containers:      - env:        - name: LANG          value: C.UTF-8        - name: JAVA_HOME          value: /usr/lib/jvm/java-1.8-openjdk/jre        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-b:0.1-SNAPSHOT        imagePullPolicy: Always        name: spring-cloud-b-new        ports:        - containerPort: 8080          protocol: TCP        resources:          requests:            cpu: 250m            memory: 512Mi        livenessProbe:          tcpSocket:            port: 20002          initialDelaySeconds: 10          periodSeconds: 30# C 应用 base 版本---apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: spring-cloud-c  name: spring-cloud-cspec:  replicas: 2  selector:    matchLabels:      app: spring-cloud-c  template:    metadata:      annotations:        msePilotCreateAppName: spring-cloud-c      labels:        app: spring-cloud-c    spec:      containers:      - env:        - name: LANG          value: C.UTF-8        - name: JAVA_HOME          value: /usr/lib/jvm/java-1.8-openjdk/jre        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-c:0.1-SNAPSHOT        imagePullPolicy: Always        name: spring-cloud-c        ports:        - containerPort: 8080          protocol: TCP        resources:          requests:            cpu: 250m            memory: 512Mi        livenessProbe:          tcpSocket:            port: 20003          initialDelaySeconds: 10          periodSeconds: 30# C 应用 gray 版本---apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: spring-cloud-c-new  name: spring-cloud-c-newspec:  replicas: 2  selector:    matchLabels:      app: spring-cloud-c-new  template:    metadata:      annotations:        alicloud.service.tag: gray        msePilotCreateAppName: spring-cloud-c      labels:        app: spring-cloud-c-new    spec:      containers:      - env:        - name: LANG          value: C.UTF-8        - name: JAVA_HOME          value: /usr/lib/jvm/java-1.8-openjdk/jre        image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-c:0.1-SNAPSHOT        imagePullPolicy: IfNotPresent        name: spring-cloud-c-new        ports:        - containerPort: 8080          protocol: TCP        resources:          requests:            cpu: 250m            memory: 512Mi        livenessProbe:          tcpSocket:            port: 20003          initialDelaySeconds: 10          periodSeconds: 30# Nacos Server---apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: nacos-server  name: nacos-serverspec:  replicas: 1  selector:    matchLabels:      app: nacos-server  template:    metadata:      labels:        app: nacos-server    spec:      containers:      - env:        - name: MODE          value: standalone        image: nacos/nacos-server:latest        imagePullPolicy: Always        name: nacos-server        resources:          requests:            cpu: 250m            memory: 512Mi      dnsPolicy: ClusterFirst      restartPolicy: Always# Nacos Server Service 配置---apiVersion: v1kind: Servicemetadata:  name: nacos-serverspec:  ports:  - port: 8848    protocol: TCP    targetPort: 8848  selector:    app: nacos-server  type: ClusterIP

动手实践

场景一:对经过机器的流量进行自动染色,实现全链路灰度

有时候,我们可以通过不同的域名来区分线上基线环境和灰度环境,灰度环境有单独的域名可以配置,假设我们通过访问 来请求灰度环境,访问 走基线环境。

调用链路 Ingress-nginx -> A -> B -> C ,其中 A 可以是一个 spring-boot 的应用。

注意:入口应用 A 的 gray 和 A 的 base 环境,需要在 MSE 服务治理控制台打开 A 应用的按照流量比例透传开关,表示开启向后透传当前环境的标签的功能。这样, 当 Ingress-nginx 路由 A 的 gray 之后,即使请求中没有携带任何 header,因为开启了此开关,所以往后调用的时候会自动添加 x-mse-tag:gray 这个 header,其中的 header的值 gray 来自于 A 应用配置的标签信息。如果原来的请求中带有 x-mse-tag:gray则会以原来请求中的标签优先。

针对入口应用 A ,配置两个 k8s service, spring-cloud-a-base 对应 A 的 base 版本,spring-cloud-a-gray 对应 A 的 gray 版本。

apiVersion: v1kind: Servicemetadata:  name: spring-cloud-a-basespec:  ports:    - name: http      port: 20001      protocol: TCP      targetPort: 20001  selector:    app: spring-cloud-a---apiVersion: v1kind: Servicemetadata:  name: spring-cloud-a-grayspec:  ports:    - name: http      port: 20001      protocol: TCP      targetPort: 20001  selector:    app: spring-cloud-a-new

配置入口的 Ingress 规则,访问 路由到 A 应用的 base 版本,访问 路由到 A 应用的 gray 版本。

apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  name: spring-cloud-a-basespec:  rules:  - host:     http:      paths:      - backend:          serviceName: spring-cloud-a-base          servicePort: 20001        path: /---apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  name: spring-cloud-a-grayspec:  rules:  - host:     http:      paths:      - backend:          serviceName: spring-cloud-a-gray          servicePort: 20001        path: /

结果验证

此时,访问 路由到 基线环境

curl -H"Host:; A[172.18.144.155] -> B[172.18.144.120] -> C[172.18.144.79]

此时,访问 路由到灰度环境

curl -H"Host:; Agray[172.18.144.160] -> Bgray[172.18.144.57] -> Cgray[172.18.144.157]

进一步的,如果入口应用 A 没有灰度环境,访问到 A 的 base 环境,又需要在 A -> B 的时候进入灰度环境,则可以通过增加一个特殊的 header x-mse-tag 来实现,header 的值是想要去的环境的标签,例如 gray

curl -H"Host:;  -H"x-mse-tag:gray" A[172.18.144.155] -> Bgray[172.18.144.139] -> Cgray[172.18.144.8]

可以看到第一跳,进入了 A 的 base 环境,但是 A->B 的时候又重新回到了灰度环境。

这种使用方式的好处是,配置简单,只需要在 Ingress 处配置好规则,某个应用需要灰度发布的时候,只需要在灰度环境中部署好应用,灰度流量自然会进入到灰度机器中,如果验证没问题,则将灰度的镜像发布到基线环境中;如果一次变更有多个应用需要灰度发布,则把他们都加入到灰度环境中即可。

最佳实践

给所有灰度环境的应用打上 gray 标,基线环境的应用默认不打标。

线上常态化引流 2% 的流量进去灰度环境中

场景二:通过给流量带上特定的 header 实现全链路灰度

有些客户端没法改写域名,希望能访问 通过传入不同的 header 来路由到灰度环境。例如下图中,通过添加 x-mse-tag:gray 这个 header,来访问灰度环境。

个时候 demo 的Ingress 规则如下,注意这里增加了 nginx.ingress.kubernetes.io/canary 相关的多条规则

apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  name: spring-cloud-a-basespec:  rules:  - host:     http:      paths:      - backend:          serviceName: spring-cloud-a-base          servicePort: 20001        path: /---apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  name: spring-cloud-a-gray  annotations:    nginx.ingress.kubernetes.io/canary: "true"    nginx.ingress.kubernetes.io/canary-by-header: "x-mse-tag"    nginx.ingress.kubernetes.io/canary-by-header-value: "gray"    nginx.ingress.kubernetes.io/canary-weight: "0"spec:  rules:  - host:     http:      paths:      - backend:          serviceName: spring-cloud-a-gray          servicePort: 20001        path: /

结果验证

此时,访问 路由到基线环境

curl -H"Host:; A[172.18.144.155] -> B[172.18.144.56] -> C[172.18.144.156]

如何访问灰度环境呢?只需要在请求中增加一个header x-mse-tag:gray 即可。

curl -H"Host:;  -H"x-mse-tag:gray" Agray[172.18.144.82] -> Bgray[172.18.144.57] -> Cgray[172.18.144.8]

可以看到 Ingress 根据这个header直接路由到了 A 的 gray 环境中。

更进一步

还可以借助 Ingress 实现更复杂的路由,比如客户端已经带上了某个header,想要利用现成的 header来实现路由,而不用新增一个 header,例如下图所示,假设我们想要 x-user-id 为 100 的请求进入灰度环境。

只需要增加下面这 4 条规则:

apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  name: spring-cloud-a-basespec:  rules:  - host:     http:      paths:      - backend:          serviceName: spring-cloud-a-base          servicePort: 20001        path: /---apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  name: spring-cloud-a-base-gray  annotations:    nginx.ingress.kubernetes.io/canary: "true"    nginx.ingress.kubernetes.io/canary-by-header: "x-user-id"    nginx.ingress.kubernetes.io/canary-by-header-value: "100"    nginx.ingress.kubernetes.io/canary-weight: "0"spec:  rules:  - host:     http:      paths:      - backend:          serviceName: spring-cloud-a-gray          servicePort: 20001        path: /

访问的时候带上特殊的 header ,满足条件进入灰度环境

curl -H"Host:;  -H"x-user-id:100" Agray[172.18.144.93] -> Bgray[172.18.144.24] -> Cgray[172.18.144.25]

不满足条件的请求,进入基线环境:

curl -H"Host:;  -H"x-user-id:101" A[172.18.144.91] -> B[172.18.144.22] -> C[172.18.144.95]

相比场景一这样的好处是,客户端的域名不变,只需要通过请求来区分。

场景三:通过自定义路由规则来进行全链路灰度

有时候我们不想要自动透传且自动路由,而是希望微服务调用链上下游上的每个应用能自定义灰度规则,例如 B 应用希望控制只有满足自定义规则的请求才会路由到 B 应用这里,而 C 应用有可能希望定义和 B 不同的灰度规则,这时应该如何配置呢,场景参见如下图:

注意,最好把场景一和二中配置的参数清除掉。

第一步,需要在入口应用 A 处(最好是所有的入口应用都增加该环境变量,包括 gray 和 base) 增加一个环境变量:alicloud.service.header=x-user-idx-user-id 是需要透传的 header,它的作用是识别该 header 并做自动透传。

注意这里不要使用 x-mse-tag, 它是系统默认的一个 header,有特殊的逻辑。

第二步,在中间的 B 应用处,在 MSE 控制台配置标签路由规则

第三步,在 Ingress 处配置路由规则,这一步参考场景二,并采用如下配置:

apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  name: spring-cloud-a-basespec:  rules:  - host:     http:      paths:      - backend:          serviceName: spring-cloud-a-base          servicePort: 20001        path: /---apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  annotations:    nginx.ingress.kubernetes.io/canary: 'true'    nginx.ingress.kubernetes.io/canary-by-header: x-user-id    nginx.ingress.kubernetes.io/canary-by-header-value: '100'    nginx.ingress.kubernetes.io/canary-weight: '0'  name: spring-cloud-a-grayspec:  rules:    - host:       http:        paths:          - backend:              serviceName: spring-cloud-a-gray              servicePort: 20001            path: /

结果验证

测试验证,访问灰度环境,带上满足条件的 header,路由到 B 的灰度环境中。

curl 120.77.215.62/a -H "Host: ; -H "x-user-id: 100" Agray[192.168.86.42] -> Bgray[192.168.74.4] -> C[192.168.86.33]

访问灰度环境,带上不满足条件的 header,路由到 B 的base环境中。

curl 120.77.215.62/a -H "Host: ; -H "x-user-id: 101" A[192.168.86.35] -> B[192.168.73.249] -> C[192.168.86.33]

去掉 Ingress Canary 配置,访问 base A 服务(基线环境入口应用需要加上 alicloud.service.header 环境变量),带上满足条件的 header,路由到 B 的灰度环境中。

curl 120.77.215.62/a -H "Host: ; -H "x-user-id: 100" A[192.168.86.35] -> Bgray[192.168.74.4] -> C[192.168.86.33]

访问 base 环境,带上不满足条件的 header,路由到 B 的 base 环境中。

curl 120.77.215.62/a -H "Host: ; -H "x-user-id: 101" A[192.168.86.35] -> B[192.168.73.249] -> C[192.168.86.33]

总结

20 分钟快速实践完具有很大技术难度的全链路灰度能力,全链路灰度其实并不是那么难!

基于 MSE 服务治理的全链路灰度能力,我们可以快速落地企业级的全链路灰度能力,以上三种场景是我们在生产实践中大规模落地的标准场景,当然我们可以基于 MSE 服务治理的能力根据自己的业务个性化定制与适配;即使在多种流量来源的背景下,也能做到按照业务定制精准引流。

同时 MSE 服务治理专业版的可观测性能力使得灰度有效性得到可衡量,灰没灰到,灰得咋样,做到“心里有数”。

灰度流量秒级监控

规范发布流程

日常发布中,我们常常会有如下一些错误的想法:

这次改动的内容比较小,而且上线要求比较急,就不需要测试直接发布上线好了。

发布不需要走灰度流程,快速发布上线即可。

灰度发布没有什么用,就是一个流程而已,发布完就直接发布线上,不用等待观察。

虽然灰度发布很重要,但是灰度环境很难搭建,耗时耗力优先级并不高。

这些想法都可能让我们进行一次错误的发布,不少故障是因为发布直接或间接引起。因此提升发布的质量,减少错误的发生,是有效减少线上故障的一个关键环节。做到安全的发布,我们需要规范发布的流程。

随着微服务的流行,越来越多公司使用了微服务框架,微服务以其高内聚、低耦合等特性,提供了更好的容错性,也更适应业务的快速迭代,为开发人员带来了很多的便利性。但是随着业务的发展,微服务拆分越来越复杂,微服务的治理也成了一个比较令人头疼的问题。

单单拿全链路灰度来看,为了保证应用新版本上线前的功能正确性的验证同时需要兼顾应用发布的效率,如果我们应用的规模很小,我们可以直接通过维护多套环境来保证发布的正确性。但是当我们的业务发展到庞大且复杂程度时,假设我们的系统由 100 个微服务构成,即使测试/灰度环境每个服务占用 1 到 2 个 pod,那么多套环境下来我们需要面临巨大的成本与运维环境带来的效率的挑战。

有没有更加简单且高效的方法来解决微服务治理的难题?

MSE 微服务引擎将推出服务治理专业版,提供开箱即用且完整专业的微服务治理解决方案,帮助企业更好地实现微服务治理能力。如果您的系统也可以像本文描述的那样,快速具备完整的全链路灰度能力,并基于该能力进行进一步的微服务治理实践,不仅可以节省客观的人力与成本,还可以让您的企业在微服务领域的探索更加有底气。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

标签: #灰度发布怎么实现