基於TableStore的海量氣象格點數據解決方案實戰

前言

氣象數據是一類典型的大數據,具備數據量大、時效性高、數據種類豐富等特色。氣象數據中大量的數據是時空數據,記錄了時間和空間範圍內各個點的各個物理量的觀測量或者模擬量,天天產生的數據量常在幾十TB到上百TB的規模,且在爆發性增加。如何存儲和高效的查詢這些氣象數據愈來愈成爲一個難題。git

傳統的方案經常採用關係型數據庫加文件系統的方式實現這類氣象數據的存儲和實時查詢,這種方案在可擴展性、可維護性和性能上都有一些缺陷,隨着數據規模的增大,缺點愈來愈明顯。最近幾年,業界開始愈來愈多的基於分佈式NoSQL來解決這一問題,好比基於TableStore來實現氣象格點數據的存儲和查詢。TableStore是一款阿里自研的分佈式NoSQL服務,能夠提供超大規模的存儲容量,支撐超大規模的併發訪問和低延遲的性能,能夠很好的解決氣象數據的規模和查詢性能問題。github

咱們以前也寫過相關的解決方案文章《基於雲上分佈式NoSQL的海量氣象數據存儲和查詢方案》,也有一些客戶基於這個方案進行了開發。出於減小客戶開發難度,提供通用的實現的想法,咱們最近開發了一個TableStore-Grid的Library,基於這個Library用戶能夠很是方便的實現氣象格點數據的存儲、查詢和管理。本文做爲一個實戰文章,主要講解這一解決方案的設計以及使用方式。數據庫

背景

格點數據的特色

格點數據具備明顯的多維特色,以模式系統每次產生的數據爲例,通常包含如下五個維度:併發

  1. 物理量,或者稱爲要素:溫度、溼度、風向、風速等等。
  2. 預報時效:將來3小時、6小時、9小時、72小時等等。
  3. 高度。
  4. 經度。
  5. 緯度。

當咱們固定某一要素某一預報時效,那麼高度、經度、緯度就構成一個三維網格數據,以下圖所示(圖片來自互聯網)。每一個格點表明了一個三維空間上的點,上面的數值爲該點在某一預報時效(好比將來三小時)下,某一物理量(好比溫度)的預報值。分佈式

假設一個三維格點空間包含10個不一樣高度的平面,每一個平面爲一個2880 x 570的格點,每一個格點保存一個4字節數據,那麼這三維的數據量爲2880 x 570 x 4 x 10, 大約64MB。這僅僅是某個模式系統對某個物理量某一時效下的一次預報,可見模式數據的總量是很是大的。性能

格點數據的查詢方式

預報員會經過頁面的形式瀏覽各類模式數據(格點數據),並進行數值模式預報。這個頁面須要提供多種模式數據的查詢方式,好比:fetch

  1. 查詢一個經緯度平面的格點數據:好比將來三小時全球地面溫度的格點數據,或者將來三小時浙江省地面溫度數據。
  2. 查詢某個格點的時間序列數據:好比阿里雲公司所在地將來3小時、將來6小時、一直到將來72小時的溫度。
  3. 查詢不一樣物理量的數據:好比查詢某一預報時效、某一高度、某一點的所有物理量的預報數據。
  4. 查詢不一樣模式系統產生的數據:好比同時查詢歐洲中心的某一模式數據和中國氣象機構產生的對應數據等。

上面提到,一個格點數據集通常是一個五維結構,各類查詢方式實際上就是對這個五維數據進行切分,好比查詢某個平面,每一個剖面,某個點序列,某個三維、四維子空間等等。而咱們的方案設計要保證在各類查詢條件的查詢性能,這是數據查詢方面的主要技術難點。大數據

基於TableStore的方案設計

標準化格點數據模型

首先,咱們定義一個規整的五維網格數據爲一個GridDataSet,表示一個格點數據集,按照維度順序,其五維分別爲:ui

  1. variable:變量,好比各類物理量。
  2. time:時間維度。
  3. z: z軸,通常表示空間高度
  4. x: x軸,通常表示經度或緯度。
  5. y:y軸,通常表示經度或緯度。

GridDataSet = F(variable, time, z, x, y)。this

一個GridDataSet除了包含五維數據,以及各個維度的長度等外,還包含一些其餘信息:

GridDataSetId:惟一標記這個GridDataSet的Id。
Attributes:自定義屬性信息,好比該數據的產生時間、數據來源、預報類型等等。用戶能夠自由定義自定義屬性,也能夠給某些屬性創建索引,創建索引後就能夠經過各類組合條件來查詢符合條件的數據集。

舉個例子來講,假設某種氣象預報,每次預報將來72小時的每一個整點的各個高度、各個經緯度的各類物理量,則此次預報就是一個標準的五維數據,是一個單獨的數據集(GridDataSet),下一次相同的預報則是另外一個數據集,這兩個數據集須要有不一樣的GridDataSetId。這兩個數據集比較相似,只是起報時間不一樣,可是由於起報時間不在五維模型中(五維內的時間爲一次預報中的將來不一樣時刻),因此屬於不一樣的數據集,起報時間能夠做爲數據集的自定義屬性。本方案中,也支持對自定義屬性設置條件進行檢索。

數據存儲方案

咱們設計了兩張表分別存儲數據集(GridDataSet)的meta和data,meta表示這個數據集的各類元數據,好比GridDataSetId、各維度長度、自定義屬性等等,data表示這個數據集裏實際的網格數據。data相比meta在數據大小上要大不少。

爲何要分爲meta和data兩張表分開存儲,主要是出於這樣的考慮:

  1. 用戶會有根據多種條件查詢數據集的要求,好比查詢最近有哪些數據集已經完成入庫,或者查詢表中有哪些某種類型的數據集等。傳統方案中主要是經過MySQL等關係型數據庫來存儲,在本方案中咱們經過單獨的meta表來存儲,並經過TableStore的多元索引功能來實現多條件的組合查詢和多種排序方式,相比傳統方案更加易用。
  2. 在查詢格點數據以前,通常要知道格點數據中各維度的長度等信息,這些信息就是存儲在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,提供如下接口:

public interface GridStore {
    /**
     * 建立相關的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;
}

下面咱們分別給出數據錄入、數據查詢、數據集檢索方面的示例。

數據錄入

數據錄入流程能夠分爲三部分:

  1. 寫入putDataSetMeta接口寫入數據集的meta信息。
  2. 經過GridDataWriter錄入整個數據集的數據。
  3. 經過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



本文做者:亦徵

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索