searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

微服务全链路灰度发布

2024-08-05 09:31:37
43
0

前言

现业务线公有云服务已经上生产,为保证后续公有云的稳定性,鲁棒性,安全性,兼容性等,需提供业务线公有云微服务的全链路灰度发布思路,注意是微服务全链路灰度!!!​全链路灰度发布在业内已经是非常成熟的技术了,暂时没有什么技术难点,主要是开发规划落实和推动​,这里记录下思考学习过程。

为何要灰度发布?

业务线公有云产品对外的目的就是为了赚钱,没赚钱那公有云产品没任何意义,所以影响赚钱的因素我们都应该考虑处理掉。业内有一个指标叫​SLA(Service-Level Agreement)​:服务等级协议是服务提供者对客户一个服务承诺,评估一个产品是否可用的方法。一般有四个SLA指标,可用性、准确性、系统容量和延迟。SLA指标降低了,那产品不可用了就影响算钱了,产品总是不可用了客户就要换产品了也影响赚钱。 因此保证系统的SLA指标是重中之重。​而可灰度和可监控是保证SLA的重要因素​​。因此我们要做业务线产品公有云微服务线的灰度发布,且必须得做。

有那些版本发布方法

企业微信截图_17225668346174.png

一些说明
业内常见的版本发布方法有全量发布,滚动发布,蓝绿发布,灰度发布(金丝雀发布)。业务线产品主要使用灰度发布方案实现公有云服务的生产微感发布。同时简单介绍下其他的发布思路。

全量发布

企业微信截图_17225671468764.png

一些说明
全量发布主要适用于简单小型的内部服务,直接将所有的微服务和老版本升级到最新。属有损发布。
优点:成本低,运维简单粗暴。​适用于小型内部业务服务​。缺点:版本切换用户感知大,升级过程中服务无法访问,无法支持类似于公有云的对外客户服务。

滚动发布

企业微信截图_17225673297593.png

一些说明
滚动发布每次只升级一个或多个服务,如果业务观察无问题,再重复这个过程,直到全部服务升级到新版本。属有损发布。不过相对于全量发布,用户感知变小,业务方可以手动保证服务最低限度对外服务。同样只适用于内部的业务服务。
优点:运维成本较低,只有在新老版本切换的时间点才会客户有感。缺点:​版本回滚麻烦。版本切换用户感知较大​,需要人工去停止业务的流量,且难判断停止的老版本节点是否还有流量。发布和回滚时间长,同样有很大的升级风险。

蓝绿发布

企业微信截图_17225674405198.png

一些说明
蓝绿部署老集群绿版本不停机,部署一套完整的蓝集群新版本,用户可以将所有流量全部迁移到蓝集群新版本中,也可以通过流量染色实现流量在蓝绿集群中切换。蓝绿版本之间物理或者逻辑隔离,没有子请求交互。属无损发布。一般公有云单组件有状态服务产品,比如国内某些厂商的存储服务就使用蓝绿部署的升级,不仅可以做到版本升级,还可以做到集群迁移,扩缩容等功能。
优点:​版本切换用户基本无感​,蓝绿版本可以做到流量快速微感切换回滚。运维版本发布方便。缺点:​成本较高,需要部署一套完整的集群​,如果是涉及到底层的有状态存储服务,还需要做到数据的迁移和同步对齐。同时对于小版本和bugfix不友好,无法支持DevOps快速的迭代开发。

灰度发布(金丝雀发布)

企业微信截图_17225675094211.png

一些说明
灰度发布也交金丝雀发布,是一种是一种渐进式的软件发布方式,它允许将新功能或更新逐步给一部分用户试用,而不是一次性全部替换。属无损发布。灰度发布也可以分为单链路发布和全链路发布,单链路发布理解为特殊的蓝绿部署,流量可以按比例分割,如老集群80%,灰度集群20%。当灰度集群稳定了后,逐步将老集群的所有流量迁移到新集群中。单链路方案有一些缺陷,比如bugfix和小版本快速迭代,只需要更新一个或者多个服务,而不是全部。因此便有了全链路发布,全链路发布的思路可以允许运维开发人员只发布部分应用。适合复杂大型对外微服务。
优点:​全链路发布对客户基本无感,运维版本发布非常方便​,且保存了稳定的老版本回滚快速,风险低。同时支持线上灰度测试,生产治理可控。缺点:成本较高,包括开发和资源成本,开发和运维流程需要严格约束。系统架构稍微复杂了,需更精细管理和维护,提高了管理和监控成本。

小结

上文简单介绍了全量发布,滚动发布,蓝绿部署,灰度发布4种发布思路。每一种发布思路都有适合的场景,​对于当前业务线公有云的微服务体系而言,全链路灰度发布是最好的选择​。可以做到客户基本无感,回滚方便,低风险,运维方便。下面着重介绍下全链路发布方案的思路。

灰度发布有什么好处?

企业微信截图_17225677788287.png

一些说明
灰度发布的好处有:​用户无感,提高系统稳定性,大大大减少了运维成本,发版不影响服务赚钱,白天也可以发布啦,用户和业务灰度测试,无缝回滚​,提高公有云产品SLA, 快速体验新产品idea,新版本影响面风险可控,迭代反馈循环,新版本影响面风险可控。下面重点说明下以下几项
1,用户无感:因为灰度发布会同时存在一个或多个版本,版本在快速切换的时候,不影响产品的正常使用。
2,大大大减少了运维成本:传统的发布方法需要再凌晨做全量的配置文件,数据库,微服务等发布操作,工作复杂且高风险,灰度发布存在网关流量控制,版本发布不用一股脑全部替换,可以先一个一个完成配置,数据库,灰度服务发布后,然后使用apisix快速切换无感切换流量即可。
3,提高系统稳定性:灰度发布因为不会全量替换老版本,可以一个一个服务灰度切换,因此系统不会由于大量操作而导致奔溃而影响这个系统的稳定。
4,提高公有云产品SLA:因为灰度发布不会影响系统对外服务,保证了全年大数据公有云产品对外的服务时间。
5,白天也可以发布啦:传统的发布方法因为会影响客户使用,由于发布工作量大,风险发,因此一般在晚上发布。而由于灰度发布的流量可控性,白天也可以发布服务,不会影响线上用户的使用。
6,用户和业务灰度测试:灰度发布可以控制部分流量到灰度版本,因此可以使部分客户作尝鲜新的功能的同时也可以作用户测试,测试人员也可以使用指定的账号做线上灰度测试。线下测试版本,架构,配置,数据,依赖,负载等维度不同难以全面覆盖线上场景,灰度测试能弥补这种情况,使产品更加的完善完整正确。
7,无缝回滚:灰度发布会存在多个版本,如果在用户和业务灰度测试过程中发现bug,可以在网关层快速切换流量。
所以让我们快速开始实现灰度发布吧~

灰度发布的一些方法

由于我们需要全链路灰度发布,因此基本上可以套模型,模型为:​注册中心+API服务网关+微服务RPC负载均衡+微服务框架流量透传​​。基本上满足这个模型才可以做到全链路灰度发布。
image.png

一些说明
image.png

灰度发布实现

由上文已确定使用APISIX+NACOS+SpringCloud+RpcLoadblance+流量透传方式实现全链路灰度发布,下面记录下详细的实现细节。

全链路灰度的必要因素

企业微信截图_17225787074500.png

一些说明
如果要实现全链路灰度发布,那么在技术层面需要支持如下几点
1,API对外网关和RPC组件能负载均衡以及路由:如果API网关和RPC组件无法做到负载均衡和路由,那么无法将请求流量分发给灰度节点。
2,流量的标记信息和染色信息能全链路透传:因为要做到全链路灰度,如果无法将流量的标记信息和染色信息透传到整个链路,那将无法做到整个链路的灰度。
3,微服务节点能标记分组和区分版本:如果微服务节点无法标记,那么rpc路由将无法识别下游的节点是否为灰度节点,也就无法做到灰度发布。
4,请求流量能染色:流量如果无法做到染色,那当前节点将无法获得下游的灰度节点。
5,请求流量有版本和灰度标记信息:请求流量又灰度标记和版本信息才能作为灰度判断的参数。

全链路灰度技术细节

image.png

RpcLoadblance和流量透传在业内有多种思路,主要有SDK,Javaagent(Sermant),以及自己编码(推荐)。我们这里为了避免又引入其他组件,直接修改ddaf框架新增 MyFeignRequestInterceptor(流量透传) 和 GrayNacosLoadBalancer(灰度路由和负载) 工具。

流量透传实现

public class MyFeignRequestInterceptor implements RequestInterceptor {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(MyFeignRequestInterceptor.class);
 
    @Override
    public void apply(RequestTemplate requestTemplate) {
        Map<String, String> headers = getHeaders();
        headers.forEach(requestTemplate::header);
    }
 
    private Map<String, String> getHeaders() {
        HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        LOGGER.info("request Header:{}",map);
        return map;
    }
}

RPC LoadBalancer实现

public class GrayNacosLoadBalancer implements ReactorServiceInstanceLoadBalancer {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(GrayNacosLoadBalancer.class);
 
    /**
     * 当前服务的名称
     */
    private final String serviceId;
 
    /**
     * 负载均衡上游服务实例
     */
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
 
    /**
     * nacos配置信息
     */
    private final NacosDiscoveryProperties nacosDiscoveryProperties;
 
 
    public GrayNacosLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
                                 String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.nacosDiscoveryProperties = nacosDiscoveryProperties;
    }
 
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier =
                serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get().next().mapNotNull(serviceInstances -> getInstanceResponse(serviceInstances, request));
    }
 
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances, Request request) {
        if (serviceInstances.isEmpty()) {
            LOGGER.warn("No servers available for service: " + this.serviceId);
            return new EmptyResponse();
        }
 
        try {
            String clusterName = this.nacosDiscoveryProperties.getClusterName();
            List<ServiceInstance> instancesToChoose = serviceInstances;
            if (StringUtils.isNotBlank(clusterName)) {
                List<ServiceInstance> sameClusterInstances = serviceInstances.stream().filter(serviceInstance -> {
                    String cluster = serviceInstance.getMetadata().get("nacos.cluster");
                    return StringUtils.equals(cluster, clusterName);
                }).collect(Collectors.toList());
 
                //--是否为灰度请求
                if (isGrayRequest(request)) {
                    //--获得灰度节点
                    sameClusterInstances =
                            sameClusterInstances.stream().filter(s -> s.getMetadata().get(SysConstants.KEY_GRAY_TAG) != null
                                    && s.getMetadata().get(SysConstants.KEY_GRAY_TAG).equals(SysConstants.VAL_GRAY_TAG)).collect(Collectors.toList());
                } else {
                    //--排除灰度节点
                    sameClusterInstances = sameClusterInstances.stream().
                            filter(s -> s.getMetadata().get(SysConstants.KEY_GRAY_TAG) == null ||
                                    !s.getMetadata().get(SysConstants.KEY_GRAY_TAG).equals(SysConstants.VAL_GRAY_TAG))
                            .collect(Collectors.toList());
                }
                if (!CollectionUtils.isEmpty(sameClusterInstances)) {
                    instancesToChoose = sameClusterInstances;
                }
            } else {
                LOGGER.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", serviceId,
                        clusterName, serviceInstances);
            }
 
            ServiceInstance instance = NacosBalancer.getHostByRandomWeight3(instancesToChoose);
            return new DefaultResponse(instance);
        } catch (Exception e) {
            LOGGER.warn("GrayNacosLoadBalancer error", e);
            return null;
        }
    }
 
    private boolean isGrayRequest(Request request) {
        RequestDataContext dataContext = (RequestDataContext) request.getContext();
        HttpHeaders headers = dataContext.getClientRequest().getHeaders();
        return headers.get(SysConstants.KEY_GRAY_TAG) != null && Objects.requireNonNull(headers.get(SysConstants.KEY_GRAY_TAG)).get(0).equals(SysConstants.VAL_GRAY_TAG);
    }
}

全链路灰度结构

企业微信截图_17225789619280.png

一些说明
0,路由规则:灰度流量如果有灰度节点则走灰度节点,没有灰度节点则走普通节点。而如果是正常流量则只能走普通节点。
1,外部流量通过APISIX网关,如果满足gray=true条件,则认定为灰度流量。
2,APISIX要能提供灰度流量分割能力,将灰度流量负载和路由到第一个微服务中,同时也支持下游所有服务节点的对外服务路由配置。
3,微服务之间的流量透传使用fegin拦截器实现,将所有的header信息透传到下游的所有节点中。
4,如果当前微服务节点接受到了灰度流量,那么在spring cloud loadbalancer中要先从nacos注册服务中获得有gray标签的节点,然后将这个灰度请求发往这个灰度节点中。
5,灰度流量到达灰度节点,灰度节点完成灰度版本的业务逻辑,同样按上一步的逻辑获得下游的节点类型。如果是灰度请求则负载发往下游的灰度节点,否则直接发完正常的节点。

Demo演示

0,基础环境准备

交付一套完整的K8S环境,基础服务中需要有Nacos,APISIX,Rancher等基础组件。
image.png

1,微服务部署

Demo服务请求链

企业微信截图_17225790719713.png

流量通过APISIX请求dwuser,dwuser在通过dworder查询订单信息。在k8s上分别给dwuser和dworder部署base和gray版本。

实际操作

a)创建2个服务(dwuser,dworder),并打包成镜像
image.png

b)k8s部署微服务

配置信息
image.png

k8s交付信息

Namespace:microservices
# 服务名称,dwuser表示服务名,gray表示灰度版本,v1.0.1表示版本,普通版本名称为dworder-release-v1.0.0
Name:dwuser-gray-v1.0.1
 
# 镜像
Image:xxxx/dwuser:v1.0.0
 
# 环境变量
Environment Variables:
NACOS_USERNAME  NACOS_USERNAME 
NACOS_PASSWORD  NACOS_PASSWORD
LANG en_US.utf8
app.env dw_dev
 
# 注册到Nacos的服务的元数据-服务节点版本
spring.cloud.nacos.discovery.metadata.version v1.0.1
 
# 注册到Nacos的服务的元数据-服务是否为灰度节点
spring.cloud.nacos.discovery.metadata.gray true
 
spring.cloud.nacos.discovery.server-addr xxxx//xxxx:18848
JAVA_OPTS -Xms128M -Xmx128M -XX:AutoBoxCacheMax=20000 -XX:+AlwaysPreTouch -XX:-UseBiasedLocking -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=5 -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps

主要注意的配置项为spring.cloud.nacos.discovery.metadata.version和spring.cloud.nacos.discovery.metadata.gray。

c)查看服务是否注册成功
image.png

d)配置APISIX服务路由
image.png

测试请求

正常请求
# 只有gray=true的才是灰度流量,其他的是正常流程
curl --request GET \
  --url xxxx://xxxx:xxxx/dwuser/v1/index/getUserInfo \
  --header 'Content-Type: application/json' \
  --header 'gray: true' \
  --header 'userid: 100' \
  --header 'version: 1'

image.png
可以看到version返回的字段均为base,表示访问base版本的信息。

灰度请求
# 如果gray=false,路由到灰度的节点
curl --request GET \
  --url xxxx://xxxx:xxxx/dwuser/v1/index/getUserInfo \
  --header 'Content-Type: application/json' \
  --header 'gray: false' \
  --header 'userid: 100' \
  --header 'version: 1'

image.png

可以看到version返回的字段均为gray,表示访问gray版本的信息。

总结

1,全链路灰度发布使用APISIX+NACOS+SpringCloud+GrayNacosLoadBalancer(RPC负载均衡和路由)+FeignRequestInterceptor(流量透传)思路能在业务线公有云的场景下能落地实现。
2,如果需要做到完整的灰度发布,需要将网关换成APISIX。方便流量分割,染色,流量监控反馈等。
3,灰度发布需要后续大数据产品更精细化的管理和维护。
4,当前灰度发布方案只能针对微服务全链路的场景,如果需要存储池支持,则需要做影子库或影子表,不过如果租户隔离做的OK,问题不大。
5,注意对外第一个网关也需要具备流量切割和负载均衡能力。

一些问题

1,数据库等有状态的服务和组件该如何做到全链路灰度?
答:本文一开始就说了,主要针对的是无状态微服务的全链路灰度发布,如果是数据库比如MySQL这种有状态存储服务,业内主要提供的是影子表或者影子库的思路,也就是创建一个新表,灰度完后再将数据同步到原表或者源库中。而其他的有状态的组件也需要做独立的灰度支持,实现成本较高。

2,灰度的服务和版本建议留多久的时间?
答:不行就是不行,行就是行。线上服务灰度的版本不建议超过1周,在灰度测试一段时间OK的情况下,尽快将灰度表示去掉作为普通节点使用,避免后续多个gray版本混乱了。

3,如果开发人员没有把新版本标识位灰度就直接上线了会怎么样?
答:如果新版本没有加上灰度标识,当前的灰度计算规则将直接把发布的版本作为普通的服务节点,所以说全链路灰度发布技术成熟,主要是开发规划落实和推动。

4,服务节点是否为灰度版本的计算规则改如何确定?
答:建议先不要太复杂,当前使用的是最简单的spring.cloud.nacos.discovery.metadata加一个gray=true的tag方式,后续如果业务需要可以按版本,按多tag,甚至使用表达式引擎来处理。

5,全链路灰度使用场景有那些?
答:a)大版本升级:所有版本或者2/3的服务要升级。建议使用单链路蓝绿方式,使用全新的nacos/k8s新namespace。b)小版本或者bugfix,使用同一个namespace,然后使用流量染色的方式验证版本。c)灰度测试,同样可以对uid做染色,然后也可以使用一个灰度数据库,线上验证灰度流量。d)A/B测试流量分割,可以使用traffic-split方式做流量切割给少部分客户做功能试用,和灰度测试场景差不多。

0条评论
0 / 1000
wanghg11
13文章数
2粉丝数
wanghg11
13 文章 | 2 粉丝
原创

微服务全链路灰度发布

2024-08-05 09:31:37
43
0

前言

现业务线公有云服务已经上生产,为保证后续公有云的稳定性,鲁棒性,安全性,兼容性等,需提供业务线公有云微服务的全链路灰度发布思路,注意是微服务全链路灰度!!!​全链路灰度发布在业内已经是非常成熟的技术了,暂时没有什么技术难点,主要是开发规划落实和推动​,这里记录下思考学习过程。

为何要灰度发布?

业务线公有云产品对外的目的就是为了赚钱,没赚钱那公有云产品没任何意义,所以影响赚钱的因素我们都应该考虑处理掉。业内有一个指标叫​SLA(Service-Level Agreement)​:服务等级协议是服务提供者对客户一个服务承诺,评估一个产品是否可用的方法。一般有四个SLA指标,可用性、准确性、系统容量和延迟。SLA指标降低了,那产品不可用了就影响算钱了,产品总是不可用了客户就要换产品了也影响赚钱。 因此保证系统的SLA指标是重中之重。​而可灰度和可监控是保证SLA的重要因素​​。因此我们要做业务线产品公有云微服务线的灰度发布,且必须得做。

有那些版本发布方法

企业微信截图_17225668346174.png

一些说明
业内常见的版本发布方法有全量发布,滚动发布,蓝绿发布,灰度发布(金丝雀发布)。业务线产品主要使用灰度发布方案实现公有云服务的生产微感发布。同时简单介绍下其他的发布思路。

全量发布

企业微信截图_17225671468764.png

一些说明
全量发布主要适用于简单小型的内部服务,直接将所有的微服务和老版本升级到最新。属有损发布。
优点:成本低,运维简单粗暴。​适用于小型内部业务服务​。缺点:版本切换用户感知大,升级过程中服务无法访问,无法支持类似于公有云的对外客户服务。

滚动发布

企业微信截图_17225673297593.png

一些说明
滚动发布每次只升级一个或多个服务,如果业务观察无问题,再重复这个过程,直到全部服务升级到新版本。属有损发布。不过相对于全量发布,用户感知变小,业务方可以手动保证服务最低限度对外服务。同样只适用于内部的业务服务。
优点:运维成本较低,只有在新老版本切换的时间点才会客户有感。缺点:​版本回滚麻烦。版本切换用户感知较大​,需要人工去停止业务的流量,且难判断停止的老版本节点是否还有流量。发布和回滚时间长,同样有很大的升级风险。

蓝绿发布

企业微信截图_17225674405198.png

一些说明
蓝绿部署老集群绿版本不停机,部署一套完整的蓝集群新版本,用户可以将所有流量全部迁移到蓝集群新版本中,也可以通过流量染色实现流量在蓝绿集群中切换。蓝绿版本之间物理或者逻辑隔离,没有子请求交互。属无损发布。一般公有云单组件有状态服务产品,比如国内某些厂商的存储服务就使用蓝绿部署的升级,不仅可以做到版本升级,还可以做到集群迁移,扩缩容等功能。
优点:​版本切换用户基本无感​,蓝绿版本可以做到流量快速微感切换回滚。运维版本发布方便。缺点:​成本较高,需要部署一套完整的集群​,如果是涉及到底层的有状态存储服务,还需要做到数据的迁移和同步对齐。同时对于小版本和bugfix不友好,无法支持DevOps快速的迭代开发。

灰度发布(金丝雀发布)

企业微信截图_17225675094211.png

一些说明
灰度发布也交金丝雀发布,是一种是一种渐进式的软件发布方式,它允许将新功能或更新逐步给一部分用户试用,而不是一次性全部替换。属无损发布。灰度发布也可以分为单链路发布和全链路发布,单链路发布理解为特殊的蓝绿部署,流量可以按比例分割,如老集群80%,灰度集群20%。当灰度集群稳定了后,逐步将老集群的所有流量迁移到新集群中。单链路方案有一些缺陷,比如bugfix和小版本快速迭代,只需要更新一个或者多个服务,而不是全部。因此便有了全链路发布,全链路发布的思路可以允许运维开发人员只发布部分应用。适合复杂大型对外微服务。
优点:​全链路发布对客户基本无感,运维版本发布非常方便​,且保存了稳定的老版本回滚快速,风险低。同时支持线上灰度测试,生产治理可控。缺点:成本较高,包括开发和资源成本,开发和运维流程需要严格约束。系统架构稍微复杂了,需更精细管理和维护,提高了管理和监控成本。

小结

上文简单介绍了全量发布,滚动发布,蓝绿部署,灰度发布4种发布思路。每一种发布思路都有适合的场景,​对于当前业务线公有云的微服务体系而言,全链路灰度发布是最好的选择​。可以做到客户基本无感,回滚方便,低风险,运维方便。下面着重介绍下全链路发布方案的思路。

灰度发布有什么好处?

企业微信截图_17225677788287.png

一些说明
灰度发布的好处有:​用户无感,提高系统稳定性,大大大减少了运维成本,发版不影响服务赚钱,白天也可以发布啦,用户和业务灰度测试,无缝回滚​,提高公有云产品SLA, 快速体验新产品idea,新版本影响面风险可控,迭代反馈循环,新版本影响面风险可控。下面重点说明下以下几项
1,用户无感:因为灰度发布会同时存在一个或多个版本,版本在快速切换的时候,不影响产品的正常使用。
2,大大大减少了运维成本:传统的发布方法需要再凌晨做全量的配置文件,数据库,微服务等发布操作,工作复杂且高风险,灰度发布存在网关流量控制,版本发布不用一股脑全部替换,可以先一个一个完成配置,数据库,灰度服务发布后,然后使用apisix快速切换无感切换流量即可。
3,提高系统稳定性:灰度发布因为不会全量替换老版本,可以一个一个服务灰度切换,因此系统不会由于大量操作而导致奔溃而影响这个系统的稳定。
4,提高公有云产品SLA:因为灰度发布不会影响系统对外服务,保证了全年大数据公有云产品对外的服务时间。
5,白天也可以发布啦:传统的发布方法因为会影响客户使用,由于发布工作量大,风险发,因此一般在晚上发布。而由于灰度发布的流量可控性,白天也可以发布服务,不会影响线上用户的使用。
6,用户和业务灰度测试:灰度发布可以控制部分流量到灰度版本,因此可以使部分客户作尝鲜新的功能的同时也可以作用户测试,测试人员也可以使用指定的账号做线上灰度测试。线下测试版本,架构,配置,数据,依赖,负载等维度不同难以全面覆盖线上场景,灰度测试能弥补这种情况,使产品更加的完善完整正确。
7,无缝回滚:灰度发布会存在多个版本,如果在用户和业务灰度测试过程中发现bug,可以在网关层快速切换流量。
所以让我们快速开始实现灰度发布吧~

灰度发布的一些方法

由于我们需要全链路灰度发布,因此基本上可以套模型,模型为:​注册中心+API服务网关+微服务RPC负载均衡+微服务框架流量透传​​。基本上满足这个模型才可以做到全链路灰度发布。
image.png

一些说明
image.png

灰度发布实现

由上文已确定使用APISIX+NACOS+SpringCloud+RpcLoadblance+流量透传方式实现全链路灰度发布,下面记录下详细的实现细节。

全链路灰度的必要因素

企业微信截图_17225787074500.png

一些说明
如果要实现全链路灰度发布,那么在技术层面需要支持如下几点
1,API对外网关和RPC组件能负载均衡以及路由:如果API网关和RPC组件无法做到负载均衡和路由,那么无法将请求流量分发给灰度节点。
2,流量的标记信息和染色信息能全链路透传:因为要做到全链路灰度,如果无法将流量的标记信息和染色信息透传到整个链路,那将无法做到整个链路的灰度。
3,微服务节点能标记分组和区分版本:如果微服务节点无法标记,那么rpc路由将无法识别下游的节点是否为灰度节点,也就无法做到灰度发布。
4,请求流量能染色:流量如果无法做到染色,那当前节点将无法获得下游的灰度节点。
5,请求流量有版本和灰度标记信息:请求流量又灰度标记和版本信息才能作为灰度判断的参数。

全链路灰度技术细节

image.png

RpcLoadblance和流量透传在业内有多种思路,主要有SDK,Javaagent(Sermant),以及自己编码(推荐)。我们这里为了避免又引入其他组件,直接修改ddaf框架新增 MyFeignRequestInterceptor(流量透传) 和 GrayNacosLoadBalancer(灰度路由和负载) 工具。

流量透传实现

public class MyFeignRequestInterceptor implements RequestInterceptor {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(MyFeignRequestInterceptor.class);
 
    @Override
    public void apply(RequestTemplate requestTemplate) {
        Map<String, String> headers = getHeaders();
        headers.forEach(requestTemplate::header);
    }
 
    private Map<String, String> getHeaders() {
        HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        LOGGER.info("request Header:{}",map);
        return map;
    }
}

RPC LoadBalancer实现

public class GrayNacosLoadBalancer implements ReactorServiceInstanceLoadBalancer {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(GrayNacosLoadBalancer.class);
 
    /**
     * 当前服务的名称
     */
    private final String serviceId;
 
    /**
     * 负载均衡上游服务实例
     */
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
 
    /**
     * nacos配置信息
     */
    private final NacosDiscoveryProperties nacosDiscoveryProperties;
 
 
    public GrayNacosLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
                                 String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.nacosDiscoveryProperties = nacosDiscoveryProperties;
    }
 
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier =
                serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get().next().mapNotNull(serviceInstances -> getInstanceResponse(serviceInstances, request));
    }
 
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances, Request request) {
        if (serviceInstances.isEmpty()) {
            LOGGER.warn("No servers available for service: " + this.serviceId);
            return new EmptyResponse();
        }
 
        try {
            String clusterName = this.nacosDiscoveryProperties.getClusterName();
            List<ServiceInstance> instancesToChoose = serviceInstances;
            if (StringUtils.isNotBlank(clusterName)) {
                List<ServiceInstance> sameClusterInstances = serviceInstances.stream().filter(serviceInstance -> {
                    String cluster = serviceInstance.getMetadata().get("nacos.cluster");
                    return StringUtils.equals(cluster, clusterName);
                }).collect(Collectors.toList());
 
                //--是否为灰度请求
                if (isGrayRequest(request)) {
                    //--获得灰度节点
                    sameClusterInstances =
                            sameClusterInstances.stream().filter(s -> s.getMetadata().get(SysConstants.KEY_GRAY_TAG) != null
                                    && s.getMetadata().get(SysConstants.KEY_GRAY_TAG).equals(SysConstants.VAL_GRAY_TAG)).collect(Collectors.toList());
                } else {
                    //--排除灰度节点
                    sameClusterInstances = sameClusterInstances.stream().
                            filter(s -> s.getMetadata().get(SysConstants.KEY_GRAY_TAG) == null ||
                                    !s.getMetadata().get(SysConstants.KEY_GRAY_TAG).equals(SysConstants.VAL_GRAY_TAG))
                            .collect(Collectors.toList());
                }
                if (!CollectionUtils.isEmpty(sameClusterInstances)) {
                    instancesToChoose = sameClusterInstances;
                }
            } else {
                LOGGER.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", serviceId,
                        clusterName, serviceInstances);
            }
 
            ServiceInstance instance = NacosBalancer.getHostByRandomWeight3(instancesToChoose);
            return new DefaultResponse(instance);
        } catch (Exception e) {
            LOGGER.warn("GrayNacosLoadBalancer error", e);
            return null;
        }
    }
 
    private boolean isGrayRequest(Request request) {
        RequestDataContext dataContext = (RequestDataContext) request.getContext();
        HttpHeaders headers = dataContext.getClientRequest().getHeaders();
        return headers.get(SysConstants.KEY_GRAY_TAG) != null && Objects.requireNonNull(headers.get(SysConstants.KEY_GRAY_TAG)).get(0).equals(SysConstants.VAL_GRAY_TAG);
    }
}

全链路灰度结构

企业微信截图_17225789619280.png

一些说明
0,路由规则:灰度流量如果有灰度节点则走灰度节点,没有灰度节点则走普通节点。而如果是正常流量则只能走普通节点。
1,外部流量通过APISIX网关,如果满足gray=true条件,则认定为灰度流量。
2,APISIX要能提供灰度流量分割能力,将灰度流量负载和路由到第一个微服务中,同时也支持下游所有服务节点的对外服务路由配置。
3,微服务之间的流量透传使用fegin拦截器实现,将所有的header信息透传到下游的所有节点中。
4,如果当前微服务节点接受到了灰度流量,那么在spring cloud loadbalancer中要先从nacos注册服务中获得有gray标签的节点,然后将这个灰度请求发往这个灰度节点中。
5,灰度流量到达灰度节点,灰度节点完成灰度版本的业务逻辑,同样按上一步的逻辑获得下游的节点类型。如果是灰度请求则负载发往下游的灰度节点,否则直接发完正常的节点。

Demo演示

0,基础环境准备

交付一套完整的K8S环境,基础服务中需要有Nacos,APISIX,Rancher等基础组件。
image.png

1,微服务部署

Demo服务请求链

企业微信截图_17225790719713.png

流量通过APISIX请求dwuser,dwuser在通过dworder查询订单信息。在k8s上分别给dwuser和dworder部署base和gray版本。

实际操作

a)创建2个服务(dwuser,dworder),并打包成镜像
image.png

b)k8s部署微服务

配置信息
image.png

k8s交付信息

Namespace:microservices
# 服务名称,dwuser表示服务名,gray表示灰度版本,v1.0.1表示版本,普通版本名称为dworder-release-v1.0.0
Name:dwuser-gray-v1.0.1
 
# 镜像
Image:xxxx/dwuser:v1.0.0
 
# 环境变量
Environment Variables:
NACOS_USERNAME  NACOS_USERNAME 
NACOS_PASSWORD  NACOS_PASSWORD
LANG en_US.utf8
app.env dw_dev
 
# 注册到Nacos的服务的元数据-服务节点版本
spring.cloud.nacos.discovery.metadata.version v1.0.1
 
# 注册到Nacos的服务的元数据-服务是否为灰度节点
spring.cloud.nacos.discovery.metadata.gray true
 
spring.cloud.nacos.discovery.server-addr xxxx//xxxx:18848
JAVA_OPTS -Xms128M -Xmx128M -XX:AutoBoxCacheMax=20000 -XX:+AlwaysPreTouch -XX:-UseBiasedLocking -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=5 -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps

主要注意的配置项为spring.cloud.nacos.discovery.metadata.version和spring.cloud.nacos.discovery.metadata.gray。

c)查看服务是否注册成功
image.png

d)配置APISIX服务路由
image.png

测试请求

正常请求
# 只有gray=true的才是灰度流量,其他的是正常流程
curl --request GET \
  --url xxxx://xxxx:xxxx/dwuser/v1/index/getUserInfo \
  --header 'Content-Type: application/json' \
  --header 'gray: true' \
  --header 'userid: 100' \
  --header 'version: 1'

image.png
可以看到version返回的字段均为base,表示访问base版本的信息。

灰度请求
# 如果gray=false,路由到灰度的节点
curl --request GET \
  --url xxxx://xxxx:xxxx/dwuser/v1/index/getUserInfo \
  --header 'Content-Type: application/json' \
  --header 'gray: false' \
  --header 'userid: 100' \
  --header 'version: 1'

image.png

可以看到version返回的字段均为gray,表示访问gray版本的信息。

总结

1,全链路灰度发布使用APISIX+NACOS+SpringCloud+GrayNacosLoadBalancer(RPC负载均衡和路由)+FeignRequestInterceptor(流量透传)思路能在业务线公有云的场景下能落地实现。
2,如果需要做到完整的灰度发布,需要将网关换成APISIX。方便流量分割,染色,流量监控反馈等。
3,灰度发布需要后续大数据产品更精细化的管理和维护。
4,当前灰度发布方案只能针对微服务全链路的场景,如果需要存储池支持,则需要做影子库或影子表,不过如果租户隔离做的OK,问题不大。
5,注意对外第一个网关也需要具备流量切割和负载均衡能力。

一些问题

1,数据库等有状态的服务和组件该如何做到全链路灰度?
答:本文一开始就说了,主要针对的是无状态微服务的全链路灰度发布,如果是数据库比如MySQL这种有状态存储服务,业内主要提供的是影子表或者影子库的思路,也就是创建一个新表,灰度完后再将数据同步到原表或者源库中。而其他的有状态的组件也需要做独立的灰度支持,实现成本较高。

2,灰度的服务和版本建议留多久的时间?
答:不行就是不行,行就是行。线上服务灰度的版本不建议超过1周,在灰度测试一段时间OK的情况下,尽快将灰度表示去掉作为普通节点使用,避免后续多个gray版本混乱了。

3,如果开发人员没有把新版本标识位灰度就直接上线了会怎么样?
答:如果新版本没有加上灰度标识,当前的灰度计算规则将直接把发布的版本作为普通的服务节点,所以说全链路灰度发布技术成熟,主要是开发规划落实和推动。

4,服务节点是否为灰度版本的计算规则改如何确定?
答:建议先不要太复杂,当前使用的是最简单的spring.cloud.nacos.discovery.metadata加一个gray=true的tag方式,后续如果业务需要可以按版本,按多tag,甚至使用表达式引擎来处理。

5,全链路灰度使用场景有那些?
答:a)大版本升级:所有版本或者2/3的服务要升级。建议使用单链路蓝绿方式,使用全新的nacos/k8s新namespace。b)小版本或者bugfix,使用同一个namespace,然后使用流量染色的方式验证版本。c)灰度测试,同样可以对uid做染色,然后也可以使用一个灰度数据库,线上验证灰度流量。d)A/B测试流量分割,可以使用traffic-split方式做流量切割给少部分客户做功能试用,和灰度测试场景差不多。

文章来自个人专栏
微服务SRE
2 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0