Не так давно, развернув в 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.

Читайте также в нашем блоге:

Комментарии (4)


  1. broken-ufa
    17.11.2021 14:13
    +2

    Серег, а как же PR в апстрим? https://github.com/enriclluelles/redis-sentinel-proxy/pulls


    1. sudoroot Автор
      17.11.2021 14:14
      +7

      Будет!)


  1. Papa_Carlo
    18.11.2021 07:35
    +3

    Спасибо за детали. Я не тестировал ваше решение. Надеюсь оно отлично работает.

    В Своем кластере редис я использовал "haproxy" готовое решение и работает быстро. Всё из коробки. И не надо ни приложение переписывать и ничего менять.

    Sentinel управляет -> redis кластером. А haproxy работает с мастером. И если происходит "failover" переключениe master- slave. То haproxy это всё очень быстренько переключает..

    Также плюсы у haproxy есть веб интерфейс и можно его мониторинг состояния итд


    1. sudoroot Автор
      18.11.2021 09:28

      Отличное решение! Спасибо, что поделились опытом!