【学习笔记一】.Net Core 3.1+微服务+Asp.Net Core开发+Core WebApi集群+Nginx+Consul+Ocelot+Polly+Ids4

入门学习.Net Core3.1 微服务架构,遇到的问题以及如何解决。代码示例及教程均参照up主“微软MVP-Eleven”的视频教程。

先上代码。 从一个单体示例开始搞起,逐步向微服务架构演化。

开发环境:VS2019、.Net Core3.1

项目结构如下图所示:

在这里插入图片描述
项目说明:

Zhaoxi.MicroService.ClientDemo --web项目 作为一个client客户端调用并展示数据
Zhaoxi.MicroService.Interface --类库项目 一个接口抽象(对应后面的服务类)
Zhaoxi.MicroService.Model --类库项目 数据模型类 模拟数据
Zhaoxi.MicroService.Service --类库项目 实现接口抽象 返回数据(数据接口服务)
麻雀虽小五脏俱全。一个简单的单体应用模型。

主要代码:(Zhaoxi.MicroService.ClientDemo)HomeController.cs

//using System;
//using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Zhaoxi.MicroService.ClientDemo.Models;
using Zhaoxi.MicroService.Interface;


namespace Zhaoxi.MicroService.ClientDemo.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IUserService _iUserService = null;

        public HomeController(ILogger<HomeController> logger, IUserService userService)
        {
            _logger = logger;
            this._iUserService = userService;
        }

        public IActionResult Index()
        {
            base.ViewBag.Users = this._iUserService.UserAll();
            return View();
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

(Zhaoxi.MicroService.ClientDemo)startup.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Zhaoxi.MicroService.Interface;
using Zhaoxi.MicroService.Service;


namespace Zhaoxi.MicroService.ClientDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddTransient<IUserService, UserService>();//配置注入  少了这句会在网站启动后报错(踩坑)
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            //为了正常启动网站(dll启动) 须添加下面一段代码并将wwwroot文件夹拷贝至生成项目目录下
            app.UseStaticFiles(new StaticFileOptions() 
            { 
                FileProvider=new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(),@"wwwroot"))
            }
                );

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

模拟数据(UserService.cs):

        private List<User> _UserList = new List<User>()
        {
            new User()
            {
                Id=1,
                Account="Adminstrator",
                Email="111111QQ",
                Name="小李1",
                Password="11111",
                LoginTime=DateTime.Now,
                Role="Admin1"


            },
             new User()
            {
                Id=2,
                Account="Adminstrator",
                Email="111111QQ",
                Name="小李2",
                Password="11111",
                LoginTime=DateTime.Now,
                Role="Admin2"
            },
              new User()
            {
                Id=1,
                Account="Adminstrator",
                Email="111111QQ",
                Name="小李3",
                Password="11111",
                LoginTime=DateTime.Now,
                Role="Admin3"
            }
        };
       

页面显示的代码home/index:

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
   <h2>当前的用户信息:</h2>
    @foreach (var user in base.ViewBag.Users)
    { 
        <p>@user.Name</p>
    }
</div>
项目运行启动:命令模式启动

项目编译成功之后,在生成的bin目录下打开dos命令窗口。执行如下命令:

dotnet Zhaoxi.MicroService.ClientDemo.dll --urls="http://*:5177" --ip="127.0.0.1" --port=5177

在这里插入图片描述
执行成功后在浏览器中输入地址(http://localhost:5177/Home/index)查看:
在这里插入图片描述

现在项目示例成功跑起来了,下面就从它开始,一步一步改造升级。

首先,对这个项目进行一个“前后端分离”的操作。

创建一个webapi,用来提供数据服务。然后用ClientDemo这个web直接调用webapi来获取要展示的数据。

在解决方案中新增一个webapi项目(Zhaoxi.MicroService.ServiceInstance),添加引用:Zhaoxi.MicroService.Interface --类库项目 一个接口抽象(对应后面的服务类)、
Zhaoxi.MicroService.Model --类库项目 数据模型类 模拟数据、
Zhaoxi.MicroService.Service --类库项目 实现接口抽象 返回数据(数据接口服务)。
在这里插入图片描述

项目主要代码:
UsersController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Zhaoxi.MicroService.Interface;
using Zhaoxi.MicroService.Model;

namespace Zhaoxi.MicroService.ServiceInstance.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        private readonly ILogger<UsersController> _logger;
        private readonly IUserService _iUserService=null;
        private IConfiguration _iConfiguration;
        public UsersController(ILogger<UsersController> logger, IUserService userService, IConfiguration configuration)
        {
            _logger = logger;
            this._iUserService = userService;
            this._iConfiguration = configuration;
        }
        [HttpGet]
        [Route("Get")]
        public User Get(int id)
        {
            return this._iUserService.FindUser(id);
        }
        [HttpGet]
        [Route("All")]
        public IEnumerable<User> Get()
        {
            Console.WriteLine($"This is  UsersController{this._iConfiguration["port"]} Invoke");
            return this._iUserService.UserAll().Select(u => new Model.User()
            {
                Id = u.Id,
                Account = u.Account,
                Name = u.Name,
                Role = $"{this._iConfiguration["ip"]}{this._iConfiguration["port"]}",
                Email = u.Email,
                LoginTime = u.LoginTime,
                Password = u.Password
            });
        }
    }
}

Startup.cs中添加完成注册

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddTransient<IUserService,UserService>();//注册
        }

Program.cs中添加支持命令行输出

public static void Main(string[] args)
        {
            new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddCommandLine(args)//支持命令行参数
                .Build();
            CreateHostBuilder(args).Build().Run();
        }

代码搞定之后,编译生成,然后用命令行的方式启动该api。
命令:dotnet Zhaoxi.MicroService.ServiceInstance.dll --urls="http://*:5726" --ip="127.0.0.1" --port=5726
在这里插入图片描述
启动成功之后在浏览器中输入地址(http://localhost:5726/api/users/all)查看:
在这里插入图片描述
到此,接口API已经搞定,可以单独的跑起来提供数据服务了。

下面对客户端clientClientDemo做一下改造,让它通过调用刚才运行的api来获取数据。
代码改造(HomeController.cs):

public IActionResult Index()
        {
            //base.ViewBag.Users = this._iUserService.UserAll();

            string url = "http://localhost:5726/api/users/all";
            string content = InvokeApi(url);
            base.ViewBag.Users = 
                Newtonsoft.Json.JsonConvert.DeserializeObject<IEnumerable<User>>(content);
            Console.WriteLine($"This is {url} Invoke");
            return View();
        }

之前直接调用的方式屏蔽 //base.ViewBag.Users = this._iUserService.UserAll();

改用通过api地址获取接口数据。(用到了Json序列化,需要项目中添加插件管理包Newtonsoft.Json)。
获取接口数据所用到的一个URL解析函数InvokeApi(url),其代码如下,拷贝至HomeController.cs中即可。

//通过接口地址获取返回数据
        public static string InvokeApi(string url)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                HttpRequestMessage message = new HttpRequestMessage();
                message.Method = HttpMethod.Get;
                message.RequestUri = new System.Uri(url);
                var result = httpClient.SendAsync(message).Result;
                string content = result.Content.ReadAsStringAsync().Result;
                return content;
            }
        }

简单改造之后,重新生成项目,之后再次启动(同样的命令行启动,参照上面)。
启动成功之后,在浏览器中查看,发现与上次的显示界面一模一样,但是背后的调用逻辑是不一样的,现在页面上的数据来源是从接口API中而来。如果把API服务停掉,再刷新页面就会发现页面报错,因为找不到数据源了。

到了这一步,已经完美实现了前后端分离,客户端是客户端,接口是接口。
接下来要搞的就是对数据接口API进行分布式改造。同样的接口多个部署,由客户端来负载均衡调用。这么一来,健壮性就有了。

下面接着整。

使用命令启动方式,启动三个api服务(同样的服务):
dotnet Zhaoxi.MicroService.ServiceInstance.dll --urls="http://*:5726" --ip="127.0.0.1" --port=5726
dotnet Zhaoxi.MicroService.ServiceInstance.dll --urls="http://*:5727" --ip="127.0.0.1" --port=5727
dotnet Zhaoxi.MicroService.ServiceInstance.dll --urls="http://*:5728" --ip="127.0.0.1" --port=5728

在三个命令窗口中分别运行,成功后在浏览器中分别访问:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从图中可以发现,三个接口api全部正常启动。(数据基本一致,可以通过其中显示的端口号进行区分。在实际生产中可能是完全一致的数据,因为干的是同样的事。)

现在,小小的一个服务集群已经组成。

再接下来,看客户端怎么来使用这个小集群了。

现在可调用的接口地址有三个:
http://localhost:5726/api/users/all
http://localhost:5727/api/users/all
http://localhost:5728/api/users/all

而现在的客户端只能使用其中的一个,当然也能多个融合使用(自己去鼓捣一通负载逻辑),不过这方法显然不够友好,而且在客户端里搞负载也不合适,因为客户端也可能是很多,每个里面都搞一下,有点憨憨了。

其实,有很多第三方的工具可以使用。这里主要使用Nginx和Consul。

下面先用Nginx来解决问题。
Nginx通过反向代理,一个入口映射三个API接口,负载策略也可以按照需求在Nginx中进行配置。
在这里插入图片描述
如上图,有了Nginx的帮助,客户端还是调用一个地址,但是可以负载使用小服务集群了。

下面来配置并启动Nginx:
配置信息(C:\nginx-1.18.0\conf\nginx.conf)如图:
在这里插入图片描述
配置完毕后启动:cmd start ngnix.exe
成功启动后,在浏览器中查看:
在这里插入图片描述
这时候,通过Nginx已经实现了负载均衡,负载策略默认为轮询模式。通过刷新页面,可以发现显示数据中端口号在轮替变化,说明负载已经成功。
现在小集群对外只有一个代理地址 http://localhost:8088/api/users/all
客户端只需要访问该地址,就可以实现获取数据。有了小集群的服务支持,服务压力也就可以分流了。

修改客户端连接服务地址的代码:
在这里插入图片描述
为了使页面有所区别,同时也为了方便查看确实是能够负载使用多个服务的,对页面显示做一下修改,新增显示ip 和端口。
在这里插入图片描述
然后重新编译生成并启动,发现页面正常访问。
第一次加载展示:
在这里插入图片描述
刷新后第二次显示:
在这里插入图片描述
再刷新后第三次显示:
在这里插入图片描述
可知,调用的服务负载已经实现。

到此一步,有必要做一个小总结:

  1. 服务剥离出来(前后端分离),这是一个升级。
  2. 单一的服务api转向小集群,这又是一个升级。

虽然有了进步,但是问题还有不少:

  1. Nginx中服务的配置是写死的,增加或减少服务,都很不方便。
  2. 缺少健康检查,集群中某个服务挂了,会影响正常使用。

为了解决这个问题,就需要更近一步升级,从Nginx到Consul。
consul可以做到动态注册服务和发现服务,同时也自带健康检查。

接下来看怎么接入Consul。

第一步 服务注册。
项目(Zhaoxi.MicroService.ServiceInstance)中添加consul包:
在这里插入图片描述
然后项目中添加一个完成注册consul的类文件,结构如图:
在这里插入图片描述
ConsulHelper.cs 代码:

using Consul;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Zhaoxi.MicroService.ServiceInstance.utility
{
    public static class ConsulHelper
    {
        public static void ConsulRegist(this IConfiguration configuration)
        { 
            ConsulClient client=new ConsulClient(c=>
            {
                c.Address=new Uri("http://192.168.153.131:8500/");
                c.Datacenter="dc1";
            });

            string ip = configuration["ip"];
            int port = int.Parse(configuration["port"]);
            int weight = string.IsNullOrWhiteSpace(configuration["weight"]) ? 1 : int.Parse(configuration["weight"]);

            client.Agent.ServiceRegister(new AgentServiceRegistration()
            {
                ID = "Service" + Guid.NewGuid(),
                Name = "ZhaoxiService",
                Address = ip,
                Port = port,
                Tags = new string[] { weight.ToString() }//标签
                
                //consul 健康检查
                Check = new AgentServiceCheck()
                {
                    Interval = TimeSpan.FromSeconds(12),//间隔12s一次
                    HTTP = $"http://{ip}:{port}/Api/Health/Index",
                    Timeout = TimeSpan.FromSeconds(8),//检测等待时间
                    DeregisterCriticalServiceAfter= TimeSpan.FromSeconds(20),//失败后多久移除
                }
            }) ;
            //命令行参数获取
            Console.WriteLine($"{ip}:{port}--weight:{weight}");
        
       
        }
    }
}

添加完毕后,需要在api服务启动时完成调用注册。在startup.cs中作如下改动:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            //服务启动时向Consul注册,写在这里,启动时只需要注册一次
            this.Configuration.ConsulRegist();
        }

为了在服务启动时能注册,首先启动consul服务,我这里是在虚拟机中启用的一个单节点consul服务,地址:http://192.168.153.131:8500/ui/dc1/services
在这里插入图片描述
然后将API服务项目Zhaoxi.MicroService.ServiceInstance重新编译并启动,查看注册情况。
在这里插入图片描述
如上图,可知启动的三个服务已经在consul中完成了注册。
注意:为了使得Consul监测能正常使用,需要保证虚拟机种运行的Consul能访问本机上运行的API服务,在服务启动时需要将参数中 127.0.0.1 更改为本机的IP地址。如果您的Consul也是在本机运行,可以不必考虑这一点。

接下来看客户端中怎么从consul中完成服务的调用,代码如下:

public IActionResult Index()
        {
            //base.ViewBag.Users = this._iUserService.UserAll();

            //string url = "http://localhost:5726/api/users/all";
            string url = "http://localhost:8088/api/users/all";//nginx

            #region 从consul中获取服务
            url = "http://ZhaoxiService/api/users/all";

            ConsulClient client=new ConsulClient(c=>
            {
                c.Address = new Uri("http://192.168.153.131:8500/");
                c.Datacenter = "dc1";
            });
            var response = client.Agent.Services().Result.Response;
            //foreach (var item in response) {
            //    Console.WriteLine("**********************************");
            //    Console.WriteLine(item.Key);
            //    var service = item.Value;
            //    Console.WriteLine($"{service.Address}--{service.Port}--{service.Service}");
            //    Console.WriteLine("**********************************");
            //}

            Uri uri = new Uri(url);
            string groupName = uri.Host;
            AgentService agentService = null;
            var serviceDictionary = response.Where(s => s.Value.Service.Equals(groupName,
                  StringComparison.OrdinalIgnoreCase)).ToArray();//获取到一个组中的所有服务
             
            int Length = serviceDictionary.Length;
            // agentService = serviceDictionary[iIndex++ % Length].Value;// 负载均衡策略  轮询

            // 负载均衡策略  平均(随机获取索引 相对平均)
            agentService = serviceDictionary[new Random(iIndex++).Next(0, Length)].Value;

            /*
            //权重策略  能给不同的实例分配不同的压力 ---注册时提供权重参数
            List<KeyValuePair<string, AgentService>> pairsList = new List<KeyValuePair<string, AgentService>>();
            foreach (var pair in serviceDictionary)
            {
                int count = int.Parse(pair.Value.Tags?[0]);
                for (int i = 0; i < count; i++)
                {
                    pairsList.Add(pair);
                }
            }
            agentService = pairsList.ToArray()[new Random(iIndex++).Next(0, pairsList.Count())].Value;
            */
            
            url = $"{uri.Scheme}://{agentService.Address}:{agentService.Port}{uri.PathAndQuery}";


            #endregion


            string content = InvokeApi(url);
            base.ViewBag.Users = 
                Newtonsoft.Json.JsonConvert.DeserializeObject<IEnumerable<User>>(content);
            Console.WriteLine($"This is {url} Invoke");
            return View();
        }

consul只是把注册的服务地址转交给客户端,客户端能够根据一定匹配规则获取需要用到的所有接口服务地址,然后从中选择一个使用。(负载策略需要在客户端自己写)

小结:Consul解决了使用Nginx不能动态注册服务以及缺少健康监测的问题,但是到现在这一步,发现负载均衡策略又需要在客户端中完成,之前就说了这是不合理的。为了解决这个问题,这就需要再引入“网关”来升级一下架构。具体的玩法,下一篇中继续!

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值