文章目录
  1. 1. 兜底利器:全局开关
  2. 2. 精准灰度:白名单机制
  3. 3. 推全策略:百分比机制
  4. 4. 综合灰度策略
  5. 5. 扩展:遗留系统重构与灰度策略
  6. 6. 本文总结

日常开发中,对于遗留系统代码逻辑的改造,通常不会粗暴地采用停机更新方式直接迁移到新业务逻辑,往往需要通过灰度方式逐步迁移到新逻辑,最后再把老业务逻辑下线。

这个过程中就涉及到新老业务逻辑并存的问题,而这个问题是需要研发同学去通过工具、编码、配置等方式实现灰度策略,制定灰度计划,并付诸实施,保障系统能够平稳地从老业务逻辑迁移至新业务逻辑。

通常情况下,如果是http接口灰度,我们可以利用网关的特性,如nginx自身的能力,根据cookie中传递的唯一标识进行百分比分流,通过免开发的方式进行灰度。

但在实际开发中,我们面对的系统不仅仅是网关类系统,大部分情况下,分布式系统开发场景下面对的是RPC类接口或者异步消息逻辑,对于这类型代码逻辑的灰度就需要研发同学通过编码方式,细粒度的进行灰度。

本文中,我们就后者展开代码级别的讨论和分享,向读者介绍笔者在日常开发中,面对接口级别的灰度场景是如何通过编码实现具体的灰度策略的,本文权当抛砖引玉,为读者全景展示​后端研发套路之灰度场景下的瑞士军刀具体是如何使用的。在今后的开发工作中如果你遇到类似场景,可以参考笔者思路,开发适合自己业务的灰度套路。

兜底利器:全局开关

在灰度策略的开发过程中,有一样东西是一定要考虑的。

无论灰度策略实现多么复杂,多么优雅。这个东西是万万不能不考虑的,它就是 全局兜底开关。用一句流行话来说,就是专治各种花里胡哨。

全局兜底开关的主要目的在于:急停,简单的说就是当灰度逻辑明显朝着不符合预期的方向进行,或者服务本身依赖的下游系统出现异常,导致服务本身也处于亚健康状态(如:出现大量超时、突增业务告警、大量异常日志打印导致磁盘可用容量急剧下降等情况)。

如果在开发阶段考虑到并增加了全局开关,那么在突发情况出现的时候就可以从容的打开开关,让异常逻辑“急刹车”。然后就可以从容不迫的对问题进行分析解决,不至于因为灰度逻辑出现异常而措手不及,甚至导致绩效受到影响,乃至拥抱变化。

事实上,在互联网场景代码开发中,我们的系统往往都微服务化了,应用大多数是分布式部署的,因此全局配置往往依赖分布式配置中心这一关键基础设施。

常见的分布式配置中心选型有Qconf、Nacos、Apollo。我们基于Apollo来进行源码的展示,关于Apollo的使用,可以直接参考 官方文档; 关于框架的使用,其他的分布式配置框架都大同小异,这不是本文关注的重点,问题的关键在于体会分布式配置中心在分布式系统中全局配置下发的思路。(当然,如果说确实没有条件实践分布式配置中心,基于数据库搞一个配置表的读写也是可以的,只不过此时的全局配置是强依赖数据库的。事实上,Nacos、Apollo底层也会依赖数据库。)

说了这么多,全局开关的代码具体如何实现呢?

public boolean globalSwitch() {
    // 获取Apollo 配置实例
    Config config = ConfigService.getAppConfig();
    // 读取服务端配置的全局开关,默认值为false 转换为boolean
    return Boolean.valueOf(config.getProperty("global_config_key", "false"));
}

这段代码逻辑很容易理解,​就做了两件事:

  • 首先获取Apollo 配置实例
  • 接着读取服务端配置的全局开关,默认值为false,读取到配置值后转换为boolean返回​。返回true,表明全局开关已经打开​

全局开关的核心思路就是从配置中心通过key获取对应的配置value,转换为boolean返回。对于大多数的配置中心客户端而言,允许指定默认值,哪怕客户端与服务端连接断开还是能够从本地缓存获取默认值,防止由于读取不到配置而影响服务的可用性。

精准灰度:白名单机制

有了全局开关作为兜底保证,就可以实现具体的灰度策略了。

首先介绍的是灰度策略常客,白名单机制(有些场景也称黑名单机制,根据具体业务场景决定具体的含义)。

白名单机制,顾名思义,其实就是精准灰度策略,对显式指定的目标进行配置,最终使其进入新的业务逻辑。

比如我们可以在线上系统中配置测试账号,通过将测试账号配置到白名单中,然后模拟真实请求,就可以在不影响线上用户数据的前提下,对新业务流程进行回归验证。

从这个角度看的话,白名单机制也是一种数据隔离的手段,它能够允许我们在执行了线上逻辑的前提下,又不至于污染其他数据。

​再进一步思考,我认为从根本上讲,白名单机制本质上是一种精确触达方案,给系统提供了少量、精细化的逻辑筛选机制。

一个基于用户/商户维度进行白名单筛选的代码实现如下:

/**
 * 根据用户id进行灰度,假设灰度配置为111,222,333,444
 * @param userId
 * @return
 */
public boolean whiteListGray(String userId) {
    if (StringUtils.isEmpty(userId)) {
        return false;
    }
    // 获取Apollo 配置实例
    Config config = ConfigService.getAppConfig();
    // 读取服务端配置的灰度白名单用户列表,默认值为false
    String value = config.getProperty("user_white_list_gray_key", "");
    // 根据配置的分隔符转换配置为List
    List<String> configList = new ArrayList<>(Arrays.asList(value.trim().split(",")));

    if (CollectionUtils.isEmpty(configList)) {
        return false;
    }
    // 判断配置项List中是否包含当前userId
    if (configList.contains(userId)) {
        return true;
    }
    return false;
}

​假设分布式配置服务上对用户的灰度配置为 “111,222,333,444”,若当前请求的userId为配置中的其中一项,则调用该方法返回的结果为true,表明当前用户处于灰度白名单中,反之则表明当前请求的用户不属于灰度用户,返回false。

推全策略:百分比机制

说完了白名单机制,我们再介绍一种推进式灰度策略,通过修改配置,使得灰度逻辑在总体执行过程中的占比逐步增加,直至全量。

这个机制就是百分比机制。

它的执行逻辑也很好理解,请求进入灰度计算流程之后,该流程会对灰度比例进行计算,常见的计算策略为灰度唯一id对100进行取模(id % 100),根据执行结果判断当前请求符合的百分比。假设计算结果为n, 当前配置的百分比数字为p​:

若n ≤ p,则表示满足灰度条件,当前请求执行灰度逻辑;反之表示当前不满足灰度条件,仍然执行原有逻辑。

看起来是有些简单,但是根据奥卡姆剃刀法则,若无必要,勿增实体。简单的方案往往是更加有效且易于维护的。

只要我们从0-100逐步增加配置阈值p,那么就能够很轻松的实现灰度逻辑逐步推进至全量的过程。

一个基于用户/商户维度进行百分比灰度的代码实现如下:

/**
 * 百分比灰度 默认为0,userId 模 100,余数为灰度百分比
 * @param userId
 * @return
 */
public boolean percentGray(String userId) {
    if (StringUtils.isEmpty(userId)) {
        return false;
    }

    // 校验百分比灰度开关是否打开
    Config config = ConfigService.getAppConfig();
    if (!Boolean.valueOf(config.getProperty("percent_gray_key", "false"))) {
        return false;
    }
    // 百分比灰度,默认为0,userId 模 100,余数为灰度百分比,
    try {
        // 获取配置中心配置的取模阈值,本地默认为0,即无灰度
        int percentThreshold = Integer.valueOf(config.getProperty("percent_gray_mode_percent_key", "0"));
        // 如果userId不是数字或不能直接转换为数字,则截取数字部分再转换
        int percentThresholdRealTime = Long.valueOf(Long.valueOf(userId) % 100).intValue();
        // 校验范围
        if (percentThresholdRealTime > percentThreshold) {
            return false;
        }
        return true;
    } catch (Exception e) {
        // 异常日志输出

    }
    return false;
}

为了保险起见,我们为百分比灰度配置专用的急停开关,当百分比灰度急停开关打开,才会进入到百分比计算流程,否则哪怕由于失误配置了百分比取模的阈值(比如配置为50),只要百分比灰度的开关是关闭的,那么就不会进入到百分比计算的流程中。

这里也体现了防御式编程思想。

综合灰度策略

实际开发中,我们通常会组合使用上文中提到的灰度策略,实现灵活的灰度配置,以应对复杂业务场景。

具体的过程,用语言描述就是:

  • 首先外层还是要有全局开关,用于紧急情况下执行急停;
  • 当请求到来后,先校验全局开关是否打开,如果打开,则继续判断是否满足白名单;否则直接返回,继续执行原有逻辑​;
  • 如果命中白名单,则直接执行灰度业务逻辑;否则继续判断是否满足百分比灰度;
  • 计算当前灰度维度id对100取模结果,判断结果是否小于等于百分比阈值,如果满足,则表示命中灰度,执行灰度业务逻辑,反之则表示其不满足当前灰度,继续执行原有业务逻辑。

用一张流程图直观表达一下这个过程,便于加深理解:

复合灰度流程

当然,作为研发还是愿意看到代码是怎样落地的,别急,满足你。

复合灰度策略的关键代码实现样本如下(其余部分同上文中提到的代码,此处不重复粘贴):

/**
 * 复合灰度
 * @param userId
 * @return
 */
public boolean isHitGray(String userId) {
    return globalSwitch() || whiteListGray(userId) || percentGray(userId);
}

不要怀疑,就是如此简洁​。

我们只需要实现一个复合灰度方法,通过逻辑或将全局开关灰度、白名单灰度、百分比灰度逻辑连接起来。

三者综合作用,就能够满足我们在上文中通过流程图展示出的复合灰度逻辑。

即:在打开全局开关的前提下,校验是否命中白名单灰度或者符合百分比灰度;但凡全局开关关闭,就返回false,也就是不执行灰度逻辑。

如果不理解,可以回过头在仔细看下流程图,相信你一定能够豁然开朗。

扩展:遗留系统重构与灰度策略

不久前,有个技术群的朋友咨询了一个问题:

不久前,有个技术群的朋友咨询了一个问题:

“我们有一个系统,目前是all in one架构,逻辑上通过包(package)划分模块。现在要使用微服务方式进行重构,应该怎样从现有的架构迁移到单独的微服务上来?要求是,迁移过程中不能停机,还需要保证新需求迭代不能停止,并且老的业务逻辑也会进行修改,应该怎么做?”

我的回答是这样的,答案经过书面化编辑。

这个问题实际上是一个遗留系统重构的场景,并且需要考虑如何将遗留代码在不停机的情况下切换到新系统中。

我的建议是,对于新的产品需求,业务流程直接在新模块中开发,对于老的业务流程还是在原先的业务逻辑上迭代。

那么具体的迁移过程是怎样的呢?

实际上也不复杂,问题的核心是不停机迁移,这个比较重要。我们分两类接口讨论。

1.如果是rpc类接口, 我们针对要迁移的功能,在新模块中将老模块的业务逻辑整体迁移过来,老模块的逻辑在保持逻辑不变的情况下,增加灰度分支,分支内调用新模块的代码逻辑。

非灰度逻辑仍旧为原先的遗留代码,随着灰度逐渐进行,老业务逻辑会逐步迁移到新流程,最终原先的老逻辑就不会有请求进入,就可以在某个窗口期直接通过一次灰度发版,把老代码删除。

此时,当灰度执行完毕之后就到达全量,接口逻辑已经完全切到新流程。

这个方式是上游不需要对接口感知的逻辑,另外一种方法是,在调用方进行灰度,服务的消费方编写灰度逻辑,灰度逻辑指向新的模块服务,当灰度结束后,移除原先对老服务的调用,然后在灰度发版的过程中将老服务提供方代码下线。

这个方式上游需要感知下游服务提供方,需要事先修改服务调用配置,并且编写对新接口的调用逻辑,也是一种常见的RPC接口灰度方式。简单讲就是上游业务逻辑层进行灰度逻辑的开发,下游只提供接口实现。好处是下游不关心灰度逻辑,代码整洁度高。坏处是上游有额外的代码开发量,且上游需要关注灰度进程。

2.需要灰度的逻辑是基于http接口,灰度逻辑也RPC也类似,最常见的策略是在http网关接入层增加灰度逻辑,在灰度逻辑块中加入对新模块中逻辑的调用,通过复合灰度策略逐步迁移到新的逻辑中,最后待灰度流程执行到全量之后,把老逻辑的遗留代码删除掉灰度到全量就可以。

本文总结

本文,笔者带领大家系统的整理了Java后端研发中常见的灰度策略,重点介绍了三种灰度策略:全局开关、黑白名单机制、百分比灰度机制以及将三者组合起来的复合策略。

通过灰度逻辑说明结合代码实战案例,从理论到落地展现了灰度策略的方方面面,这个策略在实际的开发中会经常用到,尤其是当业务处于成熟期,需要不断迭代重构,灰度策略会是开发者经常用到的工具。

系统掌握常见的灰度策略,会大幅度减少新老逻辑切换的风险,提升系统稳定性,并且在潜移默化中提升开发者对系统稳定性的思考,在方案设计中自觉考虑降级,促进系统良性迭代。

当然灰度策略也不是银弹,大量灰度逻辑的编写,会直接增加业务逻辑的复杂度,增加测试难度。并且当业务逻辑全量之后,灰度逻辑往往会成为“脏代码”,此时就可以考虑删除。

但是实际情况常常是,新加入的同学因为不了解灰度策略是否已经全量,并且生产系统本身就很少删除代码,导致灰度逻辑积少成多,最终成为系统走向腐化的推手之一。

解决灰度逻辑堆积的一个实践是:指定灰度计划,显式表明全量时间,在全量之后做好灰度逻辑删除,通过代码review与比对,确保没有多删除代码;测试同学介入对流程进行回归测试,确保业务逻辑符合预期。

通过严谨的灰度策略和回归测试,代码review,既保证灰度流程正常进行,最终迁移到新流程,又保证过时的灰度逻辑不会污染业务代码。

笔者的经验有限,更多的灰度策略使用技巧和心得还需要我们在实际开发中慢慢体会总结,最终沉淀出一套适合自己的工业级的灰度策略。



版权声明:

原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。

文章目录
  1. 1. 兜底利器:全局开关
  2. 2. 精准灰度:白名单机制
  3. 3. 推全策略:百分比机制
  4. 4. 综合灰度策略
  5. 5. 扩展:遗留系统重构与灰度策略
  6. 6. 本文总结
Fork me on GitHub