Практическая сборка многопроцессорной архитектуры Nginx

сервер Nginx балансировки нагрузки Эксплуатация и обслуживание

В последнее время меня больше интересует исходный код Nginx, и с помощью мощного VS Code я начал исследование Nginx шаг за шагом, как черт знает что. О том, как отлаживать Nginx в VS Code, см. в предыдущей статье.«VS CODE легко отлаживает Nginx».

Введение

На самом деле, Nginx не нуждается в особом представлении, как известный в отрасли высокопроизводительный сервер, он широко используется интернет-компаниями.TegineОн разработан на базе Nginx.

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

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

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

2. Традиционная архитектура веб-сервера

Давайте подумаем, если бы вас попросили создать веб-сервер, что бы вы сделали?

Первый шаг — прослушивание порта

Второй шаг — обработка запроса

Прослушивание порта очень простое, как обработать запрос? Я не знаю, задавал ли учитель домашнюю работу, такую ​​как чаты, когда вы впервые начали изучать язык C в колледже? В то время я фактически полностью полагался на Baidu для его завершения: открытый мониторинг портов, получение запросов в бесконечном цикле и непосредственное открытие нового потока для обработки каждого полученного запроса.

Это конечно возможно и очень просто, и это полностью удовлетворяло моим требованиям к работе на тот момент, ведь многие веб-сервера, например tomcat, тоже так делают, назначая на каждый запрос отдельный поток. Итак, каковы недостатки этого?

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

Вторым недостатком является снижение загрузки ЦП.Учитывая текущую ситуацию только с одним потоком, когда поток ожидает сетевого ввода-вывода, он фактически находится в заблокированном состоянии.В это время ЦП находится в состоянии простоя, что непосредственно приводит к недогрузке процессора., это просто пустая трата денег!

Такая архитектура делает веб-сервер неспособным выдерживать высокий параллелизм от ядра!

3. Многопроцессорная архитектура Nginx

Nginx может поддерживать высокий параллелизм именно потому, что он отказывается от многопоточной архитектуры традиционных веб-серверов и полностью использует ЦП.

Nginx использует архитектуру с одним мастером и несколькими рабочими.Как следует из названия, мастер — это босс, а рабочий — настоящий рабочий класс.

Давайте сначала рассмотрим общую архитектуру приема запросов Nginx.

На первый взгляд кажется, что он ничем не отличается от традиционного веб-сервера, но поток справа стал рабочим. Это на самом деле тонкость Nginx.

После того, как процесс Master запустится, он разветвит N Worker-процессов. N настраивается. Вообще говоря, его можно установить на количество ядер сервера. Ставить большее значение не имеет особого смысла, это только увеличит нагрузку на ЦП переключение процессов.

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

Если читатель раньше не понимал или не использовал асинхронный ввод-вывод, пора перезарядиться. Looper в Android и Netty, известная библиотека с открытым исходным кодом на Java, основаны на асинхронном вводе-выводе, так называемом асинхронном вводе-выводе, самое большое отличие от синхронного ввода-вывода в том, что процесс не будет заблокирован в ожидании операций ввода-вывода, а может выполнять другие задачи.Когда операция ввода-вывода будет готова, операционная система будет активно уведомлять процесс.

Nginx использует эту идею.Хотя одновременно обрабатывается много запросов, нет необходимости выделять поток для каждого запроса. Какая бы запрошенная сеть не была готова к вводу-выводу, какую из них я обработаю, не так ли? Зачем создавать тему и ждать там.

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

  • Для традиционного веб-сервера школа будет посылать учителя для обслуживания каждого ученика. В школе могут быть тысячи учеников. Не означает ли это, что нужно нанимать тысячи учителей. Руководители школ могут даже не иметь возможности платить своим заработная плата. Подумайте об этом, каждый студент не может постоянно задавать вопросы, вы должны сделать перерыв! Что делает учитель, когда ученики отдыхают? Плати и не работай.
  • Для Nginx это не дает учителям возможности бездельничать.Школа имеет несколько офисов,и она нанимает несколько учителей.Когда ученик задает вопрос,на него назначается учитель,поэтому учитель будет отвечать за многие Какой ученик поднимает руку, тот идет помочь, какому ученику решить задачу.

Некоторые читатели здесь могут быть сбиты с толку, а что, если ученик продолжает занимать учителя и не отпускает его? Разве у учителя не будет возможности ответить на вопросы других учеников? Если это веб-сервер, отвечающий за бизнес-процессы, архитектура Nginx действительно может иметь такие проблемы, но помните, что Nginx в основном используется для балансировки нагрузки, его основная задача — получать и пересылать запросы, поэтому его бизнес-обработка для пересылки запроса на другие серверы, поэтому асинхронный ввод-вывод используется для приема, а асинхронный ввод-вывод также используется для пересылки.

4. Анализ исходного кода

На основе последней версии 1.15.5

4.1 Общий механизм работы

Все начинается с main().

В методе nginx main() много логики, но для того, о чем я собираюсь поговорить сегодня, наиболее важными являются две вещи:

  1. Создать сокет, слушать порт;
  2. Разветвить N рабочих процессов.

Логики в прослушивании порта особой нет, давайте взглянем на рождение процесса Worker:

static void
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
    ngx_int_t      i;
    ngx_channel_t  ch;

    ....
    for (i = 0; i < n; i++) {

        ngx_spawn_process(cycle, ngx_worker_process_cycle,
                          (void *) (intptr_t) i, "worker process", type);
        ......
    }
}

Здесь в основном создается соответствующее количество рабочих процессов в соответствии с количеством настроенных рабочих.Для создания рабочего процесса вызывается ngx_spawn_process().Второй параметр, ngx_worker_process_cycle, является новой начальной точкой дочернего процесса.

static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
    ......

    for ( ;; ) {

        ......

        ngx_process_events_and_timers(cycle);

        ......
    }
}

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

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ......

    if (ngx_use_accept_mutex) {
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;

        } else {
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }

            ......
        }
    }

    ......

    (void) ngx_process_events(cycle, timer, flags);

    ......
}

Здесь, наконец, вызывается ngx_process_events() для получения и обработки событий.

ngx_process_events() указывает на разные модули обработки асинхронного ввода-вывода на разных платформах, например epoll в Linux, но на самом деле указывает на ngx_kqueue_process_events() в модуле kqueue в Mac OS.

static ngx_int_t
ngx_kqueue_process_events(ngx_cycle_t *cycle, ngx_msec_t timer,
    ngx_uint_t flags)
{
    int               events, n;
    ngx_int_t         i, instance;
    ngx_uint_t        level;
    ngx_err_t         err;
    ngx_event_t      *ev;
    ngx_queue_t      *queue;
    struct timespec   ts, *tp;

    n = (int) nchanges;
    nchanges = 0;

    ......

    events = kevent(ngx_kqueue, change_list, n, event_list, (int) nevents, tp);

    ......

    for (i = 0; i < events; i++) {

        ......

        ev = (ngx_event_t *) event_list[i].udata;

        switch (event_list[i].filter) {

        case EVFILT_READ:
        case EVFILT_WRITE:

            ......

            break;

        case EVFILT_VNODE:
            ev->kq_vnode = 1;

            break;

        case EVFILT_AIO:
            ev->complete = 1;
            ev->ready = 1;

            break;
        ......

        }
        ......

        ev->handler(ev);
    }

    return NGX_OK;
}

Вышеупомянутое на самом деле является относительно простым способом использования kqueue. Сказав это, мы должны поговорить о том, как используется kqueue.

kqueue в основном использует два API:

// 创建一个内核消息队列,返回队列描述符
int  kqueue(void); 

// 用途:注册\反注册 监听事件,等待事件通知
// kq,上面创建的消息队列描述符
// changelist,需要注册的事件
// changelist,changelist数组大小
// eventlist,内核会把返回的事件放在该数组中
// nevents,eventlist数组大小
// timeout,等待内核返回事件的超时事件,NULL 即为无限等待
int  kevent(int kq, 
	       const struct kevent *changelist, int nchanges,
	       struct kevent *eventlist, int nevents,
	       const struct timespec *timeout);

Давайте вернемся назад и посмотрим на код в ngx_kqueue_process_events() выше, который на самом деле вызывает kevent(), чтобы дождаться возврата сообщения ядром, а затем обработать его после получения сообщения. Обработка сообщений здесь в основном предназначена для ПРИНЯТИЯ, ЧТЕНИЯ, ЗАПИСИ и так далее.

Итак, с общей точки зрения, работа модуля событий Nginx представляет собой процесс, в котором процесс Worker находится в бесконечном цикле, постоянно ожидая, пока очередь сообщений ядра вернет сообщения о событиях, и обрабатывая их.

4.2 Шокирующая проблема стада

До сих пор мы обсуждали механизм работы отдельного рабочего процесса, так существует ли какое-либо взаимодействие между каждым рабочим процессом?

Возвращаясь к приведенному выше ngx_process_events_and_timers(), перед каждым вызовом ngx_process_events() для ожидания сообщения рабочий процесс будет выполнять операцию ngx_trylock_accept_mutex(), которая на самом деле является процессом конкуренции за квалификацию прослушивания между несколькими рабочими процессами. разработан для решения шокирующей проблемы стада.

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

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

Чтобы предотвратить каждый раз захват блокировки одним и тем же процессом, Nginx разработал небольшой алгоритм, который использует коэффициент ngx_accept_disabled для усреднения вероятности получения блокировки каждым процессом.Заинтересованные студенты могут сами просмотреть этот исходный код.

5. Практическая сборка многопроцессорной архитектуры Nginx.

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

5.1 Создайте блокировку процесса, чтобы получить право прослушивать события

    mm = (mt*)mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
    memset(mm,0x00,sizeof(*mm));
    
    pthread_mutexattr_init(&mm->mutexattr);
    pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&mm->mutex,&mm->mutexattr);

5.2 Создание сокета и прослушивание порта

   // 创建套接字
    int serverSock =socket(AF_INET, SOCK_STREAM, 0);
    if (serverSock == -1)
    {
        
        printf("socket failed\n");
        exit(0);
    }
    
    //绑定ip和端口
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(9999);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    if(::bind(serverSock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
    {
        printf("bind failed\n");
        exit(0);
    }
    
    //启动监听
    if(listen(serverSock, 20) == -1)
    {
        printf("listen failed\n");
        exit(0);
    }

5.3 Создание нескольких рабочих процессов

    // fork 出 3 个 Worker 进程
    int result;
    for(int i = 1; i< 3; i++){
        result = fork();
        if(result == 0){
            startWorker(i,serverSock);
            printf("start worker %d\n",i);
            break;
        }
    }

5.4 Запустите рабочий процесс и асинхронно прослушивайте события ввода-вывода

void startWorker(int workerId,int serverSock)
{ 
    // 创建内核事件队列
    int kqueuefd=kqueue();
    struct kevent change_list[1];  //想要监控的事件的数组
    struct kevent event_list[1];  //用来接受事件的数组

    //初始化所需注册事件
    EV_SET(&change_list[0], serverSock, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0);
    
    // 循环接受事件
    while (true) {
        // 竞争锁,获取监听资格
        pthread_mutex_lock(&mm->mutex);
        printf("Worker %d get the lock\n",workerId);
        // 注册事件,等待通知
        int nevents = kevent(kqueuefd, change_list, 1, event_list, 1, NULL);
        // 释放锁
        pthread_mutex_unlock(&mm->mutex);
        //遍历返回的所有就绪事件
        for(int i = 0; i< nevents;i++){
            struct kevent event =event_list[i];
            if(event.ident == serverSock){
                // ACCEPT 事件
                handleNewConnection(kqueuefd,serverSock);
            }else if(event.filter == EVFILT_READ){
                //读取客户端传来的数据
                char * msg = handleReadFromClient(workerId,event);
                handleWriteToClient(workerId,event,msg);
            }
        }
    }
}

5.5 Открытие нескольких тестов клиентского процесса

void startClientId(int clientId)
{
    //创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    //向Server发起请求
    struct sockaddr_in serv_addr;
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(9999);  //端口
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    
    while (true) {
        //向服务器传送数据
        string s = "I am Client ";
        s.append(to_string(clientId));
    
        char str[60];
        strcpy(str,s.c_str());
        write(sock, str, strlen(str));
        
        //读取服务器传回的数据
        char buffer[60];
        if(read(sock, buffer, sizeof(buffer)-1)>0){
            printf("Client %d receive : %s\n",clientId,buffer);
        }
        
        sleep(9);
    }
}

результат операции:

Ха-ха, в основном выполнил мою просьбу.

Исходный код демо см.:

HalfStackDeveloper/LearnNginx

6. Резюме

Причина, по которой Nginx обладает сильным параллелизмом, заключается в его отличительной архитектуре: будь то многопроцессорный или асинхронный ввод-вывод, он является неотъемлемой частью Nginx. Изучать исходный код Nginx очень интересно, но читать исходный код и писать его вручную — разные вещи, просмотр исходного кода может дать только общее понимание контекста, и только делая это самостоятельно, можно действительно понять и используй это!