记一次kafka消费异常

310 阅读4分钟

When,什么时候发现的

2021.08.13,和同事回归开发票的功能,同时发现一笔12块钱的充值流水不能开票了,找到帮忙排查。我这边看到发票服务存储的可开票金额为6块,所以发起开票时是想开12快钱的票,实际可开票金额为6块,两者不一致,会自动拒绝

What,发现了什么问题

因此,我让同事再操作一次,我盯着日志,看下具体是什么问题。公司的日志不方便放出,直接说结论。就是充值侧发出的kafka消息,充值金额是12块,到了发票侧消费时就变成了6块。莫名减少了一半。尝试了其他金额也是同样减少一半。

How,怎么出现的这个问题

首先说下,刚刚检查金额的日志,是打在producer发送消息,和consumer消费消息的出入口,那么问题只能出在传输过程中。而且这条kafka消息体是protobuf格式的,所以科普一下protobuf-kafka消息的发送和消费的简略过程。

生产消费流程.png 因为我在排查生产和消费的出入口都打印了日志,说明代码逻辑没问题。那么按照流程图所示,问题就出现在序列化和反序列化过程中。那么protobuf具体是怎么序列化,或者编码的呢。 image.png protobuf编码之后的结果都是如上图接口:tag表示是哪个类型的字段和下标值,length是字段值的长度(有的类型有,有的类型没有),value就是字段的值。 image.png 第一个16进制的编码由一下部分构成:

1、fieldNumber=1 第1个字段;

2、Wire type 哪种编码方式

3、Length(如果有)value 的长度

4、value的编码 image.png 现在揭示真正出问题的地方,proto定义文件的数据类型改变导致编码方式改变。

原proto定义

uint64 can_invoice_amount = 8; //可开票金额

新proto定义

sint64 can_invoice_amount = 8; //可开票金额

Why,为什么是这种现象

因为新消费者使用了新的数据类型,旧生产者没有更新proto定义,使用的旧的数据类型,两边的编码方式不一致导致这个问题出现。那么为什么会出现这种现象呢,接下来解析一下生产者和消费者在这种情况下的编码过程。

uint表示无符号数,认为都是正整数,编码方式为Varints编码。

sint表示有符号数,认为可能是正整数也可能是负整数,编码时先用ZigZag编码,再用Varints编码。解码时先用Varints解码,再用ZigZag解码。

生产者:tag、wire tpye等正常编码,但是当遇到值是数字时,就生产者是uint64。假设value是300,uint64方式编码,编码过程如下:

value = 300,

1、转成二进制:0000 0000 0000 0000 0000 0001 0010 1100

2、从后往前,每7位取一次,取到都为0之前,这样可以节约存储空间。本例会取2段:010 1100、000 0001 0

3、每7位前面加一个msb,最高位标识符,表示本段取完,还要不要继续取值:1010 1100 0000 0001 0

第1个1表示,后面还要接着取,第2个0表示,这段取完后面不取了。

生产者的value编码完成。

消费者:tag、wire type等正常编码,但是当遇到值是数字时,就生产者是sint64。编码过程如下

1、消费者拿到 1010 1100 0000 0010,开始用Varints解码,上述逻辑反向操作,结果是:010 1100 000 0001 0 = 300

2、拿到300这个值,再使用ZigZag解码。

 public static long decodeZigZag64(final long n) {

    return (n >>> 1) ^ -(n & 1);

  }

先对300整体右移1位,等于除以2,再乘300对1取余的结果乘-1。所以因为300是偶数,对1取余的结果是0,结果为正数,300/2 = 150,所以结果是150。到此谜题终于解开。

以上是个人对protobuf编码问题的粗浅理解,有理解错误或者表达不清楚的地方,还请各位大佬指正,谢谢。

附赠Varints编码过程截图: image.png  

 

参考文档:

blog.csdn.net/daaikuaichu…

www.lixueduan.com/post/protob…

www.cnblogs.com/en-heng/p/5…

wikimore.github.io/2016/09/22/…

developers.google.com/protocol-bu…