快速啟動一個consul集群可以參考:使用docker快速部署一個consul集群
.net core集成使用consul是通過consul提供出來api接口來實現的,可以分成兩個部分來說明:配置集成、服務注冊。
代碼比較多,已上傳到gitee上了,地址見:https://gitee.com/shanfeng1000/dotnetcore-demo/tree/master/Consul
這是一個Demo項目,介紹.net core集成使用rabbitmq消息隊列,使用的.net core 3.1,這里簡單介紹:
集成使用Consul的kv store(配置服務)
.net core從consul的kv stroe中獲取配置很容易,但是實現kv store的熱加載有三種方式:
方式一:阻塞式查詢(長輪詢)(推薦)
有關阻塞式查詢的介紹,可以參考官網:https://www.consul.io/api-docs/features/blocking
不過這里可以簡單的將阻塞式查詢理解為,consul為請求資源設置了一個index(可以理解為版本號),當資源更新時,consul會將版本號增加,而當使用api請求時可以攜帶一個index參數(起始版本)和wait參數(等待時間),當在wait時間內,存在index大於請求的index參數時(如果本就存在,那就不用等待了),會返回一個響應,響應會攜帶一個X-Consul-Index的Header,表示新的index,否則會在wait時間后超時返回響應,表示沒有新的index。
在上面gitee的Demo中的AspNetCore.WebApi.Client中的Program中集成使用:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); ...
}).UseConsul(options => { options.Address = "http://192.168.209.128:18401"; options.Datacenter = "dc1"; options.Token = "245d0a09"; options.Prefix = "Root/Consul"; //使用阻塞式查詢實現熱更新 options.Mode = WatchMode.Poll; options.Interval = TimeSpan.FromMinutes(3); });
現在consul中的kv store中Root/Consul節點下的所有kv都將被集成到.net core中的IConfiguration中去了,修改此節點下的任意kv都會自動更新IConfiguration,這是最簡單的一種熱更新方式。
方式二:consul watch
有關consul watch的介紹,可以參考官網:https://www.consul.io/docs/dynamic-app-config/watches
consul watch是一種監聽機制,可以監聽kv、service、node等信息,當它們發生改變時,觸發某些handle,而這些handle包括執行shell腳本,發送http請求等。
事實上,consul watch是基於阻塞式查詢的一種實現,但是遺憾的是,目前consul watch並沒有提供出來api接口出來注冊handle,所以在集成時,我們的項目需要提供出接口來公consul watch通知調用。
在上面gitee的Demo中的AspNetCore.WebApi.Server中的Program中集成使用:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { ... webBuilder.ConfigureAppConfiguration(builder => { builder.AddConsul(options => { options.Address = "http://192.168.209.128:18401"; options.Datacenter = "dc1"; options.Token = "245d0a09"; options.Prefix = "Root/Consul"; //使用consul-template或者watch來實現熱更新,需要在Configure中使用UseConsulWatch攔截更新配置請求 options.Mode = WatchMode.Watch; options.ReloadName = "demo"; }); }); });
這樣,項目啟動后,可以從consul的kv store中讀取配置,我們還需要提供一個回調的接口,可以在Startup的Configure方法時使用中間件UseConsulWatch來攔截:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseConsulWatch(); ... }
默認情況下,這個接口是POST請求,地址是http://host:port/consul?name=reloadName,這里的reloadName就是在Program中配置的那個ReloadName,表示收到更新后,只會重新加載指定name名稱的這個IConfigurationProvider,多個name之間使用逗號分隔。reloadName為空則表示重新加載所有的IConfigurationProvider,有了接口,接下來我們需要執行consul watch來啟動監聽:
# 執行consul watch consul watch -http-addr=192.168.209.128:18402 -datacenter=dc1 -prefix=Root/Consul -type=keyprefix "curl -X POST http://192.168.28.212:16001/consul -d ''" 說明: -http-addr:表示連接的consul地址,默認是127.0.0.1:8500 -datacenter:數據中心 -type:表示監聽的類型,可選的有key, keyprefix, services, nodes, service, checks, event,這里是配置,所以選擇keyprefix,表示具有這個前綴的所有kv(key表示單個的kv) -prefix:kv的前綴 curl -X POST http://192.168.28.212:16001/consul -d '':這是kv更新后需要執行的命令,可以是shell腳本,也可以是普通的命令,這里是調用項目接口來更新項目配置實現熱更新
現在,更新kv store,會發現程序已經實現了熱加載
方式三:consul-template
有關consul-template相關的介紹可以參考:https://github.com/hashicorp/consul-template
consul-template的下載地址:https://releases.hashicorp.com/consul-template/
consul-template是基於consul watch的一套模板工具,可以這么理解consul-template,首先你需要提供一個模板,一般是一個ctmpl文件(語法類似於go template,具體可以參考這里)。接着,需要指定consul地址,及認證等信息,這樣,consul-template連接到consul,然后會根據模板中需要的參數對consul進行監聽,當對應的參數更新后會重新渲染模板。模板渲染更新后,我們可以使用這個新模板,一般我們先要將模板輸出成文件才能使用(一般是配置文件),所以我們還需要指定一個輸出文件路徑。輸出文件之后,我們還能需要執行一些腳本,命令等等。一般為了方便管理,這些配置都是寫在一個config.hcl文件中。
.net core使用與上面consul watch一樣,只是監聽不在是使用consul watch,所以上面監聽部分換成consul-template可以寫成:
首先創建一個config.hcl文件:
consul { address = "192.168.209.128:18402", token = "245d0a09" } template { contents = "{{ tree \"Root/Consul\" | explode | toJSONPretty }}", command = "curl -X POST http://192.168.28.212:16001/consul -d ''" }
上面的contents及模板內容,這里的意思是以json格式輸出Root/Consul下的kv,當Root/Consul下的kv更新時,會重新渲染這個模板,當然,contents的內容也可以寫在一個ctmpl文件中,然后將contents換成source用於指定這個ctmpl文件的位置。command表示在模板渲染完成之后需要執行的命令,這里是使用curl發出一個http請求。更多配置說明參考:https://github.com/hashicorp/consul-template/blob/master/docs/configuration.md#configuration-file
現在可以使用consul-template來啟動監控了:
# 啟動 consul-template -config config.hcl -dry 說明: -config:指定配置文件 -dry:表示渲染后的模板輸出到標准輸出中,而不是輸出到一個文件中,如果沒有這個參數,則需要在上面config.hcl中的temlate節點中添加一個destination節點,用於指明模板渲染后的輸出文件路徑,這里因為沒使用到渲染后的文件,不需要使用文件,所以使用-dry輸出即可
現在,更新kv store,會發現程序也已經實現了熱加載。
注:
consul-template是一個很靈活的模板工具,我們可以將consul、consul-template、nginx一起使用,組合成一套服務自動發現功能:nginx使用consul-template生成conf文件,而consul-template監控consul中已經注冊的服務,當有新實例加入或者已有實例退出時,consul-template會重新渲染生成新的conf文件,渲染完成后執行命令或者腳本讓nginx重新加載即可。這種方式常常用在分布式系統的場景,因為如果我們的項目采用了分布式部署,難道要為分布式中的每個節點都手動的用consul watch?特別是容器化的應用,consul watch就不太現實了,這個時候可以使用consul-template來實現:
首先我們先創建一個ctmpl文件(如:demo.ctmpl):
#!/bin/bash #這行代碼用於添加kv的監控 #{{ tree "Root/Consul" }} #輸出server服務的所有實例,並請求重新加載配置 {{range service "server@dc1"}} curl -X POST http://{{.Address}}:{{.Port}}/consul -d '' {{end}}
接着創建一個demo.hcl文件:
consul { address = "192.168.209.128:18402", token = "245d0a09" } template { source = "./demo.ctmpl", destination = "demo.sh", command = "/bin/bash demo.sh" }
然后啟動:
# 啟動,因為有輸出文件,不能使用-dry consul-template -config demo.hcl
這是,當名稱為server的服務有多個實例時,kv 的更新會通知到所有的實例。(當然,這個例子中,配置加載采用這種方式也有缺點,因為新實例加入或者已有實例退出也會觸發熱加載)
使用Consul作為服務注冊中心
consul是一款很優秀的服務治理工具,在這個demo項目中,我簡單的做了一個封裝,方便集成使用,如AspNetCore.WebApi.Client的Startup中的服務注冊:
public void ConfigureServices(IServiceCollection services) { ... //consul服務注冊 services.AddConsulClient("consul", options => { options.Address = "http://192.168.209.128:18401"; options.Datacenter = "dc1"; //options.Token = "token";//如果有token }).AddService(options => { options.Host = ip; options.Port = port; options.Id = $"client_{ip}_{port}"; options.Name = "client"; options.Tags = new[] { "client" }; options.HealthCheckPath = "Health"; }); ... }
通過AddConsulClient方法創建一個Consul的客戶端,通過AddService使用這個客戶端添加服務,其中服務的健康檢查支持簡單的http和grpc兩種方式,如果采用grpc方式,那么需要提供一個標准的grpc服務,官方給出的health.proto如下:
syntax = "proto3"; package grpc.health.v1; message HealthCheckRequest { string service = 1; } message HealthCheckResponse { enum ServingStatus { UNKNOWN = 0; SERVING = 1; NOT_SERVING = 2; SERVICE_UNKNOWN = 3; } ServingStatus status = 1; } service Health { rpc Check(HealthCheckRequest) returns (HealthCheckResponse); rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); }
注:此文件是官方標准,不要做任何改動,否則可能導致健康檢查失敗
Grpc方式的使用如AspNetCore.WebApi.Server的Startup中的服務注冊:
public void ConfigureServices(IServiceCollection services) { ... //consul服務注冊 services.AddConsulClient(options => { options.Address = "http://192.168.209.128:18405"; options.Datacenter = "dc1"; //options.Token = "token";//如果有token }).AddService(options => { //http服務 options.Host = ip; options.Port = port; options.Id = $"server_{ip}_{port}"; options.Name = "server"; options.Tags = new[] { "server" }; options.HealthCheckPath = "Health"; }).AddService(options => { //grpc服務 options.Host = ip; options.Port = grpcPort; options.Id = $"server_grpc_{ip}_{grpcPort}"; options.Name = "server_grpc"; options.Tags = new[] { "server" }; //options.HealthCheckUrl = $"http://{ip}:{port}/Health";//使用http的健康檢測 //使用grpc做健康檢查 options.HealthCheckUseGrpc = true; options.HealthCheckUrl = $"{ip}:{grpcPort}";//依賴health.proto }); ... }
AspNetCore.WebApi.Server的Startup中注冊了兩個服務,一個用於提供http服務,一個用於提供grpc服務,添加的服務,最終使用IHostedService來完成注冊。
服務之間的通信通常采用http方式和grpc方式,如AspNetCore.WebApi.Client使用http和grpc兩種方式調用AspNetCore.WebApi.Server服務,在AspNetCore.WebApi.Client的Startup中添加HttpClient和GrpcClient的客戶端:
public void ConfigureServices(IServiceCollection services) { ... //http client services.AddHttpClient("http", client => { client.BaseAddress = new Uri("http://server");//server是Server的http服務注冊進Consul的服務名 }).AddServiceDiscovery("consul", LoadBalancerMode.RoundRobin);//添加服務發現機制 //grpc client services.AddGrpcClient<WebApiServer.WebApiServerClient>("grpc", options => { options.Address = new Uri("http://server_grpc");//server_grpc是Server的grpc服務注冊進Consul的服務名 }) .AddServiceDiscoveryPolling(options =>//添加服務發現機制 { options.Address = "http://192.168.209.128:18406"; options.Datacenter = "dc1"; //options.Token = "token";//如果有token }); ... }
AddHttpClient和AddGrpcClient就是你熟悉的那種調用方式,只是還添加了一個尾巴:AddServiceDiscovery和AddServiceDiscoveryPolling
AddServiceDiscovery:添加服務發現機制,服務名即請求地址中的host部分(不能攜帶端口),通知指定使用的consul客戶端(如果是名稱,則是使用AddConsulClient添加的客戶端,也可以指定具體的consul信息),LoadBalancerMode是均衡模式,也就是說,當發起一個請求時,會將host部分作為服務名,使用指定的這個consul客戶端去獲取所有這個服務的實例,然后采用LoadBalancerMode指定的模式從這些實例中獲得一個可用的實例,然后轉而請求這個實例的資源。
AddServiceDiscoveryPolling:作用同AddServiceDiscovery,只是不在是每次請求都是使用consul客戶端去獲取服務實例,而是從實例緩存去獲取可用的實例,這背后有一個定時器定時的去獲取服務實例,然后刷新實例緩存,這樣可以提高性能,但是不保證服務實例的可用性。
調用方式只需像原來的HttpClient和GrpcClient的方式去調用就可以了,如AspNetCore.WebApi.Client的RemoteController中的使用:
/// <summary> /// 使用Http方式調用遠程接口 /// </summary> /// <param name="name"></param> /// <returns></returns> [HttpGet] public async Task<object> Http(string name) { var httpClientFactory = HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>(); var httpClient = httpClientFactory.CreateClient("http");//http是Startup中注冊的http client名稱 var response = await httpClient.GetAsync($"/Remote?name={name}"); return await response.Content.ReadAsStringAsync(); } /// <summary> /// 使用Grpc方式調用遠程接口 /// </summary> /// <param name="name"></param> /// <returns></returns> [HttpGet] public async Task<object> Grpc(string name) { var grpcClientFactory = HttpContext.RequestServices.GetRequiredService<GrpcClientFactory>(); var grpcClient = grpcClientFactory.CreateClient<WebApiServer.WebApiServerClient>("grpc");//grpc是Startup中注冊的grpc client名稱 var data = await grpcClient.SayAsync(new DataRequest() { Name = name }); return data.Message; }