上一次我們介紹了 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; }
public string IP { get; set; }
public int Port { get; set; }
public string HealthCheckAddress { get; set; }
}
定義一個類,存儲服務的基本信息。
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["id"];
_serviceInfo.Name = sc["name"];
_serviceInfo.IP = sc["ip"];
_serviceInfo.HealthCheckAddress = sc["HealthCheckAddress"];
_serviceInfo.Port = int.Parse(sc["Port"]);
_consulClient = consulClient;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine($"start to register service {_serviceInfo.Id} to consul client ...");
await _consulClient.Agent.ServiceDeregister(_serviceInfo.Id, cancellationToken);
await _consulClient.Agent.ServiceRegister(new AgentServiceRegistration
{
ID = _serviceInfo.Id,
Name = _serviceInfo.Name,// 服務名
Address = _serviceInfo.IP, // 服務綁定IP
Port = _serviceInfo.Port, // 服務綁定端口
Check = new AgentServiceCheck()
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(0),//服務啟動多久后注冊
Interval = TimeSpan.FromSeconds(5),//健康檢查時間間隔
HTTP = $"http://{_serviceInfo.IP}:{_serviceInfo.Port}/" + _serviceInfo.HealthCheckAddress,//健康檢查地址
Timeout = TimeSpan.FromSeconds(5)
}
});
Console.WriteLine("register service info to consul client Successful ...");
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await _consulClient.Agent.ServiceDeregister(_serviceInfo.Id, cancellationToken);
Console.WriteLine($"Deregister service {_serviceInfo.Id} from consul client Successful ...");
}
}
定義一個 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;
}
public async Task<List<AgentService>> GetServicesAsync(string serviceName)
{
var result = await _consulClient.Health.Service(serviceName, "", true);
return 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"
}
};
private IConsulService _consulservice;
public OrderController(ILogger<OrderController> logger, IConsulService consulService)
{
_consulservice = consulService;
}
[HttpGet("{id}")]
public async Task<OrderVM> Get(string id)
{
var order = _orders.FirstOrDefault(x=>x.Id == id);
if (!string.IsNullOrEmpty(order.MemberId))
{
var memberServiceAddresses = await _consulservice.GetServicesAsync("member_center");
var memberServiceAddress = memberServiceAddresses.FirstOrDefault();
using (var httpClient = new HttpClient())
{
httpClient.BaseAddress = new Uri($"http://{memberServiceAddress.Address}:{memberServiceAddress.Port}");
var memberResult = await httpClient.GetAsync("/member/" + order.MemberId);
var json = await memberResult.Content.ReadAsStringAsync();
var member = JsonConvert.DeserializeObject<MemberVM>(json);
order.Member = member;
}
}
return order;
}
}
我們通過在 ordering 服務項目的一個獲取訂單詳細信息的接口來演示下如何使用ConsulService 。訂單詳細信息需要根據會員id獲取會員的詳細信息。我們通過 ConsulService 獲得 member_center 的服務列表后,取出一個配置信息,獲取 IP 跟端口號。組裝成服務的真正的請求地址,使用 HttpClient 來請求這個接口,獲取會員的基本信息。
當然這里我們有很多可以改進的地方,比如我們可以在本地緩存服務列表,這樣不用每次都通過 consul client 拉取。比如我們可以寫一個隨機算法,每次從服務列表中隨機取一個對象,從而達到負載均衡的目的,在這就不再演示了。
把所有項目都跑起來,使用 postman 去訪問一下獲取訂單詳情接口,可以看到訂單詳情的返回值包含了會員信息。
總結
通過以上,我們回顧了服務注冊發現的概念。演示了如何通過 docker/docker-compose 環境來部署 Consul 集群。還通過簡單的 .NET Core 代碼演示了如何注冊服務信息到 Consul 集群,如何通過代碼獲取服務列表並調用它。相信現在大家對服務注冊發現、Consul 組件有了一個比較直觀的了解。
謝謝閱讀。
項目地址
https://github.com/kklldog/myhotel_microservice
相關文章
NET Core with 微服務 - 什么是微服務
.Net Core with 微服務 - 架構圖
.Net Core with 微服務 - Ocelot 網關