search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

南航移動 Redis-Cluster 趟坑記

作者|鄧卓楠編輯|江柳背景

當今 IT 界正處於移動互聯浪潮中,湧現出一批批優秀的門戶網站和電商平台。在巨大的利潤驅動下,這些公司都全力打造各自的系統以適應互聯網市場發展的需要,而且在此過程中各個系統還不停地接受著億萬網民的檢驗。經歷過千錘百鍊后,那些「名門大廠」都紛紛總結出「高可用,高可靠,高併發下低延遲」的優秀實踐。

而南航電商營銷平台這種帶有國企背景和傳統行業特色的電商系統在這股浪潮的引領之下,也逐步向這些「大廠」學習,結合自身的實際情況,在「高可用,高可靠,高併發下低延遲」的系統優化之路上展開一番遊歷探索,期間也遊覽了不少大坑暗溝。

我們商務移動團隊作為部門裡先鋒部隊,在打造一套以 Redis 為基礎的緩存系統(取名為「黑曜石」)用於支撐南航電商營銷平台,幫助公司實現「南航 e 行」戰略目標的過程中對 Redis 環境中的坑澗丘壑展開了一番遊歷。

初步思考

2015 年是移動電商的「井噴式」發展年份,南航的官方 APP 承勢而起,不過事實印證了那句話——「理想很豐滿,現實很骨感」。那個時候 APP 的後台架構是按傳統的企業內部應用思路搭建的,系統的研發思路是只管實現功能,不注重介面性能。

讓人糾結的地方比較多,一時三刻難以對其進行全面優化改造,但上級下達的 KPI 要在短期內完成,必須要在短時間內提升一些關鍵介面的性能,我們第一時間想到的而且最好開刀的莫過於類似航班動態和機票信息等查詢類功能介面,因為只要加上緩存,性能馬上能大幅度提升。

基於這方面考慮,我們當時認為第一要務就是要搭建一套能面向互聯網的緩存系統,經過幾番選型后,目光開始集中到 Redis-Cluster 上。

深入探索

我們在探索期間一邊翻資料,一邊在測試環境搭建了一套 Redis-Cluster,經過一段時間折騰后,在 2016 年 3 月份正式投產使用。

在投產初期信心不大,所以找了小介面來嘗試,緩存的數據量小(小於 1KB),結構簡單,基本都是一些系統配置信息,如功能開關,APP 版本信息等,Redis-Cluster 對此表示毫無壓力。

緊接著,在 16 年 3 月底,我們開始把 Redis-Cluster 投放到正規戰場上,比如緩存機票信息,航班動態更新。這個介面數據量比之前的大(50-100KB),結構比較複雜,而且是我們關鍵功能的核心介面。使用 Redis-Cluster 緩存機票信息以及優化了介面的通訊協議后,對查票介面加速效果十分顯著,介面的響應時間從 7-8 秒降到一百多毫秒,再配合 IOS 和安卓端的原生頁面渲染方式優化后,實現機票信息"秒出",這標誌著南航系統開始踏進「秒極俱樂部」的門檻。在一次跟全美航空的交流中,機票查詢的速度把他們嚇了一跳。(當時全美 app 查一下機票信息需要 8-9 秒)

應對大流量

我們解決在常規服務狀態下的核心功能查詢類介面的返回緩慢的問題,但這僅僅是開始,因為移動電商最大的特色就是搞促銷活動,像秒殺、抽獎、派券等等,這些活動都會引發瞬時的訪問高峰,訪問量往往是常規服務狀態下的十倍或以上,南航自 2015 年 10 月 28 日搞了第一次會員日活動后,往後的每月 28 日都會搞一次,每次的零點峰值都會對我們系統造成毀滅性打擊,其實這就好像一個沒穿衣服的人在冰天雪地中行走一樣,所以幾乎在完成介面提速的同一時間,我們用 Redis-Cluster 做了件「棉襖」讓系統「穿」上——把所有流量轉嫁到 Redis-Cluster 上。

利用 Redis 的單線程原子性管理訪問許可證池,許可證池的大小根據活動介面的性能靈活調整。只有得到許可證的請求才能訪問相關介面,當介面返回后把許可證釋放回許可證池中,而得不到許可證的請求則進入 Redis blpop 的等待隊列中。 這項措施結合 Nginx-Lua 的服務升降級和限流熔斷機制(主要保護那些寫操作功能,比如下單),確保了南航營銷平台在往後的會員日或其他大促活動期間承受千萬級的訪問流量時仍能平穩地提供服務。

通過這種辦法,在 IT 團隊規模遠遠不如那些有名氣的電商公司同類系統研發團隊的情況下,經過一個月的改造,我們把一個每次都是躺著過零點高峰的系統,變成基本上能安安穩穩地站著過零點高峰的系統。

全天候服務

由於當時應用的問題較多,單鏈路基本難以保證 7*24 小時不間斷服務,三頭兩日會跪一下,最直接的處理方式就是當監控報警時讓運維幫忙重啟。儘管我們有多條鏈路,但一旦某條鏈路 down 機重啟,過程中肯定會影響到部分用戶。我們團隊的研發資源實在有限,而且那段時間全部精力放在確保會員日這種促銷活動上(支撐業務部門沖 KPI),但這個問題又不能放任不管,所以採用比較省事的方式——在 16 年的 4 月初,我們把各條鏈路的 session 狀態信息統一緩存到 Redis-Cluster 中,這樣可以把個別鏈路的 down 機對用戶的影響降到最低,另外寫了簡單監控介面讓監控系統調用,當監控系統通過這個介面發現某條鏈路 down 了就調一下該鏈路上的重啟腳本。

這樣做一方面為團隊爭取了休息時間,不用為故障疲於奔命,減輕研發人員壓力,另一方面其實也算把系統修成 7*24 不間斷服務了,最重要的是能讓團隊有更充分的時間制定優化改造計劃和方案,使得後來我們能在比較從容的情況下通過代碼層面的優化和 JVM 調優等措施把應用出現的各種問題一一解決。

進一步優化

當鏈路能保證 7*24 小時不間斷服務后,我們又回過頭來優化那些會員日和其他促銷活動中用到的寫操作功能介面,如下訂單、派優惠券和領優惠券之類,尤其優惠券相關的介面不但涉及雙表信息寫入,而且還帶事務,併發一高資料庫連接就佔滿。

最初只能通過限流的方式先處理,但這樣做極不合理,活動期間大部分用戶的感受是既派不了券又領不了券。

後來大概在 16 年 7 月改成把入庫數據丟進隊列里,排隊入庫,不過這樣用戶體驗也不好,比如有人點了領券按鈕,然後馬上去券包查看,甚至馬上使用時發現沒券,要等上一段時間才看到剛才所領的券,原因是數據還在隊列里,還沒入庫。

再後來我們就想能否先寫緩存,讀的時候也是先讀緩存,這樣就能滿足用戶需要。可是看看我們的 Redis:

  • 不能支持結構化存儲

  • 不支持事務。

當時 mongoDB 可以支持結構化存儲,而且支持 Sql 查詢,並且承諾即將支持事務,然而我們的存儲中間件已經有 Mysql 和 Redis,出於團隊規模和技術棧的管理,我們不太希望把技術棧搞得太臃腫,因為不想降低本來就不算高的研發效率,避免出現技術實現時出現選擇困難,而且就那麼一兩個寫架構代碼、封裝搭建底層組件的人,維護多套技術豈不吐血。

當時有個同事提議把結構化存儲轉化成 k-v,再利用 key 的命名規則來模仿事務,這樣完全可以做出基於 Redis 作為底層存儲的內存資料庫。於是我們進行了一些 pojo 結構轉換和 key 標籤封裝,並把所有相關的 API 通過 JDBC 來封裝,最終的效果不但支持 POJO 的結構化存儲以及 SQL 語句操作,而且還支持事務。通過這種方式把優惠券信息先緩存到 Redis-Cluster 后再根據我們封裝的」事務「持久化到 Mysql 中,這樣就基本滿足了各方需求。

在代碼上看,在 Service 層把 POJO 持久化到資料庫與緩存到 Redis 是無差別的,為此我的同事把這套實現稱為內存資料庫模塊。

/** * @author DeanPhipray * OBSI DB 存儲示例 * * */ @Transactional public int saveToRedis(String fieldName,Student student,Teacher Teacher) throws RdbException{ Row row=new Row; try { row.setValue(fieldName, SeqFactory.getOID); rStudentDao.insert("dual", row); row.setValue(fieldName, SeqFactory.getOID); rStudentDao.insert("dual", row); row.setValue(fieldName, SeqFactory.getOID); rStudentDao.insert(student); row.setValue(fieldName, SeqFactory.getOID); rTeacherDao.insert(teacher); }catch (Exception e) { log.error("緩存實例失敗"); throw new RdbException("Rdb save fail",e); } return 2; } /** * @author DeanPhipray * OBSI DB query 示例 * * */ public Student query(@RequestParam("id") Long id,String tableName) throws RdbException{ Student student= null; try { String sql = "select * from "+ tableName + "where id = ?"; List params = new LinkedList<>; params.add(id); student= (Student) rStudentDao.query(sql,params); } catch (Exception e) { log.error("查詢實例失敗"); throw new RdbException("Rdb query fail",e); } return student; }

近期團隊開始搞敏捷轉型,我們跟一些敏捷顧問的交流中提到我們一直為技術棧做 keepfit 的理念,基本得到對方的認同。

踩坑經歷一號坑:殭屍連接

在一個月黑風高的上線夜,當大家都以為上線任務快完成時,突然有同事告訴我發布新包重啟系統后,系統無法獲取 Redis-Cluster 鏈接,重啟過好幾次還是這樣,我馬上檢查集群狀態,發現一切正常,但檢查連接數時就驚奇地發現所有節點的連接數到達了上限。我們算了一下覺得很奇怪,因為接入的系統十根手指頭數得完,而且每個系統的配置都是按我們制定的模板配參數,我們最大連接數才配了 200,空閑最大連接數 50,空閑最小連接數是 10,一般各個應用實例只會以 10 個連接連到 Redis-Cluster 各個節點中,怎麼算都到不了連接數的上限啊。為了儘快恢復,我們先通過腳本命令在 Redis 伺服器上清除連接,解去燃眉之急,不過治標不治本,幸虧每次清除完連接,客戶端會自動重連,不影響服務,而連接數再次到達上限,大概要兩天時間。

echo "client list" | redis-cli -c -p {port}|awk -F '=| ' '$12>3600{print $4}' | sed "s/^/client kill /g" | redis-cli -c -p {port}

填坑攻略:消除殭屍鏈接

在隨後幾天里,我們發現客戶端設了最大超時,如果連接一直處於空閑狀態,大概 5 分鐘就會斷開與伺服器之間的長連接,但奇怪的是服務端不承認客戶端的斷連狀態,一直保持該連接,結果從客戶端的伺服器看不到這種連接,但在 Redis 伺服器上卻看到大量這種連接,最終導致服務端連接數被佔滿,無法再創建新連接對外提供服務。為了讓鏈接有一定的彈性,我們在客戶端設置連接超時時間、連接池大小、最大空閑連接數、最小空閑連接數等。

在服務端根據實際情況設置 tcp-keepalived 和 Timeout 這兩個參數,其中建議 Timeout 的值跟客戶端的超時時間一致。

二號坑:客戶端過多

隨著應用場景的逐漸增多,這套緩存系統引起了部門內很多項目組的興趣和關注,接著就是紛紛踴躍接入,一下子誕生了很多客戶端,帶來的問題就是連接數配置難以統一規管,連接數暴增,結果某些系統 / 個別鏈路分不到連接,這一來就引出一個比較經典的場景,某大領導用我們的系統總是報錯,而我們模仿操作想重現錯誤時,基本是正常 (讓我們極度崩潰)。

填坑攻略:搭建代理層

這個問題發生時監控系統是不會報警的,因為監控系統是固定頻率發送檢測請求,一直固定佔用著一條鏈路,而且此時的監控系統還沒去監控集群的連接數。後來我們通過跨鏈路的日誌分析系統檢查日誌時發現個別應用連不上 Redis-Cluster, 再看看 Redis 伺服器上的連接數是處於爆滿狀態,不過絕對大部分連接是空閑狀態,沒數據流動的,由此就誕生了用代理把連接統一管理的想法。

接下來就搭建了一套輕量級的代理層集群統一管理 Redis-Cluster 鏈接,採用 Netty 框架處理各個系統 / 各條鏈路的客戶端請求。

在客戶端和代理之間,代理模擬 Jedis 跟 Redis 之間的通訊協議,以 nio 的方式處理併發請求,在代理與 Redis-Cluster 之間採用 socket 長連接復用方式做請求轉發,原本的客戶端完全無需做任何代碼改動就能接入代理集群。

代理集群會在 Redis-Cluster 中緩存代理集群的節點信息和刷新各個節點的健康狀態,因為 Jedis 客戶端會定時詢問集群節點信息,而代理集群只需把 Master 節點替換為代理集群節點,並且對代理集群節點做一次平均的 Hash Slot 分片就能確保:

  • 客戶端請求集中連接到代理集群上

  • 代理集群在動態擴展新節點時能被客戶端自動發現

三號坑:內存最大值限制

起初緩存的數據比較少,一直沒配最大內存限制,隨著接入系統越來越多,緩存數據量不斷增大,結果在某個風和日麗的白天,內存被擠爆,系統除了報 Cluster down 外,並沒更清晰的報錯,當時我們一臉迷惘,莫名其妙地查了 1 個多小時后才發現伺服器內存被耗光了。

填坑攻略:設置最大內存限制

在服務端根據伺服器資源的實際情況設置 maxmemory 的大小,這樣有個好處就是當超過這個值時,Redis 會讓 set 操作失敗,而且有明確的異常信息返回。這個坑解決辦法雖然非常簡單,但極易被忽略,屬於暗溝。

四號坑:aof 文件佔滿磁碟空間

有一天我們剛好完成了一個季度的任務,正準備享受那份難得的按時下班帶來的小愉悅,說時遲,那時快,監控報警!集群中某台伺服器上的所有實例停止服務,我們馬上嘗試重啟上面的實例,但於事無補。於是我們只好按部就班,老老實實從 cpu、內存、磁碟空間、Redis 日誌等等逐個檢查,結果發現磁碟空間滿了,AOF 一直阻塞,一個 aof 文件體積竟然有十幾 G(其他正常的實例上 aof 文件才 2-3G),為了儘快恢復我們果斷把其中兩個從節點實例的 aof 文件刪掉,然後再重啟實例,然後就恢復正常了,不過當我們順手重啟這台伺服器一個沒有刪除 aof 文件的實例后,這個實例的 aof 文件在重啟後接近 1 分鐘后從十幾 G 變成了 2G,在此期間該實例進入僵死狀體(單線程的弊端),這明顯進行了 aof 重寫啊。

填坑攻略:控制 aof 文件大小

此後每天執行 BGREWRITEAOF 指令腳本,監控磁碟空間,減少伺服器上 Redis 的實例數並騰空一半內存,因為一台機上部署多個 Redis 實例會有個隱患,萬一多個實例扎堆做 AOF 重寫會導致 swap 或者 oom,導致重寫失敗,這種失敗會不斷重複,直至 aof 文件像滾雪球似的變大,最終塞滿磁碟,另外重寫體積較大的 aof 文件時,Redis 會進入 IO 阻塞狀態,停止對外服務。

結語和寄望

在近一年半的探索和實踐過程中,我們團隊一路坑坑窪窪,幾經顛沛地走到現在,大體上摸索出一套「高可用,高可靠,高併發下低延遲」的緩存解決方案。

希望這套脫胎於 Redis(紅寶石) 的「黑曜石」系統,乘著「南航 e 行」這股東風,能得到更好的持續的優化,在未來的日子裡走得更穩、更遠。

作者介紹

鄧卓楠,2015 年 6 月接手南航商務移動後台重構工作,2016 年 7 月至今擔任南航商務移動團隊總體架構規劃,主要負責後台介面優化與系統重構。

你知道公眾號上有很多優質內容,但除了在歷史列表人肉檢索,查詢渠道並不多。有沒有一種搜索方式,能整合 InfoQ 中文站、12 大微信公眾號矩陣的全部資源?極客搜索,這款針對極客邦科技全站內容資源的輕量級搜索引擎,做到了!掃描下方二維碼,極客(即刻)試用!

細說雲計算

「細說雲計算」是InfoQ旗下關注云計算技術的垂直社群,投稿請發郵件到[email protected],註明「細說雲計算投稿」即可。



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦