分布式应用运行时 Dapr:万物皆可 API
Dapr 分布式应用运行时 Distributed Application Runtime 的首字母缩写。有关多运行时,可以看下 Bilgin Ibryam 的 Multi-Runtime Microservices Architecture,不想看英文的可以看下我之前的翻译。
Dapr 是一个分布式系统工具包,通过提供 API 实现应用程序与外围组件的解耦合,让开发人员更加聚焦于业务逻辑的研发。解耦也是与传统 SDK 的很大区别,能力不再是通过应用程序中加入库的方式提供,而是通过应用附近的边车(sidecar)运行时提供(sidecar 不是广为人知的服务网格 sidecar - pod 中的容器,而是广泛使用在系统软件设计中的一种模式,比如操作系统的 initd
、日志采集组件,甚至是 Java 中的多线程。)。因此这里说的 Dapr sidecar 可能是个独立的进程,也可能是 pod 中的一个容器。
在 Dapr 中我们可以看到很多常见 SDK 的能力:
- 如 SpringCloud、Netflix OSS 的 服务调用,以及超时、熔断、重试等 弹性策略
- 如 Spring Data KeyValue 一样提供 状态存储 的抽象,简化各种持久存储的访问
- 如 Kafka、NATS、MQTT 等消息代理,提供 发布/订阅 抽象供服务通过消息进行通信
- 如 Kafka、MQTT、RabbitMQ 提供以事件触发应用的抽象:绑定
- 如 Redis 一样的 分布式锁
- 如 Consul、Kubernetes 等的 名称解析
- …
以上能力都是通过 HTTP 和 gRPC API 暴露给应用,这些 API 在 Dapr 中被叫做 构建块(building blocks),并且也 仅提供抽象,也就是说你可以随意替换底层实现(Dapr 中也叫做 组件)而无需修改任何应用代码。
比如你的应用需要在存储中保存状态,在开发时可以使用 内存 作为存储组件,其他环境中可以使用 Mysql、Redis 等持久化组件。
接下来,就借助官方的入门指南体验 Dapr 的。Dapr 提供了 多种入门指南,这里我选了其中的 hello-kubernetes,但实际操作可能与官方有些许差异,也正式这些差异能让(坑)我对 Dapr 有更多的了解。
环境
安装 Dapr CLI
Dapr CLI 是操作 Dapr 的工具,对可以用来安装、管理 Dapr 实例,以及进行 debug。参考官方的 安装文档,我使用的是 macOS 选择 homebrew 来安装。
brew install dapr-cli
目前最新的版本是 1.9.1。
dapr version
CLI version: 1.9.1
Runtime version: n/a
创建 Kubernetes 集群
使用 k3s v1.23.8+k3s2 作为实验环境集群。
export INSTALL_K3S_VERSION=v1.23.8+k3s2
curl -sfL https://get.k3s.io | sh -s - --disable traefik --disable servicelb --write-kubeconfig-mode 644 --write-kubeconfig ~/.kube/config
安装 Dapr
执行下面的命令将 Dapr 安装到集群中。
dapr init --kubernetes --wait
检查组件是否正常运行。在 Kubernetes 环境下,我们的很多命令都要使用 --kubernetes
或者 -k
参数。
dapr status -k
NAME NAMESPACE HEALTHY STATUS REPLICAS VERSION AGE CREATED
dapr-dashboard dapr-system True Running 1 0.11.0 47s 2023-02-11 08:30.25
dapr-sentry dapr-system True Running 1 1.9.6 47s 2023-02-11 08:30.25
dapr-sidecar-injector dapr-system True Running 1 1.9.6 47s 2023-02-11 08:30.25
dapr-operator dapr-system True Running 1 1.9.6 47s 2023-02-11 08:30.25
dapr-placement-server dapr-system True Running 1 1.9.6 47s 2023-02-11 08:30.25
示例应用
环境部署好之后,我们来看下要用的示例应用。
git clone https://github.com/dapr/quickstarts
cd quickstarts/tutorials/hello-kubernetes
示例中包含了 2 个应用 pythonapp
和 nodeapp
,以及 Redis。
nodeapp
提供 HTTP 端点来创建和查询订单,订单信息保存在 Redis 中pythonapp
会持续访问nodeapp
的 HTTP 端点来创建订单
用到了 Dapr 的两个功能:服务调用和状态存储。
创建应用命名空间
应用将部署在 dpar-test
命名空间下。
kubectl create namespace dapr-test
状态存储
状态存储使用 Redis,先部署 Redis 到命名空间 store
下。简单起见,只使用单 master 节点,并设置密码 changeme
。
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install redis bitnami/redis --namespace store --create-namespace \
--set replica.replicaCount=0 \
--set auth.password=changeme
创建组件
由于 Redis 设置了密码,需要为 Dapr 提供访问 Redis 的密码,通过 Secret 来传递。Secret 保存在 dapr-test
下。
kubectl create secret generic redis -n dapr-test --from-literal=redis-password=changeme
根据 Redis store 规范 在 dapr-test
下创建组件 statetore
:
- 组件类型
type
为state.redis
- 版本
version=v1
- 访问地址
redisHost=redis-master.store:6379
- Redis 的访问密码从秘钥
redis
的键redis-password
获取 auth.secretStore
指定秘钥存储的类型是Kubernetes
kubectl apply -n dapr-test -f - <<EOF
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: redis-master.store:6379
- name: redisPassword
secretKeyRef:
name: redis
key: redis-password
auth:
secretStore: kubernetes
EOF
访问状态存储
通过 Dapr API 访问状态存储,请求格式:POST http://localhost:<daprPort>/v1.0/state/<storename>
。
下面截取了 nodeapp
中的部分代码,stateStoreName
就是上面创建的 statestore
。应用和组件位于同一命名空间下,直接只用 statestore
;否则,就要代码组件所在的命名空间 storeName.storeNamespace
(由于代码中硬编码了组件名 statestore
,所以在同命名空间下创建组件)。
const stateStoreName = `statestore`;
const stateUrl = `http://localhost:${daprPort}/v1.0/state/${stateStoreName}`;
const state = [{
key: "order",
value: data
}];
const response = await fetch(stateUrl, {
method: "POST",
body: JSON.stringify(state),
headers: {
"Content-Type": "application/json"
}
});
服务调用
调用方 pythonapp
的代码。
- 通过 sidecar
daprd
的地址localhost
和端口3500
访问 HTTP API。 - 在请求头中通过
dapr-app-id
指定目标应用 idnodeapp
。应用 id 是通过 Kubernetes 注解dapr.io/app-id
来设置的,更多注解可参考 文档。 - 目标方法名通过请求路径来指定:
/neworder
dapr_port = os.getenv("DAPR_HTTP_PORT", 3500)
dapr_url = "http://localhost:{}/neworder".format(dapr_port)
n = 0
while True:
n += 1
message = {"data": {"orderId": n}}
try:
response = requests.post(dapr_url, json=message, timeout=5, headers = {"dapr-app-id": "nodeapp"} )
if not response.ok:
print("HTTP %d => %s" % (response.status_code,
response.content.decode("utf-8")), flush=True)
except Exception as e:
print(e, flush=True)
time.sleep(1)
部署应用
kubectl apply -n dapr-test -f deploy/node.yaml
kubectl wait --for=condition=ready pod -n dapr-test -l app=node --timeout=60s
kubectl apply -n dapr-test -f deploy/python.yaml
kubectl wait --for=condition=ready pod -n dapr-test -l app=python --timeout=60s
检查 node 容器的日志,可以接收到了来自 pythonapp
的请求,并成功持久化存储了订单。
kubectl logs -f -n dapr-test -l app=node -c node
Successfully persisted state for Order ID: 1
Got a new order! Order ID: 1
Successfully persisted state for Order ID: 2
Got a new order! Order ID: 2
Successfully persisted state for Order ID: 3
Got a new order! Order ID: 3
Successfully persisted state for Order ID: 4
Got a new order! Order ID: 4
Debug
原本官方的指南是将 Redis 和应用部署在同一个命名空间中,加上 nodeapp 中硬编码了存储组件名。而我实验的时候讲 Redis 部署在了另一个空间下,检查 node 容器日志时看到的是:
Got a new order! Order ID: 1
Failed to persist state.
daprd
容器中,只有下面的日志。
time="2023-02-11T02:55:38.166259509Z" level=info msg="HTTP API Called: POST /v1.0/state/statestore" app_id=nodeapp instance=nodeapp-857cf6f985-jnmzw scope=dapr.runtime.http-info type=log useragent="node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" ver=1.9.6
通过为 nodeapp
的 pod 添加注解 dapr.io/log-level="debug"
让 daprd
容器输出 debug 日志。
time="2023-02-11T03:05:07.663028821Z" level=debug msg="{ERR_STATE_STORE_NOT_CONFIGURED state store is not configured}" app_id=nodeapp instance=nodeapp-59b754ff54-c4x4s scope=dapr.runtime.http type=log ver=1.9.6
更多 Debug 方式,参考官方的 Troubleshooting 文档。
总结
Dapr 提供了与传统 SDK 方式完成不同的方法来实现系统集成,让开发者可以专注于业务逻辑,而无需考虑底层的实现;对组织来说,应用变得更加便携,可以使用不同的云环境。
但是 Dapr 本身无法跨云跨集群,社区正在考虑与服务网格集成来实现混合多云环境下的服务调用,大家可以期待一下。