【问题标题】:Postgresql performance issue with HikariCPHikariCP 的 Postgresql 性能问题
【发布时间】:2017-03-03 19:35:51
【问题描述】:

我正在尝试以小批量(每个 csv 中的 6000 行)将大数据加载到 PostgreSQL 服务器中的一个表中(总共 4000 万行)。我认为 HikariCP 非常适合此目的。

这是我从数据插入中获得的吞吐量 Java 8 (1.8.0_65)、Postgres JDBC 驱动程序 9.4.1211 和 HikariCP 2.4.3。

6000 行在 4 分 42 秒内。

我做错了什么,如何提高插入速度?

关于我的设置的更多信息:

  • 程序在公司网络后面的笔记本电脑上运行。
  • Postgres 服务器 9.4 是带有 db.m4.large 和 50 GB SSD 的 Amazon RDS。
  • 尚未在表上创建明确的索引或主键。
  • 程序用大线程池异步插入每一行来保存请求,如下所示:

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

数据源配置为:

        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。
  • 活动数据库会话只有 1 个,会话总数为 15 个。
  • 块 I/O 太多(达到 500 左右,不确定这是否值得关注)

【问题讨论】:

  • 减少连接池的大小和使用的线程数:更多的连接(和更多的线程)并不一定会带来更好的性能,甚至有一点(这可能远低于你当前的设置),其中更多的连接(和线程)实际上会导致性能和吞吐量的降低。此外,您应该在您的方法中关闭连接,将其返回到连接池以供重用。
  • 另外,您是否真的检查过瓶颈是否与异步插入有关,也许问题在于您未显示的代码(调用callAsyncInsert)。
  • 感谢您的回复:
  • 将连接池和线程数减少到 10。此外,插入后关闭连接(关闭 ConnectionProxy 对象)。我调用 callAsyncInsert 并没有太多繁重的工作,只需读取 csv 并将其传递给 callAsyncInsert。进行这些更改后,仍然是 4 分 42 秒。有什么想法吗?
  • 您可以尝试不异步执行,而是使用批量插入?显示调用callAsyncInsert的代码?还要了解从您的笔记本电脑连接到托管在 AWS 上的数据库可能会有相当长的延迟。您是否针对本地数据库对此进行了测试?

标签: postgresql jdbc hikaricp


【解决方案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 行/秒。

更新:如果你想多线程,连接数应该是数据库CPU核心数x1.5或2。处理线程数应该匹配,每个处理线程应该处理一个CSV文件使用上面的模式。但是,您可能会发现同一张表中的许多并发插入会在 DB 中产生过多的锁争用,在这种情况下,您需要回退处理线程的数量,直到找到最佳并发。

适当大小的池和并发应该很容易达到 >20K 行/秒。

另外,请升级到 HikariCP v2.6.0。

【讨论】:

  • 多线程导入的线程数不仅取决于服务器上的CPU数,还取决于该服务器上的硬盘数。
  • @a_horse_with_no_name 虽然是这样,但使用 Amazon RDS 无法知道该数字。
  • 好的。我已根据建议修改了程序。升级到 2.6.0。添加了批量插入并仅使用连接来加载数据。现在我看到两种不同类型的数据集有很大的不同。数据集 #1 是一个 csv 文件中的 500K 行(准确地说是 499951) - 00:02:08.670 分钟。数据集 #2 为 83 CSV 文件中的 498K,每 6K 行耗时 00:02:09.674 分钟。所以我能够获得 3840ish/sec 的吞吐量。如果我没有宏日志记录、错误处理等繁重的框架,我可能会得到更多。但我对此很满意。非常感谢 Woolridge 先生的这个框架和 Mark 的帮助。
猜你喜欢
  • 2010-09-28
  • 2011-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-23
  • 1970-01-01
相关资源
最近更新 更多