收集整理常见的开发坑点、BUG 排查记录、性能调优案例和最佳实践指南
从零搭建:轻量服务器博客部署实录
背景 手头有一台 4核4G 的轻量云服务器(Debian 13),想搭一个纯静态的技术博客。要求很简单:低资源占用、HTTPS、自动续期证书、页脚挂备案号。 方案选型 服务器上已经装了 1Panel 面板和 Docker 版 OpenResty,所以不再额外安装 Nginx,直接复用现有的 OpenResty 容器做反向代理和静态文件服务。静态博客不需要数据库、不需要 PHP,资源占用几乎为零。 Web 服务:复用 1Panel 的 OpenResty(Docker 容器,宿主机目录挂载进容器) 证书:acme.sh 申请 Let’s Encrypt 证书,HTTP-01 验证,自动续期 站点文件:纯 HTML/CSS,放 /opt/1panel/www/sites/域名/index/ 关键配置 站点配置放在 /opt/1panel/www/conf.d/ 下,这个目录被挂载到容器内 /usr/local/openresty/nginx/conf/conf.d/,主配置会自动 include 它: server { listen 80; server_name example.com www.example.com; # 保留 ACME 验证路径,续期时要用 location ~ /.well-known/acme-challenge { allow all; root /www/sites/example.com/index; } # 其余请求全部跳转 HTTPS location / { return 301 https://example.com$request_uri; } } server { listen 443 ssl; http2 on; server_name example.com www.example.com; root /www/sites/example.com/index; ssl_certificate /usr/local/openresty/nginx/conf/ssl/example/fullchain.pem; ssl_certificate_key /usr/local/openresty/nginx/conf/ssl/example/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; } 证书自动续期 acme.sh 安装证书时通过 --reloadcmd 指定续期后的重载命令,之后完全无需人工干预: ...
一次诡异的内存泄漏排查记录
现象 一个长驻的后端服务运行三四天后内存从 200MB 涨到 1.8GB,最终被 OOM Killer 杀掉。重启后一切正常,然后周而复始。 排查过程 第一步:确认是泄漏还是缓存 先看监控曲线。如果内存是"锯齿状"(涨上去又掉下来)多半是缓存策略问题;如果是"楼梯状"只涨不跌,基本就是泄漏。我们的曲线是典型的楼梯状,确认泄漏。 第二步:抓内存快照对比 在服务运行 1 小时和 24 小时分别抓堆快照,diff 对比对象数量的增长: # 运行 1 小时 jmap -dump:live,format=b,file=heap1.hprof <pid> # 运行 24 小时 jmap -dump:live,format=b,file=heap2.hprof <pid> 用 MAT 打开两个快照对比,发现 EventListenerRegistry 里的监听器对象 24 小时增长了 4 万多个——每个都持有外部对象的引用。 第三步:定位到代码 查看注册监听器的调用点,发现问题代码: // 每次请求都会 new 一个监听器注册进去,但从不注销 public void handle(Request req) { bus.subscribe(new MetricsListener(req.getSessionId()), req.getTopic()); ... } 事件总线是全局单例,监听器以强引用形式存在,请求结束后监听器及其持有的 SessionId 等对象永远无法被回收。 修复方案 短期:把强引用订阅改成弱引用(WeakReference 包装),对象无其他引用时可被回收 长期:明确订阅生命周期,请求结束时在 finally 里注销: Listener l = new MetricsListener(req.getSessionId()); bus.subscribe(l, req.getTopic()); try { ... } finally { bus.unsubscribe(l, req.getTopic()); } 经验总结 “只注册、不注销"是内存泄漏最常见的形态之一,事件总线、定时任务、回调注册都要检查配对的注销逻辑 泄漏排查的关键是两个时间点的快照对比,而不是盯着单个快照看 上线前用压测工具模拟持续请求流,几小时内就能暴露这类问题,不用等线上跑四天
接口响应从 800ms 到 80ms 的优化之路
背景 订单列表接口在数据量上来后 P99 响应达到 800ms,用户明显感知卡顿。目标:压到 100ms 以内,不升级硬件。 第一步:慢查询定位(800ms → 300ms) 开启慢查询日志,发现列表页的统计子查询没走索引: EXPLAIN SELECT o.*, (SELECT COUNT(*) FROM order_item i WHERE i.order_id = o.id) AS item_count FROM orders o WHERE o.user_id = ? ORDER BY created_at DESC LIMIT 20; order_item.order_id 没有索引,子查询对每行订单做全表扫描。加上索引后接口降到 300ms。 第一刀永远先看慢查询日志。大部分"接口慢"的本质是"SQL 慢"。 第二步:消灭 N+1 查询(300ms → 120ms) ORM 的懒加载在序列化时触发了 N+1:列表 20 条订单,每条又单独查一次用户信息和商品快照,一次请求实际执行了 40+ 条 SQL。改成批量查询 + 内存组装: orders = orderRepo.findByUser(userId, 20); userMap = userRepo.findByIds(orders.map(o => o.userId)); itemMap = itemRepo.findByOrderIds(orders.map(o => o.id)); // 内存里组装,总共 3 条 SQL 第三步:缓存热点数据(120ms → 80ms) 用户信息、商品快照这类读多写少的数据加 Redis 缓存,TTL 5 分钟 + 更新时主动失效。注意缓存 key 里带上版本号,避免出现"删了缓存但旧值又被写回"的竞态。 优化效果 优 基 + + + 化 线 项 索 批 R 引 量 e 修 查 d 复 询 i 消 s 灭 热 N 点 + 缓 1 存 1 2 3 0 耗 0 m 8 时 8 0 s 0 变 0 m m 化 0 s s m s 经验总结 优化顺序:先定位(慢日志)→ 再减量(N+1、无用字段)→ 最后加缓存。反过来做等于掩盖问题 不要 SELECT *,只取序列化需要的字段,IO 和网络开销都会显著下降 加缓存前先想清楚失效策略,缓存不一致造成的线上事故比慢接口更难排查
工程实践中最值得遵守的 10 条规范
写在前面 以下每一条都来自真实的踩坑教训,按"违反时的代价"从高到低排列。 正文 配置与代码分离。数据库地址、密钥永远不进代码仓库,环境变量或配置中心管理。泄漏一次就足够致命。 所有外部调用必须设超时。HTTP 客户端、数据库驱动、Redis 客户端,默认不设超时的库都会在对方卡死时拖垮你。 日志要能定位问题。关键链路打 traceId;日志带上下文(参数、耗时、结果状态);ERROR 只留给需要人工介入的事。 重试要配合幂等。网络抖动重试是应该的,但下游接口必须幂等,否则一次超时重试就是两笔订单。 分支合并前必须过 CI。编译、单测、lint 三件套卡在合并前,而不是部署后。坏代码进 main 的成本是卡在分支上的十倍。 数据库变更走迁移脚本。手动改表结构的环境,迟早和代码版本不一致导致无法回滚。 命名宁可长而清楚。retryCountForPaymentGateway 好过 n。代码写一次,读很多次。 删掉的代码就删干净。注释掉的旧代码放两周就没人敢删了,Git 历史本来就是备份。 上线变更要可回滚。每次发布前问自己一句:出问题怎么退?回答不了的发布不上。 文档跟着代码走。接口文档、部署步骤过时比没有更糟。CI 里加文档检查或用代码即文档的工具。 一条隐藏规则 规范本身不重要,重要的是全组用同一套。风格不一致的代码库,维护成本比任何单个坏习惯都高。