前言
后端开发的很多同学应该都有过调用第三方HTTP协议服务的经验。
比如调用百度查询经纬度编码信息、调用豆瓣查询时下热门电影、调用7牛云存储接口上传文件等;以及公司内部其他小组开放的服务接口。
常见开发模式是我们按照服务提供方定义的接口文档进行接口调用开发。
在java中常见的HTTP客户端库有:
库 | 个人建议 |
---|---|
Java原生HttpURLConnection | 功能简陋,不推荐使用 |
Apache HttpClient | 老牌框架稳定可靠,怀旧党可考虑 |
OkHttp,Retrofit | 新兴势力,发展迅猛;支付Android,支持HTTP2 |
Spring RestTemplate | 可替换底层实现,Spring生态内简单的HTTP协议调用推荐使用 |
OpenFeign | 可替换底层实现;源于Retrofit灵感支持注解驱动;支持Ribbon负载均衡、支持Java 11 Http2、支持Hystrix断路保护... 强烈推荐 |
你的第三方依赖挂了怎么办?
系统并发很高的情况下,我们依赖的第三方服务挂了,调用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
下面以调用百度地图地理位置反编码接口来演示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接口的调用
HTTP客户端这里选择Retrofit,相关文档可查看 https://square.github.io/retrofit/
- Retrofit将百度的HTTP API转换为Java接口
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);
}
- 使用Retrofit类生成的实现BaiduMapApi接口
@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);
}
}
- 创建完BaiduMapApi的实现后就可以直接使用这个接口了
@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());
}
}
}
执行单元测试后显示
表明接口实现成功
为HTTP调用增加Hystrix保护
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();
- 新增HystrixCommand实现
- 在run方法中执行具体的方法
- 通过调用HystrixCommand对象的execute或者queue或者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();
}
}
Hystrix原理
hystrix是如何隔离调用的?
hystrix缺省使用了线程池进行隔离,HystrixCommand中的run方法是在异步线程池中执行的。
- 线程池的名称缺省为HystrixCommand中groupKey的名称。
- 线程池的核心线程数为10(hystrix.threadpool.default.coreSize = 10 // 缺省为10)
- 线程池最大线程数为10(hystrix.threadpool.default.maximumSize = 10 // 缺省为10)
- 线程池满了以后立即触发拒绝策略加速熔断(hystrix.threadpool.default.maxQueueSize = -1)
使用了hystrix它的断路触发规则是什么样子的呢?
默认的触发熔断的条件是:
- 在最近的一个时间窗口期(hystrix.command.default.metrics.rollingStats.timeInMilliseconds = 10000 // 默认10秒)内
- 总请求次数>=(hystrix.command.default.circuitBreaker.requestVolumeThreshold = 20 //默认20)
- 并且发生异常次数的比例>=(hystrix.command.default.circuitBreaker.errorThresholdPercentage = 50 // 默认50%)
满足1~3条件后断路器打开,触发熔断后续的执行会被拦截直接走getFallback方法返回。5秒以后(hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds = 5000 // 缺省5秒)下一个的请求会被通过(处于半打开状态),如果该请求执行失败,回路器会在睡眠窗口期间返回OPEN,如果请求成功,回路器会被置为关闭状态。
有些异常是客户端问题却错误地统计进了熔断监控统计中该怎么办?
查看官方文档可知:
Execution Exception types
Failure Type | Exception class | Exception.cause | subject to fallback |
---|---|---|---|
FAILURE | HystrixRuntimeException | underlying exception (user-controlled) | YES |
TIMEOUT | HystrixRuntimeException | j.u.c.TimeoutException | YES |
SHORT_CIRCUITED | HystrixRuntimeException | j.l.RuntimeException | YES |
THREAD_POOL_REJECTED | HystrixRuntimeException | j.u.c.RejectedExecutionException | YES |
SEMAPHORE_REJECTED | HystrixRuntimeException | j.l.RuntimeException | YES |
BAD_REQUEST | HystrixBadRequestException | underlying exception (user-controlled) | NO |
HystrixBadRequestException 不会记录进熔断统计中我们可以此异常包装我们的客户端异常
官方wiki的两张图很好的展示了Hystrix原理
尾巴
通过Hystrix的引入再次深入了服务的容错处理实现。
构建强健的系统远比demo付出的多太多。深入再深入一点,加油_