Имеется приложение на Go, API-бекенд.
Периодически начинает возвращать 502 ошибку, хотя сам под работает и в статусе Running.
Что бы рассмотреть, как и почему Ingress и Service могут возвращать 502, и как работают readinessProbe и livenessProbe в Kubernetes Deployment — напишем простой веб-сервер на Go, в котором опишем два ендпоинта — один будет возвращать нормальный ответ, а во втором — выполнение программы будет прерываться.
Затем задеплоим его в AWS Elastic Kubernetes, создадим Kubernetes Ingress, который создаст AWS Application Load balancer, и потрестируем работу приложения.
Содержание
Golang HTTP server
Пишем приложение на Go, которое потом упакуем в Docker-контейнер, и запустим в Kubernetes:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request){
fmt.Fprintf(w, "pong")
})
http.HandleFunc("/err", func(w http.ResponseWriter, r *http.Request){
panic("Error")
})
fmt.Printf("Starting server at port 8080\n")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
Тут мы запускаем http.ListenAndServe() на порту 8080, и определяем два роута:
/ping— при обращении сюда всегда возвращаем 200/err— при обращении сюда прерываем выполнение функции сpanic, что сэмулировать некорретный ответ приложения
Проверяем локально.
Запускаем:
[simterm]
$ go run http.go Starting server at port 8080
[/simterm]
Проверяем роут /ping:
[simterm]
$ curl -I localhost:8080/ping HTTP/1.1 200 OK
[/simterm]
И URI /err, который вызовет panic:
[simterm]
$ curl -I localhost:8080/err curl: (52) Empty reply from server
[/simterm]
Лог приложения:
[simterm]
$ go run http.go Starting server at port 8080 2020/11/11 14:34:53 http: panic serving [::1]:43008: Error goroutine 6 [running]: ...
[/simterm]
Docker образ
Пишем Dockefile:
FROM golang:latest WORKDIR /app COPY . . RUN go build -o main . EXPOSE 8080 CMD ["./main"]
Собираем образ и пушим в Docker Hub:
[simterm]
$ docker build -t setevoy/go-http . $ docker push setevoy/go-http
[/simterm]
Kubernetes
Deployment
Описываем запуск пода с этим образом — создаём 1 под, Service для него, и Ingress, который создаст AWS Application Load Balancer.
Начнём с Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-http
labels:
app: go-http
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: go-http
template:
metadata:
labels:
app: go-http
spec:
containers:
- name: go-http
image: setevoy/go-http
ports:
- containerPort: 8080
imagePullPolicy: Always
livenessProbe:
httpGet:
path: /ping
port: 8080
initialDelaySeconds: 1
periodSeconds: 1
readinessProbe:
httpGet:
path: /ping
port: 8080
initialDelaySeconds: 1
periodSeconds: 1
restartPolicy: Always
Тут создаём один под, который слушает порт 8080.
В strategy деплоймента указываем Recreate, что бы при тестах не оставались старые поды.
Для него описываем проверки — livenessProbe и readinessProbe, обе проверки ходят на URI /ping, где получают ответ 200.
Позже мы поменяем путь в проверках, и посмотрим, к чему это приведёт.
Service
Создаём Kubernetes Service, который откроет на WorkerNode порт для ALB, и будет роутить трафик к нашему поду на порт 8080 (см. Kubernetes: Service, балансировка нагрузки, kube-proxy и iptables):
---
apiVersion: v1
kind: Service
metadata:
name: go-http-svc
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
protocol: TCP
selector:
app: go-http
Ingress
Добавляем Ingress, который создаст AWS Application Load Balancer, который будет направлять трафик к go-http-svc Service:
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: go-http-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/healthcheck-path: /ping
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
labels:
app: go-http
spec:
rules:
- http:
paths:
- path: /*
backend:
serviceName: go-http-svc
servicePort: 80
Создаём ресурсы:
[simterm]
$ kubectl apply -f go-http.yaml deployment.apps/go-http created service/go-http-svc created ingress.extensions/go-http-ingress created
[/simterm]
Проверяем под:
[simterm]
$ kubectl get pod -l app=go-http NAME READY STATUS RESTARTS AGE go-http-8dc5b4779-7q4kw 1/1 Running 0 8s
[/simterm]
И его Ingress:
[simterm]
$ kubectl get ingress -l app=go-http NAME HOSTS ADDRESS PORTS AGE go-http-ingress * e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com 80 31s
[/simterm]
Ждём, пока наш DNS увидит новый URL e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com, и проверяем работу приложения — выполняем запрос к /ping:
[simterm]
$ curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping HTTP/1.1 200 OK
[/simterm]
Kubernetes Ingress 502
А теперь — обращаемся к ендпоинту /err, который в Go-приложении вызовет panic, и ловим 502 ошибку:
[simterm]
$ curl -vI e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/err ... < HTTP/1.1 502 Bad Gateway HTTP/1.1 502 Bad Gateway < Server: awselb/2.0 Server: awselb/2.0
[/simterm]
Логи пода:
[simterm]
$ kubectl logs go-http-8dc5b4779-7q4kw
Starting server at port 8080
2020/11/11 12:57:10 http: panic serving 10.3.39.145:8926: Error
goroutine 5169 [running]:
net/http.(*conn).serve.func1(0xc000260e60)
/usr/local/go/src/net/http/server.go:1801 +0x147
panic(0x654840, 0x6f0ba0)
/usr/local/go/src/runtime/panic.go:975 +0x47a
main.main.func2(0x6fa0a0, 0xc00012c540, 0xc000127700)
/app/http.go:16 +0x39
net/http.HandlerFunc.ServeHTTP(0x6bab98, 0x6fa0a0, 0xc00012c540, 0xc000127700)
/usr/local/go/src/net/http/server.go:2042 +0x44
net/http.(*ServeMux).ServeHTTP(0x8615e0, 0x6fa0a0, 0xc00012c540, 0xc000127700)
/usr/local/go/src/net/http/server.go:2417 +0x1ad
net/http.serverHandler.ServeHTTP(0xc0000ea000, 0x6fa0a0, 0xc00012c540, 0xc000127700)
/usr/local/go/src/net/http/server.go:2843 +0xa3
net/http.(*conn).serve(0xc000260e60, 0x6fa4e0, 0xc00011b8c0)
/usr/local/go/src/net/http/server.go:1925 +0x8ad
created by net/http.(*Server).Serve
/usr/local/go/src/net/http/server.go:2969 +0x36c
[/simterm]
Тут всё логично — load-balancer отправил наш запрос к поду, под не ответил (вспомним curl: (52) Empty reply from server в наших первых тестах), и мы получили ответ 502 от балансировщика aka Ingress.
readinessProbe и livenessProbe
Теперь посмотрим, как изменение в readinessProbe и livenessProbe повлияют на ответы Ingress и работу самого пода.
readinessProbe
Документация — тут>>>.
readinessProbe используется для проверки того, готов ли под принимать трафик.
Сейчас наш под в статусе Ready:
[simterm]
$ kubectl get pod -l app=go-http NAME READY STATUS RESTARTS AGE go-http-8dc5b4779-7q4kw 1/1 Running 0 28m
[/simterm]
Или так:
[simterm]
$ kubectl get pod -l app=go-http -o json | jq '.items[].status.containerStatuses[].ready' true
[/simterm]
И запрос к /ping возвращает нам ответ 200:
[simterm]
$ curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping HTTP/1.1 200 OK
[/simterm]
Меняем readinessProbe — задаём path=/err, что бы проверка постоянно фейлилась:
...
readinessProbe:
httpGet:
path: /err
...
Передеплоиваем:
[simterm]
$ kubectl apply -f go-http.yaml deployment.apps/go-http configured
[/simterm]
И проверяем:
[simterm]
$ kubectl get pod -l app=go-http NAME READY STATUS RESTARTS AGE go-http-5bd557544-2djcw 0/1 Running 0 4s
[/simterm]
Если мы теперь отправим запрос даже на ендпоинт /ping — всё-равно получим 502, т.к. бекенд Сервиса, т.е. под, не принимает трафик, потому что не прошёл readinessProbe:
[simterm]
$ kubectl get pod -l app=go-http -o json | jq '.items[].status.containerStatuses[].ready' false
[/simterm]
Пробуем:
[simterm]
$ curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping HTTP/1.1 502 Bad Gateway
[/simterm]
livenessProbe
Документация — тут>>>.
Вернём readinessProbe в /ping, что бы трафик на под пошёл, но изменим livenessProbe — зададим path в /err, а initialDelaySeconds и periodSeconds установим в 15 секунд, плюс добавим failureThreshold равным одной попытке:
...
livenessProbe:
httpGet:
path: /err
port: 8080
initialDelaySeconds: 15
periodSeconds: 15
failureThreshold: 1
readinessProbe:
httpGet:
path: /ping
...
Теперь после запуска пода Kubernetes выждет 15 секунд, затем выполнит livenessProbe и будет повторять её каждые следующие 15 секунд.
Передеплоиваем, и проверяем:
[simterm]
$ kubectl get pod -l app=go-http NAME READY STATUS RESTARTS AGE go-http-78f6c66c8b-q6fkf 1/1 Running 0 6s
[/simterm]
Проверяем запрос к ендпоинту /ping:
[simterm]
$ curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping HTTP/1.1 200 OK
[/simterm]
Всё хорошо.
И /err нам ожидаемо вернёт 502:
[simterm]
$ curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/err HTTP/1.1 502 Bad Gateway
[/simterm]
Через 15 секунд, после выполнения первой live проверки — под будет перезапущен:
[simterm]
$ kubectl get pod -l app=go-http NAME READY STATUS RESTARTS AGE go-http-668c674dcb-4db9x 0/1 Running 1 19s
[/simterm]
Events этого пода:
[simterm]
$ kubectl describe pod -l app=go-http ... Normal Created 6s (x4 over 81s) kubelet, ip-10-3-55-229.us-east-2.compute.internal Created container go-http Warning Unhealthy 6s (x3 over 66s) kubelet, ip-10-3-55-229.us-east-2.compute.internal Liveness probe failed: Get http://10.3.53.103:8080/err: EOF Normal Killing 6s (x3 over 66s) kubelet, ip-10-3-55-229.us-east-2.compute.internal Container go-http failed liveness probe, will be restarted Normal Started 5s (x4 over 81s) kubelet, ip-10-3-55-229.us-east-2.compute.internal Started container go-http
[/simterm]
Контейнер в поде не прошёл проверку livenessProbe, и Kubernetes перезапускает под в попытке «починить» его.
Если получится попасть на сам момент перезапуска контейнера — увидим стаус CrashLoopBackOff, а запрос к /ping снова вернёт нам 502:
[simterm]
$ kubectl get pod -l app=go-http NAME READY STATUS RESTARTS AGE go-http-668c674dcb-4db9x 0/1 CrashLoopBackOff 4 2m21s
[/simterm]
Выводы
Используем readinessProbe для проверки того, что приложение запустилось, в данном случае — Go-бинарник начал прослушивать порт 8080, и на него можно направлять трафик, и используем livenessProbe во время работы пода для проверки того, что приложение в нём все ещё живо.
Если приложение начинает отдавать 502 на определённые запросы — то следует поискать проблему именно в запросах, т.к. если бы была проблема в настройках Ingress/Service — получали бы 502 постоянно.
Самое важное — понимать принципиальную разницу между readinessProbe и livenessProbe:
- если фейлится
readinessProbe— процесс aka контейнер в поде останется в том же состоянии, в котором был на момент сфейленой ready-проверки, но под будет отключен от трафика к нему - если фейлится
livenessProbe— трафик на под продолжает идти, но контейнер будет перезапущен
Итак, имеем ввиду, что:
- Если
readinessProbeне задана вообще —kubeletбудет считать, что под готов к работе, и направит к нему трафик сразу после старта пода. При этом если на запуск пода уходит минута — то клиенты, которые к нему были направлены после его запуска, будут ждать эту минуту, пока он ответит. - Если приложение ловит ошибку, которую не может обработать — оно должно завершить свой процесс, и Kubernetes сам перезапустит контейнер.
- Используем
livenessProbeдля проверки состояний, которые нельзя обработать в самом приложении, например — deadlock или бесконечный цикл, при которых контейнер не может ответить на ready-проверку. В таком случае если нетlivenessProbe, которая может перезапустить процесс, то под будет отключен от Service — но останется в статусе Running, продолжая потреблять реурсы WorkerNode.
Ссылки по теме
- Pod Lifecycle
- Configure Liveness, Readiness and Startup Probes
- Kubernetes best practices: Setting up health checks with readiness and liveness probes
- Kubernetes Liveness and Readiness Probes Revisited: How to Avoid Shooting Yourself in the Other Foot
- Kubernetes production best practices
- Kubernetes: Best Practices for Liveness Probes




