ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

数据库缓存扩容升级避坑指南:稳定性保障实战

数据库缓存扩容升级避坑指南:稳定性保障实战 数据库和缓存稳定性保障扩容和升级这两个动作看起来简单实际上是最容易翻车的操作。我这些年处理过不少线上事故相当一部分不是业务代码出了问题而是发生在扩容和升级这两个环节。扩容扩出数据倾斜升级升出内存暴涨这些坑我都踩过今天把我的实操经验完整梳理一遍希望对你有所帮助。1. 扩容和升级之前先想清楚这三件事很多人拿到扩容需求第一反应就是“加机器、加内存”实际上这个思路往往会把问题搞得更复杂。我在动手之前一定会先问三个问题瓶颈到底在哪里、扩容的动作会不会引发新的热点、升级会不会改变数据分布和访问行为。先说说瓶颈判断。数据库慢可能是CPU满了可能是磁盘IO扛不住也可能是连接数不够甚至可能是慢查询把资源吃干净了。缓存命中率低不一定是容量不够有可能是key过期策略设置不合理也有可能是热点数据过于集中。我见过一个案例业务方说缓存不够用要加内存结果一查发现大量key设置了相同的过期时间每到整点就集体失效数据库瞬间被打满。这种问题加再多的内存都解决不了。再说数据分布和访问行为的变化。扩容往往意味着数据要重新分片原来落在同一台机器上的数据可能会拆分到多台机器这个时候如果分片键选得不好就会出现数据倾斜。升级则更微妙比如Redis从某个版本升到另一个版本内存编码方式可能发生变化同样一份数据新版本可能多占30%的内存。如果你按照旧版本的内存估算来规划资源上线之后内存直接爆掉。所以我的建议是扩容和升级之前必须先做一轮全量的容量评估和访问特征分析。容量评估不只是看当前的水位还要看增长趋势至少要预留30%到50%的缓冲空间。访问特征分析要看key的分布是否均匀、热点key的访问量占比、大key和大value是否存在这些数据直接决定了你的扩容方案和升级策略。2. 数据库扩容的全流程实操2.1 扩容前必须完成的三项检查数据库扩容不是“加个从库”或者“加个分片”那么简单动手之前有三项检查必须完成。第一项是慢查询和长事务检查。慢查询会占用大量的CPU和IO资源如果在扩容过程中业务流量还在持续打入慢查询会把新加的节点也拖垮。长事务更危险它会导致binlog积压从库同步延迟越来越大扩容过程中做数据校验的时候数据一直对不齐。我通常会在扩容前的两到三周就开始观察慢查询日志和事务运行情况。发现有问题的SQL先优化掉有长事务的跟业务方确认是否可以终止。记住扩容窗口不是从你执行命令那一刻开始算的而是从你决定扩容那一刻就要开始准备。第二项是磁盘容量和IO能力的核查。很多数据库扩容方案需要先做全量备份再恢复到新节点这个过程中磁盘的读写压力会成倍增加。如果源库的磁盘本身已经很紧张备份文件都没地方放扩容就卡住了。我在一次MySQL扩容中就遇到过这种情况5TB的实例磁盘可用空间只剩200GB备份文件根本写不进去最后只能先清理归档日志才把空间腾出来。第三项是网络带宽和延迟的确认。数据库节点之间的数据同步网络是关键路径。如果跨机房扩容带宽不够或者延迟太高从库永远追不上主库扩容就成了摆设。同一个机房内部署也要确认交换机的带宽是否足够之前我在物理机上做扩容就是因为没用千兆网卡数据同步速度上不去一个2TB的实例同步了整整三天。2.2 容量评估的具体计算方法容量评估不是拍脑袋需要用数据说话。我分享一个我自己常用的评估表格你可以直接套用。评估项计算方式说明当前数据总量实际数据文件大小之和不包括binlog和临时文件索引开销约为主数据文件的30%到50%InnoDB引擎下索引和数据是分开存储的binlog空间约为数据变更量的2到3倍根据业务写入量估算临时文件空间预留50GB到100GB用于排序和临时表操作增长预留以上数据总量的30%到50%保证扩容后有足够的缓冲空间举个例子假设你当前主库数据文件是1TB索引按40%算就是400GBbinlog按一天变更量50GB预留2天空间就是100GB临时文件预留80GB增长率按40%算。那么扩容后的目标容量至少是100040010080*1.4大约是2.2TB。如果你规划的新节点只有2TB那就不够还得往上加。还有一点容易被忽略就是连接数的评估。一个数据库实例能支撑的最大连接数不是无限的每个连接都要占用内存MySQL的每个连接大约占用数MB的内存PostgreSQL更高。如果你扩容之后接入了更多的应用实例连接数也会成倍增加这时候不仅要看存储容量还要看连接数的上限。2.3 三种常见的扩容方案对比数据库扩容方案没有绝对的好与坏关键看你的业务场景和可用性要求。我这几年实践下来三种方案最常用。第一种是只读从库扩容适合读多写少的场景。主库继续承担写入从库承担读流量通过负载均衡把读请求分散到各个从库。这种方案实现简单风险低但有一个天然的短板从库的数据是异步同步的存在短暂的延迟。如果业务对数据一致性要求极高比如金融交易系统就不能用这种方案。第二种是分片扩容适合数据量已经非常大的场景。把数据按照某个分片键拆到多个节点上每个节点只承担一部分数据。这种方案能够从根本上解决单库容量和性能的瓶颈但复杂度很高分片键的选择、数据迁移的方案、跨分片查询的处理都是难题。我见过一个团队把订单表按照用户ID分片结果后台需要做跨分片的聚合统计每次查询都慢得离谱最后只能引入额外的汇总表。第三种是数据库原生扩展比如从单机升级到集群模式或者从传统的主备模式切换到分布式数据库。这种方案的好处是数据库层面已经帮你解决了数据分布和高可用的问题你不需要关心分片细节但代价是需要做数据迁移和版本升级操作不当容易出事故。我个人的建议是数据量还处于可控范围的时候优先用只读从库扩容简单、可靠、易回滚。等到数据量真的到了一定规模再考虑分片或者原生扩展不要提前引入复杂度。2.4 数据校验和流量切换的完整动作清单数据校验这个环节很多团队会偷懒直接跳过或者只是简单地比一下行数。这其实很危险行数一致不代表数据一致。我至少会做三层校验。第一层是行数校验用count(*)分别统计源库和目标库的表行数这个快能发现大方向的问题。第二层是checksum校验对表数据计算校验和逐表比对这个能发现具体哪些行不一致。第三层是抽样校验从每张表中随机抽取一部分数据逐字段比对确认字段值完全一致。如果校验发现问题先别急着切换排查数据不一致的原因。常见的原因包括扩容过程中有写入操作没有同步过来、大事务的回放顺序错乱、字段类型在迁移过程中发生了隐式转换。流量切换之前还有一个关键动作做好回滚预案。我的回滚预案通常会包含三步从库切换回主库的命令清单、应用连接的切换脚本、以及缓存预热和降级的开关。切流量的时候我会要求先切5%的流量观察五分钟再切30%再切100%而不是一下子就全量切过去。很多线上问题就是全量切换之后才暴露的等你发现的时候已经晚了。3. 缓存扩容的细节与陷阱3.1 集群模式选择为什么我不建议无脑上Redis Cluster一说到缓存扩容很多人第一反应就是把单机Redis换成Redis Cluster。这个思路本身没错但Redis Cluster带来的复杂度往往被严重低估。我见过不少团队用Redis Cluster之后反而出了更多的问题。Redis Cluster的数据是自动分片的每个key通过hash计算落到特定的slot上数据分布是均匀的。但问题在于如果你的业务里存在热点key或者大量key属于同一个业务模块hash分片之后这些key大概率会落在不同的节点上导致一次业务请求要访问多个Redis节点延迟增加跨节点的事务也没法保证。我在实际项目中更推荐的做法是优先考虑读写分离让主库继续承担写请求从库承担读请求。等到数据量和吞吐量确实到了单实例撑不住的地步再考虑Redis Cluster。而且启用Redis Cluster之前一定要先做key设计评审尽可能让同一个业务模块的key带有相同的前缀让它们落到同一个slot区间减少跨节点的访问。还有一点Redis Cluster的扩缩容过程本身对业务是有影响的。扩容时数据需要从老节点迁移到新节点迁移过程中key的访问会有一小段时间的阻塞如果你的业务对延迟极度敏感这个阻塞就会被放大。所以扩容操作一定要选在业务低峰期迁移的粒度也要控制好不要一次性迁移太多slot。3.2 缓存key分布和热点治理优先级高于资源扩容我发现很多团队犯的最大的错误就是缓存出问题第一反应就是扩容加机器。实际上大量缓存问题跟容量无关而是key的设计和热点治理出了问题。举个例子一个电商系统的大促活动某个商品的详情页被疯狂点击这个商品的缓存key就变成了热点key单台Redis节点上的CPU被打满。你再怎么扩容这个热点key还是只会存在一个节点上流量还是全部打到那一台扩容解决不了任何问题。这个时候要做的是热点key治理。常见的方案包括把热点key复制多份加上随机后缀让请求分散到多个节点或者在应用层做本地缓存比如Caffeine把热点数据缓存在应用进程内直接拦截大部分请求或者对热点key做多级缓存L1用本地缓存L2用RedisL3才是数据库。我强烈建议排查缓存问题的时候先看慢日志和热key分析再考虑扩容。Redis的慢日志能告诉你哪些命令执行时间过长hotkeys参数能告诉你哪些key的访问频率异常。这些数据比单纯的容量数据有价值得多。还有一个容易被忽略的点就是大key问题。一个key的value是几MB甚至几十MB虽然Redis能存但每次读写这个key网络传输和内存开销都非常大。如果大key出现在热点key上问题会成倍放大。治理的办法就是拆分大key把一个value拆成多个小value或者用hash结构替代string结构。对于缓存来说永远不要指望用扩大容量来解决大key带来的性能问题。3.3 缓存淘汰策略与内存预估缓存扩容的时候内存容量是最重要的参数但很多人对内存预估的理解是有偏差的。Redis的内存占用不只是key和value本身的大小还有大量的元数据开销。我之前实测过一个场景100万个字符串类型的keyvalue平均大小是50字节按道理数据本身只需要50MB但实际上Redis占用的内存超过了200MB。原因就在于每个key都要维护一个dictEntry里面有指针、过期时间、LRU信息等这些元数据每个key要占用几十字节。所以缓存容量预估我给一个经验公式预估内存 key数量 * (key平均长度 value平均长度 100字节)。这个100字节就是元数据开销的平均估计。如果你存的是hash、set、zset这类复杂结构开销会更大建议乘以1.5到2的系数。缓存淘汰策略也要在扩容之前想清楚。Redis提供了多种淘汰策略常用的有allkeys-lru、volatile-lru、allkeys-random等。我见过一个事故某个团队把淘汰策略设置成了noeviction结果内存满了之后新的写入直接报错业务方反馈缓存服务不可用。他们以为是容量不够赶紧扩容结果扩容之后还是报错最后才发现是淘汰策略不允许删除任何key。这个教训是扩容的时候一定要同时检查淘汰策略是否合理业务是否能接受key被淘汰。关于缓存预热也要提前准备。扩容之后新节点是空的如果没有预热大量请求会直接穿透到数据库数据库瞬间被打爆。我常用的预热方式有两种一种是从旧节点导出热点key批量写入新节点另一种是通过流量复制工具把旧节点的读流量复制一份到新节点让新节点慢慢热起来。第二种方式更平滑但实现复杂度高一些要根据你的团队能力来选择。4. 升级规范小版本、灰度、回滚三板斧4.1 为什么小版本升级也不能掉以轻心数据库和缓存的升级最让人头疼的就是版本变更。很多人觉得一个patch版本升级而已影响应该不大但这种想法往往会让你吃大亏。我遇到过的最典型的一个案例是Redis从6.0升级到6.2看起来是一个小版本升级但6.2版本修改了Redis的过期key回收机制导致某些场景下内存回收不够及时内存水位明显上涨。如果当时没有提前做好内存监控和应急预案很容易在升级之后出现内存耗尽。小版本升级的理解应该和主版本升级一样严肃。中间版本之间可能存在bugfix和性能优化但同时也可能引入新的行为变化。你不要想当然地认为补丁版本就是纯bugfix不改变任何行为。实际上一些修复bug的改动本身就意味着行为的变化。我建议任何版本升级前都要做三件事。第一去官网看清楚这个版本的release notes尤其是兼容性变化和已知问题。第二在预发环境做一轮完整的回归测试覆盖核心的读写链路。第三压缩binlog保留时间备份配置文件确保随时能回滚。4.2 灰度发布的具体步骤和可控性评估升级不是一次性的动作而是有节奏地推进。灰度发布是最稳妥的方式具体来说我会把灰度分为四个阶段。第一个阶段是单节点灰度把集群中的一个从节点升级到新版本观察运行情况。这个阶段重点观察内存变化、CPU变化、慢日志数量、以及主从同步的状态。正常情况下至少要观察24小时确认没有异常。第二个阶段是部分节点灰度把一半的从节点升级到新版本让这部分从节点承接部分读流量。这时候要比较新旧版本节点的性能指标比如平均延迟、p99延迟、CPU使用率等确认新版本没有性能回退。第三个阶段是全量从节点升级所有的从节点都升级到新版本这时候数据同步的兼容性已经验证过了风险可控。第四个阶段才是主节点升级。主节点升级不能直接改配置重启我建议用主从切换的方式先把主节点的角色切换到已升级的从节点再把旧主节点升级到新版本最后把流量切回去。这样能最大程度地减小主节点升级对写入链路的影响。每个阶段的持续时间我建议至少是24小时如果你对系统稳定性要求极高可以延长到48小时或者72小时。灰度发布的价值就在于你有足够的时间去观察问题而不是等全量上线之后才去救火。4.3 回滚预案的设计原则升级回滚预案是很多人容易忽略的一环。我见过不少团队升级之前信心满满结果出了问题手忙脚乱连备份在哪都不知道更别提回滚了。回滚预案的设计我遵循几个原则。第一版本文件必须提前准备好升级之前把旧版本的安装包和配置都放到一个固定的目录不要等出事儿了再去下载。第二数据备份必须完整。对数据库来说升级之前一定要做全量备份并且验证备份可用。我之前遇到过备份文件是生成了但恢复训练没做过真需要恢复的时候发现备份是坏的那次真的非常被动。现在我会定期做恢复演练确保备份是可用的。第三回滚要有明确的触发条件。不是说看到报错就回滚而是定义好阈值比如“升级后p99延迟上升超过20%”“CPU使用率持续超过85%”“内存增长速度超过每小时5%”只要达到这些阈值立即启动回滚不要犹豫。第四回滚之后要有流量验证。回滚不是关掉服务而是把服务恢复到旧版本恢复之后要确认业务流量是正常的数据库连接正常CPU和内存水位正常。我见过一种糟糕的情况回滚之后因为配置残留跟旧版本不兼容反而引起了新的事故。升级的风险控制本质上是概率和影响的平衡。你没法保证升级一定不出问题但你可以通过灰度、监控和回滚把问题的概率降到最低把影响控制在最小范围。5. 线上稳定性治理的黄金法则和实操工具5.1 应急预案的四板斧降级、限流、熔断、隔离稳定性保障的文件工作不只是在扩容和升级的时候做平时就要把应急预案练好。我把应急预案的核心总结为四板斧降级、限流、熔断、隔离。降级是指当一个非核心依赖不可用的时候主动关闭它保证核心链路的可用性。比如商品详情页的推荐位依赖某个第三方服务这个服务挂了你可以降级成展示默认的推荐数据而不是让整个详情页报错。限流是指在流量超过系统处理能力的时候主动拒绝一部分请求保护系统的核心能力。常见的限流算法有令牌桶和漏桶业界有很多成熟的限流组件可以直接用比如Sentinel、Hystrix等。限流不是目的限流是为了避免系统被冲垮之后完全不可用。熔断是指当某个依赖连续失败达到一定阈值时主动断开对这个依赖的调用让系统快速失败而不是一直等待超时。熔断之后通常会有一个半开状态允许部分试探性请求通过如果成功就逐步恢复全量调用。隔离是指把不同业务、不同优先级、不同流量特征的模块隔离开来避免互相影响。隔离的方式有很多种线程池隔离、进程隔离、机房隔离、容器隔离等。核心思路就是一个模块出了问题不应该拖垮另一个模块。这些机制听起来很简单但真正落地很难难就难在“阈值设定”和“时机把握”。阈值设得太保守正常流量也会被误杀阈值设得太激进又起不到保护作用。我的经验是阈值不能拍脑袋要基于线上监控的历史数据来定。先看过去一个月的平均流量、峰值流量、错误率基线的分位数再设置限流熔断的阈值并且定期复盘。5.2 监控指标大盘和告警阈值设置经验稳定性保障如果缺少监控就等于闭着眼睛开车。我踩过很大的一个坑就是监控指标覆盖不全出问题的时候才发现关键指标根本看不到。我的监控指标大盘会按照四个层面来建设。第一个层面是硬件层包括CPU、内存、磁盘空间、磁盘IO、网络带宽这些是基础设施的底线。硬件指标异常往往意味着扩容或者日志清理这些动作需要尽快执行。第二个层面是应用层包括数据库的QPS、连接数、慢查询数量、主从延迟、缓存命中率、缓存内存使用率、Redis的key数量、大key和热key数量等。这些指标直接反映系统的健康度也直接决定了你的容量规划。第三个层面是业务层包括接口的成功率、响应时间的平均值和p99、错误码的分布、核心业务消息的积压情况。业务层指标出了问题即使基础硬件都正常也要优先排查业务代码层面是否有毛病。第四个层面是资源层包含各种资源的余量和水位。比如磁盘使用率超过70%就要预警超过85%就要立即处理Redis内存使用率超过80%就要评估是否需要扩容连接数使用率超过70%就要检查应用连接池和慢查询。告警阈值的设置我给一个建议不要设置得太多告警泛滥等于没有告警。我的做法是核心指标的告警一定要全部配置次要指标的告警尽量收敛宁可少告警也不要天天被没有意义的告警轰炸否则真正出问题的时候容易被忽略。5.3 常用的工具链和自动化脚本分享最后分享几个我用着顺手的工具链这些工具能大幅提升稳定性保障的效率。数据库和缓存的操作首选官方的命令行工具。MySQL就用mysql clientRedis就用redis-cli。Redis的redis-cli支持--bigkeys参数能扫描大key支持--hotkeys参数能分析热key需要开启LFU策略。这两个参数是我排查缓存问题的第一利器。数据校验我可以推荐一个工具pt-table-checksum这是Percona Toolkit里的一个经典工具专门用来做MySQL主从数据一致性校验。它的原理是通过在主库上对每张表计算校验和然后在从库上做同样的计算对比结果能发现谁的数据不一致。对于Redis集群可以用redis-cli --cluster check命令检查slot的分配是否均匀也能用redis-cli --cluster reshard来做在线扩容。监控工具如果你是云上部署很多云平台自带的监控就能满足需求。如果是自建我推荐用Prometheus加Grafana的组合Prometheus采集数据的能力很强Grafana的展示模板也很丰富。业界有现成的MySQL Exporter、Redis Exporter装好之后就能采集到各项核心指标。告警可以用Alertmanager配置好Webhook之后可以接入钉钉、企业微信或者邮件。自动化方面数据库的例行巡检脚本建议沉淀成自动化任务。我自己写过一个巡检脚本每天定时跑一次检查磁盘使用率、慢查询数量、连接数、主从延迟、以及备份是否成功结果推送消息到工作群。这样即使我不在电脑前也能第一时间掌握核心系统的健康状态。6. 数据库和缓存连通场景下的联动风险6.1 缓存穿透、缓存击穿、缓存雪崩的应对缓存和数据库是配套使用的缓存层面的问题最终会传导到数据库。数据链路一长稳定性保障就不只是单点的问题而是整条链路的问题。缓存穿透是指大量请求查询一个不存在的数据缓存里没有数据库里也没有每次请求都会打到数据库。这种情况如果遭遇恶意攻击数据库很容易被打崩。我的应对方案有两个一个是对不存在的数据也做缓存但过期时间设置得短一些比如1分钟另一个是引入布隆过滤器在缓存层直接过滤掉不存在的key。缓存击穿是指某一个热点key突然过期大量请求同时打到数据库。应对方案是设置热点key永不过期或者使用互斥锁只允许一个请求去数据库查询和重建缓存其他请求等待缓存重建完成。缓存雪崩是指大量key在同一时间过期或者缓存节点宕机导致大量请求落到数据库数据库压力暴增。应对方案是把过期时间加上随机值避免同步过期缓存节点宕机的情况下要做好缓存的高可用比如使用哨兵保护以及应用侧的多级缓存兜底。这三个问题的应对不只是技术层面的问题更是稳定性意识的问题。每次上线新功能我都要问一句如果缓存挂掉了数据库扛得住吗如果数据库扛不住降级方案是什么多问几遍线上事故就能少很多。6.2 缓存和数据库的数据一致性保障策略缓存和数据库的数据一致性问题是老生常谈但也是线上事故的高发区。不一致的典型场景是先更新数据库再删除缓存如果删除缓存失败那缓存里就是旧数据一直到缓存过期才恢复或者先更新缓存再更新数据库如果数据库更新失败缓存和数据库就永远不一致了。我给团队定过一条铁律更新数据的时候数据库和缓存的操作顺序必须是“先更新数据库再删除缓存”而不是“先更新缓存再更新数据库”。原因也很简单数据库是数据的最终归宿缓存只是加速读取的副本。先更新数据库即使删除缓存失败最坏的情况是多一层旧数据等缓存过期就恢复了。如果先更新缓存再更新数据库数据库一旦失败数据永久错乱恢复起来非常困难。另外删除缓存失败这种小概率事件也要有兜底方案。我现在会用延迟双删的办法更新数据库之后删除一次缓存然后隔一小段时间比如500毫秒再删除一次。这样即使第一次删除失败第二次删除也能兜底。同时加上binlog监听监听数据库的变更事件发现数据变化就主动删除对应的缓存key这样即使应用层删除失败我们也能通过消息队列再次删除缓存。6.3 大促和高并发场景下的容量规划和压测方法大促和高并发场景是扩容和升级压力最大的时刻。平时跑得好好的系统在流量高峰可能会突然崩掉就是因为平时没有做大流量压测你不知道系统的真实极限在哪里。我做大促容量规划的标准流程是先根据业务预期算出峰值QPS和吞吐量目标对每个核心接口做全链路压测压测过程中同时关注数据库、缓存、以及其他依赖服务的指标。压测不是一个静态动作压出瓶颈之后就要针对性地扩容或者优化代码然后再压直到达到目标。压测工具方面我常用的是JMeter和Locust。JMeter适合HTTP接口的压测配置灵活Locust用Python写脚本适合自定义复杂的业务场景压测。如果是数据库压测可以用sysbench或者TPCC。sysbench支持oltp_read_write、oltp_point_select等场景能模拟真实业务负载简单实用。压测的结论要形成文档标注清楚系统的极限QPS、瓶颈点是什么、扩容了多少台机器、每个节点的水位是多少。这样大促期间看到监控指标异常你能第一时间判断出来是极限到了还是哪里出了岔子。大促结束之后还要复盘记录哪些系统的水位偏高哪些机器需要扩容为下一次做准备。线上稳定性的保障从来不是一次性的工作而是一个持续迭代的过程。扩容和升级都是有窗口期的但是监控、巡检、预案、复盘这些工作没有窗口期每天都要做。希望通过这篇文章的梳理你能把数据库和缓存的稳定性保障工作做得更扎实、更从容。
返回列表