线程池Feign调用内部服务异常定位

文章讲述了业务系统在异步积分初始化过程中遇到401未授权错误,经调查发现是由于Feign调用时带有已过期的Token,源于全局Feign拦截器将ThreadLocal中的Token传递。提供了三种解决方案,包括禁用线程上下文继承、动态刷新上下文和让业务方自选是否继承上下文。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

    当某业务发布计划后,将以异步方式启动积分初始化流程,以确保操作高效且不影响用户体验。初始化时涉及调用相关基础服务接口,获取必要的信息并执行积分计算。

    同时实施了异常兜底策略,如果初始化出现问题导致失败,系统会自动定时进行重试。

    某日,线上发现异步初始化时调用基础服务返回401-未授权的报错。

    看了下兜底定时任务,调用基础服务正常,奇怪的很,难道基础服务不稳定?

    异步流程时序图如下:

定位

    首先尝试在测试环境复现,居然能复现,那就简单了呢;

    在k8s容器内抓个包吧(fegin内部调用是基于http), 或通过打印详细日志、或使用Arthas都可以;

    请求头中居然有authorization及token,而且token已经过期了,这就是请求基础服务导致401的原因;( 部分内部调用的接口定义无需token,但是传了会校验)

    这里是异步处理业务,当前线程归属于线程池的核心线程,为啥会在feign请求时加上token的请求头?

    这里其实大概有怀疑方向了;

    我司框架定义的全局Feign拦截器headerInterceptor,会获取请求上下文,Feign调用时通过请求头传递请求上下文;(代码涉密,以下用类似代码说明)

@Bean
public RequestInterceptor headerInterceptor() {
    return template -> {
        Map<String, Collection<String>> headers = new HashMap<>();
        Map<String, Collection<String>> localHeaders = HeadersHolder.getHeaders();
        if (localHeaders != null && !localHeaders.isEmpty()){
            headers.putAll(localHeaders);
        }
        // 省略其它
        template.headers(headers);        
    };
}

    请求上下文类如下:

public class HeadersHolder {
    private static final TransmittableThreadLocal<Map<String, Collection<String>>> LOCAL = new TransmittableThreadLocal<>();

    public static void setBetaHeaders(Map<String, List<String>> headers){
        LOCAL.set(headers);
    }

    public static Map<String, Collection<String>> getHeaders(){
        return LOCAL.get();
    }

    public static void remove(){
        LOCAL.remove();
    }
}

    TransmittableThreadLocal是alibaba的transmittable-thread-local组件中的类:

    transmittable-thread-local对应的MAVEN坐标如下:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
</dependency>

    TransmittableThreadLocal:在使用线程池等会池化复用线程的执行组件情况下传递ThreadLocal值;

    那么,就很容易理解上面的现象;

    异步事件使用了Google Guava库的AsyncEventBus类;

    发布事件后,提交到线程池执行;如果线程池核心线程未满,此时创建核心线程,是基于当前tomcat的请求线程,会把当前ThreadLocal(包含token)存到线程池的核心线程里;

    线程池的线程在调用内部feign接口时,会把token传过去,并且该核心线程后续的上下文都是当前这个上下文;

    这就是问题所在啦~

解决

    问题清晰之后,解决方案有很多种,也和架构师同学一起聊了聊,方案各有优缺取舍,列出如下:

方案一

    某些业务代码很明确不需要用户上下文,使用TtlExecutors#getDisableInheritableThreadFactory包装ThreadFactory,这样在创建线程时就不会继承上下文;

    优点: 线程池的线程上下文很干净;

    缺点:线程上下文太干净了; 如果你的项目中使用了链路追踪、埋点日志等组件,也不会继承相关ThreadLocal信息,会导致收集不到信息;

@Bean
public EventBus asyncEventBus() {
    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
            .setNameFormat("async-event-pool-%d").build();
    Executor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(QUEUE_CAPACITY), TtlExecutors.getDisableInheritableThreadFactory(namedThreadFactory));
    return new AsyncEventBus(threadPoolExecutor);
}

     源码中,DisableInheritableThreadFactoryWrapper类在创建子线程前备份及清理了上下文,创建子线程后又恢复了;

 方案二

    动态刷新线程池中线程的上下文;

  • 线程池执行任务时,使用TtlRunnable.get(task)包装Runable; (代码侵入较大,不建议)
  • 使用TtlExecutors#getTtlExecutor包装Executor; (代码侵入较小,建议)

    优点:子线程实时刷新上下文;

    缺点:某些场景中,子线程上下文我们可能不需要继承;

    TtlExecutors#getTtlExecutor包装示例:

@Bean
public EventBus asyncEventBus() {
    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
            .setNameFormat("async-event-pool-%d").build();
    Executor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(QUEUE_CAPACITY), namedThreadFactory);
    return new AsyncEventBus(TtlExecutors.getTtlExecutor(threadPoolExecutor));
}

    Alibaba官方两种示例:

     从源码看,方案二的两种包装最后都是调用的TtlRunnable.get;

方案三

    借鉴思想,子线程需不需要业务的上下文,业务方其实是最清楚的,交给业务方选择使用。

    提供一个ThreadFactory包装类,支持是否继承用户上下文ThreadLocal,业务方按需使用。(其余ThreadLocal默认都保留继承)

参考

GitHub - alibaba/transmittable-thread-local: 📌 a missing Java std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.

<think>好的,我现在要帮助用户解决Spring Boot应用中出现Internal Server Error 500和请求超时的问题,具体是/alarm/stopSendMainBaseStation这个接口。用户提到了参考的引用内容,里面涉及Nacos客户端超时配置的调整。首先,我需要根据这些信息,逐步分析可能的原因,并提供排查步骤。 首先,用户的问题是关于500错误和超时,可能的原因有很多。我需要从多个方面考虑:网络问题、服务配置、代码异常、资源不足、依赖服务问题等。同时,用户提供的引用提到Nacos的超时配置,所以需要检查Nacos相关的设置是否正确。 第一步,检查服务可用性。用户提到Nacos,所以需要确认Nacos服务是否正常运行,并且客户端配置的地址是否正确。引用中的例子是nacos.discovery.server-addr=127.0.0.1:1111,这里需要用户确认实际配置的地址是否可达,端口是否正确。如果Nacos服务宕机或者网络不通,客户端无法注册或发现服务,可能导致超时和500错误。 第二步,调整超时时间。用户引用的资料建议增加超时时间,比如设置为nacos.discovery.timeout=10000。可能需要检查当前配置是否过小,尤其是在高延迟或高负载的情况下,适当增加超时时间可以避免因响应慢导致的超时错误。同时,其他HTTP请求的超时时间,比如Feign或RestTemplate的配置,也需要检查。例如,Spring CloudFeign客户端的默认超时可能不够,需要调整。 第三步,查看日志。应用日志和Nacos客户端日志是关键。应用日志中可能会有更详细的错误信息,比如数据库连接失败、空指针异常等具体的异常堆栈。Nacos客户端的日志可能显示连接超时或注册失败的原因。需要指导用户如何找到这些日志,比如在application.properties中设置logging.level.com.alibaba.nacos=DEBUG来开启Nacos的调试日志。 第四步,网络连接问题。需要确认客户端和服务端之间的网络是否通畅,是否存在防火墙或安全组限制。使用telnet或curl测试端口连通性,或者用ping命令测试网络延迟。如果存在跨网络区域的情况,可能需要调整网络配置。 第五步,资源限制。检查应用和Nacos服务端的CPU、内存使用情况,过高的资源占用会导致响应缓慢甚至超时。JVM的垃圾回收情况也需要关注,频繁的Full GC会导致应用暂停,无法及时处理请求。 第六步,依赖服务状态。如果/alarm/stopSendMainBaseStation接口依赖其他服务(比如数据库、消息队列、其他微服务),需要确认这些依赖服务是否正常运行。例如,数据库连接池耗尽、第三方API不可用等都可能导致超时和500错误。 第七步,代码逻辑问题。接口内部是否有死循环、长时间阻塞操作,或者未处理的异常。例如,数据库查询没有分页导致加载大量数据,或者同步调用耗时较长的外部服务。建议用户检查接口的代码逻辑,是否存在性能瓶颈或未捕获的异常。 第八步,配置检查。除了Nacos的配置外,还需要检查Spring Boot的应用配置,比如数据库连接池配置是否合理,线程池大小是否足够,HTTP客户端配置是否正确。例如,连接池最大连接数过小,导致请求堆积,进而超时。 第九步,重试机制。如果使用了服务调用时的自动重试(比如Ribbon或Feign的重试配置),过高的重试次数可能导致累积超时。需要检查重试配置是否合理,或者暂时禁用重试以排除问题。 最后,建议用户分步骤排查,从最简单的网络和配置开始,逐步深入检查日志和代码。如果问题依然存在,可能需要更详细的日志分析或性能剖析工具来定位问题。 在生成相关问题的时候,需要考虑用户可能进一步的问题,比如如何具体调整超时时间,如何分析日志,或者如何处理依赖服务故障等,这样可以帮助用户更全面地理解和解决问题。</think>### 排查Spring Boot Internal Server Error 500及超时问题的步骤 #### 1. **检查服务可用性与配置** - **Nacos服务状态**:确认Nacos服务端是否正常运行且网络可达。若Nacos服务宕机或网络隔离,客户端会因无法连接而超时。例如,检查配置`nacos.discovery.server-addr`的地址和端口是否与Nacos服务端一致[^1]。 - **超时配置调整**:在`application.properties`或`application.yml`中增加Nacos客户端超时时间: ```yaml nacos.discovery.timeout=15000 # 默认通常为3000ms,建议逐步调大测试 ``` #### 2. **分析日志定位具体错误** - **应用日志**:查看Spring Boot应用日志,搜索`/alarm/stopSendMainBaseStation`接口的报错信息。常见的500错误可能由以下原因引发: - 数据库连接失败 - 空指针异常(NPE) - 第三方服务调用超时 - **Nacos客户端日志**:启用调试日志级别以获取更多细节: ```properties logging.level.com.alibaba.nacos=DEBUG ``` #### 3. **网络连通性测试** - 使用`telnet`或`curl`验证客户端到Nacos服务端的网络连接: ```bash telnet 127.0.0.1 1111 # 替换为实际Nacos地址和端口 ``` - 若存在防火墙或安全组限制,需开放对应端口。 #### 4. **资源与性能检查** - **CPU/内存占用**:通过`top`(Linux)或任务管理器(Windows)检查应用和Nacos服务端的资源使用情况。 - **JVM垃圾回收**:使用`jstat`或GC日志分析工具(如GCViewer)检查是否存在频繁Full GC。 #### 5. **依赖服务健康状态** - 若接口依赖其他服务(如数据库、Redis),检查其可用性: - 数据库连接池是否耗尽(查看`HikariCP`或`Druid`日志) - 第三方API响应时间是否过长(通过`curl -v`或Postman测试) #### 6. **代码逻辑与异常处理** - **接口内部逻辑**:检查`/alarm/stopSendMainBaseStation`的实现代码: - 是否存在未捕获的异常(如`try-catch`遗漏) - 是否因大事务、死锁或复杂查询导致阻塞 - **同步调用优化**:若涉及远程调用,考虑异步处理或增加熔断机制(如Hystrix或Sentinel)。 #### 7. **HTTP客户端配置调整** - **RestTemplate/Feign超时设置**:若接口内部调用其他服务,需配置合理的超时时间: ```yaml feign.client.config.default.connectTimeout=5000 feign.client.config.default.readTimeout=10000 ``` #### 8. **临时解决方案与验证** - **降低负载**:通过限流(如Sentinel)暂时减少请求压力。 - **简化复现路径**:使用单元测试或Postman单独测试接口,排除其他干扰因素。 --- ### 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值