qinghuan
← 回到知识探索

后端现场 / 动手实验

改了配置文件,运行中的程序为什么没有变?

把配置文字、内存中的有效值和正在处理的请求分开,再做一次先校验、后整体替换的小实验。

展开本文目录
  1. 改纸上的内容,不会自动改抄好的那份
  2. 先准备完整的新卡,再换过去
  3. 正在处理的请求,可能继续用旧卡
  4. 坏配置不能把好配置改坏一半
  5. 配置送到了,也要看应用怎样接
  6. 记住这三点
  7. 别把例子用过头
  8. 自己试一小步
  9. 查原始资料
  10. 找一份资料继续练
开始前,会这些就够了
  • 认识 JSON 的键和值;能运行一个 Python 文件
遇到不熟悉的词,先看这 1 个解释
配置快照
在某个时刻把已通过检查的配置保存成一份稳定对象,使用方按约定读取这份值。外面的文字变化,不会自动改写已经拿到的对象。
打开完整术语小抄 ↗

假设你把配置里的等待时间从一千毫秒改成三百,保存文件后,程序却仍按原来的值工作。文件确实改了,但程序可能只在启动时读取过它,之后一直使用内存里保存的那份数值。

要判断哪里没有生效,先问三个问题:新文字在哪里,程序什么时候重新读取,读到后是否真正换成新的有效配置?本篇用字符串代替文件,不接框架,也不实现文件监听。

改纸上的内容,不会自动改抄好的那份

读取配置像把纸上的值抄进一张工作卡。之后修改纸,工作卡不会自己变化。程序也一样:json.loads 把当时的文字解析成 Python 对象,不会建立一条让文字变化自动同步到对象的连线。

实验中的 active 表示当前有效配置,old_request 表示一个已经开始的请求拿到的那份配置。它们都不等于外部文字本身,打印三者的变化,比只盯着“文件保存成功”更容易找出原因。

先准备完整的新卡,再换过去

不要一边读一边逐项改动正在使用的配置。这里先解析候选文字,检查字段、整数类型和范围,再生成一个新 Config;全部通过后,才让 active 指向新对象。解析或校验失败就返回原来的有效对象。

timeout_ms 与 page_size 的范围只是本练习的规则,不是推荐给所有服务的设置。frozen=True 用来阻止普通的字段重新赋值;对象里只有两个整数,程序不会把它当作能冻结任意复杂数据的魔法。

展开收起代码 · 小实验python · 52 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
import json
from dataclasses import dataclass


@dataclass(frozen=True)
class Config:
    timeout_ms: int
    page_size: int


def parse_config(text):
    data = json.loads(text)
    if type(data) is not dict or set(data) != {"timeout_ms", "page_size"}:
        raise ValueError("必须恰好提供 timeout_ms 和 page_size")
    for name, maximum in (("timeout_ms", 30000), ("page_size", 100)):
        if type(data[name]) is not int or not 1 <= data[name] <= maximum:
            raise ValueError(f"{name} 必须是 1 到 {maximum} 的整数")
    return Config(**data)


def reload_config(active, text):
    try:
        candidate = parse_config(text)
    except json.JSONDecodeError:
        print("重载拒绝: JSON 格式错误")
        return active
    except ValueError as error:
        print("重载拒绝:", error)
        return active
    return candidate


def main():
    text = '{"timeout_ms": 1000, "page_size": 20}'
    active = parse_config(text)
    old_request = active
    print("启动读取:", active)

    text = '{"timeout_ms": 300, "page_size": 50}'
    print("只改文本:", active)
    active = reload_config(active, text)
    print("重载成功:", active)
    print("旧请求仍用:", old_request)

    active = reload_config(active, '{"timeout_ms": 0, "page_size": 50}')
    print("校验失败后:", active)
    active = reload_config(active, '{')
    print("解析失败后:", active)


if __name__ == "__main__":
    main()

怎样运行:Python 3.10+ 标准库;保存为 config_snapshot.py,运行 python config_snapshot.py。

运行后对照结果
启动读取: Config(timeout_ms=1000, page_size=20)
只改文本: Config(timeout_ms=1000, page_size=20)
重载成功: Config(timeout_ms=300, page_size=50)
旧请求仍用: Config(timeout_ms=1000, page_size=20)
重载拒绝: timeout_ms 必须是 1 到 30000 的整数
校验失败后: Config(timeout_ms=300, page_size=50)
重载拒绝: JSON 格式错误
解析失败后: Config(timeout_ms=300, page_size=50)

正在处理的请求,可能继续用旧卡

输出里,只改 text 时 active 没变;显式调用重新加载后,新配置才生效。old_request 仍指向原来的 Config,所以它继续看到一千毫秒和二十条。这是本例主动采用的“每次请求拿一份配置”方式,不是所有框架都自动如此。

保留旧请求的一致视图,可以避免同一次处理前半程读到旧等待时间、后半程又读到新分页大小。但如果改的是必须立即撤销的权限或紧急开关,就不能直接照搬这项选择,需要另外定义何时重新检查。

坏配置不能把好配置改坏一半

例子先提交一个等待时间为零的候选,再提交一段不完整的 JSON。两次都拒绝,最后仍保留三百毫秒、五十条这份有效值。保留旧值不代表可以悄悄忽略错误:真实服务还应记录哪次加载失败,让维护者知道新配置并未采用。

如果程序第一次启动就没有任何有效配置,情况又不同:不能凭空声称“继续用旧值”。应按产品约定拒绝启动或使用明确的默认值;本例的启动解析失败会直接报错,没有藏起这条失败路径。

配置送到了,也要看应用怎样接

以 Kubernetes 的 ConfigMap 为例:通过环境变量提供的值不会自动更新,需要重启 Pod;普通卷挂载中的配置会在传播后更新,使用 subPath 的挂载则不会接收这类更新。它们是同一种配置的不同使用方式,不能只听到“支持更新”就一概而论。

即使文件内容已经换新,应用何时重读和采用,仍需看应用自己的实现。这里验证的只有单线程、字符串输入和整份对象替换,没有文件监听、跨线程同步、多进程广播或 Kubernetes 热加载实验。

记住这三点

  • 配置文字更新、读取完成和有效配置切换,是三个可以分开观察的步骤。
  • 先校验完整候选,再替换正在使用的配置,失败时保留已有有效值并报告错误。
  • 新请求与在途请求采用哪一份配置,应由业务明确约定。

别把例子用过头

  • frozen 数据类不是通用深度冻结,也没有替多线程系统实现同步。
  • 示例范围是人为设定,没有说明真实服务应该用多少毫秒或每页多少条。
  • 程序没有读写配置文件或监视文件变化,没有测试任何框架的自动刷新。

自己试一小步

如果新配置的等待时间合法,但分页大小不合法,怎样证明程序没有只更新前一半?

  1. 先写出本次候选的两个字段,并分别标记它们是否满足练习规则。
  2. 在重新加载前后都记录完整有效配置,再比较失败后的对象和数值。
  3. 换成一份两个字段都合法的配置,观察此前已拿到配置的请求与新请求各读到哪一份。

做到这些,就算完成

  • 比较的是完整配置,而不只是单个字段。
  • 能够区分候选被读取、校验通过与最终采用三个时刻。
  • 没有把单线程演示的结果扩大成所有框架或多进程服务的承诺。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • Python 3.10:json (新标签页)↗Python Software Foundation · 英文

    json.loads 与 JSONDecodeError:将给定文字解析成对象,以及解析失败的处理入口。

    已读解码示例、loads 和异常说明;字段齐全与数值范围检查由本文自行实现。

  • Python 3.10:dataclasses 的 Frozen instances (新标签页)↗Python Software Foundation · 英文

    frozen=True 模拟不可变实例,并通过 FrozenInstanceError 阻止普通字段重新赋值。

    已读 Frozen instances 与 FrozenInstanceError;本文没有把它解释成深度冻结或线程同步机制。

  • Kubernetes:ConfigMaps (新标签页)↗Kubernetes · 英文

    Using ConfigMaps 与 Mounted ConfigMaps are updated automatically:卷投射、环境变量与 subPath 的更新差异。

    已读使用方式、传播延迟与限制;只作真实系统边界对照,没有创建集群或执行 kubectl。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。