文章目录
在几个集群访问策略中,都需要通过ClientManager拿到服务端连接实例,调用了ClientManger的getClient或getAvailableClients方法,在这两个方法内部,都先调用了DefaultClusterListener#getClientList拿到指定服务url的所有连接实例,紧接着调用RouteManger的route或getAvailableClients来过滤获取满足区域要求的连接实例。
ClientManger的两个方法实现如下:
public Client getClient(InvokerConfig<?> invokerConfig, InvocationRequest request, List<Client> excludeClients) {
// 拿到指定服务url的所有连接实例
List<Client> clientList = clusterListener.getClientList(invokerConfig);
// 浅拷贝
List<Client> clientsToRoute = new ArrayList<Client>(clientList);
if (excludeClients != null) {
// 排除部分库护短
clientsToRoute.removeAll(excludeClients);
}
// 通过路由管理器过滤获取一个连接实例
return routerManager.route(clientsToRoute, invokerConfig, request);
}
public List<Client> getAvailableClients(InvokerConfig<?> invokerConfig, InvocationRequest request) {
// 拿到指定服务url的所有连接实例
List<Client> clientList = clusterListener.getClientList(invokerConfig);
// 通过路由管理器过滤获取所有可能的连接实例
return routerManager.getAvailableClients(clientList, invokerConfig, request);
}
下面重点看看RouterManager如何应用特定的区域路由策略来过滤连接实例。
DefaultRouteManager
在RouterManager接口中,定义了两个路由策略的核心方法,分别是route和getAvailableClients,在默认子类DefaultRouteManager中实现。其中route方法中调用了getAvailableClients方法,然后选择特定的负载均衡策略选取特定的连接实例返回,具体实现如下:
public Client route(List<Client> clientList, InvokerConfig<?> invokerConfig, InvocationRequest request) {
if (logger.isDebugEnabled()) {
for (Client client : clientList) {
if (client != null) {
logger.debug("available service provider:\t" + client.getAddress());
}
}
}
// 获取所有可能的连接
List<Client> availableClients = getAvailableClients(clientList, invokerConfig, request);
// 通过客户端负载均衡策略选取合适的唯一客户端连接
Client selectedClient = select(availableClients, invokerConfig, request);
// 如果不是活跃的连接,重新选取
while (!selectedClient.isActive()) {
logger.info("[route] remove client:" + selectedClient);
// 去除不活跃的
availableClients.remove(selectedClient);
if (availableClients.isEmpty()) {
// 为空退出,避免死循环
break;
}
selectedClient = select(availableClients, invokerConfig, request);
}
// 没有获取到满足要求的活跃的连接
if (!selectedClient.isActive()) {
throw new ServiceUnavailableException("no available server exists for service[" + invokerConfig + "], env:"
+ ConfigManagerLoader.getConfigManager().getEnv());
}
return selectedClient;
}
/**
* 按照权重、分组、region规则过滤客户端选择 加入对oneway调用模式的优化判断
*/
public List<Client> getAvailableClients(List<Client> clientList, InvokerConfig<?> invokerConfig,
InvocationRequest request) {
// 是否启用区域路由策略
if (regionPolicyManager.isEnableRegionPolicy()) {
clientList = regionPolicyManager.getPreferRegionClients(clientList, invokerConfig, request);
}
List<Client> filteredClients = new ArrayList<Client>(clientList.size());
for (Client client : clientList) {
if (client != null) {
String address = client.getAddress();
// 获取地址连接权重
int weight = RegistryManager.getInstance().getServiceWeightFromCache(address);
if (client.isActive() && weight > 0) {
// 连接是激活状态且存在权重
filteredClients.add(client);
} else if (logger.isDebugEnabled()) {
logger.debug("provider status:" + client.isActive() + "," + weight);
}
}
}
// 为空说明没有活跃或地址权重>0的连接
if (filteredClients.isEmpty()) {
throw new ServiceUnavailableException("no available server exists for service[" + invokerConfig.getUrl()
+ "] and group[" + RegistryManager.getInstance().getGroup(invokerConfig.getUrl()) + "].");
}
return filteredClients;
}
在getAvailableClients方法实现中,开始的第一步操作是判断是否启用区域路由策略,如果启用了,则根据路由策略过滤出合适的连接实例。isEnableRegionPolicy和getPreferRegionClients方法定义在RegionPolicyManager,接下来看RegionPolicyManager的实现
RegionPolicyManager
RegionPolicyManager是一个枚举类,以此实现了单例对象的逻辑。在服务调用方初始化流程中,会调用InvokerBootStrap#startup方法,在这个方法里,会调用RegionPolicyManager.INSTANCE.init()来对区域路由管理器进行初始化,先来看这个方法实现:
public void init() {
if (!isInitialized) {
synchronized (RegionPolicyManager.class) {
if (!isInitialized) {
// 自动切换区域路由
register(AutoSwitchRegionPolicy.NAME, null, AutoSwitchRegionPolicy.INSTANCE);
// 基于权重区域路由
register(WeightBasedRegionPolicy.NAME, null, WeightBasedRegionPolicy.INSTANCE);
// 强制区域路由
register(ForceRegionPolicy.NAME, null, ForceRegionPolicy.INSTANCE);
// 如果启用路由策略,则初始化本地路由配置
if (configManager.getBooleanValue(KEY_ENABLEREGIONPOLICY, DEFAULT_ENABLEREGIONPOLICY)) {
initRegionsConfig();
} else {
logger.info("Region policy is disabled!");
}
// 注册配置监听器,监听路由相关配置变更,则重新初始化本地路由配置
configManager.registerConfigChangeListener(new InnerConfigChangeListener());
isInitialized = true;
}
}
}
}
在初始化过程中,分为两大块逻辑,第一块是三种路由策略的注册,第二块是本地路由配置的初始化。
区域路由基础配置
下面先看看几个共用的基础配置:
-
pigeon.regions.route.enable: 是否启用区域路由策略
-
pigeon.regions: 以ip网段划分区域的归属,配置示例如:
region1:110.120,172.23;region2:192.168;region3:120.128
,注意所有的ip段都为xxx.xxx的形式,即只保留ip高16位 -
pigeon.regions.prefer.xxx,其中xxx是划分的区域名,如前面的region1,region2,region3,具体配置示例如:
pigeon.regions.prefer.region1=region1:3,region2:1,region3:0
pigeon.regions.prefer.region2=region2:10,region3:3,region1:1
pigeon.regions.prefer.region3=region3:3,region1:1,region2:0
上面配置了特定region的访问优先级策略(冒号后面为region权重,用于weight based policy)
几点注意:
- 在pigeon中,region是地区概念,如北京、上海、深圳等,不做流量分配,只做灾备。即在一个 region 不可用时,可以切换到另一个 region。
- region层面不做流量分配的原因:一般跨region的访问延时很高,像北京到上海有30ms。正常情况下做流量分配会对服务性能造成比较大的影响。
- region 不可用的定义:region 内某一服务的可用服务器数量小于一定比例。
- 在特定的region下,有不同的ip段,每个ip段对应概念是逻辑机房(idc),在同region下的idc,在访问速度上没有明显区别,可以忽略地理上的差异。
- ldc 层面需要做流量分配,每个 ldc 需要定义各自承担的流量比例。
配置初始化
在initRegionsConfig方法中,对后两个上述配置进行了相关的初始化工作,看看具体的代码实现:
private synchronized void initRegionsConfig(String pigeonRegionsConfig, String regionsPreferConfig) {
try {
// 切分配置中的分号,参考示例配置:region1:110.120,172.24;region2:192.168;region3:120.128.223
String[] regionConfigs = pigeonRegionsConfig.split(";");
int regionCount = regionConfigs.length;
// 至少配置一个区域
if (regionCount <= 0) {
logger.error("Error! Please check regions config!");
return;
}
// 存储区域名
Set<String> regionSet = new HashSet<String>();
// 存储ip段->区域名的映射
Map<String, String> patternRegionNameMappings = new HashMap<String, String>();
for (String regionConfig : regionConfigs) {
// 切分区域名和ip段
String[] regionPatternMapping = regionConfig.split(":");
// 区域名
String regionName = regionPatternMapping[0];
// 切分多个ip段
String[] patterns = regionPatternMapping[1].split(",");
regionSet.add(regionName);
for (String pattern : patterns) {
patternRegionNameMappings.put(pattern, regionName);
}
}
//初始化local region
// 解析获取本地所属ip段,只取前两个逗号以找到ip段归属
String localRegionPattern = getPattern(configManager.getLocalIp());
if (patternRegionNameMappings.containsKey(localRegionPattern)) {
// 找到本机归属区域
String localRegionName = patternRegionNameMappings.get(localRegionPattern);
// 权重处理,先获取对应区域的路由优先配置
if (StringUtils.isBlank(regionsPreferConfig)) {
regionsPreferConfig = configManager.getStringValue(KEY_REGION_PREFER_BASE + localRegionName);
}
// 初始化本机的区域路由优先级配置
List<Region> regions = initRegionsWithPriority(regionsPreferConfig);
// 存储区域名->Region对象的一一对应
Map<String, Region> _regionMap = new HashMap<>();
// pigeon.regions中配置的所有区域都能找到对应的pigeon.regions.prefer.xxx区域路由优先级配置
// 需要数量和名字一一对应
if (regionSet.size() == regions.size()) {
for (Region region : regions) {
if (!regionSet.contains(region.getName())) {
logger.error("Error! Regions prefer not match regions config: " + region.getName());
return;
}
_regionMap.put(region.getName(), region);
}
//(re)init
regionArray = Collections.unmodifiableList(regions);// 下面的步骤都基于regionArray
// 建立ip段->Region的一一映射关系
initPatterRegionMappings(patternRegionNameMappings);// 初始化pattern region映射
// 记录本地对应Region
localRegion = getRegionByName(localRegionName);
regionMap = ImmutableMap.copyOf(_regionMap);
// 清空所有连接实例内部的region引用
clearRegion();
// 标志区域路由策略完成初始化,能正常使用
isEnabled = true;
logger.info("Region route policy switch on! Local region is: " + regionArray.get(0));
} else {
logger.error("Error! Regions prefer counts not match regions config!");
}
} else {
logger.error("Error! Can't init local region: " + configManager.getLocalIp());
}
} catch (Throwable t) {
logger.error("Error! Init region policy failed!", t);
}
}
在整个初始化过程中,最终初始化的几个配置:
- isEnabled=true:标志区域路由策略正式生效
- patternRegionMappings:pigeon.regions中配置的所有ip段和对应Region(name,priority,weight)的映射关系
- regionArray:pigeon.regions中配置的所有Region实例
- localRegion:根据本地ip,从patternRegionMappings获取的本地Region
- regionMap:regionName->Region实例映射关系
路由策略应用流程
在路由策略生效后,再回到DefaultRouteManager#getAvailableClients方法,第一行通过以下函数判断路由策略是否启用:
public boolean isEnableRegionPolicy() {
return configManager.getBooleanValue(KEY_ENABLEREGIONPOLICY, DEFAULT_ENABLEREGIONPOLICY) && isEnabled;
}
经过前面的配置初始化,isEnabled已被置为true,于是会通过调用regionPolicyManager#getPreferRegionClients来应用具体的路由策略,来看看相关实现:
public List<Client> getPreferRegionClients(List<Client> clientList, InvokerConfig<?> invokerConfig, InvocationRequest request) {
// 先根据配置获取具体的路由策略
RegionPolicy regionPolicy = getRegionPolicy(invokerConfig);
// 如果获取仍是空,使用AutoSwitchRegionPolicy
if (regionPolicy == null) {
regionPolicy = AutoSwitchRegionPolicy.INSTANCE;
}
// 应用相应的路由策略过滤出满足路由要求的连接
clientList = regionPolicy.getPreferRegionClients(clientList, request);
checkClientsNotNull(clientList, invokerConfig);
// 采样打点
Region regionSample = clientList.get(0).getRegion();
if (regionSample != null) {
monitor.logEvent("PigeonCall.region", request.getServiceName() + "#" + regionSample.getName(), "");
}
return clientList;
}
public RegionPolicy getRegionPolicy(InvokerConfig<?> invokerConfig) {
// 先通过serviceId加载
String serviceId = ServiceUtils.getServiceId(invokerConfig.getUrl(), invokerConfig.getSuffix());
RegionPolicy regionPolicy = regionPolicyMap.get(serviceId);
if (regionPolicy != null) {
return regionPolicy;
}
// 加载失败再用配置的regionPolicy字符串加载
regionPolicy = regionPolicyMap.get(invokerConfig.getRegionPolicy());
if (regionPolicy != null) {
return regionPolicy;
}
// 加载失败再应用默认配置的路由策略AutoSwitchRegionPolicy
if (DEFAULT_REGIONPOLICY != null) {
regionPolicy = regionPolicyMap.get(DEFAULT_REGIONPOLICY);
if (regionPolicy != null) {
regionPolicyMap.put(invokerConfig.getRegionPolicy(), regionPolicy);
return regionPolicy;
} else {
logger.warn("the regionPolicy[" + DEFAULT_REGIONPOLICY + "] is invalid, only support "
+ regionPolicyMap.keySet() + ".");
}
}
return null;
}
从上面可以看到,在根据配置获取到具体的路由策略后,是通过调用RegionPolicy#getPreferRegionClients方法来应用具体的路由策略。下面详细看看三种路由策略的原理:
AutoSwitchRegionPolicy 自动切换区域路由
AutoSwitchRegionPolicy是默认的路由策略。
路由规则
- 按照优先级选择region中的可用client连接,当region可用率低于设置的切换阈值时,依次选择下一个优先级的region。切换阈值regionSwitchRatio默认为0.5f,即可用的client连接低于50%为region不可用。
- 在autoSwitch策略下,通过修改开关值isIdcFilterEnable,可以开启本region中的idc过滤,当同时满足以下3个条件会优先路由本地idc连接:
- 同一ip段活跃的连接数idcActive > 同一ip段总连接数idcTotal * 配置比例idcFilterThresHoldRatio
- idcActive > 最小阈值idcFilterThresholdLeast
- 同一ip段活跃的连接数idcActive > region可用连接数active * 配置比例idcFilterThresHoldRatio
源码实现
基于上面路由规则,看看具体的源码实现:
public List<Client> getPreferRegionClients(List<Client> clientList, InvocationRequest request) {
return getRegionActiveClients(clientList, request);
}
private List<Client> getRegionActiveClients(List<Client> clientList, InvocationRequest request) {
int sizeBefore = clientList.size();
Map<Region, InnerRegionStat> regionStats = new HashMap<Region, InnerRegionStat>();
List<Region> regionArrays = Lists.newArrayList(regionPolicyManager.getRegionArray());
// 缓存每个region的统计信息
for (Region region : regionArrays) {
regionStats.put(region, new InnerRegionStat());
}
// 分发client的region统计信息,遍历连接实例,建立到regionStat的映射,统计总数、活跃连接数等信息,用于后续判断区域是否可用
for (Client client : clientList) {
try {
InnerRegionStat regionStat = regionStats.get(client.getRegion());
if (regionStat != null) {
regionStat.addTotal();
if (client.isActive() && registryManager.getServiceWeightFromCache(client.getAddress()) > 0) {
regionStat.addActive();
regionStat.addClient(client);
}
}
} catch (Throwable t) {
logger.error(t);
}
}
// 优先级大小按数组大小排列,遍历所有region
for (int i = 0; i < regionArrays.size(); ++i) {
Region region = regionArrays.get(i);
try {
InnerRegionStat regionStat = regionStats.get(region);
// 本地idc,即同一ip段相关信息
int total = regionStat.getTotal();
int active = regionStat.getActive();
List<Client> regionClientList = regionStat.getClientList();
if (i == 0 && isIdcFilterEnable && regionClientList.size() > 0) { // 开启本地idc(region(0))优先
int idcTotal = 0;
int idcActive = 0;
List<Client> idcClientList = new ArrayList<Client>();
for (Client client : regionClientList) {
// 判断是否为本地idc
if (RegionUtils.isInLocalIdc(client.getHost())) {
// 统计本地idc的总连接实例数和活跃连接实例数
idcClientList.add(client);
++idcTotal;
if (client.isActive() && registryManager.getServiceWeightFromCache(client.getAddress()) > 0) {
++idcActive;
}
}
}
// 计算本地idc有效最小连接数阈值
float idcLeast = idcFilterThresHoldRatio * idcTotal;
// idc可用client比例合格
if (idcTotal > 0 && idcActive > 0 && idcActive >= idcLeast) {
if (idcActive > idcFilterThresholdLeast || idcActive > idcFilterThresHoldRatio * active) {
// idc可用client数量合格 或 idc可用client数量占region可用client数量的比例合格
monitor.logEvent("PigeonCall.idc",
request.getServiceName() + "#" + RegionUtils.getLocalIdc(), "");
return idcClientList;
}
}
}
float least = regionSwitchRatio * total;
// 判断当前活跃的连接实例是否大于阈值,大于则返回当前区域对应的连接实例,否则进入下一个区域的连接判断
if (total > 0 && active > 0 && active >= least) {
if (logger.isDebugEnabled()) {
logger.debug("b: " + sizeBefore + ", a:" + regionClientList.size());
}
return regionClientList;
} else {
if (logger.isDebugEnabled()) {
logger.debug(request.getServiceName() + " skipped region " + region.getName()
+ ", available clients less than " + least);
}
monitor.logEvent("PigeonCall.regionUnavailable",
request.getServiceName() + "#" + region.getName(), "");
}
} catch (Throwable t) {
logger.error(t);
} finally {
//todo if force region, maybe here
}
}
return clientList;
}
WeightBasedRegionPolicy 基于权重区域路由
路由规则
按照region权重,随机选择特定region中的可用client连接。前面通过pigeon.regions.prefer.xxx配置了客户端所在区域的优先规则,假设客户端机器所在区域为region1,则依据配置:pigeon.regions.prefer.region1=region1:5,region2:3,region3:1
,会先对所有的客户端连接实例归类到3个区域。计算总权重为9,先初始化n=0到9的随机数,在regionSet中遍历region,判断n所属region权重区域,如region1=[0,5],region2=[6,8],region3=9,根据n的值,判断所属region,返回相应region的连接,以此实现基于权重的区域路由。
源码实现
基于上面的路由规则,看看具体的代码实现:
public List<Client> getPreferRegionClients(List<Client> clientList, InvocationRequest request) {
return getRegionActiveClients(clientList, request);
}
// 实际调用下面函数
private List<Client> getRegionActiveClients(List<Client> clientList, InvocationRequest request) {
// 分region存储clients
Map<Region, List<Client>> regionClientsMap = new HashMap<Region, List<Client>>();
List<Region> regionArrays = Lists.newArrayList(regionPolicyManager.getRegionArray());
for (Region region : regionArrays) {
regionClientsMap.put(region, new ArrayList<Client>());
}
// 建立Region->连接实例List的映射关系
for (Client client : clientList) {
List<Client> regionClients = regionClientsMap.get(client.getRegion());
if (regionClients != null) {
regionClients.add(client);
}
}
// 初始化region中存在可用client的权重和
Integer weightSum = 0;
Set<Region> regionSet = new HashSet<Region>();
for (Region region : regionClientsMap.keySet()) {
if (regionClientsMap.get(region).size() > 0) {
weightSum += region.getWeight();
regionSet.add(region);
}
}
if (weightSum <= 0) {
throw new RouteException("Error: weightSum=" + weightSum.toString());
}
// 权重随机算法
Integer n = random.nextInt(weightSum); // n in [0, weightSum)
Integer m = 0;
// 判断n属于regionSet中哪一段region
for (Region region : regionSet) {
int weight = region.getWeight();
List<Client> regionClientList = regionClientsMap.get(region);
if (m <= n && n < m + weight) {
return regionClientList;
}
m += weight;
}
return clientList;
}
ForceRegionPolicy 强制区域路由
路由规则
ForceRegionPolicy可以当作是AutoSwitchRegionPolicy的简化版,ForceRegionPolicy按照配置的region优先级,根据forceRegionConf切换规则判断是否使用优先region。切换规则参见AutoSwitchRegionPolicy。两个策略的区别是ForceRegionPolicy使用了同一路由优先级配置(pigeon.regions.force.config),不针对不同的区域有不同的优先级策略(pigeon.regions.prefer.xxx),且没有idc的细化路由策略。
源码实现
忽略部分代码,核心部分实现代码如下:
public class ForceRegionPolicy implements RegionPolicy {
private ForceRegionPolicy() {
// 读取配置
String forceRegionConf = configManager.getStringValue(KEY_REGION_FORCE_CONFIG, "shanghai,beijing");
// 解析配置,得到路由优先级配置
initForceRegionConfig(forceRegionConf);
// 注册配置动态变更监听
configManager.registerConfigChangeListener(new InnerConfigChangeListener());
}
private void initForceRegionConfig(String forceRegionConfig) {
// 按逗号切分
if (StringUtils.isNotBlank(forceRegionConfig)) {
forceRegPrefer = forceRegionConfig.split(",");
}
}
@Override
public List<Client> getPreferRegionClients(List<Client> clientList, InvocationRequest request) {
List<Region> forceRegions = getForceRegionList();
if (forceRegions != null) {
return getRegionActiveClients(clientList, request, forceRegions);
}
return clientList;
}
private List<Region> getForceRegionList() {
// 要求强制路由的区域和pigeon.regions配置的区域有唯一映射
Map<String, Region> regionMap = regionPolicyManager.getRegionMap();
if (forceRegPrefer.length != regionMap.size()) {
logger.debug("Force region config size not match regions config, please check!");
return null;
}
List<Region> _regionArr = new ArrayList<>();
for (String reg : forceRegPrefer) {
Region _region = regionMap.get(reg);
if (_region == null) {
logger.debug("Force region config not match regions config: " + reg);
return null;
}
_regionArr.add(_region);
}
return _regionArr;
}
private List<Client> getRegionActiveClients(List<Client> clientList, InvocationRequest request, List<Region> regionArrays) {
int sizeBefore = clientList.size();
Map<Region, InnerRegionStat> regionStats = new HashMap<Region, InnerRegionStat>();
for (Region region : regionArrays) {
// 缓存每个region的统计信息
regionStats.put(region, new InnerRegionStat());
}
for (Client client : clientList) {
// 分发client的region统计信息,遍历连接实例,建立到regionStat的映射,统计总数、活跃连接数等信息,用于后续判断区域是否可用
try {
InnerRegionStat regionStat = regionStats.get(client.getRegion());
if (regionStat != null) {
regionStat.addTotal();
if (client.isActive() && registryManager.getServiceWeightFromCache(client.getAddress()) > 0) {
regionStat.addActive();
regionStat.addClient(client);
}
}
} catch (Throwable t) {
logger.error(t);
}
}
for (int i = 0; i < regionArrays.size(); ++i) {
// 优先级大小按数组大小排列,遍历所有region
Region region = regionArrays.get(i);
try {
InnerRegionStat regionStat = regionStats.get(region);
int total = regionStat.getTotal();
int active = regionStat.getActive();
List<Client> regionClientList = regionStat.getClientList();
float least = regionSwitchRatio * total;
// 判断当前活跃的连接实例是否大于阈值,大于则返回当前区域对应的连接实例,否则进入下一个区域的连接判断
if (total > 0 && active > 0 && active >= least) {
if (logger.isDebugEnabled()) {
logger.debug("b: " + sizeBefore + ", a:" + regionClientList.size());
}
return regionClientList;
} else {
if (logger.isDebugEnabled()) {
logger.debug(request.getServiceName() + " skipped region " + region.getName()
+ ", available clients less than " + least);
}
monitor.logEvent("PigeonCall.forceRegionUnavailable",
request.getServiceName() + "#" + region.getName(), "");
}
} catch (Throwable t) {
logger.error(t);
} finally {
// maybe release work
}
}
return clientList;
}
}