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公開課

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM