Главная страница
Навигация по странице:

  • Главная web-страница

  • Программные решения

  • Медленный код

  • Асинхронный код

  • Другие ресурсы

  • Оптимизация в C

  • WEb практикум. Web'cepbep


    Скачать 4.76 Mb.
    НазваниеWeb'cepbep
    АнкорWEb практикум
    Дата22.01.2023
    Размер4.76 Mb.
    Формат файлаdocx
    Имя файлаWEB.docx
    ТипДокументы
    #898678
    страница17 из 18
    1   ...   10   11   12   13   14   15   16   17   18

    Страница загружена из кеша"); return 1;

    }

    else

    return 0;

    }

    // Функция записи кеша

    function WriteCache($CacheName, $CacheData)

    {

    $cf = @fopen("cache/$CacheName.htm", "w") or die ("Can't write cache"); fputs ($cf, $CacheData); fclose ($cf);

    @chmod ("cache/$CacheName.htm", 0777);

    }

    // Основной код web-страницы if (ReadCache("MainPage")==1) exit;

    ob_start();

    print("Главная web-страница"); print("
    Это тестовая web-страница
    ");

    WriteCache("MainPage", ob_get_contents()); ob_end_flush();

    ?>

    Это максимально простое решение, которое не отличается чистотой кода, но я хо­тел просто показать идею.

    Основной код сценария начинается с запроса загрузки web-страницы из кеша. Для этого у нас в листинге создана функция ReadCache. Функции передается имя файла, который нужно загрузить, ведь в кеше могут быть сохранены и другие web- страницы. В данном случае подразумеваем, что сценарий формирует главную web- страницу с именем MainPage, и именно это значение мы передаем в качестве пара­метра.

    Давайте теперь посмотрим, что происходит в функции ReadCache. Здесь у нас про­исходит проверка на существование файла с указанным именем. Если такой файл существует, то он подключается с помощью require, и функция возвращает значе­ние 1. Для примера я еще вывожу на экран сообщение о том, что эта web-страница была взята из кеша, чтобы вы видели результат работы.

    Вернемся к основному коду. Если функция ReadCache вернула 1, то выполнение сценария прерывается. Нет смысла тратить время на формирование web-страницы, если она была взята из кеша.

    Далее выполняем функцию ob_start, чтобы начать кеширование вывода, и начина­ем формировать web-страницу. Перед тем как вызвать ob_end_flush, необходимо со­хранить содержимое сгенерированной web-страницы, которую можно получить с помощью ob_get_contents. Для сохранения кеша в нашем сценарии создана функ­ция writeCache. Ей нужно передать имя, по которому определяется имя файла кеша. Второй параметр — это данные, которые будут записаны в файл. В нашем случае это результат функции ob_get_contents.

    После создания файла изменяются его права доступа на 0777, что соответствует правам, при которых доступ к файлу разрешен всем:

    @chmod ("cache/$CacheName.htm", 0777);

    Загрузите с помощью браузера этот сценарий и попробуйте обновить. После об­новления внизу вы увидите сообщение о том, что web-страница взята из кеша.

    1. Программные решения

    Кеширование можно реализовать и с помощью сторонних программных решений, например, с помощью proxy-сервера, который можно настроить между пользовате­лем и web-сервером. В качестве такого кеша чаще используют nginx или squid. В этом случае на стороне кода нужно, главное, указать, что можно кешировать, а что нельзя, с помощью специальных параметров заголовка.

    Когда вы разрабатываете стратегию кеширования, то нужно быть очень осторож­ным, потому что в такой кеш может попасть какая-то персональная информация. Например, если есть URL: www.bankname/account/balance и на ней отображается баланс аккаунта текущего пользователя, то попадание этой информации в кеш при­ведет к тому, что в результате последующих запросов к этой же странице все будут видеть баланс одного и того же пользователя.

    Кеширование информации с помощью сторонних приложений — эффективное и очень хорошее решение, но может быть опасным с точки зрения безопасности.

    Рассмотрим некоторые заголовки, которые позволят указать, что можно или нельзя кешировать:

    cache-control - самый популярный и основной параметр. Пример использования:

    cache-control: private, max-age=0, no-cache

    После двоеточия можно указать private или public, и этот параметр как раз гово­рит есть ли на странице приватная информация. Если указать public, то такую ин­формацию может кешировать даже прокси-сервер. Если указать private, то кеширо­вать имеет право только браузер для текущего пользователя. Серверы тоже могут закешировать эту информацию, но это уже можно будет отнести к уязвимости кеш сервера.

    В данном случае указано private — значит страница содержит частные данные. До­полнительно еще указано no-cache, что запрещает кеширование даже в браузере.

    С помощью max-age можно указать, сколько времени можно хранить в кеше инфор­мацию.

    Очень часто ресурсы могут оставаться без изменения достаточно долго, и можно ука­зать заранее, что браузер может сохранить файл и хранить его определенное время — для этого используется заголовок Expires. Пример использования:

    Expires: Sat, 1 Jan 2022 01:01:01 GMT

    Не самый универсальный параметр, но все же можно использовать для определен­ных случаев, указав определенную дату.

    Более эффективный способ использование Etag:

    ETag: 1a926-130-9a7d79

    Это какой-то уникальный код, в качестве которого может выступать контрольная сумма файла или дата его последнего изменения. Браузер запрашивает файл, и если он не изменился (код остался тем же), то браузер использует версию, которая уже у него есть.

    В заголовке можно использовать и дату последнего изменения в виде параметра

    Last-Modified:

    Last-Modified: Sat, 1 Jan 2020 01:01:01 GMT

    С помощью кеширования web-страниц вы можете реально повысить скорость работы сценария, но теряете возможность создать web-сайт, ориентированный на пользова­теля. В этом случае трудно реализовать выбор расцветки, настройки отображения определенных частей web-сайта индивидуально каждым пользователем (напри­мер, в зависимости от предпочтений, дать возможность выбирать нужные катего­рии новостей для отображения). Реализовать подобное с помощью кеширования достаточно сложно, а если и возможно, то эффект от его использования уменьша­ется настолько, что овчинка выделки не стоит.

    1. Медленный код

    Достаточно популярная проблема, с которой мне приходилось сталкиваться, — это объединение двух таблиц в одну. Допустим, что у нас есть два массива Person и Address, которые мы получили из двух разных источников или от двух разных SQL-запросов. Я на работе сталкиваюсь с таким регулярно.

    Классическая задача, обычно выполняемая с помощью JOIN в SQL, должна быть сделана с помощью кода, потому что данные просто прибыли из разных источни­ков и мы не смогли объединить их заранее.

    Рассмотрим код на C#, который я видел уже много раз:

    for (int i =0; i < persons.length; i++) {

    persons[i].addresses = addresses.where(m => m.PersonId = persons[i].PersonId

    ) ;

    }

    Почему-то программисты думают, что если они используют LINQ, то он магиче­ским образом выполняется быстрее, чем следующий код:

    for (int i =0; i < persons.length; i++) {

    for (int j =0; о < addresses.length; j++) {

    if (addresses[j].PersonId = persons[i].PersonId) persons[i].addresses.Add(addresses[j]);

    ) ;

    }

    Большинство понимает, что второй вариант очень медленный, потому что он будет выполняться persons.length * address.length раз. Если в каждом цикле по 1000 элементов, то всего будет миллион итераций.

    Но первый вариант не сильно отличается по скорости. .NET умеет производить оп­ределенную оптимизацию, но он не может магически находить элементы в произ­вольном списке. Так что оба варианта ужасны с точки зрения скорости.

    Самый простой вариант решения проблемы — запустить два цикла:

    Hashtable addresses_hash = new Hashtable(); for (int j =0; о < addresses.length; j++) {

    if (!addresses_hash.containsKey(addresses[j].PersonId)) {

    addresses_hash[addresses[j].PersonId] = new List
    ();

    }

    addresses_hash[addresses[j].PersonId] .Add(addresses[j] ) ;

    );

    for (int i =0; i < persons.length; i++) {

    if (!addresses_hash.containsKey(persons[i].PersonId)) {

    persons[i].addresses = addresses_hash[persons[i].PersonId];

    }

    }

    В первом цикле создается Hash-таблица, доступ к элементам которой достаточно быстрый, зависит от реализации, но тут уже есть магия быстрого доступа. Второй цикл использует эту таблицу, и такой вариант будет выполняться persons.length + address.length раз. Если в каждом из массивов по 1000 элементов, то теперь все выполнится за 2000 раз.

    Если вы работаете с C# и предпочитаете LINQ, то первый цикл можно реализовать в одну строку:

    addresses.toLookup(m => m.PersonId);

    Казалось бы, достаточно простой код и очень простая проблема, но я продолжаю регулярно оптимизировать подобные ошибки. Именно поэтому я решил добавить этот пример в книгу, хотя по хорошей оптимизации кода можно писать отдель­ную книгу, и такие есть — на обложке обычно написано слово "алгоритмы". Кон­кретную рекомендовать не буду, потому что те, что я читал, скорее всего, уже не выпускаются.

    1. Асинхронный код

    В современных языках все больше появляется поддержки асинхронности, потому что это действительно позволяет сэкономить ресурсы сервера, хотя правильнее ска­зать - эффективнее использовать ресурсы сервера.

    Когда запрос поступает на сервер, то код начинает обрабатывать данные и форми­ровать ответ. В языках с поддержкой многопоточности для каждого запроса поль­зователя может выделяться отдельный поток, и таким образом сервер будет обра­батывать множество запросов одновременно.

    Но количество потоков не безгранично, нельзя просто выделить миллион потоков для всех пользователей, которые могут пытаться загрузить сайт.

    Допустим, что на сервер поступил запрос от пользователя загрузить страницу index. Web-сервер начинает выполнять код и находит команды, которым нужно по­лучить какие-то данные от сервера баз данных. В этот момент на сервер базы дан­ных направляется SQL-запрос, который обрабатывается в базе, а поток на web- сервере останавливается и ожидает ответа от SQL-сервера.

    Зачем сидеть и ждать ответа, на обработку которого может уйти даже секунда. Вместо этого можно обрабатывать какие-то другие пользовательские данные.

    Если в вашем приложении есть код, который ожидает обработки со стороны других серверов или сервисов, то вместо ожидания можно выполнять другие действия, и тут очень хорошо помогает асинхронное выполнение кода.

    Асинхронный код не может сделать так, чтобы код выполнялся быстрее, он будет работать с той же скоростью, как и при синхронном выполнении. Асинхронность не про скорость, а про более эффективное использование ресурсов.

    1. Блокировки

    Как еще хакер может произвести DoS-атаку? Если на web-сайте найдена ошибка, позволяющая реализовать SQL-инъекцию, то хакер может сформировать собствен­ный SQL-запрос, который будет нагружать систему. У каждой СУБД есть запросы, выполнение которых требует значительного процессорного времени. Например, для Oracle это просмотр текущих блокировок:

    SELECT *

    FROM v$lock, v$session WHERE v$lock.sid = v$session.sid and v$session. username = USER

    Если у web-сервера есть заблокированные ресурсы, то выполнение такого SQL- запроса может отнять очень много времени. А можно самому заблокировать все записи, и тогда web-сервер не сможет производить обновление данных. Например, все в той же СУБД Oracle для этого достаточно выполнить SQL-запрос:

    SELECT *

    FROM имя_таблицы FOR UPDATE

    Таким образом, установим блокировку на все записи из таблицы, и если у других пользователей запросы выполняются без опции no wait, то они будут зависать в ожидании снятия блокировки.

    Блокировки есть и в СУБД MS SQL Server. Тут необходимо учитывать, что каждая СУБД по-разному работает с блокировками. Некоторые записи блокируют данные целыми блоками или даже таблицами. Например, если изменяется только одна строка в определенной странице данных, то блокируются все записи этой страни­цы. Oracle, если не указано другого, блокирует каждую запись в отдельности. Именно поэтому определение блокировок требует значительных ресурсов.

    База данных может иметь слабое место в виде задачи, выполнение которой требует значительных ресурсов. Если web-сервер и СУБД находятся на одном физическом сервере, то это еще больше усложнит задачу по защите от данной атаки.

    1. Другие ресурсы

    Процессорное время — не единственное, что может повлиять на работу web- сервера. Он может перестать работать и при отсутствии достаточного дискового пространства. Вот тут снова возникает проблема возможности загрузки файлов на web-сервер. Хакер может загрузить столько файлов, что все пространство будет заполнено, и прием новых данных станет невозможным, ведь их негде будет сохра­нить. Это одна из причин, по которой многие программисты стремятся ограничить размер загружаемых файлов. Чем меньше возможный размер загружаемого файла, тем больше нужно приложить усилий, чтобы заполнить все дисковое пространство web-сервера. А если еще сделать ограничение на количество загрузок от опреде­ленного пользователя или IP-адреса, то возможность успеха такой атаки значитель­но уменьшается.

    Файлы — не единственное, что можно загружать. Если есть форум, то можно напи­сать программу, которая в цикле будет отправлять сообщения, содержащие Боль­шую энциклопедию. Таким образом, можно заполнить все свободное пространство СУБД, и пользователи не смогут оставлять новые сообщения, а некоторые СУБД при нехватке дискового пространстве вообще перестанут работать.

    Свободное дисковое пространство необходимо не только данным, но и журналам транзакций и журналам web-сервера. Если этого пространства не будет, то работа может быть нарушена. Желательно сделать ограничения на размер сообщений и на форумах, и в гостевых книгах, и в любых других сценариях, которые получают данные от пользователя. Например, даже через простую подписку на новости мож­но переполнить базу данных бессмысленными адресами электронной почты. Да, это уже сложнее, но при должном старании возможно все.

    Итак, при получении любых данных от пользователя и перед сохранением их в базе данных необходимо реализовать следующие две простые проверки:

    • нельзя принимать от одного пользователя более одного сообщения за опреде­ленный промежуток времени. Например, на форуме такой промежуток времени должен быть не менее 2 минут;

    • необходимо проверять размер получаемых данных перед их сохранением. На­пример, поле для хранения адреса электронной почты или имени пользователя может быть ограничено 50 символами, а вот поле для хранения сообщения на форуме может иметь тип text (или другой тип, в зависимости от базы данных). В него пользователь может загрузить огромное количество данных, и ограниче­ние на эти данные может быть очень большим, которое опять же зависит от базы данных. Определите более разумный предел. Например, для сообщения на фо­руме вполне может быть достаточно 5000 символов.

    Для особо важного функционала сайта можно реализовать каптчу. Я на своих сай­тах обязательно ставлю каптчу на добавление комментариев, потому что без нее спамеры могут замусорить базу, а любители DoS — заполнить огромным количест­вом данных, а пространство для базы на сервере у меня не резиновое. Да, мой та­риф позволяет хранить в базе данных достаточно много данных, но без каптчи ха­кер может написать скрипт, который будет бесконечно добавлять данные в базу и через какое-то время переполнит даже доступное мне пространство.

    При ограничении в 5000 символов и разрешении оставлять всего одно сообщение в две минуты, один хакер будет очень долго отправлять сообщения в надежде пере­полнить дисковое пространство сервера. Для удачной атаки тут уже понадобится сотня, а то и тысяча хакеров или один хакер с большой сетью из пользователей "зом­би". (Это пользователи интернета, компьютеры которых подвластны хакеру, и он может использовать их для реализации своих злых планов и DoS/DDoS-атак.)

    1. Оптимизация в C#

    Отдельный раздел я решил посвятить моему любимому языку C#, который облада­ет отличными возможностями, но при этом его нередко используют неверно.

    Очень частая ошибка при работе с C# — программисты не освобождают ресурсы. Платформа .NET достаточно интеллектуальная и умеет подчищать ресурсы за пользователем, но ей нужно на это время. После выполнения кода ресурсы снача­ла помечаются как неиспользуемые, а потом сборщик мусора реально освободит память.

    Посмотрим на следующий пример:

    public ActionResult Index()

    {

    SqlConnection connection = new SqlConnection(); connection.ConnectionString = "строка соединения"; connection.Open();

    return View () ;

    }

    Внимание, вопрос — когда будет закрыто соединение с базой данных? Да, соеди­нение будет закрыто за нас, и нам не нужно, по идее, заботиться о ресурсах, но вот когда эти ресурсы будут освобождены? А фиг его знает. После выполнения этого кода соединение будет все еще открытым и занятым. Реальное освобождение ре­сурсов может произойти через минуту, а может через пять. Это значит, что на сер­вере будет большое количество открытых ресурсов, запрещенных к использова­нию. Сервер не безграничен в количестве одновременно доступных соединений, тут все зависит от настроек, и когда вы дойдете до максимума, новое соединение открыть будет невозможно и сайт упадет до тех пор, пока сборщик мусора не осво­бодит неиспользуемые соединения.

    Внимание, тут я упомянул два очень важных термина — открытые и занятые со­единения. Надо понимать, что это разные вещи. Давайте посмотрим на цикл жизни соединения. Когда вы впервые открываете (вызываете метод Open) объект SqlConnection, то .NET делает следующее:

    • выделяет необходимые объекту ресурсы;

    • устанавливает соединение с базой данных.

    После этого соединение открыто и занято вашим объектом. Когда вы вызываете метод Close или Dispose, то .NET-объект SqlConnection уничтожается, а открытое соединение остается в живых на некоторое время и находится в специальном пуле. Соединение все еще открыто, но оно свободно для использования и может быть привязано к любому другому объекту SqlConnection.

    Теперь, если вы попытаетесь открыть новый объект sqlconnection, то .NET прове­рит пул на наличие уже открытых соединений с такой же строкой подключения. Если они есть, то будет создан новый объект, но новое физическое соединение с сервером устанавливаться не будет, будет использовано существующее из пула. Таким образом экономится время на обмен приветственными сообщениями с базой данных, а объект Sqlconnection практически мгновенно становится доступным к использованию.

    Я сопровождал сайт с миллионами пользователей, и у нас даже в час пик количест­во одновременно открытых соединений к базе данных не превышает 600. А вне ча­сы пик, держится на отметке в 200 соединений. Недавно мы запускали небольшое обновление и вне часа пик количество соединений взлетело до 1500, а в час пик сайт упал из-за того, что не хватило свободного места в пуле. Мы увидели пробле­му как раз тогда, когда сайт упал из-за недостаточного количества соединений. Сразу после обновления не проверили, сколько открытых соединений.

    Проблема была как раз в одном таком коде, когда открывалось соединение, но явно не закрывалось. Из-за того, что вовремя ресурсы не освобождались, мы теряли их без особого смысла. Когда я нашел проблему и исправил, количество соединений упало опять с более чем 1000 до 200.

    При разработке web-приложений, если где-то нужно открывать ресурсы, всегда ис­пользуйте конструкцию using: public ActionResult Index()

    {

    using (Sqlconnection connection = new SqlConnection())

    {

    connection.ConnectionString = "строка соединения"; connection.Open();

    }

    return View();

    }

    В этом случае .NET знает, что соединение нам нужно только на период, пока мы находимся внутри using-блока. Как только мы выходим за его пределы, платформа сразу видит, что этот ресурс нам не нужен и его можно освобождать. И тут мы точ­но можем знать (если программисты Microsoft не совершат ошибку в своем коде, что маловероятно), что соединение с базой данных будет закрыто сразу после вы­хода из блока using. Вам необязательно вызывать явно метод Close, достаточно просто использовать using.

    Сборка мусора в .NET работает отлично, главное, правильно ею пользоваться и по­могать платформе, указывая на то, когда ресурсы уже больше не нужны. Дальше она все возьмет на себя.

    Я рекомендую использовать именно using. Посмотрим на следующий пример:

    public ActionResult Index()

    {

    connection.ConnectionString = "строка соединения"; connection.Open();

    connection.Close() ; return View () ;

    }

    Казалось бы, здесь все тоже отлично, мы открываем и явно закрываем соединение, поэтому утечки не должно быть. И ее не будет, если перед открытием не произой­дет ошибки. Любая исключительная ситуация между Open и Close приведет к тому, что код прервет выполнение и соединение останется открытым. Если хакер сможет найти ситуацию, при которой генерируется исключение (например, неверный па­раметр), то он может реализовать отказ в обслуживании многократным вызовом страницы с такими неверными параметрами, чтобы соединения не освобождались, и в конце концов израсходовать все возможные соединения.

    А как насчет такого случая, когда нужно использовать соединение в разных местах программы для выполнения двух разных запросов в двух разных местах? Если using не может объять оба кода (они находятся в разных методах), то не стоит даже пытаться объять необъятное. Создайте два объекта SqlConnection и откройте со­единение дважды. Это не проблема для сервера, потому что он может использовать пул соединений Connection Pool. Сразу же после закрытия первого соединения, оно реально не будет закрыто. Будет уничтожен объект, и соединение будет помечено как свободное. В течение некоторого времени (легко конфигурируется) соединение будет оставаться открытым, и при создании следующего объекта SqlConnection, не нужно тратить время на установку соединения с сервером, можно использовать уже открытое из пула.

    В этом примере я использовал в качестве иллюстрации соединение с базой данных как наиболее популярный тип ресурса для подобных приложений. То же самое мо­жет касаться и таких ресурсов, как файлы, и, наверное, чего угодно, что требует закрытия. В таких случаях классы реализуют интерфейс Idisposable, и если вы работаете с одним из таких классов, то старайтесь использовать его вместе с using.

    Вы не обязаны закрывать соединение самостоятельно, хотя все же явное закрытие является хорошим тоном, и помните, что это позволит вам защититься от DoS- атаки.

    1   ...   10   11   12   13   14   15   16   17   18


    написать администратору сайта