Go言語実戦スパースファイル解凍、4倍向上、複数スキル!!

上の青い「Flying Snow Ruthless」をクリックして公式アカウントをフォローし、スターを付けて、できるだけ早く記事を読んでください

しばらく前に、私は Golang を使って tar ファイルを解凍するための小さなツールを作成しました。速度は Linux に付属の tar よりもはるかに速く (4 倍か 5 倍の差)、スパース ファイルもサポートしています。スパースについての具体的な説明はファイルについては、私の記事 「 Golang とスパース ファイル 」を参照してください。

エネルギーが前途多量で、少し退屈ですが、何かを学ぶことができます。

  1. tar 標準パッケージはスパース ファイルをサポートしていません。自分でサポートする方法

  2. プライベートメソッドを呼び出す方法

  3. 解凍パフォーマンスを向上させる方法

golang tarの解凍

まず、tar ファイル解凍の簡単な例を通して、その実装を段階的に分析し、golang の tar パッケージがスパース ファイルをサポートしない理由を確認します。

カレントフォルダに解凍する場合 demo.tar を例に、カレントディレクトリに解凍する場合のGolangの実装コードは以下の通りです。

func main() {
   unTarDir := "." //解压到当前目录
   tarFile, err := os.Open("demo.tar")
   if err != nil {
      log.Fatalln(err)
   }
   tr := tar.NewReader(tarFile)
   for {
      hdr, err := tr.Next()
      if err == io.EOF {
         break // End of archive
      }
      if err != nil {
         log.Fatalln(err)
      }
      if hdr.Typeflag == tar.TypeDir {
         // 创建解压目录
      } else if hdr.Typeflag == tar.TypeReg || hdr.Typeflag == tar.TypeGNUSparse {
         tarFile := path.Join(unTarDir, hdr.Name)
          file, err := os.OpenFile(tarFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.FileMode(hdr.Mode))
          if err != nil {
             log.Fatalln(err)
          }
          err = file.Truncate(hdr.Size)
          if err != nil {
             log.Fatalln(err)
          }
         _, err = io.Copy(file, tr)
         if err != nil {
            log.Fatalln(err)
         }
      }
   }
}

上記のコードは、通常のファイルとスパース ファイルを同じ方法で処理します。つまり、 io.Copy 関数を使用してコンテンツをファイルに書き込みます。

func Copy(dst Writer, src Reader) (written int64, err error) {
   return copyBuffer(dst, src, nil)
}

Copy 関数の実装規則は次のとおりです。

  1. dst インターフェースが実装されている 場合は io.WriterTo 、 dst.WriterTo メソッドを直接使用できます。

  2. 最初の点が満たされない場合は、 src インターフェイスが実装されているかどうかを 判断しio.ReadFrom 、実装されている場合はそれを直接使用します

  3. 最後に、満足できない場合は、メソッドを呼び出し src 、 Read そのメソッドを使用して dst コンテンツ Write を書き込みます。

Tar Reader のソースコード解析

具体的に は、 インターフェイスを tar.Reader 実装していないため 、 最終的にそのメソッドが呼び出されます。io.WriterToRead

// If the current file is sparse, then the regions marked as a hole
// are read back as NUL-bytes.
func (tr *Reader) Read(b []byte) (int, error) {
   if tr.err != nil {
      return 0, tr.err
   }
   n, err := tr.curr.Read(b)
   if err != nil && err != io.EOF {
      tr.err = err
   }
   return n, err
}

コメントによると、golang の tar パッケージはスパース ファイルをサポートしており、ホール領域を値 0 のバイトで埋めます。これは、go tar パッケージがスパース ファイルのシナリオを考慮していることを示しています。

func (sr *sparseFileReader) Read(b []byte) (n int, err error) {
   // 省略无关代码
   for endPos > sr.pos && err == nil {
      var nf int // Bytes read in fragment
      holeStart, holeEnd := sr.sp[0].Offset, sr.sp[0].endOffset()
      if sr.pos < holeStart { // In a data fragment
         bf := b[:min(int64(len(b)), holeStart-sr.pos)]
         nf, err = tryReadFull(sr.fr, bf)
      } else { // In a hole fragment
         bf := b[:min(int64(len(b)), holeEnd-sr.pos)]
         nf, err = tryReadFull(zeroReader{}, bf)
      }
      b = b[nf:]
      sr.pos += int64(nf)
      if sr.pos >= holeEnd && len(sr.sp) > 1 {
         sr.sp = sr.sp[1:] // Ensure last fragment always remains
      }
   }
   // 省略无关代码
}

上記のソース コードでは zeroReader 、これは 0 バイトのフィラーであり、すべて 0 のバイト スライスが生成されることに注意してください。Read ファイルに0値のデータを書き込むことでスパースファイルを実現する方法であることがわかります 。

tar パッケージはスパース ファイルをサポートしていません

残念ながら、実際のテストの結果ではスパース ファイル機能をサポートできず、ディスク使用量はファイル サイズと同じままです。

最初は MacOS ファイル システムの問題かと思いましたが、私のシステムは APFS であり、スパース ファイルもサポートしています。

そこで、Ext4 と XFS システムを再度試してみましたが、このバイト 0 の書き込み方法はスパース機能をサポートしていません。

また、この方法には、0のデータもファイルに書き込む必要があり、ファイルの書き込みにはディスクIOが発生するため、パフォーマンスの無駄が発生するという欠点もあります。次に、パディング 0 を使用する時間のかかる方法を見てみましょう。

➜  time go run cmd/main.go
go run cmd/main.go  1.13s user 7.19s system 54% cpu 15.317 total

上記は 1.43GB の tar ファイルを使用したテスト データです。所要時間は約 15 秒です。
現在、デッドロックに陥っていますが、実際にスパース ファイルに関する前の記事でテストしました。  Golang とスパース ファイル 。私のコンピュータはスパース ファイルをサポートしています。唯一の違いは、 File.Seek その時に使用される方法です。tar の解凍では、空白バイト 0 の方法が使用されます。

先ほども書きましたが、パフォーマンスが悪いのは毎回0を書き込む必要があるためですが、この部分のデータが0であることがわかっている場合、 File.Seek スキップを使用するとIOが削減できるでしょうか?スパースファイルの問題も解決するかもしれません。

プライベート メソッド writeTo

素晴らしいアイデアだし、実際に実行可能だ。この考え方に沿って考えていきますが File.Seek 、0 のデータをスキップするメソッドを 使用したい場合はfile 、変数が主役となるため、それを Seek 呼び出す方法を見つける必要があります。

_, err = io.Copy(file, tr)

file これがファイルに データを書き込む方法であることがわかりました 。Copy メソッドを分析したところ、  インターフェイスをdst 実装するかどうかが 優先されます。実装されている場合は、そのインターフェイスを使用してファイルにデータを書き込むことが優先されます。io.WriterTo

// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
   return wt.WriteTo(dst)
}

しかし、残念ながら、 インターフェースtar.Reader が実装されていないio.WriterTo ため、 io.Copy このパスは機能しません。 しかし、これは私にアイデアを与えました.ソース コードを 
調べて、プライベート メソッドを見つけました . そのコメントから判断すると、それはスパース ファイルもサポートしており、  ホール データをスキップするメソッドを使用しています。具体的な実装は、内部構造  の メソッドを通じて行われます 。詳細については、以下のコードのコメントを参照してください。tar.ReaderwriteToSeeksparseFileReaderWriteTo

// If the current file is sparse and w is an io.WriteSeeker,
// then writeTo uses Seek to skip past holes defined in Header.SparseHoles,
// assuming that skipped regions are filled with NULs.
func (tr *Reader) writeTo(w io.Writer) (int64, error) {
   if tr.err != nil {
      return 0, tr.err
   }
   n, err := tr.curr.WriteTo(w)
   if err != nil {
      tr.err = err
   }
   return n, err
}

func (sr *sparseFileReader) WriteTo(w io.Writer) (n int64, err error) {
   // 关键,先判断是否实现了io.WriteSeeker,实现了才有Seek方法
   ws, ok := w.(io.WriteSeeker)
   if ok {
      if _, err := ws.Seek(0, io.SeekCurrent); err != nil {
         ok = false // Not all io.Seeker can really seek
      }
   }
   // 如果不支持Seek,又回到了Copy方法
   if !ok {
      return io.Copy(w, struct{ io.Reader }{sr})
   }
   var writeLastByte bool
   pos0 := sr.pos
   for sr.logicalRemaining() > 0 && !writeLastByte && err == nil {
      var nf int64 // Size of fragment
      holeStart, holeEnd := sr.sp[0].Offset, sr.sp[0].endOffset()
      if sr.pos < holeStart { // In a data fragment
         nf = holeStart - sr.pos
         nf, err = io.CopyN(ws, sr.fr, nf)
      } else { // In a hole fragment
         nf = holeEnd - sr.pos
         if sr.physicalRemaining() == 0 {
            writeLastByte = true
            nf--
         }
         // 关键,使用Seek方法跳过
         _, err = ws.Seek(nf, io.SeekCurrent)
      }
      sr.pos += nf
      if sr.pos >= holeEnd && len(sr.sp) > 1 {
         sr.sp = sr.sp[1:] // Ensure last fragment always remains
      }
   }
}

意外なことに、これは私たちが望んでいる方法ですが、非公開です、どのように使用するのですか?

go:リンク名が表示されます

プライベートメソッドを呼び出すには、最初のアイデアの反映ですか? しかし、Golang には、より優れた呼び出し方法があります。それは go:linkname コンパイル命令です。これも golang の隠れたブラック テクノロジであり、独自の標準ライブラリでも使用されています。

プライベート メソッドを使用する場合は、まず、関数本体を使用せずに、独自のソース コード ファイル内で同じシグネチャを持つ関数を定義します。

func writeTo(tr *tar.Reader, w io.Writer) (int64, error)

関数を定義しているので、最初のパラメータは tar.Reader それ自体であり、この定義方法によって、  tar.Reader yes の方法を示します。
次に、 go:linkname を使用して、定義した関数の上にこの命令の説明を追加します。

//go:linkname writeTo archive/tar.(*Reader).writeTo
func writeTo(tr *tar.Reader, w io.Writer) (int64, error)

これは、スペースで区切られた 2 つの部分を指します。

  1. 最初の部分は、 writeTo 現在のソース ファイル内の writeTo 関数を表します。

  2. 2 番目の部分 でarchive/tar.(*Reader).writeTo 表されるtar.Reader プライベート メソッドは2 番目の部分に注目し、 インポートのフル パス、  構造、最後にプライベート メソッドをwriteTo
    完全に記述する必要があります。  どちらも必須であり、間違って記述することはできません。import pathReaderwriteTo

このアプローチは標準ではないため、 unsafe パッケージもインポートして、安全でないことがすでにわかっていることをコンパイラーに伝えます。

import (
   "archive/tar"
   _ "unsafe"
)

さて、準備はすべて完了したので、いよいよ使用します。これをサンプル コードの 関数にio.Copy 置き換える だけですwriteTo 。

tarFile := path.Join(unTarDir, hdr.Name)
file, err := os.OpenFile(tarFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.FileMode(hdr.Mode))
if err != nil {
   log.Fatalln(err)
}
err = file.Truncate(hdr.Size)
if err != nil {
   log.Fatalln(err)
}
_, err = writeTo(tr, file)
if err != nil {
   log.Fatalln(err)
}

ここでテストして、パフォーマンスがどの程度向上したかを確認してみましょう。

➜  time go run cmd/main.go
go run cmd/main.go  0.55s user 3.65s system 86% cpu 4.844 total

所要時間が元の 15 秒から 5 秒に短縮され、パフォーマンスが向上しました。

Seek この方法を改善すると、より多くの穴を持つスパース ファイルの場合、それらがすべてスキップされるため、効果が向上します。

要点を描くと、ようやくスパースファイルの解凍が実現しました(APFS、XFS、Ext4はすべてサポートされています) duコマンドで見ると、実際のファイルサイズよりもディスク使用量がはるかに小さくなっています

## 磁盘占用大小1.3G
$ du -h RAM 
1.3G    spare_file

## 文件实际大小12G
$ ls -lh RAM 
12G     spare_file

コードをリファクタリングする

さて、コードをリファクタリングして通常のファイルとスパース ファイルを区別し、それらを別々に処理しましょう。元の通常ファイルは引き続き io.Copy 関数を通じて実装されます。

func writeFile(root string, hdr *tar.Header, tr *tar.Reader, sparseFile bool) {
   tarFile := path.Join(root, hdr.Name)
   file, err := os.OpenFile(tarFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.FileMode(hdr.Mode))
   if err != nil {
      log.Fatalln(err)
   }
   err = file.Truncate(hdr.Size)
   if err != nil {
      log.Fatalln(err)
   }
   if sparseFile {
      _, err = writeTo(tr, file)
   } else {
      _, err = io.Copy(file, tr)
   }
   if err != nil {
      log.Fatalln(err)
   }
}

共通関数を抽出し 、writeFile スパースファイル用 writeTo 関数 を使用し、io.Copy 通常ファイル用関数を使用し、解凍時に使用します。

else if hdr.Typeflag == tar.TypeReg {
   writeFile(unTarDir, hdr, tr, false)
} else if hdr.Typeflag == tar.TypeGNUSparse {
   writeFile(unTarDir, hdr, tr, true)
}

まとめ

この時点で、実戦はすべて終了です。このプロセスから判断すると、この実戦を完了するには、まず諦めないでください。初めて使用するときに、スパース ファイルがサポートされていないことがわかっても、あきらめてはいけません。 tar パッケージ. 諦めてしまったら素晴らしいものは何も残らないでしょう。

次に、ソース コードとスパース ファイルの原理を深く理解することでのみ、より良い方法を見つけ、最適なパフォーマンスを達成することができます。

この記事はオリジナル記事です。転載して出典を明記してください。QR コードをスキャンして公式アカウントflysnow_orgまたはウェブサイト https://www.flysnow.org/ をフォローし、できるだけ早くフォローアップの刺激的な記事をお読みください。良いと思っていただけましたら、記事右下の「探す」をクリックしていただき、応援をよろしくお願いいたします。

b13c3da862ca0f7bc9f1baf12d1d89e5.png

コードをスキャンしてフォローしてください

シェア、いいね、視聴が最大のサポートです

おすすめ

転載: blog.csdn.net/flysnow_org/article/details/126615709