尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【2】Redis:上手时是“温顺的小猫”,崩盘时是“发疯的猛兽”!——Redis整体架构详解

【2】Redis:上手时是“温顺的小猫”,崩盘时是“发疯的猛兽”!——Redis整体架构详解 写在前面Redis最容易让人低估的地方不是它难而是平时用起来实在太顺了。业务请求一旦命中缓存接口耗时往往立刻就下来了。用久了以后脑子里很容易留下一个越来越简单的印象这东西无非就是把热点数据放进内存GET/SET很快也就差不多了。可一旦真的开始排查问题画风往往会突然反过来。某次延迟抖高排查链路里一起冒出来的可能是slowlog、大key、BGSAVE、BGREWRITEAOF、复制延迟甚至还有主节点切换和分片扩容。这时候问题就不再是“它是不是快”而是“它到底快在哪里边界又藏在哪里”。如果只是个内存KV为什么平时用起来越来越简单如果真这么简单为什么一到这些场景背后又会牵出事件循环、后台子进程、持久化、副本同步、哨兵和集群这一整套结构也正是这种反差让Redis值得重新拆开来看。把这套结构拆开以后主路径为什么短、重操作为什么会拖慢延迟、持久化和多节点能力又是怎么一层层补上去的都会清楚很多。从一个常见反差说起很多系统里的Redis都有同一种反差平时像个默认的前提真出问题时又像另一套系统。平时是下面这种感觉大多数请求延迟很稳命中缓存时接口体验很顺业务侧对它的印象几乎只剩下“快”可一到下面这些场景问题就开始集中暴露某条命令进了slowlog某个大key被遍历、删除或搬迁实例正在做BGSAVEAOF正在重写主从复制开始堆积主节点故障切换刚刚发生把这种违和感压成一句话大概就是平时像一把很顺手的快刀为什么一到重操作、持久化或多节点切换场景Redis就突然变成了一套远比“内存KV”复杂得多的系统这种反差背后可以引出好几个关键问题请求主路径为什么快快的边界在哪里数据为什么不会跟着进程一起消失主节点挂了以后谁来兜底单机撑不住之后怎么横向拆开只要这几个问题能连成一条线后面的架构图就不会散。先看整套结构的总览后面每一部分分别在解释哪一层也就更清楚了一次请求在Redis里怎么跑完高可用和集群可以先放一放。理解Redis最稳的起点还是一次最普通的请求。比如这样一条命令SET user:1001:name alice或者这样一条读取GET user:1001:name看起来很短但在Redis内部却做很多事情客户端和Redis建立连接请求进入 socket事件循环发现连接可读主线程接手这次请求服务端解析协议、拆出参数、定位到对应命令命令执行层根据键找到对象进入对应的数据结构读写结果被组织成响应再写回给客户端事件循环继续处理下一批连接事件把这条主路径单独拎出来Redis平时为什么快就会直观很多这条路径为什么重要因为Redis平时的“快”首先就来自主路径足够短。它和很多重型系统的差别不是某一个孤立技巧而是几件事叠在一起数据主要在内存里少了磁盘随机访问主线程串行执行核心命令少了大量锁竞争事件循环配合I/O多路复用一个线程就能管理很多连接请求路径很短中间没有复杂的跨线程协调很多人第一次看到“Redis是单线程的”时会天然觉得这应该是性能弱点。更准确的说法是Redis快不是因为“单线程”这四个字本身有性能加成而是因为它把常规命令执行这条主路径收得很短而单线程让这条路径保持了足够低的协调成本。这里顺手把一个常见误区拆掉。I/O多路复用解决的是“一个线程如何同时看很多连接”不是“多条命令如何并行执行”。“连接很多”和“命令并行”不是一回事。逻辑类型和底层结构不是一回事如果只停在“内存读写很快”理解还是会偏平。Redis处理的不是一团没有结构的字节而是对象和数据结构。平时最常见的几类命令表面上看到的是逻辑类型GET user:1001:name HSET order:1001 status paid amount 128 ZADD leaderboard 98 user:1001这三条命令对应的逻辑类型分别是命令逻辑类型关注点GETstring读取路径最短最容易体现主路径优势HSEThash字段集合操作适合承载一组属性ZADDzset既要查成员又要维护分值有序性但逻辑类型不等于底层编码。真正要记住的是同样是hash或zset在不同数据规模、不同成员数量下底层表示方式和操作成本都可能变化。这也是为什么Redis不适合被写成“就是一张大哈希表”。更接近事实的说法应该是对外暴露的是统一命令接口和逻辑数据类型对内维护的是对象抽象、类型信息和底层编码主线程执行命令时真正触达的是这些内存对象和底层结构把这一层看清后面再谈大key、慢命令和重操作为什么会拖慢主路径就不会显得突然。单线程为什么还能快“单线程也很快”这句话流传很广但如果只记这一句理解很容易带偏。原因至少有下面四层1. 纯内存访问减少了最重的一类等待常规命令的主成本不像磁盘数据库那样经常被物理I/O拉长。数据主要在内存里请求天然少了一大段等待。2. 事件循环让大量连接不会演变成大量线程如果一个连接对应一个线程连接数一上来上下文切换、线程调度和资源占用都会迅速变重。Redis这条路走得更轻事件循环感知哪些连接就绪然后按顺序处理。3. 核心命令执行串行化少了细粒度锁竞争很多系统的复杂度不是花在业务逻辑本身而是花在“多个线程同时改同一份状态怎么办”。Redis把核心命令执行放在主线程串行处理内存状态的一致性维护就简单很多。4. 常规命令的路径确实短一次普通的GET/SET从请求进入到结果返回中间没有太多绕路。它不像某些重型链路那样一路经过线程池、磁盘、日志系统、复杂事务协调再回来。把这四点放在一起才是日常语境里“Redis很快”的真实来源。这里也要顺手拆掉另一个误区单线程不等于整个进程里永远只有一个线程。更准确的说法是核心命令执行主路径主要由主线程串行处理。至于某些后台收尾工作、较新版本里的部分I/O threads并没有把这个模型改写成“命令多线程并行执行”。快的边界藏在主路径被拖长的地方主路径为什么快讲完以后边界也就更容易看清了。Redis平时怕的往往不是“请求多”而是“某个请求太重”。因为核心命令执行主路径是串行的只要某个操作把主线程占住后面的请求就得排队。最典型的几类问题有1. 慢命令比如超大范围遍历、复杂聚合、一次性处理过多元素这类命令哪怕只出现一条也可能直接把主线程卡住。slowlog记录的就是这种信号。它不是网络总时延统计而是命令执行本身已经开始变重的证据。2. 大key大key不只是字符串特别长。成员很多的hash、list、set、zset同样可能在遍历、删除、序列化、复制时把成本迅速放大。大key麻烦的地方在于它不是只在“占内存”这一点上讨厌而是会把多个链路一起拖重主线程处理时间变长持久化时数据拷贝压力变大复制传播时传输体积变大迁移和扩容时成本更高3. 阻塞操作某些操作天生就不适合长时间占着主路径。即便系统提供了异步化收尾能力也不意味着所有成本都被彻底转移走了。所以“快”不能写成无条件结论更像是一条前提明确的判断只要主线程处理的大多数是短平快命令Redis的优势就很明显一旦重操作开始频繁占住主路径延迟边界就会很快暴露。到这里开头那种违和感其实已经解释了一半不是Redis平时不快而是它的快高度依赖“主路径别被重活拖长”。为什么重启后数据还能恢复如果把Redis只理解成“高性能请求处理器”整张架构图还是缺了一半。因为真正让它从“快缓存”变成“可长期依赖的基础设施”的恰恰是持久化这层补充路径。很多人第一次接触这里时会自然追问数据明明主要在内存里为什么实例重启后它们还能回来答案不是“内存没有丢”而是“服务把内存里的状态提前变成了可恢复的落盘产物”。Redis常见的两种持久化思路是方式核心思路优点代价RDB把某一时刻的数据集写成快照文件紧凑恢复直接快照之间存在数据时间窗口AOF把写命令按顺序追加下来恢复粒度更细文件会增长需要重写这两种方式不是背诵题里的“二选一常识”而是两种不同取舍更偏向恢复直接、文件紧凑可以看RDB更偏向保留更细粒度写入历史可以看AOF但无论选哪条路都会遇到同一个问题这些事情都不轻不能直接把完整重活塞进请求主路径。也正因为这样后台子进程才有存在的必要。把主线程、AOF、RDB和后台子进程放到同一张图里这层协作关系会清楚很多fork在补什么BGSAVE和BGREWRITEAOF这类操作通常都会派生后台子进程去做。这样做的核心目的不是为了炫技而是为了把快照或重写这类重活尽量移出主线程的直接执行路径。但这里一定要把话说完整后台做事不等于对主线程完全零影响。几个常见影响来源包括fork本身就有成本数据集越大页表处理压力越明显后续写时复制会带来额外内存压力磁盘写入和fsync行为会影响整体运行状态所以线上常看到的现象才会是实例不是完全不可用但延迟会明显变差。这也解释了开头为什么BGSAVE、BGREWRITEAOF经常和“抖一下”一起出现。问题不在于后台任务“不该存在”而在于它们本来就是在补可靠性而补可靠性这件事天然不可能毫无代价。重启恢复链路怎么走实例重启以后内存里的数据集不会自己凭空长回来。恢复流程更接近下面这件事进程启动检查可用的持久化产物如果是快照就装载快照如果依赖追加日志就按规则重放恢复出新的内存数据集只看“重启后先做什么、后做什么”恢复顺序大致就是下面这条路径所以持久化这一层本质上解决的是数据别跟着进程一起消失。而主线程那一层解决的是请求尽量别绕路尽量快。这两层不是对立关系而是分工关系。混在一起写整篇文章就会乱。单机之外还要补什么能力到了这里单机内部的因果线已经比较完整主线程事件循环负责请求主路径内存对象和底层结构支撑常规命令高效执行持久化和后台子进程补上可靠性接下来很自然的问题就是单机总有边界主节点挂了怎么办容量不够又怎么办这里最容易写乱因为很多资料会把复制、哨兵、集群都一股脑归进“多节点 Redis”。名字看上去像一类解决的问题其实完全不同。问题可以拆成三层数据要不要有副本主节点挂了以后谁来切单机容量和吞吐边界到了以后怎么横向拆分这三类问题对应的也是三层东西。这三层能力名字接近很容易重新混到一起。放到一张对照图里会直白很多组件主要解决什么问题不能被写成什么主从复制副本同步“复制一开就自动高可用”Sentinel监控与故障转移“分片方案”Cluster分片与路由“只是哨兵的升级版”复制先解决副本同步主从复制最先补上的是“主节点写入怎么传播到副本”。它为读扩展和高可用打基础但自己并不自动完成完整的故障判断和切换编排。复制回答的是数据怎么多存几份但它还没有完整回答主挂了以后谁说了算Sentinel解决主节点故障切换哨兵这一层负责的是监控、判断、协商和故障转移。哪个节点出了问题、是否真的该切、谁当新的主节点、客户端该连谁这些事情属于高可用编排不属于副本同步本身。所以“已经有主从复制为什么还需要Sentinel”这个问题本质上是在追问有了数据副本不代表故障处理流程也自动长出来了。Cluster解决分片与扩展再往下一步问题就不再只是“主挂了怎么办”而是“单机本身撑不住怎么办”。这时候Cluster出场解决的是键空间分片、请求路由、多节点数据分布。常见资料里会提到哈希槽本质上就是为了让键能被稳定分配到不同分片节点上。Cluster回答的问题是单机容量和吞吐上限到了以后怎么把数据和请求拆到多台机器上承载这和哨兵不是一类事。哨兵偏高可用编排集群偏分片和扩展。两者看起来都叫“多节点架构”但关注点完全不同。Redis到底是一套什么系统如果把前面的内容压缩成一张脑内结构图Redis大概可以这样理解层次主要职责核心问题请求主路径事件循环、命令执行、对象访问、结果返回为什么平时快可靠性补充路径RDB、AOF、重写、恢复数据为什么还能回来多节点能力层复制、Sentinel、Cluster主挂了怎么办单机不够怎么办这样看前面那个最别扭的现象就没那么别扭了平时快是因为主路径确实短某些时刻会抖是因为重操作、后台任务和资源压力会把主路径边界暴露出来需要持久化是因为只靠内存不够支撑“重启后数据还在”需要复制、哨兵和集群是因为单机之外还要继续面对副本、高可用和扩容最后回到工程判断讲整体架构如果最后回不到工程判断前面就很容易变成一串记忆点。顺着上面的因果线比较实用的判断大概有几条。1. 不要把Redis当成没有边界的快存储常规请求确实快但前提是主线程大多数时候处理的是短平快操作。2. 真正需要警惕的是主路径混入重操作比起“请求量很大”很多时候更危险的是某条命令本身很慢某个key太大删除、遍历、迁移动作过重后台持久化和重写恰好撞上高峰3. 持久化策略本质上是取舍RDB、AOF、重写频率、恢复速度、数据丢失窗口、运行期开销本质上都在做取舍。这里不该写成口号而该写成工程权衡。4. 多节点不是一个概念而是三层问题要副本用复制要故障切换用Sentinel要分片扩容用Cluster只要这三层不混很多架构判断都会清楚不少。小结Redis平时显得轻巧是因为常规请求主路径足够短事件循环接住连接主线程串行执行命令内存对象读写完成后直接返回结果。它在出问题时又显得复杂是因为真正线上可用的系统不可能只有这条快路径。持久化要补数据恢复后台子进程要搬走重活复制要补副本Sentinel要补故障转移Cluster要补分片扩展。所以更准确的理解不是“Redis就是一个很快的内存KV”而是Redis是一套把请求主路径压得很短再用持久化和多节点机制把可靠性、高可用与扩展能力一层层补上去的系统。
返回列表