欢迎访问

专注系统底层与高性能服务开发,持续记录 Go / Rust / C++ / 云原生的一线实践。

从源码细节到线上治理,尽量少空话,多代码。

TypeScript Zod 边界:解析一次,后面信任结果

背景 订单接口不仅要求 quantity 是数字,还要求它是正整数并受业务上限约束。Schema 把输入约束转成真正执行的代码。 核心判断 在 transport 层验证 unknown,再使用 z.infer 推导领域输入。 校验失败返回字段级、可理解的错误,不输出内部堆栈。 转换、默认值和未知字段处理明确记录。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 const CreateOrder = z.object({ sku: z.string().min(1), quantity: z.number().int().positive() }); const input = CreateOrder.parse(req.body); 边界与误区 示例只验证结构,不能验证 SKU 存在、库存充足或用户有权限。同步 schema 校验与数据库事务中的业务规则应分别负责。 验证方式 覆盖负数、小数、空 SKU、巨大 quantity 和未知字段,确认错误码稳定;合法输入仍要经过授权与业务约束测试。 小结 在外部边界解析并返回推断类型,领域函数只接收已经验证的数据。

2026年11月9日 · 1 分钟 · BvBeJ

PostgreSQL 连接池:池大小应服从数据库预算

背景 连接数越大不一定吞吐越高,数据库可能把时间花在调度和锁竞争。连接池是限制并发和复用连接的预算工具。 核心判断 用所有应用副本与后台任务的总连接数计算数据库预算。 观察池等待时间、使用中连接和数据库活跃会话。 设置空闲数量与生命周期,长事务必须单独治理。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 db.SetMaxOpenConns(40); db.SetMaxIdleConns(10); db.SetConnMaxLifetime(30*time.Minute); db.SetConnMaxIdleTime(5*time.Minute) 边界与误区 示例是 Go database/sql 的池配置,不是 PostgreSQL 服务端参数。副本扩容会乘上每副本上限;生命周期相同还可能产生集中重连。 验证方式 逐步增加并发,观察吞吐、等待与数据库 CPU,选择可稳定运行的区间。覆盖连接失败与滚动发布,确保管理和应急连接仍有余量。 小结 按数据库连接预算、实例数、查询并发和事务时长计算池大小。

2026年11月8日 · 1 分钟 · BvBeJ

日志脱敏策略:敏感字段不能依赖开发者记忆

背景 日志泄露凭据往往发生在错误分支:一次完整请求 dump,就能绕过正常路径的脱敏。规则应位于统一日志入口。 核心判断 敏感字段优先采用允许列表,凭据直接替换为固定占位符。 嵌套结构、URL 查询参数和异常文本也纳入检查。 需要稳定关联时使用专门设计的受控标识,不保留 token 前后缀。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 func RedactToken(_ string) string { return "[REDACTED]" } func LogRejectedRequest(requestID, token string) { slog.Info("request rejected", "request_id", requestID, "authorization", RedactToken(token), ) } 边界与误区 部分展示凭据仍然泄露信息,不是默认安全的脱敏方案。哈希个人数据也不自动意味着匿名化;保留期限与查询权限仍需控制。 验证方式 用测试 token、邮箱和嵌套请求触发成功及失败日志,搜索原文与片段;同时检查 trace 事件、告警通知和错误响应中的信息。 小结 建立字段分类和集中脱敏组件,默认拒绝未知敏感字段并限制访问和保留。

2026年11月7日 · 1 分钟 · BvBeJ

Linux CPU Throttling:容器限额可能正在制造尾延迟

背景 容器平均 CPU 不高却出现周期性延迟,可能是短时间突发耗尽了 cgroup 配额。宿主机空闲并不代表容器随时可以继续运行。 核心判断 检查 cpu.max 的配额周期和 cpu.stat 的节流增量。 将节流、线程数、CPU 使用和请求 p99 对齐。 区分配额限制、调度争用与程序自身锁竞争。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 cat /sys/fs/cgroup/cpu.stat; cat /sys/fs/cgroup/cpu.max; pidstat -u -t 1 5 边界与误区 示例路径适用于 cgroup v2,必须进入目标进程所属 cgroup。累计节流时间不是单个请求等待时间,也不能直接当作 CPU 利用率。 验证方式 用短时突发负载比较不同配额,观察延迟和依赖影响。增加限额前确认节点容量,减少无效并发可能比继续扩线程更有效。 小结 同时查看 usage、quota、throttled time 和 run queue,按延迟目标设置资源限制。

2026年11月6日 · 1 分钟 · BvBeJ

Kubernetes Stateful 升级:顺序、数据和回滚缺一不可

背景 有状态副本升级不只涉及 Pod 重建,还涉及数据格式、复制协议和存储恢复。partition 可以分阶段升级,但无法替应用保证数据兼容。 核心判断 先确认新旧版本能共同工作,并保留备份与恢复方案。 利用 ordinal 和 partition 控制升级范围,验证后逐步推进。 观察复制延迟、就绪与数据一致性,而不是仅看 Pod Running。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 updateStrategy: { type: RollingUpdate, rollingUpdate: { partition: 2 } } podManagementPolicy: OrderedReady 边界与误区 片段属于 StatefulSet spec,仍需服务名、选择器与模板。OrderedReady 不是应用层安全证明;新版本写入不可逆格式后,镜像回滚可能无法恢复服务。 验证方式 在预演环境升级最高序号副本,检查重建、复制追赶和请求结果;再模拟升级卡住,验证人工干预与数据恢复步骤。 小结 有状态升级要同时考虑数据格式、选主、持久卷、客户端兼容和可恢复回滚。

2026年11月5日 · 1 分钟 · BvBeJ

Docker Rootless:降低守护进程权限也要检查兼容性

背景 容器内使用普通用户,与 Docker daemon 以普通用户运行,是两个不同的权限边界。rootless 的收益来自减少 daemon 对宿主机的特权。 核心判断 通过 docker info 确认实际 daemon 模式,不只看镜像 USER。 检查用户命名空间、文件所有权、端口与 cgroup 支持。 保留 capability、只读根文件系统与资源限制等独立防护。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 docker info --format '{{json .SecurityOptions}}'; docker run --rm --user 1000:1000 -p 8080:8080 example/app:1.0 边界与误区 docker run –user 1000:1000 本身不会把 rootful daemon 变成 rootless。网络和存储驱动差异可能影响性能,不能直接复用既有容量结论。 验证方式 以实际部署用户运行文件挂载、端口发布和内存限额演练,检查宿主机目录权限;比较网络性能并确认日志与运维工具仍可使用。 小结 rootless 降低宿主风险,但端口、存储、网络和监控行为必须在目标环境验证。

2026年11月4日 · 1 分钟 · BvBeJ

Vue3 无障碍焦点:弹窗关闭后焦点要回到用户

背景 弹窗打开后焦点仍停在背景,键盘用户会不知道当前操作范围。可访问性需要设计焦点进入、停留和恢复的完整流程。 核心判断 打开时将焦点移到有意义的控件或标题,提供可访问名称。 模态弹窗限制背景交互,并处理 Tab 循环与 Escape。 关闭时恢复到触发控件,触发控件消失时选择合理后备目标。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 let trigger: HTMLElement | null = null; async function openDialog() { trigger = document.activeElement instanceof HTMLElement ? document.activeElement : null; open.value = true; await nextTick(); dialogRef.value?.focus(); // 容器配置 tabindex="-1" } async function closeDialog() { open.value = false; await nextTick(); if (trigger?.isConnected) trigger.focus(); } 边界与误区 给容器 focus 前需满足可聚焦条件,如 tabindex=-1。仅移动焦点不能完成 focus trap,aria-modal 也不会自动禁用背景。 验证方式 只用键盘完成打开、填写、取消和重开,再用屏幕阅读器检查名称与顺序;异步加载内容不能把用户正在使用的焦点抢走。 小结 打开时保存触发元素,关闭时恢复焦点,并为 modal 提供键盘逃生路径。

2026年11月3日 · 1 分钟 · BvBeJ

C++ 分配器选择:先测分配模式,再决定替换

背景 一个请求创建大量短命对象时,arena 可以减少逐个分配和释放的成本。前提是这些对象确实共享同一个生命周期。 核心判断 先测分配热点与对象存活时间,再选择内存资源。 monotonic_buffer_resource 适合整批释放,容器销毁后才能回收其底层区域。 计算初始缓冲与上游分配的总预算,必要时禁止溢出分配。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 std::byte buffer[4096]; std::pmr::monotonic_buffer_resource arena(buffer, sizeof buffer); std::pmr::vector<Node> nodes{&arena}; 边界与误区 初始缓冲耗尽时默认会上游分配,不等于固定容量。对含析构逻辑的对象,释放 arena 不能代替对象析构;资源必须比使用它的容器活得更久。 验证方式 统计上游分配次数,覆盖超过初始容量的请求。比较尾延迟和峰值内存,并检查请求结束后资源及对象都按顺序销毁。 小结 先测量大小分布、生命周期和线程竞争,再选择对象池、PMR 或专用 allocator。

2026年11月2日 · 1 分钟 · BvBeJ

Rust Serde 兼容性:数据格式演进要有默认路径

背景 配置文件增加字段之后,旧配置是否还能加载,取决于默认值和未知字段策略。序列化兼容是明确的产品行为,不是 derive 自动保证的结果。 核心判断 新增可选字段提供符合业务含义的默认值。 重命名字段时评估 alias 与迁移周期,避免突然拒绝旧名称。 协议输入是否允许未知字段要明确,配置和外部消息可以采用不同策略。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 fn default_retries() -> u32 { 3 } #[derive(serde::Deserialize)] struct Config { #[serde(default = "default_retries")] retries: u32, endpoint: String, } 边界与误区 serde(default) 对 u32 的默认值是零,不一定是想要的重试次数。示例用显式默认函数,endpoint 仍是必填字段;反序列化成功后继续校验业务范围。 验证方式 保留上一版本配置作为夹具,测试新字段缺失、旧名称、未知字段和非法值。序列化输出变化也要检查,不能只测试读取。 小结 新增字段提供默认值,未知字段有兼容策略,格式差异集中在转换层。

2026年11月1日 · 1 分钟 · BvBeJ

幂等键不是防重开关:重放要原样返回上一次的响应

一笔订单被创建了两次,两次都返回 201 工单是这么来的:用户说只下了一单,后台有两笔。查日志,两次请求间隔 800ms——用户双击。另一张工单更隐蔽:间隔整 2s,是客户端超时后自动重试,而服务端其实 2.1 秒才处理完,重试发出时第一单还没落库。两类都成功入库,两次都返回 201。 根因不在客户端手滑。创建类接口面对的本来就是「至少一次」的投递现实:双击、超时重试、网关重发、消息重投都会发生。服务端按「恰好一次」实现业务,重复只是时间问题。幂等键是服务端的答案:客户端为每次业务操作生成唯一键,服务端保证同键请求只产生一次副作用。 幂等设计的整体思路四月有一篇概念文;这篇只干一件事:把幂等键的完整实现拆开,包括那些概念文没展开的部分。 哪些操作需要幂等键 POST 创建:订单、支付发起、开通会员——重复执行有资金或状态后果的写操作 支付回调:渠道方按合同会重推通知,渠道的 event id 就是现成的幂等键,作用域按渠道加商户号切 GET 的边界:HTTP 语义里 GET 是 safe method,天然幂等的前提是它真的没有副作用。项目里总有「顺手写个统计、更新 last_login」的 GET,而浏览器预取、CDN、爬虫都会替你「重试」GET。有副作用的读,先改成 POST,再谈别的 表结构:唯一约束是最后防线 幂等键表本身不复杂,重要的是每列都有明确用途: CREATE TABLE idempotency_keys ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, op TEXT NOT NULL, -- 操作类型:order.create / payment.callback idem_key TEXT NOT NULL, -- 客户端提供的键 request_hash TEXT NOT NULL, -- 请求体指纹:同键不同参的检测依据 status TEXT NOT NULL DEFAULT 'processing', response_status INT, -- 重放时还原的状态码 response_body JSONB, -- 重放时还原的响应体 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ NOT NULL -- TTL 依据,必须长于最大重试窗口 ); -- 作用域 = 用户 + 操作 + 键;全局唯一键会让 A 用户的键挡住 B 用户的同名键 CREATE UNIQUE INDEX uq_idempotency ON idempotency_keys (user_id, op, idem_key); 唯一约束兜底的意义:并发、重试、代码 bug 最终都撞在这条索引上。应用层的查重逻辑可以优化掉,这条索引不能省。 ...

2026年10月31日 · 4 分钟 · BvBeJ