From 217820254cc06ee885ccc4528f10cec8c0963f7e Mon Sep 17 00:00:00 2001 From: Xargin Date: Thu, 27 Dec 2018 15:40:55 +0800 Subject: [PATCH] =?UTF-8?q?ch6=20=E5=9B=BE=E7=89=87=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=BC=96=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ch6-cloud/ch6-01-dist-id.md | 10 ++++++++-- ch6-cloud/ch6-03-delay-job.md | 14 +++++++++++++- ch6-cloud/ch6-04-search-engine.md | 12 +++++++++++- ch6-cloud/ch6-07-crawler.md | 6 ++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/ch6-cloud/ch6-01-dist-id.md b/ch6-cloud/ch6-01-dist-id.md index 2451490..b754d0c 100644 --- a/ch6-cloud/ch6-01-dist-id.md +++ b/ch6-cloud/ch6-01-dist-id.md @@ -4,10 +4,12 @@ 在插入数据库之前,我们需要给这些消息/订单先打上一个ID,然后再插入到我们的数据库。对这个id的要求是希望其中能带有一些时间信息,这样即使我们后端的系统对消息进行了分库分表,也能够以时间顺序对这些消息进行排序。 -Twitter的snowflake算法是这种场景下的一个典型解法。先来看看snowflake是怎么一回事: +Twitter的snowflake算法是这种场景下的一个典型解法。先来看看snowflake是怎么一回事,见*图 6-1*: ![snowflake](../images/ch6-snowflake.png) +*图 6-1 snowflake中的比特位分布* + 首先确定我们的数值是64 位,int64类型,被划分为四部分,不含开头的第一个bit,因为这个bit是符号位。用41位来表示收到请求时的时间戳,单位为毫秒,然后五位来表示数据中心的id,然后再五位来表示机器的实例id,最后是12位的循环自增id(到达 1111 1111 1111 后会归 0)。 这样的机制可以支持我们在同一台机器上,同一毫秒内产生`2 ^ 12 = 4096`条消息。一秒共409.6万条消息。从值域上来讲完全够用了。 @@ -49,6 +51,8 @@ mysql> select last_insert_id(); ![ch6-snowflake-easy](../images/ch6-snowflake-easy.png) +*图 6-2 snowflake库* + 和标准的snowflake完全一致。使用上比较简单: ```go @@ -101,10 +105,12 @@ Epoch 就是本节开头讲的起始时间,NodeBits指的是机器编号的位 ### 6.1.2.2 sonyflake -sonyflake是Sony公司的一个开源项目,基本思路和snowflake差不多,不过位分配上稍有不同: +sonyflake是Sony公司的一个开源项目,基本思路和snowflake差不多,不过位分配上稍有不同,见*图 6-2*: ![sonyflake](../images/ch6-snoyflake.png) +*图 6-3 sonyflake* + 这里的时间只用了39个bit,但时间的单位变成了10ms,所以理论上比41位表示的时间还要久(174 years)。 `Sequence ID`和之前的定义一致,`Machine ID`其实就是节点id。`sonyflake`与众不同的地方在于其在启动阶段的配置参数: diff --git a/ch6-cloud/ch6-03-delay-job.md b/ch6-cloud/ch6-03-delay-job.md index 5363710..d9a1686 100644 --- a/ch6-cloud/ch6-03-delay-job.md +++ b/ch6-cloud/ch6-03-delay-job.md @@ -17,10 +17,12 @@ timer的实现在工业界已经是有解的问题了。常见的就是时间堆 ### 6.3.1.1 时间堆 -最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树: +最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树,见*图6-4* ![二叉堆](../images/ch6-binary_tree.png) +*图 6-4 二叉堆结构* + 小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。所以对于定时check来说,时间复杂度是O(1)的。 当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是O(LgN)。 @@ -29,6 +31,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆 ![四叉堆](../images/ch6-four-branch-tree.png) +*图 6-5 四叉堆结构* + 小顶堆的性质,父节点比其4个子节点都小,子节点之间没有特别的大小关系要求。 四叉堆中元素超时和堆调整与二叉堆没有什么本质区别。 @@ -37,6 +41,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆 ![timewheel](../images/ch6-timewheel.png) +*图 6-6 时间轮* + 用时间轮来实现timer时,我们需要定义每一个格子的“刻度”,可以将时间轮想像成一个时钟,中心有秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的tasklist是否有已经到期的任务。 从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间%时间轮元素大小。那么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。 @@ -51,6 +57,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆 ![task-dist](../images/ch6-task-sched.png) +*图 6-7 分布式任务分发* + 每一个实例每隔一小时,会去数据库里把下一个小时需要处理的定时任务捞出来,捞取的时候只要取那些task_id % shard_count = shard_id的那些task即可。 当这些定时任务被触发之后需要通知用户侧,有两种思路: @@ -70,6 +78,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆 ![数据分布](../images/ch6-data-dist1.png) +*图 6-8 任务数据分布* + 一份数据虽然有两个持有者,但持有者持有的副本会进行区分,比如持有的是主副本还是非主副本,主副本在图中为摸黑部分,非主副本为正常线条。 一个任务只会在持有主副本的节点上被执行。 @@ -78,6 +88,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆 ![数据分布2](../images/ch6-data-dist2.png) +*图 6-9 故障时数据分布* + 节点1的数据会被迁移到节点2和节点3上。 当然,也可以用稍微复杂一些的思路,比如对集群中的节点进行角色划分,由协调节点来做这种故障时的任务重新分配工作,考虑到高可用,协调节点可能也需要有1 ~ 2个备用节点以防不测。 diff --git a/ch6-cloud/ch6-04-search-engine.md b/ch6-cloud/ch6-04-search-engine.md index dd6cc3f..609264a 100644 --- a/ch6-cloud/ch6-04-search-engine.md +++ b/ch6-cloud/ch6-04-search-engine.md @@ -28,12 +28,16 @@ Elasticsearch是开源分布式搜索引擎的霸主,其依赖于Lucene实现 ![posting-list](../images/ch6-posting_list.png) +*图 6-10 倒排列表* + 对Elasticsearch中的数据进行查询时,本质就是求多个排好序的序列求交集。非数值类型字段涉及到分词问题,大多数内部使用场景下,我们可以直接使用默认的bi-gram分词。什么是bi-gram分词呢: -即将所有Ti和T(i+1)组成一个词(在es中叫term),然后再编排其倒排列表,这样我们的倒排列表大概就是这样的: +即将所有Ti和T(i+1)组成一个词(在Elasticsearch中叫term),然后再编排其倒排列表,这样我们的倒排列表大概就是这样的: ![terms](../images/ch6-terms.png) +*图 6-11 “今天天气很好”的分词结果* + 当用户搜索'天气很好'时,其实就是求:天气、气很、很好三组倒排列表的交集,但这里的相等判断逻辑有些特殊,用伪代码表示一下: ```go @@ -345,6 +349,8 @@ SQL的where部分就是boolean expression。我们之前提到过,这种bool ![ast](../images/ch6-ast-dsl.png) +*图 6-12 AST和DSL之间的对应关系* + 既然结构上完全一致,逻辑上我们就可以相互转换。我们以广度优先对AST树进行遍历,然后将二元表达式转换成json字符串,再拼装起来就可以了,限于篇幅,本文中就不给出示例了,读者朋友可以查看: > github.com/cch123/elasticsql @@ -361,6 +367,8 @@ SQL的where部分就是boolean expression。我们之前提到过,这种bool ![sync to es](../images/ch6-sync.png) +*图 6-13 基于时间戳的数据同步* + 这种同步方式与业务强绑定,例如wms系统中的出库单,我们并不需要非常实时,稍微有延迟也可以接受,那么我们可以每分钟从MySQL的出库单表中,把最近十分钟创建的所有出库单取出,批量存入es中,具体的逻辑实际上就是一条SQL: ```sql @@ -381,6 +389,8 @@ select * from wms_orders where update_time >= date_sub( ![binlog-sync](../images/ch6-binlog-sync.png) +*图 6-13 基于binlog的数据同步* + 业界使用较多的是阿里开源的Canal,来进行binlog解析与同步。canal会伪装成MySQL的从库,然后解析好行格式的binlog,再以更容易解析的格式(例如json)发送到消息队列。 由下游的Kafka消费者负责把上游数据表的自增主键作为es的document的id进行写入,这样可以保证每次接收到binlog时,对应id的数据都被覆盖更新为最新。MySQL的row格式的binlog会将每条记录的所有字段都提供给下游,所以实际上在向异构数据目标同步数据时,不需要考虑数据是插入还是更新,只要一律按id进行覆盖即可。 diff --git a/ch6-cloud/ch6-07-crawler.md b/ch6-cloud/ch6-07-crawler.md index ece079a..dd3741e 100644 --- a/ch6-cloud/ch6-07-crawler.md +++ b/ch6-cloud/ch6-07-crawler.md @@ -78,6 +78,8 @@ func main() { ![dist-crawler](../images/ch6-dist-crawler.png) +*图 6-14 爬虫工作流程* + 上游的主要工作是根据预先配置好的起点来爬取所有的目标“列表页”,列表页的html内容中会包含有所有详情页的链接。详情页的数量一般是列表页的10~100倍,所以我们将这些详情页链接作为“任务”内容,通过消息队列分发出去。 针对页面爬取来说,在执行时是否偶尔会有重复其实不太重要,因为任务结果是幂等的(这里我们只爬页面内容,不考虑评论部分)。 @@ -94,10 +96,14 @@ nats的服务端项目是gnatsd,客户端与gnatsd的通信方式为基于tcp ![nats-protocol-pub](../images/ch6-09-nats-protocol-pub.png) +*图 6-15 nats协议中的pub* + 以workers的queue从tasks subject订阅消息: ![nats-protocol-sub](../images/ch6-09-nats-protocol-sub.png) +*图 6-16 nats协议中的sub* + 其中的queue参数是可选的,如果希望在分布式的消费端进行任务的负载均衡,而不是所有人都收到同样的消息,那么就要给消费端指定相同的queue名字。 #### 基本消息生产