什么是對象存儲
對象存儲一般是指以“對象”(object)為數(shù)據(jù)組織形式的存儲服務(wù)。引用Wikipedia上給出的定義如下:
Object storage (also known as object-based storage[1]) is a computer data storage architecture that manages data as objects, as opposed to other storage architectures like file systems which manage data as a file hierarchy, and block storage which manages data as blocks within sectors and tracks
從這段描述中其實(shí)可以看出,對象存儲其實(shí)并不適合主動的定義方式,而是通過與其它典型存儲類型作比較,突出其自身特點(diǎn)。對象存儲的單位稱為對象,與文件系統(tǒng)里的文件接近,但對象存儲不一定要支持文件系統(tǒng)里樹狀的目錄結(jié)構(gòu)。真實(shí)的對象存儲服務(wù)包括Facebook存儲的圖片存儲和Spotify歌曲庫等。
對象存儲定義并不限定數(shù)據(jù)模型,但在實(shí)現(xiàn)中大家一般采用鍵 + 數(shù)據(jù) + 元數(shù)據(jù)的結(jié)構(gòu)來描述一個對象(如上圖所示),其中:
●對象鍵:用于唯一確定一個對象的id;
●數(shù)據(jù):對象的數(shù)據(jù)本身,一般為一個文件,例如一張圖片、一個視頻文件等等;
●元數(shù)據(jù)(metadata):描述對象的一組結(jié)構(gòu)化數(shù)據(jù),稱為元數(shù)據(jù),一般以key-value的形式保存;
所以,對象存儲也可以看做一種key-value形式,其中key對應(yīng)對象的鍵,value對應(yīng)一個文件和元數(shù)據(jù)。另外,一般對象存儲還有桶(bucket)的概念,主要用于方便對象管理,在一個桶里對象鍵是唯一的,但不同的桶之間對象鍵一般是可以重復(fù)的。
對象存儲現(xiàn)狀分析
本文討論面向工業(yè)大數(shù)據(jù)的對象存儲技術(shù)實(shí)踐,但對象存儲本身并不是一個新穎的概念,幾乎所有主流公有云服務(wù)供應(yīng)商都有自己的對象存儲服務(wù)。下面我們簡要介紹一些常見對象存儲服務(wù)的功能和特點(diǎn)。商業(yè)對象存儲服務(wù)中一般包含較多的定價和安全策略,這些不是本文考慮的重點(diǎn)問題。本文更關(guān)注工業(yè)大數(shù)據(jù)應(yīng)用場景對對象存儲在功能上的需求,以及滿足這些需求的所依賴的技術(shù)要素。
●AWS S3
AWS是Amazon的公有云服務(wù),S3是其對象存儲服務(wù)。每個 Amazon S3 對象都有鍵、數(shù)據(jù)和元數(shù)據(jù),與概念介紹一致。在S3,鍵表述為一種類似文件系統(tǒng)目錄的層級結(jié)構(gòu),例如:
這看起來像Unix文件系統(tǒng)里的路徑,但有一點(diǎn)區(qū)別,即它是以桶的名稱開始(例如上面的Photos和Docs是桶的名稱),而不是Unix文件系統(tǒng)里的“/"根目錄。層級結(jié)構(gòu)方便用戶以類似文件系統(tǒng)的方式組織對象數(shù)據(jù),用戶甚至可以創(chuàng)建”文件夾“。當(dāng)然,這些文件夾只是交互上的設(shè)計(jì),而實(shí)際上鍵只是一個有層級結(jié)構(gòu)的字符串。
元數(shù)據(jù)包含兩部分,通用信息和自定義內(nèi)容。通用信息由系統(tǒng)自動生成的,比如對象創(chuàng)建日期和文件大小等等;自定義內(nèi)容由用戶創(chuàng)建,以key-value形式存儲。用戶可以按鍵獲取對象文件和元數(shù)據(jù)。S3還允許用戶用標(biāo)簽(tag)來標(biāo)記對象,標(biāo)簽也是以key-value形式保存的,通過接口可以獲得某個對象對應(yīng)的標(biāo)簽列表。標(biāo)簽的目的是方便用戶將對象分類,但S3并不支持按照標(biāo)簽來篩選對象數(shù)據(jù)。但利用AWS檢索服務(wù),可以對文件名和元數(shù)據(jù)自動構(gòu)建索引,在里的文章有詳細(xì)介紹。
對于一些結(jié)構(gòu)化(或半結(jié)構(gòu)化)數(shù)據(jù),例如CSV、JSON或Parquet格式文件,S3支持稱為S3 Select功能。S3 Select使用戶可以使用SQL來獲取對象內(nèi)容,例如:
對應(yīng)從CSV格式的對象S3Object里提取前兩列,同時需要滿足第三列大于100的條件。
S3是AWS的核心服務(wù)之一,其推出年代較早,對后續(xù)的對象存儲服務(wù)產(chǎn)生了深遠(yuǎn)的影響。國內(nèi)的阿里云OSS和騰訊云COS與S3的功能比較接近;Openstack Swift是一種開源的對象存儲服務(wù),其功能上也類似S3。在數(shù)據(jù)一致性方面,S3、Swift和COS支持最終一致性;OSS支持強(qiáng)一致性。
●Azure Blob Storage
Azure Blob是微軟提供公有云的對象存儲方案。Azure Blob采用與AWS S3相似的數(shù)據(jù)模型,而在應(yīng)用場景上有一些細(xì)化。Azure Blob支持三種存儲場景:
Block blobs:用于管理隨機(jī)訪問的對象數(shù)據(jù),讀寫單位一般是整個對象文件;
Append blobs:對文件追加(append)操作有所優(yōu)化,適合類似日志存儲;
Page blobs:對文件內(nèi)容隨機(jī)訪問有所優(yōu)化,可用于虛擬機(jī)的虛擬硬盤文件;
Azure Blob除了對文件訪問場景進(jìn)行了細(xì)化,還支持Azure Search建立對象數(shù)據(jù)索引(Azure Search是微軟公有云服務(wù)推出的通用索引服務(wù))。利用Azure Search可以查詢存儲在Azure Blob里的文件內(nèi)容,根據(jù)查詢條件訪問這些數(shù)據(jù)。目前支持索引的文件格式包括PDF、Office文檔、文本文件、JSON、CSV等等。如果被索引的數(shù)據(jù)發(fā)生變化,Azure Search支持增量索引。所以相比AWS S3,Azure Blob支持更靈活的訪問方式。Azure Blob支持?jǐn)?shù)據(jù)強(qiáng)一致性。
●Google Cloud Storage
Google Cloud Storage是Google推出的對象存儲服務(wù),它的數(shù)據(jù)模型與AWS S3類似,沒有Azure Blob對數(shù)據(jù)存儲的細(xì)分場景。與Azure Blob類似的是Google Cloud Storage支持使用BigQuery建立索引和查詢,主要支持一些結(jié)構(gòu)化數(shù)據(jù)類型,如CSV、JSON、Avro等等。Google Cloud Storage支持?jǐn)?shù)據(jù)最終一致性。
●現(xiàn)狀小結(jié)
對象存儲服務(wù)是公有云平臺提供的一種標(biāo)準(zhǔn)存儲方案,一般都按照對象鍵 + 數(shù)據(jù) + 元數(shù)據(jù)的數(shù)據(jù)模型,但在實(shí)現(xiàn)細(xì)節(jié)上稍有不同。在數(shù)據(jù)訪問上,Azure Blob和Google Cloud Storage除了支持按鍵訪問,還支持與索引服務(wù)配合使用——建立索引后查詢對象數(shù)據(jù)內(nèi)容或元數(shù)據(jù)。大多數(shù)對象存儲方案中,對象數(shù)據(jù)作為一個整體進(jìn)行存取操作,而在Azure Blob下還支持對象內(nèi)數(shù)據(jù)讀寫操作(即Append blobs和Page blobs)。在一致性方面,大部分對象存儲支持最終一致性,而Azure Blob和阿里OSS支持強(qiáng)一致性。
●對象存儲設(shè)計(jì)
●工業(yè)場景下對象存儲的需求
工業(yè)場景下,對象數(shù)據(jù)的數(shù)據(jù)源一般是設(shè)備,這與面向終端用戶的互聯(lián)網(wǎng)應(yīng)用不一樣。例如在社交網(wǎng)絡(luò)應(yīng)用中,我們可能利用對象存儲來保存用戶上傳的照片,照片的鍵一般與用戶id綁定,對象數(shù)據(jù)讀寫從鍵的分布來看一般是隨機(jī)的,大部分操作都只訪問一個特定id對應(yīng)的數(shù)據(jù)(例如讀取某個用戶的照片時間線),跨多個id訪問的情況比較少。但在工業(yè)場景下,我們比較少單獨(dú)考慮某個特定設(shè)備的數(shù)據(jù),更多是獲取一段時間內(nèi)的滿足一定條件的所有對象數(shù)據(jù)供數(shù)據(jù)分析使用。
考慮一個具體的場景,假設(shè)我們記錄了10000臺風(fēng)機(jī)產(chǎn)生的故障文件,而我們常常希望通過分析回答:
不同型號的風(fēng)機(jī)發(fā)生故障的頻率是否相同?
同一型號不同批次生產(chǎn)的風(fēng)機(jī)發(fā)生故障的頻率?
某一型號的風(fēng)機(jī)隨著安裝時間增長其故障率的變化情況?
不同地理區(qū)域的風(fēng)機(jī)故障情況有何特征?
不同故障類型的分布情況?
通過對象鍵可以將對象數(shù)據(jù)組織成類似文件系統(tǒng)的層級結(jié)構(gòu),但仍然難以方便地篩選出用于分析的對象數(shù)據(jù)(如回答上述問題)。在對現(xiàn)狀的討論中,Azure Blob和Google Cloud Storage除了鍵以外還提供索引服務(wù),但是索引服務(wù)是獨(dú)立于對象存儲的,索引與數(shù)據(jù)之間的延遲并沒有保證。另外,一些功能在國內(nèi)并不支持,例如AWS Search。綜合多方面考慮,我們決定設(shè)計(jì)和實(shí)現(xiàn)面向工業(yè)場景的對象存儲服務(wù)。
●數(shù)據(jù)模型 = 元數(shù)據(jù) + 對象數(shù)據(jù)
首先我們考慮一種更適合檢索的對象數(shù)據(jù)模型,可以描述成下圖
在一般對象數(shù)據(jù)模型中元數(shù)據(jù)只起到補(bǔ)充說明的作用,但在我們的對象數(shù)據(jù)模型里:
元模型=對象鍵+補(bǔ)充說明+通用信息+數(shù)據(jù)指針
這是我們設(shè)計(jì)的對象存儲與其它對象存儲最大的區(qū)別。元數(shù)據(jù)不僅包括了對象鍵,還包括用戶自定義內(nèi)容,可以根據(jù)自定義內(nèi)容篩選符合條件的對象數(shù)據(jù)。
具體來說,一個對象的元數(shù)據(jù)包含多個列(column):其中一些列稱為id列(id-column),它們的組合對應(yīng)對象鍵;自定義內(nèi)容由用戶定義一些與對象數(shù)據(jù)相關(guān)的信息;通用信息列記錄了對象數(shù)據(jù)的統(tǒng)計(jì)信息,例如對象的大小和創(chuàng)建時間等。以存儲風(fēng)機(jī)產(chǎn)生的故障文件為例,它的元數(shù)據(jù)可能如下所示:
其中:
對象鍵 = 風(fēng)場id + 風(fēng)機(jī)id
自定義內(nèi)容 = 記錄時間 + 型號 + 經(jīng)緯度 + 錯誤碼
通用信息 = 大小 + 創(chuàng)建時間
數(shù)據(jù)指針用于記錄對象數(shù)據(jù)的存儲位置,如果對象數(shù)據(jù)存儲在某種文件系統(tǒng),例如HDFS,那么指針的形式可以是hdfs://nameservice1:port/path/to/data,所以理論上對象數(shù)據(jù)可以存儲在任意系統(tǒng)之中。
上面的表格看起來像一般的關(guān)系數(shù)據(jù),列的數(shù)據(jù)類型可以是字符串、整型、浮點(diǎn)數(shù)或日期。一個與關(guān)系數(shù)據(jù)不太一樣的地方是對象元數(shù)據(jù)的schema可能經(jīng)常發(fā)生變化。雖然關(guān)系模型也允許修改數(shù)據(jù)的schema,但在數(shù)據(jù)量較大的情況下變更schema的代價非常大,所以更適合存儲元數(shù)據(jù)的是一些NoSQL存儲引擎,例如Elasticsearch。
在這樣的數(shù)據(jù)模型下,一般的數(shù)據(jù)消費(fèi)方式是先根據(jù)某種條件篩選對象的元數(shù)據(jù),待得到一個列表后再根據(jù)數(shù)據(jù)指針讀取對應(yīng)的對象數(shù)據(jù)。例如在上面的例子里,我們可以根據(jù)風(fēng)場id和風(fēng)機(jī)id找到一臺風(fēng)機(jī)產(chǎn)生的所有數(shù)據(jù);或者找到某種特定故障類型的所有數(shù)據(jù)。另一種重要的使用方式是只消費(fèi)對象的元數(shù)據(jù),例如我們可以統(tǒng)計(jì)過去一個月內(nèi)發(fā)生的不同故障的histogram。這比起只能是根據(jù)已知的數(shù)據(jù)鍵來讀取對象數(shù)據(jù)的訪問方式要靈活得多。
●系統(tǒng)架構(gòu)
我們在本文給出一種基于Elasticsearch和HDFS的參考實(shí)現(xiàn),其系統(tǒng)架構(gòu)如下圖所示。用戶的讀寫請求經(jīng)過Load balancer分發(fā)到某臺具體的REST服務(wù)器;在REST服務(wù)里實(shí)現(xiàn)對象存儲的增刪查改操作邏輯;各項(xiàng)操作邏輯基于對底層各種服務(wù)的組合調(diào)用。底層的三種存儲的作用分別是:
MySQL用于存儲對象元數(shù)據(jù)的schema定義;
Elasticsearch用于存儲和檢索對象的元數(shù)據(jù);
HDFS用于存儲對象數(shù)據(jù);
在這樣的架構(gòu)下對象的元數(shù)據(jù)和對象數(shù)據(jù)都以多備份的形式存儲并支持HA(High Availability),而REST服務(wù)也支持HA,所以整個對象存儲服務(wù)支持HA,并且均可以根據(jù)負(fù)載情況水平擴(kuò)展。
用戶的一般使用場景是:
1. 數(shù)據(jù)建模:考慮對象的元數(shù)據(jù)定義,這里特指元數(shù)據(jù)中的對象鍵和自定義內(nèi)容的設(shè)計(jì),例如在前面的例子里用戶設(shè)計(jì)的對象鍵(風(fēng)場id、風(fēng)機(jī)id)和自定義內(nèi)容(記錄時間、型號、經(jīng)緯度、錯誤碼)。每一種對象數(shù)據(jù)記做一個對象類型(Object Class),對象存儲服務(wù)同時管理多個對象類型;
2. 數(shù)據(jù)寫入:用戶調(diào)用REST接口寫入對象,每一次寫入包括兩個部分,即元數(shù)據(jù)和對象數(shù)據(jù)。當(dāng)且僅當(dāng)元數(shù)據(jù)和對象數(shù)據(jù)都完成寫入之后一次寫入操作才向用戶返回成功;
3. 數(shù)據(jù)變更和刪除:用戶可以修改對象內(nèi)容,例如更新元數(shù)據(jù)中某個字段的值,或者更新對象數(shù)據(jù)。在發(fā)起更新請求之前用戶需要提供對象鍵來唯一確定被修改的對象,這也意味著對象鍵本身是不能被修改的。對象的刪除可以看做是更新的一種特殊場景;
4. 數(shù)據(jù)檢索和讀取:用戶可以按照對象元數(shù)據(jù)的列對某個對象類型進(jìn)行檢索,得到一組符合條件的對象元數(shù)據(jù)列表。由于元數(shù)據(jù)中包含指向?qū)ο髷?shù)據(jù)的指針,所以后續(xù)可以讀取對應(yīng)的對象數(shù)據(jù);
至此我們清楚了對象存儲服務(wù)的使用過程,下面深入討論一些技術(shù)要點(diǎn)。
●技術(shù)要點(diǎn)1:原子性寫操作
在上一節(jié)提出的對象存儲服務(wù)中,一個對象既包括對象數(shù)據(jù),還包括元數(shù)據(jù)。在實(shí)現(xiàn)對象的寫操作時,需要保證這兩部分一致,也就是說對象數(shù)據(jù)和元數(shù)據(jù)對用戶來說應(yīng)該是一體的,或者說寫操作是原子性的,體現(xiàn)在:
寫入或刪除一個對象的過程中,不應(yīng)該存在某個時刻使用戶只能讀到該對象的對象數(shù)據(jù)或元數(shù)據(jù);
更新一個對象的過程中,不應(yīng)該存在某個時刻對象的元數(shù)據(jù)與對象數(shù)據(jù)不一致,例如元數(shù)據(jù)是新的但對象數(shù)據(jù)是舊的,反之亦然;
為了達(dá)到上述目標(biāo),對象的寫入過程如下圖所示:
可分為兩個步驟,首先從對象存儲服務(wù)外部復(fù)制對象數(shù)據(jù)到對象存儲內(nèi)部,然后將元數(shù)據(jù)復(fù)制到對象存儲服務(wù)。表面上看,在上圖標(biāo)記為1的時刻會發(fā)生不一致,即對象存儲中只有對象數(shù)據(jù)而沒有元數(shù)據(jù),但考慮到我們讀取數(shù)據(jù)時總是先查詢元數(shù)據(jù),所以在此刻用戶是看不到這份對象數(shù)據(jù)的。無論元數(shù)據(jù)還是對象數(shù)據(jù),我們都是復(fù)制一份到對象存儲內(nèi)部,而不應(yīng)該使用轉(zhuǎn)移操作;另外,復(fù)制到對象存儲內(nèi)部的對象數(shù)據(jù)不再保留原文件名,而是改為一個隨機(jī)生成的文件名來防止沖突。
上面的描述里隱含了一個假設(shè)——元數(shù)據(jù)從外部復(fù)制到內(nèi)部的過程本身是原子性的,即不存在一個時刻使用戶能看到尚未寫完的元數(shù)據(jù)。在我們的參考設(shè)計(jì)里,對象的元數(shù)據(jù)存儲在Elasticsearch,可以保證寫入一條記錄的原子性,而其實(shí)大部分?jǐn)?shù)據(jù)庫服務(wù)都支持至少單條記錄操作的原子性。注意對象數(shù)據(jù)的復(fù)制是不需要滿足原子性的要求的,所以一般的文件系統(tǒng)都可以用于存儲對象數(shù)據(jù)。
下面我們考慮一下刪除的過程,如下圖所示:
刪除與寫入一樣分為兩個步驟,首先將待刪除對象的元數(shù)據(jù)標(biāo)記刪除,即對外部用戶不可見,但仍保存在Elasticsearch里,然后再將對象數(shù)據(jù)和元數(shù)據(jù)物理刪除。標(biāo)記刪除的目的在下一節(jié)關(guān)于沖突處理時展開。一個實(shí)現(xiàn)細(xì)節(jié)是在上圖中物理刪除對象時(標(biāo)記2),應(yīng)該先刪除對象數(shù)據(jù),后刪除元數(shù)據(jù),避免在系統(tǒng)故障時出現(xiàn)對象數(shù)據(jù)無法被清理的情況。刪除操作也是符合原子性要求的,這與寫入操作是相同的道理。
更新一個對象可以分兩種情況:
只更新對象的元數(shù)據(jù)中自定義內(nèi)容;
更新對象數(shù)據(jù),這通常意味著元數(shù)據(jù)也會發(fā)生變化,例如數(shù)據(jù)大小統(tǒng)計(jì)信息;
對于前一種情況,原子化的更新操作只依賴于Elasticsearch支持原子化更新,所以不需要額外注意。后一種情況類似對象寫入的過程,需要三步:
步驟1:把新的對象數(shù)據(jù)復(fù)制到對象存儲內(nèi)部;
步驟2:更新對象元數(shù)據(jù)內(nèi)容,并把元數(shù)據(jù)里的數(shù)據(jù)指針指向新的對象數(shù)據(jù)(這個步驟本身是原子性的);
步驟3:刪除原來的對象數(shù)據(jù)來清理空間;
與寫入的原理類似,后一種更新操作也是原子化的,但這里存在一個技術(shù)細(xì)節(jié)——如何確保我們能刪除舊的對象數(shù)據(jù)?步驟2和3之間是不能互換的,否則會存在某個時刻用戶可以查詢到對象的元數(shù)據(jù)但卻找不到對象數(shù)據(jù)。因此,如果在步驟2之后系統(tǒng)出現(xiàn)故障,我們?nèi)绾文苤琅f的對象數(shù)據(jù)在哪兒?注意這時候數(shù)據(jù)指針已經(jīng)指向了新的對象數(shù)據(jù)。為了解決這個問題,在步驟2中,我們需要把舊的數(shù)據(jù)指針保存下來(例如寫入寫前日志),即使系統(tǒng)發(fā)生故障,在服務(wù)恢復(fù)后再清理掉舊的對象數(shù)據(jù)。
本節(jié)討論了原子性寫操作實(shí)現(xiàn)的技術(shù)細(xì)節(jié),每種寫操作都分為多個步驟,所以在實(shí)現(xiàn)時需要寫前日志來保證操作的事務(wù)性。在系統(tǒng)發(fā)生故障后的服務(wù)重啟時,可以根據(jù)寫前日志來處理尚未完成的事務(wù)。
注:我們的參考實(shí)現(xiàn)基于Elasticsearch,在Elasticsearch里可以支持在更新一條記錄時執(zhí)行一個腳本,而整個執(zhí)行過程也是原子性的。基于這個特性,可以在對象元數(shù)據(jù)里的數(shù)據(jù)指針發(fā)生改變時將舊的指針記錄到元數(shù)據(jù)本身的一個數(shù)組里,從而免去了依賴寫前日志來記錄的麻煩。
●技術(shù)要點(diǎn)2:讀寫沖突處理
讀寫沖突發(fā)生在同時寫一個對象,或者同時發(fā)生一讀一寫的情況。我們回顧一下寫數(shù)據(jù)的過程,總是先處理對象數(shù)據(jù),然后處理元數(shù)據(jù);讀數(shù)據(jù)的過程則是先讀取元數(shù)據(jù),再獲取對象數(shù)據(jù)。所以對象的讀寫沖突只會發(fā)生在元數(shù)據(jù)相關(guān)的操作上。例如,在寫入兩個對象鍵相同的對象的過程中,真正可能發(fā)生沖突的步驟是兩邊同時向Elasticsearch寫入鍵相同的記錄(Elasticsearch里也有鍵的概念,在實(shí)現(xiàn)時我們將Elasticsearch里的鍵設(shè)定為對象鍵的字符串)。Elasticsearch內(nèi)部使用MVCC(Multi-versioned Concurrency Control)的沖突處理模式,在同時寫入兩條鍵相同的記錄時,其中一方會失敗。因此,如果在寫入兩個對象鍵相同的對象時恰好發(fā)生元數(shù)據(jù)寫入沖突,那么其中一方會發(fā)生失敗。
一讀一寫的情況會稍微復(fù)雜一些,不能完全由Elasticsearch來解決。這里的復(fù)雜性主要來自讀對象的過程被分為兩個階段,即先獲取元數(shù)據(jù),再讀取對象數(shù)據(jù)。在現(xiàn)實(shí)中這兩個階段之間可能相隔一段較長的時間,例如我們先根據(jù)某個查詢條件獲得一組對象元數(shù)據(jù)列表,然后我們在一些并行計(jì)算框架(例如Mapreduce或Spark)里消費(fèi)對象數(shù)據(jù)。假如在得到元數(shù)據(jù)之后,消費(fèi)對象數(shù)據(jù)之前,我們修改了對象數(shù)據(jù)會發(fā)生什么問題?例如,在消費(fèi)數(shù)據(jù)之前我們已經(jīng)把對象數(shù)據(jù)更新了。合理的做法是用戶根據(jù)已經(jīng)獲得的元數(shù)據(jù)仍然可以讀取更新前的對象數(shù)據(jù),而不會讀到新的對象數(shù)據(jù)(因?yàn)闀灰恢拢?。另外,也不能因?yàn)閷ο髷?shù)據(jù)已經(jīng)被更新就返回錯誤,否則對于那些一次性消費(fèi)大批對象數(shù)據(jù)的應(yīng)用場景,可能出現(xiàn)頻繁的失敗。在上一節(jié)提到了原子性寫操作,無論對于更新還是刪除,我們總是先做標(biāo)記,延遲一段時間之后再物理刪除對象數(shù)據(jù)。這段時間應(yīng)該足夠長,確保大部分更新前的讀取操作已經(jīng)完成;同時,系統(tǒng)需要維護(hù)一個進(jìn)程按照設(shè)定的時間來延遲刪除已被標(biāo)記的對象數(shù)據(jù)。
●技術(shù)要點(diǎn)3:一致性討論
從前面關(guān)于原子性操作的討論中,我們可以看到對象存儲服務(wù)的一致性其實(shí)是由其元數(shù)據(jù)存儲的一致性決定的。也就是說,如果我們能做到元數(shù)據(jù)存儲強(qiáng)一致性,那么對象服務(wù)就是強(qiáng)一致性的。我們使用Elasticsearch來存儲元數(shù)據(jù),所以這里需要討論Elasticsearch的一致性問題。Elasticsearch的一致性一直存在模糊(可以參考https://www.elastic.co/guide/en/elasticsearch/resiliency/current/index.html),隨著版本不斷升級,開發(fā)團(tuán)隊(duì)在試圖減少一些已知問題。
從一般情況來講Elasticsearch應(yīng)該屬于最終一致(Eventual consistency),但通過一些調(diào)整可以實(shí)現(xiàn)“接近”強(qiáng)一致性的行為。我們對Elasticsearch的配置調(diào)整包括:
優(yōu)先從primary節(jié)點(diǎn)讀取數(shù)據(jù);
寫入操作設(shè)定處于active的節(jié)點(diǎn)數(shù)量不低于n/2,其中n表示Elasticsearch集群的大?。?/p>
集群active節(jié)點(diǎn)數(shù)如果低于n/2則停止對外提供服務(wù);
在讀取數(shù)據(jù)前強(qiáng)制執(zhí)行refresh操作,確保讀取發(fā)生之前的寫入操作已經(jīng)對外可見;
如果希望深入理解上述配置的含義需要讀者對Elasticsearch有比較多的了解,但簡而言之,我們希望每次寫入操作都能覆蓋到集群里大部分節(jié)點(diǎn),而每次讀取則有些選擇從Leader節(jié)點(diǎn)(一般是首先被寫入的節(jié)點(diǎn))。這樣雖然無法保證強(qiáng)一致性,但確保了在大部分情況下,Elasticsearch對外的表現(xiàn)接近強(qiáng)一致性,即我們讀到的數(shù)據(jù)總是最新寫入的。
根據(jù)我們前面對架構(gòu)的討論,Elasticsearch并非唯一可選擇的對象元數(shù)據(jù)存儲。我們選擇Elasticsearch是看重其強(qiáng)大的檢索能力,但如果對一致性有非常嚴(yán)格的要求,也可以選擇其它存儲方式。
注:我們早期基于MySQL實(shí)現(xiàn)過元數(shù)據(jù)存儲,其面臨的最大問題是schema修改帶來的巨大開銷,以及在執(zhí)行一些聚合操作時耗時過大。
●技術(shù)要點(diǎn)4:文件合并
對象存儲服務(wù)往往需要儲存大量的對象數(shù)據(jù),而這些數(shù)據(jù)會以文件的形式保存在底層的文件系統(tǒng)中。如果存在大量小文件,可能造成文件系統(tǒng)效率降低。例如,在HDFS中,每個block大小通常在64M(或128M),一個block對應(yīng)一個inode,即HDFS Namenode內(nèi)存中的一條記錄。即使文件再小,在HDFS中仍然會占用一個inode,從而大量的小文件會帶給Namenode內(nèi)存壓力。如果我們能把小文件合并成大文件,可以減少對象文件對inode的占用,從而緩解內(nèi)存壓力,這就是文件合并的出發(fā)點(diǎn)。如果底層文件系統(tǒng)不是HDFS而是Linux本地文件系統(tǒng),其inode數(shù)量也是有一定上限的,也會有相應(yīng)的問題。
一種文件合并的思路是將對象按照時間順序劃分為多個區(qū)間,每個區(qū)間內(nèi)所有對象的對象數(shù)據(jù)文件合并為一個大文件。每個對象的元數(shù)據(jù)都包含其創(chuàng)建時間,這個時間是在創(chuàng)建該對象時由系統(tǒng)自動生成的。后續(xù)對該對象的更新操作不會改變對象的創(chuàng)建時間,這個性質(zhì)非常重要。假設(shè)我們設(shè)定區(qū)間的大小為1小時,那么按時間劃分的區(qū)間是:
..., (8:00,9:00],(9:00,10:00],...
以區(qū)間(8:00,9:00]為例,創(chuàng)建時間落入這個區(qū)間的所有對象的對象數(shù)據(jù)會被合并到一個大文件,而其元數(shù)據(jù)里的指針會指向大文件里的一部分,具體來說包括:
大文件的文件名;
對象數(shù)據(jù)在大文件里的offset;
對象數(shù)據(jù)的length;
根據(jù)上述三個信息,我們可以從大文件里讀取對應(yīng)的對象數(shù)據(jù)。文件合并是在對象已經(jīng)寫入對象服務(wù)之后發(fā)生的,例如上面例子里的(8:00,9:00]區(qū)間內(nèi)的數(shù)據(jù)合并一定發(fā)生在9:00之后,并且文件合并操作必須對用戶是透明的。換句話說,在文件合并的過程中,用戶應(yīng)該不會感知到底層對象數(shù)據(jù)正在被合并,而合并操作也不會影響用戶的讀寫操作。為此,合并的步驟包括:
步驟1:把待合并的小文件合并為一個大文件;
步驟2:依次更新被合并的所有對象的元數(shù)據(jù),使指針指向大文件里的一部分;
步驟3:刪除已被合并的小文件來清理空間;
對于任意一個對象來說,其操作過程類似對象的更新操作,雖然步驟2里對象元數(shù)據(jù)更新無法支持批量更新(即把更新包含在一個原子性事務(wù)中),但在任意時刻對外界用戶來說,他看到的都是最新的對象數(shù)據(jù)。如果在合并過程中發(fā)生寫操作沖突,沿用前面討論過多沖突處理方式,其中一方發(fā)生錯誤——假設(shè)合并涉及100個對象文件,而其中1個由于寫沖突失敗了,剩余99個成功,那么合并后原來的100個小文件會變成1個大文件(包含100個對象數(shù)據(jù))加1個小文件(包含由于沖突導(dǎo)致合并失敗的1個對象數(shù)據(jù))。可以看到,由于沖突造成了一定的數(shù)據(jù)冗余,但在正常使用情況下沖突的概率非常小,所以少量的數(shù)據(jù)冗余是可以容忍的。文件合并的過程如下圖所示:
文件合并還會帶來另一個數(shù)據(jù)冗余問題。如果在文件合并發(fā)生之后,其中一部分對象數(shù)據(jù)發(fā)生了更新,原本要被刪除的對象數(shù)據(jù)現(xiàn)在已經(jīng)成為大文件的一部分,而要從大文件里移出其中一部分,相當(dāng)于把其中未被更新的對象的數(shù)據(jù)重新寫一遍,同時更新對應(yīng)的數(shù)據(jù)指針??梢娪捎谖募喜?,更新已被合并的對象數(shù)據(jù)代價較大。
實(shí)際上,在工業(yè)場景下,大部分情況都是對象寫入,而發(fā)生更新的場景很少(這與互聯(lián)網(wǎng)應(yīng)用場景下的對象存儲不同),所以在更新比例極少的情況下,我們可以容忍大文件里少量數(shù)據(jù)已經(jīng)失效但仍然保留帶來的冗余開銷。
注:如果現(xiàn)實(shí)情況下更新比較頻繁,可以采取一定策略來優(yōu)化合并文件的刪除操作。例如,我們可以先統(tǒng)計(jì)某個大文件里有多少內(nèi)容已經(jīng)失效了,當(dāng)且僅當(dāng)失效比例較高的時候才真正執(zhí)行刪除操作。
●總結(jié)
本文介紹了一種面向工業(yè)大數(shù)據(jù)的對象存儲服務(wù)設(shè)計(jì)實(shí)踐。經(jīng)過場景分析,我們發(fā)現(xiàn)工業(yè)場景下對象存儲的需求與互聯(lián)網(wǎng)場景下的情況是不一樣的,尤其對于對象的檢索提出了更高的要求。為了滿足這種要求,我們在數(shù)據(jù)模型設(shè)計(jì)中強(qiáng)化了元數(shù)據(jù)的角色,改變了對象數(shù)據(jù)的消費(fèi)方式,提出了新的對象存儲服務(wù)系統(tǒng)架構(gòu),結(jié)合Elasticsearch + HDFS的參考實(shí)現(xiàn)詳細(xì)討論了其中的技術(shù)要點(diǎn),希望對從事面向工業(yè)大數(shù)據(jù)的對象存儲服務(wù)的設(shè)計(jì)和開發(fā)人員提供一定的參考。