Время ответа сервера — одна секунда

в 15:45, , рубрики: mysql, php, Веб-разработка, высокая производительность, скорость работы, метки: , ,

В данном небольшом посте я бы хотел поделиться своими мыслями насчёт скорости работы скриптов/программ, которые мы с вами пишем каждый день и обратить внимание на то, сколько «невидимой» работы совершается при выполнении привычных действий.

Выполнение SQL-запросов

Все знают, что работа с SQL обычно является одной из самых медленных компонентов любого сайта. Есть ли какие-то объективные причины для этого? Действительно ли базы данных такие медленные и срочно нужно переходить на NoSQL :)? Давайте посмотрим.

Создадим тестовую табличку в MySQL и наполним её 1 млн записей:

Код на PHP!

<?php
mysql_connect(...) or die("Cannot connectn");
mysql_select_db(...) or die("Cannot select DBn");

mysql_query('CREATE TABLE IF NOT EXISTS `one_million` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `one` int(11) NOT NULL,
  `two` int(11) NOT NULL,
  `three` int(11) NOT NULL,
  `four` int(11) NOT NULL,
  `five` int(11) NOT NULL,
  `six` int(11) NOT NULL,
  `seven` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB') or die("Cannot create tablen");

for ($i = 0; $i < 1000000; $i++) {
if ($i % 1000 === 999) echo "Done $in";
mysql_query('INSERT INTO one_million VALUES(NULL, ' . rand() . ', ' . rand() . ', ' . rand() . ', ' . rand() . ', ' . rand() . ', ' . rand() . ', ' . rand() . ')');
}

// Да, код не использует batch insert, а также использует устаревшее, но до сих пор работающее расширение mysql

Давайте теперь выполним какой-нибудь простенький запрос, который прочитает нам 1 млн записей:

SELECT COUNT(*) FROM one_million WHERE three = 333

На моем компьютере запрос выполнился за 200 мс! Таким образом, можно заключить, что InnoDB в состоянии просмотреть около 5 млн записей за 1 секунду! Давайте подумаем, где же на веб-сайтах может потребоваться просматривать столько записей на одну страницу :)? На моей прошлой работе я принудительно выставил ограничение в 20 000 просмотренных строк на все SQL-запросы, и после вычищения всех «тормозных» мест время открытия страницы упало в 5 раз!

Безусловно, таблицы всё же далеко не всегда содержат только int-поля, а неаккуратно написанные запросы часто делают вещи похуже full scan, но мне довольно сложно представить, зачем на обычном веб-сайтике на одну страницу нужно просматривать больше 20 000 строк. При этом, даже такое количество обычно не требуется, поскольку пользователь вряд ли будет читать так много информации на одной странице :).

Одним словом, не мучайте свою базу данных, заставляя её просматривать миллионы строк, и, может быть, вам не придется закупать 10 новых серверов под базы данных и переписывать своё приложение, чтобы оно поддерживало шардинг ;).

Медленный язык программирования

Есть распространенное заблуждение, что языки вроде PHP, Python и др. являются «медленными» и поэтому если сайт «тормозит», то это вина языка программирования. Ну хорошо, PHP на пару порядков медленней, чем Си, если заниматься физическими расчетами. Означает ли это, что PHP является медленным языком? И да и нет. Давайте посмотрим, сколько простейших операций может совершить PHP за 1 секунду?

<?php
$start = microtime(true);
for($i = 0; $i < 1000000; $i++);
$time_float = microtime(true) - $start;
$time = round($time_float, 3);
echo "Total time: $time sec, " . number_format(round(($i - 1) / $time_float)) . " cycles/secn";

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

Код теста, кому интересно

<?php
$start = microtime(true);
for($i = 0; $i < 1000000; $i++);
$time_float = microtime(true) - $start;

$start = microtime(true);
$j = 0;
for($i = 0; $i < 1000000; $i++) $j++;
$time_float2 = microtime(true) - $start;
$time = round($time_float2, 3);

echo "Total time: $time sec, " . number_format(round(($i - 1) / ($time_float2 - $time_float))) . " /secn";

Возможно, для каких-то целей действительно нужно выполнять 33 миллиона операций для отрисовки одной страницы, но у меня есть очень серьезные сомнения, что для большинства сайтов в этом есть необходимость :). Если вы жалуетесь на медленный язык программирования, подумайте — может быть вы просто что-то делаете не так? Что делает ваш код на каждый запрос, что ему нужно выполнять такое количество действий? Скорее всего, он делает какую-то полную херню, которая на самом деле вам не нужна.

Инклюд от 1000 файлов:
В зависимости от кода, который вы загружаете, можно получить разные цифры. Я выбрал самый простой вариант: включение одного и того же файла, который возвращает массив из 1000 элементов.

// Код для генерация файла include-array.php
<?php
echo "<?phpnreturn ";
var_export(range(0, 999));
echo ";n";

// Код теста
<?php
$start = microtime(true);
$total_count = 0;
for ($i = 0; $i < 1000; $i++) {
$arr = include('include-array.php');
$total_count += count($arr);
$arr = null;
}

echo "Total count: $total_count, time: " . round(microtime(true) - $start, 3) . " secn";

В cli-режиме (нет opcode-кеша) я получил 0.42 сек — учитывая размер файла в 13 Кб, получим примерно 31 Мб/сек.
В web-режиме (с opcode-кешом в виде APC) получается 0.13 сек — получаем 100 Мб/сек.

Забавно, что для многих Java-приложений зачастую приходится ждать намного больше, чем 1 сек, пока оно загрузится, хотя даже PHP без кеша выдает скорость загрузки кода порядка 30 Мб/сек.

Итого получаем, что даже в PHP можно загрузить около 100 Мб кода за 1 секунду (если загружать классы, а не массив, то результаты будут ещё лучше)… Честно говоря, довольно сложно представить, зачем вам может понадобиться загружать (и потом исполнять?) столько кода на один веб-запрос. Скорее всего, в этом нет никакой необходимости.

Работа с файловой системой

Давайте посмотрим на скорость работы ФС и диска в частности:

Чтение большого файла с диска: 430 Мб/сек
Чтение большого файла из кеша ОС: 7,3 Гб/сек

Методика измерения

Брался большой файл размером 4,7 Gb с расширением mkv и делалось следующее:
$ time cat /Users/yuriy/Downloads/$filename.mkv >/dev/null
real 0m10.974s
user 0m0.031s
sys 0m1.639s

$ time cat /Users/yuriy/Downloads/$filename.mkv >/dev/null
real 0m0.643s
user 0m0.008s
sys 0m0.635s

$ time cat /Users/yuriy/Downloads/$filename.mkv >/dev/null
real 0m0.644s
user 0m0.008s
sys 0m0.636s

Выполнение stat() в первый раз (с диска): 10 000 операций/сек
Выполнение stat() из кеша: 300 000 операций/сек

Методика изменения

Брался репозиторий крупного проекта и прогонялся следующий скрипт:

<?php
exec('find . -type f', $files, $retval);
$start = microtime(true);
foreach ($files as $f) stat($f);
$time_float = microtime(true) - $start;
$time = round($time_float, 3);

echo "Total time: $time sec, " . round(count($files) / $time_float) . " files/secn";

Исходя из скорости выполнения stat() можно было догадаться, что у меня — SSD, для жесткого диска результаты были бы сильно хуже, на уровне 200 операций/сек (хотя это требует проверки).

Итак, мы выяснили, что кешированный stat() будет выполняться со скоростью 200 000 обращений/сек. Некоторые фреймворки (не будем показывать пальцем на Symfony) имеют весьма оригинальную архитектуру плагинов, которая генерирует огромное число stat'ов на несуществующие файлы, причём генерируют так много, что это оказывает существенное влияние на общую скорость работы.

Мораль: да, stat() является относительно дорогой операцией, но вам, скорее всего, не должно требоваться выполнять 200 000 проверок на существование файла в одном веб-запросе (если только вы не пишете на Symfony с кучей плагинов). Единственное применение для такого количества вызовов stat() в веб-запросе — это работа файлового менеджера с очень большой директорией. Но даже в этом случае не обязательно вызывать stat() на каждый файл — пользователь всё равно не может воспринять столько информации за один раз.

На самом деле производительности хватает не всегда

Все упомянутое выше так или иначе относится к производительности CPU и памяти в современных компьютерах и не затрагивает более медленные компоненты.

Если вы пишете приложение, которое на стороне сервера делает много сетевых запросов или много некешированных обращений к диску, то это может оказывать намного большее влияние на производительность всей системы, чем описанные выше примеры. Тем не менее, большинство случаев, когда сайт на PHP «тормозит» из моей практики можно объяснить либо тяжелыми запросами в MySQL, либо неуклюжей реализацией системных вещей на PHP (и поэтому требующих большого количества вычислений), либо чрезмерной работой с ФС. Безусловно, если вы — гугл, то у вас будут совершенно другие проблемы и мой пост вряд ли будет вам полезен.

Заключение

Надеюсь, моя статья понравится разработчикам тяжелых ORM и фреймворков на PHP/Python/Ruby, а также разработчикам Java и разработчикам на Java ;). Если серьезно, я надеюсь, эта статья может помочь рядовому веб-разработчику приблизиться к тому, чтобы страницы отдавались быстро (хотя бы с серверной стороны), а пользователи были счастливы :).

Автор: youROCK

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js