manout's blog

Something about me

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

race detector 集成到了 golang 中的工具链中,只要命令行中设置 -race 选项就可以使用 race detector

1
2
3
4
go test -race
go run -race
go build
go install -race pkg

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
start := time.Now()
var t *time.Timer
t = time.AfterFunc(randomDuration(), func() {
fmt.Println(time.Now().Sub(start))
t.Reset(randomDuration())
})
time.Sleep(5 * time.Second)
}

func randomDurtion() time.Duration {
return time.Duration(rand.Int63n(1e9))
}

在这段代码看不出来有什么数据竞争带代码,运行起来也似乎没什么问题,但是在极偶尔的情况下第 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 维护消息类别的方式是主题(topic)
  • 称发布消息到 kafka 主题的进程为生产者(producer)
  • 订阅主题,获取消息的进程叫消费者(consumer)
  • kafka 是由多个服务器组成的机器,每个服务器称作代理

总的来说,生产者通过网络发布消息到 kafka 集群,kafka 集群将这些消息提供给消费者。

kafka_service

客户端与服务端通过 TCP 通信,kafka 有 java 客户端,但是许多语言也可以使用

topic 和 logs

一个 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 均衡的分散在每个实例上,来确保整体的性能稳定。

  • 发送到 partitions 中的消息将会按照接受的顺序追加到日志
  • 对于消费者而言,消费信息的顺序与日志中的消息顺序一致
  • 如果 topic 的 replicationfactor (备份因子) 为 N, 那么允许 N - 1 个 kafka 实例失效

生产者

producer 将消息发送到指定的 topic 中,同时 producer 也能决定将此消息归属于哪个 partition

消费者

传统的消息传递有两种方式

  • 队列
    一组消费者从机器上读消息,每个消息只传递给这组消费者中的一个
  • 发布 - 订阅
    消息广播到所有消费者,kafka 提供了一个消费组方式

消费者都属于一个消费组,每个消费者中可以有多个消费者。
发送到 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 将无法得到信息。

简述

  • kafka 将主题下的分区分配给消费组里的消费者,每个分区被一个消费者消费
  • 消费者的数量不能超过分区数
  • kafka 只能保证分区内的消息是有序的
  • 如果需要要消息是全局有序的,可以设置 topic 只有一个 partitions, 但也意味着只能有一个 consumer

producer 发送的消息按照他们发送的顺序追加到主题
消费者看到消息的顺序就是消息在 log 中的存储的顺序

kafka 与传统的消息系统相比,有以下不同

  • kafka 被设计为一个分布式系统,易于向外扩展
  • 同时为发布和订阅提供高吞吐量
  • 支持多订阅者,当失败时能自动平衡消费者
  • 将消息持久化到磁盘,因此可用于批量消费,以及实时应用程序

zookeeper 用于解决分布式系统中数据一致性问题,因为所有的分布式系统都会面临这一问题。
zookeeper 由 yahoo 开发,在开发 hadoop 时将这一问题抽象出来,提出了一个独立和通用的解决方案,就是 zookeeper.

设计目标

  • 类似于标准文件系统,各个进程之间的协调通过共享分级命名空间,目录或者文件被称为 znodes, 这些数据都会被保存到 zk 的实例内存中
  • 以集群形式提供服务
  • 有序性
  • 高性能
  • 极简 API, 一共 7 个(create, delete, exist, get data, set data, get children, sync)

结构

zookeeper 服务集群一般由大于等于 3 的单数个服务器构成,在每台服务器内存中维护类似于文件系统的树形数据结构,其中一台作为主控节点,其余作为从属节点。写操作只能由主控节点响应,读可由任务节点响应,如果主控节点宕机,则其余节点可以再选举出新的主控节点
由于主控节点只有一个,所以在写操作上天然存在瓶颈,所以更适合读多写少的应用场景。

集群结构

cluster_structure

节点模型

node_model

主要应用场景

  • 分布式锁
  • 服务zhuce
  • 配置管理
  • 名称服务
  • leader 选举

消息队列(message queue), 是分布式系统中重要的组件,其通用的使用场景可以简单的描述为

当不需要立即获得结果,但是并发量又需要进行控制

消息队列主要解决了应用耦合,异步处理,流量削峰等问题。
目前较为使用广泛的消息队列有 RabbitMQ, RocketMQ, ActiveMQ, Kafka, ZeroMQ, MetaMq 等。部分数据库 redis, Mysql 以及 phxsql 也可以实现消息队列的功能。

使用场景

消息队列在实际应用中包括如下几个场景

  • 应用耦合
    多个应用通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程失败
  • 异步处理
    多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,减少处理时延
  • 限流削峰
    广泛应用于瞬时高流量场景,避免流量过大导致应用系统挂掉的情况
  • 消息驱动
    系统分为消息队列,消息生产者,消息消费者,生产者负责产生消息,消费者(多个)负责对消息进行处理

应用解耦合

具体例如当用户上传一张图片,服务器要对其标识人脸,一般是上传后由上传系统调用人脸识别 API, 但是在这个场景里面

  • 如果人脸识别 API 调用失败,那么图片上传也会失败
  • 用户其实不需要立即直到人脸识别结果
  • 图片上传系统与人脸识别系统之间互相调用,耦合度高

但是如果这里使用消息队列的话,比如上传系统将图片信息批次写入消息队列,直接返回,而人脸识别 API 则定时从消息队列取任务,完成对新增图片的识别。
那么此时上传系统就与人脸系别解耦,图片上传成功与否则与人脸识别 API 无关

限流削峰

通常用于瞬时流量大的场景,比如秒杀,抢购等活动。此时由于瞬时访问量过大,服务器接受过大,导致流量暴增,可能会导致系统崩溃,而加入消息队列后,系统可以从消息队列中取数据,相当于消息队列做了一次缓冲。

  • 请求先入消息队列,而不是由业务系统直接处理,减少了业务系统压力
  • 队列长度可以做限制,比如抢购活动只有 100 个名额,那么 100 之后请求就可以直接抛弃,返回活动已结束或者商品已售完等消息

消息队列的两种模式

消息队列包含两种模式,点对点模式(point to point, queue) 和发布/订阅模式(publish, subscribe, topic)

点对点模式

点对点模式包括以下三个角色

  • 消息队列
  • 生产者
  • 消费者

消息发送者生产消息发送到 queue 中,消费者从 queue 中取出并且消费消息。消息被消费之后, queue 中不再存储,所以消息接受者不可能消费到已经被消费的消息

点对点模式特点

  • 每个消息只有一个消费者,即消息不能重复消费
  • 生产者和消费者之间没有依赖关系,即消费者的正常与否并不影响发送者的功能
  • 消费者在成功接受消息后需向队列应答成功,方便消息队列删除当前接受的消息。

发布/订阅模式

发布/定阅模式包含以下三个角色

  • 角色主题(topic)
  • 发布者(publisher)
  • 订阅者(subscriber)

发布者将消息发布到 topic, 系统将这些消息传递给多个订阅者

发布/订阅模式特点

  • 每个消息可以有多个订阅者
  • 发布者和订阅者之间有时间上的依赖性,针对某个主题(topic) 的订阅者,必须创建一个订阅者之后,才能消费发布者的信息
  • 为了消费信息,订阅者需要订阅该角色主题,并保持在线运行

常用消息队列

  • RabbitMQ
  • ActiveMQ
  • RocketMQ
  • kafka

之间对比可见下图,原图来自腾讯

message_queue

简述下 redis 中 pipeline 和 mget 的区别和优劣

pipeline

pipeline 是将多个命令一次性写入到服务器,然后等待服务器返回结果,他可以同时执行多个命令,但是各个命令之间不能有数据依赖,因为 pipeline 执行命令式不能保证执行顺序与写入时的命令顺序相同。

因为 redis 中命令是基于 request / response 模式,所以当要连续执行多个命令时,主要的时延在于 RTT, 而且还要频繁调用系统 i/o 接口,所以就有了 pipeline 将多个命令同时一次性写到服务器。

适用场景

需要多个命令及时提交,而且命令之间没有数据依赖的时候,适合用 pipeline, 可以较大的提升性能

注意

  • pipeline 独占一个连接,所以管道内命令太多,可能会请求超时
  • 可发送命令数量受 client 端缓冲区大小限制
  • redis server 存在 query buffer 限制
  • redis server 存在 output buffer 限制
  • 需要 server 和 client 的共同支持才能实现 pipeline
  • redis cluster 不建议使用 pipeline, 容易产生 max redirect 错误
  • twem proxy 可以支持 pipeline

mget 和 mset

1
2
mset a "a1" b "b1" c "c1"
mget a b c

mget 和 mset 也是为了减少网络连接和传输时间设置的,而且当 key 的数目较多时, mget 和 mset 性能要高于 pipeline, 但是 mget 和 mset 也仅限与同时对多个 key 进行操作。

kitool 工具生成的如下文件和目录

  • kite.go
    生成工具生成,不可以改动
  • server.go
    可以根据需要改动
  • handler.go
    应该由开发者实现内部对应的方法逻辑
  • build.sh
    编译打包的辅助文件,一般不需要修改
  • thrfit_gen
    thrift 生成的 go 代码
  • clients
    生成的客户端代码
  • conf
    项目的基础配置文件,根据需要修改
  • script
    上线运行必须辅助文件,一般不需要修改

背景

CRDT (Conflict-free Replicated Data Types) 直译的话即 冲突避免可复制数据类型

在研究分布式系统时,尤其是要实现最终一致性分布式系统的过程中,一个最基本的问题就是,应该采用什么样的数据结构保证最终一致性,目前关于这个问题有一个讨论较为详尽的论文

CRDT 简介

在分布式系统中,CRDT 是指一种能够无需合作就可以在网络中多个主机中并行地复制的一种数据结构,并且总能够解决可能的不一致性。

CRDT 的类型

有两种 CRDT 都可以实现数据的最终一致性

  • 基于操作的 CRDT
  • 基于状态的 CRDT

这两种 CRDT 在数学上等价的,可以相互转换。
基于操作的 CRDT 需要消息中间件的提供额外的支持,对操作命名并保证通信过程中不会丢失或者交递信息时保证消息唯一。
基于状态的消息中间件则需要消息通信时保证全部的状态都完好地交递。

基于操作的 CRDT

基于操作的 CRDT 又被称作 commutative replicated data types 或者 CmRDTs.
CmRDT 在传播时只包含数据更新操作信息。
举个栗子,一个整数在执行了(+10), (-20) 操作后,CmRDT 广播时则只包含关于这个整数进行了(+10), (-20) 操作。其他的 DC 收到这个 CmRDT 后会在本地对相应的数据执行 CmRDT 中包含的操作。
CmRDT 所能携带的操作必须是可交换的,通信模块必须保证所有 CmRDT 包都能被正确交递,但是顺序无需保证。

基于状态的 CRDTs

基于状态的 CRDTs 全称为 convergent replicated data types 或者 CvRDTs.
CvRDTs 会将本地全部的数据状态传播给其他 DC, 这些状态在接受到后会被一个函数做 merge 处理,所以这些状态必须是可交换的,关联的,幂等的。
merge 函数会为在 CvRDT 之间提供一个 join 操作,将接收到 CvRDT 与本地数据合并。

CRDT 的几种实现

目前 CRDT 有以下几种常见的实现

  • G-Counter(Grow-Only Set)
  • PN-Counter(Positive-Negative Counter)
  • G-Set(Grow-only Set)
  • 2P-Set(Two-Phase Set)
  • LWW-Element-Set(Last Write Wins element Set)
  • OR-Set(Observed-Removed Set)
  • Sequence CRDTs

G-Counter(Grow-only Counter)

这是一个只增不减的计数器,对于 N 个节点,每个节点上维护一个长度为 N 的向量
该向量表示节点 m 上的计数,当需要增加这个计数器时,只需要任意选择一个节点操作,操作会将对应节点的计数器 . 当要统计整个集群的计数器总和时,只需要对向量 V 中的所有元素求和即可。

计算伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
payload integer[n] p
initial [0, 0, ..., 0]
update increment()
let g = myId()
P[g] := P[g] + 1
query value(): integer v
let v = sum(p)
compare (X, Y): boolean b
let b = i if Y.p[i] >= X.p[i]
merge (X, Y): payload Z
for i = 0, 1, 2, ..., n - 1:
let Z.p[i] = max(X.p[i], Y.p[i])

PN-Counter(Positive-Negative Counter)

G-Counter 有一个限制,即计数器只能增加,不能减少。不过可以使用两个计数器来实现一个既能增加也能减少计数器(PN-Counter). 简单来说,就是用一个 G-Counter 来记录所有累加结果,另一个 G-Counter 来记录累减结果,查询当前结果时,只需要计算两个 G-Counter 的差即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
payload integer[n] P, integer[n] N
initial [0, 0, 0, ..., 0], [0, 0, 0, ..., 0]
update increment()
let g = myID()
P(g) := P(g) + 1
update decrement()
let g = myID()
N(g) := N(g) + 1
query value(): integer v
let v = sum(P) - sum(N)
merge (X, Y): payload Z
for i = 0, 1, ..., n - 1:
Z.p[i] = max(X.p[i], Y.p[i])
Z.n[i] = max(X.n[i], Y.n[i])

LWW-Element-Set(last-Write-Wins-Element-Set)

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-setremove-set 中的并集,并且只保留时间戳最新的即可,如果时间戳相等,就需要引入 LWW-Element-Set 中的 bias.

服务发现

服务发现中分为三个角色

  • 服务提供者
    将自己能提供的服务注册到服务中介
  • 服务消费者
    从服务中介查找自己需要的服务的地址
  • 服务中介
    提供多个服务,每个服务对应多个服务提供者

服务提供者

可以是一个 HTTP 服务器,提供 API 服务,有一个 IP 端口作为服务地址

服务消费者

可以是 client 或者其他服务,需要服务提供者提供的服务

服务中介

从使用上来说就是一个字典,字典里有很多 key/value 键值对,key 是服务名称,value 是服务提供者的地址列表。服务注册就是调用字典的 Put 方法添加 k/v 对,服务查找就是调用字典的 Get 方法.
当服务提供者节点不可用时,希望服务中介及时注销该服务,并且通知消费者重新获取服务地址
当服务提供者新加入时,要求服务中介能及时告知服务消费者,有新的服务可用。

服务发现的实现

目前较为通用的服务发现使用的开源框架有 consul, 现在学习下其实现原理

consul

这张图来自 consul 官网。
首先 Consul 支持多数据中心,在上图有两个 DataCenter ,之间通过 Internet 互联,同时为了提高通信效率,只有 Server 节点才加入跨数据中心的通信。

在单个 DC 中,Consul 分为 Client 和 Server 两种节点

  • Server
    负责保存数据
  • Client
    负责健康检查及转发数据请求到 Server

Server 节点有一个 Leader 和多个 Follower, Leader 节点会将数据同步到 Follower, Server 的数量推荐是 3 个或者 5 个,在 Leader 挂掉之后会启动选举机制产生一个新的 Leader.

gossip 协议

DC 内的 Consul 节点通过 gossip 协议(流言协议)维护成员关系,也就是某个节点了解集群内现在还有哪些节点,这些节点是 Client 还是 Server. 单个数据中心的流言协议同时使用 TCP 和 UDP 通信,并且都是用 8301 端口。跨数据中心的流言协议也同时使用 TCP 和 UDP 通信,端口使用 8302.

流言协议由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机选择周围几个节点散播消息,收到消息的节点也会重复该过程,直到最终网络中所有节点都收到了消息,这个过程需要一定时间,但是理论上最终所有节点都会收到消息,因此是一个最终一致性协议。

演示如下

gossip

服务发现原理

服务发现就是在服务中介中添加一行 set 结构存储服务的 IP:PORT 字符串。如果服务提供者加入,将服务地址添加到服务中介的数据库中,如果服务挂掉,则将其服务地址移除。
以上的方式并不健壮,存在以下问题

  • 服务提供者进程被暴力杀死
  • 服务列表变动时如何通知消费者
  • 服务中介如果宕机

服务提供进程被杀死

此时需要引入服务包活和检查机制,并更换数据结构。服务提供者需要每隔 5s 左右向服务中介汇报存活,服务中介记录服务地址和汇报时间。服务中介每隔 10s 检查 zset 数据结构,踢掉汇报时间严重落后的服务地址项。这样就可以准实时地保证服务列表中服务地址的有效性,

服务列表变动

有两种解决方案,第一种是轮询,第二种是 pubsub.

轮询

消费者需要每隔几秒查询服务列表是否有改变。如果服务很多,服务列表很大,消费者很多,redis 会有一定一定压力。
所以可以引入服务列表的版本号机制,给每个服务提供一个 key / value 设置服务的版本号,就是在服务列表发生变动时,递增版本号。消费者只需要轮询这个版本号的变动即可知道服务列表是否发生了变化。

pubsub

这种方式及时性要明显好于轮询。缺点是每个 pubsub 都会占用消费者一个线程和一个额外的 redis 连接。为了减少对线程和连接的浪费,我们使用单个 pubsub 广播全局版本号的变动。全局版本号变动即任意服务列表发生了变动,这个版本号都会递增。接收到版本变动的消费者再去检查各自的依赖服务列表的版本号是否发生了变动。

服务中介宕机

为了避免服务中介在单机情况下的宕机,目前流行的服务发现系统都是使用分布式数据库实现 zookeeper/etcd/consul 等来作为服务中介,这几种分布式数据库是多节点的,一个节点宕机仍能保证整个服务中介的正常运行。

0%