Насколько эффективным может быть Meteor, когда делится огромной коллекцией среди множества клиентов?


100

Представьте себе следующий случай:

  • 1000 клиентов подключены к странице Meteor, отображающей содержимое коллекции Somestuff.

  • Somestuff - это коллекция из 1000 предметов.

  • Кто-то вставляет новый предмет в коллекцию Somestuff

Что случится:

  • Все сообщения Meteor.Collectionна клиентах будут обновлены, т.е. вставка будет перенаправлена ​​всем из них (что означает, что одно сообщение вставки отправлено 1000 клиентам)

Какова стоимость ЦП для сервера, чтобы определить, какого клиента необходимо обновить?

Верно ли, что клиентам будет отправлено только вставленное значение, а не весь список?

Как это работает в реальной жизни? Существуют ли тесты или тесты такого масштаба?

Ответы:


119

Короткий ответ: по сети отправляются только новые данные. Вот как это работает.

Есть три важных части сервера Meteor, которые управляют подписками: функция публикации , которая определяет логику того, какие данные предоставляет подписка; водитель Монго , который следит за базой данных для изменений; и поле слияния , которое объединяет все активные подписки клиента и отправляет их клиенту по сети.

Функции публикации

Каждый раз, когда клиент Meteor подписывается на коллекцию, сервер выполняет функцию публикации . Задача функции публикации - определить набор документов, который должен иметь клиент, и отправить каждое свойство документа в поле слияния. Он запускается один раз для каждого нового подписавшегося клиента. Вы можете поместить любой JavaScript в функцию публикации, например, произвольно сложный контроль доступа с использованием this.userId. Функция публикации отправляет данные в поле слияния, вызывая this.added, this.changedи this.removed. См. Полную документацию по публикации для получения более подробной информации.

Большинство публиковать функции не слоняться вокруг с низким уровнем added, changedи removedAPI, хотя. Если опубликовать функция возвращает Монго курсора, сервер Метеор автоматически подключает выход драйвера Монго ( insert, updateи removedобратные вызовы) на вход коробки слияния ( this.added, this.changedи this.removed). Довольно удобно, что вы можете выполнить все проверки разрешений заранее в функции публикации, а затем напрямую подключить драйвер базы данных к блоку слияния без какого-либо пользовательского кода. А когда автопубликация включена, даже эта небольшая часть скрыта: сервер автоматически устанавливает запрос для всех документов в каждой коллекции и помещает их в поле слияния.

С другой стороны, вы не ограничены публикацией запросов к базе данных. Например, вы можете написать функцию публикации, которая считывает положение GPS с устройства внутри Meteor.setIntervalили опрашивает устаревший REST API из другой веб-службы. В тех случаях, вы испускаете изменения в поле слияния, вызвав низкий уровень added, changedи removedDDP API.

Драйвер Mongo

Задача драйвера Mongo - следить за базой данных Mongo на предмет изменений в живых запросах. Эти запросы непрерывно работать и возвращать обновления , как изменение результатов по телефону added, removedи changedобратных вызовов.

Mongo - это не база данных в реальном времени. Так что водитель опрашивает. Он сохраняет в памяти копию результата последнего запроса для каждого активного запроса в реальном времени. На каждом цикле опроса, он сравнивает новый результат с предыдущим сохраненным результатом, вычислением минимального набора added, removedи changed событий , которые описывают разницу. Если несколько вызывающих абонентов регистрируют обратные вызовы для одного и того же оперативного запроса, драйвер просматривает только одну копию запроса, вызывая каждый зарегистрированный обратный вызов с одним и тем же результатом.

Каждый раз, когда сервер обновляет коллекцию, драйвер пересчитывает каждый живой запрос в этой коллекции (будущие версии Meteor будут предоставлять API масштабирования для ограничения того, какие живые запросы пересчитываются при обновлении). Драйвер также опрашивает каждый живой запрос по 10-секундному таймеру, чтобы перехватить обновления базы данных по внеполосному каналу, которые обошли сервер Meteor.

Поле слияния

Работа в поле слияния является объединение результатов ( added, changedи removed звонки) всех активных публикующих функций организма клиента в единый поток данных. Для каждого подключенного клиента есть одно поле слияния. Он содержит полную копию клиентского кэша Minimongo.

В вашем примере с единственной подпиской поле слияния по сути является сквозным. Но более сложное приложение может иметь несколько подписок, которые могут перекрываться. Если две подписки устанавливают один и тот же атрибут в одном и том же документе, поле слияния решает, какое значение имеет приоритет, и отправляет его только клиенту. Мы еще не представили API для установки приоритета подписки. На данный момент приоритет определяется порядком, в котором клиент подписывается на наборы данных. Первая подписка, которую делает клиент, имеет наивысший приоритет, вторая подписка - следующая по величине и так далее.

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

Что происходит при обновлении

Итак, теперь мы подготовили почву для вашего сценария.

У нас 1000 подключенных клиентов. Каждый подписан на один и тот же живой запрос Mongo ( Somestuff.find({})). Поскольку запрос одинаков для каждого клиента, драйвер выполняет только один оперативный запрос. Есть 1000 активных ящиков слияния. И каждый клиент опубликуем функция зарегистрирован ли added, changedи removedна этой живой запрос , который питается в одном из слияния коробки. Больше ничего не связано с блоками слияния.

Сначала водитель Mongo. Когда один из клиентов вставляет новый документ Somestuff, он запускает повторное вычисление. Драйвер Mongo повторно выполняет запрос для всех документов в Somestuff, сравнивает результат с предыдущим результатом в памяти, обнаруживает, что есть один новый документ, и вызывает каждый из 1000 зарегистрированных insertобратных вызовов.

Затем функции публикации. Здесь происходит очень немногое: каждый из 1000 insertобратных вызовов отправляет данные в поле слияния путем вызова added.

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

Общая стоимость ЦП - это стоимость выполнения одного запроса Mongo плюс стоимость 1000 блоков слияния, которые проверяют состояние своих клиентов и создают новые полезные данные сообщения DDP. Единственные данные, которые передаются по сети, - это один объект JSON, отправленный каждому из 1000 клиентов, соответствующий новому документу в базе данных, плюс одно сообщение RPC на сервер от клиента, который сделал исходную вставку.

Оптимизация

Вот что мы точно запланировали.

  • Более эффективный драйвер Mongo. Мы оптимизировали драйвер в 0.5.1, чтобы запускать только одного наблюдателя для каждого отдельного запроса.

  • Не каждое изменение БД должно вызывать повторное вычисление запроса. Мы можем внести некоторые автоматические улучшения, но лучший подход - это API, который позволяет разработчику указывать, какие запросы необходимо повторно запустить. Например, разработчику очевидно, что вставка сообщения в одну комнату чата не должна приводить к недействительности текущего запроса сообщений во второй комнате.

  • Драйвер Mongo, функция публикации и поле слияния не должны запускаться в одном процессе или даже на одном компьютере. Некоторые приложения выполняют сложные запросы в реальном времени и требуют больше ЦП для наблюдения за базой данных. У других есть только несколько отдельных запросов (представьте движок блога), но, возможно, много подключенных клиентов - им требуется больше ЦП для блоков слияния. Разделение этих компонентов позволит нам масштабировать каждую деталь независимо.

  • Многие базы данных поддерживают триггеры, которые срабатывают при обновлении строки и предоставляют старые и новые строки. Благодаря этой функции драйвер базы данных может зарегистрировать триггер вместо запроса изменений.


Есть ли какой-нибудь пример того, как использовать Meteor.publish для публикации данных без курсора? Например, результаты устаревшего API для отдыха, упомянутого в ответе?
Тони

@Tony: Это есть в документации. Посмотрите пример подсчета комнат.
Mitar

Стоит отметить , что в версиях 0.7, 0.7.1, 0.7.2 Метеора переключился на OpLog Наблюдайте драйвера для большинства запросов (исключения skip, $nearи $whereсодержащие запросы) , который является гораздо более эффективным в CPU нагрузке, пропускной способности сети и позволяет масштабирование приложения серверы.
imslavko

А как насчет случаев, когда не все пользователи видят одни и те же данные. 1. они подписались на разные темы .2. у них разные роли, поэтому в рамках одной и той же основной темы есть несколько сообщений, которые не должны доходить до них.
tgkprog

@debergalis относительно кэш недействительности, возможно , вы найдете идеи из моей бумаги vanisoft.pl/~lopuszanski/public/cache_invalidation.pdf стоит
qbolec

29

По моему опыту, с версией 0.7.0.1 использование многих клиентов с одновременным обменом огромной коллекцией в Meteor практически не работает. Я постараюсь объяснить почему.

Как описано в вышеупомянутом сообщении, а также на https://github.com/meteor/meteor/issues/1821 , сервер Meteor должен хранить копию опубликованных данных для каждого клиента в поле слияния . Это то, что позволяет происходить магии Meteor, но также приводит к тому, что любые большие общие базы данных постоянно хранятся в памяти процесса узла. Даже при использовании возможной оптимизации для статических коллекций, такой как в ( Есть ли способ сказать метеору, что коллекция статична (никогда не изменится)? ), Мы столкнулись с огромной проблемой с использованием ЦП и памяти процессом Node.

В нашем случае мы публиковали набор из 15 тысяч документов для каждого клиента, который был полностью статичным. Проблема в том, что копирование этих документов в клиентское поле слияния (в памяти) при подключении в основном доводило процесс Node до 100% ЦП почти на секунду и приводило к большому дополнительному использованию памяти. Это по своей сути немасштабируемо, потому что любой подключающийся клиент поставит сервер на колени (а одновременные соединения будут блокировать друг друга), а использование памяти будет линейно расти в зависимости от количества клиентов. В нашем случае каждый клиент вызвал дополнительные ~ 60 МБ использования памяти, хотя переданные необработанные данные составляли всего около 5 МБ.

В нашем случае, поскольку коллекция была статической, мы решили эту проблему, отправив все документы в виде .jsonфайла, который был сжат с помощью nginx, и загрузив их в анонимную коллекцию, что привело к передаче данных только ~ 1 МБ без дополнительного процессора. или память в процессе узла и гораздо более быстрое время загрузки. Все операции с этой коллекцией были выполнены с использованием _ids из гораздо меньших по размеру публикаций на сервере, что позволило сохранить большую часть преимуществ Meteor. Это позволило масштабировать приложение для большего числа клиентов. Кроме того, поскольку наше приложение в основном предназначено только для чтения, мы дополнительно улучшили масштабируемость, запустив несколько экземпляров Meteor за nginx с балансировкой нагрузки (хотя и с одним Mongo), поскольку каждый экземпляр Node является однопоточным.

Однако проблема совместного использования больших записываемых коллекций между несколькими клиентами - это инженерная проблема, которую необходимо решить Meteor. Вероятно, есть лучший способ, чем хранить копию всего для каждого клиента, но это требует серьезного размышления как проблема распределенных систем. Текущие проблемы с массовым использованием ЦП и памяти просто не масштабируются.


@Harry oplog в этой ситуации не имеет значения; данные были статичными.
Эндрю Мао,

Почему он не делает различий между копиями Minimongo на стороне сервера? Может, в 1.0 все изменилось? Я имею в виду, что обычно они такие же, как я надеюсь, даже функции, которые он вызывает, будут похожими (если я буду следовать этому, это то, что также хранится там и потенциально отличается)
MistereeDevlord,

@MistereeDevlord Различие изменений и кеширование клиентских данных сейчас отделено. Даже если у всех одни и те же данные и требуется только одно различие, кеш для каждого клиента отличается, потому что сервер не может обрабатывать их одинаково. Это определенно можно было бы сделать умнее по сравнению с существующей реализацией.
Эндрю Мао

@AndrewMao Как убедиться, что сжатые файлы gzip защищены при отправке клиенту, т.е. только зарегистрированный клиент может получить к ним доступ?
FullStack

4

Эксперимент, который вы можете использовать, чтобы ответить на этот вопрос:

  1. Установите тестовый метеор: meteor create --example todos
  2. Запустите его под инспектором Webkit (WKI).
  3. Изучите содержимое сообщений XHR, перемещающихся по сети.
  4. Обратите внимание на то, чтобы вся коллекция не перемещалась по проволоке.

Советы по использованию WKI можно найти в этом статье . Это немного устарело, но в основном все еще актуально, особенно для этого вопроса.


2
Объяснение механизма опроса: eventedmind.com/posts/meteor-liveresultsset
cmather

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.