Kubernetes容器技术促进了技术栈的去耦合,通过引入栈的分层使得开发者可以更加关注自身的应用程序和业务场景。从Kubernetes本身来看,这个技术解耦也在更进一步发展,容器化的一个发展的趋势是:这些容器都将会在无服务器的基础设施上运行。
谈到基础设施,首先可以联想到云,目前在AWS、阿里云、Azure的云上都提供了无服务器化的Kubernetes服务。在serverless Kubernetes上,我们将不再关心集群与机器,只需要声明容器的镜像、CPU、内存、对外服务方式就可以启动应用。
如上图,左右两边分别是经典Kubernetes、serverless Kubernetes的形态。在从左向右发展的过程中,日志采集也变得复杂:
首先要强调的是,并非所有日志都需要实时处理,当前很多"T+1"时效的日志交付依然非常重要,比如:做BI可能天级别延迟足够,ctr预估可能1小时延迟的日志也可以。
但是,在有些场景下,秒级或更高时效性的日志是必备前提条件,下图横坐标从左到右的对比展示了数据实时性对于决策的重要性。
举两个场景来谈一下实时日志对于决策的重要性:
日志来源有很多,常见的有:文件,数据库audit log,网络数据包等等。并且,针对同一份数据,对于不同的使用者(例如:开发、运维、运营等)、不同的用途(例如:告警、数据清洗、实时检索、批量计算等),存在使用多种方式重复消费日志数据的情况。
在日志数据的系统集成上,从数据源到存储节点再到计算节点,可以定义为一个pipeline。如下图,从上到下的变化是:日志处理正在从O(N^2) pipelines向O(N) pipelines演进。
在过去,各类日志用特定的方式来存储,采集到计算链路不具被通用和复用条件,pipeline非常复杂,数据存储也可能重复冗余。当前日志数据集成上,通过依赖一个中枢(Hub)来简化日志架构的复杂度、优化存储利用率。这个基础设施级别的Hub非常重要,需要支持实时pub/sub,能够处理高并发的写入、读取请求,提供海量的存储空间。
前面一节总结了Kubernetes日志处理上的趋势,那么家下来会盘点一下Kubernetes上几种常见日志采集的做法。
Kubernetes集群上要看日志,最基础的做法就是登机器,运行kubectl logs
就可以看到容器写出的stdout/stderr。
基础的方案没法满足更多需求:
在Kubernetes node维度去处理日志,docker engine将容器的stdout/stderr重定向到logdriver,并且在logdriver上可以配置多种形式去持久化日志,比如以json格式保存文件到本地存储。
和kubectl命令行相比,更进一步是把日志做到了本地化存储。可以用grep/awk这些Linux工具去分析日志文件内容。
这个方案相当于回到了物理机时代,但仍然存在很多问题没有解决:
基于这个方案的一个进化版本是在node上部署日志采集客户端,将日志上传到中心化日志存储的设施上去。目前这也是推荐的模式,会在下一节再做介绍。
一种伴生模式,在一个pod内,除了业务容器外,还有一个日志客户端容器。这个日志客户端容器负责采集pod内容器的标准输出、文件、metric数据上报服务端。
这个方案解决了日志持久化存储等基本的功能需求,但两个地方有待提升:
直写方案一般是通过修改应用程序本身来实现,在程序内部组织几条日志,然后调用类似HTTP API将数据发送到日志存储后端。
带来的好处是:日志格式可以按需DIY,日志源和目标的路由可以任意配置。
也可以看到使用上的限制:
目前见到比较多的架构中,采集工作通过每个Kubernetes node上安装一个日志客户端完成:
日志客户端把数据格式化好之后用指定协议上传到存储端,常见的选择有Kafka。Kafka支持实时订阅、重复消费,后期可以再根据业务需要把数据同步到其它系统去,比如:业务日志到Elastic Search做关键词查询,结合Kibana做日志可视化分析;金融场景日志要长期留存,可以选择投递Kafka数据到AWS S3这样的高性价比存储上。
这个架构看起来简洁有效,但在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计算”架构来实现数据仓库。
从四个点上来描述日志服务的特点:
回顾第一节中提到的Kubernetes日志处理的趋势与挑战,这里总结了日志服务的三个优势:
Kubernetes源自社区,使用开源软件进行Kubernetes日志的处理也是一些场景下的正确选择。
日志服务保证数据的开放性,与开源社区在采集、计算、可视化等方面进行对接,帮助用户享受到社区技术成果。
如下图,举一个简单的例子:使用流计算引擎flink实时消费日志服务的日志库数据,源日志库的shard并发与flink task实现动态负载均衡,在完成与MySQL的meta完成数据join加工后,再通过connector流式写入另一个日志服务日志库做可视化查询。
在本文第二节我们回顾了Kubernetes日志采集方案演进过程中遇到的问题,第三节介绍了基于阿里云日志服务的功能、生态。在这一节会重点对Logtail采集端的设计和优化工作做一些介绍,细数Logtail如何解决Kubernetes日志采集上的痛点。
采集目标多样化
采集可靠性
动态伸缩带来的挑战
采集配置易用性
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支持以下一些日志格式化方式:
容器天生会做常态化扩容、缩容,新扩容的容器日志需要及时被采集否则就会丢失,这要求客户端有能力动态感知到采集源,且部署、配置需要做到足够的易用性。Logtail从以下两个维度来解决数据采集的完整性问题:
部署
采集配置管理
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等一些场景下,逻辑性异常需要这个时序来辅助定位,有了上下文查看功能会事半功倍。然后在分布式系统下,在源和目标上都很难保证原先的日志顺序:
传统上下文查询方案,一般是根据日志到达服务端时间、日志业务时间字段做两次排序。这在大数据场景下存在:排序性能问题、时间精度不足问题,无法真实还原事件的真实时序。
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的次数。