隨著資料量和存取量的增長,單一 Redis 伺服器可能成為效能瓶頸。本文將探討如何透過讀寫分離、分片技術以及最佳化壓縮和加密策略來提升 Redis 的效能和擴充能力。首先,我們會介紹如何使用讀取從屬伺服器擴充套件讀取操作,並討論在組態主從複製時需要注意的事項,例如主伺服器故障處理和從屬伺服器重新同步問題。接著,我們會分析加密和壓縮對 Redis 效能的影響,並提供一些最佳化建議。然後,我們將探討 Redis 的分片技術,包含如何處理分片組態、建立根據伺服器分片的連線裝飾器,以及如何將現有函式改造成支援分片連線。最後,我們將討論如何最佳化 Redis 記憶體使用和寫入量,以進一步提升效能。
擴充套件 Redis:提升效能與擴充能力
隨著 Redis 使用量的增長,單一 Redis 伺服器可能無法容納所有資料,或無法承受高頻率的讀寫操作。此時,我們需要採用多種技術來擴充套件 Redis 以滿足需求。本章將介紹如何擴充套件讀取查詢、寫入查詢、總記憶體容量,以及處理複雜查詢的方法。
10.1 擴充套件讀取操作
在第8章中,我們建立了一個類別似 Twitter 的社交網路,提供了許多相同的功能。其中一個功能是使用者可以檢視他們的首頁時間軸和個人時間軸。在檢視這些時間軸時,我們每次會擷取 30 篇文章。對於小型社交網路來說,這不是一個嚴重的問題,因為我們仍然可以支援每秒 3,000 至 10,000 名使用者擷取時間軸(如果 Redis 只做這件事)。但對於較大的社交網路來說,每秒需要處理多倍於此數量的時間軸擷取請求並不罕見。
在本文中,我們將討論使用讀取從屬伺服器來擴充套件讀取查詢,以超越單一 Redis 伺服器的處理能力。
提升效能的最佳實踐
在開始使用多個 Redis 伺服器之前,我們應該先最佳化 Redis 的效能:
- 使用小型資料結構:確保最大 ziplist 大小不會過大,以免造成效能損失。
- 選擇合適的資料結構:使用適合查詢型別的資料結構(例如,不要將 LIST 當作 SET 使用,或是不要為了在客戶端排序而擷取整個 HASH,使用 ZSET)。
- 壓縮資料:如果我們向 Redis 傳送大型物件進行快取,考慮壓縮資料以減少讀寫時的網路頻寬使用(比較 lz4、gzip 和 bzip2,以確定哪種壓縮方式在大小和效能之間提供最佳的折衷方案)。
- 使用管道和連線池:正如第4章所討論的,使用管道(有或沒有交易,視需求而定)和連線池來提高效能。
使用讀取從屬伺服器擴充套件讀取查詢
當我們已經做了所有能做的事來確保讀寫操作快速執行時,就需要解決如何處理更多讀取查詢的問題。增加 Redis 總讀取處理能力的最簡單方法是新增唯讀從屬伺服器。如第4章所述,我們可以執行額外的伺服器,連線到主伺服器,接收主伺服器資料的副本,並保持實時更新(或多或少,取決於網路頻寬)。透過對多個從屬伺服器執行讀取查詢,我們可以隨著每個新從屬伺服器的加入而獲得額外的讀取查詢容量。
注意事項:寫入主伺服器:使用讀取從屬伺服器時,通常只應寫入主 Redis 伺服器。預設情況下,嘗試寫入組態為從屬伺服器的 Redis 伺服器(即使它也是主伺服器)將導致該伺服器回覆錯誤。
組態 Redis 複製
第4章包含了有關組態 Redis 以進行複製到從屬伺服器的所有詳細資訊,包括其工作原理以及一些擴充套件到多個讀取從屬伺服器的想法。簡而言之,我們可以更新 Redis 組態檔案,加入一行 slaveof host port,將 host 和 port 替換為主伺服器的主機和埠。我們也可以透過對現有伺服器執行 SLAVEOF host port 命令來組態從屬伺服器。需要注意的是,當從屬伺服器連線到主伺服器時,之前存在於從屬伺服器上的任何資料都將被丟棄。要斷開從屬伺服器與主伺服器的連線以停止其從屬狀態,我們可以執行 SLAVEOF no one。
處理主伺服器故障
使用多個 Redis 從屬伺服器提供資料時,最大的問題之一是當主伺服器暫時或永久關閉時會發生什麼。請記住,當從屬伺服器連線時,Redis 主伺服器會啟動快照。如果多個從屬伺服器在快照完成之前連線,它們都將接收相同的快照。雖然這從效率的角度來看是好的(無需建立多個快照),但同時向多個從屬伺服器傳送多個快照副本可能會佔用伺服器可用的大部分網路頻寬。這可能會導致主伺服器的延遲增加,並可能導致先前連線的從屬伺服器斷開連線。
解決從屬伺服器重新同步問題
解決從屬伺服器重新同步問題的一種方法是減少主伺服器和從屬伺服器之間傳輸的資料量。這可以透過設定中間複製伺服器來形成一種樹狀結構來實作,如圖10.1所示,這是我們在第4章中借用的。
graph LR
A[Redis 主伺服器] --> B[從屬伺服器-1]
A --> C[從屬伺服器-2]
A --> D[從屬伺服器-3]
B --> E[從屬伺服器-a]
B --> F[從屬伺服器-b]
C --> G[從屬伺服器-d]
C --> H[從屬伺服器-e]
D --> I[從屬伺服器-g]
D --> J[從屬伺服器-h]
圖表翻譯: 此圖示呈現了一個 Redis 主從複製樹的結構,包含九個最低層的從屬伺服器和三個中間複製輔助伺服器。
使用壓縮減少網路頻寬
另一種方法是使用網路連結壓縮來減少需要傳輸的資料量。一些使用者發現,使用 SSH 連線並啟用壓縮,可以顯著降低頻寬使用。一家公司透過這種方法將複製到單個從屬伺服器的網路頻寬使用從 21 兆位降至約 1.8 兆位(http://mng.bz/2ivv)。如果使用這種方法,您需要使用一種機制,在斷開連線時自動重新連線 SSH 連線,有多種選項可供選擇。
程式碼範例:組態 Redis 複製
# 在 Redis 組態檔案中新增以下行以組態從屬伺服器
slaveof 主伺服器IP 主伺服器埠
# 或者,透過執行以下命令動態組態從屬伺服器
redis-cli SLAVEOF 主伺服器IP 主伺服器埠
# 斷開從屬伺服器與主伺服器的連線
redis-cli SLAVEOF no one
內容解密:
上述組態和命令用於設定 Redis 從屬伺服器,以實作資料複製和讀取查詢的擴充套件。其中,slaveof 組態指令或命令用於指定主伺服器的 IP 和埠,而 SLAVEOF no one 命令用於停止從屬伺服器的複製功能。
提升Redis讀取效能:加密與壓縮的效能影響
在前一章節中,我們探討瞭如何使用Redis Sentinel來實作自動容錯移轉,以提升Redis佈署的可靠性。現在,我們將關注如何擴充套件Redis的讀取和寫入能力,以滿足不斷增長的業務需求。
加密與壓縮的效能開銷
在使用SSH隧道時,加密的效能開銷通常不會對伺服器造成太大負擔。以AES-128加密演算法為例,在單核心的2.6 GHz Intel Core2處理器上,可以達到每秒180 MB的加密速度,而RC4加密演算法在相同的硬體上甚至可以達到每秒350 MB的加密速度。假設網路連線速度為1 Gbps,那麼單個中等效能的處理器核心就足以應付加密需求。
然而,壓縮操作可能會成為效能瓶頸。SSH預設使用gzip進行壓縮。在壓縮等級1(可組態)的情況下,上述2.6 GHz處理器可以將不同型別的Redis資料轉儲壓縮到每秒24-52 MB,而對於追加式檔案(流式複製),壓縮速度可以達到每秒60-80 MB。需要注意的是,更高的壓縮等級雖然可以獲得更好的壓縮率,但也會消耗更多的處理器資源,這對於高吞吐量但處理器資源有限的機器來說可能是一個問題。
一般來說,如果網路連線速度較快,建議使用較低的壓縮等級(例如1級),因為更高的壓縮等級(如9級)可能會導致處理時間大幅增加,而壓縮率的提升卻相對有限。對於大多數情況,壓縮等級1可以在處理時間和壓縮率之間取得良好的平衡。
OpenVPN中的壓縮與加密
乍看之下,OpenVPN支援AES加密和lzo壓縮,似乎為提供透明的重新連線、壓縮和加密提供了一個便捷的解決方案。然而,根據現有的資訊,啟用lzo壓縮在OpenVPN中的效能改進有限。在10 Mbps的連線上,壓縮可能帶來約25-30%的效能提升,但在更快的連線上,幾乎沒有明顯的改進。
Redis Sentinel:自動容錯移轉
Redis Sentinel是一種特殊的Redis伺服器模式,它不處理典型的Redis請求,而是監視多個主伺服器及其從伺服器的健康狀況和行為。透過結合對主伺服器的PUBLISH/SUBSCRIBE操作以及對從伺服器和主伺服器的PING呼叫,多個Sentinel程式可以獨立地發現有關可用從伺服器和其他Sentinel的資訊。當主伺服器發生故障時,根據所有Sentinel收集的資訊選出一個Sentinel,並從現有的從伺服器中選出一個新的主伺服器。之後,該從伺服器被提升為新的主伺服器,其他從伺服器則被重新組態為複製新的主伺服器。
Redis Sentinel的主要功能是提供從主伺服器到其從伺服器之一的自動容錯移轉。它還提供了容錯移轉通知、執行使用者提供的指令碼來更新組態等選項。
擴充套件Redis的寫入和記憶體容量
在第2章中,我們構建了一個系統,能夠自動將渲染的網頁快取到Redis中,這顯著減少了頁面載入時間和網頁處理的負擔。然而,隨著業務的增長,我們已經達到了單機的最大容量,現在需要將資料分散到多台較小的機器上。
擴充套件寫入量
在討論如何透過分片來增加總可用記憶體的同時,這些方法同樣適用於提高寫入吞吐量,如果單台機器的效能已經達到極限。
減少記憶體使用和寫入量
在決定擴充套件寫入容量之前,我們應該首先嘗試減少記憶體使用和寫入量:
- 檢查並最佳化所有減少讀取資料量的方法。
- 將較大的、無關的功能模組移到不同的伺服器上(如果使用了第5章中的連線裝飾器,這一步應該很容易實作)。
- 盡可能在本地記憶體中聚合寫入操作後再寫入Redis(這適用於大多數分析和統計計算方法)。
- 如果受到WATCH/MULTI/EXEC操作的限制,可以考慮使用第6章中討論的鎖定機制(或者考慮使用Lua指令碼,如第11章所述)。
- 如果使用了AOF持久化,記住磁碟需要跟上寫入資料的量(即使是小命令,如果數量龐大,也可能達到每秒數百MB的寫入速度)。
分片
在最佳化了記憶體使用和寫入量之後,如果仍然需要擴充套件寫入容量,那麼就需要將資料分片到多台機器上。分片的方法依賴於Redis伺服器的數量基本固定。如果可以預估未來寫入量的增長(例如,每6個月增長4倍),那麼可以預先將資料分片到256個分片中。這樣,可以制定一個足夠應對未來2年預期增長的計劃。
為未來增長進行預分片
在進行預分片時,可能會遇到資料量不足以充分利用所有預留的機器資源的情況。為瞭解決這個問題,可以在一台機器上執行多個Redis伺服器例項,以邏輯上分隔資料。
import redis
# 組態多個Redis例項
def configure_redis_instances(num_instances, port_start=6379):
instances = []
for i in range(num_instances):
port = port_start + i
instance = redis.Redis(host='localhost', port=port, db=0)
instances.append(instance)
return instances
# 使用多個Redis例項
def shard_data(instances, data):
# 簡單的分片策略:根據鍵的雜湊值選擇例項
for key, value in data.items():
instance_index = hash(key) % len(instances)
instance = instances[instance_index]
instance.set(key, value)
# 示例
if __name__ == "__main__":
num_instances = 5
instances = configure_redis_instances(num_instances)
data = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
shard_data(instances, data)
內容解密:
- 組態多個Redis例項:
configure_redis_instances函式根據指定的數量和起始埠號組態多個Redis例項。 - 資料分片:
shard_data函式根據鍵的雜湊值將資料分片到不同的Redis例項中。 - 使用範例:在
if __name__ == "__main__":區塊中,展示瞭如何組態5個Redis例項並將資料分片到這些例項中。
隨著業務的持續增長,Redis的擴充套件性和效能將面臨更大的挑戰。未來,我們可以探索更多先進的技術和策略來進一步最佳化Redis的佈署和維運,例如使用更高效的序列化格式、最佳化網路組態、以及利用更先進的硬體技術等。
Redis 擴充套件架構
graph LR
A[客戶端] -->|請求|> B[負載平衡器]
B -->|分發請求|> C[Redis 例項1]
B -->|分發請求|> D[Redis 例項2]
B -->|分發請求|> E[Redis 例項N]
C -->|資料儲存|> F[(資料函式庫)]
D -->|資料儲存|> F
E -->|資料儲存|> F
F -->|資料同步|> G[Redis Sentinel]
G -->|監控與容錯移轉|> C
G -->|監控與容錯移轉|> D
G -->|監控與容錯移轉|> E
圖表翻譯: 此圖示展示了Redis的擴充套件架構,包括客戶端、負載平衡器、多個Redis例項、資料函式庫以及Redis Sentinel。客戶端的請求透過負載平衡器分發到不同的Redis例項,資料儲存在資料函式庫中,並透過Redis Sentinel進行監控和容錯移轉。
Redis 水平擴充套件:分片技術詳解
在 Redis 的實際應用中,隨著資料量的增長和存取量的增加,單一 Redis 伺服器可能無法滿足效能需求。此時,我們需要透過水平擴充套件來提升 Redis 的處理能力和儲存容量。Redis 的分片技術(Sharding)是一種常見的水平擴充套件方案,它能夠將資料分散儲存於多個 Redis 例項中,從而實作負載平衡和擴充套件儲存空間。
10.2.1 處理分片組態
在進行 Redis 分片之前,我們需要定義分片的組態方案。分片組態主要涉及如何將資料合理地分散到不同的 Redis 例項中。我們可以透過一個簡單的函式來取得 Redis 連線,該函式根據一個命名的組態,程式碼如下:
def get_redis_connection(component, wait=1):
key = 'config:redis:' + component
old_config = CONFIGS.get(key, object())
config = get_config(config_connection, 'redis', component, wait)
if config != old_config:
REDIS_CONNECTIONS[key] = redis.Redis(**config)
return REDIS_CONNECTIONS.get(key)
內容解密:
- 組態取得:首先,函式根據
component生成組態鍵config:redis:<component>,並取得舊的組態資訊。 - 組態比較:接著,函式取得新的組態資訊並與舊組態進行比較。如果組態發生變化,則建立新的 Redis 連線並更新快取的連線資訊。
- 連線傳回:最後,函式傳回對應的 Redis 連線物件。
為了支援分片連線,我們需要進一步擴充套件上述函式,使其能夠根據分片鍵(Shard Key)計算出對應的分片 ID,並傳回相應的 Redis 連線。程式碼如下:
def get_sharded_connection(component, key, shard_count, wait=1):
shard = shard_key(component, 'x'+str(key), shard_count, 2)
return get_redis_connection(shard, wait)
內容解密:
- 分片計算:函式
shard_key根據component、key和shard_count計算出分片 ID。 - 連線取得:利用計算出的分片 ID,呼叫
get_redis_connection取得對應的 Redis 連線。
10.2.2 建立根據伺服器分片的連線裝飾器
為了簡化分片連線的管理,我們可以建立一個裝飾器(Decorator),自動為需要分片連線的函式提供相應的 Redis 連線。程式碼如下:
def sharded_connection(component, shard_count, wait=1):
def wrapper(function):
@functools.wraps(function)
def call(key, *args, **kwargs):
conn = get_sharded_connection(component, key, shard_count, wait)
return function(conn, key, *args, **kwargs)
return call
return wrapper
內容解密:
- 裝飾器定義:
sharded_connection裝飾器接受component、shard_count和wait作為引數,用於定義分片連線的組態。 - 包裝函式:內部的
wrapper函式對目標函式進行包裝,自動注入分片連線。 - 連線取得與函式呼叫:在
call函式中,根據key計算分片連線,並將連線和原始引數傳遞給目標函式。
利用上述裝飾器,我們可以輕鬆地將現有的函式(如 count_visit)改造成支援分片連線的版本。同時,需要注意對某些需要聚合資訊的函式(如 get_expected),應使用非分片連線,以確保資料的一致性。
隨著 Redis 的不斷發展,未來可能會出現更多高效的分片技術和工具。開發者應持續關注相關技術進展,並根據實際需求進行技術選型和架構設計。同時,對於現有的分片系統,也需要定期進行維護和最佳化,以確保系統的穩定性和效能。