Stay Hungry.Stay Foolish.
nginx代理修炼传(连载)

传道授业解惑也

nginx 各种参数解释

单单网络连接的方面的参数就有n多个,这是因为作为代理服务器的时候,需要建立两个全双工通道,connect , read, write 都可能面临超时,两个connect过程,(client->nginx)3+2(client->nginx)+3(nginx->upstream)+2(upstream->nginx)一共8个TCP操作,任何一个操作失败,整个连接就无法正常工作。

  • fastcgi_* 打头的参数是针对fastcgi服务器,比如熟悉就是php-fpm, 配置的参数对它生效;
  • client_* 打头的参数针对的是nginx作为反向代理服务器的时候,通常指我们的浏览器;
  • proxy_* 打头的参数针对的是nginx作为反向代理服务器的时候,代理的后端webserver,一般叫upstream;

我的nginx作为反向代理服务,所以fastcgi就跳过

proxy_connect_timeout   60s; 连接upstream的超时时间
proxy_send_timeout      1800; 和upstream通信write数据的时候超时时间
proxy_read_timeout      1800;  和upstream通信read数据的时候超时时间
proxy_buffers           64 8k;
proxy_buffer_size       16k;      HTTP响应头缓存
proxy_busy_buffers_size    128k;
proxy_temp_file_write_size 128k;
proxy_redirect off;

获取真实客户端IP

nginx代理前面还有个LB,找到网上通用的配置,大家基本是这么设置的

set_real_ip_from 10.10.4.11;          排除上游服务IP
real_ip_header X-Forwarded-For;  remote_addr从过来的请求的x-forwarded-for这个头里面查找设置
real_ip_recursive on;                      递归查找

proxy_set_header X-Real-IP $remote_addr;   
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  

这里一顿猛如虎的set ip操作可以用下面的伪代码表示

if $remote_addr == "10.10.4.11" :
    if $real_ip_recursive == on:
          for $ip in X-Forwared-For.split(","):
              if $ip != "10.10.4.11":
                  $remote_addr = $ip;
    else:
          if X-Forwared-For.split(",")[0] != "":
                  $remote_addr = X-Forwared-For.split(",")[0]
else:
     //pass
$HTTP_X-Real-IP = $remote_addr
$proxy_add_x_forwarded_for = $X-Forwared-For + "," + $remote_addr
  1. 前提需要你的前端LB,重设x-forwared-for = remote_add, 不信任客户端的x-forwared-for,记住一定要让前端LB重设,因为client的x-forwarded-for是可以伪造的;
  2. 如果client的IP在set_real_ip_from列表里面,那么从请求过来的x-forwarded-for头里面去获取真实IP,然后设置remote_addr这个变量为检索到的IP,否则不修改remote_addr;
  3. 从左向右检索x-forwarded-for里面的IP直到检索到IP不在set_real_ip_from列表里面, 然后设置remote_addr为检索到这个IP;

如上这样可以确保nginx代理获取到的IP是真实的客户端IP,这里set_real_ip不建议设置为某个IP段,而是设置精确的IP,不然IP段里面的机器都可以伪造x-forwarded-for IP过来,但是这样设置还存在一个问题,proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 这句其实是多余的,猛如虎的操作之后其实我的$remote_addr变成了clientip(这是因为这个模块的配置会发生在nginx的第一个阶段post-read,之后所有的配置都会收到它的影响), 所以$proxy_add_x_forwarded_for的值是clientiip+”,”+clientip, 我的下游会得到的x-forwarded-for 其实是clientip, clientip,这里传递两个没啥必要。

这里比较尴尬的就是x-forwarded-for这条链路是不正确的,下面这样设置可以保证正确的链路

proxy_set_header X-Forwarded-For "$X-Forwared-For + $realip_remote_addr"

$realip_remote_addr 这个变量根据官方的解释keeps the original client address (1.9.7),需要高版本


servername hash优化

server_names_hash_max_size 10240;
server_names_hash_bucket_size 128;

nginx的hash表在解决hash碰撞的时候使用拉链法解决,整个hash表的初始化是在加载配置的时候确定的,server_names_hash_max_size 用于控制hash表大小,server_names_hash_bucket_size用于控制一个hash bucket就是单链表的最大长度,所以如果像我上面这么设置,最理想的情况,可以负载10240*128个域名。

如果内存足够,为了优化,server_names_hash_max_size大小我建议差不多保持域名个数的1.5倍大小,然后按照2的幂次方去设置,server_names_hash_bucket_size这个参数不要太大,会降低hash查找速率,影响性能,比如我的代理服务器3000个域名,我设置了10240,这样可以保证查找速率在O(1),为啥这么设置具体原理可以看我之前写的hashtable原理分析。


以下优化在nginx作为反向代理服务器的时候有性能提升吗?

运维设置的nginx参数,问题来了,设置了这个对我的反向代理nginx会有性能提升吗?有点好奇,研究了一下

tcp_nodelay     on;
tcp_nopush      on;
sendfile        on;

tcp_nodelay on; 设置之后表示关闭古老的解决网络拥堵的算法,新算法就是tcp_nopush on;具体的可以搜文章百度一大堆解释 在nginx作为web server的时候,传统的模式是这样工作的,例如一个请求资源/music/1.mp3,会经历以下syscall

硬盘 >> kernel buffer >> user buffer>> kernel socket buffer >>协议栈
  1. 系统调用 read()产生一个上下文切换:从 user mode 切换到 kernel mode,然后 DMA 执行拷贝,把文件数据从硬盘读到一个 kernel buffer 里。
  2. 数据从 kernel buffer拷贝到 user buffer,然后系统调用 read() 返回,这时又产生一个上下文切换:从kernel mode 切换到 user mode。
  3. 系统调用write()产生一个上下文切换:从 user mode切换到 kernel mode,然后把步骤2读到 user buffer的数据拷贝到 kernel buffer(数据第2次拷贝到 kernel buffer),不过这次是个不同的 kernel buffer,这个 buffer和 socket相关联。
  4. 系统调用 write()返回,产生一个上下文切换:从 kernel mode 切换到 user mode(第4次切换了),然后 DMA 从 kernel buffer拷贝数据到协议栈(第4次拷贝了)。

然后sendfile这个api是这么工作的

硬盘 >> kernel buffer (快速拷贝到kernelsocket buffer) >>协议栈

从上面来看,sendfile系统调用通过直接把数据copy到内核socket提升性能,但是作为反向代理服务器的时候,数据处理不是走的这个流程 ,那么难道真的是没啥用吗?其实不是,当你的反向代理开启缓存的时候,数据是nginx从本地缓存文件读取的,这个时候的nginx就相当于web server的角色。


鱼和熊掌不可兼得的参数

sniffer分析出来的流量和nginx日志分析出来的流量相差很多,翻阅日志明细,发现很多499状态码的请求

查阅文档是这么解释的,默认proxy_ignore_client_abort 是关闭的,此时在请求过程中如果客户端端主动关闭请求或者客户端网络断掉,那么 Nginx 会记录 499,同时 request_time 是 「后端已经处理」的时间,而 upstream_response_time 为 “-“ (已验证), 如果proxy_ignore_client_abort on, 那么客户端主动断掉连接之后,Nginx 会等待后端处理完(或者超时),然后 记录 「后端的返回信息」 到日志。所以,如果后端 返回 200, 就记录 200 ;如果后端放回 5XX ,那么就记录 5XX 。。 在我的场景就会有一个尴尬的问题,如果我设置proxy_ignore_client_abort on两边的流量是可以对齐了,但是会带来下面的问题

  • 本来可以断开的连接会继续产生流量消耗,客户端实际没有收到任何数据,客户端会有一种被冤枉的感觉,比如重试了10次下载文件,每次下载到1M的时候失败,我最多也就10M数据阿,结果你告诉产生100M流量。。
  • 由于设置的30分钟超时,如果这时候遇上网络拥堵,小水管状态,这条TCP连接会一直ESTABLISHED状态,端口无法释放,平白无辜占用了资源。
  • 网络拥堵的情况传输大文件,连接数会不断增多,恶性循环,不人工干预,永远解不了套

如果我设置这个参数,流量又对不上,这不是nginx的锅,上层应用不会设计连接中断的时候计算发送和返回的数据大小。


nginx数据压缩算法

在带宽资源比较稀缺的情况,建议开启数据压缩,由于gzip算法比较古老,覆盖率比较广,br算法比较新,通用的代理服务器还是采用gzip比较合适,条件允许的话可以开启br算法。

gzip on;                               //开启gzip压缩
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml;
gzip_vary on;                        //判断客户端是否支持gzip

这里还有个ngx_http_gzip_static_module模块,如果你的客户端支持gzip,那么开启此模块可以让nginx直接读取gz文件返回给客户端,减少nginx压缩时间,也就是说你本地有个style.css 直接用gzip命令提前压缩为style.css.gz, nginx会再客户端支持gzip的时候读取style.css.gz, 不支持的时候读取style.css


nginx logformat

$bytes_sent和$body_bytes_sent的区别,前者算上了HTTP协议的协议头,但是不包括TCP协议头,后者就是client和server通信传输的数据,如果想依赖nginx的日志计算流量,只具备参考性,在网络环境质量比较差的时候,会有大量的TCP重传,和在4层工作的sniffer程序对比相差3-4倍都是正常的。


高并发的时候端口是否会被消耗完

这个问题其实我之前也一直有疑惑,后端承载了3000+ 服务的我瑟瑟发抖,经常上服务器ss -s看看连接数,担心我的nginx承受不住。

首先客户端发出请求和我们的listen端口建立连接,这个不需要我们考虑,端口可以复用,应为linux一切皆文件,一个socket就是一个fd,所以和系统允许一个进程打开的最大文件数量有关,nginx里面的参数worker_connections,8个worker就是8个进程8*worker_connections/2个并发,这里/2是因为需要维持一个双向通道;nginx连接后端upstream的时候,可以和每个upstream建立65535个连接,而不是我们的nginx代理只能和所有的upstream维持65535个连接,实际我们不可能一个应用会有那么高并发,有的话也只需要增加upstream的负载数量就能解决。

linux系统先调整进程最大open file数量,ulimit -c unlimited等同于ulimit -n 65535, 然后同时nginx 配置如下,这个worker_connections不会超过ulimit的最大值65535

worker_connections 65535;

所以结论是很难被消耗完,这个问题还是比较复杂,可以参考http://blog.51cto.com/liuqunying/1420556


$host, $http_host, $server_name区分

尤其是$host和$http_host很难区分,之前写lua的时候一直不知道用哪一个

  1. $server_name就是nginx里面定义的一个一个server_name,比较好理解,这里有个小坑,如果你在一行里面定义了多个server_name, 那么server_name永远等于第一个server_name的值;

  2. $http_host是HTTP协议里面parse出来的host值,nginx把每一个HTTP header定义为http_header的形式,比如cookie就是$http_cookie, 所以这里也很好理解;

  3. host变量比较复杂一些,它是nginx core里面的变量,它的值按照如下优先级获得:

    • 请求行中的host. //GET www.test.info/index.php HTTP/1.1
    • 请求头中的Host头部. // -H “Host: www.test.info”
    • 与一条请求匹配的server name. //nginx server name

作为反向代理的时候一般监听80和443端口,然后通过域名访问,这个时候$host一般等于$http_host, 如果nginx不是监听在80或者433上,我们需要带域名和端口访问,这个时候$host是域名, 而$http_host是域名+port


2017-11-10 haproxy代理https流量,使用TCP协议的时候透传客户端IP

作为一个实至名归的透明代理,要做到让后端upstream感觉自己就是和用户在通信,所以我们必须把客户端原始IP设置为remote_ip

Haproxy配置可以参考 https://www.cnblogs.com/qiyebao/p/6001427.html, 我就不多说了,nginx 在配置server的时候需要配置如下:

listen       443 ssl proxy_protocol;
set_real_ip_from hxproxy_ip;  #排除上游haproxy ip
real_ip_recursive on;
real_ip_header proxy_protocol;

开启HTTP/2导致部分lua API无法使用

HTTP/2 时代,我们果断升级与时俱进,但是nginx上面有很多lua代码,迁移的时候发现部分API无法使用 ngx.req.raw_header 目前还不支持HTTP/2, 迁移之前需要做足够的测试


2018-04-16 nginx反向代理大数据应用(spark, rm, hadoop等)

我这里使用Resourcemanager系统举例,在新版本里面,ResourceManager HA通过主动/备用架构实现 - 在任何时间点,其中一个RM是活动的,并且一个或多个RM处于待机模式,等待接管,如果活动发生任何事情。转换到活动的触发器来自管理员(通过CLI)或通过集成的故障转移控制器(当启用自动故障转移时), 由于内部已经实现了这种高可用,导致如果使用nginx去反代,不知道upstream该怎么写了。

curl -i 172.18.14.8:8088
HTTP/1.1 307 TEMPORARY_REDIRECT
Cache-Control: no-cache
Expires: Thu, 26 Apr 2018 06:32:06 GMT
Date: Thu, 26 Apr 2018 06:32:06 GMT
Pragma: no-cache
Expires: Thu, 26 Apr 2018 06:32:06 GMT
Date: Thu, 26 Apr 2018 06:32:06 GMT
Pragma: no-cache
Content-Type: text/plain; charset=UTF-8

curl -i 172.18.14.9:8088
HTTP/1.1 302 Found
Cache-Control: no-cache
Expires: Thu, 26 Apr 2018 06:31:59 GMT
Date: Thu, 26 Apr 2018 06:31:59 GMT
Pragma: no-cache
Expires: Thu, 26 Apr 2018 06:31:59 GMT
Date: Thu, 26 Apr 2018 06:31:59 GMT
Pragma: no-cache
Content-Type: text/plain; charset=UTF-8

通过curl两个机器的地址我们会发现返回的http状态码不同,再借助nginx的健康检查,我们可以把请求全部转发到活动的RM节点,相当于废弃了自带的高可用,用nginx实现高可用。

check interval=3000 rise=2 fall=5 timeout=1000 type=http;
check_http_send "HEAD /cluster HTTP/1.0\r\n\r\n";
check_http_expect_alive http_4xx;

因为自带的nginx健康检查模块无法区分302或者307,只能识别3xx, 所以选择蹩脚的访问/cluster这个路径,活动的机器会返回405状态,不活动的机器返回的是307,配置之后,10秒之后只有活动的机器会显示up,请求将都被转发到此机器,实现了反向代理

上面的方式其实比较的挫,如果想要优雅的解决,可以使用nginx lua的健康检测模块


2018-11-13 带宽打满导致连接僵死

先说下环境,数据传输需要经过代理服务器,第一阶段从内网到代理服务器,这里走内网通信,传输速度很快,不存在问题,第二阶段代理服务器到公网,这里网络质量很不好,带宽小,经常丢包。

一个应用并发上传文件,由于网络质量不好,导致丢包,客户端频繁重试提交,nginx待传输数据过大,导致n个ESTABLISHED连接僵死在nginx上,临时方案,使用tcpkill干掉这些连接。


2018-11-15 代理服务访问502

发现错误日志[error] 13907#0: *1217694104 upstream sent too big header while reading response header from upstream

后端服务器的响应头,也就是HTTP协议的头,会放到proxy_buffer_size当中,这个大小默认等于proxy_buffers当中的设置单个缓冲区的大小。 部分应用不符合规范,比如超长cookie就会导致出现这个问题。 配置如下解决, 具体还是可以根据实际情况慢慢加大调整,实在不行就针对局部server调整

proxy_buffer_size       16k;

从tengine/2.2.2升级到nginx/1.14.1

tengine使用了2年多,发现tengine社区的维护更新太慢了,很多新特性无法使用,然而我们的场景却需要这些新特性,于是决定把线上的nginx版本升级

  1. 新版本支持upstream_bytes_received,我们的场景需要日志里面记录这个值
  2. 新版本支持配置worker_shutdown_timeout

升级到nginx1.14.1版本遇到的问题

  1. Lua代码报错error: no resolver defined to resolve, 原因是内部使用的http库需要手动指定resolve;
  2. nginx lua模块lua-nginx-module需要升级到lua-nginx-module-0.10.14rc3,否则编译不过;
  3. sysguard模块tengine特有,编译的时候需要移除编译参数;
  4. 健康检测模块nginx_upstream_check_module需要打1.14.1的的patch;

nginx使用gzip全压缩传输送

为了大幅度降低网络IO, 需要在前后段nginx的传输过程开启全gzip,不管你client支持不支持gzip,后端nginx都会返回gzip压缩过的数据

  1. 前端nginx篡改client发送的http请求,加入proxy_set_header Accept-Encoding ‘gzip’;
  2. 前端nginx编译加入ngx_http_gunzip_module模块,配置参数gunzip on,负责解压;
  3. 后端nginx处理upstream的数据,开启gzip压缩
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 6;
gzip_vary on;

2018-11-22 修改proxy_set_header导致运维事故

proxy_set_header官方文档给的解释是:These directives are inherited from the previous level if and only if there are no proxy_set_header directives defined on the current level

翻译一下: proxy_set_header这个参数会继承全局配置,当且仅当局部配置没有使用proxy_set_header, 之前没有仔细看文档,一直以为这个参数的继承参数和正常理解一样,局部配置继承全局配置,并且会覆盖全局配置, 后端服务有依赖全局的proxy_set_header,这里确实后端服务也有问题不应该和代理服务偶尔,在上线测试新功能时,在局部的server域里面加了proxy_set_header,导致全局的proxy_set_header无法生效,线上发生事故

总结一下:还是有必要进行充分的测试,即便多花一点时间,不能想当然的应该不会出问题。

自由转载-非商用-非衍生-保持署名(创意共享3.0许可证
评论

暂无评论~~