Appearance
第 14 讲|企业架构形态
公司里的系统不会都长一个样——内部小工具可能就是一台机器加一个数据库,核心交易系统则涉及多机房、灰度发布、审计、容灾、专门的发布流程。架构形态由业务量、可用性要求、团队规模、历史包袱共同决定,不存在一种通用的"标准架构"。
单机、主备、集群、读写分离、分库分表、缓存、消息队列、微服务、容器化——这些形态是在不同压力阶段被逐步引入的。搞清楚每种形态各自解决什么问题、什么时候该升级到下一阶段,比直接套用一套"高大上架构"更重要。
本讲按业务规模递进,从单机到大型分布式架构,讲清楚每一种形态的适用场景、关键能力、典型陷阱。
一、单机形态
最简单的形态——入口、应用、数据库、文件全部在一台或少量几台服务器上:

单机形态的合理场景
- 内部小工具、运维平台
- 临时搭建的环境
- 项目早期阶段
- 测试 / 演示环境
- 个人项目、博客
- 边缘节点(IoT、门店本地)
单机不是"落后",而是合理简单。在适合的场景下,单机的运维成本、调试便利性、链路清晰度都远超复杂架构。
单机最大的优势是路径短
- 所有日志在一台机器上 →
tail -f /var/log/*一处搞定 - 配置在一处 → 修改不需要分发
- 备份和恢复流程简单 → 一份镜像 + 一份数据库就完整
- 调试方便 →
ssh进去什么都能看
单机要补的不是"加机器",是底线能力
单机模式真正需要的不是"扩展",而是底线能力:
| 能力 | 解决什么 |
|---|---|
| 监控告警 | 服务存活、磁盘、内存、CPU 告警 |
| 备份 | 数据库定时备份 + 异地存储 |
| 重建步骤文档 | 机器挂了后如何快速重建一台 |
| 服务自启动 | systemd 配置,机器重启服务自动起来 |
| 日志轮转 | logrotate 防止日志撑爆磁盘 |
| 关键配置版本管理 | 配置进 Git,丢了能恢复 |
单机不丢人,单机没备份才丢人——能用简单方案解决的问题,不要用复杂方案。
单机的天花板
单机形态的限制:
- 单点故障 — 这台机器挂,全停
- 资源上限 — 单机配置有物理上限
- 发布影响 — 发布时全部用户都跟着停
- 故障域大 — 一个组件出问题影响全局
业务真正增长到需要 99.9% 可用性、流量增长到单机扛不住时,才考虑升级到下一阶段。
二、主备形态
主备(Active-Standby)给关键组件准备一个接管节点——主节点正常服务,备节点同步数据或待命:
常见的主备组件
| 对象 | 主备实现方式 |
|---|---|
| 数据库 | 主库写入,从库同步,故障时提升从库为主 |
| 入口负载均衡 | Keepalived + VIP 漂移 |
| 文件服务 | 主备存储 + 同步复制,或共享存储 |
| Redis | 主从 + Sentinel(哨兵) |
| 共享存储(NAS/Ceph) | 主备节点共享后端存储 |
主备不只是"多装一台机器"
真正可用的主备方案需要一整套配套:
| 配套能力 | 作用 |
|---|---|
| 心跳检测 | 持续监控主节点状态 |
| 切换脚本 | 自动化执行切换流程 |
| 旧主隔离 | 防止脑裂(下面详述) |
| 客户端连接刷新 | 应用连接池主动重连新主 |
| 切换后验证 | 写入测试、关键路径测试 |
| 切换演练 | 定期手工触发切换,验证流程可用 |
任何一环缺失,真故障时切换都做不下去。
数据库主备切换的关键:旧主处理
数据库主备切换里最容易漏的是旧主处理。如果旧主只是网络短暂断开,网络恢复后它还以为自己是主,继续接收写请求,而新主也在接收写请求——脑裂(双写),数据冲突。
切换流程必须明确决定:
| 旧主处理方式 | 适用场景 |
|---|---|
| 关停 | 确认无法恢复 / 严重故障 |
| 设为只读 | 旧主可能恢复,但禁止写入 |
| 网络隔离(Fencing) | 通过 iptables 或物理隔离阻止旧主写入 |
| 重新作为从库接回 | 旧主硬件正常,数据用复制追平 |
不同选择对应不同的恢复路径,必须提前想清楚,出事时不能再讨论。
三、集群形态
集群用多个节点共同提供服务。最容易理解的是 Web 集群——多个无状态应用实例挂在负载均衡后面:
无状态应用最适合集群化
无状态(Stateless)的关键特征:
- 实例不保存关键状态
- 任意请求打到任意实例都能处理
- 状态从数据库、缓存、对象存储取
- 挂一个少一个,加一个多一个,扩缩容自由
无状态应用的集群能力是云原生时代所有弹性能力(K8s 自动扩缩、滚动更新、健康检查替换)的基础。
有状态组件的集群:不能一概而论
有状态组件的集群形态因组件而异,不能笼统说"做了集群就高可用了":
| 组件 | 集群形态 |
|---|---|
| MySQL | 主从复制、组复制(MGR)、Galera、分片(Vitess) |
| PostgreSQL | 流复制、Patroni、Citus |
| Redis | 主从 + Sentinel、Redis Cluster(分片) |
| MongoDB | Replica Set、Sharded Cluster |
| Kafka | 多 broker + 分区副本 |
| Elasticsearch | 分片 + 副本 |
| etcd | Raft 一致性 |
每种集群形态都要单独理解:数据归属、复制方式、故障转移机制、扩缩容步骤。MySQL 主从集群、MGR 集群、TiDB 集群,完全不是一回事——运维方法、故障处理、性能特点都不同。
四、读写分离
读写分离把写请求交给主库,读请求分摊到从库,适合"读多写少"场景:

读写分离的核心问题:复制延迟
主从复制是异步或半同步的——主库写入后,从库需要时间同步。这段时间内访问从库会读到旧数据。
最经典的体验问题:写后立即读
用户:修改头像(写主库,成功)
0.5 秒后:刷新个人页(读请求路由到延迟从库)
从库还未同步:返回旧头像
用户:"我刚改的怎么没生效?"不同请求类型的路由策略
| 场景 | 路由策略 |
|---|---|
| 写操作 | 强制走主库 |
| 写后立即读(同一用户的相关读) | 短时间内强制读主库 |
| 普通列表查询、详情查询 | 走从库分摊压力 |
| 重型报表查询 | 走专门的只读库或离线数仓 |
| 复制延迟过大时 | 暂停读从库,等复制追上 |
| 强一致要求(支付、扣款) | 全部走主库 |
读写分离的工程要求
接入读写分离需要同步设计:
| 能力 | 作用 |
|---|---|
| SQL 路由层 | 按 SQL 类型(写/读)和场景选择主从 |
| 连接池管理 | 主从分别维护连接池 |
| 复制延迟监控 | 持续监控 Seconds_Behind_Master |
| 自动降级 | 延迟过大时自动切回主库 |
| 故障切换流程 | 主库故障时的提升流程 |
"把一个从库地址塞进配置文件"不是读写分离——这种"伪读写分离"出问题时排查极其困难,因为应用根本不知道某次读为什么读到了旧数据。
五、分库分表
当单表数据量太大(千万级以上)、单库写入压力太高、或者历史与在线数据互相影响时,会走到分库分表。

拆分策略
| 拆分维度 | 拆分方式 | 适用场景 |
|---|---|---|
| 按用户 ID 哈希 | user_id % N 决定分片 | 数据按用户均匀分散 |
| 按时间 | 每月一张表,每年一个库 | 日志、订单等时间序列数据 |
| 按业务域 | 用户库、订单库、支付库 | 业务模块清晰 |
| 范围分片 | 按 ID 范围 | 数据有自然范围 |
| 一致性哈希 | 用哈希环 | 扩缩容影响最小 |
分库分表的真实代价
分库分表能分散压力,但代价是查询方式被彻底改变:
| 单库简单的事,分片后变难 | 影响 |
|---|---|
SELECT ... WHERE id = ? | 需要先算出在哪个分片 |
| 跨分片 JOIN | 几乎不可能,改成应用层聚合 |
| 跨分片分页 | 性能崩,所有分片都要查 |
| 全局唯一 ID | 不能用 AUTO_INCREMENT,要 Snowflake 等方案 |
| 全局事务 | 跨库事务复杂,通常放弃 |
| 跨分片聚合 | COUNT、SUM 需要聚合所有分片 |
| 扩容迁移 | 数据重新分布,复杂且影响业务 |
举例:"查所有用户最近 10 条订单"在单库就是简单的 ORDER BY + LIMIT,分片后需要查所有分片再合并排序——性能与复杂度同时崩坏。
走到分库分表前,先做完这些
很多系统其实根本不需要分库分表。在分片前,有一堆更低成本的优化:
单表大 → 加索引、改慢 SQL → 命中索引,问题解决
↓ 仍然慢
归档历史数据(冷数据搬到归档表)→ 在线表减小,问题解决
↓ 仍然慢
加缓存(热点数据进 Redis)→ 数据库压力下降,问题解决
↓ 仍然慢
读写分离 → 读压力分摊,问题解决
↓ 仍然慢
垂直分库(按业务拆库)→ 单库压力下降,问题解决
↓ 仍然解决不了
水平分库分表 → 不得不上的最后一步只有前面所有手段都做完仍然撑不住,才真正进入分片。
分库分表中间件
走到分库分表的项目,需要专门的中间件:
| 中间件 | 类型 |
|---|---|
| ShardingSphere(原 ShardingJDBC) | JDBC 层,Java 生态主流 |
| MyCAT | Proxy 层 |
| Vitess | Proxy 层,YouTube 出品,K8s 友好 |
| TiDB | 分布式数据库,自带分片 |
| OceanBase | 分布式数据库 |
新项目对分片有需求时,优先考虑分布式数据库(TiDB、OceanBase)——比自己用 ShardingSphere 拼一套要省心得多。
六、缓存与消息队列
缓存挡住重复读,消息队列承接异步任务和削峰。这两类组件在架构里位置不同,但常常一起出现。
缓存
| 用途 | 说明 |
|---|---|
| 数据缓存 | 热点数据放 Redis,挡住反复查询 |
| 会话存储 | 用户登录状态 |
| 分布式锁 | 控制并发修改共享资源 |
| 计数器 | 限流、统计、UV/PV |
| 排行榜 | Redis ZSet |
| 去重 | Redis Set、Bloom Filter |
缓存接入后要面对的问题:
| 问题 | 处理 |
|---|---|
| 数据过期与一致性 | 数据库更新后主动失效缓存 |
| 缓存穿透 | 缓存空值、布隆过滤器 |
| 缓存击穿 | 互斥锁、热点 key 永不过期 |
| 缓存雪崩 | 过期时间加随机抖动 |
| 大 key | 单个 value 过大,影响性能 |
详见 第 6 讲 数据库的出现 第五节。
消息队列
| 消息系统 | 适用场景 | 主要监控点 |
|---|---|---|
| RabbitMQ | 后台任务、可靠投递、传统业务 | 队列长度、未确认消息、消费者数 |
| Kafka | 日志流、事件流、大吞吐数据管道 | 消费 lag、分区均衡 |
| Redis Stream/List | 轻量内部任务 | 队列长度 |
| RocketMQ | 国内电商、金融场景 | 类似 Kafka |
异步任务排查的不同思路
接入消息队列后,异步任务的排查思路与同步请求完全不同:
用户:点"生成报表"
接口:立即返回 task_id = 12345
前端:显示"生成中"
后台:消费者从队列取出任务 12345
后台:执行报表生成
后台:更新任务状态为 done
前端:轮询任务状态接口,看到 done
前端:展示报表如果"生成中"一直转圈,排查的不是接口而是:
| 检查项 | 含义 |
|---|---|
| 任务记录 | 数据库里任务的状态是什么 |
| 消息队列积压 | 消费者是否在消费 |
| 消费者实例 | 后台服务是否正常运行 |
| 消费者日志 | 是否有处理失败、抛异常 |
| 死信队列 | 处理失败被丢到死信队列的消息 |
盯着前端页面没用,要去消息队列和消费者侧——这是异步架构排查的基本思维。
七、服务治理
服务一多,服务之间的调用需要统一管理,否则会成一锅粥。服务治理就是这一层的工作:
服务治理的核心能力
| 能力 | 解决什么 |
|---|---|
| 服务发现 | 服务名动态映射到实例地址 |
| 配置中心 | 统一管理所有服务的配置 |
| API 网关 | 统一入口、认证、路由、限流 |
| 限流熔断 | 防止慢服务拖垮上游 |
| 链路追踪 | 看一次请求经过了哪些服务 |
| 集中日志 | 多服务日志聚合查询 |
| 监控告警 | 服务级指标 + 业务指标 |
| 服务网格(Service Mesh) | 把治理能力下沉到代理层 |
服务网格:治理能力下沉
Service Mesh(Istio、Linkerd、Consul Connect)把治理能力(限流、熔断、追踪、加密)做成应用旁边的 Sidecar 代理,应用代码不需要关心这些。
传统方式 服务网格方式
应用 → 治理 SDK 应用 → Sidecar(治理逻辑)
↓ ↓
目标服务 目标服务服务网格的价值:
- 治理能力与业务代码解耦
- 多语言统一治理(不需要每个语言都做 SDK)
- 治理策略集中管理
但 Service Mesh 引入了复杂度,不到一定规模不建议上——小团队的微服务架构用传统 SDK 方式更轻量。
八、完整企业级请求的链路
一条企业级请求可能经过的对象:

公网用户
↓
CDN(静态资源加速)
↓
WAF(安全过滤)
↓
云负载均衡(LB)
↓
API 网关(认证、路由、限流)
↓
Ingress / Nginx
↓
后端服务 A(订单服务)
↓
[配置中心读取配置]
[Redis 缓存]
[消息队列 Kafka]
↓
后端服务 B(用户服务)
↓
后端服务 C(支付服务)
↓
数据库每经过一个对象,都产生日志、指标、Trace 数据。架构图不只画对象,还要标注每一层的日志和指标在哪里收集——这决定出问题时去哪里查。
一次"接口 504"的完整排查路径
现象:用户报"提交订单接口 504"
第 1 步:看入口 Nginx 日志
→ status=504 upstream_response_time=60.000
→ 入口等待后端超时,问题在后端
第 2 步:用入口日志中的 trace_id 拉链路追踪
→ 整个链路:网关 10ms → 订单服务 50ms → 报表服务 60s
→ 慢在报表服务
第 3 步:看报表服务日志中的同一个 trace_id
→ 报表服务的日志显示卡在调用数据库
第 4 步:看数据库慢查询日志
→ 同一时间一条 SQL 扫了 3000 万行,缺索引
定位:报表查询的 SQL 缺索引,加索引后接口恢复正常整个排查链路只用 4 步、几分钟,得益于完整的可观测体系(入口日志 + 链路追踪 + 应用日志 + 慢查询日志)。
如果没有这套体系,排查只能靠猜——这就是为什么"先建可观测,再上微服务"是云原生时代的标配。
九、企业架构的演进规律
总结一下企业架构形态的典型演进:
| 阶段 | 形态 | 典型规模 |
|---|---|---|
| 启动期 | 单机部署 + 单数据库 | 几百到几千用户 |
| 成长期 | 多实例 + 负载均衡 + Redis 缓存 | 几万到几十万用户 |
| 扩展期 | 数据库主从读写分离 + 消息队列 + CDN | 几十万到百万用户 |
| 复杂期 | 微服务 + 容器化 + 完整可观测 | 百万到千万用户 |
| 大型化 | 服务网格 + 多机房 + 分库分表 + 流量治理 | 千万以上用户 |
| 超大型 | 异地多活 + 全球加速 + 复杂数据中台 | 海外大型互联网 |
演进的核心原则
每一步演进都要解决具体业务痛点——不要为了"看起来现代化"提前引入复杂度。
| 错误做法 | 后果 |
|---|---|
| 5 人小团队上 K8s + 微服务 | 运维负担超过业务收益 |
| 业务还在验证阶段就分库分表 | 改业务时改架构,双倍工作量 |
| 月活 1000 的应用搞异地多活 | 成本远超价值 |
| 没有 CI/CD 就上微服务 | 部署成本指数级上升 |
合理的演进节奏:业务驱动技术演进,而不是技术驱动架构升级。看到痛点 → 评估方案 → 渐进式引入 → 验证效果。
技术债与架构演进
每一次架构升级都会引入技术债——旧代码、旧模式、旧组件不会立刻消失,长期与新架构共存。承认技术债,有意识地偿还,而不是假装它不存在:
- 给旧服务设置"维护模式"(不再加新功能,只修严重 bug)
- 安排专门的"还债"周期(每个迭代留 10%-20% 时间)
- 关键路径优先重构,边角不动
- 定期重新评估技术栈,该淘汰的尽快淘汰
健康的企业架构演进,是业务增长 + 技术演进 + 技术债治理三件事的平衡。