前言
酷家乐平台的埋点注册量及其产生的埋点数据量庞大,对数据处理和分析提出了新的需求。
-
埋点量和数据量概况:酷家乐埋点平台已有3000+的埋点,每天产生的埋点数据总量达到30亿条以上。
-
现有数据架构:目前,每个埋点事件都有其独立的Hive表进行存储,这些表支持准实时数据入库,数据延迟大约为20分钟。
-
数据使用模式:下游系统主要采用离线方式使用这些埋点数据,满足了离线数据仓库的使用和分析需求。
-
分析平台需求:随着大数据团队计划在2024年初构建自己的分析平台,首要功能需求是实现对单一行为流的获取
-
现有架构局限性:按照现有独立埋点表的架构,要实现单一行为流的分析,需要管理数十张表,并且依赖业务团队对整条业务线的埋点进行整合。
-
数据整合策略:为了提高分析效率和简化管理,大数据团队决定将分散的埋点数据整合到一张大表中,以便于统一管理和分析。
-
预期效果:通过整合到一张大表,预期将简化数据管理流程,提高数据处理速度,优化分析平台的性能,并为业务团队提供更灵活、更高效的数据分析能力。
数据流转流程
数据接收
用户通过请求统一SDK,将埋点数据发送到大数据埋点数据接收服务,埋点数据会转化为埋点日志,存放到对应服务器日志文件中。
日志数据都是统一格式【{date} {time} {level} [{class}] - {message}】
数据采集
酷家乐使用的埋点采集器是Filebeat,FIlebeat可以采集Windows,Linux,k8s上的日志文件,将日志文件统一发送到kafka集群的一个topic中。
数据拆分
Filebeat采集的日志文件数据包含:Info,error,warn,埋点日志等。下游要对不同等级的日志做单独处理,需要他们处于不同的topic中。
Flink拆分流任务:读取Filebeat采集写出的topic,将topic数据按照一定解析规则得到日志的level,按照level将日志分发到不同的topic中。
数据格式化
Flink埋点大表数据流:会将埋点日志信息格式化转为埋点数据对象
解析埋点日志
- 埋点日志元信息【对于非k8s服务包含埋点来源服务,服务环境,服务ip,mac信息】
- 埋点日志路径【对于k8s服务包含埋点来源服务,服务环境信息,pod信息】
- 埋点日志原文
字段校验和环境过滤
根据埋点元系统获取到的埋点字段和埋点服务信息,对埋点数据进行校验和环境级别过滤
- 判断是否存在字段缺失
- 判断是否存在多余字段
- 判断是否存在字段类型不匹配字段
环境级别过滤,每个服务会有多个环境,不是所有环境的埋点数据都要入库,元数据平台可以配置哪些环境需要入库,配置后当前环境数据正常写入。
埋点级别过滤
由于埋点数量太多以及埋点数据太大,并不是所有的埋点数据都需要写入到埋点大表中,过滤掉不需要入库的埋点,是一个可变参数,时效性在30s左右生效。
数据入paimon
读取格式化的kafka topic的写入paimon表中。Paimon表的建表:
CREATE TABLE if not exists paimon_hive_catalog.paimon_kdw_log.xxx ( `time` TIMESTAMP(3) COMMENT '埋点产生时间', user_id STRING , category STRING , track_identifier, track_name STRING , kio_version STRING , `action` STRING , tool_name STRING, tool_version STRING, app_id STRING , vrc STRING , `domain` STRING, page STRING , url STRING , ip STRING , one_code STRING , qhdi STRING , one_env STRING, is_trial_user STRING, platform STRING , browser_name STRING , browser_version STRING , os_name STRING , os_version STRING , pub_page_app_name STRING , pub_page_app_version STRING , prey_pns STRING , fields STRING , `ds` STRING ) PARTITIONED BY ( ds ) WITH ( 'sink.parallelism' = '50', 'bucket' = '50', 'bucket-key' = 'track_identifier,user_id' );
数据入Starrocks
读取paimon表数据通过Starrocks外表的方式写入Starrocks中。Starrocks外表语句:
CREATE TABLE default_catalog.default_database.starrocks_track_big_table ( `time` TIMESTAMP(3) COMMENT '', user_id STRING, category STRING, track_identifier STRING, track_name STRING, kio_version STRING, `action` STRING, tool_name STRING, tool_version STRING, app_id STRING, vrc STRING, `domain` STRING, page STRING, url STRING, ip STRING, one_code STRING, qhdi STRING, one_env STRING, is_trial_user STRING, platform STRING, browser_name STRING, browser_version STRING, os_name STRING, os_version STRING, pub_page_app_name STRING, pub_page_app_version STRING, prey_pns STRING, fields STRING, `hour` TIMESTAMP (3) ) WITH ( 'connector' = 'starrocks', 'jdbc-url' = 'xxx', 'load-url' = 'xxx', 'database-name' = 'kinetic', 'table-name' = 'track_big_table', 'username' = 'xxx', 'password' = 'xxx', 'sink.semantic' = 'at-least-once', 'sink.label-prefix' = 'track_big_table_05', 'sink.properties.timeout' = '1200', 'sink.properties.column_separator' = '\x01', 'sink.properties.row_delimiter' = '\x02' );
Starrocks建表说明
Starrocks内表建表语句
CREATE TABLE `track_big_table` ( `time` datetime NULL , `user_id` varchar(100) NULL, `category` varchar(300) NULL , `track_identifier` varchar(300) NULL , `track_name` varchar(300) NULL , `kio_version` varchar(100) NULL, `action` varchar(200) NULL , `tool_name` varchar(200) NULL, `tool_version` varchar(100) NULL , `app_id` varchar(200) NULL , `vrc` varchar(200) NULL , `domain` varchar(200) NULL, `page` varchar(500) NULL , `url` varchar(65533) NULL, `ip` varchar(200) NULL , `one_code` varchar(200) NULL , `qhdi` varchar(500) NULL , `one_env` varchar(100) NULL, `is_trial_user` varchar(10) NULL, `platform` varchar(100) NULL , `browser_name` varchar(100) NULL, `browser_version` varchar(100) NULL, `os_name` varchar(100) NULL COMMENT, `os_version` varchar(100) NULL COMMENT, `pub_page_app_name` varchar(100) NULL , `pub_page_app_version` varchar(100) NULL , `prey_pns` varchar(100) NULL COMMENT , `fields` json NULL , `hour` datetime NULL COMMENT "分区" ) ENGINE=OLAP DUPLICATE KEY(`time`, `user_id`, `category`, `track_identifier`) COMMENT "埋点大表包含除全埋点所有埋点信息(分为公共字段信息和埋点本身字段信息(json))" PARTITION BY RANGE(`hour`) (PARTITION p2024061816 VALUES [("2024-06-18 16:00:00"), ("2024-06-18 17:00:00")), PARTITION p2024070216 VALUES [("2024-07-02 16:00:00"), ("2024-07-02 17:00:00"))) DISTRIBUTED BY HASH(`user_id`) BUCKETS 50 PROPERTIES ( "replication_num" = "2", "bloom_filter_columns" = "track_identifier, user_id, category", "dynamic_partition.enable" = "true", "dynamic_partition.time_unit" = "HOUR", "dynamic_partition.time_zone" = "Asia/Shanghai", "dynamic_partition.start" = "-168", "dynamic_partition.end" = "168", "dynamic_partition.prefix" = "p", "dynamic_partition.history_partition_num" = "168", "in_memory" = "false", "enable_persistent_index" = "false", "replicated_storage" = "true", "storage_medium" = "SSD", "storage_cooldown_ttl" = "8640 hours", "compression" = "LZ4" );
1.表类型选择:明细表和主键表
明细表定义:
明细表是默认创建的表类型。如果在建表时未指定任何 key,默认创建的是明细表。
创建表时,支持定义排序键。如果查询的过滤条件包含排序键,则 StarRocks 能够快速地过滤数据,提高查询效率。明细表适用于日志数据分析等场景,支持追加新数据,不支持修改历史数据。
适用场景:
- 分析原始数据,例如原始日志、原始操作记录等。
- 查询方式灵活,不需要局限于预聚合的分析方式。
- 导入日志数据或者时序数据,主要特点是旧数据不会更新,只会追加新的数据。
主键表定义:
主键表中的主键具有唯一非空约束,用于唯一标识数据行。如果新数据的主键值与表中原数据的主键值相同,则存在唯一约束冲突,此时新数据会替代原数据。
使用明细表原因:
埋点数据表通常包含大量的事件记录,每条记录都是唯一的,且通常不需要对历史数据进行更新或修改。基于这些特点,使用 StarRocks 的明细表(Duplicate Key Table)可能更适合用埋点数据的存储和分析。以下是选择明细表的几个原因:
-
数据唯一性:埋点数据通常是一次性的事件记录,这与明细表的设计原则相符,明细表允许数据重复存在。
-
写入效率:明细表的写入操作通常更直接和高效,因为不需要考虑数据版本的合并或删除标记。埋点数据的写入通常是批量进行的,明细表可以很好地适应这种写入模式。
-
查询需求:埋点数据的查询需求通常涉及对特定事件或时间段内数据的聚合分析,明细表可以提供这种细粒度的查询能力,同时保持查询性能。
-
数据保留策略:埋点数据可能需要长期保留,用于历史分析和趋势研究。明细表可以方便地实现数据的归档和生命周期管理。
-
成本效益:由于埋点数据量可能非常庞大,使用明细表可以更有效地利用存储资源,因为不需要为数据版本控制预留额外空间。
-
易于实现:明细表的实现相对简单,不需要复杂的数据合并逻辑,这可以简化数据同步和ETL流程。
-
实时分析:虽然埋点数据不需要实时更新,但可能需要实时分析。明细表可以支持实时查询,帮助快速获得洞察。
-
数据模型兼容性:StarRocks 的明细模型(Duplicate Key)是为这种类型的数据设计的,它能够很好地处理大量写入和查询操作。
埋点数据表更适合使用 StarRocks 的明细表来存储,因为明细表的设计原则和性能特点与埋点数据的使用场景高度契合。
2.分桶键定义
分桶定义:
假设存在列同时满足高基数和经常作为查询条件,则建议您选择其为分桶键,进行哈希分桶。 如果不存在这些同时满足两个条件的列,则需要根据查询进行判断。
- 如果查询比较复杂,则建议选择高基数的列为分桶键,保证数据在各个分桶中尽量均衡,提高集群资源利用率。
- 如果查询比较简单,则建议选择经常作为查询条件的列为分桶键,提高查询效率。
并且,如果数据倾斜情况严重,您还可以使用多个列作为数据的分桶键,但是建议不超过 3 个列。
分桶键选取
查询条件一定包含user_id,可能包含track_identifier。
最初设置分桶键为:user_id和track_identifier,分桶数按照系统默认分配。
-
分桶键问题:查询条件没有同时包含表中设置的两个分桶键,这导致查询无法利用分桶优化,从而无法直接定位到特定的数据块,降低了查询效率。
-
数据量和分桶数问题:由于表中的数据量非常大,而数据库系统默认分配的分桶数量不足以有效分散查询负载,这导致查询操作非常缓慢。大部分的查询时间(99%)都被花费在了扫描(scan)操作上,而不是实际的数据检索上。
只使用userid作为分桶键,分桶数是50个。
数据未出现倾斜情况。
3.排序键选择
使用 user_id 作为分桶键,优化查询性能。当查询涉及到特定人员的数据时,数据库可以快速定位到存储该数据的特定分桶,而无需扫描整个表。
由于查询通常是基于时间段进行的,因此将 time 字段作为排序键的首选,这样可以在查询时更快地定位到特定时间段的数据。
遇到的问题
整个Starrocks集群IO util资源被打满
最初用于处理埋点大数据表的StarRocks集群配置包括3个前端节点(Frontends, FEs)和6个后端节点(Backends, BEs)。在执行埋点数据写入操作期间,我们观察到以下性能状况:
-
高I/O利用率:StarRocks集群的I/O利用率监控指标持续显示为100%,这表明磁盘I/O操作达到了最大负荷。
-
Sink算子压力:在使用Paimon作为数据源,并将数据写入StarRocks的Sink算子时,该算子处于高压力状态,这可能是由于I/O瓶颈导致的。
-
性能瓶颈:这种持续的高I/O利用率和Sink算子的高压状态可能指示存在性能瓶颈,这可能影响数据写入的效率和速度。
-
系统响应性:在这种高压环境下,StarRocks集群可能难以响应新的查询请求,导致查询延迟增加。
在处理高吞吐量的埋点数据时,我们采取了一系列措施来优化存储和I/O性能。
-
存储介质变更:原先的埋点数据存储在HDD上,由于性能需求,我们计划将数据迁移到SSD,以利用SSD的高速读写能力。
-
冷热数据分离策略:鉴于SSD的存储空间有限,我们实施了冷热数据存储策略。最近一天的热数据保留在SSD上,以支持高频访问;而最近30天的冷数据则迁移到HDD上,以降低存储成本。
-
StarRocks克隆操作:StarRocks自动将SSD上的数据克隆到HDD,但这一过程并没有显著降低磁盘I/O负载,这可能是因为克隆操作频繁执行,导致I/O使用率居高不下。
-
业务决策调整:与业务团队协商后,我们决定仅在StarRocks中存储最近7天的热数据,而将更旧的冷数据存储在Paimon表格中,以减少对SSD空间的需求。
-
SSD使用率问题:尽管采取了上述措施,SSD的使用率仍然超过了75%,导致数据副本不断迁移,且克隆进程持续执行,I/O负载没有得到有效降低。
-
硬件扩展:为了解决I/O瓶颈问题,我们将6个BE(Backend)节点扩展到7个,并将单个SSD扩展为两个,每块SSD的使用率控制在50%左右。这一扩展有效减少了克隆进程的数量,降低了I/O负载至正常水平。
-
结果:通过上述硬件和策略调整,我们成功地降低了I/O负载,优化了数据存储效率,并确保了系统的稳定运行。
副本管理文档:docs.starrocks.io/zh/docs/adm…
想要保留日志原始格式
在处理埋点数据时,我们面临一个挑战:如何在数据流转过程中保留字段的原始类型信息。
-
数据结构选择:我们考虑使用一个字段来存储埋点数据的所有字段信息,首选的数据结构是
MAP或JSON。这样的结构允许我们通过相应的函数(如 JSON 函数或 MAP 函数)动态访问和操作字段数据。 -
Flink SQL 限制:在使用 Flink SQL 进行数据流转时,我们发现它支持
MAP类型,但仅限于MAP<String, String>。这意味着所有非字符串类型的字段将被转换为字符串,导致原始类型信息丢失。
-
Flink SQL 不支持 JSON:我们确认 Flink SQL 目前不支持 JSON 类型,这使得我们无法直接使用 JSON 格式来保留字段的原始类型。
-
数据流转方案调整:鉴于上述限制,我们决定将字段字段定义为 STRING 类型。这允许数据通过 Flink SQL 流转,但牺牲了字段的动态访问和操作。
-
StarRocks 的灵活性:在 StarRocks 中,我们尝试将外表的字段定义为 STRING 类型,而内表定义为 JSON 类型。这种方法成功地允许了数据写入,并支持使用 StarRocks 的 JSON 函数进行解析查询。
-
解决方案:通过在 StarRocks 中使用 JSON 类型,我们能够保留字段的原始类型信息,并利用 StarRocks 提供的 JSON 函数进行复杂的查询操作。
-
结论:虽然 Flink SQL 在数据类型的支持上存在限制,但 StarRocks 的灵活性为我们提供了一个可行的解决方案。这种方法确保了数据流转的连续性,同时保留了字段的原始类型,为后续的数据分析和查询提供了便利。
通过这种方案,我们成功地解决了在数据流转过程中遇到的类型丢失问题,并充分利用了 StarRocks 的高级功能来实现高效的数据处理和查询。
flink sql data types:nightlies.apache.org/flink/flink…
Starrocks json数据解析函数:docs.starrocks.io/zh/docs/sql…
Starrocks map数据解析函数:docs.starrocks.io/zh/docs/sql…