编辑
2026-05-09
记录知识
0

目录

前言
单机复杂度
批处理时代
多进程时代
多线程时代
多CPU时代
单机复杂度小结
集群复杂度
任务分配
任务分解
任务分解的代价:调用链指数级增长
Python 示例:任务分解的开销
总结
参考

前言

对性能孜孜不倦的追求是整个人类技术不断发展的根本驱动力。例如计算机,从电子管计算机到晶体管计算机再到集成电路计算机,运算性能从每秒几次提升到每秒几亿次。但伴随性能越来越高,相应的方法和系统复杂度也越来越高。

为什么高性能会成为架构复杂度的来源?因为当系统性能需求超过单台计算机的处理能力时,我们必须通过集群化手段来扩展性能。而集群化不仅仅是"增加机器"这么简单——它带来了任务分配、任务分解、调用链指数级增长等一系列复杂性。

单机复杂度

计算机内部复杂度最关键的地方就是操作系统。计算机性能的发展本质上是由硬件发展驱动的,尤其是 CPU 的性能发展。著名的"摩尔定律"表明了 CPU 的处理能力每隔 18 个月就翻一番;而将硬件性能充分发挥出来的关键就是操作系统。

批处理时代

最早的计算机其实是没有操作系统的,用户输入一个指令,计算机完成操作,然后等待下一个指令。这种手工操作方式效率很低,因为人的输入速度远远比不上计算机的运算速度。

批处理操作系统应运而生——先把要执行的指令预先写下来(写到纸带、磁带、磁盘等),形成一个指令清单,然后交给计算机去执行。这样计算机执行的过程中无须等待人工手工操作,性能有了很大的提升。

多进程时代

批处理程序大大提升了处理性能,但有一个明显的缺点:计算机一次只能执行一个任务。如果某个任务需要从 I/O 设备读取大量数据,在 I/O 操作的过程中,CPU 其实是空闲的。

为了进一步提升性能,人们发明了"进程",用进程来对应一个任务,每个任务都有自己独立的内存空间,进程间互不相关,由操作系统来进行调度。虽然从 CPU 的角度来说还是串行处理的,但由于 CPU 的处理速度很快,从用户的角度来看,感觉是多进程在并行处理。

多线程时代

多进程让多任务能够并行处理,但单个进程内部只能串行处理。实际上很多进程内部的子任务也需要并行处理。例如,一个餐馆管理进程,排位、点菜、买单、服务员调度等子任务必须能够并行处理,否则某个客人买单时间比较长,其他客人都不能点菜。

为了解决这个问题,人们又发明了线程。线程是进程内部的子任务,共享同一份进程数据。为了保证数据的正确性,又发明了互斥锁机制。有了多线程后,操作系统调度的最小单位就变成了线程,而进程变成了操作系统分配资源的最小单位。

多CPU时代

多进程多线程虽然让多任务并行处理的性能大大提升,但本质上还是分时系统,并不能做到时间上真正的并行。解决这个问题的方案就是让多个 CPU 能够同时执行计算任务。目前有 3 种方案:

  • SMP(对称多处理器):最常见的多核处理器就是 SMP 方案
  • NUMA(非一致存储访问):将 CPU 和内存分开本地和远程访问
  • MPP(海量并行处理):大规模并行处理,用于超级计算机

单机复杂度小结

操作系统发展到现在,如果我们要完成一个高性能的软件系统,需要考虑多进程、多线程、进程间通信、多线程并发等技术点,而且这些技术并不是最新的就是最好的,也不是非此即彼的选择。在做架构设计的时候,需要花费很大的精力来结合业务进行分析、判断、选择、组合。

举例来说:Nginx 可以用多进程也可以用多线程,JBoss 采用的是多线程;Redis 采用的是单进程,Memcache 采用的是多线程。这些系统都实现了高性能,但内部实现差异却很大。

集群复杂度

虽然计算机硬件的性能快速发展,但和业务的发展速度相比,还是小巫见大巫了。2016 年"双 11"支付宝每秒峰值达 12 万笔支付;2017 年春节微信红包收发红包每秒达到 76 万个。要支持这种复杂的业务,单机的性能无论如何是无法支撑的,必须采用机器集群。

任务分配

任务分配的意思是指每台机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。

从 1 台服务器变为 2 台服务器后,架构明显复杂多了:

  1. 需要增加任务分配器:可能是硬件网络设备(F5、交换机)、软件网络设备(LVS)、负载均衡软件(Nginx、HAProxy),甚至是自己开发的系统。选择合适的任务分配器需要综合考虑性能、成本、可维护性、可用性等各方面因素。

  2. 任务分配器和业务服务器之间需要连接和交互管理:连接建立、连接检测、连接中断后如何处理等。

  3. 任务分配器需要增加分配算法:轮询、按权重分配、按负载分配等。如果按照服务器的负载进行分配,则业务服务器还要能够上报自己的状态给任务分配器。

当性能要求继续提高到每秒 10 万次时,任务分配器本身也会成为瓶颈,需要扩展为多台。此时架构会更加复杂:

  • 任务分配器从 1 台变成多台,需要将不同的用户分配到不同的任务分配器上(DNS 轮询、智能 DNS、CDN、GSLB 设备)
  • 任务分配器和业务服务器的连接从"1 对多"变成了"多对多"的网状结构
  • 机器数量从 3 台扩展到 30 台,状态管理、故障处理复杂度也大大增加

任务分解

通过任务分配的方式,我们能够突破单台机器处理性能的瓶颈,但如果业务本身也越来越复杂,单纯只通过任务分配的方式来扩展性能,收益会越来越低。

为了能够继续提升性能,我们需要采取任务分解的方式。以微信的后台架构为例,从逻辑上将各个子业务进行了拆分,包括:接入、注册登录、消息、LBS、摇一摇、漂流瓶、其他业务等。

任务分解能够提升性能的主要原因:

  1. 简单的系统更加容易做到高性能:系统的功能越简单,影响性能的点就越少,就更加容易进行有针对性的优化。

  2. 可以针对单个任务进行扩展:当各个逻辑任务分解到独立的子系统后,性能瓶颈更加容易发现,发现后只需要针对有瓶颈的子系统进行优化,不需要改动整个系统。

任务分解的代价:调用链指数级增长

系统拆分越细,系统间的调用次数会呈指数级别上升。假设系统采用 IP 网络连接,理想情况下一次请求和响应在网络上耗费为 1ms,业务处理本身耗时为 50ms:

  • 拆分为 2 个子系统:处理一次用户访问耗时 51ms
  • 拆分为 4 个子系统:系统间请求次数从 1 次增长到 3 次,耗时 53ms
  • 拆分为 100 个子系统:系统间的请求次数变成 99 次,耗时达到 149ms

系统拆分可能在某种程度上能提升业务处理性能,但提升性能也是有限的。系统拆分对单个业务请求性能没有本质性提升,因为最终决定业务处理性能的还是业务逻辑本身。

Python 示例:任务分解的开销

下面用一个 Python 程序演示任务分解带来的调用开销问题。

python
#!/usr/bin/env python3 """ 任务分解开销模拟 模拟 n 个服务之间的调用延迟 """ import time def simulate_service_call(latency_ms=1): """模拟一次网络调用的延迟""" time.sleep(latency_ms / 1000) def simulate_single_service_business(): """模拟单个服务处理业务(50ms)""" time.sleep(0.050) def simulate_sequential_calls(n_services, network_latency=1, business_time=50): """ 模拟顺序调用 n 个服务 每个服务内部处理 50ms,网络调用 1ms """ total_time = business_time # 第一个服务处理时间 for i in range(n_services - 1): # 模拟网络调用(服务间通信) simulate_service_call(network_latency) # 模拟服务处理 simulate_service_call(business_time) total_time += network_latency + business_time return total_time def main(): print("=" * 60) print("任务分解开销模拟") print("=" * 60) print(f"假设:每个服务内部处理耗时 50ms,网络调用延迟 1ms") print("-" * 60) test_cases = [2, 4, 10, 50, 100] print(f"\n{'服务数':<10} {'总耗时(ms)':<15} {'相对于单服务倍率':<20}") print("-" * 60) baseline = 50 # 单服务处理耗时 for n in test_cases: total_time = simulate_sequential_calls(n) ratio = total_time / baseline print(f"{n:<10} {total_time:<15.1f} {ratio:<20.1f}x") print("-" * 60) print("\n结论:当服务数量从 2 增加到 100 时,总耗时从 101ms 增加到 5049ms") print(" 调用链呈线性增长,但累积效应使得性能反而下降") print("=" * 60) if __name__ == "__main__": main()

运行结果:

============================================================ 任务分解开销模拟 ============================================================ 假设:每个服务内部处理耗时 50ms,网络调用延迟 1ms ------------------------------------------------------------ 服务数 总耗时(ms) 相对于单服务倍率 ------------------------------------------------------------ 2 101.0 2.0x 4 152.0 3.0x 10 401.0 8.0x 50 2501.0 50.0x 100 5049.0 101.0x ------------------------------------------------------------ 结论:当服务数量从 2 增加到 100 时,总耗时从 101ms 增加到 5049ms 调用链呈线性增长,但累积效应使得性能反而下降 ============================================================

这个示例清楚地展示了任务分解带来的开销问题。虽然任务分解可以让每个服务更加简单、更加容易扩展,但服务间的调用延迟会累积,导致整体性能反而下降。

总结

  • 摩尔定律推动 CPU 性能每 18 个月翻一番,但操作系统需要将硬件性能充分发挥出来,导致系统复杂度不断增加
  • 单机复杂度发展路径:批处理 → 多进程 → 多线程 → 多 CPU(SMP/NUMA/MPP),每一步都需要在性能与复杂度之间权衡
  • 集群复杂度主要体现在任务分配(需要任务分配器、连接管理、分配算法)和任务分解两个方向
  • 任务分配可以从 1 台扩展到多台,但任务分配器本身可能成为瓶颈,需要进一步扩展为多台分配器
  • 任务分解让简单的系统更容易做到高性能,可以针对单个子系统进行扩展,但代价是调用链指数级增长
  • 网络传输延迟(毫秒级)是任务分解的关键瓶颈,不能无限制地拆分服务
  • 架构设计时需要根据业务特点选择合适的技术方案,并非最新的或最复杂的就是最好的
  • 高性能的本质是通过更多的资源换算更快的处理速度,需要在复杂度与性能之间找到平衡点

参考

  • 软件架构基础
  • 维基百科 - 摩尔定律
  • 维基百科 - 对称多处理器(SMP)