У любого успешного 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"; } |
|
Механизм хэндлеров обладает мощными возможностями.
Фактически вы можете заменить любую стадию обработки запросов.
Рассмотрим для примера создание собственного механизма проверки пароля:
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; }
}; |
|
В файле настроек сервера 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-шаблон и изменить конфигурацию скрипта-шаблонизатора. Новая
страница статистики появлялась в системе автоматически.
Заключение
Я хотел бы еще раз повторить: нет решений на все
случаи жизни. Каждый раз, в каждом проекте вам придется придумывать
собственные методы оптимизации быстродействия и удобства работы. Я
надеюсь, что приемы, о которых я рассказал, пригодятся вам
|