今天测试反馈说有任务卡死在我的服务上,很久都跑不完。经排查发现任务在完成后移除队列时redis直接报了EOF的错误,导致任务一直在不停的重复。且出现频次很高。
出现该现象的主要是那种执行时间超过一分钟的任务,我的idle_time_out设置时间也是一分钟,经百度,发现有人说redigo在使用过程中会出现EOF现象,原因是自带的缓冲池如果空闲超过一定的时间,会被redis sever关闭,再次get时第一次一般就会出现EOF错误。解决办法是添加比超时时间短的间隔心跳。
通过查阅源码,发现redgo的pool池可以通过TestOnBorrow参数设置心跳,具体方式如下如下:
func NewPool(redisURL string) {
Pool = &redis.Pool{
MaxIdle: maxIdle,
IdleTimeout: idleTimeout,
MaxActive: maxActive,
Wait: wait,
Dial: func() (redis.Conn, error) {
return redis.DialURL(redisURL, redis.DialConnectTimeout(dialTimeout))
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < 1*time.Minute {
return nil
}
_, err := c.Do("PING")
return err
},
}
}
超时时间idleTimeout是80秒,每60秒设置一次心跳,这样应该就不会出现超时了。然而现实是并没有解决这个问题,出现确实少了一些,但是也就是一些少了一些,并没有解决这个问题。
然后发现有人说空闲连接被sever关了第一次连接会EOF,但是如果在下次超时之前进行了第二次连接就不会再EOF了,如果真是这样那么直接失败后重传一次是不是就可以成功了呢?可惜依然没有按照预想的方向进行。
痛定思痛,决心回去仔细扒源码。然后经过一番排查,终于发现了问题所在。
1、EOF并不是连接超时引起的,更大的可能是你操作了一个已经关闭的连接
2、官方给出的示例没有问题,但是在概念理解上却存在偏差,个人认为是对redis不熟造成的。
先说百度上常给的解释,基本上是没有问题的,只是他解析的不是EOF出现的根本原因,而是会造成这种结果的一种可能性。需要补充的是get时会EOF的根本原因是连接的一方去使用了一个已经失效的连接。这里补充一下百度解析的过程为什么会造成这样的结果:get方法本身只会删除idle_time_out超时的连接,同时返回一个可用空闲连接或者新连接。什么意思呢,假设客户端的idle_time_out设置的比服务器设置的大,连接因为空闲太久,超过了sever默认的超时时间被服务器关闭了,但是客户端不知道,他还认为这个连接是有效的,于是你就能通过get去拿到这个已经失效的连接,在此连接上做任何操作都会报EOF。这也是最常见的EOF现象,所以官方示例给出的解决办法也是针对这种情况的。需要说明的是TestOnBorrow这个函数并不是用来设置心跳的,或者说他的本意不是设置心跳,代码中的例子也不是一分钟发一个心跳包的意思。他真是的意图是在每次get空闲连接之前先检测这个连接是否真的是正常的,如果正常就返回连接,否则就该关闭连接并寻找先一个空闲连接或者创建新连接。有了这样的功能,我们在get的时候就一定能保证得到的是真正可用的连接。所以如果你的EOF是这中情况,上述方法绝对能够帮到你。当然,这样的操作显然也一定会导致get的时间延长,最坏的情况可能会超出服务器所能接受的范围,可能需要根据实际情况做一些限制。
然而现实永远都有一个如果,是的,我遇到的EOF不是这种情况,根本原因没变,操作了已关闭的连接,但是是超过了我自己设置的idle_time_out时间自动关闭的,而不是被服务器关闭的,但是我的程序却不知道,以为get到的连接依然存在。造成这种情况的原因主要是操作不当:我在get了连接c以后做了一个耗时很长且不可控的与redis无关的操作,然后接着把结果写回到redis,再关闭redis。于是悲剧产生了,在我写回数据之前,c已经被pool关闭了,我没有校验直接再去写,自然就会报EOF。然后在这种前提下我再去重试redis操作,自然也不能解决问题。这也就是为什么之前两次尝试都失败了。
到这里解决办法基本上已经很清晰了,操作和get连接的时间间隔应该在你的控制范围内,直白点就是要小于你和服务器设置的idle_time_out。