如何讓網(wǎng)站不下線從Redis 2遷移到Redis 3
我們?cè)?Sky Betting&Gaming 中使用 Redis 作為共享內(nèi)存緩存,用于那些需要跨 API 服務(wù)器或者 Web 服務(wù)器鑒別令牌之類的操作。在 Core Tribe 內(nèi),它用來(lái)幫助處理日益龐大的登錄數(shù)量,特別是在繁忙的時(shí)候,我們?cè)谝环昼妰?nèi)登錄數(shù)量會(huì)超過(guò) 20,000 人。這在很大程度上適用于數(shù)據(jù)存放在大量服務(wù)器的情況下(在 SSO 令牌用于 70 臺(tái) Apache HTTPD 服務(wù)器的情況下)。我們最近著手升級(jí) Redis 服務(wù)器,此升級(jí)旨在使用 Redis 3.2 提供的原生集群功能。這篇博客希望解釋為什么我們要使用集群、我們遇到的問(wèn)題以及我們的解決方案。
在開(kāi)始階段(或至少在升級(jí)之前)
我們的傳統(tǒng)緩存中每個(gè)緩存都包括一對(duì) Redis 服務(wù)器,使用 keepalive 確保始終有一個(gè)主節(jié)點(diǎn)監(jiān)聽(tīng)浮動(dòng) IP floating IP地址。當(dāng)出現(xiàn)問(wèn)題時(shí),這些服務(wù)器對(duì)需要很大的精力來(lái)進(jìn)行管理,而故障模式有時(shí)是非常各種各樣的。有時(shí),只允許讀取它所持有的數(shù)據(jù),而不允許寫(xiě)入的從屬節(jié)點(diǎn)卻會(huì)得到浮動(dòng) IP 地址,這種問(wèn)題是相對(duì)容易診斷的,但會(huì)讓無(wú)論哪個(gè)程序試圖使用該緩存時(shí)都很麻煩。
新的應(yīng)用程序
因此,這種情況下,我們需要構(gòu)建一個(gè)新的應(yīng)用程序,一個(gè)使用共享內(nèi)存緩存shared in-memory cache的應(yīng)用程序,但是我們不希望對(duì)該緩存進(jìn)行迂回的故障切換過(guò)程。因此,我們的要求是共享的內(nèi)存緩存,沒(méi)有單點(diǎn)故障,可以使用盡可能少的人為干預(yù)來(lái)應(yīng)對(duì)多種不同的故障模式,并且在事件恢復(fù)之后也能夠在很少的人為干預(yù)下恢復(fù),一個(gè)額外的要求是提高緩存的安全性,以減少數(shù)據(jù)泄露的范圍(稍后再說(shuō))。當(dāng)時(shí) Redis Sentinel 看起來(lái)很有希望,并且有許多程序支持代理 Redis 連接,比如 twemproxy。這會(huì)導(dǎo)致還要安裝其它很多組件,它應(yīng)該有效,并且人際交互最少,但它復(fù)雜而需要運(yùn)行大量的服務(wù)器和服務(wù),并且相互通信。
將會(huì)有大量的應(yīng)用服務(wù)器與 twemproxy 進(jìn)行通信,這會(huì)將它們的調(diào)用路由到合適的 Redis 主節(jié)點(diǎn),twemproxy 將從 sentinal 集群獲取主節(jié)點(diǎn)的信息,它將控制哪臺(tái) Redis 實(shí)例是主,哪臺(tái)是從。這個(gè)設(shè)置是復(fù)雜的,而且仍有單點(diǎn)故障,它依賴于 twemproxy 來(lái)處理分片,來(lái)連接到正確的 Redis 實(shí)例。它具有對(duì)應(yīng)用程序透明的優(yōu)點(diǎn),所以我們可以在理論上做到將現(xiàn)有的應(yīng)用程序轉(zhuǎn)移到這個(gè) Redis 配置,而不用改變應(yīng)用程序。但是我們要從頭開(kāi)始構(gòu)建一個(gè)應(yīng)用程序,所以遷移應(yīng)用程序不是一個(gè)必需條件。
幸運(yùn)的是,這個(gè)時(shí)候,Redis 3.2 出來(lái)了,而且內(nèi)置了原生集群,消除了對(duì)單一 sentinel 集群需要。
它有一個(gè)更簡(jiǎn)單的設(shè)置,但 twemproxy 不支持 Redis 集群分片,它能為你分片數(shù)據(jù),但是如果嘗試在與分片不一致的集群中這樣做會(huì)導(dǎo)致問(wèn)題。有參考的指南可以使其匹配,但是集群可以自動(dòng)改變形式,并改變分片的設(shè)置方式。它仍然有單點(diǎn)故障。正是在這一點(diǎn)上,我將永遠(yuǎn)感謝我的一位同事發(fā)現(xiàn)了一個(gè) Node.js 的 Redis 的集群發(fā)現(xiàn)驅(qū)動(dòng)程序,讓我們完全放棄了 twemproxy。
因此,我們能夠自動(dòng)分片數(shù)據(jù),故障轉(zhuǎn)移和故障恢復(fù)基本上是自動(dòng)的。應(yīng)用程序知道哪些節(jié)點(diǎn)存在,并且在寫(xiě)入數(shù)據(jù)時(shí),如果寫(xiě)入錯(cuò)誤的節(jié)點(diǎn),集群將自動(dòng)重定向該寫(xiě)入。這是被選的配置,這讓我們共享的內(nèi)存緩存相當(dāng)健壯,可以沒(méi)有干預(yù)地應(yīng)付基本的故障模式。在測(cè)試期間,我們的確發(fā)現(xiàn)了一些缺陷。復(fù)制是在一個(gè)接一個(gè)節(jié)點(diǎn)的基礎(chǔ)上進(jìn)行的,因此如果我們丟失了一個(gè)主節(jié)點(diǎn),那么它的從節(jié)點(diǎn)會(huì)成為一個(gè)單點(diǎn)故障,直到死去的節(jié)點(diǎn)恢復(fù)服務(wù),也只有主節(jié)點(diǎn)對(duì)集群健康投票,所以如果我們一下失去太多主節(jié)點(diǎn),那么集群無(wú)法自我恢復(fù)。但這比我們過(guò)去的好。
向前進(jìn)
隨著使用集群 Redis 配置的新程序,我們對(duì)于老式 Redis 實(shí)例的狀態(tài)變得越來(lái)越不適應(yīng),但是新程序與現(xiàn)有程序的規(guī)模并不相同(超過(guò) 30GB 的內(nèi)存專用于我們最大的老式 Redis 實(shí)例數(shù)據(jù)庫(kù))。因此,隨著 Redis 集群在底層得到了證實(shí),我們決定遷移老式的 Redis 實(shí)例到新的 Redis 集群中。
由于我們有一個(gè)原生支持 Redis 集群的 Node.js Redis 驅(qū)動(dòng)程序,因此我們開(kāi)始將 Node.js 程序遷移到 Redis 集群。但是,如何將數(shù)十億字節(jié)的數(shù)據(jù)從一個(gè)地方移動(dòng)到另一個(gè)地方,而不會(huì)造成重大問(wèn)題?特別是考慮到這些數(shù)據(jù)是認(rèn)證令牌,所以如果它們錯(cuò)了,我們的終端用戶將會(huì)被登出。一個(gè)選擇是要求網(wǎng)站完全下線,將所有內(nèi)容都指向新的 Redis 群集,并將數(shù)據(jù)遷移到其中,以希望獲得最佳效果。另一個(gè)選擇是切換到新集群,并強(qiáng)制所有用戶再次登錄。由于顯而易見(jiàn)的原因,這些都不是非常合適的。我們決定采取的替代方法是將數(shù)據(jù)同時(shí)寫(xiě)入老式 Redis 實(shí)例和正在替換它的集群,同時(shí)隨著時(shí)間的推移,我們將逐漸更多地向該集群讀取。由于數(shù)據(jù)的有效期有限(令牌在幾個(gè)小時(shí)后到期),這種方法可以導(dǎo)致零停機(jī),并且不會(huì)有數(shù)據(jù)丟失的風(fēng)險(xiǎn)。所以我們這么做了。遷移是成功的。
剩下的就是服務(wù)于我們的 PHP 代碼(其中還有一個(gè)項(xiàng)目是有用的,其它的最終是沒(méi)必要的)的 Redis 的實(shí)例了,我們?cè)谶@過(guò)程中遇到了一個(gè)困難,實(shí)際上是兩個(gè)。首先,也是最緊迫的是找到在 PHP 中使用的 Redis 集群發(fā)現(xiàn)驅(qū)動(dòng)程序,還要是我們正在使用的 PHP 版本。這被證明是可行的,因?yàn)槲覀兩?jí)到了最新版本的 PHP。我們選擇的驅(qū)動(dòng)程序不喜歡使用 Redis 的授權(quán)方式,因此我們決定使用 Redis 集群作為一個(gè)額外的安全步驟 (我告訴你,這將有更多的安全性)。當(dāng)我們用 Redis 集群替換每個(gè)老式 Redis 實(shí)例時(shí),修復(fù)似乎很直接,將 Redis 授權(quán)關(guān)閉,這樣它將會(huì)響應(yīng)所有的請(qǐng)求。然而,這并不是真的,由于某些原因,Redis 集群不會(huì)接受來(lái)自 Web 服務(wù)器的連接。 Redis 在版本 3 中引入的稱為“保護(hù)模式”的新安全功能將在 Redis 綁定到任何接口時(shí)將停止監(jiān)聽(tīng)來(lái)自外部 IP 地址的連接,并無(wú)需配置 Redis 授權(quán)密碼。這被證明相當(dāng)容易修復(fù),但讓我們保持警惕。
現(xiàn)在?
這就是我們現(xiàn)在的情況。我們已經(jīng)遷移了我們的一些老式 Redis 實(shí)例,并且正在遷移其余的。我們通過(guò)這樣做解決了我們的一些技術(shù)債務(wù),并提高了我們的平臺(tái)的穩(wěn)定性。使用 Redis 集群,我們還可以擴(kuò)展內(nèi)存數(shù)據(jù)庫(kù)并擴(kuò)展它們。 Redis 是單線程的,所以只要在單個(gè)實(shí)例中留出更多的內(nèi)存就會(huì)可以得到這么多的增長(zhǎng),而且我們已經(jīng)緊跟在這個(gè)限制后面。我們期待著從新的集群中獲得改進(jìn)的性能,同時(shí)也為我們提供了擴(kuò)展和負(fù)載均衡的更多選擇。
未來(lái)怎么樣?
我們解決了一些技術(shù)性債務(wù),這使我們的服務(wù)更容易支持,更加穩(wěn)定。但這并不意味著這項(xiàng)工作完成了,Redis 4 似乎有一些我們可能想要研究的功能。而且 Redis 并不是我們使用的唯一軟件。我們將繼續(xù)努力改進(jìn)平臺(tái),縮短處理技術(shù)債務(wù)的時(shí)間,但隨著客戶群體的擴(kuò)大,我們力求提供更豐富的服務(wù),我們總是會(huì)遇到需要改進(jìn)的事情。下一個(gè)挑戰(zhàn)可能與每分鐘超過(guò) 20,000次 登錄到超過(guò) 40,000 次甚至更高的擴(kuò)展有關(guān)。