CONFIG.SYS
  • ALL_POSTS.BAT
  • ABOUT.EXE

Load balancing UDP in Kubernetes - Sun, Oct 11, 2020

How to do UDP load balancing in Kubernetes.

UDP load balancing in Kubernetes


Recently I to migrated a server application to Kubernetes whose interface was based on the Radius protocoll . The deployment looked quiet simple: A Pod with a service:

Overview

(all artifacts like Dockerfiles and deployments referenced in this article can be found here )

Odd results during testing


Testing this setup produced some odd results:

  • Scaling server replicas down resulted in clients running into timeouts
  • Scaling server replicas up resulted in uneven load distribution, load was not redistributed to new server replicas

After doing some research I discovered that UDP based load balancing of internal services does not work as I expected so I started looking for an alternative solution.

Nginx as an alternative


Nginx offers UDP load balancing support in its non-commercial edition which is easy to set up . There are two drawbacks though:

  • First nginx needs to be compiled with the --with-stream option which the images on docker hub usually do not include.
  • Second nginx needs to be configured to discover new replicas of the server.

Using a headless service for service discovery


Headless services offer exactly that:

You can use a headless Service to interface with other service discovery mechanisms, without being tied to Kubernetes’ implementation. You can create what are termed “headless” Services, by explicitly specifying “None” for the cluster IP (.spec.clusterIP).

The new topology puts the nginx and the headless service in between the clients and the server:

New topology

The deployment specification for the headless service in front of the server looks like this:

---
apiVersion: v1
kind: Service
metadata:
  name: udp-server-service-headless
spec:
  clusterIP: None
  ports:
    - name: udp
      port: 10002
      protocol: UDP
  selector:
    app: udp-server
  type: ClusterIP

The service makes sure that endpoints are created for each new server replica and dns resolvable:

$ kubectl get endpoints
NAME                          ENDPOINTS                             AGE
udp-server-service-headless   10.244.1.27:10002,10.244.1.61:10002   98m
$ kubectl scale deployment udp-server-deployment --replicas=3
deployment.apps/udp-server-deployment scaled
$ kubectl get endpoints
NAME                          ENDPOINTS                                               AGE
udp-server-service-headless   10.244.1.27:10002,10.244.1.61:10002,10.244.1.62:10002   100m
$
$ kubectl exec udp-nginx-deployment-86bb97f9d5-jtv27 -- nslookup udp-server-service-headless
Server:		10.96.0.10
Address:	10.96.0.10#53

Name:	udp-server-service-headless.default.svc.cluster.local
Address: 10.244.1.27
Name:	udp-server-service-headless.default.svc.cluster.local
Address: 10.244.1.61
Name:	udp-server-service-headless.default.svc.cluster.local
Address: 10.244.1.62

The commands above show that scaling up the server replicas by 1 creates a new endpoint that is resolvable inside the nginx container.

Resolve DNS names in nginx


The nginx configuration for udp load balancing is only a few lines long:

#user  nobody;
worker_processes  1;

events {
    worker_connections  1024;
}

stream {
    resolver dns-server ipv6=off valid=30s;
    
    upstream udp_servers {
        zone upstream_udp_servers_dynamic 64k;
        least_conn;
        server udp-server-service-headless:10002;
    }
    
    server {
        listen     5353 udp;
        proxy_pass udp_servers;
    }
}

the resolver directive tells nginx to resolve ip addresses for the servers from the dns server dns-server which points to the kubernetes internal dns server. The upstream directive has only one server entry that contains the dns name of the kubernetes service: udp-server-service-headless.
Using this configuration nginx will lookup all ip addresses for the fqdn udp-server-service-headless and load balance between them.

BUT the non-commercial version only resolves these names at startup. Whereas the commercial edition can be configured do this periodically . The server directive would just need the additional parameter resolve:

server udp-server-service-headless:10002 resolve;

So in order to get new replicas using the non-commercial edition I had to employ a little trick described below.

Config reload

When nginx is running it can be told to reload the configuration with the command /usr/local/sbin/nginx -s reload. So the solution in this case was to start a second process that would periodically executes that command.
So I created a simple script which does just that:

#!/bin/sh
while :
do
	sleep 30
	/usr/local/sbin/nginx -s reload
done

Using that script nginx reloads the configuration every 30 seconds and thus resolves any new server replicas.
To start a second process in the container I used supervisord and instead of starting nginx directly when the container starts, servisord is started.
The supervisord configuration looks like this:

[supervisord]
nodaemon=true

[program:reload-config]
command=/reload_config.sh
...

[program:nginx]
command=nginx -g 'daemon off;'
...

[eventlistener:nginx_exit]
command=/stop-supervisor.sh
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL

I omitted some parts to make the configuration clearer. This configuration does the following:

  • Start nginx and the script for reloading the configuration when the container starts
  • Call the script stop-supervisor.sh when nginx stops to gracefully stop all processes

Load balancing that works


Using this nginx in a deployment with a single replica and a service in front does the load balancing correctly:

  • No requests are being dropped when draining the server replicas
  • No timeouts occur on the client when server replicas are scaled down
  • Load is evenly distributed when server replicas are added

Conclusion


Kubernetes internal load balancing for UDP has some odd behavior. For reliable load balancing using an nginx with a headless service as the backend will provide consistent and reliable load balancing.
Discovering new server replicas requires additional effort when using the non-commercial version of nginx.

Back to Home


21st century version | © Thomas Reuhl 2025 | Disclaimer | Built on Hugo

Linkedin GitHub