Тесты для вставки документов с использованием C# для MongoDB
3 апреля 2024 г.В предыдущих статьях я объяснил, как мы можем используйте C# и MongoDB для вставки записей. Но мы начали исследование MongoDB, охватывая множество основ, и я хотел начать изучать более интересные аспекты того, как мы используем эти вещи вместе. Когда я собирал видеоконтент по всем этим темам, мое внимание привлекла одна вещь: общие методы и методы, работающие в BsonDocument, и мне было любопытно, отличается ли производительность. Итак, для начала я решил, что мы посмотрим на тесты вставки C# MongoDB и посмотрим, есть ли какие-нибудь интересные шаблоны.
Аспекты тестирования C# MongoDB
На днях я беседовал с Дэвидом Калланом в Твиттере о тестах, которые он часто публикует в социальных сетях. Оказывается, вокруг Твиттера велась еще одна дискуссия, в которой обсуждались синхронные и асинхронные вызовы к базам данных. Предложение заключалось в том, что для очень быстрых запросов к базе данных асинхронные варианты действительно медленнее.
Конечно, это заставило шестерни повернуть вспять. Всю неделю я писал и создавал видеоролики для MongoDB на C# и намекал на предстоящие тесты производительности. Это в сочетании с моим разговором с Дэйвом И разговором, который я видел в Твиттере, заставило меня подумать, что мне нужно уделить этому больше внимания. Я предположил, что это как-то связано с Task vs ValueTask, но у меня не было для этого особых оснований.
Однако изначально я хотел изучить эти тесты, потому что мне было любопытно, будет ли использование BsonDocument по сравнению со специальным DTO (будь то вариант структуры, класса или записи) иметь разные характеристики производительности.
Это означало, что мне хотелось бы убедиться, что я покрыл всю матрицу:
- Синхронно
- Асинхронный
- Асинхронно с ValueTask
- Структура
- Класс
- Структура записи
- Класс записи
- BsonDocument
И с этим я решил написать несколько очень простых тестов! Давайте их проверим.
Вставка кода сравнения C# MongoDB
Как и при любом сравнительном тестировании, которое мы проводим в C#, BenchmarkDotNet – наш незаменимый инструмент! Обязательно начните с установки пакета BenchmarkDotNet NuGet. Мы будем использовать это для обеспечения единообразной настройки, прогрева, запуска и создания отчетов для наших тестов MongoDB.
Поскольку мы пытаемся уменьшить как можно больше возможных внешних факторов для этих тестов, мы собираемся использовать Testcontainers для запуска экземпляра MongoDB в контейнере Docker. Конечно, прямое взаимодействие с чем-либо за пределами нашего кода может привести к несогласованности и увеличению места для ошибок в наших результатах. Однако это должно помочь свести к минимуму ситуацию. Для этого вам также понадобится NuGet-пакет Testcontainers.MongoDB.
Вы можете найти весь соответствующий код на GitHub, но мы начнем с того, что точка входа выглядит так:
using BenchmarkDotNet.Running;
using System.Reflection;
BenchmarkRunner.Run(
Assembly.GetExecutingAssembly(),
args: args);
Красиво и просто, чтобы начать тесты. И самое важное здесь — тесты:
using BenchmarkDotNet.Attributes;
using MongoDB.Bson;
using MongoDB.Driver;
using Testcontainers.MongoDb;
[MemoryDiagnoser]
//[ShortRunJob]
[MediumRunJob]
public class InsertBenchmarks
{
private MongoDbContainer? _container;
private MongoClient? _mongoClient;
private IMongoCollection<BsonDocument>? _collection;
private IMongoCollection<RecordStructDto>? _collectionRecordStruct;
private IMongoCollection<RecordClassDto>? _collectionRecordClass;
private IMongoCollection<StructDto>? _collectionStruct;
private IMongoCollection<ClassDto>? _collectionClass;
[GlobalSetup]
public async Task SetupAsync()
{
_container = new MongoDbBuilder()
.WithImage("mongo:latest")
.Build();
await _container.StartAsync();
_mongoClient = new MongoClient(_container.GetConnectionString());
var database = _mongoClient.GetDatabase("test");
_collection = database.GetCollection<BsonDocument>("test");
_collectionRecordStruct = database.GetCollection<RecordStructDto>("test");
_collectionRecordClass = database.GetCollection<RecordClassDto>("test");
_collectionStruct = database.GetCollection<StructDto>("test");
_collectionClass = database.GetCollection<ClassDto>("test");
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _container!.StopAsync();
}
[Benchmark]
public async Task InsertOneAsync_BsonDocument()
{
await _collection!.InsertOneAsync(new BsonDocument()
{
["Name"] = "Nick Cosentino",
});
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_BsonDocument()
{
await _collection!.InsertOneAsync(new BsonDocument()
{
["Name"] = "Nick Cosentino",
});
}
[Benchmark]
public void InsertOne_BsonDocument()
{
_collection!.InsertOne(new BsonDocument()
{
["Name"] = "Nick Cosentino",
});
}
[Benchmark]
public async Task InsertOneAsync_RecordStruct()
{
await _collectionRecordStruct!.InsertOneAsync(new RecordStructDto("Nick Cosentino"));
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_RecordStruct()
{
await _collectionRecordStruct!.InsertOneAsync(new RecordStructDto("Nick Cosentino"));
}
[Benchmark]
public void InsertOne_RecordStruct()
{
_collectionRecordStruct!.InsertOne(new RecordStructDto("Nick Cosentino"));
}
[Benchmark]
public async Task InsertOneAsync_RecordClass()
{
await _collectionRecordClass!.InsertOneAsync(new RecordClassDto("Nick Cosentino"));
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_RecordClass()
{
await _collectionRecordClass!.InsertOneAsync(new RecordClassDto("Nick Cosentino"));
}
[Benchmark]
public void InsertOne_RecordClass()
{
_collectionRecordClass!.InsertOne(new RecordClassDto("Nick Cosentino"));
}
[Benchmark]
public async Task InsertOneAsync_Struct()
{
await _collectionStruct!.InsertOneAsync(new StructDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_Struct()
{
await _collectionStruct!.InsertOneAsync(new StructDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public void InsertOne_Struct()
{
_collectionStruct!.InsertOne(new StructDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public async Task InsertOneAsync_Class()
{
await _collectionClass!.InsertOneAsync(new ClassDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_Class()
{
await _collectionClass!.InsertOneAsync(new ClassDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public void InsertOne_Class()
{
_collectionClass!.InsertOne(new ClassDto() { Name = "Nick Cosentino" });
}
private record struct RecordStructDto(string Name);
private record class RecordClassDto(string Name);
private struct StructDto
{
public string Name { get; set; }
}
private class ClassDto
{
public string Name { get; set; }
}
}
В тестовом коде есть как можно больше того, что нам не интересно использовать, в помеченных методах GlobalSetup и GlobalCleanup.
Результаты тестов вставки C# MongoDB
Включите барабанную дробь! Пришло время взглянуть на результаты нашего теста MongoDB и провести небольшой анализ:
Вот мои выводы из приведенных выше контрольных данных:
- Все асинхронные варианты использовали примерно на 5 КБ больше, чем обычные версии методов, которые нам приходилось использовать.
- Кажется, не было никакой разницы в выделенной памяти для задач асинхронного и асинхронного значения, НО Gen0 и Gen1 не имели никакой ценности для *некоторых* тестов ValueTask — однако не для всех из них. Это почти похоже на ValueTask в сочетании с типом данных struct для результатов вставки в Gen0 и Gen1 без значения, но старый добрый BsonDocument является исключением из этого правила.
- Самым быстрым и наименьшим потреблением памяти является InsertOne_RecordClass, хотя InsertOne_BsonDocument отстает от него всего на несколько микросекунд.
- Асинхронные версии тестов также кажутся медленнее, чем их обычные версии по всем направлениям.
Кажется, это во многом соответствует некоторым вступительным мыслям Твиттера об асинхронных операциях! Итак, некоторые гипотезы подтверждены/опровергнуты:
- Async *в целом* хуже для очень быстрых операций с базой данных.
- ValueTask не обеспечивает последовательной оптимизации производительности в таких ситуациях.
- Для отдельных элементов нет большой разницы в объеме памяти, которую мы наблюдаем между любыми из этих вариантов типов данных.
Будет хорошим упражнением для продолжения сравнительного анализа, вставки множества элементов в MongoDB из C#. Я думаю, мы можем начать замечать, что некоторые из этих вариантов выделяются по-разному, когда будем работать с коллекциями предметов. Но это все еще гипотеза, которую необходимо доказать!
Подведение итогов тестов вставки C# MongoDB
Это было простое исследование тестов вставки для MongoDB с использованием C#. В целом меня ждало несколько сюрпризов, но я все же думаю, что нужно провести дополнительные исследования, когда мы работаем с несколькими записями одновременно. Я действительно был немного удивлен, увидев, что асинхронность хуже по всем направлениям, поскольку я полагал, что, возможно, любой тип ввода-вывода будет маскировать влияние асинхронных накладных расходов на производительность. Но это был забавный эксперимент, и это еще не все!
Если эта информация оказалась для вас полезной и вы ищете дополнительные возможности для обучения, рассмотрите возможность подписаться на мой бесплатный еженедельный информационный бюллетень по разработке программного обеспечения и просмотреть мой бесплатные видео на YouTube!
Также опубликовано здесь .
Оригинал