Ceph Clientモジュールのロック分離と並列性能最適化

# 一、概要

　本記事では、Cephファイルシステムのユーザー空間クライアント（Clientモジュール）に対して、従来の大域ロックを廃止し、複数のロックへと分離することで、並列性とパフォーマンスを大幅に改善した事例を紹介します。ロック再設計に加えて、マルチスレッド対応、ガーベジコレクション、非同期処理、安全な参照管理などの機構を新たに導入し、小ファイルや大規模IOにおける実効性能を最大300%まで向上させました。
# 二、背景

　Cephファイルシステムにおいて、clientモジュールはユーザー空間のクライアントとして、ユーザーによるすべてのファイルシステム操作を担当する。上位には各種プロトコル（nfs-ganesha、samba）やfuseプロセスと通信し、下位にはバックエンドのメタデータサーバ（MDS）やオブジェクトストレージデバイス（OSD）、またはキャッシュ層のobjectcacherと通信する、非常に重要なモジュールである。バックエンドの処理性能がファイルシステム全体の性能の鍵である一方、clientの性能もバックエンドサービスが処理できる最大OPS数を直接左右し、ひいてはファイルシステム全体の性能に影響を与える。そのため、clientモジュールの性能向上は、ファイルシステム全体の性能向上につながる。


![客户端图.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4103018/3aa75f9c-29c3-4e79-9b86-6fa03dd55e26.png)

# 三、改修の目的

　ユーザー空間のクライアントであるClientモジュールの性能ボトルネックは、主にclient_lockと呼ばれる1つの大きなロックにある。clientモジュール内では、ほぼすべての処理フローがこのロックを使用しているため、上位からさまざまなリクエストが発行された場合、それらは基本的に前のリクエストの処理が完了してロックが解放されるのを待つ必要がある。返信を待つ間にロックを解放して次のリクエストを処理し、返信が届いたら再びロックを取得して処理を完了させた後、上位に返してロックを再び解放する。このように、全体として直列的な処理となっており、並行性が低い。

　したがって、本改修の方法は、このclient_lockを細分化し、各処理が単一のロックに縛られないようにすることで、クライアント全体の並行性を向上させることにある。これにより、複数のリクエストを同時に処理できるようになり、パフォーマンスの向上が期待できる。
![客户端并行处理图.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4103018/8167c292-0f03-414e-9d92-99691006a21a.png)

# 四、改修方針
## 1．ロックの再設計
　clientモジュールにおいては、従来すべての処理において単一のclient_lockを使用していた。本改修では、Clientクラス内の各種クラス・メンバ変数の役割、使用頻度、利用される場面およびコンテキスト、関数の目的に基づき、client_lockを以下のような複数のロックに分割し、それぞれのロックに対応する機能および保護範囲を定義する。
| ロック名 | 種類 | 機能 | 保護対象・範囲 |
|:-----------|------------:|:------------:|:------------:|
| (Client)Mutex timer_lock; | ミューテックス|Clientモジュール内のtick処理およびGCスレッド専用のロック。呼び出し頻度が高い|ØSafeTimer timer;|
| (Client)RWLock mdsmap_lock; | 読書きロック |mdsmapに対する操作を保護。読み込みが大半を占め|Østd::unique_ptr<MDSMap> mdsmap;|
| (Client)Mutex client_lock; | ミューテックス | 使用頻度が低く重要性の低い変数や関数、および条件変数の保護 |Øint acl_type;<br>ØCond mount_cond;<br>ØCond sync_cond;|<br>Østd::map<std::string, std::string> metadata;<br>Øclient_t whoami;など|
| (Client)RWLock generic_lock; | 読書きロック | 読み込みが多く変更頻度が低い変数を保護し、ロック競合を軽減 | Øepoch_t cap_epoch_barrier;<br>Øint acl_type;<br>Øuint32_t deleg_timeout;など|
| (Client)spinlock snap_realms_lock;| スピンロック |snap_realms集合とsnaprealm処理全体を保護  |Øunordered_map<inodeno_t,SnapRealm*> snap_realms;|
| (Client)Mutex trim_lock;  | ミューテックス | dentry削除・作成処理における排他制御 |Øtrim_caps()<br>Øtrim_cache()<br>Øtrim_cache_for_reconnect()<br>Ølink()など|
| (Client)Mutex opened_session_caps_lock; | ミューテックス| opened_session_capsキューのスレッド安全性を確保  |Øunordered_multiset<uint64_t> opened_session_caps;<br>ØCond　opened_session_cond;|
| (Client)spinlock delayed_remove_caps_lock; | スピンロック | remove_capsの遅延実行用キューを保護 |Ømap<uint64_t, map<Cap*,bool>> delayed_remove_caps;|
| (Client)Mutex cr_inode_lock; | ミューテックス | inodeの生成・削除時の排他制御とwait条件保護|Øadd_update_inode()<br>Øput_inode()<br>Øopen_snapdir()|
| (Client)RWLock root_lock; | 読書きロック | root関連変数を保護。読み込みが多く変更頻度は低い |ØInode* root;<br>Ømap<Inode*, InodeRef> root_parent;<br>ØInode* root_ancestor;<br>ØInodeRef cwd;|
| (Client)Mutex oc_lock; | ミューテックス | ObjectCacherモジュール用ロック |Østd::unique_ptr<ObjectCacher> objectcacher関連|
| (Client)mds_sessions_lock; | 読書きロック | mds_sessions集合やclosing状態、拒否集合を保護 |Ømap<mds_rank_t, MetaSession> mds_sessions;<br>Østd::set<mds_rank_t> mds_ranks_closing;<br>Østd::map<mds_rank_t, entity_addrvec_t> rejected_by_mds;|
| (Client)spinlock fd_or_odirs_lock; | スピンロック | ファイルディスクリプタやオープンディレクトリ集合の保護 |Øinterval_set<int> free_fd_set;<br>Øunordered_map<int, Fh*> fd_map;<br>Øset<Fh*> ll_unclosed_fh_set;<br>ØUnordered_map<dir_result_t*> opened_dirs;|
| (Client)RWLock inode_map_lock;| 読書きロック | inode集合、fake inode集合、dirty/delay listを保護|Øceph::unordered_map<vinodeno_t, Inode*> inode_map;<br>Øceph::unordered_map<ino_t, vinodeno_t> faked_ino_map;<br>Øinterval_set<ino_t> free_faked_inos;<br>Øino_t last_used_faked_ino;<br>Øino_t last_used_faked_root;<br>Ømulti_xlist<Inode*> delay_list;<br>Ømulti_xlist<Inode*> dirty_list|
| (Client)RWLock mds_requests_lock; | 読書きロック  | mdsリクエスト集合とtid番号関連変数の保護 |Ømap<ceph_tid_t, MetaRequest*> mds_requests;<br>Øceph_tid_t last_tid = 0;<br>Øceph_tid_t oldest_tid = 0;|
| (Inode)Mutex im_lock(recursive); | 再帰ミューテックス | Inode・Cap・Dirクラスの全変数を保護 |Inode, Cap, Dir クラス全体|
| (Dentry)Mutex::dn_lock | ミューテックス | Dentryクラスの変数を保護し、一部線形処理も担当 |Dentry クラスの大部分|
| (Fh)Mutex::fh_lock | ミューテックス  | Fhクラスの全変数を保護 |Fh クラス全体|
| (MetaSession)spinlock::session_lock | スピンロック | MetaSession内の全変数を保護 |MetaSession クラス全体|
| (SnapRealm)Mutex::snaprealm_lock | 再帰ミューテックス | SnapRealm内の全変数を保護 |SnapRealm クラス全体|
| (Client)Mutex::doing_lock | ミューテックス | shardキュー使用時にshard関連処理を保護 |Ømap<uint64_t, map<uint64_t, handling_ino_info>> doing_info;<br>Ømap<uint64_t, unsigned> tid_table;<br>ØCond doing_cond;|
| (MetaRequest)Mutex::cond_lock | ミューテックス| MetaRequest内の重要なフラグ・条件変数を保護 |Ømds_rank_t resend_mds;<br>Øbool kick;<br>ØCond cond;など|
| (QoSClient)Mutex::qos_lock | ミューテックス | QoSClient内の集合を保護 |Øunordered_map<inodeno_t, uint64_t> qos_dir_map<br>Øunordered_map<ceph_tid_t, Cond *> qos_wait;|
| (Client)spinlock free_inode_map_lock | スピンロック | 削除予定inodeの集合を保護 |Øceph::unordered_map<vinodeno_t, Inode*> free_inode_map;<br>Øceph::unordered_map<vinodeno_t, bool> is_freeing_inode_map;|
| atomic変数| アトミック | Client内のカウント・状態変数の一部をロックレスで管理|Øatomic<bool> initialized<br>Øatomic<bool> mounted<br>Øatomic<int> num_flushing_capsなど|

## 2．新たに導入された仕組み
　ロックの分割によって処理が並列化されたため、以前は起こり得なかった競合やタイミングの問題が発生するようになった。これに対応するため、Client モジュール全体において以下のような新たな機構が導入された。

### (1) LRUのマルチスレッド化
　従来のLRUは単一スレッド向けに設計されていたが、Client のロック分割に伴う並行性向上に対応するため、LRU構造をマルチスレッド化した。新たに MLRUObject、MLRUList、MLRU クラスを追加し、操作ロジックは従来のLRUのルールに準拠している。
### (2) xlistのマルチスレッド化
　同様に、従来の xlist も単一スレッド設計であったが、Client 内部での利用頻度が高いため、multi_xlist クラスを新たに導入し、マルチスレッド対応を行った。基本的な使用ロジックは従来の xlist と同様である。
### (3) Inodeのpin機構
　ロックが分割されたことにより、あるスレッドが特定の inode を操作中に、別スレッドがその inode を削除しようとする状況が発生し得る。たとえば、あるスレッドが getattr を実行中に、別スレッドによる trim 処理でその inode が削除されるといったケースである。
この問題に対応するために、pin機構を導入し、スレッドが inode を操作する際には対象の inode をpin状態にする。内部的にはスレッドIDと参照カウントを集合で管理し、pinされている inode は即時削除されず、unpin されるか再試行されるまで保持される。
### (4) Dentryのpin機構
　Dentry においても、inode 同様、使用中の dentry が trim によって削除されるのを防ぐため、簡易的なpin機構が導入された。ただし、inode ほど使用頻度が高くないため、機構自体はより簡素である。
### (5) inodeのcapsの遅延削除機構
　ある inode が使用中であるにもかかわらず、他スレッドがその inode の caps を削除しようとした場合、該当 inode の caps 状態を delayed_remove_caps 集合に一時的に保存し、削除処理を遅延させる。通常、この集合は tick によって周期的に走査され、削除が実行される。ただし、削除対象の inode がその前に他のスレッドにより更新された場合は、当該項目は遅延削除キューから除外される。
### (6) Client側のms_dispatchキューの分割
　従来、Client モジュールが mds からのメッセージを受信する際には ms_dispatch インターフェースを介し、単一のキューで順番に処理していた。しかし、各メッセージの関連性が低く、頻度も異なるため、並列処理が望まれる場面が多い。そこで ms_dispatch を3つの専用キューに分割し、処理の並列化と効率化を図った。
| キュー名 | 機能説明 | 受信するメッセージ種別例 |
|:-:|:-:|:-:|
|tp_reply_queue | MDSからの各種リクエストのreply応答、転送メッセージ、caps関連メッセージの処理を担当  |  CEPH_MSG_CLIENT_REQUEST_FORWARD<br>CEPH_MSG_CLIENT_REPLY<br>CEPH_MSG_CLIENT_CAPS<br>CEPH_MSG_CLIENT_SNAP|
|tp_session_queue | MDSから送信されるセッション関連メッセージの処理専用 | CEPH_MSG_CLIENT_SESSION<br>CEPH_MSG_MDS_MAP|
|tp_generic_queue| 上記以外のその他のメッセージ（頻度が少ない、または特定機能関連のもの）を処理 |CEPH_MSG_FS_MAP<br>CEPH_MSG_OSD_MAP<br>CEPH_MSG_CLIENT_QUOTA<br>CEPH_MSG_CLIENT_WORMなど|

### (7) tp_reply_queue のシャーディング対応
　デフォルトでは tp_reply_queue は1つのキューで運用されるが、設定により複数のシャードに分割された ReplyShard スレッドプールキューを使用できるようになった。
対応する設定項目は以下の通り：
```ini
[client]
client_tp_reply_thread_timeout = 999999
client_tp_reply_thread_suicide_timeout = 999999
replypool_max_dispatch_per_run = 256      # 調整可能（1〜512、デフォルト：10）
client_reply_num_shards = 8               # 調整可能（デフォルト：4、実行中には変更不可。ceph.confにて事前設定）
client_reconnect_stale = false
```
### (8) Inode および Dentry の参照カウントロジックの変更
　Inode および Dentry の安全な使用を保証するため、それぞれの参照カウントの管理方法が見直された。たとえば Inode に関連する caps、snaprealm、dirty_list、delay_list、および Fh などが存在する場合、Inode に対して適切に参照カウントが加算される。
これにより Inode の削除処理は従来よりも複雑となり、削除可否を自律的に判定するロジックが追加された。
### (9) ガーベジコレクション機構の導入
　(8)の変更に伴い、Inode の削除にはガーベジコレクション（GC）スレッドが追加された。このスレッドは Inode が free_inode 状態であるかを判定し、参照カウントが 0、または 2 かつ cap と対応する snaprealm のみが存在する場合には回収対象とみなして非同期キューに登録し削除を行う。
また、削除状態を示すフラグを導入し、複数スレッドによる二重削除や削除中の Inode の使用を防止している。
### (10) trim_caps の非同期化
　長期稼働環境においては、trim_caps の全体走査処理が長時間かかることで他スレッドのブロックを引き起こし、結果としてシステムパフォーマンスが劣化する、あるいはクライアントの応答がゼロに陥るといった問題があった。
　そのため、trim_caps を非同期化し、mds からの指示を受けた際に専用スレッドが起動し、session 上の caps をバックグラウンドでクリーンアップするように改善された。

## 3．ネストロックの取得順序
　各ロックはそれぞれ独立して役割を持つが、実際の処理フローでは複数の保護対象にまたがる場面が多く、業務上の整合性を保つために2つ以上のロックを同時に取得する必要があるケースもある。
　たとえば session にアクセスする際に inode にも同時にアクセスしたり、mds_sessions から session を取得してさらにその session 自体をロックしたりする処理が該当する。
このような場合、ロックの取得順序を明確に定義し、逆順での取得によるデッドロックを防止する必要がある。
　ロック順序は、ロックの使用頻度、対象変数の利用環境、変数間の依存関係に基づいて設計されている。
以下の表にその一例を示す：
| 先に取得するロック（First Lock）   | 後に取得するロック（Second Lock）   |
|:-----------------------------------|:------------------------------------|
| inode_map_lock                     | root_lock                           |
| fh_lock                            | im_lock                             |
| fh_lock                            | oc_lock                             |
| fh_lock                            | mdsmap_lock                         |
| fh_lock                            | inode_map_lock                      |
| fh_lock                            | session_lock                        |
| client_lock                        | im_lock                             |
| client_lock                        | root_lock                           |
| client_lock                        | oc_lock                             |
| client_lock                        | fh_lock                             |
| client_lock                        | session_lock                        |
| client_lock                        | dn_lock                             |
| client_lock                        | mdsmap_lock                         |
| client_lock                        | inode_map_lock                      |
| inode_map_lock                     | im_lock                             |
| inode_map_lock                     | cr_inode_lock                       |
| inode_map_lock                     | session_lock                        |
| inode_map_lock                     | mdsmap_lock                         |
| inode_map_lock                     | mds_sessions_lock                   |
| inode_map_lock                     | oc_lock                             |
| inode_map_lock                     | opened_session_caps_lock            |
| im_lock                            | root_lock                           |
| im_lock                            | dn_lock                             |
| im_lock(Parent)                    | im_lock(Child)                      |
| im_lock(SnapParent)                | im_lock(Child)                      |
| im_lock(Parent)                    | dn->pin()                           |
| im_lock                            | fd_or_odirs_lock                    |
| im_lock                            | oc_lock                             |
| im_lock                            | delayed_remove_caps_lock            |
| im_lock                            | cr_inode_lock                       |
| im_lock                            | session_lock                        |
| im_lock                            | mds_sessions_lock                   |
| im_lock                            | mdsmap_lock                         |
| im_lock                            | opened_session_caps_lock            |
| im_lock                            | snaprealm_lock                      |
| session_lock                       | opened_session_caps_lock            |
| session_lock                       | snaprealm_lock                      |
| session_lock                       | snap_realms_lock                    |
| session_lock                       | mds_requests_lock                   |
| session_lock                       | delayed_remove_caps_lock            |
| session_lock                       | dn_lock                             |
| trim_lock                          | im_lock                             |
| trim_lock                          | dn_lock                             |
| trim_lock                          | session_lock                        |
| mds_sessions_lock                  | session_lock                        |
| mds_sessions_lock                  | client_lock                         |
| mds_sessions_lock                  | dn_lock                             |
| mdsmap_lock                        | mds_sessions_lock                   |
| mdsmap_lock                        | session_lock                        |
| cr_inode_lock                      | oc_lock                             |
| snap_realms_lock                   | snap_realmlock                      |
| snaprealm_lock(Child)              | snaprealm_lock(Parent)              |

　一部のロックについては、前述のロック取得順序表に記載されていないが、これはネストが発生する頻度が極めて低いために省略されているだけであり、実際にネストされて使用される可能性がある。
そのため、コードを修正または確認する際に、表に記載されていないロックのネスト関係が現れた場合には、以下の手順で対応することが推奨される：
●	該当ロックの使用箇所をコード全体から検索する
●	そのロックが既に他のロックとネストされて使用されている実例が存在するかを確認する
●	既存のネスト順が存在する場合は、それに従ってロックを取得し、必要に応じて表に追記する
●	既存の順序がない場合は、処理の論理に基づいて適切な順序でロックを取得し、その順序を表に新たに加える
## 4．関数ごとのロック取得方針
　Client モジュールには数百〜千以上の関数が存在し、それぞれにおいて使用する変数や呼び出し経路が異なるため、ロック取得の方針も関数の種類によって異なる。
### (1) OP関数およびメッセージ処理関数
　これは、上位の nfs-ganesha、samba、fuse などと連携する典型的なインターフェース関数を指す。例えば：
●	ll_* 系関数：ll_getattr、ll_lookup、ll_open、ll_read、ll_write など
●	非 ll_ 系の操作関数（OP）：getattr、lookup_ino、mkdir、read など
　これらの関数では、従来使用されていた client_lock を排除し、関数内で使用する inode、fh、dentry などに対して個別にロックを取得することでスレッド安全性を確保する。
また、メッセージ処理関数（handle_caps、handle_client_reply、handle_cap_grant、handle_mds_map、handle_client_session などの handle_* 系関数）についても、内部で使用される変数の種類や保護対象に応じて、適切なスコープと順序でロックを取得することが基本方針である。
### (2) 共通関数
　共通関数におけるロック取得方針については、以下の表に整理する：
| 使用頻度／規模 | 方針 | 代表的な関数例 |
|:-:|:-:|:-:|
| 高頻度 × 中〜大規模  | 関数内部でスレッド安全性を確保し、**呼び出し元ではロックを取得しない**  | make_request、handle_client_reply など  |
|中〜低頻度 × 中〜大規模   | 原則として関数内部でスレッド安全性を確保するが、**必要に応じて一部変数については呼び出し元でロック取得することもある**  |xattr_permission、XX_permission など   |
|中〜低頻度 × 小規模   |関数内部ではロックを取得せず、**呼び出し元でのロック取得が必須**   | caps_issued_mask、mark_caps_dirty など（Inode 内の関数が多数）  |
|非常に限定的なケースまたは特定機能でのみ使用される関数   |原則として呼び出し元でロックを取得するが、**他の処理とロックの関係がなければ関数内部で単独ロック取得も可**   | is_group_quota_bytes_exceeded、recall_deleg など  |

# 五．性能比較テスト
　典型的なワークロードにおいて、以下のようなパフォーマンス向上が確認された：
•	小ファイル（8K）を1,000万件読み書きするケースでは、性能が**100%向上**
•	大ファイルに対する小ブロック読み書きでは、性能が**300%向上**
•	その他のアクセスパターンでも、概ね**50%〜300%の改善**が得られた