歡迎光臨
每天分享高質量文章

從Memcache轉戰Redis,聊聊快取使用填過的“坑”

在高併發場景下,很多人都把 Cache(高速緩衝儲存器)當做可以“續命”的靈丹妙藥,哪裡高併發壓力大,哪裡就上傳 Cache 來解決併發問題。

但有時候,即使使用了 Cache,卻發現系統依然卡頓宕機,是因為 Cache 技術不好嗎?非也,其實這是快取的治理工作沒有做好。

2018 年 5 月 18-19 日,由 51CTO 主辦的全球軟體與運維技術峰會在北京召開。

在 19 日下午“高併發與實時處理”分會場,同程藝龍機票事業群 CTO 王曉波帶來了《高併發場景的快取治理》的主題演講。

他針對如何讓快取更適合高併發使用、如何正確使用快取、如何通過治理化解快取問題等熱點展開了闡述。

對於我們來說,我們是 OTA 的角色,所以有大量的資料要計算處理變為可售賣商品,總的來說是“商品搬運工,而非生產商”。

所以面對各種大資料併發運行的應用場景,我們需要通過各種快取技術來提升服務的質量。

想必大家都聽說過服務的治理和資料的治理,那麼是否聽說過快取的治理呢?誠然,在許多場景下,Cache 成了應對各處出現高併發問題的一顆“銀彈”。

但是它並非是放之四海皆準的,有時它反而成了一顆導致系統“掛掉”的自殺子彈。有時候這種原因的出現並非 Cache 本身的技術不好,而是我們沒有做好治理。

下麵,我們將從三個方面來具體討論快取的治理:

  • 快取使用中的一些痛點

  • 如何用好快取,用正確快取

  • 如何通過治理讓快取的問題化為無形

快取使用中的一些痛點

我們同程的業務特點是:OTA 類商品,沒有任何一個價格是固定的。像酒店,客戶今天訂、明天訂、連續訂三天、訂兩天,是否跨周末,他們最後得出的價格都是不一樣的。

價格隨著時間的變化而波動的。這些波動會引發大量的計算,進而帶來性能上的損耗。

要解決性能的損耗問題,我們勢必要插入各種 Cache,包括:價格的 Cache、時間段的 Cache、庫存的 Cache。而且這些 Cache 的寫入資料量遠大於整個外部的請求資料量,即:寫多於讀。

下麵介紹同程快取使用的歷史:

  • 一開始,我們僅使用一臺 Memcache 來提供快取服務。

  • 後來,我們發現 Memcache 存在著支持併發性不好、可運維性欠佳、原子性操作不夠、在誤操作時產生資料不一致等問題。

  • 因此,我們轉為使用 Redis,以單執行緒保證原子性操作,而且它的資料型別也比較多。當有一批新的業務邏輯被寫到 Redis 中時,我們就把它當作一個累加計數器。

    當然,更有甚者把它當作資料庫。由於資料庫比較慢,他們就讓資料線先寫到 Redis,再落盤到資料庫中。

  • 隨後,我們發現在單機 Redis 的情況下,Cache 成了系統的“命門”。哪怕上層的計算尚屬良好、哪怕流量並不大,我們的服務也會“掛掉”。於是我們引入了集群 Redis。

  • 同時,我們用 Java 語言自研了 Redis 的客戶端。我們也在客戶端里實現了二級 Cache。不過,我們發現還是會偶爾出現錯亂的問題。

  • 後來,我們還嘗試了分佈式 Cache,以及將 Redis 部署到 Docker 裡面。

最終,我們發現這些問題都是跟場景相關。如果你所構建的場景較為紊亂,則直接會導致底層無法提供服務。

下麵我們來看看有哪些需要治理的場景,通俗地說就是有哪些“坑”需要“填”。

早期在單機部署 Redis 服務的時代,我們針對業務系統部署了一套使用腳本運維的平臺。

當時在一臺虛機上能跑六萬左右的併發資料,這些對於 Redis 服務器來說基本夠用了。

但是當大量部署,並達到了數百多台時,我們碰到了兩個問題:

  • 面對高併發的性能需求,我們無法單靠腳本進行運維。一旦運維操作出現失誤或失控,就可能導致 Redis 的主從切換失敗,甚至引起服務宕機,從而直接對整個業務端產生影響。

  • 應用呼叫的凌亂。在採用微服務化之前,我們面對的往往是一個擁有各種模塊的大系統。

    而在使用場景中,我們常把 Redis 看成資料庫、存有各種工程的資料源。同時我們將 Cache 視為一個黑盒子,將各種應用資料都放入其中。

例如,對於一個訂單交易系統,你可能會把訂單積分、訂單說明、訂單數量等信息放入其中,這樣就導致了大量的業務模塊被耦合於此,同時所有的業務邏輯資料塊也集中在了 Redis 處。

那麼就算我們去拆分微服務、做代碼解耦,可是多數情況下快取池中的大資料並沒有得到解耦,多個服務端仍然通過 Redis 去共享和呼叫資料。

一旦出現宕機,就算你能對服務進行降級,也無法對資料本身採取降級,從而還是會導致整體業務的“掛掉”。

脆弱的資料消失了。由於大家都習慣把 Redit 當作資料庫使用(雖然大家都知道在工程中不應該如此),畢竟它不是資料庫,沒有持久性,所以一旦資料丟失就會出現大的麻煩。

為了防止單台掛掉,我們可以採用多台 Redis。此時運維和應用分別有兩種方案:

  • 運維認為:可以做“主從”,並提供一個浮動的虛擬 IP(VIP)地址。在一個節點出現問題時,VIP 地址不用變更,直接連到下一個節點便可。

  • 應用認為:可以在應用客戶端里寫入兩個地址,並採取“哨兵”監控,來實現自動切換。

這兩個方案看似沒有問題,但是架不住 Redis 的濫用。我們曾經碰到過一個現實的案例:如上圖右下角所示,兩個 Redis 根據主從關係可以互相切換。

按照需求,存有 20G 資料的主 Redis 開始對從 Redis 進行同步。此時網絡出現卡頓,而應用正好發現自己的請求也相應變慢了,因此上層應用根據網絡故障採取主從切換。

然而此時由於主從 Redis 正好處於同步狀態,資源消耗殆盡,那麼在上次應用看來此時主從 Redis 都是不可達的。

我們經過深入排查,最終發現是在 Cache 中某個表的一個 Key 中,被存放了 20G 的資料。

而在程式層面上,他們並沒有控制好該 Key 的消失時間(如一周),因而造成了該 Key 被持續追加增大的狀況。

由上可見,就算我們對 Redis 進行了拆分,這個巨大的 Key 仍會存在於某一個“片”上。

如上圖所示,仍以 Redis 為例,我們能夠監控的方麵包括:

  • 當前客戶端的連接數

  • 客戶端的輸出與輸入情況

  • 是否出現堵塞

  • 被分配的整個記憶體總量

  • 主從複製時的狀態信息

  • 集群的情況

  • 各服務器每秒執行的命令數量

可以說,這些監控的方面並不能及時地發現上述 20 個 G 的 Key 資料。再比如:通常系統是在客戶下訂單之後,才增加會員積分。

但是在應用設計上卻將核心訂單里的核心 Key,與本該滯後增加的積分輔助行程,放在了同一個實體之中。

由於我們能夠監控到的都是些延遲信息,因此這種將級別高的資料與級別低的資料混淆的情況,是無法被監控到的。

上面是一段運維與開發的真實對話,曾發生在我們公司內部的 IM 上,它反映了在 DevOps 推進之前,運維與開發之間的矛盾。

開發問:Redis 為什麼不能訪問?


運維答:剛纔服務器因記憶體故障自動重啟了。其背後的原因是:一個 Cache 的故障導致了某個業務的故障。業務認為自己的代碼沒有問題,原因在於運維的 Cache 上。

開發問:為什麼我的 Cache 的延遲這麼大?


運維答:發現開發在此處放了幾萬條資料,從而影響了插入排序。

開發問:我寫進去的 Key 找不到?肯定是 Cache 出錯了。這其實是運維 Cache 與使用 Cache 之間的最大矛盾。


運維答:你的 Redis 超過最大限制了,根本就沒寫成功,或者寫進去就直接被淘汰了。這就是大家都把它當成黑盒所帶來的問題。

開發問:剛剛為何讀取全部失敗?


運維答:網絡臨時中斷,在全同步完成之前,從機的讀取全部失敗了。這是一個非常經典的問題,當時運維為了簡化起見,將主從代替了集群樣式。

開發問:我的系統需要 800G 的 Redis,何時能準備好?


運維答:我們線上的服務器最大隻有 256 G。

開發問:為什麼 Redis 慢得像驢一樣,是否服務器出了故障?


運維答:對千萬級的 Key,使用 Keys*,肯定會慢。

由上可見這些問題既有來自運維的,也有來自開發的,同時還有當前技術所限制的。

我們在應對併發查詢時,只註重它給我們帶來的“快”這一性能特點,卻忽略了對 Cache 的使用規範,以及在設計時需要考慮到的各種本身缺點。

如何用好快取,用正確快取?

因此在某次重大故障發生之後,我們總結出:沒想到初始狀態下只有 30000 行代碼的小小 Redis 竟然能帶來如此神奇的功能。

以至於它在程式員手中變成了一把“見到釘子就想錘的鎚子”,即:他們看見任何的需求都想用快取去解決。

於是他們相繼開發出來了基於快取的日誌搜集器、倒計時、計數器、訂單系統等,卻忘記了它本身只是一個 Cache。一旦出現了故障,它們將如何去保證其本身呢?

下麵我們來看看快取故障的具體因素有哪些?

過度依賴

即:明明不需要設置快取之處,卻非要用快取。程式員們常認為某處可能會在將來出現大的併發量,故放置了快取,卻忘記了對資料進行隔離,以及使用的方式是否正確。

例如:在某些代碼中,一個函式會執行一到兩百次 Cache 的讀取,通過反覆的 get 操作,對同一個 Key 進行連續的讀取。

試想,一次併發會帶給 Redis 多少次操作呢?這些對於 Redis 來說負載是相當巨大的。

資料落盤

這是一個高頻次出現的問題。由於大家確實需要一個高速的 KV 儲存,來實現資料落盤需求。

因此他們都會把整個 Cache 當作資料庫去使用,將任何不允許丟失的資料都放在 Cache 之中。

即使公司有各種使用規範,此現象仍是無法杜絕。最終我們在 Cache 平臺上真正做了一個 KV 資料庫供程式員們使用,並且要求他們在使用的時候,必須宣告用的是 KV 資料庫還是 Cache。

超大容量

由於大家都知道“放到記憶體里是最快的”,因此他們對於記憶體的需求是無窮盡的。更有甚者,有人曾向我提出 10 個 T 容量的需求,而根本不去考慮營收上的成本。

雪崩效應

由於我們使用的是大量依賴於快取的資料,來為併發提供支撐,一旦快取出現問題,就會產生雪崩效應。

即:外面的流量還在,你卻不得不重啟整個快取服務器,進而會造成 Cache 被清空的情況。

由於斷絕了資料的來源,這將導致後端的服務連片“掛掉”。為了防止雪崩的出現,我們會多寫一份資料到特定磁盤上。

其資料“新鮮度”可能不夠,但是當雪崩發生時,它會被加載到記憶體中,以防止雪崩的下一波衝擊,從而能夠順利地過渡到我們重新將“新鮮”的資料灌進來為止。

我們對上面提到的“坑”總結一下:

  • 最厲害的是:使用者亂用、濫用和懶用。如前例所說,我們平時對於快取到底在哪裡用、怎麼用、防止什麼等方面考慮得實在太少。

  • 運維數千台毫無使用規則的快取服務器。我們常說 DevOps 的做法是讓應用與運維靠得更近,但是針對快取進行運維時,由於應用開發都不關心裡面的資料,又何談相互靠近呢?

  • 運維不懂開發,開發不懂運維。這導致了快取系統上各自為政,無法真正地應用好 Cache。

  • 快取在無設計、無控制的情況下被使用。一般情況下 JVM 都能監控到記憶體的爆漲,並考慮是否需要回收。但是如前例所示,在出現了一個 Key 居然有 20G 大小時、我們卻往往忽視了一個 Key 在快取服務器上的爆漲。

  • 開發人員能力的不同。由於不可能要求所有的開發人員都是前端工程師,那麼當你這個團隊裡面有不同經驗的人員時,如何讓他們能寫出同樣規範的代碼呢?

    畢竟我們做的是工程,需要更多的人能夠保證寫出來的代碼不會發生上述的問題。

  • 太多的服務器資源被浪費。特別是 Cache 的整體浪費是非常巨大的。無論併發量高或低,是否真正需要,大家都在使用它的記憶體。

    例如:在我們的幾千台 Cache Server 中,最高浪費量可達 60%。一些只有幾百或幾千 KPS 要求的系統或資料也被設計運行在了 Cache 昂貴的記憶體中。

    而實際上它們可能僅僅是為了應對一月一次、或一年一次促銷活動的 Cache 高峰需求。

  • 懶人心理,應對變化不夠快。應對高併發量,十個程式員有五個會說:為資料層添加 Cache,而不會真正去為架構做長遠的規劃。

如何通過治理讓快取的問題化為無形

那麼到底快取應當如何被治理呢?從真正的開發哲學角度上說,我們想要的是一個百變的魔術箱,它能夠快速地自我變化與處理,而不需要開發和運維人員擔心濫用的問題。

另外,其他需要應對的方面還包括:應用對快取大小的需求就像貪吃蛇一般,一堆孤島般的單機服務器,快取服務運維則像一個迷宮。

因此,我們希望構建的是一種能適用各種應用場景的快取服務,而不是冷冰冰的 Cache Server。

起初我們嘗試了各種現成的開源方案,但是後來發現它們或多或少存在著一些問題。

例如:

  • Cachecloud,對於部署和運維方面欠佳。

  • Codis,本身做了一個很大的集群,但是我們考慮到當這麼一個超大池出現問題時,整個團隊在應對上會失去靈活性。

    例如:我們會擔心業務資料塊可能未做隔離,就被放到了池中,那麼當一個實體“掛掉”時,所有的資料塊都會受到影響。

  • Pika,雖然可以使用硬碟,但是部署方式很少。

  • Twemproxy,只是代理見長,其他的能力欠佳。

後來,我們選擇自己動手,做了一個 phoenix 的方案。整個系統包含了客戶端、運維平臺、以及儲存擴容等方面。

在最初期的架構設計上,我們只讓應用端通過簡單的 SDK 去使用該系統。

為了避免服務端延續查找 Cache Server 的樣式,我們要求應用事先宣告其專案和資料場景,然後給系統分配一個 Key。SDK 籍此為應用分配一個新的或既有的快取倉庫。

如上圖所示,為了加快速度,我們將快取區分出多個虛擬的邏輯池,它們對於上層調度系統來說就是一個個的場景。

那麼應用就可以籍此申請包含需要存放何種資料的場景,最後根據所分配到的 Key 進行呼叫。

此處,底層是各種資料的複製和遷移,而兩邊則是相應的監控和運維。

但是在系統真正“跑起來”的時候,我們發現很難對其進行部署和擴容,因此在改造時,我們做重了整個快取客戶端 SDK,並引入了場景的配置。

我們通過進行本地快取的管理,添加過濾條件,以保證客戶端讀取快取時,能夠知道具體的資料源和基本的協議,從而判斷出要訪問的是 Redis、還是 MemCache、或是其他型別的儲存。

在 Cache 客戶端做好之後,我們又碰到了新的問題:由於同程使用了包括 Java、.Net、Go,Node.js 等多種語言的開發樣式,如果為每一種語言都準備和維護一套 Cache 的客戶端的話,顯然非常耗費人力。

同時,對於維護來說:只要是程式就會有 Bug,只要有 Bug 就需要升級。一旦所有事業部的所有應用都要升級 SDK,那麼對於所有嵌套應用的中間件來說,都要進行升級測試,這將會牽扯到巨大的回歸量。

可以說這樣的反覆測試幾乎是不現實的。於是我們需要做出一個代理層,通過把協議、過濾、場景等內容下沉到 Proxy 中,以實現SDK的整體輕量化。

與此同時,我們在部署時也引入了容器,將整個 Redis 都運行在容器之中,並讓容器去完成整個應用的部署。

通過容器化的部署,集群的建立變得極其簡單,我們也大幅豐富了集群的方案。

我們實現了為每個應用場景都能配有一個(或一種)Key,並且被一個(或一種)集群來服務。

眾所周知,Redis 雖然實現了遷移擴容,但是其操作較為複雜。因此我們自行研發了一套遷移調度系統,自動化地實現了從流量擴容到資料擴容、以及從縱向到橫向的擴容。

如前所述,我們有著 Redis 和 Memcache 兩種客戶端,它們是使用不同的協議進行訪問。因此,我們通過統一的 Proxy 來實現良好的支持。

如今在我們的快取平臺上,運維人員唯一需要做的就是:往該快取平臺里添加一臺物理服務器、插上網線、然後系統就能夠自動發現新的服務器的加入,進而開啟 Redis。

而對於單場景下的 Redis 實體,我們也能夠通過控制台,以獲取包括 Top10 的 Key、當前訪問最多的 Key、Key 的屬主、最後由誰執行了寫入或修改等多個監控項。

可見,由於上下層都是自建的,因此我們擴展了原來 Redis 里沒有的監控項。

上圖是 Topkey 使用情況的一個示例,就像程式在故障時經常用到的 Dump 檔案一樣,它能夠反映出後續的各種編排。

來自:51CTO技術棧(微信號:blog51cto)

作者:王曉波

王曉波,同程藝龍機票事業群 CTO,專註於高併發互聯網架構設計、分佈式電子商務交易平臺設計、大資料分析平臺設計、高可用性系統設計。 設計過多個併發百萬以上平臺。 擁有十多年豐富的技術架構、技術咨詢經驗,深刻理解電商系統對技術選擇的重要性。



●編號370,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

Web開發

更多推薦18個技術類公眾微信

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

赞(0)

分享創造快樂