.Net Core with 微服务 - Consul 注册中心
上一次我们介绍了 Ocelot 网关的基本用法。这次我们开始介绍服务注册发现组件 Consul 的简单使用方法。
服务注册发现#
首先先让我们回顾下服务注册发现的概念。
在实施微服务之后,我们的调用都变成了服务间的调用。服务间调用需要知道IP、端口等信息。再没有微服务之前,我们的调用信息一般都是写死在调用方的配置文件里(当然这话不绝对,有些公司会把这些信息写到数据库等公共的地方,以方便维护)。又由于业务的复杂,每个服务可能依赖N个其他服务,如果某个服务的IP,端口等信息发生变更,那么所有依赖该服务的服务的配置文件都要去修改,这样显然太麻烦了。有些服务为了负载是有个多个实例的,而且可能是随时会调整实例的数量。如果每次调整实例数量都要去修改其他服务的配置并重启那太麻烦了。
为了解决这个问题,业界就有了服务注册发现组件。
假设我们有服务A需要调用服务B,并且有服务注册发现组件R。整个大致流程将变成大噶3部:
- 服务B启动向服务R注册自己的信息
- 服务A从服务R拉取服务B的信息
- 服务A调用服务B
有了服务注册发现组件之后,当修改A服务信息的时候再也不用去修改其他相关服务了。
Consul#
Consul 是 HashiCorp 公司推出的一套服务注册发现工具。它使用 golang 编写并且开源。由于使用 golang 的缘故所以它天生跨平台而且部署简单。它带有 web 管理后台方便用户查看维护 Consul 集群。其实除了服务注册发现功能,Consul 还支持 Key/Value 存储可以当一个简单的配置中心使用。
架构#
上面是 Consul 官网上画的架构图。从图上可以看到 Consul 天生支持多数据中心部署。每个数据中心内部有多个 Consul 节点。Consul 的节点分为2种。
- Server
Server 模式的节点是真正意义上集群的节点。它通过RAFT算法实现CAP里的CA。当Leader Server 挂掉的时候会自动选举出新的 Leader 使集群继续正常工作。 - Client
Client 模式的节点虽然也叫节点,但是它并不会持久化数据,不维持状态,它仅仅是转发客户端的请求给后面的 Server 节点,同时负责注册到该 client 节点的服务的健康检测。它非常轻量级,按照 Consul 的说法最好是每个服务都配一个 client 。
为什么要有client模式的节点#
我初看 Consul 这套架构的时候觉得很奇怪,为什么要在 Server 节点跟真正的服务之间插入一层 client 模式的节点。按照按照 Consul 的说法还得每个服务配一个 client 节点。
经过思考说说我的一些看法。在这个模式下服务不在关心真正的集群在哪,集群的节点有哪些,只需要知道这个伴随的 client 节点的地址就行了。通过这个 client 节点去感知到真正可用的 server 节点,所有跟 server 节点的交互全部交给 client 节点代理去完成,这就简化了服务跟 consul 交互的难度。还有一个好处是服务的健康检测由 client 节点负责,在一定程度上减轻了 server 节点的压力。当然这也会带来一个问题,那就是如果 client 挂了,那么服务可能就连不上 Consul 集群了,因为对于服务来说这个 client 节点相当于是单点的。
使用 docker 运行 Consul#
docker run -p 8500:8500 --name=consulserver consul agent -server -bootstrap -client=0.0.0.0 -ui -node=0
使用 docker 命令运行初始化一个 consul 的 server 模式的节点。
- -server 启动为Server模式
- -bootstrap 设置为启动模式,这是第一个server节点,等待其它节点的加入
- -client 指定可以访问的客户端IP 。
- -ui 开启管理界面
- -node 节点的名字
docker run -d --name=consulserver1 consul agent -server -node=1 -join=172.17.0.2
有了第一个节点,我们可以开始创建更多的 Server 节点来构造集群。Consul 推荐至少3个 Server 来组建集群。上面的 docker 命令表示启动第二个 Server 然后加入第一个节点构造的集群。
- -join 加入某个集群,这里的 IP 为第一个启动的节点的内网 IP 。可以通过 docker exec XXX consul members 命令查看。后面会演示。
docker run --name=consulclient0 -e consul agent -client=0.0.0.0 -node=client0 -retry-join=172.17.0.2
我们有了 Server 集群,现在可以开始建立 Consul 的 client 节点,然后加入集群。启动 Consul client 的命令跟启动 Consul server 的差不多。去掉了 -server 就代表这个 agent 为 client 模式。
使用 docker-compose 运行 Consul#
上面分步骤演示了如何使用 docker 命令来运行 Consul 集群。一行行敲还是太麻烦,为了简化部署,这里整理成了 docker-compose 启动文件。
version: '3.9'
services:
consulserver1:
image: consul:1.9.4
restart: always
container_name: consulserver1
hostname: consulserver1
command: agent -server -bootstrap -client=0.0.0.0 -ui -node=consulserver1
ports:
- 8500:8500
consulserver2:
image: consul:1.9.4
restart: always
container_name: consulserver2
hostname: consulserver2
command: agent -server -join=consulserver1 -node=consulserver2
depends_on:
- consulserver1
consulserver3:
image: consul:1.9.4
restart: always
container_name: consulserver3
hostname: consulserver3
command: agent -server -join=consulserver1 -node=consulserver3
depends_on:
- consulserver1
consulclient1:
image: consul:1.9.4
restart: always
container_name: consulclient1
hostname: consulclient1
command: agent -client=0.0.0.0 -retry-join=consulserver1 -node=consulclient1
depends_on:
- consulserver2
- consulserver3
ports:
- 8600:8500
consulclient2:
image: consul:1.9.4
restart: always
container_name: consulclient2
hostname: consulclient2
command: agent -client=0.0.0.0 -retry-join=consulserver1 -node=consulclient2
depends_on:
- consulserver2
- consulserver3
ports:
- 8700:8500
consulclient3:
image: consul:1.9.4
restart: always
container_name: consulclient3
hostname: consulclient3
command: agent -client=0.0.0.0 -retry-join=consulserver1 -node=consulclient3
depends_on:
- consulserver2
- consulserver3
ports:
- 8800:8500
折叠
这个 docker-compose 文件描述了启动3个 server 模式的实例,3个 client 模式的实例。其中 consulserver1 开启了ui,端口映射8500,consulclient1,、consulclient2、consulclient3 端口分别映射为 8600、8700、8800 ,记住这些端口,后面要用到。
[root@localhost myservices]# docker-compose up -d
[root@localhost myservices]# docker exec consulserver1 consul members
Node Address Status Type Build Protocol DC Segment
consulserver1 172.18.0.2:8301 alive server 1.9.4 2 dc1 <all>
consulserver2 172.18.0.3:8301 alive server 1.9.4 2 dc1 <all>
consulserver3 172.18.0.4:8301 alive server 1.9.4 2 dc1 <all>
consulclient1 172.18.0.5:8301 alive client 1.9.4 2 dc1 <default>
consulclient2 172.18.0.6:8301 alive client 1.9.4 2 dc1 <default>
consulclient3 172.18.0.7:8301 alive client 1.9.4 2 dc1 <default>
使用 docker-compose up -d 命令启动所有的容器。启动完成后使用 docker exec consulserver1 consul members 查看整个集群的状态。它列出了所有节点的类型,IP,是否存活等信息。
如果上面的操作一切正常,在浏览器里输入 http://宿主机IP:8500 访问 web 管理界面。界面上会显示6个绿色的节点。表示所有节点都正常运行中。
在 asp.net core 应用内使用 Consul#
好了现在我们已经有了 Consul 集群,现在可以开始编写代码来注册跟拉取我们的服务了。我们需要完成4点操作。
- 定义一个健康检测的接口
- 在服务启动的时候自动注册该服务的基础信息
- 在服务关闭的时候自动移除该服务
- 拉取服务列表
健康检测#
我们的服务注册到 consul 节点后,节点会定时去轮询我们的服务,所以需要提供一个 http 接口,如果返回 200 ok 就表示服务存活,否则代表服务故障。
[ApiController]
[Route("[controller]")]
public class HealthController : ControllerBase
{
[HttpGet]
public string Get()
{
return "ok";
}
}
}
添加一个HealthController里面就实现一个Get方法简单的返回ok就可以了。
服务注册、移除#
我们实现一个HostedService来实现自动注册跟移除服务。HostedService 有2个方法,start 跟 stop 。start 方法会在 app 启动的时候触发 , stop 会在 app 关闭的时候触发。跟我们的需求完美符合。
Install-Package Consul -Version 1.6.10.1
使用 nuget 安装 consul .net client 类库。我们跟 consul 节点的通讯需要它来完成。
public class ServiceInfo
{
public string Id { get; set; }
public string Name { get; set; }
<span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> IP { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
<span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Port { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
<span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> HealthCheckAddress { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
定义一个类,存储服务的基本信息。
public class ConsulRegisterService : IHostedService
{
IConsulClient _consulClient;
ServiceInfo _serviceInfo;
public ConsulRegisterService(IConfiguration config, IConsulClient consulClient)
{
_serviceInfo = new ServiceInfo();
var sc = config.GetSection("serviceInfo");
_serviceInfo.Id = sc[<span class="hljs-string">"id"</span>];
_serviceInfo.Name = sc[<span class="hljs-string">"name"</span>];
_serviceInfo.IP = sc[<span class="hljs-string">"ip"</span>];
_serviceInfo.HealthCheckAddress = sc[<span class="hljs-string">"HealthCheckAddress"</span>];
_serviceInfo.Port = <span class="hljs-built_in">int</span>.Parse(sc[<span class="hljs-string">"Port"</span>]);
_consulClient = consulClient;
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">StartAsync</span>(<span class="hljs-params">CancellationToken cancellationToken</span>)</span>
{
Console.WriteLine(<span class="hljs-string">$"start to register service <span class="hljs-subst">{_serviceInfo.Id}</span> to consul client ..."</span>);
<span class="hljs-keyword">await</span> _consulClient.Agent.ServiceDeregister(_serviceInfo.Id, cancellationToken);
<span class="hljs-keyword">await</span> _consulClient.Agent.ServiceRegister(<span class="hljs-keyword">new</span> AgentServiceRegistration
{
ID = _serviceInfo.Id,
Name = _serviceInfo.Name,<span class="hljs-comment">// 服务名</span>
Address = _serviceInfo.IP, <span class="hljs-comment">// 服务绑定IP</span>
Port = _serviceInfo.Port, <span class="hljs-comment">// 服务绑定端口</span>
Check = <span class="hljs-keyword">new</span> AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(<span class="hljs-number">0</span>),<span class="hljs-comment">//服务启动多久后注册</span>
Interval = TimeSpan.FromSeconds(<span class="hljs-number">5</span>),<span class="hljs-comment">//健康检查时间间隔</span>
HTTP = <span class="hljs-string">$"http://<span class="hljs-subst">{_serviceInfo.IP}</span>:<span class="hljs-subst">{_serviceInfo.Port}</span>/"</span> + _serviceInfo.HealthCheckAddress,<span class="hljs-comment">//健康检查地址</span>
Timeout = TimeSpan.FromSeconds(<span class="hljs-number">5</span>)
}
});
Console.WriteLine(<span class="hljs-string">"register service info to consul client Successful ..."</span>);
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">StopAsync</span>(<span class="hljs-params">CancellationToken cancellationToken</span>)</span>
{
<span class="hljs-keyword">await</span> _consulClient.Agent.ServiceDeregister(_serviceInfo.Id, cancellationToken);
Console.WriteLine(<span class="hljs-string">$"Deregister service <span class="hljs-subst">{_serviceInfo.Id}</span> from consul client Successful ..."</span>);
}
}
定义一个 ConsulRegisterService 类,实现 IHostedService 接口。在 start 方法内使用 consulclient 注册服务 。在 stop 方法内取消注册该服务。
public void ConfigureServices(IServiceCollection services)
{
//注册Consulclient对象
services.AddSingleton<IConsulClient>(new ConsulClient(x => {
x.Address = new Uri(Configuration["consul:clientAddress"]);
}));
//注册ConsulRegisterService 这个servcie在app启动的时候会自动注册服务信息
services.AddHostedService<ConsulRegisterService>();
services.AddControllers();
}
在 startup 的 ConfigureServices 方法内先注入一个 IConsulClient 的实例。再注册我们的 ConsulRegisterService 服务。
"serviceInfo": {
"id": "hote_base_01", //服务id
"name": "hote_base", //服务名
"ip": "192.168.0.200", //服务部署的ip
"port": 6002, //服务对应的端口
"healthCheckAddress": "health" //健康检测的请求path
},
"consul": {
"clientAddress": "http://192.168.0.117:8700" //consul client 的地址
}
以我们的演示项目 hotel_base 为例,在 appsettings.json 文件内添加以上配置信息。其中 consul:clientAddress 为 consule client 节点的地址。
注意:这里的 ip 不要使用 localhost ,因为如果使用 docker 部署 , localhost 会出现网络访问方面的问题。
好了,让我们运行一下我们的项目。等待项目启动完成后,打开 consul 的 web 管理界面。查看 consulclient1 节点,可以看到我们的 hotel_base_01 服务被注册上去了。
我们强制把启动的app关闭,可以看到 consul 管理界面显示 hotel_base 服务红色,代表故障。
注意:要演示故障这种情况,要先注释掉 ConsulRegisterService 的 stop 方法,不然关闭的时候会先取消注册,这样 consul 管理界面上就找不到对应的服务了。
我们按照 hotel_base 的套路,把其他几个服务都添加服务注册的代码。然后全部运行起来
拉取服务列表#
下面我们演示下如何通过 consul client 读取服务列表。
public interface IConsulService
{
Task<List<AgentService>> GetServicesAsync(string serviceName);
}
public class ConsulService : IConsulService
{
public IConsulClient _consulClient;
public ConsulService(IConsulClient consulClient)
{
_consulClient = consulClient;
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task<List<AgentService>> GetServicesAsync(<span class="hljs-built_in">string</span> serviceName)
{
<span class="hljs-keyword">var</span> result = <span class="hljs-keyword">await</span> _consulClient.Health.Service(serviceName, <span class="hljs-string">""</span>, <span class="hljs-literal">true</span>);
<span class="hljs-keyword">return</span> result.Response.Select(x => x.Service).ToList();
}
}
定义一个ConsulService类,里面有个GetServicesAsync方法。该方法通过服务名称从 consul 集群获取服务的列表。
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConsulClient>(new ConsulClient(x => {
x.Address = new Uri(Configuration["consul:clientAddress"]);
}));
//注册ConsulService里面封装了一些方法
services.AddSingleton<IConsulService, ConsulService>();
services.AddHostedService<ConsulRegisterService>();
services.AddControllers();
}
在 ConfigureServices 方法内把 ConsulService 注册到容器内。
[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
private static readonly List<OrderVM> _orders = new List<OrderVM>() {
new OrderVM {
Id = "OD001",
StartDay = "2021-05-01",
EndDay = "2021-05-02",
RoomNo = "1001",
MemberId = "M001",
HotelId = "H8001",
CreateDay = "2021-05-01"
}
};
<span class="hljs-keyword">private</span> IConsulService _consulservice;
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">OrderController</span>(<span class="hljs-params">ILogger<OrderController> logger, IConsulService consulService</span>)</span>
{
_consulservice = consulService;
}
[<span class="hljs-meta">HttpGet(<span class="hljs-string">"{id}"</span>)</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task<OrderVM> <span class="hljs-title">Get</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> id</span>)</span>
{
<span class="hljs-keyword">var</span> order = _orders.FirstOrDefault(x=>x.Id == id);
<span class="hljs-keyword">if</span> (!<span class="hljs-built_in">string</span>.IsNullOrEmpty(order.MemberId))
{
<span class="hljs-keyword">var</span> memberServiceAddresses = <span class="hljs-keyword">await</span> _consulservice.GetServicesAsync(<span class="hljs-string">"member_center"</span>);
<span class="hljs-keyword">var</span> memberServiceAddress = memberServiceAddresses.FirstOrDefault();
<span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> httpClient = <span class="hljs-keyword">new</span> HttpClient())
{
httpClient.BaseAddress = <span class="hljs-keyword">new</span> Uri(<span class="hljs-string">$"http://<span class="hljs-subst">{memberServiceAddress.Address}</span>:<span class="hljs-subst">{memberServiceAddress.Port}</span>"</span>);
<span class="hljs-keyword">var</span> memberResult = <span class="hljs-keyword">await</span> httpClient.GetAsync(<span class="hljs-string">"/member/"</span> + order.MemberId);
<span class="hljs-keyword">var</span> json = <span class="hljs-keyword">await</span> memberResult.Content.ReadAsStringAsync();
<span class="hljs-keyword">var</span> member = JsonConvert.DeserializeObject<MemberVM>(json);
order.Member = member;
}
}
<span class="hljs-keyword">return</span> order;
}
}
我们通过在 ordering 服务项目的一个获取订单详细信息的接口来演示下如何使用ConsulService 。订单详细信息需要根据会员id获取会员的详细信息。我们通过 ConsulService 获得 member_center 的服务列表后,取出一个配置信息,获取 IP 跟端口号。组装成服务的真正的请求地址,使用 HttpClient 来请求这个接口,获取会员的基本信息。
当然这里我们有很多可以改进的地方,比如我们可以在本地缓存服务列表,这样不用每次都通过 consul client 拉取。比如我们可以写一个随机算法,每次从服务列表中随机取一个对象,从而达到负载均衡的目的,在这就不再演示了。
把所有项目都跑起来,使用 postman 去访问一下获取订单详情接口,可以看到订单详情的返回值包含了会员信息。
总结#
通过以上,我们回顾了服务注册发现的概念。演示了如何通过 docker/docker-compose 环境来部署 Consul 集群。还通过简单的 .NET Core 代码演示了如何注册服务信息到 Consul 集群,如何通过代码获取服务列表并调用它。相信现在大家对服务注册发现、Consul 组件有了一个比较直观的了解。
谢谢阅读。
项目地址#
https://github.com/OnePieceSpace/myhotel_microservice
相关文章#
NET Core with 微服务 - 什么是微服务
.Net Core with 微服务 - 架构图
.Net Core with 微服务 - Ocelot 网关