Clickhouse运维增强: ReHash实现

前言

转到Clickhouse已经3个月, 在了解CK原理和公司内部使用后发现:

  • 用户在功能端的需求不是很强烈, 除了实时去重String类型求UV之外, 开源社区的功能基本满足了用户的所有的述求
  • 运维方面问题不断, 除了计算隔离等Server类型常见的运维问题, 还有一些DDL一致性问题都通过治理手段处理了, 但是对于集群扩容目前却没啥特别好的方案

这篇博客将来讨论一下CK扩容的难点以及应对方案, 由于这些方案还没有被组内采纳, 因此可以外网记录一下思考的成果.

扩容问题的讨论, 将分为3篇文章:

  1. 第一篇文章, 将探讨一下reHash问题, 这是扩容最大的难点, 在该文章将提供一个简单的处理方案
  2. 第二篇文章, 将探讨一下虚拟shard的实现, 用以解决第一个方案的缺点
  3. 第三篇文章, 将探讨一下存储计算分离的架构, 实现简便的容错式的扩缩容方案.

数据视图

image-20210712151634570

Clickhouse目前是按照机器节点进行物理隔离的, 一个集群对应一个专属的业务部门, 而非整一个大的集群给所有业务方使用.

无论离线还是离线的数据, 都会写入底表数据, 底表是一张分布式表, 通过rand方式分区到CK的本地表, 由于底表数据是随机分区, 数据插入时, 可以直接写入本地表, 而不用写一遍分布式表, 减少了数据传输消耗.

一部分用户, 就会直接在底表数据上做数据查询, 但也有一部分需要用到Clickhouse比较复杂的引擎, 例如ReplacingMergeTree或者AggressivingMergeTree等, 用以实现聚合运算或者去重计算.

这个时候就会使用物化视图, 注意物化视图是构建在底表数据之上的, 在这里面数据实际上复制了2份, 但正是由于这个重复, 让集群的整体容错能力大大加强, 即使要物化视图有问题, 依然可以通过重建的方式解决. 推荐这么使用

但是这么做, 也有一个问题, 就是异常时数据重复, 直接写底表的, 会让相同的block写入不同的shard, 无法规避底表重复的问题.

物化视图是底层也是一张分布式表, 视图更像一个触发器, 将insert的数据写入到物化视图的数据表上. 由于物化视图的引擎多为ReplacingMergeTreeAggressivingMergeTree, 因此必须将相同Key的数据, 分布到同一个shard中, 这时就需要做数据分片.

常规做数据分片, 一般采用range分区, 例如HBase或者TiDB, 原因在于Range分区比较方面实现数据的Merge或者Spilt, 但range分区必须要求数据可排序, 对数据的组织形式有着严格的要求, Hash分区实现起来相对简单, 计算效率也高.

但Hash分区, 一旦遇到扩容, 就需要完成数据的重分区, 成本非常高.

Clickhouse采用的是实现相对简单的Hash分区, 从而导致扩容非常复杂, 具体有多复杂, 看下面的分析.

底表扩容

底表由于是rand分区, 因此几乎不会有任何的成本和风险, 只需要将新的shard配置项, 加入到Clickhouse集群的配置就行.

image-20210713155425389

具体步骤:

  1. 用4节点配置, 启动节点4, 接入集群
  2. 1~3节点更新4节点的配置
  3. 更新节点配置为4个节点, 开始4节点写入

以上步骤可以做到不停写不停读

步骤 数据读取 数据写入
步骤1 如果查询1~3, 读取3节点配置;如果查询节点4, 读取4节点;由于4节点无数据, 则无影响 3节点写入
步骤2 1~4都为4节点配置, 由于4节点无数据, 则无影响 3节点写入
步骤3 1~4都为4节点配置, 由于4节点无数据, 则无影响 4节点写入

数据均衡

节点4刚上线, 数据的查询, 大部分依然还会走到远1~3的节点, 因此需要做数据均衡的操作.

如果系统的负载降低, 可以根据数据TTL过期时间, 将老的非均衡的数据淘汰后, 新的数据将是均衡的.

如果系统负载已经很高了, 那就必须手动实现均衡操作, 可以通过Fetch Partition将数据从老节点拉去过来, 然后再detach partition将老集群的数据, 设置为失效状态.

不过这种方式会造成, 迁移过程中,数据查询重复, 需要放在低峰时期操作均衡.

物化视图扩容

上面已经提到了, 物化视图的扩容难度, 主要是因为rehash的需求, 后面就看一下具体的步骤.

image-20210713161038288

整体的方案, 就是创建一个临时视图, 灌入数据, 然后重命名, 与mysql修改主键的流程类似, 但其中要做到 尽量少的停读写尽量多的自动化以及尽可能的数据正确

  1. 原始状态有3个shard, 物化视图通过Hash键分区, 写入到shard中.
  2. 升级至4节点, 创建一个同结构的MV’, MV’数据hash到4个节点; 而此时MV依然写入到3个节点
    1. 底表扩容一样, 先4节点配置启动节点4, 然后再更新1~3的节点
    2. 新的shard, 有一个特殊的配置值, 名为status, 值为new; MV读取到这类配置项, 会自动忽略这类shard, 因此对于MV来说, 尽管已经4节点配置, 但它自己依然是3节点
    3. 创建MV’时, 指定配置项include_state_shard=true, 新MV将hash到4个节点; 另外创建视图指定数据初始化能力, 这样就能需要不停服的回追底表数据了.
  3. MV’消费底表的历史数据, 等历史消费完毕后, 开始将MV’重命名为MV(MV则删除)
    1. 停止底表写入, 这个步骤是为了防止在rename阶段, 分布式表上有数据积压, 因此必须停写清空积压数据, 这个停写时间能够控制在分钟级别, 对于业务方的影响还算可控范围
    2. MV 重命名为MV-Temp, 由于rename操作是一个元数据操作, 因此执行速度比较快
      1. 删除物化视图转化器
      2. 重命名数据表的本地表和分布式表
    3. MV’ 重命名为MV,
      1. 删除物化视图转化器
      2. 重命名数据表的本地表和分布式表
      3. 重建物化视图转化器(名字不一样了)
    4. 所有MV都切换后, 更新shard配置项, 去除status关键字
    5. 修改MV的配置项, 删除include_state_shard配置项.

方案点评

这个方案依赖两个实现:

  1. 分布式表根据不同配置项, 识别不同的shard的
  2. 物化视图不停写初始化构建

方案的优点:

  1. 能够实现扩shard
  2. 基本上可以实现自动化

方案的缺点为:

  1. 需要停读写, 虽然时间不长
  2. 需要重写一遍底表, 数据浪费严重.

读写优化

目前物化视图的写入都是由Flink写入的, 而Flink有非常好的容错能力, 可以人为的让写入失败重试保证数据的正确, 而我们要确保的某一段时间, 数据无法写入到底表.

可以实现一个alter table语法, 在内存的metadata标识某个底表无法被写入, 这样物化视图也就无法更新了.

该标识是一个内存状态, 不需要持久化. 另外也需要有取消的指令.

1
2
3
alter table xxx disable write;
-- replace table
alter table xxx enable write

写入时, 发现表的状态为disabled_write就直接返回错误状态.

或者直接用权限, 将用户的权限取消, 等rename完成后, 再开放写入; 但公司内部用了大账号的方式, 可能不太好实现.

另外一个需要注意的点, 尽量要将禁写时间控制在Flink Sink算子的重试时间内, 不然会出现APP重启, 出现不量的告警.