知识卡片
进程可能任意暂停多久导致基于时钟的租约机制潜藏危险
内容
分布式系统里常靠租约(带超时的锁)来确定”谁是当前的领导者”:节点持有租约期间可以 安全地认为自己是领导者,必须在租约到期前定期续期,一旦停止续期租约会过期,另一个 节点就能接管。一段典型代码会在处理请求前检查”租约剩余时间是否还够(比如10秒缓冲)”, 这个设计的隐藏假设是:从检查剩余时间到真正执行请求,这段代码执行得足够快,10秒缓冲 绰绰有余。但这个假设可能被进程暂停打破——如果线程在检查完剩余时间后、真正处理请求前 被意外挂起了15秒,等它恢复时租约可能早已过期、另一个节点可能已经接管了领导权,而 这段代码对自己曾经”睡了”这么久毫无察觉,会在没有察觉的情况下继续执行本该被禁止的 操作。这类长时间意外暂停并非危言耸听,来源相当多:Java虚拟机的”停止所有处理”GC暂停 有时长达数分钟;虚拟机整体被挂起(用于实时迁移)可以在进程执行的任意时刻发生、持续 任意长;操作系统上下文切换、虚拟机监视器切换到其他虚拟机(产生”窃取时间”);同步 磁盘I/O、意外的类加载惰性磁盘访问;内存交换到磁盘引发的页面错误,极端情况下甚至 可能”抖动”(大部分时间都在换页而几乎做不了实际工作);还有人手一抖发送的SIGSTOP 信号。这些暂停的共性是:可以在代码执行的任意一点发生,持续时间不可预知,暂停的线程 恢复后并不知道自己被冻结了多久——单机多线程编程里靠互斥量、原子计数器这些工具能 较好地应对线程安全,但这些工具在分布式系统里都不适用,因为节点之间没有共享内存, 只能通过不可靠的网络传消息。
结构图:
flowchart TD
A[节点持有租约, 定期续期] --> B[检查剩余时间是否够本次请求]
B --> C[进程在此处意外暂停: GC/VM挂起/换页/SIGSTOP等]
C --> D[暂停期间租约过期]
D --> E[另一节点接管领导权]
C --> F[暂停结束, 线程恢复继续处理请求]
F -.线程不知道自己曾暂停多久, 误以为仍持有租约.-> G[两个节点同时以为自己是领导者]
参考来源
- 位置:《数据密集型应用系统设计》第八章《分布式系统的麻烦》"进程暂停"(源文件:
_epub-src/ch8_split_003.html)
- 结论依据:原文用代码示例说明检查租约剩余时间和实际处理请求之间若发生长时间暂停,
租约可能已过期而线程未察觉,并列举GC暂停、虚拟机挂起、上下文切换、磁盘I/O、
页面交换、SIGSTOP等多种可能导致长时间意外暂停的原因,直接支撑本卡片的结构梳理。
- 原始内容:如果程序执行中出现了意外的停顿呢?……在这种情况下,在请求被处理的
时候,租约可能已经过期,而另一个节点已经接管了领导……许多编程语言运行时……都有
一个垃圾收集器……这些"停止所有处理"GC暂停有时会持续几分钟。