工程经验 - Nginx 反向代理实战

1035

(งツ)ว 最近一段时间跟 Nginx 打交道,主要是用它作为 gRPC 协议的反向代理,调研期间碰到了不少问题与有趣的现象

本篇文章以代理 gRPC 协议为出发点,分享如下:

  • 如何快速在本地搭建可用的 Nginx 代理
  • 常见的协议代理做法
  • Nginx 反向代理的基本实现原理做个初探解析

注意:本文章基于 MacOS Big Sur,使用 Win 平台的同学可能有些命令需要兼容 ”手动翻译“ 一下

一、前言

Nginx 常作为 HTTP 协议的代理网关,2018 年 gRPC 发布后,Nginx 紧跟步伐,推出了对 gRPC 协议代理的支持,不过从 Change 来看,其实 bug 也不少,但好歹紧跟着更新发布了

版本日期特性
1.13.1020 Mar 2018Feature: the ngx_http_grpc_module.
1.13.1210 Apr 2018Bugfix: connections with gRPC backends might be closed unexpectedly when returning a large response.
1.15.103 Jul 2018Bugfix: sending a disk-buffered request body to a gRPC backend might fail.
1.15.425 Sep 2018Bugfix: connections with some gRPC backends might not be cached when using the "keepalive" directive.
1.15.606 Nov 20181、Bugfix: working with gRPC backends might result in excessive memory consumption.
2、Feature: the "proxy_socket_keepalive", "fastcgi_socket_keepalive", "grpc_socket_keepalive", "memcached_socket_keepalive", "scgi_socket_keepalive", and "uwsgi_socket_keepalive" directives.
1.17.821 Jan 2020Feature: variables support in the "grpc_pass" directive.
1.19.026 May 2020Bugfix: "upstream sent frame for closed stream" errors might occur when working with gRPC backends.
1.19.107 Jul 2020Change: now after receiving a response with incorrect length from a gRPC backend nginx stops response processing with an error.
1.19.427 Oct 2020Feature: the "ssl_conf_command", "proxy_ssl_conf_command", "grpc_ssl_conf_command", and "uwsgi_ssl_conf_command" directives.
1.19.524 Nov 2020Bugfix: "upstream sent frame for closed stream" errors might occur when working with gRPC backends.
nginx 1.19.930 Mar 2021"upstream sent response body larger than indicated content length" errors might occur when working with gRPC backends; the bug had appeared in 1.19.1.
1.21.025 May 2021Feature: variables support in the "proxy_ssl_certificate", "proxy_ssl_certificate_key" "grpc_ssl_certificate", "grpc_ssl_certificate_key", "uwsgi_ssl_certificate", and "uwsgi_ssl_certificate_key" directives.
nginx 1.21.106 Jul 2021Bugfix: keepalive connections with gRPC backends might not be closed after receiving a GOAWAY frame.
nginx 1.21.231 Aug 2021Bugfix: SSL connections with gRPC backends might hang if select, poll, or /dev/poll methods were used.

二、搭建 Nginx 环境

直接使用 docker-compose 跑以下脚本即可:docker-compose -f nginx-service.ymal up -d

version: '3'
services:
  nginx-grpc:
    image: nginx:1.21.4 # latest nginx
    container_name: nginx-grpc
    restart: always
    ports:
      - 11443:443
      - 11080:80
      - 16667:6667
    volumes:
      - $PWD/data-nginx/html:/usr/share/nginx/html
      - $PWD/data-nginx/logs:/var/log/nginx
      - $PWD/data-nginx/conf/nginx.conf:/etc/nginx/nginx.conf
      - $PWD/data-nginx/cert:/etc/nginx/cert
      # 主机本机时间文件映射 与本机时间同步
      - /etc/localtime:/etc/localtime:ro

三、代理 HTTPS

这里证书配置在 Nginx,代理给目标服务器的时候就不需要配置证书了,所以最后没有转发 https 流量,而是将 http 流量转给目标服务器,客户端使用 Postman 测试的时候,没有配置证书是可以正常请求的

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
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;

    keepalive_timeout  65;

    include /etc/nginx/conf.d/*.conf;
    client_max_body_size 100m;

    # 映射到 docker 宿主机本地端口服务
    upstream  local_spring {
        server host.docker.internal:6666;
    }

    # 配置代理服务器
    server {
        listen 443 ssl;
        charset utf-8;
        ssl on;
        ssl_certificate   /etc/nginx/cert/pub.pem; # https 证书 - 可自签
        ssl_certificate_key  /etc/nginx/cert/pkcs8.pri.key; # https 私钥 - 可自签
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers on;
        ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES256-SHA:HIGH:!MEDIUM:!LOW:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:@STRENGTH";

        location /local {
            proxy_pass http://local_spring/;
        }

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $http_x_forwarded_for;
        client_max_body_size 100M;
        proxy_request_buffering off;
        proxy_connect_timeout 1800s;
        proxy_send_timeout 1800s;
        proxy_read_timeout 1800s;
    }

}

四、代理 gRPC

注意这里的代理基于 SSL 双向认证: gRPC + SSL ,gRPC Client 持有 client 证书,gRPC Server 持有 server 证书的代理

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
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;

    include /etc/nginx/conf.d/*.conf;
    client_max_body_size 100m;

    upstream local_grpc {
        server host.docker.internal:6667;
    }

    server {
        listen 6667 ssl http2;
        charset utf-8;

        # Mutula SSL/TLS between gRPC client and NGINX
        ssl_certificate   /etc/nginx/cert/pub.pem; # 公钥
        ssl_certificate_key  /etc/nginx/cert/pkcs8.pri.key; # 私钥

        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers on;
        ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES256-SHA:HIGH:!MEDIUM:!LOW:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:@STRENGTH";

        client_max_body_size 100M;
        grpc_connect_timeout 1800s;

        grpc_set_header X-Real-IP $remote_addr;
        grpc_set_header Host $host;
        grpc_set_header X-Forwarded-For $http_x_forwarded_for;
        client_header_timeout 180s;
        client_body_timeout 180s;
        location / {
    				# 由于加了 SSL,这里配置的是 grpcs,如果没有,使用 grpc 即可
            grpc_pass grpcs://local_grpc ;

            # For mutual SSL/TLS connection between NGINX and backend servers
            grpc_ssl_certificate  /etc/nginx/cert/pub.pem;
            grpc_ssl_certificate_key /etc/nginx/cert/pkcs8.pri.key;
            grpc_send_timeout 1800s;
            grpc_read_timeout 1800s;
        }
    }
}
  • 原地址:grpc.server.domain:6667
  • 代理地址:grpc.server.domain:16667

五、代理 TCP

Nginx 支持代理 TCP,代理 TCP 流量时,无论流量是经过 SSL 加密还是没有加密都可以代理(毕竟都是 TCP 流而已)

此时不管是啥应用层协议,只要你基于 TCP,都能由 Nginx 代理,比如 gRPC,或者 gRPC + SSL 的流量都能使用 Nginx 代理

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

# tcp proxy
stream {
    upstream local_grpc {
        server host.docker.internal:6667;
    }
    server {
        listen 6667;
        proxy_connect_timeout 1800s;
        proxy_timeout 1800s;
        proxy_pass local_grpc;
    }
}
  • 原地址:grpc.server.domain:6667
  • 代理地址:grpc.server.domain:16667

六、初探代理原理

由于在工程中使用代理碰到了不少问题,期间对代理服务器(Nginx)和目标服务端抓包,”有幸“ 见识了代理服务器实现代理的过程

启动 server、proxy server,

tcpdump 抓包:

sudo tcpdump -i any port 16667 or port 6667 -w ./grpc.cap

启动 client,进行连接

根据工程日志,抓到有问题的包,结束,使用 wireshark 打开后会发现代理的关系其实是:

  • client 连接 proxy server ,一条 tcp 连接
  • proxy server 连接 server,一条 tcp 连接

当 server 在发送 FIN 包,四次挥手时,是针对 proxy server 的,proxy server 断开后,再发送 FIN 包给 client,进行断开;同理针对 client 主动断开也一样

七、七层负载

值得一提的是现代代理网关中讨论到代理与负载,都会区分是否支持七层负载,四层负载与七层负载的概念不多解释,Nginx 作为一个优秀的代理产品,它是支持 gRPC 的七层负载的