Есть два способа сказать серверу SET. В первом вы отправляете HTTPS-запрос с заголовком авторизации, ждёте ответ и забываете о нём: следующий запрос начнётся с чистого листа. Во втором вы открываете соединение, оно остаётся открытым, по нему одна за другой бегут команды, а сервер помнит, кто с ним разговаривает. Снаружи оба способа выглядят как «положить значение в Redis». Внутри это два разных мира, и именно на их границе ломаются переезды, которые казались делом пяти минут: поменять строку адреса и перезапустить приложение.

Эта статья о том, почему такая замена не всегда работает, что именно нужно проверить в коде и как перенести с Upstash Redis на собственный VPS не только адрес, но и поведение приложения.

Путь одной команды: от вызова в коде до сервера

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

У Upstash есть HTTPS REST API: он принимает Redis-подобные команды через HTTP, требует авторизацию по Bearer token и возвращает ответ в JSON. В документации этот HTTP-интерфейс описан как отдельный по отношению к обычному протоколу Redis. Это ключевая фраза для переезда. Адрес вида «https://что-то» и адрес вида «хост:порт» ведут не к одной и той же двери с разными табличками, а к разным дверям.

Теперь представьте, что в приложении стоит клиент, настроенный на REST. Он собирает HTTP-запрос, подставляет токен в заголовок, отправляет его по HTTPS и разбирает JSON. Если вы направите его на IP-адрес VPS, где слушает обычный Redis, клиент по-прежнему будет говорить на языке HTTP. Redis, который ждёт собственный протокол, не поймёт заголовков и JSON. Результат: ошибка соединения, обрыв или невнятный ответ, а вовсе не «почти работает».

Верно и обратное. Если взять привычную библиотеку для Redis, которая открывает TCP-соединение и говорит на протоколе Redis, и дать ей HTTPS-адрес REST-сервиса, она тоже ничего не получит. Поэтому первый вопрос при миграции звучит не «какой у нас хост?», а «на каком языке наш код разговаривает с хранилищем?».

Как найти зависимость от REST-обёртки

В репозитории эту зависимость обычно можно найти за вечер. Не нужно читать весь код: достаточно проследить, откуда приложение получает объект для работы с Redis. Признаки REST-клиента:

  • в переменных окружения два значения: URL и токен, а не хост, порт и пароль;
  • в зависимостях стоит пакет, название которого указывает на HTTP- или REST-доступ, а не универсальный клиент протокола Redis;
  • клиент создаётся прямо в обработчике запроса или на верхнем уровне модуля, потому что «соединения» как такового нет, и заботиться о его жизни не нужно;
  • в коде нет ни пула, ни обработчиков события разрыва, ни логики повторного подключения.

Последний пункт самый показательный. Там, где транспорт без состояния, код часто пишется без мыслей о состоянии. Это не ошибка автора, а естественное следствие среды. Но при переезде именно это упрощение придётся вернуть.

Второе место, где прячется зависимость, — окружение запуска. Если приложение работает в среде, где процесс создаётся на время запроса и уничтожается после него, HTTP-доступ там удобен: нечего закрывать и нечему протухать. На обычном VPS процесс живёт долго, а значит, у соединения появляется возраст, и с этим надо уметь обращаться.

Схема изменения кода без привязки к конкретной библиотеке

Точный синтаксис зависит от выбранных вами пакетов, поэтому сверяйтесь с их документацией. Сама схема правок одинакова:

Было:   клиент = REST-клиент(URL, TOKEN)
Стало: клиент = клиент протокола Redis(HOST, PORT, пароль, TLS по необходимости)

Было: создаётся на каждый запрос или при импорте модуля
Стало: создаётся один раз при старте, закрывается при остановке

Было: ошибка = сбой одного HTTP-вызова
Стало: ошибка = сбой вызова, обрыв соединения или переподключение

Практический приём: вынесите работу с хранилищем за небольшой собственный интерфейс из трёх-четырёх функций (получить, записать с временем жизни, удалить, инкрементировать). Пока приложение напрямую вызывает методы библиотеки в сотне мест, любая замена клиента превращается в поиск и замену по всему проекту. Если все обращения идут через тонкую прослойку, переезд касается одного файла. Прослойка заодно становится точкой, где удобно логировать задержки и ошибки.

Соединение, которое живёт дольше запроса

Постоянное соединение даёт выигрыш: нет рукопожатия на каждую команду, нет повторной передачи заголовков, команды можно отправлять пачкой. Но платой за него становится жизненный цикл, о котором приложение раньше не думало. Проследим его.

Старт процесса. При запуске клиент открывает соединение. Если Redis ещё не поднялся (например, оба сервиса стартуют одновременно после перезагрузки сервера), первая попытка может провалиться. Хорошо, когда клиент пробует снова с нарастающей паузой, а не падает сразу. Плохо, когда приложение стартует «успешно», но каждая операция с кешем бросает исключение.

Перезапуск приложения. Процесс завершается, соединение закрывается, сервер освобождает ресурсы. Если при остановке клиент не закрыт аккуратно, сервер какое-то время считает соединение живым. Для одного процесса это мелочь, но если вы перезапускаете приложение десятки раз при отладке или у вас много воркеров, счётчик подключений растёт неприятно.

Перезапуск Redis. Теперь обрывается соединение с другой стороны. Клиент должен заметить обрыв, переподключиться и решить, что делать с командой, которая была в полёте. Для чтения кеша разумно получить промах и пойти в базу. Для записи платежа без идемпотентности повторять вслепую нельзя. Это решение принимает автор приложения, а не библиотека, и в HTTP-мире такого вопроса просто не стояло: каждый запрос заканчивался сам по себе.

Тихий обрыв. Сетевое оборудование на пути (балансировщики, NAT, межсетевые экраны) может закрыть простаивающее соединение, не сообщив об этом ни одной из сторон. Приложение узнает об этом на следующей команде, которая зависнет до тайм-аута. Лечится настройкой тайм-аутов на клиенте, периодической проверкой соединения и разумной политикой повторов. Конкретные параметры у каждой библиотеки свои.

Несколько воркеров. Каждый процесс держит своё соединение или пул. Ограничение числа клиентов на стороне сервера нужно сверить с реальным количеством воркеров, умноженным на размер пула, и с запасом на служебные подключения: мониторинг, скрипты, консоль администратора.

Если Redis стоит на том же VPS, что и приложение, сетевой путь коротким, а ненужные промежуточные устройства исчезают. Тогда доступ можно ограничить локальным интерфейсом и вообще не открывать порт наружу. Если Redis вынесен на отдельный сервер, потребуются пароль, правила межсетевого экрана и, вероятно, шифрование. Как поднять сам сервер, мы разбирали в статье про установку и настройку Redis на VPS; здесь же важнее то, что вокруг него должно измениться в клиенте.

Команды совместимы — а значения?

Вопрос «поддерживает ли мой Redis те команды, которыми я пользуюсь» разумно разбить на два: что делает код и что лежит в данных.

Документация Upstash описывает REST как способ передавать Redis-подобные команды через HTTP. Слово «подобные» стоит прочитать внимательно, а не как «в точности те же». На практике это означает простую инвентаризацию: составьте список реально используемых команд и сверьте каждую с версией сервера, которую собираетесь поставить на VPS. Не по памяти, а по поиску в коде. Для этого достаточно найти все вызовы методов клиента и свести их в таблицу «команда — где используется — на что влияет».

Отдельно обратите внимание на три места, где поведение зависит не только от команды:

  • Формат ответа. REST возвращает JSON, а клиент протокола Redis получает типизированные ответы. Библиотека-обёртка могла приводить числа, строки и пустые значения к удобному для вас виду, и ваш код мог неявно на это опираться. После смены клиента число может прийти строкой, а отсутствие ключа выглядеть иначе.
  • Сериализация. Если обёртка сама превращала объекты в JSON и обратно, а новый клиент этого не делает, в Redis окажутся странные строки, а при чтении вы получите сырой текст вместо объекта. Проверьте, кто отвечает за сериализацию: ваш код или библиотека.
  • Сценарии и расширения. Если приложение использует Lua-скрипты, модули или специальные структуры, убедитесь, что они есть на целевом сервере. Это проверяется не чтением документации к Upstash, а проверкой нового сервера.

Теперь про сами данные. Перенос значений — отдельная задача, независимая от замены клиента. Общая схема для живого переезда такая: приложение продолжает работать на старом хранилище, а вы читаете ключи пачками (обход по курсору, а не одной тяжёлой командой по всему пространству имён), для каждого ключа определяете тип, считываете значение и оставшееся время жизни и записываете на новый сервер вместе с этим временем. Обязательно сохраняйте TTL: если перенести ключ без него, он станет вечным и однажды съест память.

Для ключей, которые вообще не нужно переносить, это не требуется. И здесь мы подходим к самому полезному различию.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Три ключа — три судьбы

Не все данные в Redis одинаково ценны. Прежде чем решать, что переносить, выпишите назначения ключей. Возьмём три типичных, а значения времени жизни ниже условные, для иллюстрации.

Три назначения ключей и их отношение к потере и TTL
НазначениеПример ключаТипичный TTLЧто будет при потереЧто делать при переезде
Кеш ответовcache:product:1042минуты, например 300 секундвременный рост нагрузки на базу, пока кеш прогреваетсяне переносить; начать с пустого и следить за нагрузкой
Сессииsession:a81f…часы или дни, например 24 часа, с продлением при активностипользователей разлогинит; неприятно, но не катастрофаперенести с остатком TTL или принять разлогин в удобное время
Очередь задачqueue:emailsобычно без TTL у самой очередипотерянные письма, заказы, уведомленияпереносить обязательно или дренировать очередь перед переключением

Кеш по определению должен переживать собственную потерю. Если приложение без кеша не работает, значит, это уже не кеш, а хранилище, и его надо относить к другой строке таблицы. Переносить кеш обычно бессмысленно: его прогрев произойдёт сам, а для TTL в несколько минут к моменту завершения копирования половина ключей всё равно истечёт. Важнее подготовить базу к первым минутам холодного старта: если одновременно промахнётся много запросов, они все пойдут в базу. Если такой риск есть, помогает прогрев самых горячих ключей скриптом или постепенное переключение трафика.

Сессии — промежуточный случай. Потеря не разрушает данные, но бьёт по людям. Тут есть два пути. Первый: скопировать сессии вместе с оставшимся временем жизни непосредственно перед переключением и допустить, что часть активности в окне между копированием и переключением потеряется. Второй: принять массовый выход из аккаунтов и назначить его на спокойное время. Выбор зависит от продукта: для внутренней админки второй путь обычно дешевле, для магазина с корзинами и оплатой лучше первый. Отдельно проверьте, как приложение продлевает сессию: если TTL обновляется при каждом запросе, то ключ, скопированный минуту назад, уже устарел, и копия получит прежнее время жизни.

Очередь — случай, где потеря содержимого недопустима. Тут нельзя просто «скопировать ключи»: задачи в процессе выполнения, отложенные задачи и подтверждения обработки образуют связное состояние. Безопаснее остановить постановку новых задач, дать воркерам выработать очередь на старом хранилище, убедиться, что она пуста, и только потом переключить производителей и потребителей на новый адрес. Если остановка недопустима, воркеры можно запустить на обоих хранилищах одновременно на переходный период, а производителей переключить в первую очередь. И помните: при повторной доставке задачи обработчик должен переносить дубль безопасно, иначе любой сбой при переезде превратится в двойное списание. Идемпотентность нужно реализовать в самом обработчике; сам по себе новый идентификатор попытки её не создаёт.

Эта таблица заодно решает вопрос с ресурсами. Кеш можно ограничить по памяти и разрешить вытеснение старых ключей, а очередь и сессии лучше не отдавать на милость политике вытеснения. Если все три назначения живут в одном экземпляре, одна и та же политика памяти будет применяться ко всем. Поэтому полезно разнести их по разным базам или даже по разным экземплярам с разными настройками. Подбирая VPS, считайте не только объём данных, но и запас на рост, на копию в памяти при сохранении снимка и на пиковое число соединений. Ресурсы тарифа сами по себе не обещают ёмкость: их надо мерить на вашей нагрузке. Для небольшого кеша и сессий часто хватает начальных конфигураций вроде Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) или Uab (2 vCPU, 4 ГБ, 80 ГБ), но это отправная точка, а не гарантия; для заметных объёмов в памяти смотрите на старшие варианты, например Heka или Maat. NVMe полезен там, где вы включаете сохранение на диск и вам важна скорость записи журнала и снимков.

Что должен хранить сам Redis на диске

В облачном сервисе вопрос сохранности данных решает провайдер. На своём VPS его решаете вы. Redis умеет сохранять состояние на диск периодическими снимками и журналом операций; режимы включаются в конфигурации, и у каждого свой компромисс между скоростью и потерями при сбое. Для очереди и сессий разумно включить сохранение осознанно и проверить, что после перезапуска сервиса данные возвращаются. Для чистого кеша сохранение чаще отключают или оставляют по умолчанию, потому что восстановление старого кеша ценности не несёт. Автоматические резервные копии средствами тарифа не считайте данными по умолчанию: настройте копирование снимков самостоятельно, если данные важны, и проверьте восстановление на тестовой машине.

Репетиция переключения

Переезд хранилища лучше разыграть заранее, на копии окружения, в три прохода.

  1. Проход без трафика. Поднимите Redis на VPS, поменяйте клиент в тестовой сборке приложения, прогоните автотесты и ручной сценарий: вход, корзина, постановка задачи в очередь, получение результата. Здесь выявляются несовпадения форматов ответов и сериализации.
  2. Проход с обрывами. Пока приложение работает, перезапустите Redis и посмотрите, что делает клиент: восстанавливает ли соединение, сколько занимает пауза, какие ошибки видит пользователь. Затем перезапустите приложение и проверьте, нет ли утечки подключений. Именно этот шаг заменяет в вашей голове привычку HTTP-мира «каждый запрос независим».
  3. Проход с данными. Выполните перенос сессий и очереди в том порядке, который выбрали, и проверьте вручную несколько ключей: сохранился ли тип, значение и остаток TTL. Сравните количество ключей по каждому префиксу на старой и новой стороне, а не общее число в базе.

Само переключение теперь сводится к изменению конфигурации и перезапуску. Прежние значения адреса и токена оставьте на время в запасе, а в самом приложении не удаляйте REST-клиент, пока не убедитесь, что откат не понадобится: вернуть переменные окружения проще, чем восстанавливать код. Если вы сделали тонкую прослойку, переключатель можно сделать и флагом конфигурации, который выбирает реализацию.

Сеть: что изменится вокруг сервера

HTTPS-интерфейс решал за вас несколько сетевых вопросов: шифрование, аутентификацию токеном, публичный адрес. Собственный Redis ничего из этого не принесёт сам. Минимальный набор решений такой:

  • слушать только нужный интерфейс; если приложение на том же сервере, достаточно локального;
  • задать пароль или настроить учётные записи с ограничением команд;
  • если Redis доступен по сети, закрыть порт межсетевым экраном для всех адресов, кроме серверов приложения;
  • если трафик идёт между серверами через недоверенную сеть, включить шифрование на уровне Redis или туннеля.

Это не абсолютная защита, а базовая гигиена, без которой публичный Redis быстро становится проблемой. На VPS у вас есть root-доступ, значит, настроить всё перечисленное можно самостоятельно. Выбор локации тоже стоит связать с архитектурой: приложение и Redis выгоднее держать рядом, потому что задержка между ними умножается на каждую команду. Среди доступных локаций MAATRIX есть Россия, США, Франция и Великобритания; наличие конкретных образов в конкретной локации уточняйте в форме заказа.

Карта зависимостей

Когда все части разложены, переезд перестаёт быть одной задачей и превращается в три маленьких, которые можно проверять отдельно.

Что меняется при переходе с REST-доступа на собственный Redis
УровеньЧто меняетсяКак проверить
БиблиотекаREST-клиент с URL и токеном заменяется клиентом протокола Redis; появляются жизненный цикл соединения, повторные подключения, тайм-ауты; формат ответов и сериализация проверяются зановотесты на прослойке; перезапуск Redis и приложения под нагрузкой
Сетьвместо публичного HTTPS-адреса с Bearer token — хост, порт, пароль, правила экрана, при необходимости шифрование; расстояние до сервера влияет на каждую командупопытка подключения с постороннего адреса должна не проходить; замер задержки между приложением и Redis
Данныекеш начинается с пустого, сессии переносятся с TTL или сбрасываются осознанно, очередь дренируется или переносится целикомсверка количества ключей по префиксам; выборочная проверка типа, значения и оставшегося TTL

Если вы держите эту карту перед глазами, замена адреса перестаёт быть опасной: вы знаете, какой из трёх слоёв затронет каждая правка, и можете проверить его отдельно, не дожидаясь, пока приложение сломается где-то на стыке. А если на стыке что-то всё же ломается, значит, в какой-то строке карты осталась пустая клетка, и теперь вы знаете, в какой именно.