2

如何在容器时代高效使用 Nginx 三方模块

 3 years ago
source link: https://soulteary.com/2021/03/22/how-to-use-nginx-third-party-modules-efficiently-in-the-container-era.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
2021年03月22日阅读Markdown格式5694字12分钟阅读

在中文网络之中,存在着大量的陈旧内容,包括并不限于各种只能在特定环境中一次性安装使用的陈旧软件,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_VERSIONNGINX_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 .

然后,只要执行这个脚本,就能够根据每个模块的不同配置信息,构建出可复现的稳定结果啦。

基于容器快速使用 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
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 模块的通用构建方式、容器环境下相对通用的 Nginx 模块构建文件、如何快速使用预编译的三方模块制作定制的 Nginx 服务、以及针对这种积木模式产生的服务进行了简单的性能测试和对比。

填完了这个坑,下一篇我们可以继续聊聊,NJS 如何在定制过的 Nginx 镜像、环境中和三方模块一起工作,以及 NJS 到底能够干哪些更复杂的活?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK