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

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
S
Schneier on Security
P
Proofpoint News Feed
The Cloudflare Blog
S
SegmentFault 最新的问题
WordPress大学
WordPress大学
Hugging Face - Blog
Hugging Face - Blog
雷峰网
雷峰网
博客园 - 【当耐特】
博客园 - 叶小钗
大猫的无限游戏
大猫的无限游戏
F
Fortinet All Blogs
宝玉的分享
宝玉的分享
博客园 - 聂微东
Engineering at Meta
Engineering at Meta
G
Google Developers Blog
Know Your Adversary
Know Your Adversary
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
CXSECURITY Database RSS Feed - CXSecurity.com
G
GRAHAM CLULEY
T
Threatpost
T
Threat Research - Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
Latest news
Latest news
C
Cybersecurity and Infrastructure Security Agency CISA
L
LINUX DO - 热门话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
SecWiki News
SecWiki News
L
LangChain Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The Last Watchdog
The Last Watchdog
阮一峰的网络日志
阮一峰的网络日志
Security Latest
Security Latest
P
Palo Alto Networks Blog
L
LINUX DO - 最新话题
博客园 - 司徒正美
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Privacy International News Feed
N
News and Events Feed by Topic
Spread Privacy
Spread Privacy
T
Tenable Blog
有赞技术团队
有赞技术团队
MyScale Blog
MyScale Blog
aimingoo的专栏
aimingoo的专栏
AI
AI

Random Thoughts

LLM 训练与推理的基本理解 昨日的世界不再重来 Magic Brush - 画出你自己的产品宇宙 写在 AI Coding 奇点之后 尼泊尔布恩山小环线纪行 2025 投资组合年报 三十而笠 我的 8 年职业生涯回顾 我的颈椎病康复之旅 - 关于选择的故事 熊野古道中边路纪行 新加坡旅居三年再回首 高加索三国 - 阿塞拜疆行记 2024 投资组合年报 重新思考 Go:了解程序在线上是如何运行的 欧游散记 —— 环勃朗峰之旅 欧游散记 —— 特摩索斯古城 重新思考 Go:Slice 只是「操作视图」 那一天,我决定踏出一步 True Story 少数价值 A Snapshot of Myself - 2021 RPC 漫谈: 连接问题 RPC 漫谈:序列化问题 RPC 漫谈: 限流问题 科学,技术与工程 Pond: Golang 通用对象池 Golang for-range 内部实现 Golang Interface 内部实现 真理的有限性 Golang rand 库锁竞争优化 Meaningless or Meaningful 分布式文件系统的演化 从动物森友会聊主机游戏联机机制 创业公司的文化 伊朗见闻录 NodeJS 内存泄漏检测与定位 设计实现高性能本地内存缓存 Travel Map Linux I/O 栈浅析 SSD 背后的奥秘 什么是真正的编程能力 沉默与反沉默的理由 命令行里的设计艺术 自由意志下的选择 学习思维方式而非学习观点 从程序到人 —— 情头配对助手的前世今生 即刻多端实时通信实践 朝鲜 —— 小国寡民的主体思想实践 使用 Surge 提升多网络环境下的流畅开发体验 一份其实好吃的 LaTeX 入门餐 Kafka 的设计与实践思考 欧游散记 —— 民主专制下的德国 欧游散记 —— 维也纳 欧游散记 —— 西班牙 欧游散记 —— 德国国会大厦 社会矛盾讨论统一框架的一种可能性 欧游散记 —— 伪君子布拉格 Lemon : Koa 风格的 Python 异步 Web 框架 Golang : Make Programming Happy Again Podcast 闲言碎语 Kubernetes 中使用 API Gateway 替代 Ingress ElasticSearch 最佳实践 blog.joway.io Presentations
重新思考 Go:Channel 不是「消息队列」
Joway · 2024-03-31 · via Random Thoughts

重新思考 Go 系列:这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题,探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。


Go 语言是一门为实现 CSP 并发模型而设计的语言,这也是它区别于其他语言最大的特色。而为了实现这一点,Go 在语法上就内置了 chan 的数据结构来作为不同协程间通信的载体。

Go 的 channel 提供 input 和 output 两种操作语法。input 一个已经 full 的 channel ,或是 output 一个 empty 的 channel 都会引发整个协程的阻塞。这个协程阻塞性能代价很低,也是协程让渡执行权的主要方法。

ch := make(chan int, 1024)
// input
ch <- 1
// output
<-ch

然而 channel 的实现恰好和进程内消息队列的大部分需求是吻合的,所以这个结构时常被用来作为生产者消费者模型的实现,甚至还作为 channel 的主流应用场景而推广。

但事实上,如果真的把该数据结构用来作为系统内核心链路的生产消费者模型底层实现,一不留神就会遇到雪崩级别的问题,且这些问题都不是简单的代码修改便能解决的。

Input 失败导致阻塞

当 channel 满的时候,<- 操作会导致整个 goroutine 阻塞。显然这并不总是编程者希望的,所以 Go 提供了 select case 的方法来判断 <- 是否成功:

select {
case ch <- 1:
default:
    // input failed
}

但问题是,当 channel input 失败时,编程者还能怎么做?除非队列的消息是可以被丢弃的,否则我们可能只再去创建一个类似 queue 的结构,将这部分消息缓存下来。但是这个 queue 的结构可能又要和这个 ch 本身的队列顺序处理好并发关系。

再或者就是创建一个额外 goroutine 来执行 channel input 的操作,但这样的代价是 goroutine 大量增加且消息变得无序:

或者还有一种最粗暴同时也是简单的方法,就是把队列 size 扩大到 N,N 的远大于业务系统正常流量下会遇到的大小。即便这种解决方法有掩耳盗铃的嫌疑,但却是我所见过的大部分人最常用的解决问题方法。实际上,让我们回归到现实中,回想我们这些年来 make channel 的时候,真的做过非常仔细地做过估算吗?还是绝大部份时候就是随便选一个差不多的数字?况且即便是在当下经过严谨的估算,这个合理值也会随着系统流量变化或是代码逻辑的改变,导致不再合理。

在大部分时候,或许并不会有什么太大的问题。但当有问题的时候,这个 input 操作可能就会导致某个核心 goroutine 直接阻塞。尤其是 input 的目的往往就是不要阻塞核心链路处理,才会想用消息队列的方式去异步处理,所以 input 发生在核心链路的概率非常大。

Output 速度不可控

最常见的消费者的写法如下:

for msg := range ch {
	go process(msg)
}

如果这里的 process 函数是 CPU 密集型函数或者是需要访问外部网络,这里都需要单独开一个 goroutine 异步执行。否则消费速度会因为外部原因出现不受控的抖动,进而导致整个消息队列生产也被卡住。

但一旦开了 goroutine 异步执行,倘若 process 是 CPU 密集型函数,我们又完全无法控制消费速度,一旦生产速度过快,系统也会遭遇到非预期的压力。何况大部分进程内消息队列使用的目的就是为了不重要的事情延后直接来提升服务实时任务的延迟。

而这里的限速,我们并不能直接限在 ch 消费的 for 循环本身,因为 channel 的消费速度和生产速度是相等的,一旦限速在这里,会导致生产者也被阻塞。所以我们还得在 process 函数内,单独加入一些深思熟虑的限速逻辑。

当你做完所有保护措施后,你会发现或许最早的时候把 channel 作为消息队列的技术选型就是值得商榷的。

重新思考 Channel

从 Go 语言本身来说,为用户提供一个便捷的乃至是语法级别的进程内消息队列实现,显然并不是语言设计者想要考虑的事情。之所以 Go 在语法层面上提供了 chan 这样一个结构,真正的目的是为了多协程间通信和休眠唤醒。它的功能和语法也很单一,并不能真正做到一个健壮的消息队列数据结构所该有的能力,也并非它的设计初衷。

其他语言中,如果用户想要实现同类需求,会自然而然去找有更工程化设计的生产者消费者模型实现,而 Go 中由于 channel 结构用得顺手,语法简单,会自然地在技术选型中使用它作为实现。进而忽视了更多安全性的问题。

解决这个问题的可行办法,只能是 Go 需要在标准库里内置一个更加完备的消息队列实现,至少需要实现可以非阻塞且安全地生产消息的能力,才能让用户脱离对 channel 队列能力的依赖和认知。