Внутри Hyperdrive: как мы устранили редкую ошибку кэширования MySQL
- Поддержка MySQL в Hyperdrive стала общедоступной; переход от бета-версии к GA включал 20 исправлений ошибок.
- Редкие пустые ответы возникали, когда Client и Endpoint в одном дата-центре читали общий кэш с данными в разных форматах MySQL.
- Исправление сохраняет в кэше канонические результаты, закодированные с возможностями Hyperdrive.
Почему это важно: После выпуска исправления доля успешных соединений Client достигла 99,99%.
Поддержка MySQL в Hyperdrive теперь доступна всем (GA). Hyperdrive — пул соединений Cloudflare с базами данных для Workers — изначально поддерживал только Postgres. С тех пор как в прошлом году мы объявили о бета-версии поддержки протокола MySQL , мы работали над тем, чтобы обработка MySQL была столь же надёжной, как и поддержка Postgres.
Создавая пул соединений MySQL и запуская его в сети Cloudflare, мы кое-чему научились. Оказалось, что разница между бета-версией и GA — это всего 20 исправлений ошибок, замаскированных под плащом.
Во время бета-тестирования нашей целью было проверить, достигает ли реальная нагрузка клиентов целевого показателя доступности в рабочей среде — 99,99%. Мы знали, что надёжность пока не идеальна, поскольку отслеживаем, как часто клиентские соединения из Worker завершаются ошибкой, связанной с Hyperdrive. Кроме того, отзывы участников бета-тестирования помогли обнаружить редкую и трудноуловимую ошибку в рабочей среде. Чтобы объяснить сложность этой ошибки, сначала нужно рассказать, как работает Hyperdrive.
Что такое Hyperdrive?
Hyperdrive — это распределённый пул соединений с базами данных, который связывает Cloudflare Workers с базами данных SQL. Hyperdrive поддерживает открытые активные соединения SQL в нашей глобальной сети и использует их для множества запросов Workers. Помимо пула соединений, 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, локальный для центра обработки данных, где он запущен. Важно, что если оба узла работают в одном центре обработки данных, они используют общий кэш.

Hyperdrive состоит из узлов Client и Endpoint, каждый из которых использует кэш центра обработки данных.

Если Client и Endpoint в Hyperdrive находятся в одном центре обработки данных, они используют общий кэш.
Совместное использование кэша узлами Client и Endpoint обычно повышает производительность. Мы хэшируем содержимое сетевых сообщений запроса с некоторой нормализацией и сохраняем в кэше байты результата. Если Client Hyperdrive, работающий в том же центре обработки данных, что и его Endpoint, получает запрос, а Endpoint недавно кэшировал результаты такого же запроса, Client может напрямую вернуть ответ из общего кэша центра обработки данных. Для корректной работы этой схемы одни и те же байты должны кэшироваться для одного и того же ответа — независимо от того, сохранил их Client или Endpoint.
Протокол клиент-сервер MySQL
Поскольку получить прямую трассировку проблемы нам не удалось, мы решили подробнее изучить протокол клиент-сервер MySQL. Возможно, в обработку MySQL закралась ошибка из-за предположения о протоколе Postgres.

Client Hyperdrive согласует возможности MySQL со стороны Worker и преобразует ответы на запросы, а Endpoint Hyperdrive взаимодействует с исходной базой данных.
Протокол описывает взаимодействие клиента MySQL с сервером MySQL. В случае пула соединений с базой данных это может несколько запутать: у нас есть два компонента, выступающих в роли клиентов, и два — в роли серверов. Первый клиент — это Worker, использующий клиентскую библиотеку MySQL; он подключается к первому компоненту, работающему как сервер, — Client Hyperdrive. Client Hyperdrive соединяется с Endpoint Hyperdrive, который действует как клиент MySQL и подключается к фактическому исходному серверу MySQL.
Протокол начинается с этапа установления соединения, на котором клиент и сервер MySQL согласуют общий метод аутентификации и набор возможностей. После установки соединения клиент и сервер переходят к этапу выполнения команд: клиент отправляет команды (например, инструкции SQL) для выполнения, а сервер отвечает и ждёт следующую команду.
Набор возможностей согласуется на этапе установления соединения, чтобы протокол можно было развивать с сохранением обратной совместимости. Каждая возможность представляет собой функцию, например поддержку сжатия на этапе выполнения команд. Клиенты и серверы должны использовать только те функции, которые поддерживает другая сторона, иначе может нарушиться синхронизация протокола и возникнуть непредвиденное поведение.
Согласование возможностей может быть непростой задачей для Hyperdrive, поскольку он выступает в роли клиента для исходной базы данных и в роли сервера для Worker. Чтобы Hyperdrive мог разбирать сообщения от исходного сервера MySQL, он использует фиксированный набор флагов возможностей для взаимодействия с исходной базой данных. Поскольку набор возможностей Hyperdrive может отличаться от набора возможностей клиента Worker, Hyperdrive должен преобразовывать все ответы исходного сервера, закодированные с учётом его возможностей, в формат, ожидаемый Worker.
Пытаясь разобраться в этой проблеме, мы выяснили, что популярная клиентская библиотека MySQL для Node.js (mysql2) пока не поддерживает возможность CLIENT_DEPRECATE_EOF, которая меняет способ завершения наборов результатов. Без этой возможности сервер отправляет пакет EOF между определениями столбцов и строками, а затем ещё один пакет EOF в конце набора результатов. Когда согласована возможность CLIENT_DEPRECATE_EOF, пакет EOF с метаданными не отправляется, а заключительный пакет EOF заменяется пакетом OK. Сервер MySQL версии 5.7.5 и более поздних поддерживает эту возможность, однако новое поведение с пакетом OK используется, только если клиент согласовал её.

При включённой возможности CLIENT_DEPRECATE_EOF MySQL не отправляет пакет EOF после определений столбцов и завершает набор результатов пакетом OK; без неё пакеты EOF отмечают обе границы.
Hyperdrive поддерживает возможность CLIENT_DEPRECATE_EOF, но драйвер MySQL в Worker может её не поддерживать. Чтобы решить эту проблему, Hyperdrive адаптирует своё поведение к возможностям, которые ожидает клиентский драйвер Worker. Это означает, что при необходимости он переписывает наборы результатов, добавляя пакеты EOF, и преобразует все пакеты OK в EOF.
После нескольких дней изучения телеметрии, документации и кода Hyperdrive всё наконец сложилось. Мы обнаружили последний недостающий элемент: ошибочное предположение в коде кэширования Client Hyperdrive. Реализуя кэширование на стороне 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, который пытается преобразовать R_worker под возможности Worker и в итоге отправляет преждевременный EOF или зависает.
Вот как это выглядит:

Когда мы обнаружили этот сценарий сбоя, решение оказалось простым. Теперь мы всегда сохраняем в кэше каноническую версию байтов результата запроса, закодированную с учётом возможностей Hyperdrive, независимо от того, какой узел её записал. После выпуска этого изменения проблема с пустыми строками исчезла, а доля успешных соединений Client достигла 99,99%.
Надёжность — это функция
Поддержкой MySQL в Hyperdrive можно пользоваться уже сегодня! Создайте Hyperdrive для существующей базы данных, совместимой с MySQL, через панель управления или wrangler либо подключитесь к новой базе данных PlanetScale. Если у вас возникнут проблемы или вопросы, напишите нам в Discord.
npx wrangler hyperdrive create HYPERDRIVE_NAME --connection-string="mysql://user:password@db-host.example.com:3306/dbname"Cloudflare Blog прочитано 887 раз
