From 26e817f2c92c0b43b55594a45e1be224ff30edfb Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Tue, 29 Sep 2026 19:06:01 +0800 Subject: [PATCH] =?UTF-8?q?perf(bootstrap):=20k3s=20=E4=BB=A5=20GOGC=3D50?= =?UTF-8?q?=20=E8=BF=90=E8=A1=8C=EF=BC=8CVelocity=201G=20=E4=BB=A5?= =?UTF-8?q?=E5=86=85=E6=94=B9=E7=94=A8=E4=B8=B2=E8=A1=8C=E5=9B=9E=E6=94=B6?= =?UTF-8?q?=E5=92=8C=20C1=EF=BC=8C=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= =?UTF-8?q?=E7=A9=BA=E8=BD=BD=E5=8E=8B=E5=88=B0=E7=BA=A6=201=20GB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/bootstrap.sh | 62 ++++++++++++++++++++++---- deploy/bootstrap_test.sh | 94 +++++++++++++++++++++++++++++++++++----- deploy/uninstall.sh | 13 ++++++ deploy/uninstall_test.sh | 20 +++++++++ docs/operations.md | 51 ++++++++++++++-------- 5 files changed, 200 insertions(+), 40 deletions(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 3452c43..6153575 100644 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -502,6 +502,9 @@ K3S_REGISTRIES_FILE="/etc/rancher/k3s/registries.yaml" # client certificate are variables for the same reason as the file above. K3S_CONFIG_DROPIN="/etc/rancher/k3s/config.yaml.d/50-felis.yaml" K3S_UNIT_FILE="/etc/systemd/system/k3s.service" +# The installer's environment for the k3s service (write_k3s_service_dropin); k3s's own +# installer rewrites the unit and its .env file, never this. +K3S_SERVICE_DROPIN="/etc/systemd/system/k3s.service.d/50-felis.conf" K3S_KUBECONFIG="/etc/rancher/k3s/k3s.yaml" K3S_KUBELET_CERT="/var/lib/rancher/k3s/agent/client-kubelet.crt" # Where k3s imports image tarballs from as it starts (stage_k3s_airgap_images). @@ -1937,9 +1940,10 @@ configure_k3s_firewall() { install_k3s() { configure_k3s_firewall - # Before the installer runs: a fresh k3s reads the drop-in on its first start. + # Before the installer runs: a fresh k3s reads both drop-ins on its first start. K3S_RESTART_NEEDED=0 write_k3s_config + write_k3s_service_dropin local installer_ran=0 if [ -x "$K3S_BIN" ]; then @@ -1969,7 +1973,7 @@ install_k3s() { # The installer restarts k3s itself; otherwise a changed drop-in or unit takes a # restart to load. Pods keep running across it (k3s leaves the containers be). if [ "$K3S_RESTART_NEEDED" = 1 ] && [ "$installer_ran" = 0 ]; then - log "restarting k3s to load its new settings (${K3S_CONFIG_DROPIN})" + log "restarting k3s to load its new settings (${K3S_CONFIG_DROPIN}, ${K3S_SERVICE_DROPIN})" systemctl restart k3s fi export KUBECONFIG="$K3S_KUBECONFIG" @@ -2039,6 +2043,37 @@ write_k3s_config() { log "wrote ${file}${name:+ (node name pinned to ${name})}" } +# write_k3s_service_dropin runs k3s, and the containerd it starts with its own environment, +# with the Go collector at half the default heap growth (GOGC=50). An idle k3s holds about +# 150 MiB live and by default lets its heap reach twice that before collecting; at 50 it +# collects at one and a half times. Measured on the verification host: k3s 430 -> 370 MiB and +# its containerd 114 -> 104 MiB, for about 2% of one core more while idle. It sets +# K3S_RESTART_NEEDED when the file changed, since k3s reads its environment only as it starts. +write_k3s_service_dropin() { + local file="$K3S_SERVICE_DROPIN" tmp + mkdir -p "$(dirname "$file")" + # Beside its destination, like write_k3s_config's; systemd reads only *.conf from the + # directory, so the temp name is never loaded. + tmp="$(mktemp "${file}.XXXXXX")" + remember_temp "$tmp" + { + echo "# Written by the Felis installer (deploy/bootstrap.sh); a rerun rewrites it." + echo "[Service]" + echo "Environment=GOGC=50" + } > "$tmp" + if [ -f "$file" ] && cmp -s "$tmp" "$file"; then + rm -f "$tmp" + restore_label "$file" + ok "k3s service environment already current" + return 0 + fi + chmod 0644 "$tmp" + mv "$tmp" "$file" + systemctl daemon-reload + K3S_RESTART_NEEDED=1 + log "wrote ${file} (GOGC=50)" +} + # A command-line flag outranks every config file, and k3s's installer writes # INSTALL_K3S_EXEC into the unit's ExecStart one quoted word per line, so installs from # before the drop-in keep "'--write-kubeconfig-mode' \" followed by "'644' \" there. @@ -3865,12 +3900,21 @@ install_velocity_service() { # sees it -- unquoted, that spelling would hand java a stray "legacy112" argument and the unit # would not start. Quoting keeps the whole property one argv item. local legacy_forwarding_servers="${FELIS_LEGACY_FORWARDING_SERVERS}" - # The heap starts small and is not pre-touched. The proxy with its Via plugins holds about 50M - # live; a pre-touched 512M start kept ~0.7 GB resident on an idle network, ~0.25 GB without - # (measured on the verification host). The heap grows toward -Xmx as players arrive, and the - # periodic collection hands the growth back once they have left. FELIS_VELOCITY_XMX is at - # least 256M, so the start never exceeds the ceiling. - local xmx="$FELIS_VELOCITY_XMX" xms="64M" + # The heap starts small and is not pre-touched: the proxy with its Via plugins holds about 50M + # live, and a pre-touched 512M start kept ~0.7 GB resident on an idle network. Up to a 1G + # ceiling (the default, sized for about 100 players) it runs the serial collector and only the + # C1 compiler, 173 MiB idle against 267 MiB under G1 (both measured on the verification host); + # with that little live, a young collection takes milliseconds, and the proxy's compression + # and encryption run in Velocity's native library whichever compiler is on. A larger ceiling + # is for a network where a serial full collection over a big heap would stall every player at + # once, so it keeps G1, whose periodic collection hands the growth back once players have left. + # FELIS_VELOCITY_XMX is at least 256M, so the start never exceeds the ceiling. + local xmx="$FELIS_VELOCITY_XMX" jvm + if [ "$(heap_megabytes "$xmx")" -le 1024 ]; then + jvm="-Xms16M -Xmx${xmx} -XX:+UseSerialGC -XX:TieredStopAtLevel=1" + else + jvm="-Xms64M -Xmx${xmx} -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:G1PeriodicGCInterval=60000" + fi cat > "$VELOCITY_SERVICE" < "$vdir/jre/release" expect "a patched JRE restarts the proxy" "SYSTEMCTL restart felis-velocity" "$(run_velocity_service 1)" expect "a new heap size restarts the proxy" "SYSTEMCTL restart felis-velocity" "$(run_velocity_service 1 3G)" -expect "the unit carries the new ceiling" "java -Xms64M -Xmx3G " "$(cat "$vdir/unit")" +expect "a ceiling above 1G runs G1, whose periodic collection hands idle growth back" \ + "java -Xms64M -Xmx3G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:G1PeriodicGCInterval=60000 -Dmojang" "$(cat "$vdir/unit")" +case "$(cat "$vdir/unit")" in + *AlwaysPreTouch*|*UseSerialGC*|*TieredStopAtLevel*) echo "FAIL a 3G proxy carries a small proxy's flags or pre-touches: $(grep ExecStart "$vdir/unit")"; fails=$((fails + 1)) ;; + *) echo "PASS a 3G proxy keeps G1 and both compilers" ;; +esac +run_velocity_service 1 1024M >/dev/null +expect "1024M is still a small proxy" "java -Xms16M -Xmx1024M -XX:+UseSerialGC " "$(cat "$vdir/unit")" +run_velocity_service 1 1025M >/dev/null +expect "a megabyte past 1G is a large one" "java -Xms64M -Xmx1025M -XX:+UseG1GC " "$(cat "$vdir/unit")" run_velocity_service 1 384M >/dev/null -expect "a small ceiling keeps the same small start" "java -Xms64M -Xmx384M " "$(cat "$vdir/unit")" +expect "a small ceiling runs the small proxy's flags" \ + "java -Xms16M -Xmx384M -XX:+UseSerialGC -XX:TieredStopAtLevel=1 -Dmojang" "$(cat "$vdir/unit")" # `felis rotate-token velocity` rewrites service-token, which the plugin re-reads by itself: # that line alone changing leaves the proxy running, and any other change restarts it. props="$vdir/v/plugins/felis-link/felis-link.properties" @@ -3015,10 +3026,7 @@ expect "a heap in megabytes is kept" "768" "$(heap 768m)" for bad in 1 1K 0G 01G G -1G 1.5G 9999999G; do expect "the heap spelling '$bad' is refused" "0" "$(heap "$bad")" done -case "$(awk '/^install_velocity_service\(\) \{/,/^}/' "$BS")" in - *'-Xms${xms} -Xmx${xmx} '*) echo "PASS the proxy unit takes its heap from FELIS_VELOCITY_XMX" ;; - *) echo "FAIL the proxy unit's heap is not FELIS_VELOCITY_XMX"; fails=$((fails + 1)) ;; -esac +# The unit written for each ceiling is checked with the rerun tests above. # --- reproducible image ids --------------------------------------------------------------- # restart_existing_system_servers compares image ids across runs; a default BuildKit @@ -3230,6 +3238,63 @@ out="$(run_k3s_config "$kdir/broken.crt" "$kdir/k3s" renamed-host)" expect "a certificate openssl cannot parse is a warning, not a failed install" "WARN: could not read this node's k3s name" "$out" expect "and the drop-in is still written" 'write-kubeconfig-mode: "0600"' "$(cat "$dropin")" +# k3s's Go collector runs at GOGC=50 through a systemd drop-in, written beside itself and +# loaded (with a k3s restart) only when it changed. +svblock="$(awk '/^write_k3s_service_dropin\(\) \{/,/^}/' "$BS")" +[ -n "$svblock" ] || { echo "FAIL: no write_k3s_service_dropin found in $BS"; exit 1; } +svdropin="$kdir/k3s.service.d/50-felis.conf" +run_k3s_service_dropin() { + DROPIN="$svdropin" bash -c ' + set -Eeuo pipefail + ok() { echo "OK: $*"; }; log() { echo "LOG: $*"; } + remember_temp() { :; } + restore_label() { echo "RELABEL: $*"; } + systemctl() { echo "SYSTEMCTL: $*"; } + mktemp() { local p; p="$(command mktemp "$@")"; echo "MKTEMP: $(dirname "$p")" >&2; echo "$p"; } + K3S_SERVICE_DROPIN="$DROPIN" + '"$svblock"' + K3S_RESTART_NEEDED=0 + write_k3s_service_dropin + echo "RESTART=$K3S_RESTART_NEEDED"' 2>&1 +} +out="$(run_k3s_service_dropin)" +if [ "$(cat "$svdropin")" = '# Written by the Felis installer (deploy/bootstrap.sh); a rerun rewrites it. +[Service] +Environment=GOGC=50' ]; then + echo "PASS k3s runs its Go collector at GOGC=50" +else + echo "FAIL the k3s service drop-in is:"; cat "$svdropin"; fails=$((fails + 1)) +fi +expect "a new service drop-in is loaded" "SYSTEMCTL: daemon-reload" "$out" +expect "a new service drop-in asks for a k3s restart" "RESTART=1" "$out" +if [ "$(printf '%s\n' "$out" | sed -n 's/^MKTEMP: //p' | sort -u)" = "$kdir/k3s.service.d" ]; then + echo "PASS the service drop-in is made beside itself, never under /tmp" +else + echo "FAIL temporary files for the service drop-in were made in: $(printf '%s\n' "$out" | sed -n 's/^MKTEMP: //p')"; fails=$((fails + 1)) +fi +if [ "$(stat -c %a "$svdropin" 2>/dev/null || stat -f %Lp "$svdropin")" = 644 ]; then + echo "PASS the service drop-in is readable like the unit it extends" +else + echo "FAIL the service drop-in must be 0644"; fails=$((fails + 1)) +fi +out="$(run_k3s_service_dropin)" +expect "an unchanged service drop-in restarts nothing" "RESTART=0" "$out" +expect "an unchanged service drop-in says so" "OK: k3s service environment already current" "$out" +case "$out" in + *daemon-reload*) echo "FAIL an unchanged service drop-in must not reload systemd"; fails=$((fails + 1)) ;; + *) echo "PASS an unchanged service drop-in reloads nothing" ;; +esac +expect "an unchanged service drop-in is still relabelled" "RELABEL: $svdropin" "$out" +if [ "$(ls "$kdir/k3s.service.d")" = "50-felis.conf" ]; then + echo "PASS no temporary file is left beside the service drop-in" +else + echo "FAIL k3s.service.d holds: $(ls "$kdir/k3s.service.d")"; fails=$((fails + 1)) +fi +printf '[Service]\nEnvironment=GOGC=100\n' > "$svdropin" +out="$(run_k3s_service_dropin)" +expect "an edited service drop-in is put back" "Environment=GOGC=50" "$(cat "$svdropin")" +expect "and k3s is restarted onto it" "RESTART=1" "$out" + sblock="$(awk '/^strip_k3s_kubeconfig_mode_flag\(\) \{/,/^}/' "$BS")" [ -n "$sblock" ] || { echo "FAIL: no strip_k3s_kubeconfig_mode_flag found in $BS"; exit 1; } # ExecStart as k3s's installer writes it (copied off an install made with the old flag). @@ -3290,17 +3355,19 @@ iblock="$(awk '/^install_k3s\(\) \{/,/^}/' "$BS")" iblock="$iblock $(awk '/^version_newer\(\) \{/,/^}/' "$BS") $(awk '/^k3s_upgrade_allowed\(\) \{/,/^}/' "$BS")" -run_install_k3s() { # $1: installed version ("" = none), $2: drop-in changed (0|1), $3: FELIS_UPGRADE_DEPS - INSTALLED="$1" CHANGED="$2" UPGRADE="${3:-0}" KDIR="$kdir" bash -c ' +run_install_k3s() { # $1: installed version ("" = none), $2: config drop-in changed (0|1), $3: FELIS_UPGRADE_DEPS, $4: service drop-in changed (0|1) + INSTALLED="$1" CHANGED="$2" UPGRADE="${3:-0}" SVC_CHANGED="${4:-0}" KDIR="$kdir" bash -c ' set -Eeuo pipefail ok() { echo "OK: $*"; }; log() { echo "LOG: $*"; }; warn() { echo "WARN: $*"; } die() { echo "DIE: $*"; exit 1; } FELIS_K3S_VERSION=v1.36.4+k3s1 FELIS_UPGRADE_DEPS="$UPGRADE" K3S_BIN_DIR="$KDIR" K3S_BIN="$KDIR/k3s-under-test" K3S_CONFIG_DROPIN=/etc/rancher/k3s/config.yaml.d/50-felis.yaml K3S_KUBECONFIG=/etc/rancher/k3s/k3s.yaml + K3S_SERVICE_DROPIN=/etc/systemd/system/k3s.service.d/50-felis.conf rm -f "$K3S_BIN" if [ -n "$INSTALLED" ]; then printf "#!/bin/sh\necho \"k3s version %s (abc)\"\n" "$INSTALLED" > "$K3S_BIN"; chmod +x "$K3S_BIN"; fi configure_k3s_firewall() { :; } write_k3s_config() { [ "$CHANGED" = 0 ] || K3S_RESTART_NEEDED=1; } + write_k3s_service_dropin() { echo "SERVICE-DROPIN"; [ "$SVC_CHANGED" = 0 ] || K3S_RESTART_NEEDED=1; } strip_k3s_kubeconfig_mode_flag() { :; } run_k3s_installer() { echo "INSTALLER"; printf "#!/bin/sh\n" > "$K3S_BIN"; command chmod +x "$K3S_BIN"; } stage_k3s_airgap_images() { echo "STAGE"; } @@ -3316,9 +3383,12 @@ expect "the admin kubeconfig is made root-only once the node is up" "READY CHMOD: 0600 /etc/rancher/k3s/k3s.yaml" "$out" out="$(run_install_k3s v1.36.4+k3s1 0)" case "$out" in *"restart k3s"*) echo "FAIL unchanged k3s settings must not restart k3s"; fails=$((fails + 1)) ;; *) echo "PASS unchanged k3s settings restart nothing" ;; esac +expect "a new service environment alone restarts a running k3s" "SYSTEMCTL: restart k3s" "$(run_install_k3s v1.36.4+k3s1 0 0 1)" out="$(run_install_k3s "" 1)" expect "a fresh host runs the k3s installer and waits for the node" "INSTALLER SYSTEMCTL: enable --now k3s" "$out" +expect "a fresh k3s has its service environment before its first start" "SERVICE-DROPIN INSTALLER " \ + "$(printf '%s\n' "$out" | grep -E '^(SERVICE-DROPIN|INSTALLER)$' | tr '\n' ' ')" expect "a fresh host stages k3s's images before k3s first starts" "STAGE INSTALLER" "$out" expect "a fresh host's kubeconfig is made root-only too" "CHMOD: 0600 /etc/rancher/k3s/k3s.yaml" "$out" diff --git a/deploy/uninstall.sh b/deploy/uninstall.sh index 450a2e3..25575bb 100644 --- a/deploy/uninstall.sh +++ b/deploy/uninstall.sh @@ -321,6 +321,16 @@ stop_database_pod() { || warn "${PG_DEPLOYMENT} did not stop within 2 minutes; its cluster recovers from its WAL on the next start" } +# remove_k3s_service_dropin deletes the environment the installer gives the k3s service +# (bootstrap.sh, write_k3s_service_dropin). k3s-uninstall.sh removes the unit and its .env +# file and leaves the unit's drop-in directory. +remove_k3s_service_dropin() { + [ -f "${UNIT_DIR}/k3s.service.d/50-felis.conf" ] || return 0 + rm -f "${UNIT_DIR}/k3s.service.d/50-felis.conf" + rmdir "${UNIT_DIR}/k3s.service.d" 2>/dev/null || true + ok "k3s service environment removed" +} + remove_k3s() { local stamp [ "$PURGE" = 1 ] || stop_database_pod @@ -336,6 +346,7 @@ remove_k3s() { if [ -x "${K3S_BIN_DIR}/k3s-uninstall.sh" ]; then log "running k3s-uninstall.sh" "${K3S_BIN_DIR}/k3s-uninstall.sh" >/dev/null 2>&1 || warn "k3s-uninstall.sh reported an error; check /var/lib/rancher and /etc/rancher" + remove_k3s_service_dropin ok "k3s removed" else warn "k3s is at ${K3S_BIN_DIR}/k3s but ${K3S_BIN_DIR}/k3s-uninstall.sh is missing; remove k3s by hand" @@ -522,6 +533,8 @@ main() { case "$K3S_MODE" in remove) remove_k3s ;; keep) remove_from_cluster ;; + # k3s was removed some other way; what it left of the installer's goes too. + absent) remove_k3s_service_dropin ;; esac remove_nft_tables remove_firewalld_rules "$game" "$nano" diff --git a/deploy/uninstall_test.sh b/deploy/uninstall_test.sh index 0117630..24bacbf 100644 --- a/deploy/uninstall_test.sh +++ b/deploy/uninstall_test.sh @@ -35,6 +35,8 @@ fresh_host() { for u in felis-db-backup.timer felis-db-backup.service felis-velocity.service felis-postgres-firewall.service; do printf '[Unit]\n' > "$root/h/units/$u" done + mkdir -p "$root/h/units/k3s.service.d" + printf '[Service]\nEnvironment=GOGC=50\n' > "$root/h/units/k3s.service.d/50-felis.conf" printf '[Service]\nExecStart=/usr/local/bin/cloudflared --config %s tunnel run\n' "$root/h/etc/cloudflared.yml" \ > "$root/h/units/cloudflared-felis.service" printf 'tunnel: abc\ncredentials-file: %s\n' "$root/h/cf/abc.json" > "$root/h/etc/cloudflared.yml" @@ -155,6 +157,9 @@ refute "keep-data leaves the database alone" "DROP DATABASE" "$calls" || { echo "FAIL /opt/felis or the host binary is still there"; fails=$((fails + 1)); } [ -z "$(ls "$root/h/units")" ] && echo "PASS every Felis unit file is removed" \ || { echo "FAIL units left: $(ls "$root/h/units")"; fails=$((fails + 1)); } +[ ! -e "$root/h/units/k3s.service.d" ] \ + && echo "PASS the k3s service environment k3s's uninstaller leaves is removed with its directory" \ + || { echo "FAIL the k3s service drop-in is still there: $(ls -R "$root/h/units")"; fails=$((fails + 1)); } expect "the timers are disabled" "SYSTEMCTL disable --now felis-db-backup.timer" "$calls" expect "the velocity user is removed" "USERDEL felis-velocity" "$calls" expect "the run ends pointing at the reinstall steps" "Reinstall on top of kept data" "$out" @@ -190,6 +195,9 @@ expect "Felis's volumes are retained before their claims go" 'KUBE patch pv pvc- refute "a volume of another namespace is not touched" "patch pv pvc-9" "$calls" expect "Felis's namespaces are deleted" "KUBE delete namespace felis minecraft felis-build" "$calls" expect "the CRD is deleted" "KUBE delete crd minecraftservers.felis.lolicon.best" "$calls" +[ -f "$root/h/units/k3s.service.d/50-felis.conf" ] \ + && echo "PASS a k3s that stays keeps its service environment, like its config" \ + || { echo "FAIL the kept k3s lost its service drop-in"; fails=$((fails + 1)); } out="$(fresh_host; run_uninstall down --yes)" expect "a k3s that does not answer stops the run" "k3s does not answer" "$out" @@ -198,6 +206,18 @@ run_uninstall down --yes --keep-k3s >/dev/null calls="$(cat "$root/calls")" refute "--keep-k3s never runs k3s's uninstaller" "RUN k3s-uninstall.sh" "$calls" +# --- k3s already gone ---------------------------------------------------------------------- +fresh_host +rm -f "$root/h/bin/k3s" +printf '[Service]\nLimitNOFILE=4096\n' > "$root/h/units/k3s.service.d/90-admin.conf" +run_uninstall down --yes >/dev/null +[ ! -e "$root/h/units/k3s.service.d/50-felis.conf" ] \ + && echo "PASS a k3s removed some other way does not leave the installer's service environment" \ + || { echo "FAIL the k3s service drop-in outlived k3s"; fails=$((fails + 1)); } +[ -f "$root/h/units/k3s.service.d/90-admin.conf" ] \ + && echo "PASS a drop-in someone else wrote beside it stays, with the directory" \ + || { echo "FAIL the uninstall removed a k3s drop-in it did not write"; fails=$((fails + 1)); } + # --- purge ------------------------------------------------------------------------------- fresh_host out="$(run_uninstall "default felis minecraft" --purge --yes)" diff --git a/docs/operations.md b/docs/operations.md index 0f71e20..5d395d6 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -216,21 +216,30 @@ to 0 for about 8 minutes on the reference VM. ### What the platform itself uses Measured on the verification host (4 vCPU, 5.5 GB RAM, 6 GB swap, CentOS Stream 9 -aarch64) with the control plane, the login and lobby system servers and one idle Paper -server running **[VM-VERIFIED]**: +aarch64) on an idle network, as each process's proportional set size (PSS: a page shared +by several processes is split among them; `/proc//smaps_rollup`) **[VM-VERIFIED]**: -| Process | Resident memory | +| Process | Memory (PSS) | |---|---| -| k3s (server, kubelet, containerd) | ~1.1 GB | -| Velocity (`-Xms64M -Xmx1G`, idle; it grows with players) | ~0.25 GB | -| lobby (Paper, pod limit 1 GiB) | ~0.7–0.85 GB | -| login (Limbo, pod limit 512 MiB) | ~0.16 GB | -| felis-api, felis-operator, registry gate | ~50 MB each | -| PostgreSQL (the felis-postgres pod) | ~30 MB plus page cache | -| **Total in use** | **~2.9 GB** | +| k3s (API server, controllers, scheduler, kubelet) | ~370 MiB | +| k3s's containerd and the pods' shims | ~170 MiB | +| CoreDNS and the local-path volume provisioner | ~105 MiB | +| Velocity (`-Xms16M -Xmx1G`, idle; it grows with players) | ~175 MiB | +| felis-api, felis-operator, registry gate | ~85 MiB together | +| Image registry | ~25 MiB | +| PostgreSQL (the felis-postgres pod) | ~40 MiB plus page cache | +| **Infrastructure total** | **~1 GB** | -Every game server adds the memory its owner gave it: the pod's limit equals its request, -and the JVM heap is derived from it (§1a). Quotas cap it per user (panel → 管理 → 配额). +The installer runs k3s, and the containerd it starts, with Go's collector at half its +default heap growth (`GOGC=50`, in `/etc/systemd/system/k3s.service.d/50-felis.conf`): an +idle k3s holds about 150 MiB live and would otherwise let its heap reach twice that before +collecting. It saves about 70 MiB for about 2% of one core. An install from before this +picks it up on its next installer run, which restarts k3s; the pods keep running. + +The login (Limbo, pod limit 512 MiB, ~0.16 GB) and lobby (Paper, pod limit 1 GiB, +~0.7–0.85 GB) system servers come on top, and every game server adds the memory its owner +gave it: the pod's limit equals its request, and the JVM heap is derived from it (§1a). +Quotas cap it per user (panel → 管理 → 配额). A release install builds nothing (§1). When the installer builds on the host its peak is the image builds (Docker plus a Gradle container). Afterwards it stops Docker, and Docker's @@ -249,15 +258,19 @@ adds a 2 GiB `/swapfile`. The player-count rows are planning figures, not measurements: a Minecraft server's cost depends mostly on what its players do (view distance, redstone, mods). Size RAM as the -platform's ~3 GB plus the sum of the servers you expect to run at once, then add a -quarter for the page cache and PostgreSQL. Velocity itself needs little per player; raise -its heap when `journalctl -u felis-velocity` shows long GC pauses or `OutOfMemoryError`. +infrastructure's ~1 GB and the login and lobby servers' ~1 GB, plus the sum of the servers +you expect to run at once, then add a quarter for the page cache and PostgreSQL. Velocity +itself needs little per player; raise its heap when `journalctl -u felis-velocity` shows +long GC pauses or `OutOfMemoryError`. `FELIS_VELOCITY_XMX` (default `1G`, at least `256M`, written `M` or `G`) is read on -every installer run. The heap starts at 64M and grows toward the maximum as players arrive; -a periodic collection hands the growth back once they have left. Changing it rewrites the -unit, and the rerun restarts the proxy, which disconnects everyone online; do it in a quiet -hour **[VM-VERIFIED]**: +every installer run. Up to 1G the heap starts at 16M and the proxy runs the serial collector +and only the C1 compiler: its plugins hold about 50M live, so a collection takes +milliseconds, and compression and encryption run in Velocity's native library. Above 1G it +runs G1 from a 64M start, since a serial full collection over a large heap would stall +every player at once, and a periodic collection hands the growth back once players have +left. Changing it rewrites the unit, and the rerun restarts the proxy, which disconnects +everyone online; do it in a quiet hour **[VM-VERIFIED]**: ``` curl -fsSL /deploy/bootstrap.sh | sudo FELIS_VELOCITY_XMX=2G bash