专注系统底层与高性能服务开发,持续记录 Go / Rust / C++ / 云原生的一线实践。
从源码细节到线上治理,尽量少空话,多代码。
专注系统底层与高性能服务开发,持续记录 Go / Rust / C++ / 云原生的一线实践。
从源码细节到线上治理,尽量少空话,多代码。
背景 协程句柄像指针一样可复制,却通常只应有一个销毁者。包装句柄的对象必须明确移动、完成和销毁的责任。 核心判断 禁止默认复制拥有型句柄,移动后源句柄清空。 规定谁负责 resume、何时等待完成、何时 destroy。 promise 的 final_suspend 行为与拥有模型保持一致。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 struct Task { std::coroutine_handle<> h{}; explicit Task(std::coroutine_handle<> handle) : h(handle) {} Task(const Task&) = delete; Task& operator=(const Task&) = delete; Task(Task&& other) noexcept : h(std::exchange(other.h, {})) {} Task& operator=(Task&&) = delete; // 前提:没有其他线程执行该协程,frame 未自销毁。 ~Task() { if (h) h.destroy(); } }; 边界与误区 示例仅展示拥有型包装,假定 frame 保留在挂起状态并由包装器销毁。协程仍在别的线程执行时 destroy 不安全;自销毁 frame 也不能再次销毁。 验证方式 测试空句柄、移动构造、异常与正常完成路径,使用 sanitizer 检查 use-after-free。多线程调度需要额外同步,不能靠句柄包装自动获得安全。 小结 明确 coroutine handle 的拥有者、destroy 时机、异常传播和参数生命周期。
背景 Future 被丢弃时会清理局部资源,但已经发生的外部副作用不会自动撤销。在 await 前后修改共享状态,需要考虑中途取消。 核心判断 识别每个 await 点之前已经提交的状态与外部动作。 尽量先在锁外完成准备,再在短临界区内校验版本并提交。 不允许中断的业务由受监督任务持有,仍要设计超时与恢复。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 // 准备在锁外执行;锁内只校验和提交,不跨远程 await。 let next = prepare(input).await?; let mut guard = lock.lock().await; if guard.revision != expected_revision { return Err(Error::Conflict); } apply(&mut guard, next); guard.revision += 1; 边界与误区 不要拿着共享锁等待远程 I/O。示例的 prepare、lock、expected_revision 与状态对象是业务定义;版本检查失败需要重新评估,而非盲目覆盖。 验证方式 在各 await 点取消请求,确认共享状态不出现半提交。涉及付款或消息发送时,测试重试与核对机制能恢复已发生的外部动作。 小结 先在锁外完成可取消的准备,锁内校验版本并一次性提交状态,外部副作用另做幂等与核对。
背景 依赖已经持续失败时继续无限调用,只会浪费线程、连接与超时预算。熔断器让故障期间的试探受控。 核心判断 定义 Closed、Open、HalfOpen 的状态和转换依据。 只统计依赖失败,避免把业务拒绝或调用方取消全部记为故障。 HalfOpen 限制探测数量,成功与失败都必须完成状态记录。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 if breaker.Allow() != nil { return ErrDependencyUnavailable }; err := call(ctx); breaker.Record(err); return err 边界与误区 示例是自定义 breaker 的流程示意,需要明确库的统计语义。熔断不代替超时、并发上限或重试预算,多个保护层叠加要避免重复放大调用。 验证方式 注入连续失败、短暂恢复和慢响应,核对状态转换与实际调用次数。依赖恢复时逐步放量,避免全部实例同时探测形成尖峰。 小结 按失败类型和时间窗口统计,半开时限制探测并发,并记录状态变化。
背景 订单接口不仅要求 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 和未知字段,确认错误码稳定;合法输入仍要经过授权与业务约束测试。 小结 在外部边界解析并返回推断类型,领域函数只接收已经验证的数据。
背景 连接数越大不一定吞吐越高,数据库可能把时间花在调度和锁竞争。连接池是限制并发和复用连接的预算工具。 核心判断 用所有应用副本与后台任务的总连接数计算数据库预算。 观察池等待时间、使用中连接和数据库活跃会话。 设置空闲数量与生命周期,长事务必须单独治理。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 db.SetMaxOpenConns(40); db.SetMaxIdleConns(10); db.SetConnMaxLifetime(30*time.Minute); db.SetConnMaxIdleTime(5*time.Minute) 边界与误区 示例是 Go database/sql 的池配置,不是 PostgreSQL 服务端参数。副本扩容会乘上每副本上限;生命周期相同还可能产生集中重连。 验证方式 逐步增加并发,观察吞吐、等待与数据库 CPU,选择可稳定运行的区间。覆盖连接失败与滚动发布,确保管理和应急连接仍有余量。 小结 按数据库连接预算、实例数、查询并发和事务时长计算池大小。
背景 日志泄露凭据往往发生在错误分支:一次完整请求 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 事件、告警通知和错误响应中的信息。 小结 建立字段分类和集中脱敏组件,默认拒绝未知敏感字段并限制访问和保留。
背景 容器平均 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,按延迟目标设置资源限制。
背景 有状态副本升级不只涉及 Pod 重建,还涉及数据格式、复制协议和存储恢复。partition 可以分阶段升级,但无法替应用保证数据兼容。 核心判断 先确认新旧版本能共同工作,并保留备份与恢复方案。 利用 ordinal 和 partition 控制升级范围,验证后逐步推进。 观察复制延迟、就绪与数据一致性,而不是仅看 Pod Running。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 updateStrategy: { type: RollingUpdate, rollingUpdate: { partition: 2 } } podManagementPolicy: OrderedReady 边界与误区 片段属于 StatefulSet spec,仍需服务名、选择器与模板。OrderedReady 不是应用层安全证明;新版本写入不可逆格式后,镜像回滚可能无法恢复服务。 验证方式 在预演环境升级最高序号副本,检查重建、复制追赶和请求结果;再模拟升级卡住,验证人工干预与数据恢复步骤。 小结 有状态升级要同时考虑数据格式、选主、持久卷、客户端兼容和可恢复回滚。
背景 容器内使用普通用户,与 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 降低宿主风险,但端口、存储、网络和监控行为必须在目标环境验证。
背景 弹窗打开后焦点仍停在背景,键盘用户会不知道当前操作范围。可访问性需要设计焦点进入、停留和恢复的完整流程。 核心判断 打开时将焦点移到有意义的控件或标题,提供可访问名称。 模态弹窗限制背景交互,并处理 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 提供键盘逃生路径。