C#/.NET 微服务架构:从入门到精通(七):服务注册与发现(Consul)与 API 网关(Ocelot)
上一篇我们完成了里程碑式的突破:实现了单服务独立 Docker 容器化与 Jenkins 自动化流水线,现在我们的商品服务、订单服务已经能够一键构建、自动部署、独立运行。但此时的微服务还是一个个 “信息孤岛”—— 服务之间不知道彼此的存在,外部客户端也无法统一访问所有服务。
本篇将解决微服务架构最核心的两个基础问题:服务如何动态找到对方与外部流量如何统一治理。我们将基于已有的 Docker 部署体系,深度集成Consul 服务注册与发现和Ocelot API 网关,让所有独立部署的服务形成一个有机的整体,真正具备生产级微服务的通信能力。
一、为什么我们现在需要 Consul 和 Ocelot?
在上一篇的部署成果基础上,我们面临两个无法回避的痛点:
痛点 1:服务间通信的 “地址地狱”
现在我们的商品服务和订单服务都跑在 Docker 容器中:
- 每次重启容器,IP 和端口都可能变化;
- 为了高可用,每个服务部署了 3 个实例,总共有 6 个不同的地址;
- 如果硬编码地址,每次扩缩容、重启服务都要修改所有调用方的配置并重新部署。
Consul 就是来解决这个问题的:它作为统一的服务注册中心,所有服务启动时自动上报自己的地址,调用方只需知道服务名,就能从 Consul 获取所有健康实例的地址,实现动态通信。
痛点 2:外部访问的 “混乱局面”
没有网关时:
- 客户端需要维护 3 个商品服务地址、3 个订单服务地址,复杂度极高;
- 每个服务都要单独实现认证、限流、跨域、日志,重复造轮子;
- 所有服务直接暴露在公网,攻击面巨大,安全无法保障。
Ocelot 就是来解决这个问题的:它作为所有外部请求的唯一入口,统一处理认证、限流、路由等横切关注点,客户端只需知道网关一个地址,所有请求都由网关转发到对应的后端服务。
我们的最终目标架构:
客户端 → Ocelot网关 → Consul注册中心 → 健康的商品/订单服务实例
- 所有服务自动注册到 Consul,Consul 实时监控服务健康状态;
- Ocelot 从 Consul 动态拉取服务列表,根据请求路径转发到对应服务;
- 整个过程完全自动化,无需任何硬编码配置。
二、实战步骤 1:改造现有服务,集成 Consul 客户端
我们不需要修改任何业务代码,只需为已有的商品服务和订单服务添加 Consul 客户端集成,让它们启动时自动注册到 Consul。我们这里以商品服务为例:
1. 安装 Consul NuGet 包

在ProductApi中执行:
dotnet add package Consul --version 1.8.0
dotnet add package Consul.AspNetCore --version 1.8.0
2. 改造服务的 Program.cs
using Asp.Versioning; using Asp.Versioning.ApiExplorer; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; using ProductApi.DbContexts; using System.Reflection; using Consul; using Consul.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); #region 版本控制 // 注册接口版本控制 builder.Services.AddApiVersioning(options => { options.DefaultApiVersion = new ApiVersion(1, 0); // 默认版本v1.0 options.AssumeDefaultVersionWhenUnspecified = true; // 未指定版本时使用默认版本 options.ReportApiVersions = true; // 在响应头中返回支持的版本 // 配置版本读取方式(支持多种方式,这里演示URL路径方式,最直观) options.ApiVersionReader = ApiVersionReader.Combine( new UrlSegmentApiVersionReader() // URL路径版本:/api/v1/User // 也可添加查询字符串方式:/api/User?api-version=1.0 // new QueryStringApiVersionReader("api-version"), // 或请求头方式:Header中添加Api-Version: 1.0 // new HeaderApiVersionReader("Api-Version") ); }) //注册版本探索器(与Swagger集成必需) .AddMvc() .AddApiExplorer(options => { options.GroupNameFormat = "'v'VVV"; // 版本组名格式:v1、v1.1 options.SubstituteApiVersionInUrl = true; // 替换URL中的版本占位符 }); #endregion builder.Services.AddEndpointsApiExplorer(); #region Swagger集成 //注册Swagger生成器 builder.Services.AddSwaggerGen(c => {// 动态获取所有版本信息,生成对应的SwaggerDoc using var scope = builder.Services.BuildServiceProvider().CreateScope(); var provider = scope.ServiceProvider.GetRequiredService<IApiVersionDescriptionProvider>(); foreach (var description in provider.ApiVersionDescriptions) { c.SwaggerDoc(description.GroupName, new OpenApiInfo { Title = $"商品 API {description.GroupName}", Version = description.GroupName, Description = description.IsDeprecated ? "该版本已废弃" : "当前稳定版本", Contact = new OpenApiContact { Name = "挺秃然的i", Url = new Uri("https://www.cnblogs.com/lantingxu") } }); } // 引入控制器XML注释 var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath, true); // true表示包含控制器注释 // 启用JWT授权 c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "请输入JWT Token(格式:Bearer {Token})", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "Bearer" }); // 全局应用JWT认证要求 c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] { } } }); }); #endregion #region 注册EF Core DbContext //注册EF Core DbContext(连接MySQL) var connectionString = builder.Configuration.GetConnectionString("ProductDb"); builder.Services.AddDbContext<ProductDbContext>(options => { options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), mysqlOpt => mysqlOpt.MigrationsAssembly(typeof(ProductDbContext).Assembly.FullName)); }); #endregion #region Consul builder.Services.AddConsul(config => { config.Address = new Uri(builder.Configuration["Consul:ConsulAddress"]); }); #endregion var app = builder.Build(); //配置Swagger中间件(仅在开发环境启用,生产环境可关闭) if (app.Environment.IsDevelopment()) { app.UseSwagger(); // 生成Swagger JSON端点(/swagger/v1/swagger.json) app.UseSwaggerUI(c => { //c.SwaggerEndpoint("/swagger/v1/swagger.json", "商品 API v1"); // 动态获取所有版本,配置Swagger端点 var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>(); foreach (var description in provider.ApiVersionDescriptions) { c.SwaggerEndpoint( $"/swagger/{description.GroupName}/swagger.json", $"商品 API {description.GroupName}" ); } c.RoutePrefix = "swagger"; // 将Swagger UI设为swagger路径(访问http://localhost:端口/swagger即可打开) }); } app.UseAuthorization(); app.MapControllers(); // 1. 从配置读取固定值 var config = builder.Configuration; var serviceHost = config["Consul:ServiceHost"]; var servicePort = int.Parse(config["Consul:ServicePort"]); // 2. 注册到Consul(必须在 app.Run() 之前) using (var scope = app.Services.CreateScope()) { var consulClient = scope.ServiceProvider.GetRequiredService<IConsulClient>(); try { var registration = new AgentServiceRegistration { Name = config["Consul:ServiceName"], ID = $"{config["Consul:ServiceName"]}-{Guid.NewGuid()}", Address = serviceHost, // 直接用配置的容器名 Port = servicePort, // 直接用配置的80端口 Check = new AgentServiceCheck { HTTP = $"http://{serviceHost}:{servicePort}/health", // 健康检查地址也要用容器名 Interval = TimeSpan.FromSeconds(10), Timeout = TimeSpan.FromSeconds(5), DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(30) } }; await consulClient.Agent.ServiceRegister(registration); Console.WriteLine($"✅ 成功注册到Consul,服务名:{registration.Name},地址:{serviceHost}:{servicePort}"); } catch (Exception ex) { Console.WriteLine($"❌ 注册失败:{ex.Message}"); } } app.Run();
3. 修改服务的 appsettings.json

"Consul": {
"ConsulAddress": "http://consul:8500", // Docker网络内通过服务名访问Consul
"ServiceName": "Product-Api",
"ServiceHost": "productapi",
"ServicePort": "80"
}
三、实战步骤 2:Docker 安装Consul
开发模式(仅测试用):
# 拉取最新官方Consul镜像
docker pull hashicorp/consul:latest
# 启动开发模式Consul
docker run -d \
--name consul-dev \
-p 8500:8500 \
-p 8600:8600/udp \
hashicorp/consul:latest agent -dev -client=0.0.0.0
# 验证安装
docker ps | grep consul
开发模式所有数据都存储在内存中,容器重启后数据会全部丢失,绝对不能用于生产环境。
验证 Consul:
访问http://你的服务器IP:8500,打开 Consul Web UI,此时还没有任何服务注册。

四、实战步骤 3:搭建 Ocelot API 网关
1. 创建网关项目

2. 安装 NuGet 包
dotnet add package Ocelot --version 22.0.1 dotnet add package Ocelot.Provider.Consul --version 22.0.1

3. 编写 Ocelot 配置文件 ocelot.json
{
"Routes": [
// 商品服务路由
{
"DownstreamPathTemplate": "/api/{version}/Product/{everything}",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/api/{version}/product/{everything}",
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ],
"ServiceName": "Product-Api", // 与Consul中的服务名完全一致
"LoadBalancerOptions": {
"Type": "RoundRobin" // 轮询负载均衡
}
},
// 订单服务路由
{
"DownstreamPathTemplate": "/api/{version}/Order/{everything}",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/api/{version}/order/{everything}",
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ],
"ServiceName": "Order-Api",
"LoadBalancerOptions": {
"Type": "RoundRobin"
}
}
],
"GlobalConfiguration": {
"ServiceDiscoveryProvider": {
"Host": "consul", // 必须是Consul容器名,不能是IP
"Port": 8500,
"Type": "Consul", // 必须大写C,不能写错
"PollingInterval": 1000 // 可选:每秒从Consul拉取一次服务列表,避免缓存
}
}
}
4. 编写网关的 Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 # 设置中文编码 ENV LANG=zh_CN.UTF-8 ENV LC_ALL=zh_CN.UTF-8 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["Gateway/Gateway.csproj", "Gateway/"] RUN dotnet restore "Gateway/Gateway.csproj" COPY . . # 构建 WORKDIR "/src/Gateway" RUN dotnet build "Gateway.csproj" -c Release -o /app/build # 发布 FROM build AS publish RUN dotnet publish "Gateway.csproj" -c Release -o /app/publish /p:UseAppHost=false # 最终运行 FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "Gateway.dll"]
5. 编写网关的 Jenkinsfile
pipeline {
agent any
options {
timeout(time: 30, unit: 'MINUTES')
disableResume() // 符合你的禁用Resume要求
skipStagesAfterUnstable()
}
environment {
// 基础配置
GIT_URL = 'https://gitee.com/kb248/e-commerce-demo.git'
GIT_CRED_ID = 'gitee-auth'
GIT_BRANCH = '*/master'
SERVICE_NAME = 'gateway'
DOCKERFILE_PATH = 'Gateway/Dockerfile'
HOST_PORT = '5000'
CONTAINER_PORT = '80'
DOCKER_IMAGE_TAG = "${SERVICE_NAME}:${BUILD_NUMBER}"
}
stages {
stage('1. 拉取全量代码') {
steps {
echo "正在拉取代码..."
checkout scmGit(
branches: [[name: GIT_BRANCH]],
extensions: [[$class: 'CleanBeforeCheckout']], // 构建前清理工作空间
userRemoteConfigs: [[
credentialsId: GIT_CRED_ID,
url: GIT_URL
]]
)
}
}
stage('2. 验证 Dockerfile') {
steps {
script {
if (!fileExists(DOCKERFILE_PATH)) {
error "❌ 未找到 Dockerfile: ${DOCKERFILE_PATH},请在Gateway目录下创建Dockerfile"
}
echo "✅ Dockerfile 验证通过"
}
}
}
stage('3. 构建 Docker 镜像') {
steps {
echo "构建镜像: ${DOCKER_IMAGE_TAG}"
sh "docker build -t ${DOCKER_IMAGE_TAG} -f ${DOCKERFILE_PATH} ."
echo "✅ 镜像构建完成"
}
}
stage('4. 清理旧容器和镜像') {
steps {
echo "清理旧容器和悬空镜像..."
sh """
docker stop ${SERVICE_NAME} || true
docker rm ${SERVICE_NAME} || true
docker image prune -f
"""
}
}
stage('5. 启动新容器') {
steps {
echo "启动容器: ${SERVICE_NAME}"
sh "docker run -d --network dorm-network -p ${HOST_PORT}:${CONTAINER_PORT} -e ASPNETCORE_ENVIRONMENT=Development -e ASPNETCORE_URLS=http://0.0.0.0:${CONTAINER_PORT} -e LANG=zh_CN.UTF-8 -e LC_ALL=zh_CN.UTF-8 --name ${SERVICE_NAME} ${DOCKER_IMAGE_TAG}"
// 等待容器启动并验证健康状态
sleep 10
sh "docker ps | grep ${SERVICE_NAME}"
echo "✅ 容器启动成功"
}
}
}
post {
always {
cleanWs() // 构建完成后清理工作空间
echo "工作空间清理完成"
}
success {
script {
def containerId = sh(
script: "docker ps -qf name=${SERVICE_NAME}",
returnStdout: true
).trim()
echo "========================================="
echo "✅ 产品服务发布成功!"
echo "📖 访问地址: http://你的服务器IP:${HOST_PORT}/swagger"
echo "🐳 容器ID: ${containerId}"
echo "========================================="
}
}
failure {
echo "❌ 构建失败!请查看详细日志"
// 输出容器日志帮助排查(容器可能已停止,使用docker logs -f 会卡住,去掉-f)
sh "docker logs ${SERVICE_NAME} 2>&1 || true"
}
}
}
6.调整Program文件
using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Provider.Consul; // 必须引入这个命名空间 var builder = WebApplication.CreateBuilder(args); // 1. 【必须】加载ocelot.json配置文件,不能漏 builder.Configuration.AddJsonFile( "ocelot.json", optional: false, // 必须设为false,配置文件不存在会直接报错,方便排查 reloadOnChange: true ); // 2. 【致命关键】必须同时注入 Ocelot + Consul 扩展 // 只写 AddOcelot() 不写 AddConsul(),Ocelot不会从Consul拿服务,必然502 builder.Services .AddOcelot(builder.Configuration) // 必须传入Configuration .AddConsul(); // 这一行绝对不能漏! var app = builder.Build(); // 3. 【必须】启用Ocelot中间件,必须是最后一个中间件 await app.UseOcelot(); app.Run();
7. 部署网关
提交代码到 Git,触发 Jenkins 流水线,自动部署网关到服务器。
五、实战步骤 4:全链路验证
现在我们已经完成了所有组件的部署,来验证整个微服务体系是否正常工作。
1. 验证服务注册
访问 Consul Web UI http://你的服务器IP:8500,可以看到:

2. 验证网关路由
通过网关访问商品服务:

返回商品列表,说明网关成功将请求转发到商品服务。
通过网关访问订单服务:

返回订单列表,说明网关成功将请求转发到订单服务。
六、常见问题解决
1. 服务注册成功但健康检查失败
- 检查
SERVICE_HOST_IP是否为宿主机的公网 / 内网 IP,不是容器内部 IP; - 检查宿主机防火墙是否开放了服务的端口,Consul 能访问到健康检查端点;
- 检查健康检查路径是否正确,服务是否能正常返回 200 状态码。
2. Ocelot 无法连接 Consul
- 确认 Ocelot 和 Consul 在同一个 Docker 网络中;
- 检查
ocelot.json中ServiceDiscoveryProvider的Host是否为consul(Docker 服务名); - 检查 Consul 容器是否正常运行,端口 8500 是否开放。
3. Ocelot 转发请求 404
- 检查
UpstreamPathTemplate和DownstreamPathTemplate的匹配规则; - 确认 Consul 中的服务名与
ocelot.json中的ServiceName完全一致(大小写敏感); - 确认下游服务的接口路径正确,能直接访问。
七、总结
- 改造了已有的商品服务和订单服务,实现了自动注册到 Consul 和优雅注销;
- 用 Docker 部署了 Consul 服务端,实现了服务的健康检查和故障自动剔除;
- 搭建了独立的 Ocelot API 网关,实现了动态路由、负载均衡和统一认证;
- 所有组件都通过 Jenkins 自动部署,保持了整个 CI/CD 流程的一致性。

浙公网安备 33010602011771号