xtimer-01-项目构建
xtimer-01-项目构建
学习核心
- 定时微服务的价值
- 定时微服务的背景和研究现状
- 架构设计方法论
- 定时微服务难点分析
- 高精准
- 高负载
- 其他关键问题
学习资料
定时微服务的价值
定时微服务的优势
【1】场景宽泛,方便立意
在真实业务中,定时场景还是比较常见的,很多业务会涉及定时的场景。基于项目立意,理解为什么要做一个定时微服务项目,可以结合学校业务、公司业务构思定时的场景应用,合理解释项目立意。
例如公司业务中知识库管理相关,会通过定时任务推送文章信息(同步到统一存储服务、ES等);一些业务场景的消息通知等
【2】业务属性较弱,适合抽成一个独立的中台服务
基于上面的立意思考,考虑到定时场景是一个通用且业务关联性弱的场景,很多业务场景都会用到,因此将这部分的定时逻辑抽离成一个单独的服务也是合理的。在微服务场景中可以将其作为一个中台服务去维护。
【3】核心逻辑聚焦,容易掌握
定时服务并不复杂,场景很好理解,逻辑也很好理解,所以比较容易掌握。它的特点不是业务本身的复杂性,而难点在于如何提高定时的精度,如何能扛住高负载等。
【4】可以囊括很多核心技术栈
定时微服务在如何提高定时精度,以及如何提高任务吞吐量这些核心上有很多分析点,会用到MySQL、Redis等核心组件,可以将这个项目作为引子,引导对于这方面相关的技术考察。针对项目扩展,如果涉及消息队列(类似Kafka,RocketMQ消息队列提供异步回调的能力),可以将其引入作为最后回调业务方的一种方式,然后引导对于消息队列的考察。
定时微服务的价值
【1】提升架构设计能力
定时微服务采用模块化设计和分治策略等设计理念,在如何提高定时精度,如何提高系统高负载等问题解决方案上,运用了很多好用设计理念和思想,这些设计能力在后续公司的业务场景中也是可以发挥作用,适合架构能力的提高。
【2】应对技术答辩和思维发散
定时微服务的场景很好立意,基于学校或公司的业务场景,设计一款功能聚焦,容易维护的定时微服务是合理的。
项目的设计难点:定时微服务具有高精度、高负载等核心难点,其次在设计中运用了很多模块化,分治等设计理念来解决这些难点,基于这些点的扩展有助于思维发散和对核心技术栈的把控,包括MySQL、Redis、消息队列等后端核心技能点。项目的可扩展性很高,可以融入其他技术栈进行支撑,将技术考察引导到核心技术点,关注核心实现
定时微服务的背景和研究现状
1.研究背景和研究现状
定时微服务的应用场景
公司业务场景(实际业务场景)
- 对于知识库平台的文章推送,需要定时将最新的文章信息推送到统一存储平台(文章基础信息同步、文章关联附件信息同步)
- 消息通知机制:fhyd 定时短信通知、待办事项通知等(事项提醒、催办推送等场景)
参考业务场景
- 某“学习平台”团队:每天早上10点需要给学员发送学习通知
- 某“招聘平台”团队:需要在2023年9月10日-9月17期间,给所有待参加笔试的同学,每天下午14点、20点发送笔试通知
- 某“电商”平台:订单下单15分钟后,用户如果没有付钱,系统需要自动取消订单
- 微信红包业务:红包24小时未被查收,需要执行退还业务;
- 某个活动指定在某个时间点生效&失效;
定时微服务的理解
大量的业务场景其实都会涉及一个定时执行或者周期执行的情况,而关于定时本身这个功能点是完全可以做到跟业务属性剥离,由一个独立的功能模块统一提供定时和叫醒服务,至于叫醒之后需要做什么事情,则由不同的业务自行处理。
可以理解为业务共同拥有一个公用的闹钟(定时微服务),可以在闹钟上设定指定的时间点和唤醒频率,等时间到了之后,闹钟会自动唤醒业务执行相应的业务逻辑操作,而这个“闹钟”只关注定期的唤醒动作,而不关心唤醒之后业务要执行什么操作(这些是由业务决定的)
定时微服务的调研对比
定时场景的适用范围很广,市面上很多技术都涉及定时功能,基于现有的技术栈进行调研对比
方案 | 介绍 | 特性 | 不足 |
---|---|---|---|
公司内部实现 | 很多公司内部都有各自定时器的实现 | 服务于内部业务,且服务设计大多与内部业务特性进行挂钩 | 仅面向公司内部使用 |
Java Timer | Java 标准库中的定时工具 | 1.简单易用 2.基于单线程执行任务 | 1.单线程执行可能导致任务间相互影响 2.不适合执行长时间运行的任务 3.异常处理不足 |
RocketMq | 分布式消息队列系统 | 1.基于消息队列的定时任务执行 2.高可用性和可扩展性 3.支持延迟消息和定时消息 | 1.需要额外维护一个消息队列系统 2.延迟精度可能受到网络延迟和系统负载的影响 3.存在极小的消息丢失或重复消费的风险 |
elastic-job-lite | 轻量级分布式任务调度框架 | 1.支持弹性扩展和任务分片 2.集成简单、配置丰富 3.基于Zookeeper的协调服务 | 1.依赖ZooKeeper作为协调服务,增加了系统复杂性 2.对于简单的定时场景可能过于复杂 |
xxl-job | 代表性的自定义任务调度工具 | 1.可定制性强 2.可集成到现有应用 3.功能灵活 | 1.学习曲线可能较陡峭 2.可能需要额外开发和维护成本 3.对于简单的定时任务需求过于复杂 |
Quartz | 功能强大的任务调度框架 | 1.灵活的任务调度 2.支持集群和持久化 3.可与数据库结合使用 | 1.配置和管理相对复杂 2.资源消耗较大,特别是在大规模任务调度时 3.对于简单的定时需求处理过于复杂 |
Robfig/Cron | Golang版本github上star最多的开源框架 | 受到广泛的使用和测试,评测结果表示与其他go开源框架相比,其拥有最高的调度准确性 | 1.设计用于在单个应用实例中执行定时任务 2.不支持分布式任务调度 3.缺乏任务执行结果的监控和告警机制 |
很多公司业务对于定时任务功能需求并不复杂,可能就是简单“定时”需求而已。所以为了一个简单的定时功能引入功能强大但臃肿的任务调度组件是不合适的,前期的学习成本/接入成本、后期的维护成本等都会很高
基于上表整体来看,用一些常用技术,设计一个功能聚焦、接入轻量、维护成本低,同时支持动态灵活的任务周期处理(创建,激活,调度,执行)的定时微服务还是有比较大的用处的。对于公司内部业务场景实现来说,也会有很多自研的适用于公司内部业务服务的定时微服务框架
项目定时微服务xtimer
依赖简单:接入和维护成本低(例如常用的mysql、redis组件支持)
学习成本低:整个定时微服务功能聚焦,复杂度可控,非常容易上手。所以学习成本,维护成本等都比较低
特性优:Xtimer具有高精准、高负载、异常处理等特性,基本能满足大部分业务场景的定时需求
2.扩展问题分析
✨如何理解“单线程执行可能导致任务间相互影响”?
单线程执行所有任务,就是所有任务“排队”执行。前一个任务如果发生阻塞或者死锁(比如比预期时间延迟了 5s),那下一个任务的执行时间也会延迟。即任务为串行执行,性能的高低完全取决于上一条执行的好坏,所以性能差且不稳定了(类似同步机制,必须上一条执行完毕,才能执行下一步)。
✨在扩展优化中提到“使用RockerMQ做异步回调”,既然RockerMQ可以提供定时功能,那XTimer+RockerMQ的组合是否多此一举?
todo:RockerMQ 的优化引入待确认
✨定时需求的“简单性”和“复杂性”如何定义?
“简单的定时需求”是指只需要处理“定时”这一核心即可,具体的执行由业务逻辑实现。而针对“复杂的定时需求”除了“定时”这一基础功能,可能还要求平台执行定时前后对应的“业务化的任务”,可能涉及的类型千奇百怪,处理相对复杂。
而xtimer的设计核心是只关心“定时”,就像是一个公共闹钟,至于闹钟到点唤醒相应的业务操作逻辑则由相关的业务方执行,这就是定时需求相对简单的概念。
架构设计方法论
架构设计最忌讳的其实就是什么一开始没有充分思考,直接上手就想当然设计,这样设计出来的系统一定是漏洞百出的,要做一个好的架构设计,需关注如下步骤:
- 【1】一定要做好足够的准备工作,包括信息收集、包括技术调研
- 【2】有一套方法论,成体系地去进行设计
- 参考此处提供的方法论:大的方向分为准备阶段和设计阶段和优化阶段
- 准备阶段:分为诉求摸底、请求量确认、精准性深究、难点要点分析;
- 设计阶段:分为上下游服务分析,存储选型、流程串联;
- 优化阶段:分为设计复盘和潜在优化点考虑;
- 参考此处提供的方法论:大的方向分为准备阶段和设计阶段和优化阶段
1.准备阶段
架构设计也不是一开始就设计,一定要做一些准备工作:
- 【1】诉求摸底:摸清整体诉求,了解清楚是做什么?业务流程?,核心功能点核心设计?
- 【2】请求量确认:考虑QPS和并发量
- QPS(1k/10k/100k的做法是不一样的):不同请求量的方案可以说天差地别。实际工作中不能低估请求量,需要适当高估留点buffer,但同时也不能大炮打蚊子(例如引入10台Redis做一个QPS为1000/s的业务,无疑是杀鸡用牛刀)
- 更专业一点来说并发量也是需要考虑的,但是通常说gps就够了,不用那么复杂
- 【3】精准性深究:例如对于秒杀系统而言,就是能不能多卖、能不能少卖,多卖肯定是不允许的,少卖看看情况
- 【4】难点要点分析:分析一波难点要点,这对梳理设计有好处
2.设计阶段
基于前置准备分析,可以进行架构设计以把握大方向。如果一开始对整个系统的架构设计有点无从下手,则可以尝试化整为零进行拆分
架构核心:无非是服务+存储+流程,有什么服务?存储用的是什么?结合这些服务和存储如何将流程串联起来
上下游服务分析
分析服务分几层,会涉及到哪些上下游服务
例如秒杀场景下,涉及到的服务可能会很多(商品信息服务、秒杀服务、库存服务、订单服务、支付服务等)。在大公司大团队里,它们通常都是非常独立的微服务,但在中小型团队可能拆得不会这么细,微服务拆得太多其实是灾难
存储选型
后端开发一个很重要的职责就是跟数据打交道,要把数据存起来。存储选什么呢?影响因素有很多,包括存储场景、可靠性、性能要求、团队技术栈,需要综合性考虑,才能确定存储选型。比如,最常用的组合场景:高性能需要Redis、高精准需要MySQL,所以很多业务的存储就是这两个组件打配合。但不管如何,存储方案的选型要结合业务的各种影响因素和存储方案的对比来进行确认
流程串联
服务层次设计好了,存储选型也敲定了,最后需要做的就是基于流程将他们串联起来。
一般可以以一个流程为例。比如发起秒杀这个流程,如果流程很丝滑地能梳理完成,那么这个架构就不会有大问题。在这个过程中,需要开始考虑对难点、要点进行详细分析,每个系统难点要点都有区别,但也有规律,但无非是核心流程细节、高并发、高精准等问题。
比如秒杀场景,其难点无非是高并发和高精准,则需要去展开高并发的解决方案,去对比、决策,高精准的话需要考虑到超卖、少卖问题,以及各种异常情况下,是否有机制能兜底
3.优化阶段
做了初步的架构设计之后,还需要整体复盘以及优化点考虑,这个过程下是检查架构设计有没有比较明显的漏洞,架构是不是具备扩展性,还没有哪些潜在的优化点。
架构复盘(设计复盘):复盘上面的设计思路可否实现功能?例如针对秒杀场景,在脑海中或者纸上画一画,看这个架构是不是真的能很好应付秒杀流程,以及存储的选型是不是足够合理
潜在优化点考虑:例如针对瞬时流量场景下的性能优化考虑,引入消息队列是否能进一步提高性能呢?即使初版没有安排实现,但还是需要考虑架构的可扩展性,便于后续迭代优化
定时微服务难点分析
1.高精准问题
在定时场景中,高精准问题指的是预期设定的触发时间与实际的触发时间之间的误差值。这个误差值肯定不可能为0,但是依然有必要去想办法减少这个误差值。
业务容忍度:虽然不同业务场景对这个误差值容忍度不一样,但从整体来说,越高的精准度适用的业务范围会越广,服务可信度越高,业务也越不容易出问题
- 比如定时发通知,可能有的日常通知延迟个分钟级别都不会有问题
- 而如果是定时开始考试,可能容忍度就只能是秒级甚至毫秒级等
问题1:xtimer能否做到毫秒级保证吗?
不能。因为这是一个分布式的微服务,涉及到期间的网络调用、数据IO、触发时机间隔等耗时,诸多因素导致无法承诺毫秒级的精准度。因此大部分的分布式定时工具都没办法承诺毫秒级的精准度,只有单应用级别例如Java timer这种可以做到毫秒级别。所以如果有要求毫秒级的精准度的业务期望接入xtimer定时微服务,一般来说不建议。(当然,真实业务的业务场景中,很少有要求毫秒级的定时的)
问题2:常见导致时间误差大的原因
【1】基于定时脚本的方式:例如设定一个固定的定时脚本,每5分钟扫描一次所有需要执行的任务,那最大延时误差就接近5分钟。
虽然可以将时间间隔调小(例如5分钟-30秒)。但是间隔越小,带来的性能消耗就会增加,大部分时间脚本运行和任务检索都是浪费掉的(一个任务从创建到最终执行,期间被判定了N次,但只有最后一次才是有用的);
且本身脚本运行需要时间,随着系统内任务数量的增加,脚本执行时间就会增加(例如5秒),那误差延时最短也就能控制到脚本的运行时间(5秒)
【2】基于消息队列实现的定时器:当任务很多,甚至出现堆积的时候,那排队等待被消费的时间就会导致延时增加
【3】基于分布式实现的定时器:当整个任务的调度过程需要通过网络在不同组件之间流转的时候,网络问题、网络正常耗时等都会让任务延时增加
【4】任务数据的查找耗时:当一个任务的触发过程中,可能会涉及多次任务状态的查询,修改等操作,如果数据存储得不合理,导致每一次检索复杂度很高,那整个触发流程的耗时自然就会增高(例如数据库索引不合适导致全表扫描、数据库任务很多导致检索难度加大、本该使用缓存的没用到等情况)。因此定时任务的存储方式、检索复杂度等也是一个影响误差大小的关键因素
【5】同一时间需要处理的任务量很大:例如10:59:00这个时间点大量定时任务需要触发,那由于系统的处理能力是有限的,任务处理必定有先有后,形成一种排队的状态,那后面处理的任务延时肯定是更高的
高精准问题
相对于单点定时(例如Java.timer)基本能在毫秒级触发的性能来说, 几乎所有的定时任务框架或者消息队列等都只能保证秒级误差。虽然不同业务场景对于定时任务的误差容忍程度不一样,但是精准度依然是一个定时器的重要考核指标,因为越精准那能支持的业务类型就越多。所以,当业务如果对于精准度要求比较高的时候,如何通过技术手段去不断减少延时会是定时微服务的一个难点问题
考虑业务场景对定时触发误差(预期设定的触发时间和实际的出发时间之间的差值)的容忍度,当业务对于精准度要求比较高的时候,考虑如何通过技术手段去减少延时是定时微服务的一个重点难点问题,误差越小、精度越高,可支持的业务类型就越多、可靠性也越强
2.高负载问题
高负载核心概念
高负载难点分析
在真实的业务场景中,定时的运用是很多的,随着接入的业务越来越多,自然会导致任务量的暴涨,任务的增多会给数据存储、数据检索、任务执行、任务触发时效等带来影响,因此xtimer的高负载概念,主要就是要解决任务量增加之后带来的问题
高负载解决方案(高负载解决思路和方法)
(1)应用层面减少任务量
业务方整合任务列表,减少定时任务量:针对某个时间点要执行的操作列一个计划表,尽量将操作挂靠在一个定时任务上,减少定时任务量
比如业务A需要在10点发通知、关闭订单、触发脚本等,那A应该将这三个业务都挂靠在一个定时任务上,而不是拆分多个定时任务来操作(xtimer提供定时,到点之后干什么取决于业务逻辑的实现)。对于目标时间不一致或者不确定的场景,则考虑分开创建定时任务
实际场景举例:假设自己设手机闹钟的场景。比如有一个计划,需要在早上10点执行,计划内容包括(喝水、看消息、打电话),一般来说是不会设置三个闹钟(例如闹钟1(10:00 喝水)、闹钟2(10:00 看消息)、闹钟3(10:00 打电话))。实际更倾向设置一个闹钟,至于闹钟响了之后需要干嘛(执行10点后的计划列表),至于计划内容是需要使用者自己额外维护的,
即如果一个业务方同一时间有多个任务要执行,那它应该将多个任务列成一个“计划列表”(考虑子任务的优先级),然后为该计划列表创建1个定时任务,而不是为计划里每一项创建一个定时任务
(2)分库分表
分库分表的基本概念是将一个大型数据库分成多个较小的数据库(分库),并将每个数据库的数据进一步分成多个较小的表(分表),每个表只包含部分数据。这种方式使得查询和更新操作可以在多个数据库和表之间并行执行,提高了系统的扩展性和性能。常见的分库分表策略分析如下
【1】垂直分表
垂直分表是指将原始数据按照列拆分成多个表,每个表只包含某些列。这种策略通常用于处理包含大量无关字段的表
例如,对于一个包含用户信息和订单信息的表,可以将用户信息和订单信息拆分成两个表,使得用户信息和订单信息可以分别存储在不同的表中
【2】水平分表
水平分表是指将原始数据按照行拆分成多个表,每个表只包含某些行。这种策略通常用于处理数据量大的表
例如,对于一个包含订单信息的表,可以将订单信息按照订单号的哈希值分散到多个表中
【3】分库分表组合
分库分表组合是指将垂直分表和水平分表结合起来,同时对数据库和表进行切分
例如,对于一个包含用户信息和订单信息的表,可以将用户信息和订单信息拆分成两个表,并将订单信息按照订单号的哈希值分散到多个表中,最终将这些表分散到多个数据库中
(3)数据分区
数据分区的思路其实跟分库分表理念类似,都是期望将数据进行拆分后,提升并发度,提升检索效率的。所以数据分区理念还可以运用在缓存数据等地方,例如将redis中的数据同样进行分区设计等。
(4)线程池技术
即便可以基于第1点,让业务方尽量减少任务量,但这依然只是一个使用上的倡议(优化思路),但最终系统运行起来之后,依然可能会存在同一时间大量任务需要执行的情况,而如何保证大批量任务的同时执行速度和质量依然是一个需要持续提升的点。
首先可以想到的,要提高批量任务的执行速度,那一定得使用并发,例如为每一个任务创建一个线程去执行,但这种方案很容易对线程的管理失控。而在并发执行批量任务这一块,线程池技术是天然的解决方案。一方面是可以让批量任务能够并发执行,加快速度。另外一方面,线程池技术也比较成熟,相对为每一个任务创建新线程难以管理这种来说,线程池本身就具备对线程进行管理的能力。
3.其他关键问题
👻任务异常处理
(1)任务异常原因
设想一下现实场景,如果早上设定了一个9点的闹钟,结果由于手机异常等原因导致闹钟没有响,甚至可能会把这个手机扔掉,至少不会信任这个手机设闹钟。同样的,对于定时微服务来说,业务方也是有类似需求的,对于上述高精度、高负载问题,可能业务方都有一定得容忍度。但是如果设定了一个定时任务却没有触发,这想必是大部分业务都是没办法容忍的。正常业务至少有一个诉求,可以允许触发时间可以有误差,但是不能不触发。
任务异常原因分析
【1】新建、激活接口异常:例如创建任务接口,激活任务接口等发生异常。这种类型的异常是可以通过接口返回值立即反馈给用户的,所以对于这种类型的异常用户可以立即感知到,通过主动重试等操作可以解决
【2】任务流转异常:一般认为一个定时任务成功存储到数据库就算创建成功,很多定时服务或者框架从此时就会开始接管后续任务的流转职责,直到任务运行完成。参考消息队列的流程分析,将一条定时消息发送到了消息队列之后就算成功,后续怎么保证消息能够顺利准时到达接收方,则是消息队列的职责,同理定时微服务也一样。 在整个流转过程中发生的异常,导致任务无法触发,都算流转异常
- a.数据库异常:整个流转过程中会频繁发生数据库存取,期间发生的任何数据库异常,都会导致任务流转失败
- b.中间件存取异常:例如,如果为了提升任务处理效率,将数据库任务缓存了一份到redis中,那期间会发生很多redis的存取操作,如果redis出现异常,则会导致相关批次任务的失败
【3】任务触发阶段异常:当任务进入触发阶段,需要回调业务方告知触发结果。如果回调失败,对于业务方来说,同样属于任务执行失败,等于任务没有触发
(2)常用解决方案
【1】重试机制:通过上面的分析,系统运行过程中,发生异常的概率还是很高的。如果是一些偶发性的异常,例如创建接口临时不可用、数据库抖动、redis抖动等,这类异常是可以通过重试机制解决
【2】失败兜底机制:很多问题并不能通过重试解决,例如mysql、redis、消息队列等中间件节点挂了,它是有自愈机制的(例如替换节点进行服务),但是这个自愈机制需要时间,在这个间隔期间可能重试也是没用的。那这种类型的任务则需要标记失败状态,等待后续处理。所谓后续处理即等待中间件恢复正常状态之后再重试。
一般通过运行兜底的“定时脚本”的方式,扫描所有失败的任务,将这些异常的任务统一再触发。但这种脚本运行成本很高(会全表扫描),并且脚本运行也需要时间,所以一般频率不能太高,只能作为兜底机制,例如5分钟运行一次(这种方式会导致时延,某些出问题的任务会延迟5分钟才能触发)
【3】失败报警机制:失败之后,报警机制也很重要,当知道有大量任务失败之后,维护人员需要及时查看原因,快速介入。如果发现系统已经恢复,也可以通过手动触发脚本的方式,让失败任务重新运行,而不用等兜底周期。另外,报警机制更重要是针对那些没办法通过再次运行达到成功状态的任务,这些任务可能就需要人工介入才能解决了(例如回滚代码,回滚配置等)
【4】人工干预:当部分任务通过系统正常流程没办法继续运行之后,例如任务的状态出现了异常,比如任务还没触发,结果状态已经属于执行完成(这种问题要从代码逻辑上进行规避,但难免还是会发生),如果这种问题发生了,就需要进行人工干预,例如手动修改任务状态
(3)问题扩展分析
问题1:针对失败兜底机制,采用全表扫描失败任务会不会不太好?有没有其他优化或平替方案?
【1】考虑失败兜底脚本执行的性能问题,一般这个时间也可以结合实际稍微拉大一点
【2】考虑把失败的任务单独存储或者做标记,检索失败任务
【3】只检索最近时间段失败的任务,不做全表扫描
例如只检索最近5分钟的失败任务,那么会不会检索不到全部的失败任务(可能一些任务是重试后又失败了)
这种方式取决于重试机制的设定,有些任务重试一次失败大概率后面重试都会失败,基于这种考虑则没必要每次失败的都去重试,则可能需要借助别的手段来解决