攜程Redis容器化實踐

背景

攜程大部分應用是基於CRedis客戶端通過集羣來訪問到實際的Redis的實例,集羣是訪問Redis的基本單位,多個集羣對應一個Pool,一個Pool對應一個Group,每個Group對應一個或多個實例,Key是通過一致性hash散列到每個Group上,集羣拓撲圖如截圖所示。

這個圖裏面我們可以看到集羣,Pool,Group還有裏面的實例,這是攜程Redis一個比較常見的拓撲圖,如下圖:

爲什麼要容器化

標準化和自動化

Redis之前是直接部署在物理機上,而DBA是根據物理機上設定的Redis的版本來選擇需要部署的物理機,攜程的各個版本的Redis非常分散而且不容易維護,如下圖所示,容器天然支持標準化,另外容器基於K8S自動化部署的效率,根據我們估算,相比人工部署提高了59倍。

規模化

有別於社區的方案比如官方Redis Cluster或代理方案而言,攜程的技術演進方案需要對大的實例進行分拆 (內部稱爲CRedis水平擴容),實例分拆後,單個實例的內存小了,QPS降低,單個實例掛掉的影響小很多,可以說是利國利民的項目,但會帶來一個問題,實例數急劇膨脹。容器化後我們能對分拆後的實例更好地管理和運維。

另外,分拆過程中需要大量中間狀態的實例Buffer作爲過渡,比如一對60G的實例分拆爲5G,中間狀態的Buffer需要24個60G的實例,純人工分拆異常艱難,而且容易出錯,依靠容器自動調度生成實例會極大降低DBA分拆時的心智負擔,極大提升了分拆的效率並減少出錯的概率。

提高資源利用率

藉助於容器化和上層的K8S的編排系統,我們很輕易的就可以做到資源利用率的提升,至於怎麼做到的,後面細節部分會涉及。

能不能容器化

既然Redis容器化後好處這麼多,那麼Redis能不能容器化呢?對比測試最能說明問題。

實際上我們在容器化前做了很多測試,甚至因爲測試模式的細微差別在各個部門之間還有過長時間的爭論,但最終下面這幾張圖的數據獲得了大家的一致認可,容器化才得以繼續推廣下去。

我們A/B對比測試都是基於相同硬件的容器和物理機,不掛slave,圖上我們可以看到,Redis的響應相比物理機要慢一點,QPS也能看到差距很小,這些差異主要是容器化後經過多個虛擬網卡帶來的性能損失。

這第三張圖就更明顯了,這是我們測試對比生產實際物理機的流量對比,我們測試的流量遠高於生產實際運行的單臺物理機的流量。

因此總結下來就是,容器與物理機的性能有細微的差別,大概5-10%,並且攜程的使用場景Redis完全可以容器化。

架構和細節

總體架構

以上介紹無非是容器化前的一些調研和可行性分析工作。

具體的架構如下圖所示,首先最上層的是運維和治理工具CRedis和Rat,這個在攜程內部是屬於框架和DBA兩個部門,CRedis不但提供應用訪問Redis的客戶端,本身也做CMS的工作,存儲Redis實例最基本的元數據。

PaaS層爲Credis/Rat提供統一的Redis Group/實例的創建刪除接口,下面的Redis微服務提供實例申請具體的調度策略,基礎設施有很多,這裏其實只列舉了一部分比較重要的,如網絡相關的ovs和neutron,與磁盤配額相關的quota,以及監控相關的telegraf等。xpipe是攜程內部的跨IDC的DR方案,sentinel就是官方的哨兵。

容器化遇到的一些問題

在我們容器化方案落地前遇到過一些具體的問題,例如:

Redis實際上是被應用直連的,我們需要IP和宿主機固定,並且master/slave不能在一臺宿主機上。

部署之前是在物理機上,通過端口來區分不同的實例,所有的監控通過端口來區分。

重啓實例Redis.conf文件配置不能丟失,這個在容器之前甚至不算需求,但放在容器上就有點麻煩。

Master掛了不希望K8S立刻把它拉起來,希望哨兵來感知到它,因爲K8S如果在哨兵感知前拉起了它,導致哨兵還沒切換Master/Slave,Master就活過來並且數據都丟失,這時候一同步到Slave上數據也全沒有了,等於執行了一個清空操作,這對於業務和DBA來說是不能接受的。

實例幾乎沒有任何的內存控制,就是說實例不管寫多大,都是得讓maxmemory一直加上去,一直加到必須遷移走開始,再把實例遷移走,而不能控制maxmemory,讓應用那邊直接寫報錯。這個是最大的問題,決定了容器化是否能進行下去。如果不控制內存,K8S的某些功能形同虛設,但如果控制內存,與攜程之前的運維習慣和流程不太相符,業務也無法接受。

以上都是我們遇到的一些主要問題,有些K8S的原生策略就可以很好地支持,有些則不行,需自研策略來解決。

K8S原生策略

首先,我們的容器基於K8S的Statefulset,這個幾乎沒有任何疑問,畢竟Redis是有狀態的。

其次,nodeAffinity保證了調度到指定標籤的宿主機,podAntiAffinity保證同一個Statefulset的Pod不調度到同一臺宿主機上,toleations保證可以調度到taint的宿主機上,而該宿主機不會被其他資源類型調度到,如Mysql,App等,也就是說宿主機被Redis獨佔,只能調度Redis的實例。

上面提到的分拆其實也是基於nodeAffinity,podAntiAffinity等特性,我們內部劃分出一塊虛擬區域叫slaughterhouse,專門用於分拆,分拆完成再遷到常規區域。

自研策略

宿主機固定,這個是自研的調度sticky-scheduler來提供支持,如下圖所示,在創建實例的時候會看annontation有沒有對應host,有的話直接會跳過調度固化到該宿主機上,如果沒有則進入默認的調度宿主機的流程。

雖然Redis對磁盤需求不多,但我們還是得防止log或rdb文件過大將磁盤撐爆,自研的chostpath和cemptydir都是基於xfs的quotas很好的支持磁盤配額,並且我們將Redis.conf和data目錄掛載出來,保證重啓容器後配置文件不丟失,還可保證容器重啓後可以讀rdb數據。

比如我們在做風險操作升級kubelet時候可能會引起相關的Pod重啓,但我們先對相關的Redis bgsave下,哪怕重啓pod也會讀取對應的rdb數據,不會導致完全沒有數據的尷尬場面。

監控方面,之前Redis部署在物理機上,通過端口來區分不同的實例,所有的監控通過端口來區分,但容器化後每個Pod都有一個IP,自然監控策略要變。

我們的方案是每個Pod兩個容器,一個是Redis本身的實例,一個是監控程序telegraf,每60秒採集一次數據發送到公司的統一監控平臺Hickwall,所有的telegraf腳本固化在物理機上,一旦修改方便統一的推送,並且對於Redis實例沒有任何影響。

實踐證明這種監控方案最爲理想,比如有一次我們生產遷移集羣后,DBA需要集羣的聚合頁面,也就是把所有的實例聚合在一起的按集羣維度查看的頁面,我們修改telegraf的腳本將集羣的信息隨着實例推送過去立刻就能顯示在監控頁面上,非常方便。

下面兩張圖清晰地展示出容器的監控頁面和物理機完全沒有區別。

爲了解決上文提到的Master掛了不希望K8S立刻把它拉起來,希望哨兵來感知到它,我們用Supervisord作爲容器的1號進程。當Redis掛了,Supervisord默認不會拉起它,但容器還是活的,Redis進程卻不存在了,想讓Redis活過來很簡單,刪除掉Pod即可。K8S會自動重新拉起它。

最後再來看看最困難的,實例幾乎沒有任何的內存限制。實際上在容器上我們對CPU和內存也幾乎沒有限制。

CPU不限制主要是幾個方面原因,首先,12核的機器上CPU quota/period =12, 按理是佔滿了整個機器,但壓測時CPU居然有throttle,這明顯不符合我們的客觀直覺,我們懷疑Linux的cfs是有問題的,而且很神奇的是我們設置一個很大的quota值後,也就是將CPU限額設置到50核,throttle消失了。

其次Redis是單線程,最多能用一個CPU,如果一個CPU跑一個Redis實例,肯定沒問題,實際上我們設置兩個實例分配到一個核也是完全可行的。

最後一個原因也是最主要的,Redis在物理機上運行是沒有任何CPU隔離的。基於上面三個原因,我們讓CPU超分。

關於內存超分,下面這張圖清晰地說明了問題所在,對於只有一個100G的宿主機,只要放上2個實例,每個實例50G,它的內存就超了。內存超分好處很多,比如物理機遷移過來很平滑,用戶也很能接受,運維工具幾乎不需要修改就能套上去,但是,超分大法好,但OOM了怎麼辦?

方案是不讓OOM發生,只要策略合適,這顯然是可以做到的,在說到杜絕OOM的策略之前,先看下普通的調度策略。

我們在調度時對集羣重要性進行了劃分,主要分爲以下幾種:

基礎集羣,比如賬號相關的,登陸相關的,雖然訂單無關但比訂單相關都重要。

接入XPIPE,訂單相關的。

沒有接入XPIPE,訂單相關的。

訂單無關但相對重要的。

既訂單無關的又不重要的。

這樣劃分後,我們就可以很方便地讓集羣根據重要性按機器的高中低配來調度,並且讓集羣是否在多Region上打散。爲了方便理解,這裏一個Region可以簡單等同於一個K8S集羣。

單個Region如下圖,一個Statefulset兩個Pod分別是Master/Slave,每個Pod裏面有兩個容器,一個是Redis本身,一個是監控程序telegraf,部署在兩個Host上。

多個Region如下圖,這時候,其實是有2個Statefulset,這種方案可以擴散到更多Region,這樣哪怕是某個K8S集羣掛了,重要的集羣仍然有對外提供服務的能力。

介紹完一般的調度策略後,接着說上文提到的杜絕OOM的策略。首先,調度之前,對於不同配置的宿主機限定不通的Pod數量,此外設定10%的佔位策略,如下圖所示,並且設定Pod的request == maxmemory。

調度中,我們會基於宿主機實際的可用內存進行打分,在K8S默認調度後,優選時我們會將實際剩餘內存的打分賦值一個非常高的權重,當然基於其他策略的調度比如說CPU,網絡流量之類我們也在研究,但目前最優先考慮的是實際剩餘的物理內存。

以上這些策略可以杜絕大部分OOM,但還不夠,因爲Redis後續還是會自然增長的,所以在運維過程中,我們會有Job定時輪詢宿主機,看可用內存和上面的Pod分配是否合理,對於不合理的Pod,Job會自動觸發遷移任務,將一些Pod遷移到內存更空的機器上去,以達到宿主機整體可用內存方差最小。

還有一些其他的調度後的策略,比如動態調整Redis實例的HZ,我們曾遇到一個情況就是,在物理機上跑着一個實例大小都是10多個G,但跑到容器上後2天增加了20多個G。

我們排查後發現Redis的HZ值設置的過小,導致大量過期的Key沒時間來得及清理,清理完成後發現,usedmemory是下來了,但rss還保持穩定,也就是碎片率很高,所以我們會動態打開自動碎片整理,整理一次完成後再關閉它,因爲同時打開,消耗的CPU過高,目前情況下還不是很適合。

最後還有個保底的,基於宿主機內存告警,一般設置爲80%即可,這種保底策略到目前爲止也就觸發過一次。

小結下,Redis跑在容器上,尤其在生產上大規模部署,需要多個組件共同協作才能達成。其次,攜程的現狀決定了我們必須超分,那麼超分後如何不OOM是關鍵,我們從調度過程前中後容器層面和Redis層面分別都有相應的策略,調度上的閉環不但保證了Redis在容器上的平穩運行,而且資源利用率(如下圖所示)也做到了非常大的提升。

一些坑

最後再分享一下實踐過程中的一些坑,這些坑其實本身不是Redis的問題,但都是在Redis容器化過程中發現的。

System Load有規模毛刺

首先是System Load有規模毛刺,每7小時一次,我們可以看到監控上,增加Pod後毛刺上升,但看上去跟CPU利用率沒什麼關係。

降低Pod數量,毛刺減小,但還存在,所以跟Pod數量正相關,

後來我們發現是telegraf監控腳本的問題。所有瞬間會產生很多進程的Job都會導致System Load升高。

對於Redis宿主機load異常情況,主要是因爲監控程序每1min生成很多進程採集一次數據, System Load採集則是每5.001s採集一次,當telegraf的第一次採集點命中System Load採集點後,第二次則需要5s*(5/0.001)=25000s,導致Load有規律每7小時飆高。

我們修改telegraf中的collection_jitter值,用來設置一個隨機的抖動來控制telegraf採集前的休眠時間,確保瞬間不會爆發上百個進程,修改後,毛刺消失了,如下圖所示:

Slowlog的異常

其次是Slowlog的異常,該問題根因在於4.9-4.13的內核的一個bug,會導致skylake服務器的時鐘變慢,而該時鐘不斷地被NTP修正,所以導致Slowlog的兩次打點時間過長,升級內核到4.14即解決該問題。

Xfs bugs

還有一個是Xfs的bugs,Xfs我們發現的有兩個比較嚴重的問題,第一個是字節對齊的問題,這個比較隱蔽,簡答地說就是內核態的Xfs header跟用戶態的Xfs header裏面定義不同,導致內核在寫Xfs的時候會越界。下圖中就是很明顯的症狀,我們升級4.14的內核對內存對齊打了patch解決了該問題。

Xfs第二個問題是xfsaild進入D狀態緩慢導致宿主機大量D狀態進程和殭屍進程,最終導致宿主機僵死,典型的現象如下圖。

這個現象在4.10內核發現很多次,並且猜測與khugepaged有關係,我們升級到4.14並Backport 4.15-4.19的Xfs bugfix,壓測問題還是存在,但比4.10要難以復現,在free內存超過3G後不會再復現。目前升級到4.14.67 Backport的新內核實際運行中還沒出現這個問題。


作者簡介

李劍,攜程CIS資深軟件工程師。加入攜程之前主要從事音視頻流媒體的開發,目前主要負責Redis和Mysql容器化和服務化的研發。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章