相关源码: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三个中间件结合来说述了他们的调用关系与使用。
若是有文章有任何问题与更新的思路与方案,可在下方评论反馈给我