WAL(Write Ahead Logging) 相关概念
WAL(Write ahead logging) 用于数据库系统中,提供原子性和持久性操作。
在一个使用 WAL 的数据库系统中,所有改动在应用之前都要先写入日志中,同时 redo 和 undo 信息也会保存在日志中。
就是一个简单的概念
WAL(Write ahead logging) 用于数据库系统中,提供原子性和持久性操作。
在一个使用 WAL 的数据库系统中,所有改动在应用之前都要先写入日志中,同时 redo 和 undo 信息也会保存在日志中。
就是一个简单的概念
在 golang 1.8 之后,对于支持的 http 服务支持 gracefull shutdown, 相比起之前的实现,golang 中的 server 在退出后没有做资源清理,在实现 gracefull shutdown 之后,只需要调用 shutdown 就可以实现
1 | func (srv *Server) Shutdown(ctx context.Context) error |
Shutdown 将无中断的关闭正在活跃的连接,然后平滑的停止服务。处理流程如下
Context, 将在服务关闭前返回 Context 的超时错误这样在运行 server 时,ctrl + c 后,server 会先释放其所占有的资源,比如使用的端口号,占用的连接,然后才会关闭。而在之前,关闭后这些资源仍然占用,直到 OS 将其释放。
基本就是抄自 golang blog 了
数据竞争是所有并发程序都需要注意的问题,尤其是像 golang 这种原生支持并发的程序.
在 golang 发行到 1.1 版本时自带 race detector, 用来找到 go 代码中的数据竞争状态。
race detector 基于 google 开源的 C/C++ 库 ThreadSanitizer runtime library, 在集成到 golang 中后,就发现了标准库中 42 处竞争状态。
race detector 集成到了 golang 的工具链中。在命令行使用 -race 选项,编译器就会记录代码中所有的 memory access, 包括访问方式和时间。运行时库观察所有对共享变量的未同步访问,每当发现这样的行为,就会输出一个 warning, 具体实现细节可见 链接
因为依赖于运行时库的设计,race detector 只能在运行时做数据竞争状态的检查。但是在开启 race detector 后,cpu 的负载将为原来不启用 race detector 的 10 倍。所以不可能在线上应用中启用 race detector. 但是在生产环境中使用的一种情况是,在许多线上的 servers 中,只有一个 server 的应用启用了 race detector, 对业务影响不大。
race detector 集成到了 golang 中的工具链中,只要命令行中设置 -race 选项就可以使用 race detector
1 | go test -race |
1 | func main() { |
在这段代码看不出来有什么数据竞争带代码,运行起来也似乎没什么问题,但是在极偶尔的情况下第 6 行代码会出现 nil 指针引用的情况,打开 race detector 发现在 4 行代码的调用 time.sleep 会开启一个 goroutine 来执行传入的 func, func 闭包对 main 中的 t 进行了读写,但是 t 的赋值是在 main 中完成的,不一定是在 goroutine 开启之前,所以可能会发生 nil 指针引用的情况。
这段代码要修正把对 t 的读写全部放在 main 中就好。
kafka 是用于日志处理的分布式消息队列,同时支持离线和在线日志处理。
kafka 在保存消息时根据 topic 进行分类,消息发送者为 producer, 消息接受者为 consumer, 此外 kafka 集群有多个 kafka 实例组成,每个实例称为 broker.无论是 kafka 集群,还是 producer 和 consumer 都依赖于 zookeeper 来保证系统可用性,为集群保存一些 meta 信息。
记下几个常用的术语
总的来说,生产者通过网络发布消息到 kafka 集群,kafka 集群将这些消息提供给消费者。

客户端与服务端通过 TCP 通信,kafka 有 java 客户端,但是许多语言也可以使用
一个 topic 可以认为是一类消息,每个 topic 将分为多个 partition, 每个 partition 在存储层面是 append log 文件。任何发布到此 partition 的消息都会被直接追加到 log 文件的尾部,每条消息在文件中的位置成为 offset(偏移量), offset 为一个 long 型数字,是唯一标记一条信息。kafka 并没有提供其他额外的索引机制来存储 offser, 因为 kafka 几乎不允许对消息进行读和写
一个主题就是消息的类别或者名称,对于每个主题,kafka 集群都管理者一个被分区的日志
一个分区就是一个提交日志,每个分区上保留着不断被追加的消息,这些消息时有序的且顺序不可改变。
每个消息都分配了一个序列号 offset, offset 唯一标识了分区上的信息。
在 kafka 中,及时消息被消费,消息也不会被立刻删除。日志文件会根据 broker 中的配置要求,保留一定的时间之后删除,比如 log 文件保留 2 天,那么两天之后,文件会被清除,无论其中的消息是否被消费。kafka 通过这种简单的手段,来释放磁盘空间,以及减少消息消费之后对文件内容改动的 IO 开销。
对于 consumer 而言,需要保存消费信息的 offset, 由 consumer 管理 offset 的保存和使用。当consumer 正常消费时,offset 将会线性的向前驱动,即消息将依次顺序被消费。事实上 consumer 可以使用任意顺序消费信息,只需要将 offset 重置为任意值。
kafka 集群不需要维护任何 consumer 和 producer 状态信息,这些信息由 zookeeper 保存,因此 producer 和 consumer 的客户端实现非常轻量级,它们可以随时离开,而不会对集群造成额外影响。
partitions 的设计目的有多个。最根本的原因是 kafka 基于文件存储。通过分区,可以将日志内容分散到多个 service 上,避免文件尺寸达到单机磁盘上限,每个 partition 都会被当前 server(kafka 实例) 保存,可以将一个 topic 切分多任意多个 partitions 来保存小心。此外越多的 partitions 意味着可以容纳更多的 consumer, 可有效提升并发消费的能力。
对日志进行分区主要有以下几个目的
一个 topic 的多个 partitions 分布在 kafka 集群中的多个 server 上,每个 server 负责 partitions 中消息的读写操作;此外 kafka 还可以配置 partitions 需要备份的个数(replicas), 每个 partition 将会被分到多台机器上,以提高可用性
基于 replicated (冗余) 方案,那么就意味着需要对多个备份进行调度,每个 partition 都有一个机器为 leader, 零个或者多个机器作为 follower, leader 负责所有的读写操作,follow 执行 leader 的指令。如果 leader 失效,那么将会有其他 follower 来接管(成为新的 leader), follower 只是和 leader 跟进,同步消息即可。由此可见作为 leader 的 server 承载了全部的请求压力,因此从集群的整体考虑,有多少个 partitions 就会有多少个 leader, kafka 会将 leader 均衡的分散在每个实例上,来确保整体的性能稳定。
producer 将消息发送到指定的 topic 中,同时 producer 也能决定将此消息归属于哪个 partition
传统的消息传递有两种方式
消费者都属于一个消费组,每个消费者中可以有多个消费者。
发送到 topic 中的消息,只会被订阅此 topic 的消费组中的一个消费者消费。如果所有的消费者都有相同的消费组,这种情况近似于 queue 模式。
消息在 consumer 之间负载均衡,如果所有 consumer 都有不同的 group, 那就是纯粹的发布 - 订阅模式,消息会广播给所有的消费者
在 kafka 中,一个 partition 中的消息只会被 group 中的一个 conusmer 消费;每个 group 中 consumer 消息消费相互独立。
可以认为一个 group 是一个订阅者,一个 topic 中的每个 partitions, 只会被一个订阅者中的一个 consumer 消费,不过一个 consumer 可以消费多个 partition 中的消息。kafka 只能保证一个 partition 中的消息被某个 consumer 消费时,消息是有序的。
从 topic 角度来说,消息仍不是有序的。 kafka 的设计原理决定,对于一个 topic, 通一个 group 中不能有多于 partitions 个数的 consumer 同时消费,否则意味着某些 consumer 将无法得到信息。
producer 发送的消息按照他们发送的顺序追加到主题
消费者看到消息的顺序就是消息在 log 中的存储的顺序
kafka 与传统的消息系统相比,有以下不同
zookeeper 用于解决分布式系统中数据一致性问题,因为所有的分布式系统都会面临这一问题。
zookeeper 由 yahoo 开发,在开发 hadoop 时将这一问题抽象出来,提出了一个独立和通用的解决方案,就是 zookeeper.
zookeeper 服务集群一般由大于等于 3 的单数个服务器构成,在每台服务器内存中维护类似于文件系统的树形数据结构,其中一台作为主控节点,其余作为从属节点。写操作只能由主控节点响应,读可由任务节点响应,如果主控节点宕机,则其余节点可以再选举出新的主控节点
由于主控节点只有一个,所以在写操作上天然存在瓶颈,所以更适合读多写少的应用场景。


消息队列(message queue), 是分布式系统中重要的组件,其通用的使用场景可以简单的描述为
当不需要立即获得结果,但是并发量又需要进行控制
消息队列主要解决了应用耦合,异步处理,流量削峰等问题。
目前较为使用广泛的消息队列有 RabbitMQ, RocketMQ, ActiveMQ, Kafka, ZeroMQ, MetaMq 等。部分数据库 redis, Mysql 以及 phxsql 也可以实现消息队列的功能。
消息队列在实际应用中包括如下几个场景
具体例如当用户上传一张图片,服务器要对其标识人脸,一般是上传后由上传系统调用人脸识别 API, 但是在这个场景里面
但是如果这里使用消息队列的话,比如上传系统将图片信息批次写入消息队列,直接返回,而人脸识别 API 则定时从消息队列取任务,完成对新增图片的识别。
那么此时上传系统就与人脸系别解耦,图片上传成功与否则与人脸识别 API 无关
通常用于瞬时流量大的场景,比如秒杀,抢购等活动。此时由于瞬时访问量过大,服务器接受过大,导致流量暴增,可能会导致系统崩溃,而加入消息队列后,系统可以从消息队列中取数据,相当于消息队列做了一次缓冲。
消息队列包含两种模式,点对点模式(point to point, queue) 和发布/订阅模式(publish, subscribe, topic)
点对点模式包括以下三个角色
消息发送者生产消息发送到 queue 中,消费者从 queue 中取出并且消费消息。消息被消费之后, queue 中不再存储,所以消息接受者不可能消费到已经被消费的消息
点对点模式特点
发布/定阅模式包含以下三个角色
发布者将消息发布到 topic, 系统将这些消息传递给多个订阅者
发布/订阅模式特点
之间对比可见下图,原图来自腾讯

简述下 redis 中 pipeline 和 mget 的区别和优劣
pipeline 是将多个命令一次性写入到服务器,然后等待服务器返回结果,他可以同时执行多个命令,但是各个命令之间不能有数据依赖,因为 pipeline 执行命令式不能保证执行顺序与写入时的命令顺序相同。
因为 redis 中命令是基于 request / response 模式,所以当要连续执行多个命令时,主要的时延在于 RTT, 而且还要频繁调用系统 i/o 接口,所以就有了 pipeline 将多个命令同时一次性写到服务器。
需要多个命令及时提交,而且命令之间没有数据依赖的时候,适合用 pipeline, 可以较大的提升性能
1 | mset a "a1" b "b1" c "c1" |
mget 和 mset 也是为了减少网络连接和传输时间设置的,而且当 key 的数目较多时, mget 和 mset 性能要高于 pipeline, 但是 mget 和 mset 也仅限与同时对多个 key 进行操作。
kitool 工具生成的如下文件和目录
kite.goserver.gohandler.gobuild.shthrfit_genclientsconfscriptCRDT (Conflict-free Replicated Data Types) 直译的话即 冲突避免可复制数据类型
在研究分布式系统时,尤其是要实现最终一致性分布式系统的过程中,一个最基本的问题就是,应该采用什么样的数据结构保证最终一致性,目前关于这个问题有一个讨论较为详尽的论文
在分布式系统中,CRDT 是指一种能够无需合作就可以在网络中多个主机中并行地复制的一种数据结构,并且总能够解决可能的不一致性。
有两种 CRDT 都可以实现数据的最终一致性
这两种 CRDT 在数学上等价的,可以相互转换。
基于操作的 CRDT 需要消息中间件的提供额外的支持,对操作命名并保证通信过程中不会丢失或者交递信息时保证消息唯一。
基于状态的消息中间件则需要消息通信时保证全部的状态都完好地交递。
基于操作的 CRDT 又被称作 commutative replicated data types 或者 CmRDTs.
CmRDT 在传播时只包含数据更新操作信息。
举个栗子,一个整数在执行了(+10), (-20) 操作后,CmRDT 广播时则只包含关于这个整数进行了(+10), (-20) 操作。其他的 DC 收到这个 CmRDT 后会在本地对相应的数据执行 CmRDT 中包含的操作。
CmRDT 所能携带的操作必须是可交换的,通信模块必须保证所有 CmRDT 包都能被正确交递,但是顺序无需保证。
基于状态的 CRDTs 全称为 convergent replicated data types 或者 CvRDTs.
CvRDTs 会将本地全部的数据状态传播给其他 DC, 这些状态在接受到后会被一个函数做 merge 处理,所以这些状态必须是可交换的,关联的,幂等的。
merge 函数会为在 CvRDT 之间提供一个 join 操作,将接收到 CvRDT 与本地数据合并。
目前 CRDT 有以下几种常见的实现
这是一个只增不减的计数器,对于 N 个节点,每个节点上维护一个长度为 N 的向量
该向量表示节点 m 上的计数,当需要增加这个计数器时,只需要任意选择一个节点操作,操作会将对应节点的计数器
计算伪代码如下
1 | payload integer[n] p |
G-Counter 有一个限制,即计数器只能增加,不能减少。不过可以使用两个计数器来实现一个既能增加也能减少计数器(PN-Counter). 简单来说,就是用一个 G-Counter 来记录所有累加结果,另一个 G-Counter 来记录累减结果,查询当前结果时,只需要计算两个 G-Counter 的差即可
1 | payload integer[n] P, integer[n] N |
LWW-Element-Set 包含一个 add set, remove set, 在每个元素上都有一个时间戳.
每个元素都在添加到 LWW-Element-Set 时都会添加到 add-set 中,并且附带一个时间戳。将元素从 LWW-Element-Set 中移除也会添加到 remove set 中一行,并且附带一个时间戳。
当一个元素在 add-set 中时,他就会认为是 LWW-Element-Set 中的成员,或者在 remove-set 中时,就不是 LWW-Element-Set 的成员。但是如果两者都在,且 remove-set 中的时间戳小于 add-set 中的时间戳,也认为在数据库中,相反则不在。
合并两个 LWW-Element-Set 需要求两个数据库的 add-set 和 remove-set 中的并集,并且只保留时间戳最新的即可,如果时间戳相等,就需要引入 LWW-Element-Set 中的 bias.
服务发现中分为三个角色
可以是一个 HTTP 服务器,提供 API 服务,有一个 IP 端口作为服务地址
可以是 client 或者其他服务,需要服务提供者提供的服务
从使用上来说就是一个字典,字典里有很多 key/value 键值对,key 是服务名称,value 是服务提供者的地址列表。服务注册就是调用字典的 Put 方法添加 k/v 对,服务查找就是调用字典的 Get 方法.
当服务提供者节点不可用时,希望服务中介及时注销该服务,并且通知消费者重新获取服务地址
当服务提供者新加入时,要求服务中介能及时告知服务消费者,有新的服务可用。
目前较为通用的服务发现使用的开源框架有 consul, 现在学习下其实现原理

这张图来自 consul 官网。
首先 Consul 支持多数据中心,在上图有两个 DataCenter ,之间通过 Internet 互联,同时为了提高通信效率,只有 Server 节点才加入跨数据中心的通信。
在单个 DC 中,Consul 分为 Client 和 Server 两种节点
Server 节点有一个 Leader 和多个 Follower, Leader 节点会将数据同步到 Follower, Server 的数量推荐是 3 个或者 5 个,在 Leader 挂掉之后会启动选举机制产生一个新的 Leader.
DC 内的 Consul 节点通过 gossip 协议(流言协议)维护成员关系,也就是某个节点了解集群内现在还有哪些节点,这些节点是 Client 还是 Server. 单个数据中心的流言协议同时使用 TCP 和 UDP 通信,并且都是用 8301 端口。跨数据中心的流言协议也同时使用 TCP 和 UDP 通信,端口使用 8302.
流言协议由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机选择周围几个节点散播消息,收到消息的节点也会重复该过程,直到最终网络中所有节点都收到了消息,这个过程需要一定时间,但是理论上最终所有节点都会收到消息,因此是一个最终一致性协议。
演示如下

服务发现就是在服务中介中添加一行 set 结构存储服务的 IP:PORT 字符串。如果服务提供者加入,将服务地址添加到服务中介的数据库中,如果服务挂掉,则将其服务地址移除。
以上的方式并不健壮,存在以下问题
此时需要引入服务包活和检查机制,并更换数据结构。服务提供者需要每隔 5s 左右向服务中介汇报存活,服务中介记录服务地址和汇报时间。服务中介每隔 10s 检查 zset 数据结构,踢掉汇报时间严重落后的服务地址项。这样就可以准实时地保证服务列表中服务地址的有效性,
有两种解决方案,第一种是轮询,第二种是 pubsub.
消费者需要每隔几秒查询服务列表是否有改变。如果服务很多,服务列表很大,消费者很多,redis 会有一定一定压力。
所以可以引入服务列表的版本号机制,给每个服务提供一个 key / value 设置服务的版本号,就是在服务列表发生变动时,递增版本号。消费者只需要轮询这个版本号的变动即可知道服务列表是否发生了变化。
这种方式及时性要明显好于轮询。缺点是每个 pubsub 都会占用消费者一个线程和一个额外的 redis 连接。为了减少对线程和连接的浪费,我们使用单个 pubsub 广播全局版本号的变动。全局版本号变动即任意服务列表发生了变动,这个版本号都会递增。接收到版本变动的消费者再去检查各自的依赖服务列表的版本号是否发生了变动。
为了避免服务中介在单机情况下的宕机,目前流行的服务发现系统都是使用分布式数据库实现 zookeeper/etcd/consul 等来作为服务中介,这几种分布式数据库是多节点的,一个节点宕机仍能保证整个服务中介的正常运行。