6

使用 Nginx 容器为 Traefik 配置高性能通用错误页面

 3 years ago
source link: https://soulteary.com/2020/12/06/use-nginx-container-to-configure-high-performance-general-error-pages-for-traefik.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.

使用 Traefik 比较久的读者应该会发现,在服务重启的时候,原来的网站会展示 404 not found 的空白页面,虽然多数情况下服务恢复很快,但是这个恢复时间取决于部署启动的应用和监控检查配置策略,如果没有配置流量切换规则,那么有的时候,会看到很久的空白页面,这样的体验显然不好。

为了提升体验,我们可以使用 Traefik 提供的错误页面中间件来解决这个问题,优化访问体验。本篇思路同样可以处理通用 Nginx 错误页面的创建。

如何使用 Traefik 错误页中间件

虽然 官方文档 中有明确记录“错误页面”中间件的使用方法:

labels:
  - "traefik.http.middlewares.test-errorpage.errors.status=500-599"
  - "traefik.http.middlewares.test-errorpage.errors.service=serviceError"
  - "traefik.http.middlewares.test-errorpage.errors.query=/{status}.html"

但是这只描述了如何使用中间件,我们还需要实际的“应用服务”来支持在错误发生的时候,能够有对应的错误页面展示给用户,所以处理这段逻辑对应的配置如下:

labels:
  - "traefik.enable=true"
  - "traefik.docker.network=traefik"
 
  # 定义中间件
  - "traefik.http.middlewares.error-pages-middleware.errors.status=400-599"
  - "traefik.http.middlewares.error-pages-middleware.errors.service=error-pages-service"
  - "traefik.http.middlewares.error-pages-middleware.errors.query=/{status}.html"
 
  # 使用中间件
  - "traefik.http.routers.error-pages-router.middlewares=error-pages-middleware@docker"
  - "traefik.http.routers.errorpage.entrypoints=https"
  - "traefik.http.routers.errorpage.tls=true"
  - "traefik.http.routers.errorpage.rule=HostRegexp(`{host:.+}`)"
  - "traefik.http.routers.errorpage.priority=1"
  - "traefik.http.services.error-pages-service.loadbalancer.server.port=80"

在进行配置的时候,还需要注意一个细节:

labels:
  - "traefik.http.routers.errorpage.priority=1"

我们务必降低这个服务的优先级,避免影响业务正常运行。这样才能保证在其他业务中断的时候,展示这个页面,而非遇到一些极端情况下的时候,我们看到的不是预期中的内容。

另外,如果不希望准备多个错误页面的话,可以考虑将 {status}.html 改为指定的固定页面 index.html

labels:
  - "traefik.http.middlewares.error-pages-middleware.errors.query=/index.html"

寻找HTTP错误码页面相关的开源项目

在配置书写完毕之后,我们需要准备对应的错误页面,我们都知道常用的 HTTP 错误码有至少20个,所以如果依赖人工来处理,非常不利于维护。

考虑到现在 traefik 用户量不少了,应该有人有类似需求,经过搜索果然找到了国外小哥编写的项目: https://github.com/tarampampam/error-pages

简单使用这个开源项目,感觉还好,但是如果你想定制页面的话,需要准备的内容稍微有一些多:

  • 依赖一个页面生成工具,构建 Node 构建镜像。
  • 依赖自定义的 Nginx docker-entrypoint.sh ,并需要构建 Nginx 运行镜像,以及需要修改默认的 Nginx.conf

追求简洁高效是工程师的基础素养,所以我们能否有更简单的方案呢?

使用官方 Nginx 镜像进行定制

我们知道 Nginx 在 1.18 之后提供了一个特殊功能,允许用户自定义及额外的扩展 docker-entrypoint.d 脚本,以及支持使用基于 envsubst 的自定义 Nginx 配置文件而不需要修改官方镜像中的 nginx.confdocker-entrypoint.sh 文件。

稍微扩展一些思路,不难想到可以使用 envsubst 以及 扩展的 docker-entrypoint.d 来进行自定义页面的预处理。

出于分发性能考虑,我们使用 alpine 版本的 Nginx Docker 容器镜像。

编写模版页面

出于演示,这里简化我们的模版结构,仅演示如何使用 envsubst 来完成需求:

<html lang="en-US">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>${DEFAULT_CODE} ${DEFAULT_TITLE}</title>
</head>
<body>
  <h1>${DEFAULT_TITLE}</h1>
  <p>${DEFAULT_DESC}</p>
</body>
</html>

在页面中定义需要使用的数据变量后,便可以着手准备页面内的数据了。

准备错误码列表数据

准备数据的时候,考虑计划使用 shell 来进行处理,shell 默认对 JSON 处理支持能力不佳,所以这里需要将错误码进行整理,最好整理为一行几列的模式,方便程序读取和解析。

因为描述文本在后续调整更新过程中,潜在会引入逗号,所以这里使用分号作为分隔符,避免潜在问题:

400;Bad Request;The server did not understand the request
401;Unauthorized;The requested page needs a username and a password
403;Forbidden;Access is forbidden to the requested page
404;Not Found;The server can not find the requested page
405;Method Not Allowed;The method specified in the request is not allowed
407;Proxy Authentication Required;You must authenticate with a proxy server before this request can be served
408;Request Timeout;The request took longer than the server was prepared to wait
409;Conflict;The request could not be completed because of a conflict
410;Gone;The requested page is no longer available
411;Length Required;The "Content-Length" is not defined. The server will not accept the request without it
412;Precondition Failed;The pre condition given in the request evaluated to false by the server
413;Payload Too Large;The server will not accept the request, because the request entity is too large
416;Requested Range Not Satisfiable;The requested byte range is not available and is out of bounds
418;I'm a teapot;Attempt to brew coffee with a teapot is not supported
429;Too Many Requests;Too many requests in a given amount of time
500;Internal Server Error;The server met an unexpected condition
502;Bad Gateway;The server received an invalid response from the upstream server
503;Service Unavailable;The server is temporarily overloading or down
504;Gateway Timeout;The gateway has timed out
505;HTTP Version Not Supported;The server does not support the "http protocol" version

将上面的内容保存为 pages.csv 后,继续编写数据解析脚本。

编写解析脚本

因为我们预期使用 alpine 版本的镜像,镜像内默认只有 sh ,所以这里编写功能的时候,不能使用 array 拆分的方式,需要进行变通:

cat "pages.csv" | grep ";" | while read line; do
    CODE=$(echo "$line" | cut -d";" -f1)
    TITLE=$(echo "$line" | cut -d";" -f2)
    DESC=$(echo "$line" | cut -d";" -f3)
 
    echo $CODE;
    echo $TITLE;
    echo $DESC;
done

执行脚本进行验证,可以看到解析结果是符合预期的:

400
Bad Request
The server did not understand the request
401
Unauthorized
The requested page needs a username and a password
403
Forbidden
Access is forbidden to the requested page
...

核心功能编写完毕,接下来是站在“巨人的肩膀”上,参考官方镜像的脚本,实现“自动读取数据生成各种错误码页面”。

编写模版生成脚本

官方容器中用于生成 nginx 配置的 “ docker-entrypoint.d/20-envsubst-on-templates.sh ” 脚本是这样编写的:

#!/bin/sh
 
set -e
 
ME=$(basename $0)
 
auto_envsubst() {
  local template_dir="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}"
  local suffix="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}"
  local output_dir="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx/conf.d}"
 
  local template defined_envs relative_path output_path subdir
  defined_envs=$(printf '${%s} ' $(env | cut -d= -f1))
  [ -d "$template_dir" ] || return 0
  if [ ! -w "$output_dir" ]; then
    echo >&3 "$ME: ERROR: $template_dir exists, but $output_dir is not writable"
    return 0
  fi
  find "$template_dir" -follow -type f -name "*$suffix" -print | while read -r template; do
    relative_path="${template#$template_dir/}"
    output_path="$output_dir/${relative_path%$suffix}"
    subdir=$(dirname "$relative_path")
    # create a subdirectory where the template file exists
    mkdir -p "$output_dir/$subdir"
    echo >&3 "$ME: Running envsubst on $template to $output_path"
    envsubst "$defined_envs" < "$template" > "$output_path"
  done
}
 
auto_envsubst
 
exit 0

可以看到思路还是比较清晰的,我们将前文中的解析脚本和这段脚本适当合并,来完成我们的需求。

#!/bin/sh
 
set -e
 
ME=$(basename $0)
 
auto_envsubst() {
 
  local template_dir="${ERRORPAGE_ENVSUBST_TEMPLATE_DIR:-/pages}"
  local suffix="${ERRORPAGE_ENVSUBST_TEMPLATE_SUFFIX:-.html}"
  local output_dir="${ERRORPAGE_ENVSUBST_OUTPUT_DIR:-/usr/share/nginx/html}"
 
  local template defined_envs relative_path output_path subdir
  defined_envs=$(printf '${%s} ' $(env | cut -d= -f1))
  [ -d "$template_dir" ] || return 0
  if [ ! -w "$output_dir" ]; then
    echo >&3 "$ME: ERROR: $template_dir exists, but $output_dir is not writable"
    return 0
  fi
 
  find "$template_dir" -follow -type f -name "*$suffix" -print | while read -r template; do
    relative_path="${template#$template_dir/}"
    output_path="$output_dir/${relative_path%$suffix}$suffix"
    subdir=$(dirname "$relative_path")
    # create a subdirectory where the template file exists
    mkdir -p "$output_dir/$subdir"
    echo >&3 "$ME: Running envsubst on $template to $output_path"
    envsubst "$defined_envs" < "$template" > "$output_path"
 
    sed -i "s/^[[:space:]\t\n]*//g" "$output_path"
 
    cat "${template_dir}/pages.csv" | grep ";" | while read line; do
        CODE=$(echo "$line" | cut -d";" -f1)
        TITLE=$(echo "$line" | cut -d";" -f2)
        DESC=$(echo "$line" | cut -d";" -f3)
 
        export DEFAULT_CODE=$CODE
        export DEFAULT_TITLE=$TITLE
        export DEFAULT_DESC=$DESC
        export output_path="$output_dir/$CODE$suffix"
 
        envsubst "$defined_envs" < "$template" > "$output_path"
    done
 
  done
}
 
auto_envsubst
 
exit 0

将内容保存为 30-envsubst-on-pages.sh ,稍后使用。

编写 Nginx 配置

因为官方镜像支持扩展配置,所以我们无需修改主 Nginx.conf ,只需要根据需求书写新的配置即可:

server {
    listen        ${NGINX_PORT};
    server_name   ${NGINX_HOST};
 
    charset       utf-8;
    gzip on;
 
    access_log    off;
    log_not_found off;
    server_tokens off;
 
    location / {
        root   /usr/share/nginx/html;
        index  index.html;
    }
 
    error_page 400 /400.html;
    error_page 401 /401.html;
    error_page 403 /403.html;
    error_page 404 /404.html;
    error_page 405 /405.html;
    error_page 407 /407.html;
    error_page 408 /408.html;
    error_page 409 /409.html;
    error_page 410 /410.html;
    error_page 411 /411.html;
    error_page 412 /412.html;
    error_page 413 /413.html;
    error_page 416 /416.html;
    error_page 418 /418.html;
    error_page 429 /429.html;
    error_page 500 /500.html;
    error_page 502 /502.html;
    error_page 503 /503.html;
    error_page 504 /504.html;
    error_page 505 /505.html;
 
    location = /favicon.ico {
        add_header 'Content-Type' 'image/x-icon';
        return 200 "";
    }
 
    location = /robots.txt {
        return 200 "User-agent: *\nDisallow: /";
    }
}

将上面的内容保存为 default.conf.template ,接下来完成容器配置,就可以使用这个服务啦。

编写服务容器配置

我们的容器配置文件其实很简单:

version: '3'
 
services:
 
  errorpage-nginx:
    image: nginx:1.19.4-alpine
    volumes:
       - ./templates:/etc/nginx/templates:ro
       - ./docker-entrypoint.d/30-envsubst-on-pages.sh:/docker-entrypoint.d/30-envsubst-on-pages.sh:ro
       - ./pages:/pages:ro
    environment:
      - NGINX_HOST=localhost
      - NGINX_PORT=80
      - DEFAULT_CODE=404
      - DEFAULT_TITLE=The page you're looking for is now beyond our reach. Let's get you..
      - DEFAULT_DESC=Page not found
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
 
      - "traefik.http.routers.errorpage.entrypoints=https"
      - "traefik.http.routers.errorpage.tls=true"
      - "traefik.http.routers.errorpage.rule=HostRegexp(`{host:.+}`)"
      - "traefik.http.routers.errorpage.priority=1"
      - "traefik.http.services.error-pages-service.loadbalancer.server.port=80"
 
      - "traefik.http.routers.error-pages-router.middlewares=error-pages-middleware@docker"
      - "traefik.http.middlewares.error-pages-middleware.errors.status=400-599"
      - "traefik.http.middlewares.error-pages-middleware.errors.service=error-pages-service"
      - "traefik.http.middlewares.error-pages-middleware.errors.query=/{status}.html"
 
networks:
  traefik:
    external: true

你或许会疑问,为什么还有三个默认环境变量 DEFAULT_CODEDEFAULT_TITLEDEFAULT_DESC ,这些变量是用于处理服务站点首页 index.html 文件,如果你愿意的话,可以自由发挥整点不一样的内容。

最后

RNNRnaf.png!mobile

想要查看在线例子,可以访问: https://error.soulteary.com/ ,例子模版编写参考了 https://www.mantralabsglobal.com/404 的设计创意,感谢 Mantra Labs 的分享。

不得不说,新版本的 Nginx 容器镜像相当强大,从历史文章中也应该看的出我对它的喜欢:小巧、简洁、高性能、接口丰富。如果你还在使用老版本的 Nginx ,不妨考虑升级到最新版本。

–EOF


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK