Анализ и оптимизация серверной памяти Crawlab golang — на основе go pprof

Go

1. Предпосылки

CrawlabС момента своего выпуска в течение нескольких месяцев он пережил множество итераций.Благодаря положительным отзывам пользователей платформа обходного робота постепенно стабилизировалась.Однако некоторые пользователи недавно сообщили, что после того, как обходной компьютер был запущен в течение определенного периода времени, основной компьютер узла будет иметь чрезмерное использование памяти.Проблема в том, что машина с 4G памятью может занимать более 3.5G после запуска crawlab.Почти наверняка интерфейс серверной службы будет занимать память из-за неправильного кода, поэтому я решил проведите анализ памяти на crawlab.

2. Анализ

Анализировать память, разрывая код вручную, сложнее, и всегда нужно использовать какие-то инструменты. Golang pprof — это официальный инструмент Go для профилирования, очень мощный и простой в использовании.

Во-первых, мы встраиваем следующие строки кода в проект Crawlab:

 import _ "net/http/pprof"
go func() {
		http.ListenAndServe("0.0.0.0:8888", nil)
	}()

После запуска серверной службы обхода введите в браузереhttp://ip:8899/debug/pprof/Вы можете увидеть страницу сводного анализа, показывающую следующую информацию:

/debug/pprof/

profiles:
0    block
32    goroutine
552    heap
0    mutex
51    threadcreate

full goroutine stack dump

Нажмите на кучу, и в верхней части страницы сводного анализа вы увидите, как показано на рисунке ниже: красная стрелка указывает на используемую в настоящее время память кучи размером 25 МБ, потрясающе! Когда я загружаю только один файл сканера, объем памяти, используемый серверной службой, может достигать 25 МБ.

mYnJuF.jpg

Далее нам нужно использоватьgo tool pprofанализировать:

go tool pprof -inuse_space http://本机Ip:8888/debug/pprof/heap

После ввода этой команды она аналогичнаgdbинтерактивный интерфейс, введитеtopКоманда может иметь 10 лучших выделений памяти,flat— значение используемой памяти текущего уровня в стеке, а cum — кумулятивное значение используемой памяти этого уровня в стеке (включая значение используемой памяти вызываемой функции, уровень выше)

mYMF91.png

Видно, что встроенный метод bytes.makeSlice на самом деле использует память 24M.Если продолжить смотреть вниз, то можно увидеть метод ReadFrom.Поискав, я обнаружил, чтоioutil.ReadAll()позвонюbytes.Buffer.ReadFrom, иbytes.Buffer.ReadFromбудет осуществлятьсяmakeSlice. Давайте вернемся к реализации кода io/ioutil.readAll,

func readAll(r io.Reader, capacity int64) (b []byte, err error) {
    buf := bytes.NewBuffer(make([]byte, 0, capacity))
    defer func() {
        e := recover()
        if e == nil {
            return
        }
        if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
            err = panicErr
        } else {
            panic(e)
        }
    }()
    _, err = buf.ReadFrom(r)
    return buf.Bytes(), err
}

// bytes.MinRead = 512
func ReadAll(r io.Reader) ([]byte, error) {
    return readAll(r, bytes.MinRead)
}

можно увидеть,ioutil.ReadAllКаждый раз он будет выделять и инициализировать размерbytes.MinReadбуфер,bytes.MinReadВ Голанге есть константа, значение которой равно512. То есть каждый звонокioutil.ReadAllвыделит блок памяти размером 512 байт, что в настоящее время кажется нормальным, но давайте посмотрим на это еще разReadFromреализация,

// ReadFrom reads data from r until EOF and appends it to the buffer, growing
// the buffer as needed. The return value n is the number of bytes read. Any
// error except io.EOF encountered during the read is also returned. If the
// buffer becomes too large, ReadFrom will panic with ErrTooLarge.
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
    b.lastRead = opInvalid
    // If buffer is empty, reset to recover space.
    if b.off >= len(b.buf) {
        b.Truncate(0)
    }
    for {
        if free := cap(b.buf) - len(b.buf); free < MinRead {
            // not enough space at end
            newBuf := b.buf
            if b.off+free < MinRead {
                // not enough space using beginning of buffer;
                // double buffer capacity
                newBuf = makeSlice(2*cap(b.buf) + MinRead)
            }
            copy(newBuf, b.buf[b.off:])
            b.buf = newBuf[:len(b.buf)-b.off]
            b.off = 0
        }
        m, e := r.Read(b.buf[len(b.buf):cap(b.buf)])
        b.buf = b.buf[0 : len(b.buf)+m]
        n += int64(m)
        if e == io.EOF {
            break
        }
        if e != nil {
            return n, e
        }
    }
    return n, nil // err is EOF, so return nil explicitly
}

Основная функция этой функции состоит в том, чтобыio.ReaderДанные, считанные из буфера, помещаются в буфер, если места в буфере недостаточно,2x + MinReadАлгоритм увеличен, здесьMinReadРазмер файла также составляет 512 байт, что означает, что если файл, который мы читаем за один раз, слишком велик, используемая память удвоится.Предполагая, что наш файл сканера имеет общий размер 500 МБ, тогда используемая память составляет 500 МБ * 2. + 512B, К тому же в файле краулера очень много лог-файлов.Посмотрим, какой раздел исходников кроулаба используется.ioutil.ReadAllПрочитайте файл Shrawler и найдите его здесь:

mYQTWn.jpg

Здесь все содержимое файла напрямую считывается в двоичной форме, что приводит к удвоению памяти и удушающим операциям.

На самом деле при чтении большого файла все содержимое файла считывается в память, а машина переворачивается напрямую.Правильный способ - два метода обработки, один потоковый:

func ReadFile(filePath string, handle func(string)) error {
    f, err := os.Open(filePath)
    defer f.Close()
    if err != nil {
        return err
    }
    buf := bufio.NewReader(f)

    for {
        line, err := buf.ReadLine("\n")
        line = strings.TrimSpace(line)
        handle(line)
        if err != nil {
            if err == io.EOF{
                return nil
            }
            return err
        }
        return nil
    }
}

Второе решение — обработка фрагментации, при чтении бинарного файла без перевода строки целесообразнее использовать это решение:

func ReadBigFile(fileName string, handle func([]byte)) error {
    f, err := os.Open(fileName)
    if err != nil {
        fmt.Println("can't opened this file")
        return err
    }
    defer f.Close()
    s := make([]byte, 4096)
    for {
        switch nr, err := f.Read(s[:]); true {
        case nr < 0:
            fmt.Fprintf(os.Stderr, "cat: error reading: %s\n", err.Error())
            os.Exit(1)
        case nr == 0: // EOF
            return nil
        case nr > 0:
            handle(s[0:nr])
        }
    }
    return nil
}

Здесь мы используем второй метод для оптимизации, а затем смотрим на анализ памяти после оптимизации:

mYltYj.png

Он занимает 1 МБ памяти, что соответствует размеру памяти обычного серверного сервиса. Исходный код был помещен в CrowLab и может быть прочитан в исходном коде проекта GitHub.

Наконец, ссылка на проект прилагается,GitHub.com/tika вместе/позор...Звоните в краулаб! Каждый может внести совместный вклад, чтобы сделать Crawlab более полезным!