软件工程架构

【后端架构基础:如何设计可扩展的软件系统 | 负载均衡 / 微服务 / 缓存策略 / 系统扩展】

软件工程两年剧变,是时候停下来聊聊架构了

软件工程这两年变化真的太快了,尤其是最近这几个月。我觉得现在是时候停下来,好好想想到底发生了什么。

现在满世界的机器都在替我们写代码。我们跟它们聊聊天,它们就把代码写出来了。我自己每天也在用,这感觉真是太棒了,效率确实提升了不少。但有时候,我们也会看到那些系统崩溃和报错的消息,那些大型平台上到处都是错误,而且越来越频繁。

所以,我想和大家分享一些关于软件架构的知识。我觉得,现在比以往任何时候都更需要理解我们在构建什么 ,而不仅仅是知道怎么去构建它。是的,理解"做什么"变得至关重要。

我的油管频道上有很多视频,我一直都会给大家讲解原理。哪怕是在 AI 出现之前,我也会讲理论,讲每个角色背后的利弊权衡。这对于软件工程来说是非常深入的探讨。因此,我想给那些可能不太懂技术的朋友们,做一个软件架构的入门介绍。没错,就是软件架构。

动手构建:从一台服务器开始

在这个视频里,我们要动手构建一个东西。

首先,我们会尝试定义什么是软件架构,然后一步一步地在概念层面搭建一个类似谷歌网盘的东西。我们会从最小规模开始------只有一台服务器,然后逐步演进到微服务架构。在这一路上的每一个决策,我都会尽量提炼出我的想法,并展示背后的原因。

对我来说,谈到软件设计,我总会引用 Martin Fowler 的观点。我会把文章的链接放在描述区里。但我想先试着定义一下什么是架构,以及它为什么有用,然后再讲你接下来要做什么。

视频后面的部分,我们将一起设计一个谷歌网盘的克隆版,功能包括上传文件、下载等等。我们从非常小的规模开始:一台服务器,数据就存在服务器上。然后逐步演进到单体架构,期间会讨论限流、缓存、水平扩展、垂直扩展等等很多内容。我们的目标是展示这些决策逐步演化的过程,以及思考软件设计的多种方式。这不只是代码的事,而是要用系统思维去考虑问题,同时我们也可以用系统思维来考虑编码这件事。

架构为什么重要?

好了,话不多说,我先给大家划一下那篇文章的重点:

好的架构非常重要,否则将来要实现新功能的时候,会变得又慢又贵。

这其实就是我们大多数人在构建生产级软件时的切身体会,对吧?如果架构不好,后续的维护和扩展就会举步维艰,业务就会受到影响。这就是架构之所以重要的真正原因------它要支撑业务,这样收入才能持续,业务才能平稳运行,我们也不用给客户发那种"系统故障"的道歉消息。当然,我不是在怪这些公司,它们很清楚自己在做什么。

这基本上会是一次速成课。我们会一步一步、循序渐进地演进。当然,没有哪个方案是完美无缺的。我不想过度设计,而是想教大家有意识地进行决策的过程。某个架构可能最适合你目前所在的公司阶段,也可能不适合。所以这才是决策的关键所在------你需要理解其中的利弊。

第一课:数据与计算分离

我们从一个非常简单的例子开始。这里我有一个服务,这大概就是你第一个程序可能的样子:只有一个服务器,然后有用户。

用户会向这个服务器发出请求。很简单,就当这是我们的谷歌云端硬盘,所有数据都存放在服务器里,这是一个单一实例。这样是可行的,运行得很好。

但我开始提几个问题了:如果服务器挂了怎么办?以及如果你想扩容该怎么办?

我们先看第一个问题。因为你看,我们的数据和服务器是耦合在一起的,数据实际上就在服务器内部。如果服务器挂了,数据也会丢失。当然,你可能已经知道怎么解决了------加个数据库就行了。

但这里的重点不只是加个数据库,而是你要把服务器解耦,让它变成无状态,然后连接到数据库。

现在,当我们发出请求时,我们会从数据库获取数据,这样数据就不会丢失了。我们解决了第一个难题,也就是持久化数据的问题。

第二课:横向扩展与负载均衡

这是我们的第二个问题。如果你真的把这个带数据的实例扩展开来,比如出现两台服务器,我们要做水平扩展,那么就会出现重复数据的问题。如果一个用户向一台机器写入数据,另一台机器并不知道这件事。

这个问题正是通过有一个解耦的数据库实例来解决的。这意味着我们可以对服务器和用户进行扩展,不管用户使用的是哪个实例都没有关系,因为我们有唯一的一个关系型数据库。这就是软件工程中经典的关注点分离:服务器负责服务用户,数据库负责存储数据。

核心概念是:数据与机器耦合,无法扩展,无法容错。

现在让我们想象一下,我们的应用运行得很好,获得了大量用户,但我们收到了投诉,说应用响应很慢。服务器没有处理好所有的请求,那我们就来扩展它。

当我们添加不止一台服务器时,问题就多了。我们开始进入一种分布式架构 。我们需要一个中间机制来决定哪台服务器来为用户服务,这通常就叫负载均衡器。它的任务就是将流量重定向到健康的机器上。

比如,它可以采用轮询算法,在服务器之间轮流切换,让它们均分流量。但流量真的是均等的吗?如果一个用户在上传文件,另一个只是在下载文件,它们消耗的资源是不同的。所以负载均衡器也可以更智能一些,通过健康检查来了解各服务器的运行状态。

而且,负载均衡器还能根据路径名来做路由。如果你的 API 是 POST 请求且用于上传,你就可以直接把请求导向专门处理文件上传的服务器。为了不多花时间,我再提几种实现方式:加权轮询、粘性会话(Sticky Session)和最少连接数。

这里我演示了一段 Nginx 的负载均衡配置,你可以看到轮询和粘性会话在实际中是如何工作的。知道了如何协调服务器之间的流量,我们就可以开始疯狂扩展了。

这里需要区分两种扩展方式:横向扩展 ------增加更多的机器;垂直扩展------提升单台服务器的配置,比如更多内存、CPU。大多数情况下,两种方式你都会用到。而自动伸缩器通常能让你的负载均衡器实现自动伸缩,根据流量大小来增减机器数量。

第三课:微服务与 API 网关

现在假设我们的应用开始盈利了,我们招聘了更多的人,有了更多的工程师。同时,我们也遇到了新问题:有些端点的响应速度变慢了。我们打开了一扇通往全新世界的大门,一个危险而又强大的世界------微服务

我们从文件上传变慢这个痛点开始。通过拥有多个专门的服务,每个服务只精通一件事,我们来解决这个问题。所以,我们试着为我们的谷歌网盘克隆版创建一些服务:

  • 文件服务:负责处理上传。
  • 通知服务:负责推送通知。
  • 身份验证服务:只管身份验证这一块。
  • 实时同步服务:处理多设备间的实时同步。

这就引出了一个问题:用户向 /api/auth/login 发请求,负载均衡器是怎么知道该路由到哪里的?也许负载均衡器并不是干这活的料。而且,身份验证的逻辑到底应该在哪里做?

因此,我们加入一个新组件------API 网关

API 网关收到一个请求,分析它,并将其路由到正确的服务。不仅如此,它还能聚合多个响应。基本上,网关是唯一一个可以和我们的系统通信的入口,所有内部服务器都不会向外部暴露端口。它们只会待在这个虚拟网络(VPC)内部。客户和我们业务沟通的唯一途径就是通过这个网关。

现在我们有了这种分布式架构,来深入探讨一下认证。大体流程是:客户端请求到达网关,网关验证 JWT 令牌的有效性。如果只是验证令牌,我们可能不需要专门去调用认证服务,在网关层就能做。而当用户需要生成 token(登录)时,请求会被网关重定向到认证服务,认证服务用私钥签名一个 token。这就是认证 ,问的是"你有没有权限访问这个应用"。而授权问的是"你有没有权限上传文件",它们是不同的,每个服务都可以有自己的授权层级。

第四课:文件上传与对象存储

现在来理解一下到底怎么上传文件。你不能直接把文件上传到你的关系型数据库里,也不应该把所有数据都流式传输到你的服务器上。

常见的做法是使用对象存储(比如 S3 或谷歌云存储桶)。整个流程是:客户端先发请求给 API 网关,附带文件名、大小等元数据。网关访问文件服务,文件服务在关系型数据库里生成一条文件元数据记录。然后,它调用对象存储,获取一个带有短期有效期的、直传到存储桶的链接。客户端拿到这个链接后,直接开始把文件流式传输到存储桶。

这个方案完全绕开了我们的核心服务器,直接访问存储桶,完美解决了文件上传把服务器挤爆的问题。

第五课:服务间通信与消息代理

当文件到达存储桶时,存储桶会向我们的内部系统发送一个事件,比如"文件已上传"。随后,我们需要执行一些操作,比如让缩略图服务生成预览图,让实时同步服务更新其他设备,让通知服务发推送。

这就是服务之间的通信。但让对象存储直接去调用所有这些服务是不现实的。比如,如果缩略图服务宕机了,请求超时了,我们刚上传的视频就会没有缩略图,而且我们还不知道为什么。这种直接的同步调用是行不通的。

一个更好的解决方案是,在中间加一个角色------消息代理(Message Broker),比如 Kafka、RabbitMQ。它就像一个中间人,负责接收并持久化消息,然后可靠地传递给相应的服务。如果消息投递失败,它会重试,或者放入死信队列,并触发警报。

这再次体现了关注点分离,只不过是在一个更大的规模上。

第六课:缓存、CDN 与限流

最后,我想以缓存、CDN 和限流来收尾。当我们的应用有了大量用户,就开始进入优化的领域了。你不应该过早优化,但现在正是时候。

设想一个经典场景:每天有 500 个用户试图打开同一个文件。如果我们每次都重复"网关 -> 文件服务 -> 数据库 -> 对象存储"这一完整流程,就会一直浪费算力。

我们先从缓存文件元数据下手。可以用 Redis 这样的内存键值存储系统,缓存文件名、属性这些几乎不变化的信息。算法大致是:先查缓存,有就直接返回;没有再去查数据库,然后把结果存入缓存再返回。这能避免查询数据库。

但你不能把整个 200MB 的图片放进 Redis,内存太贵了。那怎么缓存视频、图片等静态资源呢?这里就要用到 CDN

CDN 在全球有成百上千个边缘节点。当用户发出请求时,他们不再连接到你的服务器,而是直接连接离他们最近的 CDN 节点。第一个用户可能会经历完整的流程,但 CDN 会把这张图片缓存起来。接下来,其他所有用户都能从 CDN 的缓存中极快地拿到这张图片,延迟可能只有 20ms,而不是 1 秒钟。这对用户体验和节省业务成本来说,是至关重要的基石。

最后简单说一下速率限制。一旦开始扩展,就必须有它,否则恶意用户可能耗尽你的资源。我们通常会有一个速率限制器,它能识别用户(通过 IP 等),然后往像 Redis 这样的缓存里写入计数。比如,用户 123 在过去一分钟内发出了 5 次请求,如果他在一分钟内达到 10 次请求,就会收到一个 HTTP 429 状态码,表示他被限制了。

总结:架构是理解问题本身

在整个职业生涯里,这些模式都会成为你的老朋友。我特别想让你明白,你可以从一个非常简单的架构一步步演进成更复杂的系统,而在这个过程中,我们就能理解系统里每个决策的利与弊。

再说一遍,架构不光是基础设施的事。 你也看到了,我们聊缓存的时候,还涉及到了算法、产品决策、用什么类型的数据库等等。所以它其实是理解问题本身,理解领域,然后构建出最合适的软件方案,并且以后还能扩展。

相关推荐
张忠琳1 小时前
【NVIDIA】NVIDIA k8s-device-plugin v0.19.3 配置API模块深度分析之二
云原生·容器·架构·kubernetes·nvidia
心念枕惊2 小时前
【Agent Harness】Gliding Horse 整体架构拼图:当 AI Agent 有了自己的操作系统
人工智能·架构
梁辰兴2 小时前
软件工程:需求分析的过程
软件工程·需求分析·需求管理·需求规格说明书·过程概述·需求获取·需求验证
2601_960567962 小时前
电商套图批量生成的效率瓶颈量化——逐图架构与流水线架构的性能对比
架构
LONGZETECH3 小时前
新能源汽车动力系统仿真硬核拆解:五系统分层架构的设计逻辑与技术权衡
c语言·开发语言·架构·汽车·汽车仿真教学软件·汽车教学软件
Zacks_xdc3 小时前
【从0开发一个 Agent】第十七章:完整项目回顾与架构终章
人工智能·架构·agent·next.js
Slice_cy3 小时前
Mint 自研框架设计与实现:从重复开发走向配置驱动(二)
前端·后端·架构
誰能久伴不乏3 小时前
深入理解 C++ 多态:对象模型与底层机制解析
开发语言·c++·架构
得物技术4 小时前
推荐系统体验的数字化突破:得物自动化评测平台的技术实践|AICon 文章整理
算法·架构·llm
奇树谦5 小时前
工业制造领域中的 AAA 架构:是什么、为什么用、有哪些代价,以及如何避免过度设计
架构·制造