【.net core】电商平台升级之微服务架构应用实战(core-grpc)

一、前言

这篇文章本来是继续分享IdentityServer4 的相关文章,由于之前有博友问我关于微服务相关的问题,我就先跳过IdentityServer4的分享,进行微服务相关的技术学习和分享。微服务在我的分享目录里面是放到四月份开始系列文章分享的,这里就先穿越下,提前安排微服务应用的开篇文章 电商系统升级之微服务架构的应用
本博客以及公众号坚持以架构的思维来分享技术,不仅仅是单纯的分享怎么使用的Demo

二、场景

先来回顾下我上篇文章 Asp.Net Core 中IdentityServer4 授权中心之应用实战 中,电商架构由单体式架构拆分升级到多网关架构

升级之前

升级之后:

然而升级之后问题又来了,由于之前增加了代理商业务并且把授权中心支付网关单独拆出来了,这使得公司的业务订单量翻了几十倍,这个时候整个电商系统达到了瓶颈,如果再不找解决方案系统又得宕机了。

2.1 问题及解决方案

经过技术的调研及问题分析,导致这个瓶颈的问题主要有以下几个原因,只需要把下面问题解决就可以得到很大的性能提升

  • 每天的订单量暴增,导致订单数据太大,然而整个电商系统数据存储在一个数据库中,并且是单表单数据库(未进行读写分离),以致于订单数据持续暴增。
  • 相关业务需要依赖订单查询,订单数据查询慢以至于拖垮数据库
  • 整个电商系统连接数达到瓶颈(已经分布式部署,在多加服务器会损耗更多的经费而达不到最佳性价比)

为了一劳永逸的解决以上问题,经过技术的调研,决定对订单业务做如下升级改造:

  • 拆分独立的订单微服务(本章节着重分享)
  • 使用ES进行数据迁移(按年进行划分,并且进行读写分离,这里就不着重讲,下次来跟大家一起学习和分享)
  • 增加分布式缓存 (也不是本次的重点,后续再来跟大家学习和分享)

经过升级后的架构图如下:

架构图说明:

  • 右边同一颜色的代表还是原先电商系统的单体式架构,为拆分的单体架构业务,其中在业务处理上夹杂了一层分布式缓存的处理
  • 左边的是微服务的架构,是这次升级拆分后的架构,其中数据库也已经从原有的数据库拆分并且数据迁移到了ES集群中,并进行了读写分离。
  • 订单服务可以随意扩容成分布式服务,通过一些工具动态扩展服务及服务器的支持。
  • 右边的业务后续也可以进行拆分,拆分成不同的业务服务。
  • 后续升级还可以考虑消息队列等相关方面,架构图中未构思(后续再来分享升级用到的相关技术,这里还是回归到本文的核心微服务

三、微服务概述

微服务的相关概念我就不多说了,以下就先简单概况下微服务带来的利和弊。

3.1 微服务的优势

  • 使大型的复杂应用程序可以持续交付和持续部署:持续交付和持续部署是DevOps的一部分,DevOps是一套快速、频繁、可靠的软件交付实践。高效的DevOps组织通常将软件部署到生产环境时面临更少的问题和故障。DevOps工具有DockerKubernetsJenkinsGit等。
  • 每个服务相对较小并容易维护:微服务架构相比单体应用要小的多,开发者理解服务中的逻辑代码更容易。代码库小,打包,启动服务速度也快。
  • 服务可以独立部署:每个服务都可以独立于其他服务进行部署
  • 服务可以独立扩展:服务可以独立扩展,不论是采用X轴扩展的实例克隆,还是Z轴的流量分区方式。此外每个服务都可以部署到适合它们需求的硬件之上
  • 微服务架构可以实现团队的自治:可以根据服务来把开发团队拆分。每个团队都有自己负责的微服务,而不用关心不属于他们负责的服务。
  • 更容易实验和采纳新的技术:最后,微服务可以消除对某个技术栈的长期依赖。因为服务更小,使用更换的编程语言和技术来重写一项服务变得有可能,这也意味着,对一项新技术尝试失败后,可以直接丢弃这部分工作而不至于给整个应用带来失败的风险。
  • 更好的容错性:微服务架构也可以实现更换的故障隔离。例如,某个服务引发的致命错误,不会影响其他服务。其他服务仍然正常运行。
  • 服务可以独立扩容:对于整个架构来说,可以随意选择相关业务进行扩容和负载,通过相关技术工具动态进行随意扩容

3.2 微服务的劣势

  • 服务拆分和定义是一项挑战:采用微服务架构首当其冲的问题,就是根本没有一个具体的、良好定义的算法可以完成服务的拆分工作。与软件开发一样,服务的拆分和定义更像一门艺术。更糟糕的是,如果对系统的服务拆分出现了偏差,很有可能会构建出一个分布式的单体应用;一个包含了一大堆互相之间紧耦合的服务,却又必须部署在一起的所谓分布式系统。这将会把单体架构和微服务架构两者的弊端集于一身。
  • 分布式系统带来的各种复杂性、使开发、测试和部署变得更困难:使用微服务架构的另一个问题是开发人员必须处理创建分布式系统的额外复杂性。服务必须是进程间通信。这比简单的方法调用要复杂的多。
  • 当部署跨越多个服务的功能时需要谨慎地协调更多的开发团队:使用微服务架构的另外一项挑战在于当部署跨越多个服务的功能时需要谨慎地协调更多开发团队。必须制定一个发布计划,把服务按照依赖关系进行排序。这跟单体架构下部署多个组件的方式截然不同。
  • 开发者需要思考到底应该在应用的什么阶段使用微服务架构:使用微服务架构的另一个问题是决定在应用程序生命周期的哪个阶段开始使用这种架构。
  • 跨服务数据的问题:在单体应用中,所有的数据都在一个数据库中,而在微服务架构中,每个服务都有自己的数据库,想要获取,操作其他服务的数据,只能通过该服务提供API进行调用,这样就带来一个问题,进程通信的问题,如果涉及到事务,那么还需要使用Saga来管理事务,增加了开发的难度。

3.3 微服务拆分原则

说到单体架构拆分,那也不是随意拆分,是要有一定的原则,拆分的好是优势,拆分的不好是混乱。以下是我查阅资料以及我的经验总结出来的拆分原则

  • 1、单一职责、高内聚低耦合
  • 2、微服务粒度适中
  • 3、考虑团队结构
  • 4、以业务模型切入
  • 5、演进式拆分
  • 6、避免环形依赖与双向依赖
  • 7、DDD(可以考虑使用领域驱动设计去进行底层服务的设计,后续会单独分析该设计的相关文章)

四、微服务实战

好了,到这里大家已经对微服务有了一定的理解,就不继续详细概述相关理念的东西,下面来直接撸代码,让大家熟悉微服务的应用。这里我使用 莫堇蕈 在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端口,现在完美的启动了,我们访问下订单接口,看下是否成功。访问结果如下图:

微服务完美的运行成功。

上面的构建微服务还是比较麻烦,官方提供了比较快速构建你需要的微服务方式,不需要写上面的那些代码,那些代码全部通过模板的方式进行构建你的微服务,有需要学习的可以到点击
微服务项目构建模板使用教程
教程地址:https://www.cnblogs.com/jlion/p/12494525.html

文章中的Demo 代码已经提交到github 上,代码地址:https://github.com/a312586670/NetCoreDemo
微服务框架开源项目地址:https://github.com/overtly/core-grpc

posted @ 2020-03-16 07:40  Jlion  阅读(6422)  评论(25编辑  收藏