简介:面向关注系统架构与性能优化的工程师,这份指南系统梳理了大型网站高性能、高并发、高可用架构设计的完整方法论。内容涵盖架构目标(高性能、高可用、可伸缩、可扩展与安全)与分层、分割、分布式、集群、缓存、异步等核心模式,并针对前端、浏览器、应用层、代码与存储逐层给出优化策略;高可用部分涉及冗余备份与失效转移,伸缩与扩展部分则讨论了分库分表、消息队列、分布式服务等常用手段。资源包内含1个docx文档,约3.19MB,文档以七层逻辑架构和大型电商网站演变过程作为实例,展示从中小型系统到成熟平台的成长路径,适合正面临高流量挑战或准备大规模扩展的技术团队参考。已有1124人学习下载,可作为架构设计、性能调优与安全加固的实用案头资料。
1. 架构设计:为什么上来就画大图救不了燃眉之急
接手过线上事故的人都有这种体感:系统撑不住的时候,压垮它的往往不是某一段代码写得烂,而是架构层面从一开始就没给流量留出冗余和退路。我拆过不少大型网站的高并发、高可用架构方案,最深的体会是——架构设计不是画一张漂亮的分层图,而是要在“用户量增长、业务变复杂、故障必然发生”这三个前提下,提前把性能、可用性、伸缩性和安全性这些约束埋进每一层决策里。这份《大型网站高性能、高并发、高可用架构设计指南》覆盖面很全,从架构模式到性能优化、从高可用策略到电商案例的容量预估都有涉及,适合正面临流量增长压力的架构师和开发人员。但它不是拿来即用的操作手册,更像一份“架构决策地图”,需要你带着自己的业务场景去读、去取舍。这篇笔记,我按自己拆解这份资料时踩过的坑和验证过的路径,把它重新走一遍。
2. 先立目标再谈技术:高性能、高可用、可伸缩、扩展性到底怎么排序
2.1 架构目标不是口号,是互相制约的取舍
资料里列了高性能、高可用、可伸缩、扩展性、安全性、敏捷性六个目标,这没问题,但新手容易犯的错是——想把六个目标一次性全做到,结果一个都做不扎实。我见过一个团队,为了追求扩展性,一上来就上微服务和消息队列,结果业务还没起来,排查一个问题要跨五个服务,性能反而比单体架构还差。
这六个目标里,真正的硬约束是高性能和高可用,因为用户能直接感知到;可伸缩和可扩展是手段,是为了在不推翻架构的前提下应对增长和变化;安全性和敏捷性更像是贯穿始终的底线和节奏。所以问自己一个问题:当前阶段,哪个目标是主要矛盾?
如果你的系统日活还不到十万,性能问题大概率出在慢查询和缓存缺失上,这时候搞服务化拆分就是过度设计;如果你的系统正在备战双十一,那高可用和弹性伸缩就是第一优先级,扩展性可以往后放。资料里给了很好的思路:架构目标是随业务阶段动态调整的,不是一成不变的。
我一般会建议团队先做一轮压力测试摸底,拿到当前系统的吞吐量、响应时间、错误率这三个基线数据,再对照业务增长预期,判断瓶颈在应用层、数据层还是网络层,然后才谈得上目标排序。没有基线数据就谈架构目标,等于闭着眼睛开车。
2.2 十个架构模式:分层、分割、分布式、集群是地基,缓存和异步是加速器
资料里列的十个架构模式——分层、分割、分布式、集群、缓存、异步、冗余、安全、自动化、敏捷性——这十个不是平级关系,它们有依赖顺序。分层是所有模式的起点,把系统拆成应用层、服务层、数据层;分割是在分层基础上按业务维度切块,比如把“用户中心”从主应用中拆出去;只有完成了分层和分割,分布式部署才有意义。
集群是分布式部署后的必然选择——每个拆分出来的应用,至少要有两个副本,否则它本身就是单点。负载均衡负责把请求分发到多个副本上,常见做法是Nginx做七层负载均衡、LVS做四层负载均衡,硬件F5虽然稳但太贵,一般中小团队用不上。
缓存和异步这两个模式,是性能优化里见效最快的。缓存的核心逻辑是“热点数据离用户越近越好”,80%的请求落在20%的数据上,把这20%的数据放到Redis或者本地缓存里,数据库压力能降一个数量级。异步解决的是“慢操作拖垮快操作”的问题,比如下单后发短信、扣库存后的积分更新,这些都不适合同步等待,丢进消息队列里异步处理,用户体验和系统吞吐量都能上来。
冗余是数据层的核心策略,主备、主从、多副本,都是为了在故障发生时不让数据丢、不让服务停。安全、自动化、敏捷性这三个模式更像是治理层面的要求——安全要制度化地做扫描和审计,自动化是把重复的运维操作脚本化,敏捷性则是让架构能快速响应需求变更。我的经验是,前六个模式是“硬架构”,后四个是“软治理”,硬架构解决能不能扛住的问题,软治理解决能不能持续演进的问题。
3. 把性能优化拆到每一层:浏览器、应用、代码、存储一个都不能漏
3.1 前端和浏览器优化:减少请求数永远比压缩体积先做
资料把性能优化分成了前端、浏览器、应用层、代码、存储五层,很多人一上来就调JVM参数或者改数据库索引,其实性价比最高的优化往往在最前面——浏览器这一层。
减少HTTP请求数是第一优先级。一个页面如果有一百个资源请求,就算每个只有1KB,握手和传输的开销也远大于把请求数压到二十个以内的方案。具体做法我一般分三步走:
// 合并JS/CSS文件,减少请求数 // 构建工具配置示例(webpack) module.exports = { // 将多个JS模块打包成一个bundle entry: { main: ['./src/index.js', './src/common.js'] }, output: { // 使用contenthash做缓存控制 filename: '[name].[contenthash:8].js' }, optimization: { // 提取公共依赖,避免重复加载 splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors' } } } } }这段配置解决的是“代码层面的资源合并”。splitChunks把第三方库提取成单独的vendor文件,配合contenthash让浏览器在文件内容不变时直接走本地缓存,不用重新下载。参数说明:contenthash:8是八位哈希值,内容变了哈希才变,缓存就不会失效;test: /[\\/]node_modules[\\/]/是正则匹配node_modules目录下的依赖,把它们单独打包。
浏览器缓存策略用对了,能省掉大量重复请求。我最常用的配置是:静态资源的Cache-Control设为max-age=31536000(一年),文件名带哈希值,这样文件名不变就永远命中强缓存;HTML页面本身不设缓存,保证每次都能拿到最新的引用。
CDN和反向代理也是浏览器层的重要优化,但我不建议一开始就上。先把请求合并和缓存策略做对了,再上CDN,效果会好很多。顺序反了的话,你会发现CDN回源率居高不下,因为源站根本没设置有效的缓存头。
3.2 应用层优化:缓存、异步、集群三板斧的落地顺序
应用层的优化手段,资料里点到了缓存、异步、集群,但没有说清楚先做哪个。我的经验是:先做缓存,再做异步,最后才是加机器。
缓存为什么排第一?因为它直接砍掉了数据库的重复查询。来看一个最典型的代码:
// 使用Redis做分布式缓存,减少数据库压力 public class ProductService { // 注入Redis客户端 private RedisTemplate<String, Object> redisTemplate; // 注入商品Mapper private ProductMapper productMapper; public Product getProductById(Long productId) { // 先查缓存 String key = "product:" + productId; // 缓存命中则直接返回 Product product = (Product) redisTemplate.opsForValue().get(key); if (product != null) { return product; } // 缓存未命中,查数据库 product = productMapper.selectByPrimaryKey(productId); if (product != null) { // 回填缓存,设置10分钟过期 redisTemplate.opsForValue().set(key, product, 10, TimeUnit.MINUTES); } return product; } }这段代码的逻辑很简单:先查Redis,命中就直接返回;没命中才查数据库,查到后回填缓存并设置过期时间。参数说明:key的格式product:123是规范缓存键的做法,方便按业务维度管理;过期时间10分钟是折中值,太短缓存命中率上不去,太长会面临数据不一致。这里有个细节容易被忽略:过期时间要加随机偏移量,比如10分钟±30秒,否则大量key同时过期,数据库会被瞬时冲垮。
异步化放在第二,因为它的改造范围比缓存大。同步改异步最常见的场景是“下单后发通知、更新库存、记录日志”这一串操作。经验做法是先把这些操作里“不重要的”“可延迟的”挑出来,丢进消息队列:
// 使用RocketMQ做异步化处理 public class OrderService { // 注入消息生产者 private RocketMQTemplate rocketMQTemplate; public boolean createOrder(OrderDO order) { // 核心流程:写入订单(同步) boolean result = orderMapper.insert(order); if (result) { // 非核心流程:发送异步消息 // 主题:order-after-create // 消息内容:订单ID和用户ID Message<String> message = MessageBuilder .withPayload(JSON.toJSONString(order.getId())) .build(); rocketMQTemplate.syncSend("order-after-create", message); } return result; } }这里的核心逻辑是把“下单成功”和“后续处理”解耦。syncSend是同步发送,确保消息确实进队了才返回成功;如果消息队列挂了,下单主流程不应该被影响,所以生产环境要用try-catch包住发送逻辑,发送失败就记录日志,后续补偿。参数说明:主题名order-after-create要按业务事件来命名,方便下游消费者订阅;如果对实时性要求不高的场景,可以用asyncSend异步发送,进一步降低链路耗时。
集群扩容排在最后,是因为它最贵。加机器只能解决并发连接数的问题,解决不了慢查询和缓存穿透——你加十台机器,数据库还是那一台,连接一多,数据库先死。所以我的原则是:缓存和异步都做到位了,再考虑加机器。加机器本身也有讲究,负载均衡策略决定了每台机器的压力是否均匀:
# Nginx负载均衡配置:权重策略 # 配置位于nginx.conf的upstream块中 upstream backend_servers { # weight参数:服务器权重,越高分配的请求越多 # max_fails和fail_timeout:连续失败达到次数则标记不可用 server 192.168.1.101 weight=5 max_fails=2 fail_timeout=30s; server 192.168.1.102 weight=5 max_fails=2 fail_timeout=30s; # 新加入的机器给较低权重,观察稳定性后再调整 server 192.168.1.103 weight=3 max_fails=2 fail_timeout=30s; }weight参数的意义是让性能好的机器多承担请求,新机器先给低权重观察。fail_timeout=30s表示30秒内失败两次,就把这台机器摘除,等30秒后再重新尝试放进来。这里有一个容易被忽略的细节:应用服务器如果做了Session保存,扩容时负载均衡策略不能随便切换——从ip_hash切到轮询,用户Session就丢了,登录状态全部失效。
3.3 存储优化:读写分离、分库分表、NoSQL的适用边界
资料里存储优化列了缓存、固态硬盘、分布式存储、NoSQL,但我拆解后的判断是:存储优化的第一原则是“能不碰数据库就不碰数据库”,第二原则是“数据量大到一定阈值才做分库分表”。
读写分离是最先要做的。一台主库负责写,至少一台从库负责读,主从通过binlog同步。这一步的收益非常直接——把读流量从主库上卸掉,主库的压力立刻减半。但读写分离有个坑:主从延迟。刚写完的数据在从库上可能查不到,这时候需要在业务层做“强制走主库”的标记。我来补一段演示代码:
# 读写分离的强制路由逻辑 # 使用Spring @Transactional注解的readOnly属性区分读写 class UserService: # readOnly=True的查询走从库 @Transactional(readOnly=True) def get_user(self, user_id): return user_mapper.select_by_id(user_id) # 没有readOnly属性的查询走主库 @Transactional def create_user(self, user): user_mapper.insert(user) # 刚写入的数据立刻查询,这里强制走主库 # 最常见的做法:ThreadLocal里设置一个路由标记 RouteHolder.set("master") return user_mapper.select_by_id(user.id)这段代码演示的是一种强制路由策略。@Transactional(readOnly=True)只是框架层面的提示,真正的数据库路由还需要配合AbstractRoutingDataSource实现,根据当前操作是读还是写,自动选择从库还是主库。对于“刚写入就要读”的场景,常见的做法是设置一个短暂的延迟读取窗口,或者用ThreadLocal标记强制走主库查询。
分库分表的时机判断,我一般看两个指标:单表数据量超过2000万行,或者单库连接数长期吃紧。垂直切分是按照业务把表拆到不同数据库,水平切分是把同一张表的数据按某个维度(比如用户ID)散到多个表/库。水平切分最痛的点是跨库查询和跨库事务基本告别了,所以我会再三问团队:真的需要分库分表吗?
NoSQL的定位是补充而不是替代。MongoDB适合文档型数据场景,比如商品详情;HBase适合海量写入和时间范围查询,比如用户行为日志;Elasticsearch是搜索场景必备,但不是所有表都需要放进去。我的建议是:优先级最高的是Redis做缓存,其次才是这些大数据组件,顺序不要反了。
4. 高可用是设计出来的:冗余、失效转移与容量预估的实操思路
4.1 数据层高可用:主备、主从、哨兵、集群怎么选
高可用为什么难?因为故障是必然的——服务器会宕机、磁盘会写满、网络会闪断、程序会OOM。架构要做的不是“防止故障发生”,而是“故障发生后自动恢复”。资料里的核心观点是“冗余备份 + 失效转移”,这八个字,就是数据高可用的全部秘密。
数据层的冗余方案,我帮你按使用场景排个序:
| 方案 | 冗余方式 | 生效耗时 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| 冷备 | 定期备份,故障后人工恢复 | 小时级 | 低价值数据恢复 | 数据丢失窗口大 |
| 热备(异步) | 实时同步,有延迟 | 分钟级 | 高可用要求一般 | 主库故障可能丢数据 |
| 热备(同步) | 强一致的实时同步 | 秒级甚至更低 | 金融、电商交易核心 | 写性能有损耗 |
| 主从+自动切换 | 半自动切换 | 秒到分钟级 | 绝大多数业务 | 需要配置监控和切换脚本 |
| 分布式集群(如Redis Cluster) | 分片多副本 | 毫秒级 | 缓存层 | 需要处理脑裂问题 |
主从加自动切换是性价比最高的选择。我一般用MHA(Master High Availability)做MySQL的主从切换,用Sentinel做Redis的高可用,用Keepalived做应用层的VIP漂移。这里面最大的坑是“脑裂”——主库和备库同时对外服务,数据出现分叉。解决脑裂的常见做法是设置切换的仲裁节点,或者让备库在切换前先检查主库是否真的挂了(比如通过多个网络路径探测)。
4.2 服务层和应用层的高可用:无状态设计是根基
应用层高可用的核心是四个字:无状态设计。什么叫无状态?就是这个请求放到任何一台服务器上处理,结果都一样。一旦服务器需要保存用户Session,负载均衡就不能随意分发,这就是有状态。
线上最常见的翻车现场是:应用服务器本地保存了Session,负载均衡配了轮询策略,用户登录后第一次请求打到A机器,第二次请求被分到B机器,Session找不到,用户被强制踢下线。解决这个问题的方案有三个,按优先级排:
第一,彻底移除本地Session,改用Redis统一保存:
// Spring Boot配置Redis替代默认的本地Session @Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) public class RedisSessionConfig { // 这个配置将Session数据存储到Redis // 应用服务器无论怎么扩容缩容,Session都能共享 }第二,如果不想引入Redis,至少做Session粘滞——负载均衡策略改成ip_hash,让同一个IP的请求始终打在同一台服务器。但这样做会导致服务器压力不均,且服务器宕机时Session照样丢。
第三,Cookie-based Session,把Session数据加密后写入客户端Cookie,服务器不保存状态。这个方案对网络带宽有损耗,Cookie大小限制也严格,不推荐大项目使用。
我的建议是无状态优先。无状态设计意味着你可以凌晨两点直接加十台服务器,而不用做任何数据迁移。
4.3 容量预估实战:从1000万用户倒推出多少台服务器
资料里给了一套容量预估的方法论,从注册用户数推导UV、PV、QPS,最后落到服务器数量。这套方法我实践过多次,非常实用,但注意它是估算,不是精确测量,用来做架构选型绰绰有余。
我按资料里的电商案例,重新走一遍这个推导过程:
已知条件: - 3~5年用户数目标:1000万注册用户 - 二八原则:每天活跃用户(UV) = 1000万 × 20% = 200万 推导过程: 1. 每天总PV = 200万UV × 30次/用户 = 6000万次 2. 集中访问时段 = 24小时 × 20% = 4.8小时 3. 集中访问PV = 6000万 × 80% = 4800万次 4. 每分钟并发 = 4800万 / (4.8 × 60) ≈ 16.7万次/分钟 5. 每秒并发 = 16.7万 / 60 ≈ 2780 QPS 6. 高峰系数(3倍平常量)= 2780 × 3 ≈ 8340 QPS得到这一步,再换算服务器数量。Tomcat默认线程数是150,一台服务器保守估计支撑300个并发,那么:
- 平常时段:2780 / 300 ≈ 10台
- 高峰时段:8340 / 300 ≈ 30台(约等于)
这里要注意,这个估算假设业务逻辑足够简单,每条请求平均耗时在合理范围。如果应用里有个慢接口要等2秒,300并发瞬间就被占满了,实际支撑能力会大打折扣。所以我做容量预估,一定在估算值上再乘一个0.5的安全系数,也就是假设单机只能支撑150并发——宁可多准备机器,也不要在活动当天被流量打穿。
5. 电商架构演进案例:从三台服务器到分布式服务,我们到底加上了什么
5.1 演进路径复盘:每一步解决的是什么问题
资料里给了一条非常完整的架构演进路径:单台服务器 → 应用/数据/文件分离 → 引入缓存 → 应用集群 → 读写分离 → CDN/反向代理 → 分布式文件系统 → NoSQL/搜索引擎 → 业务拆分 → 分布式服务。
我实际拆解后发现,这条路径每一步的驱动力都不同,可以归纳成三类矛盾:性能矛盾、数据矛盾、组织矛盾。
性能矛盾最先出现。单台服务器撑不住压力,所以先做应用/数据库/文件分离,把资源和瓶颈分离开;然后加缓存,把热点数据从数据库里挪走;再然后上集群,把并发请求分散到多台机器。
数据矛盾是第二阶段。用户量增长后,数据库成为新瓶颈,所以先做读写分离卸掉读压力,再分库分表解决单表数据过大的问题;文件数据也越来越多,单台文件服务器不够了,得上分布式文件系统;搜索场景来了,NoSQL和搜索引擎开始介入。
组织矛盾是最后爆发的。代码越来越臃肿,团队越来越大,所有业务挤在一个应用里,改一个模块要全员回归测试。这时候必须做业务拆分,按照产品、购物、支付、评论、客服这样的边界,把大应用拆成多个小应用;拆完发现公共模块重复代码很严重,于是再抽一层分布式服务,把用户、订单、支付这些公共服务独立出来。
5.2 电商案例需求分析:哪些功能是核心系统,哪些可以降级
资料里针对电商案例做了一套需求功能矩阵,我按自己的理解提炼一下。电商网站的核心链路是:商品浏览 → 加入购物车 → 下单 → 支付 → 订单处理。这条链路每一环都不能掉链子,对应的子系统是产品子系统、购物子系统、支付子系统。这三块是核心系统,必须做最高等级的高可用保障。
非核心系统包括评论子系统、客服子系统、接口子系统。评论和客服的特点是容忍延迟,商品发布后评论不会马上出现,客服响应慢几分钟也没关系。接口子系统对接进销存、短信等外部系统,外部系统不可控,必须做超时熔断。
这个“核心/非核心”的划分,最大的价值是让你在流量突发时知道先牺牲谁。秒杀活动来了,服务器资源不够用,一个成熟的架构会做服务降级:关闭评论功能,停止推荐位更新,把资源全部让给下单和支付链路。资料里点到的“优雅降级”,实际操作里就是做熔断、限流、兜底数据这几件事:
# 服务降级的伪代码示例 # 场景:秒杀流量高峰,评论服务被熔断 def get_product_detail(product_id): # 先取商品基本信息(核心数据) product = product_service.get(product_id) # 尝试获取评论数据,但设置超时和兜底 try: # 设置500毫秒超时 comments = comment_service.get_comments(product_id, timeout=500) except TimeoutException: # 超时了,返回空的评论列表 # 不能让评论接口拖垮整个详情页 comments = [] # 尝试获取推荐位(非核心) try: recommendations = recommend_service.get_recommendations(product_id) except ServiceUnavailableException: # 推荐服务挂了,用默认推荐 recommendations = get_default_recommendations() return render_page(product, comments, recommendations)这段代码的核心价值是“核心数据必须取到,非核心数据可以失败”。timeout=500参数是关键——不给评论服务超过500毫秒的等待时间,超时就当它不存在。这里最常见的错误是层层try-catch都失效,原因通常是超时设置没有透传——调用线程等到了500毫秒,但底层的数据库连接可能还在跑,线程池里的线程慢慢被耗尽,最终拖垮整个应用。
5.3 容量预估后的架构决策:服务器数量、缓存层级、数据切分如何联动
容量预估不只是为了算服务器数量,它应该驱动一系列架构决策。假设我们通过预估得出高峰QPS是8340,那么从这些数字能推导出一整套设计:
缓存策略:如果80%的请求能命中缓存层,实际打到数据库的QPS只有约1668。这个量级是MySQL单主库能扛住的,所以缓存是第一道防线。
应用集群规模:高峰需要30台应用服务器,这意味着负载均衡要支持至少30个后端节点,Nginx完全可以胜任。但30台机器只为了秒杀活动准备,平时用不上,所以需要弹性伸缩能力——公有云环境下按CPU使用率自动扩缩容,平时保持10台,压力上升自动加到30台。
服务拆分与降级策略:业务拆分后,产品、购物、支付、评论、客服五个子系统分别部署。秒杀场景下,评论和客服必须能快速降级,把资源让给核心链路。
数据层设计:数据库读写分离后,读库支撑详情的访问,写库主要承接下单和支付,两者可以独立扩缩容。如果订单数据量增长快,订单库需要按用户ID做水平切分,比如分16个库,每个库再分若干表。
我给你补一个缓存层级设计的典型参数,这部分资料没有细讲,但实际落地时非常关键:
| 缓存层 | 缓存内容 | 容量 | 过期策略 | 命中率目标 |
|---|---|---|---|---|
| 浏览器本地缓存 | 静态资源(JS/CSS/图片) | 视用户设备而定 | 文件名哈希,最长一年 | 高 |
| CDN | 页面静态化内容、图片 | 按流量购买 | 自定义,一般15-30分钟 | 高 |
| Nginx本地缓存 | 热点商品详情片段 | 几百MB | LRU淘汰 | 中 |
| Redis分布式缓存 | 全量商品详情、用户会话 | 几十到几百GB | TTL,10分钟+随机偏移 | 核心,尽量高 |
| JVM本地缓存 | 数据字典、分类树 | 几百MB | 定时刷新 | 高 |
这个多级缓存的架构,核心思路是让请求在最靠近用户的地方就返回,层层递减,最后剩下的少量请求才打到应用和数据库。参数不是固定的——业务侧重点不同,缓存层级的深度和容量都要调整。
6. 避坑与常见问题:架构设计、容量预估和落地中的典型踩坑记录
6.1 踩坑记录一:缓存穿透冲垮了数据库
- 现象:某个商品ID的详情接口被刷,数据库QPS暴涨,直接被打挂。我们的第一反应是扩容应用服务器,结果没有任何改善,因为瓶颈在数据库。
- 原因:缓存里没有这个key(数据不存在),每次请求都直接打数据库。攻击者专门请求不存在的商品ID,缓存永远 miss,数据库被无效请求淹没。后来还叠加了缓存雪崩——一批key同时过期,正常用户的请求也打到了数据库。
- 解决:三件事同时做。第一,对不存在的key也缓存一个空值(比如“EMPTY”),过期时间设置为60秒;第二,使用布隆过滤器,将所有商品ID预先加载到过滤器里,查询前先判断ID是否存在,不存在的请求直接返回;第三,过期时间加随机偏移,避免大量key同时失效的问题。从那以后,我只要做缓存设计,这三件事默认全上。
6.2 踩坑记录二:加机器之后性能反而下降了
- 现象:双十一前为了应对流量,从10台应用服务器扩到30台,结果接口平均响应时间反而从200ms涨到了400ms,用户体验更差了。
- 原因:多个应用服务器同时连数据库和Redis,连接数成倍增加。数据库的连接池被打满,连接排队时间变长;Redis的连接数也到了上限,部分请求阻塞在获取连接上。说白了,短板不在应用层,而在数据层。
- 解决:扩容必须是全链路的。应用扩容之前,先确认数据库连接池上限、Redis最大连接数是否够用;如果不够,要么同步扩容数据层的连接配额,要么先在代码层做连接池复用优化。我后来给自己定了一条规矩:扩容前先画出完整链路,确认每一层都有余量,再动应用层的机器。
6.3 踩坑记录三:Nginx配置了轮询,用户Session全丢了
- 现象:新上线了一组应用服务器,测试的时候发现,用户登录之后点击几个页面就掉线,重新登录后过一会儿又掉。
- 原因:应用服务器默认使用本地Session保存登录状态,Nginx的负载均衡策略是轮询,同一个用户的请求被分发到不同服务器,Session信息在每台服务器上各自独立,换一台机器就找不到了。
- 解决:两个方案。长期方案是把Session统一迁移到Redis,实现真正的无状态;短期方案是把Nginx策略改为ip_hash,让同一个IP的请求固定打到同一台服务器。另外在Spring Boot项目里加了一个全局过滤器,拦截所有带SessionId的请求,自动从Redis里重新绑定Session,这样轮询也没跑了。从这个坑之后,我接手任何项目的第一件事就是检查Session存储方案。
6.4 踩坑记录四:分库分表后发现跨库查不了数据
- 现象:订单表按用户ID分到16个库,上线后运营提了一个需求——按订单号查询所有订单信息,然后发现订单号的主键是自增的,没有好走的维度,只能遍历16个库去查,查询慢得无法接受。
- 原因:分库分表时只考虑了写入维度的分散(按用户ID),没有提前设计查询维度的支持。订单号查询场景没有预先规划路由规则。
- 解决:增加一张“订单号→用户ID”的映射表,查询时先查映射表拿到用户ID,再按用户ID路由到对应的库。或者干脆在订单号里带上用户ID的拆分维度,比如订单号的前几位就是用户ID经过哈希后的值,这样订单号本身就能推导出分库路由。这个教训让我养成一个习惯:分库分表设计时,必须把所有高频查询维度列出来,每个维度都要有对应的路由方案。
6.5 踩坑记录五:容量预估做的太乐观,活动当天被打穿
- 现象:促销活动开始前按资料里的公式算了一遍,乐观预估8台服务器够用,留了2台冗余。结果活动一开始,QPS远超预估,数据库CPU直接满负荷,应用服务大面积超时。
- 原因:预估公式本身没错,但用错了参数。分母的“单机支持300并发”是按简单业务逻辑假设的,实际业务中一个详情页的查询要聚合商品、库存、价格、优惠信息,一个请求内部要打4-5次数据库,300并发变成50并发都吃力。另外高峰系数按3倍太乐观了,电商活动高峰往往是日常的10倍以上。
- 解决:修正两个参数:单机并发能力从300下调到150(保守系数0.5);高峰系数从3倍上调到10倍。同时压测先行,用JMeter从单机开始压,测出真实的单机QPS上限,再用这个实测值去做容量预估。从那以后我每次做预估都会在结果后面打一个问号,刘确认有实测数据兜底才签方案。
7. 进阶验证:用压测数据校正架构决策,而不是靠“感觉”
架构设计到这里,你手里应该已经有了完整的目标体系、技术选型、容量预估和避坑经验,但还有一个关键环节没有做完——如何验证这套架构真的能扛住预估的流量。这一步不做,所有理论都停留在纸面上。我最后想分享一个具体技巧:用压测数据来反向校正架构决策。
压测不是随便跑两个脚本看结果,而是要有节奏地推进。我一般按这三个步骤来:
第一步,单机压测摸清基线。用JMeter或wrk对单台应用服务器做阶梯加压,从100并发开始,每档增加50并发,记录QPS、响应时间、错误率三个指标的变化。这个环节的关键是找到拐点——并发数加到某个值以后,QPS不再线性增长,响应时间开始急剧上升,这个拐点就是单机能力的真实上限。用这个数据重新校正第4章的容量预估,比拍脑袋靠谱一百倍。
第二步,全链路压测找瓶颈。在测试环境用与生产几乎等量的数据和配置,模拟真实流量,从负载均衡到应用、缓存、数据库全链路打一遍。这时候不需要精确模拟业务数据,重点是观察哪一层先撑不住。数据库CPU先飙到90%?说明SQL和索引有问题。Redis连接数先满?说明连接池配置要调整。应用线程池先排队?说明业务代码里有慢操作。每一层都有监控指标兜底,压测过程中盯紧这些指标的变化趋势。
第三步,故障演练验证高可用。压测之后,直接在测试环境拔掉一台应用服务器的电源(或者kill进程),验证负载均衡是否能自动剔除故障节点;再拉起节点,验证新节点是否能自动加入集群;再试一次杀掉主数据库,验证主从切换是否在预期时间内完成。这种演练的价值在于,它测试的不是单个组件的能力,而是整个系统的故障响应链路。
容量预估的公式、高可用的策略、性能优化的手段,这些知识都很重要,但它们只是“纸面能力”。架构能力真正的分水岭,是你敢不敢在压测报告和故障演练记录上签下自己的名字。
我自己的习惯是,每次上线大促前,强制走一遍这个流程:拉出压测报告 → 对比容量预估的数字 → 标注偏差超过20%的参数项 → 找到偏差原因 → 修正下一轮的预估模型。循环迭代过几轮之后,预估的准确率会明显提升,系统的真实能力也会越来越清晰。这套方法论是拆这份指南后,我觉得最值得带走的成果,希望帮到你。
本文还有配套的精品资源,点击获取