配置中心不必上etcd:一张表加一个接口

一、为什么要在服务器上装一套配置中心

很多个人站长维护三四台机器之后,第一件体会到痛苦的事情不是服务器性能不够,而是「改一个参数要登三台机器」。Nginx 上游地址变了、应用要加一个限流阈值、定时任务的时间要调,这些都是配置。当配置散落在每台机器的文件里,你会遇到三个几乎无解的问题:版本不一致、变更不可追溯、回滚靠记忆。

配置中心就是把这些散落的配置收拢到一个地方,由它统一下发、统一版本、统一监控。业界最出名的是 etcd 加 Apollo、Nacos 这一套,但对一个只有几台机器的个人站来说,那套东西本身的运维成本比被管的应用还高。etcd 需要三节点保证仲裁、需要定期快照、需要有人盯着 Raft 日志,装完之后你每天要担心的东西反而变多了。

所以这篇写的是另一种思路:用一台机器上已经存在的 SQLite 或者 MySQL 做配置表,加一个最简单的 HTTP 接口做下发,再配合 Nginx map 或者脚本拉取完成生效。核心思想是「配置的唯一来源」而不是「配置中心软件」。工具可以很简单,纪律必须很硬。

二、先分清哪一类配置值得集中管理

不是所有配置都适合搬进中心。搬错了地方,等于把手动修改文件的麻烦换成了「配置中心挂了站也挂了」的更大麻烦。按下面三类分开处理:

  • 基础设施配置:Nginx 的 upstream 地址、证书路径、日志级别。这类配置对外部依赖敏感,一旦中心不可用应该沿用本地最后一份,绝对不能出现「拉不到配置就启动失败」。
  • 业务开关:限流阈值、维护模式开关、某个功能是否启用。这类最适合放中心,因为改动频繁、影响面可控、需要留操作记录。
  • 机密信息:数据库密码、API key。这类不要放普通配置表,只能放权限收紧的单独表或者文件,接口返回时必须脱敏,读取要单独授权。

判据很简单:如果这份配置丢了,站会不会挂?会挂的留在本地做兜底,不会挂的再搬上来。反过来判断也行:如果这份配置改了要立刻生效,那它就该在中心;如果它改了需要重启才生效,放中心的意义就不大,因为重启本身就是一次人工介入的机会。

三、最简可用形态:一张表加一个只读接口

先建表。用 SQLite 就够,因为配置的读写频率极低,一天几百次都算多。关键是这张表要自带三类信息:值、版本、变更记录。

CREATE TABLE config (
  k          TEXT PRIMARY KEY,   -- 配置键,如 nginx.upstream.api
  v          TEXT NOT NULL,      -- 配置值
  ver        INTEGER NOT NULL,   -- 每次修改自增
  updated_at TEXT NOT NULL,
  note       TEXT                -- 为什么改,必填
);
CREATE TABLE config_log (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  k          TEXT, old_v TEXT, new_v TEXT,
  changed_at TEXT, changed_by TEXT, note TEXT
);

接口只做两件事:全量拉取,和按版本号增量拉取。不要一开始就做推送,推送需要长连接、需要心跳、需要处理断线重连,个人站完全没有必要。客户端每分钟拉一次,加上版本号比对,成本几乎为零。

GET /api/config?since=42
{
  "ver": 45,
  "items": {"nginx.upstream.api": "...", "limit.rps": "30"},
  "deleted": ["old.key"]
}

接口必须做的事只有三条:只允许内网访问或者带 token 访问;返回内容在写入时就序列化好存进缓存表,不要每次现算;把 since 参数当作单调递增的游标,客户端拿到的版本号永远只能前进不能后退。

四、客户端怎么拉:一分钟一次的幂等循环

客户端不要写成一个复杂的守护进程。一个 shell 或 Python 循环加 systemd 定时器就足够,重点是幂等:拉到同一份配置重复执行,结果必须完全一样。

#!/bin/bash
set -euo pipefail
STATE=/var/lib/cfg/last_ver
LOCAL=/etc/myapp/config.json
since=$(cat "$STATE" 2>/dev/null || echo 0)

resp=$(curl -s -m 5 -H "X-Token: $TOKEN" \
  "http://10.0.0.5:8090/api/config?since=$since") || exit 0

# 拉取失败直接退出,沿用本地最后一份,绝不覆盖
newver=$(printf '%s' "$resp" | python3 -c 'import sys,json;print(json.load(sys.stdin)["ver"])')
printf '%s' "$resp" | python3 -c '
import sys, json
r = json.load(sys.stdin)
cur = {}
try:
    cur = json.load(open("/etc/myapp/config.json"))
except FileNotFoundError:
    pass
cur.update(r.get("items", {}))
for k in r.get("deleted", []):
    cur.pop(k, None)
json.dump(cur, open("/etc/myapp/config.json.tmp", "w"), ensure_ascii=False, indent=2)
'
mv "$LOCAL.tmp" "$LOCAL"          # 原子替换,避免读到半截文件
echo "$newver" > "$STATE"
systemctl reload myapp || true

这段脚本里有四个必须保留的细节。第一,curl 失败时直接 exit 0,不要写任何东西,保证「中心挂了站还能用」。第二,写临时文件再 mv,因为 mv 在同一文件系统内是原子的,应用不会读到写了一半的 JSON。第三,版本号只在写完之后才落地,否则会漏掉一次变更。第四,reload 失败不要紧跟着重试,让它下一分钟自然再跑一遍,重试风暴比一次没生效危险得多。

五、四个最容易踩的坑

坑一:配置中心成了单点

最典型的故障是这样写出来的:应用启动脚本第一行就是 curl 配置接口 || exit 1。于是配置中心一重启,所有机器跟着起不来。正确的形态是本地必须永远保有一份可用的完整配置,中心只负责「更新」,不负责「是否存在」。上线前一定要做一次演练:把配置服务停掉,重启应用,确认照常启动。

坑二:没有版本号,靠时间戳判断新旧

用 updated_at 做游标在跨机器时必然会翻车。容器时间可能差几秒,NTP 同步瞬间会让时间倒退,秒级精度下同一秒内的两次修改无法排序。游标必须是单调自增的整数,由数据库的自增序列产生。

坑三:改了配置没人知道为什么改

note 字段看起来是多余的一列,实际是这张表最有价值的部分。半年后你看到一个诡异的阈值,唯一能救你的就是当时写下的那句话。建议把 note 做成必填,并且强制格式是「现象 + 小改动 + 预期」,例如「接口 P99 超过 2 秒,把 rps 从 50 降到 30,观察一天」。

坑四:一次改太多键

配置应该按业务域拆开下发,而不是每次都全量覆盖。同一个批次里改动超过三个键,就应该拆成两次操作。因为一旦出问题,你无法判断是哪个键引起的。这条纪律在小团队里几乎没人遵守,出事时才后悔。

六、和 Nginx 这类需要重载的组件怎么配合

配置中心对 Nginx 这类组件天生不友好:它不能热改内存里的配置,必须重载。而频繁重载会带来 worker 进程重建,长连接会被断开,正在上传的文件会中断。

实用的做法是分级。第一级是可以用 map 表达的东西,比如上游地址列表、限流白名单,把它们放在一个由脚本生成的 include 文件里,用 Nginx 的 map 指令加载,这样改完只需要一次 reload。第二级是真正需要改 server 块的东西,这类直接走人工审批加计划窗口,不要接进自动下发。

# /etc/nginx/conf.d/upstream_map.conf  由配置中心生成
map $host $backend_api {
    default          10.0.0.21:8080;
    "admin.example.com"  10.0.0.22:8080;
}
include /etc/nginx/conf.d/upstream_map.conf;

生成脚本必须在 nginx -t 通过之后才替换文件。顺序是:写临时文件,nginx -t 校验临时文件,通过后覆盖,reload,reload 后立刻请求一次健康检查地址。任何一步失败就停在原地,保留上一份可用配置。这个「校验后才替换」的顺序是所有配置下发流程的通用铁律。

七、监控:怎么知道配置真的生效了

配置系统的失败几乎都是静默的:改完之后没报错,但就是没生效。所以必须有一个反向确认机制。最简单可靠的做法是让服务在上报状态时带上当前配置版本号。

GET /healthz
{"ok":true,"config_ver":45,"uptime":3600}

然后在中心这边做一次比对:中心的最新版本是 45,每台机器健康检查里报的版本也应该是 45。有一台报 43,说明它的拉取任务挂了或者网络不通。这个检查只需要一条命令,成本极低,却能在老板发现之前就定位到问题机器。

for h in 10.0.0.21 10.0.0.22 10.0.0.23; do
  v=$(curl -s -m 3 "http://$h/healthz" | grep -oE '"config_ver":[0-9]+')
  echo "$h $v"
done

八、什么时候该上真正的配置中心

这套方案的天花板是很清楚的。当出现下面任何一种情况时,就该考虑换 etcd、Nacos 或 Consul 了:机器数超过十台,一台一台核对版本已经不现实;需要毫秒级的变更推送,一分钟一次的轮询不能满足;多人同时改配置,需要加锁、审批、灰度发布;配置需要按环境、按机房分层继承。

反过来说,如果只是两三台机器、一个人维护、一天改不了几次,那么这套「一张表 + 一个接口 + 一分钟轮询」的方案不但够用,而且比任何成品都可靠,因为它的每一行你都看得懂,出问题你自己就能修。

九、小结

配置中心的第一价值是「唯一来源」而不是技术本身。把不值得集中的配置留在本地做兜底,把真正需要频繁调整的开关交给中心,用单调版本号做游标,用临时文件加原子替换做落地,用健康检查里的版本号做反向确认,这四件事做对了,一套几十行的脚本就能顶住几千行中间件的活儿。关键是纪律:note 必填、一次少改、校验后才替换、中心挂了站不倒。这四条比选哪个软件重要得多。

Last modification:October 30th, 2026 at 07:25 pm

Leave a Comment