第5章:从零搭建多服务.NET应用:亲手组装你的第一个“微服务乐高”

本章学完你将能:告别手动管理多个容器的混乱,像指挥交响乐一样,用一份配置文件一键编排、启动、管理由多个服务(你的.NET应用、数据库、缓存等)组成的完整应用栈。这是从“玩容器”到“用容器架构”的关键一步。


一、开篇:当微服务从“一个”变成“一群”

经过前几章的修炼,你已经能把单个.NET应用熟练地装进容器。但真实的微服务世界,从来不是“单打独斗”。

设想一个最简单的电商场景,你的系统可能包含:

  • 订单服务(处理下单、付款)
  • 商品服务(管理商品信息)
  • MySQL数据库(存储核心数据)
  • Redis缓存(加速热点访问)

现在,你需要同时管理这4个容器。这意味着你要:

  1. 打开多个终端,分别执行复杂的 docker run 命令。
  2. 手动创建网络,确保它们能互相“找到”对方。
  3. 容器挂了?手动重启。配置改了?手动更新。

我们需要一个“总指挥”:一份声明式的蓝图,能定义谁是谁、谁需要谁、以及它们如何交谈。这个总指挥,就是 Docker Compose


二、认识总指挥:Docker Compose 核心概念

你可以将 docker-compose.yml 文件视作你的应用架构图和自动化运维手册的合体。理解这三个核心概念,就掌握了编排的精髓:

概念 通俗理解 在蓝图中的角色 关键影响
服务 一个独立的、可运行的容器实例。 蓝图中的一个 “功能模块”,如订单服务、MySQL。 每个服务对应一个 container,是编排的基本单位。
项目 由一组关联服务组成的完整应用。 整栋 “建筑”,默认以当前目录名命名(如 ecommerce-lego)。 执行 docker-compose 命令时,所有操作都基于整个项目。
网络 服务间通信的私有、安全通道 建筑内部的 “专用通信线路”,与外界隔离。 核心特性:在此网络内,服务间可直接使用服务名称作为主机名互相访问(服务发现)。

一句话心法:Compose为你的所有服务创建了一个专属聊天群。在这个群聊中,大家不叫IP地址这种冷冰冰的编号,而是直接喊服务名,比如“嘿,product-service,给我商品信息!”


三、实战:亲手绘制你的第一份微服务蓝图

让我们以“电商乐高”为例,搭建包含订单服务、商品服务、MySQL和Redis的完整栈。

步骤1:规划项目结构

创建清晰的项目目录:

ecommerce-lego/               # 项目根目录
├── docker-compose.yml        # 【核心】编排蓝图
├── order-service/            # 订单服务项目
│   ├── OrderService.csproj
│   ├── Dockerfile           # 订单服务的“集装箱说明书”
│   ├── Program.cs
│   └── ...
├── product-service/          # 商品服务项目
│   ├── ProductService.csproj
│   ├── Dockerfile
│   └── ...
└── .env                      # (可选)环境变量配置文件,用于管理密码等敏感信息

行动:创建两个基础的.NET Web API项目作为我们的服务原型。

# 创建订单服务项目
dotnet new webapi -n OrderService -o order-service -f net10.0
# 创建商品服务项目
dotnet new webapi -n ProductService -o product-service -f net10.0

在这里插入图片描述

步骤2:编写核心蓝图 docker-compose.yml

在项目根目录创建 docker-compose.yml

version: '3.8'

# 1. 定义所有服务(我们的乐高积木)
services:
  # 积木A:MySQL数据库
  mysql-db:
    image: mysql:8.0
    container_name: ecommerce-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root123456
      MYSQL_DATABASE: orderdb
    volumes:
      - mysql-data:/var/lib/mysql  # 数据持久化
    ports:
      - "3306:3306"
    networks:
      - app-network
    healthcheck: # 健康检查,确保数据库就绪后其他服务再启动
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  # 积木B:Redis缓存
  redis-cache:
    image: redis:7-alpine
    container_name: ecommerce-redis
    ports:
      - "6379:6379"
    networks:
      - app-network

  # 积木C:商品服务 (.NET)
  product-service:
    build: ./product-service  # 根据目录下的Dockerfile构建镜像
    container_name: product-service
    environment:
      - ConnectionStrings__Redis=redis-cache:6379
      - ConnectionStrings__MySql=Server=mysql-db;Database=orderdb;Uid=root;Pwd=root123456;
    depends_on:
      mysql-db:
        condition: service_healthy # 等待数据库健康
      redis-cache:
        condition: service_started
    ports:
      - "5005:8080"
    networks:
      - app-network

  # 积木D:订单服务 (.NET)
  order-service:
    build: ./order-service
    container_name: order-service
    environment:
      - ProductService__BaseUrl=http://product-service:8080 # 关键!通过服务名调用
      - ConnectionStrings__MySql=Server=mysql-db;Database=orderdb;Uid=root;Pwd=root123456;
    depends_on:
      - product-service # 等待商品服务启动
    ports:
      - "5000:80"
    networks:
      - app-network

# 2. 定义所有服务共用的资源
networks:
  app-network: # 创建一个专用网络,所有服务加入其中
    driver: bridge

volumes:
  mysql-data: # 声明一个数据卷,供MySQL使用
    driver: local
步骤3:在.NET服务中实现服务间通信

蓝图定义了“道路”,代码实现“运输”。订单服务需要调用商品服务,关键在于使用Compose网络提供的服务名作为主机名

改造订单服务(OrderApi):调用商品服务接口

order-service 中,通常通过 HttpClient 调用,我们需要:

  • 进入OrderApi目录,安装HttpClient包。修改Program.cs,配置HttpClient(用于调用商品服务)。

    cd ..\order-service && dotnet add package Microsoft.Extensions.Http
    
  • 修改Program.cs,配置HttpClient(用于调用商品服务)。
    在这里插入图片描述

  • 在Controller或Service中注入IHttpClientFactory并使用
    在这里插入图片描述

  • 编写OrderApi的Dockerfile(放在OrderApi根目录):

    FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
    WORKDIR /src
    COPY ["OrderService.csproj", "."]
    RUN dotnet restore "OrderService.csproj"
    COPY . .
    RUN dotnet publish -c Release -o /app/publish --no-self-contained
    
    FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS runtime
    WORKDIR /app
    COPY --from=build /app/publish .
    EXPOSE 80
    ENV ASPNETCORE_URLS=http://0.0.0.0:80
    ENTRYPOINT ["dotnet", "OrderService.dll"]
    
改造商品服务(ProductApi):提供商品查询接口

product-service 中,使用Redis缓存商品,我们需要:

  • 修改Program.cs,添加Redis缓存配置(先安装NuGet包):

    cd product-service && dotnet add package StackExchange.Redis 
    

    在这里插入图片描述

  • 创建并实现Controllers/ProductController.cs
    在这里插入图片描述
    在这里插入图片描述

  • 编写ProductApi的Dockerfile(放在ProductApi根目录):

    FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
    WORKDIR /src
    COPY ["ProductService.csproj", "."]
    RUN dotnet restore "ProductService.csproj"
    COPY . .
    RUN dotnet publish -c Release -o /app/publish --no-self-contained
    
    FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS runtime
    WORKDIR /app
    COPY --from=build /app/publish .
    EXPOSE 8080
    ENV ASPNETCORE_URLS=http://0.0.0.0:8080
    ENTRYPOINT ["dotnet", "ProductService.dll"]
    
步骤4:一键启动与运维你的“乐团”

一切就绪,感受总指挥的力量:

# 启动所有服务(-d 表示后台运行)
docker-compose up -d

# 查看所有服务的运行状态和日志
docker-compose ps
docker-compose logs -f order-service # 跟踪订单服务日志

在这里插入图片描述

# 验证服务
curl http://localhost:5005/api/products  # 测试商品服务
curl http://localhost:5000/api/orders # 测试订单服务

# 验证服务间通信
curl -X POST http://localhost:5000/api/orders   -H "Content-Type: application/json"   -d '{"productId": 1, "quantity": 1}'

在这里插入图片描述

# 一键停止并清理所有服务
docker-compose down
# 如果想同时删除数据卷(谨慎!)
# docker-compose down -v

四、进阶技巧与避坑指南

  1. 环境变量与配置分离:永远不要将密码等硬编码在yml文件中。可以使用 .env 文件。

    # 创建 .env 文件(并加入 .gitignore)
    MYSQL_ROOT_PASSWORD=YourSuperStrongPassword!
    

    docker-compose.yml 中引用:${MYSQL_ROOT_PASSWORD}

  2. 理解 depends_on 的局限:它只控制容器启动顺序,不保证应用就绪状态。对于数据库,必须结合 healthcheck 使用。

  3. 开发效率技巧:在开发时,可以使用绑定挂载实现代码热重载,避免重复构建镜像。

    services:
      order-service:
        build: ./order-service
        volumes:
          - ./order-service:/app:ro  # 将本地代码目录挂载到容器,实时同步
          - ~/.nuget/packages:/root/.nuget/packages:ro # 挂载NuGet缓存,加速还原
        environment:
          - ASPNETCORE_ENVIRONMENT=Development
    
  4. 镜像构建优化:在CI/CD中,可以先单独构建镜像,再使用 image 而非 build 指令,提升编排启动速度。


五、本章心法总结

  1. 基础设施即代码docker-compose.yml 是你应用运行环境的权威声明,应纳入版本控制。
  2. 网络即服务发现:Compose的私有网络是微服务通信的基石,服务名即域名,简化了服务间调用。
  3. 状态分离:通过 volumes 将数据库等有状态数据外置,确保应用容器本身无状态,可随时销毁和重建,这是云原生的核心思想。

下章预告

现在,你的订单服务和商品服务已经可以通过HTTP(REST)优雅地“打电话”沟通了。但“打电话”要求对方必须实时在线并立即接听,这在复杂的分布式场景中并非总是最佳选择。如果对方忙线、或者操作需要耗时很久,该怎么办?

下一章:《微服务对话艺术:何时用REST“打电话”,何时用消息队列“发邮件”?》,我们将深入对比同步与异步通信模式,并引入消息队列这位强大的“异步信使”,解决服务解耦、流量削峰和最终一致性等核心难题。

卡在环境配置?想要一个开箱即用的完整项目参考?
我们为你打包好了本章的全套可运行源码(含Dockerfile与编排脚本)。
微信搜索关注「黑棠会长」,后台回复 「微服务乐高」 ,一键获取完整资源包,快速复现所有操作。

上节指路第4章:Docker 数据“生存指南”:容器销毁,你的文件如何“永生”?


原创文章,未经授权禁止转载。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐