本篇為Redis性能問題診斷系列的第二篇,本文主要從應用發起的典型命令使用上進行講解,由於Redis為單線程服務架構,對於一些命令如果使用不當會極大的影響Redis的性能表現,這裡也會對不合理的使用方式給出優化解決方案。 ...
(本文首發於“資料庫架構師”公號,訂閱“資料庫架構師”公號,一起學習資料庫技術) 本篇為Redis性能問題診斷系列的第二篇,本文主要從應用發起的典型命令使用上進行講解,由於Redis為單線程服務架構,對於一些命令如果使用不當會極大的影響Redis的性能表現,這裡也會對不合理的使用方式給出優化解決方案。 一、Redis慢日誌功能 分析Redis訪問變慢,其中有個最基礎的方法就是先去看Redis是否有慢日誌【就像MySQL的慢SQL一樣】。Redis提供了一個簡單的慢命令統計記錄功能,它會記錄有哪些命令在執行時耗時較長。Redis慢日誌功能由兩個核心參數控制: slowlog-log-slower-than 1000 #慢日誌命令執行閾值,這裡指超過1ms就會被記錄【單位為微秒】 slowlog-max-len 4096 #保留慢日誌命令的個數,類似一個先進先出的隊列,超過4096個最早的就會被清理 Redis的這個慢日誌功能比較粗糙簡單,有個嚴重的不足:沒有持久化記錄能力。 由於Redis的慢日誌記錄都在記憶體中,不像MySQL會持久化到文件里,那麼如果慢日誌產生較快,即使設置的slowlog-max-len比較大也會很快被填滿,診斷問題時也就不能統計到那個時間段產生的所有慢命令詳情。 為了避免產生的慢日誌被清理,目前一個折中的解決方案是寫一個收集程式周期性的將新增慢命令查出並記錄到MySQL或者本地文件中,以備事後分析。但是這個頻率一般都是分鐘級,Redis處理的吞吐能力又太大,在慢命令較多的情況下往往也不能全部記錄下來。 配置好慢日誌相關閾值後,可以執行以下命令查詢最近的慢日誌記錄了: 127.0.0.1:6379> SLOWLOG get 5 1) 1) (integer) 42343 2) (integer) 1653659194 #慢日誌產生的時間戳 3) (integer) 73536 #慢日誌執行的耗時 4) 1) "KEYS" #慢日誌命令詳情 2) "permission::userMenuList:*" 5) "192.168.1.11:20504" #慢日誌命令發起來源IP【4.0及以後版本支持】 6) ""2) 1) (integer) 42342 2) (integer) 1653659194 3) (integer) 73650 4) 1) "KEYS" 2) "userPermission:*" 5) "192.168.1.10:20362" 6) "" 3) 1) (integer) 42341 2) (integer) 1653659193 3) (integer) 81505 4) 1) "KEYS" 2) "userRole:*" 5) "192.168.1.13:19926" 6) "" 二、幾種典型導致Redis變慢的不合理使用方式 1.使用keys命令進行正則匹配 Keys的正則匹配是阻塞式的、全量掃描過濾,這對於單線程服務的Redis來說是致命的,僅僅幾十萬個Key的匹配查詢在高併發訪問下就有可能將Redis打崩潰!這其實就像MySQL的無索引查詢大表數據,全表掃描狀態下幾個併發查詢就可能會將資料庫堵死。 redis> SLOWLOG get 5 1) 1) (integer) 42343 2) (integer) 1653659194 3) (integer) 73536 4) 1) "KEYS" 2) "Testper::userList:*" 5) "192.168.1.10:20504" 6) "" 2) 1) (integer) 42342 2) (integer) 1653659194 3) (integer) 73650 4) 1) "KEYS" 2) "TestuserPermission:*" 5) "192.168.1.11:20362" 6) "" 3) 1) (integer) 42341 2) (integer) 1653659193 3) (integer) 81505 4) 1) "KEYS" 2) "TestuserRole:*" 5) "192.168.1.12:19926" 6) "" 上述示例中使用Keys來模糊查詢某些Key,每次的執行都在70ms以上,嚴重影響了正常的Redis響應時長和吞吐。 針對這種問題的一個解決方案是使用scan代替keys。這是一個查詢迭代命令,用於迭代當前資料庫中的緩存數據。它是一個基於游標的迭代器,每次被調用之後, 都會向用戶返回一個新的游標, 用戶在下次迭代時需要使用這個新游標作為Scan命令的游標參數, 以此來延續之前的迭代過程。具體的命令語法這裡不再詳述。 2.大量使用了複雜度較高的命令 (1)應用中高頻使用了 O(N) 及以上複雜度的命令,例如:SUNION、SORT、ZUNIONSTORE、ZINTERSTORE 聚合類命令。SORT命令的時間複雜度:O(N+M*log(M)), N 為要排序的列表或集合內的元素數量, M 為要返回的元素數量。 這種導致Redis請求變慢的原因是,Redis 在操作數據排序時,時間複雜度過高,要花費更多的 CPU計算資源。 (2)使用 O(N) 複雜度的命令,但 N 的值非常大,比如hgetall、smembers、lrange、zrange等命令。 這種變慢的原因在於,Redis 一次需要返回給客戶端的數據過多,需要花費更多時間在數據組裝和網路傳輸中。對於hgetall、smembers這種命令,需要警惕項目剛上線之初hash、set或者list存儲的成員個數較少,但是隨著業務發展成員數量極有可能會膨脹的非常大,如果仍然採用上述命令不加控制,會極大拖累整個Redis服務的響應時間。 針對這兩種情況還都可以從資源使用率層面來分析,如果應用程式訪問 Redis 的QPS不是很大,但 Redis 實例的 CPU 使用率卻很高,那麼很有可能是使用了複雜度過高的命令導致的。 因為Redis 是單線程處理請求的,如果你經常使用以上複雜度較高的命令,那麼當 Redis 處理程式請求時,一旦前面某個命令發生耗時較長,就會導致後面的請求發生阻塞排隊,對於應用程式來說,響應延遲也會變長。 3.存儲使用了bigkey 在分析慢日誌發現很多請求並不是複雜度高的命令,都是一些del、set、hset等的低複雜度命令,那麼就要評估是否寫入了大key。 在往Redis寫入數據時,需要為新數據分配記憶體塊,相對應的,當刪除數據時,Redis也會釋放對應的記憶體空間。如果一個 key 寫入Redis的值非常大,那麼在分配記憶體時就會相對比較耗時。同樣的當刪除這個 key 時,釋放記憶體也會比較耗時,這種被稱為bigKey。 當然這個描述仍然比較寬泛,因為Redis中的資料庫結構類型比較多,更完善的一些說法可以這麼定義:將含有較大數據或含有大量成員、列表數的Key定義為bigkey。 我們一般要求研發使用Redis時,對於String類型Value大小不要超過1KB。 大Key帶來的問題比較多,主要有下麵幾種情況:
- 由於大Key的記憶體分配及釋放開銷變大,直接影響就是導致應用訪問Redis的響應變慢;
- 刪除時會造成較長時間的阻塞並有可能造成集群主備節點切換【4.0之前的版本有這個問題】;
- 記憶體占用過多甚至達到maxmemory配置,會造成新寫入阻塞或一些不應該被提前刪除的Key被逐出,甚至導致OOM發生;
- 併發讀請求因為Key過大會可能打滿伺服器帶寬,如果單機多實例部署則同時會影響到該伺服器上的其它服務【假設一個bigkey為1MB,客戶端每秒訪問量為1000,那麼每秒產生1000MB的流量】;
- 運維麻煩,比如RedisCluster的數據跨節點均衡,因為均衡遷移原理是通過migrate命令來完成的,這個命令實際是通過dump + restore + del三個命令組合成原子命令完成,如果是bigkey,可能會使遷移失敗,而且較慢的migrate也會阻塞Redis正常請求;
- 分片集群RedisCluster中的出現嚴重的數據傾斜,導致某個節點的記憶體使用過大;
- 對於集合類型的Hash、List、Set、ZSet僅僅統計的是包含的成員個數,個數多並代表占用的記憶體大,僅僅是個參考;
- 對於高併發訪問的集群,使用該命令會造成QPS增加,帶來額外的性能開銷,建議在業務低峰或者從節點進行掃描。
- 降低使用 O(N) 以上複雜度的命令,對於數據的計算聚合操作等可以適當的放在應用程式側處理;
- 使用O(N) 複雜度的命令時,保證 N 儘量的小(推薦 N <= 500),每次處理的更小的數據量,降低阻塞的時長;
- 對於Hgetall、Smembers操作的集合對象,應從應用層面保證單個集合的成員個數不要過大,可以進行適當的拆分等。
- 是否有定時任務的腳本程式,定時或者間隔性的操作Redis
- Redis的Key數量出現集中過期清理
- 被動過期:只有應用發起訪問某個key 時,才判斷這個key是否已過期,如果已過期,則從Redis中刪除
- 主動過期:在Redis 內部維護了一個定時任務,預設每隔 100 毫秒(1秒10次)從全局的過期哈希表中隨機取出 20 個 key,判斷然後刪除其中過期的 key,如果過期 key 的比例超過了 25%,則繼續重覆此過程,直到過期 key 的比例下降到 25% 以下,或者這次任務的執行耗時超過了 25 毫秒,才會退出迴圈
- allkeys-lru:清理最近最少使用(LRU)的Key,不管 key 是否設置了過期時間
- volatile-lru:清理最近最少使用(LRU)的Key,但是只回收有設置過期的Key
- allkeys-random:隨機清理部分Key,不管 key 是否設置了過期時間
- allkeys-lfu:不管 key 是否設置了過期,清理訪問頻次最低的 key(4.0+版本支持)
- volatile-lfu:清理訪問頻次最低且設置了過期時間 key(4.0+版本支持)
- volatile-random:隨機清理部分設置了過期時間的部分Key
- volatile-ttl:清理有設置過期的Key,嘗試先回收離 TTL 最短時間的Key
- noeviction:不清理任何Key,當到達記憶體最大限制時,當客戶端嘗試執行命令時會導致更多記憶體占用時直接返回錯誤(大多數寫命令,除了 DEL 和一些例外)。
- 合理預估記憶體占用,避免達到記憶體的使用上限。這裡有兩種方法可以參考:
- 設置合理的Key過期時間,滿足業務的最小保留時間即可。
- 數據量過大建議拆分成多套Redis或者使用RedisCluster分片集群,建議單集群最大記憶體不超過20G。
- 數據清理策略改為隨機模式,隨機清理比 LRU 要快很多(不過這個要根據業務情況評定,業務優先滿足原則)。
- 如果使用的是 Redis 4.0 及以上版本,開啟 layz-free 機制,把淘汰 key 釋放記憶體的操作放到後臺線程中執行(配置 lazyfree-lazy-eviction = yes)
- 增加剩餘可用記憶體的監控,提前預警併進行最大記憶體上限的擴容或者提前清理釋放記憶體。