Maison  >  Article  >  Opération et maintenance  >  Une analyse approfondie de la façon de mettre en œuvre l'isolation des processus de travail via le code source Nginx

Une analyse approfondie de la façon de mettre en œuvre l'isolation des processus de travail via le code source Nginx

藏色散人
藏色散人avant
2022-11-06 16:41:151949parcourir

Contexte

Récemment, notre passerelle en ligne a été remplacée par APISIX et nous avons rencontré quelques problèmes. L'un des problèmes les plus difficiles à résoudre est le problème d'isolation des processus d'APISIX.

APISIX L'influence mutuelle des différents types de requêtes

La première chose que nous avons rencontrée était le problème selon lequel le plug-in APISIX Prometheus affecte la réponse normale de l'interface métier lorsqu'il y a trop de données de surveillance. Lorsque le plug-in Prometheus est activé, les informations de surveillance collectées en interne par APISIX peuvent être obtenues via l'interface HTTP puis affichées sur un tableau de bord spécifique.

curl http://172.30.xxx.xxx:9091/apisix/prometheus/metrics

Le système commercial connecté à notre passerelle est très complexe, avec plus de 4 000 routes. Chaque fois que le plug-in Prometheus est extrait, le nombre de métriques dépasse 500 000 et la taille dépasse 80 millions. et envoyé au niveau de la couche Lua. Lorsque la requête Cela entraînera une utilisation très élevée du processeur du processus de travail qui gère cette requête, et le temps de traitement dépassera 2 s, ce qui entraînera un délai de 2 s+ pour que ce processus de travail traite normalement. demandes commerciales. [Recommandé : Tutoriel Nginx]

La mesure temporaire qui m'est venue à l'esprit à ce moment-là était de modifier le plug-in Prometheus pour réduire la portée et la quantité de collecte et d'envoi, et de contourner temporairement ce problème. Après analyse des informations collectées par le plug-in Prometheus, le nombre de données collectées est le suivant.

407171 apisix_http_latency_bucket
29150 apisix_http_latency_sum
29150 apisix_http_latency_count
20024 apisix_bandwidth
17707 apisix_http_status
  11 apisix_etcd_modify_indexes
   6 apisix_nginx_http_current_connections
   1 apisix_node_info

En fonction des besoins réels de notre entreprise, certaines informations ont été supprimées et certains délais ont été réduits.

Puis après consultation sur le problème de github (github.com/apache/apis…), j'ai découvert qu'APISIX propose cette fonction dans la version commerciale. Parce que je souhaite toujours utiliser directement la version open source, ce problème peut être contourné pour le moment, je n'approfondirai donc pas.

Mais nous avons rencontré un autre problème plus tard, c'est-à-dire que le traitement de l'API Admin n'a pas été traité à temps pendant le pic d'activité. Nous utilisons l'API Admin pour effectuer un changement de version. Pendant une période de pointe, la charge APISIX était élevée, ce qui affectait les interfaces liées à l'administration et provoquait des échecs occasionnels lors du changement de version.

La raison ici est évidente, et l'impact est bidirectionnel : le précédent plug-in Prometheus est une requête interne APISIX qui affecte les requêtes métier normales. L'inverse est vrai ici, les requêtes commerciales normales affectent les requêtes au sein d'APISIX. Par conséquent, il est crucial d’isoler les requêtes internes d’APISIX des requêtes métier normales, la mise en œuvre de cette fonction a donc pris un certain temps.

La correspondance ci-dessus générera le fichier d'exemple de configuration nginx.conf suivant comme suit. nginx.conf 配置示例文件如下。

// 9091 端口处理 Prometheus 插件接口请求
server {
    listen 0.0.0.0:9091;

    access_log off;

    location / {
        content_by_lua_block {
            local prometheus = require("apisix.plugins.prometheus.exporter")
            prometheus.export_metrics()
        }
    }
}// 9180 端口处理 admin 接口
server {
    listen 0.0.0.0:9180;
    location /apisix/admin {
        content_by_lua_block {
            apisix.http_admin()
        }
    }
}// 正常处理 80 和 443 的业务请求
server {
    listen 0.0.0.0:80;
    listen 0.0.0.0:443 ssl;
    server_name _;

    location / {
        proxy_pass  $upstream_scheme://apisix_backend$upstream_uri;

    access_by_lua_block {
        apisix.http_access_phase()
    }
}

修改 Nginx 源码实现进程隔离

对于 OpenResty 比较了解的同学应该知道,OpenResty 在 Nginx 的基础上进行了扩展,增加了 privilege

privileged agent 特权进程不监听任何端口,不对外提供任何服务,主要用于定时任务等。

我们需要做的是增加 1 个或者多个 woker 进程,专门处理 APISIX 内部的请求即可。

Nginx 采用多进程模式,master 进程会调用 bind、listen 监听套接字。fork 函数创建的 worker 进程会复制这些 listen 状态的 socket 句柄。

Nginx 源码中创建 worker 子进程的伪代码如下:

voidngx_master_process_cycle(ngx_cycle_t *cycle) {
    ngx_setproctitle("master process");
    ngx_start_worker_processes()        for (i = 0; i < n; i++) { // 根据 cpu 核心数创建子进程
            ngx_spawn_process(i, "worker process");
                pid = fork();
                ngx_worker_process_cycle()
                    ngx_setproctitle("worker process")                    for(;;) { // worker 子进程的无限循环 
                        // ...
                    }
        }
    }    for(;;) {        // ... master 进程的无限循环 
    }
}

我们要做修改就是在 for 循环中多启动 1 个或 N 个子进程,专门用来处理特定端口的请求。

这里的 demo 以启动 1 个 worker process 为例,修改 ngx_start_worker_processes 的逻辑如下,多启动一个 worker process,命令名为 "isolation process" 表示内部隔离进程。

static voidngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type){    ngx_int_t  i;    
// ...
    for (i = 0; i < n + 1; i++) { // 这里将 n 改为了 n+1,多启动一个进程

        if (i == 0) { // 将子进程组中的第一个作为隔离进程
            ngx_spawn_process(cycle, ngx_worker_process_cycle,
                              (void *) (intptr_t) i, "isolation process", type);
        } else {
            ngx_spawn_process(cycle, ngx_worker_process_cycle,
                              (void *) (intptr_t) i, "worker process", type);
        }
    }    // ...}

随后在 ngx_worker_process_cycle 的逻辑对第 0 号 worker 做特殊处理,这里的 demo 使用 18080、18081、18082 作为隔离端口示意。

static voidngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
    ngx_int_t worker = (intptr_t) data;
    
    int ports[3];
    ports[0] = 18080;
    ports[1] = 18081;
    ports[2] = 18082; 
    ngx_worker_process_init(cycle, worker);

    if (worker == 0) { // 处理 0 号 worker 
        ngx_setproctitle("isolation process");        ngx_close_not_isolation_listening_sockets(cycle, ports, 3);
    } else { // 处理非 0 号 worker
        ngx_setproctitle("worker process");        ngx_close_isolation_listening_sockets(cycle, ports, 3);
    }
}

这里新写了两个方法

  • ngx_close_not_isolation_listening_sockets:只保留隔离端口的监听,取消其它端口监听
  • ngx_close_isolation_listening_sockets:关闭隔离端口的监听,只保留正常业务监听端口,也就是处理正常业务

ngx_close_not_isolation_listening_sockets 精简后的代码如下:

// used in isolation processvoidngx_close_not_isolation_listening_sockets(ngx_cycle_t *cycle, int isolation_ports[], int port_num){    ngx_connection_t  *c;    int port_match = 0;    ngx_listening_t* ls = cycle->listening.elts;    for (int i = 0; i < cycle->listening.nelts; i++) {

        c = ls[i].connection;        // 从 sockaddr 结构体中获取端口号
        in_port_t port = ngx_inet_get_port(ls[i].sockaddr) ;        // 判断当前端口号是否是需要隔离的端口
        int is_isolation_port = check_isolation_port(port, isolation_ports, port_num);        // 如果不是隔离端口,则取消监听事情的处理
        if (c && !is_isolation_port) {            // 调用 epoll_ctl 移除事件监听
            ngx_del_event(c->read, NGX_READ_EVENT, 0);
            ngx_free_connection(c);
            c->fd = (ngx_socket_t) -1;
        }        if (!is_isolation_port) {
            port_match++;
            ngx_close_socket(ls[i].fd); // close 当前 fd
            ls[i].fd = (ngx_socket_t) -1;
        }
    }
    cycle->listening.nelts -= port_match;
}

对应的 ngx_close_isolation_listening_sockets

voidngx_close_isolation_listening_sockets(ngx_cycle_t *cycle, int isolation_ports[], int port_num){    ngx_connection_t  *c;    int port_match;

    port_match = 0;    ngx_listening_t   * ls = cycle->listening.elts;    for (int i = 0; i < cycle->listening.nelts; i++) {
        c = ls[i].connection;        in_port_t port = ngx_inet_get_port(ls[i].sockaddr) ;        int is_isolation_port = check_isolation_port(port, isolation_ports, port_num);        // 如果是隔离端口,关闭监听
        if (c && is_isolation_port) { 
            ngx_del_event(c->read, NGX_READ_EVENT, 0);
            ngx_free_connection(c);
            c->fd = (ngx_socket_t) -1;
        }        if (is_isolation_port) {
            port_match++;   
            ngx_close_socket(ls[i].fd); // 关闭 fd
            ls[i].fd = (ngx_socket_t) -1;
        }
    }
    cle->listening.nelts -= port_match;
}

Modifiez le code source de Nginx pour implémenter l'isolation des processus

Les étudiants qui connaissent OpenResty doivent savoir qu'OpenResty a été étendu en fonction de Nginx et a ajouté des privilèges

agent privilégié Le processus privilégié n'écoute aucun port et ne fournit aucun service au monde extérieur. Il est principalement utilisé pour le timing des tâches, etc.

Ce que nous devons faire est d'ajouter un ou plusieurs processus de travail pour gérer spécifiquement les demandes au sein d'APISIX.

Nginx adopte le mode multi-processus, et le processus maître appellera bind et écoutera pour écouter le socket. Le processus de travail créé par la fonction fork copiera les handles de socket de ces états d'écoute.

🎜🎜🎜Créé dans la source Nginx code Le pseudo-code du sous-processus de travail est le suivant : 🎜
server {
        listen 18080; // 18081,18082 配置一样
        server_name localhost;

        location / {
            content_by_lua_block {
                 local sum = 0;
                 for i = 1,10000000,1 do                    sum = sum + math.sqrt(i)
                 end
                 ngx.say(sum)
            }
        }
}

server {
    listen 28080;
    server_name localhost;

    location / {
        content_by_lua_block {
             local sum = 0;
             for i = 1,10000000,1 do                sum = sum + math.sqrt(i)
             end
             ngx.say(sum)
        }
    }
}
🎜 Ce que nous devons modifier est de démarrer 1 ou N sous-processus supplémentaires dans la boucle for, spécifiquement pour gérer les demandes de ports spécifiques. 🎜🎜La démo ici prend le démarrage d'un processus de travail comme exemple. La logique de modification de ngx_start_worker_processes est la suivante. Pour démarrer un processus de travail supplémentaire, le nom de la commande est "processus d'isolation" pour représenter le processus d'isolation interne. 🎜
ab -n 10000 -c 10 localhost:18080top -p 3355,3356,3357,3358,3359
🎜Ensuite, la logique dans ngx_worker_process_cycle effectue un traitement spécial sur le travailleur n°0. La démo ici utilise 18080, 18081 et 18082 comme ports d'isolation. 🎜
ab -n 10000 -c 10 localhost:28080top -p 3355,3356,3357,3358,3359
🎜Deux nouvelles méthodes sont écrites ici🎜
  • ngx_close_not_isolation_listening_sockets : Conserver uniquement l'écoute du port isolé et annuler l'écoute des autres ports
  • ngx_close_isolation_listening_sockets
  • code> : fermez l'écoute du port d'isolation et ne conservez que le port d'écoute professionnel normal, c'est-à-dire traitez les affaires normales
🎜ngx_close_not_isolation_listening_sockets Le simplifié le code est le suivant : 🎜
init_by_lua_block {    local process = require "ngx.process"

    local ports = {18080, 18081, 18083}    local ok, err = process.enable_isolation_process(ports)    if not ok then
       ngx.log(ngx.ERR, "enable enable_isolation_process failed")       return
    else
       ngx.log(ngx.ERR, "enable enable_isolation_process success")    end}复制代码
🎜correspondingngx_close_isolation_listening_sockets Ferme tous les ports d'isolation et ne conserve que l'écoute normale du port professionnel. Le code simplifié est le suivant. 🎜rrreee🎜De cette façon, nous avons réalisé l'isolation des processus basée sur le port Nginx. 🎜🎜🎜Vérification des effets🎜🎜🎜Ici, nous utilisons les ports 18080 ~ 18082 comme vérification de port isolé, et d'autres ports comme ports professionnels normaux. Afin de simuler la situation où la requête occupe un processeur élevé, nous utilisons ici lua pour calculer sqrt plusieurs fois afin de mieux vérifier l'équilibrage de charge des travailleurs de Nginx. 🎜rrreee🎜Tout d’abord, enregistrons l’état actuel du processus de travail. 🎜🎜🎜🎜

可以看到现在已经启动了 1 个内部隔离 worker 进程(pid=3355),4 个普通 worker 进程(pid=3356~3359)。

首先我们可以看通过端口监听来确定我们的改动是否生效。

可以看到隔离进程 3355 进程监听了 18080、18081、18082,普通进程 3356 等进程监听了 20880、20881 端口。

使用 ab 请求 18080 端口,看看是否只会把 3355 进程 CPU 跑满。

ab -n 10000 -c 10 localhost:18080top -p 3355,3356,3357,3358,3359

可以看到此时只有 3355 这个 isolation process 被跑满。

接下来看看非隔离端口请求,是否只会跑满其它四个 woker process。

ab -n 10000 -c 10 localhost:28080top -p 3355,3356,3357,3358,3359

符合预期,只会跑满 4 个普通 worker 进程(pid=3356~3359),此时 3355 的 cpu 使用率为 0。

到此,我们就通过修改 Nginx 源码实现了特定基于端口号的进程隔离方案。此 demo 中的端口号是写死的,我们实际使用的时候是通过 lua 代码传入的。

init_by_lua_block {    local process = require "ngx.process"

    local ports = {18080, 18081, 18083}    local ok, err = process.enable_isolation_process(ports)    if not ok then
       ngx.log(ngx.ERR, "enable enable_isolation_process failed")       return
    else
       ngx.log(ngx.ERR, "enable enable_isolation_process success")    end}复制代码

这里需要 lua 通过 ffi 传入到 OpenResty 中,这里不是本文的重点,就不展开讲述。

后记

这个方案有一点 hack,能比较好的解决当前我们遇到的问题,但是也是有成本的,需要维护自己的 OpenResty 代码分支,喜欢折腾的同学或者实在需要此特性可以试试。

上述方案只是我对 Nginx 源码的粗浅了解做的改动,如果有使用不当的地方欢迎跟我反馈。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer