一、背景
Hystrix 是 Netlifx 开源的一款容错框架,防雪崩利器,具备服务降级,服务熔断,依赖隔离,监控 (Hystrix Dashboard) 等性能。
只管说 Hystrix 官网已不再保护,且有 Alibaba Sentinel 等新框架抉择,但从组件成熟度和利用案例等方面看,其实还是有很多我的项目在持续应用 Hystrix 中,自己所参加的我的项目就是其一。故联合集体的 Hystrix 实战经验与大家分享交换。
二、经验总结
2.1 隔离策略的抉择
Hystrix 提供两种资源隔离策略,线程池和信号量。它们之间的异同点如下:
而在应用缓存 (本地内存缓存更适宜该场景,Redis 等网络缓存须要评估) 时,咱们能够应用信号量隔离策略,因为这类服务响应快,不会占用容器线程太长时间,而且也缩小了线程切换的一些开销,进步了服务效率。
具体应用哪种策略,需依据业务场景综合评估。个别状况下,举荐应用线程池隔离。
2.2 线程池大小与超时工夫设置
在线程池隔离策略下,线程池大小及超时工夫的设置至关重要,间接影响着零碎服务的响应能力。如线程池大小若设置的太大会造成资源节约及线程切换等开销;若设置的太小又支撑不了用户申请,造成申请排队。而超时工夫设置的太长会呈现局部长耗时申请阻塞线程,造成其它失常申请排队期待;若设置的太短又会造成太多失常申请被熔断。
对此 Hystrix 官网给的倡议如图:
即转换为以下计算公式:
- 线程池大小 = 服务 TP99 响应时长(单位秒)* 每秒申请量 + 冗余缓冲值
- 超时工夫(单位毫秒)= 1000(毫秒)/ 每秒申请量
例如某服务 TP99 状况下每秒钟会接管 30 个申请,而后每个申请的响应时长是 200ms,按如上公式计算可得:线程池大小 = 0.2 * 30 + 4(冗余缓冲值)= 10,超时工夫 = 300ms
2.3 注解叠加
在理论开发中可能会遇到某内部调用办法有 Hystrix 注解与其它注解一起应用的状况,例如查询方法加上缓存注解。此时需特地留神注解间的执行程序,避免出现非预期的后果:
- 缓存注解未失效
此时 Hystrix 注解切面的执行是在最外层,因为 Hystrix 外部执行是通过 ProceedingJoinPoint.getTarget()获取指标对象,应用反射调用的形式间接执行到指标对象办法上,从而造成两头其它注解逻辑失落。可通过指定注解执行程序 @Order 解决保障 Hystrix 注解执行在最里层。
- 因缓存异样造成该查询方法被熔断
如果 Hystrix 注解切面的执行是在最外层,此时 Hystrix 熔断治理的办法逻辑除了第三方服务近程调用,也包含了缓存调用逻辑。如果缓存调用出现异常就会算作整个办法异样,从而引起整个办法被熔断。
2.4 服务的异样解决
先给大家工夫看如下代码,查看是否存在问题:
@HystrixCommand(fallbackMethod="queryUserByIdFallback")
public User queryUserById(String userId) {if(StringUtils.isEmpty(userId)) {throw new BizException("参数不非法");
}
Result<User> result;
try {result = userFacade.queryById(userId);
} catch(Exception e) {log.error("query user error. id={}", id, e);
}
if(result != null && result.isSuccess()) {return result.getData();
}
return null;
}
Hystrix 在运行过程中会依据调用申请的成功率或失败率信息来确定每个依赖命令的熔断器是否关上。如果关上,后续的申请都会被回绝。由此可见,对异样的管制是 Hystrix 运行成果起很大影响。
再回头看下面的例子,会发现两个异样解决问题:
- 参数校验不通过时的异样解决
非法参数校验等非零碎调用的异样失败不应该影响熔断逻辑,不应该算作失败统计范畴内。对此优化倡议是将参数校验放到近程调用封装办法的里面,或者封装成 HystrixBadRequestException 进行抛出。因为在 Hystrix 外部逻辑中 HystrixBadRequestException 异样已默认为不算作失败统计范畴内。
- try-catch 近程调用的异样解决
对近程服务的间接调用进行 try-catch 会把异样间接“吞掉”,会间接造成 Hystrix 获取不到网络异样等服务不可用异样。倡议在 catch 日志记录解决后将异样再 throw 进去。
2.5 fallback 办法
Hystrix 在依赖服务调用时通过减少 fallback 办法返回默认值的形式来反对服务优雅降级。但 fallback 的应用也有很多须要留神的中央,大抵总结如下:
- fallback 办法拜访级别、参数等要与对应依赖服务统一
- fallback 办法中执行的逻辑尽量轻量,如用本地缓存或动态默认值,防止近程调用
- 如果 fallback 办法里有近程调用,倡议也应用 Hystrix 包装起来,且保障与主命令线程池的隔离
- 对于写操作的近程调用不倡议应用 fallback 降级
2.6 groupKey、commandKey、threadPoolKey
在应用 Hystrix 开发中必定都见过这三个 key,但很多人并不了解这三个 key 的意义以及对 Hystrix 的作用,尤其是 threadPooKey,故在此总结下:
groupKey
通过 group key 能够对命令办法进行分组,便于 Hystrix 数据统计、告警及 dashboad 展现。个别会依据近程服务的业务类型进行辨别,如账户服务定义一个 group key,订单服务定义另一个 group key。
默认值是 @HystrixCommand 注解标注的办法所在的类名。
commandKey
具体命令办法的标识名称,罕用于对该命令进行动静参数设置。
默认值是 @HystrixCommand 注解标注的办法名。
threadPoolKey
用于标识命令所归属的线程池,具备雷同 threadPoolKey 的命令应用同一个线程池。
若该 key 不指定,默认值就是 groupKey,即 @HystrixCommand 注解标注的办法所在的类名。
在理论我的项目中,咱们会倡议尽量通过 threadPoolKey 来指定线程池,而不是通过 groupKey 的默认形式划分,因为会存在某个命令须要跟同组其余命令进行线程隔离的场景,以防止相互影响。
2.7 参数优先级
Hystrix 默认提供 4 个级别的参数值配置形式:
全局默认值(Default Value)
Hystrix 本身代码默认值,写死在源码中的值,应用方不配置任何参数状况下失效。
例:execution.isolation.thread.timeoutInMilliseconds 超时工夫全局默认值是 1000,单位毫秒
动静全局默认参数(Default Property)
此类配置参数可变更全局默认值。
例:通过属性名 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 设置的超时工夫值
实例初始值(Instant Value)
熔断器实例初始值,配置此类参数后,不再应用默认值。即写在代码注解中的属性值。
例:@HystrixProperty(name = “execution.isolation.thread.timeoutInMilliseconds”, value = “5000”)
动静实例参数(Instant Property)
可动静调整一个熔断器实例的参数值
例:通过属性名 hystrix.command.HystrixCommandKey.execution.isolation.thread.timeoutInMilliseconds 设置的超时工夫值
优先级关系:
动静实例参数(Instance Property) > 实例初始值 > 动静全局默认参数(Default Property) > 全局默认值(Default Value)
2.8 基于配置核心实现参数动静配置
Hystrix 默认应用 Archaius 实现动静设置,而 Archaius 默认会加载 classpath 下的 config.properties 文件,可通过在配置文件中退出对应属性 key-value 实现动态控制 Hystrix 行为。在分布式我的项目中应用配置核心进行对立配置管理是标配,因而须要基于配置核心的扩大实现 Hystrix 参数动静配置性能。
通过跟踪 HystrixCommand 的创立,发现 hystrix 最终通过 HystrixDynamicProperties 实现类依据参数属性名获取值,而 Hystrix 自身提供了 HystrixDynamicProperties 类的扩大机制,见 HystrixPlugins 类 367 行代码,可知 Hystrix 提供四种扩大办法:
- 通过零碎参数
- 基于 Java SPI 机制
- Archaius 动静属性扩大实现类(默认)
- Hystrix 内置基于 System.getProperty 的 HystrixDynamicProperties 实现;
2.8.1 基于 Java SPI 机制
基于 spi 机制的扩大实现依赖两个类别离是 HystrixDynamicProperties 与 HystrixDynamicProperty,其中 HystrixDynamicProperties 类是须要实现的 Hystrix 动静属性扩大 spi 接口,提供了多个获取动静属性的办法,接口定义如下:
public interface HystrixDynamicProperties {
/**
* Requests a property that may or may not actually exist.
* @param name property name, never <code>null</code>
* @param fallback default value, maybe <code>null</code>
* @return never <code>null</code>
*/
public HystrixDynamicProperty<String> getString(String name, String fallback);
/**
* Requests a property that may or may not actually exist.
* @param name property name, never <code>null</code>
* @param fallback default value, maybe <code>null</code>
* @return never <code>null</code>
*/
public HystrixDynamicProperty<Integer> getInteger(String name, Integer fallback);
/**
* Requests a property that may or may not actually exist.
* @param name property name, never <code>null</code>
* @param fallback default value, maybe <code>null</code>
* @return never <code>null</code>
*/
public HystrixDynamicProperty<Long> getLong(String name, Long fallback);
/**
* Requests a property that may or may not actually exist.
* @param name property name
* @param fallback default value
* @return never <code>null</code>
*/
public HystrixDynamicProperty<Boolean> getBoolean(String name, Boolean fallback);
}
而 HystrixDynamicProperty 类具体示意一个参数属性,且有动静变更的能力,接口定义如下:
public interface HystrixDynamicProperty<T> extends HystrixProperty<T>{public String getName();
/**
* Register a callback to be run if the property is updated.
* @param callback callback.
*/
public void addCallback(Runnable callback);
}
其中 addCallback 办法是实现属性动静变更的外围所在,如其正文阐明的那样,它会在属性变更时注册 callback 回调办法进行属性动静刷新。而这块动静刷新逻辑是 Hystrix 外部已实现的,对于咱们只须要自定义扩大时将 callback 保留,而后在配置核心变更时触发对应属性对象的 callback 办法即可。
实现步骤如下:
1、定义 HystrixDynamicProperty 实现类
实现动静属性类的自定义实现,包含 String/Integer/Long/Boolean 四种类型动静属性态实现。
如下面 HystrixDynamicProperty 类形容中说的那样,须要对 callback 进行保留,并在在收到配置核心属性变更时触发这些属性的 callback 办法,来实现属性的动静变更。这块逻辑能够参照观察者模式进行设计实现。
代码如下:
private abstract static class CustomDynamicProperty<T> implements HystrixDynamicProperty<T>, PropertyObserver {
protected final String name;
protected final T defaultValue;
protected List<Runnable> callbacks;
protected CustomDynamicProperty(String propName, T defaultValue) {
this.name = propName;
this.defaultValue = defaultValue;
PropertyObserverManager.add(this);
}
@Override
public String getName() {return name;}
@Override
public void addCallback(Runnable callback) {if (callbacks == null)
callbacks = new ArrayList<>(1);
this.callbacks.add(callback);
}
@Override
public String keyName() {return name;}
@Override
public void update(PropertyItem item) {if(getName().equals(item.getName())) {for(Runnable r : callbacks) {r.run();
}
}
}
}
private static class StringDynamicProperty extends CustomDynamicProperty<String> {protected StringDynamicProperty(String propName, String defaultValue) {super(propName, defaultValue);
}
@Override
public String get() {return ConfigManager.getString(name, defaultValue);
}
}
private static class IntegerDynamicProperty extends CustomDynamicProperty<Integer> {protected IntegerDynamicProperty(String propName, Integer defaultValue) {super(propName, defaultValue);
}
@Override
public Integer get() {String configValue = ConfigManager.get(name);
if(StringUtils.isNotEmpty(configValue)) {return Integer.valueOf(configValue);
}
return defaultValue;
}
}
private static class LongDynamicProperty extends CustomDynamicProperty<Long> {protected LongDynamicProperty(String propName, Long defaultValue) {super(propName, defaultValue);
}
@Override
public Long get() {String configValue = ConfigManager.get(name);
if(StringUtils.isNotEmpty(configValue)) {return Long.valueOf(configValue);
}
return defaultValue;
}
}
private static class BooleanDynamicProperty extends CustomDynamicProperty<Boolean> {protected BooleanDynamicProperty(String propName, Boolean defaultValue) {super(propName, defaultValue);
}
@Override
public Boolean get() {String configValue = ConfigManager.get(name);
if(StringUtils.isNotEmpty(configValue)) {return Boolean.valueOf(configValue);
}
return defaultValue;
}
}
其中 ConfigManager 类临时默认为配置核心配置管理类,提供参数获取与参数监听器等性能。而 PropertyObserver 类(keyName/update 办法属于其定义)、PropertyObserverManager 类就是参照观察者模式定义实现的,负责观察者的注册与告诉治理,来实现动静属性与配置核心变更告诉间的联动。这两个类实现比较简单就不展现形容。
2、定义 HystrixDynamicProperties 实现类
基于第 1 步定义的 HystrixDynamicProperty 扩大类实现 HystrixDynamicProperties 的自定义。代码如下:
public class DemoHystrixDynamicProperties implements HystrixDynamicProperties {
@Override
public HystrixDynamicProperty<String> getString(String name, String fallback) {return new StringDynamicProperty(name, fallback);
}
@Override
public HystrixDynamicProperty<Integer> getInteger(String name, Integer fallback) {return new IntegerDynamicProperty(name, fallback);
}
@Override
public HystrixDynamicProperty<Long> getLong(String name, Long fallback) {return new LongDynamicProperty(name, fallback);
}
@Override
public HystrixDynamicProperty<Boolean> getBoolean(String name, Boolean fallback) {return new BooleanDynamicProperty(name, fallback);
}
}
3、注册 SPI 实现类
在 META-INF/services/ 增加名为 com.netflix.hystrix.strategy.properties.HystrixDynamicProperties 的文本文件,内容为第 2 步 HystrixDynamicProperties 自定义实现类全路径名。
2.8.2 基于默认 Archaius 进行扩大
Hystrix 默认通过 Archaius 实现参数动静获取,而 Archaius 本身也提供自定义的参数获取形式,别离是 PolledConfigurationSource 接口 和 AbstractPollingScheduler 类,其中 PolledConfigurationSource 接口示意配置获取源,AbstractPollingScheduler 类示意配置定时刷新机制。
实现步骤如下:
1、创立配置获取源:
public class CustomCfgConfigurationSource implements PolledConfigurationSource {
private final static String CONFIG_KEY_PREFIX = "hystrix";
@Override
public PollResult poll(boolean initial, Object checkPoint) throws Exception {Map<String, Object> map = load();
return PollResult.createFull(map);
}
private Map<String, Object> load() throws Exception{Map<String, Object> map = new HashMap<>();
Set<String> keys = ConfigManager.keys();
for(String key : keys) {if(key.startsWith(CONFIG_KEY_PREFIX)) {map.put(key, ConfigManager.get(key));
}
}
return map;
}
}
其实现非常简单,外围实现就是 poll 办法,遍历配置核心中所有 hystrix 结尾的配置参数并返回保留。
2、定义配置刷新形式:
public class CustomCfgPollingScheduler extends AbstractPollingScheduler {private final static Logger logger = LoggerFactory.getLogger("CustomCfgPollingScheduler");
private final static String CONFIG_KEY_PREFIX = "hystrix";
@Override
public void startPolling(PolledConfigurationSource source, final Configuration config) {super.startPolling(source, config);
//
ConfigManager.addListener(new ConfigListener() {
@Override
public void eventReceived(PropertyItem item, ChangeEventType type) {String name = item.getName();
if(name.startsWith(CONFIG_KEY_PREFIX)) {String newValue = item.getValue();
// 新增 & 批改
if(ChangeEventType.ITEM_ADDED.equals(type) || ChangeEventType.ITEM_UPDATED.equals(type)) {addOrChangeProperty(name, newValue, config);
}
// 删除
else if(ChangeEventType.ITEM_REMOVED.equals(type)) {deleteProperty(name, config);
}
else {logger.error("error config change event type {}.", type);
}
}
}
});
}
private void addOrChangeProperty(String name, Object newValue, final Configuration config) {if (!config.containsKey(name)) {config.addProperty(name, newValue);
} else {Object oldValue = config.getProperty(name);
if (newValue != null) {if (!newValue.equals(oldValue)) {config.setProperty(name, newValue);
}
} else if (oldValue != null) {config.setProperty(name, null);
}
}
}
private void deleteProperty(String key, final Configuration config) {if (config.containsKey(key)) {config.clearProperty(key);
}
}
@Override
protected void schedule(Runnable pollingRunnable) {//IGNORE OPERATION}
@Override
public void stop() {//IGNORE OPERATION}
}
但对应理论我的项目,通过定时刷新的形式一是不太实时,二是每次都得全量查看配置核心是否有批改,逻辑简单,所以此处改用 ConfigManager.addListener 减少配置核心监听来实现。
3、定义并初始化主动配置:
DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(new CustomCfgConfigurationSource(), new CustomCfgPollingScheduler());
ConfigurationManager.install(dynamicConfiguration);
最初只须要在容器启动时执行以上初始化脚本即可。
仔细的同学可能发现下面步骤中第 3 步,最终“装置”install 到 Hystrix 配置管理类中的是 DynamicConfiguration 类实现,且第 2 步的定时刷新类也比拟鸡肋,就想着是否持续简化下面计划,只须要实现一个自定义的 ”DynamicConfiguration” 就蕴含配置源获取与监听配置批改性能,实现如下:
public class CustomCfgDynamicConfiguration extends ConcurrentMapConfiguration {private final static Logger logger = LoggerFactory.getLogger("CustomCfgDynamicConfiguration");
private final static String CONFIG_KEY_PREFIX = "hystrix";
public CustomCfgDynamicConfiguration() {super();
load();
initEvent();}
/**
* 从配置核心全量加载 Hystrix 配置参数信息
*/
private void load() {Set<String> keys = ConfigManager.keys();
for(String key : keys) {if(key.startsWith(CONFIG_KEY_PREFIX)) {map.put(key, ConfigManager.get(key));
}
}
}
/**
* 通过配置核心监听事件回调解决,针对 Hystrix 配置参数变更进行同步
*/
private void initEvent() {ConfigManager.addListener(new ConfigListener() {
@Override
public void eventReceived(PropertyItem item, ChangeEventType type) {String name = item.getName();
if(name.startsWith(CONFIG_KEY_PREFIX)) {String newValue = item.getValue();
// 新增 & 批改
if(ChangeEventType.ITEM_ADDED.equals(type) || ChangeEventType.ITEM_UPDATED.equals(type)) {addOrChangeProperty(name, newValue);
}
// 删除
else if(ChangeEventType.ITEM_REMOVED.equals(type)) {deleteProperty(name);
}
else {logger.error("error config change event type {}.", type);
}
}
}
});
}
/**
* 新增或批改参数值
* @param name
* @param newValue
*/
private void addOrChangeProperty(String name, Object newValue) {if (!this.containsKey(name)) {this.addProperty(name, newValue);
} else {Object oldValue = this.getProperty(name);
if (newValue != null) {if (!newValue.equals(oldValue)) {this.setProperty(name, newValue);
}
} else if (oldValue != null) {this.setProperty(name, null);
}
}
}
/**
* 删除参数值
* @param key
*/
private void deleteProperty(String key) {if (this.containsKey(key)) {this.clearProperty(key);
}
}
}
最初通过 ConfigurationManager.install(new CustomCfgDynamicConfiguration());“装置”该实现即可。
三、写在最初
笔者联合我的项目实战对 Hystrix 应用进行总结分享,有对于隔离策略、线程池设置、参数优先级等知识点解说,也有对于注解叠加、异样解决、参数动静配置等具体问题解决方案,心愿对大家有所帮忙。
作者:vivo 官网商城开发团队