Производительность агрегирования MongoDB медленнее, чем миллионы документов

фон

Наша система носит носитель и очень надежна, она тестировалась на нагрузку для обработки 5000 транзакций в секунду, и для каждой транзакции документ вставляется в единую коллекцию MongoDB (без обновлений или запросов в этом приложении, только). Это составляет ~ 700 млн. Документов в день, что является нашим эталоном.

Развертывание MongoDB еще не очерчено, у нас есть 1x replicaset с 1 master и 2 slaves, все из которых являются типами m2.2xlarge экземпляров на ec2. Каждый экземпляр поддерживается 1ТБ RAID0-полосой, состоящей из 8 томов (без PIOPS). Мы используем родной драйвер node -mongodb с собственным синтаксическим анализатором BSON С++ для оптимальной производительности записи и попытались соответствующим образом моделировать структуру документа.

Примечание

  • Документы крошечные (120 байт)
  • Документ включает в себя "ведро времени" (h [наш], ​​d [ay], m [onth], y [ear]) вместе с полем "t [ime]"
  • У нас есть индекс в коллекции для запроса с помощью "c [ustomer]" и "a", который является очень случайным, но не уникальным тегом
  • Мы рассмотрели разделение данных на отдельные коллекции, хотя в этом примере все данные горячие.
  • Мы также изучаем предварительную агрегацию, хотя это невозможно сделать в реальном времени.

требование

  • Для отчетности нам нужно рассчитать количество уникальных "а" тегов в месяц вместе с их итогами клиентом за любой данный период.
  • Отчет занимает около 60 секунд для запуска образца (полной коллекции) документов 9.5MM, хранящихся в течение 2 часов. Подробности ниже:

Документ

{
  _id: ObjectID(),
  a: ‘string’,
  b: ‘string’,
  c: ‘string’ or <int>,
  g: ‘string’ or <not_exist>,
  t: ISODate(),
  h: <int>,
  d: <int>,
  m: <int>,
  y: <int>
}

Индекс

col.ensureIndex({ c: 1, a: 1, y: 1, m: 1, d: 1, h: 1 });

запрос агрегации

col.aggregate([
    { $match: { c: 'customer_1', y: 2013, m: 11 } },
    { $group: { _id: { c: '$c', y: '$y', m: '$m' }, a: { $addToSet: '$a' }, t: { $sum: 1 } } },
    { $unwind: '$a' },
    { $group: { _id: { c: '$_id.c', y: '$_id.y', m: '$_id.m', t: '$t' }, a: { $sum: 1 } } },
    { $sort: { '_id.m': 1 } },
    {
        $project: {
            _id: 0,
            c: '$_id.c',
            y: '$_id.y', 
            m: '$_id.m',
            a: 1,
            t: '$_id.t'
        }
    },
    { $group: { _id: { c: '$c', y: '$y' }, monthly: { $push: { m: '$m', a: '$a', t: '$t' } } } },
    { $sort: { '_id.y': 1 } },
    {
        $project: {
            _id: 0,
            c: '$_id.c',
            y: '$_id.y', 
            monthly: 1
        }
    },
    { $group: { _id: { c: '$c' }, yearly: { $push: { y: '$y', monthly: '$monthly' } } } },
    { $sort: { '_id.c': 1 } },
    {
        $project: {
            _id: 0,
            c: '$_id.c',
            yearly: 1
        }
    }    
]);

результат агрегации

[
    {
        "yearly": [
            {
                "y": 2013,
                "monthly": [
                    {
                        "m": 11,
                        "a": 3465652,
                        "t": 9844935
                    }
                ]
            }
        ],
        "c": "customer_1"
    }
]

63181ms

Объяснение агрегации

{
        "cursor" : "BtreeCursor c_1_a_1_y_1_m_1_d_1_h_1",
        "isMultiKey" : false,
        "n" : 9844935,
        "nscannedObjects" : 0,
        "nscanned" : 9844935,
        "nscannedObjectsAllPlans" : 101,
        "nscannedAllPlans" : 9845036,
        "scanAndOrder" : false,
        "indexOnly" : true,
        "nYields" : 27,
        "nChunkSkips" : 0,
        "millis" : 32039,
        "indexBounds" : {
                "c" : [ [ "customer_1", "customer_1" ] ],
                "a" : [ [ { "$minElement" : 1 }, { "$maxElement" : 1 } ] ],
                "y" : [ [ 2013, 2013 ] ],
                "m" : [ [ 11, 11 ] ],
                "d" : [ [ { "$minElement" : 1 }, { "$maxElement" : 1 } ] ],
                "h" : [ [ { "$minElement" : 1 }, { "$maxElement" : 1 } ] ]
        }
}

вопросы

  • Учитывая высокую частоту вставок и нашу потребность в выполнении запросов агрегирования рангов с течением времени. Является ли практика времени хорошей практикой, учитывая, что приложение может вставлять 30MM-документы за один час?

  • Мы понимали, что MongoDB может запрашивать миллиарды документов за считанные секунды:

    • Несомненно, наш запрос агрегирования по документам 9.5MM может вернуться через 1сек или около того?
    • Используем ли мы правильную технику для достижения этого, а если нет, то где мы должны сосредоточить наши усилия на получении результатов отчета почти мгновенно?
    • Возможно ли это без осколков на этом этапе?
  • Будет ли MapReduce (параллельно) лучше альтернативой?

Ответы

Ответ 1

Вы задаетесь вопросом, почему ваша агрегировка занимает так много времени. Помимо пунктов, сделанных @Avish в его ответе (вы делаете некоторые ненужные шаги), вам также нужно учитывать ваши физические ресурсы и время, в которое идет время.

Вот часть вашего объяснения:

    "n" : 9844935,
    "nscannedObjects" : 0,
    "nscanned" : 9844935,
    "scanAndOrder" : false,
    "indexOnly" : true,
    "nYields" : 27,
    "millis" : 32039,

Следует отметить, что агрегация заняла 32 секунды (не 50), ей никогда не приходилось извлекать один документ, поскольку он получил всю информацию из самого индекса. Это не нужно было делать в памяти. Но он должен был дать 27 раз... Почему? Есть две причины, по которым возникают процессы чтения: один - когда есть ожидание записи (авторы имеют приоритет, а длинные чтения должны уступать им), либо произошла ошибка страницы - все операции должны выполняться, когда любые данные, к которым они пытаются получить доступ не находится в ОЗУ (это значит, что процесс не блокирует других от выполнения работы, пока они ожидают, что их данные будут загружены в ОЗУ).

Вопросы, которые приходят на ум, были: был ли холод DB? Соответствуют ли индексы в ОЗУ? Были ли записи в то же время, что читатели должны были бороться?

Я бы проверял, что индекс может поместиться в ОЗУ, запустите команду "touch" , чтобы убедиться, что она находится в ОЗУ, упростить мою агрегацию конвейер не делать ненужной работы, а затем запускать его снова, пару раз подряд и посмотреть, как выглядят тайминги.

Ответ 2

Я не понимаю, почему вам нужно $unwind значения a и почему вам нужно группировать по сумме в любой точке. Это также кажется ошибкой, так как для каждого разворотного значения a вы выведете то же значение t, рассчитанное для всего времени.

Насколько я понимаю, ваш запрос должен выглядеть так:

col.aggregate([
  // Pre-filter
  { $match: { /* ... */ } },

  // Pre-sort to aid in grouping
  { $sort: { c: 1, y: 1, m: 1, a: 1 },      

  // Group by month, customer and `a` to find unique `a` values and their totals
  { $group: { 
     _id: { c: '$c', y: '$y', m: '$m', a: '$a' },
     t: { $sum: 1 } 
    }
  },

  // Not sure if another sort is required at this point, I'd assume MongoDB
  // is smart enough to understand we're grouping by a subset of the original 
  // grouping key

  // Group by month and customer to count unique `a` values and grand total 
  { $group: {
    _id: { c: '$_id.c', y: '$_id.y', m: '$_id.m' },
    a: { sum: 1 }, // number of unique `a` values in group
    t: { sum: '$t' } // rolled-up total of all `a`-totals in group
  },

  // You can tack on further groupings by year and customer here,
  // although I believe these would be better done in the UI layer
]);

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

Ответ 3

Я бы предложил попробовать индекс на y, m и d (год, месяц, дата?) в этом порядке, поскольку они известны как int, а не текущие, которые c могут быть int или строку. Поскольку данные основаны на времени, может также иметь смысл.