pulsar包括三个部分:
核心组成:
broker集群: 负责接收用户请求。这里我们仅启动一个broker
zookeeper集群: 保存集群元数据。这里我们的集群仅配置一个zk节点。
bookkeeper集群:保存数据。这里我们的集群配置三个bookkeeper节点。因为默认的配置是w=2,r=2,所以我们这里配置3个节点(bookkeeper是无主结构,要求w+r>n)。当然,简单起见也可以只配置1个节点,不过要改启动配置,否则会无法写入和读取
集群简介:
1:client向一个broker发送请求,请求读取topic a,如果该topic不归broker管,则broker把该topic所属的broker ip发回给client,client再去请求这个topic所在的broker,反之broker直接处理。
2:当broker启动的时候,因为broker是无状态的,也就是不存储任何信息,所以他就要去一个元数据存放的地方拉取集群元数据,包括有哪些topic,这个broker要申请哪些topic的管理权限(申请以后其他broker不管理这些topic)等,这个元数据中心可以用etcd、zk等,常用的是zk,我们会在broker的配置文件里把这个元数据库的类型(etcd/zk/...)以及url地址告诉broker,然后broker启动后就去这个地址拉取元数据,我们这里就配置一个zk集群来当做元数据库。!!bookkeeper节点与bookkeeper节点之间无需互相感知,对于每个bookkeeper节点,只需要知道一个zk地址就行了,在启动时无需知道其他bookkeeper节点的存在。
3:broker是无状态的,不会保存元数据也不会存储数据,所以还需要一个数据库来存储数据,pulsar使用bookkeeper集群来存储数据。broker收到client的写入请求后,就调用相关api把数据写入bookkeeper。因为broker是通过元数据库来知道集群含有哪些bookkeeper,所以bookkeeper启动时需要把自己注册到zookeeper中。也就是相当于解耦,broker和bookkeeper通过zk来知道对方的存在。
环境搭建:
前言:
1:我们的一期目标是调试broker,不包括bookkeeper,所以搭建的时候对于zk和bookkeeper我们是直接从网上下载可执行包,对于broker,我们则是通过idea编译的方式来debug运行。
2:注意zk和broker、bookkeeper之间的版本兼容性。pulsar3.3+版本用到的zk是3.9.2版本,否则broker会连不上zk。
3:pulsar broker只能在linux下编译运行,所以我们需要新建一个虚拟机,然后安装idea,然后编译运行(略),否则在windows下通过idea启动broker的时候,broker会启动失败。
搭建步骤:
1:直接搭建zk集群(1个节点)。我是用k8s来部署的,所以我这里就列出yaml文件,如果不用k8s则直接使用相应的命令即可。
2:搭建bookkeeper集群(3个节点)。一样是用k8s来部署的
3:idea编译broker。
4:通过./bin/pulsar-admin手动创建cluster。目前的pulsar必须手动创建cluster才能访问,暂时还不懂为什么。
5:通过./bin/pulsar-client或者代码的方式来检测集群部署是否成功。
1:k8s搭建zk集群(一个节点)
包括svc,pvc,statefulset,configmap
#configmap
apiVersion: v1
kind: ConfigMap
metadata:
name: zk3-9-2
namespace: test-d-mw
data:
zoo.cfg: |
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/tmp/zookeeper #这个必须配置,我们保险起见会在这个目录下挂一个pvc
clientPort=2181
clientPortAddress=0.0.0.0 #必须配置这个,否则默认时localhost,导致pod外会连不上zk
#statefuset
apiVersion: apps/v1
kind: StatefulSet
metadata:
namespace: test-d-mw
name: zk3-9-2
spec:
selector:
matchLabels:
app: zk3-9-2
replicas: 1 #单节点集群
template:
metadata:
labels:
app: zk3-9-2
spec:
containers:
- name: container
image: bitnami/zookeeper:3.9.2 #注意zk镜像版本
ports:
- containerPort: 2181
protocol: TCP
- containerPort: 2888
protocol: TCP
- containerPort: 3888
protocol: TCP
#command: ["/bin/bash", "-c", "cp /opt/bitnami/zookeeper/conf/zoo_sample.cfg /opt/bitnami/zookeeper/conf/zoo.cfg;/opt/bitnami/zookeeper/bin/zkServer.sh start;while true; do sleep 3600; done"]
#我们是通过zkServer.sh这个脚本来启动zk节点,这个脚本默认使用conf/zoo.cfg的配置文件,所以需要我们手动创建一个conf/zoo.cfg文件,我们可以直接复制zoo_sample.cfg,不过这样只能本地访问,我们需要从其他机器访问,所以需要配置一下clientPortAddress这个变量,这里我们通过configmap来是实现
#zk的conf/目录下有一个zoo_sample.cfg的文
#zkServer.sh start这个命令启动zk后就会返回,所以我们执行zkServer.sh start命令后还需要一个while循环来保持这个pod一直处于运行状态,shell通过分号来执行多条命令
command: ["/bin/bash", "-c", "/opt/bitnami/zookeeper/bin/zkServer.sh start;while true; do sleep 3600; done"]
volumeMounts:
- name: zk-conf
mountPath: /opt/bitnami/zookeeper/conf/zoo.cfg #zkServer.sh默认使用这个文件
subPath: zoo.cfg
- name: zk-data
mountPath: /tmp/zookeeper
volumes:
- name: zk-conf
configMap:
name: zk3-9-2
- name: zk-data #挂一个pvc保证pod重启数据不会丢失,因为pulsar集群中zk是一个核心节点
persistentVolumeClaim:
claimName: zk3-9-2
#svc
apiVersion: v1
kind: Service
metadata:
name: zk3-9-2
namespace: test-d-mw
spec:
selector:
app: zk3-9-2
ports:
- protocol: TCP
port: 2181 # Zookeeper 客户端端口
targetPort: 2181
name: p1
- protocol: TCP
port: 2888 # Zookeeper 集群通信端口
targetPort: 2888
name: p2
- protocol: TCP
port: 3888 # Zookeeper 选举端口
targetPort: 3888
name: p3
#pvc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: zk3-9-2
namespace: test-d-mw
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
2:搭建bookkeeper集群。
官方文档:https://bookkeeper.apache.org/docs/deployment/manual/
搭建bookkeeper集群需要两步,第一步是初始化集群元数据(就是去zk里面写一些元数据);第二步就是启动bookkeeper节点。我也是通过k8s来部署的,所以这里也是给出yaml。总共需要三个yaml,一个config,一个bookkeeper集群手动初始化,一个bookkeeper节点。其中config用来告诉bookkeeper集群,注册到哪里。
#configmap: 用来告诉bookie zk的地址
apiVersion: v1
kind: ConfigMap
metadata:
name: bookieconfig
namespace: test-d-mw
data:
bookie.conf: |
zkServers=zk3-9-2.test-d-mw.svc.cluster.local:2181
#因为当集群初始化以后zk里面会有对应的信息,然后shell format的时候会需要交互来确认是否重置,所以需要另外开一个deployment,然后进入容器,然后手动执行shell format来交互,否则就会不停的询问是否重置,然后导致cpu使用率飙升。(可以通过限制limits来解决)
apiVersion: apps/v1
kind: Deployment
metadata:
name: bookkeeper-format
namespace: test-d-mw
spec:
replicas: 1
selector:
matchLabels:
app: bookkeeper-keeper
template:
metadata:
labels:
app: bookkeeper-keeper
spec:
containers:
- name: metaformat #必须先执行 shell metaformat在zk里面格式化一些东西,一个集群只需要初始化一次,后续再运行的话这个程序会卡住
image: apache/bookkeeper
command: ["/bin/bash", "-c", "while true; do sleep 3600; done"]
#因为重置操作需要确认即需要交互,而pod有可能频繁重启,所以这里通过手动进入pod然后执行初始化命令来初始化集群
#进入容器执行 /opt/bitnami/bookkeeper/bin/bookkeeper shell metaformat
volumeMounts:
- name: bookie-config #需要知道zk的地址,我们通过configmap来实现
mountPath: /dcf/conf
env:
- name: BOOKIE_CONF
value: "/dcf/conf/bookie.conf"
volumes:
- name: bookie-config
configMap:
name: bookieconfig
#初始化完成后,就可以启动bookie节点了
#用来启动bookie节点,一个副本可以算作一个节点
apiVersion: apps/v1
kind: Deployment
metadata:
name: bookkeeper
namespace: test-d-mw
spec:
replicas: 3 #一个副本对应一个节点,3个副本就对应3个节点,因为bookkeeper是无主结构,无需副本与副本之间协调,直接新增副本就行了,因为有zk
selector:
matchLabels:
app: bookkeeper
template:
metadata:
labels:
app: bookkeeper
spec:
containers:
- name: bookkeeper-container
image: apache/bookkeeper
#启动一个bookie节点
command: ["/bin/bash", "-c", "/opt/bookkeeper/bin/bookkeeper bookie"]
volumeMounts:
- name: bookie-config #需要zk地址,这里就通过configmap方式实现
mountPath: /dcf/conf
env:
- name: BOOKIE_CONF
value: "/dcf/conf/bookie.conf"
volumes:
- name: bookie-config
configMap:
name: bookieconfig
3:idea编译运行broker
包括两步,第一步是成功编译,能运行起来;第二步是能成功运行,即可以正常接受和响应外部请求
1:编译:通过mvn来编译。!!要求java 17以上;mvn 要xx以上,我这里直接用最新的,目前是最新mvn版本 3.9.2,直接上最新版mvn
1.1:mvn package -DskipTests -Dspotbugs.skip=true -Dlicense.skip=true #需要这一步,否则会报一个xx.jar相关的问题(function相关的)
1.2:mvn install -DskipTests -Dspotbugs.skip=true -Dlicense.skip=true #因为pulsar broker依赖pulsar的其他子模块,所以编译broker之前要install
#执行完上两步后,就通过idea来编译运行broker了,broker启动时需要一个conf文件
2:成功运行。
2.1:编写一个mybroker.conf文件,假设放在路径x下,即x/mybroker.conf,文件内容如下(pulsar项目也有一个配置文件模板,可以参考,路径位于pulsar/conf/broker.conf):
managedLedgerDefaultEnsembleSize=2 #默认是2,如果只有一个节点,则需要手动改为1
managedLedgerDefaultWriteQuorum=2 #默认是2,如果只有一个节点,则需要手动改为1
managedLedgerDefaultAckQuorum=2 #默认是2,如果只有一个节点,则需要手动改为1
clusterName=pulsar-cluster #必须的
allowLoopback=true #必须开启,没搞懂
metadataStoreUrl=zk:10.255.86.228:2181 #元数据存储中心,因为pulsar broker是无状态的,所以所有数据都放在zk,所以也叫metaStore更合适,因为可以有多种存储集群比如etcd、zk、rocksdb
advertisedAddress=192.168.129.129 #客户端请求一个topic,然后请求到达broker a后,broker 会告诉client 该topic位于哪个broker,也就是把该topic对应的advertiseAddress返回给clietn,然后client再去请求这个地址,默认是localhost:6650,如果client于broker不在同一台机器上,那么client请求localhost的时候就肯定会连接失败。设置advertiedAddress后本地pulsar-client/pulsar-admin还是照样可以用
2.2:idea图形界面运行 pulsar/pulsar-broker/PulsarBrokerStart.java里面的main函数,主要,运行之前要添加命令行参数 -c x/mybroker.conf
4:通过脚本手动创建cluster,因为pulsar好像新增了一个allowCluster的概念(暂时还不懂),然后需要手动创建一个cluster,否则创建tenant、namespace、topic的时候会报错:cluster is empty。
#官方脚本使用文档:https://pulsar.apache.org/reference/#/next/
#不会的话可以直接百度或者chatgpt
./bin/pulsar-admin clusters list #查看所有集群,刚开始是空的
./bin/pulsar-admin clusters crate dcf #创建一个叫做dcf的集群
./bin/pulsar-admin clusters list #再次查看则有一个叫dcf的集群,可以创建多个cluster
./bin/pulsar-admin tenants create dcf #创建叫做dcf的租户
./bin/pulsar-admin namespaces create dcf/dcf #在租户dcf下创建叫做dcf的命名空间
./bin/pulsar-admin topics create dcf/dcf/dcf #在租户dcf下的dcf命名空间下创建叫做dcf的命名空间
#注意pulsar-admin是管理集群的脚本,比如cluster/tenant/namespace/topic
#pulsar-client是管理生产者消费者以及其他的脚本
#topic有persistent和non-persistent之分,这个如果创建的时候不指定,则默认是persistent的即持久化的
./bin/pulsar-client consume "persistent://dcf/dcf/dcf" -s "sub-1" -n 0 #启动一个消费者,叫做sub-1,订阅persistent://dcf/dcf/dcf,从最新的位置开始消费
#他会一直在这里监听,所以我们需要另外开一个窗口来发送消息到persistent://dcf/dcf/dcf
#当另一个窗口发送消息后,这个消费者窗口就会输出消费的消息
./bin/pulsar-client produce persistent://dcf/dcf/dcf --messages "Hello, World!" #发送消息到指定topic
5:通过go代码来访问pulsar集群。
官方文档:https://pulsar.apache.org/docs/next/client-libraries-go/ 注意,有多个pulsar包,不同的用途使用不同的pulsar包,比如admin包和client包。这里就不再重复了,官方示例很详细了。值得备注一下的如果你的go程序和broker不再同一台机器,那么必须在broker的配置文件mybroker.conf里配置advertisedAddress=broker的地址(默认是6650端口)。因为前面说过,client向一个broker发送请求,请求读取topic a,如果该topic不归broker管,则broker把该topic所属的broker ip发回给client,client再去请求这个topic所在的broker,而该broker的地址默认是localhost,如果client和broker不在同一台机器上,那么client访问localhost肯定就会报错了,所以需要配置broker的对外ip地址
package main
import (
"context"
"fmt"
pulsaradmin "github.com/streamnative/pulsar-admin-go" //注意一个是pulsar-admin
"time"
"github.com/apache/pulsar-client-go/pulsar" //一个是pulsar-client
)
func testAdmin() {
cfg := &pulsaradmin.Config{
WebServiceURL: "http://192.168.129.129:8080", #管理pulsar走的是http协议,默认8080端口
TLSAllowInsecureConnection: true,
}
admin, err := pulsaradmin.NewClient(cfg)
if err != nil {
fmt.Println("create adminClient failed")
return
}
tenants, err := admin.Tenants().List()
if err != nil {
fmt.Println("get tenants failed:", err.Error())
} else {
fmt.Println("tenants:", tenants)
}
}
func testProduce() {
client, err := pulsar.NewClient(pulsar.ClientOptions{
TLSAllowInsecureConnection: true,
URL: "pulsar://192.168.129.129:6650", #生产者消费者走的是pulsar协议,默认6650端口
OperationTimeout: 30 * time.Second,
ConnectionTimeout: 30 * time.Second,
})
client.Close()
if err != nil {
fmt.Print("error:", err.Error())
}
producer, err := createProduct(client)
if err != nil {
fmt.Println("createProduct failed:", err.Error())
return
}
_, err = producer.Send(context.Background(), &pulsar.ProducerMessage{
Payload: []byte("hello world"),
})
producer.Close()
if err != nil {
fmt.Println("send msg failed:", err.Error())
} else {
fmt.Print("send msg ok: hello world")
}
}
func createProduct(client pulsar.Client) (pulsar.Producer, error) {
producer, err := client.CreateProducer(pulsar.ProducerOptions{
Topic: "persistent://dcf/dcf/dcf",
})
if err != nil {
return nil, err
}
return producer, nil
}
func main() {
testAdmin()
testProduce()
}