【云原生&微服务五】Ribbon负载均衡策略之随机ThreadLocalRandom

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 【云原生&微服务五】Ribbon负载均衡策略之随机ThreadLocalRandom

@[toc]

一、前言

在前面的Ribbon系列文章:

  1. 【云原生&微服务一】SpringCloud之Ribbon实现负载均衡详细案例(集成Eureka、Ribbon)
  2. 【云原生&微服务二】SpringCloud之Ribbon自定义负载均衡策略(含Ribbon核心API)
  3. 【云原生&微服务三】SpringCloud之Ribbon是这样实现负载均衡的(源码剖析@LoadBalanced原理)
  4. 【云原生&微服务四】SpringCloud之Ribbon和Erueka集成的细节全在这了(源码剖析)

我们聊了以下问题:

  1. 为什么给RestTemplate类上加上了@LoadBalanced注解就可以使用Ribbon的负载均衡?
  2. SpringCloud是如何集成Ribbon的?
  3. Ribbon如何作用到RestTemplate上的?
  4. 如何获取到Ribbon的ILoadBalancer?
  5. ZoneAwareLoadBalancer(属于ribbon)如何与eureka整合,通过eureka client获取到对应注册表?
  6. ZoneAwareLoadBalancer如何持续从Eureka中获取最新的注册表信息?
  7. 如何根据负载均衡器ILoadBalancer从Eureka Client获取到的List<Server>中选出一个Server?
  8. Ribbon如何发送网络HTTP请求?
  9. Ribbon如何用IPing机制动态检查服务实例是否存活?

本篇文章我们继续看Ribbon内置了哪些负载均衡策略?RandomRule负载均衡策略的算法是如何实现的?

PS:Ribbon依赖Spring Cloud版本信息如下:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.3.7.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合spring cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合spring cloud alibaba-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.5.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

二、Ribbon内置了哪些负载均衡算法?

  1. RandomRule --> 随机选择一个Server
  2. RoundRobinRule --> 轮询选择,轮询Index,选择index对应位置的Server,请求基本平摊到每个Server上。
  3. WeightedResponseTimeRule --> 根据响应时间加权,响应时间越长,权重越小,被选中的可能性越低。
  4. ZoneAvoidanceRule --> 综合判断Server所在Zone的性能和Server的可用性选择server,在没Zone的环境下,类似于轮询(RoundRobinRule)。默认策略
  5. BestAvailableRule --> 选择一个最小的并发请求的Server,逐个考察Server,如果Server被tripped了,则跳过。
  6. RetryRule --> 对选定的负载均衡策略上 重试机制,在一个配置时间段内选择Server不成功,就一直尝试使用subRule(默认是RoundRobinRule)的方式选择一个可用的Server。
  7. AvailabilityFilteringRule --> 过滤掉一直连接失败的(被标记为circuit tripped的)的Server,并过滤掉那些高并发的后端Server 或者 使用一个AvailabilityPredicate来定义过滤Server的逻辑,本质上就是检查status里记录的各个Server的运行状态;其具体逻辑如下:

    先用round robin算法,轮询依次选择一台server,如果判断这个server是否是存活的、可用的,如果这台server是不可以访问的,那么就用round robin算法再次选择下一台server,依次循环往复10次,还不行,就走RoundRobin选择。

三、随机算法 --> RandomRule

我们知道Ribbon负载均衡算法体现在IRule的choose(Object key)方法中,而choose(Object key)方法中又会调用choose(ILoadBalancer lb, Object key)方法,所以我们只需要看各个IRule实现类的choose(ILoadBalancer lb, Object key)方法;

在这里插入图片描述

PS:allList和upList的一些疑问和解惑!

最近和一个大V聊了一下RandomRule中Server的选择,随机的下标是以allList的size为基数,而Server的选择则是拿到随机数以upList为准;当时我们考虑极端情况可能存在越界问题!

当天晚上博主又追了一下Ribbon的整个执行流程,结论如下:

  1. upList和allList是Ribbon维护在自己内存的,在服务启动时会从服务注册中心把服务实例信息拉到upList和allList;
  2. 后续无论是通过ping机制还是每30s从注册中心拉取全量服务实例列表,

但凡all list发生变更,都会触发一个事件,然后修改本地内存的up list。

  1. 另外默认ping机制并不会定时每10s执行,因为默认的IPing实现是DummyPing,而BaseLoadBalancer#canSkipPing()里会判断IPing实现是DummyPing则不启动Timer定时做Ping机制。

Eureka和Ribbon整合之后,EurekaRibbonClientConfiguration(spring-cloud-netflix-eureka-client包下)类中新定义了一个IPing(NIWSDiscoveryPing),此时会启动Timer每10s做一次ping操作。

随机算法体现在RandomRule#chooseRandomInt()方法:
在这里插入图片描述
然而,chooseRandomInt()方法中居然使用的不是Random,而是ThreadLocalRandom,并直接使用ThreadLocalRandom#nextInt(int)方法获取某个范围内的随机值,ThreadLocalRandom是个什么东东?

1、ThreadLocalRandom详解

在这里插入图片描述
ThreadLocalRandom位于JUC(java.util.concurrent)包下,继承自Random。

1)为什么不用Random?

从Java1.0开始,java.util.Random就已经存在,其是一个线程安全类,多线程环境下,科通通过它获取到线程之间互不相同的随机数,其线程安全性是通过原子类型AtomicLong的变量seed + CAS实现的。

在这里插入图片描述

尽管Random使用 CAS 操作来更新它原子类型AtomicLong的变量seed,并且在很多非阻塞式算法中使用了非阻塞式原语,但是CAS在资源高度竞争时的表现依然糟糕。

2)ThreadLocalRandom的诞生?

JAVA7在JUC包下增加了该类,意在将它和Random结合以克服Random中的CAS性能问题;
虽然可以使用ThreadLocal<Random>来避免线程竞争,但是无法避免CAS 带来的开销;考虑到性能诞生了ThreadLocalRandom;ThreadLocalRandom不是ThreadLocal包装后的Random,而是真正的使用ThreadLocal机制重新实现的Random。

ThreadLocalRandom的核心实现细节:

  1. 使用一个普通long类型的变量SEED替换Random中的AtomicLong类型的seed
  2. 不能同构构造函数创建ThreadLocalRandom实例,因为它的构造函数是私有的,要使用静态工厂ThreadLocalRandom.current()
  3. 它是CPU缓存感知式的,使用8个long虚拟域来填充64位L1高速缓存行

3)ThreadLocalRandom的错误使用场景

1> 代码示例:

package com.saint.random;

import java.util.concurrent.ThreadLocalRandom;

/**
 * @author Saint
 */
public class ThreadLocalRandomTest {

    private static final ThreadLocalRandom RANDOM =
            ThreadLocalRandom.current();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new SonThread().start();
        }
    }

    private static class SonThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " obtain random value is : " + RANDOM.nextInt(100));
        }
    }
}

2> 运行结果:

在这里插入图片描述

  • 居然每个线程获取到的随机值都是一样的!!!

3> 运行结果分析:

上述代码中之所以每个线程获取到的随机值都是一样,因为:

  1. ThreadLocalRandom 类维护了一个类单例字段,线程通过调用 ThreadLocalRandom#current() 方法来获取 ThreadLocalRandom单例对象;然后以线程维护的实例字段 threadLocalRandomSeed 为种子生成下一个随机数和下一个种子值;
  2. 线程在调用 current() 方法的时候,会根据用每个线程 thread 的一个实例字段 threadLocalRandomProbe 是否为 0 来判断当前线程实例是是第一次调用随机数生成方法,进而决定是否要给当前线程初始化一个随机的 threadLocalRandomSeed 种子值。
  3. 所以,如果其他线程绕过 current() 方法直接调用随机数方法(比如nextInt()),那么它的种子值就是可预测的,即一样的。

4)ThreadLocalRandom的正确使用方式

每次要获取随机数时,调用ThreadLocalRandom的正确使用方式是ThreadLocalRandom.current().nextX(int)

public class ThreadLocalRandomTest {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new SonThread().start();
        }
    }

    private static class SonThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " obtain random value is : " + ThreadLocalRandom.current().nextInt(100));
        }
    }
}

运行结果如下:

在这里插入图片描述

5)ThreadLocalRandom源码解析

1> nextInt(int bound)方法获取随机值

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    // 1. 使用当前种子值SEED获取新种子值,mix32()可以看到是一个扰动函数
    int r = mix32(nextSeed());
    int m = bound - 1;
    // 2. 使用新种子值获取随机数
    if ((bound & m) == 0) // power of two
        r &= m;
    else { // reject over-represented candidates
        for (int u = r >>> 1;
             u + m - (r = u % bound) < 0;
             u = mix32(nextSeed()) >>> 1)
            ;
    }
    return r;
}

当bound=100时,代码执行如下:

在这里插入图片描述
在这里插入图片描述

2> nextSeed()方法获取下一个种子值

final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    //r = UNSAFE.getLong(t, SEED) 获取当前线程中对应的SEED值
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}
nextSeed()方法中首先使用 基于主内存地址的Volatile读的方式获取老的SEED种子值,然后再使用 基于主内存地址的Volatile写的方式设置新的SEED种子值;

种子值相关常量:

// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
// 种子值
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
    try {
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> tk = Thread.class;
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception e) {
        throw new Error(e);
    }
}

3> 总述

  1. ThreadLocalRandom中直接基于主内存地址的Volatile读方式读取老SEED值。
  2. ThreadLocalRandom中直接基于主内存地址的Volatile写方式将老SEED值替换为新SEED值;因为这里的种子值都是线程级别的,所以不需要原子级别的变量,也不会出现多线程竞争修改种子值的情况。

谈到基于主内存地址的Volatile读写,ConCurrentHashMap中也有大量使用,参考博文:https://blog.csdn.net/Saintmm/article/details/122911586

相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
1天前
|
监控 负载均衡 Java
【阿里云云原生专栏】微服务架构在阿里云云原生平台上的应用实例与优化策略
【5月更文挑战第20天】本文介绍了在阿里云云原生平台实现微服务架构的步骤,包括基于Spring Cloud的Docker化部署、使用ACK部署微服务,以及优化策略:服务发现与负载均衡(借助Istio)和监控日志管理。通过这种方式,企业能提升应用的可扩展性、可维护性和敏捷性。
170 5
|
5天前
|
缓存 负载均衡 监控
探索分布式系统演进之路:从负载均衡到微服务架构
小米分享了分布式系统的发展,从早期的负载均衡(入口级、网关和客户端)到微服务架构的演进。微服务实现服务解耦,增强系统弹性,但带来了新的挑战。为优化数据库性能,实施了主备读写分离、全文搜索引擎、缓存集群等措施。通过微服务治理,如服务注册、动态配置、灰度发布等,提升了系统稳定性和可靠性。未来将继续优化分布式系统,提供更好的服务体验。关注公众号“软件求生”了解更多。
26 6
|
6天前
|
缓存 负载均衡 算法
【微服务 SpringCloud】实用篇 · Ribbon负载均衡
【微服务 SpringCloud】实用篇 · Ribbon负载均衡
24 0
|
6天前
|
Kubernetes Cloud Native 持续交付
构建高效稳定的云原生应用:容器编排与微服务治理实践
【5月更文挑战第14天】 随着企业数字化转型的深入,云原生技术以其弹性、敏捷和可扩展的特性成为现代应用开发的首选模式。本文将探讨如何通过容器编排工具如Kubernetes以及微服务架构的有效治理,构建和维护高效且稳定的云原生应用。我们将分析容器化技术的优势,并结合案例讨论在多云环境下实现持续集成、持续部署(CI/CD)的最佳实践,同时解决微服务带来的分布式复杂性问题。通过本文的阐述,读者将获得一套提升系统可靠性和业务连续性的策略框架。
8 0
|
6天前
|
负载均衡 算法 Java
Ribbon自定义负载均衡算法
Ribbon自定义负载均衡算法
16 1
|
6天前
|
Kubernetes Cloud Native 持续交付
构建高效云原生应用:Kubernetes与微服务架构的融合
【5月更文挑战第6天】 在数字化转型的浪潮中,企业正迅速采纳云原生技术以实现敏捷性、可扩展性和弹性。本文深入探讨了如何利用Kubernetes这一领先的容器编排平台,结合微服务架构,构建和维护高效、可伸缩的云原生应用。通过分析现代软件设计原则和最佳实践,我们提出了一个综合指南,旨在帮助开发者和系统架构师优化云资源配置,提高部署流程的自动化水平,并确保系统的高可用性。
34 1
|
6天前
|
负载均衡 算法 应用服务中间件
【微服务系列笔记】负载均衡
本文介绍了负载均衡的概念和重要性,指出随着流量增长,通过垂直扩展和水平扩展来提升系统性能,其中水平扩展引入了负载均衡的需求。负载均衡的目标是将流量分布到多台服务器以提高响应速度和可用性,常见的硬件和软件负载均衡器包括F5、A10、Nginx、HAProxy和LVS等。 文章接着提到了Ribbon,这是一个客户端实现的负载均衡器,用于Spring Cloud中。Ribbon在发起REST请求时进行拦截,根据预设的负载均衡算法(如随机算法)选择服务器,并重构请求URI。文中还介绍了如何通过代码和配置文件两种方式自定义Ribbon的负载均衡策略。
45 3
|
6天前
|
Cloud Native Devops 持续交付
构建未来:云原生架构在企业数字化转型中的关键作用构建高效微服务架构:后端开发的新范式
【4月更文挑战第30天】 随着企业加速其数字化进程,云原生架构已成为支撑复杂、可伸缩和灵活应用的骨干。本文探讨了云原生技术的崛起,重点分析了其在促进业务敏捷性、提高运营效率及推动创新方面的核心价值。通过深入剖析云原生生态系统的关键技术组件,如容器化、微服务、持续集成/持续部署(CI/CD)和DevOps实践,揭示了企业如何利用这些技术来构建和维护高度可用且动态的IT环境。文章还提出了一个多维度的采纳框架,帮助企业评估和实施云原生解决方案,以实现真正的业务价值。 【4月更文挑战第30天】在现代软件开发的快速演变中,微服务架构已经成为一种领先的设计模式,用于构建可扩展、灵活且容错的应用程序。与传
|
6天前
|
负载均衡
Ribbon负载均衡策略
Ribbon负载均衡策略
|
6天前
|
负载均衡 算法 应用服务中间件
面试题:Nginx有哪些负载均衡算法?Nginx位于七层网络结构中的哪一层?
字节跳动面试题:Nginx有哪些负载均衡算法?Nginx位于七层网络结构中的哪一层?
50 0