溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

發(fā)布時(shí)間:2022-05-20 12:32:42 來(lái)源:億速云 閱讀:319 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

    使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

    本文介紹了緩存的常見(jiàn)使用場(chǎng)景、選型以及注意點(diǎn),比較有價(jià)值。
    譯自:Implementing robust in-memory cache with Go

    內(nèi)存型緩存是一種以消費(fèi)內(nèi)存為代價(jià)換取應(yīng)用性能和彈性的方式,同時(shí)也推遲了數(shù)據(jù)的一致性。在使用內(nèi)存型緩存時(shí)需要注意并行更新、錯(cuò)誤緩存、故障轉(zhuǎn)移、后臺(tái)更新、過(guò)期抖動(dòng),以及緩存預(yù)熱和轉(zhuǎn)換等問(wèn)題。

    由來(lái)

    緩存是提升性能的最便捷的方式,但緩存不是萬(wàn)能的,在某些場(chǎng)景下,由于事務(wù)或一致性的限制,你無(wú)法重復(fù)使用某個(gè)任務(wù)的結(jié)果。緩存失效是計(jì)算機(jī)科學(xué)中最常見(jiàn)的兩大難題之一。

    如果將操作限制在不變的數(shù)據(jù)上,則無(wú)需擔(dān)心緩存失效。此時(shí)緩存僅用于減少網(wǎng)絡(luò)開銷。然而,如果需要與可變數(shù)據(jù)進(jìn)行同步,則必須關(guān)注緩存失效的問(wèn)題。

    最簡(jiǎn)單的方式是基于TTL來(lái)設(shè)置緩存失效。雖然這種方式看起來(lái)遜于基于事件的緩存失效方式,但它簡(jiǎn)單且可移植性高。由于無(wú)法保證事件能夠即時(shí)傳遞,因此在最壞的場(chǎng)景中(如事件代理短時(shí)間下線或過(guò)載),事件甚至還不如TTL精確。

    短TTL通常是性能和一致性之間的一種折衷方式。它可以作為一道屏障來(lái)降低高流量下到數(shù)據(jù)源的負(fù)載。

    Demo應(yīng)用

    下面看一個(gè)簡(jiǎn)單的demo應(yīng)用,它接收帶請(qǐng)求參數(shù)的URL,并根據(jù)請(qǐng)求參數(shù)返回一個(gè)JSON對(duì)象。由于數(shù)據(jù)存儲(chǔ)在數(shù)據(jù)庫(kù)中,因此整個(gè)交互會(huì)比較慢。

    下面將使用一個(gè)名為plt的工具對(duì)應(yīng)用進(jìn)行壓測(cè),plt包括參數(shù):

    • cardinality - 生成的唯一的URLs的數(shù)據(jù),會(huì)影響到緩存命中率

    • group - 一次性發(fā)送的URL相似的請(qǐng)求個(gè)數(shù),模擬對(duì)相同鍵的并發(fā)訪問(wèn)。

    go run ./cmd/cplt --cardinality 10000 --group 100 --live-ui --duration 10h --rate-limit 5000 curl --concurrency 200 -X 'GET'   'http://127.0.0.1:8008/hello?name=World&locale=ru-RU'   -H 'accept: application/json'

    上述命令會(huì)啟動(dòng)一個(gè)client,循環(huán)發(fā)送10000個(gè)不同的URLs,每秒發(fā)送5000個(gè)請(qǐng)求,最大并發(fā)數(shù)為200。每個(gè)URL會(huì)以100個(gè)請(qǐng)求為批次將進(jìn)行發(fā)送,用以模仿單個(gè)資源的并發(fā),下面展示了實(shí)時(shí)數(shù)據(jù):

    怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

    Demo應(yīng)用通過(guò)CACHE環(huán)境變量定義了三種操作模式:

    • none:不使用緩存,所有請(qǐng)求都會(huì)涉及數(shù)據(jù)庫(kù)

    • naive:使用簡(jiǎn)單的map,TTL為3分鐘

    • advanced:使用github.com/bool64/cache 庫(kù),實(shí)現(xiàn)了很多特性來(lái)提升性能和彈性,TTL也是3分鐘。

    Demo應(yīng)用的代碼位于:github.com/vearutop/cache-story,可以使用make start-deps run命令啟動(dòng)demo應(yīng)用。

    在不使用緩存的條件下,最大可以達(dá)到500RPS,在并發(fā)請(qǐng)求達(dá)到130之后DB開始因?yàn)?Too many connections而阻塞,這種結(jié)果不是最佳的,雖然并不嚴(yán)重,但需要提升性能。

    怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

    使用advanced緩存的結(jié)果如下,吞吐量提升了60倍,并降低了請(qǐng)求延遲以及DB的壓力:

    怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

    go run ./cmd/cplt --cardinality 10000 --group 100 --live-ui --duration 10h curl --concurrency 100 -X 'GET'   'http://127.0.0.1:8008/hello?name=World&locale=ru-RU'   -H 'accept: application/json'
    Requests per second: 25064.03
    Successful requests: 15692019
    Time spent: 10m26.078s
    Request latency percentiles:
    99%: 28.22ms
    95%: 13.87ms
    90%: 9.77ms
    50%: 2.29ms

    字節(jié) VS 結(jié)構(gòu)體

    哪個(gè)更佳?

    取決于使用場(chǎng)景,字節(jié)緩存([]byte)的優(yōu)勢(shì)如下:

    • 數(shù)據(jù)不可變,在訪問(wèn)數(shù)據(jù)時(shí)需要進(jìn)行解碼

    • 由于內(nèi)存碎片較少,使用的內(nèi)存也較少

    • 對(duì)垃圾回收友好,因?yàn)闆](méi)有什么需要遍歷的

    • 便于在線路上傳輸

    • 允許精確地限制內(nèi)存

    字節(jié)緩存的最大劣勢(shì)是編解碼帶來(lái)的開銷,在熱點(diǎn)循環(huán)中,編解碼導(dǎo)致的開銷可能會(huì)非常大。

    結(jié)構(gòu)體的優(yōu)勢(shì):

    • 在訪問(wèn)數(shù)據(jù)時(shí)無(wú)需進(jìn)行編碼/解碼

    • 更好地表達(dá)能力,可以緩存那些無(wú)法被序列化的內(nèi)容

    結(jié)構(gòu)體緩存的劣勢(shì):

    • 由于結(jié)構(gòu)體可以方便地進(jìn)行修改,因此可能會(huì)被無(wú)意間修改

    • 結(jié)構(gòu)體的內(nèi)存相對(duì)比較稀疏

    • 如果使用了大量長(zhǎng)時(shí)間存在的結(jié)構(gòu)體,GC可能會(huì)花費(fèi)一定的時(shí)間進(jìn)行遍歷,來(lái)確保這些結(jié)構(gòu)體仍在使用中,因此會(huì)對(duì)GC采集器造成一定的壓力

    • 幾乎無(wú)法限制緩存實(shí)例的總內(nèi)存,動(dòng)態(tài)大小的項(xiàng)與其他所有項(xiàng)一起存儲(chǔ)在堆中。

    本文使用了結(jié)構(gòu)體緩存。

    Native 緩存

    使用了互斥鎖保護(hù)的map。當(dāng)需要檢索一個(gè)鍵的值時(shí),首先查看緩存中是否存在該數(shù)據(jù)以及有沒(méi)有過(guò)期,如果不存在,則需要從數(shù)據(jù)源構(gòu)造該數(shù)據(jù)并將其放到緩存中,然后返回給調(diào)用者。

    整個(gè)邏輯比較簡(jiǎn)單,但某些缺陷可能會(huì)導(dǎo)致嚴(yán)重的問(wèn)題。

    并發(fā)更新

    當(dāng)多個(gè)調(diào)用者同時(shí)miss相同的鍵時(shí),它們會(huì)嘗試構(gòu)建數(shù)據(jù),這可能會(huì)導(dǎo)致死鎖或因?yàn)榫彺娌忍?dǎo)致資源耗盡。此外如果調(diào)用者嘗試構(gòu)建值,則會(huì)造成額外的延遲。

    如果某些構(gòu)建失敗,即使緩存中可能存在有效的值,此時(shí)父調(diào)用者也會(huì)失敗。

    怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

    可以使用低cardinality和高group來(lái)模擬上述問(wèn)題:

    go run ./cmd/cplt --cardinality 100 --group 1000 --live-ui --duration 10h --rate-limit 5000 curl --concurrency 150 -X 'GET'   'http://127.0.0.1:8008/hello?name=World&locale=ru-RU'   -H 'accept: application/json'

    怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

    上圖展示了使用naive緩存的應(yīng)用,藍(lán)色標(biāo)志標(biāo)識(shí)重啟并使用advanced緩存。可以看到鎖嚴(yán)重影響了性能(Incoming Request Latency)和資源使用(DB Operation Rate)。

    一種解決方案是阻塞并行構(gòu)建,這樣每次只能進(jìn)行一個(gè)構(gòu)建。但如果有大量并發(fā)調(diào)用者請(qǐng)求各種鍵,則可能會(huì)導(dǎo)致嚴(yán)重的鎖競(jìng)爭(zhēng)。

    更好的方式是對(duì)每個(gè)鍵的構(gòu)建單獨(dú)加鎖,這樣某個(gè)調(diào)用者就可以獲取鎖并執(zhí)行構(gòu)建,其他調(diào)用者則等待構(gòu)建好的值即可。

    怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

    后臺(tái)更新

    當(dāng)緩存過(guò)期時(shí),需要一個(gè)新的值,構(gòu)建新值可能會(huì)比較慢。如果同步進(jìn)行,則可以減慢尾部延遲(99%以上)??梢蕴崆皹?gòu)建那些被高度需要的緩存項(xiàng)(甚至在數(shù)據(jù)過(guò)期前)。如果可以容忍老數(shù)據(jù),也可以繼續(xù)使用這些數(shù)據(jù)。

    這種場(chǎng)景下,可以使用老的/即將過(guò)期的數(shù)據(jù)提供服務(wù),并在后臺(tái)進(jìn)行更新。需要注意的是,如果構(gòu)建依賴父上下文,則在使用完老數(shù)據(jù)之后可能會(huì)取消上下文(如滿足父HTTP請(qǐng)求),如果我們使用這類上下文來(lái)訪問(wèn)數(shù)據(jù),則會(huì)得到一個(gè)context canceled錯(cuò)誤。

    解決方案是將上下文與父上下文進(jìn)行分離,并忽略父上下文的取消行為。

    另外一種策略是主動(dòng)構(gòu)建那些即將過(guò)期的緩存項(xiàng),而無(wú)需父請(qǐng)求,但這樣可能會(huì)因?yàn)橐恢碧蕴切o(wú)人關(guān)心的緩存項(xiàng)而導(dǎo)致資源浪費(fèi)。

    同步過(guò)期

    假設(shè)啟動(dòng)了一個(gè)使用TTL緩存的實(shí)例,由于此時(shí)緩存是空的,所有請(qǐng)求都會(huì)導(dǎo)致緩存miss并創(chuàng)建值。這樣會(huì)導(dǎo)致數(shù)據(jù)源負(fù)載突增,每個(gè)保存的緩存項(xiàng)的過(guò)期時(shí)間都非常接近。一旦超過(guò)TTL,大部分緩存項(xiàng)幾乎會(huì)同步過(guò)期,這樣會(huì)導(dǎo)致一個(gè)新的負(fù)載突增,更新后的值也會(huì)有一個(gè)非常接近的過(guò)期時(shí)間,以此往復(fù)。

    這種問(wèn)題常見(jiàn)于熱點(diǎn)緩存項(xiàng),最終這些緩存項(xiàng)會(huì)同步更新,但需要花費(fèi)一段時(shí)間。

    對(duì)這種問(wèn)題的解決辦法是在過(guò)期時(shí)間上加抖動(dòng)。

    如果過(guò)期抖動(dòng)為10%,意味著,過(guò)期時(shí)間為0.95 * TTL1.05 * TTL。雖然這種抖動(dòng)幅度比較小,但也可以幫助降低同步過(guò)期帶來(lái)的問(wèn)題。

    下面例子中,使用高cardinality 和高concurrency模擬這種情況。它會(huì)在短時(shí)間內(nèi)請(qǐng)求大量表項(xiàng),以此構(gòu)造過(guò)期峰值。

    go run ./cmd/cplt --cardinality 10000 --group 1 --live-ui --duration 10h --rate-limit 5000 curl --concurrency 200 -X 'GET' 'http://127.0.0.1:8008/hello?name=World&locale=ru-RU' -H 'accept: application/json'

    怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

    從上圖可以看出,使用naive緩存無(wú)法避免同步過(guò)期問(wèn)題,藍(lán)色標(biāo)識(shí)符表示重啟服務(wù)并使用帶10%抖動(dòng)的advanced緩存,可以看到降低了峰值,且整體服務(wù)更加穩(wěn)定。

    緩存錯(cuò)誤

    當(dāng)構(gòu)建值失敗,最簡(jiǎn)單的方式就是將錯(cuò)誤返回給調(diào)用者即可,但這種方式可能會(huì)導(dǎo)致嚴(yán)重的問(wèn)題。

    例如,當(dāng)服務(wù)正常工作時(shí)可以借助緩存處理10K的RPS,但突然出現(xiàn)緩存構(gòu)建失敗(可能由于短時(shí)間內(nèi)數(shù)據(jù)庫(kù)過(guò)載、網(wǎng)絡(luò)問(wèn)題或如錯(cuò)誤校驗(yàn)等邏輯錯(cuò)誤),此時(shí)所有的10K RPS都會(huì)命中數(shù)據(jù)源(因?yàn)榇藭r(shí)沒(méi)有緩存)。

    對(duì)于高負(fù)載系統(tǒng),使用較短的TTL來(lái)緩存錯(cuò)誤至關(guān)重要。

    故障轉(zhuǎn)移模式

    有時(shí)使用過(guò)期的數(shù)據(jù)要好于直接返回錯(cuò)誤,特別是當(dāng)這些數(shù)據(jù)剛剛過(guò)期,這類數(shù)據(jù)有很大概率等于后續(xù)更新的數(shù)據(jù)。

    故障轉(zhuǎn)移以精確性來(lái)?yè)Q取彈性,通常是分布式系統(tǒng)中的一種折衷方式。

    緩存?zhèn)鬏?/h4>

    緩存有相關(guān)的數(shù)據(jù)時(shí)效果最好。

    當(dāng)啟動(dòng)一個(gè)新的實(shí)例時(shí),緩存是空的。由于產(chǎn)生有用的數(shù)據(jù)需要花費(fèi)一定的時(shí)間,因此這段時(shí)間內(nèi),緩存效率會(huì)大大降低。

    有一些方式可以解決"冷"緩存帶來(lái)的問(wèn)題。如可以通過(guò)遍歷數(shù)據(jù)來(lái)預(yù)熱那些可能有用的數(shù)據(jù)。

    例如可以從數(shù)據(jù)庫(kù)表中拉取最近使用的內(nèi)容,并將其保存到緩存中。這種方式比較復(fù)雜,且并不一定能夠生效。

    此外還可以通過(guò)定制代碼來(lái)決定使用哪些數(shù)據(jù)并在緩存中重構(gòu)這些表項(xiàng)。但這樣可能會(huì)對(duì)數(shù)據(jù)庫(kù)造成一定的壓力。

    還可以通過(guò)共享緩存實(shí)例(如redis或memcached)來(lái)規(guī)避這種問(wèn)題,但這也帶來(lái)了另一種問(wèn)題,通過(guò)網(wǎng)絡(luò)讀取數(shù)據(jù)要遠(yuǎn)慢于從本地緩存讀取數(shù)據(jù)。此外,網(wǎng)絡(luò)帶寬也可能成為性能瓶頸,網(wǎng)絡(luò)數(shù)據(jù)的編解碼也增加了延遲和資源損耗。

    最簡(jiǎn)單的辦法是將緩存從活動(dòng)的實(shí)例傳輸?shù)叫聠?dòng)的實(shí)例中。

    活動(dòng)實(shí)例緩存的數(shù)據(jù)具有高度相關(guān)性,因?yàn)檫@些數(shù)據(jù)是響應(yīng)真實(shí)用戶請(qǐng)求時(shí)產(chǎn)生的。

    傳輸緩存并不需要重構(gòu)數(shù)據(jù),因此不會(huì)濫用數(shù)據(jù)源。

    在生產(chǎn)系統(tǒng)中,通常會(huì)并行多個(gè)應(yīng)用實(shí)例。在部署過(guò)程中,這些實(shí)例會(huì)被順序重啟,因此總有一個(gè)實(shí)例是活動(dòng)的,且具有高質(zhì)量的緩存。

    Go有一個(gè)內(nèi)置的二進(jìn)制系列化格式encoding/gob,它可以幫助以最小的代價(jià)來(lái)傳輸數(shù)據(jù),缺點(diǎn)是這種方式使用了反射,且需要暴露字段。

    使用緩存?zhèn)鬏數(shù)牧硪粋€(gè)注意事項(xiàng)是不同版本的應(yīng)用可能有不兼容的數(shù)據(jù)結(jié)構(gòu),為了解決這種問(wèn)題,需要為緩存的結(jié)構(gòu)添加指紋,并在不一致時(shí)停止傳輸。

    下面是一個(gè)簡(jiǎn)單的實(shí)現(xiàn):

    // RecursiveTypeHash hashes type of value recursively to ensure structural match.
    func recursiveTypeHash(t reflect.Type, h hash.Hash74, met map[reflect.Type]bool) {
        for {
            if t.Kind() != reflect.Ptr {
                break
            }
            t = t.Elem()
        }
        if met[t] {
            return
        }
        met[t] = true
        switch t.Kind() {
        case reflect.Struct:
            for i := 0; i < t.NumField(); i++ {
                f := t.Field(i)
                // Skip unexported field.
                if f.Name != "" && (f.Name[0:1] == strings.ToLower(f.Name[0:1])) {
                    continue
                }
                if !f.Anonymous {
                    _, _ = h.Write([]byte(f.Name))
                }
                recursiveTypeHash(f.Type, h, met)
            }
        case reflect.Slice, reflect.Array:
            recursiveTypeHash(t.Elem(), h, met)
        case reflect.Map:
            recursiveTypeHash(t.Key(), h, met)
            recursiveTypeHash(t.Elem(), h, met)
        default:
            _, _ = h.Write([]byte(t.String()))
        }
    }

    可以通過(guò)HTTP或其他合適的協(xié)議來(lái)傳輸緩存數(shù)據(jù),本例中使用了HTTP,代碼為/debug/transfer-cache。注意,緩存可能會(huì)包含不應(yīng)該對(duì)外暴露的敏感信息。

    在本例中,可以借助于單個(gè)啟用了不同端口的應(yīng)用程序?qū)嵗齺?lái)執(zhí)行傳輸:

    CACHE_TRANSFER_URL=http://127.0.0.1:8008/debug/transfer-cache HTTP_LISTEN_ADDR=127.0.0.1:8009 go run main.go
    2022-05-09T02:33:42.871+0200    INFO    cache/http.go:282       cache restored  {"processed": 10000, "elapsed": "12.963942ms", "speed": "39.564084 MB/s", "bytes": 537846}
    2022-05-09T02:33:42.874+0200    INFO    brick/http.go:66        starting server, Swagger UI at http://127.0.0.1:8009/docs
    2022-05-09T02:34:01.162+0200    INFO    cache/http.go:175       cache dump finished     {"processed": 10000, "elapsed": "12.654621ms", "bytes": 537846, "speed": "40.530944 MB/s", "name": "greetings", "trace.id": "31aeeb8e9e622b3cd3e1aa29fa3334af", "transaction.id": "a0e8d90542325ab4"}

    怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存

    上圖中藍(lán)色標(biāo)識(shí)標(biāo)識(shí)應(yīng)用重啟,最后兩條為緩存?zhèn)鬏敗?梢钥吹叫阅懿皇苡绊?,而在沒(méi)有緩存?zhèn)鬏數(shù)那闆r下,會(huì)受到嚴(yán)重的預(yù)熱懲罰。

    一個(gè)不那么明顯的好處是,可以將緩存數(shù)據(jù)傳輸?shù)奖镜亻_發(fā)機(jī)器,用于重現(xiàn)和調(diào)試生產(chǎn)環(huán)境的問(wèn)題。

    鎖競(jìng)爭(zhēng)和底層性能

    基本每種緩存實(shí)現(xiàn)都會(huì)使用鍵值映射來(lái)支持并發(fā)訪問(wèn)(通常是讀)。

    大多數(shù)場(chǎng)景下可以忽略底層性能帶來(lái)的影響。例如,如果使用內(nèi)存型緩存來(lái)處理HTTP API,使用最簡(jiǎn)單的map+mutex就足夠了,這是因?yàn)镮O操作所需的時(shí)間要遠(yuǎn)大于內(nèi)存操作。記住這一點(diǎn)很重要,以免過(guò)早地進(jìn)行優(yōu)化以及增加不合理的復(fù)雜性。

    如果依賴內(nèi)存型緩存的應(yīng)用是CPU密集型的,此時(shí)鎖競(jìng)爭(zhēng)可能會(huì)影響到整體性能。

    為了避免并發(fā)讀寫下的數(shù)據(jù)沖突,可能會(huì)引入鎖競(jìng)爭(zhēng)。在使用單個(gè)互斥鎖的情況下,這種同步可能會(huì)限制同一時(shí)間內(nèi)只能進(jìn)行一個(gè)操作,這也意味著多核CPU可能無(wú)法發(fā)揮作用。

    對(duì)于以讀為主的負(fù)載,標(biāo)準(zhǔn)的sync.Map 就可以滿足性能要求,但對(duì)于以寫為主的負(fù)載,則會(huì)降低其性能。有一種比sync.Map性能更高的方式github.com/puzpuzpuz/xsync.Map,它使用了 Cache-Line Hash Table (CLHT)數(shù)據(jù)結(jié)構(gòu)。

    另一種常見(jiàn)的方式是通過(guò)map分片的方式(fastcache, bigcache, bool64/cache)來(lái)降低鎖競(jìng)爭(zhēng),這種方式基于鍵將值分散到不同的桶中,在易用性和性能之間做了折衷。

    內(nèi)存管理

    內(nèi)存是一個(gè)有限的資源,因此緩存不能無(wú)限增長(zhǎng)。

    過(guò)期的元素需要從緩存中淘汰,這個(gè)步驟可以同步執(zhí)行,也可以在后臺(tái)執(zhí)行。使用后臺(tái)回收方式不會(huì)阻塞應(yīng)用本身,且如果將后臺(tái)回收進(jìn)程配置為延遲回收的方式時(shí),在需要故障轉(zhuǎn)移時(shí)就可以使用過(guò)期的數(shù)據(jù)。

    如果上述淘汰過(guò)期數(shù)據(jù)的方式無(wú)法滿足內(nèi)存回收的要求,可以考慮使用其他淘汰策略。在選擇淘汰策略時(shí)需要平衡CPU/內(nèi)存使用和命中/丟失率。總之,淘汰的目的是為了在可接受的性能預(yù)算內(nèi)優(yōu)化命中/丟失率,這也是評(píng)估一個(gè)淘汰策略時(shí)需要注意的指標(biāo)。

    下面是常見(jiàn)的選擇淘汰策略的原則:

    • 最近最少頻率使用(LFU),需要在每次訪問(wèn)時(shí)維護(hù)計(jì)數(shù)器

    • 最近最少使用(LRU),需要在每次訪問(wèn)時(shí)更新元素的時(shí)間戳或順序

    • 先進(jìn)先出(FIFO),一旦創(chuàng)建緩存就可以使用緩存中的數(shù)據(jù),比較輕量

    • 隨機(jī)元素,性能最佳,不需要任何排序,但精確性最低

    上述給出了如何選項(xiàng)一個(gè)淘汰策略,下一個(gè)問(wèn)題是"何時(shí)以及應(yīng)該淘汰多少元素?"。

    對(duì)于[]byte緩存來(lái)說(shuō),該問(wèn)題比較容易解決,因?yàn)榇蠖鄶?shù)實(shí)現(xiàn)中都精確提供了控制內(nèi)存的方式。

    但對(duì)于結(jié)構(gòu)體緩存來(lái)說(shuō)就比較棘手了。在應(yīng)用執(zhí)行過(guò)程中,很難可靠地確定特定結(jié)構(gòu)體對(duì)堆內(nèi)存的影響,GC可能會(huì)獲取到這些內(nèi)存信息,但應(yīng)用本身則無(wú)法獲取。下面兩種獲取結(jié)構(gòu)體內(nèi)存的指標(biāo)精確度不高,但可用:

    • 緩存中的元素個(gè)數(shù)

    • 應(yīng)用使用的總內(nèi)存

    由于這些指標(biāo)并不與使用的緩存內(nèi)存成線性比例,因此不能據(jù)此計(jì)算需要淘汰的元素。一種比較合適的方式是在觸發(fā)淘汰時(shí),淘汰一部分元素(如占使用內(nèi)存10%的元素)。

    緩存數(shù)據(jù)的堆影響很大程度上與映射實(shí)現(xiàn)有關(guān)??梢詮南旅娴男阅軠y(cè)試中看到,相比于二進(jìn)制序列化(未壓縮)的數(shù)據(jù),map[string]struct{...}占用的內(nèi)存是前者的4倍。

    基準(zhǔn)測(cè)試

    下面是保存1M小結(jié)構(gòu)體(struct { int, bool, string })的基準(zhǔn)測(cè)試,驗(yàn)證包括10%的讀操作以及0.1%的寫操作。字節(jié)緩存通過(guò)編解碼結(jié)構(gòu)體來(lái)驗(yàn)證。

    goos: darwin
    goarch: amd64
    cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    name             MB/inuse   time/op (10%) time/op (0.1%)      
    sync.Map         192 ± 0%   142ns ± 4%    29.8ns ±10%   // Great for read-heavy workloads.
    shardedMap       196 ± 0%   53.3ns ± 3%   28.4ns ±11%   
    mutexMap         182 ± 0%   226ns ± 3%    207ns ± 1%    
    rwMutexMap       182 ± 0%   233ns ± 2%    67.8ns ± 2%   // RWMutex perf degrades with more writes.
    shardedMapOf     181 ± 0%   50.3ns ± 3%   27.3ns ±13%   
    ristretto        346 ± 0%   167ns ± 8%    54.1ns ± 4%   // Failed to keep full working set, ~7-15% of the items are evicted.
    xsync.Map        380 ± 0%   31.4ns ± 9%   22.0ns ±14%   // Fastest, but a bit hungry for memory.
    patrickmn        184 ± 0%   373ns ± 1%    72.6ns ± 5%   
    bigcache         340 ± 0%   75.8ns ± 8%   72.9ns ± 3%   // Byte cache.
    freecache        333 ± 0%   98.1ns ± 0%   77.8ns ± 2%   // Byte cache.
    fastcache       44.9 ± 0%   60.6ns ± 8%   64.1ns ± 5%   // A true champion for memory usage, while having decent performance.

    如果實(shí)際場(chǎng)景支持序列化,那么fastcache可以提供最佳的內(nèi)存使用(fastcache使用動(dòng)態(tài)申請(qǐng)的方式來(lái)分配內(nèi)存)

    對(duì)于CPU密集型的應(yīng)用,可以使用xsync.Map。

    從上述測(cè)試可以看出,字節(jié)緩存并不一定意味著高效地利用內(nèi)存,如bigcachefreecache

    開發(fā)者友好

    程序并不會(huì)總是按照我們期望的方式允許,復(fù)雜的邏輯會(huì)導(dǎo)致很多非預(yù)期的問(wèn)題,也很難去定位。不幸的是,緩存使得程序的狀況變得更糟,這也是為什么讓緩存更友好變得如此重要。

    緩存可能成為多種問(wèn)題的誘發(fā)因素,因此應(yīng)該盡快安全地清理相關(guān)緩存。為此,可以考慮對(duì)所有緩存的元素進(jìn)行校驗(yàn),在高載情況下,失效不一定意味著“刪除”,一旦一次性刪除所有緩存,數(shù)據(jù)源可能會(huì)因?yàn)檫^(guò)載而失敗。更優(yōu)雅的方式是為所有元素設(shè)置過(guò)期時(shí)間,并在后臺(tái)進(jìn)行更新,更新過(guò)程中使用老數(shù)據(jù)提供服務(wù)。

    如果有人正在調(diào)查特定的數(shù)據(jù)源問(wèn)題,緩存項(xiàng)可能會(huì)因?yàn)檫^(guò)期而誤導(dǎo)用戶??梢越锰囟ㄕ?qǐng)求的緩存,這樣就可以排除緩存帶來(lái)的不精確性。可以通過(guò)特定的請(qǐng)求頭以及并在中間件上下文中實(shí)現(xiàn)。注意這類控制并不適用于外部用戶(會(huì)導(dǎo)致DOS攻擊)。

    “怎么使用Go實(shí)現(xiàn)健壯的內(nèi)存型緩存”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    go
    AI