-
C#网络编程之微服务架构基础(服务发现、负载均衡)
第44章 微服务架构基础
44.1 微服务架构基础(服务发现、负载均衡)
一、我踩过的微服务坑:从“硬编码改到崩溃”到“负载均衡策略选错导致服务雪崩”
做电商微服务时,我一开始用硬编码配置服务地址,结果服务扩容到10台后,每次上线都要手动改10个配置文件,差点崩溃;后来用Consul做服务发现,又因为选了轮询负载均衡,把所有请求都打到了一台性能差的服务上,导致服务雪崩,订单超时率从0.1%涨到20%。这节我把这些踩坑经验揉进去,用大白话讲透服务发现和负载均衡的核心原理,结合C#实战代码逐行拆解,拓展生产级优化技巧,让你的微服务既灵活又稳定。
二、服务发现:微服务的“通讯录”,解决硬编码的噩梦
服务发现是微服务架构的核心组件,核心是“自动管理服务地址”,避免vb.net教程C#教程python教程SQL教程access 2010教程硬编码配置服务地址,解决服务动态扩容、缩容、故障转移的问题。
核心原理(大白话+通讯录例子)
把服务发现比作“公司通讯录”:
1.服务注册:新员工入职时,把自己的名字、工位号、电话加到通讯录(服务注册到Consul/Eureka);
2.服务发现:员工找同事时,直接查通讯录,不用记工位号(客户端从Consul/Eureka获取服务地址);
3.健康检查:通讯录自动标记离职员工(Consul定期检查服务健康状态,剔除不健康的服务)。
我踩过的坑:一开始没开健康检查,某个服务挂了,客户端还在调用它,导致大量超时——健康检查是服务发现的核心,必须开启!
实战1:用Consul实现服务发现(C#)
用Consul作为服务发现组件,实现服务注册、发现、健康检查。
步骤1:安装Consul和Consul.NET
1.下载Consul:Consul官网,启动Consul:
bash
2.
consul agent -dev -client=0.0.0.0
3.
4.安装NuGet包:
bash
5.
Install-Package Consul
6.
步骤2:服务注册代码(逐行讲解)
csharp
using Consul;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microservice.ServiceDiscovery;
public class ConsulServiceRegistry
{
private readonly ConsulClient _consulClient;
private readonly string _serviceName;
private readonly string _serviceId;
private readonly string _serviceAddress;
private readonly int _servicePort;
public ConsulServiceRegistry(string consulAddress, string serviceName, string serviceAddress, int servicePort)
{
_consulClient = new ConsulClient(c => c.Address = new Uri(consulAddress));
_serviceName = serviceName;
_serviceId = $"{serviceName}-{Guid.NewGuid():N}"; // 唯一服务ID,避免同一服务重复注册
_serviceAddress = serviceAddress;
_servicePort = servicePort;
}
/// <summary>
/// 注册服务到Consul
/// </summary>
public async Task RegisterServiceAsync()
{
// 1. 配置健康检查:每隔10秒发送一次HTTP请求,超时5秒,失败3次标记为不健康
var healthCheck = new AgentServiceCheck
{
HTTP = $"http://{_serviceAddress}:{_servicePort}/health", // 健康检查接口
Interval = TimeSpan.FromSeconds(10), // 检查间隔
Timeout = TimeSpan.FromSeconds(5), // 超时时间
DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1), // 不健康1分钟后自动注销
};
// 2. 注册服务
var registration = new AgentServiceRegistration
{
ID = _serviceId,
Name = _serviceName,
Address = _serviceAddress,
Port = _servicePort,
Check = healthCheck,
Tags = new[] { "v1", "api" } // 服务标签,用于过滤服务
};
await _consulClient.Agent.ServiceRegister(registration);
Console.WriteLine($"服务 {_serviceName} 注册成功,ID:{_serviceId}");
// 3. 程序退出时注销服务
AppDomain.CurrentDomain.ProcessExit += async (s, e) => await DeregisterServiceAsync();
}
/// <summary>
/// 注销服务
/// </summary>
public async Task DeregisterServiceAsync()
{
await _consulClient.Agent.ServiceDeregister(_serviceId);
Console.WriteLine($"服务 {_serviceName} 注销成功,ID:{_serviceId}");
}
}
// 测试代码:模拟订单服务注册
class Program
{
static async Task Main(string[] args)
{
var registry = new ConsulServiceRegistry(
"http://localhost:8500",
"order-service",
"localhost",
5000);
await registry.RegisterServiceAsync();
Console.WriteLine("服务已注册,按任意键退出...");
Console.ReadKey();
}
}
步骤3:服务发现代码(逐行讲解)
csharp
using Consul;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microservice.ServiceDiscovery;
public class ConsulServiceDiscoverer
{
private readonly ConsulClient _consulClient;
private readonly string _serviceName;
public ConsulServiceDiscoverer(string consulAddress, string serviceName)
{
_consulClient = new ConsulClient(c => c.Address = new Uri(consulAddress));
_serviceName = serviceName;
}
/// <summary>
/// 发现健康的服务实例
/// </summary>
public async Task<List<ServiceInstance>> DiscoverHealthyServicesAsync()
{
// 1. 查询服务实例,过滤健康的服务
var queryResult = await _consulClient.Health.Service(_serviceName, tag: null, passing: true);
if (queryResult.Response == null || !queryResult.Response.Any())
{
throw new InvalidOperationException($"没有健康的 {_serviceName} 服务实例");
}
// 2. 转换为ServiceInstance列表
var services = queryResult.Response.Select(s => new ServiceInstance
{
Address = s.Service.Address,
Port = s.Service.Port,
ServiceId = s.Service.ID,
Tags = s.Service.Tags
}).ToList();
Console.WriteLine($"发现 {services.Count} 个健康的 {_serviceName} 服务实例");
return services;
}
}
public class ServiceInstance
{
public string Address { get; set; }
public int Port { get; set; }
public string ServiceId { get; set; }
public string[] Tags { get; set; }
}
// 测试代码:模拟用户服务发现订单服务
class Program
{
static async Task Main(string[] args)
{
var discoverer = new ConsulServiceDiscoverer("http://localhost:8500", "order-service");
var services = await discoverer.DiscoverHealthyServicesAsync();
foreach (var service in services)
{
Console.WriteLine($"服务实例:{service.Address}:{service.Port},ID:{service.ServiceId}");
}
}
}
核心代码拆解:
1.服务注册:
1.用唯一服务ID避免同一服务重复注册;
2.配置健康检查,Consul定期调用/health接口,标记不健康的服务;
3.程序退出时自动注销服务,避免Consul中存在无效服务;
2.服务发现:
1.查询健康的服务实例(passing: true),只返回健康的服务;
2.转换为ServiceInstance列表,方便后续负载均衡使用;
3.健康检查接口:服务需要实现/health接口,返回200表示健康,比如ASP.NET Core的健康检查:
csharp
app.MapHealthChecks("/health");
拓展知识:服务发现的两种模式
| 模式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 客户端发现 | 性能好,无需中间件 | 客户端需要实现负载均衡,代码侵入性强 | 微服务架构,客户端可控的场景 |
| 服务端发现 | 客户端无侵入,配置简单 | 依赖中间件(如Nginx、Traefik),性能差 | 传统架构,客户端不可控的场景 |
我踩过的坑:一开始用服务端发现(Nginx),结果Nginx成为单点故障,换成客户端发现(Consul)后,配合负载均衡策略,解决了单点问题!
三、负载均衡:微服务的“调度员”,解决服务过载的问题
负载均衡是微服务架构的核心组件,核心是“把请求均匀分配到多个服务实例”,避免单个服务过载,提升系统的可用性和性能。
核心原理(大白话+排队叫号例子)
把负载均衡比作“银行排队叫号机”:
1.轮询:按顺序叫号,每个窗口处理相同数量的请求;
2.随机:随机叫号,适合服务性能相近的场景;
3.加权轮询:根据窗口的效率分配叫号数量,比如VIP窗口处理更多请求;
4.最少连接数:叫号给当前排队最少的窗口,适合服务性能不同的场景。
我踩过的坑:一开始用轮询,结果某个服务性能差(比如数据库慢),导致所有分配到这个服务的请求都超时,换成最少连接数后,请求自动分配到排队少的服务,解决了整体延迟高的问题!
实战2:实现常见负载均衡策略(C#)
用C#实现轮询、随机、加权轮询、最少连接数四种负载均衡策略,逐行讲解。
步骤1:定义负载均衡接口
csharp
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microservice.LoadBalancing;
public interface ILoadBalancer
{
Task<ServiceInstance> SelectAsync(List<ServiceInstance> services);
}
public class ServiceInstance
{
public string Address { get; set; }
public int Port { get; set; }
public string ServiceId { get; set; }
public int Weight { get; set; } = 1; // 权重,默认1
public int CurrentConnections { get; set; } = 0; // 当前连接数
}
步骤2:轮询策略实现(逐行讲解)
csharp
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microservice.LoadBalancing;
public class RoundRobinLoadBalancer : ILoadBalancer
{
private int _currentIndex = -1; // 当前选中的索引
public async Task<ServiceInstance> SelectAsync(List<ServiceInstance> services)
{
if (services == null || services.Count == 0)
{
throw new InvalidOperationException("没有可用的服务实例");
}
// 原子递增索引,避免多线程冲突
int nextIndex = System.Threading.Interlocked.Increment(ref _currentIndex) % services.Count;
// 索引溢出时重置为0
if (nextIndex < 0) nextIndex = 0;
var selectedService = services[nextIndex];
Console.WriteLine($"轮询策略选中服务:{selectedService.Address}:{selectedService.Port},索引:{nextIndex}");
return await Task.FromResult(selectedService);
}
}
核心代码拆解:
用Interlocked.Increment原子递增索引,避免多线程冲突;
用取模运算实现轮询,索引超过服务数量时自动重置为0;
适合服务性能相近的场景,实现简单,性能好。
步骤3:加权轮询策略实现(逐行讲解)
csharp
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microservice.LoadBalancing;
public class WeightedRoundRobinLoadBalancer : ILoadBalancer
{
private int _currentIndex = -1;
private int _currentWeight = 0;
private int _maxWeight;
private int _gcdWeight; // 所有权重的最大公约数
public WeightedRoundRobinLoadBalancer()
{
// 可以在构造函数中预计算最大公约数,或者在SelectAsync中计算
}
public async Task<ServiceInstance> SelectAsync(List<ServiceInstance> services)
{
if (services == null || services.Count == 0)
{
throw new InvalidOperationException("没有可用的服务实例");
}
// 计算最大权重和最大公约数
_maxWeight = services.Max(s => s.Weight);
_gcdWeight = CalculateGcd(services.Select(s => s.Weight));
while (true)
{
// 循环遍历服务实例
_currentIndex = (_currentIndex + 1) % services.Count;
if (_currentIndex == 0)
{
// 遍历完一轮,权重递减
_currentWeight -= _gcdWeight;
if (_currentWeight <= 0)
{
_currentWeight = _maxWeight;
if (_currentWeight == 0)
{
throw new InvalidOperationException("所有服务权重为0");
}
}
}
var service = services[_currentIndex];
if (service.Weight >= _currentWeight)
{
Console.WriteLine($"加权轮询策略选中服务:{service.Address}:{service.Port},权重:{service.Weight}");
return await Task.FromResult(service);
}
}
}
/// <summary>
/// 计算最大公约数
/// </summary>
private int CalculateGcd(IEnumerable<int> numbers)
{
return numbers.Aggregate((a, b) => Gcd(a, b));
}
private int Gcd(int a, int b)
{
while (b != 0)
{
int temp = b;
b = a % b;
a = temp;
}
return a;
}
}
核心代码拆解:
用最大公约数优化加权轮询,减少循环次数;
遍历服务实例,选中权重大于等于当前权重的服务;
适合服务性能不同的场景,比如给性能好的服务分配更高的权重。
步骤4:最少连接数策略实现(逐行讲解)
csharp
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microservice.LoadBalancing;
public class LeastConnectionsLoadBalancer : ILoadBalancer
{
public async Task<ServiceInstance> SelectAsync(List<ServiceInstance> services)
{
if (services == null || services.Count == 0)
{
throw new InvalidOperationException("没有可用的服务实例");
}
// 选中当前连接数最少的服务,如果有多个,选第一个
var selectedService = services.OrderBy(s => s.CurrentConnections).First();
// 原子递增连接数,避免多线程冲突
System.Threading.Interlocked.Increment(ref selectedService.CurrentConnections);
Console.WriteLine($"最少连接数策略选中服务:{selectedService.Address}:{selectedService.Port},当前连接数:{selectedService.CurrentConnections}");
return await Task.FromResult(selectedService);
}
/// <summary>
/// 释放连接数
/// </summary>
public void ReleaseConnection(ServiceInstance service)
{
if (service != null)
{
System.Threading.Interlocked.Decrement(ref service.CurrentConnections);
}
}
}
核心代码拆解:
按当前连接数排序,选中最少的服务;
用Interlocked.Increment原子递增连接数,避免多线程冲突;
调用服务后需要调用ReleaseConnection释放连接数,比如:
csharp
var service = await _loadBalancer.SelectAsync(services);
try
{
// 调用服务
}
finally
{
((LeastConnectionsLoadBalancer)_loadBalancer).ReleaseConnection(service);
}
适合服务性能不同的场景,自动分配请求到负载低的服务。
拓展知识:负载均衡的两种方式
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 客户端负载均衡 | 性能好,灵活可控 | 客户端需要实现负载均衡,代码侵入性强 | 微服务架构,客户端可控的场景 |
| 服务端负载均衡 | 客户端无侵入,配置简单 | 依赖中间件(如Nginx、Traefik),性能差 | 传统架构,客户端不可控的场景 |
我踩过的坑:用Nginx做服务端负载均衡,结果Nginx成为性能瓶颈,换成客户端负载均衡(Consul+最少连接数)后,QPS提升了30%!
四、服务发现与负载均衡的生产级最佳实践
-
服务发现最佳实践
开启健康检查:必须配置健康检查,剔除不健康的服务实例;
服务注册缓存:客户端缓存服务发现结果,避免每次调用都去Consul查询,比如用MemoryCache缓存5分钟;
服务发现重试:查询Consul失败时,重试几次,避免网络波动导致的服务不可用;
Consul集群:部署Consul集群,避免单点故障,至少3个节点;
服务标签:用标签区分不同版本、不同环境的服务,比如v1、prod。 -
负载均衡最佳实践
选择合适的策略:服务性能相近用轮询,性能不同用加权轮询或最少连接数;
故障转移:调用服务失败时,自动切换到其他服务实例,比如用Polly实现重试和熔断;
服务权重动态调整:根据服务的性能动态调整权重,比如CPU使用率高的服务降低权重;
连接数统计:最少连接数策略需要准确统计连接数,避免多线程冲突;
负载均衡缓存:缓存负载均衡的选中结果,避免每次调用都重新计算。 -
我踩过的坑总结
1.没开健康检查:某个服务挂了,客户端还在调用它,导致大量超时——必须开启健康检查;
2.轮询策略不适合性能不同的服务:某个服务性能差,导致整体延迟高——换成最少连接数;
3.Consul单点故障:Consul挂了,客户端无法发现服务——部署Consul集群;
4.服务发现没有缓存:每次调用都去Consul查询,导致Consul压力大——客户端缓存服务列表;
5.负载均衡没有故障转移:某个服务调用失败,直接返回错误——用Polly实现重试和熔断。
五、总结
服务发现解决了微服务动态扩容、缩容的问题,负载均衡解决了服务过载的问题,两者是微服务架构的核心组件。选择合适的服务发现模式(客户端/服务端)和负载均衡策略(轮询/最少连接数),结合生产级优化,能让你的微服务既灵活又稳定。
下一节我们会学习微服务网关:Ocelot的实战,解决微服务的统一入口问题。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49568.html










