1 背景和问题

随着云计算、PaaS平台的普及,虚拟化、容器化等技术的应用,例如Docker等技术,越来越多的服务会部署在云端。通常,我们需要需要获取日志,来进行监控、分析、预测、统计等工作,但是云端的服务不是物理的固定资源,日志获取的难度增加了,以往可以SSH登陆的或者FTP获取的,现在可不那么容易获得,但这又是工程师迫切需要的,最典型的场景便是:上线过程中,一切都在GUI化的PaaS平台点点鼠标完成,但是我们需要结合tail -F、grep等命令来观察日志,判断是否上线成功。当然这是一种情况,完善的PaaS平台会为我们完成这个工作,但是还有非常多的ad-hoc的需求,PaaS平台无法满足我们,我们需要日志。本文就给出了在分布式环境下,容器化的服务中的分散日志,如何集中收集的一种方法。

2 设计约束和需求描述

做任何设计之前,都需要明确应用场景、功能需求和非功能需求。

2.1 应用场景

分布式环境下可承载百台服务器产生的日志,单条数据日志小于1k,最大不超过50k,日志总大小每天小于500G。

2.2 功能需求

1)集中收集所有服务日志。

2)可区分来源,按服务、模块和天粒度切分。

2.3 非功能需求

1)不侵入服务进程,收集日志功能需独立部署,占用系统资源可控。

2)实时性,低延迟,从产生日志到集中存储延迟小于4s。

3)持久化,保留最近N天。

4)尽量递送日志即可,不要求不丢不重,但比例应该不超过一个阈值(例如万分之一)。

4)可以容忍不严格有序。

5)收集服务属于线下离线功能,可用性要求不高,全年满足3个9即可。

3 实现架构

一种方案实现的架构如下图所示:

3.1 Producer层分析

PaaS平台内的服务假设部署在Docker容器内,那么为了满足非功能需求#1,独立另外一个进程负责收集日志,因此不侵入服务框架和进程。采用Flume NG来进行日志的收集,这个开源的组件非常强大,可以看做一种监控、生产增量,并且可以发布、消费的模型,Source就是源,是增量源,Channel是缓冲通道,这里使用内存队列缓冲区,Sink就是槽,是个消费的地方。容器内的Source就是执行tail -F这个命令的去利用linux的标准输出读取增量日志,Sink是一个Kafka的实现,用于推送消息到分布式消息中间件。

3.2 Broker层分析

PaaS平台内的多个容器,会存在多个Flume NG的客户端去推送消息到Kafka消息中间件。Kafka是一个吞吐量、性能非常高的消息中间件,采用单个分区按照顺序的写入的方式工作,并且支持按照offset偏移量随机读取的特性,因此非常适合做topic发布订阅模型的实现。这里图中有多个Kafka,是因为支持集群特性,容器内的Flume NG客户端可以连接若干个Kafka的broker发布日志,也可以理解为连接若干个topic下的分区,这样可以实现高吞吐,一来可以在Flume NG内部做打包批量发送来减轻QPS压力,二来可以分散到多个分区写入,同时Kafka还会指定replica备份个数,保证写入某个master后还需要写入N个备份,这里设置为2,没有采用常用的分布式系统的3,是因为尽量保证高并发特性,满足非功能需求中的#4。

3.3 Consumer层分析

消费Kafka增量的也是一个Flume NG,可以看出它的强大之处,在于可以接入任意的数据源,都是可插拔的实现,通过少量配置即可。这里使用Kafka Source订阅topic,收集过来的日志同样先入内存缓冲区,之后使用一个File Sink写入文件,为了满足功能需求#2,可区分来源,按服务、模块和天粒度切分,我自己实现了一个Sink,叫做RollingByTypeAndDayFileSink,源代码放到了github上,可以从:https://github.com/neoremind/flume-byday-file-sink/releases/tag/1.0.0下载jar,直接放到flume的lib目录即可。

4 实践方法

4.1 容器内配置

Dockerfile

Dockerfile是容器内程序的运行脚本,里面会含有不少docker自带的命令,下面是要典型的Dockerfile,BASE_IMAGE是一个包含了运行程序以及flume bin的镜像,比较重要的就是ENTRYPOINT,主要利用supervisord来保证容器内进程的高可用。

FROM ${BASE_IMAGE}
MAINTAINER ${MAINTAINER}
ENV REFRESH_AT ${REFRESH_AT}
RUN mkdir -p /opt/${MODULE_NAME}
ADD ${PACKAGE_NAME} /opt/${MODULE_NAME}/
COPY service.supervisord.conf /etc/supervisord.conf.d/service.supervisord.conf
COPY supervisor-msoa-wrapper.sh /opt/${MODULE_NAME}/supervisor-msoa-wrapper.sh
RUN chmod +x /opt/${MODULE_NAME}/supervisor-msoa-wrapper.sh
RUN chmod +x /opt/${MODULE_NAME}/*.sh
EXPOSE
ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

下面是supervisord的配置文件,执行supervisor-msoa-wrapper.sh脚本。

[program:${MODULE_NAME}]
command=/opt/${MODULE_NAME}/supervisor-msoa-wrapper.sh

下面是supervisor-msoa-wrapper.sh,这个脚本内的start.sh或者stop.sh就是应用程序的启动和停止脚本,这里的背景是我们的启停的脚本都是在后台运行的,因此不会阻塞当前进程,因此直接退出了,Docker就会认为程序结束,因此应用生命周期也结束,这里使用wait命令来进行一个阻塞,这样就可以保证即使后台运行的进程,我们可以看似是前台跑的。

这里加入了flume的运行命令,–conf后面的参数标示会去这个文件夹下面寻找flume-env.sh,里面可以定义JAVA_HOME和JAVA_OPTS。–conf-file指定flume实际的source、channel、sink等的配置。

#! /bin/bash
function shutdown()
{
 date
 echo "Shutting down Service"
 unset SERVICE_PID # Necessary in some cases
 cd /opt/${MODULE_NAME}
 source stop.sh
}

## 停止进程
cd /opt/${MODULE_NAME}
echo "Stopping Service"
source stop.sh

## 启动进程
echo "Starting Service"
source start.sh
export SERVICE_PID=$!

## 启动Flume NG agent,等待4s日志由start.sh生成
sleep 4
nohup /opt/apache-flume-1.6.0-bin/bin/flume-ng agent --conf /opt/apache-flume-1.6.0-bin/conf --conf-file /opt/apache-flume-1.6.0-bin/conf/logback-to-kafka.conf --name a1 -Dflume.root.logger=INFO,console &

# Allow any signal which would kill a process to stop Service
trap shutdown HUP INT QUIT ABRT KILL ALRM TERM TSTP

echo "Waiting for $SERVICE_PID"
wait $SERVICE_PID

Flume配置

source本应该采用exec source,执行tailf -F日志文件即可。但是这里使用了一个自行开发的StaticLinePrefixExecSource,源代码可以在github上找到。之所以采用自定义的,是因为需要将一些固定的信息传递下去,例如服务/模块的名称以及分布式服务所在容器的hostname,便于收集方根据这个标记来区分日志。如果这里你发现为什么不用flume的拦截器interceptor来做这个工作,加入header中一些KV不就OK了吗?这是个小坑,我后续会解释一下。

例如原来日志的一行为:

[INFO] 2016-03-18 12:59:31,080 [main] fountain.runner.CustomConsumerFactoryPostProcessor (CustomConsumerFactoryPostProcessor.java:91) -Start to init IoC container by loading XML bean definitions from classpath:fountain-consumer-stdout.xml

按照如下配置,那么实际传递给Channel的日志为:

service1##$$##m1-ocean-1004.cp [INFO] 2016-03-18 12:59:31,080 [main] fountain.runner.CustomConsumerFactoryPostProcessor (CustomConsumerFactoryPostProcessor.java:91) -Start to init IoC container by loading XML bean definitions from classpath:fountain-consumer-stdout.xml

channel使用内存缓冲队列,大小标识可容乃的日志条数(event size),事务可以控制一次性从source以及一次性给sink的批量日志条数,实际内部有个timeout超时,可通过keepAlive参数设置,超时后仍然会推送过去,默认为3s。

sink采用Kafka sink,配置broker的list列表以及topic的名称,需要ACK与否,以及一次性批量发送的日志大小,默认5条一个包,如果并发很大可以把这个值扩大,加大吞吐。

# Name the components on this agent
a1.sources = r1
a1.sinks = k1
a1.channels = c1

a1.sources.r1.type = com.baidu.unbiz.flume.sink.StaticLinePrefixExecSource
a1.sources.r1.command = tail -F /opt/MODULE_NAME/log/logback.log
a1.sources.r1.channels = c1
a1.sources.r1.prefix=service1
a1.sources.r1.separator=##$$##
a1.sources.r1.suffix=m1-ocean-1004.cp

# Describe the sink
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.topic = keplerlog
a1.sinks.k1.brokerList = gzns-cm-201508c02n01.gzns:9092,gzns-cm-201508c02n02.gzn
s:9092
a1.sinks.k1.requiredAcks = 0
a1.sinks.k1.batchSize = 5

# Use a channel which buffers events in memory
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000000
a1.channels.c1.transactionCapacity = 100

# Bind the source and sink to the channel
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1

4.2 Broker配置

参考Kafka官方的教程,这里新建一个名称叫做keplerlog的topic,备份数量为2,分区为4。

> bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 2 --partitions 4 --topic keplerlog

制造一些增量信息,例如如下脚本,在终端内可以随便输入一些字符串:

> bin/kafka-console-producer.sh --broker-list localhost:9092 --topic keplerlog

打开另外一个终端,订阅topic,确认可以看到producer的输入的字符串即可,即表示联通了。

> bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic keplerlog --from-beginning

4.3 集中接收日志配置

Flume配置

首先source采用flume官方提供的KafkaSource,配置好zookeeper的地址,会去找可用的broker list进行日志的订阅接收。channel采用内存缓存队列。sink由于我们的需求是按照服务名称和日期切分日志,而官方提供的默认file roll sink,只能按照时间戳,和时间interval来切分。

# Name the components on this agent
a1.sources = r1
a1.sinks = k1
a1.channels = c1

a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource
a1.sources.r1.zookeeperConnect = localhost:2181
a1.sources.r1.topic = keplerlog
a1.sources.r1.batchSize = 5
a1.sources.r1.groupId = flume-collector
a1.sources.r1.kafka.consumer.timeout.ms = 800

# Describe the sink
a1.sinks.k1.type = com.baidu.unbiz.flume.sink.RollingByTypeAndDayFileSink
a1.sinks.k1.channel = c1
a1.sinks.k1.sink.directory = /home/work/data/kepler-log

# Use a channel which buffers events in memory
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000000
a1.channels.c1.transactionCapacity = 100

# Bind the source and sink to the channel
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1

定制版RollingByTypeAndDayFileSink

源代码见github。RollingByTypeAndDayFileSink使用有两个条件:

1)Event header中必须有timestamp,否则会忽略事件,并且会抛出{@link InputNotSpecifiedException}

2)Event body如果是按照##$$##分隔的,那么把分隔之前的字符串当做模块名称(module name)来处理;如果没有则默认为default文件名。

输出到本地文件,首先要设置一个跟目录,通过sink.directory设置。其次根据条件#2中提取出来的module name作为文件名称前缀,timestamp日志作为文件名称后缀,例如文件名为portal.20150606或者default.20150703。

规整完的一个文件目录形式如下,可以看出汇集了众多服务的日志,并且按照服务名称、时间进行了区分:

~/data/kepler-log$ ls
authorization.20160512
default.20160513
default.20160505
portal.20160512
portal.20160505
portal.20160514

不得不提的两个坑

坑1

回到前两节提到的自定义了一个StaticLinePrefixExecSource来进行添加一些前缀的工作。由于要区分来源的服务/模块名称,并且按照时间来切分,根据官方flume文档,完全可以采用如下的Source拦截器配置。例如i1表示时间戳,i2表示默认的静态变量KV,key=module,value=portal。

a1.sources.r1.interceptors = i2 i1
a1.sources.r1.interceptors.i1.type = timestamp
a1.sources.r1.interceptors.i2.type = static
a1.sources.r1.interceptors.i2.key = module
a1.sources.r1.interceptors.i2.value = portal

但是flume官方默认的KafkaSource(v1.6.0)的实现:

95 while (eventList.size() < batchUpperLimit &&
96 System.currentTimeMillis() < batchEndTime) {
97 iterStatus = hasNext();
98 if (iterStatus) {
99 // get next message
100 MessageAndMetadata<byte[], byte[]> messageAndMetadata = it.next();
101 kafkaMessage = messageAndMetadata.message();
102 kafkaKey = messageAndMetadata.key();
103
104 // Add headers to event (topic, timestamp, and key)
105 headers = new HashMap<String, String>();
106 headers.put(KafkaSourceConstants.TIMESTAMP,
107 String.valueOf(System.currentTimeMillis()));
108 headers.put(KafkaSourceConstants.TOPIC, topic);
109 if (kafkaKey != null) {
110 headers.put(KafkaSourceConstants.KEY, new String(kafkaKey));
111 }
112 if (log.isDebugEnabled()) {
113 log.debug("Message: {}", new String(kafkaMessage));
114 }
115 event = EventBuilder.withBody(kafkaMessage, headers);
116 eventList.add(event);
117 }

可以看出自己重写了Event header中的KV,丢弃了发送过来的header,因为这个坑的存在因此,tailf -F在event body中在前面指定模块/服务名称,然后RollingByTypeAndDayFileSink会按照分隔符切分。否则下游无法能达到KV。

坑2

exec source需要执行tail -F命令来通过标准输出和标准错误一行一行的读取,但是如果把tail -F封装在一个脚本中,脚本中再执行一些管道命令,例如tail -F logback.log | awk ‘{print "portal##$$##"$0}’,那么exec source总是会把最近的输出丢弃掉,导致追加到文件末尾的日志有一些无法总是“姗姗来迟”,除非有新的日志追加,他们才会被“挤”出来。这里可以依靠unbuffer tail来解决,详见链接(感谢denger的评论)。

5 结语

大家有任何疑问的话都可以留言,关注我的主页【点击进入】,了解更多!

从这个分布式服务分散日志的集中收集方法,可以看出利用一些开源组件,可以非常方便的解决我们日常工作中所发现的问题,而这个发现问题和解决问题的能力才是工程师的基本素质要求。对于其不满足需求的,需要具备有钻研精神,知其然还要知其所以然的去做一些ad-hoc工作,才可以更加好的leverage这些组件。

另外,日志的收集只是起点,利用宝贵的数据,后面的使用场景和想象空间都会非常大,例如

1)利用Spark streaming在一个时间窗口内计算日志,做流量控制和访问限制。

2)使用awk脚本、scala语言的高级函数做单机的访问统计分析,或者Hadoop、Spark做大数据的统计分析。

3)除了端口存活和语义监控,利用实时计算处理日志,做ERROR、异常等信息的过滤,实现服务真正的健康保障和预警监控。

4)收集的日志可以通过logstash导入Elastic Search,使用ELK方式做日志查询使用。

Flume+Kafka收集Docker容器内分布式日志应用实践的更多相关文章

  1. elk-filebeat收集docker容器日志

    目录 使用docker搭建elk filebeat安装与配置 docker容器设置 参考文章 首发地址 使用docker搭建elk 1.使用docker-compose文件构建elk.文件如下: ve ...

  2. 利用 ELK 搭建 Docker 容器化应用日志中心

    利用 ELK 搭建 Docker 容器化应用日志中心 概述 应用一旦容器化以后,需要考虑的就是如何采集位于 Docker 容器中的应用程序的打印日志供运维分析.典型的比如SpringBoot应用的日志 ...

  3. 记录一次docker容器内修改my.cnf配置文件max_allowed_packet参数的过程

    1. 问题背景 在一次新版本功能开发完毕,配合测试的过程中,测试反馈某个XxlJob定时任务一直执行失败,在分析了日志之后,找到了报错的原因: Packet for query is too larg ...

  4. Jenkins(Docker容器内)使用宿主机的docker命令

    1.Jenkins镜像 Docker容器内的Jenkins使用容器外宿主机的Docker(即DooD,还有另外的情况就是DioD),google一下有几种说法,但是都没试成功(试过一种就是修改宿主机/ ...

  5. centos:解决docker容器内挂载目录无权限 ls: cannot open directory .: Permission denied

    docker运行一个容器后,将主机中当前目录下的文件夹挂载到容器的文件夹后 进入到docker容器内对应的挂载目录中,运行命令ls后提示: ls: cannot open directory .: P ...

  6. Docker容器内中文乱码

    Docker容器内中文乱码 一.通过Dockerfile解决中文乱码问题 方式二: 二.临时解决 方式二: 三.修改jre/lib/fonts下的字体 CSDN:黑猫_:Dockerfile 创建容器 ...

  7. Docker容器内Mysql大小写敏感方案解决

    Docker容器内Mysql大小写敏感方案解决 一.(lower_case_table_names)参数说明 二.Docker 部署 MySql 并修改为大小写不敏感 2.1直接在Docker启动的时 ...

  8. Docker容器内连接宿主机即CentOS的Mysql服务器

    docker的宿主机是虚拟机下的CentOS 博主最近遇到一种情况,从服务器拷贝了一份数据库在宿主机Mysql服务器上,想要用本地的数据库测试自己的代码正确性,但是项目程序都是靠docker一键部署的 ...

  9. ELK:收集Docker容器日志

    简介 之前写过一篇博客 ELK:日志收集分析平台,介绍了在Centos7系统上部署配置使用ELK的方法,随着容器化时代的到来,容器化部署成为一种很方便的部署方式,收集容器日志也成为刚需.本篇文档从 容 ...

随机推荐

  1. Linux实战型企业运维工程师试题

    1.如何通过Linux配置一个局域网或者IDC机房上网网关,请给出步骤及命令?答:上网网关配置(1)开启内核转发:sed -i 's#net.ipv4.ip_forward = 0#net.ipv4. ...

  2. 基于Netty的四层和七层代理性能方面的一些压力测试

    本文我们主要是想测试和研究几点: 基于Netty写的最简单的转发HTTP请求的程序,四层和七层性能的差异 三种代理线程模型性能的差异,下文会详细解释三种线程模型 池和非池化ByteBuffer性能的差 ...

  3. vuex分模块后,如何获取state的值

    问题:vuex分模块后,一个模块如何拿到其他模块的state值,调其他模块的方法? 思路:1.通过命名空间取值--this.$store.state.car.list // OK 2.通过定义该属性的 ...

  4. 关于Jvm类加载机制,这一篇就够了

    前言 一个月没更新了,这个月发生了太多的事情,导致更新的频率大大降低,不管怎样收拾心情,技术的研究不能落下! jvm作为每个java程序猿必须了解的知识,博主推荐一本书<深入理解Java虚拟机& ...

  5. Mysql索引优化之索引的分类

    Mysql的历史 简单回顾一下Mysql的历史,Mysql 是一个关系型数据库管理系统,由瑞典 Mysql AB 公司开发,目前属于 Oracle 公司.关系型数据库​将数据保存在不同的表中,而不是将 ...

  6. iOS开发系列之性能优化(上)

    本篇主要记录一下我对界面优化上的一些探索.关于时间优化的探索将会在中篇里进行介绍.下篇将主要介绍一些耗电优化.安装包瘦身的探索. ### 1.卡顿原理 要了解卡顿原理,需要对帧缓冲区.垂直同步.CPU ...

  7. <h2>js数组操作大全(pop,push,unshift,splice,shift方法)</h2>

    ---恢复内容开始--- shift:删除原数组第一项,并返回删除元素的值:如果数组为空则返回undefined var a = [1,2,3,4,5]; var b = a.shift(); //a ...

  8. 微信小程序注册流程

    响应公司号召,跟上时代潮流,接下来我将独自开发微信小程序,接下来我介绍下注册流程,后续会补上小程序开发心得. 注册流程 注册之前,需要使用一个邮箱,该邮箱作为登录小程序的账号,这个邮箱不能被微信开放平 ...

  9. django实战仿慕课网在线视频网站(完成了85%以上的功能已上传github:https://github.com/huwei86/mxonline):

    1. 前台功能模块 基本模块:登录 注册 找回密码 / 全局搜索 / 个人中心, 课程功能:课程管理 / 讲师管理 / 授课机构管理  热门推荐 相关课程推荐 用户操作管理:用户收藏 / 课程评论   ...

  10. Oracle数据库---存储过程、存储函数

    --创建存储过程CREATE OR REPLACE PROCEDURE first_procISBEGIN DBMS_OUTPUT.PUT_LINE('我是过程'); DBMS_OUTPUT.PUT_ ...