Golang中如何将控制台日志更快地重定向到文件

Golang中如何将控制台日志更快地重定向到文件 我正在开发一个使用cgo绑定调用C库的应用程序。该库默认将日志输出到控制台。

为了将日志从控制台重定向,我们使用了以下代码:

file, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_SYNC, 0644)
if err != nil {
return errors.New("error creating log file, using stderr")
}
syscall.Dup2(int(file.Fd()), 1)
syscall.Dup2(int(file.Fd()), 2)

这种方法有效,但会显著降低整个程序的运行速度。如果日志量很大,程序的执行会延迟。 如果我在RAM磁盘上运行这段代码(RAM磁盘的I/O速度比磁盘I/O快得多),情况会好一些。但如果日志量巨大,将所有日志都放在RAM中意义不大,因为RAM磁盘空间有限。 对于我们用Go编写的代码,我们使用了logrus进行日志记录,效果很好。

有人能建议一种更快的方法将日志从控制台重定向到文件吗?我无法修改C库中的任何内容。


更多关于Golang中如何将控制台日志更快地重定向到文件的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

这里有一个更新。尝试了Dup3变体。它甚至更慢。

更多关于Golang中如何将控制台日志更快地重定向到文件的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


谢谢 @petrus。这解决了我的问题。

skillian:

抱歉,但有没有办法配置它直接转储到文件?

不,该库不支持写入文件。

关于重定向的进一步说明,如果我禁用上面两行,速度会提高。

我仍需检查“从标准输出或标准错误重定向到磁盘,然后再从控制台重定向”时的行为。

你说那个C库“默认”将日志输出到控制台。我理解你不能修改那个C库,但是有没有办法配置它,让它直接输出到文件呢?

我很好奇性能下降是因为Go,还是因为输出到磁盘本身就很慢。如果你在代码中不做任何处理来将标准输出或标准错误重定向到磁盘,而是从控制台进行重定向,它仍然会变慢还是没问题?

请建议一种将日志从控制台重定向到文件的更快方法。

file, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_SYNC, 0644)

停止要求写入操作变得非常、非常、非常慢。不要使用 O_SYNC。

$ man open

O_SYNC

Write operations on the file will complete according to the  re‐
quirements  of  synchronized  I/O  file integrity completion (by
contrast with the synchronized  I/O  data  integrity  completion
provided by O_DSYNC.)

By  the  time write(2) (or similar) returns, the output data and
associated file metadata have been transferred to the underlying
hardware  (i.e.,  as though each write(2) was followed by a call
to fsync(2)).

你可以考虑使用管道(pipe)配合缓冲区来减少文件写入的阻塞延迟。这种方法将日志数据先写入内存缓冲区,然后由单独的goroutine异步写入文件,从而减少主程序的等待时间。以下是示例代码:

package main

import (
    "bufio"
    "io"
    "os"
    "syscall"
)

func redirectLogsToFile(logFile string) error {
    // 创建管道
    r, w, err := os.Pipe()
    if err != nil {
        return err
    }

    // 打开日志文件
    file, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        return err
    }
    defer file.Close()

    // 使用带缓冲的写入器
    bufferedWriter := bufio.NewWriterSize(file, 64*1024) // 64KB缓冲区

    // 启动goroutine异步写入文件
    go func() {
        defer r.Close()
        io.Copy(bufferedWriter, r)
        bufferedWriter.Flush()
    }()

    // 重定向标准输出和错误
    syscall.Dup2(int(w.Fd()), 1)
    syscall.Dup2(int(w.Fd()), 2)
    w.Close()

    return nil
}

对于更高性能的场景,可以考虑使用双缓冲区技术:

package main

import (
    "bytes"
    "io"
    "os"
    "sync"
    "syscall"
    "time"
)

type bufferedFileWriter struct {
    file     *os.File
    bufPool  sync.Pool
    writeCh  chan *bytes.Buffer
    flushCh  chan struct{}
    stopCh   chan struct{}
}

func newBufferedFileWriter(logFile string) (*bufferedFileWriter, error) {
    file, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        return nil, err
    }

    w := &bufferedFileWriter{
        file:    file,
        writeCh: make(chan *bytes.Buffer, 1000),
        flushCh: make(chan struct{}),
        stopCh:  make(chan struct{}),
    }

    w.bufPool.New = func() interface{} {
        return bytes.NewBuffer(make([]byte, 0, 64*1024))
    }

    go w.writerLoop()
    return w, nil
}

func (w *bufferedFileWriter) writerLoop() {
    ticker := time.NewTicker(100 * time.Millisecond)
    defer ticker.Stop()

    for {
        select {
        case buf := <-w.writeCh:
            w.file.Write(buf.Bytes())
            buf.Reset()
            w.bufPool.Put(buf)
        case <-ticker.C:
            w.file.Sync()
        case <-w.stopCh:
            w.file.Sync()
            w.file.Close()
            return
        }
    }
}

func (w *bufferedFileWriter) Write(p []byte) (int, error) {
    buf := w.bufPool.Get().(*bytes.Buffer)
    buf.Write(p)
    select {
    case w.writeCh <- buf:
    default:
        // 如果通道满,直接写入文件
        w.file.Write(p)
        w.bufPool.Put(buf)
    }
    return len(p), nil
}

func redirectWithBufferedWriter(logFile string) error {
    writer, err := newBufferedFileWriter(logFile)
    if err != nil {
        return err
    }

    // 创建管道
    r, w, err := os.Pipe()
    if err != nil {
        return err
    }

    // 启动复制goroutine
    go func() {
        defer r.Close()
        io.Copy(writer, r)
        writer.stopCh <- struct{}{}
    }()

    // 重定向标准输出和错误
    syscall.Dup2(int(w.Fd()), 1)
    syscall.Dup2(int(w.Fd()), 2)
    w.Close()

    return nil
}

如果C库支持设置自定义文件描述符,你可以直接传递文件描述符:

package main

import (
    "os"
    "syscall"
)

func redirectWithDirectFD(logFile string) error {
    file, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        return err
    }

    // 使用非阻塞I/O
    syscall.SetNonblock(int(file.Fd()), true)

    // 直接重定向到文件描述符
    syscall.Dup2(int(file.Fd()), 1)
    syscall.Dup2(int(file.Fd()), 2)

    return nil
}

对于Linux系统,你还可以考虑使用tee系统调用将日志同时输出到控制台和文件,但这需要更复杂的实现。选择哪种方法取决于你的具体性能要求和系统环境。

回到顶部