基于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



本文做者:亦征

阅读原文

本文为云栖社区原创内容,未经容许不得转载。

相关文章
相关标签/搜索