微服务不是把一个单体应用拆成多个 Spring Boot 项目就结束了。真正进入生产环境后,系统会面对更多运行时问题:服务实例会扩缩容,网络会抖动,下游接口会变慢,配置需要动态调整,链路需要追踪,故障需要被隔离。

这篇文章以 Java 后端常见技术栈为背景,梳理一套微服务治理的落地思路。重点不是堆砌组件名,而是回答几个工程问题:

  • 服务如何被发现和调用?

  • 配置如何统一管理并动态生效?

  • 网关应该承担哪些职责?

  • 下游服务变慢时,调用方如何自我保护?

  • 如何通过日志、指标和链路追踪定位问题?

一、微服务治理解决的核心问题

单体应用里,大部分方法调用都发生在同一个进程内。拆成微服务后,一个业务流程往往需要跨多个进程、多个数据库、多个网络边界完成。

例如用户下单可能涉及:

  1. 订单服务创建订单。

  2. 库存服务扣减库存。

  3. 优惠券服务核销优惠券。

  4. 支付服务生成支付单。

  5. 消息服务发送通知。

拆分带来独立发布、独立扩展和职责清晰的好处,但也引入了新的复杂度:

问题 典型表现
服务发现 实例数量变化后,调用方不知道应该请求哪台机器
负载均衡 流量分配不均,某些实例压力过高
配置管理 多个服务配置分散,修改后难以统一发布
调用保护 下游超时拖垮上游,故障沿调用链扩散
灰度发布 新版本上线缺少可控流量入口
可观测性 出问题后只能翻日志,难以还原完整请求链路

微服务治理的目标,就是让服务在动态环境里仍然具备可发现、可控制、可恢复、可观测的能力。

二、注册中心:服务实例的通讯录

注册中心用于维护服务名和实例地址之间的关系。服务启动时把自己的地址注册上去,调用方根据服务名获取可用实例。

常见选择包括 Nacos、Eureka、Consul、Zookeeper。Java 业务系统中,Spring Cloud Alibaba Nacos 是比较常见的组合。

一个订单服务的注册配置可以这样写:

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: dev
        group: DEFAULT_GROUP

库存服务也注册到同一个注册中心:

spring:
  application:
    name: stock-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

这样订单服务就不需要写死库存服务的 IP 和端口,而是通过服务名调用。

三、OpenFeign:把远程调用写得像本地接口

在 Java 微服务里,OpenFeign 经常用来声明式调用 HTTP 服务。它的优点是接口清晰,调用代码简洁,便于统一配置超时、日志和拦截器。

库存服务提供扣减接口:

@RestController
@RequestMapping("/stocks")
public class StockController {
​
    private final StockService stockService;
​
    public StockController(StockService stockService) {
        this.stockService = stockService;
    }
​
    @PostMapping("/deduct")
    public ApiResponse<Void> deduct(@RequestBody DeductStockRequest request) {
        stockService.deduct(request.getSkuId(), request.getQuantity());
        return ApiResponse.success();
    }
}

订单服务中定义 Feign Client:

@FeignClient(
        name = "stock-service",
        path = "/stocks",
        configuration = StockFeignConfig.class
)
public interface StockClient {
​
    @PostMapping("/deduct")
    ApiResponse<Void> deduct(@RequestBody DeductStockRequest request);
}

业务中调用:

@Service
public class OrderService {
​
    private final StockClient stockClient;
    private final OrderRepository orderRepository;
​
    public OrderService(StockClient stockClient, OrderRepository orderRepository) {
        this.stockClient = stockClient;
        this.orderRepository = orderRepository;
    }
​
    @Transactional
    public Long createOrder(CreateOrderCommand command) {
        Order order = Order.create(command.getUserId(), command.getSkuId(), command.getQuantity());
        orderRepository.save(order);
​
        DeductStockRequest request = new DeductStockRequest(order.getSkuId(), order.getQuantity());
        stockClient.deduct(request);
​
        return order.getId();
    }
}

这段代码看起来像本地方法调用,但本质上仍然是网络请求。因此必须认真配置超时、重试和异常处理。

四、远程调用的第一原则:必须有超时

微服务调用中最危险的情况不是失败,而是一直不返回。一个下游接口如果长时间卡住,会占满上游线程池,最终让看似无关的接口也开始变慢。

Feign 超时配置示例:

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connectTimeout: 1000
            readTimeout: 3000
          stock-service:
            connectTimeout: 500
            readTimeout: 1500

超时设置要结合业务场景:

调用类型 建议策略
核心同步链路 短超时,失败后快速返回明确错误
查询类接口 可适当重试,但要控制总耗时
通知类接口 不放在主链路里,改为异步消息
第三方接口 使用隔离线程池,避免拖垮内部服务

不要把超时时间配置得很长来掩盖慢接口。慢接口应该通过缓存、SQL 优化、异步化或容量扩展解决。

五、网关:统一入口,而不是业务垃圾桶

API 网关通常位于客户端和内部微服务之间,负责统一入口治理。Spring Cloud Gateway 是 Java 微服务中常见的选择。

一个基础路由配置如下:

spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1
​
        - id: stock-service
          uri: lb://stock-service
          predicates:
            - Path=/api/stocks/**
          filters:
            - StripPrefix=1

网关适合承担这些职责:

  • 路由转发。

  • 统一认证鉴权。

  • 限流和黑白名单。

  • 请求日志和 TraceId 注入。

  • 跨域处理。

  • 灰度发布和流量染色。

网关不适合承担复杂业务逻辑。例如订单金额计算、库存扣减、会员等级判断不应该放在网关。网关一旦混入业务,后续会变成另一个难以拆分的单体入口。

六、熔断:避免故障沿调用链扩散

当下游服务持续异常或响应过慢时,上游服务应该停止继续请求它一段时间,直接走降级逻辑。这就是熔断。

以 Resilience4j 为例,可以为库存服务调用配置熔断器:

resilience4j:
  circuitbreaker:
    instances:
      stockService:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 20
        minimumNumberOfCalls: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 3

调用侧代码:

@Service
public class StockGateway {
​
    private final StockClient stockClient;
​
    public StockGateway(StockClient stockClient) {
        this.stockClient = stockClient;
    }
​
    @CircuitBreaker(name = "stockService", fallbackMethod = "deductFallback")
    public void deduct(Long skuId, Integer quantity) {
        stockClient.deduct(new DeductStockRequest(skuId, quantity));
    }
​
    public void deductFallback(Long skuId, Integer quantity, Throwable ex) {
        throw new BusinessException("库存服务暂时不可用,请稍后重试");
    }
}

熔断不是为了让错误消失,而是为了让故障可控。对于强一致要求高的操作,降级逻辑通常是明确失败;对于非核心查询,可以返回缓存数据或默认值。

七、限流:保护系统的入口水位

限流用于控制单位时间内进入系统的请求数量。它解决的是系统无法无限承压的问题。

常见限流维度包括:

维度 示例
接口维度 /orders/create 每秒最多 500 次
用户维度 单个用户每分钟最多提交 10 次订单
IP 维度 单个 IP 每秒最多 50 次请求
租户维度 不同租户按套餐限制调用量

在网关中可以使用 Redis RateLimiter:

spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100
                redis-rate-limiter.burstCapacity: 200
                key-resolver: "#{@userKeyResolver}"

KeyResolver 示例:

@Configuration
public class RateLimitConfig {
​
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
            return Mono.just(userId == null ? "anonymous" : userId);
        };
    }
}

限流后的返回要清晰。对于前端用户,可以提示稍后再试;对于开放 API,可以返回标准错误码和重试建议。

八、隔离:不要让一个下游拖垮所有请求

隔离的目标是限制故障影响范围。常见方式包括:

  • 线程池隔离:不同业务使用不同线程池。

  • 连接池隔离:不同下游使用独立 HTTP 连接池。

  • 数据隔离:核心库和报表库分离。

  • 实例隔离:核心服务和后台任务分开部署。

例如订单服务同时调用库存、优惠券和通知服务。如果通知服务响应慢,不应该占满订单主流程的线程资源。通知这类非核心动作更适合放到消息队列中异步处理:

@Transactional
public Long createOrder(CreateOrderCommand command) {
    Order order = Order.create(command.getUserId(), command.getSkuId(), command.getQuantity());
    orderRepository.save(order);
​
    domainEventPublisher.publish(new OrderCreatedEvent(order.getId()));
​
    return order.getId();
}

异步消费者处理通知:

@Component
public class OrderCreatedConsumer {
​
    private final NotifyService notifyService;
​
    public OrderCreatedConsumer(NotifyService notifyService) {
        this.notifyService = notifyService;
    }
​
    @RabbitListener(queues = "order.created.notify")
    public void onMessage(OrderCreatedEvent event) {
        notifyService.sendOrderCreatedMessage(event.getOrderId());
    }
}

这样主链路只负责创建订单,通知失败不会直接影响下单结果。

九、配置中心:让配置可管理、可审计

配置中心用于统一管理不同服务、不同环境的配置。它适合存放:

  • 数据库连接配置。

  • Redis、MQ、第三方服务地址。

  • 业务开关。

  • 灰度规则。

  • 限流阈值。

  • 日志级别。

Nacos 配置示例:

spring:
  config:
    import:
      - optional:nacos:order-service.yaml
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        namespace: dev
        group: DEFAULT_GROUP

读取动态配置:

@RefreshScope
@Component
@ConfigurationProperties(prefix = "order.risk")
public class OrderRiskProperties {
​
    private BigDecimal maxAmount = new BigDecimal("5000");
    private boolean enabled = true;
​
    public BigDecimal getMaxAmount() {
        return maxAmount;
    }
​
    public void setMaxAmount(BigDecimal maxAmount) {
        this.maxAmount = maxAmount;
    }
​
    public boolean isEnabled() {
        return enabled;
    }
​
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
}

配置中心要注意权限和审计。生产环境配置变更应该有审批、记录和回滚方案,不能把配置中心当成随手改线上行为的入口。

十、可观测性:让每一次请求都有迹可循

微服务系统排查问题时,单个服务日志往往不够。需要把日志、指标和链路追踪结合起来。

常见三件套:

类型 关注内容
Logging 某次请求发生了什么
Metrics 系统整体是否健康
Tracing 请求经过了哪些服务,每一步耗时多少

日志中至少应该包含 TraceId:

@Component
public class TraceIdFilter implements Filter {
​
    private static final String TRACE_ID = "traceId";
​
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String traceId = UUID.randomUUID().toString().replace("-", "");
        MDC.put(TRACE_ID, traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove(TRACE_ID);
        }
    }
}

日志格式中打印 TraceId:

logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger{36} - %msg%n"

在更完整的体系中,可以使用 Micrometer Tracing、OpenTelemetry、Prometheus、Grafana、SkyWalking 或 Zipkin 来构建观测平台。

十一、灰度发布:让新版本先小范围验证

微服务上线不能只依赖一次性全量发布。灰度发布可以让新版本先承接一小部分流量,确认稳定后再逐步放量。

常见灰度策略:

  • 按用户 ID 灰度。

  • 按租户灰度。

  • 按请求头灰度。

  • 按地域灰度。

  • 按随机比例灰度。

例如网关根据请求头把流量路由到带有 version=v2 元数据的实例:

spring:
  cloud:
    nacos:
      discovery:
        metadata:
          version: v2

灰度的关键不是路由规则本身,而是配套能力:

  • 可以快速调整灰度比例。

  • 可以快速回滚。

  • 新旧版本数据结构兼容。

  • 监控能区分不同版本的错误率和响应时间。

十二、生产落地清单

微服务治理上线前,可以用下面的清单做检查:

检查项 要点
注册发现 服务名稳定,实例上下线能自动感知
超时配置 所有远程调用都有连接超时和读取超时
重试策略 只对幂等操作重试,限制最大重试次数
熔断降级 核心下游有熔断策略和清晰降级结果
限流保护 网关和核心接口有入口流量保护
线程隔离 慢任务、第三方调用和核心链路资源隔离
配置中心 配置分环境管理,生产变更可审计
日志规范 日志包含 TraceId、业务主键和错误上下文
指标监控 接口 RT、错误率、线程池、连接池、JVM 指标可见
灰度发布 新版本可小流量验证,可快速回滚

总结

Java 微服务治理的核心不是使用多少组件,而是围绕运行时稳定性建立一套工程约束。注册发现解决服务定位,网关统一入口,配置中心管理动态参数,熔断限流控制故障影响,可观测性帮助快速定位问题。

一个成熟的微服务系统应该做到:服务变多但调用关系仍然清晰,实例变化但访问入口仍然稳定,下游异常但故障不会无限扩散,线上出问题时能通过数据而不是猜测定位原因。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐