Go语言高级编程-第6章 分布式系统
Go语言高级编程系列是我读《Go语言高级编程》时的一些要点总结。
- 有时我们需要能够生成类似 MySQL 自增 ID 这样不断增大, 同时又不会重复的 id。以支持业务中的高并发场景。比较典型 的,电商促销时,短时间内会有大量的订单涌入到系统,比如 每秒 10w+。明星出轨时,会有大量热情的粉丝发微博以表心 意,同样会在短时间内产生大量的消息。
在插入数据库之前,我们需要给这些消息/订单先打上一个 ID,然后再插入到我们的数据库。对这个 id 的要求是希望其 中能带有一些时间信息,这样即使我们后端的系统对消息进行 了分库分表,也能够以时间顺序对这些消息进行排序。
Twitter 的 snowflake 算法是这种场景下的一个典型解法。首先确定我们的数值是 64 位,int64 类型,被划分为四部分, 不含开头的第一个 bit,因为这个 bit 是符号位。用 41 位来表 示收到请求时的时间戳,单位为毫秒,然后五位来表示数据中 心的 id,然后再五位来表示机器的实例 id,最后是 12 位的循 环自增 id(到达 1111 1111 1111 后会归 0)。一般不同数据中心的机器,会提供对应的获取数据中心 id 的 api
在单机程序并发或并行修改全局变量时,需要对修改行为加锁以创造临界区
如果我们的逻辑限定每个 goroutine 只有成功执行了 Lock 才会 继续执行后续逻辑,因此在 Unlock 时可以保证 Lock struct 中 的 channel 一定是空,从而不会阻塞,也不会失败。这种方法可以叫trylock。
在单机系统中,trylock 并不是一个好选择。因为大量的 goroutine 抢锁可能会导致 cpu 无意义的资源浪费。有一个专有 名词用来描述这种抢锁的场景:活锁。
活锁指的是程序看起来在正常执行,但实际上 cpu 周期被浪费 在抢锁,而非执行任务上,从而程序整体的执行效率低下。活 锁的问题定位起来要麻烦很多。所以在单机场景下,不建议使 用这种锁。
redis 的 setnx 很适合在高并发场景下,用来争抢一些“唯一”的资源。
基于 zk 的锁与基于 redis 的锁的不同之处在于 Lock 成功之前 会一直阻塞,这与我们单机场景中的 mutex.Lock 很相似。
业务发展到一定量级的话,就需要从多方面来考虑了。首先是 你的锁是否在任何恶劣的条件下都不允许数据丢失,如果不允 许,那么就不要使用 redis 的 setnx 的简单锁。
对锁数据的可靠性要求极高的话,那只能使用 etcd 或者 zk 这 种通过一致性协议保证数据可靠性的锁方案。但可靠的背面往 往都是较低的吞吐量和较高的延迟。
定时器英文为 timer
实现timer,最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树。小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元 素比当前的时间还要大,那么说明堆内所有元素都比当前时间 大。进而说明这个时刻我们还没有必要对时间堆进行任何处 理。所以对于定时 check 来说,时间复杂度是 O(1) 的。当我们发现堆顶的元素 < 当前时间时,那么说明可能已经有一 批事件已经开始过期了,这时进行正常的弹出和堆调整操作就 好。当我们发现堆顶的元素 < 当前时间时,那么说明可能已经有一 批事件已经开始过期了,这时进行正常的弹出和堆调整操作就 好。每一次堆调整的时间复杂度都是 O(LgN)。
在 web 一章中,我们提到 MySQL 很脆弱。数据库系统本身要 保证实时和强一致性,所以其功能设计上都是为了满足这种一 致性需求。
关系型数据库一般被用于实现 OLTP 系统,所谓 OLTP,援引 wikipedia:
在线交易处理(OLTP, Online transaction processing)是指 透过信息系统、电脑网络及数据库,以线上交易的方式处 理一般即时性的作业数据,和更早期传统数据库系统大量 批量的作业方式并不相同。OLTP通常被运用于自动化的 数据处理工作,如订单输入、金融业务…等反复性的日常 性交易活动。和其相对的是属于决策分析层次的联机分析 处理(OLAP)。
如果我们所经营的是一个大型电商,根据关键字进行一次 like 查询,可能整个 MySQL 就直接挂掉了。这时候我们就需要搜索引擎来救场了。
elasticsearch 是开源分布式搜索引擎的霸主,其依赖于 Lucene 实现,在部署和运维方面做了很多优化。当今搭建一个分布式 搜索引擎比起 Sphinx 的时代已经是容易很多很多了。只要简 单配置客户端 ip 和端口就可以了。实际应用 中常常用 es 来作为 database 来使用,就是因为倒排列表的特 性。
json 本身是可以表达树形结构的,我们的程序代码在被编译器parse 之后,也会变成 AST,而 AST 抽象语法树,顾名思义, 就是树形结构。理论上 json 能够完备地表达一段程序代码被 parse 之后的结果。这里的 Boolean Expression 被编译器 Parse 之后也会生成差不多的树形结构,而且只是整个编译器实现的 一个很小的子集。
在实际应用中,我们很少直接向搜索引擎中写入数据。更为常 见的方式是,将 MySQL 或其它关系型数据中的数据同步到搜 索引擎中。而搜索引擎的使用方只能对数据进行查询,无法进 行修改和删除。
没有随机种子。在没有随机种子的情况下,rand.Intn 返回的伪随机数序列是固定的。
比如我们为了对去下游 的流量进行限制,在内存中堆积一些数据,并对堆积设定时 间/总量的阈值。在任意阈值达到之后将数据统一发送给下 游,以避免频繁的请求超出下游的承载能力而将下游打垮。这 种情况下重启要做到优雅就比较难了。所以我们的目标还是尽量避免采用或者绕过上线的方式,对线上程序做一些修改。比较典型的修改内容就是程序的配置项。
为了快速止损,最快且最有效 的办法就是进行版本管理,并支持按版本回滚。
使用 MySQL 来存储配置文件/字符串的不同版 本内容,在需要回滚时,只要进行简单的查询即可。
在给业务提供配置读取的 sdk 时,最好能够将拿到 的配置在业务机器的磁盘上也缓存一份。这样远程配置中心不 可用时,可以直接用硬盘上的内容来做兜底。当重新连接上配 置中心时,再把相应的内容进行更新。
加入缓存之后务必需要考虑的是数据一致性问题,当个别业务机器因为网络错误而与其它机器配置不一致时,我们也应该能够从监控系统中知晓。
分布式爬虫是一套任务分发和执行系统。而常见的任务分发,因为上下游存在速度不匹配问题,必然要借助消息队列。
如果希望在分布式的消费端进行 任务的负载均衡,而不是所有人都收到同样的消息,那么就要 给消费端指定相同的 queue 名字。