Автор оригинала: Christian Amor Kvalheim.
В этой статье мы собираемся расследовать разницу в производительности между использованием Mongodb транзакции
или Двухфазная
совершить простую передачу банковского счета. Тесты были запущены на местном рабочем столе и на ограниченном наборе счетов, чтобы убедиться, что мы можем получить прерывистые конфликты пишета.
Ссылки
Узнайте больше о Монгодб
в следующих местах.
http://learnmongodbthehardway.com/ | Официальный сайт, чтобы узнать о MongoDB |
https://github.com/learn-mongodb-the-hardway | Официальный сайт GitHub Организация |
https://leanpub.com/mongodbschemadesign | Маленькая схемы дизайна книги |
https://gitter.im/mongodb/learnmongodbthehardway | Gitter Chatroom. |
http://christiankvalheim.com/ | Авторы веб-страницы |
https://twitter.com/christkv | Авторы Twitter. |
Код
Мы собираемся попытаться передать сумму денег между двумя аккаунтами и откатными, если это не удается. Есть две реализации. Первый основан на использовании Двухфазная
совершать без использования Multi-Document
транзакции. Вторая реализация использует новый Multi-Document
Поддержка транзакции в MongoDB 4.0.x или выше. Для простого ради которого мы представляем код, используя Mongo Shell
синтаксис.
Для фактического бенчмаркинга мы использовали рамки Kotlin/Java, которая находится в разработке, которые можно найти в HTTPS://github.com/learn-mongodb-the-hardway/mongodb-schema-simulator.
Многолетняя транзакция
Для Multi-Document
Подход транзакций мы повторяем транзакции о неудаче, чтобы она проходила. Ниже приведен некоторый пример код того, как это можно сделать в Mongo Shell
Отказ
var db = db.getSisterDB("bank"); var session = db.getMongo().startSession(); var accounts = session.getDatabase("bank").accounts; var transactions = session.getDatabase("bank").transactions; // Retries a transaction commit function retryUnknownTransactionCommit(session) { while(true) { try { // Attempt to commit the transaction session.commitTransaction(); break; } catch (err) { if (err.errorLabels != null && err.errorLabels.includes("UnknownTransactionCommitResult")) { // Keep retrying the transaction continue; } // The transaction cannot be retried, // return the exception return err; } } } function executeTransaction(session, from, to, amount) { while (true) { try { // Start a transaction on the current session session.startTransaction({ readConcern: { level: "snapshot" }, writeConcern: { w: "local" } }); // Debit the `from` account var result = accounts.updateOne( { name: from, amount: { $gte: amount } }, { $inc: { amount: -amount } }); // If we could not debit the account, abort the // transaction and throw an exception if (result.modifiedCount == 0) { session.abortTransaction(); throw Error("failed to debit the account [" + from + "]"); } // Credit the `from` account result = accounts.updateOne( { name: to }, { $inc: { amount: amount } }); // If we could not credit the account, abort the // transaction and throw an exception if (result.modifiedCount == 0) { session.abortTransaction(); throw Error("failed to credit the account [" + to + "]"); } // Insert a record of the transaction transactions.insertOne( { from: from, to: to, amount: amount, on: new Date() }); // Attempt to commit the transaction session.commitTransaction(); // Transaction was committed successfully break the while loop break; } catch (err) { // If we have no error labels rethrow the error if (err.errorLabels == null) { throw err; } // Our error contains UnknownTransactionCommitResult label if (err.errorLabels.includes("UnknownTransactionCommitResult")) { // Retry the transaction commit var exception = retryUnknownTransactionCommit(session, err); // No error, commit as successful, break the while loop if (exception == null) break; // Error has no errorLabels, rethrow the error if (exception.errorLabels == null) throw exception; // Error labels include TransientTransactionError label // Start while loop again, creating a new transaction if (err.errorLabels.includes("TransientTransactionError")) { continue; } // Rethrow the error throw exception; } // Error labels include TransientTransactionError label // Start while loop again, creating a new transaction if (err.errorLabels.includes("TransientTransactionError")) { continue; } // Rethrow the error throw err; } } } executeTransaction(session, "Peter", "Joe", 100);
Давайте сломаем код в грубых шагах. Сначала давайте посмотрим на Идеальное управление
метод.
function executeTransaction(session, from, to, amount) { while (true) { try { // Start a transaction on the current session session.startTransaction({ readConcern: { level: "snapshot" }, writeConcern: { w: "local" } }); // Debit the `from` account var result = accounts.updateOne( { name: from, amount: { $gte: amount } }, { $inc: { amount: -amount } }); // If we could not debit the account, abort the // transaction and throw an exception if (result.modifiedCount == 0) { session.abortTransaction(); throw Error("failed to debit the account [" + from + "]"); } // Credit the `from` account result = accounts.updateOne( { name: to }, { $inc: { amount: amount } }); // If we could not credit the account, abort the // transaction and throw an exception if (result.modifiedCount == 0) { session.abortTransaction(); throw Error("failed to credit the account [" + to + "]"); } // Insert a record of the transaction transactions.insertOne( { from: from, to: to, amount: amount, on: new Date() }); // Attempt to commit the transaction session.commitTransaction(); // Transaction was committed successfully break the while loop break; } catch (err) { // If we have no error labels rethrow the error if (err.errorLabels == null) { throw err; } // Error labels include TransientTransactionError label // Start while loop again, creating a new transaction if (err.errorLabels.includes("TransientTransactionError")) { continue; } // Our error contains the UnknownTransactionCommitResult label if (err.errorLabels.includes("UnknownTransactionCommitResult")) { // Retry the transaction commit var exception = retryUnknownTransactionCommit(session); // No error, commit as successful, break the while loop if (exception == null) break; // Error has no errorLabels, rethrow the error if (exception.errorLabels == null) throw exception; // Error labels include TransientTransactionError label // Start while loop again, creating a new transaction if (err.errorLabels.includes("TransientTransactionError")) { continue; } // Rethrow the error throw exception; } // Rethrow the error throw err; } } }
Сначала мы создаем новый транзакция
И тогда мы применяем операции, чтобы передать деньги с одного аккаунта на другой. Первое утверждение Дебеты
от
учетная запись.
var result = accounts.updateOne( { name: from, amount: { $gte: amount } }, { $inc: { amount: -amount } }); if (result.modifiedCount == 0) { session.abortTransaction(); throw Error("failed to debit the account [" + from + "]"); }
Если дебет не удается прервать транзакцию и бросить ошибку, чтобы сигнализировать дебет
операция не удалась. Далее мы кредит
к
учетная запись.
result = accounts.updateOne( { name: to }, { $inc: { amount: amount } }); if (result.modifiedCount == 0) { session.abortTransaction(); throw Error("failed to credit the account [" + to + "]"); }
Если кредит
Не удается прервать транзакцию и бросить ошибку, чтобы сигнализировать кредит
операция не удалась. Наконец мы запись
перевод.
transactions.insertOne( { from: from, to: to, amount: amount, on: new Date() });
Как только мы создали все операции, мы пытаемся совершить транзакция
Отказ
session.commitTransaction();
Если транзакция терпит неудачу, вот когда начинается веселье. Давайте посмотрим на Исключение
умение обращаться.
} catch (err) { // If we have no error labels rethrow the error if (err.errorLabels == null) { throw err; } // Error labels include TransientTransactionError label // Start while loop again, creating a new transaction if (err.errorLabels.includes("TransientTransactionError")) { continue; } // Our error contains the UnknownTransactionCommitResult label if (err.errorLabels.includes("UnknownTransactionCommitResult")) { // Retry the transaction commit var exception = retryUnknownTransactionCommit(session); // No error, commit as successful, break the while loop if (exception == null) break; // Error has no errorLabels, rethrow the error if (exception.errorLabels == null) throw exception; // Error labels include TransientTransactionError label // Start while loop again, creating a new transaction if (err.errorLabels.includes("TransientTransactionError")) { continue; } // Rethrow the error throw exception; } // Rethrow the error throw err; }
Если у нас есть объект ошибки без ErrorLabels
Мы образуемся, так как транзакция не может быть получена. Однако, если у нас есть ErrorLabels
Нам нужно осмотреть их.
Если этикетка Трансэртеррансдействорор
присутствует мы не можем повторить текущую транзацию, чтобы мы Продолжить
Будучи петлями, заставляя создание новой транзакции.
Однако, если ErrorLabels
Содержит метку UnvernuretransactionCommitresult
Мы можем повторить текущую транзакцию. Мы делаем это, позвонив в RetryunknowntransactionCommit
Функция с текущим сеансом. Давайте посмотрим на функцию в деталях.
// Retries a transaction commit function retryUnknownTransactionCommit(session) { while(true) { try { // Attempt to commit the transaction session.commitTransaction(); break; } catch (err) { if (err.errorLabels != null && err.errorLabels.includes("UnknownTransactionCommitResult")) { // Keep retrying the transaction continue; } // The transaction cannot be retried, // return the exception return err; } } }
Пока session.committransaction ()
Вызов возвращает UnvernuretransactionCommitresult
Мы продолжаем повторять транзакцию. Если совершение транзакции успешно, мы вырвались из цикла возвращающихся NULL. Если ошибка возвращается, отличается от UnvernuretransactionCommitresult
Мы возвращаем ошибку.
Вернувшись в точку, где мы называем RetryunknowntransactionCommit
Функция мы видим следующую логику.
// Retry the transaction commit var exception = retryUnknownTransactionCommit(session); // No error, commit as successful, break the while loop if (exception == null) break; // Error has no errorLabels, rethrow the error if (exception.errorLabels == null) throw exception; // Error labels include TransientTransactionError label // Start while loop again, creating a new transaction if (err.errorLabels.includes("TransientTransactionError")) { continue; } // Rethrow the error throw exception;
Если возвращено Исключение
это null
Мы разбиваем в то время как
петля, поскольку наша транзакция была успешно совершена. Если Исключение
не включает ErrorLabels
Мы Retrow исключение. С другой стороны, если исключение содержит ErrorLabels
И этикетки включают метку Трансэртеррансдействорор
Мы не можем повторить текущую транзацию, чтобы мы Продолжить
Будучи петлями, заставляя создание новой транзакции.
Два фазных фиксации
Две фазы
Подход Commit использует другой подход с использованием двойного бухгалтерского учета для обеспечения последовательных передач учетной записи. Давайте посмотрим на автономный пример ниже, что витрины, как можно реализовать шаблон с использованием Mongo Shell
Отказ
var db = db.getSisterDB("bank"); db.dropDatabase(); var accounts = db.accounts; var transactions = db.transactions; accounts.insertOne({ _id: 1, name: "Joe Moneylender", balance: 1000, pendingTransactions:[] }); accounts.insertOne({ _id: 2, name: "Peter Bum", balance: 1000, pendingTransactions:[] }); function cancel(id) { transactions.updateOne( { _id: id }, { $set: { state: "canceled" } } ); } function rollback(from, to, amount, id) { // Reverse debit accounts.updateOne({ name: from, pendingTransactions: { $in: [id] } }, { $inc: { balance: amount }, $pull: { pendingTransactions: id } }); // Reverse credit accounts.updateOne({ name: to, pendingTransactions: { $in: [id] } }, { $inc: { balance: -amount }, $pull: { pendingTransactions: id } }); cancel(id); } function cleanup(from, to, id) { // Remove the transaction ids accounts.updateOne( { name: from }, { $pull: { pendingTransactions: id } }); // Remove the transaction ids accounts.updateOne( { name: to }, { $pull: { pendingTransactions: id } }); // Update transaction to committed transactions.updateOne( { _id: id }, { $set: { state: "done" } }); } function executeTransaction(from, to, amount) { var transactionId = ObjectId(); transactions.insert({ _id: transactionId, source: from, destination: to, amount: amount, state: "initial" }); var result = transactions.updateOne( { _id: transactionId }, { $set: { state: "pending" } } ); if (result.modifiedCount == 0) { cancel(transactionId); throw Error("Failed to move transaction " + transactionId + " to pending"); } // Set up pending debit result = accounts.updateOne({ name: from, pendingTransactions: { $ne: transactionId }, balance: { $gte: amount } }, { $inc: { balance: -amount }, $push: { pendingTransactions: transactionId } }); if (result.modifiedCount == 0) { rollback(from, to, amount, transactionId); throw Error("Failed to debit " + from + " account"); } // Setup pending credit result = accounts.updateOne({ name: to, pendingTransactions: { $ne: transactionId } }, { $inc: { balance: amount }, $push: { pendingTransactions: transactionId } }); if (result.modifiedCount == 0) { rollback(from, to, amount, transactionId); throw Error("Failed to credit " + to + " account"); } // Update transaction to committed result = transactions.updateOne( { _id: transactionId }, { $set: { state: "committed" } } ); if (result.modifiedCount == 0) { rollback(from, to, amount, transactionId); throw Error("Failed to move transaction " + transactionId + " to committed"); } // Attempt cleanup cleanup(from, to, transactionId); } executeTransaction("Joe Moneylender", "Peter Bum", 100);
Давайте сломаемся функцией Идеальное управление
Шаг за шагом и обсудите, что происходит на каждом шаге, и как восстановить от ошибки.
transactions.insert({ _id: transactionId, source: from, destination: to, amount: amount, state: "initial" });
Первый шаг вставляет новый документ в транзакции
Коллекция, которая содержит информацию о передаче, которую мы собираемся выполнять. Состояние транзакция
установлен на Первоначальный
Сигнализация мы только начали процесс.
var result = transactions.updateOne( { _id: transactionId }, { $set: { state: "pending" } } ); if (result.modifiedCount == 0) { cancel(); throw Error("Failed to move transaction " + transactionId + " to pending"); }
Далее мы пытаемся перевернуть транзакцию в состояние В ожидании
Отказ Если это не удается ( Результат. ModiDedCount
) Мы пытаемся отменить транзакцию, вызывающую функцию Отмена
Отказ Давайте посмотрим на то, что Отмена
Функция делает.
function cancel(id) { transactions.updateOne( { _id: id }, { $set: { state: "canceled" } } ); }
Функция в основном пытается установить Государство
транзакции до отменен
Отказ После возвращения из Отмена
Функция, мы бросаем исключение сигнализацию вызывающего абонента Идеальное управление
Функция, которая не удалась.
Однако, если мы успешным при установке транзакция
Государство в В ожидании
Мы можем начать процесс применения транзакция
Отказ
result = accounts.updateOne({ name: from, pendingTransactions: { $ne: transactionId }, balance: { $gte: amount } }, { $inc: { balance: -amount }, $push: { pendingTransactions: transactionId } }); if (result.modifiedCount == 0) { rollback(from, to, amount, transactionId); throw Error("Failed to debit " + from + " account"); }
Мы смотрим на от
Учетная запись, обеспечение того, чтобы PendendsTransactions
Массив не содержит трансзариемый
И что аккаунт Баланс
это Большой или равный
на сумму, которую мы собираемся в дебету. Если мы сочтены документами, мы собираемся дебет
аккаунт Баланс
По сумма
и нажмите трансзариемый
к PendendsTransactions
множество.
Если документ не был изменен, мы знаем Обновить
от учетной записи не удалось, и нам нужно позвонить в Откат
Функция, чтобы отменить транзакцию, прежде чем мы выбрасываем исключение, сигнализирующее приложение, что перенос не удался. Давайте посмотрим на Откат
функция.
function rollback(from, to, amount, id) { // Reverse debit accounts.updateOne({ name: from, pendingTransactions: { $in: [id] } }, { $inc: { balance: amount }, $pull: { pendingTransactions: id } }); // Reverse credit accounts.updateOne({ name: to, pendingTransactions: { $in: [id] } }, { $inc: { balance: -amount }, $pull: { pendingTransactions: id } }); cancel(id); }
Чтобы откатиться транзакцией, нам нужно обратить вспять на от
и к
учетные записи. Сначала мы должны удалить транзакцию из от
Учетная запись, возвращая зарезервированные сумма
к Баланс
Отказ
accounts.updateOne({ name: from, pendingTransactions: { $in: [id] } }, { $inc: { balance: amount }, $pull: { pendingTransactions: id } });
Мы обновим учетную запись, если она содержит транзакция
Подходя на Имя
И если PendendsTransactions
Массив содержит Идентификатор транзакции
Отказ Если документы совпадают, мы добавим сумма
к Баланс
и удалить Идентификатор транзакции
от PendendendTransaction
Отказ Далее нам нужно обратить вспять транзакция
на к
аккаунт также.
accounts.updateOne({ name: to, pendingTransactions: { $in: [id] } }, { $inc: { balance: -amount }, $pull: { pendingTransactions: id } });
Единственное отличие от от
Учет в том, что мы вычесть сумма
от Баланс
отчета. Наконец мы называем Отмена
Метод установить транзакцию Государство
к отменен
Отказ Возвращаясь к Идеальное управление
Функция позволяет посмотреть на следующее утверждение.
result = accounts.updateOne({ name: to, pendingTransactions: { $ne: transactionId } }, { $inc: { balance: amount }, $push: { pendingTransactions: transactionId } }); if (result.modifiedCount == 0) { rollback(from, to, amount, transactionId); throw Error("Failed to credit " + to + " account"); }
Так же, как в случае применения трансзариемый
к от
аккаунт мы гарантируем учетная запись
еще не содержит трансзариемый
В PendendendTransaction
Отказ Если это не существует в PendendendTransaction
Мы добавляем сумма
к Баланс
и нажмите трансзариемый
к PendendsTransactions
множество.
Если документ не удается обновить, мы называем Откат
Функция, поскольку мы делали ранее, а затем выбросьте исключение, чтобы сигнализировать о приложении, транзакция не удалась.
Наконец мы собираемся перевернуть состояние транзакция
к совершил
Отказ
result = transactions.updateOne( { _id: transactionId }, { $set: { state: "committed" } } ); if (result.modifiedCount == 0) { rollback(from, to, amount, transactionId); throw Error("Failed to move transaction " + transactionId + " to committed"); }
Если это не удается, мы называем Откат
функция, чтобы изменить транзакцию. Наконец мы называем Очистка
функция. Давайте посмотрим на то, что делает функция.
function cleanup(from, to, id) { // Remove the transaction ids accounts.updateOne( { name: from }, { $pull: { pendingTransactions: id } }); // Remove the transaction ids accounts.updateOne( { name: to }, { $pull: { pendingTransactions: id } }); // Update transaction to committed transactions.updateOne( { _id: id }, { $set: { state: "done" } }); }
Первое обновление удалит трансзариемый
от от
учетная запись. Второе обновление сделает то же самое для к
учетная запись. Наконец последнее обновление установит транзакцию Государство
к сделано
завершение передачи между двумя счетами.
Выполнение производительности и анализ
Давайте запустим два сравнительных ориентира, чтобы посмотреть на два специфических сценария трафика, применяемые к обоим транзакция
подход, а также Двухфазная
подход.
Первый сценарий – это тот, где у нас есть Одиночная нить
Выполнение передачи учетной записи каждый Миллисекан
для 35 секунд
Отказ
Второй сценарий мы запускаем одинаковую передачу каждого Миллисекан
Но используя Пять ниток
для 35 секунд
Отказ
Мы используем инструмент симулятора схемы в HTTPS://github.com/learn-mongodb-the-hardway/mongodb-schema-simulator для генерации нагрузки и записи результатов.
Мы берем измерение от От 5 до 35 секунд
Чтобы избежать начального периода кеш разматывает
на Монгодб
а также Java Jit Warmup
Отказ
Одиночная нить
Для сценария единого потока мы получаем следующие результаты.
График выше показывает результаты транзакция
подход.
График выше показывает результаты Двухфазная
подход. Давайте возьмем ключ и поместите их в таблицу для ослабления сравнения.
Среднее мс. | 2.02 мс. | 4,35 мс. |
мин мс. | 1.6335 мс | 3,2685 мс. |
Макс мс | 47.2947 мс | 70,4311 мс |
95 процентилей MS. | 2,38 мс. | 5,45 мс. |
99 процентилей MS. | 2,64 мс. | 6,02 мс |
Я> Среднее
это Среднее геометрическое
Отказ Среднее геометрическое означает среднее или среднее, что указывает на центральную тенденцию или типичное значение набора чисел Я> мин
это минимальное значение, найденное в наборе. Я> Макс
Максимальное значение найдено в наборе. Я> PTH процентиль
это Процент
данных, которые меньше значения. А 95 процентилей
100
будет означать 95%
значений в наборе VAS ниже 100
Отказ
Давайте сломаем цифры.
-
Среднее
Для подхода к транзакциям есть~ 2x
ниже. -
мин
Для подхода к транзакциям есть~ 2x
ниже. -
Макс
Для подхода к транзакциям есть~ 2x
ниже. -
95 процентилей
Для подхода к транзакциям есть~ 2x
ниже. -
99 процентилей
Для подхода к транзакциям есть~ 2x
ниже.
Глядя на это, мы видим, что поддержка транзакции примерно в два раза быстрее, чем Двухфазная
Сделайте подход. Это имеет смысл, так как объем операций, которые нам нужно выполнить против Монгодб
закончить Двухфазная
Commit, больше, чем то, что нужно для транзакция
подход.
Учитывая это, мы могли бы сделать вывод, что транзакция
Подход превосходит Двухфазная
подход. Но держите своих лошадей. Давайте посмотрим, что произойдет, когда мы заставляем транзакция
столкновения путем увеличения нагрузки.
Несколько потоков
Для сценария нескольких потоков мы получаем следующие результаты.
Первый график показывает передачу учетной записи, используя Mongodb 4.0.x
Подход поддержки транзакции.
Второй график показывает передачу учетной записи, используя Двухфазная
Сделайте подход. Давайте схватить главные числа и сравнивать их.
Среднее мс. | 7,42 мс. | 7,79 мс. |
мин мс. | 1,6434 мс. | 4.7189 мс. |
Макс мс | 87.0411 MS. | 116,416 мс |
95 процентилей MS. | 19,98 мс | 11,23 мс. |
99 процентилей MS. | 30,62 мс | 34,54 мс. |
Давайте сломаем цифры.
-
Среднее
очень похоже между двумя подходами. -
мин
Для подхода транзакции ниже. -
Макс
Для подхода транзакции ниже. -
95 процентилей
выше для подхода транзакции. -
99 процентилей
очень похоже между двумя подходами.
Мы можем видеть, что Двухфазная
совершать в целом лучшую общую характеристику производительности из-за 95 процентилей
говоря …| 95% операций занял
19,98 мс или меньше по сравнению с
30,62 мс для подхода к транзакциям.
Причина Двухфазная
Подход сейчас конкурентоспособен с транзакция
Подход, связан с транзакция
Подход, начинающий опыт писать конфликты
Из-за более высокой одновременной нагрузки, заставив ориентир, чтобы повторить транзакции, пока они не будут успешными.
Заключение
С начальной проверки казалось бы, что использование транзакции
будет все еще жизнеспособным способом вперед, так как разница не является огромной между двумя подходами. Однако прежде чем мы решим, мы должны принимать во внимание пару дополнительных факторов.
- Транзакции работают только на
реплизетки
какMongodb 4.0.x
Отказ -
Максимум
Время работы для транзакции составляет минуту (что-либо на минуту становится прерванным). - Блокировка документов для транзакций может привести к узким местам производительности на документах, которые получают много пишет, поскольку все транзакции получают сериализацию и должны дождаться их очередь.
Однажды Монгодб
Поддерживает Шардированный
транзакции ваши Воспроизведение
транзакции могут в конечном итоге распределить транзакции, которые включают несколько осколки
Отказ Это представляет большой штраф производительности, а также новые и очень сложные режимы отказа, любые, которые могут привести к сбою транзакции. В этом случае его очень вероятно, Двухфазная
Подход Commit, значительно превзойдет подход транзакций.