Kirito 推荐语:最近秋招开始了,很多学生开始准备起了秋招,有很多人想知道进一些有名的互联网公司实习有什么要求,正好最近跟一位阿里春招的实习小伙子聊了一些 RPC 相关的知识点,于是我把这篇他的思考转发过来,给大家参考下,我觉得有这样的实力,进大厂实习应该是没有问题的。以下是原文:

自从春招实习之后,眼界真的就一下子开阔起来了,也感觉到了以前的自己好菜啊(虽然现在也是,笑~)。果然学习之路不能停!

微服务如今应当是一个优秀的程序员必须学习的一种架构思想,而RPC框架作为微服务的核心,不说读一遍源码吧,起码它的实现原理还是应该知道的。

然而目前的RPC服务框架,大多存在一个问题,就是当服务提供端Provider应用中,有的服务流量大,耗时长,导致线程池资源被这些服务占尽,从而影响同一应用中的其他服务正常提供。为此,这次博文主要介绍一下我对于这方面的思考。

前言

在进入正文之前,可以先看一下阿里中间件岛风大佬的这篇博文(传送门),这篇博文复现了Dubbo应用中,线程池耗尽的场景。这其实在线上是十分普遍,解决方法无非是根据业务调整参数,或者引入其他的限流、资源隔离框架,例如Hystrix、Sentinel等,使得资源间互不干扰。其实本身Dubbo也可以对不同的服务配置不同的业务线程池(通过配置protocol)从而实现服务的资源隔离,但是这种方式的弊端在于,一旦服务增多,线程数量会迅速膨胀。线程池过多不便于统一管理,同时过多的线程所带来过多的上下文切换也会影响服务器性能。

在绝大多数场景下,对服务资源的隔离可以通过开源框架Sentinel来实现,其通过配置某个服务的并发数,来达到限流和线程资源隔离的目的。坦白的讲,这已经能够满足绝大多数需求了,但是手动取配置这些参数还是比较有难度的,大多得靠大佬们的经验了,而且也不够灵活。

我在学习的时候,也突发奇想,有没有可能不依赖外部的组件,而实现内部的服务资源隔离?再更进一步,有没有可能根据应用内各个服务的流量数据,对每个服务资源进行动态的分配和绑定呢?

打个比方说,某个应用里存在A、B两个服务,100个线程。白天的时候,A服务的流量大,B服务的流量很小,那么在这个时间段内,我们的应用分配给A的资源理应更多。但是也不能全给A拿走了,B也得喝口汤,不然又会出现线程耗尽的情况,所以此时我们可能根据流量数据的比对分给A服务80个线程,B服务20个线程;而到了晚上,A服务没啥人用了,B服务流量来了,那我们就给B更多的资源,但也要保证A可用,比如说,A服务20线程,B服务80线程。

我承认我一开始只是想简单写个RPC框架,学习实现原理而已。但突然有了这样一个想法,我就来了动力,想看看自己的想法行不行得通,下面我便介绍下我的思考,说的有不对的地方也欢迎大家指出和探讨。

线程隔离的三个组件

借鉴了传统的RPC框架的实现原理后,我们只需要修改或者增加三样东西,就可以完成上述的功能,分别为:线程池、数据监控节点Metric和线程动态分配的Monitor。这三者之间的关系可以先看一下这张图有个大概的印象。

线程池

首先需要修改的自然是线程池。以Dubbo为例,其默认的线程池为fixed线程池,io线程接收到请求后,委托Dubbo线程池完成后续的处理,通过调用ExecutorService.execute。

但是在这里,使用JDK中的线程池显然是行不通了。线程池中的Thread也不再是单纯的Thread,而需要更进一步的抽象。这里参考Netty中NioEventLoop的设计思想,将每条Thread抽象为一条Loop,其既是任务执行的本体Thread,也是ExecutorService的抽象,而所有Loop交由LoopGroup统一管理,由LoopGroup决定将任务提交至哪一个线程。这里我实现的比较简单,每个线程有个专属的id,通过拿到线程的id,将任务提交到对应的线程,原理可以参考下图:

私以为核心在于维护服务与线程id的对应关系,以及在请求到来时,LoopGroup会根据请求中服务的类型,选择对应id的线程,并交由该线程去处理请求。

数据监控

数据的监控相对来说是最好办的。这里我参考了Sentinel的实现,使用时间窗口法统计各个服务的流量数据,包括pass、success、rt、reject、excetpion等。(关于Sentinel中的时间窗口,后面有时间再专门写篇源码分析)

而至于监控节点的形式,根据调用链路的具体实现不同,在Dubbo中可以是一个filter,而我因为将调用链路抽象为一个Pipeline,所以它作为Pipeline上的一个节点,参考下图:

这里贴上MetricContext的关键源码:

//处理请求时,pass+1,同时记录开始时间并保存在线程上下文中
@Override
protected void handle(Object obj) {if(obj instanceof RpcRequest){RpcContext rpcContext=RpcContext.getContext();rpcContext.setStartTime(TimeUtil.currentTimeMillis());paladinMetric.addPass(1);}
}//响应请求时,说明请求处理正常,则通过线程上下文拿到开始时间,
//计算出响应时间rt后将rt写入统计数据,同时success+1
@Override
protected void response(Object obj) {RpcContext rpcContext=RpcContext.getContext();Long startTime=rpcContext.getStartTime();if(startTime!=null){Long rt=TimeUtil.currentTimeMillis()-startTime;paladinMetric.addRT(rt);paladinMetric.addSuccess(1);logger.warn(rpcContext.getRpcRequest().getClassName()+":"+rpcContext.getRpcRequest().getMethodName()+" 's RT is "+rt);}else{logger.error(rpcContext.getRpcRequest().getClassName()+":"+rpcContext.getRpcRequest().getMethodName()+"has no start time!");}
}//这里就是统一处理异常的方法,区分为普通异常和拒绝异常,
//如果是拒绝异常,说明线程已满,拒绝添加任务,reject+1
@Override
protected void caughtException(Object obj) {paladinMetric.addException(1);if(obj instanceof RejectedExecutionException){paladinMetric.addReject(1);}
}

每个Context都会继承AbstractContext,只需要实现handle、response和caughtException方法即可,由AbstractContext屏蔽了底层pipeline的顺序调用。

线程分配

最后就是如何动态的将线程分配给服务。在这里,我们需要抽象一个评价模型,去评估各个服务应该占用多少资源(线程),可以参考下图:

简单来说,由于监控节点的存在,我们很容易就拿到每个服务的流量数据,然后抽象出每一个服务的评价模型,最后通过某种策略,得到线程分配的结果。

同时服务-线程的对应关系的读写,显然是一个读多写少的场景。可以后台开启一个线程,每隔一段时间(比如20s),执行一次动态分配的策略。采用CopyOnWrite的思想,将对应关系的引用用volatile修饰,线程重新分配完成之后,直接替换掉其引用即可,这样对性能的影响便没有那么大了。

这里的问题在于,如何合理的制定分配的策略。由于我实在缺乏相应的经验,所以写的比较捞,希望有大佬可以指点一二。

效果如何

说了这么多,那我们便来看看效果如何。代码我都放在了github上(由于时间比较短再加上本人菜,写得比较粗糙,请大家见谅T T),代码样例都在paladin-demo模块中,这里我就直接上结果了。

先定义一下参数,线程数总共20,每个服务最少能分配线程数为5,每条线程的阻塞队列容量为4,服务端两个服务,一个阻塞时间长,另一个无阻塞。

这里先定义一个阻塞时间长的服务HelloWorld。

然后我们通过http请求触发任务,模拟大流量请求。

同时给出一个无阻塞的服务HelloPaladin,可以通过http访问。

先后启动服务服务提供端和消费端,开启任务。控制台直接炸裂。

服务疯狂抛出拒绝异常。我们再输入localhost:8080/helloPaladin?value=lalala,多点几次,可以发现页面很快就能返回结果,这也意味着这个服务并没有被干扰。

最后我们来看一下,在任务启动后,线程分配的情况如何:

22:15:06,653  INFO PaladinMonitor:81 - totalScore: 594807
22:15:06,653  INFO PaladinMonitor:91 - service: com.lcf.HelloPaladin:1.0.0_paladin, score: 1646
22:15:06,653  INFO PaladinMonitor:91 - service: com.lcf.HelloWorld:1.0.0_paladin, score: 593161
22:15:06,654  INFO PaladinMonitor:113 - Threads re-distribution result: {com.lcf.HelloPaladin:1.0.0_paladin=[1, 2, 3, 4, 5], com.lcf.HelloWorld:1.0.0_paladin=[6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]}

第一行输出的是所有服务总共的分数,接下来两行分别是两个服务所得到的分数,最后一行是线程分配之后的结果。

我们穿插调用的HelloPaladin服务得到的分数远远低于跑任务的服务HelloWorld,但是由于设置了最小线程数,所以HelloPaladin服务分到了5条线程,而HelloWorld服务占据了其余的线程。(这里由于还开启了一个单线程服务,所以没有0号线程,至于什么是单线程服务可以看后文)

可以看到,服务间的线程资源确实隔离了,某一个服务的不可用不会影响到其他服务,同时资源也会向大流量的服务倾斜。

更花哨的玩法

在实现上面的功能之后,或许还有更加花哨的玩法。考虑这样一个场景,如果某个服务存在频繁加锁的场景,那么多个线程并发加锁执行,未必会有单个线程串行无锁执行来的效率高,毕竟锁和线程切换的开销也不容忽视。

在实现了服务与线程的对应关系之后,这种串行无锁执行的思路就很容易实现了,在初始化的时候,直接分配给这个服务固定的线程id号即可,这个线程也不会参与后续的动态分配流程。可以通过注解参数的方式来实现:

@RpcService(type = RpcConstans.SINGLE)
public class HelloSynWorldImpl implements HelloSynWorld

就是这么简单,服务器启动之后你就会发现,这个服务都会使用某条固定的线程去执行,自然也就用不着加锁了(除非要跟其他服务同时操作共享资源,那就不适用于这种场景),不过这种串行场景我想了想,好像并不多,只有在那种纯内存的操作中可能会比较有性能优势(是不是很像Redis),所以也就图一乐。

相比原来的线程模型有何优劣?

话又说回来了,虽然解决了服务资源隔离和分配的问题,那么相比原来的线程模型是否就没有劣势了呢?

因为加入了更多的组件,考虑到监控节点的性能损耗,增加了分配线程、选择线程的逻辑,或许在性能上相比原来的线程模型会差一点,至于差多少,我可能也没法定量给出解答,还需要进一步的测试。不过可以肯定的是,可以通过更多的优化,使得两者的性能更加接近,例如:用JcTool的无锁队列替换JDK中的阻塞队列;给出合适的评价模型,使得资源分配更合理以及分配过程性能更优等等。

当然最关键的还是你业务代码写的咋样,毕竟框架优化的再好,业务代码不大行,那点优化效果微乎其微。

后记

这一次的经历,对我自己而言收获颇丰,不仅仅是因为学到了RPC框架的一些实现原理,更是对于“学习-发现问题-思考解决方案-实践验证”这一整条路的尝试。至于下一步,或许想要尝试在一些开源框架中去实现这个东西吧,毕竟开源这个事,想想就很cool~虽然未必能得到认可,但是实践本身也是学习的一部分,而且并不是足够强了才去参与开源,而是参与开源了才会越来越强。

一文探讨 RPC 框架中的服务线程隔离相关推荐

  1. RPC框架与REST服务

    1.常见的RPC框架 Dubbo:阿里开源的框架,仅支持Java语言. gRPC:Google开源的框架,支持多种语言. Thrift:Facebook开源框架,支持多种语言. Tars:腾讯开源的框 ...

  2. 一文理解分布式开发中的服务治理

    我们在分布式开发中经常听到的一个词就是 "服务治理". 在理解"服务治理"的概念之前让我们先理解什么是分布式系统,分布式系统之间如何通过RPC(Remote P ...

  3. java rpc框架 hsf_分布式服务框架HSF学习

    转载:http://googi.iteye.com/blog/1884754 HSF提供的是分布式服务开发框架,taobao内部使用较多,总体来说其提供的功能及一些实现基础: 1.标准Service方 ...

  4. 一文详解 Kubernetes 中的服务发现,运维请收藏

    K8S 服务发现之旅 Kubernetes 服务发现是一个经常让我产生困惑的主题之一.本文分为两个部分: 网络方面的背景知识 深入了解 Kubernetes 服务发现 要了解服务发现,首先要了解背后的 ...

  5. 一文带你实现RPC框架

    原文地址:一文带你实现RPC框架 想要获取更多文章可以访问我的博客 - 代码无止境. 现在大部分的互联网公司都会采用微服务架构,但具体实现微服务架构的方式有所不同,主流上分为两种,一种是基于Http协 ...

  6. rpc框架都有哪些_这六种微服务RPC框架,你知道几个?

    开源 RPC 框架有哪些呢? 一类是跟某种特定语言平台绑定的,另一类是与语言无关即跨语言平台的. 跟语言平台绑定的开源 RPC 框架主要有下面几种. Dubbo:国内最早开源的 RPC 框架,由阿里巴 ...

  7. 【分布式服务架构】常用的RPC框架

    1. RPC 框架的原理 RPC(Remote Procedure Call,远程服务调用),用来实现部署在不同机器之间系统的方法调用,使程序像当问本地系统资源一样,通过网络传出资源. 1)Clien ...

  8. 微服务架构之 —— RPC框架

    RPC简介 RPC是什么 Remote Procedure Call,远程过程调用. 首先来说本地方法调用,假设在main方法中调用一个本地的方法multiply(同一个进程内的方法调用).无非是做了 ...

  9. 支撑微博千亿调用的轻量级RPC框架:Motan

    随着微博容器化部署以及混合云平台的高速发展,RPC 在微服务化的进程中越来越重要,对 RPC 的需求也产生了一些变化.今天主要介绍一下微博 RPC 框架 Motan,以及为了更好的适应混合云部署所做的 ...

最新文章

  1. Linux 无法使用su
  2. python如何更新包_python如何更新包 python更新包代码示例
  3. linux文件操作命令--转
  4. 印度程序培训之ISAS考试方法及评分参考准则
  5. php 开源 采集,迅睿CMS 火车头内容采集
  6. 如何深入学习python_菜鸟如何学好python
  7. 对服务器端接口的调用,自己手写了一个脚本,但返回信息的中文总是乱码(这个方法很不错,重要的是解决思路,寻找手写脚本与录制脚本 生成目录文件的区别)...
  8. 数据库实验一——数据库定义与操作语言实验
  9. 【Mark】计算机科学导论
  10. Linux查看日志文件
  11. 版本控制软件Git的使用(小白版)
  12. mac装虚拟机真的好吗?
  13. cmd默认是以管理员身份运行
  14. 阿里企业邮箱POP\SMTP\IMAP地址和端口信息
  15. 神牛TT685C闪光灯ETTL模式不同步解决方案
  16. cross-request插件下载
  17. 【转】TPC-C 、TPC-H和TPC-DS区别
  18. 答题卡识别任务--opencv python(附代码)
  19. 东田纳西州立大学计算机排名,东田纳西州立大学如何
  20. SQL Server 登录出错 用户 ‘sa‘ 登录失败 (Microsoft SQL Server, Error: 18456)

热门文章

  1. Day 26: TogetherJS —— 让我们一起来编程!
  2. 惨一个字!Windows 10 October 2018 Update市占率太低
  3. Python的介绍与安装
  4. Solr安装步骤 + dataimport导入数据配置
  5. 基于Python Tornado的在线问答系统
  6. Windows 10 RedStone2值得期待的五大功能猜想
  7. 《数据结构与抽象:Java语言描述(原书第4版)》一2.1.4 让实现安全
  8. 《火球——UML大战需求分析》(第2章 耗尽脑汁的需求分析工作)——2.3 给客户带来价值,需求分析之正路...
  9. Android性能优化案例研究(上)
  10. Citrix VDI实战攻略之八:测试验收