隔离|异步任务处理系统,如何解决业务长耗时、高并发难题?( 四 )


K8s 的 HPA 一般难以满足任务场景下的自动伸缩 。 Keda 等开源项目提供了按排队任务数等指标伸缩的模式 。 AWS 也结合 CloudWatch 提供了类似的解决方案 。K8s 一般需要配合队列来实现异步任务 , 队列资源的管理需要用户自行负责 。K8s 原生的作业调度和启动时间比较慢 , 而且提交作业的 tps 一般小于 200 , 所以不适合高 tps , 短延时的任务 。注意:K8s 中的作业(Job)和本文讨论的任务(task)有一些区别 。 K8s 的 Job 通常包含处理一个或者多个任务 。 本文的任务是一个原子的概念 , 单个任务只在一个实例上执行 。 执行时长从几十毫秒到数小时不等 。

大规模多租户异步任务处理系统实践 接下来 , 笔者以阿里云函数计算的异步任务处理系统为例 , 探讨大规模多租户异步任务处理系统的一些技术挑战和应对策略 。 在阿里云函数计算平台上 , 用户只需要创建任务处理函数 , 然后提交任务即可 。 整个异步任务的处理是弹性、高可用的 , 具备完整的可观测能力 。 在实践中 , 我们采用了多种策略来实现多租户环境的隔离、伸缩、负载均衡和流控 , 平滑处理海量用户的高度动态变化的负载 。
动态队列资源伸缩和流量路由 如前所述 , 异步任务系统通常需要队列实现任务的分发 。 当任务处理中台对应很多业务方 , 那么为每一个应用/函数 , 甚至每一个用户都分配单独的队列资源就不再可行 。 因为绝大多数应用都是长尾的 , 调用低频 , 会造成大量队列 , 连接资源的浪费 。 并且轮询大量队列也降低了系统的可扩展性 。
但如果所有用户都共享同一批队列资源 , 则会面临多租户场景中经典的“noisy neighbor”问题 , A 应用突发式的负载挤占队列的处理能力 , 影响其他应用 。
实践中 , 函数计算构建了动态队列资源池 。 一开始资源池内会预置一些队列资源 , 并将应用哈希映射到部分队列上 。 如果某些应用的流量快速增长时 , 系统会采取多种策略:
如果应用的流量持续保持高位 , 导致队列积压 , 系统将为他们自动创建单独的队列 , 并将流量分流到新的队列上 。将一些延时敏感 , 或者优先级高的应用流量迁移到其他队列上 , 避免被高流量应用产生的队列积压影响 。允许用户设置任务的过期时间 , 对于有实时性要求的任务 , 在发生积压时快速丢弃过期任务 , 确保新任务能更快的处理 。 负载随机分片 在一个多租环境中 , 防止“破坏者”对系统造成灾难性的破坏是系统设计的最大挑战 。 破坏者可能是被 DDoS 攻击的用户 , 或者在某些 corner case 下正好触发了系统 bug 的负载 。 下图展示了一种非常流行的架构 , 所有用户的流量以 round-robin 的方式均匀的发送给多台服务器 。 当所有用户的流量符合预期时 , 系统工作得很好 , 每台服务器的负载均匀 , 而且部分服务器宕机也不影响整体服务的可用性 。 但当出现“破坏者”后 , 系统的可用性将出现很大的风险 。

如下图所示 , 假设红色的用户被 DDoS 攻击或者他的某些请求可能触发服务器宕机的 bug , 那么他的负载将可能打垮所有的服务器 , 造成整个服务不可用 。

上述问题的本质是任何用户的流量都会被路由到所有服务器上 , 这种没有任何负载隔离能力的模式在面临“破坏者”时相当脆弱 。 对于任何一个用户 , 如果他的负载只会被路由到部分服务器上 , 能不能解决这个问题?如下图所示 , 任何用户的流量最多路由到2台服务器上 , 即使造成两台服务器宕机 , 绿色用户的请求仍然不受影响 。 这种将用户的负载映射到部分而非全部服务器的负载分片模式 , 能够很好的实现负载隔离 , 降低服务不可用的风险 。 代价则是系统需要准备更多的冗余资源 。

接下来 , 让我们调整下用户负载的映射方式 。 如下图所示 , 每个用户的负载均匀的映射到两台服务器上 。 不但负载更加均衡 , 更棒的是 , 即使两台服务器宕机 , 除红色之外的用户负载都不受影响 。 如果我们把分区的大小设为 2 , 那么从3台服务器中选择2台服务器的组合方式有 C_{3^{2=3 种 , 即3种可能的分区方式 。 通过随机算法 , 我们将负载均匀的映射到分区上 , 那么任意一个分区不可服务 , 则最多影响1/3的负载 。 假设我们有100台服务器 , 分区的大小仍然是 2 , 那么分区的方式有 C_{100{2=4950 种 , 单个分区不可用只会影响 1/4950=0.2% 的负载 。 随着服务器的增多 , 随机分区的效果越明显 。 对负载随机分区是一个非常简洁却强大的模式 , 在保障多租系统的可用性中起到了关键的作用 。