一、背景
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 官网商城开发团队