已发表的技术专栏
0 grpc-go、protobuf、multus-cni 技术专栏 总入口
4 grpc、oauth2、openssl、双向认证、单向认证等专栏文章目录)
本篇文章先从服务器端一侧介绍保持链接的相关原理。
1、服务器端keepalive原理图 |
服务器端的keepalive功能,存在3种类型的定时器,这3种定时器主要是对三种情况进行处理:
- kpTimer, 在规定的时间段内,服务器端是否接收到客户端发送的数据
- ageTimer, 任何链接都一个存活的时长,超过时长如何处理
- idleTimer, 链接处于idle状态的时长,也有一个规定的最大时长,超过后,如何处理
keepalive功能是select+timer.NewTimer来实现的。
其中, timer.NewTimer设置的定时时长,是可以动态调整的。
2、服务器端keepalive源码分析 |
2.1.服务器端何时触发keepalive功能启动呢? |
在服务器端完成客户端的链接请求后,即客户端跟服务器端双方底层建立起链接了,服务器端以协程的方式,启动了keepalive功能。
服务器端一侧的方法调用链:
main.go→Serve→handleRawConn→newHTTP2Transport→NewServerTransport→newHTTP2Server
进入grpc-go/internal/transport/http2_server.go文件中的newHTTP2Server方法里:
1.func newHTTP2Server(conn net.Conn, config *ServerConfig) (_ ServerTransport, err error) {
2. // ---省略不相关代码---如http2Server的创建 ----接收客户端的帧
3. // ---省略不相关代码---帧发送器的创建
4.
5. go t.keepalive()
6. return t, nil
7.}
第5行,就是启动keepalive功能
2.2、keepalive源码分析 |
进入grpc-go/internal/transport/http2_server.go文件中的keepalive方法里
1.func (t *http2Server) keepalive() {
2. p := &ping{}
3.
4. outstandingPing := false
5. kpTimeoutLeft := time.Duration(0)
6. prevNano := time.Now().UnixNano()
7.
8. idleTimer := time.NewTimer(t.kp.MaxConnectionIdle)
9. ageTimer := time.NewTimer(t.kp.MaxConnectionAge)
10. kpTimer := time.NewTimer(t.kp.Time)
11. defer func() {
12. idleTimer.Stop()
13. ageTimer.Stop()
14. kpTimer.Stop()
15. }()
16. for {
17. select {
18. case <-idleTimer.C:
19. t.mu.Lock()
20. idle := t.idle
21. if idle.IsZero() { // The connection is non-idle.
22. t.mu.Unlock()
23. idleTimer.Reset(t.kp.MaxConnectionIdle)
24. continue
25. }
26. val := t.kp.MaxConnectionIdle - time.Since(idle)
27. t.mu.Unlock()
28. if val <= 0 {
29. t.drain(http2.ErrCodeNo, []byte{})
30. return
31. }
32.
33. idleTimer.Reset(val)
34. case <-ageTimer.C:
35. t.drain(http2.ErrCodeNo, []byte{})
36. ageTimer.Reset(t.kp.MaxConnectionAgeGrace)
37. select {
38. case <-ageTimer.C:
39. t.Close()
40. case <-t.done:
41. }
42. return
43. case <-kpTimer.C:
44. lastRead := atomic.LoadInt64(&t.lastRead)
45.
46. if lastRead > prevNano {
47. outstandingPing = false
48. kpTimer.Reset(time.Duration(lastRead) + t.kp.Time - time.Duration(time.Now().UnixNano()))
49. prevNano = lastRead
50. continue
51. }
52. if outstandingPing && kpTimeoutLeft <= 0 {
53. t.Close()
54. return
55. }
56. if !outstandingPing {
57. if channelz.IsOn() {
58. atomic.AddInt64(&t.czData.kpCount, 1)
59. }
60. t.controlBuf.put(p)
61. kpTimeoutLeft = t.kp.Timeout
62. outstandingPing = true
63. }
64.
65. sleepDuration := minTime(t.kp.Time, kpTimeoutLeft)
66. kpTimeoutLeft -= sleepDuration
67. kpTimer.Reset(sleepDuration)
68. case <-t.done:
69. return
70. }
71. }
72.}
主要处理逻辑说明
- 第2-10行:声明初始一些变量,如ping,针对三种情况的定时器
- 第18-33行:链接处于idle状态的处理逻辑
- 第34-42行:任何一个链接超过规定的运行时间后的处理逻辑
- 第43-67行:就是keepalive的主要目的,判断一链接是否还有数据传输的处理逻辑
- 第68-69行:http2Server关闭时的处理逻辑
2.2.1、当链接处于idle的时长,超过了规定的时长时,服务器端如何处理? |
服务器端处理idle时的逻辑流程图? |
可以通过下面的流程图,来快速理解:
- 先判断链接是否处于idle状态
- 若链接处于非idle状态时
- 重置idleTimer定时器的时长为t.kp.MaxConnectionIdle
- 若链接处于idle状态时:
- 计算当前链接还能允许处于idle状态的最长时长是多少,即val;
- 判断处于idle状态的时长是否超过了规定允许的最长时间
- 超过时,给客户端发送goAway帧;即客户端会关闭链接
- 没有超过时,重置idleTimer定时器的时长为val
可以按照下面的思路去理解,就很容易理解: 两个大的分支
- 分之一:先判断是否处于idle状态?
- 是,如何处理?
- 不是,又是如何处理
- 分支二:已经处于idle状态的话,判断处于IDlE状态的时长是否超过了规定的时长;
- 超过,如何处理;
- 未超过,如何处理
好,继续分析源码:
keepalive源码重点分析:针对的是第18-33行的功能: |
- 第20-25行:主要校验链接是否处于idle状态
- 第20行:t.idle,这个是时间;(不是表示状态的,感觉这个变量idle用于产生误会,感觉好像是状态);
- 如果t.idle的值形式,如2021-02-26 15:24:04.312007 +0800 形式的话,idle.IsZero就是false,表示该连接处于IDLE状态;
- 如果t.idle的值形式,如0001-01-01 00:00:00 +0000 UTC形式的话,idle.IsZero就是true,表示该连接处于非IDLE状态,
- 什么情况下,连接处于IDLE状态呢?
- 刚建立起链接,创建http2Server时,就是空闲状态,idle.IsZero就是false
- 如果http2Server,至少有一个流维护着,就是非空闲状态,idle.IsZero就是true;
- 服务器端接收到客户的流时,就会判断是否将状态由空闲状态改变非空闲状态。(http2_server.go文件operateHeaders方法里有)
- 服务器端删除流时, 就会判断是否将状态由非空闲状态改为空闲状态,即 idle.IsZero由true,变为了false;(http2_server.go文件deleteStream方法里有)
- 第21-24行:idle.IsZero就是true,即连接处于非空闲状态时的处理逻辑。即非idle状态
- 第23-24行:需要重置idle定时器的时间为t.kp.MaxConnectionIdle,并继续运行
- 就是说在idleTimer, ageTimer, kpTimer这三个定时器中,idleTimer定时器时间到了,因此进入此分支,校验时发现idle.IsZero为true,表明该连接还在接受客户端的数据,因此,对此定时器进行重置,不做任何处理。
- 第20行:t.idle,这个是时间;(不是表示状态的,感觉这个变量idle用于产生误会,感觉好像是状态);
- 第26-33行:idle.IsZero就是false,即连接处于空闲状态时的处理逻辑。即idle状态
- 第26行:计算一下,当前连接处于IDLE的时长跟规定允许处于IDLE的时长的关系
- time.Since(idle)表示,当前连接处于idle状态的时长,类型是time.Duration;假设是5s
- t.kp.MaxConnectionIdle,grpc服务器端启动时,设置的参数,运行某个连接处于IDLE的最长时长; 假设是1s
- 第28-31行:处理的是,当前链接处于IDLE的时长超过了规定的时长时,服务器端如何办?
- 调用drain,内部其实就是向客户端发送了一个goAway帧;
- 最终导致的效果是:客户端关键链接;客户端关闭链接后,服务器端也会关闭链接。
- 第33行:处理的是,当前链接处于IDLE的时长没有超过规定的时长时,服务器端如何办?
- 计算一下,当前链接还能允许处于IDLE状态的最长时间是多少;就是前面计算的val := t.kp.MaxConnectionIdle - time.Since(idle)
- 服务器端需要重置idleTimer定时器的时间为val,即idleTimer.Reset(val)
- 第26行:计算一下,当前连接处于IDLE的时长跟规定允许处于IDLE的时长的关系
2.2.2、当一个链接超过规定的运行时长时,服务器端如何处理? |
服务器端针对超过运行时长链接的处理逻辑流程图? |
可以通过下面的流程图,来快速理解:
- 给客户端发送goAway帧,客户端接收到后,关闭客户端链接。
- 重置ageTimer定时器的时长,为优雅关闭时长t.kp.MaxConnectionAgeGrace,假设为5秒
- 假设在这5秒内:
- 客户端已经关闭链接了,服务器端的帧接收器接收到EOF后,会主动关闭服务器端的链接
- 如果在这5秒内,服务器端一直没有接收到EOF的话,优雅关闭到期了,就主动调用t.close关闭服务器端的链接。
优雅关闭?
给客户端发送了goAway帧,先等待一下,等待时长t.kp.MaxConnectionAgeGrace,这时间后,再关闭服务器端链接。
就是说,先让客户端处理一会,服务器端再处理。
只要超过运行时长,就必须关闭链接。
keepalive源码重点分析:针对的是第35-42行的功能: |
- 第35行:直接给客户端发送goAway帧;客户端接收到此帧后,关闭链接。
- 第36行:将ageTimer.C定时器的时间,重置为优雅关闭时间,t.kp.MaxConnectionAgeGrace
- 第37-41行:多路复用器,判断ageTimer.C先执行,还是t.done先执行
- 如果在优雅关闭时间内,客户端先关闭了链接,服务器端的帧接收器接收到EOF后,就会触发服务器端关闭链接;
- 如果在优雅关闭时间内,服务器端始终没有接收到EOF,就在keepalive里调用t.close关闭链接。
实际测试时,如何测试让ageTimer.C先执行,还是让先t.done执行? |
只关注服务器端,测试用例为examples/features/keepalive/server/main.go。
我本地测试是,grpc 客户端 跟grpc服务器端都是在同一个物理机上做的测试。
1.如何让keepalive,先执行第40行的t.done语句呢? |
其实,不需要修改任何代码,直接用main.go文件中的测试用例即可。
因为测试用例中,客户端接收到goAway帧后,很快就关闭了,耗时远远小于MaxConnectionAgeGrace的时长
进入grpc-go/internal/transport/http2_server.go文件中的HandleStreams方法里:
1.func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) {
2.for {
3. t.controlBuf.throttle()
4. frame, err := t.framer.fr.ReadFrame()
5. atomic.StoreInt64(&t.lastRead, time.Now().UnixNano())
6. if err != nil {
7. //---省去---不相关代码---
8. if err == io.EOF || err == io.ErrUnexpectedEOF {
9. t.Close()
10. return
11. }
12. t.Close()
13. return
14. }
第8行,接收到EOF后,就关闭连接了。
2.如何让keepalive,先执行第38行的ageTimer.C语句呢? |
方式有很多。可以修改MaxConnectionAgeGrace的参数值,如10*time.Millisecond;
var kasp = keepalive.ServerParameters{
MaxConnectionIdle: 5 * time.Second,
MaxConnectionAge: 10 * time.Second,
MaxConnectionAgeGrace: 10 * time.Millisecond,
Time: 5 * time.Second,
Timeout: 1 * time.Second,
}
这样的话,客户端正处于关闭连接状态时,而服务器端的优雅关闭时间已到期了,就主动调用第39行,t.close()关闭连接了。
2.2.3、如何来验证服务器端跟客户端还保持着链接?如果链接已经挂掉的话,服务器端如何处理 |
核心思路就是: |
判断服务器端在规定的时间段内,如5秒里,是否接收到过数据,或者说任何类型的帧;
接收到过,就表明链接存在。
没有接收到过,表明链接存在异常了。
验证服务器端跟客户端保持链接流程图
- kpTimer定时器到期后,判断在kpTimer定时器的时长内,如kpT,是否接收到过数据,或者说任何类型的帧
- 若接收到过帧:
- 需要重新计算kpTimer定时器的定时时长,如val
- 重置kpTimer定时器的时长为val
- 若没有接收到过帧:
- 判断在定时时长kpT内,是否已经发送过ping帧了;(发送的ping帧中,data属性未空,可以认为发送了一个测试帧,判断链接是否处于运行状态)
- 若没有发送过ping帧的话:
- 向客户端发送ping帧
- 重新计算kpTimer定时器的时长为sleepDuration
- 计算ping帧接收到ACK反馈帧的最少时间,为kpTimeoutLeft
- 重置kpTimer定时器的时长为sleepDuration
- 若已经发送过ping帧了的话:
- 判断接收ACK反馈帧的时长是否满足条件;就是判断kpTimeoutLeft值是否满足条件
- 若满足条件,例如kpTimeoutLeft > 0
- 表明还可以继续等待接收ack帧;即重新执行上面步骤”若没有发送过ping帧的话”中的第2-4步;
- 若不满足条件,例如kpTimeoutLeft <= 0
- 表明,服务器端在规定的时间kpTimeoutLeft 内,没有接收到客户端发送的ack属性为true的ping帧。
- 若没有发送过ping帧的话:
- 判断在定时时长kpT内,是否已经发送过ping帧了;(发送的ping帧中,data属性未空,可以认为发送了一个测试帧,判断链接是否处于运行状态)
简单的理解,就是:(或者说,整个核心思路,可以用下面的话来总结)
- 设置一条规则,这个规则来判断是否接收到客户端发送的数据
- 如果有,则重新执行keeplive功能
- 如果没有接收到数据,则发送一个空的ping帧,用以测试链接是否还存在。
- 若在规定的时间内,接收到ack帧了,就表明,链接还在。
- 若在规定的时间内,没有接收到ack帧,就可以关闭服务器端的链接了。
keepalive源码重点分析:针对的是:44-67行的功能 |
- 第44行:加载服务器端最后一次接收到帧的时间,lastRead
- 第46-51行:处理的是,在规定的时间t.kp.Time内,服务器端接收到过客户端的帧,服务器端需要如何处理?
-
lastRead,表示的是最后一次接收到帧的时间
-
prevNano,表示的是接收到前一个帧的时间
-
若lastRead > prevNano的话,说明在接收到前一个帧后,在规定的时间内,又接收到了新的帧,时间是lastRead;即表明,服务器端跟客户端还保持着链接
-
第47行:outstandingPing重置为false,表示的是服务器端没有向客户端发送ping帧
-
第48行:重置kpTimer定时器的时长
- time.Duration(lastRead),表示的是最近一次读取到帧的时间距离1970年1月1日有多长时间,如448463h59m33.436238s
- time.Duration(time.Now().UnixNano()),表示的是当前时间距离1970年1月1日有多长时间,如448464h0m12.616233s
- t.kp.Time,表示的是kpTimer定时器的定时时长,如5s
- 可以用下面的时间轴,来简单表示它们之间的关系:val=time.Duration(lastRead) + t.kp.Time - time.Duration(time.Now().UnixNano())
- 重置kpTimer定时器的时长为val,需要在val时间段内接收到帧,如果接收不到,说明链接可能存在异常了
-
第49行:更新prevNano时间为lastRead时间
-
第50行:退出case分支,继续keepalive功能
-
- 第52-67行:针对的是,在规定的时间内,没有接收到数据的情况时,服务器端的处理逻辑:
- 此时,表明服务器端跟客户端的链接,可能已经存在问题了
- 由于outstandingPing 初始值为false,因此,程序会执行第56-63行,
- 第60行:向客户端发送一个空的ping帧
- 第61行:重置kpTimeoutLeft为t.kp.Timeout,表示的是服务器端发送出去ping帧后,规定在多长时间内,接收到ack帧。
- 第62行:重置outstandingPing 为true;当outstandingPing为true时表明,ping帧发出去后,此刻,还没有接收到任何帧
- 第65行:计算kpTimer定时器的定时时长为sleepDuration;
- 第 66行:更新kpTimeoutLeft时长;kpTimeoutLeft抛去定时时长sleepDuration,表示,还允许最多等待多长时间接收到ack帧;
- 第67行:重置kpTimer定时时长为sleepDuration;
- 进入下一轮:若kpTimer定时器又到期了,那么
- 若kpTimeoutLeft等于0时(kpTimeoutLeft=kpTimeoutLeft-sleepDuration >=0),执行第52-55行,即调用t.close关闭连接
- 若kpTimeoutLeft 大于0时(kpTimeoutLeft=kpTimeoutLeft-sleepDuration > 0),表示,还可以继续等待接收ack帧;需要重新计算sleepDuration,kpTimeoutLeft,重置kpTimer定时器时长,进入下一轮运算
下一篇文章
客户端keepalive原理图介绍以及源码分析
点击下面的图片,返回到专栏大纲 |