对性能孜孜不倦的追求是整个人类技术不断发展的根本驱动力。例如计算机,从电子管计算机到晶体管计算机再到集成电路计算机,运算性能从每秒几次提升到每秒几亿次。但伴随性能越来越高,相应的方法和系统复杂度也越来越高。
为什么高性能会成为架构复杂度的来源?因为当系统性能需求超过单台计算机的处理能力时,我们必须通过集群化手段来扩展性能。而集群化不仅仅是"增加机器"这么简单——它带来了任务分配、任务分解、调用链指数级增长等一系列复杂性。
计算机内部复杂度最关键的地方就是操作系统。计算机性能的发展本质上是由硬件发展驱动的,尤其是 CPU 的性能发展。著名的"摩尔定律"表明了 CPU 的处理能力每隔 18 个月就翻一番;而将硬件性能充分发挥出来的关键就是操作系统。
最早的计算机其实是没有操作系统的,用户输入一个指令,计算机完成操作,然后等待下一个指令。这种手工操作方式效率很低,因为人的输入速度远远比不上计算机的运算速度。
批处理操作系统应运而生——先把要执行的指令预先写下来(写到纸带、磁带、磁盘等),形成一个指令清单,然后交给计算机去执行。这样计算机执行的过程中无须等待人工手工操作,性能有了很大的提升。
批处理程序大大提升了处理性能,但有一个明显的缺点:计算机一次只能执行一个任务。如果某个任务需要从 I/O 设备读取大量数据,在 I/O 操作的过程中,CPU 其实是空闲的。
为了进一步提升性能,人们发明了"进程",用进程来对应一个任务,每个任务都有自己独立的内存空间,进程间互不相关,由操作系统来进行调度。虽然从 CPU 的角度来说还是串行处理的,但由于 CPU 的处理速度很快,从用户的角度来看,感觉是多进程在并行处理。
多进程让多任务能够并行处理,但单个进程内部只能串行处理。实际上很多进程内部的子任务也需要并行处理。例如,一个餐馆管理进程,排位、点菜、买单、服务员调度等子任务必须能够并行处理,否则某个客人买单时间比较长,其他客人都不能点菜。
为了解决这个问题,人们又发明了线程。线程是进程内部的子任务,共享同一份进程数据。为了保证数据的正确性,又发明了互斥锁机制。有了多线程后,操作系统调度的最小单位就变成了线程,而进程变成了操作系统分配资源的最小单位。
多进程多线程虽然让多任务并行处理的性能大大提升,但本质上还是分时系统,并不能做到时间上真正的并行。解决这个问题的方案就是让多个 CPU 能够同时执行计算任务。目前有 3 种方案:
操作系统发展到现在,如果我们要完成一个高性能的软件系统,需要考虑多进程、多线程、进程间通信、多线程并发等技术点,而且这些技术并不是最新的就是最好的,也不是非此即彼的选择。在做架构设计的时候,需要花费很大的精力来结合业务进行分析、判断、选择、组合。
举例来说:Nginx 可以用多进程也可以用多线程,JBoss 采用的是多线程;Redis 采用的是单进程,Memcache 采用的是多线程。这些系统都实现了高性能,但内部实现差异却很大。
虽然计算机硬件的性能快速发展,但和业务的发展速度相比,还是小巫见大巫了。2016 年"双 11"支付宝每秒峰值达 12 万笔支付;2017 年春节微信红包收发红包每秒达到 76 万个。要支持这种复杂的业务,单机的性能无论如何是无法支撑的,必须采用机器集群。
任务分配的意思是指每台机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。
从 1 台服务器变为 2 台服务器后,架构明显复杂多了:
需要增加任务分配器:可能是硬件网络设备(F5、交换机)、软件网络设备(LVS)、负载均衡软件(Nginx、HAProxy),甚至是自己开发的系统。选择合适的任务分配器需要综合考虑性能、成本、可维护性、可用性等各方面因素。
任务分配器和业务服务器之间需要连接和交互管理:连接建立、连接检测、连接中断后如何处理等。
任务分配器需要增加分配算法:轮询、按权重分配、按负载分配等。如果按照服务器的负载进行分配,则业务服务器还要能够上报自己的状态给任务分配器。
当性能要求继续提高到每秒 10 万次时,任务分配器本身也会成为瓶颈,需要扩展为多台。此时架构会更加复杂:
通过任务分配的方式,我们能够突破单台机器处理性能的瓶颈,但如果业务本身也越来越复杂,单纯只通过任务分配的方式来扩展性能,收益会越来越低。
为了能够继续提升性能,我们需要采取任务分解的方式。以微信的后台架构为例,从逻辑上将各个子业务进行了拆分,包括:接入、注册登录、消息、LBS、摇一摇、漂流瓶、其他业务等。
任务分解能够提升性能的主要原因:
简单的系统更加容易做到高性能:系统的功能越简单,影响性能的点就越少,就更加容易进行有针对性的优化。
可以针对单个任务进行扩展:当各个逻辑任务分解到独立的子系统后,性能瓶颈更加容易发现,发现后只需要针对有瓶颈的子系统进行优化,不需要改动整个系统。
系统拆分越细,系统间的调用次数会呈指数级别上升。假设系统采用 IP 网络连接,理想情况下一次请求和响应在网络上耗费为 1ms,业务处理本身耗时为 50ms:
系统拆分可能在某种程度上能提升业务处理性能,但提升性能也是有限的。系统拆分对单个业务请求性能没有本质性提升,因为最终决定业务处理性能的还是业务逻辑本身。
下面用一个 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 调用链呈线性增长,但累积效应使得性能反而下降 ============================================================
这个示例清楚地展示了任务分解带来的开销问题。虽然任务分解可以让每个服务更加简单、更加容易扩展,但服务间的调用延迟会累积,导致整体性能反而下降。