UPYUN CDN 高可用架构实践
10 月 22 日,2015 全國架構師大會(SACC)在北京新云南皇冠假日酒店盛大召開,來自全國各地的逾 2000 名開發者參加大會。今天 ,UPYUN 架構師張聰受邀出席了會議,并做了關于 UPYUN CDN 專題技術實戰分享。
?
在“互聯網+ ”的構建中,云、網、端被很多人稱作“最強有力的三件武器”。而對于擔負著數據托管重責的云計算平臺來說,保證穩定可靠是最為關鍵的。這點,必須通過構建完善的應用架構來實現。在分享中,張聰圍繞著架構議題,首先闡述了 NGINX 作為反向代理服務器,如何取其精華為 UPYUN 的 CDN 節點提供服務,使得程序員在開發一個模塊的代碼從原來的 500 行減少到 50 行,并且保持同樣的性能。
ngx_lua 在 UPYUN CDN 的應用
張聰介紹,UPYUN 的 CDN 節點大量使用了 NGINX 作為反向代理服務器,其中絕大部分的業務邏輯由 Lua 來驅動。而 UPYUN CDN 的框架,就是借鑒了 Openresty 的組織方式,把 ngx_lua 以及 UPYUN 需要用到的 lua-resty-* 類庫直接集成進來自身系統維護的。Openresty 是一套基于 NGINX 核心的相對完整的 Web 應用開發框架,包含了 ngx_lua 在內的眾多第三方優秀的 NGINX C 模塊。
UPYUN CDN 線上通過 Redis 從復制的方式由中心節點向外圍節點同步用戶配置,另外,由于 Redis 本身不支持加密傳輸,UPYUN 還在此基礎上利用 stunnel 對傳輸通道進行了加密,保障數據傳輸的安全性。
緩存是萬金油
當然,不是說節點上有了 Redis 就能直接把它當做主要的緩存層來用了,要知道從 NGINX 到 Redis 獲取數據是要消耗一次網絡請求的,而這個毫秒級別的網絡請求對于外圍節點巨大的訪問量來說是不可接受的。所以,在這里 Redis 更多地承擔著數據存儲的角色,而主要的緩存層則是在 NGINX 的共享內存上。
根據業務特點,我們的緩存內容與數據源是不需要嚴格保持一致的,既能夠容忍一定程度的延遲,因此這里簡單采用被動更新緩存的策略即可。ngx_lua 提供了一系列共享內存相關的 API ,可以很方便地通過設置過期時間來使得緩存被動過期,值得一提的是,當緩存的容量超過預先申請的內存池大小的時候,ngx.shared.DICT.set 方法則會嘗試以 LRU 的形式淘汰一部分內容。
以下代碼片段給出了一個簡陋的實現,當然我們下面會提到這個實現其實存在不少問題,但基本結構大致上是一樣的,可以看到下面區分了 4 種狀態,分別是:HIT 和 MISS, HIT_NEGATIVE 和 NO_DATA,前兩者是對于有數據的情況而言的,后兩者則是對于數據不存在的情況而言的,一般來說,對于 NO_DATA 我們會給一個相對更短的過期時間,因為數據不存在這種情況是沒有一個固定的邊界的,容易把容量撐滿。
什么是 Dog-Pile 效應?
在緩存系統中,當緩存過期失效的時候,如果此時正好有大量并發請求進來,那么這些請求將會同時落到后端數據庫上,可能造成服務器卡頓甚至宕機。
很明顯上面的代碼也存在這個問題,當大量請求進來查詢同一個 key 的緩存返回 nil 的時候,所有請求就會去連接 Redis,直到其中有一個請求再次將這個 key 的值緩存起來為止,而這兩個操作之間是存在時間窗口的,無法確保原子性:
避免 Dog-Pile 效應一種常用的方法是采用主動更新緩存的策略,用一個定時任務主動去更新需要變更的緩存值,這樣就不會出現某個緩存過期的情況了,數據會永遠存在,不過,這個不適合我們這里的場景;另一種常用的方法就是加鎖了,每次只允許一個請求去更新緩存,其它請求在更新完之前都會等待在鎖上,這樣一來就確保了查詢和更新緩存這兩個操作的原子性,沒有時間窗口也就不會產生該效應了。
lua-resty-lock – 基于共享內存的非阻塞鎖實現
首先,我們先來消除下大家對鎖的抗拒,事實上這把共享內存鎖非常輕量。第一,它是非阻塞的,也就是說鎖的等待并不會導致 NGINX Worker 進程阻塞;第二,由于鎖的實現是基于共享內存的,且創建時總會設置一個過期時間,因此這里不用擔心會發生死鎖,哪怕是持有這把鎖的 NGINX Worker Crash 了。
那么,接下來我們只要利用這把鎖按如下步驟來更新緩存即可:
- 檢查某個 Key 的緩存是否命中,如果 MISS,則進入步驟 2。
- 初始化 resty.lock 對象,調用 lock 方法將對應的 Key 鎖住,檢查第一個返回值(即等待鎖的時間),如果返回 nil,按相應錯誤處理;反之則進入步驟 3。
- 再次檢查這個 Key 的緩存是否命中,如果依然 MISS,則進入步驟 4;反之,則通過調用 unlock 方法釋放掉這把鎖。
- 通過數據源(這里特是 Redis)查詢數據,把查詢到的結果緩存起來,最后通過調用 unlock 方法釋放當前 Hold 住的這把鎖。
當數據源故障的時候怎么辦?NO_DATA?
同樣,我們以上面的代碼片段為例,當 Redis 返回出現 err 的時候,此時的狀態即不是 MISS 也不是 NO_DATA,而這里統一把它歸類到 NO_DATA 了,這就可能會引發一個嚴重的問題,假設線上這么一臺 Redis 掛了,此時,所有更新緩存的操作都會被標記為 NO_DATA 狀態,原本舊的拷貝可能還能用的,只是可能不是最新的罷了,而現在卻都變成空數據緩存起來了。
那么如果我們能在這種情況下讓緩存不過期是不是就能解決問題了?答案是 yes。
ua-resty-shcache – 基于 ngx.shared.DICT 實現了一個完整的緩存狀態機,并提供了適配接口
恩,這個庫幾乎解決了我們上面提到的所有問題:1. 內置緩存鎖實現 2. 故障時使用陳舊的拷貝 – STALE
所以,不想折騰的話,直接用它就是的。另外,它還提供了序列化、反序列化的接口,以 UPYUN 為例,我們的元數據原始格式是 JSON,為了減少內存大小,我們又引入了 MessagePack,所以最終緩存在 NGINX 共享內存上是被 MessagePack 進一步壓縮過的二進制字節流。
序列化、反序列化太耗時?!
由于 ngx.shared.DICT 只能存放字符串形式的值(Lua 里面字符串和字節流是一回事),所以即使緩存命中,那么在使用前,還是需要將其反序列化為 Lua Table 才行。而無論是 JSON 還是 MessagePack,序列化、反序列操作都是需要消耗一些 CPU 的。
如果你的業務場景無法忍受這種程度的消耗,那么不妨可以嘗試下這個庫:lua-resty-lrucache。它直接基于 LuaJIT FFI 實現,能直接將 Lua Table 緩存起來,這樣就不需要額外的序列化反序列化過程了。當然,我們目前還沒嘗試這么做,如果要做的話,建議在 shcache 共享內存緩存層之上再加一層 lrucache,也就是多一級緩存層出來,且這層緩存層是 Worker 獨立的,當然緩存過期時間也應該設置得更短些。
當然,我們在這基礎上還增加了一些東西,例如 shcache 無法區分數據源中數據不存在和數據源連接不上兩種狀態,因此我們額外新增了一個 NET_ERR 狀態來標記連接不上這種情況。
ngx_lua 在 UPYUN 還有很多方面的應用,例如流式上傳、跨多個 NGINX 實例的訪問速率控制等,這些保證了 UPYUN 的Web 應用開發框架整體的穩固性,也是UPYUN 多年來能夠保持 CDN 節點服務領先的重要基礎。
那么,用戶該如何感知NGINX的便捷性呢?傳統 CDN 企業的用戶只能適配該企業特定的功能模塊,UPYUN 將之開發成后臺用戶可操作的功能模塊,用戶可用類似于 NGINX 的配置來配置回源源站(見上圖),達到方便、快捷、滿足用戶的個性化需求。
總結
以上是生活随笔為你收集整理的UPYUN CDN 高可用架构实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DNS support edns-cli
- 下一篇: JSON、Protobuf、Thrift