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