一、微服务,从“单打独斗”到“团队协作”的转变
想象一下,你正在开发一个电商系统。在传统的“单体架构”里,用户管理、商品浏览、下单支付、库存扣减所有这些功能,都像被塞进了一个巨大的、密不透风的箱子里。这个箱子就是你的整个应用。一开始可能还好,但随着功能越来越多,这个箱子变得无比沉重。你想改一下支付逻辑,可能不小心就影响了商品展示;想升级用户模块,整个系统都得停下来重新部署。这就像一辆公交车,不管你去哪里,路线都是固定的,而且只要一个零件坏了,整辆车都得趴窝。
微服务架构就是为了解决这个问题而生的。它的核心思想很简单:把那个大箱子拆开,变成许多个独立的小盒子。每个小盒子(也就是一个微服务)只负责一件明确的事情,比如专门管用户的“用户服务”、专门管商品的“商品服务”。它们各自独立开发、独立部署、独立运行。这样一来,系统就变得灵活多了,哪个服务需要升级就升级哪个,出了问题也不容易“城门失火,殃及池鱼”。
但是,问题也随之而来。以前都在一个箱子里,互相喊一嗓子就能沟通。现在大家分散在各个小盒子里,甚至可能运行在不同的服务器上,我们面临两个核心挑战:
- 服务发现:订单服务想要调用库存服务来扣减库存,它首先得知道库存服务现在在哪台机器上、地址是什么。这个“找地址”的过程,就是服务发现。
- 服务通信:找到地址后,用什么方式、什么协议去调用对方,才能既高效又可靠,这就是服务通信。
接下来,我们就来看看在 .NET Core 的世界里,如何用一些成熟的工具来解决这两个问题。
二、服务发现的“电话簿”:Consul实战
服务发现需要一个“中央登记处”,就像公司的通讯录或者手机里的电话簿。所有服务在启动时,都去这里登记自己的姓名(服务名)和住址(IP和端口)。当某个服务需要找另一个服务时,也先来这个“电话簿”查询。目前业界常用的“电话簿”有 Consul、Eureka、ZooKeeper 等。我们以 Consul 为例,因为它功能强大,与 .NET Core 集成也很方便。
技术栈:.NET 6, Consul, ASP.NET Core
首先,我们需要一个服务,它会向 Consul 注册自己。我们创建一个简单的“商品服务”。
// 技术栈:.NET 6, Consul, ASP.NET Core
// ProductService.cs - 一个简单的商品服务,用于注册到Consul
using Consul;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ProductService
{
// 一个后台服务,在应用启动时自动执行服务注册
public class ConsulHostedService : IHostedService
{
private readonly IConsulClient _consulClient;
private readonly ILogger<ConsulHostedService> _logger;
private readonly IConfiguration _configuration;
private string _registrationId; // 用于保存注册ID,以便注销时使用
public ConsulHostedService(IConsulClient consulClient, ILogger<ConsulHostedService> logger, IConfiguration configuration)
{
_consulClient = consulClient;
_logger = logger;
_configuration = configuration;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// 从配置中读取服务信息
var serviceConfig = _configuration.GetSection("ConsulService");
var serviceName = serviceConfig["ServiceName"]; // 例如 "product-service"
var serviceId = serviceConfig["ServiceId"]; // 唯一标识,例如 "product-service-01"
var serviceAddress = serviceConfig["ServiceAddress"]; // 服务自身可被访问的地址,如 "http://localhost:5001"
var consulAddress = serviceConfig["ConsulAddress"]; // Consul服务器的地址,如 "http://localhost:8500"
// 构建服务注册信息
var registration = new AgentServiceRegistration()
{
ID = serviceId,
Name = serviceName,
Address = new Uri(serviceAddress).Host,
Port = new Uri(serviceAddress).Port,
// 健康检查端点,Consul会定期调用此接口来检查服务是否健康
Check = new AgentServiceCheck()
{
HTTP = $"{serviceAddress}/health",
Interval = TimeSpan.FromSeconds(10), // 每10秒检查一次
Timeout = TimeSpan.FromSeconds(5), // 检查超时时间5秒
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(30) // 健康检查失败30秒后注销服务
}
};
_registrationId = registration.ID;
_logger.LogInformation($"正在向Consul注册服务:{serviceName},地址:{serviceAddress}");
// 执行注册
await _consulClient.Agent.ServiceRegister(registration, cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(_registrationId))
{
_logger.LogInformation($"正在从Consul注销服务:{_registrationId}");
// 应用关闭时,从Consul注销,避免无效地址残留
await _consulClient.Agent.ServiceDeregister(_registrationId, cancellationToken);
}
}
}
}
同时,我们需要在 Program.cs 中配置 Consul 客户端和健康检查端点。
// 技术栈:.NET 6, Consul, ASP.NET Core
// Program.cs - 配置Consul客户端和健康检查
using Consul;
var builder = WebApplication.CreateBuilder(args);
// 1. 添加Consul客户端依赖
builder.Services.AddSingleton<IConsulClient>(sp =>
new ConsulClient(config =>
{
// 配置Consul服务器地址
config.Address = new Uri(builder.Configuration["ConsulService:ConsulAddress"]);
})
);
// 2. 将我们上面写的注册服务添加到后台运行
builder.Services.AddHostedService<ConsulHostedService>();
// 添加Controllers,用于提供健康检查接口
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
// 3. 添加一个简单的健康检查端点(也可以使用专用的健康检查库)
app.MapGet("/health", () => Results.Ok(new { status = "Healthy" }));
app.Run();
这样,当我们的商品服务启动后,它就会自动到 Consul 那里“报到”。我们可以在 Consul 的 Web 界面(默认 http://localhost:8500)上看到它,并且能看到它的健康状态。
三、服务通信的“快递员”:HttpClient与Polly
找到了服务地址,接下来就是如何调用。在 .NET 微服务中,最常用、最基础的方式就是通过 HTTP 协议进行 RESTful API 调用。.NET Core 提供了强大的 HttpClient 类。但是,直接使用 HttpClient 在微服务环境下会遇到问题,比如需要处理服务发现(从 Consul 获取地址)、需要处理网络故障(重试、熔断)等。因此,我们通常会结合服务发现客户端和弹性处理库来使用。
这里我们引入 Consul 的服务发现功能来获取地址,并使用 Polly 库来处理故障。我们创建一个“订单服务”,它需要调用上面注册的“商品服务”来获取商品信息。
技术栈:.NET 6, Consul, Polly, HttpClientFactory
首先,我们需要一个能从 Consul 发现服务并处理通信的类。
// 技术栈:.NET 6, Consul, Polly, HttpClientFactory
// ProductServiceClient.cs - 一个封装了服务发现和弹性通信的商品服务客户端
using Consul;
using Polly;
using Polly.Retry;
using Polly.Timeout;
using Polly.Wrap;
namespace OrderService
{
public interface IProductServiceClient
{
Task<Product> GetProductByIdAsync(int productId);
}
public class ProductServiceClient : IProductServiceClient
{
private readonly IConsulClient _consulClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<ProductServiceClient> _logger;
private readonly AsyncPolicyWrap<HttpResponseMessage> _resiliencePolicy; // 组合弹性策略
public ProductServiceClient(IConsulClient consulClient, IHttpClientFactory httpClientFactory, ILogger<ProductServiceClient> logger)
{
_consulClient = consulClient;
_httpClientFactory = httpClientFactory;
_logger = logger;
// 1. 重试策略:当发生短暂网络故障或服务暂时不可用时,重试3次,每次等待指数级增长的时间
var retryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>() // 处理超时
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 2, 4, 8秒
onRetry: (exception, timeSpan, retryCount, context) =>
{
_logger.LogWarning(exception, $"第{retryCount}次重试,等待{timeSpan.TotalSeconds}秒后执行。");
});
// 2. 超时策略:单个请求超过2秒未响应则视为超时
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(2));
// 3. 熔断器策略:当失败率达到50%(最近10次请求中5次失败),熔断5秒,期间快速失败,5秒后尝试放行一个请求探测
var circuitBreakerPolicy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(5),
onBreak: (exception, breakDelay) =>
{
_logger.LogError(exception, $"熔断器开启,服务暂停调用{breakDelay.TotalSeconds}秒。");
},
onReset: () =>
{
_logger.LogInformation("熔断器关闭,服务恢复正常调用。");
});
// 将三个策略组合起来:重试(超时(熔断器(实际请求)))
_resiliencePolicy = Policy.WrapAsync(retryPolicy, timeoutPolicy, circuitBreakerPolicy);
}
public async Task<Product> GetProductByIdAsync(int productId)
{
// 第一步:服务发现 - 从Consul获取健康的商品服务实例
var services = await _consulClient.Health.Service("product-service", null, true);
if (services.Response == null || !services.Response.Any())
{
throw new Exception("未找到可用的商品服务实例。");
}
// 简单负载均衡:随机选择一个健康实例
var instance = services.Response[new Random().Next(services.Response.Length)];
var serviceUrl = $"http://{instance.Service.Address}:{instance.Service.Port}";
// 第二步:构建HTTP请求
var requestUrl = $"{serviceUrl}/api/products/{productId}";
var httpClient = _httpClientFactory.CreateClient(); // 使用工厂创建,管理连接池更高效
// 第三步:使用组合弹性策略执行请求
HttpResponseMessage response = await _resiliencePolicy.ExecuteAsync(async () =>
{
var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
return await httpClient.SendAsync(request);
});
// 处理响应
response.EnsureSuccessStatusCode();
var product = await response.Content.ReadFromJsonAsync<Product>();
return product;
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
然后,在 OrderService 的 Program.cs 中注册这个客户端和必要的服务。
// 技术栈:.NET 6, Consul, Polly, HttpClientFactory
// OrderService的Program.cs
using Consul;
var builder = WebApplication.CreateBuilder(args);
// 注册Consul客户端
builder.Services.AddSingleton<IConsulClient>(sp =>
new ConsulClient(config =>
{
config.Address = new Uri(builder.Configuration["Consul:Address"]);
})
);
// 注册HttpClientFactory,这是使用HttpClient的最佳实践
builder.Services.AddHttpClient();
// 注册我们自定义的商品服务客户端
builder.Services.AddScoped<IProductServiceClient, ProductServiceClient>();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
现在,在订单服务的控制器里,我们就可以轻松地通过 IProductServiceClient 来调用商品服务了,完全不用关心对方的具体地址和网络故障处理。
四、更优雅的通信:gRPC与服务网格展望
HTTP RESTful API 虽然通用,但在性能要求极高的内部服务通信场景下,可能不是最优选择。这时,我们可以考虑 gRPC。
gRPC 是一个高性能、开源、通用的 RPC 框架。它默认使用 Protocol Buffers 作为接口定义语言和序列化工具,传输基于 HTTP/2,支持双向流、头部压缩等特性,性能远超 JSON over HTTP。在 .NET Core 中,对 gRPC 的支持是一流的。
应用场景对比:
- HTTP + RESTful API:适合对外暴露的、需要广谱兼容性的 API(如提供给手机App、网页前端),或者对性能要求不是极端苛刻的内部服务。
- gRPC:非常适合对延迟敏感、吞吐量要求高的内部服务间通信,比如大数据传输、实时通信、微服务集群内部调用。
对于更复杂的微服务网络治理(如流量管理、安全、可观测性),还有一个更高级的概念叫服务网格,比如 Linkerd、Istio。它们通常通过一个叫“边车”的代理来接管服务的所有网络流量,从而实现对通信的透明化管理,开发者几乎不用改代码。但在中小型项目中,Consul + 弹性客户端的方式往往更简单直接。
五、实战方案全貌与总结
让我们回顾一下我们构建的实战解决方案全貌:
- 服务注册:每个微服务(如商品服务)启动时,通过
ConsulHostedService自动向 Consul 注册中心注册自己的网络位置和健康检查端点。 - 服务发现:调用方(如订单服务)通过
ConsulClient查询 Consul,获得目标服务(商品服务)的可用实例列表,并实现简单的负载均衡(如随机选择)。 - 服务通信:调用方使用
HttpClientFactory创建客户端,并集成Polly策略(重试、超时、熔断)来发起 HTTP 调用,确保通信的弹性。 - 可选升级:对于性能敏感的内部调用,可以将 HTTP 通信替换为 gRPC。
技术优缺点:
- 优点:
- 解耦与灵活:服务彻底独立,技术栈可选,部署升级互不影响。
- 弹性与可靠:通过 Polly 等机制,系统能够容忍网络波动和临时故障。
- 可扩展:通过 Consul 可以轻松水平扩展服务实例。
- 技术成熟:使用的都是 .NET 生态和业界广泛认可的成熟组件。
- 缺点:
- 复杂度提升:引入了服务发现、通信治理等分布式系统固有的复杂性。
- 运维成本:需要维护 Consul 等中间件,监控点也变多了。
- 调试难度:问题可能出现在网络、服务发现、目标服务等多个环节,排查链路更长。
注意事项:
- 网络是脆弱的:必须为所有服务间调用实现超时、重试和熔断,永远不要假设网络是可靠的。
- 幂等性设计:因为重试机制的存在,你的服务接口(特别是写操作)需要设计成幂等的,即多次调用产生的结果与一次调用相同。
- 配置管理:Consul 地址、重试次数、超时时间等参数不要硬编码,应该放在统一的配置中心(Consul 本身也提供 KV 存储做配置中心)。
- 监控与日志:完善的日志记录和链路追踪(如集成 SkyWalking、Jaeger)是微服务可观测性的生命线。
总的来说,在 .NET Core 微服务架构下,采用 Consul 进行服务发现,结合 HttpClientFactory 和 Polly 进行弹性通信,是一套非常务实且高效的解决方案。它平衡了功能、复杂度和可控性,能够帮助团队平滑地从单体架构过渡到微服务,并构建出健壮、可扩展的分布式系统。随着业务发展,你可以在此基础上,逐步引入 gRPC 优化性能,或者探索服务网格来获得更强大的运维能力。
评论