在微服务中,Consul是注册中心,服务提供者、服务消费者等都要注册到Consul中,这样就可以实现服务提供者与服务消费者的隔离。
除了Consul之外,还有Eureka、Zookeeper等类似软件.
用DNS举例来理解Consul的作用:
# Consul 服务器安装
consul 下载地址 https://www.consul.io/
运行 consul.exe agent -dev
这是开发环境测试,生产环境要建集群,要至少一台 Server,多台 Agent。
consul 的监控页面 http://127.0.0.1:8500/
# .Net Core 连接 Consul
使用 Nuget 包管理工具
Install-Package Consul
# Rest 服务的准备
先使用使用默认生成的 ValuesController 做测试 再提供一个 HealthController.cs
[Route("api/[controller]")]
public class HealthController : Controller
{
[HttpGet]
public IActionResult Get(){
return Ok("ok");}
}
}
服务器从命令行中读取 ip 和端口。
# 让Rest服务注册到 Consul 中
在 Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//......
app.UseMvc();
//部署到不同服务器的时候不能写成 127.0.0.1 或者 0.0.0.0
//因为这是让服务消费者调用的地址
String ip = Configuration["ip"];
int port = int.Parse(Configuration["port"]);
var client = new ConsulClient(ConfigurationOverview);
var result = client.Agent.ServiceRegister(new AgentServiceRegistration()
{
ID = "WebApplication4" + Guid.NewGuid(),
Name = "WebApplication4",
Address = ip,
Port = port,
Check = new AgentServiceCheck
{
//服务启动多久后注册
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),
//健康检查时间间隔,或者称为心跳间隔
Interval = TimeSpan.FromSeconds(10),
//健康检查地址
HTTP = $"http://{ip}:{port}/api/health",
Timeout = TimeSpan.FromSeconds(5)
}
});
//......
}
private static void ConfigurationOverview(ConsulClientConfiguration obj)
{
obj.Address = new Uri("http://127.0.0.1:8500");
obj.Datacenter = "dc1";
}
注意不同实例一定要用不同的 Id,即使是相同服务的不同实例也要用不同的 Id,上面的代码用 Guid 做 Id,确保不重复。相同的服务用相同的 Name。Address、Port 是供服务消费者访问的服务器地址(或者 IP 地址)及端口号。Check 则是做服务健康检查的(解释一下)。
在注册服务的时候还可以通过 AgentServiceRegistration 的 Tags 属性设置额外的标签。
通过命令行启动两个实例
dotnet WebApplication4.dll --ip 127.0.0.1 --port 5001
dotnet WebApplication4.dll --ip 127.0.0.1 --port 5002
# 编写服务消费者
这里用控制台测试,真实项目中服务消费者通产哥也是另外一个 Web 应用。 nuget 安装:Consul、Newtonsoft.Json 首先编写一个 RestTemplate(模仿 Spring Cloud 中的)
public class RestTemplate
{
private String consulServerUrl;
public RestTemplate(String consulServerUrl = "http://127.0.0.1:8500")
{
this.consulServerUrl = consulServerUrl ;
}
/// <summary>
/// 获取服务的第一个实现地址
/// </summary>
/// <param name="consulClient"></param>
/// <param name="serviceName"></param>
/// <returns></returns>
private async Task<String> ResolveRootUrlAsync(String serviceName)
{
using (var consulClient =
new ConsulClient(c => c.Address = new Uri(consulServerUrl)))
{
var services = (await consulClient.Agent.Services()).Response;
var agentServices = services.Where(s =>
s.Value.Service.Equals(serviceName, StringComparison.CurrentCultureIgnoreCase)).Select(s => s.Value);
//TODO:注入负载均衡策略
var agentService = agentServices.ElementAt(Environment.TickCount %
agentServices.Count());
//根据当前TickCount对服务器个数取模,“随机”取一个机器出来
//避免“轮询” 的负载均衡策略需要计数加锁问题
return agentService.Address + ":" + agentService.Port;
}
}
private async Task<String> ResolveUrlAsync(String url)
{
Uri uri = new Uri(url);
String serviceName = uri.Host;
String realRootUrl = await ResolveRootUrlAsync(serviceName);
return uri.Scheme + "://" + realRootUrl + uri.PathAndQuery;
}
public async Task<ResponseEntity<T>> GetForEntityAsync<T>(String url, HttpRequestHeaders requestHeaders = null)
{
using (HttpClient httpClient = new HttpClient())
{
HttpRequestMessage requestMsg = new HttpRequestMessage();
if (requestHeaders != null)
{
foreach (var header in requestHeaders)
{
requestHeaders.Add(header.Key, header.Value);
}
}
requestMsg.Method = HttpMethod.Get;
requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url));
var result = await httpClient.SendAsync(requestMsg);
ResponseEntity<T> respEntity = new ResponseEntity<T>();
respEntity.StatusCode = result.StatusCode;
String bodyStr = await result.Content.ReadAsStringAsync();
respEntity.Body = JsonConvert.DeserializeObject<T>(bodyStr);
respEntity.Headers = respEntity.Headers;
return respEntity;
}
}
}
class ResponseEntity<T>
{
public HttpStatusCode StatusCode { get; set; }
public T Body { get; set; }
public HttpResponseHeaders Headers { get; set; }
}
}
编写控制台:
using Consul;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
RestTemplate rest = new RestTemplate();
var data =
rest.GetForEntityAsync<string[]>"http://WebApplication4/api/Values/").Result;
Console.WriteLine(data.StatusCode);
Console.WriteLine(string.Join(",", data.Body));
Console.ReadKey();
}
}
}
解析RestTemplate代码。主要作用:
- 根据url到Consul中根据服务的名字解析获取一个服务实例,把路径转换为实际连接的服务器;
负载均衡,这里用的是简单的随机负载均衡,这样服务的消费者就不用自己指定要访问那个服务提供 者了,解耦、负载均衡。
- 负载均衡还可以根据权重随机(不同服务器的性能不一样,这样注册服务的时候通过Tags来区
分),还可以根据消费者IP地址来选择服务实例(涉及到一致性Hash的优化)等。
- RestTemplate还负责把响应的json反序列化返回结果。
# 简化服务的注册
每次启动、注册服务都要指定一个端口,本地测试集群的时候可能要启动多个实例,很麻烦。 在 ASP.Net Core 中只要设定端口为 0,那么服务器会随机找一个可用的端口绑定(测试一下)。 但是没有找到读取到这个随机端口号的方法。 因此自己写:
/// <summary>
/// 产生一个介于 minPort-maxPort 之间的随机可用端口
/// </summary>
/// <param name="minPort"></param>
/// <param name="maxPort"></param>
/// <returns></returns>
public static int GetRandAvailablePort(int minPort=1024,int maxPort = 65535)
{
Random rand = new Random();
while(true)
{
int port = rand.Next(minPort, maxPort);
if (!IsPortInUsed(port))
{
return port;
}
}
}
/// <summary>
/// 判断 port 端口是否在使用中
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public static bool IsPortInUsed(int port)
{
//using System.Net.NetworkInformation;
IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
IPEndPoint[] ipsTCP = ipGlobalProperties.GetActiveTcpListeners();
if(ipsTCP.Any(p=>p.Port==port))
{
return true;
}
IPEndPoint[] ipsUDP = ipGlobalProperties.GetActiveUdpListeners();
if (ipsUDP.Any(p => p.Port == port))
{
return true;
}
TcpConnectionInformation[] tcpConnInfoArray = ipGlobalProperties.GetActiveTcpC
onnections();
if (tcpConnInfoArray.Any(conn => conn.LocalEndPoint.Port == port))
{
return true;
}
return false;
}
在程序启动的时候如果 port=0 或者没有指定 port,则自己调用 GetRandAvailablePort 获取可 用端口。
如鹏网《Net微服务》-by杨中科
← NLog教程 微服务 - JWT 算法 →