【Kafka源码】日志处理
目前来说,kafka的日志中记录的内容比较多,具体的存储内容见这篇博客,写的比较好。可以看到,存储的内容还是比较多的,当存储文件比较大的时候,我们应该如何处理这些日志?下面我们通过kafka启动过程的源码,分析下kafka的日志处理过程。
一、入口方法
在kafkaServer.scala中的start方法中,有一个这样的调用:
/* start log manager */
logManager = createLogManager(zkUtils.zkClient, brokerState)
logManager.startup()
二、定时任务总方法
这块就是启动了日志相关的定时任务,具体都有哪些内容?我们跟进去看一下:
def startup() {
/* Schedule the cleanup task to delete old logs */
if(scheduler != null) {
info("Starting log cleanup with a period of %d ms.".format(retentionCheckMs))
scheduler.schedule("kafka-log-retention",
cleanupLogs,
delay = InitialTaskDelayMs,
period = retentionCheckMs,
TimeUnit.MILLISECONDS)
info("Starting log flusher with a default period of %d ms.".format(flushCheckMs))
scheduler.schedule("kafka-log-flusher",
flushDirtyLogs,
delay = InitialTaskDelayMs,
period = flushCheckMs,
TimeUnit.MILLISECONDS)
scheduler.schedule("kafka-recovery-point-checkpoint",
checkpointRecoveryPointOffsets,
delay = InitialTaskDelayMs,
period = flushCheckpointMs,
TimeUnit.MILLISECONDS)
}
if(cleanerConfig.enableCleaner)
cleaner.startup()
}
可以看到,这块主要使用了一个定时任务线程池,来处理任务的定时执行。具体包括两块,一部分是清理日志,另一部分是将日志写入文件。
2.1 清理日志
首先是cleanupLogs,这块涉及到配置,log.retention.check.interval.ms,也就是多长时间执行一次日志清理。我们看下具体的方法:
/**
* Delete any eligible logs. Return the number of segments deleted.
*/
def cleanupLogs() {
debug("Beginning log cleanup...")
var total = 0
val startMs = time.milliseconds
for(log <- allLogs; if !log.config.compact) {
debug("Garbage collecting '" + log.name + "'")
total += cleanupExpiredSegments(log) + cleanupSegmentsToMaintainSize(log)
}
debug("Log cleanup completed. " + total + " files deleted in " +
(time.milliseconds - startMs) / 1000 + " seconds")
}
这块还涉及到另一个配置:cleanup.policy,也就是清理的策略,目前有几种,一种是compact,也就是日志压缩,不会清理掉日志文件;还有一种就是delete,也就是删除。这块主要有两个方法,我们分别看下:
2.1.1 清理过期日志
/**
* Runs through the log removing segments older than a certain age
*/
private def cleanupExpiredSegments(log: Log): Int = {
if (log.config.retentionMs < 0)
return 0
val startMs = time.milliseconds
log.deleteOldSegments(startMs - _.lastModified > log.config.retentionMs)
}
这块又涉及到一个配置:retention.ms,这个参数表示日志保存的时间。如果小于0,表示永不失效,也就没有了删除这一说。
当然,如果文件的修改时间跟当前时间差,大于设置的日志保存时间,就要执行删除动作了。具体的删除方法为:
/**
* Delete any log segments matching the given predicate function,
* starting with the oldest segment and moving forward until a segment doesn't match.
* @param predicate A function that takes in a single log segment and returns true iff it is deletable
* @return The number of segments deleted
*/
def deleteOldSegments(predicate: LogSegment => Boolean): Int = {
lock synchronized {
//find any segments that match the user-supplied predicate UNLESS it is the final segment
//and it is empty (since we would just end up re-creating it)
val lastEntry = segments.lastEntry
val deletable =
if (lastEntry == null) Seq.empty
else logSegments.takeWhile(s => predicate(s) && (s.baseOffset != lastEntry.getValue.baseOffset || s.size > 0))
val numToDelete = deletable.size
if (numToDelete > 0) {
// we must always have at least one segment, so if we are going to delete all the segments, create a new one first
if (segments.size == numToDelete)
roll()
// remove the segments for lookups
deletable.foreach(deleteSegment(_))
}
numToDelete
}
}
这块的逻辑是:根据传入的predicate来判断哪些日志符合被删除的要求,放入到deletable中,最后遍历deletable,进行删除操作。
private def deleteSegment(segment: LogSegment) {
info("Scheduling log segment %d for log %s for deletion.".format(segment.baseOffset, name))
lock synchronized {
segments.remove(segment.baseOffset)
asyncDeleteSegment(segment)
}
}
private def asyncDeleteSegment(segment: LogSegment) {
segment.changeFileSuffixes("", Log.DeletedFileSuffix)
def deleteSeg() {
info("Deleting segment %d from log %s.".format(segment.baseOffset, name))
segment.delete()
}
scheduler.schedule("delete-file", deleteSeg, delay = config.fileDeleteDelayMs)
}
这块是一个异步删除文件的过程,包含一个配置:file.delete.delay.ms。表示每隔多久删除一次日志文件。删除的过程是先把日志的后缀改为.delete,然后定时删除。
2.1.2 清理过大日志
/**
* Runs through the log removing segments until the size of the log
* is at least logRetentionSize bytes in size
*/
private def cleanupSegmentsToMaintainSize(log: Log): Int = {
if(log.config.retentionSize < 0 || log.size < log.config.retentionSize)
return 0
var diff = log.size - log.config.retentionSize
def shouldDelete(segment: LogSegment) = {
if(diff - segment.size >= 0) {
diff -= segment.size
true
} else {
false
}
}
log.deleteOldSegments(shouldDelete)
}
这块代码比较清晰,如果日志大小大于retention.bytes,那么就会被标记为待删除,然后调用的方法是一样的,也是deleteOldSegments。就不赘述了。
2.2 日志刷到硬盘
这块有两个定时任务。
scheduler.schedule("kafka-log-flusher",
flushDirtyLogs,
delay = InitialTaskDelayMs,
period = flushCheckMs,
TimeUnit.MILLISECONDS)
scheduler.schedule("kafka-recovery-point-checkpoint",
checkpointRecoveryPointOffsets,
delay = InitialTaskDelayMs,
period = flushCheckpointMs,
TimeUnit.MILLISECONDS)
涉及到两个配置:
- log.flush.scheduler.interval.ms:检查是否需要固化到硬盘的时间间隔
- log.flush.offset.checkpoint.interval.ms:控制上次固化硬盘的时间点,以便于数据恢复一般不需要去修改
我们分别看下两个任务做了啥。
2.2.1 flushDirtyLogs
/**
* Flush any log which has exceeded its flush interval and has unwritten messages.
*/
private def flushDirtyLogs() = {
debug("Checking for dirty logs to flush...")
for ((topicAndPartition, log) <- logs) {
try {
val timeSinceLastFlush = time.milliseconds - log.lastFlushTime
debug("Checking if flush is needed on " + topicAndPartition.topic + " flush interval " + log.config.flushMs +
" last flushed " + log.lastFlushTime + " time since last flush: " + timeSinceLastFlush)
if(timeSinceLastFlush >= log.config.flushMs)
log.flush
} catch {
case e: Throwable =>
error("Error flushing topic " + topicAndPartition.topic, e)
}
}
}
这个方法的目的是把日志刷新到硬盘中,保证数据不丢。
这块设计到一个配置:flush.ms。当日志的刷新时间与当前时间差,大于配置的值时,就会执行flush操作。
/**
* Flush all log segments
*/
def flush(): Unit = flush(this.logEndOffset)
/**
* Flush log segments for all offsets up to offset-1
* @param offset The offset to flush up to (non-inclusive); the new recovery point
*/
def flush(offset: Long) : Unit = {
if (offset <= this.recoveryPoint)
return
debug("Flushing log '" + name + " up to offset " + offset + ", last flushed: " + lastFlushTime + " current time: " +
time.milliseconds + " unflushed = " + unflushedMessages)
for(segment <- logSegments(this.recoveryPoint, offset))
segment.flush()
lock synchronized {
if(offset > this.recoveryPoint) {
this.recoveryPoint = offset
lastflushedTime.set(time.milliseconds)
}
}
}
/**
* Flush this log segment to disk
*/
@threadsafe
def flush() {
LogFlushStats.logFlushTimer.time {
log.flush()
index.flush()
}
}
找到当前segment的最后一个offset,即logEndOffset,然后调用flush方法,刷新到日志文件中。首先判断,当前offset是否小于recoveryPoint,也就是第一个需要刷新到硬盘的offset,如果小于的话,直接返回,否则继续flush操作。
将日志中从recoveryPoint到offset的所有日志,刷新到日志文件中,调用segment.flush()方法上。刷新log文件和index文件。
2.2.2 checkpointRecoveryPointOffsets
/**
* Write out the current recovery point for all logs to a text file in the log directory
* to avoid recovering the whole log on startup.
*/
def checkpointRecoveryPointOffsets() {
this.logDirs.foreach(checkpointLogsInDir)
}
/**
* Make a checkpoint for all logs in provided directory.
*/
private def checkpointLogsInDir(dir: File): Unit = {
val recoveryPoints = this.logsByDir.get(dir.toString)
if (recoveryPoints.isDefined) {
this.recoveryPointCheckpoints(dir).write(recoveryPoints.get.mapValues(_.recoveryPoint))
}
}
这块主要是用于写一些恢复点的数据到文件中去,文件名是recovery-point-offset-checkpoint,里面的内容是:
- 第一行是当前的版本version
- 第二行是所有偏移量的数字和,每个topic和partition的组合的数量
- 之后会遍历所有的topic和partition组合,每行展示的内容是:topic partition offset
但是这块的写文件不是直接向目标文件写入,而是先写一个临时文件,然后再将临时文件移动到目标文件中。
三、总结
以上就是kafka中日志处理的一些源码,我们总结一下,其中涉及到的配置项有:
- log.retention.check.interval.ms
- cleanup.policy
- retention.ms
- file.delete.delay.ms
- retention.bytes
- log.flush.scheduler.interval.ms
- log.flush.offset.checkpoint.interval.ms
- flush.ms
可能还有其他的一些配置,这块没有涉及到。当然,这些参数如何配置,才能使性能达到最优,也需要不断地进行测试和探索,目前只能依靠默认的参数来进行配置,这显然是不够的。
【Kafka源码】日志处理的更多相关文章
- kafka源码分析之一server启动分析
0. 关键概念 关键概念 Concepts Function Topic 用于划分Message的逻辑概念,一个Topic可以分布在多个Broker上. Partition 是Kafka中横向扩展和一 ...
- Kafka 源码剖析
1.概述 在对Kafka使用层面掌握后,进一步提升分析其源码是极有必要的.纵观Kafka源码工程结构,不算太复杂,代码量也不算大.分析研究其实现细节难度不算太大.今天笔者给大家分析的是其核心处理模块, ...
- Linux Kafka源码环境搭建
本文主要讲述的是如何搭建Kafka的源码环境,主要针对的Linux操作系统下IntelliJ IDEA编译器,其余操作系统或者IDE可以类推. 1.安装和配置JDK确认JDK版本至少为1.7,最好是1 ...
- windows下IntelliJ IDEA搭建kafka源码环境
于kafka核心原理的资料,网上有很多,但是如果不自己研究其源码,永远是知其然而不知所以然.下面就来演示如何在windows环境下来编译kafka源码,并通过IntelliJ IDEA开发工具搭建ka ...
- Kafka源码分析(三) - Server端 - 消息存储
系列文章目录 https://zhuanlan.zhihu.com/p/367683572 目录 系列文章目录 一. 业务模型 1.1 概念梳理 1.2 文件分析 1.2.1 数据目录 1.2.2 . ...
- Kakfa揭秘 Day3 Kafka源码概述
Kakfa揭秘 Day3 Kafka源码概述 今天开始进入Kafka的源码,本次学习基于最新的0.10.0版本进行.由于之前在学习Spark过程中积累了很多的经验和思想,这些在kafka上是通用的. ...
- apache kafka & CDH kafka源码编译
Apache kafka编译 前言 github网站kafka项目的README.md有关于kafka源码编译的说明 github地址:https://github.com/apache/kafka ...
- gradle 编译kafka源码慢
我前提已经在环境变量中将GRADLE_HOME设置到了gradle的目录(在E盘),并且在环境变量里设置了本地仓库GRADLE_USER_HOME. 编译kafka源码时候,很慢很慢.百度了一下,有说 ...
- intellij idea 编译 kafka 源码
1. 从 GitHub 网站,git clone kafka 源码 2. 下载安装好 gradle,scala 3. 进入 kafka 项目目录,依次执行 gradle wrapper,gradle ...
- 使用IntelliJ IDEA搭建kafka源码环境时遇到Output path错误解决办法
kafka源码环境搭建好之后,需要在IntelliJ IDEA开发工具中以debug方式启动kafka服务器来测试消息的生产和消费. 但是在启动kafka.Kafka类中的main方法(也就是运行 k ...
随机推荐
- 引入jQuery
用于测试和开发(未压缩,是可读的代码) uncompressed, 用于实际的网站中,已被精简和压缩. minified jQuery 1.xjQuery Migrate 过渡版jQuery ...
- 装饰者模式及C++实现
装饰者模式 时常会遇到这样一种情况,我已经设计好了一个接口,并且也有几个实现类,但是这时我发现我设计的时候疏忽了,忘记了一些功能,或者后来需求变动要求加入一些功能,最简单的做法就是修改接口,添加函数, ...
- ZOJ3775 ?(>_o)! 2017-04-13 23:37 110人阅读 评论(0) 收藏
?(>_o)! Time Limit: 2 Seconds Memory Limit: 65536 KB ?(>_o)! is a pseudo-object-oriented ...
- ASP.NET Core2集成Office Online Server(OWAS)实现办公文档的在线预览与编辑(支持word\excel\ppt\pdf等格式)
Office Online Server是微软开发的一套基于Office实现在线文档预览编辑的技术框架(支持当前主流的浏览器,且浏览器上无需安装任何插件,支持word.excel.ppt.pdf等文档 ...
- [Erlang32]ibrowse流程及性能测试
1.简介 ibrowse是用erlang写的一个HTTP client.github地址:https://github.com/cmullaparthi/ibrowse 使用方法见项目的readme. ...
- shell脚本参数中有空格
shell脚本参数中有空格 在shell脚本中如果有空格的处理如下: sh test.sh "hello word" echo $1 得到的是hello,而不是hello word ...
- 用canvas画布画一个画板
前段时间,在对H5的回顾中突然对canvas有了感觉,闲来无事便对其进行了一些捯饬.这不,上周我还做了一个好玩的画板呢,废话不多说,直接上代码(PS:翠花,上代码~): HTML部分: <!DO ...
- AutoMapper之如何开始,适合入门和演示
原来想应该介绍下背景说明下好处什么的,仔细想都是废话 ,直接上代码吧. 首先有两个类,一个是和数据库对应的实体 Student,一个是和页面展示相关的页面模型 StudentModel. /// &l ...
- 「NOI2014」魔法森林
题目链接 戳我 \(Solution\) 两个变量,emm...不好搞啊. 于是我们可以按照\(A\)排序.然后动态加边,因为\(A\)是越来越大,所以不需要管他,只要使得\(1\)~\(n\)的路径 ...
- 1、认识Struts2
先上百度百科的权威说明:一定注意我下面做标记的话: 框架就是一个半成品,就是可以帮我们完成一些业务 1. 什么是Struts2的框架 * Struts2是Struts1的下一代产品,是在 struts ...