redis實(shí)踐及思考-創(chuàng)新互聯(lián)

導(dǎo)語(yǔ):當(dāng)面臨存儲(chǔ)選型時(shí)是選擇關(guān)系型還是非關(guān)系型數(shù)據(jù)庫(kù)? 如果選擇了非關(guān)系型的redis,redis常用數(shù)據(jù)類型占用內(nèi)存大小如何估算的? redis的性能瓶頸又在哪里?

創(chuàng)新互聯(lián)網(wǎng)站建設(shè)公司一直秉承“誠(chéng)信做人,踏實(shí)做事”的原則,不欺瞞客戶,是我們最起碼的底線! 以服務(wù)為基礎(chǔ),以質(zhì)量求生存,以技術(shù)求發(fā)展,成交一個(gè)客戶多一個(gè)朋友!專注中小微企業(yè)官網(wǎng)定制,成都網(wǎng)站建設(shè)、網(wǎng)站制作,塑造企業(yè)網(wǎng)絡(luò)形象打造互聯(lián)網(wǎng)企業(yè)效應(yīng)。
背景
前段時(shí)間接手了一個(gè)業(yè)務(wù),響應(yīng)時(shí)間達(dá)到 10s左右。 閱讀源碼后發(fā)現(xiàn),每一次請(qǐng)求都是查詢多個(gè)分表數(shù)據(jù)(task1,task2….),然后再join其他表(course,teacher..), 時(shí)間全部花在了大量磁盤(pán)I/O上。 腦袋一拍,重構(gòu),上redis!
為什么選擇redis
拍腦袋做技術(shù)方案肯定是不行的,得用數(shù)據(jù)和邏輯說(shuō)服別人才可以。

時(shí)延

時(shí)延=后端發(fā)起請(qǐng)求db(用戶態(tài)拷貝請(qǐng)求到內(nèi)核態(tài))+ 網(wǎng)絡(luò)時(shí)延 + 數(shù)據(jù)庫(kù)尋址和讀取 如果想要降低時(shí)延,只能減少請(qǐng)求數(shù)(合并多個(gè)后端請(qǐng)求)和減少數(shù)據(jù)庫(kù)尋址和讀取得時(shí)間。 從降低時(shí)延的角度,基于 單線程和內(nèi)存的redis,每秒10萬(wàn)次得讀寫(xiě)性能肯定遠(yuǎn)遠(yuǎn)勝過(guò)磁盤(pán)讀寫(xiě)性能。

數(shù)據(jù)規(guī)模

以redis一組K-V為例(”hello” -> “world”),一個(gè)簡(jiǎn)單的set命令最終會(huì)產(chǎn)生4個(gè)消耗內(nèi)存的結(jié)構(gòu)。

redis實(shí)踐及思考

關(guān)于Redis數(shù)據(jù)存儲(chǔ)的細(xì)節(jié),又要涉及到內(nèi)存分配器(如jemalloc),簡(jiǎn)單說(shuō)就是存儲(chǔ)170字節(jié),其實(shí)內(nèi)存分配器會(huì)分配192字節(jié)存儲(chǔ)。

redis實(shí)踐及思考

那么總的花費(fèi)就是

  • 一個(gè)dictEntry,24字節(jié),jemalloc會(huì)分配32字節(jié)的內(nèi)存塊

  • 一個(gè)redisObject,16字節(jié),jemalloc會(huì)分配16字節(jié)的內(nèi)存塊

  • 一個(gè)key,5字節(jié),所以SDS(key)需要5+9=14個(gè)字節(jié),jemalloc會(huì)分配16字節(jié)的內(nèi)存塊

  • 一個(gè)value,5字節(jié),所以SDS(value)需要5+9=14個(gè)字節(jié),jemalloc會(huì)分配16字節(jié)的內(nèi)存塊

綜上,一個(gè)dictEntry需要32+16+16+16=80個(gè)字節(jié)。

上面這個(gè)算法只是舉個(gè)例子,想要更深入計(jì)算出redis所有數(shù)據(jù)結(jié)構(gòu)的內(nèi)存大小,可以參考 這篇文章 。 筆者使用的是哈希結(jié)構(gòu),這個(gè)業(yè)務(wù)需求大概一年的數(shù)據(jù)量是200MB,從使用redis成本上考慮沒(méi)有問(wèn)題。

需求特點(diǎn)

筆者這個(gè)需求背景讀多寫(xiě)少,冷數(shù)據(jù)占比比較大,但數(shù)據(jù)結(jié)構(gòu)又很復(fù)雜(涉及多個(gè)維度數(shù)據(jù)總和),因此只要啟動(dòng)定時(shí)任務(wù)離線增量寫(xiě)入redis,請(qǐng)求到達(dá)時(shí)直接讀取redis中的數(shù)據(jù),無(wú)疑可以減少響應(yīng)時(shí)間。

redis實(shí)踐及思考 [ 最終方案 ]
redis瓶頸和優(yōu)化

HGETALL

最終存儲(chǔ)到redis中的數(shù)據(jù)結(jié)構(gòu)如下圖。

redis實(shí)踐及思考

采用同步的方式對(duì)三個(gè)月(90天)進(jìn)行HGETALL操作,每一天花費(fèi)30ms,90次就是2700ms! redis操作讀取應(yīng)該是ns級(jí)別的,怎么會(huì)這么慢? 利用多核cpu計(jì)算會(huì)不會(huì)更快?

redis實(shí)踐及思考 常識(shí)告訴我,redis指令執(zhí)行速度 >> 網(wǎng)絡(luò)通信(內(nèi)網(wǎng)) > read/write等系統(tǒng)調(diào)用。 因此這里其實(shí)是I/O密集型場(chǎng)景,就算利用多核cpu,也解決不到根本的問(wèn)題,最終影響redis性能, **其實(shí)是網(wǎng)卡收發(fā)數(shù)據(jù) 用戶態(tài)內(nèi)核態(tài)數(shù)據(jù)拷貝 **

pipeline

這個(gè)需求qps很小,所以網(wǎng)卡也不是瓶頸了,想要把需求優(yōu)化到1s以內(nèi),減少I/O的次數(shù)是關(guān)鍵。 換句話說(shuō), 充分利用帶寬,增大系統(tǒng)吞吐量。

于是我把代碼改了一版,原來(lái)是90次I/O,現(xiàn)在通過(guò)redis pipeline操作,一次請(qǐng)求半個(gè)月,那么3個(gè)月就是6次I/O。 很開(kāi)心,時(shí)間一下子少了1000ms。

redis實(shí)踐及思考 redis實(shí)踐及思考

pipeline攜帶的命令數(shù)

代碼寫(xiě)到這里,我不經(jīng)反問(wèn)自己,為什么一次pipeline攜帶15個(gè)HGETALL命令,不是30個(gè),不是40個(gè)? 換句話說(shuō),一次pipeline攜帶多少個(gè)HGETALL命令才會(huì)發(fā)起一次I/O?

我使用是golang的 redisgo  的客戶端,翻閱源碼發(fā)現(xiàn),redisgo執(zhí)行pipeline邏輯是 把命令和參數(shù)寫(xiě)到golang原生的bufio中,如果超過(guò)bufio默認(rèn)大值(4096字節(jié)),就發(fā)起一次I/O,flush到內(nèi)核態(tài)。

redis實(shí)踐及思考

redisgo編碼pipeline規(guī)則 如下圖, *表示后面參數(shù)加命令的個(gè)數(shù),$表示后面的字符長(zhǎng)度 ,一條HGEALL命令實(shí)際占45字節(jié)。

那其實(shí)90天數(shù)據(jù),一次I/O就可以搞定了(90 * 45 < 4096字節(jié))!

redis實(shí)踐及思考

果然,又快了1000ms,耗費(fèi)時(shí)間達(dá)到了1秒以內(nèi)

redis實(shí)踐及思考

對(duì)吞吐量和qps的取舍

筆者需求任務(wù)算是完成了,可是再進(jìn)一步思考,redis的pipeline一次性帶上多少HGETALL操作的key才是合理的呢? 換句話說(shuō),服務(wù)器吞吐量大了,可能就會(huì)導(dǎo)致qps急劇下降(網(wǎng)卡大量收發(fā)數(shù)據(jù)和redis內(nèi)部協(xié)議解析,redis命令排隊(duì)堆積,從而導(dǎo)致的緩慢),而想要qps高,服務(wù)器吞吐量可能就要降下來(lái),無(wú)法很好的利用帶寬。 對(duì)兩者之間的取舍,同樣是不能拍腦袋決定的,用壓測(cè)數(shù)據(jù)說(shuō)話!

簡(jiǎn)單寫(xiě)了一個(gè)壓測(cè)程序,通過(guò)比較請(qǐng)求量和qps的關(guān)系,來(lái)看一下吞吐量和qps的變化,從而選擇一個(gè)適合業(yè)務(wù)需求的值。

package main
import (
    "crypto/rand"
    "fmt"
    "math/big"
    "strconv"
    "time"
    "github.com/garyburd/redigo/redis"
)
const redisKey = "redis_test_key:%s"
func main() {
    for i := 1; i < 10000; i++ {
        testRedisHGETALL(getPreKeyAndLoopTime(i))
    }
}
func testRedisHGETALL(keyList [][]string) {
    Conn, err := redis.Dial("tcp", "127.0.0.1:6379")
    if err != nil {
        fmt.Println(err)
        return
    }
    costTime := int64(0)
    start := time.Now().Unix()
    for _, keys := range keyList {
        for _, key := range keys {
            Conn.Send("HGETALL", fmt.Sprintf(redisKey, key))
        }
        Conn.Flush()
    }
    end := time.Now().Unix()
    costTime = end - start
    fmt.Printf("cost_time=[%+v]ms,qps=[%+v],keyLen=[%+v],totalBytes=[%+v]",
        1000*int64(len(keyList))/costTime, costTime/int64(len(keyList)), len(keyList), len(keyList)*len(keyList[0])*len(redisKey))
}
//根據(jù)key的長(zhǎng)度,設(shè)置不同的循環(huán)次數(shù),平均計(jì)算,取除網(wǎng)絡(luò)延遲帶來(lái)的影響
func getPreKeyAndLoopTime(keyLen int) [][]string {
    loopTime := 1000
    if keyLen < 10 {
        loopTime *= 100
    } else if keyLen < 100 {
        loopTime *= 50
    } else if keyLen < 500 {
        loopTime *= 10
    } else if keyLen < 1000 {
        loopTime *= 5
    }
    return generateKeys(keyLen, loopTime)
}
func generateKeys(keyLen, looTime int) [][]string {
    keyList := make([][]string, 0)
    for i := 0; i < looTime; i++ {
        keys := make([]string, 0)
        for i := 0; i < keyLen; i++ {
            result, _ := rand.Int(rand.Reader, big.NewInt(100))
            keys = append(keys, strconv.FormatInt(result.Int64(), 10))
        }
        keyList = append(keyList, keys)
    }
    return keyList
}
windows上單機(jī)版redis結(jié)果如下: redis實(shí)踐及思考
擴(kuò)展 (分布式方案下pipeline操作)
需求最終是完成了,可是轉(zhuǎn)念一想,現(xiàn)在都是集群版的redis,pipeline批量請(qǐng)求的key可能分布在不同的機(jī)器上,但pipeline請(qǐng)求最終可能只被一臺(tái)redis server處理,那不就是會(huì)讀取數(shù)據(jù)失敗嗎? 于是,筆者查找?guī)讉€(gè)通用的redis 分布式方案,看看他們是如何處理這pipeline問(wèn)題的。

redis cluster

redis cluster 是官方給出的分布式方案。 Redis Cluster在設(shè)計(jì)中沒(méi)有使用一致性哈希,而是使用數(shù)據(jù)分片(Sharding)引入哈希槽(hash slot)來(lái)實(shí)現(xiàn)。 一個(gè) Redis Cluster包含16384(0~16383)個(gè)哈希槽,存儲(chǔ)在Redis Cluster中的所有鍵都會(huì)被映射到這些slot中,集群中的每個(gè)鍵都屬于這16384個(gè)哈希槽中的一個(gè),集群使用公式slot=CRC16 key/16384來(lái)計(jì)算key屬于哪個(gè)槽。 比如redis cluster有5個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)就負(fù)責(zé)一部分哈希槽, 如果參數(shù)的多個(gè)key在不同的slot,在不同的主機(jī)上,那么必然會(huì)出錯(cuò)。
因此redis cluster分布式方案是不支持pipeline操作,如果想要做,只有客戶端緩存slot和redis節(jié)點(diǎn)的關(guān)系,在批量請(qǐng)求時(shí),就通過(guò)key算出不同的slot以及redis節(jié)點(diǎn),并行的進(jìn)行pipeline。

github.com/go-redis就是這樣做的,有興趣可以閱讀下源碼。

redis實(shí)踐及思考

codis

市面上還流行著一種在客戶端和服務(wù)端之間增設(shè)代理的方案,比如codis就是這樣。 對(duì)于上層應(yīng)用來(lái)說(shuō),連接 Codis-Proxy 和直接連接 原生的 Redis-Server 沒(méi)有的區(qū)別,也就是說(shuō)codis-proxy會(huì)幫你做上面并行分槽請(qǐng)求redis server,然后合并結(jié)果在一起的操作,對(duì)于使用者來(lái)說(shuō)無(wú)感知。
總結(jié)
在做需求的過(guò)程中,發(fā)現(xiàn)了很多東西不能拍腦袋決定,而是前期做技術(shù)方案的時(shí)候,想清楚,調(diào)研好,用數(shù)據(jù)和邏輯去說(shuō)服自己。

標(biāo)題名稱:redis實(shí)踐及思考-創(chuàng)新互聯(lián)
網(wǎng)頁(yè)鏈接:http://muchs.cn/article30/djegpo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供企業(yè)建站、標(biāo)簽優(yōu)化、自適應(yīng)網(wǎng)站、商城網(wǎng)站、App開(kāi)發(fā)、企業(yè)網(wǎng)站制作

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

手機(jī)網(wǎng)站建設(shè)