Рубрики
Uncategorized

[бизнес-обучение] анализ способа повышения производительности серверного параллельного ввода — вывода-от основы сетевого программирования до epoll

Автор оригинала: David Wong.

байюнь

Из базовой концепции сетевого программирования

Мы часто используем протокол HTTP для передачи данных различных форматов. Фактически, нижний уровень протокола прикладного уровня HTTP основан на протоколе TCP уровня передачи. TCP обрабатывает эти данные только как серию бессмысленных потоков данных. Поэтому мы можем сказать: Клиент и сервер взаимодействуют, отправляя поток байтов по установленному соединению 。 Механизм связи архитектуры C/S должен идентифицировать сетевой адрес и информацию о номере порта обеих сторон. Для клиента нам нужно знать местоположение моего получателя данных. Мы используем сетевой адрес и порт для уникальной идентификации объекта сервера. Для сервера нам нужно знать, откуда поступают данные. Мы также используем сетевой адрес и порт для уникальной идентификации объекта клиента. Затем структура данных, используемая для уникальной идентификации обоих концов связи, называется сокетом . Соединение может быть однозначно определено адресами сокетов на обоих его концах:

(client address: client port number, server address: server port number)

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

  • Сокет (): создайте дескриптор сокета
  • Connect(): клиент устанавливает соединение с сервером, вызывая функцию connect
  • Bind(): указывает ядру подключить сокет, созданный socket (), к адресу и порту сервера, которые будут отслеживаться позже
  • Listen(): скажите ядру, чтобы оно рассматривало сокет как пассивный объект, подобный серверу (сервер является пассивным объектом, ожидающим подключения клиента, в то время как ядро считает, что сокет, созданный socket (), по умолчанию является активным объектом, поэтому функция listen() необходима, чтобы сообщить ядру о преобразовании активного объекта в пассивный объект)
  • Принять(): дождитесь запроса клиента на подключение и верните новый дескриптор подключения

Простейший сервер с одним процессом

Из-за проблем, оставшихся после истории UNIX, исходный интерфейс сокета непросто инкапсулировать такие данные, как адрес и порт. Чтобы упростить детали, на которые мы не обращаем внимания, мы фокусируемся только на всем процессе. Мы используем PHP для анализа. PHP инкапсулирует интерфейс UNIX, связанный с сокетом. Все функции, связанные с сокетом, имеют префикс socket, и вместо файлового дескриптора FD в UNIX используется дескриптор сокета типа ресурса. В следующем описании вместо файлового дескриптора FD в UNIX используется “сокет”. Простой серверный псевдокод реализации PHP выглядит следующим образом:

php

if (($listenSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP))=== false) {
    Echo 'socket creation failed';
}
if (socket_bind($listenSocket, '127.0.0.1', 8888) === false) {
    Echo 'failed to bind address and port';
}
if (socket_listen($listenSocket) === false) {
    Echo 'failed to convert active socket to passive socket';
}
while (1) {
    if (($connSocket = socket_accept($listenSocket)) === false) {
        Echo 'the client's connection request has not arrived';
    } else {
        Socket close ($listensocket); // release listening socket
        Socket_read ($connsocket); // read client data, blocking
        Socket_write ($connsocket); // returns data to the client, blocking
        
    }
    socket_close($connSocket);
}

Давайте рассмотрим этот простой процесс создания сервера:

  • Socket_create(): создайте сокет, представляющий конечную точку в установленном соединении. Первый параметр AF ﹣ INET является базовым используемым протоколом IPv4; второй параметр sock ﹣ stream указывает, что поток байтов используется для передачи данных; третий параметр SQL ﹣ TCP указывает, что этот протокол уровня является протоколом TCP. Сокет, созданный здесь, является всего лишь одной из конечных точек соединения абстрактная Концепция.
  • Socket_bind(): привязать этот сокет к определенному адресу сервера и порту. Это правда создание экземпляра Эта розетка. Параметр представляет собой абстрактный сокет, созданный вами ранее, а также ваш конкретный сетевой адрес и порт.
  • Socket_listen(): мы заметили, что только один параметр функции является ранее созданным сокетом. Некоторые студенты могут подумать, что этот шаг не нужен. Но это говорит ядру, что я сервер, и что преобразование сокета в пассивную сущность на самом деле имеет большой эффект.
  • Socket_accept(): получение запроса от клиента. Потому что после запуска сервера я не знаю, когда придет соединение с клиентом. Поэтому эту функцию необходимо вызывать непрерывно в цикле while. Если поступит запрос на подключение, будет возвращен новый сокет. Мы можем общаться с клиентом через этот новый сокет. Если нет, мы можем продолжать цикл только до тех пор, пока не поступит запрос.

Обратите внимание, что здесь я разделяю сокеты на две категории: один – прослушивающий сокет Один – подключенный сокет 。 Обратите внимание на различие между двумя сокетами, которое будет использоваться в следующем обсуждении:

  • Прослушивающий сокет: сервер прослушивает порт, который используется для представления порта ($прослушивающий сокет).
  • Сокет подключения: сервер установил соединение с клиентом. Все операции чтения и записи должны выполняться в соединительном сокете (сокет$conn).

Итак, мы анализируем этот сервер, в чем его проблемы?

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

Способ повышения производительности параллельного ввода-вывода

Поскольку производительность вышеуказанных серверов слишком низкая для одновременной обработки нескольких клиентских подключений и операций чтения-записи, отличные разработчики предложили следующие решения для повышения эффективности серверов, соответственно:

  • Многопроцессный
  • Многопоточность
  • Мультиплексирование ввода-вывода на основе одного процесса (выбор/опрос/|/epoll )

Многопроцессный

Итак, как оптимизировать один процесс? Очень просто, один процесс не подходит, поэтому многие процессы не могут обрабатывать несколько клиентских подключений одновременно? Мы подумали об этом и написали код:

Мы в основном фокусируемся на этом цикле for, который представляет начальное количество подпроцессов в общей сложности 10 раз. Мы установили его на 10. Затем мы вызываем функцию pcntl ou fork() для создания подпроцесса. Потому что подключение клиента соответствует согласию сервера. Поэтому в 10 подпроцессах после каждой вилки мы делаем системный вызов, чтобы принять и дождаться подключения клиента. Таким образом, вы можете получать соединения от 10 клиентов и предоставлять услуги чтения-записи данных для 10 клиентов одновременно с помощью 10 серверных процессов. Обратите внимание на такую деталь. Поскольку все подпроцессы создаются заранее, при поступлении запроса подпроцессы создаваться не будут, и эффективность обработки каждого запроса на подключение будет повышена. В то же время, с помощью концепции пула процессов, эти подпроцессы не перерабатываются сразу после обработки запроса на подключение. Они могут продолжать обслуживать следующий запрос на подключение клиента без повторных системных вызовов fork () и могут

  • По требованию: начните по требованию. Когда php-fpm запускается, он не запускает ни одного подпроцесса (рабочего процесса), только когда поступает запрос на подключение клиента
  • Динамический: при запуске php-fpm сначала будут запущены некоторые подпроцессы, и количество работников будет динамически корректироваться в соответствии с ситуацией во время выполнения
  • Статический: при запуске php-fpm будет запущено фиксированное количество подпроцессов, и емкость не будет расширена во время выполнения

Возвращаясь к главному, мультипроцесс действительно решает проблему, заключающуюся в том, что сервер может обрабатывать только один запрос на подключение клиента за раз. Однако этот режим обработки клиентских соединений на основе нескольких процессов по-прежнему имеет следующие недостатки:

  • Системные вызовы, такие как fork (), создают контекст переключения процесса, что неэффективно
  • Количество создаваемых процессов увеличивается по мере увеличения запросов на подключение. Например, для 100000 запросов требуется 100000 процессов, что слишком дорого
  • Адресное пространство между процессами является частным и независимым, что затрудняет обмен данными между процессами

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

Многопоточность

Поток – это логический поток, выполняемый в контексте процесса. Процесс может содержать много потоков, многие потоки выполняются в одном контексте процесса, поэтому совместное использование всего содержимого адресного пространства этого процесса решает проблему связи между процессами. В то же время, поскольку контекст потока намного меньше, чем у процесса, переключение контекста потока намного эффективнее, чем у процесса. Поток-это легкий процесс, который решает проблему низкой эффективности переключения контекста процесса. Поскольку в PHP нет концепции многопоточности, мы можем изменить только часть процесса создания в приведенном выше псевдокоде на создание потока. Код в основном похож, поэтому мы не будем повторять его здесь.

Мультиплексирование ввода-вывода

Мы говорили о том, как обрабатывать несколько сокетов одновременно, увеличивая количество процессов и потоков. Мультиплексирование ввода-вывода требует только одного процесса для обработки нескольких сокетов. Термин мультиплексирование ввода-вывода выглядит очень сложным и глубоким. Фактически, основными результатами этой технологии являются: Серверный процесс может обрабатывать несколько дескрипторов сокетов одновременно

  • Несколько : несколько клиентских соединений (соединения являются дескрипторами сокетов)
  • мультиплексирование : с помощью одного процесса вы можете обрабатывать несколько клиентских подключений одновременно

В предыдущем обсуждении серверный процесс может обрабатывать только одно соединение одновременно. Если вы хотите обрабатывать несколько клиентских подключений одновременно, вам потребуется помощь нескольких процессов или потоков, что не может избежать затрат на переключение контекста. Технология мультиплексирования ввода-вывода решает проблему переключения контекста. Разработку технологии мультиплексирования ввода – вывода можно разделить на три этапа: Выбор – > опрос – > epoll.

Суть мультиплексирования ввода-вывода заключается в добавлении администратора коллекции сокетов Оно может. Прослушивание нескольких сокетов одновременно . Из-за случайности подключения клиента и событий чтения-записи нам нужен этот администратор для планирования событий нескольких сокетов в одном процессе.

выбирать

Оригинал Администратор коллекции сокетов Это системный вызов select (), который может управлять несколькими сокетами одновременно. Функция select() уведомляет основной процесс сервера, когда состояние сокета или сокетов изменяется с нечитаемого на читаемое или доступное для записи. Таким образом, вызов функции select () сам по себе блокируется. Но мы не знаем, какой сокет или какой сокет становится доступным для чтения или записи, поэтому нам нужно просмотреть все сокеты, возвращенные select (), чтобы определить, какой сокет может быть обработан. И эти гнезда можно разделить на гнездо для прослушивания И подключенный разъем (упомянутый выше). Мы можем использовать функцию socket [u select (), предоставляемую PHP. В прототипе функции select () сокеты разделены на два класса: чтение, запись и коллекция сокетов исключений, которая соответственно прослушивает события чтения, записи и исключения сокетов.:

function socket_select (array &$read, array &$write, array &$except, $tv_sec, $tv_usec = 0) {}

Например, если клиент подключается к прослушивающему сокету сервера On ($listensocket), статус прослушивающего сокета изменится с нечитаемого на читаемый. Поскольку существует только один прослушивающий сокет, функция select () по-прежнему заблокирована для обработки в прослушивающем сокете. Прослушивающий сокет существует на протяжении всего жизненного цикла сервера, поэтому реализация select () не отражает оптимизированное управление прослушивающим сокетом. Когда сервер использует функцию accept() для приема нескольких клиентских подключений и генерирует несколько подключенных сокетов После этого будет отражено управление функцией select (). В это время список мониторов select () содержит Прослушивающий сокет И подведем итоги Куча Вновь созданный подключенный сокет . В это время возможно, что все клиенты, установившие соединение, будут отправлять данные через сокет подключения и ждать, пока сервер их получит. Если имеется пять соединительных разъемов с одновременной передачей данных,

 $val) {
                    If ($Val = = $read) unset ($read_socks [$key]); // remove the invalid socket
                }
                foreach ($write_socks as $key => $val) {
                    if ($val == $read) unset($write_socks[$key]);
                }
                socket_close($read);
            }Else {// can read data from the connection socket. Now $read is the connection socket
                if (in_array($read, $tmp_writes)) {
                    Socket_write ($read, $data); // if the client can write the data back to the client
                }
            }
        }
    }
}
socket_close($listenSocket);

Однако вызов самой функции select() блокируется. Потому что select () необходимо дождаться, пока сокет с изменением статуса (например, прослушивание сокета или подключение сокета с нечитаемого на читаемый) разблокирует сам select () и продолжит обработку сокета, готового к чтению и записи. Хотя здесь он заблокирован, он может возвращать несколько готовых сокетов одновременно, вместо обработки только одного сокета в одном процессе, что значительно повышает эффективность. Подводя итог, select() имеет следующие преимущества:

  • Реализуйте одновременное и централизованное управление несколькими сокетами
  • Путем обхода всех наборов сокетов можно получить все готовые сокеты, и операции с этими готовыми сокетами не будут блокироваться

Однако с select()существует несколько проблем:

  • Существует ограничение на количество дескрипторов сокетов, управляемых select. В UNIX процесс одновременно прослушивает не более 1024 дескрипторов сокетов
  • Когда select возвращается, вы не знаете, какой дескриптор сокета готов, поэтому вам нужно просмотреть все сокеты, чтобы определить, какой из них готов, и вы можете продолжить чтение и запись

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

опрос

Опрос решает проблему максимального количества дескрипторов сокетов, приведенных select . Поскольку расширение сокета PHP не имеет реализации, соответствующей опросу, вот реализация прототипа языка UNIX C:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

Параметр FDS опроса задает массивы сокетов чтения, записи и исключения select, которые объединяются в один. В опросе нет ограничения в 1024 FDS. Когда состояние некоторых дескрипторов меняется и они готовы, опрос возвращается точно так же, как select. К сожалению, мы также не знаем, какая розетка или розетка готова. Нам все еще нужно просмотреть набор сокетов, чтобы определить, какой сокет готов. Это не решает вторую проблему выбора, только что упомянутую. Мы можем резюмировать, что как реализации выбора, так и реализации опроса должны пройти все дескрипторы сокетов, чтобы получить готовые дескрипторы сокетов после возврата. Фактически, большое количество клиентов, подключенных одновременно, может одновременно находиться только в нескольких состояниях готовности, поэтому по мере увеличения числа отслеживаемых дескрипторов его эффективность также будет линейно снижаться. Чтобы решить проблему незнания того, какие или какие дескрипторы готовы после возврата, и избежать обхода всех дескрипторов сокетов, умные разработчики изобрели epoll

epoll

Epoll является администратором самых передовых сокетов, который решает вышеуказанные проблемы при выборе и опросе. Он делит заблокированный системный вызов select и poll на три этапа. Выбор или опрос можно рассматривать как состоящий из одного epoll [создать], нескольких epoll [CTL] и нескольких epoll [ждать]:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • Эполл? Создать(): создайте экземпляр epoll. Последующие операции будут использовать
  • Epoll_ctl(): добавьте, удалите и измените набор дескрипторов сокетов и сообщите ядру, какие события ему необходимо прослушивать для дескрипторов сокетов
  • Epoll UU wait(): ожидание Событий подключения (прослушивание дескрипторов сокетов) или Событий чтения и записи (будет происходить только дескриптор сокета подключения.). Если одно или несколько событий сокета готовы, эти готовые сокеты возвращаются

Похоже, что эти три функции четко разделены от функции выбора и опроса на три функции. Операция добавления, удаления и изменения дескриптора сокета изменилась с предыдущей реализации кода на вызов epoll · ctl(). Значение параметра epoll ﹣ ctl() выглядит следующим образом:

  • EPFD: возвращаемое значение epoll? Создать()
  • OP: указывает на операцию со следующим дескриптором сокета FD. Эполл? CTL? Добавить: добавить дескриптор в список прослушиваемых; epoll? CTL? Del: больше не слушаете дескриптор; epoll? CTL? Мод: изменение дескриптора
  • FD: объект дескриптора сокета описанной выше операции op (ранее в PHP это был сокет $listensocket и $conn). Например, установите сокет Добавить в Для списка мониторинга
  • Событие: сообщает ядру, какие события (такие как чтение/запись, подключение и т.д.) Ему необходимо прослушивать для дескриптора сокета

Наконец, мы вызываем epoll’ wait (), чтобы дождаться таких событий, как подключение или чтение-запись, и подготовиться к дескриптору сокета. Когда событие готово, оно сохраняется во втором параметре epoll? Структура событий. Получив доступ к этой структуре, вы можете получить дескрипторы сокетов для всех подготовленных событий. Нет необходимости просматривать все дескрипторы сокетов, такие как select и poll, прежде чем мы сможем узнать, какой из них готов. Это сокращает один o (n) обход и значительно повышает эффективность. Среди всех дескрипторов сокетов, возвращенных наконец, есть также два дескриптора, упомянутых ранее: Прослушивание дескриптора сокета и дескриптор подключенного сокета . Затем нам нужно просмотреть все готовые дескрипторы, затем определить, следует ли прослушивать или подключать дескрипторы сокетов, а затем выполнить процесс принятия или чтения в зависимости от обстоятельств. Псевдокод сервера epoll, написанный на языке C, выглядит следующим образом (с акцентом на аннотации кода)::

int main(int argc, char *argv[]) {

    Listensocket = socket (af'inet, sock'stream, 0); // as above, create a listening socket descriptor
    
    Bind (listensocket) // as above, bind address and port
    
    Listen (listensocket) // as above, convert from the default active socket to the server's applicable passive socket
    
    EPFD = epoll_create (epoll_size); // create an epoll instance
    
    Ep_events = (epoll_event *) malloc (sizeof (epoll_event) * epoll_size); // create an epoll_event structure to store socket collection
    event.events = EPOLLIN;
    event.data.fd = listenSocket;
    
    Epoll? CTL (EPFD, epoll? CTL? Add, listensocket, & event); // add the listening socket to the listening list
    
    while (1) {
    
        Event < CNT = epoll < wait (EPFD, EP < events, epoll < size, - 1); // wait for the ready socket descriptors to be returned
        
        For (int i = 0; I < event_cnt; + + I) {// traverses all ready socket descriptors
            If (ep_events [i]. Data. FD = = listensocket) {// if the listening socket descriptor is ready, a new client connection will come
            
                Connsocket = accept (listensocket); // call accept() to establish a connection
                
                event.events = EPOLLIN;
                event.data.fd = connSocket;
                
                Epoll? CTL (EPFD, epoll? CTL? Add, connsocket, & event); // add listening to the newly established connection socket descriptor to listen for subsequent read and write events on the connection descriptor
                
            }Else {// if the connection socket descriptor event is ready, it can be read or written
            
                Strlen = read (ep_events [i]. Data. FD, buf, buf_size); // read the data from the connection socket descriptor, and the data will be read without blocking
                If (strlen = = 0) {// the data cannot be read from the connection socket. You need to remove the listening on the socket
                
                    Epoll? CTL (EP FD, epoll? CTL? Del, EP? Events [i]. Data. FD, null); // delete the listening of this descriptor
                    
                    close(ep_events[i].data.fd);
                } else {
                    Write (ep_events [i]. Data. FD, buf, str_len); // if the client can write the data back to the client
                }
            }
        }
    }
    close(listenSocket);
    close(epfd);
    return 0;
}

Давайте рассмотрим структуру кода сервера мультиплексирования ввода-вывода, реализованного epoll. За исключением того, что одна функция разделена на три функции, остальная часть процесса выполнения в основном похожа на выбор и опрос. Но epoll вернет только набор готовых дескрипторов сокетов, а не набор всех дескрипторов. Эффективность ввода-вывода не будет снижаться с увеличением числа отслеживаемых FDS, что значительно повышает эффективность. В то же время он определяет и стандартизирует управление каждым дескриптором сокета (например, процесс добавления, удаления и изменения). Кроме того, нет ограничений на дескрипторы сокетов, которые он прослушивает, поэтому все остальные проблемы выбора и опроса решены.

резюме

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