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如何将http访问的网站改成https访问
Mar 31 Servers
详解Nginx 工作原理
Mar 31 Servers
提升Nginx性能的一些建议
Mar 31 Servers
配置nginx 重定向到系统维护页面
Jun 08 Servers
Nginx图片服务器配置之后图片访问404的问题解决
Mar 21 Servers
nginx常用配置conf的示例代码详解
Mar 21 Servers
Kubernetes关键组件与结构组成介绍
Mar 31 Servers
iSCSI服务器CHAP双向认证配置
Apr 01 Servers
详解Nginx的超时keeplive_timeout配置步骤
May 25 Servers
Windows Server 2008配置防火墙策略详解
Jun 28 Servers
nginx代理实现静态资源访问的示例代码
Jul 07 Servers
Flink 侧流输出源码示例解析
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分页效率终结版(推荐)
2013/07/01 PHP
php生成扇形比例图实例
2013/11/06 PHP
ThinkPHP在新浪SAE平台的部署实例
2014/10/31 PHP
PHP验证信用卡卡号是否正确函数
2015/05/27 PHP
php简单实现多语言切换的方法
2016/05/09 PHP
Laravel学习基础之migrate的使用教程
2017/10/11 PHP
使用swoole 定时器变更超时未支付订单状态的解决方案
2019/07/24 PHP
php5.3/5.4/5.5/5.6/7常见新增特性汇总整理
2020/02/27 PHP
运用jquery实现table单双行不同显示并能单行选中
2009/07/25 Javascript
Extjs4 消息框去掉关闭按钮(类似Ext.Msg.alert)
2013/04/02 Javascript
使用js解决由border属性引起的div宽度问题
2013/11/26 Javascript
js中for in的用法示例解析
2013/12/25 Javascript
sliderToggle在写jquery的计时器setTimeouter中不生效
2014/05/26 Javascript
js鼠标悬浮出现遮罩层的方法
2015/01/28 Javascript
Javascript操作表单实例讲解(下)
2016/06/20 Javascript
Bootstrap Modal遮罩弹出层代码分享
2016/11/21 Javascript
NodeJS处理Express中异步错误
2017/03/26 NodeJs
关于Vue Webpack2单元测试示例详解
2017/08/14 Javascript
javascript 通过键名获取键盘的keyCode方法
2017/12/31 Javascript
Python网络爬虫项目:内容提取器的定义
2016/10/25 Python
python实现单向链表详解
2018/02/08 Python
python检测文件夹变化,并拷贝有更新的文件到对应目录的方法
2018/10/17 Python
Python线程池模块ThreadPoolExecutor用法分析
2018/12/28 Python
Django学习之文件上传与下载
2019/10/06 Python
python绘制规则网络图形实例
2019/12/09 Python
python函数定义和调用过程详解
2020/02/09 Python
html5 乒乓球(碰撞检测)实例二
2013/07/25 HTML / CSS
探讨HTML5移动开发的几大特性(必看)
2015/12/30 HTML / CSS
实习生自荐信范文
2013/11/13 职场文书
西式结婚主持词
2014/03/14 职场文书
一年级学生评语大全
2014/04/21 职场文书
无刑事犯罪记录证明范本
2014/09/29 职场文书
国庆放假通知怎么写
2015/07/30 职场文书
篮球拉拉队口号
2015/12/25 职场文书
2016年感恩母亲节活动总结
2016/04/01 职场文书
OpenCV-Python实现人脸美白算法的实例
2021/06/11 Python