Java应用启动慢、接口超时、频繁Full GC?别再把锅甩给JVM了!
问题现场扩容反而引发告警风暴最近我们的服务在业务高峰进行一次常规扩容新增了两个POD。本以为能平滑分担流量结果启动后不久P1级别接口超时告警和频繁Full GC告警接连爆发。从监控图可以清晰看到新启动的POD在刚接收流量的前几分钟接口平均耗时飙升到秒级JVM的Full GC次数陡增Old区几乎被打满运行几分钟后一切又逐渐恢复正常。这种现象并非第一次出现应用启动时也会出现类似的现象。扩容或者重启变成“陪葬”到底是谁在作祟结论先行RefreshScope的滥用团队没有止步于表象而是深入Spring Cloud源码并结合Demo进行复现最终锁定了“真凶”——RefreshScopel滥用。RefreshScope的初始化“陷阱”在我们的应用中大量Controller、Service类上都标注了RefreshScope初衷是为了实现Nacos配置的热更新。然而这个注解的初始化行为却暗藏玄机普通Bean在Web容器初始化完成之前实例化服务注册到Nacos时所有依赖都已就绪。RefreshScope Bean采用懒加载模式它要等到ContextRefreshedEvent事件该事件发生在Web容器初始化之后才会触发实例化。也就是说当Nacos完成服务注册、上游流量开始涌向新POD时被RefreshScope修饰的那些Bean包括Controller、Service可能根本还没创建完成锁粒度惊人读写锁阻塞所有请求翻看RefreshScope的源码发现它的get()和destroy()方法都使用了读写锁。当请求并发进入时大量线程需要等待锁释放等到Bean实例化完成才能继续。这个锁的粒度是整个Scope级别相当于把一个Controller里所有接口、甚至多个Service都串行化了。启动瞬间的高并发直接导致请求排队、响应超时。运行时配置变更也是“定时炸弹”你以为只在启动时卡太天真了。当Nacos配置发生变更时RefreshScope会执行refreshAll()——销毁所有被RefreshScope标注的Bean实例但并不立即重建。等到下一次请求进来时再走一遍加锁、实例化的重流程。于是每次配置发布都可能引发新一轮的请求阻塞和P1告警。抽丝剥茧源码分析RefreshScope注解Bean初始化SpringCloud提供的服务注册抽象类AbstractAutoServiceRegistration通过监听Web容器的初始化完成事件注册服务注册中心客户端(不限于Nacos)实现这个抽象类就会在 Web容器初始化完成后向注册中心注册服务。Nacos的服务注册NacosAutoServiceRegistration实现了这个抽象类 所以Nacos客户端会在Web容器初始化完成后注册服务 。业务Bean的实例化过程分为两类没有RefreshScope修饰的Bean初始化过程初始化完成后在onRefresh()方法中初始化Web容器Bean容器Ben初始化完成发布容器初始化完成事件ServletWebServerInitializedEvent,ServletWebServerInitializedEvent继承自WebServerInitializedEvent事件此后Nacos客户端注册服务被RefreshScope修饰的Bean初始化过程被RefreshScope修饰的Bean使用RefreshScope类监听ContextRefreshedEvent事件初始化BeanContextRefreshedEvent事件发生在WebServerInitializedEvent之后此时初始化Bean时服务已经被注册到注册中心请求已经进来但Bean还在初始化中造成服务启动时接口响应慢、GC异常RefreschScope注解Bean实现配置热更新在上面的源码分析中知道RefreshScope修饰的Bean使用RefreshScope类监听ContextRefreshedEvent事件初始化Bean那它是如何做到热更新配置的Nacos的配置中心Client类NacosContextRefresher在Spring容器发布ApplicationReadyEvent事件时预注册了一个AbstractSharedListener在AbstractSharedListener的中发布RefreshEvent事件

相关新闻