redis性能チューニング一則
redisは、Webサービス側のデータインタラクション能力を向上させる重要な利点として、それ自体にもオーバーヘッドがあり、redisをより速くするためには、redisとインタラクションする場所の性能最適化が必要である.今日はgolangで有名なredisライブラリ-redigoについてお話しします.そのconn.Do()、Send()、Flush()、Receive()の合理的な使用が必要です.まず、ローカルテストの例を示します.
実験結果:
その結果,同様に1000回のクエリを行い,2番目のメソッドは1番目のメソッドより20倍速くなったことが明らかになった.これはなぜですか.次にその原理を説明する.このときredigoのソースコードを見て、まずこのconnの構造を見てみましょう.
これはredis接続の構造で、私たちは2人のメンバーがいることを発見しました.それぞれ*bufioです.リーダーと*bufio.Writer.そしてこのDo():
真相を探るためにc.writeCommand()の実現を見た.
ここまで、Do()という方法は基本的にSend(),Flush(),Receive()をパッケージ化していることを知っていますが、なぜ2番目のテスト関数はDo()よりずっと速いのでしょうか.なぜなら、私はforループで複数のSend()を実行し、実行する命令をバッファに書き込むことを目的としています.
これはSend()のソースコードですが、実はDo()が最初にやったことと同じようにc.writeCommand(cmd,args)です.違いは,Do()のようにsend,flush,recvを絶えず実行するのではなく,複数の命令がバッファに書き込まれることである.バッファに書いた後、Flush()を統一し、命令をネットワークioに全部書きます.redis serverはpipeliningをサポートしているので、ioの中からもう一つReceiveを出せばいいです.このように見ると、1000個の命令は、私は一度だけネットワーク伝送を行いました.Doを用いると,1000回のネットワーク伝送が実行され,この差は明らかである.
func main(){
_=InitRedis(10,"127.0.0.1","6379","requirepass",false) // redis,
GetRedisKey()
GetRedisKey2()
}
func GetRedisKey() {
now:=time.Now()
conn := redisPool.Get()
defer conn.Close()
for i:=0;i<1000;i++{ // 1000 get
key := "1125"+"test"+strconv.Itoa(i)
//_, err := conn.Do("set", key,"testValue") set
_, err := redis.String(conn.Do("get", key)) // get,
if err!=nil {
fmt.Println(err)
}
//fmt.Println(result)
}
finish1:=time.Since(now) //
fmt.Println(finish1)
}
func GetRedisKey2() {
now:=time.Now()
conn := redisPool.Get()
defer conn.Close()
var count int
for i:=0;i<1000;i++{ // 1000 get
key := "1125"+"test2_"+strconv.Itoa(i)
//err := conn.Send("set", key,"testValue") set
err := conn.Send("get",key) // send, Do
if err!=nil {
fmt.Println(err)
}
count++
}
err := conn.Flush() //
if err != nil {
fmt.Println(err)
}
for i:=0 ; i
_, err := redis.String(conn.Receive()) // get
if err != nil {
fmt.Println(err)
}
}
finish2:=time.Since(now) //
fmt.Println(finish2)
}
実験結果:
80.0561ms //GetRedisKey()
4.0033ms //GetRedisKey2()
その結果,同様に1000回のクエリを行い,2番目のメソッドは1番目のメソッドより20倍速くなったことが明らかになった.これはなぜですか.次にその原理を説明する.このときredigoのソースコードを見て、まずこのconnの構造を見てみましょう.
type conn struct { // Shared mu sync.Mutex pending int err error conn net.Conn // Read readTimeout time.Duration br *bufio.Reader // Write writeTimeout time.Duration bw *bufio.Writer // Scratch space for formatting argument length. // '*' or '$', length, "\r\n" lenScratch [32]byte // Scratch space for formatting integers and floats. numScratch [40]byte }
これはredis接続の構造で、私たちは2人のメンバーがいることを発見しました.それぞれ*bufioです.リーダーと*bufio.Writer.そしてこの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 { // , io.Writer , , , c.writeCommand() 。
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 { // redis server
return nil, c.fatal(e)
}
if e, ok := reply.(Error); ok && err == nil {
err = e
}
}
return reply, err
}
真相を探るためにc.writeCommand()の実現を見た.
func (c *conn) writeCommand(cmd string, args []interface{}) error {
c.writeLen('*', 1+len(args))
if err := c.writeString(cmd); err != nil { // ,
return err
}
for _, arg := range args {
if err := c.writeArg(arg, true); err != nil {
return err
}
}
return nil
}
func (c *conn) writeString(s string) error {
c.writeLen('$', len(s))
c.bw.WriteString(s) // bufio WriteString() ,
_, err := c.bw.WriteString("\r
")
return err
}
ここまで、Do()という方法は基本的にSend(),Flush(),Receive()をパッケージ化していることを知っていますが、なぜ2番目のテスト関数はDo()よりずっと速いのでしょうか.なぜなら、私はforループで複数のSend()を実行し、実行する命令をバッファに書き込むことを目的としています.
func (c *conn) Send(cmd string, args ...interface{}) error {
c.mu.Lock()
c.pending += 1
c.mu.Unlock()
if c.writeTimeout != 0 {
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
}
if err := c.writeCommand(cmd, args); err != nil {
return c.fatal(err)
}
return nil
}
これはSend()のソースコードですが、実はDo()が最初にやったことと同じようにc.writeCommand(cmd,args)です.違いは,Do()のようにsend,flush,recvを絶えず実行するのではなく,複数の命令がバッファに書き込まれることである.バッファに書いた後、Flush()を統一し、命令をネットワークioに全部書きます.redis serverはpipeliningをサポートしているので、ioの中からもう一つReceiveを出せばいいです.このように見ると、1000個の命令は、私は一度だけネットワーク伝送を行いました.Doを用いると,1000回のネットワーク伝送が実行され,この差は明らかである.