前言
氣象資料是一類典型的大資料,具有資料量大、時效性高、資料種類豐富等特點。氣象資料中大量的資料是時空資料,記錄了時間和空間範圍内各個點的各個實體量的觀測量或者模拟量,每天産生的資料量常在幾十TB到上百TB的規模,且在爆發性增長。如何存儲和高效的查詢這些氣象資料越來越成為一個難題。
傳統的方案常常采用關系型資料庫加檔案系統的方式實作這類氣象資料的存儲和實時查詢,這種方案在可擴充性、可維護性和性能上都有一些缺陷,随着資料規模的增大,缺點越來越明顯。最近幾年,業界開始越來越多的基于分布式NoSQL來解決這一問題,比如基于TableStore來實作氣象格點資料的存儲和查詢。TableStore是一款阿裡自研的分布式NoSQL服務,可以提供超大規模的存儲容量,支撐超大規模的并發通路和低延遲的性能,可以很好的解決氣象資料的規模和查詢性能問題。
我們之前也寫過相關的解決方案文章《
基于雲上分布式NoSQL的海量氣象資料存儲和查詢方案》,也有一些客戶基于這個方案進行了開發。出于減少客戶開發難度,提供通用的實作的想法,我們最近開發了一個TableStore-Grid的Library,基于這個Library使用者可以非常友善的實作氣象格點資料的存儲、查詢和管理。本文作為一個實戰文章,主要講解這一解決方案的設計以及使用方式。
背景
格點資料的特點
格點資料具有明顯的多元特點,以模式系統每次産生的資料為例,一般包含以下五個次元:
- 實體量,或者稱為要素:溫度、濕度、風向、風速等等。
- 預報時效:未來3小時、6小時、9小時、72小時等等。
- 高度。
- 經度。
- 緯度。
當我們固定某一要素某一預報時效,那麼高度、經度、緯度就構成一個三維網格資料,如下圖所示(圖檔來自網際網路)。每個格點代表了一個三維空間上的點,上面的數值為該點在某一預報時效(比如未來三小時)下,某一實體量(比如溫度)的預報值。

作者:王懷遠
連結:
https://zhuanlan.zhihu.com/p/64980990來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
假設一個三維格點空間包含10個不同高度的平面,每個平面為一個2880 x 570的格點,每個格點儲存一個4位元組資料,那麼這三維的資料量為2880 x 570 x 4 x 10, 大約64MB。這僅僅是某個模式系統對某個實體量某一時效下的一次預報,可見模式資料的總量是非常大的。
格點資料的查詢方式
預報員會通過頁面的形式浏覽各種模式資料(格點資料),并進行數值模式預報。這個頁面需要提供多種模式資料的查詢方式,比如:
- 查詢一個經緯度平面的格點資料:比如未來三小時全球地面溫度的格點資料,或者未來三小時浙江省地面溫度資料。
- 查詢某個格點的時間序列資料:比如阿裡雲公司所在地未來3小時、未來6小時、一直到未來72小時的溫度。
- 查詢不同實體量的資料:比如查詢某一預報時效、某一高度、某一點的全部實體量的預報資料。
- 查詢不同模式系統産生的資料:比如同時查詢歐洲中心的某一模式資料和中國氣象機構産生的對應資料等。
上面提到,一個格點資料集一般是一個五維結構,各種查詢方式實際上就是對這個五維資料進行切分,比如查詢某個平面,每個剖面,某個點序列,某個三維、四維子空間等等。而我們的方案設計要保證在各種查詢條件的查詢性能,這是資料查詢方面的主要技術難點。
基于TableStore的方案設計
标準化格點資料模型
首先,我們定義一個規整的五維網格資料為一個GridDataSet,表示一個格點資料集,按照次元順序,其五維分别為:
- variable:變量,比如各種實體量。
- time:時間次元。
- z: z軸,一般表示空間高度
- x: x軸,一般表示經度或緯度。
- y:y軸,一般表示經度或緯度。
GridDataSet = F(variable, time, z, x, y)。
一個GridDataSet除了包含五維資料,以及各個次元的長度等外,還包含一些其他資訊:
GridDataSetId:唯一标記這個GridDataSet的Id。
Attributes:自定義屬性資訊,比如該資料的産生時間、資料來源、預報類型等等。使用者可以自由定義自定義屬性,也可以給某些屬性建立索引,建立索引後就可以通過各種組合條件來查詢符合條件的資料集。
舉個例子來說,假設某種氣象預報,每次預報未來72小時的每個整點的各個高度、各個經緯度的各種實體量,則這次預報就是一個标準的五維資料,是一個單獨的資料集(GridDataSet),下一次相同的預報則是另一個資料集,這兩個資料集需要有不同的GridDataSetId。這兩個資料集比較類似,隻是起報時間不同,但是因為起報時間不在五維模型中(五維内的時間為一次預報中的未來不同時刻),是以屬于不同的資料集,起報時間可以作為資料集的自定義屬性。本方案中,也支援對自定義屬性設定條件進行檢索。
資料存儲方案
我們設計了兩張表分别存儲資料集(GridDataSet)的meta和data,meta表示這個資料集的各種中繼資料,比如GridDataSetId、各次元長度、自定義屬性等等,data表示這個資料集裡實際的網格資料。data相比meta在資料大小上要大很多。
為什麼要分為meta和data兩張表分開存儲,主要是出于這樣的考慮:
- 使用者會有根據多種條件查詢資料集的要求,比如查詢最近有哪些資料集已經完成入庫,或者查詢表中有哪些某種類型的資料集等。傳統方案中主要是通過MySQL等關系型資料庫來存儲,在本方案中我們通過單獨的meta表來存儲,并通過TableStore的多元索引功能來實作多條件的組合查詢和多種排序方式,相比傳統方案更加易用。
- 在查詢格點資料之前,一般要知道格點資料中各次元的長度等資訊,這些資訊就是存儲在meta表中的,即需要先查詢meta表,再查詢data表。因為meta資料一般都很小,是以查詢效率相比查詢data要高,多一次查詢并不會明顯增加延遲。
meta表設計
meta表的設計比較簡單,主鍵隻有一列,記錄GridDataSetId,因為GridDataSetId就可以唯一标記一個GridDataSet。各種系統屬性和自定義屬性儲存在meta表的屬性列中。
查詢meta表有兩種方式,一種是通過GridDataSetId直接查詢,另外一種是通過多元索引,可以根據多種屬性條件組合進行查詢,比如篩選某種類型的資料,按照入庫時間從新到老傳回等。
data表設計
data表的設計要解決五維資料在不同的切分模式下的查詢效率問題,不能簡單直接的對資料進行存儲。
首先,為了查詢效率最高,我們要盡量減少一次查詢需要掃描的資料量。一個資料集的資料量可能在幾GB的級别,但是一次查詢往往隻需要其中的幾MB的資料,如果無法高效的定位要查詢的資料,那麼就要掃描全部的幾GB的資料,從中篩選出符合某個範圍的資料,顯然效率是很低的。那麼怎麼才能做到高效的定位到需要的資料之中呢?
我們首先設計一種表結構設計方式,我們使用四列主鍵列,分别為:
GridDataSetId:資料集Id,唯一标記這個資料集。
Variable:變量名,即五維模型中的第一維。
Time:時間,即五維模型中的第二維。
Z:高度,即五維模型中的第三維。
這四列主鍵列标記一行TableStore中的資料,這行資料需要儲存後兩維的資料,即一個格點平面。
這種設計下,對于五維中的前三維,我們都可以通過主鍵列的值來定位,即對于前三維的每一種情況,都對應TableStore中的一行。因為前三維分别代表變量、時間和高度,一般而言不會特别的多,每個次元在幾個到幾十個的級别,我們可以通過一些并行查詢的方法來加速查詢速度。
剩下的問題就在于後兩維資料如何存儲和查詢。首先後兩維代表了一個水準的平面,一般是一個經緯度網格,這兩維的大小是比前三維要大很多的,每維在幾百到幾千的級别,随着數值預報越來越精細化,這個網格的大小還會成倍增加。這樣的一個稠密的網格資料,我們不能把每個格點都用一列來儲存,這樣列的數量會非常多,存儲效率也會非常的低。另一方面,如果我們把一個平面的格點資料存儲到一列中,在整讀整取時效率比較高,但是如果隻讀取某個點,就會讀取很多的無效資料,效率又會變得比較低。是以我們采取一種折中的方案,對平面的二維資料再次進行切分,切分成更小的平面資料塊,這樣就可以做到隻讀取部分資料塊,而不總是讀取整個平面,是以極大的提高了查詢性能。
方案實作
基于上面的存儲方案,我們實作了一個TableStore-Grid的library,提供以下接口:
/**
* 建立相關的meta、data表,資料錄入前調用。
* @throws Exception
*/
void createStore() throws Exception;
/**
* 寫入gridDataSet的meta資訊。
* @param meta
* @throws Exception
*/
void putDataSetMeta(GridDataSetMeta meta) throws Exception;
/**
* 更新meta資訊。
* @param meta
* @throws Exception
*/
void updateDataSetMeta(GridDataSetMeta meta) throws Exception;
/**
* 通過gridDataSetId擷取meta。
* @param dataSetId
* @return
* @throws Exception
*/
GridDataSetMeta getDataSetMeta(String dataSetId) throws Exception;
/**
* // 建立meta表的多元索引。
* @param indexName
* @param indexSchema
* @throws Exception
*/
void createMetaIndex(String indexName, IndexSchema indexSchema) throws Exception;
/**
* 通過多種查詢條件來查詢符合條件的資料集。
* @param indexName 多元索引名。
* @param query 查詢條件,可以通過QueryBuilder建構。
* @param queryParams 查詢相關參數,包括offset、limit、sort等。
* @return
* @throws Exception
*/
QueryGridDataSetResult queryDataSets(String indexName, Query query, QueryParams queryParams) throws Exception;
/**
* 擷取GridDataWriter用于寫入資料。
* @param meta
* @return
*/
GridDataWriter getDataWriter(GridDataSetMeta meta);
/**
* 擷取GridDataFetcher用于讀取資料。
* @param meta
* @return
*/
GridDataFetcher getDataFetcher(GridDataSetMeta meta);
/**
* 釋放資源。
*/
void close();
}
public interface GridDataWriter {
/**
* 寫入一個二維平面。
* @param variable 變量名。
* @param t 時間維的值。
* @param z 高度維的值。
* @param grid2D 平面資料。
* @throws Exception
*/
void writeGrid2D(String variable, int t, int z, Grid2D grid2D) throws Exception;
}
public interface GridDataFetcher {
/**
* 設定要查詢的變量。
* @param variables
* @return
*/
GridDataFetcher setVariablesToGet(Collection<String> variables);
/**
* 設定要讀取的各次元起始點和大小。
* @param origin 各次元起始點。
* @param shape 各次元大小。
* @return
*/
GridDataFetcher setOriginShape(int[] origin, int[] shape);
/**
* 擷取資料。
* @return
* @throws Exception
*/
GridDataSet fetch() throws Exception;
}
下面我們分别給出資料錄入、資料查詢、資料集檢索方面的示例。
資料錄入
資料錄入流程可以分為三部分:
- 寫入putDataSetMeta接口寫入資料集的meta資訊。
- 通過GridDataWriter錄入整個資料集的資料。
- 通過updateDataSetMeta接口更新資料集的meta資訊,标記資料已經錄入完成。
下面的例子中,我們讀取一個NetCDF(氣象格點資料常用的格式)檔案,然後将其中的資料通過GridDataWriter錄入到TableStore中。通過GridDataWriter每次寫入時,隻能寫入一個二維平面,是以我們需要在外層進行3層循環,分别枚舉變量維、時間維、高度維的值,然後讀取對應的二維平面的資料進行錄入。
public void importFromNcFile(GridDataSetMeta meta, String ncFileName) throws Exception {
GridDataWriter writer = tableStoreGrid.getDataWriter(meta);
NetcdfFile ncFile = NetcdfFile.open(ncFileName);
List<Variable> variables = ncFile.getVariables();
for (Variable variable : variables) {
if (meta.getVariables().contains(variable.getShortName())) {
for (int t = 0; t < meta.gettSize(); t++) {
for (int z = 0; z < meta.getzSize(); z++) {
Array array = variable.read(new int[]{t, z, 0, 0}, new int[]{1, 1, meta.getxSize(), meta.getySize()});
Grid2D grid2D = new Grid2D(array.getDataAsByteBuffer(), variable.getDataType(),
new int[] {0, 0}, new int[] {meta.getxSize(), meta.getySize()});
writer.writeGrid2D(variable.getShortName(), t, z, grid2D);
}
}
}
}
}
資料查詢
GridDataFetcher支援對五維資料進行任意次元的查詢。第一維是變量維,通過setVariablesToGet接口設定要讀取哪些變量,其餘四維通過設定起始點(origin)和讀取的大小(shape)就可以實作任意次元讀取。
public Array queryByTableStore(String dataSetId, String variable, int[] origin, int[] shape) throws Exception {
GridDataFetcher fetcher = this.tableStoreGrid.getDataFetcher(this.tableStoreGrid.getDataSetMeta(dataSetId));
fetcher.setVariablesToGet(Arrays.asList(variable));
fetcher.setOriginShape(origin, shape);
Grid4D grid4D = fetcher.fetch().getVariable(variable);
return grid4D.toArray();
}
多條件檢索資料集
本方案中,對Meta表建立多元索引後,可以支援通過各種組合條件來進行資料集檢索,查詢出符合條件的資料集,這個功能對于氣象管理系統來說非常重要。
下面舉一個例子,假設我們要查詢已經完成入庫的,建立時間為最近一天的,來源為ECMWF(歐洲中期天氣預報中心)或者NMC(全國氣象中心),精度為1KM的氣象預報,并按照建立時間從新到老排序,可以用以下代碼實作:
查詢條件: (status == DONE) and (create_time > System.currentTimeMillis - 86400000) and (source == "ECMWF" or source == "NMC") and (accuracy == "1km")
QueryGridDataSetResult result = tableStoreGrid.queryDataSets(
ExampleConfig.GRID_META_INDEX_NAME,
QueryBuilder.and()
.equal("status", "DONE")
.greaterThan("create_time", System.currentTimeMillis() - 86400000)
.equal("accuracy", "1km")
.query(QueryBuilder.or()
.equal("source", "ECMWF")
.equal("source", "NMC")
.build())
.build(),
new QueryParams(0, 10, new Sort(Arrays.<Sort.Sorter>asList(new FieldSort("create_time", SortOrder.DESC)))));
是不是非常簡單?這一部分功能利用了TableStore的多元索引,多元索引可以實作多字段組合查詢、模糊查詢、全文檢索、排序、範圍查詢、嵌套查詢、空間查詢等功能,給中繼資料管理場景提供了強大的底層能力。
相關代碼的擷取
可以在github上擷取TableStore-Grid的實作代碼和示例代碼,歡迎大家體驗、使用以及給我們提出建議。 代碼連結:
https://github.com/aliyun/tablestore-examples/tree/master/demos/TableStore-Grid