聊聊 Docker Swarm 部署 gRPC 服务的坑
gRPC?是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計,也是目前流行的微服務架構中比較突出的跨語言 RPC 框架。
一直以來,我們的微服務都是基于 gRPC 來開發(fā),使用的語言有?.NET、JAVA、Node.js,整體還比較穩(wěn)定,當然整個過程中踩過的坑也不少,今天主要介紹 gRPC 服務使用 Docker Swarm 部署遇到的問題。
問題描述
服務端空閑(沒有接受到任何請求)一段時間后(不到 20 分鐘),客戶端?第一次?向服務端發(fā)請求會失敗,重新請求則成功,具體錯誤日志如下,提示 gRPC 服務端將連接重置:
| 1 2 3 4 5 6 7 | Grpc.Core.RpcException: Status(StatusCode=Unavailable, Detail="Connection reset by peer") at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Grpc.Core.Internal.AsyncCall`2.UnaryCall(TRequest msg) at Grpc.Core.DefaultCallInvoker.BlockingUnaryCall[TRequest,TResponse](Method`2 method, String host, CallOptions options, TRequest request) at Grpc.Core.Interceptors.InterceptingCallInvoker.<BlockingUnaryCall>b__3_0[TRequest,TResponse](TRequest req, ClientInterceptorContext`2 ctx) at Grpc.Core.ClientBase.ClientBaseConfiguration.ClientBaseConfigurationInterceptor.BlockingUnaryCall[TRequest,TResponse](TRequest request, ClientInterceptorContext`2 context, BlockingUnaryCallContinuation`2 continuation) |
解決方案
方案1:重試機制
最初通過查看官方文檔對?StatusCode=Unavailable?的解釋,發(fā)現(xiàn)當前遇到的問題確實可以使用重試機制來處理,所以在客戶端對 gRPC 服務的調(diào)用全部添加了重試策略。
雖然當時確實解決了問題,但也一直懷疑我們在使用方式上肯定有問題,畢竟 gRPC 在很多開源項目中都被驗證過,理論上肯定不是這么處理問題的,所以并不推薦這么玩。
方案2:調(diào)整 TCP keepalive
在進行日志分析時,發(fā)現(xiàn)生產(chǎn)環(huán)境并沒有此類型錯誤日志,所以問題基本和代碼本身沒什么關系,猜測是環(huán)境上的原因,而本地開發(fā)環(huán)境和生產(chǎn)環(huán)境的最大區(qū)別是:開發(fā)環(huán)境的服務通過 Docker Swarm 進行部署,線上環(huán)境則是使用 k8s?。所以嘗試從 Docker Swarm 上進行問題定位,最終找到相關資料?gRPC streaming keepAlive doesn’t work with docker swarm?(雖然 issue 聊的是?grpc-go?,但其實和語言無關) 和?IPVS connection timeout issue?,問題和我們遇到的基本一致。
經(jīng)過多次測試驗證確定出問題的原因是當通過 Docker Swarm 部署 (基于 overlay 網(wǎng)絡) gRPC 服務(基于 TCP),客戶端調(diào)用服務端會經(jīng)過?IPVS?處理,IPVS?簡單來說就是傳輸級的負載均衡器,可以將基于 TCP 和 UDP 的服務請求轉(zhuǎn)發(fā)到真實服務。gRPC 服務啟動時,IPVS?中會將此 TCP 連接記錄到連接跟蹤表,但為了保持連接跟蹤表干凈,900s(默認的 timeout,不支持調(diào)整)內(nèi)空閑的連接會被清理 ,IPVS?更多介紹
| 1 2 | [root@node1]~# ipvsadm -l --timeout Timeout (tcp tcpfin udp): 900 120 300 |
所以當客戶端發(fā)請求時,如果?IPVS?的連接跟蹤表中不存在對應連接,則會返回?Connection reset by peer?,重置后第二次請求就正常了。
所以解決方式就是使?IPVS?的連接跟蹤表一直有該服務的連接狀態(tài),在 Linux 的內(nèi)核參數(shù)中,有 TCP 的 keepalive 默認設置,時間是 7200s,我們只需要將其改成小于 900s,這樣不到 900s 就發(fā)起探測,使連接狀態(tài)一直保持。因為如果使用默認的 7200s 探測一次,IPVS?的連接跟蹤表中此服務可能在 900s 的時候就已經(jīng)被清理,所以在 901s~7200s 這個區(qū)間內(nèi)有客戶端請求進來就會出錯。
| 1 2 3 4 | [root@node1]~# sysctl -a | grep keepalive net.ipv4.tcp_keepalive_time = 7200 # 表示當 keepalive 啟用的時候,TCP 發(fā)送 keepalive 消息的頻度,缺省是2小時 net.ipv4.tcp_keepalive_probes = 9 # 如果對方不予應答,探測包的發(fā)送次數(shù) net.ipv4.tcp_keepalive_intvl = 75 # keepalive 探測包的發(fā)送間隔 |
修改可通過編輯?/etc/sysctl.conf?文件,調(diào)整后需?重啟 gRPC 服務?:
| 1 2 3 | net.ipv4.tcp_keepalive_time = 800 #800s 沒必要太小,其他兩個參數(shù)也可相應做些調(diào)整 net.ipv4.tcp_keepalive_probes = 3 net.ipv4.tcp_keepalive_intvl = 15 |
如果不希望修改內(nèi)核參數(shù),也可以在 gRPC 服務代碼中通過修改?grpc.keepalive_time_ms,參考:Keepalive User Guide for gRPC Core?和?Grpc_arg_keys,服務端默認?grpc.keepalive_time_ms?也是 7200s,和內(nèi)核參數(shù)一樣,以下是 .NET 代碼例子(其他語言類似):
| 1 2 3 4 5 6 7 8 9 10 | var server = new Server(new List<ChannelOption> { new ChannelOption("grpc.keepalive_time_ms", 800000), // 發(fā)送 keepalive 探測消息的頻度 new ChannelOption("grpc.keepalive_timeout_ms", 5000), // keepalive 探測應答超時時間 new ChannelOption("grpc.keepalive_permit_without_calls", 1) // 是否允許在沒有任何調(diào)用時發(fā)送 keepalive }) { Services = { ServiceA }, Ports = { new ServerPort(host, port, ServerCredentials.Insecure) }, }; |
再回頭看看為什么生產(chǎn)環(huán)境的 k8s 沒有這個問題,首先?kube-proxy?是支持?IPTABLES?和?IPVS?兩種模式的,但目前我們使用的是?IPTABLES,當然還有很多區(qū)別,不過涉及更多運維層面的介紹我就不吹逼了,畢竟不在掌握范圍內(nèi) 。
參考鏈接
gRPC streaming keepAlive doesn’t work with docker swarm
IPVS
IPVS connection timeout issue
Keepalive User Guide for gRPC Core
Grpc_arg_keys
總結(jié)
以上是生活随笔為你收集整理的聊聊 Docker Swarm 部署 gRPC 服务的坑的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在微软工作一年,我学会了什么
- 下一篇: 动手造轮子:实现一个简单的依赖注入(零)