系统设计:理论基础
系统设计:理论基础
一、认识分布式系统
课程的最开始,我们先来了解以下分布式系统。为什么要先讲这个呢?因为现在的后端服务基本上都是构建在分布式系统之上的,可以这么说吧,分布式系统是构建高可用、高性能、可扩展后端服务的必选方案。
1. 分布式系统的定义
早期的系统架构,单体应用是主流,也就是,将所有代码打包运行在单台服务器上。起初,应用的功能比较简单、性能要求也不高,单台机器完全能满足。但是,随着技术的发展,用户的需求越来越丰富,应用的功能越来越多,性能要求也越来越高,单台机器的CPU、内存、磁盘I/O等慢慢就接近性能上限。
起初我们还可以通过升级机器硬件(俗称垂直扩展),用更多核的CPU、更大的内存、更快的磁盘来解决性能瓶颈。但是,单台机器的处理能力总是有上限的,考虑到技术和成本,当硬件升级达到上限但性能仍然无法满足要求时,我们与其造一只更大船,不如组建一支舰队。
于是,我们将系统拆分成更小的、独立的组件(服务),部署在多台(通常是大量的、相对廉价的)计算机(节点)上,通过网络连接协同工作,共同完成一个更大的目标,这样由一组机器组成的集群,对外来看像一台超级机器一样运转。
实际上,分布式系统并不是一个多牛逼、多神秘的概念。只要有2个或2个以上的子系统(或叫应用、程序)协同工作对外提供一致的服务,就叫做分布式系统。我们用Nignx做负载均衡,后面挂着多个Tomcat部署应用程序,应用程序再链接数据库,这就是一个简单的分布式系统。
除了分布式系统,后面我们还会讲到分布式锁、分布式事务、分布式一致性、分布式缓存。那么,这些概念里的“分布式”跟分布式系统里的“分布式”所具有的意思是一样的。即,多个子系统协同工作。分布式缓存是指将比如Redis这样的缓存系统部署在多台机器上,对外提供更强大(更快、容量更大)的缓存服务。分布式锁指的就是多个应用对同一数据进行互斥访问。分布式事务就是多个子系统的逻辑一起执行,表现出事务特性(ACID),简单理解为,要么都执行,要么都不执行。分布式一致性问题就是指多个节点上的数据是否一致的问题。
2. 分布式系统的优势
现在的淘宝、百度、抖音,背后都有成千上万的机器来提供服务,但对于我们用户来说,它的功能特性就跟一台机器提供服务是没有区别的。如果没有分布式系统,这些庞大的服务都只是部署在单台超级计算机上,那是一个相当困难和恐怖的事情。因此,分布式系统是架构演进的必然产物,那么,分布式系统相较于单体应用有哪些优势呢?
首先,它突破了单机的性能瓶颈。这个前面已经提到。机器硬件的升级我们叫做垂直扩展,增加机器我们叫做水平扩展。垂直扩展一定是有上限的,但从理论上讲,只要系统设计得当,水平扩展是没有上限的。
其次,它提升了系统的可用性。对于单体应用来说,单点故障是致命弱点。服务器硬件故障、操作系统崩溃、代码异常出错,甚至一次部署,都可能导致整个应用瘫痪或中断、以及所有或部分数据的丢失。
而分布式系统可以消除单点故障。只要设计得当,即使部分节点宕机,整体系统仍能继续提供服务。比如,Redis的主从模式,从节点复制主节点中的数据,数据通过备份,避免了丢失,而且,即便主节点挂掉,Redis也会自动在从节点中选举新的主节点,对外继续提供服务,避免了服务的瘫痪。再比如,Nginx作为负载均衡和反向代理,将流量路由到多台部署了应用的Tomcat机器上,单台Tomcat机器挂掉,Nginx会将流量分配给其他机器,这样,整个服务还可以照常运行,提高了可用性。
最后,分布式系统还可以提高开发效率。分布式系统将大的系统拆分成多个小系统,每个小系统可以独立开发、测试、部署,显然开发效率要更高。而且,每次代码改动,影响的范围更小,系统整体的稳定性更高。
3. 分布式系统的难题
分布式带来了巨大的优势,但也引入了单体架构中不存在的、极其复杂的挑战。这些挑战源于一个根本事实:网络是不可靠的,节点是可能失效的,信息传递是有延迟的。我们具体来看下有哪些挑战。
(1)故障检测转移
分布式系统中的任何节点都可能在任何时刻因硬件故障(如电源损坏、内存错误)和软件Bug等原因而停止工作。在单体架构中,服务器挂了,服务就挂了,这是一个确定的状态。但在分布式系统中,我们面对的是“部分失败”:集群中有一部分节点正常工作,一部分节点悄无声息地挂了。如何让其他节点“感知”到某个节点挂了?如何保证挂了之后任务能被转移?这引入了故障检测和状态转移的难题。
(2)数据一致性
为了保证高可用,我们通常会将数据复制多份(冗余设计),存储在多个节点上。当一个节点上的数据需要更新时,如何保证所有副本在其他节点上在同一时刻看到相同的值?
在网络延迟和节点故障存在的情况下,我们无法实现“同时”更新。例如,用户更新了头像,请求先到达节点A,节点A更新成功,但还没来得及同步给节点B,节点B就收到了读取头像的请求。此时,用户看到的是旧头像。这就是数据不一致。
(3)拜占庭问题
节点间通信需要时间,且时间长短不确定(可能几十毫秒,也可能几秒)。而且,消息可能丢失、重复、乱序。
在单机程序中,调用一个函数,结果要么成功,要么失败,是确定的。但在网络中,你发出一条消息后,可能面临以下几种情况:
- 消息没到达对方(丢失)。
- 对方处理了,但响应消息在返回途中丢失。
- 对方处理了,响应回来了,但耗时过长,超时了。
这让调用方无法区分“对方没收到”和“对方收到了但我没收到响应”。这种不确定性是分布式系统中最令人头疼的问题之一,被称之为“拜占庭问题”。
(4)分布式共识问题
共识问题指的是:在分布式系统中,多个节点需要就某个值或某个决策达成一致,即使其中部分节点可能出现故障或网络不可靠。简单来说,就是让一群“意见可能不一致”的节点,最终能统一意见。在Redis、Kafka、Etcd 等分布式系统中,当主节点宕机时,剩余节点需要通过共识算法选举出新的主节点。
(5)分布式事务管理
跨多个服务的事务(分布式事务)比单机数据库事务复杂得多。保证 ACID 属性需要引入如 2PC、3PC、TCC、Saga 等补偿机制,这些机制本身也有复杂性和性能开销。
(6)测试与调试困难
复现分布式环境下的复杂故障场景极其困难。系统状态分散在多个节点,问题定位犹如大海捞针。日志散落在几十台机器上,排查一个超时问题往往需要关联多个节点的时序信息,甚至需要借助分布式追踪系统(如 Jaeger、SkyWalking)才能还原请求的全貌。
4. 分布式系统设计:CAP 定理
CAP 定理由 Eric Brewer 提出,他指出,在一个异步网络(存在延迟、丢包、分区风险)且可能出现节点故障的分布式系统中,以下三个特性无法同时完全满足:
- C (Consistency - 一致性): 所有节点在同一时刻看到的数据是完全相同的(强一致性)。任何一次读操作都能读到最近一次写操作的结果。
- A (Availability - 可用性): 系统始终可用,每一个向系统发出的请求,都必须在有限的时间内得到一个非错的响应(不能是超时或失败)。
- P (Partition Tolerance - 分区容错性): 当系统中节点之间因网络故障导致通信中断(形成网络分区 Network Partition)时,系统整体仍然能够继续提供服务。
CAP理论迫使我们在开发分布式系统时,根据业务特点和要求,对CAP做抉择,选择牺牲哪一个。
CP 系统 (牺牲 A): 当发生网络分区时,为了保证所有节点数据强一致(C),系统可能拒绝部分或所有写操作,甚至拒绝某些读操作(返回错误或超时),从而牺牲可用性(A)。例如:ZooKeeper, etcd, HBase(强一致模式下)。它们优先确保数据在所有分区内都是一致的,即使代价是暂时不可用。再例如:银行核心转账系统通常选择CP。想象跨行转账时网络分区,系统宁愿告诉你“服务暂时不可用,请稍后再试”,也绝不会冒险让两边账户不一致(比如钱扣了但对方没收到)。
AP 系统 (牺牲 C): 当发生网络分区时,为了保证系统整体仍然可用(A),允许不同分区内的节点数据暂时不一致(牺牲 C)。系统会继续处理读写请求,并在分区恢复后解决冲突。例如:Cassandra, DynamoDB,它们优先确保用户可以读写,接受短暂的数据不一致。再例如:社交媒体的点赞、状态发布通常选择AP。假设微信朋友圈发布时网络分区,系统允许你在本地分区发布成功并立即看到(可用性),但可能稍后网络恢复时,其他分区才看到你的更新(暂时不一致)
CA 系统 (牺牲 P): 这通常只存在于理论上或极小的局域网内。它假设网络永远不会分区(P)。这在广域网环境下是不现实的。也就是说,在绝大多数分布式系统中,网络分区是基本上无法避免的,因此,CA分布式系统可以看做是不存在的。
5. 分布式系统设计:BASE理论
当然,对于大多数分布式系统来说,我们并不能非黑即白地粗略地选择CP还是AP。一方面,可用性对于互联网产品非常重要,我们不能抛弃,另一方面,数据的一致性也非常重要,还有,强一致性也很难实现,因此,结合实际,工程师又提出了另一个分布式系统设计的理论:BASE理论。
Basically Available (基本可用): 系统在出现不可预知故障时,允许损失部分功能或响应时间稍有延长,但核心功能仍然可用。比如电商大促时,可能关闭非核心的商品评论功能,或查询响应稍慢,但下单支付主流程必须保证可用。
Soft state (软状态): 系统中的数据状态不要求时刻保持强一致,允许存在中间状态(不同副本间数据存在短暂差异),且该状态的存在不会影响系统整体可用性。比如用户积分,可能在主库更新后,异步复制到其他库,中间存在延迟。
Eventually consistent (最终一致): 系统保证在没有新的更新操作情况下,经过一段时间的同步(如秒级、分钟级),所有数据副本最终将达到一致的状态。 这是弱一致性中被广泛接受的一种形式。比如你修改了头像,可能几秒钟后,所有好友才都能看到新头像。
6. 最后,没有银弹,只有权衡
理解分布式系统的本质(多节点协作)、核心目标(扩展性、可用性)以及它带来的根本性挑战(节点故障、网络问题、数据一致性等),是设计、开发和运维分布式系统的前提条件。每一次架构决策,尤其是在面对 CAP 时,都是对不同业务需求(例如,银行交易需要强一致 CP,社交动态可以接受最终一致 AP)和技术的权衡。
在后续文章中,我们将深入探讨分布式系统的关键组件和解决方案,如服务发现、负载均衡、容错机制、消息队列、分布式事务等,帮助你逐步构建应对这些挑战的实用工具箱。
二、系统架构评价指标
这节课的内容不仅有助于你平时项目中做架构设计、架构评审,还有助于你在系统设计面试中,多维度的考虑和优化你的设计方案。
在《设计模式之美》中,我们讲到如何评价一个代码的质量/好坏,当时讲到很多指标,比如可读性、可扩展性、可维护性、可复用性等等。
今天,我们来聊聊怎么评价一个系统(或架构设计)好与不好。评价一个系统,除了关注它的功能是否实现完备,还要看它的非功能特性,这就好比你买车不能只看外观,还得看油耗、安全性、舒适性、操控性一样,评价一个系统设计是否优秀,也得从多个维度去分析。
平时我们整天把“高并发”、“高可用”、“高性能”这些词挂在嘴边,这三个指标是常用的系统架构评价指标。其实,除了它们之外,还有其他一些评价指标,比如,可靠性、伸缩性等。
这篇文章我们就具体聊一聊这些指标。
1. 高性能
评价系统性能好不好,就像评价一辆车,不能光看它能跑多快,还得看它能拉多少货,对应到系统性能上,就是响应时间、吞吐量。我们依次详细看下这两个指标。
(1)响应时间
这是最直观、用户感知最强的指标。想象一下,你点个外卖,APP 转半天圈圈才告诉你附近有啥店,你肯定想骂人。
从用户的角度来说,响应时间包含:从点击页面按钮/链接开始,到浏览器完全渲染出内容、用户可以交互为止的时间(俗称“页面加载时间”)。这包含了网络传输、后端处理、前端渲染所有环节。
聚焦在后端架构,响应时间指服务器端处理的耗时,通常指API 响应时间,不包含请求从用户端到服务器、以及响应从服务器回到用户端的网络传输时间(尤其是公网环境,这部分延迟可能很大且不可控)。
一般来说,我们会关注响应时间的百分位值而非平均值,因为平均值很容易掩盖问题。想象一下:10个请求,9个耗时 100ms,1个耗时 10s,平均是 1090ms,看起来还行?但那个等了10秒的用户可能已经骂娘了!因此,我们一般关注P90(90分位值,也就是90% 的请求响应时间小于等于这个值)、P95、P99、P99.9等。
(2)吞吐量
这个指标衡量系统在单位时间内能处理多少“工作量”。它反映的是系统的处理能力。对于实时系统,常见的指标有QPS、TPS。对于离线系统,比如后台JOB,可能会自定义吞吐量指标,比如每秒钟处理的任务数等。
QPS很好理解,是指每秒能成功处理的请求数量。比如一个 API 接口每秒能处理 1000 次调用,其 QPS 就是 1000。这也是最常用的吞吐量指标。
TPS为每秒事务数。特指每秒能成功完成的业务事务数量。一个“事务”通常代表一个完整的业务操作(比如“用户下单”这个事务,可能包含扣库存、创建订单、支付等多个步骤)。TPS 更能反映核心业务的处理能力。
我们再来看下吞吐量跟响应时间之间的关系。
吞吐量和响应时间之间有关联,但非线性!我们举个例子来讲解,假设有一个收银台:正常情况下,处理一个顾客(请求)需要 2 秒(响应时间),那么 1 秒能处理 0.5 个顾客(QPS=0.5)。如果顾客(请求)来得太快,开始排队,处理一个顾客可能就需要 5 秒(因为要等待嘛,响应时间变长),但收银员还是 1 秒处理 0.5 个顾客(QPS 不变)。如果收银员提高处理速度,那么,响应时间减少,QPS也会增大。
总结一下,提升处理速度(降低单次响应时间)能提高吞吐量(QPS)。资源饱和后,增加并发请求数(更多的顾客)只会导致排队,响应时间急剧上升,但吞吐量(QPS)可能达到瓶颈甚至下降(因资源争抢开销增大)。
实际上,吞吐量最终受限于系统中瓶颈资源(CPU、内存带宽、磁盘IO、网络带宽、数据库连接池大小等)。提升吞吐量,关键在于找到并消除瓶颈。
2. 高并发
我发现,有些同学经常把高性能和高并发混淆,以为高性能和高并发指的是一个意思,实际差已。
想象一下春运抢票,或者双十一零点开抢,又或者网红主播带货,平时系统可能挺快,但瞬间涌进来海量用户请求,系统能不能顶住?会不会直接“躺平”报错?高并发能力就是衡量系统在短时间内处理大量同时涌入请求的能力。它和高性能紧密相关,但侧重点不同:性能关心单个请求快不快,并发关心同时来一大波请求时系统整体能不能撑住、会不会崩溃。
我们还是拿收银员的例子来举例。假设收银员处理一个顾客的时间是1秒,那么QPS就是1。如果同时涌入5个顾客,并且,我们没有使用排队机制,这个时候,其他4个请求都会拒绝,这个时候,系统的并发数为1。为了提高并发能力,我们使用排队机制,在顾客对响应时间可以接受的情况下(最长可接受5秒),在QPS性能不变的情况下,并发数提高到了5。你看,性能没有变,QPS都是1,采用排队的机制,并发数从1提高了5。其实,排队也是秒杀这种高并发场景的其中一种解决方案。
当然,高并发和高性能也是有关系的。在同样的可接受的响应时间范围内,单个请求的处理速度变快,系统可支持的并发数也会相应提高。
3. 高可用
高可用的终极目标就是让用户感觉不到系统出过问题,服务一直“在线”。我们经常听,因为某个明星出轨等热点新闻导致微博宕机,这就是所谓的系统不可用。衡量系统可用性的最直观的指标就是SLA(服务等级协议),比如承诺“全年99.9%可用”,那一年里允许的宕机时间大概就是8.76小时。对应SLA 99.99%的允许宕机时间为52.6分钟了,对应SLA 99.999%的允许宕机时间为5.26分钟。很多大厂的关键应用都要求4个9或者5个9的SLA。当然,SLA要求越高,付出的成本和系统的复杂度就会成指数级增长。
高可用是我们在做大型系统设计时必须要考虑的方面。互联网产品使用的机器大都是普通的服务器,可靠性等各方面都不会很高,而大型应用都对应成百上千甚至上万的服务器,服务器的数量比较多,即便很小的故障概率也会被放大,每天也必定会有几台服务器发生硬件故障。除此之外,网络通信中断、软件运行异常、代码bug等等,都有可能导致某个服务器临时运行异常。
高可用设计的目的就是不管是硬件还是软件故障发生的情况下,服务依然可以使用,数据依然不会丢失。要达到这样的目的,要做的工作有很多,根据对SLA不同的要求,可以选择性的使用其中某些方法。
- 冗余: 消除单点,比如集群部署、主备复制、异地多活。应用部署在多台服务器上同时提供访问,数据存储在多台服务器上互相备份,任何一台服务器宕机都不会影响应用的整体可用,也不会导致数据丢失。
- 容错: 降级、熔断、限流、异步,部分挂不影响全局。某个非核心服务挂了(比如积分服务暂时不可用),别影响用户下单这个核心流程,给它降级处理(比如先记下来,等恢复了再补算)。依赖的外部服务挂了(比如短信网关),要有备用方案或者优雅地提示用户。
- 可观测性: 监控、日志、追踪都要有,能时刻了解系统的运行情况,即便出了问题,也可以快速发现和定位问题,是网络抽风了,还是某段代码把CPU吃满了,或者数据库连接池爆了?快速解决问题,也能提高可用性。
- 自动化:故障发现、切换、恢复这些操作,不能光靠人工干预,否则,效率太低还容易出错,得靠监控系统自动发现、自动处理。
4. 可靠性
我们再来看可靠性,很多同学可能认为可靠性跟可用性讲的是一件事情,实际上,并不全是,从英文翻译上可以看出它们的区别。可用性的翻译是Availability,可靠性的翻译是Reliability。
可靠性更宽泛一些,它不仅关注系统“活着”(可用),还关注系统“正确地活着”。它衡量的是系统在指定条件下、指定时间内,无故障地完成指定功能的能力。比如,你往数据库存一条数据,它是不是100%存进去了,并且是否存对了?处理一笔支付,是不是准确无误地完成了?这涉及到数据的正确性、一致性、服务的健壮性(容错性)。避免单点故障是高可靠的基础,同时还需要完善的错误处理机制、数据校验、事务保证、幂等设计等等。一个系统可能可用性很高(很少宕机),但如果它时不时给你算错账、丢数据,那可靠性也是不及格的。
根据墨菲定律,所有可能发生的错误都会发生。在编写代码、设计架构时,发现可能出现的异常情况,提前为异常情况编写处理逻辑进行防御性编程,以及严格的代码审查、测试,这些都可以有效提高可靠性。
5. 伸缩性
伸缩性也叫做扩展性,英文翻译Scalability。伸缩性衡量的是系统通过增加(或减少)资源来应对负载变化的能力。比如,用户访问量翻10倍,目前的架构是否可以通过增加资源(机器)轻松应对。
伸缩分为两类:垂直伸缩和水平伸缩。垂直伸缩指的是给单台服务器加CPU、加内存、换更好的硬盘(比如机械硬盘换成SSD)。简单粗暴,但有上限,成本也高。水平伸缩指的是通过增加服务器数量来分担压力。这才是互联网分布式系统的终极解决方案。
水平伸缩性好不好,关键看架构设计是否无状态(Stateless)、能否方便地添加/移除节点、负载均衡是否高效、数据存储是否支持水平拆分(分库分表、NoSQL)等。一个伸缩性好的系统,面对业务增长时,能相对平滑地通过加机器来应对,而不是动不动就要推倒重来。
当然,伸缩并不只是增加资源,还有可能减少资源,能够根据流量动态调整资源的投入。几乎所有的业务都访问高峰和低谷,比如白天比晚上高,周末比工作日高,促销时期或者突发事件也可能导致流量的突增,为实现高可用与成本效益的平衡,系统需要在高峰时弹性扩容保障性能,低谷时缩容释放冗余资源,完成动态的按需伸缩。
6. 可观测性
这一条可能是很多初学架构的人容易忽略的:可观测性(Observability)。通俗点说,就是让系统运行时的内部状态能被我们“看”到。
为什么需要可观测性?因为分布式系统就像一个黑盒子,成百上千个节点在跑,用户访问慢,是网络问题?是数据库慢?是某个服务死锁?没有数据,你只能靠猜。靠猜的结果往往是通宵排查问题,最后发现可能只是日志级别设错了。
可观测性通常包含三大数据支柱:日志、指标(Metrics)、追踪(Tracing)。
- 日志数据:记录离散的事件,比如“用户A下单成功”“数据库连接超时”。日志适合排查问题,但要结构化,最好用JSON格式,方便集中收集(如ELK)。
- 指标数据:聚合的数据,比如QPS、响应时间、错误率。通过Prometheus等工具采集,可以设置阈值告警,比如“订单服务错误率超过1%”时立即通知。
- 追踪数据:记录一次请求在多个服务之间的完整链路。想象用户下单,经过网关、订单服务、库存服务、支付服务、积分服务,如果其中某一步慢了,你能在Jaeger或SkyWalking上看到是哪个环节耗时最长。
可观测性设计的核心是让系统可被理解。一个系统再复杂,只要你能清楚地看到它的内部运行状态,就能快速定位问题、优化性能、预判风险。反之,再简单的系统,如果它是不可观测的,早晚会让你在凌晨三点爬起来对着寥寥无几的日志发呆。
7. 最后总结
这节课我们讲了系统架构的评价指标,包括高性能、高并发、高可用、可靠性、伸缩性、可观测性,当然,根据业务的不同、架构设计的不同,还可能会有其他重点关注的指标,比如可观测性、安全性,这里我们就不一一介绍了。优秀的架构设计,就是在深刻理解业务需求的前提下,在这些指标之间找到最符合当前和未来一段时间预期的那个平衡点。
三、系统架构设计原则
在讲解《设计模式之美》中,我们提到了代码的一些设计原则,比如KISS、DRY、SOLID等等,那么,架构设计是不是也应该遵从一些设计原则呢?这节课,我们就来聊聊基本的架构设计原则。
1. 简单
在《设计模式之美》中,我们讲到KISS原则,这个原则同样适用于架构设计。代码里的KISS是说一个函数、一个类别搞得太复杂。架构上的KISS呢?核心思想就是:能用简单方案解决的,绝对不用复杂方案!
这听起来像废话?但在实际项目中,工程师们(包括我自己)常常被“技术虚荣心”、“对未来的过度焦虑”或者“对流行技术的盲目追逐”带偏,把架构搞得无比复杂,最后把自己搞的特别辛苦。KISS原则就是对抗这种过度设计的。
一个预期用户量就几千的内部审批系统,数据量很小,业务逻辑也不复杂。结果架构师为了体现项目的难度和技术含量,要求必须上微服务!要用 Kubernetes 编排!消息队列用 Kafka 保证高吞吐!数据库分库分表预留未来十年容量!结果呢?团队大部分精力都耗在搭建、运维、调试这套“豪华”基础设施上了,核心审批流程反而不关注,导致功能实现不完善、代码bug多多。项目部署一次像打仗,出个问题排查链路长得能绕地球一圈。这就是典型的“杀鸡用牛刀”,为了技术而技术,完全违背 KISS。
思从深而行从简,但凡能用简单的架构解决复杂问题的,都是有实战经验的老师傅。不管自己业务的规模大小,动不动就要上业界经典架构的,都是些看多了八股文的没有实战的新手。比如,明明可以通过select for update数据库悲观锁实现的分布式锁,非得引入一个新的系统Redis来实现分布式锁。
在架构设计中,每引入一个组件,对架构的复杂度、维护成本、可用性等都随之带来很大的影响。我们拿可用性举例,你想,如果系统引入5个组件,一个组件的可用性是99%,那么,组合在一起,这个系统的可用性就是99%的5次方,也就是0.95,所以,能用已有的组件简单解决的问题,就不要大动干戈的引入新的复杂组件。
2. 演进
我们去看开源项目代码,往往都会惊叹于它的复杂,觉得自己写不出这么复杂的代码,实际上,罗马不是一天建成的,你之所以看到现在的代码很复杂,实际上,都是一点点堆起来的。我想最初版本的代码应该都很简单,即便是Linux这么复杂的开源项目,最初版本的代码也仅有不到一万行。在不停的往里添加功能,解决问题,提高性能等等需求的推动下,代码才变得越来越复杂。
架构设计也是如此。复杂的架构都是演进而来的。你看现在的淘宝、微信等大型应用,架构都非常的复杂,但这也都是几年、十几年一点一点演进的结果,你去网上搜一下,就会发现,它们的最初架构也都是非常简单的。是用户量、性能压力、高可用的要求、业务需求等等实实在在的需求推动了架构的演进,才导致架构越来越复杂,而非架构师脱离需求单纯靠拍脑袋设计出来的。
其实,架构过度设计的情况非常常见,甚至我也经常如此。设计一个新电商平台,才刚起步,用户、商品、订单量都很少,架构师就开始担心:“万一我们明天就成淘宝了呢?”于是,在设计订单系统时,预设了支撑每秒 10 万订单的架构;设计了极其复杂的分布式事务补偿机制;把库存、优惠券、积分等服务拆得极细,预设了它们未来各自独立演进、需要强事务协调的场景。结果:开发周期巨长,初期版本臃肿不堪,性能可能还不如一个设计良好的单体(因为分布式调用开销)。更惨的是,业务发展可能根本没达到预期,或者方向变了,这些“提前量”全成了无用功,成了维护的负担。
而正确的做法应该是,清晰定义当前的核心需求和近期的(比如未来3-6个月)可预见需求。比如,初期订单系统,保证核心下单流程正确、库存不超卖是重点。一个在单体应用内,利用数据库事务 + 清晰的业务逻辑就能搞定,简单可靠。当真实需求出现、真实瓶颈暴露时(比如订单量真的大幅增长了,数据库成为瓶颈;或者业务要求积分和订单解耦独立发展),再根据当时的实际情况,运用合适的架构模式(比如引入消息队列异步化、拆分服务、优化数据库、分布式事务、分布式锁、分布式缓存)去解决。让架构随着业务一起成长。
其实,对于大部分产品,初期,一个单体应用配合清晰的模块划分(比如使用gradle module组织模块)和一个够用的数据库,可能比强行拆分成一堆微服务要简单高效得多,开发快、部署快、问题也好排查。当然,简单不是简陋,而是用最匹配当前需求和团队能力的方案。 别为了显得“高级”而堆砌技术栈,那是技术虚荣心作祟。
除此之外,在过往的面试中,除了算法之外,我也经常会出一些系统设计题目,很多同学在根本不问清楚性能要求的情况下,比如有多少用户,每天的QPS是多少,数据量有多大等等,上来就用业界大厂的方案往上套,我一般会认为候选人架构理论知识丰富,但实战经验不足。
只有有实战经验的、经历过架构演进的同学,才能做到知然知其所以然,也就是,不仅知道架构长什么样子,还应该知道为什么长这个样子。学习架构的演进,如何根据当前业务,做架构决策,比单纯的背各种大厂架构解决方案,要更加重要!我常说,要想成为一名合格的架构师,除了学习理论知识之外,还要躬身入局去一线实践,不然就会成为满嘴跑火车,但一线同学看见就烦的PPT架构师。
3. 权衡
设计架构是工程实践,不是理论研究。理论研究有最优解,但架构设计并没有放之四海而皆准的最优解,抛开业务谈论架构设计都是耍流氓。我们设计架构,目标从来不是捣鼓出一个理论上无懈可击的“完美艺术品”,而是工程上合适、可用、稳定的务实解决方案。千万别掉进追求“牛逼”、追求“完美”的陷阱里。
曾经有读者跟我抱怨,新加入的公司技术栈“落后”,还在用老旧的JDK8,没上微服务,业界大多数公司都用上云原生那一套了,我们还在吭哧吭哧维护一个超大单体!提了好几次想引入 Redis 缓存优化性能,领导却说MySQL 扛得住,实在不行升级一下硬件就行,想重构一下祖传屎山代码,刚提个方案就被否了,说业务需求都做不完,别瞎折腾!他觉得:领导“能力不行”,只追求能用就行,不肯投入搞技术升级,对新技术不敏感...
实际上,这些看似“落后”或“保守”的决策,往往都是团队(尤其是领导层)在特定背景下,艰难地权衡业务生存压力、资源成本与长期技术愿景之后的结果。 因为要活下去、把眼前业务跑通、控制成本的优先级,在当前阶段远高于追求技术先进性或架构的优雅性。
特别是小公司或创业初期,钱紧、人少、业务不确定性高,每一分钱和每一分钟都得花在刀刃上,这个“刀刃”就是 验证商业模式、获取用户、产生现金流。这时候,“能用、稳定、便宜” 比 “先进、优雅、前瞻” 重要得多。强行上“高大上”的架构,可能还没等业务起飞,就被高昂的运维成本、复杂的技术栈、研发团队投入拖垮了,或者因为过度设计耽误了宝贵的上线时间。领导说“加机器就行”,可能算下来确实比投入人力重构+引入新组件更省钱省事(至少在短期内)。这不是“能力不行”,而是在残酷的商业现实面前,做出的基于成本效益的务实取舍。
再比如分布式系统中,我们要实现强一致性的分布式事务,需要使用2PC、3PC这种强一致性算法,虽然它们在保证一致性方面表现的非常好,但是,在绝大多数互联网产品技术架构中,我们很少选择使用它们,而是权衡性能和一致性的实时性,选择使用本地消息表、事务消息等方案,实现最终一致性,这其实也是一种妥协和权衡。
因此,我们在设计技术架构的时候,一定不要追求完美,既要性能,又要可用性,又要极致的用户体验,这个是办不到的,即便能做到,系统的复杂度也会上升好几个数量级,得不偿失。就比如刚刚提到分布式事务问题,即便要保证最终一致性,如果要追求完美,不需要一丁点的人为干预,也是需要做大量工作,这个我们后面再详细的讲,因此,我们一般会在用户体验上做一些妥协,用一些看似不怎么高大上的技术,对极端情况下没法达成一致性的情况,进行离线的对账、监控告警、人工补偿。
4. 最后总结
今天讲的内容可以说是架构设计原则,也可以说是架构设计思想,更可以说是架构设计的正确认知,简单、演进、权衡(妥协),是一个架构师或者工程师走向成熟的标志。
四、系统设计硬件常识
尽管在大型公司里,通常有专业的运维团队负责管理机器资源,但作为架构师甚至普通工程师,掌握一些基础的计算机硬件知识依然至关重要。这能让你更有效地与运维团队沟通协作。对于众多中小公司而言,工程师往往需要身兼开发和运维的角色,硬件知识更是必备的生存技能,具备一定的硬件知识,能够让你在选型、部署、调优乃至故障排查时心里有底。
理解硬件特性是进行合理技术选型和资源匹配的关键。不同的系统对硬件资源的需求差别巨大:比如 Nginx 这类反向代理服务是典型的 CPU 消耗大户,处理海量并发连接需要强劲的计算能力;而像 RocketMQ、Kafka 这样的消息队列以及 Redis 这类内存数据库,则对内存容量和带宽有着极高的需求;数据库应用则常常受限于磁盘 I/O 性能(尤其是随机读写 IOPS)。如果给 Nginx 配了大内存但 CPU 较弱,或者给 RocketMQ 配了顶级 CPU 却内存不足,就如同给跑车加柴油,好的架构设计也难以发挥其应有的优势。
除此之外,硬件知识也深刻影响着日常的编程开发。例如,理解 CPU 的多级缓存机制,能促使你写出缓存友好的代码(比如优化数据结构、利用空间局部性),显著提升热点代码效率。因此,无论是为了高效协作、独立运维,还是为了精准匹配资源、优化程序性能,甚至是设计出真正贴合底层运行的健壮架构,了解计算机硬件的基本知识,都是工程师不可或缺的基础素养。
1. CPU
我们先看下计算机中最重要的资源:CPU。对于CPU,我们需要了解以下几个重要的硬件指标。以下的很多内容,在计算机组成原理课程中,我们都有更加详细的讲解,这里只是简单介绍。
(1)CPU主频
主频,单位是 GHz,比如 3.5 GHz。简单说,它代表了 CPU 内部时钟一秒钟能“滴答”多少次。每一次“滴答”,CPU 就有可能完成一个最基本的操作(注意,并不是完成一个CPU指令的时间)。这个可以看我们的《计算机组成原理》这么课,里面有详细讲解主频。
高主频往往伴随高功耗和高发热。主频越高,CPU 内部的晶体管开关就越快,消耗的功率和产生的热量也会急剧上升。因此,在服务器领域,我们很少见到 5GHz 以上的 CPU,因为散热和供电会成为巨大的挑战。在计算机组成原理课程中,我们也讲到,在历史的某个时期,CPU厂商(Intel、AMD等)曾一度追求过设计出更高主频的CPU,但发现这个方向行不通,于是,转而去设计多核CPU架构。
(2)多核CPU
既然提高单个核心的主频越来越难,那就干脆多造几个核心塞进一个 CPU 里!这就是多核 CPU。 每一个核心为一个独立的、完整的 CPU 执行单元,拥有自己的 L1/L2 缓存(通常),能独立执行指令流。
这是提升并行处理能力的根本。 操作系统可以把不同的程序(进程),或者同一个程序的不同部分(线程),分配到不同的核心上去同时执行。编译大项目、渲染视频、运行大型服务器处理并发请求,多核的优势非常明显。
多核带来了真正的并行处理能力,但也引入了新的挑战:资源竞争和缓存一致性。当多个核心同时访问同一块内存时,硬件需要通过缓存一致性协议(如 MESI)来保证数据同步,这会产生额外的开销。在编写多线程程序时,如果大量线程争抢同一个锁,会导致 CPU 核心之间频繁“沟通”,性能急剧下降,甚至比单核还差。这也是为什么Redis使用单线程执行命令的原因,避免了锁竞争。
在做技术选型时,是选择高主频还是多核,还要结合业务类型:如果是 Nginx、Redis 这类对延迟敏感的服务,高主频的优势明显;如果是 Hadoop、Spark 这类并行计算任务,多核比单核主频更重要。
(3)CPU架构
CPU架构,也叫做CPU指令集,主流的就那么两个:x86(x86-64针对64位计算机)、ARM。Intel、AMD生成的CPU都是x86架构的,也是服务器和PC机的主流。x86的指令集是“复杂指令集(CISC)”,一条指令可以做很多事情,但功耗相对较高。
对于手机芯片来讲,功耗是需要考虑的重要因素。因此,ARM架构成为了移动设备的绝对主流。ARM采用的是“精简指令集(RISC)”,设计哲学是简单即高效。每条指令只完成一个基本操作,执行速度快,电路更简单,功耗也远低于x86。
但ARM的影响力早已不局限于手机。近年来,ARM架构开始大举进军服务器市场。亚马逊的AWS推出了基于ARM的Graviton系列处理器,华为有鲲鹏,阿里有倚天。这些ARM服务器在云原生、微服务、容器化场景中表现出色,因为这类场景通常需要支持大量轻量级进程并行处理,对功耗和核数更敏感,而ARM正好擅长“多核、低功耗”。
CPU架构选择背后的核心逻辑,是功耗与性能的权衡。x86追求极致单核性能,适合复杂计算;ARM追求能效比,适合大规模并行。没有绝对的好坏,只有是否匹配业务场景。
(4)多级缓存
CPU 缓存是隐藏在核心内部的超高速存储器,用来弥补 CPU 与内存之间的速度鸿沟。现代 CPU 通常有三层缓存:L1、L2、L3。
L1 缓存最小最快!通常每个 CPU 核心独享一份(比如每个核有自己独立的 32KB 或 64KB)。L2 缓存比 L1 大一些(比如 256KB 或 512KB 每核),速度稍慢一点,但依然很快。通常也是每个核心独享。L3 缓存更大(几 MB 到几十 MB),更慢(但依然远快于内存),由同一颗 CPU 上的所有核心共享。
缓存之所以能提升性能,依赖于程序的“局部性原理”:时间局部性(刚访问过的数据很可能再次被访问)和空间局部性(访问某个数据后,其相邻的数据也很可能被访问)。理解这一点,就能更好地写出“缓存友好”的代码。
一个经典的例子是遍历二维数组。如果用“行优先”顺序访问,内存地址连续,缓存命中率高;如果用“列优先”顺序,则每次访问都会导致大量缓存未命中,性能相差几十倍。像 Java 的 Disruptor 框架为什么快?核心之一就是它设计的数据结构能最大化利用 CPU 缓存行(Cache Line,通常是 64 字节),减少“伪共享”(False Sharing),即一个缓存行里的数据被不同核心频繁修改导致的频繁失效重新加载。在计算机组成原理的都有详细讲到,不懂的可以去看看那门课。
2. 内存
讲完了CPU,我们再来看看内存。内存是CPU与硬盘之间的“中转站”,它的速度比硬盘快几个数量级,但比CPU缓存慢得多。内存的大小、速度、带宽,直接影响系统的整体表现。
(1)内存大小
服务器内存大小是架构选型时最直观的指标。不同应用对内存的需求差异极大:Redis、Kafka这类内存型中间件,数据几乎都在内存里,内存大小直接决定了能存储多少数据;Java应用受限于堆内存配置,如果给得不够,频繁的GC(垃圾回收)会把CPU拖垮(CPU利用率飙升);数据库虽然主要依赖磁盘,但内存用于缓存索引和数据页,足够的内存可以大幅减少磁盘I/O。
除此之外,在操作系统课程中,我们讲到,当物理内存不够用时,操作系统会把一部分暂时不用的数据交换到慢得多的硬盘上,等需要时再读回来。这个过程叫 Swapping。我不说你也能看得出,Swapping严重影响程序的性能。因此,架构师必须根据应用类型预估内存需求,一定要给服务器配置充足的内存,“宁大勿小”是基本原则!
一个常见的经验公式是:操作系统本身会占用2~4GB,还要预留30%左右的内存应对突发流量和系统缓存。例如,一台32GB内存的服务器,操作系统占用4GB,留给应用约28GB,再预留30%(约8GB)给临时需求,实际可用给核心业务的内存大约20GB。如果运行Redis,这个容量就意味着大约20GB的数据集上限。
这里再补充一下:32位系统只能用 32 根“地址线”来指定内存位置。这最多能管理 2^32 = 4GB 的内存地址空间。相应地,64位系统理论上能管理 2^64 字节的内存,这是一个天文数字(16 EB),远远超出目前任何单台服务器的物理内存容量。要使用超过4GB的内存,必须使用64位操作系统。现在几乎所有服务器和主流桌面都是 64 位。
(2)访问速度
实际上,我们在购买内存时,常听到“DDR4-3200”“DDR5-4800”这样的术语,这里的“3200”和“4800”指的是内存的传输速率(MT/s),即每秒能进行多少百万次数据传输。频率越高,内存访问速度越快。
其中,DDR4、DDR5是内存类型。DDR4是目前主流、成熟稳定的内存类型,DDR5 比 DDR4 更快、更省电、容量更大,是趋势。尤其是追求高性能或大容量(>256GB),DDR5 是更优选择。
除此之外,内存的通道数也很重要。通道数直接决定了内存和CPU之间的带宽(也就是一次读写可以传输的数据量)!通道越多,CPU 和内存之间能同时传输的数据量就越大。现在普通消费级内存多为双通道或者四通道,八通道多应用于高性能服务器。关于通道,在计算机组成原理中有详细讲解。
不过,对于大多数业务系统来说,容量比频率更重要。内存不足导致的Swap远比频率稍慢带来的性能损失更致命。所以,在预算有限时,优先保证内存容量,再考虑高性能内存。
3. 硬盘
硬盘的选择非常重要,因为它往往是系统性能的瓶颈。
硬盘一般分为HDD(机械硬盘)和SSD(固态硬盘)。机械硬盘靠移动磁头和旋转的盘片读写数据,磁头需要移动到正确的磁道(寻道时间),然后等待盘片旋转到正确的位置(旋转延迟),最后才能读写数据。这两个机械动作导致延迟高达毫秒(ms)级(通常是 5ms - 20ms),因此,随机读写延迟高,读写速度慢。而固态硬盘用闪存芯片存储,没有机械部件,数据通过电子信号读写。随机读写性能碾压 HDD(快几十上百倍),延迟超低(us级),读写速度快。
我们一般用IOPS、吞吐量、延迟、容量来评价硬盘的性能。
(1)IOPS
IOPS (Input/Output Operations Per Second) 指的是每秒读写操作次数,衡量硬盘处理小文件、随机读写能力。数据库、消息队列等非常关注这个指标。一块普通 7200 转的机械硬盘,IOPS 大约在 100 左右;而一块消费级 SATA SSD,IOPS 可达几万到十万;高端 NVMe SSD 则可以达到百万级别。
(2)吞吐量
吞吐量(Throughput)指的是单位时间内传输的数据量(如 MB/s, GB/s),它衡量的是处理大文件、连续读写的能力。视频流、日志写入、备份等场景对吞吐量要求更高。机械硬盘顺序读写在 100 ~ 200 MB/s 之间,SATA SSD 约 500 MB/s,NVMe SSD 可达 3000 ~ 7000 MB/s。
(3)延迟
延迟指的是从发出 I/O 请求到收到响应的时间,它衡量磁盘对单个请求的响应速度。对延迟敏感型应用(如高频交易数据库、实时分析)极端重要!机械硬盘平均延迟几毫秒,而SATA SSD的延迟通常在几十到一百多微秒之间,已经比机械硬盘快了几十倍。但如果追求极致性能,NVMe SSD的延迟可以做到十几微秒甚至个位数微秒,比SATA SSD又快了一个数量级。
(4)容量
硬盘能存多少数据。机械硬盘单盘容量可达 20TB 以上,SSD 目前也在快速追赶(单盘 8TB、16TB 已常见),但单位容量成本上机械硬盘仍有优势。
HDD和SSD性能差距很大,在技术选型时,如何抉择呢?
- 高并发在线业务(数据库、消息队列、缓存持久化) 这类场景对随机读写和延迟极度敏感。用 HDD 会导致数据库查询动不动几百毫秒,Kafka 消息堆积时 I/O 吃满。必须上 SSD,最好是 NVMe 接口的高性能 SSD。比如,MySQL 的 InnoDB 引擎,其数据文件、redo log、undo log 都需要高性能存储,否则整个业务都会被拖慢。
- 海量数据存储与归档(日志、冷备份、对象存储) 这类场景对容量要求高,对随机读写性能不敏感,多是顺序读写。例如,每天几十 TB 的访问日志,存到 SSD 上成本太高,而且根本不需要那么高的 IOPS。此时机械硬盘是经济实惠的选择。可以配合分布式存储(如 HDFS)来提升吞吐量。
- 混合场景 很多系统既有热数据又有冷数据,可以采取分层存储。比如,在 Kafka 中,最新的消息(热数据)放在 SSD 上保证高性能消费,旧的消息(冷数据)自动转移到 HDD 上节省成本。又或者,数据库的 binlog 和慢日志放到 HDD,数据文件放在 SSD。
4. 网络
对于分布式系统来说,系统之间的通信都要经过网络,因此,网络的性能也是需要特别关注的。网络我们一般关注带宽和延迟两个指标。
(1)带宽
带宽指的是每秒传输的数据量,单位bit per second,缩写为bps,千兆网就是1000Mbps,也就是1Gbps。注意,带宽的单位是bit,而非byte,也就是说,带宽为100Mbps的网络的理论最高下载速度为100/8=12.5MB/s,不过,受设备性能、线路衰减、高峰期拥塞影响,实际仅达理论值60%-80%。视频类应用往往消耗大量网络带宽。
(2)延迟
延迟的单位是毫秒,指的是数据包从节点A 点跑到 B 点再回来的时间,它衡量的是响应速度。我们平常打游戏的时候,感觉到很卡,可能就是因为网络延迟高的原因。一般来讲,靠的越近,延迟越小,局域网内的机器之间通信,延迟要远小于公域网内的机器之间通信。
延迟对在线服务的影响极大。一次 Redis 查询如果要在网络上花费几毫秒,累积起来就会严重影响接口响应时间。更可怕的是,在分布式系统中,一个请求往往要调用多个服务(比如网关→订单→库存→支付),每一跳的网络延迟都会叠加。如果每个服务之间的 RTT(往返时延)是 5ms,五个服务串行调用,仅网络开销就高达 25ms,再加上业务处理时间,用户体验就会明显变差。
5. 常识数字
以上分别讲解了CPU、内存、硬盘、网络的主要性能评价指标,这里我们对于一些常见的计算机操作做一个性能的对比,这样,当你在设计系统架构、编写代码或进行性能优化时,就能更清晰地认识到不同操作的相对代价,从而做出更明智的决策。
这份来自 Jeff Dean(谷歌杰出工程师,写BigTable、GFS、MapReduce的那位)2010年的数据确实非常经典,它清晰地展示了计算机系统中不同操作的相对性能差异。虽然具体数值在今天已显著过时(尤其是磁盘、SSD、网络速度、内存带宽),但层级关系和数量级差异变化不大,有很大的参考价值。
Latency Comparison Numbers
--------------------------
L1 cache reference 0.5 ns
Branch mispredict 5 ns
L2 cache reference 7 ns 14x L1 cache
Mutex lock/unlock 25 ns
Main memory reference 100 ns 20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy 10,000 ns 10 us
Send 1 KB bytes over 1 Gbps network 10,000 ns 10 us
Read 4 KB randomly from SSD* 150,000 ns 150 us ~1GB/sec SSD
Read 1 MB sequentially from memory 250,000 ns 250 us
Round trip within same datacenter 500,000 ns 500 us
Read 1 MB sequentially from SSD* 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memory
Disk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtrip
Read 1 MB sequentially from 1 Gbps 10,000,000 ns 10,000 us 10 ms 40x memory, 10X SSD
Read 1 MB sequentially from disk 30,000,000 ns 30,000 us 30 ms 120x memory, 30X SSD
Send packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 ms
Notes
-----
1 ns = 10^-9 seconds
1 us = 10^-6 seconds = 1,000 ns
1 ms = 10^-3 seconds = 1,000 us = 1,000,000 ns6. 最后总结
本节内容总结了每个程序员都必须知道的硬件常识,特别是CPU、内存、硬盘、网络这些主要硬件的性能评价指标,在做架构设计时,对于性能需求,我们可以合理的进行架构设计的评估和硬件资源选型。
