Ubuntu 22.04 Redis 記憶體優化策略

Ubuntu 22.04 Redis Memory Optimization Strategies

Redis 作為高效能的記憶體資料庫,在現代應用架構中扮演著關鍵角色。然而,記憶體資源始終是有限的,如何在 Ubuntu 22.04 環境下有效優化 Redis 的記憶體使用,是每位系統管理員和開發人員必須面對的課題。本文將深入探討各種記憶體優化策略,幫助您充分發揮 Redis 的效能潛力。

1. Redis 記憶體使用分析

在進行優化之前,我們需要先了解 Redis 目前的記憶體使用狀況。Redis 提供了豐富的命令來分析記憶體使用情況。

使用 INFO memory 命令

1
redis-cli INFO memory

輸出範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Memory
used_memory:1073741824
used_memory_human:1.00G
used_memory_rss:1181116416
used_memory_rss_human:1.10G
used_memory_peak:1610612736
used_memory_peak_human:1.50G
used_memory_overhead:52428800
used_memory_startup:803424
used_memory_dataset:1021313024
used_memory_dataset_perc:95.12%
mem_fragmentation_ratio:1.10
mem_allocator:jemalloc-5.2.1

關鍵指標解讀

指標說明
used_memoryRedis 分配的記憶體總量
used_memory_rss作業系統實際分配給 Redis 的記憶體
used_memory_peak記憶體使用峰值
mem_fragmentation_ratio記憶體碎片率(rss/used_memory)
used_memory_dataset實際存儲資料使用的記憶體

使用 MEMORY DOCTOR 診斷

1
redis-cli MEMORY DOCTOR

此命令會自動分析記憶體使用狀況並提供優化建議。

分析特定鍵的記憶體使用

1
2
3
4
5
# 查看單一鍵的記憶體使用量
redis-cli MEMORY USAGE mykey

# 查看鍵的詳細資訊
redis-cli DEBUG OBJECT mykey

2. 資料結構選擇優化

Redis 提供多種資料結構,選擇合適的資料結構對記憶體效率至關重要。

資料結構記憶體效率比較

資料結構適用場景記憶體效率
String簡單鍵值對、計數器中等
Hash物件屬性存儲高(小 Hash 使用 ziplist)
List佇列、時間序列高(使用 quicklist)
Set唯一值集合、標籤中等(小 Set 使用 intset)
Sorted Set排行榜、優先佇列中等(小 ZSet 使用 ziplist)

使用 Hash 替代多個 String

不推薦的做法:

1
2
3
SET user:1000:name "John"
SET user:1000:email "john@example.com"
SET user:1000:age "30"

推薦的做法:

1
HSET user:1000 name "John" email "john@example.com" age "30"

Hash 分桶策略

對於大量小物件,可以使用 Hash 分桶來節省記憶體:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import redis
import hashlib

def get_bucket_key(user_id, bucket_size=100):
    bucket = int(user_id) // bucket_size
    return f"users:{bucket}"

def get_field_key(user_id):
    return str(user_id)

r = redis.Redis()

# 存儲使用者資料
user_id = 12345
bucket_key = get_bucket_key(user_id)
field_key = get_field_key(user_id)
r.hset(bucket_key, field_key, '{"name": "John", "email": "john@example.com"}')

# 讀取使用者資料
data = r.hget(bucket_key, field_key)

3. 記憶體淘汰策略(maxmemory-policy)

當 Redis 記憶體達到上限時,淘汰策略決定了如何處理新的寫入請求。

設定最大記憶體

編輯 Redis 設定檔 /etc/redis/redis.conf

1
2
3
4
5
# 設定最大記憶體為 2GB
maxmemory 2gb

# 或使用命令動態設定
redis-cli CONFIG SET maxmemory 2gb

淘汰策略選項

1
2
# 設定淘汰策略
redis-cli CONFIG SET maxmemory-policy allkeys-lru
策略說明適用場景
noeviction不淘汰,寫入時返回錯誤資料不可丟失的場景
allkeys-lru從所有鍵中淘汰最近最少使用的通用快取
allkeys-lfu從所有鍵中淘汰最不常使用的熱點資料明顯的場景
volatile-lru從設有過期時間的鍵中淘汰 LRU混合持久化與快取
volatile-lfu從設有過期時間的鍵中淘汰 LFU混合場景,熱點明顯
allkeys-random隨機淘汰訪問模式隨機
volatile-random從過期鍵中隨機淘汰測試環境
volatile-ttl淘汰 TTL 最短的鍵需要控制過期順序

LRU 取樣數設定

1
2
# 增加取樣數可提高 LRU 準確度,但會消耗更多 CPU
redis-cli CONFIG SET maxmemory-samples 10

設定範例

1
2
3
4
# /etc/redis/redis.conf
maxmemory 4gb
maxmemory-policy allkeys-lfu
maxmemory-samples 10

4. 編碼優化(ziplist、intset)

Redis 針對小型資料結構使用特殊的緊湊編碼格式來節省記憶體。

Ziplist 編碼

Ziplist 是一種緊湊的雙向鏈表,適用於小型 Hash、List 和 Sorted Set。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Hash 使用 ziplist 的條件
hash-max-ziplist-entries 512    # 元素數量閾值
hash-max-ziplist-value 64       # 單一值大小閾值(位元組)

# List 使用 quicklist(基於 ziplist)
list-max-ziplist-size -2        # -2 表示每個 ziplist 最大 8KB

# Sorted Set 使用 ziplist 的條件
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

查看鍵的編碼方式

1
redis-cli OBJECT ENCODING mykey

可能的編碼類型:

  • embstr:短字串(<=44 位元組)
  • int:整數
  • raw:長字串
  • ziplist:壓縮列表
  • quicklist:快速列表(List 專用)
  • hashtable:雜湊表
  • intset:整數集合
  • skiplist:跳躍表

Intset 編碼

當 Set 只包含整數且數量較小時,使用 intset 編碼:

1
2
# Set 使用 intset 的條件
set-max-intset-entries 512

優化設定範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# /etc/redis/redis.conf

# Hash 優化
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

# List 優化
list-max-ziplist-size -2
list-compress-depth 1

# Set 優化
set-max-intset-entries 512

# Sorted Set 優化
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

驗證編碼效果

1
2
3
4
5
6
7
8
9
# 建立測試資料
redis-cli HSET test:hash field1 value1 field2 value2

# 檢查編碼
redis-cli OBJECT ENCODING test:hash
# 輸出: "ziplist" 或 "listpack"

# 檢查記憶體使用
redis-cli MEMORY USAGE test:hash

5. 過期鍵清理機制

Redis 使用兩種機制清理過期鍵:惰性刪除和定期刪除。

惰性刪除

當客戶端訪問一個鍵時,Redis 會檢查該鍵是否過期。如果過期,則刪除。

定期刪除

Redis 定期掃描帶有過期時間的鍵,並刪除已過期的鍵。

1
2
3
# 調整定期刪除的頻率(預設 10,範圍 1-500)
# 數值越高,CPU 使用越高,但過期鍵清理越及時
hz 10

設定過期時間的最佳實踐

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 設定鍵和過期時間(原子操作)
redis-cli SET session:abc123 "user_data" EX 3600

# 為現有鍵設定過期時間
redis-cli EXPIRE mykey 3600

# 設定精確到毫秒的過期時間
redis-cli PEXPIRE mykey 3600000

# 設定絕對過期時間
redis-cli EXPIREAT mykey 1735689600

過期鍵統計

1
2
# 查看過期鍵相關統計
redis-cli INFO stats | grep expired

輸出範例:

1
2
3
expired_keys:123456
expired_stale_perc:0.50
expired_time_cap_reached_count:0

批量設定過期時間

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import redis

r = redis.Redis()
pipe = r.pipeline()

# 為多個鍵設定過期時間
keys_to_expire = ['key1', 'key2', 'key3']
for key in keys_to_expire:
    pipe.expire(key, 3600)

pipe.execute()

6. 記憶體碎片處理

記憶體碎片會導致 Redis 實際使用的記憶體超過存儲資料所需的記憶體。

識別記憶體碎片

1
redis-cli INFO memory | grep fragmentation

輸出:

1
2
mem_fragmentation_ratio:1.52
mem_fragmentation_bytes:536870912
  • mem_fragmentation_ratio > 1.5:存在嚴重碎片問題
  • mem_fragmentation_ratio < 1:表示使用了 swap,效能會大幅下降

啟用自動碎片整理

Redis 4.0+ 支援自動記憶體碎片整理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# /etc/redis/redis.conf

# 啟用自動碎片整理
activedefrag yes

# 碎片率超過此值時開始整理
active-defrag-ignore-bytes 100mb

# 碎片率閾值
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100

# CPU 使用限制
active-defrag-cycle-min 1
active-defrag-cycle-max 25

# 每次掃描的最大鍵數
active-defrag-max-scan-fields 1000

手動觸發碎片整理

1
2
# 手動觸發碎片整理(Redis 4.0+)
redis-cli MEMORY PURGE

監控碎片整理狀態

1
redis-cli INFO memory | grep defrag

輸出:

1
2
3
4
5
active_defrag_running:0
active_defrag_hits:1234
active_defrag_misses:5678
active_defrag_key_hits:100
active_defrag_key_misses:50

7. 大鍵識別與處理

大鍵(Big Key)是指佔用大量記憶體的單一鍵,它們可能導致效能問題和記憶體浪費。

識別大鍵

使用 redis-cli 掃描

1
2
3
4
5
# 掃描大鍵(--bigkeys 選項)
redis-cli --bigkeys

# 使用取樣方式掃描
redis-cli --bigkeys -i 0.1

輸出範例:

1
2
3
4
5
6
7
8
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.

[00.00%] Biggest string found so far 'large_string' with 10485760 bytes
[00.00%] Biggest hash   found so far 'user:sessions' with 50000 fields
[00.00%] Biggest list   found so far 'queue:tasks' with 100000 items
[00.00%] Biggest set    found so far 'followers:123' with 80000 members
[00.00%] Biggest zset   found so far 'leaderboard' with 200000 members

使用 MEMORY USAGE 分析

1
2
# 查看特定鍵的記憶體使用
redis-cli MEMORY USAGE large_key SAMPLES 0

使用 Python 腳本掃描

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import redis

def find_big_keys(r, threshold_bytes=1048576):
    """找出超過閾值的大鍵"""
    big_keys = []
    cursor = 0

    while True:
        cursor, keys = r.scan(cursor=cursor, count=1000)

        for key in keys:
            try:
                memory = r.memory_usage(key)
                if memory and memory > threshold_bytes:
                    key_type = r.type(key).decode()
                    big_keys.append({
                        'key': key.decode(),
                        'type': key_type,
                        'memory': memory,
                        'memory_human': f"{memory / 1048576:.2f} MB"
                    })
            except Exception as e:
                continue

        if cursor == 0:
            break

    return sorted(big_keys, key=lambda x: x['memory'], reverse=True)

r = redis.Redis()
big_keys = find_big_keys(r, threshold_bytes=10*1024*1024)  # 10MB

for key in big_keys:
    print(f"{key['key']}: {key['memory_human']} ({key['type']})")

處理大鍵

分割大 Hash

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import redis
import json

def split_large_hash(r, source_key, chunk_size=1000):
    """將大 Hash 分割成多個小 Hash"""
    cursor = 0
    chunk_index = 0

    while True:
        cursor, data = r.hscan(source_key, cursor=cursor, count=chunk_size)

        if data:
            new_key = f"{source_key}:chunk:{chunk_index}"
            r.hset(new_key, mapping=data)
            chunk_index += 1

        if cursor == 0:
            break

    return chunk_index

r = redis.Redis()
chunks = split_large_hash(r, 'large_hash')
print(f"Split into {chunks} chunks")

漸進式刪除大鍵

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import redis
import time

def delete_large_key(r, key, batch_size=1000, delay=0.01):
    """漸進式刪除大鍵,避免阻塞"""
    key_type = r.type(key).decode()

    if key_type == 'hash':
        cursor = 0
        while True:
            cursor, fields = r.hscan(key, cursor=cursor, count=batch_size)
            if fields:
                r.hdel(key, *fields.keys())
            if cursor == 0:
                break
            time.sleep(delay)

    elif key_type == 'set':
        while r.scard(key) > 0:
            members = r.srandmember(key, batch_size)
            if members:
                r.srem(key, *members)
            time.sleep(delay)

    elif key_type == 'zset':
        while r.zcard(key) > 0:
            r.zremrangebyrank(key, 0, batch_size - 1)
            time.sleep(delay)

    elif key_type == 'list':
        while r.llen(key) > 0:
            r.ltrim(key, batch_size, -1)
            time.sleep(delay)

    else:
        r.delete(key)

r = redis.Redis()
delete_large_key(r, 'very_large_hash')

8. 監控與調校工具

Redis 內建監控命令

INFO 命令

1
2
3
4
5
6
7
8
# 查看所有資訊
redis-cli INFO

# 查看特定部分
redis-cli INFO memory
redis-cli INFO stats
redis-cli INFO clients
redis-cli INFO replication

MONITOR 命令(謹慎使用)

1
2
# 即時監控所有命令(會影響效能)
redis-cli MONITOR

SLOWLOG 慢查詢日誌

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 設定慢查詢閾值(微秒)
redis-cli CONFIG SET slowlog-log-slower-than 10000

# 設定慢查詢日誌最大長度
redis-cli CONFIG SET slowlog-max-len 128

# 查看慢查詢日誌
redis-cli SLOWLOG GET 10

# 重置慢查詢日誌
redis-cli SLOWLOG RESET

使用 redis-stat 監控

1
2
3
4
5
6
# 安裝 redis-stat
sudo apt-get install ruby ruby-dev
sudo gem install redis-stat

# 啟動監控(每秒更新)
redis-stat --server=8080 1

使用 Prometheus 和 Grafana

安裝 Redis Exporter

1
2
3
4
5
6
7
# 下載 Redis Exporter
wget https://github.com/oliver006/redis_exporter/releases/download/v1.54.0/redis_exporter-v1.54.0.linux-amd64.tar.gz
tar xvfz redis_exporter-v1.54.0.linux-amd64.tar.gz
cd redis_exporter-v1.54.0.linux-amd64

# 啟動 Exporter
./redis_exporter --redis.addr=redis://localhost:6379

Prometheus 設定

1
2
3
4
5
# /etc/prometheus/prometheus.yml
scrape_configs:
  - job_name: 'redis'
    static_configs:
      - targets: ['localhost:9121']

自動化監控腳本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# /usr/local/bin/redis-memory-check.sh

REDIS_CLI="redis-cli"
THRESHOLD_MB=3000
ALERT_EMAIL="admin@example.com"

# 取得記憶體使用量(MB)
USED_MEMORY=$($REDIS_CLI INFO memory | grep used_memory: | cut -d: -f2 | tr -d '\r')
USED_MEMORY_MB=$((USED_MEMORY / 1024 / 1024))

# 取得碎片率
FRAG_RATIO=$($REDIS_CLI INFO memory | grep mem_fragmentation_ratio | cut -d: -f2 | tr -d '\r')

# 記錄到日誌
echo "$(date '+%Y-%m-%d %H:%M:%S') - Memory: ${USED_MEMORY_MB}MB, Fragmentation: ${FRAG_RATIO}" >> /var/log/redis-memory.log

# 發送警告
if [ $USED_MEMORY_MB -gt $THRESHOLD_MB ]; then
    echo "Redis memory usage is high: ${USED_MEMORY_MB}MB" | mail -s "Redis Memory Alert" $ALERT_EMAIL
fi

設定 cron 定時執行:

1
2
# 每 5 分鐘執行一次
*/5 * * * * /usr/local/bin/redis-memory-check.sh

完整的監控 Dashboard 指標

建議監控以下關鍵指標:

指標說明警告閾值
used_memory記憶體使用量> 80% maxmemory
mem_fragmentation_ratio碎片率> 1.5
connected_clients連線數> 1000
blocked_clients阻塞客戶端數> 0
evicted_keys被淘汰的鍵數持續增加
expired_keys過期的鍵數觀察趨勢
keyspace_hits/misses命中率< 90%
instantaneous_ops_per_sec每秒操作數觀察趨勢

總結

Redis 記憶體優化是一個持續的過程,需要根據實際使用情況不斷調整。以下是關鍵建議:

  1. 定期監控:使用 INFO memory 和監控工具追蹤記憶體使用趨勢
  2. 選擇合適的資料結構:優先使用 Hash 存儲物件,利用編碼優化
  3. 設定合理的淘汰策略:根據業務場景選擇 LRU 或 LFU
  4. 控制鍵的大小:避免大鍵,使用分桶策略
  5. 設定過期時間:為快取資料設定合理的 TTL
  6. 處理記憶體碎片:啟用自動碎片整理
  7. 建立告警機制:在記憶體使用超標時及時通知

透過以上策略的綜合運用,您可以在 Ubuntu 22.04 環境下充分優化 Redis 的記憶體使用效率,確保系統穩定高效運行。

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy