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 deliberately. Photon's index file format is sensitive to the binary version: a mismatch between the running pod and the index in the GCS bucket will fail to load. When bumping this tag also bump the matching tag in k8s/envs/platform/photon-indexer/Dockerfile (PHOTON_RUNTIME_IMAGE) and in the developer-facing docker-compose.dev.yml over in the application repo. rtuszik/photon-docker:2.2.0 ships Photon 1.1.0.
Deployment is single-replica with strategy.type: Recreate. There is no rolling update — when a new pod comes up the old one must already be gone, because both would want to mount the same ReadWriteOnce PVC. Multi-replica scale-out is not supported by this layout; Photon itself can be replicated but would need a per-pod copy of the index (or RWX storage, which we don't run).
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
The size is set for UPDATE_STRATEGY=PARALLEL (see below) — during an atomic swap both the current index and the incoming index sit on disk side-by-side, so peak usage is roughly 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. There is no CPU limit on purpose: the index extraction (bzip2) is bursty and CPU-heavy, and a limit would throttle it. The cold-start path (download index, 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, and until real traffic exists this is the only lever needed.
Horizontal scale-out is blocked by the ReadWriteOnce PVC: only one pod can mount the index at a time, so raising replicas does nothing (the second pod stays Pending). The index is read-only once loaded, so the clean horizontal path is:
- Move the index onto a ReadOnlyMany disk — one shared read-only PD that N replicas mount at once — populated out-of-band (a writer Job, or a PD built from a snapshot) rather than downloaded in-pod. A read-only disk cannot be the target of the in-pod
UPDATE_STRATEGY=PARALLELdownload, so this splits "populate the index" from "serve the index". - Run N replicas behind the existing ClusterIP Service, with an HPA on CPU.
Avoid a StatefulSet with per-pod PVCs: each replica would hold its own ~77 GB copy of the index (N× storage, N× download), the wrong cost profile for a large shared read-only dataset.
Pre-wiring this while staying at one replica buys little. An HPA on the current RWO Deployment can't scale past one pod, and switching to a ReadOnlyMany disk forces the populate re-architecture up front for no current benefit. Do the ReadOnlyMany disk + populate Job + HPA as one deliberate change when query load warrants it; at that point raising the replica count (or the HPA ceiling) is all that is left.
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 is ${project_id}-photon-index-usc1, co-located in us-central1 with the cluster so the ~77 GB pull is same-region ($0 egress). Photon polls the .md5 once every 24h; if it has changed it downloads the new tar alongside the running index, verifies the checksum, and atomically flips to the new index. This is the only update path — bumping the bucket contents is what rolls Photon forward.
A pod restart does not re-download: the extracted index persists on the photon-data-pvc, and Photon compares the on-disk index's MD5 to the remote one and skips the download when they match. A full re-download only happens when the PVC has no complete index yet (first boot, or a restart that interrupted extraction) or when the remote index actually changes.
Multilingual index¶
The index served from the bucket today is the 28-language multilingual build covering: 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 list mirrors services.SupportedLocalizationLanguages in the backend.
To rebuild and ship a new multilingual 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. This exact URL is wired into the Tomoda backend as PHOTON_URL (k8s/apps/tomoda/base/backend-api-deployment.yaml, k8s/apps/tomoda/base/backend-async-deployment.yaml) and is the target of the blackbox readiness probe (k8s/envs/platform/manifests/photon-probe.yaml). The Service is not exposed via Ingress; there is no public Photon endpoint.
Operations¶
- New index — see Building the Photon multilang index. Upload to GCS, update the
*-latestaliases; 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-pvcon 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.