[享学Netflix] 四十六、Ribbon负载均衡策略服务器状态总控:LoadBalancerStats

当你选择了一种语言,意味着你还选择了一组技术、一个社区 ------ Joshua Bloch

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning

前言

上篇文章 介绍了Ribbon它对每台Server的状态管理ServerStats,当然它也包括了数据收集以及数据发布。作为更宏观的负载均衡器LoadBalancer,做的就是一种类似适配工作:后面管理着一批Server,然后由LB负责挑选出一个最为合适的Server提供服务。

Ribbon使用LoadBalancerStats来管控所有的zone、所有的Server。它自己并不收集指标数据,它的作用是更为宏观的分析、总控。


正文

LoadBalancerStats

用作操作特性和统计信息的存储库LaodBalancer中的每个节点/服务器,这些信息可以用来观察和理解运行时行为的LoadBalancer,用来决定负载平衡策略。简单的说,它就是作为ServerStats实例列表的容器,统一维护(当然还有zone区域的概念)。


缓存类型的成员属性

LoadBalancerStats内部有三个缓存类型的成员变量,一是upServerListZoneMap,二是serverStatsCache,他俩的关系如下图所示:

在这里插入图片描述
三大缓存变量如下:

// 它实现了IClientConfigAware接口,所以很方便的得到IClientConfig配置
public class LoadBalancerStats implements IClientConfigAware {
    volatile Map<String, ZoneStats> zoneStatsMap = new ConcurrentHashMap<>();
    volatile Map<String, List<? extends Server>> upServerListZoneMap = new ConcurrentHashMap<>();

	// 该变量最初使用的ConcurrentHashMap缓存
	// Map<Server,ServerStats> serverStatsMap = new ConcurrentHashMap<Server,ServerStats>();

    private static final DynamicIntProperty SERVERSTATS_EXPIRE_MINUTES = 
        DynamicPropertyFactory.getInstance().getIntProperty("niws.loadbalancer.serverStats.expire.minutes", 30);
	private final LoadingCache<Server, ServerStats> serverStatsCache = CacheBuilder.newBuilder()
				// 在一定时间内没有读写,会移除该key
				// 在30s内没有读写该Server的时候会移除对应的没有被访问的key
				.expireAfterAccess(SERVERSTATS_EXPIRE_MINUTES.get(), TimeUnit.MINUTES)
				// 移除的时候把其pulish的功能也关了(不然定时任务一直在运行)
				.removalListener(notification -> notification.getValue().close());
				// 首次get木有的话,就会调用此方法给你新建一个新的Server实例
				.build(server -> createServerStats(server));

	// 为何这里默认值为何是1000和1000???和ServerStats里的常量值并不一样哦
    protected ServerStats createServerStats(Server server) {
        ServerStats ss = new ServerStats(this);
        //configure custom settings
        ss.setBufferSize(1000);
        ss.setPublishInterval(1000);                    
        // 请务必调用此方法完成初始化
        ss.initialize(server);
        return ss;        
    }
}
  • zoneStatsMap:每个zone对应一个ZoneStats,代表着该可用区的健康状态
  • upServerListZoneMap:存储了zone和server状态ZoneStats的对应关系(一个zone内可以有多台Server)
  • serverStatsCache:存储了ServerServerStats的对应关系。老版本使用的Map缓存的,新版本使用了guaua的cache(增加了过期时间,对内存更友好)
    • Server默认的缓存时长是30s,请尽量保持此值>=熔断最大时长的值(它默认也是30s)

根据以上结构,能得到每个Server的状态,进而可计算出每个zone可用区的状态


其它成员属性
LoadBalancerStats:

    private static final String PREFIX = "LBStats_";
    String name;

	// 这三个参数对应ServerStats的三个同名参数,已解释
    private volatile CachedDynamicIntProperty connectionFailureThreshold;
    private volatile CachedDynamicIntProperty circuitTrippedTimeoutFactor;
    private volatile CachedDynamicIntProperty maxCircuitTrippedTimeout;

	// ============赋值============
	// 这三个属性的赋值可以使用和name关联的个性化赋值方式,而非defualt全局公用
	// 这是和ServerStat默认值不同之处
    CachedDynamicIntProperty getConnectionFailureCountThreshold() {
        if (connectionFailureThreshold == null) {
            connectionFailureThreshold = new CachedDynamicIntProperty(
                    "niws.loadbalancer." + name + ".connectionFailureCountThreshold", 3);
        }
        return connectionFailureThreshold;
    }
    ...
  • PREFIX@Monitor监控的前缀,可忽略
  • name:负载均衡器LoadBalancer的名称,虽然不强制跟ClientName一致,但事实情况是它的值就是clientConfig.getClientName()
  • connectionFailureThreshold/circuitTrippedTimeoutFactor/maxCircuitTrippedTimeout三个参数最终是落到ServerStats身上,只不过这里配置是和ClientName相关里,具有更强的定制性

重要方法介绍

介绍完了属性,就来到方法。在介绍普通成员方法之前,需先介绍重要的方法。其中最重要方法便是getZoneSnapshot(),先来认识一下。


ZoneSnapshot

zone的快照,一个简单POJO。涵盖有如下信息:

public class ZoneSnapshot {
    final int instanceCount;
    final double loadPerServer;
    final int circuitTrippedCount;
    final int activeRequestsCount;
    
	... // 省略普通的get/set方法
}
  • instanceCount:实例总数
  • loadPerServer:平均load(活跃请求量/实例数量)
  • circuitTrippedCount:打开断路器了的实例总数
  • activeRequestsCount:总活跃请求量(所有server加起来)

LoadBalancerStats#getZoneSnapshot(String zone)

根据zone获取到server列表,根据server获取到统计信息,从而计算出整个zone的快照状态,包含的信息就是一个ZoneSnapshot实例。

LoadBalancerStats:

	// zone不区分大小写。拿到该zone的一个快照,它的一句是该zone下的ServerList
    public ZoneSnapshot getZoneSnapshot(String zone) {
        if (zone == null) {
            return new ZoneSnapshot();
        }
        zone = zone.toLowerCase();
        List<? extends Server> currentList = upServerListZoneMap.get(zone);
        return getZoneSnapshot(currentList);        
    }
    // 根据这些servers,计算出快照值(4个属性)
    public ZoneSnapshot getZoneSnapshot(List<? extends Server> servers) {
		int instanceCount = servers.size();
		int activeConnectionsCount = 0;
		int activeConnectionsCountOnAvailableServer = 0;
		int circuitBreakerTrippedCount = 0;
		
		// 从每个Server身上统计数据
		for (Server server: servers) {
			// 先拿到每个Server自己所属的stat
			ServerStats stat = getSingleServerStat(server);  
			if (stat.isCircuitBreakerTripped(currentTime)) {
				circuitBreakerTrippedCount++;
			}
			...
		}
		...
		return new ZoneSnapshot(instanceCount, circuitBreakerTrippedCount, activeConnectionsCount, loadPerServer);
    }

给一批ServerList“照快照”的逻辑并不复杂,总而言之:根据该zone下所有的Server状态信息,统计出四大指标的值作为一个快照对象。并且针对快照的4大指标,也提供了快捷的直接访问:

LoadBalancerStats:

    public int getActiveRequestsCount(String zone) {
        return getZoneSnapshot(zone).getActiveRequestsCount();
    }
    public double getActiveRequestsPerServer(String zone) {
        return getZoneSnapshot(zone).getLoadPerServer();
    }
    public int getCircuitBreakerTrippedCount(String zone) {
        return getZoneSnapshot(zone).getCircuitTrippedCount();
    }
    // 获取所有可用区,所有可用区内的熔断了的Server总数
	public int getCircuitBreakerTrippedCount() { ... }

成员方法

下面是LoadBalancerStats普通成员方法例举:

LoadBalancerStats:

	// 给name赋值为ClientName
    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        this.name = clientConfig.getClientName();
        Monitors.registerObject(name, this);
    }

	// 增加一台Server到缓存里。该方法仅被下面调用
	public void addServer(Server server) { ... }
	// 这个update的作用有两个:
	// 1、touch一下,保证缓存里的不过期
	// 2、touch的时候发现缓存木有了,就给新建一个
    public void updateServerList(List<Server> servers){
        for (Server s: servers){
            addServer(s);
        }
    }

	// 获取Server对应的ServerStats实例:从缓存里获取
    protected ServerStats getServerStats(Server server) { ... }
    private ZoneStats getZoneStats(String zone) { ... }

	// 拿到所有的可用的zone区域(有对应的up的Server的就叫有用的zone,叫可用区)
    public Set<String> getAvailableZones() {
        return upServerListZoneMap.keySet();
    }
    public ServerStats getSingleServerStat(Server server) {
        return getServerStats(server);
    }
    public Map<Server,ServerStats> getServerStats(){
        return serverStatsCache.asMap();
    }

    // 用心的Map代替掉缓存内容。每次都调用一次getZoneStats()是为了确保每个zone都能有一个ZoneStats实例
    // updateZoneServerMapping是唯一给upServerListZoneMap赋值的方法哦~~~
    // 改方法会在DynamicServerListLoadBalancer#setServerListForZones调用
    public void updateZoneServerMapping(Map<String, List<Server>> map) {
        upServerListZoneMap = new ConcurrentHashMap<String, List<? extends Server>>(map);
        for (String zone: map.keySet()) {
            getZoneStats(zone);
        }
    }

	// 获取指定zone下Server实例总数(从upServerListZoneMap缓存里拿)
    public int getInstanceCount(String zone) {
        ...
    }

除此之外,它代理还代理了ServerStatsZoneStats常用的一些方法:

LoadBalancerStats:

	// 该方法的效果和ZoneSnapshot.loadPerServer效果基本一致
	// 该方法并没有任何调用,可忽略
	public int getCongestionRatePercentage(String zone) {
		...
		return (int) ((activeConnectionsCount + circuitBreakerTrippedCount) * 100L / serverCount); 
	}

	// 记录响应时间数据:dataDist和responseTimeDist会记录
    public void noteResponseTime(Server server, double msecs){
        ServerStats ss = getServerStats(server);  
        ss.noteResponseTime(msecs);
    }

    public void incrementNumRequests(){
        totalRequests.incrementAndGet();
    }
    public void incrementActiveRequestsCount(Server server) {
        ServerStats ss = getServerStats(server); 
        ss.incrementActiveRequestsCount();
    }
    ...

	// 判断当前Server是否已经处于熔断状态
    public boolean isCircuitBreakerTripped(Server server) {
        ServerStats ss = getServerStats(server);
        return ss.isCircuitBreakerTripped();
    }
    ...

	// 打理ZoneStats的方法
    public void incrementZoneCounter(Server server) {
        String zone = server.getZone();
        if (zone != null) {
            getZoneStats(zone).incrementCounter();
        }
    }

ZoneStats

它代表一个可用区的状态,它仅被LoadBalancerStats所使用。非常简单,几乎所有方法都由LoadBalancerStats代理完成,略。


代码示例

@Test
public void fun5() throws InterruptedException {
    LoadBalancerStats lbs = new LoadBalancerStats("YoutBatman");

    // 添加Server
    List<Server> serverList = new ArrayList<>();
    serverList.add(createServer("华南", 1));
    serverList.add(createServer("华东", 1));
    serverList.add(createServer("华东", 2));

    serverList.add(createServer("华北", 1));
    serverList.add(createServer("华北", 2));
    serverList.add(createServer("华北", 3));
    serverList.add(createServer("华北", 4));
    lbs.updateServerList(serverList);

    Map<String, List<Server>> zoneServerMap = new HashMap<>();
    // 模拟向每个Server发送请求  记录ServerStatus数据
    serverList.forEach(server -> {
        ServerStats serverStat = lbs.getSingleServerStat(server);
        request(serverStat);

        // 顺便按照zone分组
        String zone = server.getZone();
        if (zoneServerMap.containsKey(zone)) {
            zoneServerMap.get(zone).add(server);
        } else {
            List<Server> servers = new ArrayList<>();
            servers.add(server);
            zoneServerMap.put(zone, servers);
        }
    });
    lbs.updateZoneServerMapping(zoneServerMap);
    // 从lbs里拿到一些监控数据
    monitor(lbs);

    TimeUnit.SECONDS.sleep(500);
}


// 单独线程模拟刷页面,获取监控到的数据
private void monitor(LoadBalancerStats lbs) {
    List<String> zones = Arrays.asList("华南", "华东", "华北");
    new Thread(() -> {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        executorService.scheduleWithFixedDelay(() -> {
            zones.forEach(zone -> {
                System.out.println("区域[" + zone + "]概要:");
                int instanceCount = lbs.getInstanceCount(zone);
                int activeRequestsCount = lbs.getActiveRequestsCount(zone);
                double activeRequestsPerServer = lbs.getActiveRequestsPerServer(zone);
                ZoneSnapshot zoneSnapshot = lbs.getZoneSnapshot(zone);

                System.out.printf("实例总数:%s,活跃请求总数:%s,平均负载:%s\n", instanceCount, activeRequestsCount, activeRequestsPerServer);
                System.out.println(zoneSnapshot);
            });
            System.out.println("======================================================");
        }, 5, 5, TimeUnit.SECONDS);
    }).start();
}


// 请注意:请必须保证Server的id不一样,否则放不进去List的(因为Server的equals hashCode方法仅和id有关)
// 所以此处使用index作为port,以示区分
private Server createServer(String zone, int index) {
    Server server = new Server("www.baidu" + zone + ".com", index);
    server.setZone(zone);
    return server;
}


// 多线程,模拟请求
private void request(ServerStats serverStats) {
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            while (true) {
                // 请求之前 记录活跃请求数
                serverStats.incrementActiveRequestsCount();
                serverStats.incrementNumRequests();
                long rt = doSomething();
                // 请求结束, 记录响应耗时
                serverStats.noteResponseTime(rt);
                serverStats.decrementActiveRequestsCount();
            }
        }).start();
    }
}

// 模拟请求耗时,返回耗时时间
private long doSomething() {
    try {
        int rt = randomValue(10, 200);
        TimeUnit.MILLISECONDS.sleep(rt);
        return rt;
    } catch (InterruptedException e) {
        e.printStackTrace();
        return 0L;
    }
}

// 本地使用随机数模拟数据收集
private int randomValue(int min, int max) {
    return min + (int) (Math.random() * ((max - min) + 1));
}

运行程序,控制台打印:

区域[华南]概要:
实例总数:1,活跃请求总数:5,平均负载:5.0
ZoneSnapshot [instanceCount=1, loadPerServer=5.0, circuitTrippedCount=0, activeRequestsCount=5]
区域[华东]概要:
实例总数:2,活跃请求总数:10,平均负载:5.0
ZoneSnapshot [instanceCount=2, loadPerServer=5.0, circuitTrippedCount=0, activeRequestsCount=10]
区域[华北]概要:
实例总数:4,活跃请求总数:20,平均负载:5.0
ZoneSnapshot [instanceCount=4, loadPerServer=5.0, circuitTrippedCount=0, activeRequestsCount=20]
======================================================
区域[华南]概要:
实例总数:1,活跃请求总数:5,平均负载:5.0
ZoneSnapshot [instanceCount=1, loadPerServer=5.0, circuitTrippedCount=0, activeRequestsCount=5]
区域[华东]概要:
实例总数:2,活跃请求总数:10,平均负载:5.0
ZoneSnapshot [instanceCount=2, loadPerServer=5.0, circuitTrippedCount=0, activeRequestsCount=10]
区域[华北]概要:
实例总数:4,活跃请求总数:20,平均负载:5.0
ZoneSnapshot [instanceCount=4, loadPerServer=5.0, circuitTrippedCount=0, activeRequestsCount=20]
======================================================
...

可能你会惊讶,为毛活跃请求总数是恒定的呢???这是因为request()方法它针对每台Server都是启用的5个线程,所以该区域zone下的活跃请求永远是5 * 实例总数喽。

小细节:平均负载ZoneSnapshot.loadPerServer的值是个数值,并不是百分比哦(它表示当前时刻有N个活跃请求在我这处理,这就是负载)

为了让结果“动起来”,我们只需要改变request()方法,让其并发数随机起来即可:

private void request(ServerStats serverStats) {
    new Thread(() -> {
        // 每10ms发送一个请求(每个请求处理10-200ms的时间),持续不断
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
        executorService.scheduleWithFixedDelay(() -> {
            new Thread(() -> {
                // 请求之前 记录活跃请求数
                serverStats.incrementActiveRequestsCount();
                serverStats.incrementNumRequests();
                long rt = doSomething();
                // 请求结束, 记录响应耗时
                serverStats.noteResponseTime(rt);
                serverStats.decrementActiveRequestsCount();
            }).start();
        }, 10, 10, TimeUnit.MILLISECONDS);
    }).start();
}

再次运行程序,打印:

区域[华南]概要:
实例总数:1,活跃请求总数:10,平均负载:10.0
ZoneSnapshot [instanceCount=1, loadPerServer=10.0, circuitTrippedCount=0, activeRequestsCount=10]
区域[华东]概要:
实例总数:2,活跃请求总数:17,平均负载:8.5
ZoneSnapshot [instanceCount=2, loadPerServer=8.5, circuitTrippedCount=0, activeRequestsCount=17]
区域[华北]概要:
实例总数:4,活跃请求总数:34,平均负载:8.5
ZoneSnapshot [instanceCount=4, loadPerServer=8.5, circuitTrippedCount=0, activeRequestsCount=34]
======================================================
区域[华南]概要:
实例总数:1,活跃请求总数:10,平均负载:10.0
ZoneSnapshot [instanceCount=1, loadPerServer=10.0, circuitTrippedCount=0, activeRequestsCount=10]
区域[华东]概要:
实例总数:2,活跃请求总数:20,平均负载:10.0
ZoneSnapshot [instanceCount=2, loadPerServer=10.0, circuitTrippedCount=0, activeRequestsCount=20]
区域[华北]概要:
实例总数:4,活跃请求总数:41,平均负载:10.25
ZoneSnapshot [instanceCount=4, loadPerServer=10.25, circuitTrippedCount=0, activeRequestsCount=41]
======================================================
区域[华南]概要:
实例总数:1,活跃请求总数:9,平均负载:9.0
ZoneSnapshot [instanceCount=1, loadPerServer=9.0, circuitTrippedCount=0, activeRequestsCount=9]
区域[华东]概要:
实例总数:2,活跃请求总数:23,平均负载:11.5
ZoneSnapshot [instanceCount=2, loadPerServer=11.5, circuitTrippedCount=0, activeRequestsCount=23]
区域[华北]概要:
实例总数:4,活跃请求总数:31,平均负载:7.75
ZoneSnapshot [instanceCount=4, loadPerServer=7.75, circuitTrippedCount=0, activeRequestsCount=31]
======================================================
...

此处10ms发送一个请求,每个请求处理10ms到200ms之间,几乎看不到请求积压。但是,但是,但是你若把问题放大:比如把rt响应时间改为10s,你会明显的看到活跃请求数积压严重,越来越多,从而负载也越来越高。所以说:如果你的某个接口处理过慢,要是没有隔离、熔断机制的话,很容易打垮整个Server的哦~


总结

关于Ribbon的LoadBalancer总控:LoadBalancerStats部分的内容就先介绍到这,它是在负载均衡器LoadBalancer在执行负载均衡算法,找到一个最合适Server时非常重要的一个数据参考类。

LoadBalancerStats它相当于一个总控,它管理着一批Server的状态。所以它既能够得到可用区的状态信息,亦能得到每台Server的状态信息,从而可以做到负载均衡策略基于可用区、基于Server均可。
分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了362 篇原创文章 · 获赞 531 · 访问量 48万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/104912207
今日推荐