최근 온라인 게이트웨이가 APISIX로 대체되었으며, 해결하기 가장 어려운 문제 중 하나는 APISIX의 프로세스 격리 문제입니다.
APISIX 다양한 요청 유형의 상호 영향
가장 먼저 접한 문제는 APISIX Prometheus 플러그인이 모니터링 데이터가 너무 많을 때 정상적인 비즈니스 인터페이스 응답에 영향을 미치는 문제였습니다. Prometheus 플러그인을 활성화하면 APISIX 내부에서 수집한 모니터링 정보를 HTTP 인터페이스를 통해 가져와서 특정 대시보드에 표시할 수 있습니다.
curl http://172.30.xxx.xxx:9091/apisix/prometheus/metrics
우리 게이트웨이에 연결된 비즈니스 시스템은 4000개 이상의 경로로 매우 복잡합니다. Prometheus 플러그인을 가져올 때마다 메트릭 수가 500,000개를 초과하고 크기가 80M 이상을 초과합니다. 요청이 발생하면 이 요청을 처리하는 작업자 프로세스의 CPU 사용량이 매우 높아지고 처리 시간이 2초를 초과하여 이 작업자 프로세스가 정상적으로 처리하는 데 2초 이상의 지연이 발생합니다. 비즈니스 요청. [추천: Nginx Tutorial]
당시 떠오른 임시방편은 프로메테우스 플러그인을 수정해 수집과 전송의 범위와 양을 줄이는 것이었고, 이 문제를 일시적으로 우회했다. Prometheus 플러그인을 통해 수집된 정보를 분석한 결과, 수집된 데이터 개수는 다음과 같습니다.
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
비즈니스의 실제 요구에 따라 일부 정보가 제거되고 일부 지연이 줄어들었습니다.
그러던 중 github 이슈(github.com/apache/apis…)에 대한 상담을 진행한 결과 APISIX가 상용 버전에서 이 기능을 제공한다는 것을 알게 되었습니다. 아직은 오픈소스 버전을 직접 사용해 보고 싶기 때문에 이 문제는 당분간 우회할 수 있으므로 더 이상 다루지 않겠습니다.
그런데 나중에 또 다른 문제가 발생했습니다. 즉, 비즈니스 피크 시간에 Admin API 처리가 제때 처리되지 않았습니다. Admin API를 사용하여 버전 전환을 수행합니다. 비즈니스가 가장 많은 기간 동안 APISIX 부하가 높아서 관리 관련 인터페이스에 영향을 미치고 버전 전환 시 간헐적으로 시간 초과 오류가 발생했습니다.
이유는 분명하며 영향은 양방향입니다. 이전 Prometheus 플러그인은 일반적인 비즈니스 요청에 영향을 미치는 APISIX 내부 요청입니다. 여기서는 그 반대입니다. 일반적인 비즈니스 요청은 APISIX 내의 요청에 영향을 미칩니다. 따라서 APISIX 내부 요청을 일반 비즈니스 요청과 분리하는 것이 중요하므로 이 기능을 구현하는 데 시간이 걸렸습니다.
위의 대응은 다음 nginx.conf
구성 예제 파일을 생성합니다. 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; }
Nginx 소스 코드를 수정하여 프로세스 격리 구현
OpenResty에 익숙한 학생은 OpenResty가 Nginx를 기반으로 확장되고 권한이 추가되었다는 점을 알아야 합니다
권한 있는 에이전트 권한 있는 프로세스는 어떤 포트도 수신하지 않으며 해당 프로세스에 어떤 서비스도 제공하지 않습니다. 외부 세계. 주로 타이밍 등에 사용됩니다. 우리가 해야 할 일은 APISIX 내에서 요청을 구체적으로 처리하기 위해 하나 이상의 작업자 프로세스를 추가하는 것입니다.Nginx는 다중 프로세스 모드를 채택하고 마스터 프로세스는 바인딩을 호출하고 소켓을 수신합니다. 포크 기능으로 생성된 작업자 프로세스는 이러한 청취 상태의 소켓 핸들을 복사합니다.
🎜🎜🎜Nginx 소스에서 생성됨 code 작업자 하위 프로세스의 의사 코드는 다음과 같습니다. 🎜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) } } }🎜 수정해야 할 것은 특히 특정 포트에 대한 요청을 처리하기 위해 for 루프에서 1개 이상의 하위 프로세스를 시작하는 것입니다. 🎜🎜여기 데모에서는 하나의 작업자 프로세스를 시작하는 것을 예로 들어 ngx_start_worker_processes를 수정하는 논리는 다음과 같습니다. 하나 이상의 작업자 프로세스를 시작하려면 명령 이름이 내부 격리 프로세스를 나타내는 "격리 프로세스"입니다. 🎜
ab -n 10000 -c 10 localhost:18080top -p 3355,3356,3357,3358,3359🎜그런 다음
ngx_worker_process_cycle
의 로직은 작업자 번호 0에 대해 특수 처리를 수행합니다. 여기 데모에서는 18080, 18081 및 18082를 격리 포트로 사용합니다. 🎜ab -n 10000 -c 10 localhost:28080top -p 3355,3356,3357,3358,3359🎜여기에 두 가지 새로운 방법이 작성되었습니다🎜
ngx_close_not_isolation_listening_sockets
: 격리된 포트만 청취하고 다른 포트는 청취를 취소합니다. ngx_close_isolation_listening_sockets
ngx_close_not_isolation_listening_sockets
단순화된 🎜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 모든 격리 포트를 닫고 일반 비즈니스 포트 청취만 유지합니다. 단순화된 코드는 다음과 같습니다. 🎜rrreee🎜이런 방식으로 Nginx 포트 기반 프로세스 격리를 달성했습니다. 🎜🎜🎜효과 검증🎜🎜🎜여기서는 18080~18082 포트를 격리 포트 검증으로 사용하고, 그 외의 포트는 일반 업무용 포트로 사용합니다. 요청이 높은 CPU를 차지하는 상황을 시뮬레이션하기 위해 여기서는 Nginx의 작업자 로드 밸런싱을 더 잘 검증하기 위해 lua를 사용하여 sqrt를 여러 번 계산합니다. 🎜rrreee🎜먼저 현재 작업자 프로세스 상태를 기록해 보겠습니다. 🎜🎜🎜🎜
可以看到现在已经启动了 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 源码的粗浅了解做的改动,如果有使用不当的地方欢迎跟我反馈。
위 내용은 Nginx 소스 코드를 통해 작업자 프로세스 격리를 구현하는 방법에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!