redis 簡介
Redis 是大家日常工作中使用較多的典型 KV 存儲,常年位居 DB-Engines Key-Value 存儲第一。Redis 是基于內(nèi)存的存儲,提供了豐富的數(shù)據(jù)結(jié)構(gòu),支持字符串類型、哈希/列表/集合類型以及 stream 結(jié)構(gòu)。Redis 內(nèi)置了很多特性,其中比較重要的有:
- 復制:Redis 支持異步的全量和增量同步,可以把數(shù)據(jù)從 Master 復制到 Slave, 實現(xiàn) Redis 數(shù)據(jù)的高可用。
- 持久化:支持數(shù)據(jù)的持久化,可以通過 RDB 和 AOF 機制實現(xiàn)數(shù)據(jù)落盤。
- 支持哨兵工具:哨兵工具的主要工作模式是監(jiān)控 Master 節(jié)點的健康狀況。當發(fā)現(xiàn) Master 節(jié)點不可用時,會主動執(zhí)行 Failover, 把 Slave 節(jié)點提升成 Master,保證 Redis 服務(wù)的高可用。
- 提供集群模式:單體 Redis 實例受限于物理機內(nèi)存,當需要很大的 Redis 集群容量時,可以使用 Redis 集群模式。Redis 集群模式的原理是把保存在其中的數(shù)據(jù)做了分片,每一部分數(shù)據(jù)由不同的 Redis 實例承擔。
Redis 的典型應(yīng)用場景有以下 3 種:
- 緩存:因為 Redis 是基于內(nèi)存的存儲,它的讀寫請求會在內(nèi)存執(zhí)行,請求響應(yīng)的延遲很低,所以很多場景下會把 Redis 當做緩存使用。
- 數(shù)據(jù)庫:Redis 支持持久化,可以把它當做 KV 數(shù)據(jù)庫使用。
- 消息隊列:Redis 支持 stream 數(shù)據(jù),在 stream 數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)上封裝了 pub-sub 命令,實現(xiàn)了數(shù)據(jù)的發(fā)布和訂閱,即提供了消息隊列的基本功能。
Redis 協(xié)議是二進制安全的文本協(xié)議。它很簡單,可以通過 telnet 連接到一個 Redis server 實例上執(zhí)行 get 和 set 操作。
K8s 簡介
K8s 是一個容器編排系統(tǒng),可以自動化容器應(yīng)用的部署、擴展和管理。K8s 提供了一些基礎(chǔ)特性:
- 自動裝箱:可指定 K8s 里 Pod 所需資源的最小值和最大值,即 limit 和 request 的值。K8s 可以根據(jù) request 的值做 Pod 調(diào)度,在一個節(jié)點上拉起 Pod。
- 服務(wù)發(fā)現(xiàn)與負載均衡:K8s 提供基于 DNS 的服務(wù)發(fā)現(xiàn)機制,同時也提供基于 service 的負載均衡。
- 自動化上線和回滾:這里會涉及到 K8s 的工作負載資源。K8s 提供幾種不同的工作負載資源對應(yīng)不同的業(yè)務(wù)場景。這些不同的工作負載資源可以實現(xiàn)服務(wù)的配置變更,例如更新 image、升級 binary、進行副本的擴縮容等。
- 支持 Deployment/DaemonSet
- 支持 StatefulSet
- 支持 CronJob/Job
- 水平擴縮容:K8s 天然支持水平擴縮容,可以基于 Pod 的 CPU 利用率、內(nèi)存利用率以及第三方自定義 metrics 對 Pod 進行水平動態(tài)擴縮容。
- 存儲編排:K8s 支持基于 PV 和 PVC 的存儲供應(yīng)模式,可以通過 PV 和 PVC 在 Pod 內(nèi)部使用存儲。
- 自我修復:舉一個例子就是副本保持。比如用 Deployment 來托管一個服務(wù),如果 Deployment 下的一個 Pod 所在的宿主機出現(xiàn)了不可用的情況, K8s 會在可用的節(jié)點上重新拉起一個新的 Pod 來提供服務(wù)。
現(xiàn)實工作中遇到的服務(wù)根據(jù)是否需要數(shù)據(jù)持久化可分為有狀態(tài)服務(wù)和無狀態(tài)服務(wù)。不需要數(shù)據(jù)持久化的服務(wù)被認為是無狀態(tài)的,包含以下幾種類型:
- API 類服務(wù):可在任意節(jié)點上執(zhí)行。如果要在 K8s 上部署這類服務(wù),可使用 K8s Deployment。
- Agent 或 Deamon 類服務(wù):需要部署在每一臺機器上,而且每臺機器上最多部署一個進程。在 K8s 上可選擇 DaemonSet 來完成對應(yīng)的部署。
- 還有一類無狀態(tài)服務(wù)對固定的唯一標識有需求。要滿足這些需求,可使用 K8s 的 StatefulSet 來滿足。雖然 StatefulSet 是用來部署有狀態(tài)服務(wù)的,但它可提供固定的唯一標識,也可用來托管無狀態(tài)服務(wù)。
有狀態(tài)服務(wù)需要穩(wěn)定的持久化存儲。除此之外,可能還會有一些其它的特性要求:
- 穩(wěn)定的唯一標識
- 有序、優(yōu)雅地部署和縮放
- 有序的自動滾動更新
在 K8s 上,我們一般會用 StatefulSet resource 來托管有狀態(tài)服務(wù)。
Redis 云原生實踐
下面將介紹火山引擎 Redis 云原生實踐。首先我們會明確 Redis 云原生的目標,主要有以下幾個:
- 資源的抽象和交付由 K8s 來完成,無需再關(guān)注具體機型。在物理機時代我們需要根據(jù)不同機型上的 CPU 和內(nèi)存配置來決定每個機型的機器上可以部署的 Redis 實例的數(shù)量。通過 Redis 云原生,我們只需要跟 K8s 聲明需要的 CPU 和內(nèi)存的大小,剩下的調(diào)度、資源供給、機器篩選由 K8s 來完成。
- 節(jié)點的調(diào)度由 K8s 來完成。在實際部署一個 Redis 集群時,為了保證高可用,需要讓 Redis 集群的一些組件滿足一定的放置策略。要滿足放置策略,在物理機時代需要運維系統(tǒng)負責完成機器的篩選以及計算的邏輯,這個邏輯相對比較復雜。K8s 本身提供了豐富的調(diào)度能力,可以輕松實現(xiàn)這些放置策略,從而降低運維系統(tǒng)的負擔。
- 節(jié)點的管理和狀態(tài)保持由 K8s 完成。在物理機時代,如果某臺物理機掛了,需要運維系統(tǒng)介入了解其上部署的服務(wù)和組件,然后在另外一些可用的機器節(jié)點上重新拉起新的節(jié)點,填補因為機器宕機而缺少的節(jié)點。如果由 K8s 來完成節(jié)點的管理和狀態(tài)的保持,就可以降低運維系統(tǒng)的復雜度。
- 標準化 Redis 的部署和運維的模式。盡量減少人工介入,提升運維自動化能力,這是最重要的一點。
Redis 集群架構(gòu)
下面介紹一下我們的 Redis 集群架構(gòu)。集群里有三個組件:Server、Proxy 和 Configserver,分別完成不同的功能。
- Server:存儲數(shù)據(jù)的組件,即 Redis Server,其后端部署模型是一個多分片的模型。分片之間的 Server Pod 沒有通信,為 share-nothing 的架構(gòu)。分片內(nèi)部為一主多從的模式,可以一主一從、一主兩從,甚至更多。
- Proxy:承接 client 發(fā)來的請求,同時根據(jù)讀寫拓撲,把請求轉(zhuǎn)發(fā)給后端的 Server 分片。
- Configserver:配置管理組件,本身是無狀態(tài)的,所有的狀態(tài)信息都存儲在 etcd。集群生命周期里 Server 所有的分片信息都保存在 Configserver 里。Configserver 會對每一個分片的 Master 節(jié)點進行定期探活,如果發(fā)現(xiàn)某一個分片的 Master 節(jié)點不可用,就會執(zhí)行 Failover,把分片內(nèi)可用的 Slave 提成新的 Master,保證分片可繼續(xù)對外提供服務(wù)。同時,Configserver 也會定期根據(jù) Failover 或其他一些實例信息的變更來更新自己的讀寫拓撲關(guān)系,保證 Proxy 可以從 Configserver 拉取新的正確的配置。

結(jié)合以上介紹的 Redis 架構(gòu)以及 K8s 的特性,我們抽象了一個 Redis 集群在 K8s 集群上部署的基本形態(tài):
- 使用 Deployment 將無狀態(tài)的 Configserver 部署在 K8s 上。因為 Configserver 可被所有 Redis 集群共用,為了簡化運維復雜度,我們規(guī)定所有的 Redis 集群共用一個 Configserver。
- Proxy 也是無狀態(tài)的組件,也用 Deployment 來部署。
- 因為我們有多分片,而且 Server 是有狀態(tài)的,所以每一個分片用 StatefulSet 進行托管。在新建集群時,我們默認分片內(nèi)的 0 號 Pod 為 Master Pod,其余所有的 Pod 是 Slave。這是一個初始狀態(tài),后續(xù)可能會跟隨 Failover 或其他異常發(fā)生變更,但是 Configserver 里會實時記錄最新的狀態(tài)信息。
Redis Server 啟動的時候需要一些配置文件,里面涉及到一些用戶名和密碼,我們是用 Secret 來存儲的。在 Server Pod 運行的時候通過 volume 機制掛載到 Server Pod 內(nèi)部。
對于 Proxy,通過 HPA,基于 Proxy 的 CPU 利用率,支持 Proxy 服務(wù)的動態(tài)擴縮容。

放置策略
對于一個 Redis 集群涉及到的 Server 和 Proxy 組件,我們有一些放置策略的要求,比如:
- 同一個 Server 分片下的節(jié)點不能在同一臺機器上,即,一個分片內(nèi)的主從節(jié)點不能在同一臺機器上。轉(zhuǎn)換成 K8s 里面的模型,即我們希望一個 StatefulSet 下所有的 Pod 部署在不同的機器上。我們會利用 Pod-AntiAffinity 下面的 required 語義,來保證 StatefulSet 下所有的 Pod 都部署在不同的機器上。
- 一個集群下的 Proxy Pod 需要盡可能分布在不同的機器上,可通過 Pod-AntiAffinity 下的 preferred 語義加上拓撲分布約束來滿足。preferred 語義只能保證 Pod 盡可能分布在不同的機器上,為了避免極端情況下所有 Pod 都在同一臺機器上的情況,我們會使用拓撲分布約束。
存儲
存儲使用的是 PVC 加 PV 再加上具有動態(tài)供給能力的 StorageClass。使用 StorageClass 是為了抽象不同的存儲后端,可支持本地磁盤和分布式存儲。可以通過 StorageClass 的配置直接申請對應(yīng)的存儲,不用了解具體后端的實現(xiàn)。
另外,我們使用的是支持動態(tài)供給的 StorageClass,可自動按需創(chuàng)建不同大小的 PV。如果使用靜態(tài)供給,就無法提前預知所有 Redis 實例的規(guī)格,也無法把它們對應(yīng)的指定數(shù)量的 PV 都創(chuàng)建出來。
Redis 云原生功能介紹
Redis 云原生化以后,Operator 組件是基于 Operator Pattern 實現(xiàn)的一個 custom controller,主要用于編排 Redis Cluster resource,管理 Redis 集群的一些變更。Configserver 也部署在 K8s 上,所有跟 Redis 相關(guān)的組件都是云原生化的。
新建集群

- 對于常見的新建集群的請求,會先發(fā)給 ApiServer。ApiServer 接收到請求之后,會通過 client go 的 watch 機制讓 Operator 感知到。
- 隨后 Operator 會請求 ApiServer 創(chuàng)建對應(yīng) Server 的 StatefulSet。
- K8s 把所有 Server 的 StatefulSet 創(chuàng)建成功之后,等所有的 Pod 都處于 ready 狀態(tài),這時所有分片內(nèi)的 Server Pod 之間是沒有主從關(guān)系的。
- Operator 感知到所有的 StatefulSet 都已經(jīng)處于 ready 的狀態(tài)之后,會獲取所有 Server Pod 信息,并注冊到 Configserver。
- Configserver 接下來會連接到所有分片內(nèi)的 Slave 節(jié)點,執(zhí)行實際的 SLAVEOF 命令,保證建立真正的主從關(guān)系。
- Operator 會定期查詢 Configserver 里建立主從關(guān)系的進度。等所有分片的主從關(guān)系建立成功之后, Operator 會請求 ApiServer 把對應(yīng)的 Proxy 創(chuàng)建出來。
- Proxy 創(chuàng)建出來之后,會去 Configserver 拉取最新的拓撲,保證對外提供服務(wù)的時候可以把請求打給后端正常的分片。
分片擴容

在實際使用的過程中如果遇到容量不足的情況,需要進行水平擴容。分片擴容的請求也是類似的:
- 請求發(fā)給 ApiServer。
- ApiServer 把請求推送給 Operator。
- Operator 感知到之后,會先給 ApiServer 發(fā)請求,把新的分片對應(yīng)的 StatefulSet 創(chuàng)建出來。
- K8s 會把新分片的 StatefulSet 創(chuàng)建好,在處于 ready 狀態(tài)之后,一個 StatefulSet 下的每個 Pod 也都是獨立的狀態(tài),沒有建立真正的主從關(guān)系。
- Operator 獲知新創(chuàng)建的 Server StatefulSet 分片已經(jīng)處于 ready 狀態(tài)之后,會把新的 Server 分片的實例地址注冊到 Configserver。Configserver 現(xiàn)在會有兩個階段:
- 第一步:指導新分片內(nèi)真實主從關(guān)系的建立。即連到所有的新分片的 Slave 上,執(zhí)行 SLAVEOF 的命令;
- 第二步:指導數(shù)據(jù)從老分片遷移到新分片。這樣新的分片才能發(fā)揮作用,這一步很重要。
- Operator 會一直檢查數(shù)據(jù)遷移或者 rebalance 的進度。等進度結(jié)束之后,Operator 會更新 Redis Cluster 里 status 的字段,反映出來當前的分片擴容的操作已經(jīng)結(jié)束了。
分片縮容

分片縮容的流程和分片擴容類似:請求先發(fā)送給 ApiServer,Operator 會感知到請求,然后把縮容分片的請求發(fā)送給 Configserver。
Configserver 此時做的事情是:
- 先指導數(shù)據(jù)遷移。考慮到后邊的一些分片下線,需要把分片上的數(shù)據(jù)先遷移到其他可用分片上,保證數(shù)據(jù)不丟。
- Operator 會一直查詢 Configserver 指導的數(shù)據(jù) rebalance 的進度。等縮容操作在 Configserver 完成之后,Operator 會請求 ApiServer 執(zhí)行真正的 Server StatefulSet 刪除,這時才是安全的刪除操作。
組件升級

一個 Redis 集群會涉及到兩個組件:Proxy 和 Server。
無狀態(tài)的 Proxy 用 Deployment 托管,如果要進行組件升級,直接升級對應(yīng)的 image 即可。
Server 是一個有狀態(tài)的組件,它的升級流程相對來說復雜一點。為了簡化流程,我們以 Server 只有 1 分片的 Redis 集群為例,介紹升級過程。
- Server 組件的升級請求發(fā)送給 ApiServer,ApiServer 接收到這個請求之后會把它推送給 Operator。
- Operator 首先會按照分片內(nèi) Pod 編號從大到小的順序選擇要升級的 Pod。
- 選定 Pod 之后,會把它從 Configserver 的讀拓撲里摘掉。(如果要摘除的這個 Pod 在集群拓撲里是 Master,我們會先調(diào)用 Configserver 的 API,執(zhí)行 Failover,把它變成 Slave,然后再把它從讀的拓撲里邊給摘除掉。)
- 之后,Operator 等待 30 秒。這個機制的出發(fā)點是:
- 首先,Proxy 去 Configserver 拉取配置是異步過程,可能需要經(jīng)過至少一輪的數(shù)據(jù)同步才能正常拿到數(shù)據(jù)。等待 30 秒主要是為了保證所有的 Proxy 都已經(jīng)拿到了最新的讀拓撲,新的讀請求不會再發(fā)送到要升級的 Server 節(jié)點上。
- 另外,我們要保證等待 30 秒的時間,讓已經(jīng)被要升級的 Server Pod 接收的這些請求都成功地被處理,并且返回之后,才能把要升級的 Server Pod kill 掉。
- 30 秒之后請求 ApiServer 執(zhí)行實際的 Pod 刪除操作。刪除之后 K8s 會重新調(diào)度一個新的 Pod 起來,這時新創(chuàng)建的 Server Pod 也是一個獨立的 Server 的狀態(tài),沒有跟任何節(jié)點建立主從關(guān)系。Operator 感知到新的 Server Pod 已經(jīng)處于 ready 的狀態(tài),會把它注冊到 Configserver。
- Configserver 連到新的 Server Pod 上,根據(jù)它所處的分片跟所在分片的 Master 節(jié)點建立主從關(guān)系,同時進行數(shù)據(jù)同步。
- Operator 會定期檢查新的 Server 分片在 Configserver 是否已經(jīng)完成數(shù)據(jù)同步。如果是,Operator 才會認為一個 Server Pod 完成了升級。該分片內(nèi)其他所有 Server pod 的升級流程都是類似的,直到該分片內(nèi)所有 Server Pod 都升級完,才認為這個分片升級完成。最后 Operator 會更新自己 Redis Cluster 的 CRD 里對應(yīng)的 Status 狀態(tài)信息,反映當前組件升級的流程和變更操作已經(jīng)結(jié)束了。
總結(jié)展望
本次分享的以 Redis 為例,介紹了有狀態(tài)服務(wù)部署到 K8s 的抽象流程,并介紹了火山引擎在 Redis 云原生方向的一些探索和實踐。目前火山引擎對于 Redis 云原生已經(jīng)完成了基本功能的構(gòu)建,未來會在動態(tài)擴縮容、異常恢復、細粒度的資源分配和管理方面,結(jié)合 K8s 的特性,進行更多深層次的思考和實踐,期望通過云原生化的方式,進一步提升運維自動化能力和資源的利用率。
Q&A
Q1:沒用 Cluster 的模式嗎?
A:沒有,最早也使用過 Cluster 模式,后來業(yè)務(wù)體量變大,發(fā)現(xiàn) Cluster 有集群規(guī)模上限,不能滿足業(yè)務(wù)的需求。
Q2:Redis 的 Proxy 會計算 Key 在哪個分片嗎?
A:會的,Proxy 會參考類似 Redis Cluster 的 Key Hash 算法對 Key 進行 hash,之后分布到不同的 Server 分片上。
Q3:如何界定 Slave 可以提升為 Master?切換步驟是怎樣的?
A:Configserver 會定期給 Master 發(fā)送 health check 請求進行探活。只有連續(xù)多次對一個 Master 的探活都失敗時,才會認為 Master 不可用。這時 Configserver 會從分片內(nèi)所有 Slave 中選擇可用的提升成新的 Master(不是所有的 Slave 都可用,可能某一個 Slave 也掛了,或者主從數(shù)據(jù)同步的延遲比較高)。
Q4:Proxy 是每個 Redis 集群獨有還是所有集群共享的?
A:Proxy 不是每個 Redis 集群獨占的。首先,所有集群共享一個 Proxy 集群,有隔離性的問題。另外,Proxy 支持動態(tài)擴縮容,可以做到彈性資源擴縮容,所以不會導致資源浪費。
Q5:系統(tǒng)的穩(wěn)定性如何,主從切換耗時怎樣?
A:穩(wěn)定性挺好。主從切換的耗時是一個策略問題,需要做一些 tradeoff。如果判斷策略太激進,可能會因為臨時的網(wǎng)絡(luò)抖動等因素頻繁觸發(fā)主從切換。實際使用中我們的主從切換耗時在 10s 左右。
Q6:Redis 在什么規(guī)模等級下的 K8s 部署會需要修改較多默認配置或者直接更改源碼? 在動態(tài)擴容的基礎(chǔ)上建立 Redis 集群是否會加大困難?有什么方式可以讓 Redis 集群無限擴容嗎?最多到多少?
A:Redis 目前部署的 K8s 集群規(guī)模可選,根據(jù)需要的 Redis 集群容量來選擇 K8s 規(guī)模就可以。適配云原生會需要調(diào)整一些組件之間的服務(wù)發(fā)現(xiàn)方式,但是不需要太多源碼的修改。我們目前只支持 Proxy 的動態(tài)擴縮容,Server 是有狀態(tài)的服務(wù),還不太好接入 HPA(因為可能會涉及到一些數(shù)據(jù)的遷移),雖然 HPA 也支持對 Statefulset 服務(wù)的自動化擴縮容。我們的 Redis 架構(gòu)理論上集群的規(guī)模可以很大,現(xiàn)在 CRD 的限制是一個 Redis 集群最多 1024 個分片。
關(guān)于火山引擎
火山引擎是字節(jié)跳動旗下的數(shù)字服務(wù)與智能科技品牌,基于公司服務(wù)數(shù)億用戶的大數(shù)據(jù)、人工智能和基礎(chǔ)服務(wù)等技術(shù)能力,為企業(yè)提供系統(tǒng)化的全鏈路解決方案,助力企業(yè)務(wù)實地創(chuàng)新,給企業(yè)帶來持續(xù)、快速增長。