You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Cloud Vault续期Session Token遇SocketTimeoutException解决方案咨询

问题描述

我有一个基于Spring Boot + Kotlin的微服务,通过Vault获取AWS凭证以连接DynamoDB。但在续期Session Token时,Vault实现会随机抛出SocketTimeoutException,相关堆栈信息如下:

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://my.vault.server:443/v1/aws/sts/prod-documentrecognitionservice": Read timed out; nested exception is java.net.SocketTimeoutException: Read timed out
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:785)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:711)
    at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:334)
    at org.springframework.vault.core.VaultTemplate.lambda$doRead$5(VaultTemplate.java:461)
    at org.springframework.vault.core.VaultTemplate.doWithSession(VaultTemplate.java:448)
    at org.springframework.vault.core.VaultTemplate.doRead(VaultTemplate.java:458)
    at org.springframework.vault.core.VaultTemplate.read(VaultTemplate.java:353)
    at org.springframework.vault.core.lease.SecretLeaseContainer.doGetSecrets(SecretLeaseContainer.java:645)
    at org.springframework.vault.core.lease.SecretLeaseContainer.doStart(SecretLeaseContainer.java:390)
    at org.springframework.vault.core.lease.SecretLeaseContainer.onLeaseExpired(SecretLeaseContainer.java:742)
    at org.springframework.vault.core.lease.SecretLeaseContainer.lambda$scheduleLeaseRotation$3(SecretLeaseContainer.java:595)
    at org.springframework.vault.core.lease.SecretLeaseContainer$LeaseRenewalScheduler$1.run(SecretLeaseContainer.java:862)
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
    at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:95)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.net.SocketTimeoutException: Read timed out
    at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:283)
    at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:309)
    at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:350)
    at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:803)
    at java.base/java.net.Socket$SocketInputStream.read(Socket.java:966)
    at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:478)
    at java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:472)
    at java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:70)
    at java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1455)
    at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:1059)
    at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137)
    at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153)
    at org.apache.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:280)
    at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138)
    at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
    at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259)
    at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)
    at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:157)
    at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273)
    at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125)
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:272)
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
    at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:87)
    at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)
    at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:109)
    at org.springframework.vault.core.VaultTemplate.lambda$getSessionInterceptor$1(VaultTemplate.java:255)
    at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:93)
    at org.springframework.vault.client.RestTemplateBuilder.lambda$createTemplate$4(RestTemplateBuilder.java:239)
    at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:93)
    at org.springframework.vault.client.VaultClients.lambda$createRestTemplate$0(VaultClients.java:122)
    at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:93)
    at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:77)
    at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:776)
    ... 19 common frames omitted

请问是否可以为Spring Cloud Vault提供自定义的RestTemplate配置,增加超时时间并针对IO异常进行重试?

解决方案

完全可以通过自定义RestTemplate配置解决该问题,以下是具体实现步骤:

1. 自定义带超时配置的RestTemplate

通过VaultRestTemplateCustomizer或RestTemplateBuilder,可以为Spring Cloud Vault指定自定义的连接超时和读取超时参数:

import org.springframework.boot.web.client.RestTemplateBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.vault.client.VaultRestTemplateCustomizer
import java.time.Duration

@Configuration
class VaultConfig {

    @Bean
    fun vaultRestTemplateCustomizer(): VaultRestTemplateCustomizer {
        return VaultRestTemplateCustomizer { restTemplate ->
            val requestFactory = restTemplate.requestFactory as org.springframework.http.client.HttpComponentsClientHttpRequestFactory
            // 根据实际网络情况调整超时时长
            requestFactory.connectTimeout = Duration.ofSeconds(10).toMillis().toInt()
            requestFactory.readTimeout = Duration.ofSeconds(30).toMillis().toInt()
        }
    }

    // 也可通过RestTemplateBuilder全局配置Vault使用的RestTemplate
    @Bean
    fun vaultRestTemplateBuilder(): RestTemplateBuilder {
        return RestTemplateBuilder()
            .setConnectTimeout(Duration.ofSeconds(10))
            .setReadTimeout(Duration.ofSeconds(30))
    }
}

2. 添加IO异常重试机制

可以通过两种方式实现重试逻辑,按需选择:

方式一:配置Apache HttpClient原生重试策略

直接为HttpClient添加重试规则,针对SocketTimeoutException等IO异常自动重试:

import org.apache.http.client.HttpClient
import org.apache.http.client.config.RequestConfig
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.impl.client.StandardHttpRequestRetryHandler
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
import org.springframework.vault.client.VaultRestTemplateCustomizer
import java.time.Duration

@Configuration
class VaultConfig {

    @Bean
    fun vaultRestTemplateCustomizer(): VaultRestTemplateCustomizer {
        return VaultRestTemplateCustomizer { restTemplate ->
            // 构建重试处理器:最多重试3次,仅针对SocketTimeoutException重试
            val retryHandler = StandardHttpRequestRetryHandler(
                3,
                true,
                setOf(java.net.SocketTimeoutException::class.java)
            )

            val requestConfig = RequestConfig.custom()
                .setConnectTimeout(Duration.ofSeconds(10).toMillis().toInt())
                .setSocketTimeout(Duration.ofSeconds(30).toMillis().toInt())
                .build()

            val httpClient: HttpClient = HttpClientBuilder.create()
                .setDefaultRequestConfig(requestConfig)
                .setRetryHandler(retryHandler)
                .build()

            restTemplate.requestFactory = HttpComponentsClientHttpRequestFactory(httpClient)
        }
    }
}

方式二:使用Spring Retry框架实现重试

如果项目已引入Spring Retry,可通过拦截器为Vault请求添加重试逻辑:

首先在build.gradle.kts中添加依赖:

implementation("org.springframework.retry:spring-retry")
implementation("org.springframework.boot:spring-boot-starter-aop")

然后配置重试模板和RestTemplate拦截器:

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.retry.annotation.EnableRetry
import org.springframework.retry.backoff.FixedBackOffPolicy
import org.springframework.retry.policy.SimpleRetryPolicy
import org.springframework.retry.support.RetryTemplate
import org.springframework.vault.client.VaultRestTemplateCustomizer
import java.net.SocketTimeoutException

@Configuration
@EnableRetry
class VaultRetryConfig {

    @Bean
    fun vaultRetryTemplate(): RetryTemplate {
        val retryPolicy = SimpleRetryPolicy().apply {
            setMaxAttempts(3)
            setRetryableExceptions(mapOf(SocketTimeoutException::class.java to true))
        }

        val backOffPolicy = FixedBackOffPolicy().apply {
            setBackOffPeriod(1000) // 重试间隔1秒
        }

        return RetryTemplate().apply {
            setRetryPolicy(retryPolicy)
            setBackOffPolicy(backOffPolicy)
        }
    }

    @Bean
    fun vaultRestTemplateCustomizer(retryTemplate: RetryTemplate): VaultRestTemplateCustomizer {
        return VaultRestTemplateCustomizer { restTemplate ->
            // 保留原有拦截器,添加重试逻辑
            val originalInterceptors = restTemplate.interceptors.toList()
            restTemplate.interceptors.clear()
            restTemplate.interceptors.add { request, body, execution ->
                retryTemplate.execute {
                    execution.execute(request, body)
                }
            }
            restTemplate.interceptors.addAll(originalInterceptors)
        }
    }
}

注意事项

  • 超时时间和重试次数需根据Vault服务器性能、网络稳定性调整,避免过度重试增加服务器负载。
  • 不要同时启用HttpClient原生重试和Spring Retry,防止重复重试。
  • 确认Vault的STS端点支持重复请求,避免重试导致凭证重复生成问题。

内容的提问来源于stack exchange,提问作者Randy Hector

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 17:42:15