-
C#网络编程之微服务架构基础(服务发现、负载均衡)
第18章 微服务架构基础
18.1 微服务架构基础(服务发现、负载均衡)
一、我踩过的“微服务裸奔坑”:从硬编码到服务发现的血泪史
刚做第一个微服务项目时,为了图快,直接在客户端硬编码服务地址:var url = "http://192.168.1.100:8080/api/order";,结果服务扩容到3台服务器,需要修改所有客户端代码,上线时差点搞崩整个系统;后来用Nginx做反向代理,把流量分散到3台服务器,结果Nginx挂了,所有服务都访问不了——这就是没有服务发现和高可用负载均衡的后果!后来用Consul做服务发现,又遇到健康检查不及时的问题:服务已经崩溃了,但Consul还认为服务在线,导致客户端一直发请求到死服务,报错率飙升到50%;最后用Polly做客户端负载均衡,结合Consul的健康检查,终于解决了服务扩容、缩容、故障转移的问题。这节我把自己从“裸奔vb.net教程C#教程python教程SQL教程access 2010教程硬编码”到“智能服务发现+负载均衡”的踩坑经验揉进去,用大白话讲透服务发现和负载均衡的核心原理,结合C#实战代码逐行讲解Consul服务注册、服务发现、Polly客户端负载均衡、Yarp反向代理负载均衡的用法,以及常见坑和最佳实践,让你的微服务既灵活又稳定。
二、先搞懂微服务的核心痛点:“服务地址变来变去,怎么找?”
微服务的核心是拆分——把一个大系统拆成多个小服务(比如订单服务、用户服务、商品服务),每个服务可以独立部署、扩容、缩容。但拆分后带来两个核心问题:
1.服务地址动态变化:服务扩容时会新增服务器,缩容时会下线服务器,地址经常变,客户端怎么找服务?
2.流量分配不均:如果所有请求都发到一台服务器,这台服务器会崩溃,怎么把流量分散到多台服务器?
类比:微服务就像你开了多家分店(服务提供者),顾客(客户端)不知道哪家分店开门,也不知道哪家分店人少——服务发现就是“分店导航”,告诉顾客哪家分店开门;负载均衡就是“排队引导员”,把顾客引导到人少的分店。
三、服务发现:“分店导航”,让客户端找到服务
服务发现是微服务的核心组件——它负责管理所有服务的地址、健康状态,客户端可以通过服务发现找到健康的服务地址。
核心角色
服务注册中心:Consul、Eureka、Nacos,就像“分店导航系统”,存储所有服务的地址和健康状态;
服务提供者:订单服务、用户服务,启动时把自己的地址注册到注册中心,下线时注销;
服务消费者:客户端,从注册中心获取健康的服务地址,然后发送请求。
服务发现流程
1.服务提供者启动,向注册中心注册自己的地址、端口、健康检查地址;
2.注册中心定期向服务提供者发送健康检查请求,检查服务是否在线;
3.服务消费者从注册中心获取健康的服务地址列表;
4.服务消费者用负载均衡策略选择一个服务地址,发送请求;
5.服务提供者下线时,向注册中心注销自己的地址,注册中心更新服务地址列表。
实战1:用Consul做服务注册与发现(C#实战)
Consul是目前最流行的服务注册中心,支持服务注册、健康检查、KV存储、多数据中心,用Go语言开发,性能好,稳定性高。
步骤1:安装Consul(Windows)
从Consul官网下载压缩包,解压后运行:
bash
consul agent -dev
访问http://localhost:8500,打开Consul的Web UI,看到Consul已经启动。
步骤2:服务提供者注册到Consul(ASP.NET Core)
csharp
// Program.cs(ASP.NET Core 8.0)
var builder = WebApplication.CreateBuilder(args);
// 1. 配置Consul服务注册
builder.Services.AddConsul(consulConfig =>
{
// Consul地址
consulConfig.Address = new Uri("http://localhost:8500");
// 服务注册配置
consulConfig.ServiceRegistration = new AgentServiceRegistration
{
ID = "order-service-1", // 服务实例唯一ID(每个实例ID不同)
Name = "order-service", // 服务名称(所有实例名称相同)
Address = "127.0.0.1",
Port = 5001,
// 健康检查配置:每隔10秒检查一次,超时5秒,失败3次后标记为不健康
Check = new AgentServiceCheck
{
HTTP = "http://127.0.0.1:5001/health",
Interval = TimeSpan.FromSeconds(10),
Timeout = TimeSpan.FromSeconds(5),
DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1)
}
};
});
// 2. 添加健康检查接口
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
// 健康检查接口
app.MapGet("/health", () => Results.Ok("Healthy"));
// 3. 启动时注册到Consul,停止时注销
app.UseConsul();
app.Run();
代码逐行讲解:
1.AddConsul:配置Consul地址和服务注册信息——ID是服务实例的唯一ID(比如启动多个实例,ID要不同,比如order-service-1、order-service-2),Name是服务名称(所有实例名称相同);
2.AgentServiceCheck:健康检查配置——HTTP是健康检查地址,Interval是检查间隔,Timeout是超时时间,DeregisterCriticalServiceAfter是服务不健康1分钟后自动注销;
3.UseConsul:中间件会在应用启动时注册到Consul,停止时注销,避免服务下线后注册中心还保留地址;
4./health:健康检查接口,返回200表示健康,返回500表示不健康。
我踩过的坑:健康检查地址写错了,Consul认为服务不健康,客户端找不到服务——一定要确保健康检查接口能正常返回200!
步骤3:服务消费者从Consul获取服务地址(C#实战)
csharp
// 服务消费者控制台程序
using Consul;
using System.Net.Http;
using System.Threading.Tasks;
namespace ServiceConsumer;
class Program
{
static async Task Main(string[] args)
{
// 1. 创建Consul客户端
var consulClient = new ConsulClient(config =>
{
config.Address = new Uri("http://localhost:8500");
});
// 2. 从Consul获取健康的服务地址列表
var serviceResponse = await consulClient.Health.Service("order-service", tag: null, passing: true);
if (serviceResponse.Response.Length == 0)
{
Console.WriteLine("没有健康的服务实例");
return;
}
// 3. 简单的轮询负载均衡(选择第一个实例,实际项目用Polly或Ocelot)
var service = serviceResponse.Response[0].Service;
var serviceUrl = $"http://{service.Address}:{service.Port}";
Console.WriteLine($"找到健康服务:{serviceUrl}");
// 4. 发送请求到服务提供者
var httpClient = new HttpClient();
var response = await httpClient.GetAsync($"{serviceUrl}/api/order/1");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"服务响应:{content}");
}
}
代码逐行讲解:
1.Health.Service:获取健康的服务实例——passing: true表示只获取健康的实例(健康检查通过的);
2.轮询负载均衡:简单的轮询,实际项目建议用成熟的负载均衡库(比如Polly、Ocelot);
3.服务地址拼接:从Consul获取服务的Address和Port,拼接成完整的URL,避免硬编码。
服务发现的常见坑与最佳实践
1.服务实例ID必须唯一:每个服务实例的ID要不同,否则Consul会覆盖之前的注册信息;
2.健康检查配置要合理:检查间隔不要太短(比如1秒,会增加Consul的压力),也不要太长(比如1分钟,服务不健康后客户端还会发请求),建议10-30秒;
3.注册中心高可用:不要只用一个Consul节点,用Consul集群(至少3个节点),避免注册中心单点故障;
4.服务注销要及时:服务下线时要主动注销,或者配置DeregisterCriticalServiceAfter,避免注册中心保留不健康的服务地址;
5.本地缓存服务地址:客户端可以本地缓存服务地址,定期从注册中心更新,减少注册中心的压力。
三、负载均衡:“排队引导员”,把流量分散到多台服务器
负载均衡是把流量分散到多台服务器,避免单台服务器崩溃,同时提高系统的吞吐量和可用性。常见的负载均衡类型有两种:
服务端负载均衡:Nginx、Yarp、F5,流量先到负载均衡器,再分发到服务器;
客户端负载均衡:Polly、Ribbon,客户端自己选择服务器地址,直接发送请求。
类比:服务端负载均衡就像“商场入口的引导员”,所有顾客先到入口,引导员再分配到不同楼层;客户端负载均衡就像“顾客自己看导航,直接去人少的楼层”。
实战2:用Polly做客户端负载均衡(C#实战)
Polly是C#的弹性库,支持负载均衡、重试、熔断、降级,非常适合微服务客户端。结合Consul服务发现,实现客户端负载均衡。
步骤1:安装Polly NuGet包
bash
Install-Package Polly
Install-Package Polly.Extensions.Http
步骤2:客户端负载均衡+服务发现+重试
csharp
using Consul;
using Polly;
using Polly.Extensions.Http;
using System.Net.Http;
using System.Threading.Tasks;
namespace PollyLoadBalancer;
class Program
{
static async Task Main(string[] args)
{
// 1. 创建Consul客户端
var consulClient = new ConsulClient(config =>
{
config.Address = new Uri("http://localhost:8500");
});
// 2. 配置HttpClient,添加Polly负载均衡、重试策略
var httpClient = new HttpClient(new PolicyHttpMessageHandler(GetRetryPolicy())
{
InnerHandler = new HttpClientHandler()
});
// 3. 从Consul获取健康服务地址列表
var serviceResponse = await consulClient.Health.Service("order-service", tag: null, passing: true);
if (serviceResponse.Response.Length == 0)
{
Console.WriteLine("没有健康的服务实例");
return;
}
// 4. Polly轮询负载均衡策略
var instances = serviceResponse.Response.Select(s => $"http://{s.Service.Address}:{s.Service.Port}").ToList();
var loadBalancerPolicy = Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.Or<HttpRequestException>()
.RetryForeverAsync(async (result, context) =>
{
// 重试时选择下一个服务实例
var currentIndex = context.Get<int>("index");
var nextIndex = (currentIndex + 1) % instances.Count;
context["index"] = nextIndex;
Console.WriteLine($"请求失败,重试下一个实例:{instances[nextIndex]}");
});
// 5. 发送请求,用负载均衡策略
var index = 0;
var response = await loadBalancerPolicy.ExecuteAsync(async context =>
{
var url = $"{instances[context.Get<int>("index")]}/api/order/1";
return await httpClient.GetAsync(url);
}, new Context { ["index"] = index });
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"服务响应:{content}");
}
// Polly重试策略:请求失败时重试3次
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError() // 处理网络错误、500、502、503、504
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); // 指数退避重试:1秒、2秒、4秒
}
}
代码逐行讲解:
1.PolicyHttpMessageHandler:把Polly策略添加到HttpClient,实现重试、熔断;
2.RetryForeverAsync:请求失败时永远重试,选择下一个服务实例;
3.HandleTransientHttpError:处理临时错误(网络错误、500、502、503、504);
4.WaitAndRetryAsync:指数退避重试,避免短时间内发送大量请求,加重服务器压力;
5.Context:存储当前选择的服务实例索引,重试时切换到下一个实例。
我踩过的坑:没有设置重试间隔,请求失败时立即重试,导致服务器压力更大——一定要用指数退避重试,间隔逐渐变长!
实战3:用Yarp做服务端反向代理负载均衡(ASP.NET Core)
Yarp是微软官方的反向代理库,适合做服务端负载均衡,支持路由、负载均衡、健康检查、限流。
步骤1:安装Yarp NuGet包
bash
Install-Package Yarp.ReverseProxy
步骤2:配置Yarp反向代理(Program.cs)
csharp
// Program.cs(ASP.NET Core 8.0)
var builder = WebApplication.CreateBuilder(args);
// 1. 添加Yarp反向代理
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
// 2. 启用反向代理
app.MapReverseProxy();
app.Run();
appsettings.json配置:
json
{
"ReverseProxy": {
"Routes": {
"order-service-route": {
"ClusterId": "order-service-cluster",
"Match": {
"Path": "/api/order/{**catch-all}"
}
}
},
"Clusters": {
"order-service-cluster": {
"LoadBalancingPolicy": "RoundRobin", // 轮询负载均衡策略
"Destinations": {
"order-service-1": {
"Address": "http://127.0.0.1:5001"
},
"order-service-2": {
"Address": "http://127.0.0.1:5002"
}
},
"HealthCheck": {
"Path": "/health",
"Interval": "00:00:10", // 10秒检查一次
"Timeout": "00:00:05" // 5秒超时
}
}
}
}
}
代码逐行讲解:
1.AddReverseProxy:添加Yarp反向代理服务;
2.LoadFromConfig:从配置文件加载路由和集群配置;
3.Routes:路由配置——把路径/api/order/**的请求转发到order-service-cluster集群;
4.Clusters:集群配置——包含多个服务实例,LoadBalancingPolicy是负载均衡策略(RoundRobin轮询、LeastConnections最少连接、Random随机);
5.HealthCheck:健康检查配置——定期检查服务实例的健康状态,不健康的实例会被排除在负载均衡之外。
Yarp负载均衡策略对比:
策略 适用场景 优点
RoundRobin(轮询) 所有服务实例性能相同 简单公平,适合大多数场景
LeastConnections(最少连接) 服务实例性能不同,请求处理时间不同 把请求发送到连接最少的实例,更公平
Random(随机) 服务实例性能相同,需要分散流量 简单,适合高并发场景
负载均衡的常见坑与最佳实践
1.服务端负载均衡vs客户端负载均衡:
服务端负载均衡:适合统一管理流量,比如Nginx、Yarp,缺点是单点故障(需要集群);
客户端负载均衡:适合微服务客户端,直接访问服务实例,减少中间环节,缺点是客户端需要集成负载均衡库;
2.健康检查必须启用:不管是服务端还是客户端负载均衡,都要启用健康检查,排除不健康的实例;
3.负载均衡策略选择:根据服务实例的性能选择——性能相同用轮询,性能不同用最少连接;
4.避免会话粘滞:除非必要,不要用会话粘滞(把同一个用户的请求发到同一台服务器),会导致流量分配不均;
5.监控负载均衡状态:用Prometheus、Grafana监控每个服务实例的请求数、响应时间、错误率,发现流量分配不均及时调整。
四、总结:服务发现与负载均衡的核心价值
组件 核心价值 适用场景
服务发现 动态管理服务地址,找到健康服务 微服务、分布式系统,服务地址动态变化
负载均衡 分散流量到多台服务器,避免单点故障 高并发场景,需要扩容、缩容的服务
最佳实践:
1.注册中心高可用:用Consul集群(至少3个节点),避免注册中心单点故障;
2.健康检查合理配置:检查间隔10-30秒,超时5-10秒,不健康实例及时注销;
3.分层负载均衡:客户端负载均衡+服务端负载均衡,比如Yarp反向代理+Polly客户端负载均衡,多层分散流量;
4.监控告警:监控注册中心的服务实例数、健康状态,负载均衡的流量分配、错误率,设置告警阈值;
5.自动化扩容缩容:用Kubernetes、Docker Swarm结合服务发现,实现服务的自动扩容、缩容,根据流量自动调整服务实例数。
下一节我们会学习微服务的通信方式:RESTful API、gRPC、消息队列,让你的微服务之间通信既高效又可靠。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49543.html










