curl有时获取不到数据 什么原因导致_缓存击穿导致 golang 组件死锁的问题分享...
大纲
思路排查
Dump 堆栈很重要
关键思路
终于找到你
思路整理
发现蛛丝马迹
完整的推理流程
思考总结
分享一个线上遇到的死锁问题,什么, golang 也会有死锁?
思路排查
Dump 堆栈很重要
线上某个环境发现 S3 上传请求卡住,请求不返回,卡了30分钟,长时间没有发现有效日志。一般来讲,死锁问题还是好排查的,因为现场一般都在。类似于 c 程序,遇到死锁问题都会用 pstack 看一把。golang 死锁排查思路也类似(golang 不适合使用 pstack,因为 golang 调度的是协程,pstack 只能看到线程栈),我们其实是需要知道 S3 程序里 goroutine 的栈状态。golang 遇到这个问题我们有两个办法:
- 方法一:条件允许的话,gcore 出一个堆栈,这个是最有效的方法,因为是把整个 golang 程序的内存镜像 dump 出来,然后用 dlv 分析;
- 方法二:如果你提前开启 net/pprof 库的引用,开启了 debug 接口,那么就可以调用 curl 接口,通过 http 接口获取进程的状态信息;
需要注意到,golang 程序和 c 程序还是有点区别,goroutine 非常多,成百上千个 goroutine 是常态,甚至上万个也不稀奇。所以我们一般无法在终端上直接看完所有的栈,一般都是把所有的 goroutine 栈 dump 到文件,然用 vi 打开慢慢分析。
- 调试这个 core 文件,意图从堆栈里找到些东西,由于堆栈太多了,所以就使用
gorouties -t -u
这个命令,并且把输出 dump 到文件; curl xxx/debug/pprof/goroutine
关键思路
成千上万个 goroutine ,直接显示到终端是不合适的,我们 dump 到文件 test.txt,然后分析 test.txt 这个文件。去查找发现了一些可疑堆栈,那么什么是可疑堆栈?重点关注加锁等待的堆栈,关键字是 runtime_notifyListWait
、semaphore
、sync.(*Cond).Wait
、Acquire
这些阻塞场景才会用到的,如果业务堆栈上出现这个加锁调用,就非常可疑。
划重点:
- 留意阻塞关键字
runtime_notifyListWait
、semaphore
、sync.(*Cond).Wait
、Acquire
; - 业务堆栈(非 runtime 的一些内部堆栈)
统计分析发现,有 11 个这个堆栈都在这同一个地方,都是在等同一把锁 blockingKeyCountLimit.lock
,所以基本确认了阻塞的位置,就是这个地方阻塞到了所有的请求,但是这把锁我们使用 defer 释放的,使用姿势如下:
// do sometinglock.Acquire(key)defer lock.Release(key)// 以下为锁内操作;
blockingKeyCountLimit 是我们封装针对 key 操作流控的组件。举个例子,如果 limit == 1,key为 "test" 在 g1 上 Acquire 成功,g2 acquire("test") 就会等待,这个可以算是我们优化的一个逻辑。如果 limit == 2,那么就允许两个人加锁到,后面的人都等待。
从代码来看,函数退出一定会释放的,但是偏偏现在锁就卡在这个地方,所以就非常奇怪。我们先找哪个 goroutine 占着这把锁不释放,看看能不能搞清楚怎样导致这里抢不到锁的原因。
通过审查业务代码分析,发现可能的源头函数(这个函数是向后端请求的函数):
api.(*Client).getBytesNolc
确认是 getBytesNolc
这个函数执行的操作,那么大概率就是卡在这个地方了。用这个 getBytesNolc
字符串搜索堆栈,找下是哪个堆栈 ?搜索到这个堆栈 goroutine 19458
大概率就是第 1 个堆栈了,也就是其他的 11 个 goroutine 都在等这 goroutine 19458
来放锁,仔细看这个堆栈。那么为啥这个堆栈不放锁呢?这里有个细节要注意下,这里是卡到 gihub.com/golang/groupcache/singleflight/singleflight.go:48
这一行:
终于找到你
这是一个开源库,singleflight 实现了缓存防击穿的功能。
简单减少下 singleflight 的功能,这是一个非常有效的工具。在缓存大量失效的场景,如果针对同一个 key ,其实只需要有一个人穿透到后端请求数据,其他人等待他完成,然后取缓存结果即可。这个就是 singleflight 实现的功能。具体实现就是:来了请求之后,把 key 插入到 map 里,后面的请求如果发现同名 key 在 map 里面,那么就等待它完成就好;
截屏显示卡到 c.wg.Wait()
这一行,那么说明 map 里面肯定有已经存在的 key,说明 goroutine 19458
不是第一个人?但是外面还有一个 blockingKeyCountLimit
的互斥呢,按道理其他的人也进不来(因为 limit == 1),这里这么讲来肯定要是源头才对?
思路整理
伪代码显示如下:
func xxx () { // 大部分协程都卡在这里(11个) // 这个锁的效果主要是流控,limit 值初始化赋值,可以是 1,也可以是其他; // locker 为 blockingKeyCountLimit 类型 limitLocker.Acquire( key ) defer limitLocker.Release( key ) // 获取数据 getBytesNolc( key , ...)}func getBytesNolc () { // ... // 下面就是 singleflight.Group 的用法,防穿透 // 同一时间只允许一个人去后端更新 ret, err = x.Group.Do(id, func() (interface{}, error) { // 去服务后台获取,更新数据; }) // ...}
图示显示当前的现状:
现状小结:
- 大量的协程都在等
blockingKeyCountLimit
这把锁释放; - 协程
goroutine 19458
持有blockingKeyCountLimit
这把锁; - 协程
goroutine 19458
却在等一个相同 key 名字的任务的完成( singleflight 一个防击穿的库,同一时间相同 key 只允许放到一个后端去执行),却永远没等到,协程因此呈现死锁;
当前的疑问就是第一个 key 的任务为啥永远完不成,堆栈也找不到了,去哪里了?
发现蛛丝马迹
我们再仔细审一下 singleflight 的代码:
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() if g.m == nil { g.m = make(map[string]*call) } // 如果找到同名 key 已经存在; if c, ok := g.m[key]; ok { g.mu.Unlock() // 等待者走到这个分支:等待第一个人执行完成,最后直接返回它的结果就行了; c.wg.Wait() return c.val, c.err } // 如果同名 key 不存在(第一个人走到这个分支) c := new(call) c.wg.Add(1) // map 里放置 key g.m[key] = c g.mu.Unlock() // 执行任务 c.val, c.err = fn() // 唤醒所有的等待者 c.wg.Done() g.mu.Lock() // 删除 map 里的 key delete(g.m, key) g.mu.Unlock() return c.val, c.err}
发现有个线索,我们的 S3 服务程序一个 http 请求对应一个协程处理,为了提高服务端进程的可用性,在框架里会捕捉 panic,这样确保单个协程处理不会影响到其他的请求。基于这个前提,我们假设:如果 fn()
执行异常,panic 掉了,那么就不会走 delete(g.m, key)
的代码,那么 key 就永远都残留在 map 里面,而进程却又还活着。恍然大悟。
完整的推理流程
- 第一个协程 g1 来了,加了
blockingKeyCountLimit
锁,然后准备穿透到后端,调用函数getBytesNolc
获取数据,并走进了singlelight
,添加了一个 key:x, 准备干活;- 干活发生了一些不可预期的异常(后面发现是配置的异常),nil 指针引用之类的, panic 堆栈了,panic 导致后面 delete key 操作没有执行;
- 虽然 g1 现在 panic 了,但是由于在函数 func xxx 里面
blockingKeyCountLimit
是 defer 执行的,所以这把锁还是,但是singlelight
的 key 还存在,于是残留在 map 里面; - 但是由于我们服务程序为了高可用是 recover 了 panic 的,单个请求的失败不会导致整个进程挂掉,所以进程还是好好的;
- 第二个
goroutine 19458
协程来了,blockingKeyCountLimit
加锁,然后走到singlelight
的时候,发现有 key: x 了,于是就等待;- 并且等待的是一个永远得不到的锁,因为 g1 早就没了;
- 后续的 11 个 协程来了,于是被
blockingKeyCountLimit
阻塞住,并且永远不能释放;
实锤:后续基于这个猜想,再去搜索一遍日志,发现确实是有一条 panic 相关的日志。这个时间点后面的请求全部被卡住。
思考总结
一般来讲 c 语言写程序容易出现死锁问题,因为各种异常逻辑可能会导致忘记放锁,从而导致抢一个永远都不可能得到的锁。golang 为了解决这个问题,一般是用 defer 机制来实现,使用姿势如下:
func test () { mtx.Lock() defer mtx.Unlock() /* 临界区 */}
golang 的 defer 机制是一个经过经验沉淀下来的有效功能。我们必须要合理使用。defer 实现原理是和所在函数绑定,保证函数 return 的时候一定能调用到( panic 退出也能),所以 golang 加锁放锁的有效实践是写在相邻的两行。
其实思考下,singleflight 作为一个通用开源库,其实可以把 delete map key 放到 defer 里,这样就能保证 map 里面的 key 一定是可以被清理的。
还有一点,其实 golang 是不提倡异常-捕捉这样的方式编程,panic 一般不让随便用,如果真是严重的问题,挂掉就挂掉,这个估计还好一些。当然这是要看场景的,还是有一些特殊场景的,毕竟 golang 都已经提供了 panic-recover 这样的一个手段,就说明还是有需求。这个就跟 unsafe 库一样,你只有明确知道自己的行为影响,才去使用这个工具,否则别用。
坚持思考,方向比努力更重要。关注我:奇伢云存储
来都来了,点个“在看”再走叭~~
curl有时获取不到数据 什么原因导致_缓存击穿导致 golang 组件死锁的问题分享...相关推荐
- python获取实时基金数据由银河证券提供_我用Python写了个金融数据爬虫,半小时干了组里实习生一周的工作量...
原标题:我用Python写了个金融数据爬虫,半小时干了组里实习生一周的工作量 最近,越来越多的研究员.基金经理甚至财务会计领域的朋友,向小编咨询:金融人需要学Python么? 事实上在2019年,这已 ...
- python获取实时基金数据由银河证券提供_聚合数据Python的基金财务数据接口调用代码实例...
#!/usr/bin/python # -*- coding: utf-8 -*- import json, urllib from urllib import urlencode #-------- ...
- PHP CURL模拟POST提交XML数据
PHP CURL模拟POST提交XML数据 来源: 时间:2013-09-05 19:26:56 阅读数:974 分享到:0 [导读] 本文章来给大家介绍一个利用PHP CURL模拟POST提 ...
- php curl加密获取数据,PHP利用Curl模拟登录并获取数据例子
几乎所有的程序员来模仿用户登录或抓取数据都会使用到curl函数,下面我来给各位介绍利用curl函数实现登录并抓取数据,希望下面例子对各位有帮助. PHP的curl()在抓取网页的效率方面是比较高的,而 ...
- php 接收curl json数据格式,curl发送 JSON格式POST数据的接收,以及在yii2框架中的实现原理【精细剖析】...
1.通过curl发送json格式的数据,譬如代码: function http_post_json($url, $jsonStr) { $ch = curl_init(); curl_setopt($ ...
- 【java 性能优化实战】3 工具实践:如何获取代码性能数据?
首先解答一下上一课时的问题.磁盘的速度这么慢,为什么 Kafka 操作磁盘,吞吐量还能那么高? 这是因为,磁盘之所以慢,主要就是慢在寻道的操作上面.Kafka 官方测试表明,这个寻道时间长达 10ms ...
- 怎么获取codeforces的数据_手把手教你学会新媒体运营——如何通过数据分析来优化新媒体运营...
一般来说,我们希望运营的新媒体的每篇内容都可以获得高打开率和高分享率.但事实并不总是如愿,因此,在新媒体运营过程当中,我们需要不断地优化自身内容,以求可以获得更多的关注和流量.那么我们今天来讨论下如何 ...
- 使用腾讯开发平台获取QQ用户数据资料
<今天是七夕:祝大家七夕嗨皮,前可么么哒,后可啪啪啪> Tips:本篇博客将教你如何使用腾讯开发平台获取QQ用户资料 ----------------------------------- ...
- 怎么调用获取被创建的预制体_Go 语言 Web 编程系列—— 获取用户请求数据(上)...
0.GET/POST 请求数据 在 PHP 中,可以直接通过全局变量 $_GET 和 $_POST 快速获取 GET/POST 请求数据,GET 请求数据主要是 URL 查询字符串中包含的参数,以前面 ...
- sentinel 端口_Sentinel原理:控制台是如何获取到实时数据的
Sentinel 系列教程,现已上传到 github 和 gitee 中: GitHub: https://github.com/all4you/sentinel-tutorial Gitee: ht ...
最新文章
- 人类认知对设计规律有这些影响
- 【斩获7枚offer,入职阿里平台事业部】横扫阿里、美团、京东、 去哪儿之后,写下了这篇面经!
- OSChina 周日乱弹 ——已经打码请放心观看
- mysql 错误等级_Mysql5.7 的错误日志中最常见的note级别日志解释
- (02)FPGA芯片厂商介绍
- 群里又会python的吗_自从会了Python在群里斗图就没输过,Python批量下载表情包!...
- bq24075 锂电池 充电电路分析
- 摄影测量(一):概述
- android脚本,安卓好用的脚本程序—Gscript
- 百度网盘加速教程(绝对有效)
- Elasticsearch顶尖高手系列:高手进阶篇(一)
- idea报Establishing SSL connection without server‘s identity verification is not recommended.
- ue4 unreal4 json序列化工具 数据转成字符串等
- python做地图导航_【Python】利用高德地图做你想做之事
- ChatGPT工作提效之初探路径独孤九剑遇强则强
- android 分享图片到朋友圈 大小限制,朋友圈传播图的尺寸是多少?朋友圈图片大小限制...
- 利用Python创建Excel条形图
- Improved Pre-Warping for Wide Angle, Head Mounted Displays
- 收集各种 iOS App 开发可以用到的代码示例
- 2022年1~6中国市场汽车销量可视化