Golang实现数据存储到归档文件的最佳实践

Golang实现数据存储到归档文件的最佳实践 大家好,我是Go开发的新手,今天有一个小问题想请教大家。

我需要开发一个小型包,能够将文件存储到归档文件中。一旦文件被写入归档,并且主程序关闭,下次运行程序时,我不仅需要向归档中添加新文件,还需要向归档中已有的文件追加新数据。

我首先想到的是使用“archive/zip”库,结果发现该库不支持向现有zip文件添加新文件,也不支持向zip内现有文件追加内容。甚至有一个关于此问题的GitHub issue,其中包含一些想法和建议(https://github.com/golang/go/issues/15626)。似乎tar和gzip也存在同样的情况。

问题是:是否有实现上述功能的最佳实践或推荐方法?我应该扩展“archive/zip”库来处理我需要的这些情况吗?

谢谢。


更多关于Golang实现数据存储到归档文件的最佳实践的实战教程也可以访问 https://www.itying.com/category-94-b0.html

10 回复

我需要澄清一下,这个更新只是一个追加操作。我不会更改旧数据,只会在末尾追加新数据。

更多关于Golang实现数据存储到归档文件的最佳实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


考虑到您提到的大文件问题,我建议采用类似logrotate的方法,而不是压缩文件。超过一定大小后,文件将变得无法使用。

文件大小可能会大幅增加并超过 5GB。它们主要是 JSON 编码的文件。

你是说你有一个 5GB 的 JSON 文件需要频繁更新吗?你的应用程序是做什么的?

感谢您的回答。我觉得我的方法可能完全错了,也许使用压缩存档才是正确的做法。但是,由于我会有很多不同的文件,我认为把所有内容放在一个“容器”里,而不是散落在文件夹中,可能会更好。

我正在做的是保存进入代理的HTTP请求和响应。我没有考虑使用SQLite,因为我并不真正需要关系型数据库,有人建议我考虑使用一个简单的JSON文件来追加记录请求和响应。问题是,这似乎很容易产生大文件。大约244条记录的历史就已经有几MB了。

感谢您的回答。我已经做了一些测试,本质上:

  1. 每个负载变化的平均和极端情况文件大小是多少? 文件大小可能会大幅增加,超过5GB。它们主要是JSON编码的文件。
  2. 您更改负载的频率如何? 非常频繁
  3. 您的客户如何通过您选择的压缩方式访问负载? 如果他们想自己解压存档,他们可以这样做,或者他们可以使用Go程序打开它。

将所有内容放在一个zip容器中似乎是一件非常好的事情,因为根据我的初步测试,压缩可以节省大量空间。

rhaidiz:

我最初的想法是使用“archive/zip”库,结果却发现该库不支持向现有zip文件添加新文件,也不支持向zip内的现有文件追加内容。

嗨,欢迎来到 Go 论坛 🚀 🎆


根据我对压缩归档机制流程的理解,每次修改/删除有效载荷后,为了获得有效的压缩效果,都需要重新压缩。

因此,这就是为什么追加/删除功能被认为是代价高昂的“锦上添花”特性。你可以做的是在标准库之上设计一个包装函数(如果你确实需要的话)。这个包装函数应遵循以下步骤:

  1. 解压缩归档文件。
  2. 更新有效载荷。
  3. 重新压缩有效载荷。

rhaidiz:

我觉得我的方法可能完全错了,也许压缩归档才是正确的处理方式。

这取决于需求以及负载数据的性质。如果未压缩文件的总量不大(小于5GB)且修改不频繁,你可以使用你的方法。

否则,你可能需要考虑其他方法,比如将归档分散到多个小归档中(重组)。这更多是关于策略规划。

归根结底是以下几点:

  1. 每次负载变化的文件平均大小和极端情况大小是多少?
  2. 你修改负载的频率有多高?
  3. 你的客户如何通过你选择的压缩方式来访问负载?

rhaidiz:

但由于我将会有许多不同的文件,我认为把所有东西放在一个“容器”里,而不是散落在文件夹中,可能会更好。

那是另一回事了。在Linux上(不确定Mac/Windows),我们可以将一个加密的存储驱动器挂载为一个“容器”,将所有内容存储在里面。使用这种方法只需要使用 os.exec 进行一次挂载。

这种方法提供了一种更简便的方式来修改负载,而无需归档到压缩文件中。即使有归档要求,你也可以随时轻松地归档那个已挂载的目录。

代价是:

  1. 不跨平台
  2. 需要你学习一些知识,比如RAID 1(可选)、cryptsetup和lvm。

我相信在Windows上已经有这样的解决方案了。(我已经超过5年不是Windows用户了,所以 :yum:)

  1. 你的客户如何通过你选择的压缩方式访问有效载荷? 如果他们想自己解压归档文件,他们可以这样做,或者他们可以用Go程序打开它。

你说你有5GB的JSON数据,并且经常更新吗?

我需要澄清一下,更新只是追加操作。我不更改旧数据,只追加新数据。

归根结底,这取决于你的客户如何在特定时间查询信息以更新他们的本地“数据库”。这是一个“数据库”和“设计”问题。

例如,对于像物联网传感器每秒转储这样的大型跟踪器数据,我会按(30分钟时间戳)链式归档来组织数据(类似于软件开发中的日志记录机制或git)。然后在客户端,有一个函数在每次接收一个归档文件后,简单地将这些归档文件重建为一个可缓存的数据库。

这样做有两个原因:

  1. 小型的归档文件通过网络传输和分发要容易得多。
  2. 客户端可以分阶段消费数据(例如,可以包含或不包含特定的更新)。

大型归档文件在某个时间点会被分割成多个小文件片段以进行有效的网络传输,因此这是冗余的工作。此外,如果你需要优化或保护“数据库”,使用消费级笔记本电脑会等待很长时间。

我没有考虑SQLite,因为我并不真正需要关系型数据库,有人建议我考虑使用一个简单的JSON文件,我可以在其中追加请求和响应。

如果你确实需要单个文件并且增长超过5GB(假设峰值达到5TB),我建议你使用某种事务型数据库,比如NoSQL数据库,例如:

如果你偏好:

  1. 无关系
  2. 基于文本的文件
  3. 事务
  4. 不希望用户安装这个或那个依赖项

最终,你将会面临查询问题,因此你可以考虑尽早选择数据库。

如果以上都不符合要求,有很多数据库供你考虑:

GitLab GitLab

cznic / ql · GitLab

ql包是一个纯Go语言编写的嵌入式SQL数据库。

GitHub GitHub

GitHub - avelino/awesome-go: 一个精心策划的Go框架、库和软件列表…

一个精心策划的Go框架、库和软件列表 - GitHub - avelino/awesome-go: 一个精心策划的Go框架、库和软件列表

对于你的需求,直接使用标准库的archive/zip确实无法实现向现有zip文件追加内容,因为zip格式的中央目录结构在文件末尾,修改时需要重写整个文件。不过,可以通过以下两种方式实现你的需求:

方案一:使用临时文件进行重建(推荐)

每次运行时读取现有归档,添加新文件/数据后写入新归档文件,最后替换原文件。这种方式兼容标准zip格式:

package main

import (
    "archive/zip"
    "bytes"
    "io"
    "os"
    "path/filepath"
)

func appendToZip(zipPath string, newFiles map[string][]byte) error {
    // 创建临时文件
    tmpFile, err := os.CreateTemp(filepath.Dir(zipPath), "temp-*.zip")
    if err != nil {
        return err
    }
    defer os.Remove(tmpFile.Name())
    defer tmpFile.Close()

    // 创建zip写入器
    zipWriter := zip.NewWriter(tmpFile)
    defer zipWriter.Close()

    // 1. 复制现有zip中的文件(如果存在)
    if _, err := os.Stat(zipPath); err == nil {
        existingZip, err := zip.OpenReader(zipPath)
        if err == nil {
            defer existingZip.Close()
            
            for _, file := range existingZip.File {
                rc, err := file.Open()
                if err != nil {
                    return err
                }
                
                // 创建zip文件头
                header := &zip.FileHeader{
                    Name:   file.Name,
                    Method: file.Method,
                }
                header.SetModTime(file.ModTime())
                
                writer, err := zipWriter.CreateHeader(header)
                if err != nil {
                    rc.Close()
                    return err
                }
                
                _, err = io.Copy(writer, rc)
                rc.Close()
                if err != nil {
                    return err
                }
            }
        }
    }

    // 2. 添加新文件或追加数据到现有文件
    for filename, content := range newFiles {
        // 检查是否已存在同名文件
        var existingContent []byte
        if _, err := os.Stat(zipPath); err == nil {
            existingZip, err := zip.OpenReader(zipPath)
            if err == nil {
                for _, file := range existingZip.File {
                    if file.Name == filename {
                        rc, err := file.Open()
                        if err == nil {
                            existingContent, _ = io.ReadAll(rc)
                            rc.Close()
                        }
                        break
                    }
                }
                existingZip.Close()
            }
        }

        // 合并旧数据和新数据
        combinedContent := append(existingContent, content...)
        
        // 创建新文件条目
        writer, err := zipWriter.Create(filename)
        if err != nil {
            return err
        }
        
        _, err = writer.Write(combinedContent)
        if err != nil {
            return err
        }
    }

    // 关闭zip写入器以确保数据写入
    if err := zipWriter.Close(); err != nil {
        return err
    }
    
    // 关闭临时文件
    if err := tmpFile.Close(); err != nil {
        return err
    }

    // 替换原文件
    return os.Rename(tmpFile.Name(), zipPath)
}

// 使用示例
func main() {
    newFiles := map[string][]byte{
        "log1.txt": []byte("New log entry\n"),
        "data.csv": []byte("2024-01-01,value1\n"),
    }
    
    err := appendToZip("archive.zip", newFiles)
    if err != nil {
        panic(err)
    }
}

方案二:使用支持追加的归档格式

考虑使用支持追加的文件格式,如tar配合gzip压缩。虽然标准库的archive/tar也不直接支持修改,但可以通过类似的重建方式实现:

package main

import (
    "archive/tar"
    "compress/gzip"
    "io"
    "os"
    "path/filepath"
)

func appendToTarGz(tarPath string, newFiles map[string][]byte) error {
    tmpFile, err := os.CreateTemp(filepath.Dir(tarPath), "temp-*.tar.gz")
    if err != nil {
        return err
    }
    defer os.Remove(tmpFile.Name())
    defer tmpFile.Close()

    gzWriter := gzip.NewWriter(tmpFile)
    tarWriter := tar.NewWriter(gzWriter)

    // 读取现有tar文件
    if _, err := os.Stat(tarPath); err == nil {
        file, err := os.Open(tarPath)
        if err == nil {
            defer file.Close()
            
            gzReader, err := gzip.NewReader(file)
            if err == nil {
                defer gzReader.Close()
                
                tarReader := tar.NewReader(gzReader)
                for {
                    header, err := tarReader.Next()
                    if err == io.EOF {
                        break
                    }
                    if err != nil {
                        return err
                    }
                    
                    // 跳过要更新的文件(后面会重新添加)
                    if _, exists := newFiles[header.Name]; exists {
                        continue
                    }
                    
                    // 复制其他文件
                    if err := tarWriter.WriteHeader(header); err != nil {
                        return err
                    }
                    if _, err := io.Copy(tarWriter, tarReader); err != nil {
                        return err
                    }
                }
            }
        }
    }

    // 添加/更新文件
    for filename, content := range newFiles {
        header := &tar.Header{
            Name: filename,
            Mode: 0644,
            Size: int64(len(content)),
        }
        
        if err := tarWriter.WriteHeader(header); err != nil {
            return err
        }
        if _, err := tarWriter.Write(content); err != nil {
            return err
        }
    }

    tarWriter.Close()
    gzWriter.Close()
    tmpFile.Close()
    
    return os.Rename(tmpFile.Name(), tarPath)
}

方案三:使用第三方库

如果需要更高效的解决方案,可以考虑以下第三方库:

  1. github.com/yeka/zip - 支持向现有zip文件添加文件
  2. github.com/klauspost/compress/zip - 性能优化的zip库,支持流式处理

示例使用github.com/yeka/zip

import "github.com/yeka/zip"

func appendWithYekaZip(zipPath string, filename string, content []byte) error {
    // 打开现有zip文件(如果存在)
    var zipFile *zip.ReadCloser
    if _, err := os.Stat(zipPath); err == nil {
        zipFile, err = zip.OpenReader(zipPath)
        if err != nil {
            return err
        }
        defer zipFile.Close()
    }

    // 创建新zip文件
    newZip, err := os.Create(zipPath + ".new")
    if err != nil {
        return err
    }
    defer os.Remove(newZip.Name())
    defer newZip.Close()

    zipWriter := zip.NewWriter(newZip)
    
    // 复制现有文件
    if zipFile != nil {
        for _, file := range zipFile.File {
            writer, err := zipWriter.Create(file.Name)
            if err != nil {
                return err
            }
            
            rc, err := file.Open()
            if err != nil {
                return err
            }
            
            io.Copy(writer, rc)
            rc.Close()
        }
    }
    
    // 添加新文件
    writer, err := zipWriter.Create(filename)
    if err != nil {
        return err
    }
    
    writer.Write(content)
    zipWriter.Close()
    
    // 替换原文件
    return os.Rename(newZip.Name(), zipPath)
}

选择哪种方案取决于你的具体需求:

  • 如果数据量不大,方案一的临时文件方法最可靠
  • 如果需要更好的压缩性能,方案二的tar+gzip可能更合适
  • 如果频繁追加且性能要求高,方案三的第三方库值得考虑
回到顶部