一、微服务,从“单打独斗”到“团队协作”的转变

想象一下,你正在开发一个电商系统。在传统的“单体架构”里,用户管理、商品浏览、下单支付、库存扣减所有这些功能,都像被塞进了一个巨大的、密不透风的箱子里。这个箱子就是你的整个应用。一开始可能还好,但随着功能越来越多,这个箱子变得无比沉重。你想改一下支付逻辑,可能不小心就影响了商品展示;想升级用户模块,整个系统都得停下来重新部署。这就像一辆公交车,不管你去哪里,路线都是固定的,而且只要一个零件坏了,整辆车都得趴窝。

微服务架构就是为了解决这个问题而生的。它的核心思想很简单:把那个大箱子拆开,变成许多个独立的小盒子。每个小盒子(也就是一个微服务)只负责一件明确的事情,比如专门管用户的“用户服务”、专门管商品的“商品服务”。它们各自独立开发、独立部署、独立运行。这样一来,系统就变得灵活多了,哪个服务需要升级就升级哪个,出了问题也不容易“城门失火,殃及池鱼”。

但是,问题也随之而来。以前都在一个箱子里,互相喊一嗓子就能沟通。现在大家分散在各个小盒子里,甚至可能运行在不同的服务器上,我们面临两个核心挑战:

  1. 服务发现:订单服务想要调用库存服务来扣减库存,它首先得知道库存服务现在在哪台机器上、地址是什么。这个“找地址”的过程,就是服务发现。
  2. 服务通信:找到地址后,用什么方式、什么协议去调用对方,才能既高效又可靠,这就是服务通信。

接下来,我们就来看看在 .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; }
    }
}

然后,在 OrderServiceProgram.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 + 弹性客户端的方式往往更简单直接。

五、实战方案全貌与总结

让我们回顾一下我们构建的实战解决方案全貌:

  1. 服务注册:每个微服务(如商品服务)启动时,通过 ConsulHostedService 自动向 Consul 注册中心注册自己的网络位置和健康检查端点。
  2. 服务发现:调用方(如订单服务)通过 ConsulClient 查询 Consul,获得目标服务(商品服务)的可用实例列表,并实现简单的负载均衡(如随机选择)。
  3. 服务通信:调用方使用 HttpClientFactory 创建客户端,并集成 Polly 策略(重试、超时、熔断)来发起 HTTP 调用,确保通信的弹性。
  4. 可选升级:对于性能敏感的内部调用,可以将 HTTP 通信替换为 gRPC。

技术优缺点:

  • 优点
    • 解耦与灵活:服务彻底独立,技术栈可选,部署升级互不影响。
    • 弹性与可靠:通过 Polly 等机制,系统能够容忍网络波动和临时故障。
    • 可扩展:通过 Consul 可以轻松水平扩展服务实例。
    • 技术成熟:使用的都是 .NET 生态和业界广泛认可的成熟组件。
  • 缺点
    • 复杂度提升:引入了服务发现、通信治理等分布式系统固有的复杂性。
    • 运维成本:需要维护 Consul 等中间件,监控点也变多了。
    • 调试难度:问题可能出现在网络、服务发现、目标服务等多个环节,排查链路更长。

注意事项:

  1. 网络是脆弱的:必须为所有服务间调用实现超时、重试和熔断,永远不要假设网络是可靠的。
  2. 幂等性设计:因为重试机制的存在,你的服务接口(特别是写操作)需要设计成幂等的,即多次调用产生的结果与一次调用相同。
  3. 配置管理:Consul 地址、重试次数、超时时间等参数不要硬编码,应该放在统一的配置中心(Consul 本身也提供 KV 存储做配置中心)。
  4. 监控与日志:完善的日志记录和链路追踪(如集成 SkyWalking、Jaeger)是微服务可观测性的生命线。

总的来说,在 .NET Core 微服务架构下,采用 Consul 进行服务发现,结合 HttpClientFactory 和 Polly 进行弹性通信,是一套非常务实且高效的解决方案。它平衡了功能、复杂度和可控性,能够帮助团队平滑地从单体架构过渡到微服务,并构建出健壮、可扩展的分布式系统。随着业务发展,你可以在此基础上,逐步引入 gRPC 优化性能,或者探索服务网格来获得更强大的运维能力。