官网原文标题《Concepts and Architecture--Messaging Concepts》
翻译时间:2018-10-04
官网原文地址:http://pulsar.apache.org/docs/en/concepts-messaging/
译者:本文介绍了Pulsar消息系统中的核心概念,是入门学习pulsar的开篇文档。他的核心概念和其他消息系统类似,尤其是kafka,但是又都有所不同。只有深刻理解好这些基础的概念,将来对pulsar的学习才能更为轻松!
Pular采用了发布订阅的设计模式,也称作pub-sub。producer发布消息到topic,consumer可以订阅这些topic,处理发布过来的消息,在处理完成后发送确认。
一旦订阅被创建,所有的消息都将被Pulsar保存,即使consumer已断开了连接。只有在consumer确认消息已经被成功处理后,保存下来的消息才会被丢弃。
消息是Pulsar的基础单元。producer发给topic的内容,consumer从topic消费的内容,就是消息。和邮政系统类比,消息就相当于信件。
组成 | 用途 |
value/data payload | 消息携带的数据,所有pulsar的消息携带原始bytes,但是消息数据也需要遵循数据shcema |
Key | 消息可以被Key打标签。这可以对topic压缩之类的事情起作用 |
properties | 可选的,用户定义属性 的key/value map |
Producer Name | 生产消息的producer的名称(producer被自动赋予默认名称,但你也可以请求自己指定) |
Sequence ID | 在topic中,每个Pulsar消息属于一个有序的序列。消息的sequence ID是他在序列中的次序 |
Publish time | 消息发布的时间戳 |
Event time | 可选的时间戳,应用可以附在消息上,代表某个事件发生的时间,例如,消息被处理时。如果没有 明确的设置,那么event time为0 |
若想了解Pulsar消息内容的更深入分解,请参考Pulasr的binary protocol文档
生产者是关联到topic的程序,它发布消息到Pulsar的broker上。
producer可以以同步或者异步的方式发布消息到broker。
模式 | 描述 |
同步发送 | 发送消息后,producer等待broker的确认。如果没有收到确认,producer会认为发送失败。 |
异步发送 | producer将会把消息放入blocking队列,然后马上返回。客户端类库将会在背后把消息发送给broker。 如果队列满了,根据传给producer的参数,producer可能阻塞或者直接返回失败。 |
为了节省带宽,在传输过程中,producer发布的消息可以被压缩。目前pulsar支持两种压缩类型:
如果批处理开启,producer将会累积一批消息,然后通过一次请求发送出去。批处理的大小取决于最大的消息数量及最大的发布延迟。
消费者通过订阅关联到主题,然后接收消息的程序。
消息可以通过同步或者异步的方式从broker接受。
模式 | 描述 |
同步接收 | 同步接收将会阻塞,直到消息可用 |
异步接收 | 异步接收立即返回future值--java中的CompletableFuture,一旦新消息可用,他即刻完成 |
消费者成功处理了消息,需要发送确认给broker,以让broker丢掉这条消息(否则它将存储着此消息)。
消息的确认可以一个接一个,也可以累积一起。累积确认时,消费者只需要确认最后一条他收到的消息。所有之前(包含此条)的消息,都不会被重新发给那个消费者。
累积消息确认不能用于shared 订阅模式,因为shared订阅为同一个订阅引入了多个消费者。
客户端类库提供了他们对于consumer的监听实现。举一个Java客户端的例子,它提供了MessageListener接口。在这个接口中,一旦接受到新的消息,received方法将被调用。
和其他的发布订阅系统一样,Pulsar中的topic是带有名称的通道,用来从producer到consumer传输消息。Topic的名称是符合良好结构的URL。
{persistent|non-persistent}://tenant/namespace/topic
Topic名称组成 | 描述 |
persistent/ non-persistent | 定义了topic类型,Pulsar支持两种不同topic:持久和非持久 (默认是持久类型,如果你没有指明类型,topic将会是持久类型)。持久topic的所有消息都会保存在硬盘上 (这意味着多块硬盘,除非是单机模式的broker),反之,非持久topic的数据不会存储到硬盘上 |
tenant | 实例中topic的租户。tenant是Pulsar多租户的基本要素。可以被跨集群的传播。 |
namespace | topic的管理单元,相关topic组的管理机制。大多数的topic配置在namespace层面生效。 每个tenant可以有多个namespace |
topic | 主题名称的最后组成部分,topic的名称很自由,没有什么特殊的含义。 |
不需要显式的创建topic
你并不需要显式的创建topic。如果客户端尝试从一个还不存在的topic写或者接受消息,pulsar将会按在topic名称提供的namnespace下自动创建topic。
命名空间是租户内部逻辑上的命名术语。一个租户可以通过admin API创建多个命名空间。例如,一个对接多个应用的租户,可以为每个应用创建不同的namespace。
订阅是命名过的配置规则,指导消息如何投递给消费者。Pulsar有三种订阅模式:exclusive,shared,failover,下图展示了这三种模式:
独占模式,只能有一个消费者绑定到订阅(subscription)上。如果多于一个消费者尝试以同样方式去订阅主题,消费者将会收到错误。
上面的图中,只有Consumer A可以消费。
Exclusive模式为默认订阅模式。
shared或者round robin模式中,多个消费者可以绑定到同一个订阅上。消息通过round robin轮询机制分发给不同的消费者,并且每个消息仅会被分发给一个消费者。当消费者断开连接,所有被发送给他,但没有被确认的消息将被重新安排,分发给其它存活的消费者。
第一幅图中,Consumer-B-1和Consumer-B-2都可以订阅主题,其实Consumer-C-1或者其它Consumer也可以订阅。
Shared模式的限制
使用shared模式时,需要重点注意以下两点:
- 消息的顺序无法保证
- 你不可以使用累积确认
Failover模式中,多个consumer可以绑定到同一个subscription。consumer将会按字典顺序排序,第一个consumer被初始化为唯一接受消息的消费者。这个consumer被称为master consumer。
当master consumer断开时,所有的消息(未被确认和后续进入的)将会被分发给队列中的下一个consumer。
第一个图中,Consumer-C-1是master consumer,当Consumer-C-1断开连接时,由于Consumer-C-2在队列中下一个位置,那么它将会开始接收消息。
当consumer订阅pulsar的主题,默认情况下,它订阅了一个指定的主题,例如:persistent://public/default/my-topic。从Pulsar的1.23.0-incubating的版本,Pulsar消费者可以同时订阅多个topic。你可以用以下两种方式定义topic的列表:
通过正则订阅多主题时,所有的主题必须在同一个命名空间(namespace)
当订阅多主题时,pulsar客户端会自动调用Pulsar的API来发现匹配表达式的所有topic,然后全部订阅。如果此时有暂不存在的topic,那么一旦这些topic被创建,conusmer会自动订阅。
不能保证顺序性
当消费者订阅多主题时,pulsar所提供对单一主题订阅的顺序保证,就hold不住了。如果你在使用pulsar的时候,遇到必须保证顺序的需求,我们强烈建议不要使用此特性
下面是多主题订阅在java中的例子:
import java.util.regex.Pattern;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.PulsarClient;
PulsarClient pulsarClient = // Instantiate Pulsar client object
// Subscribe to all topics in a namespace
Pattern allTopicsInNamespace = Pattern.compile("persistent://public/default/.*");
Consumer allTopicsConsumer = pulsarClient.subscribe(allTopicsInNamespace, "subscription-1");
// Subscribe to a subsets of topics in a namespace, based on regex
Pattern someTopicsInNamespace = Pattern.compile("persistent://public/default/foo.*");
Consumer someTopicsConsumer = pulsarClient.subscribe(someTopicsInNamespace, "subscription-1");
代码例子,请见:
通常一个topic仅被一个broker服务,这限制了topic最大吞吐量。分区topic是特殊的topic类型,他可以被多个broker处理,这让topic有更高的吞吐量。
其实在背后,分区的topic通过N个内部topic实现,N是分区的数量。当向分区的topic发送消息,每条消息被路由到其中一个broker。Pulsar自动处理跨broker的分区分布。
下图对此做了阐明(partition译为分区):
此处,Topic1有5个分区(P0到P4),分布在三个broker上。因为分区多于broker数量,其中有两个broker要处理两个分区。第三个broker则只处理一个。(再次强调,分区的分布是自动处理的)
这个topic的消息被广播给两个consumer。路由模式决定哪个broker处理哪个partition, 订阅模式决定哪条消息送到哪个consumer。
在大多数境况下,路由和订阅模式可以分开制定。通常来讲,吞吐能力的要求,决定了 分区/路由 的方式。订阅模式则应该由应用的需求来做决定。
分区topic和普通topic,对于订阅模式如何工作,没有任何不同。分区只是决定了从生产者生产消息到消费者处理及确认消息过程中发生的事情。
分区topic需要通过admin API指定创建。创建的时候可以指明分区的数量。
当发布消息到分区topic,你必须要指定路由模式。路由模式决定了每条消息被发布到的分区(其实是内部主题)。
下面是三种默认可用的路由模式
模式 | 描述 | 顺序保证 |
Key hash | 如果message指定了key,producer将会把key hash,然后把他分配给指定分区 | 同一个key下有序 |
single default partition | 如果没有key,每个生产者的消息将会被路由分发给专用的分区。初始时候随机选择 | 同一个生产者下有序 |
round robin分发 | 如果没有key,所有的消息通过round-robin方式被路由到不同的分区,以达到最大的生产能力 | 无 |
这些默认的模式之外,你还可以创建客制化的路由模式,如果你在使用Java client,可以通过实现MessageRouter接口来做到。
默认的,Pulsar保存所有没有确认的消息到多个BookKeeper的bookies中(存储节点)。持久topic的消息数据可以在broker重启或者订阅者出问题的情况下存活下来。
Pulsar也提供了非持久topic。非持久topic的消息不会被保存在硬盘上,只存活于内存中。当使用非持久topic分发时,杀掉Pulsar的broker或者关闭订阅者,意味着客户端可能会遭遇消息丢失。
非持久topic有如下格式的名称(注意名字中的non-persistent):
non-persistent://tenant/namespace/topic
使用非持久topic的更多信息,请参考 Non-persistent messaging cookbook.
非持久topic中,broker会立即发布消息给所有连接的订阅者,而不会在BookKeeper中存储。如果有一个订阅者断开连接,broker将无法重发这些瞬时消息。订阅者将永远也不能收到这些消息了。去掉持久化存储的步骤,在某些情况下,使得非持久topic的消息比持久topic稍微变快。但是同时,Pulsar的一些核心优势也丧失掉了。
非持久topic,消息数据仅存活在内存。如果broker挂掉或者其他情况不能从内存取出,你的消息数据就可能会丢失。只有真的觉得你的使用场景符合,并且你可以忍受时,才可去使用非持久topic。
默认非持久topic在broker上是开启的,你可以通过broker的配置关闭。你可以通过使用pulsar-admin-topics接口管理非持久topic。
非持久消息通常比持久消息会更快,因为broker无须持久化消息,当消息被分发给所有订阅者时,会立即发送ack给producer。非持久topic让producer有更低的发布延迟。
producer和consumer以连接持久topic同样的方式连接到非持久topic。重要的区别是topic的名称必须以non-persistent开头。三种订阅模式--exclusive,shared,failover对于非持久topic都是支持的。
下面是一个非持久topic的java consumer例子:
PulsarClient client = PulsarClient.create("pulsar://localhost:6650");
String npTopic = "non-persistent://public/default/my-topic";
String subscriptionName = "my-subscription-name";
Consumer consumer = client.subscribe(npTopic, subscriptionName);
这里还有一个非持久topic的java producer例子:
Producer producer = client.createProducer(npTopic);
Pulsar broker默认如下:
Pulsar有两个特性,这使得你可以覆盖上面的默认行为。
所有消息存留和过期在namespace层面管理。具体操作请查看Message retention and expiry
下图说明了这两种概念:
图中上面的是消息存留,存留规则会被用于某namespace下所有的topic,指明一些消息会被持久存储,即使已经被确认过。没有被留存规则覆盖的消息将会被删除。没有留存规则的话,所有被确认的消息都会被删除。
图中下面的是消息过期,有些消息还没有被确认,也被删除掉了。因为根据设置在namespace上的TTL,他们已经过期了。(例如,TTL为5分钟,过了十分钟消息还没被确认)
当消息被Pulsar持久化多于一次的时候,会发生数据重复。消息去重是Pulsar可选的特性,阻止不必要的消息重复,每条消息仅处理一次。
下图展示了开启和关闭消息去重的场景:
最上面的场景中,消息去重被关闭。producer发布消息1到一个topic,消息到达broker后,被持久化到BookKeeper。然后producer又发送了消息1(可能因为某些重试逻辑),然后消息被接收后又持久化在BookKeeper。消息重复发生了。
在第二个场景中,producer发送了消息1,消息被broker接收然后持久化,和第一个场景是一样的。当producer再次发送消息时,broker知道已经收到个消息1,所以不会再持久化消息1.
消息去重在命名空间层面处理。更多介绍请参考message deduplication cookbook.
消息去重的另外一种方法是确保每条消息仅被生产一次。这种方法通常被叫做生产者幂等。这种方式的缺点是,把消息去重的工作推给了应用去做。在Pulsar中,这是被broker处理的,这意味着你不需要修改你的客户端代码,你只需要做一些管理上的变化(参考Managing message deduplication)
消息去重,使Pulsar成为与流处理引擎(SPE)或者其他寻求“实际一次”处理语义的系统连接的完美消息系统。消息系统若不提供自动消息去重,则需要SPE或者其他系统保证去重。这意味着严格的消息顺序来自于让程序承担额外的去重工作。使用Pulsar,严格的顺序保证不会带来任何应用层面的消耗。
更深入的信息可以参考 this post及 Streamlio blog