随着负载的增长,你精心设计的程序可能会遭遇很多突如其来的问题:系统原有的平稳将被打破,我们将对这些问题逐一考察。当然,你可以进行横向或纵向的扩展,也可以选择编写出更好的程序,让你的系统可以处理更高的负载。这样做可以节省更多的硬件开支,并让你的整个应用更加可靠,具有更快的响应时间。对于程序员来说,这必将获得非常大的满足感。
大量的对象
当系统中的对象数量增长到一定程度,我们通常将面临规模问题。随着对象数目的增长,所有类型的资源开销显然会带来很大的压力。
持续的故障会产生一个无穷事件流
在大型网络的故障场景中,我们没有时间进行系统恢复。因为我们始终处于一个持续的压力状态下。
大量的高优先级任务
举个例子,重选路由(rerouting)是个高优先级的活动。如果存在大量不能分发(shed)或取消(squelched)的重选路由任务,资源将连续不断的被消耗以支持高优先级任务。
更大的数据流
当媒体资源大小增长得更大时,系统的负载将会增长。系统负载会随着请求来源数目的增长而增长。
功能变更
在系统原有设计之外添加更多的功能后,系统中的漏洞也随之暴露。
客户端的增长
更多的客户端意味着更多的资源消耗。需要设置线程为客户端推送事件;客户端队列会消耗内存;通信会消耗带宽;需要为每个客户端维持一份独立数据。
不够好的设计决策
以下是一些会加剧规模问题的设计问题。
- 设计未考虑处理大量对象。
- 缺乏端到端应用级别的流控。
- 应用级别的重试将导致资源再分配和消息发送。
- 内存中代码库的开销。
- 并不真正可靠的发布。
- 特定数据结构过度占用内存。
- 消息协议没有处理所有故障场景。
- 使用磁盘作为存储。
- 磁盘同步没有使用块复制。
- 认为应用层协议会更有好。
- 依靠大功耗的CPU来提升功能负载。
- 操作系统不支持进程架构。(译者注:该点讨论见原文链接处的评论)
- 缺乏对于某些敏感操作(甚至是单条消息的删除)的硬件支持。
- 特别的网络问题,比如ARP数据包,当网络负载增长时就会出现丢失。
无效的假设
你所做的大部分假设都是无效的,比如你需要使用多少内存,某些任务需要运行多少时间,设置多少超时时间才算合理,一次会消费多少资源,可能发生什么样的故障,系统在不同点的延迟,你的队列需要多大,等等。
内存不足
内存使用基线的增长和内存使用峰值的增长,都会导致内存不足。
CPU饥饿
随着对象操作的增加,将需要占用更长的时间,这是因为这些操作需要操作更多的对象。可用CPU将变得更少从而导致其他的操作将面临CPU饥饿。系统一处出现了饥饿,就会传播到其他各处。这样一来就没有足够的CPU来处理那些需要去完成的必要任务。导致这样的原因可能是任务的基数过高或某些情况下很多高优先级的任务需要完成。
原始资源使用率增长
更多的对象将占用更多的内存。如果某人想要支持1000个并发对象,很可能是做不到的,因为你可能根本就没有足够的内存。
隐式资源使用率增长
大多数功能中,针对“原始”资源使用开销中的每项资源,都将会需要大量的额外资源。如果你将一个对象存储在两个不同的列表里,你的对象数目和内存开销将是原来的两倍;队列大小可能也需要向上调整;磁盘的数目需要增加;将数据复制到从机的时间在增长;将数据加载到应用中的时间在增长;为了处理这些工作CPU的使用率在增长;启动时间也在增长。
事件的叠加
很多系统面临的一个新的现实是无穷工作流。Web服务器和应用服务器服务着非常大的用户群,这是一个真实的可预计的关于新工作的无穷流。而工作将永不结束。一周7天24小时都会有请求进来。以100%的CPU使用率进行工作可以很容易使服务器达到饱和。
习惯上我们将100%的CPU使用率视为一个不良的信号。作为补偿,我们将创建复杂的基础设施来对工作进行负载均衡,复制状态并做好主机集群。
CPU永不疲倦,所以你可能会认为我们应该尝试使用尽可能多的CPU。
在其他领域我们试图通过最大限度的利用资源来提升生产率。
而在服务器的世界里,我们尝试通过人为的方式强制地降低CPU使用率从而保证一定的响应水平。该理由是因为如果我们没有更高的CPU可用性,我们将无法在合理的延迟时间内响应新的工作或完成现有的工作。
CPU使用率达到100%真的存在问题吗?我们在对系统做架构设计时,宁可使用CPU可用性和任务优先级作为架构设计认知上简略的一种解释依据,也不愿意先去理解下我们系统的底层工作流,从而使用这些信息再来做出明确的规划决策。难道这不正是真正的问题所在吗?
我们基于负载均衡服务器做出了拙劣的架构决策,通过主观臆想猜测了使用的线程数以及这些线程的优先级。除了以上这些,我们根本没有使用工具做过任何其他事。
扩展一个系统需要仔细地关注架构。在当前框架的应用程序中,却很少有对所辖应用是如何运行的进行说明。
延迟的增长
你所经历的延迟增长与规模增长可能是完全不同的。CPU饥饿是该问题的主要原因。
任务优先级被证明是错误的
任务优先级方案可以有效地工作在较小负载下,但是在高负载下就会产生问题。举个典型的例子,有一套简陋的流控装置,一个高优先级任务向一个较低优先级的任务传递工作将会导致工作丢失和内存使用峰值,因为低优先级的任务的运行机会将会很少。
队列容量不足够大
大量的对象意味着可以进行更多的并发操作,这意味着队列容量将很可能需要扩充。
启动时间更久
更多的对象需要更久的时间才能将它们从磁盘加载到应用中。
同步时间更久
需要更多的时间才能将更多的对象在应用之间进行同步。
在大型配置中没有进行足够的测试
因为测试装置成本很高,所以我们实际花费在大型设置上的测试时间非常少。你在开发期间不需要接触大型系统,所以很可能你的设计一开始就不能支持大规模的场景。
操作耗时更久
如果一个操作作用于每个对象,当更多的对象被添加进来后,该操作的耗时也将会更久。当数据表变得更大时,过去针对一定数据量足够快速的查询,如今的耗时也会大幅度的增长。
更多的随机故障
在正常操作中你可能不会看到某些故障。但在一定规模下,响应将丢失,ARP请求将丢失,文件系统可能会出现某些错误,消息可能丢失,回复也可能丢失,等等。
更大的故障窗口
规模化导致每个环节都会耗时更久,这意味着出现故障的概率就会更大。一个数据交换协议在处理少量数据集的时候会很快,这意味着它只有很小的机会遭遇重启或超时。但是更大的规模中,故障窗口将扩大从而第一之间就会遭遇到新问题。
没有提高超时设置
任何超时在较小的数据集中可以有效工作,但是当数据集逐渐增长后将不再适用。就CPU饥饿问题而言,你所编写的代码可能还没来得及跑,超时时间却早已达到。
没有增加重试次数
没有办法在确定故障之前为某应用指定重试次数,因为他们没有这方面足够的信息来支持决策。每秒4次重试是否合理?为什么不是20次呢?
优先级继承
更久的持有大范围的锁将有更好的机会遭遇优先级继承问题。
消费模式的打破
在一种规模下你可以从生产者获取所有数据,但是在另一规模下你将会耗尽队列的空间或内存。举个例子:某个轮询程序在将数据传递到下一个队列之前,会一直向远程队列轮询所有的数据源。当队列中只有很少量数据项的时候该程序可以有效的运行。但是很可能因为某个功能的变更扩大了该远程队列中的数据项数量,这样一来该轮询程序将会导致某个节点内存不足。
监控器超时
100%CPU的情况将导致监控器超时。这在小规模系统中很少发生,但是在设计不良的较大型规模系统中就会发生。
慢速的内存泄露变成快速泄露
较小规模系统中不大引人注意的一个内存泄露问题在较大规模的系统中就变得影响重大。
原本未注意到的锁问题变得引人注目
应该在适当的地方使用锁。但是如果使用不当,该问题在较小规模的系统中也许会被人忽略。因为持有锁的线程会在另一段产生问题的指令运行前释放掉它长期占用的CPU使用权。但是在大规模系统中将会有更多的CPU抢占,这意味着将会有更多的机会看到不同线程对同一数据的并发访问。
死锁的机会变大
不同的调度模式将以不同的路径运行代码,所以遭遇死锁的机会也就更大。举个例子,当CPU使用率很高时文件系统没有机会得到运行,而当以某种方式打破这一情形时,文件系统随即占用了100%的CPU使用权却再也没有运行。
时间同步变糟
时间同步任务的优先级并不高,所以当可用的CPU和网络资源变得更少时,不同节点的时钟将出现偏差。
日志数据丢失
由于日志队列容量过小从而无法应对增长的负载或是因为CPU太忙而没有时间片给予日志记录器分发日志数据,都将可能导致日志记录器开始丢失数据。根据队列的容量和类型不同,或将导致内存不足。
定时器没有在准确的时间触发
一个繁忙的系统无法在期望的时间触发定时器,这将导致系统的其余部分出现一连串的延迟。
ARP数据包丢失
在高负载的CPU或网络环境中,在主机间传送的ARP数据包可能会丢失。这是因为数据包被发送到了错误的网卡,一旦更新完路由表,将不会再发生这种情况。
文件描述符限制
在一个硬件上通常都会有一个固定的文件描述符数量上限。系统设计必须将所需的最大文件描述符数量限制在该上限以内。如果取用的套接字描述符超过了文件描述符的可用池,那么涉及到大量连接(ftp,com,启动,客户端等等)的设计将会产生问题。规模化将可能造成描述符需求数量的峰值。当规模增长时,描述符泄露将会耗尽可用池。
套接字缓冲限制
系统都会为每个套接字分配一定量的缓冲空间,大量的套接字可能会减少系统整体的可用内存。随着规模增长,消息丢失也开始增长。这是因为接收消息的缓冲空间数量不足从而跟不上负载的压力。这同样也和优先级相关,因为一个任务没有足够的优先级从套接字中读取数据。较低优先级的任务可能会被发送者一方某个高优先级任务的消息所淹没。
启动镜像服务限制
一个节点的启动卡同一时间可服务的限制为X。FTP服务器基础设施必须限制启动卡服务的数量,否则将造成该节点发生CPU饥饿。
消息次序混乱
你的消息系统在高负载压力下传递消息的次数可能会发生混乱,这对非幂等的操作来说将产生问题。
协议的弱点
除非小心谨慎的创建应用层协议,否则规模的增长将带来大量的问题。
连接限制
一个某种类型的中央服务器在应付十个客户端的情况下也许绰绰有余。但是当有一千客户端的时候,它将无法满足到响应时间的需求。在这种情况下,平均响应时间将根据客户端数量成线性增长,我们称该复杂度为O(N)(“order N”),但是若是其他更差的复杂度将会产生问题。举个例子,我们希望一个网络中的N个节点可以互相通信,我们可以让每个节点链接到一台中央交换服务器,这将需要O(N)条连接线。或者我们在每两个节点之间直接建立连接,这将需要O(N^2)条连接线(确切的数字或公式通常不重要,这只跟涉及到的N的最高次有关)。
分层架构
这是一个很好的总结,所以我在此处引用了它:基于分层的架构从来就不是用来构建低延迟,高吞吐量应用的。对于多层架构,究其本质是被创建用于解决昨天的历史问题的。从客户端-服务器时代过渡到互联网时代,它是可伸缩性方面最完美的解决方案。
该问题域是关于如何扩展应用的规模以支持成百上千的用户。然而今天的我们都知道对于该问题的解决方案就是n层架构。在可伸缩性的维度上我们选择了通过负载均衡的表现层,事实上这的确解决了问题。然而,当今时代,问题发生了演变。这些日子里,很多行业的问题已不再是仅仅关于提升用户体验了,数据量也成为了一个问题。
多处理器性能问题
当处理器被要求在大量无关工作间切换时,原本强大的硬件缓存加速常常会失效。
查看英文原文:42 Monster Problems That Attack As Loads Increase
解决方案可参看:抵御负载怪兽攻击,确保可伸缩性的7条秘诀
感谢杨赛对本文的审校。