Right-size CPU requests and stage controlled Odroid rebalancing
This commit is contained in:
1 parent
c4f2f82f29
commit
bbabf768ec
23 files changed
+249
-17
No files matched your search
@@ -49,7 +49,7 @@ server:
|
||||
minAvailable: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
cpu: 150m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
# Deliberately leave CPU unlimited so login bursts can use idle cores.
|
||||
@@ -91,6 +91,9 @@ server:
|
||||
startupProbe:
|
||||
timeoutSeconds: 10
|
||||
worker:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 150m
|
||||
enabled: true
|
||||
livenessProbe:
|
||||
timeoutSeconds: 10
|
||||
|
||||
@@ -20,3 +20,7 @@ ingress:
|
||||
- hosts:
|
||||
- brunner.ninja
|
||||
secretName: brunner-ninja-tls
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 175m
|
||||
@@ -23,3 +23,8 @@ certificate:
|
||||
secretName: coturn
|
||||
dnsName: brunner.ninja
|
||||
issuerName: letsencrypt-dns
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
limits:
|
||||
cpu:
|
||||
@@ -0,0 +1,24 @@
|
||||
# Odroid CPU rebalancing
|
||||
|
||||
The upstream Descheduler chart runs every five minutes. `values.yaml` is the
|
||||
policy; the bootstrap Application selects its chart version.
|
||||
|
||||
The policy uses Metrics Server CPU usage: nodes below 40% are possible
|
||||
receivers and nodes above 75% are possible eviction sources. Kubernetes still
|
||||
chooses replacement placement. It does not live-migrate processes or guarantee
|
||||
that a replacement lands on a different node.
|
||||
|
||||
Each cycle permits at most two evictions overall, one per node and namespace.
|
||||
Pods must be at least one hour old, belong to a controller with at least two
|
||||
replicas, have a PodDisruptionBudget, have no PVC, and fit another eligible
|
||||
node. Only the six Odroids are eligible. Storage, networking, monitoring,
|
||||
certificate management, and GitOps namespaces are excluded. EmptyDir alone is
|
||||
not a protection for these replicated stateless workloads.
|
||||
|
||||
Start with `cmdOptions.dry-run: true`, inspect Job logs, then remove that option
|
||||
to allow evictions. To stop balancing, set `suspend: true` in values and let Argo
|
||||
sync. Existing running Jobs must finish before suspension takes full effect.
|
||||
|
||||
Review after changes to workload mix; do not remove PVC and infrastructure
|
||||
protections to force a more even CPU graph. CPU requests determine whether a
|
||||
replacement can fit even when observed utilization is low.
|
||||
@@ -0,0 +1,58 @@
|
||||
schedule: "*/5 * * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
activeDeadlineSeconds: 120
|
||||
successfulJobsHistoryLimit: 2
|
||||
failedJobsHistoryLimit: 2
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: null
|
||||
memory: 256Mi
|
||||
# Observe the first cycle before enabling evictions.
|
||||
cmdOptions:
|
||||
dry-run: true
|
||||
v: 3
|
||||
deschedulerPolicy:
|
||||
maxNoOfPodsToEvictPerNode: 1
|
||||
maxNoOfPodsToEvictPerNamespace: 1
|
||||
maxNoOfPodsToEvictTotal: 2
|
||||
metricsProviders:
|
||||
- source: KubernetesMetrics
|
||||
profiles:
|
||||
- name: odroid-cpu-balance
|
||||
pluginConfig:
|
||||
- name: DefaultEvictor
|
||||
args:
|
||||
nodeFit: true
|
||||
minPodAge: 1h
|
||||
minReplicas: 2
|
||||
nodeSelector: "kubernetes.io/hostname in (odroid-h3-00,odroid-h3-01,odroid-h3-02,odroid-h3-03,odroid-h3-04,odroid-h3-05)"
|
||||
podProtections:
|
||||
defaultDisabled:
|
||||
- PodsWithLocalStorage
|
||||
extraEnabled:
|
||||
- PodsWithPVC
|
||||
- PodsWithoutPDB
|
||||
- name: LowNodeUtilization
|
||||
args:
|
||||
metricsUtilization:
|
||||
source: KubernetesMetrics
|
||||
thresholds:
|
||||
cpu: 40
|
||||
targetThresholds:
|
||||
cpu: 75
|
||||
evictableNamespaces:
|
||||
exclude:
|
||||
- kube-system
|
||||
- rook-ceph
|
||||
- argocd
|
||||
- elastic
|
||||
- prometheus
|
||||
- metallb-system
|
||||
- cert-manager
|
||||
plugins:
|
||||
balance:
|
||||
enabled:
|
||||
- LowNodeUtilization
|
||||
@@ -1,5 +1,5 @@
|
||||
managedNamespaces:
|
||||
- elastic
|
||||
- elastic
|
||||
|
||||
installCRDs: true
|
||||
createClusterScopedResources: true
|
||||
@@ -9,7 +9,7 @@ resources:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
cpu: null
|
||||
memory: 1Gi
|
||||
|
||||
config:
|
||||
|
||||
@@ -98,7 +98,7 @@ spec:
|
||||
protocol: UDP
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
cpu: 75m
|
||||
memory: 1Gi
|
||||
limits:
|
||||
cpu: "2"
|
||||
|
||||
@@ -47,10 +47,9 @@ spec:
|
||||
- name: elasticsearch
|
||||
resources:
|
||||
requests:
|
||||
cpu: "2"
|
||||
cpu: 500m
|
||||
memory: 12Gi
|
||||
limits:
|
||||
cpu: "4"
|
||||
memory: 12Gi
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
@@ -119,7 +118,7 @@ spec:
|
||||
- name: kibana
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
cpu: 100m
|
||||
memory: 4Gi
|
||||
limits:
|
||||
cpu: "2"
|
||||
|
||||
@@ -82,3 +82,7 @@ grafana.ini:
|
||||
security:
|
||||
allow_embedding: true
|
||||
x_frame_options: ""
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 75m
|
||||
@@ -20,3 +20,7 @@ ingress:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.middlewares: default-authentik@kubernetescrd
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 175m
|
||||
@@ -15,10 +15,10 @@ env:
|
||||
value: Europe/Vienna
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
cpu: 100m
|
||||
memory: 2Gi
|
||||
limits:
|
||||
cpu: "2"
|
||||
cpu:
|
||||
memory: 16Gi
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||
@@ -49,7 +49,7 @@ mastodon:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
cpu: 50m
|
||||
memory: 768Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
@@ -70,7 +70,7 @@ mastodon:
|
||||
- fasp
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
cpu: 50m
|
||||
memory: 768Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
@@ -83,7 +83,7 @@ mastodon:
|
||||
workers: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
cpu: 25m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
|
||||
@@ -4,3 +4,8 @@ serviceMonitor:
|
||||
enabled: true
|
||||
labels:
|
||||
release: prometheus
|
||||
resources:
|
||||
requests:
|
||||
cpu: 175m
|
||||
limits:
|
||||
cpu:
|
||||
@@ -44,5 +44,5 @@ affinity:
|
||||
values: [arschrock]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
cpu: 50m
|
||||
memory: 1Gi
|
||||
@@ -7,7 +7,7 @@ ingress:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: default-authentik@kubernetescrd
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
cpu: 35m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
memory: 2Gi
|
||||
@@ -77,6 +77,9 @@ prometheus:
|
||||
kubernetes.io/ingress.class: "traefik"
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
prometheusSpec:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
retention: "15d"
|
||||
storageSpec:
|
||||
volumeClaimTemplate:
|
||||
|
||||
@@ -13,5 +13,5 @@ ingress:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: default-authentik@kubernetescrd
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
cpu: 25m
|
||||
memory: 256Mi
|
||||
@@ -11,7 +11,7 @@ podDisruptionBudget:
|
||||
minAvailable: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
cpu: 700m
|
||||
memory: 1Gi
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
|
||||
@@ -20,7 +20,7 @@ readinessProbe:
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
cpu: 75m
|
||||
memory: 256Mi
|
||||
ephemeral-storage: 128Mi
|
||||
limits:
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: AppProject
|
||||
metadata:
|
||||
name: descheduler
|
||||
namespace: argocd
|
||||
spec:
|
||||
description: Controlled CPU rebalancing on the Odroid workers
|
||||
sourceRepos:
|
||||
- https://kubernetes-sigs.github.io/descheduler/
|
||||
- https://code.brunner.ninja/feedc0de/homelab-gitops.git
|
||||
destinations:
|
||||
- namespace: kube-system
|
||||
server: https://kubernetes.default.svc
|
||||
clusterResourceWhitelist:
|
||||
- group: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
- group: rbac.authorization.k8s.io
|
||||
kind: ClusterRoleBinding
|
||||
namespaceResourceWhitelist:
|
||||
- group: ""
|
||||
kind: ConfigMap
|
||||
- group: ""
|
||||
kind: ServiceAccount
|
||||
- group: batch
|
||||
kind: CronJob
|
||||
- group: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
- group: rbac.authorization.k8s.io
|
||||
kind: RoleBinding
|
||||
@@ -0,0 +1,26 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: descheduler
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: descheduler
|
||||
sources:
|
||||
- repoURL: https://kubernetes-sigs.github.io/descheduler/
|
||||
chart: descheduler
|
||||
targetRevision: 0.36.0
|
||||
helm:
|
||||
releaseName: descheduler
|
||||
valueFiles:
|
||||
- $values/apps/descheduler/values.yaml
|
||||
- repoURL: https://code.brunner.ninja/feedc0de/homelab-gitops.git
|
||||
targetRevision: main
|
||||
ref: values
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: kube-system
|
||||
syncPolicy:
|
||||
automated:
|
||||
enabled: true
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -151,3 +151,5 @@ resources:
|
||||
- ghostfolio.yaml
|
||||
- oldipmi-project.yaml
|
||||
- oldipmi.yaml
|
||||
- descheduler-project.yaml
|
||||
- descheduler.yaml
|
||||
@@ -0,0 +1,66 @@
|
||||
# CPU sizing and balancing
|
||||
|
||||
## Review on 2026-10-31
|
||||
|
||||
Prometheus was queried for the 95th percentile of five-minute CPU rates over
|
||||
its available history within the previous 14 days, sampled every 15 minutes:
|
||||
|
||||
```promql
|
||||
quantile_over_time(0.95,
|
||||
(sum by(namespace,pod,container)
|
||||
(rate(container_cpu_usage_seconds_total{container!="",container!="POD"}[5m]))
|
||||
)[14d:15m])
|
||||
```
|
||||
|
||||
The largest observed replica value was considered for shared workload sizing.
|
||||
New pods have shorter histories; this is not a guarantee of fourteen complete
|
||||
days per workload. These measurements include the recent recovery period.
|
||||
|
||||
The starting recommendation is p95 times 1.2, rounded upwards. Request floors
|
||||
are retained for startup, databases, recovery, and bursty workloads. Requests
|
||||
are not CPU limits. Memory requests and limits are unchanged.
|
||||
|
||||
Reviewed changes include Elasticsearch/Kibana, Netflow, Authentik, Traefik,
|
||||
InfluxDB, MKTXP, Mastodon, Paperless, scanner/PDF editor, Uptime Kuma, TURN,
|
||||
Home Assistant, WordPress, Grafana and Prometheus. Busy services without CPU
|
||||
requests receive explicit reservations. Elasticsearch retains a 500m floor
|
||||
per data node for startup/recovery, substantially above its measured baseline.
|
||||
|
||||
Matrix is still a legacy Helm release. Its canonical values live in
|
||||
`matrix/authentication/values.yaml` in the homelab workspace: the main Synapse
|
||||
process gets 500m, federation sender 225m, and other workers 50m. Init-container
|
||||
requests can make effective pod reservations higher than the main container.
|
||||
The shared RBD claim and worker/offload affinity remain unchanged.
|
||||
|
||||
Ceph daemons, the API server, Cilium, CoreDNS, Argo CD, and ingress already have
|
||||
no CPU limits. Their low or absent requests are not interpreted as spare
|
||||
physical capacity; Descheduler uses measured CPU. The legacy Ceph CSI operator
|
||||
CPU cap is removed in `rook/deploy/examples/csi-operator.yaml`. Ceph daemon
|
||||
resources are left alone to avoid a storage-wide restart. CPU caps are also
|
||||
removed from the Elastic operator/data nodes, InfluxDB, MKTXP, and TURN.
|
||||
|
||||
## Deliberate exclusions
|
||||
|
||||
- Do not infer batch, transcoding, game-server or GPU requirements from idle
|
||||
CPU measurements. Their explicit reservations are retained.
|
||||
- Do not resize workloads that are down or lack representative history.
|
||||
- Do not automatically rewrite operator-managed database resources or apply
|
||||
VPA mutation alongside Argo. This review changes owning configuration only.
|
||||
- Loops and Pixelfed charts hard-code some resource requests; overriding an
|
||||
unsupported values key would do nothing. Those require a separate chart
|
||||
interface change before their requests can be tuned through GitOps.
|
||||
- Small existing requests are retained where a reduction offers little useful
|
||||
headroom. The aim is schedulability and contention protection, not identical
|
||||
requests across applications.
|
||||
|
||||
## Ongoing operation
|
||||
|
||||
Descheduler runs automatically; request changes are reviewed configuration,
|
||||
not a continuously mutating autoscaler. Repeat the history review after a
|
||||
representative week or a substantial workload change. Never set every request
|
||||
from a single current-usage sample. Monitor pending pods, CPU throttling,
|
||||
latency, and node CPU usage as well as reserved capacity.
|
||||
|
||||
For a new request change, render the owning chart, inspect all container/init
|
||||
resources, and reconcile through Argo (or the existing legacy Helm installer).
|
||||
Keep the prior commit as rollback; do not delete PVCs to resolve placement.
|
||||
Reference in new issue
Block a user