Table of Contents |
---|
Status
Current state: Under DiscussionAdopted
Vote Discussion thread: here [Change the link from the KIP proposal email archive to your own email thread]
JIRA:
Jira | ||||||
---|---|---|---|---|---|---|
|
Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).
...
Terminology
...
- Kafka Controller: The component that generates snapshots, reads snapshots and reads logs for voter replicas of the topic partition
__cluster_metadata
. - Active Controller: The Kafka Controller that is also a leader of the
__cluster_metadata
topic partition. This component is allowed to append to the log. - Metadata Cache: The component that generates snapshots, reads snapshots and reads logs for observer replicas of the topic partition
__cluster_metadata
. This is needed to reply toMetadata
RPCs, for connection information to all of the brokers, etc. - SnapshotId: Each snapshot is uniquely identified by a
SnapshotId
, the epoch and end offset of the records in the replicated log included in the snapshot.
Motivation
In KIP-500 the Active Controller, which is the quorum leader from KIP-595, will materialize the entries in the metadata log into memory. Each broker in KIP-500, which will be a voter or an observer replica of the metadata log, will materialize the entries in the log into a metadata cache. We want to provide stronger guarantee on the consistency of the metadata than is possible today. If all entries in the metadata log are indefinitely retained, then the design described in KIP-595 achieves a consistent log and as a result a consistent metadata cache. If Kafka doesn’t limit the size of this replicated log it , which is the quorum leader from KIP-595, will materialize the entries in the metadata log into memory. Each broker in KIP-500, which will be a replica of the metadata log, will materialize the entries in the log into a metadata cache. We want to provide stronger guarantee on the consistency of the metadata than is possible today. If all entries in the metadata log are indefinitely retained, then the design described in KIP-595 achieves a consistent log and as a result a consistent metadata cache. If Kafka doesn’t limit the size of this replicated log it can affect Kafka availability by:
...
Kafka uses the cleanup policies compact
and delete
to limit the size of the log. The delete
cleanup policy deletes data for a prefix of the log based on time. The delete
cleanup policy is not viable when used on metadata log because the Kafka controller Controller and metadata cache Metadata Cache will not be able to generate a consistent view from such a log.
...
The type of in-memory state machines what that we plan to implement for the Kafka Controller and metadata cache (see KIP-631) doesn't map very well to an a key and offset based clean up policy. The data model for the Kafka Controller and metadata cache requires us to supports deltas/events. Currently the controller uses ZK for both storing the current state of the cluster and the deltas that it needs to apply. One example of this is topic deletion, when the user asks for a topic to be deleted 1) the Kafka Controller (AdminManager
in the code) persists and commits the delete event, 2) applies the event to its in-memory state and sends RPC to all of the affected brokers, 3) persists and commits the changes to ZK. We don't believe that a key-value and offset based compaction model can be used for events/deltas in a way that it is intuitive to program against.
Loading State and Frequency of Compaction
Another operation that the Kafka Controller models as an event is the FenceBroker
message from KIP-631. Every time a broker is fenced because of controlled shutdown or failure to send a heartbeat it affects every topic partition assigned to the fenced broker. For details on the impact on replication of event operations (FenceBroker
) vs key-value operations (IsrChange
) see the section "Amount of Data Replicated".
Loading State and Frequency of Compaction
When starting a broker either because it is a new broker or it is restarting, loading When starting a broker either because it is a new broker, a broker was upgraded or a failed broker is restarting. Loading the state represented by the __cluster_metadata
and __consumer_offsets
topic partition is required before the broker is available. With snapshot based By taking snapshots of the in-memory state as described below, Kafka can be much more efficient and aggressive on how often it generates the snapshot. By having snapshots in a separate file and not updating the replicated log the snapshot can include up to the high watermark. In the future this mechanism will also be useful for high-throughput topic partitions like the Group Coordinator and Transaction Coordinator.
Amount of Data Replicated
Because the Kafka Controller as described in KIP-631 will use events/deltas to represent some of it in-memory state changes the amount of data that needs to be persistent and replicated to all of the brokers can be reduced. As an example if a broker goes offline and we assume that we have 1 million topic partitions, 10 brokers, an IsrChange
message of 40 bytes (4 bytes for partition id, 16 bytes for topic id, 8 bytes for ISR ids, 4 bytes for leader id, 4 bytes for leader epoch, 2 bytes for api key and 2 bytes for api version) and a DeleteBroker
message of 8 bytes (4 bytes for broker id, 2 bytes for api key and 2 bytes for api version).
If events/deltas are not allowed in the replicated log for the __cluster_metadata
topic partition then the Kafka Controller and each broker needs to persist 40 bytes per topic partitions hosted by the broker. In the example above that comes out to 3.81 MB. The Kafka Controller will need to replicate 3.81 MB to each broker in the cluster (10) or 38.14 MB.
Overview
This KIP assumes that KIP-595 has been approved and implemented. With this improvement replicas will continue to replicate their log among each others. Replicas will snapshot their log independent of each other. Snapshots will be consistent because the logs are consistent across replicas. Follower and observer replicas fetch the snapshots from the leader they attempt to fetch an offset from the leader and the leader doesn’t have that offset in the log because it is included in the snapshot.
Generating and loading the snapshot will be delegated to the Kafka Controller. The Kafka Controller will notify the Kafka Raft client when it has generated a snapshot and up to which offset is included in the snapshot. The Kafka Raft client will notify the Kafka Controller when a new snapshot has been fetched from the leader.
This KIP focuses on the changes required for use by the Kafka Controller's __cluster_metadata topic partition but we should be able to extend it to use it in other internal topic partitions like __consumer_offset
and __transaction
.
Proposed Changes
The __cluster_metadata
topic will have snapshot
as the cleanup.policy
. The Kafka Controller will have an in-memory representation of the replicated log up to at most the high-watermark. When performing a snapshot the Kafka Controller will serialize this in-memory state to disk. This snapshot file on disk is described by the largest offset and epoch from the replicated log that has been included.
The Kafka Controller will notify the Kafka Raft client when it has finished generating a new snapshots. It is safe for the log to truncate a prefix of the log up to the latest snapshot. The __cluster_metadata
topic partition will have the latest snapshot and zero or more older snapshots. Having multiple snapshots is useful for minimizing re-fetching of the snapshot and will be described later in the document. These additional snapshots would have to be deleted, a process describe later in the document.
Design
Visually a Kafka Raft Log would look as follow:
Code Block |
---|
Kafka Replicated Log:
LBO -- high-watermark -- LEO --
V V V
---------------------------------------------------
offset: | x + 1 | ... | y | y + 1 | ... | | ... | |
epoch: | b | ... | c | d | ... | | ... | |
---------------------------------------------------
Kafka Snapshot Files:
<topic_name>-<partition_index>/x-a.checkpoint
<topic_name>-<partition_index>/y-c.checkpoint |
Note: The extension checkpoint
will be used since Kafka already has file with the snapshot
extension.
Legends
LEO - log end offset - the largest offset and epoch that has been written to disk.
high-watermark - the largest offset and epoch that has been replicated to N/2 + 1 replicas.
LBO - log begin offset - the smallest offset in the replicated log. This property used to be called log start offset. It is renamed such that the acronym doesn’t conflict with last stable offset (LSO).
In the example above, if the Kafka topic partition leader receives a fetch request with an offset and epoch greater than or equal to the log begin offset (x, a)
then the leader handles the request as describe in KIP-595. Otherwise, the leader will respond with the offset and epoch of the latest snapshot (y, c)
and with the next fetch offset and epoch (y + 1, d)
.
The followers and observers can concurrently fetch both the snapshot and the replicated log. During leader election, followers with incomplete or missing snapshot will send a vote request and response as if they had an empty log.
Snapshot Format
The Kafka Controller is responsible for the format of the snapshot. The snapshot will be stored in the topic partition directory and will have a name of <SnapshotOffset>-<SnapshotOffsetEpoch>.checkpoint
. For example for the topic __cluster_metadata, partition 0, snapshot offset 5120793 and snapshot epoch 2, the full filename will be __cluster_metadata-0/00000000000005120793-2.checkpoint
.
When to Snapshot
If the Kafka Controller generates a snapshot too frequently then can negatively affect the performance of the disk. If it doesn't generate a snapshot often enough then it can increase the amount of time it takes to load its state into memory and it can increase the amount space taken up by the replicated log. The Kafka Controller will have a new configuration option controller.snapshot.minimum.records
. If the number of records between the latest snapshot and the high-watermark is greater than controller.snapshot.minimum.records,
then the Kafka Controller will perform a new snapshot.
When to Increase the Log Begin Offset
To minimize the need to send snapshots to slow replicas, the leader can delay increasing the log begin offset up to the minimum offset that it knows it has been replicate to all of the live replicas. Followers and observers will increase their log begin offset to the value sent on the fetch response as long as the local state machine has generated a snapshot that includes to the log begin offset minus one.
LBO
can be increase/set to an offset X
if the following is true:
There is a snapshot for the topic partition which includes the offset for X
- 1
and.One of the following is true:
All of the live replicas (followers and observers) have replicated
LBO
orLBO
ismax.replication.lag.ms
old.
Kafka allows the clients to delete records that are less than a given offset by using the DeleteRecords
RPC . Those requests will be validated using the same logic enumerated above.
When to Delete Snapshots
The broker will delete any snapshot with a latest offset and epoch (SnapshotOffsetAndEpoch
) less than the LBO - 1
(log begin offset). The means that the question of when to delete a snapshot is the same as when to increase the LBO
which is covered the the section “When To Increase the Log Begin Offset”.
Interaction Between Kafka Raft and the Kafka Controller
This section describe the interaction between the state machine and Kafka Raft.
From Kafka Raft to the Kafka Controller
- Notify when a snapshot is available because of a
FetchSnapshot
.
From the Kafka Controller to Kafka Raft
Notify when snapshot finished. This includes the largest included offset and epoch.
Loading Snapshots
There are two cases when the Kafka Controller needs to load the snapshot:
When the broker is booting.
When the follower and observer replicas finishes fetches a new snapshot from the leader.
After loading the snapshot, the Kafka Controller will apply operations in the replicated log with an offset greater than the snapshot’s offset and less than the high-watermark.
Changes to Leader Election
The followers of the __cluster_metadata
topic partition will concurrently fetch the snapshot and replicated log. This means that candidates with incomplete snapshots will send a vote request with a LastOffsetEpoch
of -1
and a LastOffset
of -1
no matter the LEO of the replicated log.
Validation of Snapshot and Log
There is an invariant that every log must have at least one snapshot where the offset of the snapshot is between LogBeginOffset - 1
and High-Watermark
. If this is not the case then the replica should assume that the log is empty. Once the replica discovers the leader for the partition it can bootstrap itself by using the Fetch
and FetchSnapshot
RPCs as described in this document.
Public Interfaces
Topic configuration
The
__cluster_metadata
topic will have ancleanup.policy
value ofsnapshot
. This configuration can only be read. Updates to this configuration will not be allowed.controller.snapshot.minimum.records
is the minimum number of records committed after the latest snapshot before the Kafka Controller will perform another snapshot. See section "When to Snapshot"max.replication.lag.ms
- The maximum amount of time that leader will wait for an offset to get replicated to all of the live replicas before advancing theLBO
. See section “When to Increase the Log Begin Offset”.
Network Protocol
Fetch
Request Schema
This KIP doesn’t require any changes to the fetch request outside of what is proposed in KIP-595.
Response Schema
The fetch response proposed in KIP-595 will be extended such that:
It includes new optional fields for snapshot offset and snapshot epoch.
The
LogStartOffset
field is renamed toLogBeginOffset
.
For topics like __cluster_metadata and __consumer_offsets we can assume that the set of keys rarely changes but that values change frequently. With this assumption, to generate a snapshot from the in-memory state as propose in this document the broker needs to write O(the size of the data). For log compaction the broker has to write O(the size of the data) + O(the size of tombstones).
The difference is in the read pattern for the two solutions. For in-memory snapshots every log record will be read once to update the in-memory state. The Kafka Controller and the Group Coordinator will read from the head of the log. No additional reads are required to generate a snapshot.
For log compaction there are three reads:
- One read to update the in-memory state.
- One read to generate the map of keys to offsets.
- One read to copy the live records from current log segments to the new log segments. These may read old offsets and may not be in the OS's file cache
If we assume an in-memory state of 100MB and a rate of change of 1MB/s, then
- With in-memory snapshot the broker needs to read 1MB/s from the head of the log.
- With log compaction the broker needs to
- read 1MB/s from the head of the log to update the in-memory state
- read 1MB/s to update the map of keys to offsets
- read 3MB/s (100MB from the already compacted log, 50MB from the new key-value records) from the older segments. The log will accumulate 50MB in 50 seconds worth of changes before compacting because the default configuration has a minimum clean ratio of 50%.
We mentioned __consumer_offsets
in this section because in the future we are interested in using this mechanism for high-throughput topic partitions like the Group Coordinator and Transaction Coordinator.
Amount of Data Replicated
Because the Kafka Controller and Metadata Cache as described in KIP-631 will use events/deltas to represent some of its in-memory state changes the amount of data that needs to be written to the log and replicated to all of the brokers can be reduced. As an example if a broker goes offline and we assume that we have 100K topics each with 10 partitions with a replication factor of 3 and 100 brokers. In a well balanced cluster we can expect that no broker will have 2 or more partitions for the same topic. In this case the best we can do for IsrChange
message and FenceBroker
message are the following message sizes:
IsrChange
has a size of 40 bytes. 4 bytes for partition id, 16 bytes for topic id, 8 bytes for ISR ids, 4 bytes for leader id, 4 bytes for leader epoch, 2 bytes for api key and 2 bytes for api version.FenceBroker
has a size of 8 bytes. 4 bytes for broker id, 2 bytes for api key and 2 bytes for api version.
When a broker is shutdown and the Active Controller uses IsrChange
to include this information in the replicated log for the __cluster_metadata
topic partition then the Kafka Controllers and each broker needs to persist 40 bytes per topic partitions hosted by the broker. Each broker will be a replica for 30,000 partitions, that is 1.14 MB that needs to get written in the replicated log by each broker. The Active Controller will need to replicate 1.14 MB to each broker in the cluster (99 brokers) or 113.29 MB.
When using events or deltas (FenceBroker
) each broker needs to persist 8 bytes in the replicated log and 1.14 MB in the snapshot. The Active Controller will need to replica 8 bytes to each broker in the cluster (99 brokers) or .77 KB. Note that each broker still needs to persist 1.14 MB - the difference is that those 1.14 MB are not sent through the network.
Consistent Log and Tombstones
It is very important that the log remain consistent across all of the replicas after log compaction. The current implementation of log compaction in Kafka can lead to divergent logs because it is possible for slow replicas to miss the tombstone records. Tombstone records are deleted after a timeout and are not guarantee to have been replicated to all of the replicas before they are deleted. It should be possible to extend log compaction and the fetch protocol as explained in the Rejected Alternatives "Just fix the tombstone GC issue with log compaction". We believed that this conservative approach is equally, if not more complicated than snapshots as explained in this proposal and more importantly it doesn't address the other items in the motivation section above.
Overview
This KIP assumes that KIP-595 has been approved and implemented. With this improvement replicas will continue to replicate their log among each others. This means that voters (leader and followers) and observers will have the replicated log on disk. Replicas will snapshot their log independent of each other. Snapshots will be consistent because the logs are consistent across replicas. Follower and observer replicas fetch the snapshots from the leader when they attempt to fetch an offset from the leader and the leader doesn’t have that offset in the log.
Generating and loading the snapshot will be delegated to the Kafka Controllers and Metadata Cache. The Kafka Controllers and Metadata Cache will notify the Kafka Raft layer when it has generated a snapshot and the end offset of the snapshot. The Kafka Raft layer will notify the Kafka Controller or Metadata Cache when a new snapshot has been fetched from the Active Controller. See section "Interaction with the Kafka Controller or Metadata Cache" and KIP 595 for more details.
This KIP focuses on the changes required for use by the __cluster_metadata
topic partition but we should be able to extend it to use it in other internal topic partitions like __consumer_offsets
and __transaction
.
Proposed Changes
The __cluster_metadata
topic will have snapshot
as the cleanup.policy
. The Kafka Controller and Metadata Cache will have an in-memory representation of the replicated log up to at most the high-watermark. When performing a snapshot the Kafka Controller and Metadata Cache will serialize this in-memory state to disk. This snapshot file on disk is described by the end offset and epoch from the replicated log that has been included.
The Kafka Controller and Metadata Cache will notify the Kafka Raft client when it has finished generating a new snapshots. It is safe to truncate a prefix of the log up to the latest snapshot. The __cluster_metadata
topic partition will have the latest snapshot and zero or more older snapshots. These additional snapshots would have to be deleted, this is described in "When to Delete Snapshots".
Design
Visually a Kafka Raft topic partition would look as follow:
Code Block |
---|
Kafka Replicated Log:
LogStartOffset -- high-watermark -- LEO --
V V V
-----------------------------------------------
offset: | x | ... | y - 1 | y | ... | | ... | |
epoch: | b | ... | c | d | ... | | ... | |
-----------------------------------------------
Kafka Snapshot Files:
<topic_name>-<partition_index>/x-a.checkpoint
<topic_name>-<partition_index>/y-c.checkpoint |
Note: The extension checkpoint
will be used since Kafka already has a file with the snapshot
extension.
Legend
LEO - log end offset - the next offset to be written to disk.
high-watermark - the largest offset and epoch that has been replicated to N/2 + 1 replicas.
LogStartOffset - log start offset - the smallest offset in the replicated log.
In the example above, offset=x - 1, epoch=a does not appear in the replicated log because it is before the LogStartOffset
(x, b). If the Kafka topic partition leader receives a fetch request with an offset and last fetched epoch greater than or equal to the LogStartOffset
then the leader handles the request as describe in KIP-595. Otherwise, the leader will respond with the SnapshotId,
epoch and end offset, of the latest snapshot (y, c)
.
Adding snapshots to KIP-595 changes how voters grant votes and how candidate request votes. See section "Changes to Leader Election" for details on those changes.
Snapshot Format
The Kafka Controller and Metadata Cache are responsible for the content of the snapshot. Each snapshot is uniquely identified by a SnapshotId
, the epoch and end offset of the records in the replicated log included in the snapshot. The snapshot will be stored in the topic partition directory and will have a name of <SnapshotId.EndOffset>-<SnapshotId.Epoch>.checkpoint
. For example, for the topic __cluster_metadata, partition 0, snapshot end offset 5120793 and snapshot epoch 2, the full filename will be __cluster_metadata-0/00000000000005120793-00000000000000000002.checkpoint
.
The snapshot epoch will be used when ordering snapshots and more importantly when setting the LastFetchedEpoch
field in the Fetch request. It is possible for a follower to have a snapshot and an empty log. In this case the follower will use the epoch of the snapshot when setting the LastFetchEpoch
in the Fetch request.
The disk format for snapshot files will be the same as the version 2 of the log format. This is the version 2 log format for reference:
Code Block |
---|
RecordBatch => BatchHeader [Record]
BatchHeader
BaseOffset => Int64
Length => Int32
PartitionLeaderEpoch => Int32
Magic => Int8
CRC => Uint32
Attributes => Int16
LastOffsetDelta => Int32 // also serves as LastSequenceDelta
FirstTimestamp => Int64
MaxTimestamp => Int64
ProducerId => Int64
ProducerEpoch => Int16
BaseSequence => Int32
Record =>
Length => Varint
Attributes => Int8
TimestampDelta => Varlong
OffsetDelta => Varint
Key => Bytes
Value => Bytes
Headers => [HeaderKey HeaderValue]
HeaderKey => String
HeaderValue => Bytes |
Using version 2 of the log format will allow the Kafka Controllers and Metadata Cache to compress records and identify corrupted records in the snapshot. Even though snapshot use the log format for storing this state there is no requirement:
- To use valid
BaseOffset
andOffsetDelta
in theBatchHeader
andRecord
respectively. - For the records in the snapshot to match the records in the replicated log.
Snapshot Control Records
To allow the KRaft implementation to include additional information on the snapshot without affect the Kafka Controller and Metadata cache the snapshot will include two control record batches. The control record batch SnapshotHeaderRecord
will always be the first record batch in a snapshot. The control record batch SnapshotFooterRecord
will be the last record batch in a snapshot. These two records will have the following schema.
Snapshot Header Schema
Code Block |
---|
{
"type": "data",
"name": "SnapshotHeaderRecord",
"validVersions": "0",
"flexibleVersions": "0+",
"fields": [
{"name": "Version", "type": "int16", "versions": "0+",
"about": "The version of the snapshot header record"},
{ "name": "LastContainedLogTimestamp", "type": "int64", "versions": "0+",
"about": "The append time of the last record from the log contained in this snapshot" }
]
} |
Snapshot Footer Schema
Code Block |
---|
{
"type": "data",
"name": "SnapshotFooterRecord",
"validVersions": "0",
"flexibleVersions": "0+",
"fields": [
{ "name": "Version", "type": "int16", "versions": "0+",
"about": "The version of the snapshot footer record" }
]
} |
When to Snapshot
If the Kafka Controller or Metadata Cache generates a snapshot too frequently then it can negatively affect the performance of the disk. If it doesn't generate a snapshot often enough then it can increase the amount of time it takes to load its state into memory and it can increase the amount space taken up by the replicated log. The Kafka Controller and Metadata Cache will have new configuration options metadata.snapshot.min.changed_records.ratio
and metadata.log.max.record.bytes.between.snapshots
. Both of these configuration need to be satisfied before the Controller or Metadata Cache will generate a new snapshot.
metadata.snapshot.min.changed_records.ratio
is the minimum number of snapshot records that have changed (deleted or modified) between the latest snapshot and the current in-memory state. Note that new snapshot records don't count against this ratio. If a new snapshot record was added since that last snapshot then it doesn't affect the dirty ratio. If a snapshot record was added and then modify or deleted then it counts against the ratio.-
metadata.log.max.record.bytes.between.snapshots
is the minimum size of the records in the replicated log between the latest snapshot and then high-watermark.
When to Increase the LogStartOffset
To minimize the need to send snapshots to slow replicas, the leader can delay increasing the log start offset up to the minimum offset that it knows has been replicate to all of the live replicas. The leader's LogStartOffset
can be increased to an offset X
if the following is true:
There is a snapshot for the topic partition with an end offset of
X
.One of the following is true:
All of the live replicas (followers and observers) have replicated
LogStartOffset
orLogStartOffset
ismetadata.start.offset.lag.time.max.ms
milliseconds old.
Followers and observers will increase their log start offset to the value sent on the fetch response as long as the local Kafka Controller and Metadata Cache has generated a snapshot with an end offset greater than or equal to the new log start offset. In other words for followers and observers the local log start offset is the minimum of the leader's log start offset and the largest local snapshot.
Kafka allows the clients to delete records that are less than a given offset by using the DeleteRecords
RPC . Those requests will be rejected for the __cluster_metadata
topic with a POLICY_VIOLATION error.
When to Delete Snapshots
The broker will delete any snapshot with a end offset and epoch (SnapshotId
) less than the LogStartOffset
. That means that the question of when to delete a snapshot is the same as when to increase the LogStartOffset
which is covered in the section “When To Increase the LogStartOffset”.
Interaction with the Kafka Controller and Metadata Cache
This section describe the interaction between the state machine and Kafka Raft.
From Kafka Raft to the Kafka Controller and Metadata Cache
- Notify when a snapshot is available because of
FetchSnapshot
.
From the Kafka Controller and Metadata Cache to Kafka Raft
Notify when snapshot finished. This includes the end offset and epoch.
Loading Snapshots
There are two cases when the Kafka Controller or Metadata Cache needs to load a snapshot:
When it is booting.
When the follower and observer replicas finishes fetching a new snapshot from the leader.
After loading the snapshot, the Kafka Controller and Metadata Cache can apply operations in the replicated log with an offset greater than or equal the snapshot’s end offset.
Changes to Leader Election
The followers of the __cluster_metadata
topic partition need to take the snapshots into account when granting votes and asking for votes (becoming a candidate). Conceptually, a follower implements the following algorithm:
- Follower sends a fetch request
- Leader replies with a snapshot epoch and end offset.
- Follower pauses fetch requests
- Follower fetches snapshot chunks from the leader into a temporary snapshot file (
<EndOffset>-<Epoch>.checkpoint.part
) - Validate fetched snapshot
- All snapshot chunks are fetched
- Verify the CRC of the records in the snapshot
- Atomically move the temporary snapshot file (
<EndOffset>-<Epoch>.checkpoint.part
) to the permanent location (<EndOffset>-<Epoch>.checkpoint
)
- Follower resumes fetch requests by
- Setting the
LogStartOffset
to the snapshot's end offset. - Setting the
LEO
orFetchOffset
in the fetch request to the snapshot's end offset. - Setting the
LastFetchedEpoch
in the fetch request to the snapshot's epoch.
- Setting the
Assuming that the snapshot fetched in bullet 4 has an epoch E1
and a offset O1
. The state of the follower between bullet 4. and 5. is as follow:
- Contains a snapshot with epoch
E1
and end offsetO1
. - Contains zero or more snapshots older/less than epoch
E1
and end offsetO
1. - Contains a replicated log with
LEO
older/less than epochE
1 and end offsetO
1.
In this case the follower needs to use the snapshot with epoch E
1 and end offset O
1, and not the replicated log when granting votes and requesting votes. In the general case, the latest epoch and end offset for a replica is the latest snapshot or replicated log.
Validation of Snapshot and Log
For each replica we have the following invariant:
- If the
LogStartOffset
is 0, there are zero or more snapshots where the end offset of the snapshot is betweenLogStartOffset
and theHigh-Watermark.
- If the
LogStartOffset
is greater than 0, there are one or more snapshots where the end offset of the snapshot is betweenLogStartOffset
and theHigh-Watermark.
If the latest snapshot has an epoch E1
and end offset O1
and is it newer than the LEO
of the replicated log, then the replica must set the LogStartOffset
and LEO
to O
1.
Public Interfaces
Topic configuration
The
__cluster_metadata
topic will have acleanup.policy
value ofsnapshot
. This configuration can only be read. Updates to this configuration will not be allowed.metadata.snapshot.min.changed_records.ratio
- The minimum ratio of snapshot records that have to change before generating a snapshot. See section "When to Snapshot". The default is .5 (50%).metadata.log.max.record.bytes.between.snapshots
- This is the minimum number of bytes in the replicated log between the latest snapshot and the high-watermark needed before generating a new snapshot. See section "When to Snapshot". The default is 20MB.metadata.start.offset.lag.time.max.ms
- The maximum amount of time that leader will wait for an offset to get replicated to all of the live replicas before advancing theLogStartOffset
. See section “When to Increase the LogStartOffset”. The default is 7 days.
Network Protocol
Fetch
Request Schema
This KIP doesn’t require any changes to the fetch request outside of what is proposed in KIP-595.
Response Schema
The fetch response proposed in KIP-595 will be extended such that it includes new optional fields for snapshot end offset and snapshot epoch.
Code Block | ||
---|---|---|
| ||
{
"apiKey": 1,
"type": "response",
"name": "FetchResponse",
"validVersions": "0-12",
"flexibleVersions": "12+",
"fields": [
{ "name": "ThrottleTimeMs", "type": "int32", "versions": "1+", "ignorable": true,
"about": "The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota." },
{ "name": "ErrorCode", "type": "int16", "versions": "7+", "ignorable": false,
"about": "The top level response error code." },
{ "name": "SessionId", "type": "int32", "versions": "7+", "default": "0", "ignorable": false,
"about": "The fetch session ID, or 0 if this is not part of a fetch session." },
{ "name": "Topics", "type": "[]FetchableTopicResponse", "versions": "0+",
"about": "The response topics.", "fields": [
{ "name": "Name", "type": "string", "versions": "0+", "entityType": "topicName",
"about": "The topic name." },
{ "name": "Partitions", "type": "[]FetchablePartitionResponse", "versions": "0+",
| ||
Code Block | ||
| ||
{ "apiKey": 1, "type": "response", "name": "FetchResponse", "validVersions": "0-12", "flexibleVersions": "12+", "fields": [ { "name": "ThrottleTimeMs", "type": "int32", "versions": "1+", "ignorable": true, "about": "The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota." }, topic partitions.", "fields": [ { "name": "ErrorCodePartitionIndex", "type": "int16int32", "versions": "70+", "ignorable": false, "about": "The top level response error code "The partition index." }, { "name": "SessionIdErrorCode", "type": "int32int16", "versions": "70+", "default": "0", "ignorable": false, "about": "The fetcherror session IDcode, or 0 if thisthere is not part of a was no fetch sessionerror." }, { "name": "TopicsHighWatermark", "type": "[]FetchableTopicResponseint64", "versions": "0+", "about": "The current high responsewater topicsmark." }, "fields": [ { "name": "NameLastStableOffset", "type": "stringint64", "versions": "0+4+", "default": "-1", "entityTypeignorable": "topicName"true, "about": "The topic name." }, { "name": "Partitions", "type": "[]FetchablePartitionResponse", "versions": "0+", "about": "The topic partitions.", "fields": [ last stable offset (or LSO) of the partition. This is the last offset such that the state of all transactional records prior to this offset have been decided (ABORTED or COMMITTED)" }, { "name": "PartitionIndexLogStartOffset", "type": "int32int64", "versions": "0+",5+", "default": "-1", "ignorable": true, "about": "The partiiton indexcurrent log start offset." }, { "name": "ErrorCodeDivergingEpoch", "type": "int16EpochEndOffset", "versions": "012+", "taggedVersions": "12+", "tag": 0, "about": "The error code, or 0 if there was no fetch error." }, { "name": "HighWatermark", "type": "int64", "versions": "0+"In case divergence is detected based on the `LastFetchedEpoch` and `FetchOffset` in the request, this field indicates the largest epoch and its end offset such that subsequent records are known to diverge", "aboutfields": "The current high water mark." }, [ { "name": "LastStableOffsetEpoch", "type": "int64int32", "versions": "412+", "default": "-1" }, "ignorable": true, { "aboutname": "EndOffset"The last stable offset (or LSO) of the partition. This is the last offset such that the state of all transactional records prior to this offset have been decided (ABORTED or COMMITTED)" }, "type": "int64", "versions": "12+", "default": "-1" } ]}, { "name": "CurrentLeader", "type": "LeaderIdAndEpoch", // Field renamed from LogStartOffset to LogBeginOffset "versions": "12+", "taggedVersions": "12+", "tag": 1, fields": [ { "name": "LogBeginOffsetLeaderId", "type": "int64int32", "versions": "50+", "default": "-1", "ignorable": true, "about": "The currentID logof beginthe offset." }, // ---------- Start new fields by KIP-595 ---------- current leader or -1 if the leader is unknown."}, { "name": "LeaderEpoch", "type": "NextOffsetAndEpochint32", "typeversions": "OffsetAndEpoch0+", "versionsabout": "12+", "taggedVersions": "12+", "tag": 0, "fields": [ The latest known leader epoch"} ]}, { "name": "NextFetchOffset", "type": "int64", "versions": "0+",// ---------- Start of new field ---------- { "aboutname": "If setSnapshotId", this is the offset that the follower should truncate to"}"type": "SnapshotId", { "nameversions": "NextFetchOffsetEpoch12+", "typetaggedVersions": "int3212+", "versionstag": "0+"2, "about": "TheIn the epochcase of thefetching nextan offset in case less than the LogStartOffset, this is the followerend needsoffset to truncate"} ]}and epoch that should be used in the FetchSnapshot request.", { "namefields": "CurrentLeader", "type": "LeaderIdAndEpoch", [ { "versionsname": "12+EndOffset", "taggedVersionstype": "12+int64", "tagversions": 1"0+"}, fields": [ { "name": "LeaderIdEpoch", "type": "int32", "versions": "0+",} ]}, "about": "The ID of the current leader or -1 if the leader is unknown."},// ---------- End new field ---------- { "name": "LeaderEpochAborted", "type": "int32[]AbortedTransaction", "versions": "0"4+", "nullableVersions": "4+", "ignorable": false, "about": "The latest known leader epoch"}aborted transactions.", "fields": [ ]}, // ---------- End new fields by KIP-595 ---------- { "name": "ProducerId", "type": "int64", "versions": "4+", "entityType": "producerId", "about": "The producer // ---------- Start new fields ----------id associated with the aborted transaction." }, { "name": "SnapshotOffsetAndEpochFirstOffset", "type": "int64", "versions": "OffsetAndEpoch4+", "versionsabout": "12+", "taggedVersions": "12+", "tag": 0, "fields": [ The first offset in the aborted transaction." } ]}, { "name": "FetchOffsetPreferredReadReplica", "type": "int64int32", "versions": "011+", "ignorable": true, "about": "IfThe set,preferred thisread isreplica the offsetfor that the followerconsumer shouldto use on forits thenext FetchSnapshotfetch request"}, { "name": "FetchOffsetEpochRecords", "type": "int32bytes", "versions": "0+", "nullableVersions": "0+", "about": "The record data." } ]} "about": "If set, this is the epoch that the follower should use for the FetchSnapshot request"} ]}, // ---------- End new fields ---------- { "name": "Aborted", "type": "[]AbortedTransaction", "versions": "4+", "nullableVersions": "4+", "ignorable": false, "about": "The aborted transactions.", "fields": [ { "name": "ProducerId", "type": "int64", "versions": "4+", "entityType": "producerId", ]} ] } |
Handling Fetch Request
The leader’s handling of fetch request will be extended such that if FetchOffset
is less than LogStartOffset
then the leader will respond with a SnapshotId
of the latest snapshot.
Handling Fetch Response
The replica's handling of fetch response will be extended such that if SnapshotId
is set then the follower will pause fetching of the log, and start fetching the snapshot from the leader. The value use in SnapshotId
will be used in the FetchSnapshot
requests. Once the snapshot has been fetched completely, the local log will be truncated and fetch will resume.
Fetching Snapshots
Request Schema
Code Block | ||
---|---|---|
| ||
{ "apiKey": 59, "type": "request", "name": "FetchSnapshotRequest", "validVersions": "0", "flexibleVersions": "0+", "fields": [ { "name": "ClusterId", "type": "string", "versions": "0+", "nullableVersions": "0+", "default": "null", "taggedVersions": "0+", "tag": 0, "about": "The clusterId producerif idknown, associatedthis withis theused abortedto transaction." }, validate metadata fetches prior to broker registration" }, { "name": "FirstOffsetReplicaId", "type": "int64int32", "versions": "40+", "default": "-1", "entityType": "brokerId", "about": "The firstbroker offsetID inof the aborted transaction.follower" } ]}, { "name": "PreferredReadReplicaMaxBytes", "type": "int32", "versions": "110+", "ignorable": true, "about": "The preferredmaximum read replica for the consumer bytes to usefetch onfrom itsall nextof fetchthe requestsnapshots." }, { "name": "RecordsTopics", "type": "bytes[]TopicSnapshot", "versions": "0+", "nullableVersions": "0+", "about": "The record data." } ]} "0+", ]} ] } |
Handling Fetch Request
The replica’s handling of fetch request will be extended such that if FetchOffset
is less than LogBeginOffset
then the leader will respond with a SnapshotOffsetAndEpoch
of the latest snapshot and a NextOffsetAndEpoch
of the offset right after SnapshotOffsetAndEpoch
.
Handling Fetch Response
The replica's handling of fetch response will be extended such that if SnapshotOffsetAndEpoch
is set then the follower will truncate its entire log and start fetching from both the log and the snapshot. Where to fetch in the Log is describe by NextOffsetAndEpoch
while which snapshot to fetch is described by SnapshotOffsetAndEpoch
.
Fetching Snapshots
Request Schema
Code Block | ||
---|---|---|
| ||
{ "apiKey": #, "type": "request", "name": "FetchSnapshotRequest", "validVersions": "0", "flexibleVersions": "0+", "about": "The topics to fetch.", "fields": [ { "name": "Name", "type": "string", "versions": "0+", "entityType": "topicName", "about": "The name of the topic to fetch" }, { "name": "Partitions", "type": "[]PartitionSnapshot", "versions": "0+", "about": "The partitions to fetch", "fields": [ { "name": "ReplicaIdPartition", "type": "int32", "versions": "0+", "about": "The broker ID of the follower, of -1 if this request is from a consumer. "The partition index" }, { "name": "TopicsCurrentLeaderEpoch", "type": "[]TopicSnapshotsint32", "versions": "0+", "about": "The topics to fetch.", "fields": [ current leader epoch of the partition, -1 for unknown leader epoch" }, { "name": "NameSnapshotId", "type": "stringSnapshotId", "versions": "0+", "entityType": "topicName", "about": "The namesnapshot ofendOffset theand topicepoch to fetch." }, ", "fields": [ { "name": "PartitionsEndOffset", "type": "[]PartitionSnapshotsint64", "versions": "0+" }, { "aboutname": "Epoch"The partitions to fetch., "type": "int32", "fieldsversions": ["0+" } ]}, { "name": "PartitionIndexPosition", "type": "int32int64", "versions": "0+", "about": "The partition index." }, { "name": "SnapshotOffsetAndEpoch",byte position within the snapshot to start fetching from" } ]} ]} ] } |
Response Schema
Code Block | ||
---|---|---|
| ||
{ "apiKey": 59, "type": "OffsetAndEpochresponse", "versions"name": "12+FetchSnapshotResponse", "taggedVersionsvalidVersions": "12+0", "tagflexibleVersions": "0+", "fields": [ { "name": "OffsetThrottleTimeMs", "type": "int64int32", "versions": "0+", "ignorable": true, "about": "If set, this is the offset that the follower should use for the FetchSnapshot request"}, The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota." }, { "name": "OffsetEpochErrorCode", "type": "int32int16", "versions": "0+", "ignorable": false, "about": "IfThe set,top thislevel isresponse the epoch that the follower should use for the FetchSnapshot request"} ]error code." }, { "name": "PositionTopics", "type": "int64[]TopicSnapshot", "versions": "0+", "about": "The bytetopics position within the snapshotto fetch." }, "fields": [ { "name": "MaxBytesName", "type": "int32string", "versions": "0+", "entityType": "topicName", "about": "The name maximumof the bytestopic to fetch from this snapshot." }, ]} ]} ] } |
Response Schema
Code Block | ||
---|---|---|
| ||
{ "apiKeyname": #"Partitions", "type": "request[]PartitionSnapshot", "nameversions": "FetchSnapshotResponse0+", "validVersionsabout": "0", "flexibleVersions": "0+", The partitions to fetch.", "fields": [ { "name": "ThrottleTimeMsIndex", "type": "int32", "versions": "0+", "ignorable": true, "about": "The durationpartition in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota." }, index." }, { "name": "ErrorCode", "type": "int16", "versions": "0+", "ignorable "about": false, "The error code, or 0 "about": "The top level response error codeif there was no fetch error." }, { "name": "TopicsSnapshotId", "type": "[]TopicSnapshotsSnapshotId", "versions": "0+", "about": "The topics to fetch.", snapshot endOffset and epoch fetched", "fields": [ { "name": "NameEndOffset", "type": "stringint64", "versions": "0+" }, "entityType { "name": "topicNameEpoch", "type": "about": "The name of the topic to fetch." "int32", "versions": "0+" } ]}, { "name": "PartitionsCurrentLeader", "type": "[]PartitionSnapshotsLeaderIdAndEpoch", "versions": "0+", "abouttaggedVersions": "0+"The partitions to fetch.", "tag": 0, "fields": [ { "name": "PartitionIndexLeaderId", "type": "int32", "versions": "0+", "about": "The partition index." ID of the current leader or -1 if the leader is unknown."}, { "name": "ErrorCodeLeaderEpoch", "type": "int16int32", "versions": "0+", "about": "The latest errorknown code, or 0 if there was no fetch error." }, leader epoch"} ]}, { "name": "ContinueSize", "type": "boolint64":, "versions": "0+", "about": "TrueThe iftotal theresize isof data remaining to fetchthe snapshot." }, { "name": "Position", "type": "int64", "versions": "0+", "about": "The starting byte position within the snapshot included in the Bytes field." }, { "name": "BytesUnalignedRecords", "type": "bytesrecords":, "versions": "0+", "about": "Snapshot data." }, ", "about": "Snapshot data in records format which may not be aligned on an offset boundary" } ]} ]} ] } |
Request Handling
When the leader receives a FetchSnapshot
request it would do the following:
Find the snapshot for
SnapshotId
for the topicName
and partitionIndex
.- Set the
Size
of each snapshot. Send the bytes in the snapshot from
Position
. If there are multiple partitions in theFetchSnapshot
request, then the leader will evenly distribute the number of bytes sent across all of the partitions. The leader will not send more bytes in the response thanResponseMaxBytes
, which is the minimum ofMaxBytes
in the request and the value configured inreplica.fetch.response.max.bytes
.- Each topic partition is guaranteed to receive at least the average of
ResponseMaxBytes
if that snapshot has enough bytes remaining. - If there are topic partitions with snapshots that have remaining bytes less than the average
ResponseMaxBytes
, then those bytes may be used to send snapshot bytes for other topic partitions
- Each topic partition is guaranteed to receive at least the average of
Find the snapshot for
SnapshotOffsetAndEpoch
for the topicName
and partitionPartitionIndex
.Send the bytes in the snapshot from
Position
up to at mostMaxBytes
.- Set
Continue
to true if there are renaming bytes in the snapshots- .
Errors:
SNAPSHOT_NOT_FOUND
- when the fetch snapshot request specifies aSnapshotOffsetAndEpoch
SnapshotId
that doesn’t exists on the leader.OFFSETPOSITION_OUT_OF_RANGE
- when the fetch snapshot request’s offset request specifies a position that is greater than the size of the snapshot.NOT_LEADER_FOR_PARTITION
- when the fetch snapshot request is sent to a replica that is not the leader.
Response Handling
- Copy the bytes in
Bytes
UnalignedRecords
to the local snapshot file name<SnapshotId.EndOffset>-<SnapshotId.Epoch>.checkpoint.part
starting at file positionPosition
. If
Continue
istrue
,Size
is greater than the size of the current snapshot file, then the follower is expected to send anotherFetchSnapshotRequest
with aPosition
set to the response'sPosition
plus the response'slen(BytesUnalignedRecords)
.If
Continue
isfalse
Size
is equal to the size of the current snapshot file, then fetching the snapshot has finished.- Validate the snapshot file.
Atomically move the file named
<SnapshotId.EndOffset>-<SnapshotId.Epoch>.checkpoint.part
to a file named<SnapshotId.EndOffset>-<SnapshotId.Epoch>.checkpoint
.Notify the Kafka Controller or Metadata Cache that a new snapshot is available.
The Kafka Controller and Metadata Cache will load this new snapshot and read records from the log after the snapshot's end offset.
Errors:
SNAPSHOT_NOT_FOUND
,OFFSETPOSITION_OUT_OF_RANGE
,NOT_LEADER_FOR_PARTITION
- The follower can rediscover the new snapshot by sending a fetch request with an offset and epoch of zeroanotherFetch
request.
Metrics
Full Name | Description |
---|---|
kafka.controller:type=KafkaController,name=GenSnapshotLatencyMs | A histogram of the amount of time it took to generate a snapshot. |
kafka.controller:type=KafkaController,name=LoadSnapshotLatencyMs | A histogram of the amount of time it took to load the snapshot. |
kafka.controller:type=KafkaController,name=SnapshotSizeBytes | Size of the latest snapshot in bytes. |
kafka.controller:type=KafkaController,name=SnapshotLag | The number of offsets between the largest snapshot offset and the high-watermark. |
Compatibility, Deprecation, and Migration Plan
This KIP is only implemented for the internal topic __cluster_metadata
. Internal and external clients for all other topics can ignore the SnapshotOffsetAndEpoch
as that field will not be set for those topic partitionsThe inter-broker protocol (IBP
) will be increased to indicate that all of the brokers in the cluster support KIP-595 and KIP-630.
Rejected Alternatives
Append the snapshot to the Kafka log: Instead of having a separate file for snapshots and separate RPC for downloading the snapshot, the leader could append the snapshot to the log. This design has a few drawbacks. Because the snapshot gets appended to the head of the log that means that it will be replicated to every replica in the partition. This will cause Kafka to use more network bandwidth than necessary.
...
Use Kafka Log infrastructure to store snapshots: Kafka could use the Log functionality to persists the snapshot to disk and to facilitate the replication of the snapshot to the replicas. Log could be used to store the snapshot to disk and the Fetch request on a special topic partition could be used to the replicate the snapshot to the replicas. There are a couple observations with this design decision:
- The
Fetch
RPC is typically used to download a never ending stream of records. Snapshot have a known size and the fetching of a snapshot ends when all of those bytes are received by the replica. TheFetch
RPC includes a few fields for dealing with this access pattern that are not needed for fetching snapshots. - This solution ties the snapshot format to Kafka
...
- 's log record format. To lower the overhead of writing and loading the snapshot by the Kafka Controller and Metadata Cache, it is important to use a format that is compatible with the in-memory representation of the Kafka Controller and Metadata Cache.
Change leader when generating snapshots: Instead of supporting concurrent snapshot generation and writing to the in-memory state, we could require that only non-leaders generate snapshots. If the leader wants to generate a snapshot then it would relinquish leadership. We decided to not implement this because:
...
Just fix the tombstone GC issue with log compaction: we We have also talked discussed about just fixing the tombstone garbage collecting issue as listed in motivation instead of introducing the snapshot mechanism. The key idea is that fetchers either from follower or from consumer needs to be notified if they "may" be missing some tombstones that have deprecated some old records. Since the offset span between the deprecated record and the tombstone for the same key can be arbitrarily long — for example, we have a record with key A at offset 0, and then a tombstone record for key A appended a week later, at offset 1 billion — we'd have to take a conservative approach here. More specifically:
- The leader would maintain an additional offset marker up to which we have deleted tombstones, i.e. all tombstones larger than this offset are still in the log. Let's call it a tombstone horizon marker. This horizon marker would be returned in each fetch response.
- Follower replicas would not do delete its tombstones beyond the received leader's horizon marker, and would maintain it's own marker as well which should always be smaller than leader's horizon marker offset.
- All fetchers (follower and consumer) would keep track of the horizon marker from the replica it is fetching from. The expectation is that leaders should only delete tombstones in a very lazy manner, controlled by the existing config, so that the fetching position should be larger than the horizon marker most of the time. However, if the fetcher does found its fetching position below the horizon marker, then it needs to handle it accordingly:
- For consumer fetchers, we need to throw an extended error of
InvalidOffsetException
to notify the users that we may missed some tombstones, and let the users to reset is consumed record states — for example, if the caller of consumer is a streams client, and it is building a local state from the fetched records, that state need to be wiped. - For replica fetchers, we need to wipe the local log all together and then restart fetching from the starting offset.
- For either types of fetchers, we will set an internal flag, let's call it unsafe-bootstrapping mode to true, while remembering the horizon marker offset at the moment from the replica it is fetching from.
- When this flag is set to true, we would not fall back to a loop of this handling logic even when the fetching position is smaller than the horizon marker, since otherwise we would fall into a loop of rewinding.
- However, during this period of time, if the horizon marker offsets get updated from the fetch response, meaning that some tombstones are deleted again, then we'd need to retry this unsafe-bootstrap from step one.
- After the fetching position as advanced beyond the remembered horizon marker offset, we would reset this flag to false to go back to the normal mode.
- For consumer fetchers, we need to throw an extended error of
We feel this conservative approach is equally, if not more complicated than adding a snapshot mechanism since it requires rather tricky handling logic on the fetcher side; plus it only resolves one of the motivations — the tombstone garbage collection issue.
Another thought againts against this idea is that for now most of the internal topics (controller, transaction coordinator, group coordinator) that would need the snapshot mechanism describe in this KIP can hold their materialized cache in memory completely, and hence generating snapshots should be comparably more efficient to execute.
...