案例
在网上看到有人分享一个案例:
之前做车辆实时定位(汽车每10s上传一次报文)显示的时候,发现地图显示车辆会突然退回去,开始排查怀疑是后端处理的逻辑问题导致的,但是后台保证了一台车只被一个线程处理,理论上不会出现这种情况;于是猜测是不是程序接收到消息的时候时间序就已经乱了,查阅了kafka相关资料,发现kafka同一个topic是无法保证数据的顺序性的,但是同一个partition中的数据是有顺序的;根据这个查看了接入端的代码(也就是kafka的生产者),发现是按照kafka的默认分区策略(topic有10个分区,3个副本)发送的;于是将此处发送策略改为按照key(车辆VIN码)进行分区,后面车辆的定位显示就正常了。
虽然在我的工作中对于数据是否有序并不关注,但是在实际的生产环境中还是有很多场景是需要保证有序性的。
kafka有序性的方案
生产者
首先,kafka的分区策略:
-
默认分区器
- 指定partition,将指定的值作为分区
- 没有指明partition值但有key的情况下,将key的hash值与topic的partition数进行取余得到partition值。例:key1的hash值为5,5%2=1,那么key1对应的value写入1号分区
- 既没有partition值又没有key值的情况下,kafka采用Sticky Partition(黏性分区),会随机选择一个分区,并尽可能一致使用该分区,待该分区的batch已满或者已完成,Kafka在随机一个分区进行使用(和上一次得分区不同,如果相同,则会继续随机,直到找到不同得分区为止)。
-
指定分区器
所以如果要保证数据的有序性,首先应该根据key值进行分区,在一个分区中的数据是有序的。
MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION参数
该参数指定了生产者在收到服务端响应之前可以发送多少个batch消息,默认值为5。batch是当有多个消息需要被发送到同一个分区时,生产者会把它们放到同一个批次中,该参数指定了一个批次可以使用的内存大小。一般要求MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION小于等于 5 的主要原因是:Server 端的 ProducerStateManager 实例会缓存每个 PID 在每个 Topic-Partition 上发送的最近 5 个batch 数据(这个 5 是写死的,至于为什么是 5,可能跟经验有关,当不设置幂等性时,当这个设置为 5 时,性能相对来说较高,社区是有一个相关测试文档),如果超过 5,ProducerStateManager 就会将最旧的 batch 数据清除。
因为Kafka的重试机制有可能会导致消息乱序,所以我们一般为了保证消息有序会把MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION设置为1,即使发生了重试也会保证分区写入有序. 比如我们常见的订单系统和会员积分系统就是非常鲜明的场景,订单是要创建过后才能取消的,而对应的会员积分是要先增后减的,如果这个顺序不能保证,系统就会出现问题。但设置为1时,传输效率非常差,但是可以解决乱序的问题(当然这里有序只是针对单 client 情况,多 client 并发写是无法做到的)。
在kafka2.0+版本上,只要开启幂等性,不用设置MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION为1也能保证发送数据的顺序性。可以设置enable.idempotence=true,开启生产者的幂等生产,可以解决顺序性问题,并且允许max.in.flight.requests.per.connection设置大于1。当enable.idempotence设置成true后,Producer自动升级成幂等性Producer。Kafka会自动去重。Broker会多保存一些字段。当Producer发送了相同字段值的消息后,Broker能够自动知晓这些消息已经重复了。