欢迎访问

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

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

Docker HEALTHCHECK 的边界:它只报状态,从不负责治疗

healthy 的僵死进程,和被集体摘除的健康容器 第一起事故的样子是监控全绿、用户报障。登上机器看一眼: $ docker ps CONTAINER ID IMAGE STATUS PORTS NAMES 9f31c2a1b04e shop/api:v3 Up 4 hours (healthy) 0.0.0.0:8080->8080/tcp api 进程"活着",事件循环卡死在死锁上,accept 队列堆满,谁也连不进来。而健康检查写的是 pgrep -f app——PID 在,检查就过。healthy 证明的全部内容,是"那条命令上一次的退出码是 0",仅此而已。 第二起方向相反。数据库例行维护重启 90 秒,十分钟后告警炸了:所有 api 容器 unhealthy,负载均衡把实例摘了个干净,全站零容量。健康检查写的是 psql -h $DB_HOST -c 'select 1'——DB 停一分钟,等于给了它一张对全网的否决票。 两起事故方向相反,根因相同:把一个单值布尔当成了服务的真相。要写对它,得先把这个布尔什么时候翻转搞清楚。 四个旋钮的精确语义 HEALTHCHECK 的全部参数就四个(默认值:interval 30s、timeout 30s、start-period 0s、retries 3): HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \ CMD-SHELL 'curl -fsS http://127.0.0.1:8080/healthz || exit 1' 规则逐条说死,不留模糊空间: 探测按 interval 周期执行,退出码 0 记成功,非 0 记失败;单次执行超过 timeout 直接按失败计。 配了健康检查的容器初始状态是 starting,第一次成功后变 healthy。 连续失败 retries 次才翻成 unhealthy;中间任何一次成功,连续计数清零。 start-period 是冷启动宽限:宽限期内的失败不计入 retries;但只要期内成功过一次,宽限提前结束,之后的失败正常计数。 从 unhealthy 翻回 healthy 只需一次成功。跌落要连续失败多次,回升只要一次,这个不对称决定了恢复回流的流量冲击必须由编排层消化,HEALTHCHECK 自己不提供"连续 N 次成功才算恢复"的旋钮。 退出码 2 是保留值,别拿它表达"启动中",语义没有保证。 按这套规则推演一个时间线(interval=10s、start-period=30s、retries=3,服务冷启动需要 45 秒):t=10/20/30 的失败落在宽限期内,不计数;t=40 失败,计 1;t=50 成功,healthy。如果服务永远起不来,t=40/50/60 三连击,t≈60 后状态翻成 unhealthy。也就是说,状态滞后至少是 retries × interval 的量级,LB 的摘除与回流逻辑要按这个窗口预留时间。 ...

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

Vue3 大表格掉帧:渲染数量、响应式开销、派生计算各算各的账

从一张 8000 行的表格说起 运维后台的任务列表页,一次拉取 8000 条。运营同学的日常操作是:滚到中间、按状态筛一下、再按开始时间排个序。上线第一天的反馈: 滚轮划快了掉帧、白屏一闪 筛选框输入一个字,页面卡半秒 点排序的瞬间,整页像死机 团队的第一反应是"上虚拟列表"。但这三种症状对应三种不同的病:渲染数量、响应式开销、派生计算,药方完全不同。通用的响应式与首屏话题四月那篇《Vue3 性能优化》已经聊过,这篇只盯"大表格"一个场景往深打。 先定性:long task 里 scripting 与 rendering 各占多少 打开 Chrome DevTools 的 Performance 面板,开 4x CPU throttling,录制一段"滚动 + 输入 + 排序",看 Main 线路上超过 50ms 的 long task 的类别占比: Rendering(紫色)占比高:Recalculate Style / Layout 耗时长,说明 DOM 数量太多、选择器太贵。这是渲染数量问题,虚拟列表对症。 Scripting(黄色)占比高:JS 执行久。往 Flame chart 里再钻一层:时间花在 reactive 的 getter/setter 上是响应式开销;花在自己的 filter/sort/格式化函数上是派生计算。 两者都高:先把 scripting 的账还清再虚拟化,否则只是把同样的开销搬进更小的窗口。 砍渲染数量:手写虚拟滚动的窗口计算 虚拟列表的本质是一道算术题:visibleCount = viewportHeight / rowHeight + overscan,其余全靠占位容器骗过滚动条。 import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue' export function useVirtualRows(rowHeight: number, overscan = 6) { const containerRef = ref<HTMLElement | null>(null) const rowCount = shallowRef(0) const scrollTop = shallowRef(0) const viewportHeight = shallowRef(600) function onScroll() { scrollTop.value = containerRef.value?.scrollTop ?? 0 } let ro: ResizeObserver | undefined onMounted(() => { const el = containerRef.value! // 窗口缩放、侧栏折叠都会改视口高度;observe 时回调会先触发一次 ro = new ResizeObserver(() => (viewportHeight.value = el.clientHeight)) ro.observe(el) }) onUnmounted(() => ro?.disconnect()) // 视口能装下的行数 + 上下各一份 overscan,快速滚动时留缓冲 const visibleCount = computed( () => Math.ceil(viewportHeight.value / rowHeight) + overscan * 2, ) // 视口之上的第一行;占位容器撑总高度,translateY 推可见窗口 const start = computed(() => Math.max(0, Math.floor(scrollTop.value / rowHeight) - overscan), ) const end = computed(() => Math.min(rowCount.value, start.value + visibleCount.value)) const totalHeight = computed(() => rowCount.value * rowHeight) const offsetY = computed(() => start.value * rowHeight) return { containerRef, rowCount, onScroll, start, end, totalHeight, offsetY } } <script setup lang="ts"> const { containerRef, rowCount, onScroll, start, end, totalHeight, offsetY } = useVirtualRows(40) const windowRows = computed(() => displayed.value.slice(start.value, end.value)) watch(() => displayed.value.length, (n) => (rowCount.value = n), { immediate: true }) </script> <template> <div ref="containerRef" class="viewport" @scroll.passive="onScroll"> <div :style="{ height: totalHeight + 'px' }"> <div :style="{ transform: `translateY(${offsetY}px)` }"> <RowView v-for="row in windowRows" :key="row.id" v-memo="[row]" :row="row" /> </div> </div> </div> </template> <style scoped> /* 重排重绘不逃出滚动容器 */ .viewport { height: 640px; overflow: auto; contain: layout paint; } </style> 一个务实提醒:原生 <table> 的 <tr> 不能 transform 也不能 absolute,虚拟化很别扭。两条路:上下各插一根占位 <tr>(把 offsetY 换算成占位高度),或者放弃 table 用 div grid 自己画行。很多组件库的"虚拟表格"内部就是这么改造的。 ...

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

C++ 性能剖析流程:从用户症状到热点证据

背景 一个函数看起来复杂,不代表它是热点。优化之前需要固定请求模型,让采样结果可以解释和复现。 核心判断 记录二进制版本、构建参数、负载与 CPU 拓扑。 先看调用树和时间占比,再按假设做一次最小改动。 同步观察吞吐、尾延迟与分配,避免局部收益换来整体退化。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 perf record -F 99 -g -- ./service --scenario=steady; perf report --stdio | head -80 边界与误区 无符号或不可靠的栈展开会让火焰图失真。perf 权限、架构和事件支持不同,容器采样前要确认访问范围。 验证方式 同样场景至少重复多次,比较稳定区间;保留原始 perf.data 与符号,让他人能复查,而不是只留下截图。 小结 固定负载、编译选项和采样窗口,先用系统指标定位再用 profiler 解释热点。

2026年10月13日 · 1 分钟 · BvBeJ

Rust 零拷贝边界:少一次复制也要守住生命周期

背景 解析网络头时返回输入切片可以避免复制,但切片也会绑定输入缓冲区的生命周期。跨队列长期保存时,零拷贝可能变成资源占用。 核心判断 短生命周期解析返回借用切片,长期保存显式转 owned。 按原始协议长度检查边界,未找到分隔符不能无限积累缓冲。 同时测 CPU、分配次数和保留内存,而不是只统计 memcpy。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 fn parse_header(buf: &[u8]) -> Option<&[u8]> { let end = buf.iter().position(|&b| b == b'\n')?; Some(&buf[..end]) } 边界与误区 一个小切片若依赖大型 backing buffer,可能阻止整块缓冲释放。示例只定位换行,不执行编码校验,也不保证内容是合法完整协议头。 验证方式 覆盖无换行、空头、大包和跨读取边界,比较复制方案与借用方案的峰值内存;让生命周期约束在编译期保持可见。 小结 先建立拥有数据的正确性基线,再在测量证明有收益的局部引入切片和共享缓冲。

2026年10月12日 · 1 分钟 · BvBeJ

Go 重试预算:重试次数必须受截止时间约束

背景 依赖变慢时每层都重试三次,原本一次请求可能被放大成几十次。重试需要全链路预算,而不是固定循环次数。 核心判断 仅重试明确的暂时失败,并确认操作幂等。 退避加入随机抖动,同时受入口 deadline 限制。 只在一层负责重试,记录尝试次数与预算耗尽。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 var lastErr error for attempt := 0; attempt < 3; attempt++ { if err := ctx.Err(); err != nil { return err } lastErr = call(ctx) if lastErr == nil || !retryable(lastErr) { return lastErr } if attempt < 2 { if err := sleep(ctx, backoff(attempt)); err != nil { return err } } } return lastErr 边界与误区 示例中 call、retryable、sleep 和 backoff 是业务适配函数,sleep 必须响应取消。最后一次失败后不再等待,错误要保留最后一次调用结果。 验证方式 注入超时、永久拒绝和短暂故障,核对实际调用数;多实例同时失败时,检查抖动是否减少同步重试峰值。 小结 以请求 deadline 为总边界,对不可重试错误立即返回,并限制链路放大倍数。

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

TypeScript Monorepo 边界:依赖方向比目录更重要

背景 Monorepo 解决了代码放在哪里,却不会自动解决谁可以依赖谁。一个工具包反向依赖业务层,很快会让整个仓库无法独立演进。 核心判断 画出允许的依赖方向,公共入口仅导出稳定契约。 项目引用描述编译依赖,包 exports 与 lint 管理实际导入路径。 构建缓存键包含源码、依赖版本和配置输入。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 { "references": [{ "path": "../domain" }], "compilerOptions": { "composite": true, "declaration": true } } 边界与误区 project references 不等于访问控制,也不会自动阻止跨包深层导入。composite 项目的输出目录和包解析必须与运行方式匹配。 验证方式 新增一个故意违规的导入,确认 lint 能拦截;修改底层包,检查受影响包重建,干净环境仍能按依赖顺序完成构建。 小结 每个包只暴露稳定入口,规定依赖方向并阻止通过深层路径绕过边界。

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

PostgreSQL VACUUM:把膨胀治理当成持续工作

背景 表持续更新之后,查询变慢和空间增长不一定靠加索引解决。旧版本清理、统计信息和长事务都可能影响维护效果。 核心判断 观察死元组估计、autovacuum 时间、事务年龄与维护进度。 区分 VACUUM 清理旧版本和 ANALYZE 更新统计信息的职责。 优先缩短长事务,再按高变化表调整阈值与维护预算。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 SELECT relname, n_live_tup, n_dead_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 20; 边界与误区 n_dead_tup 是估计值,不是精确计数。普通 VACUUM 通常不把空间归还操作系统;VACUUM FULL 会重写并需要强锁,不能当作无风险清理。 验证方式 对持续更新表观察维护前后的大小、计划和延迟;检查被长事务阻挡的清理效果,并评估维护对磁盘 I/O 的影响。 小结 区分 autovacuum 未跟上、长事务阻塞和表设计放大,再按表配置阈值。

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

告警可执行性:每条告警都应有下一步动作

背景 半夜响起一条“CPU 高”告警,如果不知道用户是否受影响、应该做什么,它只会增加疲劳。告警必须对应可执行动作。 核心判断 定义影响范围、持续时间和责任团队。 优先使用错误、延迟与预算消耗等用户相关信号。 绑定 runbook,说明排查入口、临时缓解、升级条件和恢复标准。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 - alert: ApiHighErrorRate expr: error_rate > 0.02 for: 10m labels: { severity: page } annotations: { runbook: "runbooks/api-5xx" } 边界与误区 示例的 error_rate 是需要预先定义的 recording rule,不是内置指标。低流量分母与缺失数据策略要明确,阈值不能直接照搬。 验证方式 回放真实或合成事故,核对触发、恢复与通知路由。让未参与编写的人只依据 runbook 操作一次,检查能否完成有效缓解。 小结 告警必须说明症状、影响、负责人、时间窗口和排查入口,避免只堆数量。

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

Linux TCP 队列:从 listen backlog 追到应用读取

背景 监听端口存在,却仍有客户端连接超时,问题可能出在握手或应用接收速度。不同 TCP 队列反映不同阶段,不能混为一谈。 核心判断 区分握手中的请求队列与等待 accept 的连接队列。 把 ss 的监听队列与 ListenOverflows、ListenDrops 对照。 同时检查应用 accept 速度、FD 限额和调度延迟。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 ss -lntp; ss -s; nstat -az | grep -E 'Listen|Syncookies|Retrans'; cat /proc/net/netstat | grep Listen 边界与误区 提高 backlog 只是增加缓冲,不能解决长期接收能力不足。Syncookies 也不是业务容量扩展机制,队列含义要按 socket 状态解读。 验证方式 在受控环境制造连接突发,观察计数器增量和客户端连接耗时;若队列稳定增长,先查服务线程和资源限额,再逐项调参。 小结 把 SYN 队列、accept 队列、socket 状态和应用 accept 速率放在同一时间轴。

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

Kubernetes HPA 信号:CPU 高不一定代表该扩容

背景 CPU 很低但请求队列越来越长时,仅靠 CPU 扩容可能不够。HPA 指标必须与实际瓶颈和单副本处理能力建立关系。 核心判断 CPU utilization 依赖合理的 requests,先确认分母与指标可用性。 队列型任务考虑 backlog 或处理延迟等业务信号。 设定扩缩容范围、稳定窗口和依赖容量,避免副本数增长压垮数据库。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 metrics: - type: Resource resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } } behavior: { scaleDown: { stabilizationWindowSeconds: 300 } } 边界与误区 配置是 HPA spec 片段,还需 scaleTargetRef 与副本范围。扩容不会即时完成,镜像拉取和启动预热都属于响应延迟。 验证方式 用阶梯流量观察期望副本、实际就绪数、队列与 p99;再撤掉负载,确认缩容没有导致反复抖动。 小结 选择与瓶颈和 SLO 相关的指标,设置稳定窗口和最大副本避免扩缩容震荡。

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