У любого успешного 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