记录一次前端做请求负载处理的思考

记录一次前端做请求负载处理的思考

某一天,米卡收到了后端一个比较神奇的需求:

后端:米卡啊,咱们中间层的转发请求需要做一下处理,我们这边的前端会携带一个文章的ID过来,要求请求到的后端接口是请求池中的固定的一个 IP,除非这个 IP 已经被卸载掉,那么另外选择一个 IP 进行请求。

嗯?固定的IP请求?那就需要有某种映射关系吧,让我们来一步一步分析这个需求。

回复:什么时候要?我们确认一下技术方案先~

这个需求的本意其实是,在我们后端的文章相关服务中,文章的请求,会初始化后端服务 Pod 中相当大的一个内存,所以希望前端可以通过一定的规则来进行请求发送,固定的文章 ID 将会请求固定的 IP,如果只是这么说的话可能大家比较难理解,这里我给一张图给大家看一下。

记录一次前端做请求负载处理的思考

由于我们的后端服务绝大部分为 GRPC 接口,所以需要中间层进行一次请求的转发,将 HTTP 请求转为 GRPC 请求到后端服务,所以这里存在一层中间层的架构,中间层的存在也方便于进行接口的鉴权、基础接口的整合以及处理请求网关的部分。

最简单的方案

最简单?OK 啊,做一个 Key-Value 的对照不就行了。全局做一个唯一的 Map,当请求的文章 ID 存在于这个 Map 中的时候且指向一个 IP 的时候,认为已有选中的 IP 了,如果没有则随机取位置。

const map = {}

// 请求进入
const ip_pool = ['1.1.1.1', '2.2.2.2', '3.3.3.3']
const art_id = 100 // 会变动
if (map[art_id]) {
  return map[art_id]
} else {
  const ip_target = Math.floor(ip_pool.length * Math.random())
  return ips[ip_target]
}

看上去这个设计是美好的,但是问题也很明显,那就是全局的唯一性:我们中间层存在多个 Pod,启动了多个中间层的实例,实际上是无法做到这个 map 的唯一性的,因为多个中间层其实对这个 Map 并不共享,也无法通过环境变量,或者是访问内存来做到这一点。

后端请求

既然最简单的方案没办法确认请求映射关系的唯一性,我们考虑找一个可以控制的、唯一的来处理这个唯一性吧。我们可以使用后端的 http 口来做到这点,请求进入时,存在文章 ID 的请求,会带上这个文章 ID 额外去后端数据库中请求,如果存在 IP 记录,则返回这个 IP,没有则随机选择一个 IP,并向后端存下这个选中的 IP。

嗯,这个方案乍一听还可以,可以满足映射关系的唯一性,但是有一个致命的问题:高并发情况下的数据处理

为什么说这个方案容易在高并发情况下出现问题呢?我们用代码来举个例子🌰:

// 请求进入
const ip_pool = ['1.1.1.1', '2.2.2.2', '3.3.3.3']
const art_id = 100 // 会变动

// 请求后端的映射关系的函数
const map = await getMap({ key: art_id })
if (map[art_id]) {
  return map[art_id]
} else {
  const ip_target = Math.floor(ip_pool.length * Math.random())
  setMap({ key: art_id, value: ip_target })
  return ips[ip_target]
}

这里我们想象一个这样的场景,后端某个 Pod 被卸载或者整个 ip_pool 被清空了,会发生什么:中间层在 ip_pool 变动的前后,分别进入了两次请求,并且在几乎相同的时间向后端请求了一个 key: 100 的请求,接口告知说对应的 IP 为1.1.1.1,然而这个时候两个请求的状态是存在差异的,第一次的请求认为 ip_pool 存在这个已经被选中的 IP,所以直接发起了这个请求,并且导致了请求错误,第二次的请求认为 ip_pool 不存在这个已经被选中的 IP,所以随机了一个 IP 去做这个请求,请求了2.2.2.2,并告知后端接口更新这个映射关系。然而重要的是,这里可能有相同的十个请求,那么意味着这个setMap({ key: art_id, value: ip_target })的动作执行了相当多次,并根据并发的请求当时的情况,更新了不同的随机选中的 IP,直到波动稳定。

有没有一种方案可以有唯一的映射关系,同时还存在某种进程锁或者是说请求锁的东西呢?

出现吧,Redis!

诶,米卡,dev 环境的 Redis 刚才崩掉了,检查一下你的服务有没有影响

?嗯?我司的 Redis 还真容易崩呢,也不知道我的中间层有没有崩,偷偷上去看一下。。。

扯远了,在这个方案中,最不好被使用的应该就是 Redis方案了,一个是作为前端中间层,只为这个单独的需求接入 Redis 的成本相当高,这意味着以后的部署、研发都需要对此进行依赖和单独处理,另一个是,当存在 Redis 重启的时候,请求相关的映射关系都会被清空,那么在后端服务其实没有 IP 池变更的情况下,居然也出现了【不请求固定 IP】的情况,其实对于这个需求也就完全没有意义了,同理第一个方案如果中间层重启或者更新,也会存在问题。

哈希取模

事实上最后和后端讨论后发现,他们并不想考虑后端 Pod 的增加导致的取模紊乱,只需要这个功能能够在足够短的时间内给到 sdk 的最新版本,满足在稳定状态下,请求文章 ID 与一个固定后端 IP 对应即可,所以最符合要求的其实只有哈希取模,也就是用文章的 ID,根据某种算法,这里我们使用了 CRC16 算法来取一个 hash 值之后,对 ip_pool 来进行取模,从而得出一个固定的 IP,这样的话能够满足以下多个要求:

  1. 多个中间层,计算取到的值是同一个
  2. 高并发情况安全
  3. 后端 ip_pool 变更后,多个中间层计算取到的值依旧会是相同的
  4. 补充:后端可能也需要对这个IP进行相同的计算,这样可以确保其他后端的服务请求到的IP和前端是一致的。

最终给到后端的内容其实就是类似下面的处理,这里面 art_id 不存在的情况已经提前排除:

export function RandomNum(Min: number, Max: number) {
  var Range = Max - Min;
  var Rand = Math.random();
  var num = Min + Math.floor(Rand * Range); //舍去
  return num;
}
function getHash(ip_pool: string[], art_id: string) {
  const hashCode = crc16('X-25', art_id)
  const ips_path = Math.floor(hashCode / (65535 / ip_pool.length))
  const ip = ips_path < ip_pool.length ? ip_pool[ips_path] : ip_pool[RandomNum(0, ip_pool.length)]
  log('[Hash Check Info]', {
    art_id,
    ip_pool,
    ip,
    ips_path,
    timer: Date.now()
  })
  return ip
}

// 请求进入
const ip_pool = ['1.1.1.1', '2.2.2.2', '3.3.3.3']
const art_id = 100 // 会变动
if (ip_pool && ip_pool.length) {
  return ip_pool.length > 1 ? ip_pool[getHash(ip_pool, art_id)] : ip_pool[0]
} else {
  throw new BaseException(({
    msg: '未找到 IP',
    ...connectionInfo
  }));
}

不过作为总结的文章,这里我们还要考虑一个点,为了调节机器压力,事实上后端服务是很有可能会进行增减 Pod 的操作的,当一个 ip_pool 长度为 3 变成了 4,原来请求到1.1.1.1的文章 ID,就可能请求到3.3.3.3上,然而1.1.1.1并没有被移除,而是由于节点变更而导致的取模紊乱,从而导致哈希的大面积更新,并不满足【被移除才重新取值】的条件。

所以问题还是回到了一致性哈希,这里简单介绍一下一致性哈希,它实际上是将原本的线性哈希转为了一个哈希环,空间为 2 的 32 次方个桶,按 Pod 节点数对这个哈希环进行等分,当存在 Pod 缩减的时候,只需要将原本属于下一个节点的 hash 值,挪移到上一个节点内就可以,反向同理,避免了每次的节点变更而导致的大量取模紊乱。事实上单独在前端使用一致性 hash 的成本相对较高,因为在实际情况中,可能出现所有的 hash 集中存在于某一个 Pod 中,所有的负载全部压在那个机器之后,很容易造成节点雪崩,也就是压垮一个节点后,对应的大量负载持续性压在下一节点上,所以这种情况一般都需要配合 Nginx 做虚拟节点来处理这个问题。

原文链接:https://juejin.cn/post/7258924456050311226 作者:开心的米卡

(0)
上一篇 2023年7月24日 上午10:10
下一篇 2023年7月24日 上午10:21

相关推荐

发表回复

登录后才能评论