Dapr - Hello Dapr

13 March 2020 — Written by Sky Chang
#Dapr#K8S#AKS#ASP.NET Core#Microservice

前言

繼,前面幾篇記錄了ㄧ些關於最近遇到的一些問題紀錄,終於要開始寫本年度的第一篇技術文了! ( 希望不要也是最後一篇 QQ )。 總之,趁著最近有一點寫文章的 feel,就加緊寫吧,不然垃圾場不止有垃圾,也都長草了 QQ..

Dapr

今天要介紹的是,登登登,Dapr !!

什麼是 Dapr , 就是 Distributed Application Runtime !! ( 不要問我那個 p 在哪裏..),他是由微軟發展出來,純 Open Source 的專案 ( 而且星星數也破 5xxx 了!! ),專門用來處理開發人員搞 Microservice 時,遇到的一些問題。

01

Dapr 的功能

那他有什麼強大的功能呢!!?

  • 簡單就可以處理 Event-Driven
  • 能建構出 Stateful 的 Microservice
  • 可以放到雲端環境和 Edge
  • 支援眾多語言! ( 連提供的 Demo 都不是用 C# 呢 泣)

而且,他支援任何語言,任何平台,透過 http / gRPC 來調用中間 Dapr 的服務,所以你可以想像,你想要執行的服務 ( ex: 儲存到 Readis ),就不需要使用 Redis 的 SDK,或是 Redis 特定的呼叫方式。只需要透過 http / gRPC 的調用,就可以將東西存到 Redis 上。當然如有時候使用 http / gRPC 還是很麻煩,所以 Dapr 也提供了多語言版本的 SDK,讓你使用上更方便,如下圖。

overview

當然,他實際上不像 Istio 那樣,可以不用改 Code:就可以把原有的程式碼套上去,就像剛剛上面說的,從 Redis 的 SDK 改成呼叫 Dapr 的 SDK,還是有部分需要去處理,所以,如果不想改 Code,就想使用,是有難度低~ ( 當然以前切分的好,現在要轉上去也容易了 )

Dapr 提供的功能,如下 ( 寫這篇文章為 0.5 版本 )

  • Service Invocation : 提供了 Service To Service 的服務,包含重試,位置的導向等等。
  • State Management : 搭配 Key/Value 來進行狀態的管理,可以輕鬆地將長期運行,讓你的應用程式擁有高可用性之有狀態服務與無狀態服務。狀態存儲是可注入的,可以使用 Azure CosmosDB,AWS DynamoDB 或 Redis 來進行搭配。
  • Publish and Subscribe Messaging : 輕鬆地使用發布事件,和訂閱事件的能力。
  • Resource Bindings : Event-driven 的架構,可綁定 Resource,當發生事件的時候,就自動觸發。
  • Distributed Tracing : 提供了分散式的 Trace 和簡單的記錄查找
  • Actors : 可以使用 Actor 模式,此模式針對有狀態和無狀態對象來設計,通過方法和狀態封裝使操作變得簡單。Dapr 在其 actor 運行時中提供了許多功能,包括並發,狀態,actor 激活/停用 的生命週期管理以及計時器、喚醒等。

而本篇文章的 Hello World 會使用到 Service Invocation 和 State Management。

Dapr 和 Sidecar

基本上 Dapr 也是 Sidecar 模式的操作方式,所以很多人會拿 Dapr 和 Istio 相比,雖然兩個都是 Sidecar 模式,但 Dapr 重心放在服務的調用而 Istio 怎放在網路層的監控與管理。

overview sidecar

如果是 K8S 的架構,當然就是包在一個 Pod 裡面了。

overview sidecar kubernetes

提外話,去年 Study4.TW 的 dotnet Conf,Alan Liu 的 Session 最後也有提到喔!!!有興趣也可以去看看影片。

Dapr 環境準備

首先,開始式前,當然要先準備環境。Dapr 基本上可以 run 在 local ( 獨立模式,非 K8S 底下),或是 K8S 底下 ( AKS 也通 )。

通常獨立模式,主要給開發使用,然後再部署到正式 ( K8S / AKS ) 環境。

當然,這東西,主要還是以 Container 為主,所以 Docker 是跑不掉的~~

安裝 Dapr CLI

目前都是透過指令安裝,未來可能會改變,建議參考官方文件

Windows - 安裝 Dapr CLI 到 C:\dapr 和加到 $PATH 路徑

powershell -Command "iwr -useb https://raw.githubusercontent.com/dapr/cli/master/install/install.ps1 | iex"

MacOs - 安裝 Dapr CLI 到 /usr/local/bin

curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | /bin/bash

02

當然,你也可以手動下載.....

安裝標準環境

dapr init

當輸入完後,會產生兩個 Container。沒錯,他背後就是 Container,所以對環境來說,真的很乾淨..

02

如果不想要用就下,加上 --all 的原因是因為,預設他不會把 Redis 給移除,需要透過 --all 才會全部移除。

dapr uninstall --all

如果想安裝特定版本:

dapr init --runtime-version 0.1.0

至於 K8S / AKS 的安裝,下一篇再提吧 ( 希望會有下一篇. )

來寫一個 Hello World 吧

接下來,我們就來嘗試寫一個 Dapr 的應用程式看看。目前目標,就如同官網上面的這張架構圖一樣。官網的架構是使用 Node.js 當作開發目標,目前小弟用 ASP.NET Core 來重新撰寫。

Architecture Diagram

而從上圖我們可以發現,基本上,未來存取 ASP.NET Core 的 API ( 也就是上圖的 Node Code 位置 ),並不會直接進入 ASP.NET Core 的 API,而是會透過 Dapr Runtime 來進行處理,而同樣的 Dapr Runtime 會提供 API 的接口。

那 ASP.NET Croe API 要如何存取 State stores 呢 ( 也就是上圖的 Cosmos、Redis ),當然也是透過 Dapr 這一層來做處理。 也因此,要存取這些狀態,就不需要特別去理解 Cosmos、Redis 等等 SDK 或是 API 的調用,換言之,Dapr 提供了一層來讓使用者開發更加方便。

好,接下來我們就在 ASP.NET Core 寫一個 Post API,底下這個程式碼其實滿簡單的,我們會透過 API 傳入 Order 這個物件進來,而為了 Demo 方便,所以 Order 裡面其實裡面也才兩個屬性,id 和 Name。

而下面的 Post 用途,其實就是當 Order 物件進來的時候,會透過 HttpClient 將此物件傳入到 Dapr 裡面去 ( 透過 Dapr 提供的 URL ),然後 Dapr 會自己去將 Order 存到指定的 Storge 裡面去 ( Storge 的定義,會在 Dapr 的 YAML 檔案裡面定義 )

( 這邊先用笨方法,但其實有 SDK 可以使用 )

至於 Dapr 提供的 URL 為 "http://localhost:3500/v1.0/state/statestore",在底下我們會用 _stateUrl 變數來存放這個位置,而在 Local 測試用的 Port 預設為 3500,而上面的 statestore 這個名稱,也是透過 Dapr 的 YAML 來對應。

[HttpPost]
public async Task<IActionResult> Post([FromBody] Order dto)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var data = new List<Object>()
    {
        new { Key = "order" , Value = dto }
    };

    var client = new HttpClient();
    
    var content = new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, @"application/json");

    var result = await client.PostAsync(_stateUrl, content);

    if(!result.IsSuccessStatusCode)
    {
        throw new Exception(await result.Content.ReadAsStringAsync());
    }

    return CreatedAtRoute("GetOrder", new { id = dto.Id }, dto);
}

完成後,我們可以下底下指令,其中 app-id 代表的是我們 ASP.NET Core API 的自訂名稱,未來要呼叫 ASP.NET Core 就會使用 api 這個 id 來處理,app-port 代表的是 ASP.NET Core API 的 Port ( 我們這邊把 https 先關掉 ),port 3500 就是 Dapr 提供的 Port 號,最後就是大家熟悉的 dotnet run。

所以底下指令,除了會將 Dapr run 起來外,還會順便把 dotnet run 起來。

dapr run --app-id api --app-port 5000 --port 3500 dotnet run

完成後,也可以使用 dapr list 來查看目前的狀況。

dapr list

如下,我們可以看到 ASP.NET Core API 已經 Run 起來了

04

接著,我們用 Postman,簡單測試一下,我們先直接打 http://localhost:5000/api/orders 這個位置,目前這就是直接去呼叫 ASP.NET Core 的 API,從下圖可以看到完全沒問題。

05

等等,剛剛不是說要透過 Dapr 來呼叫 ASP.NET Core API 嗎!?,對的,所以這邊我們換個位置來呼叫,改用 http://localhost:3500/v1.0/invoke/api/method/api/orders 來呼叫,如下圖。

06

得到的答案是一樣的,但這樣的呼叫過程,其實就透過 Dapr 來呼叫了,而透過 Dapr 的格式,就是 http://localhost:3500/v1.0/invoke/{app-id}/method/{位置},其中 app-id 就是我們剛剛執行指令所給的名稱,而位置會對應到 我們 http://localhost:5000/api/orders 裡面的 api/orders

另外,當執行 Dapr run 的時候,他也會在原始碼這邊建立 components 的資料夾,裡面兩個 YAML 就會描述 statestore 這個名稱等資訊。

最後,因為我們沒有寫 Get 方法到 ASP.NET Core API 裡面,但我們也可以直接調用 Dapr 來取得我們剛剛 Post 的資料

我們呼叫 http://localhost:3500/v1.0/state/statestore/order ,其中 statestore 這個名稱的定義,就是我們上面提到,當 Dapr 建構時,自動幫我們加的 YAML ,裡面有定義 statestore 這個名稱,而 order 要注意,我們調用這個 URL,並非是透過 Dapr 存取 ASP.NET API,而是透過 Dapr 直接存取 Redis,所以這邊的 order ,並不是我們上面提的 ASP.NET Core 位置,而是程式碼裡面定義的這段,我們給了 Key 值,所以這個 order 是對應到我們給的 Key 值。

 new { Key = "order" , Value = dto }

簡單的說,這個網址,就是透過 Darp 去 Redis 撈資料..

打出來的結果如下。

07

當然,正常流程應該是,外部的應用程式去 call Dapr,然後讓 Dapr 轉到 ASP.NET Core API 的 Get Order 方法,再讓 Order 方法去調用 Dapr 的 statestore 的位置來取得,然後再由 Get Order 回覆給 Dapr,Dapr 在回覆給外部的應用程式。

所以我們這邊加上 ASP.NET Core Get Order 方法,可想而知,我們就是透過 http://localhost:3500/v1.0/state/statestore/order 這個 Dapr 提供的街口取得資料。

[HttpGet]
public async Task<IActionResult> Get()
{
    var client = new HttpClient();
    
    var result = await client.GetAsync($"{_stateUrl}/order");

    result.EnsureSuccessStatusCode();
    var resp = await result.Content.ReadAsStringAsync();

    var order = JsonSerializer.Deserialize<Order>(resp);

    return Ok(order);
}

最後,我們透過 http://localhost:3500/v1.0/invoke/api/method/api/orders 這個 Dapr 接口,去調用 ASP.NET Core 的 Get Order 方法,而 ASP.NET Core Get Order 方法,會再去調用 Dapr 取得 statestore,然後最終由 Dapr 返回。

08

總之,任何的輸入輸出都是靠 Dapr 來管理。

透過指令方式

除了上面用 Postman 工具外,其實也支援指令方式。請注意,MacOS 和 Powershell 的指令可能會略有不同。底下為 MacOS。

塞入一筆資料

dapr invoke --app-id api  --method 'api/orders' --payload '{ "id": 41, "name":"Hello World2" }'

取得資料

curl http://localhost:3500/v1.0/invoke/api/method/api/orders

完成如下

09

加入一個 WorkRole

接下來,我們要繼續完成官方的練習一,從下圖可以看到,我們完成了右半邊,也就是 Node.js 這邊的練習,接下來,我們要寫一個定時器,每過一段時間,並會修改 Order 的資料。

Architecture Diagram B

首先,我們要完成上圖 Python 的事情,但我們這邊還是使用 C# 改寫。我們使用 ASP.NET Core 3 的新功能 Worker Service 樣板。 然後修改底下的執行,這代表著每秒鐘會去呼叫 我們 API 方的 Dapr,然後加一筆 Order 進去。

請注意,你會發現,我們不使用 ASP.NET Core 原生的 API 位置,而是透過 Dapr 來呼叫 ASP.NET Core API,這意味著說,你可以不需要知道 原來 API 的 URL,你只需要透過 Dapr 就可以呼叫到 ASP.NET Core API。

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("Start Worker");

    var id = 0;
    while (!stoppingToken.IsCancellationRequested)
    {
        id++;
        var data = new Order(){ Id = id, Name = $"Hello Wolrd {id}" };

        var client = new HttpClient();

        var content = new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, @"application/json");

        var result = await client.PostAsync("http://localhost:3500/v1.0/invoke/api/method/api/orders", content);

        result.EnsureSuccessStatusCode();

        _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
        _logger.LogInformation($"Add Order id = {id}" );

        await Task.Delay(1000, stoppingToken);
    }
}

完成 Code 之後,我們可以下以下指令,只是這次我們名稱改為 Worker,而不用特別設定 Port。

dapr run --app-id worker dotnet run

如下圖,完成後,他每秒都會更新 Order 一次。

10

我們也可以用 Postman 看結果。

11

到此,我們就完成了 Dapr 的第一個練習。

但不知道大家由沒有注意,其實不用把 Worker 的 Dapr run 起來,也是可以直接透過 http://localhost:3500/v1.0/invoke/api/method/api/orders 呼叫。那透過 Dapr run 起來的 Worker,為什麼也是直接打 http://localhost:3500/v1.0/invoke/api/method/api/orders 呢?這樣和直接呼叫有什麼不同。

其實小弟這邊也有點困惑,但官方的解釋是說,當 Worker run 起 Dapr 的時候,打 API 的 Dapr ( http://localhost:3500/v1.0/invoke/api/method/api/orders ) 其實還是有經過 Worker 的 Dapr 的,所以當我們使用 Dapr run Worker 這個應用的時候,其實背後 Dapr 就會在旁邊監控與託管。

service invocation

總之,暫時也只能相信他了 XDDDD,如果有錯也請多包涵了。

停止 Dapr

如果要停止,可以下,就可以停止。

dapr stop --app-id api

備註

因為了簡單示範,像 HttpClient 就沒使用 HttpClientFactory 來建立,所以在真實環境,還是要好好注意效能的議題喔。

後記

這一篇,其實寫的有點久,雖然已概念來說·不算難,但整理資料花了不少時間:但不管怎樣,從這個簡單的練習,就可以看到 Dapr 的強大之處,但 Dapr 其實還有更多功能,後續再讓我們繼續看下去。

參考資料

Sky & Study4.TW