Перейти к содержимому
Последнее обновление

Подготовленные операторы

Кратко Apache Doris поддерживает подготовленные операторы на стороне сервера по сетевому протоколу MySQL. Если в URL JDBC задать useServerPrepStmts=true, FE разбирает оператор и строит его план один раз за сеанс, а затем повторно использует этот план при каждом EXECUTE. При большом QPS точечных запросов путь short-circuit повышает пропускную способность примерно в 4 раза; при пакетных INSERT и запросах с повторяющейся структурой больше не приходится выполнять разбор и построение плана для каждой строки.

Подготовленные операторы в Apache Doris: поддержка подготовленных операторов на стороне сервера по протоколу MySQL с кешированием планов после разбора в рамках сеанса, что снижает нагрузку на CPU FE при повторных запросах.

Зачем использовать подготовленные операторы в Apache Doris?​

Подготовленные операторы Apache Doris кешируют план после разбора в рамках сеанса и повторно используют его при каждом EXECUTE. Это устраняет затраты на разбор, анализ и оптимизацию на стороне FE, которые преобладают в нагрузках с большим QPS. Большинство аналитических движков выполняют разбор и построение плана для каждого запроса. В ежедневном задании по формированию отчёта эти затраты незаметны. Но они становятся определяющими, когда сервис начинает выполнять один и тот же запрос тысячи раз в секунду. Пользовательская панель при каждом нажатии считывает строку по первичному ключу. Хранилище признаков при каждом предсказании считывает вектор по идентификатору сущности. Загрузчик массовых данных снова и снова выполняет один и тот же INSERT INTO t VALUES (?, ?, ?).

Во всех трёх случаях текст SQL одинаковый; меняются только параметры. Повторный разбор и построение плана этого оператора при каждом вызове зря расходуют CPU FE. Уже при нескольких сотнях QPS FE достигает предела загрузки, прежде чем BE начинает выполнять существенную работу, и добавление узлов BE не помогает. В такой ситуации помогает PreparedStatement.

  • При большом QPS узким местом становится планировщик Frontend, а не движок хранения.
  • Повторяющиеся запросы при каждом вызове требуют затрат на разбор, анализ, оптимизацию и сериализацию.
  • Отдельное KV-хранилище требует слишком много усилий ради снижения стоимости точечных запросов.

Что представляет собой подготовленный оператор Apache Doris?​

Подготовленный оператор Apache Doris представляет собой серверную реализацию пары команд сетевого протокола MySQL COM_STMT_PREPARE / COM_STMT_EXECUTE (с COM_STMT_CLOSE для удаления из кеша). Эти команды обрабатывает FE, используя кеш планов для каждого сеанса. Когда клиент открывает соединение с useServerPrepStmts=true, драйвер JDBC отправляет COM_STMT_PREPARE один раз для каждого уникального текста SQL. FE разбирает и анализирует оператор, а затем сохраняет план в кеше сеанса, используя идентификатор оператора в качестве ключа. При каждом последующем EXECUTE передаются только байты параметров в бинарном формате. FE подставляет значения в кешированный план и выполняет его.

Основные термины

  • useServerPrepStmts=true: флаг URL JDBC, который переключает драйвер MySQL Connector/J с подстановки значений в строку на стороне клиента на обмен по бинарному протоколу с помощью COM_STMT_PREPARE / COM_STMT_EXECUTE.
  • cachePrepStmts=true: отдельный флаг JDBC, который кеширует дескриптор подготовленного оператора на клиенте. Без него каждый вызов повторно отправляет COM_STMT_PREPARE, сводя ускорение на нет.
  • Кеш операторов сеанса: ассоциативный массив в памяти FE (ConnectContext.preparedStatementContextMap), содержащий разобранный PrepareCommand, StatementContext и необязательный ShortCircuitQueryContext. Его размер ограничен параметром max_prepared_stmt_count (по умолчанию 100,000).
  • Заполнитель (?): позиционный параметр, значение которого задаётся при выполнении. Один оператор может содержать до 65,536 таких параметров.
  • ShortCircuitQueryContext: дополнительный кеш, который существует наряду с подготовленным планом, если запрос подходит для выполнения по пути short-circuit для точечных запросов. Он содержит сериализованную таблицу дескрипторов, выходные выражения и UUID, по которому BE находит собственные заранее построенные структуры.

Как работает подготовленный оператор Apache Doris?​

Работа подготовленного оператора Apache Doris состоит из пяти этапов: подготовка SQL на FE, кеширование плана short-circuit при соблюдении условий, выполнение с передачей параметров в бинарном формате, необязательное журналирование аудита и закрытие с удалением из кеша.

  1. Подготовка. Клиент отправляет COM_STMT_PREPARE "SELECT ... WHERE k1 = ?". FE разбирает SQL, выполняет анализ Nereids, сохраняет PreparedStatementContext в ассоциативном массиве соединения и возвращает идентификатор оператора и количество параметров.
  2. Кеширование пути short-circuit (при соблюдении условий). Когда запрос обращается к одной таблице Unique Key и содержит предикаты равенства по всем столбцам ключа, правило преобразования LogicalResultSinkToShortCircuitPointQuery устанавливает флаг, а FE сериализует таблицу дескрипторов и выходные выражения в ShortCircuitQueryContext с UUID в качестве ключа.
  3. Выполнение. Каждый COM_STMT_EXECUTE содержит идентификатор оператора, битовую карту NULL, коды типов (при первом выполнении) и байты параметров. FE декодирует их в значения Literal, подставляет на места заполнителей и выполняет кешированный план. Повторный разбор, анализ и оптимизация не выполняются.
  4. Аудит (необязательно). При enable_prepared_stmt_audit_log=true в fe.audit.log восстанавливается текст оператора со значениями параметров для отладки. По умолчанию эта возможность отключена, поскольку при большом QPS журнал быстро заполняется.
  5. Закрытие. COM_STMT_CLOSE удаляет запись из кеша сеанса. При закрытии соединения удаляется весь ассоциативный массив.

Быстрый старт​

CREATE TABLE tbl_point_query (
k1 INT,
v1 VARCHAR(64)
)
UNIQUE KEY(k1)
DISTRIBUTED BY HASH(k1) BUCKETS 1
PROPERTIES (
"enable_unique_key_merge_on_write" = "true",
"light_schema_change" = "true",
"store_row_column" = "true"
);
String url = "jdbc:mysql://fe-host:9030/db"
+ "?useServerPrepStmts=true&cachePrepStmts=true"
+ "&prepStmtCacheSize=500&prepStmtCacheSqlLimit=1024";

try (Connection c = DriverManager.getConnection(url, "root", "");
PreparedStatement ps = c.prepareStatement(
"SELECT * FROM tbl_point_query WHERE k1 = ?")) {
ps.setInt(1, 42);
try (ResultSet rs = ps.executeQuery()) { /* ... */ }
}

Ожидаемый результат

В fe.audit.log для второго и последующих вызовов отображается:

Stmt=EXECUTE(-2147481418)

вместо исходного текста SQL. Это признак повторного использования кешированного плана. Дополнительно выполните EXPLAIN: в узле сканирования должно отображаться SHORT-CIRCUIT, что подтверждает работу обоих уровней.

Когда следует использовать подготовленные операторы Apache Doris?​

Подготовленные операторы Apache Doris подходят для точечных запросов с большим QPS к таблицам Unique Key, повторяющихся пакетных INSERT и любых клиентских нагрузок, в которых один и тот же оператор выполняется тысячи раз в секунду. Они не подходят для разовых произвольных запросов, драйверов без корректной поддержки подготовки на стороне сервера и операторов с недетерминированными функциями на пути short-circuit.

Подходящие сценарии

  • Точечные запросы с большим QPS к таблицам Unique Key, в которых преобладают затраты на разбор.
  • Пакетные INSERT с Group Commit, когда загрузчик выполняет один и тот же INSERT INTO t VALUES (?, ?, ?) тысячи раз в секунду.
  • Любой запрос, который один клиент повторяет достаточно часто: получение признаков, проверка состояния, проверка по спискам разрешённых значений, чтение данных онлайн-сервисами.
  • Нагрузки, для которых вы предпочли бы обойтись без отдельного KV-хранилища рядом с Apache Doris.

Неподходящие сценарии

  • Разовые произвольные запросы. Обмен сообщениями PREPARE обходится дороже, чем сэкономленный разбор.
  • Драйверы без корректной поддержки подготовленных операторов на стороне сервера. Например, для драйвера Rust sqlx сообщалось о проблемах с реализацией Apache Doris. Используйте MySQL Connector, который корректно работает с бинарным протоколом.
  • Соединения, в которых задано useServerPrepStmts=true без cachePrepStmts=true. Каждый вызов повторно отправляет COM_STMT_PREPARE, что медленнее стандартного режима подготовки на стороне клиента.
  • Операторы с недетерминированными функциями (NOW(), RAND()). BE отказывается повторно использовать кешированный контекст short-circuit, поэтому экономятся только затраты на разбор.
  • Диапазонные запросы и запросы с агрегацией, если вы рассчитываете на ускорение short-circuit. PreparedStatement по-прежнему экономит затраты на разбор, но BE выполняет обычный распределённый план. Для таких нагрузок используйте отсечение данных.

Производительность и проверка​

Doris сообщает об увеличении пропускной способности в 4 раза и более благодаря PreparedStatement в нагрузках с точечными запросами, где узким местом является CPU FE. Это тот же прирост в 4 раза, который описан с точки зрения short-circuit в карточке функции «Точечные запросы с высокой конкурентностью»: единый совокупный эффект, а не два независимых ускорения, которые можно перемножить. Полный набор оптимизаций для высокой конкурентности (Unique Key + Merge-on-Write + row store + row cache + short-circuit + PreparedStatement) достигает 30,000+ QPS на узел в YCSB на машине с 16 ядрами и 64GB памяти, а средняя задержка снижается на 96% по сравнению с исходной конфигурацией без оптимизации. Вклад PreparedStatement трудно отделить от остальных оптимизаций; когда ограничением служит FE, ориентируйтесь на рост в 4 раза как на нижнюю границу при планировании.

Две проверки позволяют убедиться, что этот путь выполнения используется:

  • В fe.audit.log для повторных вызовов отображается Stmt=EXECUTE(stmtId) вместо исходного текста SQL.
  • Для точечных запросов EXPLAIN показывает SHORT-CIRCUIT в узле сканирования.

Если ограничением по-прежнему остаётся CPU FE, увеличьте число Observers и используйте балансировку нагрузки JDBC: jdbc:mysql:loadbalance://h1,h2,h3/db?useServerPrepStmts=true&cachePrepStmts=true.

Дополнительные материалы​