kubernetes nginx graceful scale & update

What is graceful scale

kubernetes应用slb+多个Pod部署时,某些Pod容器终止时不丢弃请求,用户无感知。以HTTP为例,切断容器入向流量,等待应用接受的请求全部执行完毕,再关闭容器进程。注意,官方文档preStop阶段nginx -quit根本不清真,自己ab一下就会发现问题。

How

参考kubernetes1.8的文档 https://kubernetes.io/docs/concepts/workloads/pods/pod/ 似乎kubernetes关闭容器时并不会事先切走流量,可以通过实验证实,假设现有1个部署,包含两个nginx容器,通过slb负载均衡,关掉其中一个观察。

具体操作是weighttp持续压测 weighttp -n 10000 -c 10 http://IP ,同时进行实例缩容 kubectl scale deployment nginx-deployment --replicas=1 观察到异常请求。

Google+Gayhub

先贴出官方几个issues来看看, https://github.com/kubernetes/contrib/issues/1123,还有一些官方教出来的错误姿势https://pracucci.com/graceful-shutdown-of-kubernetes-pods.html、https://segmentfault.com/a/1190000008233992 、https://stackoverflow.com/questions/39493043/graceful-pod-termination,被这些错误的姿势坑了一天,有一句mmp不知道当讲不当讲。

Try #1

经过类似上面的压测实验,readinessProbe的确可以切除故障的Pod,正所谓条条大路通罗马,我们可以制造Pod故障的假象刻意使流量不再路由到这个Pod,虽然稍微Hack了点不过在k8s目前版本似乎没什么好办法。

修改deployment如下

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: nginx
        image: registry-vpc.cn-hangzhou.aliyuncs.com
        ports:
        - containerPort: 80
        readinessProbe:
          exec:
            command:
            - cat
            - /tmp/online
          initialDelaySeconds: 5
          periodSeconds: 5
        lifecycle:
          preStop:
            exec:
              command: ["sh", "-c", "rm /tmp/online && sleep 20"]
          postStart:
            exec:
              command: ["sh", "-c", "touch /tmp/online"]

测试命令如下:weighttp -n 10000 -c 10 http://IP 成功完成了0失败!
然后再测试Keepalive的请求weighttp -k -n 10000 -c 10 http://IP,果不其然丢了6个请求。

Keepalive

https://www.chrismoos.com/2016/09/28/zero-downtime-deployments-kubernetes/ 这个作者索性在服务前面再加了一个nginx来实现graceful具体原理是对外Keepalive对内不Keepalive,的确这样可以不丢请求,随着请求数增加吃枣药丸。于是不得不找其他办法,于是我又去看Nginx源码找到关于keepalive连接的实现,在 ngx_http_request.c 完成请求处理时会检查是否需要保持一个TCP连接Keepalive(或者延迟关闭)

    r = r->main;

    if (r->reading_body) {
        r->keepalive = 0;
        r->lingering_close = 1;
    }

    if (!ngx_terminate
         && !ngx_exiting
         && r->keepalive
         && clcf->keepalive_timeout > 0)
    {
        ngx_http_set_keepalive(r);
        return;
    }

    if (clcf->lingering_close == NGX_HTTP_LINGERING_ALWAYS
        || (clcf->lingering_close == NGX_HTTP_LINGERING_ON
            && (r->lingering_close
                || r->header_in->pos < r->header_in->last
                || r->connection->read->ready)))
    {
        ngx_http_set_lingering_close(r);
        return;
    }

    ngx_http_close_request(r, 0);
}

从语义上得出keepalive_timeout指的是处理完完整的HTTP请求之后经过多少秒再没读到数据则关闭连接。

Nginx reload

这个过程比较复杂,想了解细节可以看看ngx_process_cycle.c,大致过程就是新启动一个Worker process然后等老的process处理完再关闭达到平滑重启的目的。根据源码来看,执行reload之后会老的process进入exiting状态会设置ngx_exiting=1,已经Handle的TCP连接不再进入keepalive逻辑。

Finally

设置nginx参数keepalive_timeout 3s,3s,这个参数不建议超过10秒,网上甚至还有教你设置成300怕不是没遇到过ddcc。

如果你的kubernetes >= 1.8.4恭喜你,k8s在删除pod之前会确保不在会有新的链接进来,因此只需要注意正确处理长连接即可。
nginx -s reload && sleep 3 && nginx -s quit && sleep 5 测试通过。如果你的kubernetes < 1.8.4 那么请升级or往下看。

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: nginx
        image: registry-vpc.cn-hangzhou.aliyuncs.com
        ports:
        - containerPort: 80
        readinessProbe:
          exec:
            command:
            - cat
            - /tmp/online
          initialDelaySeconds: 5
          periodSeconds: 5
        lifecycle:
          preStop:
            exec:
              command: ["sh", "-c", "rm /tmp/online && sleep 10 && nginx -s reload && sleep 1 && nginx -s quit" && sleep 5"]
          postStart:
            exec:
              command: ["sh", "-c", "touch /tmp/online"]

通过测试!

Why

更通俗的解释一下整个Hack的流程

  1. sleep 10 保证代码执行完这一句的时候不再有新的请求TCP连接进来,这个值10 >> periodSeconds。
  2. nginx -s reload 强制性让ngx_exiting = 1,此后进入的HTTP请求回复Keepalive:close
  3. sleep 1 没看代码,不知道这个nginx reload 是同步的还是异步的,总之先睡一会。
  4. nginx -s quit nginx优雅退出,这个是立即返回的。
  5. sleep 5 这个是等nginx里面请求真正处理完,正常来说业务不会超过1秒更graceful可以用wait命令。
添加新评论