RavenDB в «одноразовых» приложениях

На выходных попробовал RavenDB на небольшой задаче обработки массива документов. Документов было не очень много — порядка 50К, их обработка — задача разовая, но её длительность получалась однозначно порядка 10 часов, плюс всё это отлично параллелилось.
Поэтому возникла мысль загнать все эти документы в БД, чтобы без проблем сохранять промежуточные результаты и не волноваться за exception’ы, безвозвратно прерывающие обработку 10-часового процесса в самом конце :)
Raven-Embedded видился неплохим кандидатом для такого использования, поскольку позволял не париться с маппингами, быстро «установить» БД, просто добавив nuget пакет, позволял динамически добавлять в документы новые структуры данных (результаты обработок) ну и по идее должен был быстро работать :)
Что же из всего этого получилось?

Сразу оговорюсь, что использовал последнюю Prerelease-версию.

Мой код, в общих чертах, представлял из себя выборку и обработку блоков по 100 документов, предварительно внесенных в БД.
Выглядело это как-то так (опуская обработку ошибок и некоторые мелкие детали):

lock (_lockObject) {
	//получаем список необработанных домов.
	houses = session.Query<HouseInfo>()
				.Where(x => x.State == HouseInfo.DownloadState.NotStarted)
				.Customize(x => x.WaitForNonStaleResultsAsOfNow())  // необходимо, чтобы дома "недавно" помеченные как ParseStarted не попали на повторную обработку
				.Take(100).ToList();
				
	//помечаем дома, для которых начата обработка
	houses.ForEach(x =>
						{
							x.State = HouseInfo.DownloadState.ParseStarted;
						});
	session.SaveChanges();
}

foreach (var house in houses) {
	//производим обработку
	houses.ParsedData = ParseData(house);
	//переводим в завершенное состояние
	houses.State = HouseInfo.DownloadState.Finished;
}
session.SaveChanges();	

Первой неожиданностью стала проблема с .Customize(x => x.WaitForNonStaleResultsAsOfNow()). Точнее, с её изначальным отсутствием :) Из-за этого два соседних потока регулярно принимали в обработку одни и те же документы (потому что измененный State не успевал попасть в индекс). Проблема «невалидных» индексов вообще стала достаточно серьезной в контексте использования Рейвена в небольших «одноразовых» приложениях.
В моем случае все индексы создавались «динамически» (то есть я не писал MapReduce-классов), поэтому индексы получали статус «временных», и по выходу из приложения, насколько я понял, не сохранялись. Соответственно при новом запуске и попытке выполнить запрос типа session.Query().OrderBy(x => x.IntId).Take(50000).ToList() индекс создавался заново, и мне легко возвращалась лишь половина из существующих документов (из тех, что успели проиндексироваться). Это частенько вводило в замешательство :)

Второй большой проблемой стала производительность БД в контексте использования WaitForNonStaleResults. С учетом того, что обработка блоков в 100 документов занимала не слишком много времени (порядка 3-4 секунд на 100 документов), основным «тормозом» стала как раз выборка данных. Пауза на запросе достигала 15 секунд, что меня весьма печалило.
К концу обработки (где-то на 35К документе) Рейвен раскочегарился и сократил «зависания» до 1-2 секунд, что, в общем-то, вполне приемлемо, однако возможно, что это продлилось бы недолго :)

В сухом итоге впечатления от RavenDB Embedded:
+ очень быстрый старт (установка nuget-пакета), отсутствие маппингов;
+ удобный и понятный синтаксис запросов (знакомый всем linq)
— производительность при последовательных операциях записи/чтения, при строгой необходимости актуальности данных
— несколько непривычный синтаксис запросов в Raven Studio (lucene, a не SQL или linq)
В принципе поставленную задачу Рейвен решил.

В общем-то, задачу оптимизации WaitForNonStaleResults также можно было решить, если в запросах проверять не меняющийся State, а, допустим, id и обрабатывать документы в строго возрастающем порядке. Да, использование nosql даже в относительно простых случаях заставляет думать слегка «по-другому» в сравнении с миром sql :)

P.S. Ну и слегка непонятно, насколько легально такое «одноразовое» использование с учетом отсутствия у меня каких бы то ни было лицензий на Рейвен :)

Опубликовать в Facebook
Опубликовать в Google Plus

4 комментария

  1. Хм.. Считаю приведенный подход неправильным, даже для прототипа или одноразового решения
    Следовало делать выборку в одном потоке и пихать в какую нить очередь, а в других потоках — обрабатывать эту очередь.
    Размер очереди ограничить, например — количеством потоков обрабатывающих сообщения
    Выбирать сразу сто и помечать их тут же как обрабатываемые.. странно, а что если памяти не хватит, свет выключат, или астероид упадет, а у вас пока 99-е сообщение не обработается — все предыдущие не пометятся как обрабатываемые =)

    По сабжу: RavenDB в целом вещь прикольная

  2. Спасибо за наводку на очереди, можно ссылку на какой-нибудь пример обработки с их помощью?
    В данной ситуации я не до конца представляю как реализовать «динамическое пополнение» очереди, если только не пихать в неё все 50К документов сразу..

    Я догадывался, что моё решение скорее всего неидеальное, шел просто по быстрейшему пути — вся утилитка вместе с более чем 10-часовой обработкой уложились в двое суток :)

    > Выбирать сразу сто и помечать их тут же как
    > обрабатываемые.. странно, а что если памяти не хватит,
    > свет выключат, или астероид упадет, а у вас пока 99-е ?
    > сообщение не обработается — все предыдущие не
    > пометятся как обрабатываемые =)
    Я там сохранял еще дату последней операции, и «зависшие» в обработке можно было просто сбрасывать через некоторое время. Ну и потерянное время на обработке сотни доков — пара секунд, так что это были мелочи :)
    Ну а вынос SaveChanges за пределы цикла был в качестве небольшой оптимизации, возможно, лишней :)

    Рейвен удобен, но я не ожадал такой засады в производительности :) Казалось бы, 100 изменившихся документов переиндексировать.. а задержки секунд на 10.

  3. Пример с очередью…
    Лучше ознакомьтесь с Concurrent-классами, например ConcurrentQueue http://msdn.microsoft.com/ru-ru/library/dd267265.aspx
    Там и пример, кстати, есть )

    Почитайте вот тут небольшие заметки http://www.sansys.net/search/label/RavenDb

    + рекомендую книги:
    1. Pro .NET 4 Parallel Programming in C# http://goo.gl/DROUb (хотя, признаюсь, сам читал лишь пару глав из нее)
    2. CLR via C#, но уверен, что она у вас есть )

  4. Спасибо, с очередями как таковыми я знаком, конечно, но книжку обязательно пролистаю :)
    Интересен вопрос конкретного применения для параллелизации обработки большого массива документов, может есть какие-то примеры?

    Решение «в лоб» — засунуть все документы в очередь и обрабатывать её в двух потоках — как-то не очень хорошо смотрится. Интересны другие возможные варианты :)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *