上一次我們介紹了 Ocelot 閘道器的基本用法。這次我們開始介紹服務註冊發現元件 Consul 的簡單使用方法。

服務註冊發現

首先先讓我們回顧下服務註冊發現的概念。

在實施微服務之後,我們的呼叫都變成了服務間的呼叫。服務間呼叫需要知道IP、埠等資訊。再沒有微服務之前,我們的呼叫資訊一般都是寫死在呼叫方的配置檔案裡(當然這話不絕對,有些公司會把這些資訊寫到資料庫等公共的地方,以方便維護)。又由於業務的複雜,每個服務可能依賴N個其他服務,如果某個服務的IP,埠等資訊發生變更,那麼所有依賴該服務的服務的配置檔案都要去修改,這樣顯然太麻煩了。有些服務為了負載是有個多個例項的,而且可能是隨時會調整例項的數量。如果每次調整例項數量都要去修改其他服務的配置並重啟那太麻煩了。

為了解決這個問題,業界就有了服務註冊發現元件。

假設我們有服務A需要呼叫服務B,並且有服務註冊發現元件R。整個大致流程將變成大噶3部:

  1. 服務B啟動向服務R註冊自己的資訊
  2. 服務A從服務R拉取服務B的資訊
  3. 服務A呼叫服務B

有了服務註冊發現元件之後,當修改A服務資訊的時候再也不用去修改其他相關服務了。

Consul

Consul 是 HashiCorp 公司推出的一套服務註冊發現工具。它使用 golang 編寫並且開源。由於使用 golang 的緣故所以它天生跨平臺而且部署簡單。它帶有 web 管理後臺方便使用者檢視維護 Consul 叢集。其實除了服務註冊發現功能,Consul 還支援 Key/Value 儲存可以當一個簡單的配置中心使用。

架構



上面是 Consul 官網上畫的架構圖。從圖上可以看到 Consul 天生支援多資料中心部署。每個資料中心內部有多個 Consul 節點。Consul 的節點分為2種。

  1. Server

    Server 模式的節點是真正意義上叢集的節點。它通過RAFT演算法實現CAP裡的CA。當Leader Server 掛掉的時候會自動選舉出新的 Leader 使叢集繼續正常工作。
  2. 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點操作。

  1. 定義一個健康檢測的介面
  2. 在服務啟動的時候自動註冊該服務的基礎資訊
  3. 在服務關閉的時候自動移除該服務
  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 元件有了一個比較直觀的瞭解。

謝謝閱讀。

相關文章

NET Core with 微服務 - 什麼是微服務

.Net Core with 微服務 - 架構圖

.Net Core with 微服務 - Ocelot 閘道器

關注我的公眾號一起玩轉技術