Audit: what a Felhom box needs — floor, 64 apps' memory and CPU, power (N100 bench); R-993..R-995; R-231 due check read green
Measured on demo-felhom (N100, 16 GB): 61 apps deployed and removed through the product, sampled every 5 s; heavy apps with one real action (Immich 200 photos, Jellyfin transcode, Paperless OCR, Nextcloud 500 MB, ...). sizing.json for the estimator (tools/build_sizing.py), SIZING.md with the proposed minimum machine, METHOD.md. Power is RAPL CPU-package only (no meter). R-993 asks the operator to approve the minimum machine; R-994 lists catalog limits/requests that are too low; R-995 records leftover unnamed volumes after removal. R-231's dated check measured: dooplex-backup.service success, journal green.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""catalog_check.py — §7 C: measured memory against each template's mem_request and mem_limit.
|
||||
|
||||
Writes ../evidence/catalog-check.json and prints two lists:
|
||||
RISK the app's peak > 80 % of its mem_limit, OR any one container's bench peak > 80 % of that
|
||||
container's own compose limit (the limit Docker enforces is per container)
|
||||
OVER the app's typical use > its mem_request (the catalog under-states what it needs)
|
||||
Read only: nothing in the catalog is changed.
|
||||
"""
|
||||
import json, os
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
EV = os.path.join(os.path.dirname(HERE), "evidence")
|
||||
t = json.load(open(os.path.join(EV, "apps-table.json")))
|
||||
risk, over = [], []
|
||||
for a in sorted(t, key=lambda a: a["slug"]):
|
||||
if a["source"] == "unmeasured":
|
||||
continue
|
||||
lim, req = a["catalog_limit_mb"], a["catalog_request_mb"]
|
||||
why = []
|
||||
if lim and a["mem_peak_mb"] and a["mem_peak_mb"] > 0.8 * lim:
|
||||
why.append(f"app peak {a['mem_peak_mb']} MB = {100 * a['mem_peak_mb'] / lim:.0f} % of mem_limit {lim} MB")
|
||||
for cname, c in ((a.get("bench") or {}).get("containers") or {}).items():
|
||||
if c.get("limit_mb") and c["peak_used_mb"] > 0.8 * c["limit_mb"]:
|
||||
why.append(f"container {cname} peak {c['peak_used_mb']} MB = {100 * c['peak_used_mb'] / c['limit_mb']:.0f} % of its {c['limit_mb']} MB limit")
|
||||
if why:
|
||||
risk.append(dict(slug=a["slug"], why=why, source=a["source"]))
|
||||
if req and a["mem_typical_mb"] and a["mem_typical_mb"] > req:
|
||||
over.append(dict(slug=a["slug"], typical_mb=a["mem_typical_mb"], mem_request_mb=req,
|
||||
ratio=round(a["mem_typical_mb"] / req, 1), source=a["source"]))
|
||||
json.dump(dict(risk=risk, over_request=over), open(os.path.join(EV, "catalog-check.json"), "w"), indent=1)
|
||||
print("RISK (peak > 80 % of a limit):", len(risk))
|
||||
for r in risk:
|
||||
print(f" {r['slug']:16} {r['source']:12} " + "; ".join(r["why"]))
|
||||
print("OVER (typical > mem_request):", len(over))
|
||||
for o in sorted(over, key=lambda o: -o["ratio"]):
|
||||
print(f" {o['slug']:16} typical {o['typical_mb']:>5} > request {o['mem_request_mb']:>5} x{o['ratio']} ({o['source']})")
|
||||
Reference in New Issue
Block a user