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

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

最小活跃数算法

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

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

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 模块。

总结

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

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

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

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

相关文章
|
4天前
|
机器学习/深度学习 存储 算法
心得经验总结:微信红包随机算法转载
心得经验总结:微信红包随机算法转载
|
25天前
|
存储 运维 算法
社交软件红包技术解密(十三):微信团队首次揭秘微信红包算法,为何你抢到的是0.01元
本文中,我们将介绍几种主流的IM红包分配算法,相信聪明的你一定能从中窥见微信红包技术实现的一些奥秘。
16 0
|
26天前
|
弹性计算 负载均衡 算法
加权轮询算法介绍
加权轮询算法
33 3
|
26天前
|
弹性计算 负载均衡 算法
轮询算法介绍
轮询算法介绍
29 4
|
12月前
|
算法 Python
用 Python 实现一个简单的微信红包算法
基本思路就是,有多少个红包,就循环多少次,每一次,在剩下的钱里面随机出一个值作为这个红包的金额,然后把金额从总金额中扣除。这里要注意,需要保证每个人至少能拿得到 1 分钱。只剩最后一个人时,拿走剩下所有的金额。另外,为了保证计算时候方便,采用“分”作为金额的计算单位。
|
算法 小程序 Go
Golang 微信小程序加密数据解密算法实现
Golang 微信小程序加密数据解密算法实现
358 0
|
算法 PHP
PHP实现微信支付签名算法(MD5版本及HMAC-SHA256版本)
PHP实现微信支付签名算法(MD5版本及HMAC-SHA256版本)
695 0
|
资源调度 算法
基于启发式蝙蝠算法、粒子群算法、花轮询算法和布谷鸟搜索算法的换热器PI控制器优化(Matlab代码实现)
基于启发式蝙蝠算法、粒子群算法、花轮询算法和布谷鸟搜索算法的换热器PI控制器优化(Matlab代码实现)
基于启发式蝙蝠算法、粒子群算法、花轮询算法和布谷鸟搜索算法的换热器PI控制器优化(Matlab代码实现)
|
前端开发 小程序 算法
【微信小程序】基于百度大脑人体检测、人脸识别以及调用阿里垃圾分类识别小程序利用canvas完成人脸画图、分割手部部分图片算法
【微信小程序】基于百度大脑人体检测、人脸识别垃圾分类人体出现在镜头里用红色框将人脸圈出来、用黄色框将手部圈出来,定时器触发后,通过百度返回的top+、left+、width+、height+将拍照的截图用canvas画出来,最后保存上传到阿里云垃圾分类识别检测博主用的是手部关键点识别,手部截取包括手肘部分,当出现手肘没有手掌时会出现截取不到目标的问题,目前解决办法:定时器设置时间长一点供演示员做好调整,另外就是出现手掌,可以尽量把掌心打开方便识别这样手肘部分就不会被检测到了在截取的时候canvas用不了..
278 0
【微信小程序】基于百度大脑人体检测、人脸识别以及调用阿里垃圾分类识别小程序利用canvas完成人脸画图、分割手部部分图片算法
|
23小时前
|
算法 索引
基于Prony算法的系统参数辨识matlab仿真
Prony算法在MATLAB2022a中用于信号分析,识别复指数信号成分。核心程序通过模拟信号X1,添加不同SNR的噪声,应用Prony方法处理并计算误差。算法基于离散序列的复指数叠加模型,通过构建矩阵并解线性方程组估计参数,实现LTI系统动态特性的辨识。