共计 5923 个字符,预计需要花费 15 分钟才能阅读完成。
本文首发自「慕课网」,想理解更多 IT 干货内容,程序员圈内热闻,欢送关注!
作者 | 慕课网 Stannum
面向故障编程是一种思维形式。对于有志于成为架构师的小伙伴,这更是十分重要的一种思维形式。总而言之,你是一个成熟的程序员了,是时候学习面向故障编程的思维了。心愿明天的分享能带给大家一些无关的思考。
01 前言
作为程序员,最大的噩梦,可能就是下班时间,当我正在开心的浪着,忽然传来一阵短促的铃声,运维的共事说零碎不行了,我必须马上上线帮忙抢救 …… 之前还看过一个更惨烈的新闻,有一位程序员新郎,在本人的婚礼上,还不得不上线保护零碎 …… 等你好不容易折腾了半天,终于把零碎稳固住了,还没来得及喘口气,老板就顶着一张黑脸,收回了灵魂的拷问:为什么测试的时候没产生问题,生产环境里却出了故障?这是一个价值百万的问题。我来试着帮你答复一下:功能测试只笼罩了侧面测试 (positive test),而疏忽了负面测试(negative test) 整合测试没有笼罩到的某个在生产环境中引起故障的内部零碎没有进行压力测试,或者压力测试的水平与生产环境状况相差过大这个清单还能够始终写下去。你无妨检视一下你本人的测试环境和测试设计,置信你还会发现更多的有余。老板看着这个清单,脸色越来越黑。接下来,他问出了更扎心的问题:今后怎么防止相似的故障再产生?你眼睛一亮,举起你刚刚列出的测试环境毛病清单,向老板保障你会把清单上每一个毛病都改正过来!然而,这真的是最好的解决办法吗?增强测试覆盖度是十分值得提倡的做法。然而,这未必能防止生产环境中故障的产生。因为测试环境终归和生产环境不同。如果你只是为了能通过测试而进行开发,那你的产品上线之后,注定要裸露于新的危险之下。
02 除了以来测试,还有什么更好的方法
明天,咱们就要来探讨一下,除了依赖测试,开发人员还有什么更好的方法来打造更加强壮的零碎。首先,咱们来思考一下,故障是什么?明天咱们不去探讨 bug,因为实践上来说,有 bug 的零碎基本不能通过根本测试,也就不会被部署到生产环境。如果任何 bug 侵入到了生产环境,造成了服务的中断或零碎事变,那这个责任必然要由开发人员和测试人员一起承当。
咱们想要探讨的,是在生产环境中,经常被归因于 ” 内部 ” 因素或者环境因素所造成的故障。比方配置不正确的防火墙屏蔽了零碎发送的申请。比方其余客户大量拜访数据库,从而阻塞了我的零碎收回的数据库申请。比方上游零碎产生故障,忽然发送了海量的垃圾音讯等等。遇上这样的故障,咱们会说,” 真是倒了霉了 ”,” 明天运气不好 ”,” 天有不测风云 ”。也就是说,咱们认为在生产环境中遇到故障是种 ” 异样 ” 状况,所以咱们的零碎才无奈 ” 失常 ” 运行。明天,我要扭转这个观点,在一个成熟的程序员眼里,生产环境中的 ” 故障 ” 才是 ” 实在 ”,” 异样 ” 才是 ” 失常 ”。
墨菲定律通知咱们:有可能出错的中央,就肯定会出错。在生产环境中,有可能产生故障的中央,早晚都会产生故障。作为开发人员,咱们能做的,就是利用各种设计模式和技巧,被动踊跃地去正视故障,解决故障,修复故障,将故障杀死于襁褓之中。这种思维,就叫做面向故障编程。大家跳过的坑,都是类似的。让咱们单刀直入,来看一看常见的故障模式,和它们所对应的解决办法:零碎中最单薄,最容易引起故障的中央,就是零碎中的 ” 连接点 ”,或者叫做 ” 集成点 ”。任意 socket/ 过程 / 管道 / 近程程序之间发送的申请和数据都有可能(所以早晚会)产生故障,从而造成零碎的阻塞或解体。让咱们仔细观察一下几种有代表性的 ” 交接点 ”。
目前,大部分高级通信协议都依赖于上层的 tcp 协定以及 socket 连贯来实现通信。说到 tcp 协定,大家都很理解,” 三次握手 ” 也是耳熟能详
客户端发送 SYN 到服务器监听端口以发动连贯申请,如果此时没有过程正在监听这个端口,服务器就会返回 TCP reset 以停止此次连贯申请。而如果服务器端过程正在监听此端口,服务器就会返回 SYN/ACK 示意承受连贯申请。客户端收到之后,再发送 ACK,到此为止,新的连贯就建设起来了。可是在生产环境中,事件却没有这么简略。如果客户端与服务器端之间存在一道防火墙呢?因为测试环境往往是 100% 的外部环境,咱们简直从没在测试环境下遭逢相似的情境。防火墙就像一个路由。依据外部配置,防火墙每次见到 SYN 申请,都会决定到底要容许(即失常转发 SYN 申请去指标服务器端口),还是拦截(即返回 tcp reset 音讯),或是疏忽(既不转发音讯,也不返回任何音讯)。而一旦防火墙决定容许一个 SYN 申请通过,就会把这个容许通过的连贯记录在外部的列表中,今后遇到这条连贯上发送的音讯,就不用再做额定的考查,间接放行。听下来没有什么问题吧~ 然而,防火墙外部的连贯列表并不是有限增长的。当某个列表中的连贯长时间处于闲置状态(无数据传输),防火墙会把这个连贯从列表中移除。可是,防火墙并不会像一般的路由那样,发送任何 reset 音讯来提醒连贯两端的 socket。所以客户端和服务器端都认为两者之间的连贯还是无效的。【发问:为什么防火墙不能发送一个 reset 音讯作为革除缓存连贯的提醒呢?答复:因为这样的 reset 音讯有可能被歹意用户利用,从而威逼到零碎安全性】只是,当它们相互之间试图持续发送音讯时,这些音讯会被防火墙有情的疏忽掉(既不放行,也不返回 reset)。此时的防火墙,齐全成为了一个网络黑洞,默默地吃掉了这条连贯上发送的数据。
作为发送音讯的一方,因为音讯被防火墙吃掉,所以无奈收到 ACK。于是 TCP 协定就会要求从新发送这条音讯,而后又被防火墙吃掉······这样周而复始,直到超过 os 内核锁预设的 TCP 重试次数最大值,才会抛出谬误。个别内核设定的 TCP 重试最大值在 15 左右,这可能导致长达 20 分钟以上的重试工夫!而接管音讯的一方更惨。它只能徒劳的期待黑洞那里传来任何数据(这当然是不可能的)。
如果接管方是以阻塞式调用来进行读取数据的操作,那么实践上来说,这个接管操作可能永远地被阻塞上来 ……. 这还只是咱们为了向大家阐明状况,解说的一个单个连贯被阻塞的情境。在生产环境中,如果咱们把例子中的客户端换成一个罕用的连接池,流量大的忙碌时段,连接池里的所有无效连贯都在不停的发送数据,所以不会造成防火墙移除超时连贯的情况产生。到了夜晚流量变少,连接池中据大部分连贯都会长工夫闲置,导致防火墙大量的移除这些超时连贯。而后第二天一早,零碎的流量又上来了,连接池中的所有连贯都被取出用来发送数据,而这些数据全副被防火墙吞掉 …… 此时你的零碎会呈现大面积的无响应正告,画面太美 …… 预计此时你也曾经接到运维小伙伴的电话了。而更糟的是,当你查看连接池这边的客户端过程,发现一切正常。当你查看服务器端的过程时,也是一切正常,网络自身也是失常状态,因为防火墙往往是由网络安全团队设置的,有些业务开发人员可能基本不晓得防火墙的存在。。。于是这个问题会成为一个悬案,往往最终都是由运维团队重启零碎来解决。这个故障情境,简直不可能通过晋升测试覆盖度来检测。咱们只能在开发阶段被动的去躲避这些可能产生(所以早晚会产生)的连贯层面的故障。针对零碎中这些容易产生连贯故障的 ” 连接点 ”,咱们给大家举荐两个最常见的办法来升高故障带来的影响,从而提醒零碎的稳定性。第一个办法就是 Timeout。Timeout 的原理很简略。为了防止连贯故障造成申请方和应答方陷入长时间的阻塞,一旦发送的申请超过肯定工夫还没有返回后果(不论是胜利还是失败的后果),咱们就停止这个申请。这样咱们才能够及时的发现失败的连贯。因为古代零碎大量应用分布式构造,零碎中的 ” 连接点 ” 不再是一个两个,而是相当大的一个数字(尤其是微服务架构),还会一直减少。
零碎中常见的问题就是 Timeout 机制的缺失。我会倡议大家将 Timeout 的逻辑包装成一个可复用的实现。这样就能够一次实现,到处调用,缩小了代码重复性。同时,也能够升高其余开发人员应用 Timeout 的难度,促使大家多多应用 Timeout 来爱护零碎。另一个经常和 Timeout 搭配应用的办法,就是 Circuit Breaker(翻译成熔断器吧)。这个词自身的意思,就是指电路中的保险丝,在电流过大时,熔断本人,爱护整条电路的平安。当咱们的申请长时间无响应,导致 Timeout 之后,咱们须要怎么解决这个未实现的申请呢?大家的第一个反馈肯定是重试,兴许刚刚应答方太忙,所以才不能及时处理咱们的申请。我再试一次,兴许应答方就能够回答了呢。然而我劝你审慎。因为作为开发人员,咱们无奈猜想导致申请 Timeout 的起因。如果是之前所讲的防火墙黑洞的例子,那你重试一辈子也是没用的。即使造成申请 Timeout 的起因确实是暂时性的,可修复的,比方是因为应答方临时忙碌所造成的,你也要留神重试的频率和次数。如果自觉的频繁大量重试,只会给应答方造成更大的流量压力,岂但对你本人的申请没帮忙,还间接影响了整个零碎的稳定性。而 Circuit Breaker 是一个能够帮到你的设计模式。你能够为有可能 Timeout 的操作增加一个 Circuit Breaker,在初始状态下,Circuit Breaker 处于连贯的状态(保险丝完整,电路连通),咱们要求 Circuit Breaker 发送的申请,都会被失常的发送进来。而当后续的申请开始呈现 Timeout 或申请的失败的情况时,Circuit Breaker 会记录下失败的次数或者频率。当失败次数或频率超过一个阈值时,Circuit Breaker 就会转换到断开状态(保险丝熔断,电路断开)。此时,Circuit Breaker 不会执行任何新的申请,而是在接到申请之后立刻返回一个谬误,告知申请的发起方,目前连贯不失常,请等一等再尝试。在通过一段时间的熔断之后(这里又用到了 Timeout 机制),Circuit Breaker 会转换到一个非凡的 ” 半连贯 ” 状态。此时 Circuit Breaker 会把收到的申请发送进来,如果发送胜利,那么 Circuit Breaker 会马上转入连贯状态,恢复正常工作。而如果这次申请发送失败或再次 Timeout,Circuit Breaker 就会立即转回断开状态,直到断开状态再次 Timeout。
由此可见,Circuit Breaker 这种机制,就是在连贯故障起因未知的状况下,试图用一种 ” 聪慧 ” 的策略来主动调整 ” 连接点 ” 的流量,以便在零碎稳定性和可恢复性之间获得一个均衡。当咱们面向故障编程时,一个很大的艰难就是故障的未知性。在开发层面,咱们很难去判断故障产生的起因。所以咱们不得不 ” 戴着脚镣跳舞 ”,在未知的状况下抉择最好的策略。Circuit Breaker 机制就是一个很好的例子。另一个可能给生产环境带来可怕结果的故障和集群无关。在古代零碎设计中,为了加强可用性,或是为了加强可扩展性以应酬更大的流量,咱们往往在一个集群中运行多个服务过程,而后在集群上通过一个 Load Balancer,负载均衡器,将发送到集群上的申请尽量均匀的调配到集群中的各个服务过程下面去。
这种集群架构,在大多数状况下,能够很无效地帮忙咱们进步整个零碎的强壮度,因为咱们有 ” 备胎 ” 了,咱们集群里有的是节点,所有节点一起死光光的概率是很小的啊。没错,这个假如通常是对的。可是,这个架构对于某一种故障十分敏感。那就是在大流量压力下所造成的节点解体。如果咱们在服务过程的实现中有个缺点,会造成内存透露。那么,当整个集群的流量增大时,每一个节点上分担的流量也很大,大流量可能放慢内存透露的速度,使得某一个节点因为系统资源耗尽而解体。而后会产生什么呢?因为集群中少了一个节点,其余节点就必须分担更多的流量。别忘了,大部分节点运行的都是同样版本的服务。所以这个要命的内存透露很可能存在于所有节点中。于是剩下的节点在接受了更大的流量之后,也会更容易耗尽系统资源而解体,而后留下更少的节点别离承当更大的流量 …… 不言而喻,这是一个恶行循环。从第一个节点解体开始,这个故障可能像洪水一样,迅速蔓延至整个集群从而导致整个集群解体。咱们管这种故障叫做连锁反应故障。听起来有点可怕,是不是?那么咱们怎么能力躲避这个故障呢?首先,改良你的测试方法,尽量在测试环境中发现相似内存透露,或是潜在死锁这样的 bug 是很重要的。因为这些 bug 都对流量很敏感。一旦把这种 bug 部署到生产环境,无论过程是否跑在集群中,都相当于在生产环境中埋了一个地雷。越是须要零碎保持稳定的大流量情境,这个地雷越容易爆炸。不过正如咱们一开始所说的,bug 不是咱们明天议论的重点。写了几万行代码,不免蕴含几个 bug……
即便这样的 bug 存在,咱们还有能够躲避连锁反应故障的办法。首先,如果集群的上游零碎总是在一次申请失败之后,就疯狂地向集群发送重试申请,那么集群的流量压力很快就被搞大了。如果说大流量是由能够产生利润的用户申请所带来的,那咱们违心接受。而如果流量是因为愚昧的零碎重试申请所造成的,并且还把咱们的集群搞垮了,那就太不值得了。所以,在开发中应用下面介绍的 Circuit Breaker 机制,不只能够爱护你正在开发的服务组件,还很有可能间接的爱护了上游的其余服务组件。你在开发上做的一点额定致力,可能援救了整个零碎。相同的,你在开发上偷的一点懒,可能会坑死上游的兄弟团队呢。除此之外,最无效的躲避连锁反应故障的办法,就是实现一个能够主动伸缩的集群。尤其是当你在 cloud 容器上运行节点时,这样的主动伸缩集群性能就更容易实现了。当集群中的一个节点解体,咱们最好尽快主动的启动一个新节点(或者重启解体的节点)。这样至多能够在短时间内,尽量保障集群的尺寸不要萎缩的太厉害,从而防止了集群中余下的节点承当(它们这个年龄无奈接受的)急剧增大的流量累赘。
到此为止,咱们梳理了连接点故障,和连锁反应故障两种在生产环境中可能造成重大影响的故障,以及 Timeout,Circuit Breaker,主动伸缩集群这三种能够用来被动躲避故障的设计模式。这只是面向故障编程思维中的冰山一小角。如果今后有机会,心愿还能够和大家持续探讨其余的模式和技巧。明天说了这么多,其实最重要的一点,就是心愿大家扭转思维,明确故障才是生产环境中的 ” 失常 ”,一个零故障的生产环境是不存在的。产品经理兴许不会把躲避这些故障作为产品需要写在文档中,然而作为开发人员,咱们本人要做到成竹在胸,其实这些都是一个优良的零碎所应该实现的隐形的需要。
欢送关注「慕课网」,发现更多 IT 圈优质内容,分享干货常识,帮忙你成为更好的程序员!