存在的意义
业务直接去接kafka或者其他的mq,需要自己处理rebalance场景下的各种极端case,以及一些安全,网络分区问题。
SDK直连kafka的话,会面临消费者数量不能超过partition数量的限制。如果有proxy做代理,可以解决这个问题。而且可以降低rebalance的概率。
重复消费问题:重复消费和rebalance其实没有关系,和重连有关系,只要重连都有可能重复消费。只要proxy不发版重启,理论上就不会有重复消费。
消息投递SLA
At Least Once
At Least Once是绝大多数消息服务系统的SLA(服务水平承诺),即指所有投递成功的消息将会被至少成功消费一次。对于MSM来说,普通消息/顺序消息/延迟消息类型均符合这一SLA,其含义有三点: 1)生产者需要成功投递消息。2)消费者需要使用ack机制来确保消息消费的成功。3)在某些情况下,消费者收到的消息是可能产生重复的,比如,proxy实例重启,消费者client非优雅退出,后端provider重启等等。消费者需要有一定的容错机制来避免少量重复消息的影响。
At Most Once
At Most Once是另一种SLA,指所有成功投递的消息将会被至多消费一次,采用At Most Once的系统将不会有重复的消费,但是可能会丢失消息,其吞吐量相比At Least Once类型要高一个数量级。目前内存型消息队列均为At Most Once,如nats,nsq等。
Exact Once
Exact Once是最严格的一种SLA,即所有被成功投递的消息都将被恰好消费一次,采用Exact Once的系统既不会有重复消费也不会有消息丢失,只有投递成功或失败,其吞吐量显著低于前两种SLA。一般只有事务消息类型会采用exact once的SLA。
常见消息类型
* **普通消息**。遵循队列先进先出,同一个consumer实例消费消息有序,不同的consumer实例之间消费消息无序,除非特殊场景,正常的生产和消费都应该使用普通消息。
* **顺序消息**。topic的某个subscription同一时间只允许一个consumer实例消费,消息严格有序。
* **延迟消息**。生产的消息并不第一时间可见,而是在规定的时间之后可以被consumer消费,但是并不保证一定是在规定的时间被消费。
* **重投队列**。消费失败的消息,用户可以配置进入重投队列进行一定次数的重投,重投超过次数之后将进入死信队列。
* **死信队列**。用户可以在申请topic时同时申请一个死信队列,任何消费失败(Nack)的消息都会进入死信队列,用户可以离线消费死信队列的消息。
* **事务消息**。生产者投递消息之后,执行事务处理逻辑,根据处理的结果执行Commit或者Rollback,只有执行成功的消息才能被消费者消费到(两阶段提交),参考流程: [https://rocketmq.apache.org/docs/4.x/producer/06message5/](https://rocketmq.apache.org/docs/4.x/producer/06message5/)。
特性
容灾和可用性
无状态proxy。目前proxy服务是无状态,理论上可以无限水平扩容,其任何一个节点都是可替换的。
存储provider。provider主要为云消息队列产品,依赖云产品的可用性。国内普通消息类型使用的是aliyun kafka,延迟消息类型使用的是rocketmq,海外延迟消息类型使用的是sqs。
topic迁移。支持topic从一个provider实例(zone)无缝迁移到另一个provider实例(zone),支持topic在发生zone级别的容灾时在有损条件下直接迁移,从而实现异地多活。
连接管理。支持不同业务的proxy集群划分,做到更严格的租户隔离和资源独享。
使用维度:
跨区生产消费,国内生产海外消费
自定义监控,消息轨迹,限流
安全性,租户管理/隔离
批量处理,顺序提交
会在sdk和proxy都维护一个滑动窗口,随着窗口移动提交。如果有一个一直没提交,则窗口将停止移动。
proxy和sdk的关系
sdk可以理解为业务的消费者,sdk与proxy通过grpc的stream进行连接,proxy为每个stream维护一个滑动窗口。
proxy的数量根据整个公司的业务情况决定,大部分场景下是不需要改变的。申请的kafka的topic,比如有6个partition,但是有100个proxy,会随机连到6个proxy上。然后有一个地方记录topic和对应proxy实例的列表,sdk连接topic的时候,就会找到实例列表,然后建立stream。
如果需要提高消费速度,一个业务进程,可以开多消费者,建立多个stream,每个stream有自己的滑动窗口,从而提高并发量。
注意:并发量越高,重连的时候,可能重复消费的消息就越多。
消息中间件代理还能做哪些事
消息中间件代理可以在业务层和消息中间件(如 Kafka、RabbitMQ、ActiveMQ 等)之间提供附加功能和服务。以下是一些消息中间件代理可以做的事情:
1. **消息转换**:代理可以将消息从一种格式转换为另一种格式,以满足业务层的需求。例如,代理可以将 JSON 消息转换为 Protocol Buffers 格式或 Avro 格式。
2. **消息过滤**:代理可以根据业务层的需求过滤消息。例如,代理可以只将包含某个关键词的消息传递给业务层,以减小业务层处理无关消息的负担。
3. **消息路由**:代理可以根据消息内容或其他规则将消息路由到不同的消费者或业务组件。这可以帮助您实现更灵活的消息处理架构,例如将高优先级消息发送到高性能的处理组件。
4. **消息缓存**:代理可以缓存消息,以便在消费者出现问题时重新发送消息。这有助于提高系统的可靠性,确保消息不会因为临时故障而丢失。
5. **消息重试和死信队列**:代理可以实现消息重试策略,当业务层处理失败时,代理可以在一段时间后重新发送消息。同时,代理还可以实现死信队列功能,将连续失败的消息移入死信队列,以便后续处理和分析。
6. **负载均衡**:代理可以实现负载均衡策略,根据消费者的处理能力动态调整消息的分发。这有助于确保系统资源的充分利用,提高处理性能。
7. **监控和度量**:代理可以收集关于消息处理的度量和统计信息,例如消息处理速度、失败率、延迟等。这些信息可以帮助您监控系统的性能和健康状况,及时发现和解决问题。
8. **安全和认证**:代理可以实现消息的加密和解密,以保护消息传输过程中的数据安全。此外,代理还可以实现消费者和生产者的身份验证和授权,以确保只有合法的用户可以访问消息。
通过实现这些功能,消息中间件代理可以帮助您构建更强大、灵活且可靠的消息处理系统。代理可以让业务层更关注业务逻辑,而无需处理消息中间件的底层细节。同时,代理还可以提供一组丰富的工具和服务,助力您更好地监控、调试和优化您的系统。
生产环境消息队列使用注意事项
生产者优雅关机:很多生产者的客户端都是异步的,本地有缓存,生产接口返回成功,并不代表消息已经写到远端了【好的sdk,多线程提交会等写到远端以后再返回】。关机前,先停止生产,在flush一下,然后关机。
消费者优雅关机:先停止消费,然后commit已经处理的,之后关机。尽量减少重复消费的消息数量
滑动窗口实现
消息中间件proxy在面多对个客户端连接的时候如何做滑动窗口
在消息中间件代理面对多个客户端连接的情况下,可以使用滑动窗口算法来实现流量控制和负载均衡。滑动窗口算法可以确保代理不会因为单个客户端的延迟或拥塞而影响其他客户端。以下是使用滑动窗口算法的实现步骤:
1. **为每个客户端分配窗口**:给每个连接的客户端分配一个固定大小的窗口。这个窗口表示代理可以在任何时候向该客户端发送的最大消息数量。例如,如果窗口大小为 5,代理最多可以在等待客户端响应之前发送 5 条消息。
2. **记录已发送消息**:对于每个客户端,代理需要记录已发送但尚未收到确认的消息。这可以通过一个队列或列表来实现。同时,代理还需要记录发送的消息数量。
3. **发送消息**:当向客户端发送消息时,代理首先检查客户端的滑动窗口是否已满(即已发送但未确认的消息数量等于窗口大小)。如果窗口未满,则代理可以发送新消息,并将新消息添加到已发送消息记录中。
4. **处理确认**:当代理收到客户端的消息确认时(例如,客户端成功处理了一条消息),代理需要将对应的消息从已发送消息记录中移除,并将滑动窗口向前移动(即增加可发送的消息数量)。
5. **负载均衡**:代理可以根据每个客户端的滑动窗口使用情况来实现负载均衡。例如,代理可以优先向拥有更多可用窗口空间的客户端发送消息,或者根据窗口使用率来分配消息。这可以确保代理根据客户端的处理能力来分发消息,从而实现更有效的负载均衡。
6. **超时和重试**:为了处理丢失的确认或失败的消息传递,代理可以引入超时和重试机制。当已发送的消息在规定时间内未收到确认时,代理可以将消息重发,并更新滑动窗口。同时,代理还可以实现一个最大重试次数限制,以避免无限制的重发导致资源耗尽。
通过使用滑动窗口算法,您可以在消息中间件代理中实现更有效的流量控制和负载均衡。这可以确保代理在面对多个客户端连接时能够平滑地分发消息,降低拥塞和延迟,从而提高整个系统的性能和可靠性。
kafka提交的细节
kafka-go在开启手动commit的情况下,如果老消息如果在没提交,去提交新消息会有什么表现?
在 Kafka-go 中,手动提交偏移量时,提交的是当前消费者已经处理完毕的最新偏移量。当老消息尚未提交时,手动提交新消息实际上会导致将消费者的偏移量更新为新消息的偏移量。这意味着在 Kafka 中,老消息以及在它之前的所有消息都被认为已经处理并提交了。
处理这种情况时,需要注意以下几点:
1. 数据一致性:如果手动提交新消息导致了老消息的丢失,您需要确保这不会对您的应用程序产生负面影响。在某些情况下,您可能需要修改消费者的逻辑以确保在提交新消息之前处理并提交所有旧消息。
2. 消息处理顺序:Kafka 中的消息是按分区内的顺序进行处理的,但是当分区之间并行处理时,消息的全局顺序可能会受到影响。如果您的应用程序对顺序有要求,那么在处理新消息之前,请确保已经处理并提交所有旧消息。
3. 消息重复处理:由于 Kafka-go 在开启手动提交模式下可能会在错误情况下多次处理相同的消息,所以应在消费者中处理这种情况。例如,使用幂等操作或在数据库中维护一个已处理消息的列表。
在使用 Kafka-go 手动提交模式时,应确保在提交新消息之前正确处理并提交所有旧消息。这可以通过检查消费者的错误日志、调整提交间隔等方式来实现。同时,确保有足够的容错处理机制以应对可能的消息丢失或重复处理情况。
proxy实现细节
stream级别的滑动窗口
生产stream不需要窗口,生产stream的窗口应该sdk做。proxy收到消息以后,给sdk会ack,sdk根据ack去移动窗口控制生产速率。【实际生产中,建议一条一条生产,批量生产容易出错,且不知道错在哪条,可能会多会少】
消费stream需要窗口,sdk消费的消息,先写window,再写和sdk的stream,window满了则阻塞,通过sdk的ack去移动窗口,控制消费速率。【防止单个stream把某个partiton的commit-window占满,导致频繁reset】【partion的commit-window大小=partion支持的stream个数*每个stream的window大小,实际可以设置的比这个小一些,提高reset的频率,设置的越小,会重复消费的消息越少,同样,在不稳定情况下出发reset的概率也会增加】
commit细节
如果offset为1的消息被push到客户端,但是还没ack,这时候sdk和proxy的stream断了,下次stream重连的时候,还能拿到这条消息吗
解决办法就是proxy维护partition级别的commit-window,如果window满了【这个值的设置需要根据业务情况调整】,则reset一下offset
sdk实现细节
sdk的消费stream不需要window,由proxy来进行限制,只需要无脑向proxy拉,处理完以后进行commit即可。
sdk的生产stream可以不用window,逐条进行投递,返回成功则投递成功。
如果生产stream非要使用window,可以进场并发生产,批量投递,proxy进行ack的时候,需要明确指出自己ack了哪几条消息,然后sdk把生产这几条消息的阻塞的goroutine给解开。【要保证投递线程返回的时候,该线程投递的数据一定都成功了,如果投递的线程返回err,则一定都是失败的】【建议逐条投递,没必要搞批量,真要搞批量,也在业务级别搞,即一个kafka的msg里面存储多个业务包,而不是只存储一个】
总结
生产的最佳实践就是逐条投递,等返回,需要批量的话业务层自己做。
消费的最佳实践是批量并发消费,proxy维护commit-window和stream-window,根据不同的业务,调整reset-window的大小,选择合适的reset策略。在commit-window卡住的时候,即时去reset。
其中只要commit-window不要stream-window也行,但是会导致不同的stream之间处理不均衡,所以最好还是都要一下。
很糙的中间件实现
一个stream错误,直接关闭reader进行rebalance,commit出错也是一样。
rebalance时,中间件正确的操作
rebalance时read方法的表现
rebalance时commit方法的表现
正确的处理姿势
rebalance时,读阻塞,不需要额外处理。commit失败,进行一定次数的重试,还失败的话就关闭stream,但是不需要关闭reader,因为可能你commit的partiton被分给别人了。
commit失败的原因
分区所有权变更导致commit失败处理办法
commit失败以后,可以查询一下当前reader的分区所有权,如果要提交消息不属于当前reader了,则关闭stream,且消息不需要重新投递,因为会被其他reader再次消费到。
renbalance的stalelize
kafka的rebalance是有一个stablize阶段的,就是一段时间之内连上来的客户端都会参与到rebalance,而且这个时候是不会报错的,stablize阶段过了之后才会真正rebalance成功,否则的话任何网络抖动都会导致频繁rebalance,kafka server就没有稳定性了。
很多时候rebalance的表现就是没有响应,所以当出现超时的时候,客户端很难确定是rebalance还是真的网络不通了。
generation
rebalance之后的consumer是带generation的,非本generation的commit会全部失败。所以理论上说consumer必须全部重新取消息重新commit。但是kafka-go帮我们屏蔽了这个点,只要partion没被分走,之前消费的消息也可以commit。