Как ClickHouse пишет пакетами?

Введение

Пакетная запись также называется массовой записью.Для сценария вставки нескольких фрагментов данных в одну таблицу это может уменьшить количество запросов на вставку и повысить пропускную способность и эффективность. Официальный драйвер Golang от Clickhouse clickhouse-go [1] поддерживает эту ключевую функцию, но вступление к документу не очень подробное, всего одно предложение:

Bulk write support : begin->prepare->(in loop exec)->commit

Он не представляет подробно использование и принципы.Библиотека, используемая автором при разработке бизнеса, - sqlx [2] , и sql также поддерживает драйвер clickhouse-go. Обратитесь к официальному образцу кода [3] :

...
tx, err := connect.Begin()
checkErr(err)
stmt, err := tx.Prepare("INSERT INTO example (country_code, os_id, browser_id, categories, action_day, action_time) VALUES (?, ?, ?, ?, ?, ?)")
checkErr(err)

for i := 0; i < 100; i++ {
 if _, err := stmt.Exec(
  "RU",
  10+i,
  100+i,
  []int16{1, 2, 3},
  time.Now(),
  time.Now(),
 ); err != nil {
  log.Fatal(err)
 }
}
...

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

Здесь следует отметить, что если вы используете функцию перехода по коду в редакторе, вы перейдете к реализации функции database/sqlв библиотеке Exec, На самом деле код, который мы хотим увидеть, является реализацией в clickhouse-go, Что касается причины почему редактор прыгает на базу данных/sql, я не разобрался, когда писал эту статью, так что давайте сначала копать яму .

основная реализация

Основной код stmt.Exec выглядит следующим образом [4] :

func (stmt *stmt) execContext(ctx context.Context, args []driver.Value) (driver.Result, error) {
 if stmt.isInsert {
  stmt.counter++
  if err := stmt.ch.block.AppendRow(args); err != nil {
   return nil, err
  }
  if (stmt.counter % stmt.ch.blockSize) == 0 {
   stmt.ch.logf("[exec] flush block")
   if err := stmt.ch.writeBlock(stmt.ch.block); err != nil {
    return nil, err
   }
   if err := stmt.ch.encoder.Flush(); err != nil {
    return nil, err
   }
  }
  return emptyResult, nil
 }
 if err := stmt.ch.sendQuery(stmt.bind(convertOldArgs(args))); err != nil {
  return nil, err
 }
 if err := stmt.ch.process(); err != nil {
  return nil, err
 }
 return emptyResult, nil
}

Вышеприведенный код невелик и очень нагляден, при выполнении Exec stmt.ch.block.AppendRow(args)параметр sql сначала будет привязан к блоку локального кеша, а потом уже (stmt.counter % stmt.ch.blockSize)будет судить, достигает ли размер локального кеша порога, и достигает ли он порога , он будет выполнен, и Flush()данные будут записаны на удаленный конец. Подводя итог, основная логика реализации в clickhouse-go такова:

  1. Нижний уровень поддерживает блок кеша и устанавливает block_size для управления размером кеша.

  2. При выполнении stmt.Exec не будет напрямую писать в удаленный ClickHouse, а будет вставлять в блок параметр Append

  3. После каждого Append судите о соотношении между размером блока и block_size, если точно делится, обновляйте блок (то есть пишите в clickhouse)

Поэтому параметр block_size очень важен, он представляет собой верхний предел локального кеша, если он слишком велик, то программа будет занимать часть памяти. Сначала автор поставил так, чтобы 100000в журнале отладки не было видно stmt.ch.logf("[exec] flush block")распечатанный лог.После настройки мало, можно увидеть следующий вывод:

...
[clickhouse][connect=1][begin] tx=false, data=false
[clickhouse][connect=1][prepare]
[clickhouse][connect=1][read meta] <- data: packet=1, columns=6, rows=0
[clickhouse][connect=1][exec] flush block
[clickhouse][connect=1][exec] flush block
....

Подведем итог

Многие драйверы БД поддерживают функцию массовой записи, и драйвер clickhouse-go не исключение, но его документация не очень подробная, но в документации указано, что это нужно делать в begin/commit. Кроме того, clickhouse не поддерживает транзакции, и формулировка begin/commit может сбивать с толку.

В этой статье анализируется исходный код clickhouse-go, разбирается процесс выполнения массовой записи и помогает разобраться в его конкретной реализации.

Рекомендации

[1]

кликхаус-го: https://github.com/ClickHouse/clickhouse-го

[2]

sqlx: https://github.com/jmoiron/sqlx

[3]

Официальный пример кода: https://github.com/ClickHouse/clickhouse-go/blob/master/examples/sqlx.go#L35-L51 .

[4]

Основной код выглядит следующим образом: https://github.com/clickhouse/clickhouse-go/blob/master/stmt.go#L44-L68 .

[5]

Заявление INSERT INTO: https://clickhouse.tech/docs/en/sql-reference/statements/insert-into/

[6]

go-clickhouse-batchinsert: https://github.com/MaruHyl/go-clickhouse-batchinsert/blob/master/batch.go#L349-L354

Guess you like

Origin blog.csdn.net/u011387521/article/details/112001211