nginx作grpc的反向代理踩坑总结


Posted in Servers onJuly 07, 2021

背景

众所周知,nginx是一款高性能的web服务器,常用于负载均衡和反向代理。所谓的反向代理是和正向代理相对应,正向代理即我们常规意义上理解的“代理”:例如正常情况下在国内是无法访问google的,如果我们需要访问,就需要通过一层代理去转发。这个正向代理代理的是服务端(也就是google),而反向代理则相反,代理的是客户端(也就是用户),用户的请求到达nginx后,nginx会代理用户的请求向实际的后端服务发起请求,并将结果返回给用户。

nginx作grpc的反向代理踩坑总结

(图片来自维基百科)

正向代理和反向代理实际上是站在用户的角度来定义的,正向也就是代理用户所要请求的服务,而反向则是代理用户向服务发起请求。两者一个很重要的区别:

正向代理服务方不感知请求方,反向代理请求方不感知服务方。
思考一下上面的例子,你通过代理访问google时,google只能感知到请求来自代理服务器,而无法直接感知到你(当然通过cookie等手段也可以追踪到);而通过nginx反向代理时,你是不感知请求具体被转发到哪个后端服务器上的。

nginx最常被用于反向代理的场景就是我们所熟知的http协议,通过配置nginx.conf文件可以很简单地定义一个反向代理规则:

worker_processes  1;

events {
    worker_connections  1024;
}

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

    server {
        listen       80;
        server_name  localhost;

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

nginx从1.13.10以后就支持gRPC协议的反向代理,配置类似:

worker_processes  1;

events {
    worker_connections  1024;
}

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

    server {
        listen       81 http2;
        server_name  localhost;

        
        location / {
            grpc_pass http://ip;
        }
    }
}

但是当需求场景更加复杂的时候,就发现nginx的gRPC模块实际上有很多坑,实现的能力不如http完整,当套用http的解决方案时就会出现问题

场景

最开始我们的场景很简单,通过gRPC协议实现一个简单的C/S架构:

nginx作grpc的反向代理踩坑总结

但这种单纯的直连有些场景下是不可行的,例如client和server在两个网络环境下,彼此不相连通,那就无法通过简单的gRPC连接访问服务。一种解决办法是通过中间的代理服务器转发,用上面说的nginx反向代理gRPC方法:

nginx作grpc的反向代理踩坑总结

nginx proxy部署在两个环境都能访问的集群上,这样就实现了跨网络环境的gRPC访问。随之而来的问题是如何配置这个路由规则?注意我们最开始的gRPC的目标节点都是清晰的,也就是server1和server2的ip地址,当中间加了一层nginx proxy后,client发起的gRPC请求的对象都是nginx proxy的ip地址。那client与nginx建立连接后,nginx如何知道需要将请求转发给server1还是server2呢?(这里server1和server2不是简单的同一个服务的冗备部署,可能需要根据请求的属性决定由谁响应,例如用户id等,因此不能使用负载均衡随机挑选一个响应请求)

解决办法

如果是http协议,那有很多实现方法:

通过路径区分

请求将server的信息添加在path里,例如:/server1/service/method,然后nginx转发请求的时候还原为原始的请求:

worker_processes  1;

events {
    worker_connections  1024;
}

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

    server {
        listen       80;
        server_name  localhost;

        location ~ ^/server1/ {
            proxy_pass http://domain1/;
        }
        
        location ~ ^/server2/ {
            proxy_pass http://domain2/;
        }
    }
}

注意http://domain/最后的斜杠,如果没有这个斜杠请求的路径会是/server1/service/method,而服务端只能响应/service/method的请求,这样就会报404的错误。

通过请求参数区分

也可以将server1的信息放在请求参数里:

worker_processes  1;

events {
    worker_connections  1024;
}

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

    server {
        listen       80;
        server_name  localhost;

        location /service/method {
            if ($query_string ~ x_server=(.*)) {
                proxy_pass http://$1;
            }
        }
    }
}

但对于gRPC就没这么简单了,首先gRPC不支持URI的写法,nginx转发的请求会保留原来的path,无法在转发的时候修改path,这意味着上述的第一种办法不可行。其次gRPC是基于HTTP 2.0协议的,HTTP2没有queryString这一概念,请求头里有一项:path代表请求的路径,例如/service/method,而这一路径是不能携带请求参数的,也就是:path不能写为/service/method?server=server1。这意味着上述的第二种方法也不可行。

注意到HTTP2中请求头:path是指定请求的路径的,那我们直接修改:path不就行了吗:

worker_processes  1;

events {
    worker_connections  1024;
}

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

    server {
        listen       80 http2;
        server_name  localhost;

        location ~ ^/(.*)/service/.* {
            grpc_set_header :path /service/$2;
            grpc_pass http://$1;
        }
    }
}

但是实际验证表明这种方法也不可行,直接修改:path的请求头会导致服务端报错,一种可能的错误如下:

rpc error: code = Unavailable desc = Bad Gateway: HTTP status code 502; transport: received the unexpected content-type "text/html"

抓包后发现,grpc_set_header并没有覆盖:path的结果,而是新增了一项请求头,相当于请求header里存在两个:path,可能就是因为这个原因导致服务端报了502的错误。

山穷水尽之际想起gRPC的metadata功能,我们可以在client端将server的信息存储在metadata中,然后在nginx路由时根据metadata中server的信息转发给对应的后端服务,这样就实现了我们的需求。对于go语言,设置metadata需要实现PerRPCCredentials接口,然后在发起连接的时候传入这个实现类的实例:

type extraMetadata struct {
    Ip string
}

func (c extraMetadata) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "x-ip": c.Ip,
    }, nil
}

func (c extraMetadata) RequireTransportSecurity() bool {
    return false
}

func main(){
    ...
    // nginxProxy是nginx proxy的ip或域名地址
    var nginxProxy string
    // serverIp是根据请求属性计算好的后端服务的ip
    var serverIp string
    con, err := grpc.Dial(nginxProxy, grpc.WithInsecure(),
        grpc.WithPerRPCCredentials(extraMetadata{Ip: serverIp}))
}

然后在nginx配置里根据这个metadata转发到对应的server:

worker_processes  1;

events {
    worker_connections  1024;
}

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

    server {
        listen       80 http2;
        server_name  localhost;

        location ~ ^/service/.* {
            grpc_pass grpc://$http_x_ip:8200;
        }
    }
}

注意这里使用了$http_x_ip这一语法引用了我们传递的x-ip这个metadata信息。这一方法验证有效,client可以通过nginx proxy成功访问到server的gRPC服务。

总结

nginx的gRPC模块的文档太少了,官方文档只给出了几个指令的用途,并没有说明metadata这一方法,网上的文档也鲜有涉及,导致花了两三天的时间在排查。将整个过程总结在这里,希望能帮助到遇到同一问题的人。

到此这篇关于nginx作grpc的反向代理踩坑总结的文章就介绍到这了,更多相关nginx grpc反向代理内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Servers 相关文章推荐
Nginx反向代理多个服务器的实现方法
Mar 31 Servers
Nginx开启Brotli压缩算法实现过程详解
Mar 31 Servers
nginx proxy_cache 缓存配置详解
Mar 31 Servers
nginx location优先级的深入讲解
Mar 31 Servers
Nginx下配置Https证书详细过程
Apr 01 Servers
Nginx源码编译安装过程记录
Nov 17 Servers
Linux安装apache服务器的配置过程
Nov 27 Servers
阿里云 Windows server 2019 配置FTP
Apr 28 Servers
解决IIS7下无法绑定https主机的问题
Apr 29 Servers
配置nginx负载均衡
May 06 Servers
ubuntu下常用apt命令介绍
Jun 05 Servers
apache虚拟主机配置的三种方式(小结)
Jul 23 Servers
使用 Apache Superset 可视化 ClickHouse 数据的两种方法
使用nginx配置访问wgcloud的方法
Jun 26 #Servers
Nginx反向代理配置的全过程记录
制作能在nginx和IIS中使用的ssl证书
解析在浏览器地址栏输入一个URL后发生了什么
Linux中Nginx的防盗链和优化的实现代码
详解nginx进程锁的实现
Jun 14 #Servers
You might like
PHP对象递归引用造成内存泄漏分析
2014/08/28 PHP
php设计模式之简单工厂模式详解
2014/09/04 PHP
thinkphp3.2.3 分页代码分享
2016/07/28 PHP
php求数组全排列,元素所有组合的方法总结
2017/03/14 PHP
[原创]来自ImageSee官方 JavaScript图片浏览器
2008/01/16 Javascript
JS获取键盘上任意按键的值(实例代码)
2013/11/12 Javascript
基于javascript实现仿百度输入框自动匹配功能
2016/01/03 Javascript
JavaScript事件详细讲解
2016/06/27 Javascript
ionic由于使用了header和subheader导致被遮挡的问题的两种解决方法
2016/09/22 Javascript
KVM虚拟化技术之使用Qemu-kvm创建和管理虚拟机的方法
2016/10/05 Javascript
微信小程序微信支付接入开发实例详解
2017/04/12 Javascript
jQuery实现打开网页自动弹出遮罩层或点击弹出遮罩层功能示例
2017/10/19 jQuery
Vue中render方法的使用详解
2018/01/26 Javascript
vue 2.x 中axios 封装的get 和post方法
2018/02/28 Javascript
jQuery实现日历效果
2020/09/11 jQuery
python使用xmlrpc实例讲解
2013/12/17 Python
Python collections模块实例讲解
2014/04/07 Python
python中list循环语句用法实例
2014/11/10 Python
新手常见6种的python报错及解决方法
2018/03/09 Python
Python查看微信撤回消息代码
2018/06/07 Python
python中import与from方法总结(推荐)
2019/03/21 Python
Django Rest framework频率原理与限制
2019/07/26 Python
django实现web接口 python3模拟Post请求方式
2019/11/19 Python
Python处理PDF与CDF实例
2020/02/26 Python
pycharm 2020 1.1的安装流程
2020/09/29 Python
五种Python转义表示法
2020/11/27 Python
python 自定义异常和主动抛出异常(raise)的操作
2020/12/11 Python
CSS实现圆形放大镜狙击镜效果 只有圆圈里的放大
2012/12/10 HTML / CSS
css3边框_动力节点Java学院整理
2017/07/11 HTML / CSS
prAna官网:瑜伽、旅行和冒险服装
2019/03/10 全球购物
优秀员工自荐书范文
2013/12/08 职场文书
团支部建设方案
2014/05/02 职场文书
公司员工手册范本
2015/05/14 职场文书
微信早安问候语
2015/11/10 职场文书
2016年三八节红领巾广播稿
2015/12/17 职场文书
Mysql中mvcc各场景理解应用
2022/08/05 MySQL