不可靠进程
不可靠进程
休眠的进程
让我们考虑在分布式系统中使用危险时钟的另一个例子。假设你有一个数据库,每个分区只有一个领导者。只有领导被允许接受写入。一个节点如何知道它仍然是领导者(它并没有被别人宣告为死亡
如果节点发生故障,就会停止续期,所以当租约过期时,另一个节点可以接管。可以想象,请求处理循环看起来像这样:
while(true){
request=getIncomingRequest();
// 确保租约还剩下至少10秒
if (lease.expiryTimeMillis-System.currentTimeMillis()< 10000){
lease = lease.renew();
}
if(lease.isValid()){
process(request);
}}
}
这个代码有什么问题?首先,它依赖于同步时钟:租约到期时间由另一台机器设置(例如,当前时间加上
但是,如果程序执行中出现了意外的停顿呢?例如,想象一下,线程在
假设一个线程可能会暂停很长时间,这是疯了吗?不幸的是,这种情况发生的原因有很多种:
-
许多编程语言运行时(如
Java 虚拟机)都有一个垃圾收集器(GC) ,偶尔需要停止所有正在运行的线程。这些“停止世界(stop-the-world)”GC 暂停有时会持续几分钟!甚至像HotSpot JVM 的CMS 这样的所谓的“并行”垃圾收集器也不能完全与应用程序代码并行运行,它需要不时地停止世界。尽管通常可以通过改变分配模式或调整GC 设置来减少暂停,但是如果我们想要提供健壮的保证,就必须假设最坏的情况发生。 -
在虚拟化环境中,可以挂起(suspend)虚拟机(暂停执行所有进程并将内存内容保存到磁盘)并恢复(恢复内存内容并继续执行
) 。这个暂停可以在进程执行的任何时候发生,并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移,而不需要重新启动,在这种情况下,暂停的长度取决于进程写入内存的速率。 -
在最终用户的设备(如笔记本电脑)上,执行也可能被暂停并随意恢复,例如当用户关闭笔记本电脑的盖子时。
-
当操作系统上下文切换到另一个线程时,或者当管理程序切换到另一个虚拟机时(在虚拟机中运行时
) ,当前正在运行的线程可以在代码中的任意点处暂停。在虚拟机的情况下,在其他虚拟机中花费的CPU 时间被称为窃取时间(steal time) 。如果机器处于沉重的负载下(即,如果等待运行的线程很长) ,暂停的线程再次运行可能需要一些时间。 -
如果应用程序执行同步磁盘访问,则线程可能暂停,等待缓慢的磁盘
I/O 操作完成。在许多语言中,即使代码没有包含文件访问,磁盘访问也可能出乎意料地发生——例如,Java 类加载器在第一次使用时惰性加载类文件,这可能在程序执行过程中随时发生。I/O 暂停和GC 暂停甚至可能合谋组合它们的延迟。如果磁盘实际上是一个网络文件系统或网络块设备(如亚马逊的EBS ) ,I/O 延迟进一步受到网络延迟变化的影响。 -
如果操作系统配置为允许交换到磁盘(分页
) ,则简单的内存访问可能导致页面错误(page fault) ,要求将磁盘中的页面装入内存。当这个缓慢的I/O 操作发生时,线程暂停。如果内存压力很高,则可能需要将不同的页面换出到磁盘。在极端情况下,操作系统可能花费大部分时间将页面交换到内存中,而实际上完成的工作很少(这被称为抖动(thrashing) ) 。为了避免这个问题,通常在服务器机器上禁用页面调度(如果你宁愿干掉一个进程来释放内存,也不愿意冒抖动风险) 。 -
可以通过发送
SIGSTOP 信号来暂停Unix 进程,例如通过在shell 中按下Ctrl-Z 。这个信号立即阻止进程继续执行更多的CPU 周期,直到SIGCONT 恢复为止,此时它将继续运行。即使你的环境通常不使用SIGSTOP ,也可能由运维工程师意外发送。
所有这些事件都可以随时抢占(preempt)正在运行的线程,并在稍后的时间恢复运行,而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全:你不能对时机做任何假设,因为随时可能发生上下文切换,或者出现并行运行。当在一台机器上编写多线程代码时,我们有相当好的工具来实现线程安全:互斥量,信号量,原子计数器,无锁数据结构,阻塞队列等等。不幸的是,这些工具并不能直接转化为分布式系统操作,因为分布式系统没有共享内存,只有通过不可靠网络发送的消息。
分布式系统中的节点,必须假定其执行可能在任意时刻暂停相当长的时间,即使是在一个函数的中间。在暂停期间,世界的其它部分在继续运转,甚至可能因为该节点没有响应,而宣告暂停节点的死亡。最终暂停的节点可能会继续运行,在再次检查自己的时钟之前,甚至可能不会意识到自己进入了睡眠。
限制垃圾收集的影响
过程暂停的负面影响可以在不诉诸昂贵的实时调度保证的情况下得到缓解。语言运行时在计划垃圾回收时具有一定的灵活性,因为它们可以跟踪对象分配的速度和随着时间的推移剩余的空闲内存。一个新兴的想法是将
这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象要快速收集