<p>I have a GKE cluster (1.12.10-gke.17).</p>

<p>I'm running the <a href="https://github.com/kubernetes/ingress-nginx" rel="nofollow noreferrer">nginx-ingress-controller</a> with <code>type: LoadBalancer</code>.</p>

<p>I've set <code>externalTrafficPolicy: Local</code> to <a href="https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-type-loadbalancer" rel="nofollow noreferrer">preserve the source ip</a>.</p>

<p>Everything works great, except during rolling updates. I have <code>maxSurge: 1</code> and <code>maxUnavailable: 0</code>.</p>

<p>My problem is that during a rolling update, I start getting request timeouts. I suspect the Google load balancer is still sending requests to the node where the pod is <code>Terminating</code> even though the health checks are failing. This happens for about 30-60s starting right when the pod changes from <code>Running</code> to <code>Terminating</code>.  Everything stabilizes after a while and traffic eventually goes only to the new node with the new pod. </p>

<p><strong>If the load balancer <em>is</em> slow to stop sending requests to a terminating pod, is there some way to make these rolling deploys hitless?</strong></p>

<hr>

<p>My understanding is that in a <em>normal</em> k8s service, where <code>externalTrafficPolicy</code> is not normal, the Google load balancer simply sends requests to all nodes and let's the iptables sort it out. When a pod is <code>Terminating</code> the iptables are updated quickly and traffic does not get sent to that pod anymore. In the case where <code>externalTrafficPolicy</code> is <code>Local</code> however, if the node that receives the request does not have a <code>Running</code> pod, then the request times out, which is what is happening here.</p>

<p>If this is correct, then I only see two options</p>

<ol>
<li>stop sending requests to the node with a <code>Terminating</code> pod</li>
<li>continue servicing requests even though the pod is <code>Terminating</code></li>
</ol>

<p>I feel like option 1 is difficult since it requires informing the load balancer that the pod is about to start <code>Terminating</code>.</p>

<p>I've made some progress on option 2, but so far haven't gotten it working. I've managed to continue serving requests from the pod by adding a <code>preStop</code> lifecycle hook which just runs <code>sleep 60</code>, but I think the problem is that the <code>healthCheckNodePort</code> reports <code>localEndpoints: 0</code> and I suspect something is blocking the request between arriving at the node and getting to the pod. Perhaps, the iptables aren't routing when <code>localEndpoints: 0</code>.  </p>

<p>I've also adjusted the Google load balancer health check, which is different from the <code>readinessProbe</code> and <code>livenessProbe</code>, to the "fastest" settings possible e.g. 1s interval, 1 failure threshold and I've verified that the load balancer backend aka k8s node, indeed fails health checks quickly, but continues to send requests to the terminating pod anyway.</p>

Jesse Shieh
<p>There is a similar discussion <a href="https://stackoverflow.com/questions/58751354/testing-graceful-shutdown-on-an-http-server-during-a-kubernetes-rollout/58752566#58752566">here</a>. Although it's not identical, it's a similar use case.</p>

<p>Everything sounds like it is working as expected.</p>

<ul>
<li><p>The LoadBalancer will send traffic to any healthy node based on the LoadBalancer health check. The LoadBalancer is unaware of individual pods. </p></li>
<li><p>The health check will mark a node as unhealthy once the health check threshold is crossed, ie HC is sent every x seconds with x timeout delay, x number of failed requests. This causes a delay between the time that the pod goes into terminating and it is marked as unhealthy.</p></li>
<li><p>Also note that once the pod is marked as notReady, the pod is removed from the service endpoint. If there is no other pod on a node, traffic will continue reaching this node (because of the HC behaviour explained above), the requests can't be forwarded because of the externalTrafficPolicy (traffic remains on the node where it was sent).</p></li>
</ul>

<p>There are a couple of ways to address this.</p>

<ol>
<li><p>To minimize the amount of time between a terminated pod and the node being marked as unhealthy, you can set a more aggressive health check. The trouble with this is that an overly sensitive HC may cause false positives, usually increases the overhead on the node (additional health check requests), and it will not fully eliminate the failed requests.</p></li>
<li><p>Have enough pods running so that there are always at least 2 pods per node. Since the service removes the pod from the endpoint once it goes into notReady, requests will just get sent to the running pod instead. The downside here is that you will either have additional overhead (more pods) or a tighter grouping (more vulnerable to failure). It also won't fully eliminate the failed requests, but they will be incredibly few.</p></li>
<li><p>Tweak the HC and your container to work together:
3a.Have the HC endpoint be separate from the normal path you use.
3b. Configure the container <code>readinessProbe</code> to match the main path your container serves traffic on (it will be different from the LB HC path)
3c. Configure your image so that when <code>SIGTERM</code> is received, the first thing to go down is the HC path.
3d. Configure the image to gracefully drain all connections once a <code>SIGTERM</code> is received rather than immediately closing all sessions and connections.</p></li>
</ol>

<p>This should mean that ongoing sessions will gracefully terminate which reduces errors.
It should also mean that the node will start failing HC probes even though it is ready to serve normal traffic, this gives time for the node to be marked as unhealthy and the LB will stop sending traffic to it before it is no longer able to serve requests.</p>

<p>The problem with this last option is 2 fold. First, it is more complex to configure. The other issue is that it means your pods will take longer to terminate so rolling updates will take longer, so will any other process that relies on gracefully terminating the pod such as draining the node. The second issue isn't too bad unless you are in need of a quick turn around. </p>
