• ClickHouse数据一致性


      在生产环境中,数据一致性的重要性,不论如何强调都不过分。而 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/)。
     
    我们在使用 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
    Query id: 04dd344f-62ce-4434-809c-377d5e224870
    Ok.
    0 rows in set. Elapsed: 0.039 sec. 
     
    其中:
    user_id 是数据去重更新的标识;
    create_time 是版本号字段,每组数据中 create_time 最大的一行表示最新的数据;
    deleted 是自定的一个标记位,比如 0 代表未删除,1 代表删除数据。
     

    1.2写入1000万测试数据

    INSERT INTO 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)
    Query id: 40b4ea66-5856-4ebb-887f-748a1d25c666
    Ok.
    0 rows in set. Elapsed: 1.184 sec. Processed 10.49 million rows, 83.88 MB (8.86 million rows/s., 70.86 MB/s.)

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

    INSERT INTO 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)
    Query id: e0668a9b-e8dc-43a0-a1d7-4afb2a337269
    Ok.
    0 rows in set. Elapsed: 0.078 sec. Processed 500.00 thousand rows, 4.00 MB (6.42 million rows/s., 51.37 MB/s.)
     

    1.4统计总数

    SELECT COUNT()
    FROM test_a
    Query id: 64df094e-2c7b-4b69-83da-e16f04627ebc
    ┌──count()─┐
    │ 10500000 │
    └──────────┘
    1 rows in set. Elapsed: 0.007 sec. 
     
    还未触发分区合并,所以还未去重。
     

    2.手动optimize

    在写入数据后,立刻执行 OPTIMIZE 强制触发新写入分区的合并动作。
    superset-BI :) OPTIMIZE TABLE test_a FINAL;
    OPTIMIZE TABLE test_a FINAL
    Query id: 76479daa-1d23-4ac4-9938-6b1cf41c2dbd
    Ok.
    0 rows in set. Elapsed: 1.197 sec.
    superset-BI :) SELECT COUNT() FROM test_a;
    SELECT COUNT()
    FROM test_a
    Query id: b81f2d27-3af6-4fe0-9751-010cc1ba6c71
    ┌──count()─┐
    │ 10000000 │
    └──────────┘
    1 rows in set. Elapsed: 0.002 sec. 
     

    3.通过Group by 去重

    3.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
    Query id: 3c3e698a-9dd7-48c3-8957-3cacd8357b0e
      Showed first 10000.
    10000000 rows in set. Elapsed: 3.014 sec. Processed 10.00 million rows, 230.50 MB (3.32 million rows/s., 76.47 MB/s.)
     
    函数说明:
    argMax(field1,field2):按照 field2 的最大值取 field1 的值。
    当我们更新数据时,会写入一行新的数据,例如上面语句中,通过查询最大的create_time 得到修改后的 score 字段值。
     

    3.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.3插入重复数据,再次查询

    #再次插入一条数据
    INSERT INTO test_a (user_id, score, create_time) FORMAT Values
    Query id: 64630a6a-1339-4a9f-a5d8-18f6c3f964df
    Ok.
    1 rows in set. Elapsed: 0.010 sec.
     
    #再次查询
    SELECT *
    FROM test_a
    WHERE user_id = 0
    Query id: 12c40b30-c9dd-4db5-aa90-38f65f5cc213
    ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
    │       0 │ AA    │       02022-05-09 18:23:09 │
    └─────────┴───────┴─────────┴─────────────────────┘
    ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
    │       0 │ AAAA  │       02022-05-09 21:44:27 │
    └─────────┴───────┴─────────┴─────────────────────┘
    ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
    │       0 │ AA    │       02022-05-09 21:41:34 │
    └─────────┴───────┴─────────┴─────────────────────┘
    3 rows in set. Elapsed: 0.004 sec. Processed 16.39 thousand rows, 393.24 KB (4.54 million rows/s., 108.98 MB/s.)
     
    从视图查,只能看到最新一条数据
    SELECT *
    FROM view_test_a
    WHERE user_id = 0
    Query id: 35d437b9-5d09-4ab3-8c7f-30d1a3a67fd5
    ┌─user_id─┬─score─┬─deleted─┬───────────────ctime─┐
    │       0 │ AAAA  │       02022-05-09 21:44:27 │
    └─────────┴───────┴─────────┴─────────────────────┘
    1 rows in set. Elapsed: 0.018 sec. Processed 16.39 thousand rows, 393.24 KB (894.47 thousand rows/s., 21.47 MB/s.)
     

    3.4删除数据测试

    #再次插入一条标记为删除的数据
    INSERT INTO test_a (user_id, score, deleted, create_time) FORMAT Values
    Query id: 96130c41-b29e-4d3e-b9ea-058d91afe7b9
    Ok.
    1 rows in set. Elapsed: 0.003 sec. 
     
    #再次查询
    SELECT *
    FROM test_a
    WHERE user_id = 0
    Query id: e1062fc6-07c1-4ef6-9555-2e051e707c87
    ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
    │       0 │ AAAA  │       02022-05-09 21:44:27 │
    └─────────┴───────┴─────────┴─────────────────────┘
    ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
    │       0 │ AAAA  │       12022-05-09 21:47:28 │
    └─────────┴───────┴─────────┴─────────────────────┘
    ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
    │       0 │ AA    │       02022-05-09 18:23:09 │
    └─────────┴───────┴─────────┴─────────────────────┘
    ┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
    │       0 │ AA    │       02022-05-09 21:41:34 │
    └─────────┴───────┴─────────┴─────────────────────┘
    4 rows in set. Elapsed: 0.004 sec. Processed 16.39 thousand rows, 393.27 KB (3.82 million rows/s., 91.65 MB/s.)
     
    查视图刚才那条数据看不到了
    SELECT *
    FROM view_test_a
    WHERE user_id = 0
    Query id: 5026ccbd-eae1-45c2-abb2-6ac214ed48d1
    Ok.
    0 rows in set. Elapsed: 0.006 sec. Processed 16.39 thousand rows, 393.27 KB (2.87 million rows/s., 68.94 MB/s.)
     
    这行数据并没有被真正的删除,而是被过滤掉了。在一些合适的场景下,可以结合 表级别的 TTL 最终将物理数据删除。
     

    4.通过final查询

    在查询语句后增加 FINAL 修饰符,这样在查询的过程中将会执行 Merge 的特殊逻辑(例如数据去重,预聚合等)。
    但是这种方法在早期版本基本没有人使用,因为在增加 FINAL 之后,我们的查询将会变成一个单线程的执行过程,查询速度非常慢。
    在 v20.5.2.7-stable 版本中,FINAL 查询支持多线程执行,并且可以通过 max_final_threads参数控制单个查询的线程数。但是目前读取 part 部分的动作依然是串行的。
    FINAL 查询最终的性能和很多因素相关,列字段的大小、分区的数量等等都会影响到最终的查询时间,所以还要结合实际场景取舍。
    使用 hits_v1 表进行测试:
     

    4.1普通语句查询

    SELECT *
    FROM datasets.visits_v1
    WHERE StartDate = '2014-03-17'
    LIMIT 100
    SETTINGS max_threads = 2
     
    100 rows in set. Elapsed: 0.073 sec. Processed 13.18 thousand rows, 21.79 MB (181.18 thousand rows/s., 299.57 MB/s.)
     
    查看执行计划
    EXPLAIN PIPELINE
    SELECT *
    FROM datasets.visits_v1
    WHERE StartDate = '2014-03-17'
    LIMIT 100
    SETTINGS max_threads = 2
    Query id: 83200393-6f6e-4df9-9c88-8744084b7bd8
    ┌─explain─────────────────────────┐
    │ (Expression)                    │
    │ ExpressionTransform × 2         │
    │   (SettingQuotaAndLimits)       │
    │     (Limit)                     │
    │     Limit 22                 │
    │       (ReadFromMergeTree)       │
    │       MergeTreeThread × 2 01 │
    └─────────────────────────────────┘
    7 rows in set. Elapsed: 0.012 sec. 
     
    明显将由2个线程并行读取part查询
     

    4.2final查询

    SELECT *
    FROM datasets.visits_v1 final
    WHERE StartDate = '2014-03-17'
    LIMIT 100
    SETTINGS max_threads = 2;
    100 rows in set. Elapsed: 0.548 sec. Processed 152.74 thousand rows, 239.19 MB (278.91 thousand rows/s., 436.76 MB/s.)
     
    查询速度没有普通的查询快,但是相比之前已经有了一些提升,查看 FINAL 查询的执行计划:
    EXPLAIN PIPELINE
    SELECT *
    FROM datasets.visits_v1
    FINAL
    WHERE StartDate = '2014-03-17'
    LIMIT 100
    SETTINGS max_threads = 2
    Query id: ba101258-083a-417e-b77f-ac71af6fdb73
    ┌─explain──────────────────────────────────┐
    │ (Expression)                             │
    │ ExpressionTransform × 2                  │
    │   (Limit)                                │
    │   Limit 22                            │
    │     (Filter)                             │
    │     FilterTransform × 2                  │
    │       (SettingQuotaAndLimits)            │
    │         (ReadFromMergeTree)              │
    │         ExpressionTransform × 2          │
    │           CollapsingSortedTransform × 2  │
    │             Copy 12                   │
    │               AddingSelector             │
    │                 ExpressionTransform      │
    │                   MergeTreeInOrder 01 │
    └──────────────────────────────────────────┘
    14 rows in set. Elapsed: 0.017 sec. 
    从 CollapsingSortedTransform 这一步开始已经是多线程执行,但是读取 part 部分的动作还是串行。
     
  • 相关阅读:
    iOS深入学习(Block全面分析)
    iOS 多快好省的宏定义
    1.ARC和非ARC文件共存
    简单的实现UIpicker上面的取消确定按钮
    ios 简单的倒计时验证码数秒过程实现
    jquerymobile 基础教程
    得到UIView中某个非子视图在UIView中的位置
    状态栏问题
    html表格,列表
    html简单样式
  • 原文地址:https://www.cnblogs.com/EnzoDin/p/16251252.html
Copyright © 2020-2023  润新知