Help us shape the future of Go! Take the 2026 Go Developer Survey

lith

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: Apache-2.0

README

lith

Read-only, high-performance POSIX filesystem over an existing S3 bucket in its native key layout.

lith presents any S3 bucket — a Registry of Open Data dataset, a lab bucket, an instrument dump — as a read-only filesystem, using the bucket's key namespace directly. It writes nothing to the bucket, needs no sidecar objects, and serves all metadata (readdir, lookup, getattr, open) locally from an index after a one-time build.

What it is

  • Read-only. There is no write path. Every mutating operation returns EROFS.
  • Native layout. The bucket's keys are the filesystem. Any bucket, any producer.
  • Local metadata. After an index build, listing and stat make zero S3 calls.
  • Fast. Sequential throughput near NIC line rate; random reads bounded by cache hit rate, not S3 request latency.
  • A single static Go binary.

What it is not

  • Not writable. No writes, ever — not "writes disabled", writes absent.
  • Not a re-layout. It does not own or rewrite the bucket's object format.
  • Linux only (for v0.x). It relies on FUSE; macOS/Windows are out of scope.

Install

go install github.com/scttfrdmn/lith/cmd/lith@latest

Or download a static binary from the releases page.

Commands

lith mount   s3://bucket[/prefix] /mnt/point [flags]   # mount as a filesystem (M2)
lith index   build|refresh|inspect s3://bucket[/prefix] --index-file F
lith bench   s3://bucket/key --pattern seq|rand4k|stride [--against PATH]  (M2)
lith version                                            # version, commit, build date

Status: mount and bench are implemented in milestone M2 and currently report "not implemented in this build". index lands in M1.

Example: a public Registry of Open Data bucket

The 1000genomes bucket is public; use --no-sign-request to read it without credentials:

lith index build s3://1000genomes/changelog_details --index-file /tmp/1kg.lithidx --no-sign-request
lith index inspect /tmp/1kg.lithidx
lith mount  s3://1000genomes/changelog_details /mnt/1kg --index-file /tmp/1kg.lithidx --no-sign-request
ls -l /mnt/1kg
cat /mnt/1kg/changelog_details_20081219

Caching / No NVMe?

lith serves file reads from a two-tier cache: a bounded in-memory tier (--mem-cache, default 25% of system memory) and an optional on-disk tier (--disk-cache, default off). The disk tier is worth it only on fast local storage — an instance-store NVMe volume.

If the machine has no local NVMe (most instances, laptops, cluster head nodes), either:

  • leave --disk-cache off and rely on the memory tier plus the kernel page cache (sequential and re-read workloads still benefit), or
  • point --disk-cache at /dev/shm (tmpfs, i.e. RAM): --disk-cache 8GiB --disk-path /dev/shm/lith.

Do not put the disk cache on EBS, EFS, or NFS — caching a block there can be slower than just re-fetching it from S3 in-region. lith mount prints a warning if the --disk-cache path resolves onto the root filesystem or a network volume.

Sizing the node

For cold reads the network is the ceiling: lith streams from S3, so its throughput is bounded by the instance's NIC, not by CPU or disk. Size the node to the bandwidth you want.

  • Match the NIC baseline to your sustained demand. Many instance classes advertise a burst bandwidth well above their baseline (e.g. c8gd.4xlarge is 7.5 Gbps baseline, 15 Gbps burst). A short read stays in burst; a long or many-file read drains the burst credits and settles at the baseline. In measurement, a single cold sequential read of a >1 GiB object showed a post-first-window p99 of ~4.9 ms on a 30 Gbps c8gd.16xlarge versus ~7.4 ms on a c8gd.4xlarge once its burst credits depleted — same code, different NIC headroom.
  • For sustained or multi-reader workloads, pick a class whose baseline meets your target, or a 16xlarge+ where the advertised bandwidth is sustained (no burst-credit model). A c8gd.16xlarge (30 Gbps) sustains multi-GB/s aggregate; a 4xlarge will settle at ~0.9 GB/s under a long many-reader run.
  • Local NVMe (d instance types) is worth it for re-read and random workloads — point --disk-cache at the instance-store mount. Without it, see "No NVMe?" above.
  • lith sizes its S3 concurrency to the bandwidth-delay product automatically (--inflight-bytes, default 2 × NIC × 100 ms via ethtool); override it if the NIC speed can't be detected.

Expectations

Measured against s3://1000genomes in us-east-1, lith vs mountpoint-s3, all MB/s. lith --disk-cache 0, --mem-cache 25% of RAM; single = a 14 GiB CRAM read sequentially; 8-reader = eight 3.8–14 GiB CRAMs read concurrently; warm = re-read from cache; metadata = 100k stats from a prebuilt local index. Full data and every per-run number are in bench/results/; the S3 client ceiling can be reproduced independently with cmd/lith-s3bench.

instance RAM NVMe cold seq (lith / mnt-s3) warm seq (lith) 8-reader (lith / mnt-s3) 100k stat warm rand4k
c8g.2xlarge 16 GiB no 1354 / 1527 1163 1550 / 1602 0.16 s, 0 S3 543k IOPS
c8gd.4xlarge 32 GiB yes 1440 / 1547 1400 1688 / 1637 0.17 s, 0 S3 —
c8gd.16xlarge 128 GiB yes 2848 / 2613 21383 3385 / 3221 0.16 s, 0 S3 589k IOPS
c8gd.8xlarge † 64 GiB yes 1361 / 1581 19214 1696 / 1686 0.16 s, 0 S3 547k IOPS
c8i.4xlarge (x86) † 32 GiB no 1295 / 1545 1402 1137 / 1683 0.12 s, 0 S3 687k IOPS
m8g.4xlarge † 64 GiB no 1398 / 1611 20408 1703 / 1704 0.16 s, 0 S3 518k IOPS

† pre-fix (the earlier run); their multi-reader improves the same way the re-run rows do — the 32 GiB x86 box would rise from 1137 toward mount-s3 like the 32 GiB c8gd.4xlarge did (1095 → 1688).

Reading guide: cold sequential is your NIC; warm and random are your cache; metadata is free. A single cold reader fills the instance's NIC (the readahead window is sized to the bandwidth-delay product), reaching mount-s3 parity or better. The 8-reader aggregate reaches mount-s3 parity across box sizes, from a 16 GiB c8g.2xlarge (1550, 97% of mount-s3) to a 128 GiB c8gd.16xlarge (3385, 105%): aggregate readahead is bounded to the memory tier and shared fairly across handles, so a small-RAM box no longer thrashes. With RAM ≥ ~2× your working file, warm reads come from cache at ~20 GB/s. Metadata is served entirely from the local index: 100k stats in ~0.15 s with zero S3 requests on every class. (mount-s3 still edges ahead on cold single-reader on the smallest burst-credit boxes, where it rides NIC burst harder.)

Copy first, or mount?

Cost-to-result on a c8gd.4xlarge, in-region (transfer/egress $0). Each cell is wall-to-result / dollars / bytes pulled from S3 (full data in bench/results/copy-vs-mount-v0.1.0.csv):

access path region query (1% of 14 GiB) full read (3.8 GiB) parallel batch (8 × = 60 GiB)
copy → gp3 EBS, then compute 1.3 min / $0.019 / 14.1 GB 0.95 min / $0.013 / 3.8 GB 15.5 min / $0.21 / 60 GB
copy → local NVMe, then compute 0.45 min / $0.006 / 14.1 GB 0.90 min / $0.012 / 3.8 GB 3.4 min / $0.05 / 60 GB
mount with lith, compute in place 0.007 min / $0.0001 / 0.05 GB 0.79 min / $0.011 / 3.8 GB 3.1 min / $0.05 / 68 GB

Copying first pays off only when you re-read a dataset many times from fast local storage and the copy amortizes — and a local-NVMe copy always beats an EBS copy for later compute. For query-once or selective-access workloads, mounting is both faster and cheaper: no staging wall-clock, and for a selective query lith moves a tiny fraction of the bytes (a 1% region query pulled 49 MB instead of the whole 14 GiB object).

Testing

Unit tests run with the race detector and touch no network:

make test

An optional end-to-end test runs only when LITH_E2E_BUCKET is set. It builds an index from a real public prefix and asserts a known key resolves. It is verified against s3://1000genomes/changelog_details (key changelog_details_20081219):

LITH_E2E_BUCKET=1000genomes LITH_E2E_PREFIX=changelog_details go test ./internal/index/ -run E2E -v

License

Apache-2.0. Copyright 2026 Scott A Friedman.

Directories

Path Synopsis
cmd
lith command
Command lith is a read-only, high-performance POSIX filesystem over an existing S3 bucket in its native key layout.
Command lith is a read-only, high-performance POSIX filesystem over an existing S3 bucket in its native key layout.
lith-s3bench command
Command lith-s3bench isolates the S3 client/transport from lith's cache, prefetch, and FUSE layers.
Command lith-s3bench isolates the S3 client/transport from lith's cache, prefetch, and FUSE layers.
internal
blockstore
Package blockstore implements lith's read-only cache over S3 range GETs.
Package blockstore implements lith's read-only cache over S3 range GETs.
fuse
Package fuse implements lith's read-only FUSE layer on the hanwen/go-fuse/v2 raw API.
Package fuse implements lith's read-only FUSE layer on the hanwen/go-fuse/v2 raw API.
index
Package index holds the immutable namespace snapshot of a bucket: an in-memory sorted arena of keys plus parallel metadata arrays, from which directory structure is derived rather than stored.
Package index holds the immutable namespace snapshot of a bucket: an in-memory sorted arena of keys plus parallel metadata arrays, from which directory structure is derived rather than stored.
metrics
Package metrics defines lith's Prometheus metrics and an HTTP handler for the --metrics endpoint.
Package metrics defines lith's Prometheus metrics and an HTTP handler for the --metrics endpoint.
prefetch
Package prefetch implements lith's per-open-file-handle access-pattern detector and readahead planner.
Package prefetch implements lith's per-open-file-handle access-pattern detector and readahead planner.
s3client
Package s3client defines the narrow S3 surface lith depends on and provides a tuned aws-sdk-go-v2 implementation of it.
Package s3client defines the narrow S3 surface lith depends on and provides a tuned aws-sdk-go-v2 implementation of it.
s3client/fake
Package fake is an in-process implementation of the s3client.API surface (ListObjectsV2, HeadObject, GetObject with Range) over an in-memory map.
Package fake is an in-process implementation of the s3client.API surface (ListObjectsV2, HeadObject, GetObject with Range) over an in-memory map.
version
Package version holds build-time identifying information for the lith binary.
Package version holds build-time identifying information for the lith binary.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL