jTDS 驱动导致 cpu 100%
保存线上环境相关信息
需要保存的线上环境信息
线程栈信息,连续 dump 3~5 次,使用命令:jstack -l pid,也可通过show-busy-java-threads 保存线程信息
内存空间使用信息, 使用命令:jmap -heap pid
内存中每个类的对象占用大小和实例数排序, 使用命令:jmap -histo:live pid
gc 信息:jstat -gcutil pid interval count 例如:jstat -gcutil 18756 1000 10
分析线上环境信息
分析线程日志
"qtp1434669784-7501" prio=10 tid=0x00007f9c8c00c800 nid=0x9c0 runnable [0x00007f9bd0c52000]
java.lang.Thread.State: RUNNABLE
at java.lang.String.regionMatches(String.java:1363)
at java.lang.String.equalsIgnoreCase(String.java:1119)
at net.sourceforge.jtds.jdbc.JtdsResultSet.findColumn(JtdsResultSet.java:976)
at net.sourceforge.jtds.jdbc.JtdsResultSet.getTimestamp(JtdsResultSet.java:1319)
at net.sf.log4jdbc.sql.jdbcapi.ResultSetSpy.getTimestamp(ResultSetSpy.java:343)
at com.alibaba.druid.filter.FilterChainImpl.resultSet_getTimestamp(FilterChainImpl.java:1258)
at com.alibaba.druid.filter.FilterAdapter.resultSet_getTimestamp(FilterAdapter.java:1829)
at com.alibaba.druid.filter.FilterChainImpl.resultSet_getTimestamp(FilterChainImpl.java:1254)
at com.alibaba.druid.filter.FilterAdapter.resultSet_getTimestamp(FilterAdapter.java:1829)
at com.alibaba.druid.filter.FilterChainImpl.resultSet_getTimestamp(FilterChainImpl.java:1254)
at com.alibaba.druid.proxy.jdbc.ResultSetProxyImpl.getTimestamp(ResultSetProxyImpl.java:740)
at com.alibaba.druid.pool.DruidPooledResultSet.getTimestamp(DruidPooledResultSet.java:365)
at org.hibernate.type.TimestampType.get(TimestampType.java:30)
at org.hibernate.type.NullableType.nullSafeGet(NullableType.java:163)
at org.hibernate.type.NullableType.nullSafeGet(NullableType.java:154)
at org.hibernate.type.AbstractType.hydrate(AbstractType.java:81)
at org.hibernate.persister.entity.AbstractEntityPersister.hydrate(AbstractEntityPersister.java:2101)
at org.hibernate.loader.Loader.loadFromResultSet(Loader.java:1380)
at org.hibernate.loader.Loader.instanceNotYetLoaded(Loader.java:1308)
at org.hibernate.loader.Loader.getRow(Loader.java:1206)
at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:580)
at org.hibernate.loader.Loader.doQuery(Loader.java:701)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:236)
at org.hibernate.loader.Loader.doList(Loader.java:2213)
at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2104)
at org.hibernate.loader.Loader.list(Loader.java:2099)
at org.hibernate.loader.criteria.CriteriaLoader.list(CriteriaLoader.java:94)
at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1569)
at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:283)
at org.springframework.orm.hibernate3.HibernateTemplate$36.doInHibernate(HibernateTemplate.java:1065)
at org.springframework.orm.hibernate3.HibernateTemplate.doExecute(HibernateTemplate.java:419)
at org.springframework.orm.hibernate3.HibernateTemplate.executeWithNativeSession(HibernateTemplate.java:374)
at org.springframework.orm.hibernate3.HibernateTemplate.findByCriteria(HibernateTemplate.java:1055)
at org.springframework.orm.hibernate3.HibernateTemplate.findByCriteria(HibernateTemplate.java:1048)
at
...
org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:599)
at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:534)
at java.lang.Thread.run(Thread.java:662)
复制代码
寻找可疑点
分析线程日志,大部分 RUNNABLE
线程都在执行 java.lang.String.regionMatches(String.java:1363)
方法
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true :
(anotherString != null) && (anotherString.count == count) &&
regionMatches(true, 0, anotherString, 0, count);
}
public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
char ta[] = value;
int to = offset + toffset;
char pa[] = other.value;
int po = other.offset + ooffset;
// Note: toffset, ooffset, or len might be near -1>>>1.
if ((ooffset < 0) || (toffset < 0) || (toffset > (long)count - len) ||
(ooffset > (long)other.count - len)) {
return false;
}
while (len-- > 0) {
char c1 = ta[to++];
char c2 = pa[po++];
if (c1 == c2) {
continue;
}
if (ignoreCase) {
// If characters don't match but case may be ignored,
// try converting both characters to uppercase.
// If the results match, then the comparison scan should
// continue.
char u1 = Character.toUpperCase(c1);
char u2 = Character.toUpperCase(c2);
if (u1 == u2) {
continue;
}
// Unfortunately, conversion to uppercase does not work properly
// for the Georgian alphabet, which has strange rules about case
// conversion. So we need to make one last check before
// exiting.
if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
continue;
}
}
return false;
}
return true;
}
复制代码
疑问:String::equalsIgnoreCase
是一个简单的方法,执行速度应该非常快,为什么大部分线程在执行这个方法?难道是 JDK 的String::equalsIgnoreCase
方法有性能问题?
前辈的经验告诉我们,JDK 封装的方法 99.99% 不会有问题。猜测可能是使用的姿势不对,或者上层调用方法频率太高。从 at java.lang.String.equalsIgnoreCase(String.java:1119)
往方法调用链上找。
当问题是 cpu 高时,遇到循环语句就需要格外注意了, 找到上层方法at net.sourceforge.jtds.jdbc.JtdsResultSet.findColumn(JtdsResultSet.java:976)
里面有 for
循环。
public class JtdsResultSet implements ResultSet {
/** Cache to optimize findColumn(String) lookups */
private HashMap columnMap;
public int findColumn(String columnName) throws SQLException {
checkOpen();
if (columnMap == null) {
columnMap = new HashMap(columnCount);
} else {
Object pos = columnMap.get(columnName);
if (pos != null) {
return ((Integer) pos).intValue();
}
}
// Rather than use toUpperCase()/toLowerCase(), which are costly,
// just do a sequential search. It's actually faster in most cases.
for (int i = 0; i < columnCount; i++) {
if (columns[i].name.equalsIgnoreCase(columnName)) {
columnMap.put(columnName, new Integer(i + 1));
return i + 1;
}
}
throw new SQLException(Messages.get("error.resultset.colname", columnName), "07009");
}
}
复制代码
调试代码发现这里 columnMap
原本是为了缓存下数据库列名对应的 index ,但是放入缓存 columnMap.put(columnName, new Integer(i + 1))
的时机不对,因为每次执行 sql 返回结果集都会生成 JtdsResultSet
对象。
由于项目里面涉及到大量高频使用的数据库表都是列比较多的大表,并且通常查所有列,很多表有 500 个字段左右。每次多执行 500 X 500/2 = 125,000 次 ``String::equalsIgnoreCase
方法。
验证怀疑点
修改怀疑的问题点
将 JtdsResultSet
类的代码改为第一次 调用 findColumn
时将所有列名和对应的 index 放入 columnMap
中,后面再调用 findColumn
方法时直接从 columnMap
中取,循环次数变为了 N(表的列数),从 Map 里面取时间复杂度约等于 O(1)。这里由于项目里面数据库字段名全是“大写字符”,就没有去兼容小写了。
public class JtdsResultSet implements ResultSet {
/** Cache to optimize findColumn(String) lookups */
private HashMap columnMap;
public int findColumn(String columnName) throws SQLException {
checkOpen();
if (columnMap == null) {
// init cache
columnMap = new HashMap(columnCount);
for (int i = 0; i < columnCount; i++) {
columnMap.put(columns[i].name, new Integer(i + 1));
}
}
Object pos = columnMap.get(columnName);
if (pos != null) {
return ((Integer) pos).intValue();
}
// Bottom-up plan
// Rather than use toUpperCase()/toLowerCase(), which are costly,
// just do a sequential search. It's actually faster in most cases.
for (int i = 0; i < columnCount; i++) {
if (columns[i].name.equalsIgnoreCase(columnName)) {
columnMap.put(columnName, new Integer(i + 1));
return i + 1;
}
}
throw new SQLException(Messages.get("error.resultset.colname", columnName), "07009");
}
}
复制代码
验证
测试环境测试后,代码更新到线上环境验证,经过几天的观察 cpu 比较稳定,问题解决。
总结
排查问题优先
上层应用代码与底层代码,优先排查是否上层应该代码有问题
自己写的代码与同事的代码,优先排查是否同事的代码导致有问题(自信,如果遇到大佬的代码还是先查自己的代码)
公司的代码与知名第三方 jar 包,优先查公司的代码
知名第三方 jar 包里面的代码与 jdk 源码,优先查第三方 jar 包
同事的代码 > 自己的代码 > 知名第三方 jar 包 > jdk 源码
如何查 cpu 高问题
链接
https://github.com/oldratlee/useful-scripts/blob/dev-2.x/bin/show-busy-java-threads
https://github.com/alibaba/arthas/issues/1202
https://github.com/milesibastos/jTDS/issues/17
评论