.Net Core 3.1簡單搭建微服務

一葉、知秋發表於2021-07-02

學如逆水行舟,不進則退!最近發現微服務真的是大勢所趨,停留在公司所用框架裡已經嚴重滿足不了未來的專案需要了,所以抽空了解了一下微服務,並進行了程式碼落地。

雖然專案簡單,但過程中確實也學到了不少東西。

寫在前面:先看下專案總體目錄以及拓撲圖,其中包括2個服務(幾乎一樣),一個閘道器,一個mvc專案。我的伺服器是用虛擬機器搭建的,環境是CentOS 7。本片文章看似繁雜冗長,其實只是記錄的步驟比較詳細,實操完成你會發現並不難理解,只是微服務的簡單入門而已。還請大神多多指教!

 

 

一、準備Consul註冊中心

在Docker中安裝一個Consul

 1. 拉取映象

docker pull consul

2. 啟動Server

啟動前, 先建立 /consul/data資料夾, 儲存 consul 的資料

mkdir -p /data/consul

 

 

 

3. 使用 docker run 啟動 server

docker run -d -p 8500:8500 -v /consul/data:/consul/data -e --name=consul1 consul agent -server -bootstrap -ui -client='0.0.0.0'
  • agent: 表示啟動 agent 程式
  • server: 表示 consul 為 server 模式
  • client: 表示 consul 為 client 模式
  • bootstrap: 表示這個節點是 Server-Leader
  • ui: 啟動 Web UI, 預設埠 8500
  • node: 指定節點名稱, 叢集中節點名稱唯一
  • client: 繫結客戶端介面地址, 0.0.0.0 表示所有地址都可以訪問
4. 啟動後,就可以訪問您的伺服器Ip+8500埠看到Consul控制檯了,如圖:
 

 

二、準備服務

1. 建立訂單服務專案

 

 

 2. 建立OrderController和HealthCheckController,用於顯示訂單資訊和進行健康檢查。

      (1)OrderController程式碼如下:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Order.API.Controllers
{
    [ApiController,Route("[controller]")]
    public class OrderController:ControllerBase
    {
        private readonly ILogger<OrderController> _logger;
        private readonly IConfiguration _configuration;

        public OrderController(ILogger<OrderController> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;
        }

        [HttpGet]
        public IActionResult Get()
        {
            string result = $"【訂單服務】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
                $"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
            return Ok(result);
        }
    }
}

  (2)HealthCheckController控制器程式碼如下:

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Order.API.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class HealthCheckController : ControllerBase
    {
        /// <summary>
        /// 健康檢查介面
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public IActionResult Get()
        {
            return Ok();
        }
    }
}

  (3)建立ConsulHelper幫助類,用來向Consul(服務註冊中心)進行服務註冊。注意,先在Nuget中新增Consul類庫。

 

 

 

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

namespace Order.API.Helper
{
    public static class ConsulHelper
    {
        /// <summary>
        /// 服務註冊到consul
        /// </summary>
        /// <param name="app"></param>
        /// <param name="lifetime"></param>
        public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime)
        {
            var consulClient = new ConsulClient(c =>
            {
                //consul地址
                c.Address = new Uri(configuration["ConsulSetting:ConsulAddress"]);
            });

            var registration = new AgentServiceRegistration()
            {
                ID = Guid.NewGuid().ToString(),//服務例項唯一標識
                Name = configuration["ConsulSetting:ServiceName"],//服務名稱
                Address = configuration["ConsulSetting:ServiceIP"], //服務所在宿主機IP
                Port = int.Parse(configuration["ConsulSetting:ServicePort"] ?? "5000"),//服務埠 因為要執行多個例項,所以要在在docker容器啟動時時動態傳入 --ConsulSetting:ServicePort="port"
                Check = new AgentServiceCheck()
                {
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服務啟動多久後註冊
                    Interval = TimeSpan.FromSeconds(10),//健康檢查時間間隔
                    HTTP = $"http://{configuration["ConsulSetting:ServiceIP"]}:{configuration["ConsulSetting:ServicePort"]}{configuration["ConsulSetting:ServiceHealthCheck"]}",//健康檢查地址
                    Timeout = TimeSpan.FromSeconds(5)//超時時間
                }
            };

            //服務註冊
            consulClient.Agent.ServiceRegister(registration).Wait();

            //應用程式終止時,取消註冊
            lifetime.ApplicationStopping.Register(() =>
            {
                consulClient.Agent.ServiceDeregister(registration.ID).Wait();
            });

            return app;
        }
    }
}

  (4)appsetting.json配置檔案程式碼

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConsulSetting": {
    "ServiceName": "order", //服務名稱
    "ServiceIP": "192.168.183.129", //服務所在宿主機的IP地址
    "ServiceHealthCheck": "/healthcheck",//健康檢查地址
    "ConsulAddress": "http://192.168.183.129:8500/" //Consul註冊中心所在宿主機的IP地址和埠
  }
}

  (5)Program類程式碼如下

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Order.API
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseUrls("http://0.0.0.0:5000");//用來在伺服器中監聽外部請求的,0.0.0.0代表任何地址都可訪問該服務,5000埠
                    webBuilder.UseStartup<Startup>();
                });
    }
}

啟動專案測試,結果如下

 

 

 2. 建立產品服務,內容跟Order服務一樣,這裡就不贅述了。注意為了區分兩個服務,把ProductController中的輸出資訊做一下改變,如:把【訂單服務】修改為【產品服務】

  (1)ProductController控制器程式碼

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Product.API.Controllers
{
    [ApiController,Route("[controller]")]
    public class ProductController:ControllerBase
    {
        private readonly ILogger<ProductController> _logger;
        private readonly IConfiguration _configuration;

        public ProductController(ILogger<ProductController> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;
        }

        [HttpGet]
        public IActionResult Get()
        {
            string result = $"【產品服務】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
                $"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
            return Ok(result);
        }
    }
}

  (2)appsetting.json程式碼

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConsulSetting": {
    "ServiceName": "product",//注意這裡的服務名稱跟Order不一樣
    "ServiceIP": "192.168.183.129",
    "ServiceHealthCheck": "/healthcheck",
    "ConsulAddress": "http://192.168.183.129:8500/" 
  }
}

  (3)執行測試,輸出內容跟Order服務大體一致。

二、部署到Docker容器(以Order服務為例)

  1. 建立DockerFile檔案,檔案程式碼如下

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:3.1 AS base
WORKDIR /app
EXPOSE 5000
COPY . .
ENTRYPOINT ["dotnet", "Order.API.dll"]

  2. 釋出專案

 

 

 3. 釋出完成之後將釋出以後的檔案目錄放到伺服器上,並將DockerFile檔案放入根目錄,如圖

 

 

 4. 構建Order服務的映象

在Order服務的根目錄下,執行docker命令,

docker build -t orderapi .

構建完成後,啟動一個docker容器同時啟動我們已經拷貝過來的Order服務,啟動命令如下(以下啟動了三個容器,分別為9050、9051、9052埠,都對映到了容器的5000埠)

docker run -d -p 9050:5000 --name order1 orderapi --ConsulSetting:ServicePort="9050"
docker run -d -p 9051:5000 --name order2 orderapi --ConsulSetting:ServicePort="9051"
docker run -d -p 9052:5000 --name order3 orderapi --ConsulSetting:ServicePort="9052"

以上,9050:5000,表示將伺服器的9050埠對映到容器的5000埠,--ConsulSetting:ServicePort="9050" 表示向服務動態傳入埠,服務通過此埠來向Consul註冊埠資訊

5. 觀察Consul控制檯,如果5秒後,控制檯出現了新的服務,並從紅色變味了綠色,那麼恭喜您,您的第一個服務就向註冊中心註冊成功了!

    點選Order服務,您會發現有3個服務例項,這就是您剛剛用容器啟動的三個服務例項了。

                                                                                       

同理,再次部署Product服務,注意,Product的服務埠要改為別的埠,比如9060,不能跟Order重複了!

docker命令如下:

 

docker build -t productapi .

 

docker run -d -p 9060:5000 --name product1 productapi --ConsulSetting:ServicePort="9060"
docker run -d -p 9061:5000 --name product2 productapi --ConsulSetting:ServicePort="9061"
docker run -d -p 9062:5000 --name product3 productapi --ConsulSetting:ServicePort="9062"

 

6. 至此,我們所需要的2個服務,6個例項已經都部署好了,那麼我們在請求介面時,就會有6個請求地址:

http://ip:9050/order

http://ip:9051/order

http://ip:9052/order

http://ip:9060/product

http://ip:9061/product

http://ip:9062/product

那麼我們mvc專案在請求介面的時候不可能挨個請求啊,這也不太現實,這時候,我麼則需要一個閘道器Ocelot。有了Ocelot,我們只需要請求閘道器地址就可以了,閘道器就會根據我們請求的地址,自動匹配下游的服務。

比如,閘道器地址為http://ip:5000,那麼當我們請求http://ip:5000/oder的時候,閘道器就會匹配路由,自動請求http://ip:9050/order、http://ip:9051/order、http://ip:9052/order的任意一個地址。

當然,這幾個地址是通過Ocelot與Consul的整合實現的,Ocelot會自動獲取Consul中已經存在的服務的地址,供路由進行自動匹配。

下面,我們來搭建閘道器。

 

三、搭建閘道器

1. 新建空的Web專案,如圖:

 

 

 2. 新增Ocelot和Consul的類庫,如圖

 

 

 3. 新增程式碼

  (1)startup類的程式碼

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Consul;
using Ocelot.Provider.Polly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Ocelot.APIGateway
{
    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.AddOcelot()
                    .AddConsul()//整合Consul服務發現
                    .AddPolly();//新增超時/熔斷服務

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseOcelot().Wait();
        }
    }
}

  (2)新增ocelot.json配置檔案,程式碼如下

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/order",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/order",
      "UpstreamHttpMethod": [ "GET", "POST" ],
      "ServiceName": "order", //Consul服務名稱
      "RateLimitOptions": { //限流
        "ClientWhitelist": [ "SuperClient" ], //白名單,不受限
        "EnableRateLimiting": true,
        "Period": "5s", //1s,5m,1h,1d等
        "PeriodTimespan": 2,
        "Limit": 1
      },
      "QoSOptions": { //超時/熔斷配置
        "ExceptionsAllowedBeforeBreaking": 3, //代表發生錯誤的次數
        "DurationOfBreak": 10000, //代表熔斷時間
        "TimeoutValue": 5000 //代表超時時間
      }
    },
    {
      "DownstreamPathTemplate": "/product",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/product",
      "UpstreamHttpMethod": [ "GET", "POST" ],
      "ServiceName": "product", //Consul服務名稱
      "RateLimitOptions": { //限流
        "ClientWhitelist": [ "SuperClient" ], //白名單,不受限
        "EnableRateLimiting": true,
        "Period": "5s", //1s,5m,1h,1d等
        "PeriodTimespan": 2,
        "Limit": 1 //最重要的就是Period,PeriodTimespan,Limit這幾個配置。
      },
      "QoSOptions": { //超時/熔斷配置
        "ExceptionsAllowedBeforeBreaking": 3, //代表發生錯誤的次數
        "DurationOfBreak": 10000, //代表熔斷時間
        "TimeoutValue": 5000 //代表超時時間
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://192.168.183.129:5000", //用來訪問服務的地址
    "ServiceDiscoveryProvider": {//Consul服務發現,用來獲取Consul的服務地址的
      "Scheme": "http",
      "Host": "192.168.183.129",//Consul的IP地址
      "Port": 8500,//
      "Type": "Consul"
    },
    "RateLimitOptions": { //限流
      "DisableRateLimitHeaders": false, //代表是否禁用X-Rate-Limit和Retry-After標頭(請求達到上限時response header中的限制數和多少秒後能重試)
      "QuotaExceededMessage": "too many requests...", //代表請求達到上限時返回給客戶端的訊息
      "HttpStatusCode": 999, //代表請求達到上限時返回給客戶端的HTTP狀態程式碼
      "ClientIdHeader": "Test" //可以允許自定義用於標識客戶端的標頭。預設情況下為“ ClientId”
    }
  }
}

  (3)Startup.cs類程式碼

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Consul;
using Ocelot.Provider.Polly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Ocelot.APIGateway
{
    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.AddOcelot()
                    .AddConsul()//整合Consul服務發現
                    .AddPolly();//新增超時/熔斷服務

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseOcelot().Wait();
        }
    }
}

  (4)Program.cs類程式碼如下

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Ocelot.APIGateway
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((hostingContext, config) => {
                    config.AddJsonFile("ocelot.json");
                })  
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

  (5)啟動閘道器專案,進行測試,結果如下

 

 

 

 

 

 至此,閘道器搭建完成,之後,我們的MVC專案只需要訪問閘道器的地址進行介面呼叫就可以了。

 

四、新建MVC專案,進行呼叫模擬

1. 新建空MVC專案並新增程式碼,如圖

 

 

 2. 新增程式碼

  (1)HomeController程式碼

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Web.MVC.Helper;

namespace Web.MVC.Controllers
{
    public class HomeController:Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IServiceHelper _serviceHelper;

        public HomeController(ILogger<HomeController> logger, IServiceHelper serviceHelper)
        {
            _logger = logger;
            _serviceHelper = serviceHelper;
        }

        public async Task<IActionResult> Index()
        {
            ViewBag.OrderData = await _serviceHelper.GetOrder();
            ViewBag.ProductData = await _serviceHelper.GetProduct();
            return View();
        }
    }
}

  (2)IServiceHelper、ServiceHelper程式碼

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Web.MVC.Helper
{
    public interface IServiceHelper
    {
        /// <summary>
        /// 獲取產品資料
        /// </summary>
        /// <returns></returns>
        Task<string> GetProduct();

        /// <summary>
        /// 獲取訂單資料
        /// </summary>
        /// <returns></returns>
        Task<string> GetOrder();
    }
}
using Consul;
using Microsoft.Extensions.Configuration;
using RestSharp;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Web.MVC.Helper
{
    public class ServiceHelper : IServiceHelper
    {
        public async Task<string> GetOrder()
        {
            var Client = new RestClient("http://ip:5000");//此處指的是閘道器專案的地址
            var request = new RestRequest("/order", Method.GET);
            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }

        public async Task<string> GetProduct()
        {
            var Client = new RestClient("http://ip:5000");//此處指的是閘道器的地址
            var request = new RestRequest("/product", Method.GET);
            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }
    }
}

  (3)Home/Index.cshtml的程式碼

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

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>
        @ViewBag.OrderData
    </p>
    <p>
        @ViewBag.ProductData
    </p>
</div>

  (4)Startup.cs中在ConfigureServices中,新增程式碼

services.AddSingleton<IServiceHelper, ServiceHelper>();

  (5)設定應用的訪問地址

  

 

 3. 啟動專案,檢視結果

 

 訪問成功,至此,我們就把一個簡單版的微服務搭建起來了!

 

看起來步驟繁瑣,其實只是本人記錄的比較詳細而已,實操完成後並沒有那麼多東西!下圖為本專案的基本架構:

 

 

 好了,今天就到這裡,下班!

 

參考連結:.Net Core微服務入門全紀錄

相關文章