mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-24 04:22:22 +00:00
zk -> ZooKeeper
This commit is contained in:
parent
d5a3c92c9b
commit
7d2936a8fb
@ -226,7 +226,7 @@ setnx 很适合在高并发场景下,用来争抢一些“唯一”的资源
|
||||
|
||||
所以,我们需要依赖于这些请求到达 Redis 节点的顺序来做正确的抢锁操作。如果用户的网络环境比较差,那也只能自求多福了。
|
||||
|
||||
## 6.2.4 基于 zk
|
||||
## 6.2.4 基于 ZooKeeper
|
||||
|
||||
```go
|
||||
package main
|
||||
@ -257,7 +257,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
基于 zk 的锁与基于 Redis 的锁的不同之处在于 Lock 成功之前会一直阻塞,这与我们单机场景中的 mutex.Lock 很相似。
|
||||
基于ZooKeeper的锁与基于 Redis 的锁的不同之处在于 Lock 成功之前会一直阻塞,这与我们单机场景中的 mutex.Lock 很相似。
|
||||
|
||||
其原理也是基于临时 sequence 节点和 watch API,例如我们这里使用的是 `/lock` 节点。Lock 会在该节点下的节点列表中插入自己的值,只要节点下的子节点发生变化,就会通知所有 watch 该节点的程序。这时候程序会检查当前节点下最小的子节点的 id 是否与自己的一致。如果一致,说明加锁成功了。
|
||||
|
||||
@ -305,7 +305,7 @@ etcd 中没有像 ZooKeeper 那样的 sequence 节点。所以其锁实现和基
|
||||
3. watch `/lock` 下的事件,此时陷入阻塞
|
||||
4. 当 `/lock` 路径下发生事件时,当前进程被唤醒。检查发生的事件是否是删除事件(说明锁被持有者主动 unlock),或者过期事件(说明锁过期失效)。如果是的话,那么回到 1,走抢锁流程。
|
||||
|
||||
## 6.2.6 redlock
|
||||
## 6.2.6 Redlock
|
||||
|
||||
```go
|
||||
package main
|
||||
@ -365,20 +365,20 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
redlock 也是一种阻塞锁,单个节点操作对应的是 `set nx px` 命令,超过半数节点返回成功时,就认为加锁成功。
|
||||
Redlock也是一种阻塞锁,单个节点操作对应的是 `set nx px` 命令,超过半数节点返回成功时,就认为加锁成功。
|
||||
|
||||
关于 redlock 的设计曾经在社区引起一场口水战,分布式专家各抒己见。不过这个不是我们要讨论的内容,相关链接在参考资料中给出。
|
||||
关于Redlock设计曾经在社区引起一场口水战,分布式专家各抒己见。不过这个不是我们要讨论的内容,相关链接在参考资料中给出。
|
||||
|
||||
## 6.2.7 如何选择
|
||||
|
||||
业务还在单机就可以搞定的量级时,那么按照需求使用任意的单机锁方案就可以。
|
||||
|
||||
如果发展到了分布式服务阶段,但业务规模不大,比如 qps < 1000,使用哪种锁方案都差不多。如果公司内已有可以使用的 ZooKeeper/etcd/Redis 集群,那么就尽量在不引入新的技术栈的情况下满足业务需求。
|
||||
如果发展到了分布式服务阶段,但业务规模不大,比如 qps < 1000,使用哪种锁方案都差不多。如果公司内已有可以使用的 ZooKeeper、etcd或者Redis集群,那么就尽量在不引入新的技术栈的情况下满足业务需求。
|
||||
|
||||
业务发展到一定量级的话,就需要从多方面来考虑了。首先是你的锁是否在任何恶劣的条件下都不允许数据丢失,如果不允许,那么就不要使用 Redis 的 setnx 的简单锁。
|
||||
业务发展到一定量级的话,就需要从多方面来考虑了。首先是你的锁是否在任何恶劣的条件下都不允许数据丢失,如果不允许,那么就不要使用Redis的setnx的简单锁。
|
||||
|
||||
如果要使用 redlock,那么要考虑你们公司 Redis 的集群方案,是否可以直接把对应的 Redis 的实例的 ip+port 暴露给开发人员。如果不可以,那也没法用。
|
||||
如果要使用Redlock,那么要考虑你们公司Redis的集群方案,是否可以直接把对应的Redis的实例的ip+port暴露给开发人员。如果不可以,那也没法用。
|
||||
|
||||
对锁数据的可靠性要求极高的话,那只能使用 etcd 或者 zk 这种通过一致性协议保证数据可靠性的锁方案。但可靠的背面往往都是较低的吞吐量和较高的延迟。需要根据业务的量级对其进行压力测试,以确保分布式锁所使用的 etcd/zk 集群可以承受得住实际的业务请求压力。需要注意的是,etcd 和 zk 集群是没有办法通过增加节点来提高其性能的。要对其进行横向扩展,只能增加搭建多个集群来支持更多的请求。这会进一步提高对运维和监控的要求。多个集群可能需要引入 proxy,没有 proxy 那就需要业务去根据某个业务 id 来做 sharding。如果业务已经上线的情况下做扩展,还要考虑数据的动态迁移。这些都不是容易的事情。
|
||||
对锁数据的可靠性要求极高的话,那只能使用etcd或者ZooKeeper这种通过一致性协议保证数据可靠性的锁方案。但可靠的背面往往都是较低的吞吐量和较高的延迟。需要根据业务的量级对其进行压力测试,以确保分布式锁所使用的 etcd/ZooKeeper 集群可以承受得住实际的业务请求压力。需要注意的是,etcd 和 Zookeeper 集群是没有办法通过增加节点来提高其性能的。要对其进行横向扩展,只能增加搭建多个集群来支持更多的请求。这会进一步提高对运维和监控的要求。多个集群可能需要引入 proxy,没有 proxy 那就需要业务去根据某个业务 id 来做分片。如果业务已经上线的情况下做扩展,还要考虑数据的动态迁移。这些都不是容易的事情。
|
||||
|
||||
在选择具体的方案时,还是需要多加思考,对风险早做预估。
|
||||
|
@ -4,13 +4,13 @@
|
||||
|
||||
## 6.5.1 常见的负载均衡思路
|
||||
|
||||
如果我们不考虑均衡的话,现在有 n 个 endpoint,我们完成业务流程实际上只需要从这 n 个中挑出其中的一个。有几种思路:
|
||||
如果我们不考虑均衡的话,现在有n个endpoint,我们完成业务流程实际上只需要从这n个中挑出其中的一个。有几种思路:
|
||||
|
||||
1. 按顺序挑: 例如上次选了第一台,那么这次就选第二台,下次第三台,如果已经到了最后一台,那么下一次从第一台开始。这种情况下我们可以把 endpoint 都存储在数组中,每次请求完成下游之后,将一个索引后移即可。在移到尽头时再移回数组开头处。
|
||||
1. 按顺序挑: 例如上次选了第一台,那么这次就选第二台,下次第三台,如果已经到了最后一台,那么下一次从第一台开始。这种情况下我们可以把endpoint都存储在数组中,每次请求完成下游之后,将一个索引后移即可。在移到尽头时再移回数组开头处。
|
||||
|
||||
2. 随机挑一个: 每次都随机挑,真随机伪随机均可。假设选择第 x 台机器,那么 x 可描述为 `rand.Intn() % n`。
|
||||
2. 随机挑一个: 每次都随机挑,真随机伪随机均可。假设选择第 x 台机器,那么x可描述为 `rand.Intn() % n`。
|
||||
|
||||
3. 根据某种权重,对下游 endpoints 进行排序,选择权重最大/小的那一个。
|
||||
3. 根据某种权重,对下游endpoints进行排序,选择权重最大/小的那一个。
|
||||
|
||||
当然了,实际场景我们不可能无脑轮询或者无脑随机,如果对下游请求失败了,我们还需要某种机制来进行重试,如果纯粹的随机算法,存在一定的可能性使你在下一次仍然随机到这次的问题节点。
|
||||
|
||||
@ -71,13 +71,13 @@ func request(params map[string]interface{}) error {
|
||||
|
||||
真的没有问题么?实际上还是有问题的。这段简短的程序里有两个隐藏的隐患:
|
||||
|
||||
1. 没有随机种子。在没有随机种子的情况下,rand.Intn 返回的伪随机数序列是固定的。
|
||||
1. 没有随机种子。在没有随机种子的情况下,rand.Intn()返回的伪随机数序列是固定的。
|
||||
|
||||
2. 洗牌不均匀,会导致整个数组第一个节点有大概率被选中,并且多个节点的负载分布不均衡。
|
||||
|
||||
第一点比较简单,应该不用在这里给出证明了。关于第二点,我们可以用概率知识来简单证明一下。假设每次挑选都是真随机,我们假设第一个位置的 endpoint 在 len(slice) 次交换中都不被选中的概率是 ((6/7)*(6/7))^7 ≈ 0.34。而分布均匀的情况下,我们肯定希望被第一个元素在任意位置上分布的概率均等,所以其被随机选到的概率应该 ≈ 1/7 ≈ 0.14。
|
||||
|
||||
显然,这里给出的洗牌算法对于任意位置的元素来说,有 30% 的概率不对其进行交换操作。所以所有元素都倾向于留在原来的位置。因为我们每次对 shuffle 数组输入的都是同一个序列,所以第一个元素有更大的概率会被选中。在负载均衡的场景下,也就意味着 endpoints 数组中的第一台机器负载会比其它机器高不少(这里至少是 3 倍以上)。
|
||||
显然,这里给出的洗牌算法对于任意位置的元素来说,有 30% 的概率不对其进行交换操作。所以所有元素都倾向于留在原来的位置。因为我们每次对shuffle数组输入的都是同一个序列,所以第一个元素有更大的概率会被选中。在负载均衡的场景下,也就意味着endpoints数组中的第一台机器负载会比其它机器高不少(这里至少是3倍以上)。
|
||||
|
||||
### 6.5.2.2 修正洗牌算法
|
||||
|
||||
@ -104,21 +104,21 @@ func shuffle(n int) []int {
|
||||
|
||||
在当前的场景下,我们只要用 rand.Perm 就可以得到我们想要的索引数组了。
|
||||
|
||||
## 6.5.3 zk 集群的随机节点挑选问题
|
||||
## 6.5.3 ZooKeeper 集群的随机节点挑选问题
|
||||
|
||||
本节中的场景是从 N 个节点中选择一个节点发送请求,初始请求结束之后,后续的请求会重新对数组洗牌,所以每两个请求之间没有什么关联关系。因此我们上面的洗牌算法,理论上不初始化随机库的种子也是不会出什么问题的。
|
||||
|
||||
但在一些特殊的场景下,例如使用 zk 时,客户端初始化从多个服务节点中挑选一个节点后,是会向该节点建立长连接的。并且之后如果有请求,也都会发送到该节点去。直到该节点不可用,才会在 endpoints 列表中挑选下一个节点。在这种场景下,我们的初始连接节点选择就要求必须是“真”随机了。否则,所有客户端起动时,都会去连接同一个 zk 的实例,根本无法起到负载均衡的目的。如果在日常开发中,你的业务也是类似的场景,也务必考虑一下是否会发生类似的情况。为 rand 库设置种子的方法:
|
||||
但在一些特殊的场景下,例如使用ZooKeeper时,客户端初始化从多个服务节点中挑选一个节点后,是会向该节点建立长连接的。并且之后如果有请求,也都会发送到该节点去。直到该节点不可用,才会在 endpoints 列表中挑选下一个节点。在这种场景下,我们的初始连接节点选择就要求必须是“真”随机了。否则,所有客户端起动时,都会去连接同一个ZooKeeper的实例,根本无法起到负载均衡的目的。如果在日常开发中,你的业务也是类似的场景,也务必考虑一下是否会发生类似的情况。为rand库设置种子的方法:
|
||||
|
||||
```go
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
```
|
||||
|
||||
之所以会有上面这些结论,是因为某个使用较广泛的开源 zk 库的早期版本就犯了上述错误,直到 2016 年早些时候,这个问题才被修正。
|
||||
之所以会有上面这些结论,是因为某个使用较广泛的开源ZooKeeper库的早期版本就犯了上述错误,直到 2016 年早些时候,这个问题才被修正。
|
||||
|
||||
## 6.5.4 负载均衡算法效果验证
|
||||
|
||||
我们这里不考虑加权负载均衡的情况,既然名字是负载“均衡”。那么最重要的就是均衡。我们把开篇中的 shuffle 算法,和之后的 fisher yates 算法的结果进行简单地对比:
|
||||
我们这里不考虑加权负载均衡的情况,既然名字是负载“均衡”。那么最重要的就是均衡。我们把开篇中的shuffle算法,和之后的fisher yates算法的结果进行简单地对比:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
Loading…
x
Reference in New Issue
Block a user