新书荐读 | 对新手与老司机都适用的容错之道

王新栋 2019-05-25 10:29:00
PREEMPTIVE READING
 
 

dbaplus社群 · 新书抢读-03期-

编者有言:架构修炼之道,重在修炼,而这探索之路却并不轻松。参考前行者的修炼历程,不论对互联网新手还是老司机,都是进阶的重要方法。本期dbaplus社群将带着大家感悟资深架构师的修炼之道,期望读者能修得自己的技术架构之道。

 
 
 

本文将带你抢先阅读第九章:容错之道(节选)

作者介绍

王新栋2014年加入京东,无界零售开放平台资深架构师。熟悉各种开源软件架构,在Web开发、架构优化上有较丰富实战经历。有多年NIO领域的设计、开发经验,对HTTP、TCP长连接技术有深入研究与领悟,目前主要致力于移动与PC平台网关技术的优化与实现。

 

一、认识容错

 

容错在不同的领域有不同的理解,也会给出不同的定义。在软件架构领域,容错特指容忍并防范局部错误,不让这种局部错误扩大。

 

所谓容忍的含义是,局部错误是客观存在的,有时候还不可避免,所以我们也就把容错放在防范上面;防范的含义是不让局部的错误引起整体不可用,“千里之堤,溃于蚁穴”,防范就是要用切实有效的措施与方法不让系统里面发生的“蚁穴”进一步扩大。

 

在识别风险领域里,风险可分为已知的风险和未知的风险,容错应对的是已知的风险。比如系统之间调用延时、线程的数量急剧飙升导致CPU使用率升高、集群服务器的磁盘被打满等等。

 

对我们目前识别到的风险如果不积极地确定应对措施,那么将来有可能让这种局部错误放大直至对外提供服务的系统不可用。

 

在本书网关之道这章中,介绍了“一个传统网关系统有几种‘死’法”,实际也是甄别出了已知的风险,这里即将要讲述的容错就是要阻止这样的风险与错误进一步扩大化。

 

容错的方法有很多,比如降级限流、熔断、线程池隔离、信号量隔离等,这篇文章主要介绍线程池隔离是如何实现的。

 

二、为什么要做线程池隔离

 

如果现在有一个系统需要调用3个业务请求,分别是查询订单、查询商品、查询用户,而且这三个业务请求都依赖第三方服务——订单服务、商品服务、用户服务。三个服务均通过RPC调用。

 

当查询订单服务时,假如响应持续延迟,这时后续有大量的查询订单请求过来,那么容器中的线程数量会持续增加直致CPU资源耗尽(100%),整个服务对外不可用,在集群环境下就会发生雪崩。

 

如下面两张图所示,从一个订单服务不可用最后演变成整个应用不可用:

 

 (订单服务出现延时)

 

 (整个应用不可用)

 

如果我们给调用订单服务的请求分配一个固定的线程池,用一个线程池隔离其他业务,那么就能够防范这样的事故发生,因为线程的使用不会超过系统负载阈值。

 

三、实现一个线程池隔离

 

本书前面的章节中介绍了Servlet3的异步的内容,将请求线程和业务工作线程分离,这样我们对业务线程就可以采取下面两种方法来实现线程池隔离。

 

方法一
 

 

使用一个大的线程池,固定线程池的大小,比如为1000。通过权重的思路为某个方法分配一个固定的大小线程数,如下图所示:

 

 

比如为某一个方法请求分配了10个线程。此时又有两种形式,一种是最多10个,我们称之为“限制型”,另一种是至少10个,我们称之为“保守型”。通过计数器来实现线程的分配。

 

限制型代码示例如下:

 

 

boolean flag = true;

 

if (限制型) {//最多只能有10个线程,count表示当前方法的线程数

  if (count.incrementAndGet() <= 10)) {

      if (publicCount.incrementAndGet() > 1000) {//publicCount代表实时总的线程数,参与计数,如果大于1000,在flag置为false,后续不做处理

          count.decrementAndGet();

          publicCount.decrementAndGet();

          flag = false;

      }

  } else {//大于10个线程,flag置为false,后续不做处理

      flag = false;

      count.decrementAndGet();

  }

  return flag;

}

 

保守型代码示例如下:

 

 

if (保守型) { //最少 10个线程

    if (publicCount.incrementAndGet() > 1000) {//同样要判断。如果实时总的线程数大于1000,则后续要拒绝处理

        publicCount.decrementAndGet();

        flag = false;

    }

    return flag;

}

 

方法二
 

 

在方法一中,严格意义上讲它并不属于线程池隔离,因为它只有一个公用的线程池,然后大家来瓜分它,不过也达到了隔离的目标。

 

下面要讲的方法就是为每个方法设置真正的线程池,如下图所示:

 

 

我们利用ConcurrentHashMap来存储线程池,key是方法名,值是每个方法对应的一个ThreadPool。当请求过来的时候,我们获取方法名,然后直接从map对象中取到响应的线程池去处理。

 

public ConcurrentHashMap<String, ThreadPoolExecutor> chm= new ConcurrentHashMap<String, ThreadPoolExecutor>

 

上面讲到的两种方法,线程池的粒度都是在方法上,是不是太细了呢?

 

这就需要结合实际的生产情况,也可以按组来分,比如订单分为一组,查询单个订单方法、查询订单列表方法等都规划到这个组里面。

 

四、线程池隔离的优缺点

 

线程池隔离的优点:

 

  • 当前面向用户的应用程序得到保护,线程池隔离后,即使一个业务线程池的线程数使用完,也不会影响其他业务。

  • 当依赖服务一旦恢复正常,比如上面的订单服务,此时应用程序可以立即恢复。

 

注意,尽管线程池提供了线程隔离,应用程序的代码也必须有超时时间设置,不能无限制地阻塞以至线程池一直饱和。

 

线程池隔离的缺点:

 

  • 主要的缺点是增加了CPU的开销,每一个业务在执行的时候,会涉及请求排队、调度和上下文的切换。

 

不过这些缺点还不足以阻碍我们使用线程池隔离的技术,比如Netflix的Hystrix中的线程池隔离,后面会专门讲解它的使用。

 

官方文档中有具体描述:

 

“The Netflix API processes 10+ billion Hystrix Command executions per day using thread isolation. Each API instance has 40+ thread-pools with 5–20 threads in each (most are set to 10).” 

 

意思是,Netflix API每天使用线程隔离处理10亿次Hystrix Command执行。每个API实例都有40多个线程池,每个线程池中有5-20个线程(大多数设置为10个)。

 

对于不依赖网络访问的服务,比如只依赖内存缓存,就不适合用线程池隔离技术,而是采用信号量隔离。

 

五、总结

 

线程池隔离是我们在日常保护线上系统稳定的常用的容错技术之一,而且生产实践效果好。此篇文章以及大多数时候我们讲的容错场景都是基于同步调用的,那么可能会有疑问,为什么不使用异步调用呢?

 

基本成熟的RPC都会支持异步调用,目前大家仍然广泛采用同步的方式主要有以下几个原因:

 

  • 首先同步调用已经可以满足绝大部分业务场景;

  • 其次异步的方式需要更关注内存的监控和顺序的问题;

  • 再者在编码习惯上同步调用更直接,不用添加诸如监听回调这样的方式,只有考虑到确实有需要异步场景的时候才会选用异步的方式,比如网关系统采用异步方式就会收到更好的效果。

 

因此掌握这种线程池隔离的容错技术在我们的大部分业务场景下仍然十分必要。

 

—To be continued—

 

SPECIAL THANKS
 

特别鸣谢@博文视点为本专栏推荐优质图书。本书适用于互联网研发人员、传统行业转互联网研发人员、各软件行业架构师。若对该书感兴趣,可通过点击查看购买链接~

购书链接:
https://item.jd.com/12560888.html

最新评论
访客 2019年09月18日

请问下为什么不用logstash同步呢?

访客 2019年09月16日

写的太好了,我这就开始学

访客 2019年09月16日

Discretized Streams 就要过时了??

访客 2019年09月07日

写的就跟屎一样

访客 2019年09月01日

没看懂啊,PK的时候,没懂,怎就就从 (1,1)变成了…

活动预告