在云原生时代,Java微服务架构早已不是简单的Spring Boot + Netflix OSS组合。真正支撑起日均亿级流量的生产环境,需要一套融合服务网格、可观测性、弹性韧性、以及Kubernetes调度策略的完整体系。本文结合图灵Java架构师课程中的实战沉淀,从网关路由、配置动态刷新、分布式事务、链路追踪与日志关联、K8s部署策略五个维度,给出可直接落地的代码方案与性能调优参数,拒绝概念堆砌。
网关是流量的“守门员”,但绝大多数项目只用了RouteLocator基础功能。生产级网关必须支持多版本灰度、全局限流、请求级染色。
@Configuration
public class GrayRouteConfig {
@Bean
public RouteLocator grayRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("gray_service", r -> r
.header("version", "v2") // 携带v2头走灰度
.and()
.weight("service-a", 10) // 10%流量随机走灰度
.uri("lb://service-a-gray"))
.route("stable_service", r -> r
.alwaysTrue()
.uri("lb://service-a"))
.build();
}
}实际生产会结合
Metadata和DiscoveryClient动态维护实例标签,权重值通过配置中心实时调整,无需重启。
@Component
public class TraceIdFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String traceId = exchange.getRequest().getHeaders().getFirst("X-Trace-Id");
if (StringUtils.isEmpty(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
// 写入MDC,同时向下游传递
MDC.put("traceId", traceId);
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-Trace-Id", traceId)
.build();
return chain.filter(exchange.mutate().request(mutated).build())
.doFinally(signal -> MDC.clear());
}
@Override
public int getOrder() { return -1000; }
}配置漂移是微服务“慢性毒药”。我们使用Nacos作为配置中心,结合@RefreshScope + Bus实现配置变更的即时广播,但更关键的是配置回滚机制。
@Component
public class NacosConfigListener implements ApplicationListener<EnvironmentChangeEvent> {
private static final Map<String, Object> CONFIG_SNAPSHOT = new ConcurrentHashMap<>();
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
Set<String> keys = event.getKeys();
for (String key : keys) {
// 变更前备份
CONFIG_SNAPSHOT.put(key, environment.getProperty(key));
// 触发业务回调,如线程池参数调整
if (key.startsWith("thread.pool.")) {
ThreadPoolManager.refresh(key, environment.getProperty(key, Integer.class));
}
}
}
// 回滚API
public void rollback(String key) {
if (CONFIG_SNAPSHOT.containsKey(key)) {
// 通过Nacos OpenAPI 推送旧值
nacosClient.publishConfig(key, DEFAULT_GROUP, CONFIG_SNAPSHOT.get(key).toString());
}
}
}@ConfigurationProperties(prefix = "thread.pool")
@Data
public class ThreadPoolProperties {
private int coreSize = 10;
private int maxSize = 50;
private int queueCapacity = 200;
}
@Bean
@RefreshScope
public ThreadPoolExecutor bizExecutor(ThreadPoolProperties props) {
return new ThreadPoolExecutor(
props.getCoreSize(),
props.getMaxSize(),
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(props.getQueueCapacity()),
new NamedThreadFactory("biz-"),
new CallerRunsPolicy() // 背压保护
);
}AT模式对数据库侵入小,但长事务锁竞争严重。我们选用TCC模式处理核心订单链路,并引入事务日志表保证最终一致性。
@LocalTCC
public interface StockTccAction {
@TwoPhaseBusinessAction(name = "deductStock", commitMethod = "commit", rollbackMethod = "rollback")
boolean tryDeduct(@BusinessActionContextParameter(paramName = "skuId") Long skuId,
@BusinessActionContextParameter(paramName = "count") Integer count,
BusinessActionContext context);
boolean commit(BusinessActionContext context);
boolean rollback(BusinessActionContext context);
}@Component
public class StockTccActionImpl implements StockTccAction {
@Autowired
private TccTransactionLogMapper logMapper;
@Override
public boolean tryDeduct(Long skuId, Integer count, BusinessActionContext context) {
String xid = context.getXid();
// 先查日志,若已回滚则直接返回false防止悬挂
TccLog log = logMapper.selectByXid(xid);
if (log != null && log.getStatus() == TccStatus.ROLLBACKED) {
return false; // 空回滚
}
// 扣减库存(Redis预扣 + DB异步落盘)
int affected = stockService.tryFreeze(skuId, count);
if (affected > 0) {
logMapper.insert(new TccLog(xid, "TRY", TccStatus.TRYING));
return true;
}
return false;
}
@Override
public boolean commit(BusinessActionContext context) {
TccLog log = logMapper.selectByXid(context.getXid());
if (log == null || log.getStatus() == TccStatus.COMMITTED) {
return true; // 幂等
}
// 真正扣减DB
stockService.confirmDeduct(context.getActionContext("skuId"));
log.setStatus(TccStatus.COMMITTED);
logMapper.updateById(log);
return true;
}
}关键点:TCC的Try阶段必须预留资源并记录日志,Commit/Rollback需要幂等,否则重试会导致数据错乱。
很多团队把链路追踪和日志割裂,排查问题时来回切换。我们通过增强SkyWalking的log4j2插件,将traceId和segmentId自动打入日志,并索引到ES。
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%traceId] [%tid] %-5level %logger{36} - %msg%n"/>
</Console>
<Elasticsearch name="esAppender"
indexName="app-logs-${date:yyyy-MM}"
servers="es-cluster:9200">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %traceId %tid %level %logger %msg"/>
</Elasticsearch>
</Appenders>@Aspect
@Component
public class BizSpanAspect {
@Around("@annotation(io.swagger.annotations.ApiOperation)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Tracer tracer = GlobalTracer.get();
Span span = tracer.activeSpan();
if (span != null) {
span.setTag("biz.method", pjp.getSignature().getName());
// 提取订单号等
Object[] args = pjp.getArgs();
for (Object arg : args) {
if (arg instanceof OrderRequest) {
span.setTag("order.id", ((OrderRequest) arg).getOrderId());
}
}
}
return pjp.proceed();
}
}这样,在SkyWalking UI中可以根据order.id直接检索整个调用链,并关联到对应时间段的日志,排查效率提升80%。
K8s默认的滚动更新会造成短暂503,因为Pod销毁时Endpoint尚未摘除。我们的根治方案:
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15 && /app/shutdown.sh"]
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
failureThreshold: 3同时,在Spring Boot中配置server.shutdown=graceful,并设置spring.lifecycle.timeout-per-shutdown-phase=30s。
externalTrafficPolicy: Local配合terminationGracePeriodSecondsapiVersion: v1
kind: Service
metadata:
name: biz-service
spec:
externalTrafficPolicy: Local
publishNotReadyAddresses: false # 关键:就绪前不注册Endpoint
ports:
- port: 8080
selector:
app: biz-service@Component
public class GracefulShutdown implements SmartLifecycle {
private volatile boolean running = true;
private final ThreadPoolExecutor executor;
@PreDestroy
public void shutdown() {
running = false;
executor.shutdown();
try {
if (!executor.awaitTermination(20, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 关闭数据库连接池
HikariDataSource ds = (HikariDataSource) dataSource;
ds.close();
}
}压测时最常见的OOM和GC停顿,我们通过G1GC参数调整和业务线程池独立隔离来解决。
-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m -XX:InitiatingHeapOccupancyPercent=45
-XX:+PrintGCDetails -Xloggc:/logs/gc.log
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump当系统负载超过70%时,自动将CallerRunsPolicy切换为AbortPolicy并报警,防止雪崩。
public class AdaptiveRejectPolicy implements RejectedExecutionHandler {
private volatile RejectedExecutionHandler current = new CallerRunsPolicy();
@Scheduled(fixedDelay = 5000)
public void adjust() {
double load = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
if (load > 0.7 * Runtime.getRuntime().availableProcessors()) {
current = new AbortPolicy();
alarmService.send("线程池切换为拒绝抛出,当前负载:" + load);
} else {
current = new CallerRunsPolicy();
}
}
}维度 | 传统方案 | 本架构方案 | 吞吐量提升 |
|---|---|---|---|
网关灰度 | 需重启或Nginx分流 | 动态权重+Header路由,秒级生效 | +30% 路由效率 |
配置更新 | 重启生效 | 长轮询+热刷新+回滚 | 零停机,MTTR降低60% |
分布式事务 | AT模式锁行 | TCC+防悬挂,异步化 | TPS从800→2200 |
可观测性 | 分散查询 | TraceId贯穿日志+自定义Tag | 排障时间从30min→5min |
K8s滚动更新 | 有503 | 就绪探针+preStop延迟摘除 | 零错误发布 |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。