名词约定
1、轻重接口
轻接口=轻量级接口,运行时间短,性能高,对服务器资源(内存、CPU、数据库)占用少;
重接口=重量级接口,运行时间长,性能差,对服务器资源(内存、CPU、数据库)占用多;
2、隔离
隔离是指通过技术手段使两个本来相互竞争资源的接口,在线程资源占用、内存资源占用、CPU资源、容器占用上达到互不影响的效果
背景
在容器化部署环境中,对单个容器的资源占用有上限控制,超限后容器可被自动销毁或者重启。
引起单个容器资源超限的原因一般是由于 重量级接口 、批量操作引起,随着容器的重启,容器上原本正在的运行的线程(轻重线程混合在一起)全部被终止销毁,这就导致一种现象:某一个客户的一个重量级请求影响了其它几百个客户的轻量级请求,客户端的表现就是多处功能响应缓慢,而不单单是那个重量级功能。
这就是轻重分离的需求所在。
解决方案
轻重接口隔离的解决有几种方案:
方案1:代码的拆分和独立部署
重接口的独立,将重接口的代码拆分到另一个独立的工程或者叫微服务中,从而实现部署和资源占用的独立。
优点:
未来代码可维护性好,天然部署隔离
缺点:
1、有一定工作量,代码的改动、关联服务的改动;
2、复杂接口的依赖接口是否都需要代码拆分,这是有难度的,可能造成微服务爆炸,甚至不可行的;
适合场景:
1、适合内聚性非常高的接口,对其它接口依赖少或者无依赖的功能
方案2:运行时的隔离
运行时的隔离指的是业务代码不做拆分,通过运行时将不同的请求路由到不同的实例上,从而实现资源占用的独立。
优点:
技术手段,无需业务代码改动,快速解决问题
缺点:
可能存在类似于单体应用的缺点
适用场景:
任何场景,尤其适合人员少、业务复杂、依赖关系复杂、不愿意动代码 的隔离场景
本次老吕要讲述的是方案2如何通过技术手段实现运行时的隔离
运行时的隔离方案的实现
整体思路是在提供者端打标签,在消费者端做路由策略实现,如下图:
1、对微服务进行运行时打标签(增加环境变量)
2、RPC路由策略的实现
3、对请求进行打标,可做成动态配置的,随时将请求调度到指定服务上
在Dubbo中实现轻重分离的步骤:
1、对服务提供者进行标签化配置,比如增加 tagMethod=weight 环境变量
<dubbo:provider >
<dubbo:parameter key="tagMethod" value="${tagMethod:}"/>
</dubbo:provider>
通过这个步骤的配置,可以使Dubbo的提供者带上特有的参数(我把它称为标签),这个特有的参数会自动带入到注册中心的,最终可以被消费者端获取
2、实现Dubbo轻重分离路由策略
Router接口的实现(大家可以按需修改)
/**
* 1、带tagMethod标签的请求优先路由到"带tagMethod且标签内容相同的服务实例"上
* 2、不带tagMethod标签的请求路由到"不带tagMethod标签的服务实例"上
* 3、如果带tagMethod标签的请求严格匹配服务提供者失败,则降级到"不带tagMethod标签的服务实例"上(或者降级到"带tagMethod标签但内容不同的实例上")
* 4、如果不带tagMethod标签的请求匹配不带标签的服务提供者失败,则降级到"带tagMethod标签的任意服务实例"上
* 5、也就是说:无论何种情况下只要有服务提供者实例存在,请求一定能找到一个提供者来用。
* @param invokers
* @param url
* @param invocation
* @param <T>
* @return
* @throws RpcException
*/
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
if (!enable) {
return invokers;
}
logger.info("method router");
if (invokers == null && invokers.size() == 0){
return invokers;
}
try {
//获取需要自定义路由的tag
String consumerMethodTag = TraceUtil.getTags().getTagMethod();
List<Invoker<T>> result = new ArrayList();
Iterator iterator = invokers.iterator();
while(iterator.hasNext()) {
Invoker<T> invoker = (Invoker)iterator.next();
URL invokerUrl = invoker.getUrl();
//获取提供者携带的mytag参数
String providerMethodTag = invokerUrl.getParameter(RouterTags.TAG_METHOD);
if ("".equals(providerMethodTag)) {
providerMethodTag = null;
}
if(consumerMethodTag==null&&providerMethodTag==null){
//轻请求走轻通道
result.add(invoker);
}else if (consumerMethodTag!=null&&providerMethodTag!=null&&providerMethodTag.equals(consumerMethodTag)) {
//重请求走重通道
result.add(invoker);
}
}
if (result.size()==0) {
//没有合适的通道则走任意通道(车道借用)
return invokers;
}else{
//匹配到了合适的通道
return result;
}
} catch (Throwable e) {
logger.error("method router 异常:"+e.getMessage());
return invokers;
}
}
RouterFactory接口的实现(略)
通过这个步骤就实现了消费者端的路由策略
3、配置中心增加要分离的接口配置
这个就随意定义了,怎么喜欢怎么来,比如配置格式如下:标签:url1,url2,url3 例如:
weight:/v1/edf/importaccount/import,/v1/edf/user/login,/v1/cw/reportBatchPrint/getPrintDataAsync
这样就把那三个url会被路由到带weight标签的服务提供者上;
记着DB配置中的tagMethod值和k8s环境变量设置的tagMethod需要保持一致;
4、支持同时配置多个不同的tagMethod
比如又增加了tagMethod=light的标签。则配置格式如下:
weight:url1,url2,url3;
light:url4,url5,url6
5、请求识别、请求打标、RPC时标签传递
1)当请求进入网关后在拦截器中识别url,根据配置中心的配置决定是否打标,打什么标
2)打上标后,绑定到请求线程上,RPC时要带过去,这个都知道,弄个Dubbo过滤器就解决了
3)请求结束后把标清理下
以上就是实现步骤,可动态配置,还是挺方便的,性能考虑记着配置信息做下缓存,非关键代码我就不贴了,思路我都写清楚了,大家有不明白的可以公众号联系我
总结
看过我文章的都知道我之前写过好几篇关于Dubbo路由的文章,
它们的实现思路是类似的,都是对Dubbo路由策略的定制,感兴趣的朋友可以试一试你想要的路由策略。
隔离不单单用在今天老吕提到的场景下,它是一种思想、一种方法论,可以用在很多场景:
1、按请求渠道隔离服务:如移动端、PC端的请求路由到不同的服务上;
2、按用户所在区域隔离服务:如按省市区域的分离,同一个省的路由到带同一个标签的服务上
3、整个集群被隔离为多个分片,便于灰度发布、AB测试
4、按所在数据库分片的隔离,这可以做到按数据库分片灰度发布了
今天就到这里,觉得有用的给老吕点个看一看赞一赞。