一. 簡介
本節架構圖:
(PS:該圖僅服務於本節,完整版的微服務架構圖見后最后章節)
1. 什么是Consul?
Consul是一個用來實現分布式系統的服務發現與配置的開源工具,它的可以實現服務提供者 和 服務消費者的隔離,比如:比如服務提供者(GoodsService)將自身注冊到Consul中, 注冊的信息是:ServiceName + ip/port,這樣服務消費者只需要知道ServiceName就可以知道對應服務的ip+端口,從而進行訪問,就好比DNS的功能。
PS. 和Consul同類的還有:zookeeper、etcd、Eureka。
2. Consul的好處
同一個ServiceName下可以對應多個 ip+port,只要ServiceID不同即可,也就是說同一個項目多次注冊到同一個ServiceName下,這樣消費者通過ServiceName可以拿到其中一個或者多個,可以在消費者層次書實現負載均衡,即客戶端的負載均衡。另外服務消費者不需要記住多個 ip+port 了,只需要記住一個ServiceName即可,即不需要關心SeviceName下的業務服務器是否增加,是否宕機的問題。
PS:當然客戶端不能直接訪問Consul了,后續會通過Ocelot轉發。 本節這樣的設置,最終消費者還是要訪問業務服務器的,正常情況下業務服務器是不對外開放,而且也不利於做授權校驗,這就需要后續章節Ocelot來解決了。
3. Consul相關信息
(1).consul的官網:https://www.consul.io/ 最新的Server下載地址:https://www.consul.io/downloads
(2).consul客戶端UI地址:http://127.0.0.1:8500
4. Consul的啟動
(1).開發模式:【consul.exe agent -dev】,開發模式不會持久化數據,重啟之后保存的配置信息就會丟失.
(2).生產模式:【consul.exe agent -server -bootstrap-expect 1 -data-dir d:/consul/data 】
PS:在D盤創建consul/data文件夾,用於持久化Consul。
(3).客戶端模式:通過.Net程序來啟動
補充Consul啟動的各個參數:
a. agent:consul的核心命令,主要作用有維護成員信息、運行狀態檢測、聲明服務以及處理請求等
b. -server:就是代表server模式
c. -bootstrap-expect:代表想要創建的集群數目,官方建議3或者5
d. -data-dir:數據存儲目錄
e. -client:是一個客戶端服務注冊的地址,可以和當前server的一致也可以是其他主機地址,提供HTTP、DNS、RPC等服務,系統默認是127.0.0.1,所以不對外提供服務,如果你要對外提供服務改成0.0.0.0
f. -bind:集群通訊地址,集群內的所有節點到地址都必須是可達的,默認是0.0.0.0
g. -node:代表當前node的名稱,節點在集群中的名稱,在一個集群中必須是唯一的,默認是該節點的主機名
h. -ui:代表開啟web 控制台
i. -config-dir:配置文件目錄,里面所有以.json結尾的文件都會被加載
二. 應用
前提:下面所有的測試都是基於Consul開發模式啟動的,注冊或者消費者都需要Nuget安裝consul程序包,目前最新:【Consul 1.6.1.1】
1.服務注冊
(1).含義:通俗的來說就是將一個 Api業務服務(比如 OrderService 和 GoodService), 起一個ServiceName,然后對應啟動地址即ip+port對應,注冊到Consul中,供別人通過ServiceName來調用。 一個ServiceName下可以注冊多個 ip+port,即同一個項目可以注冊在一個ServiceName下, 只要ServiceId不同即可.
(2).以Api項目為例:在Configure管道中進行注冊,這里面動態獲取ip和端口(需要添加 AddCommandLine支持),然后ServiceRegister進行注冊,ServiceDeregister進行注銷, 注銷代碼中寫在 applicationLifetime.ApplicationStopped.Register里,即程序退出的時候注銷Consul中的服務.
詳細代碼如下:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime) { // 前面core本身的代碼省略 //---------------一以下是注冊Consul代碼------------------ string ip = Configuration["ip"]; int port = Convert.ToInt32(Configuration["port"]); string serviceName = "OrderService"; string serviceId = serviceName + Guid.NewGuid(); using (var client = new ConsulClient(ConsulConfig)) { //注冊服務到 Consul client.Agent.ServiceRegister(new AgentServiceRegistration() { ID = serviceId,//服務編號,不能重復,用Guid 最簡單 Name = serviceName,//服務的名字 Address = ip,//服務提供者的能被消費者訪問的ip 地址(可以被其他應用訪問的地址,本地測試可以用127.0.0.1,機房環境中一定要寫自己的內網ip 地址) Port = port,// 服務提供者的能被消費者訪問的端口 //Tags //可以配置一下額外的參數用於傳遞 Check = new AgentServiceCheck { DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服務停止多久后注銷 Interval = TimeSpan.FromSeconds(10),//健康檢查時間間隔,或者稱為心跳 間隔 HTTP = $"http://{ip}:{port}/api/health",//健康檢查地址 Timeout = TimeSpan.FromSeconds(3) //超時時間 } }).Wait();//Consult 客戶端的所有方法幾乎都是異步方法,但是都沒按照規范加上 Async 后綴,所以容易誤導。記得調用后要Wait()或者 await } //程序正常退出的時候從Consul 注銷服務 ,要通過方法參數注入 IHostApplicationLifetime applicationLifetime.ApplicationStopped.Register(() => { using (var client = new ConsulClient(ConsulConfig)) { Console.WriteLine("程序正常退出的時候從Consul 注銷服務 "); client.Agent.ServiceDeregister(serviceId).Wait(); } }); } private void ConsulConfig(ConsulClientConfiguration c) { c.Address = new Uri("http://127.0.0.1:8500"); }
(3).UI頁面:服務注冊后,可以訪問:http://127.0.0.1:8500, 來直觀的查看注冊情況.
2.服務的健康檢查
(1).含義:Consul可以提供與給定服務相關的健康檢查(Web服務器返回200 ok)或者本地節點(“內存利用率低於90%”),即心跳檢測,如果服務不通,該服務在n秒內會自動從 Consul中注銷。
(2).如何配置:在注冊的時候里面有個參數check,即配置AgentServiceCheck對象,比如參數如下:
A.HTTP:健康檢查的地址,這里僅支持http請求,接口返回ok
B.DeregisterCriticalServiceAfter:代表服務停止多久后進行注銷
C.Interval:健康檢查時間間隔,或者稱為心跳 間隔
D.Timeout:超時時間
詳見代碼配置:
Check = new AgentServiceCheck { DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服務停止多久后注銷 Interval = TimeSpan.FromSeconds(10),//健康檢查時間間隔,或者稱為心跳 間隔 HTTP = $"http://{ip}:{port}/api/health",//健康檢查地址 Timeout = TimeSpan.FromSeconds(3) //超時時間 }
健康檢查的api接口(這里用的是Restful風格):
[Route("api/[controller]")] [ApiController] public class HealthController : ControllerBase { [HttpGet] public IActionResult Get() { return Ok("ok"); } }
3.服務發現
(1). 含義
通俗的來說,就是通過消費者可以通過連接Consul獲取到所有注冊在線ServiceName或者指定的ServiceName,然后根據ServiceName拿到其對應的ip+port(1個或多個),從而進行訪問。
(2). 應用
前提准備:將GoodsService項目注冊在Consul中,服務名是GoodsService,注冊三個端口分別是:7001 7002 7003
將OrderService項目注冊在Consul中,服務名是OrderService,注冊三個端口分別是:7004 7005 7006
A. 獲取所有的服務
B. 獲取指定名稱的服務,然后自己寫一個隨機算法,拿其中一個ip+port:
C. 獲取指定名稱服務,輪訓策略
相關代碼:
using (var consulClient = new ConsulClient(c => c.Address = new Uri("http://127.0.0.1:8500"))) { #region 1.獲取所有登記在策的consul集合 { var services = consulClient.Agent.Services().Result.Response; foreach (var service in services.Values) { Console.WriteLine($"id={service.ID},name={service.Service},ip={service.Address},port={service.Port} "); } } #endregion #region 2.隨機找一個登記在冊的服務進行連接(客戶端負載均衡-隨機策略) { var services = consulClient.Agent.Services().Result.Response.Values.Where(item => item.Service.Equals("OrderService", StringComparison.OrdinalIgnoreCase)); Random rand = new Random(); int index = rand.Next(services.Count());//[0,services.Count()) var s = services.ElementAt(index); Console.WriteLine($"index={index},id={s.ID},service={s.Service},addr={s.Address},port={s.Port}"); } #endregion #region 3.輪訓策略(這里只是演示,實際要考慮並發和溢出的問題) { Console.WriteLine(0 % 3); //結果0 Console.WriteLine(1 % 3); //結果1 Console.WriteLine(2 % 3); //結果2 Console.WriteLine(3 % 3); //結果1 Console.WriteLine(4 % 3); //結果2 int Num = 0; var services = consulClient.Agent.Services().Result.Response.Values.Where(item => item.Service.Equals("OrderService", StringComparison.OrdinalIgnoreCase)); for (int i = 1; i < 6; i++) { int index = Num++ % services.Count(); var s = services.ElementAt(index); Console.WriteLine($"第{i}次的地址如下:"); Console.WriteLine($"index={index},id={s.ID},service={s.Service},addr={s.Address},port={s.Port}"); } } #endregion #region 4.接口測試 { var services = consulClient.Agent.Services().Result.Response.Values.Where(item => item.Service.Equals("OrderService", StringComparison.OrdinalIgnoreCase)); Random rand = new Random(); int index = rand.Next(services.Count()); var s = services.ElementAt(index); var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider(); //等價於以上三句話 IHttpClientFactory httpClientFactory = serviceProvider.GetService<IHttpClientFactory>(); string url2 = $"http://{s.Address}:{s.Port}/api/Buy/pOrder2"; var client = httpClientFactory.CreateClient(); var content = new StringContent("userId=001&goodId=123456&num=100", Encoding.UTF8, "application/x-www-form-urlencoded"); var response = client.PostAsync(url2, content).Result; string result = ""; if (response.IsSuccessStatusCode) { result = response.Content.ReadAsStringAsync().Result; Console.WriteLine(result); } } #endregion } Console.ReadKey(); }
運行結果:
!
- 作 者 : Yaopengfei(姚鵬飛)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
- 聲 明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。