¡Ir a la descompresión de archivos dispersos de combate real del idioma, mejora 4 veces, habilidades múltiples! !

Haga clic en el azul "Flying Snow Ruthless" arriba para seguir mi cuenta oficial, establecer una estrella y leer el artículo lo antes posible.

Hace algún tiempo, usé Golang para escribir una pequeña herramienta para descomprimir archivos tar. La velocidad es mucho más rápida que el tar que viene con Linux (4 o 5 veces la diferencia), y también admite archivos dispersos. Para obtener explicaciones específicas sobre dispersos archivos, puede consultar mi artículo  Golang y Sparse Files .

Mucha energía por delante, un poco aburrido, pero puedo aprender algo:

  1. El paquete tar estándar no admite archivos dispersos, cómo hacerlo usted mismo

  2. Cómo llamar a métodos privados

  3. Cómo mejorar el rendimiento de descompresión

descompresión de alquitrán de golang

En primer lugar, a través de un ejemplo simple de descompresión de archivos tar, analizo su implementación paso a paso para ver por qué el paquete tar de golang no admite archivos dispersos.

Tome la descompresión en la carpeta actual  demo.tar como ejemplo y descomprímala en el directorio actual. El código de implementación de Golang es el siguiente:

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)
         }
      }
   }
}

El código anterior maneja archivos regulares y archivos dispersos de la misma manera, es decir, usa  io.Copy funciones para escribir el contenido en el archivo.

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

Copy Las reglas de implementación de la función son las siguientes:

  1. Si  dst se implementa  la interfaz io.WriterTo , entonces  dst.WriterTo el método se puede usar directamente

  2. Si el primer punto no se cumple, entonces juzgue  src si la interfaz está implementada  io.ReadFrom y, de ser así, utilícela directamente.

  3. Finalmente, si no está satisfecho, llame  src al  Read método y luego use  dst el  Write método para escribir el contenido

Análisis del código fuente de Tar Reader

Específicamente  tar.Reader , debido a que no implementa  io.WriterTo la interfaz,  Read finalmente se llama a su método.

// 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
}

Según los comentarios, el paquete tar de golang admite archivos dispersos y llenará el área del agujero con bytes con un valor de 0, lo que indica que el paquete tar de go considera el escenario de archivos dispersos.

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
      }
   }
   // 省略无关代码
}

Tenga en cuenta que en el código fuente anterior  zeroReader , este es un relleno con 0 bytes, que generará un segmento de bytes con todos los 0. Se puede ver  Read que el método consiste en realizar archivos dispersos escribiendo datos de valor 0 en el archivo.

El paquete tar no admite archivos dispersos

Desafortunadamente, los resultados de mi prueba real no son compatibles con la función de archivo disperso y el uso del disco sigue siendo el mismo que el tamaño del archivo.

Al principio, pensé que era un problema con mi sistema de archivos MacOS, mi propio sistema es APFS, que también admite archivos dispersos.

Así que probé de nuevo los sistemas Ext4 y XFS.Este método de escribir el byte 0 no es compatible con la característica escasa.

Además, este método también tiene una desventaja, es decir, los datos de 0 también deben escribirse en el archivo, y escribir el archivo implicará E/S de disco, lo que provocará un desperdicio de rendimiento. Ahora mire el método lento de usar padding 0:

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

Lo anterior son los datos de prueba de un archivo tar de 1,43 GB utilizado, que tarda unos 15 segundos.
Ahora estoy en un punto muerto, pero en realidad lo probé en el artículo anterior sobre archivos dispersos  Golang y archivos dispersos  . Mi computadora admite archivos dispersos. La única diferencia es  File.Seek el método utilizado en ese momento. La descompresión Tar utiliza el método de byte 0 en blanco.

Como se mencionó hace un momento, el rendimiento deficiente se debe a que siempre se debe escribir 0. Si sabemos que esta parte de los datos es 0, ¿usar  File.Seek skip puede reducir el IO? También podría resolver el problema del archivo disperso.

método privado writeTo

Buena idea, y en realidad es factible. Sigamos pensando en esta línea de pensamiento: si desea utilizar  File.Seek el método para omitir los datos que son 0, entonces  file la variable juega un papel principal y debe encontrar una manera de permitir que se  Seek llame.

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

Resulta que así es como escribimos datos  file en el archivo.  Copy Hemos analizado el método y priorizará  dst si implementar  io.WriterTo la interfaz. Si es así, priorizará usarla para escribir datos en el archivo.

// 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)
}

Pero, lamentablemente,  la interfaz tar.Reader no se ha implementado io.WriterTo , por lo que  io.Copy esta ruta no funcionará.
Pero esto me dio una idea. Revisé tar.Reader el código fuente y encontré un método privado  writeTo . A juzgar por sus comentarios, también admite archivos dispersos y usa  Seek el método para omitir datos de agujeros. sparseFileReader La implementación específica es a través del método de  la estructura interna  WriteTo , consulte los comentarios del código a continuación para obtener más detalles.

// 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
      }
   }
}

Sorprendentemente, este es el método que queremos, pero es privado, ¿cómo usarlo?

ir: aparece el nombre del enlace

Para llamar a un método privado, ¿la primera idea es un reflejo? Pero en Golang, hay un mejor método de llamada, que es la instrucción de compilación go:linkname, que también es una tecnología negra oculta de golang, y también se usa en su propia biblioteca estándar.

Si desea utilizar un método privado, primero defina una función con la misma firma en su propio archivo de código fuente, ¡sin un cuerpo de función! ! !

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

Debido a que estamos definiendo una función, el primer parámetro es  tar.Reader él mismo, a través de este método de definición, indica  tar.Reader el método de sí.
Luego es el uso de go:linkname, agregando la descripción de esta instrucción arriba de la función que definimos

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

Esto se refiere a dos partes, separadas por espacios:

  1. La primera parte  representa  la función writeTo en nuestro archivo fuente actual writeTo

  2.  El método privado archive/tar.(*Reader).writeTo representado por la segunda parte  presta atención a la segunda parte, se debe escribir completo   , la ruta completa de la importación, luego   la estructura y finalmente  el método privado, ambos son indispensables y no se pueden escribir mal.tar.ReaderwriteTo
    import pathReaderwriteTo

Debido a que este enfoque no es estándar, también importamos  unsafe el paquete y le indicamos al compilador que ya sabemos que no es seguro.

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

Bueno, todos los preparativos están hechos, y ahora es el momento de usarlo. io.Copy Simplemente reemplácelo  con una función en nuestro código de muestra  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)
}

Ahora probemos y veamos cuánto ha mejorado el rendimiento:

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

El consumo de tiempo se ha reducido de los 15 segundos originales a 5 segundos, lo que es una mejora de rendimiento, ¡justo ahora! ! !

Seek La mejora del método, para los archivos dispersos con más agujeros, mejor es el efecto, porque se saltan todos.

La clave es dibujar el punto clave. Finalmente nos dimos cuenta de la descompresión de archivos dispersos (APFS, XFS y Ext4 son compatibles). Visto a través del comando du, el uso del disco es mucho menor que el tamaño real del archivo .

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

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

Refactorizar el código

Ok, ahora vamos a refactorizar el código para distinguir entre archivos regulares y archivos dispersos, y luego tratarlos de manera diferente. El archivo regular original todavía se  io.Copy implementa a través de funciones.

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)
   }
}

Extraiga una función común  , use  funciones  writeFile para archivos dispersos  y use  funciones para archivos normales, y utilícelas al descomprimir.writeToio.Copy

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

resumen

En este punto, todo el combate real ha terminado. A juzgar por este proceso, si desea completar este combate real, primero no debe darse por vencido. No puede darse por vencido cuando descubre que los archivos dispersos no son compatibles cuando usa por primera vez el paquete de alquitrán Si te rindes, no quedará nada maravilloso.

En segundo lugar, tenemos una comprensión profunda de los principios del código fuente y los archivos dispersos. Solo de esta manera podemos encontrar mejores métodos y lograr un rendimiento óptimo.

Este artículo es un artículo original, reimprime e indica la fuente, bienvenido a escanear el código QR para seguir la cuenta oficial flysnow_orgo el sitio web https://www.flysnow.org/, y lee los emocionantes artículos de seguimiento lo antes posible. Si cree que es bueno, haga clic en "Buscar" en la esquina inferior derecha del artículo, gracias por su apoyo.

b13c3da862ca0f7bc9f1baf12d1d89e5.png

Escanear código de atención

Compartir, dar me gusta y mirar son el mayor apoyo.

Supongo que te gusta

Origin blog.csdn.net/flysnow_org/article/details/126615709
Recomendado
Clasificación