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

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

Здесь не будут рассматриваться стратегии, подразумевающие горизонтальное масштабирование путем добавления серверов и вертикальное путем добавления более мощного оборудования.

Приступим.

1. Проанализируйте приложение

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

И хотя для некоторых приложений это так и есть, на моем опыте бэкенд-проекты всегда удавалось отлично анализировать простым образом, например добавляя в код отчет о времени выполнения от начала до завершения, записывая потребление памяти на различных этапах выполнения, а также используя инструменты вроде Apache Bench и команду Unix time в терминале. 

Вот пример использования именно этой команды для оценки количества времени, необходимого для достижения определенной конечной точки API:

% time curl -s https://api.coindesk.com/v1/bpi/currentprice.json 

-вывод удален для краткости-

0.01s user 0.00s system 23% cpu 0.064 total

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

Кроме того, есть несколько отличных инструментов для управления производительностью приложения (APM), например NewRelic, DataDog и AppDynamics. С их помощью можно получать подробную информацию за прошедшие периоды времени.

Информационная панель DataDog

2. Просмотрите код

Прежде, чем задействовать сложные, а иногда и дорогостоящие, инструменты повышения производительности, я рекомендую просто почитать и проанализировать сам код. Это оказывается полезно, когда нет возможности воссоздать контекст.

Например, при разработке Underworld Empire мы столкнулись с проблемой производительности в то время, когда в игре происходило событие. Естественно, мы не могли воссоздать его в среде разработки, но выяснили проблему, просмотрев код. Это может быть не так просто в случае с огромными базами кода, так что тут будет кстати обратить внимание на другие рекомендации.

3. Замените несколько запросов get одним mget, чтобы ускорить работу ввода-вывода

Ввод-вывод является самой частой причиной падения производительности. Простым, но при этом эффективным, приемом будет использовать вместо множества функций get одну объединенную mget.

Независимо от того, работаете ли вы с вводом-выводом файлов или же с удаленными источниками данных, множественные запросы get серьезно нагружают систему. Выполнение одного комплексного запроса может существенно такую нагрузку сократить.

4. Фоновая обработка, многопоточность и асинхронный ввод-вывод

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

Обратите внимание, что многопоточность вызывает ряд проблем, связанных с состоянием гонки, которые могут привести к длительным сеансам отладки. Используйте эти приемы с умом и не забывайте о потокобезопасности не только кода, но и используемых библиотек.

5. Кэшируйте данные на уровне приложения

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

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

Вот пример подобного кэширования:

<?php

class LocalCache {

use Singleton;

protected store = [];

public function get($key, default = null) {
return isset($this->store[$key])) ? $this->store[$key] : $default;
}

public function set($key, $value) {
$this->store[$key] = $value;
}

}

6. Кэшируйте данные на уровне машины/сервера

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

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

7. Используйте внешний кэш для сокращения числа обращений к хранилищу

В этой технике для хранения данных задействуется внешнее хранилище вроде Memcache и Redis, что избавляет вас от необходимости повторно запрашивать их из БД или хранилища. Нередко такое решение называют паттерном проектирования со сторонним кэшем.

Вот его пример в коде:

<?php

require __DIR__ . '/../vendor/autoload.php';

// использует Predis https://github.com/nrk/predis
use Predis\Client as PredisClient;

$user = get_user(125);
print_r($user);

/**
* @param $id
* @return stdClass|null
*/
function get_user($id)
{
$client = new PredisClient();

// подключение к локальному серверу redis
$client->connect();

// проверка redis
$key = "user:{$id}";
$obj = $client->get($key);
if ($obj) {
echo ("Loading user:{$id} from redis\n");
$obj = unserialize($obj);
return $obj;
}

// загрузка из базы данных
$obj = get_user_from_database($id);
if (!$obj) {
echo ("Loading user:{$id} from database\n");
return null;
}

// сохранение в кэше перед возвращением
echo ("Saving user:{$id} in redis\n");
$client->set($key, serialize($obj));

// итоговое возвращение объекта вызывающему
return $obj;
}

/**
* Fake database mock
* @param $id
* @return stdClass|null
*/
function get_user_from_database($id) {

$obj = null;
switch($id)
{
case 125:
$obj = new \stdClass();
$obj->id = 125;
$obj->name = "test user";
$obj->email = "[email protected]";
break;

default:
break;
}

return $obj;
}

8. Повысьте эффективность запросов для снижения нагрузки на хранилище 

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

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

9. Повысьте производительность сети с помощью сжатия

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

10. Повысьте производительность сети с помощью более быстрых протоколов

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

Должен сказать, что у нас были сложности с использованием этой техники в PHP.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Naveed Khan: Improving Backend Application Performance

Предыдущая статьяКак работает обратное распространение в нейронных сетях
Следующая статьяМетрики для улучшения архитектуры ПО