美文网首页
ClickHouse——数据一致性

ClickHouse——数据一致性

作者: 小波同学 | 来源:发表于2023-03-04 00:35 被阅读0次

    前言

    在生产环境中,数据一致性的重要性,不论如何强调都不过分。而 ClickHouse 在进行数据变更时,都会产生一个临时分区,而不会更改原始数据文件,对数据文件的修改操作会要等到数据合并时才进行。所以 ClickHouse 只能保证数据的最终一致性,而不能保证强一致性。很可能数据变更后,程序通过 ClickHouse 查到之前的错误数据。因此使用 ClickHouse ,要尽量避免数据的增删改这类数据变更操作。但是实际使用时,又不可避免的要使用数据变更操作。这时就需要有一套策略来全面处理数据一致性问题。

    首先,对于分布式表,最好的办法是尽量避免使用。如果非要使用分布式表,一定要打开internal_replication。每个分片一定要配置多副本机制,使用副本机制来保证副本之间的数据一致性。

    一般来说,分布式表会带来非常多的问题。往分布式表中导入数据时,数据是异步写入到不同的分片当中的,这样数据写入过程中就不可避免的有先有后。在最后一个分片的数据写入完成之前,不可避免的就会产生数据一致性的问题。

    另外,对于分布式表,如果在数据写入时,这个分片的服务宕机了,那么插入的数据就有可能会丢失。ClickHouse 的做法是将这个数据分片转移到 broken 子目录中,并不再使用这个数据分片。也就是说,这时,ClickHouse 这一次的数据写入操作 ius 丢失了。造成的结果就是有可能就是一次 update 操作要更新 1000 条数据,但是最终却只更新了 900 条。

    然后,对于本地的数据库,也一定要注意多副本造成的数据一致性问题。ClickHouse 中,即使是提供了去重功能的 ReplacingMergeTree,它只能保证在数据合并时会去重,只能保证数据的最终一致性,而不能保证强一致性(具体可参考官网说明:https://clickhouse.com/docs/zh/engines/table-engines/mergetree-family/replacingmergetree/)。

    ClickHouse 数据一致性

    查询CK手册发现,即便对数据一致性支持最好的Mergetree,也只是保证最终一致性:


    我们在使用 ReplacingMergeTree、SummingMergeTree 这类表引擎的时候,会出现短暂数据不一致的情况。在某些对一致性非常敏感的场景,通常有以下几种解决方案。

    1准备测试表和数据

    1.1创建表

    CREATE TABLE test_a(
      user_id UInt64,
      score String,
      deleted UInt8 DEFAULT 0,
      create_time DateTime DEFAULT toDateTime(0)
    )ENGINE= ReplacingMergeTree(create_time)
    ORDER BY user_id;
    

    其中:

    • user_id 是数据去重更新的标识;
    • create_time 是版本号字段,每组数据中 create_time 最大的一行表示最新的数据;
    • deleted 是自定的一个标记位,比如 0 代表未删除,1 代表删除数据。

    1.2 写入1000万测试数据

    INSERT INTO TABLE test_a(user_id,score)
    WITH(
      SELECT ['A','B','C','D','E','F','G']
    )AS dict
    SELECT number AS user_id, dict[number%7+1] FROM numbers(10000000);
    

    1.3 修改前 50万 行数据,修改内容包括 name 字段和 create_time 版本号字段

    INSERT INTO TABLE test_a(user_id,score,create_time)
    WITH(
      SELECT ['AA','BB','CC','DD','EE','FF','GG']
    )AS dict
    SELECT number AS user_id, dict[number%7+1], now() AS create_time FROM numbers(500000);
    

    1.4 统计总数

    select count() test_a;
    
    # 10500000条
    # 还未触发分区合并,所以还未去重
    

    2、手动OPTIMIZE(不推荐)

    在写入数据后,立刻执行OPTIMIZE强制触发新写入分区的合并动作。生产环境不建议使用,使用 OPTIMIZE 会阻塞别人进行数据写入,性能开销大;

    OPTIMIZE TABLE test_a FINAL;
    
    语法:OPTIMIZE TABLE [db.]name [ON CLUSTER cluster] [PARTITION partition | PARTITION ID 'partition_id'] [FINAL] [DEDUPLICATE [BY expression]]
    

    3 通过 Group by 去重

    • 1、执行去重的查询
    SELECT
      user_id ,
      argMax(score, create_time) AS score,
      argMax(deleted, create_time) AS deleted,
      max(create_time) AS ctime
    FROM test_a
    GROUP BY user_id
    HAVING deleted = 0;
    

    argMax(field1,field2):按照 field2 的最大值取 field1 的值,当我们更新数据时,会写入一行新的数据,例如上面语句中,通过查询最大的 create_time 得到修改后的score字段值。

    • 2、创建视图,方便测试
    CREATE VIEW view_test_a AS
    SELECT
      user_id ,
      argMax(score, create_time) AS score,
      argMax(deleted, create_time) AS deleted,
      max(create_time) AS ctime
    FROM test_a
    GROUP BY user_id
    HAVING deleted = 0;
    
    • 3、插入重复数据,再次查询
    #再次插入一条数据
    INSERT INTO TABLE test_a(user_id,score,create_time) VALUES(0,'AAAA',now())
     
    #再次查询
    SELECT *
    FROM view_test_a
    WHERE user_id = 0;
    
    • 4、删除数据测试
    #再次插入一条标记为删除的数据
    INSERT INTO TABLE test_a(user_id,score,deleted,create_time) VALUES(0,'AAAA',1,now());
    
    #再次查询,刚才那条数据看不到了
    SELECT *
    FROM view_test_a
    WHERE user_id = 0;
    

    这行数据并没有被真正的删除,而是被过滤掉了。在一些合适的场景下,可以结合 表级别的 TTL 最终将物理数据删除。

    4 通过 FINAL 查询

    在查询语句后增加FINAL修饰符,这样在查询的过程中将会执行Merge的特殊逻辑(例如数据去重,预聚合等)。

    但是这种方法在早期版本基本没有人使用,因为在增加 FINAL之后,我们的查询将会变成一个单线程的执行过程,查询速度非常慢。

    在v20.5.2.7-stable版本中,FINAL查询支持多线程执行,并且可以通过max_final_threads 参数控制单个查询的线程数。但是目前读取part部分的动作依然是串行的。

    FINAL查询最终的性能和很多因素相关,列字段的大小、分区的数量等等都会影响到最终的查询时间,所以还要结合实际场景取舍。

    参考链接:https://github.com/ClickHouse/ClickHouse/pull/10463 使用hits_v1表进行测试:

    新版本测试

    • 普通语句查询
    select *
    from datasets.visits_v1
    WHERE StartDate = '2014-03-17'
    limit 100
    settings
    max_threads = 2;
    

    查看执行计划:

    explain pipeline
    select *
    from datasets.visits_v1
    WHERE StartDate = '2014-03-17'
    limit 100
    settings
    max_threads = 2; 
    
    (Expression)
    ExpressionTransform × 2
      (SettingQuotaAndLimits)
        (Limit)
        Limit 2 → 2
          (ReadFromMergeTree)
          MergeTreeThread × 2 0 → 1
    

    明显将由2个线程并行读取 part 查询。

    select *
    from datasets.visits_v1 final
    WHERE StartDate = '2014-03-17'
    limit 100
    settings
    max_final_threads = 2;
    

    查询速度没有普通的查询快,但是相比之前已经有了一些提升,查看 FINAL 查询的执行计划:

    explain pipeline select * from datasets.visits_v1 final WHERE StartDate = '2014-03-17' limit 100  settings max_final_threads = 2;
    (Expression)
    ExpressionTransform × 2
      (SettingQuotaAndLimits)
        (Limit)
        Limit 2 → 2
          (ReadFromMergeTree)
          ExpressionTransform × 2
            CollapsingSortedTransform × 2
              Copy 1 → 2
                AddingSelector
                  ExpressionTransform
                    MergeTree 0 → 1
    

    从CollapsingSortedTransform这一步开始已经是多线程执行,但是读取 part 部分的动作还是串行。

    参考:
    https://www.cnblogs.com/wdh01/p/16879841.html

    https://blog.csdn.net/qq_40378034/article/details/120473116

    https://blog.csdn.net/qq_38304392/article/details/125301713

    相关文章

      网友评论

          本文标题:ClickHouse——数据一致性

          本文链接:https://www.haomeiwen.com/subject/zpbeldtx.html