如何自动化的生成 Nginx 配置文件

本文讨论如何自动化的生成 Nginx 配置文件,Nginx 配置文件指的是 server 的配置,这里有一个很重要的概念,就是:服务树,我们希望基于服务树来维护 server 配置需要的信息。

 

服务树的每一个节点就是一个服务,那么,我们对每个节点定义必要的信息,包括:

node_id – 节点 id,代表服务;

domain_uris – 代表服务绑定的域名和路径,支持多个,因为一个服务可能同时需要绑定外网域名和内网域名,而且有绑定多个路径的需求,它的格式如下:

[{‘domain_id’: domain_id, ‘uri’: uri }, …] ;

upstream node name – 这个服务的 upstream 名称;

servers – node_id 下绑定的机器列表(每台机器可以绑定不同的权重,以实现流量控制);

port – 这个服务监听的端口;

ip_hash – 是否对 upstream 启用 ip hash。

 

上面的 domain_id 表示域名信息,事实上域名信息包括:

domain – 域名;

ports – 端口,可以包含多个端口,并且支持是否开启 ssl;

access_log_path – 正常日志路径,默认为 ${domain}_access.log;

error_log_path – 错误日志路径,默认为 ${domain}_error.log;

ssl_cert_path – 证书文件路径,如果 ports 中没有开启 ssl,此处可以留空;

ssl_cert_key_path – 私钥文件路径,如果 ports 中没有开启 ssl,此处可以留空;

要注意的一点,如果 ssl 文件配置在 server 中,如果客户端不支持 SNI,ssl 连接可能会失败,这里我们不讨论多个域名证书不放在 server 中的配置问题。

生成配置文件基于模板,很简单,不过要注意的是,location $uri 有顺序要求,只要能保证按照 uri 的长度从高到低就没问题了。

 

自动生成配置文件的好处:

1. 在页面上点点就能增加、修改或删除服务的 Nginx 配置,比较方面,而且彻底解决了使用 git 管理配置带来的各种 rewrite 、if 等冗长的配置问题;

2. 可以基于服务做可用率的报警,根据服务可以查到 domain 和 path,然后过滤 Nginx 日志来计算可用率。而且当服务出问题的时候可以查看到服务的每台机器的可用率状况,然后做出自动剔除机器的动作。

使用 Nginx 做翻墙代理

配置

/etc/nginx/nginx.conf 配置,可以采用自签名证书,客户端连的时候不 verify 证书即可。

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

events {
    worker_connections 10240;
}

http {
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for" '
    '"upstream=$upstream_addr" "scheme=$scheme"     "X-Remote-App=$http_x_remote_app" "reqtime=$request_time"     "upstream_resptime=$upstream_response_time"     "$upstream_cache_status" "host=$host"';
    ssl_certificate /etc/nginx/ssl/52.77.252.184.crt;
    ssl_certificate_key /etc/nginx/ssl/52.77.252.184.key;
    ssl_session_cache shared:SSL:100m;
    ssl_session_timeout 10m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SH    A256:ECDHE-RSA-AES128-SHA:AES128-GCM-SHA256:AES128-SHA256:AE    S128-SHA:DES-CBC3-SHA';
    ssl_prefer_server_ciphers on;

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Load modular configuration files from the     /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;
}

/etc/nginx/conf.d/server.conf 配置。

#
# A virtual host using mix of IP-, name-, and port-based configuration
#

server {
    listen 443 ssl;
    server_name _;

    access_log /var/log/nginx/443_access.log main;
    resolver 8.8.8.8;

    set $upstream_endpoint $host;

    location / {
        proxy_pass https://$upstream_endpoint;
    }
}

server {
    listen 80;
    server_name _;

    access_log /var/log/nginx/80_access.log main;
    resolver 8.8.8.8;

    set $upstream_endpoint $host;

    location / {
        proxy_pass http://$upstream_endpoint;
    }
}

测试

要修改 header,而且 curl 要加 -k 不 verify 证书。

$ curl -k -I -H "Host:play.google.com" "https://52.77.252.184/store/apps/details?hl=en&id=tr.com.fugo.kelimeavi2.en"
HTTP/1.1 200 OK

$ curl -k -I -H "Host:www.baidu.com" http://52.77.252.184
HTTP/1.1 200 OK

这样翻墙的好处是可以有多台 Nginx,程序能够控制访问哪台 Nginx。

一次 CDN 源站流量暴增的问题分析

上周经历了一次 CDN 源站流量暴增的问题,原因是有 CDN 有一批文件过期,大量回源。

CDN 回源过程:

  1. 第一次回源,如果源站返回 Last-Modified 头,CDN 会记录下来;

  2. CDN 的文件过期后,如果源站之前已经返回了 Last-Modified 头,就以 If-Modified-Since 头(内容是 源站返回的Last-Modified 值) 来访问源站,如果文件未变更,则返回 304;如果之前源站没返回  Last-Modified 头,则会下载整个文件,源站返回 200。

流量暴增的问题就出现在源站不返回 Last-Modified 头上,解决方法是源站返回 Last-Modified 头。

但是需要注意几点,先看看源站前面的 Nginx 配置文件:

location ~ /(?<name>.+?)/.* {
    expires 100d;

    rewrite ^/.+?/(.*)$ /$1 break;

    proxy_cache m_wdjcdn_com;
    proxy_cache_key &quot;$request_uri&quot;;
    proxy_cache_valid 200 30d;

    proxy_set_header Host $name.cdn.xxxx.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_pass http://new-source-nodes;
}
  1. 如果源站文件放在本地硬盘(使用 root 指令) 而不是通过 proxy_pass 打到后端,就不会出现这个问题,因为 Nginx 对静态文件完全支持 Last-Modified 和 If-Modified-Since 语义;

  2. Nginx 有一个指令 if_modified_since,默认是 exact,表示 If-Modified-Since 头的值必须和上次获取的 Last-Modified 值一致,否则还会 200;可以把 if_modified_since 改成 before,表示 If-Modified-Since 值可以比 Last-Modified 值大,也就是更新。

Tengine request_time 和 upstream_response_time 相等的问题排查

最近一段时间我们把 Tengine 从 1.4.6(nginx/1.2.9) 升级到了 Tengine 2.1.0(nginx/1.6.2),发现 request_time 出奇的小,大部分情况下都和 upstream_response_time 相等,特别对于 https 请求,一般要 0.x 秒,但是日志记录的只有 0.0x 秒。

 

搜遍了 Tengine,也没搜到有关系的内容,由于 Tengine 是 fork 的 Nginx,所以在 Nginx 测试一下,发现 nginx/1.6.2 同样有问题,继续测试 nginx/1.2.9 发现没问题。

所以是在 nginx/1.2.9 和 nginx/1.6.2 中间的某个版本引文的问题,实在没辙,只有用二分法找到了出问题的版本:

nginx-1.4.7    NOTOK
nginx-1.4.3    NOTOK
nginx-1.4.0    NOTOK
ngnx-1.3.16   NOTOK
nginx-1.3.15  NOTOK
ngnx-1.3.14   OK
nginx-1.3.9    OK
nginx-1.2.9    OK

nginx-1.3.15 的 changlog:

Changes with nginx 1.3.15 26 Mar 2013 *) Change: opening and closing a connection without sending any data in
it is no longer logged to access_log with error code 400. *) Feature: the ngx_http_spdy_module.
Thanks to Automattic for sponsoring this work.

*) Feature: the “limit_req_status” and “limit_conn_status” directives.
Thanks to Nick Marden.

*) Feature: the “image_filter_interlace” directive.
Thanks to Ian Babrou.

*) Feature: $connections_waiting variable in the
ngx_http_stub_status_module.

*) Feature: the mail proxy module now supports IPv6 backends.

*) Bugfix: request body might be transmitted incorrectly when retrying a
request to the next upstream server; the bug had appeared in 1.3.9.
Thanks to Piotr Sikora.

*) Bugfix: in the “client_body_in_file_only” directive; the bug had
appeared in 1.3.9.

*) Bugfix: responses might hang if subrequests were used and a DNS error
happened during subrequest processing.
Thanks to Lanshun Zhou.

*) Bugfix: in backend usage accounting.

changlog 都没提,擦,搜了一下,搜到了这个:

http://trac.nginx.org/nginx/ticket/356

解释如下:

As of 1.3.15, $request_time calculation was slightly changed due to some optimizations introduced, and for a first request in a connection it no longer includes time between opening a connection and reading a first byte from a client. It still works correctly though.

看样子是从收到请求数据的第一个字节开始算的,而不是按照之前建立连接时的时间(之前是否是建立时间我没真正测过,属猜测),在官方文档中看到 request_time 的说明:

request processing time in seconds with a milliseconds resolution; time elapsed between the first bytes were read from the client and the log write after the last bytes were sent to the client.

官方文档写的是第一个字节。

 

抓个包看看:request_time.pcap

日志中记录:”request_time=0.013″ “upstream_response_time=0.013”

E06FF4CC-110F-4739-B015-DBD55B0D9F75

10.0.23.74 是 Nginx,Nginx 接受到真实的请求数据是在 16,17 便是 Nginx 发送完数据的时间点,两者的时间差正好是 0.013,确认。

 

参考:

http://nginx.org/en/CHANGES-1.4

http://trac.nginx.org/nginx/ticket/356

解决 HTTP 请求被 LVS reset 的问题

最近两天遇到了 HTTP 请求被 LVS reset 的问题,LVS 默认有三个超时参数,如下:

# ipvsadm -L –timeout
Timeout (tcp tcpfin udp): 90 3 300

第一个就是 TCP 的空闲连接时间,这里超过 90 s 连接没数据会被 LVS reset 掉,在 HTTP Client 端可以看到 Connection reset by peer。我的某个服务 HTTP 请求时间很长,至少有 5 min,所以每次都会被 reset。

 

一开始我想到的解决办法是在 HTTP Client (HTTP 请求使用 requests)指定 so_keepidle参数小于 90 s。

因为 requests 使用 urllib3,而 urllib3 使用标准库 http.client(Python 3),Python 2.x 版本使用 httplib(它们会调用 socket.create_connection),所以我们可以封装一下来实现修改 so_keepidle:

import httplib
orig_connect = httplib.HTTPConnection.connect
def monkey_connect(self):
    orig_connect(self)
    self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    self.sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 10)
    # sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 10)
    # sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 100)
httplib.HTTPConnection.connect = monkey_connect
import requests

但是抓包发现 HTTP Client 端根本不发送 TCP keepalive 的包来保活,很是奇怪(可能是这段代码有问题,引用的还是老的模块,没继续debug,mark)。

 

后来发现可以在 Nginx 来实现 so_keepidle(LVS 后面是 Nginx),而且 Nginx 支持 so_keepidle 的配置,在 linsten 后面指定,格式如下:

222111h4s4ipokimf3zppr

比如:listen 80 so_keepalive=60::;

实测有效,终于解决了因为 HTTP 请求时间太长被 LVS reset 的问题,而且对域名生效,不影响其他域名(如果修改 LVS TCP idle timeout,对所有域名都有影响)。

 

参考:

http://stackoverflow.com/questions/15148225/is-it-possible-to-override-the-default-socket-options-in-requests

https://linux.cn/article-3766-1.html

http://nginx.org/en/docs/http/ngx_http_core_module.html