いくつかのGolang接続プールの実装ケース

TCP 3ウェイハンドシェイクやその他の理由なので、接続を確立するためには、比較的高コストの動作です。プログラム内の特定のエンティティと対話するために必要とする非常に多くの時間が、我々は繰り返し使用のために再利用することができます接続している接続プールを維持する必要があります。

接続プールを維持しながら、基本的な要件が行うことです:スレッドセーフ(スレッドセーフ)、特にGolangにこの機能がゴルーチン言語です。

簡単な接続のプール

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

この単純な接続プールは、我々は、接続ストレージ池ちゃんに使用しました。方法と新しい構造が比較的簡単です:

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
}

唯一の植物の大きさと、それが接続されているプールに対応する機能を提供する必要があります。

接続しなさい

では、どのようなリソースを導き出す必要がありますか?私たちの内部ストレージ接続の構造はちゃんで、これだけの単純な選択することができます保証スレッドの安全性を必要とするので:

//从资源池里获取一个资源
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()
	}
}

私たちは、この接続解像度は、その後、我々はすでに準備された工場の機能構築の接続を使用しますない場合ちゃんが、中に入るプールを開始します。私たちは、最初の接続プールは、RESから接続を取得するために使用OKの時に閉じられているかどうかを判断します。あなたは既に閉じている場合は、我々は準備された接続が閉じられたエラーだっ既に返されます。

接続プールを閉じます

クローズ接続が挙げプーリングので、まあ、それは我々は、接続プールを閉じる方法ですか?

//关闭资源池,释放资源
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()
	}
}

ここでは、pmLock()ロック操作を行う必要があり、我々は理由読み取りおよび書き込み密閉構造のこれを実行する必要があります。このフラグを配置する必要がありますが設定された後は、もはや新しい接続を取得することができます解像度ちゃん、作るの獲得方法をオフにしません。再び我々ちゃんの内部接続の解像度は、この閉じるアクションを実行します。

接続解除

接続プーリングがまだ閉じられていないことを前提との接続開始]をリリース。接続プールが閉じられた場合には、トリガパニックのような接続内の解像度を送信するために下ります。

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()
	}
}

上記のシンプルかつスレッドセーフの接続プールの実装です。接続プールが達成されているものの、私たちは、今、それを見ることができますが、いくつかの小さな欠点があります:

接続の最大数には制限がスレッドプールが空ならば、我々は直接返された接続新しいデフォルトを作成する必要があり、存在しません。同時高の量は、それが新しい接続を作成していきますと、それはあまりにも引き起こした(特にMySQLの)簡単で
、エラーの多くの接続が発生しました。

我々は、利用可能な接続の最大数は、その後、我々はあまりにも死者の数を設定しないことを確認する必要がありますので。自由な時間がアイドル状態の接続idleNumの一定数を維持することができますが、彼らは我々が接続の最大数利用可能MAXNUMを制限することができますことを願っていますことを願っています。

最初のケースはそう同時場合が少なすぎることの、あまりにも多くの状況によって複雑になりますか?今、私たちは、新しい接続を作成し、我々はもはやこの接続を使用久しぶりに戻りました。その後、接続は数時間で可能性があるか、さらに長い前に確立していました。私たちは、接続とその可用性を保証する方法の期間を延長しました。私たちの次の接続はもはや有効な接続で取得しないことが可能です。

その後、我々は、彼らがこれらの問題を解決する方法を確認するために、プールやRedisのデータベース接続プーリングライブラリを使用するためのMySQL熟しから接続することができます。

SQL Golang標準ライブラリの接続プール

標準ライブラリデータベース/ SQL / sql.go.の下で達成Golang接続プール 我々は実行すると:

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

そして、それは、接続プールを開きます。私たちは、返されたDBの構造を見ることができます。

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.
}

上記の懸念フィールドの一部を省略する必要はありません。私たちは、私たちは前に使用されますが、** [] driverConn **、接続スライスこの構造はfreeConn内部ストレージ接続、ないちゃん、DB接続プールを見ることができます。また、空き接続の数を制御するmaxIdle他の関連する変数があることがわかります。これは、初期化関数のオープン機能DBなし新しいデータベース接続ことは注目に値します。どの機能で新しい接続?FUNC(デシベル* DB)CONN(CTX context.Context、戦略connReuseStrategy)(* driverConn、エラー):我々は、この機能を見ることができ、クエリーメソッド内のすべての方法の背中を見ることができます。そして、私たちはここから、接続プールからの接続の方法を取得します:

接続しなさい

// 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
}

接続は、接続を待っている解決取得にキューイング機構connRequests同様の手順を追加し、スライスを格納されていることを除いて簡単に説明すると、DB構造。同時に、接続が健康の良好なバランスを持っていると判断されます。その機構がキューイングされているので、リターン接続があるときにどのようにそれを行うには?

接続解除

私たちは、FUNC(デシベル* DB)putConnDBLocked(DC * driverConn、ERRエラー)がこのメソッドを直接BOOL見つけることができます。コメントが言ったように、この方法の主な目的は以下のとおりです。

満足connRequestまたはアイドルプールにdriverConnを入れて、
真または偽のリターンを返します。

私たちの主な焦点は、数行の内部を見ることでした。

...
	// 如果已经超过最大打开数量了,就不需要在回归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++
	}
...

直接キューイング人にプールに戻さないの要請でキューイングがある場合私たちは、ときリターン接続することを確認することができます。

今、基本的にその小さな問題の前に解決しました。それはあまりにも多くの接続は、あまりにも多くのコントロールすることができないことはありません
接続の例を。また、非常に良好な接続プールの最小数を維持します。しかし、また、接続されている健康のための操作の関連チェックを行います。
標準ライブラリ、関連ノートとコードのコードが完全であるように、それは本当に見てスッキリ、ことは注目に値します。

RedisのGolangはRedisのクライアントを達成しました

このGolang達成Redisのクライアントは、接続プールを達成する方法です。ここでの考え方は非常に素晴らしいです、あなたはまだ良いアイデアの多くを学ぶことができます。もちろん、比較的小さなコードのコメントに、一緒に食べることはまだ最初に混乱少しです。関連するコードアドレスはhttps://github.com/go-redis/redis/blob/master/internal/pool/pool.goで見ることができます。

次のように接続プールとその構造があります

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{}
}

私たちは、ストレージ接続の内部構造やスライスを見ることができます。しかし、我々は、これらの変数は後述するフォーカスキュー、conns、idleConnsで見ることができます。しかし、それはそれを注目する価値があります!私たちが見ることができる、2 ** []コネティカット**は構造があります:conns、idleConnsは、その後、疑問が生じます:

接続が確立されて最終的には?

新しい接続プール

私たちは、新しい接続プールの始まりを見て起動します。

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

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

機能の初期化と接続プールは、2つの異なる場所の目の前にあります。

  1. checkMinIdleConns方法は、接続プールの初期化は、接続プールの空き接続に入力されます。
  2. 接続プールが接続されて排除されるために、定期的にドライブの接続プール内のを上がるときに行くp.reaper(opt.IdleCheckFrequency)が初期化されます。

接続しなさい

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
}

最後にそこに接続されている:私たちは、初めにその質問に答えることを試みることができますか?CNからの答えは:= p.popIdle(この文を見ることができる)、idleConnsから得られ、そして機能する接続を得るこの動作は、この点を証明しました。しかし、実際にこの事を好きですか?私たちは戻って見てみましょう。

同時に、私はそれを理解します:

  1. 私は、SQLキューイングアプリケーションが接続プールに接続されている意味し、数字は独自の接続プールを伝えます。私はアイドルそこに接続を見て、私の番号を呼ばれます。私は叫びを約束し、その後、私に接続プールに直接接続してください。私は返さない場合は、接続プールは、次の番号と呼ばれていません。
  2. ここでのRedisのは、私が行って、コネクションプーリングアプリケーションが接続されているがトークンされていない、意味します。私は私のためにトークンプールを結ぶ、並んで待っていた、私は自分自身にアイドル状態の接続を見つけたり、新しい接続を作成するために、倉庫内で行ってきました。コネクタのリターン実行接続されていることに加えて、トークンが返されることがあります。私は、新しい接続が間違っを作成し、私は接続のバックホームを取得できない場合でも、私はトークン接続プール、以下の接続プールのトークン数をお返しする必要がある場合はもちろん、最大接続数も削減されます。

そして、:

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

このちゃんでは、トークンの数を維持するために、キューにあります。

だから、connsの役割は何ですか?私たちは、この新しい接続機能を見てみることができます。

新しい接続

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
}

基本的なロジックアウト。それは、新しい接続が、その後、私は直接idleConnsの内側に配置されることはありません場合は、ですが、内部の最初のプット・conns。なしの完全な池で、同じ時間を見ました。フルタイムは、その後、マーク戻さ、背中が削除されます。当時は、ときに参照、削除されますか?それは言うために、次の接続を返すために時間です。

リターン接続

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()
}

その答えは、実際には、すべての接続が、このスライス内のconnsに格納されています。接続が状態のためのアイドル待ち時間がある場合、それは自分自身の内部idleConnsプラスポインタであります!

それが必要プーリングではなく、それを削除した場合、実際には、私もこの接続を確認するつもり何反発のプロセスは、オーバーブッキングは、製品ではありません。、つまり、idleConnsで接続自体(ポインタ)も、それを追加しません。
ように、上記のロジックは、ビット間違っているようですか?さんが散髪接続プロセスを取得してみましょう:

まずwaitTurnは、トークンを取得します。キュー内のトークンの数は、プール内で決定されます。
倉庫idleConns内の空き接続を取得するには、トークンを取得します。内部connsを記録するために彼を入れていない場合は自分でnewConn、と。

、コール・プット・リターンを実行します:である、idleConnsに接続connsからポインタを追加します。newConn時間にチェックを返されたとき、それはすでに売られ過ぎマーク行われていません。はいidleConnsに転送されません。

常時接続して取得するトークンを取得する必要があるため、私は、長い時間のために困惑して、トークンの数が与えられています。なぜそれを売られ過ぎでしょうか?ソースは少し回し、私の答えは:
接続を取得するためのGetメソッドがnewConnは売られ過ぎにつながることはありませんトークン制御により、このプライベートな方法ではあるが。プールされたブール値を:しかし、この方法では、パラメーターの受け渡し受け付けます。私が推測するように、他の人々はそれが成長しているプールサイズで、その結果、ちょうど真渡され、このメソッドを呼び出すことを心配しています。

全体的に、接続のRedisの数は、接続プールを制御する、またはキューに私が動作し、このトークンちゃん呼んでいます。

概要

基本的な保証接続プール、上に見られることができ、それは時にスレッド安全な接続を取得することです。しかし、まだ時間の多くの追加機能を達成するために、異なる角度から実現しています。それはまだ非常に興味深いです。しかしちゃんやスライスかどうかのストレージ構造があり、これを達成するために非常に良いことができます。接続を格納するSQLまたはRedisのようなスライスと同様の効果を表現するためにキューイング構造を維持する必要があり、場合。

上記の私を修正するために歓迎を共有するために私自身の考えのいくつかは、ある、関心の波を見つける方法は、パートナーシップのアイデアは、コメントやプライベートの手紙できるIああ〜
ここに画像を挿入説明

公開された22元の記事 ウォン称賛7 ビュー7606

おすすめ

転載: blog.csdn.net/ZYQZXF/article/details/104574547