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:
El paquete tar estándar no admite archivos dispersos, cómo hacerlo usted mismo
Cómo llamar a métodos privados
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:
Si
dst
se implementa la interfazio.WriterTo
, entoncesdst.WriterTo
el método se puede usar directamenteSi el primer punto no se cumple, entonces juzgue
src
si la interfaz está implementadaio.ReadFrom
y, de ser así, utilícela directamente.Finalmente, si no está satisfecho, llame
src
alRead
método y luego usedst
elWrite
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:
La primera parte representa la función
writeTo
en nuestro archivo fuente actualwriteTo
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.Reader
writeTo
import path
Reader
writeTo
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.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)
}
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_org
o 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.
Escanear código de atención
Compartir, dar me gusta y mirar son el mayor apoyo.