如何高效处理亿级海量数据-Clickhouse速通(上)

一、海量数据

在工作中遇到了这样的问题,客户会产生海量的网页访问日志原始未压缩数据40Gb/天以上,这还是按照Clickhouse数据结构优化后,按照Json格式在100Gb/天以上。基于传统的关系数据库无论是MySQL还是PostgreSQL,进行存储和统计,都会反复出现问题,包括磁盘容量不够,经常性的异常崩溃等。于是急需一种更好的解决方案。

在搜索中发现了Cloudflare使用了Clickhouse,可以处理6百万条/秒的数量(注1)。于是就深入了解了一下,发现很适合当前场景。再深入分析感觉就是量身定制。其实原因也很简单,Clickhouse的创始公司,Yandex,是俄罗斯最大的搜索引擎,其处理的数据也必然都是互联网的数据,我们面对的数据本质上是相同的。

Clickhouse是列数据库,列数据库与行数据最大的不同就是数据存储的物理位置不同。列数据库是同一列存储在磁盘的同一位置上,而行数据库是同一行存储在磁盘的同一位置。这样的差别有什么特点呢?

带来的第一个特性就是压缩特性,因为同一列的数据通常是相似的,因此相比行它们具有更高的压缩比,所以列数据库可以很方便的利用压缩减少存储占用。压缩比能达到多少呢?十倍甚至百倍。

第二个特性就是高效聚合。因为同样的列存储在同一个位置,所以可以同时加载进内存,利用内存及CPU并行指令高速进行计算。而更高效的是只需要取出相关的列即可,无需取出整行,这就减少了磁盘的读取。

图片

图片

其实无需第二个特性。第一个特性的高压缩比就足够我们采纳Clickhouse了。现在我们因为存储空间不足,只能存储很少的数据,并使用了一些复杂的逻辑轮转。基于这次的需求,我们想要至少达到半年甚至1年。因为日志可以做很多数据挖掘工作,这是一个巨大的金矿。

二、Clickhouse存储性能

鉴于Clickhouse有如此高的压缩比,我们先测试一下它的存储性能。

测试存储的思路很简单:

  1. 部署;

  2. 创建表结构;

  3. 记录一周的数据

  4. 查看存储效果;

实操部分

  1. 部署

Clickhouse整体都是一股简洁风,无论是docker还是直接部署都非常简洁,让我感觉很舒爽。

docker 方式(注2)

server:docker run -d -p 8123:8123 -p 9000:9000 --name clickhouse-server yandex/clickhouse-server
client:docker run -it --rm --link clickhouse-server:clickhouse-server yandex/clickhouse-client --host clickhouse-server
a0180a992097 :) show databases

docker 启动有认证的server

docker run -d --name clickhouse-server \ -p 8123:8123 -p 9000:9000 \ -e CLICKHOUSE_USER=root \ -e CLICKHOUSE_PASSWORD=root \ yandex/clickhouse-server

直接部署到Ubuntu上(注3)
 

sudo apt-get install -y apt-transport-https ca-certificates dirmngr
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 8919F6BD2B48D754
echo "deb https://packages.clickhouse.com/debstable main" | sudo tee \ 
 /etc/apt/sources.list.d/clickhouse.list
sudo apt-get update
sudo apt-get install -y clickhouse-server clickhouse-client
sudo service
clickhouse-server start
clickhouse-client #
or "clickhouse-client --password" if you've set up a password.

默认监听的是127.0.0.1,需要修改配置文件里的监听地址(注4)

chmod u+w /etc/clickhouse-server/config.xmlvim /etc/clickhouse-server/config.xml# 打开注释 <!-- Same for hosts without support forIPv6: --> <listen_host>0.0.0.0</listen_host>systemctl restart clickhouse-server

主要关注listen_host参数如果你要修改其它参数也同理。默认用户名是default,密码在安装过程中填写。通常各类客户端使用9000端口进行HTTP通讯。

2. 创建表结构

创建表结构的时候,要考虑整个结构的压缩比要足够好。因为实际上存储是目前的瓶颈,所以考虑尽可能提高压缩比。Optimizing ClickHouse with Schemas and Codecs (注5)这个文章很好地介绍了Clickhouse的压缩特性。实践中大量借鉴了里面提到的方法。Clickhouse也支持多种压缩方法,来平衡写入性能与磁盘消耗的占比。找到了一个压缩方法的对比文章,详细可以参见:Evaluating Database Compression Methods: Update (percona.com) (注6)。

主要选择了平衡的ZSTD压缩方法,压缩比更高,CPU消耗处于中间档位。表结构如下:

CREATE TABLE IF NOT EXISTS log (
    client_ip IPv4,
    client_port UInt16,
    client_region LowCardinality(String),
    domain LowCardinality(String),
    form String CODEC(ZSTD(3)),
    method LowCardinality(String),
    package_size UInt64,
    request_headers Map(String, String) CODEC(ZSTD(3)),  -- 使用 Map 类型存储键值对
    response_headers Map(String, String) CODEC(ZSTD(3)), -- 使用 Map 类型存储键值对
    response_statuscode UInt16,
    url String CODEC(ZSTD(3)),
    time DateTime,
) ENGINE = MergeTree()
ORDER BY (time, domain, client_ip)
PARTITION BY toYYYYMMDD(time)
SETTINGS index_granularity = 16384;

解释下用到的一些技巧。
表整体上的:

  1.  分区PARTITION BY toYYYYMMDD(time):按照天分区,因为数据太大,一天的量就已经很多了。因此按照天分区,可以提高查询效率,只要筛序限定了日期,已经从分区上干掉了很多无关数据。

  2. SETTINGS index_granularity = 16384 这个是参考了Cloudflare的方案,因为日志量大集中,用较大的粒度可以更好的利用存储和提高压缩比,计算也更快。

  3. ORDER BY 选择按照时间、域名来排序。因为发现如果按照域名第一位,虽然在查询的时候会高效,但写入性能完全跟不上。因为几乎每次写入都要把整个分区拿出来排序。按照时间排序基于日志本身的时间特性,几乎不需要太多排序,因此写入速度飞快。

字段层面的:

  1. IP使用IPv4,实际上是将'127.0.0.1' 这样的转化为整数保存。好处是节省存储空间。这里没有指定CODEC(ZSTD(3)) 压缩模式,并不是没有压缩,而是会使用系统默认的LZ4。

  2. 端口号使用UInt16即可;

  3. 地区、域名等使用的是LowCardinality(String) 这是Clickhouse特有的一种字段类型,实际上LowCardinality()更像是一个函数,修饰类型的。其相当于查字典进行转换。将类似aaaaaaaa,bbbbbbb,ccccccc,aaaaaaaa,bbbbbbb 转化为1,2,3,1,2这样的结构。而写入和查询的时候根据字典再翻译。这可以极大节约存储空间,压缩比能达到200多倍。

  4. CODEC(ZSTD(3)) 是指定ZSTD压缩方法3级,级别越高,压缩的越好,但也更消耗CPU。

  5. 还有例如Delta等其它方法,可以将递增的如日期进一步压缩。

3. 记录一周的数据

接下来记录一周数据,采用的Python的客户端从kafka读入数据,写入到Clickhouse。有个小插曲就是发现go的客户端消费性能很差200条/s,使用的是segmentio/kafka-go。通过搜索发现这个库是性能最差的那个,感兴趣的可以看对比(注7)。使用Python的客户端达到7w条/s的写入,这还是没有做任何优化的。

Python的客户端核心逻辑是:kafka消息多个topic --> 进入对应的queue -->  不同的线程并行消费。以下代码可以看做伪代码,并不是完整逻辑。

class TopicWriter:
    def __init__(self, topic, table, clickhouse_client, batch_size=10000) -> None:
        self.topic = topic
        self.table = table
        self.clickhouse_client = clickhouse_client
        self.batch_size = batch_size
        self.count = 0 
        self.batch = []
        self.handle_func = self.init_data_func(self.topic)
        self.init_table(self.topic, self.table)
        self.timeout = 10
        self.running = True
        self.last_time = time.time()
        self.queue = Queue()
        self.t = threading.Thread(target=self.consume)
        self.t.daemon = True
        self.t.start()
    
    def write(self, d):
        self.queue.put(d)

    def consume(self):
        while self.running:
            try:
                d = self.queue.get(timeout=0.2)
            except KeyboardInterrupt:
                break
            except:
                continue
            d = self.handle_func(d)
            self.batch.append(d)
            self.count += 1
            
            if len(self.batch) >= self.batch_size or (time.time() - self.last_time) > self.timeout:
                self.clickhouse_client.execute('INSERT INTO %s VALUES'%self.table, self.batch)
                self.last_time = time.time()
                self.batch = []
                print(self.topic, self.count)

topics = config['kafka']['topics']
consumer = config.get_kafka_connection()
consumer.subscribe(list(topics.keys()))
topic_consumers = {}
for k,v in topics.items():
    clickhouse_client = connect_clickhouse(**config['clickhouse'])
    topic_consumers[k] = TopicWriter(k, v, clickhouse_client, batch_size = 50000)

for message in consumer:
    d = json.loads(message.value)
    topic_consumers[message.topic].write(d)

其中需要注意的是,给kafka消费者指定一个group_id,可以:

a. 让多个消费者负担同一个topic的不同分区,起到负载均衡的作用。

b. 当消费者离线的时候,其group_id会被记录消息的停滞数量,再次上线会接续上去。

另,写入clickhouse的时候,尽量使用较大的批量写入,以提高写入性能。此处使用了5W的批量大小。在实测中发现瓶颈居然在网络带宽上,峰值传输数据量大概60Mb/S,而磁盘的写入上限在260Mb/s,还没有完全发挥出clickhouse的性能。

4. 查看存储效果;

-- 查看每列的压缩比SELECTname,formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratioFROM system.columnsWHERE table = 'log'GROUP BY nameORDER BY sum(data_compressed_bytes) DESC;
name               |compressed_size|uncompressed_size|ratio |-------------------+---------------+-----------------+------+form               |1.75 GiB       |8.22 GiB         |  4.68|request_headers    |1.41 GiB       |24.19 GiB        | 17.15|response_headers   |412.04 MiB     |12.66 GiB        | 31.46|url                |351.26 MiB     |2.77 GiB         |  8.09|client_ip          |73.63 MiB      |151.42 MiB       |  2.06|client_port        |67.68 MiB      |75.71 MiB        |  1.12|package_size       |55.35 MiB      |302.83 MiB       |  5.47|response_size      |54.87 MiB      |302.83 MiB       |  5.52|client_region      |8.76 MiB       |38.00 MiB        |  4.34|method             |1.55 MiB       |37.96 MiB        | 24.43|response_statuscode|1.35 MiB       |75.71 MiB        | 55.88|local_port         |580.33 KiB     |75.71 MiB        |133.59|domain             |235.96 KiB     |37.97 MiB        |164.76|...
-- 查看整个表的压缩比SELECTformatReadableSize(sum(data_compressed_bytes)) AS compressed_size,formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratioFROM system.columnsWHERE table = 'log';
compressed_size|uncompressed_size|ratio|---------------+-----------------+-----+4.23 GiB       |49.80 GiB        |11.78|

这里展示了存储了一天的数据量。从字段看压缩比在几倍到上百倍不等,压缩比较差的是那些大块的文本/JSON,实际上结构化的数据压缩比都很高。从整体看压缩比也达到了11.78。也就是原始数据是压缩后的11倍还多。这就相当于将磁盘扩容了11倍。每天4.23Gb的存储,如果按照5Gb半年需要 5*180 = 900 也就是不到1T的存储,现代机械硬盘动不动2T、4T都能轻松存下。并且由于整块读取,Clickhouse使用机械硬盘就足够快了。当然也可以使用SSD做冷热数据分离。

做了几个简单查询、聚合,也都能在1s内返回。考虑数据量在4千万以上,相比MySQL或者PG可能根本就无法查询结束,这个速度相当惊人了。

clickhouse同样支持分布式集群,但在考察后发现,以当前数据量,单台高性能机器(104核128G内存1T+机械硬盘)已经能完全hold住,并且大概只发挥了Clickhouse不到三成的能力。因此暂时放弃维护集群,随着规模增大再考虑。

三、统计分析

Clickhouse有两个特性,聚合表引擎和物化视图。这让Clickhouse可以达到存算一体,无需再增加额外的模块进行计算,减少反复加载写入数据,进一步提高了整体效率。

接下来再测试它的统计。物化表本质是将统计数据缓存到硬盘上。物化表的另一个特性是可以在数据写入的时候进行聚合,就是可以将计算平均分摊到各个时间点上。思路如下:

  1. 基于现有的数据表;

  2. 创建物化表;

  3. 插入已有数据;

  4. 创建物化视图;

  5. 查看结果并与原始数据计算对比;

(未完待续)

注:

1. HTTP Analytics for 6M requests per second using ClickHouse (cloudflare.com)  https://blog.cloudflare.com/http-analytics-for-6m-requests-per-second-using-clickhouse/

2. Docker部署ClickHouse(超详细图文教程)_docker clickhouse-CSDN博客 https://blog.csdn.net/fy512/article/details/123482700

3. 安装 | ClickHouse Docs https://clickhouse.com/docs/zh/getting-started/install

4. ClickHouse更换默认端口和绑定端口到0.0.0.0_clickhouse 0.0.0.0-CSDN博客  https://blog.csdn.net/data2tech/article/details/116589489

5.Optimizing ClickHouse with Schemas and Codecs https://clickhouse.com/blog/optimize-clickhouse-codecs-compression-schema

6. Evaluating Database Compression Methods: Update (percona.com) https://www.percona.com/blog/evaluating-database-compression-methods-update/

7. gguridi/benchmark-kafka-go-clients: Benchmark to test different kafka go clients to compare them under the same conditions. (github.com) https://github.com/gguridi/benchmark-kafka-go-clients

  • 30
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值