-
C#网络编程之微服务架构基础(服务发现、负载均衡)
第24章 微服务架构基础
24.1 微服务架构基础(服务发现、负载均衡)
一、我踩过的微服务坑:从“硬编码地址”到“服务雪崩”
刚做第一个微服务项目时,为了图快,把商品服务的地址硬编码在订单服务里:var productServiceUrl = "http://192.168.1.100:5000/api/products";,结果商品服务扩容到3台机器,订单服务还是只调用第一台,导致第一台机器CPU跑满,其他两台闲置;后来用Nginx做负载均衡,把地址改成http://nginx:80/api/products,结果Nginx挂了,整个电商系统直接瘫痪,损失了20万订单;再后来用Consul做服务发现,订单服务自动发现所有商品服务节点,还能自动剔除挂掉的节点,系统稳定性直接提升到99.99%。这节我把自己从“微服务小白”到“架构师”的踩坑经验揉进去,用大白话讲透服务发现和负载均衡的核心原理,结合C#实战代码逐行讲解,以及常见坑和最佳实践,让你的微服务既灵活又稳定。
二、服务发现:微服务的“通讯录”,自动找到服务在哪里
服务发现是微服务架构的核心组件,它就像一个动态更新的通讯录:服务启动时把自己的地址注册到“通讯录”,服务调用时从“通讯录”里找到目标服务的地址——解决了硬编码地址的问题,服务扩容、缩容、故障时自动更新地址,不需要修改代码。
核心原理(大白话)
1.服务注册:服务启动时,把自己的IP、端口、服务名、健康检查地址等信息注册到注册中心(比如Consul、Eureka);
2.服务发现:服务调用时,从注册中心获取目标服务的所有可用地址;
3.健康检查:注册中心定期检查服务的健康状态,如果服务挂了,自动从通讯录里删除它的地址;
4.动态更新:服务扩容、缩容时,注册中心自动更新通讯录,服务调用时总能拿到最新的可用地址。
服务发现的两种模式(拓展知识)
1.客户端发现:服务消费者自己从注册中心获取地址,然后选择一个地址调用(比如Consul + Polly);
2.服务器端发现:服务消费者调用一个统一的负载均衡器(比如Nginx、K8s Service),负载均衡器从注册中心获取地址,选择一个地址转发请求。
类比:客户端发现就像你自己查通讯录,然后打电话给朋友;服务器端发现就像你打给总机,总机帮你转接到朋友的电话。
实战1:用Consul做服务发现(C#实战)
Consul是.NET生态最常用的服务发现工具,支持服务注册、健康检查、KV存储等功能,开箱即用。
步骤1:安装Consul(开发环境)
从官网下载Consul:https://www.consul.io/downloads,然后启动:
bash
# 开发环境启动单节点Consul
consul agent -dev -ui
访问http://localhost:8500,就能看到Consul的UI界面,管理服务注册和发现。
步骤2:服务注册(商品服务)
用Consul.NET库把商品服务注册到Consul:
csharp
using System;
using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// 1. 注册Consul客户端
builder.Services.AddSingleton<IConsulClient>(sp => new ConsulClient(config =>
{
// Consul地址(开发环境)
config.Address = new Uri("http://localhost:8500");
}));
builder.Services.AddControllers();
var app = builder.Build();
// 2. 注册服务到Consul
var consulClient = app.Services.GetRequiredService<IConsulClient>();
var serviceId = Guid.NewGuid().ToString(); // 每个服务实例的唯一ID
var serviceName = "ProductService"; // 服务名,订单服务通过这个名字发现
var ipAddress = "127.0.0.1"; // 服务的IP地址(生产环境用真实IP)
var port = 5001; // 服务的端口
// 服务注册参数
var registration = new AgentServiceRegistration
{
ID = serviceId,
Name = serviceName,
Address = ipAddress,
Port = port,
// 健康检查:Consul每隔10秒访问一次/health,返回200则认为服务健康
Check = new AgentServiceCheck
{
HTTP = $"http://{ipAddress}:{port}/health",
Interval = TimeSpan.FromSeconds(10), // 检查间隔
Timeout = TimeSpan.FromSeconds(5), // 超时时间
DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1), // 服务不健康1分钟后从注册中心删除
}
};
// 注册服务
consulClient.Agent.ServiceRegister(registration).Wait();
// 3. 健康检查接口
app.MapGet("/health", () => Results.Ok("ProductService is healthy!"));
app.MapControllers();
// 4. 服务停止时从Consul注销
app.Lifetime.ApplicationStopping.Register(() =>
{
consulClient.Agent.ServiceDeregister(serviceId).Wait();
});
app.Run($"http://{ipAddress}:{port}");
代码逐行讲解:
1.AddSingleton
2.AgentServiceRegistration:服务注册的核心参数:
1.ID:每个服务实例的唯一ID,比如订单服务有3台机器,每台机器的ID不同;
2.Name:服务名,订单服务通过这个名字发现商品服务;
3.Check:健康检查配置,Consul每隔10秒访问一次/health接口,如果返回200,认为服务健康,否则标记为不健康,1分钟后从注册中心删除;
3.ApplicationStopping.Register:服务停止时从Consul注销,避免注册中心里残留无效的服务地址;
4./health接口:返回200表示服务健康,Consul通过这个接口判断服务是否可用。
我踩过的坑:一开始忘记配置健康检查,结果商品服务挂了,Consul还认为它在线,订单服务一直调用挂掉的地址,导致大量错误——健康检查是服务发现的核心,必须配置!
步骤3:服务发现(订单服务调用商品服务)
订单服务从Consul获取商品服务的地址,然后调用:
csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// 1. 注册Consul客户端
builder.Services.AddSingleton<IConsulClient>(sp => new ConsulClient(config =>
{
config.Address = new Uri("http://localhost:8500");
}));
// 2. 注册HttpClient,用于调用商品服务
builder.Services.AddHttpClient();
builder.Services.AddControllers();
var app = builder.Build();
// 3. 订单服务调用商品服务的接口
app.MapGet("/api/orders/{id}", async (string id, IConsulClient consulClient, HttpClient httpClient) =>
{
// 从Consul获取商品服务的所有可用地址
var serviceName = "ProductService";
var queryResult = await consulClient.Health.Service(serviceName, passing: true); // passing: true表示只获取健康的服务
if (queryResult.Response == null || queryResult.Response.Count == 0)
{
return Results.StatusCode(503, "No healthy ProductService available");
}
// 简单的负载均衡:随机选择一个服务地址(后面会讲更复杂的策略)
var random = new Random();
var service = queryResult.Response[random.Next(queryResult.Response.Count)].Service;
var productServiceUrl = $"http://{service.Address}:{service.Port}/api/products/123";
// 调用商品服务
var productResponse = await httpClient.GetAsync(productServiceUrl);
var product = await productResponse.Content.ReadAsStringAsync();
return Results.Ok($"Order {id} created, product info: {product}");
});
app.Run("http://localhost:5000");
代码逐行讲解:
1.Health.Service:从Consul获取健康的商品服务地址,passing: true表示只返回健康的服务;
2.随机负载均衡:从可用地址里随机选一个,简单但有效,后面会讲轮询、加权轮询等更复杂的策略;
3.调用商品服务:用HttpClient调用商品服务的API,拿到商品信息后返回给客户端。
拓展知识:Consul vs Eureka
| 特性 | Consul | Eureka |
|---|---|---|
| 健康检查 | 支持HTTP、TCP、脚本等多种方式 | 仅支持HTTP方式 |
| 一致性协议 | Raft(强一致性) | AP(最终一致性) |
| KV存储 | 支持(用于配置中心) | 不支持 |
| 多数据中心 | 支持 | 支持 |
| .NET生态支持 | 好(Consul.NET库成熟) | 一般(Eureka.NET维护不活跃) |
| 适用场景 | 中小团队、需要强一致性的场景 | 大团队、需要高可用的场景 |
最佳实践:
1.必须配置健康检查:至少配置HTTP健康检查,Consul定期检查服务状态;
2.服务名要统一:比如所有商品服务都叫“ProductService”,不要用“ProductServiceV1”、“ProductServiceV2”,版本管理用标签;
3.注册中心集群部署:开发环境用单节点,生产环境用3-5节点集群,避免注册中心单点故障;
4.服务注销要及时:服务停止时要从Consul注销,避免残留无效地址。
三、负载均衡:微服务的“调度员”,把请求均匀分给服务
负载均衡是微服务架构的核心组件,它就像一个调度员:把请求均匀分给多个服务实例,避免单个实例压力过大,提高系统的吞吐量和可用性——解决了单节点性能瓶颈的问题,服务扩容时自动分担压力,故障时自动跳过挂掉的实例。
核心原理(大白话)
1.请求分发:负载均衡器把请求分给多个服务实例;
2.负载策略:根据不同的策略选择实例(比如轮询、随机、最少连接数);
3.健康检查:负载均衡器只把请求分给健康的实例;
4.动态调整:服务扩容、缩容时,负载均衡器自动调整分发策略。
负载均衡的两种模式(拓展知识)
1.客户端负载均衡:服务消费者自己选择实例调用(比如Consul + Polly);
2.服务器端负载均衡:请求先到负载均衡器,再转发到服务实例(比如Nginx、K8s Service)。
类比:客户端负载均衡就像你自己选一个窗口办理业务;服务器端负载均衡就像你到取号机取号,取号机帮你分配到一个窗口。
实战2:客户端负载均衡(Consul + Polly)
客户端负载均衡适合微服务内部调用,不需要额外的负载均衡器,服务消费者自己选择实例,延迟更低。
步骤1:安装Polly NuGet包
Polly是.NET生态最常用的弹性库,支持负载均衡、重试、熔断等功能:
bash
Install-Package Polly
Install-Package Polly.Extensions.Http
步骤2:用Polly实现轮询负载均衡
csharp
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Polly;
using Polly.Extensions.Http;
var builder = WebApplication.CreateBuilder(args);
// 1. 注册Consul客户端
builder.Services.AddSingleton<IConsulClient>(sp => new ConsulClient(config =>
{
config.Address = new Uri("http://localhost:8500");
}));
// 2. 注册HttpClient,配置Polly的负载均衡和重试策略
builder.Services.AddHttpClient("ProductServiceClient")
// 重试策略:调用失败时重试3次,间隔1秒
.AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
// 负载均衡策略:轮询选择服务实例
.AddPolicyHandler((services, request) =>
{
var consulClient = services.GetRequiredService<IConsulClient>();
return new Policy<HttpResponseMessage>(async (context, token) =>
{
// 从Consul获取健康的商品服务地址
var serviceName = "ProductService";
var queryResult = await consulClient.Health.Service(serviceName, passing: true, token: token);
if (queryResult.Response == null || queryResult.Response.Count == 0)
{
return PolicyResult<HttpResponseMessage>.Failure(new HttpResponseMessage(System.Net.HttpStatusCode.ServiceUnavailable));
}
// 轮询策略:用Interlocked.Increment实现线程安全的轮询
var index = Interlocked.Increment(ref _roundRobinIndex) % queryResult.Response.Count;
var service = queryResult.Response[index].Service;
var productServiceUrl = $"http://{service.Address}:{service.Port}{request.RequestUri.PathAndQuery}";
// 修改请求的地址
request.RequestUri = new Uri(productServiceUrl);
return PolicyResult<HttpResponseMessage>.Success();
});
});
builder.Services.AddControllers();
var app = builder.Build();
// 3. 订单服务调用商品服务的接口
app.MapGet("/api/orders/{id}", async (string id, IHttpClientFactory httpClientFactory) =>
{
var httpClient = httpClientFactory.CreateClient("ProductServiceClient");
var productResponse = await httpClient.GetAsync("/api/products/123");
productResponse.EnsureSuccessStatusCode(); // 如果调用失败,抛出异常,触发Polly的重试
var product = await productResponse.Content.ReadAsStringAsync();
return Results.Ok($"Order {id} created, product info: {product}");
});
app.Run("http://localhost:5000");
// 轮询索引,线程安全
private static int _roundRobinIndex = -1;
代码逐行讲解:
1.AddPolicyHandler:配置Polly的策略,这里用了两种策略:
1.重试策略:调用失败时(比如503、超时)重试3次,每次间隔翻倍(1秒、2秒、4秒),避免瞬间大量请求失败;
2.轮询负载均衡策略:用Interlocked.Increment实现线程安全的轮询,每次请求选择下一个服务实例,请求分布均匀;
2.IHttpClientFactory:用HttpClientFactory创建HttpClient,避免HttpClient的套接字耗尽问题;
3.修改请求地址:从Consul获取服务地址后,修改请求的RequestUri,然后调用商品服务。
我踩过的坑:一开始用随机负载均衡,结果请求分布不均,有的实例压力大,有的实例压力小,后来改用轮询,请求分布均匀——轮询是最常用的负载均衡策略,简单且有效。
实战3:服务器端负载均衡(Nginx配置)
服务器端负载均衡适合外部请求(比如用户访问网站、APP调用API),Nginx是最常用的服务器端负载均衡器。
Nginx配置文件(nginx.conf)
nginx
http {
upstream product_service {
# 轮询策略(默认)
server 192.168.1.100:5001 max_fails=3 fail_timeout=30s;
server 192.168.1.101:5001 max_fails=3 fail_timeout=30s;
server 192.168.1.102:5001 max_fails=3 fail_timeout=30s;
# 加权轮询策略:权重越高,分到的请求越多
# server 192.168.1.100:5001 weight=2;
# server 192.168.1.101:5001 weight=1;
# server 192.168.1.102:5001 weight=1;
# 最少连接数策略:把请求分给连接数最少的实例
# least_conn;
}
server {
listen 80;
server_name api.example.com;
location /api/products {
proxy_pass http://product_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
配置逐行讲解:
1.upstream product_service:定义商品服务的上游服务器组;
2.max_fails=3 fail_timeout=30s:如果3次请求失败,标记为不可用,30秒后再重试;
3.轮询策略:默认策略,请求均匀分给每个实例;
4.加权轮询策略:权重越高,分到的请求越多,比如权重2的实例分到的请求是权重1的2倍;
5.最少连接数策略:把请求分给当前连接数最少的实例,适合请求处理时间差异大的场景;
6.proxy_pass:把请求转发到上游服务器组。
拓展知识:负载均衡算法对比
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 轮询 | 简单、请求分布均匀 | 不考虑实例的性能差异 | 所有实例性能相同的场景 |
| 加权轮询 | 能根据实例性能调整请求分布 | 权重配置复杂 | 实例性能不同的场景(比如高配机器权重高) |
| 随机 | 简单、实现容易 | 请求分布不均 | 小流量场景 |
| 最少连接数 | 考虑实例的负载情况 | 需要统计连接数,性能开销大 | 请求处理时间差异大的场景 |
| IP哈希 | 同一IP的请求分到同一个实例 | 实例扩容时请求分布变化大 | 需要会话保持的场景(比如购物车) |
最佳实践:
1.根据场景选择策略:一般场景用轮询,实例性能不同用加权轮询,请求处理时间差异大用最少连接数;
2.配置健康检查:Nginx配置max_fails和fail_timeout,自动跳过挂掉的实例;
3.客户端和服务器端结合:外部请求用Nginx服务器端负载均衡,内部调用用客户端负载均衡;
4.监控负载情况:用Prometheus、Grafana监控每个实例的CPU、内存、请求数,及时调整负载策略。
四、总结:服务发现和负载均衡的核心作用
特性 服务发现 负载均衡
核心作用 动态获取服务地址,解决硬编码问题 均匀分发请求,解决单节点瓶颈问题
核心组件 注册中心(Consul、Eureka) 负载均衡器(Polly、Nginx)
适用场景 微服务内部调用、服务扩容缩容 所有需要分发请求的场景
最佳实践 必须配置健康检查、集群部署注册中心 根据场景选择策略、配置健康检查
微服务架构的核心思想
服务发现和负载均衡是微服务架构的基础,它们让微服务变得灵活、可扩展、高可用:
1.灵活:服务扩容、缩容时不需要修改代码,注册中心自动更新地址;
2.可扩展:增加服务实例就能提高系统的吞吐量;
3.高可用:服务故障时自动跳过,请求不会被分到挂掉的实例。
下一节我们会学习微服务的通信协议:REST、gRPC、消息队列,让你的微服务通信既高效又可靠。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49554.html










