Проблем с производителността на Postgresql с HikariCP

Опитвам се да заредя големи данни в една таблица в PostgreSQL сървър (общо 40 милиона реда) на малки партиди (6000 реда във всеки csv). Мислех, че HikariCP ще бъде идеален за тази цел.

Това е пропускателната способност, която получавам от моето вмъкване на данни с помощта на Java 8 (1.8.0_65), Postgres JDBC драйвер 9.4.1211 и HikariCP 2.4.3.

6000 реда за 4 минути и 42 секунди.

Какво правя грешно и как мога да подобря скоростта на вмъкване?

Още няколко думи за моята настройка:

  • Програмата работи в моя лаптоп зад корпоративната мрежа.
  • Postgres сървър 9.4 е Amazon RDS с db.m4.large и 50 GB SSD.
  • Все още няма дефиниран изричен индекс или първичен ключ, създаден в таблицата.
  • Програмата вмъква всеки ред асинхронно с голям набор от нишки, за да държи заявки, както е показано по-долу:

    private static ExecutorService executorService = new ThreadPoolExecutor(5, 1000, 30L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100000));
    

Конфигурацията на DataSource е:

        private DataSource getDataSource() {
                if (datasource == null) {
                    LOG.info("Establishing dataSource");
                    HikariConfig config = new HikariConfig();
                    config.setJdbcUrl(url);
                    config.setUsername(userName);
                    config.setPassword(password);
                    config.setMaximumPoolSize(600);// M4.large 648 connections tops
                    config.setAutoCommit(true); //I tried autoCommit=false and manually committed every 1000 rows but it only increased 2 minute and half for 6000 rows
                    config.addDataSourceProperty("dataSourceClassName","org.postgresql.ds.PGSimpleDataSource");
                    config.addDataSourceProperty("dataSource.logWriter", new PrintWriter(System.out));
                    config.addDataSourceProperty("cachePrepStmts", "true");
                    config.addDataSourceProperty("prepStmtCacheSize", "1000");
                    config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
                    config.setConnectionTimeout(1000);

                    datasource = new HikariDataSource(config);
                }
                return datasource;
            }

Тук чета изходни данни:

    private void readMetadata(String inputMetadata, String source) {
            BufferedReader br = null;
            FileReader fr = null;
            try {
                br = new BufferedReader(new FileReader(inputMetadata));
                String sCurrentLine = br.readLine();// skip header;
                if (!sCurrentLine.startsWith("xxx") && !sCurrentLine.startsWith("yyy")) {
                    callAsyncInsert(sCurrentLine, source);
                }
                while ((sCurrentLine = br.readLine()) != null) {
                    callAsyncInsert(sCurrentLine, source);
                }
            } catch (IOException e) {
                LOG.error(ExceptionUtils.getStackTrace(e));
            } finally {
                try {
                    if (br != null)
                        br.close();

                    if (fr != null)
                        fr.close();

                } catch (IOException ex) {
                    LOG.error(ExceptionUtils.getStackTrace(ex));
                }
            }
    }

Вмъквам данни асинхронно (или се опитвам с jdbc!):

            private void callAsyncInsert(final String line, String source) {
                    Future<?> future = executorService.submit(new Runnable() {
                        public void run() {
                            try {
                                dataLoader.insertRow(line, source);
                            } catch (SQLException e) {
                                LOG.error(ExceptionUtils.getStackTrace(e));
                                try {
                                    errorBufferedWriter.write(line);
                                    errorBufferedWriter.newLine();
                                    errorBufferedWriter.flush();
                                } catch (IOException e1) {
                                    LOG.error(ExceptionUtils.getStackTrace(e1));
                                }
                            }
                        }
                    });
                    try {
                        if (future.get() != null) {
                            LOG.info("$$$$$$$$" + future.get().getClass().getName());
                        }
                    } catch (InterruptedException e) {
                        LOG.error(ExceptionUtils.getStackTrace(e));
                    } catch (ExecutionException e) {
                        LOG.error(ExceptionUtils.getStackTrace(e));
                    }
                }

Моят DataLoader.insertRow е по-долу:

            public void insertRow(String row, String source) throws SQLException {
                    String[] splits = getRowStrings(row);
                    Connection conn = null;
                    PreparedStatement preparedStatement = null;
                    try {
                        if (splits.length == 15) {
                            String ... = splits[0];
                            //blah blah blah

                            String insertTableSQL = "insert into xyz(...) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ";
                            conn = getConnection();
                            preparedStatement = conn.prepareStatement(insertTableSQL);
                            preparedStatement.setString(1, column1);
                            //blah blah blah
                            preparedStatement.executeUpdate();
                            counter.incrementAndGet();
                            //if (counter.get() % 1000 == 0) {
                                //conn.commit();
                            //}
                        } else {
                            LOG.error("Invalid row:" + row);
                        }
                    } finally {
                        /*if (conn != null) {
                            conn.close();   //Do preparedStatement.close(); rather connection.close
                        }*/
                        if (preparedStatement != null) {
                            preparedStatement.close();
                        }
                    }
                }

При наблюдение в pgAdmin4 забелязах няколко неща:

  • най-големият брой транзакции в секунда беше близо 50.
  • Активната сесия на базата данни беше само една, общият брой на сесиите беше 15.
  • Твърде много блокови I/O (постигане на около 500, не съм сигурен дали това трябва да е проблем)

екранна снимка от pgAdmin


person bkrish    schedule 03.03.2017    source източник
comment
Намалете размера на вашия пул от връзки и броя на използваните нишки: повече връзки (и повече нишки) не водят непременно до по-добра производителност, дори има точка (която вероятно е много под текущите ви настройки), където повече връзки (и нишки) всъщност ще доведе до намаляване на производителността и пропускателната способност. Освен това трябва да затворите връзката във вашия метод, който я връща в пула за връзки за повторно използване.   -  person Mark Rotteveel    schedule 03.03.2017
comment
Също така, всъщност проверихте ли дали проблемът е с асинхронното вмъкване, може би проблемът е в кода, който не показвате (който извиква callAsyncInsert).   -  person Mark Rotteveel    schedule 03.03.2017
comment
Благодаря ви за отговора:   -  person bkrish    schedule 04.03.2017
comment
Намален е както наборът от връзки, така и броят на нишките до 10. Освен това връзката е затворена след вмъкване (което затваря обекта ConnectionProxy). Няма много тежка работа, когато извиквам callAsyncInsert, просто прочетете csv и го предайте на callAsyncInsert. След извършване на тези промени, все още е след 4 минути 42 секунди. някакви мисли?   -  person bkrish    schedule 04.03.2017
comment
Може да опитате да не го правите async и вместо това да използвате пакетно вмъкване? Показване на кода, който извиква callAsyncInsert? Също така разберете, че свързването от вашия лаптоп към база данни, хоствана на AWS, може да има доста малко забавяне. Тествали ли сте това спрямо локална база данни?   -  person Mark Rotteveel    schedule 04.03.2017
comment
Актуализирах въпрос с кода, който извиква callAsyncInsert. Ще опитам с batchInsert без async. Не мога да инсталирам локална база данни, но крайната ми цел е да изпълня този код от EC2.   -  person bkrish    schedule 04.03.2017


Отговори (1)


Абсолютно искате да използвате пакетни вмъквания, като операторът се подготвя извън на цикъла и автоматичното ангажиране е изключено. В псевдокод:

PreparedStatement stmt = conn.prepareStatement("insert into xyz(...) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")
while ( <data> ) {
   stmt.setString(1, column1);
   //blah blah blah
   stmt.addBatch();
}
stmt.executeBatch();
conn.commit();

Дори една нишка на една връзка трябва да може да вмъква > 5000 реда/сек.

АКТУАЛИЗАЦИЯ: Ако искате да го направите с няколко нишки, броят на връзките трябва да бъде броят на процесорните ядра на базата данни x1,5 или 2. Броят на нишките за обработка трябва да съответства на това и всяка нишка за обработка трябва да обработва един CSV файл, използвайки шаблона по-горе. Въпреки това може да откриете, че много едновременни вмъквания в една и съща таблица създават твърде много конкуренция за заключване в DB, ​​в който случай трябва да намалите броя на нишките за обработка, докато намерите оптималната едновременност.

Правилно оразмерен пул и паралелност трябва лесно да бъдат по-високи, за да се постигнат >20K редове/сек.

Също така, моля, надстройте до HikariCP v2.6.0.

person brettw    schedule 04.03.2017
comment
Броят на нишките за многопоточно импортиране не се определя само от броя на процесорите на сървъра, но и от броя на твърдите дискове на този сървър. - person a_horse_with_no_name; 04.03.2017
comment
@a_horse_with_no_name Въпреки че е вярно, с Amazon RDS няма начин да разберете този номер. - person brettw; 04.03.2017
comment
Добре. Промених програмата по предложение. Надстроен до 2.6.0. Добавено пакетно вмъкване и използвана само връзка за зареждане на данни. Сега виждам голяма разлика с 2 различни типа набор от данни. Набор от данни #1 е 500K реда в един csv файл (499951, за да бъдем точни) - 00:02:08,670 минути. Набор от данни #2 е 498K в 83 CSV файла, като всеки 6K реда отнема 00:02:09,674 минути. Така че мога да получа пропускателна способност 3840ish/sec. Може да получа повече, ако нямам тежка рамка за регистриране на макроси, обработка на грешки и т.н. Но съм доволен от това. Благодаря ви много, г-н Уулридж за тази рамка и Марк за помощта. - person bkrish; 06.03.2017