又拍雲葉靖:OpenResty 在又拍雲存儲中的應用

2019 年 7 月 6 日,OpenResty 社區聯合又拍雲,舉辦 OpenResty × Open Talk 全國巡迴沙龍·上海站,又拍雲平臺開發部負責人葉靖在活動上作了《OpenResty 在又拍雲存儲中的應用》的分享。OpenResty x Open Talk 全國巡迴沙龍是由 OpenResty 社區、又拍雲發起,邀請業內資深的 OpenResty 技術專家,分享 OpenResty 實戰經驗,增進 OpenResty 使用者的交流與學習,推進 OpenResty 開源項目的發展。活動將陸續在深圳、北京、武漢、上海、成都、廣州、杭州等城市巡迴舉辦。html

葉靖,又拍雲平臺開發部負責人,目前主要負責又拍雲彈性雲處理平臺以及內部私有云的設計和開發工做,兼部分文件上傳接口相關的工做。對 Python/Lua/Go 等語言有較深刻的研究,在 ngx_lua 和 OpenResty 模塊開發方面有豐富經驗,專一於高併發、高可用服務架構設計,對 Docker 容器有較多的實踐。平時熱衷於參與開源社區分享開源經驗。git

如下是分享全文:github

你們好,我是又拍雲葉靖,今天與你們分享 OpenResty 在又拍雲存儲系統中的應用,一方面介紹 OpenResty 的應用,另外一方面會介紹又拍雲存儲系統的原理,又拍雲使用 OpenResty 來實現雲存儲的網關層和 API 接入層。算法

分佈式存儲,尤爲是公有云存儲系統都離不開三個要求:sql

  • 高可用,系統不管如何都不可能出現不可服務的狀態,即便機器掛了幾臺,都應該能夠寫入,並且須要儘量地能夠讀取;
  • 易擴展,存儲的容量是在不斷上升的,並且上升的速度很是快,若是系統不能支持快速、方便的擴展,整個系統在運維上會面臨很大的壓力;
  • 易維護,存儲系統有不少組件,這些組件必需要很是容易維護,不能有太多相互的依賴。

存儲數據I:拆分

分區

存儲數據的拆分,第一步要作分區,這在分佈式系統裏是很是重要的概念,也是最經常使用的作法。在一個大型數據庫中,一般會把整個數據庫分紅一個個小的子集,最經常使用的作法就是按 key 分區。對於一個雲存儲系統,key 就是 url 後面的 path,又拍雲就是根據 url 後面的 path ,按 A 到 Z 來排列,把數據進行分區。這樣分區能夠方便地作前綴掃描,由於咱們常常會作目錄,列目錄無非就是相同的 path 前綴的一些文件,若是 key 是有序排列,這個操做就會很是方便。數據庫

第二步操做是須要對 key 進行 hash,來把訪問打散,下文會詳細介紹。api

上面的這些工做在又拍雲都是用 Lua 代碼來寫的,由 OpenResty 完成數據拆分。
緩存

上圖是的 key 的 hash,雲存儲文件原始的請求是一個 url,可是若是寫到存儲時也用這個 url 做爲它的 key 會形成熱點很是嚴重。又拍雲有超過 50 萬的付費客戶,通過咱們觀察,其中有不少客戶,尤爲是大客戶,他們文件的 key 都是帶日期的,所以文件的 key 的前綴可能都是某一個日期,而最近上傳的文件確定是最熱的,這就會致使今天上傳的文件所有放在同一臺機器上,會使這臺機器的帶寬被撐滿。所以咱們把文件的 url 變成一個 hash,這個 hash 並非 key 的 MD5,也不是某種算法算出來的 hash,其實就是內部生成的一個 UUID 對應這個文件,而後把對應關係記錄下來。架構

索引的拆分

索引在存儲系統裏是文件的元數據信息,元數據信息是指這條記錄原始的 key、內部的 key、文件大小、存在於哪些集羣內等相似的信息。併發

上圖的流程是外部存儲訪問的上傳文件流程。首先是發一個 put 請求,put 請求的 url 就是文件的 key。接着到 OpenResty 層,這層是基於 OpenResty 作的存儲網關,存儲網關會把 url 生成一個內部的 UUID 並作對應。生成以後會帶着 UUID 作上傳,接收數據,這裏咱們是用 Lua 來作,ngx_lua 裏面有一個 req.socket,它會拿到 socket 而後讀取上傳的數據,數據讀到以後存到一個叫 Block 集羣內,Block 集羣是真正存放文件二進制的地方。整個過程是流式的,邊讀邊寫,因此不會帶來一些大文件的問題,當文件數據存完以後,再把 UUID、一些元數據信息寫到 KeyIndex (元數據集羣)。

內容內部拆分

第二步要對 Block 數據進行拆分,前面提到的只是一個簡單的過程,其實在接收上傳數據並寫到 Block 集羣的過程當中,並非把全部數據都寫到同一個 Block 集羣中,而是會作拆分。又拍雲支持最大 40T 的文件上傳,如今用的磁盤最大也就單個 8T,單文件 40T 是如何支持呢?作法實際上是把 Block 作一個拆分,假如把這個數據拆分紅 10M、10M 的塊,能夠把他上傳到不一樣的機器和磁盤,只要記錄下它的對應關係就能夠了。

實際上在 OpenResty 網關裏,接收數據的原理也是如此,先收一個 Block 大小,好比 10 M,而後 10M 變成一個 UUID-0 寫到 Block 裏,再收第二個 10 M,變成 UUID-1,寫到 Block 集羣,一直到接收完畢,這樣一個 G 的文件可能就產生了 100 多個 「UUID-數字」的分塊文件,他們分別被存到不一樣的機器、磁盤裏,這樣就能支持超大的文件存儲了。

接收數據並寫入數據的過程實際上是有策略的。不一樣於通常的 OpenResty 用法,好比在作一些鑑權操做、限速操做,只要是在 access 階段或者 rewrite 階段去作一些控制,後面就交給 Nginx proxy_pass 作代理,把數據代理出來就能夠了;而在這裏是徹底沒有走 Nginx proxy_pass,直接用 Lua 代碼去控制數據的讀和寫,而後返回,整個的過程都是 Lua 代碼去控制。

總的來講,上面的內容講了拆分,一共分爲三步:

  • 第一次拆分,文件路徑(url) 對應多個 Meta 集羣,固定分區。在存儲裏面,Meta 集羣是有多個的,Block 集羣也有不少個,一個 Meta 集羣會對應多個 Block 集羣這樣的關係。當文件上傳上來,它應該存到哪一個集羣是有策略的,第一步會對 url 作判斷,這個 url 屬於哪一個存儲分區。咱們常常會在建存儲時看到一個選項,建華東數據中心、華南數據中心仍是華北數據中心,此時它已經肯定了,這個存儲空間之後的數據永遠都是寫到哪一個 Meta 集羣中,此一次拆分主要作這個事情;
  • 第二次拆分,一個 Meta 集羣對應多個 Block 集羣,這是 Meta 集羣根據內部的一些權重和配置作的調整;
  • 第三次拆分,Meta 和 Block 子系統內部分區,把一個數據分到不一樣的磁盤不一樣的機器。

存儲數據II:路由

第二部份內容,介紹存儲裏面的路由。

路由模式選擇

一般提到路由會想到一種模式就是代理,代理的角色是上圖中間的第②種,它中間作了一層代理,全部下面的 MySQL 或 Redis 都只是作單節點的存儲,其中前面的代理知道下面全部的節點的存儲的分佈的路由狀況,全部的請求都是通過代理的。

左邊第①種模式全部的節點都是對等的,全部節點都知道數據存在哪一個節點上,Redis 在訪問的時候就能夠隨便找一個節點訪問,若是數據恰好在這個節點上就直接返回,若是不在這個節點上,此節點會代理到其餘的節點上去。

第③種是 Java 生態系統常用的,像 Hbase 就是是使用第③種方式,路由信息存在 client 中,client 直接找到那個節點,省去了好多中間的過程。可是有一個問題是 client 會很是複雜,在存儲系統裏面,第③種確定是不行的,由於 client 即客戶的 rest api ,它只有一個 HTTP,不可能帶路由信息。

又拍雲選擇的是第②種模式,第②種模式中 routing tier 就是 OpenResty 存儲網關,它裏面有路由信息,知道這個 url 應該去哪一個集羣。上圖是一個下載文件的 get 請求流程,一個 url 進來後,網關會先去 Meta 集羣,即左邊的 KeyIndex,拿 url 去找到內部對應的 UUID,而後拿內部的「 UUID-數字」,去 Block 集羣裏把的分塊讀出來,而後一塊一塊流式地吐回去,這就是 get 的過程。

Meta 集羣路由都是固定路由,分爲幾個層次:

  • 不一樣的用戶或者空間,一個 url 最前面是空間,空間應該對應到哪一個存儲集羣,這些都是固定的;
  • 不一樣存儲類型,好比普通的存儲、低頻的存儲它們分別是在哪些集羣內都是固定不可改變的;
  • 不一樣的索引功能。

列目錄

又拍雲內部經過網關列目錄,簡單來講就是 key 的前綴匹配,咱們建了單獨的目錄系統來實現目錄功能。

上圖中左邊的 KeyIndex (Meta 集羣),裏面的數據會實時同步,把一些須要的信息同步到目錄索引中,好比列目錄只須要文件的 key 的名稱、大小、類型、修改時間等,它會把這些信息抽取出來輸入到目錄系統裏面,若是前面的網關收到的是一個列目錄的請求,就會直接去目錄系統裏面,根據前綴匹配把數據列出來。

文件按時間過濾

咱們常常會碰到一個需求,要按照文件的上傳時間來列最近上傳的文件,或者某天上傳的文件,亦或是一年前的上傳文件。此時須要單獨再建一套按時間排列的索引,不一樣於本地的文件系統,本地的文件系統少,要怎麼列就怎麼列,而云存儲文件數量都是千億以上級別的,若是不事先作好索引,等請求到了再去列,是不可能完成的。

路由

Block 集羣的路由和 Meta 集羣同樣,也是按照存儲類型和用戶空間劃分。此外,不一樣的 Block 集羣能夠有不一樣的 Weight ,來控制它不一樣的寫入量,若是一個新加的 Block 集羣須要多寫一點數據,就能夠把它的 Weight 調高。

又拍雲很早以前就開源了一個模塊 lua-resty-checkups(https://github.com/upyun/lua-resty-checkups),路由在 OpenResty 裏面就是經過這個模塊來實現,這個模塊在又拍雲幾乎全部的 ngx_lua 的機器上都有,已經用了好幾年了,很是穩定。這個模塊主要的工做是管理 upsteam 地址,去作主動的健康檢查、被動的健康檢查、動態更新 upsteam 地址以及路由的策略等,前面提到的全部的路由功能都是經過這個模塊來實現。

又拍雲把路由的配置放在 consul 裏面,OpenResty 網關會定時去 consul 裏面拿最新的配置,而後緩存到本身的進程裏面,目前咱們是一分鐘拿一次路由配置,緩存到進程裏,每一個進程都按照這份配置來工做。關於配置功能,又拍雲還開源了一個項目 slardar (https://github.com/upyun/slardar),這裏的配置原理和 slardar 原理如出一轍,並且不少模塊是直接拿過來的。

存儲數據III: 經常使用功能

前面介紹了上傳、下載和列目錄,咱們常用的還有 Head 操做,Head 操做是檢查文件存不存在,它和文件真實的數據沒有關係,Head 過來後網關就會拿這個 url 去檢查,看是否有這個文件,若是存在就 200 ,不存在就 404 。

DELETE

Delete 操做是不須要 Block 數據的參與的,由於在一個存儲系統裏面,Delete 並無從磁盤上把數據真正地刪除掉,這裏的刪除只是在元數據庫 KeyIndex 裏面作一個標記,把這個文件標記爲刪除。而數據的清理實際上是經過一個異步的 worker 來收集已經被標記刪除的文件,而後去 GC 把它們真正地刪除掉,而且 GC 會有延遲,並非標記刪除就立刻就去 GC,由於有可能會遇到一些誤操做的狀況,爲了不這種狀況,咱們一般會把 GC 延遲 7 天甚至 1 個月。整個過程,網關會經過 Lua 中的 kafka 模塊,發消息到 kafka 隊列,代表這是一次刪除操做,kafka 這條消息就會被 GC 的消費者消費,當它拿到這條日誌就會定時,時間到了就會去 Block 數據裏面把這個文件真正刪除。

其餘經常使用功能

存儲系統裏除了剛纔說的操做以外,還有不少其餘的操做:

  • Move,重命名;
  • Copy,拷貝;
  • Append,追加寫;
  • Patch,修改一些文件的源信息;
  • Mkdir,建目錄;
  • Random,隨機寫;

Random 功能目前咱們尚未實現,可是隨機讀的功能是能夠的。除了 Random 功能,其餘的都是能夠經過 Lua 代碼來實現的,這些是用 OpenResty 來寫業務邏輯的很好的一個例子。

存儲數據IV: 擴容

接下來介紹存儲的擴容,這部份內容和 OpenResty 關係不大,可是是存儲必定要講的一個問題。擴容涉及兩個方面,一個是 Meta 集羣的擴容,另外一個是 Block 集羣的擴容。

Meta 集羣的擴容

Meta 集羣存的是文件的元數據信息,value 其實很是小,可能就只有幾百個字節,再大也大不過 1K,它的擴容是相對容易的,好比加一臺機器,它的總量也小,balance 速度很是快。

事實上,咱們通常不會作 Mata 集羣的擴容,印象中又拍雲這麼多年只作過一次,由於 Meta 集羣的容量能夠算出來的,好比要支持一千億條文件的存儲,能夠計算出大概須要的 Meta 集羣的容量,幾百個 T 確定夠了,所以你買一批設備放在那,就不用考慮擴容的事情了。總的來講,Meta 集羣的擴容是比較簡單的。

Block 集羣的擴容

相對來講比較麻煩的是 Block 集羣的擴容。Block 的文件可大可小,它的容量很是大,幾十個P,甚至幾百個 P。若是你的一個集羣有好幾個 P,當你加一臺機器要從新 balance ,全部的其餘的機器要挪出一部分的數據來寫到當前你新加的這臺機器上,這是一件很是恐怖的事情,可能會須要幾天甚至一星期,整個集羣都處於一種數據倒來倒去的狀態,這是確定會影響業務的。

咱們要儘可能避免這種 balance 的操做,因而想了一種比較取巧的辦法,儘可能不作集羣內部的 balance,當須要擴容時,就直接新增一個集羣。固然有時候也是須要作 balance,若是必定要加就讓它慢慢擴,擴幾天或者一星期。可是咱們通常的作法是估算出下一個集羣須要多少機器、多少容量,直接整個集羣上去,在網關層把整個集羣配進去,而後調高 Weight 值,讓大量的數據都寫到新的集羣中,這樣去作整個雲存儲的擴容。

其餘V

複製

不管是 Meta 集羣仍是 Block 集羣,都須要有複製的能力,由於咱們都是使用多副本存儲,或者 EC 存儲。Meta 集羣能夠選用 Hbase,Postgresql/Mysql,Hbase 有 HDFS 能自帶複製功能,而若是是 Postgresql/Mysql,須要配置它的主從或者給它作一些同步、複製的功能。

此外,Meta 數據的備份也很重要,由於 Meta 集羣關係到全部的數據是否可以訪問,一旦出現問題就會很是嚴重,因此這裏就須要在網關層把 Meta 數據寫到 kafka,另一種辦法是直接在數據庫弄個插件,再導到 kafka。

Block 集羣的複製比較複雜,一般是集羣內部要完成的事情,和網關層沒有太大的關係。

事務

事務也是存儲很是重要的概念,在雲存儲系統中,沒有辦法作到像單機數據庫那樣的事務,它只能作到單個對象級別的事務,保證這個對象是處在事務裏面的。整個操做是須要一個 Meta 集羣支持一個 CAS(compare-and-set)操做。一個對象不能被兩個線程同時寫入,這樣會形成其中一個線程失敗,會之後面寫入的 Meta 信息爲準。

前面提到一個 Key 只能一次被寫入,這裏會涉及到限速,咱們使用的是 openresty/lua-resty-limit-traffic,又拍雲在此基礎上增長了 token bucket 的方法,token bucket 這個模塊目前也是開源放在咱們的 github 上,咱們內部都是用這個模塊,測試下來這個模塊是最平滑的,能很好應對突發的請求。

分佈式存儲以外

前面介紹的都是存儲的網關層、以及存儲下面的功能,其實作一個雲存儲系統,不僅僅是作一個網關或存儲,後面還有許多配套的東西,好比 API,API 又拍雲也是經過 Lua 來寫的,這裏也有不少的業務邏輯,好比表單 API 涉及到表單的解析、參數的解析、上傳到存儲網關等。此外,還有認證的算法、斷點續傳也都是經過 Lua 來寫的。斷點續傳,是指一個大文件如十幾個 G 的文件,能夠把它切成 1M、1M 的文件塊分別傳到存儲,存儲會先把這些文件寫到 Block 集羣,當接收到最後一個 finish 消息,存儲就會把這些臨時的數據拼成一整個文件。

又拍雲存儲系統

上圖是又拍雲存儲系統模塊關係圖,OpenResty 在裏面主要是左上角這塊,UpyunApi 是又拍雲的 API 層,像認證、鑑權、上傳的表單 API 等都是它作的事情;Avalon 是 OpenResty 的雲存儲網關,內部與存儲相關的流量都會通過這裏,包括 CDN 的 get 流量也會通過這裏;左邊的是 Meta 集羣,它有不少組件,包括 Hbase、Postgresql、Redis 以及備份的工做;右邊的是一些消費者,由於存儲系統須要不少的消費者來完成一些特定的工做,好比自動過時、TTL、GC、壞盤的修復等;最下面的部分是 Block 集羣,是真正存數據的地方。

又拍雲 OpenResty 相關的開源項目

下面是前面提到的一些又拍雲開源出來的開源項目,這些在 upyun 的倉庫裏面均可以找到,又拍雲內部也是大量使用這些模塊,主要包括:

[1] upyun/slardar : https://github.com/upyun/slardar

[2] upyun/lua-resty-checkups : https://github.com/upyun/lua-resty-checkups

[3] upyun/lua-resty-limit-rate :https://github.com/upyun/lua-resty-limit-rate

演講視頻及PPT下載:

OpenResty 在又拍雲存儲中的應用 - 又拍雲

相關文章
相關標籤/搜索