У любого успешного web-проекта рано или поздно возникает проблема
роста. Существующие программно-аппаратные ресурсы перестают
справляться с растущей нагрузкой. Универсальных рецептов, к сожалению
не существует. В каждом проекте хороший программист будет
программировать по-разному. Тем не менее, в этой статье я попробую
дать несколько типичных рекомендаций по созданию больших web-проектов.
Такие проекты в процессе создания и развития сталкиваются, как
правило, с двумя почти противоположными по способам решения проблемами
- большими скоростями и большими объемами данных.
Большие скорости
В качестве идеального примера сайта, для которого жизненно важна
скорость, можно взять баннерную сеть. Итак, несколько приемов для
ускорения работы баннерных сетей и других серверов, критичных к
скорости работы.
Создание модулей
Смысл этого приема - вкомпилировать наиболее важные функции в сервер.
Идея очень проста. Если мы посмотрим на соотношение времени, которое
тратится на различные стадии выполнения запроса, то увидим интересную
картину. Например, при выполнении простейшего perl-скрипта
последовательно происходит следующее:
1) сервер Apache определяет perl-скрипт для запуска, подготавливает и
запускает его;
2) запуск скрипта фактически начинается с запуска perl-интерпретатора
(это файл, размером около полумегабайта). Perl-интерпретатор,
запустившись, размещается на 2-х мегабайтах в памяти машины, и только
после этого приступает к работе с пользовательским скриптом;
3) эта работа начинается с компиляции программы. Компиляция программы
- это, как правило, один из самых длительных этапов обработки
программы;
4) только после предварительной компиляции (в байткод) скрипт начнет
выполняться.
Статистика удручает: время, которое тратится на запуск
perl-интерпретатора и компиляцию скрипта, как правило, на порядок
больше времени, за которое он выполняется.
На каждом сайте существуют узкие места - программы, которые вызываются
очень часто. Например, баннерный движок. Как правило, на один просмотр
страницы приходится два-три баннера, а значит и вызова программы.
Понятно, что если избавиться от накладных расходов (пункты 2 и 3),
работа сервера значительно ускорится. Это можно сделать двумя похожими
способами.
Первый - написать модуль к Apache и вкомпилировать его в сервер.
Именно так в баннерной сети Фламинго-2 (http://www.f2.ru), в создании
которой я принимал участие, была реализована часть системы, которая
раздавала баннеры пользователям. Это был модуль, написанный на языке
C, который функционировал как часть сервера Apache и поэтому работал
очень быстро.
Второй способ - использовать технологии предкомпиляции программ. Таких
технологий достаточно много. Например, для perl-скриптов это могут
быть FastCGI и mod_perl. Расскажу подробней о mod_perl. Это
вкомпилированный (опять же в виде модуля) в Apache perl-компилятор.
Во-первых, даже для простых скриптов (при надлежащей настройке) это
исключает вторую стадию выполнения. Но кроме этого mod_perl дает
возможность писать хэндлеры - обработчики определенных стадий
выполнения запроса. Это очень мощная технология, поэтому рассмотрим ее
подробнее.
Можно, например, написать хэндлер, который будет вызываться при
запросе определенного URL. Делается это так. В файл httpd.conf вы
прописываете следующие строки:
Тем самым вы указываете Apache и модулю mod_perl, что если
пользователь запросит URL /myhandler, то для его обработки должен
запуститься модуль MyHandler, а в нем процедура view. После изменения
httpd.conf надо перезагрузить Apache. Кстати, все указанные в
конфигурационном модуле файлы будут компилироваться при загрузке
сервера, а не при первом запросе. Это в несколько раз увеличит
скорость работы сервера.
Модуль MyHandler.pm может выглядеть, например, так:
package MyHandler;
use strict;
# Процедура view
sub view {
print "<HTML>n<BODY>nУра! Это отработал наш хэндлер!</BODY>n</HTML>n";
}
1;
Механизм хэндлеров обладает мощными возможностями. Фактически вы
можете заменить любую стадию обработки запросов. Рассмотрим для
примера создание собственного механизма проверки пароля:
package MyAuthorization;
use strict;
# Обработчик, запрашивающий пароль
sub handler {
my $r = shift;
return AUTH_REQUIRED unless $r;
my (undef, $password) = $r->get_basic_auth_pw;
my ($login) = $r->connection->user;
return AUTH_REQUIRED unless $password;
# Проверяем, все ли в порядке
# Проверка может быть любой
# Можно свериться с базой данных, а мы будем считать, что пароль должен быть
# равен логину, прочитанному задом наперед.
my $rev_login = reverse($login);
# Проверка пароля
if ($rev_login ne $passwd_sent)
{
return AUTH_REQUIRED;
} else {
return OK;
}
};
1;
В файле настроек сервера httpd.conf необходимо указать, что
авторизовать пользователя мы будем сами:
Теперь доступ к /myhandler защищен - браузер выведет пользователю
стандартное окно для ввода пароля.
Более подробно с технологией mod_perl можно познакомиться на сайте
http://perl.apache.org/
Использование конвейеров
Старайтесь не производить обработку данных в интерактивных скриптах.
Записывайте их в лог-файлы, а затем агрегируйте и обрабатывайте уже
отдельным процессом. Например, ответ пользователя в интерактивном
голосовании может вызывать у вас изменения в десятке различных
параметров статистики (распределение ответов, активность
пользователей, общее число проголосовавших и так далее). Не проводите
их сразу. Вместо этого разбейте процедуру на две части. Первая -
непосредственно голосование, запись результата и вывод ответной
страницы пользователю. Вторая - обработка голосования, изменение
статистики и т.д.
Вообще надо стараться минимизировать количество интерактивных
операций. В идеальном случае скрипт для учета голосования вообще
ничего не делает, кроме записи информации в лог-файл. А для обработки
данных из лог-файла можно запускать отдельный процесс-демон.
Для примера рассмотрим механизм обработки статистики в баннерной сети
Фламинго-2. В ней был реализован 4-х ступенчатый конвейер:
1) Информация о каждом запросе записывалась в полный лог. Это была
очень подробная информация и записывалась она без всякого сжатия, на
которое потратилось бы много времени. Размер этого лога очень велик -
одна запись в нем занимала 250 байт. Данные в этом логе не хранились
дольше нескольких часов.
2) С периодичностью раз в 10 минут запускалась программа, которая
обрабатывала полный лог и в компактном виде писала информацию в
таблицы базы данных. На этой же стадии учитывались показы, изменялись
временные таблицы, используемые для выдачи баннеров пользователю и для
работы следующих стадий.
3) Часовой демон, который строил почасовую статистику, производил
сложные географические расчеты и многое другое, запускался в конвейере
один раз в час. Он уже не имел доступа к полному логу и использовал
информацию исключительно из второй стадии.
4) В задачи последней стадии входила дневная ротация файлов,
статистика, подведение балансов и рассылка почтовых предупреждений.
Эта стадия работала каждые сутки поздно ночью, когда нагрузка на
сервер была минимальной.
Как видите, механизм достаточно сложный, и наладить его корректную
работу было нелегко. Чем больше стадий, тем больше проблем при их
сопряжении друг с другом. Тем не менее, такая система позволяла
достаточно эффективно распределять нагрузку и шустро работала на
простом IDE-диске (расчетная пропускная способность была около 2-3
миллионов обращений в день при пиковой нагрузке 200 обращений в
секунду). При этом система вела большое количество статистики.
Итак, резюмируем: для увеличения скорости работы программ,
взаимодействующих с пользователем, разбиваем их работу на части,
причем интерактивная часть должна содержать минимум расчетов и
операций записи. Все необходимые расчеты можно произвести позднее, в
более благоприятное с точки зрения нагрузки время и более эффективно.
Базы данных
Используйте хорошую базу данных. Какую выбрать? Единого рецепта нет.
Все зависит от решаемой задачи. Если она достаточно простая и вам не
требуется выполнять сложные SQL-запросы (например, вложенные), то
наилучшим решением будет, пожалуй, база данных MySQL.
MySQL - один из самых простых серверов БД. Но даже в этой простой базе
есть свои способы оптимизации для ускорения запросов. Например, не
секрет, что INSERT - одна из самых длительных операций (вычисление
физического адреса для вставки, вставка, решение проблемы
фрагментации, изменение индексов и служебных таблиц). Хороший прием
для ускорения работы скрипта, который вставляет данные в БД - замена
операции INSERT операцией INSERT DELAYED (отложенная вставка).
Обновление данных будет выполнено только тогда, когда это не приведет
к замедлению работы сервера.
Другой пример: если внимательно почитать документацию MySQL, можно
найти упоминание о таблицах, расположенных в памяти (HEAP tables).
Очевидно, что операции с такими таблицами совершаются значительно
быстрее. Heap-таблицы можно использовать для решения некоторых задач.
Существует большое количество параметров запуска сервера БД,
оптимизирующих буферы сортировки, вычислений, количество детей и
другие параметры. Как правило, вам заранее известно, что вы будете
делать с базой, и для повышения быстродействия можно задать
соответствующие параметры.
Например, возьмем вполне реальную задачу:
построение какого-нибудь каталога. Ясно, что это будет одна большая
таблица с большим количеством индексов. Вы знаете, что будете
использовать представления. Работа с этой таблицей будет заключаться в
запросах по индексу без использования сортировки. Посмотрим, как можно
настроить сервер БД на выполнение такой задачи (пример из MySQL
3.23.25):
* join_buffer_size - буфер для создания представлений, по умолчанию
равен 131072 байта;
* key_buffer_size - буфер для работы с ключами и индексами. Размер
по умолчанию - 1048540;
* sort_buffer - буфер для сортировки. По умолчанию - 2097116 байт.
Скорее всего, при увеличении какого-то буфера, скорость выполнения
связанной с ним задачи увеличится. Исходя из нашей задачи, мы увеличим
буфер для работы с ключами (скорость выборки значений из таблицы
увеличится), уменьшим буфер сортировки (уменьшится скорость
сортировки) и буфер представлений (уменьшится скорость работы с
представлениями).
Строка запуска демона MySQL будет выглядеть примерно так (конкретные
значения зависят от количества памяти в системе):
Резюмируем. При использовании базы данных работу скрипта можно
значительно ускорить правильной настройкой сервера БД. В руководстве
базы данных MySQL есть специальный раздел, посвященный оптимизации. За
более подробной информацией можно обратиться на сайты:
Разработчики MySQL - http://www.mysql.com
Разработчики PostgreSQL - http://www.PostgreSQL.org/
Оптимизация MySQL -
http://www.mysql.cz/information/presentations/presentation-oscon2000-20000719/index.html
и http://support.ultrahost.ru/mysql_opt.php
Большие объемы
Еще одна проблема больших сайтов - большой объем информации. Если не
применять никаких ухищрений, то поддержка простого html-сайта в
какой-то момент потребует слишком много времени.
Объектно-ориентированное программирование
О пользе объектно-ориентированного подхода я уже рассказывал. Повторю
вкратце. Каждый, кто хоть раз пробовал создавать динамические сайты,
знает, что во многом это - очень однообразная задача. Гостевая книга,
конференция, форма для отправления комментариев, подписка,
регистрация. Как правило, эти скрипты слабо интегрированы и, в лучшем
случае, используют общую библиотеку с константами и общими
процедурами.
Однако если перечислить сущности, с которыми имеют дело
вышеперечисленные скрипты, мы получим очень интересные результаты:
* Сущность "пользователь". Имеет свое имя, фамилию, ник, пароль,
электронный адрес... Используется практически во всех скриптах в
разных ипостасях.
* Сущность "сообщение". Вы можете возразить, что сообщения везде
разные. Ничего подобного! Различаются формы представления
сообщений, а данные, структура полей и методы обработки - одни.
Автор, заголовок, тело - и так во всех проектах.
Вот фактически и все сущности, с которыми оперирует большинство
скриптов на сайте. Гостевая книга (она, кстати, сама может быть
объектом в более сложных проектах) представляет собой цепочку объектов
класса "сообщение". Форум или конференция - те же сообщения,
организованные иерархически. Отправка письма владельцу сайта -
сообщение. Рассылка анонсов - перебор объектов класса "пользователь" и
отправка каждому объекта класса "сообщение".
Было бы эффективно описать все эти объекты в одном месте, а потом
строить из них, как из кирпичиков, программы и скрипты, просто
вставляя вызовы объектов в код. К тому же, единое пространство
сообщений, пользователей и других объектов значительно расширяет поле
для творчества.
В этом и есть сущность объектного подхода. Вы создаете множество
объектов - кирпичиков будущих программ - и из них строите свои сайты.
Кроме того, вы можете использовать такие мощные методы ООП как
наследование и полиформизм, без которых уже немыслимо построение
крупных проектов.
Шаблонирование
Об этом я тоже расскажу вкратце; возможно этому будет посвящена статья
в одном из следующих номеров "Программиста". Вернемся к системе
Фламинго. Как был организован интерфейс этой баннерной сети? 400 видов
статистики соответствуют 400 страницам? Нет. Один скрипт-шаблонизатор,
которому передаются параметры - номер статистики и другие данные:
даты, ограничения и т.д.
По уникальному номеру статистики скрипт считывал описание, которое
состояло из имени файла с псевдо-html и имен файлов с SQL-запросами.
Файл с описанием выглядел так:
Общая схема очень проста - выполнить все SQL-запросы и вставить
результаты в псевдо-html, получив таким образом полноценную страничку,
и выдать ее пользователю. Например, для вывода статистики с номером 2
(информация об аккаунте), требовалось выполнить SQL-запрос
data/queries/info.sql, результаты вставить в data/html/2.htx.
Результат вывести на экран.
А вот как обстояло дело подробнее. Первая задача - формирование
SQL-запроса. В него нужно вставить идентификатор пользователя и другие
параметры, которые переданы скрипту. Типичный пример SQL-запроса
(data/queries/info.sql):
select AccountName, OwnerName, OwnerEmail, MainSite, SiteName
from Accounts where AccountId = <--AccountId-->
При разборе такого запроса значение параметра вставлялось на место
строки <--ИмяПараметра-->. Существовали и специальные параметры,
например - <--UserName--> - имя пользователя и <--AccountId--> -
вычисленный по имени идентификатор аккаунта.
Результат выполнения полученного запроса заносился в html следующим
образом. Каждое полученное из базы данных значение получало "имя", с
помощью которого обозначалось его местоположение в html-шаблоне. Имя
было составным. Первая часть - порядковый номер SQL-запроса, вторая
часть - индекс значения в массиве результатов.
Допустим, выполнялся SQL-запрос с порядковым номером 1 (для примера
рассмотрим запрос data/queries/info.sql). Запрос возвращал массив
значений. Соответственно, значение AccountName, возвращенное базой
данных, имело порядковый номер 0 в этом массиве. В html-шаблоне место,
куда необходимо было вставить AccountName обозначалось как <--1.1-->.
Кусочек HTML-шаблона data/html/2.htx из нашего примера:
Несмотря на кажущуюся сложность схемы, она имеет ряд преимуществ. С ее
помощью мы смогли за короткое время построить систему с более чем 400
видами различных статистик. Впоследствии для добавления новой
статистики надо было только написать SQL-запросы, нарисовать
HTML-шаблон и изменить конфигурацию скрипта-шаблонизатора. Новая
страница статистики появлялась в системе автоматически.
Заключение
Я хотел бы еще раз повторить: нет решений на все случаи жизни. Каждый
раз, в каждом проекте вам придется придумывать собственные методы
оптимизации быстродействия и удобства работы. Я надеюсь, что приемы, о
которых я рассказал, пригодятся вам. Если у вас возникнут какие-нибудь
вопросы или уточнения, я готов обсудить их с вами - vbob@aha.ru
1085 Прочтений • [Рекомендации по созданию больших web-проектов (web apache tune optimization perl mod_perl mysql)] [08.05.2012] [Комментариев: 0]