Several Golang connection pool implementation case

Because the TCP three-way handshake and other reasons, to establish a connection is a relatively high cost behavior. So many times in a need to interact with a particular entity in the program, we need to maintain a connection pool, which has a connection can be reused for repeated use.

While maintaining a connection pool, the basic requirement is to do: thread safe (thread-safe), especially in the Golang this feature is goroutine language.

Simple connection pool

type Pool struct {
	m sync.Mutex // 保证多个goroutine访问时候,closed的线程安全
	res chan io.Closer //连接存储的chan
	factory func() (io.Closer,error) //新建连接的工厂方法
	closed bool //连接池关闭标志
}

This simple connection pool, we used to connect storage pond chan. The method and the new structure is relatively simple:

func New(fn func() (io.Closer, error), size uint) (*Pool, error) {
	if size <= 0 {
		return nil, errors.New("size的值太小了。")
	}
	return &Pool{
		factory: fn,
		res:     make(chan io.Closer, size),
	}, nil
}

Only needs to provide a function corresponding to the size of the plant and the pool it is connected.

Get connected

So how should we derive resources? Because the structure of our internal storage connectivity is chan, so only need a simple select can guarantee thread safety:

//从资源池里获取一个资源
func (p *Pool) Acquire() (io.Closer,error) {
	select {
	case r,ok := <-p.res:
		log.Println("Acquire:共享资源")
		if !ok {
			return nil,ErrPoolClosed
		}
		return r,nil
	default:
		log.Println("Acquire:新生成资源")
		return p.factory()
	}
}

We start this connection res pool chan get inside, if not, then we'll use our already prepared factory function constructed connection. We first determine whether the connection pool has been closed at the time of use ok to get a connection from the res. If you have already closed, then we will return already prepared connection was closed error.

Close the connection pool

Well, since the close connection pooling mentioned, it is how we close the connection pool it?

//关闭资源池,释放资源
func (p *Pool) Close() {
	p.m.Lock()
	defer p.m.Unlock()

	if p.closed {
		return
	}

	p.closed = true

	//关闭通道,不让写入了
	close(p.res)

	//关闭通道里的资源
	for r:=range p.res {
		r.Close()
	}
}

Here we need to make pmLock () locking operation, we need to do this because of the closed structure which read and write. After the need to put this flag is set, turn off the res chan, making Acquire method can no longer get new connections. Again we chan inside connections res perform this Close action.

Connection release

Release the connection Start with a premise that connection pooling is not yet closed. If the connection pool has been closed, then go down to send res inside connections like trigger panic.

func (p *Pool) Release(r io.Closer){
	//保证该操作和Close方法的操作是安全的
	p.m.Lock()
	defer p.m.Unlock()

	//资源池都关闭了,就省这一个没有释放的资源了,释放即可
	if p.closed {
		r.Close()
		return
	}

	select {
	case p.res <- r:
		log.Println("资源释放到池子里了")
	default:
		log.Println("资源池满了,释放这个资源吧")
		r.Close()
	}
}

The above is a simple and thread-safe connection pool implementations of. We can see that now, although the connection pool has been achieved, but there are a few small drawbacks:

There is no limit on the maximum number of connections, if the thread pool empty, then we have to create a new default directly connected returned. Once the amount of concurrent high, it will continue to create a new connection, it is easy (especially MySQL) caused TOO
MANY Connections of error occurred.

Since we need to ensure that the maximum number of connections available, then we do not want to set the number of dead too. Hope that free time can maintain a certain number of idle connections idleNum, but they hope that we can limit the maximum number of connections available maxNum.

The first case is complicated by too many circumstances, so if a concurrent too little of it? Now we create a new connection and returned after a long time we no longer use this connection. Then the connection is likely in a few hours or even longer before had established a. We extended period of connection and no way to guarantee its availability. It is possible to get our next connection is no longer valid connection.

Then we can connect from MySQL ripe for use of the pool and Redis database connection pooling library to see how they solve these problems.

Sql Golang standard library connection pool

Golang connection pool achieved under standard library database / sql / sql.go. When we run:

db, err := sql.Open("mysql", "xxxx")

And it will open a connection pool. We can look at the structure of db returned:

type DB struct {
	waitDuration int64 // Total time waited for new connections.
	mu           sync.Mutex // protects following fields
	freeConn     []*driverConn
	connRequests map[uint64]chan connRequest
	nextRequest  uint64 // Next key to use in connRequests.
	numOpen      int    // number of opened and pending open connections
	// Used to signal the need for new connections
	// a goroutine running connectionOpener() reads on this chan and
	// maybeOpenNewConnections sends on the chan (one send per needed connection)
	// It is closed during db.Close(). The close tells the connectionOpener
	// goroutine to exit.
	openerCh          chan struct{}
	closed            bool
	maxIdle           int                    // zero means defaultMaxIdleConns; negative means 0
	maxOpen           int                    // <= 0 means unlimited
	maxLifetime       time.Duration          // maximum amount of time a connection may be reused
	cleanerCh         chan struct{}
	waitCount         int64 // Total number of connections waited for.
	maxIdleClosed     int64 // Total number of connections closed due to idle.
	maxLifetimeClosed int64 // Total number of connections closed due to max free limit.
}

No need to omit some of the above concerns field. We can see, DB connection pool this structure freeConn internal storage connections, not chan we used before, but ** [] driverConn **, a connection slice. We can also see that there are maxIdle other related variables to control the number of free connections. It is worth noting that the initialization function Open function DB and no new database connection. The new connection in which function? We can look all the way back in Query method, we can see that this function: func (db * DB) conn (ctx context.Context, strategy connReuseStrategy) (* driverConn, error). And we get method of connection from the connection pool, start here:

Get connected

// conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
    // 先判断db是否已经关闭。
	db.mu.Lock()
	if db.closed {
		db.mu.Unlock()
		return nil, errDBClosed
	}
	// 注意检测context是否已经被超时等原因被取消。
	select {
	default:
	case <-ctx.Done():
		db.mu.Unlock()
		return nil, ctx.Err()
	}
	lifetime := db.maxLifetime

	// 这边如果在freeConn这个切片有空闲连接的话,就left pop一个出列。注意的是,这边因为是切片操作,所以需要前面需要加锁且获取后进行解锁操作。同时判断返回的连接是否已经过期。
	numFree := len(db.freeConn)
	if strategy == cachedOrNewConn && numFree > 0 {
		conn := db.freeConn[0]
		copy(db.freeConn, db.freeConn[1:])
		db.freeConn = db.freeConn[:numFree-1]
		conn.inUse = true
		db.mu.Unlock()
		if conn.expired(lifetime) {
			conn.Close()
			return nil, driver.ErrBadConn
		}
		// Lock around reading lastErr to ensure the session resetter finished.
		conn.Lock()
		err := conn.lastErr
		conn.Unlock()
		if err == driver.ErrBadConn {
			conn.Close()
			return nil, driver.ErrBadConn
		}
		return conn, nil
	}

	// 这边就是等候获取连接的重点了。当空闲的连接为空的时候,这边将会新建一个request(的等待连接 的请求)并且开始等待
	if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
		// 下面的动作相当于往connRequests这个map插入自己的号码牌。
		// 插入号码牌之后这边就不需要阻塞等待继续往下走逻辑。
		req := make(chan connRequest, 1)
		reqKey := db.nextRequestKeyLocked()
		db.connRequests[reqKey] = req
		db.waitCount++
		db.mu.Unlock()

		waitStart := time.Now()

		// Timeout the connection request with the context.
		select {
		case <-ctx.Done():
			// context取消操作的时候,记得从connRequests这个map取走自己的号码牌。
			db.mu.Lock()
			delete(db.connRequests, reqKey)
			db.mu.Unlock()

			atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))

			select {
			default:
			case ret, ok := <-req:
                // 这边值得注意了,因为现在已经被context取消了。但是刚刚放了自己的号码牌进去排队里面。意思是说不定已经发了连接了,所以得注意归还!
				if ok && ret.conn != nil {
					db.putConn(ret.conn, ret.err, false)
				}
			}
			return nil, ctx.Err()
		case ret, ok := <-req:
            // 下面是已经获得连接后的操作了。检测一下获得连接的状况。因为有可能已经过期了等等。
			atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))

			if !ok {
				return nil, errDBClosed
			}
			if ret.err == nil && ret.conn.expired(lifetime) {
				ret.conn.Close()
				return nil, driver.ErrBadConn
			}
			if ret.conn == nil {
				return nil, ret.err
			}
			ret.conn.Lock()
			err := ret.conn.lastErr
			ret.conn.Unlock()
			if err == driver.ErrBadConn {
				ret.conn.Close()
				return nil, driver.ErrBadConn
			}
			return ret.conn, ret.err
		}
	}
	// 下面就是如果上面说的限制情况不存在,可以创建先连接时候,要做的创建连接操作了。
	db.numOpen++ // optimistically
	db.mu.Unlock()
	ci, err := db.connector.Connect(ctx)
	if err != nil {
		db.mu.Lock()
		db.numOpen-- // correct for earlier optimism
		db.maybeOpenNewConnections()
		db.mu.Unlock()
		return nil, err
	}
	db.mu.Lock()
	dc := &driverConn{
		db:        db,
		createdAt: nowFunc(),
		ci:        ci,
		inUse:     true,
	}
	db.addDepLocked(dc, dc)
	db.mu.Unlock()
	return dc, nil
}

Briefly, DB structure except that the connection is stored slice, also added a queuing mechanism connRequests similar procedure to resolve acquisition waiting connection. At the same time it is determined that the connection has a good balance of health. Since there is so queuing mechanism, when the return connection is how to do it?

Connection release

We can find func (db * DB) putConnDBLocked (dc * driverConn, err error) bool this method directly. Like the comment said, the main purpose of this method are:

Satisfy a connRequest or put the driverConn in the idle pool and
return true or return false.

Our main focus was to look inside a few lines:

...
	// 如果已经超过最大打开数量了,就不需要在回归pool了
	if db.maxOpen > 0 && db.numOpen > db.maxOpen {
		return false
	}
	// 这边是重点了,基本来说就是从connRequest这个map里面随机抽一个在排队等着的请求。取出来后发给他。就不用归还池子了。
	if c := len(db.connRequests); c > 0 {
		var req chan connRequest
		var reqKey uint64
		for reqKey, req = range db.connRequests {
			break
		}
		delete(db.connRequests, reqKey) // 删除这个在排队的请求。
		if err == nil {
			dc.inUse = true
		}
        // 把连接给这个正在排队的连接。
		req <- connRequest{
			conn: dc,
			err:  err,
		}
		return true
	} else if err == nil && !db.closed {
        // 既然没人排队,就看看到了最大连接数目没有。没到就归还给freeConn。
		if db.maxIdleConnsLocked() > len(db.freeConn) {
			db.freeConn = append(db.freeConn, dc)
			db.startCleanerLocked()
			return true
		}
		db.maxIdleClosed++
	}
...

We can see that when the return connection, if there is queuing at the request of not returned to the pool directly to the people queuing up.

Now basically solved in front of that little problem. It will not cause too many connections can not control too many
cases of connections. Also very good to maintain a minimum number of connection pool. But also do the relevant checks of operations for the connected health.
It is noteworthy that, as the code of the standard library, related notes and the code is perfect, really refreshing to see.

redis Golang achieved Redis client

This Golang achieved Redis client, is how to achieve the connection pool. The idea here is very wonderful, you can still learn a lot of good ideas. Of course, due to the relatively small code comments, eating together is still a little confused at first. The relevant code address can be seen in https://github.com/go-redis/redis/blob/master/internal/pool/pool.go.

Connection pool and its structure is as follows

type ConnPool struct {
	...
	queue chan struct{}

	connsMu      sync.Mutex
	conns        []*Conn
	idleConns    []*Conn
	poolSize     int
	idleConnsLen int

	stats Stats

	_closed  uint32 // atomic
	closedCh chan struct{}
}

We can see the structure or slice inside storage connectivity. But we can look at the focus queue, conns, idleConns these variables will be mentioned later. But it is worth noting that! We can see, there are two ** [] Conn ** structure: conns, idleConns, then the question arises:

In the end where there is the connection?

New connection pool

We start to see the beginning of a new connection pool:

func NewConnPool(opt *Options) *ConnPool {
	....
	p.checkMinIdleConns()

	if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 {
		go p.reaper(opt.IdleCheckFrequency)
	}
	....
}

Initialization function and there is a connection pool in front of two different places.

  1. checkMinIdleConns method, the initialization of the connection pool will be filled to the connection pool free connections.
  2. go p.reaper (opt.IdleCheckFrequency) will be initialized when the connection pool will go up a drive connection pool inside out periodically to be eliminated is connected.

Get connected

func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
	if p.closed() {
		return nil, ErrClosed
	}
	
    //这边和前面sql获取连接函数的流程先不同。sql是先看看连接池有没有空闲连接,有的话先获取不到再排队。这边是直接先排队获取令牌,排队函数后面会分析。
	err := p.waitTurn(ctx)
	if err != nil {
		return nil, err
	}
	//前面没出error的话,就已经排队轮候到了。接下来就是获取的流程。
	for {
		p.connsMu.Lock()
        //从空闲连接里面先获取一个空闲连接。
		cn := p.popIdle()
		p.connsMu.Unlock()

		if cn == nil {
            // 没有空闲连接时候直接跳出循环。
			break
		}
		// 判断是否已经过时,是的话close掉了然后继续取出。
		if p.isStaleConn(cn) {
			_ = p.CloseConn(cn)
			continue
		}

		atomic.AddUint32(&p.stats.Hits, 1)
		return cn, nil
	}

	atomic.AddUint32(&p.stats.Misses, 1)
	
    // 如果没有空闲连接的话,这边就直接新建连接了。
	newcn, err := p.newConn(ctx, true)
	if err != nil {
        // 归还令牌。
		p.freeTurn()
		return nil, err
	}

	return newcn, nil
}

We can try to answer that question at the beginning: Where there is connected in the end? The answer from cn: = p.popIdle () this sentence can be seen, this action obtaining a connection, which is obtained from idleConns, and which functions also proved this point. But, really like this thing? Let us look back.

At the same time I understand that:

  1. I mean sql queuing application is connected to the connection pool, the numbers tell their own connection pool. I see a connection there idle, and called my number. I promised a cry, and then connect it directly to a pool connected to me. If I do not return, no connection pool has been called the next number.
  2. redis here mean, I went and connection pooling application is not connected but token. I have been waiting in line, connecting the token pool for me, I went inside the warehouse to find themselves idle connection or create a new connection. In addition to the return run of the connector is connected, the tokens have to be returned. Of course, if I create a new connection wrong, even if I can not get the connection back home, I have to give back the token connection pool, or a token number of connection pools less, the maximum number of connections is also reduced.

and:

func (p *ConnPool) freeTurn() {
	<-p.queue
}
func (p *ConnPool) waitTurn(ctx context.Context) error {
...
	case p.queue <- struct{}{}:
		return nil
...
}

In this chan it is on queue to maintain the number of tokens.

So what is the role of conns it? We can take a look at this new connection function:

New Connection

func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
	cn, err := p.dialConn(ctx, pooled)
	if err != nil {
		return nil, err
	}

	p.connsMu.Lock()
	p.conns = append(p.conns, cn)
	if pooled {
		// 如果连接池满了,会在后面移除。
		if p.poolSize >= p.opt.PoolSize {
			cn.pooled = false
		} else {
			p.poolSize++
		}
	}
	p.connsMu.Unlock()
	return cn, nil
}

The basic logic out. That is, if the new connection, then I will not be placed directly idleConns inside, but first put conns inside. At the same time look at the pond full of no. Full time, then returned back marked, the back will be deleted. Then the back will be deleted, referring to when? It's time to return the connection of the following to say.

Return connection

func (p *ConnPool) Put(cn *Conn) {
	if cn.rd.Buffered() > 0 {
		internal.Logger.Printf("Conn has unread data")
		p.Remove(cn, BadConnError{})
		return
	}
	//这就是我们刚刚说的后面了,前面标记过不要入池的,这边就删除了。当然了,里面也会进行freeTurn操作。
	if !cn.pooled {
        // 这个方法就是前面的标志位,判断里面可以知道,前面标志不要池化的,这里会将它删除。
		p.Remove(cn, nil)
		return
	}

	p.connsMu.Lock()
	p.idleConns = append(p.idleConns, cn)
	p.idleConnsLen++
	p.connsMu.Unlock()
    //我们可以看到很明显的这个归还号码牌的动作。
	p.freeTurn()
}

The answer is, in fact, all the connections are stored in conns this slice inside. If the connection is idle waiting for the state, then it is in their own idleConns inside plus a pointer!

In fact, the process of restitution, that is what I intend to also check this connection, overbooking is not a product, if it is not necessary pooling, and just delete it. Not, that is, the connection itself (a pointer) in idleConns also append it.
And so on, the above logic seems a bit wrong? Let's get a haircut connection process:

First waitTurn, get a token. The number of tokens in the queue is determined inside the pool.
Get a token, to get inside the warehouse idleConns free connections. If not a newConn on their own, and put him to record conns inside.

Run out, call put return: that is, add the pointer from conns connected to idleConns. When returned checks in newConn time it is not already done oversold marked. Yes not transferred to idleConns.

I was puzzled for a long time, since always need to get a token to get connected, the number of tokens is given. Why would oversold it? Source turned a bit, my answer is:
Although the Get method to get connected is newConn this private method, by the token control will not lead to oversold. However, this method accepts parameter passing: pooled bool. So I guess it is worried that other people call this method when it passed just-true, resulting in poolSize growing.

Overall, the number of connections redis control the connection pool, or in the queue I call this token chan operate.

to sum up

Can be seen above, the basic guarantee connection pool, it is to get a secure connection when the thread. But yet to achieve from different angles to achieve many additional features of the time. It is still very interesting. But whether or storage structure is chan or slice, it can be very good to achieve this. If, as with the slice as sql or redis storing connections must maintain a queuing structure to represent the effect.

Above are some of my own thoughts, to share out the welcome to correct me, the way to find a wave of concern, the idea of ​​partnership can comment or private letter I oh ~
Here Insert Picture Description

Published 22 original articles · won praise 7 · views 7606

Guess you like

Origin blog.csdn.net/ZYQZXF/article/details/104574547