云原生 | .NET 5 with Dapr 初體驗

分布式應(yīng)用運(yùn)行時Dapr目前已經(jīng)發(fā)布了1.1.0版本,阿里云也在積極地為Dapr貢獻(xiàn)代碼和落地實踐。作為一名開發(fā)者,自然也想玩一玩,看看Dapr帶來的新“視”界到底是怎么樣的。
Dapr(Distributed Application Runtime)是一個開源、可移植、事件驅(qū)動的運(yùn)行時。它使開發(fā)人員能夠輕松地構(gòu)建運(yùn)行在云平臺和邊緣的彈性而微服務(wù)化的應(yīng)用程序,無論是無狀態(tài)還是有狀態(tài)。Dapr 讓開發(fā)人員能夠?qū)W⒂诰帉憳I(yè)務(wù)邏輯,而不是解決分布式系統(tǒng)的挑戰(zhàn),從而顯著提高生產(chǎn)力并減少開發(fā)時間。此外,Dapr 也降低了大部分中小型企業(yè)基于微服務(wù)架構(gòu)構(gòu)建現(xiàn)代云原生應(yīng)用的準(zhǔn)入門檻。

服務(wù)調(diào)用: 彈性服務(wù)與服務(wù)之間(service-to-service)調(diào)用可以在遠(yuǎn)程服務(wù)上啟用方法調(diào)用,包括重試,無論遠(yuǎn)程服務(wù)在受支持的托管環(huán)境中運(yùn)行在何處。
狀態(tài)管理:通過對鍵 / 值對的狀態(tài)管理,可以很容易編寫長時間運(yùn)行、高可用性的有狀態(tài)服務(wù),以及同一個應(yīng)用中的無狀態(tài)服務(wù)。狀態(tài)存儲是可插入的,并且可以包括 Azure Cosmos 或 Redis,以及組件路線圖上的其他組件,如 AWS DynamoDB 等。
在服務(wù)之間發(fā)布和訂閱消息(Pub/Sub):使事件驅(qū)動的架構(gòu)能夠簡化水平可擴(kuò)展性,并使其具備故障恢復(fù)能力。
事件驅(qū)動的資源綁定:資源綁定和觸發(fā)器在事件驅(qū)動的架構(gòu)上進(jìn)一步構(gòu)建,通過從任何外部資源(如數(shù)據(jù)庫、隊列、文件系統(tǒng)、blob 存儲、webhooks 等)接收和發(fā)送事件,從而實現(xiàn)可擴(kuò)展性和彈性。例如,你的代碼可以由 Azure EventHub 服務(wù)上的消息觸發(fā),并將數(shù)據(jù)寫入 Azure CosmosDB。
虛擬角色:無狀態(tài)和有狀態(tài)對象的模式,通過方法和狀態(tài)封裝使并發(fā)變得簡單。Dapr 在其虛擬角色(Virtual Actors)運(yùn)行時提供了許多功能,包括并發(fā)、狀態(tài)、角色激活 / 停用的生命周期管理以及用于喚醒角色的計時器和提醒。
服務(wù)之間的分布式跟蹤:使用 W3C 跟蹤上下文(W3C Trace Context)標(biāo)準(zhǔn),輕松診斷和觀察生產(chǎn)中的服務(wù)間調(diào)用,并將事件推送到跟蹤和監(jiān)視系統(tǒng)。
目前Dapr提供了如下所示的主流語言的SDK:

更多關(guān)于Dapr的介紹不是本文的重點(diǎn),有興趣的讀者可以移步閱讀:
本文的試玩會主要集中在服務(wù)調(diào)用(service invocation)和 發(fā)布訂閱(pub / sub)上面,并且只會在入門小DEMO的程度,期望值過高的童鞋可以自行學(xué)習(xí) 或 繞道行走,畢竟我的時間也有限。
一臺Linux虛擬機(jī)
為了后面的DEMO,在VMware Workstation中準(zhǔn)備一個Linux虛擬機(jī)環(huán)境,這里我選擇的是CentOS 7.6。
在此虛擬機(jī)中設(shè)定靜態(tài)IP地址(本示例為 192.168.2.100),關(guān)閉防火墻,設(shè)定主機(jī)名等一系列基本操作。
安裝.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 SDKsudo yum install dotnet-sdk-5.0
安裝Dapr CLI
官網(wǎng)提示直接在Linux下執(zhí)行以下命令就可以將Dapr CLI下載到/usr/local/bin目錄下:
wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash不過由于網(wǎng)絡(luò)原因,我選擇了直接下載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.0Runtime version: 1.1.0
初始化Dapr
安裝好Dapr CLI之后,就可以在Linux上初始化Dapr了,命令如下:
dapr init這個命令會幫你做一些列的事情,包括但不限于 拉取一波docker鏡像 & 運(yùn)行一波docker容器,如下圖所示:

可以看到,dapr, redis, zipkin都已經(jīng)運(yùn)行起來了。
為什么有redis?因為它會作為默認(rèn)的pub/sub中間件為dapr提供具體的實現(xiàn)能力。
為什么會有zipkin?因為它會作為默認(rèn)的tracing中間件為我們提供鏈路追蹤的能力。

OK,到此為止,本地的Dapr運(yùn)行時基礎(chǔ)環(huán)境已基本就緒。
準(zhǔn)備三個.NET WebAPI
這里我們準(zhǔn)備了三個WebAPI項目,分別是訂單服務(wù)、購物車服務(wù) 以及 商品服務(wù)。

具體的代碼可以去github上查看,github地址為:。
為所有WebAPI項目添加集成
為所有項目添加Dapr SDK的nuget包,這里是 Dapr.AspNetCore 組件。

為所有WebAPI項目注冊Dapr
在StartUp類中,對Dapr Client進(jìn)行注冊,這里的AddDapr背后的操作其實就是給IoC容器注入了一個單例的DaprClient對象。
public void ConfigureServices(IServiceCollection services){services.AddControllers().AddDapr();......}
這里假設(shè)CartService要和ProductService進(jìn)行通信,通過REST獲取商品數(shù)據(jù)。這里,就可以借助Dapr提供的服務(wù)間調(diào)用的功能進(jìn)行通信。其工作原理如下圖所示:

這里使用的方式是通過DaprClient直接InvokeMethod進(jìn)行服務(wù)間的通信,傳遞了兩個重要的參數(shù),一個是依賴服務(wù)的app-id(根據(jù)你部署時設(shè)定的名字來寫),另一個是依賴接口的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;}}
這里對應(yīng)ProductService的接口默認(rèn)返回一些假數(shù)據(jù):
[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;}}
然后,將這兩個服務(wù)發(fā)布到Linux服務(wù)器上,當(dāng)然,我們要通過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"
你會發(fā)現(xiàn),當(dāng)你run成功之后,會看到以下log,其中既有dapr的log,也有.net application的log,雖然他們是兩個應(yīng)用程序,但是你看到的它們是一體的。

最后,通過swagger來測試一下,結(jié)果如下,成功進(jìn)行了服務(wù)調(diào)用。

發(fā)布訂閱模式(Publish-Subscribe)是眾所周知且廣泛使用的消息模式。這里我們假設(shè)OrderService的某個接口完成后就發(fā)布一個消息,告知訂閱方有新訂單的事件產(chǎn)生。
在Dapr中其工作原理如下圖所示:

具體代碼示例如下,借助DaprClient的PublishEvent接口實現(xiàn)消息發(fā)布:
[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 mappingId = orderDto.Id,ProductId = orderDto.ProductId,Count = orderDto.Count};// some other logic for ordervar 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;}}
假設(shè)ProductService作為訂閱方,需要消費(fèi)這個事件,并扣減某個商品的庫存。而基于Dapr,我們需要對ProductService添加一點(diǎn)配置:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env){......app.UseCloudEvents(); // 標(biāo)準(zhǔn)化的消息傳遞格式app.UseEndpoints(endpoints =>{endpoints.MapSubscribeHandler(); // 訂閱消費(fèi)處理......});}
然后,在ProductService中添加一個方法/接口 來作為訂閱處理。
具體代碼示例如下,需要注意的就是:
(1)作為消息處理接口,需要指定為HttpPost方式。
(2)需要指定Topic特性,并標(biāo)注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默認(rèn)的pubsub實現(xiàn)是基于Redis的,而在配置中為Redis設(shè)置的name就是 pubsub,因此對于我們?nèi)腴T的話,就不要去更改,或者和配置中的name保持一致。
[root@dapr-lab-server ~]# cat ~/.dapr/components/pubsub.yamlapiVersion: dapr.io/v1alpha1kind: Componentmetadata:name: pubsubspec:type: pubsub.redismetadata:- name: redisHostvalue: localhost:6379- name: redisPasswordvalue: ""
當(dāng)然,我們也可以將默認(rèn)的pubsub實現(xiàn)Redis換為熟悉的RabbitMQ。我們只需要更改上面的yml文件內(nèi)容如下:
apiVersion: dapr.io/v1alpha1kind: Componentmetadata:name: pubsub-rqspec:type: pubsub.rabbitmqversion: v1metadata:- name: hostvalue: "amqp://localhost:5672"- name: durablevalue: true
然后,將這兩個服務(wù)發(fā)布到Linux服務(wù)器上,當(dāng)然,我們要通過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 查看,可以看到三個服務(wù)都已經(jīng)啟動起來了,它們是三個由.net application + dapr sidecar 組成的“合體應(yīng)用”。

最后,我們通過swagger來測試一下,測試結(jié)果如下圖所示:
(1)OrderService:

(2)ProductService:

這里的99其實是假總庫存100 - 消息傳遞過來的商品數(shù)量得到的,具體可以參考代碼示例。
本文總結(jié)了我試玩Dapr的一些經(jīng)過,包括Dapr的Local環(huán)境搭建、.NET 5 Application與Dapr的集成 和 兩個具體場景的小DEMO(服務(wù)調(diào)用 和 Pub/Sub)。
這里借助知乎上 iyacontrol 童鞋的評論(來源:https://www.zhihu.com/question/351298264),作為結(jié)尾:
Dapr 本身是一種 Sidecar 模式(雖然Dapr也提供了SDK,但是個人認(rèn)為這并不是Dapr以后的發(fā)展方向)。Sidecar 模式的意義在于, 解耦了基礎(chǔ)設(shè)施和核心業(yè)務(wù)。
簡單來看,Dapr的意義在于:
對于小公司,甚至沒有基礎(chǔ)架構(gòu)和中間件團(tuán)隊的公司,Dapr 提供了開箱即用的基礎(chǔ)設(shè)施功能,可以讓小公司輕松構(gòu)建彈性,分布式應(yīng)用。
對于中等單位,具備一定的基礎(chǔ)架構(gòu)能力,在使用Dapr的過程中,可能Dapr并不能完全滿足需求,那么也可以在Dapr框架體系下,花費(fèi)較小的成本進(jìn)行自定義擴(kuò)展。
對于大公司,Dapr 提供了一種思路。相信基礎(chǔ)架構(gòu)團(tuán)隊會越來越傾向于通過交付Sidecar的形式來提供基礎(chǔ)設(shè)施。
長遠(yuǎn)來看,Dapr背后的架構(gòu)模式是符合未來架構(gòu)趨勢(多運(yùn)行時架構(gòu))和云原生發(fā)展趨勢的。
代碼示例
github:https://github.com/EdisonChou/EDT.Dapr.Sample
參考資料
Microsoft,《Dapr for .NET Developer》: https://docs.microsoft.com/zh-cn/dotnet/architecture/dapr-for-net-developers
Tony,《Dapr公開課》:https://www.bilibili.com/video/BV1Fb4y197fT
【推薦】.NET Core開發(fā)實戰(zhàn)視頻課程 ★★★
.NET Core實戰(zhàn)項目之CMS 第一章 入門篇-開篇及總體規(guī)劃
【.NET Core微服務(wù)實戰(zhàn)-統(tǒng)一身份認(rèn)證】開篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個接口多種實現(xiàn)的依賴注入與動態(tài)選擇看這篇就夠了
用abp vNext快速開發(fā)Quartz.NET定時任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實現(xiàn)作業(yè)調(diào)度
現(xiàn)身說法:實際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化
