序
本文主要记录一下spring schedule调度异常。
相关背景
- 调度配置
@Scheduled(cron = "0 0 22 * * ?") public void doSomething() throws InterruptedException { //remote http call }复制代码
- 问题
从某一天开始这个调度没有执行
排查
查看日志
查看了下日志,没有抛异常
查看系统状态
查了那个时间点的系统指标,没啥异常,附近有个young gc,不过也就80ms
jstack
jstack -l pid复制代码
初步看来线程堆栈,没看到有啥死锁信息
假设
一开始有点怀疑是不是spring schedule因为线程池线程不够,或者前面几个时间点的定时任务执行时间过长导致这个调度任务一直无法执行,或者导致了misfire的情况。
后来线下单独试图去复现,发现spring schedule只要有放入队列,即使misfire了,过了设定的调度时间点,还是会执行的。就单单这个定时任务有问题,其他的定时任务每天都执行好好的。
ScheduledThreadPoolExecutor
java/util/concurrent/ScheduledThreadPoolExecutor.java
public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler);}public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueworkQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }复制代码
这里创建的是DelayedWorkQueue
ThreadPoolExecutor#runWorker
java/util/concurrent/ThreadPoolExecutor.java
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }复制代码
这里调用了task.run();
ScheduledThreadPoolExecutor.ScheduledFutureTask#run
/** * Overrides FutureTask version so as to reset/requeue if periodic. */ public void run() { boolean periodic = isPeriodic(); if (!canRunInCurrentRunState(periodic)) cancel(false); else if (!periodic) ScheduledFutureTask.super.run(); else if (ScheduledFutureTask.super.runAndReset()) { setNextRunTime(); reExecutePeriodic(outerTask); } } /** * Requeues a periodic task unless current run state precludes it. * Same idea as delayedExecute except drops task rather than rejecting. * * @param task the task */ void reExecutePeriodic(RunnableScheduledFuture task) { if (canRunInCurrentRunState(true)) { super.getQueue().add(task); if (!canRunInCurrentRunState(true) && remove(task)) task.cancel(false); else ensurePrestart(); } }复制代码
这里每当执行完之后判断下一次的执行时间,然后重新放入队列,这样完成周期性调度。
看到这里再去仔细看来下日志,发现有一天的调度任务执行了没有打印结束日志,那么开始怀疑是不是这个定时任务一直没有执行完,导致接下来的几天的调度都一直没法放入队列执行。
线程堆栈
"job-async-5" #44 prio=5 os_prio=0 tid=0x00007fb9aa7a4000 nid=0x42 runnable [0x00007fb98dfee000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:170) at java.net.SocketInputStream.read(SocketInputStream.java:141) at java.io.BufferedInputStream.fill(BufferedInputStream.java:246) at java.io.BufferedInputStream.read1(BufferedInputStream.java:286) at java.io.BufferedInputStream.read(BufferedInputStream.java:345) - locked <0x00000006c6167e60> (a java.io.BufferedInputStream) at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:704) at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:647) at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1536) - locked <0x00000006c6167eb8> (a sun.net.www.protocol.http.HttpURLConnection) at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1441) - locked <0x00000006c6167eb8> (a sun.net.www.protocol.http.HttpURLConnection) at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480) at org.springframework.http.client.SimpleClientHttpResponse.getRawStatusCode(SimpleClientHttpResponse.java:52) at org.springframework.cloud.netflix.metrics.DefaultMetricsTagProvider.clientHttpRequestTags(DefaultMetricsTagProvider.java:44) at org.springframework.cloud.netflix.metrics.MetricsClientHttpRequestInterceptor.intercept(MetricsClientHttpRequestInterceptor.java:75) at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:86) at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:70) at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:652) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613) at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:407) at org.springframework.web.client.RestTemplate$$FastClassBySpringCGLIB$$aa4e9ed0.invoke() at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:721) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85) at org.springframework.cloud.netflix.metrics.RestTemplateUrlTemplateCapturingAspect.captureUrlTemplate(RestTemplateUrlTemplateCapturingAspect.java:33) at sun.reflect.GeneratedMethodAccessor235.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656) at org.springframework.web.client.RestTemplate$$EnhancerBySpringCGLIB$$68afae5e.postForEntity( )复制代码
又在查看了没有结束的定时任务的线程堆栈,发现确实是卡住了,不过这里线程状态一直是RUNNABLE.
SocketInputStream.socketRead0
罪魁祸首就在这个SocketInputStream.socketRead0。不过有个疑问,默认RestTemplate应该是有设置超时时间才对的。
spring-web-4.3.7.RELEASE-sources.jar!/org/springframework/web/client/RestTemplate.java
protectedT doExecute(URI url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor responseExtractor) throws RestClientException { Assert.notNull(url, "'url' must not be null"); Assert.notNull(method, "'method' must not be null"); ClientHttpResponse response = null; try { ClientHttpRequest request = createRequest(url, method); if (requestCallback != null) { requestCallback.doWithRequest(request); } response = request.execute(); handleResponse(url, method, response); if (responseExtractor != null) { return responseExtractor.extractData(response); } else { return null; } } catch (IOException ex) { String resource = url.toString(); String query = url.getRawQuery(); resource = (query != null ? resource.substring(0, resource.indexOf(query) - 1) : resource); throw new ResourceAccessException("I/O error on " + method.name() + " request for \"" + resource + "\": " + ex.getMessage(), ex); } finally { if (response != null) { response.close(); } } }复制代码
这里调用了createRequest
spring-web-4.3.7.RELEASE-sources.jar!/org/springframework/http/client/support/HttpAccessor.java
private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException { ClientHttpRequest request = getRequestFactory().createRequest(url, method); if (logger.isDebugEnabled()) { logger.debug("Created " + method.name() + " request for \"" + url + "\""); } return request; }复制代码
默认是SimpleClientHttpRequestFactory spring-web-4.3.7.RELEASE-sources.jar!/org/springframework/http/client/SimpleClientHttpRequestFactory.java
private int connectTimeout = -1; private int readTimeout = -1;@Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { HttpURLConnection connection = openConnection(uri.toURL(), this.proxy); prepareConnection(connection, httpMethod.name()); if (this.bufferRequestBody) { return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming); } else { return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming); } } /** * Template method for preparing the given {@link HttpURLConnection}. *The default implementation prepares the connection for input and output, and sets the HTTP method. * @param connection the connection to prepare * @param httpMethod the HTTP request method ({@code GET}, {@code POST}, etc.) * @throws IOException in case of I/O errors */ protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { if (this.connectTimeout >= 0) { connection.setConnectTimeout(this.connectTimeout); } if (this.readTimeout >= 0) { connection.setReadTimeout(this.readTimeout); } connection.setDoInput(true); if ("GET".equals(httpMethod)) { connection.setInstanceFollowRedirects(true); } else { connection.setInstanceFollowRedirects(false); } if ("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod) || "DELETE".equals(httpMethod)) { connection.setDoOutput(true); } else { connection.setDoOutput(false); } connection.setRequestMethod(httpMethod); }复制代码
这里prepareConnection进行了超时设置,不过connectTimeout和readTimeout默认都是-1,相当于永远阻塞。这就是出问题的地方。
修复问题
设置timeout
创建resttemplate的时候,指定设置好超时时间的ClientHttpRequestFactory
- SimpleClientHttpRequestFactory 如果没有依赖http client,则默认使用的是SimpleClientHttpRequestFactory
@Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory(); clientHttpRequestFactory.setConnectTimeout(30000); clientHttpRequestFactory.setReadTimeout(30000); RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory); return restTemplate; }复制代码
注意这里单位是毫秒
- 如果有http client的依赖,底层使用HttpComponentsClientHttpRequestFactory
@Bean public RestTemplate customRestTemplate(){ HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory(); httpRequestFactory.setConnectionRequestTimeout(3000); httpRequestFactory.setConnectTimeout(3000); httpRequestFactory.setReadTimeout(3000); return new RestTemplate(httpRequestFactory); }复制代码
设置了timeout还不行
如果你设置了timeout,依然还阻塞在SocketInputStream.socketRead0,那么请升级JDK,JDK有个bug: 。