PageHelperページングプラグインにより、オンラインダボスレッドプールがいっぱいであることが調査されました。この記事の実際のIPは127.0.0.1に置き換えられました。
しばらく前に、アプリケーションは会社のフレームワークをアップグレードしました。監視システムは多くの警告を出しました。このシステムも特別で、サービスを提供するためのダボインターフェイスはわずかですが、リクエストの量は特に多くなります。
ログを確認して、ダボスレッドプールがいっぱいであることを確認します。次の例外がスローされます。
Thread pool is EXHAUSTED!
Thread Name: DubboServerHandler-127.0.0.1:20880,
Pool Size: 200 (active: 200, core: 200, max: 200, largest: 200),
Task: 58475360 (completed: 58475160),
Executor status:(isShutdown:false, isTerminated:false, isTerminating:false), in dubbo://127.0.0.1:20880!
アプリケーションがオンラインになった後、スレッドの数が増え続けることが監視システムを通じて観察されます。
同時に、ログには多数のorg.mybatis.spring.MyBatisSystemException例外があります。
そのため、異常が多く、システムの処理能力が低下していることが考えられます。多数のリクエストがダボスレッドプールをいっぱいにします。
例外情報によると、データベースが最終的に検出されたときにデータ検証が追加されなかったため、SQL例外が発生しました。変更されたコードが再びオンラインになった後、監視システムはすぐに警告しなくなります。
ただし、しばらくすると、ダボスレッドプールは再びいっぱいになり、新しいスレッドの数は台形の形で増加します。リクエストが届かないようにするには、トラフィックをただちにシャットダウンすることしかできません。
フレームワークでdubboのバージョンがアップグレードされているため、dubboスレッドプールにバグがあるのではないかと心配しています。例外情報によると、dubboのソースコードを確認したところ、AbortPolicyWithReport
このクラスが見つかりました。スレッドプールがいっぱいになると、スタック情報が出力され、ユーザーディレクトリに保存されます。
com.alibaba.dubbo.common.threadpool.support.AbortPolicyWithReport
private void dumpJStack() {
long now = System.currentTimeMillis();
//dump every 10 minutes
if (now - lastPrintTime < 10 * 60 * 1000) {
return;
}
if (!guard.tryAcquire()) {
return;
}
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
String dumpPath = url.getParameter(Constants.DUMP_DIRECTORY, System.getProperty("user.home"));
SimpleDateFormat sdf;
String OS = System.getProperty("os.name").toLowerCase();
// window system don't support ":" in file name
if(OS.contains("win")){
sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
}else {
sdf = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
}
String dateStr = sdf.format(new Date());
FileOutputStream jstackStream = null;
try {
jstackStream = new FileOutputStream(new File(dumpPath, "Dubbo_JStack.log" + "." + dateStr));
JVMUtil.jstack(jstackStream);
} catch (Throwable t) {
logger.error("dump jstack error", t);
} finally {
guard.release();
if (jstackStream != null) {
try {
jstackStream.flush();
jstackStream.close();
} catch (IOException e) {
}
}
}
lastPrintTime = System.currentTimeMillis();
}
});
}
次のようにDubbo_JStack.logファイルの内容を確認し、ほとんどのスレッドがPageAutoDialect.getUrl
このメソッドを実行することを確認します。
"DubboServerHandler-127.0.0.1:20880-thread-193" Id=366 RUNNABLE
at java.lang.Thread.setPriority0(Native Method)
at java.lang.Thread.setPriority(Thread.java:1092)
at java.lang.Thread.init(Thread.java:414)
at java.lang.Thread.init(Thread.java:349)
at java.lang.Thread.<init>(Thread.java:461)
at com.github.pagehelper.page.PageAutoDialect.getUrl(PageAutoDialect.java:159)
at com.github.pagehelper.page.PageAutoDialect.getDialect(PageAutoDialect.java:208)
at com.github.pagehelper.page.PageAutoDialect.initDelegateDialect(PageAutoDialect.java:81)
at com.github.pagehelper.PageHelper.skip(PageHelper.java:65)
at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:89)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
at com.sun.proxy.$Proxy106.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
at sun.reflect.GeneratedMethodAccessor173.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:434)
at com.sun.proxy.$Proxy81.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:239)
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:126)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:68)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:53)
コードを見ると、PageHelperページングプラグインのPageAutoDialect.getUrl
このコードが会社のフレームワークで変更されていることがわかります。変更されたコードは次のとおりです
。メソッド:getUrlメソッドが実行されるたびに、新しいスレッドが作成され、FutureTaskはスレッドが完了してデータを返すのを待ちます。ただし、この段落はSQLが実行され、スレッドが作成されるたびに呼び出されます。これは明らかに問題です。そして、スレッドが開始した後、FutureTask#get()を待機します。それは完全に時間とリソースを消費し、役に立たない操作のようです。
ストレステストの後、このコードにパフォーマンスの問題があることが確認されました。これが、図1のスレッドの数が台形に増加する理由です。
どうすればそのようなコードを書くことができますか???最後に、git送信レコードを確認したところ、これはSharding-JdbcとPageHelperの統合を解決してSQLを実行するためであることがわかりました。これは、PageHelperがデータベースダイアレクトを取得するときに実行ShardingConnection#close()
メソッドを引き起こし、Sharding-Jdbcの現在のコンテキスト情報をクリアするためです。 。特定の問題については、問題を参照してください。5.0.1バージョンPageHelperにより、ShardingJdbcの強制ページングが失敗します
解決策
検索の結果、getUrlメソッドを呼び出す場所はPageAutoDialect#getDialect
メソッドのみであり、このメソッドの戻り値の型はAbstractHelperDialect
であることがわかったため、AbstractHelperDialect
毎回getUrlを呼び出してデータベースを判断するのではなく、現在のデータベースダイアレクトオブジェクトをキャッシュすることを考えました。方言。
pagehelperでgetUrlメソッドを呼び出すソース
com.github.pagehelper.page.PageAutoDialect
コードを見てみましょう。
private AbstractHelperDialect getDialect(MappedStatement ms) {
//改为对dataSource做缓存
DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();
String url = getUrl(dataSource);
if (urlDialectMap.containsKey(url)) {
return urlDialectMap.get(url);
}
try {
lock.lock();
if (urlDialectMap.containsKey(url)) {
return urlDialectMap.get(url);
}
if (StringUtil.isEmpty(url)) {
throw new PageException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
}
String dialectStr = fromJdbcUrl(url);
if (dialectStr == null) {
throw new PageException("无法自动获取数据库类型,请通过 helperDialect 参数指定!");
}
AbstractHelperDialect dialect = initDialect(dialectStr, properties);
urlDialectMap.put(url, dialect);
return dialect;
} finally {
lock.unlock();
}
}
private String getUrl(DataSource dataSource) {
Connection conn = null;
try {
conn = dataSource.getConnection();
return conn.getMetaData().getURL();
} catch (SQLException e) {
throw new PageException(e);
} finally {
if (conn != null) {
try {
if (closeConn) {
conn.close();
}
} catch (SQLException e) {
//ignore
}
}
}
}
ソースコードを見ると、pagehelperが実際にそのようなキャッシュを実行していることがわかります。ただし、キャッシュキーは、毎回呼び出されるgetUrlの戻り値であり、キャッシュキーとして使用され(つまり、現在のデータベース接続アドレスがキーとして使用されます)、値はデータベースダイアレクトオブジェクトです。AbstractHelperDialect
。
ただし、このアプローチには2つの問題があります。
- 現在のデータベースのURLを取得するために、接続は毎回データベース接続プールから取得され、URLはconn.getMetaData()。getURL()を呼び出すことで取得できます。接続プールのリソースを無駄にします。
- データベースのURLは変更されていませんが、getUrlを繰り返し呼び出すたびにパフォーマンスが低下します。
解決策:キャッシュキーを変更します。実際、データベースダイアレクトは、アプリケーションの実行プロセス全体DataSource
を通じて変更されないため、インスタンスオブジェクトがキャッシュとして使用されている限り、getUrlメソッドへの繰り返しの呼び出しを回避できます。コードは次のように表示されます。
private AbstractHelperDialect getDialect(MappedStatement ms) {
//改为对dataSource做缓存
DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();
AbstractHelperDialect helperDialect = urlDialectMap.get(dataSource);
if (helperDialect != null) {
return helperDialect;
}
try {
lock.lock();
helperDialect = urlDialectMap.get(dataSource);
if (helperDialect != null) {
return helperDialect;
}
String url = getUrl(dataSource);
if (StringUtil.isEmpty(url)) {
throw new PageException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
}
String dialectStr = fromJdbcUrl(url);
if (dialectStr == null) {
throw new PageException("无法自动获取数据库类型,请通过 helperDialect 参数指定!");
}
AbstractHelperDialect dialect = initDialect(dialectStr, properties);
urlDialectMap.put(dataSource, dialect);
return dialect;
} finally {
lock.unlock();
}
}
概要:ただし、このソリューションには問題もありDataSource
ます。これはインターフェイスであるため、実装クラスがどのように書き換えられるか、hashCode
およびequals
メソッドがわからないためです。システムに複数のデータソースがある場合、値が置き換えられることがあります。もちろん、これはそうですhashCode
、そして、このequals
ような状況があるかもしれないことを知っている状況は、不合理な書き直しにつながるでしょう。