.NET Core基于Consul的服务注册与发现和集群管理

如今微服务正当道,微服务解决了单体结构中的多个模块的紧耦合、无法扩展和运维困难。对单体应用本身进行服务化和组件化,每个组件单独部署为小应用(从DB到UI)。于是微服务间通过API 互相调用进行通信,详情请参考前文。同时为了支持水平扩展、性能提升和服务可用性,单个服务允许同时部署一个或者多个服务实例。在运行时,每个实例通常是一个云虚拟机或者Docker容器。那么问题来了,服务和服务之间是如何知道对方的存在,以及是否活着,服务间如果知道对方的服务地址,写死了不灵活,而动态分配又怎么知道如何调用,对方有哪些方法可用等等。于是有了三方的服务注册中心,提供对生产者服务节点的注册管理和消费者服务节点的发现管理,比如本文的Consul

conusl是什么

Consul通过HTTP API和DNS提供服务发现服务。

从官网介绍了看具体以下四大特性

  • Service Discovery (服务发现)

  • Health Check (健康检查)

  • Multi Datacenter (多数据中心)

  • Key/Value Storage(kv存储)

Consul相关的一些知识点

Agent

Agent 是一个守护进程

运行在Consul集群的每个成员上

有Client 和 Server 两种模式

所有Agent都可以被调用DNS或者HTTP API,并负责检查和维护同步

Client

Client 将所有RPC请求转发至Server

Client 是相对无状态的

Client 唯一做的就是参与LAN Gossip Pool

Client 只消耗少量的资源和少量的网络带宽

Server

参与 Raft quorum(一致性判断)

响应RPC查询请求

维护集群的状态

转发查询到Leader 或 远程数据中心

Datacenter数据中心

私有的

低延迟

高带宽

Consensus (一致性)

Consul 使用consensus protocol 来提供CAP(一致性,高可用,分区容错性)

Gossip

一种协议: 用来保证 最终一致性 , 即: 无法保证在某个时刻, 所有节点状态一致, 但可以保证”最终”一致

官网找到的一张图Consul的架构以及相关的角色,如图

结构

要想利用Consul提供的服务实现服务的注册与发现,我们需要建立Consul Cluster。在Consul方案中,每个提供服务的节点上都要部署和运行Consul的Client Agent,所有运行Consul Agent节点的集合构成Consul Cluster。Consul Agent有两种运行模式:Server和Client。这里的Server和Client只是Consul集群层面的区分,与搭建在Cluster之上的应用服务无关。以Server模式运行的Consul Agent节点用于维护Consul集群的状态,官方建议每个Consul Cluster至少有3个或以上的运行在Server Mode的Agent,Client节点不限。

Consul支持多数据中心,每个数据中心的Consul Cluster都会在运行于Server模式下的Agent节点中选出一个Leader节点,这个选举过程通过Consul实现的raft协议保证,多个 Server节点上的Consul数据信息是强一致的。处于Client Mode的Consul Agent节点比较简单,无状态,仅仅负责将请求转发给Server Agent节点。

我们的服务结构是这样的

consul

安装与配置Consul

1.解压Consul.zip:

分别在三台节点中解压,解压命令:


unzip consul_1.1.0_linux_386.zip

当然也可以解压之后将consul复制到我们的自定义文件目录中,比如:/usr/local/consul

cp consul /usr/local/consul

2.设置环境变量

分别在三台节点中设置环境变量:

vim /etc/profile 

在profile中增加一行CONSUL_HOME并更改PATH:

Consul

export CONSUL_HOME=/usr/local/consul
export PATH=$PATH:$JAVA_HOME/bin:$CONSUL_HOME;  

重载配置

source /etc/profile

测试是否生效,在三个节点测试输入consul

看到下图所示的命令提示,就代表OK了。 runok

3.启动Server(s)

分别在三台节点上执行以下命令即可启动Consul

consul agent -server -ui -bootstrap-expect=3 -data-dir=/tmp/consul -node=consul-1 -client=0.0.0.0 -bind=192.168.80.100 -datacenter=dc1
consul agent -server -ui -bootstrap-expect=3 -data-dir=/tmp/consul -node=consul-2 -client=0.0.0.0 -bind=192.168.80.101 -datacenter=dc1 -join 192.168.80.100
consul agent -server -ui -bootstrap-expect=3 -data-dir=/tmp/consul -node=consul-3 -client=0.0.0.0 -bind=192.168.80.102 -datacenter=dc1 -join 192.168.80.100

注意101和102的启动命令中,有一句 -join 192.168.80.100 => 有了这一句,就把101和102加入到了100所在的集群中。

启动之后,集群就开始了Vote(投票选Leader)的过程

通过下面的命令可以看到集群的情况:

consule status

在power shell总启动一个客户端

 consul agent -bind 0.0.0.0 -client 192.168.80.71 -data-dir=C:\Counsul\tempdata -node EDC.DEV.WebServer -join 192.168.80.100

启动后会有如下提示:

power shell

4.通过UI查看集群

Consul不仅提供了丰富的命令查看集群情况,还提供了一个WebUI,默认端口8500,我们可以通过访问这个URL(eg.http://192.168.80.100:8500)

得到如下图所示的WebUI:

web ui

可以看到三个节点都正常启动

下面我们就来试试向Consul注册一下我们基于dotnet Core的WebAPI服务。

5.模拟Leader挂掉,查看Consul集群的新选举Leader

这里我暴力一点直接将Leader节点关机:

shutdown -h now,可以看到我们的80.100已经挂了。

leader

查看其余两个节点的日志可以发现,consul-3 (80.102)被选为了新的leader:

leader2

当然,也可以通过80.101或102的WebUI查看:

web ui

也可以通过以下命令查看目前的各个Server的角色状态:

 consul operator raft list-peers

role

虽然这里80.100这个原leader节点挂掉了,但是只要超过一半的Server(这里是2/3还活着)还活着,集群是可以正常工作的,这也是为什么像Consul、ZooKeeper这样的分布式管理组件推荐我们使用3个或5个节点来部署的原因。

三、dotnet Core 服务注册


public void Start()
{
var config = ContainerManager.Container.
Resolve<IConfiguration>().
GetSection("consul").
GetSection("service").
Get<ConsulLocalServiceConfig>();

var rbbitMQConf = ContainerManager.
Container.
Resolve<IConfiguration>().
GetSection("RabbitMQ").Get<MQConfig>();
            //启动MQ
MQStarter.Instance.UseConfig(rbbitMQConf).Start();

GrpcConfiguration.Instance
.UseExtensionsOptions(options => options.MonitorLoggerInterval = new TimeSpan(0,0,1))
            //.UseExtensionsOptions(options => options.MonitorLoggerIntervalSeconds = 1)
.UseServerMiddleware(new RequestCounterMiddleWare())
.UseLoggerAccessor(logger =>
{
logger.LoggerMonitor = (s) =>
{
accessLog.LogInformation(s);
};
logger.LoggerError = (s) =>
{
accessLog.LogError(s.ToString());
};
})
.UseBaseMiddlewares();

server = new Server
{
Services = { TradeSrv.BindService(new TradesServiceImpl()) },
}.
UseBaseServices();
server.StartAndRegisterService(config).Wait();

var port = server.Ports.FirstOrDefault();
if (port != null) {
Console.WriteLine($"listen {port.Host}:{port.BoundPort}");
}            
}

具体的配置如下:


"consul": {
"service": {
"ServiceName": "domain.srv.trade.csharp",
"ConsulAddress": "http://192.168.8.6:8500",
"ServiceAddress": "192.168.*.*:0",
"ConsulIntegration": "true",
"ConsulTags": "v-1.0.0.1",
"TCPInterval": 10
},
"remotes": {
"gateway": {
"name": "gateway",
"ServiceName": "followme.srv.copytrade.gateway",
"FreshInterval": 10000,
"ConsulAddress": "http://192.168.8.6:8500",
"ConsulIntegration": "true",
"ServiceAddress": ""
},

}

为了方便,我们把consul组件封装了下并开源在github上,上面有更详细的注册服务例子

github

这样在程序启动的时候就注册到consul中了,bingo

docker部署

实际上,这样的开销还是比较大,的小小的一个服务,把他扔docker里面跑就好了,

其实我们不仅把dotnet服务发布到docker,还搭建了CI/CD

大概结构如下

ci+docker

另外一个服务也分布在多个节点的容器上

于是还用上了portainer进行多容器的管理

这一堆东西都放在jenkins上实现自动化

目前上面有我们自己写的docker基础镜像,python写的poetainer管理脚本,jenkin ci管理和私有的nuget服务器等

具体的实施后期整理下再发上来

在自动化的脚本启动服务就变的非常简单,为了防止服务崩溃,docker加上自动重启参数-restart=always


if [ "$env"  = "dev" ]

then

    pull_cmd=$(printf 'docker pull %s' $full_docker_image_name)

    stop_cmd=$(printf 'docker ps |grep %s && docker container stop %s && docker container rm %s -f' $dockerImgName $dockerImgName $dockerImgName)

    start_cmd=$(printf 'docker run -d -e "ASPNETCORE_ENVIRONMENT=Development" --restart=always -v %s/%s/logs:/app/logs --net=host --name %s %s' $devdeploypath $dockerImgName $dockerImgName $full_docker_image_name)



    ssh -oPort=$devport $devaddress "$pull_cmd"

    ssh -oPort=$devport $devaddress "$stop_cmd"

    ssh -oPort=$devport $devaddress "$start_cmd"

elif [ "$env" = "beta" ]

then

    echo "publish to beta"

    python /root/pubtool/main.py --env=beta --node=2 --container_name="$dockerImgName" --docker_image="$local_docker_image_name"

    #see detail of script at gitlab

else

    echo "publish to Production !"

    python /root/pubtool/main.py --env=prod --node=2 --container_name="$dockerImgName" --docker_image="$local_docker_image_name"

fi