Kafka日志压缩剖析
1.概述
最近有些同学在学习Kafka时,问到Kafka的日志压缩(Log Compaction)问题,对于Kafka的日志压缩有些疑惑,今天笔者就为大家来剖析一下Kafka的日志压缩的相关内容。
2.内容
2.1 日志压缩是什么?
Kafka是一个基于Log的流处理系统,一个Topic可以有若干个Partition,Partition是复制的基本单元,在一个Broker节点上,一个Partition的数据文件可以存储在若干个独立磁盘目录中,每个Partition的日志文件存储的时候又会被分成一个个的Segment,默认的Segment的大小是1GB,有属性offsets.topic.segment.bytes来控制。Segment是日志清理的基本单元,当前正在使用的Segment是不会被清理的,对于每一个Partition的日志,以Segment为单位,都会被分为两部分,已清理和未清理的部分。同时,未清理的那部分又分为可以清理和不可清理。日志压缩是Kafka的一种机制,可以提供较为细粒度的记录保留,而不是基于粗粒度的基于时间的保留。
Kafka中的每一条数据都包含Key和Value,数据存储在磁盘上,一般不会永久保留,而是在数据达到一定的量或者时间后,对最早写入的数据进行删除。日志压缩在默认的删除规则之外提供了另一种删除过时数据(或者说是保留有价值的数据)的方式,就是对于具有相同的Key,而数据不同,值保留最后一条数据,前面的数据在合适的情况下删除。
2.2 日志压缩的应用场景
日志压缩特性,就实时计算来说,可以在异常容灾方面有很好的应用途径。比如,我们在Spark、Flink中做实时计算时,需要长期在内存里面维护一些数据,这些数据可能是通过聚合了一天或者一周的日志得到的,这些数据一旦由于异常因素(内存、网络、磁盘等)崩溃了,从头开始计算需要很长的时间。一个比较有效可行的方式就是定时将内存里的数据备份到外部存储介质中,当崩溃出现时,再从外部存储介质中恢复并继续计算。
使用日志压缩来替代这些外部存储有哪些优势及好处呢?这里为大家列举并总结了几点:
- Kafka即是数据源又是存储工具,可以简化技术栈,降低维护成本
- 使用外部存储介质的话,需要将存储的Key记录下来,恢复的时候再使用这些Key将数据取回,实现起来有一定的工程难度和复杂度。使用Kafka的日志压缩特性,只需要把数据写进Kafka,等异常出现恢复任务时再读回到内存就可以了
- Kafka对于磁盘的读写做了大量的优化工作,比如磁盘顺序读写。相对于外部存储介质没有索引查询等工作量的负担,可以实现高性能。同时,Kafka的日志压缩机制可以充分利用廉价的磁盘,不用依赖昂贵的内存来处理,在性能相似的情况下,实现非常高的性价比(这个观点仅仅针对于异常处理和容灾的场景来说)
2.3 日志压缩方式的实现细节
当Topic中的cleanup.policy(默认为delete)设置为compact时,Kafka的后台线程会定时将Topic遍历两次,第一次将每个Key的哈希值最后一次出现的offset记录下来,第二次检查每个offset对应的Key是否在较为后面的日志中出现过,如果出现了就删除对应的日志。
日志压缩是允许删除的,这个删除标记将导致删除任何先前带有该Key的消息,但是删除标记的特殊之处在于,它们将在一段时间后从日志中清理,以释放空间。这些需要注意的是,日志压缩是针对Key的,所以在使用时应注意每个消息的Key值不为NULL。
压缩是在Kafka后台通过定时的重新打开Segment来完成的,Segment的压缩细节如下图所示:

日志压缩可以确保的内容,这里笔者总结了以下几点:
- 任何保持在日志头部以内的使用者都将看到所写的每条消息,这些消息将具有顺序偏移量。可以使用Topic的min.compaction.lag.ms属性来保证消息在被压缩之前必须经过的最短时间。也就是说,它为每个消息在(未压缩)头部停留的时间提供了一个下限。可以使用Topic的max.compaction.lag.ms属性来保证从编写消息到消息符合压缩条件之间的最大延时
- 消息始终保持顺序,压缩永远不会重新排序消息,只是删除一些而已
- 消息的偏移量永远不会改变,它是日志中位置的永久标识符
- 从日志开始的任何使用者将至少看到所有记录的最终状态,按记录的顺序写入。另外,如果使用者在比Topic的log.cleaner.delete.retention.ms短的时间内到达日志的头部,则会看到已删除记录的所有delete标记。保留时间默认是24小时。
2.4 日志压缩核心代码实现
日志压缩的核心实现代码大部分的功能在CleanerThread中,核心实现逻辑在Cleaner中的clean方法中,实现细节如下:
/**
* Clean the given log
*
* @param cleanable The log to be cleaned
*
* @return The first offset not cleaned and the statistics for this round of cleaning
*/
private[log] def clean(cleanable: LogToClean): (Long, CleanerStats) = {
// figure out the timestamp below which it is safe to remove delete tombstones
// this position is defined to be a configurable time beneath the last modified time of the last clean segment
val deleteHorizonMs =
cleanable.log.logSegments(0, cleanable.firstDirtyOffset).lastOption match {
case None => 0L
case Some(seg) => seg.lastModified - cleanable.log.config.deleteRetentionMs
} doClean(cleanable, deleteHorizonMs)
} private[log] def doClean(cleanable: LogToClean, deleteHorizonMs: Long): (Long, CleanerStats) = {
info("Beginning cleaning of log %s.".format(cleanable.log.name)) val log = cleanable.log
val stats = new CleanerStats() // build the offset map
info("Building offset map for %s...".format(cleanable.log.name))
val upperBoundOffset = cleanable.firstUncleanableOffset
buildOffsetMap(log, cleanable.firstDirtyOffset, upperBoundOffset, offsetMap, stats)
val endOffset = offsetMap.latestOffset + 1
stats.indexDone() // determine the timestamp up to which the log will be cleaned
// this is the lower of the last active segment and the compaction lag
val cleanableHorizonMs = log.logSegments(0, cleanable.firstUncleanableOffset).lastOption.map(_.lastModified).getOrElse(0L) // group the segments and clean the groups
info("Cleaning log %s (cleaning prior to %s, discarding tombstones prior to %s)...".format(log.name, new Date(cleanableHorizonMs), new Date(deleteHorizonMs)))
val transactionMetadata = new CleanedTransactionMetadata val groupedSegments = groupSegmentsBySize(log.logSegments(0, endOffset), log.config.segmentSize,
log.config.maxIndexSize, cleanable.firstUncleanableOffset)
for (group <- groupedSegments)
cleanSegments(log, group, offsetMap, deleteHorizonMs, stats, transactionMetadata) // record buffer utilization
stats.bufferUtilization = offsetMap.utilization stats.allDone() (endOffset, stats)
}
日志压缩通过两次遍历所有的数据来实现,两次遍历之间交流的通道就是一个OffsetMap,下面是OffsetMap的内容:
trait OffsetMap {
def slots: Int
def put(key: ByteBuffer, offset: Long): Unit
def get(key: ByteBuffer): Long
def updateLatestOffset(offset: Long): Unit
def clear(): Unit
def size: Int
def utilization: Double = size.toDouble / slots
def latestOffset: Long
}
这基本就是一个普通的MuTable Map,在Kafka代码中,它的实现只有一个叫做SkimpyOffsetMap
2.4.1 PUT方法
PUT方法会为每个Key生成一份信息,默认使用MD5方法生成一个Byte,根据这个信息在Byte中哈希的到一个下标,如果这个下标已经被别的占用,则线性查找到下个空余的下标为止,然后对应位置插入该Key的Offset,实现代码如下:
/**
* Associate this offset to the given key.
* @param key The key
* @param offset The offset
*/
override def put(key: ByteBuffer, offset: Long): Unit = {
require(entries < slots, "Attempt to add a new entry to a full offset map.")
lookups += 1
hashInto(key, hash1)
// probe until we find the first empty slot
var attempt = 0
var pos = positionOf(hash1, attempt)
while(!isEmpty(pos)) {
bytes.position(pos)
bytes.get(hash2)
if(Arrays.equals(hash1, hash2)) {
// we found an existing entry, overwrite it and return (size does not change)
bytes.putLong(offset)
lastOffset = offset
return
}
attempt += 1
pos = positionOf(hash1, attempt)
}
// found an empty slot, update it--size grows by 1
bytes.position(pos)
bytes.put(hash1)
bytes.putLong(offset)
lastOffset = offset
entries += 1
}
2.4.2 GET方法
GET方法使用和PUT同样的算法获取Key的信息,通过信息获得Offset的存储位置,实现代码如下:
/**
* Get the offset associated with this key.
* @param key The key
* @return The offset associated with this key or -1 if the key is not found
*/
override def get(key: ByteBuffer): Long = {
lookups += 1
hashInto(key, hash1)
// search for the hash of this key by repeated probing until we find the hash we are looking for or we find an empty slot
var attempt = 0
var pos = 0
//we need to guard against attempt integer overflow if the map is full
//limit attempt to number of slots once positionOf(..) enters linear search mode
val maxAttempts = slots + hashSize - 4
do {
if(attempt >= maxAttempts)
return -1L
pos = positionOf(hash1, attempt)
bytes.position(pos)
if(isEmpty(pos))
return -1L
bytes.get(hash2)
attempt += 1
} while(!Arrays.equals(hash1, hash2))
bytes.getLong()
}
3.配置实践注意事项
默认情况下,启动日志清理器,若需要启动特定Topic的日志清理,请添加特定的属性。配置日志清理器,这里为大家总结了以下几点:
- log.cleanup.policy设置为compact,该策略属性是在Broker中配置,它会影响到集群中所有的Topic。
- log.cleaner.min.compaction.lag.ms这个属性用来防止对更新超过最小消息进行压缩,如果没有设置,除最后一个Segment之外,所有Segment都有资格进行压缩
- log.cleaner.max.compaction.lag.ms这个可以用来防止低生产速率的日志在无限制的时间内不适合压缩
4.总结
Kafka的日志压缩原理并不复杂,就是定时把所有的日志读取两遍,写一遍,而CPU的速度超过磁盘完全不是问题,只要日志的量对应的读取两遍和写入一遍的时间在可接受的范围内,那么它的性能就是可以接受的。
另外,笔者开源的一款Kafka监控关系系统Kafka-Eagle,喜欢的同学可以Star一下,进行关注。
Kafka Eagle源代码地址:https://github.com/smartloli/kafka-eagle
5.结束语
这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!
另外,博主出书了《Kafka并不难学》和《Hadoop大数据挖掘从入门到进阶实战》,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。
Kafka日志压缩剖析的更多相关文章
- Apache Kafka 源码剖析
Getting Start 下载 http://kafka.apache.org/ 优点和应用场景 Kafka消息驱动,符合发布-订阅模式,优点和应用范围都共通 发布-订阅模式优点 解耦合 : 两个应 ...
- Kafka日志清除策略
一.更改日志输出级别 config/log4j.properties中日志的级别设置的是TRACE,在长时间运行过程中产生的日志大小吓人,所以如果没有特殊需求,强烈建议将其更改成INFO级别.具体修改 ...
- kafka 日志策略
日志查看: usr/local/kafka/kafka_2.11-2.4.0/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /t ...
- Sqlserver2008日志压缩
SqlServer2008日志压缩语句如下: USE [master] GO ALTER DATABASE DBName SET RECOVERY SIMPLE WITH NO_WAIT GO ALT ...
- lagstash + elasticsearch + kibana 3 + kafka 日志管理系统部署 02
因公司数据安全和分析的需要,故调研了一下 GlusterFS + lagstash + elasticsearch + kibana 3 + redis 整合在一起的日志管理应用: 安装,配置过程,使 ...
- 我是如何利用Hadoop做大规模日志压缩的
背景 刚毕业那几年有幸进入了当时非常热门的某社交网站,在数据平台部从事大数据开发相关的工作.从日志收集.存储.数据仓库建设.数据统计.数据展示都接触了一遍,比较早的赶上了大数据热这波浪潮.虽然今天的人 ...
- 关于Kafka日志留存策略的讨论
关于Kafka日志留存(log retention)策略的介绍,网上已有很多文章.不过目前其策略已然发生了一些变化,故本文针对较新版本的Kafka做一次统一的讨论.如果没有显式说明,本文一律以Kafk ...
- Shell + crontab 实现日志压缩归档
Shell + crontab 实现日志压缩归档 crontab # archive the ats log days. */ * * * * root /bin/>& shell #! ...
- kafka 日志结构
1.kafka日志结构 直接举例子: 例如kafka有个名字叫 haha 的topic,那么kafka日志下面有kafka-0,kafka-1,kafka-2...,kafka-n,具体多少个,创建分 ...
随机推荐
- H5 移动端获取当前位置
3种方法:1.H5自带的方法,获取经纬度2.通过地图提供的JS.获取位置3.通过微信的API(这个需要公众号 / 小程序) 1.通过H5自带的获取经纬度的方法 优点: 需要引用的资源较少,H5自带的方 ...
- 利用SpEL 表达式实现简单的动态分表查询
这里的动态分表查询并不是动态构造sql语句,而是利用SpEL操作同一结构的不同张表. 也可以参考Spring Data Jpa中的章节http://docs.spring.io/spring-data ...
- Spring boot+JPA+Druid
pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ...
- js基础——流程控制语句
1.if语句 if(条件表达式){ 若条件成立只执行我,下面的分支不执行 }else if(条件表达式){ 语句; }else if(条件表达式){ 语句; }else{ ...
- easyui—element-ui框架套用(表格宽度自适应)
外层使用easyui框架中window组件,便于使用最大化功能:内部表格使用element-ui在的el-table,el-table列宽须设置为最小宽度才能在最大化窗口时列表中列宽自适应window ...
- P1081 弹珠游戏
题目出处 灵灵和他的小伙伴聪聪发掘了一个骨灰级别的游戏--超级弹珠. 游戏的内容是:在一个 n*n 的矩阵里,有若干个敌人,你的弹珠可以摧毁敌人,但只能攻击你所在的行.列里的所有敌人,然后你就可以获得 ...
- PHP PDO扩展整理,包括环境配置\基本增删改查\事务\预处理
相关文章:PHP的mysql扩展整理,操作数据库的实现过程分析 PHPmysqli扩展整理,包括面向过程和面向对象的比较\事务控制\批量执行\预处理 介绍 PDO是一种PHP程序连接数据库的接口 ...
- jquery的offset().top和js的offsetTop的区别,以及jquery的offset().top的实现方法
jquery的offset().top和js的offsetTop的区别,以及jquery的offset().top的实现方法 offset().top是JQ的方法,需要引入JQ才能使用,它获取的是你绑 ...
- vue权限篇
前言 在一个项目中,一些功能会涉及到重要的数据管理,为了确保数据的安全,我们会在项目中加入权限来限制每个用户的操作.作为前端,我们要做的是配合后端给到的权限数据,做页面上的各种各样的限制. 需求 因为 ...
- 【u034】追查坏奶牛
Time Limit: 1 second Memory Limit: 128 MB [问题描述] 你第一天接手三鹿牛奶公司就发生了一件倒霉的事情:公司不小心发送了一批有三聚氰胺的牛奶.很不幸,你发现这 ...