Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

MirrorMaker 2 Reference

MirrorMaker 2 (MM2) is Apache Kafka's tool for replicating data between Kafka clusters. Built on Kafka Connect, it provides topic replication, consumer offset synchronization, and automatic topic configuration mirroring.


MirrorMaker 2 replaces the legacy MirrorMaker (MM1) with a redesigned architecture based on Kafka Connect. It addresses fundamental limitations of MM1:

Limitation (MM1)Solution (MM2)
No offset translationCheckpoint-based offset synchronization
Topic configuration not syncedAutomatic config mirroring
No ACL replicationACL synchronization support
Manual topic creationAutomatic topic creation
Single cluster pairMultiple cluster topologies
Difficult to monitorKafka Connect metrics and status

MM2 consists of three Kafka Connect connectors:

ConnectorPurpose
MirrorSourceConnectorReplicates topic data from source to target cluster
MirrorCheckpointConnectorSynchronizes consumer group offsets between clusters
MirrorHeartbeatConnectorEmits heartbeats for replication health monitoring
MirrorMaker 2 connectors replicating data, offsets, and heartbeatsMirrorMaker 2 connectors replicating data, offsets, and heartbeatsSource ClusterMirrorMaker 2Target Clustertopics__consumer_offsetsMirrorSourceConnectorMirrorCheckpointConnectorMirrorHeartbeatConnectorsource.topicssource.checkpoints.internalheartbeatsreplicate datasync offsetsemit heartbeats

Run MM2 as a standalone Connect cluster dedicated to replication.

Dedicated MirrorMaker 2 Connect cluster between source and targetDedicated MirrorMaker 2 Connect cluster between source and targetSource ClusterMM2 Connect ClusterTarget ClusterBrokersWorker 1Worker 2Worker 3BrokersDedicated resourcesIndependent scalingIsolated failuresconsumeproduce

Advantages:

  • Isolated from other Connect workloads
  • Independent scaling
  • Clear resource allocation

Deploy MM2 connectors alongside other connectors.

Advantages:

  • Simpler infrastructure
  • Shared monitoring
  • Lower operational overhead

Disadvantages:

  • Resource contention with other connectors
  • Replication affected by other connector issues

Run MM2 workers on target cluster broker nodes.

Advantages:

  • Reduced network hops for writes
  • Simpler networking

Disadvantages:

  • Competes for broker resources
  • Not recommended for high-volume replication

# Define clusters
clusters = source, target
# Source cluster connection
source.bootstrap.servers = source-broker-1:9092,source-broker-2:9092,source-broker-3:9092
# Target cluster connection
target.bootstrap.servers = target-broker-1:9092,target-broker-2:9092,target-broker-3:9092
# Security for source cluster (if required)
source.security.protocol = SASL_SSL
source.sasl.mechanism = SCRAM-SHA-512
source.sasl.jaas.config = org.apache.kafka.common.security.scram.ScramLoginModule required \
username="mm2-user" \
password="secret";
# Security for target cluster (if required)
target.security.protocol = SASL_SSL
target.sasl.mechanism = SCRAM-SHA-512
target.sasl.jaas.config = org.apache.kafka.common.security.scram.ScramLoginModule required \
username="mm2-user" \
password="secret";
# Enable replication from source to target
source->target.enabled = true
# Topics to replicate (regex patterns)
source->target.topics = .*
# Topics to exclude from replication
source->target.topics.exclude = .*[\-\.]internal, .*\.replica, __.*
# Consumer groups to replicate offsets for
source->target.groups = .*
# Groups to exclude
source->target.groups.exclude = console-consumer-.*, connect-.*
# Replication factor for replicated topics
replication.factor = 3
# Replication factor for MM2 internal topics
checkpoints.topic.replication.factor = 3
heartbeats.topic.replication.factor = 3
offset-syncs.topic.replication.factor = 3
# Topic creation settings
refresh.topics.enabled = true
refresh.topics.interval.seconds = 60
# Sync topic configurations
sync.topic.configs.enabled = true
sync.topic.configs.interval.seconds = 60
# Sync topic ACLs (requires ACL access)
sync.topic.acls.enabled = false
# Enable consumer group offset sync
sync.group.offsets.enabled = true
sync.group.offsets.interval.seconds = 60
# Emit checkpoints for offset translation
emit.checkpoints.enabled = true
emit.checkpoints.interval.seconds = 60
# Emit heartbeats
emit.heartbeats.enabled = true
emit.heartbeats.interval.seconds = 1
# Number of tasks (parallelism)
tasks.max = 10
# Producer settings for replication
producer.buffer.memory = 67108864
producer.batch.size = 524288
producer.linger.ms = 100
producer.compression.type = lz4
# Consumer settings
consumer.fetch.min.bytes = 1048576
consumer.fetch.max.wait.ms = 500
consumer.max.poll.records = 1000
# Offset sync frequency
offset.lag.max = 100
# Replication policy (controls topic naming)
replication.policy.class = org.apache.kafka.connect.mirror.DefaultReplicationPolicy
# Topic separator (default is ".")
replication.policy.separator = .
# Custom replication policy example:
# replication.policy.class = com.example.CustomReplicationPolicy

Replicated topics are prefixed with the source cluster alias:

Source ClusterSource TopicTarget Topic
eastorderseast.orders
westeventswest.events
produser.activityprod.user.activity

Identity Naming (IdentityReplicationPolicy)

Section titled “Identity Naming (IdentityReplicationPolicy)”

Preserves original topic names (use with caution—requires careful loop prevention):

replication.policy.class = org.apache.kafka.connect.mirror.IdentityReplicationPolicy
Source TopicTarget Topic
ordersorders

Loop Prevention Required

IdentityReplicationPolicy requires explicit topic filtering to prevent replication loops in bidirectional setups.

Implement custom naming logic:

public class CustomReplicationPolicy implements ReplicationPolicy {
@Override
public String formatRemoteTopic(String sourceClusterAlias, String topic) {
// Custom naming: replicated-<source>-<topic>
return "replicated-" + sourceClusterAlias + "-" + topic;
}
@Override
public String topicSource(String topic) {
if (topic.startsWith("replicated-")) {
String[] parts = topic.split("-", 3);
return parts.length >= 2 ? parts[1] : null;
}
return null;
}
@Override
public String upstreamTopic(String topic) {
if (topic.startsWith("replicated-")) {
String[] parts = topic.split("-", 3);
return parts.length >= 3 ? parts[2] : null;
}
return null;
}
@Override
public boolean isInternalTopic(String topic) {
return topic.endsWith(".internal") || topic.startsWith("__");
}
}

Source and target clusters have different offsets for the same logical data. MM2 maintains mappings via checkpoints.

Offset translation through the checkpoints topicOffset translation through the checkpoints topicSource ClusterTarget Clusterordersoffset 0: order-1offset 1: order-2offset 2: order-3offset 3: order-4source.ordersoffset 0: order-1offset 1: order-2offset 2: order-3offset 3: order-4source.checkpoints.internalConsumer group: app1Source offset: 3Target offset: 3replicate
{
"consumer_group": "my-consumer-group",
"topic": "orders",
"partition": 0,
"upstream_offset": 1000,
"downstream_offset": 1000,
"metadata": ""
}
Terminal window
# View checkpoints
kafka-console-consumer.sh \
--bootstrap-server target:9092 \
--topic source.checkpoints.internal \
--from-beginning \
--property print.key=true \
--property key.separator=": "
# Use RemoteClusterUtils API for programmatic translation
// Programmatic offset translation
Map<TopicPartition, OffsetAndMetadata> translatedOffsets =
RemoteClusterUtils.translateOffsets(
targetProperties,
"source",
consumerGroupId,
Duration.ofSeconds(30)
);
// Reset consumer to translated offsets
consumer.commitSync(translatedOffsets);

  1. Create MM2 configuration file
mm2.properties
clusters = source, target
source.bootstrap.servers = source:9092
target.bootstrap.servers = target:9092
source->target.enabled = true
source->target.topics = .*
replication.factor = 3
  1. Start MirrorMaker 2
Terminal window
# Dedicated mode
connect-mirror-maker.sh mm2.properties
# Or as Connect connectors
curl -X POST -H "Content-Type: application/json" \
--data @mirror-source-connector.json \
http://connect:8083/connectors
  1. Verify replication
Terminal window
# Check connector status
curl http://connect:8083/connectors/MirrorSourceConnector/status
# Verify topics created on target
kafka-topics.sh --bootstrap-server target:9092 --list | grep "source\."
# Check replication lag
kafka-consumer-groups.sh --bootstrap-server target:9092 \
--group connect-MirrorSourceConnector \
--describe

Topics matching the configured patterns are automatically replicated when created. To replicate additional existing topics:

  1. Update topic pattern:
source->target.topics = orders.*,events.*,new-topic-pattern.*
  1. Restart or reconfigure MM2

  2. Verify new topics appear on target

  1. Update exclusion pattern:
source->target.topics.exclude = .*internal,deprecated-topic
  1. Restart or reconfigure MM2

  2. Optionally delete replicated topic from target:

Terminal window
kafka-topics.sh --bootstrap-server target:9092 \
--delete --topic source.deprecated-topic

MetricDescriptionAlert Threshold
kafka.connect.mirror.source.connector.record-countRecords replicatedSudden drops
kafka.connect.mirror.source.connector.record-age-msAge of last replicated record> 60000 ms
kafka.connect.mirror.source.connector.replication-latency-msEnd-to-end replication latency> 30000 ms
kafka.connect.mirror.source.connector.byte-countBytes replicatedBaseline deviation
kafka.connect.mirror.checkpoint.connector.checkpoint-latency-msCheckpoint sync delay> 120000 ms
Terminal window
# Connector status
curl -s http://connect:8083/connectors/MirrorSourceConnector/status | jq .
# Task status
curl -s http://connect:8083/connectors/MirrorSourceConnector/tasks/0/status | jq .
# Replication lag
kafka-consumer-groups.sh --bootstrap-server target:9092 \
--group connect-MirrorSourceConnector \
--describe
# Heartbeat verification
kafka-console-consumer.sh --bootstrap-server target:9092 \
--topic heartbeats \
--from-beginning \
--max-messages 5
monitor-mm2-lag.sh
#!/bin/bash
CONNECT_HOST="localhost:8083"
TARGET_BOOTSTRAP="target:9092"
# Check connector status
STATUS=$(curl -s "http://${CONNECT_HOST}/connectors/MirrorSourceConnector/status" | jq -r '.connector.state')
echo "Connector status: ${STATUS}"
# Check consumer group lag
kafka-consumer-groups.sh --bootstrap-server ${TARGET_BOOTSTRAP} \
--group connect-MirrorSourceConnector \
--describe 2>/dev/null | \
awk 'NR>1 {sum += $6} END {print "Total lag: " sum}'

  1. Stop producers to source cluster

  2. Wait for replication to complete

Terminal window
# Check lag is zero
kafka-consumer-groups.sh --bootstrap-server target:9092 \
--group connect-MirrorSourceConnector \
--describe
  1. Stop MirrorMaker 2

  2. Translate consumer offsets

Terminal window
# For each consumer group
kafka-consumer-groups.sh --bootstrap-server target:9092 \
--group my-app \
--reset-offsets \
--to-offset <translated-offset> \
--topic source.my-topic \
--execute
  1. Redirect applications to target cluster

  2. Start consumers on target cluster

  1. Detect source cluster failure

  2. Stop MirrorMaker 2 (prevent split-brain when source recovers)

  3. Assess data loss

Terminal window
# Compare last checkpoint with consumer position
kafka-console-consumer.sh --bootstrap-server target:9092 \
--topic source.checkpoints.internal \
--from-beginning | tail -n 100
  1. Translate offsets using last known checkpoint

  2. Accept potential data loss (records between last checkpoint and failure)

  3. Redirect applications

  4. Start consumers

  1. Restore source cluster

  2. Configure reverse replication (target -> source)

  3. Replicate changes made during outage

  4. Stop reverse replication

  5. Redirect applications back to source

  6. Resume normal replication (source -> target)


IssueSymptomsResolution
Connector not startingFAILED stateCheck logs, verify connectivity, check credentials
High replication lagIncreasing lag metricIncrease tasks.max, tune producer/consumer settings
Topics not replicatingMissing topics on targetVerify topic pattern matches, check ACLs
Offset sync failingCheckpoint connector errorsVerify consumer groups exist, check permissions
Authentication failuresSASL errors in logsVerify credentials, check JAAS config
Terminal window
# View connector logs
docker logs kafka-connect 2>&1 | grep -i mirror
# Check source cluster connectivity
kafka-broker-api-versions.sh --bootstrap-server source:9092
# Verify topic exists on source
kafka-topics.sh --bootstrap-server source:9092 --describe --topic my-topic
# Check target cluster ACLs
kafka-acls.sh --bootstrap-server target:9092 --list
# Test producer to target
echo "test" | kafka-console-producer.sh --bootstrap-server target:9092 --topic test-topic
Terminal window
# Check task distribution
curl -s http://connect:8083/connectors/MirrorSourceConnector/tasks | jq .
# Monitor network throughput
iftop -i eth0 -f "port 9092"
# Check producer metrics
curl -s http://connect:8083/connectors/MirrorSourceConnector/tasks/0/status | jq '.trace'

clusters = primary, dr
primary.bootstrap.servers = primary-1:9092,primary-2:9092,primary-3:9092
dr.bootstrap.servers = dr-1:9092,dr-2:9092,dr-3:9092
primary->dr.enabled = true
primary->dr.topics = .*
primary->dr.topics.exclude = .*\.internal, __.*
primary->dr.groups = .*
sync.group.offsets.enabled = true
emit.checkpoints.enabled = true
replication.factor = 3
tasks.max = 10

Active-active replication requires careful provenance configuration to prevent infinite replication loops and enable consumers to distinguish data origins.

The DefaultReplicationPolicy automatically handles provenance by prefixing replicated topics with the source cluster alias:

Topic prefixing that preserves provenance in active-active replicationTopic prefixing that preserves provenance in active-active replicationEast ClusterWest Clusterorders(local)west.orders(from west)orders(local)east.orders(from east)Consumers in East see:- orders (local writes)- west.orders (from West)Consumers in West see:- orders (local writes)- east.orders (from East)MM2 adds"east." prefixMM2 adds"west." prefix

Loop Prevention: MM2 never replicates prefixed topics. east.orders in the West cluster is not replicated back to East because it already has the east. prefix, indicating it originated from East.

clusters = east, west
east.bootstrap.servers = east-1:9092,east-2:9092,east-3:9092
west.bootstrap.servers = west-1:9092,west-2:9092,west-3:9092
# East to West
east->west.enabled = true
east->west.topics = orders, events, users
# West to East
west->east.enabled = true
west->east.topics = orders, events, users
# Use DefaultReplicationPolicy (default) - adds cluster prefix
replication.policy.class = org.apache.kafka.connect.mirror.DefaultReplicationPolicy
# Both directions
sync.group.offsets.enabled = true
emit.checkpoints.enabled = true
emit.heartbeats.enabled = true
replication.factor = 3
tasks.max = 6
ClusterLocal TopicReplicated FromResulting Topic
Eastorders-orders
East-Westwest.orders
Westorders-orders
West-Easteast.orders

Consumers that need to see all data from both clusters must subscribe to both local and replicated topics:

// Consumer in East cluster wanting all orders globally
consumer.subscribe(Arrays.asList(
"orders", // Local East orders
"west.orders" // Replicated West orders
));
// Process with origin awareness
for (ConsumerRecord<String, Order> record : records) {
String origin = record.topic().startsWith("west.") ? "west" : "east";
processOrder(record.value(), origin);
}
StrategyDescriptionConfiguration
Topic prefixingEach DC writes to local topic, reads bothDefault behavior
Key partitioningRoute specific keys to owning DCApplication-level routing
Last-write-winsAccept all writes, latest timestamp winsApplication-level merge

Recommended Pattern

Use the default DefaultReplicationPolicy with topic prefixing. Have consumers subscribe to both local and prefixed topics. This provides clear provenance with no additional configuration.

clusters = central, region-us, region-eu, region-apac
central.bootstrap.servers = central:9092
region-us.bootstrap.servers = us:9092
region-eu.bootstrap.servers = eu:9092
region-apac.bootstrap.servers = apac:9092
central->region-us.enabled = true
central->region-us.topics = global-.*
central->region-eu.enabled = true
central->region-eu.topics = global-.*
central->region-apac.enabled = true
central->region-apac.topics = global-.*
replication.factor = 3
clusters = region-us, region-eu, region-apac, central
region-us.bootstrap.servers = us:9092
region-eu.bootstrap.servers = eu:9092
region-apac.bootstrap.servers = apac:9092
central.bootstrap.servers = central:9092
region-us->central.enabled = true
region-us->central.topics = events
region-eu->central.enabled = true
region-eu->central.topics = events
region-apac->central.enabled = true
region-apac->central.topics = events
# Central sees: region-us.events, region-eu.events, region-apac.events
replication.factor = 3