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性能的一些建议
Mar 31 Servers
nginx配置虚拟主机的详细步骤
Jul 21 Servers
Nginx内网单机反向代理的实现
Nov 07 Servers
CentOS7和8下安装Maven3.8.4
Apr 07 Servers
Tomcat项目启动失败的原因和解决办法
Apr 20 Servers
Windows Server 2012 修改远程默认端口3389的方法
Apr 28 Servers
在Windows Server 2012上安装 .NET Framework 3.5 所遇到的问题
Apr 29 Servers
nginx实现多geoserver服务的负载均衡
May 15 Servers
nginx访问报403错误的几种情况详解
Jul 23 Servers
Windows server 2016服务器基本设置
Aug 14 Servers
Fluentd搭建日志收集服务
Sep 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中ob(Output Buffer 输出缓冲)函数使用方法
2007/07/21 PHP
PHP之变量、常量学习笔记
2008/03/27 PHP
IIS php环境配置PHP5 MySQL5 ZendOptimizer phpmyadmin安装与配置
2008/11/18 PHP
php _autoload自动加载类与机制分析
2012/02/10 PHP
简单介绍win7下搭建apache+php+mysql开发环境
2015/08/06 PHP
php curl常用的5个经典例子
2017/01/20 PHP
PHP架构及原理知识点详解
2019/12/22 PHP
Prototype使用指南之hash.js
2007/01/10 Javascript
Jquery数独游戏解析(一)-页面布局
2010/11/05 Javascript
基于jquery的3d效果实现代码
2011/03/23 Javascript
Nodejs中调用系统命令、Shell脚本和Python脚本的方法和实例
2015/01/01 NodeJs
JavaScript 拖拽实例代码
2016/09/21 Javascript
如何开发出更好的JavaScript模块
2017/12/22 Javascript
基于webpack4搭建的react项目框架的方法
2018/06/30 Javascript
浅谈Vue.use的使用
2018/08/29 Javascript
JS 验证码功能的三种实现方式
2018/11/26 Javascript
微信小程序websocket实现即时聊天功能
2019/05/21 Javascript
微信小程序request请求封装,验签代码实例
2019/12/04 Javascript
[43:48]Ti4正赛第一天 VG vs NEWBEE 2
2014/07/19 DOTA
使用NumPy和pandas对CSV文件进行写操作的实例
2018/06/14 Python
python 统计一个列表当中的每一个元素出现了多少次的方法
2018/11/14 Python
关于python之字典的嵌套,递归调用方法
2019/01/21 Python
Python实现根据日期获取当天凌晨时间戳的方法示例
2019/04/09 Python
对PyQt5的输入对话框使用(QInputDialog)详解
2019/06/25 Python
python3中rank函数的用法
2019/11/27 Python
html5仿支付宝密码框的实现代码
2017/09/06 HTML / CSS
世界上最大的餐具公司:Oneida
2016/12/17 全球购物
争论的故事教学反思
2014/02/06 职场文书
士力架广告词
2014/03/20 职场文书
党员公开承诺书范文
2014/03/25 职场文书
大二学习计划书范文
2014/04/27 职场文书
2014年化验员工作总结
2014/11/18 职场文书
教师业务学习材料
2014/12/16 职场文书
家长对学校的意见和建议
2015/06/03 职场文书
情感电台广播稿
2015/08/18 职场文书
Python通用验证码识别OCR库ddddocr的安装使用教程
2022/07/07 Python