【问题标题】:Uploading multiple files in parallel to Amazon S3 with Goroutines & Channels使用 Goroutines 和 Channels 将多个文件并行上传到 Amazon S3
【发布时间】:2019-10-01 15:53:01
【问题描述】:

我正在尝试将目录上传到 Amazon S3 存储桶。但是,上传目录的唯一方法是遍历目录内的所有文件并逐个上传。

我正在使用 Go 来遍历目录中的文件。但是,对于我遍历的每个文件,我想派生一个上传文件的 goroutine,而主线程遍历目录中的下一个元素并派生另一个 goroutine 以上传相同的文件。

知道如何使用 Goroutines 和 Channels 并行上传目录中的所有文件吗?

修改后的代码 sn-p 实现了一个 goroutine 和一个并行上传文件的通道。但我不确定这是否是正确的实现方式。

func uploadDirToS3(dir string, svc *s3.S3) {
    fileList := []string{}
    filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
        fmt.Println("PATH ==> " + path)
        fileList = append(fileList, path)
        return nil
    })
    for _, pathOfFile := range fileList[1:] {
        channel := make(chan bool)
        go uploadFiletoS3(pathOfFile, svc, channel)
        <-channel
    }
}

func uploadFiletoS3(path string, svc *s3.S3, channel chan bool) {
    file, err := os.Open(path)
    if err != nil {
        fmt.Println(err)
    }
    defer file.Close()
    fileInfo, _ := file.Stat()
    size := fileInfo.Size()

    buffer := make([]byte, size)
    file.Read(buffer)
    fileBytes := bytes.NewReader(buffer)
    fileType := http.DetectContentType(buffer)

    s3Path := file.Name()

    params := &s3.PutObjectInput{
        Bucket:        aws.String("name-of-bucket"),
        Key:           aws.String(s3Path),
        Body:          fileBytes,
        ContentLength: aws.Int64(size),
        ContentType:   aws.String(fileType),
    }

    resp, err := svc.PutObject(params)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("response %s", awsutil.StringValue(resp))
    close(channel)
}

关于如何更好地实现这一点的任何想法?我研究了 WaitGroups,但出于某种原因,我发现 Channels 在这种情况下更容易理解和实现。

【问题讨论】:

  • 是的,您可以将 for 循环的内容放入 goroutine 中(但请确保在循环内制作 pathOfFile 的本地副本或将其作为参数传递给 goroutine 函数)。您可能想要使用sync.WaitGroup,这样您就可以等待它们全部完成(或不完成——这取决于您的程序的结构)。此外,您不需要将文件读入缓冲区。可以在PutObjectInput中设置fileBody的值
  • 不幸的是,在当前代码中,您还没有实现任何并发。做好功课,学习 Go 并发模式,尝试一些东西,遇到困难,我们会帮助你。 google.com/search?q=go+concurrency+patterns
  • “对于我遍历的每个文件,我想分拆一个上传文件的 goroutine”,所以实现它。您当前的代码块,因为它是单线程的。 Tour of Go 涵盖了 Goroutines、通道和其他基本概念。
  • @AndySchweig 所以我修改了代码以实现通道而不是 WaitGroup。但是,我不完全确定这是否是正确的方法。文件仍在上传,代码正在运行,但不确定它是否并行运行。
  • @mh-cbon 刚刚使用 Goroutines 和 Channels 实现了这一点。不确定这是否是正确的方法。如果我能得到你的建议,那就太好了。

标签: amazon-web-services go amazon-s3 concurrency channels


【解决方案1】:

以下严格来说并没有回答 OP,而是尝试使用 go 语言引入并行处理。

希望这会有所帮助。

package main

import (
    "log"
    "sync"
    "time"
)

func main() {

    // processInSync()
    // The processing takes up to 3seconds,
    // it displays all the output and handles errors.

    // processInParallel1()
    // The processing takes up to few microseconds,
    // it displays some of the output and does not handle errors.
    // It is super fast, but incorrect.

    // processInParallel2()
    // The processing takes up to 1s,
    // It correctly displays all the output,
    // But it does not yet handle return values.

    processInParallel3()
    // The processing takes up to 1s,
    // It correctly displays all the output,
    // and it is able to return the first error encountered.

    // This merely just an introduction to what you are able to do.
    // More examples are required to explains the subtletlies of channels
    // to implement unbound work processing.
    // I leave that as an exercise to the reader.
    // For more information and explanations about channels,
    // Read The Friendly Manual and the tons of examples
    // we left on the internet.
    // https://golang.org/doc/effective_go.html#concurrency
    // https://gobyexample.com/channels
    // https://gobyexample.com/closing-channels
}

func aSlowProcess(name string) error {
    log.Println("aSlowProcess ", name)
    <-time.After(time.Second)
    return nil
}

//processInSync a dummy function calling a slow function one after the other.
func processInSync() error {
    now := time.Now()
    // it calls the slow process three time,
    // one after the other;
    // If an error is returned, returns asap.
    if err := aSlowProcess("#1"); err != nil {
        return err
    }
    if err := aSlowProcess("#2"); err != nil {
        return err
    }
    if err := aSlowProcess("#3"); err != nil {
        return err
    }
    // This is a sync process because it does not involve
    // extra synchronisation mechanism.
    log.Printf("processInSync spent %v\n", time.Now().Sub(now))
    return nil
}

// processInParallel1 implements parallel processing example.
// it is not yet a fully working example, to keep it simple,
// it only implements the sending part of the processing.
func processInParallel1() error {
    now := time.Now()

    // We want to execute those function calls in parallel
    // for that we use the go keyword which allows to run the function
    // into a separate routine/process/thread.
    // It is called async because the main thread and the
    // the new routines requires to be synchronized.
    // To synchronize two independant routine we must use
    // atomic (race free) operators.

    // A channel is an atomic operator because it is safe to
    // read and write from it from multiple parallel
    // and independant routines.

    // before we implement such processing, we must ask ourselve
    // what is the input i need to distribute among routines,
    // and what are the values i want to get from those routines.

    // lets create a channel of string to distribute the input to multiple
    // independant workers.
    distributor := make(chan string)

    // The input channel MUST be read from the new routines.
    // We create three workers of slow process, reading and processing.
    go func() {
        value := <-distributor
        aSlowProcess(value)
    }()
    go func() {
        value := <-distributor
        aSlowProcess(value)
    }()
    go func() {
        value := <-distributor
        aSlowProcess(value)
    }()

    // we must now write the values into the distributor
    // so that each worker can read and process data.
    distributor <- "#1"
    distributor <- "#2"
    distributor <- "#3"

    log.Printf("processInParallel1 spent %v\n", time.Now().Sub(now))

    return nil
}

// processInParallel2 implements parallel processing example.
// it is not yet a fully working example, to keep it simple,
// it implements the sending part of the processing,
// and the synchronization mechanism to wait for all workers
// to finish before returning.
func processInParallel2() error {
    now := time.Now()

    // We saw in the previous example how to send values and process
    // them in parallel, however, that function was not able to wait for
    // those async process to finish before returning.

    // To implement such synchronization mechanism
    // where the main thread waits for all workers to finish
    // before returning we need to use the sync package.
    // It provides the best pattern to handle that requirements.

    // In addition to the previous example we now instantiate a
    // WaitGroup https://golang.org/pkg/sync/#WaitGroup
    // The purpose of the wait group is to record a number
    // of async jobs to process and wait for them to finish.

    var wg sync.WaitGroup

    distributor := make(chan string)

    // Because we have three workers, we add three to the group.
    wg.Add(1)
    go func() {
        // Then we make sure that we signal to the waitgroup 
    // that the process is done.
        defer wg.Done()
        value := <-distributor
        aSlowProcess(value)
    }()
    //-
    wg.Add(1)
    go func() {
        defer wg.Done() // as an exercise, comment this line 
    // and inspect the output of your program.
        value := <-distributor
        aSlowProcess(value)
    }()
    //-
    wg.Add(1)
    go func() {
        defer wg.Done()
        value := <-distributor
        aSlowProcess(value)
    }()

    // we can now write the data for processing....
    distributor <- "#1"
    distributor <- "#2"
    distributor <- "#3"

    //....and wait for their completion
    wg.Wait()

    log.Printf("processInParallel2 spent %v\n", time.Now().Sub(now))

    return nil
}

// processInParallel3 implements parallel processing example.
// It is a fully working example that distribute jobs, 
// wait for completion and catch for return values.
func processInParallel3() error {
    now := time.Now()

    var wg sync.WaitGroup
    distributor := make(chan string)

    // To catch for return values we must implement a
    // way for output values to safely reach the main thread.
    // We create a channel of errors for that purpose.
    receiver := make(chan error)

    // As previsouly we start the workers, and attach them to a waitgroup.
    wg.Add(1)
    go func() {
        defer wg.Done()
        value := <-distributor
        err := aSlowProcess(value)
        // to return the value we write on the output channel.
        receiver <- err
    }()
    //-
    wg.Add(1)
    go func() {
        defer wg.Done()
        value := <-distributor
        receiver <- aSlowProcess(value)
    }()
    //-
    wg.Add(1)
    go func() {
        defer wg.Done()
        value := <-distributor
        receiver <- aSlowProcess(value)
    }()

    // we can now write the data for processing....
    distributor <- "#1"
    distributor <- "#2"
    distributor <- "#3"

    /// ... read the output values
    err1 := <-receiver
    err2 := <-receiver
    err3 := <-receiver

    //....and wait for routines completion....
    wg.Wait()

    log.Printf("processInParallel3 spent %v\n", time.Now().Sub(now))

    // finally check for errors
    if err1 != nil {
        return err1
    }
    if err2 != nil {
        return err2
    }
    if err3 != nil {
        return err3
    }

    return nil
}

【讨论】:

    【解决方案2】:

    所以,您正在寻找基于go 指令的并发性。对于started in loop goroutine之间的同步,你可以使用chanelssync.WaitGroup。第二个选项更容易做到。 此外,您还必须重构您的函数并将内部 for 逻辑移动到一个单独的函数中。

    func uploadDirToS3(dir string, svc *s3.S3) {
        fileList := []string{}
        filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
            fileList = append(fileList, path)
            return nil
        })
        var wg sync.WaitGroup
        wg.Add(len(fileList))
        for _, pathOfFile := range fileList[1:] {
            //maybe spin off a goroutine here??
            go putInS3(pathOfFile, svc, &wg)
        }
        wg.Wait()
    }
    
    func putInS3(pathOfFile string, svc *s3.S3, wg *sync.WaitGroup) {
        defer func() {
            wg.Done()
        }()
        file, _ := os.Open(pathOfFile)
        defer file.Close()
        fileInfo, _ := file.Stat()
        size := fileInfo.Size()
        buffer := make([]byte, size)
        file.Read(buffer)
        fileBytes := bytes.NewReader(buffer)
        fileType := http.DetectContentType(buffer)
        path := file.Name()
        params := &s3.PutObjectInput{
            Bucket:        aws.String("bucket-name"),
            Key:           aws.String(path),
            Body:          fileBytes,
            ContentLength: aws.Int64(size),
            ContentType:   aws.String(fileType),
        }
    
        resp, _ := svc.PutObject(params)
        fmt.Printf("response %s", awsutil.StringValue(resp))
    }
    

    【讨论】:

    • 感谢您非常详尽的回复。我将尝试使用 WaitGroups 来实现这一点。我已经修改了上面的代码,我使用 Channels 来实现相同的目标。但我不完全确定这是否是正确的方法。我发现 Channels 比 WaitGroup 更容​​易理解和实现。如果您对如何改进代码有任何建议,请告诉我。但我一定会尝试实施您的解决方案,以便更好地理解 WaitGroups。
    • 我不建议为每个文件创建一个 goroutine。如果有 1000 个文件,它将创建 1000 个 go 例程,并尝试一次上传所有文件。相反,您应该查看工作池以控制并发上传的数量。
    • @ankit-deshpande 同意你的看法。但我只是展示了如何同时运行它。关于工人池 - 也许他在其他地方读过
    • @Solorad 同意。
    猜你喜欢
    • 2018-10-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-16
    • 1970-01-01
    • 2015-03-17
    • 2021-08-22
    • 1970-01-01
    相关资源
    最近更新 更多