这是一个关于修改 Kafka 源码时遇到的一个 bug,以及后续引起的一些思考。

问题描述

我们的目标是将 Kafka 中的 ProducerConsumer 客户端提供的 metrics,写入到一个 Json 对象中,然后再将这个 Json 对象通过 Http 请求发送到一个 service 服务上。

Kafka 原生提供的 metrics 信息非常多,这些 metrics 信息的大概内容如下图所示。

Client Metrics

在使用下面代码将 metrics 写入到 json 对象

1
2
3
for (Map.Entry<MetricName, ? extends Metric> metricEntry : metrics.entrySet()) {
jsonObject.put(metricEntry.getKey().toString(),metricEntry.getValue().value();
}

因为这部分是直接添加到 kafka-client 中的,将 kafka-client 打包后,再依赖这个 jar 包,client 在启动时就报了错误,但是并没有退出。

原因查找

刚开始以为json 对象的大小超出了 Http 协议中的长度限制,因为重写了 MetricName 类的 toString() 方法,如下

1
2
3
public String toFlitString() {
return "MetricName [name=" + name + ", group=" + group + ", tags=" + tags + "]";
}

将长度过长而且用途不大的 description 字段去掉,但是结果依然,并没有解决问题。

接下来为了想知道是往 json 对象添加 metrics 这部分是否执行,因此在 for 循环内添加了一行输出,最后发现输出十行后程序就不再输出,这一行的内容如下所示

1
Key: MetricName [name=request-latency-max, group=consumer-node-metrics, description=, tags={client-id=consumer-1, node-id=node-2}]	Value: -Infinity

这时候发现了 -Infinity 这个值,参考这篇文章 java中的NAN和INFINITY,才知道这是一个特殊的值,所以就怀疑是这个问题,因此,写了一个小测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.meituan.kafka.json;

import org.json.JSONObject;

/**
* Created by wangmeng on 15/10/2016.
*/
public class JsonTest {
public static void main(String[] args) {
JSONObject jsonObject=new JSONObject();
jsonObject.put("name","matt");
jsonObject.put("age",24);
jsonObject.put("double",10/0.0);
System.out.println(jsonObject.toString());
}
}

程序结果真是报错,报错内容如下:

1
2
3
4
5
6
7
8
9
10
Exception in thread "main" org.json.JSONException: JSON does not allow non-finite numbers.
at org.json.JSONObject.testValidity(JSONObject.java:1578)
at org.json.JSONObject.put(JSONObject.java:1291)
at org.json.JSONObject.put(JSONObject.java:1220)
at com.meituan.kafka.json.JsonTest.main(JsonTest.java:13)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

因此,查看了一下 org.json.JSONObject.testValidity 源码,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* Put a key/value pair in the JSONObject. If the value is null, then the
* key will be removed from the JSONObject if it is present.
*
* @param key
* A key string.
* @param value
* An object which is the value. It should be of one of these
* types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
* String, or the JSONObject.NULL object.
* @return this.
* @throws JSONException
* If the value is non-finite number or if the key is null.
*/
public JSONObject put(String key, Object value) throws JSONException {
if (key == null) {
throw new NullPointerException("Null key.");
}
if (value != null) {
testValidity(value);
this.map.put(key, value);
} else {
this.remove(key);
}
return this;
}

/**
* Throw an exception if the object is a NaN or infinite number.
*
* @param o
* The object to test.
* @throws JSONException
* If o is a non-finite number.
*/
public static void testValidity(Object o) throws JSONException {
if (o != null) {
if (o instanceof Double) {
if (((Double) o).isInfinite() || ((Double) o).isNaN()) {
throw new JSONException(
"JSON does not allow non-finite numbers.");
}
} else if (o instanceof Float) {
if (((Float) o).isInfinite() || ((Float) o).isNaN()) {
throw new JSONException(
"JSON does not allow non-finite numbers.");
}
}
}
}

查到这里,找到了具体的原因,对 Double 类型的对象,其值不能为 NANINFINITY,下面再看一下这个两个值在 java 中是如何定义的,对于 double 型的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final class Double extends Number implements Comparable<Double> {
/**
* A constant holding the positive infinity of type
* {@code double}. It is equal to the value returned by
* {@code Double.longBitsToDouble(0x7ff0000000000000L)}.
*/
public static final double POSITIVE_INFINITY = 1.0 / 0.0;

/**
* A constant holding the negative infinity of type
* {@code double}. It is equal to the value returned by
* {@code Double.longBitsToDouble(0xfff0000000000000L)}.
*/
public static final double NEGATIVE_INFINITY = -1.0 / 0.0;

/**
* A constant holding a Not-a-Number (NaN) value of type
* {@code double}. It is equivalent to the value returned by
* {@code Double.longBitsToDouble(0x7ff8000000000000L)}.
*/
public static final double NaN = 0.0d / 0.0;
.....
}

至此,对于这个问题,我们已经完整地解决了,并且也查找到了最终的原因。

写在最后

之前的文章,总是会写写在前面这一章,本文写了一章写在最后

这本是一个 bug,实际上并没必要写成一篇文章进行分析,而我这样做的原因是:告诉自己,或者是提醒自己,遇到问题,不但要想着解决问题,还要深入理解这个问题产生的原因。

以前自己在开发中,遇到过很多的坑,很多坑,找到解决办法之后就过去了,后来好久之后再遇到这个问题时,结果还需要花一些时间去查找,一个是自己的记性确实不是太好,另一个当时并没有对遇到的问题深入剖析,把问题的内部原因详细记录下来,希望这篇文章是一个起点,以后博客中,不但要有总结性的文章、思考性的文章,还要有一些详细剖析 bug 的文章。