IsleDB / Docs / Read cache

Read cache

Run the cache

isledb cache is the read cache, run as a service. It serves one or more databases to the readers in a region. It checks each database for new writes every 500 ms and tells waiting readers at once, and it keeps the data readers ask for on its local disk, so the bucket is read once for all of them instead of once for each. Readers use it through the cache HTTP API.

isledb cache --config /etc/isledb/isledb.yaml

or, without a file:

isledb cache \
  --bucket-url 's3://my-bucket?region=us-east-1' \
  --db prod/policy --db prod/flags \
  --disk-dir /var/cache/isledb --disk-bytes 200GiB \
  --auth-token-file /etc/isledb/tokens

Storage credentials come from the provider's usual environment (for S3, the AWS SDK chain: environment variables, shared config, instance or pod role). They are never set in the cache's own configuration.

Configuration

Each setting can come from the YAML file, an environment variable or a flag. A flag overrides the environment, the environment overrides the file, and the file overrides the default. --db on the command line, or ISLEDB_CACHE_DATABASES, replaces the file's list of databases rather than adding to it.

One file can configure every IsleDB service: settings they share sit at the top, and each service reads its own section. The cache reads cache:.

bucket_url: s3://my-bucket?region=us-east-1
log_level: info

cache:
  listen: :8090
  admin_listen: 127.0.0.1:8091

  disk:
    dir: /var/cache/isledb
    bytes: 200GiB

  reads:
    max_bytes: 256MiB
    max_wait: 5s

  current:
    poll: 500ms
    long_poll: 30s

  databases:
    - prefix: prod/policy
    - prefix: prod/flags
      current_poll: 250ms

  auth:
    token_file: /etc/isledb/tokens

  tls:
    cert: /etc/isledb/tls/cert.pem
    key: /etc/isledb/tls/key.pem
    client_ca: /etc/isledb/tls/clients.pem

  shutdown_drain: 5s

A database at the root of the bucket is written /. An empty prefix is an error, so a blank value never names the root, and so is one with an empty, . or .. segment (prod//a, prod/../a), whose keys would never match.

isledb cache --config isledb.yaml --check validates the configuration, prints it as resolved from every source, and exits.

Every setting, with its flag, environment variable and default, is in the configuration reference.

Authentication

The cache refuses to start without authentication unless cache.auth.insecure is set. Readers send a token from the cache's auth.token_file, and with tls.client_ca also a client certificate; an isledb reader sets these with its read_cache settings. See Secure IsleDB.

Each database is served only its own objects: those right under its prefix, not those of a database nested under it (prod/a/b under prod/a), and, for the root, not those of the databases under prefixes. A client can read through the cache only the databases it is configured to serve.

Admin endpoints

On admin_listen, without authentication; keep it on an address only operators and scrapers reach.

Path
/healthz 200 while the process runs
/readyz 200 once it has checked every database for new writes once; 503 before, and while shutting down. It stays 200 while storage is down: the cache keeps serving what it has, and readers judge its age.
/metrics Prometheus
/debug/pprof/ Go profiles

Metrics

The cache's metrics, and what to watch, are in the metrics reference: above all, storage not answering, reads waiting for their turn, and a disk too small for what readers read.

Shutdown

On SIGTERM or SIGINT the cache:

  1. reports not ready on /readyz;
  2. waits cache.shutdown_drain for load balancers to notice;
  3. answers every held long poll with the position it has;
  4. finishes requests in flight, for up to 30 seconds, and exits.

A second SIGTERM or SIGINT during these steps ends the process at once.

Chunks on disk are kept. After a restart, requests carrying Isledb-Object-Size are served from them at once.

Deploying

Kubernetes

The admin address defaults to 127.0.0.1:8091, which the kubelet's probes cannot reach from outside the container. Bind it to all interfaces in the pod, and keep it off the Service.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: isledb-cache
spec:
  serviceName: isledb-cache
  replicas: 2
  selector:
    matchLabels: { app: isledb-cache }
  template:
    metadata:
      labels: { app: isledb-cache }
    spec:
      # Longer than cache.shutdown_drain (5s) plus the 30s given to
      # requests in flight.
      terminationGracePeriodSeconds: 45
      securityContext:
        fsGroup: 65532   # the image's user: lets it write the chunk volume
      containers:
        - name: cache
          image: ghcr.io/ankur-anand/isledb:0.20.0
          args: ["cache", "--config", "/etc/isledb/isledb.yaml"]
          env:
            - name: ISLEDB_CACHE_ADMIN_LISTEN
              value: "0.0.0.0:8091"
          ports:
            - { name: api, containerPort: 8090 }
            - { name: admin, containerPort: 8091 }
          readinessProbe:
            httpGet: { path: /readyz, port: admin }
            periodSeconds: 2
          livenessProbe:
            httpGet: { path: /healthz, port: admin }
            periodSeconds: 10
          volumeMounts:
            - { name: config, mountPath: /etc/isledb, readOnly: true }
            - { name: chunks, mountPath: /var/cache/isledb }
      volumes:
        - name: config
          projected:
            sources:
              - configMap: { name: isledb-config }   # isledb.yaml
              - secret: { name: isledb-tokens }      # tokens
  volumeClaimTemplates:
    - metadata: { name: chunks }
      spec:
        accessModes: [ReadWriteOnce]
        storageClassName: local-ssd
        resources: { requests: { storage: 200Gi } }
---
apiVersion: v1
kind: Service
metadata:
  name: isledb-cache
spec:
  selector: { app: isledb-cache }
  ports:
    - { name: api, port: 8090, targetPort: api }   # the admin port is not here
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: isledb-cache-admin
spec:
  podSelector:
    matchLabels: { app: isledb-cache }
  policyTypes: [Ingress]
  ingress:
    - ports: [{ port: 8090 }]                 # the API, from anywhere it is routed
    - ports: [{ port: 8091 }]                 # the admin port, from monitoring only
      from:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: monitoring }

Cache HTTP API

The read cache sits between IsleDB readers in one region and object storage. It serves two things:

Running it: Read cache.

Version: v1. Every path starts with /v1/.

Endpoints

Method and path Serves
GET /v1/current?db=P The cache's copy of database P's CURRENT
GET /v1/current?db=P&after=N Its position only, once it passes N (long poll)
GET /v1/object/{key} Bytes of an immutable object, whole or by range

P is a database's prefix in the bucket, as configured on the cache, and / for a database at the bucket root. db may be left out when the cache serves one database.

{key} is the object's full storage key, including the database prefix, exactly as the reader's store names it, for example db/sstable/a3f/000123.sst. Only objects of a database the cache serves are answered (see below).

Authentication

Every request to these endpoints carries Authorization: Bearer <token>, or comes over TLS with a client certificate, as the cache is configured. A request without valid credentials answers 401 with WWW-Authenticate: Bearer.

GET /v1/current

Returns the cache's copy of CURRENT, or, as a long poll, only its position once it passes a given one. A long poll is a doorbell: a reader that sees a new position reads CURRENT with a plain GET /v1/current, so CURRENT reaches readers by one path, where the client rules below apply.

Log position. The position is CURRENT's next_seq: the sequence number the next manifest log entry will get. Every entry appended to the manifest log raises it by one, in the same conditional write of CURRENT that adds the entry. Entries record changes to the database's structure: a flush adding an SST (with its change batch), a compaction, SSTs removed, a checkpoint, or a writer or compactor taking ownership. User writes do not move it until a flush. It never goes down. Some rewrites of CURRENT add no entry and keep the position, such as folding the log into a new snapshot.

Query

Parameter Meaning
after=N Optional. Long poll: hold the request until the copy's log position is above N, or until the server's long-poll timeout, at most 30 seconds, then answer with the position only. N is a log position.

Response headers

Header Without after With after
Isledb-Position Log position of the CURRENT returned: its next_seq The copy's log position
Isledb-Age-Ms Milliseconds since the cache read this CURRENT from storage, rounded up. Measured on the cache's monotonic clock; not a timestamp. The copy's age, the same way
Content-Type application/json –

Without after, the body is CURRENT's bytes as stored. With after, the body is empty.

Status

Status When
200 The copy, with position and age; or, for a long poll, the position. A long poll that timed out also answers 200, with a position not above N.
400 after is not a number, or db is missing on a cache serving several databases
401 No valid credentials
404 db names a database the cache does not serve
503 The cache has not read CURRENT once yet; a long poll gets it at once, not after the timeout

Behaviour

Client rules (enforced by the reader, so a faulty cache cannot break it)

  1. Age limit. Reject a CURRENT whose age is above 5 seconds and read storage directly.
  2. Load time = the time the request was sent (own clock) − the age. This is when the view's MaxPinnedViewAge starts.
  3. Older means read storage. A CURRENT from the cache at a position below the reader's published view, as a lagging replica can give, is not replayed: the refresh reads CURRENT from storage instead, which also renews the view.
  4. Refresh reads storage directly. The relay can be up to one poll interval behind.
  5. Long poll. Ask with after=<published position>; on an answer above it, call RequestRefresh and ask again. RequestRefresh reads CURRENT with a plain GET, where rules 1 to 3 apply. Give up on a long poll unanswered after 45 seconds: the connection is dead. An answer not above it whose age is past the limit of rule 1 means the cache cannot read storage and will not ring for new commits: read CURRENT, which then goes to storage. After a timeout or error, wait a random 0 to 1 second before asking again.
  6. Fallback. On a connection error, a timeout or a 5xx, read storage directly.

Example

$ curl -si -H "Authorization: Bearer $TOKEN" 'http://cache:8090/v1/current?db=prod/policy&after=41'
HTTP/1.1 200 OK
Isledb-Age-Ms: 3
Isledb-Position: 42
Content-Length: 0

$ curl -si -H "Authorization: Bearer $TOKEN" 'http://cache:8090/v1/current?db=prod/policy'
HTTP/1.1 200 OK
Content-Type: application/json
Isledb-Age-Ms: 212
Isledb-Position: 42

{"layout_version":…,"next_seq":42,…}

GET /v1/object/{key}

Returns bytes of an immutable object.

Keys served. Only objects that are never rewritten, each a fixed shape right after its database's prefix P (nothing before the shape for the root):

Object Key
SST P/sstable/{bucket}/{id}, where {bucket} is the one id maps to
Change batch P/changes/{bucket}/{id}, same rule
Manifest snapshot P/manifest/snapshots/{name}.manifest.zst
Manifest page P/manifest/pages/l{NN}/{name}.page.zst

A key is answered only when what comes before its shape is exactly a database the cache serves. So each database is served only its own objects: serving prod/a does not serve prod/a/b/sstable/… of a database nested under it, and serving the root does not serve prod/a/sstable/…. Anything else, including manifest/CURRENT and maintenance/HEAD, answers 403.

Request headers

Header Meaning
Range Optional. One range: bytes=a-b, bytes=a- or bytes=-n. Without it, the whole object. Several ranges in one header are refused (416).
Isledb-Object-Size Optional. The object's size in bytes, its full stored length, at most 5 TiB. With it, the cache does not ask storage for the size. IsleDB readers always send it.
Isledb-No-Cache Optional, any value. Serve the bytes without keeping what is read from storage. Chunks already on disk are still used. For scans.

Response headers

Header Meaning
Content-Range On 206: bytes a-b/size. On 416: bytes */size.
Content-Length Bytes in the body
Isledb-Error On an error response, its kind where the status alone does not say: size-mismatch on a 400
Isledb-Cache How the first 4 MiB of the response were served: hit (all from disk), shared (read from storage by another request in flight, none by this one), miss (this request read from storage)
Accept-Ranges bytes
Content-Type application/octet-stream

Status

Status When Client should
200 Whole object Use it
206 The range asked for Use it
400 Isledb-Object-Size is not a positive number up to 5 TiB, or is not the object's size in storage; the latter carries Isledb-Error: size-mismatch Read storage directly; report a bug
401 No valid credentials Fix the token or certificate; read storage directly meanwhile
403 Not an immutable object of a database the cache serves Read storage directly; report a bug
404 The object is not in storage Retry storage directly once, then report not found
405 HEAD request; only GET is served (Allow: GET) Use GET
416 Range outside the object, or several ranges Report a bug
502 Storage failed Read storage directly
other 5xx, connection error, timeout The cache is unhealthy Read storage directly

A 404 is retried against storage because a cache pointed at the wrong bucket or prefix would answer 404 for everything; readers must not trust it alone.

The size

Behaviour

Examples

A lookup's chunk, cold and then warm:

$ curl -si -H "Authorization: Bearer $TOKEN" http://cache:8090/v1/object/db/sstable/a3f/000123.sst \
    -H 'Range: bytes=131072-262143' -H 'Isledb-Object-Size: 8421376'
HTTP/1.1 206 Partial Content
Content-Range: bytes 131072-262143/8421376
Content-Length: 131072
Isledb-Cache: miss
…

The next request for that chunk answers Isledb-Cache: hit.

A manifest page, whole:

$ curl -s -H "Authorization: Bearer $TOKEN" http://cache:8090/v1/object/db/manifest/pages/l00/p17.page.zst \
    -H 'Isledb-Object-Size: 20381' -o p17.page.zst

A scan that should not fill the cache:

$ curl -s -H "Authorization: Bearer $TOKEN" http://cache:8090/v1/object/db/sstable/a3f/000123.sst \
    -H 'Range: bytes=0-4194303' -H 'Isledb-Object-Size: 8421376' \
    -H 'Isledb-No-Cache: 1'

Reader support

Readers configured with a read cache read SSTs, manifest pages and snapshots through it, with fallback to storage, and refresh through the relay with the client rules above and the long poll. Change readers configured with a read cache read change batch indexes and blocks, the feed's view and its manifest pages through it, with the same rules and fallback, and wait for changes with the long poll.