系统工程(System Engineering)是夹缝中的生存艺术

IT江湖上流传有一种说法,说的是“编译器和操作系统是计算科学的皇后”(不好意思,两个皇后)。编译器追求的是编程艺术,无数先贤发明编程语言获得图灵奖。如果说编译器是珠峰上的明珠,那笔者认为操作系统就是喜马拉雅山的垫脚石。操作系统没有编译器那么高大上,没有特别高级的算法,也没有让人眼前一亮的漂亮功能。它只是为所有可能的程序提供高效稳定的运行环境。单想做到这一点就足以耗尽无数系统程序猿的心血,却往往换不来一句好评。

刚入行时,听一位老司机说:“我们搞计算机的和木匠瓦匠是一回事,都是手艺人”。计算机科学与物理化学不一样,本质上不发现自然界的未知规律,它的目标是用已有的工具或者创造新工具解决已知的问题。它更像是经济学,追求的是解决问题的效率。在计算机领域的所有手艺中,系统开发(泛指操作系统内核、存储、计算基础架构等系统级工程)是“两头堵”的艺术,系统程序猿们整天忙着拆东墙补西墙,众口难调调众口。

写了很多编程宝典的侯捷说:“源码之前,了无秘密”。笔者想说,这句话可能不适用于系统软件。系统软件一旦运行,往往展现出复杂的动态行为,万“相”丛生,非“代码”两字足以囊括。笔者借用一个名词,称它是动力系统,说明其动态演化的特点。笔者在本篇和大家聊聊这些动态系统的机制,以及它们对真实的计算系统或者业务的决定性影响。

不会看病的程序猿不是好员工

任何一个产品交付给用户使用后,自然形成一个责任边界,即哪些责任由产品提供商负责,哪些责任由用户负责。实际情况是怎么样的呢?供需不平等决定了甲乙双方地位实质上不对等,用户往往进行有罪推定,而产品提供方则需要自证清白。基础架构产品(存储或者计算),他们对应用程序的运行时行为有间接但是决定性影响,责任边界更难划分。做系统的工程师都得会两手:一手给自己找茬(程序的Bug到底在哪里?),一手替别人看病(这是你的问题,不是我的Bug)。如果问题出在企业的生产环境,工程师更得如履薄冰,小心翼翼。

线上运维之“悬丝诊脉”

我们从小说和影视剧中常常可以看到“悬丝诊脉”的情节。神医把丝线的一头搭在贵妃的手腕上,另一头则由自己掌握。神医必须凭借着从悬丝传来的手感猜测、感觉脉象,诊断疾病,因为男女授受不亲,贵妃的身体更是碰不得。企业生产环境的应用跟贵妃一样,乙方能不碰是尽量不去碰的。例如用户一个典型的组装植物基因组的程序,经常得跑上几个星期,疯狂使用内存或者存储。如果跑到第十天还没有结束,用户会理直气壮地问“为什么还没有结束啊?系统出问题了吧?”。这个时候我们的工程师就得上去诊断。用户的程序是不可能停的,也不能调整配置做实验,或者调试一下存储。如果用户的程序被你弄挂了,就更说不清楚了。工程师们既要帮用户解决问题,还得离这个程序远远的。这就是典型的悬丝诊脉嘛。

当然也有乙方是比较生猛的。我们曾经在一个公有云的大厂上跑一个流程,运行了几天,程序卡住了。原因很简单,从两个不同的机器看一个目录,内容不一样。做过分布式存储的同志们都知道,这是数据不一致了,大概率是分布式锁的问题。乙方上去折腾了几天,搞不定。最后他们的支持工程师来了个猛的,提出把挂载点umount/mount解决问题,我们直接就傻眼了:“2B的工程师们不可以这么生猛吧?”。这显然不是典型的系统工程师啊。

学会当系统工程师

系统工程师这个说法笔者刚开始参加工作的时候从一个洋人大老板那里听来的,大体意思是从硬件到软件,从内核到用户态程序,从代码到脚本都得精通。这个系统的所有问题都是你的,需要你去搞定。当时觉得老板你真会忽悠人干活。后来自己参与存储创业,才明白了这个道理。为什么对工程师这么要求呢?因为一个存储或者计算系统是一个端到端的全栈解决方案。存储不等同于硬件,计算不等同于调度。用户的应用出了问题,你没法直接说问题不是你的。你得上去从头到脚的分析,直到证明这是应用程序自己的问题。在这个过程中,还要保证应用不受影响。

操作系统是个动力系统

操作系统面面观

在计算机中,操作系统是其最基本也是最为重要的基础性系统软件。教科书把计算机系统结构定义为程序编写者所看到的外特性,即计算机的概念性结构和功能特性。操作系统也类似。从用户的角度来说,操作系统是他需要使用的各项服务;从程序员的角度来说,操作系统是指用户登录的界面或者接口;如果从设计人员的角度来说,就是指各式各样模块和单元之间的联系。经过几十年以来的发展,操作系统已经由一开始的简单控制循环体发展成为较为复杂的分布式操作系统,再加上计算机用户需求的愈发多样化,计算机操作系统已经成为既复杂而又庞大的计算机软件系统之一。

对用户来说,操作系统是这样的:

用户看见的操作系统

对系统工程师来说,操作系统是这样的:

Linux操作系统

操作系统是典型的共享经济

对应用程序猿来说,操作系统是一个黑盒子。对经历过严酷生产环境的系统程序猿来说,操作系统是一个动力系统。操作系统被多个或者多种用户程序共享,其中CPU、内存、Disk、网络和文件系统IO交互影响,在整体上表现出难于通过代码逻辑直接推理的现象。共享的难点在于保护和调度。保护所用的机制叫锁(Lock),这个后面再聊。调度本质上解决的就是资源供求不匹配的问题供求匹配只需要管理,供不应求则需要调度。学过计算机理论的同志们都知道,大多数调度问题都是NP难问题。这句黑话说的是调度在理论上没有简单但是完美的解决方案。用户程序通过操作系统的系统调用进行博弈,操作系统则采用各种高明的动态机制倒腾资源。正是这些动态机制带来了复杂性。

举一个常见的例子。相比于CPU这种可以分时复用的资源,内存是不可压榨的资源。内存特定区域存储程序A的内容,就不能存储程序B的内容,要复用就得倒腾,在内存和硬盘之间交换,即所谓的swap机制。应用程序分配的是虚拟内存,程序不访问虚拟内存区域的时候,操作系统不会这个区域分配并映射物理内存。当程序需要访问虚拟内存的时候,操作系统通过硬件的缺页中断(Page Fault)机制动态映射物理内存到应用程序。当系统物理内存不够的时候,操作系统自动将应用程序的最近不太可能使用的内存区域的数据写入到硬盘某个地方,回收物理内存,给其它程序使用。大家看着是不是有点眼熟,这不就是超分资源,典型的共享经济嘛。操作系统的开发者们设计出了各种聪明的策略和算法来倒腾内存,共享经济在大多数情况下运行良好。但是计算密集型和IO密集型的应用场景,系统资源使用达到极限,相当于大面积的挤兑(类似于广大人民群众一起要回ofo的押金),操作系统的共享经济就宣告破产了。

操作系统破产后的结果是什么呢?其中一个最常见的现象就是程序卡(Block)住。系统程序猿们常用的一个黑话叫“Hang”,意思就是调用不返回,系统暂时不响应服务。Hang不是最坏的结果,操作系统尽最大努力在倒腾出资源给你用,大多数情况下你只是响应变慢,Hang住的操作最终返回,造成的是性能问题。如果操作系统自己也被卡住,那就是死锁了,游戏正式结束。

大量IO是操作系统破产的重要原因

一般来说,普通应用程序对内存或者CPU资源的使用不会特别厉害,操作系统的那一套机制完全应付得来。提供数据IO服务的存储系统,或者IO密集型的计算任务,它们对资源的使用接近极限,是系统中的不稳定因素。极道的计算平台常常用来测试极限存储性能,一旦大量的fstress或者IO测试工具运行,系统明显变慢,如果运行足够长,系统经常性Hang住或者崩溃。其中大多是内存的过度使用引起了多米诺骨牌的连锁反应。

内存问题与文件系统IO的相互影响非常微妙。IO系统会在短时间内消耗大量内存,这部分内存并不容易回收。因为回收内存需要将数据同步写出,写的过程可能涉及到新的IO操作和额外内存使用。文件系统、网络协议栈和其它内核子系统通过内存管理机制动态博弈,产生非常复杂的行为。一旦内核某个部分因为分配不到内存,或者因为等待IO不能返回,即所谓Hang住。如果Hang住的代码正好获得(Hold)了某个关键的资源(例如锁)的话,就可能阻塞它的调用者或者依赖它的其它子系统。最终Hang会像癌细胞一样,逐步蚕食周围的细胞,一步一步蔓延到整个内核。如果这台机器是某个分布式系统的一部分,Hang还会通过网络进一步传递。

IO系统与其它子系统之间的竞争也非常微妙。笔者在FreeBSD曾经调整过一个内核网络缓存分配参数,ZFS文件系统的性能直线下降,无论怎么都调整不上去,复原参数性能也精确复原。这些问题很难从程序代码上分析,也不容易量化,只能通过不断的调整和测试获得经验参数。

死锁是并行编程的并发症

操作系统发展史上有位荷兰的大师叫Dijkstra,他奠定了并发程序设计的基础。他发明了很多算法和机制,确保同时运行的程序安全共享数据,而且程序使用这些机制后具备简单可证明的正确性。锁(Lock)就是其中一个发明。在他之前,程序猿们需要精确计算系统中的每个程序访问某个资源的时间,保证不会有两段程序同时访问这个资源,类似于错峰出行。在他之后的程序猿们要确保独占访问资源的时候,只需要采用下面的编程模式:先拿锁(Lock),访问临界区资源,结束后放锁(Unlock)。只要保证不会有两个人同时拿到同一把锁,就保证了程序在临界区内的绝对互斥。正确性保证从每个程序转移到锁的实现上。你听起来觉得这是一个简单的发明,自己也能想到,其实这是一个一个重大突破。它改变了程序猿的思维方式,将程序互斥访问的正确性与程序执行顺序分开

锁解决了互斥访问的问题,但是带来了另一个问题,那就是死锁。死锁产生的原因是资源的需求成了一个环。例如程序1,拿到了A锁,试图去拿B锁。同时程序2拿到了B锁,试图去拿A锁,两个程序形成了A->B->A 的环。它和我们交通道路上车辆之间各不相让,互相堵死的情况类似。这个时候除非某个拿到资源的人愿意主动放弃,死锁是不能解开的。但是我们伟大的Dijkstra先生发明锁的时候,就是要让每个程序执行顺序与其它程序无关,不会有多少程序猿写程序的时候还考虑其它程序。系统开发人员常常使用某些约定俗成的规则避免死锁发生,例如确保锁的顺序,保证资源按照既定顺序申请和释放等等。

在真实的环境中死锁难于避免,避免的结果是死锁发生场景日趋复杂,形成的资源依赖环越来越长。在IO密集型或者数据密集型计算的场景下,系统资源使用达到极限,死锁的概率非常高,因为当有IO参与其中的时候,内存、Hang和锁相互交织成复杂的依赖关系。这也是为什么很多大型计算系统运行时间长后经常跑死。这些程序通常不是纯粹的CPU计算,需要大量复杂的IO,将底层系统资源使用推到极限。

定位还是容错?这是个问题

就像曾经聆听前辈程序猿的教诲,笔者也经常煞有介事的“教训”小朋友程序猿们:“计算机没有灵异事件,所有问题都要重现并定位才叫解决,不要胡乱绕过去”。曾经安排一个小朋友部署一套存储系统。硬件有点问题,部署总是在一个地方出问题。小朋友也挺努力的,不断重试,从傍晚到天明,当然最终也没有成功。小朋友后来没有通过试用期,原因很简答。你不断重复一个确定的过程,不去寻找原因,也没有烧香拜佛,结果怎么可能会不同呢?这不是程序猿思维嘛!

但是系统问题不同于程序Bug,是制度性缺陷在压力下演变成的经济崩溃。任何制度在某个极限压力下都有可能崩溃。曾经听某顶级互联网公司的同志介绍,他们经常用几千台机器跑一个分布式计算任务,最后总会有几个机器死掉,无法响应。如果再跑一遍,还是有几台机器没有结果,但不是刚才那几台机器了。工程师大牛们都去找原因,总找不到原因。最后解决的办法就是监控计算任务的进程,如果某几个机器上的任务进度落后太多,就在其它机器上启动一个同样的备份,谁先完成就用谁的结果,把没有完成的任务杀掉。说到这里大家明白了,这就是Google的Map Reduce系统和Hadoop采用的机制。问题没有定位,任务圆满完成,这就是容错方法的本质

归根结底一句话,程序Bug要定位,系统问题靠容错。前者追求最坏情况下的正确性,后者确保极限压力下的鲁棒性。

分布式系统是个更大的动力系统

什么是分布式系统?

随着云计算和互联网应用的发展,分布式计算得到迅猛发展。什么微服务架构,云原生啥的都是要用分布式架构做应用。我们每天谈论分布式系统,可能很少有人想去给分布式系统下一个定义,或者探究分布式系统的定义。著名的图灵奖获得者,同时也是分布式计算的奠基人之一的Lamport给分布式系统下了一个通俗的定义:分布式系统就是你因为一个你不知道的机器故障,而不能继续工作

笔者举一个很普通具体的例子来解释一下Lamport的定义的深刻性。例如你的Linux系统使用LDAP做用户认证,LDAP服务器在一个你不知道的服务器上运行。为了保证配置高可用,LDAP使用的数据库存储到一个分布式存储中。如果分布式存储故障,读请求不能响应,LDAP服务也不能响应用户的认证请求。这个时候用户在Linux上运行一个简单的top命令,就会被卡住。你的一个简单命令因为一个跟你没有任何关系的机器故障产生了异常。

网络问题属于偏头疼

分布式系统的本质是多个物理机器上操作系统的服务通过网络交互完成计算,是一个更大的动力系统。网络的存在让这个动力系统更加动态复杂。由于分布式系统固有的复杂性,网络硬件以及网络硬件之间的交互都不是百分之百靠谱的,因此经典的TCP/IP网络协议具备很强的容错性。这种容错性给程序猿提供了可靠的操作界面,同时也是一种双刃剑,增加了分布式系统行为的动态性。

例如从网络硬件层面看,丢包是一个常见的情况。你如果仔细研究网卡硬件的数据会发现,总是会有一些错误或者丢包。但是一般负荷或者故障情况下,丢包不会太严重,TCP协议自动容忍错误,应用程序根本感觉不到。当丢包比率进一步增加,达到某个限度的时候,容错继续发挥作用,应用程序明显感觉到交互变慢了。这个时候错误演变为性能问题。如果故障进一步升级,触发TCP的超时机制,则会发生连接断掉,演化成为应用程序级别的故障。这有点像物理学中的相变,随着温度升高,从固态、液态到气态动态转化,随着压力不一样转化温度也不一样。对网络问题来说,丢包率类似温度,负载类似压强,决定着系统从健康、性能问题到故障的突变。

你可能会说:“这也太邪乎了吧,真的假的啊?” 笔者和极道的同志们亲身经历过各种网络的妖魔鬼怪。其中一次客户反映说计算任务读写数据性能不太好,同志们通过各种工具分析,从CPU、内存、硬盘、HBA、磁盘柜(JBOD)到网卡从头到脚都过了一遍,都没有问题。但是用户程序端到端的性能确实比平常下降了30%。折腾了好几天,最后发现连接交换机的网线松了。网线接触不好的程度没有造成连接问题,没有造成不能工作,只是性能达不到峰值。笔者和小伙伴曾经经历过FreeBSD的intel网络驱动Bug,问题的根源是一个buffer没有初始化。造成的结果是大块数据读写没有任何问题,ssh连接有时候正常,有时候慢,有时候无故断掉。后来用TCPDump把机器之间的包都抓出来,挨个比对,就发现偶尔个别包发出去消失的无影无踪,对方的TCP序号乱掉了。

网络问题的复杂根源在于它不是一个0或者1的问题,硬件问题、软件Bug与协议容错能力博弈之后,故障软着陆,调试难度指数级上升。笔者有时候头疼的厉害,去医院检查,CT、核磁检查不出问题,就被医生诊断为偏头疼,推荐到神经内科。老婆就说:“完啦!一个病人进了神经内科,被诊断为偏头疼,表示他的病再也找不到原因了”。笔者经历的很多网络问题大多变成了偏头疼。当然机器比人有一个优势,从光纤、模块到网络都可以换个遍,通常换完之后问题也差不多了。

一致行动的敏感和脆弱

分布式系统的一大难点在于获得一致性,即多台计算机上的服务在可能的各种故障场景下通过消息交换达成一致。Lamport老先生对分布式计算的贡献在于大量的一致性或同步算法,最著名的莫过于实现谷歌的Chubby系统的PAXOS协议。后来又有了Zookeeper,还有后续的基于RAFT协议的ETCD。它们提供了分布式系统需要的同步服务,避免广大程序猿同志们重新造轮子。有兴趣的同志可以去查查Lamport早年的论文,其中一篇是七十年代末参与一个叫SIFT系统的设计。这个系统研究软件方法容错,活脱脱一个用在飞机上的分布式系统。容错和一致性是伴随着分布式系统诞生和应用的中心问题。

理论早已证明,在通信不可靠的系统中不存在达成一致性的方法。企业涉及到的计算系统大多不会涉及到跨地域的强耦合,因此可以视为同步系统。所谓同步系统指的是消息的传递和机器之间执行同一个任务之间的时间偏差不会超过某个上限。而异步系统则没有这个假定。理论上在分布式系统要同时实现容错和一致依赖各种各样的故障检测器(Failure Detector)。心跳机制是同步系统中典型的故障检测器,如果消息传递时间上限可以假定,则故障检测器是可以做到绝对精确的。实际系统中,故障检测器的精确性被认为故障的机器真的故障了)和完备性所有的故障机器最终都被检测到)常常是一个矛盾。真实的系统网络通信不可能完全可靠,消息丢失是常有的。同志们也许会问:“既然通信不可靠,那为什么我们实际的系统为什么可以达成一致呢?”。因为超时机制(timeout)充斥在我们的系统的每个地方。很多实际系统的正确性最后都依赖于一个超时,系统设计者在超时发生的时候要在正确性可用性之间做出两难抉择。如果要保证数据绝对不丢,或者系统绝对一致,往往服务不能继续。这也是所谓的CAP定理的结果。

操作系统极限情况下的动态性会加大分布式系统设计假定与实际情况下的偏离。例如大量的峰值IO读写会占用大量内存,造成网络延迟加大,从而造成心跳大面积超时,触发系统各个层面的超时故障检测和恢复机制,引起连锁反应。自动故障恢复听起来是一个很炫的功能,但当它发生在没有故障的时候,就是恶意的破坏。举一个简单的例子。计算或者存储系统常常实现IP地址迁移功能,确保服务高可用。如果系统因为大量IO发生心跳超时,被误判为机器故障,触发IP地址迁移,则服务的数据访问可能马上断掉。悲剧的是,大量读写发生的时候可能是最需要维持系统稳定的时候,但是维稳机制本身破坏了系统的稳定。这也是系统恢复时间和稳定性之间两难的选择。大多数时候,你需要调节系统参数不要过于敏感,减少误报故障造成的系统震荡,同样的参数在真实故障的情况下意味着服务迁移较大的延迟。

就像顶级运动员一样,实际可用性高的系统往往具有大心脏,不会过于敏感。例如检测到硬盘故障不能马上让他掉线,因为这可能只是一个抖动。如果大量硬盘同时掉线,可能得更慢一点,因为可能是连接的SAS线抖动了一下。这也叫每临大事有静气。分布式服务要减少运行时依赖。例如高可用设计的系统往往要求服务之间通过域名而不是IP地址去访问服务,这样域名服务反而被引入到关键路径中。很多时候域名服务更容易崩溃,因为它常常是设计中被忽略的部分。其中一个解决方法是服务自己缓存IP地址,只有在通过IP访问目标服务出问题的时候才重新询问域名服务,这样鲁棒性自然增加了。

计算耦合存储形成另一个动力系统

随着数据驱动理念在各个领域普及,计算不再是单纯的CPU计算,常常伴随着大量的数据读写。很多应用领域的大规模计算涉及到多机并行程序通过分布式文件系统交换数据,而生命科学领域的计算涉及的数据量尤其大。这样计算和存储之间联动形成了典型的动力系统

计算程序性能由哪些因素决定?

在实际系统运维过程中,用户常常抱怨自己的程序运行慢,或者说比自己开发环境中慢。那么决定程序运行的性能的主要因素有哪些呢?首先是程序自己的并行性。程序是否有足够的并行粒度,有效利用CPU资源,并给程序分配足够的资源。这是用户比较容易理解的。另外两个不容易理解的因素是内存分配和数据IO。

很多计算程序需要使用大量内存。前面讨论操作系统的时候已经介绍过了,程序直接使用的虚拟内存,操作系统根据需要动态分配物理内存。当系统内存不足的时候,操作系统会将物理内存回收,将数据临时存储到硬盘的交换(swap)区域。如果应用需要访问这部分数据,操作系统通过硬件中断重新分配物理内存,将数据从swap加载到内存。这看上去很麻烦,但是操作系统借此给应用程序提供了远大于物理内存的内存空间,可以说非常聪明。这个机制运行的效率在于程序的局部性,即操作系统总能将不会马上访问的数据交换出去。一旦局部性假设失效,或者系统负荷太重,物理内存严重不足,操作系统就会大部分时间倒腾数据,系统绝大部分时间用来做无用功。这就是所谓颠簸(thrashing)现象。这个时候用iotop去观察用户程序,会发现90%的时间花在等待swap读写上。这种情况在传统的HPC平台上很常见,因为对程序资源不做限制,每个人倾向于多用内存,最终结局是系统会被跑死。在容器运行环境下,这个问题也很普遍,用户给容器设置内存上限,虽然可以通过Linux内核的CGroup的OOM(内存超限)机制将超过物理内存限制的程序杀死避免系统崩溃。但是如果CGroup的物理内存限制设定在某个范围,操作系统对程序的物理内存按需分配和通过swap释放维持在平衡状态,程序不会被杀死,但是会花绝大多数时间swap,结果是程序运行很慢。

另一个性能杀手是访问存储。数据IO相对于CPU计算是很慢的操作。如果程序运行过程中涉及到大量的随机IO、小IO或者大量元数据操作,程序会因为等待数据读写耗费大量时间,不能充分利用CPU,造成性能大打折扣。

存储Hang是一把双刃剑

之前部分谈过存储的Hang往往引起连锁反应。有人可能会问:“为什么要阻塞住程序呢?应该出错返回啊”。这涉及到存储的设计原则和一致性语义。大多数时候存储或者文件系统Hang住应用程序是一种良性的保护措施。

绝大多数人对存储的理解是一个硬件,或者说提供服务实现高速的硬盘读写。其实存储系统或者分布式文件系统最复杂的是保证数据的可靠性和一致性。可靠即数据在故障情况下不丢失,一致性可以理解为数据在故障情况下不矛盾。由于存储设备相对于内存是慢速设备,为了提升性能,文件系统引入了异步IO的语义。也就是说程序写数据,操作马上返回,但是不保证数据落盘。程序可以调用一个sync系统调用确保数据落盘,这个操作是阻塞的,也就是说不落盘不返回。这个机制可以显示提高程序的数据写性能。但是它加剧了实现复杂性。因为它意味着需要提供某种程度的一致性保证用户程序的正确性。例如最典型的是读写一致性:“程序写入数据值A,写操作返回后,后续的读操作应该返回A”。假设没有这种一致性,你的程序写入数据A覆盖旧数据B,没有后续修改,第一次读返回A,第二次读返回B,第三次返回A,你会不会立马有一种砸掉系统的赶脚?而存储系统的复杂性就在于各种故障模型下需要保证某种程度的一致性。

一致性语义决定了存储系统必须在某些时候阻塞住用户的读写操作,确保数据一致写入,或者正确版本的数据被读取。这是存储系统的契约精神。绝大多数程序在读写数据失败后除了重试外,无法继续下去。因此存储或者文件系统会尽可能阻塞住应用程序。笔者遇到一个真实的例子。我们在某个公有云上做计算,云提供商承诺存储可以做到动态扩容。我们的程序运行到某个时候,数据空间满了,结果写操作马上返回失败。得到的回复是扩容是自动的,但是会返回失败。事实上企业级应用程序开发者对动态扩容的理解是IO不返回失败且完成扩容。这个时候Hang是一种良性机制。

但凡是存储系统的Hang,都不能确定操作的返回时间。这就意味着应用程序开发者需要遵循某些规则,例如不在临界区内访问慢速设备。然而大多数应用程序开发者对这件事没有意识。例如大家常用的Docker服务,就在一个锁的临界区内调用了stat文件系统操作。如果关于这个目录的IO不能返回,这把锁也不再可用。这个连锁反应造成的结果是docker ps操作被卡住。在实际中,这类存储引发的蝴蝶效应屡见不鲜。这是Hang引发恶果的一面。

结束语

本篇描述的问题,只是提供系统级产品服务的兄弟姐妹日常遭遇的一小部分。一个好的存储或者计算系统,应该设计成在极限状况下,或者两难处境中依然提供高质量的服务。存储不等于硬件,系统开发不等于写程序,期望更多的同志理解并加入严肃、枯燥、一旦进入无法自拔的系统开发大家庭。

Powered by XTAO TechnologyLast Modified On:2021 2023-03-24 09:05:14

results matching ""

    No results matching ""