特定の継続的ストレス テスト中に、リクエスト量が安定しているときでも、OOM によって強制終了されるまで、GreptimeDB のフロントエンド ノード メモリが増加し続けることがわかりました。フロントエンドにメモリ リークが発生しているはずだと判断し、メモリ リークのトラブルシューティングを開始しました。
ヒーププロファイリング
大規模なプロジェクトでは、コードを見るだけでメモリ リークを見つけることはほとんど不可能です。したがって、最初にプログラムのメモリ使用量の統計分析を行う必要があります。幸いなことに、GreptimeDB で使用される jemalloc にはヒープ プロファイリングが付属しており、jemalloc プロファイル ダンプ ファイルのエクスポートもサポートされています。そこで、GreptimeDB のフロントエンド ノードのメモリが 300MB と 800MB に達したときに、それぞれのメモリ プロファイル ファイルをダンプし、jemalloc の組み込みツールを使用して2 つのjeprof
メモリの差 (--base
パラメータ) を分析し、最後にフレーム グラフで表示しました。
明らかに、画像の中央にある長いブロックは、増加し続ける 500MB のメモリによって占有されています。よく観察すると、スレッド関連のスタック トレースがあることがわかります。スレッドが多すぎる可能性がありますか?コマンドを使用してps -T -p
フロントエンドノードのプロセスを数回確認したところ、スレッド数は 84 で安定しており、すべて作成されると予測されたスレッドでした。したがって、「スレッドが多すぎる」という理由を排除できます。
さらに下を見ると、Tokio ランタイムに関連するスタック トレースが多数見つかりました。Tokio タスク リークも一般的なメモリ リークです。現時点では、別のアーティファクトであるTokio-consoleを使用します。
トキオコンソール
Tokio Console は Tokio の公式診断ツールです。出力結果は次のとおりです。
実際には 5559 個の実行中のタスクがあり、そのほとんどがアイドル状態であることがわかります。したがって、Tokio のタスクでメモリ リークが発生していることが確認できます。ここで疑問が生じます。GreptimeDB コードのどこに、終了できないほど多くの Tokio タスクが生成されているのでしょうか?
上の図の「場所」列から、タスクが生成される場所がわかります。
impl Runtime {
/// Spawn a future and execute it in this thread pool
///
/// Similar to Tokio::runtime::Runtime::spawn()
pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
self.handle.spawn(future)
}
}
次のタスクは、このメソッドを呼び出す GreptimeDB 内のすべてのコードを検索することです。
..Default::default()
!
コードを注意深く検査した結果、最終的に Tokio タスクのリーク箇所を特定し、PR #1512のリークを修正しました。簡単に言えば、頻繁に作成される構造体のコンストラクターで、バックグラウンドで実行し続けることができる Tokio タスクを生成しましたが、時間内にリサイクルできませんでした。リソース管理の場合、Drop
タスクが で正常に終了できる限り、コンストラクター内でタスクを作成すること自体は問題ありません。メモリ リークの悪い点は、この規則を無視したことです。
このコンストラクターはDefault::default()
構造体のメソッドでも呼び出されるため、根本原因を見つけることがさらに困難になります。
Rustには、別の構造体を使用して独自の構造体を構築するための非常に便利な方法、つまり「Struct Update Syntax」があります。 struct が実装されている場合はDefault
、それを struct のフィールド コンストラクターで簡単に使用できます..Default::default()
。Default::default()
内部に「副作用」がある場合(たとえば、今回のメモリ リークの理由 - バックグラウンドで実行される Tokio タスクの作成)、特別な注意を払う必要があります。構造体の構築が完了した後、Default
一時的な構造体が作成されます。資源リサイクルをしっかり行いましょう。
たとえば、次の小さな例: (Rust Playground )
struct A {
i: i32,
}
impl Default for A {
fn default() -> Self {
println!("called A::default()");
A { i: 42 }
}
}
#[derive(Default)]
struct B {
a: A,
i: i32,
}
impl B {
fn new(a: A) -> Self {
B {
a,
// A::default() is called in B::default(), even though "a" is provided here.
..Default::default()
}
}
}
fn main() {
let a = A { i: 1 };
let b = B::new(a);
println!("{}", b.a.i);
}
struct A のメソッドdefault
が呼び出され、出力されますcalled A::default()
。
要約する
- Rust プログラムのメモリ リークをトラブルシューティングするには、jemalloc のヒープ プロファイリングを使用してダンプ ファイルをエクスポートし、メモリ使用量を視覚的に表示するフレーム グラフを生成します。
- Tokio-console は、Tokio ランタイムのタスク実行ステータスを簡単に表示でき、増加するアイドル タスクに特に注意を払うことができます。
- よく使用される構造体のコンストラクターに副作用のあるコードを残さないようにしてください。
Default
値型の構造体にのみ使用してください。
グレプタイムについて
Greptime Greptime Technology は 2022 年に設立され、現在、時系列データベース GreptimeDB と GreptimeCloud という 2 つの製品の改善と構築を行っています。
GreptimeDB は、Rust 言語で書かれた時系列データベースであり、分散型、オープンソース、クラウド ネイティブであり、企業が長期的なコストを削減しながらリアルタイムで時系列データの読み取り、書き込み、処理、分析を行うのに役立ちます。ストレージ。
GreptimeCloud は、オープンソースの GreptimeDB に基づいて、フルマネージド DBaaS だけでなく、可観測性、モノのインターネット、その他の分野と組み合わせたアプリケーション製品をユーザーに提供します。クラウドを使用してソフトウェアとサービスを提供すると、迅速なセルフサービスのプロビジョニングと配信、標準化された運用とメンテナンスのサポート、およびリソースの柔軟性の向上が実現します。 GreptimeCloud は内部テストのために正式にオープンしました。最新の開発状況については、公式アカウントまたは公式 Web サイトをフォローしてください。
公式サイト:https://greptime.com/
パブリックアカウント: GreptimeDB
GitHub: https://github.com/GreptimeTeam/greptimedb
ドキュメント: https://docs.greptime.com/
Twitter: https://twitter.com/Greptime
スラック: https://greptime.com/slack
LinkedIn: https://www.linkedin.com/company/greptime/
1990 年代生まれのプログラマーがビデオ移植ソフトウェアを開発し、1 年足らずで 700 万以上の利益を上げました。結末は非常に罰的でした。 高校生が成人式にオープンソースプログラミング言語を自作―ネチズンの鋭いコメント: 詐欺横行でRustDesk依存、国内サービスの タオバオ(taobao.com)は国内サービスを一時停止、ウェブ版の最適化作業を再開 Java最も一般的に使用されている Java LTS バージョンは 17 、Windows 11 は減少し続ける Open Source Daily | Google がオープンソースの Rabbit R1 を支持、Microsoft の不安と野心; Electricがオープンプラットフォームを閉鎖 AppleがM4チップをリリース GoogleがAndroidユニバーサルカーネル(ACK)を削除 RISC-Vアーキテクチャのサポート Yunfengがアリババを辞任し、将来的にはWindowsプラットフォーム用の独立したゲームを制作する予定