在中文网络之中,存在着大量的陈旧内容,包括并不限于各种只能在特定环境中一次性安装使用的陈旧软件,Nginx 编译安装的内容尤甚。
在继续 Nginx NJS 实战之前,我们可以先了解下如何相对快速、安全的使用 Nginx 三方模块。
写在前面
本文中的三方模块相关代码,已经提交至开源仓库:https://github.com/soulteary/prebuilt-nginx-modules,欢迎自取或者贡献你觉得还不错的模块。
在《Nginx 模块系统:前篇》中,我提到过 Nginx 动态模块的来龙去脉,不了解的同学可以自行补习下前置内容。
在聊如何高效使用前,首先需要知道如何高效的“制作”这些模块。
Nginx 的模块编译通用逻辑
想要了解如何高效的构建 Nginx 模块,首先需要了解下什么是 Nginx 模块编译的通用逻辑。
编译一个 Nginx 模块一般只需要三个步骤:
- 第一步:准备源代码
- 获取某个指定版本的 Nginx 代码,以及对应的模块代码,进行简单的处理,调整代码目录结构和名称,留作后用。
- 第二步:准备系统环境
- 安装目标运行环境(如 Linux)的各种开发依赖,确保代码编译依赖满足,可以进行后续的编译流程。
- 第三步:调整编译参数和编译模式
- 调整 Nginx 编译参数,以及设置模块编译模式,选择进行静态模块或者动态模块编译操作,并等待编译结果顺利完成。
基于容器环境进行实战
在 使用 Docker 和 Nginx 打造高性能二维码服务(二) 一文中,我提到过:
“之前构建服务的时候,采用的是使用通用基础镜像编译 Nginx 和它的“小伙伴”(模块),在三年后的今天,我们不妨直接使用 Nginx 基础镜像,所谓“原汤化原食”,最大限度复用官方提供的环境、配置参数、入口脚本…毕竟,偷懒是工程师的美德。”
所以,这次我们也使用官方的容器环境来进行编译和构建操作。
构建基础编译镜像
参考官方 Dockerfile ,不难写出类似下面的基础编译环境的 Dockerfile:
ARG NGINX_VERSION=1.19.7
FROM nginx:${NGINX_VERSION}-alpine
# Mirror
# RUN cat /etc/apk/repositories | sed -e "s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/" | tee /etc/apk/repositories
ARG NGINX_SHASUM=0dde53b5a948efc9dc852814186052e559d190ea
RUN apk add --no-cache --virtual .build-deps gcc libc-dev make openssl-dev pcre-dev zlib-dev linux-headers libxslt-dev gd-dev geoip-dev perl-dev libedit-dev mercurial bash alpine-sdk findutils && \
mkdir -p /usr/src && cd /usr/src && \
curl -L "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" -o nginx.tar.gz && \
echo "$NGINX_SHASUM nginx.tar.gz" | shasum -c && \
tar -zxC /usr/src -f nginx.tar.gz && \
cd /usr/src && \
mv /usr/src/nginx-$NGINX_VERSION /usr/src/nginx && \
CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \
CONFARGS=${CONFARGS/-Os -fomit-frame-pointer -g/-Os} && \
export CONFARGS=$CONFARGS;
上面的镜像支持在构建时传递参数,并将官方镜像中的构建命令最大程度复用到接下来的模块构建中。
如果你想编译目前最新的 1.19.8,或者未来的版本,只需要在构建的时候传递 NGINX_VERSION
和 NGINX_SHASUM
给 docker 就好了,为了能够将执行过程变为代码的一部分进行长时间的维护管理,这里使用 .env
文件对我们的构建参数进行存储。
NGINX_VERSION=1.19.8
NGINX_SHASUM=c60654a70bea0a9bbc009b83cd95e1e76f0dd7ec
同样,为了将构建命令也一同保持下来,我们可以写一个简单的构建脚本:
#!/bin/bash
RELEASE_DIR="./baseImage";
REPO_NAME="soulteary/prebuilt-nginx-modules"
set -a
. "$RELEASE_DIR/.env"
set +a
TAG=base-$NGINX_VERSION-alpine;
DIST=$REPO_NAME:$TAG
echo "Build: $DIST";
echo $NGINX_VERSION:$NGINX_SHASUM
echo "docker build --build-arg NGINX_VERSION=$NGINX_VERSION --build-arg NGINX_SHASUM=$NGINX_SHASUM --tag $DIST -f "$RELEASE_DIR/Dockerfile" ."
docker build --build-arg NGINX_VERSION="$NGINX_VERSION" --build-arg NGINX_SHASUM="$NGINX_SHASUM" --tag $DIST -f "$RELEASE_DIR/Dockerfile" .
假设我们将上面的 Dockerfile
和 .env
都放置于一个名为 ** baseImage**的目录中,并将上面的脚本保存为 build.sh
。
接着执行这个脚本,不需多久,便能够得到一个基于 Nginx 某个确切版本的构建环境镜像。
使用容器编译 Nginx 模块
有了构建环境,编译模块的步骤将能大幅简化,以常用的 Nginx 三方模块“headers-more-nginx-module”为例子,基于前文中的构建环境,我们编写一个模块构建脚本也很容易:
ARG NGINX_VERSION=1.19.7
FROM soulteary/prebuilt-nginx-modules:base-${NGINX_VERSION}-alpine AS Builder
ARG MODULE_CHECKSUM=7d6af910dae98f0dbc67bf77e82eab8b7da5d0b1
ARG MODULE_VERSION=0.33
ARG MODULE_NAME=headers-more-nginx-module
RUN cd /usr/src && \
curl -L "https://github.com/openresty/headers-more-nginx-module/archive/v${MODULE_VERSION}.tar.gz" -o "v${MODULE_VERSION}.tar.gz" && \
echo "${MODULE_CHECKSUM} v${MODULE_VERSION}.tar.gz" | shasum -c && \
tar -zxC /usr/src -f v${MODULE_VERSION}.tar.gz && \
cd /usr/src && \
mv ${MODULE_NAME}-${MODULE_VERSION}/ ${MODULE_NAME} && \
cd /usr/src/nginx && \
CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \
CONFARGS=${CONFARGS/-Os -fomit-frame-pointer -g/-Os} && \
echo $CONFARGS && \
./configure --with-compat $CONFARGS --add-dynamic-module=../${MODULE_NAME}/ && \
make modules
FROM scratch
COPY --from=Builder /usr/src/nginx/objs/ngx_http_headers_more_filter_module.so /
这个 Dockerfile 主要分为三部分,我们来详细讲解下各部分的职责。
第一部分,负责抽象我们使用的 Nginx 环境和代码版本,以及我们使用的模块名称、版本、代码包校验和,为我们今后能够尽可能少的写代码来维护镜像做准备。
ARG NGINX_VERSION=1.19.7
FROM soulteary/prebuilt-nginx-modules:base-${NGINX_VERSION}-alpine AS Builder
ARG MODULE_CHECKSUM=7d6af910dae98f0dbc67bf77e82eab8b7da5d0b1
ARG MODULE_VERSION=0.33
ARG MODULE_NAME=headers-more-nginx-module
第二部分,准备源代码、调整代码目录结构、复用官方构建命令、对模块进行静态编译。
RUN cd /usr/src && \
curl -L "https://github.com/openresty/headers-more-nginx-module/archive/v${MODULE_VERSION}.tar.gz" -o "v${MODULE_VERSION}.tar.gz" && \
echo "${MODULE_CHECKSUM} v${MODULE_VERSION}.tar.gz" | shasum -c && \
tar -zxC /usr/src -f v${MODULE_VERSION}.tar.gz && \
cd /usr/src && \
mv ${MODULE_NAME}-${MODULE_VERSION}/ ${MODULE_NAME} && \
cd /usr/src/nginx && \
CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \
CONFARGS=${CONFARGS/-Os -fomit-frame-pointer -g/-Os} && \
echo $CONFARGS && \
./configure --with-compat $CONFARGS --add-dynamic-module=../${MODULE_NAME}/ && \
make modules
第三部分,使用一个特殊的空镜像,将我们的构建产物保留,以供未来生产环境的镜像快速复用。
FROM scratch
COPY --from=Builder /usr/src/nginx/objs/ngx_http_headers_more_filter_module.so /
和处理基础编译镜像一样,我们同样可以使用独立的 .env
文件来持久化构建参数,比如下面这样:
NGINX_VERSION=1.19.8
MODULE_CHECKSUM=7d6af910dae98f0dbc67bf77e82eab8b7da5d0b1
MODULE_VERSION=0.33
MODULE_NAME=headers-more-nginx-module
完成批量模块的构建
为了能够对多个模块进行构建管理,我们来了解下如何编写“支持多个模块构建”的构建脚本。假设项目目录结构类似下面的形式:
.
├── README.md
├── baseImage
│ └── Dockerfile
├── make-base.sh
├── make-image.sh
├── modules
│ ├── echo
│ │ └── Dockerfile
│ ├── headers-more
│ │ └── Dockerfile
│ ├── http-redis
│ │ └── Dockerfile
│ ├── memc
│ │ └── Dockerfile
│ ├── misc
│ │ └── Dockerfile
│ ├── redis2
│ │ └── Dockerfile
│ ├── srcache
│ │ └── Dockerfile
│ └── waf
│ └── Dockerfile
└── push-image.sh
我们对之前的“基础编译环境”的构建脚本进行适当调整和修改,可以得到支持批量构建模块的脚本:
#!/bin/bash
RELEASE_DIR="./modules";
REPO_NAME="soulteary/prebuilt-nginx-modules"
for moduleName in $RELEASE_DIR/*; do
set -a
. "$moduleName/.env"
set +a
tag=$(echo $moduleName | cut -b 11-);
BUILD_NAME="$REPO_NAME:$tag-$NGINX_VERSION-alpine"
echo "Build: $BUILD_NAME";
BUILD_ARGS=$(tr '\n' ';' < "$moduleName/.env" | sed 's/;$/\n/' | sed 's/^/ --build-arg /' | sed 's/;/ --build-arg /g')
docker build $BUILD_ARGS --tag $BUILD_NAME -f $moduleName/Dockerfile .
done
然后,只要执行这个脚本,就能够根据每个模块的不同配置信息,构建出可复现的稳定结果啦。
基于容器快速使用 Nginx 三方模块
目前为止,我们已经了解了如何在容器内快速编译构建 Nginx 三方模块,接下来我们可以步入正题,如何快速使用这些模块。
假设我们现在需要一个能够直接返回简单 JSON 的接口,接口包含当前服务器端端时间,并且这个接口有很高的调用压力,诸如活动、秒杀等场景的高频调用,可以使用 Nginx 借助 Nginx Echo 和 Set Misc 模块来进行实现。
相关代码已上传至 https://github.com/soulteary/docker-nginx-time-api,可以自行获取。
编写使用预编译模块的容器文件
在《从封装 Nginx NJS 工具镜像聊起》一文中,我曾提到过如何使用二阶段构建保存动态模块和它的依赖。
这里,我们使用预构建模块也非常简单,只需要将编译好的模块文件复制到目标镜像即可:
FROM nginx:1.19.8-alpine
COPY --from=soulteary/prebuilt-nginx-modules:misc-1.19.8-alpine /ndk_http_module.so /etc/nginx/modules/
COPY --from=soulteary/prebuilt-nginx-modules:misc-1.19.8-alpine /ngx_http_set_misc_module.so /etc/nginx/modules/
COPY --from=soulteary/prebuilt-nginx-modules:echo-1.19.8-alpine /ngx_http_echo_module.so /etc/nginx/modules/
因为 Set Misc 模块依赖 NDK 模块,所以这里要复制三个文件。将上面的内容保存为 Dockerfile
,然后执行 docker build -t nginx-time-api:1.19.8-alpine .
构建第一个基于预编译模块的 Nginx 镜像。
接着以官方镜像中的 Nginx 为模版,编写一个简单的 Nginx 配置文件:
load_module modules/ndk_http_module.so;
load_module modules/ngx_http_set_misc_module.so;
load_module modules/ngx_http_echo_module.so;
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
charset utf-8;
gzip on;
location / {
default_type application/json;
set_formatted_gmt_time $timestr "%a %b %e %H:%M:%S %Y GMT";
echo $timestr;
}
}
}
将上面的内容保存为 nginx.conf
,为了验证方便,我们再编写一个简单容器编排脚本:
version: '3'
services:
ngx-time-api:
image: nginx-time-api:1.19.8-alpine
ports:
- 8080:80
volumes:
# 在 Linux 环境中需要使用
# - /etc/localtime:/etc/localtime:ro
# - /etc/timezone:/etc/timezone:ro
- ./nginx.conf:/etc/nginx/nginx.conf
将上面的内容保存为 docker-compose.yml
,然后使用 docker-compose down && docker-compose up
启动容器,访问 127.0.0.1:8080,不出意外,将看到一条类似:Mon Mar 22 05:32:50 2021 GMT
的内容。
到这里为止,我们就已经完成了“打印服务端时间”的接口应用啦。
进行不严谨的性能测试
这里就不使用 ab
来进行“鲁大师”测试了,我们直接使用 wrk
做一个简单测试,可以看到在容器环境下,经过 NAT 转发,依旧能够达到每秒 2万 QPS。
wrk -t2 -c 100 -d 10s http://localhost:8080
Running 10s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 4.26ms 1.77ms 27.69ms 76.80%
Req/Sec 11.67k 1.21k 13.81k 62.00%
232312 requests in 10.01s, 44.74MB read
Requests/sec: 23218.65
Transfer/sec: 4.47MB
为了让大家有更直观的了解,我们继续使用运行相对较快的动态语言运行时 Node 进行相同类型的测试。
因为 Nginx 运行在 Alpine 中,为了相对公平,同样使用基于 alpine 3.13 的镜像: node:15.12.0-alpine3.13
。(此处我有试验过 fibjs,结果激动人心,但是为了普适性,这里先不展开,后续有机会我一定会写一些使用 fibjs 实践的内容)
// node 15.12
var http = require('http');
http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end(new Date()+'');
}).listen(8888);
简单编写一个编排脚本,和 Nginx 一样,使用挂载的方式将文件映射到容器内:
version: '3'
services:
node:
image: node:15.12.0-alpine3.13
ports:
- 8082:8888
volumes:
- ./web.js:/web.js
command: node /web.js
然后同样使用 wrk
执行测试。
wrk -t2 -c 100 -d 10s http://localhost:8082
Running 10s test @ http://localhost:8082
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 13.12ms 11.55ms 200.41ms 94.13%
Req/Sec 4.25k 1.27k 10.92k 80.10%
85035 requests in 10.10s, 18.65MB read
Requests/sec: 8417.80
Transfer/sec: 1.85MB
可以看到,在容器内、有 NAT 转发的情况下进行测试,Node 单机响应在 13 ms,相比相同环境的 Nginx 慢了至少 8ms,响应相比 Nginx 的2万,少了1万5千QPS。
直接在主机内执行 Node
如果我们直接使用主机环境中的 Node ,可以看到性能会跃升到2万6千左右。
wrk -t2 -c 100 -d 10s http://localhost:8888
Running 10s test @ http://localhost:8888
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.87ms 1.25ms 40.60ms 95.79%
Req/Sec 13.13k 1.67k 14.43k 93.07%
263920 requests in 10.10s, 55.88MB read
Requests/sec: 26128.23
Transfer/sec: 5.53MB
如果使用宿主机运行的“性能释放”是接近线性的,那么请自行脑补 Nginx 在相同的宿主环境运行的结果。
因为 Nginx 的执行文件并非类似 Node 只有一个可执行文件,出于不想污染本地环境,我就不在本地编译使用或者安装 Nginx 了,感兴趣的同学可以自行测试。
其他
接下来简单一些相关联的内容,后面有时间我会单独写成文章,展开聊聊。
目前开箱即用的模块
在prebuilt-nginx-modules这个开源仓库中,我们目前有以下常见模块可以直接使用,后续我会根据需求逐步将更多的常用、好用的模块加进来:
- soulteary/prebuilt-nginx-modules:headers-more-1.19.8-alpine
- soulteary/prebuilt-nginx-modules:http-redis-1.19.8-alpine
- soulteary/prebuilt-nginx-modules:echo-1.19.8-alpine
- soulteary/prebuilt-nginx-modules:set-misc-1.19.8-alpine
- soulteary/prebuilt-nginx-modules:redis2-1.19.8-alpine
- soulteary/prebuilt-nginx-modules:memc-1.19.8-alpine
- soulteary/prebuilt-nginx-modules:srcache-1.19.8-alpine
- soulteary/prebuilt-nginx-modules:base-1.19.8-alpine
- soulteary/prebuilt-nginx-modules:waf-1.19.8-alpine
简单针对老模块进行动态模块编译
Nginx 发展十余年,许多公司目前还是在宿主机上使用,所以不会提供动态模块,这时我们就需要进行动态模块改造,一般情况下我们只需要调整 config
文件,添加动态模块编译依赖,以及调整编译使用的目标脚本即可。
假设原始的 config
文件内容如下:
ngx_addon_name=ngx_http_hello_world_module
HTTP_MODULES="$HTTP_MODULES ngx_http_hello_world_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_hello_world_module.c"
如果模块代码不需要针对 Nginx 核心模块进行 Patch,一般情况,只需要简单调整为类似下面的格式,即可完成“动态模块”编译改造,是不是很简单?
ngx_addon_name=ngx_http_hello_world_module
if test -n "$ngx_module_link"; then
ngx_module_type=HTTP
ngx_module_name=ngx_http_hello_world_module
ngx_module_srcs="$ngx_addon_dir/ngx_http_hello_world_module.c"
. auto/module
else
HTTP_MODULES="$HTTP_MODULES ngx_http_hello_world_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_hello_world_module.c"
fi
最后
好了,还是来总结一下。
本篇文章中,我们了解了 Nginx 模块的通用构建方式、容器环境下相对通用的 Nginx 模块构建文件、如何快速使用预编译的三方模块制作定制的 Nginx 服务、以及针对这种积木模式产生的服务进行了简单的性能测试和对比。
填完了这个坑,下一篇我们可以继续聊聊,NJS 如何在定制过的 Nginx 镜像、环境中和三方模块一起工作,以及 NJS 到底能够干哪些更复杂的活?
–EOF