#!/usr/bin/env bash # # docker-userland-proxy-off.sh # # 关闭 Docker userland-proxy,消除"每个发布端口一个 docker-proxy 进程"的内存开销。 # 实测收益:14 个发布端口时约省 70-90MB 常驻内存。 # 现代 Docker(>=23,dockerd 统一监听发布端口)关闭后本机 127.0.0.1 访问仍可用, # 外部访问走 iptables DNAT 不受影响。 # # 适用系统: Debian/Ubuntu(systemd)/ Alpine(OpenRC) # 要求: root 权限 # # 使用方法: # URL="https://git.1-h.cc/Scripts/Linux/raw/branch/2026/docker-userland-proxy-off.sh"; curl -fsSL "$URL" | bash # # 幂等: 已关闭时直接报告"已生效"并退出,不会重复重启 docker。 set -euo pipefail # 颜色 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # root 检查 if [ "$(id -u)" -ne 0 ]; then echo -e "${RED}错误:需要 root 权限${NC}" >&2 exit 1 fi # docker 检查 if ! command -v docker >/dev/null 2>&1; then echo -e "${RED}错误:未检测到 docker${NC}" >&2 exit 1 fi # --- 1. 检测服务管理器,决定重启命令 --- RESTART_CMD="" if [ -f /etc/alpine-release ] && command -v rc-service >/dev/null 2>&1; then RESTART_CMD="rc-service docker restart" echo -e "${GREEN}[系统] Alpine / OpenRC${NC}" elif command -v systemctl >/dev/null 2>&1; then RESTART_CMD="systemctl restart docker" echo -e "${GREEN}[系统] systemd${NC}" else echo -e "${RED}错误:无法识别的服务管理器(仅支持 Alpine/OpenRC 与 systemd)${NC}" >&2 exit 1 fi # --- 2. 幂等检查:当前是否已生效 --- # 注意: docker info 的 EnableUserlandProxy 字段只在 true 时显示,false 时字段消失, # 因此以 daemon.json 配置为准,docker info 仅作辅助。 CFG_VAL="unset" if [ -f /etc/docker/daemon.json ]; then if command -v jq >/dev/null 2>&1; then CFG_VAL=$(jq -r '."userland-proxy" // "unset"' /etc/docker/daemon.json 2>/dev/null || echo unset) elif command -v python3 >/dev/null 2>&1; then CFG_VAL=$(python3 -c 'import json;v=json.load(open("/etc/docker/daemon.json")).get("userland-proxy", "unset");print("false" if v is False else ("true" if v is True else v))' 2>/dev/null || echo unset) elif grep -q '"userland-proxy"[[:space:]]*:[[:space:]]*false' /etc/docker/daemon.json 2>/dev/null; then CFG_VAL="false" fi fi if [ "$CFG_VAL" = "false" ]; then # 注意: [d]ocker-proxy 正则避免 pgrep 匹配到脚本自身;|| true 规避 pipefail 下 pgrep 无匹配(退出1)触发 set -e COUNT=$(pgrep -f '[d]ocker-proxy' 2>/dev/null | wc -l || true) echo -e "${GREEN}[已生效] daemon.json 已配置 userland-proxy: false,当前 docker-proxy 进程数: ${COUNT},无需操作${NC}" exit 0 fi echo -e "${YELLOW}[状态] 当前 daemon.json 未配置 userland-proxy: false(值: ${CFG_VAL})${NC}" # --- 3. 记录优化前内存基线 --- MEM_BEFORE_USED=$(free -m | awk '/^Mem:/{print $3}') MEM_BEFORE_AVAIL=$(free -m | awk '/^Mem:/{print $7}') echo -e "${GREEN}[基线] 优化前内存: used ${MEM_BEFORE_USED}MB / available ${MEM_BEFORE_AVAIL}MB${NC}" # --- 4. 备份现有 daemon.json --- mkdir -p /etc/docker if [ -f /etc/docker/daemon.json ]; then BAK="/etc/docker/daemon.json.bak-$(date +%Y%m%d%H%M%S)" cp /etc/docker/daemon.json "$BAK" echo -e "${GREEN}[备份] $BAK${NC}" fi # --- 5. 合并写入 userland-proxy: false(保留原有其他配置)--- if [ -f /etc/docker/daemon.json ]; then if command -v jq >/dev/null 2>&1; then TMP="/etc/docker/daemon.json.tmp.$$" jq '."userland-proxy" = false' /etc/docker/daemon.json > "$TMP" mv "$TMP" /etc/docker/daemon.json elif command -v python3 >/dev/null 2>&1; then python3 - <<'PYEOF' import json p = "/etc/docker/daemon.json" with open(p) as f: cfg = json.load(f) cfg["userland-proxy"] = False with open(p, "w") as f: json.dump(cfg, f, indent=2) PYEOF else echo -e "${RED}错误:需要 jq 或 python3 来合并 daemon.json,请先安装其一${NC}" >&2 exit 1 fi else printf '{\n "userland-proxy": false\n}\n' > /etc/docker/daemon.json fi echo -e "${GREEN}[配置] /etc/docker/daemon.json -> userland-proxy: false${NC}" cat /etc/docker/daemon.json # --- 6. 重启 docker(容器会短暂中断几秒)--- # 注: systemctl restart 在个别环境下会因时序返回非零,实际已生效; # 因此不在此处退出,改由下面的验证步骤给出真实结果。 echo -e "${YELLOW}[重启] $RESTART_CMD${NC}" if ! $RESTART_CMD; then echo -e "${YELLOW}[提示] 重启命令返回非零,继续验证实际状态...${NC}" fi sleep 6 # 等待 docker 就绪(最多 30s) for _ in $(seq 1 15); do if docker info >/dev/null 2>&1; then break; fi sleep 2 done # --- 7. 验证 --- PROXY_COUNT=$(pgrep -f '[d]ocker-proxy' 2>/dev/null | wc -l || true) if [ "$PROXY_COUNT" = "0" ]; then echo -e "${GREEN}[验证] docker-proxy 进程数: 0${NC}" else echo -e "${RED}[警告] docker-proxy 进程数: ${PROXY_COUNT}(期望 0),userland-proxy 可能未生效,请检查${NC}" fi CONF=$(docker info 2>/dev/null | awk -F': ' '/EnableUserlandProxy/{gsub(/ /,"",$2); print $2}' | head -1) echo -e "${GREEN}[验证] daemon.json 配置: userland-proxy: false(docker info 字段: ${CONF:-不显示=false 时正常})${NC}" MEM_AFTER_USED=$(free -m | awk '/^Mem:/{print $3}') MEM_AFTER_AVAIL=$(free -m | awk '/^Mem:/{print $7}') SAVED=$((MEM_BEFORE_USED - MEM_AFTER_USED)) echo -e "${GREEN}[内存] 优化前: used ${MEM_BEFORE_USED}MB / available ${MEM_BEFORE_AVAIL}MB${NC}" echo -e "${GREEN}[内存] 优化后: used ${MEM_AFTER_USED}MB / available ${MEM_AFTER_AVAIL}MB${NC}" if [ "$SAVED" -gt 0 ]; then echo -e "${GREEN}[内存] 净释放: ${SAVED}MB(used 口径;available 变化: $((MEM_AFTER_AVAIL - MEM_BEFORE_AVAIL))MB)${NC}" else echo -e "${YELLOW}[内存] used 未下降(${SAVED}MB): 可能受页缓存波动影响,以 available 为准(变化: $((MEM_AFTER_AVAIL - MEM_BEFORE_AVAIL))MB)${NC}" fi echo "--- 容器状态 ---" docker ps --format '{{.Names}}\t{{.Status}}' 2>/dev/null || true echo "" echo -e "${YELLOW}验证建议:${NC}" echo " 1. 外部访问发布端口(如 curl http://<公网IP>:<端口>)" echo " 2. 本机 loopback(如 curl http://127.0.0.1:<端口>)" echo " 3. 容器互访(反代容器访问业务容器内网地址)" echo -e "${YELLOW}回滚:${NC} rm /etc/docker/daemon.json && $RESTART_CMD"