即时通讯系统IM存储设计 之 大Key问题(session绑定)与热Key问题

本文主要围绕个人在设计IM中核心业务(信息收发)中的存储&缓存层的构建时的一些思考。苦逼大学生,才疏学浅,请多指教!

回到上文所留下的问题,在设计缓存时,某些不收敛的业务场景。随着程序运行数据的积压,会不可避免的形成大Key,并且会有部分Key访问量大,即热Key。两者对程序运行均有伤害,下方将分析部分优化的解法。

大Key 问题

大Key问题指的是在使用缓存或存储中间件(如Redis)时,单个Key对应的Value非常大,导致一系列性能和稳定性问题。主要包括:占用大量内存资源,网络传输需要较长时间与带宽,迁移困难等等。

主要可以从业务上 & 架构上着手优化:

架构层面上

若抛开业务上的优化空间,业界对于此问题的解决方案为拆分Key压缩Key ,或结合持久性KV

  1. 拆分Key:将原单Key根据大小均衡分片至多个Key中。可在原Key的基础上加入分片的序列号,在获取值时,使用mGet一并获取所有分片的值即可。(mGet同时获取多个键的值 并同时返回)

eg:user1 -> user1.1 + user1.2 + user1.3

  1. 压缩Key :将原Key在存储前,使用压缩算法减少Value的大小,并且在读取时进行解压。会降低存取效率 ,并且对服务器CPU造成一定压力
  2. 持久性KV :使用CoreKV等持久性NoSql替换现有架构,大大提升大Key的基准值。但是会降低读取效率
业务层面上

而在业务层面上,针对IM的场景,可使用session绑定机制进行优化:

按照此前架构,当用户发送消息时,会经由userID -> sessionID -> deviceID的路线查询下发设备。所有登录设备信息和用户信息都在作为分布式缓存存储在业务server中,并在其中执行所有的查询。

而session绑定如果不严谨的类比,其实类似是打表 的思想。在用户登录(设备上线)时,通知业务server登录状态发生改变

同时业务层将该用户相关(userID)的会话(sessionID)信息,即群聊成员设备信息(did) 存储到各网关机的本地内存当中。这么说可能有点绕,下方分点阐述优化前后的工作流程:

(忽略错误情况)

前:

  1. 用户登录,设备上线。
  2. 用户发送上行消息,业务server无误收到,并准备消息下发。
  3. 在业务server中的分布式缓存中根据sessionID查询到userID,根据userID查询到deviceID,明确下发对象。
  4. 明确发送的deviceID后,消息下发。

(原架构示意图)

后:

  1. 用户登录,设备上线,通知业务server。
  2. 业务server将此用户所进入的会话sessionID及其设备信息deviceID先计算出来
  3. 业务server将相关设备信息存入指定网关机的本地内存当中(网关机的选择可以根据用户的地理位置或吞吐能力等因素进行选择)
  4. 用户发送上行消息
  5. 业务server无误收到,携带sessionID来到指定网关机当中
  6. 各网关机根据本地内存中session和device的映射关系,下发消息

(后架构示意图)

本质上,其实就是将原在业务server中查询缓存,分发到了各网关机当中(此处的冗余存储也许还可以优化?)。但是当用户量巨大时,造成的空间压力还是很大的。并且按照上方的设计,是就将登录用户的所有会话都存储入网关机当中。假如用户不发言 ,或某些群聊的活跃程度相当低,就白白浪费了这多份内存。

所以其实根据IM的设计用途,这里也需要具体问题具体分析。一种可行的解法是,将网关机存储这一过程(第3 步)的执行时机修改为:用户进入指定会话时 。这一修改,理论上有几率会降低用户首次进入不同会话后首条信息(后面会说明)的发送体验。但是对各网关机的空间存储都是一个优化。还是再说一下第二回修改后的工作流程吧:

  1. 用户登录,设备上线,通知业务server。
  2. 用户进入会话 or 发送上行消息 (进入会话本身和发送信息是两个独立的过程,并且发送信息必须在进入会话后)
  3. 业务server记录当前session,查询出该session所对应的所有deviceID(这里的查询还是要sessionId -> userId -> deviceId
  4. 将session和device的映射关系发送到指定网关机
  5. ....... , 根据关系下发信息。

注意,网关机本地内存存储的是同个session内所有的device的信息。也许你已经想到了,就是同个session内不同的用户,可以设法复用这一份本地内存 。譬如说,可以是 成功在网关机中存储之后,返回回执,并且在业务server本地记录缓存 ,表示各网关机内,有哪几个session的相关信息,其实就是一个从业务server下发至网关机的路由表 。这也是为什么有几率降低用户首次发信息的体验,即下方第二种情况:

  • 业务server指定表中,某个网关机存在指定session中device信息,路由到对应网关机并进行信息下发,无需进行其他操作。
  • 业务server指定表中,没有任何网关机有指定session的device信息。此时请求业务server查询映射关系记录到网关机后 。进行信息下发

(再次修改后的流程图 无device信息时的查询下发流程)

当然,多了一份本地内存,也多了一个需要维护一致性的对象😢。需要维护网关机本地内存与业务server中映射关系的一致性。可参考上文旁路模式相关的缓存策略。

这一方法本质上增加了网关机中的空间压力,但是解决了大Key带来的难处。但是整个运行的流程更加复杂。

热Key 问题

大Key 主要在于数据量大,而热Key主要在于访问量大,远远高于其他Key,从而造成性能瓶颈或系统不稳定。这种情况在分布式系统中尤为明显,可能导致某些节点过载,影响整体性能。

对热Key的处理可以分为 降频 & 止损

降频

想方设法降低请求打在目标单Key的频率

  • 备份存储副本 :注意,副本 ≠ 分片 ,指对热Key相同的内容存多几份。在读取的时候,根据负载均衡随机读。但是在更新的时候,需要同时更新多个副本,所以比较适用于读多写少缓存。刚好适配了IM中对映射关系的存储(有关映射关系的讨论见上一文)。
  • 本地LRU / LFU缓存热Key :上方在讨论大Key已经涉及到了网关机的本地缓存了。此处指的是将热Key数据存储在业务server内存中,可以是一个HashMap等数据结构;与分布式缓存相比,它可存储空间更少,但是存取效率更高。但是也面临同样的一致性问题 。上一文所讨论到的活跃群聊场景适合使用,但是必须需经过测试,评估出在保证一致性的环境下,更新的最优时限。(CAP)
  • 业务上拆分Key :这才是分片,通过将热Key中存储数据根据业务,进行进一步的解耦,拆分为多个Key,即可分担原热Key的压力。这种模式其实很适合搭配旁路缓存 ,在保证降温Key同时也保证了高一致性
止损

热Key大量的访问非常影响资源分配,非常影响服务器的运行。所以在无法优化的情况下。我们也需要设置一些兜底的方案。这些方案相比之下更具普遍性(只能算是个方向而非具体方案)。

  • 对热Key限流:人为限制key的访问量
  • 手动禁用 & 兜底策略:当热Key影响非常严重时,切换该服务的planB(如果有的话)

上述主要围绕session绑定,在业务上改善了大Key的问题。但是不分青红皂白的对全部会话情景(单聊 ,小群聊 & 非活跃群聊,大群聊 & 活跃群聊)使用session绑定。部分情况下还是比较浪费带宽等资源。并且,目前的架构本质上还是存在很严重的读放大 问题。注意,这个问题是存储架构决定的。本质上不能解决,只能应对不同情况而进行改善(会话规模)。之后将围绕此类问题再做讨论。

相关推荐
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平2 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
58沈剑3 小时前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构
BiteCode_咬一口代码3 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod4 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man5 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*5 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go