ASP.NET 5 with Dapr 初体验

分布式应用运行时Dapr目前已经发布了1.1.0版本,阿里云也在积极地为Dapr贡献代码和落地实践。作为一名开发者,自然也想玩一玩,看看Dapr带来的新“视”界到底是怎么样的。

1 关于Dapr

Dapr(Distributed Application Runtime)是一个开源、可移植、事件驱动的运行时。它使开发人员能够轻松地构建运行在云平台和边缘的弹性而微服务化的应用程序,无论是无状态还是有状态。Dapr 让开发人员能够专注于编写业务逻辑,而不是解决分布式系统的挑战,从而显著提高生产力并减少开发时间。此外,Dapr 也降低了大部分中小型企业基于微服务架构构建现代云原生应用的准入门槛。

Dapr 的核心构建模块 (或者说核心功能)如下:

  • 服务调用: 弹性服务与服务之间(service-to-service)调用可以在远程服务上启用方法调用,包括重试,无论远程服务在受支持的托管环境中运行在何处。

  • 状态管理:通过对键 / 值对的状态管理,可以很容易编写长时间运行、高可用性的有状态服务,以及同一个应用中的无状态服务。状态存储是可插入的,并且可以包括 Azure Cosmos 或 Redis,以及组件路线图上的其他组件,如 AWS DynamoDB 等。

  • 在服务之间发布和订阅消息(Pub/Sub):使事件驱动的架构能够简化水平可扩展性,并使其具备故障恢复能力。

  • 事件驱动的资源绑定:资源绑定和触发器在事件驱动的架构上进一步构建,通过从任何外部资源(如数据库、队列、文件系统、blob 存储、webhooks 等)接收和发送事件,从而实现可扩展性和弹性。例如,你的代码可以由 Azure EventHub 服务上的消息触发,并将数据写入 Azure CosmosDB。

  • 虚拟角色:无状态和有状态对象的模式,通过方法和状态封装使并发变得简单。Dapr 在其虚拟角色(Virtual Actors)运行时提供了许多功能,包括并发、状态、角色激活 / 停用的生命周期管理以及用于唤醒角色的计时器和提醒。

  • 服务之间的分布式跟踪:使用 W3C 跟踪上下文(W3C Trace Context)标准,轻松诊断和观察生产中的服务间调用,并将事件推送到跟踪和监视系统。

目前Dapr提供了如下所示的主流语言的SDK:

更多关于Dapr的介绍不是本文的重点,有兴趣的读者可以移步阅读:

(1)阿里巴巴的Dapr实践与探索

(2)Dapr是否会引领云原生中间件的未来

(3)分布式运行时 Dapr 知多少

本文的试玩会主要集中在服务调用(service invocation)和 发布订阅(pub / sub)上面,并且只会在入门小DEMO的程度,期望值过高的童鞋可以自行学习 或 绕道行走,毕竟我的时间也有限。

2 准备工作

一台Linux虚拟机

为了后面的DEMO,在VMware Workstation中准备一个Linux虚拟机环境,这里我选择的是CentOS 7.6。

在此虚拟机中设定静态IP地址(本示例为 192.168.2.100),关闭防火墙,设定主机名等一系列基本操作。

安装.NET 5 SDK

这里我的DEMO是基于local-host部署模式(也可以选择Kubernetes模式部署,但我没时间弄),因此给Linux安装一下.NET 5 SDK,命令如下:

添加受信源
sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
安装.NET 5 SDK
sudo yum install dotnet-sdk-5.0

安装Dapr CLI

官网提示直接在Linux下执行以下命令就可以将Dapr CLI下载到/usr/local/bin目录下:

wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash

不过由于网络原因,我选择了直接下载Release来安装:

(1)到github上下载1.1.0的release压缩包(dapr_linux_amd64.tar.gz),并将其传到Linux中。

(2)解压该压缩包,并将解压后的目录移动到/usr/local/bin目录下:

tar -zvxf dapr_linux_amd64.tar.gz

(3)通过输入 dapr 来验证是否安装成功:

此外,也可以通过 dapr --version 查看Dapr版本:

CLI version: 1.1.0 
Runtime version: 1.1.0

 初始化Dapr

安装好Dapr CLI之后,就可以在Linux上初始化Dapr了,命令如下:

dapr init

这个命令会帮你做一些列的事情,包括但不限于 拉取一波docker镜像 & 运行一波docker容器,如下图所示:

可以看到,dapr, redis, zipkin都已经运行起来了。

为什么有redis?因为它会作为默认的pub/sub中间件为dapr提供具体的实现能力。

为什么会有zipkin?因为它会作为默认的tracing中间件为我们提供链路追踪的能力。

OK,到此为止,本地的Dapr运行时基础环境已基本就绪。

3 .NET 5 应用集成Dapr SDK

准备三个.NET WebAPI

这里我们准备了三个WebAPI项目,分别是订单服务、购物车服务 以及 商品服务。

具体的代码可以去github上查看,github地址为:https://github.com/EdisonChou/EDT.Dapr.Sample

为所有WebAPI项目添加集成

为所有项目添加Dapr SDK的nuget包,这里是 Dapr.AspNetCore 组件。

为所有WebAPI项目注册Dapr

在StartUp类中,对Dapr Client进行注册,这里的AddDapr背后的操作其实就是给IoC容器注入了一个单例的DaprClient对象。

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

4 服务调用示例

这里假设CartService要和ProductService进行通信,通过REST获取商品数据。这里,就可以借助Dapr提供的服务间调用的功能进行通信。其工作原理如下图所示:

 

这里使用的方式是通过DaprClient直接InvokeMethod进行服务间的通信,传递了两个重要的参数,一个是依赖服务的app-id(根据你部署时设定的名字来写),另一个是依赖接口的route。

具体如下代码所示:

[ApiController]
[Route("[controller]")]
public class CartController : ControllerBase
{
    private readonly ILogger<CartController> _logger;
    private readonly DaprClient _daprClient;

    public CartController(ILogger<CartController> logger, DaprClient daprClient)
    {
        _logger = logger;
        _daprClient = daprClient;
    }

    [HttpGet]
    public async Task<IEnumerable<SKU>> Get()
    {
        _logger.LogInformation("[Begin] Query product data from Product Service");

        var products = await _daprClient.InvokeMethodAsync<IEnumerable<SKU>>
            (HttpMethod.Get, "ProductService", "Product");

        _logger.LogInformation($"[End] Query product data from Product Service, data : {products.ToArray().ToString()}");

        return products;
    }
}

这里对应ProductService的接口默认返回一些假数据:

[ApiController]
[Route("[controller]")]
public class ProductController : ControllerBase
{
    private static readonly string[] FakeProducts = new[]
    {
        "SKU1", "SKU2", "SKU3", "SKU4", "SKU5", "SKU6", "SKU7", "SKU8", "SKU9", "SKU10"
    };

    ......

    [HttpGet]
    public IEnumerable<SKU> Get()
    {
        _logger.LogInformation("[Begin] Query product data.");

        var rng = new Random();
        var result = Enumerable.Range(1, 5).Select(index => new SKU
        {
            Date = DateTime.Now.AddDays(index),
            Index = rng.Next(1, 100),
            Summary = FakeProducts[rng.Next(FakeProducts.Length)]
        })
        .ToArray();

        _logger.LogInformation("[End] Query product data.");

        return result;
    }
}

 然后,将这两个服务发布到Linux服务器上,当然,我们要通过dapr来部署,让.net application和dapr sidecar形成一体。

部署命令如下所示,可以看到我们既要为.net application指定端口,也要为dapr sidecar指定端口(这里主要为dapr指定了http端口,也可以为其指定grpc端口)。

dapr run --app-id CartService --app-port 5000 --dapr-http-port 5005 -- dotnet EDT.EMall.Cart.API.dll --urls "http://*:5000" 
dapr run --app-id ProductService --app-port 5010 --dapr-http-port 5015 -- dotnet EDT.EMall.Product.API.dll --urls "http://*:5010"

你会发现,当你run成功之后,会看到以下log,其中既有dapr的log,也有.net application的log,虽然他们是两个应用程序,但是你看到的它们是一体的。

最后,通过swagger来测试一下,结果如下,成功进行了服务调用。

5 消息发布及订阅示例

发布订阅模式(Publish-Subscribe)是众所周知且广泛使用的消息模式。这里我们假设OrderService的某个接口完成后就发布一个消息,告知订阅方有新订单的事件产生。

在Dapr中其工作原理如下图所示:

具体代码示例如下,借助DaprClient的PublishEvent接口实现消息发布:

[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
    private const string DaprPubSubName = "pubsub";

    private readonly ILogger<OrderController> _logger;
    private readonly DaprClient _daprClient;

    public OrderController(ILogger<OrderController> logger, DaprClient daprClient)
    {
        _logger = logger;
        _daprClient = daprClient;
    }

    [HttpPost]
    public async Task<Models.Order> Post(OrderDto orderDto)
    {
        _logger.LogInformation("[Begin] Create Order.");

        var order = new Models.Order()
        {
            // some mapping
            Id = orderDto.Id,
            ProductId = orderDto.ProductId,
            Count = orderDto.Count
        };
        // some other logic for order

        var orderStockDto = new OrderStockDto()
        {
            ProductId = orderDto.ProductId,
            Count = orderDto.Count
        };
        await _daprClient.PublishEventAsync(DaprPubSubName, "neworder", orderStockDto);

        _logger.LogInformation($"[End] Create Order Finished. Id : {orderStockDto.ProductId}, Count : {orderStockDto.Count}");

        return order;
    }
}

假设ProductService作为订阅方,需要消费这个事件,并扣减某个商品的库存。而基于Dapr,我们需要对ProductService添加一点配置:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ......
    app.UseCloudEvents(); // 标准化的消息传递格式
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapSubscribeHandler(); // 订阅消费处理
        ......
    });
}

然后,在ProductService中添加一个方法/接口 来作为订阅处理。

具体代码示例如下,需要注意的就是:

(1)作为消息处理接口,需要指定为HttpPost方式。

(2)需要指定Topic特性,并标注pubsubname 和 事件名。

private const string DaprPubSubName = "pubsub";

[HttpPost]
[Topic(DaprPubSubName, "neworder")]
public Models.Product SubProductStock(OrderStockDto orderStockDto)
{
    _logger.LogInformation($"[Begin] Sub Product Stock, Stock Need : {orderStockDto.Count}.");

    var product = _productService.GetProductById(orderStockDto.ProductId);
    if (orderStockDto.Count < 0 || orderStockDto.Count > product.Stock)
    {
        throw new InvalidOperationException("Invalid Product Count!");
    }
    product.Stock = product.Stock - orderStockDto.Count;
    _productService.SaveProduct(product);

    _logger.LogInformation($"[End] Sub Product Stock Finished, Stock Now : {product.Stock}.");

    return product;
}

这里的DaprPubSubName是pubsub,这是因为Dapr默认的pubsub实现是基于Redis的,而在配置中为Redis设置的name就是 pubsub,因此对于我们入门的话,就不要去更改,或者和配置中的name保持一致。

[root@dapr-lab-server ~]# cat ~/.dapr/components/pubsub.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""

当然,我们也可以将默认的pubsub实现Redis换为熟悉的RabbitMQ。我们只需要更改上面的yml文件内容如下:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub-rq
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: host
    value: "amqp://localhost:5672"
  - name: durable
    value: true

然后,将这两个服务发布到Linux服务器上,当然,我们要通过dapr来部署,让.net application和dapr sidecar形成一体。

dapr run --app-id OrderService --app-port 5020 --dapr-http-port 5025 -- dotnet EDT.EMall.Order.API.dll --urls "http://*:5020"
dapr run --app-id ProductService --app-port 5010 --dapr-http-port 5015 -- dotnet EDT.EMall.Product.API.dll --urls "http://*:5010"

run成功后,通过 dapr list 查看,可以看到三个服务都已经启动起来了,它们是三个由.net application + dapr sidecar 组成的“合体应用”。

最后,我们通过swagger来测试一下,测试结果如下图所示:

(1)OrderService:

(2)ProductService:

这里的99其实是假总库存100 - 消息传递过来的商品数量得到的,具体可以参考代码示例。

6 小结

本文总结了我试玩Dapr的一些经过,包括Dapr的Local环境搭建、.NET 5 Application与Dapr的集成 和 两个具体场景的小DEMO(服务调用 和 Pub/Sub)。

这里借助知乎上 iyacontrol 童鞋的评论(来源:https://www.zhihu.com/question/351298264),作为结尾:

Dapr 本身是一种 Sidecar 模式(虽然Dapr也提供了SDK,但是个人认为这并不是Dapr以后的发展方向)。Sidecar 模式的意义在于, 解耦了基础设施和核心业务

简单来看,Dapr的意义在于:

  • 对于小公司,甚至没有基础架构和中间件团队的公司,Dapr 提供了开箱即用的基础设施功能,可以让小公司轻松构建弹性,分布式应用

  • 对于中等单位,具备一定的基础架构能力,在使用Dapr的过程中,可能Dapr并不能完全满足需求,那么也可以在Dapr框架体系下,花费较小的成本进行自定义扩展

  • 对于大公司,Dapr 提供了一种思路。相信基础架构团队会越来越倾向于通过交付Sidecar的形式来提供基础设施

长远来看,Dapr背后的架构模式是符合未来架构趋势(多运行时架构)和云原生发展趋势的

代码示例

github:https://github.com/EdisonChou/EDT.Dapr.Sample

参考资料

Microsoft,《Dapr for .NET Developer》: 

Tony,《Dapr公开课

 

posted @ 2021-04-14 17:05  EdisonZhou  阅读(314)  评论(1编辑  收藏  举报