ARTICLE DETAIL

资讯详情

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

从KEYS阻塞到可视化治理:自研Redis管理平台的设计与实践

从KEYS阻塞到可视化治理:自研Redis管理平台的设计与实践 最近在排查一个诡异的缓存不一致问题登录 Redis 一看key 密密麻麻全是乱码再一看同事正在用命令行手敲KEYS *扫全库我当时心里就咯噔一下这要是碰上生产环境Redis 基本就卡死了。这事之后我就下定决心团队里必须有一个统一的 Redis 管理平台而不是谁想连就连、想敲命令就敲命令。于是就有了 redis-manger 这个项目。名字其实是个彩蛋。本来打算叫 redis-manager结果建仓库的时候手一抖拼成了 redis-manger同事说这名字看着像“投喂员”怪好记的后来就真用这个了。这个平台说白了就是一个内网自用的 Redis 可视化管理与治理系统把连接管理、数据浏览、命令执行审计、大 Key 扫描、分布式锁排查这些日常运维动作都收拢到一个 Web 界面里。写这篇文章是想把整个设计思路、踩过的坑、以及背后需要用到的 Redis 基本功都梳理一遍给同样在维护 Redis 的工程师一个参考。1. 先聊聊我为什么放着现成的可视化管理工具不用市面上不是没有 Redis 可视化工具Redis Desktop Manager、Another Redis Desktop Manager 都用过个别开源项目的界面还挺好看的。但团队规模一大你要面对的就不是“连一次 Redis 看看 key”这么简单了。1.1 多环境切换与凭据管理是第一个痛点我们这边有本地、测试、预发、生产至少四套环境每个环境又分主从、集群连接信息散落在每个人的电脑里。有人用Another Redis Desktop Manager有人用 IDEA 的 Redis 插件还有人干脆命令行redis-cli直连。新同事入职第一周光找连接地址和密码就要问一圈人。更麻烦的是生产库的密码会定期轮换每次轮换完就是一场灾难到处有人喊“连不上了”。自研平台把连接配置统一收口存到后端数据库里并且支持按团队、按项目做权限隔离。测试环境的密码可以明文展示生产环境只允许申请临时审批链接拿到的是一个有时效的只读凭证。这样就算轮换密码在平台里改一处所有人下次登录就自动用新凭据不用再去问。1.2 数据安全问题比想象中严重命令行直连生产 Redis 最大的问题不是不会用而是没人知道谁执行了什么命令。尤其是FLUSHDB、FLUSHALL、CONFIG SET这种危险操作一旦误执行整个缓存层几分钟内就冻住紧接着数据库就被打爆。有个真实教训我们一位同事排查问题的时候在测试环境执行了FLUSHALL结果他的 Redis Desktop Manager 里保存了好几个连接切换环境时没注意直接对着生产库也来了一下。还好当时生产没缓存什么不可再生的数据只是把热数据打没了短暂回源了一波。但从那以后我就强调平台必须有命令审计而且高危命令要默认拦截除非有审批授权否则一条都别想发出去。1.3 数据浏览要面对的不止是 String很多可视化工具对 String 类型的 key 支持得很好可一旦涉及 Hash、List、Set、ZSet、Stream界面就拉胯了。比如 Hash 里几百个 field不少工具只能一页一页翻List 想按 range 查工具界面就不知道怎么填参数更别说公司内部很多服务用了 JDK 序列化key 和 value 全是\xAC\xED\x00\x05t...这种乱码工具直接显示成二进制根本没法用。于是我把这些真实痛点列成需求决定自己做一个平台既要能管连接又要能优雅地查数据还要能拦住危险命令最后再带上缓存治理的辅助功能比如大 Key 扫描、慢日志、命中率曲线。这个想法其实挺朴素就是“我受够了东拼西凑的运维方式想要一个顺手的管理平台”。2. 管理平台的核心功能是怎么设计的整个平台从需求上分成五块每一块都对应一个日常高频操作。这里挑几个重点展开讲。2.1 连接管理与多环境隔离平台上的每个“连接”都对应一个 Redis 实例或集群包括单机、主从、哨兵、Cluster 集群四种模式。连接配置里除了 host、port、password还有数据库编号dbIndex、连接超时、读超时、是否 SSL 等参数。后端保存配置时会把密码用 AES 加密密钥存放在环境变量里而不是写死在数据库明文。为了防住“测试环境顺手打到生产”这种手滑事件我在平台上做了一个叫“环境标签”的设计。每个连接都必须标记环境local、dev、test、prod。生产环境的连接默认置灰不能直接点击进入需要提交申请写明理由和操作时限审批通过后才会临时解锁。而且生产环境的连接只允许只读模式写命令、删除命令全部禁止。这一条规则上线后团队里的误操作概率直接降到了零。2.2 数据浏览与序列化兼容数据浏览是管理平台里最常用的功能。用户输入 key 之后平台要做的第一件事是判断 key 的类型是 string、list、hash、set、zset 还是 stream然后用对应方式拉数据。这里就涉及一个核心问题Redis 里存的数据不一定都是字符串。在实际开发里我见过四种序列化方案StringRedisSerializer存在 Redis 里的就是明文显示最友好。Jackson2JsonRedisSerializerJSON 字符串显示友好但对象会带类型信息有时候看着乱。JDK 序列化JdkSerializationRedisSerializer这是最常见的\xAC\xED开头乱码来源。Protobuf / Kryo二进制序列化工具基本无法直接显示。平台的做法是给用户提供“显示编码”选项。选 UTF-8就按字符串解码选 Hex就把二进制转成十六进制显示选 Base64则转成 Base64 显示。虽然还不能做到完全还原业务对象但至少能看到内容排查起来比一堆转义字符舒服太多了。2.3 命令执行、危险拦截与审计日志既然要做管理平台命令执行器肯定是核心功能。但是绝对不能做成一个 Web 版的redis-cli随便敲。我把它设计成三层防护第一层命令白名单。允许执行的命令有限制常见读命令如GET、HGETALL、LRANGE、ZRANGE、MGET等可以正常执行写命令如SET、HSET只有非生产环境允许危险命令如FLUSHDB、FLUSHALL、KEYS、SAVE、BGSAVE、SHUTDOWN、CONFIG、EVAL默认全部拒绝。第二层按连接环境动态调整。生产环境强制只读所有写命令直接不给入口。第三层审计日志。每次命令执行都会记录操作人、目标实例、命令内容、执行时间、耗时、返回结果的状态。有事情要追溯的时候一查日志全清楚。这里特别说明一下KEYS命令。很多人喜欢用KEYS *查 key在数据量小的时候没问题但一旦线上 key 数量过百万这个命令会阻塞 Redis 单线程导致所有请求排队。平台里我用SCAN替代了KEYS并且加了键值匹配模式。用户能感受到的差异是结果是一批批返回的不会一把梭拉全量。2.4 缓存治理辅助大 Key、热 Key 和命中率以前做缓存治理靠的是人为经验和直觉。大 Key 往往是在某个接口突然变慢之后才发现然后翻半天慢日志最后定位到一个几分钟前线上写入的超大 List。平台里我加了一个“大 Key 扫描”模块它会异步地对指定 Redis 实例执行SCAN逐个抽样统计 key 的大小并标记出超过阈值的对象比如单个 String 超过 10MBList 元素数量超过 1 万个Hash 的 field 超过 5000 个。热 Key 这里不做全自动探测平台提供了简单的访问频率统计。通过INFO KEYSPACE和MONITOR的采样生产环境慎用MONITOR只有在低峰期才会短暂开启可以估算哪些 key 访问频繁。命中率则是定期拉取INFO STATS里的keyspace_hits和keyspace_misses计算后绘制曲线展示在首页。这一整套工具的价值不在于它用了多高深的技术而在于把原来需要人肉靠命令行凑出来的信息集中到了一起。2.5 分布式锁的在线排查Redis 分布式锁是后端面试必问也是生产环境最容易出幺蛾子的地方。平台里专门做了一个“锁查询”入口。约定团队内部使用统一的分布式锁工具类锁的 key 固定为lock:{业务标识}value 写入持有者标识和过期时间。当线上出现“抢不到锁”的告警时操作人员可以直接在平台上查询这个 key 是否存在value 里写的持有者是谁剩余过期时间还有多久。如果需要手动释放锁平台会执行一段 Lua 脚本先比对 value 是否一致一致才删除避免误删别人的锁。这个逻辑其实就是分布式锁防误删的标准做法但是落到管理平台上之后线上问题排查的速度提升了一个数量级不用再临时拼命令了。3. 技术选型与关键模块实现平台本身的技术栈不复杂后端是 Spring Boot前端是 Vue Element Plus数据库用的 MySQL部署在公司内网的一台 4 核 8G 机器上上面跑一个容器就够。真正花时间的是几个关键模块的实现细节。3.1 后端用哪种 Redis 客户端Java 世界里的 Redis 客户端主流就是 Jedis 和 Lettuce 两种。一开始我图 Lettuce 是 Spring Boot 默认直接就用上了后来踩了坑才意识到管理平台这种频繁建立连接、还要连接几十个不同实例的场景可能 Jedis 更合适。Lettuce 基于 Netty底层是一个共享连接并发性能确实好但问题是它在某些版本的 Redis 集群模式下对超时异常的处理很折腾我曾经遇到过一个RedisCommandTimeoutException线程直接打满排了很久才发现是连接池默认配置问题。而 Jedis 实现更直白每次操作就是一条连接虽然相对重一些但是对于管理平台这种“单个用户操作并发量不大”的场景稳定性和可排查性反而更重要。所以我最后的方案是Jedis 为主Lettuce 只保留给某些需要异步管道的模块用。连接池统一用 Apache Commons Pool 2 来管理每个 Redis 连接配置对应一个独立的 JedisPool 实例缓存在 Map 里避免每次操作都新建连接。3.2 前端数据实时刷新与长连接数据浏览页面上我希望看到的数据能自动刷新尤其是集群监控那部分。前端不是傻傻地轮询而是用 WebSocket 和 Spring Boot 建立一个连接服务端一旦发现有慢日志、大 Key、连接异常就主动推送给页面。这个其实不复杂后端用一个ConcurrentHashMap保存 WebSocket Session定时任务扫描 Redis 实例并推送指标。前端还有一个比较实用的功能“批量命令执行”允许用户同时选多个 Redis 连接执行同一条只读命令。比如我要对比三套环境的DBSIZE直接在平台里写好命令一键跑完结果以表格形式对比展示省去了来回切换连接的麻烦。3.3 统一处理序列化问题这是一个避不开的坎。同一个 Redis 实例里可能有不同的服务写入不同类型的 value。如果盲目用get(byte[])再转字符串遇到二进制乱码就是家常便饭。我的做法是在平台里封装一个RedisValueDecoder。用户浏览 key 时先拿到原始 byte[]然后根据 key 的前缀规则猜测编码比如公司的用户缓存前缀为user:info:业务方约定使用 JSON那么平台就优先按 UTF-8 解码并格式化 JSON比如session:前缀使用 JDK 序列化那就先尝试反序列化失败的话再退化为 Base64 显示。这套规则写在一个配置表里业务团队可以自己登记前缀与序列化方式平台就会用最合适的方式展示而不是把乱码原样甩给用户。3.4 集群模式下的 Scan、Pipeline 与 Hash Tag连接集群的时候SCAN要对每个 master 节点分别执行然后合并结果。这是很多可视化工具不愿意做的工作因为做不好就容易漏 key 或者重复扫描。平台的做法是先通过CLUSTER NODES解析出 master 节点列表然后对每个节点创建一个 Jedis 连接分别执行SCAN游标按节点独立维护。这里有个经验千万不要用全局游标去扫集群因为SCAN的游标是节点本地的混着用会死循环或漏数据。批量操作方面我利用 redis pipeline 优化了大 Key 扫描时的采样速度。其实原理并不难就是先SCAN出 key 列表然后通过pipeline分批请求每个 key 的STRLEN、LLEN、HLEN等命令批量获取大小信息。这样比一条条命令请求快上一个数量级。3.5 安全防护SSRF、正则过滤与超时控制既然是 Web 平台连接目标是用户填的那就必须考虑一个安全问题用户提交一个内网地址平台去连接万一用户是个恶意攻击者这不就成 SSRF 了吗虽然内网平台但还是要有基本底线。我在连接配置里加了目标地址校验禁止连接非内网 IP 段对于 host 也要求必须是内网域名不能随便填外网地址。命令执行模块也做了正则过滤任何包含FLUSHALL、KEYS、SHUTDOWN、CONFIG等关键字的命令在入口直接拒绝后面再结合 Redis 命令本身的只读限制双保险。此外所有操作必须有超时控制。平台上针对每条命令设置了默认 3 秒超时一些大范围扫描会放到异步任务里执行不走同步接口。这个设计很关键因为 Redis 单线程执行慢命令时平台自己的连接池也有可能被拖死所以异步化是必须的。4. 从多环境踩坑到生产可用我会记住的五个问题平台从第一个版本到能够在团队里稳定使用中间经历了四个多月的迭代其中有五个问题印象特别深写在这里给后来人参考。4.1 大 Key 扫描差点把测试环境扫挂第一次上线大 Key 扫描功能时我直接在测试环境跑了一轮全量扫描。测试环境的 Redis 本身数据量不大跑完没感觉。但等我在一台数据量达到 500 万的预发环境上测试时发现主线程延迟从 0.2ms 飙到了 40ms。排查后发现有两点设计失误第一SCAN虽然不会阻塞但每次扫描 COUNT 调得太大比如默认 1000其实单次扫描执行时间还是会有波动第二虽然每条命令很快但我搭建了一个很大的线程池几十个并发去扫同一个实例每个 SCAN 都会占一个连接Redis 是单线程命令队列越长延迟越高。后来我把并发度降到了 2COUNT 降到 100并且加了一个全局开关扫描操作只在低峰期允许运行。优化后延迟波动从 40ms 降到了 2ms 左右基本没有感知了。经验总结就是针对 Redis 的操作宁可慢点也不要并发猛冲。4.2 分布式锁“误删”问题的真实复现另外一个踩坑是分布式锁的误删问题。当时团队用的锁工具类还不统一有人用SETNXEXPIRE有人用 Redisson还有人直接SET key value EX 30 NX。平台里我提供了手动解锁功能但最开始写的解锁逻辑是直接判断 key 存在就执行DEL。结果有一次真的翻车了一位同事在平台里看到某个锁 key 一直存在以为是死锁就手动删了但其实那个 key 是另一个服务正在持有的有效锁。删除之后两个服务同时进入临界区导致数据重复处理。还好影响范围可控但教训很深刻。修复方案其实就是标准答案解锁必须用 Lua 脚本比对 value 是否等于当前持有者标识相等才删。平台层面我甚至不允许用户手动填 value 来解锁而是从命令审计日志里自动带上持有者标识杜绝了这类误操作。4.3 RedisTemplate 序列化带来的乱码问题SpringBoot 整合 Redis 是很多团队的项目标配但 RedisTemplate 默认使用 JdkSerializationRedisSerializer导致缓存到 Redis 里的 key 是\xAC\xED\x00\x05t\x00...这种value 也全是二进制。平台在处理这些 key 时如果检测到以\xAC\xED开头就会尝试按 JDK 序列化解析。解析成功就显示成可读对象结构解析失败就退化为 Base64。后来我干脆把团队内部的解决方案统一了新服务一律使用GenericJackson2JsonRedisSerializer并且日期类型用时间戳字符串字符串类型直接明文。这样不仅平台好展示排查问题也轻松。这个经历让我意识到很多线上问题根源不是 Redis 本身而是使用方序列化策略千奇百怪。4.4 Incr 不准背后的原子性误解热词搜索里大家经常搜redis incr不准其实不是 incr 不准而是统计方式不对。有的业务用GET之后再SET累加两个命令之间被其他线程插队自然就丢更新。还有的用INCR做计数器但到期后没有妥善处理持久化导致重启后回退。平台里我加了一个计数器操作模块直接调用原子命令INCR、DECR、INCRBY并且提示用户不要用“读改写”方式。这个小功能上线后团队里类似的口水问题少了很多。4.5 Lettuce 超时异常的迷雾另一个让我记忆深刻的问题是开头提到的RedisCommandTimeoutException。当时平台的监控模块使用 Lettuce连接超时设的是 2 秒但是 Redis 执行一个慢命令超过 2 秒后Lettuce 会把它当作超时连接状态变得非常奇怪后续命令全部在这个连接上排队超时。后来我把监控模块全部切换到 Jedis并且对每条命令设置了 socket 超时。Jedis 的超时控制要直接得多一旦超时当前线程直接抛异常连接池把坏连接剔除不会影响其他连接。这个切换让监控模块的稳定性提升了一个档次。5. 平台背后的 Redis 基本功安装、数据类型与缓存治理速查管理平台做得越久越发现很多问题不是工具不够好而是 Redis 基础功不扎实。这里把平台设计过程中反复用到的 Redis 高频知识点做一次梳理。5.1 不同环境下的安装与主从/集群部署安装 Redis 本身不算难但不同操作系统各有各的坑。macOS 上最简单的是brew install redis装完直接brew services start redis就能开机自启。Windows 上官方没有支持很好的版本大家常用的 5.0.14.1 是微软老分支的社区版本配置redis.windows.conf然后redis-server redis.windows.conf启动注意 Windows 下守护进程配置无效得靠服务方式或者窗口挂着。Linux 上建议大家不要直接用系统源里的太老版本去官网下稳定版源码编译./configure make make install。Docker 部署最省心我给大家一个主从部署的参考docker network create redis-net # 主节点 docker run -d --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7.2 redis-server --appendonly yes # 从节点 docker run -d --name redis-slave \ --network redis-net \ -p 6380:6379 \ redis:7.2 redis-server --appendonly yes \ --replicaof redis-master 6379哨兵和集群也可以用 Docker 编排但生产环境我一般建议用官方推荐的部署方式每个节点独立成机不要在同一台宿主机上硬堆。5.2 数据类型怎么在管理界面里展示平台的数据浏览模块本质上是把 Redis 不同数据结构的操作命令翻译成了界面操作。我做了几个常用映射String 类型GET、SET、STRLEN展示单行文本。Hash 类型HGETALL、HLEN展示成 key-field 表格支持按 field 模糊搜索。List 类型LLEN、LRANGE key 0 -1展示成有序列表支持分页和倒序。Set 类型SMEMBERS、SCARD展示成无序集合。ZSet 类型ZRANGE key 0 99 WITHSCORES展示成带分数的排行表。StreamXLEN、XRANGE展示消息流适合排查消息队列问题。这里要提醒一句SMEMBERS在 set 足够大的时候也有阻塞风险平台内部是改用SSCAN分批拉取的。我把这个差异隐藏在了界面后面用户看到的还是一样的集合但底层不会打爆 Redis。5.3 缓存穿透、击穿、雪崩与一致性治理管理平台里有一个“缓存治理建议”模块它会根据业务方填写的缓存策略自动生成一份风险清单。虽然这个模块还没做到智能判断但我会在平台上展示一些经典问题的典型特征。缓存穿透指的是查一个不存在的数据每次请求都到数据库。治理办法是缓存空值或者用布隆过滤器。平台提供了一键生成“空值缓存工具类”代码的功能直接复制粘贴到工程里就能用。缓存击穿指的是热点 key 过期瞬间大量请求打到数据库。治理办法是热点数据永不过期 异步刷新或者使用互斥锁重建缓存。平台里会建议用户设置一个lock:refresh:key的分布式锁在锁内回源数据库并更新缓存锁外等待重试。缓存雪崩指的是大量 key 同时过期。治理办法是过期时间加随机偏移量。平台在缓存管理页面可以直接对一批 key 做“批量设置过期时间”并自动加上随机偏移避免每次都写固定值。缓存一致性是最难的问题没有绝对完美的方案。平台能做的就是帮用户减少不一致的时间窗口推荐写操作时先更新数据库再删除缓存删除失败就发一条消息到延迟队列异步重试删除直到成功。这块我也在平台上内置了一个简陋的“延迟重试”模拟器可以直观看到不同策略下的短暂不一致窗口。5.4 持久化策略与日志排查很多团队把 Redis 当缓存用就忽略了持久化。但生产环境绝对不建议关闭持久化至少要开 AOF。平台里连接详情页会展示INFO里的相关指标rdb_last_save_time、aof_enabled、aof_rewrite_in_progress等帮助判断持久化状态。有一次遇到一个诡异问题服务重启后缓存全部丢失查了半天才发现该实例的save参数被改成也就是关闭了 RDB 快照AOF 也是关的等于 Redis 只活在内存里。平台里我专门加了一个“持久化状态检查”的告警规则一旦发现某个连接关闭了 AOF就标记为高危险。日志排查方面Redis 的慢日志非常关键。平台会定期执行SLOWLOG GET拉取慢查询并展示耗时排行。这样用户不用登录服务器敲命令直接在平台里就能看到哪个 key、哪条命令拖慢了实例。5.5 常见面试点和命令误区虽然平台是工程工具但我发现很多读者也是冲着 Redis 面试题来的所以这里列几个和管理平台强相关的点。incr为什么不安全实际不是incr不安全而是你错误的读改写方式不安全。KEYS和SCAN的区别KEYS全量阻塞SCAN游标非阻塞但SCAN可能重复返回元素应用要做去重或容忍重复。分布式锁的正确姿势用SET key value EX seconds NX加锁 Lua 脚本解锁不要SETNXEXPIRE两条命令组合。RedisCommandTimedOutException怎么排查除了网络问题更要看是不是有慢命令把单线程阻塞了此时所有命令都会排队超时。这些知识在平台设计时全都派上了用场而且每一次线上问题的排查又反过来加深了对这些基础知识的理解。管理平台不是空中楼阁它只是把原本该掌握的命令行功底封装成了可视化的交互层。6. 一些后续想做的方向与个人体会redis-manger 现在还在持续迭代近期我在考虑接入 Kubernetes 部署把 Redis 实例的监控数据自动关联到 Pod 事件做到异常时能一键查看宿主机状态。另外还想支持 WebSSH 式的命令终端让高级用户能连进实例执行白名单命令而不是只靠提前配置的按钮。如果你也在维护一套 Redis 设施我的建议是不要盲目把“管理平台”当成一个高优先级项目先梳理清楚自己团队的使用场景是数据浏览为主还是排障为主还是权限管控为主。我的平台最初只是解决“乱连生产库”这个安全问题后面才慢慢长出其他模块。最后再分享一个自己在项目管理上的小经验给平台设计核心闭环时一定要把“审计日志”放在第一优先级。没有日志任何操作都是裸奔出了事只能靠猜。有了这个日志哪怕平台 UI 丑一点团队用起来也是有底气的。redis-manger 这个名字虽然拼错了但每个操作记录都拼得很准确。
返回列表