Оптимизация серверов, не поддерживающих состояние
Чтобы понять, какие тонкости связаны с решением задачи оптимизации, рассмотрим сервер без установления логического соединения, который позволяет клиентам считывать информацию из файлов, хранящихся на диске, подключенном к компьютеру, на котором работает сервер. Чтобы исключить необходимость использования в протоколе информации о состоянии, проектировщик должен предусмотреть, чтобы в каждом клиентском запросе были указаны имя файла, позиция в файле и число считываемых байтов. Наиболее простая реализация сервера предусматривает обработку каждого запроса независимо от других: сервер открывает указанный файл, переводит указатель в заданную позицию, считывает указанное число байтов, отправляет требуемую информацию клиенту, а затем закрывает файл. Для вас. Отличные межкомнатные двери Новосибирск собираем из готовых деталей.
Например, предположим, что опытный программист, получивший задание разработать серверную программу, заметил следующее: во-первых, высоки издержки, связанные с открытием и закрытием файлов, во-вторых, иногда клиенты, использующие этот сервер, считывают лишь несколько байтов в каждом запросе, и в-третьих, клиенты обычно считывают файлы последовательно. Кроме того, программист знает по опыту, что сервер может считывать данные из буфера в памяти на несколько порядков быстрее по сравнению с чтением с диска. Поэтому для оптимизации производительности сервера программист решил вести небольшую таблицу с информацией о файле, например, такую, как показано на рис. 8.1; В сервере для поиска записи в этой таблице используются IP-адрес и номер порта протокола клиента. Такая оптимизация равносильна ведению информации о состоянии.

Программист использует IP-адрес и номер порта протокола клиента в качестве индекса в таблице и предусматривает хранение в каждой записи таблицы указателя на большой буфер данных из считываемого файла. При выдаче клиентом первого запроса сервер обращается к таблице и находит, что в ней нет записи для данного клиента. Он распределяет большой буфер для хранения данных из файла, вводит новую запись в таблицу, которая указывает на буфер, открывает указанный файл и считывает данные в буфер. Затем сервер копирует информацию из буфера при формировании ответа. При поступлении следующего запроса от того же клиента сервер находит соответствующую запись в таблице, проходит по указателю к буферу и извлекает из него данные, не открывая файл. Как только клиент прочитает весь файл, сервер освобождает буфер и удаляет запись из таблицы, в результате чего эти ресурсы становятся доступными для использования другим клиентом.
Безусловно, наш опытный программист тщательно строит программу, чтобы в ней предусматривалась проверка наличия затребованных данных в буфере и чтение новых данных в буфер из файла по мере необходимости. Сервер также сравнивает имя файла, указанное в запросе, с именем файла в записи таблицы для проверки того, продолжает ли клиент обращаться к тому же файлу, как и в предыдущем запросе.
Если клиенты действуют в соответствии с перечисленными выше предположениями и программа составлена правильно, то распределение на сервере больших файловых буферов и применение простой таблицы позволяет намного повысить производительность. Кроме того, с учетом сделанных предположений, оптимизированная версия сервера будет работать, по меньшей мере, не хуже, чем первоначальная версия, поскольку сервер будет затрачивать очень мало времени на сопровождение структур данных по сравнению с затратами времени на чтение с диска. Таким образом, создается впечатление, что такой вариант оптимизации позволяет повысить производительность без каких-либо сложностей.
Однако введение предложенной таблицы влечет за собой внешне малозаметное, но существенное изменение проекта сервера, поскольку оно равносильно введению поддержки информации о состоянии. Безусловно, если поддержка информации о состоянии будет введена непродуманно, она может привести к появлению ошибок, связанных с формированием ответа сервером. Например, если в сервере для поиска буфера используется IP-адрес и номер порта протокола, полученных от клиента, но не предусматривается проверка в запросе имени файла или смещения в файле, то запросы, поступившие как дубликаты или с нарушением исходного порядка, могут привести к тому, что сервер вернет неправильные данные. Но напомним, что программист, разработавший эту оптимизированную версию, достаточно опытен и предусмотрел проверку сервером имени файла и смещения в каждом запросе, хотя бы на тот случай, что в сети какой-то запрос будет продублирован или потерян, или в клиентской программе будет принято решение перейти к чтению из нового файла, а не продолжать последовательное чтение из старого файла. Таким образом, может показаться, что введение информации о состоянии не меняет способа формирования ответов сервером. И действительно, если программист тщательно проработал все детали, протокол по-прежнему будет работать правильно. Если это действительно так, то почему бы постоянно не использовать информацию о состоянии?
К сожалению, применение даже небольшого объема информации о состоянии может вызвать нарушение в работе сервера в случае отказа компьютеров, клиентских программ или сетей. Чтобы понять, с чем это связано, рассмотрим, что произойдет, если работа одной из клиентских программ закончится аварийно и придется выполнить ее перезапуск. Весьма велика вероятность того, что клиент запросит произвольный номер порта протокола и программное обеспечение UDP назначит новый номер, отличный от того, который был назначен в связи с выполнением предыдущих запросов. После поступления запроса от того же клиента сервер не будет иметь информации о том, что произошло аварийное завершение и перезапуск клиентской программы, поэтому распределит новый буфер для файла и новую запись в таблице. Следовательно, сервер не будет знать, что старая запись в таблице, которая использовалась для этого клиента, должна быть удалена. А если сервер не будет удалять старые записи, в конечном итоге в его таблице не останется свободного места.
Может показаться, что появление неиспользуемых записей в таблице не составляет проблемы, если в сервере будет предусмотрено удаление ненужных записей, когда потребуется место для новых записей. Например, в сервере может использоваться алгоритм удаления записей с наиболее давним использованием (LRU — Least Recently Used), во многом аналогичный алгоритму замены страниц, применяемому во многих системах виртуальной памяти. Однако в сети, где множество клиентов обращаются к одному серверу, частые аварийные завершения могут привести к тому, что из-за повторных попыток подключения одного и того же клиента вся таблица будет заполнена записями, которые никогда больше не будут использоваться. В наихудшем случае может оказаться, что каждый поступающий на сервер запрос будет вызывать удаление записи для освобождения места. Вели достаточно часто будет происходить аварийное завершение работы программы и перезагрузка одного и того же клиентского компьютера, это может привести к тому, что сервер начнет удалять записи успешно функционирующих клиентов. В результате сервер будет затрачивать все больше ресурсов на управление таблицей и буферами, чем на формирование ответов на запросы.
Программист должен тщательно продумывать решения по оптимизации работы сервера, не поддерживающего состояние, поскольку сопровождение даже небольших объемов информации о состоянии может потребовать использования дополнительных ресурсов, если часто происходит аварийный останов и перезагрузка клиентских компьютеров или если базовая сеть дублирует или задерживает сообщения.