Некоторое время назад я показал вам способ масштабирования записей Neo4j с использованием RabbitMQ . Это было круто, но некоторые из вас спрашивали меня о другом решении, которое не включало бы добавление еще одного программного компонента в стек.
Оказывается, мы можем сделать это всего за Neo4j, используя небольшую помощь из библиотеки Guava . Решение включало запуск фоновой службы, которая хранит записи в очереди и время от времени (например, каждую секунду) фиксирует эти записи в одной транзакции.
There are many different ways to get data into Neo4j, this solution is really meant for when you don’t know what the data will be ahead of time. For example if you are building a consumer facing application and you have 10k users logged in concurrently all performing actions. In this example we are going to build something very generic. Let’s say you are keeping track of web sites users have visited, so you can do further analysis on that data.
Служба будет принимать идентификатор пользователя, URL-адрес веб-сайта и создавать уникальные отношения между ними. Пользователи и URL-адреса также будут уникальными узлами, поэтому «записи» довольно тяжелые (мы не просто создаем пустой узел). Мы сделаем две версии наших записей. Синхронная версия, которая выполняет запись немедленно в транзакции, и асинхронная версия, которая делегирует записи фоновой службе. AbstractScheduledService быть точным. Служба будет хранить очередь для наших записей:
public class BatchWriterService extends AbstractScheduledService{ public LinkedBlockingQueue<HashMap<String, Object>> queue = new LinkedBlockingQueue<>();
Планировщик сообщает, когда выполнять, в нашем случае он будет запускаться каждую секунду:
@Override protected Scheduler scheduler() { return Scheduler.newFixedRateSchedule(0, 1, TimeUnit.SECONDS); }
Метод runOneIteration истощит очередь и выполнит запись.
@Override protected void runOneIteration() throws Exception { ...
Я не буду рассказывать о том, как работает служба написания графиков в этом посте, вместо этого я хочу показать вам, что она делает. Полный исходный код — это ваша награда за чтение всего этого (или за читерство и прокрутку до конца). Я всегда начинаю с точки приветствия, чтобы убедиться, что она подключена правильно:
Я всегда включаю процедуру разогрева в свои неуправляемые расширения, поэтому при тестировании я могу быстро получить данные в памяти или в производственном процессе, подготовить сервер перед добавлением его в балансировщик нагрузки.
Мы сделали конечную точку для инициализации, которая в основном создает два уникальных ограничения индекса. Один для пользователей и один для сайтов. Мы запустим это только один раз.
Теперь давайте попробуем добавить некоторые данные. В этом случае я скажу, что пользователь «max» посетил сайт http://www.neo4j.org :
Давайте посмотрим, что теперь есть в нашей базе данных:
Мы можем видеть, что он создал два узла и отношения между ними. Далее мы попробуем асинхронную версию, перейдя на другой сайт http://www.maxdemarzi.com :
Давайте снова посмотрим на наши данные:
Он не создавал другой «максимальный» узел, вместо этого он использовал тот, который у нас был. В обоих случаях Neo4j создал наши данные. Давайте посмотрим на console.log, чтобы увидеть, что сделал наш асинхронный процесс.
Выполнен набор транзакций с 1 записью за 50 [мсек] @ 2014-06-30T16: 07: 25.460-05: 00
Здесь мы видим, что наш сервис пакетной записи получил запись и выполнил ее для нас в фоновом режиме. Потрясающие! У нас работает простой случай … но давайте попробуем это с некоторой нагрузкой и сравним его производительность. Сначала мы попробуем запустить синхронную версию, которая создаст транзакцию для каждой записи. Мы будем использовать Gatling для этого теста производительности, исходный код для теста на github.
Тест длился 30 секунд, и ему удалось создать 2815 записей в довольно дурацких 93 запросах в секунду со средним значением 170 мс и максимальным значением 510 мс. Что еще хуже, мы даже получили ошибку. Взглянув на логи, мы видим, что две транзакции пытались обновить один и тот же узел одновременно.
16: 19: 56.813 [qtp6401337-74] ПРЕДУПРЕЖДЕНИЕ oejetty.servlet.ServletHandler — / v1 / service / 4234497 / посетило
org.neo4j.kernel.DeadlockDetectedException: не паникуйте.Сценарий тупика был обнаружен и его удалось избежать. Это означает, что две или более транзакций, которые удерживали блокировки, хотели ожидать блокировок, удерживаемых друг другом, что привело бы к тупику между этими транзакциями. Это исключение было брошено вместо того, чтобы оказаться в этом тупике.
Чтобы узнать, как этого избежать, обратитесь к разделу о тупиках в руководстве по Neo4j: http://docs.neo4j.org/chunked/stable/transactions-deadlocks.html
Подробности: «Транзакция (2149, владелец:» qtp6401337-74 ″) [STATUS_ACTIVE, Resources = 2] не может ожидать ресурс RWLock [Узел [3272]], поскольку => Транзакция (2149, владелец: »qtp6401337-74 ″) [STATUS_ACTIVE, Resources = 2] <- [: HELD_BY] — RWLock [IndexLock [Site: url]] <- [: WAITING_FOR] — Транзакция (2148, владелец: «qtp6401337-79») [STATUS_ACTIVE, Resources = 2] < — [: HELD_BY] — RWLock [Node [3272]] ‘.
Давайте попробуем асинхронную версию:
Ух ты, посмотри на это! Нет ошибок, так как у него только один писатель, поэтому он не может зайти в тупик. Сейчас мы достигаем чуть менее 6 тыс. Запросов / с, что кажется довольно удивительным по сравнению с менее чем 100 об / с! 1 мс означает и максимум 290 мс, но давайте посмотрим, что происходит под одеялом, прежде чем мы объявим о победе:
Выполнено множество транзакций с 566 записями за 3366 [мсек] @ 2014-06-30T19: 46: 29.661-05: 00
Выполнено транзакция из 1000 записей за 9678 [мсек] @ 2014-06-30T19: 46: 39.340-05 : 00
Выполнена транзакция 1000 записей за 5957 [мсек] @ 2014-06-30T19: 46: 45.298-05: 00
Выполнена транзакция 1000 записей за 4270 [мсек] @ 2014-06-30T19: 46: 49.569-05 : 00
Выполнена транзакция 1000 записей в 2845 [мсек] @ 2014-06-30T19: 46: 52.415-05: 00
Выполнена транзакция 1000 записей в 2599 [мсек] @ 2014-06-30T19: 46: 55.015-05 : 00
Выполнено множество транзакций с 5000 записями в 25352 [мсек] @ 2014-06-30T19: 46: 55.015-05: 00
Выполнено транзакция из 1000 записей в 1218 [мсек] @ 2014-06-30T19: 46: 56.234 -05: 00
Выполнена транзакция 1000 записей за 622 [мсек] @ 2014-06-30T19: 46: 56.857-05: 00
Выполнена транзакция 1000 записей за 962 [мсек] @ 2014-06-30T19: 46: 57.820-05: 00
Выполнена транзакция 1000 записей за 682 [мсек] @ 2014-06-30T19: 46: 58.503-05: 00
Выполнена транзакция 1000 записей за 654 [мсек] @ 2014-06-30T19: 46: 59.157-05: 00
Выполнено множество транзакций с 5000 записями за 4142 [мсек] @ 2014-06-30T19: 46: 59.158-05: 00
Выполнено транзакция из 1000 записей за 486 [мсек] @ 2014-06-30T19: 46: 59.644-05 : 00
Выполнена транзакция 1000 записей за 421 [мсек] @ 2014-06-30T19: 47: 00.066-05: 00
Выполнена транзакция 1000 записей за 374 [мсек] @ 2014-06-30T19: 47: 00.441-05 : 00
Выполнена транзакция 1000 записей за 416 [мсек] @ 2014-06-30T19: 47: 00.858-05: 00
Выполнена транзакция 1000 записей за 461 [мсек] @ 2014-06-30T19: 47: 01.320-05: 00
Выполнено множество транзакций с 5000 записями за 2162 [мсек] @ 2014-06-30T19: 47: 01.320-05: 00
Выполнено транзакция из 1000 записей за 419 [мсек] @ 2014-06-30T19: 47: 01.740-05 : 00
Выполнена транзакция 1000 записей за 481 [мсек] @ 2014-06-30T19: 47: 02.222-05: 00
Выполнена транзакция 1000 записей за 365 [мсек] @ 2014-06-30T19: 47: 02.588-05 : 00
Выполнена транзакция 1000 записей за 286 [мсек] @ 2014-06-30T19: 47: 02.875-05: 00
Выполнена транзакция 1000 записей за 320 [мсек] @ 2014-06-30T19: 47: 03.195-05 : 00
…
Выполнено множество транзакций с 5000 записями в 1693 [мсек] @ 2014-06-30T19: 47: 57.424-05: 00
Выполнено транзакция из 1000 записей за 365 [мсек] @ 2014-06-30T19: 47: 57.789-05 : 00
Выполнена транзакция 1000 записей за 261 [мсек] @ 2014-06-30T19: 47: 58.051-05: 00
Выполнена транзакция 1000 записей за 278 [мсек] @ 2014-06-30T19: 47: 58.330-05 : 00
Выполнено 1000 транзакций записи за 293 [мсек] @ 2014-06-30T19: 47: 58.623-05: 00
Выполнено множество транзакций с 4335 записями за 1281 [мсек] @ 2014-06-30T19: 47: 58.706 -05: 00
So what’s happening here? Looks like it takes a while to warm up, but then settles in at 1000 writes every 300ms or so. We can also see the writes finished at 19:47:58, but our tests finished at 19:46:55. That’s a whole minute behind. So what’s happening is we built a system that will keep on accepting requests until it eventually crashes… and when it does we’ll lose a minute, an hour, a day, who knows how much data!
We don’t want that to happen! So let’s write a little back pressure in to the system. We’ll force our queue to have a capacity of 25000 writes. Any requests that come in while the queue is full will block until the queue is drained.
That brings us down to around 1900 r/s, but our test ended at 20:23:40 and our writes finished 8 seconds later at 20:23:48. Our maximum loss is the size of the queue, and we can be at most “however long it takes one queue size set of writes” behind. I didn’t spend much time tuning it for the optimal numbers because that’s going to be hardware dependent. The numbers are much better than single transactions, but will get even better once the system has data as it would be in production. Instead of creating 2 new unique nodes (and their corresponding index entries) as well as a unique relationship between them, we’ll be just updating a relationship. Here is what those numbers look like if you are curious:
You can generate similar numbers by running the test multiple times. Take a look at the log, you’ll see you’ll be at most a second or two behind. The source code for the batch writer service is on github. I’ll be going over this in more detail on my Neo4j High Performance Video Course.