玩转Nginx

Posted by Hsz on March 20, 2019

玩转Nginx

Nginx是一个用于处理静态文件,负载均衡和反向代理的服务器,几乎是运维必备的技能之一,本文只介绍如何实现这些功能,并不会讲解原理.要看原理的可以看由淘宝核心系统服务器平台组成员整理的攻略书或者这篇文档说明

Nginx的的设计目的是作为一个http静态服务器.但路越走越歪现在的功能早已不再局限于http静态服务器,在微服务架构盛行的当下Nginx也常作为微服务的中间件充当前端代理的角色.当然由于其设计老旧,功能上并不能完全满足微服务架构的需求,现在逐渐有被envoy取代的趋势,但瑕不掩瑜,它依然是目前最有通用性也最值得广泛使用的技术之一.

它现在可以做:

  • http静态服务器(http cache)
  • ssl传输加密
  • http代理(作用在网络结构第7层的代理)
  • tcp代理(作用在第四层的代理)
  • 负载均衡
  • 虚拟主机

而且这些功能是正交的,也就是说他们不会相互冲突.Nginx在架构上虽然老旧但相当优秀,唯一的缺憾是没法动态配置,这也是其命门所在.

一个典型的配置

下面是一个典型的Nginx配置:


user  nginx;
worker_processes  1;

error_log  /dev/stdout info;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


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

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

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

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    # ssl_session_cache   shared:SSL:10m;
    # ssl_session_timeout 10m;

    #gzip  on;
    client_max_body_size  2000m;

    server {
        listen       80;
        server_name  www.hszofficial.site;

        location / {
            root   /usr/local/static/;
            autoindex on;

        }
    }
    server {
        listen       443 ssl;
        server_name  www.hszofficial.site;
        ssl_certificate /run/secrets/hszofc_crt;
        ssl_certificate_key /run/secrets/hszofc_key;
        ssl_session_timeout 5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
        ssl_prefer_server_ciphers on;

        location / {
            root   /usr/local/static/;
            autoindex on;
        }
    }
    upstream gogs{
        server  gogs:3000;
    }
    server {
        listen       80;
        server_name  code.hszofficial.site;

        location / {
            proxy_pass http://gogs;
        }
    }
    server {
        listen       443 ssl;
        server_name  code.hszofficial.site;
        ssl_certificate /run/secrets/code_crt;
        ssl_certificate_key /run/secrets/code_key;
        ssl_session_timeout 5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
        ssl_prefer_server_ciphers on;

        location / {
            proxy_pass http://gogs;
        }
    }
}

本文需要的前置知识

  • 网络基础,工科本科内容即可
  • http基础,推荐参考图解http
  • docker使用方法

使用的工具

本文我们将基于nginx镜像,但官方的debian镜像并没有将我们要的功能都打包进去,所以我们使用alpine版本的镜像,基于它我们将配置放入镜像中构造要测试的环境,所有代码将放在我的博客项目的标签中.

nginx操作

nginx说到底只是一个软件,了解它的第一步是先会操作它.虽然在docker中我们我们基本已经抛弃了这些命令而是使用docker直接操作实例,但如果要体验下也是可以进入容器内部试试的.

检测配置文件是否格式正确

nginx -t -c /path/to/nginx.conf

启动

nginx -c /usr/nginx/nginx.conf

停止

  • nginx -s stop 快速停止nginx
  • nginx -s quit 有序停止

如果上面的不行可以直接kill进程

ps -ef | grep nginx查找到进程后用

kill -QUIT 主进程号     :从容停止Nginx
kill -TERM 主进程号     :快速停止Nginx
pkill -9 nginx          :强制停止Nginx

重启

  • nginx -s reload :修改配置后重新加载生效
  • nginx -s reopen :重新打开日志文件

Nginx配置语法

Nginx中的预置变量

nginx内置了大量的$开头的全局变量,这些变量在有些时候会非常有用,但网上很少有介绍的很全面的文章,这边是我整理的一部分,都是网上各处找的,后续有碰到新的会更新

预置变量 说明
$arg_PARAMETER 如果在请求中设置了查询字符串,那么这个变量包含在查询字符串是GET请求PARAMETER中的值.
$args 该变量的值是GET请求在请求行中的参数.
$binary_remote_addr 二进制格式的客户端地址
$body_bytes_sent 响应体的大小,即使发生了中断或者是放弃,也是一样的准确.
$content_length 该变量的值等于请求头中的Content-length字段的值
$cookie_COOKIE 该变量的值是cookie COOKIE的值
$document_root 该变量的值为当前请求的location(http,server,location,location中的if)中root指令中指定的值.
$document_uri $uri
$host 该变量的值等于请求头中Host的值.如果Host无效时那么就是处理该请求的server的名称.</br>在下列情况中$host变量的取值不同于$http_host变量.</br>当请求头中的Host字段未指定(使用默认值)或者为空值,那么$host等于server_name指令指定的值;</br>当Host字段包含端口时,$host并不包含端口号.</br>另外从0.8.17之后的nginx中,$host的值总是小写.
$hostname 有gethostname返回值设置机器名.
$http_HEADER 该变量的值为HTTP请求头HEADER,具体使用时会转换为小写,并且将”——”转换为”_“(下划线)
$http_origin 相当于request.getHeader("Origin"),同理还有其他的头信息比如$http_referer,$http_host都是一个构造方式
$is_args 如果设置了$args,那么值为”?”,否则为
$limit_rate 该变量允许限制连接速率.
$nginx_version 当前运行的nginx的版本号
$query_string $args
$remote_addr 客户端的IP地址
$remote_user 该变量等于用户的名字,一般在Basic-Auth中使用.
$remote_port 客户端连接端口
$request_filename 该变量等于当前请求文件的路径,由指令root或者alias和URI构成
$request_body 该变量包含了请求体的主要信息.该变量与proxy_pass或者fastcgi_pass相关
$request_body_file 客户端请求体的临时文件
$request_completion 如果请求成功完成那么显示OK;如果请求没有完成或者请求不是该请求系列的最后一部分,那么它的值为空.
$request_method 该变量表示请求使用http方法.
$request_uri 该变量的值等于原始的URI请求,就是说从客户端收到的参数包括了原始请求的URI,该值是不可以被修改的,不包含主机名.例如/foo/bar.php?arg=baz
$scheme 该变量表示HTTP scheme(例如HTTP,HTTPS),根据实际使用情况来决定.例如:rewrite ^ $scheme://example.com$uri redirect;
$server_addr 该变量的值等于服务器的地址.通常来说在完成一次系统调用之后就会获取变量的值,为了避开系统钓鱼,那么必须在listen指令中使用bind参数.
$server_name 该变量为server的名字.
$server_port 该变量等于接收请求的端口.
$server_protocol 该变量的值为请求协议的值,通常是HTTP/1.0,HTTP/1.1,HTTP/2
$uri 该变量的值等于当前请求中的URI(没有参数,不包括$args)的值.它的值不同于request_uri.另外需要注意$uri不包含主机名,例如/foo/bar.html,

使用map指令自定义变量 (updated @ 2019-03-21)

map指令是由ngx_http_map_module模块提供的,默认情况下安装nginx都会安装该模块.

map的主要作用是创建自定义变量,通过使用nginx的内置变量去匹配某些特定规则,如果匹配成功则设置某个值给自定义变量.而这个自定义变量又可以作于他用.

一个例子:

map $args $foo {
    default 0;
    debug   1;
}

$args是nginx内置变量,可以获取的请求url的参数.上面的例子如果$args匹配到debug那么$foo的值会被设为1. 如果$args一个都匹配不到,$foo就是default定义的值,在这里就是0.

可以看出map中的规则有点类似一般编程语言中的switch.只是并不是匹配到了就break.

利用location匹配规则寻找符合要求的url

nginx灵活的根源就在于它支持匹配命令,基于这个我们可以识别请求过来的访问路径,根据需要修改这个路径以分发给合适的后端服务.

location匹配命令

  • ~ 波浪线表示执行一个正则匹配,区分大小写
  • ~* 表示执行一个正则匹配,不区分大小写
  • ^~ ^~表示普通字符匹配,如果该选项匹配,只匹配该选项,不匹配别的选项,一般用来匹配目录
  • = 进行普通字符精确匹配
  • @ @ 定义一个命名的 location,使用在内部定向时,例如 error_page, try_files

location 匹配的优先级(与location在配置文件中的顺序无关)

  • = 精确匹配会第一个被处理。如果发现精确匹配,nginx停止搜索其他匹配。
  • 普通字符匹配,正则表达式规则和长的块规则将被优先和查询匹配,也就是说如果该项匹配还需去看有没有正则表达式匹配和更长的匹配。
  • ^~ 则只匹配该规则,nginx停止搜索其他匹配,否则nginx会继续处理其他location指令。
  • 最后匹配理带有”~”和”~*“的指令,如果找到相应的匹配,则nginx停止搜索其他匹配;当没有正则表达式或者没有正则表达式被匹配的情况下,那么匹配程度最高的逐字匹配指令会被使用。

location 优先级官方文档

  • =前缀的指令严格匹配这个查询。如果找到,停止搜索。
  • 所有剩下的常规字符串,最长的匹配。如果这个匹配使用^〜前缀,搜索停止。
  • 正则表达式,在配置文件中定义的顺序。
  • 如果第3条规则产生匹配的话,结果被使用。否则,使用第2条规则的结果。

例子

location  = / {
  # 只匹配"/".
  [ configuration A ] 
}
location  / {
  # 匹配任何请求,因为所有请求都是以"/"开始
  # 但是更长字符匹配或者正则表达式匹配会优先匹配
  [ configuration B ] 
}
location ^~ /images/ {
  # 匹配任何以 /images/ 开始的请求,并停止匹配其它location
  [ configuration C ] 
}
location ~* .(gif|jpg|jpeg)$ {
  # 匹配以 gif, jpg, or jpeg结尾的请求. 
  # 但是所有 /images/ 目录的请求将由 [Configuration C]处理.   
  [ configuration D ] 
}

通过子配置实现模块化配置

nginx如何执行完全依赖于其配置文件.通常我们不会将所有的配置都放在同一个文件下,这样不利于管理.nginx支持使用include <xxx>.conf的方式导入多个单独的配置文件组合成完整的配置.其实现类似C语言中的include,也就是说只是将文件中的内容复制进来放在了导入的位置.

不同服务的配置文件可以放在对应的文件中.我们可以人为的规定

  • http静态文件配置放在/etc/nginx/conf.d/static.d文件夹
  • http代理配置放在/etc/nginx/conf.d/httpproxy.d文件夹
  • tcp代理配置放在/etc/nginx/conf.d/streamproxy.d文件夹

接着我们改造我们的默认配置文件nginx.conf如下:

user  nginx;
worker_processes  1;

error_log  /dev/stdout info;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


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

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

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

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    # ssl_session_cache   shared:SSL:10m;
    # ssl_session_timeout 10m;

    #gzip  on;
    client_max_body_size  2000m;
    include /etc/nginx/conf.d/static.d/*.conf;
    include /etc/nginx/conf.d/httpproxy.d/*.conf;
}
stream {
    include /etc/nginx/conf.d/streamproxy.d/*.conf;
}

作为http静态服务器

http下我们通过设置server来配置.

server {
    listen       80;
    server_name  0.0.0.0; # 你的域名
    location / {
        root   /usr/share/nginx/html; #存放静态文件的目标文件夹绝对路径
        index  index.html index.htm;
    }
}

例子代码在nginx-example-static

虚拟主机

如果我们要运行多个带前端渲染和复杂路由逻辑的app,那最方便的方法就是使用虚拟主机.

Nginx下一个server标签就可以是一个虚拟主机.虚拟主机不光可以是http静态服务,也可以是反向代理.

nginx支持三种虚拟主机形式:

  1. 基于域名的虚拟主机,通过域名来区分虚拟主机,常应用于对外的服务划分业务边界,比如二级域名为blog就是博客页面,www就是主页,devlop就是开发者页面.

  2. 基于端口的虚拟主机,通过端口来区分虚拟主机,常应用于公司内部网站,外部网站的管理后台,以及聚合代理相关的api服务

  3. 基于ip的虚拟主机,因为ip地址是一种比较难获取的资源,所以几乎不用(不做介绍)

基于域名配置虚拟主机

例子代码在nginx-example-vh

步骤:

  1. 去域名解析商那边注册解析二级域名

  2. 配置主机的http或stream

server {

  listen 80;#监听的端口
  server_name www.linuxidc.com;#www为二级域名的域名
  location /{
        root   /data/www;#存放静态文件的地方
        index  index.html index.htm;
    }
}
server {
    listen 80;#监听的端口
    server_name bbs.linuxidc.com;#bbs为二级域名的域名
    location /{
        root   /data/bbs;#存放静态文件的地方
        index  index.html index.htm;
    }
}

基于端口的虚拟主机

使用端口来区分,浏览器使用域名或ip地址:端口号访问,这种方式只用修改配置文件即可

server {

    listen 8000;#监听的端口
    server_name www.linuxidc.com;#域名
    location /{
        root   /data/www;#存放静态文件的地方
        index  index.html index.htm;
    }
}
server{
    listen 8001;#监听的端口
    server_name www.linuxidc.com;#域名
    location /{
        root   /data/bbs;#存放静态文件的地方
        index  index.html index.htm;
    }
}

例子代码在nginx-example-vh-port

这两种方式我们可以发现是正交的,也就是说两种虚拟机的形式可以任意组合.

将http服务升级为https服务

https协议就是在http协议的基础上加上tls协议,它可以保证服务器可信,保护请求响应数据不被拦截篡改.

通常https有两种形式:

  • 单向校验,客户端确保服务端可信

  • 双向校验,客户端和服务端各自确信对方可信.

这两种形式nginx都支持.

无论是单向校验还是双向校验,我们都需要先有证书.如何获得证书可以参考我之前的文章

nginx中与SSL/TLS相关的配置可以查看官网文档常用的配置有如下:

配置选项 含义
ssl_protocols tls协议版本,一般设置为TLSv1 TLSv1.1 TLSv1.2
ssl_certificate tls服务证书路径
ssl_certificate_key tls服务证书对应的私钥路径
ssl_verify_client 设置为on启动双向认证,optional则有证书就用,没有就不用
ssl_client_certificate 指定双向认证的根证书位置

单项校验

例子代码在nginx-example-https

https服务的配置如下:

server {
    listen       443 ssl;
    server_name  localhost;
    ssl_certificate /data/crt/server-cert.pem; #设置ssl证书的绝对路径,
    ssl_certificate_key /data/crt/server-key.pem; #设置ssl证书私钥的绝对路径
    ssl_session_timeout 5m; #ssl session的过期时间
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl使用的协议,这个要看发证书的如何定义

    location / {
        root   /usr/share/nginx/html;
        autoindex on;
    }
}

通常如果你的证书不是自签证书那么最好是按照证书提供方给出的方法配置.通常情况下只需要配置好服务证书和对应私钥即可.

双向校验(updated @ 2020-12-04)

例子代码在nginx-example-https-client-verify中.

如果使用双向校验,那么配置方法略有不同,我们需要激活nginx的ssl_verify_client并指名客户证书的根证书

https服务的配置如下:

server {
    listen       443 ssl;
    server_name  localhost;
    ssl_certificate /data/crt/server-cert.pem; #设置ssl证书的绝对路径,
    ssl_certificate_key /data/crt/server-key.pem; #设置ssl证书私钥的绝对路径
    ssl_session_timeout 5m; #ssl session的过期时间
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl使用的协议,这个要看发证书的如何定义

    ssl_verify_client on;
    ssl_client_certificate /data/crt/ca.pem;

    location / {
        root   /usr/share/nginx/html;
        autoindex on;
    }
}

强制使用https

例子代码在nginx-example-https-force

安全起见,现在的网站都要求使用https,但毕竟不是所有的访问者都习惯用https替代http的.有一种方式就是我们让http的访问强制跳转为https的.这个也是可以在nginx中设置的.

server {
    listen       8080;
    listen       8443 ssl;
    server_name  localhost;
    ssl_certificate /data/crt/server-cert.pem; #设置ssl证书的绝对路径,
    ssl_certificate_key /data/crt/server-key.pem; #设置ssl证书私钥的绝对路径
    ssl_session_timeout 5m; #ssl session的过期时间
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl使用的协议,这个要看发证书的如何定义

    if ($server_port = 8080 ) {
            return 301 https://$host:8443$request_uri;
    }
    location / {
        root   /usr/share/nginx/html;
        autoindex on;
    }
}

注意上面例子我们因为指定的端口不是http和https协议的默认端口,因此需要额外注意端口,如果都是默认端口,则可以写作:

server {
    listen       80;
    listen       443 ssl;
    server_name  localhost;
    ssl_certificate /data/crt/server-cert.pem; #设置ssl证书的绝对路径,
    ssl_certificate_key /data/crt/server-key.pem; #设置ssl证书私钥的绝对路径
    ssl_session_timeout 5m; #ssl session的过期时间
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl使用的协议,这个要看发证书的如何定义

    if ($server_port = 80 ) {
            return 301 https://$host$request_uri;
    }
    location / {
        root   /usr/share/nginx/html;
        autoindex on;
    }
}

使用server push提高前端性能(updated @ 2019-03-21)

服务器推送(server push)是HTTP/2协议里面唯一一个需要开发者自己配置的功能.它的作用是允许客户端在建立连接后服务端自作主张的将没有请求的资源也推给客户端,具体原理可以看图解http这本书中的相关知识.

这个功能基本只有一个使用场景–为单页应用预加载资源.我们知道单页应用的请求流程是

  1. 请求html页面
  2. 在获取到html页面后根据其中的script标签指示请求需要加载的js文件.

也就是说这个流程中至少有两次请求,他们是串行的.单页应用html页面一般相当小,而js文件则比较大,因为js文件是后获取到的,在这之前一次网络请求就已经花了不少时间,再加上浏览器渲染页面本身也挺花时间,因此说单页应用往往用户体验不好.

如果使用server push,那么在请求html后我们的服务端可以在给客户端响应html文件的同时一起把js文件也推过去,这样就可以省了一次网络请求的时间,从而优化了前端性能.

注意因为使用的是http2协议,所以必须使用https

最基本的server push是在linsten的参数中加上http2并在location中使用http2_push /style/css这样的形式的语句申明要推送的内容.

但这种不是推荐的方式,它的缺点有:

  • 每次有改动就得修改nginx配置
  • 已经请求过一次的再次请求还是会被推一遍文件,这样就浪费带宽了

针对这个我们可以:

  • 借助请求头上的cookie里的Link字段来确定需要加载哪些推送
  • 借助请求头上cookie里的一个标志位来判断是否已经加载过
server {
    listen       8080;
    listen       8443 ssl http2;
    server_name  localhost;
    ssl_certificate /data/crt/server-cert.pem; #设置ssl证书的绝对路径,
    ssl_certificate_key /data/crt/server-key.pem; #设置ssl证书私钥的绝对路径
    ssl_session_timeout 5m; #ssl session的过期时间
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl使用的协议,这个要看发证书的如何定义

    if ($server_port = 8080 ) {
            return 301 https://$host:8443$request_uri;
    }
    http2_push_preload on; # 自动根据请求的Link字段推送相应的资源

    location / {
        root   /usr/share/nginx/html;
        autoindex on;
        add_header Set-Cookie "session=1"; # 设置标志位存到cookie
        add_header Link $resources; # 将
    }
}


map $http_cookie $resources {
    "~*session=1" "";
    default "</style.css>; as=style; rel=preload";
}

这块的例子在nginx-example-serverpush,

例子中我们使用map$resources赋值,其规则是,如果碰到http_cookie里面有大小写不敏感的session=1标志位,那么就把它的值设置为空,如果没匹配到,那么就把它的值设置为"</style.css>; as=style; rel=preload"

这个奇怪的默认值时Link的描述语法,大致是:

  • 单个要推送的情况下头部Link为Link: </styles.css>; rel=preload; as=style
  • 多个要推送的情况下头部Link为Link: </styles.css>; rel=preload; as=style, </example.png>; rel=preload; as=image
  • 不同资源的as值可以看w3c上的规范

为http服务提供代理

所谓的代理就是在客户端和服务端之间的一种中间件,可以分为两种:

正向代理和反向代理

  • 正向代理

    有点类似团购的角色,它代理的是客户的要求

  • 反向代理

    有点类似淘宝的角色,它代理的是服务端.

正向代理

试想下这样一个场景,我们无法直接访问百度了,但有一台服务器它可以访问到,我们就可以在其上安装一个代理服务器,让我们的请求透过它来访问百度.

本例子代码在nginx-example-forward-proxy中,我们只是简单指定了下要指向的服务就可以实现正向代理了

server {  
    resolver 114.114.114.114;       #指定DNS服务器IP地址  
    listen 5000;  
    server_name localhost;
    location / {  
        proxy_pass http://www.baidu.com;     #设定代理服务器的协议和地址  
    }  
}

location中的proxy_pass申明了请求将会代理给哪个服务器,这边直接以百度为目标即可.

反向代理

反向代理一般会和负载均衡一起使用,在docker环境下负载均衡可以交给swarm的多实例负载均衡,swarm网络会使用虚拟ip(VIP)为请求分配实例进行处理.

反向代理长连接配置

随着http1.1的出现,连http协议都可以用上长连接了,nginx的反向代理也支持长连接,可以在上游(upstream)中设置相关参数:

  • keepalive 2000; 只有设置了keepalive才会使用长连接,不然nginx做反向代理转发一律会按照短连接处理.

  • keepalive_requests 30000; 一个tcp连接上跑了30000个请求,然后就断开连接.

  • keepalive_timeout 300s一个tcp连接300s空闲就会断开连接.

为RESTful接口提供反向代理和负载均衡

http接口服务的反向代理常用来聚合后端服务接口并暴露给不同的url.

反向代理

反向代理的例子中我们需要有两个后台服务,我们使用使用Javascript构建http接口服务文中的第一个例子第3个例子两个例子,这两个例子都已经打包好了上传在我的dockerhub下.有兴趣的可以去看下具体的实现.

这个例子的代码在nginx-example-reverse_proxy 反向代理的配置如下

upstream notification {
    server rest-notifucation:3000;
}

upstream helloworld {
    server rest-helloworld:3000;
}

server {
    listen 5000;  
    server_name localhost;
    location = / {  
        proxy_pass http://helloworld;     #设定代理服务器的协议和地址  
    }  
    location /api/ {
        #proxy_set_header Host $host;#http请求的头部 设置也可能是proxy_set_header Host $proxy_host
        #proxy_set_header X-Real-IP $remote_addr;
        #proxy_set_header X-Forwarded-For $remote_addr;
        #禁用缓存
        #proxy_buffering off;

        proxy_pass http://notification/;     #设定代理服务器的协议和地址  
    }  
}  

其中

  • upstream用于申明上游,也就是服务端.这边因为使用的是docker,因此用的是hostname,多数时候内网中还是用ip的多,需要注意端口也需要在这边申明中写上
  • location中的proxy_pass声明了代理指向的上游服务器.这边的上游使用了上面申明的upstream notification,如果没有申明我们可以直接写host
  • 多个location的匹配规则用于将请求分流给不同的上游.
  • 请求的url会在匹配规则下自动重组,比如请求的url为/api/notification会在进入location /api/这个匹配后以/notification的形式请求上游http://notification,具体可以看官方文档上的描述
  • proxy_set_header常用于添加或者设置请求头以实现一些特殊功能,上面例子中的Host,X-Real-IPX-Forwarded-For一般设置了便于反向代理的服务器了解真实请求的客户端ip信息.
负载均衡

负载均衡是用来减轻单个服务的压力的,要使用负载均衡就必须使用关键字upstream用于申明上游.我们在一个上游中申明多个服务,以及这些服务的权重等负载均衡分配方式信息. 之后再反向代理中使用proxy_pass指定这个申明的上游就可以在反向代理中带上负载均衡了.

Nginx的upstream支持5种分配方式,其中轮询,加权,ip_hash这三种为Nginx原生支持的分配方式,fair则需要使用模块提供实现,而hash则是ip_hash的扩展.

轮询 轮询是upstream的默认分配方式,即每个请求按照时间顺序轮流分配到不同的后端服务器.如果某个后端服务器down掉后它会被自动剔除.

upstream <name> {
    server 192.168.1.251;
    server 192.168.1.252;
    server 192.168.1.247;
}

加权

加权是轮询的威力加强版,它相当于按比例分配给不同的服务不同的执行机会,主要应用于后端服务器异质的场景下

upstream <name> {
    server 192.168.1.251 weight=1;
    server 192.168.1.252 weight=4;
    server 192.168.1.247 weight=2;
}

ip_hash

每个请求按照访问Ip(即Nginx的前置服务器或客户端IP)的hash结果分配,这样每个访客会固定访问一个后端服务器,可以解决session一致问题

upstream <name> {
    ip_hash;
    server 192.168.1.251;
    server 192.168.1.252;
    server 192.168.1.247;
}

fair

需要在编译nginx时指定模块ngx_http_upstream_fair_module.

fair顾名思义,公平地按照后端服务器的响应时间(rt)来分配请求.响应时间小的后端服务器优先分配请求.

upstream backend {
    server 192.168.1.251;
    server 192.168.1.252;
    server 192.168.1.247;
    fair;
}

hash

nginx现在也支持使用hash key [consistent]指令指定依靠什么信息来做散列 与ip_hash类似也是按hash结果来分配请求,使得每个请求定向到同一个后端服务器.主要应用于后端服务器为缓存的场景下.

常见的指定内容为:

  • $remote_addr可以根据客户端ip映射
  • $request_uri根据客户端请求的uri映射
  • $arg根据客户端携带的参数进行映射
upstream backend {
    hash $request_uri
    server 192.168.1.251;
    server 192.168.1.252;
    server 192.168.1.247;
}

这个例子的代码在nginx-example-load_balance

解决跨域问题

不少时候我们做反向代理是为了聚合api给前端调用的,但如果我们聚合的api和前端页面不再一个域就尴尬了,当然我们可以在服务端使用CORS解决,但每个都这么搞还是比较麻烦的,如果在nginx这个聚合api的网关上直接把这个问题解决了就最好了.

nginx确实可以做这个事情.有两种思路:

  • 将聚合的API放在和页面相同的域下,比如页面在http://www.myhost:80,那聚合的api就放在http://www.myhost:80/api.

    这种方式最简单,但可能并不好.我们往往更愿意接口和页面分离使用不同的二级域名,这样在业务上更加清晰.那就得用第二种方式

  • Nginx配置CORS

    这个例子借用我的前端技术攻略中相关部分代码.这个服务我们将其修改后也放在这个例子项目下,它在server文件夹下,使用bash huild.sh来打包成镜像

    例子的代码在nginx-example-cors,我们使用基于域名配置虚拟主机把后端api挂在8000端口下,而前端页面挂在8001端口下.这样就会出现跨域问题了.我们在api的部分设置上CORS的头部

      upstream helloworld {
          server rest-api:4000;
      }
    
      server {
          listen 8000;  
          server_name localhost;
          location / {
              add_header 'Access-Control-Allow-Origin' '*';
              add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS';
              add_header 'Access-Control-Allow-Credentials' true;
              add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
              add_header 'Access-Control-Max-Age' 1728000;
              proxy_pass http://helloworld;     #设定代理服务器的协议和地址  
          }
      }
    

    其中add_header 'Access-Control-Allow-Origin'可以设置准许的host,add_header 'Access-Control-Allow-Methods'可以设置准许的http方法使用的时候要注意的也就这两处.

为websocket提供反向代理 (updated @ 2019-03-22)

websocket通过http握手,我们的nginx也是在http下对其反向代理做配置.

下面是例子nginx-example-proxy-ws的配置,这个配置基本通用.

upstream helloworld {
    server ws-api:3000;
    keepalive 2000;
}
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 8000;  
    server_name localhost;
    location / {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS';
        add_header 'Access-Control-Allow-Credentials' true;
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        add_header 'Access-Control-Max-Age' 1728000;
        proxy_pass http://helloworld;     #设定代理服务器的协议和地址
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

这个配置有两处要注意的:

  • map $http_upgrade $connection_upgrade这段用于自定义一个变量$connection_upgrade,这个变量是给后面proxy_set_header Connection赋值的.
  • 在指定代理的上游后需要设置头信息 proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection $connection_upgrade;,原因可以看我的js服务端开发教程中的相关描述

为grpc提供反向代理和负载均衡 (updated @ 2019-03-27)

自nginx 1.13.10起nginx可以代理grpc了,它需要在编译时使用模块with-http_ssl_module,with-http_v2_module

下面是例子nginx-example-proxy-grpc的配置.

upstream helloworld {
    server grpc-api:5000;
    keepalive 2000;
}

server {
    listen 8000 http2;
    server_name localhost;
    location / {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS';
        add_header 'Access-Control-Allow-Credentials' true;
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        add_header 'Access-Control-Max-Age' 1728000;
        grpc_pass grpc://helloworld;     #设定代理服务器的协议和地址
    }
}

这个配置与一般的http代理区别在于:

  • 它基于http2
  • 它的代理声明使用的不是proxy_pass而是grpc_pass
  • 它的scheme不是http或者https,而是单独的grpc

作为第四层协议的反向代理 (updated @ 2019-03-27)

自1.9.0版本开始nginx可以在第四层(tcp/udp)代理服务了,这也就意味着我们可以使用nginx代理如redis,postgresql等基于tcp的服务了.

要在第四层代理服务需要使用和http同级的stream关键字来定义,一个完整的config文件大概是这样

user  nginx;
worker_processes  1;

error_log  /dev/stdout info;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


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

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

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

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    # ssl_session_cache   shared:SSL:10m;
    # ssl_session_timeout 10m;

    #gzip  on;
    client_max_body_size  2000m;
    include /etc/nginx/conf.d/static.d/*.conf;
    include /etc/nginx/conf.d/httpproxy.d/*.conf;
    include /etc/nginx/conf.d/tcpproxy.d/*.conf;
}
stream {
    include /etc/nginx/conf.d/streamproxy.d/*.conf;
}

我们先把streamproxy.d文件夹作为第四层代理的保存位置.

tcp协议的反向代理

以代理redis为例,例子在nginx-example-proxy-tcp. 我们需要在streamproxy.d下新增配置文件tcpproxy.conf:

upstream redis {
    server redis-test:6379;
}

server {
    listen 5000;
    proxy_pass redis;     #设定代理服务器的协议和地址  
    proxy_connect_timeout 1h;
    proxy_timeout 1h;
}

可以看到这个结构和http的没什么区别.