Mr. Panda
Tech For Fun

[架构] 从零开始学软件架构:核心服务层架构设计之调度与池化

本文是从零开始学软件架构系列文章的第五篇,主题为核心服务层架构设计,主要解读核心服务中的任务调度和池化技术。本文的主要内容包括: Timer 定时器调度机制、ScheduledExecutor 调度机制、Quartz 等单机调度机制、以及分布式调度架构、线程池、连接池、对象池等池化技术等内容。

任务调度

背景问题:

  • 什么是任务?任务:Task,需要依靠计算机程序完成的一系列事情。
  • 什么是调度?调度:Control,执行控制任务的指挥,处罚,规则程序。
  • 为什么要有任务调度?任务调度:使用一系列的触发规则在特定的时间点指挥计算机完成一系列的事情。

应用场景:

  • 业务跑批轮训等待处理
  • 失败异常重试
  • 定时处理任务

单机调度方式及实现:

  • Timer 定时器机制
  • ScheduledExecutor
  • Quartz

Timer 定时器机制

Timer 由于所有的任务都是同一个线程来调度,因此所有的任务都是串行执行的,同一个时间只能有一个任务在执行。

Timer 定时器机制

Timer 类负责维护一个任务队列 TaskList,并且根据一定的调度策略在时间片来临时从 TaskList 中取出任务,交给 TimerTask 执行,其中 TimerTask 是在一个线程中执行的。

Java Timer 机制代码示例

JS定时器机制

什么是定时器?
定时器是一种异步任务,通常浏览器都有一个独立的定时器模块,定时器的延迟时间就由定时器模块来管理,当某个定时器到了可执行状态,就会被加入主线程队列。

定时器不是JavaScript的一项功能,而是作为对象和方法的一部分,在浏览器中使用。也就是说,在非浏览器环境中使用JavaScript,很可能定时器不存在。

定时器的运行机制是什么?
定时器的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。

定时器解决了什么问题?
由于JS的单线程特性,定时器提供了一种跳出单线程限制的方法,即让一段代码在一定毫秒之后,再异步执行。

JavaScript是单线程的,这也决定了在异步事件(鼠标单击、定时器等)程序的处理中,在线程中没有代码的时候才会执行。即处理程序需要排队执行,且不会被其他处理程序中断。

下面通过例子来了解定时器的详细机制:

JavaScript 定时器的详细机制
  • 0ms时,启动一个10ms延迟的定时器,以及一个10ms间隔定时器。
  • 6ms时,触发鼠标点击事件。
  • 10ms时,定时器和第一个间隔定时器都过期了(由于主程序还在执行,所以定时器仍然在排队,等待空闲在执行)。
  • 由于定时器和点击事件都是异步事件,所以他们会进行事件排队,当主程序的同步事件执行完成(即在18ms之后),线程空闲时才执行。
  • 18ms时,主线程执行完毕,开始执行队列里面的事件,队列里面现在有鼠标单击事件、setTimeout定时器和setInterval定时器。
  • 20ms时,由于队列里面有setInterval定时器,所以第二个到期的间隔定时器就会作废处理。
  • 28ms时,单击事件执行完成,并且在10ms就应该执行的setTimeout定时器,现在才开始执行。
  • 30ms时,第三个setInterval定时器到期,因队列中有间隔定时器,所以第三个也作废。
  • 34ms时,setTimeout定时器执行完成,开始执行setInterval,但由于第一个间隔定时器在42ms时结束,所以40ms时,到期的第二个间隔定时器,又要进行排队等待。
  • 47ms时,由于第二个setInterval可以在第三个间隔定时器50ms到期时执行完,所以不需要排队直接执行。

根据上面的流程进行小结:

  • 事件排队:同时发生了这么多事情,由于js的单线程特性,当线程正在执行状态,有异步事件触发时,它就会排队,并且在线程空闲时才进行执行。并且依照先进先出的顺序执行(先排队的先执行)。
  • setInterval调用被废弃:在线程被占用的情况下,并且队列中已经有setInterval在排队,则下一个到期的setInterval会被废弃。
  • 定时器无法保证准时执行回调函数:在主线程还没有结束,即使定时器时间到期仍然不会执行,必须等到主程序同步代码全部执行完。
  • setTimeout和setInterval的区别:其最主要的区别是功能上的区别,setTimeout只延迟执行一次,setInterval按时间周期性的执行。

其他相关知识:

  • 定时器不能非常细粒化的控制执行的时间,建议在15ms以上。
  • 可以使用定时器来分解长时间运行的任务。

ScheduledExecutor

线程池

线程池(ThreadPool)是什么?

线程池是由系统维护的容纳线程的容器。线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。

性能:每开启一个新的线程都要消耗内存空间及资源(默认情况下大约1 MB的内存),同时多线程情况下操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还对性能不利。

时间:无论何时启动一个线程,都需要时间(几百毫秒),用于创建新的局部变量堆,线程池预先创建了一组可回收线程,因此可以缩短过载时间。

为什么用线程池?

  • 线程并发数量过多,抢占系统资源从而导致阻塞。线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况。
  • 创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率。如果创建、销毁线程的成本高于任务执行的成本,那么使用线程将得不偿失。
  • 对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等。

线程池的工作机制

  • 在线程池的编程模式下,任务提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
  • 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

ScheduledExecutor

ScheduledExecutor
Java ScheduledExecutor 代码示例
线程池能够使任务间是多线程的,但是任务本身仍然是单线程的。因此,任务间可以并行执行,但是任务本身仍然是串行执行。

Quartz

Quartz 是什么?

Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。Quartz 实现了作业(任务)和触发器(调度)的多对多的关系,还能把多个作业与不同的触发器关联。

Quartz 核心概念

  1. Job 表示一个工作,要执行的具体内容。此接口中只有一个方法,如下:void execute(JobExecutionContext context) 
  2. JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。
  3. Trigger 代表一个调度参数的配置,什么时候去调。
  4. Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。

Quartz 调度原理图如下:

Quartz 调度原理图
Quartz 调度任务代码示例

参考:

分布式调度

分布式调度方式及实现

  • Quartz 分布式版本
  • Elastic-Job 分片分布式

Quartz 分布式版本

Quartz 分布式版本原理图如下:

Quartz 分布式版本原理图

Quartz 分布式集群中的实例通过竞争数据库的 trigger 的竞争锁来获取到 jobDetail 的执行权限,这种方式可以解决重复计算、资源竞争的问题,但是执行权限是限制在单一机器上的。对于资源消耗型的任务单一机器的算力并不能满足性能需求,那么怎么将任务进行切片并分散到分布式集群中呢?这个问题可以通过 Elastic-Job 来解决。

Elastic-Job 分片分布式

Elastic-Job是ddframe中dd-job的作业模块中分离出来的分布式弹性作业框架。去掉了和dd-job中的监控和ddframe接入规范部分。该项目基于成熟的开源产品Quartz和Zookeeper及其客户端Curator进行二次开发。

Elastic-Job 基本原理图

参考:

池化技术

什么是池化技术

池化技术 (Pool) 是一种很常见的编程技巧,在请求量大时能明显优化应用性能,降低系统频繁建连的资源开销。常见的有数据库连接池、线程池、对象池等,它们的特点都是将 “昂贵的”、“费时的” 的资源维护在一个特定的 “池子” 中,规定其最小连接数、最大连接数、阻塞队列等配置,方便进行统一管理和复用,通常还会附带一些探活机制、强制回收、监控一类的配套功能。

目的:池化技术是用来减少系统消耗,提升系统性能的。

原则:宁可限,不要滥。当池中资源用尽的时候,让新的消费任务进入等待队列。

对象池:利用复用对象来减少创建对象、垃圾回收的开销。例如线程池通过复用线程提升性能。

连接池:(数据库连接池、Redis 连接池、HTTP连接池、Dubbo 连接池、Tomcat 连接池)通过复用 TCP 连接来减少创建和释放连接的时间。

优点

  1. 减少内存碎片的产生
  2. 提高内存的使用效率

缺点

  • 造成内存的浪费

进程池/线程池

线程池的原理很简单,类似于操作系统中的缓冲区的概念,它的流程如下:先启动若干数量的线程,并让这些线程都处于睡眠状态,当需要一个开辟一个线程去做具体的工作时,就会唤醒线程池中的某一个睡眠线程,让它去做具体工作,当工作完成后,线程又处于睡眠状态,而不是将线程销毁。详细内容参见上文 ScheduledExecutor 中线程池部分。进程池与线程池同理。

进程池/线程池原理图

连接池

数据库连接池:

数据库连接池的基本思想是在系统初始化的时候将数据库连接作为对象存储在内存中,当用户需要访问数据库的时候,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象。

在使用完毕后,用户也不是将连接关闭,而是将连接放回到连接池中,以供下一个请求访问使用。这些连接的建立、断开都由连接池自身来管理。

同时,还可以设置连接池的参数来控制连接池中的初始连接数、连接的上下限数和每个连接的最大使用次数、最大空闲时间等。当然,也可以通过连接池自身的管理机制来监视连接的数量、使用情况等。

数据库连接池有两个最重要的配置:最小连接数和最大连接数,它们控制着从连接池中获取连接的流程:

  • 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
  • 如果线程池中有空闲连接,则使用空闲连接;
  • 如果没有空闲连接,并且当前连接数小于最大连接数,则继续创建新的连接;
  • 如果当前连接数大于等于最大连接数,并且没有空闲连接了,则请求按照超时时间等待旧连接可用。
  • 超时之后,则获取数据库连接失败;

一般在线上建议最小连接数控制在 10 左右,最大连接数控制在 20~30 左右即可。

Http 连接池:

HttpClient 我们经常用来进行 HTTP 服务访问。如果 HttpClient 的每次请求都会新建一个连接,当创建连接的频率比关闭连接的频率大的时候,就会导致系统中产生大量处于 TIME_CLOSED 状态的连接。这个时候使用连接池复用连接就能解决这个问题。

常用连接池、对象池

Java 线程池

Java 线程池示意图

数据库/HTTP 连接池

连接池示意图

Dubbo/Tomcat 连接池

Dubbo/Tomcat 连接池示意图

总结

  • 池子的最大值和最小值的设置很重要,初期可以依据经验来设置,后面还是需要根据实际运行情况做调整。
  • 池子中的对象需要在使用之前预先初始化完成,这叫做池子的预热。使用线程池时就需要预先初始化所有的核心线程。如果池子未经过预热可能会导致系统重启后产生比较多的慢请求。
  • 池化技术核心是一种空间换时间优化方法的实践,所以要关注空间占用情况,避免出现空间过度使用出现内存泄露或者频繁垃圾回收等问题。

Jonsam

一个理科IT宅男,喜欢旅游、分享和美食,做点想做的事情,遇见想见的人。

🍒 美食 | 🌐 FE | 🕌 旅行 | 💻 加班 | ♍ 处女座

#
首页      技术      [架构] 从零开始学软件架构:核心服务层架构设计之调度与池化
jonsam ng

jonsam ng

文章作者

海阔凭鱼跃,天高任鸟飞。

[架构] 从零开始学软件架构:核心服务层架构设计之调度与池化
本文是从零开始学软件架构系列文章的第五篇,主题为核心服务层架构设计,主要解读核心服务中的任务调度和池化技术。本文的主要内容包括: Timer 定时器调度机制、ScheduledExecutor 调度…
扫描二维码继续阅读
2022-04-04