Поддержка Hyperdrive для MySQL теперь общедоступна (GA). Hyperdrive, пул соединений с базами данных Cloudflare для Workers, изначально поддерживал только Postgres. С момента анонса бета-версии поддержки протокола MySQL в прошлом году мы работали над тем, чтобы сделать обработку MySQL такой же надежной, как и поддержку Postgres.
Мы извлекли определенные уроки из создания и запуска пула соединений MySQL в сети Cloudflare. Оказывается, разница между бета-версией и релизом (GA) заключается всего лишь в 20 исправленных ошибках.
Во время бета-тестирования нашей целью было подтвердить, что реальное использование клиентами соответствует целевому показателю доступности в производстве на уровне 99,99%. Мы знали, что в нашей надежности есть пробелы, так как отслеживаем частоту завершения клиентских соединений из Worker с ошибкой, связанной с Hyperdrive. Кроме того, мы получали отзывы от участников бета-тестирования, которые привели к обнаружению редкой и неочевидной ошибки в нашей производственной среде. Чтобы передать всю сложность этой конкретной ошибки, нам сначала нужно объяснить, как работает Hyperdrive.
Что такое Hyperdrive?
Hyperdrive — это распределенный пул соединений с базами данных, который соединяет Cloudflare Workers с базами данных SQL. Hyperdrive поддерживает активные соединения SQL в нашей глобальной сети и распределяет эти соединения между множеством запросов Worker. Помимо пула соединений, Hyperdrive также может кэшировать частые запросы в нашем глобальном кэше CDN, снижая задержку и нагрузку на исходную базу данных.
Установление соединения с базой данных SQL стоит дорого и требует как минимум пяти сетевых обращений туда и обратно. На бессерверной вычислительной платформе, такой как Workers, устанавливать соединение для каждого запроса было бы нецелесообразно как из-за дополнительной задержки для каждого запроса, так и потому, что большое количество открытых соединений может привести к высоким затратам ресурсов на каждое соединение. Хотя MySQL здесь выглядит лучше, чем Postgres, поскольку MySQL не использует целый процесс ОС на одно соединение, типичное развертывание все равно может столкнуться с проблемами ресурсов при большом количестве соединений.
Для получения дополнительной информации см. наш блог-пост, представляющий Hyperdrive.
Путь к стабильности
Некоторые из проблем, с которыми мы столкнулись, были небольшими и легко исправимыми после сообщения о них. Одной из первых проблем, о которых сообщили в бета-версии, было то, что смайлики, записанные в базу данных, возвращались некорректно. Это была ошибка, связанная с кодировкой набора символов. Hyperdrive согласовывает кодировку для соединения во время начального рукопожатия, и мы согласовываем utf8 в качестве кодировки. Как выяснилось, utf8 — это устаревший псевдоним для utf8mb3, который не охватывает весь набор UTF-8: он поддерживает только 1–3 байта на кодовую точку Unicode, в то время как полный UTF-8 имеет максимум 4 байта на кодовую точку. Чтобы получить весь набор UTF-8, нам нужно было изменить наше рукопожатие на использование utf8mb4, что и исправило проблему с эмодзи.
Другая ошибка, связанная с кодировкой, возникла, когда мы добавили очистку соединений с помощью COM_RESET_CONNECTION. Это легкая команда MySQL для очистки любого состояния сеанса в данном соединении, что как раз и нужно Hyperdrive, чтобы гарантировать отсутствие утечки состояния на уровне сеанса между проверками. Но добавление этого шага очистки соединения снова привело к проблемам с кодировкой! Оказывается, COM_RESET_CONNECTION не сбрасывает состояние соединения до кодировки набора символов, согласованной нами при установлении соединения; вместо этого она сбрасывает соединение до значений по умолчанию сервера. Это означает, что после согласования utf8mb4 на экземпляре сервера MySQL, у которого по умолчанию используется latin1, после выполнения COM_RESET_CONNECTION наше соединение получает кодировку latin1. Исправление заключается в том, чтобы всегда выполнять команду установки кодировки utf8mb4 после сброса соединения.
Трудноуловимая ошибка
Во время бета-тестирования наш клиент с наибольшим объемом трафика MySQL сообщил о небольшом, но постоянном проценте пустых ответов на запросы, для которых всегда ожидался непустой ответ. Мы начали расследование с прочесывания всей нашей телеметрии и понимания логики пользователя. Затем мы также увеличили частоту трассировки для этого экземпляра Hyperdrive в надежде увидеть последовательность выполнения этой ошибки. Мы искали сетевые проблемы, которые могли бы объяснить это поведение, но ничто, казалось, не объясняло его. Эта ошибка возникала примерно в 0,001% случаев, что сильно затрудняло поиск релевантных сквозных трассировок выполнения.
Но у нас была одна улика: ошибка возникала только на экземплярах Hyperdrive с включенным кэшированием. Это заставило нас заподозрить проблему с кэшированием MySQL в Hyperdrive.
Кэширование Hyperdrive
Чтобы понять кэширование Hyperdrive, нам сначала нужно рассказать о топологии Hyperdrive и о том, как она взаимодействует с кэшем CDN. Каждый экземпляр Hyperdrive имеет разделенную архитектуру с двумя узлами: клиентом (Client) и конечной точкой (Endpoint). Узел Client работает на том же компьютере, что и Worker пользователя, обрабатывает локальное кэширование (если оно включено) и при необходимости пересылает запросы на узел Endpoint. Узел Endpoint удерживает пул соединений и располагается в дата-центре, ближайшем к исходной базе данных. Каждый узел использует кэш CDN Cloudflare, локальный для того дата-центра, в котором он запущен. Важно отметить, что это означает, что если оба узла запущены в одном дата-центре, они будут разделять общий кэш.
Совместное использование кэша между Client и Endpoint в целом полезно для производительности. Мы используем хэш содержимого сетевых сообщений запроса с некоторой нормализацией и сохраняем результирующие байты в кэше. Если узел Hyperdrive Client, запущенный в том же дата-центре, что и его Endpoint, получает запрос, и этот Endpoint недавно кэшировал результаты того же запроса, Client может обслужить ответ напрямую из общего кэша дата-центра. Для корректной работы эта схема требует, чтобы для одного и того же ответа кэшировались одни и те же байты, независимо от того, сохранены ли они клиентом или конечной точкой.
Протокол клиент/сервер MySQL
Поскольку мы не могли получить прямую трассировку проблемы, мы решили уделить время изучению MySQL client / server protocol. Возможно, в нашу обработку MySQL было заложено предположение о протоколе Postgres, которое и вызывало эти проблемы.
Протокол описывает, как клиент MySQL взаимодействует с сервером MySQL. Это может быть несколько запутанным в случае пула соединений с базой данных, так как у нас есть два компонента, действующих как клиенты, и два, действующих как серверы. Первый клиент — это Worker, использующий клиентскую библиотеку MySQL, и он подключается к первому компоненту, действующему как сервер, — клиенту Hyperdrive (Hyperdrive Client). Клиент Hyperdrive подключается к конечной точке Hyperdrive (Hyperdrive Endpoint), которая действует как клиент MySQL, подключающийся к фактическому исходному серверу MySQL.
Этот протокол начинается с фазы подключения, на которой клиент MySQL и сервер MySQL согласовывают общий метод аутентификации и возможности. После установления соединения клиент и сервер начинают фазу команд, в ходе которой клиент отправляет команды (например, операторы SQL) для выполнения, а сервер отвечает и ожидает следующую команду.
Возможности обмениваются во время Фазы Подключения для поддержки эволюции протокола с сохранением обратной совместимости. Каждая возможность представляет собой функцию, например, поддержку сжатия во время Фазы Команд. Клиенты и серверы должны использовать только те функции, которые поддерживаются другой стороной, в противном случае может произойти рассинхронизация протокола, приводящая к неожиданному поведению.
Это согласование возможностей может оказаться сложной задачей для Hyperdrive, поскольку Hyperdrive выступает в роли клиента по отношению к исходной базе данных и в роли сервера по отношению к Worker. Чтобы гарантировать, что Hyperdrive может синтаксически анализировать сообщения от исходного сервера MySQL, Hyperdrive имеет фиксированный набор флагов возможностей, которые используются для связи с исходной базой данных. Поскольку набор возможностей Hyperdrive может отличаться от набора возможностей клиента Worker, Hyperdrive должен преобразовывать все ответы источника, закодированные с помощью его возможностей, в возможности, ожидаемые Worker.
При попытке отладить эту проблему мы обнаружили, что популярная клиентская библиотека MySQL для Node.js (mysql2) в настоящее время не поддерживает возможность CLIENT_DEPRECATE_EOF, которая изменяет порядок завершения наборов результатов. Без этой возможности сервер отправляет EOF-пакет между определениями столбцов и строками, а затем еще один EOF-пакет в конце набора результатов. Когда согласовано CLIENT_DEPRECATE_EOF, метаданные EOF-пакета опускаются, а финальный EOF-пакет заменяется на OK-пакет. Сервер MySQL версии 5.7.5 и выше поддерживает эту возможность, но новое поведение с OK-пакетом используется только тогда, когда клиент согласовывает его.
Хотя Hyperdrive поддерживает возможность CLIENT_DEPRECATE_EOF, вполне возможно, что драйвер MySQL в Worker ее не поддерживает. Чтобы справиться с этим, Hyperdrive корректирует свое поведение так, чтобы поддерживать возможности, ожидаемые клиентским драйвером Worker. Это означает переписывание наборов результатов с включением EOF и преобразование всех OK-пакетов в EOF при необходимости.
После нескольких дней изучения телеметрии, документации и кода Hyperdrive все встало на свои места. Мы наконец-то натолкнулись на последний недостающий элемент: ошибочное предположение в нашем коде кэширования Hyperdrive Client. Мы заложили в основу при реализации кэширования Client для Postgres следующее предположение: мы можем кэшировать ровно те байты, которые мы записали обратно в Worker. Хотя это было разумное предположение для сетевого протокола Postgres, оно больше не выполняется из-за несоответствия в возможностях MySQL. Если узел Client кэширует ровно то, что он записывает обратно в Worker, эти байты имеют другой формат, чем тот, который ожидает узел Endpoint. Это происходит потому, что узел Endpoint всегда работает с возможностями Hyperdrive, в то время как узел Client записывает байты, преобразованные в соответствии с возможностями Worker.
Теперь, если мы сложим все эти подсказки воедино, мы сможем понять сценарии, в которых Hyperdrive может вернуть ответ MySQL, закодированный с неправильными возможностями, что приводит к неожиданным пустым результатам и зависаниям. Ошибка возникает, когда и Client, и Endpoint находятся в одном датацентре, используют общий кэш, и когда один из них читает значение, кэшированное другим.
Вот более подробный взгляд на два таких сценария.
Endpoint кэширует, Client читает
- Endpoint находится в процессе обслуживания запроса для запроса Q. Он получает ответ R от исходного сервера и кэширует байты результата ответа R_hyperdrive (ответ, закодированный с возможностями Hyperdrive) в общий кэш.
- Client получает другой запрос для запроса Q, проверяет общий кэш и находит R_hyperdrive.
- Client отвечает Worker с помощью R_hyperdrive без преобразования в возможности Worker.
На этой диаграмме показан этот поток:
Client кэширует, Endpoint читает
- Client завершает обслуживание запроса для запроса Q и записывает байты переведенного ответа R_worker в кэш.
- Endpoint находится в процессе обслуживания запроса для Q и проверяет кэш на наличие R_worker.
- Endpoint отправляет R_worker другому Client, и этот Client пытается преобразовать R_worker в возможности Worker, в результате чего отправляет преждевременный EOF или зависает.
Вы можете увидеть это здесь:
Как только мы обнаружили этот тип сбоя, исправление оказалось простым. Теперь мы следим за тем, чтобы всегда записывать обратно в кэш каноническую версию байтов результата запроса, закодированную с использованием возможностей Hyperdrive, независимо от того, какой узел ее сохранил. После выпуска этого изменения проблема с пустыми строками исчезла, а коэффициент успешности клиентских соединений достиг 99,99%.
Надежность — это функция
Вы уже сегодня можете использовать поддержку MySQL в Hyperdrive! Создайте Hyperdrive для существующей базы данных, совместимой с MySQL, через панель управления или wrangler, либо подключитесь к новой базе данных PlanetScale. Если у вас возникли какие-либо проблемы или вопросы, обращайтесь на наш Discord.










