背景
随着公司业务的快速发展,项目微服务化已经成为了一个普遍常态。一个项目涉及到的微服务的少说三五个,多则几十个甚至几十个不等,而服务之间的调用所使用的技术进几年来也是层出不穷,例如我们熟知常见的 Http Restful、dubbo、motan、grpc、还有Spring 的Feign等。每个公司都有自己的技术栈,而我们公司大部分Java服务普遍使用的都是Spring的Feign调用,由于公司Java微服务众多,每个部门甚至每个团队都有自己负责的维护的微服务,由于feign Client的开发还没有统一的规范,因此每个项目的实现也是层出不穷,因此给整个Feign Client的维护也带来了诸多的不变。
一、现状
- 部分服务提供Client依赖包,部分服务没有完善Client包甚至没有。
- 每个微服务定义Client包的方式不进相同。
- 每个服务都调用其他的微服务都有自己的熔断,导致Client升级新增了接口的时候,调用方必须更新自己的业务代码,重写熔断接口采能避免编译报错。
-
每个服务的调用方不明确,也就是说一个微服务上线时间久了,随着业务的快速发展,到了后期不清楚自己的服务有哪些系统在调用,导致重构或者升级过程中对风险的评估不准确带来潜在风险。
相信做Java开发的大佬们都隐隐约约的遇到了以上提到的难题,那么针对以上问题,我们应该怎么来规范是定义Client或者解决以上问题呢?
二、思考
1.为何要做Feign Hystrix。
什么是服务熔断,我这里在百度百科上找到定义如下
服务熔断按照层级来划分也分为很多种,比如服务之间调用的熔断,网关调用的熔断等。从定义上来看可能比较难理解,我们举个例子。
假设A服务的某个接口依赖B服务的某个接口,当某时刻A服务的这个接口流量成倍数增长,假设这个时候A服务依赖的B服务这个接口因为mysql、或者缓存 或其他原因发生异常,试想会发生什么?很显然此时B服务将会接收大量 的无效请求,那么就会占用B服务的系统资源从而造成资源浪费。
那么有了熔断会怎么处理呢?其实如果A服务对B服务做了熔断处理,那么当大量请求B服务的时候,当第一次请求B服务异常后,那么后续的其他请求将在一段时间内将不会连续再次请求B服务了,这样就给B服务节省了大量的资源。除此之外,在A服务对B服务做熔断的过程中在业务允许范围内也可以对异常响应做mock default处理。
2.统一规范Feign Client会带来哪些好处。
大家都晓得在对接一些三方服务的时候都会有对应开发语言的一些SDK,例如我们熟知的腾讯微信有自己的微信相关SDK、腾讯对象存储COS有自己的 SDK、阿里相关功能也都会有自己的sdk。其实 Feign Client包从某种意义上来说也是属于我们给下游系统提供的sdk。
那么对于下游系统来说会带来哪些好处呢?服务接入将会变得简单,原来没有一sdk可能需要自己要写大量的http请求相关代码,如果你的请求需要有签名、加密相关要求,那么每个客户端都需要重复的开发一套逻辑去进行对接,如果对你们的服务接口进行升级,那么对下游的系统将会影响巨大,甚至可能还需要重新写一份。
那么对于服务端会带来哪些好处呢?这里好处不再一一列举,例如可以对所有的客户端进行统一的升级维护,可以规范下游系统调用服务的规范,例如加固定的请求头信息,统一的签名处理,统一的加密处理等,这样既方便的自己,也给下游系统带来了便利,何乐而不为呢?
三、实现
包结构说明
└─plan
├─client
├─config
│ ├─fallback
│ │ └─plan
│ ├─interceptor
│ └─log
├─request
└─response
client:接口定义,定义提供服务的接口。
config: 配置定义
fallback: 定义熔断支持。
interceptor:定义拦截器,处理统一的请求头、签名、加密等。
log:定义统一的日志搜集打印。
request: 定义统一的请求dto参数定义。
response:定义统一的响应体参数定义。
Client接口定义
熔断配置
整体熔断相关包接口定义如下
AbstractFallbackFactory:为抽象的熔断工厂。
由于一个Client SDK可能存在有多个API Client接口定义,如 XxxAClient、XxxBClient....等,而每个Client都有自己的熔断定义,所以这里在fallback包下又为每个Client单独定义一个包名,如这里的plan就是为PlanClient定义的熔断配置。
XxxFallback 熔断处理接口定义需要继承XxxClient接口,例如这里的 PurchasePlanFallback定义如下。
public interface PurchasePlanFallback extends PurchasePlanClient {
}
AbstractFallbackFactory抽样工厂配置
@Slf4j
public abstract class AbstractFallbackFactory implements FallbackFactory {
private static final String SERVICE_NAME = "plan-service";
@Override
public T create(Throwable error) {
log.error("[{}]服务调用异常", SERVICE_NAME, error);
T fallbackInstance = getFallbackInstance();
log.info("Purchase Plan Service Feign FallBack Instance[{}]", fallbackInstance);
return fallbackInstance;
}
/**
* 获取熔断处理实例
*
* @return 熔断处理实例
*/
public abstract T getFallbackInstance();
}
XxxFallbackFactory具体的熔断工厂配置类,主要负责定义和实现具体的熔断实例UML图如下
具体实现如下
@Slf4j
@Component
public class PurchasePlanFallbackFactory extends AbstractFallbackFactory {
@Autowired
private PurchasePlanFallback fallbackInstance;
/**
* PurchasePlanClient 默认熔断配置
*/
@Bean
@ConditionalOnMissingBean
public PurchasePlanFallback getPurchasePlanFallback() {
return new DefaultPurchasePlanFallback();
}
@Override
public PurchasePlanClient getFallbackInstance() {
return fallbackInstance;
}
}
DefaultPurchasePlanFallback 为默认的熔断处理配置。如想自定义熔断异常处理可以有如下方案
1.如果想自定义全部Client接口熔断可以直接定义类实现 PurchasePlanFallback 接口全部实现类即可。
//实例化到IOC中
@Component
public class MyFallBack implements PurchasePlanFallback{
@Override
public ... A(... request) {
你的实现
}
@Override
public ... B(... request) {
}
}
2.如果想自定义部分接口熔断,可直接继承 DefaultPurchasePlanFallback 类,重写你需要自定义的方法接口即可。
例如:
//实例化到IOC中
@Component
public class myFallBack extends DefaultPurchasePlanFallback{
@Override
public ... A(... request) {
你的实现
}
}
默认熔断定义
public class DefaultPurchasePlanFallback implements PurchasePlanFallback {
private static final String SERVICE_NAME = "plan-service";
@Override
public ResponseBaseVo cancelPurchasePlan(CancelPlanRequest request) {
ResponseBaseVo responseBaseVo = new ResponseBaseVo<>();
responseBaseVo.setCode(-1);
responseBaseVo.setData(false);
responseBaseVo.setMsg(String.format("[%s]服务[cancelPurchasePlan]接口调用异常", SERVICE_NAME));
return responseBaseVo;
}
}
拦截器配置:添加调用来源
@Slf4j
public class PurchasePlanFeignInterceptor implements RequestInterceptor {
@Value("${app.name:}")
private String appId;
private static final String HEADER_APP_SOURCE_ID = "App-Source-Id";
@Override
public void apply(RequestTemplate template) {
//处理定义请求头添加 调用方应用名称
template.header(HEADER_APP_SOURCE_ID, appId);
}
}
四、参考
https://docs.spring.io/spring-cloud-openfeign/docs/2.2.5.RELEASE/reference/html/#spring-cloud-feign