• 还在篮子里

    分布式应用运行时 Dapr X Kubernetes · 语雀

    分布式应用运行时 Dapr X Kubernetes

    前言

    我前不久体验了 Dapr 的单机使用,体验不错。不过Dapr 作为一个分布式应用的运行时,当然需要加入集群使用才能完全体验呀。我们使用 GitHub 上的文档 部署。首先我们需要一个 Kubernetes 集群来做实验,选择 Rancher 由于我比较熟悉 Rancher ,同时 UI 操作才能让人感受到工具的便利嘛。之后会使用 RancherHelm 部署 DaprRedis 图表,不过由于 UI 操作的便利,点一下就部署了。

    集群的环境部署比较长,不过本来这些工具就是部署之后一劳永逸降低心智负担,当然你有一个 Kubernetes 可以直接实验就更好了。

    下面是 架构 图

    image.png

    可以看到我们已经接触到了 Dapr 多语言的特性了。两个 Pod 包括不同语言的应用:Python 和 Node.js ,他们通过 Dapr Injects 的 sidecar 交互。

    那么我们开始吧!

    环境

    Kubernetes 部署

    Docker

    1$ yum install docker-ce-18.06.2.ce-3.el7 -y
    此处为语雀文档,点击链接查看:https://www.yuque.com/opte0v/shu93f/gss3ca

    Rancher

    1$ sudo docker run -d --restart=unless-stopped -p 80:80 -p 443:443 rancher/rancher:latest
    此处为语雀文档,点击链接查看:https://www.yuque.com/opte0v/shu93f/td5aq3

    K8S By Rancher

    此处为语雀文档,点击链接查看:https://www.yuque.com/opte0v/shu93f/k8sbyrancher

    你也可以使用其他的工具快速启动 Kubernetes 集群

    可选部署 Kubernetes 方法

    Dapr

    1. Using Helm(Advanced)

    添加 Dapr 源 https://daprio.azurecr.io/helm/v1/repo 到商店

    image.png

    1. 启动 Dapr

    image.png

    1. 默认配置即可

    image.png

    1. 等待一段时间完成

    image.png

    你可以选择其他方法安装 Dapr

    Using Dapr CLI【可选】官方文档

    Redis

    1. 同样在 Rancher 的商店中找到 Redis Chart,默认部署即可。(可以自己设定一些选项比如密码和访问端口等方便你接下来的操作)

    image.png

    1. 等待一会儿部署完毕

    image.png

    1. 使用 Rancher 自带的 kubectl 即可执行命令

    image.png

    1. 使用 以下命令获取到 redis 命名空间下设置的密码

    kubectl get secret --namespace redis redis -o jsonpath="{.data.redis-password}" | base64 --decode

    image.png

    可以看到密码是 123456(方便实验,内网环境,生产环境请使用生成密码)

    1. 找到 Redis 的访问端口,我的是 192.168.0.116:32091(这是随机的端口)

    image.png

    1. 接下来我们把这些值置换到官方给的模板里的

    Configuration

    这里是明文存储的 secret ,生产环境请参照.secret management .

    Configuring Redis for State Persistence and Retrieval

    在刚才的 kubectl 命令行中继续操作。

    1$ vi redis-state.yaml

    记得将上文 4 ,5 步骤得到的 HOST 地址 和 Password 密码替换.

     1apiVersion: dapr.io/v1alpha1
     2kind: Component
     3metadata:
     4  name: statestore
     5spec:
     6  type: state.redis
     7  metadata:
     8  - name: redisHost
     9    value: <HOST>
    10  - name: redisPassword
    11    value: <PASSWORD>

    同时这个文件也可以在 samples/2.hello-kubernetes/deploy 下找到

    我的 Yaml 文件

    Apply the configuration

    使用 kubectl apply 部署。

    1$ kubectl apply -f redis-state.yaml
    2$ kubectl apply -f redis-pubsub.yaml

    HelloKubernetes

    Download Code

    首先下载示例代码

    1$ git clone https://github.com/dapr/samples.git
    2$ cd samples/2.hello-kubernetes/node

    Node

    该示例有多个应用,首先我们看老朋友 Node.js 的代码

    Cat app.js

     1// $ cat app.js 
     2
     3const express = require('express');
     4const bodyParser = require('body-parser');
     5require('isomorphic-fetch');
     6
     7const app = express();
     8app.use(bodyParser.json());
     9
    10const daprPort = process.env.DAPR_HTTP_PORT || 3500;
    11const daprUrl = `http://localhost:${daprPort}/v1.0`;
    12const port = 3000;
    13
    14app.get('/order', (_req, res) => {
    15    fetch(`${daprUrl}/state/order`)
    16        .then((response) => {
    17            return response.json();
    18        }).then((order) => {
    19            res.send(order);
    20    });
    21});
    22
    23app.post('/neworder', (req, res) => {
    24    const data = req.body.data;
    25    const orderId = data.orderId;
    26    console.log("Got a new order! Order ID: " + orderId);
    27
    28    const state = [{
    29        key: "order",
    30        value: data
    31    }];
    32
    33    fetch(`${daprUrl}/state`, {
    34        method: "POST",
    35        body: JSON.stringify(state),
    36        headers: {
    37            "Content-Type": "application/json"
    38        }
    39    }).then((response) => {
    40        console.log((response.ok) ? "Successfully persisted state" : "Failed to persist state");
    41    });
    42
    43    res.status(200).send();
    44});
    45
    46app.listen(port, () => console.log(`Node App listening on port ${port}!`));

    可以看到这个和我们之前使用的单机的 Node.js 一样,可以参照我的这篇文章

    Node Application Explain

    此处为语雀文档,点击链接查看:https://www.yuque.com/abser/process/fa0ntp#zjdNP

    Deploy Node Application

    虽然代码一样,但是我们部署的方法变了,不是直接使用 dapr 进行部署,而是使用 kubectl(注意我们当前目录还在 samples/2.hello-kubernetes/node

    1$ kubectl apply -f ../deploy/node.yaml
    2
    3service/nodeapp created
    4deployment.apps/nodeapp created

    如果你好奇,我们可以看一看 node.yaml 的内容(我把代码块调小了,可以自己滑动查看)

     1kind: Service
     2apiVersion: v1
     3metadata:
     4  name: nodeapp
     5  labels:
     6    app: node
     7spec:
     8  selector:
     9    app: node
    10  ports:
    11  - protocol: TCP
    12    port: 80
    13    targetPort: 3000
    14  type: LoadBalancer
    15
    16---
    17apiVersion: apps/v1
    18kind: Deployment
    19metadata:
    20  name: nodeapp
    21  labels:
    22    app: node
    23spec:
    24  replicas: 1
    25  selector:
    26    matchLabels:
    27      app: node
    28  template:
    29    metadata:
    30      labels:
    31        app: node
    32      annotations:
    33        dapr.io/enabled: "true"
    34        dapr.io/id: "nodeapp"
    35        dapr.io/port: "3000"
    36    spec:
    37      containers:
    38      - name: node
    39        image: dapriosamples/hello-k8s-node
    40        ports:
    41        - containerPort: 3000
    42        imagePullPolicy: Always
    

    注意这两个选项,    dapr.io/enabled: "true" 告诉 Dapr 控制器注入边车

     dapr.io/id: "nodeapp" 告诉 Dapr 这个部署的唯一 ID(用来 Dapr 之间相互通信)

    现在我们来查看状态和外部 IP(外部 IP 在 pending 是因为我们使用 Rancher 的原因,4 层负载均衡并不是每一个运营商都支持,不过外部 IP 不影响本次 Demo, 我会在下面做一个 Ingress 访问它

    1$ kubectl get svc nodeapp
    2NAME      TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
    3nodeapp   LoadBalancer   10.43.255.238   <pending>     80:30557/TCP   5m17s

    将 NodeApp 导入环境变量【跳过:由于我们没有 EXTERNAL-IP 】

    1$ export NODE_APP=$(kubectl get svc nodeapp --output 'jsonpath={.status.loadBalancer.ingress[0].ip}')

    Python

    我们还有一个 Python 的 Application 需要部署。

    1$cd samples/2.hello-kubernetes/python 
    2$cat app.py

    Python 的代码很简单,他把 JSON 消息发到 localhost:3500 (这个端口是可变的,3500Dapr 默认端口) 调用 Node App 的 neworder endpoint 。我们的消息也是很简单的每秒递增一次的消息。

     1import time
     2import requests
     3import os
     4
     5dapr_port = os.getenv("DAPR_HTTP_PORT", 3500)
     6dapr_url = "http://localhost:{}/v1.0/invoke/nodeapp/method/neworder".format(dapr_port)
     7
     8n = 0
     9while True:
    10    n += 1
    11    message = {"data": {"orderId": n}}
    12
    13    try:
    14        response = requests.post(dapr_url, json=message)
    15    except Exception as e:
    16        print(e)
    17
    18    time.sleep(1)

    部署并等待 Pod 进入 Running 状态

    1$ kubectl apply -f ./deploy/python.yaml
    2$ kubectl get pods --selector=app=python -w
    3
    4NAME                        READY   STATUS              RESTARTS   AGE
    5pythonapp-b5fd4474d-tk84x   0/2     ContainerCreating   0          9s
    6pythonapp-b5fd4474d-tk84x   1/2     ErrImagePull        0          38s
    7pythonapp-b5fd4474d-tk84x   1/2     ImagePullBackOff    0          39s
    8pythonapp-b5fd4474d-tk84x   2/2     Running             0          3m24s

    Observe

    我们毕竟使用的是 UI 界面,现在我们可以通过 Rancher UI 检验一下我们的成果。

    可以看到 nodeapppythonapp 两个 Pod 都在 active 状态。同时我们点进入更多信息界面可以看到 Pod 中还有 Dapr 的 边车

    image.png

    我们进入 nodeapp 检查日志

    image.png

    或者使用命令行

    1$ kubectl logs --selector=app=node -c node
    2
    3Got a new order! Order ID: 1
    4Successfully persisted state
    5Got a new order! Order ID: 2
    6Successfully persisted state
    7Got a new order! Order ID: 3
    8Successfully persisted state

    Persistence

    当然,我们不能使用 LoadBalance 或者 外部 IP 并不能阻止我们检查持久性。

    我们可以通过 Rancher 添加一个 Ingress 七层负载均衡到 目标容器进行访问。

    image.png

    然后设置 nodeapp 的端口 3000

    image.png

    等待一会儿出现分配的地址 http://nodeapp.default.192.168.0.102.xip.io

    image.png

    浏览器或者命令行访问,会返回最新的 orderID

    1$ curl http://nodeapp.default.192.168.0.102.xip.io/order
    2{"orderID":"42"}

    Clean

    这会清理所有的东西,包括 状态组件。

    1$ cd samples/2.hello-kubernetes/deploy
    2$ kubectl delete -f .

    image.png

    Summary

    首先使用 Dapr 的开发感受还需要一步,就是修改代码之后编译为自己的 Docker 然后部署在 Kubernetes 上,这里不详述,留给读者当练手即可。引用官方的 Next Steps 大概导引一下。

    Next Steps

    Now that you’re successfully working with Dapr, you probably want to update the sample code to fit your scenario. The Node.js and Python apps that make up this sample are deployed from container images hosted on a private Azure Container Registry. To create new images with updated code, you’ll first need to install docker on your machine. Next, follow these steps:

    1. Update Node or Python code as you see fit!
    2. Navigate to the directory of the app you want to build a new image for.
    3. Run docker build -t <YOUR_IMAGE_NAME> . . You can name your image whatever you like. If you’re planning on hosting it on docker hub, then it should start with <YOUR_DOCKERHUB_USERNAME>/.
    4. Once your image has built you can see it on your machines by running docker images.
    5. To publish your docker image to docker hub (or another registry), first login: docker login. Then rundocker publish <YOUR IMAGE NAME>.
    6. Update your .yaml file to reflect the new image name.
    7. Deploy your updated Dapr enabled app: kubectl apply -f <YOUR APP NAME>.yaml.

    这一次的 Dapr 体验遇到的一些问题主要是集群本身的网络问题。本身 Dapr 在这个 Demo 中起到的就是通信和存储状态的服务,但是在实际使用中并没有看到非常多的日志可供调试。也就是说依旧需要专业的运维人员进行 Dapr 开发的维护。不过,本来 Dapr 的打算就是制定标准,划开开发人员和运维人员的界限降低心智负担。

    Demo 中是两个语言的应用,我在查看日志的时候看了下环境变量,看到了 GRPC 的 50001 端口。同时 Dapr 也提供 各个语言的 SDK。我们看过了两个应用的代码,都很简单,不过可以看到的是对于通信的感知是不大的,或者说对于 Dapr 的感知不大,意味着微服务改为 Dapr 的并不需要改动太多,就是服务的 URL 变动一下。剩余的交给 Dapr 的 Sidecar 就行了。

    如果你不选择 Rancher 作为 搭建 Kubernetes 的集群的工具的话,跳转 也有其他的教程指导。没有一些非常明白好看的 UI 了。Rancher 的可视化部署状态在这个 Demo 中起到了很大的作用。

    接下来对于 Dapr 会尝试一下 他的事件驱动特性和 Pub/Sub 。这对于分布式应用来说也比较好玩。我非常赞同边车的开发模式,说到 sidecar 很多人都会想到 Istio 和 envoy,我也是一开始因为 ServiceMesh 和 sidecar 才开始关注 Dapr 的。刚看到开源的消息到现在,GitHub stars 已经到 4k 了,不过一天时间涨了 1k 多。微软的能量还是挺大的。

    Refer

    来源: 分布式应用运行时 Dapr X Kubernetes · 语雀

  • 还在篮子里

    Dapr HelloWorld · 语雀

    Dapr HelloWorld

    Dapr

    Distributed Application Runtime. An event-driven, portable runtime for building microservices on cloud and edge.

    分布式应用运行时、事件驱动、为云和边缘构建微服务提供便携化运行时。

    我现在也不是很懂。

    dapr/dapr

    GitHub

    Dapr is a portable, serverless, event-driven runtime that makes it easy for developers to build resilient, stateless and stateful microservices that run on the cloud and edge and embraces the diversity of languages and developer frameworks.

    Dapr codifies the best practices for building microservice applications into open, independent, building blocks that enable you to build portable applications with the language and framework of your choice. Each building block is independent and you can use one, some, or all of them in your application.

    比上面的介绍多了 stateless or stateful 的标签。学《计算理论》的时候接触过一些状态机。

    ”状态是万恶之源“

    注意提到了多语言和多开发者框架,我认为这是他选择的通过通信共享信息,即 HTTPGRPC 支持多语言等特性。微软想通过这个设定一个构建微服务应用的规则。从根本上确立你开发的每一个应用的独立性。

    下面进行一个 QuickStart

    环境

    1. Install Docker(微服务已经离不开容器化了)
    2. Install Dapr
    3. Node.js version 8 or greater(这个 Helloworld 是 node 应用)

    On macOS

    Install the latest darwin Dapr CLI to /usr/local/bin

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

    有条件可以加速

    执行初始化(会启动 docker 容器)

    1$ dapr init
    2⌛  Making the jump to hyperspace...
    3Downloading binaries and setting up components
    4✅  Success! Dapr is up and running
    5
    6$ docker ps
    7CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                        NAMES
    8b3a5600e672f        redis                "docker-entrypoint.s…"   44 hours ago        Up 44 hours         0.0.0.0:6379->6379/tcp       xenodochial_hofstadter
    9e5010ba0c33f        daprio/dapr          "./placement"            44 hours ago        Up 44 hours         0.0.0.0:50005->50005/tcp     dapr_placement

    HelloWorld

    Application Architecture

    能够看到暴露两个 endpoint 是 HTTP 访问,一个创建一个查询。

    主要看我们使用 Dapr 的交互。在图中它作为 Runtime

    • 提供 Dapr API 给多语言调用。
    • 提供 状态管理 By state stores

    Download Code

    下载并进入相应文件夹

    1git clone https://github.com/dapr/samples.git
    2cd samples/1.hello-world

    Cat app.js

     1// $ cat app.js 
     2// ------------------------------------------------------------
     3// Copyright (c) Microsoft Corporation.
     4// Licensed under the MIT License.
     5// ------------------------------------------------------------
     6
     7const express = require('express');
     8const bodyParser = require('body-parser');
     9require('isomorphic-fetch');
    10
    11const app = express();
    12app.use(bodyParser.json());
    13
    14const daprPort = process.env.DAPR_HTTP_PORT || 3500;
    15const stateUrl = `http://localhost:${daprPort}/v1.0/state`;
    16const port = 3000;
    17
    18app.get('/order', (_req, res) => {
    19    fetch(`${stateUrl}/order`)
    20        .then((response) => {
    21            return response.json();
    22        }).then((orders) => {
    23            res.send(orders);
    24        });
    25});
    26
    27app.post('/neworder', (req, res) => {
    28    const data = req.body.data;
    29    const orderId = data.orderId;
    30    console.log("Got a new order! Order ID: " + orderId);
    31
    32    const state = [{
    33        key: "order",
    34        value: data
    35    }];
    36
    37    fetch(stateUrl, {
    38        method: "POST",
    39        body: JSON.stringify(state),
    40        headers: {
    41            "Content-Type": "application/json"
    42        }
    43    }).then((response) => {
    44        console.log((response.ok) ? "Successfully persisted state" : "Failed to persist state");
    45    });
    46
    47    res.status(200).send();
    48});
    49
    50app.listen(port, () => console.log(`Node App listening on port ${port}!`));

    这是一些路由和 handlers

    注意 14-16 行

    1const daprPort = process.env.DAPR_HTTP_PORT || 3500;
    2const stateUrl = `http://localhost:${daprPort}/v1.0/state`;
    3const port = 3000;

    3500 是 Dapr 的环境端口,如果你安装时有改动,需要考虑。

    stateurl 就是 Dapr 提供的 URL 了

    Handlers

    /neworder
     1app.post('/neworder', (req, res) => {
     2    const data = req.body.data;
     3    const orderId = data.orderId;
     4    console.log("Got a new order! Order ID: " + orderId);
     5
     6    const state = [{
     7        key: "order",
     8        value: data
     9    }];
    10
    11    fetch(stateUrl, {
    12        method: "POST",
    13        body: JSON.stringify(state),
    14        headers: {
    15            "Content-Type": "application/json"
    16        }
    17    }).then((response) => {
    18        console.log((response.ok) ? "Successfully persisted state" : "Failed to persist state");
    19    });
    20
    21    res.status(200).send();
    22});

    这里重点是状态存储,即将 state 通过 stateurl 存储在 Dapr 中。

    /order

    我们并不是直接通过 res.json 作为 Response 来进行已经持久化的数据的使用,而是通过暴露一个 GET endpoint 通过访问它来验证持久化是否成功。

    1app.get('/order', (_req, res) => {
    2    fetch(`${stateUrl}/order`)
    3        .then((response) => {
    4            return response.json();
    5        }).then((orders) => {
    6            res.send(orders);
    7        });
    8});

    现在我们通过状态转移在 Dapr 里实现了 stateless,同样我们也可以在加上一个 local cache 并通过一个新的 endpoint 访问来使 Node application 变成 stateful

    Dapr Run Node.js App

    1. npm install :通过当前目录下的 package.json , 会安装 express 和 body-parser ,在 app.js 7-8行我们可以看到这两项。
    2. dapr run --app-id mynode --app-port 3000 --port 3500 node app.js
    1$ dapr run --app-id mynode --app-port 3000 --port 3500 node app.js
    2ℹ️  Starting Dapr with id mynode. HTTP Port: 3500. gRPC Port: 55099
    3✅  You're up and running! Both Dapr and your app logs will appear here.

    应该是有后台运行的 CLI 命令,这里是前台打印的日志

     1== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="starting Dapr Runtime -- version 0.1.0 -- commit 4358565-dirty"
     2== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="log level set to: info"
     3== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="standalone mode configured"
     4== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="dapr id: mynode"
     5== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="loaded component messagebus (pubsub.redis)"
     6== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="loaded component statestore (state.redis)"
     7== DAPR == time="2019-11-06T10:37:41+08:00" level=info msg="application protocol: http. waiting on port 3000"
     8== APP == Node App listening on port 3000!
     9== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="application discovered on port 3000"
    10== DAPR == 2019/11/06 10:37:42 redis: connecting to localhost:6379
    11== DAPR == 2019/11/06 10:37:42 redis: connected to localhost:6379 (localAddr: [::1]:55130, remAddr: [::1]:6379)
    12== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actor runtime started. actor idle timeout: 1h0m0s. actor scan interval: 30s"
    13== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: starting connection attempt to placement service at localhost:50005"
    14== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="http server is running on port 3500"
    15== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="gRPC server is running on port 55099"
    16== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="local service entry announced"
    17== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="dapr initialized. Status: Running. Init Elapsed 945.8297490000001ms"
    18== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: established connection to placement service at localhost:50005"
    19== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: placement order received: lock"
    20== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: placement order received: update"
    21== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: placement tables updated"
    22== DAPR == time="2019-11-06T10:37:42+08:00" level=info msg="actors: placement order received: unlock"

    ⚠️:注意到 Node App 在指定的 3000 端口运行,同时还有状态存储的 redis6379 端口运行

    Post and Get

    接下来注意,文中的端口是 app.js 里默认的 3500

    Post

    Curl
    1curl -XPOST -d @sample.json http://localhost:3500/v1.0/invoke/mynode/method/neworder
    vscode

    如果你用 vscode ,使用这个插件 Rest Client Plugin

    然后打开目录下的 sample.http , 可以看到 send request 的选项

    sample.http

    1POST http://localhost:3500/v1.0/invoke/mynode/method/neworder
    2{
    3  "data": {
    4    "orderId": "42"
    5  } 
    6}
    Postman

    如图: http://localhost:3500/v1.0/invoke/mynode/method/neworder 

    Result Update

    你可以在你启动的终端中看到新的日志

    1== APP == Got a new order! Order ID: 42
    2== APP == Successfully persisted state

    Get

    Curl
    1curl http://localhost:3500/v1.0/invoke/mynode/method/order
    Vscode

    sample.http

    1GET http://localhost:3500/v1.0/invoke/mynode/method/order
    Postman

    image.png

    Terminate

    ctrl + c 或者 dapr stop --app-id mynode

    1^C
    2ℹ️  terminated signal received: shutting down
    3✅  Exited Dapr successfully
    4✅  Exited App successfully

    Feature

    • 具有可插入提供程序和至少一次语义的事件驱动的Pub-Sub系统
    • 使用可插入提供程序的输入和输出绑定
    • 具有可插拔数据存储的状态管理
    • 一致的服务到服务发现和调用
    • 选择加入状态模型:强大/最终一致性,首次写入/最后写入获胜
    • 跨平台虚拟演员
    • 限速
    • 使用OpenTelemetry的内置分布式跟踪
    • 使用专用的Operator和CRD在Kubernetes上本地运行
    • 通过HTTP和gRPC支持所有编程语言
    • 来自Azure,AWS,GCP的多云,开放式组件(绑定,发布-订阅,状态)
    • 作为过程或容器化在任何地方运行
    • 轻量级(58MB二进制,4MB物理内存)
    • 作为辅助工具运行-无需特殊的SDK或库
    • 专用的CLI-易于调试的开发人员友好体验
    • Java,Dotnet,Go,Javascript和Python的客户端

    Refer

    来源: Dapr HelloWorld · 语雀