= 基于Nginx的高可用分布式网关开发手册 == 一. 开发环境搭建 === 1. 开发环境组成 * 操作系统:Windows 11(需要支持WSL2,WSL2为Ubuntu-22.04); * 容器: Docker Desktop * IDE: Visual Studio Code * 编程语言:C语言为主 * 编译器: GCC * 调试工具:gdb * 其他工具:kimi.ai === 2. 开发环境安装 暂略 === 3. 开发环境截图 * WSL2:不想装虚拟机软件,就用WSL2替代了 在windows系统查看wsl版本号: image::./imgs/wsl.png[WSL版本] 进入wsl后,查看Linux系统版本号: image::./imgs/wsl-linux.png[Linux版本号] * Docker Desktop:通过创建多个docker容器模拟分布式计算环境:Nginx集群+后端服务器集群 Docker Desktop设置截图: image::./imgs/DockerDesktop.png[Docker Desktop] 其中,Docker镜像仓库代理设置参考如下: [source,json] ---- { "builder": { "gc": { "defaultKeepStorage": "20GB", "enabled": true } }, "experimental": false, "registry-mirrors": [ "https://dockerproxy.com", "https://hub-mirror.c.163.com", "https://mirror.baidubce.com", "https://ccr.ccs.tencentyun.com" ] } ---- * Visual Studio Code,建议安装以下插件:插件安装方法见Visual Studio Code官网 1. C语言的插件群,安装方法见:link:https://code.visualstudio.com/docs/languages/cpp[C语言插件] 2. 安装WSL插件(可选):远程连接WSL子系统,使用WSL子系统作为编译和运行环境,详见link:https://code.visualstudio.com/docs/remote/wsl-tutorial[在WSL进行远程开发]; 3. 安装Dev Container插件:远程连接Docker容器,以将Docker容器作为Nginx组件开发和调试环境,详见:link:https://code.visualstudio.com/docs/devcontainers/create-dev-container[创建Dev Container] image::./imgs/dev-container.png[运行中的Dev Container] image::./imgs/dev-container-2.png[Visual Studio Code连接Dev Container] == 二. Nginx原理 === 1. 下载Nginx源码 link:https://nginx.org/en/download.html[下载地址] image::./imgs/download-nginx-src.png[下载Nginx源码] === 2. 编译Nginx源码 编译Nginx之前,需要提前安装GCC、Make、GDB等编译工具,安装过程略。 Nginx源码结构(不同下载路径,结构略有不同) image::./imgs/nginx-src-structure.png[Nginx源码结构] 编译方法:link:https://nginx.org/en/docs/configure.html[编译Nginx源代码] === 3. 一个开源实例模块 开源地址:link:https://github.com/yaoweibin/nginx_upstream_check_module[nginx_upstream_check_module] === 4. 编译开源模块 在编译之前,需要参照开源模块的README文件,对nginx源码打补丁,否则主动探测功能会失败: [source,shell] ---- cd /path/to/nginx_source_code/nginx-1.26.0/ patch -p1 < /path/to/nginx_http_upstream_check_module/check_1.20.1+.patch ---- 为了便于编译,我们将编译命令和参数写成shell脚本(`configure-health-check.sh`): [source,shell] ---- #!/bin/sh # -g3 -gdwarf-2 允许在gdb调试时查看macro definitions以及macro expansion # -O0 -DNDEBUG 允许compiler debugging # --with-debug 开启nginx debugging # -fexec-charset=GBK 允许nginx输出中文 ./configure --with-cc-opt="-O0 -DNDEBUG -fexec-charset=GBK -g3 -gdwarf-2 -fsanitize=address -DNGX_DEBUG_PALLOC=1" --with-debug --prefix=/opt/nginx --with-http_ssl_module --add-module=/workspaces/cpp-5/nginx_upstream_check_module ---- 其中,+++--prefix+++表示nginx安装目录;+++--with-debug+++允许nginx打开debug模式,并与+++-O0 -g3 -DNDEBUG -gdwarf-2+++配合,避免由于编译优化导致部分调试信息丢失;+++--add-module+++指定开源模块的源码路径。 +++-fsanitize=address -DNGX_DEBUG_PALLOC=1+++将屏蔽nginx memory pool,并允许内存debug 使用以下命令执行脚本: [source,shell] ---- sh configure-health-check.sh ---- 然后再分别执行make和make install安装nginx: [source,shell] ---- make make install ---- 清理编译结果: [source,shell] ---- make clean ---- === 5. 调试Nginx 执行以下命令,进入gdb调试界面 [source,shell] ---- gdb /opt/nginx/sbin/nginx ---- image::./imgs/gdb-entry.png[打开gdb调试] 在gdb命令行中开启多线程调试:当主进程fork出子进程后,将自动对子进程调试 [source,shell] ---- set follow-fork-mode child set detach-on-fork on ---- image::./imgs/gdb-mutil-threads.png[多线程调试开关] 在想要调试的地方设置断点后,开始调试: [source,shell] ---- br ngx_http_upstream_check_status_handler ... start ... cont .... ---- image::./imgs/debug-nginx.png[调试nginx] 关于gdb调试指令可参考:link:https://wizardforcel.gitbooks.io/100-gdb-tips/content/index.html[gdb调试小技巧] == 三. 网关原理 网关可以执行多种功能,包括路由、负载均衡、认证、授权、限流、监控和安全等。负载均衡是网关最基本的功能之一。负载均衡器中的主动探测和被动探测是两种不同的健康检查机制,用于确定后端服务器或服务实例是否能够正常处理请求。以下是它们的主要区别: [%autowidth] |=== | |主动探测(Active Probing) | 被动探测(Passive Probing) |定义| 负载均衡器定期向后端服务器发送探测请求(如HTTP请求、TCP连接或ICMP消息),以检查它们是否健康|负载均衡器通过监控经过它的实际用户请求来检测后端服务器的健康状况 | 依赖| 不依赖于用户请求 | 依赖于用户请求的响应来评估后端服务器的状态,不会主动发送额外的探测请求 |类型|可以是简单的TCP连接尝试,或者更复杂的应用层探测,如发送HTTP HEAD请求并检查HTTP状态码|取决于用户请求 |频率|根据需要设置探测的时间间隔|取决于用户请求频率 |响应|如果后端服务器在指定的时间内响应了探测请求并且返回了健康的响应,那么它被认为是健康的|如果用户请求得到了成功的响应(如HTTP 200 OK),则认为后端服务器是健康的;如果响应表明错误(如HTTP 5xx错误),则认为服务器可能不健康 |优点|及时发现不健康的服务器,在用户请求到达之前将其从负载均衡池中移除|仅依赖于实际的用户请求,不会给后端服务器带来额外的负载 |缺点|频繁的探测请求可能给后端服务器带来额外的负载|只有在用户请求到达不健康的服务器时才会被检测到,可能无法及时发现不健康的服务器 |=== 本文档以link:https://github.com/yaoweibin/nginx_upstream_check_module[nginx_upstream_check_module]为例,深入理解nginx的主动探测原理。 === 3.1 源码解读 ==== 3.1.1 配置与编译 ngx_http_upstream_check_module模块的 `config` 文件内容如下: [source,shell,numbered] ---- ngx_feature="ngx_http_upstream_check_module" ngx_feature_name= ngx_feature_run=no ngx_feature_incs= ngx_feature_libs="" ngx_feature_path="$ngx_addon_dir" ngx_feature_deps="$ngx_addon_dir/ngx_http_upstream_check_module.h" ngx_check_src="$ngx_addon_dir/ngx_http_upstream_check_module.c" ngx_feature_test="int a;" . auto/feature if [ $ngx_found = yes ]; then have=NGX_HTTP_UPSTREAM_CHECK . auto/have CORE_INCS="$CORE_INCS $ngx_feature_path" ngx_addon_name=ngx_http_upstream_check_module HTTP_MODULES="$HTTP_MODULES ngx_http_upstream_check_module" NGX_ADDON_DEPS="$NGX_ADDON_DEPS $ngx_feature_deps" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_check_src" else cat << END $0: error: the ngx_http_upstream_check_module addon error. END exit 1 fi ---- **解读如下**: `auto/feature` 是 Nginx 源码树中的一个脚本文件,位于 auto 目录下。这个脚本的作用是自动检测当前编译环境是否支持某些特定的系统特性(features),并据此设置编译配置。这对于确保 Nginx 能够在不同操作系统和硬件平台上以最优方式运行至关重要。该脚本主要功能是: . **检测特性**: 检查编译环境中是否存在 Nginx 所需的系统调用、库函数或编译器特性。 . **设置宏定义**: 如果检测到特性存在,则在 Nginx 的配置头文件中定义相应的宏,以便编译时启用相关的特性代码。 . **生成配置**: 根据检测结果,生成或更新配置文件,如 `nginx.conf` 和 `ngx_auto_config.h`(低版本Nginx该文件名为 `auto_config.h`)。 `auto/feature` 脚本通过命令行参数指定要检测的特性。以下是一些常见的参数: * `ngx_feature`: 特性的名称,用于日志输出。 * `ngx_feature_name`: 这个参数预期是一个宏名称字符串,它用于在 Nginx 的配置头文件(通常是 `ngx_auto_config.h`)中定义一个宏,以指示编译器和运行时环境该特性是可用的。 * `ngx_feature_run`: 这个参数的值通常为 `yes` 或者不设置(空)。如果设置为 `yes`,则表示编译后的测试程序需要被执行,以确保不仅编译通过了,而且程序在运行时也能正确工作。如果不设置或者留空,编译后的程序将不会被执行,仅仅编译成功即认为特性支持。。 * `ngx_feature_incs`: 这个参数预期是一个或多个头文件的列表,这些头文件对于编译测试程序来说是必需的,它们通常用于声明用于检测特定系统特性的函数、结构或宏。 * `ngx_feature_path`: 头文件的搜索路径。 * `ngx_feature_libs`: 编译测试程序时需要链接的库。 * `ngx_feature_test`: 这个参数是一个字符串,包含了用于检测特性的 C 语言代码片段。这段代码通常包括函数调用、结构体定义、宏检查或其他编译时的检查,旨在验证系统是否提供了所需的功能。 注意,`ngx_check_src` 和 `ngx_feature_deps` 并不是feature脚本内置参数,而是用户自定义参数。 `auto/feature` 脚本的工作流程: . **生成测试代码**: 根据参数 `ngx_feature_test` 提供的参数,脚本生成一个 C 语言的测试程序。 . **编译测试程序**: 使用参数 `ngx_feature_incs` 和 `ngx_feature_libs` 指定的编译器选项、头文件路径和库链接选项编译测试程序。 . **运行测试**: 如果 `ngx_feature_run` 设置为 `yes` ,则执行编译后的测试程序。 . **分析结果**: 根据测试程序的编译和运行结果,确定特性是否存在。 . **更新配置**: 如果特性存在,更新配置文件和头文件,使用 `ngx_feature_name` 指定的名称定义相应的宏。 其他变量: * `NGX_ADDON_DEPS` : 主要用于指定第三方模块(被编译为 addon 模块)所依赖的其他模块或文件。这个变量在 Nginx 的 `auto/modules` 脚本中被处理,负责设置模块依赖. * `CORE_INCS` : 是一个变量,用于定义 Nginx 编译所需的头文件的搜索路径。在编译过程中,`CORE_INCS` 变量的值会被用作编译器的 `-I` 参数,这是指定头文件搜索目录的标准编译器选项。在 Nginx 的 `auto/configure` 脚本或 `auto/modules` 脚本中,`CORE_INCS` 会根据检测到的模块和特性动态地设置和更新。 * `ngx_addon_name`` :指定第三方模块的名称,是必须定义的变量, `ngx_addon_name` 被添加到模块列表中,这样在构建过程中,构建脚本就知道需要编译哪些模块 * `HTTP_MODULES` : 定义了所有编译进 Nginx 的 HTTP 模块的列表,包括 Nginx 核心 HTTP 功能以及任何第三方 HTTP 模块。 * `NGX_ADDON_DEPS` : 指定第三方模块(也称为 addon 模块)在编译时所依赖的其他文件或模块,在 Nginx 的构建脚本 auto/modules 中被使用。 * `NGX_ADDON_SRCS` : 指定构成第三方模块的所有源代码文件,在 Nginx 的构建脚本 auto/modules 中被使用。 [小技巧]: 使用kimi对config文件解读如下: ---- 这段 Nginx 第三方模块的 `config` 文件是一个脚本,用于配置和检测模块 `ngx_http_upstream_check_module`。下面是对脚本中每行的详细解释: 1. `ngx_feature="ngx_http_upstream_check_module"` 定义了正在配置的 Nginx 模块的名称。 2. `ngx_feature_name=` 通常用于定义一个宏名称,但这里留空,表示可能不需要定义特定的宏。 3. `ngx_feature_run=no` 表示不需要执行编译后的测试程序来验证特性。这可能是因为模块的存在与否不依赖于运行时检测。 4. `ngx_feature_incs=` 留空,表示没有特定的头文件需要包含。 5. `ngx_feature_libs=""` 留空,表示没有特定的库需要链接。 6. `ngx_feature_path="$ngx_addon_dir"` 设置模块的路径为 `ngx_addon_dir` 变量的值,这通常是模块的安装目录。 7. `ngx_feature_deps="$ngx_addon_dir/ngx_http_upstream_check_module.h"` 定义模块的依赖头文件,这里是模块的头文件。 8. `ngx_check_src="$ngx_addon_dir/ngx_http_upstream_check_module.c"` 指定模块的源文件路径。 9. `ngx_feature_test="int a;"` 提供一个非常简单的测试代码,这里仅仅声明了一个整型变量,实际上这个测试可能不会对模块的检测产生实际作用。 10. `. auto/feature` 执行 Nginx 的 `auto/feature` 脚本来处理配置。 11. `if [ $ngx_found = yes ]; then` 如果 `auto/feature` 脚本执行后 `ngx_found` 变量被设置为 `yes`,则继续执行以下命令。 12. `have=NGX_HTTP_UPSTREAM_CHECK . auto/have` 使用 `auto/have` 脚本定义一个宏,表示检测到 `ngx_http_upstream_check_module`。 13. `CORE_INCS="$CORE_INCS $ngx_feature_path"` 将模块的路径添加到 Nginx 核心的头文件搜索路径中。 14. `ngx_addon_name=ngx_http_upstream_check_module` 设置第三方模块的名称。 15. `HTTP_MODULES="$HTTP_MODULES ngx_http_upstream_check_module"` 将模块名称添加到 HTTP 模块列表中。 16. `NGX_ADDON_DEPS="$NGX_ADDON_DEPS $ngx_feature_deps"` 将模块的依赖项添加到第三方模块的依赖列表中。 17. `NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_check_src"` 将模块的源文件添加到第三方模块的源文件列表中。 18. `else` 如果 `ngx_found` 不是 `yes`,执行错误处理。 19. `cat << END ... END` 输出错误信息,指出 `ngx_http_upstream_check_module` 模块的添加出现错误。 20. `exit 1` 退出脚本并返回状态码 1,表示配置过程中出现错误。 整体来看,这个 `config` 文件的作用是检测 `ngx_http_upstream_check_module` 是否可以被添加到 Nginx 中,并根据检测结果更新 Nginx 的配置,以便在编译时包含这个模块。如果检测失败,则输出错误信息并退出配置过程。 ---- ==== 3.1.2 代码调试与跟踪 首先,使用gdb对编译后的nginx进行debugging [source,shell] ---- # 调式nginx gdb /opt/nginx/sbin/nginx # 载入事先准备好的断点 source gdb.cfg # 开启子进程调试 set follow-fork-mode child set set detach-on-fork on # 终端显示更加友好 set print pretty # 开始运行nginx run ---- 【此处忽略阅读代码过程...】 ==== 3.1.3 代码总结 ===== 3.1.3.1 关键数据结构 结构体: `ngx_module_t` 结构体,名为 `ngx_http_upstream_check_module` 的变量 [source,c] ---- ngx_module_t ngx_http_upstream_check_module = { NGX_MODULE_V1, &ngx_http_upstream_check_module_ctx, /* module context */ ngx_http_upstream_check_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ ngx_http_upstream_check_init_process, /* init process: 在master进程fork出workder进程的时候调用 */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; ---- 注意: **其中的回调函数 `ngx_http_upstream_check_init_process`是在Nginx master进程创建worker进程后开始执行的,是`ngx_http_uptstream_check_module`模块的入口** ,我们看看这个函数做了什么? ===== 3.1.3.2 模块入口 [source,c] ---- static ngx_int_t ngx_http_upstream_check_init_process(ngx_cycle_t *cycle) { ngx_http_upstream_check_main_conf_t *ucmcf; if (ngx_process != NGX_PROCESS_WORKER) { return NGX_OK; } ucmcf = ngx_http_cycle_get_module_main_conf(cycle, ngx_http_upstream_check_module); if (ucmcf == NULL) { return NGX_OK; } return ngx_http_upstream_check_add_timers(cycle); } ---- 1. 首先判断当前进程是不是worker进程,如果不是worker进程,则直接返回; 2. 然后,获取当前module的、已经**事先装载的配置数据**(“事先”的意思是:在创建worker进程之前、在初始化和装载配置数据阶段,此处暂略,后面再讲); 3. 最后进入 `ngx_http_upstream_check_add_timers` 函数,这是核心函数,为当前模块注册timer:定时向后端服务器发送健康检测请求,并统计检测结果。关键代码如下: [source,c] ---- static ngx_int_t ngx_http_upstream_check_add_timers(ngx_cycle_t *cycle) { ... for (i = 0; i < peers->peers.nelts; i++) { peer[i].shm = &peer_shm[i]; peer[i].check_ev.handler = ngx_http_upstream_check_begin_handler; /* 注册check事件的回调函数 */ ... peer[i].check_timeout_ev.handler = ngx_http_upstream_check_timeout_handler; /* 注册check事件的超时回调函数 */ ... cf = ucscf->check_type_conf; /* check_type_conf的赋值来自于全局变量ngx_check_types[] */ ... peer[i].send_handler = cf->send_handler; /* 发送check数据的回调函数 */ peer[i].recv_handler = cf->recv_handler; /* 接收check响应数据的回调函数 */ ... ngx_add_timer(&peer[i].check_ev, t); /* 为check事件添加定时器 */ } return NGX_OK; } ---- 需要注意几点: - 首先,Nginx的高性能秘诀是其Event模型和Event Loop机制(略,读者可自行查阅资料),同样地,这个模块为主动探测也创建了两个event变量(`ngx_http_upstream_check_peer_s` 中的 `check_ev` 和 `check_timeout_ev` 变量),当前函数就为这两个事件各注册一个回调函数(handler); - 除此以外,还有nginx内置的两个事件:`ngx_connection_t` 中的 `write` 和 `read` 事件,主要完成网络连接的IO操作,Nginx规定每个event都必须注册一个回调函数,当前函数也为 `write` 和 `read` 事件注册回调函数,回调函数的指派来自全局变量数组: `ngx_check_types` (细节藏在 `ngx_http_upstream_check_begin_handler` 函数中),我们暂时只关注通过http协议的主动探测; [source,c] ---- static ngx_check_conf_t ngx_check_types[] = { ... { NGX_HTTP_CHECK_HTTP, ngx_string("http"), ngx_string("GET / HTTP/1.0\r\n\r\n"), NGX_CONF_BITMASK_SET | NGX_CHECK_HTTP_2XX | NGX_CHECK_HTTP_3XX, ngx_http_upstream_check_send_handler, /* ngx_connection_t结构体中 write 事件的回调函数 */ ngx_http_upstream_check_recv_handler, /* ngx_connection_t结构体中 send 事件的回调函数 */ ... 1, 1 }, ... ---- - 最后,调用 `ngx_add_timer()` 为 `check_ev` 事件注册定时器,Nginx会定时调用 `ngx_http_upstream_check_begin_handler()` 函数,一旦发现超时,则会调用 `ngx_http_upstream_check_timeout_handler`。 ===== 3.1.3.3 主动检测逻辑 继续跟踪 `ngx_http_upstream_check_begin_handler()` 函数即可,注意其中一个点: 1. 如何保证多个worker进程之间的互斥操作的? 2. 如何挑选后端服务器(peer)的? 在此文中,peer的选择主要由nginx内置负载均衡模块完成,例如, `ngx_http_upstream_round_robin` 模块或ip hash模块, `ngx_http_upstream_check` 模块只完成地后端服务器的主动探测和探测结果汇总。 通过给nginx源码打补丁, `ngx_http_upstream_check` 模块将 `ngx_http_upstream_check_add_peer()` 的调用插入到nginx原生round_robin模块中 `ngx_http_upstream_round_robin.c` 的初始化函数 `ngx_http_upstream_init_round_robin()` 中。 3. 如何解析后端服务器的响应数据的? 跟踪 `ngx_http_upstream_check_recv_handler()` 函数。 ===== 3.1.3.4 检测超时逻辑 跟踪 `ngx_http_upstream_check_timeout_handler()` 函数即可 ===== 3.1.3.5 配置初始化逻辑 一个数据结构:注册模块配置指令的数据接口 [source,c] ---- static ngx_command_t ngx_http_upstream_check_commands[] = { { ngx_string("check"), NGX_HTTP_UPS_CONF|NGX_CONF_1MORE, ngx_http_upstream_check, /* 从配置文件中解析check指令的参数,即主动探测的配置*/ 0, 0, NULL }, ... ---- 接下来,跟踪 `ngx_http_upstream_check()` 函数即可。 == 四. 参考资料 1. 负载均衡解释:link:https://www.nginx.org.cn/article/detail/440[] 2. nginx中文配置手册:link:https://wizardforcel.gitbooks.io/nginx-doc/content/index.html[] 3. 高可用配置示例:link:https://blog.csdn.net/IT_10/article/details/89365436[] 4. nginx开发参考1:link:https://tengine.taobao.org/book/[] 5. nginx开发参考2:link:https://www.nginx.org.cn/article/detail/443[] 6. nginx开发参考3:link:https://www.kancloud.cn/kancloud/master-nginx-develop/51798[]