Skip to content

Photon

Photon is the self-hosted geocoder that backs Tomoda's place-search autocomplete. It runs as a single in-cluster Deployment in the platform namespace, serves the backend at http://photon.platform.svc.cluster.local:2322, and pulls its index from a GCS bucket maintained by the Photon Indexer pipeline.

The Argo CD Application at k8s/envs/platform/photon/application.yaml points at k8s/envs/platform/photon/manifests.yaml and syncs into namespace: platform.

Image and replication

image: rtuszik/photon-docker:2.2.0

The tag is pinned: Photon's index file format is version-sensitive, so a mismatch between the running pod and the bucket's index fails to load. When bumping, also bump PHOTON_RUNTIME_IMAGE in k8s/envs/platform/photon-indexer/Dockerfile and the tag in the app repo's docker-compose.dev.yml. rtuszik/photon-docker:2.2.0 ships Photon 1.1.0.

Deployment is single-replica with strategy.type: Recreate: the new pod comes up only after the old one is gone, since both mount the same ReadWriteOnce PVC. Multi-replica scale-out needs a per-pod index copy or RWX storage (not run) — see Scaling.

The serving Deployment runs on the on-demand pool=app (nodeSelector: {pool: app}), off spot: the planet index (6 Gi, 250 Gi PVC, ~1.8h to rebuild) must not be preempted mid-serve. See GKE node pools.

The batch photon-indexer CronJob targets the spot pool=burst (nodeSelector: {pool: burst}), since a rebuild tolerates preemption.

Indexer image is not multi-arch yet

The photon-indexer image has no arm64 build. Until it does, build the planet index out-of-cluster (a Compute Engine VM via scripts/photon-index-local.sh) rather than the in-cluster CronJob. The index bucket development-485000-photon-index-usc1 stays in us-central1.

Storage

A single PersistentVolumeClaim named photon-data-pvc requests 250 Gi on the standard-rwo StorageClass:

accessModes: [ReadWriteOnce]
storageClassName: standard-rwo
resources:
  requests:
    storage: 250Gi

Sized for UPDATE_STRATEGY=PARALLEL (see below): during an atomic swap the current and incoming index sit side-by-side, so peak usage is ~2× the index size.

Resources and probes

requests: { memory: 2Gi, cpu: 250m }
limits:   { memory: 6Gi }

JAVA_OPTS=-Xmx4g caps the heap inside the 6Gi memory limit. No CPU limit: index extraction (bzip2) is bursty and CPU-heavy, and a limit would throttle it. The cold-start path (download, bzip2 extract, load from disk, warm Lucene) is slow, so probes are tuned long:

Probe Path Initial delay Period Notes
Startup /status 900s 15-min grace before liveness/readiness apply, to cover a first-time index download + extract
Readiness /status 120s 30s Pod stays out of the Service until Photon answers
Liveness /status 300s 60s failureThreshold: 10 to tolerate transient stalls during the atomic swap

Scaling

Photon is a read-only geocoder, memory- and CPU-bound per query, so it scales vertically first: raise the CPU/memory requests and the -Xmx heap. A single replica serves a large query volume.

Horizontal scale-out is blocked by the ReadWriteOnce PVC: only one pod mounts the index at a time, so raising replicas leaves the second pod Pending. The index is read-only once loaded, so the horizontal path is:

  1. Move the index onto a ReadOnlyMany disk (one shared read-only PD N replicas mount at once), populated out-of-band (a writer Job, or a PD built from a snapshot). A read-only disk can't be the target of the in-pod UPDATE_STRATEGY=PARALLEL download, so this splits "populate" from "serve".
  2. Run N replicas behind the existing ClusterIP Service, with a CPU HPA.

Avoid a StatefulSet with per-pod PVCs: each replica would hold its own ~77 GB copy (N× storage, N× download), the wrong cost profile for a shared read-only dataset.

Do the ReadOnlyMany disk + populate Job + HPA as one deliberate change when query load warrants it; then raising the replica count is all that's left. Pre-wiring at one replica buys nothing.

Index updates

Photon is configured to auto-update from a static GCS URL:

env:
  - name: UPDATE_STRATEGY
    value: "PARALLEL"     # atomic swap; no downtime
  - name: UPDATE_INTERVAL
    value: "24h"          # poll the MD5 once a day
  - name: FILE_URL
    value: "https://storage.googleapis.com/development-485000-photon-index-usc1/planet/photon-db-planet-multilang-latest.tar.bz2"
  - name: MD5_URL
    value: "https://storage.googleapis.com/development-485000-photon-index-usc1/planet/photon-db-planet-multilang-latest.tar.bz2.md5"

The bucket ${project_id}-photon-index-usc1 is co-located in us-central1 so the ~77 GB pull is same-region ($0 egress). Photon polls the .md5 every 24h; on a change it downloads the new tar alongside the running index, verifies the checksum, and atomically flips. Bumping the bucket contents is the only path that rolls Photon forward.

A pod restart does not re-download: the extracted index persists on photon-data-pvc, and Photon skips the download when the on-disk MD5 matches the remote. A full re-download happens only when the PVC has no complete index (first boot, or an interrupted extraction) or the remote index changes.

Multilingual index

The served index is the 28-language multilingual build: en, ja, ko, zh, zh-Hans, zh-Hant, ar, he, hi, vi, th, id, tr, es, fr, de, it, pt, nl, pl, ru, sv, no, da, fi, el, cs, uk. This mirrors services.SupportedLocalizationLanguages in the backend.

To rebuild and ship a new index, see the Photon indexing runbook.

Service

kind: Service
metadata:
  name: photon
  namespace: platform
spec:
  type: ClusterIP
  ports:
    - port: 2322
      targetPort: 2322

Backend pods reach Photon at http://photon.platform.svc.cluster.local:2322, wired into the backend as PHOTON_URL (k8s/apps/tomoda/base/backend-api-deployment.yaml, k8s/apps/tomoda/base/backend-async-deployment.yaml) and the target of the blackbox readiness probe (k8s/envs/platform/manifests/photon-probe.yaml). No Ingress; no public Photon endpoint.

Operations

  • New index — see Building the Photon multilang index. Upload to GCS, update the *-latest aliases; the running pod picks it up within 24h.
  • Force an update — restart the pod; on boot Photon re-checks the MD5 and downloads if the local copy is stale.
  • Storage pressure — if the PVC fills (large planet rebuild), expand it via kubectl edit pvc photon-data-pvc on a CSI driver that supports online expansion. Then bounce the pod.
  • Version bump — coordinate the runtime image and the indexer image in the same PR; cross-version index loads will fail readiness.