Rumah  >  Artikel  >  Operasi dan penyelenggaraan  >  Analisis mendalam tentang cara melaksanakan pengasingan proses pekerja melalui kod sumber Nginx

Analisis mendalam tentang cara melaksanakan pengasingan proses pekerja melalui kod sumber Nginx

藏色散人
藏色散人ke hadapan
2022-11-06 16:41:152022semak imbas

Latar Belakang

Baru-baru ini, gerbang dalam talian kami telah digantikan dengan APISIX, dan kami menghadapi beberapa masalah Salah satu masalah yang lebih sukar untuk diselesaikan ialah masalah pengasingan proses APISIX.

Pengaruh bersama pelbagai jenis permintaan APISIX

Perkara pertama yang kami hadapi ialah masalah bahawa pemalam APISIX Prometheus mempengaruhi tindak balas antara muka perniagaan biasa apabila terdapat adalah terlalu banyak data pemantauan. Apabila pemalam Prometheus didayakan, maklumat pemantauan yang dikumpul secara dalaman oleh APISIX boleh diperolehi melalui antara muka HTTP dan kemudian dipaparkan pada papan pemuka tertentu.

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

Sistem perniagaan yang disambungkan ke gerbang kami sangat kompleks, dengan 4000 laluan Setiap kali pemalam Prometheus ditarik, bilangan metrik melebihi 500,000 dan saiznya melebihi 80M maklumat perlu berada dalam lapisan lua Memasang dan menghantar, apabila permintaan dibuat, penggunaan CPU proses pekerja yang mengendalikan permintaan ini akan menjadi sangat tinggi, dan masa pemprosesan akan melebihi 2s, mengakibatkan kelewatan 2s untuk pekerja ini proses untuk memproses permintaan perniagaan biasa. [Disyorkan: Tutorial Nginx]

Langkah sementara yang terlintas di fikiran pada masa itu ialah mengubah suai pemalam Prometheus untuk mengurangkan julat dan kuantiti pengumpulan dan penghantaran, dan memintasnya buat sementara waktu dahulu Masalah ini. Selepas menganalisis maklumat yang dikumpul oleh pemalam Prometheus, bilangan data yang dikumpul adalah seperti berikut.

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

Berdasarkan keperluan sebenar perniagaan kami, beberapa maklumat telah dialih keluar dan beberapa kelewatan telah dikurangkan.

Kemudian selepas berunding mengenai isu github (github.com/apache/apis… ), saya mendapati bahawa APISIX menyediakan fungsi ini dalam versi komersial. Kerana saya masih mahu menggunakan versi sumber terbuka secara langsung, masalah ini boleh dipintas buat sementara waktu, jadi saya tidak akan menyelidiki lebih lanjut.

Tetapi kami menghadapi masalah lain kemudian, iaitu pemprosesan API Admin tidak dikendalikan tepat pada masanya semasa puncak perniagaan. Kami menggunakan API Pentadbiran untuk melakukan penukaran versi Semasa tempoh perniagaan puncak, beban APISIX adalah tinggi, yang menjejaskan antara muka berkaitan Pentadbiran dan menyebabkan kegagalan masa tamat sekali-sekala dalam penukaran versi.

Sebab di sini jelas dan impaknya adalah dua hala: pemalam Prometheus sebelumnya ialah permintaan dalaman APISIX yang mempengaruhi permintaan perniagaan biasa. Perkara sebaliknya berlaku di sini, permintaan perniagaan biasa mempengaruhi permintaan dalam APISIX. Oleh itu, adalah penting untuk mengasingkan permintaan dalaman APISIX daripada permintaan perniagaan biasa, jadi ia mengambil sedikit masa untuk melaksanakan fungsi ini.

Korespondensi di atas akan menghasilkan yang berikut nginx.conf Fail contoh konfigurasi adalah seperti berikut.

// 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()
    }
}

Ubah suai kod sumber Nginx untuk mencapai pengasingan proses

Pelajar yang biasa dengan OpenResty harus tahu bahawa OpenResty telah dikembangkan berdasarkan Nginx dan keistimewaan tambahan

ejen istimewa Proses istimewa tidak mendengar sebarang port dan tidak menyediakan sebarang perkhidmatan kepada dunia luar Ia digunakan terutamanya untuk tugas yang dijadualkan, dsb.

Apa yang perlu kita lakukan ialah menambah satu atau lebih proses pekerja untuk mengendalikan permintaan secara khusus dalam APISIX.

Nginx menggunakan mod berbilang proses, dan proses induk akan memanggil bind dan mendengar untuk mendengar soket. Proses pekerja yang dicipta oleh fungsi garpu akan menyalin pemegang soket bagi keadaan mendengar ini.

Pseudokod untuk mencipta sub-proses pekerja dalam kod sumber Nginx adalah seperti berikut:

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 进程的无限循环 
    }
}

Apa yang perlu kita ubah suai adalah untuk memulakan satu lagi atau lebih dalam sub-proses untuk gelung N, direka khas untuk mengendalikan permintaan untuk port tertentu.

Demo di sini mengambil satu proses pekerja sebagai contoh Logik pengubahsuaian ngx_start_worker_processes adalah seperti berikut. Nama arahan ialah "proses pengasingan".

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);
        }
    }    // ...}

Kemudian logik dalam ngx_worker_process_cycle melakukan pemprosesan khas untuk pekerja No. 0. Demo di sini menggunakan 18080, 18081 dan 18082 sebagai port pengasingan.

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);
    }
}

Dua kaedah baharu ditulis di sini

  • ngx_close_not_isolation_listening_sockets: Hanya pastikan pemantauan pelabuhan terpencil dan batalkan pemantauan pelabuhan lain
  • ngx_close_isolation_listening_sockets: Matikan pemantauan port terpencil dan simpan hanya port pemantauan perniagaan biasa, iaitu untuk memproses perniagaan biasa

ngx_close_not_isolation_listening_sockets Kod yang dipermudahkan adalah seperti berikut:

// 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;
}

sepadan dengan ngx_close_isolation_listening_sockets Tutup semua port pengasingan dan simpan hanya port perniagaan biasa untuk pemantauan Kod yang dipermudahkan adalah seperti berikut.

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;
}

Dengan cara ini, kami telah mencapai pengasingan proses berasaskan port Nginx.

Pengesahan Kesan

Di sini kami menggunakan port 18080~18082 sebagai pengesahan port pengasingan dan port lain sebagai port akhir perniagaan biasa. Untuk mensimulasikan situasi di mana permintaan itu menduduki CPU yang tinggi, di sini kami menggunakan lua untuk mengira sqrt beberapa kali untuk mengesahkan pengimbangan beban pekerja Nginx dengan lebih baik.

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)
        }
    }
}

Pertama, mari kita rekod status proses pekerja semasa.

可以看到现在已经启动了 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 源码的粗浅了解做的改动,如果有使用不当的地方欢迎跟我反馈。

Atas ialah kandungan terperinci Analisis mendalam tentang cara melaksanakan pengasingan proses pekerja melalui kod sumber Nginx. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:juejin.im. Jika ada pelanggaran, sila hubungi admin@php.cn Padam