微信红包业务,为什么采用轮询算法?(二)

简介: 微信红包业务,为什么采用轮询算法?(二)

最小活跃数算法

上述分析的基本算法、平滑轮询加权、一致性哈希等算法都属于静态算法,也就是说这些算法配置后,并不会根据线上的实际运行情况进行调整,只会根据已配置的规则进行请求分发。

最小活跃数算法则会根据线上的实际情况进行分发,能够灵活的检测出集群中各个节点的状态,能够自动寻找并调用活跃度最低的节点处理请求。

Java 实现如下:

// 节点类:用于封装集群中的每个节点
public class Server {
    private String IP;
    private AtomicInteger active;
//    private Integer weight;
    public Server(){}
    public Server(String IP,int active) {
        this.IP = IP;
        // 将外部传递的活跃数作为默认活跃数
        this.active = new AtomicInteger(active);
    }
    public String getIP() {
        // 每分发一个请求时自增一次活跃数
        active.incrementAndGet();
        return IP;
    }
    public AtomicInteger getActive() {
        return active;
    }
}
// 集群类:用于模拟集群节点列表
public class Servers {
    // 活跃度衰减器
    public static void attenuator(){
        new Thread(()->{
            // 遍历集群中的所有节点
            for (Server server : Servers.SERVERS) {
                // 如果活跃度不为0
                if (server.getActive().get() != 0){
                    // 则自减一个活跃度
                    server.getActive().getAndDecrement();
                }
            }
            try {
                // 每隔 2 秒中衰减一次活跃度
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
    // 模拟的集群节点信息,活跃数最开始默认为0
    public static List<Server> SERVERS = Arrays.asList(
            new Server("44.120.110.001:8080",0),
            new Server("44.120.110.002:8081",0),
            new Server("44.120.110.003:8082",0)
    );
}
// 最小活跃数算法实现类
public class LeastActive {
    public static String getServer(){
        // 初始化最小活跃数和最小活跃数的节点
        int leastActive = Integer.MAX_VALUE;
        Server leastServer = new Server();
        // 遍历集群中的所有节点
        for (Server server : Servers.SERVERS) {
            // 找出活跃数最小的节点
            if (leastActive > server.getActive().get()){
                leastActive = server.getActive().get();
                leastServer = server;
            }
        }
        // 返回活跃数最小的节点IP
        return leastServer.getIP();
    }
    public static void main(String[] args){
        Servers.attenuator();
        for (int i = 1; i <= 10; i++){
            System.out.println("第"+ i + "个请求:" + getServer());
        }
    }
}
/********运行结果*********/
第1个请求:44.120.110.001:8080
第2个请求:44.120.110.002:8081
第3个请求:44.120.110.003:8082
第4个请求:44.120.110.001:8080
第5个请求:44.120.110.002:8081
第6个请求:44.120.110.003:8082
第7个请求:44.120.110.001:8080
第8个请求:44.120.110.002:8081
第9个请求:44.120.110.003:8082
第10个请求:44.120.110.001:8080

观察如上案例的运行结果,似乎结果好像是轮询的效果呀?确实是的,这是因为在最开始,所有节点的活跃数都为 0,三个节点的活跃数都相同。

所以默认会先取集群中的第一个活跃数为 0 的节点处理请求,第一个节点的活跃数会变成 1,第二次请求时最小活跃数也为 0,然后取第二个节点处理请求,依此类推......

在线上环境下,不会出现轮询的效果,因为每台服务器随着运行时间的增长,活跃数必然会不同,因此该算法总会取活跃数最小的节点提供服务。

当然,上述案例中实现的最小活跃数,是比较简易的版本,对于完善的实现可以参考 Dubbo 框架中的 com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance 类,其中也实现了权重机制。

简单阐述一下其中的原理实现:

  • 先从注册中心中拉取所有的服务实例,然后找出活跃数最小的节点。
  • 如果只有一个,那么则直接返回对应的实例节点处理本次请求。
  • 如果存在多个,则根据每个节点配置的权重值来决定本次处理请求的具体节点。
  • 如果权重值不同,优先选取权重值最大的实例,作为处理本次请求的节点。
  • 如果存在相同的最大权重值,那么则通过随机的方式选择一个节点提供服务。

当然,由于需要对每个节点去实现活跃数监听,所以在 Dubbo 框架中,想要配置最小活跃数策略,那么需要首先启用 ActiveLimitFilter 记录每个节点的活跃数。

或者也可以参考 Ribbon 框架 com.netflix.loadbalancer 包下面的 BestAvailableRule 最小活跃数算法实现类。

从最小活跃数算法特性不难得知,该算法带来的优势极为明显,永远都能选取节点列表中最空闲的那台服务器处理请求,从而避免某些负载过高的节点,还依旧承担需要承担新的流量访问,造成更大的压力。

最优响应算法

与前面分析的最小活跃数算法一样,最优响应算法也是一种动态算法,但它比最小活跃数算法更加智能,因为最小活跃数算法中,如果一台节点存在故障,导致它自身处理的请求数比较少,那么它会遭受最大的访问压力,这显然是并不合理的。

最小活跃数算法就类似于平时的搬砖工作,谁事情做的最少谁留下来加班,在正常情况下,这种算法都能够找到“摸鱼”最厉害的员工留下来加班。

但如果有一天,某个员工由于身体出问题了,导致自己做的工作量比较少,但按照这种算法的逻辑,依旧会判定为该员工今天最闲,所以留下来加班。

从上述这个案例中,大家略微能够感受出来最小活跃数算法的不合理性。

而最优响应算法则更加智能,该算法在开始前,会对服务列表中的各节点发出一个探测请求(例如 Ping 或心跳包检测),然后根据各节点的响应时间来决定由哪台服务器处理客户端请求,该算法能较好根据节点列表中每台机器的当前运行状态分发请求。

Java 实现如下:

public class Servers {
    // 模拟的集群节点信息,活跃数最开始默认为0
    public static List<Server> SERVERS = Arrays.asList(
            new Server("44.120.110.001:8080"),
            new Server("44.120.110.002:8081"),
            new Server("44.120.110.003:8082")
    );
}
public class Server {
    private String IP;
    public Server(){}
    public Server(String IP) {
        this.IP = IP;
    }
    public String getIP() {
        return IP;
    }
    public void setIP(String IP){
        this.IP = IP;
    }
    public String ping(){
        // 生成一个1000~3000之间的随机数
        int random = ThreadLocalRandom.current().nextInt(1000, 2000);
        try {
            // 随机休眠一段时间,模拟不同的响应速度
            Thread.sleep(random);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 最后返回自身的IP
        return this.IP;
    }
}
public class ResponseTime {
    // 创建一个定长的线程池,用于去执行ping任务
    static ExecutorService pingServerPool = 
        Executors.newFixedThreadPool(Servers.SERVERS.size());
    public static String getServer() throws InterruptedException {
        // 创建一个CompletableFuture用于拼接任务
        CompletableFuture cfAnyOf;
        // 创建一个接收结果返回的server节点对象
        final Server resultServer = new Server();
        // 根据集群节点数量初始化一个异步任务数组
        CompletableFuture[] cfs = new CompletableFuture[Servers.SERVERS.size()];
        // 遍历整个服务器列表,为每个节点创建一个ping任务
        for (Server server : Servers.SERVERS) {
            // 获取当前节点在集群列表中的下标
            int index = Servers.SERVERS.indexOf(server);
            // 为每个节点创建一个ping任务,并交给pingServerPool线程池执行
            CompletableFuture<String> cf =
                    CompletableFuture.supplyAsync(server::ping,pingServerPool);
            // 将创建好的异步任务加入数组中
            cfs[index] = cf;
        }
        // 将创建好的多个Ping任务组合成一个聚合任务并执行
        cfAnyOf = CompletableFuture.anyOf(cfs);
        // 监听执行完成后的回调,谁先执行完成则返回谁
        cfAnyOf.thenAccept(resultIP -> {
             System.out.println("最先响应检测请求的节点为:" + resultIP);
            resultServer.setIP((String) resultIP);
        });
        //  阻塞主线程一段时间,防止CompletableFuture退出
        Thread.sleep(3000);
        // 返回最先响应检测请求(ping)的节点作为本次处理客户端请求的节点
        return resultServer.getIP();
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 5; i++){
            System.out.println("第"+ i + "个请求:" + getServer());
        }
    }
}
/******运行结果:******/
最先响应检测请求的节点为:44.120.110.002:8081
第1个请求:44.120.110.002:8081
最先响应检测请求的节点为:44.120.110.002:8081
第2个请求:44.120.110.002:8081
最先响应检测请求的节点为:44.120.110.003:8082
第3个请求:44.120.110.003:8082
最先响应检测请求的节点为:44.120.110.003:8080
第4个请求:44.120.110.001:8080
最先响应检测请求的节点为:44.120.110.002:8081
第5个请求:44.120.110.002:8081

在该案例中,其实现过程对比之前的算法略微复杂一些,首先在 Server 实例类中定义了一个 Ping() 方法,该方法中使用随机数+线程休眠的方式简单模拟了一下节点的不同的响应速度。

然后在算法实现类中,利用 CompletableFuture 分别对每一个节点都创建了对应的 Ping 任务,然后同时执行,又通过 thenAccept() 回调方法监听了执行结果,谁最先响应,则取其作为处理本次请求的节点。

这个算法的实现过程中,唯一难理解的就是 CompletableFuture,它是 JDK8 中推出的一种异步任务。

这里只是举例实现,所以通过 CompletableFuture 实现了检测请求,但实际过程中如果要选择这种算法,那么基于 Netty 会更为合适。

从上述案例的运行结果中也可以得知:最优响应算法无论在何种情况下,都能从集群中选取性能最好的节点对外服务,Nginx 中也支持配置这种算法,但需要先安装对应的 nginx-upstream-fair 模块。

总结

在本文中,对于比较常用的请求分发算法进行了剖析及手写实践,其中提到了较为传统的静态调度算法:轮询、随机、加权、一致性哈希等,也谈到了一些较为智能的动态算法:最小活跃数、最优响应等。

但需要牢记的一点是:并非越智能的算法越好,越是并发高、流量大的场景下,反而选用最基本的算法更合适,例如微信的红包业务,就是采用最基本的轮询算法进行集群调度。

那这又是为何呢?因为越智能的调度算法,进行节点选择时的开销会更大,如果你对于文中给出的调度算法实现都一一运行过,那么大家会明显感知出:越到后面的算法,分发请求的速度越慢。

因此在面临巨大访问压力的情景中,选择最简单的算法反而带来的收益更高,但前提是需要集群中所有的节点硬件配置都一致,所有节点分配的资源都相同,轮询算法则是最佳的调度算法。

相关文章
|
23天前
|
存储 缓存 算法
优化轮询算法以提高资源分配的效率
【10月更文挑战第13天】通过以上这些优化措施,可以在一定程度上提高轮询算法的资源分配效率,使其更好地适应不同的应用场景和需求。但需要注意的是,优化策略的选择和实施需要根据具体情况进行详细的分析和评估,以确保优化效果的最大化。
|
4月前
|
缓存 负载均衡 算法
(四)网络编程之请求分发篇:负载均衡静态调度算法、平滑轮询加权、一致性哈希、最小活跃数算法实践!
先如今所有的技术栈中,只要一谈关于高可用、高并发处理相关的实现,必然会牵扯到集群这个话题,也就是部署多台服务器共同对外提供服务,从而做到提升系统吞吐量,优化系统的整体性能以及稳定性等目的。
|
5月前
|
弹性计算 负载均衡 算法
轮询算法介绍
轮询算法介绍
85 4
|
5月前
|
弹性计算 负载均衡 算法
加权轮询算法介绍
加权轮询算法
127 3
|
4月前
|
算法 安全 数据安全/隐私保护
支付系统---微信支付09------数字签名,现在Bob想要给Pink写一封信,信件的内容不需要加密,怎样能够保证信息的完整性,使用信息完整性的主要手段是摘要算法,散列函数,哈希函数,H称为数据指纹
支付系统---微信支付09------数字签名,现在Bob想要给Pink写一封信,信件的内容不需要加密,怎样能够保证信息的完整性,使用信息完整性的主要手段是摘要算法,散列函数,哈希函数,H称为数据指纹
|
5月前
|
机器学习/深度学习 存储 算法
心得经验总结:微信红包随机算法转载
心得经验总结:微信红包随机算法转载
51 0
|
5月前
|
存储 运维 算法
社交软件红包技术解密(十三):微信团队首次揭秘微信红包算法,为何你抢到的是0.01元
本文中,我们将介绍几种主流的IM红包分配算法,相信聪明的你一定能从中窥见微信红包技术实现的一些奥秘。
91 0
|
算法 Python
用 Python 实现一个简单的微信红包算法
基本思路就是,有多少个红包,就循环多少次,每一次,在剩下的钱里面随机出一个值作为这个红包的金额,然后把金额从总金额中扣除。这里要注意,需要保证每个人至少能拿得到 1 分钱。只剩最后一个人时,拿走剩下所有的金额。另外,为了保证计算时候方便,采用“分”作为金额的计算单位。
|
27天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
11天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。