1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 04:22:22 +00:00
This commit is contained in:
Xargin 2019-01-01 20:03:56 +08:00
parent 3506dcd652
commit 92c923f504
2 changed files with 16 additions and 14 deletions

View File

@ -9,11 +9,13 @@
1. 实现一套类似crontab的分布式定时任务管理系统。 1. 实现一套类似crontab的分布式定时任务管理系统。
2. 实现一个支持定时发送消息的消息队列。 2. 实现一个支持定时发送消息的消息队列。
两种思路进而衍生出了一些不同的系统,但其本质是差不多的。都是需要实现一个定时器。定时器英文为timer在单机的场景下其实并不少见例如我们在和网络库打交道的时候经常会写`SetReadDeadline`,这实际上就是在本地创建了一个定时器,在到达指定的时间后,我们会收到定时器的通知,告诉我们时间已到。这时候如果读取还没有完成的话,就可以认为发生了网络问题,从而中断读取。 两种思路进而衍生出了一些不同的系统,但其本质是差不多的。都是需要实现一个定时器timer。在单机的场景下定时器其实并不少见例如我们在和网络库打交道的时候经常会调用`SetReadDeadline()`函数,这实际上就是在本地创建了一个定时器,在到达指定的时间后,我们会收到定时器的通知,告诉我们时间已到。这时候如果读取还没有完成的话,就可以认为发生了网络问题,从而中断读取。
timer的实现在工业界已经是有解的问题了。常见的就是时间堆和时间轮 下面我们从定时器开始,探究延时任务系统的实现
## 6.3.1 timer实现 ## 6.3.1 定时器的实现
定时器timer的实现在工业界已经是有解的问题了。常见的就是时间堆和时间轮。
### 6.3.1.1 时间堆 ### 6.3.1.1 时间堆
@ -23,11 +25,11 @@ timer的实现在工业界已经是有解的问题了。常见的就是时间堆
*图 6-4 二叉堆结构* *图 6-4 二叉堆结构*
小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。所以对于定时check来说时间复杂度是`O(1)` 小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复杂度是`O(1)`
当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是`O(LgN)` 当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是`O(LgN)`
Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆,而是使用了扁平一些的四叉堆。在最近的版本中,还加了一些优化,我们先不说优化,先来看看四叉的小顶堆长什么样: Go自身的内置定时器就是用时间堆来实现的,不过并没有使用二叉堆,而是使用了扁平一些的四叉堆。在最近的版本中,还加了一些优化,我们先不说优化,先来看看四叉的小顶堆长什么样:
![四叉堆](../images/ch6-four-branch-tree.png) ![四叉堆](../images/ch6-four-branch-tree.png)
@ -43,7 +45,7 @@ Go自身的timer就是用时间堆来实现的不过并没有使用二叉堆
*图 6-6 时间轮* *图 6-6 时间轮*
用时间轮来实现timer时,我们需要定义每一个格子的“刻度”,可以将时间轮想像成一个时钟,中心有秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的tasklist是否有已经到期的任务。 用时间轮来实现定时器时,我们需要定义每一个格子的“刻度”,可以将时间轮想像成一个时钟,中心有秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的任务列表是否有已经到期的任务。
从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间%时间轮元素大小。那么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。 从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间%时间轮元素大小。那么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。
@ -51,7 +53,7 @@ Go自身的timer就是用时间堆来实现的不过并没有使用二叉堆
## 6.3.2 任务分发 ## 6.3.2 任务分发
有了基本的timer实现方案,如果我们开发的是单机系统,那么就可以撸起袖子开干了,不过本章我们讨论的是分布式,距离“分布式”还稍微有一些距离。 有了基本的定时器实现方案,如果我们开发的是单机系统,那么就可以撸起袖子开干了,不过本章我们讨论的是分布式,距离“分布式”还稍微有一些距离。
我们还需要把这些“定时”或是“延时”(本质也是定时)任务分发出去。下面是一种思路: 我们还需要把这些“定时”或是“延时”(本质也是定时)任务分发出去。下面是一种思路:
@ -59,22 +61,22 @@ Go自身的timer就是用时间堆来实现的不过并没有使用二叉堆
*图 6-7 分布式任务分发* *图 6-7 分布式任务分发*
每一个实例每隔一小时,会去数据库里把下一个小时需要处理的定时任务捞出来,捞取的时候只要取那些task_id % shard_count = shard_id的那些task即可。 每一个实例每隔一小时,会去数据库里把下一个小时需要处理的定时任务捞出来,捞取的时候只要取那些`task_id % shard_count = shard_id`的那些任务即可。
当这些定时任务被触发之后需要通知用户侧,有两种思路: 当这些定时任务被触发之后需要通知用户侧,有两种思路:
1. 将任务被触发的信息封装为一条event消息,发往消息队列,由用户侧对消息队列进行监听。 1. 将任务被触发的信息封装为一条消息,发往消息队列,由用户侧对消息队列进行监听。
2. 对用户预先配置的回调函数进行调用。 2. 对用户预先配置的回调函数进行调用。
两种方案各有优缺点如果采用1那么如果消息队列出故障会导致整个系统不可用当然现在的消息队列一般也会有自身的高可用方案大多数时候我们不用担心这个问题。其次一般业务流程中间走消息队列的话会导致延时增加定时任务若必须在触发后的几十毫秒到几百毫秒内完成那么采用消息队列就会有一定的风险。如果采用2会加重定时任务系统的负担。我们知道单机的timer执行时最害怕的就是回调函数执行时间过长,这样会阻塞后续的任务执行。在分布式场景下,这种忧虑依然是适用的。一个不负责任的业务回调可能就会直接拖垮整个定时任务系统。所以我们还要考虑在回调的基础上增加经过测试的超时时间设置,并且对由用户填入的超时时间做慎重的审核。 两种方案各有优缺点如果采用1那么如果消息队列出故障会导致整个系统不可用当然现在的消息队列一般也会有自身的高可用方案大多数时候我们不用担心这个问题。其次一般业务流程中间走消息队列的话会导致延时增加定时任务若必须在触发后的几十毫秒到几百毫秒内完成那么采用消息队列就会有一定的风险。如果采用2会加重定时任务系统的负担。我们知道单机的定时器执行时最害怕的就是回调函数执行时间过长,这样会阻塞后续的任务执行。在分布式场景下,这种忧虑依然是适用的。一个不负责任的业务回调可能就会直接拖垮整个定时任务系统。所以我们还要考虑在回调的基础上增加经过测试的超时时间设置,并且对由用户填入的超时时间做慎重的审核。
## 6.3.3 rebalance 和幂等考量 ## 6.3.3 数据再平衡和幂等考量
当我们的任务执行集群有机器故障时,需要对任务进行重新分配。按照之前的求模策略,对这台机器还没有处理的任务进行重新分配就比较麻烦了。如果是实际运行的线上系统,还要在故障时的任务平衡方面花更多的心思。 当我们的任务执行集群有机器故障时,需要对任务进行重新分配。按照之前的求模策略,对这台机器还没有处理的任务进行重新分配就比较麻烦了。如果是实际运行的线上系统,还要在故障时的任务平衡方面花更多的心思。
下面给出一种思路: 下面给出一种思路:
我们可以参考Elasticsearch的设计每份任务数据都有多个副本这里假设两副本如*图 6-8*所示: 我们可以参考Elasticsearch的数据分布设计,每份任务数据都有多个副本,这里假设两副本,如*图 6-8*所示:
![数据分布](../images/ch6-data-dist1.png) ![数据分布](../images/ch6-data-dist1.png)
@ -92,6 +94,6 @@ Go自身的timer就是用时间堆来实现的不过并没有使用二叉堆
节点1的数据会被迁移到节点2和节点3上。 节点1的数据会被迁移到节点2和节点3上。
当然也可以用稍微复杂一些的思路比如对集群中的节点进行角色划分由协调节点来做这种故障时的任务重新分配工作考虑到高可用协调节点可能也需要有1 ~ 2个备用节点以防不测。 当然也可以用稍微复杂一些的思路比如对集群中的节点进行角色划分由协调节点来做这种故障时的任务重新分配工作考虑到高可用协调节点可能也需要有12个备用节点以防不测。
之前提到我们会用消息队列触发对用户的通知,在使用消息队列时,很多队列是不支持`exactly once`的语义的,这种情况下我们需要让用户自己来负责消息的去重或者消费的幂等处理。 之前提到我们会用消息队列触发对用户的通知,在使用消息队列时,很多队列是不支持`exactly once`的语义的,这种情况下我们需要让用户自己来负责消息的去重或者消费的幂等处理。

View File

@ -1,4 +1,4 @@
# 6.5 Load-Balance 负载均衡 # 6.5 负载均衡
本节将会讨论常见的分布式系统负载均衡手段。 本节将会讨论常见的分布式系统负载均衡手段。