DUICUO

Arthasの簡潔な入門チュートリアル

Arthasは、強力なオープンソースのJava診断プログラムです。簡単に起動でき、グラフィカルインターフェースでJavaプログラムを操作できます。プログラムのメモリ使用量、スレッド情報、GCステータスの監視をサポートし、既存のコードを逆コンパイルして変更することも可能です。

下の図に示すように、Arthas の動作原理は一般的に次の手順で構成されます。

  • Arthas を起動し、対象の Java プログラムを選択すると、Arthas は対象プログラムにプロキシを挿入します。
  • プロキシは、HTTP と Telnet を組み合わせてクライアントとの接続を確立するサーバーを作成します。
  • クライアントはサーバーとの接続を確立します。
  • その後のクライアント側の監視やプログラムの調整は、サーバー側の Java インストルメンテーション メカニズムを通じてアプリケーションと対話することで実行できます。

読者が Arthas の基本を素早く学習して習得できるように、この記事では、よく遭遇するいくつかの例を使用して、Arthas の日常的な使用方法の一般的な例を示します。

Arthasの設定と使用方法の詳細な説明

クイックスタート

いくつかの典型的な事例を紹介する前に、Arthasをダウンロードして基本的な使い方を学ぶ必要があります(注:デモ環境はWindows 10に基づいていますが、Linuxでも操作は同様です)。Arthasの公式サイトは以下の通りです。

https://arthas.aliyun.com/

便宜上、私は通常、ダウンロードにコマンドラインを使用します。

 curl -O https://arthas.aliyun.com/arthas-boot.jar

完了したら、次のコマンドを使用して Arthas を起動できます。

 java -jar arthas-boot.jar

インターフェースに初めてアクセスするとわかるように、Arthasはサーバー上で実行されている各Javaプログラムに番号を付けます。対応する番号を入力してEnterキーを押すだけで、対象プログラムを監視できます。デモのために、ランダムに2を選択してEnterキーを押しました。

次に、下の画像に示すように、インタラクティブ インターフェイスに入りました。

Arthasで最もよく使われる監視コマンドはdashboardです。dashboardの使い方を説明するには、Linuxと同様にdashboardコマンドに--helpを追加します。

下の画像に示すように、ダッシュボードは通常次のように使用されます。

  • デフォルトのパラメータを追加しないと、パネルは 5 秒ごとに更新されます。
  • -i はパネルの更新間隔を指定します。
  • -n は更新回数を指定します。

したがって、10 秒ごとに更新する場合、3 回実行されるコマンドは次のようになります。

 dashboard -i 10000 -n 3

パネルは10秒ごとに更新されます。監視インターフェースは以下の画像に示されています。この画像から、ダッシュボードがおおまかに3つのセクションで構成されていることがわかります。

  • スレッド情報セクションには、ID、スレッド名、スレッド グループ、優先度、スレッドの実行ステータス、CPU 使用率、待機時間、実行時間、中断の有無などの列名があります。
  • メモリ使用量セクションでは、主に各世代のメモリとガベージ コレクション (GC) の状態が記録されます。
  • サーバー ランタイム パラメータ セクションには、現在プログラムを実行しているサーバーのカーネル バージョンや JDK バージョンなどの情報が記録されます。

Arthas では、ダッシュボードを終了するには Ctrl+C を押すだけです。現在の Java プログラムの対話型インターフェースを終了するには、「exit」と入力します。

CPU 100% 問題の場所を特定する

CPU 100% 問題は、本番環境におけるトラブルシューティングが最も難しい問題の一つです。Arthas 導入前は、一般的なトラブルシューティングのアプローチはおおよそ次のとおりでした。

  • Java プロセスかどうかを判断するには、`top` を使用します。
  • その場合は、jps を使用して Java プロセス ID を見つけます。
  • 最も多くのリソースを消費するスレッド番号を見つけます。
  • スレッド番号を16進数に変換します。
  • jstack を使用してログをエクスポートし、グローバル検索を使用して 16 進数のスレッド番号で問題のあるコード セグメントを見つけます。

上で述べたように、かなり面倒ではないでしょうか?Arthasを使えば、問題の特定がはるかに簡単かつ迅速になります。この例を説明するために、Spring Bootプロジェクトを使ってCPU使用率100%の問題をシミュレートするコードスニペットを作成しました。以下のコードを使用していることがわかります。

 /** * 一个线程数只有1的线程池*/ private static ExecutorService threadPool = Executors.newCachedThreadPool(); @RequestMapping("cpu-100") public static void cpu() { loopThread(); cpuHigh(); } /** * 极度消耗CPU的线程*/ private static void cpuHigh() { // 添加到线程threadPool.submit(() -> { while (true) { log.info("cpu working"); } }); } /** * 无限循环的线程*/ private static void loopThread() { for (int i = 0; i < 10; i++) { new Thread(() -> { while (true) { log.info("死循环线程工作中"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "thread" + i).start(); } }

コードが完成したら、プロジェクトを起動してアドレスをリクエストします。するとすぐにCPU使用率が100%近くまで急上昇することがわかります。ここでArthasが役立ちます。当然ですが、まずArthasを起動します。

 java -jar arthas-boot.jar

この時点で、コンソールには以下のオプションが表示されます。各オプションは異なるJavaプログラムを示すために異なる番号で識別されます。ターゲットプログラムであるArthasExampleApplicationの番号は1なので、1を入力してEnterキーを押します。

 F:\github>java -jar arthas-boot.jar [INFO] JAVA_HOME: D:\myinstall\jdk8\jre8 [INFO] arthas-boot version: 3.6.9 [INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER. * [1]: 18720 com.example.arthasExample.ArthasExampleApplication [2]: 19300 org.jetbrains.jps.cmdline.Launcher [3]: 7876 org.jetbrains.idea.maven.server.RemoteMavenServer [4]: 14488

コンソールに入り、thread コマンドを直接入力すると、CPU 使用率が非常に高い pool-1-thread-1 という名前のスレッドがあることがわかります。そのため、このスレッドが動作しているコード セグメントを見つける必要があります。

コンソールからわかるように、シリアル番号は 59 なので、次のように直接入力できます。

 thread 59

問題のあるコード セグメントは TestController の 42 行目にすぐに見つかりました。

コードの場所を特定したら、クラスのパッケージパス(com.example.arthasExample.TestController)に基づいてArthasを使用して直接逆コンパイルし、ソースコードを表示できます。コマンドは以下のとおりです。

 jad --source-only com.example.arthasExample.TestController

最終的に問題のあるコードが見つかり、すぐに修正されました。

スレッドデッドロックの問題の特定

スレッドデッドロック問題に関して、以下のサンプルコードも提供しています。スレッド1はまずロック1を取得し、次にロック2を取得しますが、スレッド2はその逆を行います。この2つのロック取得順序がループを形成し、デッドロックが発生します。

 //两把锁private Object lock1 = new Object(); private Object lock2 = new Object(); @RequestMapping("dead-lock") public void deadLock() { //线程1先取得锁1,休眠后取锁2 new Thread(() -> { synchronized (lock1) { try { log.info("t1 successfully acquired the lock1......"); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { log.info("t1 successfully acquired the lock1......"); } } }, "t1").start(); //线程2先取得锁2,休眠后取锁1 new Thread(() -> { synchronized (lock2) { try { log.info("t2 successfully acquired the lock2......"); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { log.info("t2 successfully acquired the lock1......"); } } }, "t2").start(); }

コードフローを理解した後、このインターフェースを直接呼び出します。Arthasを開き、「thread」と入力してスレッド情報を表示します。2つのスレッド(t1とt2)が待機状態になっていることがわかります。これはおそらく問題が発生していることを示しています。

次に「thread -b」と入力すると、スレッド t2 がロックされていることがわかり、2 つのスレッド間にデッドロックが存在することが判明しました。

上記の結果から、2つのスレッドのIDはそれぞれ65と66であることがわかります。したがって、スレッドID番号を指定したコマンドを使用することで、問題のあるコードセグメントを直接特定し、問題を解決できます。

コードを逆コンパイルして表示する

上記で既にjad逆コンパイルコマンドを使用しました。私にとって、jadには2つの一般的な用途があります。クラスを逆コンパイルするコマンド(jad --source-only class_package_path)に加えて、メソッドのコードセグメントを検索するコマンド(jad --source-only class_package_path_method_name)もあります。

たとえば、TestController 内の deadLock コードを見つけたい場合は、次のように入力します。

 jad --source-only com.example.arthasExample.TestController deadLock

下の画像に示すように、メソッドのコードを直接確認できます。

場所フィールド情報(あまり使用されない)

特定のクラスの下にあるすべてのフィールドの詳細を表示する場合は、このコマンドを使用できます。

 sc -d -f 类的包路径

たとえば、TestController のフィールドの詳細を表示する場合は、次のコマンドを入力します。

 sc -d -f com.example.arthasExample.TestController

ご覧のとおり、このコマンドを使用すると、フィールドの定義と注釈だけでなく、スレッド プールの使用状況とコレクション内の値も表示できます。

メソッドのリストを表示する(あまり使用されない)

このコマンドはあまり使いませんが、完全性のために説明しておきます。クラスのメソッドを表示したい場合は、次のようにします。

 sm 类的包路径

私自身を例にとると、TestController を表示する方法は次のとおりです。

 sm com.example.arthasExample.TestController

出力は次のようになります。

静的変数モニタリング(一般的に使用される)

Arthasは静的変数の解析機能を提供します。例えば、次のコードスニペットでは、リスト内の詳細を確認したい場合はognlを使用できます。

 private static List<String> list = new ArrayList<>(); @RequestMapping("add") public void add() { for (int i = 0; i < 10; i++) { list.add("val" + i); } }

API呼び出しを完了し、アイテムを追加したら、OGNLを使って直接監視できます。例えば、上記のリストの内容を確認したい場合は、次のコマンドを使用します。

 ognl '@类的包路径@变量名'

したがって、リストを表示したい場合、コマンドは次のようになります。

 ognl '@com.example.arthasExample.TestController@list'

出力は次のようになります。

もちろん、OGNLには、コレクションの長さのチェックやコレクションへの要素の追加など、より特殊な用途もあります。詳細については、こちらのGitHubのissueをご覧ください。

https://github.com/alibaba/arthas/issues/71

実行時パフォーマンスの問題のトラブルシューティング

操作の実行時間はログ記録によって監視されることがよくあります。しかし、複雑な本番環境では、考慮不足のために特定のメソッドの監視を忘れてしまうことがよくあります。Arthasは同様に、メソッドの実行時間を監視および計算するための便利なコマンドを提供しています。

ここでは、ユーザー クエリ中にパラメータ検証、他のサービス、Redis、および MySQL への呼び出しをシミュレートする UserServiceImpl の例を示しました。

 @Service @Slf4j public class UserServiceImpl { public JSONObject queryUser(Integer uid) throws Exception { check(uid); service(uid); redis(uid); return mysql(uid); } public void service(Integer uid) throws Exception { log.info("调用其他service。。。。。"); TimeUnit.SECONDS.sleep(RandomUtil.randomLong(1, 2)); } public void redis(Integer uid) throws Exception { log.info("查看redis缓存数据。。。。。"); TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(100, 200)); } public JSONObject mysql(Integer uid) throws Exception { log.info("查询MySQL数据......"); TimeUnit.SECONDS.sleep(RandomUtil.randomInt(3, 4)); JSONObject jsonObject = new JSONObject(); jsonObject.putOnce("name", "xiaoming"); jsonObject.putOnce("age", 18); return jsonObject; } public boolean check(Integer uid) throws Exception { if (uid == null || uid < 0) { log.error("非法用户id,uid:{}", uid); throw new Exception("非法用户id"); } return true; } }

対応するコントローラーコードは以下の通りです。本番環境でこのインターフェースが非常に時間がかかり、ログも取得できない場合、Arthasを使用してどのようにトラブルシューティングすればよいでしょうか?

 @Autowired private UserServiceImpl userService; @GetMapping(value = "/user") public JSONObject queryUser(Integer uid) throws Exception { return userService.queryUser(uid); }

`trace` コマンドを使うことができます。まずは `TestController` 内の `queryUser` への時間のかかる呼び出しを `trace` を使ってトレースしてみましょう。

 trace com.example.arthasExample.TestController queryUser

TestController には例外がなく、常に UserServiceImpl で消費が発生していることがわかります。

したがって、UserServiceImpl の queryUser に対してコールバックを実行します。

 trace com.example.arthasExample.UserServiceImpl queryUser

コマンドを入力すると、コンソールはこのメソッドをブロックして監視します。その後、このインターフェースを呼び出すと、MySQLクエリに非常に時間がかかることがわかります。これにより、問題をさらに推測することができます。

メソッド時間消費統計

特定の時間単位内でのメソッドのリクエスト時間と呼び出しステータスを監視したい場合があります。`monitor` コマンドを使用できます。例えば、`TestController` の `queryUser` ステータスを5秒ごとに確認したい場合は、次のように入力します。

 monitor -c 5 com.example.arthasExample.TestController queryUser

コンソールには、リクエストの数、成功と失敗の数、5 秒ごとの平均所要時間などの情報が表示されます。

入力/出力パラメータエラーの特定

ログが欠落しているメソッド(上記の mysql() の入出力パラメータなど)の入出力パラメータの詳細を特定したい場合があります。Arthas の watch メソッドと以下のコマンドを使用することで、これを実現できます。

 watch com.example.arthasExample.UserServiceImpl mysql '{params[0],returnObj}'

ご覧のとおり、入力パラメータは 1 で、出力パラメータはオブジェクトです。

さらに、オブジェクトの内容を印刷したい場合は、toString メソッドを使用できます。

 watch com.example.arthasExample.UserServiceImpl mysql '{params[0],returnObj.toString()}'

これらの機能に加えて、ウォッチは他の多くの高度な機能をサポートしています。詳細については、公式ドキュメントを参照してください: https://arthas.aliyun.com/doc/watch.html

メソッド呼び出しパスの監視

上記の MySQL メソッドを例にとると、その呼び出しパスをすばやく見つけたい場合は、stack メソッドを使用できます。

 stack com.example.arthasExample.UserServiceImpl mysql

詳細な呼び出しパスを確認し、コンソールに直接出力することができます。

メソッド呼び出しを取得するプロセス

メソッド呼び出し中に例外が発生した理由を確認し、入力パラメータと出力パラメータを調べるには、`tt` コマンドを使用します。例えば、`check` メソッドがエラーをスローする理由を確認したい場合は、`tt` を使用します。

 tt -t com.example.arthasExample.UserServiceImpl check

出力から判断すると、例外が2回目にスローされたようです。インデックス1001を使って問題箇所を特定できます。

 tt -i 1001

最終的に、入力パラメータは -1 であると結論付けることができます。

通話を再開したい場合は、以下を使用できます。

 tt -i 1001 -p

OOM問題

以下のコードを例に、コマンド -Xms100m -Xmx100m を使用してヒープ メモリを 100m に変更したところ、起動直後に OOM の問題が再現されました。

 final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());// 创建线程池,通过线程池,保证创建的线程存活@RequestMapping(value = "/oom") public String oom(HttpServletRequest request) { poolExecutor.execute(() -> { Byte[] c = new Byte[4* 1024* 1024]; localVariable.set(c);// 为线程添加变量}); return "success"; }

その後、Arthas を使用してみると、旧世代のメモリがほぼいっぱいになっており、ガベージ コレクションが非常に頻繁に行われていることがわかりました。

したがって、Arthas の heapdump 関数を直接使用して、ファイルを .mat ファイルにエクスポートし、さらに分析することができます。

 heapdump D://heap.hprof

エクスポートされた結果は以下に表示されます。「詳細」をクリックすると、問題の原因を推測できます。

問題のあるコードはすぐに見つかりました。

オンラインコード置換

テスト中に、どうしても見落としてしまうケースがあります。例えば、以下に示すように、ビジネス要件では id が 1 未満の場合にのみ例外をスローする必要があります。しかし、不注意により、条件を id<2 と記述してしまいました。その結果、このコードを知らずに本番環境にデプロイしてしまい、ビジネスクエリで問題が発生してしまいました。

 @GetMapping(value={"/user/{id}"}) public JSONObject findUserById(@PathVariable Integer id) { log.info("id: {}",id); if (id != null && id < 2) { throw new IllegalArgumentException("id < 1"); } return new JSONObject().putOnce("name","user"+id).putOnce("age",18); }

本番環境では、JARファイルをすぐに再起動して置き換えることはできません。このような問題には、Arthasを使用してオンラインホットアップデートを実現できます。

まず、以下に示すように、本番環境からバイトコードを逆コンパイルし、ローカル マシンにエクスポートします。

 jad --source-only com.example.arthasExample.TestController > d://TestController.java

次に、対応するコード スニペットを変更します。

次に、このクラスに対応するクラスローダーのハッシュコードを見つける必要があります。

 sc -d *TestController | grep classLoaderHash

対応するハッシュ コードが見つかったら、このクラス ローダーを使用して、変更された Java ファイルをバイトコードにコンパイルできます。

 mc -c 18b4aac2 d://TestController.java -dd://

最後に、実行中のプログラムにコードをホットアップデートします。

 redefine d://com/example/arthasExample/TestController.class

ここで、パラメータとして 1 を渡すと、エラーは発生せず、コードのホット アップデートが成功したことが示されます。

さらに分析するために Spring コンテキストを取得します。

Springコンテナの解析情報を取得し、オンラインで問題箇所を特定したい場合があります。Arthasを介してこのクラスをインターセプトし、さらに呼び出して解析を行うことができます。

Spring MVCのソースコードに詳しい読者の方は、HTTPリクエストがWebコンテナに送信されるたびに、マッピングおよび転送プロセス中にRequestMappingHandlerAdapterを経由することをご存知でしょう。下のクラス図からわかるように、このクラスはWebApplicationObjectSupportを継承しており、getWebApplicationContextメソッドによってSpringコンテナのコンテキストを取得し、Springコンテナを分析・管理することができます。

したがって、Arthas の tt 命令を使用して、このクラスへの呼び出しをトレースできます。

 tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod

次に、任意の API を呼び出して呼び出しログを取得します。

インデックス 1000 の呼び出しレコードを使用し、-w で OGNL を指定して Spring コンテキストと testController を取得し、メソッド呼び出しを完了します。

 tt -i 1000 -w 'target.getApplicationContext().getBean("testController").findUserById(3)'

下の画像に示すように、呼び出しが正常に完了し、返された結果を受け取ったことがわかります。

まとめ

Arthasの一般的な操作のデモはこれで終了です。Arthasの使い方の詳細については、Arthasの公式ウェブサイト(https://arthas.aliyun.com/doc/)をご覧ください。