一、概要
上篇介绍服务之间调用 ( 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 )
回顾一下之前的路径图:
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 )
- tracing 主要使用 zipkin 协议,方式可以使用openzipkin
- logging主要使用 EFK、ELK
- 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 )
回顾一下之前的路径图:
官方介绍:此 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 实战中,碰到问题,欢迎在评论区留言沟通。
推荐阅读
招贤纳士
政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 [email protected]
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注