以极高的速度获取行

Posted

技术标签:

【中文标题】以极高的速度获取行【英文标题】:Fetching rows at extremely high speed 【发布时间】:2014-08-16 00:11:58 【问题描述】:

我在 Oracle 中有一个非常大的表(数亿行,包含数字和字符串),我需要读取该表的所有内容,对其进行格式化并写入文件或任何其他资源。 通常我的解决方案是这样的:

package my.odp;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.lang.Throwable;
import java.sql.*;


public class Main 
public static volatile boolean finished = false;

public static void main(final String[] args) throws InterruptedException 
    final ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(10000);
    final Thread writeWorker = new Thread("ODP Writer") 
        public void run() 
            try 
                File targetFile = new File(args[0]);
                FileWriter fileWriter = new FileWriter(targetFile);
                BufferedWriter writer = new BufferedWriter(fileWriter);
                String str;
                try 
                    while (!finished) 
                        str = queue.poll(200, TimeUnit.MILLISECONDS);
                        if (str == null) 
                            Thread.sleep(50);
                            continue;
                        
                        writer.write(str);
                        writer.write('\n');
                    
                 catch (InterruptedException e) 
                    writer.close();
                    return;
                
            
            catch (Throwable e) 
                e.printStackTrace();
                return;
            
        
    ;

    final Thread readerThread = new Thread("ODP Reader") 
        public void run() 
            try 
                Class.forName("oracle.jdbc.OracleDriver");
                Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@//xxx.xxx.xxx.xxx:1521/orcl", "user", "pass");

                Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
                stmt.setFetchSize(500000);
                ResultSet rs = stmt.executeQuery("select * from src_schema.big_table_view");
                System.out.println("Fetching result");
                while (rs.next()) 
                    StringBuilder sb = new StringBuilder();
                    sb.append(rs.getString(1)).append('\t');//OWNER
                    sb.append(rs.getString(2)).append('\t');//OBJECT_NAME
                    sb.append(rs.getString(3)).append('\t');//SUBOBJECT_NAME
                    sb.append(rs.getLong(4)).append('\t');//OBJECT_ID
                    sb.append(rs.getLong(5)).append('\t');//DATA_OBJECT_ID
                    sb.append(rs.getString(6)).append('\t');//OBJECT_TYPE
                    sb.append(rs.getString(7)).append('\t');//CREATED
                    sb.append(rs.getString(8)).append('\t');//LAST_DDL_TIME
                    sb.append(rs.getString(9)).append('\t');//TIMESTAMP
                    sb.append(rs.getString(10)).append('\t');//STATUS
                    sb.append(rs.getString(11)).append('\t');//TEMPORARY
                    sb.append(rs.getString(12)).append('\t');//GENERATED
                    sb.append(rs.getString(13)).append('\t');//SECONDARY
                    sb.append(rs.getString(14)).append('\t');//NAMESPACE
                    sb.append(rs.getString(15));//EDITION_NAME
                    queue.put(sb.toString());
                

                rs.close();
                stmt.close();
                conn.close();
                finished = true;
             catch (Throwable e) 
                e.printStackTrace();
                return;
            
        
    ;
    long startTime = System.currentTimeMillis();
    writeWorker.start();
    readerThread.start();
    System.out.println("Waiting for join..");
    writeWorker.join();
    System.out.println("Exit:"+ (System.currentTimeMillis() - startTime));

有两个线程:一个用于从结果集中获取行,一个用于写入字符串值。测得的加载速度约为 10Mb/s,在我的情况下,我需要让它快 10 倍。 Profiler 显示最耗时的方法是

oracle.jdbc.driver.OracleResultSetImpl.getString()

oracle.net.ns.Packet.receive()

你知道如何让 jdbc 更快地加载数据吗? 任何关于查询优化、字符串加载优化、调整 JDBC 驱动程序或使用另一个、直接使用 oracle JDBC 实现、调整 Oracle 的想法都值得赞赏。

更新: 我整理并列出了讨论结果如下:

    我无法访问 DBMS 服务器,但连接到 Oracle db 并且服务器无法连接到任何外部资源。无法应用任何使用服务器或远程文件系统的转储和提取工具,也无法在服务器上安装和使用任何外部 java 或 PL/SQL 例程。仅用于执行查询的连接 - 仅此而已。

    我使用了分析器并在 Oracle JDBC 驱动程序中进行了挖掘。我发现最昂贵的操作是读取数据,即 Socket.read()。所有字符串字段都表示为一个 char 数组,对性能几乎没有影响。一般来说,我用分析器检查了整个应用程序,而 Socket.read() 绝对是最昂贵的操作。提取字段、构建字符串、写入数据几乎不消耗任何东西。问题只在于读取数据。

    服务器端数据表示的任何优化都不会产生实际效果。连接字符串和转换时间戳对性能没有任何影响。

    应用程序被重写为具有多个读取器线程,这些线程将准备好的数据放入写入器队列。每个线程都有自己的连接,没有使用池,因为它们会减慢提取速度(我使用了 oracle 推荐的 UCP 池,它消耗了大约 10% 的执行时间,所以我放弃了)。结果集 fetchSize 也增加了,因为从默认值 (10) 切换到 50000 会带来高达 50% 的性能增长。

    我测试了多线程版本如何与 4 个阅读线程一起工作,发现增加阅读器数量只会减慢提取速度。 我尝试启动 2 个实例,其中每个实例都有两个读取器,并且都与单个实例同时工作,即双数据提取需要与单实例相同的时间。不知道为什么会发生这种情况,但看起来 oracle 驱动程序有一些性能限制。具有 4 个独立连接的应用程序运行速度比具有 2 个连接的 2 个应用程序实例慢。 (Profiler 用于确保驱动程序的 Socket.read() 仍然是主要问题,所有其他部分在多线程模式下工作正常)。

    我尝试使用 SAS 获取所有数据,它执行相同的提取操作的速度比 JDBC 快 2 倍,两者都使用到 Oracle 的单一连接并且不能使用任何转储操作。 Oracle 确保 JDBC 瘦驱动程序与原生驱动程序一样快..

也许 Oracle 有其他方法可以通过 ODBC 或其他方式快速提取到远程主机?

【问题讨论】:

RDBMS 不是处理数亿行的正确工具,更不用说像网络套接字或文件这样的单线程资源 , 你能指望什么?在 I/O 受限资源的情况下,更多线程 == 更慢而不是更快。 Map Reduce 和 NoSQL 数据库的创建是有原因的。 有比使用 Java 更好的方法从 Oracle 获取所有数据。检查 Oracle 工具集或询问您的 DBA。他们可以将数据爆破到文件中。然后你可以把它复制到需要去的地方。 这是在乞求 ETL 过程。 以 200 毫秒超时然后休眠 50 毫秒的轮询到底有什么意义?睡眠部分实际上是在浪费时间:根据定义,您已经在 poll() 方法中花费了 200 毫秒。所以你可以在那里提高你的速度。指定这么短的poll() 超时也没有多大意义。一两秒钟就可以了。 你已经有了 setFetchSize ,这通常是缺少的东西。还有一些其他的事情要尝试,首先你能改变查询,让它进行连接,这样当你取回数据时,你只需要做 1 个 getString 吗?第二,这个过程是否可以在数据库服务器上运行,然后使用BEQUEATH连接,即取出网元?您是否对表进行全表扫描(可能是),那么为什么不添加 /*+ PARALLEL */ 提示(程度取决于当时的备用 CPU)。 【参考方案1】:

假设您已经检查了接口、防火墙、代理等基本网络内容,以及数据库服务器的硬件元素。

选项 1:

而不是:

Class.forName("oracle.jdbc.OracleDriver");
Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@//xxx.xxx.xxx.xxx:1521/orcl", "user", "pass");

尝试使用:

OracleDataSource ods = new OracleDataSource();
java.util.Properties prop = new java.util.Properties();
prop.setProperty("MinLimit", "2");
prop.setProperty("MaxLimit", "10");
String url = "jdbc:oracle:oci8:@//xxx.xxx.xxx.xxx:1521/orcl";
ods.setURL(url);
ods.setUser("USER");
ods.setPassword("PWD");
ods.setConnectionCachingEnabled(true);
ods.setConnectionCacheProperties (prop);
ods.setConnectionCacheName("ImplicitCache01");

更多详情here

选项 2:Fetchsize

正如 Stephen 强烈指出的那样,fetchsize 似乎太大了。

而且,对于 500,000 的获取大小,您的 -Xms 和 -Xmx 是多少。另外,在分析器中,最大的堆大小是多少?

选项 3:数据库

检查src_schema.big_table_view的索引和查询计划

这是一个工具还是一个应用系统。如果只是一个工具,你可以 基于数据库系统添加并行度、索引提示、分区等 能力

选项 4:线程

n

您可以启动 n 编写器的线程,每个线程都配置为处理某个存储桶,例如thread1 处理 0 到 10000,写入 n 不同的文件,一旦所有线程完成后,加入后,最好使用低级 OS 命令将文件合并在一起。

也就是说,这一切都不应该是像现在这样的预定义代码。 'n' 并且桶应该在运行时计算。并且创建的线程数量超过您的系统支持的数量只会搞砸。

选项 5:

代替

select * from src_schema.big_table_view

你可以使用

SELECT column1||CHR(9)||column2||CHR(9).....||columnN FROM src_schema.big_table_view

这避免了创建 500000 StringBuildersStrings。 (假设不涉及其他复杂的格式)。 CHR(9) 是制表符。

选项 6:

同时,您还可以与您的 DBA 核实任何数据库系统问题,并通过Oracle support 提出 SR。

【讨论】:

感谢您的回复。我已经尝试了一些这样的优化: 选项 1:我尝试按照您的建议以相同的方式连接字符串,但它并没有提供任何性能改进。实际上,这个实现中最慢的部分是JDBC驱动,最长的操作是Socket read() 我也做了一些调查并挖掘了驱动程序的实现。它不使用任何 StringBuilders 来创建字符串,所有 varchar 数据在内部都表示为单个 char 数组,并且不会花费太多时间来获取。在读取字段中使用 StringBuidler 需要的工作时间太少(分析时间几乎没有)。关于选项 3 - 写入线程几乎一直在等待,因为 JDBC 读取器生成数据太慢。实际上问题在于 JDBC 驱动程序的读取速度。 JDBC阅读器被改写为多线程(每个线程提取自己的部分数据并写入队列) 我尝试同时提取多个部分的数据,但我使用的阅读器越多,我的性能结果就越差。我尝试启动 2 个实例,其中每个实例都有两个读取器......并且都与单个实例同时工作,即双提取需要与单实例相同的时间。不知道为什么会发生这种情况,但看起来 oracle 驱动程序有一些性能限制。 具有 4 个独立连接的应用程序运行速度比具有 2 个连接的 2 个应用程序实例慢。 我在有 2 个和 4 个线程的情况下使用分析器检查了代码,并且我的代码中没有任何东西消耗太多 cpu 时间。主要消费者始终是 JDBC 驱动程序中的 Socket.read() ......不知道为什么它会这样 我尝试使用 SAS 获取所有数据,它执行提取的速度比 JDBC 快 2 倍,两者都使用与 Oracle 的单一连接,并且不能使用任何转储操作。 Oracle 确保 JDBC 瘦驱动程序与本地驱动程序一样快。也许 Oracle 有另一种方法可以通过 ODBC 或其他方式快速提取到远程主机?【参考方案2】:

您似乎已经找到并调整了行预取参数。但是,根据 Oracle 文档:

“没有最大预取设置,但经验证据表明 10 是有效的。Oracle 从未观察到将预取设置为高于 50 的性能优势。如果您没有为连接,则默认为 10。"

您将其设置为 500000。按照 Oracle 的建议,尝试将其回绕到 50 左右。 (为什么?嗯,可能是一个巨大的预取大小导致服务器或客户端使用过多的内存来缓冲预取的数据。这可能会对其他事情产生“连锁反应” ,导致吞吐量降低。)

参考资料(来自 Oracle 10g 文档):

Oracle Row-Prefetching Limitations

您可以通过在多个 Java 线程(例如,在表的单独“部分”上)同时运行查询,将每个结果集写入单独的流/文件来获得更大的吞吐量。但是你有将输出流/文件拼接在一起的问题。 (而您是否确实获得整体改进将取决于客户端和服务器端内核的数量、网络和 NIC 容量以及磁盘 I/O 容量。)

除此之外,我想不出任何方法可以在 Java 中更快地做到这一点。但是您可以尝试 PL/SQL 或更低级别的东西。 (我不是 Oracle 专家。请咨询您的 DBA。)

在 Java 中将速度提高 10 倍是……雄心勃勃的。

【讨论】:

我对发布答案持怀疑态度。看来他真的需要在java中做到这一点。在同一个循环上同时进行读取和写入怎么样?他可以只使用 JDBC 缓冲区,而不是将其复制到列表中的内存中。 @ra2085 - 根据 OP,瓶颈(如分析器所示)在于从 Oracle 读取数据包并将其转换为 Java 字符串。他不是在创建一个列表。他将字符串(要写入的行)放入队列中。这应该具有允许数据库读取和文件写入同时发生的效果......而不是强制它们交错。 (它是否真的有效很难预测,但我认为 OP 对他的分析结果的解释是它是有效的。) 我的案例表明,将 FetchSize 值设置为 50000 最多可以提高 50% 的性能,我认为默认值适用于较小的结果集和频繁的请求【参考方案3】:

您的分析有缺陷

您列出的方法很可能已经高度优化。我分析了在 Oracle JDBC 代码中调用最多且时间最长的系统,因为整个系统都使用了PreparedStatement,并且它调用了该方法很多!。不用说,这在我们的案例中是一个红鲱鱼。

分析您的网络流量:

如果您的连接已饱和,那是您的瓶颈,而不是您列出的代码。

如果必须是 Oracle 作为数据源,则需要在服务器端完成。您永远不会通过网络连接提取数亿条记录,然后再以现在获得的速度的 10 倍返回,除非您在两个端点中都有 10 倍的网卡并将它们全部绑定。即使那样我也怀疑你会得到 10 倍的吞吐量

如果您确实仅限于 Java 和 Oracle,那么您获得比现在更多的吞吐量的唯一方法是在服务器上 run the Java as a stored procedure 生成您需要的文件,然后从远程检索它们系统。

我已经构建了可以在几分钟内处理数百万笔交易的系统,这种吞吐量不会发生在单个网络连接上,它发生在具有多个网络接口的机器网格上,专用发送/接收交换机上的专用子网与数据中心的其余流量隔离。

还有

您的线程代码充其量是幼稚的。您永远不应该手动创建和管理线程。 ExecutorService已经有10年了,用吧! ExecutorCompletionService 是您在这种情况下要使用的,actually in almost all cases。

如果可以使用 Guava,ListenableFuture 是更好的选择。

【讨论】:

我无法使用任何本机转储工具,因为我无权访问服务器文件系统。你知道有什么方法可以加快使用 oracle jdbc 驱动程序获取字符串的速度吗?

以上是关于以极高的速度获取行的主要内容,如果未能解决你的问题,请参考以下文章

TVS管相关知识

由于查询速度慢而导致 CPU 使用率极高的 Wordpress 网站

高性能算力中心 — InfiniBand — Overview

浅析电阻与TVS管的不同之处

TVS的参数&与稳压管异同对比

揭秘TVS管电压和电流中的奥秘