软件架构设计(第二篇)

软件架构六大复杂度来源

结合当下云原生、微服务、分布式最新实践,通俗易懂解读,附带案例、原理、示意图说明,适合架构入门、技术评审、学习笔记。

前言

架构设计本质目的:解决软件系统复杂度带来的问题 。

软件的复杂度主要来自 6 个维度:高性能、高可用、可扩展性、低成本、安全、规模 。很多新手做架构容易陷入误区:一味追求新技术、追求高大上的微服务、分布式,但忽略复杂度本身。所有架构方案都要为复杂度做取舍,没有银弹,只有合适的方案。

一、复杂度来源 1:高性能

追求性能是技术发展底层驱动力,性能越高,实现手段的复杂度就越高。高性能带来的复杂度分为两大块:单机内部高性能 、多机集群高性能 。

1.1 单机内部高性能的复杂度

单机高性能本质由硬件 CPU 驱动,操作系统把硬件能力释放给上层应用。发展历史:

  1. 无操作系统:人工输入指令,CPU 大量等待人工输入,效率极低。
  2. 批处理系统:预录入一批任务,机器自动执行;缺点:I/O 读写的时候 CPU 空闲浪费。
  3. 进程 :一个进程代表一个任务,CPU 分时复用,宏观上实现 "并行";进程拥有独立内存,进程之间隔离;为了进程之间互通,衍生管道、消息队列、信号量、共享内存等进程间通信手段。
  4. 线程 :一个进程内部多个线程,进程是资源分配最小单元,线程是 CPU 调度最小单元 ,线程共享进程内存;为保证多线程数据不出错,引入互斥锁、并发控制,带来锁竞争、死锁等一系列复杂度。
  5. 多核 CPU:SMP 对称多处理器(普通多核 CPU)、NUMA、MPP 三种架构,实现真正物理并行。

现实案例:同样实现高性能,选型差异巨大

  • Nginx:多进程模型
  • JBoss:多线程模型
  • Redis:单进程单线程(避免多线程锁的复杂度,靠单线程 + IO 多路复用,一台机器部署多个实例利用多核)
  • Memcache:多线程模型

现代补充:云原生时代,协程(Go goroutine)进一步降低切换开销,但同样带来协程调度、异常处理、泄露等新复杂度。

1.2 集群高性能带来的复杂度

单机硬件有物理天花板,互联网业务流量远超单机上限,需要集群。集群提升性能两种手段:任务分配、任务分解 。

(1)任务分配(水平副本)

通俗理解:同样一份业务程序,复制多份实例,请求分发到不同机器处理。示意图:单服务器 → 增加任务分配器(负载均衡),后端多台业务服务器。

带来复杂度:

  1. 引入任务分配器(Nginx、LVS、F5、HAProxy、DNS 轮询、GSLB);分配器本身会成为新瓶颈,分配器也要集群化。
  2. 连接管理:连接建立、心跳探测、故障断开、重试逻辑。
  3. 分配算法:轮询、权重、负载感知;负载感知还需要业务服务器上报自身状态。

现代对应:K8s Deployment 多副本 + Service 负载均衡,就是典型任务分配模式。注意:理论性能线性提升,但实际会有网络、同步损耗,通常只能达到理论 80% 左右。

(2)任务分解(业务拆分)

通俗理解:把一个大而全的单体系统,按业务功能拆成多个不同子系统,每个子系统独立部署。示例:微信后台,拆分为接入服务器、注册登录、消息、LBS、摇一摇、漂流瓶等独立子服务。

任务分解为什么可以提升性能:

  1. 系统功能简单后,更容易定位性能瓶颈、针对性优化;单体中一个模块慢查询会拖累整个系统。
  2. 可以针对性扩容 :哪个子系统压力大,就只扩容哪个子系统,不需要整体扩容。

⚠️ 重要陷阱:不是拆分越细越好! 拆分过细,原本进程内函数调用,变成跨机器网络调用,网络 RTT 开销急剧上涨,调用链变长,整体性能反而下降。

书中举例:业务处理耗时 50ms,每次网络交互 1ms;拆成 2 个子系统总耗时 51ms;拆成 100 个子系统串行调用总耗时 149ms。现实痛点:很多公司过度微服务,拆分几十个服务,一次业务跨十几、二十次网络调用,超时、链路追踪、事务一致性全部变成难题。

现代微服务本质就是任务分解,粒度把控是架构师核心能力,拆分收益和网络损耗做权衡 。

高性能小结

  • 单机:多进程 / 多线程 / 协程、并发锁、IO 模型;
  • 集群:任务分配(多副本负载均衡)、任务分解(业务拆分);
  • 核心权衡:性能收益 vs 新增复杂度(锁、网络、一致性)。

二、复杂度来源 2:高可用

高可用定义:系统无中断地执行功能的能力 。硬件会坏、软件有 bug、还会遇到断电、光缆挖断等灾难。✨高可用核心手段:冗余 。注意区分:

  • 高性能加机器:目标是扩展处理能力 ;
  • 高可用加机器:目标是冗余备份,防止单点故障 。

2.1 计算高可用(业务逻辑服务)

计算服务是无状态,同一输入任意实例输出结果一致。架构:任务分配器 + 多台计算服务器。

分配策略选择:1 主 3 备、2 主 2 备、全主 0 备等。例如 Zookeeper 是 1 主多备;Memcached 全主 0 备。复杂度点:分配器本身的高可用、连接健康检测、故障摘除、切换逻辑。

2.2 存储高可用(最大难点)

存储和计算本质不同:数据要在多节点之间传输复制,网络存在延迟、丢包、中断。

举例:北京机房存钱 1 万元,网络延迟,上海机房还没同步;用户查询路由到上海,余额看不到新增 1 万元,带来业务故障。

存储高可用难点不是备份数据,而是规避数据不一致带来业务伤害 。理论依据:CAP 定理,分布式存储不能同时满足一致性、可用性、分区容错,架构设计必须取舍。

现实案例:银行核心优先 CP(强一致);普通互联网社交产品优先 AP(最终一致性)。

2.3 状态决策:高可用的底层基础

冗余集群必须能够判断节点死活,做故障切换。但是状态判断很难做到 100% 准确,分三种模式:

独裁式(单决策者) 有一个独立决策者,所有节点上报状态。优点:决策唯一不会混乱;致命缺点:决策者本身单点故障,整个集群决策瘫痪。

协商式(主备模式) 两台机器互相通信协商,选出主机,另一台备机。痛点:心跳网络断开,备机无法区分:是主机挂了?只是网络断了?

如果直接升级备机 → 双主(脑裂风险);

如果不升级 → 主机真故障,整个系统无主机不可用。增加多条心跳链路只能降低概率,不能根除。

民主式(投票选举,Raft/Paxos) 集群所有节点平等,通过投票多数派做出决策,Zookeeper、etcd 就是该模式。风险:脑裂 ,网络分区集群分裂成 2 个子集群,各自选出主节点。解决办法:投票必须超过半数节点;代价:部分节点全部故障,哪怕剩余节点是好的,如果凑不齐多数派,系统直接停止服务,牺牲可用性换一致性。

现代云原生实践:etcd 使用 Raft 民主投票;数据库主备是协商式;部分监控调度组件采用独裁模式,同时给决策者做集群来规避单点。

高可用小结

  1. 核心手段冗余,计算高可用相对简单;存储高可用最难,重点解决数据不一致。
  2. 状态决策没有完美方案,每种模式都有缺陷,业务场景做取舍。
  3. 业界指标:99.99%(4 个 9),一年最多允许 52.56 分钟不可用。

三、复杂度来源 3:可扩展性

可扩展性定义:系统应对未来需求变化的能力。新增需求时尽量少修改旧代码,不需要整体重构系统 。面向对象、设计模式最初诞生,就是为了解决可扩展问题。

设计好可扩展性有两个前提:正确预测变化、完美封装变化 ,两件事本身都充满复杂度。

3.1 预测变化的困境

  • 不能什么地方都预留扩展:每个点都做抽象预留未来,带来严重过度设计,项目庞大无法落地。
  • 又不能完全不考虑扩展:需求一变就要推倒重构,成本爆炸。
  • 预测本身会出错:预估的需求永远不来,投入大量开发做的扩展完全用不上。

实战建议:不要为想象中的未来做过度设计;只有变化概率高的点做扩展,没有发生的变化,等到真正来了再重构。

3.2 应对变化的两种架构手段

方案一:分离「变化层」和「稳定层」

把系统中经常变动的逻辑放到变化层;把核心不变逻辑放到稳定层。两种形式:

多种接入协议(XML/JSON/Protobuf)是变化层,底层业务逻辑稳定层。

多种数据库实现(MySQL/Oracle/DB2)是变化层,上层业务逻辑稳定层。

复杂度来源:

  1. 划分哪部分是变化、哪部分稳定,团队之间容易有分歧。
  2. 层之间接口设计难度极高 ,接口既要稳定,又要兼容多种变化层的差异。
方案二:抽象层 + 实现层

抽象层规则固定稳定;新增业务只新增实现,不改动抽象。典型代表:设计模式(装饰器、策略模式)、规则引擎。代价:抽象本身会增加代码复杂度,简单业务硬套模式,反而让代码难懂。

现代云原生案例:

  • Dapr 就是抽象层,屏蔽消息队列、状态存储不同中间件差异,业务代码只调用 Dapr 抽象 API;
  • SPI 机制,接口是抽象层,不同厂商实现是实现层。

可扩展性小结

可扩展性不等于 "全部预留未来能力",警惕过度设计 ;

核心思想:隔离变化,变化部分和稳定部分解耦;

开闭原则:对扩展开放,对修改关闭;但是落地很难,需要结合业务判断。

四、复杂度来源 4:低成本

高性能、高可用通常做法是增加服务器;低成本正好相反,希望减少机器,低成本往往是附加约束,不是第一目标 。

矛盾:想要更高性能更高可用,通常要花更多钱;想要省钱,就要在性能可用性上做取舍。

实现低成本主要依靠创新 ,分两种:

  1. 引入新技术(中小企业主流) :学习新技术,集成到现有系统。

案例:Redis、Elasticsearch、Kafka,解决传统数据库痛点,降低机器数量。

  1. 自研创造新技术(大公司) :投入人力从零造轮子,成本极高,只有大厂有资源。

案例:LinkedIn 开发 Kafka;Facebook HHVM;新浪微博 SSD 二级缓存架构。

现代云原生时代低成本手段:

  1. 使用云厂商托管服务,不用自己运维中间件,降低人力成本;
  2. 弹性伸缩:流量低峰缩掉机器,高峰扩容,减少闲置资源浪费;
  3. 语言替换:把 PHP/Python 热点服务改成 Go,相同 QPS 需要机器数大幅下降。

复杂度:新技术学习成本、新旧系统兼容、迁移风险。

五、复杂度来源 5:安全

安全分为两类:功能安全(防小偷)、架构安全(防强盗) 。

功能安全:防小偷

对应 XSS、CSRF、SQL 注入、密码漏洞。根源代码缺陷。特点:攻防对抗,无法一次性做到完美,框架只能防御已知漏洞,新漏洞会不断出现,持续迭代修复。

架构安全:防强盗

大规模恶意攻击,例如 DDoS 大流量打垮系统。传统企业依靠多层防火墙隔离网络区域。互联网业务特点:大并发,防火墙性能扛不住海量 DDoS 攻击。现在主流依靠云厂商 / 运营商流量清洗,自己很少硬扛大攻击。

现代云原生安全:零信任、网络策略 NetworkPolicy、WAF、容器镜像安全扫描,属于架构安全;代码 SAST 扫描属于功能安全。

六、复杂度来源 6:规模(量变引发质变)

系统功能数量、数据量达到阈值,会发生量变引起质变 ,带来全新复杂度,哪怕没有高性能高可用需求,系统依然会很复杂。

功能规模膨胀

系统功能越来越多,功能之间互相调用关联,复杂度不是线性上涨,是组合爆炸。

举例:3 个功能,复杂度 = 3+3=6;8 个功能,复杂度 = 8+28=36。新增功能,不是只新增自己,而是新增和其他所有老功能的交互。现象:老系统,没人敢改,看不懂,改一处牵动全身。

数据规模膨胀

例如 MySQL 单表从百万上涨到十亿行:索引建立、DDL 变更、备份、查询性能全部出现问题,不得不做分库分表。分库分表又引出一堆新问题:分片规则、跨分片查询、join、分布式事务。

现代案例:大数据技术栈 Hadoop、ClickHouse 就是为了解决传统数据库扛不住的数据规模问题。

七、六大复杂度综合实战思考

  1. 没有系统会同时把 6 点做到极致,一定需要取舍 。
  • 银行支付系统:优先高可用、安全;其次性能;可扩展、成本次之。
  • 初创小业务:优先可扩展性,低成本;高性能高可用不需要过度设计。
  • 大数据分析平台:优先规模、成本;可用性可以适当放宽。
  1. 常见误区:
  • 一味堆机器解决一切:会带来成本和运维复杂度。
  • 为扩展而过度拆分微服务:带来网络、一致性、链路复杂度。
  • 只看功能需求,忽略六大非功能复杂度。
  1. 架构师核心工作:识别系统主要的复杂度来源,针对性设计方案,拒绝为不存在的问题提前买单 。

思考题(原文课后思考)

  1. 你的业务系统中,哪几个复杂度来源最突出?是否还有其他额外复杂度?
  2. 高性能与高可用相比,哪个更复杂,为什么?
  3. 你写过哪些可扩展的代码 / 架构,实际效果如何?

补充:现实还会衍生额外复杂度,比如团队人员水平、第三方外部系统集成、合规监管等,也会进一步放大整体系统复杂度。

相关推荐
要开心吖ZSH1 小时前
DDD(领域驱动设计)篇三:实战对照——同一个订单业务,两种架构怎么写
java·架构·领域模型·ddd·领域驱动设计·设计思想
吴建旭 智宅焕13 小时前
智能家居交付商品化的系统架构设计与实现:从附属服务到标准化交付商品
系统架构·智能家居
天河归来13 小时前
从原始消息到业务数据:三层数据架构的设计与实践
数据库·数据仓库·架构
天远Date Lab14 小时前
零信任架构实战:基于天远企业年报信息核验构建自动化商业保理财务与社保穿透评估网关
人工智能·架构·自动化
EnCi Zheng14 小时前
后端怎么把AI接口稳定安全低成本接进业务:超时重试、密钥隔离与成本熔断清单
人工智能·后端·安全·架构·api
救救孩子把14 小时前
LangGraph 工程化架构:类型、状态、流式通信与可观测性
架构·agent·trace·opentelemetry·langgraph·graphrag
这个DBA有点耶14 小时前
从 MySQL 到时序数据库:我帮朋友做迁移的完整记录
数据库·架构·dba
wu_solo15 小时前
6.1 Transformer架构概述
深度学习·架构·transformer
张一凡9315 小时前
把“不要乱改”从提示词里拿出来,放进可校验的提案里
前端·后端·架构