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

问题描述

我们的目标是将 Kafka 中的 Producer 和 Consumer 客户端提供的 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 类型的对象,其值不能为 NAN 和 INFINITY,下面再看一下这个两个值在 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 的文章。