背景
在压测过程中,发现服务器的CPU使用率较高,造成整体的QPS和服务器的性能下降,需要进一步要分析原因,因此展开了相关分析。
如下图所示,CPU平均利用率在70%以上
分析
首先通过top等命令查询了具体的CPU耗费线程,发现很奇怪的是没有出现特别高的占用线程,但是会存在大量占用率不高的线程(占用值不多,但是数量比较多)
因此进一步展开使用jstack进行线程状态的统计分析,发现结果图下所示,存在大量的block线程:
打开看具体的堆栈后,结果如图所示,发现大量线程卡在了gson的这个反序列化方法上:
由于进一步查看了gson相关的文档和issue,发现已经有相关的issue如下:
至此问题应该比较清晰了,看来就是gson反序列化的时候,因为使用了dateFormater,且由于线程安全考虑进行了上锁,导致可能在大量反序列化日期数据的时候会造成线程阻塞,而由于jdk锁的升级机制,这里可能存在一些自旋锁在里面,加上大量的线程切换开销,导致了CPU升高。
具体也进一步查看了gson相关的源码,发现确实如此 (gson 2.8.x):
// These methods need to be synchronized since JDK DateFormat classes are not thread-safe
// See issue 162
@Override
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
synchronized (localFormat) {
String dateFormatAsString = enUsFormat.format(src);
return new JsonPrimitive(dateFormatAsString);
}
}
解决方案
那么清楚了原因后,问题就比较简单了。对于我们服务来说,之所以会产生这个现像主要是有个权限校验的切面类,里面的数据有几个日期字段,那么由于压测过程中线程较多因此导致冲突,所以会出现频繁加锁block的情况。
但是比较有意思的是,本来觉得这个可能是gson的一些bug,因此进一步查了相关文档和版本情况,发现google对这块有自己的看法如下:
看起来简单的升级一下可能并不能很好的解决问题,因此这里存在两个解决方案:
- 自定义反序列化的dateFormatter方法 (以不加锁的方式来处理,但是要注意考虑线程安全)
- 切换json序列化工具到其他lib
由于我们项目本身是引入了fastjson的,所以我们选择的方案2来处理,在修改后,相关的CPU使用和线程统计如下所示,发现问题解决(CPU使用率大概降低了20%左右):
至此,问题解决,但是考虑到部分小伙伴可能不想切换json序列化工具,且gson本身也是一个很优秀的json处理工具,也附上github上看到的一个自定义format的解决方案供参考(详见参考资料第3个链接):
public class DateTimeSerializer implements JsonSerializer<Date>, JsonDeserializer<Date> {
private String dateFormat;
public DateTimeSerializer(String dateFormat) {
this.dateFormat = dateFormat;
}
@Override
public JsonElement serialize(Date date, Type typeOfSrc, JsonSerializationContext context) {
// 这里是重点!!!
// uses FastDateFormat singleton
return new JsonPrimitive(DateFormatUtils.format(date, dateFormat));
}
@Override
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
try {
return DateUtils.parseDate(json.getAsString(), dateFormat);
} catch (ParseException e) {
return null;
}
}
}
----下面是使用初始化的地方----
public class MyClass {
private static Gson gson = new GsonBuilder()
.registerTypeAdapter(Date.class, new DateTimeSerializer("yyyy-MM-dd HH:mm:ss"))// any format is fine
.registerTypeAdapter(java.sql.Date.class, new DateTimeSerializer("yyyy-MM-dd HH:mm:ss"))
.create();
public static String foo_serialize(Object bar) {
return gson.toJson(bar);
}
public static Bar foo_deserialize(String bar) {
return gson.fromJson(bar,Bar.class);
}
}