likes
comments
collection
share

灰度方案中的坑点:注意开关、随机放量等状态不一致问题

作者站长头像
站长
· 阅读数 59

在灰度方案中,为了降低新功能的风险,我们通常会采用渐进式发布策略。然而,在实践中,随着代码的不断迭代,开发人员可能会遇到一些坑点。其中最常见的问题是在开关控制和随机放量时出现状态不一致的情况。

首先,对于开关控制,我们需要确保在不同环境下使用相同的配置参数。如果存在多个开关,那么必须要注意它们之间的依赖关系,并确保在打开或关闭任何一个开关时,不会影响其他开关的状态。

其次,随机放量也是一个潜在的问题。例如,我们可能会将新功能的流量分配给一组用户,但是这些用户并非完全随机选择。如果没有正确处理好这种随机性,就可能会导致一些用户无法获得新功能,或者有部分用户在某段时间内连续收到过多的新功能。

最后,灰度发布过程中还需要进行错误监控和回滚。如果出现了异常情况,我们应该及时切换回老版本,并且要能够快速识别和排查问题的根本原因。

总之,灰度发布策略可以帮助我们更加安全地推出新功能。但是,在实践中,我们需要注意开关控制和随机放量等状态不一致的问题,并且在出现异常情况时能够快速回滚。

开关篇

以下是示例代码:

同一个业务流程,多个逻辑使用同一开关,Bad Case(不使用标识符):

public class SwitchDemo {
    public static boolean isSwitchOn = false; // 全局开关状态

    public void logicA() {
        if (isSwitchOn) {
            System.out.println("逻辑A被执行");
        }
    }

    public void logicB() {
        if (isSwitchOn) {
            System.out.println("逻辑B被执行");
        }
    }
}

在这个示例代码中,isSwitchOn 是一个全局变量,代表了一个开关的状态。逻辑 A 和逻辑 B 都依赖这个开关的状态来控制自己的行为。然而,如果在并发情况下,当逻辑 A 执行时这个开关被关闭了,那么逻辑 B 就会受到影响,因为它也使用了这个开关。这种情况下,就会出现逻辑问题。

Good Case(使用标识符):

public class SwitchDemo {
    private static final String SWITCH_FLAG = "SWITCH_ON"; // 标识符
    private static final Map<String, Boolean> switchMap = new ConcurrentHashMap<>(); // 存储所有标识符对应的开关状态

    public void logicA(String orderNo) {
        setSwitch(orderNo); // 为该单据号打上标识符
        Boolean isSwitchOn = getSwitch(orderNo); // 获取该单据号对应的开关状态
        if (isSwitchOn != null && isSwitchOn) {
            System.out.println("逻辑A被执行");
        }
    }

    public void logicB(String orderNo) {
        Boolean isSwitchOn = getSwitch(orderNo); // 获取该单据号对应的开关状态
        if (isSwitchOn != null && isSwitchOn) {
            System.out.println("逻辑B被执行");
        }
    }

    private static void setSwitch(String orderNo) {
        switchMap.putIfAbsent(orderNo, true);
    }

    private static Boolean getSwitch(String orderNo) {
        return switchMap.get(orderNo);
    }
}

在这个示例代码中,我们使用了一个标识符 SWITCH_FLAG 来代表开关的状态。而且,我们采用了一种新的方法来控制每个单据号对应的开关状态。

在 logicA() 方法中调用了 setSwitch() 函数,并将单据号作为参数传递过去。这个函数会为该单据号打上标识符,并将其与开关状态绑定起来。在 logicB() 方法中,我们只需要获取该单据号对应的开关状态即可。

通过这种方式,我们可以确保每个单据号都拥有自己独立的开关状态,并且不同的单据之间不会产生影响。这样,即使在并发情况下,当逻辑 A 执行时这个开关被关闭了,但是由于标识符保证了单据号对应的开关状态,逻辑 B 仍然能够正确执行。

总之,通过使用标识符来代表开关状态,并且为不同的单据分别维护它们自己的开关状态,可以有效地避免在多线程环境下出现逻辑问题。同时,为了确保代码的可靠性和健壮性,我们还应该采取一些其他的最佳实践。

随机放量篇

以下是一个针对随机放量灰度方案的示例:

在生产环境中逐步推出新的功能、流程或系统,通过将其暴露给一小部分用户进行测试和评估后,再决定是否将其推广到所有用户。这里的测试比例为10%。

在该例子中,灰度测试是针对下单流程和发货流程的改进,即展示新的下单流程和使用新的发货流程给一小部分用户进行测试,然后根据测试结果来决定是否将其推广到所有用户。如果测试结果良好,那么这些新的流程就会被应用于所有用户;否则,开发人员需要进行进一步修正和改进,直到它们可以顺利地应用于所有用户。

重点:本次需求就是想保证用户在下单和发货环节,要么都走旧逻辑,要么都走新逻辑;基于此目的,我们需要保证用户在请求时,整个流程中,涉及多次调用随机变量的时候,需要保证多次调用的结果是一致的,否则将会出现下单和发货出现新旧混合的流程。

package com.example.demo.random;

import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

public class OrderService {
    private static final double TEST_PERCENTAGE = 0.5; // 灰度测试比例
    private static final Map<Integer, Random> randomMap = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        User user = new User(10001); // 假设用户ID为 10001

        // 下单
        placeOrder(user, new Order("order1"));
        // 发货
        deliverOrder(user, new Order("order1"));
    }

    private static void placeOrder(User user, Order order) {
        if (shouldShowNewOrderProcess(user)) {
            showNewOrderProcess(user); // 展示新的下单流程
        } else {
            showNormalOrderProcess(user); // 展示普通的下单流程
        }
    }

    private static void deliverOrder(User user, Order order) {
        if (shouldUseNewDeliveryProcess(user)) {
            useNewDeliveryProcess(user, order); // 使用新的发货流程
        } else {
            doNormalDelivery(user, order); // 使用普通的发货流程
        }
    }

    private static boolean shouldShowNewOrderProcess(User user) {
        Random random = getRandomForUser(user.getId());
        return random.nextDouble() < TEST_PERCENTAGE; // 返回一个随机布尔值,决定是否展示新的下单流程
    }

    private static void showNewOrderProcess(User user) {
        System.out.println("展示新的下单流程给用户:" + user.getId());
    }

    private static void showNormalOrderProcess(User user) {
        System.out.println("展示旧的下单流程给用户:" + user.getId());
    }

    private static boolean shouldUseNewDeliveryProcess(User user) {
        Random random = getRandomForUser(user.getId());
        return random.nextDouble() < TEST_PERCENTAGE; // 返回一个随机布尔值,决定是否使用新的发货流程
    }

    private static void useNewDeliveryProcess(User user, Order order) {
        System.out.println("使用新的发货流程给用户:" + user.getId() + " 的订单:" + order.getOrderId());
    }

    private static void doNormalDelivery(User user, Order order) {
        System.out.println("使用旧的发货流程给用户:" + user.getId() + " 的订单:" + order.getOrderId());
    }

    private static Random getRandomForUser(int userId) {
        Random random = randomMap.get(userId);
        if (random == null) {
            random = new Random(userId); // 使用固定的随机数种子
            randomMap.put(userId, random);
        }
        // 可以注释上面的代码,使用下面代码
//        Random random = new Random();
        return random;
    }

    private static class User {
        private int id;

        public User(int id) {
            this.id = id;
        }

        public int getId() {
            return id;
        }
    }

    private static class Order {
        private String orderId;

        public Order(String orderId) {
            this.orderId = orderId;
        }

        public String getOrderId() {
            return orderId;
        }
    }
}

上面只是简单的两个案例,对于灰度方案设计,大家可以举一反三去思考、实践。

值得一提的是,在做技术方案设计时,比如灰度方案,我们一般要拥有“闭环”能力,要有始有终,既要设计灰度方案,也要考虑如何卸载灰度方案,形成闭环。

欢迎关注公众号:程序员的思考与落地

公众号提供大量实践案例,Java入门者不容错过哦,可以交流!!

转载自:https://juejin.cn/post/7221882958717091896
评论
请登录