Нужны рекомендации по выходу за рамки с SqlBulkCopy на SQL Server

Я разрабатываю приложение, одним из аспектов которого является то, что оно должно принимать огромные объемы данных в базу данных SQL. Я разработал структуру базы данных как одну таблицу с идентификатором bigint, примерно так:

CREATE TABLE MainTable
(
   _id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
    field1, field2, ...
)

Я опущу, как я собираюсь выполнять запросы, поскольку это не имеет отношения к моему вопросу.

Я написал прототип, который вставляет данные в эту таблицу с помощью SqlBulkCopy. Похоже, в лаборатории это сработало очень хорошо. Мне удалось вставить десятки миллионов записей со скоростью ~ 3К записей / сек (полная запись сама по себе довольно большая, ~ 4К). Поскольку единственный индекс в этой таблице - это автоинкремент bigint, я не заметил замедления даже после того, как было отправлено значительное количество строк.

Учитывая, что лабораторный SQL-сервер представлял собой виртуальную машину с относительно слабой конфигурацией (4 Гб ОЗУ, совместно используемая с дисковой системой других виртуальных машин), я ожидал получить значительно лучшую пропускную способность на физической машине, но этого не произошло, или, скажем так, прирост производительности был незначительным. Я мог бы, возможно, получить на 25% более быстрые вставки на физической машине. Даже после того, как я настроил RAID0 с 3 дисками, который работал в 3 раза быстрее, чем один диск (измерено с помощью программного обеспечения для тестирования), я не получил никаких улучшений. По сути: более быстрая подсистема накопителей, выделенный физический процессор и двойная оперативная память почти не привели к увеличению производительности.

Затем я повторил тест, используя самый большой экземпляр в Azure (8 ядер, 16 ГБ), и получил тот же результат. Таким образом, добавление дополнительных ядер не повлияло на скорость вставки.

На данный момент я поигрался со следующими параметрами программного обеспечения без какого-либо значительного увеличения производительности:

  • Изменение параметра SqlBulkInsert.BatchSize
  • Одновременная вставка из нескольких потоков и настройка количества потоков
  • Использование параметра блокировки таблицы в SqlBulkInsert
  • Устранение задержки в сети за счет вставки из локального процесса с использованием драйвера общей памяти

Я пытаюсь увеличить производительность как минимум в 2-3 раза, и моя первоначальная идея заключалась в том, что добавление большего количества оборудования может помочь, но пока этого не происходит.

Итак, может ли кто-нибудь порекомендовать меня:

  • Какой ресурс можно было заподозрить здесь узким местом? Как подтвердить?
  • Есть ли методика, по которой я мог бы попытаться получить надежно масштабируемое улучшение массовой вставки, учитывая, что существует единственная система SQL-сервера?

ОБНОВЛЕНИЕ. Я уверен, что загрузить приложение не проблема. Он создает запись во временной очереди в отдельном потоке, поэтому, когда есть вставка, она выглядит следующим образом (упрощенно):

===>start logging time
int batchCount = (queue.Count - 1) / targetBatchSize + 1;
Enumerable.Range(0, batchCount).AsParallel().
    WithDegreeOfParallelism(MAX_DEGREE_OF_PARALLELISM).ForAll(i =>
{
    var batch = queue.Skip(i * targetBatchSize).Take(targetBatchSize);
    var data = MYRECORDTYPE.MakeDataTable(batch);
    var bcp = GetBulkCopy();
    bcp.WriteToServer(data);
});
====> end loging time

тайминги регистрируются, и часть, которая создает очередь, никогда не занимает значительного фрагмента

ОБНОВЛЕНИЕ 2. Я реализовал сбор, сколько времени занимает каждая операция в этом цикле, и его макет выглядит следующим образом:

  • queue.Skip().Take() - незначительно
  • MakeDataTable(batch) - 10%
  • GetBulkCopy() - незначительно
  • WriteToServer(data) - 90%

UPDATE3 Я разрабатываю для стандартной версии SQL, поэтому я не могу полагаться на разделение, поскольку оно доступно только в версии Enterprise. Но я попробовал вариант схемы разметки:

  • создано 16 файловых групп (от G0 до G15),
  • сделал 16 таблиц только для вставки (от T0 до T15), каждая из которых привязана к своей отдельной группе. Таблицы вообще не имеют индексов, даже не имеют кластеризованного идентификатора int.
  • потоки, которые вставляют данные, будут циклически перебирать все 16 таблиц каждый. Это делает почти гарантией того, что каждая операция массовой вставки использует свою собственную таблицу.

Это позволило улучшить объемную вставку примерно на 20%. Ядра ЦП, интерфейс LAN, ввод-вывод накопителя не были максимизированы и использовались примерно на 25% от максимальной емкости.

ОБНОВЛЕНИЕ4. Думаю, сейчас он настолько хорош, насколько это возможно. Мне удалось довести пластинки до разумной скорости, используя следующие методы:

  • Каждая массовая вставка помещается в отдельную таблицу, а затем результаты объединяются в основную.
  • Таблицы воссоздаются свежими для каждой объемной вставки, используются замки таблиц.
  • Использовал реализацию IDataReader отсюда вместо DataTable.
  • Массовые вставки, сделанные от нескольких клиентов
  • Каждый клиент обращается к SQL через индивидуальную гигабитную VLAN.
  • Побочные процессы, обращающиеся к основной таблице, используют параметр NOLOCK
  • Я проверил sys.dm_os_wait_stats и sys.dm_os_latch_stats, чтобы исключить разногласия

На данный момент мне сложно решить, кому будет засвидетельствован ответ на вопрос. Те из вас, кто не получил «ответа», прошу прощения, это было действительно трудное решение, и я благодарю вас всех.

ОБНОВЛЕНИЕ 5: следующий элемент можно оптимизировать:

  • Использовал реализацию IDataReader отсюда вместо DataTable.

Если вы не запустите свою программу на машине с большим количеством ядер ЦП, она может потребовать некоторого повторного факторинга. Поскольку для генерации методов get / set используется отражение, это становится основной нагрузкой на ЦП. Если производительность является ключевым фактором, она увеличивает производительность, когда вы кодируете IDataReader вручную, так что он компилируется вместо использования отражения


person galets    schedule 23.06.2012    source источник
comment
Возможно, узким местом производительности является способность приложения предоставлять записи в SqlBulkCopy?   -  person ta.speot.is    schedule 23.06.2012
comment
Какие виды использования ресурсов вы наблюдаете во время этого? Без такой информации невозможно произвести эффективную настройку производительности.   -  person RBarryYoung    schedule 23.06.2012
comment
И пока мы занимаемся этим, многопоточность вставки может не помочь, если приложение C # генерирует строки. В какой-то момент GC будет запускать и освобождать память и (потенциально) приостанавливать потоки для этого (в зависимости от версии .NET Framework, которую вы используете, и от того, является ли GC сервером / рабочей станцией).   -  person ta.speot.is    schedule 23.06.2012
comment
@ ta.speot.is & all: у меня есть веские основания полагать, что приложение, используемое для вставки фиктивных строк, не является проблемой (см. обновление).   -  person galets    schedule 23.06.2012
comment
@galets Это Skip операция O(1) или O(n)? И MakeDataTable звучит дорого. WriteToServer может принимать IDataReader, вы можете реализовать легкую IDataReader оболочку над всем, что вы отправляете на сервер. Вы профилировали приложение .NET?   -  person ta.speot.is    schedule 23.06.2012
comment
@ ta.speot.это хорошие замечания. Я посмотрю, изменится ли это, будет ли разница.   -  person galets    schedule 25.06.2012
comment
@ ta.speot.is ОБНОВЛЕНИЕ: я изменил шаблон Skip (). Take () на List.GetRange () без заметных улучшений, так что это не так.   -  person galets    schedule 25.06.2012
comment
@galets GetRange создает мелкую копию диапазона элементов в источнике , так что мы все еще говорим O(n).   -  person ta.speot.is    schedule 25.06.2012
comment
Вы можете запустить пробную версию jetbrains.com/profiler для своего приложения и убедиться, что она не теряя времени до звонка WriteToServer.   -  person ta.speot.is    schedule 25.06.2012
comment
@ ta.speot.is Я провел еще несколько тестов. Оба паттерна GetRange () и Skip (). Take () примерно одинаковы по производительности, и оба берут ничтожно мало времени из цикла. MakeDataTable () занимает ~ 10% времени цикла, а 90% - это WriteToServer. Вот типичная разбивка: ticks_SkipTake 0, ticks_MakeDataTable 7 620 762, ticks_GetBulkCopy 80 008, ticks_ WriteToServer 76 737 673. Итак, хотя MakeDataTable можно было бы улучшить, самым большим нарушителем по-прежнему является массовая вставка. Кстати, а как вы предлагаете реализовать IDataReader? Есть масса методов для реализации   -  person galets    schedule 25.06.2012
comment
@ ta.speot.is Я реализовал IDataReader на основе приведенного здесь кода: stackoverflow.com/questions/2258310/ и добился увеличения скорости на ~ 10%.   -  person galets    schedule 26.06.2012
comment
из любопытства, какой тип данных для field1, field2 и т. д. они фиксированной или переменной длины? что прибл. размер для каждой строки?   -  person Dharmendar Kumar 'DK'    schedule 30.06.2012
comment
@DK переменная. Около 150 полей, сочетание int, datetime, uniqueidentifier и nvarchar (max). В среднем запись составляет ~ 4-5К   -  person galets    schedule 30.06.2012


Ответы (3)


Рекомендации по настройке SQL Server для массовой загрузки см. В Руководство по загрузке данных и производительности от MS, а также Рекомендации по оптимизации массового импорта из книг в Интернете. Хотя они ориентированы на массовую загрузку из SQL Server, большинство советов применимо к массовой загрузке с использованием клиентского API. Эти документы относятся к SQL 2008 - вы не говорите, на какую версию SQL Server вы нацеливаетесь. В обоих документах содержится довольно много информации, которую стоит рассмотреть в деталях. Однако некоторые основные моменты:

  • Минимально регистрируйте массовую операцию. Используйте восстановление с неполным протоколированием или простое восстановление. Возможно, вам потребуется включить traceflag 610 (но см. Предостережения по этому поводу)
  • Настройте размер партии
  • Рассмотрите возможность разделения целевой таблицы
  • Рассмотрите возможность удаления индексов во время массовой загрузки

Красиво резюмировано на этой блок-схеме из Загрузка данных и Руководство по производительности: введите описание изображения здесь

Как говорили другие, вам нужно получить некоторые счетчики производительности, чтобы установить источник узкого места, поскольку ваши эксперименты показывают, что ввод-вывод может не быть ограничением. Руководство по загрузке данных и производительности включает список типов ожидания SQL и счетчиков производительности для мониторинга (в документе нет привязок, на которые можно ссылаться, но это около 75% в документе, в разделе «Оптимизация массовой загрузки»)

ОБНОВЛЕНИЕ

Мне потребовалось время, чтобы найти ссылку, но этот доклад по SQLBits Томаса Кейзера тоже неплох. стоит посмотреть - слайды доступны, если у вас нет времени все посмотреть. Он повторяет некоторые материалы, ссылки на которые приведены здесь, но также включает несколько других предложений о том, как бороться с высокой частотой возникновения определенных счетчиков производительности.

person Ed Harper    schedule 23.06.2012
comment
Я просмотрю размещенные вами ссылки и выразю вам благодарность за ваши усилия. Но я должен отметить, что я перевел db в простой режим восстановления (который не является журналом), настроил размер пакета (как я уже сказал), относительно индексов и секционирования: есть только один индекс, и если вы посмотрите на него, он не должно влиять на скорость вставки. Поскольку скорость вставки не снижается с количеством записей, я не думаю, что разбиение на разделы необходимо. - person galets; 23.06.2012
comment
@galets - добавил еще одну ссылку. Ваш автоматически увеличивающийся PK может ограничивать скорость, поскольку все потоки вставки могут конкурировать за горячую страницу в конце таблицы. В этом случае рассмотрите возможность секционирования хеша, как описано в беседе Kejser SQLBits по ссылке выше. Кроме того, проведите небольшое тестирование, чтобы убедиться, что ведение журнала минимально. Простой режим восстановления не разлочен; журнал транзакций будет записываться (и увеличиваться) на протяжении транзакции. - person Ed Harper; 23.06.2012
comment
Я проверил теорию о том, что _id может быть проблемой, полностью отбросив его и оставив таблицу без индекса полностью. Вроде не проблема, скорость осталась ровно такой, как была раньше. Тем не менее, я благодарю вас за ваши усилия, а также за отправленные вами ссылки. Они дали мне несколько идей, которые я буду опробовать - person galets; 25.06.2012

Кажется, вы многое сделали, однако я не уверен, что у вас была возможность изучать Альберто Феррари Отчет" Анализ производительности SqlBulkCopy ", в котором описаны несколько факторов, которые необходимо учитывать при рассмотрении производительности, связанной с SqlBulkCopy. Я бы сказал, что многое из того, что обсуждается в этой статье, все еще стоит попробовать, и было бы неплохо попробовать в первую очередь.

person AvkashChauhan    schedule 23.06.2012
comment
спасибо, отличное чтение, я почерпнул из него несколько хороших идей. Единственное, чего я не могу полностью достичь, - это 100% -ное использование процессора, диска или сети, и это сводит меня с ума. - person galets; 26.06.2012

Я не уверен, почему вы не получаете 100% -ное использование ЦП, ввода-вывода или памяти. Но если вы просто хотите улучшить скорость массовой загрузки, вот что следует учесть:

  1. Разделите файл данных на разные файлы. Или, если они поступают из разных источников, просто создайте разные файлы данных.
  2. Затем запустите несколько массовых вставок одновременно.

В зависимости от вашей ситуации вышеуказанное может оказаться невозможным; но если вы можете, я уверен, что это должно улучшить вашу скорость загрузки.

person Dharmendar Kumar 'DK'    schedule 29.06.2012
comment
Спасибо за предложение, но я уже пробовал. Хотя это действительно сработало, разница не была существенной. См. Мой комментарий UPDATE3. - person galets; 29.06.2012