Pulsar Functions是一个运行在Pulsar之上的无服务器计算框架
Pulsar Functions是一个运行在Pulsar之上的无服务器计算框架,并以以下方式处理消息:
每次函数接收到消息时,它都会完成以下消费-应用-发布步骤:
我们可以用Java、Python和Go编写函数。例如,您可以使用Pulsar Functions来设置以下处理链.
Pulsar函数在将消息路由到消费者之前对消息执行简单的计算。这些lambda风格的功能是专门设计的,并与Pulsar集成在一起。
该框架在Pulsar集群上提供了一个简单的计算框架,并负责发送和接收消息的底层细节。我们只需要关注业务逻辑。
Pulsar函数使我们的组织能够最大限度地发挥数据的价值,并享受数据的好处:
每个函数都有一个完全限定函数名(FQFN),具有指定的租户、命名空间和函数名。使用FQFN,我们可以在不同的名称空间中使用相同的函数名创建多个函数。
tenant/namespace/name
函数实例是函数执行框架的核心元素,由以下元素组成:
一个函数可以有多个实例,每个实例执行一个函数的副本。可以在配置文件中指定实例数
函数实例中的使用者使用FQFN作为订阅者名,以基于订阅类型在多个实例之间实现负载平衡。订阅类型可以在函数级别指定
每个函数都有一个单独的FQFN状态存储。我们可以指定一个状态接口,以便在BookKeeper中持久化中间结果。其他用户可以查询函数的状态并提取这些结果
函数worker是一个逻辑组件,用于在Pulsar Functions的集群模式部署中监视、编排和执行单个函数
在函数工作者中,每个函数实例都可以作为线程或进程执行,具体取决于所选的配置。或者,如果Kubernetes集群可用,则可以在Kubernetes中以StatefulSets的形式生成函数
函数工作者形成了一个工作者节点集群,其工作流程描述如下:
函数实例是在运行时内调用的,许多实例可以并行运行。Pulsar支持三种不同成本和隔离保证的函数运行时类型,以最大限度地提高部署灵活性
下表概述了三种类型的函数运行时:
类型 | 描述 |
---|---|
线程运行(Thread runtime) | 每个实例都作为一个线程运行,由于线程模式的代码是用Java编写的,所以它只适用于Java实例。当函数以线程模式运行时,它与函数工作者运行在同一个Java虚拟机(JVM)上 |
进程运行时(process runtime) | 每个实例都作为一个进程运行,当函数以进程模式运行时,它运行在函数工作者运行的同一台机器上 |
K8s运行时(Kubernetes runtime) | 函数由worker以Kubernetes StatefulSet的形式提交,每个函数实例作为pod运行。Pulsar支持在启动函数时向Kubernetes StatefulSets和服务添加标签,这有助于选择目标Kubernetes对象 |
Pulsar提供了三种不同的消息传递语义,我们可以将它们应用于一个函数。根据ack时间节点确定不同的传递语义实现
传递语义 | 描述 | 采用的订阅类型 |
---|---|---|
最多传送一次 | 发送到函数的每个消息都将尽最大努力处理。不能保证信息是否会被处理 当我们选择这个语义时,autoAck配置必须设置为true,否则启动将失败(autoAck配置将在将来的版本中被弃用) Ack时间节点:函数处理之前 | Shared |
至少一次递送 默认 | 发送到函数的每个消息都可以被处理多次(以防处理失败或重新交付) 如果创建函数时没有指定–processing-guaranteed标志,则该函数提供至少一次交付保证 Ack时间节点:发送消息到输出后 | Shared |
一次有效交付 | 发送到函数的每条消息都可以被处理多次,但它只有一个输出。重复的消息将被忽略 有效地在至少一次处理和有保证的服务器端重复数据删除的基础上实现一次。这意味着一个状态更新可以发生两次,但是相同的状态更新只应用一次,另一个重复的状态更新在服务器端被丢弃 Ack时间节点:发送消息到输出后 | Failover |
人工投放 | 当我们选择这个语义时,框架不会执行任何ack操作,我们需要在函数中调用context.getCurrentRecord().ack()方法来手动执行ack操作 Ack时间节点:在函数方法中自定义 | Shared |
bin/pulsar-admin functions create \
--name my-effectively-once-function \
--processing-guarantees EFFECTIVELY_ONCE \
update:
bin/pulsar-admin functions update \
--processing-guarantees ATMOST_ONCE \
Java、Python和Go sdk提供了对函数可以使用的上下文对象的访问。这个上下文对象为函数提供了各种各样的信息和功能,包括
目前,窗口函数仅在Java中可用,并且不支持MANUAL和effective-once delivery语义
窗口函数是跨数据窗口(即事件流的有限子集)执行计算的函数。如下图所示,流被划分为“桶”,其中可以应用函数
函数的数据窗口定义涉及两个策略:
触发策略和驱逐策略都由时间或计数驱动:
同时支持处理时间和事件时间:
根据相邻的两个窗口是否可以共享公共事件,窗口可以分为以下两种类型:
相反,如下面的示例所示,滚动窗口的窗口长度为10秒,这意味着当10秒的时间间隔过去时,函数将被触发,而不管窗口中有多少事件:
functionsWorkerEnabled=true
./bin/pulsar standalone
telnet localhost 6650
./bin/pulsar-admin functions-worker get-cluster
{
"workerId" : "c-standalone-fw-localhost-8080",
"workerHostname" : "localhost",
"port" : 8080
}
./bin/pulsar-admin tenants list
public
pulsar
sample
./bin/pulsar-admin namespaces list public
public/default
public/functions
telnet localhost 4181
./bin/pulsar-admin tenants create test
public
pulsar
sample
test
./bin/pulsar-admin namespaces create test/test-namespace
test/test-namespace
./bin/pulsar-admin functions create --function-config-file examples/example-function-config.yaml --jar examples/api-examples.jar
Created successfully
./bin/pulsar-admin functions get --tenant test --namespace test-namespace --name example
{
"tenant": "test",
"namespace": "test-namespace",
"name": "example",
"className": "org.apache.pulsar.functions.api.examples.ExclamationFunction",
"inputSpecs": {
"test_src": {
"isRegexPattern": false,
"schemaProperties": {},
"consumerProperties": {},
"poolMessages": false
}
},
"output": "test_result",
"producerConfig": {
"useThreadLocalProducers": false,
"batchBuilder": ""
},
"processingGuarantees": "ATLEAST_ONCE",
"retainOrdering": false,
"retainKeyOrdering": false,
"forwardSourceMessageProperty": true,
"userConfig": {
"PublishTopic": "test_result"
},
"runtime": "JAVA",
"autoAck": true,
"parallelism": 1,
"resources": {
"cpu": 1.0,
"ram": 1073741824,
"disk": 10737418240
},
"cleanupSubscription": true,
"subscriptionPosition": "Latest"
}
./bin/pulsar-admin functions status --tenant test --namespace test-namespace --name example
{
"numInstances" : 1,
"numRunning" : 1,
"instances" : [ {
"instanceId" : 0,
"status" : {
"running" : true,
"error" : "",
"numRestarts" : 0,
"numReceived" : 0,
"numSuccessfullyProcessed" : 0,
"numUserExceptions" : 0,
"latestUserExceptions" : [ ],
"numSystemExceptions" : 0,
"latestSystemExceptions" : [ ],
"averageLatency" : 0.0,
"lastInvocationTime" : 0,
"workerId" : "c-standalone-fw-localhost-8080"
}
} ]
}
./bin/pulsar-client consume -s test-sub -n 0 test_result
./bin/pulsar-client produce -m "test-message-`date`" -n 10 test_src
2023-03-24T17:41:29,306+0800 [pulsar-client-io-1-1] INFO com.scurrilous.circe.checksum.Crc32cIntChecksum - SSE4.2 CRC32C provider initialized
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARAAIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARABIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARACIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARADIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARAEIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARAFIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARAGIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARAHIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARAIIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
----- got message -----
key:[null], properties:[__pfn_input_msg_id__=CNbSARAJIAAwAQ==, __pfn_input_topic__=persistent://public/default/test_src], content:test-message-Fri Mar 24 17:41:22 CST 2023!
2023-03-24T17:41:36,948+0800 [pulsar-timer-6-1] INFO org.apache.pulsar.client.impl.ConsumerStatsRecorderImpl - [test_result] [test-sub] [c48f6] Prefetched messages: 0 --- Consume throughput received: 0.17 msgs/s --- 0.00 Mbit/s --- Ack sent rate: 0.17 ack/s --- Failed messages: 0 --- batch messages: 0 ---Failed acks: 0
Pulsar的独立模式为有状态函数提供了BookKeeper table 服务
tenant: "test"
namespace: "test-namespace"
name: "word_count"
className: "org.apache.pulsar.functions.api.examples.WordCountFunction"
inputs: ["test_wordcount_src"] # this function will read messages from these topics
autoAck: true
parallelism: 1
./bin/pulsar-admin functions create --function-config-file examples/example-stateful-function-config.yaml --jar examples/api-examples.jar
Created successfully
bin/pulsar-admin functions get --tenant test --namespace test-namespace --name word_count
{
"tenant": "test",
"namespace": "test-namespace",
"name": "word_count",
"className": "org.apache.pulsar.functions.api.examples.WordCountFunction",
"inputSpecs": {
"test_wordcount_src": {
"isRegexPattern": false,
"schemaProperties": {},
"consumerProperties": {},
"poolMessages": false
}
},
"producerConfig": {
"useThreadLocalProducers": false,
"batchBuilder": ""
},
"processingGuarantees": "ATLEAST_ONCE",
"retainOrdering": false,
"retainKeyOrdering": false,
"forwardSourceMessageProperty": true,
"userConfig": {},
"runtime": "JAVA",
"autoAck": true,
"parallelism": 1,
"resources": {
"cpu": 1.0,
"ram": 1073741824,
"disk": 10737418240
},
"cleanupSubscription": true,
"subscriptionPosition": "Latest"
}
./bin/pulsar-admin functions status --tenant test --namespace test-namespace --name word_count
{
"numInstances" : 1,
"numRunning" : 1,
"instances" : [ {
"instanceId" : 0,
"status" : {
"running" : true,
"error" : "",
"numRestarts" : 0,
"numReceived" : 0,
"numSuccessfullyProcessed" : 0,
"numUserExceptions" : 0,
"latestUserExceptions" : [ ],
"numSystemExceptions" : 0,
"latestSystemExceptions" : [ ],
"averageLatency" : 0.0,
"lastInvocationTime" : 0,
"workerId" : "c-standalone-fw-localhost-8080"
}
} ]
}
./bin/pulsar-admin functions querystate --tenant test --namespace test-namespace --name word_count -k hello -w
这样会不断的监听数据
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
key 'hello' doesn't exist.
.....
./bin/pulsar-client produce -m "hello" -n 10 test_wordcount_src
{
"key": "hello",
"numberValue": 10,
"version": 9
}
{
"key": "hello",
"numberValue": 10,
"version": 9
}
.....
./bin/pulsar-client produce -m "hello" -n 10 test_wordcount_src
{
"key": "hello",
"numberValue": 20,
"version": 19
}
{
"key": "hello",
"numberValue": 20,
"version": 19
}
{
"key": "hello",
"numberValue": 20,
"version": 19
}
......
窗函数是脉冲星函数的一种特殊形式
./bin/pulsar-admin functions create --function-config-file examples/example-window-function-config.yaml --jar examples/api-examples.jar
这里会报错,因为yaml指定的function 的name为example,与我们之前的创建重复了,所以有如下错误:
Function example already exists
tenant: "test"
namespace: "test-namespace"
name: "example_window"
className: "org.apache.pulsar.functions.api.examples.AddWindowFunction"
inputs: ["test_src_window"]
userConfig:
"PublishTopic": "test_result_window"
output: "test_result_window"
autoAck: true
parallelism: 1
windowConfig:
windowLengthCount: 10
slidingIntervalCount: 5
就可以创建成功了,Created successfully
./bin/pulsar-admin functions get --tenant test --namespace test-namespace --name example_window
{
"tenant": "test",
"namespace": "test-namespace",
"name": "example_window",
"className": "org.apache.pulsar.functions.api.examples.AddWindowFunction",
"inputSpecs": {
"test_src_window": {
"isRegexPattern": false,
"schemaProperties": {},
"consumerProperties": {},
"poolMessages": false
}
},
"output": "test_result_window",
"producerConfig": {
"useThreadLocalProducers": false,
"batchBuilder": ""
},
"processingGuarantees": "ATLEAST_ONCE",
"retainOrdering": false,
"retainKeyOrdering": false,
"forwardSourceMessageProperty": true,
"userConfig": {
"PublishTopic": "test_result_window"
},
"runtime": "JAVA",
"autoAck": false,
"parallelism": 1,
"resources": {
"cpu": 1.0,
"ram": 1073741824,
"disk": 10737418240
},
"windowConfig": {
"windowLengthCount": 10,
"slidingIntervalCount": 5,
"actualWindowFunctionClassName": "org.apache.pulsar.functions.api.examples.AddWindowFunction"
},
"cleanupSubscription": true,
"subscriptionPosition": "Latest"
}
./bin/pulsar-admin functions status --tenant test --namespace test-namespace --name example_window
{
"numInstances" : 1,
"numRunning" : 1,
"instances" : [ {
"instanceId" : 0,
"status" : {
"running" : true,
"error" : "",
"numRestarts" : 0,
"numReceived" : 0,
"numSuccessfullyProcessed" : 0,
"numUserExceptions" : 0,
"latestUserExceptions" : [ ],
"numSystemExceptions" : 0,
"latestSystemExceptions" : [ ],
"averageLatency" : 0.0,
"lastInvocationTime" : 0,
"workerId" : "c-standalone-fw-localhost-8080"
}
} ]
}
./bin/pulsar-client consume -s test-sub -n 0 test_result_window
./bin/pulsar-client produce -m "3" -n 10 test_src_window
----- got message -----
key:[null], properties:[], content:15
----- got message -----
key:[null], properties:[], content:30
我们有两种方式设置Functions Workers:
图中的service url表示Pulsar client和Pulsar admin用来连接到Pulsar集群的Pulsar服务url
在conf/broker.conf文件(conf/standalone.conf for pulsar standalone)中,将functionsWorkereNabled设置为true
functionsWorkerEnabled=true
在与run-with-brokers模式下,大多数function workers的设置是从我们的代理配置继承的(例如,配置存储设置,认证设置等)
我们可以根据我们的需求配置conf/functions_worker.yml文件来自定义其他workers设置
提示优化点:
在BookKeeper集群上启用身份验证时,需要为function workers配置以下身份验证设置:
一旦正确配置了function workers,就可以启动代理
要验证每个worker是否正在运行,可以使用以下命令:
curl :8080/admin/v2/worker/cluster
如果返回活动function workers的列表,则意味着它们已经成功启动。回显信息如下所示:
[{"workerId":"","workerHostname":"","port":8080}]
下图说明了function workers如何在不同的机器中作为单独的进程运行:
图中的service url表示Pulsar client和Pulsar admin用来连接到Pulsar集群的Pulsar服务url
需要进行隔离运行function worker ,需要设置functionsWorkerEnabled=false(conf/broker.conf)
配置worker所需的参数(conf/functions_worker.yml):
当访问函数工作者来管理函数时,pulse-admin CLI或任何客户端应该使用配置的workerHostname和workerPort来生成–admin-url
配置numFunctionPackageReplicas用来存储功能包的副本数量(conf/functions_worker.yml)
要确保生产部署中的高可用性,请将numFunctionPackageReplicas设置为与bookies数量相等。仅在单节点集群部署时,默认值为“1”
为函数元数据配置所需的参数(conf/functions_worker.yml):
如果在代理集群上启用了身份验证,则必须为函数工作者配置以下身份验证设置,以便与代理通信:
当我们在配置了身份验证的集群中单独运行函数worker时,我们的函数worker需要与代理通信并验证传入的请求。因此,我们需要配置代理进行身份验证和授权所需的属性
我们必须为服务器配置工作人员身份验证和授权功能,以便对传入请求进行身份验证,并配置经过身份验证的客户端与代理通信
使用token认证时,需要配置以下属性(conf/function-worker.yml):
brokerClientAuthenticationPlugin: org.apache.pulsar.client.impl.auth.AuthenticationToken
brokerClientAuthenticationParameters: file:///etc/pulsar/token/admin-token.txt
configurationMetadataStoreUrl: zk:zookeeper-cluster:2181 # auth requires a connection to zookeeper
authenticationProviders:
- "org.apache.pulsar.broker.authentication.AuthenticationProviderToken"
authorizationEnabled: true
authenticationEnabled: true
superUserRoles:
- superuser
- proxy
properties:
tokenSecretKey: file:///etc/pulsar/jwt/secret # if using a secret token, key file must be DER-encoded
tokenPublicKey: file:///etc/pulsar/jwt/public.key # if using public/private key tokens, key file must be DER-encoded
我们可以设置如下的安全策略:
useTLS: true
pulsarServiceUrl: pulsar+ssl://localhost:6651/
pulsarWebServiceUrl: https://localhost:8443
tlsEnabled: true
tlsCertificateFilePath: /path/to/functions-worker.cert.pem
tlsKeyFilePath: /path/to/functions-worker.key-pk8.pem
tlsTrustCertsFilePath: /path/to/ca.cert.pem
// The path to trusted certificates used by the Pulsar client to authenticate with Pulsar brokers
brokerClientTrustCertsFilePath: /path/to/ca.cert.pem
authenticationEnabled: true
authenticationProviders: [provider1, provider2]
brokerClientAuthenticationPlugin: org.apache.pulsar.client.impl.auth.AuthenticationTls
brokerClientAuthenticationParameters: tlsCertFile:/path/to/admin.cert.pem,tlsKeyFile:/path/to/admin.key-pk8.pem
authenticationEnabled: true
authenticationProviders: ['org.apache.pulsar.broker.authentication.AuthenticationProviderTls']
properties:
saslJaasClientAllowedIds: .*pulsar.*
saslJaasServerSectionName: Broker
properties:
tokenSecretKey: file://my/secret.key
# If using public/private
# tokenPublicKey: file://path/to/public.key
密钥文件必须是DER(区分编码规则)编码的
要对函数工作者启用授权,请完成以下步骤:
authorizationEnabled: true
authorizationProvider: org.apache.pulsar.broker.authorization.PulsarAuthorizationProvider
configurationMetadataStoreUrl: :
superUserRoles:
- role1
- role2
- role3
如果在BookKeeper集群上启用了身份验证,则需要为函数工作者配置以下BookKeeper身份验证设置:
在启动函数工作者之前,请确保配置了函数运行时
我们可以使用pulse-daemon CLI工具在后台启动函数工作:
bin/pulsar-daemon start functions-worker
要在前台启动一个函数工作者,可以使用pulse-admin CLI,如下所示:
bin/pulsar functions-worker
当我们在一个单独的集群中运行函数工作者时,admin rest endpoints被分成两个集群,如下图所示。function、function-worker、source和sink现在由工作者集群提供服务,
而所有其他剩余端点由代理集群提供服务。这要求我们在pulse-admin CLI中相应地使用正确的服务URL。为了解决这种不便,我们可以启动一个代理集群,作为管理服务的中央入口点,用于路由管理rest请求
启用代理,将与函数相关的管理请求路由到函数工作者,我们需要做如下配置(conf/proxy.conf):
functionWorkerWebServiceURL=
functionWorkerWebServiceURLTLS=
函数工作者使用JVM中的java.io.tmpdir作为默认的临时文件路径,它也用作每个NAR包的默认提取文件路径。NAR包需要一个本地文件路径来提取并加载到Java类加载器
如果希望将NAR包的默认提取文件路径更改到另一个目录,可以在functions_worker.yml中添加以下参数和所需的目录。配置取决于我们使用的函数运行时
当需要Pulsar函数的有状态api时,例如putState()和queryState()相关的接口,我们需要在函数工作者中启用有状态函数特性
##################################################################
##################################################################
# Settings below are used by stream/table service
##################################################################
##################################################################
### Grpc Server ###
# the grpc server port to listen on. default is 4181
storageserver.grpc.port=4181
### Dlog Settings for table service ###
#### Replication Settings
dlog.bkcEnsembleSize=3
dlog.bkcWriteQuorumSize=2
dlog.bkcAckQuorumSize=2
### Storage ###
# local storage directories for storing table ranges data (e.g. rocksdb sst files)
storage.range.store.dirs=data/bookkeeper/ranges
# whether the storage server capable of serving readonly tables. default is false.
storage.serve.readonly.tables=false
# the cluster controller schedule interval, in milliseconds. default is 30 seconds.
storage.cluster.controller.schedule.interval.ms=30000
telnet localhost 4181
stateStorageServiceUrl: bk://:4181
当使用地理复制运行多个集群时,需要为每个集群使用不同的函数名称空间。否则,所有函数共享一个名称空间,并可能跨集群调度分配
例如,如果我们有两个集群:east-1和west-1,我们可以分别在conf/functions_worker.yml文件中配置,这确保了两个不同职能的worker使用不同的主题集进行内部协调
pulsarFunctionsCluster: east-1
pulsarFunctionsNamespace: public/functions-east-1
pulsarFunctionsCluster: west-1
pulsarFunctionsNamespace: public/functions-west-1
Error message: Namespace missing local cluster name in clusters list
Failed to get partitioned topic metadata: org.apache.pulsar.client.api.PulsarClientException$BrokerMetadataException: Namespace missing local cluster name in clusters list: local_cluster=xyz ns=public/functions clusters=[standalone]
当发生以下任何情况时,将显示错误消息:
如果发生上述任何一种情况,请按照下面的说明解决问题:
bin/pulsar-admin namespaces get-clusters public/functions
bin/pulsar-admin namespaces set-clusters --clusters , public/functions
我们可以在conf/functions_worker.yml文件中使用线程运行时的默认配置
如果需要自定义更多参数,如线程组名称,请参考以下示例:
functionRuntimeFactoryClassName: org.apache.pulsar.functions.runtime.thread.ThreadRuntimeFactory
functionRuntimeFactoryConfigs:
threadGroupName: "Your Function Container Group"
要设置线程运行时的客户机内存限制,可以配置pulsarClientMemoryLimit:
functionRuntimeFactoryConfigs:
# pulsarClientMemoryLimit
# # the max memory in bytes the pulsar client can use
# absoluteValue:
# # the max memory the pulsar client can use as a percentage of max direct memory set for JVM
# percentOfMaxDirectMemory:
如果同时设置了absoluteValue和percentOfMaxDirectMemory,则使用较小的值.
我们可以在配置文件conf/functions_worker.yml文件中使用流程运行时的默认配置
如果需要自定义更多参数,请参考以下示例:
functionRuntimeFactoryClassName: org.apache.pulsar.functions.runtime.process.ProcessRuntimeFactory
functionRuntimeFactoryConfigs:
# the directory for storing the function logs
logDirectory:
# change the jar location only when you put the java instance jar in a different location
javaInstanceJarLocation:
# change the python instance location only when you put the python instance jar in a different location
pythonInstanceLocation:
# change the extra dependencies location:
extraFunctionDependenciesDir:
当函数工作者生成并应用Kubernetes清单时,Kubernetes运行时工作。由函数工作者生成的清单包括:
为了快速配置Kubernetes运行时,可以在conf/functions_worker.yml文件中使用KubernetesRuntimeFactoryConfig的默认设置
如果你已经使用Helm chart在Kubernetes上建立了一个Pulsar集群,这意味着函数工作者也已经在Kubernetes上建立了,你可以使用与函数工作者运行的pod关联的serviceAccount。
否则,我们可以通过将functionRuntimeFactoryConfigs设置为k8Uri来配置函数工作者与Kubernetes集群通信
Kubernetes中的Secret是一个保存一些机密数据(如密码、令牌或密钥)的对象。当我们在部署函数的Kubernetes名称空间中创建一个Secret时,函数可以安全地引用和分发它。
要启用该特性,请将配置文件conf/functions-worker.yml的secretsProviderConfiguratorClassName设置为org.apache.pulsar.functions.secretsproviderconfigurator.KubernetesSecretsProviderConfigurator
例如,我们将一个函数部署到pulsar-func Kubernetes名称空间,并且有一个名为database-creds的密钥名称和一个字段名password,我们希望将其作为一个名为DATABASE_PASSWORD的环境变量挂载到pod中。下面的配置允许函数引用密钥并将该值作为pod中的环境变量挂载
tenant: "mytenant"
namespace: "mynamespace"
name: "myfunction"
inputs: [ "persistent://mytenant/mynamespace/myfuncinput" ]
className: "com.company.pulsar.myfunction"
secrets:
# the secret will be mounted from the `password` field in the `database-creds` secret as an env var called `DATABASE_PASSWORD`
DATABASE_PASSWORD:
path: "database-creds"
key: "password"
当我们使用令牌身份验证、TLS加密或自定义身份验证来保护与Pulsar集群的通信时,Pulsar将我们的证书颁发机构(CA)传递给客户端,以便客户端可以使用我们的签名证书对集群进行身份验证
要为Pulsar集群启用身份验证,需要通过实现org.apache.pulsar.functions.auth.KubernetesFunctionAuthProvider接口,为运行函数的pod指定一种机制来对代理进行身份验证
对于令牌身份验证,Pulsar包含了上述接口的实现,用于分发CA。函数工作者捕获部署(或更新)函数的令牌,将其保存为密钥,并将其装入pod
在配置文件conf/function-worker.yml中配置functionAuthProviderClassName:
functionAuthProviderClassName: org.apache.pulsar.functions.auth.KubernetesSecretsTokenAuthProvider
对于TLS或自定义身份验证,我们可以实现org.apache.pulsar.functions.auth.KubernetesFunctionAuthProvider接口或使用替代机制
如果用于部署函数的令牌有到期日期,则可能需要在到期后重新部署函数
自定义Kubernetes运行时允许我们自定义由运行时创建的Kubernetes资源,包括如何生成清单,如何将经过身份验证的数据传递到pods,以及如何集成密钥secrets
需要在配置文件conf/functions-worker.yml配置runtimeCustomizerClassName,使用全限定类名
函数API提供了一个名为customRuntimeOptions的标志,该标志被传递给org.apache.pulsar.functions.runtime.KubernetesManifestCustomizer 接口。
去实例化KubernetesManifestCustomizer,可以在配置文件conf/functions-worker.yml中设置runtimeCustomizerConfig
runtimeCustomizerConfig在所有函数中都是相同的。如果同时提供runtimeCustomizerConfig和customRuntimeOptions,则需要决定如何在KubernetesManifestCustomizer接口的实现中管理这两个配置
Pulsar包含一个用runtimeCustomizerConfig初始化的内置实现。它允许我们将JSON文档作为customRuntimeOptions传递,并添加某些属性。
要使用这个内置实现,将runtimeCustomizerClassName设置为org.apache.pulsar.functions.runtime.kubernetes.BasicKubernetesManifestCustomizer
如果同时提供了runtimeCustomizerConfig和customRuntimeOptions并且存在冲突,BasicKubernetesManifestCustomizer使用customRuntimeOptions覆盖runtimeCustomizerConfig
customRuntimeOptions配置实例:
{
"jobName": "jobname", // the k8s pod name to run this function instance
"jobNamespace": "namespace", // the k8s namespace to run this function in
"extractLabels": { // extra labels to attach to the statefulSet, service, and pods
"extraLabel": "value"
},
"extraAnnotations": { // extra annotations to attach to the statefulSet, service, and pods
"extraAnnotation": "value"
},
"nodeSelectorLabels": { // node selector labels to add on to the pod spec
"customLabel": "value"
},
"tolerations": [ // tolerations to add to the pod spec
{
"key": "custom-key",
"value": "value",
"effect": "NoSchedule"
}
],
"resourceRequirements": { // values for cpu and memory should be defined as described here: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container
"requests": {
"cpu": 1,
"memory": "4G"
},
"limits": {
"cpu": 2,
"memory": "8G"
}
}
}
如果我们在Kubernetes上运行Pulsar Functions或连接器,则需要遵循Kubernetes命名约定来定义Pulsar资源的名称,无论我们使用哪个管理界面
Kubernetes需要一个RFC 1123中定义的可以用作DNS子域名的名称。Pulsar支持比Kubernetes命名惯例更多的合法字符
如果我们使用Kubernetes不支持的特殊字符创建Pulsar资源名称(例如,在Pulsar名称空间名称中包含冒号),Kubernetes运行时将Pulsar对象名称转换为符合RFC 1123格式的Kubernetes资源标签。
因此,我们可以使用Kubernetes运行时运行函数或连接器。Pulsar对象名称转换为Kubernetes资源标签的规则如下:
此设置仅适用于进程运行时和Kubernetes运行时
为函数工作者启动的每个进程向JVM命令行传递附加参数,我们可以配置additionalJavaRuntimeArguments 参数在conf/functions_worker.yml配置文件中
additionalJavaRuntimeArguments: ['-XX:+ExitOnOutOfMemoryError','-Dfoo=bar']
部署函数有两种模式:
我们可以在pulse-admin CLI下使用功能相关的命令来部署功能。pulsar提供了多种命令,例如:
CLI下需要配置的参数及默认值如下表所示:
参数 | 默认值 |
---|---|
函数名称 | N/A,可以为函数名指定任何值 |
Tenant | N/A,可以为函数名指定任何值 |
Namespace | N/A,可以为函数名指定任何值 |
Output topic | {input topic}-{function name}-output 如果函数的输入主题名是incoming且函数名是exclamation,则输出主题名是incoming-exclamation-output。 |
Processing guarantees | ATLEAST_ONCE |
Pulsar service URL | pulsar://localhost:6650 |
以create命令为例。下面的函数具有函数名(MyFunction)、租户(public)、命名空间(default)、订阅类型(SHARED)、处理保证(至少一次)的默认值,和Pulsar服务URL:pulsar://localhost:6650
bin/pulsar-admin functions create \
--jar my-pulsar-functions.jar \
--classname org.example.MyFunction \
--inputs my-function-input-topic1,my-function-input-topic2
在localrun模式下部署函数时,它将运行在我们在笔记本电脑上输入命令的机器上,或者运行在AWS EC2实例中
可以使用localrun命令运行函数的单个实例。要运行多个实例,可以多次使用localrun命令
以localrun命令的使用示例如下:
bin/pulsar-admin functions localrun \
--py myfunc.py \
--classname myfunc.SomeFunction \
--inputs persistent://public/default/input-1 \
--output persistent://public/default/output-1
在localrun模式下,Java函数使用线程运行时;Python和Go函数使用进程运行时。
默认情况下,该函数通过本地代理服务URL连接到运行在同一台机器上的Pulsar集群。如果希望将其连接到非本地Pulsar集群,可以使用–brokerServiceUrl标志指定不同的代理服务URL
bin/pulsar-admin functions localrun \
--broker-service-url pulsar://my-cluster-host:6650 \
# Other function parameters
在集群模式下部署函数会将函数上传到函数worker,这意味着函数是由该worker调度的
在集群模式下部署功能,使用create命令:
bin/pulsar-admin functions create \
--py myfunc.py \
--classname myfunc.SomeFunction \
--inputs persistent://public/default/input-1 \
--output persistent://public/default/output-1
当需要更新运行在集群模式下的函数时,使用update命令:
bin/pulsar-admin functions update \
--py myfunc.py \
--classname myfunc.SomeFunction \
--inputs persistent://public/default/new-input-topic \
--output persistent://public/default/new-output-topic
在集群模式下运行函数时,可以指定可以分配给每个函数实例的资源
下表概述了可以分配给函数实例的资源:
资源 | 规定 | 支持运行时 |
---|---|---|
CPU | cpu核数 | Kubernetes |
RAM | 内存 | Kubernetes |
Disk space | 磁盘空间 | Kubernetes |
例如,下面的命令为一个函数分配8个内核、8GB RAM和10GB磁盘空间:
bin/pulsar-admin functions create \
--jar target/my-functions.jar \
--classname org.example.functions.MyFunction \
--cpu 8 \
--ram 8589934592 \
--disk 10737418240
在集群模式下,我们可以指定并行度(要运行的实例数量)以启用一个函数的并行处理
实例1:
在部署函数时指定create命令的–parallelism标志
bin/pulsar-admin functions create \
--parallelism 3 \
# Other function info
对于已存在的函数,可以使用update命令调整并行度。
实例2:
在通过YAML部署函数配置时指定并行度参数:
# function-config.yaml
parallelism: 3
inputs:
- persistent://public/default/input-1
output: persistent://public/default/output-1
# other parameters
对于已存在的函数,可以使用update命令调整并行度:
bin/pulsar-admin functions update \
--function-config-file function-config.yaml
要执行端到端加密,我们可以在pulse-admin CLI中使用应用程序配置的公钥和私钥对指定--producer-config和--input-specs
只有拥有有效密钥的使用者才能解密加密的消息
加密/解密的相关配置 CryptoConfig包含了两个ProducerConfig 和inputSpecs,关于CryptoConfig的具体可配置字段如下:
public class CryptoConfig {
private String cryptoKeyReaderClassName;
private Map<String, Object> cryptoKeyReaderConfig;
private String[] encryptionKeys;
private ProducerCryptoFailureAction producerCryptoFailureAction;
private ConsumerCryptoFailureAction consumerCryptoFailureAction;
}
包管理服务支持版本管理和简化升级和回滚用于函数、接收器和源的进程。在不同的名称空间中使用相同的函数、接收器和源时,可以将它们上传到公共包管理系统
启用包管理服务后,可以将功能包上传到服务并获取包URL。因此,您可以通过设置–jar、–py或–go到包URL来创建函数
缺省情况下,关闭包管理服务。在集群中启用它,需要在conf/broker.conf配置如下:
enablePackagesManagement=true
packagesManagementStorageProvider=org.apache.pulsar.packages.management.storage.bookkeeper.BookKeeperPackagesStorageProvider
packagesReplicas=1
packagesManagementLedgerRootPath=/ledgers
要确保生产部署(具有多个代理的集群)中的高可用性,请将packagesReplicas设置为与broker数量相等。仅在单节点集群部署时,默认值为“1”
与内置连接器类似,被打包为NAR的Java函数代码放在函数工作者的函数目录中,在启动时加载,并可以在创建函数时引用
例如,如果你有一个内置函数的名称exclamation 在Pulsar-io.yaml,你可以创建一个函数实例:
bin/pulsar-admin functions create \
--function-type exclamation \
--inputs persistent://public/default/input-1 \
--output persistent://public/default/output-1
触发函数意味着通过CLI向其中一个输入主题生成消息来调用函数。可以在任何时候使用trigger命令触发某个函数
使用pulsar-admin CLI,我们可以向函数发送消息,而无需使用pulsar-client工具或特定于语言的客户端库
要了解如何触发函数,可以从一个Python函数开始,该函数根据输入返回一个简单的字符串,如下所示:
# myfunc.py
def process(input):
return "This function has been triggered with a value of {0}".format(input)
bin/pulsar-admin functions create \
--tenant public \
--namespace default \
--name myfunc \
--py myfunc.py \
--classname myfunc \
--inputs persistent://public/default/in \
--output persistent://public/default/out
bin/pulsar-client consume persistent://public/default/out \
--subscription-name my-subscription \
--num-messages 0 # Listen indefinitely
bin/pulsar-admin functions trigger \
--tenant public \
--namespace default \
--name myfunc \
--trigger-value "hello world"
在trigger命令中,topic信息是不需要的。只需要指定函数的基本信息,如租户、命名空间、函数名等
我们可以使用Java,Python和Go的API进行开发Pulsar function
语言原生接口提供了一种简单而干净的编写Java/Python函数,通过向所有传入字符串添加感叹号并将输出字符串发布到主题。它没有外部依赖。
package org.tony.pulsar.function.develop;
import java.util.function.Function;
/**
* 通过原生api开发function
* @author Tony
*/
public class JavaNativeFunction implements Function<String,String> {
@Override
public String apply(String s) {
return String.format("%s!",s);
}
}
SDK指定了一个包含上下文对象作为参数的功能接口
package org.tony.pulsar.function.develop;
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
/**
* 通过pulsar sdk开发pulsar function
* @author Tony
*/
public class JavaPulsarSdkDevelopFunction implements Function<String,String> {
@Override
public String process(String input, Context context) throws Exception {
return String.format("%s!",input);
}
}
函数的返回类型可以包装在Record泛型中,这使您可以更好地控制输出消息,例如主题、模式、属性等。使用Context::newOutputRecordBuilder方法来构建这个Record输出
package org.tony.pulsar.function.develop;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import org.apache.pulsar.functions.api.Record;
import java.util.HashMap;
import java.util.Map;
/**
* 包装返回类型
* @author Tony
*/
public class JavaPulsarSdkRecordFunction implements Function<String, Record<String>> {
@Override
public Record<String> process(String input, Context context) throws Exception {
String output = String.format("%s!",input);
Map<String,String> properties = new HashMap<>(context.getCurrentRecord().getProperties());
context.getCurrentRecord().getTopicName().ifPresent(topic-> properties.put("input_topic",topic));
return context.newOutputRecordBuilder(Schema.STRING).value(output).properties(properties).build();
}
}
这个扩展的Pulsar Functions SDK提供了两个额外的接口来初始化和释放外部资源
下面的示例使用了Pulsar Functions SDK for Java的扩展接口,在函数实例启动时初始化RedisClient,并在函数实例关闭时释放它:
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import io.lettuce.core.RedisClient;
public class InitializableFunction implements Function<String, String> {
private RedisClient redisClient;
private void initRedisClient(Map<String, Object> connectInfo) {
redisClient = RedisClient.create(connectInfo.get("redisURI"));
}
/**
* init的时候的操作逻辑
* @param context
*/
@Override
public void initialize(Context context) {
Map<String, Object> connectInfo = context.getUserConfigMap();
redisClient = initRedisClient(connectInfo);
}
@Override
public String process(String input, Context context) {
String value = client.get(key);
return String.format("%s-%s", input, value);
}
/**
* 关闭的时候的操作逻辑
*/
@Override
public void close() {
redisClient.close();
}
}
我们在运行和升级函数的时候,可以通过--user-config传递相关的配置,格式为json格式,key/value的形式
bin/pulsar-admin functions create \
# Other function configs
--user-config '{"word-of-the-day":"verdure"}'
我们在java中可以这样使用:
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import org.slf4j.Logger;
import java.util.Optional;
public class UserConfigFunction implements Function<String, Void> {
@Override
public void apply(String input, Context context) {
Logger LOG = context.getLogger();
Optional<String> wotd = context.getUserConfigValue("word-of-the-day");
if (wotd.isPresent()) {
LOG.info("The word of the day is {}", wotd);
} else {
LOG.warn("No word of the day provided");
}
return null;
}
}
要确保正在运行的函数在任何时候都是健康的,可以配置函数将任意指标发布到可查询的指标接口
注意:使用Java或Python的语言原生接口无法向Pulsar发布指标和统计信息
可以使用内置指标和自定义指标来监视函数:
下面是一个示例,说明了如何通过在每个键的基础上使用Context对象来定制Java、Python和Go函数的指标。
例如,可以为process-count键设置一个指标,并在函数每次处理消息时为eleven-count键设置另一个指标:
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
public class MetricRecorderFunction implements Function<Integer, Void> {
@Override
public void apply(Integer input, Context context) {
// Records the metric 1 every time a message arrives
context.recordMetric("hit-count", 1);
// Records the metric only if the arriving number equals 11
if (input == 11) {
context.recordMetric("elevens-count", 1);
}
return null;
}
}
如果我们需要启用安全设置,我们需要在function workers中启用安全设置
配置function workers
要使用上下文中的secret api,我们需要为函数工作者设置以下两个参数:
Pulsar Functions提供了两种类型的SecretsProviderConfigurator实现,它们都可以直接用作secretsProviderConfiguratorClassName的值
function workers使用org.apache.pulsar.functions.secretsproviderconfigurator.SecretsProviderConfigurator接口在启动函数实例时选择SecretsProvider类名及其相关配置
函数实例使用org.apache.pulsar.functions.secretsprovider.SecretsProvider接口来获取Secrets。SecretsProvider使用的实现由SecretsProviderConfigurator决定
一旦设置了SecretsProviderConfigurator,我们就可以使用上下文对象获取secrets,如下所示:
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import org.slf4j.Logger;
public class GetSecretValueFunction implements Function<String, Void> {
@Override
public Void process(String input, Context context) throws Exception {
Logger LOG = context.getLogger();
String secretValue = context.getSecret(input);
if (!secretValue.isEmpty()) {
LOG.info("The secret {} has value {}", intput, secretValue);
} else {
LOG.warn("No secret with key {}", input);
}
return null;
}
}
Pulsar函数使用Apache BookKeeper作为状态存储接口。Pulsar集成了BookKeeper表服务来存储函数的状态。例如,WordCount函数可以通过state api将其计数器的状态存储到BookKeeper表服务中。
状态是键值对,其中一个键是一个字符串和它的值是任意二进制数据,计数器被存储为64位的二进制。键的作用域为单个函数,并在该函数的实例之间共享
Pulsar函数公开了用于改变和访问状态的api
void incrCounter(String key, long amount);
要异步增加计数器,可以使用incrCounterAsync
CompletableFuture<Void> incrCounterAsync(String key, long amount);
long getCounter(String key);
要异步检索由incrCounterAsync改变的计数器
CompletableFuture<Long> getCounterAsync(String key);
void putState(String key, ByteBuffer value);
要异步更新给定键的状态,可以使用putStateAsync
CompletableFuture<Void> putStateAsync(String key, ByteBuffer value);
ByteBuffer getState(String key);
要异步检索给定键的状态,可以使用getStateAsync
CompletableFuture<ByteBuffer> getStateAsync(String key);
void deleteState(String key);
除了使用State api将函数的状态存储在Pulsar的状态存储中并从存储中检索它之外,我们还可以使用CLI命令查询函数的状态
bin/pulsar-admin functions querystate \
--tenant \
--namespace \
--name \
--state-storage-url \
--key \
[---watch]
WordCountFunction的例子演示了如何在脉冲星函数中存储状态:
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import java.util.Arrays;
public class WordCountFunction implements Function<String, Void> {
@Override
public Void process(String input, Context context) throws Exception {
Arrays.asList(input.split("\\.")).forEach(word -> context.incrCounter(word, 1));
return null;
}
}
使用Java SDK的Pulsar函数可以访问Pulsar管理客户端,这允许Pulsar管理客户端管理对Pulsar集群的API调用
下面是如何使用从函数上下文中公开的Pulsar管理客户端的示例:
import org.apache.pulsar.client.admin.PulsarAdmin;
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
/**
* In this particular example, for every input message,
* the function resets the cursor of the current function's subscription to a
* specified timestamp.
*/
public class CursorManagementFunction implements Function<String, String> {
@Override
public String process(String input, Context context) throws Exception {
PulsarAdmin adminClient = context.getPulsarAdmin();
if (adminClient != null) {
String topic = context.getCurrentRecord().getTopicName().isPresent() ?
context.getCurrentRecord().getTopicName().get() : null;
String subName = context.getTenant() + "/" + context.getNamespace() + "/" + context.getFunctionName();
if (topic != null) {
// 1578188166 below is a random-pick timestamp
adminClient.topics().resetCursor(topic, subName, 1578188166);
return "reset cursor successfully";
}
}
return null;
}
}
使我们的函数能够访问Pulsar管理客户端,需要在conf/functions_worker.yml设置exposeAdminClientEnabled=true
要测试它是否启用,可以使用命令pulsar-admin functions localrun,并带标志–web-service-url,如下所示:
bin/pulsar-admin functions localrun \
--jar my-functions.jar \
--classname my.package.CursorManagementFunction \
--web-service-url http://pulsar-web-service:8080 \
# Other function configs
Pulsar有一个内置的模式注册表,并与流行的模式类型捆绑在一起,如Avro、JSON和Protobuf。Pulsar Functions可以利用来自输入主题的现有模式信息并派生输入类型。模式注册表也适用于输出主题
当向Pulsar主题发布数据或从Pulsar主题消费数据时,Pulsar函数使用SerDe(序列化和反序列化)。
默认情况下,SerDe的工作方式取决于我们为特定函数使用的语言(Java或Python)。
但是,在这两种语言中,我们都可以为更复杂的、特定于应用程序的类型编写自定义SerDe逻辑
以下基本Java类型是内置的,默认情况下支持Java函数:string、double、integer、float、long、short和byte。要自定义Java类型,需要实现以下接口:
public interface SerDe<T> {
T deserialize(byte[] input);
byte[] serialize(T input);
}
SerDe以以下方式处理Java函数:
例如,假设我们正在编写一个处理tweet对象的函数。我们可以参考下面的Java中的Tweet类示例:
public class Tweet {
private String username;
private String tweetContent;
public Tweet(String username, String tweetContent) {
this.username = username;
this.tweetContent = tweetContent;
}
// Standard setters and getters
}
要在函数之间直接传递Tweet对象,需要提供一个自定义SerDe类。在下面的例子中,Tweet对象基本上是字符串,用户名和Tweet内容由|分隔:
package com.example.serde;
import org.apache.pulsar.functions.api.SerDe;
import java.util.regex.Pattern;
public class TweetSerde implements SerDe<Tweet> {
public Tweet deserialize(byte[] input) {
String s = new String(input);
String[] fields = s.split(Pattern.quote("|"));
return new Tweet(fields[0], fields[1]);
}
public byte[] serialize(Tweet input) {
return "%s|%s".format(input.getUsername(), input.getTweetContent()).getBytes();
}
}
要将定制的SerDe应用于特定函数,我们需要:
下面是使用create命令通过应用自定义SerDe来部署函数的示例:
bin/pulsar-admin functions create \
--jar /path/to/your.jar \
--output-serde-classname com.example.serde.TweetSerde \
# Other function attributes
注意:自定义SerDe类必须打包到函数jar中。
要调试函数启动失败的原因,可以查看logs/functions////-.log文件
与任何具有输入和输出的函数一样,我们可以以与测试任何其他函数类似的方式测试Pulsar function
Pulsar使用TestNG进行测试
例如,如果您通过Java的语言本地接口编写了以下函数:
import java.util.function.Function;
public class JavaNativeExclamationFunction implements Function<String, String> {
@Override
public String apply(String input) {
return String.format("%s!", input);
}
}
我们可以编写一个简单的单元测试来测试该函数:
@Test
public void testJavaNativeExclamationFunction() {
JavaNativeExclamationFunction exclamation = new JavaNativeExclamationFunction();
String output = exclamation.apply("foo");
Assert.assertEquals(output, "foo!");
}
下面的示例是通过Java SDK编写的:
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
public class ExclamationFunction implements Function<String, String> {
@Override
public String process(String input, Context context) {
return String.format("%s!", input);
}
}
我们可以编写一个单元测试来测试这个函数并模拟Context参数,如下所示:
@Test
public void testExclamationFunction() {
ExclamationFunction exclamation = new ExclamationFunction();
String output = exclamation.process("foo", mock(Context.class));
Assert.assertEquals(output, "foo!");
}
在localrun模式下,函数消耗并生成实际数据到Pulsar集群,并反映该函数在Pulsar集群中的运行方式。这提供了一种测试函数的方法,并允许我们在本地机器上作为线程启动函数实例,以便于调试
使用localrun模式进行调试仅适用于Pulsar 2.4.0或更高版本中的Java函数
在使用localrun模式之前,我们需要添加以下依赖项:
org.apache.pulsar
pulsar-functions-local-runner-original
${pulsar.version}
com.google.protobuf
protobuf-java
3.21.9
例如,我们可以以以下方式运行函数:
FunctionConfig functionConfig = new FunctionConfig();
functionConfig.setName(functionName);
functionConfig.setInputs(Collections.singleton(sourceTopic));
functionConfig.setClassName(ExclamationFunction.class.getName());
functionConfig.setRuntime(FunctionConfig.Runtime.JAVA);
functionConfig.setOutput(sinkTopic);
LocalRunner localRunner = LocalRunner.builder().functionConfig(functionConfig).build();
localRunner.start(true);
我们可以使用IDE调试函数。设置断点并手动步进函数以使用实际数据进行调试:
下面的代码示例展示了如何在localrun模式下运行函数
public class ExclamationFunction implements Function<String, String> {
@Override
public String process(String s, Context context) throws Exception {
return s + "!";
}
public static void main(String[] args) throws Exception {
FunctionConfig functionConfig = new FunctionConfig();
functionConfig.setName("exclamation");
functionConfig.setInputs(Collections.singleton("input"));
functionConfig.setClassName(ExclamationFunction.class.getName());
functionConfig.setRuntime(FunctionConfig.Runtime.JAVA);
functionConfig.setOutput("output");
LocalRunner localRunner = LocalRunner.builder().functionConfig(functionConfig).build();
localRunner.start(false);
}
}
使用Pulsar Functions时,可以将函数中预定义的日志生成到指定的日志主题,并配置消费者消费该日志主题的消息
例如,下面的函数根据输入的字符串是否包含单词danger记录warning级别或info级别的日志:
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import org.slf4j.Logger;
public class LoggingFunction implements Function<String, Void> {
@Override
public void apply(String input, Context context) {
Logger LOG = context.getLogger();
String messageId = new String(context.getMessageId());
if (input.contains("danger")) {
LOG.warn("A warning was received in message {}", messageId);
} else {
LOG.info("Message {} received\nContent: {}", messageId, input);
}
return null;
}
}
如示例中所示,我们可以通过context.getLogger()获取记录器,并将记录器分配给slf4j的LOG变量,这样我们就可以使用LOG变量在函数中定义所需的日志
同时,我们需要指定可以生成日志的主题。示例如下:
bin/pulsar-admin functions create \
--log-topic persistent://public/default/logging-function-logs \
# Other function configs
发布到日志主题的消息包含以下几个属性:
使用Pulsar函数CLI,我们可以使用以下子命令调试Pulsar函数
get
status
list
trigger
./bin/pulsar-admin functions get public/default/ExclamationFunctio6
或者,我们可以指定–name、–namespace和–tenant,如下所示:
./bin/pulsar-admin functions get \
--tenant public \
--namespace default \
--name ExclamationFunctio6
如下所示,get命令显示了关于ExclamationFunctio6函数的输入、输出、运行时和其他信息。
{
"tenant": "public",
"namespace": "default",
"name": "ExclamationFunctio6",
"className": "org.example.test.ExclamationFunction",
"inputSpecs": {
"persistent://public/default/my-topic-1": {
"isRegexPattern": false
}
},
"output": "persistent://public/default/test-1",
"processingGuarantees": "ATLEAST_ONCE",
"retainOrdering": false,
"userConfig": {},
"runtime": "JAVA",
"autoAck": true,
"parallelism": 1
}
bin/pulsar-admin functions list \
--tenant public \
--namespace default
./bin/pulsar-admin functions status \
--tenant public \
--namespace default \
--name ExclamationFunctio6
bin/pulsar-admin functions stats \
--tenant public \
--namespace default \
--name ExclamationFunctio6
./bin/pulsar-admin functions trigger \
--tenant public \
--namespace default \
--name ExclamationFunctio6 \
--topic persistent://public/default/my-topic-1 \
--trigger-value "hello pulsar functions"
打包Java函数有两种方法,即uber JAR和NAR
如果我们计划打包和分发我们的函数以供其他人使用,那么我们有义务对自己的代码进行适当的许可和版权保护。
请记住将许可证和版权添加到代码使用的所有库和我们的发行版中。
如果使用NAR方法,NAR插件会自动在生成的NAR包中创建一个DEPENDENCIES文件,其中包括函数的所有库的适当许可和版权
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>java-functiongroupId>
<artifactId>java-functionartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.apache.pulsargroupId>
<artifactId>pulsar-functions-apiartifactId>
<version>2.11.1version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-pluginartifactId>
<configuration>
<appendAssemblyId>falseappendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependenciesdescriptorRef>
descriptorRefs>
<archive>
<manifest>
<mainClass>org.example.test.ExclamationFunctionmainClass>
manifest>
archive>
configuration>
<executions>
<execution>
<id>make-assemblyid>
<phase>packagephase>
<goals>
<goal>assemblygoal>
goals>
execution>
executions>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.11.0version>
<configuration>
<release>17release>
configuration>
plugin>
plugins>
build>
project>
mvn package
Java函数打包完成后,会自动创建target 目录。打开target目录,检查是否有类似于java-function-1.0-SNAPSHOT.jar的JAR包
docker exec -it [CONTAINER ID] /bin/bash
docker cp <path of java-function-1.0-SNAPSHOT.jar> CONTAINER ID:/pulsar
./bin/pulsar-admin functions localrun \
--classname org.example.test.ExclamationFunction \
--jar java-function-1.0-SNAPSHOT.jar \
--inputs persistent://public/default/my-topic-1 \
--output persistent://public/default/test-1 \
--tenant public \
--namespace default \
--name JavaFunction
4.0.0
java-function
java-function
1.0-SNAPSHOT
org.apache.pulsar
pulsar-functions-api
2.11.1
org.apache.nifi
nifi-nar-maven-plugin
1.2.0
org.apache.maven.plugins
maven-compiler-plugin
3.11.0
17
我们也可以创建pulsar-io.yaml文件在resources/META-INF/services,在下面的代码示例中,functionClass的值是函数类名。该名称是函数作为内置函数部署时使用的名称
name: java-function
description: my java function
functionClass: org.example.test.ExclamationFunction
mvn package
Java函数打包完成后,会自动创建target 目录。打开target目录,检查是否有类似于java-function-1.0-SNAPSHOT.nar的NAR包
docker cp <path of java-function-1.0-SNAPSHOT.nar> CONTAINER ID:/pulsar
./bin/pulsar-admin functions localrun \
--jar java-function-1.0-SNAPSHOT.nar \
--inputs persistent://public/default/my-topic-1 \
--output persistent://public/default/test-1 \
--tenant public \
--namespace default \
--name JavaFunction
可以使用预定义的YAML文件来配置功能。下表概述了所需的字段和参数:
字段名 | 类型 | 相关命令参数 | 描述 |
---|---|---|---|
runtimeFlags | String | N/A | 我们想传递给运行时的任何标志(仅适用于进程和Kubernetes运行时) |
tenant | String | –tenant | 函数的租户 |
namespace | String | –namespace | 函数的命名空间 |
name | String | –name | 函数的名称 |
className | String | –classname | 函数的类名 |
functionType | String | –function-type | 函数的类型 |
inputs | List | -i, --inputs | 函数的输入主题。可以将多个主题指定为逗号分隔的列表 |
customSerdeInputs | Map |
–custom-serde-inputs | 从输入主题到SerDe类名的映射 |
topicsPattern | String | –topics-pattern | 要从名称空间下的主题列表中使用的主题模式,注意:–input和–topic-pattern互斥。对于Java函数,需要在–custom-serde-inputs中为模式添加SerDe类名 |
customSchemaInputs | Map |
–custom-schema-inputs | 从输入主题到模式属性的映射 |
customSchemaOutputs | Map |
–custom-schema-outputs | 从输出主题到模式属性的映射 |
inputSpecs | Map |
–input-specs | 从输入到自定义配置的映射 |
output | String | -o, --output | 函数的输出主题。如果不指定,则不输出 |
producerConfig | ProducerConfig | –producer-config | 生产者的自定义配置 |
outputSchemaType | String | -st, --schema-type | 用于消息输出的内置模式类型或自定义模式类名称 |
outputSerdeClassName | String | –output-serde-classname | 用于消息输出的SerDe类 |
logTopic | String | –log-topic | 生成函数日志的主题 |
processingGuarantees | String | –processing-guarantees | 应用于函数的处理保证(交付语义)。可用值:至少一次,最多一次,有效一次,手动 |
retainOrdering | Boolean | –retain-ordering | 函数是否按顺序消费和处理消息 |
retainKeyOrdering | String | –retain-key-ordering | 函数是否按键顺序消费和处理消息 |
batchBuilder | String | –batch-builder | 使用 producerConfig.batchBuilder 替换. 注意:batchBuilder将很快在代码中弃用 |
forwardSourceMessageProperty | Boolean | –forward-source-message-property | 在处理过程中是否将输入消息的属性转发到输出主题。设置为false时,表示关闭转发功能 |
userConfig | Map |
–user-config | 用户定义的配置键/值 |
secrets | Map |
–secrets | 从secretName到对象的映射,这些对象封装了底层秘密提供程序获取secrets的方式 |
runtime | String | N/A | 函数的运行时。取值:java、python、go |
autoAck | Boolean | –auto-ack | 框架是否自动确认消息,在未来的版本中将弃用此配置。如果指定传递语义,框架将自动确认消息。如果我们不希望框架自动ack消息,请将processinGassurances设置为MANUAL |
maxMessageRetries | Int | –max-message-retries | 在放弃之前重试处理消息的次数 |
deadLetterTopic | String | –dead-letter-topic | 用于存储未成功处理的消息的主题 |
subName | String | –subs-name | 如果需要,输入主题消费者使用的脉冲星源订阅的名称 |
parallelism | Int | –parallelism | 函数的并行性因子,即要运行的函数实例的数量 |
resources | Resources | N/A | N/A |
fqfn | String | –fqfn | 函数的完全限定函数名(FQFN) |
windowConfig | WindowConfig | N/A | N/A |
timeoutMs | Long | –timeout-ms | 消息超时(以毫秒为单位) |
jar | String | –jar | 函数(用Java编写)的JAR文件的路径。它还支持worker可以下载包的URL路径,包括HTTP、HTTPS、file(文件协议,假设文件已经存在于worker主机上)和function(来自包管理服务的包URL) |
py | String | –py | 主python/python wheel 文件的路径(用python编写)。它还支持工人可以从HTTP,HTTPS,FILE(文件协议)和功能(Package URL中的文件协议)中下载软件包的URL路径(文件协议) |
go | String | –go | 函数的主Go可执行二进制文件的路径(用Go语言编写)。它还支持worker可以下载包的URL路径,包括HTTP、HTTPS、file(文件协议,假设文件已经存在于worker主机上)和function(来自包管理服务的包URL) |
cleanupSubscription | Boolean | N/A | 当函数被删除时,是否应该删除函数创建或使用的订阅 |
customRuntimeOptions | String | –custom-runtime-options | 对选项进行编码以自定义运行时的字符串 |
maxPendingAsyncRequests | Int | –max-message-retries | 每个实例挂起的最大异步请求数,以避免大量并发请求 |
exposePulsarAdminClientEnabled | Boolean | N/A | Pulsar管理客户端是否公开给函数上下文。缺省情况下,禁用该功能 |
subscriptionPosition | String | –subs-position | 用于从指定位置消费消息的pulsar源订阅的位置。默认值为“Latest” |
字段名 | 类型 | 相关命令参数 | 描述 |
---|---|---|---|
schemaType | String | N/A | N/A |
serdeClassName | String | N/A | N/A |
isRegexPattern | Boolean | N/A | N/A |
schemaProperties | Map |
N/A | N/A |
consumerProperties | Map |
N/A | N/A |
receiverQueueSize | Int | N/A | N/A |
cryptoConfig | CryptoConfig | N/A | |
poolMessages | Boolean | N/A | N/A |
字段名 | 类型 | 相关命令参数 | 描述 |
---|---|---|---|
maxPendingMessages | Int | N/A | 用于保存等待从代理接收确认的消息的队列的最大大小 |
maxPendingMessagesAcrossPartitions | Int | N/A | 跨所有分区的maxPendingMessages的数量 |
useThreadLocalProducers | Boolean | N/A | N/A |
cryptoConfig | CryptoConfig | N/A | |
batchBuilder | String | –batch-builder | 批量construction方法的类型。取值为DEFAULT和KEY_BASED。默认值为DEFAULT |
字段名 | 类型 | 相关命令参数 | 描述 |
---|---|---|---|
cpu | double | –cpu | 需要为每个函数实例分配的核心CPU(仅适用于Kubernetes运行时) |
ram | Long | –ram | 需要分配每个功能实例的字节中的RAM(仅适用于process/kubernetes运行时) |
disk | Long | –disk | 每个函数实例需要分配的磁盘字节数(仅适用于Kubernetes运行时) |
字段名 | 类型 | 相关命令参数 | 描述 |
---|---|---|---|
windowLengthCount | Int | –window-length-count | 每个窗口的消息数 |
windowLengthDurationMs | Long | –window-length-duration-ms | 每个窗口的持续时间(毫秒) |
slidingIntervalCount | Int | –sliding-interval-count | 窗口滑动后的消息数 |
slidingIntervalDurationMs | Long | –sliding-interval-duration-ms | 窗口滑动的持续时间 |
lateDataTopic | String | N/A | N/A |
maxLagMs | Long | N/A | N/A |
watermarkEmitIntervalMs | Long | N/A | N/A |
timestampExtractorClassName | String | N/A | N/A |
actualWindowFunctionClassName | String | N/A | N/A |
字段名 | 类型 | 相关命令参数 | 描述 |
---|---|---|---|
cryptoKeyReaderClassName | String | N/A | |
cryptoKeyReaderConfig | Map |
N/A | N/A |
encryptionKeys | String[] | N/A | N/A |
producerCryptoFailureAction | ProducerCryptoFailureAction | N/A | N/A |
consumerCryptoFailureAction | ConsumerCryptoFailureAction | N/A | N/A |
tenant: "public"
namespace: "default"
name: "config-file-function"
inputs:
- "persistent://public/default/config-file-function-input-1"
- "persistent://public/default/config-file-function-input-2"
output: "persistent://public/default/config-file-function-output"
jar: "function.jar"
parallelism: 1
resources:
cpu: 8
ram: 8589934592
autoAck: true
userConfig:
foo: "bar"
Java SDK提供了对可由Window 函数使用的Window 上下文对象的访问。这个上下文对象为脉冲星窗口函数提供了大量的信息和功能,如下所示
获取input topics:
public class GetInputTopicsWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
Collection<String> inputTopics = context.getInputTopics();
System.out.println(inputTopics);
return null;
}
}
获取 output topics:
public class GetOutputTopicWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
String outputTopic = context.getOutputTopic();
System.out.println(outputTopic);
return null;
}
}
获取tenant:
public class GetTenantWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
String tenant = context.getTenant();
System.out.println(tenant);
return null;
}
}
获取命名空间:
public class GetNamespaceWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
String ns = context.getNamespace();
System.out.println(ns);
return null;
}
}
获取函数名:
public class GetNameOfWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
String functionName = context.getFunctionName();
System.out.println(functionName);
return null;
}
}
获取函数id:
public class GetFunctionIDWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
String functionID = context.getFunctionId();
System.out.println(functionID);
return null;
}
}
获取函数版本:
public class GetVersionOfWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
String functionVersion = context.getFunctionVersion();
System.out.println(functionVersion);
return null;
}
}
获取实例id:
public class GetInstanceIDWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
int instanceId = context.getInstanceId();
System.out.println(instanceId);
return null;
}
}
获取实例数:
public class GetNumInstancesWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
int numInstances = context.getNumInstances();
System.out.println(numInstances);
return null;
}
}
获取output schema type:
public class GetOutputSchemaTypeWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
String schemaType = context.getOutputSchemaType();
System.out.println(schemaType);
return null;
}
}
使用Java SDK的Pulsar window functions可以访问SLF4j Logger对象,该对象可用于在所选日志级别生成日志
import java.util.Collection;
import org.apache.pulsar.functions.api.Record;
import org.apache.pulsar.functions.api.WindowContext;
import org.apache.pulsar.functions.api.WindowFunction;
import org.slf4j.Logger;
public class LoggingWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
Logger log = context.getLogger();
for (Record<String> record : inputs) {
log.info(record + "-window-log");
}
return null;
}
}
如果需要函数生成日志,请在创建或运行函数时指定日志主题:
bin/pulsar-admin functions create \
--jar my-functions.jar \
--classname my.package.LoggingFunction \
--log-topic persistent://public/default/logging-function-logs \
# Other function configs
当我们运行或更新使用SDK创建的Pulsar函数时,可以使用–user-config标志将任意键/值对传递给它们。键/值对必须指定为JSON
bin/pulsar-admin functions create \
--name word-filter \
--user-config '{"forbidden-word":"rosebud"}' \
# Other function configs
我们可以使用以下api获取window function的用户定义信息
getUserConfigMap:
/**
* Get a map of all user-defined key/value configs for the function.
*
* @return The full map of user-defined config values
*/
Map<String, Object> getUserConfigMap();
getUserConfigValue:
/**
* Get any user-defined key/value.
*
* @param key The key
* @return The Optional value specified by the user for that key.
*/
Optional<Object> getUserConfigValue(String key);
getUserConfigValueOrDefault:
/**
* Get any user-defined key/value or a default value if none is present.
*
* @param key
* @param defaultValue
* @return Either the user config value associated with a given key or a supplied default value
*/
Object getUserConfigValueOrDefault(String key, Object defaultValue);
Java SDK上下文对象使我们可以通过命令行访问向Pulsar窗口函数提供的键/值对(作为JSON)
对于传递给Java窗口函数的所有密钥/值对,键和值都是字符串。要将该值设置为不同的类型,我们需要从字符串类型中对其进行验证
bin/pulsar-admin functions create \
--user-config '{"word-of-the-day":"verdure"}' \
# Other function configs
每次调用该函数时,UserConfigFunction都会记录字符串“今日单词是绿色的”(这意味着每次消息到达时)。
只有当通过多种方式(如命令行工具或REST API)使用新的配置值更新函数时,word-of-the-day的用户配置才会更改
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
import org.slf4j.Logger;
import java.util.Optional;
public class UserConfigWindowFunction implements WindowFunction<String, String> {
@Override
public String process(Collection<Record<String>> input, WindowContext context) throws Exception {
Optional<Object> whatToWrite = context.getUserConfigValue("WhatToWrite");
if (whatToWrite.get() != null) {
return (String)whatToWrite.get();
} else {
return "Not a nice way";
}
}
}
如果没有提供值,则可以访问整个用户配置映射或设置默认值
// Get the whole config map
Map<String, String> allConfigs = context.getUserConfigMap();
// Get value or resort to default
String wotd = context.getUserConfigValueOrDefault("word-of-the-day", "perspicacious");
可以使用context.publish()接口发布任意数量的结果
public class PublishWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> input, WindowContext context) throws Exception {
String publishTopic = (String) context.getUserConfigValueOrDefault("publish-topic", "publishtopic");
String output = String.format("%s!", input);
context.publish(publishTopic, output);
return null;
}
}
Pulsar window function可以将任意的度量发布到度量接口,并可以查询
注意:如果Pulsar window functions使用Java的语言原生接口,则该函数无法向Pulsar发布度量和统计信息
我们可以在每个键的基础上使用上下文对象记录指标:
import java.util.Collection;
import org.apache.pulsar.functions.api.Record;
import org.apache.pulsar.functions.api.WindowContext;
import org.apache.pulsar.functions.api.WindowFunction;
/**
* Example function that wants to keep track of
* the event time of each message sent.
*/
public class UserMetricWindowFunction implements WindowFunction<String, Void> {
@Override
public Void process(Collection<Record<String>> inputs, WindowContext context) throws Exception {
for (Record<String> record : inputs) {
if (record.getEventTime().isPresent()) {
context.recordMetric("MessageEventTime", record.getEventTime().get().doubleValue());
}
}
return null;
}
}
pulsar window function使用Apache BookKeeper作为状态存储接口。Apache Pulsar安装(包括独立安装)包括BookKeeper bookies的部署
Apache Pulsar集成了Apache BookKeeper表服务来存储函数的状态。例如,WordCount函数可以通过Pulsar Functions状态api将其计数器状态存储到BookKeeper表服务中
状态是键值对,其中键是字符串,值是任意的二进制数据,将示例存储为64位大型二进制值。键的范围为单个pulsar function,并在该function的实例之间共享
目前,Pulsar window function公开Java API来访问、更新和管理状态。当我们使用Java SDK函数时,可以在上下文对象中使用这些api
Java API | 描述 |
---|---|
incrCounter | 增加按键引用的内置分布式计数器 |
getCounter | 获取键的计数器值 |
putState | 更新键的状态值 |
我们可以使用以下api来访问、更新和管理Java窗口函数中的状态:
incrCounter:
/**
* Increment the built-in distributed counter referred by key
* @param key The name of the key
* @param amount The amount to be incremented
*/
void incrCounter(String key, long amount);
getCounter:
/**
* Retrieve the counter value for the key.
*
* @param key name of the key
* @return the amount of the counter value for this key
*/
long getCounter(String key);
putState:
/**
* Update the state value for the key.
*
* @param key name of the key
* @param value state value of the key
*/
void putState(String key, ByteBuffer value);