#troubleshooting #devops #одинденьизжизни
Однажды пришёл на работу, я oncall, вижу алерт - поды рестартятся.
Смотрю, что там:
kubectl get pods -n | grep redis
redis-ha-server-1 1/4 Running 7 10m
Ага, один под постоянно рестартится. 7 раз за 10 минут.
Смотрю логи:
kubectl logs -p redis-ha-server-1 -c redis -n
1:S * MASTER REPLICA sync started
1:S * Full resync from master
1:S * MASTER REPLICA sync: receiving streamed RDB from master
1:signal-handler Received SIGTERM scheduling shutdown...
1:S * User requested shutdown...
Реплика начинает Full Resync, а через 27 секунд получает SIGTERM и умирает. А синхронизация большой базы занимает минуты.
Смотрю ивенты:
kubectl get events -n --field-selector involvedObject.name=redis-ha-server-1 --sort-by='.lastTimestamp'
Normal Killing pod/redis-ha-server-1 Container redis failed startup probe, will be restarted
Warning Unhealthy pod/redis-ha-server-1 Startup probe failed: role=slave; repl=sync
Ага, контейнер убивает стартап проб.
Проверяю настройки:
kubectl describe pod redis-ha-server-1 -n | grep -A1 Startup:
Startup: exec [sh -c /health/redis_readiness.sh] delay=5s timeout=15s period=10s #success=1 #failure=3
failureThreshold: 3, periodSeconds: 10, initialDelaySeconds: 5.
Первая проверка на 5-й секунде, дальше каждые 10: 5, 15, 25. Третья неудача подряд, и контейнер убивают. Плюс пара секунд на сам скрипт, отсюда и мои 27.
А база весит 550 мегабайт, и Full Resync идёт дольше.
Пока реплика синхронизируется, health check возвращает ошибку.
Через три неудачи подряд startup probe считает контейнер мёртвым, и кублет его перезапускает. Синк начинается заново, и так по кругу.
Проблема вроде бы понятна. Нужно увеличить failureThreshold.
Правлю StatefulSet:
kubectl edit statefulset redis-ha-server -n
Меняю failureThreshold у контейнера redis с 3 на 120. Сохраняю.
Удаляю под:
kubectl delete pod redis-ha-server-1 -n
Жду, смотрю логи.
Та же херня, рестарты продолжаются.
Сука.
Проверяю, что применилось в поде:
kubectl get pod redis-ha-server-1 -n
-o jsonpath='{.spec.containers[?(@.name=="redis")].startupProbe.failureThreshold}'
3
Бл, под создался со старым значением.
А в StatefulSet что?
kubectl get statefulset redis-ha-server -n
-o jsonpath='{.spec.template.spec.containers[?(@.name=="redis")].startupProbe.failureThreshold}'
120
В StatefulSet уже 120, а в поде 3.
Что за ебанная магия?
Проверяю, кто управляет StatefulSet:
kubectl get statefulset redis-ha-server -n -o yaml | head -30
metadata:
annotations:
meta.helm.sh/release-name:
meta.helm.sh/release-namespace:
labels:
app.kubernetes.io/managed-by: Helm
Helm. Но сам по себе Helm ничего не откатывает: reconcile у него нет, ресурсы он трогает только на helm upgrade.
Кто именно подсунул поду старый шаблон, я так и не выяснил. Если поймаете такое, смотрите managedFields: там видно, кто и когда писал в объект.
kubectl get statefulset redis-ha-server -n --show-managed-fields -o yaml
Ладно. Шаблон в StatefulSet правильный, значит, надо заставить контроллер пересоздать поды из него. Смотрю стратегию обновления:
kubectl get statefulset redis-ha-server -n -o jsonpath='{.spec.updateStrategy}'
{"type":"OnDelete"}
При OnDelete контроллер обновляет под, только когда его удаляют. Я его и удалил, а он всё равно поднялся со старым шаблоном. Почему, честно, не знаю.
Переключаю на RollingUpdate, чтобы контроллер перекатил всё сам:
kubectl patch statefulset redis-ha-server -n --type=json -p='[
{"op": "replace", "path": "/spec/updateStrategy/type", "value": "RollingUpdate"}
]'
Проверяю новый под:
kubectl get pod redis-ha-server-1 -n
-o jsonpath='{.spec.containers[?(@.name=="redis")].startupProbe.failureThreshold}'
120
Отлично, теперь 120.
Смотрю логи: Full Resync завершился успешно, всё синхронизировалось.
Но дальше вижу, что сразу после успешной синхронизации соединение теряется.
Смотрю логи мастера:
kubectl logs redis-ha-server-0 -c redis -n --tail=50
1:M * Connection with replica lost.
1:M # closed for overcoming of output buffer limits
Ага, мастер рвёт соединение, потому что переполнился output buffer.
Проверяю лимиты:
kubectl exec redis-ha-server-0 -c redis -n --
sh -c 'redis-cli -a $AUTH CONFIG GET client-output-buffer-limit'
slave 268435456 67108864 60
Hard limit 256 MB, soft limit 64 MB на 60 секунд.
Буфер на мастере дорос до 152 MB. До hard limit не дотянул, но soft limit (64 MB) держался превышенным дольше 60 секунд. Этого достаточно, чтобы мастер убил соединение.
Пока мастер стримит RDB, все новые записи копятся в этом буфере. Большая база и активная запись дают ровно такую картину.
Увеличиваю лимиты:
kubectl exec redis-ha-server-0 -c redis -n --
sh -c 'redis-cli -a $AUTH CONFIG SET client-output-buffer-limit "slave 1073741824 536870912 60"'
Теперь hard limit 1 GB, soft limit 512 MB. CONFIG SET применяется сразу, без рестарта. Важен он именно на мастере, но я прописал его на всех нодах: sentinel может переключить мастера в любой момент.
Заодно поднимаю repl-backlog-size до 512 MB. Full sync это не лечит: backlog нужен, чтобы после короткого разрыва реплика догналась частичным PSYNC, а не начинала полный синк заново. Но и памяти он съедает столько же, так что проверь лимиты, если не хочешь поймать OOMKill.
Перезапускаю реплику, чтобы она заново прошла full sync уже с новыми лимитами:
kubectl delete pod redis-ha-server-1 -n
Смотрю логи: теперь всё ок. Соединение не теряется, реплика стабильна, никто не пиздит в канале алёртов.
Что сделать, чтобы всё это не слетело
Всё, что я накрутил руками, временное:
- kubectl edit, kubectl patch и смена updateStrategy слетят на следующем helm upgrade или sync
- CONFIG SET живёт до рестарта пода, потом конфиг снова берётся из ConfigMap
По-нормальному это должно жить в values чарта, в Git:
redis-ha:
redis:
startupProbe:
failureThreshold: 120
config:
client-output-buffer-limit: "slave 1073741824 536870912 60"
repl-backlog-size: "512mb"
Итог.
- дефолты редиса (или чарта редиса?) не рассчитаны на 550 MB кеша (на тот момент было около 25000+ аппилкейшнов)
- Helm не делает reconcile, но ручные правки в кластере всё равно не живут, всё постоянное должно лежать в values
- если под пересоздаётся со старым спеком, смотрите managedFields; RollingUpdate может помочь, но мастер тоже перекатится, будет failover
- CONFIG SET живёт до рестарта 🫠
Довольный, на дейли рассказываю, как героически победил редис.
Лид дослушивает, кивает и спрашивает:
- А нахера ты с этим возился? Это ж редис арго, там только кеш.
Дропнул бы базу, и всё.
- ...
Читаю интернеты, так и есть. Всё лечилось одной командой на мастере:
kubectl exec redis-ha-server-0 -c redis -n --
sh -c 'redis-cli -a $AUTH FLUSHALL'
Реплика синкает пустую базу за секунды, арго сам заново греет кеш.
Справедливости ради, кеш снова дорастёт до 550 MB, так что values с лимитами всё равно пригодятся. Но это можно было сделать спокойно, а не под алертами!
Мораль: прежде чем героически чинить, спроси себя, что это вообще за данные и жалко ли их.🤡🤡🤡
Сгорая от стыда, пишу эту заметку и скрываюсь в тумане 🚶♀
Однажды пришёл на работу, я oncall, вижу алерт - поды рестартятся.
Смотрю, что там:
kubectl get pods -n | grep redis
redis-ha-server-1 1/4 Running 7 10m
Ага, один под постоянно рестартится. 7 раз за 10 минут.
Смотрю логи:
kubectl logs -p redis-ha-server-1 -c redis -n
1:S * MASTER REPLICA sync started
1:S * Full resync from master
1:S * MASTER REPLICA sync: receiving streamed RDB from master
1:signal-handler Received SIGTERM scheduling shutdown...
1:S * User requested shutdown...
Реплика начинает Full Resync, а через 27 секунд получает SIGTERM и умирает. А синхронизация большой базы занимает минуты.
Смотрю ивенты:
kubectl get events -n --field-selector involvedObject.name=redis-ha-server-1 --sort-by='.lastTimestamp'
Normal Killing pod/redis-ha-server-1 Container redis failed startup probe, will be restarted
Warning Unhealthy pod/redis-ha-server-1 Startup probe failed: role=slave; repl=sync
Ага, контейнер убивает стартап проб.
Проверяю настройки:
kubectl describe pod redis-ha-server-1 -n | grep -A1 Startup:
Startup: exec [sh -c /health/redis_readiness.sh] delay=5s timeout=15s period=10s #success=1 #failure=3
failureThreshold: 3, periodSeconds: 10, initialDelaySeconds: 5.
Первая проверка на 5-й секунде, дальше каждые 10: 5, 15, 25. Третья неудача подряд, и контейнер убивают. Плюс пара секунд на сам скрипт, отсюда и мои 27.
А база весит 550 мегабайт, и Full Resync идёт дольше.
Пока реплика синхронизируется, health check возвращает ошибку.
Через три неудачи подряд startup probe считает контейнер мёртвым, и кублет его перезапускает. Синк начинается заново, и так по кругу.
Проблема вроде бы понятна. Нужно увеличить failureThreshold.
Правлю StatefulSet:
kubectl edit statefulset redis-ha-server -n
Меняю failureThreshold у контейнера redis с 3 на 120. Сохраняю.
Удаляю под:
kubectl delete pod redis-ha-server-1 -n
Жду, смотрю логи.
Та же херня, рестарты продолжаются.
Сука.
Проверяю, что применилось в поде:
kubectl get pod redis-ha-server-1 -n
-o jsonpath='{.spec.containers[?(@.name=="redis")].startupProbe.failureThreshold}'
3
Бл, под создался со старым значением.
А в StatefulSet что?
kubectl get statefulset redis-ha-server -n
-o jsonpath='{.spec.template.spec.containers[?(@.name=="redis")].startupProbe.failureThreshold}'
120
В StatefulSet уже 120, а в поде 3.
Что за ебанная магия?
Проверяю, кто управляет StatefulSet:
kubectl get statefulset redis-ha-server -n -o yaml | head -30
metadata:
annotations:
meta.helm.sh/release-name:
meta.helm.sh/release-namespace:
labels:
app.kubernetes.io/managed-by: Helm
Helm. Но сам по себе Helm ничего не откатывает: reconcile у него нет, ресурсы он трогает только на helm upgrade.
Кто именно подсунул поду старый шаблон, я так и не выяснил. Если поймаете такое, смотрите managedFields: там видно, кто и когда писал в объект.
kubectl get statefulset redis-ha-server -n --show-managed-fields -o yaml
Ладно. Шаблон в StatefulSet правильный, значит, надо заставить контроллер пересоздать поды из него. Смотрю стратегию обновления:
kubectl get statefulset redis-ha-server -n -o jsonpath='{.spec.updateStrategy}'
{"type":"OnDelete"}
При OnDelete контроллер обновляет под, только когда его удаляют. Я его и удалил, а он всё равно поднялся со старым шаблоном. Почему, честно, не знаю.
Переключаю на RollingUpdate, чтобы контроллер перекатил всё сам:
kubectl patch statefulset redis-ha-server -n --type=json -p='[
{"op": "replace", "path": "/spec/updateStrategy/type", "value": "RollingUpdate"}
]'
Проверяю новый под:
kubectl get pod redis-ha-server-1 -n
-o jsonpath='{.spec.containers[?(@.name=="redis")].startupProbe.failureThreshold}'
120
Отлично, теперь 120.
Смотрю логи: Full Resync завершился успешно, всё синхронизировалось.
Но дальше вижу, что сразу после успешной синхронизации соединение теряется.
Смотрю логи мастера:
kubectl logs redis-ha-server-0 -c redis -n --tail=50
1:M * Connection with replica lost.
1:M # closed for overcoming of output buffer limits
Ага, мастер рвёт соединение, потому что переполнился output buffer.
Проверяю лимиты:
kubectl exec redis-ha-server-0 -c redis -n --
sh -c 'redis-cli -a $AUTH CONFIG GET client-output-buffer-limit'
slave 268435456 67108864 60
Hard limit 256 MB, soft limit 64 MB на 60 секунд.
Буфер на мастере дорос до 152 MB. До hard limit не дотянул, но soft limit (64 MB) держался превышенным дольше 60 секунд. Этого достаточно, чтобы мастер убил соединение.
Пока мастер стримит RDB, все новые записи копятся в этом буфере. Большая база и активная запись дают ровно такую картину.
Увеличиваю лимиты:
kubectl exec redis-ha-server-0 -c redis -n --
sh -c 'redis-cli -a $AUTH CONFIG SET client-output-buffer-limit "slave 1073741824 536870912 60"'
Теперь hard limit 1 GB, soft limit 512 MB. CONFIG SET применяется сразу, без рестарта. Важен он именно на мастере, но я прописал его на всех нодах: sentinel может переключить мастера в любой момент.
Заодно поднимаю repl-backlog-size до 512 MB. Full sync это не лечит: backlog нужен, чтобы после короткого разрыва реплика догналась частичным PSYNC, а не начинала полный синк заново. Но и памяти он съедает столько же, так что проверь лимиты, если не хочешь поймать OOMKill.
Перезапускаю реплику, чтобы она заново прошла full sync уже с новыми лимитами:
kubectl delete pod redis-ha-server-1 -n
Смотрю логи: теперь всё ок. Соединение не теряется, реплика стабильна, никто не пиздит в канале алёртов.
Что сделать, чтобы всё это не слетело
Всё, что я накрутил руками, временное:
- kubectl edit, kubectl patch и смена updateStrategy слетят на следующем helm upgrade или sync
- CONFIG SET живёт до рестарта пода, потом конфиг снова берётся из ConfigMap
По-нормальному это должно жить в values чарта, в Git:
redis-ha:
redis:
startupProbe:
failureThreshold: 120
config:
client-output-buffer-limit: "slave 1073741824 536870912 60"
repl-backlog-size: "512mb"
Итог.
- дефолты редиса (или чарта редиса?) не рассчитаны на 550 MB кеша (на тот момент было около 25000+ аппилкейшнов)
- Helm не делает reconcile, но ручные правки в кластере всё равно не живут, всё постоянное должно лежать в values
- если под пересоздаётся со старым спеком, смотрите managedFields; RollingUpdate может помочь, но мастер тоже перекатится, будет failover
- CONFIG SET живёт до рестарта 🫠
Довольный, на дейли рассказываю, как героически победил редис.
Лид дослушивает, кивает и спрашивает:
- А нахера ты с этим возился? Это ж редис арго, там только кеш.
Дропнул бы базу, и всё.
- ...
Читаю интернеты, так и есть. Всё лечилось одной командой на мастере:
kubectl exec redis-ha-server-0 -c redis -n --
sh -c 'redis-cli -a $AUTH FLUSHALL'
Реплика синкает пустую базу за секунды, арго сам заново греет кеш.
Справедливости ради, кеш снова дорастёт до 550 MB, так что values с лимитами всё равно пригодятся. Но это можно было сделать спокойно, а не под алертами!
Мораль: прежде чем героически чинить, спроси себя, что это вообще за данные и жалко ли их.🤡🤡🤡
Сгорая от стыда, пишу эту заметку и скрываюсь в тумане 🚶♀