系列文章

相关源码:https://github.com/SkyChenSky/Sikiro

前言

  2020.1.10,陪我老婆到她所属的千亿企业的科技部值班,顺便参观了一下他们IT部门,舒适的环境让我灵感大发,终于把这篇拖了半年的博文完成了。

  上一篇文章《.Net微服务实战之负载均衡(上)》从DNS、LVS和Nginx讲解如何在实战中结合使用,那么以上三种负载方式离开发人员相对来说比较远,平常也不容易接触到,更多是由团队的运维或者技术Leader关注的比较多。

  该篇主要讲解在微服务架构中,如何使用我们耳熟能详的API网关+服务注册中心进行负载均衡的请求。让大家在实际工作中知道,如何将拆分后的微服务应用衔接起来,如何在微服务应用之间跨主机的访问容器进行请求。

  下文的中间件的部署与使用,我将以下面的网络拓扑图的形式大家进行演示。在实际开发项目中,是以Docker Overlay的网络方式部署的,有些中间件为了开放给开发人员使用并且在文章中很好的展示给各位读者,我是把容器端口映射到了宿主,大家可以根据自生的实际情况进行定义。该文虽然是说.Net的微服务,但是实际上这几个中间件可以使用到其他各种平台,也是比较开源界相对热门、稳定的。

  其次我也把在日常和同行沟通的时候,讨论得最多的问题给整理了出来,也方便入门微服务的读者能解答心中的疑惑,只有基础、理论理解清楚了,才能很好的进行实施。

我被问得多的那些问题

服务之间的调用关系容易混乱,该怎么划分?

  下图的架构分层图是我当时实施后的应用分层,在这张图有几个关键点我给大家列一下:

  •   自顶向下的分为UI层、聚合API层、公共API层,
  •   每一层只会依赖于下一层,不会跨层依赖,也不会同一层之间存在调用依赖
  •   聚合API层与公共API层都是属于内网环境,无法被外网直接访问,聚合API层如果需要被UI层调用或者外网访问则由API网关暴露出去,公共API层需要被聚合API层访问则由RPC、Consul与Fabio进行衔接请求。
  •        纵向维度的从物理视角出发,分层目的是为了职责分离,避免调用关系乱套,循环依赖的问题
  •   水平维度的从业务视角出发,分解目的是为了解耦与复用

在微服务架构里前后端分离后不知道如何调用API接口?

  该问题其实跟微服务无关,也就是前后端分离的基本问题。提出的人应该是属于做单体系统多了,然后去了解微服务的时候发现概念多中间件多,什么API网关、服务注册中心、RPC的直接把他们搞晕了。

  对于该问题的回答就是,客户端与API之间是使用HTTP协议进行交互的,甚至是微服务内的服务与服务之间都是以HTTP协议进行交互,因为马丁福勒在他的博客里说了个重要的单词【轻量】,该词就是指轻量级的通信协议也就是HTTP。

  那么对于该问题的一个衍生问题就是,我怎么知道该接口怎么调用呢?答案就是Swagger,Swagger担任的服务描述的重任,他描述了,接口路径、协议类型、参数结构,只要有了以上3者是不是就很好让前后端人员对接了。

  清楚上面的问题后,再引入API网关,API网关其实就是把原本零散的API服务给整合起来,形成统一的流量入口,由API网关进行路由转发,如下图:

微服务里服务与服务之间是怎么通信的?

  首先协议跟上面的问题一致是HTTP的,那么在.Net里HTTP API是不是可以通过HttpClient进行请求?但是HttpClient调用API时是需要关注很多参数的细节,那么RPC的优势就来了。

  RPC主要工作是像调用内部方法一样做远程调用和隐藏请求细节。在.Net里WebApiClient和gRPC都是不错的RPC框架。

  此外,RPC框架也是我认为做微服务第一个考虑选型并且慎重选型的组件。

从服务注册中心拿到的是服务地址列表,该怎么做负载均衡请求?

  我们从服务注册中心拿到某个服务信息是一组ip+port的集合,那么需要对该集合的某一项进行请求。

  有两种解决方式:

  •   调用端RPC集成,从注册中心获取服务地址列表,然后使用负载均衡算法选择其中一个IP+Port让RPC进行请求
  •   使用中间件,该中间件是与注册中心集成的,例如Consul+Fabio,调用端会通过RPC框架请求Fabio,Fabio会从Consul获取健康的地址请求转发。

  下面的使用我主要以中间件的方式来解决上述的问题,主要.Net多数RPC是没有集成注册中心,如果由开发人员整合起来,改动相对会花精力与时间。

PS:上面的提到的API网关、Fabio请求转发如果把大家绕晕了话,你们可以把他们两个当成类似Nginx功能(不完全一样)的中间件。

  那么经过上面问题讲述后,那么就可以开始接下来的Kong、Consul、Fabio与.Net Core的集成使用。

Docker环境的准备

所有服务器关闭防火墙,不然下面使用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了。

Kong与KongA的部署

对于中间件的部署,我建议在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

KongA的使用

初始化配置与仪表盘

Kong的一些基本概念

  Service。顾名思义,就是我们自己定义的上游服务,通过Kong匹配到相应的请求要转发的地方, Service 可以与下面的Route进行关联,一个Service可以有很多Route,匹配到的Route就会转发到Service中。Service服务,通过Kong匹配到相应的请求要转发的地方(eg: 理解nginx 配置文件中server)

  Route。实体定义匹配客户端请求的规则. 每个路由都与一个服务相关联,而服务可能有多个与之相关联的路由. 每一个匹配给定路线的请求都将被提交给它的相关服务。Route 路由相当于nginx 配置中的location

  Upstream。用来配置转发真实地址的集合,类似于Nginx的Upstream模块。

添加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一致。

效果图

Consul的部署

在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;
}

Docker构建.Net Core微服务

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

Fabio的部署

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集成使用

在该篇文章,我主要使用了中间件代理的方式处理了微服务内部的负载均衡请求,那么在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三个中间件结合来讲述了他们的调用关系与使用。

  如果有文章有任何问题与更新的思路与方案,可在下方评论反馈给我

  

.Net微服务实战之负载均衡(下)的更多相关文章

  1. .Net微服务实战之负载均衡(上)

    系列文章 .Net微服务实战之技术选型篇 .Net微服务实战之技术架构分层篇 .Net微服务实战之DevOps篇 相关源码:https://github.com/SkyChenSky/Sikiro P ...

  2. .Net微服务实战之必须得面对的分布式问题

    系列文章 .Net微服务实战之技术选型篇 .Net微服务实战之技术架构分层篇 .Net微服务实战之DevOps篇 .Net微服务实战之负载均衡(上) .Net微服务实战之CI/CD .Net微服务实战 ...

  3. .Net微服务实战之可观测性

    系列文章 .Net微服务实战之技术选型篇 .Net微服务实战之技术架构分层篇 .Net微服务实战之DevOps篇 .Net微服务实战之负载均衡(上) .Net微服务实战之CI/CD .Net微服务实战 ...

  4. 微服务实战(二):使用API Gateway

    微服务实战(一):微服务架构的优势与不足 微服务实战(二):使用API Gateway 微服务实战(三):深入微服务架构的进程间通信 微服务实战(四):服务发现的可行方案以及实践案例 微服务实践(五) ...

  5. Spring Cloud微服务实战阅读笔记(一) 基础知识

    本文系<Spring Cloud微服务实战>作者:翟永超,一书的阅读笔记. 一:基础知识   1:什么是微服务架构     是一种架构设计风格,主旨是将一个原本独立的系统拆分成多个小型服务 ...

  6. 微服务实战(二):使用API Gateway - DockOne.io

    原文:微服务实战(二):使用API Gateway - DockOne.io [编者的话]本系列的第一篇介绍了微服务架构模式.它讨论了采用微服务的优点和缺点,除了一些复杂的微服务,这种模式还是复杂应用 ...

  7. 微服务实战(一):微服务架构的优势与不足 - DockOne.io

    原文:微服务实战(一):微服务架构的优势与不足 - DockOne.io [编者的话]本文来自Nginx官方博客,是微服务系列文章的第一篇,主要探讨了传统的单体式应用的不足,以及微服务架构的优势与挑战 ...

  8. SpringCloud Alibaba微服务实战三 - 服务调用

    导读:通过前面两篇文章我们准备好了微服务的基础环境并让accout-service 和 product-service对外提供了增删改查的能力,本篇我们的内容是让order-service作为消费者远 ...

  9. springcloud微服务实战--笔记

    目前对Springcloud对了解仅限于:“用[注册服务.配置服务]来统一管理其他微服务” 这个水平.有待提高 Springcloud微服务实战这本书是翟永超2017年5月写的,时间已经过去了两年,略 ...

随机推荐

  1. flask加载配置文件的三种方法

    1.第一种方法也是我们最长用到的,包括我们项目中也是采用第一种的方法,加载配置文件 配置信息全部写在config.py里面,在主app.py的文件中写入 import config app.confi ...

  2. 巨经典论文!推荐系统经典模型Wide & Deep

    今天我们剖析的也是推荐领域的经典论文,叫做Wide & Deep Learning for Recommender Systems.它发表于2016年,作者是Google App Store的 ...

  3. PCANBasic开发(二)

    使用Peak的PCan转换器开发,使用其中的PCanBasic.dll // PCANBasic.cs // // ~~~~~~~~~~~~ // // PCAN-Basic API // // ~~ ...

  4. 【题单】最近遇见的 SHIT DP题 三连

    Hint: 本题单适合用于自虐和消磨时间. CF-Gym101620E https://codeforces.com/gym/101620 ARC109F https://atcoder.jp/con ...

  5. 基于Fisco-Bcos的区块链智能合约-简单案例实践

    一.智能合约介绍 智能合约是指把合同/协议条款以代码的形式电子化地放到区块链网络上.FISCO BCOS平台支持两种智能合约类型:Solidity智能合约与预编译智能合约 Solidity与Java类 ...

  6. MySQL的验证方式

    mysql8之后root用户的密码验证方式修改了,mysql8的加密方式为caching_sha2_passoword,而navicat连接所用的方式为native_password. 使用命令mys ...

  7. springboot配置ssl证书

    springboot默认使用的是tomcat: 1.先到阿里云上注册一个证书,绑定域名:后面可以在管理中下载证书,下载tomcat对应的证书(一个*.pfx文件和*.txt文件) 2.将pfx文件拷贝 ...

  8. github拉去代码慢的处理方式(最简单)

    https://github.com/xxx/xxxx 替换成 https://github.com.cnpmjs.org/xxx/xxxx 再去拉取,速度快很多,亲测可用

  9. AWT08-绘图

    1.组件绘图原理 Java GUI能展示出不同对话框.窗口等等组件外观的本质其实就是绘图. 在AWT中,真正提供绘图功能的是Graphics对象,在Component中提供了三个方法来完成组件图形的绘 ...

  10. 【智简联接,万物互联】华为云·云享专家董昕:Serverless和微服务下, IoT的变革蓄势待发

    摘要:Serverless.微服务,这些新技术和IoT有什么关系?纵观IoT行业的发展,云服务又扮演了什么角色? IoT并不是一个新名词.新技术,很长一段时间,它甚至给人一种"下工地&quo ...