dapr实战(三)

政采云技术团队.png

水手.png

一、概要

​ 上篇介绍服务之间调用 ( Service-to-service invocation )、状态管理 ( State management ) 功能。

本文介绍剩下的几个功能:

  • 发布订阅( Publish and subscribe )
  • 资源绑定( Resource bindings )
  • Actors
  • 可观测性( Observability )
  • 秘钥管理( Secrets management)
  • 配置管理( Configuration )

所有 Demo 均以 Java 的形式展示。以上功能都基于 Dapr实战(二)的基础上增加额外的配置,基础部分不再赘述。

注意:Dapr 是作为 Sidecar存在的,所以设置好组件之后,所以必须启动 http-demo 项目。如果组件未启用的话,http-demo 内不要进行配置例如:dapr.io/config: "zipkin-config"

二、发布订阅( Publish and Subscribe )

回顾一下之前的路径图:

image-20211229165213277

2.1 发布( Publish )

建立 pub 的配置( pub.yaml)

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: redis-pubsub
  namespace: default
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: xx.xx.xx.xx:6379
  - name: redisPassword
    value: 
复制代码

执行组件初始化

kubectl apply -f pub.yaml
复制代码

启动项目 http-demo.yaml

kubectl create -f http-demo.yaml
复制代码

测试功能:调用Dapr接口

curl -X POST http://xx.xx.xx.xx:3500/v1.0/publish/redis-pubsub/deathStarStatus -H "Content-Type: application/json"  -d '{
       "status": "completed-test"
     }'
复制代码

查看redis内容,可以看到对应的消息内容

xread streams deathStarStatus 0
复制代码

2.2 订阅( Subscribe )

建立 sub 的配置( sub.yaml )

apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
  name: redis-sub
spec:
  topic: deathStarStatus
  route: /dapr-sample-topic
  pubsubname: redis-pubsub
复制代码

controller 代码片段

@PostMapping("/dapr-sample-topic")
public Response<String> sampleTopic() {
  System.out.println("sample-topic");
  return Response.ok("sample-topic");
}
复制代码

执行组件初始化

kubectl apply -f sub.yaml
复制代码

启动项目 http-demo-dapr.yaml

kubectl create -f http-demo-dapr.yaml
复制代码

测试功能:调用 pub 的接口进行发布,查看http-demo-dapr的日志,日志可以看到sample-topic消费到了对应的日志

kubectl logs -f daprnodeapp-xx-xx -c dapr-http-demo
复制代码

三、资源绑定( Resource bindings )

资源绑定我们以kafka为例,先建立kafka中间件

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
kubectl create ns kafka
helm install dapr-kafka bitnami/kafka --wait --namespace kafka -f ./kafka-non-persistence.yaml
复制代码

kafka 的 yaml 内容( kafka-non-persistence.yaml )

# ------------------------------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------------------------------

replicas: 1

# Disable persistent storage
persistence:
  enabled: false
zookeeper:
  persistence:
    enabled: false
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
            - key: kubernetes.io/os
              operator: In
              values:
              - linux
            - key: kubernetes.io/arch
              operator: In
              values:
              - amd64

autoCreateTopicsEnable: true

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/os
            operator: In
            values:
            - linux
          - key: kubernetes.io/arch
            operator: In
            values:
            - amd64
复制代码

创建 kafka 的组件

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: sample-topic
spec:
  type: bindings.kafka
  version: v1
  metadata:
  # Kafka broker connection setting
  - name: brokers
    value: "xx.xx.xx.xx:9092,xx.xx.xx.xx:9093"
  # consumer configuration: topic and consumer group
  - name: topics
    value: sample
  - name: consumerGroup
    value: group1
  # publisher configuration: topic
  - name: publishTopic
    value: sample
  - name: authRequired
    value: "false"
复制代码

初始化项目( http-demo-dapr.yaml )用于验证输出绑定

kubectl create -f  http-demo-dapr.yaml 
复制代码

测试功能:输出绑定(输出绑定允许您调用外部资源( kafka ))

解释:向 sample-topic 组件输出请求topic sample创建消息的事件)

curl -X POST -H 'Content-Type: application/json' http://xx.xx.xx.xx:3500/v1.0/bindings/sample-topic -d '{ "data": { "message": "Hi!" }, "operation": "create" }'
复制代码

初始化项目( http-demo.yaml )用于验证输入绑定

kubectl create -f  http-demo.yaml 
复制代码

测试功能:输入绑定( kafka 外部资源的事件时触发您的应用程序)

解释:只要接口 url 是sample-topic就能接收到kafka发生的变化,通过日志查看到 sample-topic ,接收到了 kafka 的外部资源事件

kubectl logs -f nodeapp-xx-xx -c http-demo
复制代码

四、Actors

Actors目前功能包含:计时器( timers )和 提醒( reminders ),主要差别是 timers 是无状态的,reminders是有状态。无状态即重启就信息就丢失,有状态是重启还是能继续运转。

创建状态存储提供给 actor ( statestore.yaml )

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
  namespace: default
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: xx.xx.xx.xx:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"
复制代码

初始化 http-demo 、 http-demo-dapr 项目

kubectl create -f http-demo.yaml
kubectl create -f http-demo-dapr.yaml
复制代码

创建 actor sample 相关代码

@GetMapping("/dapr-sample-actor/{actorId}")
public Response<Integer> sampleActor(@PathVariable("actorId") String actorId) throws InterruptedException {
    System.out.println("sample-actor");
    int messageNumber = doActor(actorId);
    return Response.ok(messageNumber);
}

@GetMapping("/dapr-sample-actor/clock/{actorId}")
public Response<Integer> sampleActorClock(@PathVariable("actorId") String actorId) {
    System.out.println("sample-actor");
    int messageNumber = doActorClock(actorId);
    return Response.ok(messageNumber);
}

private int doActorClock(String actorId){
    ActorClient client = new ActorClient();
    //创建actor
    ActorProxyBuilder<DemoActor> builder = new ActorProxyBuilder(DemoActor.class, client);
    DemoActor actor = builder.build(new ActorId(actorId));
    actor.registerActorTimer();
    return sayMessageToActor(actorId, actor);
}

private int doActor(String actorId) {
    ActorClient client = new ActorClient();
    //创建actor
    ActorProxyBuilder<DemoActor> builder = new ActorProxyBuilder(DemoActor.class, client);
    DemoActor actor = builder.build(new ActorId(actorId));
    //设置定时机制 10 触发一次 结束
    actor.registerReminder();
    //执行定时任务 call
    return sayMessageToActor(actorId, actor);
}

/**
 * Makes multiple method calls into actor until interrupted.
 *
 * @param actorId Actor's identifier.
 * @param actor   Actor to be invoked.
 */
private int sayMessageToActor(String actorId, DemoActor actor) {
    // Invoke actor method to increment counter by 1, then build message.
    int messageNumber = actor.incrementAndGet(1).block();
    String message = String.format("Actor %s said message #%d", actorId, messageNumber);
    //invoker and save
    actor.say(message);
    return messageNumber;
}
复制代码

创建 actor 功能类

@ActorType(name = "DemoActor")
public interface DemoActor {

    void registerReminder();

    @ActorMethod(name = "echo_message")
    String say(String something);

    void clock(String message);

    void registerActorTimer();

    @ActorMethod(returns = Integer.class)
    Mono<Integer> incrementAndGet(int delta);
}
复制代码

创建 actor 实现类( DemoActorImpl )

package com.test.dapr.sample.interfaces.http;

import io.dapr.actors.ActorId;
import io.dapr.actors.runtime.AbstractActor;
import io.dapr.actors.runtime.ActorRuntimeContext;
import io.dapr.actors.runtime.Remindable;
import io.dapr.utils.TypeRef;
import reactor.core.publisher.Mono;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Calendar;
import java.util.TimeZone;

/**
 * @author 水手
 * @since 2021/12/30 7:25 下午
 */
public class DemoActorImpl extends AbstractActor implements DemoActor, Remindable<Integer> {

    /**
     * Format to output date and time.
     */
    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    /**
     * This is the constructor of an actor implementation, while also registering a timer.
     *
     * @param runtimeContext The runtime context object which contains objects such as the state provider.
     * @param id             The id of this actor.
     */
    public DemoActorImpl(ActorRuntimeContext runtimeContext, ActorId id) {
        super(runtimeContext, id);
    }

    @Override
    public void registerActorTimer() {
        super.registerActorTimer(
                "my-actorTimer",
                "clock",
                "ping!",
                Duration.ofSeconds(5),
                Duration.ofSeconds(2)).block();
    }

    /**
     * Registers a reminder.
     */
    @Override
    public void registerReminder() {
        super.registerReminder(
                "my-reminder",
                (int) (Integer.MAX_VALUE * Math.random()),
                Duration.ofSeconds(10),
                Duration.ZERO).block();
    }

    /**
     * Prints a message and appends the timestamp.
     *
     * @param something Something to be said.
     * @return What was said appended with timestamp.
     */
    @Override
    public String say(String something) {
        Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());

        // Handles the request by printing message.
        System.out.println("Server say method for actor " + super.getId() + ": "
                + (something == null ? "" : something + " @ " + utcNowAsString));
        //储存redis 对应的值
        super.getActorStateManager().set("lastMessage", something).block();

        // Now respond with current timestamp.
        return utcNowAsString;
    }

    /**
     * Increments a persistent counter, saves and returns its updated value.
     * Example of method implemented with Reactor's Mono class.
     * This method could be rewritten with blocking calls in Mono, using block() method:
     *
     * <p>public int incrementAndGet(int delta) {
     * int counter = 0;
     * if (super.getActorStateManager().contains("counter").block()) {
     * counter = super.getActorStateManager().get("counter", int.class).block();
     * }
     * counter = counter + 1;
     * super.getActorStateManager().set("counter", counter).block();
     * return counter;
     * }</p>
     *
     * @param delta Amount to be added to counter.
     * @return Mono response for the incremented value.
     */
    @Override
    public Mono<Integer> incrementAndGet(int delta) {
        return super.getActorStateManager().contains("counter")
                .flatMap(exists -> exists ? super.getActorStateManager().get("counter", int.class) : Mono.just(0))
                .map(c -> c + delta)
                .flatMap(c -> super.getActorStateManager().set("counter", c).thenReturn(c));
    }

    /**
     * Method invoked by timer.
     *
     * @param message Message to be printed.
     */
    @Override
    public void clock(String message) {
        Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());

        // Handles the request by printing message.
        System.out.println("clock for actor "
                + super.getId() + ": "
                + (message == null ? "" : message + " @ " + utcNowAsString));
    }

    /**
     * Method used to determine reminder's state type.
     *
     * @return Class for reminder's state.
     */
    @Override
    public TypeRef<Integer> getStateType() {
        return TypeRef.INT;
    }

    /**
     * Method used be invoked for a reminder.
     *
     * @param reminderName The name of reminder provided during registration.
     * @param state        The user state provided during registration.
     * @param dueTime      The invocation due time provided during registration.
     * @param period       The invocation period provided during registration.
     * @return Mono result.
     */
    @Override
    public Mono<Void> receiveReminder(String reminderName, Integer state, Duration dueTime, Duration period) {
        return Mono.fromRunnable(() -> {
            Calendar utcNow = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
            String utcNowAsString = DATE_FORMAT.format(utcNow.getTime());

            String message = String.format("Server reminded actor %s of: %s for %d @ %s",
                    this.getId(), reminderName, state, utcNowAsString);

            // Handles the request by printing message.
            System.out.println(message);
        });
    }
}
复制代码

测试功能:创建 timers、reminders 任务

创建 timers 的 clock 任务

Java创建方式

curl http://xx.xx.xx.xx:3500/v1.0/invoke/daprnodeapp/method/dapr-sample-actor/clock/88
复制代码

删除 timers 的 clock 任务

curl -X DELETE http://xx.xx.xx.xx:3500/v1.0/actors/DemoActor/88/timers/my-actorTimer
复制代码

创建 reminders 任务

Java 创建方式

curl -X GET http://xx.xx.xx.xx:3500/v1.0/invoke/daprnodeapp/method/dapr-sample-actor/90
复制代码

Api 创建或更新方式(第一次9秒执行,以后每3秒执行一次)

curl -X PUT -H "Content-Type: application/json" http://xx.xx.xx.xx:3500/v1.0/actors/DemoActor/90/reminders/echo_message -d '{"dueTime":"0h0m9s0ms","period":"0h0m3s0ms"}'
复制代码

删除 reminders 任务

curl -X DELETE http://xx.xx.xx.xx:3500/v1.0/actors/DemoActor/90/reminders/echo_message
复制代码

五、可观测性( Observability )

可观测性主要包括三部分: 链路跟踪( tracing )、日志( logging )、指标( metrics )

  1. tracing 主要使用 zipkin 协议,方式可以使用openzipkin
  2. logging主要使用 EFK、ELK
  3. metrics主要使用 grafana + prometheus

建立 openzipkin.yaml 配置

apiVersion: apps/v1
kind: Deployment
metadata:
  name: zipkin
  labels:
    app: zipkin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: zipkin
  template:
    metadata:
      labels:
        app: zipkin
    spec:
      containers:
      - name: zipkin
        image: openzipkin/zipkin
        ports:
        - containerPort: 9411

---

kind: Service
apiVersion: v1
metadata:
  name: zipkin
  labels:
    app: zipkin
spec:
  selector:
    app: zipkin
  ports:
  - protocol: TCP
    port: 9411
    targetPort: 9411
  type: ClusterIP
复制代码

打开网页 http://xx.xx.xx.xx:9411/zipkin 正常访问

初始化 openzipkin

kubectl create -f zipkin.yaml
复制代码

创建 zipkin 组件

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: zipkin-config
spec:
  tracing:
    samplingRate: "1"
    zipkin:
      endpointAddress: "http://xx.xx.xx.xx:9411/api/v2/spans"
复制代码

项目的 yaml 文件,在相应的位置增加配置

dapr.io/log-as-json: "true"
dapr.io/config: "zipkin-config"
复制代码

增加 http-demo-dapr controller

@GetMapping("/dapr-helloWorld")
public Response<String> getHelloWorld(@RequestHeader Map<String, String> headers) {
  System.out.println("getHelloWorld");
  HttpExtension httpExtension = new HttpExtension(DaprHttp.HttpMethods.GET, null, headers);

  InvokeMethodRequest request = new InvokeMethodRequest("nodeapp", "helloWorld")
    .setBody(null)
    .setHttpExtension(httpExtension);
  Response response = daprClient.invokeMethod(request, TypeRef.get(Response.class)).block();
  //                .subscriberContext(getReactorContext()).block();
  System.out.println("finish getHelloWorld");
  if (response != null) {
    return Response.ok(response.getResult().toString());
  }
  return null;
}
复制代码

初始化 http-demo 、 http-demo-dapr 项目

kubectl create -f http-demo.yaml
kubectl create -f http-demo-dapr.yaml
复制代码

测试功能:openzipkin 链路跟踪是否正常,注意( http 透传的 key 约定为 traceparent,grpc 透传的 key 约定为 grpc-trace-bin )

curl -H "traceparent: 00-0af7651916cd43dd8438eb211c19319c-b7ad5b7169203331-02" -H "tracestate: congo=t39rcWkgMzE" http://xx.xx.xx.xx:3500/v1.0/invoke/daprnodeapp/method/dapr-helloWorld
复制代码

验证结果,打开openzipkin地址:http://xx.xx.xx.xx:9411/zipkin ,显示有条日志,SHOW明细,显示调用链路是从应用daprnodeapp -> nodeapp

可观测性就介绍到这里,至于 EFK 以及 grafana + prometheus 有兴趣的同学可以自己动手试试。

六、秘钥管理( Secrets management )

首先建立一个 K8S 的秘钥管理

kubectl create secret generic dapr-secret --from-literal=my-secret="I'm Batman"
复制代码

基于 K8S 的秘钥管理,初始化一个 Dapr 组件( daprSecretStore.yaml )

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: mycustomsecretstore
  namespace: default
spec:
  type: secretstores.kubernetes
  version: v1
  metadata:
  - name: "dapr-secret"
复制代码

执行初始化组件命令:

kubectl create -f daprSecretStore.yaml
复制代码

测试功能:通过 Dapr 获取 K8S 设置的秘钥,获取结果 {"my-secret":"I'm Batman"}

curl http://xx.xx.xx.xx:3500/v1.0/secrets/kubernetes/dapr-secret
复制代码

七、配置管理( Configuration )

回顾一下之前的路径图:

image-20211229172623715

官方介绍:此 API 当前处于Alpha状态,仅在 gRPC 上可用。后续开放后支持语法为*/v1.0/configuration*

根据官方提供的文档和图确定:

组件的配置( configuration.yaml )为:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: configstore
  namespace: default
spec:
  type: configuration.redis
  version: v1
  metadata:
  - name: redisHost
    value: xx.xx.xx.xx:6379
  - name: redisPassword
    value: ''
复制代码

执行初始化组件命令:

kubectl create -f configuration.yaml
复制代码

获取方式为:

curl http://10.246.3.147:3500/v1.0/configuration/configstore/{redisKey}
复制代码

八、小结

​ 至此,Dapr 相关的 Java Demo 全部讲完了,如果大家完成了实战相关的 Demo,恭喜已经 Dapr 入门了。如果 Demo 实战中,碰到问题,欢迎在评论区留言沟通。

推荐阅读

Dapr 实战(一)

Dapr 实战(二)

政采云Flutter低成本屏幕适配方案探索

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 [email protected]

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

政采云技术团队.png

猜你喜欢

转载自juejin.im/post/7088102232423399438