Тайна Stunnel, ulimits и максимально открытые клиенты

Несколько месяцев назад мы начали получать уведомления Bugsnag о новой ошибке:

Redis::TimeoutError Connection timeout

Мы используем Redis по-разному в нашем приложении, от очередей фоновых заданий до отслеживания состояния регулирования запросов. Мы запускаем кластер Redis и используем Stunnel для обеспечения SSL-шифрования серверов Redis из разных систем.

Когда я начал устранение неполадок с этой ошибкой, я обнаружил странное сообщение об ошибке в журнале stunnel:

2016.10.27 12:59:20 LOG4[1885:139630634207040]: Connection rejected: too many clients (>=500)

После быстрого поиска в Google я обнаружил, что stunnel каким-то образом зависит от ограничений ресурсов, налагаемых на оболочку ядром Linux, в частности, в данном случае от общего количества дескрипторов открытых файлов.

Увеличение лимитов в запущенном процессе для подтверждения первой гипотезы

Моим первым решением было проверить эту гипотезу. Первая проблема, с которой я столкнулся, заключалась в том, что я не хотел перезапускать stunnel, чтобы помешать нашим воркерам получить доступ к Redis. Существует инструмент под названием prlimit, который предоставляет способ изменить ограничения на запущенный процесс, но по причинам, которые я не смог определить, он был удален из пакета util-linux в Ubuntu 14.04, дистрибутиве Linux, который мы используем. Я пытался написать напрямую /proc/<pid>/limits, как подсказал этот твит, но и это не сработало.

Поэтому я решил собрать пакет prlimit. К счастью, инженеры Shopify уже сделали это в репозитории Github с бэкпортами util-linux. Я скомпилировал пакет и загрузил его в наш репозиторий packagecloud.

После развертывания пакета я попытался изменить ограничения запущенного процесса, но на следующий день ошибки все еще были. Я был озадачен, потому что ядро ​​Linux сообщило об изменении ограничений. Были рассмотрены различные возможности: что ошибка слишком многих клиентов была отвлекающим маневром, или что prlimit на самом деле не работал, потому что отсутствовала какая-то странная настройка ядра, или даже что ядро ​​Ubuntu не поддерживало его.

На данный момент я еще не смог подтвердить свою гипотезу, а ошибки (разумеется) только появлялись в продакшене.

Попытка воспроизвести гипотезу другими средствами

Следующим моим шагом была попытка воспроизвести гипотезу в нашей тестовой среде. Для этой задачи я использовал инструмент redis-benchmark, чтобы сгенерировать нагрузку на другой стороне туннеля и попытаться достичь этого предела в 500 подключений.

После проб и ошибок я смог достичь лимита подключений. Для этого мне пришлось манипулировать ограничениями работающей оболочки с помощью ulimit, поскольку я смог достичь предела на той стороне туннеля, где я запускал redis-benchmark (Stunnel должен создавать туннели в обоих направлениях).

Это дало первую подсказку, потому что мне нужно было перезапустить stunnel в моих тестах после изменения ограничений в моей оболочке. Я приближался, но все же не мог по-настоящему определить процесс.

Я попытался вернуться к документации, но автор не сказал слишком много об этой проблеме, кроме необходимости изменить ограничения.

Как найти настоящий источник проблемы

В этот момент я начал копаться в исходном коде stunnel и нашел причину: Stunnel внутренне устанавливает максимальное количество клиентов во время своего запуска, используя мягкое ограничение на количество дескрипторов открытых файлов. Это конкретная формула, которая используется:

max_clients=max_fds*125/256

Это вполне логично, учитывая настройки ulimit на серверах, поэтому у меня было ощущение, что я близок к подтверждению своей первоначальной гипотезы. Недостаток: мне пришлось перезапустить stunnel для этого.

Перезапуск stunnel, не влияющий на сервис и делающий ограничения постоянными

В этот момент я попросил своего товарища по команде Дэвида помочь мне разработать разумное решение для перезапуска stunnel без ущерба для обслуживания. Решение было довольно простым: просто создайте еще один туннель и перенастройте воркеры sidekiq, чтобы они указывали на этот туннель, затем увеличьте лимиты и перезапустите исходный туннель.

Лучшим вариантом для этого было добавление параметров ulimit в скрипт инициализации stunnel, таким образом мы были уверены, что stunnel всегда работает с правильными ограничениями.

Подтверждение гипотезы и смягчение проблемы

Через несколько дней у нас стало гораздо меньше ошибок с stunnel. На данный момент проблема все еще присутствует, но, по крайней мере, смягчена. На данный момент количество ошибок терпимо, пока мы не сможем глубже понять, почему генерируется так много соединений Redis.

Подводя итог

В заключение, вот четыре момента, которые следует учитывать при попытке решить любую сложную операционную проблему:

  • Используйте теорию устранения неполадок
  • настойчиво
  • Просмотрите источник
  • Попросите о помощи, если вы заблудились

Автор: Хакобо Гарсия. Первоначально опубликовано на blog.dnsimple.com.