Создание больших web-проектов
Автор: Олег Бунин
Источник: www.woweb.ru
У любого успешного 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
|