本文记录应用nginx作gRPC的反向代理踩的坑和解决办法。
背景
家喻户晓,nginx是一款高性能的web服务器,罕用于负载平衡和反向代理。所谓的反向代理是和正向代理绝对应,正向代理即咱们惯例意义上了解的“代理”:例如失常状况下在国内是无法访问google的,如果咱们须要拜访,就须要通过一层代理去转发。这个正向代理代理的是服务端(也就是google),而反向代理则相同,代理的是客户端(也就是用户),用户的申请达到nginx后,nginx会代理用户的申请向理论的后端服务发动申请,并将后果返回给用户。
(图片来自维基百科)
正向代理和反向代理实际上是站在用户的角度来定义的,正向也就是代理用户所要申请的服务,而反向则是代理用户向服务发动申请。两者一个很重要的区别:
正向代理服务方不感知申请方,反向代理申请方不感知服务方。
思考一下下面的例子,你通过代理拜访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架构:
但这种单纯的直连有些场景下是不可行的,例如client和server在两个网络环境下,彼此不相连通,那就无奈通过简略的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这一办法,网上的文档也鲜有波及,导致花了两三天的工夫在排查。将整个过程总结在这里,心愿能帮忙到遇到同一问题的人。