如何做到秒级扩容1000加业务节点

写在前面

这里如果想要了解这个项目的背景可以看我之前写过的一篇文章.
经过一段时间业务方的打磨在弹性扩容上我们遇到了很多问题, 这里我简单列举一下:

  1. 扩容时间长:ecs自动申请分钟级耗时;
  2. 服务响应慢:ecs申请到之后还需要加入k8s集群以及安装os和相关的网络资源;
  3. 服务发现模式单一:仅支持基于svc的rr负载;
  4. 容器之间隔离不健康:容器之间的隔离使用污点进行配置,操作繁琐并且依然存在抢占的肯能行;
  5. 服务扩缩容断流:扩缩容时存在流量闪断的情况;
  6. 大量弹性需求导致集群ip不够用:ip可用端超出集群上线;

对于上面的6个问题我会在下文中一一做出解答, 这些问题的答案也是我们如何做到秒级扩容1000加业务节点的方法. 希望能帮助到大家在k8s以及云原生相关的设计中带来一些启发.

一. 扩容时间长/服务响应慢

再使用ECS作为扩容节点时主要流程如下图所示:

  1. 到达定时扩容的时间,触发扩容动作;
  2. 判断当前弹性伸缩池中是否还有可用的机器,如果没有则申请新的ECS资源;
  3. 等待ECS初始化结束之后将其加入到k8s集群中,作为一个可用节点,初始化node节点,安装一些DaemonSet以及网络插件;
  4. 拉取镜像,进行容器初始化;
  5. 将服务注册到eureka上等待接受请求;
  6. 完成扩容动作,pod status转为Running.

通过梳理上述流程我们可以发现动作2/3/4是拖慢扩容的主要原因, 也是整个ECS扩容流程时延在分钟级别的主要原因. 为了是扩容的动作时间缩短到秒级我们就需要解决2/3/4让其可以快速的加入集群进行初始化, 快速的拉取镜像(甚至是不需要拉取镜像).

替换容器载体为ECI

为了解决之前提到的问题这里会引入一个新的技术–ECI.

阿里云弹性容器实例(Elastic Container Instance)为Kubernetes提供了基础的容器Pod运行环境,但业务间的依赖、负载均衡、弹性伸缩、定期调度等能力依然需要Kubernetes来提供。本文为您介绍如何通过Kubernetes集群与ECI对接,使用ECI作为Pod的运行资源。
ECI为您提供免运维、弹性、低成本、快速启动的容器运行环境。

  • Serverless:容器基础设施托管
    您只需要提交容器镜像,无需关心底层服务器。不需要预先创建集群和集群的维护,也不需要关注运行过程中的容量规划,专注业务领域创新。
  • 弹性:灵活部署
    以阿里云全球计算基础设施作为资源池,按需提供海量、高并发、多种资源类型(CPU、高主频、GPU、本地盘大数据计算加速)的容器计算资源。
  • 低成本:按量按秒收费
    按实例启动到结束时间段消耗资源收费。配合Kubernetes或者您自建的调度系统,ECI可根据业务流量自动弹性伸缩,减少空置费用。
  • 效率:秒级启动
    实例快速启动,从容面对突发访问,不需要提前预估集群和业务流量,按需扩容,轻松应对百倍的业务突发流量。
  • 兼容 Kubernetes
    Kubernetes集群上Pod能直接调度至ECI。无缝集成至阿里云容器服务托管版(ACK)和Serverless版(ASK),同时支持通过virtual kubelet对接用户自建Kubernetes。
  • 服务集成
    自动集成阿里云上产品,例如:SLS(日志服务)、NAS(文件存储)、云盘、ARMS(监控服务)等。

上述内容简单介绍了ECI的特点, 我们可以知道基于ECI部署的服务是秒级扩容的这个特性可以帮助我们解决之前的问题2.
当ECI与k8s进行结合的时候使用需要在k8s集群中创建一个虚拟节点"如下图所示

这个节点使用来处理ECI的流量, 实际的工作场景如下图所示

通过上图可以了直观的看到想要在k8s中使用ECI只需要在调度pod的时候将其指定到virtual-node-eci-0这个node上就可以了.
由于virtaul-node-eci-0这个虚拟节点是提前加入到k8s及群中的所以在创建ECI时不需要重新创建网络插件而是直接复用虚拟节点的就可以.

通过这些操作我们可以规避调之前提到的2和3问题.

实际应用

由于cronhpa的deployment和原有的deployment是完全独立的所以只需要在生产CronhpaDeployment的时候将nodeName指定进去就好

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: nginx-base-eci
......
      serviceAccountName: filebeat-test
      nodeName: virtual-node-eci-0
      containers:
      # 指定ECI nodename

        - name: nginx-base-eci

          image: registry.cn-beijing.aliyuncs.com/ailab-paas/nginx:1.1.1
......

实际部署效果如下图所示

二. 服务发现模式单一

这个问题的产生和我们最初的集群架构设计存在一定关系, 我们是基于SpringCloud+k8s实现的整体框架, 服务发现使用到了Eureka但是实际的请求负载是交给svc去做的, 整体网络结构如下图所示.

在负载的时候, 请求进入gateway, 之后从eureka上获取对应的服务信息, 获取其中的hostname, gateway拿着hostname去请求对应的svc, 最终的负载是交给svc去做的, 但是基于k8s部署的svc只有一种负载策略就是RR.
对于扩出来的cronpod我们只能将他加入到现有的svc中作为ep中的一个端点使用RR的方式处理请求. 但如果想要以权重的方式配置负载就无法实现.

解决方法

在处理这个问题的时候需要考虑如何实现按照权重进行负载, 对于gateway而言, 可以在转发请求的时候根据hostname的不同进行权重负载, 这也就意味着在向eureka注册的时候同一个application下需要有不同的hostname进行注册. 具体的网络结构如下图所示

对于弹性扩容出来的pod单独指定一个svc这样在配置权重的时候可以指定具体的svc想要匹配的权重大小

三. 容器隔离不健康

原有的弹性容器组在进行隔离的时候是居于污点进行隔离的, 但是基于污点的隔离操作无法做到精准控制, 如果拥有同样的容忍的类型的业务同事部署仍然会出现资源抢占的问题.

对于cronhpa的需求场景是需要尽量做到稳定扩容的, 如果出现抢占的情况就会造成资源无法稳定的完成部署. 之前提到的ECI在k8s场景中每个pod的部署都是完全隔离的, 一个ECI容器只会搭载一个pod, 而ECI的使用时从资源池中将资源划分出来的, 所以在这种场景下资源可以做到物理层面的隔离, 可以大大的提高服务的稳定性, 防止在扩容任务并发是产生资源抢占导致服务迟迟无法部署的问题.

四. 服务扩缩容断流

在进行服务扩缩容的时候会造成服务断流, 这个问题的主要原因是解决服务发现模式单一而引入的, 在之前的逻辑中负载均衡是交给k8s的svc去做的, 但是当引入了权重负载的概念后同时也增加了一个service, 对于扩容服务而言当进行缩容时所有的pod会全部下掉, 但是在网管中仍然保留了之前的缓存, 这也就意味着请求通过网关依然会转发到后端服务上, 为了解决这个问题我们需要在服务下线之前通知eureka将自己注销掉, 并且对于pod自身仍然需要保持Running状态持续接收网关发出的请求.

解决方法

k8s对于生命周期的处理自身是提供了preStop这样的监听去处理的但是当pod进行下线时出发了preStop中的逻辑就会将pod的状态立刻转为terminating同时对于svc而言会立刻把pod踢出
ep这也就意味着svc不会选择这个pod作为转发的地址.

如果是对于之前的情况(只是用同一个svc作为负载)那是没有问题的, 因为gateway最终获得的是svcName无论是原有的pod还是扩容出的pod使用的都是同一个svc这样gateway中的instance即使得到的是定时扩容的实例, 在负载时也会交到同一个svc上, 这样流量就会转到原有的pod上进行处理, 详情见下图.

支持了权重负载之后由于创建了两个svc所以请求获取到扩容出来的pod是取到的svcName实际上是专门负责处理定时扩容的pod由于流量
为了解决这个问题我们需要从另一个方面入手

cronhpa控制器

cronhpa控制器是用来处理定时伸缩任务的, 它在k8s是将任务作为CRD对象保存在etcd中, 通过cronhpa controller去管理定时任务, 到达伸缩的时间后通过clientSet修改对应的Deployments的副本个数.
了解了cronhpa的基本逻辑之后我们可以推断出修改Deployments中replicas个数的动作是由cronhpa的controller发出的, 所以可以在到达缩容时间后让控制器去请求eureka主动下掉对应的application然后在去修改Deployments的replicas完成缩容.

上图是修改之后的流程:

  1. 当到达缩容时间后,由cronhpa控制器用被缩容服务instanceId从control服务获取对应的下线脚本;
  2. 返回脚本
  3. 执行脚本下线对应服务
  4. 等待60秒钟让网关重新从eureka获取注册列表
  5. 通知对应的deployment进行伸缩
  6. 通知ApiServer删除对应的Pod
  7. EP发现Pod状态不为Running,将其ip移除
  8. 执行后续preStop并结束缩容动作

五. 扩容需求过多导致ip不够用

在k8s集群中每个node会有属于自己的ip范围, 这个ip范围来自于同一个网段, 如果我们的网段地址是C类地址, 可用总数约为200w个在ACK(阿里云k8s容器服务)每添加一台机去需要提前指定可用ip范围, 指定范围之后这段ip就只能被这一台机器使用, 如果一台机器我们指定可用ip为4096个那么集群内可以添加的机器总数上线就是488个, 随着业务规模的增长我们的k8s集群内的node个数早已逼近这个范围, 所以当需要扩容1000个业务pod时使用传统的ECS方案已经无法满足了.

在之前介绍的ECI中有提到, ECI加入到k8s集群时并不是作为一个node节点加入进去的, 而是提前创建一个虚拟node然后在虚拟node中进行一次跳转, 将请求路由到ECI的具体服务上

通过这两张图可以看到虚拟节点的ip与原有node不属于同一组, 这样在扩容的时候就可以很好地隔离原有pod与cronpod的ip资源防止大量的扩容请求导致线上正在运行的业务受到影响.

总结

上面的内容就是我们目前在定时伸缩方向上的探索,网校,题拍拍,培优,智康,小猴等部门的考验已经做到了在使用定时伸缩的前情况下达到了日调用了五个九的稳定性.
但是目前仍然有很多问题需要解决:

  1. 如何加快拉取镜像的速度
  2. 如何更优雅的进行eureka下线(避免修改cronhpa control的代码)
  3. 增加伸缩动作相关的告警
Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐