The Storage Engine — Intensive

How does a shard actually land on disk? You have read that the engine drives a Lucene IndexWriter, that the translog makes writes durable, and that a flush commits Lucene. This masterclass goes one layer lower: it is about the bytes — the directory tree on disk, the Store object that wraps Lucene's Directory, the FsDirectoryFactory that decides whether a file is memory-mapped or read with positional I/O, the CRC32 checksum footer on every Lucene file, and the chain of fsync calls that turns an acknowledged write into something that survives a power cut.

This chapter extends three deep-dives. Read them first if you have not: engine-internals (the engine that calls the storage layer), translog (the write-ahead log), and refresh-flush-merge (the commit). And it builds on the Lucene file zoo from segments-and-codecs. Here we connect the Lucene Directory SPI to the OS — page cache, mmap, vm.max_map_count — and to OpenSearch's Store integrity layer.

After this chapter you can: name the on-disk path of any shard file; explain what index.store.type: hybridfs mmaps and what it does not; reason about when mmap helps and when it hurts; trace a write from Translog.add through fsync to a Lucene commit; explain the CRC32 footer and how Store.MetadataSnapshot uses it during recovery; and say what changes when the shard is remote-backed.

Note: "Durable" (survives a crash) and "visible" (searchable) are two different milestones, and this chapter is about durability and integrity — the bytes. Visibility (refresh) is covered in refresh-flush-merge. Do not conflate them.

The labs that go with this chapter:


First principles: a shard is a Lucene index plus a translog

Strip away the cluster, the coordinator, the REST layer. At the bottom, a single shard is exactly two things on a single node's filesystem:

  1. A Lucene index — a directory of immutable segment files plus a segments_N commit point (see segments-and-codecs).
  2. A translog — a per-shard append-only write-ahead log (translog).

Everything in this chapter is about how those two are persisted, accessed, and verified. Lucene never writes to a raw file path directly; it writes through an abstraction called a Directory. OpenSearch wraps that Directory in a Store, which adds reference counting, checksum verification, and a metadata snapshot used by recovery. So the layering, top to bottom, is:

flowchart TD
    Engine["InternalEngine (drives IndexWriter)"] --> Store["org.opensearch.index.store.Store (ref-count, checksums, metadata)"]
    Store --> Dir["Lucene Directory (the SPI)"]
    Dir --> Impl["MMapDirectory / NIOFSDirectory (the impl FsDirectoryFactory picked)"]
    Impl --> OS["OS: page cache + filesystem"]
    OS --> Disk[("disk: .cfs .tim .doc .kdd .vec ... + translog")]

Every concept in this chapter lives on one of those rungs. Store is the OpenSearch rung; Directory/MMapDirectory/NIOFSDirectory are the Lucene rung; the page cache and fsync are the OS rung.

# Locate the three classes that define the storage layer (in an OpenSearch checkout):
find server -name "Store.java"               -path "*index/store*"
find server -name "FsDirectoryFactory.java"  -path "*index/store*"
grep -rn "class MMapDirectory\|class NIOFSDirectory\|class FSDirectory" \
  lucene/core/src/java/org/apache/lucene/store/

The Store: OpenSearch's wrapper around a Lucene Directory

org.opensearch.index.store.Store is the object the engine holds. It does not itself read or write segment bytes — Lucene's Directory does that. Store adds the things Lucene's Directory deliberately does not:

Store responsibilityWhy it lives here, not in Lucene
Reference counting (incRef/decRef, AbstractRefCounted)A shard's Store is shared by the engine, recovery, and snapshot code; the directory must not close while any of them hold it.
Checksum verification (verify, checkIntegrityNoException)OpenSearch verifies the CodecUtil CRC32 footer of each file on recovery and snapshot restore, beyond what Lucene checks on open.
MetadataSnapshotA map of filename → (length, checksum, Lucene version) for every file in the latest commit — the basis of recovery diffing (see below).
StoreFileMetadataOne file's identity: name, length, checksum string, the Lucene version that wrote it, and (for segment files) the segment's writtenBy.
checkIndex / corruption markerRuns Lucene's CheckIndex on demand; writes a corrupted_* marker file so a known-bad shard is not silently reused.
grep -n "class Store\|class MetadataSnapshot\|class StoreFileMetadata\|incRef\|decRef\|checkIndex\|verify(\|getMetadata\|markStoreCorrupted" \
  server/src/main/java/org/opensearch/index/store/Store.java | head -40

The single most important method to understand is getMetadata(...), which builds a MetadataSnapshot. It reads the segments_N, walks each segment's files, and records each file's checksum (cheaply — the checksum is read from the file's footer, not recomputed over the whole file). That snapshot is what lets a recovering replica ask the primary "which of your files do I already have, byte for byte?" — covered under recovery.


FsDirectoryFactory and index.store.type

When a shard opens, OpenSearch must turn the abstract Directory into a concrete implementation. That decision is org.opensearch.index.store.FsDirectoryFactory, driven by the index setting index.store.type.

grep -n "index.store.type\|hybridfs\|mmapfs\|niofs\|simplefs\|HYBRIDFS\|MMAPFS\|NIOFS\|newFSDirectory\|preLoad\|HybridDirectory" \
  server/src/main/java/org/opensearch/index/store/FsDirectoryFactory.java | head -40
index.store.typeImplementationReads via
hybridfs (default)a HybridDirectory (mmap for some extensions, NIO for the rest)mmap for the hot files, pread for the rest
mmapfsMMapDirectorymmap for all files
niofsNIOFSDirectorypositional FileChannel.read (pread) for all files
simplefsSimpleFSDirectory (deprecated, single-threaded RandomAccessFile)synchronized RandomAccessFile — avoid

Note: hybridfs is the default for a reason: it gives you the speed of mmap on the files that benefit most (term dictionaries, doc-values, KD-trees, vectors) without exhausting the process's virtual-memory mappings on files where mmap buys little. Most clusters should never change it.

Exactly which extensions does hybridfs mmap?

HybridDirectory keeps a set of extensions that go to MMapDirectory; everything else goes to NIOFSDirectory. The mmapped set is the random-access, frequently re-read structures:

Extension(s)StructureWhy mmap it
tim / tipterms dictionary + FST indexrandom term lookups, re-read constantly
doc / pos / paypostingsthe hot path of every query
dvddoc-values datacolumnar reads for sort/aggs
cfscompound file (packs many small files)one mapping covers many sub-files
kdd / kdi / kdmBKD points treerange/geo queries do many random reads
vec / vex / vemHNSW vectors + graphk-NN graph traversal is pure random access
nvdnorms dataread during scoring

Everything else — fdt/fdx (stored fields / _source), liv (live docs), si, fnm, the segments_N — is opened with NIOFSDirectory. Stored fields are the big one to notice: _source is fetched only for the handful of hits you return, so streaming it with pread (and not consuming a long-lived mmap region for the whole .fdt) is the right call.

Warning: The exact extension set is a static final Set in HybridDirectory and it has changed across versions (vectors and points were added as those features matured). Do not memorize it — grep your checkout:

grep -n "tim\|tip\|dvd\|cfs\|kdd\|kdi\|kdm\|vec\|vex\|vem\|doc\|pos\|pay\|nvd\|Set.of\|getExtension\|useDelegate" \
  server/src/main/java/org/opensearch/index/store/FsDirectoryFactory.java

The dispatch logic is in HybridDirectory.openInput: it pulls the extension off the filename, checks it against the mmap set, and routes to the mmap or NIO delegate accordingly. Lab ST3 proves this by pmap-ing the live process and seeing exactly the .cfs/.tim/.dvd/.vec files mapped and the .fdt not mapped.


MMapDirectory vs NIOFSDirectory: two ways to read a file

This is the heart of the store-type story. Both are FSDirectory subclasses; they differ in how a byte at offset N is fetched.

MMapDirectoryNIOFSDirectory
Mechanismmmap(2) the file into the address space; reads are pointer dereferences into mapped pagesFileChannel.read(ByteBuffer, position) — a positional pread(2) syscall per read
Backing in LuceneMemorySegmentIndexInput (via the Panama Foreign Memory API on modern JDKs)NIOFSIndexInput wrapping a FileChannel
Page cacheYes — mapped pages are page-cache pages; no copy into heapYes — the kernel caches, but each read copies kernel→user buffer
Cost modelNear-zero per-read overhead once pages are resident; page faults on cold pagesOne syscall + one copy per read, always
Virtual memoryConsumes address space and a kernel VMA per mapping (vm.max_map_count)Negligible address space
Failure modeOutOfMemoryError: Map failed / mapping limit; SIGBUS if file truncated under itnormal IOExceptions

The crucial property of mmap is that a memory-mapped file shares pages with the OS page cache. When Lucene reads a term-dictionary block, the bytes are already in RAM (because some other query touched them), and the read is a CPU load instruction — no syscall, no copy. This is why mmap dominates for the random-access structures and why hybridfs mmaps exactly those.

flowchart LR
    subgraph "MMapDirectory"
      Q1[query reads term block] --> P[pointer into mapped page]
      P --> PC1[page cache page]
      PC1 -. page fault if cold .-> D1[("disk")]
    end
    subgraph "NIOFSDirectory"
      Q2[query reads term block] --> SC[pread syscall]
      SC --> Copy[copy kernel buffer to heap]
      Copy --> PC2[page cache]
      PC2 -. read if cold .-> D2[("disk")]
    end

Note: mmap does not load the file into heap and does not count against the JVM heap. The mapped data lives in OS page cache (off-heap, reclaimable by the kernel). This is why an OpenSearch node wants a small heap and large free RAM: the leftover RAM is the page cache that backs every mmap. People who give OpenSearch 90% of RAM as heap starve the page cache and make mmap useless.

index.store.preload

By default the OS faults pages in lazily — the first query that touches a cold term block pays a page-fault + disk read. index.store.preload tells MMapDirectory to pre-fault (warm) chosen extensions on open, by touching every page:

# Warm term dictionaries and doc-values on open (per-index or node default):
curl -s -XPUT 'localhost:9200/orders/_settings' -H 'Content-Type: application/json' -d'
{ "index.store.preload": ["tim","tip","dvd","kdd","vec"] }'
# Or node-level in opensearch.yml:  index.store.preload: ["nvd", "dvd", "tim", "tip"]

It maps to Lucene MMapDirectory.setPreload(...), which calls MemorySegment.load() on the mapping. Use it sparingly: ["*"] preloads everything and will read your entire index off disk on every shard open, which can be slower overall and pointless for files the workload never touches. Lab ST3 measures the warm-up difference.

vm.max_map_count

Each mmap region costs one kernel VMA. A node with many shards × many segments × many mmapped extensions can blow past Linux's default vm.max_map_count (often 65530). The symptom is shards failing to open with Map failed / OutOfMemoryError. OpenSearch's bootstrap check requires at least 262144:

# Check and raise (Linux):
sysctl vm.max_map_count
sudo sysctl -w vm.max_map_count=262144
echo 'vm.max_map_count=262144' | sudo tee -a /etc/sysctl.conf

This is purely a mmapfs/hybridfs concern. niofs does not consume VMAs, which is one (rare) reason to choose it. See Lab ST3 troubleshooting.


The on-disk shard layout

Now the directory tree itself. Find the data path, then walk it:

curl -s 'localhost:9200/_nodes/settings?filter_path=**.path.data' | python3 -m json.tool
# Then, under <data>:
find <data>/nodes/0/indices -maxdepth 3 -type d | head

The layout (a single-node ./gradlew run puts everything under server/build/run or your configured path.data):

<data>/
  nodes/
    0/
      _state/                       <- NODE-level state (node metadata, manifest)
        node-N.st
        ...
      indices/
        <index-uuid>/               <- one dir per index, named by UUID not name
          _state/                   <- INDEX-level metadata (mappings, settings)
            state-N.st
          0/                        <- SHARD 0
            index/                  <- the Lucene index (segment files)
              segments_5
              _3.cfs  _3.cfe  _3.si
              _7.tim  _7.tip  _7.doc  _7.dvd  _7.kdd  _7.vec  ...
              write.lock
            translog/               <- the write-ahead log
              translog-12.tlog
              translog-13.tlog      <- current generation
              translog.ckp          <- checkpoint (which gen, offsets, seq nos)
            _state/                 <- SHARD state (allocation id, primary term)
              state-N.st
          1/                        <- SHARD 1 (if this node holds it)
            index/  translog/  _state/
flowchart TD
    Node["nodes/0/"] --> NState["_state/ (node manifest)"]
    Node --> Indices["indices/"]
    Indices --> IDX["<index-uuid>/"]
    IDX --> IState["_state/ (mappings, settings)"]
    IDX --> S0["0/ (shard)"]
    S0 --> Lucene["index/ (segments_N, .cfs, .tim, .doc, .kdd, .vec, write.lock)"]
    S0 --> TL["translog/ (.tlog, translog.ckp)"]
    S0 --> SState["_state/ (allocation id, primary term)"]
PathWritten byHolds
nodes/0/_state/PersistedClusterStateService / node metadatanode id, the on-disk cluster-state manifest
indices/<uuid>/_state/MetaStateServicethe IndexMetadata: mappings, settings, aliases
indices/<uuid>/<shard>/index/Lucene IndexWriter via the Store's Directoryall segment files + segments_N + write.lock
indices/<uuid>/<shard>/translog/TranslogWriter.tlog generations + translog.ckp
indices/<uuid>/<shard>/_state/IndexShard / ShardStateMetadataallocation id, primary term, the "is-primary" flag

Note: Indices are stored under their UUID, not their name, so an index can be deleted and recreated with the same name without colliding on disk. Map name→uuid with curl 'localhost:9200/_cat/indices?v&h=index,uuid'. Lab ST1 walks this tree end to end.

The .st files are OpenSearch's own metadata format (MetadataStateFormat, length-prefixed + checksummed XContent), not Lucene segment files. The write.lock in index/ is the Lucene IndexWriter lock — exactly one writer per directory.


Durability: translog + fsync + Lucene commit

Persistence is a two-tier story, and you already met both tiers in the deep-dives. This chapter ties them to the actual fsync calls.

  1. Translog tier (per write). Each op is appended to the current .tlog and, under default index.translog.durability: request, fsynced before the write is acknowledged. That fsync is the durability guarantee for an acked write — full detail in translog.
  2. Lucene tier (per flush). A flush calls IndexWriter.commit(), which writes the new segments_N and fsyncs every referenced segment file plus the commit file, then rolls the translog generation and trims the old ones.
sequenceDiagram
    participant E as InternalEngine.index
    participant IW as Lucene IndexWriter
    participant T as Translog (.tlog)
    participant FS as filesystem
    E->>IW: addDocument / updateDocument (in-RAM buffer)
    E->>T: Translog.add(op) -> Location
    Note over T,FS: durability=request: fsync NOW
    T->>FS: fsync(translog-N.tlog)
    FS-->>T: synced
    T-->>E: durable
    E-->>E: 200 OK
    Note over IW,FS: ... later, on flush ...
    E->>IW: commit()
    IW->>FS: fsync(_7.cfs, _7.si, ... )
    IW->>FS: write + fsync(segments_6)
    FS-->>IW: committed
    IW->>T: roll generation, trim old .tlog

The key fact the diagram encodes: the per-write fsync is on the translog, not on Lucene. Lucene only fsyncs on commit (flush). That is the entire reason the translog exists — committing Lucene per write would be ruinously slow. An acked write is durable because its op is in an fsynced .tlog, recoverable by replay even though Lucene has not committed it. Lab ST2 proves this by killing the JVM between an acked write and the next flush, then watching the translog replay restore it.

Two flush triggers to remember (grep IndexSettings.java):

grep -n "flush_threshold_size\|FLUSH_THRESHOLD\|translog.flush" \
  server/src/main/java/org/opensearch/index/IndexSettings.java

index.translog.flush_threshold_size (default 512mb) auto-flushes when the translog grows; an explicit POST /index/_flush forces one.


Every Lucene file is written with a header and a footer by CodecUtil. The footer's last 8 bytes hold a CRC32 checksum of everything before it. This is not optional metadata — it is how Lucene and OpenSearch detect a corrupt file.

grep -n "writeFooter\|checkFooter\|retrieveChecksum\|writeIndexHeader\|checkIndexHeader\|FOOTER_MAGIC\|CRC32" \
  lucene/core/src/java/org/apache/lucene/codecs/CodecUtil.java | head
CodecUtil methodWhat it does
writeIndexHeadermagic + codec name + version + segment id/suffix at the file start
writeFootera footer magic + algorithm id + the CRC32 of all preceding bytes
checkFooterreads the footer, recomputes/compares CRC, throws CorruptIndexException on mismatch
retrieveChecksumreads just the footer's stored CRC without re-reading the file (this is what Store.getMetadata uses)
checksumEntireFilerecomputes the CRC over the whole file (the expensive, thorough check)

The clever bit: Store.MetadataSnapshot reads each file's checksum via retrieveChecksum — it seeks to the footer and reads the stored CRC, an O(1) operation, not an O(file-size) scan. That makes building a full index metadata map cheap, which makes recovery diffing cheap.

flowchart LR
    F["a Lucene file _7.tim"] --> H["header: magic + codec + version + segId"]
    H --> Body["body: the actual terms-dict bytes"]
    Body --> Foot["footer: footer-magic + algId + CRC32(header+body)"]
    Foot --> Verify{checkFooter}
    Verify -->|match| OK[trusted]
    Verify -->|mismatch| Corrupt[CorruptIndexException]

Store.MetadataSnapshot and recovery diffing

When a replica recovers from a primary (recovery), it does not blindly copy the whole index. The primary sends its MetadataSnapshot; the replica builds its own; they diff. The diff classifies each file as:

BucketMeaningAction
identicalsame name, length, and checksumskip — already have it
differentsame name, different checksum/lengthre-copy
missingprimary has it, replica does notcopy

Because segment files are immutable and content-addressed by checksum, "same name

  • same checksum" is a safe byte-for-byte identity. Only the differing/missing files cross the wire. This is the storage-layer foundation of fast peer recovery and segment replication.
grep -n "class MetadataSnapshot\|RecoveryDiff\|public RecoveryDiff diff\|identical\|different\|missing" \
  server/src/main/java/org/opensearch/index/store/Store.java

CheckIndex and CorruptIndexException

org.apache.lucene.index.CheckIndex is the offline (and online, via Store.checkIndex) tool that opens an index and verifies every segment's structures and checksums, reporting any CorruptIndexException. OpenSearch can run it on shard open via index.shard.check_on_startup (false / checksum / true), and when it finds corruption it writes a corrupted_<uuid> marker into index/ so the shard is failed rather than silently serving bad data.

grep -n "check_on_startup\|CheckIndex\|markStoreCorrupted\|CORRUPTED_MARKER\|isMarkedCorrupted" \
  server/src/main/java/org/opensearch/index/store/Store.java \
  server/src/main/java/org/opensearch/index/shard/IndexShard.java | head

You will run CheckIndex by hand in Lab ST1:

# Against a *copy* of a shard's index dir (never the live one):
java -cp "$(find ~/.gradle -name 'lucene-core-*.jar' | head -1)" \
  -ea org.apache.lucene.index.CheckIndex /tmp/shard-copy/index

How remote-backed storage changes all of this

Everything above assumes the durable copy lives on the node's local disk. With remote-backed storage (see remote-store-and-durability) the source of truth moves to a remote object store (S3 and friends), and the local disk becomes a cache:

Local-onlyRemote-backed
Durability = local translog fsync + local Lucene commitDurability = translog and segments uploaded to the remote store
Recovery = peer copy of local segmentsRecovery = download segments + translog from the remote store
MetadataSnapshot diff happens node↔nodeMetadata lives in the remote store; local is a cache to refill
A lost node ⇒ rebuild from a replicaA lost node ⇒ rebuild from the object store (replicas can be cheaper/fewer)

The Store/Directory abstraction is what makes this clean: OpenSearch slots a remote-aware directory (and a remote translog) under the same Store API. The CRC footers, the MetadataSnapshot, the commit/flush mechanics — all unchanged; only where the durable bytes live changes. Read the engineering chapter for the upload path, the segment-vs-translog split, and the consistency model.


Worked example: a write's full journey to disk

Trace one document, _id=order-42, into index orders (8 shards), from REST to fsynced bytes:

  1. Coordinator routes it (see sharding-routing) to, say, shard 2 on node B.
  2. InternalEngine.index() calls IndexWriter.updateDocument(...) — the doc lands in the in-RAM buffer, not yet on disk, not yet searchable.
  3. Translog.add(op) appends to nodes/0/indices/<uuid>/2/translog/translog-13.tlog.
  4. Default request durability ⇒ fsync(translog-13.tlog). Now durable. The 200 OK returns. A kill -9 here loses nothing — replay restores it.
  5. A refresh (default ~1s) opens a new reader; the doc becomes searchable (still no Lucene commit).
  6. Eventually a flush: IndexWriter.commit() fsyncs the new segment files (_8.cfs _8.si ...) and writes+fsyncs segments_6 in 2/index/; the translog rolls to gen 14 and old gens are trimmed.
  7. From now on, that doc is recovered from the Lucene commit, not the translog.

Lab ST2 makes steps 4–7 observable: kill before 6, restart, and _recovery plus the logs show the translog-replay path; index more, flush, kill again, and recovery loads from the commit with zero translog ops.


Common bugs and symptoms

SymptomRoot causeWhere to look
Shards fail to open: Map failed / OutOfMemoryError on a healthy-disk nodevm.max_map_count too low for the number of mmapped filessysctl vm.max_map_count; raise to 262144; or switch hot indices to niofs
High heap, poor query latency, disk reads on every queryHeap too large ⇒ tiny page cache ⇒ mmap pages not residentshrink heap to leave RAM for page cache; mmap needs free RAM
Slow first queries after a restartCold page cache; nothing pre-faultedindex.store.preload for tim/tip/dvd/kdd/vec; expect a warm-up
CorruptIndexException / shard won't allocateCRC32 footer mismatch — bit rot, bad disk, truncated writethe corrupted_* marker; CheckIndex; replace disk; recover from replica/remote
Disk full of .tlog filesTranslog not trimming (flush not happening / global checkpoint stuck)translog; flush_threshold_size; lagging replica
_source fetches slow but term queries fast.fdt is NIO (correct), but best_compression + large _sourceexpected; consider index.codec trade-off (segments-and-codecs)
Recovery copies the whole index every timeMetadataSnapshot diff finding everything "different" — checksums not matching (codec mismatch / corruption)Store.getMetadata; segment .si Lucene version; recovery
Switching to niofs didn't help latencyThe hot structures want mmap; NIO adds a syscall+copy per readrevert to hybridfs; NIO helps mainly to dodge max_map_count, not latency

Reading exercise

# 1. The Store and its metadata snapshot.
grep -n "class Store\|class MetadataSnapshot\|getMetadata\|RecoveryDiff\|incRef\|decRef\|markStoreCorrupted" \
  server/src/main/java/org/opensearch/index/store/Store.java

# 2. Store-type dispatch and the hybridfs extension set.
grep -n "index.store.type\|hybridfs\|HybridDirectory\|openInput\|getExtension\|preLoad" \
  server/src/main/java/org/opensearch/index/store/FsDirectoryFactory.java

# 3. The two Lucene directory impls.
grep -rn "class MMapDirectory\|class NIOFSDirectory\|setPreload\|MemorySegment" \
  lucene/core/src/java/org/apache/lucene/store/

# 4. The checksum footer.
grep -n "writeFooter\|checkFooter\|retrieveChecksum\|checksumEntireFile\|CorruptIndexException" \
  lucene/core/src/java/org/apache/lucene/codecs/CodecUtil.java

# 5. Live on-disk inspection.
curl -s 'localhost:9200/_nodes/settings?filter_path=**.path.data' | python3 -m json.tool
find <data>/nodes/0/indices -maxdepth 3 -type d

Answer:

  1. Draw the layering from InternalEngine down to disk and name the class at each rung (Store, Directory, MMapDirectory/NIOFSDirectory, page cache).
  2. What four things does Store add that Lucene's Directory does not?
  3. Which file extensions does hybridfs mmap, and which does it leave on NIO? Why is .fdt (stored fields) on the NIO side?
  4. Contrast MMapDirectory and NIOFSDirectory mechanically. Why does mmap want a small JVM heap?
  5. Explain vm.max_map_count: what consumes a VMA, what the symptom of running out is, and why niofs sidesteps it.
  6. Walk a write to disk: at which exact step (and which fsync) does an acked write become durable, and when does the Lucene commit fsync happen?
  7. Explain the CRC32 footer and how Store.MetadataSnapshot reads a checksum without scanning the whole file. How does that enable recovery diffing?
  8. State one thing remote-backed storage changes and one thing it leaves identical.

Validation: prove you understand this

  • Reproduce the on-disk shard layout from memory: the node _state/, the index <uuid>/_state/, and the shard's index/ + translog/ + _state/.
  • On a running node, find a real shard, then point at the file that is the Lucene commit and explain what MMapDirectory vs NIOFSDirectory would do for _7.tim vs _7.fdt.
  • In Lab ST3, pmap the node and show the mmapped .cfs/.tim/.dvd/.vec and the un-mapped .fdt, then explain it from the hybridfs extension set.
  • In Lab ST2, kill the JVM after an acked write but before a flush, restart, and show the translog-replay log lines — then explain which fsync made that write recoverable.
  • Corrupt one byte in a copy of a segment file, run CheckIndex, and read the CorruptIndexException; explain which CRC32 footer caught it.
  • State precisely what changes and what stays the same when the shard becomes remote-backed (remote-store-and-durability).