Skip to content

企业架构形态

公司里的系统不会都长一个样——内部小工具可能就是一台机器加一个数据库,核心交易系统则可能涉及多机房、灰度发布、审计、容灾、专门的发布流程。架构形态跟业务量、可用性要求、团队规模、历史包袱都有关系。单机、主备、集群、读写分离、缓存、消息队列、微服务、容器化——这些形态都是在不同压力阶段被逐步引入的,搞清楚每种形态各自解决什么问题、什么时候该升级到下一阶段,比直接跟一套"标准架构"更有用。

一、单机形态

最简单的形态,把入口、应用、数据库、文件全装在一台或者少量几台服务器上:

内部小工具、临时搭的平台、项目的早期阶段,基本都从这里起步。单机的最大优点是路径短——日志都在一台机器上、配置都在一处、备份和恢复流程也容易写清楚。调试问题非常方便,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,前端显示"生成中"。如果后台消费者挂了,接口不会报错,但任务状态会一直停在 runningpending,前端一直转圈。这时候盯着前端页面没用,要去消息队列看积压、看消费者日志。所以异步任务的排查思路跟同步请求完全不一样,要从队列和消费者入手。

七、服务治理

服务一多,服务之间的调用就需要被统一管理起来,不然会乱成一锅粥。这就是"服务治理"要解决的问题:

能力解决什么
服务发现服务名动态映射到实例地址(Nacos、Consul)
配置中心统一管理所有服务配置和变更记录
限流熔断防止慢服务把上游拖垮
链路追踪看一次请求经过了哪些服务、各段耗时
API 网关统一入口、认证、审计、限流、协议转换

一条企业级请求可能会经过这么多对象:公网入口 → WAF → 负载均衡 → API 网关 → 几个后端服务 → 缓存 → 数据库 → 消息队列。架构图不仅要能画出这些对象的位置,还要能标出日志和指标分别从哪里来——这决定了出问题的时候去哪里查。

排查问题的时候可以按这条链路一层层走。比如用户报"接口 504",完整排查路径:入口日志看到 upstream 超时 → 网关日志里同一个 trace id 耗时 60s → 后端日志停在调用报表服务那一步 → 报表服务里数据库查询扫了几千万行。这样才知道问题不是浏览器、不是 DNS,而是报表查询拖住了整条调用链——直接定位到根因。如果没有这套完整的可观测体系,排查只能靠猜。