【.net core】電商平臺升級之微服務架構應用實戰(core-grpc)

1、前言

這篇文章原本是繼續分享IdentityServer4 的相關文章,因爲以前有博友問我關於微服務相關的問題,我就先跳過IdentityServer4的分享,進行微服務相關的技術學習和分享。微服務在個人分享目錄裏面是放到四月份開始系列文章分享的,這裏就先穿越下,提早安排微服務應用的開篇文章 電商系統升級之微服務架構的應用
本博客以及公衆號堅持以架構的思惟來分享技術,不只僅是單純的分享怎麼使用的Demohtml

2、場景

先來回顧下我上篇文章 Asp.Net Core 中IdentityServer4 受權中心之應用實戰 中,電商架構由單體式架構拆分升級到多網關架構git

升級以前

升級以後:

然而升級以後問題又來了,因爲以前增長了代理商業務而且把受權中心支付網關單獨拆出來了,這使得公司的業務訂單量翻了幾十倍,這個時候整個電商系統達到了瓶頸,若是再不找解決方案系統又得宕機了。github

2.1 問題及解決方案

通過技術的調研及問題分析,致使這個瓶頸的問題主要有如下幾個緣由,只須要把下面問題解決就能夠獲得很大的性能提高算法

  • 天天的訂單量暴增,致使訂單數據太大,然而整個電商系統數據存儲在一個數據庫中,而且是單表單數據庫(未進行讀寫分離),以至於訂單數據持續暴增。
  • 相關業務須要依賴訂單查詢,訂單數據查詢慢以致於拖垮數據庫
  • 整個電商系統鏈接數達到瓶頸(已經分佈式部署,在多加服務器會損耗更多的經費而達不到最佳性價比)

爲了一勞永逸的解決以上問題,通過技術的調研,決定對訂單業務作以下升級改造:數據庫

  • 拆分獨立的訂單微服務(本章節着重分享)
  • 使用ES進行數據遷移(按年進行劃分,而且進行讀寫分離,這裏就不着重講,下次來跟你們一塊兒學習和分享)
  • 增長分佈式緩存 (也不是本次的重點,後續再來跟你們學習和分享)

通過升級後的架構圖以下:
編程

架構圖說明:json

  • 右邊同一顏色的表明仍是原先電商系統的單體式架構,爲拆分的單體架構業務,其中在業務處理上夾雜了一層分佈式緩存的處理
  • 左邊的是微服務的架構,是此次升級拆分後的架構,其中數據庫也已經從原有的數據庫拆分而且數據遷移到了ES集羣中,並進行了讀寫分離。
  • 訂單服務能夠隨意擴容成分佈式服務,經過一些工具動態擴展服務及服務器的支持。
  • 右邊的業務後續也能夠進行拆分,拆分紅不一樣的業務服務。
  • 後續升級還能夠考慮消息隊列等相關方面,架構圖中未構思(後續再來分享升級用到的相關技術,這裏仍是迴歸到本文的核心微服務

3、微服務概述

微服務的相關概念我就很少說了,如下就先簡單概況下微服務帶來的利和弊。api

3.1 微服務的優點

  • 使大型的複雜應用程序能夠持續交付和持續部署:持續交付和持續部署是DevOps的一部分,DevOps是一套快速、頻繁、可靠的軟件交付實踐。高效的DevOps組織一般將軟件部署到生產環境時面臨更少的問題和故障。DevOps工具備DockerKubernetsJenkinsGit等。
  • 每一個服務相對較小並容易維護:微服務架構相比單體應用要小的多,開發者理解服務中的邏輯代碼更容易。代碼庫小,打包,啓動服務速度也快。
  • 服務能夠獨立部署:每一個服務均可以獨立於其餘服務進行部署
  • 服務能夠獨立擴展:服務能夠獨立擴展,不管是採用X軸擴展的實例克隆,仍是Z軸的流量分區方式。此外每一個服務均可以部署到適合它們需求的硬件之上
  • 微服務架構能夠實現團隊的自治:能夠根據服務來把開發團隊拆分。每一個團隊都有本身負責的微服務,而不用關心不屬於他們負責的服務。
  • 更容易實驗和採納新的技術:最後,微服務能夠消除對某個技術棧的長期依賴。由於服務更小,使用更換的編程語言和技術來重寫一項服務變得有可能,這也意味着,對一項新技術嘗試失敗後,能夠直接丟棄這部分工做而不至於給整個應用帶來失敗的風險。
  • 更好的容錯性:微服務架構也能夠實現更換的故障隔離。例如,某個服務引起的致命錯誤,不會影響其餘服務。其餘服務仍然正常運行。
  • 服務能夠獨立擴容:對於整個架構來講,能夠隨意選擇相關業務進行擴容和負載,經過相關技術工具動態進行隨意擴容

3.2 微服務的劣勢

  • 服務拆分和定義是一項挑戰:採用微服務架構首當其衝的問題,就是根本沒有一個具體的、良好定義的算法能夠完成服務的拆分工做。與軟件開發同樣,服務的拆分和定義更像一門藝術。更糟糕的是,若是對系統的服務拆分出現了誤差,頗有可能會構建出一個分佈式的單體應用;一個包含了一大堆互相之間緊耦合的服務,卻又必須部署在一塊兒的所謂分佈式系統。這將會把單體架構和微服務架構二者的弊端集於一身。
  • 分佈式系統帶來的各類複雜性、使開發、測試和部署變得更困難:使用微服務架構的另外一個問題是開發人員必須處理建立分佈式系統的額外複雜性。服務必須是進程間通訊。這比簡單的方法調用要複雜的多。
  • 當部署跨越多個服務的功能時須要謹慎地協調更多的開發團隊:使用微服務架構的另一項挑戰在於當部署跨越多個服務的功能時須要謹慎地協調更多開發團隊。必須制定一個發佈計劃,把服務按照依賴關係進行排序。這跟單體架構下部署多個組件的方式大相徑庭。
  • 開發者須要思考到底應該在應用的什麼階段使用微服務架構:使用微服務架構的另外一個問題是決定在應用程序生命週期的哪一個階段開始使用這種架構。
  • 跨服務數據的問題:在單體應用中,全部的數據都在一個數據庫中,而在微服務架構中,每一個服務都有本身的數據庫,想要獲取,操做其餘服務的數據,只能經過該服務提供API進行調用,這樣就帶來一個問題,進程通訊的問題,若是涉及到事務,那麼還須要使用Saga來管理事務,增長了開發的難度。

3.3 微服務拆分原則

說到單體架構拆分,那也不是隨意拆分,是要有必定的原則,拆分的好是優點,拆分的很差是混亂。如下是我查閱資料以及個人經驗總結出來的拆分原則緩存

  • 一、單一職責、高內聚低耦合
  • 二、微服務粒度適中
  • 三、考慮團隊結構
  • 四、以業務模型切入
  • 五、演進式拆分
  • 六、避免環形依賴與雙向依賴
  • 七、DDD(能夠考慮使用領域驅動設計去進行底層服務的設計,後續會單獨分析該設計的相關文章)

4、微服務實戰

好了,到這裏你們已經對微服務有了必定的理解,就不繼續詳細概述相關理念的東西,下面來直接擼代碼,讓你們熟悉微服務的應用。這裏我使用 莫堇蕈 在github 上開源的微服務框架,框架源代碼地址 :https://github.com/overtly/core-grpc我這裏強烈推薦該框架,目前已經比較成熟的用於公司生產環境服務器

爲了更好的維護開源項目以及技術交流,特地建立了一個交流羣,羣號:1083147206 有興趣者開源加入交流

4.1 core-grpc 微服務框架的優點:

  • 集成Consul 實現服務發現和註冊以及健康檢查等機制
  • 實時監聽服務狀態
  • 多節點 輪詢機制
  • 故障轉移,拉入黑名單
  • 支持.Net Core 和Framework 兩種框架
  • 實現基於Grpc的微服務
  • 部署支持環境變量

4.2 實戰

建立Jlion.NetCore.OrderService 訂單微服務

咱們用vs2019 建立控制檯應用程序 選擇框架.Net Core 3.1 命名爲Jlion.NetCore.OrderService 後面簡稱訂單服務,建立完後咱們經過nuget包引入 core-grpc微服務框架,以下圖:

目前core-grpc微服務框架,最新正式發佈版本是 1.0.3

引用了core-grpc 後咱們還須要安裝一個工具VS RPC Menu,這個工具也是大神免費提供的,圖片以下:

因爲微軟官方下載比較慢,我這裏共享到 百度網盤,百度網盤下載地址以下:

連接: https://pan.baidu.com/s/1twpmA4_aErrsg-m0ICmOPw 提取碼: cshs

若是經過下載後安裝不是vs 集成安裝方式,下載完成後須要關閉vs 2019相關才能正常安裝。

VS RPC Menu 工具說明以下:

OrderRequest.proto代碼以下:

syntax = "proto3";
package Jlion.NetCore.OrderService.Service.Grpc;


//定義訂單查找參數實體
message OrderSearchRequest{
    string OrderId = 1; //定義訂單ID
    string Name = 2;
}

//定義訂單實體
message OrderRepsonse{
    string OrderId = 1;
    string Name = 2;
    double Amount = 3;
    int32 Count = 4;
    string Time = 5;
}

//定義訂單查找列表
message OrderSearchResponse{
    bool Success = 1;
    string ErrorMsg = 2;
    repeated OrderRepsonse Data = 3;
}

上面主要是定義了幾個消息實體,
咱們再建立JlionOrderService.proto,代碼以下:

syntax = "proto3";
package Jlion.NetCore.OrderService.Service.Grpc;

import "OrderRequest.proto";

service JlionOrderService{
    rpc Order_Search(OrderSearchRequest) returns (OrderSearchResponse){} 
}

上面的代碼中均可以看到最上面有 package Jlion.NetCore.OrderService.Service.Grpc 代碼,這是聲明包名也就是後面生成代碼後的命名空間,這個很重要
同時定義了JlionOrderService服務入口,而且定義了一個訂單搜索的方法Order_Search,到這裏咱們已經完成了一小部分了。

生成客戶端代碼

再在JlionOrderService.proto文件裏面右鍵 》選擇Grpc代碼生成》Grpc 代碼 會自動生存微服務客戶端代碼 。
生存工具中具備以下功能:

  • 生存Grpc客戶端代碼
  • Grpc 編譯(不經常使用)
  • Grpc 打包(經常使用,用來把客戶端dll發佈到nuget服務器上)
  • 還能夠對Thrift 代碼進行生成和打包

建立Jlion.NetCore.OrderService.Grpc 類庫

把剛剛經過工具生成的Grpc客戶端代碼直接copy到 Jlion.NetCore.OrderService.Grpc這個類庫中(必須和上面Grpc 的代碼聲明的package 一致)如下簡稱訂單服務客戶端,而且須要經過Nuget包添加Overt.Core.Grpc 的依賴,代碼結構以下:

Jlion.NetCore.OrderService.Grpc類庫已經構建完成,如今讓 Jlion.NetCore.OrderService 服務引用Jlion.NetCore.OrderService.Grpc 類庫

訂單服務中 實現本身的IHostedService

建立HostService類,繼承IHostedService代碼以下:

public class HostedService : IHostedService
{
    readonly ILogger _logger;
    readonly JlionOrderServiceBase _grpcServImpl;
    public HostedService(
        ILogger<HostedService> logger,
        JlionOrderServiceBase grpcService)
    {
        _logger = logger;
        _grpcServImpl = grpcService;
    }

    //服務的啓動機相關配置
    public Task StartAsync(CancellationToken cancellationToken)
    {
        return Task.Factory.StartNew(() =>
        {
            var channelOptions = new List<ChannelOption>()
            {
                 new ChannelOption(ChannelOptions.MaxReceiveMessageLength, int.MaxValue),
                 new ChannelOption(ChannelOptions.MaxSendMessageLength, int.MaxValue),
            };
            GrpcServiceManager.Start(BindService(_grpcServImpl), channelOptions: channelOptions, whenException: (ex) =>
            {
                _logger.LogError(ex, $"{typeof(HostedService).Namespace.Replace(".", "")}開啓失敗");
                throw ex;
            });
            System.Console.WriteLine("服務已經啓動");
            _logger.LogInformation($"{nameof(Jlion.NetCore.OrderService.Service).Replace(".", "")}開啓成功");
        }, cancellationToken);
    }

    //服務的中止
    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.Factory.StartNew(() =>
        {
            GrpcServiceManager.Stop();

            _logger.LogInformation($"{typeof(HostedService).Namespace.Replace(".", "")}中止成功");
        }, cancellationToken);
    }
 }

上面代碼主要是建立宿主機而且實現了StartAsync 服務啓動及StopAsync 服務中止方法。
咱們建立完HostedServicce代碼再來建立以前定義的Grpc服務的方法實現類JlionOrderServiceImpl,代碼以下:

public partial class JlionOrderServiceImpl : JlionOrderServiceBase
{
    private readonly ILogger _logger;
    private readonly IServiceProvider _serviceProvider;

    public JlionOrderServiceImpl(ILogger<JlionOrderServiceImpl> logger, IServiceProvider provider)
    {
        _logger = logger;
        _serviceProvider = provider;
    }

    public override async Task<OrderSearchResponse> Order_Search(OrderSearchRequest request, ServerCallContext context)
    {
        //TODO 從底層ES中查找訂單數據,
        //能夠設計成DDD 方式來進行ES的操做,這裏我就爲了演示直接硬編碼了

        var response = new OrderSearchResponse();
        try
        {
            response.Data.Add(new OrderRepsonse()
            {
                Amount = 100.00,
                Count = 10,
                Name = "訂單名稱測試",
                OrderId = DateTime.Now.ToString("yyyyMMddHHmmss"),
                Time = DateTime.Now.ToString()
            });

            response.Data.Add(new OrderRepsonse()
            {
                Amount = 200.00,
                Count = 10,
                Name = "訂單名稱測試2",
                OrderId = DateTime.Now.ToString("yyyyMMddHHmmss"),
                Time = DateTime.Now.ToString()
            });

            response.Data.Add(new OrderRepsonse()
            {
                Amount = 300.00,
                Count = 10,
                Name = "訂單名稱測試2",
                OrderId = DateTime.Now.ToString("yyyyMMddHHmmss"),
                Time = DateTime.Now.ToString()
            });
            response.Success = true;
        }
        catch (Exception ex)
        {
            response.ErrorMsg = ex.Message;
            _logger.LogWarning("異常");
        }
        return response;
    }
 }

再修改Program代碼,並把HostedServiceJlionOrderServiceImpl 注入到容器中,代碼以下:

class Program
 {
    static void Main(string[] args)
    {
        var host = new HostBuilder()
           .UseConsoleLifetime() //使用控制檯生命週期
           .ConfigureAppConfiguration((context, configuration) =>
           {
               configuration
               .AddJsonFile("appsettings.json", optional: true)
               .AddEnvironmentVariables();
           })
           .ConfigureLogging(logger =>
           {
               logger.AddFilter("Microsoft", LogLevel.Critical)
                     .AddFilter("System", LogLevel.Critical);
           })
           .ConfigureServices(ConfigureServices)
           .Build();

        AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
        {
            var logFactory = host.Services.GetService<ILoggerFactory>();
            var logger = logFactory.CreateLogger<Program>();
            logger.LogError(e.ExceptionObject as Exception, $"UnhandledException");
        };

        host.Run();
    }

    /// <summary>
    /// 通用DI注入
    /// </summary>
    /// <param name="context"></param>
    /// <param name="services"></param>
    private static void ConfigureServices(HostBuilderContext context, IServiceCollection services)
    {
        //HostedService 單例注入到DI 中
        services.AddSingleton<IHostedService, HostedService>();
        services.AddTransient<JlionOrderServiceBase, JlionOrderServiceImpl>();
    }
 }

到了這裏簡單的微服務已經編碼完成,可是還缺乏兩個配置文件,咱們建立appsettings.json配置文件和consulsettings.json 服務註冊發現的配置文件
consulsettings.json配置文件以下:

{
  "ConsulServer": {
    "Service": {
      "Address": "127.0.0.1:8500"// 你的Consul 服務註冊及發現配置地址
    }
  }
}

上面的地址配置只是簡單的例子,我這裏假定個人Consul服務地址是 127.0.0.1:8500 等下服務啓動是會經過這個地址進行註冊。

appsettings.json配置文件以下:

{
  "GrpcServer": {
    "Service": {
      "Name": "JlionOrderService",
      "Port": 10001,
      "HostEnv": "serviceaddress",
      "Consul": {
        "Path": "dllconfigs/consulsettings.json"
      }
    }
  }
}

我這裏服務監聽了10001 端口,後面註冊到Consul中也會看到該端口
官方完整的配置文件以下:

{
  "GrpcServer": {
    "Service": {
      "Name": "OvertGrpcServiceApp",                    // 服務名稱使用服務名稱去除點:OvertGrpcServiceApp
      "Host": "service.g.lan",                          // 專用註冊的域名 (可選)格式:ip[:port=default]
      "HostEnv": "serviceaddress",                      // 獲取註冊地址的環境變量名字(可選,優先)環境變量值格式:ip[:port=default]
      "Port": 10001,                                    // 端口自定義
      "Consul": {
        "Path": "dllconfigs/consulsettings.json"        // Consul路徑
      }
    }
  }
}

好了,訂單服務已經所有完成了,訂單服務服務總體結構圖以下:

好了,咱們這裏經過命令行啓動下JlionOrderService服務,生產環境大家能夠搭建在Docker 容器裏面

咱們能夠來看下我以前搭建好的Consul服務 ,打開管理界面,如圖:

圖片中能夠發現剛剛啓動的服務已經註冊進去了,可是裏面有一個健康檢查未經過,主要是因爲服務端不能訪問我本地的訂單服務,全部健康檢查不能經過。你能夠在你本地搭建 Consul服務用於測試。

我本地再來開啓一個服務,配置中的的端口號由10001 改爲10002,再查看下Consul的管理界面,以下圖:

發現已經註冊了兩個服務,端口號分別是10001 和10002,這樣能夠經過自定化工具自動添加服務及下架服務,分佈式服務也即完成。
到這裏訂單服務的啓動已經徹底成功了,咱們接下來是須要客戶端也就是上面架構圖中的電商業務網關或者支付網關等等要跟訂單服務進行通信了。

建立訂單網關(跟訂單服務進行通訊)

建立訂單網關以前我先把上面的 訂單服務客戶端 類庫發佈到個人nuget包上,這裏就不演示了。我發佈的測試包名稱JlionOrderServiceDemo nuget官方能夠搜索找到。大家也能夠直接搜索添加到大家的Demo中進行測試。
我經過VS 2019 建立Asp.Net Core 3.1 框架的WebApi 取名爲Jlion.NetCore.OrderApiService 下面簡稱訂單網關服務
如今我把前面發佈的微服務客戶端依賴包 JlionOrderServiceDemo 添加到訂單網關服務中,以下圖:

如今在訂單網關服務中添加OrderController api控制器,代碼以下:

namespace Jlion.NetCore.OrderApiService.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class OrderController : ControllerBase
    {
        private readonly IGrpcClient<OrderService.Service.Grpc.JlionOrderService.JlionOrderServiceClient> _orderService;
        public OrderController (IGrpcClient<OrderService.Service.Grpc.JlionOrderService.JlionOrderServiceClient> orderService)
        {
            _orderService = orderService;
        }
     
        [HttpGet("getlist")]
        public async Task<List<OrderRepsonse>> GetList()
        {
            var respData =await _orderService.Client.Order_SearchAsync(new OrderService.Service.Grpc.OrderSearchRequest()
            {
                Name = "test",
                OrderId = "",
            });

            if ((respData?.Data?.Count ?? 0) <= 0)
            {
                return new List<OrderRepsonse>();
            }

            return respData.Data.ToList();
        }
    }
}

代碼中經過構造函數注入 OrderService 而且提供了一個GetList的接口方法。接下來咱們還須要把OrderService.Service.Grpc.JlionOrderService注入到容器中,代碼以下:

public void ConfigureServices(IServiceCollection services)
{
     services.AddControllers();

     //註冊Grpc 客戶端,具體能夠查看源代碼
     services.AddGrpcClient();
}

如今整個訂單網關服務項目結構以下圖:

項目中有兩個最重要的配置dllconfig//Jlion.NetCore.OrderService.Grpc.dll.jsonconsulsettings.json 他們分別是幹什麼的呢?咱們先分別來看我本地這兩個配置的內容
Jlion.NetCore.OrderService.Grpc.dll.json 配置以下:

{
   "GrpcClient": {
       "Service": {
        "Name": "JlionOrderService",  // 服務名稱與服務端保持一致
        "MaxRetry": 0,                // 最大可重試次數,默認不重試
        "Discovery": {
            "Consul": {              // Consul集羣,集羣優先原則
                "Path": "dllconfigs/consulsettings.json"
            },
            "EndPoints": [           // 單點模式
                {
                    "Host": "127.0.0.1",
                    "Port": 10001
             }]
            }
        }
    }
}

Jlion.NetCore.OrderService.Grpc.dll.json 配置主要是告訴訂單網關服務訂單服務應該怎樣進行通訊,以及通訊當中的一些參數配置。我爲了測試,本地使用單點模式,不使用Consul模式
consulsettings.json 配置以下:

{
  "ConsulServer": {
    "Service": {
      "Address": "127.0.0.1:8500"
    }
  }
}

有沒有發現這個配置和以前服務端的配置同樣,主要是告訴訂單網關服務(客戶端調用者)和訂單服務服務端服務發現的集羣地址,若是上面的配置是單點模式則這個配置不會起做用。

到這裏訂單網關服務 (客戶調用端)編碼完成,咱們開始啓動它:

我這裏固定5003端口,如今完美的啓動了,咱們訪問下訂單接口,看下是否成功。訪問結果以下圖:

微服務完美的運行成功。

上面的構建微服務仍是比較麻煩,官方提供了比較快速構建你須要的微服務方式,不須要寫上面的那些代碼,那些代碼所有經過模板的方式進行構建你的微服務,有須要學習的能夠到點擊
微服務項目構建模板使用教程
教程地址:http://www.javashuo.com/article/p-nvoqxwey-cg.html

文章中的Demo 代碼已經提交到github 上,代碼地址:https://github.com/a312586670/IdentityServerDemo
微服務框架開源項目地址:https://github.com/overtly/core-grpc

相關文章
相關標籤/搜索