前端 Docker 独立部署需要知道的 nginx 配置

2,861 阅读5分钟

作者介绍

绪彤,专有钉钉前端团队成员,负责专有钉钉工作台 & 开放平台相关业务模块。

背景

写这篇文章的主要缘由是自己负责的业务日常迭代更新了前端静态资源,线上验证回归功能一切正常,但是第二天部分用户反馈第一次进入到前端应用,页面内容显示异常,第二次或者第三次才能正常打开页面,异常情况如下图客户端所示(安卓和 ios 的表现):

image.png

image.png

面对如此情况,决定前端内容紧急回滚到上个版本,果然一切都正常啦(流量成功访问数保持昨日正常的水准)。

image.png

然后查看出现问题时间点的应用监控日志:

image.png

请求前端静态资源的请求都是 status: 502、504 的报错。联想到这次前端静态资源发布体积增加了约700KB 大小的体积,大概率是触发了所谓的限流(nginx 限流,访问量流量增加,导致部分连接未响应)。

前端部署历史

  • 在 jQuery 的时代中,前端项目上线部署的资源基本都是随着后端资源一起部署,后端帮忙做配置转发规则。
  • 现代化的前端工程项目都是前后端分开部署的,前后端都是通过 docker 部署的。docker 中通过 nginx.conf 配置访问静态资源方式。

问题产生的原因

原本 nginx 的配置是:

# proxy conf
user                        admin admin;

worker_processes            2;

worker_rlimit_nofile        100000;

events                       { xxxxx; }


http {
    include                 mime.types;
    default_type            application/octet-stream;

    root                    xxxx;

    sendfile                on;
    tcp_nopush              on;

    server_tokens           off;

    keepalive_timeout       0;

    client_header_timeout   1m;
    send_timeout            1m;
    client_max_body_size    2048m;
    check_shm_size          20m;


    index                   index.html index.htm;
    log_format              xxxxx;
    access_log              xxxxx;
    log_not_found           off;
    # log_empty_request       off;
    #eagleeye_traceid_var    $eagleeye_traceid;

    gzip                    on;
    gzip_http_version       1.0;
    gzip_comp_level         6;
    gzip_min_length         1024;
    gzip_proxied            any;
    gzip_vary               on;
    gzip_disable            msie6;
    gzip_buffers            96 8k;
    gzip_types              text/xml text/javascript text/plain text/css application/javascript application/x-javascript application/rss+xml application/json;


    proxy_set_header        Host $host;
    proxy_set_header        Web-Server-Type nginx;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    #proxy_set_header        EagleEye-TraceId $eagleeye_traceid;
    proxy_redirect          off;
    proxy_buffers           128 8k;
    proxy_intercept_errors  on;

    server_names_hash_bucket_size 512;
    
    # 其他省略
    xxx:   xxxxx;
    }

这里 nginx 有些参数的含义是需要知道的:

  • worker_processes:工作进程的数量,一般情况设置成CPU核的数量即可。应当根据服务器实际的性能和负载去配置一个合理的数值,如果你不知道如何配置一个合理的数值,那么设置成 auto 是最佳的。
  • http:可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。
  • server:配置虚拟主机的相关参数,一个http中可以有多个server。
  • gzip 压缩:gzip_comp_level 压缩级别,总共分为1-9,数字越大压缩的越好,也越占用CPU时间
  • proxy_cache 缓存设置:项目的页面需要加载很多数据,也不是经常变化的,不涉及个性化定制,为每次请求去动态生成数据,性能比不上根据请求路由和参数缓存一下结果,使用 Nginx 缓存将大幅度提升请求速度。只需要配置 proxy_cache_path 和 proxy_cache 就可以开启内容缓存,前者用来设置缓存的路径和配置,后者用来启用缓存。
  • proxy_cache_valid:配置nginx cache中的缓存文件的缓存时间,如果配置项为:proxy_cache_valid 200 304 2m;说明对于状态为200和304的缓存文件的缓存时间是2分钟,两分钟之后再访问该缓存文件时,文件会过期,从而去源服务器重新取数据
  • open_file_cache模块 文件描述符缓存:只针对于打开的文件句柄以及源信息,缓存了文件句柄就意味着不用每次都close一个文件再open一个文件,减少了系统调用的操作。
  • location: 指定模式来与客户端请求的URI相匹配
    • 匹配 URI 类型,有四种参数可选,当然也可以不带参数。
    • 命名location,用@来标识,类似于定义goto语句块。
location [ = | ~ | ~* | ^~ ] /URI { … }
location @/name/ { … }

备注: nginx 是两层指令来匹配请求 URI 。第一个是 server 指令,它通过域名、ip 和端口来做第一层级匹配,当找到匹配的 server 后就进入此 server 的 location 匹配。

location 的匹配并不完全按照其在配置文件中出现的顺序来匹配,请求URI 会按如下规则进行匹配:

  1. 先精准匹配 = ,精准匹配成功则会立即停止其他类型匹配;
  2. 没有精准匹配成功时,进行前缀匹配。先查找带有 ^~ 的前缀匹配,带有 ^~ 的前缀匹配成功则立即停止其他类型匹配,普通前缀匹配(不带参数 ^~ )成功则会暂存,继续查找正则匹配;
  3. =^~ 均未匹配成功前提下,查找正则匹配 ~~* 。当同时有多个正则匹配时,按其在配置文件中出现的先后顺序优先匹配,命中则立即停止其他类型匹配;
  4. 所有正则匹配均未成功时,返回 2 中暂存的普通前缀匹配(不带参数 ^~ )结果
  • proxy_pass: 指定的是 ngx_http_proxy_module 模块的 proxy_pass 指令,用来做配置转发的,在proxy_pass后面的url加/,表示绝对根路径;如果没有/,表示相对路径

问题分析:

再看看上面线上环境配置的 nginx.conf 文件,简直要哭了。

  • 线上 Pod 是4核的,但是写死只用到2核,并且只有2台Pod
  • 没有缓存
  • gzip 的压缩级别较高,每次都会占用较多的 CPU 时间

这些原因加上早高峰流量大,直接翻车啦。

解决方案

最终版本的 nginx 的部分配置是这个样子的:

server {
  listen       8080;
  server_name  _;

  client_max_body_size  1024M;
  location / {
    root   xxxxx;
    index  index.html index.htm;
  }

  location =/nginx_status {
    allow   127.0.0.0/24;
    deny    all;
    stub_status     on;
    expires         off;
    access_log off;
  }
}

server {
  listen       80;
  server_name  _;

  client_max_body_size  1024M;
  
  proxy_cache_path:xxxx;
  
  location / {
    proxy_cache static_cache;
    proxy_cache_valid 200 302 1h;
    add_header  Nginx-Cache "$upstream_cache_status";
    proxy_pass http://127.0.0.1:8080;
    root   xxxxx;
    index  index.html index.htm;
  }

  location =/nginx_status {
    allow   127.0.0.0/24;
    deny    all;
    stub_status     on;
    expires         off;
    access_log off;
  }
}
  • 针对 proxy 代理的前端请求 200 302 的进行了1小时的缓存
  • 设置了 open_file_cache 缓存进一步提高性能
  • 降低 gzip_level 的等级
  • 添加 Pod 机器
  • 将 worker_processes 配置成最大复用 Pod 规格参数,或者写个脚本在初始化的时候,最大复用 Pod 的性能
let cpu_num=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us)/$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us)
 if [ ${cpu_num} -lt 2 ];then
   cpu_num=2
 fi
 sed -i '/worker_processes/s/worker_processes.*/worker_processes            '"${cpu_num}"';/g' $BASE_CONF_DIR/nginx-proxy.conf

通过以上这些优化手段,压测后的效果将 QPS 的指标提升了近 100倍,一次性解决了问题。

总结:

以上关于 nginx 的配置还是比较复杂的,涉及到的内容其实比较多,比较简单的前端资源配置可以参考下面这个:

location ~ .*.(html|htm)$ {
    add_header Cache-Control "public, no-cache";  #html文件协商缓存,也就是每次都询问服务器,浏览器本地是是否是最新的,是最新的就直接用,非最新的服务器就会返回最新
}

location ~ .*.(js|css|png|jpg|jpeg|svg|eot|woff2|ttf)$ {
    add_header Cache-Control "public, max-age=xxxx"; #非 html 内容缓存正常迭代一个周期的时间:一个月或更多时间
}