Дмитрий Сергеевич (axshavan) wrote,
Дмитрий Сергеевич
axshavan

Deadlocks (Bitrix, MySQL, InnoDB, высокая нагрузка)

Хотел немного рассказать про одну штуку, с которой столкнулся недавно на работе. У нас, значит, вся система базируется на Битриксе. Так уж исторически сложилось. Активнее всего мы используем модули инфоблоков и магазина. Если с инфоблоками ещё хоть как-то более-менее всё идёт путём, то с магазином вообще жопа. Класс работы с заказом CSaleOrder мы уже полностью переписали, так как его возможностей нам не хватает. Началось это когда мы уткнулись в ограничение MySQL - не более 32 джойнов в запросе на 32-битной системе. Но да ладно, сейчас речь не об этом, а о корзине заказа.

Устройство примерно такое. Есть таблица с заказами b_sale_order, к ней привязана таблица с элементами корзины b_sale_basket по ORDER_ID, а к ней в свою очередь привязана таблица со свойствами элемента корзины b_sale_basket_props по BASKET_ID. Там хранятся только пользовательские свойства, то есть те, которых нет в обычном элементе корзины. В подавляющем большинстве интернет-магазинов стандартных свойств корзины обычно должно хватать. Но у нас-то магазин необычный, поэтому в свойства корзины мы засовываем очень много всего.

А вот как интересно Битрикс работает с корзиной и её свойствами. Ему, значит, в CSaleBasket::Update() передаётся массив, в котором ключи соответствуют названиям стандартных свойств, значения - новым значениям. Элемент с ключом PROPS сам является таким же индексированным массивом, и его смысл - пользовательские свойства элемента корзины. Во-первых, битрикс не обновляет ничего вообще, если надо обновить только свойства корзины (то есть приходится подставлять ещё одно какое-нибудь поле, чтоб он хотя бы что-то сделал). Во-вторых, свойства элемента корзины он обновляет очень интересно. Он сначала все существующие пользовательские свойства (относящиеся к обновляемому элементу корзины, а не вообще все) удаляет, а потом набивает заново, основываясь на массиве, который PROPS. То есть для того, чтоб обновить значение одного пользовательского свойства элемента корзины, надо сначала все их выбрать, чтоб они не потерялись. Но самое ужасное начинается при работе под высокой нагрузкой. Битрикс вообще, по всей видимости, не проектировался для работы под этой самой высокой нагрузкой.

В MySQL 5.1, кажется, этот досадный баг уже исправлен, но если у вас стоит MySQL 5.0, то, возможно, придётся с ним столкнуться. Так как при обновлении пользовательских свойств элемента корзины Битрикса они сначала все удаляются, потом по одному добавляются снова, нагрузка на эту таблицу очень высока. А у нас свойств этих много, работа с корзиной идёт плотная. Да, движло - InnoDB.

Итак, что иногда происходит. Одна транзакция (обозначу её Т1) после удаления некоторых записей из таблицы b_sale_basket_props начинает их добавлять. В MySQL это организовано следующим образом: сначала Т1 лочит индекс таблицы. Потом лочит автоинкремент, записывает ряд, отпускает автоинкремент. Снова лочит автоинкремент, записывает ещё ряд, отпускает автоинкремент. Ну, то есть Т1 лочит индекс таблицы на всё время, а автоинкремент - только при добавлении рядов, отдельно для каждого. Но индекс лочится не весь, а постранично, то есть та его часть, которая касается ближайших рядов.

В это время подваливает транзакция Т2. Ей тоже нужно обновить пользовательские свойства элемента корзины Битрикса. Она хочет начать с удаления. Транзакция Т1 всё ещё работает. Т2 лочит автоинкремент как раз в тот момент, когда Т1 только его отпустила, добавив очередной ряд. Что происходит дальше. Т2 хочет залочить индекс, но индекс держит Т1. Т1 же хочет продолжить запись рядов и хочет залочить автоинкремент, но автоинкремент залочен Т2. Тридцать секунд обе транзакции висят в таком нелепом положении, затем обе отваливаются с ошибкой. В статусе InnoDB обновляется информация о последнем замеченном дедлоке.

Сначала мы попробовали перед обновлением свойств корзины накладывать IX-лок на таблицу b_sale_basket_props. Потом - синхронизироваться с помощью функций GET_LOCK(), IS_FREE_LOCK() и RELEASE_LOCK(). Это вызывает безумные тормоза. Куча транзакций так и не дожидаются своей очереди и отваливаются с ошибками.

Тогда я просто переписал работу с корзиной и её свойствами.

Как был устроен испытательный стенд. Разумеется, все эти эксперименты проводились не на промышленном сервере, а на отладочном. Я таки сумел организовать на него периодические DDoS-атаки. Раз в несколько минут по крону инициировалось сразу несколько попыток обновить все пользовательские свойства первых двухсот элементов корзины в цикле в одной из баз. Практически каждый раз возникал дедлок.
После того, как я переделал работу с корзиной, дедлоки возникать перестали, несмотря на то, что атаки не прекратились. Посмотрим, как будут себя вести мои изменения после установки на промышленный сервер.
Tags: web, работа
Subscribe

  • Как у меня дела (четверг)

    Постепенно жизнь возвращается в прежнее, ещё до-хоум-офисное, русло. А надо-то было всего ничего, неделю походить на работу в офис. Но, разумеется,…

  • Два дня в офисе

    Ах да, забыл написать пост :) Совсем заработался. Так вот, результат PCR-теста пришёл отрицательный примерно через 25 часов после того, как я его…

  • И снова про работу

    Два дня — четверг и пятницу — сходил в офис поработать. Всё, мой многомесячный хоум-офис закончился, теперь снова работаю по-старинке, с личным…

  • Post a new comment

    Error

    Comments allowed for friends only

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 5 comments