高可用出现在哪?
出现在你数据量非常的庞大,比如你的业务在1秒内有上亿条数据,然后调用你的业务接口,把结果存到你的数据库里面,进行保存。
那么都需要考虑哪方面呢?
- 亿级数据如何存储?
- 如何避免跑批任务重复调度?
- 如何利用集群进行负载均衡?
- 同一节点上任务如何并行?
- 如何动态调整任务并发度?
- 节点挂了如何保证任务不受影响?
答案解决
数据怎么存?
亿级的数据普通的 MySQL 数据库单表肯定存不下,因为 MySQL 的单表极限一般在 3000万行。首先想到的解决方案是分库分表,比如把 1 亿行分到 10 个表中。
但是使用数据库也伴随者两个问题:一是遍历数据对数据库的压力是很大的,越往后速度越慢。二是一般跑批的数据只用一次,每次跑完都要删除数据,会造成数据碎片问题。(这两个问题也不是说不能解决,遍历数据库越往后查压力越大,可以设置在每次查询的时候携带上一次的极值,让你分页查找的offect永远控制在0;至于碎片问题我现在确实没找到有效的方法,但是我知道它肯定存在)
所以考虑另一种方案,选择文件系统。一般跑批的数据都是从数仓或者其它来源导入的,跟分库分表的思路一样,导入时就切分成多个文件。这个文件系统可以是 FTP,也可以是分布式对象存储,比如阿里云的 oss。使用文件系统的好处有 2 个,一个是数据可以一直存着,什么时候想重跑都行。二是顺序读文件的速度很快,可以利用磁盘io顺序读的特性提高性能。
如何避免跑批任务重复调度?
在集群上调度任务,首选分布式任务调度,主流的比如 xxl-job。
分布式任务调度只能保证准时调到一个节点上,而且通常都有失败重试的功能。所以任务幂等都是要的,一般都是通过分布式锁来实现,这里遵循简单原则使用数据库就可以了,可以通过在任务表里 insert 一条唯一的任务记录,通过唯一键来防止重复调度。
除了用唯一键,还可以在记录中增加一个状态字段,使用乐观锁来更新状态。比如开始是初始化状态,更新成正在运行的状态,更新失败说明别的节点已经在跑这个任务。当然分布式锁的实现方案有很多,比如 redis、zk 等等。
集群怎么搭?
上一步中,只创建了一个任务,要上集群首先要做的是任务拆分,就是把一个跑批任务拆分成多个子任务。记不记得第一步我们把文件拆成多个了,这样就可以一个文件创建一个子任务。
子任务有了,那怎么分发出去呢?最简单的方式是通过消息队列。每个子任务一个消息,集群中所有节点在一个消费者组中,消息队列会保证任务得均匀分发。这里就到了消息队列得选型问题了,当然也可以选择 Kafka 或者 RocketMQ,以及Redis的list来实现。
任务并行
如果上一步选择了 MQ,那在每个节点上,肯定还是要同时跑多个子任务才能资源利用最大化。那么你可以使用线程池。
如果选择的是Kafka或者 RocketMQ,他们的客户端本来就是线程池消费的,只需要合理调整客户端参数就可以了。如果使用的是 Redis,那就需要自己创建一个线程池,然后让一个 EventLoop 线程从 Redis 队列中取任务。
放入线程池中运行,因为我们已经使用 Redis 队列做缓冲,所以线程池的队列长度设为0,这里直接使用JDK提供的 SynchronousQueue。(这里以java为例)
动态调整并发度
跑批任务中能动态调整速度是很重要的,有 2 个地方可以进行操作:
- 任务中调用远程接口,这个速度控制其实用 Thread.sleep() 就好了。
- 控制任务并发度,就是有多少个线程同时运行任务。这个控制可以通过调整线程池的线程数来实现,但是线程池动态调整线程数比较麻烦。动态调整可以通过开源的限流组件来实现,比如 Guava 的 RateLimiter。可以在每次调用远程接口前调用限流组件来控制并发速度。
失败任务如何继续
理一下整个路径:分布式任务调度创建跑批任务,然后拆分子任务并发到消息队列。线程池执行任务调用远程接口。
在这个链条中,可能导致任务失败或者中止的原因无非下面几个。
- 机器重启,导致这台上面的任务都异常中止了
- 任务执行过程中,接口一直失败,超过次数子任务终止
- 机器资源不足,内存溢出了。
要解决上面的问题,重点就 2 个:记录进度、任务重试。
首先在子任务执行过程中,需要一直刷新现在执行到文件哪一行了,同时记录更新时间,再次,分布式任务调度系统中增加一个补偿任务,定时扫描所有还在执行中的子任务,如果发现任务进度长时间未更新。那就说明任务没有在运行了。将这个任务状态改成待执行,重新放入消息队列就可以了。
ok,如果感兴趣的话,可以添加我网站主页的微信,或者在此网站上面注册账号进行登录,注册账号的会在我每次发布文章后都会收到邮箱提醒,谢谢你的观看。
我的个人网站:www.zpf0000.com