写点什么

还在用 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:069520

评论 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
回复
没有更多了
发现更多内容

优酷播放黑科技 | 自由视角技术的全链路策略与落地实践

阿里巴巴终端技术

客户端 音视频技术 视频技术

私有化部署是什么意思?企业私有化部署的几种类型和利弊分析

WorkPlus

企业怎么制作帮助文档

小炮

企业 帮助文档

区块链中的共识机制简介

中原银行

区块链 中原银行

Q1过去了,Gartner战略技术趋势在不动产领域落了几项?

大数据 技术 低代码 AIOT 分布式,

轻轻松松实现本地和云主机之间的文件上传下载

天翼云开发者社区

Flink CDC 2.2 正式发布,新增四种数据源,支持动态加表,提供增量快照框架

Apache Flink

大数据 flink 编程 流计算 实时计算

基于Prometheus的企业级监控体系探索与实践

中原银行

分布式 微服务 云原生 Prometheus 中原银行

AI工具-标注工具labelme

AIWeker

人工智能 标注工具

分布式事务揭秘

中原银行

分布式 分布式事务 云原生 中原银行

深度确定性策略梯度(DDPG)

行者AI

玩转天翼云安全组

天翼云开发者社区

数字化转型-基本认知

Geek_XOXO

数字化转型

从2018到2022: 一个大数据工程师眼中的TiDB

TiDB 社区干货传送门

协同·转型·智慧,WorkPlus移动平台帮助企业走好数字化转型之路

WorkPlus

《中国金融科技与数字普惠金融发展报告(2022)》发布 十大趋势研判未来行业发展

WorkPlus

浅谈外挂常识和如何防御

行者AI

低代码实现探索(三十九)组件库的开发

零道云-混合式低代码平台

【征文大赛】TiDB 社区专栏第一届征文大赛,快来一次性集齐所有周边吧!

TiDB 社区干货传送门

将 AWS S3 数据迁移至 TiDB Cloud 集群

TiDB 社区干货传送门

HAVE FUN|Layotto 源码解析

SOFAStack

GitHub 开发者 活动 源码解析 源码剖析

Linux内核权限提升漏洞

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 漏洞挖掘

模块1 作业

KennyQ

一文简述:云端架构的演变过程

穿过生命散发芬芳

3月月更

国产化浪潮下TiDB解决的痛点问题

TiDB 社区干货传送门

AI目标检测概要

AIWeker

人工智能 目标检测

windowsXP用户无法远程桌面连接天翼云2008云主机

天翼云开发者社区

【技术干货分享】一文了解Nginx反向代理与conf原理

Linux服务器开发

nginx 负载均衡 反向代理 后端开发 Linux服务器开发

Apache Flink 在翼支付的实践应用

Apache Flink

大数据 flink 编程 流计算 实时计算

一张图看懂全球最新DDoS攻击趋势

科技热闻

AI观点说-关于深度学习的一点思考

AIWeker

人工智能 深度学习

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