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 МБ.
Далее нам нужно использоватьgo tool pprof
анализировать:
go tool pprof -inuse_space http://本机Ip:8888/debug/pprof/heap
После ввода этой команды она аналогичнаgdb
интерактивный интерфейс, введитеtop
Команда может иметь 10 лучших выделений памяти,flat
— значение используемой памяти текущего уровня в стеке, а cum — кумулятивное значение используемой памяти этого уровня в стеке (включая значение используемой памяти вызываемой функции, уровень выше)
Видно, что встроенный метод 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 и найдите его здесь:
Здесь все содержимое файла напрямую считывается в двоичной форме, что приводит к удвоению памяти и удушающим операциям.
На самом деле при чтении большого файла все содержимое файла считывается в память, а машина переворачивается напрямую.Правильный способ - два метода обработки, один потоковый:
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
}
Здесь мы используем второй метод для оптимизации, а затем смотрим на анализ памяти после оптимизации:
Он занимает 1 МБ памяти, что соответствует размеру памяти обычного серверного сервиса. Исходный код был помещен в CrowLab и может быть прочитан в исходном коде проекта GitHub.
Наконец, ссылка на проект прилагается,GitHub.com/tika вместе/позор...Звоните в краулаб! Каждый может внести совместный вклад, чтобы сделать Crawlab более полезным!