HikariPool и Firebird: снижение производительности при многопоточности

Дано: служба на джаве, посылающая в базу запросы (insert и update, иногда ещё select для проверки вставленного). Для соединения используем Hikari. Его инициализация:

HikariConfig config = new HikariConfig();
config.setJdbcUrl(connectionString + String.format(";poolId=%s", poolId));
config.setMinimumIdle(minConn);
config.setMaximumPoolSize(maxConn);
config.setConnectionTimeout(30 * 1000);
config.setAutoCommit(false);
config.setMaxLifetime(0);
config.setIdleTimeout(600 * 1000);
config.setTransactionIsolation("TRANSACTION_READ_COMMITTED");
if (connectionString.startsWith("jdbc:firebirdsql")) {
    Properties props = new Properties();
    props.setProperty("TRANSACTION_READ_COMMITTED", "isc_tpb_read_committed,isc_tpb_rec_version,isc_tpb_wait");
    config.setDataSourceProperties(props);
}
connectionPool = new HikariPool(config);

Вот как пересылаем запрос в базу:

connection = getConnectionPool().getConnection();
preparedStatement = connection.prepareStatement(strQuery);
preparedStatement.setFetchSize(fetchSize);
if (strQuery.startsWith("exec")) {
    preparedStatement.executeUpdate();
} else {
    resultSet = preparedStatement.executeQuery();
}
if (resultSet != null && resultSet.next()) {
    //...
}

//закрытие всего. всё обёрнуто try-catch так, что дальше некуда
if (resultSet != null) {
    try {
        if (!resultSet.isClosed()) {
            try {
                resultSet.clearWarnings();
            } catch (SQLException ex) {
                logger.warn();
            }
            resultSet.close();
        }
    } catch (SQLException ex) {
        logger.error();
    }
    resultSet = null;
}
if (preparedStatement != null) {
    try {
        if (!preparedStatement.isClosed()) {
            try {
                preparedStatement.clearWarnings();
            } catch (SQLException ex) {
                logger.warn();
            }
            preparedStatement.clearBatch();
            preparedStatement.close();
        }
    } catch (SQLException ex) {
        logger.error();
    }
    preparedStatement = null;
}
try {
    if (connection != null && !connection.isClosed()) {
        try {
            connection.clearWarnings();
        } catch (SQLException ex) {
            logger.warn();
        }
        connection.commit();
        connection.close();
    }
} catch (SQLException ex) {
    logger.error();
}

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

Может быть, база или драйвер jdbc не успевают прожевать? Мы попробовали ввести семафор, чтобы из четырёх потоков одновременно в базу писал только один - по идее, это должно быть равносильно одному потоку. Но нет, запросы отрабатывают так же медленно, как при четырёх потоках без семафора, и очередь копится.

Вопрос: на чём могут быть потери? Понятно, что база тут первый кандидат, но наш базист говорит, что на файрбёрде отладиться всё равно нельзя. Может, есть какой-то способ определить, кто виноват - Firebird, Java или что-то на их стыке (например, Hikari или jdbc).


Ответы (1 шт):

Автор решения: Anton Shchyrov

но наш базист говорит, что на файрбёрде отладиться всё равно нельзя.

Гоните его в шею

Пишете два триггера

CREATE OR ALTER TRIGGER TR_LOG_BEF FOR MY_TABLE
ACTIVE BEFORE INSERT OR UPDATE OR DELETE POSITION 10000
AS
  DECLARE VASRIABLE var_action VARCHAR(16);
BEGIN
  var_action = CASE
    WHEN INSERTING THEN 'INSERTING'
    WHEN UPDATING THEN 'UPDATING'
    WHEN DELETING THEN 'DELETING'
  END;
  IN AUTONOMOUS TRANSACTION DO
    INSERT INTO logs (
      "ID",
      "CON",
      "TRN",
      "TIMESTAMP",
      "MESSAGE"
    ) VALUES (\
      GEN_ID(gen_log, 1),
      CURRENT_CONNECTION,
      CURRENT_TRANSACTION,
      'NOW',
      'Before ' || :var_action
    );
END

CREATE OR ALTER TRIGGER TR_LOG_AFT FOR MY_TABLE
ACTIVE AFTER INSERT OR UPDATE OR DELETE POSITION 0
AS
  DECLARE VASRIABLE var_action VARCHAR(16);
BEGIN
  var_action = CASE
    WHEN INSERTING THEN 'INSERTING'
    WHEN UPDATING THEN 'UPDATING'
    WHEN DELETING THEN 'DELETING'
  END;
  IN AUTONOMOUS TRANSACTION DO
    INSERT INTO logs (
      "ID",
      "CON",
      "TRN",
      "TIMESTAMP",
      "MESSAGE"
    ) VALUES (\
      GEN_ID(gen_log, 1),
      CURRENT_CONNECTION,
      CURRENT_TRANSACTION,
      'NOW',  -- !! нельзя CURRENT_TIMESTAMP !!
      'After ' || :var_action
    );
END

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

Ну и каждый поток должен иметь свой коннект к базе

→ Ссылка