Experience Dapr - Run on K8s


上一篇 整理了體驗 Dapr 在本機模式的 Hello World,Dapr 在官方文件稱為 Self-Hosted,這篇要體驗則是 Run on K8s


Preparation

K8s

在開始之前,請先確認以下:

  • K8s Cluster
    1. 有足夠的權限對 K8s Cluster 操作。
    2. 如果沒有 K8s 環境可以做 Lab,可以使用輕量化 K8s,像是 minikube、K3sK0s、K8s on Docker。
    3. 或者跟我一樣 自裝 K8s Cluster
  • K8s Controller / Client / GUI
    • K8s 的管理工具有很多,最基本的是 kubectl。
    • 重量級的則是 rancher,
    • 其他 GUI 像是 K8s lensoctant 也都不錯。
    • 我個人推薦比較輕量、但又容易上手的 k9s

確認:在開始之前,先確認你的 k8s config 是否有正確的 context 可以使用,可以透過 kubectl config current-context 知道現在狀況。

底下的筆記,我在 自裝 K8s Cluster 以及 K3s 上都跑過。

Setup: Dapr Control Plane

Dapr v1.0 預設所有的 cli 只要加 -k 參數,就會直接把同樣的動作,透過 K8s 權限直接 apply,所以跟上一篇的步驟都一樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
## 直接初始化
❯ dapr init -k
⌛ Making the jump to hyperspace...
↑ Downloading binaries and setting up components...
Dapr runtime installed to /Users/rick/.dapr/bin, you may run the following to add it to your path if you want to run daprd directly:
export PATH=$PATH:/Users/rick/.dapr/bin
✅ Downloaded binaries and completed components set up.
ℹ️ daprd binary has been installed to /Users/rick/.dapr/bin.
ℹ️ dapr_placement container is running.
ℹ️ dapr_redis container is running.
ℹ️ dapr_zipkin container is running.
ℹ️ Use `docker ps` to check running containers.
✅ Success! Dapr is up and running. To get started, go here: https://aka.ms/dapr-getting-started

順利完成後,可以發現多了一個叫做 dapr-system 的 namespace,查看狀況如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
~$ k get po,deploy,svc -n dapr-system
NAME READY STATUS RESTARTS AGE
pod/dapr-dashboard-79b6846f6c-v5vk5 1/1 Running 0 9d
pod/dapr-operator-7fb567cf76-78mv7 1/1 Running 0 9d
pod/dapr-placement-server-0 1/1 Running 0 9d
pod/dapr-sentry-6f46dd98c7-qpskp 1/1 Running 0 9d
pod/dapr-sidecar-injector-5fcd998fbb-vxprb 1/1 Running 0 9d

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/dapr-dashboard 1/1 1 1 9d
deployment.apps/dapr-operator 1/1 1 1 9d
deployment.apps/dapr-sentry 1/1 1 1 9d
deployment.apps/dapr-sidecar-injector 1/1 1 1 9d

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/dapr-api ClusterIP 10.98.196.60 <none> 80/TCP 9d
service/dapr-dashboard ClusterIP 10.98.17.33 <none> 8080/TCP 9d
service/dapr-placement-server ClusterIP None <none> 50005/TCP,8201/TCP 9d
service/dapr-sentry ClusterIP 10.108.116.38 <none> 80/TCP 9d
service/dapr-sidecar-injector ClusterIP 10.107.76.60 <none> 443/TCP 9d

上一篇 的本機比較起來,有著明顯的差異,在 k8s 上有以下的 pod 以及大致上的功能:

  • dashboard: 這個與本機透過 dapr dashboard 看到的東西一樣
  • operator: operator 會與 sidecar-injector 一起協作,用來依據 app 的 annotation 的宣告,然後動態在 pod 裡面放入 sidecar container,也就是 daprd (Dapr Daemon)
  • placement: 這個是負責處理 actor 的 building block ,actor pattern 是用來處理平行運算的分散式演算法。Dapr 實作 Virtual Actor pattern
  • sentry: Dapr 支援在 pod 之間的傳輸加密,使用的機制為 mTLS (mutual authentication TLS),透過一個叫做 Sentry 的角色作為 集中式 CA (Certificate Authority).
  • sidecar-injector: 與 operator 配合,動態置入 sidecar container 到 Pod 裡面。

關於 Actor Pattern 的資訊以及相關論文,整理如下:

上述的 pod 稱為 dapr control Plane

除了標準的初始化流程,也可以額外指定其他參數,整理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
## 指定 namespace
dapr init -k -n mynamespace

## 初始時,每個 pod 有三個
dapr init -k --enable-ha=true

## 關閉 mTLS, 預設是開啟的
dapr init -k --enable-mtls=false

## 移除 dapr control plane
dapr uninstall -k

# 透過 helm 安裝
helm repo add dapr https://dapr.github.io/helm-charts/
helm repo update

## 安裝 1.0.1
helm upgrade --install dapr dapr/dapr \
--version=1.0.1 \
--namespace dapr-system \
--create-namespace \
--wait

## 開啟多複本的 pod
helm upgrade --install dapr dapr/dapr \
--version=1.0.1 \
--namespace dapr-system \
--create-namespace \
--set global.ha.enabled=true \
--wait

## 移除
helm uninstall dapr --namespace dapr-system

Dapr 更版,例如從 v1.0.0 to v1.1.0:

1
2
3
4
5
6
7
8
9
10
11
12
~$ dapr upgrade --runtime-version 1.1.0 -k
ℹ️ Dapr control plane version 1.0.1 detected in namespace dapr-system
ℹ️ Starting upgrade...
✅ Dapr control plane successfully upgraded to version 1.1.0. Make sure your deployments are restarted to pick up the latest sidecar version.

~$ dapr status -k
NAME NAMESPACE HEALTHY STATUS REPLICAS VERSION AGE CREATED
dapr-operator dapr-system True Running 1 1.1.0 8m 2021-04-07 21:29.36
dapr-sidecar-injector dapr-system True Running 1 1.1.0 8m 2021-04-07 21:29.36
dapr-sentry dapr-system True Running 1 1.1.0 8m 2021-04-07 21:29.36
dapr-dashboard dapr-system True Running 1 0.6.0 8m 2021-04-07 21:29.35
dapr-placement-server dapr-system True Running 1 1.1.0 8m 2021-04-07 21:29.42

注意,Dapr APP 的 Sidecar 不會自動更版,但只要重新建立 Pod 就 sidecar-injector 就會發配新版。


Hello World - Run on K8s

直接開始主題,同樣使用 官方的範例,下圖是官方提供的架構圖。

一些基礎相關資訊請參考 上一篇的介紹

我在圖中註記 Pod A, Pod B, Pod C,分別摘要說明:

  1. Pod A: Dapr.Operator: 是放在 dapr-system ns 裡的 Pod,與 sidecar-injector 配合,動態在 pod 裡面配置 sidecar container
  2. Pod B: DaprApp.PythonApp: 模擬 Client 的應用程式,使用 python
  3. Pod C: DaprApp.NodeApp: 主要的訂單處理與狀態紀錄
    • 應用程式:使用 node.js 開發
    • 狀態管理:透過 Dapr 的 state management (building block 的一種),在 K8s 則透過自定義 (CRD) 的 component 宣告,使用 redis。這段後面整理詳細概念。
  4. dapr-system: 這個 ns 是一開始 dapr init -k 建立的 ns
  5. dapr-lab02: 這是這個 lab 的 app 預定放置的 ns,包含 redis

Pod B, Pod C 都有額外的 Dapr Runtime Container,也就是前面提到的 sidecar。

安裝步驟

1. 準備 redis

我直接在 rancher 上透過 application management 安裝。

2. 定義 component

Dapr 透過 K8s 的 Component 宣告有哪一些 Building Block 要使用,然後 APP 可以直接透過 Dapr Runtime 的 API 對 Component 的實際資源做操作。

底下是官方提供的 Component 定義,要注意的是 redisHost 要指對,如果錯了 Pod 會起不來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
namespace: dapr-lab02
name: statestore
spec:
type: state.redis
version: v1
metadata:
# These settings will work out of the box if you use `helm install
# bitnami/redis`. If you have your own setup, replace
# `redis-master:6379` with your own Redis master address, and the
# Redis password with your own Secret's name. For more information,
# see https://docs.dapr.io/operations/components/component-secrets .
- name: redisHost
value: redis-master.dapr-lab02.svc.cluster.local:6379
# - name: redisPassword
# secretKeyRef:
# name: redis
# key: redis-password
auth:
secretStore: kubernetes

3. apply deployments, service

官方的範例有兩個 services: nodeapp, pythonapp,依照官方文件步驟 apply 即可。

1
2
kubectl apply -f ./deploy/node.yaml
kubectl apply -f ./deploy/python.yaml

這兩個 yaml 內容要特別說明的是這段 annotations:

1
2
3
4
5
6
7
8
template:
metadata:
labels:
app: node
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "nodeapp"
dapr.io/app-port: "3000"

Dapr control plane 會依據這段 annotations:

  1. 動態增加 Dapr sidecar 到 pod 裡
  2. 同時增加一個叫做 <appneme>-dapr 的 Headless Service (No ClusterIP)

4. 檢查

完成部署後,檢查會看到以下資源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
~$ k get po,deploy,svc,components

NAME READY STATUS RESTARTS AGE
pod/nodeapp-d5b697767-62js6 2/2 Running 0 33h
pod/pythonapp-7797554b99-jxs7q 2/2 Running 0 33h
pod/redis-master-0 1/1 Running 0 33h
pod/zipkin-789f9f965f-9zq49 1/1 Running 5 35h

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nodeapp 1/1 1 1 35h
deployment.apps/pythonapp 1/1 1 1 35h
deployment.apps/zipkin 1/1 1 1 35h

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nodeapp-dapr ClusterIP None <none> 80/TCP,50001/TCP,50002/TCP,9090/TCP 35h
service/nodeapp-service ClusterIP 10.102.6.181 <none> 80/TCP 35h
service/pythonapp-dapr ClusterIP None <none> 80/TCP,50001/TCP,50002/TCP,9090/TCP 35h
service/redis-headless ClusterIP None <none> 6379/TCP 35h
service/redis-master ClusterIP 10.96.235.86 <none> 6379/TCP 35h
service/zipkin ClusterIP 10.101.186.178 <none> 9411/TCP 35h

NAME AGE
component.dapr.io/statestore 35h


## 透過 dapr cli 看 dapr app
~$ dapr list -k
APP ID APP PORT AGE CREATED
nodeapp 3000 1d 2021-03-18 23:06.06
pythonapp 1d 2021-03-18 23:35.20

過程可能會遇到因為 docker rate limit 問題而無法建立 Container: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit

觀察上述的 Pod 與 Service 的內容:

  1. Pod 裡面除了 NodeAPP, PytonnApp 之外,他們都自動產生一個 Dapr Sidecar,如下圖:
  2. 如前述,自動產生名為 nodeapp-dapr, pythonapp-dapr 的 Headless Service.

上述 pod 的結構如下圖:

圖片來源:Dapr at 20,000 feet

最後可以透過 dapr dashboard 看看,先確認右上角的 scope 是正確的 ns,然後 overview 會看到兩個 Dapr Applications,如下圖:

測試

用以下位址測試 NodeApp,順利的話,會回傳 DAPR 的 HTTP / gRPC 的 Ports。

1
2
3
4
~$ kubectl port-forward service/nodeapp-service 8080:80
~$ curl http://localhost:8080/ports

{"DAPR_HTTP_PORT":"3500","DAPR_GRPC_PORT":"50001"}%

pythonapp 是模擬 下訂單的服務,每秒會對 nodeapp 的 /neworder 打一次,所以直接對 node container 看 log 會看到以下訊息:

1
2
3
4
5
6
~$ k logs -f nodeapp-d5b697767-v9tms
Node App listening on port 3000!
Got a new order! Order ID: 4714
Successfully persisted state.
Got a new order! Order ID: 4715
Successfully persisted state.

我這已經跑一段時間了,所以序號已經跑到 47xx 了 XDD

如果出現以下訊息,則表示 redis 有問題,component 定義沒問題:

1
2
3
4
5
~$ k logs -f nodeapp-d5b697767-62js6 node
Got a new order! Order ID: 5375
Failed to persist state.
Got a new order! Order ID: 5376
Failed to persist state.

component 定義有問題,則 pod 會起不來。但如果 pod 啟動後,redis 壞了,則會出現上述問題,同時無法再長新的 pod,如下:

1
2
3
4
5
6
~$ k get po
NAME READY STATUS RESTARTS AGE
nodeapp-d5b697767-62js6 2/2 Running 0 3m10s
nodeapp-d5b697767-c4vvh 1/2 Error 0 24s
pythonapp-7797554b99-5vm6c 2/2 Running 0 100m
zipkin-789f9f965f-9zq49 1/1 Running 0 112

完整使用情境:下訂單、取得訂單

發送一個 POST 給 Application,如下:

1
2
3
4
5
6
7
8
9
10
11
12
## 啟動 forward
~$ kubectl port-forward service/nodeapp-service 8080:80

## 下訂單
~$ curl -X POST \
-H "Content-Type: application/json" \
-d '{"data": { "orderId": "42" } }' \
http://localhost:8080/neworder

## 取得訂單
curl http://localhost:8080/order
{"orderId":42}

上述的進入點和 Lab01 是不一樣的。

進入點的差異

在了解架構時,清楚 E2E 經過的路徑很重要,在 系統發生異常時,第一時間如何快速止血? 一文中的第一點就提到這樣的思路。

所以如果有跟著上一篇做過一次,就會發現這次打的 endpoint 路徑跟上次的不一樣,整理兩次的進入點 (entry point),底下是第一次的 Lab01 - Self-Hosted:

底下這張圖是這次的 Run on K8s Lab02 的進入點:

我嘗試把 Lab02 測試,調整到 Dapr sidecar,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 啟動 forward
~$ kubectl port-forward service/nodeapp-dapr 8080:80

## 下訂單
~$ curl -X POST \
-H "Content-Type: application/json" \
-d '{"data": { "orderId": "43" } }' \
http://localhost:8080/v1.0/invoke/nodeapp/method/neworder


## 取得 dapr app metadata
curl http://localhost:8080/v1.0/metadata

## 測試 ports 資訊
curl http://localhost:8080/v1.0/invoke/nodeapp/method/ports

## 取得訂單
curl http://localhost:8080/v1.0/invoke/nodeapp/method/order

結果是不行的,因為在 K8s 裡的 Dapr Sidecar Service,預設 Headless Service,所以不會有 ClusterIp,透過 Ingress 就可以存取。如果是使用 minikube 記得把 ingress 打開。

Q&A

Q1: Run on K8s 的 外部 請求,應該打 NodeApp 的 Service ?還是打 Dapr Sidecar?

可以,只要 Ingress 指定到 <appid>-dapr service 的 80 ,然後就可以打 Dapr API,例如 /v1.0/metadata 可以當 health check.

這牽涉到整體架構 規劃設計 問題,以目前 Dapr 的設計 Sidecar 是內部通訊使用的,外部通訊則還是透過 Ingress。

  • 規劃: 指的是整個服務的應用對象與場景
  • 設計: 是能否滿足應用場景的需求能力。
    這段後面有個更完整應用案例整理。

Q2: 如何針對 Dapr App 做 Health Check?

參考 Dapr 官方文件:Sidecar healthz

Q3: 怎麼配置 sidecar 的設定?範圍是怎麼圈的?

Dapr 每個應用程式都可以有自己的 Sidecar ConfigControl-plane 的 Configuration,詳細參閱 Dapr configuration options

Q4: Sidecar 透過 annotation 設定,有哪一些可以使用?

參閱 Dapr Kubernetes pod annotations spec

Q5: K8s Service / Ingress 可以直接接到 Dapr Sidecar Service?

可以。
實際使用情境同 Q1:Dapr Sidecar 的用途是 App 之間溝通使用的,也就是內部通訊的介面,對外的 入口還是會直接到 app 而不會經過 sidecar。更實際的案例可以參考這個: eShopOnDapr

Q6: mTLS 是 E2E 外部的資料加密?

他是 Dapr Instance 之間的通訊加密,不是 E2E。官方說明:Dapr supports in-transit encryption of communication between Dapr instances using Sentry, a central Certificate Authority。詳細參閱 Sidecar-to-sidecar communication

Q7: Dapr Sidecar 都沒有 log,怎麼 debug?

目前官方文件沒找到可以調整的。
20210407 Updated: v1.1.0 會提供 debug flag, 參閱 K8S debug support #3008

Q8: dapr 的 sidecar 是從 docker.io 抓,遇到 rate limit 怎麼辦?

底下討論串有這議題,看起來暫時是: ImagePullPolicy for Dapr sidecar Docker image from Allways to IfNotPresent

上面 Issue 已經在 1.0.0M2 Merge PR, Source Code 也 Merge 了,但是文件沒有提到如何設定。

但是在 Dapr Control Plane 的 dapr-sidecar-injector,環境變數有找到以下變數可以設定:

  • SIDECAR_IMAGE_PULL_POLICY: v1.0.0 預設是 Always,v1.1.0 改成 IfNotPresent
  • SIDECAR_IMAGE: 可以改成自己的 private registry.

Troubleshooting

Q: Dapr App 的 Pod 起不來?

先到 Pod 裡面看看是否每個 Container 都正常,如果是 daprd 有問題,那可能是 Component 定義的 redisHost 問題,可以從 log 中確認狀況,如下:

1
2
3
4
5
6
daprd time="2021-03-18T14:47:30.141871173Z" level=warning msg="error initializing state store statestore (state.redis/v1): redis store: err
│ or connecting to redis at redis-master.dapr-lab2.svc.cluster.local:6379: dial tcp: lookup redis-master.dapr-lab2.svc.cluster.local on 10.96
│ .0.10:53: no such host" app_id=nodeapp instance=nodeapp-d5b697767-s9k5s scope=dapr.runtime type=log ver=1.0.1
│ daprd time="2021-03-18T14:47:30.141916107Z" level=fatal msg="process component statestore error: redis store: error connecting to redis at
│ redis-master.dapr-lab2.svc.cluster.local:6379: dial tcp: lookup redis-master.dapr-lab2.svc.cluster.local on 10.96.0.10:53: no such host" ap
│ p_id=nodeapp instance=nodeapp-d5b697767-s9k5s scope=dapr.runtime type=log ver=1.0.1

確認問題,把 component 定義檔修好,長新的 pod 應該就好了。


小結

這個案例的過程,帶出 Dapr 設計背後的意圖,也就是解決大型分散式系統的核心問題:

複雜度

降低複雜度的原則是 分而治之,Dapr 最核心的概念就是 Sidecar Pattern + Building Block,如下圖:


圖片來源:Dapr at 20,000 feet

  • Sidecar Pattern: 透過職責分離與 Container 的隔離特性,降低應用程式的複雜度。更多 Container 隔離的概念參閱: 為什麼要使用容器?Java Obsolete in the Age of Docker
  • Building Block: 類似於積木組裝的想法,透過 Dapr 提供的核心組件 (Component),分離與抽象化系統架構。

下一篇 將整理 Dapr 核心的重要設計概念與實踐。


延伸閱讀

站內文章

參考資料




Comments