#!/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