当你选择了一种语言,意味着你还选择了一组技术、一个社区 ------ 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
:存储了Server
和ServerStats
的对应关系。老版本使用的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) {
...
}
除此之外,它代理还代理了ServerStats
和ZoneStats
常用的一些方法:
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高工、架构师 系列群大家庭学习和交流。
- [享学Netflix] 一、Apache Commons Configuration:你身边的配置管理专家
- [享学Netflix] 二、Apache Commons Configuration事件监听机制及使用ReloadingStrategy实现热更新
- [享学Netflix] 三、Apache Commons Configuration2.x全新的事件-监听机制
- [享学Netflix] 四、Apache Commons Configuration2.x文件定位系统FileLocator和FileHandler
- [享学Netflix] 五、Apache Commons Configuration2.x别样的Builder模式:ConfigurationBuilder
- [享学Netflix] 六、Apache Commons Configuration2.x快速构建工具Parameters和Configurations
- [享学Netflix] 七、Apache Commons Configuration2.x如何实现文件热加载/热更新?
- [享学Netflix] 八、Apache Commons Configuration2.x相较于1.x使用上带来哪些差异?
- [享学Netflix] 九、Archaius配置管理库:初体验及基础API详解
- [享学Netflix] 十、Archaius对Commons Configuration核心API Configuration的扩展实现
- [享学Netflix] 十一、Archaius配置管理器ConfigurationManager和动态属性支持DynamicPropertySupport
- [享学Netflix] 十二、Archaius动态属性DynamicProperty原理详解(重要)
- [享学Netflix] 十三、Archaius属性抽象Property和PropertyWrapper详解
- [享学Netflix] 十四、Archaius如何对多环境、多区域、多云部署提供配置支持?
- [享学Netflix] 十五、Archaius和Spring Cloud的集成:spring-cloud-starter-netflix-archaius
- [享学Netflix] 十六、Hystrix断路器:初体验及RxJava简介
- [享学Netflix] 十七、Hystrix属性抽象以及和Archaius整合实现配置外部化、动态化
- [享学Netflix] 十八、Hystrix配置之:全局配置和实例配置
- [享学Netflix] 十九、Hystrix插件机制:SPI接口介绍和HystrixPlugins详解
- [享学Netflix] 二十、Hystrix跨线程传递数据解决方案:HystrixRequestContext
- [享学Netflix] 二十一、Hystrix指标数据收集(预热):滑动窗口算法(附代码示例)
- [享学Netflix] 二十二、Hystrix事件源与事件流:HystrixEvent和HystrixEventStream
- [享学Netflix] 二十三、Hystrix桶计数器:BucketedCounterStream
- [享学Netflix] 二十四、Hystrix在滑动窗口内统计:BucketedRollingCounterStream、HealthCountsStream
- [享学Netflix] 二十五、Hystrix累计统计流、分发流、最大并发流、配置流、功能流(附代码示例)
- [享学Netflix] 二十六、Hystrix指标数据收集器:HystrixMetrics(HystrixDashboard的数据来源)
- [享学Netflix] 二十七、Hystrix何为断路器的半开状态?HystrixCircuitBreaker详解
- [享学Netflix] 二十八、Hystrix事件计数器EventCounts和执行结果ExecutionResult
- [享学Netflix] 二十九、Hystrix执行过程核心接口:HystrixExecutable、HystrixObservable和HystrixInvokableInfo
- [享学Netflix] 三十、Hystrix的fallback回退/降级逻辑源码解读:getFallbackOrThrowException
- [享学Netflix] 三十一、Hystrix触发fallback降级逻辑的5种情况及代码示例
- [享学Netflix] 三十二、Hystrix抛出HystrixBadRequestException异常为何不会触发熔断?
- [享学Netflix] 三十三、Hystrix执行目标方法时,如何调用线程池资源?
- [享学Netflix] 三十四、Hystrix目标方法执行逻辑源码解读:executeCommandAndObserve
- [享学Netflix] 三十五、Hystrix执行过程集大成者:AbstractCommand详解
- [享学Netflix] 三十六、Hystrix请求命令:HystrixCommand和HystrixObservableCommand
- [享学Netflix] 三十七、源生Ribbon介绍 — 客户端负载均衡器
- [享学Netflix] 三十八、Ribbon核心API源码解析:ribbon-core(一)IClient请求客户端
- [享学Netflix] 三十九、Ribbon核心API源码解析:ribbon-core(二)IClientConfig配置详解
- [享学Netflix] 四十、Ribbon核心API源码解析:ribbon-core(三)RetryHandler重试处理器
- [享学Netflix] 四十一、Ribbon核心API源码解析:ribbon-core(四)ClientException客户端异常
- [享学Netflix] 四十二、Ribbon的LoadBalancer五大组件之:IPing心跳检测
- [享学Netflix] 四十三、Ribbon的LoadBalancer五大组件之:ServerList服务列表
- [享学Netflix] 四十四、netflix-statistics详解,手把手教你写个超简版监控系统
- [享学Netflix] 四十五、Ribbon服务器状态:ServerStats及其断路器原理