Redis через VPN кажется медленным: меньше круговых обменов на запрос
VPN не делает удалённый вызов Redis локальным. Планируйте круговые обмены на запрос, переиспользуйте соединения и держите конвейеры подальше от транзакций.
Страница, отвечающая за 200 мс при локальном Redis, занимает четыре секунды, как только клиент оказывается за VPN, а счётчики сервера остаются простаивать. Медленный журнал не показывает ничего выше миллисекунды, график CPU ровный, и экземпляр без жалоб обслуживает других клиентов. Изменилось расстояние, а расстояние умножает число остановок и ожиданий в вашем коде. Пройдите путь запроса от клиента наружу, и решение почти всегда — меньше последовательных ожиданий, а не более быстрый сервер.
Начните с клиента, а не с метрик сервера
Наблюдайте за процессом клиента между первой и последней командой. Если трассировка показывает ответ, паузу, а затем следующую команду, вы платите полный круговой обмен за каждую команду, и никакая настройка сервера это не изменит. Медленный журнал здесь не тот инструмент, потому что он фиксирует время выполнения и намеренно исключает сетевой ввод-вывод. Чистый медленный журнал рядом с медленной конечной точкой — подпись проблемы круговых обменов.
Измерьте сам путь, прежде чем трогать код. redis-cli -h redis.example.internal --latency отправляет повторные PING и печатает среднее время запроса и ответа, то есть вашу стоимость за один обмен. Умножьте её на число последовательных команд в одном действии пользователя, и вы получите нижнюю границу для конечной точки.
Посчитайте круговые обмены, которые действительно нужны одному действию пользователя
Возьмите одну конечную точку и выпишите каждый вызов Redis по порядку, включая скрытые внутри ORM, промежуточного слоя сессии или поиска флага функции. Большинство медленных конечных точек делают двадцать или тридцать вызовов там, где хватило бы трёх, и скрытые обычно составляют большинство.
Цепочку зависимостей нельзя свернуть: если второй команде нужен ключ, который возвращает первая, ожидание реально, и единственные варианты — другая модель данных или серверная операция, выражающая весь шаг сразу. Независимые чтения — другой случай, потому что ничто не заставляет ждать первого ответа до отправки второго. Отметьте каждый вызов как зависимый или независимый до любых изменений, и форма решения станет очевидной.
Важное число — последовательные ожидания, а не общее число команд. Тридцать команд в одном пакете всё равно стоят одного ожидания.
Объединяйте в пакет только команды, которые не зависят друг от друга
Конвейер ставит независимые команды в очередь и собирает ответы по порядку, превращая N ожиданий в одно. Большинство клиентов предлагают его как блок, обратный вызов или менеджер контекста, а не отдельный режим соединения, поэтому проверьте API, который поставляет установленная версия. Выигрыш растёт со временем кругового обмена, и именно поэтому он важен на VPN и почти незаметен на localhost.
import redis
r = redis.Redis(host="redis.example.internal", port=6379, socket_timeout=2)
pipe = r.pipeline(transaction=False)
for key in ("user:1001", "user:1002", "user:1003"):
pipe.get(key)
print(pipe.execute())
Передайте transaction=False, когда нужно только пакетирование. По умолчанию клиент оборачивает пакет в MULTI и EXEC, что делает его атомарным и меняет то, что другие клиенты могут наблюдать в полёте. Атомарность — решение о корректности, а не рычаг производительности, и конвейер не является транзакцией только потому, что оба сокращают круговые обмены.
Читайте каждый ответ, а не только отправку пакета. Команда в очереди может вернуть ошибку, пока пакет в целом завершается, а клиент, который выбрасывает список ответов, превращает неудачную запись в тихую. Сравните длину списка ответов с числом поставленных в очередь команд.
Держите соединение живым между действиями
Каждое новое соединение платит за разрешение имён, рукопожатие TCP, рукопожатие TLS и AUTH, а через VPN каждое из них — круговой обмен, который нельзя пакетировать. Создавайте клиент один раз на процесс и используйте повторно или берите пул, который предлагает ваша библиотека, с ограниченным размером. Переподключение на каждый запрос — вторая по частоте причина этого симптома после ожиданий на команду.
Храните приватное имя хоста в конфигурации, а не буквальный адрес. Проверка сертификата и идентичность службы по-прежнему действуют в приватной сети, а закрепление IP ломается в тот момент, когда провайдер перенумерует подсеть. Если разрешение имён — медленная часть, исправьте путь резолвера, а не строку подключения.
Задайте явный таймаут сокета, чтобы зависший туннель быстро завершался сбоем, а не подвешивал воркер. Затем подтвердите, что пул действительно переиспользуется, залогировав создание соединений в клиенте.
Определите размер пакета до того, как он станет проблемой памяти
Redis хранит ответы в очереди в течение всего пакета, а клиент затем хранит разобранный результат. Конвейер из десяти тысяч больших значений меняет сетевые ожидания на всплески памяти и долгие паузы на обоих концах.
Выбирайте размер пакета по объёму ответа, а не по числу команд. Несколько сотен мелких команд в одном пакете обычно комфортны, а сто многокилобайтных нагрузок — нет. Делите по байтам и по количеству, и держите каждый пакет в пределах бюджета задержки, который у конечной точки действительно есть.
Измерьте от начала до конца после изменения: redis-cli -h redis.example.internal --latency-history показывает, стабилен ли сам путь, а ваш собственный таймер запросов показывает, улучшилась ли конечная точка. Если конечная точка всё ещё медленная, а история задержек ровная, оставшиеся затраты — работа на сервере или ваша собственная сериализация, и следующий шаг — профиль обработчика, а не очередное изменение пакетирования.