Redis是一個開源高性能的Key-Value存儲系統(tǒng),雖然Redis本身具備了非常高的可用性,但是在實際應(yīng)用中也會隨著系統(tǒng)業(yè)務(wù)的復(fù)雜性以及不合理的使用,而導(dǎo)致很多的問題。本文將講述如何通過混沌工程來暴露可能存在的使用風(fēng)險,提升緩存問題的應(yīng)急能力。

緩存重要性

Redis是一個開源高性能的Key-Value存儲系統(tǒng),因為其極高的讀寫性能,豐富的數(shù)據(jù)類型,原子性的操作以及其他特性而被廣發(fā)運用。

Redis的應(yīng)用場景包括且不限于以下場景:

  • 用來做分布式緩存。
  • 用來做分布式鎖。
  • 用來處理某些特定高并發(fā)業(yè)務(wù),例如秒殺等。

示例架構(gòu)

在實施混沌工程之前,先了解業(yè)務(wù)是如何使用Redis的。由于Redis最常用來做分布式緩存,本文以簡單的商品查詢場景為例,涉及的基本信息如下:

  • 業(yè)務(wù)場景是查詢商品信息,首先查詢緩存;如果沒有查詢到,則查詢數(shù)據(jù)庫。
  • 使用Jedis連接Redis,并且使用了Jedis-pool的技術(shù)。
  • Redis是自建的集群(當(dāng)然也可以使用云服務(wù)),并且使用Sentinel技術(shù)來提升集群的高可用性。更多信息,請參見Redis Sentinel文檔

示例架構(gòu)圖如下:

Redis 1.png

從架構(gòu)圖可以看出,在Jedis配置、緩存查詢、網(wǎng)絡(luò)傳輸、服務(wù)端處理這條鏈路上,每個環(huán)節(jié)都有可能出現(xiàn)問題。借助混沌工程可以了解到問題發(fā)生時對系統(tǒng)、業(yè)務(wù)的影響面是否符合預(yù)期。

梳理演練場景

對于示例應(yīng)用,可以按照以下思路來梳理演練場景:

  1. 明確緩存監(jiān)控的指標。
  2. 分析影響這些指標可能的因素、故障場景、參數(shù)等。
  3. 因為客戶端層面的影響面可控,所以可以嘗試從客戶端層面去制造故障。
  4. 因為服務(wù)端出現(xiàn)故障更加真實,所以可以從服務(wù)端層面去制造故障,但對于問題定位和排查的要求會更高。
  5. 注入故障,觀察指標的變化。

緩存監(jiān)控指標

目前支持的可監(jiān)控的緩存指標如下:

指標 說明
緩存QPS QPS是最通用也是最易觀察的指標。
緩存命中率 緩存未命中可能會在大流量下引發(fā)穿透、擊穿、雪崩等問題,如果業(yè)務(wù)沒有做好應(yīng)急處理,很容易壓垮數(shù)據(jù)庫。
  • 穿透:Key對應(yīng)的數(shù)據(jù)在數(shù)據(jù)源并不存在,每次針對此Key的請求從緩存獲取不到,請求都會到數(shù)據(jù)源,從而可能壓垮數(shù)據(jù)源。例如用一個不存在的用戶ID獲取用戶信息,不論緩存還是數(shù)據(jù)庫都沒有,若黑客利用此漏洞進行攻擊可能壓垮數(shù)據(jù)庫。
  • 擊穿:Key對應(yīng)的數(shù)據(jù)存在,但在Redis中過期。此時若有大量并發(fā)請求,這些請求發(fā)現(xiàn)緩存過期一般都會從后端數(shù)據(jù)庫加載數(shù)據(jù)并回設(shè)到緩存,這個時候大并發(fā)的請求可能會瞬間把后端數(shù)據(jù)庫壓垮。
  • 雪崩:當(dāng)緩存服務(wù)器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給后端系統(tǒng)(例如數(shù)據(jù)庫)帶來很大壓力。
緩存RT 緩存響應(yīng)時間。緩存RT對業(yè)務(wù)的影響分成多個方面。如果RT變化較少,對于業(yè)務(wù)訪問緩存很少次數(shù)的情況下影響可控。但是如果一條請求需要多次訪問緩存,那么哪怕RT只是幾毫秒的增長,也會因為訪問次數(shù)過多引起總的RT增長過多,引發(fā)蝴蝶效應(yīng),造成業(yè)務(wù)異常。
緩存成功率 緩存的請求成功率過低也會造成和命中率一樣的后果。
業(yè)務(wù)成功率 在對業(yè)務(wù)成功率要求較高的業(yè)務(wù)當(dāng)中,緩存必定是作為一個弱依賴存在。當(dāng)緩存出現(xiàn)問題,系統(tǒng)應(yīng)該有其他兜底策略。但是隨著系統(tǒng)越來越復(fù)雜,改造的越來越頻繁,原本預(yù)計的緩存弱依賴也會不經(jīng)意間被改造成強依賴,一旦出現(xiàn)這種情況,就會導(dǎo)致業(yè)務(wù)受損。

影響因素

由于影響系統(tǒng)的因素有很多,例如機房、電源、集群服務(wù)、操作系統(tǒng)、應(yīng)用配置等。

本文主要梳理操作系統(tǒng)層面和應(yīng)用層面的影響因素:

  • 系統(tǒng)層面的影響因素有網(wǎng)絡(luò)、磁盤、IO、內(nèi)存、CPU等因素。
  • 應(yīng)用層面的影響有超時配置、連接池配置、查詢不合理等因素。

結(jié)合緩存監(jiān)控指標、操作系統(tǒng)層面和應(yīng)用層面的影響因素,本文從客戶端和服務(wù)端兩個角度來分析最終影響系統(tǒng)的因素和后果(假設(shè)業(yè)務(wù)請求QPS保持穩(wěn)定)。

表 1. 客戶端
因素 模擬手段 可能后果 可能影響指標
網(wǎng)絡(luò)延遲 6379端口網(wǎng)絡(luò)延遲
  • 讀寫請求RT變長
  • 連接池滿
  • QPS
  • RT
  • 成功率
網(wǎng)絡(luò)中斷 6379端口網(wǎng)絡(luò)丟包
  • 讀寫失敗
  • 無法連接
  • QPS
  • RT
  • 成功率
單次查詢耗時過長 如果Key過多,可以模擬Keys*查詢
  • 單次請求RT變長
  • 連接池滿
  • QPS
  • RT
  • 成功率
連接池設(shè)置不合理(連接池過小或者過高) Jedis連接池占滿 無法建立連接
  • QPS
  • RT
  • 成功率
緩存未命中 Jedis返回值攔截
  • 穿透
  • 擊穿
  • 雪崩
命中率
緩存異常 Jedis拋異常 緩存強依賴,業(yè)務(wù)失敗。 成功率
表 2. 服務(wù)端
因素 模擬手段 可能后果 可能影響指標 如何改進
磁盤空間不足 磁盤填充
  • 開啟了AOF會導(dǎo)致日志無法寫入。
  • 無法備份緩存數(shù)據(jù)。
  • QPS
  • RT
  • 成功率
  • 監(jiān)控磁盤利用率。
  • 禁AOF。
網(wǎng)絡(luò)異常
  • 端口延遲
  • 端口丟包
指定客戶端請求超時。
  • QPS
  • RT
  • 成功率
  • 網(wǎng)絡(luò)監(jiān)控。
  • 集群。
連接池滿 建立網(wǎng)絡(luò)連接 無法分配新連接,客戶端建連失敗。
  • 設(shè)置timeout和tcp-keeplive參數(shù)。
  • 網(wǎng)絡(luò)監(jiān)控。
單次查詢耗時過長 如果Key過多,可以模擬Keys*查詢。
  • 單次請求RT變長。
  • 連接池占滿。
  • QPS
  • RT
  • 成功率
  • 避免Keys*類查詢。
  • RT監(jiān)控。
IO讀寫過高 磁盤讀寫IO過高 讀寫變慢,響應(yīng)超時。
  • QPS
  • RT
  • 成功率
  • 避免Key類查詢。
  • 主從持久化策略修改。
內(nèi)存不足 Jedis返回值攔截
  • 內(nèi)存不足導(dǎo)致AOF無法備份。
  • 內(nèi)存不足導(dǎo)致無法寫入。
  • 命中率
  • 成功率
  • 內(nèi)存監(jiān)控。
  • 設(shè)定內(nèi)存閾值,合理內(nèi)存策略。
  • 緩存設(shè)定失效,避免冷數(shù)據(jù)庫過多。
CPU過高 CPU滿載 讀寫RT變長。
  • RT
  • 成功率
系統(tǒng)監(jiān)控。

實戰(zhàn)演練

下面通過Chaos故障演練平臺從客戶端層面來評測業(yè)務(wù)對Redis的合理使用。

  1. 分析演練系統(tǒng)。

    本文示例的業(yè)務(wù)場景是簡單的商品購物車查詢,為了便于理解,對該系統(tǒng)做了邏輯簡化,同時為了盡可能的模擬真實情況,您可以制定相關(guān)的業(yè)務(wù)指標如下:

    • 整個系統(tǒng)分為首頁和購物車頁面。
    • 首頁通過Dubbo來調(diào)用購物車接口,購物車服務(wù)端的接口超時設(shè)置為3000 ms。
    • 每一次購物車的內(nèi)部查詢,都需要查詢50次的緩存(為了更好觀看演練效果,次數(shù)稍微放大),每次緩存的操作約10 ms。
    • 購物車的內(nèi)部查詢優(yōu)先經(jīng)過緩存,失敗了以后再使用數(shù)據(jù)庫。
    • 連接緩存的SDK使用Java的JedisClient,設(shè)置的超時時間為100 ms。

    核心查詢代碼如下:

        //弱依賴緩存。
        @Override
        public List<CartItem> viewCart(String userId) {
            try {
                for (int i = 0; i < 50; i++) {
                    logger.info("query redis,count:" + i);
                    redisRepository.getUserCartItems(userId);
                }
                return redisRepository.getUserCartItems(userId);
            } catch (Exception exception) {
                logger.error("get data from redis failed ,use local data", exception);
            }
            logger.info("get data from redis failed ,query from db");
            return cartDBRepository.findByUserId(userId).stream().map(this::fromCart).collect(Collectors.toList());
        }

    相關(guān)的架構(gòu)圖如下:

    Redis2.png
  2. 假設(shè)演練場景。

    從影響因素里可以看到影響Redis使用穩(wěn)定性有很多原因,這里挑選一個場景:評測網(wǎng)絡(luò)延遲對Redis使用的影響,來觀察RT變化之后業(yè)務(wù)能否繼續(xù)保持正常服務(wù)。

    基于網(wǎng)絡(luò)延遲這個場景,可以提出這樣的假設(shè):

    • 緩存的RT變化不應(yīng)該影響到購物車查詢的成功率。
    • 由于緩存RT的增加導(dǎo)致購物車查詢RT會先增加,接著緩存RT增加到一定的值使得緩存徹底無法訪問,此時會觸發(fā)緩存降級,購物車會查詢數(shù)據(jù)庫,這樣又會使得查詢RT回落。
    • 雖然RT變化了,但是因為操作了強弱依賴治理,購物車查詢成功率不會有很明顯的變化。
    Redis3.png
  3. 設(shè)計演練場景。

    針對上面的假設(shè)結(jié)合系統(tǒng)特征,可以設(shè)計出以下的演練場景:

    • Redis延遲增加20 ms,緩存累計耗時50*20=1000 ms,此時CartServiceRT小于配置的接口超時3000 ms,業(yè)務(wù)正常。
    • Redis延遲增加80 ms,緩存累計耗時50*80=4000 ms,購物車內(nèi)部查詢繼續(xù),但此時CartServiceRT大于配置的接口超時3000 ms,購物車查詢失敗。
    • Redis延遲增加10000 ms,此時緩存超出了Jedis的超時配置時間100 ms,使得查詢緩存故障,導(dǎo)致查詢路徑切換至數(shù)據(jù)庫,此時業(yè)務(wù)正常。
  4. 實施演練。

    通過阿里云Chaos演練平臺可以快速的配置以上的演練場景,并且結(jié)合平臺提供的業(yè)務(wù)探活功能,可以快速實現(xiàn)整個故障演練的自動化評測。

    1. 通過探針管理向Cart服務(wù)所在的機器安裝演練探針。
      Redis4.png
    2. 創(chuàng)建演練場景。

      本示例創(chuàng)建網(wǎng)絡(luò)延遲的故障場景。

      1. 登錄AHAS控制臺,在左側(cè)欄選擇故障演練 > 我的空間
      2. 我的空間頁面,單擊新建演練 > 新建空白演練
      3. 演練配置頁面,填寫相關(guān)參數(shù),選擇演練內(nèi)容主機內(nèi)網(wǎng)絡(luò)延遲。更多參數(shù)信息,請參見創(chuàng)建演練
      4. 單擊主機內(nèi)網(wǎng)絡(luò)延遲,在本地監(jiān)聽端口文本框輸入6379,在延遲時間文本框輸入延遲時間。更多信息,請參見網(wǎng)絡(luò)類場景
    3. 增加業(yè)務(wù)探活的節(jié)點。

      由于要觀測演練前和故障注入后系統(tǒng)的業(yè)務(wù)情況,因此除了故障注入節(jié)點之外,還需要增加業(yè)務(wù)探活的節(jié)點。故障演練提供了類似K8s的探活功能,可以通過訪問指定接口來判斷業(yè)務(wù)是否可用。參數(shù)配置說明如下:

      參數(shù) 描述 示例值
      failureThreshold 重試次數(shù),重試幾次失敗后判斷為校驗失敗。 5
      periodSeconds 探測時間間隔。 2秒
      successThreshold 連續(xù)成功幾次算成功。 2
      url 需要探測的URL。 http://www.example.com(購物車的查詢地址)
      method GET或POST方法。 GET
      最終配置成如下完整演練流程:Redis5 完整演練流程.png
      重要 在演練前需要確保業(yè)務(wù)系統(tǒng)處于正常狀態(tài),所以在故障注入前需要判斷下應(yīng)用是否可用。
    4. 執(zhí)行演練。具體操作,請參見執(zhí)行演練

      配置完畢之后,可以發(fā)起自動演練、自動探測,最終得出結(jié)論(故障演練支持演練節(jié)點自動推進,也支持手動一步步推進)。

      Redis6 執(zhí)行演練.png
    5. 驗證結(jié)果。

      從演練執(zhí)行結(jié)果可以看出,最終的運行結(jié)果和假設(shè)一致,當(dāng)延遲注入80 ms之后,購物車不可用。但當(dāng)延遲注入20 ms和10000 ms時候,雖然購物車可用,但還需要進一步驗證是否如預(yù)期:一個是RT延長但是接口未超時,一個是緩存降級導(dǎo)致的業(yè)務(wù)成功。

      可以通過單擊校驗購物車是否可用的節(jié)點來查看業(yè)務(wù)成功的原因:

      1. 查看演練開始的探活節(jié)點,單擊購物車校驗是否可用,查看探活記錄。發(fā)現(xiàn)查詢RT處于正常范圍內(nèi)。Redis7.png
      2. 查看注入20 ms之后的探活節(jié)點。發(fā)現(xiàn)業(yè)務(wù)RT明顯增長,但是還是在超時的3秒內(nèi),因此業(yè)務(wù)正常。Redis8.png
      3. 查看注入80 ms之后的探活節(jié)點,發(fā)現(xiàn)業(yè)務(wù)異常。Redis9.png
      4. 查看注入10000 ms之后的探活節(jié)點,發(fā)現(xiàn)RT回落,此時業(yè)務(wù)正常。
        說明 但RT相比正常值還是有所延長。這是由于緩存出現(xiàn)故障,導(dǎo)致購物車查詢緩存失敗,此時購物車則需再去查詢數(shù)據(jù)庫。這個查詢路徑切換的過程導(dǎo)致RT相較于正常值有所延長。
        Redis10.png
    通過以上的演練證明了以下幾點:
    • 緩存RT輕微增長,對業(yè)務(wù)影響可控。但是如果業(yè)務(wù)內(nèi)部存在多次的緩存查詢,會導(dǎo)致整體RT增加明顯,就像本示例RT延長處于客戶端連接超時范圍內(nèi),無法觸發(fā)弱依賴降低,但是整個接口RT超時,最終導(dǎo)致業(yè)務(wù)受損。
    • 在緩存RT增長很明顯的情況下,緩存降級策略能夠正常生效,使得業(yè)務(wù)正常訪問。當(dāng)然在實際情況中,這種兜底策略可能導(dǎo)致數(shù)據(jù)庫直接崩潰。

演練價值

通過對不同網(wǎng)絡(luò)延遲的演練,可以了解到緩存RT變化對系統(tǒng)造成的影響,以及防護策略有效性。隨著業(yè)務(wù)規(guī)模的不斷增長,這個簡單的業(yè)務(wù)系統(tǒng)也會面臨新的問題:

  • 在某次重構(gòu)中,又新增加了緩存查詢,結(jié)果導(dǎo)致20 ms的延遲使得接口整體超時。
  • 業(yè)務(wù)邏輯簡單的時候,能夠很好地分析強弱依賴。但是隨著微服務(wù)的膨脹,以及代碼多次重構(gòu),可能原有的弱依賴在某次變更中變成了強依賴,這種通過功能測試是無法發(fā)現(xiàn)的。
  • 本示例Jedis設(shè)置的超時時間是100 ms,不同業(yè)務(wù)對RT的要求不同,您可以根據(jù)實際情況設(shè)置合理的超時時間。

上述的一些問題都要通過故障演練來發(fā)現(xiàn)。在日常的發(fā)布、架構(gòu)升級中除了功能測試、性能測試的回歸,還需要進行常態(tài)化的故障演練,同時演練的形態(tài)和場景復(fù)雜性也要不斷擴充。對于故障演練來說,難的不是注入手段,而是對業(yè)務(wù)架構(gòu)、業(yè)務(wù)場景的理解。故障注入不是目的,演練的目的是加深對系統(tǒng)的理解,這樣當(dāng)真實的問題來臨時候,才能更加有信心地去處理。