问题背景
在 Kubernetes 环境中,你是否遇到过这样的场景:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 10s default-scheduler Successfully assigned myapp-xxx to node-xxx
Normal Pulling 9s kubelet Pulling image "myapp:latest"
Normal Pulled 5s kubelet Successfully pulled image "myapp:latest" in 4.2s
Normal Created 5s kubelet Created container myapp
Normal Started 5s kubelet Started container myapp
Warning Unhealthy 0s kubelet Liveness probe failed: HTTP probe failed with statuscode: 503
Normal Killing 0s kubelet Container myapp failed liveness probe, will be restarted
Pod 刚启动就被 K8s 杀死,原因是健康检查超时。这是因为 Spring Boot 应用的启动时间超过了 K8s 的 initialDelaySeconds 设置。
启动慢的常见原因
- Bean 初始化耗时:大量 Bean 在启动时同步初始化,包括数据库连接、Redis 连接、MQ 连接等
- 静态代码块执行:类加载时执行耗时操作
- 配置加载:从远程配置中心拉取大量配置
- 数据库迁移:启动时执行数据库迁移脚本
这些同步操作导致应用启动时间过长,超过 K8s 健康检查阈值,最终 Pod 被强制重启,形成恶性循环。
核心概念
K8s 健康检查配置
apiVersion: v1
kind: Pod
spec:
containers:
- name: myapp
image: myapp:latest
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
K8s 的存活探针和就绪探针设置直接影响应用的启动和运行状态。存活探针检测应用是否存活,就绪探针决定 Pod 是否接收流量。当存活探针失败时,K8s 会重启容器。
懒加载(Lazy Initialization)
懒加载策略将资源初始化推迟到首次使用时执行,避免启动时的不必要开销。这样可以快速启动应用并响应健康检查,后续按需初始化各项服务。
并行 Bean 初始化
Spring Boot 2.2+ 支持并行初始化 Bean,通过 spring.main.lazy-initialization=true 和 @Lazy 注解实现。结合 @DependsOn 控制依赖关系,既能减少启动时间,又能保证初始化顺序。
实现方案
方案一:懒加载优化
将非关键 Bean 设置为懒加载,延迟初始化时机。
1. 全局懒加载配置
spring:
main:
lazy-initialization: true
2. 局部懒加载(使用 @Lazy 注解)
@Configuration
public class LazyConfig {
@Bean
@Lazy
public DataSource lazyDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/mydb")
.username("root")
.password("password")
.build();
}
@Bean
@Lazy
public RedisTemplate<String, Object> lazyRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
return template;
}
@Bean
@Lazy
public KafkaProducer<String, String> lazyKafkaProducer() {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
return new KafkaProducer<>(props);
}
}
方案二:并行 Bean 初始化
利用 Spring Boot 的并行初始化特性,减少启动时间。
1. 配置并行初始化
spring:
main:
lazy-initialization: false
allow-circular-references: true
threads:
virtual:
enabled: true
2. 使用 @DependsOn 控制依赖
@Configuration
public class ParallelConfig {
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/mydb")
.username("root")
.password("password")
.build();
}
@Bean
@DependsOn("dataSource")
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory("localhost", 6379);
}
@Bean
@DependsOn("redisConnectionFactory")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
return template;
}
}
方案三:健康检查分阶段
实现自定义健康检查,分阶段报告应用状态。
1. 自定义健康指示器
@Component
public class StartupHealthIndicator implements HealthIndicator {
private volatile boolean isReady = false;
private final Set<String> initializedComponents = new ConcurrentHashMap<>().newKeySet();
@Override
public Health health() {
if (isReady) {
return Health.up()
.withDetail("status", "fully initialized")
.withDetail("components", initializedComponents)
.build();
}
return Health.down()
.withDetail("status", "initializing")
.withDetail("initialized-components", initializedComponents)
.build();
}
public void markComponentReady(String componentName) {
initializedComponents.add(componentName);
}
public void markReady() {
this.isReady = true;
}
public boolean isReady() {
return isReady;
}
}
2. 就绪检查指示器
@Component
public class ReadinessHealthIndicator implements HealthIndicator {
private final StartupHealthIndicator startupHealthIndicator;
public ReadinessHealthIndicator(StartupHealthIndicator startupHealthIndicator) {
this.startupHealthIndicator = startupHealthIndicator;
}
@Override
public Health health() {
if (startupHealthIndicator.isReady()) {
return Health.up().build();
}
return Health.down().withDetail("reason", "application still initializing").build();
}
}
方案四:异步初始化组件
将耗时组件的初始化放到异步线程中执行。
1. 异步初始化配置
@Configuration
public class AsyncInitConfig {
@Bean
public Executor initExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setThreadNamePrefix("init-");
executor.initialize();
return executor;
}
}
2. 异步初始化服务
@Component
public class AsyncInitializer {
private static final Logger log = LoggerFactory.getLogger(AsyncInitializer.class);
private final StartupHealthIndicator startupHealthIndicator;
private final Executor initExecutor;
public AsyncInitializer(StartupHealthIndicator startupHealthIndicator,
Executor initExecutor) {
this.startupHealthIndicator = startupHealthIndicator;
this.initExecutor = initExecutor;
}
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
log.info("Application ready, starting async initialization...");
CompletableFuture.allOf(
initializeDatabase(),
initializeRedis(),
initializeKafka(),
initializeCache(),
initializeExternalServices()
).whenComplete((result, error) -> {
if (error != null) {
log.error("Async initialization failed", error);
} else {
log.info("All components initialized");
startupHealthIndicator.markReady();
}
});
}
private CompletableFuture<Void> initializeDatabase() {
return CompletableFuture.runAsync(() -> {
log.info("Starting database initialization...");
try {
Thread.sleep(3000);
startupHealthIndicator.markComponentReady("database");
log.info("Database initialization completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Database init interrupted", e);
}
}, initExecutor);
}
private CompletableFuture<Void> initializeRedis() {
return CompletableFuture.runAsync(() -> {
log.info("Starting Redis initialization...");
try {
Thread.sleep(2000);
startupHealthIndicator.markComponentReady("redis");
log.info("Redis initialization completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Redis init interrupted", e);
}
}, initExecutor);
}
private CompletableFuture<Void> initializeKafka() {
return CompletableFuture.runAsync(() -> {
log.info("Starting Kafka initialization...");
try {
Thread.sleep(2500);
startupHealthIndicator.markComponentReady("kafka");
log.info("Kafka initialization completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Kafka init interrupted", e);
}
}, initExecutor);
}
private CompletableFuture<Void> initializeCache() {
return CompletableFuture.runAsync(() -> {
log.info("Starting cache initialization...");
try {
Thread.sleep(1500);
startupHealthIndicator.markComponentReady("cache");
log.info("Cache initialization completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Cache init interrupted", e);
}
}, initExecutor);
}
private CompletableFuture<Void> initializeExternalServices() {
return CompletableFuture.runAsync(() -> {
log.info("Starting external services initialization...");
try {
Thread.sleep(4000);
startupHealthIndicator.markComponentReady("external-services");
log.info("External services initialization completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("External services init interrupted", e);
}
}, initExecutor);
}
}
完整实现示例
1. 启动监控过滤器
@Component
public class StartupTimingFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(StartupTimingFilter.class);
private static long startTime;
static {
startTime = System.currentTimeMillis();
}
@Override
public void init(FilterConfig filterConfig) {
long initTime = System.currentTimeMillis() - startTime;
log.info("Filter initialized in {}ms", initTime);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
2. Bean 初始化监控
@Component
public class BeanInitListener implements ApplicationListener<ContextRefreshedEvent> {
private static final Logger log = LoggerFactory.getLogger(BeanInitListener.class);
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
ConfigurableApplicationContext context = event.getApplicationContext();
if (context.getParent() == null) {
String[] beanDefinitionNames = context.getBeanDefinitionNames();
log.info("Total beans initialized: {}", beanDefinitionNames.length);
for (String beanName : beanDefinitionNames) {
try {
Object bean = context.getBean(beanName);
log.debug("Bean initialized: {} ({})", beanName, bean.getClass().getSimpleName());
} catch (Exception e) {
log.warn("Failed to get bean: {}", beanName, e);
}
}
}
}
}
3. 延迟初始化服务
@Service
public class LazyService {
private static final Logger log = LoggerFactory.getLogger(LazyService.class);
private final StartupHealthIndicator startupHealthIndicator;
public LazyService(StartupHealthIndicator startupHealthIndicator) {
this.startupHealthIndicator = startupHealthIndicator;
log.info("LazyService instantiated (not initialized yet)");
}
@PostConstruct
public void init() {
log.info("LazyService initialization started...");
try {
Thread.sleep(1000);
startupHealthIndicator.markComponentReady("lazy-service");
log.info("LazyService initialization completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("LazyService init interrupted", e);
}
}
public String doWork() {
return "LazyService working";
}
}
4. 控制器示例
@RestController
@RequestMapping("/api")
public class StartupController {
private static final Logger log = LoggerFactory.getLogger(StartupController.class);
private final StartupHealthIndicator startupHealthIndicator;
private final LazyService lazyService;
public StartupController(StartupHealthIndicator startupHealthIndicator,
LazyService lazyService) {
this.startupHealthIndicator = startupHealthIndicator;
this.lazyService = lazyService;
}
@GetMapping("/health-check")
public ResponseEntity<Map<String, Object>> healthCheck() {
Map<String, Object> health = new HashMap<>();
health.put("status", startupHealthIndicator.isReady() ? "READY" : "INITIALIZING");
health.put("timestamp", System.currentTimeMillis());
return ResponseEntity.ok(health);
}
@GetMapping("/trigger-lazy")
public ResponseEntity<String> triggerLazyInit() {
log.info("Triggering lazy service initialization...");
String result = lazyService.doWork();
return ResponseEntity.ok(result);
}
@GetMapping("/init-status")
public ResponseEntity<Map<String, Object>> getInitStatus() {
Map<String, Object> status = new HashMap<>();
status.put("ready", startupHealthIndicator.isReady());
return ResponseEntity.ok(status);
}
}
K8s 部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
ports:
- containerPort: 8080
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
timeoutSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
启动时间对比
| 优化策略 | 启动时间 | 健康检查响应 |
|---|---|---|
| 原始同步启动 | 15s+ | 15s后才响应 |
| 全局懒加载 | 3s | 3s后响应(部分功能未就绪) |
| 并行初始化 | 6s | 6s后响应 |
| 异步初始化 + 分阶段健康检查 | 2s | 2s后响应(存活),10s后就绪 |
最佳实践
1. 区分存活探针和就绪探针
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
- liveness:只检查应用是否存活,快速响应
- readiness:检查应用是否就绪接收流量,等待关键组件初始化完成
2. 使用 startupProbe(K8s 1.18+)
startupProbe:
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 30
periodSeconds: 10
startupProbe 会在成功后停止,并将健康检查交给 livenessProbe 和 readinessProbe。
3. 关键组件预加载
@Component
public class CriticalComponentLoader {
@EventListener(ApplicationStartedEvent.class)
public void loadCriticalComponents() {
log.info("Loading critical components...");
// 预加载关键组件
}
}
4. 启动日志分析
@Component
public class StartupLogger {
@EventListener
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartingEvent) {
log.info("Application starting...");
} else if (event instanceof ApplicationEnvironmentPreparedEvent) {
log.info("Environment prepared");
} else if (event instanceof ApplicationContextInitializedEvent) {
log.info("ApplicationContext initialized");
} else if (event instanceof ApplicationStartedEvent) {
log.info("Application started");
} else if (event instanceof ApplicationReadyEvent) {
log.info("Application ready");
}
}
}
监控与告警
1. 启动时间监控
@Component
public class StartupMetrics {
private static final Logger log = LoggerFactory.getLogger(StartupMetrics.class);
private static final long START_TIME = System.currentTimeMillis();
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
long startupTime = System.currentTimeMillis() - START_TIME;
log.info("Application startup completed in {}ms", startupTime);
if (startupTime > 10000) {
log.warn("Startup time exceeds 10s: {}ms", startupTime);
}
}
}
2. 组件初始化监控
@Component
public class ComponentInitMetrics {
private final Map<String, Long> initTimes = new ConcurrentHashMap<>();
public void recordInitTime(String componentName, long durationMs) {
initTimes.put(componentName, durationMs);
if (durationMs > 5000) {
log.warn("Component {} initialization took {}ms", componentName, durationMs);
}
}
public Map<String, Long> getInitTimes() {
return initTimes;
}
}
总结
通过懒加载优化和并行 Bean 初始化,可以显著减少 Spring Boot 应用的启动时间,避免 K8s 健康检查超时导致的 Pod 重启:
- 懒加载:将非关键组件延迟到首次使用时初始化
- 并行初始化:利用 Spring Boot 的并行特性同时初始化多个 Bean
- 异步初始化:将耗时操作放到异步线程中执行
- 分阶段健康检查:快速响应存活探针,延迟响应就绪探针
- 使用 startupProbe:K8s 1.18+ 提供的启动探针,专门处理慢启动场景
互动话题:你在 K8s 部署中遇到过哪些启动相关的问题?欢迎留言讨论!
