作者|程伟,MetaAPP 大数据部门负责人
本文涉及开源组件
CubeFS | github.com/cubefs/cube... |
CubeFS 是新一代云原生存储产品,目前是云原生计算基金会(CNCF)托管的毕业开源项目, 兼容 S3、POSIX、HDFS 等多种访问协议,支持多副本与纠删码两种存储引擎,为用户提供多租户、 多 AZ 部署以及跨区域复制等多种特性,广泛应用于大数据、AI、容器平台、数据库、中间件存算分离、数据共享以及数据保护等场景。现由OPPO主导维护。
ByConity| github.com/ByConity/By... |
ByConity 是基于 ClickHouse 内核研发的开源分布式的云原生SQL数仓引擎,擅长交互式查询和即席查询,具有支持多表关联复杂查询、集群扩容无感、离线批数据和实时数据流统一汇总等特点。由字节开源与维护。
关于MetaApp
MetaApp 是国内领先的游戏开发与运营商,专注移动端信息高效分发,致力于构建面向全年龄段的虚拟世界。截至 2023 年,MetaApp 注册用户已超 2 亿,联运合作 20 万款游戏,累计分发量过 10 亿,拥有千万日活233乐园、233派对等应用
万亿级OLAP数据分析平台 Pandora 简介
随着业务的增长,精细化运营的提出,产品对数据部门提出了更高的要求,包括需要对实时数据进行查询分析,快速调整运营策略;对小部分人群做 AB 实验,验证新功能的有效性;减少数据查询时间,降低数据查询难度,让非专业人员可以自主分析、探查数据等。
为满足业务需求,早期我们基于Clickhouse实现了集事件分析、转化分析、自定义留存、用户分群打标、行为流分析、AB实验等功能于一体的 OLAP 数据分析平台。
平台功能简介
- 事件分析: 对收集到的用户事件进行灵活的筛选、分组、汇总。根据埋点数据进行筛选,做一些用户属性的分组或者是全局属性的筛选,查询完成以后进行数据展示,可按列按天展示。

- 转化分析: 分析用户使用产品的转化情况,即漏斗分析。根据开始事件和结束事件选定,查询具体时间内的转化。

- 留存: 分析用户在使用产品后再次回到产品的时间。根据起始事件、回访事件生成阶梯图。

- 用户分群: 用来对指定用户群体的数据进行过滤分析。根据条件将符合条件的用户筛选出来并进行分群,并根据分群结果查询对应指标,如停留时间、玩游戏的时间等,为业务提供辅佐。

- 行为流: 用来对单个用户的单个行为流进行分析。主要是用来查看某些用户的具体事件,如是否完整观看广告。

- AB实验: 从线上取一小部分流量进行新功能的测试

平台数据流向图

这是一个典型的 OLAP 的架构,分成两部分,一部分是离线,一部分是实时。
在离线场景中,我们使用 DataX 把 Kafka 的数据集成到 Hive 数仓,再生成 BI 报表。BI 报表使用了Pandora的 BI报表功能 来进行结果展示;
在实时场景中,一条线使用 GoSink 进行数据集成,把 GoSink 的数据集成到 ClickHouse,另外一条线使用 CnchKafka 把数据集成到 ByConity。最后通过 OLAP 查询平台获取数据进行查询。
遇到的问题
数据量与业务情况:

业务高峰时,我们的应用埋了1500+个埋点,产生的日志数据量一天将近有400+亿条,未压缩前将占用100+T的存储空间。
由于业务的潮汐性,节假日业务量爆发式增长,工作日业务量平静的没有波澜。
我们将所有的埋点存在一个Clickhouse大宽表中,将近3000+列。
随着使用量的增长,Clickhouse很快就露出了问题:
问题一:读写一体容易抢占资源,无法保证读/写稳定
业务高峰期时,数据写入将大量挤占 IO 和 CPU 资源,导致查询受到影响(查询时间变长)。数据查询也是如此;
问题二:扩/缩容麻烦,周期长
-
扩/缩容时间长:由于机器在 IDC,属于私有云,其中一个问题在于节点增加周期特别长。从增加节点需求发出到真正增加好节点需要一周到两周的时间,影响业务;
-
无法快速进行扩缩容:扩缩容以后要重新进行数据分布,否则节点压力非常大;
问题三:运维繁琐,业务高峰期无法保证 SLA
-
常常因为业务的节点故障导致数据查询缓慢,数据写入延迟(逐渐从延迟几小时到几天的程度);
-
业务高峰期时资源出现严重不足,短期内无法扩容资源,只能通过删减部分业务的数据,为优先级高的业务提供服务;
-
业务低峰期时,资源大量空闲,成本虚高。虽然我们在 IDC,但是 IDC 的机器购买也受成本控制,且不能无限制的节点扩容,另外在正常使用时也有一定的成本消耗;
-
无法和云上资源进行交互使用;
解决方案
为啥选ByConity
由于ByConity基于Clickhouse内核开发,兼容Clickhouse的SQL,所以ByConity测试的成本最低,而且历经字节的生产考验,有字节作为背书,没有后顾之忧。
ByConity的介绍:
ByConity 是基于 ClickHouse 内核研发的开源云原生数据仓库,采用存算分离的架构。两者都具有以下特点:
- 写入速度非常快,适用于大量数据的写入,写入数据量可达 50MB - 200MB/s
- 查询速度非常快,在海量数据下,查询速度可达 2-30GB/s
- 数据压缩比高,存储成本低,压缩比 可达 0.2~0.3
ByConity 拥有 ClickHouse 的优点,与 ClickHouse 保持了较好的兼容性,在读写分离、弹性扩缩容、数据强一致方面进行了增强。两者对于以下 OLAP 场景均适用:
- 数据集可能很大 - 数十亿或数万亿行
- 数据表中包含许多列
- 仅查询特定几列
- 结果必须以毫秒或秒为单位返回
在之前的分享中,ByConity 社区对二者从使用角度进行过对比,概括总结如下:

在 OLAP 平台构建过程中,我们主要关注资源隔离、在扩缩容、复杂查询, 以及对分布式事务的支持。
为啥选择CubeFS
我们选择了 ByConity 作为替换 Clickhouse的组件,但是由于ByConity 是存算分离的云原生架构,需要把数据存储到HDFS/S3中,选择一个S3存储就成了我们的主要目标。在对minio、seaweedfs、juicefs 进行调研后最终还是选了 CubeFS。CubeFS 在OPPO、京东等大厂生产中大量应用,稳定性成熟度更高。
CubeFS的简介:
CubeFS(原名 ChubaoFS)是一款新一代云原生开源存储系统,是云原生计算基金会(CNCF)的毕业项目。该开源项目由社区和OPPO等多家公司共同维护和发展,持续在云存储、高性能计算等领域优化演进。
CubeFS 支持 S3、HDFS 和 POSIX 等访问协议,广泛适用于大数据、AI/LLMs、容器平台、数据库和中间件等场景,提供存储与计算分离、数据共享和分发等能力。可以快速、按需进行扩缩容。
CubeFS 由 元数据子系统 (Metadata Subsystem) ,数据子系统 (Data Subsystem) 和 资源管理节点 (Master) 以及 对象网关(Object Subsystem) 组成,可以通过 POSIX/HDFS/S3 接口访问存储数据。

最终的组件图

解决了哪些问题
首先,ByConity 读写分离计算资源隔离可以保证读写任务比较稳定。如果读的任务不够,可以扩展相应资源,哪里不够补哪里,包括使用云上资源进行扩容。
其次,扩缩容比较简单,可以在分钟级别进行扩缩容。由于使用 CubeFS 分布式存储,计算存储分离,所以扩容以后不需要进行数据重分布,扩容后可以直接使用。
另外,ByConity的云原生部署,运维相对简单。
-
CubeFS 的组件相对成熟稳定,扩缩容,灾备方案成熟,出现问题可快速解决;
-
业务高峰期时,可以通过快速扩容资源保障 SLA;
-
业务低峰期时,可以通过缩减存储/计算资源达到降低成本的目的;
-
CubeFS 高达5GB/s-8GB/s每秒的 写-读 速度, 使ByConity的查询速度更快。
在MetaApp是如何使用CubeFS的
部署篇
1、使用docker在14台机器上进行部署:
-
三节点部署 Master、Clustermgr、MetaNode、Consul
- 配置为: 40核心、256G内存、 一块1T SATA SSD、10Gbps 网络
-
10节点部署 Access、BlobNode
- 配置为:96核心、256G内存、5块7.6TNvme、10块3.68T SATA SSD(两块聚合为一个7T的磁盘)、20Gbps网络(10Gbps两个网口聚合)
-
1节点部署 Proxy、Scheduler、ObjectNode
- 配置为:40核心、256G内存,8T HDD, 10Gbps 网络
2、参照官方提供的文档 集成 prometheus 与 Grafana
- 一个节点部署prometheus 与 Grafana
- cubefs集群监控

- Cubefs Master、Clustermgr、MetaNode、Consul 节点监控

- Cubefs Access、BlobNode 节点监控

3、使用 vector 来收集日志,clickhouse 存储查询日志。
- Clickhouse 建表语句
scss
-- 用于存放 cubefs 的日志
CREATE TABLE cubefs_log.cubefs_logs
(
`_timestamp` Int64,
`timestamps` DateTime64(3) MATERIALIZED toDateTime64(_timestamp / 1000000, 6),
`ck_date` Date MATERIALIZED toDate(timestamps),
`module_name` String,
`host` Nullable(String),
`level` Nullable(String),
`code_path` Nullable(String),
`trace_span` Nullable(String),
`message` Nullable(String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ck_date)
ORDER BY _timestamp
TTL ck_date + toIntervalDay(16)
SETTINGS index_granularity = 8192
-- 用于存放 cubefs 的审计日志
CREATE TABLE cubefs_log.cubefs_auditlog
(
`_timestamp` Int64,
`timestamps` DateTime64(3) MATERIALIZED toDateTime64(_timestamp / 1000000, 6),
`ck_date` Date MATERIALIZED toDate(timestamps),
`module_name` String,
`host` Nullable(String),
`req` Nullable(String),
`server_name` Nullable(String),
`req_type` Nullable(String),
`req_api` Nullable(String),
`req_headers` Nullable(String),
`req_body` Nullable(String),
`response_code` Nullable(String),
`response_headers` Nullable(String),
`response_body` Nullable(String),
`response_length` Nullable(Int64),
`response_time_us` Nullable(Int64)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ck_date)
ORDER BY _timestamp
TTL ck_date + toIntervalDay(16)
SETTINGS index_granularity = 8192
-
vector 收集日志 vector.toml配置
- 需要大家自行安装 vector, 根据自身情况修改下面的 vector.toml 配置中的 日志地址 和 clickhouse 的配置地址
ini
# Set global options
data_dir = "/var/lib/vector"
# Vector's API (disabled by default)
# Enable and try it out with the command
[api]
enabled = false
# address = "127.0.0.1:8686"
[sources.cubefs_cfs_logs]
type = "file"
include = ["/ssd1/cubefs/metanode/logs/metaNode/metaNode_*.log","/ssd1/cubefs/master/logs/master/master_*.log", "/ssd1/cubefs/clustermgr/logs/*.log"] # 这里配置为你自己的cubefs的日志地址
ignore_older_secs = 604800
[transforms.cubefs_cfs_log_parser]
inputs = ["cubefs_cfs_logs"]
type = "remap"
timezone = "local"
source = '''
.file_parts = split!(.file, "/")
.module_name = .file_parts[3]
. |= parse_regex!(.message, r'^(?P<log_timestamp>\d+/\d+/\d+ \d+:\d+:\d+.\d+) [(?P<level>\w+)[\s]?] (?P<code_path>\S+) (?P<trace_span>[[^:]+:[^:]+] )?(?P<message>.*)$')
.timestamp = parse_timestamp(.log_timestamp, "%Y/%m/%d %H:%M:%S.%f") ?? now()
._timestamp = to_unix_timestamp(.timestamp, unit: "microseconds")
del(.file_parts)
del(.log_timestamp)
'''
# Ingest data by tailing one or more files
[sources.cubefs_blob_logs]
type = "file"
include = ["/var/log/cubefs/*/*.log", ] ## 这里配置为你自己的cubefs的日志地址
ignore_older_secs = 604800
# Structure and parse via Vector's Remap Language
[transforms.cubefs_blob_log_parser]
inputs = ["cubefs_blob_logs"]
type = "remap"
timezone = "local"
source = '''
.file_parts = split!(.file, "/")
.module_name = .file_parts[4]
. |= parse_regex!(.message, r'^(?P<log_timestamp>\d+/\d+/\d+ \d+:\d+:\d+.\d+) [(?P<level>\w+)] (?P<code_path>\S+) (?P<trace_span>[[^:]+:[^:]+] )?(?P<message>.*)$')
.timestamp = parse_timestamp(.log_timestamp, "%Y/%m/%d %H:%M:%S.%f") ?? now()
._timestamp = to_unix_timestamp(.timestamp, unit: "microseconds")
del(.file_parts)
del(.log_timestamp)
'''
[sinks.ck_cubefs_logs]
type = "clickhouse"
inputs = ["cubefs_blob_log_parser", "cubefs_cfs_log_parser"]
compression = "gzip"
database = "cubefs_log"
endpoint = "http://172.16.13.160:8123" # 这里刚换为你自己的Clickhouse地址
format = "json_each_row"
table = "cubefs_logs"
skip_unknown_fields = true
auth.strategy = "basic"
auth.user = "default"
auth.password = "xxxxx" # 这里刚换为你自己的Clickhouse密码
batch.max_bytes = 20971520
batch.timeout_secs = 10
buffer.max_events = 10000
###### cubefs auditlog
# Ingest data by tailing one or more files
[sources.cubefs_blob_auditlog]
type = "file"
include = ["/var/log/cubefs/*/auditlog/*.log"] // 这里配置为你自己的cubefs的日志地址
ignore_older_secs = 604800
# Structure and parse via Vector's Remap Language
[transforms.cubefs_blob_auditlog_parser]
inputs = ["cubefs_blob_auditlog"]
type = "remap"
timezone = "local"
source = '''
.file_parts = split!(.file, "/")
.module_name = .file_parts[4]
split_message = split!(.message, "\t")
.req = split_message[0]
.server_name = split_message[1]
.log_timestamp = split_message[2]
.req_type = split_message[3]
.req_api = split_message[4]
.req_headers = split_message[5]
.req_body = split_message[6]
.response_code = split_message[7]
.response_headers = split_message[8]
.response_body = split_message[9]
.response_length = split_message[10]
.response_time_us = split_message[11]
.log_timestamp = to_int(.log_timestamp)*100 ?? 0
.timestamp = from_unix_timestamp!(.log_timestamp, unit: "nanoseconds")
._timestamp = to_unix_timestamp(.timestamp, unit: "microseconds")
del(.timestamp)
del(.log_timestamp)
'''
[sinks.ck_cubefs_auditlog]
type = "clickhouse"
inputs = ["cubefs_blob_auditlog_parser"]
compression = "gzip"
database = "cubefs_log"
endpoint = "http://172.16.13.160:8123" # 这里刚换为你自己的Clickhouse地址
format = "json_each_row"
table = "cubefs_auditlog"
skip_unknown_fields = true
auth.strategy = "basic"
auth.user = "default"
auth.password = "xxxx" # 这里刚换为你自己的Clickhouse密码
batch.max_bytes = 20971520
batch.timeout_secs = 10
buffer.max_events = 10000
-
我们目前直接使用SQL查询日志,如果大家有精力也可以尝试clickvisual 来做日志分析查询。
-
查询日志
-
-
查询审计日志
-
-
4、在ByConity的部署节点上部署 ObjectNode,降低LB负载均衡的压力,无需100Gbps/s的网卡,就可以通过节点带宽聚合实现80Gbps的网络效果。
遇到的问题与解决方式
CubeFS Object Node 服务部署: cubefs 通过 objectNode 实现S3接口,由于cubefs 需要大量的读带宽高达64Gbps,而我们没有这么大的LB机器,所以只能将 objectNode部署到ByConiy所在的机器上,通过直接负载访问Access,实现了8节点64Gbps带宽的访问效果


CubeFS S3场景空目录优化: CubeFS 由于使用了在创建对象文件时,会将文件前缀转换成目录,而ByConity在使用时会创建大量带有前缀的对象,删除对象后,不删除目录,导致空目录大量占据元数据的存储空间,通过在ObjectNode的删除接口中添加递归删除空目录的代码解决这个问题。(由于这个问题只存在S3场景,所以只修改ObjectNode的S3接口)
CubeFS 纠删码 策略调整: 由于我们只有10个存储节点,使用6+3的部署模式的话,只要两个节点挂掉就会导致无法创建新的卷,所以我们调整为5+3,这样挂掉两个节点后,依然可以正常读写数据。经过我们测试5+3 在纠删码编码/解码上相对是比较快的。
BlobNode节点内核调整:由于BlobNode节点使用centos7系统, 内核版本3.10.0太低,导致 导致nvem的平均IO 延迟太高(30多ms), 升级内核到 6.6.12后,平均IO延迟降低到了 4ms。
多交换机之间的带宽限制: 由于 数据存储节点与数据读写节点在不同交换机下, 我们发现下载速度卡在40Gbps, 通过排除发现交换机之间的带宽被打满了(万兆网络通畅为40gbps),我们通过把数据读节点与数据存储节点的网线插到一个交换机上, 就可以充分打满网卡. 而写场景的需求并不大,就可以部署在其他交换机上.
未来展望:
- 尝试CubeFS的分层模式,在性能和性价比之间找到一个平衡点。
- 尝试CubeFS的ACCESS通过SDK的方式与ObjectNode集成在一起的模式,降低延迟,在有限的硬件基础上提供更高的带宽。