工程经验 - Nginx 反向代理实战
(งツ)ว 最近一段时间跟 Nginx 打交道,主要是用它作为 gRPC 协议的反向代理,调研期间碰到了不少问题与有趣的现象
本篇文章以代理 gRPC 协议为出发点,分享如下:
- 如何快速在本地搭建可用的 Nginx 代理
- 常见的协议代理做法
- Nginx 反向代理的基本实现原理做个初探解析
注意:本文章基于 MacOS Big Sur,使用 Win 平台的同学可能有些命令需要兼容 ”手动翻译“ 一下
一、前言
Nginx 常作为 HTTP 协议的代理网关,2018 年 gRPC 发布后,Nginx 紧跟步伐,推出了对 gRPC 协议代理的支持,不过从 Change
来看,其实 bug 也不少,但好歹紧跟着更新发布了
版本 | 日期 | 特性 |
---|---|---|
1.13.10 | 20 Mar 2018 | Feature: the ngx_http_grpc_module. |
1.13.12 | 10 Apr 2018 | Bugfix: connections with gRPC backends might be closed unexpectedly when returning a large response. |
1.15.1 | 03 Jul 2018 | Bugfix: sending a disk-buffered request body to a gRPC backend might fail. |
1.15.4 | 25 Sep 2018 | Bugfix: connections with some gRPC backends might not be cached when using the "keepalive" directive. |
1.15.6 | 06 Nov 2018 | 1、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.8 | 21 Jan 2020 | Feature: variables support in the "grpc_pass" directive. |
1.19.0 | 26 May 2020 | Bugfix: "upstream sent frame for closed stream" errors might occur when working with gRPC backends. |
1.19.1 | 07 Jul 2020 | Change: now after receiving a response with incorrect length from a gRPC backend nginx stops response processing with an error. |
1.19.4 | 27 Oct 2020 | Feature: the "ssl_conf_command", "proxy_ssl_conf_command", "grpc_ssl_conf_command", and "uwsgi_ssl_conf_command" directives. |
1.19.5 | 24 Nov 2020 | Bugfix: "upstream sent frame for closed stream" errors might occur when working with gRPC backends. |
nginx 1.19.9 | 30 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.0 | 25 May 2021 | Feature: 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.1 | 06 Jul 2021 | Bugfix: keepalive connections with gRPC backends might not be closed after receiving a GOAWAY frame. |
nginx 1.21.2 | 31 Aug 2021 | Bugfix: 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 的七层负载的