Не так давно, развернув в Kubernetes уже привычный инфраструктурный компонент в виде кластера Redis Sentinel + redis-sentinel-proxy, мы столкнулись с интересными проблемами. При тестировании времени переключения мастера выяснилось, что оно составляет полторы минуты. Это очень долго.
Расскажу, как получилось ускорить процесс.
Введение
Redis Sentinel — это инструмент, который позволяет собрать кластер из СУБД Redis в режиме master-replica. Redis Sentinel сам мониторит, настраивает и переключает роли. Если мастер-узел падает, сервис сам выбирает узел на замену, назначает его мастером и выполняет перенастройку. Более подробно о работе сервиса можно почитать в документации.
N.B. Далее для простоты буду называть Redis Sentinel просто Sentinel.
Казалось бы, звучит классно. Давайте везде использовать Sentinel вместо обычного Redis! Но тут есть определенные сложности. Взять и просто подменить не получится, потому что с Sentinel надо взаимодействовать иначе:
сначала нужно обратиться к нему, чтобы получить адрес мастера,
и только потом установить подключение к самому мастеру.
Если классическая работа с Redis — скажем, из Python — выглядит так:
import redis
r = redis.Redis(host='localhost', port=6379, db=0,
ssl=True, ssl_cert_reqs=None)
r.set('foo', 'bar')
#True
r.get('foo')
#b'bar'
… то в случае с Sentinel необходимо выполнить чуть больше действий:
from redis.sentinel import Sentinel
sentinel = Sentinel([('localhost', 26379)], socket_timeout=0.1)
master = sentinel.master_for('mymaster', socket_timeout=0.1)
master.set('foo', 'bar')
#True
master.get('foo')
#b'bar'
С одной стороны, вроде бы несложно. Но все зависит от контекста.
Когда мы начинаем работать с клиентом, обычно проводим кубернетизацию приложения: заводим его в контейнеры, описываем манифесты для работы в Kubernetes, добавляем чарты с инфраструктурой. Но клиент не всегда готов «прямо сейчас» взять и исправить код для работы с уже привычным ему инструментом по другому алгоритму. Бывают более приоритетные задачи — какие-то issues, которые надо закрыть «вчера». Именно для таких переходных ситуаций, когда код будет адаптирован, но позже, придумана утилита redis-sentinel-proxy.
Как она работает:
При старте подключается к Sentinel и начинает раз в секунду опрашивать адрес текущего мастера.
Одновременно с этим ожидает входящие соединения.
Когда приходит запрос на соединение от клиента, она начинает работать как прокси, подставляя адрес текущего мастера в Sentinel-кластере.
Таким образом, подключение к redis-sentinel-proxy работает как будто это прямое подключение к Redis (к актуальному мастеру кластера).
И все бы хорошо, но ровно до того момента, когда — по какой-либо причине — падает мастер.
Проблемы
И вот как это происходит:
мастер падает;
Sentinel выжидает по умолчанию 5 секунд*, давая мастеру время вернуться;
если не дожидается — начинает выбор нового лидера:
1:X 31 Oct 2021 04:57:07.876 # +sdown master mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:07.928 # +odown master mymaster 10.111.1.96 6379 #quorum 2/2
1:X 31 Oct 2021 04:57:07.928 # +new-epoch 1
1:X 31 Oct 2021 04:57:07.928 # +try-failover master mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:07.936 # +vote-for-leader fe2f2d90e64748eaee767fe372368816c3781e7b 1
1:X 31 Oct 2021 04:57:07.948 # 2e3a1bb98b4599ce44085e138a418b387d72996d voted for fe2f2d90e64748eaee767fe372368816c3781e7b 1
1:X 31 Oct 2021 04:57:07.951 # 3287a37d2b90f7784dd5720f9ead5580bab38134 voted for fe2f2d90e64748eaee767fe372368816c3781e7b 1
1:X 31 Oct 2021 04:57:07.992 # +elected-leader master mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:07.992 # +failover-state-select-slave master mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:08.063 # +selected-slave slave 10.111.5.104:6379 10.111.5.104 6379 @ mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:08.063 * +failover-state-send-slaveof-noone slave 10.111.5.104:6379 10.111.5.104 6379 @ mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:08.147 * +failover-state-wait-promotion slave 10.111.5.104:6379 10.111.5.104 6379 @ mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:08.866 # +promoted-slave slave 10.111.5.104:6379 10.111.5.104 6379 @ mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:08.867 # +failover-state-reconf-slaves master mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:08.961 * +slave-reconf-sent slave 10.111.6.33:6379 10.111.6.33 6379 @ mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:09.932 * +slave-reconf-inprog slave 10.111.6.33:6379 10.111.6.33 6379 @ mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:09.932 * +slave-reconf-done slave 10.111.6.33:6379 10.111.6.33 6379 @ mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:10.007 # +failover-end master mymaster 10.111.1.96 6379
1:X 31 Oct 2021 04:57:10.007 # +switch-master mymaster 10.111.1.96 6379 10.111.5.104 6379
1:X 31 Oct 2021 04:57:10.008 * +slave slave 10.111.6.33:6379 10.111.6.33 6379 @ mymaster 10.111.5.104 6379
1:X 31 Oct 2021 04:57:10.008 * +slave slave 10.111.1.96:6379 10.111.1.96 6379 @ mymaster 10.111.5.104 6379
1:X 31 Oct 2021 04:57:15.041 # +sdown slave 10.111.1.96:6379 10.111.1.96 6379 @ mymaster 10.111.5.104 6379
1:X 31 Oct 2021 04:57:23.933 # +set master mymaster 10.111.5.104 6379 down-after-milliseconds 5000
1:X 31 Oct 2021 04:57:23.939 # +set master mymaster 10.111.5.104 6379 failover-timeout 10000
* За это отвечает параметр down-after-milliseconds
. Из документации:
The down-after-milliseconds value is 5000 milliseconds, that is 5 seconds, so masters will be detected as failing as soon as we don't receive any reply from our pings within this amount of time.
В этот момент в redis-sentinel-proxy рвутся соединения. Что делает клиент? Правильно, переподключается. И вот тут-то и кроется корень всех зол!
Пока новый мастер не поднялся, все новые соединения подвисают, пока не отвалятся по tcp-timeout
.
И здесь добавляется еще одна проблема — подвисает проверка контейнера.
В Kubernetes есть readiness probe. Единственное, что она делает: redis-cli -p 9999 ping
. Поскольку probe проходит внутри контейнера, при выполнении она подвисает — так же, как и ее нерадивое соединение. При этом redis-sentinel-proxy не может принимать новые соединения.
redis-sentinel-proxy-7c64fd975b-6hmc8 0/1 Running 0 11h 10.111.2.21 k8s-worker-8efd466b-bddb8-wnck2 <none> <none>
Как мы выяснили ранее, у Sentinel уходит около 5 секунд на то, чтобы подождать мастера (вдруг вернётся), и еще 2 — на переключение роли мастера. А соединение в redis-sentinel-proxy висит намного дольше. Сделав замеры, я увидел страшную цифру: переключение redis-sentinel-proxy на нового мастера, с учетом зависших соединений занимает порядка 80-90 секунд!
Решение
Первый подход в решении проблемы оказался быстрым, но, будем честными, костыльным. Я просто добавил timeout 1
перед командой liveness probe. В принципе, это частично решило проблему. Получилось: timeout 1 redis-cli -p 9999 ping
. Если ping в пробе не прошел, и таймаут в 1 секунду истёк, контейнер получает SIGTERM, и совершает перезапуск. Время переключения redis-sentinel-proxy на новый мастер сократилось в среднем до 20 секунд.
Но мне не давало покоя то, что решение не выглядело оптимальным. К тому же, ведь код redis-sentinel-proxy несложный — там всего один файлик на 120 строк…
И тогда решил вспомнить молодость и сдуть пыль с IDE (на самом деле, я ею пользуюсь часто, но для написания YAML-файлов). Потратив несколько часов на поднятие из глубин памяти синтаксиса Golang, изучение исходников и библиотеки работы с TCP, я нашел решение.
Было:
remote, err := net.DialTCP("tcp", nil, remoteAddr)
if err != nil {
log.Println(err)
local.Close()
return
}
d := net.Dialer{Timeout: 1 * time.Second}
remote, err := d.Dial("tcp", remoteAddr.String())
if err != nil {
log.Println(err)
local.Close()
return
}
Да-да, ради этих пары строк кода весь шум! Ведь проблема была именно в отсутствии таймаута на соединение.
А теперь — тесты!
Финальная проверка
После того, как нужные изменения были внесены, соединения больше не висели.
Временные затраты по итогам тестов:
5 секунд — на ожидание Sentinel, поднимется ли мастер;
2 секунды — на смену лидера;
1-2 секунды (если укладываемся в таймаут) — на отработку readiness probe.
Итого — 10 секунд (против 80-90).
P.S.
Читайте также в нашем блоге: