Appearance
企业架构形态
公司里的系统不会都长一个样——内部小工具可能就是一台机器加一个数据库,核心交易系统则可能涉及多机房、灰度发布、审计、容灾、专门的发布流程。架构形态跟业务量、可用性要求、团队规模、历史包袱都有关系。单机、主备、集群、读写分离、缓存、消息队列、微服务、容器化——这些形态都是在不同压力阶段被逐步引入的,搞清楚每种形态各自解决什么问题、什么时候该升级到下一阶段,比直接跟一套"标准架构"更有用。
一、单机形态
最简单的形态,把入口、应用、数据库、文件全装在一台或者少量几台服务器上:
内部小工具、临时搭的平台、项目的早期阶段,基本都从这里起步。单机的最大优点是路径短——日志都在一台机器上、配置都在一处、备份和恢复流程也容易写清楚。调试问题非常方便,ssh 进去什么都能看。
单机最需要补的不是"加机器",而是底线能力:监控、备份、重建步骤文档、磁盘容量告警、服务自启动(systemd 或者 supervisord)。没有这些,一次磁盘写满或者误删文件,就能让整个恢复过程变得非常狼狈。单机不丢人,单机不备份才丢人。
二、主备形态
主备形态给关键组件准备一个接管节点——主节点干活,备节点同步数据或者等待切换。最常见的几处:
| 对象 | 怎么做主备 |
|---|---|
| 数据库 | 主库写入,从库同步,故障时把从库提升为主 |
| 入口负载均衡 | Keepalived + VIP 漂移 |
| 文件服务 | 主备存储、同步复制,或者共享存储 |
主备不是只多装一台机器那么简单。要有一整套配套:心跳检测、切换脚本、旧主隔离、客户端连接刷新、切换后的功能验证。任何一环缺失,真故障的时候切换都做不下去。
数据库主备切换里最容易漏的是旧主处理。旧主如果只是网络短暂断开,网络恢复之后它还以为自己是主,继续接写请求,而新主也在接写——这就是脑裂(双写),数据会冲突。所以切换流程里必须明确:旧主是要关停、设为只读、网络隔离,还是作为新从库重新接回——不同选择对应不同的恢复路径,要提前想清楚,不能等出事再决定。
三、集群形态
集群用多个节点共同提供服务。最容易理解的是 Web 集群——多个无状态的应用实例挂在负载均衡后面:
无状态应用最适合集群化——实例本身不保存关键状态,请求打到哪台都能处理,需要的状态从数据库、缓存、对象存储里取。挂一个实例少一个,加一个多一个,扩缩容非常自由。
有状态组件的集群就复杂多了,要看具体分工方式:Kafka 按分区分布到 broker、Redis Cluster 按 slot 分片、数据库可能是主从、组复制、分片或者多主。每一种都要单独理解它的数据归属、故障转移、扩缩容方式——不能笼统地说"数据库做了集群就高可用了",MySQL 主从集群、MGR 集群、TiDB 集群,完全不是一回事。
四、读写分离
读写分离把写请求交给主库,读请求分给从库,主要解决"读多写少"场景下主库压力大的问题:
读写分离最常遇到的坑是复制延迟。用户刚提交一条记录,马上刷新列表,如果列表查询恰好路由到了延迟从库,从库还没同步过来,用户就看不到刚写入的数据——他会以为"提交没成功",然后又提交一次。这种体验问题看起来小,实际很影响用户信心。
通常按请求类型分别处理:
| 场景 | 怎么做 |
|---|---|
| 写后立刻读(用户刚提交完就查) | 短时间内强制读主库 |
| 普通列表查询、详情查询 | 走从库分摊压力 |
| 重型报表查询 | 走专门的只读库或者离线数仓 |
| 复制延迟过大 | 暂停读从库,等复制追上再放开 |
读写分离接入之后,SQL 路由、连接池管理、复制延迟监控、故障切换流程都要一起设计。不是把一个从库地址塞进配置文件就完事——这种"伪读写分离"出问题的时候,排查会让你怀疑人生。
五、分库分表
当单表数据量太大(千万级以上)、单库写入压力太高、或者历史数据和在线数据互相影响的时候,会走到分库分表这一步。
| 拆分维度 | 怎么拆 |
|---|---|
| 按用户 ID 哈希 | 用户 1 到库 A,用户 2 到库 B,均匀分散 |
| 按时间 | 每月一张表,每年一个库(适合日志、订单这种时间序列数据) |
| 按业务域 | 用户库、订单库、支付库各自独立 |
分库分表能分散压力,但代价是查询方式被彻底改变。跨分片查询、跨分片分页、聚合统计、跨库事务、扩容迁移,每一个都很难——比如"查所有用户最近 10 条订单"这种简单需求,在分片之后要查所有分片再合并排序,性能和复杂度都崩了。
很多系统其实根本不需要走到分库分表。在分片之前,有一堆更低成本的优化能做:补索引、改慢 SQL、归档历史数据(把冷数据搬到归档表)、加缓存、做读写分离。只有这些手段都做完还撑不住,才真正进入分片。不要为了"显得高大上"就上分库分表,后期的运维成本会压垮整个团队。
六、缓存和消息队列
缓存用来挡住重复读、消息队列用来承接异步任务和削峰。这俩在架构里位置不一样,但经常一起出现:
| 组件 | 解决什么问题 | 主要排查点 |
|---|---|---|
| Redis 缓存 | 热点数据反复查库 | 命中率、过期时间、内存使用、慢查询 |
| Redis 分布式锁 | 控制并发修改共享资源 | 锁超时、误释放、可重入性 |
| RabbitMQ | 后台任务、可靠投递 | 队列积压、消费失败、重试、死信队列 |
| Kafka | 日志、事件流、大吞吐数据管道 | 消费 lag、分区均衡、消费者状态 |
缓存接入之后要面对数据过期和一致性问题(数据库更新了缓存还是旧值),消息队列接入之后要面对重复消费、消费失败、任务积压问题——这些不是"接入就完了",要持续运维。
举个具体场景:用户点"生成报表",接口立刻返回任务 ID,前端显示"生成中"。如果后台消费者挂了,接口不会报错,但任务状态会一直停在 running 或 pending,前端一直转圈。这时候盯着前端页面没用,要去消息队列看积压、看消费者日志。所以异步任务的排查思路跟同步请求完全不一样,要从队列和消费者入手。
七、服务治理
服务一多,服务之间的调用就需要被统一管理起来,不然会乱成一锅粥。这就是"服务治理"要解决的问题:
| 能力 | 解决什么 |
|---|---|
| 服务发现 | 服务名动态映射到实例地址(Nacos、Consul) |
| 配置中心 | 统一管理所有服务配置和变更记录 |
| 限流熔断 | 防止慢服务把上游拖垮 |
| 链路追踪 | 看一次请求经过了哪些服务、各段耗时 |
| API 网关 | 统一入口、认证、审计、限流、协议转换 |
一条企业级请求可能会经过这么多对象:公网入口 → WAF → 负载均衡 → API 网关 → 几个后端服务 → 缓存 → 数据库 → 消息队列。架构图不仅要能画出这些对象的位置,还要能标出日志和指标分别从哪里来——这决定了出问题的时候去哪里查。
排查问题的时候可以按这条链路一层层走。比如用户报"接口 504",完整排查路径:入口日志看到 upstream 超时 → 网关日志里同一个 trace id 耗时 60s → 后端日志停在调用报表服务那一步 → 报表服务里数据库查询扫了几千万行。这样才知道问题不是浏览器、不是 DNS,而是报表查询拖住了整条调用链——直接定位到根因。如果没有这套完整的可观测体系,排查只能靠猜。