上の青い「Flying Snow Ruthless」をクリックして公式アカウントをフォローし、スターを付けて、できるだけ早く記事を読んでください
しばらく前に、私は Golang を使って tar ファイルを解凍するための小さなツールを作成しました。速度は Linux に付属の tar よりもはるかに速く (4 倍か 5 倍の差)、スパース ファイルもサポートしています。スパースについての具体的な説明はファイルについては、私の記事 「 Golang とスパース ファイル 」を参照してください。
エネルギーが前途多量で、少し退屈ですが、何かを学ぶことができます。
tar 標準パッケージはスパース ファイルをサポートしていません。自分でサポートする方法
プライベートメソッドを呼び出す方法
解凍パフォーマンスを向上させる方法
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
関数の実装規則は次のとおりです。
dst
インターフェースが実装されている 場合はio.WriterTo
、dst.WriterTo
メソッドを直接使用できます。最初の点が満たされない場合は、
src
インターフェイスが実装されているかどうかを 判断しio.ReadFrom
、実装されている場合はそれを直接使用します最後に、満足できない場合は、メソッドを呼び出し
src
、Read
そのメソッドを使用してdst
コンテンツWrite
を書き込みます。
Tar Reader のソースコード解析
具体的に は、 インターフェイスを tar.Reader
実装していないため 、 最終的にそのメソッドが呼び出されます。io.WriterTo
Read
// 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.Reader
writeTo
Seek
sparseFileReader
WriteTo
// 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 つの部分を指します。
最初の部分は、
writeTo
現在のソース ファイル内のwriteTo
関数を表します。2 番目の部分 で
archive/tar.(*Reader).writeTo
表されるtar.Reader
プライベート メソッドは2 番目の部分に注目し、 インポートのフル パス、 構造、最後にプライベート メソッドをwriteTo
完全に記述する必要があります。 どちらも必須であり、間違って記述することはできません。import path
Reader
writeTo
このアプローチは標準ではないため、 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/ をフォローし、できるだけ早くフォローアップの刺激的な記事をお読みください。良いと思っていただけましたら、記事右下の「探す」をクリックしていただき、応援をよろしくお願いいたします。
コードをスキャンしてフォローしてください
シェア、いいね、視聴が最大のサポートです