Pulsar 是云原生时代的分布式消息队列平台,最早由 Yahoo 开发,现由 Apache 基金会维护。
一般来说,在日常开发中,我们可能较少需要直接接触到需要应用消息队列的场景,通常一般的数据库或 Redis 等内存缓存即可满足我们的要求。
但是在开发中,我们却经常使用发布-订阅模式,例如跨语言的 ReactiveX
,前端中的 document.addEventListener
,Node.js 中的 process.on
等。
消息队列即是在不同的服务,客户端间实现了这一模式,当系统本身不同服务间具有类似生产者和消费者的发布订阅关系时,消息队列即可以提供方便的功能。当然,在系统需要将耗时事件滞后处理时,消息队列也可以通过缓存事件,提供了提升系统的总体吞吐量,提升系统性能。
消息队列的工作模式不难理解,很多地方也有介绍,但怎么具体使用起来的实例还是较少,因此,本文就此用一个简单的例子具体讲解:
部署 Pulsar
和其他文章中一样,为了部署的便捷性和不同系统环境下的一致性,以及最终方便的迁移到云原生平台,我们尽量借助 Docker 实现 Pulsar 的部署。
具体的,在 Docker Hub 上,Pulsar 官方账号 apachepulsar
提供了很多官方镜像,我们通过编写 docker-compose
文件实现它的本地部署。
version: "3.1"
services:
pulsar:
# 使用 pulsar 官方镜像
image: apachepulsar/pulsar:latest
# 将 pulsar 的数据和配置映射出来
volumes:
- ./pulsar/data:/pulsar/data
- ./pulsar/conf:/pulsar/conf
# 导出两个外部需要的端口
ports:
# 二进制 API 端口
- 6650:6650
# HTTP API 端口
- 8080:8080
# 以 standalone 模式运行 pulsar
command: bin/pulsar standalone
# 限制使用内存,加速启动
environment:
PULSAR_MEM: " -Xms512m -Xmx512m -XX:MaxDirectMemorySize=1g"
pulsar-dashboard:
image: apachepulsar/pulsar-dashboard
depends_on:
- pulsar
environment:
# 指定 pulsar HTTP 端口的路径
#(pulsar 会被 docker-compose 的 DNS 服务解析为 docker 内部的虚拟 IP)
SERVICE_URL: http://pulsar:8080/
ports:
# Web Dashboard 端口导出到 80
- 80:80
Pulsar Node.js Client 安装
接下来,我们用 Node.js 来与 Pulsar 建立连接,一般来说,SDK 的安装只需要 npm install xxx
即可,可惜 Pulsar 的 Node.js Client 并不是很完善,而且依赖于 C++ Client,故这一步还略为复杂。
我们可以通过这里提供的方式以及要求安装 apache-pulsar-client
和 apache-pulsar-client-dev
两个包:
wget https://downloads.apache.org/pulsar/pulsar-2.7.2/DEB/apache-pulsar-client-dev.deb
wget https://downloads.apache.org/pulsar/pulsar-2.7.2/DEB/apache-pulsar-client.deb
apt install ./apache-pulsar-client.deb ./apache-pulsar-client-dev.deb
# 初始化 NPM 项目
npm init
接下来,我们安装 Pulsar Node.js 客户端:
# 安装 Pulsar 客户端
npm install pulsar-client@1.2.0
Windows 下开发
需要注意的是,在 Windows 下因为无法下载到 libpulsar 的二进制包,故我们只能采用在虚拟机中安装的方式,这里,我们利用容器创建一个开发环境:
FROM node:lts
WORKDIR /client
COPY package.json ./
RUN wget https://downloads.apache.org/pulsar/pulsar-2.7.2/DEB/apache-pulsar-client-dev.deb
RUN wget https://downloads.apache.org/pulsar/pulsar-2.7.2/DEB/apache-pulsar-client.deb
RUN ls
RUN apt install ./apache-pulsar-client.deb ./apache-pulsar-client-dev.deb
RUN npm install pulsar-client --save
在 compose 中加入构建并运行 pulsar-client
部分:
version: "3.1"
services:
pulsar:
# ...
pulsar-dashboard:
# ...
# `tail -f` 可以防止镜像运行后退出
pulsar-client:
build: ./client
command: tail -f
在构建成功后,我们可以借助 vscode 的 Docker 插件接入容器继续进行开发:
Node.js 聊天室开发
首先,和数据库类似的,我们需要创建一个 Pulsar 的客户端实例:
// 引入 Client
const Pulsar = require('pulsar-client');
(async () => {
console.log('[Enter Chatroom]')
// 创建 Pulsar Client 的实例
const client = new Pulsar.Client({
// 因为 pulsar 和 Client 在一个 docker-compose 服务中,用 pulsar 作为主机名即可查到它在服务中的 IP
serviceUrl: 'pulsar://pulsar:6650',
log: () => {},
operationTimeoutSeconds: 30,
});
})();
而消息队列分为生产者 Producer
和消费者 Consumer
两种角色,在这里,我们发送消息的人对应为生产者,而所有人都可以接受到他人的消息,即为消费者。而生产者和消费者都是对于一个主题 Topic
进行操作,生产者将消息放入一个主题中,而消费者使用其中的消息,不同主题直接不会互相影响。
首先,我们在全局变量中定义固定的主题名,并从环境变量中获取用户名:
const topic = 'persistent://public/default/chat-room';
const username = process.env.USERNAME;
接下来,我们监听用户的输入,并在用户输入一行消息后,将消息发送到消息队列中:
async function createMessageSender(client) {
// 创建生产者
const producer = await client.createProducer({
topic,
sendTimeoutMs: 1000,
batchingEnabled: true,
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// 当输入一行时,发送消息
rl.on('line', async (input) => {
const data = { username, message: input };
// 序列化消息并发送
const msg = JSON.stringify(data);
await producer.send({
data: Buffer.from(msg),
});
console.log(`[SEND] ${data.message}`);
});
}
而当消费者收到消息时,我们则将消息打印出来:
async function createMessageReceiver(client) {
// 当收到消息时,输出消息发送者和内容
await client.subscribe({
topic,
subscription: username,
subscriptionType: 'Shared',
ackTimeoutMs: 10000,
listener: (msg, msgConsumer) => {
// 反序列化消息并输出
const data = JSON.parse(msg.getData().toString());
console.log(`[FROM ${data.username}] ${data.message}`);
msgConsumer.acknowledge(msg);
},
});
}
最后,在创建 Pulsar Client 后调用这两个函数,我们的程序就完成了,完整的程序如下:
// 引入 Client
const Pulsar = require('pulsar-client');
const readline = require('readline');
const topic = 'persistent://public/default/chat-room';
const username = process.env.USERNAME;
async function createMessageSender(client) {
// 创建生产者
const producer = await client.createProducer({
topic,
sendTimeoutMs: 1000,
batchingEnabled: true,
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// 当输入一行时,发送消息
rl.on('line', async (input) => {
const data = { username, message: input };
// 序列化消息并发送
const msg = JSON.stringify(data);
await producer.send({
data: Buffer.from(msg),
});
console.log(`[SEND] ${data.message}`);
});
}
async function createMessageReceiver(client) {
// 当收到消息时,输出消息发送者和内容
await client.subscribe({
topic,
subscription: username,
subscriptionType: 'Shared',
ackTimeoutMs: 10000,
listener: (msg, msgConsumer) => {
// 反序列化消息并输出
const data = JSON.parse(msg.getData().toString());
console.log(`[FROM ${data.username}] ${data.message}`);
msgConsumer.acknowledge(msg);
},
});
}
(async () => {
console.log('[Enter Chatroom]')
const client = new Pulsar.Client({
serviceUrl: 'pulsar://pulsar:6650',
log: () => {},
operationTimeoutSeconds: 30,
});
createMessageSender(client);
createMessageReceiver(client);
process.on('SIGINT', async () => {
console.log('[Exit Chatroom]')
await client.close();
process.exit();
})
})();
运行并测试
我们可以在三个命令行窗口中依次分别启动三个不同用户名的程序,具体如下:
USERNAME=user1 node index.js
USERNAME=user2 node index.js
USERNAME=user3 node index.js
然后,在三个窗口中输入消息并回车,他们就可以看到相互发送的消息了:
就此,我们借助消息队列的能力实现了一个简单的聊天室,借助该程序,只要能够连接到 Pulsar 消息队列上,即可和其他客户端交换信息。
总结
借这个简单的例子,我们大概看到了 Pulsar 消息队列的使用方式和工作的效果,相较 Kafka,Pulsar 的部署还是非常方便的。但是,Pulsar 对不同操作系统和语言环境的支持也还不是特别完善。
另外,Pulsar 的强大之处还在于它分布式部署的能力和实现消息异地同步的功能,在这里我们只是使用了基本的功能,还没有对它的这些特性进行实践与应用。