Как распараллелить запросы с MySQL + PHP + nginx? - PullRequest
0 голосов
/ 17 января 2020

Недавнее обнаружение

Из всего, что я пробовал, я заменил свой профиль JMeter на пользовательский JavaScript, который поочередно поразил каждую из моих конечных точек API в бесконечном l oop, а затем запустил этот скрипт параллельно в разных браузерах (один Firefox, один Chrome, один Safari) - чтобы попытаться исключить проблемы, связанные со всеми моими соединениями, исходящими из одного и того же источника (один и тот же пользовательский агент, одни и те же файлы cookie, один и тот же идентификатор сеанса и т. д. c)

Когда я это сделал, я заметил, что все мои проблемы исчезли. Запросы выполнялись параллельно, и приложение было намного более отзывчивым, чем JMeter, по вашему мнению,

Мне кажется невозможным, что JMeter будет сериализовать запросы, поскольку это де-факто стандарт для нагрузочного тестирования. Поэтому я начал пытаться воспроизвести поведение

В попытке воссоздать JMeter я создал следующие два PHP сценария, которые (надеюсь) имитировали мое приложение Yii:

медленно. php

<?php

session_start();

$_SESSION['some'] = 'value';
// Yii is calling session_write_close() almost immediately after
// the session is initialized, but to try and exacerbate issues,
// I've commented it out:
// session_write_close();

$dsn = "mysql:host=localhost;dbname=platypus;unix_socket=/tmp/mysql.sock";
$pdo = new PDO($dsn, "user", "password");
// Yii was using whatever the default persistence behavior was,
// but to try and exacerbate issues I set this flag:
$pdo->setAttribute(PDO::ATTR_PERSISTENT, true);
// Simulate a query running for 1 second by issuing a 1-second sleep
$pdo->query("DO SLEEP(1)");

echo "Done";

быстро. php

<?php

session_start();

$_SESSION['some'] = 'value';

$dsn = "mysql:host=localhost;dbname=platypus;unix_socket=/tmp/mysql.sock";
$pdo = new PDO($dsn, "user", "password");
$pdo->setAttribute(PDO::ATTR_PERSISTENT, true);
// Simulate a query running for 0.1 seconds
$pdo->query("DO SLEEP(0.1)");

echo "Done";

Бег JMeter против этих двух новых конечных точек не было сериализации запросов. Все бежало параллельно. fast. php всегда возвращаются через 100-150 мс и медленно. php всегда возвращаются через 1000-1050 мс, даже когда я масштабировал до 3, 4 и 5 потоков. Я был в состоянии наблюдать, как все разваливается в 11 потоках, но это потому, что я превысил число рабочих потоков в PHP

Итак, подведем итог:

  • Эта проблема возникает только при профилировании моего API с помощью JMeter и не присуща самому приложению
  • Проблема не просто в ошибке JMeter, а в том, как она связана на мое приложение или Yii 1.1
  • Я пытался, но не смог придумать минимальный случай повторения

Несмотря на то, что при профилировании с другими инструментами не было проблемы, многие люди ответили и дал много полезной информации:

  • Избегайте постоянных соединений в PHP ( может вызывать множественные запросы на совместное использование соединения, вероятно, нет)
  • Избегать сеанса блокировка путем вызова session_write_close() как можно раньше
  • Убедитесь, что у вас достаточно PHP рабочих потоков для обработки количества одновременных подключений
  • MySQL полностью поддерживает параллельные запросы (если оборудование может справиться с этим)
  • Остерегайтесь блокировки таблиц (любая транзакция с оператором UPDATE может потенциально заблокировать таблицы)
  • MyISAM выполняет блокировку на уровне таблицы вместо блокировки на уровне строки

Original Post

Я унаследовал веб-приложение и пытаюсь разобраться в его профиле производительности, чтобы начать его оптимизацию по скорости.

Одна вещь, которую я довольно рано заметили, что запросы к серверу сериализуются. Например, предположим, что у меня есть три конечные точки с временем отклика, например, так:

/api/endpoint1 --> 50ms
/api/endpoint2 --> 1000ms
/api/endpoint3 --> 100ms

Если я попал в одну конечную точку, я измеряю соответствующее время отклика. Но когда я настраиваю сценарий, чтобы он ударил все 3 сразу, я иногда вижу что-то вроде следующего:

endpoint1: 50ms
endpoint2: 1050ms
endpoint3: 1150ms

Ясно, что вызов к конечной точке 3 был поставлен в очередь и ждал, пока вызов к конечной точке 2 завершится sh до того, как он получил ответ.

Моя первая мысль была о том, что это должно быть тривиально решено с помощью многопоточности, поэтому я взглянул на конфигурацию сервера. Диспетчер процессов PHP -FPM был установлен на «Dynami c» с «start_servers», равным 1, «max_children», равным 5, и «max_spare_servers», равным 2. Для целей тестирования я поменял его на «stati c», поэтому что 5 PHP процессов останутся открытыми для параллельной обработки соединений (больше, чем 3 для числа конечных точек, которые я выполняю, поэтому они должны иметь возможность обрабатывать одновременно)

Это не повлияло на производительность, поэтому я посмотрел на мой nginx конфиг. «worker_processes» был установлен в 1 с «worker_connections», установленным в 1024. Я знаю, что nginx использует модель события l oop, поэтому он не должен блокироваться, пока он ждет ответа от PHP -FPM. Но на всякий случай , я увеличил "worker_processes" до 5

Тем не менее, никакого эффекта. Итак, затем я посмотрел на базу данных. Все 3 конечные точки должны были попасть в базу данных, и я знаю, что время отклика в 1000 мс в основном тратится на ожидание долгосрочного запроса к базе данных. Я попытался установить "thread_pool-size" на 5, а также в MySQL REPL, который я установил "innodb_parallel_read_threads" и "mysqlx_min_worker_threads" на 5

Тем не менее, мои запросы были сериализованы. Когда я вошел в систему MySQL REPL и набрал show processlist; во время работы моего скрипта (используя значение true * l oop для многократного попадания в эти 3 конечные точки API), я заметил, что к веб-приложению всегда было только одно соединение user

К сожалению, я не уверен, что моя проблема связана с базой данных (не допускается более одного подключения), с PHP -FPM (не обрабатывается более одного запроса одновременно) или с nginx (не пересылать более одного запроса одновременно PHP -FPM). Я также не уверен, как выяснить, какой из них действует как узкое место

Обновление

Оглядываясь назад, я обнаружил этот пост , который, кажется, предполагает, что MySQL не поддерживает параллельные запросы от одного и того же пользователя (например, от пользователя веб-приложения)

Это правда? Конечно, такой вездесущий механизм баз данных не будет иметь такого недостатка в производительности, особенно учитывая, как часто он используется с AWS для масштабируемых приложений. Я понимаю, что для простых запросов «чтение с диска» распараллеливание их запросов не повысит производительность, поскольку им просто придется стоять в очереди в ожидании дискового ввода-вывода, но современные базы данных имеют кэш-память в памяти, и большинство действительно медленные операции, такие как сортировка файлов, обычно происходят в памяти. Нет причин, по которым связанный с диском запрос не может выполняться параллельно (сделать запрос на диск и начать ожидание ввода-вывода), пока связанный с процессором запрос занят сортировкой таблицы в ОЗУ. Переключение контекста может немного замедлить связанные с процессором запросы, но если замедление их с 1000 мс до 1200 мс означает, что мой 5 мс запрос может быть выполнен за 5 мс, думаю, это того стоит.

Мои запросы

Вот запросы для моих 3 конечных точек. Обратите внимание, что перечисленные временные интервалы - это время отклика для полного конвейера HTTP (от запроса браузера к ответу), так что это включает в себя служебные данные от nginx и PHP плюс любые пост- обработка запроса выполнена в PHP. Тем не менее, запрос в конечной точке 2 составляет 99% времени выполнения и блокирует базу данных, так что конечные точки 1 и 3 ставятся в очередь вместо быстрого возврата.

конечная точка1 (50 мс)

SELECT * FROM Widget WHERE id = 1 LIMIT 1

(Обратите внимание, что 50 мс - это полное время ответа для конечной точки, а не время выполнения запроса. Этот запрос явно имеет порядок микросекунд)

endpoint2 ( 1000 мс)

USE platypus;
SELECT `t`.`(49 fields)` AS `t0_cX`,
       `site`.`(29 fields)` AS `t2_cX`,
       `customer`.`(26 fields)` AS `t4_cX`,
       `domain`.`(20 fields)` AS `t6_c0`,
       `domain-general_settings`.`(18 fields)` AS `t8_cX`,
       `domain-access_settings`.`(17 fields)` AS `t9_cX`,
       `customer-general_settings`.`(18 fields)` AS `t10_cX`,
       `customer-access_settings`.`(17 fields)` AS `t11_cX`,
       `site-general_settings`.`(18 fields)` AS `t12_cX`,
       `site-access_settings`.`(17 fields)` AS `t13_cX`,
       `backup_broadcast`.`(49 fields)` AS `t14_cX`,
       `playlists`.`(11 fields)` AS `t16_cX`,
       `section`.`(10 fields)` AS `t17_cX`,
       `video`.`(16 fields)` AS `t18_cX`,
       `general_settings`.`(18 fields)` AS `t19_cX`,
       `access_settings`.`(17 fields)` AS `t20_cX`,
FROM   `broadcast` `t`
       LEFT OUTER JOIN `site` `site`
                    ON ( `t`.`site_id` = `site`.`id` )
       LEFT OUTER JOIN `customer` `customer`
                    ON ( `site`.`customer_id` = `customer`.`id` )
       LEFT OUTER JOIN `domain` `domain`
                    ON ( `customer`.`domain_id` = `domain`.`id` )
       LEFT OUTER JOIN `generalsettings` `domain-general_settings`
                    ON ( `domain`.`general_settings_id` =
                         `domain-general_settings`.`id` )
       LEFT OUTER JOIN `accesssettings` `domain-access_settings`
                    ON
       ( `domain`.`access_settings_id` = `domain-access_settings`.`id` )
       LEFT OUTER JOIN `generalsettings` `customer-general_settings`
                    ON ( `customer`.`general_settings_id` =
                         `customer-general_settings`.`id` )
       LEFT OUTER JOIN `accesssettings` `customer-access_settings`
                    ON ( `customer`.`access_settings_id` =
                         `customer-access_settings`.`id` )
       LEFT OUTER JOIN `generalsettings` `site-general_settings`
                    ON ( `site`.`general_settings_id` =
                         `site-general_settings`.`id` )
       LEFT OUTER JOIN `accesssettings` `site-access_settings`
                    ON ( `site`.`access_settings_id` =
                         `site-access_settings`.`id` )
       LEFT OUTER JOIN `broadcast` `backup_broadcast`
                    ON ( `t`.`backup_broadcast_id` = `backup_broadcast`.`id` )
                       AND ( backup_broadcast.deletion IS NULL )
       LEFT OUTER JOIN `playlist_broadcast` `playlists_playlists`
                    ON ( `t`.`id` = `playlists_playlists`.`broadcast_id` )
       LEFT OUTER JOIN `playlist` `playlists`
                    ON
       ( `playlists`.`id` = `playlists_playlists`.`playlist_id` )
       LEFT OUTER JOIN `section` `section`
                    ON ( `t`.`section_id` = `section`.`id` )
       LEFT OUTER JOIN `video` `video`
                    ON ( `t`.`video_id` = `video`.`id` )
                       AND ( video.deletion IS NULL )
       LEFT OUTER JOIN `generalsettings` `general_settings`
                    ON ( `t`.`general_settings_id` = `general_settings`.`id` )
       LEFT OUTER JOIN `accesssettings` `access_settings`
                    ON ( `t`.`access_settings_id` = `access_settings`.`id` )
WHERE
(
    (
        t.id IN (
    SELECT `broadcast`.id FROM broadcast
       LEFT JOIN `mediashare` `shares`
              ON ( `shares`.`media_id` = `broadcast`.`id` )
                 AND `shares`.media_type = 'Broadcast'
    WHERE 
    (
        (
            broadcast.site_id IN(
                '489', '488', '253', '1083', '407'
            )
            OR
            shares.site_id IN(
                '489', '488', '253', '1083', '407'
            )
        )
    )
        )
    )
    AND
    (
        (
            (
                (t.deletion IS NULL)
            )
        )
        AND
        (
            IF(
                t.backup_mode IS NULL,
                t.status,
                IF(
                    t.backup_mode = 'broadcast',
                    backup_broadcast.status,
                    IF(
                        t.backup_mode = 'embed',
                        IF(
                            t.backup_embed_status,
                            t.backup_embed_status,
                            IF(
                                '2020-01-08 16:34:52' < t.date,
                                1,
                                IF(
                                    t.date > Date_sub(
                                        '2020-01-08 16:34:52',
                                        INTERVAL IF(t.expected_duration IS NULL, 10800, t.expected_duration) second
                                    ),
                                    10,
                                    12
                                )
                            )
                        ),
                        t.status
                    )
                )
            ) != 0
        )
    )
)
LIMIT  10;

Этот запрос выполняется приблизительно 1000 мс, но PHP для конечной точки чрезвычайно прост (запустите запрос, верните результаты как JSON) и добавьте только пара миллисекунд служебных данных

конечная точка 3 (100 мс)

SELECT * FROM platypus.Broadcast
    WHERE deletion IS NULL
    AND site_id IN (SELECT id FROM platypus.Site
               WHERE deletion IS NULL
                 AND customer_id = 7);

Здесь есть дополнительная проверка на стороне PHP, которая заставляет эту конечную точку принимать 100 мс. SQL, как вы можете видеть, все еще довольно прост.

Создание операторов таблицы

Поскольку в StackOverflow существует ограничение длины записи, я не могу показать CREATE TABLE для каждой таблицы. коснулся конечной точки 2, но я могу показать хотя бы одну таблицу. Другие используют тот же механизм.

CREATE TABLE `Widget` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `widget_name` varchar(255) NOT NULL,
  `widget_description` varchar(255) NOT NULL,
  `status` varchar(255) NOT NULL,
  `date_created` datetime NOT NULL,
  `date_modified` datetime NOT NULL,
  `auto_play` varchar(255) NOT NULL,
  `on_load_show` varchar(255) NOT NULL,
  `widget_content_source` varchar(255) NOT NULL,
  `associated_sites` text NOT NULL,
  `author_id` int NOT NULL,
  `associated_sections` text,
  `after_date` datetime DEFAULT NULL,
  `before_date` datetime DEFAULT NULL,
  `show_playlists` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `is_classic` tinyint(1) NOT NULL,
  `default_site` int unsigned DEFAULT NULL,
  `auth_code_url` varchar(255) DEFAULT NULL,
  `widget_layout_id` int unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_Widget_widget_layout_id_WidgetLayout_id` (`widget_layout_id`),
  CONSTRAINT `fk_Widget_widget_layout_id_WidgetLayout_id` FOREIGN KEY (`widget_layout_id`) REFERENCES `WidgetLayout` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1412 DEFAULT CHARSET=utf8

Примечание

Обратите внимание, что конечная точка 2 даже не касается таблицы виджетов, но конечная точка 1 (которая касается ТОЛЬКО таблицы виджетов) также ставится в очередь , Это исключает возможность блокировки таблицы.

При просмотре списка процессов в MySQL пользователь базы данных устанавливает только одно соединение с базой данных. Следовательно, проблема может быть l ie в моей конфигурации PHP.

Объяснение для запроса 2

Прилагается запрос EXPLAIN SELECT ... для конечной точки 2

enter image description here

Упрощенные эксперименты

Чтобы попытаться определить, где разваливается параллельный конвейер, я создал два простых сценария:

сон. php

<?php
sleep(5);
echo "Done sleeping";

возвращение. php

<?php
echo "That's all";

Делать это (спать в PHP) и Выполнение моего сценария, чтобы поразить эти две конечные точки с помощью трех потоков, я не увидел никаких проблем. возврат. php всегда возвращался через ~ 11 миллисекунд, несмотря на сон. php, принимая в среднем 5066. Затем я попытался сделать сон в MySQL:

сон. php

<?php
$pdo = new PDO("...", "user", "pass");
$pdo->query("DO SLEEP(5)");
echo "Done sleeping";

Это, снова , не было никаких проблем , Спящая конечная точка не блокировала не спящую.

Это означает, что проблема не существует на уровне nginx, PHP или PDO, но существует должен быть какой-то блокировкой таблицы или строки. Я собираюсь снова включить общий журнал запросов и сканировать каждый запрос, который выполняется, чтобы увидеть, смогу ли я выяснить, что происходит.

Окончательное обновление

Если Вы прокрутите вверх до «Недавнего обнаружения» в верхней части этого поста, и вы заметите, что я изменил свое понимание проблемы.

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

Ответы [ 4 ]

4 голосов
/ 31 января 2020

MySQL + PHP + Apache всегда хорошо справлялся с выполнением отдельных операторов SQL параллельно. Если отдельные пользователи выдают HTTP-запросы, они, естественно, будут быстро go - Apache (вероятно, последовательно, но быстро) и попадут в отдельные экземпляры PHP (при условии, что Apache настроил достаточно «детей»). Каждый скрипт PHP будет устанавливать свое собственное соединение MySQL. MySQL довольно быстро примет несколько соединений (при условии, что max_connections достаточно высоко, что по умолчанию). Каждое MySQL соединение будет работать независимо (за исключением низкоуровневых блокировок базы данных, мьютексов и т. Д. c). Каждый из них завершит sh, когда завершится, то же самое для PHP и Apache, возвращающих результаты пользователю.

Я предполагаю (не зная точно), что nginx работает аналогично.

Примечание: я предлагаю, чтобы Apache (и nginx) действовали последовательно. Но я подозреваю, что для передачи HTTP-запроса на PHP требуется порядка миллисекунды, поэтому этот «последовательный» шаг не объяснит время, которое вы нашли.

Я пришел к выводу, что один из них на самом деле не происходит:

  • Конфигурация на каждом шаге не разрешает 3 дочерних / подключений / et c или
  • Существует 3 отдельных HTTP-запроса .
  • Существует 3 отдельных сценария PHP.
  • Операторы 3 SQL не блокируют друг друга. (Пожалуйста, укажите SQL.) Примечание: ENGINE=MyISAM использует блокировку таблицы; это само по себе может объяснить проблему. (Пожалуйста, предоставьте SHOW CREATE TABLE.)

Возможно (после просмотра SQL) ускорить SQL, тем самым уменьшая общую проблему медлительности.

Запросы

Предполагая, что id является PRIMARY KEY каждой таблицы, тогда эти другие индексы могут быть полезными для ускорения Запроса 2:

backup_broadcast:  (deletion, id)
shares:  (media_type, media_id, site_id)
broadcast:  (site_id, id)
video:  (deletion, id)
playlists_playlists:  (playlist_id, broadcast_id)

playlist_broadcast пахнет как таблица «многие ко многим». Если это так, я рекомендую следовать советам в http://mysql.rjweb.org/doc.php/index_cookbook_mysql#many_to_many_mapping_table. (То же самое для любых подобных таблиц.)

OR и IN ( SELECT ... ) имеют тенденцию быть неэффективными конструкциями. Но, похоже, у вас нет контроля над запросами?

Это LIMIT без ORDER BY ?? Вас волнует, какие 10 рядов вы получите ?? Это не будет предсказуемо.

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

Для запроса 3 site требуется INDEX(deletion, customer_id) (в любом порядке). Однако переформулировка его для использования JOIN или EXISTS, вероятно, будет работать быстрее.

2 голосов
/ 31 января 2020

MySQL может обрабатывать множество параллельных запросов, но вы не можете выполнить более одного запроса за раз для каждого соединения. Способ PHP обычно настраивается так: каждый запрос направляется в отдельный поток / процесс, поэтому каждый процесс будет иметь свое собственное соединение с MySQL, поэтому упомянутая проблема устраняется. Если вы не используете постоянное соединение внутри PHP, а затем вы можете использовать одно и то же соединение для каждого запроса. Если это так, то должно быть легко отключить его и go вернуться к стандартному соединению с одной базой данных на модель запроса.

Мое первое предположение состоит в том, что конечная точка 2 вызывает некоторую блокировку базы данных, и поэтому запрос конечной точки 3 ставится в очередь до завершения запроса enpoint2. Это можно исправить, изменив логи c в коде (избегайте или минимизируйте блокировку базы данных), или измените конфигурацию базы данных или механизмы таблиц, используемые для лучшего удовлетворения потребностей приложений. Пример: InnoDB выполняет блокировку уровня строки MyISAM и выполняет блокировку всей таблицы.

Профилирование будет действительно полезным, если вы не против его настройки. Я предлагаю взглянуть на Blackfire.io, New Reli c или xdebug profiling, если вы go этот маршрут. Таким образом вы сможете быстрее найти узкие места.

1 голос
/ 01 февраля 2020

Я думаю, что у вас есть проблема с php блокировкой сеанса: ваш второй и третий запрос пытаются получить доступ к одному и тому же сеансу php и ожидают.

Попытайтесь вызвать session_write_close как можно скорее как вы можете освободить ваш php сеанс. Как только вы сможете: когда вы уверены, что больше не будете записывать данные в свой php сеанс.

Простой способ отметьте это, чтобы попытаться использовать 2 браузера или в режиме анонимного / инкогнито: ваши файлы cookie не будут переданы, и у вас должно быть 2 сеанса, не блокирующих друг друга.

0 голосов
/ 31 января 2020

HM ... слишком долго для комментария.

немного упрощенный, у каждого движка есть одна очередь, в которой он собирает запросы для вычисления, в зависимости от аппаратного обеспечения, которое использует 2 или 3 или даже больше потоков для вычисления каждый запрос. Чем больше потоков выполняется, тем больше времени требуется каждому запросу из-за блокировок, как, например, блокировка всей таблицы при вставке новой строки с автоинкрементом (в результате поиска вы найдете много примеров блокировок). Конечно, для каждого запроса требуется память и другие ресурсы, которыми они должны поделиться с остальной частью всего компьютерного программного обеспечения, работающего на сервере.

С кластерами вы платите дополнительную цену за управление несколькими серверами sql.

Таким образом, со стороны sql сервер работает параллельно, однако вам необходимо аппаратное обеспечение для поддержки множества потоков / многих механизмов (которое следует использовать только очень осторожно)

Конечно, вы можете иметь в sql много пользователей, но для удобства у вас обычно по одному на приложение, а иногда даже по одному на сервер. Но один и тот же пользователь может получить доступ к базе данных одновременно, но вы, конечно, можете отключить ее.

Ваш php работает параллельно, потому что веб-сервер построен для выполнения запросов papallel, и там не имеет значения, работает ли он php, Python (django) или javascript (nodejs), apache, IIS, nginx, и их намного больше, каждая технология имеет свои преимущества и, конечно, больше модулей, которые вы добавляете к движку, намного медленнее.

Таким образом, все в некоторой степени параллельно, и вы можете увеличить мощность таких систем, как вы видите в облачных провайдерах или виртуальных серверах и т. д.

Ограничения, которые вы замечаете только в случае появления Pokemon go или новых игр, где даже огромные облачные провайдеры предпочитают sh. Или катастрофа с ObamaCare, где ничего не тестировалось в таком масштабе, какой бы ни ... ответственный,

Распараллеливание таких задач затруднительно, потому что в случае веб-сервера и sqlserver он до некоторой степени кэширует, где они паркуются запросы, которые часто делаются, но обычно каждому запросу нужны свои собственные данные.

На самом деле все намного сложнее, начиная с процессора с 3 конвейерами, несколькими ядрами и общей памятью (что вызвало Meltdown и их братьев), просматривает таблицы или базы данных, которые находятся только в памяти, для высокой производительности, или веб-сервер, который работает только в кеше процессора, который намного быстрее, чем память или жесткие диски .....

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...