問題描述
如圖所示,在 Swarm 集群中部署了 ServiceA
和 ServiceB
這兩個(gè)服務(wù)礼华,服務(wù)間通過 grpc 建立長連接實(shí)現(xiàn)服務(wù)間調(diào)用。然而 ServiceA
在調(diào)用 ServiceB
時(shí)拗秘,偶爾會(huì)出現(xiàn)如下錯(cuò)誤:
java.io.IOException: Connection reset by peer
at sun.nio.ch.FileDispatcherImpl.read0(Native Method)
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
at sun.nio.ch.IOUtil.read(IOUtil.java:192)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380)
at io.netty.buffer.PooledUnsafeDirectByteBuf.setBytes(PooledUnsafeDirectByteBuf.java:288)
at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1100)
at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:367)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:118)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:642)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:565)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:479)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:441)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:144)
at java.lang.Thread.run(Thread.java:745)
在我們查看容器日志時(shí)圣絮,這個(gè)錯(cuò)誤出現(xiàn)次數(shù)不是很頻繁,但是一定會(huì)出現(xiàn)雕旨,由于這個(gè)錯(cuò)誤會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)異常扮匠,所以我們花了點(diǎn)時(shí)間去處理它。
問題排查
1凡涩、 grpc 中間件的問題棒搜?
并發(fā)測試:50 個(gè)線程,10萬次請求活箕,重復(fù)了 3 次力麸,均能正常響應(yīng)。因此,排除這種可能性克蚂。
2闺鲸、測試環(huán)境網(wǎng)絡(luò)波動(dòng)導(dǎo)致的?
持續(xù)請求測試:多線程持續(xù)請求 4 小時(shí)埃叭,均能正常響應(yīng)摸恍。然而另外一套測試環(huán)境,測試妹子人工測試的時(shí)候還是出現(xiàn)這個(gè)問題赤屋。因此立镶,排除這種可能性。
3益缎、搜索 Connection reset by peer
相關(guān)信息
網(wǎng)上很多文章都說明了這個(gè)異趁栈牛可能出現(xiàn)的原因,經(jīng)過各種 DEBUG莺奔,發(fā)現(xiàn)這個(gè)異常發(fā)生時(shí)欣范,ServiceA
沒有將數(shù)據(jù)發(fā)送到 ServiceB
。結(jié)合上述 1 和 2 兩步的測試令哟,長連接一直維持時(shí)無異常恼琼;人工測試時(shí),中途會(huì)停止請求屏富,時(shí)間過長晴竞,長連接會(huì)斷開,ServiceA
無法將數(shù)據(jù)發(fā)送給 ServiceB
狠半,就能解釋通了噩死。
4、分析 Docker Swarm 中的網(wǎng)絡(luò)模型
Docker Swarm 中使用 IPVS 將 ServiceA
的請求路由到 ServiceB
的一個(gè)實(shí)例神年,ServiceA
與 ServiceB
長連接的建立會(huì)經(jīng)過 IPVS已维。此處 IPVS 的規(guī)則是:當(dāng) TCP 會(huì)話空閑超過15分鐘(900秒)時(shí),IPVS 連接超時(shí)并從連接表中清除已日,即圖中 IPVS 與 ServiceB
之間的連接垛耳。
下面是兩種不同的 timeout ,一種是 IPVS 的飘千,另一種是 TCP 的:
默認(rèn) IPVS timeout 值:
ipvsadm -l --timeout
Timeout (tcp tcpfin udp): **900** 120 300
默認(rèn) TCP timeout 值:
-
tcp_keepalive_time = **7200**
(秒堂鲜,連接時(shí)長) -
tcp_keepalive_intvl = 75
(秒,探測時(shí)間間隔) -
tcp_keepalive_probes = 9
(次护奈,探測頻率)
當(dāng) IPVS 超時(shí), 它將從連接表中清除缔莲。而 IPVS 超時(shí)后,時(shí)間在 7200s 之內(nèi)霉旗,ServiceA
還是會(huì)認(rèn)為長連接處于連接狀態(tài)痴奏,實(shí)則不然磺箕,繼續(xù)調(diào)用 ServiceB
則會(huì)出現(xiàn)問題。
5抛虫、精準(zhǔn)復(fù)現(xiàn)問題
ServiceA
調(diào)用 ServiceB
松靡,正常響應(yīng),等待 15 分鐘以上建椰,ServiceA
繼續(xù)調(diào)用 ServiceB
雕欺,一定出現(xiàn) Connection reset by peer
的異常。
問題解決
方式一:ServiceA
在代碼層面實(shí)現(xiàn)連接重試邏輯
方式二:系統(tǒng)層面設(shè)置 TCP 的 timeout
設(shè)置 tcp_keepalive_time
小于 900s 棉姐,建議 600 ~ 800
sysctl -w net.ipv4.tcp_keepalive_time=600
sysctl -w net.ipv4.tcp_keepalive_intvl=30
sysctl -w net.ipv4.tcp_keepalive_probes=10
或者編輯文件 /etc/sysctl.conf
屠列,添加如下內(nèi)容:
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 10
為了使配置生效,必須重啟 Swarm 中的服務(wù)伞矩。建議同時(shí)應(yīng)用上述的兩種方法笛洛。