При построении крупных PHP-проектов многие сталкивались с нехваткой производительности, даже на мощных серверах. Даже небольшой участок кода может ощутимо повлиять на весь ресурс в целом: в плане прибыли, и в плане затрат на поддержку и обслуживание данного ресурса.
У нашей кампании был проект, построенный на Drupal, которому не хватало производительности под нагрузкой примерно в «25K Daily Page Views».
На протяжении года, мы постоянно добавляли новый функционал: писали больше кода, создавали больше модулей, модули из модулей, больше таблиц с миллионами записями, которые участвовали в перекрестной выборке. Проект рос с большой скоростью. Состав разработчиков не раз менялся, а это хоть и несущественно, но, все же, отрицательно сказывалось на проекте, что также добавляло лишних проблем. В общем, достаточно большой проект, как это бывает у крупных кампаний.
Уже когда все написано, работает, и продолжает дальше разрабатываться, и ни времени, ни бюджета переделывать что-либо – дабы улучшить производительность – нет, а двигаться нужно только вперед, причем как можно быстрее, я получаю очередное задание. Сначала я посмотрел на него как на обычный тикет: вся личная информация пользователя: фамилия, адрес, телефон, идентификационный код – должна храниться в базе в зашифрованном виде, и быть доступна только при запросе с ключами для расшифровки. Так как это мой первый серьезный опыт, связанный с шифрованием данных, я начал искать в гугле возможные пути решения задачи средствами 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 (он же – вектор инициализации) в функции mcrypt_encrypt не к месту — так как он вообще не используется в режиме шифрования ECB (Electronic codebook). И меня вообще удивляет, почему данный пример присутствует в документации — это сбивает с толку.
Наш Engineer Lead провел code review, и сделал два аргументированных замечания:
- Initialization vector не используется в режиме ECB (о чем я писал выше)
- 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 модуль, состоящий из двух частей:
- Botan (http://botan.randombit.net/) открытая библиотека, написанная на С++, которая реализует множество алгоритмов шифрования, в том числе AES256, который нам нужен, и при этом имеет возможность использовать AES-NI.
- libaecrypt – уже наша часть – служит переходником C++ интерфейса библиотеки Botan в C интерфейс (функции, а не классы), который можно вызвать из главного С файла модуля.
В модуле мы реализовали три функции:
- Генератор случайных ключей — возвращает случайные данные длиной в N байт, которые можно использовать как ключ или вектор.
- Шифрование
- Расшифровка
Шифрование/расшифровка – использует в качестве параметров:
- ключ данных – 32 байта, который должен быть создан один раз и храниться скрыто
- ключ вектора – 32 байта, который тоже должен быть создан один раз и храниться скрыто
- вектор инициализации – 16 байт, который создается «генератором случайных ключей»
Алгоритм выглядит примерно так: генерируется случайный вектор инициализации, потом шифруются данные, используя ключ данных и вектор инициализации; шифруется вектор с помощью ключа вектора. Зашифрованный вектор добавляется к зашифрованным данным с разделителем # и сохраняется в базе, расшифровка идет в обратном порядке.
Основная особенность 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 – в пользу нашего модуля.
P.S. 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 есть в списке).
UDP: Насчет примеров кода и ссылки на мой модуль: проблема заключается в том, что я подписал контракт, который не позволяет мне выкладывать публично исходный код проекта, т.к. это может повлиять на безопасность (речь идет о 100-ни тысяч пользователей США). Но я постараюсь сегодня выложить измененный вариант модуля для просмотра, чтобы не нарушать условия контракта.
Автор: dxArtem