惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
博客园_首页
博客园 - 【当耐特】
V
Visual Studio Blog
博客园 - 叶小钗
月光博客
月光博客
美团技术团队
J
Java Code Geeks
小众软件
小众软件
Y
Y Combinator Blog
博客园 - Franky
Martin Fowler
Martin Fowler
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
Microsoft Security Blog
Microsoft Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备
使用etcd分布式锁导致的协程泄露与死锁问题
zhengwenfeng · 2025-05-14 · via 郑文峰的博客

# 简介

本文记录自己在工作中排查etcd应用分布式锁而导致的泄露与死锁问题,并通过分析源码找到根因,最终解决。

# 问题重现

# 现象描述

服务出现数据入库失败,且伴随内存持续增长。关键现象:

  1. 锁残留:通过etcdctl get --prefix /my-lock/可看到锁KEY长期存在
  2. 租约续期etcdctl lease timetolive显示租约TTL不断重置
  3. 资源增长:Go程序协程数随请求量线性增长(可通过pprof观测)

# 最小复现代码

func main() {
	// ... etcd client初始化代码不变...

	// 关键问题点1:缺失session关闭
	session, _ := concurrency.NewSession(cli)
	// defer session.Close() // 故意注释导致协程泄漏

	// 关键问题点2:未释放锁
	mutex := concurrency.NewMutex(session, "/my-lock/")
	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
	_ = mutex.Lock(ctx)
	
	// ...业务逻辑代码...
	
	// 关键问题点3:阻止程序退出(仅用于demo)
	select {} 
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 根因分析

# 架构示意图

+--------------+      定期续约      +------+
|  Go routine  |----------------->| etcd |
+--------------+  (KeepAlive)     +------+
       ▲
       │ 未调用Close()
       └──────+
              |
+---------------------------+
| session.Close() 核心作用:|
| 1. 停止续约协程           |
| 2. 释放租约              |
+---------------------------+

1
2
3
4
5
6
7
8
9
10
11
12

# 源码关键路径

协程泄漏路径: NewSession() → go keepAlive协程 → client.KeepAlive() → 后台协程续约(sendKeepAliveLoop)

资源释放路径: session.Close() → Orphan() → 关闭上下文 → 触发keepAlive协程退出

# 解决方案

在创建session后,确保调用Close()方法,及时释放资源。

func main() {
	// ...初始化代码不变...

	session, err := concurrency.NewSession(cli)
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close() // 新增关键修复

	mutex := concurrency.NewMutex(session, "/my-lock/")
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel() // 确保上下文取消

	if err := mutex.Lock(ctx); err != nil {
		log.Fatal(err)
	}
	defer mutex.Unlock(context.TODO()) // 双保险释放锁

	// ...业务逻辑...
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 最佳实践

  1. 资源释放三原则:
  • 对每个NewSession()必须配对defer Close()
  • 锁操作必须包裹在Lock()/Unlock()中
  • 使用带超时的上下文(建议不超过5s)

# 总结

  1. etcd特性:会话型锁的设计需要客户端主动维护生命周期
  2. 调试技巧:使用go tool pprof观察goroutine增长趋势