玩转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版本的镜像,基于它我们将配置放入镜像中构造要测试的环境,所有代码将放在我的get_along_well_with_nginx项目中.

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;
    }
}

例子代码在C1-S1

虚拟主机

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

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

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

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

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

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

基于域名配置虚拟主机

例子代码在C1-S2

步骤:

  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;
    }
}

例子代码在C1-S3

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

将http服务升级为https服务

例子代码在C1-S4

无论是http静态服务还是走http的反向代理,还是http的虚拟主机,我们都可以将其http协议升级为https协议.这之中只需要做两件事:

  1. 获得ssl证书
  2. 部署ssl证书到nginx

获得ssl证书

要将http服务器升级为https服务器,我们需要ssl的私钥和证书.正式的证书需要证书当局(CA)的签名,如果你已经有域名了可以在腾讯云上免费申请类似的阿里云也提供这样的服务. 在没有域名的情况下我们可以自己做一个签名.这个方法参考了digitalocean上的例子

在你的机器有OpenSSL的情况下使用如下命令即可创建一对私钥和证书.

sudo openssl req \
  -x509 \
  -nodes \
  -days 365 \
  -newkey rsa:2048 \
  -keyout example.key \
  -out example.crt

上面命令的各个参数含义如下

参数 说明
req 处理证书签署请求
-x509 生成自签名证书
-nodes 跳过为证书设置密码的阶段,这样Nginx才可以直接打开证书.
-days 365 证书有效期为一年
-newkey rsa:2048 生成一个新的私钥,采用的算法是2048位的RSA
-keyout 新生成的私钥文件为当前目录下的example.key
-out 新生成的证书文件为当前目录下的example.crt

执行后命令行会跳出一堆问题要你回答,就和npm init一样,其他的都可以瞎填,唯独Common Name,正常情况下应该填入一个域名.这里可以填你本机的内网ip地址或者localhost

回答完问题,当前目录应该会多出两个文件:example.keyexample.crt,这样要的证书和签名就有了.

部署ssl证书

http下我们通过设置server来设置,一般来说https服务默认的监听端口为443

server {
    listen       443 ssl;
    server_name  www.hszofficial.site;
    ssl_certificate /path/to/crt; #设置ssl证书的绝对路径,
    ssl_certificate_key /path/to/key; #设置ssl证书私钥的绝对路径
    ssl_session_timeout 5m; #ssl session的过期时间
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl使用的协议,这个要看发证书的如何定义
    ssl_ciphers HIGH:!aNULL:!MD5; # ssl的加密算法,这个要看发证书的如何定义
    ssl_prefer_server_ciphers on;

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

注意如果是按上面的方法自己创建的证书私钥对,可以按上面的配置来使用,但如果是腾讯阿里拿的或者自己买的,那请按官方给出的参数进行配置

在docker环境下配置证书和私钥

通常我们在docker中配置ssl不会直接将文件放入镜像,这样挺奇怪的,而是会将其放入docker swarm的secrets中.

在Docker中,Secret是一种BLOB(二进制大对象)数据,像密码,SSH私钥,SSL证书或那些不应该未加密就直接存储在Dockerfile或应用程序代码中的数据就应该放在其中.在Docker 1.13及更高版本中我们可以使用Docker Secrets集中管理这些数据并将其安全地传输给需要访问的容器.一个给定的Secret只能被那些已被授予明确访问权限的服务正在运行的情况下使用.

不想在镜像或代码中管理的任何敏感数据我们都可以使用Secret来管理,比如:

  • 用户名和密码
  • TLS certificates and keys
  • SSH keys
  • 数据库名
  • 内部服务器信息
  • 通用的字符串或二进制内容 (最大可达 500 Kb)
secrets的操作
  • 创建Secret,通常我们都是通过文件创建secret对象的.
docker secret create [参数] SECRET [file|-]

参数:

简写 参数 默认值 描述
-d --driver Secret 驱动
-l --label 配置标签

例子:

docker secret create mysecret ./secret.json
  • 删除一条secret
docker secret rm SECRET
  • 查看secret列表
docker secret ls [参数]

参数:

简写 参数 默认值 描述
-f --filter 按条件过滤输出
--format GO模板转化
-q --quiet 仅展示ID
  • 查看某一条secret
docker secret inspect [参数] SECRET [SECRET...]

参数:

简写 参数 默认值 描述
-f --format GO模板转化
--pretty 以人性化的格式打印信息
在配置中使用secret

secret可以当做就是一个文件,它的路径默认在/run/secrets/[secret]上所以只要拿这个地址放在配置中相应的位置即可.

顺道一提,docker同样提供了configs用于管理配置文件,但nginx的配置文件比较特殊不建议使用这个管理,因为nginx换个配置文件做的事情就完全不一样了和代码其实是差不多的不是传统意义上的配置. config无法做到版本管理,所以建议还是讲它的配置文件放入镜像,拿label管理好版本.

强制使用https

例子代码在C1-S5

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

server {
    listen       80;
    listen       443 ssl;
    server_name  test.dooffe.com;
    ssl on;

    ssl_certificate /path/to/crt; #设置ssl证书的绝对路径,
    ssl_certificate_key /path/to/key; #设置ssl证书私钥的绝对路径
    ssl_session_timeout 5m; #ssl session的过期时间
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # ssl使用的协议,这个要看发证书的如何定义
    ssl_ciphers HIGH:!aNULL:!MD5; #ssl的加密算法,这个要看发证书的如何定义
    ssl_prefer_server_ciphers on;

    if ($server_port = 80 ) {
            return 301 https://$host$request_uri;
    }
    location / {
        root   /usr/local/static/;
        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       80;
    listen       443 ssl http2; # http2支持
    server_name  localhost;
    ssl_certificate /data/crt/example.crt;
    ssl_certificate_key /data/crt/example.key;
    ssl_session_timeout 5m;
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    if ($server_port = 80 ) {
                return 301 https://$host$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";
}

这块的例子在C1-S6,

例子中我们使用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服务提供代理

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

正向代理和反向代理

  • 正向代理

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

  • 反向代理

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

正向代理

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

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

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)为请求分配实例进行处理.

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

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

反向代理

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

这个例子的代码在C2-S2 反向代理的配置如下

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 $proxy_add_x_forwarded_for;
        #禁用缓存
        #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,具体可以看官方文档上的描述
负载均衡

负载均衡是用来减轻单个服务的压力的,要使用负载均衡就必须使用关键字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;
}

这个例子的代码在C2-S3,我们使用默认的轮询配置来做,负载均衡,这个例子由上面的C2-S2,修改而来,我们为rest-helloworld多做几个实例,而另一和相关的就不启动了.

解决跨域问题

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

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

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

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

  • Nginx配置CORS

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

    例子的代码在C2-S4,我们使用基于域名配置虚拟主机把后端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下对其反向代理做配置.

下面是例C2-S5的配置,这个配置基本通用.

upstream helloworld {
    server ws-api:3000;
}
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

下面是例C2-S6的配置.

upstream helloworld {
    server grpc-api:5000;
}

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

为grpc-web提供代理

在上面的为grpc提供反向代理和负载均衡部分我们介绍的方法只适用于客户端为非浏览器的情况,在客户端为浏览器的情况时我们需要使用grpc-web

grpc-web需要有一层envoy作为网关,而nginx为grpc-web提供代理实际就是为这层网关做代理.

例子C2-S7使用了前端使用grpc-web攻略中的例子

不同之处只是我们用nginx代理envoy网关并将静态页面放在nginx中

  • 静态页面配置

      server {
          listen 8001;
          server_name localhost;
          location /{
              root   /data/www;
              index  index.html index.htm;
          }
      }
    
  • 代理网关配置

      upstream helloworld {
          server grpc_web_proxy:8000;
      }
    
      server {
          listen 8000;
          server_name localhost;
          location / {
              grpc_pass helloworld;     #设定代理服务器的协议和地址
              if ($request_method = 'OPTIONS') {
                  add_header 'Access-Control-Allow-Origin' '*';
                  add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
                  add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Transfer-Encoding,Custom-Header-1,X-Accept-Content-Transfer-Encoding,X-Accept-Response-Streaming,X-User-Agent,X-Grpc-Web';
                  add_header 'Access-Control-Max-Age' 1728000;
                  add_header 'Content-Type' 'text/plain charset=UTF-8';
                  add_header 'Content-Length' 0;
                  return 204;
              }
          }
      }
    

    注意这个代理依然需要使用grpc_pass,但不再需要标明http2,同时需要为$request_method = 'OPTIONS'设置CROS,并返回一个204让客户端继续使用post方法访问

作为第四层协议的反向代理 (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为例,我们需要在streamproxy.d下新增配置文件tcpproxy.conf:

upstream redis {
    server redis-test:6379;
}

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

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