Сокет Node.Js зависает в пуле соединений ClickhouseDb

Я подключаю свое приложение Node.Js к собственной базе данных Clickhouse на EC2. В первой реализации все работало нормально, когда для каждого запроса создавалось новое соединение, а затем соединение закрывалось в конце этого запроса, но когда я переключился на пул соединений, чтобы уменьшить накладные расходы на создание новых соединений по каждому запросу. , в некоторых запросах отображается ошибка зависания сокета. Это отображается только в том случае, если одновременно выполняется несколько запросов для нагрузочного тестирования.

Точное сообщение об ошибке:

Error: socket hang up
    at connResetException (node:internal/errors:705:14)
    at Socket.socketCloseListener (node:_http_client:467:25)
    at Socket.emit (node:events:525:35)
    at TCP.<anonymous> (node:net:301:12) {
  code: 'ECONNRESET'
}

Я попытался увеличить тайм-аут запроса, а также тайм-аут соединения. Вот конфиг для подключения:

const { createClient } = require("@clickhouse/client");

const clickhouseClients = {};

const clickhouseClient = (dbName) => {
  if (!clickhouseClients[dbName]) {
    clickhouseClients[dbName] = createClient({
      host: process.env.CLICKHOUSE_HOST,
      username: process.env.CLICKHOUSE_USERNAME,
      password: process.env.CLICKHOUSE_PASSWORD,
      database: dbName,
      max_open_connections: 10,
      min_open_connections: 2,
      connection_timeout: 120000,
      request_timeout: 400000,
    });

    clickhouseClients[dbName]
      .ping()
      .then(() =>
        console.info(`Successfully connected to ClickHouse database: ${dbName}`)
      )
      .catch((error) => {
        console.error(`Failed to ping ClickHouse database ${dbName}:`, error);
        throw new Error(
          `Failed to initialize ClickHouse client for database ${dbName}`
        );
      });
  }

  return clickhouseClients[dbName];
};

module.exports = clickhouseClient;

Я также пробовал увеличить максимальное количество открытых соединений, но это не сработало. Я ожидаю, что это как-то связано с «keep_alive.idle_socket_ttl» в конфигурации Clickhouse.

Версия узла: 20.10.0 Версия клиента Clickhouse: 0.2.10

🤔 А знаете ли вы, что...
Node.js - это среда выполнения JavaScript, разработанная на основе JavaScript V8 движка, созданного Google.


72
1

Ответ:

Решено

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

Прежде всего, клиент уже использует HTTP-агент внутри себя, который должен управлять соединениями.

Если у вас много баз данных и запросов, вы можете в конечном итоге открыть слишком много сокетов, больше, чем позволяет LB/CH в их конфигурации (поскольку у каждого клиента будет до 10 сокетов в вашей конфигурации); Я не уверен, является ли это основной причиной проблемы, но потенциально это может быть так.

Вы можете явно указать имя базы данных в запросе или методе вставки, например.

const rows = await client.query({
  query: `SELECT * FROM ${dbName}.${tableName} LIMIT 10`,
  format: format,
})

Точно так же вы можете сделать то же самое в методе вставки:

await client.insert({
  table: `${dbName}.${tableName}`,
  format: 'JSONEachRow',
  values: [{ 
    // ...
  }]
})

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

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

По поводу дополнительных настроек:

  clickhouse_settings: {
    send_progress_in_http_headers: 1,
    http_headers_progress_interval_ms: "110000", // UInt64, should be passed as a string
  },

Если у вас нет (очень) длительных запросов, например, чего-то вроде INSERT FROM SELECT, вам это может не понадобиться; см. https://clickhouse.com/docs/en/integrations/language-clients/javascript#keep-alive-troubleshooting

Эта часть выглядит как висячее обещание

clickhouseClients[dbName]
  .ping()
  .then(() =>
    console.info(`Successfully connected to ClickHouse database: ${dbName}`)
  )
  .catch((error) => {
    console.error(`Failed to ping ClickHouse database ${dbName}:`, error);
    throw new Error(
      `Failed to initialize ClickHouse client for database ${dbName}`
    );
  });

Дополнительно пинг не кидает; см. документацию https://clickhouse.com/docs/en/integrations/language-clients/javascript#ping

Учитывая ваш генератор запросов и dbName/tableName, он, вероятно, также будет работать следующим образом:

const queryResponse = gcpQueryBuilder(
  dbClient,
  `${dbName}.${tableName}`,
  response.customer_id,
  selectedGroupBys,
  startDate,
  endDate,
  filters,
  true
);

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

Версия узла: 20.10.0 Версия клиента Clickhouse: 0.2.10

Пожалуйста, обновите клиент до последней стабильной версии. Начиная с версии 0.2.10, внутреннее управление сокетами было переработано, чтобы избежать проблемы зависания сокета.