本文旨在为您介绍Hologres中Table Group和Shard Count选定的基本原则、设置策略以及最佳实践,以助您获取更优的查询性能。

背景信息

Hologres是一款高性能的、计算存储分离的分布式实时数仓引擎,数据存储在位于底层存储系统的数据分片上(又称shard)。

在Hologres中,1个DB包含0个或多个Table Group,每个Table Group包含多个table,每个table只能属于一个Table Group。一个Table Group唯一对应一组shard,由这组shard来负责其中表的数据存储和查询,其包含的shard个数称为Shard Count,Table Group一旦建立,shard数不可调整。

合理的Table Group选择与Shard Count制定,是写入和查询效率的基石,能够从根本上改善数据的存储与计算效率;反之,如果Table Group和Shard Count制定不当,很容易出现性能不如预期的情况,且无法根本上调优到最佳性能。

适用场景

本最佳实践适用于对Hologres原理、架构有一定了解,且编程开发经验较为丰富的用户,且主要适用的场景如下:

  1. Hologres的默认Table Group已经能够适用于大多数情况,足够应对90%的场景,如果没有特殊需求,或者不明确新建Table Group的收益和风险时,不建议新建Table Group。
  2. 只有在一些特殊需求下,例如写入数据量太大,或者数据量过小而分析发现Query启动开销太大(启动开销和shard数呈正向关系)等场景才建议您新建Table Group。
  3. Table Group一个非常关键的作用就是做Local Join从而大大提升Join效率,因此,当您有表需要互相Join,且Join Key为Distribution Key时,将Join的左右表放在同一Table Group即可获得Local Join的效果,产生具体的性能提升。

基本概念

一个table的数据将会存储在固定的一组shard上,数据写入时,会按照distribution key,将数据sharding到具体的shard。从创建table开始,负责存储table的这一组shard就已经分配好了,这一组shard称为一个Table Group,直到它被删除,负责table底层存储的shard始终是最初分配的那些shard。

多个Table可以位于同一组shard,即多个table的数据存储在同一组shard上。一个Table Group必须有1张或多张表,如果Table Group无表,则Table Group会自动删除。一个表只能属于一个Table Group,且除非重建,否则无法变更表所属的Table Group。

Table Group是Hologres特有的一个存储逻辑概念(PostgreSQL无此概念)。Table Group与PostgreSQL中的TABLESPACE也是不一样的:TABLESPACE唯一标识了数据库对象的存储位置,类似一个目录的概念,而Table Group代表的是底层的逻辑shard组。

图1为Table Group与Schema的概念区别,Schema是一个标准的数据库概念,而Table Group是一个逻辑存储概念,并非数据库标准,不同Schema的表可以位于同一个Table Group,即底层使用同一组shard存储。

图 1. 作为逻辑存储概念的Table Group
861

图2为分别具有5个shard和2个shard的Table Group,两个Table Group的shard互不相交,每个shard在实例级别拥有独特的编号。

图 2. 分别具有5个shard和2个shard的Table Group
862

图3为Hologres底层的计算存储原理。Hologres为计算存储分离架构,数据本身不存储在计算Worker上,而是存储在分布式存储上,因此在每个Hologres计算Worker中都有若干的actor,每个actor唯一对应一个真实存储shard,actor一对一地负责对应shard的数据读取、写入和管理。正因为是一对一的,所以Table Group可以认为是一组actor的集合,也可以认为是一组shard的集合,没有本质区别。

图 3. Worker中的一组actor和Table Group中一组shard唯一对应
863

一个Table Group拥有的shard数量(即shard count,后同)是它的一个重要属性,在创建时就已决定,无法变更。shard数多的Table Group,其中的数据写入和查询分析处理可以得到更大的并行度,一定范围内,增大shard数可以加快数据写入和查询分析的速度,但shard数也并非越多越好,更多的shard数需要更多的节点间通信资源、计算资源以及内存资源,在资源不满足的时候,或者Query很小时可能会导致适得其反的效果。

DB创建后,Hologres不会为该DB创建默认的Table Group。只有当第一个表被创建,并且没有指定Table Group信息时,Hologres才会创建出一个默认的Table Group,名为{DBNAME}_tg_default。此后,用户建表时如不指定特别的Table Group选项,表都将被放置在默认的Table Group中。默认Table Group的shard数=计算节点的总Core数(20*Worker数) ,这是一个实践中表现较好的经验值,对于不同数据量的表也普通适合。

除Shard数量外,Table Group本身的数量,也不是越多越好。每个Shard无论是否正在使用,都会占据一定的内存空间,如果Table Group越多,则总Shard数越多,内存空间占用越大。另外,多个表之间有一些特殊的关系(例如需要LOCAL JOIN)时,这些表必须要处在同一Table Group下才行。

建表时如何选取Table Group

那么,如何决定是否需要新建Table Group?如何选择表放置的最合适的Table Group?又如何为新建的Table Group选择合适的Shard数?可以从以下3个步骤来分析:

图 4. 三步分析决定Table Group和Shard Count
867
  1. 如何判断是否应该新建Table Group?

    我们建表的时候,最常面临的问题是:将表建在一个新的Table Group中去,还是放到已有Table Group中?

    答案是:除非在DB下已有的所有Table Group都不能满足要求的时候,在资源允许的情况下新建Table Group。

    新建Table Group需要考虑的因素包括:

    • 负载分离:已有的Table Group容纳的表数量都很多,负载很高,而即将建的新表需要较高的查询和写入吞吐,这时将新表建立在新的Table Group可以实现写入和查询一定程度上的独立,不受其他表写入查询影响。
    • 表的相关性:创建一系列具有独特写入查询模式的表,且这一系列表之间具有(或未来具有)LOCAL JOIN需求(local join需要左右表同在一个Table Group才能实现),并和其他Table Group的表具有很浅的联系或根本没有联系。这种情况下可以独立出一个Table Group。也就是说,如果您有一组表之间相关性很强的表,而这组表与其他表相关性很低、联合查询的概率很低,可以考虑新建一个Table Group。
    • 数据量:已有Table Group的Shard Count不适合当前表的预估数据量,小表不适合放在大Table Group中,大表也不适合放在小Table Group中。
    • 实例资源扩缩容:实例进行过缩容,缩容前的Shard Count过大,或者实例进行过扩容,原来定的Shard Count过小而不再满足需求,这两者出现则都可以考虑创建新的Table Group。
      说明 要注意的是,在如下的情况尤其不建议新建Table Group:

      LOCAL JOIN:需要重点与已有Table Group中的表进行local join,以高效完成多表Join操作。有时表可能需要与多个表进行Join,如果Join操作很多,建议将他们放到同一个Table Group。如果表无法实现在同一个Table Group,那应该放到能够获得Join收益最大的Table Group中。

  2. 如何选择表放置的最合适的Table Group?

    当我们决定不新建Table Group时,如何将新表放置在合适的Table Group中实现性能最优呢?根据之前的探讨,需要从以下几个方面考虑:

    • 负载:在有多个Shard数相似的Table Group都可以放置的时候,建议选择负载更低的Table Group。这里说的“负载”指Table Group已有表的数量,通过如下命令可以查看(table_num的值即代表已有表数量)。
      select * from hologres.hg_table_group_properties ;
      
       tablegroup_name | property_key  | property_value
      -----------------+---------------+----------------
       test_tg_default | is_default_tg | 1
       test_tg_default | shard_count   | 40
       test_tg_default | tg_version    | 1
       test_tg_default | table_num     | 1
      (4 rows)
    • 数据量:大表适合放在shard数更多的Table Group中,在资源条件允许的情况下,查询时可以拥有更大的并发,写入时拥有更大的RPS。小表适合放在shard数更小的Table Group中,因为总latency很低,并发查询会带来一定的启动开销,集中存储可以减小此启动开销在总的latency中的占比,从而做到小Query更快。
    • LOCAL JOIN:放到需要频繁Join的表所在的Table Group中。
  3. 如何选择合适的Shard数?

    若决定新建Table Group,那么创建多少Shard是合适的?类似地,应该考虑如下因素:

    • 数据量:首先应该考虑的是数据量的因素,也就是大表放更多的Shard,中小表放更少的Shard。
    • 实例资源:实例资源中有多少Worker节点用来做Hologres内表的分析?假设您的内表计算资源(Worker总core数)为40个core,那么平常大小数据量的表,建在shard数为40的Table Group是一个比较好的经验值。如果数据量偏大,shard数可以增为2倍=80个。
      1. 理论最大总静止shard数:这里静止shard指的是存在于实例中且都未处于计算处理状态的shard。由于处理shard数据的每个actor都会预留32MB的内存,以保证shard都能够加载起来。而一个worker的总内存是worker core数*4GB per core,总内存的30%用来供slot所用。因此,理论最大静止shard数就是能够按照最小预留内存分配出来的shard数。因此我们得出实例总静止shard数上限的经验公式,#shard=Worker个数*#cores/worker*4*30%*1024MB/32MB=Worker个数*#cores/worker*38.4~=Worker个数*768865
      2. 合适的活跃shard数:活跃shard数指的是存在于实例中,并且正在同时进行计算/写入的shard数量。要注意的是,每个shard都需要计算资源(core)来处理数据,保险的情况下1个shard至少需要1个core来负责计算,因此当实例(而不是DB)中,活跃的shard超出了Worker节点的总core数,那么将会有计算资源争抢的局面,有时会降低性能。这也是推荐的shard个数为Worker总core数的原因,同时这也是默认Table Group的shard数,这说明我们默认的Table Group已经是比较普适的shard数量,除非有特殊原因,否则无需切换Table Group。

        在不同的资源情况下,默认的Worker个数及cores per worker的对照关系如下表所示:

        资源 Worker个数
        32 core 1
        64 core 1
        128 core 2
        256 core 5
        512 core 11
        > 512 core 请咨询技术支持
    • 写入性能需求:Shard数和数据写入性能呈一定的正相关性,单个shard的写入能力是相对固定的,shard越多,写入的并发越多,写入的吞吐越高。因此,如果表有较高RPS的写入需求,需要增大shard数。在计算资源足够的情况下,Hologres单shard写入RPS为5000-10000,因此,举例而言,如果您要求写入RPS 60W/s,则应该选择的shard数约在60-120之间,可适当上下调整。
    • JOIN需求:如果Local Join查询太慢,在多方调优后发现性能还是不够,那么可以检查下shard数是否合理,制定匹配数据量规模的shard数。
    • Table Group负载:在建立一个新的Table Group之初,就应该考虑到当前需要承载的表的数量,以及未来潜在地将属于此Table Group的表的数量,预见性地做一些设计。例如,如果将来放在此Table Group的表很多,并且多数表都需要经常访问,那么shard数很小显然存在并发不够的风险。

操作指南

下面简单介绍一下在Hologres中Table Group相关的操作和元信息表:

  1. 新建表tbl1,并新建shard数为60的Table Group来放置
    BEGIN;
    CREATE TABLE tb1 (
      a int not null,
      b text,
      c bigint
      ...
    );
    call set_table_property('tbl', 'shard_count', '60'); 
    call set_table_property(...
    ...
    COMMIT;
    说明 执行call set_table_property('tbl', 'shard_count', '60');完成后,一定会新创建一个Table Group,而不是将表放进已有shard_count为60的Table Group中,因此请不要过多或者误执行此句,导致新建过多无必要的Table Group。
  2. 将表放在已有的Table Group中
    使用colocate_with属性,将表和已有的该Table Group某个表放置在同一Table Group
    BEGIN;
    CREATE TABLE tb2 (
      a int not null,
      b text,
      c bigint
      ...
    );
    call set_table_property('tb2', 'colocate_with', 'tb1'); 
    call set_table_property(...
    ...
    COMMIT;
    820
  3. 查看表所属Table Group
    select * from hologres.hg_table_properties where property_key = 'table_group';
    
     table_id | table_namespace | table_name | property_key |              property_value
    ----------+-----------------+------------+--------------+-------------------------------------------
            0 | public          | tb1        | table_group  | test_tg_default
            0 | public          | tb2        | table_group  | test_tg_default
            0 | public          | tb3        | table_group  | test_925c1ce6_2260_48c5_ba95_5c2705771c2d
    (3 rows)
  4. 查看Table Group信息
    select * from hologres.hg_table_group_properties ;
    
                  tablegroup_name              | property_key  | property_value
    -------------------------------------------+---------------+----------------
     test_tg_default                           | is_default_tg | 1
     test_tg_default                           | shard_count   | 40
     test_tg_default                           | tg_version    | 4
     test_tg_default                           | table_num     | 2
     test_925c1ce6_2260_48c5_ba95_5c2705771c2d | is_default_tg | 0
     test_925c1ce6_2260_48c5_ba95_5c2705771c2d | shard_count   | 60
     test_925c1ce6_2260_48c5_ba95_5c2705771c2d | tg_version    | 1
     test_925c1ce6_2260_48c5_ba95_5c2705771c2d | table_num     | 1
    (8 rows)
  5. 快速查找Table所在的Table Group信息
    select t.table_id, t.table_namespace, t.table_name, g.* from hologres.hg_table_properties t join hologres.hg_table_group_properties g on (t.property_value = g.tablegroup_name) where t.table_name = 'tb1';
    
    
     table_id | table_namespace | table_name | tablegroup_name | property_key  | property_value
    ----------+-----------------+------------+-----------------+---------------+----------------
            0 | public          | tb1        | test_tg_default | is_default_tg | 1
            0 | public          | tb1        | test_tg_default | shard_count   | 40
            0 | public          | tb1        | test_tg_default | tg_version    | 4
            0 | public          | tb1        | test_tg_default | table_num     | 2
    (4 rows)
  6. 更改默认Table Group的shard数
    如果发现默认table group的shard数不再满足要求,可以使用以下命令更改默认table group的shard数。
    call hg_update_database_property('shard_count', '60'); # 更改默认Table Group的shard数为60

    执行成功后将会新建一个shard数为60的Table Group,原来的default table group不再作为默认Table Group,但属性、shard组和上面的表都保持不变,此后不指定Table Group的建表,都会将表建在新的Table Group下。

  7. 删除Table Group

    Hologres中,无需手动删除Table Group,Table Group有引用计数,当删除Table Group中的最后一张表时,该Table Group会自动删除。

推荐实践

总结来说,Table Group是一种比较高级的用法,更适用于对Hologres有深度使用且开发经验丰富的用户,对于一般用户而言无须考虑。

您可以根据以下Table Group实践的基本原则,来决定是否需要新建以及如何放置Table Group。同时推荐一种整理colocate_with的实践方法,以简化高级用法中的杂乱colocate_with问题。

  1. 基本原则

    推荐的Table Group实践原则,总结如下,您可以根据业务情况选择是否新建Table Group:

    • 没特殊情况建议不要新建Table Group。
    • 数据量过大,可新建独立的较大shard数的Table Group。
    • 有大量数据量很小的表,可适当独立出一个小shard数的Table Group,减小query启动开销。
    • 需要Join的表,尽量放在同一个Table Group
  2. 更整洁的colocate_with

    如果项目中有需求需要定义多个Table Group,而长此以往,colocate_with关系可能会较多较复杂,在这种场景中,建议采取Table Group哨兵表的实践,会更加清晰。

    BEGIN;
    CREATE TABLE tg100 (a int);                             # 哨兵表
    call set_table_property('tg100', 'shard_count', '100'); # 新建shard数=100的table group
    COMMIT;
    
    BEGIN;
    CREATE TABLE tbx (
      a int not null,
      b text,
      c bigint
      ...
    );
    call set_table_property('tbx', 'colocate_with', 'tg100'); # 等价于将表放在shard数=100的table group
    COMMIT;
    图 5. 哨兵表使colocate_with关系更加整洁
    866

总结

本文介绍了在建表时应该如何选择合适的Table Group放置,以及新建Table Group应该如何考虑shard数规格等问题。但这些问题仍然强依赖于自身业务的数据规模、访问特性、资源规格和使用重心等。例如,如果您的Hologres实例只用于对新建外表加速查询MaxCompute场景,那么针对内表的Table Group等概念无法适用。

Hologres拥有的灵活指定Shard和Table Group的特性,相比一些同类产品,具备更加灵活、易用而方便的根据具体场景定制化Schema的能力,更加灵活应对业务需求,加上用户对自身业务的理解,能够比较高效地、充分地利用好Hologres高性能。