为第三方HTTP协议依赖增加Hystrix保护你的系统
为第三方HTTP协议依赖增加Hystrix保护你的系统
前言
后端开发的很多同学应该都有过调用第三方HTTP协议服务的经验。
比如调用百度查询经纬度编码信息、调用豆瓣查询时下热门电影、调用7牛云存储接口上传文件等;以及公司内部其他小组开放的服务接口。
常见开发模式是我们按照服务提供方定义的接口文档进行接口调用开发。
在java中常见的HTTP客户端库有:
java中常见的HTTP客户端库
为第三方HTTP协议依赖增加Hystrix保护你的系统
后端开发的很多同学应该都有过调用第三方HTTP协议服务的经验。
比如调用百度查询经纬度编码信息、调用豆瓣查询时下热门电影、调用7牛云存储接口上传文件等;以及公司内部其他小组开放的服务接口。
常见开发模式是我们按照服务提供方定义的接口文档进行接口调用开发。
在java中常见的HTTP客户端库有:
java中常见的HTTP客户端库
系统并发很高的情况下,我们依赖的第三方服务挂了,调用HTTP协议接口超时线程阻塞一直得不到释放,系统的线程资源很快被耗尽,导致整个系统不可用。
试想一下如果业务系统中我们依赖的第三方服务只是一个增强型的功能没有的化也不影响主体业务的运行或者只是影响一部分服务,如果导致系统整体不可用这是绝对不允许的。
有什么办法可以解决这个问题呢?
我们可以使用代理模式增加服务调用的监控统计,在发现问题的时候直接进行方法返回从而避免产生雪崩效应。
伪代码如下
public interface ApiService { /** * 获取token * * @param username * @param password * @return */ String getToken(String username, String password); } public static <S> S getSafeApiService(Class<S> serviceClass) { S instance = ApiServiceFactory.createService(serviceClass); return (S) Proxy.newProxyInstance(serviceClass.getClassLoader(), new Class<?>[]{serviceClass}, (proxy, method, args) -> { // 代理接口发现服务不可用时直接返回不执行下面的invoke方法 if (failStatus) { log.error("error info") return null; } else { // 执行具体的调用 Object result = method.invoke(instance, args); return result; } }); }
总结就是我们需要包裹请求,对请求做隔离。那么业内有没有此类功能成熟的框架呢?
答案是肯定的,Netflix这家公司开源的微服务组件中Hystrix就有对于服务隔离、降级和熔断的处理。
下面以调用百度地图地理位置反编码接口来演示Hystrix的使用
使用Spring Initializr初始化SpringBoot工程,在pom文件中新增如下依赖Retrofit和Hystrix依赖
<properties> <java.version>1.8</java.version> <hytrix.version>1.5.18</hytrix.version> <retrofit.version>2.3.0</retrofit.version> <slf4j.version>1.7.7</slf4j.version> <logback.version>1.1.2</logback.version> <lombok.version>1.16.14</lombok.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--Retrofit 客户端框架--> <dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>retrofit</artifactId> <version>${retrofit.version}</version> </dependency> <dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>converter-gson</artifactId> <version>${retrofit.version}</version> </dependency> <!--Hystrix 断路器框架--> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-core</artifactId> <version>${hytrix.version}</version> </dependency> 其他依赖... </dependencies>
HTTP客户端这里选择Retrofit,相关文档可查看
https://square.github.io/retrofit/
public interface BaiduMapApi { @GET("reverse_geocoding/v3/") Call<AddressBean> decode(@Query("ak") String ak, @Query("output") String output, @Query("coordtype") String coordtype, @Query("location") String location); }
@SpringBootApplication public class HystrixDemoApplication { public static void main(String[] args) { SpringApplication.run(HystrixDemoApplication.class, args); } @Bean public BaiduMapApi baiduMapApi() { Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://api.map.baidu.com/") .addConverterFactory(GsonConverterFactory.create()) .build(); return retrofit.create(BaiduMapApi.class); } }
@Slf4j @SpringBootTest class BaiduMapApiTest { @Autowired private BaiduMapApi baiduMapApi; @Test void decode() throws IOException { AddressBean addressBean = baiduMapApi.decode( "v1Xba4zeGLr6CScN39OFgvhiADPaXezd", "json", "wgs84ll", "31.225696563611,121.49884033194").execute().body(); if (addressBean != null) { log.info(addressBean.toString()); } } }
执行单元测试后显示
表明接口实现成功
Hystrix官方示例:
https://github.com/Netflix/Hystrix/wiki/How-To-Use
Hello World!Code to be isolated is wrapped inside the run() method of a HystrixCommand similar to the following:
public class CommandHelloWorld extends HystrixCommand<String> { private final String name; public CommandHelloWorld(String name) { super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); this.name = name; } @Override protected String run() { return "Hello " + name + "!"; } }
This command could be used like this:
String s = new CommandHelloWorld("Bob").execute(); Future<String> s = new CommandHelloWorld("Bob").queue(); Observable<String> s = new CommandHelloWorld("Bob").observe();
参考官方示例按照前面的分析对调用做隔离保护需要使用代理模式包裹请求,我们包装一下对百度接口的调用
@Autowired private BaiduMapApi baiduMapApi; @GetMapping("/decode") public AddressBean test(Double lon, Double lat) { // 使用HystrixCommand包裹请求 return HystrixCommandUtil.execute( "BaiduMapApi", "decode", baiduMapApi.decode("v1Xba4zeGLr6CScN39OFgvhiADPaXezd", "json", "wgs84ll", lat + "," + lon) , throwable -> { log.error("触发出错返回,告警!", throwable); return null; }); }
@Slf4j public class HystrixCommandUtil { /** * 客户端参数异常时将抛出HystrixBadRequestException * * @param groupKey * @param commandKey * @param call * @param fallback * @param <T> * @return * @throws HystrixBadRequestException */ public static <T> T execute(String groupKey, String commandKey, Call<T> call, HystrixFallback<T> fallback) throws HystrixBadRequestException { if (groupKey == null) { throw new IllegalArgumentException("groupKey 不能为空"); } if (commandKey == null) { throw new IllegalArgumentException("CommandKey 不能为空"); } if (call == null) { throw new IllegalArgumentException("call 不能为空"); } if (fallback == null) { throw new IllegalArgumentException("fallback 不能为空"); } return new HystrixCommand<T>(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey)) .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey))) { @Override protected T run() throws Exception { Response<T> response = call.execute(); if (response != null) { if (response.code() >= 200 && response.code() < 300) { return response.body(); } else if (response.code() >= 400 && response.code() < 500) { if (response.errorBody() != null) { throw new HystrixBadRequestException(response.errorBody().string()); } else { throw new HystrixBadRequestException("客户端参数非法"); } } else { if (response.errorBody() != null) { throw new RuntimeException(response.errorBody().string()); } else { throw new RuntimeException("服务端未知异常"); } } } else { throw new RuntimeException("未知异常"); } } @Override protected T getFallback() { return fallback.fallback(getExecutionException()); } }.execute(); } }
上述示例代码GitHub地址
hystrix缺省使用了线程池进行隔离,HystrixCommand中的run方法是在异步线程池中执行的。
默认的触发熔断的条件是:
满足1~3条件后断路器打开,触发熔断后续的执行会被拦截直接走getFallback方法返回。5秒以后(
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds = 5000 // 缺省5秒)下一个的请求会被通过(处于半打开状态),如果该请求执行失败,回路器会在睡眠窗口期间返回OPEN,如果请求成功,回路器会被置为关闭状态。
查看官方文档可知:
Execution Exception types
Execution Exception types
HystrixBadRequestException 不会记录进熔断统计中我们可以此异常包装我们的客户端异常
Hystrix Flow Chart
断路器触发规则
通过Hystrix的引入再次深入了服务的容错处理实现。
构建强健的系统远比demo付出的多太多。深入再深入一点,加油^_^