即时通讯系统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绑定。部分情况下还是比较浪费带宽等资源。并且,目前的架构本质上还是存在很严重的读放大 问题。注意,这个问题是存储架构决定的。本质上不能解决,只能应对不同情况而进行改善(会话规模)。之后将围绕此类问题再做讨论。

相关推荐
M1A115 小时前
小红书重磅升级!公众号文章一键导入,深度内容轻松入驻
后端
0wioiw016 小时前
Go基础(④指针)
开发语言·后端·golang
南山二毛16 小时前
机器人控制器开发(导航算法——导航栈关联坐标系)
人工智能·架构·机器人
只因在人海中多看了你一眼16 小时前
B.50.10.10-微服务与电商应用
微服务·云原生·架构
李姆斯17 小时前
复盘上瘾症:到底什么时候该“复盘”,什么时候不需要“复盘”
前端·后端·团队管理
javachen__18 小时前
Spring Boot配置error日志发送至企业微信
spring boot·后端·企业微信
seabirdssss18 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续18 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升
Lei活在当下19 小时前
【业务场景架构实战】1. 多模块 Hilt 使用原则和环境搭建
性能优化·架构·客户端
OC溥哥99919 小时前
Flask论坛与个人中心页面开发教程完整详细版
后端·python·flask·html