06月11, 2018

Golang short write

背景

自定义数据结构,替换golang标准库"os/exec"中cmd原始的标准输出、标准错误输出,达到获取并限制其大小,便于日志上报。在输出达到上限后遇到“short write”报错,本文记录排查思路及过程。

经验的锅

基于多年运维经验,遇到这样的报错,马上主观的认为是linux系统进程报出,然后各种姿势google后无果。怒摔电脑,出去买杯咖啡换换心情。下楼的过程中,脑中一个闪电,莫非是golang内部报错,于是着眼于golang内部实现是否哪有问题。

这个过程浪费了些时间,在排查过程遇到死结,不妨去换个思路。

追溯代码

接着就是逐层查找error了。

先是自己封装的输出数据结构、实现Write方法满足Writer接口。看起来没毛病

type Writer struct {
    data *bytes.Buffer
    size int
}

func NewWriter() *Writer {
    w := &Writer{
        data: new(bytes.Buffer),
        size: size,
    }

    return w
}

func (w *Writer) Write(b []byte) (n int, err error) {
    allowLen := w.size - w.data.Len()
    bSize := len(b)

    if allowLen <= 0 {
        return len(b), nil
    }

    if len(b) > allowLen {
        b = b[0:(allowLen - 1)]
        b = append(b, '\n')
    }

    _, e := w.data.Write(b)
    return len(b), e

重定向cmd的输出,

    // res是自定义的process数据结构,跟本次问题关系不大就不列出了。
    res.Cmd.Stdout = res.Stdout
    res.Cmd.Stderr = res.Stderr

由于cmd是通过Start()方法触发,接着看下这个方法,重点看error是哪来的。这个方法中总共有三处出现error。process一般不会报出这样的问题,着重看另外两处。

func (c *Cmd) Start() error {
    if c.lookPathErr != nil {
        c.closeDescriptors(c.closeAfterStart)
        c.closeDescriptors(c.closeAfterWait)
        return c.lookPathErr
    }
    if runtime.GOOS == "windows" {
        lp, err := lookExtensions(c.Path, c.Dir)
        if err != nil {
            c.closeDescriptors(c.closeAfterStart)
            c.closeDescriptors(c.closeAfterWait)
            return err
        }
        c.Path = lp
    }
    if c.Process != nil {
        return errors.New("exec: already started")
    }
    if c.ctx != nil {
        select {
        case <-c.ctx.Done():
            c.closeDescriptors(c.closeAfterStart)
            c.closeDescriptors(c.closeAfterWait)
            return c.ctx.Err()
        default:
        }
    }

    type F func(*Cmd) (*os.File, error)
    // 可能是这里
    for _, setupFd := range []F{(*Cmd).stdin, (*Cmd).stdout, (*Cmd).stderr} {
        fd, err := setupFd(c)
        if err != nil {
            c.closeDescriptors(c.closeAfterStart)
            c.closeDescriptors(c.closeAfterWait)
            return err
        }
        c.childFiles = append(c.childFiles, fd)
    }
    c.childFiles = append(c.childFiles, c.ExtraFiles...)

    var err error
    c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{
        Dir:   c.Dir,
        Files: c.childFiles,
        Env:   dedupEnv(c.envv()),
        Sys:   c.SysProcAttr,
    })
    if err != nil {
        c.closeDescriptors(c.closeAfterStart)
        c.closeDescriptors(c.closeAfterWait)
        return err
    }

    c.closeDescriptors(c.closeAfterStart)

    // 也可能是这里
    c.errch = make(chan error, len(c.goroutine))
    for _, fn := range c.goroutine {
        go func(fn func() error) {
            c.errch <- fn()
        }(fn)
    }

    if c.ctx != nil {
        c.waitDone = make(chan struct{})
        go func() {
            select {
            case <-c.ctx.Done():
                c.Process.Kill()
            case <-c.waitDone:
            }
        }()
    }

    return nil
}

在看了c.goroutine, (*Cmd).stdout, (*Cmd).stderr代码后,聚焦到writerDescriptor方法

func (c *Cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {
    if w == nil {
        f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0)
        if err != nil {
            return
        }
        c.closeAfterStart = append(c.closeAfterStart, f)
        return
    }

    if f, ok := w.(*os.File); ok {
        return f, nil
    }

   // 看代码,排除了os.Pipe()出现“short write”的可能性
    pr, pw, err := os.Pipe()
    if err != nil {
        return
    }

    c.closeAfterStart = append(c.closeAfterStart, pw)
    c.closeAfterWait = append(c.closeAfterWait, pr)
    // 那只剩这里了,将进程的输出拷贝到我们自定义数据结构,c.gorourine是不是也很熟悉。
    c.goroutine = append(c.goroutine, func() error {
        _, err := io.Copy(w, pr)
        pr.Close() // in case io.Copy stopped due to write error
        return err
    })
    return pw, nil
}

来到io.go有意外收获,看到了曙光。

var ErrShortWrite = errors.New("short write")

接着看io.Copy方法

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

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
    // 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)
    }
    // Similarly, if the writer has a ReadFrom method, use it to do the copy.
    if rt, ok := dst.(ReaderFrom); ok {
        return rt.ReadFrom(src)
    }
    size := 32 * 1024
    if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
        if l.N < 1 {
            size = 1
        } else {
            size = int(l.N)
        }
    }
    if buf == nil {
        buf = make([]byte, size)
    }
    for {
        nr, er := src.Read(buf)
        if nr > 0 {
            nw, ew := dst.Write(buf[0:nr])
            if nw > 0 {
                written += int64(nw)
            }
            if ew != nil {
                err = ew
                break
            }
            // 这里返回了“short write”,恍然大悟,由于我们的截断,
            // 字符串长度不匹配了
            if nr != nw {
                err = ErrShortWrite
                break
            }
        }
        if er != nil {
            if er != EOF {
                err = er
            }
            break
        }
    }
    return written, err
}

最终将我们实现的Write方法做如下修改,问题解决。


func (w *Writer) Write(b []byte) (n int, err error) {
    allowLen := w.size - w.data.Len()
    bSize := len(b)

    if allowLen <= 0 {
        return bSize, nil
    }

    if bSize > allowLen {
        b = b[0:(allowLen - 1)]
        b = append(b, '\n')
    }

    _, e := w.data.Write(b)

    // 这里需要返回截取前字符长度,否则io包中copyBuffer会由于dst、src字符长度不一致,报错"short write"
    return bSize, e
}

本文链接:https://www.opsdev.cn/post/short-write.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。