夜间维护班车
PRODprod-mysql-01 · orders
这一页讲的是几个设计决定,以及每个决定背后被否掉的那个更省事的做法。
语句进入网关后依次过 4 层:菜单权限决定这个角色开不开这扇门;能力矩阵按「角色 × 能力 × 分层」给出三态结论(放行 / 转审批 / 拒绝);高危命令字典按「命令 × 分层」给出这条命令在这一层的档位;最后一层拦下不带 WHERE 的 DELETE 与 UPDATE。这 4 层不在同一个地方:第一层是挂在每条路由上的守卫,后三层在同一个判定函数里。
字典那一档最容易被读反:high 不是「禁止」,是拦下并强制走审批;mid 同样转审批;off 表示这一层不给结论,落回能力矩阵。出厂字典里只有 DROP · TRUNCATE · DELETE · ALTER · RENAME · GRANT · REVOKE,UPDATE / INSERT / CREATE 不在里面,只过能力矩阵那一层;字典是运维可编辑的,出厂那几条是默认值不是上限。
4 层规则都按分层存,内置 5 个分层:生产环境 · 法务环境 · 预发布环境 · 演练环境 · 开发环境。无 WHERE 拦截原来是一个全局开关,想让开发同学能跑 DELETE 练手,就得把生产的那道也一起卸了。ADR 0013 之后它改为按分层:同一条 DROP 在 PROD 拦下转审批、在 DEV 直接放行,是同一张表里的两行,不是两套配置。
两处刻意的短路要说在明处:会话级设置(SET search_path …、Oracle 的 ALTER SESSION SET …)只查「查询数据」这一维,跳过字典与无 WHERE;只看计划的 EXPLAIN 同样跳过后两层。而 EXPLAIN ANALYZE 与 DWS/GaussDB 的 EXPLAIN PERFORMANCE 会真的执行被包住的语句,它们按真实动词走完整判定。
角色:二线运维(L2)(固定展示)
控制台界面复刻 · 非截图
8 个维度控制台上都有,参与逐语句判定的只有 6 个 —— 连接实例、审批工单在判定层没有读者,改了它们不改变任何一条语句的裁决。
终端工单通过后,AegisDB 只做一件事:通知发起人。审批本身不发任何 SQL,审计那一行停在「已批准、尚未执行」。回来按下「执行」的不必是发起人 —— 任何够得到那台实例的人都可以,而每一道闸都按实际按按钮的那个人重算:标签授权、实例维护态、能力矩阵复判,一律按 actor 算而不是按发起人算。
「通过不代执行」只对终端工单成立,另外三类单各不相同。发布单:执行归流水线的执行阶段,插队执行会把同一次变更应用两次。执行窗口单:批准即生效,那扇门从此到点自己开。导出申请单:批准即入队,由导出 worker 跑。后两类根本没有可执行的命令 —— 它们的 Command 是一句中文描述,放它走执行路径,网关会把那句中文当 SQL 发给数据库。
副作用产品自己认下了:审批人可以执行自己批过的单 —— 这条两人控制被 ADR 0010 末段刻意放弃了。下发前会再判一次,只在结论变成「拒绝」时拦下。但复判挡得住的只是「规则变严了」,挡不住「当时的判断依据已经不成立」—— 表结构变了、数据量涨了一个量级,复判都看不出来。
AP-20260919-0042已通过SQL
ALTER TABLE orders ADD COLUMN note varchar(64)命中规则
dictDenyPROD 禁止直接执行,需转审批审批链
执行前会再判一次规则 —— 这张单可能在待办里停了几个小时。
示意按钮,不可点击 —— 通过 ≠ 代为执行(见工作原理页「第二层」)。
控制台界面复刻 · 非截图
维护车次里一晚上几十条 DDL,逐条把审批人叫醒不现实。现实里的解法通常是「今晚先把规则改松,明早改回来」—— 而这比没有审批更糟:改回来这件事没有系统记着,没人知道门还开着。
执行窗口把那个约定搬进系统:任何人可以申请,提交后生成审批单,通过后才生效。按库开,库名必填,前端刻意不预选默认库 —— 空库名会让一扇门悄悄覆盖整台实例。时间模型两种:周期班车按 IANA 时区给出每班起止,或一次性窗口用绝对起止时刻。
「到点自动关上」是准确的说法,「过期后申请自动失效」不是:没有任何清扫窗口的定时任务,过期只是「时间表不再覆盖当下时刻」这个算给界面看的判断 —— 窗口行不消失、审批不作废,判定恢复原样而已。
夜间维护班车
PRODprod-mysql-01 · orders
订单库紧急发布
PRODprod-pg-02 · billing
周末例行维护
STAGINGstaging-mysql-03 · reports
控制台界面复刻 · 非截图
回给浏览器的那个 JSON 里,身份证号已经不在了。整个网关只有两处把用户 SQL 的结果行读出来:终端结果与数据导出。脱敏就挂在这两处,新增的调用路径天然被覆盖。端到端测试直接断言原始响应体里明文一个字节都不能出现,而不是断言界面上显示成了星号。
规则按「表名 + 字段名」配置(表名留空 = 所有表),方式有留头留尾、全部遮蔽、同值同码三种,第三种用于对账:能核对是不是同一个人,看不出是谁。终端与导出算的是同一份打码计划,两边各算一遍会让导出文件和界面上看到的不一样。
每条审计的 hash = SHA256(前一条 hash + 本条内容),创世行的前驱是空串。prev_hash 上建着单列唯一索引 —— 两条记录不可能挂在同一个前驱上,链分叉由数据库层面拒绝,不靠应用侧自觉。改动中间任何一条,它之后的每一条都对不上。
校验是控制台上一个人工触发的动作,从创世行一路重算到链尾,报出第一条对不上的行号。它不拦截任何写入、不阻止服务、不自动告警 —— 它是一次显式的整表扫描,产出的是一份带结论的报告。报告自己写清了覆盖不到什么:从链尾整段截断查不出来,要查出这种得把链尾定期锚到网关之外。
在网关出现之前,一次数据库变更靠人在终端里按顺序做对若干件事:看一眼 SQL 规不规范,发起审批,批完回终端执行,再手工查一下结果。每一步都有,但顺序、是否做过、做的结果只存在于做事人的记忆里。发布流水线把这条路写下来:一条流程就是一个有序的阶段列表。(ADR 0005)
阶段类型是一个封闭集合,七种:规范审查、人工审批、备份 / 回滚点、执行变更、执行后校验(必须只读)、人工确认(发起人不能自己放行)、结果通知。封闭是因为每一种都需要一个执行器:未知类型要么被跳过,那就是一次没人跑的审查,要么让调度器崩溃。
到了执行阶段,发布单无条件先停在等待确认:代码里这道闸没有任何条件分支。不是「多一道确认更安全」—— 那句话是废话。它有四条互相独立、层层递进的理由。
一、审批回答「可不可以做」,执行确认回答「现在做」—— 两个问题,答案在两个不同的人手里。业务低峰、应用停没停、备份就不就绪,这些只有发起人和当班的人知道。
二、没有人在看着它。一条 DROP 在半夜被批准并自动落库,发起人不在现场;出了事,发现得比谁都晚 —— 而 DDL 恰恰是最需要有人盯着回滚窗口的那一类。
三、这道闸就是「按当时规则复判」的那个「当时」。发布单可能在审批里停留数小时,这期间高危字典可以改、实例可以被挪进更严的分层、发起人的角色可以变。自动落库的话,「那一刻」是一个没有人选择的时刻;有了确认闸,它就是一个有人负责的时刻 —— 确认之后代码立刻做的第一件事就是复判。
四、发布流水线是这个网关里最有吸引力的一条执行旁路,而这个仓库每一条新执行通道都栽在同一个地方。ER6 就是其中一次:异步通道曾经让在终端里被拦下的人,把同一条语句改成后台任务提交,然后它就跑了。而流水线是异步的,它以服务身份运行,而它的全部目的就是把变更打到生产上。
复判按谁算,这里必须说准,而且它与终端工单正好相反。终端工单那条路,每一道闸都按实际按按钮的人算。发布单不是:确认执行只检查确认人的角色,而且不重新检查他够不够得到这台实例;确认之后那次复判,用的是发起人的角色。两条路各自讲得通,但没有一句话能同时概括它们。
最后一句最重要:这条流水线没有自动回滚,而且它不假装有。备份阶段只执行运维显式配置的备份语句,没配就跳过并如实说明 —— 报一个绿色的「备份完成」而底下什么都没发生,是整条流水线里最危险的一行。多条语句之间没有事务,失败时前面成功的那几条不会被撤销。
判定层的失败一律朝严:读不到规则表、时区解析不出来、库名为空、定义写不通,一律不生效或当场拒绝;任何一层读不出来,结论是拒绝加最高风险,终端上打出来的那句话是「风险控制暂时不可用 · 已按最严处理」。认不出的 SQL 动词按「写入数据」算,打码列匹配不确定时宁可多打一列。这意味着某些故障会表现为「什么都跑不了」,但不会表现为「什么都跑得了」。诚实起见,两条刻意不朝严的例外也该说:判断不出发起人账号时不把它算成自审(不过度拦截),以及角色存在性查询失败时保留原有的那组角色 id —— 一次数据库抖动不该让所有人瞬间失去全部角色,而保留的是真实存在过的 id,不是把未知当成放行。