开始前,会这些就够了
- 认识 JSON 的键和值;能运行一个 Python 文件
假设你把配置里的等待时间从一千毫秒改成三百,保存文件后,程序却仍按原来的值工作。文件确实改了,但程序可能只在启动时读取过它,之后一直使用内存里保存的那份数值。
要判断哪里没有生效,先问三个问题:新文字在哪里,程序什么时候重新读取,读到后是否真正换成新的有效配置?本篇用字符串代替文件,不接框架,也不实现文件监听。
改纸上的内容,不会自动改抄好的那份
读取配置像把纸上的值抄进一张工作卡。之后修改纸,工作卡不会自己变化。程序也一样:json.loads 把当时的文字解析成 Python 对象,不会建立一条让文字变化自动同步到对象的连线。
实验中的 active 表示当前有效配置,old_request 表示一个已经开始的请求拿到的那份配置。它们都不等于外部文字本身,打印三者的变化,比只盯着“文件保存成功”更容易找出原因。
先准备完整的新卡,再换过去
不要一边读一边逐项改动正在使用的配置。这里先解析候选文字,检查字段、整数类型和范围,再生成一个新 Config;全部通过后,才让 active 指向新对象。解析或校验失败就返回原来的有效对象。
timeout_ms 与 page_size 的范围只是本练习的规则,不是推荐给所有服务的设置。frozen=True 用来阻止普通的字段重新赋值;对象里只有两个整数,程序不会把它当作能冻结任意复杂数据的魔法。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
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 数据类不是通用深度冻结,也没有替多线程系统实现同步。
- 示例范围是人为设定,没有说明真实服务应该用多少毫秒或每页多少条。
- 程序没有读写配置文件或监视文件变化,没有测试任何框架的自动刷新。
自己试一小步
如果新配置的等待时间合法,但分页大小不合法,怎样证明程序没有只更新前一半?
- 先写出本次候选的两个字段,并分别标记它们是否满足练习规则。
- 在重新加载前后都记录完整有效配置,再比较失败后的对象和数值。
- 换成一份两个字段都合法的配置,观察此前已拿到配置的请求与新请求各读到哪一份。
做到这些,就算完成
- 比较的是完整配置,而不只是单个字段。
- 能够区分候选被读取、校验通过与最终采用三个时刻。
- 没有把单线程演示的结果扩大成所有框架或多进程服务的承诺。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Python 3.10:json (新标签页)↗
json.loads与JSONDecodeError:将给定文字解析成对象,以及解析失败的处理入口。 - Python 3.10:dataclasses 的 Frozen instances (新标签页)↗
frozen=True 模拟不可变实例,并通过
FrozenInstanceError阻止普通字段重新赋值。 - Kubernetes:ConfigMaps (新标签页)↗
Using
ConfigMaps与 MountedConfigMapsare updated automatically:卷投射、环境变量与subPath的更新差异。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
Nacos 中文:配置改了,程序怎样知道 (新标签页)↗
对比一次读取与持续监听,写清新配置校验成功后才替换业务对象。
查看学习准备与访问说明 →