在非容器(叢集)環境下執行dapr

語言: CN / TW / HK

前一段時間一直關注的 dapr 正式釋出了v1.0版本(實際上本文釋出時還更新了v1.0.1),代表 dapr 在某些程度上進入穩定狀態,可以嘗試在實際中進行運用。作為我一直關注的專案,在第一時間中進行了嘗試,並試圖引入實際專案中,本文則是針對這些的一些先期測試內容.

什麼是dapr?

dapr 最早是由微軟開源的(不愧是你),一個可移植的、事件驅動的程式執行時,它使任何開發者都能輕鬆地構建執行在雲和邊緣的彈性、無狀態/有狀態的應用程式,並且可以靈活支援多種開發語言。換而言之,在我看來, dapr 可以作為一個 serverless 落地方案看待和處理,對程式而言,只關注提供的store和訊息佇列介面,無需關心架構層面更多內容。

不過在官方的示例教程中,使用的環境為容器環境部署和管理dapr。實際上,除了在容器環境或者容器叢集環境下, dapr 可以配置為在本地機器上以自託管模式執行。

本地安裝

dapr 安裝可以通過官方的 dapr-cli 實現, dapr-cli 可以通過一鍵安裝命令快速安裝:

# wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
Your system is linux_amd64

Dapr CLI is detected:
main: line 86: 43656 Segmentation fault      $DAPR_CLI_FILE --version
Reinstalling Dapr CLI - /usr/local/bin/dapr...

Getting the latest Dapr CLI...
Installing v1.0.0 Dapr CLI...
Downloading https://github.com/dapr/cli/releases/download/v1.0.0/dapr_linux_amd64.tar.gz ...
dapr installed into /usr/local/bin successfully.
CLI version: 1.0.0
Runtime version: n/a

To get started with Dapr, please visit https://docs.dapr.io/getting-started/

可以通過輸入 dapr 命令確認 dapr-cli 程式是否被正常安裝成功。

接下來使用dapr-cli安裝所有的runtime等應用。

# dapr init --slim
:hourglass:  Making the jump to hyperspace...
↘  Downloading binaries and setting up components...
Dapr runtime installed to /root/.dapr/bin, you may run the following to add it to your path if you want to run daprd directly:
    export PATH=$PATH:/root/.dapr/bin
:white_check_mark:  Downloaded binaries and completed components set up.
:information_source:  daprd binary has been installed to /root/.dapr/bin.
:information_source:  placement binary has been installed to /root/.dapr/bin.
:white_check_mark:  Success! Dapr is up and running. To get started, go here: https://aka.ms/dapr-getting-started

# dapr --version
CLI version: 1.0.0
Runtime version: 1.0.1

在官方文件中,如果選擇使用 init 命令初始化, dapr-cli 將會自動嘗試使用容器環境管理相關程式,只有新增 --slim 引數才會選擇本地化執行。更多用法可以參考 dapr help init 幫助。預設程式相關內容會安裝在 $HOME/.dapr 目錄下,這裡因為我為了簡便使用了 root 使用者,因此程式命令所在目錄為 /root/.dapr/bin ,共安裝瞭如下命令:

# ls ~/.dapr/bin
daprd  dashboard  placement  web

從檔名可以看出來 daprd 是deamon程序, dashboard 就是管理面板, placement 是用於管理 actor 分佈方案和金鑰範圍的工具。官方文件中提到在安裝後會使用 Reids 作為預設的儲存和pub/sub元件,但是我實際安裝下來其實是並沒沒有的,不知道是不是文件有些過期導致的。這時如果按照官方文件的例子進行操作啟動程式並嘗試在儲存中儲存資料,則會出現報錯的情況:

// 第一個session中執行:
# dapr run --app-id myapp --dapr-http-port 3500

// 第二個session中執行:
# curl -X POST -H "Content-Type: application/json" -d '[{ "key": "name", "value": "Bruce Wayne"}]' http://localhost:3500/v1.0/state/statestore

{"errorCode":"ERR_STATE_STORES_NOT_CONFIGURED","message":"state store is not configured"}

不過實際上新增元件在 dapr 中也是比較簡單的,可以通過在 $HOME/.dapr/components 下新增對應 yaml 檔案實現。

新增Redis作為元件

我們可以在 官方文件 中找到一個 Redis 元件配置模版,可以快速使用:

# redis-store.yml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: redis-store
  namespace: default
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: 127.0.0.1:6379
  - name: redisPassword
    value: ""

當然我們也可以使用 Redis Stream 功能做pub/sub功能,雖然這個功能已經GA,但是介於 Redis Stream 的特點,你需要謹慎使用這個功能,這裡只是因為是演示所以無所謂:

# redis-pubsub.yml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: redis-pubsub
  namespace: default
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: 127.0.0.1:6379
  - name: redisPassword
    value: ""
  - name: consumerID
    value: "myGroup"

這裡我們定義了一個 store 名叫做 redis-store ,所以我們要把上面的命令修改一下:

# curl -X POST -H "Content-Type: application/json" -d '[{ "key": "name", "value": "Bruce Wayne"}]' http://localhost:3500/v1.0/state/redis-store

// 獲取儲存內容
# curl http://localhost:3500/v1.0/state/redis-store/name
"Bruce Wayne"

同時也可以通過 redis-cli 獲取 Redis 中儲存的內容:

# redis-cli
127.0.0.1:6379> keys *
1) "myapp||name"
127.0.0.1:6379> hgetall "myapp||name"
1) "data"
2) "\"Bruce Wayne\""
3) "version"
4) "1"

我們在新增 Redis 作為儲存時還額外添加了 Redis 支援釋出/訂閱功能,這個功能如何實現呢?這裡可能就需要編寫額外程式實現了。我們這裡採用官方的例子進行。訂閱在 dapr 中有兩種形式,一種是採用 yaml 宣告元件形式,另外一種則可以通過編寫程式碼形式實現。當然第一種方式和第二種方式互有優劣,前者更適合無縫整合,後者方便開發控制。這裡為了演示直觀性直接採用了編寫程式碼方式實現。

package main

import (
	"io"
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.GET("/dapr/subscribe", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, []map[string]string{
			{
				"pubsubname": "redis-pubsub",
				"topic":      "deathStarStatus",
				"route":      "dsstatus",
			},
		})
	})
	r.POST("/dsstatus", func(c *gin.Context) {
		b, _ := io.ReadAll(c.Request.Body)
		defer c.Request.Body.Close()
		log.Println(string(b))
		c.JSON(http.StatusOK, map[string]interface{}{"success": true})
	})
	r.Run("127.0.0.1:5000")
}

使用如下命令啟動編譯後的 daprdemo ,注意指定檔名時需要填寫路徑或者在 $PATH 中:

# dapr --app-id subapp --app-port 5000 run ~/daprdemo

在程式啟動日誌中我們可以看到 dapr 會嘗試訪問一些預設的 endpoint 讀取可能的配置:

INFO[0000] application discovered on port 5000           app_id=subapp instance=127.0.0.1 scope=dapr.runtime type=log ver=1.0.1
== APP == [GIN] 2021/03/11 - 10:45:02 | 404 |         949ns |       127.0.0.1 | GET      "/dapr/config"

INFO[0000] application configuration loaded              app_id=subapp instance=127.0.0.1 scope=dapr.runtime type=log ver=1.0.1
INFO[0000] actor runtime started. actor idle timeout: 1h0m0s. actor scan interval: 30s  app_id=subapp instance=127.0.0.1 scope=dapr.runtime.actor type=log ver=1.0.1
== APP == [GIN] 2021/03/11 - 10:45:02 | 200 |     540.891µs |       127.0.0.1 | GET      "/dapr/subscribe"

INFO[0000] app is subscribed to the following topics: [deathStarStatus] through pubsub=redis-pubsub  app_id=subapp instance=127.0.0.1 scope=dapr.runtime type=log ver=1.0.1
WARN[0000] redis streams: BUSYGROUP Consumer Group name already exists  app_id=subapp instance=127.0.0.1 scope=dapr.contrib type=log ver=1.0.1
INFO[0000] dapr initialized. Status: Running. Init Elapsed 49.674504ms  app_id=subapp instance=127.0.0.1 scope=dapr.runtime type=log ver=1.0.1

接下來我們嘗試使用 dapr-cli 對我們之前啟動的 myapp 傳送訊息:

dapr publish --publish-app-id myapp --pubsub redis-pubsub --topic deathStarStatus --data '{"status": "completed"}'

在程式日誌中獲取到的輸出為:

== APP == [GIN] 2021/03/11 - 10:45:05 | 200 |      122.15µs |       127.0.0.1 | POST     "/dsstatus"

== APP == 2021/03/11 10:45:05 {"id":"9c237504-7cab-4a13-8582-92d9130fd016","source":"myapp","pubsubname":"redis-pubsub","traceid":"00-fba669a086f84650e882e3cadc55082c-ea466c080e359e68-00","data":{"status":"completed"},"specversion":"1.0","datacontenttype":"application/json","type":"com.dapr.event.sent","topic":"deathStarStatus"}

當然,除了pub/sub方式,我們也可以藉助 dapr 提供的路由功能,直接進行服務呼叫:

# curl http://127.0.0.1:3500/v1.0/invoke/subapp/method/dsstatus -X POST
{"success":true}

其他的元件功能則可以參考官方文件中描述進行配置即可。

總結

dapr 是一個功能強大的 serverless 執行時,除了上面提到的面向訊息和請求儲存的功能以外,還可以控制程式的 HTTP 請求與 gRPC 請求等等。除了這些功能外,還包含了服務的管理,還有可觀測性支援等功能,是一個非常有潛力的執行時選擇。