1. 背景

某线上应用部署在ack集群内,用于根据用户定义的http请求项配置,周期性检查http接口的请求响应状态,接口地址为内网或公网,当接口地址为公网时,频繁出现请求超时的报警错误。经过排查发现,在公网请求发生超时的时刻,集群vpc NAT带宽达到上限200Mbps,也就是25MB/s

分析:由于集群占用vpcvpc内所有ip(node和pod的ip)都属于vpc内各子网地址,这些地址出公网的请求共用vpc绑定的公网NAT,在某时刻子网ip请求公网使用的EIP是随机的,无法控制,会产生和其他ip同时请求公网使用同一个EIP的情况,由此发生了带宽内拥挤,超限的情况,因此上述超时报警其实是客户端带宽不够产生的误报

2. 解决方案

为了避免发生上述情况,解决办法是避免这些特定的pod请求公网时和其他ip发生公网带宽的争抢,走独立的公网出口,有以下两种方案:

  • 方案一

将这些pod调度到特定的子网nodenodeippodip会使用特定的子网,这个特定的子网由于在特定的vSwitch虚拟交换机下,因此可以为这个子网单独指定路由条目,也就是将请求公网地址的请求的下一跳路由到特定的公网NAT,特定的公网NAT使用特定的EIP且不和其他子网共用

  • 方案二

ack集群的网络使用的是阿里云Terway,在此前提下,阿里云提供了为Terway网络中的Pod挂载独立的公网EIP的解决方案,具体可以查看文档说明,简单来说就是阿里云提供了集群内的控制器,实现了通过k8s原生的声明式配置,调用EIP产品相关api动态为pod绑定EIP的功能

3. 方案选取和实现

方案一,改动较大,需要将pod调度到特定的node上,且需要人工维护路由条目,维护性较差;
方案二,更为直接,在安装控制器插件后,通过给pod添加特定注解实现目的,主要分为两种方式:

  • 一种是根据声明式配置动态随机购买EIP
apiVersion: apps/v1
kind: Deployment
metadata:
  name: example
  labels:
    app: example
spec:
  replicas: 1
  selector:
    matchLabels:
      app: example
  template:
    metadata:
      labels:
        app: example
      annotations:
        k8s.aliyun.com/pod-with-eip: "true"  # 自动创建并绑定EIP
        k8s.aliyun.com/eip-bandwidth: "200"  # EIP峰值带宽
        k8s.aliyun.com/eip-internet-charge-type: "PayByTraffic" # EIP的计量方式
        k8s.aliyun.com/eip-instance-charge-type: "PostPaid"  # EIP的计费方式
        k8s.aliyun.com/eip-name: "app-eip"  # EIP名称
        k8s.aliyun.com/eip-description: "app-eip"  # EIP描述
  • 一种是先购买EIP,pod注解中声明EIP的id
apiVersion: apps/v1
kind: Deployment
metadata:
  name: example
  labels:
    app: example
spec:
  replicas: 1
  selector:
    matchLabels:
      app: example
  template:
    metadata:
      labels:
        app: example
      annotations:
        k8s.aliyun.com/pod-eip-instanceid: eip-2zeXXXXXx  # EIP ID

3.1 配置RAM策略并安装插件

按照文档操作,配置挂载EIP所需的RAM权限
安装插件ack-extend-network-controller,安装时启用插件pod eip的能力,一键安装即可

3.2 购买EIP

由于业务目前只有1pod,为了后续排查方便,选择通过先购买EIP再根据EIP id绑定的方式
购买EIP,按量付费,带宽上限为200Mbps

3.3 为pod添加注解

deploymentyaml中为pod添加注解,根据EIP id绑定这个EIP即可

spec:
  template:
    metadata:
      annotations:
        k8s.aliyun.com/pod-eip-instanceid: eip-xxx

绑定后效果

  • 集群cr

控制器会自动创建一个和pod名称相同的PodEIPcr,从这个crstatus中可以看到和上面购买的EIP相关的信息

~ kubectl get pods -o wide|grep app
app-79dcf755fb-ks2ld                                       1/1     Running             0                47h     10.245.36.4      ack-010245035222packets-spot       <none>           1/1
~ kubectl get podeips app-79dcf755fb-ks2ld -o yaml
apiVersion: alibabacloud.com/v1beta1
kind: PodEIP
metadata:
  creationTimestamp: "2024-04-01T07:55:46Z"
  finalizers:
  - podeip-controller.alibabacloud.com/finalizer
  generation: 1
  name: app-79dcf755fb-ks2ld
  namespace: dev
  resourceVersion: "2389078403"
  uid: 292773d3-80b4-4c88-b0ef-17f011a1530e
spec:
  allocationID: eip-xxxxx
  allocationType:
    releaseStrategy: Follow
    type: Static
status:
  eipAddress: 101.xxx.xxx.5
  internetChargeType: PayByTraffic
  isp: BGP
  name: app独享
  networkInterfaceID: eni-2zexxxxxxxqvrrpxxx
  podLastSeen: "2024-04-01T07:39:39Z"
  privateIPAddress: 10.xxx.36.4
  resourceGroupID: rg-xxxxx5afyhf3xky
  status: InUse
  • EIP绑定情况,与从集群查看cr得到的状态一致

3.4 其他说明

以上实现了pod绑定特定EIP的功能,在此条件下,pod请求公网时会固定为此eip出公网,为了保障配置的稳定,经过测试,上面提到此业务pod只有一个副本,且通过deployment管理,属于无状态应用,只做上面为pod添加注解的方式会有以下问题

3.4.1 如何控制当pod状态变为ready后才绑定EIP?

控制器会在Pod IP分配后,为Pod配置EIP地址,Pod Ready状态可能早于EIP绑定成功时间。解决办法是为pod添加就绪前的检测

  • 一种方式是为Pod配置Readiness gates
kind: Pod
...
spec:
  readinessGates:
  - conditionType: "k8s.aliyun.com/eip"
...
status:
  conditions:
  - lastProbeTime: "2022-12-12T03:45:48Z"
    lastTransitionTime: "2022-12-12T03:45:48Z"
    reason: Associate eip succeed
    status: "True"type: k8s.aliyun.com/eip
...
  • 一种方式是为Pod配置init container,在init container中检查EIP是否已经分配成功
apiVersion: v1
kind: Pod
metadata:
  name: example
  annotations:
    k8s.aliyun.com/pod-with-eip: "true"
spec:
  containers:
  - name: example
    image: busybox:1.28
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init
    image: busybox:1.28
    command: ['timeout', '-t' ,'60', 'sh','-c', "until grep -E '^k8s.aliyun.com\\/allocated-eipAddress=\\S?[0-9]+\\S?' /etc/podinfo/annotations; do echo waiting for annotations; sleep 2; done"]
    volumeMounts:
      - name: podinfo
        mountPath: /etc/podinfo
  volumes:
    - name: podinfo
      downwardAPI:
        items:
          - path: "labels"
            fieldRef:
              fieldPath: metadata.labels
          - path: "annotations"
            fieldRef:
              fieldPath: metadata.annotations

3.4.2 如何控制pod更新时,EIP始终只绑定了一个pod?

pod如果发生了滚动更新,且pod在配置有探针的情况下,可以保障始终只有一个pod接收流量,但是无法保证EIP的正常绑定,因为发布过程中有两个pod同时绑定了这个EIP,新pod启动后,老的pod下线调用了解绑EIP的动作,EIP绑定是需要调用接口到vpc去绑定,只有绑定了这个EIP pod的后续探针才会ready。在新pod滚动更新的过程中,会重新绑定EIP,但是旧pod的回收,又会卸载绑定这个EIP

这里EIP当做了创建pod所需的基础资源,实际上EIP并不是pod运行所必须的,解决办法:

  • A. 修改控制器实现支持在pod滚动更新结束后才将新的pod ipEIP绑定(下述C可以避免此问题)

  • B. 将pod的滚动更新模式修改为销毁重建Recreate,这样会损失一定流量

  • C. 控制器支持有状态应用的pod在一定时间内发生更新后仍然使用之前的EIP,因此把poddeployment改为statefulset,并声明pod在更新过程中仍然使用之前的EIP即可(固定EIP可以保证Pod重建后依然使用之前的EIP地址。该策略可与自动分配EIP能力结合,用于有状态应用的固定EIP

      annotations:
        k8s.aliyun.com/pod-with-eip: "true"
        k8s.aliyun.com/pod-eip-release-strategy: "10m"

结合现状,业务pod如果支持多副本模式,可以切换到statefulset并创建2个副本,有状态应用的滚动更新本身就是副本销毁和重新创建,在多副本的情况下不会有流量丢失。目前只有一个(暂不支持多副本模式),且可以接受在升级的过程中丢失一定的流量,在不改变原有的无状态应用属性的情况下,选择的解决办法为B

最终配置如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  strategy:
    type: Recreate
  template:
    metadata:
      annotations:
        k8s.aliyun.com/pod-eip-instanceid: eip-XXXXXXXlus72fax
    spec:
      readinessGates:
      - conditionType: "k8s.aliyun.com/eip"

3.4.3 pod绑定EIP后pod本身监听的端口是否也就通过EIP暴露?

pod由于没有通过NAT请求公网,是EIPPod IP(ecs 弹性辅助网卡)直接绑定,因此默认情况下,通过pod ip(vpc内网):端口可以直接访问到pod暴露的接口,通过eip(公网):端口也可以访问到,但是由于EIP绑定的是ecs的弹性辅助网卡,因此和ecs共用了一个安全组,ecs是集群node,只开放了集群子网间互通,因此这个问题已经规避