context 与 goroutine 泄漏:取消信号如何穿过调用链
2018 年我们在线上遇到过一次奇怪的故障:服务平稳运行六小时后,HTTP 接口开始出现零星超时;八小时后超时雪崩,pprof 显示 goroutine 数从启动时的 80 涨到 47 万。内存没爆,CPU 没满,唯一异常是 goroutine 数量。重启后一切恢复,但同样的故事第二天又演了一遍。
最终定位是一个 HTTP handler 里启动了后台 goroutine,handler 提前超时返回后,这些 goroutine 没人通知它们退出,全部卡在 ch <- result 的发送上。每个超时请求泄漏一个 goroutine,撑爆了运行时调度。
goroutine 泄漏和内存泄漏不一样。它不会立刻爆掉,而是悄悄吃掉内存、句柄、连接,最终在高峰期把系统打穿。这篇文章想讲清楚:泄漏的常见形态、为什么 context 是它的标准解药、context 内部怎么把取消信号自上而下广播,以及怎么定位一个已经泄漏的现场。
本周阅读清单 20190808
PHP 正则 preg_match 匹配长度限制
https://learnku.com/articles/7193/php-regular-preg-match-matching-length-limit深悉正则(pcre)最大回溯/递归限制
http://www.laruence.com/2010/06/08/1579.htmlRedis 的内存优化
https://cachecloud.github.io/2017/02/16/Redis内存优化Content-Disposition 的 filename 与 filename 区别*
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-DispositionRedis Scan 命令原理
https://segmentfault.com/a/1190000018218584Redis 字典的遍历 dictScan 算法
http://www.langdebuqing.com/redis%20notebook/redis源码解读:字典的遍历dictScan.html
channel 与 sync:Go 并发原语的实现代价
刚学 Go 的人最容易有的两个错觉:一是把 channel 当成"无锁的协程队列";二是把 sync.Mutex 当成"channel 慢就用 mutex 替换"的银弹。两者都不对。
channel 内部有一把 mutex,它能做到"快"完全靠编译器把 <-/-> 翻译成直接内存拷贝到对方 goroutine 栈帧——离开调度器配合,这个优化就废了。sync.Pool 在 Go 1.12 的实现里,每次 GC 都会清空全部缓存对象,这意味着它根本不能拿来当连接池。
这篇文章按 Go 1.12 源码把 channel 和 sync.* 主要原语的实现拆开讲:hchan 结构、三种收发路径、关闭语义、select 调度、Mutex 两种模式、WaitGroup/Once 的位操作、Pool 的两级结构与 GC 行为。读完你应该能在心里画出每条路径的关键路径成本,并在"用 channel 还是 mutex"之间做出有理由的选择。
epoll 到底解决了什么问题
2019 年的高并发服务端(Redis、Nginx、Node.js、envoy)几乎都用 epoll 作为底层 I/O 多路复用机制。但多数文章只讲 epoll_create / epoll_ctl / epoll_wait 三个 API 长什么样,没人讲清楚它到底解决了上一代的什么瓶颈。这篇文章想从一连接一线程这个最朴素的模型讲起,沿四代 I/O 模型看 epoll 解决了哪些具体的工程问题,再落到 Redis 和 Go 这两种截然不同的上层用法。
库存扣减与超卖:一个需求逼出的四种并发方案
2019 年初做秒杀系统复盘时翻过十几起线上事故,超卖几乎占了一半。表面看是"库存扣成了负数",往里追都是同一类问题:多个请求同时读到同一个库存数,各自减 1 后写回去,结果卖了 10 件只扣了 1 次。
这种 bug 在开发环境复现不出来,单线程测试一切正常;上了几千 QPS 立刻原形毕露。这篇文章想回答几个问题:为什么"先 SELECT 再 UPDATE"必然超卖?悲观锁、Redis 预扣、分段库存各自解决什么又引入什么?面对真实的库存扣减需求,怎么在四种方案里挑一个?
Redis 数据结构内幕:编码切换与 SCAN 的巧思
凌晨两点,线上 Redis 集群报警 used_memory_rss 飙到物理内存的 95%。你下意识敲下 DEBUG OBJECT mykey,看到 encoding: hashtable——本来期望的 ziplist 已经被自动升级成 dict 了。更糟的是 redis-cli --scan --pattern 'user:*' | wc -l 在主节点上跑了一分多钟,期间 P99 延迟从 2ms 跳到 800ms。
这是典型的"不知道 Redis 在背后做了什么"引发的血案。Redis 不只是一个 key-value 存储:它在每个 key 背后藏了一套对象系统、多种底层编码和渐进式 rehash 机制。理解这些,就知道为什么 KEYS 是生产禁令、为什么 SCAN 是"魔法"、为什么"小 key 聚合"比"大 key 拆分"更重要。
微服务架构演进:从单体到分布式的路径
2014 年 Martin Fowler 发表那篇著名的《Microservices》时,整个 Java 社区还在为 Spring Boot 的"约定优于配置"而兴奋。五年过去,单体(Monolith)依然是大多数团队的首选结构——不是因为它好,而是因为拆分的代价足够大,没人愿意轻易付账。
我们见过太多团队把"微服务"当成万能膏药:在还没有遇到单体痛点的时候强行拆服务,结果既享受不到单体的开发效率,又背上了分布式系统的运维负担。
这篇文章想回答一个朴素的问题:什么时候应该拆?怎么拆?拆完又该如何收尾?
slice、string、map:Go 的三个内存模型陷阱
2017 年我把一个内部项目从 Python 迁到 Go,最初的几个 PR 都因为同一个原因被打回:内存涨得太快。reviewer 不是说"哪里有内存泄漏",而是让我自己看——pprof 里堆对象 80% 都是某种 map[string][]byte,另一个服务则是被 append 撑爆。
那时候我才意识到,Go 高级数据结构的内存模型比看上去复杂得多。slice、string、map 这三个日常 API,背后各自有自己的"坑":共享底层数组、子切片写穿、string/[]byte 拷贝代价、map 遍历顺序随机、map 元素不可寻址、append 扩容的实际 cap 跟理论值不一样……任何一条没注意到,就会在生产环境变成事故。
这篇文章想回答几个问题:slice 的三字段到底是什么?append 究竟按什么规则扩容?为什么子切片写入会"穿透"原数组?string 为什么不能像 []byte 那样随便改?map 遍历为什么"故意"乱序,元素又为什么不能取地址?