除了高性能和高可用之外,架构设计还面临三个重要的复杂度来源:低成本、安全和规模。这三者各有其独特的挑战和应对策略。理解它们的本质,有助于架构师在设计中做出更明智的决策。
当架构方案只涉及几台或十几台服务器时,成本通常不是重点关注目标。但如果架构方案涉及几百上千甚至上万台服务器,成本就会变成一个非常重要的考虑点。
例如:A 方案需要 10000 台机器,B 方案只需要 8000 台机器,单从比例来看节省了 20%,但从数量来看,B 方案能节省 2000 台机器。按照每台机器每年约 2 万元成本计算,一年就能节省 4000 万元——这笔钱足以给 100 人的团队每人发 40 万元奖金。
低成本的本质:设计"高性能"和"高可用"的架构时,通用手段都是增加更多服务器;而低成本正好与此相反,需要减少服务器数量才能达成。因此,低成本本质上是与高性能和高可用冲突的。
基于这个原因,低成本很多时候不是架构设计的首要目标,而是附加约束。通常的流程是:先根据高性能、高可用的要求设计架构,然后评估是否满足成本目标,如果不行则重新设计。
低成本给架构设计带来的主要复杂度体现在:往往只有"创新"才能达到低成本目标。这里的"创新"包括两种形式:
引入新技术:
创造新技术:
引入新技术的主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合起来。创造新技术则复杂度更高,需要自己去创造全新的理念和技术,且与旧技术相比需要有质的飞跃。
安全本身是一个庞大而又复杂的技术领域,一旦出问题,对业务和企业形象影响非常大。
从技术角度来讲,安全可以分为两类:
1. 功能安全("防小偷")
常见的功能安全攻击包括 XSS 攻击、CSRF 攻击、SQL 注入、密码破解等。这些攻击的本质是因为系统实现有漏洞,黑客利用漏洞潜入系统进行破坏或盗取。
功能安全更多是和具体的编码相关,与架构关系不大。虽然现在很多开发框架都内嵌了常见的安全功能,能够减少安全相关功能的重复开发,但框架只能预防常见的安全漏洞,无法预知新的安全问题,而且框架本身也可能存在漏洞(如 Apache Struts2 多次爆出的远程代码执行高危漏洞)。
功能安全是一个"攻"与"防"的矛盾,只能在这种攻防大战中逐步完善,不可能在系统架构设计时一劳永逸地解决。
2. 架构安全("防强盗")
如果说功能安全是"防小偷",那么架构安全就是"防强盗"。强盗会直接用大锤将门砸开,或者用炸药将围墙炸倒——很多时候就是故意搞破坏,对系统的影响要大得多。
传统的架构安全主要依靠防火墙。防火墙通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。
但在互联网领域,防火墙的应用场景并不多。原因在于:
基于上述原因,互联网系统的架构安全更多依靠运营商或云服务商强大的带宽和流量清洗能力,较少自己来设计和实现。
规模带来复杂度的主要原因是"量变引起质变",当数量超过一定的阈值后,复杂度会发生质的变化。
1. 功能复杂度:指数级增长
以一个简单的抽象模型计算:假设系统间的功能都是两两相关的,系统的复杂度 = 功能数量 + 功能之间的连接数量。
连接数量的计算公式:n × (n-1) / 2
3 个功能的系统复杂度 = 3 + 3 = 6 8 个功能的系统复杂度 = 8 + 28 = 36
复杂度不是线性增长,而是呈指数级增长,主要原因在于随着系统功能数量增多,功能之间的连接呈指数级增长。
2. 数据复杂度:质变临界点
即使数据没有达到大数据规模,增长也可能给系统带来复杂性。以 MySQL 为例,单表数据量一般在 5000 万行左右。如果单表数据达到 10 亿行,会产生很多问题:
当 MySQL 单表数据量太大时,必须考虑将单表拆分为多表,这个拆分过程也会引入更多复杂性。
下面通过 Python 示例演示功能数量与复杂度的关系:
python"""
功能数量与复杂度关系示例
用于演示 n 个功能之间的连接数如何呈指数级增长
复杂度公式:complexity = n + n*(n-1)/2 = n*(n+1)/2
- n: 功能数量
- n*(n-1)/2: 功能间的两两连接数
"""
def calculate_complexity(n: int) -> tuple[int, int, int]:
"""
计算 n 个功能的复杂度
Returns:
(功能数, 连接数, 总复杂度)
"""
connections = n * (n - 1) // 2
total = n + connections
return n, connections, total
def print_complexity_table():
"""打印复杂度表格"""
print("功能数量 | 连接数 | 总复杂度 | 增长趋势")
print("-" * 50)
prev_total = 0
for n in [3, 5, 8, 10, 15, 20, 30, 50]:
n_count, connections, total = calculate_complexity(n)
growth = f"+{total - prev_total}" if prev_total > 0 else ""
print(f"{n_count:^8} | {connections:^6} | {total:^7} | {growth}")
prev_total = total
print("\n结论:")
print("- 功能数从 3 增长到 50(增长约 17 倍)")
print("- 连接数从 3 增长到 1225(增长约 408 倍)")
print("- 总复杂度从 6 增长到 1275(增长约 213 倍)")
print("- 连接数的增长速度远超功能数,这就是'量变引起质变'")
def demo_connection_growth():
"""演示连接数的指数级增长"""
print("\n=== 连接数增长演示 ===")
# 计算不同功能数量下的连接数
test_sizes = [5, 10, 20, 50, 100]
print("\n功能数 | 连接数 | 连接数/功能数比值")
print("-" * 40)
for size in test_sizes:
connections = size * (size - 1) // 2
ratio = connections / size
print(f"{size:^6} | {connections:^6} | {ratio:^10.1f}")
if __name__ == "__main__":
print_complexity_table()
demo_connection_growth()
# 预期输出:
# 功能数量 | 连接数 | 总复杂度 | 增长趋势
# -------------------------------------------------
# 3 | 3 | 6 |
# 5 | 10 | 15 | +9
# 8 | 28 | 36 | +21
# 10 | 45 | 55 | +19
# 15 | 105 | 120 | +65
# 20 | 190 | 210 | +90
# 30 | 435 | 465 | +255
# 50 | 1225 | 1275 | +810
#
# 结论:
# - 功能数从 3 增长到 50(增长约 17 倍)
# - 连接数从 3 增长到 1225(增长约 408 倍)
# - 总复杂度从 6 增长到 1275(增长约 213 倍)
# - 连接数的增长速度远超功能数,这就是'量变引起质变'
运行结果:
功能数量 | 连接数 | 总复杂度 | 增长趋势 ------------------------------------------------- 3 | 3 | 6 | 5 | 10 | 15 | +9 8 | 28 | 36 | +21 10 | 45 | 55 | +19 15 | 105 | 120 | +65 20 | 190 | 210 | +90 30 | 435 | 465 | +255 50 | 1225 | 1275 | +810 结论: - 功能数从 3 增长到 50(增长约 17 倍) - 连接数从 3 增长到 1225(增长约 408 倍) - 总复杂度从 6 增长到 1275(增长约 213 倍) - 连接数的增长速度远超功能数,这就是'量变引起质变' === 连接数增长演示 === 功能数 | 连接数 | 连接数/功能数比值 ---------------------------------------- 5 | 10 | 2.0 10 | 45 | 4.5 20 | 190 | 9.5 50 | 1225 | 24.5 100 | 4950 | 49.5
这个示例清晰地展示了功能数量增长带来的复杂度爆炸:当功能数从 10 增长到 100(10 倍),连接数却从 45 增长到 4950(110 倍)。这就是架构设计中"量变引起质变"的典型现象。