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 proxy_cache 缓存配置详解
Mar 31 Servers
nginx location优先级的深入讲解
Mar 31 Servers
详解Apache SkyWalking 告警配置指南
Apr 22 Servers
学习nginx基础知识
Sep 04 Servers
教你利用Nginx 服务搭建子域环境提升二维地图加载性能的步骤
Sep 25 Servers
Kubernetes控制节点的部署
Apr 01 Servers
Windows Server 2012 R2 磁盘分区教程
Apr 29 Servers
使用Nginx的访问日志统计PV与UV
May 06 Servers
Nginx HTTP跳转至HTTPS
May 15 Servers
Nginx利用Logrotate实现日志分割
May 20 Servers
nginx 配置指令之location使用详解
May 25 Servers
Windows10安装Apache2.4的方法步骤
Jun 25 Servers
使用 Apache Superset 可视化 ClickHouse 数据的两种方法
使用nginx配置访问wgcloud的方法
Jun 26 #Servers
Nginx反向代理配置的全过程记录
制作能在nginx和IIS中使用的ssl证书
解析在浏览器地址栏输入一个URL后发生了什么
Linux中Nginx的防盗链和优化的实现代码
详解nginx进程锁的实现
Jun 14 #Servers
You might like
当年上海收录机产品生产,进口和价格情况
2021/03/04 无线电
php 异常处理实现代码
2009/03/10 PHP
php中取得URL的根域名的代码
2011/03/23 PHP
php中session与cookie的比较
2015/01/27 PHP
Codeigniter实现发送带附件的邮件
2015/03/19 PHP
Smarty高级应用之缓存操作技巧分析
2016/05/14 PHP
javascript 简单高效判断数据类型 系列函数 By shawl.qiu
2007/03/06 Javascript
javascript错误的认识不用关心内存管理
2012/12/15 Javascript
Jquery 在页面加载后执行的几种方式
2014/03/14 Javascript
jquery序列化表单去除指定元素示例代码
2014/04/10 Javascript
jquery实现多条件筛选特效代码分享
2015/08/28 Javascript
js传值后台中文出现乱码的解决方法
2016/06/30 Javascript
jstl中判断list中是否包含某个值的简单方法
2016/10/14 Javascript
javascript跨域请求包装函数与用法示例
2016/11/03 Javascript
js分页之前端代码实现和请求处理
2017/08/04 Javascript
弱类型语言javascript开发中的一些坑实例小结【变量、函数、数组、对象、作用域等】
2019/08/07 Javascript
LayUI动态设置checkbox不显示的解决方法
2019/09/02 Javascript
vue 点击其他区域关闭自定义div操作
2020/07/17 Javascript
Kettle中使用JavaScrip调用jar包对文件内容进行MD5加密的操作方法
2020/09/04 Javascript
用Python制作检测Linux运行信息的工具的教程
2015/04/01 Python
Python通过matplotlib绘制动画简单实例
2017/12/13 Python
Centos 升级到python3后pip 无法使用的解决方法
2018/06/12 Python
使用django-guardian实现django-admin的行级权限控制的方法
2018/10/30 Python
学习python可以干什么
2019/02/26 Python
python pip安装包出现:Failed building wheel for xxx错误的解决
2019/12/25 Python
浅谈matplotlib.pyplot与axes的关系
2020/03/06 Python
Django ModelForm组件原理及用法详解
2020/10/12 Python
CSS3利用text-shadow属性实现多种效果的文字样式展现方法
2016/08/25 HTML / CSS
彩色的非洲教学反思
2014/02/18 职场文书
入职担保书怎么写
2014/05/12 职场文书
篝火晚会策划方案
2014/05/16 职场文书
住宿生擅自离校检讨书
2014/09/22 职场文书
公安个人四风问题对照检查及整改措施
2014/10/28 职场文书
公务员年度考核个人总结
2015/02/12 职场文书
Python requests用法和django后台处理详解
2022/03/19 Python
如何设置多台电脑共享打印机?多台电脑共享打印机的方法
2022/04/08 数码科技