Pull to refresh

Использование дополнительных инструкций CPU в одной из задач на PHP для ускорения производительности

Reading time 6 min
Views 2.2K
При построении крупных PHP-проектов многие сталкивались с нехваткой производительности, даже на мощных серверах. Даже небольшой участок кода может ощутимо повлиять на весь ресурс в целом: в плане прибыли, и в плане затрат на поддержку и обслуживание данного ресурса. Расскажу Вам мой опыт о нестандартном подходе решения одной задачи.

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

Уже когда все написано, работает, и продолжает дальше разрабатываться, и ни времени, ни бюджета переделывать что-либо – дабы улучшить производительность – нет, а двигаться нужно только вперед, причем как можно быстрее, я получаю очередное задание. Сначала я посмотрел на него как на обычный тикет: вся личная информация пользователя: фамилия, адрес, телефон, идентификационный код – должна храниться в базе в зашифрованном виде, и быть доступна только при запросе с ключами для расшифровки. Так как это мой первый серьезный опыт, связанный с шифрованием данных, я начал искать в гугле возможные пути решения задачи средствами PHP, и, естественно, наткнулся на всем известную библиотеку mcrypt. Не нужно особо много времени, чтобы разобраться, как с ней работать. Библиотека работала – на форумах можно найти много примеров, комментариев, обсуждений. Она показалась мне идеальным вариантом для решения моей задачи, особенно учитывая, что времени было совсем немного.

В итоге, я использовал код, который находится прямо на странице описания функции mcrypt_encrypt:
http://us2.php.net/manual/en/function.mcrypt-encrypt.php
<?php
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$key = "This is a very secret key";
$text = "Meet me at 11 o'clock behind the monument.";
echo strlen($text) . "\n";
$crypttext = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $text, MCRYPT_MODE_ECB, $iv);
echo strlen($crypttext) . "\n";
?>


Все работает хорошо, за исключением одного маленького НО: 5-ый параметр $iv (он же – IV — вектор инициализации) в функции mcrypt_encrypt не к месту — так как он вообще не используется в режиме шифрования ECB (Electronic codebook). И меня вообще удивляет, почему данный пример присутствует в документации — это сбивает с толку.

Наш Engineer Lead провел code review, и сделал два аргументированных замечания:
  1. IV не используется в режиме ECB (о чем я писал выше) — это что касается безопасности, к производительности дела не имеет.
  2. mcrypt является слишком тяжелым и медленным, чтобы позволить вызывать его на каждом page load, лучше найти куски кода, где действительно нужны эти данные и расшифровывать их только в тех случаях.

Первое – не проблема, погуглив дальше, сразу же натыкаешься на режим CBC (Cipher-block chaining). Но вот что делать со вторым – это ведь нужно перерыть все модули, ведь фамилии пользователей используются почти на каждой странице сайта. Это слишком много, подумал я, учитывая сроки, риски – ведь все еще должно будет пройти QA.

Одним вечером, обсуждая ежедневные проблемы, связанные с работой, попивая пиво с другом, который далек от PHP и «этих» проблем, но очень опытен в низкоуровневом программировании и С++ – это оказалось не только приятным времяпрепровождением, но к тому же очень полезным для работы.
Раскрыл он мне одну тайну (на самом деле только для меня это было тайной, а вот для мира С++ программистов, конечно же это очевидность): если использовать определённые инструкции процессора, то можно поднять в 10-ки раз производительность вычислительных задач, в том числе и задач, связанных с шифрованием данных. Новые процессоры intel уже поддерживают инструкции для ускорения шифрования и расшифровывания данных — Advanced Encryption Standard (AES) Instruction Set. И к счастью, как оказалось, наш проект работает на серверах с процессорами Intel Xeon E5645, которые уже имеют в наличии эти инструкции (AES New Instructions).

Но как все это использовать в PHP?

Мы напишем свой PHP модуль, который будет принимать значение из PHP и шифровать/расшифровывать, используя возможности процессора. После нескольких бессонных ночей, сравнения результатов производительности и вообще – концепции, что должен делать модуль, где и как хранить вектор с данными (ведь он необходим для расшифровки) – получилось нижеследующее.
PHP модуль, состоящий из двух частей:
  1. Botan (http://botan.randombit.net/) открытая библиотека, написанная на С++, которая реализует множество алгоритмов шифрования, в том числе AES256, который нам нужен, и при этом имеет возможность использовать AES-NI.
  2. libaecrypt – уже наша часть – служит переходником C++ интерфейса библиотеки Botan в C интерфейс (функции, а не классы), который можно вызвать из главного С файла модуля.

В модуле мы реализовали три функции:
  1. Генератор случайных ключей — возвращает случайные данные длиной в N байт, которые можно использовать как ключ или вектор.
  2. Шифрование
  3. Расшифровка

Шифрование/расшифровка – использует в качестве параметров:
  • ключ данных – 32 байта, который должен быть создан один раз и храниться скрыто
  • ключ вектора – 32 байта, который тоже должен быть создан один раз и храниться скрыто
  • IV – 16 байт, который создается «генератором случайных ключей»


Алгоритм выглядит примерно так: генерируется случайный IV, потом шифруются данные, используя ключ данных и IV; шифруется вектор с помощью ключа вектора. Зашифрованный вектор добавляется к зашифрованным данным с разделителем # и сохраняется в базе, расшифровка идет в обратном порядке.

Основная особенность Botan:

Я не думаю, что выкладывать листинги кода в статье разумно, потому что их много, поэтому расскажу о инициализации модуля, что и есть самый сок.
В комплекте с библиотекой Ботан, идет вспомогательный инструмент, который позволяет определить процессор и его инструкции (botan/cpuid.h). Для ускорения шифрования/расшифровки проверяется, есть ли у процессора AES-NI; если нет – то есть ли SSSE3.
int Init()
{
// Инициализируем Ботана
pInitObj = new Botan::LibraryInitializer();

// Узнаем какой процессор
CPUID::initialize();

// Узнаем есть ли у процессора поддержка инструкций
if(CPUID::has_aes_ni())
global_state().algorithm_factory().set_preferred_provider("AES-256", "aes_isa");
else if(CPUID::has_ssse3())
global_state().algorithm_factory().set_preferred_provider("AES-256", "simd");
else
global_state().algorithm_factory().set_preferred_provider("AES-256", "core");

return 1;
}


В результате, инструмент для тестирование нагрузок от Apache – ab (Apache Benchmark), показал разницу между нашим модулем и реализацией такого же алгоритма с использованием mcrypt: приблизительно 600 requests/second против 1400 requests/second – в пользу нашего модуля.

Вывод:


OpenSSL, который так же поставляется с PHP, начиная с версии 1.0.1, выпущенной 14 марта 2012 года (после всех наших мучений), уже тоже умеет использовать инструкции AES-NI (и SSSE3), и в производительности схожий алгоритм, написаный на PHP c OpenSSL, уступает нашему модулю всего-то в 200 requests per second (Software supporting AES instruction set, OpenSSL from version 1.0.1 есть в списке).
Лично я, в будущем, буду использовать OpenSSL, вместо MCrypt. Помимо того, что mcrypt медленнее, он в качестве вектора инициализации требует ключ в 32 байта! — что не совсем стандартно, так как OpenSSL, Botan, и как я понимаю, многие другие библиотеки реализующие криптование в режиме AES256-CBC принимают ключ для IV размером в 16 байт. Если использовать mcrypt, то уже только им можно будет расшифровать данные.

UPD1: Насчет примеров кода и ссылки на мой модуль: проблема заключается в том, что я подписал контракт, который не позволяет мне выкладывать публично исходный код проекта, т.к. это может повлиять на безопасность (речь идет о 100-ни тысяч пользователей США). Но я постараюсь сегодня выложить измененный вариант модуля для просмотра, чтобы не нарушать условия контракта.

UPD2: Я был удивлен резкому негативу и минусу в карму, поэтому хочу сказать: я хотел поделиться опытом, рассказать, что если вы работаете на PHP и имеете дело с шифрованием, то mcrypt не самый лучший выбор, поскольку эта библиотека имеет проблемы с производительностью. В комплекте php, так же поставляется OpenSSL, который с версии 1.0.1 (как я уже писал выше), использует инструкции процессора, работает намного быстрее и на отлично выполняет криптование данных. После выхода новой версии OpenSSL, наш самописный модуль уже не имеет значения, но это, к сожалению, было до его выхода, и опять же отмечу, что у нас было слишком мало времени.

UPD3: Еще раз прошу заметить, что 25k Page Views и проблемы с производительностью нашего проекта — не главный смысл, пожалуйста, акцентируйте внимание на главный вывод из моего опыта: использование AES-NI (инструкции процессора для ускорения производительности) и OpenSSL vs. MCrypt. Спасибо всем, кто прокомментировал и высказал свое мнение, я как можно скорее постараюсь переписать статью, дабы уделить больше внимание AES-NI, OpenSSL vs. MCrypt и как написать модуль для PHP.
Tags:
Hubs:
+50
Comments 70
Comments Comments 70

Articles