思考(四十三):服务器架构中 session affinity 的应用

session affinity

session affinity, 会话亲和性,又称会话保持。通常用于 web service 的微服务部署中。

如,K8S 编排脚本中,有字段 sessionAffinity 来设置,使得 1 客户端请求都投递到 1 Pod 上。

这样可以有效的避免请求被投递到多个 Pod 上,造成的并发问题。

在设计服务器架构的登陆环节,通过引入 session affinity 的概念,有效的降低了登陆的复杂性,且安全性得到保证。

在正式介绍 session affinity 如何应用于 服务器架构的登陆环节 前,我们先看个反面例子。

通过定时互斥锁的登陆流程

先看时序图:

Created with Raphaël 2.1.2 Client Client LoginServer LoginServer MgrServer MgrServer Gateway Gateway Other Gateway Other Gateway Redis Redis 1. 账号登陆 1. 账号相关验证(略) 2. 本地缓存中选取一个Gateway 2. SET EX NX { uid, lock } (定时互斥锁;设置失败则本次登陆失败,流程结束) 2. GETSET { uid, gatewayid }(设置并返回旧gatewayid) 2. 返回上次分配的gatewayid 2. SET { uid, token } 键值对 3. 请求转发 踢人(uid) 给特定的Gateway 3. 转发 踢人(uid) 给特定的Gateway 3. 请求转发 返回OK 3. 转发返回OK 3. DEL(删除定时互斥锁;做下过期判断,已过期了就不用删了 ) 3. 返回OK, IP/Port/Token

分析:

  • SET EX NX 设置锁,使得只有 1 个 LoginServer 可以进行登陆流程,确保了登陆安全性
  • 锁是可过期的,是考虑到 LoginServer 可能失效,导致死锁问题

缺陷:

  • 锁的过期时间是个问题,时间过长,LoginServer 的失效会导致,锁过期期间,账号无法登陆
  • 锁的过期时间是个问题,时间过短,本次登陆流程未走完,而下次登陆可能重入
  • 3. DEL 操作必须确保能在到期前被执行到,不然会有可能的时序问题,因此锁的过期应该设置的不能太短,如1 分钟

总结:

  • 该登陆过程存在小概率登陆问题
  • 相对复杂,涉及多个 Gateway

基于 session affinity 的登陆流程

Created with Raphaël 2.1.2 Client Client LoginServer LoginServer Redis Redis Gateway Gateway 1. 账号登陆 1. 账号相关验证(略) 2. 本地缓存中选取一个Gateway 2.1 SET NX { uid, gatewayid } 键值对 2.1 如果设置失败 2.1 GET { uid, gatewayid } 键值对 2.1 返回 gatewayid 2.2 如果返回的 gateway 已失效 (小概率) 2.2 DEL { uid, gatewayid } 键值对 (本次登陆流程失败) 3. SET { uid, token } 键值对 4. 返回OK, IP/Port/Token 5. 账号登陆 Gateway 等等(略) 5.1 EXPIRE { uid, gatewayid } 键值对(设置过期1年) 6. 账号连接断开事件触发 6. EXPIRE { uid, gatewayid } 键值对(设置过期10分钟)

分析:

  • SET NX 保证只有 1 个账号设置成功
  • 如果设置失败的,则获取 Gateway,因此保证了 1 个账号同时登陆,始终对同一个 Gateway , 从而保证了 session affinity。

缺陷:

  • gateway失效这里,还是可能导致问题

    2.2 步骤中的 DEL 可能会把 新登陆请求的 SET NX 删除 (概率非常小)

    因此需要用 redis module 做一个 DELX :条件删除, if value = “xxx” then del key

  • 6 步骤中的 EXPIRE 后,有小概率 可能会把 新登陆请求的 SET NX 删除

    因此需要用 redis module 做一个 SETX : 不管 SET NX 有没有设置成功,都重置过期时间为 1 年

总结:

  • 流程简化很多
  • 通过对 DEL 、SET 命令打下补丁,可以保证登陆流程安全

基于 session affinity Lobby 服务请求处理

Lobby服务有 2 种,有状态、无状态。

2 种 Lobby 都有需求,底层机制支持 session affinity 。

Created with Raphaël 2.1.2 Client Client Gateway Gateway Redis Redis Lobby Lobby 1.0 登陆 Gateway 1.0 客户端连接 Token 验证、Session 创建等等完毕后 1.1 Get { Client Session Id, Lobby Id } 键值对 1.1 返回结果 1.2 如果没有或者 Lobby 连接已失效 1.2 本地缓存中获取 1 Lobby 连接 (轮询方式即可) 1.2 Set { Client Session Id, Lobby Id } 键值对 1.3 本地保存 { Client Session Id, Lobby Id } 键值对 1.4 返回登陆 Gateway 成功 2.0 Lobby 协议请求 2.1 如果 Lobby 连接已失效 2.1 踢人处理 2.1 Del { Client Session Id, Lobby Id } 键值对(流程结束) 2.2 如果 Lobby 连接 2.2 转发请求 2.2 处理请求 2.2 处理结果 2.2 转发结果 3. 触发客户端断开连接事件 3. Del { Client Session Id, Lobby Id } 键值对(流程结束)

分析:

  • 由于 Client 只连接 1 个 Gateway ,对 redis 的操作不存在竞态。因此不需要锁机制
  • 通过在 redis 上, 保存 { Client Session Id, Lobby Id } 键值对,保证 session affinity

缺陷: 无

总结

还有 2 个问题:

  • 以上时序过程分析中,都没有提到 Redis 失效。
    上面的都是基于 Redis 不会失效的。
    运维有能力达成 Redis 不失效。

  • Login 判断 Gateway 是否失效问题。
    极小概率存在 有 Login 判断 Gateway 失效;而有 Login 判断 Gateway 有效
    一般多出现在多机柜部署时可能会发生吧
    这种情况上述方法也无法解决

你可能感兴趣的:(Go游戏服务器开发的一些思考)