redigoを使用してredis proxyが踏んだ穴を共有する

4275 ワード

最近プロジェクトを開発する時、redis関連の穴を踏んで、今みんなに分かち合います.
使用するサードパーティ製ライブラリはredigoで、
接続されたredisアドレスはproxyで、
まず、redigoもredis proxyもいいもので、redigoのソースコードがはっきり見えて、redis proxyも素晴らしいです.ナイフはいいナイフですが、姿勢が悪いと自分を傷つけます.
このピットはredigo接続プールについてです.
RedisPool = &redis.Pool{
		MaxIdle:     5,
		IdleTimeout: 240 * time.Second,
		Dial: func() (redis.Conn, error) {
			c, err := redis.Dial("tcp", address)
			if err != nil {
				println(err)
				return nil, err
			}
			_, err = c.Do("SELECT", db)
			if err != nil {
				println(err)
				return nil, err
			}
			return c, nil
		},
		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			_, err := c.Do("PING")
			if err != nil {
				println(err)
			}
			return err

		},
	}

上図は古いプロジェクトのredigo redis poolですが、どう見ても問題ありません.
MaxIdle:プール内の最大アイドル接続
IdleTimeout:このdurationの空き接続を超えると閉じられます
TestOnBorrow:この接続が健康かどうかを調べる前に
問題はこのDialです
Dial is an application supplied function for creating and configuring a connection.
pool構造体のこのDialは、redis接続を作成し、構成するために使用されます.
ピットはここにあります.redisコマンドを実行するのはconnectionオブジェクトのDoメソッドです.
func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
	c.mu.Lock()
	pending := c.pending
	c.pending = 0
	c.mu.Unlock()

	if cmd == "" && pending == 0 {
		return nil, nil
	}

	if c.writeTimeout != 0 {
		c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
	}

	if cmd != "" {
		if err := c.writeCommand(cmd, args); err != nil {
			return nil, c.fatal(err)
		}
	}

	if err := c.bw.Flush(); err != nil {
		return nil, c.fatal(err)
	}

	if c.readTimeout != 0 {
		c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
	}

	if cmd == "" {
		reply := make([]interface{}, pending)
		for i := range reply {
			r, e := c.readReply()
			if e != nil {
				return nil, c.fatal(e)
			}
			reply[i] = r
		}
		return reply, nil
	}

	var err error
	var reply interface{}
	for i := 0; i <= pending; i++ {
		var e error
		if reply, e = c.readReply(); e != nil {
			return nil, c.fatal(e)
		}
		if e, ok := reply.(Error); ok && err == nil {
			err = e
		}
	}
	return reply, err
}

上記のソースコードでtimeoutの値を入力しないと、デフォルトの0値では、この2つのset deadlineの論理はスキップされます.の
read/write timeoutを設定しないとどのような問題が発生しますか?もしネットワークに変動があれば、redisコマンドを実行するとき、サーバーの応答を受信していないと、今回の要求が戻ってこないので、そこに干してしまいます.Redisサーバが設定したタイムアウト時間になるまで、接続を閉じ、EOFのエラーが表示されます.
  • 単点redisの場合、MaxActiveを設定しないと、redis poolの接続数に上限がなく、問題は露呈しません.これは私たちのサービスにとって、影響も大きくありません.エラーログには、redis関連のEOFログがいくつかありますが、これは本当に大丈夫ですか.もちろん問題がありますが、redisからメッセージを読んでread timeoutを設定していないと、ずっと読めません.この協程はそこに詰まっていて、なかなか応答してくれないので、ユーザーにとってよくありません.
  • クラスタモード、一般redis_Proxyは接続数を制限するのでredis poolはMaxActiveで池の最大接続数を制限すべきで、このときread/write timeoutを設定しないと問題が来て、池の接続はなくなるまで少なくなります.

  • だから、その状況にかかわらず、私たちはredisにあげるべきだ.Dialという方法は,3つのタイムアウト時間,DialConnectTimeout,DialReadTimeout,DialWriteTimeoutを伝達する.
    今使っているredigoのredis poolを添付します.
    redis.Pool{
    			MaxIdle: cf.StatsRedis.MaxIdle,
    			MaxActive: cf.StatsRedis.MaxActive,
    			Dial: func() (redis.Conn, error) {
    				c, err := redis.Dial("tcp", cf.StatsRedis.ProxyAddress,
    					redis.DialConnectTimeout(time.Duration(cf.StatsRedis.ConnectTimeout) * time.Millisecond),
    					redis.DialReadTimeout(time.Duration(cf.StatsRedis.ReadTimeout) * time.Millisecond),
    					redis.DialWriteTimeout(time.Duration(cf.StatsRedis.WriteTimeout) * time.Millisecond))
    				if err != nil {
    					l4g.Error(err)
    					return nil, err
    				}
    				return c, nil
    			},
    			// Use the TestOnBorrow function to check the health of an idle connection
    			// before the connection is returned to the application.
    			TestOnBorrow: func(c redis.Conn, t time.Time) error {
    				if time.Since(t) < time.Minute {
    					return nil
    				}
    				_, err := c.Do("PING")
    				return err
    			},
    			IdleTimeout: 300 * time.Second,
    			// If Wait is true and the pool is at the MaxActive limit,
    			// then Get() waits for a connection to be returned to the pool before returning
    			Wait: true,
    		},
    

    前はずっとredisと思っていた.Dialという方法ではデフォルトのタイムアウト時間がありますが、結果は事実上ないことを証明します.
    コードを書くときは必ず「と思う」ことはできません.
    実はredigoのせいではありません.このライブラリの下部にはgolangのnetライブラリが使われています.default timeoutは0ですね.which is interpreted as「no timeout」...