Replication: Document and Segment
A shard has a primary and zero or more replicas, and the replicas have to stay consistent with the primary. OpenSearch offers two fundamentally different ways to keep them in sync. The classic model is document replication: the primary indexes a write, then forwards the same operation to each replica, which indexes it independently. The newer model is segment replication: the primary indexes alone, and replicas copy the resulting Lucene segments from the primary instead of re-doing the indexing work. The choice trades CPU against a different durability/visibility story, and as a contributor you must understand both because half the engine's correctness arguments hinge on which one is active.
This chapter contrasts the two, dissects the document-replication write path
(TransportReplicationAction, ReplicationOperation, ReplicationTracker,
in-sync allocation IDs, the checkpoints), and the segment-replication services. It
builds on Recovery (which uses the same checkpoints) and feeds
Refresh, Flush, and Merge (segrep changes who merges).
After this chapter you can:
- Explain the document-replication primary→replica write path and how a write is acknowledged.
- Define in-sync allocation IDs, local/global checkpoints, and what they guarantee.
- Contrast document vs segment replication on CPU, durability, and freshness.
- Reason about when
index.replication.type: SEGMENTis the right call.
Two models at a glance
| Document replication (default) | Segment replication | |
|---|---|---|
| What crosses to replica | the indexing operation (the doc) | the Lucene segment files |
| Replica work | re-indexes the op (CPU + own merges) | copies segments (I/O), no re-indexing, no own merges |
| Setting | index.replication.type: DOCUMENT | index.replication.type: SEGMENT |
| Driver classes | TransportReplicationAction, ReplicationOperation | SegmentReplicationSourceService, SegmentReplicationTargetService |
| Freshness on replica | as fresh as the op forwarded (then refresh) | as fresh as the last copied checkpoint (lags primary) |
| Durability of replica copy | replica has the op in its own translog | replica relies on copied segments + primary's translog |
| Best for | low write-to-search latency, write-heavy with many replicas needing fresh reads | read-heavy, many replicas, reduce duplicate indexing CPU |
grep -rn "replication.type\|ReplicationType\|SEGMENT\|DOCUMENT" \
server/src/main/java/org/opensearch/indices/replication/common/ReplicationType.java \
server/src/main/java/org/opensearch/index/IndexSettings.java
Document replication: the write path
A write enters via TransportShardBulkAction → IndexShard.applyIndexOperationOnPrimary
→ InternalEngine.index(...) (Lucene + translog). The replication itself is
orchestrated by TransportReplicationAction / ReplicationOperation: the primary
applies the op locally first, assigns it a sequence number, then forwards it
to every replica in the in-sync set. The request is acknowledged to the client
after enough copies confirm (per the write wait_for_active_shards/consistency
rules).
sequenceDiagram
participant C as Client
participant P as Primary shard
participant R1 as Replica 1
participant R2 as Replica 2
C->>P: index op
Note over P: applyIndexOperationOnPrimary -> Engine.index -> assign seqNo + translog
par forward to in-sync replicas
P->>R1: replica request (same op + seqNo + primary term)
P->>R2: replica request
end
R1-->>P: applied (local checkpoint advances)
R2-->>P: applied
Note over P: ReplicationTracker advances global checkpoint = min(in-sync local checkpoints)
P-->>C: ack
grep -n "class ReplicationOperation\|performOnPrimary\|performOnReplica\|markUnavailableShardsAsStale\|getReplicationGroup" \
server/src/main/java/org/opensearch/action/support/replication/ReplicationOperation.java
grep -n "class TransportReplicationAction\|shardOperationOnPrimary\|shardOperationOnReplica" \
server/src/main/java/org/opensearch/action/support/replication/TransportReplicationAction.java
In-sync allocation IDs
The primary doesn't replicate to all assigned copies — only to those in the
in-sync set: copies that are known to have every op up to the global
checkpoint. The set is part of IndexMetadata (inSyncAllocationIds) and is
maintained through cluster-state updates. A replica that falls behind or fails is
removed from the in-sync set (so the primary stops blocking acks on it) and must
re-enter via recovery (markAllocationIdAsInSync after it catches
up). This is the mechanism that prevents acknowledging writes that a "replica"
never actually received.
grep -rn "inSyncAllocationIds\|in_sync_allocation\|markAllocationIdAsInSync\|removeAllocationId" \
server/src/main/java/org/opensearch/cluster/metadata/IndexMetadata.java \
server/src/main/java/org/opensearch/index/seqno/ReplicationTracker.java
The checkpoints (shared with recovery)
| Checkpoint | Definition | Owner |
|---|---|---|
| local checkpoint | highest seqno below which a copy has contiguously applied every op | each copy, LocalCheckpointTracker |
| global checkpoint | min of in-sync copies' local checkpoints — every op ≤ it is on all in-sync copies | primary, ReplicationTracker |
| primary term | monotonically increasing generation of "who is primary" | cluster, bumped on primary failover |
The primary term guards against split-brain writes: a stale ex-primary's replica requests carry an old term and are rejected. Operations below the global checkpoint are durable across the in-sync set and are what recovery builds on.
grep -n "primaryTerm\|getGlobalCheckpoint\|getLocalCheckpoint\|computeGlobalCheckpoint" \
server/src/main/java/org/opensearch/index/seqno/ReplicationTracker.java
Segment replication: copy segments, don't re-index
With index.replication.type: SEGMENT, only the primary indexes documents
(runs Lucene IndexWriter, refreshes, and merges). After a refresh produces new
segments, the primary publishes a new ReplicationCheckpoint; replicas fetch the
new/changed segment files from the primary and open a reader over them. The
replica never re-indexes and never runs its own merges — it just mirrors files.
flowchart TD
subgraph Primary
W[index ops] --> IW[Lucene IndexWriter]
IW -->|refresh| SEG[new segments]
SEG --> CP[publish ReplicationCheckpoint]
end
CP -->|notify| TS[SegmentReplicationTargetService on replica]
TS -->|request diff| SRC[SegmentReplicationSourceService on primary]
SRC -->|stream missing segment files| TS
TS --> RR[replica opens reader over copied segments]
RR --> V[replica search now sees primary's segments]
| Concern | Class | grep target |
|---|---|---|
| Replica side: drives copy | SegmentReplicationTargetService + SegmentReplicationTarget | server/src/main/java/org/opensearch/indices/replication/ |
| Primary side: serves segments | SegmentReplicationSourceService | same dir |
| The version marker | ReplicationCheckpoint | .../replication/checkpoint/ReplicationCheckpoint.java |
| Publish-checkpoint action | PublishCheckpointAction | .../replication/checkpoint/ |
ls server/src/main/java/org/opensearch/indices/replication/
grep -n "class SegmentReplicationTargetService\|onNewCheckpoint\|startReplication\|class ReplicationCheckpoint" \
server/src/main/java/org/opensearch/indices/replication/SegmentReplicationTargetService.java \
server/src/main/java/org/opensearch/indices/replication/checkpoint/ReplicationCheckpoint.java
Why segrep changes the story
- CPU: indexing and merging happen once on the primary, not on every
replica. For a 1-primary-5-replica read-heavy index, that's a large CPU/IO
saving. (See Refresh, Flush, and Merge: replicas
don't run
TieredMergePolicy.) - Freshness: a replica is only as fresh as the last checkpoint it copied. There is inherent replication lag — a search hitting a replica may return slightly staler results than one hitting the primary. Document replication forwards each op, so replicas can refresh to near-primary freshness.
- Durability/visibility: the replica's searchable state is the primary's segments, not ops it indexed itself. Writes are still made durable via the primary's translog; the replica doesn't independently re-derive segments. This is why the durability argument must be reasoned about per replication type.
Warning: Under segment replication, "read your write" is not guaranteed against a replica until the relevant checkpoint has been copied. If your test indexes a doc and immediately searches a replica, it can legitimately miss — this is a common source of "flaky" integration tests that are actually correct behavior. Search the primary, or wait for the checkpoint.
Comparison summary
flowchart LR
subgraph DOC[Document replication]
DP[Primary indexes op] --> DF[forward op to replicas]
DF --> DR[each replica re-indexes + merges]
end
subgraph SEG[Segment replication]
SP[Primary indexes + merges] --> SCP[publish checkpoint]
SCP --> SR[replicas copy segments]
end
| Axis | Document | Segment |
|---|---|---|
| Indexing CPU | paid N+1 times (primary + N replicas) | paid once (primary) |
| Merge CPU/IO | paid on every copy | paid on primary only |
| Network | small (ops) | larger (segment files) |
| Replica freshness | near-primary after refresh | lags by checkpoint interval |
| Read-your-write on replica | strong after refresh | not guaranteed until checkpoint copied |
Reading exercise
# 1. The replication operation: primary then in-sync replicas
grep -n "ReplicationOperation\|performOnReplicas\|getReplicationGroup\|onSuccessfulReplica" \
server/src/main/java/org/opensearch/action/support/replication/ReplicationOperation.java
# 2. In-sync set + global checkpoint
grep -n "inSyncAllocationIds\|markAllocationIdAsInSync\|updateGlobalCheckpointOnPrimary\|computeGlobalCheckpoint" \
server/src/main/java/org/opensearch/index/seqno/ReplicationTracker.java
# 3. Segment replication target flow
grep -n "onNewCheckpoint\|startReplication\|getCheckpoint\|forceReplication" \
server/src/main/java/org/opensearch/indices/replication/SegmentReplicationTargetService.java
# 4. Where the replication type is chosen and what it gates
grep -rn "ReplicationType\|isSegRepEnabled\|replication.type" \
server/src/main/java/org/opensearch/index/IndexSettings.java
Answer:
- In document replication, after the primary applies an op locally, how does it decide which replicas to forward to? Name the set and the class that owns it.
- Define the global checkpoint as a function of local checkpoints, and explain why an op below it is safe.
- What role does the primary term play when a former primary's stale request arrives at a replica? Why is this essential for correctness?
- In segment replication, what triggers a replica to start copying, and what object tells it what to copy?
- Explain precisely why "index a doc, immediately search a replica" can return zero hits under segment replication but (after refresh) not under document replication.
- For a read-heavy index with 1 primary and 5 replicas, argue from CPU and merge cost why segment replication is attractive, and name the cost you accept.
Common bugs and symptoms
| Symptom | Likely cause | Where to look |
|---|---|---|
| Replica returns staler results than primary | segment replication lag (checkpoint not yet copied) | _cat/segment_replication, ReplicationCheckpoint |
| Flaky IT: index then search replica misses doc | segrep read-your-write not guaranteed; test assumption wrong | search primary or assertBusy on checkpoint |
| Write acked but lost on primary failover | replica wasn't actually in-sync; or stale-primary write | inSyncAllocationIds, primary term |
Writes blocked / not enough active shards | wait_for_active_shards can't be satisfied | replication settings, allocation |
| High CPU across all replicas under heavy indexing (doc rep) | every replica re-indexes + merges | consider segment replication |
| Replica stuck behind, growing lag (segrep) | source can't keep up streaming segments / network | SegmentReplicationSourceService, throttling |
| Replica rejected after old node rejoins | stale primary term on its requests | ReplicationTracker term checks |
Validation: prove you understand this
- Diagram the document-replication write path from client to ack, marking where the seqno is assigned, where the global checkpoint advances, and what condition gates the ack.
- Define in-sync allocation IDs and explain what would go wrong if the primary acked writes to copies not in the in-sync set.
- Give the formula for the global checkpoint in terms of in-sync local checkpoints and explain why a lagging in-sync replica holds the global checkpoint back.
- Draw both replication models side by side, labeling what crosses the wire and who runs Lucene merges in each.
- Explain the read-your-write difference between the two models with a concrete test scenario, and how you'd write a correct integration test for a segment-replicated index.
- Recommend document vs segment replication for (a) a write-heavy log index with 1 replica needing fresh reads, and (b) a read-heavy catalog index with 6 replicas. Justify each from CPU, freshness, and network.