nginx作grpc的反向代理踩坑總結(jié)
背景
眾所周知,nginx是一款高性能的web服務(wù)器,常用于負(fù)載均衡和反向代理。所謂的反向代理是和正向代理相對(duì)應(yīng),正向代理即我們常規(guī)意義上理解的“代理”:例如正常情況下在國(guó)內(nèi)是無(wú)法訪問(wèn)google的,如果我們需要訪問(wèn),就需要通過(guò)一層代理去轉(zhuǎn)發(fā)。這個(gè)正向代理代理的是服務(wù)端(也就是google),而反向代理則相反,代理的是客戶端(也就是用戶),用戶的請(qǐng)求到達(dá)nginx后,nginx會(huì)代理用戶的請(qǐng)求向?qū)嶋H的后端服務(wù)發(fā)起請(qǐng)求,并將結(jié)果返回給用戶。

(圖片來(lái)自維基百科)
正向代理和反向代理實(shí)際上是站在用戶的角度來(lái)定義的,正向也就是代理用戶所要請(qǐng)求的服務(wù),而反向則是代理用戶向服務(wù)發(fā)起請(qǐng)求。兩者一個(gè)很重要的區(qū)別:
正向代理服務(wù)方不感知請(qǐng)求方,反向代理請(qǐng)求方不感知服務(wù)方。
思考一下上面的例子,你通過(guò)代理訪問(wèn)google時(shí),google只能感知到請(qǐng)求來(lái)自代理服務(wù)器,而無(wú)法直接感知到你(當(dāng)然通過(guò)cookie等手段也可以追蹤到);而通過(guò)nginx反向代理時(shí),你是不感知請(qǐng)求具體被轉(zhuǎn)發(fā)到哪個(gè)后端服務(wù)器上的。
nginx最常被用于反向代理的場(chǎng)景就是我們所熟知的http協(xié)議,通過(guò)配置nginx.conf文件可以很簡(jiǎn)單地定義一個(gè)反向代理規(guī)則:
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協(xié)議的反向代理,配置類(lèi)似:
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;
}
}
}
但是當(dāng)需求場(chǎng)景更加復(fù)雜的時(shí)候,就發(fā)現(xiàn)nginx的gRPC模塊實(shí)際上有很多坑,實(shí)現(xiàn)的能力不如http完整,當(dāng)套用http的解決方案時(shí)就會(huì)出現(xiàn)問(wèn)題
場(chǎng)景
最開(kāi)始我們的場(chǎng)景很簡(jiǎn)單,通過(guò)gRPC協(xié)議實(shí)現(xiàn)一個(gè)簡(jiǎn)單的C/S架構(gòu):

但這種單純的直連有些場(chǎng)景下是不可行的,例如client和server在兩個(gè)網(wǎng)絡(luò)環(huán)境下,彼此不相連通,那就無(wú)法通過(guò)簡(jiǎn)單的gRPC連接訪問(wèn)服務(wù)。一種解決辦法是通過(guò)中間的代理服務(wù)器轉(zhuǎn)發(fā),用上面說(shuō)的nginx反向代理gRPC方法:

nginx proxy部署在兩個(gè)環(huán)境都能訪問(wèn)的集群上,這樣就實(shí)現(xiàn)了跨網(wǎng)絡(luò)環(huán)境的gRPC訪問(wèn)。隨之而來(lái)的問(wèn)題是如何配置這個(gè)路由規(guī)則?注意我們最開(kāi)始的gRPC的目標(biāo)節(jié)點(diǎn)都是清晰的,也就是server1和server2的ip地址,當(dāng)中間加了一層nginx proxy后,client發(fā)起的gRPC請(qǐng)求的對(duì)象都是nginx proxy的ip地址。那client與nginx建立連接后,nginx如何知道需要將請(qǐng)求轉(zhuǎn)發(fā)給server1還是server2呢?(這里server1和server2不是簡(jiǎn)單的同一個(gè)服務(wù)的冗備部署,可能需要根據(jù)請(qǐng)求的屬性決定由誰(shuí)響應(yīng),例如用戶id等,因此不能使用負(fù)載均衡隨機(jī)挑選一個(gè)響應(yīng)請(qǐng)求)
解決辦法
如果是http協(xié)議,那有很多實(shí)現(xiàn)方法:
通過(guò)路徑區(qū)分
請(qǐng)求將server的信息添加在path里,例如:/server1/service/method,然后nginx轉(zhuǎn)發(fā)請(qǐng)求的時(shí)候還原為原始的請(qǐng)求:
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/最后的斜杠,如果沒(méi)有這個(gè)斜杠請(qǐng)求的路徑會(huì)是/server1/service/method,而服務(wù)端只能響應(yīng)/service/method的請(qǐng)求,這樣就會(huì)報(bào)404的錯(cuò)誤。
通過(guò)請(qǐng)求參數(shù)區(qū)分
也可以將server1的信息放在請(qǐng)求參數(shù)里:
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;
}
}
}
}
但對(duì)于gRPC就沒(méi)這么簡(jiǎn)單了,首先gRPC不支持URI的寫(xiě)法,nginx轉(zhuǎn)發(fā)的請(qǐng)求會(huì)保留原來(lái)的path,無(wú)法在轉(zhuǎn)發(fā)的時(shí)候修改path,這意味著上述的第一種辦法不可行。其次gRPC是基于HTTP 2.0協(xié)議的,HTTP2沒(méi)有queryString這一概念,請(qǐng)求頭里有一項(xiàng):path代表請(qǐng)求的路徑,例如/service/method,而這一路徑是不能攜帶請(qǐng)求參數(shù)的,也就是:path不能寫(xiě)為/service/method?server=server1。這意味著上述的第二種方法也不可行。
注意到HTTP2中請(qǐng)求頭:path是指定請(qǐng)求的路徑的,那我們直接修改: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;
}
}
}
但是實(shí)際驗(yàn)證表明這種方法也不可行,直接修改:path的請(qǐng)求頭會(huì)導(dǎo)致服務(wù)端報(bào)錯(cuò),一種可能的錯(cuò)誤如下:
rpc error: code = Unavailable desc = Bad Gateway: HTTP status code 502; transport: received the unexpected content-type "text/html"
抓包后發(fā)現(xiàn),grpc_set_header并沒(méi)有覆蓋:path的結(jié)果,而是新增了一項(xiàng)請(qǐng)求頭,相當(dāng)于請(qǐng)求header里存在兩個(gè):path,可能就是因?yàn)檫@個(gè)原因?qū)е路?wù)端報(bào)了502的錯(cuò)誤。
山窮水盡之際想起gRPC的metadata功能,我們可以在client端將server的信息存儲(chǔ)在metadata中,然后在nginx路由時(shí)根據(jù)metadata中server的信息轉(zhuǎn)發(fā)給對(duì)應(yīng)的后端服務(wù),這樣就實(shí)現(xiàn)了我們的需求。對(duì)于go語(yǔ)言,設(shè)置metadata需要實(shí)現(xiàn)PerRPCCredentials接口,然后在發(fā)起連接的時(shí)候傳入這個(gè)實(shí)現(xiàn)類(lèi)的實(shí)例:
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是根據(jù)請(qǐng)求屬性計(jì)算好的后端服務(wù)的ip
var serverIp string
con, err := grpc.Dial(nginxProxy, grpc.WithInsecure(),
grpc.WithPerRPCCredentials(extraMetadata{Ip: serverIp}))
}
然后在nginx配置里根據(jù)這個(gè)metadata轉(zhuǎn)發(fā)到對(duì)應(yīng)的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這一語(yǔ)法引用了我們傳遞的x-ip這個(gè)metadata信息。這一方法驗(yàn)證有效,client可以通過(guò)nginx proxy成功訪問(wèn)到server的gRPC服務(wù)。
總結(jié)
nginx的gRPC模塊的文檔太少了,官方文檔只給出了幾個(gè)指令的用途,并沒(méi)有說(shuō)明metadata這一方法,網(wǎng)上的文檔也鮮有涉及,導(dǎo)致花了兩三天的時(shí)間在排查。將整個(gè)過(guò)程總結(jié)在這里,希望能幫助到遇到同一問(wèn)題的人。
到此這篇關(guān)于nginx作grpc的反向代理踩坑總結(jié)的文章就介紹到這了,更多相關(guān)nginx grpc反向代理內(nèi)容請(qǐng)搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!
版權(quán)聲明:本站文章來(lái)源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來(lái)源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非maisonbaluchon.cn所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來(lái)源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來(lái),僅供學(xué)習(xí)參考,不代表本站立場(chǎng),如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。
關(guān)注官方微信