grpc-go源码剖析七十二之服务器端keepalive原理图介绍以及源码分析

已发表的技术专栏
0  grpc-go、protobuf、multus-cni 技术专栏 总入口

1  grpc-go 源码剖析与实战  文章目录

2  Protobuf介绍与实战 图文专栏  文章目录

3  multus-cni   文章目录(k8s多网络实现方案)

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方法里:

1func newHTTP2Server(conn net.Conn, config *ServerConfig) (_ ServerTransport, err error) {
2// ---省略不相关代码---如http2Server的创建   ----接收客户端的帧
3// ---省略不相关代码---帧发送器的创建
45go t.keepalive()

6return t, nil
7}

第5行,就是启动keepalive功能

2.2、keepalive源码分析

进入grpc-go/internal/transport/http2_server.go文件中的keepalive方法里

1func (t *http2Server) keepalive() {
2.	p := &ping{}
34.	outstandingPing := false
5.	kpTimeoutLeft := time.Duration(0)
6. 	prevNano := time.Now().UnixNano()
78.	idleTimer := time.NewTimer(t.kp.MaxConnectionIdle) 
9.	ageTimer := time.NewTimer(t.kp.MaxConnectionAge) 
10.	kpTimer := time.NewTimer(t.kp.Time) 
11defer func() {
12.		idleTimer.Stop()
13.		ageTimer.Stop()
14.		kpTimer.Stop()
15}()

16for {
17select {
18case <-idleTimer.C:
19.			t.mu.Lock()
20.			idle := t.idle
21if idle.IsZero() { // The connection is non-idle.
22.				t.mu.Unlock()
23.				idleTimer.Reset(t.kp.MaxConnectionIdle)
24continue
25}

26.			val := t.kp.MaxConnectionIdle - time.Since(idle)
27.			t.mu.Unlock()

28if val <= 0 {
29.				t.drain(http2.ErrCodeNo, []byte{})
30return
31}
3233.			idleTimer.Reset(val)
34case <-ageTimer.C:
35.			t.drain(http2.ErrCodeNo, []byte{})
36.			ageTimer.Reset(t.kp.MaxConnectionAgeGrace)
37select {
38case <-ageTimer.C:
39.				t.Close()
40case <-t.done:
41}
42return
43case <-kpTimer.C:
44.			lastRead := atomic.LoadInt64(&t.lastRead)
4546if lastRead > prevNano {
47.				outstandingPing = false
48.				kpTimer.Reset(time.Duration(lastRead) + t.kp.Time - time.Duration(time.Now().UnixNano()))
49.				prevNano = lastRead
50continue
51}
52if outstandingPing && kpTimeoutLeft <= 0 {
53.				t.Close()
54return
55}
56if !outstandingPing {
57if channelz.IsOn() {
58.					atomic.AddInt64(&t.czData.kpCount, 1)
59}
60.				t.controlBuf.put(p)
61.				kpTimeoutLeft = t.kp.Timeout
62.				outstandingPing = true
63}
6465.			sleepDuration := minTime(t.kp.Time, kpTimeoutLeft)
66.			kpTimeoutLeft -= sleepDuration
67.			kpTimer.Reset(sleepDuration)
68case <-t.done:
69return
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,表明该连接还在接受客户端的数据,因此,对此定时器进行重置,不做任何处理。
  • 第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)

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方法里:

1func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) {
2for {
3.		t.controlBuf.throttle()
4.		frame, err := t.framer.fr.ReadFrame()
5.		atomic.StoreInt64(&t.lastRead, time.Now().UnixNano())
6if err != nil {
7//---省去---不相关代码---
8if err == io.EOF || err == io.ErrUnexpectedEOF {
9.				t.Close()
10return
11}
12.			t.Close()
13return
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帧。

简单的理解,就是:(或者说,整个核心思路,可以用下面的话来总结)

  • 设置一条规则,这个规则来判断是否接收到客户端发送的数据
    • 如果有,则重新执行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原理图介绍以及源码分析

点击下面的图片,返回到专栏大纲

gRPC-go源码剖析与实战之专栏大纲

gRPC-go源码剖析与实战感谢

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码二哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值