将原始域名透传

proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

今天 搭建 Webscoket 服务, 测试 内网IP + 端口, 客户端连接没有任何问题.

运维同学配置了公网域名后, 测试 客户端连接 公网域名

你 4:27:10
等待服务器Websocket握手包...
你 4:27:12
收到服务器Websocket握手包.
服务器 4:27:12
Websocket连接已建立,正在等待数据...
服务器 4:27:12
和服务器断开连接!
就像上面一样 一直 握手 -> 连接建立 -> 断开.

问题是 我服务端 进到 onOpen 事件 想要主动回包给 客户端都做不到.
$server->exist($fd) 返回的是false, 发现客户端行为不是长连接, 比较像短连接请求,更过分的是断连不挥手.

排除了一些可能后想到 是不是 运维的 Nginx 配置出错了.

server {
    listen       80;
    server_name  xxxx.xxx.com;
                                                                                                                   
     location  / {

            proxy_pass http://localhost:8888;
            proxy_set_header   X-Forwarded-For  $http_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
               
        }
  
} 

发现缺少 proxy_http_version 1.1; 配置.
因为 nginx 默认配置 proxy_http_version1.0 , HTTP协议中对长连接的支持是从1.1版本之后才有的, 所以Webscocket 用的时候至少要配置 1.1.

通过allow, deny模块

allow 45.43.23.21;
deny all;

各模块下使用方式

http{
   ...
   allow 45.43.23.21;
   deny all;
   ...
}

server{
    ...
    allow 45.43.23.21;
    deny all;
    ...
}


location / {
   allow 45.43.23.21;
   deny all;
}

通过geo模块

http模块

http {
    ........
    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    geo $remote_addr $geo {
           default 1; #1表示禁止访问
           127.0.0.1 0; #0表示可以访问
    }

    include /usr/local/nginx/conf.d/*.conf;
}

server模块

server {
    listen 80;
    server_name jenkins.aa.bb;
    location / {
        # 如果不是白名单则 显示403 禁止访问
        if ( $geo  = 1 ) {
            return 403;
        }
        proxy_set_header        Host $host:$server_port;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header        X-Forwarded-Proto $scheme;
        proxy_pass          http://127.0.0.1:59932;
        proxy_read_timeout  90;
        proxy_http_version 1.1;
        proxy_request_buffering off;
    }
}

geo语法

Syntax:     geo [$address] $variable { ... }
Default:    —
Context:    http
address 默认是 [Math Processing Error] http_x_forwarded_for才能获取到客户端ip

通过geo对指定ip地址流量限制

http{
     #定义白名单ip列表变量
     geo $whiteiplist {
         default 1 ;
         127.0.0.1/32 0;
         10.0.0.0/8 0;
         64.223.160.0/19 0;
     }
     
     map $whiteiplist $limit{
         1 $binary_remote_addr ;
         0 "";
     }
     
     #配置请求限制内容
     limit_conn_zone $limit zone=conn:10m;
     limit_req_zone $limit zone=allips:10m rate=20r/s;
     
     server{
         listen       8080;
         server_name  test.qiangsh.com;
                  
         location /app {
           proxy_pass http://192.168.1.111:8888/app;
           limit_conn conn 50;
           limit_rate 500k;
           limit_req zone=allips burst=5 nodelay;
         }
     }
}
# 测试方法:
# ab -c 100 -n 300 http://test.qiangsh.com:8080/app/docs/nginx_guide.pdf
  1. geo指令定义一个白名单$whiteiplist, 默认值为1, 所有都受限制。
    如果客户端IP与白名单列出的IP相匹配,则$whiteiplist值为0也就是不受限制。
  2. map指令是将$whiteiplist值为1的,也就是受限制的IP,映射为客户端IP。将$whiteiplist值为0的,也就是白名单IP,映射为空的字符串。
  3. limit_conn_zonelimit_req_zone指令对于键为空值的将会被忽略,从而实现对于列出来的IP不做限制。
  4. $remote_addr$binary_remote_addr 区别, $remote_addr 是7-15字节,$binary_remote_addr是4字节ipv4, 16字节ipv6
  5. 以上命令的限制是指白名单内的不限制, 不在白名单内的, 每个ip允许50个连接, 每个连接限速500k, 10m的容器,
    按照32bytes/session, 可以处理320000个`` 每秒20个请求, 超过25(20+5,

其中前20个是有时间间隔匀速控制的, 后5个是缓存, 如果100ms内有10个请求, 则会通过7个)个的立即丢弃, 返回503

环境 阿里云 Centos 6.5 nginx/1.10.3

查看当前Nginx 版本及编译信息 :

[root@xxx nginx]#/usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.10.3
built by gcc 6.1.0 (GCC)
built with OpenSSL 1.0.1e-fips 11 Feb 2013
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx --user=www --group=www --with-http_stub_status_module --with-http_ssl_module --with-pcre=/usr/src/make/software/pcre-8.38 --add-module=/usr/src/make/software/redis2-nginx-module-master --add-module=../ngx_devel_kit-0.3.0 --add-module=../lua-nginx-module-0.10.8

挑选最新版本nginx (个人使用不考虑稳定性, 使用jishu)

[root@xxx src]#wget http://nginx.org/download/nginx-1.13.9.tar.gz
--2018-03-02 10:57:14--  http://nginx.org/download/nginx-1.13.9.tar.gz
Resolving nginx.org... 206.251.255.63, 95.211.80.227, 2606:7100:1:69::3f, ...
Connecting to nginx.org|206.251.255.63|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 994802 (971K) [application/octet-stream]
Saving to: “nginx-1.13.9.tar.gz”

100%[======================================================================================================================================================================================================================================>] 994,802     84.8K/s   in 16s     

2018-03-02 10:57:32 (59.1 KB/s) - “nginx-1.13.9.tar.gz” saved [994802/994802]

个人习惯安装软件在 /usr/local/src

[root@xxx src]#pwd
/usr/local/src

解压

[root@xxx src]#tar zxvf nginx-1.13.9.tar.gz
[root@xxx src]#cd nginx-1.13.9
[root@xxx nginx-1.13.9]#ll
auto  CHANGES  CHANGES.ru  conf  configure  contrib  html  LICENSE  man  README  src

开始编译老套路 中间有插曲 编译参数错误了 所以生成了Makefile 文件 使用 make clean 清理编译文件

[root@xxx nginx-1.13.9]#make clean
rm -rf Makefile objs

开始编译参数

./configure  --prefix=/usr/local/nginx --user=www --group=www --with-http_stub_status_module --with-http_ssl_module --with-pcre=/usr/src/make/software/pcre-8.38 --add-module=/usr/src/make/software/redis2-nginx-module-master --add-module=../ngx_devel_kit-0.3.0 --add-module=../lua-nginx-module-0.10.8

沮丧又出问题了 缺少第三方模块 ngx_devel_kitlua 从网上查来安装教程

下载ngx_devel_kit(NDK)模块 :https://github.com/simpl/ngx_devel_kit/tags,不需要安装

wget https://github.com/simplresty/ngx_devel_kit/archive/v0.3.1rc1.tar.gz
tar -xzvf v0.3.1rc1.tar.gz

下载最新的lua-nginx-module 模块 :https://github.com/openresty/lua-nginx-module/tags,不需要安装

wget https://github.com/openresty/lua-nginx-module/archive/v0.10.12rc2.tar.gz
tar -xzvf v0.10.12rc2.tar.gz

重新编译

./configure  --prefix=/usr/local/nginx --user=www --group=www --with-http_stub_status_module --with-http_ssl_module  --with-pcre=/usr/src/make/software/pcre-8.38 --add-module=/usr/src/make/software/redis2-nginx-module-master --add-module=/usr/local/src/ngx_devel_kit-0.3.1rc1 --add-module=/usr/local/src/lua-nginx-module-0.10.12rc2

Configuration summary
  + using PCRE library: /usr/src/make/software/pcre-8.38
  + using system OpenSSL library
  + using system zlib library

  nginx path prefix: "/usr/local/nginx"
  nginx binary file: "/usr/local/nginx/sbin/nginx"
  nginx modules path: "/usr/local/nginx/modules"
  nginx configuration prefix: "/usr/local/nginx/conf"
  nginx configuration file: "/usr/local/nginx/conf/nginx.conf"
  nginx pid file: "/usr/local/nginx/logs/nginx.pid"
  nginx error log file: "/usr/local/nginx/logs/error.log"
  nginx http access log file: "/usr/local/nginx/logs/access.log"
  nginx http client request body temporary files: "client_body_temp"
  nginx http proxy temporary files: "proxy_temp"
  nginx http fastcgi temporary files: "fastcgi_temp"
  nginx http uwsgi temporary files: "uwsgi_temp"
  nginx http scgi temporary files: "scgi_temp"

终于ok了 可以开始 make 了 平滑升级 记住 只能make 千万别 make install

[root@xxx nginx-1.13.9]#make
make -f objs/Makefile
make[1]: Entering directory `/usr/local/src/nginx-1.13.9'
cd /usr/src/make/software/pcre-8.38 \
    && if [ -f Makefile ]; then make distclean; fi \
    && CC="cc" CFLAGS="-O2 -fomit-frame-pointer -pipe " \
    ./configure --disable-shared
/bin/sh: line 0: cd: /usr/src/make/software/pcre-8.38: No such file or directory
make[1]: *** [/usr/src/make/software/pcre-8.38/Makefile] Error 1
make[1]: Leaving directory `/usr/local/src/nginx-1.13.9'
make: *** [build] Error 2

哦 抛错了 不要慌 看看什么问题

[root@xxx nginx-1.13.9]#cd /usr/src/make/software/pcre-8.38 
-bash: cd: /usr/src/make/software/pcre-8.38: No such file or directory 没有'-'
#把准备好的pcre包解压
[root@xxx software]#tar zxf pcre-8.38.tar.gz

make 正在运行中 我的服务器配置太低 如果你是多核 make -j 加速编译速度

make[1]: Leaving directory `/usr/local/src/nginx-1.13.9'

终于编译完了 . 查看objs 目录下 生成了新的nginx

[root@xxx nginx-1.13.9]#ls objs/
addon  autoconf.err  Makefile  nginx  nginx.8  ngx_auto_config.h  ngx_auto_headers.h  ngx_modules.c  ngx_modules.o  src

保存并替换nginx

[root@xxx nginx-1.13.9]#mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx-20180302
[root@xxx nginx-1.13.9]#cp objs/nginx /usr/local/nginx/sbin/nginx

执行升级 失效 通知 kill -user2 通知主进程失败 正常到这步就结束了 但通知主进程失败 未生成

/usr/local/nginx/logs/nginx.pid.oldbin 文件老nginx 进程在垂死挣扎

[root@xxx nginx-1.13.9]#make upgrade
/usr/local/nginx/sbin/nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
kill -USR2 `cat /usr/local/nginx/logs/nginx.pid`
sleep 1
test -f /usr/local/nginx/logs/nginx.pid.oldbin
make: *** [upgrade] Error 1
You have mail in /var/spool/mail/root

手动试下 还是没用 中午了 吃饭去 回来再看看 进程死没死

[root@xxx nginx-1.13.9]#kill -USR2 `cat /usr/local/nginx/logs/nginx.pid`
[root@xxxx nginx-1.13.9]#test -f /usr/local/nginx/logs/nginx.pid.oldbin && echo OK!

介绍下Nginx的信号控制

  • TERM,INT 快速关闭
  • QUIT 从容关闭
  • HUP 平滑重启,重新加载配置文件
  • USR1 重新打开日志文件,在切割日志时用途较大
  • USR2 平滑升级克执行程序
  • WINCH 从容关闭工作进程

出现异常 USR2 失效了.

最终也没有成功做到平滑升级.

推荐失败后解决方案

先写个简单的监控脚本 查看服务器是否断连

#!/bin/bash

while [ 0 ]
    do
        curl -I 'http://localhost:80/'
        sleep 1
    done

尝试多次nginx -s reload 进程ID仍未改变.

采用方案

  • kill -WINCH #先关闭工作进程 后观察 脚本输出停止了
  • kill -HUP #平滑重启 观察 脚本重新输出 但并未做到更新
  • kill -QUIT #从容关闭 观察 脚本输出 连接错误

nginx #重启成功

HTTP/1.1 200 OK
Server: nginx/1.13.9
Date: Fri, 02 Mar 2018 08:41:53 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding

总结: 本次平滑升级失败 . 失败原因受个人能力问题未找到.

后记: 模块加载错了 我又重新编译安装错误仍然存在. kill -USR2 无效 未生成 nginx.pid.oldbin

过程

0x01

经搜索得知: 哪些情况下会使 Nginx 返回 HTTP CODE 499?

即:「客户端主动关闭连接」

但某一时间段内全部请求均为返回 499,这显然不是所有客户端主动意识上的「关闭」,可能是因为客户端等待超时,自动关闭连接;加上 499 的时间段内包含部分 502,让我不得不怀疑:

PHP 进程「死」了。

0x02

这里的死,不一定是进程结束,也有可能是僵尸,或是陷入死循环,一直在执行某个脚本……

若是逐个检查代码时间来不及(以先解决问题为重),遂排查: Nginx+FastCGI 到底是谁影响超时时间

以及: PHP-max_execution_time 与 fpm.request_terminate_timeout 介绍

0x03

经过上面的调整,大约一周后再次维护服务器。发现情况有所改善—— 499 错误已经由某一时段大量、集中出现变为偶尔发生,且只出现在某几个特定 URI 请求上。

我决定对这几个 URI 对应的接口控制器代码进行检查。由于系统开发时间紧张,代码质量并不高,怀疑是否是程序内有 BUG。

首先查看代码执行时间,约为 1900 ms 左右,简直太慢!经过仔细检查,发现几个严重问题:

  • 查出某表「全部结果」,再「遍历」结果集,查询每条记录「多个字段」的关联模型
  • 未执行 php artisan optimize
  • 未关闭 debug 模式
  • 未调整 log_level

其中,后几条或许无关紧要,但第一条绝对是致命的。

假设一种常见的模型关联场景:

某作者有多篇文章,每篇文章又有多条评论、赞。

由此,若是采用类似:

posts = posts::where('user_id', 1);
foreach(posts as post){
    likes = post->likes;
    comments = post->comments;
}

Laravel 框架内使用类似如上的方式查询,假设作者的文章数为 n,每篇文章关联的模型有 2 个(likes & comments),则执行此控制器,对于数据库的时间复杂度为:O(n*2+1),需要执行如此大量的 SQL 语句!这在后端设计中应该是需要完全避免的,理想情况的时间复杂度应该是 O(n),n 为常量,不受数据规模的影响。

于是修改代码,过程不再详叙,参见 Laravel 官方文档,或: Laravel 学习笔记之模型关联预加载

经过修改,在 Chrome 开发者工具内查看请求 Timing,缩短为原来时间的一半,800ms 左右。

(但此值仍然不够理想,受到视图渲染、操作系统等原因的影响,后期继续优化,不属于本文讨论范围。)

后记

对于部分接口,请求一次需要执行几百条 SQL;那么,回到最开始的问题:

某次请求后,突然引发大量 499。究其根本原因,是否在于因代码的不严谨,引起的 MySQL 死锁呢?

值得研讨。

原文地址 : https://wi1dcard.dev/posts/nginx-php-http-status-499/

karp

创建我自己的巨人