Kubernetes: Istio Locality Based Load Balancing


Cluster

Create a Regional cluster with 3 zones (1 node per zone).

List the nodes and labels. We need this to understand which node belongs to which zone.

kubectl get nodes --label-columns failure-domain.beta.kubernetes.io/region,failure-domain.beta.kubernetes.io/zone
NAME STATUS ROLES AGE VERSION REGION ZONE
gke-itsmetommy-default-pool-5ba92622-dbgg Ready <none> 11m v1.16.8-gke.15 us-west1 us-west1-c
gke-itsmetommy-default-pool-65c4a9e0-g4l0 Ready <none> 11m v1.16.8-gke.15 us-west1 us-west1-b
gke-itsmetommy-default-pool-fee8f05c-3ghq Ready <none> 11m v1.16.8-gke.15 us-west1 us-west1-a

We can see that each node is located in a separate zone.

Istio

Note: I’ve observed that you must install 1.5.2 or above.

Install Istio.

curl -L https://istio.io/downloadIstio | sh -
cd istio-1.6.0
export PATH=$PWD/bin:$PATH
istioctl verify-install
istioctl manifest apply --set profile=default

Create namespace locality.

kubectl create ns locality

Enable Istio on namespace locality.

kubectl label namespace locality istio-injection=enabled

Backend

Create backend:v1, backend:v2, backend:v3 folders.

{
  mkdir -p backend-v1/public-html
  mkdir -p backend-v2/public-html
  mkdir -p backend-v3/public-html
}

Create backend:v1, backend:v2, backend:v3 Dockerfiles.

cat <<EOF > backend-v1/Dockerfile
FROM httpd
COPY ./public-html/ /usr/local/apache2/htdocs/
EXPOSE 80
EOF
cat <<EOF > backend-v2/Dockerfile
FROM httpd
COPY ./public-html/ /usr/local/apache2/htdocs/
EXPOSE 80
EOF
cat <<EOF > backend-v3/Dockerfile
FROM httpd
COPY ./public-html/ /usr/local/apache2/htdocs/
EXPOSE 80
EOF

Create backend:v1, backend:v2, backend:v3 index.html.

{
  echo "Backend Version 1 us-west1-a" > backend-v1/public-html/index.html
  echo "Backend Version 2 us-west1-b" > backend-v2/public-html/index.html
  echo "Backend Version 3 us-west1-c" > backend-v3/public-html/index.html
}

Build backend:v1, backend:v2, backend:v3 images.

{
  docker build -t itsmetommy/backend:v1 backend-v1
  docker build -t itsmetommy/backend:v2 backend-v2
  docker build -t itsmetommy/backend:v3 backend-v3
}

Push backend:v1, backend:v2, backend:v3 images to Docker Hub.

{
  docker push itsmetommy/backend:v1
  docker push itsmetommy/backend:v2
  docker push itsmetommy/backend:v3
}

Deploy backend:v1, backend:v2, backend:v3 per zone.

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-v1
namespace: locality
spec:
replicas: 1
selector:
matchLabels:
app: backend
version: v1
template:
metadata:
labels:
app: backend
version: v1
spec:
containers:
- image: itsmetommy/backend:v1
imagePullPolicy: IfNotPresent
name: backend
ports:
- containerPort: 80
nodeSelector:
failure-domain.beta.kubernetes.io/zone: us-west1-a --- apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-v2
namespace: locality
spec:
replicas: 1
selector:
matchLabels:
app: backend
version: v2
template:
metadata:
labels:
app: backend
version: v2
spec:
containers:
- image: itsmetommy/backend:v2
imagePullPolicy: IfNotPresent
name: backend
ports:
- containerPort: 80
nodeSelector:
failure-domain.beta.kubernetes.io/zone: us-west1-b --- apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-v3
namespace: locality
spec:
replicas: 1
selector:
matchLabels:
app: backend
version: v3
template:
metadata:
labels:
app: backend
version: v3
spec:
containers:
- image: itsmetommy/backend:v3
imagePullPolicy: IfNotPresent
name: backend
ports:
- containerPort: 80
nodeSelector:
failure-domain.beta.kubernetes.io/zone: us-west1-c
EOF

Create backend service.

kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: locality
labels:
app: backend
spec:
ports:
- name: http-web
port: 80
targetPort: 80
selector:
app: backend
EOF

Verify that each pod is running on a separate node, which at this point means that they are running in different zones.

kubectl get pods -l app=backend -n locality -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
backend-v1-699dcf4487-qmkqq 2/2 Running 0 24s 10.1.16.4 gke-itsmetommy-default-pool-0e8a1c3c-f5w4 <none> <none>
backend-v2-5879564fcb-8mwxg 2/2 Running 0 24s 10.1.18.6 gke-itsmetommy-default-pool-43f27618-czfr <none> <none>
backend-v3-64d6bcd449-dgd24 2/2 Running 0 23s 10.1.17.9 gke-itsmetommy-default-pool-64cb61f4-jgpc <none> <none>

Frontend

Create frontend folder.

mkdir frontend

Create frontend Dockerfile.

cat <<EOF > frontend/Dockerfile
FROM ubuntu:latest RUN apt-get update
RUN apt-get install -y curl
EOF

Build frontend image.

docker build -t itsmetommy/frontend frontend

Push frontend image to Docker Hub.

docker push itsmetommy/frontend

Create frontend depoloyment.

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: locality
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: itsmetommy/frontend command: ["/bin/sleep","infinity"]
imagePullPolicy: IfNotPresent
name: frontend
EOF

List frontend pods. They should be running across all nodes.

kubectl get pods -l app=frontend -n locality -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
frontend-c59d5b9c5-cb49g 2/2 Running 0 50s 10.1.18.7 gke-itsmetommy-default-pool-43f27618-czfr <none> <none>
frontend-c59d5b9c5-q4bld 2/2 Running 0 50s 10.1.17.10 gke-itsmetommy-default-pool-64cb61f4-jgpc <none> <none>
frontend-c59d5b9c5-rsh55 2/2 Running 0 50s 10.1.16.5 gke-itsmetommy-default-pool-0e8a1c3c-f5w4 <none> <none>

Create variables per pod.

{
  POD_ZONE_1=$(kubectl get pods -n locality -l app=frontend -o jsonpath="{.items[0].metadata.name}")
  POD_ZONE_2=$(kubectl get pods -n locality -l app=frontend -o jsonpath="{.items[1].metadata.name}")
  POD_ZONE_3=$(kubectl get pods -n locality -l app=frontend -o jsonpath="{.items[2].metadata.name}")
}

Curl the backend from each frontend pod to see which backends responds. You will notice that they are load balanced in a round robin fashion.

for i in {1..10}
do
kubectl exec -it $POD_ZONE_1 -c frontend -n locality -- sh -c 'curl http://backend'
done Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
for i in {1..10}
do
kubectl exec -it $POD_ZONE_2 -c frontend -n locality -- sh -c 'curl http://backend'
done Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
for i in {1..10}
do
kubectl exec -it $POD_ZONE_3 -c frontend -n locality -- sh -c 'curl http://backend'
done Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b

Locality-Prioritized Load Balancing

Locality-prioritized load balancing is the default behavior for locality load balancing. In this mode, Istio tells Envoy to prioritize traffic to the workload instances most closely matching the locality of the Envoy sending the request. When all instances are healthy, the requests remains within the same locality. When instances become unhealthy, traffic spills over to instances in the next prioritized locality. This behavior continues until all localities are receiving traffic.

https://istio.io/docs/ops/configuration/traffic-management/locality-load-balancing/#locality-prioritized-load-balancing

Enable locality-prioritized load balancing by creating a VirtualService and DestinationRule.

kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: backend
namespace: locality
spec:
hosts:
- backend
http:
- route:
- destination:
host: backend
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: backend
namespace: locality
spec:
host: backend
trafficPolicy:
outlierDetection:
consecutiveErrors: 7
interval: 30s
baseEjectionTime: 30s
EOF

Test each frontend response.

Run a curl test from each pod. You should see that all requests go to a single backend pod/zone.

for i in {1..10}
do
  kubectl exec -it $POD_ZONE_1 -c frontend -n locality -- sh -c 'curl  http://backend'
done
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
for i in {1..10}
  do kubectl exec -it $POD_ZONE_2 -c frontend -n locality -- sh -c 'curl  http://backend'
done
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
for i in {1..10}
do
  kubectl exec -it $POD_ZONE_3 -c frontend -n locality -- sh -c 'curl  http://backend'
done
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a

This shows that locality-prioritized load balancing is working correctly.

Locality-Weighted Load Balancing

Locality-weighted load balancing allows administrators to control the distribution of traffic to endpoints based on the localities of where the traffic originates and where it will terminate. These localities are specified using arbitrary labels that designate a hierarchy of localities in {region}/{zone}/{sub-zone} form.

https://istio.io/docs/reference/config/networking/destination-rule/#LocalityLoadBalancerSetting

We will apply the following:

  • Route 80% of us-west1-a traffic to us-west1-a and 20% to us-west1-b
  • Route 80% of us-west1-b traffic to us-west1-b and 20% to us-west1-c
  • Route 80% of us-west1-c traffic to us-west1-c and 20% to us-west1-a

Update the DestinationRule.

kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: backend
namespace: locality
spec:
host: backend
trafficPolicy:
outlierDetection:
consecutiveErrors: 7
interval: 30s
baseEjectionTime: 30s
loadBalancer:
localityLbSetting:
enabled: true
distribute:
- from: us-west1/us-west1-a/*
to:
"us-west1/us-west1-a/*": 80
"us-west1/us-west1-b/*": 20
- from: us-west1/us-west1-b/*
to:
"us-west1/us-west1-b/*": 80
"us-west1/us-west1-c/*": 20 - from: us-west1/us-west1-c/*
to:
"us-west1/us-west1-c/*": 80
"us-west1/us-west1-a/*": 20
EOF

Test each frontend response.

Run a curl test from each pod. You should see that 80% of requests go to a single backend zone and 20% to another.

for i in {1..10}
do
  kubectl exec -it $POD_ZONE_1 -n locality -c frontend -- sh -c 'curl  http://backend'
done
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 2 us-west1-b
Backend Version 3 us-west1-c
Backend Version 2 us-west1-b
Backend Version 3 us-west1-c
Backend Version 2 us-west1-b
for i in {1..10}
do
  kubectl exec -it $POD_ZONE_2 -n locality -c frontend -- sh -c 'curl  http://backend'
done
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 3 us-west1-c
Backend Version 3 us-west1-c
for i in {1..10}
do
  kubectl exec -it $POD_ZONE_3 -n locality -c frontend -- sh -c 'curl  http://backend'
done
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 2 us-west1-b
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a
Backend Version 1 us-west1-a

This shows that locality-weighted load balancing is working correctly.

Errors

The DestinationRule "backend" is invalid:
* : Invalid value: "": "spec.trafficPolicy.loadBalancer" must validate one and only one schema (oneOf). Found none valid
* spec.trafficPolicy.loadBalancer.simple: Required value

Fix

Upgrade Istio to at least 1.5.4.

Clean up

kubectl delete ns locality