Гость
Форумы / WinForms, .Net Framework [игнор отключен] [закрыт для гостей] / Oracle.DataAccess.Client ест память класс Pooler / 14 сообщений из 14, страница 1 из 1
10.01.2020, 15:09
    #39912454
VSVLAD
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
Доброго всем!

Есть приложение. Один из шагов, это раздробить огромный файл CSV и залить в оракловую таблицу. Делать нужно максимально быстро, поэтому читаю файл в несколько потоков (каждый поток читает свою часть строк с N до M).
Формирую через StringBuilder по 1000 строк такие простыни:
Код: plsql
1.
2.
3.
4.
5.
6.
begin
   insert into t1(a, b, c, d) values(1, 'data data', 'text', to_date('2020-01-10 12:00:00','yyyy-mm-dd hh24:mi:ss'));
   insert into t1(a, b, c, d) values(2, 'data data', 'text', to_date('2020-01-10 12:00:00','yyyy-mm-dd hh24:mi:ss'));
   insert into t1(a, b, c, d) values(3, 'data data', 'text', null);
   insert into t1(a, b, c, d) values(4, 'data data', null, null);
end;


и далее скармливаю методу-обёртке с доп. логикой. Для простоты перепишем код в таком виде:
Код: vbnet
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
    Public Shared Sub Exec(ByVal SQL As String)
        Using conn As New Oracle.DataAccess.Client.OracleConnection(oraString)
            conn.Open()
            
            Using cmd As Data.IDbCommand = conn.CreateCommand()
                cmd.CommandText = SQL
                cmd.ExecuteNonQuery()
            End Using
        End Using
    End Sub



Всё красиво работает и замечательно... но плохо то, что приложение сжирает 500 мб памяти сейчас и она не освобождается. Сборка GC не помогает, значит ссылка где-то есть. При том, как видим в коде всё вычищается, объекты диспозятся.

Далее, смотрим в профайлере куда это девается. Оказывается есть внутренний класс типа Oracle.DataAccess.Client.Pooler, который инициализируется для переменной Oracle.DataAccess.Client.UTF8CommandText.m_pooler так:
Код: c#
1.
internal static Pooler m_pooler = new Pooler(10, 200);



Он хранит до 200 последних текстов команд, они сортируются по частоте использования и ещё какая-то логика (подсмотрел через IL Spy). Похоже из-за static она внутри методов не чистится и не предполагалось, что в неё начнут пихать столько много запросов.

Может быть кто имел дело с этой библиотекой и возникала похожая проблема? Либо есть методы для чистки пула комманд (не путать с пуллом коннектов)

Переписывать параметрические запросы в данном случае не подойдёт. Т.к. это очень медленно. Либо у меня не завелось. Либо как-то сказать драйверу ODAC чтобы он открыл транзакцию и когда выполняю .ExecuteNonQuery() фактически нужно добавить в текущую транзакцию. Обрамлять в begin insert into t .... end; ещё не пробовал. Но не выполняя запрос в анонимном блоке, а прямо .CommandText = "insert into t ...." с подготовленными параметрами, всё равно работает медленно.
...
Рейтинг: 0 / 0
10.01.2020, 16:29
    #39912518
carrotik
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
VSVLAD,

.. так а это .. sql*loader не пробовали? .. вроде он под это заточен ..

Oracle Bulk Import
...
Рейтинг: 0 / 0
10.01.2020, 16:30
    #39912519
VSVLAD
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
carrotik,

не хотелось бы внешние утилиты тащить
...
Рейтинг: 0 / 0
10.01.2020, 17:32
    #39912555
hVostt
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
VSVLAD
carrotik,

не хотелось бы внешние утилиты тащить


https://www.c-sharpcorner.com/article/two-ways-to-insert-bulk-data-into-oracle-database-using-c-sharp/
...
Рейтинг: 0 / 0
10.01.2020, 17:33
    #39912556
hVostt
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
VSVLAD,

у вас совершенно отвратительный способ для вставки огромного массива данных
...
Рейтинг: 0 / 0
10.01.2020, 18:17
    #39912573
buser
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
+1 - hVostt
Но феерия начинается чуть раньше "поэтому читаю файл в несколько потоков (каждый поток читает свою часть строк с N до M)"
...
Рейтинг: 0 / 0
10.01.2020, 18:46
    #39912583
VSVLAD
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
buser,

если критикуешь, предлагай решение. Рабочее решение, с определёнными условиями
...
Рейтинг: 0 / 0
10.01.2020, 19:02
    #39912593
Roman Mejtes
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
VSVLAD
buser,

если критикуешь, предлагай решение. Рабочее решение, с определёнными условиями

операция чтения из файла тут наименее затратная, можно просто почитать N строк, разбить и выполняться параллельно, а так еще не известно, + если длинная строки не фиксированная, нужно будет найти заданную строку, чтоб начать чтение с неё.
Ну и как по мне, всё это имеет смысл только тогда, когда вставка идет параллельно в разные таблицы, тогда скорость возрастёт сильно.
...
Рейтинг: 0 / 0
10.01.2020, 20:00
    #39912618
VSVLAD
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
Roman Mejtes,

Практически так и работает. Сначала считаем количество строк в файле. Эта операция на ~ 2.5 млн строк занимает где-то 1 секунду. Далее логически файл делим на сегменты и читает каждый таск (поток) свою часть строк, создаёт из них наборы инсертов и выполняет. Замерял, в многопоточном режиме, этот способ эффективнее, чем допустим читать файл в Parallel.ForEach или в одном потоке работать. Проблемы в скорости сейчас нет, заливка занимает около 15 минут в среднем. sqloader также чуточку быстрее работает, но не прям махом.

А проблема в том, что память odac съел. Приложение запускается шедулером и работает примерно 3-4 часа, включая выполнение других шагов. На самом деле приложение это не совсем работает как типичное приложение. Это самописный продвинутый "блокнот" который читает файл скрипта на VB.NET, компилирует в сборку в памяти и исполняет "пользовательский" код. Сделано затем, чтобы обычные пользователи не программисты могли запускать свои скрипты, которые имеют продвинутый доступ к базам, сети, ftp через классы-обертки выполняющие задачу в 1 строку. Конкретно в этой задаче потребовалось грузить csv в базу, такова задача, помимо чтение его с sftp и далее после обработки, через soap выполняются определенные методы. Возможно т.к. библиотека в домене приложения и статик переменная не подчищена, то ссылка на неё всё ещё жива, пока приложение запущено.
...
Рейтинг: 0 / 0
11.01.2020, 11:22
    #39912733
Сон Веры Павловны
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
VSVLAD,

VSVLAD
Доброго всем!

Есть приложение. Один из шагов, это раздробить огромный файл CSV и залить в оракловую таблицу. Делать нужно максимально быстро, поэтому читаю файл в несколько потоков (каждый поток читает свою часть строк с N до M).
Формирую через StringBuilder по 1000 строк такие простыни:
Код: plsql
1.
2.
3.
4.
5.
6.
begin
   insert into t1(a, b, c, d) values(1, 'data data', 'text', to_date('2020-01-10 12:00:00','yyyy-mm-dd hh24:mi:ss'));
   insert into t1(a, b, c, d) values(2, 'data data', 'text', to_date('2020-01-10 12:00:00','yyyy-mm-dd hh24:mi:ss'));
   insert into t1(a, b, c, d) values(3, 'data data', 'text', null);
   insert into t1(a, b, c, d) values(4, 'data data', null, null);
end;


Раз уж решили сами свой загрузчик писать, то будьте в курсе поговорки "row by row is slow by slow". В данном случае надо заполнять блоками фиксированного размера пакетированный экземпляр коллекции, а по достижении этого размера уже делать bulk-вставку в целевую таблицу через forall..insert. Фиксированного размера - чтобы не забить весь доступный UGA (или, еще хуже, выбрать свой лимит SGA, если пакет с прагмой SERIALLY_REUSABLE).
Как-то так:
Код: plsql
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
create table TEST (
  id number not null,
  td date not null,
  name varchar2(1000) not null
);
/
create or replace package pk_bulk_insert
as
  type
    t_bulk_rec is record (
      id number,
      td date,
      name varchar2(1000)
    );
  type
    t_bulk_data is table of t_bulk_rec;
  procedure Init;
  procedure Add(id number, td date, name varchar2);
  procedure Commit;
end pk_bulk_insert;
/
create or replace package body pk_bulk_insert
as
  MAX_BUFFER_SIZE constant binary_integer:=100;
  empty_data t_bulk_data:=t_bulk_data();
  bulk_data t_bulk_data;

  procedure Init
  is
  begin
    bulk_data:=empty_data;
  end;

  procedure InsertData
  is
  begin
    forall idx in bulk_data.first..bulk_data.last
      insert into TEST (id, td, name)
      values (
        bulk_data(idx).id,
        bulk_data(idx).td,
        bulk_data(idx).name
      );
    bulk_data:=empty_data;
  end;

  procedure Add(id number, td date, name varchar2)
  is
  begin
    if bulk_data.count>=MAX_BUFFER_SIZE then
      InsertData;
    end if;
    bulk_data.extend;
    bulk_data(bulk_data.count).id:=id;
    bulk_data(bulk_data.count).td:=td;
    bulk_data(bulk_data.count).name:=name;
  end;

  procedure Commit
  is
  begin
    if bulk_data.count>0 then
      InsertData;
    end if;
    commit;
  end;
end pk_bulk_insert;
/

Небольшой бенчмарк:
Код: c#
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
public class Program
{
  public static void Main(string[] args)
    => BenchmarkRunner.Run<Program>();

  string _bulkSql;
  string _directSql;
  [GlobalSetup]
  public void CreateScipts()
  {
    var bulkBuilder = new StringBuilder();
    var directBuilder = new StringBuilder();
    bulkBuilder.AppendLine("begin").AppendLine("execute immediate 'truncate table TEST';").AppendLine("pk_bulk_insert.Init;");
    directBuilder.AppendLine("begin").AppendLine("execute immediate 'truncate table TEST';");
    for(var i=0;i<1000;i++)
    {
      bulkBuilder.AppendLine($"pk_bulk_insert.Add({i}, sysdate+{i}, '{(char)(i%26+65)}');");
      directBuilder.AppendLine($"insert into TEST (id, td, name) values ({i}, sysdate+{i}, '{(char) (i % 26 + 65)}');");
    }
    bulkBuilder.AppendLine("pk_bulk_insert.Commit;").AppendLine("end;");
    directBuilder.AppendLine("commit;").AppendLine("end;");
    _bulkSql = bulkBuilder.ToString();
    _directSql = directBuilder.ToString();
  }

  [Benchmark]
  public void DirectInsert()
  {
    using(var cnn = new OracleConnection(<.....>))
    using(var cmd = new OracleCommand(_directSql, cnn))
    {
      cnn.Open();
      cmd.ExecuteNonQuery();
    }
  }

  [Benchmark]
  public void BulkInsert()
  {
    using(var cnn = new OracleConnection(<.....>))
    using(var cmd = new OracleCommand(_bulkSql, cnn))
    {
      cnn.Open();
      cmd.ExecuteNonQuery();
    }
  }
}


Результат:

Код: plaintext
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
// * Summary *

BenchmarkDotNet=v0.12.0, OS=Windows 7 SP1 (6.1.7601.0)
Intel Core i5-3470 CPU 3.20GHz (Ivy Bridge), 1 CPU, 4 logical and 4 physical cores
Frequency=3117998 Hz, Resolution=320.7186 ns, Timer=TSC
  [Host]     : .NET Framework 4.8 (4.8.3928.0), X64 RyuJIT
  DefaultJob : .NET Framework 4.8 (4.8.3928.0), X64 RyuJIT


|       Method |      Mean |     Error |     StdDev |
|------------- |----------:|----------:|-----------:|
| DirectInsert | 947.81 ms | 57.476 ms | 168.568 ms |
|   BulkInsert |  52.54 ms |  2.425 ms |   7.151 ms |

Причина такой картины очень проста: 10 физических обращений к накопителю в случае BulkInsert, и 1000 физических обращений в случае DirectInsert.
...
Рейтинг: 0 / 0
11.01.2020, 11:45
    #39912739
hVostt
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
VSVLAD
А проблема в том, что память odac съел.


Проблема не в этом. Вы сами генерируете конские тексты запросов. Какого, спрашивается?
...
Рейтинг: 0 / 0
11.01.2020, 23:47
    #39912897
VSVLAD
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
hVostt,

спасибо за ссылку, буду на работе, попробую использовать Oracle.DataAccess.Client.OracleBulkCopy
...
Рейтинг: 0 / 0
13.01.2020, 12:31
    #39913164
VSVLAD
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
Сон Веры Павловны, спасибо за пример. Но я пошёл путём попроще, через класс OracleBulkCopy.

Класс и память корретно освобождает и заливает 500К (42 Мб файл) за 5-6 секунд. Самый большой файл у меня 150 Мб пока

Готовое решение на VB, кому интересно в спойлере.
Код: vbnet
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
Public Shared Sub UploadToOracle(ByVal Context As Object)
        Context.Info.Print("Чтение файла-дампа ...")
        Context.Info.Print("Всего строк: " & FileLineCount(Context, RftOptions.ftpLocalFile))
        
        Dim queryDateFormat = "yyyy-MM-dd HH:mm:ss", queryCount As Integer, N As Integer
        Dim dtDump As New DataTable
        
        ' Создаём структуру таблицы в DataTable с типами
        With dtDump.Columns
            .Add("b1")
            .Add("b2")
            .Add("b3")
            .Add("b4")
            .Add("b5")
            .Add("b6")
        End With
        
        With dtDump
            .Columns(0).DataType = GetType(String)
            .Columns(1).DataType = GetType(String)
            .Columns(2).DataType = GetType(Date)
            .Columns(3).DataType = GetType(Integer)
            .Columns(4).DataType = GetType(Date)
            .Columns(5).DataType = GetType(String)
        End With
        
        Try
            ' По каждой строке
            For Each line As String In Context.File.ReadLine(RftOptions.ftpLocalFile)
                N += 1
                
                ' Дробим CSV на запчасти
                Dim fields() As String = line.Split(";")                
                Dim dr As DataRow      = dtDump.NewRow()

                dr("b1")  = CStr(fields(0))
                dr("b2") = CStr(fields(1))
                dr("b3") = If(Not String.IsNullOrEmpty(fields(2)), Date.ParseExact(fields(2).Substring(0, 19), queryDateFormat, Globalization.CultureInfo.InvariantCulture), DBNull.Value)
                dr("b4") = CInt(fields(3))
                dr("b5") = If(Not String.IsNullOrEmpty(fields(4)), Date.ParseExact(fields(4).Substring(0, 19), queryDateFormat, Globalization.CultureInfo.InvariantCulture), DBNull.Value)
                dr("b6") = CStr(fields(5).Trim())
                dtDump.Rows.Add(dr)
                
                ' Каждые 100К строк фиксируем в базе
                queryCount += 1
                If queryCount Mod 100000 = 0 Then
                    Context.Info.Print("Выполняем вставку #" & Thread.CurrentThread.ManagedThreadId & " всего: " & queryCount)
                    
                    ' Реализация паузы
                    Do While Context.Info.IsPaused()
                        Context.Info.Pause(10)
                    Loop
                    
                    ' Реализация отмены
                    If Context.Info.IsStopped() Then Exit For
                    
                    ' Выполняем запрос
                    BulkInsert(oraString, "blabla", dtDump)
                    dtDump.Clear()
                End If
            Next
            
            ' Вставка остатка
            If dtDump.Rows.Count > 0 Then BulkInsert(oraString, "blabla", dtDump)
        
        Catch ex As Exception
            Context.Info.NewError("Вставка выполнилась с ошибками! Текущая пачка запросов: " & queryCount & ". Ошибка: " & Context.Info.LastError)
        End Try
  
        Context.Info.Print("Файл дампа залит в таблицу")
    End Sub

...
Рейтинг: 0 / 0
13.01.2020, 14:47
    #39913237
VSVLAD
Участник
Скрыть профиль Поместить в игнор-лист Сообщения автора в теме
Oracle.DataAccess.Client ест память класс Pooler
+ Сама процедура. Забыл про неё

Код: vbnet
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
    Public Shared Sub BulkInsert(ByVal ConnectionString As String, ByVal TableName As String, DT As DataTable)
        Using conn As New OracleConnection(ConnectionString)
            conn.Open()

            Using bulkCopy As New OracleBulkCopy(conn, OracleBulkCopyOptions.UseInternalTransaction)
                bulkCopy.DestinationTableName = TableName
                bulkCopy.BulkCopyTimeout = 1200
                bulkCopy.WriteToServer(DT)
            End Using
        End Using
    End Sub
...
Рейтинг: 0 / 0
Форумы / WinForms, .Net Framework [игнор отключен] [закрыт для гостей] / Oracle.DataAccess.Client ест память класс Pooler / 14 сообщений из 14, страница 1 из 1
Целевая тема:
Создать новую тему:
Автор:
Найденые пользователи ...
Разблокировать пользователей ...
Читали форум (0):
Пользователи онлайн (0):
x
x
Закрыть


Просмотр
0 / 0
Close
Debug Console [Select Text]