【锁定直播】字节、华为云、阿里云等技术专家讨论如何将大模型接入 AIOps 解决实际问题,戳>>> 了解详情
写点什么

还在用 ES 查日志吗,快看看石墨文档 Clickhouse 日志架构玩法

  • 2022-02-22
  • 本文字数:5338 字

    阅读完需:约 18 分钟

还在用ES查日志吗,快看看石墨文档 Clickhouse 日志架构玩法

日志作为线上定位问题的关键手段,我们在选择日志采集、日志查询系统的时候需要考虑成本,架构,美观,易用性等问题,针对这些方面,本文由石墨文档架构师彭友顺,采用 Clickhouse 技术和石墨开源的 Mogo 日志查询系统,介绍了日志采集、日志传输、日志存储、日志管理的日志架构玩法。

1 背景

石墨文档全部应用部署在Kubernetes上,每时每刻都会有大量的日志输出,我们之前主要使用SLSES作为日志存储。但是我们在使用这些组件的时候,发现了一些问题。


  • 成本问题:

  • SLS个人觉得是一个非常优秀的产品,速度快,交互方便,但是 SLS 索引成本比较贵。

  • 我们想减少SLS索引成本的时候,发现云厂商并不支持分析单个索引的成本,导致我们无法知道是哪些索引构建的不够合理。

  • ES使用的存储非常多,并且耗费大量的内存。

  • 通用问题:

  • 如果业务是混合云架构,或者业务形态有SAAS和私有化两种方式,那么SLS并不能通用。

  • 日志和链路,需要用两套云产品,不是很方便。

  • 精确度问题:SLS存储的精度只能到秒,但我们实际日志精度到毫秒,如果日志里面有traceidSLS中无法通过根据traceid信息,将日志根据毫秒时间做排序,不利于排查错误。


我们经过一番调研后,发现使用Clickhouse能够很好的解决以上问题,并且 Clickhouse 省存储空间,非常省钱,所以我们选择了Clickhouse方案存储日志。但当我们深入研究后,Clickhouse作为日志存储有许多落地的细节,但业界并没有很好阐述相关Clickhouse采集日志的整套流程,以及没有一款优秀的Clickhouse日志查询工具帮助分析日志,为此我们写了一套Clickhouse日志系统贡献给开源社区,并将Clickhouse的日志采集架构的经验做了总结。先上个Clickhouse日志查询界面,让大家感受下石墨最懂前端的后端程序员。


2 架构原理图

我们将日志系统分为四个部分:日志采集、日志传输、日志存储、日志管理。


  • 日志采集:LogCollector采用Daemonset方式部署,将宿主机日志目录挂载到LogCollector的容器内,LogCollector通过挂载的目录能够采集到应用日志、系统日志、K8S 审计日志等。

  • 日志传输:通过不同Logstore映射到Kafka中不同的Topic,将不同数据结构的日志做了分离。

  • 日志存储:使用Clickhouse中的两种引擎数据表和物化视图。

  • 日志管理:开源的Mogo系统,能够查询日志,设置日志索引,设置LogCollector配置,设置Clickhouse表,设置报警等。



以下我们按照这四大部分,阐述其中的架构原理。

3 日志采集

3.1 采集方式

Kubernetes容器内日志收集的方式通常有以下三种方案。


  • DaemonSet 方式采集:在每个 node 节点上部署LogCollector,并将宿主机的目录挂载为容器的日志目录,LogCollector读取日志内容,采集到日志中心。

  • 网络方式采集:通过应用的日志 SDK,直接将日志内容采集到日志中心 。

  • SideCar 方式采集:在每个 pod 内部署LogCollectorLogCollector只读取这个 pod 内的日志内容,采集到日志中心。


以下是三种采集方式的优缺点:


DaemonSet方式网络方式SideCar方式
采集日志类型标准输出+文件应用日志
部署运维一般,维护DaemonSet低,维护配置文件
日志分类存储可通过容器/路径等映射业务独立配置
支持集群规模取决于配置数无限制
适用场景日志分类明确、功能较单一性能要求极高的场景
性能资源消耗


我们主要采用DaemonSet方式和网络方式采集日志。DaemonSet方式用于ingress、应用日志的采集,网络方式用于大数据日志的采集(可能需要简单的说明下原因)。以下我们主要介绍下DeamonSet方式的采集方式。​

3.2 日志输出

从上面的介绍中可以看到,我们的DaemonSet会有两种方式采集日志类型,一种是标准输出,一种是文件。引用元乙的描述:虽然使用 Stdout 打印日志是 Docker 官方推荐的方式,但大家需要注意:这个推荐是基于容器只作为简单应用的场景,实际的业务场景中我们还是建议大家尽可能使用文件的方式,主要的原因有以下几点:


  • Stdout 性能问题,从应用输出 stdout 到服务端,中间会经过好几个流程(例如普遍使用的JSON LogDriver):应用 stdout -> DockerEngine -> LogDriver -> 序列化成 JSON -> 保存到文件 -> Agent 采集文件 -> 解析 JSON -> 上传服务端。整个流程相比文件的额外开销要多很多,在压测时,每秒 10 万行日志输出就会额外占用 DockerEngine 1 个 CPU 核;

  • Stdout 不支持分类,即所有的输出都混在一个流中,无法像文件一样分类输出,通常一个应用中有 AccessLogErrorLogInterfaceLog(调用外部接口的日志)、TraceLog 等,而这些日志的格式、用途不一,如果混在同一个流中将很难采集和分析;

  • Stdout 只支持容器的主程序输出,如果是 daemon/fork 方式运行的程序将无法使用 stdout

  • 文件的 Dump 方式支持各种策略,例如同步/异步写入、缓存大小、文件轮转策略、压缩策略、清除策略等,相对更加灵活。


从这个描述中,我们可以看出在docker中输出文件在采集到日志中心是一个更好的实践。所有日志采集工具都支持采集文件日志方式,但是我们在配置日志采集规则的时候,发现开源的一些日志采集工具,例如fluentbitfilebeatDaemonSet部署下采集文件日志是不支持追加例如podnamespacecontainer_namecontainer_idlabel信息,并且也无法通过这些label做些定制化的日志采集。


agent类型采集方式daemonset部署sidecar部署
ilogtail文件日志能够追加label信息,能够根据label过滤采集能够追加label信息,能够根据label过滤采集
fluentbit文件日志无法追加label信息,无法根据label过滤采集能够追加abel信息,能够根据label过滤采集
filebeat文件日志无法追加label信息,无法根据label过滤采集能够追加label信息,能够根据label过滤采集
ilogtail标准输出能够追加label信息,能够根据label过滤采集能够追加label信息,能够根据label过滤采集
fluentbit标准输出能够追加label信息,能够根据label过滤采集能够追加abel信息,能够根据label过滤采集
filebeat标准输出能够追加label信息,能够根据label过滤采集能够追加label信息,能够根据label过滤采集


基于无法追加label信息的原因,我们暂时放弃了DeamonSet部署下文件日志采集方式,采用的是基于DeamonSet部署下标准输出的采集方式。​

3.3 日志目录

以下列举了日志目录的基本情况:


目录描述类型
/var/log/containers存放的是软链接,软链到/var/log/pods里的标准输出日志​标准输出
/var/log/pods存放标准输出日志​标准输出
/var/log/kubernetes/master存放Kubernetes 审计输出日志标准输出
/var/lib/docker/overlay2存放应用日志文件信息文件日志
/var/run获取docker.sock,用于docker通信文件日志
/var/lib/docker/containers用于存储容器信息两种都需要


因为我们采集日志是使用的标准输出模式,所以根据上表我们的LogCollector只需要挂载/var/log/var/lib/docker/containers两个目录。

3.3.1 标准输出日志目录

应用的标准输出日志存储在/var/log/containers目录下,​文件名是按照 K8S 日志规范生成的。这里以nginx-ingress的日志作为一个示例。我们通过ls /var/log/containers/ | grep nginx-ingress指令,可以看到nginx-ingress的文件名。



nginx-ingress-controller-mt2wx_kube-system_nginx-ingress-controller-be3741043eca1621ec4415fd87546b1beb29480ac74ab1cdd9f52003cf4abf0a.log


我们参照 K8S 日志的规范:/var/log/containers/%{DATA:pod_name}_%{DATA:namespace}_%{GREEDYDATA:container_name}-%{DATA:container_id}.log。可以将nginx-ingress日志解析为:


  • pod_name:nginx-ingress-controller-mt2w

  • namespace:kube-system

  • container_name:nginx-ingress-controller

  • container_id:be3741043eca1621ec4415fd87546b1beb29480ac74ab1cdd9f52003cf4abf0a


通过以上的日志解析信息,我们的LogCollector 就可以很方便的追加podnamespacecontainer_namecontainer_id的信息。​

3.3.2 容器信息目录

应用的容器信息存储在/var/lib/docker/containers目录下,目录下的每一个文件夹为容器ID,我们可以通过cat config.v2.json获取应用的 docker 基本信息。



3.4 LogCollector 采集日志

3.4.1 配置

我们LogCollector采用的是fluent-bit,该工具是cncf旗下的,能够更好的与云原生相结合。通过Mogo系统可以选择Kubernetes集群,很方便的设置fluent-bit configmap的配置规则。


3.4.2 数据结构

fluent-bit的默认采集数据结构。


  • @timestamp字段:string or float,用于记录采集日志的时间。

  • log字段:string,用于记录日志的完整内容。


Clickhouse如果使用@timestamp的时候,因为里面有@特殊字符,会处理的有问题。所以我们在处理fluent-bit的采集数据结构,会做一些映射关系,并且规定双下划线为Mogo系统日志索引,避免和业务日志的索引冲突。


  • _time_字段:string or float,用于记录采集日志的时间。

  • _log_字段:string,用于记录日志的完整内容。


例如你的日志记录的是{"id":1},那么实际fluent-bit采集的日志会是{"_time_":"2022-01-15...","_log_":"{\"id\":1}" 该日志结构会直接写入到kafka中,Mogo系统会根据这两个字段_time__log_设置clickhouse中的数据表。

3.4.3 采集

如果我们要采集ingress日志,我们需要在input配置里,设置ingress的日志目录,fluent-bit会把ingress日志采集到内存里。



然后我们在filter配置里,将log改写为_log_



然后我们在ouput配置里,将追加的日志采集时间设置为_time_,设置好日志写入的kafka borkerskafka topics,那么fluent-bit里内存的日志就会写入到kafka中。



日志写入到Kafka_log_需要为json,如果你的应用写入的日志不是json,那么你就需要根据fluent-bitparser文档,调整你的日志写入的数据结构:https://docs.fluentbit.io/manual/pipeline/filters/parser

4 日志传输

Kafka主要用于日志传输。上文说到我们使用fluent-bit采集日志的默认数据结构,在下图kafka工具中我们可以看到日志采集的内容。



在日志采集过程中,会由于不用业务日志字段不一致,解析方式是不一样的。所以我们在日志传输阶段,需要将不同数据结构的日志,创建不同的Clickhouse表,映射到Kafka不同的Topic。这里以ingress为例,那么我们在Clickhouse中需要创建一个ingress_stdout_stream的 Kafka 引擎表,然后映射到Kafkaingress-stdout Topic里。

5 日志存储

我们会使用三种表,用于存储一种业务类型的日志。


  • Kafka引擎表:将数据从Kafka采集到Clickhouseingress_stdout_stream数据表中。


create table logger.ingress_stdout_stream(  _source_ String,  _pod_name_ String,  _namespace_ String,  _node_name_ String,  _container_name_ String,  _cluster_ String,  _log_agent_ String,  _node_ip_ String,  _time_ Float64,  _log_ String)engine = Kafka SETTINGS kafka_broker_list = 'kafka:9092', kafka_topic_list = 'ingress-stdout', kafka_group_name = 'logger_ingress_stdout', kafka_format = 'JSONEachRow', kafka_num_consumers = 1;
复制代码


  • 物化视图:将数据从ingress_stdout_stream数据表读取出来,_log_根据Mogo配置的索引,提取字段在写入到ingress_stdout结果表里。


CREATE MATERIALIZED VIEW logger.ingress_stdout_view TO logger.ingress_stdout ASSELECT    toDateTime(toInt64(_time_)) AS _time_second_,fromUnixTimestamp64Nano(toInt64(_time_*1000000000),'Asia/Shanghai') AS _time_nanosecond_,  _pod_name_,  _namespace_,  _node_name_,  _container_name_,  _cluster_,  _log_agent_,  _node_ip_,  _source_,  _log_ AS _raw_log_,JSONExtractInt(_log_, 'status') AS status,JSONExtractString(_log_, 'url') AS url  FROM logger.ingress_stdout_stream where 1=1;
复制代码


  • 结果表:存储最终的数据。


create table logger.ingress_stdout(  _time_second_ DateTime,  _time_nanosecond_ DateTime64(9, 'Asia/Shanghai'),  _source_ String,  _cluster_ String,  _log_agent_ String,  _namespace_ String,  _node_name_ String,  _node_ip_ String,  _container_name_ String,  _pod_name_ String,  _raw_log_ String,  status Nullable(Int64),  url Nullable(String),)engine = MergeTree PARTITION BY toYYYYMMDD(_time_second_)ORDER BY _time_second_TTL toDateTime(_time_second_) + INTERVAL 7 DAYSETTINGS index_granularity = 8192;
复制代码

6 总结流程



  • 日志会通过fluent-bit的规则采集到kafka,在这里我们会将日志采集到两个字段里。

  • _time_字段用于存储fluent-bit采集的时间

  • _log_字段用于存放原始日志

  • 通过mogo,在clickhouse里设置了三个表。

  • app_stdout_stream: 将数据从Kafka采集到ClickhouseKafka引擎表

  • app_stdout_view: 视图表用于存放mogo设置的索引规则

  • app_stdout:根据app_stdout_view索引解析规则,消费app_stdout_stream里的数据,存放于app_stdout结果表中

  • 最后mogoUI界面,根据app_stdout的数据,查询日志信息。

7 Mogo 界面展示

查询日志界面:



设置日志采集配置界面:



以上文档描述是针对石墨Kubernetes的日志采集,想了解物理机采集日志方案的,可以在下文中找到《Mogo使用文档》的链接,运行docker-compose体验Mogo 全部流程,查询Clickhouse日志。限于篇幅有限,Mogo的日志报警功能,下次讲解。​

8 资料

2022-02-22 12:068674

评论 3 条评论

发布
用户头像
所有日志都使用标准输出?标准输出的日志是dockerengine,默认输出会在/var/log/docker目录,会进行io操作。一般这种类型的目录都是操作系统自带磁盘,性能较差。 当大量的标准输出日志会把磁盘的io打满,当磁盘io打满时,docker启动容器时需要从本地磁盘加载镜像或者配置文件需要做io(也是这个/var磁盘),可能会导致容器因为io问题无法启动。这个问题如何避免?
2022-02-25 23:29
回复
Docker的日志目录是可以改的呀,改到外挂的 ssd 就好了
2023-03-08 16:52 · 北京
回复
用户头像
为什么不直接用Loki?感觉这个设计思路和loki都差不多
2022-02-22 13:29
回复
没有更多了
发现更多内容

近年来面试阿里总结出360道面试题,【Java岗】(JVM

Java 程序员 后端

金三银四助力面试-手把手轻松读懂HashMap源码

Java 程序员 后端

我终于知道,中国互联网是怎么弯道超车,干翻美国了!

博文视点Broadview

这400道面试题,决定了你去BAT还是TMD

Java 程序员 后端

【Flutter 专题】11 图解 ListView 的多种绑定方式

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 11月日更

【LeetCode】环形链表 II Java题解

Albert

算法 LeetCode 11月日更

软件设计师复习(一)

Java 程序员 后端

还没搞懂Java中的路径?看完这篇文章你就全懂了

Java 程序员 后端

这篇Kafka笔记真是写的太好了!不收藏都对不起我的眼睛

Java 程序员 后端

送给你一份程序员大佬整理的 HTTP 基础知识大图

Java 程序员 后端

适合程序员表白的情话【保你脱单】(1)

Java 程序员 后端

超进化!阿里腾讯大佬联合撰写亿级网关、分布式、微服务等太香了!

Java 程序员 后端

软考(软件设计师)考点总结 -- 数据库技术

Java 程序员 后端

这六个 MySQL 死锁案例,能让你理解死锁的原因!

Java 程序员 后端

适合程序员表白的情话【保你脱单】

Java 程序员 后端

逼着面试官问了我ArrayList和LinkedList的区别,他对我彻底服了

Java 程序员 后端

超进化!阿里腾讯大佬联合撰写亿级网关、分布式、微服务等太香了!(1)

Java 程序员 后端

通过AOP和自定义注解实现请求日志收集功能

Java 程序员 后端

遇到Java内存溢出(OOM)时,这样排查

Java 程序员 后端

Vue进阶(幺捌叁):IE9兼容性问题-数据初始化问题

No Silver Bullet

Vue 11月日更

拥有一台服务器后,我竟然这么酷?

老表

Python Linux web开发 云服务器 跟老表学云服务器

超详细:常用的设计模式汇总

Java 程序员 后端

跨年巨作!13万字!腾讯高工纯手写“JDK源码笔记”直接带你飙向实战

Java 程序员 后端

还在傻乎乎得背MyISAM与InnoDB 的区别?一篇文章让你理解的明明白白

Java 程序员 后端

还在担心面试遇到SpringBoot,莫慌,我送你套神级pdf文档

Java 程序员 后端

还在用 Guava Cache?它才是 Java 本地缓存之王!

Java 程序员 后端

远景能源java后台开发实习面试题

Java 程序员 后端

透过根源从而探究红黑树的本质,究竟二叉树是什么神仙鬼怪?

Java 程序员 后端

dart系列之:dart语言中的异常

程序那些事

flutter dart 程序那些事 11月日更

重走JAVA之路(五):面试又被问线程池原理?教你如何反击(1)

Java 程序员 后端

重走JAVA之路(五):面试又被问线程池原理?教你如何反击

Java 程序员 后端

还在用ES查日志吗,快看看石墨文档 Clickhouse 日志架构玩法_开源_彭友顺@石墨文档_InfoQ精选文章