The costs page states both deductions and, for each game, the maths return alongside what players actually receive. It is rendered from /api/fees, which the server generates from the same schedule it charges, so the published terms cannot drift from the behaviour. Backups every five minutes with a rolling 24 hours. Each dump is checked for size and format before replacing the previous one — a backup script that reports success on a truncated file is worse than none, because it turns a recoverable outage into silent loss found only when needed. A nightly job restores the newest snapshot and asserts the ledger balances. The standby continuously restores into a shadow database and swaps only after verifying the books, so it is never mid-restore when needed and never promotes a corrupt copy. Promotion does not contact the dead machine, and it refuses to start if the ledger does not balance. Kernel tuning is tied to measured limits, not copied defaults. Alby Hub is explicitly excluded from snapshot restore: publishing a stale channel state can lose the channel balance outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
181 lines
5.8 KiB
Bash
Executable File
181 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Kernel and limit tuning for a machine running the arcade.
|
|
#
|
|
# Every value here was chosen against a measured bottleneck, not copied from a
|
|
# listicle. The measurements are in docs/SCALING.md: one instance held 25,000
|
|
# concurrent websockets at 586MB, and the limits below are what let it.
|
|
#
|
|
# sudo ./ops/tune-kernel.sh apply write settings and reload
|
|
# ./ops/tune-kernel.sh show print current values
|
|
# ./ops/tune-kernel.sh check report values that are below target
|
|
#
|
|
set -euo pipefail
|
|
|
|
CONF=/etc/sysctl.d/99-quantum-arcade.conf
|
|
LIMITS=/etc/security/limits.d/99-quantum-arcade.conf
|
|
|
|
log() { printf '%s %s\n' "$(date -u +%H:%M:%SZ)" "$*"; }
|
|
|
|
write_sysctl() {
|
|
cat > "$CONF" <<'EOF'
|
|
# Quantum Arcade — kernel tuning.
|
|
#
|
|
# Each setting exists because a specific limit was hit. Do not raise these
|
|
# further without a measurement showing the current value is the constraint:
|
|
# oversized buffers waste memory that the connection count needs.
|
|
|
|
# --- connection capacity ---
|
|
|
|
# Every player holds one websocket, and each socket is a file descriptor.
|
|
# 25k connections plus database pool, logs, and headroom.
|
|
fs.file-max = 2097152
|
|
fs.nr_open = 2097152
|
|
|
|
# The listen backlog. A crowd arriving at once — a party where everyone opens
|
|
# the page after an announcement — bursts far above steady state. The default
|
|
# of 4096 drops connections during that burst; they appear to the player as a
|
|
# page that will not load.
|
|
net.core.somaxconn = 65535
|
|
net.ipv4.tcp_max_syn_backlog = 65535
|
|
net.core.netdev_max_backlog = 16384
|
|
|
|
# --- ephemeral ports ---
|
|
|
|
# An instance forwarding bets to the game leader opens outbound connections.
|
|
# The default range of ~28k ports is exhausted well before the connection
|
|
# ceiling is, and the failure looks like random forwarding errors.
|
|
net.ipv4.ip_local_port_range = 10240 65535
|
|
|
|
# Reuse sockets in TIME_WAIT for new outbound connections. Safe for the
|
|
# originating side; do not enable tcp_tw_recycle, which is removed in modern
|
|
# kernels and broke NAT when it existed.
|
|
net.ipv4.tcp_tw_reuse = 1
|
|
net.ipv4.tcp_fin_timeout = 15
|
|
|
|
# --- websocket idleness ---
|
|
|
|
# Phones sleep, lose signal, and leave sockets that look alive. Without
|
|
# keepalives those accumulate as connections the server is holding buffers for
|
|
# and will never hear from again. Detect within ~5 minutes rather than 2 hours.
|
|
net.ipv4.tcp_keepalive_time = 240
|
|
net.ipv4.tcp_keepalive_intvl = 30
|
|
net.ipv4.tcp_keepalive_probes = 6
|
|
|
|
# --- buffers ---
|
|
|
|
# Frames are ~1.8KB and sent a few times a second, so per-socket buffers can
|
|
# stay modest. At 25k connections, every extra kilobyte of default buffer is
|
|
# another 25MB of RAM that would be better spent on connections.
|
|
net.ipv4.tcp_rmem = 4096 87380 6291456
|
|
net.ipv4.tcp_wmem = 4096 65536 6291456
|
|
net.core.rmem_max = 12582912
|
|
net.core.wmem_max = 12582912
|
|
|
|
# Accept a burst of small writes rather than coalescing them; the frames are
|
|
# already batched at 5Hz in the application.
|
|
net.ipv4.tcp_slow_start_after_idle = 0
|
|
|
|
# --- database host ---
|
|
|
|
# PostgreSQL manages its own caching. Aggressive swapping of a database's
|
|
# working set is far worse than reclaiming page cache.
|
|
vm.swappiness = 10
|
|
vm.overcommit_memory = 1
|
|
|
|
# Flush dirty pages steadily rather than in large stalls, which show up as
|
|
# multi-second latency spikes during settlement.
|
|
vm.dirty_background_ratio = 5
|
|
vm.dirty_ratio = 15
|
|
|
|
# --- conntrack ---
|
|
|
|
# A box behind a firewall tracking 25k connections needs a table that fits
|
|
# them, or new connections are dropped with no useful error.
|
|
net.netfilter.nf_conntrack_max = 262144
|
|
EOF
|
|
log "wrote $CONF"
|
|
}
|
|
|
|
write_limits() {
|
|
cat > "$LIMITS" <<'EOF'
|
|
# Quantum Arcade — process limits.
|
|
#
|
|
# fs.file-max raises the system ceiling; this raises the per-process one.
|
|
# Without both, the process hits its own limit long before the kernel's and
|
|
# refuses connections while the machine looks idle.
|
|
* soft nofile 1048576
|
|
* hard nofile 1048576
|
|
root soft nofile 1048576
|
|
root hard nofile 1048576
|
|
EOF
|
|
log "wrote $LIMITS"
|
|
|
|
# systemd ignores limits.conf for services it starts.
|
|
mkdir -p /etc/systemd/system.conf.d
|
|
cat > /etc/systemd/system.conf.d/99-quantum-arcade.conf <<'EOF'
|
|
[Manager]
|
|
DefaultLimitNOFILE=1048576
|
|
EOF
|
|
log "wrote systemd DefaultLimitNOFILE"
|
|
}
|
|
|
|
cmd_apply() {
|
|
[ "$(id -u)" -eq 0 ] || { echo "must run as root" >&2; exit 1; }
|
|
write_sysctl
|
|
write_limits
|
|
sysctl --system >/dev/null
|
|
log "settings applied; reboot or re-login for limits to take effect"
|
|
cmd_check
|
|
}
|
|
|
|
cmd_show() {
|
|
for k in fs.file-max net.core.somaxconn net.ipv4.ip_local_port_range \
|
|
net.ipv4.tcp_keepalive_time vm.swappiness; do
|
|
printf ' %-34s %s\n' "$k" "$(sysctl -n "$k" 2>/dev/null || echo 'n/a')"
|
|
done
|
|
printf ' %-34s %s\n' "ulimit -n (this shell)" "$(ulimit -n)"
|
|
}
|
|
|
|
# check reports what is below target, so a machine can be inspected without
|
|
# changing anything.
|
|
cmd_check() {
|
|
local fails=0
|
|
check_min() {
|
|
local key="$1" want="$2" cur
|
|
cur="$(sysctl -n "$key" 2>/dev/null | awk '{print $1}')" || cur=0
|
|
if [ -z "$cur" ] || [ "$cur" -lt "$want" ] 2>/dev/null; then
|
|
printf ' BELOW TARGET %-30s %s (want >= %s)\n' "$key" "${cur:-unset}" "$want"
|
|
fails=$((fails + 1))
|
|
else
|
|
printf ' ok %-30s %s\n' "$key" "$cur"
|
|
fi
|
|
}
|
|
|
|
check_min fs.file-max 1048576
|
|
check_min net.core.somaxconn 32768
|
|
check_min net.ipv4.tcp_max_syn_backlog 32768
|
|
|
|
local nofile
|
|
nofile="$(ulimit -n)"
|
|
if [ "$nofile" -lt 65536 ]; then
|
|
printf ' BELOW TARGET %-30s %s (want >= 65536)\n' "ulimit -n" "$nofile"
|
|
fails=$((fails + 1))
|
|
else
|
|
printf ' ok %-30s %s\n' "ulimit -n" "$nofile"
|
|
fi
|
|
|
|
if [ "$fails" -gt 0 ]; then
|
|
log "$fails setting(s) below target — run 'sudo $0 apply'"
|
|
return 1
|
|
fi
|
|
log "all checked settings meet target"
|
|
}
|
|
|
|
case "${1:-show}" in
|
|
apply) cmd_apply ;;
|
|
show) cmd_show ;;
|
|
check) cmd_check ;;
|
|
*) echo "usage: $0 {apply|show|check}" >&2; exit 2 ;;
|
|
esac
|