解决golang redis连接池的io异常BUG?

栏目: Go · 发布时间: 5年前

内容简介:前言golang redis连接的一个bug?为什么是带个疑问问号?  因为我也不确定这是否算一个bug,但不管是go呀,lua呀,python呀,绝大数的连接池的构建不仅仅会用来复用连接,而且会针对异常io的连接进行重试。

前言

golang redis连接的一个bug?为什么是带个疑问问号?  因为我也不确定这是否算一个bug,但不管是 go 呀,lua呀,python呀,绝大数的连接池的构建不仅仅会用来复用连接,而且会针对异常io的连接进行重试。 

解决golang redis连接池的io异常BUG?

golang的 redis 库出名的就两个,一个是gomodule的redigo,另一个是go-redis的redis库。这里说有连接池使用问题的redis库是gomodule,而go-redis没有这个问题。

简单说,当你获取了从连接吃获取了一个连接,但是这时候连接中断了,你再去使用该连接肯定是有问题的了。go-redis会根据error的信息做连接的重试,而gomodule的redis就不管不问了。 曾经在社区问过,给的回复是小概率事件,如果想做重试需要在调用方做判断。

redis pool源码分析

要分析问题,当然要看redis pool的源码了。 我们看下get方法,  当连接池的IdleTimeout 大于 0,会触发一次空闲连接的整理,这里的空闲连接整理也是被动的,当你触发get()的时候,才会去触发一次。每次触发会轮询所有的client对象。 当你的idle.front链表不为空,那么尝试去拿一个连接,如果绑定了TestOnBorrow自定义方法,那么进行检测连接是否可用。 

后面我们会具体说明下,TestOnBorrow 其实并不能完全解决io异常问题。

// xiaorui.cc
 
func (p *Pool) get(ctx interface {
    Done() <-chan struct{}
    Err() error
}) (*poolConn, error) {
 
    ...
 
    p.mu.Lock()
 
    // Prune stale connections at the back of the idle list.
    if p.IdleTimeout > 0 {
        n := p.idle.count
        for i := 0; i < n && p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()); i++ {
            pc := p.idle.back
            p.idle.popBack()
            p.mu.Unlock()
            pc.c.Close()
            p.mu.Lock()
            p.active--
        }
    }
 
    // Get idle connection from the front of idle list.
    for p.idle.front != nil {
        pc := p.idle.front
        p.idle.popFront()
        p.mu.Unlock()
        if (p.TestOnBorrow == nil || p.TestOnBorrow(pc.c, pc.t) == nil) &&
            (p.MaxConnLifetime == 0 || nowFunc().Sub(pc.created) < p.MaxConnLifetime) {
            return pc, nil
        }
        pc.c.Close()
        p.mu.Lock()
        p.active--
    }
 
    // Check for pool closed before dialing a new connection.
    if p.closed {
        p.mu.Unlock()
        return nil, errors.New("redigo: get on closed pool")
    }
 
    // Handle limit for p.Wait == false.
    if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive {
        p.mu.Unlock()
        return nil, ErrPoolExhausted
    }
    ...
    return &poolConn{c: c, created: nowFunc()}, err
}

再来看下redis pool释放连接到连接池的put方法, 没什么好说的,就是把连接对象返回给连接池,更改active计数就完了。

// xiaorui.cc
 
func (p *Pool) put(pc *poolConn, forceClose bool) error {
	p.mu.Lock()
	if !p.closed && !forceClose {
		pc.t = nowFunc()
		p.idle.pushFront(pc)
		if p.idle.count > p.MaxIdle {
			pc = p.idle.back
			p.idle.popBack()
		} else {
			pc = nil
		}
	}
 
	if pc != nil {
		p.mu.Unlock()
		pc.c.Close()
		p.mu.Lock()
		p.active--
	}
 
        ...
	p.mu.Unlock()
	return nil
}

TestOnBorrow是我们创建redis连接池的时候注册的回调方法。当我们每次从连接池获取连接的时候,都会调用这个方法一次。

你可以这么用,每次都用ping pong来探测连接的可用,但每个操作都占用RTT,加大业务的延迟消耗,虽然内网下redis单次操作在100us左右。

// xiaorui.cc
 
    TestOnBorrow: func(c redis.Conn, t time.Time) error {
        _, err := c.Do("PING")
        if nil != err {
            WxReport("redis ping error:"+err.Error(), "error")
        }
        return err
    },

回调TestOnBorrow的时候,会传递给你连接对象和上次的时间,你可以一分钟检验一次。

// xiaorui.cc
 
    TestOnBorrow: func(c redis.Conn, t time.Time) error {
        if time.Since(t) < time.Minute {
            return nil
        }
        _, err := c.Do("PING")
        if nil != err {
            WxReport("redis ping error:"+err.Error(), "error")
        }
        return err
    },

我们上面有说过 TestOnBorrow 不能完全解决连接io异常的问题? 我们设想一下,当我pop一个连接的时候, TestOnBorrow帮我测试连接是可用的,但是 探测完了后,连接中断了,这时候我去使用自然就异常了。 

大家会觉得这个概率不大,但我们遇到了几次,简单说下有两个场景。

第一个:

我们配置了超时是3600s,redis server空闲连接超时配置了600s,redis server会在我们之前把连接给关了,该redis client自然就没法用了,使用redis操作的业务逻辑自然就走不下去了,难道让我的代码都写判断,再来一次连接获取?

第二个:

同事的一个bug,  已经从pool里获取了连接,但是业务逻辑特别的繁杂,可能在一分钟后才会使用。但用之前redis做了重启呀,升级呀,又引起redis client异常了。

简单说,哪怕 TestOnBorrow是每次都ping pong检查,也是有概率出现io引起的异常。现在绝大数的连接池基本都规避了该问题。

解决方法

就是判断网络io异常引起的redis对象,然后重新new一个连接就可以了。

// xiaorui.cc
 
package main
 
import (
    "errors"
    "fmt"
    "io"
    "strings"
    "time"
 
    "github.com/gomodule/redigo/redis"
)
 
var (
    RedisClient *redis.Pool
)
 
func init() {
    var (
        host string
        auth string
        db   int
    )
    host = "127.0.0.1:6379"
    auth = ""
    db = 0
    RedisClient = &redis.Pool{
        MaxIdle:     100,
        MaxActive:   4000,
        IdleTimeout: 180 * time.Second,
        Dial: func() (redis.Conn, error) {
            c, err := redis.Dial("tcp", host, redis.DialPassword(auth), redis.DialDatabase(db))
            if nil != err {
                return nil, err
            }
            return c, nil
        },
        TestOnBorrow: func(c redis.Conn, t time.Time) error {
            if time.Since(t) < time.Minute {
                return nil
            }
            _, err := c.Do("PING")
            return err
        },
    }
 
}
 
func main() {
    rd := RedisClient.Get()
    defer rd.Close()
 
    fmt.Println("please kill redis server")
    time.Sleep(5 * time.Second)
 
    fmt.Println("please start redis server")
    time.Sleep(5 * time.Second)
 
	resp, err := redis.String(redo("SET", "push_primay", "locked"))
    fmt.Println(resp, err)
}
 
func IsConnError(err error) bool {
    var needNewConn bool
 
    if err == nil {
        return false
    }
 
    if err == io.EOF {
        needNewConn = true
    }
    if strings.Contains(err.Error(), "use of closed network connection") {
        needNewConn = true
    }
    if strings.Contains(err.Error(), "connect: connection refused") {
        needNewConn = true
    }
    return needNewConn
}
 
// 在pool加入TestOnBorrow方法来去除扫描坏连接
func redo(command string, opt ...interface{}) (interface{}, error) {
    rd := RedisClient.Get()
    defer rd.Close()
 
    var conn redis.Conn
    var err error
    var maxretry = 3
    var needNewConn bool
 
    resp, err := rd.Do(command, opt...)
    needNewConn = IsConnError(err)
    if needNewConn == false {
        return resp, err
    } else {
        conn, err = RedisClient.Dial()
    }
 
    for index := 0; index < maxretry; index++ {
        if conn == nil && index+1 > maxretry {
            return resp, err
        }
        if conn == nil {
            conn, err = RedisClient.Dial()
        }
        if err != nil {
            continue
        }
 
        resp, err := conn.Do(command, opt...)
        needNewConn = IsConnError(err)
        if needNewConn == false {
            return resp, err
        } else {
            conn, err = RedisClient.Dial()
        }
    }
 
    conn.Close()
    return "", errors.New("redis error")
}
 
// xiaorui.cc

只要看到 “use of closed network connection” 、 “connect: connection refused”、io.EOF 都会new一个先连接。常规的思路应该是,当前连接有io的异常,重新new一个新连接,然后把新连接替换老连接,但是redigo没有类似的操作入口,导致新连接游离在pool外面。 我们的主要目的是为了 兼容redis client的异常,所以临时的短连接也是可以接受,只需要等到下个TestOnBorrow的检测周期就可以了。

总结:

gomodule / redigo的易用性不错,个人感觉体验要比go-redis强一些,但是gomodule/redigo的作者在issue里说,不打算支持redis cluster协议 ? look   https://github.com/gomodule/redigo/issues/319

好在社区里有人基于redigo做了易用性相当高的redis cluster client库 https://github.com/chasex/redis-go-cluster 。大家可以看下 redis-go-cluster的源码,里面做了各种multi key下的聚合操作。默认redis cluster client是不支持单次多slot区间key的使用,但redis-go-cluster解决了这该类问题,实现的原理很简单,就是内部把key分到不同的队列里,然后开多个goroutine发送。如果中间出现slot migrate问题,那么重来一遍。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

About Face 3

About Face 3

Alan Cooper、Robert Reimann、David Cronin / John Wiley & Sons / 2007-5-15 / GBP 28.99

* The return of the authoritative bestseller includes all new content relevant to the popularization of how About Face maintains its relevance to new Web technologies such as AJAX and mobile platforms......一起来看看 《About Face 3》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具