fix: give each instance's Postgres a 12 GB disk image and a 4 GB memory limit (#16)
- db disk image 6144 -> 12288 MiB (VOLUMES["db"]): instance 4 had reached 4.7 GB of its 6 GB and instances kept being recreated when the image filled. - db memory request/limit 512/2048 -> 1024/4096 MiB (MEMORY["db"]); the per-instance totals become 2.375 GiB requested and 11 GiB limit. - README (disk and memory rows, totals) and the unit tests follow the new numbers; release 1.6.5. Only instances created afterwards get the bigger disk image: existing ones have to be deleted and created again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# supaspin
|
# supaspin
|
||||||
|
|
||||||
Version 1.6.4
|
Version 1.6.5
|
||||||
|
|
||||||
Create, list, show and delete throwaway [Supabase](https://supabase.com) instances on a
|
Create, list, show and delete throwaway [Supabase](https://supabase.com) instances on a
|
||||||
single-node [microk8s](https://microk8s.io) server, from a terminal menu.
|
single-node [microk8s](https://microk8s.io) server, from a terminal menu.
|
||||||
@@ -16,8 +16,8 @@ endangering them. Nothing in it is specific to any one app.
|
|||||||
| Web API | `https://<slot hostname>` (auth, rest, graphql, realtime, storage, functions); the certificate is from Let's Encrypt when that is enabled, otherwise self-signed |
|
| Web API | `https://<slot hostname>` (auth, rest, graphql, realtime, storage, functions); the certificate is from Let's Encrypt when that is enabled, otherwise self-signed |
|
||||||
| Postgres | `<LAN IP>:544N` (instance 1 → 5441, 2 → 5442, …); which addresses listen is set by **Port bind** |
|
| Postgres | `<LAN IP>:544N` (instance 1 → 5441, 2 → 5442, …); which addresses listen is set by **Port bind** |
|
||||||
| Studio (admin UI) | `http://<LAN IP>:300N` (3001, 3002, …), not on the public hostname |
|
| Studio (admin UI) | `http://<LAN IP>:300N` (3001, 3002, …), not on the public hostname |
|
||||||
| Disk | fixed-size ext4 image files, mounted at boot: **db 6 GB, storage 1 GB, every other volume 256 MB** |
|
| Disk | fixed-size ext4 image files, mounted at boot: **db 12 GB, storage 1 GB, every other volume 256 MB** |
|
||||||
| Memory | limits add up to **9 GiB** per instance, requests to 1.875 GiB (table below) |
|
| Memory | limits add up to **11 GiB** per instance, requests to 2.375 GiB (table below) |
|
||||||
| Priority | pods run at priority -100 (`supaspin-low`); when the server runs short of memory they are evicted first and never evict anyone else |
|
| Priority | pods run at priority -100 (`supaspin-low`); when the server runs short of memory they are evicted first and never evict anyone else |
|
||||||
| Secrets | generated fresh per instance: JWT secret, anon and service keys, Postgres password, Studio password, and the internal keys |
|
| Secrets | generated fresh per instance: JWT secret, anon and service keys, Postgres password, Studio password, and the internal keys |
|
||||||
| Uploads | stored in the instance's own storage image (no MinIO); deleted with the instance |
|
| Uploads | stored in the instance's own storage image (no MinIO); deleted with the instance |
|
||||||
@@ -270,13 +270,13 @@ stores the new memory limits and leaves it asleep; they take effect when you res
|
|||||||
|
|
||||||
| Service | Request | Limit | Highest use measured |
|
| Service | Request | Limit | Highest use measured |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| db | 512 MiB | 2048 MiB | 273 MiB |
|
| db | 1024 MiB | 4096 MiB | 273 MiB |
|
||||||
| analytics | 128 MiB | 1536 MiB | 694 MiB |
|
| analytics | 128 MiB | 1536 MiB | 694 MiB |
|
||||||
| vector | 128 MiB | 512 MiB | 290 MiB |
|
| vector | 128 MiB | 512 MiB | 290 MiB |
|
||||||
| kong | 128 MiB | 768 MiB | 269 MiB |
|
| kong | 128 MiB | 768 MiB | 269 MiB |
|
||||||
| realtime | 128 MiB | 768 MiB | 300 MiB |
|
| realtime | 128 MiB | 768 MiB | 300 MiB |
|
||||||
| auth, functions, imgproxy, meta, rest, storage, studio | 128 MiB each | 512 MiB each | 203 MiB (studio) |
|
| auth, functions, imgproxy, meta, rest, storage, studio | 128 MiB each | 512 MiB each | 203 MiB (studio) |
|
||||||
| **Total** | **1.875 GiB** | **9 GiB** | |
|
| **Total** | **2.375 GiB** | **11 GiB** | |
|
||||||
|
|
||||||
The request is what the scheduler sets aside for a service and can never be given to anything else.
|
The request is what the scheduler sets aside for a service and can never be given to anything else.
|
||||||
The limit is the ceiling the kernel kills the container at. They do different jobs here.
|
The limit is the ceiling the kernel kills the container at. They do different jobs here.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import tty
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
VERSION = "1.6.4"
|
VERSION = "1.6.5"
|
||||||
CONFIG_PATH = os.environ.get("SUPASPIN_CONFIG", "/etc/supaspin/config.yaml")
|
CONFIG_PATH = os.environ.get("SUPASPIN_CONFIG", "/etc/supaspin/config.yaml")
|
||||||
STATE_DIR = os.environ.get("SUPASPIN_STATE", "/var/lib/supaspin")
|
STATE_DIR = os.environ.get("SUPASPIN_STATE", "/var/lib/supaspin")
|
||||||
CHART_REPO = "https://supabase-community.github.io/supabase-kubernetes"
|
CHART_REPO = "https://supabase-community.github.io/supabase-kubernetes"
|
||||||
@@ -56,7 +56,9 @@ CERTS_NS = "supaspin-certs"
|
|||||||
# The db image is 6 GB because of the analytics logs: browser-test traffic wrote about 1.2 GB of
|
# The db image is 6 GB because of the analytics logs: browser-test traffic wrote about 1.2 GB of
|
||||||
# logs a day on instance 2 (2026-10-14), the nightly cleanup keeps LOG_RETENTION of them, and the
|
# logs a day on instance 2 (2026-10-14), the nightly cleanup keeps LOG_RETENTION of them, and the
|
||||||
# write-ahead log may take up to 1 GB on top. That peaks near 4.5 GB; 2 GB filled up on instance 4.
|
# write-ahead log may take up to 1 GB on top. That peaks near 4.5 GB; 2 GB filled up on instance 4.
|
||||||
VOLUMES = {"db": 6144, "storage": 1024, "functions": 256,
|
# Doubled to 12 GB on 2026-11-03 at Igor's request: instance 4 had reached 4.7 GB of its 6 GB and
|
||||||
|
# instances kept having to be recreated when the image filled up.
|
||||||
|
VOLUMES = {"db": 12288, "storage": 1024, "functions": 256,
|
||||||
"snippets": 256, "deno": 256, "pgsodium": 256}
|
"snippets": 256, "deno": 256, "pgsodium": 256}
|
||||||
# The user Postgres runs as inside supabase/postgres 17.6.1.136 (`id postgres` in the image).
|
# The user Postgres runs as inside supabase/postgres 17.6.1.136 (`id postgres` in the image).
|
||||||
# Production's older image used 2000; do not copy that number.
|
# Production's older image used 2000; do not copy that number.
|
||||||
@@ -74,7 +76,8 @@ DB_UID, DB_GID = 100, 101
|
|||||||
# realtime 269 / 287 / 300 MiB against 384
|
# realtime 269 / 287 / 300 MiB against 384
|
||||||
# kong 260 / 269 / 265 MiB against 512
|
# kong 260 / 269 / 265 MiB against 512
|
||||||
# studio 203, storage 187, rest 162 MiB against 256
|
# studio 203, storage 187, rest 162 MiB against 256
|
||||||
# db 163 / 175 / 273 MiB against 2048 - left alone, it has room
|
# db 163 / 175 / 273 MiB against 2048 - doubled to 1024 / 4096 on 2026-11-03 at Igor's
|
||||||
|
# request, together with the db image, so a larger database has room to work in
|
||||||
# On 2026-10-06 instance 3 filled its 2 GB database image with logs: analytics (Logflare) never
|
# On 2026-10-06 instance 3 filled its 2 GB database image with logs: analytics (Logflare) never
|
||||||
# deletes what it stored, the db died of it, and vector's undelivered backlog was killed against
|
# deletes what it stored, the db died of it, and vector's undelivered backlog was killed against
|
||||||
# the 1024 MiB ceiling every few minutes. The fix is setup_log_retention: a pg_cron job deletes
|
# the 1024 MiB ceiling every few minutes. The fix is setup_log_retention: a pg_cron job deletes
|
||||||
@@ -84,7 +87,7 @@ DB_UID, DB_GID = 100, 101
|
|||||||
# would take the memory away from everything else on the node straight away.
|
# would take the memory away from everything else on the node straight away.
|
||||||
SERVICES = ["db", "analytics", "kong", "auth", "functions", "imgproxy", "meta",
|
SERVICES = ["db", "analytics", "kong", "auth", "functions", "imgproxy", "meta",
|
||||||
"realtime", "rest", "storage", "studio", "vector"]
|
"realtime", "rest", "storage", "studio", "vector"]
|
||||||
MEMORY = {"db": (512, 2048), "analytics": (128, 1536), "kong": (128, 768), "realtime": (128, 768),
|
MEMORY = {"db": (1024, 4096), "analytics": (128, 1536), "kong": (128, 768), "realtime": (128, 768),
|
||||||
"vector": (128, 512)}
|
"vector": (128, 512)}
|
||||||
DEFAULT_MEMORY = (128, 512)
|
DEFAULT_MEMORY = (128, 512)
|
||||||
|
|
||||||
|
|||||||
@@ -109,10 +109,10 @@ class Values(unittest.TestCase):
|
|||||||
|
|
||||||
def test_memory_caps_add_up(self):
|
def test_memory_caps_add_up(self):
|
||||||
req, lim = sp.total_memory_mib()
|
req, lim = sp.total_memory_mib()
|
||||||
# db 2048, analytics 1536, kong 768, realtime 768, vector 512, the other seven 512 each
|
# db 4096, analytics 1536, kong 768, realtime 768, vector 512, the other seven 512 each
|
||||||
self.assertEqual(lim, 2048 + 1536 + 768 + 768 + 512 + 7 * 512)
|
self.assertEqual(lim, 4096 + 1536 + 768 + 768 + 512 + 7 * 512)
|
||||||
self.assertEqual(lim / 1024, 9.0)
|
self.assertEqual(lim / 1024, 11.0)
|
||||||
self.assertEqual(req, 512 + 11 * 128) # 1.875 GiB
|
self.assertEqual(req, 1024 + 11 * 128) # 2.375 GiB
|
||||||
# Measured on three running instances on 2026-09-22; the ceiling has to stay clear of the
|
# Measured on three running instances on 2026-09-22; the ceiling has to stay clear of the
|
||||||
# highest use, or the kernel kills the container in a loop the way it did on instance 1.
|
# highest use, or the kernel kills the container in a loop the way it did on instance 1.
|
||||||
self.assertGreaterEqual(sp.MEMORY["analytics"][1], 1536) # used 694 MiB, died at 768
|
self.assertGreaterEqual(sp.MEMORY["analytics"][1], 1536) # used 694 MiB, died at 768
|
||||||
@@ -125,8 +125,9 @@ class Values(unittest.TestCase):
|
|||||||
self.assertGreaterEqual(sp.DEFAULT_MEMORY[1], 512) # studio used 203 against 256
|
self.assertGreaterEqual(sp.DEFAULT_MEMORY[1], 512) # studio used 203 against 256
|
||||||
# The requests stay where they were: the node is shared with production and reserving
|
# The requests stay where they were: the node is shared with production and reserving
|
||||||
# what the instances really use would leave production nothing to schedule into.
|
# what the instances really use would leave production nothing to schedule into.
|
||||||
|
# Only db's went up, from 512 to 1024 MiB, with its limit (2026-11-03).
|
||||||
self.assertTrue(all(m[0] == 128 for s, m in sp.MEMORY.items() if s != "db"))
|
self.assertTrue(all(m[0] == 128 for s, m in sp.MEMORY.items() if s != "db"))
|
||||||
self.assertEqual(req / 1024, 1.875)
|
self.assertEqual(req / 1024, 2.375)
|
||||||
for svc in sp.SERVICES:
|
for svc in sp.SERVICES:
|
||||||
self.assertEqual(self.v["deployment"][svc]["priorityClassName"], "supaspin-low")
|
self.assertEqual(self.v["deployment"][svc]["priorityClassName"], "supaspin-low")
|
||||||
self.assertIn("memory", self.v["deployment"][svc]["resources"]["limits"])
|
self.assertIn("memory", self.v["deployment"][svc]["resources"]["limits"])
|
||||||
@@ -143,7 +144,7 @@ class Values(unittest.TestCase):
|
|||||||
|
|
||||||
def test_volume_caps(self):
|
def test_volume_caps(self):
|
||||||
p = self.v["persistence"]
|
p = self.v["persistence"]
|
||||||
self.assertEqual(p["db"]["size"], "6144Mi")
|
self.assertEqual(p["db"]["size"], "12288Mi")
|
||||||
self.assertEqual(p["storage"]["size"], "1024Mi")
|
self.assertEqual(p["storage"]["size"], "1024Mi")
|
||||||
for vol in ("functions", "snippets", "deno", "pgsodium"):
|
for vol in ("functions", "snippets", "deno", "pgsodium"):
|
||||||
self.assertEqual(p[vol]["size"], "256Mi")
|
self.assertEqual(p[vol]["size"], "256Mi")
|
||||||
@@ -478,7 +479,7 @@ class Manifests(unittest.TestCase):
|
|||||||
pv = sp.pv_manifest(CFG, 2, "db")
|
pv = sp.pv_manifest(CFG, 2, "db")
|
||||||
self.assertEqual(pv["spec"]["claimRef"], {"namespace": "supaspin-2", "name": "supa2-supabase-db"})
|
self.assertEqual(pv["spec"]["claimRef"], {"namespace": "supaspin-2", "name": "supa2-supabase-db"})
|
||||||
self.assertEqual(pv["spec"]["local"]["path"], "/data/supabase/2/db/data")
|
self.assertEqual(pv["spec"]["local"]["path"], "/data/supabase/2/db/data")
|
||||||
self.assertEqual(pv["spec"]["capacity"]["storage"], "6144Mi")
|
self.assertEqual(pv["spec"]["capacity"]["storage"], "12288Mi")
|
||||||
self.assertEqual(pv["spec"]["persistentVolumeReclaimPolicy"], "Retain")
|
self.assertEqual(pv["spec"]["persistentVolumeReclaimPolicy"], "Retain")
|
||||||
self.assertEqual(pv["spec"]["nodeAffinity"]["required"]["nodeSelectorTerms"][0]
|
self.assertEqual(pv["spec"]["nodeAffinity"]["required"]["nodeSelectorTerms"][0]
|
||||||
["matchExpressions"][0]["values"], ["node1"])
|
["matchExpressions"][0]["values"], ["node1"])
|
||||||
|
|||||||
Reference in New Issue
Block a user