Создание больших web-проектов
У любого успешного 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/ban-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,
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
из нашего примера:
<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
Автор: Олег Бунин