У любого успешного 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 вы прописываете
следующие строки:
<Perl>
unshift(@INC, 'Путь к Вашему модулю');
@PerlModule = qw(MyHandler);
%Location = (
'/myhandler' => {
'PerlHandler' => 'MyHandler::view',
'SetHandler' => 'perl-script',
'PerlSendHeader' => 'on'
},
);
</Perl>
Тем самым вы указываете 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 необходимо указать, что авторизовать
пользователя мы будем сами:
%Location = (
'/myhandler' => {
'PerlHandler' => 'MyHandler::view',
'SetHandler' => 'perl-script',
'PerlSendHeader' => 'on'
'require' => 'valid-user',
'Limit' => {
'METHODS' => 'GET POST'
},
'AuthType' => 'Basic',
'AuthName' => 'PersonaUser',
'PerlAuthenHandler' => 'MyAuthorization ->handler()'
},
);
Теперь доступ к /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 будет выглядеть примерно так (конкретные
значения зависят от количества памяти в системе):
shell>safe_mysqld -O key_buffer=8M -O sort_buffer=1M -O join_buffer=16K
Резюмируем. При использовании базы данных работу скрипта можно значительно
ускорить правильной настройкой сервера БД. В руководстве базы данных
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-запросами.
Файл с описанием выглядел так:
2:data/html/2.htx,data/queries/info.sql
9: data/html/9.htx,data/queries/ban-list-one.sql,data/queries/ get-banners-list.sql
12:data/html/12.htx,data/queries/ba n-getinfo.sql
38:data/html/38.htx,data/queries/acc-hosts -hits.sql
44:data/html/44.htx,data/queries/acc-getsites-today.sql
Общая схема очень проста - выполнить все SQL-запросы и вставить результаты
в псевдо-html, получив таким образом полноценную страничку, и выдать
ее пользователю. Например, для вывода статистики с номером 2 (информация
об аккаунте), требовалось выполнить SQL-запрос data/queries/info.sql,
результаты вставить в data/html/2.htx. Результат вывести на экран.
А вот как обстояло дело подробнее. Первая задача - формирование SQL-запроса.
В него нужно вставить идентификатор пользователя и другие параметры,
которые переданы скрипту. Типичный пример SQL-запроса (data/queries/info.sql):
select
AccountName,
OwnerName,
Owner Email,
MainSite,
SiteName
from
Accounts
wher e
AccountId = <--AccountId-->
При разборе такого запроса значение параметра вставлялось на место
строки <--ИмяПараметра-->. Существовали и специальные параметры,
например - <--UserName--> - имя пользователя и <--AccountId-->
- вычисленный по имени идентификатор аккаунта.
Результат выполнения полученного запроса заносился в html следующим
образом. Каждое полученное из базы данных значение получало "имя",
с помощью которого обозначалось его местоположение в html-шаблоне.
Имя было составным. Первая часть - порядковый номер SQL-запроса, вторая
часть - индекс значения в массиве результатов.
Допустим, выполнялся SQL-запрос с порядковым номером 1 (для примера
рассмотрим запрос data/queries/info.sql). Запрос возвращал массив
значений. Соответственно, значение AccountName, возвращенное базой
данных, имело порядковый номер 0 в этом массиве. В html-шаблоне место,
куда необходимо было вставить AccountName обозначалось как <--1.1-->.
Кусочек HTML-шаблона data/html/2.htx из нашего примера:
<TABLE BORDER=0 WIDTH=460>
<TR>
<TD WIDTH="50%">
<FONT SIZE="-1">
Имя, фамилия ответственного:
</FONT>
</TD><TD><INPUT type="text" name="OwnerName"
size=33 value="<--1.1-->">
</TD>
</TR>
<TR>
<TD>
<FONT SIZE="-1">
Электронный адрес:
</TD><TD>
<INPUT type="text" name="OwnerEmail" size=33
value="<--1.2-->">
</TD>
</TR>
Несмотря на кажущуюся сложность схемы, она имеет ряд преимуществ.
С ее помощью мы смогли за короткое время построить систему с более
чем 400 видами различных статистик. Впоследствии для добавления новой
статистики надо было только написать SQL-запросы, нарисовать HTML-шаблон
и изменить конфигурацию скрипта-шаблонизатора. Новая страница статистики
появлялась в системе автоматически.
Заключение
Я хотел бы еще раз повторить: нет решений на все случаи жизни. Каждый
раз, в каждом проекте вам придется придумывать собственные методы
оптимизации быстродействия и удобства работы. Я надеюсь, что приемы,
о которых я рассказал, пригодятся вам. Если у вас возникнут какие-нибудь
вопросы или уточнения, я готов обсудить их с вами - vbob@aha.ru