相關源碼:https://github.com/SkyChenSky/Sikirohtml
2020.1.10,陪我老婆到她所屬的千億企業的科技部值班,順便參觀了一下他們IT部門,溫馨的環境讓我靈感大發,終於把這篇拖了半年的博文完成了。node
上一篇文章《.Net微服務實戰之負載均衡(上)》從DNS、LVS和Nginx講解如何在實戰中結合使用,那麼以上三種負載方式離開發人員相對來講比較遠,日常也不容易接觸到,更可能是由團隊的運維或者技術Leader關注的比較多。nginx
該篇主要講解在微服務架構中,如何使用咱們耳熟能詳的API網關+服務註冊中心進行負載均衡的請求。讓你們在實際工做中知道,如何將拆分後的微服務應用銜接起來,如何在微服務應用之間跨主機的訪問容器進行請求。git
下文的中間件的部署與使用,我將如下面的網絡拓撲圖的形式你們進行演示。在實際開發項目中,是以Docker Overlay的網絡方式部署的,有些中間件爲了開放給開發人員使用而且在文章中很好的展現給各位讀者,我是把容器端口映射到了宿主,你們能夠根據自生的實際狀況進行定義。該文雖然是說.Net的微服務,可是實際上這幾個中間件可使用到其餘各類平臺,也是比較開源界相對熱門、穩定的。github
其次我也把在平常和同行溝通的時候,討論得最多的問題給整理了出來,也方便入門微服務的讀者能解答心中的疑惑,只有基礎、理論理解清楚了,才能很好的進行實施。redis
下圖的架構分層圖是我當時實施後的應用分層,在這張圖有幾個關鍵點我給你們列一下:算法
該問題其實跟微服務無關,也就是先後端分離的基本問題。提出的人應該是屬於作單體系統多了,而後去了解微服務的時候發現概念多中間件多,什麼API網關、服務註冊中心、RPC的直接把他們搞暈了。docker
對於該問題的回答就是,客戶端與API之間是使用HTTP協議進行交互的,甚至是微服務內的服務與服務之間都是以HTTP協議進行交互,由於馬丁福勒在他的博客裏說了個重要的單詞【輕量】,該詞就是指輕量級的通訊協議也就是HTTP。數據庫
那麼對於該問題的一個衍生問題就是,我怎麼知道該接口怎麼調用呢?答案就是Swagger,Swagger擔任的服務描述的重任,他描述了,接口路徑、協議類型、參數結構,只要有了以上3者是否是就很好讓先後端人員對接了。bootstrap
清楚上面的問題後,再引入API網關,API網關其實就是把本來零散的API服務給整合起來,造成統一的流量入口,由API網關進行路由轉發,以下圖:
首先協議跟上面的問題一致是HTTP的,那麼在.Net裏HTTP API是否是能夠經過HttpClient進行請求?可是HttpClient調用API時是須要關注不少參數的細節,那麼RPC的優點就來了。
RPC主要工做是像調用內部方法同樣作遠程調用和隱藏請求細節。在.Net裏WebApiClient和gRPC都是不錯的RPC框架。
此外,RPC框架也是我認爲作微服務第一個考慮選型而且慎重選型的組件。
咱們從服務註冊中心拿到某個服務信息是一組ip+port的集合,那麼須要對該集合的某一項進行請求。
有兩種解決方式:
下面的使用我主要以中間件的方式來解決上述的問題,主要.Net多數RPC是沒有集成註冊中心,若是由開發人員整合起來,改動相對會花精力與時間。
PS:上面的提到的API網關、Fabio請求轉發若是把你們繞暈了話,大家能夠把他們兩個當成相似Nginx功能(不徹底同樣)的中間件。
那麼通過上面問題講述後,那麼就能夠開始接下來的Kong、Consul、Fabio與.Net Core的集成使用。
全部服務器關閉防火牆,否則下面使用Overlay2後,容器之間也沒法ping通,若是本來已經啓動了防火牆後再關閉的後須要重啓docker。
#關閉防火牆
systemctl disable firewalld
#重啓docker
systemctl restart docker
在Server A初始化Docker Swarm
docker swarm init --advertise-addr 192.168.88.138
而後在其餘worker節點Server B和Server C執行上面反饋的指令加入Docker Swarm集羣
docker swarm join --token SWMTKN-1-0odogegq3bwui4o76aq5v05doqqvuycb5jmuckjmvzy4bfmm59-ewht2cz6fo0r39ky44uv00aq5 192.168.88.138:2377
在Server A上能夠查看Docker Swarm節點信息
docker node ls
在Server A建立Overlay2網絡覆蓋,方便後續建立的容器之間能夠跨主機訪問
docker network create -d overlay --attachable overlay
測試容器之間是否能夠跨主機訪問
#建立nginx集羣 docker service create -d --network=overlay --replicas 3 --name=nginx nginx #查找出某個實例的Ip docker inspect 1af2984adda9 #進入另外容器實例嘗試請求跨主機請求 docker exec -it 1af2984adda9 /bin/bash curl 10.0.1.8
有如下響應結果就是網絡環境OK了。
對於中間件的部署,我建議在docker run的指令裏指定【--ip】,避免每次啓動的時候IP不一致,所以在應用配置須要指定。
安裝postgres數據庫
docker run -d --name kong-database --network=overlay -p 5432:5432 -e "POSTGRES_USER=kong" -e "POSTGRES_PASSWORD=kong" -e "POSTGRES_DB=kong" postgres:9.6
初始化kong數據庫
docker run --rm -e "KONG_DATABASE=postgres" -e "KONG_PG_HOST=192.168.88.144" -e "KONG_PG_USER=kong" -e "KONG_PG_PASSWORD=kong" -e "KONG_CASSANDRA_CONTACT_POINTS=postgres" kong:2.2 kong migrations bootstrap
啓動kong應用
docker run -d --ip=10.0.1.111 --name kong --network=overlay -e "KONG_DATABASE=postgres" -e "KONG_PG_HOST=192.168.88.144" -e "KONG_PG_USER=kong" -e "KONG_PG_PASSWORD=kong" -e "KONG_CASSANDRA_CONTACT_POINTS=postgres" -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" -e "KONG_PROXY_ERROR_LOG=/dev/stderr" -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" -p 8000:8000 -p 8443:8443 -p 8001:8001 -p 8444:8444 kong:2.2
請求看看kong是否部署成功
curl -i http://192.168.88.144:8001/services
安裝konga
docker run -d --ip=10.0.1.112 -p 1337:1337 --network=overlay --restart=always -e "TOKEN_SECRET=chengong1218" -e "DB_ADAPTER=postgres" -e "DB_HOST=192.168.88.144" -e "DB_USER=kong" -e "DB_PASSWORD=kong" -e "DB_DATABASE=kong" -e "NODE_ENV=development" --name konga pantsel/konga:0.14.9
初始化配置與儀表盤
Kong的一些基本概念
Service。顧名思義,就是咱們本身定義的上游服務,經過Kong匹配到相應的請求要轉發的地方, Service 能夠與下面的Route進行關聯,一個Service能夠有不少Route,匹配到的Route就會轉發到Service中。Service服務,經過Kong匹配到相應的請求要轉發的地方(eg: 理解nginx 配置文件中server),等同於下面nginx的配置:
http { server { listen 80; location / { proxy_pass http://msg.upstream; } } }
Route。實體定義匹配客戶端請求的規則. 每一個路由都與一個服務相關聯,而服務可能有多個與之相關聯的路由. 每個匹配給定路線的請求都將被提交給它的相關服務。Route 路由至關於nginx 配置中的location
http { server { listen 80; server_name api.service.com; location /test{ proxy_pass https://msg.upstream; } } }
Upstream。用來配置轉發真實地址的集合,相似於Nginx的Upstream模塊。
upstream upstream.api { server www.jd.com:443 weight=100; server www.baidu.com443 weight=100; }
添加Service
把圈起來的4項填寫好,在實際場景能夠根據本身的技術狀況填寫Protocol=http,Port=80,下面我將有taobao.com和baidu.com因此暫時用https和443.
添加Route
把Route模塊的Name、Path和Methods填寫好,在這裏須要注意的是Path和Methods每填寫一項得回車一次,否則保存後是沒有效果的。
添加Upstream
Kong的Upstream設置要添加Target和Upstream,注意Target的Name須要與Service配置的Host一致。
通過上面的操做後,使用是沒有多大問題的了,可是應用基於Docker啓動後容器IP也是不固定的,那麼手動添加的場景確定不方便,不靈活。國人開源了一款Kong.Net-https://github.com/lianggx/Kong.Net,讓微服務應用在啓動後把他自己的信息註冊到Kong,這樣Kong也不須要與Consul作整合,能夠理解成微服務應用經過Kong.Net把IP+Port註冊到了kong裏。
/ This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, KongClient kongClient) { UseKong(app, kongClient); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(); } public void UseKong(IApplicationBuilder app, KongClient kongClient) { var upStream = Configuration.GetSection("kong:upstream").Get<UpStream>(); var target = Configuration.GetSection("kong:target").Get<TargetInfo>(); var uri = new Uri(Configuration["server.urls"]); // This target is your host:port target.Target = uri.Authority; app.UseKong(kongClient, upStream, target); }
在Server C執行如下指令
#Server模式 docker run -d --ip=10.0.1.101 --net=overlay -p 8500:8500 -p 8600:8600/udp --name=consul-server-c consul agent -server -ui -node=consul-server-c -bootstrap-expect=1 -advertise=10.0.1.101 -bind=10.0.1.101 -client=0.0.0.0 #Client模式 docker run -d --ip=10.0.1.102 --net=overlay --name=consul-client-c consul agent -node=consul-client-c -advertise=10.0.1.102 -bind=10.0.1.102 -client=0.0.0.0 -join=10.0.1.101
在Server A執行下面指令
#Server模式 docker run -d --ip=10.0.1.103 --net=overlay -p 8500:8500 -p 8600:8600/udp --name=consul-server-a consul agent -server -ui -node=consul-server-a -bootstrap-expect=1 -advertise=10.0.1.103 -bind=10.0.1.103 -client=0.0.0.0 -join=10.0.1.101 #Client模式 docker run -d --ip=10.0.1.104 --net=overlay --name=consul-client-a consul agent -node=consul-client-a -advertise=10.0.1.104 -bind=10.0.1.104 -client=0.0.0.0 -join=10.0.1.101
在Server B執行如下指令
#Server模式 docker run -d --ip=10.0.1.105 --net=overlay -p 8500:8500 -p 8600:8600/udp --name=consul-server-b consul agent -server -ui -node=consul-server-b -bootstrap-expect=1 -advertise=10.0.1.105 -bind=10.0.1.105 -client=0.0.0.0 -join=10.0.1.101 #Client模式 docker run -d --ip=10.0.1.106 --net=overlay --name=consul-client-b consul agent -node=consul-client-b -advertise=10.0.1.106 -bind=10.0.1.106 -client=0.0.0.0 -join=10.0.1.101
.Net Core應用註冊到Consul,須要注意的是得在應用啓動後把服務註冊到Consul(lifetime.ApplicationStarted),否則是沒法拿到微服務應用在Overlay2的地址,微服務默認是用HTTP由於是內網應用,因此不須要HTTPS,端口也是默認80,由於Docker會給每一個容器分配一個獨立的IP。此外Tags是Fabio約定的格式,主要讓Fabio路由用的。
/// <summary> /// Consul服務註冊 /// </summary> /// <param name="app"></param> /// <param name="lifetime"></param> /// <param name="configuration"></param> /// <returns></returns> public static IApplicationBuilder UseConsul(this IApplicationBuilder app, IHostApplicationLifetime lifetime, IConfiguration configuration) { var option = configuration.GetSection("Consul").Get<ConsulOption>(); option.ThrowIfNull(); //建立Consul客戶端 var consulClient = new ConsulClient(x => x.Address = new Uri(option.ConsulHost));//請求註冊的 Consul 地址 AgentServiceRegistration registration = null; lifetime.ApplicationStarted.Register(() => { var selfHost = new Uri("http://" + LocalIpAddress + ":" + option.SelfPort); //註冊服務 registration = new AgentServiceRegistration { Checks = new[] { new AgentServiceCheck { Interval = TimeSpan.FromSeconds(option.HealthCheckInterval), HTTP = $"{selfHost.OriginalString}/health",//健康檢查地址 Timeout = TimeSpan.FromSeconds(3) } }, ID = selfHost.OriginalString.EncodeMd5String(), Name = option.ServiceName, Address = selfHost.Host, Port = selfHost.Port, Tags = new[] { $"urlprefix-/{option.ServiceName} strip=/{option.ServiceName}" }//添加 urlprefix-/servicename 格式的 tag 標籤,以便 Fabio 識別 }; consulClient.Agent.ServiceRegister(registration).Wait(); }); //反註冊服務 lifetime.ApplicationStopping.Register(() => { if (registration != null) consulClient.Agent.ServiceDeregister(registration.ID).Wait(); }); return app; }
.Net微服務配置
{ "Logging": { "LogLevel": { "Default": "Warning", "Microsoft": "Information" } }, "AllowedHosts": "*", "redisUrl": "", "MongoDbUrl": "", "Consul": { "ServiceName": "Msg", "ConsulHost": "http://10.0.1.101:8500", "SelfPort": 80 }, "IsDebug": false }
Docker File
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base WORKDIR /app EXPOSE 80 FROM base AS final WORKDIR /app COPY ./ /app ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
Docker構建指令
docker build -t msgserver . docker run -d -p 8801:80 --network=overlay --name msgserver msgserver
registry_consul_addr爲Consul的Overlay2的IP,能夠經過docker inspect指令進行查看。
docker run -d --net=overlay -p 443:443 -p 9998:9998 -p 9999:9999 --name=fabio -e 'registry_consul_addr=10.0.1.101:8500' magiconair/fabio
部署成功後,能夠經過fabio_ip+9998端口查看服務註冊的狀況。
而後能夠fabio_ip+9999進行請求轉發,下面GIF效果圖
在該篇文章,我主要使用了中間件代理的方式處理了微服務內部的負載均衡請求,那麼在RPC的層面基本上就不須要花多餘的功夫進行集成與擴展。
下面以WebApiClient做爲例子展現如何作微服務調用(按需可使用gRPC,思路與實現方式差很少)
註冊到IOC
/// <summary> /// 註冊消息服務內部api /// </summary> /// <param name="services"></param> /// <param name="configuration"></param> private static void AddMsgApi(this IServiceCollection services, IConfiguration configuration) { services.AddHttpApi<ITest>().ConfigureHttpApiConfig(c => { c.HttpHost = new Uri("http://192.168.88.143:9999/Msg/"); c.FormatOptions.DateTimeFormat = "yyyy-MM-dd HH:mm:ss"; }); }
RPC API調用
private readonly IUser _iUser; private readonly ICode _iCode; private readonly IId _id; public UserController(IUser iUser, ICode iCode, IId id, IHttpContextAccessor httpContextAccessor) { _iUser = iUser; _iCode = iCode; _id = id; } #region 無登陸驗證請求 /// <summary> /// 註冊 /// </summary> /// <param name="registerRequest"></param> /// <returns></returns> [HttpPost("Register")] [AllowAnonymous] public async Task<ApiResult<UserLogonResponse>> RegisterUser(UserRegisterRequest registerRequest) { //手機驗證 var codeVaildResult = await _iCode.Vaild(registerRequest.CountryCode + registerRequest.Phone, registerRequest.Code); if (codeVaildResult.Failed) return codeVaildResult.ToApiResult<UserLogonResponse>(); registerRequest.UserNo = await _id.Create("D4"); var registerResult = await _iUser.RegisterUser(registerRequest.MapTo<RegisterUserRequest>()); if (registerResult.Failed) return ApiResult<UserLogonResponse>.IsFailed("註冊成功"); var token = BuildJwt(registerResult.Data.MapTo<AdministratorData>()); var response = registerResult.Data.MapTo<UserLogonResponse>(); response.Token = token; return ApiResult<UserLogonResponse>.IsSuccess("註冊成功", response); }
Api SDK提供
public interface ITest : IHttpApi { /// <returns></returns> [HttpGet("Test/Index")] ITask<ServiceResult> Test(); }
Api邏輯
[Route("[controller]/[action]")] [ApiController] public class TestController : Controller { [HttpGet] public string Index() { var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); string localIp = NetworkInterface.GetAllNetworkInterfaces() .Select(p => p.GetIPProperties()) .SelectMany(p => p.UnicastAddresses) .FirstOrDefault(p => p.Address.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(p.Address))?.Address.ToString(); var result = new List<string>(); var b = NetworkInterface.GetAllNetworkInterfaces() .Select(p => p.GetIPProperties()) .SelectMany(p => p.UnicastAddresses) .Where(p => p.Address.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(p.Address)) .Select(a => { return new { Address = a.Address.ToStr() }; }).ToList(); return b.ToJson() + "------" + localIp; } }
該篇主要講解了API網關、註冊中心怎麼集成微服務的,怎麼讓請求路由到對應的服務,這也是大多數初學的微服務相對比較難啃的一道。那麼我也是經過Kong、Consul、Fabio三個中間件結合來說述了他們的調用關係與使用。
若是有文章有任何問題與更新的思路與方案,可在下方評論反饋給我