A third deployment next to the two on the DGX Spark, which stay as they
are: deploy/k8s/ has its own image and Kustomize manifests. The API is a
Deployment with probes, the data a PersistentVolumeClaim, the nightly
update a CronJob; overlays/gpu adds cuOpt on a GPU of the cluster.
Five assumptions of the single-container setup did not hold for pods, so
the service changed (on the Spark nothing changes that one would notice):
- probes: GET /health/live (async, no I/O) and GET /health/ready (data
directory readable); neither needs cuOpt or market data, so a cuOpt
outage never gets the API restarted. /health is unchanged
- DataStore replaces every file atomically (temporary file renamed into
place) and saves the manifest last; the API reloads its market data
when the manifest changes, so it serves what a CronJob wrote
- one writer at a time across processes and pods: an flock on
/data/.write.lock, taken by API jobs and the CLI's data commands
(setup, reselect, update, clean, snapshot); a second writer gets 409 or
exit status 3, with the holder's name. `clean` now updates the manifest
- job records live in /data/jobs/ and are written at every status
change: every replica answers GET /jobs/{id}, and a job whose process
has gone is reported failed ("interrupted")
- the image runs as uid 10001 under a read-only root file system, with
the library versions pinned to deploy/apptainer/constraints.txt (SciPy
1.18 releases the GIL while HiGHS solves, so the liveness probe is
answered during an optimisation)
docs/KUBERNETES.md is the guide (build, deploy, load the data, operate,
troubleshooting); README, USAGE and ARCHITECTURE point to it.
Both kustomizations render (kubectl kustomize); the k3s test of the
deployment is recorded in docs/KUBERNETES.md. 149 tests (148 pass, the
cuOpt round trip skipped without a server).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
3.0 KiB
YAML
98 lines
3.0 KiB
YAML
# The API (and the test bench at /ui/): uvicorn with one worker per pod; scale with replicas.
|
|
#
|
|
# Replicas share the data volume (see pvc.yaml): job records and the write lock live in it, and each
|
|
# pod reloads the market data when a data job has written new data. One replica is the default.
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: quantcore
|
|
labels:
|
|
app.kubernetes.io/name: quantcore
|
|
app.kubernetes.io/component: api
|
|
spec:
|
|
replicas: 1
|
|
strategy:
|
|
type: RollingUpdate
|
|
rollingUpdate:
|
|
maxSurge: 1
|
|
maxUnavailable: 0 # the new pod is ready before the old one stops: no downtime on a rollout
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: quantcore
|
|
app.kubernetes.io/component: api
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: quantcore
|
|
app.kubernetes.io/component: api
|
|
spec:
|
|
# SIGTERM lets uvicorn finish the requests in flight: an optimisation can take the whole
|
|
# QUANTCORE_CUOPT_TIME_LIMIT (300 s) before it answers
|
|
terminationGracePeriodSeconds: 360
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 10001
|
|
runAsGroup: 10001
|
|
fsGroup: 10001 # makes the data volume writable for that user
|
|
fsGroupChangePolicy: OnRootMismatch
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
- name: api
|
|
image: quantcore
|
|
imagePullPolicy: IfNotPresent
|
|
ports:
|
|
- name: http
|
|
containerPort: 5001
|
|
envFrom:
|
|
- configMapRef:
|
|
name: quantcore-config
|
|
resources:
|
|
requests:
|
|
cpu: "1"
|
|
memory: 1Gi
|
|
limits:
|
|
cpu: "4" # = the BLAS threads of quantcore.env
|
|
memory: 4Gi
|
|
# live: the process answers (no I/O, no cuOpt); ready: the data volume can be read
|
|
startupProbe:
|
|
httpGet:
|
|
path: /health/live
|
|
port: http
|
|
periodSeconds: 2
|
|
failureThreshold: 60
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health/live
|
|
port: http
|
|
periodSeconds: 20
|
|
timeoutSeconds: 5
|
|
failureThreshold: 3
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /health/ready
|
|
port: http
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
lifecycle:
|
|
preStop:
|
|
exec:
|
|
command: ["sleep", "5"] # the Service stops sending requests before uvicorn stops
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /data
|
|
- name: tmp
|
|
mountPath: /tmp
|
|
volumes:
|
|
- name: data
|
|
persistentVolumeClaim:
|
|
claimName: quantcore-data
|
|
- name: tmp
|
|
emptyDir:
|
|
sizeLimit: 1Gi
|