Анатомия запроса до и после движка ClickHouse

В данной статье описано, как устроен запрос в CHYT и откуда можно получить информацию об исполнении запроса.

Идентификаторы запроса: trace id, query id, datalens request id

С любым запросом связаны несколько идентификаторов, которые позволяют постфактум восстановить всю необходимую информацию об исполнении запроса.

  • query id — это идентификатор, назначаемый каждому запросу в ClickHouse. В отличие от оригинального ClickHouse, в CHYT этот идентификатор не контролируется пользователем и всегда имеет вид YTsaurus GUID – четыре шестнадцатеричных uint32, разделенных дефисами.
  • trace id — это идентификатор, который позволяет провязывать цепочки вызовов в распределенных системах, создавая "след" исполнения запроса. Это часть протокола трассировки opentracing, а конкретно — его реализации под названием Jaeger Tracing, использующейся в YTsaurus. trace id также является YTsaurus GUID, который в некоторых ситуациях совпадает с query id, а в некоторых не совпадает, о чем подробно будет написано ниже.

Путь запроса от клиента до тяжёлых прокси

Чтобы лучше понимать, по какой логике существуют и назначаются эти идентификаторы, следует разобраться в том, через какие компоненты в каком порядке проходит запрос в CHYT. Единственным публичным API доступа к CHYT на текущий момент является HTTP API, поэтому дальнейшее описание относится именно к протоколу HTTP.

Первым важным пунктом на пути исполнения запроса являются так называемые тяжёлые прокси YTsaurus, сетевой доступ к которым есть у всех пользователей YTsaurus. Тяжёлые прокси узнают, где сейчас живут инстансы клики с заданным алиасом, а затем отправляют запрос на случайно выбранный инстанс.

Ниже показаны 3 самых популярных способа доступа к YTsaurus.

При обращении к CHYT напрямую из скрипта либо из командной строки посредством утилиты curl в качестве endpoint используется SLB-балансер, например http://$YT_PROXY. За ним скрывается сложная конструкция из балансеров, которая направляет запрос на так называемые контрольные прокси, которые отвечают HTTP-редиректом на тяжелые прокси, обслуживающие всю тяжелую нагрузку в YTsaurus. При таком интерфейсе доступа query id совпадает с trace id: их можно увидеть в хедерах X-Yt-Trace-Id и X-ClickHouse-Query-Id. Ниже показан пример взаимодействия с CHYT через утилиту CURL, в котором отмечены наиболее интересные заголовки ответа.

curl -v --location-trusted 'http://$YT_PROXY/query?database=*ch_public' -d 'select max(a) from "//sys/clickhouse/sample_table"' -H "Authorization: OAuth `cat ~/.yt/token`"
*   Trying ip_address:80...
* Connected to $YT_PROXY (ip_address) port 80 (#0)
> POST /query?database=*ch_public HTTP/1.1
> Host: $YT_PROXY
> User-Agent: curl/7.69.1-DEV
> Accept: */*
> Authorization: OAuth <i>...<my_token>...</i>
> Content-Length: 50
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 50 out of 50 bytes
* Mark bundle as not supporting multiuse
<b> // Получаем redirect на тяжелую прокси.</b>
< HTTP/1.1 307 Temporary Redirect
< Content-Length: 0
< Location: http://sas4-9923-proxy-$YT_PROXY/query?database=*ch_public
< X-Yt-Trace-Id: 8e9bcc43-5c2be9b4-56f18c4e-117ea314
<
* Connection #0 to host $YT_PROXY left intact
* Issue another request to this URL: 'http://sas4-9923-$YT_PROXY/query?database=*ch_public'
*   Trying ip_address:80...
* Connected to sas4-9923-$YT_PROXY (ip_address) port 80 (#1)
> POST /query?database=*ch_public HTTP/1.1
> Host: sas4-9923-$YT_PROXY
> User-Agent: curl/7.69.1-DEV
> Accept: */*
> Authorization: OAuth <i>...<my_token>...</i>
> Content-Length: 50
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 50 out of 50 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Transfer-Encoding: chunked
<b>  // Обратите внимание, query id = trace id. </b>
< <b>X-ClickHouse-Query-Id:</b> 3fa9405e-15b29877-524e3e67-2be50e94
< <b>X-Yt-Trace-Id:</b> 3fa9405e-15b29877-524e3e67-2be50e94
<b>  // По техническим причинам X-Yt-Trace-Id встретится дважды.</b>
< X-Yt-Trace-Id: 3fa9405e-15b29877-524e3e67-2be50e94
< Keep-Alive: timeout=10
<b>  // Адрес инстанса-координатора запроса. </b>
< <b>X-ClickHouse-Server-Display-Name:</b> sas2-1374-node-$YT_PROXY
< X-Yt-Request-Id: 3fa9405d-26285349-db14531a-2a12b9f9
< Date: Sun, 05 Apr 2020 18:49:57 GMT
< Content-Type: text/tab-separated-values; charset=UTF-8
< X-ClickHouse-Summary: {"read_rows":"0","read_bytes":"0","written_rows":"0","written_bytes":"0","total_rows_to_read":"0"}
<b>  // Адрес тяжелой прокси, которая обслуживала запроса. </b>
< <b>X-Yt-Proxy:</b> sas4-9923-$YT_PROXY
<
1100
* Connection #1 to host sas4-9923-$YT_PROXY left intact

Исполнение запроса внутри клики

Тяжёлая прокси выбирает случайным образом один инстанс из клики и делегирует ему исполнение запроса. Данный инстанс называется координатором (или инициатором) запроса. Если запрос не использует таблицы YTsaurus (например, он обращается только к системным таблицам, начинающимся с префикса system., или это примитивный запрос типа SELECT 1), то он исполняется на координаторе также, как и в обычном ClickHouse.

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

SELECT someColumns, someFunctions(someColumns) FROM "//path/to/table" WHERE somePredicate(someColumns) [GROUP BY someColumns].

Данный запрос приводит к разбиению таблицы //path/to/table на примерно равные части, каждая из которых будет независимо обрабатываться на одном из узлов клики. Фактически, на каждый инстанс поступит видоизменённый запрос, у которого в FROM-клаузе //path/to/table будет заменено на некоторое выражение вида ytSubquery(...), в аргументе которого закодировано описание очередной части таблицы.

Таким образом, исполнение запроса сводится к исполнению некоторого количества удалённых подзапросов на инстансах клики. Картинка выше иллюстрирует данный механизм.

Здесь полезно понимать, что запросы бывают двух видов:

  • QueryKind: InitialQuery — исходные запросы, приходящие от клиентов;
  • QueryKind: SecondaryQuery — это переписанные удалённые подзапросы, в которых в FROM-клаузе фигурируют конкретные части таблиц YTsaurus.

Каждый secondary запрос наследует trace id родителя. При этом у каждого secondary запроса свой собственный query id; но несмотря на это каждый secondary запрос помнит initial запрос, к которому он относится. В логах системы родительский query id исходного initial запроса для secondary запроса обозначается термином InitialQueryId.

IO и компрессия в CHYT

Ниже приведён ряд фактов и рекомендаций касательно чтения данных из YTsaurus. Учитывайте их при использовании CHYT.

Про сеть

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

При этом следует отметить, что сеть внутри кластеров YTsaurus редко оказывается узким местом, но и такое иногда случается.

Про компрессию

Данные в YTsaurus, как правило, хранятся в сжатом виде. За компрессию отвечает атрибут /@compression_codec на таблице, его также можно увидеть в веб-интерфейсе просмотра таблицы. Про это есть отдельная статья в документации.

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

Про uncompressed block cache

В CHYT есть uncompressed block cache, под который по умолчанию отводится 20 GiB. Размер кеша можно регулировать параметром --uncompressed-block-cache-size в строке запуска клики.

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

  • Кеш хорошо работает, пока состав инстансов в клике остаётся неизменным. Если какой-то инстанс пропадает (например, из-за preemption, выхода ноды YTsaurus из строя или падения CHYT), то распределение будущих подзапросов по инстансам меняется произвольным образом и большая часть закешированных блоков данных в памяти инстансов может стать бесполезной, т. е. вырастет block cache miss. Впрочем, следующий же запрос после такой ситуации восстановит в кеше нужные данные.
  • Если данные в таблице меняются, то и распределение частей таблицы по подзапросом меняется произвольным образом, что аналогично приводит к block cache miss.
  • Наконец, если на один инстанс приходится существенно больше обрабатываемых данных, чем размер block cache (в терминах uncompressed data size), то довольно очевидно, что использование кеша не даст большой пользы для последующих запросов по тем же данным, так как большую часть все равно придётся заново прочитать по сети.

Про HDD vs SSD

Чтение с HDD медленнее, чем с SSD, однако не следует бросаться перекладывать свои данные на SSD, как только ваши запросы начинают тормозить. Есть вероятность, что ваши запросы упираются не в IO, а в CPU — например, все данные лежат готовые в block cache, но для ответа на запрос нужно обработать пару десятков гигабайт разжатых данных. В такой ситуации перекладывание таблиц с HDD на SSD не даст пользы.

Если вам кажется, что перекладывание ваших данных на SSD ускорит ваши запросы, то можете проследовать плану действий из FAQ.

Про поколоночный формат хранения

В большинстве случаев при работе с ClickHouse вы захотите использовать поколоночный формат хранения таблиц, который достигается выставлением атрибута /@optimize_for в значение scan.

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

Тем не менее, возможна ситуация, при которой понадобится построчный формат (/@optimize_for = lookup). Недостатком поколоночного формата является высокое потребление памяти, особенно для таблиц с большим числом колонок, так как на каждую колонку требуется отдельный буфер. Помимо этого можно отметить, что чтение lookup-таблиц несколько дешевле по CPU.

Предыдущая
Следующая