人类对技术性能的不懈追求促进了技术的持续发展。以计算机技术为例,我们见证了从电子管计算机到晶体管计算机,再到集成电路计算机的进化。这一进程中,计算性能经历了从每秒几次运算到每秒几亿次运算的飞跃。随着性能的提升,计算机的系统和方法复杂度也随之增加。现代CPU集成的晶体管数量达到几亿,无论是逻辑还是制造复杂度,都远超早期的晶体管计算机。
软件系统的发展轨迹与此相似。过去几十年间,软件系统的性能得到了巨大提升,例如从最初的计算机仅能执行基本科学计算,到现在谷歌能够每秒处理数万次搜索。软件系统的规模也从单机发展到了涵盖成千上万台计算机的大型系统;从单用户单任务的字符界面Dos操作系统,发展到今日支持多用户多任务的图形界面Windows 10操作系统。
然而,技术进步带来的性能提升并不总意味着系统复杂度的增加。以存储技术的发展为例,从纸带到磁带、磁盘再到SSD,这一进程并没有导致系统复杂度显著上升。新技术的出现通常会淘汰旧技术,因此只需采纳新技术即可,而无需担心系统复杂度的提升。但对于那些开辟新领域而非替代旧技术的进步,确实会增加软件系统的复杂度。这是因为,在设计软件系统时,需要在这些新技术之间做出选择或组合,正如出行时需在汽车、火车和飞机之间权衡选择一样,考虑价格、时间、速度和舒适度等多种因素,选择过程相对复杂。
高性能对软件系统复杂度的影响主要从两个方面展开:一是提升单台计算机性能带来的内部复杂度;二是为实现多台计算机集群的高性能而导致的复杂度。
单机复杂度
计算机的性能提升,根本上受到硬件进步的驱动,特别是CPU性能的快速发展。根据“摩尔定律”,CPU处理能力大约每18个月翻一番。然而,要充分利用这些硬件性能,操作系统的作用至关重要。随着硬件的演进,操作系统也相应地发展,因为它为软件系统提供运行环境。从这个角度来看,操作系统的复杂性直接影响到软件系统的复杂性。
操作系统中,与性能密切相关的概念包括进程和线程。早期计算机缺乏操作系统,仅具备输入、计算和输出的基本功能。用户输入指令后,计算机执行操作,但大部分时间处于等待用户输入的状态,显然效率低下,因为人的输入速度远不及计算机的处理速度。
为提高效率,批处理操作系统随之诞生。简而言之,批处理涉及将指令预先编写好,形成任务清单,然后由计算机执行这些“任务”。通过自动读取和处理任务清单,避免了手动输入带来的延迟,显著提升了性能。
尽管批处理系统提高了效率,但它存在一个缺陷:计算机一次只能处理一个任务。如果任务需从I/O设备读取大量数据,CPU在I/O操作期间处于空闲状态,本可以用来处理其他计算任务。
为进一步优化性能,人们引入了“进程”概念,每个任务通过进程隔离,拥有独立的内存空间,互不干扰,由操作系统调度。尽管当时的CPU尚未实现多核和多线程,通过时间分片实现类似并行的效果,从用户角度看似乎多个进程同时进行。
多进程使得任务能够似乎同时进行,但这并不意味着内部子任务的并行执行。例如,在一个餐馆管理进程中,若排位、点菜、买单等子任务不能同时进行,则会极大降低效率。因此,线程应运而生,允许进程内部子任务并行执行,共享相同的进程数据。为保证数据一致性,引入了互斥锁机制。
虽然多进程多线程提升了任务处理能力,但仍是基于分时系统,无法实现真正的并行。真正的多任务并行要求多个CPU同时执行计算任务。目前解决此问题的方案包括对称多处理器结构(SMP)、非一致存储访问结构(NUMA)和海量并行处理结构(MPP),其中SMP是最常见的方案。
现代操作系统的发展,如果要构建一个高性能软件系统,就需要考虑多进程、多线程、进程间通信等技术点。这些技术选择并非越新越好,也不是非黑即白的决策。架构设计时需要根据业务需求进行综合分析和选择,这个过程相当复杂。例如,Nginx既可以采用多进程也可以采用多线程,JBoss采用多线程,而Redis采用单进程,Memcache采用多线程,尽管这些系统性能高效,但其内部实现差异显著。
集群的复杂度
随着互联网时代的到来,业务发展速度的确远远超过了硬件性能的提升速度,尤其是对于大规模、高并发的业务系统,如支付宝和微信,它们需要处理海量的数据和交易,单台机器的性能已远远不能满足需求。因此,采用机器集群成为了实现高性能系统的必由之路。但是,仅仅增加机器数量,并不能直接解决问题。要让这些机器协同工作,实现整个系统的高性能,需要采用一些特定的技术和策略。以下是几种常见的高性能实现方式:
1.任务分配
任务分配的意思是指每台机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。
我从最简单的一台服务器变两台服务器开始,来讲任务分配带来的复杂性,整体架构示意图如下:

需要增加一个任务分配器,这个分配器可能是硬件网络设备(例如,F5、交换机等),可能是软件网络设备(例如,LVS),也可能是负载均衡软件(例如,Nginx、HAProxy),还可能是自己开发的系统。选择合适的任务分配器也是一件复杂的事情,需要综合考虑性能、成本、可维护性、可用性等各方面的因素。
任务分配器和真正的业务服务器之间有连接和交互(即图中任务分配器到业务服务器的连接线),需要选择合适的连接方式,并且对连接进行管理。例如,连接建立、连接检测、连接中断后如何处理等。任务分配器需要增加分配算法。例如,是采用轮询算法,还是按权重分配,又或者按照负载进行分配。如果按照服务器的负载进行分配,则业务服务器还要能够上报自己的状态给任务分配器。
2.任务分解
当我们面对单台机器性能的局限性时,通过增加更多的机器来扩展系统性能确实是一个有效的策略。这种通过任务分配来实现的扩展方式,能够在业务相对简单时显著提高性能。然而,随着业务逻辑的增加和复杂化,单纯依靠增加机器的方式来提升性能的效率会逐渐降低。例如,当业务较为简单时,将服务从一台机器扩展到十台机器,性能可能提升8倍,但随着业务复杂度的增加,相同的扩展可能只能带来5倍的性能提升。这是因为,随着业务复杂度的上升,每台机器上的处理效率实际上在下降。
为了应对这一挑战,我们需要采取另一种策略:任务分解。这意味着将复杂的业务拆分成多个更小、更容易管理的部分,以此来提高整体系统的处理能力和效率。以微信的后台架构为例,我们可以如下理解:

通过上面的架构示意图可以看出,微信后台架构从逻辑上将各个子业务进行了拆分,包括:接入、注册登录、消息、LBS、摇一摇、漂流瓶、其他业务(聊天、视频、朋友圈等)。通过这种任务分解的方式,能够把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统。从业务的角度来看,任务分解既不会减少功能,也不会减少代码量(事实上代码量可能还会增加,因为从代码内部调用改为通过服务器之间的接口调用),那为何通过任务分解就能够提升性能呢?
简单的系统更加容易做到高性能
在系统功能较为简单时,影响性能的因素较少,这使得进行针对性优化变得更为直接和高效。反之,对于复杂的系统,确定并优化关键性能瓶颈变得更加困难。这是因为在复杂系统中,需要考虑的性能影响点众多,难以快速精确地识别出哪些因素最关键。即便在投入大量资源对某个性能瓶颈进行优化后,也可能因为系统内部复杂的相互依赖关系,导致优化一个部分而不小心降低了另一个部分的性能。这样,整体系统的性能不仅可能未得到提升,有时还可能出现下降的情况。
可以针对单个任务进行扩展
当系统被拆分成多个独立的子系统以后,识别并针对特定子系统的性能瓶颈变得更为直接和简单。这样,一旦某个子系统出现性能问题,我们只需针对该子系统进行优化或增加资源,无需对整个系统进行修改,从而降低了风险。以微信的后台为例,假设由于用户增长过快,导致注册和登录子系统性能出现瓶颈,这时只需对该子系统进行优化(例如,通过代码优化或增加服务器)而不需要调整消息处理、LBS逻辑等其他子系统。

然而,将系统细分为多个子系统是否意味着越细越好呢?例如,将微信后台的7个逻辑子系统进一步细分为100个子系统,这是否能进一步提高性能?实际上,这种做法并不会提高性能,反而可能导致性能下降。主要原因是,如果系统被拆分得过细,完成一项业务所需的系统间调用次数会显著增加。由于系统间调用通常通过网络进行,其性能远低于系统内部的函数调用。简单模拟一下:当系统由2个子系统组成时,处理一次用户请求可能需要1次系统间调用和1次响应;当系统拆分为4个子系统时,系统间的调用次数增加到3次;若拆分为100个子系统,为完成同一次用户访问,系统间的调用次数会激增至99次。
假设理想情况下网络请求响应时间为1ms,而业务处理本身需要50ms。如果系统仅拆分为2个子系统,处理一次访问的总耗时为51ms;而拆分为100个子系统时,总耗时升至149ms。尽管系统拆分在一定程度上可以提升处理性能,但这种提升是有限的。最终,业务处理性能的上限主要由业务逻辑本身决定,而不是系统拆分的粒度。因此,系统拆分带来的性能提升存在一个度,过细的任务分解并不总是更好。对于架构设计而言,找到恰当的拆分粒度是关键。
. 任
原文作者:二进制跳动;如有侵权,请联系管理员删除。