简单轻量的微服务架构

微服务 打算写两篇文章,今天主要介绍微服务的请求流程以及流程处理过程中涉及到的代码,第二篇结合简单业务逻辑,详细介绍代码实现以及数据一致性的问题。微服务这个名词,我在2014年就接触了,当时是在一家医疗公司。这里简单介绍下医疗行业的技术背景,医疗行业我所接触到的,技术更新还是比较滞后的,尤其是HIS,当然这并不是医疗行业受制于技术,我觉得还是业务。本人工作快10年,接触过最复杂的业务系统应该当属HIS。当时是由王总牵头,准备把HIS升级到3.0,并且走服务化 & 微服务,由长沙和武汉团队部分人员协作,那时也是我第一次接触微服务这个概念和一些技术栈。继续我们的目标。
一些概念性的东西我就不写了,啥优点缺点啊啥的。个人一点见解,微服务架构能不用尽量不用,如果要用必须要满足两个条件,其一开发产品团队对微服务的技术栈有一定的沉淀,其二devops团队必须要牛逼,纯属个人的一点看法。
架构图,我们先看下简单架构图(参考微软开源框架eshop)。
一图胜千言,通过这张图,这里简单介绍下整个请求流程:
1.客户端通过浏览器或者app访问gateway网关envoy服务网格;
2.如果该请求需要聚合器聚合,envoy路由请求到agg聚合器,聚合器通过grpc访问services,返回聚合结果到client;
3.service处理请求入库,如果需要其他service协作,需要写入eventlog到mssql,并publish到mq。
4.如果该请求不需要聚合,由gateway直接路由到service。
5.idp是统一认证服务,基于ids4实现,遵循oauth2和openidconect1协议规范,通过accesstoken授权访问。
图片解说就到这吧。
技术栈,下面简单介绍下整个练手微服务涉及到的所有技术栈,1.webapicore 2.eventbus 3.envoy 4.rabbitmq 5.efcore 6.identityserver4 7.DDD领域驱动 8.docker & docker-compose 9.mediatR 10.protobuffer、CQRS等等。
解决方案,下面我们看下代码结构。
今天这篇文章主要介绍微服务的一个完整请求,代码不错详细介绍,下面简单介绍下所有工程:
EventBus:事件总线,里面有三个工程,分别为总线接口、rabbitmq实现、efcore实现;
Gateway:网关,里面包含两个工程,聚合器和服务网格,服务网格很简单就一个yaml配置文件;
Cart:购物车服务,标准的DDD分层架构;
Order:订单服务,标准的DDD分层架构;
Product:产品服务,标准的DDD分层架构。
解决方案结构就到这吧,下面我们就从入口开始一起分析下代码的实现,web端还未实现,正在实现中,用的是vue框架(前后端完全分离),对这个不熟正打算系统的学一下前端技术vue webpark等,非常怀念只要有jQuery在手,打遍天下无敌手。呵呵,感觉现在的前端跟后端完全是一个模式了,以前的前端有点打游击的感觉,现在的前端工程化了,题外话。我们从gateway网关开始吧。
envoy由c++实现,个人觉得这家伙要火有点难度,即便它站在巨人(service mesh)肩膀上。为什么?就因为它是c++实现,不是因为c++不行,还是因为现在网关产品是golang的天下,一个网关多少会牵扯中后台的一些技术平台,个人愚见,不要当真。使用envoy非常简单,就一份配置文件,随后通过docker-comose up就可启动,看代码
 1 admin:  # 后台
 2   access_log_path: "/dev/null" # log路径
 3   address:
 4     socket_address:
 5       address: 0.0.0.0  # ip
 6       port_value: 8001  # 端口
 7 static_resources:   # 静态资源,提供前端请求
 8   listeners:
 9   - address:
10       socket_address:
11         address: 0.0.0.0   # web请求地址
12         port_value: 80     # 监听80端口
13     filter_chains:  # 筛选器
14     - filters:
15       - name: envoy.http_connection_manager  
16         config:
17           codec_type: auto
18           stat_prefix: ingress_http
19           route_config:
20             name: backend_route
21             virtual_hosts:
22             - name: backend
23               domains:
24               - "*"   
25               routes:   # 路由
26               - name: "product"
27                 match:
28                   prefix: "/product/"  # 前缀匹配
29                 route:
30                   auto_host_rewrite: true
31                   prefix_rewrite: "/product-api/"
32                   cluster: product  #  服务名称 逻辑主机配置
33               - name: "agg"
34                 match:
35                   prefix: "/"
36                 route:
37                   auto_host_rewrite: true
38                   prefix_rewrite: "/"
39                   cluster: webagg
40           http_filters:
41           - name: envoy.router
42           access_log:
43           - name: envoy.file_access_log
44             filter:
45               not_health_check_filter: {}
46             config:
47               json_format:
48                 time: "%START_TIME%"
49                 protocol: "%PROTOCOL%"
50                 duration: "%DURATION%"
51                 request_method: "%REQ(:METHOD)%"
52               path: "/tmp/access.log"
53   clusters:
54   - name: webagg
55     connect_timeout: 0.25s
56     type: strict_dns
57     lb_policy: round_robin
58     hosts:
59     - socket_address:
60         address: webagg
61         port_value: 80
62   - name: product  # 对应上面cluster
63     connect_timeout: 0.25s  # 超时
64     type: strict_dns  # envoy 监听 DNS,A记录域名对应的IP地址
65     lb_policy: round_robin  # 负载均衡 轮询主机
66     hosts:
67     - socket_address:
68         address: product-api  # service域名,配置ip也可,线上环境肯定用域名,同时可以做服务发现等
69         port_value: 80   # 端口
  
代码比较长删减了一部分,以上做了相关注释,配置也比较简单,其他配置可以参考官网: https://www.servicemesher.com/envoy/ 。接下来配置docker-compose.yml.
1   webshoppingapigw:   #  service名称
2     image: envoyproxy/envoy # envoy镜像
3     volumes:   # 数据卷
4       - ./ApiGateways/Envoy/config/webagg:/etc/envoy  # 宿主和容器地址映射
5     ports:
6     - "6000:80"   # 前台
7     - "6001:8001"  # 后台
envoy配置和启动就是这么简单,下面我们就可以通过ip:port访问网关。网关介绍就到这,下面看下聚合器,题外话,网关都有聚合能力,包括netcore平台的ocelot网关等产品,但是我相信没几个人会用网关的聚合,个人感觉太弱了。
聚合器基于webapicore开发,跟后台service通信主要采用grpc(远程过程调用),其实就是一种rpc通信框架,类似thrift,底层原理就是通过本地代理实现,默认通过protobuf语言描述及做为底层通信格式。protobuf是一种轻便高效的结构化数据存储格式,个人理解二进制的性能,面向对象的效率。
聚合器工程比较简单,普通的webapicore项目。我们有这么一个操作,添加商品到购物车,相信大家都在京东、淘宝等电商买过商品吧。如果是单体应用是不是直接在购物车的application层或者api层里面直接聚合产品和购物车信息。但是在微服务架构里面,我们需要聚合器完成这个操作。看代码。
 1 public async Task<ActionResult> AddCartItemAsync([FromBody] AddCartItemRequest data)
 2         {
 3             // 其他代码...
 4             var item = await _product.GetProductItemAsync(data.ProductItemId); // 通过grpc 从product微服务 获取产品信息
 5             var currentCart = (await _cart.GetById(data.CartId)) ?? new  CartData(data.CartId);// 通过grpc从cart微服务 获取购物车信息
 6             
 7             var product = currentCart.Items.SingleOrDefault(i => i.ProductId == item.Id); // 检索当前购物车是否有该产品信息
 8             if (product != null)
 9             {
10                 product.Quantity += data.Quantity;  // 如果有数量+1
11             }
12             else
13             {    // 添加新商品到购物车
14                 currentCart.Items.Add(new CartDataItem()
15                 {
16                     ProductId = item.Id,
17                     ProductName = item.Name,
18                     Quantity = data.Quantity,
19                     Id = Guid.NewGuid().ToString()
20                 });
21             }
22             // 通过grpc更新cart微服务信息
23             await _cart.UpdateAsync(currentCart);
24             return Ok();
25         }
我们继续看看,聚合器通过grpc调用产品微服务的代码:
 1 public async Task<ProductItem> GetProductItemAsync(int id)
 2         {
 3             return await GrpcCallerService.CallService(_urls.GrpcProduct, async channel =>
 4             {
 5                                 //  其他代码...
 6                 var client = new ProcuctClient(channel);  // 通过proto描述文件生成  
 7                 var request = new ProductItemRequest { Id = id };  // proto请求消息
 8                 var response = await client.GetItemByIdAsync(request);  // 通过本地代理调用
 9                 return MapToProductItemResponse(response);
10             });
11         }

最后docker部署代码 1

   // dockerfile
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS base 2 WORKDIR /app 3 EXPOSE 80 4 5 FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build 6 WORKDIR /src 7 8 9 COPY "Gateways/Web.Aggregator/Web.Aggregator.csproj" "Gateways/Web.Aggregator/" 10 11 COPY "NuGet.config" "NuGet.config" 12 13 RUN dotnet restore "Gateways/Web.Aggregator/Web.Aggregator.csproj" 14 15 COPY . . 16 WORKDIR /src/Gateways/Web.Aggregator 17 RUN dotnet publish --no-restore -c Release -o /app 18 19 FROM build AS publish 20 21 FROM base AS final 22 WORKDIR /app 23 COPY --from=publish /app . 24 ENTRYPOINT ["dotnet", "Web.Aggregator.dll"]

docker-compose

 1   webshoppingagg:
 2     image: centos
 3     build:
 4       context: .
 5       dockerfile: Gateways/Web.Aggregator/Dockerfile
 6     depends_on: 8       - sqldata
 9       - identity-api
10       - rabbitmq
11       - order-api13       - product-api
14       - cart-api
15     ports:
16       - "7000:80"
以上就是聚合器通过grpc调用后端服务,并完成聚合,这里还有个小问题需要注意,客户端的本地代理对象,我们需要copy一份后端的proto文件。我们继续看下后台服务,产品服务。
 
ProductService 产品服务,我们继续上面grpc。先看下productService工程。
product、order、cart三个微服务工程都是基于DDD领域驱动设计,标准的4层结构(一个小插曲啊,记得以前刚接触DDD,有一天因为下午请假,比较急当时对DDD又不熟,为了赋值方便直接把domain里面的private set的private改成public,哈哈)。我们继续看下加入产品到购物车,产品微服务这边的处理,看代码
 1 // proto代码
 2 syntax = "proto3";
 3 package ProductApi;
 4 message ProductItemRequest {
 5         int32 id = 1;
 6 }
 7  
 8 // 其他成员...
 9  
10 message ProductItemResponse {
11         int32 id = 1;
12         string name = 2;
13     string description=3;
14     int32 available_stock=4;
15     ProductBrand product_brand=5;
16     ...
17 }
18 message ProductBrand {
19         int32 id = 1;
20         string name = 2;
21 }
22 service ProductGrpc {
23   rpc GetItemById (ProductItemRequest) returns (ProductItemResponse) {
24         }
25 }
26  
27  
28 c#代码
29 public class ProductGrpcService : ProductApi.ProductGrpc.ProductGrpcBase // 编译器编译proto文件生成的基类
30     {
31         // 其他成员...
32         private readonly ProductContext _productContext;
33         private readonly ProductSettings _settings;
34         private readonly ILogger _logger;
35         public ProductGrpcService(ProductContext dbContext, IOptions<ProductSettings>  settings, ILogger<ProductGrpcService> logger)
36         {
37                     // 其他代码...
38         }
39         public override async Task<ProductItemResponse> GetItemById(ProductItemRequest  request, ServerCallContext context)
40         {
41             // 其他代码...
42             var item = await _productContext.ProductItems.SingleOrDefaultAsync(ci => ci.Id  == request.Id);
43                     
44             if (item != null)
45             {
46                 return new ProductItemResponse()
47                 {
48                     Description = item.Description,
49                     Id = item.Id,
50                     Name = item.Name,
51                     Price = (double)item.Price
52                 };
53             }
54             return null;
55         }
56 }
 
以上就是grpc服务端处理聚合器grpc请求的流程。代码很简单。到此微服务流程应该走通了一半多了,接下来再介绍微服务处理的后半部分,我们还是以productService微服务为例吧。产品有这么一个操作,那就是修改产品价格,假如我们在京东买商品,商品A添加到了购物车,添加之后,商品A的价格发生了改变,这个时候是不是需要通知购物车服务,修改信息?直接看代码
 1 // controller
 2 public class ProductController : ControllerBase
 3     {
 4         // 其他成员...
 5         private readonly IMediator _mediator;
 6         private readonly ProductSettings _settings;
 7         public ProductController(IMediator mediator, IOptionsSnapshot<ProductSettings>  settings)
 8         {
 9             _mediator = mediator;
10             _settings = settings.Value;
11         }
12         public async Task<ActionResult>  UpdateProductPriceAsync([FromBody]UpdateProductCommand updateProductCommand)
13         {
14             // command验证统一处理
15             bool commandResult = await _mediator.Send(updateProductCommand);
16             if (!commandResult)
17             {
18                 return BadRequest();
19             }
20             return Ok();
21         }
22 }
23  
24 // command
25 [DataContract]
26     public class UpdateProductCommand : IRequest<bool>
27     {
28         [DataMember]
29         public int ProductId { get;private set; }
30         [DataMember]
31         public decimal NewPrice { get; private set; }
32         public UpdateProductCommand(int productId, decimal newPrice)
33         {
34             this.ProductId = productId;
35             this.NewPrice = NewPrice;
36         }
37 }
 
productService基于mediator实现CQRS读写分离,在controller里面直接就调用mediator.send方法,mediator如果不熟可以先百度一下,很简单。我们继续看下UpdateProductCommandHandler处理器的Hanlder实现。
 1 public class UpdateProductCommandHandler : IRequestHandler<UpdateProductCommand, bool>
 2     {
 3         private readonly IProductRepository _productRepository;
 4         public UpdateProductCommandHandler(IProductRepository productRepository)
 5         {
 6             _productRepository = productRepository;
 7         }
 8         public async Task<bool> Handle(UpdateProductCommand request, CancellationToken  cancellationToken)
 9         {
10             var product = await  _productRepository.FindByIdAsync(request.ProductId.ToString());
11             if (product == null)
12             {
13                 return false;
14             }
15             var oldPrice = product.Price;
16            
17             product.SetProductPrice(request.NewPrice, oldPrice);
18             return await  _productRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
19         }
20     }
代码很简单,直接通过domain模型修改价格,看代码。
      
1   public void SetProductPrice(decimal newPrice, decimal oldPrice)
2         {
3             this.Price = newPrice;
4             AddDomainEvent(new ProductPriceChangedDomainEvent(this, oldPrice));
5         }
修改价格,并跟踪domain状态。接着我们继续看 _productRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);方法的实现。
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken =  default(CancellationToken))
        {
            // 其他代码...
            await _mediator.DispatchDomainEventsAsync(this);
            
            var result = await base.SaveChangesAsync(cancellationToken);
            return true;
        }
通过mediator.Publish发布domain事件。我们继续看看具体domaineventhandler处理器。
 1 public class ProductPriceChangedDomainEventHandler :  INotificationHandler<ProductPriceChangedDomainEvent>
 2     {
 3         private readonly IProductIntegrationEventService _productIntegrationEventService;
 4         private readonly ILoggerFactory _logger;
 5         public ProductPriceChangedDomainEventHandler(IProductIntegrationEventService  productIntegrationEventService,
 6             ILoggerFactory logger)
 7         {
 8             _logger = logger;
 9             _productIntegrationEventService = productIntegrationEventService;
10         }
11         public async Task Handle(ProductPriceChangedDomainEvent notification,  CancellationToken cancellationToken)
12         {
13             // 其他代码...
14             var productPriceChangedDomainEvent = new  ProductPriceChangedIntegrationEvent(notification.ProductItem.Id,  notification.ProductItem.Price, notification.OldPrice);
15             await  _productIntegrationEventService.SaveEventAndProductContextChangesAsync(productPriceChangedDomainEvent);
16         }
17  
18 // SaveEventAndProductContextChangesAsync 保存数据库
19 public async Task SaveEventAndProductContextChangesAsync(IntegrationEvent evt)
20         {
21           
22             await ResilientTransaction.New(_productContext).ExecuteAsync(async () =>
23             {
24                 await _productContext.SaveChangesAsync();
25                 await _eventLogService.SaveEventAsync(evt,  _productContext.Database.CurrentTransaction);
26             });
27         }
创建event,并保存event和修改price价格到数据库,注意这是在一个本地事务中完成的,借助mssql事务实现。这样就完了?是不是没通知cartservice服务啊?不要急,在application层,我实现了mediatR的IPipelineBehavior<TRequest, TResponse>管道服务,我们看下代码
 1 public class TransactionBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest,  TResponse>
 2     {
 3         private readonly ILogger<TransactionBehaviour<TRequest, TResponse>> _logger;
 4         private readonly ProductContext _dbContext;
 5         private readonly IProductIntegrationEventService _productIntegrationEventService;
 6         public TransactionBehaviour(ProductContext dbContext,
 7             IProductIntegrationEventService productIntegrationEventService,
 8             ILogger<TransactionBehaviour<TRequest, TResponse>> logger)
 9         {
10                         // ....
11         }
12         public async Task<TResponse> Handle(TRequest request, CancellationToken  cancellationToken, RequestHandlerDelegate<TResponse> next)
13         {
14             var response = default(TResponse);
15             var typeName = request.GetGenericTypeName();
16             try
17             {
18                 if (_dbContext.HasActiveTransaction)
19                 {
20                     return await next();
21                 }
22                 var strategy = _dbContext.Database.CreateExecutionStrategy();
23                 await strategy.ExecuteAsync(async () =>
24                 {
25                     Guid transactionId;
26                     using (var transaction = await _dbContext.BeginTransactionAsync())
27                     using (LogContext.PushProperty("TransactionContext",  transaction.TransactionId))
28                     {
29                         
30                         response = await next();  // 执行mediatR 处理逻辑
31                         await _dbContext.CommitTransactionAsync(transaction);
32                         transactionId = transaction.TransactionId;
33                     }
34                     await  _productIntegrationEventService.PublishPublishEventsThroughEventBusAsync(transactionId); // eventbus publish事件到mq
35                 });
36                 return response;
37             }
38             catch (Exception ex)
39             {
40                 throw;
41             }
42         }
43     }
44  
45 // PublishPublishEventsThroughEventBusAsync 
46 public async Task PublishPublishEventsThroughEventBusAsync(Guid transactionId)
47         {
48             var pendingLogEvents = await  _eventLogService.RetrieveEventLogsPendingToPublishAsync(transactionId); // 从mssql获取事件数据
49             foreach (var logEvt in pendingLogEvents)
50             {
51                 try
52                 {
53                     await _eventLogService.MarkEventAsInProgressAsync(logEvt.EventId);  修改状态
54                     _eventBus.Publish(logEvt.IntegrationEvent); // 发布到mq
55                     await _eventLogService.MarkEventAsPublishedAsync(logEvt.EventId); // 修改状态
56                 }
57                 catch (Exception ex)
58                 {
59                     await _eventLogService.MarkEventAsFailedAsync(logEvt.EventId);
60                 }
61             }
62         }
在controller层里面mediatR.send方法执行完成之后,执行了IPipelineBehavior 里面的post操作,其实你可以理解为aop或者中介者,我们看下publish里面的实现。
 1 public void Publish(IntegrationEvent @event)
 2         {    // 其他代码...
 3             using (var channel = _persistentConnection.CreateModel())
 4             {
 5                 
 6                 policy.Execute(() =>
 7                 {
 8                     var properties = channel.CreateBasicProperties();
 9                     properties.DeliveryMode = 2; 
10                     channel.BasicPublish(
11                         exchange: BROKER_NAME,
12                         routingKey: eventName,
13                         mandatory: true,
14                         basicProperties: properties,
15                         body: body);
16                 });
17             }
18         }
通过重试熔断策略发布消息到rabbitmq,到此,productservice服务边界的处理已完成,好困要睡觉了,留着第二篇说吧。简单总结一下,微服务架构个人觉得最难处理的就是数据一致性的问题,如果是强一致性的需求,根本不适合微服务架构。上面代码在修改价格时开启了事务,并把事件日志纳入了该事务,这样在源头就确保了事件和数据的强一致性,并持久化事件,接下来再发布到mq,当然这里如果此时mq挂了,至少事件被持久化了,最终还可以补偿实现最终一致性,好了,就到这吧。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

猜你喜欢

转载自www.cnblogs.com/adair-blog/p/12784035.html