Jitter Before Starting
go-micro
的 registry
缓存层针对每个服务监听注册中心事件的代码如下:
1// run starts the cache watcher loop
2// it creates a new watcher if there's a problem.
3func (c *cache) run(service string) {
4 c.Lock()
5 c.watchedRunning[service] = true
6 c.Unlock()
7 // reset watcher on exit
8 defer func() {
9 c.Lock()
10 c.watched = make(map[string]bool)
11 c.watchedRunning[service] = false
12 c.Unlock()
13 }()
14
15 var a, b int
16
17 for {
18 // exit early if already dead
19 if c.quit() {
20 return
21 }
22
23 // jitter before starting
24 j := rand.Int63n(100)
25 time.Sleep(time.Duration(j) * time.Millisecond)
26
27 // create new watcher
28 w, err := c.Registry.Watch(registry.WatchService(service))
29 if err != nil {
30 if c.quit() {
31 return
32 }
33
34 d := backoff(a)
35 c.setStatus(err)
36
37 if a > 3 {
38 a = 0
39 }
40
41 time.Sleep(d)
42 a++
43
44 continue
45 }
46
47 // reset a
48 a = 0
49
50 // watch for events
51 if err := c.watch(w); err != nil {
52 if c.quit() {
53 return
54 }
55
56 d := backoff(b)
57 c.setStatus(err)
58
59 if b > 3 {
60 b = 0
61 }
62
63 time.Sleep(d)
64 b++
65
66 continue
67 }
68
69 // reset b
70 b = 0
71 }
72}
在分布式系统或并发编程中,“jitter before starting”的作用是通过引入随机延迟来避免多个客户端或协程同时执行操作,从而减少资源竞争、服务端压力或“惊群效应”(thundering herd problem)。
具体分析这段代码中的作用:
-
分散请求峰值:
- 当多个客户端/协程尝试同时启动监视器(如服务注册中心的
Watch
操作)时,可能导致服务端瞬间负载激增。 rand.Int63n(100)
生成0-99毫秒的随机延迟,使得不同客户端/协程的启动时间分散,避免同时发起大量请求。
- 当多个客户端/协程尝试同时启动监视器(如服务注册中心的
-
避免连续的同步重试:
- 在错误恢复场景中(如
c.Registry.Watch
失败后的重试),如果所有失败的客户端立即重试,可能形成同步的重试浪潮。 - 随机延迟破坏了这种同步性,使重试时间点随机化,提高服务端恢复的可能性。
- 在错误恢复场景中(如
-
配合指数退避(backoff):
- 代码中的
backoff(a)
和backoff(b)
是逐渐增加等待时间的退避策略(如指数退避)。 jitter
与退避策略结合,既避免短时间内的密集重试(退避),又防止多个客户端退避后同时唤醒(jitter)。
- 代码中的
-
提高鲁棒性:
- 在网络不稳定的场景中,随机延迟可以降低因短暂故障(如网络抖动)导致多个客户端同时重建连接而加剧问题的风险。
类比现实场景:
假设某个服务注册中心(如Consul或Etcd)短暂宕机,所有依赖它的客户端都会尝试重建Watch
连接。如果没有jitter:
- 所有客户端可能严格按照固定间隔(如0ms, 100ms, 200ms...)重试,导致服务端恢复后瞬间被再次压垮。
- 加入jitter后,客户端的重试时间会在退避基础上叠加随机偏移(如12ms, 87ms, 45ms...),分散请求压力,提高整体恢复成功率。
总结:
此处的jitter
是一种轻量级的容错机制,通过随机化操作的时间分布,优化系统在高并发或故障场景下的行为,避免自发性同步引发的雪崩效应。