LC3视角:Kubernetes下日志采集、存储与处理技术实践
摘要: 在Kubernetes服务化、日志处理实时化以及日志集中式存储趋势下,Kubernetes日志处理上也遇到的新挑战,包括:容器动态采集、大流量性能瓶颈、日志路由管理等问题。本文介绍了“Logtail + 日志服务 + 生态”架构,介绍了:Logtail客户端在Kubernetes日志采集场景下的优势;日志服务作为基础设施一站式解决实时读写、HTAP两大日志强需求;日志服务数据的开放性以及与云产品、开源社区相结合,在实时计算、可视化、采集上为用户提供的丰富选择。
Kubernetes日志处理的趋势与挑战
Kubernetes的serveless化
Kubernetes容器技术促进了技术栈的去耦合,通过引入栈的分层使得开发者可以更加关注自身的应用程序和业务场景。从Kubernetes本身来看,这个技术解耦也在更进一步发展,容器化的一个发展的趋势是:这些容器都将会在无服务器的基础设施上运行。
谈到基础设施,首先可以联想到云,目前在AWS、阿里云、Azure的云上都提供了无服务器化的Kubernetes服务。在serverless Kubernetes上,我们将不再关心集群与机器,只需要声明容器的镜像、CPU、内存、对外服务方式就可以启动应用。
如上图,左右两边分别是经典Kubernetes、serverless Kubernetes的形态。在从左向右发展的过程中,日志采集也变得复杂:
- 在一个Kubernetes node上,可能会运行更大规模量级的pod,每个pod上都可能有日志或监控指标采集需求,意味着单node上的日志量会更大。
- 一个Kubernetes node上可能会运行更多种类的pod,日志采集来源变得多样化,各类日志的管理、打标需求越来越迫切。
日志实时性需求越来越强
首先要强调的是,并非所有日志都需要实时处理,当前很多"T+1"时效的日志交付依然非常重要,比如:做BI可能天级别延迟足够,ctr预估可能1小时延迟的日志也可以。
但是,在有些场景下,秒级或更高时效性的日志是必备前提条件,下图横坐标从左到右的对比展示了数据实时性对于决策的重要性。
举两个场景来谈一下实时日志对于决策的重要性:
- 告警处理。搞devops同学都深有体会,线上问题越早发现、越早处理我们就可以更淡定,处理时间拖得久了故障就可能跟着升级。实时日志帮助我们更快发现系统的异常指标,触发应急处理流程。
- AIOps。目前已经有一些算法基于日志做异常点检测、趋势预测:根据日志的pattern变化态势发现正常和异常情况下各类型日志出现的分布;针对IT业务系统,给定参数因子、变量对诸如硬盘故障等问题进行建模,加以实时日志来实现故障事件预警。
日志的集中式存储
日志来源有很多,常见的有:文件,数据库audit log,网络数据包等等。并且,针对同一份数据,对于不同的使用者(例如:开发、运维、运营等)、不同的用途(例如:告警、数据清洗、实时检索、批量计算等),存在使用多种方式重复消费日志数据的情况。
在日志数据的系统集成上,从数据源到存储节点再到计算节点,可以定义为一个pipeline。如下图,从上到下的变化是:日志处理正在从O(N^2) pipelines向O(N) pipelines演进。
在过去,各类日志用特定的方式来存储,采集到计算链路不具被通用和复用条件,pipeline非常复杂,数据存储也可能重复冗余。当前日志数据集成上,通过依赖一个中枢(Hub)来简化日志架构的复杂度、优化存储利用率。这个基础设施级别的Hub非常重要,需要支持实时pub/sub,能够处理高并发的写入、读取请求,提供海量的存储空间。
Kubernetes日志采集方案的演进
前面一节总结了Kubernetes日志处理上的趋势,那么家下来会盘点一下Kubernetes上几种常见日志采集的做法。
命令行工具
Kubernetes集群上要看日志,最基础的做法就是登机器,运行kubectl logs
就可以看到容器写出的stdout/stderr。
基础的方案没法满足更多需求:
- 只支持标准输出
- 数据不能持久化
- 除了看做不了别的事
节点日志文件落盘
在Kubernetes node维度去处理日志,docker engine将容器的stdout/stderr重定向到logdriver,并且在logdriver上可以配置多种形式去持久化日志,比如以json格式保存文件到本地存储。
和kubectl命令行相比,更进一步是把日志做到了本地化存储。可以用grep/awk这些Linux工具去分析日志文件内容。
这个方案相当于回到了物理机时代,但仍然存在很多问题没有解决:
- 基于docker engine和logdriver,除了默认的标准输出,不支持应用程序的日志
- 日志文件rotate多次或者Kubernetes node被驱逐后数据会丢失
- 没法跟开源社区、云上的数据分析工具集成
基于这个方案的一个进化版本是在node上部署日志采集客户端,将日志上传到中心化日志存储的设施上去。目前这也是推荐的模式,会在下一节再做介绍。
sidecar模式日志客户端采集
一种伴生模式,在一个pod内,除了业务容器外,还有一个日志客户端容器。这个日志客户端容器负责采集pod内容器的标准输出、文件、metric数据上报服务端。
这个方案解决了日志持久化存储等基本的功能需求,但两个地方有待提升:
- 一个节点上如果运行了N个pod,就意味着会同时运行着N个日志客户端,造成CPU、内存、端口等资源的浪费。
- Kubernetes下需要为每个pod单独进行采集配置(采集日志目录,采集规则,存储目标等),不易维护。
日志直写
直写方案一般是通过修改应用程序本身来实现,在程序内部组织几条日志,然后调用类似HTTP API将数据发送到日志存储后端。
带来的好处是:日志格式可以按需DIY,日志源和目标的路由可以任意配置。
也可以看到使用上的限制:
- 侵入代码会对业务改造有直接的依赖,推动业务改造一般比较漫长。
- 应用程序在发数据到远端遇到异常(比如网络抖动,接收服务端内部错误)时,需要在有限内存中缓存数据做重试,最终还是有概率造成数据丢失。
Kubernetes日志处理架构
来自社区的架构
目前见到比较多的架构中,采集工作通过每个Kubernetes node上安装一个日志客户端完成:
- Kubernetes官方推荐的是treasure data公司开源的fluentd,它的性能现、插件丰富度比较均衡。
- 社区有也不少在使用elastic公司开源的beats系列,性能不错,插件支持略少一些。针对一种数据需要部署特定的客户端程序,比如文本文件通过filebeats来采集。
- 也有些架构在使用logstash,ETL支持非常丰富,但是JRuby实现导致性能很差。
日志客户端把数据格式化好之后用指定协议上传到存储端,常见的选择有Kafka。Kafka支持实时订阅、重复消费,后期可以再根据业务需要把数据同步到其它系统去,比如:业务日志到Elastic Search做关键词查询,结合Kibana做日志可视化分析;金融场景日志要长期留存,可以选择投递Kafka数据到AWS S3这样的高性价比存储上。
这个架构看起来简洁有效,但在Kubernetes下距离完美还有些细节问题要解决:
- 首先,这是一个标准的节点级采集方案,Kubernetes下fluentd等客户端的程序部署、采集配置管理是个难题,在日志采集路由、数据打标、客户端配置等问题没有针对性优化。
- 其次,在日志的消费上,虽然Kafka的软件生态足够丰富,但是仍然需要专业人员来维护,要做业务规划、考虑机器水位、处理硬件损坏。如果要查询分析日志,还需要有对Elastic Search系统有足够了解。我们知道在PB级数据场景下,分布式系统的性能、运维问题开始凸显,而驾驭这些开源系统需要很强的专业能力。
日志服务的Kubernetes日志架构实践
我们提出基于阿里云日志服务的Kubernetes日志处理架构,用以补充社区的方案,来尝试解决Kubernetes场景下日志处理的一些细节体验问题。这个架构可以总结为:“Logtail + 日志服务 + 生态”。
首先,Logtail是日志服务的数据采集客户端,针对Kubernetes场景下的一些痛点做了针对性设计。也是按照Kubernetes官方建议的方式,在每个node上只部署一个Logtail客户端,负责这个node上所有的pod日志采集。
其次,针对关键词搜索和SQL统计这两个基本的日志需求:日志服务提供了基础的LogHub功能,支持数据的实时写入、订阅;在LogHub存储的基础上,可以选择开启数据的索引分析功能,开启索引后可以支持日志关键词查询以及SQL语法分析。
最后,日志服务的数据是开放的。索引数据可以通过JDBC协议与第三方系统对接,SQL查询结果与诸如阿里云DataV、开源社区的Grafana系统很方便地集成;日志服务的高吞吐的实时读写能力支撑了与流计算系统的对接,spark streaming、blink、jstorm等流计算系统上都有connector支持;用户还可以通过全托管的投递功能把数据写入到阿里云的对象存储OSS,投递支持行存(csv、json)、列存(parquet)格式,这些数据可以用作长期低成本备份,或者是通过“OSS存储+E-MapReduce计算”架构来实现数据仓库。
日志服务的优势
从四个点上来描述日志服务的特点:
- 在可靠性和稳定性上,它支撑了阿里集团和蚂蚁金服多次双十一和双十二的大促。
- 在功能上一站式覆盖了Kafka + ElasticSearch大部分场景。
- 作为云上的基础设施可以方便地实现弹性伸缩,对于用户来说,大促时不需要操心买机器扩容、每天坏上数十个盘需要维修等问题。
- 日志服务也同样具备云的0预付成本、按需付费的特点,并且目前每月有500MB的免费额度。
回顾第一节中提到的Kubernetes日志处理的趋势与挑战,这里总结了日志服务的三个优势:
- 作为日志基础设施,解决了各种日志数据集中化存储问题。
- 服务化的产品带给用户更多的易用性,与Kubernetes在serverless的目标上也是契合的。
- 功能上同时满足实时读写、HTAP需求,简化了日志处理的流程与架构。
日志服务结合社区力量进行Kubernetes日志分析
Kubernetes源自社区,使用开源软件进行Kubernetes日志的处理也是一些场景下的正确选择。
日志服务保证数据的开放性,与开源社区在采集、计算、可视化等方面进行对接,帮助用户享受到社区技术成果。
如下图,举一个简单的例子:使用流计算引擎flink实时消费日志服务的日志库数据,源日志库的shard并发与flink task实现动态负载均衡,在完成与MySQL的meta完成数据join加工后,再通过connector流式写入另一个日志服务日志库做可视化查询。
Logtail在Kubernetes日志采集场景下的设计
在本文第二节我们回顾了Kubernetes日志采集方案演进过程中遇到的问题,第三节介绍了基于阿里云日志服务的功能、生态。在这一节会重点对Logtail采集端的设计和优化工作做一些介绍,细数Logtail如何解决Kubernetes日志采集上的痛点。
Kubernetes采集的难点
采集目标多样化
- 容器stdout/stderr
- 容器应用日志
- 宿主机日志
- 开放协议:Syslog、HTTP等
采集可靠性
- 性能上需要满足单node上大流量日志场景,同时兼顾采集的实时性
- 解决容器日志易失性问题
- 在各种情况下尽量保证采集数据的完整性
动态伸缩带来的挑战
- 容器扩、缩容对自动发现的要求
- 降低Kubernetes部署的复杂度
采集配置易用性
- 采集配置怎么部署、管理
- 不同用途的pod日志需要分门别类存储,数据路由怎么去管理
Logtail高可靠采集
Logtail支持至少at-least-once采集的语义保证,通过文件和内存两种级别的checkpoint机制来保证在容器重启场景下的断点续传。
日志采集过程中可能遇到各种各样的来自系统或用户配置的错误,例如日志格式化解析出错时我们需要及时调整解析规则。Logtail提供了采集监控功能,可以将异常和统计信息上报日志库并支持查询、告警。
优化计算性能解决单节点大规模日志采集问题,Logtail在不做日志字段格式化的情况(singleline模式)下做到单cpu核100MB/s左右的处理性能。针对网络发送的慢IO操作,客户端将多条日志batch commit到服务端做持久化,兼顾了采集的实时性与高吞吐能力。
在阿里集团内部,Logtail目前有百万级规模的客户端部署,稳定性是不错的。
丰富的数据源支持
应对Kubernetes环境下复杂多样的采集需求,Logtail在采集源头上可以支持:stdout/stderr,容器、宿主机日志文件,syslog、lumberjack等开放协议数据采集。
将一条日志按照语义切分为多个字段就可以得到多个key-value对,由此将一条日志映射到table模型上,这个工作使得在接下来的日志分析过程中事半功倍。Logtail支持以下一些日志格式化方式:
- 多行解析。比如Java stack trace日志是由多个自然行组成的,通过行首正则表达式的设置来实现日志按逻辑行切分。
- 自描述解析。支持csv、json等格式,自动提取出日志字段。
- 通过正则、自定义插件方式满足更多特定需求。
- 对于一些典型的日志提供内置解析规则。比如,用户只需要在web控制台上选择日志类别是Nginx访问日志,Logtail就可以自动把一条访问日志按照Nginx的log format配置抽取出client_ip、uri等等字段。
应对节点级容器动态伸缩
容器天生会做常态化扩容、缩容,新扩容的容器日志需要及时被采集否则就会丢失,这要求客户端有能力动态感知到采集源,且部署、配置需要做到足够的易用性。Logtail从以下两个维度来解决数据采集的完整性问题:
部署
- 通过DaemonSet方式来快速部署Logtail到一个Kubernetes node上,一条指令就可以完成,与K8S应用发布集成也很方便。
- Logtail客户端部署到node上以后,通过domain socket与docker engine通信来处理该节点上的容器动态采集。增量扫描可以及时地发现node上的容器变化,再加上定期全量扫面机制来保证不会丢失掉任何一个容器更改事件,这个双重保障的设计使得在客户端上可以及时、完整发现候选的监控目标。
采集配置管理
- Logtail从设计之初就选择了服务端集中式采集配置管理,保证采集指令可以从服务端更高效地传达给客户端。这个配置管理可以抽象为"机器组+采集配置"模型,对于一个采集配置,在机器组内的Logtail实例可以即时获取到机器组上所关联的采集配置,去开启采集任务。
- 针对Kubernetes场景,Logtail设计了自定义标识方式来管理机器。一类pod可以声明一个固定的机器标识,Logtail使用这个机器标识向服务端汇报心跳,同时机器组使用这个自定义标识来管理Logtail实例。当Kubernetes节点扩容时,Logtail上报pod对应的自定义机器标识到服务端,服务端就会把这个机器组上的挂载的采集配置下发给Logtail。目前在开源采集客户端上,常见的做法是使用机器ip或hostname来标识客户端,这样在容器伸缩时,需要及时去增删机器组内的机器ip或hostname,否则就会导致数据采集的缺失,需要复杂的扩容流程保证。
解决采集配置管理难题
Logtail提供两种采集配置的管理方式,用户根据自己的喜好任选来操作:
- CRD。与Kubernetes生态深度集成,通过在客户端上事件监听可以联动创建日志服务上的日志库、采集配置、机器组等资源。
- WEB控制台。上手快,可视化方式来配置日志格式化解析规则,通过wizard完成采集配置与机器组的关联。用户只需要按照习惯来设置一个容器的日志目录,Logtail在上开启采集时会自动渲染成宿主机上的实际日志目录。
我们将日志从源到目标(日志库)定义为一个采集路由。使用传统方案实现个性化采集路由功能非常麻烦,需要在客户端本地配置,每个pod容器写死这个采集路由,对于容器部署、管理会有强依赖。Logtail解决这个问题的突破点是对环境变量的应用,Kubernetes的env是由多个key-value组成,在部署容器时可以进行env设置。Logtail的采集配置中设计了IncludeEnv和ExcludeEnv配置项,用于加入或排除采集源。在下面的图中,pod业务容器启动时设置log_type环境变量,同时Logtail采集配置中定义了IncludeEnv: log_type=nginx_access_log
,来指定收集nginx类用途的pod日志到特定日志库。
所有在Kubernetes上采集到的数据,Logtail都自动进行了pod/namesapce/contanier/image维度的打标,方便后续的数据分析。
日志上下文查询的设计
上下文查询是指:给定一条日志,查看该日志在原机器、文件位置的上一条或下一条日志,类似于Linux上的grep -A -B
。
在devops等一些场景下,逻辑性异常需要这个时序来辅助定位,有了上下文查看功能会事半功倍。然后在分布式系统下,在源和目标上都很难保证原先的日志顺序:
- 在采集客户端层面,Kubernetes可能产生大量日志,日志采集软件需要利用机器的多个cpu核心解析、预处理日志,并通过多线程并发或者单线程异步回调的方式处理网络发送的慢IO问题。这使得日志数据不能按照机器上的事件产生顺序依次到达服务端。
- 在分布式系统的服务端层面,由于水平扩展的多机负载均衡架构,使得同一客户端机器的日志会分散在多台存储节点上。在分散存储的日志基础上再恢复最初的顺序是困难的。
传统上下文查询方案,一般是根据日志到达服务端时间、日志业务时间字段做两次排序。这在大数据场景下存在:排序性能问题、时间精度不足问题,无法真实还原事件的真实时序。
Logtail与日志服务(关键词查询功能)相结合来解决这个问题:
一个容器文件的日志在采集上传时,其数据包是由一批的多条日志组成,多条日志对应特定文件的一个block,比如512KB。在这一个数据包的多条日志是按照源文件的日志序排列,也就意味着某日志的下一条可能是在同一个数据包里也可能在下一个数据包里。
Logtail在采集时会给这个数据包设置唯一的日志来源sourceId,并在上传的数据包里设置包自增Id,叫做packageID。每个package内,任意一条日志拥有包内的位移offset。
虽然数据包在服务端后存储可能是无序状态,但日志服务有索引可以去精确seek指定sourceId和packageId的数据包。
当我们指定容器A的序号2日志(source_id:A,package_id:N,offset:M)查看其下文时,先判断日志在当前数据包的offset是否为数据包的末尾(包的日志条数定义为L,末尾的offset为L-1):如果offset M小于(L-1),则说明它的下一条日志位置是:source_id:A,package_id:N,offset:M+1;而如果当前日志是数据包的最后一条,则其下一条日志的位置是:source_id:A,package_id:N+1,offset:0。
在大部分场景下,利用关键词随机查询获取到的一个package,可以支持当前包长度L次数的上下文翻页,在提升查询性能同时也大大降低的后台服务随机IO的次数。