+86 135 410 16684Mon. - Fri. 10:00-22:00

Polling + Inotify 组合下的日志保序采集方案

Polling + Inotify 组合下的日志保序采集方案

Polling + Inotify 组合下的日志保序采集方案

日志数据采集

提到数据分析,大部分人首先想到的都是Hadoop,流计算,API等数据加工的方式。如果从整个过程来看,数据分析其实包含了4个过程:采集,存储,计算和理解四个步骤。

  • 采集:从各种产生数据的源头,将数据集中到存储系统。包括硬盘上的历史数据,用户网页的点击,传感器等等
  • 存储:以各种适合计算的模式集中式存储数据,其中既包含大规模的存储系统(例如数仓),也有例如临时的存储(例如Kafka类消息中间件)
  • 计算:形态多种多样,但大部分计算完成后会将结果再放入存储
  • 理解:利用机器学习、可视化、通知等手段将结果呈现出来

1512715770-4275-a98b15da6d2f7803daa9af76567e

数据采集是一门很大的范畴,从实时性上和规模上分,一般可以分为3类:

  • 实时采集:例如日志,database change log等
  • 定时任务:例如每隔5分钟从FTP或数据源去批量导出数据
  • 线下导数据:例如邮寄硬盘,AWS Snowmobile 卡车等
    从数据的价值以及体量上而言,实时数据采集毫无疑问最重要的,而其中最大的部分就是日志实时采集。

1512715769-3532-3748720c7352865edd6c35c69d63

日志采集Agent做了哪些工作?

日志采集Agent看起来很简单:安装在操作系统中,将实时产生的日志(文本)数据采集到类似消息中间件(类似Kafka)服务中。很多人可能觉得这是一个tail 命令就能干的,哪有这么复杂?

如果我们把其中细节展开就会发现一大堆工作,除了需要解决分布式日志汇聚的问题,还需要处理各种日志格式、不同采集目录、不同运行环境、多租户资源隔离、资源限制、配置管理、系统监控、容错、升级等等问题,而日志采集Agent就是为了解决这些问题应运而生的产物。

试想如果不用Agent,就拿最简单的收集nginx访问日志来讲,需要写一个脚本定期检测access.log有无更新,把更新的日志发送到服务端,除此之外还需要将原始访问日志解析成key/value字段、处理日志轮转、处理本地/服务端网络异常、处理访问流量burst时的削峰填谷、处理脚本异常等等,当一个接一个的问题解决完之后,回过头原来你又造了一遍轮子。

阿里云日志服务 的logtail就是一款进行日志实时采集的Agent,当前几十万台部署logtail的设备运行在各种不同环境上(集团、蚂蚁、阿里云,还有用户部署在公网、IOT设备),每天采集数PB的数据,支撑上千种应用的日志采集。从刚开始几个应用、几千台、每天几T数据的规模发展到今天,我们踩过很多坑,也从中学到很多,积累了很多宝贵的经验。

本期主要和大家一起分享logtail设计中对于轮询和事件模式共存情况下如何解决日志采集保序、高效、可靠的问题。

为什么要轮询+事件

什么是轮询什么是事件

对于日志采集,大家很容易想到通过定期检测日志文件有无更新来进行日志采集,这种我们一般称之为轮询(polling)的方式。轮询是一种主动探测的收集方式,相对也存在被动监听的方式,我们一般称之为事件模式。事件模式依赖于操作系统的事件通知,在linux下2.6.13内核版本引入inotify, 而windows在xp中引入FindFirstChangeNotification,两者都支持以被动监听的方式获取日志文件的修改事件。

轮询vs事件

下面来看看轮询和事件之间的区别,对比如下:

轮询 事件
实现复杂度
跨平台 不依赖操作系统 不同操作系统单独实现
采集延迟
资源消耗
系统限制 基本无限制 依赖内核/驱动
资源限制 基本无限制 依赖系统
大规模场景 支持较差 支持

轮询相对事件的实现复杂度要低很多、原始支持跨平台而且对于系统限制性不高;但轮询的采集延迟(默认加上轮询间隔一半的采集延迟)以及资源消耗较高,而且在文件规模较大(十万级/百万级)时轮询一次的时间较长,采集延迟非常高。

传统Agent怎么做

一般Agent(例如logstash、fluentd、filebeats、nxlog等)都采用基于轮询的方式,相对事件实现较为简单,而且对于大部分轻量级场景基本适用。但这种方式就会暴露以上对比中出现的采集延迟、资源消耗以及大规模环境支持的问题,部分对于这些条件要求较高的应用只能望而却步。

logtail的方案是什么

为了同时兼顾采集效率以及支持各类特殊采集场景,logtail使用了轮询与事件并存的混合方式(目前只支持linux,windows下方案正在集成中)。一方面借力inotify的低延迟与低性能消耗,另一方面使用轮询兼容不支持事件的运行环境。然而混合方案相比纯粹轮询/事件的方案都要复杂,这里主要存在3个问题:

  1. 如何解决高效采集的问题
  2. 如何解决日志顺序保证问题
  3. 如何保证可靠性问题

下面围绕这些问题对我们的方案进行展开

logtail轮询+inotify事件实现方式

轮询+inotify事件混合方案简介

1512715769-5067-a4ed731154b5577db8282ba12171

logtail内部以事件的方式触发日志读取行为,轮询和inotify作为较为独立的两个模块,对于同一文件/模块会分别产生独立的Create/Modify/Delete事件,事件分别存储于Polling Event Queue和Inotify Event Queue中。

轮询模块由DirFilePolling和ModifyPolling两个线程组成,DirFilePolling负责根据用户配置定期遍历文件夹,将符合日志采集配置的文件加入到modify cache中;ModifyPolling负责定期扫描modify cache中文件状态,对比上一次状态(Dev、Inode、Modify Time、Size),若发现更新则生成modify event。

Inotify属于事件监听方式,因此不存在独立线程,该模块根据用户配置监听对应的目录以及子目录,当监听目录存在变化,内核会将事件push到相应的file descriptor中。
1512715769-5173-ee8d069824ad0c75732c97cc343d

最终由Event Handler线程负责将两个事件队列合并(merge)到内部的Event Queue中,并处理相应的Create/Modify/Delete事件,进行实际的日志读取。

高效性如何保证

相信读者在看到混合两个字时一定想到一个非常明显的问题:logtail采用了两种方案,那是不是开销就是2倍啊?答案当然不是,logtail在混合方案中采取了以下几个措施来保证两种方案混合的情况下如何采两家之长并尽可能去两家之短:

  1. 事件合并(merge):为减少轮询产生的事件和inotify产生的事件多次触发事件处理行为,logtail在事件处理之前将重复的轮询/inotify事件进行合并,减少无效的事件处理行为;
  2. 轮询自动降级:如果在系统支持且资源足够的场景下,inotify无论从延迟和性能消耗都要优于轮询,因此当某个目录inotify可以正常工作时,则该目录的轮询进行自动降级,轮询间隔大幅降低到对CPU基本无影响的程度;
  3. 轮询与inotify cache共享:日志采集中的很大一部分开销来源于日志文件匹配,在集团内外经常会出现一台机器上logtail配置了上百种不同的配置的情况,对于一个文件需要对上百个配置进行逐一判断是否匹配。logtail内部对于匹配结果维护了一个cache,而且cache对于轮询和inotify共享,尽可能减少这部分较大的开销。

日志收集顺序保证

日志收集顺序难点分析

日志顺序性保证是日志采集需要提供的基本功能,也是较难实现的一种功能,尤其在以下几种场景并存的情况下:

  1. 日志轮转(rotate):日志轮转是指当日志满足一定条件(日志跨天、超过一定条数、超过一定大小)进行重命名/压缩/删除后重新创建并写入的情况,例如Ngnix访问日志可设置以20M位单位进行轮转,当日志超过20M时,将access.log重命名为access.log.1,之前的access.log.1重命名为access.log.2,以此类推。agent需要保证日志轮转时收集顺序与日志产生顺序相同;
  2. 不同配置方式:优秀的日志采集agent并不应该强制限制用户的配置方式,尤其在指定日志采集文件名时,有的用户习惯配置成*.log,有的用户习惯配置成*.log*,而无论哪种配置agent都应该能够兼容,不会出现*.log在日志轮转情况下少收集或*.log*在日志轮转情况下多收集的情况;
  3. 轮询与inotify并存问题:若系统不支持inotify,则只有轮询产生的事件,而若inotify正常工作,那么同一文件的修改会产生两次事件,而且由于inotify延迟较低,所以事件很可能会先于轮询的事件被处理。我们需要保证延迟到来的事件不会影响日志exactly once的读取;

基于轮转队列与文件签名的日志采集方法

基本概念

在logtail中,我们设计了一套用于在日志轮转、不同用户配置、轮询与inotify并存、日志解析阻塞情况下依然可以保证日志采集顺序的机制。本文将重点该机制的实现方法,在展开之前首先介绍logtail中用到的几个基本概念:

  • 文件的dev和inode标识
    • dev这里指的是设备编号、 inode是该文件在file system中的唯一标识,通过dev+inode的组合可唯一标识一个文件(这里需要排除硬连接)。文件的move操作虽然可以改变文件名,但并不涉及文件的删除创建,dev+inode并不会变化,因此通过dev+inode可以非常方便的判断一个文件是否发生了轮转。
  • inode引用计数
    • 每个文件都对应着一个inode,inode指向文件的meta信息,其中有一个字段是reference count,默认文件创建时引用计数为1,引用计数为0时文件被文件系统回收。以下情况会改变文件的引用计数:若文件open,则引用计数加1,文件close后减1;硬连接创建引用计数加1;文件/硬链接删除,引用计数减1。因此,虽然文件被删除,但只要有应用保持该文件的open状态,则该文件并不会被文件系统回收,应用还可以对该文件进行读取。
  • 文件签名(signature)
    • dev+inode只能保证同一时刻该文件的唯一性,但并不代表整个life cycle中的唯一性。在文件从文件系统中删除时,对应的inode也会被回收,内核file system实现中存在分配唯一inode的机制,为了提高inode分配性能,回收的inode会保留在文件系统的cache中,下一次创建文件时,若存在inode cache则直接将该inode赋给新文件。因此纯粹通过dev+inode判断轮转并不可行(例如日志文件到达一定size被删除后,重新创建继续写,只要期间没有其他文件创建,则dev+inode都没变),logtail中使用日志文件的前1024字节的hash作为该文件的签名(signature),只有当dev+inode+signature一致的情况下才会认为该文件是轮转的文件。

在logtail的设计中利用了以上几个概念的功能,下面介绍一下日志收集顺序保证的几个数据结构:

  • LogFileReader
    • LogFileReader存储了日志文件读取的元数据,包括sorcePath、signature、devInode、deleteFlag、filePtr、readOffset、lastUpdateTime、readerQueue(LogFileReaderQueue)。其中sorcePath是reader文件路径,,signature是文件的签名,devInode是改文件的dev+inode组合,deleteFlag用于标识该文件是否被删除,filePtr是文件指针,readOffset代表当前日志解析进度,lastUpdateTime记录最后一次进行读取的时间,readerQueue标识该reader所在的读取队列(参见下面介绍)。
  • LogFileReaderQueue
    • LogFileReaderQueue中存储sourcePath相同且未采集完毕的reader列表,reader按照日志文件创建顺序进行排列。
  • NamedLogFileReaderQueueMap
    • 以sourcePath为key/LogFileReaderQueue为value的map,用于存储当前正在读取的所有ReaderQueue
  • DevInodeLogFileReaderMap
    • 以devInode为key/LogFileReader为value的map,用于存储当前正在读取的所有Reader
  • RotatorLogFileReaderMap
    • 以devInode为key/LogFileReader为value的map,用于存储处于轮转状态且已经读取完毕的Reader

事件处理流程

logtail基于以上的数据结构实现了日志数据顺序读取,具体处理流程如下:
1512715769-6204-5bfe36268d6c5a4e0c9a8666a6e1

CreateEvent处理方式
  1. 对于日志的Create Event,首先从当前的devInodeReaderMap中查找是否存在该dev+inode的Reader(因为在轮询和Inotify共存的情况下,可能会出现在处理Create Event时Reader已经被创建的情况),若不存在则创建Reader。
  2. Reader通过dev+inode和sourcePath创建,创建Reader后需加入到devInodeReaderMap以及其sourcePath对应的ReaderQueue尾部
DeleteEvent处理方式
  1. 对于日志文件的Delete Event,若该Reader所在队列长度大于1(当前解析进度落后,文件虽被删除但日志未采集完成),则忽略此Delete事件;若Reader所在队列长度为1,设置该Reader的deleteFlag,若一定时间内该Reader没有处理过Modify事件且日志解析完毕则删除该Reader
ModifyEvent处理方式
  1. 首先根据dev+inode查找devInodeReaderMap,找到该Reader所在的ReaderQueue,获取ReaderQueue的队列首部的Reader进行日志读取操作;
  2. 日志读取时首先检查signature是否改变,若改变则认为日志被truncate写,从文件头开始读取;若signature未改变,则从readOffset处开始读取并更新readOffset
  3. 若该日志文件读取完毕(readOffset==fileSize)且ReaderQueue的size > 1,则从ReaderQueue中移除该Reader并加入到rotatorReadrMap中(日志已经发生了轮转,且轮转后的文件已经读取完毕,所以可以从ReaderQueue中移除),此时继续把Modify Event push到Event队列中,触发队列后续文件的读取,进入下一循环;若日志文件读取完毕且ReaderQueue的size==1(size为1说明该文件并没有轮转,极有可能后续还有写入,所以不能从ReaderQueue中移除),则完成次轮Modify Event处理,进入下一循环
  4. 若日志文件没有读取完成,则把Modify Event push到Event队列中,进入下一循环(避免所有时间都被同一文件占用,保证日志文件读取公平性)
  • RotatorLogFileReaderMap主要用于解决轮询事件延迟问题:当inotify事件处理完成、日志读取完毕、ReaderQueue size > 1同时发生,若直接删除该Reader,则轮询的事件到达时,将会查找不到Reader并创建一个新的Reader重新进行日志读取。因此我们在Reader读取完毕时将其放入到RotatorLogFileReaderMap保存,若事件查找不到Reader时会检测RotatorLogFileReaderMap,若存在则跳过此次事件处理,避免多重事件造成日志重复采集的情况。

日志采集可靠性保证

考虑到性能、资源、性价比等问题,logtail在设计之初并不保证exact once或者at least once,但这并不代表logtail不可靠,有很多用户基于logtail采集的access日志用来计费。下面主要介绍可靠性中较难解决的三个场景:

  1. 日志解析阻塞:由于各种原因(网络阻塞、日志burst写入、流量控制、CPU/磁盘负载)等问题可能造成日志解析进度落后于日志产生速度,而在此时若发生日志轮转,logtail需在有限资源占用情况下尽可能保证轮转后的日志文件不丢失
  2. 采集配置更新/进程升级:配置更新或进行升级时需要中断采集并重新初始化采集上下文,logtail需要保证在配置更新/进程升级时即使日志发生轮转也不会丢失日志
  3. 进程crash、宕机等异常情况:在进程crash或宕机时,logtail需尽可能保证日志重复采集数尽可能的少丢失日志

日志采集阻塞处理

1512715769-4881-6272f1a8ef95c4f8fe1f7dadfc32

正常情况下,日志采集进度和日志产生进度一致,此时ReaderQueue中只有一个Reader处于采集状态。如上图所示,正在被采集的access.log由于磁盘上存在、应用和logtail正在打开,所以引用计数为3,其他轮转的日志文件引用计数为1。

而当应用日志burst写入、网络暂时性阻塞、服务端Quota不足、CPU/磁盘负载较高等情况发生,日志采集进度可能落后于日志产生进度,此时我们希望logtail能够在一定的资源限制下尽可能保留住这些日志,等待网络恢复或系统负载下降时将这些日志采集到服务器,并且保证日志采集顺序不会因为采集阻塞而混乱。
1512715770-5231-be1ef6bd7983f6d7681ef3282449

如上图所示,logtail内部通过保持轮转日志file descriptor的打开状态来防止日志采集阻塞时未采集完成的日志文件被file system回收(在ReaderQueue中的file descriptor一直保持打开状态,保证文件引用计数至少为1)。通过ReaderQueue的顺序读取保证日志采集顺序与日志产生顺序一致。

  1. 当ReaderQueue的size大于1时说明日志解析出现阻塞,此时logtail会将该ReaderQueue中所有Reader的file descriptor保持打开状态,这样即使在日志文件轮转后被删除或被压缩(本质还是被删除)时logtail依然能够采集到该日志。
  2. 当日志轮转时(dev+inode变化,文件名未变),logtail会根据新的dev+inode创建Reader,并加入其文件名对应的ReaderQueue尾部,ReaderQueue保持顺序读取,以此保证日志文件解析顺序。
  • 若日志采集进度一直低于日志产生进度,则很有可能出现ReaderQueue会无限增长的情况,因此logtail内部对于ReaderQueue设置了上限,当size超过上限时禁止后续Reader的创建

配置更新/升级过程处理

logtail配置采用中心化的管理方式,用户只需在管理页面配置,保存后会自动将配置更新到远程的logtail节点。此外logtail具备自动升级的功能,当推出新版本时,logtail会自动从服务器下载最新版本并升级到该版本。

  • 为保证配置更新/升级过程中日志数据不丢失,在logtail升级过程中,会将当前所有Reader的状态保存到内存/本地的checkpoint文件中;当新配置应用/新版本启动后,会加载上一次保存的checkpoint,并通过checkpoint恢复Reader的状态。
  • 然而在老版本checkpoint保存完毕到新版本Reader创建完成的时间段内,很有可能出现日志轮转的情况,因此新版本在加载checkpoint时,会检查对应checkpoint的文件名、dev+inode有无变化
    1. 若文件名与dev+inode未变且signature未变,则直接根据该checkpoint创建Reader
    2. 若文件名与dev+inode变化则从当前目录查找对应的dev+inode,若查找到则对比signature是否变化;若signature未变则认为是文件轮转,根据新文件名创建Reader;若signature变化则认为是该文件被删除后重新创建,忽略该checkpoint。

进程crash、宕机等异常情况处理

  1. 进程异常crash:logtail运行时会产生两个进程,分别是守护进程和工作进程,当工作进程异常crash时(概率极低)守护进程会立即重新拉起工作进程
  2. 进程重新启动时状态恢复:logtail除配置更新/进程升级会保存checkpoint外,还会定期将采集进度dump到本地,进程重新启动的过程与版本升级的过程相似:除了恢复正常日志文件状态外,还会查找轮转后的日志,尽可能降低日志丢失风险

参考文档

日益增长的数据采集需求

Logtail基于Polling+Notify的组合方案以及日志轮转队列等相关技术实现了单一配置下的日志保序、高效、可靠采集问题。

然而日志采集并不仅仅是单一用户/应用需要完成的工作,例如一个典型的服务器上需要采集的日志数据有:资源类Metric数据、系统监控日志、Nginx访问数据、中间件请求数据、安全审计日志、各类应用中各个不同组件的日志等等;如果应用docker话,保守估计一个docker内的应用有6-7类日志,一台物理机运行50个docker,那即使采集docker内的日志就有300多种配置。

而现在涉及需要获取数据的角色上到看交易大盘的CEO下到查询日志的debug小弟,几乎所有人都会和日志相关。正是这些宝贵的数据才让我们真正成为一家数据型公司。因此,保障各种日志的有效采集是一款合格的日志采集Agent必须要解决的问题。
1512716197-6543-35981ea3f6bfb77e32e2f05c3407

多租户隔离的特性与挑战

多租户隔离技术早在20世纪60年代的大型主机中就已经开始使用,发展到今天非常多的应用/系统都应用了该技术。在每种不同的应用/系统中对于多租户隔离都有不同的诠释,本文中我们仅仅对应用在日志采集中的多租户隔离进行探讨。

多租户隔离特性

首先需要搞清楚日志采集场景下的多租户隔离需具备哪些特性,这里我们总结以下5点:隔离性、公平性、可靠性、可控性、性价比

  • 隔离性: 多租户隔离最基本特性,多个采集工作之间互不影响,部分采集配置阻塞不影响其他正常采集
  • 公平性: 保证各个阶段(读取、处理、发送)多个配置之间的公平性,不能因为某个配置下日志写入量大而导致其他配置被处理的概率降低
  • 可靠性: 无论在何种场景,可靠性都至关重要,多租户隔离下,如果部分采集阻塞,agent可以暂停该配置采集,但恢复时需尽可能保证数据不丢失
  • 可控性: 可控性主要体现在资源和行为的可控,agent需要具备控制各个配置的资源占用在合理范围,并且具备控制采集速率、暂停/开启等行为
  • 性价比: 以上特性最终方案实现时最需要关注的就是性价比,如何在尽可能少的资源占用情况下实现尽可能优的多租户隔离方案才是技术可行性与适用性的关键

多租户隔离挑战

目前logtail正逐步成为集团的基础设施之一,在集团内部一台服务存在数百个采集配置属于常态,每个配置的优先级、日志产生速度、处理方式、上传目的地址等都有可能不同,如何有效隔离各种自定义配置,保证采集配置QoS不因部分配置异常而受到影响?

一台服务器的数百个配置涉及不同应用的不同层面日志,其中部分日志优先级高而部分优先级低,关键时刻需要对低优先级日志降级停采,但又希望降级期间过后还能够将之前的数据追回,如何有效地限制低优先级配置以及动态降级且极可能保证日志不丢失?

目前Logtail在整个集团以及公有云的部署量近百万,如果logtail能够节省1MB内存使用,那整体将能够减少近1TB的内存,同时日志采集Agent的定位是服务的辅助应用,一台服务器并不能将主要资源提供给日志采集。如何在尽可能低的资源占用情况下尽可能、尽量公平以及尽量有效地调度各个配置?

业界采集Agent多租户隔离方案

首先我们来看一下业界的采集agent对于多租户隔离方面主要采用的技术,这里主要关注Logstash、Fluentd以及最近较火的Filebeat,我们分别从以上5个特性对这三款产品进行对比和分析。

logstash fluentd filebeat
隔离性 每个配置至少1个线程,独立的可持久化队列 每个配置至少1个线程,独立的可持久化队列 每个配置若干go runtime,独立队列
公平性 各配置间无协调,基于多线程调度 各配置间无协调,基于多线程调度 各配置间无协调,基于go runtime调度
可靠性 基于可持久化队列缓存保证 基于可持久化队列缓存保证 队列满后停止采集
可控性 可控制持久化队列资源,删除配置停采,支持远程配置 可控制持久化队列资源,删除配置停采,本地配置 可控制队列资源占用,删除配置停采
性价比 较低 较低 较高

Logstash、Fluentd和Filebeat都属于pipeline的架构,根据语言不同,分别使用独立的线程/go runtime实现了pipeline功能,每个pipeline内部顺序执行,各个pipeline间互相独立运行,此种方式隔离性较好,实现较为简单,在小规模场景下较为适用。然而随着配置数量增长,相应的线程数/go runtime呈等比上升,在采集配置较多的情况下资源难以控制;而且由于各个pipeline间完全依赖底层(操作系统/go runtime)调度,当CPU资源无法全部满足时,数据量较高的配置会占用较多的执行时间,导致其他数据较少的配置获取资源的概率降低。

Logtail多租户隔离方案

整体架构

不同于当前主流的开源采集agent实现,logtail采用的是更加复杂的架构,事件发现、数据读取、解析、发送等都采用固定数量的线程(解析线程可配置),线程规模不会随配置数增多。虽然所有配置都运行在同一执行环境,但我们采取了一系列的措施保障各个配置处理流程的互相隔离、配置间调度的公平、数据采集可靠性、可控性以及非常高的资源性价比。
1512716196-6481-0e4692ac4570a2e328d0edbe878e

下面我们主要介绍该方案中一些实现多租户隔离的较为关键性的技术

基于时间片的采集调度

业界主流的Agent对于每个配置会分配独立的线程/go runtime来进行数据读取,而logtail数据的读取只配置了一个线程,主要原因是:

  • 单线程足以完成所有配置的事件处理以及数据读取,数据读取的瓶颈并不在于计算而是磁盘,对于正常的服务器,每秒基本不可能产生超过100MB的日志,而logtail数据读取线程可完成每秒200MB以上的数据读取(SSD速率可以更高)
  • 单线程的另一个优势是可以使事件处理和数据读取在无锁环境下运行,相对多线程处理性价比较高

但单线程的情况下会存在多个配置间资源分配不均的问题,如果使用简单的FCFS方式,一旦一个写入速度极高的文件占据了处理单元,它就一直运行下去,直到该文件被处理完成并主动释放资源,此方式很有可能造成其他采集的文件被饿死。因此我们采用了基于时间片的采集调度方案,使各个配置间尽可能公平的调度。
1512716196-5353-78baa54d84f28a81dfea2c1296b6

  1. Logtail会将Polling以及Inotify事件合并到无锁的事件队列中(参见上一篇),每个文件的修改事件会触发日志读取;
  2. 日志读取将从上一次读取的偏移量LastReadOffset处开始,尝试在固定的时间片内将文读取到EOF处;
  3. 如果时间片内读取完毕,则认为该事件处理完毕,删除该事件;
  4. 如果时间片内读取未完成,则将该时间重新push到队列尾部,等待下一次调度。

该方案使得每个采集目标得到公平地对待,所有文件都有被调度运行的机会,很好地解决了采集目标的饿死现象。

多级高低水位反馈队列

基于时间片的采集调度保证了各个配置的日志在数据读取时得到公平的调度,满足了多租户隔离中基本的公平性,但对于隔离性并未起到帮助作用。例如当部分采集配置因处理复杂或网络异常等原因阻塞时,阻塞配置依然会进行处理,最终会导致队列到达上限而阻塞数据读取线程,影响其他正常配置。

为此我们设计了一套多级高低水位反馈队列用以实现多个采集配置间以及读取、解析、发送各个步骤间有效的协调和调度。虽然和进程调度中Multilevel feedback queue命名较为类似,但队列实现以及适用场景有很大区别。
1512716196-3044-ad97afdf95db983dfef4b4045cf6

  • 多级
    • 这里的多级指的是处理过程的多级,即各个处理过程间会有一个这样的队列且相邻队列互相关联
    • 例如在Logtail的数据读取、处理、发送流程中需要在读取->解析以及解析->发送间各自设置一个这样的队列
  • 高低水位:
    • 单一队列中设置了高低两个水位
    • 当队列增长到高水位时,停止非紧急数据写入(例如进程重启时、数据拆分等特殊情况允许写入)
    • 当队列从高水位消费到低水位时,再次允许写入
  • 反馈:
    • 反馈分为同步和异步两种
    • 在准备读取当前队列数据时会同步检查下一级队列状态,当下级队列到达高水位时跳过此队列
    • 当前队列从高水位消费到低水位时,异步通知关联的前一级队列

1512716196-4916-7e47927a624a669ce6b5d1ae966c

由于多个配置存在,所以我们会为每个配置创建一组队列,每个队列使用指针数组实现,每一级中所有配置队列公用一个锁,对于性能以及内存消耗较为友好。Logtail中的多级高低水位反馈队列结构如下:
1512716196-3407-2639b33b18efd7a025f619978395

我们以日志解析这个步骤的工作方式来观察多级反馈队列的行为:

  1. 初始状态下解析线程处理Wait状态,当有数据到达或下一级发送线程某一配置的队列从高水位消费到低水位时,进入FindJob状态
  2. FindJob会从上一次处理的队列位置顺序查找当前有数据且下一级队列可以写入的队列,若查找到则进行Process状态,否则进行Wait状态
  3. Process对于当前job解析完后,判断该job所属队列是否从高水位到达低水位,若是则进入Feedback状态,否则回到FindJob查找下一个有效job
  4. Feedback状态会向关联的上一级队列发送信号,参数携带当前队列ID,用以触发上一级流程运行,信号发送完毕后进入FindJob状态

基于多级高低水位反馈队列的处理过程中,当遇到下一级阻塞的队列时直接跳过,防止因阻塞Job的处理导致线程阻塞,具有较高的隔离性;FindJob会记录上一次查找的队列ID,下次查找时会从该ID之后的队列开始,保证了各个配置间调度的公平性。

流控以及阻塞处理

上一节的多级高低水位反馈队列解决了多配置间的隔离性和公平性问题,但对于可控性以及可靠性方面还存在一些问题。例如:

  1. 无法精确控制每个配置的的采集流量,只能通过删除采集配置停止采集
  2. 如果某一配置完全阻塞时,当该配置关联日志文件轮转,恢复阻塞时将丢失轮转前的数据

这里主要包括三个部分:事件处理、数据读取逻辑以及数据发送控制:

  1. 事件处理与数据读取无关,即使读取关联的队列满也照常处理,这里的处理主要是更新文件meta、将轮转文件放入轮转队列,具体可查看上一篇文章;此种方式可保证即使在配置阻塞/暂停的情况下依然保证及时文件轮转也不会丢失数据;
  2. 当配置关联的解析队列满时,如果将事件重新放回队列尾,则会造成较多的无效调度,使CPU空转。因此我们在遇到解析队列满时,将该事件放到一个专门的blocked队列中,当解析队列异步反馈时重新将blocked队列中的数据放回事件队列;
  3. Sender中每个配置的队列关联一个SenderInfo,SenderInfo中记录该配置当前网络是否正常、Quota是否正常以及最大允许的发送速率。每次Sender会根据SenderInfo中的状从队列中取数据,这里包括:网络失败重试、Quota超限重试、状态更新、流控等逻辑

整体流程梳理

现在让我们回顾一下logtail在多租户隔离中使用的相关技术,具体如下图:

  • 通过时间片采集调度保证各个配置数据入口的隔离性和公平性
  • 通过多级高低水位反馈队列保证在极低的资源占用下依然可以保证各处理流程间以及多个配置间的隔离性和公平性,
  • 通过事件处理不阻塞的机制保证即使在配置阻塞/停采期间发生文件轮转依然具有较高的可靠性
  • 通过各个配置不同的流控/停采策略以及配置动态更新保证数据采集具备较高的可控性

1512716199-1582-38938a3c7ae409281815004275d9

通过以上几点设计的组合,我们使用极低的资源构建出了虚拟的多租户Pipleline结构:
1512716199-5829-434d3fd2b4dbdb52effa395f26b6

实战见真知

多租户隔离中的性价比问题只靠语言描述是苍白无力的,最好的方式还是拿数据说话:

双11背后的日志采集

目前logtail已承载阿里云全站、所有云产品服务、全球各Region部署、阿里巴巴集团(淘宝、天猫、菜鸟等)上重要服务的数据采集。每天采集接近百万服务器上的实时数据,对接数千个应用与消费者。今年双十一蚂蚁金服(支付宝)几乎所有应用、用户及服务器产生的数据都由Logtail采集。

目前logtail在蚂蚁安装了数十万台,平均每台机器上存在近百个采集配置,每天采集上千个应用数PB的日志。在双11期间,为了保证核心应用数据采集正常,双11零点前对几百个应用提前降级停采,零点高峰过后逐批恢复日志采集,logtail在3个小时追完了这几百个应用5小时的高峰数据,并且保证停采期间日志即使轮转/删除也不会丢失。下图是追数据期间logtail的CPU、内存占用:
1512716199-4579-cc60237eb33eb50c8d9d5ef97f27

可以看到即使在双11期间logtail的平均cpu占用也只有单核的1.7%,平均内存最高也只有42M。追数据期间cpu相对只上升0.4%、内存只升高7M。抛开功能不谈,logtail对于资源的控制是开源采集Agent无法达到的。

性能对比

在logstash、fluentd和filebeat中,多租户实现最好且性能最高的是filebeat,所以这里我们拿filebeat进行对比。

  • 测试机配置:
    CPU : 16核 Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz

MEM : 64GB
DISK: 4块1TB SSD(日志写入单独一块SSD)

  • filebeat配置优化
    网上filebeat的benchmark约为30K/s(日志条数每秒),这里为了尽可能提高filebeat性能,我们把filebeat读文件缓存设置为512k,将输出配置到一块单独的SSD中并将日志rotate的size设为4GB(相对网上benchmark性能提升一倍左右)

配置数较少情况

下面是logtail与filebeat在极简模式(单行日志,不对日志解析)分别在1、2、4、8个配置下、日志写入速度分别为0.1、1、5 MB/s的CPU和内存占用

0.1 M/s 1M/s 5M/s
1配置 FB:1.8% 22M LT: 0.5% 34M FB:15.9% 22M LT: 1.3% 36M FB: 76.3% 26M LT: 5.5% 42M
2配置 FB:3.0% 22M LT: 0.6% 35M FB:27.5% 24M LT: 2.5% 44M FB: 137.9% 31M LT: 8.3% 50M
4配置 FB:6.3% 30M LT: 0.8% 35M FB:52.4% 30M LT: 4.3% 59M FB: 190.4% 31M(丢数据) LT: 15.3% 69M
8配置 FB:10.9% 36M LT: 1.1% 46M FB:103% 37M LT: 6.5% 82M FB: (丢数据) LT: 30.5% 83M

1512716200-4118-da7128aa11e7df625ff74b0e0dcb

filebeat对于内存控制较优,但性能相对logtail有一定差距:filebeat处理能力约为18MB/s,测试中一条日志约为300字节,换算成日志条数约为60K/s(网上benchmark约为30K/s)。

可以看出filebeat相对logstash和fluentd有一定的优势(快10倍左右);logtail的极简模式(不对日志解析,和filebeat类似)下的处理能力约为150M/s,相对优化后的filebeat有8倍左右的性能提升。由于filebeat达到18MB/s耗费了200%左右的cpu,logtail达到150MB/s只需消耗100%的cpu,所以如果从cpu的性价比上logtail相比filebeat有十多倍的优势。

配置数量较多

下面对比100个配置情况下filebeat和logtail的性能:

0.01 M/s 0.1M/s 1M/s
Logtail 2.7% 60M 8.0% 65M 65.4% 98M
filebeat 13.3% 236M 102.5% 238M 210.3% 238M(丢数据)

1512716200-1858-6bd128e7d857c1b075d4749c5cd8

当配置上升至100个时,filebeat 内存占用明显升高,而logtail的内存相对增长较低;100个配置下logtail的cpu消耗与相同数据量情况下2配置的基本无区别。同时观察filebeat cpu消耗情况,可以看到约20%的cpu消耗在涉及和调度相关的futex调用,可见当配置数增多时即使依赖go的协程调度方式也会消耗较多的cpu。
1512716200-4664-a3d701c1ce7164473f624a9d4256

总结

一个数据采集软件看起来很微小很简单,但当遇到超大的规模、超多的用户、超级的数据量时一切都需要重新考虑。阿里云日志服务的logtail就是这样一款历经上百万的部署量、每天数PB的数据、近万个应用洗礼的日志采集Agent。

相对开源软件,我们最大的优势是有阿里、有双11这样的环境给我们练兵、采坑。今天分享的多租户隔离技术,对于开源agent或许都不会考虑这个问题,但相信未来业务规模上升到一定体量肯定会遇到,希望到时候我们的分享能给大家一定的帮助。

展望

改进

  • 目前logtail虽然在多租户的公平性上做的比较好,但对于配置的优先级上面并没有做太多的优化,未来我们需要在这方面多下功夫,毕竟不同类型的数据重要程度是不一致的,采集的优先级也需要更精确。
  • 当前logtail的监控功能对于用户的可见度太低,未来我们考虑会将logtail的运行数据、错误数据等打包成一套Agent监控报警方案,将logtail的一体化采集方案做的更加完备。

新功能

logtail即将推出的新版本中还将支持http、mysql binlog以及mysql sql输入源,欢迎大家体验:

  • 支持http作为输入源,用户可以配置特定的url,logtail会定期请求并将请求数据处理并上传到日志服务。此方式能支持采集nginx、haproxy、docker daemon等能提供http接口的数据。
  • 支持mysql binlog作为输入源(包括RDS),以binlog形式同步数据到日志服务中,类似canal
  • 支持mysql sql作为输入源,可通过select语句自定义将数据采集到日志服务中,支持增量采集

参考资料

Multilevel feedback queue
cpu scheduling
log service
logtail vs logstash, flunetd
logstash
filebeat
fluentd

使用日志服务LogHub替换Kafka