Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8d36431
Added remote log segments retention functionality based on time and size
satishd Apr 8, 2023
ce82067
Fixed a few log message typos.
satishd May 25, 2023
d12b8b6
Addressed minor reveiw comments
satishd May 31, 2023
e03f3e6
Delete only when the segment is copied to tiered storage
satishd May 31, 2023
05d9945
Addressed review comments
satishd Jun 1, 2023
4c7cc13
Addressed review comments
satishd Jun 5, 2023
aa81e14
Addressed review comments
satishd Jun 12, 2023
c360a5a
Addressed a minor review comments
satishd Jun 19, 2023
e3de851
Addressed reveiw comments
satishd Jun 20, 2023
cc78b46
Addressed review comments
satishd Jun 26, 2023
db51cc1
Addressed review comments
satishd Jul 27, 2023
746f745
Added more checks and respective tests.
satishd Jul 28, 2023
7a0ae8f
A few more cases added to the test
satishd Aug 1, 2023
e659a41
Addressed reveiw comments.
satishd Aug 2, 2023
9f53ed4
Addressed review comments
satishd Aug 4, 2023
c8e5059
Pulled the trunk changes and fixed the conflicts.
satishd Aug 4, 2023
b4749e9
Addressed review comments
satishd Aug 9, 2023
ce97bf8
Addressed reveiw comments
satishd Aug 14, 2023
6917b72
Addressed review comments
satishd Aug 15, 2023
8fbc973
Addressed review comments
satishd Aug 16, 2023
d997a60
Addressed review comments
satishd Aug 16, 2023
be14a6d
Minor cleanup in the test to cover more scenarios.
satishd Aug 17, 2023
7dc99ed
Added a UT for incrementing log-start-offset with remote storage enabled
satishd Aug 17, 2023
3dd0d18
Addressed review comments
satishd Aug 17, 2023
0d57dcf
Addressed review comments
satishd Aug 18, 2023
de5ac76
Addressed review comments
satishd Aug 19, 2023
e2cb247
Addressed review comments
satishd Aug 20, 2023
46c96f4
Fixed setting right start offset while searching for offsets based on…
satishd Aug 20, 2023
3eaf741
Addressed review comments
satishd Aug 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,9 @@

<!-- storage -->
<suppress checks="CyclomaticComplexity"
files="(LogValidator|RemoteLogManagerConfig).java"/>
files="(LogValidator|RemoteLogManagerConfig|RemoteLogManager).java"/>
<suppress checks="NPathComplexity"
files="(LogValidator|RemoteIndexCache).java"/>
files="(LogValidator|RemoteLogManager|RemoteIndexCache).java"/>
<suppress checks="ParameterNumber"
files="(LogAppendInfo|RemoteLogManagerConfig).java"/>

Expand Down
455 changes: 444 additions & 11 deletions core/src/main/java/kafka/log/remote/RemoteLogManager.java

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion core/src/main/scala/kafka/log/LogManager.scala
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,8 @@ class LogManager(logDirs: Seq[File],
val remainingLogs = decNumRemainingLogs(numRemainingLogs, dir.getAbsolutePath)
val currentNumLoaded = logsToLoad.length - remainingLogs
log match {
case Some(loadedLog) => info(s"Completed load of $loadedLog with ${loadedLog.numberOfSegments} segments in ${logLoadDurationMs}ms " +
case Some(loadedLog) => info(s"Completed load of $loadedLog with ${loadedLog.numberOfSegments} segments, " +
s"local-log-start-offset ${loadedLog.localLogStartOffset()} and log-end-offset ${loadedLog.logEndOffset} in ${logLoadDurationMs}ms " +
s"($currentNumLoaded/${logsToLoad.length} completed in $logDirAbsolutePath)")
case None => info(s"Error while loading logs in $logDir in ${logLoadDurationMs}ms ($currentNumLoaded/${logsToLoad.length} completed in $logDirAbsolutePath)")
}
Expand Down
153 changes: 117 additions & 36 deletions core/src/main/scala/kafka/log/UnifiedLog.scala
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,27 @@ class UnifiedLog(@volatile var logStartOffset: Long,

def localLogStartOffset(): Long = _localLogStartOffset

// This is the offset(inclusive) until which segments are copied to the remote storage.
@volatile private var highestOffsetInRemoteStorage: Long = -1L

locally {
def updateLocalLogStartOffset(offset: Long): Unit = {
_localLogStartOffset = offset

if (highWatermark < offset) {
updateHighWatermark(offset)
}

if (this.recoveryPoint < offset) {
localLog.updateRecoveryPoint(offset)
}
}

initializePartitionMetadata()
updateLogStartOffset(logStartOffset)
updateLocalLogStartOffset(math.max(logStartOffset, localLog.segments.firstSegmentBaseOffset.getOrElse(0L)))
Comment thread
satishd marked this conversation as resolved.
Outdated
if (!remoteLogEnabled())
logStartOffset = localLogStartOffset()
Comment thread
satishd marked this conversation as resolved.
maybeIncrementFirstUnstableOffset()
Comment thread
satishd marked this conversation as resolved.
Outdated
initializeTopicId()

Expand All @@ -162,6 +178,14 @@ class UnifiedLog(@volatile var logStartOffset: Long,
logOffsetsListener = listener
}

def updateLogStartOffsetFromRemoteTier(remoteLogStartOffset: Long): Unit = {
if (!remoteLogEnabled()) {
error("Ignoring the call as the remote log storage is disabled")
return;
}
maybeIncrementLogStartOffset(remoteLogStartOffset, LogStartOffsetIncrementReason.SegmentDeletion)
}

def remoteLogEnabled(): Boolean = {
// Remote log is enabled only for non-compact and non-internal topics
remoteStorageSystemEnable &&
Expand Down Expand Up @@ -520,6 +544,7 @@ class UnifiedLog(@volatile var logStartOffset: Long,
localLog.updateRecoveryPoint(offset)
}
}

def updateHighestOffsetInRemoteStorage(offset: Long): Unit = {
if (!remoteLogEnabled())
warn(s"Unable to update the highest offset in remote storage with offset $offset since remote storage is not enabled. The existing highest offset is $highestOffsetInRemoteStorage.")
Expand Down Expand Up @@ -956,6 +981,15 @@ class UnifiedLog(@volatile var logStartOffset: Long,
}
}

private def maybeIncrementLocalLogStartOffset(newLocalLogStartOffset: Long, reason: LogStartOffsetIncrementReason): Unit = {
lock synchronized {
if (newLocalLogStartOffset > localLogStartOffset()) {
_localLogStartOffset = newLocalLogStartOffset
info(s"Incremented local log start offset to ${localLogStartOffset()} due to reason $reason")
}
}
}

/**
* Increment the log start offset if the provided offset is larger.
*
Expand All @@ -966,7 +1000,8 @@ class UnifiedLog(@volatile var logStartOffset: Long,
* @throws OffsetOutOfRangeException if the log start offset is greater than the high watermark
* @return true if the log start offset was updated; otherwise false
*/
def maybeIncrementLogStartOffset(newLogStartOffset: Long, reason: LogStartOffsetIncrementReason): Boolean = {
def maybeIncrementLogStartOffset(newLogStartOffset: Long,
reason: LogStartOffsetIncrementReason): Boolean = {
// We don't have to write the log start offset to log-start-offset-checkpoint immediately.
// The deleteRecordsOffset may be lost only if all in-sync replicas of this broker are shutdown
// in an unclean manner within log.flush.start.offset.checkpoint.interval.ms. The chance of this happening is low.
Expand All @@ -977,11 +1012,15 @@ class UnifiedLog(@volatile var logStartOffset: Long,
throw new OffsetOutOfRangeException(s"Cannot increment the log start offset to $newLogStartOffset of partition $topicPartition " +
s"since it is larger than the high watermark $highWatermark")

if (remoteLogEnabled()) {
// This should be set log-start-offset is set more than the current local-log-start-offset
_localLogStartOffset = math.max(newLogStartOffset, localLogStartOffset())
}

localLog.checkIfMemoryMappedBufferClosed()
if (newLogStartOffset > logStartOffset) {
updatedLogStartOffset = true
updateLogStartOffset(newLogStartOffset)
_localLogStartOffset = newLogStartOffset
info(s"Incremented log start offset to $newLogStartOffset due to $reason")
leaderEpochCache.foreach(_.truncateFromStart(logStartOffset))
producerStateManager.onLogStartOffsetIncremented(newLogStartOffset)
Expand Down Expand Up @@ -1292,33 +1331,36 @@ class UnifiedLog(@volatile var logStartOffset: Long,
latestEpochAsOptional(leaderEpochCache)))
} else {
// We need to search the first segment whose largest timestamp is >= the target timestamp if there is one.
val remoteOffset = if (remoteLogEnabled()) {
if (remoteLogEnabled()) {
if (remoteLogManager.isEmpty) {
throw new KafkaException("RemoteLogManager is empty even though the remote log storage is enabled.")
}
if (recordVersion.value < RecordVersion.V2.value) {
throw new KafkaException("Tiered storage is supported only with versions supporting leader epochs, that means RecordVersion must be >= 2.")
}

remoteLogManager.get.findOffsetByTimestamp(topicPartition, targetTimestamp, logStartOffset, leaderEpochCache.get)
} else Optional.empty()

if (remoteOffset.isPresent) {
remoteOffset.asScala
val remoteOffset = remoteLogManager.get.findOffsetByTimestamp(topicPartition, targetTimestamp, logStartOffset, leaderEpochCache.get)
if (remoteOffset.isPresent) {
remoteOffset.asScala
} else {
// If it is not found in remote log storage, search in the local log storage from local log start offset.
searchOffsetInLocalLog(targetTimestamp, localLogStartOffset())
}
} else {
// If it is not found in remote storage, search in the local storage starting with local log start offset.

// Cache to avoid race conditions. `toBuffer` is faster than most alternatives and provides
// constant time access while being safe to use with concurrent collections unlike `toArray`.
val segmentsCopy = logSegments.toBuffer

val targetSeg = segmentsCopy.find(_.largestTimestamp >= targetTimestamp)
targetSeg.flatMap(_.findOffsetByTimestamp(targetTimestamp, _localLogStartOffset))
searchOffsetInLocalLog(targetTimestamp, logStartOffset)
}
}
}
}

private def searchOffsetInLocalLog(targetTimestamp: Long, startOffset: Long): Option[TimestampAndOffset] = {
// Cache to avoid race conditions. `toBuffer` is faster than most alternatives and provides
// constant time access while being safe to use with concurrent collections unlike `toArray`.
val segmentsCopy = logSegments.toBuffer
val targetSeg = segmentsCopy.find(_.largestTimestamp >= targetTimestamp)
targetSeg.flatMap(_.findOffsetByTimestamp(targetTimestamp, startOffset))
}

def legacyFetchOffsetsBefore(timestamp: Long, maxNumOffsets: Int): Seq[Long] = {
// Cache to avoid race conditions. `toBuffer` is faster than most alternatives and provides
// constant time access while being safe to use with concurrent collections unlike `toArray`.
Expand Down Expand Up @@ -1390,7 +1432,13 @@ class UnifiedLog(@volatile var logStartOffset: Long,
private def deleteOldSegments(predicate: (LogSegment, Option[LogSegment]) => Boolean,
reason: SegmentDeletionReason): Int = {
def shouldDelete(segment: LogSegment, nextSegmentOpt: Option[LogSegment]): Boolean = {
highWatermark >= nextSegmentOpt.map(_.baseOffset).getOrElse(localLog.logEndOffset) &&
val upperBoundOffset = nextSegmentOpt.map(_.baseOffset).getOrElse(localLog.logEndOffset)

// Check not to delete segments which are not yet copied to tiered storage if remote log is enabled.
(!remoteLogEnabled() || (upperBoundOffset > 0 && upperBoundOffset - 1 <= highestOffsetInRemoteStorage)) &&
// We don't delete segments with offsets at or beyond the high watermark to ensure that the log start
// offset can never exceed it.
highWatermark >= upperBoundOffset &&
predicate(segment, nextSegmentOpt)
}
lock synchronized {
Expand All @@ -1402,6 +1450,11 @@ class UnifiedLog(@volatile var logStartOffset: Long,
}
}

private def incrementStartOffset(startOffset: Long, reason: LogStartOffsetIncrementReason): Unit = {
if (remoteLogEnabled()) maybeIncrementLocalLogStartOffset(startOffset, reason)
else maybeIncrementLogStartOffset(startOffset, reason)
}

private def deleteSegments(deletable: Iterable[LogSegment], reason: SegmentDeletionReason): Int = {
maybeHandleIOException(s"Error while deleting segments for $topicPartition in dir ${dir.getParent}") {
val numToDelete = deletable.size
Expand All @@ -1419,7 +1472,7 @@ class UnifiedLog(@volatile var logStartOffset: Long,
// remove the segments for lookups
localLog.removeAndDeleteSegments(segmentsToDelete, asyncDelete = true, reason)
deleteProducerSnapshots(deletable, asyncDelete = true)
maybeIncrementLogStartOffset(localLog.segments.firstSegmentBaseOffset.get, LogStartOffsetIncrementReason.SegmentDeletion)
incrementStartOffset(localLog.segments.firstSegmentBaseOffset.get, LogStartOffsetIncrementReason.SegmentDeletion)
}
numToDelete
}
Expand All @@ -1442,19 +1495,21 @@ class UnifiedLog(@volatile var logStartOffset: Long,
}

private def deleteRetentionMsBreachedSegments(): Int = {
if (config.retentionMs < 0) return 0
val retentionMs = localRetentionMs(config, remoteLogEnabled())
if (retentionMs < 0) return 0
val startMs = time.milliseconds

def shouldDelete(segment: LogSegment, nextSegmentOpt: Option[LogSegment]): Boolean = {
startMs - segment.largestTimestamp > config.retentionMs
startMs - segment.largestTimestamp > retentionMs
}

deleteOldSegments(shouldDelete, RetentionMsBreach(this))
deleteOldSegments(shouldDelete, RetentionMsBreach(this, remoteLogEnabled()))
}

private def deleteRetentionSizeBreachedSegments(): Int = {
if (config.retentionSize < 0 || size < config.retentionSize) return 0
var diff = size - config.retentionSize
val retentionSize: Long = localRetentionSize(config, remoteLogEnabled())
if (retentionSize < 0 || size < retentionSize) return 0
var diff = size - retentionSize
def shouldDelete(segment: LogSegment, nextSegmentOpt: Option[LogSegment]): Boolean = {
if (diff - segment.size >= 0) {
diff -= segment.size
Expand All @@ -1464,15 +1519,15 @@ class UnifiedLog(@volatile var logStartOffset: Long,
}
}

deleteOldSegments(shouldDelete, RetentionSizeBreach(this))
deleteOldSegments(shouldDelete, RetentionSizeBreach(this, remoteLogEnabled()))
}

private def deleteLogStartOffsetBreachedSegments(): Int = {
def shouldDelete(segment: LogSegment, nextSegmentOpt: Option[LogSegment]): Boolean = {
nextSegmentOpt.exists(_.baseOffset <= logStartOffset)
nextSegmentOpt.exists(_.baseOffset <= (if (remoteLogEnabled()) localLogStartOffset() else logStartOffset))
}

deleteOldSegments(shouldDelete, StartOffsetBreach(this))
deleteOldSegments(shouldDelete, StartOffsetBreach(this, remoteLogEnabled()))
}

def isFuture: Boolean = localLog.isFuture
Expand All @@ -1482,6 +1537,11 @@ class UnifiedLog(@volatile var logStartOffset: Long,
*/
def size: Long = localLog.segments.sizeInBytes

/**
* The log size in bytes for all segments that are only in local log but not yet in remote log.
*/
def onlyLocalLogSegmentsSize: Long = UnifiedLog.sizeInBytes(logSegments.filter(_.baseOffset >= highestOffsetInRemoteStorage))

/**
* The offset of the next message that will be appended to the log
*/
Expand Down Expand Up @@ -2173,6 +2233,14 @@ object UnifiedLog extends Logging {
}
}

private[log] def localRetentionMs(config: LogConfig, remoteLogEnabled: Boolean): Long = {
if (remoteLogEnabled) config.remoteLogConfig.localRetentionMs else config.retentionMs
}

private[log] def localRetentionSize(config: LogConfig, remoteLogEnabled: Boolean): Long = {
if (remoteLogEnabled) config.remoteLogConfig.localRetentionBytes else config.retentionSize
}

}

object LogMetricNames {
Expand All @@ -2186,35 +2254,48 @@ object LogMetricNames {
}
}

case class RetentionMsBreach(log: UnifiedLog) extends SegmentDeletionReason {
case class RetentionMsBreach(log: UnifiedLog, remoteLogEnabled: Boolean) extends SegmentDeletionReason {
override def logReason(toDelete: List[LogSegment]): Unit = {
val retentionMs = log.config.retentionMs
val retentionMs = UnifiedLog.localRetentionMs(log.config, remoteLogEnabled)
toDelete.foreach { segment =>
segment.largestRecordTimestamp match {
case Some(_) =>
log.info(s"Deleting segment $segment due to retention time ${retentionMs}ms breach based on the largest " +
s"record timestamp in the segment")
if (remoteLogEnabled)
log.info(s"Deleting segment $segment due to local log retention time ${retentionMs}ms breach based on the largest " +
s"record timestamp in the segment")
else
log.info(s"Deleting segment $segment due to log retention time ${retentionMs}ms breach based on the largest " +
s"record timestamp in the segment")
case None =>
log.info(s"Deleting segment $segment due to retention time ${retentionMs}ms breach based on the " +
s"last modified time of the segment")
if (remoteLogEnabled)
log.info(s"Deleting segment $segment due to local log retention time ${retentionMs}ms breach based on the " +
s"last modified time of the segment")
else
log.info(s"Deleting segment $segment due to log retention time ${retentionMs}ms breach based on the " +
s"last modified time of the segment")
}
}
}
}

case class RetentionSizeBreach(log: UnifiedLog) extends SegmentDeletionReason {
case class RetentionSizeBreach(log: UnifiedLog, remoteLogEnabled: Boolean) extends SegmentDeletionReason {
override def logReason(toDelete: List[LogSegment]): Unit = {
var size = log.size
toDelete.foreach { segment =>
size -= segment.size
log.info(s"Deleting segment $segment due to retention size ${log.config.retentionSize} breach. Log size " +
if (remoteLogEnabled) log.info(s"Deleting segment $segment due to local log retention size ${UnifiedLog.localRetentionSize(log.config, remoteLogEnabled)} breach. " +
s"Local log size after deletion will be $size.")
else log.info(s"Deleting segment $segment due to log retention size ${log.config.retentionSize} breach. Log size " +
s"after deletion will be $size.")
}
}
}

case class StartOffsetBreach(log: UnifiedLog) extends SegmentDeletionReason {
case class StartOffsetBreach(log: UnifiedLog, remoteLogEnabled: Boolean) extends SegmentDeletionReason {
override def logReason(toDelete: List[LogSegment]): Unit = {
log.info(s"Deleting segments due to log start offset ${log.logStartOffset} breach: ${toDelete.mkString(",")}")
if (remoteLogEnabled)
log.info(s"Deleting segments due to local log start offset ${log.localLogStartOffset()} breach: ${toDelete.mkString(",")}")
else
log.info(s"Deleting segments due to log start offset ${log.logStartOffset} breach: ${toDelete.mkString(",")}")
}
}
8 changes: 7 additions & 1 deletion core/src/main/scala/kafka/server/BrokerServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,13 @@ class BrokerServer(
}

Some(new RemoteLogManager(config.remoteLogManagerConfig, config.brokerId, config.logDirs.head, clusterId, time,
(tp: TopicPartition) => logManager.getLog(tp).asJava, brokerTopicStats));
(tp: TopicPartition) => logManager.getLog(tp).asJava,
(tp: TopicPartition, remoteLogStartOffset: java.lang.Long) => {
logManager.getLog(tp).foreach { log =>
log.updateLogStartOffsetFromRemoteTier(remoteLogStartOffset)
}
},
brokerTopicStats))
} else {
None
}
Expand Down
8 changes: 7 additions & 1 deletion core/src/main/scala/kafka/server/KafkaServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,13 @@ class KafkaServer(
}

Some(new RemoteLogManager(config.remoteLogManagerConfig, config.brokerId, config.logDirs.head, clusterId, time,
(tp: TopicPartition) => logManager.getLog(tp).asJava, brokerTopicStats));
(tp: TopicPartition) => logManager.getLog(tp).asJava,
(tp: TopicPartition, remoteLogStartOffset: java.lang.Long) => {
logManager.getLog(tp).foreach { log =>
log.updateLogStartOffsetFromRemoteTier(remoteLogStartOffset)
}
},
brokerTopicStats));
} else {
None
}
Expand Down
Loading