跳转到内容
123xiao | 无名键客

《从源码到部署:基于开源项目构建可观测微服务的实战指南》

字数: 0 阅读时长: 1 分钟

从源码到部署:基于开源项目构建可观测微服务的实战指南

很多团队做微服务时,第一阶段通常都很顺:服务能跑、接口能通、容器能起。但一到联调、压测、线上灰度,问题就开始集中爆发:

  • 某个接口突然变慢,到底慢在哪一层?
  • 调用链跨了 3 个服务,异常日志散在不同容器里,根本串不起来
  • CPU 不高但用户超时严重,怀疑是数据库连接池或线程池配置有问题
  • 服务在本地正常,部署后却没有指标、没有 trace、日志也没结构化

说白了,系统“能跑”不等于“可运营”。而可观测性(Observability)正是把“黑盒运行”变成“可理解运行”的关键能力。

这篇文章我不打算只讲概念,而是按一个更接地气的路径来:从源码启动一个开源微服务,到接入指标、日志、链路追踪,再到容器化部署与排障。你可以把它当成一份可跟着操作的实战教程。


背景与问题

在中级工程实践里,很多人已经会:

  • 用 Spring Boot 或类似框架写服务
  • 用 Docker 打包部署
  • 配 Nginx、MySQL、Redis
  • 接一点基础日志

但如果没有完整的可观测性体系,问题仍然很难定位。

这里我们选一个最小但完整的技术组合,避免一上来就把系统堆得过重:

  • 微服务框架:Spring Boot
  • 指标采集:Micrometer + Prometheus
  • 可视化面板:Grafana
  • 链路追踪:OpenTelemetry + Jaeger
  • 日志:Logback + JSON 输出
  • 容器编排:Docker Compose

为什么这么选?

  1. 全是开源项目
  2. 社区成熟,资料多
  3. 既适合本地演练,也能平滑迁移到 Kubernetes
  4. 足够覆盖“指标 + 日志 + trace”三件套

我们会实现两个微服务:

  • order-service:订单服务
  • inventory-service:库存服务

下单时,订单服务会调用库存服务扣减库存。这样自然形成一条调用链,方便我们观察 trace、日志与指标。


前置知识

开始前,你最好具备这些基础:

  • 会读简单的 Spring Boot 项目结构
  • 知道 REST API 是怎么调用的
  • 了解 Docker 和 Docker Compose 的基本命令
  • 对 Prometheus / Grafana / Jaeger 听说过,不要求很熟

如果你刚接触 OpenTelemetry,也不用担心,本文会直接给可运行配置。


环境准备

建议环境:

  • JDK 17
  • Maven 3.8+
  • Docker 20+
  • Docker Compose 2+
  • curl 或 Postman

目录结构我建议这样组织:

observable-microservices/
├── order-service/
├── inventory-service/
├── docker-compose.yml
├── prometheus.yml
└── grafana/

核心原理

可观测性最常见的三个支柱是:

  • Metrics(指标):告诉你“系统整体是否正常”
  • Logs(日志):告诉你“具体发生了什么”
  • Traces(链路):告诉你“一个请求经过了哪些服务、每段耗时多少”

这三者各管一部分,单独用都不够,联合起来才真正有效。

1. 指标:从“感觉慢”变成“量化慢”

Prometheus 定期拉取服务暴露的 /actuator/prometheus 指标,比如:

  • 请求总数
  • 响应时延
  • JVM 内存
  • 线程池状态
  • 数据源连接池

如果一个接口 P95 延迟突增,通常先看指标能快速判断是全局变慢,还是某个依赖变慢。

2. 日志:从“报错了”变成“上下文完整”

日志如果只是普通文本,线上检索成本会很高。比较好的做法是:

  • 输出 JSON
  • 带上 traceIdspanId
  • 保留业务关键字段,比如 orderIdskuCode

这样一条异常日志就能和一条 trace 串起来。

3. 链路追踪:从“猜测瓶颈”变成“定位瓶颈”

一个请求从网关进入订单服务,再调用库存服务,最终访问数据库。Trace 能展示:

  • 哪些服务参与了调用
  • 每一步耗时
  • 哪个 span 抛了异常
  • 下游调用是快还是慢

一张图看整体架构

flowchart LR
    U[User / Client] --> O[order-service]
    O --> I[inventory-service]
    O --> P[(Prometheus Metrics Endpoint)]
    I --> P
    P --> G[Grafana]
    O --> J[Jaeger]
    I --> J
    O --> L[Structured Logs]
    I --> L

这张图表达的是:两个服务既处理业务,也持续输出“可观测数据”。


调用链时序图

sequenceDiagram
    participant C as Client
    participant O as order-service
    participant I as inventory-service
    participant J as Jaeger
    participant P as Prometheus

    C->>O: POST /orders
    O->>I: POST /inventory/deduct
    I-->>O: success
    O-->>C: order created

    O->>J: export trace/span
    I->>J: export trace/span
    P->>O: scrape /actuator/prometheus
    P->>I: scrape /actuator/prometheus

逐步搭建:从源码写出可运行微服务

下面我们从零做两个服务。为了控制篇幅,我用最小实现,但保留可运行性。


第一步:创建 inventory-service

inventory-service/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>inventory-service</artifactId>
    <version>1.0.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>

        <dependency>
            <groupId>io.opentelemetry.instrumentation</groupId>
            <artifactId>opentelemetry-spring-boot-starter</artifactId>
            <version>2.4.0</version>
        </dependency>
    </dependencies>
</project>

inventory-service/src/main/java/com/example/inventory/InventoryApplication.java

package com.example.inventory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class InventoryApplication {
    public static void main(String[] args) {
        SpringApplication.run(InventoryApplication.class, args);
    }
}

inventory-service/src/main/java/com/example/inventory/InventoryController.java

package com.example.inventory;

import io.micrometer.core.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/inventory")
public class InventoryController {

    private static final Logger log = LoggerFactory.getLogger(InventoryController.class);

    @PostMapping("/deduct")
    @Timed(value = "inventory_deduct_duration", description = "Inventory deduct duration")
    public Map<String, Object> deduct(@RequestParam String skuCode, @RequestParam Integer count) throws InterruptedException {
        log.info("deduct inventory, skuCode={}, count={}", skuCode, count);

        if (count <= 0) {
            throw new IllegalArgumentException("count must be greater than 0");
        }

        Thread.sleep(100);

        return Map.of(
                "success", true,
                "skuCode", skuCode,
                "count", count
        );
    }
}

inventory-service/src/main/resources/application.yml

server:
  port: 8081

spring:
  application:
    name: inventory-service

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      show-details: always

otel:
  service:
    name: inventory-service
  exporter:
    otlp:
      endpoint: http://jaeger:4318
  traces:
    exporter: otlp

第二步:创建 order-service

order-service/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>order-service</artifactId>
    <version>1.0.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>

        <dependency>
            <groupId>io.opentelemetry.instrumentation</groupId>
            <artifactId>opentelemetry-spring-boot-starter</artifactId>
            <version>2.4.0</version>
        </dependency>
    </dependencies>
</project>

order-service/src/main/java/com/example/order/OrderApplication.java

package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

order-service/src/main/java/com/example/order/OrderController.java

package com.example.order;

import io.micrometer.core.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/orders")
public class OrderController {

    private static final Logger log = LoggerFactory.getLogger(OrderController.class);
    private final RestTemplate restTemplate;

    public OrderController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @PostMapping
    @Timed(value = "order_create_duration", description = "Order create duration")
    public Map<String, Object> createOrder(@RequestParam String skuCode, @RequestParam Integer count) {
        String orderId = UUID.randomUUID().toString();
        log.info("create order start, orderId={}, skuCode={}, count={}", orderId, skuCode, count);

        String url = "http://inventory-service:8081/inventory/deduct?skuCode=" + skuCode + "&count=" + count;
        Map inventoryResp = restTemplate.postForObject(url, null, Map.class);

        log.info("create order success, orderId={}, inventoryResp={}", orderId, inventoryResp);

        return Map.of(
                "success", true,
                "orderId", orderId,
                "inventory", inventoryResp
        );
    }
}

order-service/src/main/resources/application.yml

server:
  port: 8080

spring:
  application:
    name: order-service

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      show-details: always

otel:
  service:
    name: order-service
  exporter:
    otlp:
      endpoint: http://jaeger:4318
  traces:
    exporter: otlp

第三步:加入结构化日志

严格说,完整的日志平台通常会接 ELK 或 Loki。不过为了让这篇教程聚焦,我先做最关键的一步:日志里保留 trace 上下文。这样哪怕先用 docker logs 看,也已经比纯文本强很多。

order-service/src/main/resources/logback-spring.xml

<configuration>
    <property name="CONSOLE_LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] traceId=%X{trace_id} spanId=%X{span_id} %logger - %msg%n"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

inventory-service/src/main/resources/logback-spring.xml

<configuration>
    <property name="CONSOLE_LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] traceId=%X{trace_id} spanId=%X{span_id} %logger - %msg%n"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

我个人经验是:traceId 一旦进日志,排障效率会明显提高。哪怕你暂时没上 ELK,这一步也很值。


第四步:编写 Dockerfile

order-service/Dockerfile

FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/order-service-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

inventory-service/Dockerfile

FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/inventory-service-1.0.0.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]

第五步:部署 Prometheus、Grafana、Jaeger

prometheus.yml

global:
  scrape_interval: 5s

scrape_configs:
  - job_name: 'order-service'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['order-service:8080']

  - job_name: 'inventory-service'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['inventory-service:8081']

docker-compose.yml

version: '3.9'

services:
  order-service:
    build: ./order-service
    container_name: order-service
    ports:
      - "8080:8080"
    depends_on:
      - inventory-service
      - jaeger

  inventory-service:
    build: ./inventory-service
    container_name: inventory-service
    ports:
      - "8081:8081"
    depends_on:
      - jaeger

  prometheus:
    image: prom/prometheus:v2.52.0
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:10.4.2
    container_name: grafana
    ports:
      - "3000:3000"

  jaeger:
    image: jaegertracing/all-in-one:1.57
    container_name: jaeger
    ports:
      - "16686:16686"
      - "4318:4318"

第六步:启动整个系统

在项目根目录执行:

docker compose up --build

看到服务都正常启动后,访问:

  • 订单服务:http://localhost:8080/actuator/health
  • 库存服务:http://localhost:8081/actuator/health
  • Prometheus:http://localhost:9090
  • Grafana:http://localhost:3000
  • Jaeger:http://localhost:16686

第七步:发起一次业务请求

curl -X POST "http://localhost:8080/orders?skuCode=SKU-1001&count=2"

如果成功,你会得到类似响应:

{
  "success": true,
  "orderId": "7c6f1e80-4f6a-4d24-b8e4-0d8f1a8c3456",
  "inventory": {
    "success": true,
    "skuCode": "SKU-1001",
    "count": 2
  }
}

第八步:验证可观测数据是否完整

这个步骤非常重要。很多人服务启动起来就以为接入成功了,实际上 trace 没上报、指标没抓到、日志没有上下文,最后排障时才发现链条断了。

验证清单

1. 验证指标

浏览器打开:

  • http://localhost:8080/actuator/prometheus
  • http://localhost:8081/actuator/prometheus

确认能看到类似:

http_server_requests_seconds_count
jvm_memory_used_bytes
order_create_duration_seconds_count
inventory_deduct_duration_seconds_count

在 Prometheus 页面查询:

http_server_requests_seconds_count

2. 验证链路

打开 Jaeger:

  • http://localhost:16686

选择服务 order-service,点击 Find Traces。你应该能看到一条请求链路,里面包含:

  • order-service
  • inventory-service

3. 验证日志

查看容器日志:

docker logs order-service
docker logs inventory-service

确认日志中有:

  • traceId=...
  • spanId=...

可观测组件之间的关系

classDiagram
    class Microservice {
      +REST API
      +Business Logic
      +Actuator Endpoint
      +Trace Export
      +Structured Logs
    }

    class Prometheus {
      +scrape metrics
    }

    class Grafana {
      +dashboards
      +visual query
    }

    class Jaeger {
      +trace store
      +trace search
    }

    Microservice --> Prometheus : expose metrics
    Prometheus --> Grafana : data source
    Microservice --> Jaeger : export spans

实战代码补强:加入自定义业务指标

默认 JVM 和 HTTP 指标已经很有用,但业务指标更直接。比如我们记录“订单创建总数”。

order-service/src/main/java/com/example/order/OrderMetrics.java

package com.example.order;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;

@Component
public class OrderMetrics {

    private final Counter orderCreatedCounter;

    public OrderMetrics(MeterRegistry meterRegistry) {
        this.orderCreatedCounter = Counter.builder("business_order_created_total")
                .description("Total created orders")
                .register(meterRegistry);
    }

    public void incrementOrderCreated() {
        orderCreatedCounter.increment();
    }
}

修改 OrderController.java

package com.example.order;

import io.micrometer.core.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/orders")
public class OrderController {

    private static final Logger log = LoggerFactory.getLogger(OrderController.class);
    private final RestTemplate restTemplate;
    private final OrderMetrics orderMetrics;

    public OrderController(RestTemplate restTemplate, OrderMetrics orderMetrics) {
        this.restTemplate = restTemplate;
        this.orderMetrics = orderMetrics;
    }

    @PostMapping
    @Timed(value = "order_create_duration", description = "Order create duration")
    public Map<String, Object> createOrder(@RequestParam String skuCode, @RequestParam Integer count) {
        String orderId = UUID.randomUUID().toString();
        log.info("create order start, orderId={}, skuCode={}, count={}", orderId, skuCode, count);

        String url = "http://inventory-service:8081/inventory/deduct?skuCode=" + skuCode + "&count=" + count;
        Map inventoryResp = restTemplate.postForObject(url, null, Map.class);

        orderMetrics.incrementOrderCreated();

        log.info("create order success, orderId={}, inventoryResp={}", orderId, inventoryResp);

        return Map.of(
                "success", true,
                "orderId", orderId,
                "inventory", inventoryResp
        );
    }
}

然后在 Prometheus 中查询:

business_order_created_total

这类指标特别适合做业务看板,比如每分钟下单量、成功率、失败率。


Grafana 面板建议

Grafana 里建议先做 4 类基础图表:

  1. 接口吞吐

    • rate(http_server_requests_seconds_count[1m])
  2. 接口延迟

    • histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))
  3. JVM 内存

    • jvm_memory_used_bytes
  4. 业务订单数

    • business_order_created_total

如果你是第一次搭面板,不用追求“大而全”,先把这几个关键图跑起来,排障时就已经很有帮助。


常见坑与排查

这一节我尽量写得像真实排障,因为这些问题我确实见过不少。

1. Jaeger 里看不到 trace

现象

请求已经打通,但 Jaeger 页面没有任何链路数据。

排查顺序

  1. 检查 otel.exporter.otlp.endpoint 是否写对
  2. 确认 Jaeger 是否开放了 4318 端口
  3. 看应用日志中是否有 exporter 报错
  4. 确认请求确实经过了被自动埋点的组件

常见原因

  • 写成了 localhost:4318。在容器里,localhost 指向容器自身,不是 Jaeger。
  • 用了错误协议端口。
  • 依赖版本不兼容。

建议

容器互联时,优先使用服务名:

otel:
  exporter:
    otlp:
      endpoint: http://jaeger:4318

2. Prometheus 抓不到指标

现象

Prometheus 的 Targets 页面显示 DOWN

排查顺序

  1. 打开服务自身的 /actuator/prometheus
  2. 检查 management.endpoints.web.exposure.include
  3. 检查 prometheus.yml 的目标地址是否是容器名
  4. 查看服务是否真的监听了对应端口

常见原因

  • 忘了引入 micrometer-registry-prometheus
  • Actuator 端点没暴露
  • 写成 localhost:8080,而 Prometheus 容器访问不到宿主机 localhost

3. traceId 没进日志

现象

Jaeger 有链路,但日志里没有 traceId

排查方向

  • 检查日志 pattern 是否包含 %X{trace_id}%X{span_id}
  • 检查 OpenTelemetry starter 是否生效
  • 确认日志框架是 Logback 而不是别的实现覆盖了

4. 调用链断掉,只看到 order-service

现象

Jaeger 里只有上游服务,看不到下游 inventory-service span。

常见原因

  • 下游服务没接 OpenTelemetry
  • HTTP 客户端未被正确自动埋点
  • 自定义客户端绕过了框架默认拦截

如果你后面换成 WebClient、Feign 或 OkHttp,也要验证上下游 trace 传播是否仍然成立,不要想当然。


5. 指标太多,Grafana 查询很慢

原因

默认指标很多,如果标签维度控制不好,时间序列会迅速膨胀。

典型错误

把高基数字段当成标签,比如:

  • orderId
  • userId
  • traceId

这些值几乎每次都不同,会让 Prometheus 压力激增。

原则

指标适合聚合,不适合记录每个请求的唯一标识。 唯一标识应放到日志和 trace,不要放到 metrics label。


安全/性能最佳实践

这一节很重要,因为“可观测性本身”也会影响系统。

1. 不要把所有 Actuator 端点都暴露出去

很多教程图省事会写:

management:
  endpoints:
    web:
      exposure:
        include: "*"

本地调试还行,线上不建议。更稳妥的是只开放必要端点:

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

如果是公网环境,最好再配访问控制,至少不要裸奔。


2. 日志里不要打印敏感信息

尤其避免直接输出:

  • 用户手机号
  • 身份证号
  • token
  • 密码
  • 完整 SQL 参数中的隐私字段

我见过最离谱的一次,是排查方便把整个请求头都打出来,结果把认证信息带进日志。这个坑真的不值当。

建议做法:

  • 对敏感字段脱敏
  • 仅记录必要业务上下文
  • 错误堆栈只在服务端保留,不回传给前端

3. 控制 trace 采样率

不是每个请求都必须全量追踪,特别是高 QPS 服务。

例如生产环境可以考虑:

  • 低流量核心服务:较高采样率
  • 高流量接口:按比例采样
  • 异常请求:优先保留

如果全量采样,链路系统本身会吃掉不少网络和存储成本。


4. 指标命名遵循统一规范

建议:

  • 用业务前缀
  • 单位写清楚
  • 含义单一

比如:

  • business_order_created_total
  • inventory_deduct_duration_seconds

而不要写这种模糊名:

  • order_count
  • time_cost
  • metric1

名字一旦乱,后面面板和告警都会越来越难维护。


5. 业务日志、访问日志、错误日志分层

如果项目再大一点,我建议至少分三类:

  • 业务事件日志
  • HTTP 访问日志
  • 错误异常日志

否则所有日志混在一起,关键异常会被大量普通信息淹没。


6. 给超时、重试和线程池加指标

可观测性不是只盯 HTTP 请求。真正影响稳定性的,往往是这些内部资源:

  • 线程池活跃线程数
  • 队列积压
  • 数据库连接池使用率
  • 下游调用超时次数
  • 重试次数

如果后续你接数据库、消息队列、缓存,这些指标要尽快补上。


从本地到生产:部署边界怎么把握

本文用的是 Docker Compose,这很适合:

  • 本地学习
  • 小型测试环境
  • 单机演示
  • 团队内部 PoC

但如果你进入生产环境,通常会演进到:

  • Kubernetes 部署服务
  • Prometheus Operator 管理监控
  • Grafana 做统一看板
  • Tempo / Jaeger / SkyWalking 做链路
  • Loki / ELK 做日志平台
  • Alertmanager 做告警路由

这里的关键不是“立刻上全家桶”,而是先把最小链路打通:

  1. 服务暴露指标
  2. 请求带 trace
  3. 日志能关联 traceId
  4. 关键看板和告警先跑起来

只要这四点做到,系统的“可维护性”就已经和纯黑盒状态完全不同了。


一份实用的逐步验证清单

如果你打算把本文方案真正落地,我建议照着这份清单做,不容易漏项。

功能验证

  • order-service 启动成功
  • inventory-service 启动成功
  • 下单接口能返回成功
  • 错误参数能触发异常路径

指标验证

  • /actuator/prometheus 可访问
  • Prometheus Targets 全部 UP
  • 能查到 http_server_requests_seconds_count
  • 能查到 business_order_created_total

链路验证

  • Jaeger 能看到 order-service
  • trace 中包含 inventory-service
  • 异常请求也能看到错误 span

日志验证

  • 日志带 traceId
  • 日志带 spanId
  • 关键业务字段可检索
  • 不包含敏感信息

运维验证

  • 容器重启后系统可恢复
  • Prometheus 配置变更可生效
  • Grafana 数据源可正常连接
  • 服务依赖异常时有足够观测数据

总结

从源码到部署,构建一个可观测微服务,核心不是多装几个组件,而是把这条链真正连通:

  • 指标告诉你系统是否异常
  • 日志告诉你具体发生了什么
  • 链路告诉你问题在哪一跳发生

本文这套方案的价值在于,它不是纸上谈兵,而是一个能本地跑起来的最小闭环:

  1. 写两个微服务
  2. 暴露 Prometheus 指标
  3. 输出带 traceId 的日志
  4. 用 OpenTelemetry 上报链路
  5. 用 Docker Compose 整体部署
  6. 通过一次真实请求完成验证

如果你准备在自己的项目里落地,我的建议是:

  • 先做最小闭环,不要一开始就追求全量平台化
  • 先保证 trace、metrics、logs 能互相关联
  • 先覆盖核心业务链路,再逐步扩展到数据库、缓存、MQ
  • 生产环境重点关注采样率、标签基数、敏感信息和端点暴露

边界条件也要明确:

  • 这套 Compose 方案适合学习、测试和 PoC,不是最终生产形态
  • 日志这里只做了控制台结构增强,真正生产通常还要配日志采集系统
  • 指标和 trace 默认配置偏演示,生产需要结合流量规模做精细化调优

如果你按这篇文章完整走一遍,最大的收获通常不是“我会用了 Prometheus/Jaeger”,而是你开始真正建立起一种工程意识:服务上线,不只是能提供接口,还要能解释自己为什么正常、为什么失败、为什么变慢。 这才是微服务走向可运营、可维护的关键一步。


分享到:

上一篇
《Node.js 中基于 Worker Threads 与消息队列的高并发任务处理实战-490》
下一篇
《Node.js 中基于 Worker Threads 与消息队列的 CPU 密集型任务处理实战》