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:
- Lab ST1: Dissect a Shard's Data Directory —
findthe files, map every extension, runCheckIndex. - Lab ST2: Translog and Crash Recovery —
kill -9before a flush and watch replay. - Lab ST3: Store Types and Memory Mapping —
pmapthe mmapped files, tuneindex.store.preloadandvm.max_map_count.
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:
- A Lucene index — a directory of immutable segment files plus a
segments_Ncommit point (see segments-and-codecs). - 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 responsibility | Why 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. |
MetadataSnapshot | A map of filename → (length, checksum, Lucene version) for every file in the latest commit — the basis of recovery diffing (see below). |
StoreFileMetadata | One file's identity: name, length, checksum string, the Lucene version that wrote it, and (for segment files) the segment's writtenBy. |
checkIndex / corruption marker | Runs 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.type | Implementation | Reads via |
|---|---|---|
hybridfs (default) | a HybridDirectory (mmap for some extensions, NIO for the rest) | mmap for the hot files, pread for the rest |
mmapfs | MMapDirectory | mmap for all files |
niofs | NIOFSDirectory | positional FileChannel.read (pread) for all files |
simplefs | SimpleFSDirectory (deprecated, single-threaded RandomAccessFile) | synchronized RandomAccessFile — avoid |
Note:
hybridfsis 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) | Structure | Why mmap it |
|---|---|---|
tim / tip | terms dictionary + FST index | random term lookups, re-read constantly |
doc / pos / pay | postings | the hot path of every query |
dvd | doc-values data | columnar reads for sort/aggs |
cfs | compound file (packs many small files) | one mapping covers many sub-files |
kdd / kdi / kdm | BKD points tree | range/geo queries do many random reads |
vec / vex / vem | HNSW vectors + graph | k-NN graph traversal is pure random access |
nvd | norms data | read 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 SetinHybridDirectoryand 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.
MMapDirectory | NIOFSDirectory | |
|---|---|---|
| Mechanism | mmap(2) the file into the address space; reads are pointer dereferences into mapped pages | FileChannel.read(ByteBuffer, position) — a positional pread(2) syscall per read |
| Backing in Lucene | MemorySegmentIndexInput (via the Panama Foreign Memory API on modern JDKs) | NIOFSIndexInput wrapping a FileChannel |
| Page cache | Yes — mapped pages are page-cache pages; no copy into heap | Yes — the kernel caches, but each read copies kernel→user buffer |
| Cost model | Near-zero per-read overhead once pages are resident; page faults on cold pages | One syscall + one copy per read, always |
| Virtual memory | Consumes address space and a kernel VMA per mapping (vm.max_map_count) | Negligible address space |
| Failure mode | OutOfMemoryError: Map failed / mapping limit; SIGBUS if file truncated under it | normal 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)"]
| Path | Written by | Holds |
|---|---|---|
nodes/0/_state/ | PersistedClusterStateService / node metadata | node id, the on-disk cluster-state manifest |
indices/<uuid>/_state/ | MetaStateService | the IndexMetadata: mappings, settings, aliases |
indices/<uuid>/<shard>/index/ | Lucene IndexWriter via the Store's Directory | all segment files + segments_N + write.lock |
indices/<uuid>/<shard>/translog/ | TranslogWriter | .tlog generations + translog.ckp |
indices/<uuid>/<shard>/_state/ | IndexShard / ShardStateMetadata | allocation 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.
- Translog tier (per write). Each op is appended to the current
.tlogand, under defaultindex.translog.durability: request, fsynced before the write is acknowledged. That fsync is the durability guarantee for an acked write — full detail in translog. - Lucene tier (per flush). A flush
calls
IndexWriter.commit(), which writes the newsegments_Nand 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.
Integrity: the CodecUtil CRC32 footer
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 method | What it does |
|---|---|
writeIndexHeader | magic + codec name + version + segment id/suffix at the file start |
writeFooter | a footer magic + algorithm id + the CRC32 of all preceding bytes |
checkFooter | reads the footer, recomputes/compares CRC, throws CorruptIndexException on mismatch |
retrieveChecksum | reads just the footer's stored CRC without re-reading the file (this is what Store.getMetadata uses) |
checksumEntireFile | recomputes 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:
| Bucket | Meaning | Action |
|---|---|---|
identical | same name, length, and checksum | skip — already have it |
different | same name, different checksum/length | re-copy |
missing | primary has it, replica does not | copy |
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-only | Remote-backed |
|---|---|
| Durability = local translog fsync + local Lucene commit | Durability = translog and segments uploaded to the remote store |
| Recovery = peer copy of local segments | Recovery = download segments + translog from the remote store |
MetadataSnapshot diff happens node↔node | Metadata lives in the remote store; local is a cache to refill |
| A lost node ⇒ rebuild from a replica | A 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:
- Coordinator routes it (see sharding-routing) to, say, shard 2 on node B.
InternalEngine.index()callsIndexWriter.updateDocument(...)— the doc lands in the in-RAM buffer, not yet on disk, not yet searchable.Translog.add(op)appends tonodes/0/indices/<uuid>/2/translog/translog-13.tlog.- Default
requestdurability ⇒fsync(translog-13.tlog). Now durable. The200 OKreturns. Akill -9here loses nothing — replay restores it. - A refresh (default ~1s) opens a new reader; the doc becomes searchable (still no Lucene commit).
- Eventually a flush:
IndexWriter.commit()fsyncs the new segment files (_8.cfs _8.si ...) and writes+fsyncssegments_6in2/index/; the translog rolls to gen 14 and old gens are trimmed. - 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
| Symptom | Root cause | Where to look |
|---|---|---|
Shards fail to open: Map failed / OutOfMemoryError on a healthy-disk node | vm.max_map_count too low for the number of mmapped files | sysctl vm.max_map_count; raise to 262144; or switch hot indices to niofs |
| High heap, poor query latency, disk reads on every query | Heap too large ⇒ tiny page cache ⇒ mmap pages not resident | shrink heap to leave RAM for page cache; mmap needs free RAM |
| Slow first queries after a restart | Cold page cache; nothing pre-faulted | index.store.preload for tim/tip/dvd/kdd/vec; expect a warm-up |
CorruptIndexException / shard won't allocate | CRC32 footer mismatch — bit rot, bad disk, truncated write | the corrupted_* marker; CheckIndex; replace disk; recover from replica/remote |
Disk full of .tlog files | Translog 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 _source | expected; consider index.codec trade-off (segments-and-codecs) |
| Recovery copies the whole index every time | MetadataSnapshot diff finding everything "different" — checksums not matching (codec mismatch / corruption) | Store.getMetadata; segment .si Lucene version; recovery |
Switching to niofs didn't help latency | The hot structures want mmap; NIO adds a syscall+copy per read | revert 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:
- Draw the layering from
InternalEnginedown to disk and name the class at each rung (Store,Directory,MMapDirectory/NIOFSDirectory, page cache). - What four things does
Storeadd that Lucene'sDirectorydoes not? - Which file extensions does
hybridfsmmap, and which does it leave on NIO? Why is.fdt(stored fields) on the NIO side? - Contrast
MMapDirectoryandNIOFSDirectorymechanically. Why does mmap want a small JVM heap? - Explain
vm.max_map_count: what consumes a VMA, what the symptom of running out is, and whyniofssidesteps it. - 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?
- Explain the CRC32 footer and how
Store.MetadataSnapshotreads a checksum without scanning the whole file. How does that enable recovery diffing? - 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'sindex/+translog/+_state/. -
On a running node,
finda real shard, then point at the file that is the Lucene commit and explain whatMMapDirectoryvsNIOFSDirectorywould do for_7.timvs_7.fdt. -
In Lab ST3,
pmapthe node and show the mmapped.cfs/.tim/.dvd/.vecand the un-mapped.fdt, then explain it from thehybridfsextension 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 theCorruptIndexException; 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).