Java Native RuntimeでのFUSEのラッパーの作成

この記事では、カーネルコードを1行も使わずに、Javaのユーザー空間にファイルシステムを実装する方法を説明します。 また、最大のパフォーマンスを維持しながら、Cコードを記述せずにJavaとネイティブコードを接続する方法も示します。







面白い? 猫へようこそ!



ラッパーの実装を開始する前に、FUSEとは何かを理解する必要があります。

FUSE(ユーザー空間のファイルシステム)-ユーザー空間のファイルシステム。ユーザーは特権なしでカーネルコードを書き直すことなく、独自のファイルシステムを作成できます。 これは、ユーザー空間でファイルシステムコードを実行することで実現されますが、FUSEモジュールは現在のカーネルインターフェイスのブリッジのみを提供します。 FUSEは、バージョン2.6.14のメインLinuxコードツリーに公式に含まれていました。







つまり 実際、いくつかのメソッドを実装することにより、独自のファイルシステムを簡単に作成できます( 単純なFSの例 )。 これには数百万のアプリケーションがあります。たとえば、DropboxまたはGitHubのバックエンドでファイルシステムをすばやく作成できます。

または、そのような場合を考えてみましょう。すべてのユーザーファイルがデータベースに格納されているビジネスアプリケーションがありますが、クライアントは突然、すべてのファイルが置かれているサーバー上のディレクトリに直接アクセスする必要がありました。 もちろん、データベースとFSでファイルを複製することは最善の解決策ではありません。ここでは、仮想ファイルシステムが役立ちます。 FUSEラッパーを記述するだけで、ファイルにアクセスするとデータベース内のファイルにアクセスします。



Javaおよびネイティブコード



素晴らしいですが、FUSEの実装は「include the header file <fuse.h>」で始まり、ビジネスアプリケーションはJavaで記述されています。 明らかに、ネイティブコードと何らかの形で対話する必要があります。



ジュニ



標準ツールはJNIですが、特にFUSEを実装するにはネイティブコードからJavaクラスへのコールバックを作成する必要があることを考えると、プロジェクトに多くの複雑さが導入されます。 はい、そして「1回書き込み」は実際に苦しみますが、FUSEの場合、これは私たちにとってそれほど重要ではありません。

実際、JNIでFUSEのラッパーを実装するプロジェクトを見つけようとすると、長い間サポートされておらず、APIカーブを提供するいくつかのプロジェクトを見つけることができます。



Jna



別のオプションはJNAライブラリです。 JNA(Java Native Access)を使用すると、JNIを使​​用せずにネイティブコードに非常に簡単にアクセスでき、Javaコードの記述に制限されます。 すべてが非常に単純です。ネイティブコードに対応するインターフェイスを宣言し、「Native.loadLibrary」を介して実装を取得し、それだけで使用します。 JNAの別のプラスは、最も詳細なドキュメントです。 このプロジェクトは現在も活発に開発されています。



さらに、JNAでラッパーを実装するFUSEの優れたプロジェクトがすでにあります。

ただし、JNAには特定のパフォーマンスの問題があります。 JNAはリフレクションに基づいており、すべての構造をJavaオブジェクトに変換するネイティブコードからの移行は非常に高価です。 ネイティブ呼び出しがまれな場合、これはあまり目立ちませんが、ファイルシステムの場合はそうではありません。 fuse-jnaを高速化する唯一の方法は、大きなチャンクでファイルを読み取ろうとすることですが、これは常に機能するとは限りません。 たとえば、クライアントコードにアクセスできない場合、またはすべてのファイルが小さい場合-多数のテキストファイル。

明らかに、JNIのパフォーマンスとJNAの利便性を組み合わせたライブラリが表示されるはずでした。



Jnr



これが、JNR(Java Native Runtime)の出番です。 JNRはJNAと同様にlibffiに基づいていますが、リフレクションの代わりにバイトコード生成が使用されるため、パフォーマンスが大幅に向上します。

JNRに関する多くの情報はありません。最も詳細なものは、JVMLS 2013でのCharles Nutterのプレゼンテーションですプレゼンテーション )。 ただし、JNRはすでにかなり大きなエコシステムであり、JRubyで積極的に使用されています。 unix-sockets、posix-apiなどのパーツの多くは、サードパーティのプロジェクトでも積極的に使用されています。







そのJNRは、Java 10を対象とするJEP 191 -Foreign Function Interfaceの開発の基礎です。

JNAとは異なり、JNRにはドキュメントがありません。質問に対するすべての回答はソースコードで見つける必要があり、これが小さなガイドを書く主な理由でした。



Java Native Runtimeのコード作成機能



関数バインディング



単純なlibcバインディングは次のようになります。

import jnr.ffi.*; import jnr.ffi.types.pid_t; /** * Gets the process ID of the current process, and that of its parent. */ public class Getpid { public interface LibC { public @pid_t long getpid(); public @pid_t long getppid(); } public static void main(String[] args) { LibC libc = LibraryLoader.create(LibC.class).load("c"); System.out.println("pid=" + libc.getpid() + " parent pid=" + libc.getppid()); } }
      
      





LibraryLoaderを使用して、転送されたインターフェイスに対応するライブラリを名前でロードします。



FUSEの場合、すべてのコールバックを含むFuseOperations構造が渡されるfuse_main_realメソッドとのインターフェースが必要です。

 public interface LibFuse { int fuse_main_real(int argc, String argv[], FuseOperations op, int op_size, Pointer user_data); }
      
      







構造体の実装



多くの場合、特定のアドレスにある構造(fuse_bufvec構造など)を使用する必要があります。

 struct fuse_bufvec { size_t count; size_t idx; size_t off; struct fuse_buf buf[1]; };
      
      





JNRで実装するには、jnr.ffi.Structから継承する必要があります。

 import jnr.ffi.*; public class FuseBufvec extends Struct { public FuseBufvec(jnr.ffi.Runtime runtime) { super(runtime); } public final size_t count = new size_t(); public final size_t idx = new size_t(); public final size_t off = new size_t(); public final FuseBuf buf = inner(new FuseBuf(getRuntime())); }
      
      





各構造内には、メモリに配置されるポインタが格納されます。 構造体を操作するためのAPIのほとんどは、Structの静的メソッドを見るとわかります。

size_tはStructの内部クラスであり、それを作成すると、各フィールドについて、メモリ内のこのフィールドのオフセットが記憶されます。これにより、各フィールドはメモリ内のオフセットによって認識されます。 多くの内部クラス(Signed64、Unsigned32、time_tなど)が既に実装されているため、いつでも独自のクラスを実装できます。



コールバック



 struct fuse_operations { int (*getattr) (const char *, struct stat *); }
      
      





国鉄でコールバックを操作するための注釈があります
  @デリゲート 


 public interface GetAttrCallback { @Delegate int getattr(String path, Pointer stbuf); } public class FuseOperations extends Struct { public FuseOperations(Runtime runtime) { super(runtime); } public final Func<GetAttrCallback> getattr = func(GetAttrCallback.class); }
      
      





次に、たとえばgetattrフィールドに目的のコールバック実装を設定できます。



 fuseOperations.getattr.set((path, stbuf) -> 0);
      
      







列挙型



いくつかの非自明なことのうち、列挙型のラッパーに注目する価値もあります。これは、jnr.ffi.util.EnumMapper.IntegerEnumから列挙型を継承し、intValueメソッドを実装する必要があるためです。

 enum fuse_buf_flags { FUSE_BUF_IS_FD = (1 << 1), FUSE_BUF_FD_SEEK = (1 << 2), FUSE_BUF_FD_RETRY = (1 << 3), };
      
      





 public enum FuseBufFlags implements EnumMapper.IntegerEnum { FUSE_BUF_IS_FD(1 << 1), FUSE_BUF_FD_SEEK(1 << 2), FUSE_BUF_FD_RETRY(1 << 3); private final int value; FuseBufFlags(int value) { this.value = value; } @Override public int intValue() { return value; } }
      
      







メモリを操作する





この知識は、単純なクロスプラットフォームラッパーをネイティブライブラリに簡単に実装するのに十分です。



jnr-fuse



これはまさにjnr-fuseプロジェクトでFUSEを使用して行ったことです。 最初はfuse-jnaライブラリが使用されていましたが、FSの実装ではボットネットでした。 APIを開発するとき、fuse-jnaとネイティブ実装(<fuse.h>)との互換性を可能な限り維持しようとしました。



ユーザー空間にファイルシステムを実装するには、ru.serce.jnrfuse.FuseStubFSを継承し、必要なメソッドを実装する必要があります。 Fuse_operationsには多くのメソッドが含まれていますが、機能するFSを取得するには、いくつかの基本的なメソッドを実装するだけで十分です。

これは非常に簡単です。FSの動作例をいくつか示します。



Linuxは現在サポートされています(x86およびx64)。



ライブラリはjcenterにあり、近い将来、Maven Centralにミラーを追加します。



グラドル


 repositories { jcenter() } dependencies { compile 'com.github.serceman:jnr-fuse:0.1' }
      
      





メイヴン


  <repositories> <repository> <id>central</id> <name>bintray</name> <url>http://jcenter.bintray.com</url> </repository> </repositories> <dependencies> <dependency> <groupId>com.github.serceman</groupId> <artifactId>jnr-fuse</artifactId> <version>0.1</version> </dependency> </dependencies>
      
      







fuse-jnaとjnr-fuseのパフォーマンスを比較する



私の場合、FSは読み取り専用であり、特にスループットに興味がありました。 パフォーマンスはFSの実装に大きく依存するため、突然fuse-jnaを使用する場合、jnr-fuseを簡単に接続し、負荷プロファイルに基づいてテストを作成し、違いを確認できます。 (とにかくこのテストはあなたにとって有用です、私たちは皆、パフォーマンスのために運転するのが大好きですか?)



違いの順序を示すために、最小限の変更でMemoryFSの実装をfuse-jnaからfuse-jnrに移行し、fio読み取りテストを実行しました。 テストには、 fioフレームワークを使用しました。これについては、少し前にハブに関する良い記事がありました。



テスト構成
[readtest]

ブロックサイズ= 4k

ディレクトリ= / tmp / mnt /

rw = randread

ダイレクト= 1

バッファリング= 0

ioengine = libaio

time_based = 60

サイズ= 16M

ランタイム= 60



結果fuse-jna
serce @ SerCe-FastLinux:〜/ git / jnr-fuse / bench $ fio read.ini

readtest:(g = 0):rw = randread、bs = 4K-4K / 4K-4K / 4K-4K、ioengine = libaio、iodepth = 1

fio-2.1.3

1つのプロセスを開始

readtest:IOファイルのレイアウト(s)(1ファイル(s)/ 16MB)

ジョブ:1(f = 1):[r] [100.0%完了] [24492KB / 0KB / 0KB / s] [6123/0/0 iops] [eta 00m:00s]

readtest:(groupid = 0、jobs = 1):err = 0:pid = 10442:Sun Jun 21 14:49:13 2015

読み取り:io = 1580.2MB、bw = 26967KB / s、iops = 6741、runt = 60000msec

スラット(usec):最小= 46、最大= 29997、平均= 146.55、stdev = 327.68

clat(usec):最小= 0、最大= 69、平均= 0.47、標準偏差= 0.66

lat(usec):最小= 47、最大= 30002、平均= 147.26、stdev = 327.88

clatパーセンタイル(usec):

| 1.00th = [0]、5.00th = [0]、10.00th = [0]、20.00th = [0]、

| 30.00th = [0]、40.00th = [0]、50.00th = [0]、60.00th = [1]、

| 70.00th = [1]、80.00th = [1]、90.00th = [1]、95.00th = [1]、

| 99.00番目= [2]、99.50番目= [2]、99.90番目= [3]、99.95番目= [12]、

| 99.99 = [14]

bw(KB / s):最小= 17680、最大= 32606、per = 96.09%、平均= 25913.26、stdev = 3156.20

lat(usec):2 = 97.95%、4 = 1.96%、10 = 0.02%、20 = 0.06%、50 = 0.01%

lat(usec):100 = 0.01%

cpu:usr = 1.98%、sys = 5.94%、ctx = 405302、majf = 0、minf = 28

IO深度:1 = 100.0%、2 = 0.0%、4 = 0.0%、8 = 0.0%、16 = 0.0%、32 = 0.0%、> = 64 = 0.0%

送信:0 = 0.0%、4 = 100.0%、8 = 0.0%、16 = 0.0%、32 = 0.0%、64 = 0.0%、> = 64 = 0.0%

完了:0 = 0.0%、4 = 100.0%、8 = 0.0%、16 = 0.0%、32 = 0.0%、64 = 0.0%、> = 64 = 0.0%

発行済み:合計= r = 404511 / w = 0 / d = 0、short = r = 0 / w = 0 / d = 0



実行ステータスグループ0(すべてのジョブ):

読み取り:io = 1580.2MB、aggrb = 26967KB / s、minb = 26967KB / s、maxb = 26967KB / s、mint = 60000msec、maxt = 60000msec



JNRヒューズの結果
serce @ SerCe-FastLinux:〜/ git / jnr-fuse / bench $ fio read.ini

readtest:(g = 0):rw = randread、bs = 4K-4K / 4K-4K / 4K-4K、ioengine = libaio、iodepth = 1

fio-2.1.3

1つのプロセスを開始

readtest:IOファイルのレイアウト(s)(1ファイル(s)/ 16MB)

ジョブ:1(f = 1):[r] [100.0%完了] [208.5MB / 0KB / 0KB / s] [53.4K / 0/0 iops] [eta 00m:00s]

readtest:(groupid = 0、jobs = 1):err = 0:pid = 10153:Sun Jun 21 14:45:17 2015

読み取り:io = 13826MB、bw = 235955KB / s、iops = 58988、runt = 60002msec

スラット(usec):最小= 6、最大= 23671、平均= 15.80、標準偏差= 19.97

clat(usec):最小= 0、最大= 1028、平均= 0.37、標準偏差= 0.78

lat(usec):最小= 7、最大= 23688、平均= 16.29、stdev = 20.03

clatパーセンタイル(usec):

| 1.00th = [0]、5.00th = [0]、10.00th = [0]、20.00th = [0]、

| 30.00th = [0]、40.00th = [0]、50.00th = [0]、60.00th = [0]、

| 70.00th = [1]、80.00th = [1]、90.00th = [1]、95.00th = [1]、

| 99.00th = [1]、99.50th = [1]、99.90th = [2]、99.95th = [2]、

| 99.99 = [10]

lat(usec):2 = 99.88%、4 = 0.10%、10 = 0.01%、20 = 0.01%、50 = 0.01%

lat(usec):100 = 0.01%、250 = 0.01%

lat(ミリ秒):2 = 0.01%

cpu:usr = 9.33%、sys = 34.01%、ctx = 3543137、majf = 0、minf = 28

IO深度:1 = 100.0%、2 = 0.0%、4 = 0.0%、8 = 0.0%、16 = 0.0%、32 = 0.0%、> = 64 = 0.0%

送信:0 = 0.0%、4 = 100.0%、8 = 0.0%、16 = 0.0%、32 = 0.0%、64 = 0.0%、> = 64 = 0.0%

完了:0 = 0.0%、4 = 100.0%、8 = 0.0%、16 = 0.0%、32 = 0.0%、64 = 0.0%、> = 64 = 0.0%

発行済み:合計= r = 3539449 / w = 0 / d = 0、short = r = 0 / w = 0 / d = 0



実行ステータスグループ0(すべてのジョブ):

読み取り:io = 13826MB、aggrb = 235955KB / s、minb = 235955KB / s、maxb = 235955KB / s、mint = 60002msec、maxt = 60002msec







このテストでは、fuse-jnaとfuse-jnrのファイルの読み取り速度の違いのみを示していますが、それに基づいて、JNAとJNRの速度の違いを把握できます。 希望する人は、すべての機能を考慮して、 JMHを使用してネイティブコールのより詳細なテストをいつでも作成できます。私自身は、これらのテストに興味があります。



Charles Nutterによるプレゼンテーションのように、JNRとJNAのスループットとレイテンシの両方の差は、約10倍になると予想されます。



参照資料





jnr-fuseプロジェクトはGitHubホストされています 。 プロジェクトを改善するための星、プールのリクエスト、提案に満足しています。

また、JNRとjnr-fuseに関するすべての質問にお答えします。



All Articles