文章 587
评论 5
浏览 230163
线程池用了LinkedBlockingQueue没设容量,把整台机器搞OOM了

线程池用了LinkedBlockingQueue没设容量,把整台机器搞OOM了

凌晨运维群炸了,一条告警:订单服务响应超时率飙到 80%,紧接着 JVM 挂掉,Pod 重启。 上去一看,OOM 前老年代被打满了。heap dump 里最大的对象是一个 LinkedBlockingQueue,里面堆了 300 多万个任务对象,光队列就占了将近 2G。 翻代码,线程池是这么配的: ExecutorService executor = new ThreadPoolExecutor( 10, // corePoolSize 20, // maximumPoolSize 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>() // ← 没设容量,默认 Integer.MAX_VALUE ); 10 个核心线程忙不过来的时候,任务进了队列。队列没有上限,上游的定时任务每 10 毫秒投递一个任务,半小时就塞了 300 万个。 线程在消费,队列在膨胀——消费速度跟不上生产速度,OOM 只是时间问题。 这就是 LinkedBlockingQueue 最坑人的地方:默认构造是无界的。 一、为什么无界队列是定时炸弹 new L....

一个导出接口把整台机器CPU打满,查了半天是JSON序列化

一个导出接口把整台机器CPU打满,查了半天是JSON序列化

有次运维群里收到报警:一台 8 核 16G 的服务 CPU 突然飙到 100%,持续 3 分钟没下来。 上去 top -H 一看,GC 线程没跑,业务线程没打满。占了 700% CPU 的是 tomcat 的 http-nio-8080-exec——就一个线程。 这就邪门了。一个请求,一个线程,吃的 CPU 顶得上 7 个核。 顺着线程 dump 看下去,热方法不是业务逻辑,不是数据库查询,是 com.fasterxml.jackson.core.json.UTF8JsonGenerator——Jackson 的序列化方法。 翻了一下接口参数:前端传了 pageSize=100000。数据库一条 SELECT * FROM order_history WHERE ... 跑出来 10 万行,MyBatis 映射完开始在 controller 里序列化成 JSON 写回 Response。 10 万条订单记录,每条约 80 个字段,序列化完 200MB 的 JSON。Jackson 在单线程里逐条序列化,边序列化边往内存里攒——CPU 不炸才怪。 一、全量序列化为什么这么耗CPU 很多....

秒杀开始3秒,数据库连接池就被打满了,100万用户在页面上干等

秒杀开始3秒,数据库连接池就被打满了,100万用户在页面上干等

去年我们搞了一次周年庆秒杀。活动页面提前一周预热,到点那一刻,后台监控直接变成一片红。 不是 QPS 被打爆——网关和应用层都扛住了。是数据库连接池,三秒之内从 0 飙到 200(最大连接数),然后所有请求开始报 Cannot get JDBC Connection。 最离谱的是,库存表一行没动。 因为所有请求刚到数据库门口,就被连接池耗尽的异常挡回去了。秒杀还没开始,数据库先倒下了。 一、连接池为什么瞬间被榨干 常规架构下,每次请求处理到数据库这一步才会获取连接。秒杀场景的流量是瞬时脉冲——0 秒之前无人问津,0 秒之后几十万并发同时冲到数据库门口。 连接池有个坑:它不会拒绝你,它只会让你排队等。 HikariCP 默认最大连接数 10(或者你手动设的 200),当并发的数据库请求数超过这个值时,获取连接的线程进入等待队列。每个请求最多等 connectionTimeout(默认 30 秒)。秒杀场景下 30 秒太长了—— 所有线程都在等连接 连接被前面进来的请求占用着(那些请求可能正在执行慢查询) 后面的请求继续涌进来抢连接 线程池里的线程全卡在获取连接上,CPU 空转 tom....

100个微服务100种日志格式:排一次故障要开10个面板,直到统一了

100个微服务100种日志格式:排一次故障要开10个面板,直到统一了

去年我们团队跑了差不多半年微服务,拆分得挺爽——订单、支付、库存、物流、用户中心,每个服务独立开发独立部署。架构图上画得漂漂亮亮。 直到有一次线上故障,把我整破防了。 一个下单请求超时,从网关一路追下去:Gateway → 订单服务 → 库存服务 → 支付服务 → 回调通知。五个服务,五个开发者写的日志,五种不同的格式。 订单服务的日志长这样: 2026-06-15 14:23:11.456 [http-nio-8080-exec-3] INFO c.o.s.OrderService - 订单创建成功,订单号: ORD20260615001 库存服务的日志长这样: [INFO] 2026/06/15 14:23:12.789 - StockServiceImpl: 扣减库存成功 [skuId=SKU8823, qty=1] 支付服务的日志干脆连时间戳格式都不一样: {"level":"info","msg":"支付回调处理完成","tradeNo":"TRD20260615001","time":"2026-06-15T14:23:13.456+08:00"} 我在 Kibana....

服务一直返回200,K8s却说它活着,直到业务全挂了

服务一直返回200,K8s却说它活着,直到业务全挂了

去年双十一前一天,凌晨3点,被叫起来复盘一次诡异的事故。 事故的表现很魔幻:K8s 集群里所有 Pod 状态都是 Running,健康检查全部通过,Grafana 大盘一片绿。但用户反馈说下单按钮点不动,支付页面转圈圈,客诉量每分钟十几条。 运维同学第一个反应是"网络问题"。查了半小时,网络一切正常。第二个反应是"数据库挂了",DBA 看了一眼连接池——没问题。 最后顺着日志一层层摸,发现 Redis Cluster 里有一个分片的 Master 选举失败,变成了只读模式。订单服务在查 Redis 缓存的时候拿不到写入权限,但它的 /health 接口还在正常返回 200。 因为健康检查只做了 ping——一个什么都没验证的空壳。 一、返回 200 不等于活着 大多数 Spring Boot 项目配健康检查都是这样: GET /actuator/health → {"status":"UP"} 加个 Spring Security 的登录校验就算完事了。看起来挺稳的,直到某天业务挂了,你才发现这玩意儿根本没用。 为什么?因为 Spring Boot Actuator 默认的健康检查....

慢SQL日志一天吞65G磁盘?加了动态采样率降到1.2G

慢SQL日志一天吞65G磁盘?加了动态采样率降到1.2G

凌晨两点,报警群里炸了。 "订单服务三台机器磁盘使用率全部超过 95%,其中一台已经 99%。下单接口开始超时,用户付不了款了。" 我迷迷糊糊掏出手机看了一眼,心头一紧。这是线上大促前夕,每秒几千单的交易量,磁盘满了意味着日志写不进去、服务随时可能挂掉。 赶紧登上服务器一看,/data/logs 目录占了整整 230G。顺着 du 一层层摸下去: /data/logs/slow-sql/ ├── 2026-06-20.log 68G ├── 2026-06-19.log 61G ├── 2026-06-18.log 55G └── ... 好家伙,慢 SQL 日志每天能写六七十 G。 我们慢 SQL 阈值设的 500ms,全量记录每一条慢查询的完整 SQL、执行时间、调用堆栈。业务高峰期每秒几千个查询,大促期间并发一上来,哪怕只有 5% 的 SQL 超过 500ms,每天就是几百万条记录。 那一刻我突然意识到一个问题:我们到底需要记录这么多慢 SQL 吗? 一、全量记录的代价,不光是磁盘 回过头来看,全量慢 SQL 日志的代价远比磁盘空间要大: 磁盘 IO 争抢。 每一条慢 SQL....

日志爆炸防护机制:异常打印刷爆磁盘?动态限频+异步落盘救急

日志爆炸防护机制:异常打印刷爆磁盘?动态限频+异步落盘救急

公司有一次线上故障——某个下游服务挂了,调用方在 catch 块里打了 log.error("调用失败", e)。这行代码每分钟被执行了 5 万次,5 万条堆栈日志,每条 3KB,一分钟就写了 150MB 的日志文件。运维发现的时候磁盘已经满了,其他服务也跟着挂。灾难的起点不是下游挂了,而是日志把磁盘写爆了。 这种日志爆炸场景的典型特征是突发性——平时打日志没问题,一旦某个循环里遇到了异常,日志量瞬间暴涨。今天聊聊怎么给日志加上限频和异步落盘,让它在异常场景下也不会失控。 问题出在哪:日志写入是同步阻塞的 Logback 默认的 FileAppender 是同步写盘的。log.error() 调用时,线程会等日志写完了才继续执行。平时没感觉,但一旦日志量暴增,磁盘 IO 就成了瓶颈——所有打日志的线程都堵在等磁盘写入。 而且同步模式下,每行日志都是一次 write() 系统调用。一分钟 5 万条就是 5 万次系统调用,再加上堆栈的格式化,CPU 也被打满。 方案一:异步落盘,业务线程不等 IO Logback 的 AsyncAppender 就是干这个的: <appende....

对象存储生命周期自动化:闲置文件自动转冷存储,云存储成本直降 50%

对象存储生命周期自动化:闲置文件自动转冷存储,云存储成本直降 50%

公司用阿里云 OSS 存用户上传的文件,标准存储每月 0.12 元/GB。半年后发现月账单从几百涨到了两万——文件总量从 1TB 涨到了 20TB,其中 80% 是三个月前上传的、再也没人访问过的文件和日志备份。这些文件一直按标准存储计费,实际上低频存储的价格只要标准存储的三分之一。 云存储的价格差异很大——标准存储最贵,低频存储便宜一半,归档存储几乎不要钱(但取回要等几小时)。问题是很多公司的文件从上传第一天就放在标准存储里,从来没动过,也从来没降级。 今天聊聊怎么用生命周期策略,让不同类型的文件自动流转到合适的存储层级,存储账单直接砍半。 云存储的三种温度 各大云厂商的存储分层都差不多,本质上是"访问频率越高价格越贵、访问频率越低价格越便宜但取回有代价"的 trade-off: 存储类型单价(约)数据取回最短存储适合场景 标准存储0.12 元/GB/月免费无限制频繁访问的热数据 低频存储0.08 元/GB/月按量付费30 天一个月访问几次的温数据 归档存储0.03 元/GB/月解冻 1~5 分钟60 天几乎不访问的冷数据 深度归档0.015 元/GB/月解冻 12 小时....

大文件下载内存溢出防护:拒绝全量加载,零拷贝流式输出抗住万级并发

大文件下载内存溢出防护:拒绝全量加载,零拷贝流式输出抗住万级并发

朋友公司的文件服务,一到月底报表下载高峰期就崩。运维排查发现每次下载一个 200MB 的 Excel,后端代码里居然是 byte[] data = file.readAllBytes()——把整个文件加载到堆内存里再往外写。10 个人同时下载,就是 10 个 200MB 的数组在堆里,JVM 直接 OOM。重启后又崩,加内存也撑不住——因为下载峰值不是你能通过加内存解决的线性问题。 大文件下载的 OOM 问题本质就一句话:你把整个文件搬进了 JVM 堆里,但 JVM 堆不是为文件 IO 设计的。 文件应该从磁盘流到网卡,中间经过你的应用,但不要在你的堆里停留。 今天聊聊怎么用流式传输和零拷贝,让 X GB 的文件下载跟 X KB 的一样轻松。 错误姿势:全量加载 最常见的错误写法: @GetMapping("/download") public ResponseEntity<byte[]> download(String filePath) { byte[] data = Files.readAllBytes(Path.of(filePath)); // ❌ 全量加载到....

分片上传并发冲突解决:多人同时传同名文件?分布式锁保障数据完整!

分片上传并发冲突解决:多人同时传同名文件?分布式锁保障数据完整!

公司的网盘系统出了个诡异的 bug。用户 A 上传了一个 500MB 的视频文件,分成了 100 个分片。上传到第 80 片的时候,用户 B 也上传了一个同名的视频文件——可能是一个修订版。两个上传请求同时往同一个文件名下写分片,最后合并出来的文件里混了一半 A 的内容和一半 B 的内容。文件打不开,用户投诉。 分片上传的并发冲突比单文件上传难处理得多。单文件上传一次 HTTP 请求就完成了,有冲突也容易看出来。分片上传是几十上百个 HTTP 请求,冲突可能发生在任何时候——第一个分片被 B 覆盖,最后一个分片还是 A 的。结果就是一个"拼接怪物"。 今天聊聊怎么用分布式锁把分片上传的整个过程保护起来,确保同一时间只有一个上传任务在操作同一个文件。 冲突是怎么发生的 分片上传的流程通常是这样: 1. 前端发起分片上传 → 后端返回 uploadId 2. 前端逐个上传分片 → 每片带 uploadId + 分片号 3. 全部分片上传完 → 发起合并请求 两个人的冲突: 时间线: 用户 A 用户 B T1 创建 uploadId=A1 T2 上传分片 0~50 T3 创建 uploa....

权限缓存一致性难题:管理员改角色用户未生效?事件总线广播 + 本地缓存失效

权限缓存一致性难题:管理员改角色用户未生效?事件总线广播 + 本地缓存失效

一、问题背景:权限缓存的"脏数据"困境 你是否遇到过这样的场景: 管理员在后台修改了某个用户的角色权限 用户重新登录后发现权限没有变化 只有重启服务或等待缓存过期,权限才会生效 这就是典型的权限缓存一致性问题。为了提高系统性能,我们通常会将用户权限信息缓存到本地,但当管理员修改权限后,其他节点的缓存并不会自动更新,导致用户获取到过期的权限数据。 真实案例:某电商平台在大促期间,管理员紧急调整了部分运营人员的权限,但由于缓存未及时刷新,导致权限变更延迟生效,影响了订单处理效率。 二、核心概念:缓存一致性模型 2.1 缓存更新策略对比 策略描述优点缺点适用场景 Cache-Aside先更新数据库,再删除缓存简单存在竞态条件读多写少 Write-Through同时更新缓存和数据库一致性好写入性能低一致性要求高 Write-Behind先写缓存,异步写数据库写入性能高数据可能丢失允许最终一致性 Event-Based事件驱动更新缓存解耦性好实现复杂分布式系统 2.2 事件总线架构 ┌───────────────────────────────────────────────....

JWT 接口 CSRF 防护:无状态架构如何防跨站请求伪造?Double Submit Cookie 方案

JWT 接口 CSRF 防护:无状态架构如何防跨站请求伪造?Double Submit Cookie 方案

一、问题背景:JWT 的 CSRF 隐患 你是否认为使用 JWT 就天然安全?实际上,JWT 在某些场景下依然存在 CSRF 风险! 当 JWT 存储在 Cookie 中(尤其是 HttpOnly=false 的情况),攻击者可以通过以下方式发起 CSRF 攻击: 用户登录你的网站,服务器返回 JWT 并存放在 Cookie 中 攻击者诱导用户访问恶意网站 恶意网站发起对目标网站的请求(如转账、修改密码) 浏览器自动携带 Cookie 中的 JWT,请求成功执行 真实案例:某电商平台的支付接口使用 JWT 认证,但未做 CSRF 防护。攻击者通过构造恶意页面,诱导用户点击后成功发起支付请求,造成用户资金损失。 二、核心概念:CSRF 与 JWT 的博弈 2.1 CSRF 攻击原理 ┌──────────────────────────────────────────────────────────────────┐ │ CSRF 攻击流程 │ ├──────────────────────────────────────────────────────────────────┤ ....

任务执行日志自动归档:历史数据膨胀拖慢查询?定时清理 + 冷热数据分离

任务执行日志自动归档:历史数据膨胀拖慢查询?定时清理 + 冷热数据分离

一、问题背景:日志数据的"熵增困境" 你是否遇到过这样的场景:系统运行一段时间后,任务执行日志表的数据量达到数十亿条,导致: 查询变慢:简单的日志查询需要数秒甚至数分钟 存储成本飙升:SSD 存储费用持续增长 备份困难:全量备份耗时过长 DDL 操作阻塞:添加索引或修改表结构需要长时间锁表 这就是典型的历史数据膨胀问题。任务执行日志通常具有"写多读少"的特点,超过90%的日志数据写入后很少被访问,但却占用着宝贵的存储资源和查询性能。 二、核心概念:冷热数据分离原理 2.1 数据生命周期模型 ┌──────────────────────────────────────────────────────────────────┐ │ 数据生命周期 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 热数据 ──► 温数据 ──► 冷数据 ──► 归档数据 ──► 删除 │ │ (7天) (30天) (90天) (365天) │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ ....

Async 线程耗尽排查:Future.get() 阻塞导致雪崩?队列限制 + 降级回调机制

Async 线程耗尽排查:Future.get() 阻塞导致雪崩?队列限制 + 降级回调机制

一、问题背景:线程池耗尽的"死亡螺旋" 你是否遇到过这样的场景:系统使用 @Async 异步执行任务,在流量高峰期突然出现大量请求超时,最终导致整个服务不可用? 这很可能是 Future.get() 阻塞导致的线程池雪崩。当异步任务的处理速度跟不上请求速度时,任务会在队列中堆积,而调用线程在 Future.get() 处阻塞等待,最终导致调用线程也被耗尽。 // 危险的异步调用方式 @Async public CompletableFuture<String> doAsyncTask() { // 执行耗时操作... } // 调用方 public void process() { CompletableFuture<String> future = asyncService.doAsyncTask(); String result = future.get(); // 阻塞等待,可能导致线程耗尽 } 真实案例:某支付系统在双十一期间,异步对账任务因数据库慢查询导致处理延迟,调用线程在 Future.get() 处阻塞,最终导致 Tomcat 线程池被占满,新....

定时任务线程池隔离:单个任务卡顿拖垮全局调度?独立线程池 + 超时中断方案

定时任务线程池隔离:单个任务卡顿拖垮全局调度?独立线程池 + 超时中断方案

一、问题背景:定时任务的"多米诺骨牌效应" 你是否遇到过这样的场景:线上运行着多个定时任务,其中一个任务因为外部服务超时、数据库慢查询或死循环而卡住,导致整个调度器线程池被占满,其他所有定时任务都无法按时执行? 这就是典型的线程池饥饿问题。Spring 的 @Scheduled 默认使用单线程调度器,所有任务共用一个线程。一旦某个任务阻塞,后续任务都会排队等待,形成"多米诺骨牌效应"。 // 默认的单线程调度器 - 危险! @Configuration @EnableScheduling public class SchedulerConfig { // 所有 @Scheduled 任务共用这个单线程 } 真实案例:某电商平台在大促期间,库存同步任务因数据库慢查询卡住,导致订单统计、报表生成等关键任务全部延迟,最终影响了实时数据展示。 二、核心概念:线程池隔离原理 线程池隔离的核心思想是:将不同类型、不同重要程度的定时任务分配到独立的线程池中执行,避免相互影响。 隔离策略对比 策略描述适用场景 按业务类型隔离不同业务域使用独立线程池订单、库存、用户等不同业务模块 按重要性隔....

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程