最近这段时间在设计和实现日志系统,在整个日志系统系统中Zookeeper的作用非常重要——它用于协调各个分布式组件并提供必要的配置信息和元数据。这篇文章主要分享一下Zookeeper的使用场景。这里主要涉及到Zookeeper在日志系统中的使用,但其实它在我们的消息总线和搜索模块中也同样非常重要。
日志的类型和日志的字段这里我们统称为日志的元数据。我们构建日志系统的目的最终主要是为了:日志搜索,日志分析。这两大块我们很大程度上依赖于——ElasticSearch(关于什么是ElasticSearch,这里就不多做介绍了)。
日志的字段定义,在ElasticSearch中是一个索引中某个mapping的Schema。ElasticSearch是一个号称<code>Schema Free</code>的分布式全文检索系统。这里需要正确地理解<code>Schema Free</code>,它并不是不需要Schema,也不是强制要求你必须有明确的Schema,有没有都可以做全文检索。但是像聚合、分析等很多高级功能都建立在明确的Schema的基础上。因此,从分析统计的角度看,我们对日志进行明确的字段定义是很有必要的。
日志元数据是日志系统的基础信息,我们在web管控台管理它并同步至Zookeeper供其他模块使用,比如搜索模块。因为上文提到日志的类型、字段其实跟ElasticSearch的<code>Mapping Type</code>是对等的映射关系,所以搜索模块会重度依赖日志元数据。另外,为了保证新的日志元数据(通常是一个新的日志类型被创建)尽快同步至ElasticSearch,我们利用了Zookeeper的事件Push机制(或者叫Pub/Sub机制)来实时获知日志元数据的变化。一旦有新的日志元数据产生,我们的搜索模块会立即得到事件通知,它会获取最新的日志元数据,然后为其在ElasticSearch的indices中新建一个mapping,为这种日志类型的日志存入ElasticSearch做好准备。
这种方式带来了哪些好处,目前来看至少有三点:
实时性:搜索模块能第一时间感知到新的日志类型的创建
低耦合性:管控台上日志模块跟搜索模块,没有因为信息的依赖而产生较强的耦合性;它们通过Zookeeper进行了解耦
Mapping Type的可控性:ElasticSearch有个非常好的特性,就是当你将一个文档存入某个mapping
type,如果该文档中存在mapping未曾定义的字段,ElasticSearch将会为你自动添加该字段的定义。我们认为这种机制将会使日志字段变得不可控。因此我们通过统一日志元数据再加上后面基于同样的解析行为来保证Schema的可控性。
而上面这个命令中,唯一需要变动的只有以下几个部分:
zookeeper的服务器(集群)信息
即将收集的日志的类型的flume路径
即将收集的日志的flume配置的znode名称,如上例是<code>mysql-slowquery-30</code>
其实原先需要手动修改配置文件的部分参数项将在提供的管控台中进行配置,但基于web的表单填写显然要比在服务器上以命令行的方式来得容易得多。
这里我们的做法是拆解了flume的配置文件,将其固定不变的部分做成模板,将其可变部分做成表单。在提交之前,通过模板引擎将模板跟配置信息进行合并为完整的配置并推送到Zookeeper中去。
其中需要配置的部分参数有:
日志文件所在目标服务器的路径位置
日志文件名称的格式
日志是单行模式还是多行模式
消息源的secret(消息总线部分)
消息槽的名称(消息总线部分)
消息流的token(消息总线部分)
….
同样在之前的文章中我也提及我们在日志解析上的选择是morphline。morphline是个在Hadoop生态系统中的ETL
Framework。morphline也有一个配置文件用于定义一系列的Commands。而它也有固定部分和可变部分,因为解析主要应用了morphline的<code>Grok</code>命令,所以针对这个命令,可变部分主要是:
解析字典
解析的正则表达式
我们的做法同样类似于日志采集模块,将morphline的配置文件的固定部分做成固定模板,然后将可变部分在管控台上进行配置,最终合并提交到Zookeeper中。
日志解析服务(归属于下面的<code>后台服务</code>),在启动时会根据自己的日志类型,从Zookeeper的特定节点下找到该日志类型的morphline的配置,将其获取下来并保存在本地文件系统中,然后构建Mrophline对象(因为morphline目前只提供基于File对象的构造方式,所以多了一个先保存至本地文件再基于文件构造Morphline对象的步骤)进行解析。
日志解析这边只是给解析任务提供了 元数据
,真正的解析由后台的解析任务来完成,我们将类似的这些允许在后台的所有任务笼统得归结为 后台服务 。
后台服务遵循:<code>任务组</code>-><code>任务</code>-><code>工作线程</code>的层次性的组织方式。按照服务的业务类型(说白了就是同一套处理逻辑),将其划分为不同的任务组(比如:ETL组、日志索引组等),不同的任务组下会有
至少
一个任务,比如ETL任务组下就会有很多个任务(比如:nginx访问日志解析任务、mysql慢查询日志解析任务等),而某一个任务下又存在至少一个工作线程。
在管控台有一个后台服务模块,专门用于管理任务对象以及它们的元数据。
执行任务的工作者线程本身是无状态的,它们在启动的时候会去Zookeeper上下载它们执行任务所需要的元数据。
通常,为了服务的可用性我们会为每个任务配备不少于一个工作者线程。当然,这里的 不少于一个
并不只是基于一个运行后台服务的JVM进程或一个主机节点来计数的,而是针对由多个节点组成的集群而言。
这通过一个配置来实现:
它的意义是:对每个task而言,启动的最少的worker线程数。如果一个主机节点上启动两个后台服务的JVM进程,那么这个task就会对应4个工作者线程。
正常情况下,每个任务在同一时刻只有一个处于active状态的工作者线程,而其他抢占失败的都会将自己切换为standby模式,作为备援随时待命。
这个机制是如何实现的?这得益于Zookeeper提供的 临时顺序 节点。
当多个工作者线程去竞争一个任务的时候,它们首先去该任务的path下创建一个子path,并注册自己的主机等信息。注意这里创建的子path的类型不同于其他Zookeeper使用场景的path类型(其他path通常都是持久型的),它是临时、顺序的。这两个属性非常重要:
临时: 当一个工作者线程挂掉之后,它本地的Zookeeper会话也会随之失效,在其会话失效之后,临时节点将会消失。
顺序:它能仲裁出创建path的客户端的先后顺序,并在新建的path中追加标识
各个工作者线程创建临时顺序的path后,由于具有 顺序
性,Zookeeper会按照它们创建的顺序在path后追加带有从1开始递增的编号。各个工作者创建完成后会得到各自的编号,然后它们作一个顺序判断,谁是最小的谁就会获得任务的执行机会并成为active工作者,而其他抢占失败的将默认切换到standby模式,这些抢占失败的工作者线程会注册成为它们抢占task的
子节点变更 watcher。这时 临时
属性就派上用场了,当处于active模式的工作者线程丢失会话之后,这些standby将会收到通知,而这时它们会再次去判断自己的编号是不是最小的,如果是那么就可以接替之前的工作者线程成为active的了。这时如果有新加入的工作者线程也会触发变更通知,但这并不会影响正常的逻辑。
当然这里还存在一些问题有待完善:
active线程因为网络延迟出现短时会话丢失的问题,可能会导致Zookeeper错误的判断
子节点频繁变更可能会产生广播风暴
每个任务组都会有一个watcher来监控是否有新的任务被创建(比如一种新的日志类型被提交)。如果有新任务则会在其所属的线程池中新建新的工作者来执行新任务。目前暂时这个watcher默认只关注新增任务,而针对任务被移除或者任务的元数据变更,watcher暂时还没有相应的响应机制。这也是后续需要考虑和完善的部分。
这种机制之前已经分别应用于<code>日志采集</code>、<code>日志解析</code>模块了,在这里也是为了简化后台服务启动时配置的问题。
综上,整个设计的Zookeeper 拓扑图大致如下:
从上面的分析可以看到,我们最大程度地将各种可变的参数配置到Zookeeper中,使其成为串联起整个分布式系统的配置中心,而我们的管控台某种意义上退化成了“配置系统”——将配置界面化、持久化。同时,我们也利用了Zookeeper的实时事件Push机制,来进行分布式协调。
原文发布时间为:2015-12-26
本文作者:vinoYang