01 背景介绍日志作为线上定位问题排障的重要手段,在可观测领域有着不可替代的作用。稳定性、成本、易用性、可扩展性都是日志系统需要追求的关键点。 B站基于Elastic Stack的日志系统(Billions) 从2017建设以来, 已经服务了超过5年,目前规模超过500台机器,每日写入日志量超过700TB。 ELK体系是业界最常用的日志技术栈,在传输上以结合规范key的JSON作为传输格式,易于多种语言实现和解析,并支持动态结构化字段。存储上ElasticSearch支持全文检索,能够快速从杂乱的日志信息中搜寻到关键字。展示上Kibana具有美观、易用等特性。 随着业务系统的高速发展,日志系统的规模也随之快速扩展,我们遇到了一系列的问题,同时可观测业界随着OpenTelemetry规范的成熟,推动着我们重新考量,迈入下一代日志系统。 02 遇到的问题
03 新架构体系针对上述的一系列问题,我们设计了Bilibili日志服务2.0的体系,主要的进化为使用ClickHouse作为存储,实现了自研的日志可视化分析平台,并使用OpenTelemetry作为统一日志上报协议。 如图所示为日志实时上报和使用的全链路。日志从产生到消费会经过采集→摄入 →存储 →分析四个步骤,分别对应我们在链路上的各个组件,先做个简单的介绍:
下边我们将针对几个重点进行详细设计阐述。 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作为非常成熟的日志分析界面,具有非常多的细节,都是在使用过程中沉积下来的功能。任何一个功能用户都有不低的使用频率。如图所示我们能实现了查询语句高亮和提示、字段分布分析、日志时间分布预览、查询高亮以及日志略缩展示等等。 用户在查询日志时, 使用标准SQL的Where condition部分, 如 log.level = 'ERROR' 进行日志过滤, 并对用户将隐式列和具体的表透明化(通过查询网关的能力)。 我们还使用code-mirror2实现了查询的自动补全和查询提示。提示的包括常用历史查询, 字段, 关键字和函数, 进一步降低用户的使用门槛。 在此基础之上,我们开发了能够领先于Kibana的使用体验的杀手锏,我们自研的可视化分析平台集成了快速分析能力。用户可以直接输入SQL聚合语句,即时对日志进行聚合分析。 同时我们可以将查询分析界面作为一个入口,打通相关信息和功能。如日志告警快速的快速配置、日志写入量统计和优化点、快速配置二级索引、快速跳转分布式追踪平台等。 这些都是为了能让原先使用Kibana的用户能够在无缝地切换到新平台上来使用的基础上,拥有更好的排障体验。 3.4 日志告警除了在日志分析界面上人为进行日志查询排障外,日志监控规则也是常用且好用的快速感知系统问题的手段。 在日志服务2.0的版本中,日志告警服务在兼容了日志ClickHouse作为数据源的基础上,将计算模型进行了统一化,剥离了原先Elasticsearch场景的特有语义,使得计算和触发规则更灵活,配置更容易。 我们将一个日志告警规则定义为由以下几个属性组成:
目前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 Merge相关的参数,我们主要修改了以下几个:
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对于我们而言主要有两个瓶颈:
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隐式列实现
在数据写入的过程中,每个part对应的columns.txt文件,我们额外追加隐式列的信息。这样在加载part信息的时候,clichouse就可以通过loadColumns方法把隐式列的信息加载到part的元信息之中了。
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每一个隐式列来实现。为此,我们会将每个part的隐式列都写到系统表system.parts中。当用户不知道有哪些key值时则可以通过查询系统表获取。 日志场景下,在功能上隐式列能够满足其对于动态schema的需求。同时,在查询时,用户一般都会勾选特定的字段。而在这种指定字段的查询上,隐式列的查询性能够做到与普通列保持一致。而且,因为隐式列能够像普通列一样配置二级索引,对于一些对性能要求更加极致的场景,我们的优化手段也要更加的灵活一些。 05 下一步的工作5.1 日志模式提取目前虽然我们通过日志最佳实践和日志规范引导用户尽可能继续结构化日志的输出,但是依旧不可避免部分场景难以进行结构化,用户会将大段文本输入到日志中,在分析和查询上都有一定受限。我们计划实现日志模式并使用在查询交互,日志压缩, 后置结构化,异常模式检测这些场景上。 5.2 结合湖仓一体在湖仓一体日益成熟的背景下,日志入湖会带来以下收益:
此外,我们长远期探索减少日志上报的中间环节,如从agent直接到ClickHouse,去掉中间的Kafka,以及更深度的结合ClickHouse和湖仓一体,打通ClickHouse和iceberg。 5.3 Clickhouse全文检索
|
相关文章
短视频带货哪个平台好(做短视频带货的全套流程)
“近半年,抖音挂车图文日均发布次数增长了5倍,日均GMV增长10倍以上。 凭借着低成本、高转化、变现快等特点,图文带货已经成为了抖音电商带货的新热门阵地,而且流量规模及成交效率还在增长中。” 这是抖音官方出的数据。 美食分享,一篇图文只卖巧克力,带货超32万元,这就是图文带货的魅力。...
抖音开店的费用和条件是什么?抖音开店教程介
抖音开店的费用和条件是什么?抖音开店教程介绍 抖音开店的流程和费用是多少? 开一家抖音店,需要500元的保证金。 1.首先,你必须有自己的抖音号,注册一个并登录,在“我”这个页面的右上角会有三条整齐的水平线,“三”点开,下面会有一个“设置”点击,先打开账号,安全...
抖音人气榜第一有什么奖励
抖音人气榜第一有什么奖励?很多人不清楚主播打榜到底是为了什么?人气榜第一有什么用,下面是详细的介绍。 灯牌变色,每小时榜获得前10名的主播可以获得粉丝灯牌变色的奖励,黄色的灯牌会变成红色的样子,并且会维持一整天的时间,晚上12点的时候会取消。 &n...
微信视频号怎么推广引流(视频号上热门方法技巧)
“视频号怎么引流精准客户?为什么我吸引来的客户总是不精准,虎哥”。 你还记得早年间的百度时代吗?纯靠一个搜索,让无数屌丝实现了暴富。 做视频号也是一样,如果你想要超级精准流量,就要布局SEO搜索流量, 用户主动带着明确目的去搜索的关键词,背后的需求一定是刚需且急迫。 换位思考一下,当...
抖音如何运营?抖音运营的方式有哪些?(2)
上期给大家介绍了直播账号的一个基础搭建,这期主要讲一讲直播间该怎么玩,怎么做的更长久。直播间玩法直播运营过程:账号从定位后,为了实现阶段性目标,做了什么事情。账号定位:基于现在要播的品,是怎么对账号进行定位运营的视频情况:为了符合定位跟爆点,前端视频在创作时,采取什么模式,...
百度广告如何投放?本文为您解答!
百度除了拥有搜索引擎外,还有百度地图、百度音乐、百度贴吧等产品,丰富的产品资源,满足了用户在日常生活中的需求,这也为广告主以及企业营销提供了平台,实现多场景下的广告投放,实现推广价值。那么,在推广中需要注意哪些问题?遇到这些问题如何解决,本文将为您提供详细解答! 1、在百度推广的流程是怎样的?...






