Java后端服務(wù)間歇性響應(yīng)慢的問(wèn)題排查與解決
分享一個(gè)之前在公司內(nèi)其它團(tuán)隊(duì)找到幫忙排查的一個(gè)后端服務(wù)連接超時(shí)問(wèn)題,問(wèn)題的表現(xiàn)是服務(wù)部署到線上后出現(xiàn)間歇性請(qǐng)求響應(yīng)非常慢(大于10s),但是后端業(yè)務(wù)分析業(yè)務(wù)日志時(shí)卻沒(méi)有發(fā)現(xiàn)慢請(qǐng)求,另外由于服務(wù)容器livenessProbe
也出現(xiàn)超時(shí),導(dǎo)致容器出現(xiàn)間歇性重啟。
復(fù)現(xiàn)
該服務(wù)基于spring-boot開(kāi)發(fā),通過(guò)spring-mvc框架對(duì)外提供一些web接口,業(yè)務(wù)簡(jiǎn)化后代碼如下:
@Controller @SpringBootApplication public class Bootstrap { public static void main(String[] args) { SpringApplication.run(Bootstrap.class, args); } @GetMapping("/ping") public String ping() { return "pong"; } }
客戶(hù)端訪問(wèn)該服務(wù)(記為backend)的路徑為: client => ingress => backend
,客戶(hù)端的代碼簡(jiǎn)化如下,其實(shí)就是在一個(gè)循環(huán)里面持續(xù)訪問(wèn)ingress(這里以一個(gè)nginx代替):
import time import requests while True: try: start = time.time() r = requests.get('http://nginx/ping', timeout=(3, 10)) spend = int((time.time() - start) * 1000) r.raise_for_status() print(f'{time.strftime("%Y-%m-%dT%H:%M:%S")} OK {spend}ms {r.content.decode("utf-8")}') except requests.HTTPError as err: print(f'{time.strftime("%Y-%m-%dT%H:%M:%S")} HTTP error: {err}') except Exception as err: print(f'{time.strftime("%Y-%m-%dT%H:%M:%S")} Error: {err}') time.sleep(0.1)
下面是一個(gè)docker-compose
文件構(gòu)造了一個(gè)最小可復(fù)現(xiàn)的環(huán)境:
version: '3' services: backend: image: shawyeok/128-slowbackend:backend nginx: image: shawyeok/128-slowbackend:nginx depends_on: - backend client: image: shawyeok/128-slowbackend:client depends_on: - nginx
通過(guò)docker-compose
啟動(dòng)后,檢查client容器的日志,你將會(huì)在client看到間歇性出現(xiàn)read timeout
的記錄
$ docker-compose up -d $ docker ps $ docker logs -f xxx-client-1 2024-05-23T08:02:51 OK 52ms pong 2024-05-23T08:02:51 OK 6ms pong 2024-05-23T08:02:51 OK 3ms pong 2024-05-23T08:02:51 OK 5ms pong 2024-05-23T08:02:51 OK 17ms pong 2024-05-23T08:02:51 OK 14ms pong 2024-05-23T08:02:51 OK 11ms pong 2024-05-23T08:02:51 OK 16ms pong 2024-05-23T08:02:52 OK 7ms pong 2024-05-23T08:02:52 OK 10ms pong 2024-05-23T08:02:52 OK 6ms pong 2024-05-23T08:02:52 OK 8ms pong 2024-05-23T08:03:02 Error: HTTPConnectionPool(host='nginx', port=80): Read timed out. (read timeout=10) 2024-05-23T08:03:12 Error: HTTPConnectionPool(host='nginx', port=80): Read timed out. (read timeout=10) 2024-05-23T08:03:12 OK 15ms pong 2024-05-23T08:03:12 OK 15ms pong 2024-05-23T08:03:12 OK 15ms pong
完整的復(fù)現(xiàn)代碼在Shawyeok/128-slowbackend,讀者看到這里可以先嘗試通過(guò)上面步驟把環(huán)境運(yùn)行起來(lái)自己動(dòng)手分析一下原因。
分析
今天終于抽出時(shí)間來(lái)完成這篇文章,讀者在看下面分析過(guò)程之前,我建議還是先動(dòng)手用docker-compose把案例復(fù)現(xiàn)一下,然后自己嘗試分析,分析過(guò)程肯定會(huì)遇到這樣那樣的問(wèn)題,直到dead-end或者分析完了再回過(guò)頭看我的分析過(guò)程,這樣在實(shí)際工作中遇到類(lèi)似問(wèn)題的時(shí)候我想更有可能callback。
當(dāng)然,如果你有別的思路和手段分析這個(gè)問(wèn)題,非常歡迎在評(píng)論區(qū)分享你的見(jiàn)解。
下面開(kāi)始回顧一下我當(dāng)時(shí)記錄的分析過(guò)程。
嘗試問(wèn)題重現(xiàn)時(shí)抓取threaddump(進(jìn)入到backend容器執(zhí)行命令jstack -l <pid>
),主要觀察tomcat工作線程池(線程名:http-nio-0.0.0.0-8080-exec-*
)的線程狀態(tài),發(fā)現(xiàn)都是處于等待從線程池隊(duì)列獲取任務(wù)的狀態(tài),并未見(jiàn)工作線程卡在一些業(yè)務(wù)操作上:
"http-nio-0.0.0.0-8080-exec-1" #167 daemon prio=5 os_prio=0 tid=0x00007f0461487000 nid=0xb1 waiting on condition [0x00007f043d8fd000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000006f99c3ba8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:108) at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None
同時(shí)通過(guò)在服務(wù)提供方tcpdump抓包分析,到目前分析結(jié)論是延遲發(fā)生在backend
這一端(但并不能再縮小問(wèn)題范圍,kernel處理慢或者內(nèi)部隊(duì)列堆積都有可能):
為了縮小問(wèn)題范圍,嘗試開(kāi)啟tomcat的訪問(wèn)日志和內(nèi)部DEBUG日志,看請(qǐng)求具體什么時(shí)間點(diǎn)到達(dá)tomcat的隊(duì)列,什么時(shí)間點(diǎn)開(kāi)始執(zhí)行用戶(hù)代碼,以及什么時(shí)候處理完的,這樣就可以進(jìn)一步確定延遲發(fā)生在哪個(gè)過(guò)程。
# 程序啟動(dòng)添加下面參數(shù) # 開(kāi)啟tomcat訪問(wèn)日志 --server.tomcat.accesslog.enabled=true # 開(kāi)啟tomcat內(nèi)部DEBUG日志 --logging.level.org.apache.tomcat=DEBUG --logging.level.org.apache.catalina=DEBUG
在我們的例子中,在compose.yml
給backend配置上JAVA_OPTS
環(huán)境變量即可
services: backend: image: shawyeok/128-slowbackend:backend environment: - JAVA_OPTS=-Dserver.tomcat.accesslog.enabled=true -Dlogging.level.org.apache.tomcat=DEBUG -Dlogging.level.org.apache.catalina=DEBUG
開(kāi)啟日志后可以看到tomcat處理的請(qǐng)求的詳細(xì)過(guò)程:
2021-09-28 15:35:06.409 DEBUG 1 --- [0-8080-Acceptor] o.apache.tomcat.util.threads.LimitLatch : Counting up[http-nio-0.0.0.0-8080-Acceptor] latch=10 2021-09-28 15:35:06.409 DEBUG 1 --- [0.0-8080-exec-3] o.apache.tomcat.util.threads.LimitLatch : Counting down[http-nio-0.0.0.0-8080-exec-3] latch=9 2021-09-28 15:35:06.409 DEBUG 1 --- [0.0-8080-exec-3] o.a.tomcat.util.net.SocketWrapperBase : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@f099444:org.apache.tomcat.util.net.NioChannel@50bf632e:java.nio.channels.SocketChannel[connected local=java-security-operation-platform-64f57cf5f9-pvnnn/10.50.63.246:8080 remote=/10.50.63.247:45142]], Read from buffer: [0] 2021-09-28 15:35:06.409 DEBUG 1 --- [0.0-8080-exec-3] org.apache.tomcat.util.net.NioEndpoint : Calling [org.apache.tomcat.util.net.NioEndpoint@44c861c].closeSocket([org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@f099444:org.apache.tomcat.util.net.NioChannel@50bf632e:java.nio.channels.SocketChannel[connected local=java-security-operation-platform-64f57cf5f9-pvnnn/10.50.63.246:8080 remote=/10.50.63.247:45142]]) 2021-09-28 15:35:06.410 DEBUG 1 --- [0.0-8080-exec-1] o.apache.catalina.valves.RemoteIpValve : Incoming request /v2/platform/health with originalRemoteAddr [10.50.63.247], originalRemoteHost=[10.50.63.247], originalSecure=[false], originalScheme=[http], originalServerName=[platform-fengkong.zhaopin.com], originalServerPort=[80] will be seen as newRemoteAddr=[192.168.11.63], newRemoteHost=[192.168.11.63], newSecure=[false], newScheme=[http], newServerName=[platform-fengkong.zhaopin.com], newServerPort=[80] 2021-09-28 15:35:06.410 DEBUG 1 --- [0.0-8080-exec-1] org.apache.catalina.realm.RealmBase : No applicable constraints defined 2021-09-28 15:35:06.410 DEBUG 1 --- [0.0-8080-exec-1] o.a.c.authenticator.AuthenticatorBase : Security checking request GET /v2/platform/health ...
但這個(gè)時(shí)候注意到一個(gè)Logger比較眼熟:o.apache.tomcat.util.threads.LimitLatch
,而且有Limit字眼,難道延遲是由于tomcat內(nèi)部在競(jìng)爭(zhēng)某種資源?仔細(xì)看這個(gè)Logger的日志:
看到這里就很值得懷疑了,重新查看之前的threadump文件,發(fā)現(xiàn)tomcat Acceptor
線程正是block在這里?。?/p>
"http-nio-8080-Acceptor" #29 daemon prio=5 os_prio=0 cpu=26.62ms elapsed=112.10s tid=0x00007ffff8ae8000 nid=0x3b waiting on condition [0x00007fff896fe000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@11.0.23/Native Method) - parking to wait for <0x0000000083ad3860> (a org.apache.tomcat.util.threads.LimitLatch$Sync) at java.util.concurrent.locks.LockSupport.park(java.base@11.0.23/LockSupport.java:194) at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(java.base@11.0.23/AbstractQueuedSynchronizer.java:885) at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(java.base@11.0.23/AbstractQueuedSynchronizer.java:1039) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(java.base@11.0.23/AbstractQueuedSynchronizer.java:1345) at org.apache.tomcat.util.threads.LimitLatch.countUpOrAwait(LimitLatch.java:117) at org.apache.tomcat.util.net.AbstractEndpoint.countUpOrAwaitConnection(AbstractEndpoint.java:1309) at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:94) at java.lang.Thread.run(java.base@11.0.23/Thread.java:829)
原來(lái)上面在分析線程dump時(shí)真相就在眼前了,卻給忽略了,這很致命~
現(xiàn)在這個(gè)問(wèn)題表層的原因已經(jīng)清楚了:由于該服務(wù)配置的tomcat連接數(shù)太少,觸發(fā)了LimitLatch限制,阻塞等待老的連接釋放(這點(diǎn)可以通過(guò)抓包分析得以驗(yàn)證,被阻塞的請(qǐng)求得以響應(yīng)之前總是有一個(gè)TCP連接釋放)
查看源碼中src/main/resources/application.yml文件,有如下配置:
server.tomcat.max-connections: 10
這里因?yàn)槭亲詈?jiǎn)復(fù)現(xiàn)Demo,這個(gè)配置單獨(dú)放在這里是非??梢傻?,然而現(xiàn)實(shí)情況中它可能隱藏在大量的配置中,你未必能注意到,特別是線上排查問(wèn)題時(shí)往往情況都比較急。
查看當(dāng)前和tomcat 8080端口建立的連接,剛好是10個(gè),查看spring boot文檔默認(rèn)值是8192(server.tomcat.max-connections
),關(guān)于這個(gè)當(dāng)初為什么要添加上面最大連接數(shù)的配置,我就不好細(xì)說(shuō)了,總之是人為方面的原因。
再看nginx的配置,worker_processes
配置為16,是大于10的,因此當(dāng)backend的連接數(shù)達(dá)到10時(shí),acceptor線程就會(huì)阻塞等待,直到有連接釋放,這就是為什么會(huì)出現(xiàn)間歇性請(qǐng)求響應(yīng)慢的現(xiàn)象。
worker_processes 16;
解決這個(gè)問(wèn)題,就是把max-connections
的配置刪掉即可,但是這個(gè)問(wèn)題如果細(xì)究的話你可以還會(huì)注意其它的點(diǎn)。
問(wèn)題的表現(xiàn),往往以多種形式呈現(xiàn)。
在這個(gè)case中,我們也可以通過(guò)ss命令查看tcp syn連接隊(duì)列的當(dāng)前狀態(tài),會(huì)發(fā)現(xiàn)Recv-Q
這一列始終大于0,說(shuō)明有連接正在等待用戶(hù)線程accept(2)。
tomcat線程模型
我們看一下tomcat線程模型,在一個(gè)新連接上發(fā)起一次http請(qǐng)求會(huì)首先經(jīng)過(guò)Acceptor線程,這個(gè)線程只負(fù)責(zé)接收新的連接然后放到連接隊(duì)列中,后續(xù)的解析http報(bào)文、執(zhí)行應(yīng)用邏輯、發(fā)送響應(yīng)結(jié)果都在Worker線程池中執(zhí)行。
通過(guò)上面ss
命令的截圖,Rec-Q
那一列顯示3
即說(shuō)明有三個(gè)新連接的請(qǐng)求Acceptor線程還沒(méi)有來(lái)得及處理,為什么沒(méi)有來(lái)得及處理呢?即受到了server.tomcat.max-connections
配置的約束導(dǎo)致的。
總結(jié)
本文主要是分享一個(gè)tomcat間歇性響應(yīng)慢的case,在筆者的第一次排查過(guò)程中,其實(shí)真相就隱藏在線程dump中,但是最開(kāi)始的時(shí)候錯(cuò)過(guò)了。
以上就是Java后端服務(wù)間歇性響應(yīng)慢的問(wèn)題排查與解決的詳細(xì)內(nèi)容,更多關(guān)于Java服務(wù)間歇性響應(yīng)慢的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring?MVC響應(yīng)結(jié)果和設(shè)置舉例詳解
在Web開(kāi)發(fā)中,通過(guò)@Controller和@RestController注解,可以分別返回HTML頁(yè)面和JSON數(shù)據(jù),@Controller配合@ResponseBody可以返回HTML代碼片段或JSON,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-11-11關(guān)于@Component注解的含義說(shuō)明
這篇文章主要介紹了關(guān)于@Component注解的含義說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11HttpClient實(shí)現(xiàn)表單提交上傳文件
這篇文章主要為大家詳細(xì)介紹了HttpClient實(shí)現(xiàn)表單提交上傳文件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08java常量字符串過(guò)長(zhǎng)報(bào)錯(cuò)的解決辦法以及原因分析
在IDEA中字符串長(zhǎng)度超過(guò)65535,進(jìn)行打印,IDEA會(huì)提示java:常量字符串過(guò)長(zhǎng),這篇文章主要給大家介紹了關(guān)于java常量字符串過(guò)長(zhǎng)報(bào)錯(cuò)的解決辦法以及原因分析,需要的朋友可以參考下2023-01-01SpringEvents與異步事件驅(qū)動(dòng)案例詳解
本文深入探討了SpringBoot中的事件驅(qū)動(dòng)架構(gòu),特別是通過(guò)Spring事件機(jī)制實(shí)現(xiàn)組件解耦和系統(tǒng)擴(kuò)展性增強(qiáng),介紹了事件的發(fā)布者、事件本身、事件監(jiān)聽(tīng)器和事件處理器的概念,感興趣的朋友跟隨小編一起看看吧2024-09-09深入理解Java中的構(gòu)造函數(shù)引用和方法引用
java構(gòu)造函數(shù),也叫構(gòu)造方法,是java中一種特殊的函數(shù)。函數(shù)名與相同,無(wú)返回值。方法引用是用來(lái)直接訪問(wèn)類(lèi)或者實(shí)例的已經(jīng)存在的方法或者構(gòu)造方法。下面我們來(lái)詳細(xì)了解一下它們吧2019-06-06Java中調(diào)用DLL動(dòng)態(tài)庫(kù)的操作方法
在Java編程中,有時(shí)我們需要調(diào)用本地代碼庫(kù),特別是Windows平臺(tái)上的DLL(動(dòng)態(tài)鏈接庫(kù)),本文中,我們將詳細(xì)討論如何在Java中加載和調(diào)用DLL動(dòng)態(tài)庫(kù),并通過(guò)具體示例來(lái)展示這個(gè)過(guò)程,感興趣的朋友跟隨小編一起看看吧2024-03-03