写点什么

B 站基于 Clickhouse 的下一代日志体系建设实践

作者:Java-fenn
  • 2022 年 9 月 16 日
    湖南
  • 本文字数:9953 字

    阅读完需:约 33 分钟

01 背景介绍日志作为线上定位问题排障的重要手段,在可观测领域有着不可替代的作用。稳定性、成本、易用性、可扩展性都是日志系统需要追求的关键点。


B 站基于 Elastic Stack 的日志系统(Billions) 从 2017 建设以来, 已经服务了超过 5 年,目前规模超过 500 台机器,每日写入日志量超过 700TB。


ELK 体系是业界最常用的日志技术栈,在传输上以结合规范 key 的 JSON 作为传输格式,易于多种语言实现和解析,并支持动态结构化字段。存储上 ElasticSearch 支持全文检索,能够快速从杂乱的日志信息中搜寻到关键字。展示上 Kibana 具有美观、易用等特性。


随着业务系统的高速发展,日志系统的规模也随之快速扩展,我们遇到了一系列的问题,同时可观测业界随着 OpenTelemetry 规范的成熟,推动着我们重新考量,迈入下一代日志系统。


02 遇到的问题首先必须要提的就是成本和稳定性。日志作为一种应用产生的实时数据,随着业务应用规模发展而紧跟着扩大。日志系统必须在具备高吞吐量的同时,也要具备较高的实时性要求。Elasticsearch 由于分词等特性,在写吞吐量上有着明显的瓶颈,分词耗 CPU 且难以解决热点问题。如果资源冗余不足,就容易导致稳定性下降,日志摄入发生延迟,日志的延迟会对排障产生极大负面影响。


同时由于压缩率不高的原因,ES 的存储成本也较高,对内存有着较高的要求。这些因素导致我们日志必须进行常态化的采样和限流,对用户使用上造成了困扰,限制了排障的场景。


内存使用率的问题也迫使我们必须将 warm 阶段的索引进行 Close,避免占用内存。用户如果需要查询就必须操作进行 Open,牺牲了一定的易用性。


为了稳定性和成本,动态 Mapping 也必须被关闭,有时用户引导不到位,就会导致用户发现自己搜索的日志遗留了 Mapping 配置而导致难以追溯查询。


在运维上,ES7 之前缺少生命周期的能力,我们必须维护一整套生命周期相关组件,来对索引进行预创建、关闭和删除,不可避免的带来高维护成本。


Kibana 虽然好用,但也不是没有缺点的,整体代码复杂,二次开发困难。且每次升级 ES 必须升级到对应的 Kibana 版本也增加了用户迁移的成本。还有一点就是 Kibana Query 虽然语法较为简单,但对于初次接触的研发还是有一定的学习成本的。


在采集和传输上,我们制定了一套内部的日志格式规范,使用 JSON 作为传输格式,并提供了 Java 和 Golang 的 SDK。这套传输格式本身在序列化/反序列化上性能一般,且私有协议难以避免兼容性和可维护性问。


03 新架构体系针对上述的一系列问题,我们设计了 Bilibili 日志服务 2.0 的体系,主要的进化为使用 ClickHouse 作为存储,实现了自研的日志可视化分析平台,并使用 OpenTelemetry 作为统一日志上报协议。


如图所示为日志实时上报和使用的全链路。日志从产生到消费会经过采集→摄入 →存储 →分析四个步骤,分别对应我们在链路上的各个组件,先做个简单的介绍:


OTEL Logging SDK


完整实现 OTEL Logging 日志模型规范和协议的结构化日志高性能 SDK,提供了 Golang 和 Java 两个主要语言实现。


Log-Agent


日志采集器,以 Agent 部署方式部署在物理机上,通过 Domain Socket 接收 OTEL 协议日志,同时进行低延迟文件日志采集,包括容器环境下的采集。支持多种 Format 和一定的加工能力,如解析和切分等。


Log-Ingester


负责从日志 kafka 订阅日志数据, 然后将日志数据按时间维度和元数据维度(如 AppID) 拆分,并进行多队列聚合, 分别攒批写入 ClickHouse 中.


ClickHouse


我们使用的日志存储方案,在 ClickHouse 高压缩率列式存储的基础上,配合隐式列实现了动态 Schema 以获得更强大的查询性能,在结构化日志场景如猛虎添翼。


Log-Query


日志查询模块,负责对日志查询进行路由、负载均衡、缓存和限流,以及提供查询语法简化。


BLS-Discovery


新一代日志的可视化分析平台,提供一站式的日志检索、查询和分析,追求日志场景的高易用性,让每个研发 0 学习成本无障碍使用。


下边我们将针对几个重点进行详细设计阐述。


3.1 基于 ClickHouse 的日志存储新方案的最核心的部分就是我们将日志的通用存储换成了 ClickHouse。


先说结果,我们在用户只需要付出微小迁移成本的条件下( 转过来使用 SQL 语法进行查询),达到了 10 倍的写入吞吐性能,并以原先日志系统 1/3 的成本,存储了同等规模量的日志。在查询性能上,结构化字段的查询性能提升 2 倍, 99%的查询能够在 3 秒内完成。


下图为同一份日志在 Elasticsearch, ClickHouse 和 ClickHouse(zstd)中的容量, 最终对比 ES 达到了 1:6。


ClickHouse 方案里另一个最大的提升是写入性能,ClickHouse 的写入性能达到了 ES 的 10 倍以上。


在通用结构化日志场景,用户往往是使用动态 Schema 的,所以我们引入了隐式列 Map 类型来存储动态字段, 以同时获得动态性和高查询性能,稍后将会重点介绍隐式列的实现。


我们的表设计如下,我们针对每个日志组,都建立了一张复制表。表中的字段分为公共字段(即 OTEL 规范的 Resource 字段, 以及 trace_id 和 span_id 等),以及隐式列字段,string_map,number_map,bool_map 分别对应字符串字段,数字字段和布尔字段。我们对常用的日志值类型进行分组,使用这三个字段能满足大部分查询和写入的需求。


同时,我们根据日志重要程度和用户需求定义了不同时间范围的 TTL。根据统计大部分(90%)的日志查询集中在 4 小时以内,我们为日志制定了三个阶段的日志生命周期,Hot,Warm 和 Cold.Hot 阶段,所有日志在高速存储中,保证高写入和检索性能,通常我们的资源保证 24 小时日志数据会在此阶段中;Warm 阶段,日志迁移到 Sata 盘中,同样可以进行检索,通常需要更长时间;Cold 阶段,在达到了第二个 TTL 时间后,在 ClickHouse 中删除腾出空间,如果有需求会在 HDFS 中有备份。


对于大部分的字段,我们都使用了 ZSTD(1)的压缩模式,经过测试相比默认的 Lz4 提升了 50%的压缩率,在写入和查询性能上的代价不超过 5%,适合日志写多读少的场景。


3.2 查询网关查询网关承担着查询入口的任务。我们主要在这个组件上集成了查询路由,查询负载均衡,简化查询语法,缓存,限流等功能。


这里先介绍下简化查询语法的功能。作为日志对前端以及对外的接口,我们的目标是对用户屏蔽一些底层复杂实现。如 ClickHouse 的 Local 表/分布式表,隐式列和公共字段的查询区别,以及对用户查询进行限制(强制 Limit,强制时间范围)。


上图可见我们允许用户在编写查询 SQL 时,不需要关心字段是否为隐式列,也不需要关心目标表和集群,以最简单的方式通过 Restful API 与日志查询网关进行交互获取日志数据。这样的实现还有个目的就是为以后可能再一次的存储引擎迭代做铺垫,将日志查询与底层实现解耦。


此外,在查询网关上还集成了 Luence 语法的解析器,将其自动转化为 SQL 语法,为用户的 API 迁移提供便利。


3.3 可视化分析平台在升级日志架构的过程中,很多用户反应希望能够延续之前的日志使用方式,尽量减少迁移学习的成本。我们考虑了让 Kibana 兼容 ClickHouse 作为存储引擎。然而由于我们同时维护中 Kibana5 和 7 两个版本,且越高版本的 Kibana 功能丰富,与 Elastic Stack 绑定过深,我们决定还是自研日志可视化查询分析平台。


我们的目标是:


提供接近 Kibana 使用习惯的界面,减少用户的迁移成本;


作为排障的入口,和内部其他组件打通,如监控告警,分布式追踪等。


紧记着上面两个关键目标,我们开始了自研之路。


Kibana 作为非常成熟的日志分析界面,具有非常多的细节,都是在使用过程中沉积下来的功能。任何一个功能用户都有不低的使用频率。如图所示我们能实现了查询语句高亮和提示、字段分布分析、日志时间分布预览、查询高亮以及日志略缩展示等等。


用户在查询日志时, 使用标准 SQL 的 Where condition 部分, 如 log.level = 'ERROR' 进行日志过滤, 并对用户将隐式列和具体的表透明化(通过查询网关的能力)。


我们还使用 code-mirror2 实现了查询的自动补全和查询提示。提示的内容包括常用历史查询, 字段, 关键字和函数, 进一步降低用户的使用门槛。


在此基础之上,我们开发了能够领先于 Kibana 的使用体验的杀手锏,我们自研的可视化分析平台集成了快速分析能力。用户可以直接输入 SQL 聚合语句,即时对日志进行聚合分析。


同时我们可以将查询分析界面作为一个入口,打通相关信息和功能。如日志告警快速的快速配置、日志写入量统计和优化点、快速配置二级索引、快速跳转分布式追踪平台等。


这些都是为了能让原先使用 Kibana 的用户能够在无缝地切换到新平台上来使用的基础上,拥有更好的排障体验。


3.4 日志告警除了在日志分析界面上人为进行日志查询排障外,日志监控规则也是常用且好用的快速感知系统问题的手段。


在日志服务 2.0 的版本中,日志告警服务在兼容了日志 ClickHouse 作为数据源的基础上,将计算模型进行了统一化,剥离了原先 Elasticsearch 场景的特有语义,使得计算和触发规则更灵活,配置更容易。


我们将一个日志告警规则定义为由以下几个属性组成:


名称和数据源;


查询时间范围,如 1 分钟;


计算间隔,如 1 分钟;


计算函数,如日志计数,日志字段和,日志字段最大值最小值,日志字段去重计数等;


日志过滤规则,如 region = 'sh' AND log.level = 'ERROR';


触发条件,如 大于/小于,环比上升/下降,基于智能时序异常检测等;


告警通道,如电话,短信,邮件等;


告警附带信息,如固定文案,关键日志等;


通知对象,一般为可配置的 Group(可以结合 Oncall 切换人员),或者可以是固定的人;


告警风暴抑制相关,如告警间隔时间,连续触发规则等。


目前 B 站系统中目前已配置超过 5000+的日志告警,我们提供了告警迁移工具,能够自动通过原 ES 语法的规则,生成对应的 2.0 版本过滤规则,来帮助用户快速迁移。


3.5 OpenTelemetry Logging 原 OpenTracing [1] 和 OpenCensus [2] 项目已经合并入 OpenTelemetry 项目,从趋势和未来考虑,新项目不再推荐使用前述两个项目,建议直接使用 Opentelemtry 通用 api 收集可观测数据。


Opentelemetry [3] 是一套工具集,专注于 可观测性 领域的数据收集端,致力于可观测性 3 大领域 metrics,logs,traces 的通用 api 规范以及支持编程语言的 sdk 实现。除了嵌入用户代码的 sdk ,Opentelemetry 也提供了可观测性数据收集时的 收集器 [4] 实现,该收集器可作为 sidecar,daemonset,proxy 3 种形式部署,支持多种可观测协议的收集和导出。


目前,OTEL Logging 的协议已经处于 stable,主要定义了以下的标准日志模型。


我们完整的按照 OTEL 的标准实现了 Golang 和 Java 的 SDK,并在采集器(Log-Agent)集成了 OTEL 兼容层。


3.6 如何解决日志搜索问题得益于 ClickHouse 的高压缩率和查询性能,小日志量的应用日志直接可以搜索即可。在大日志量场景,对于某种唯一 id 的搜索,使用 tokenbf_v1 建立二级索引,并引导用户使用 hasToken)或通过上文描述的~`操作符进行搜索,跳过大部分的 part,能获得不亚于 ES 的查询性能。对于某种日志模式的搜索,引导用户尽可能使用 logger_name,或者 source(代码行号)来进行搜索 同时尽量减少需要搜索的日志范围。在此基础上,我们还必须推进日志结构化。


一开始的日志往往是无结构化的,人可读的。随着微服务架构发展,日志作为可观测性的三大支柱,越来越需要关注日志的机器可读性。统一的日志平台也对日志的结构化有要求,来进行复杂聚合分析。


ClickHouse 方案中,由于缺少倒排索引,对日志结构化程度的要求会更高。在推进业务迁于新方案时,我们也需要同步进行结构化日志的推进。


对于这样一段常见日志:


log.Info("report id=32 created by user 4253)"结构化我们可以抽取 report id 和 user id,作为日志属性独立输出,并定义该段日志的类型,如:


log.Infov(log.KVString("log_type","report_created"),log.KVInt("report_id",32), log.KVInt("user_id",4253))结构化后对于整体存储和查询性能上都能获得提升,用户可以更方便的对 user id 或 report id 进行搜索或分析。


04 Clickhouse 功能增强与优化在日志场景中,我们选用 clickhouse 作为底层的查询引擎,主要原因有两个:一个是 clickhouse 相较于 ES 具备更高的资源利用效率,在磁盘,内存方面的消耗都更低;另外一个就是 Clichouse 支持的丰富的数据和索引类型能够满足我们针对特定 pattern 的查询性能需求。


4.1 Clickhouse 配置优化因为日志的数据量比较大,在实际的接入过程中我们也是遇到了一些问题。


Too many parts 这个问题在 clickhouse 写入过程中还是比较常见的,根因是 clickhouse merge 的速度跟不上 part 的 生成速度。这个时候我们的优化方向可以从两个方面层面考虑,写入侧和 clickhouse 侧。


在日志的写入侧我们降低写入的频次,通过攒 batch 这种方式来降低 clickhouse part 的生成速度。缺点是这种攒批会导致数据的时延会增高,对于实时查询的体感较差。而且,如果写入只由 batch 大小控制,那么在数据断流的时候就会有一部分数据无法写入。因为日志场景下对数据查询的时效性要求比较高,所以我们除了设置 batch 大小还会有一个超时时间的配置项。当达到这个超时时间,即便 batch 大小没有达到指定大小,也会执行写入操作。


在写入侧配置了满足业务需求的参数配置之后,我们的 part 生成速度依然超过了 merge 的速度。所以我们在 clickhouse 这边也进行了 merge 相关的参数配置,主要目的是控制 merge 的消耗的资源,同时提升 merge 的速度。


Merge 相关的参数,我们主要修改了以下几个:


min_bytes_for_wide_part


这个参数是用来指定落盘的 part 格式。当 part 的大小超过了配置就会生成 wide part,否则为 compact part。日志属于写多读少的一个场景,通过增大这个参数,我们可以让频繁生成的 part 在落盘时多为 compact part。因为 compact part 相较于 wide part 小文件的个数要更少一些,测试下来,我们发现相同的数据量,compact part 的 merge 速度要优于 wide part。相关的还有另一个参数 min_rows_for_wide_part,作用跟 min_bytes_for_wide_part 相似。


max_bytes_to_merge_at_min_space_in_pool


当 merge 的线程资源比较紧张时,我们可以通过调整这个参数来配置可 merge part 的最大大小。默认大小是 1M,我们将这个参数上调了。主要的原因是,我们在频繁写入的情况下,merge 资源基本处于打满的状态。而写入的 part 大小基本也都超过了 1M,此时这些 part 就不被 merge,进而导致 part 数据不断变多,最终抛出 Too many parts 的问题。


max_bytes_to_merge_at_max_space_in_pool


这个参数是用来指定 merge 线程资源充足时可 merge 的最大 part 大小。通过调整这个参数,我们可以避免去 merge 一些较大的 part。因为这些 part 的合并耗时可能是小时级别的,如果在这期间有较为频繁的数据写入,那就有可能会出现 merge 线程不够而导致的 too many parts 问题。


background_pool_size


默认线程池大小为 16,我们调整为 32,可以有更多的线程资源参与 merge。之所以没有继续上调这个参数,是因为较多的 merge 线程可能会导致系统的 CPU 和 IO 负载过高。


Zookeeper 负载过高 Clickhouse 的副本表需要频繁的读写 clickhouse,熟悉 clickhouse 的用户都知道 zookeeper 集群是 clickhouse 集群的性能瓶颈之一。因为 zookeeper 的写压力是没法通过增加节点数得到缓解,所以当一个集群的写请求过于频繁时,zookeeper 就可能成为我们写入失败的主要原因了。我们的日志系统为了能够在服务级别进行优化和数据 TTL 设置,就将每个接入的服务通过不同的表进行隔离。所以接入的服务越多,对应的表和分区就会越多。而分区和 part 过多,就会导致整个集群对 zookeeper 的读写请求频次增加,进而导致 zookeeper 集群负载过高。针对这个问题,社区已经有相关的 PR 解决了,那就是 auxiliary zookeeper。这个特性让我们可以在一个 clickhouse 集群中配置多个 zookeeper 集群,配置好之后我们只需要在建分布式表时指定对应的 zookeeper 集群即可。这样,zookeeper 写请求过于频繁的问题也能得到很好的解决了。


4.2 Clickhouse 动态类型 MapMap 类型作为数据库中一个重要的数据类型,能够很好地满足用户对于表动态 schema 的需求。对于后期可能会动态增减的字段,或者因为数据属性而不同的字段,我们可以将其抽象成一个或多个 map 存储,使用不同的 k-v 来存储这些动态字段。Clickhouse 的在版本 v21.1.2.15 将 Map 类型作为一个实验特性增加到支持的数据类型之中。而 Map 类型在 B 站的日志系统中解决的就是日志系统中不同服务具有不同的特有字段问题。使用 Map 字段一方面可以统一不同服务的表结构,另一方面使用 Map 能够更好地预设表的 schema,避免了后期因业务新增导致表结构频繁变更的问题, 降低了整个日志链路的复杂程度。


但是随着数据体量的增加和查询时间跨度的延伸,针对 clickhouse 原生 map 类型的查询和过滤效果越来越不如人意,虽然 clickhouse 目前支持的 map 类型在功能上能够满足我们的需求,但是性能上却依然有提升的空间。


4.3 Clickhouse Map 实现原理首先我们可以看看原生 Map 的实现。原生的 Map 是通过 Array(Tupple(key, value))这种嵌套的数据结构来保存 Map 的数据。当我们读取某个指定 key 值的数据时,Clickhouse 需要将指定的对应的 ColumnKey 和 ColumnValue 都读取到内存中进行反序列化。


假设原始表的数据如下表所示:


当我们需要查询 k1 对应的数据时,select map[’k1’] from map_table,那么每一行除了 key 值为 k1 的数据对我们而言都是不需要的。但是,在 Clickhouse 反序列化时,这些数据都会被读取到内存之中,产生了不必要的计算和 I/O。而且因为 Clickhouse 不支持 Map 类型的索引,这种读放大造成的损耗在数据体量很大的情况下对查询性能有很大的影响。


通过上面的例子,可以看出,原生 Map 对于我们而言主要有两个瓶颈:


Clickhouse 不支持 Map 类型索引,没法很好地下推过滤掉不包含指定 key 值的数据;


同一个 Map 中不需要的 kv 数据也会在查询时读取到内存中处理。


4.4 Map 类型索引支持针对上面的问题,我们的首次尝试是对 Clickhouse 索引进行加强,使其可以支持 Map 类型。因为我们的场景中 key 值多为 String 类型,所以我们优先改造了 bloom filter 相关的索引。在数据写入的时候我们会把每行数据的 map 都单独拎出来处理,获取到每一个 key 值,对 key 值创建索引。查询时,我们会解析出需要查询的 key 值,索引中不包含该值时则会跳过对应的 granule,以达到尽量少读取不必要数据的目的。


通过上述方式,我们在执行 select map[’k1’] from map_table 时,可以将未命中索引的 granule 都过滤掉,这样可以在一定程度上减少不必要的计算和 I/O。


创建测试表:


CREATE TABLE bloom_filter_map


(


`id` UInt32,
`map` Map(String, String),
INDEX map_index map TYPE tokenbf_v1(128, 3, 0) GRANULARITY 1
复制代码


)


ENGINE = MergeTree


ORDER BY id


SETTINGS index_granularity = 2


----插入数据


insert into bloom_filter_map values (1, {'k1':'v1', 'k2':'v2'});


insert into bloom_filter_map values (2,{'k1':'v1_2','k3':'v3'});


insert into bloom_filter_map values (3,{'k4':'v4','k5':'v5'});


----查询数据


select map['key1'] from bloom_filter_map;


通过 query_id 可以查看到只查询了两行数据,索引生效。


通过上述的实现在索引粒度设置合适的情况下我们能够有效地下推过滤掉部分非必要的数据。这部分的源码我们已经贡献到社区, 相关 PR [5] 。


虽然支持了 Map 类型的索引能够过滤部分数据,但是当 map 的 key 分布地比较稀疏时,例如上例中的 k1,如果在每个 granule 中都出现的话我们还是会读取很多无效的数据,同时,一个 map 中的我们不需要的 kv 我们还是没法跳过。为了解决这个问题,我们实现了 Clickhouse Map 类型的隐式列。


4.5 Map 隐式列简介隐式列就是在 Map 数据写入的时候,我们会把 map 中的每个 key 单独抽出来在底层作为一个 Column 存储。通过这种对用户透明的转换,我们可以在查询指定 key 时读取对应的 column,通过这种方式来避免读取需要的 kv。


通过这种转换,当我们再执行类似 select map[’k1’] from map_table 这种 SQL 时,Clickhouse 就只会去读取 column_k1 这个 Column 的数据,能够极大地避免读放大的情况。


考虑到已有的业务,我们并没有直接在原生的 Map 类型上进行改造,而是支持了一种新的数据类型 MapV2,底层则是实现了 Map 到隐式列的转换。


4.6 Map 隐式列实现数据写入


隐式类的写入格式跟原生 Map 一致。写入时主要是在 Map 的反序列化过程中加入了一个构造隐式列的流程,主要的功能就是检查 map 中的每一个 key 值。如果 key 值对应的 Column 存在则将 value 写入到存在的 Column 中。如果 Column 不存在,就会创建一个新的 Column,然后将 value 插入。需要注意的就是,每个隐式的 Column 行数需要保持一致,如果无值则需要插入默认值。


在数据写入的过程中,每个 part 对应的 columns.txt 文件,我们额外追加隐式列的信息。这样在加载 part 信息的时候,clichouse 就可以通过 loadColumns 方法把隐式列的信息加载到 part 的元信息之中了。


数据查询


查询流程我们没有做过多的改造, 就是在 Parser 层就的把 select map[’k1’] from map_table 转换成针对隐式列的查询,接下把它当做普通的 Column 查询即可。


4.7 查询测试创建测试表:


---- 隐式列


CREATE TABLE lt.implicit_map_local


(


`id` UInt32,
`map` MapV2(String, String)
复制代码


)


ENGINE = MergeTree


ORDER BY id


SETTINGS index_granularity = 8192;


---- 原始 map 类型


CREATE TABLE lt.normal_map_local


(


`id` UInt32,
`map` Map(String, String)
复制代码


)


ENGINE = MergeTree


ORDER BY id


SETTINGS index_granularity = 8192;


测试数据通过 jdbc 写入,自动生成 10000000 条数据,数据完全相同。


测试简单的指定 key 值查询。


此次测试中 Map 类型隐式列对于指定 key 值的查询场景有显著的性能提升。


Map 类型是我们目前解决动态 schame 最优的选择,原生 Map 类型的读放大问题在通过我们的优化之后能够一定缓解。而隐式列的实现则让我们可以像对待普通列一样,极大地避免了读取非必要的 map 数据。同时,我们后面也加上了对隐式列的二级索引支持,进一步增加了优化读取的手段。


当然,隐式列也有一些不适用的场景。


不支持 select 整个 map 字段


因为 Map 被拆成了多个 column 存储,所以无法支持 select *或者 select map 这样的查询。当然我们也进行过尝试。主要实现过两个方案:其一是冗余存储一份 Map 数据,按照原来的格式查询,当需要 select 整个 map 字段时我们就按原来的流程读取。这样查询性能跟原生的 Map 一样;还有一个方案就是在查询时读取 Map 对应的所有隐式列,然后实时生成 Map 返回,这种方案经过测试之后发现性能太差,最终也被放弃。


不适用于 map key 值非常稀疏的场景通过以上几个部分的讲解,我们知道 clickhouse 的隐式列实际上就是把 map 中的 key 拆出来作为单独的 columns 存储。在这个情况下,如果用户的 map 字段中的 key 值基数过高就会导致底层存储的列过多。针对这种情况,我们是增加了一个 max_implicit_columns 的 MergeTree 参数,通过这个参数我们可以控制隐式列的个数,当超过阈值时会直接返回错误信息给用户。在我们的日志系统的使用实践手册中我们会引导用户按照规范创建 key 值,避免 key 的基数过高的情况出现。如果有不按照约束写入的用户,max_implicit_columns 这个参数也会对其进行限制。


针对于第一个问题,我们也是实现了一套退而求其次的方案,那就是通过 select 每一个隐式列来实现。为此,我们会将每个 part 的隐式列都写到系统表 system.parts 中。当用户不知道有哪些 key 值时则可以通过查询系统表获取。


日志场景下,在功能上隐式列能够满足其对于动态 schema 的需求。同时,在查询时,用户一般都会勾选特定的字段。而在这种指定字段的查询上,隐式列的查询性能够做到与普通列保持一致。而且,因为隐式列能够像普通列一样配置二级索引,对于一些对性能要求更加极致的场景,我们的优化手段也要更加的灵活一些。


05 下一步的工作 5.1 日志模式提取目前虽然我们通过日志最佳实践和日志规范引导用户尽可能继续结构化日志的输出,但是仍然不可避免部分场景难以进行结构化,用户会将大段文本输入到日志中,在分析和查询上都有一定受限。我们计划实现日志模式并使用在查询交互,日志压缩, 后置结构化,异常模式检测这些场景上。


5.2 结合湖仓一体在湖仓一体日益成熟的背景下,日志入湖会带来以下收益:


部分日志有着三年以上的存储时间要求,比如合规要求的审计日志,关键业务日志等,数据湖的低成本存储特性是这个场景的不二之选。


现在日志除了用来进行研发排障外,也有大量的业务价值蕴含其中。日志入湖后可以结合湖上的生态体系,快速结合如机器学习、BI 分析、报表等功能,将日志的价值发挥到最大。


此外,我们长远期探索减少日志上报的中间环节,如从 agent 直接到 ClickHouse,去掉中间的 Kafka,以及更深度的结合 ClickHouse 和湖仓一体,打通 ClickHouse 和 iceberg。


5.3 Clickhouse 全文检索在全文检索场景下,Clickhouse 的性能表现依旧与 ES 有一定的差距,为了能够覆盖日差查询的全场景,clickhouse 在这方面的探索依旧任重道远。


在全文检索索引的实践上,我们也需要尝试不同的数据结构,极力做到内存资源和查询性能的双重保证。


参考链接:


[1] https://opentracing.io/


[2] https://opencensus.io/


[3] https://opentelemetry.io/


[4] https://opentelemetry.io/docs/collector/


[5] https://github.com/ClickHouse/ClickHouse/pull/28511

用户头像

Java-fenn

关注

需要Java资料或者咨询可加我v : Jimbye 2022.08.16 加入

还未添加个人简介

评论

发布
暂无评论
B站基于Clickhouse的下一代日志体系建设实践_Java_Java-fenn_InfoQ写作社区