[Go] Использование сжатых файлов для оптимизации ввода-вывода (1)

Go

Оригинальная ссылка:blog.thinker idea.com/201906/go/from…

Недавно столкнулся с проблемой, что лог бэкап io слишком высокий.Бэкап бизнес лога бэкапится каждые десять минут.Он изначально был написан на Python для сканирования лога бекапа по правилам.Чем больше файлов,тем больше количество файлов, что приводит к серьезной блокировке ввода-вывода в момент каждого резервного копирования, аномально высокой загрузке ЦП и нагрузки.К счастью, скорость резервного копирования высокая, и влияние на бизнес невелико.Эта проблема будет расти с ростом дело.Все очевиднее становится,что за этот промежуток времени метод бекапа оптимизировался,и эффект очень значительный.Я организую статью,чтобы это зафиксировать.

справочная информация

Конфигурация сервера: 4 ядра 8G Диск: 500G

Необходимо загружать каждые десять минут: 18 файлов, около 10G в часы пик.

Для обеспечения надежности бизнес-журналы сначала записываются в файлы на диске, а файлы журналов разбиваются каждые 10 минут, а затем журналы копируются в OSS в первую минуту следующих десяти минут. Служба анализа данных будет извлекать журналы для анализ после завершения резервного копирования.Резервное копирование журнала должно быть эффективным и быстрым, и резервное копирование может быть выполнено в кратчайшие сроки.Как правило, резервное копирование может быть завершено в течение десятков секунд.

Скорость и эффективность резервного копирования не является проблемой, оно достаточно быстрое, но аномальная нагрузка на ЦП и нагрузка, вызванная сильной блокировкой ввода-вывода во время резервного копирования, становится узким местом бизнес-сервисов.В часы пик бизнес-сервисы потребляют только половину системных ресурсов, но при резервном копировании ЦП часто 100%, а iowait может достигать более 70, а простаивающих ресурсов очень мало, поэтому с расширением бизнеса, хотя время резервного копирования журнала очень короткое, оно стало узким местом система.

Решение до и после оптимизации будет подробно описано далее, а тест будет написан на ходу, а для тестирования будет использован 2-ядерный 4G сервер Размер тестового набора данных:

  • Количество файлов: 336
  • Исходный файл: 96G
  • Сжатый файл: 24G
  • Схема сжатия: lzo
  • Количество горутин: 4

До оптимизации

Процесс резервного копирования журнала перед оптимизацией:

  • Сканировать файлы для резервного копирования в соответствии с правилами резервного копирования
  • использоватьlzopкоманда для сжатия журналов
  • Загружать сжатые журналы в OSS

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

.../pkg/aliyun_ossЭто пакет, основанный на операциях Alibaba Cloud OSS, которые я инкапсулировал сам. Этот путь неверен. Он предназначен только для демонстрации. Если вы хотите запустить следующий код, вам нужно реализовать часть взаимодействия с OSS самостоятельно.

package main

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"sync"
	"time"

	".../pkg/aliyun_oss"
)

func main() {
	var oss *aliyun_oss.AliyunOSS
	files := os.Args[1:]
	if len(files) < 1 {
		fmt.Println("请输入要上传的文件")
		os.Exit(1)
	}

	fmt.Printf("待备份文件数量:%d\n", len(files))

	startTime := time.Now()
	defer func(startTime time.Time) {
		fmt.Printf("共耗时:%s\n", time.Now().Sub(startTime).String())
	}(startTime)

	var wg sync.WaitGroup
	n := 4
	c := make(chan string)

	// 压缩日志
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func() {
			defer wg.Done()
			for file := range c {
				cmd := exec.Command("lzop", file)
				cmd.Stderr = &bytes.Buffer{}
				err := cmd.Run()
				if err != nil {
					panic(cmd.Stderr.(*bytes.Buffer).String())
				}
			}
		}()
	}

	for _, file := range files {
		c <- file
	}

	close(c)
	wg.Wait()
	fmt.Printf("压缩耗时:%s\n", time.Now().Sub(startTime).String())

	// 上传压缩日志
	startTime = time.Now()
	c = make(chan string)
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func() {
			defer wg.Done()
			for file := range c {
				name := filepath.Base(file)
				err := oss.PutObjectFromFile("tmp/"+name+".lzo", file+".lzo")
				if err != nil {
					panic(err)
				}
			}
		}()
	}

	for _, file := range files {
		c <- file
	}

	close(c)
	wg.Wait()
	fmt.Printf("上传耗时:%s\n", time.Now().Sub(startTime).String())
}

Вывод при запуске программы:

待备份文件数量:336
压缩耗时:19m44.125314226s
上传耗时:6m14.929371103s
共耗时:25m59.118002969s

Из бегущих результатов видно, что сжатый файл занимает много времени, а фактическоеiostatАнализ команд также показал, что потребление ресурсов при сжатии относительно велико.iostat -m -x 5 10000Команда собирает данные на каждом этапе.

  • Перед запуском программы
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           2.35    0.00    2.86    0.00    0.00   94.79

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00    0.00    0.00   0.00   0.00
vdb               0.00     0.60    0.00    0.60     0.00     4.80    16.00     0.00    0.67    0.00    0.67   0.67   0.04
  • При сжатии журнала
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          10.84    0.00    6.85   80.88    0.00    1.43

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.00    0.60    0.00     2.40     0.00     8.00     0.00    0.67    0.67    0.00   0.67   0.04
vdb              14.80  5113.80 1087.60   60.60 78123.20 20697.60   172.13   123.17  106.45  106.26  109.87   0.87 100.00

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          10.06    0.00    7.19   79.06    0.00    3.70

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.00    1.60    0.00   103.20     0.00   129.00     0.01    3.62    3.62    0.00   0.50   0.08
vdb              14.20  4981.20  992.80   52.60 79682.40 20135.20   190.97   120.34  112.19  110.60  142.17   0.96 100.00
  • При загрузке журналов
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           6.98    0.00    7.81    7.71    0.00   77.50

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.00   13.40    0.00   242.40     0.00    36.18     0.02    1.63    1.63    0.00   0.19   0.26
vdb               0.40     2.40  269.60    1.20 67184.80    14.40   496.30     4.58   15.70   15.77    0.33   1.39  37.74

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           7.06    0.00    8.00    4.57    0.00   80.37

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.00    0.60    0.00    75.20     0.00   250.67     0.00    2.67    2.67    0.00   2.00   0.12
vdb               0.20     0.00  344.80    0.00 65398.40     0.00   379.34     5.66   16.42   16.42    0.00   1.27  43.66

отiostatВ результате выяснилось, что при сжатии программыr_awaitа такжеw_awaitдостигли более ста, иiowaitГандам80.88%, почти исчерпал весь ЦП, при загрузкеiowaitЭто приемлемо, потому что сжатый файл просто читается, а сжатый файл еще и небольшой.

анализировать проблему

Из вышеприведенных результатов видно, что программа в основном потребляет сжатый лог, а оптимизация также фокусируется на логике сжатия лога.

При сжатии журнал сначала будет сжат вlzoфайл перед загрузкойlzoВ процессе передачи файлов в Alibaba Cloud OSS есть несколько процессов:

  • Чтение необработанного файла журнала
  • сжатые данные
  • записыватьlzoдокумент
  • читатьlzoдокумент
  • httpотправить прочитанный контент

при сжатииr_awaitа такжеw_awaitвысоки, в основном возникает при чтении необработанных файлов журнала, записиlzoфайл, как его оптимизировать?

Сначала подумайте об исходных требованиях, прочитайте исходный файл -> загрузите данные. Но загрузка исходного файла напрямую, файл относительно большой, передача по сети медленная, а стоимость хранения относительно высока, что мне делать?

В это время мы ожидаем загрузки сжатых файлов, поэтому у нас есть логика перед оптимизацией, которая генерирует промежуточный процесс, то есть с использованиемlzopкоманда сжимает файл и создает промежуточный файлlzoдокумент.

Необходимо прочитать исходный файл и загрузить данные, тогда процесс сжатия можно оптимизировать, поэтомуr_awaitНет возможности оптимизировать, тогда только оптимизацияw_await,w_awaitКак получилось, именно написатьlzoгенерируется, может быть, а может и не бытьlzoфайл? Что делает этот файл?

Если мы сжимаем поток данных файла, в процессе чтения исходного файла -> загрузка данных загруженный поток данных сжимается в режиме реального времени, а сжатый контент загружается для реализации сжатия при чтении и обработки потока данных. , как промежуточное ПО, поэтому вам не нужно писатьlzoфайл, затемw_awaitОн полностью оптимизирован.

lzoЧто делает файл? Я думаю, что потребление сжатия файла может быть сохранено один раз только после неудачной загрузки. Сколько раз загрузка не удалась? Пользуюсь Alibaba Cloud OSS уже несколько лет.За исключением сбоя в интранете ни разу не сталкивался с файлом, который не удалось загрузить.Думаю, этот файл не нужен, а он генерируетlzoФайлы также должны занимать место на диске и регулярно очищаться, что увеличивает потребление ресурсов и затраты на обслуживание.

Оптимизировано

Согласно предыдущему анализу, давайте посмотрим на процесс резервного копирования файлов после оптимизации:

  • читать необработанный журнал
  • Сжать поток данных в памяти
  • http отправить сжатый контент

Этот процесс экономит два шага, записываяlzoфайл и читатьlzoфайл, не только нетw_await, четноеr_awaitОн также был немного оптимизирован.

План оптимизации определен, но как добитьсяlzoКак насчет сжатия файлового потока?GithubПроверьте его, чтобы увидеть, есть лиlzoбиблиотека алгоритмов сжатия, найденнаяgithub.com/cyberdelia/lzo, хотя он реализован обращением к библиотеке C, два классических алгоритма (lzo1x_1а такжеlzo1x_999) все предоставляют интерфейсы, и кажется, что Go может использовать эту библиотеку напрямую.

Обнаружено, что эта библиотека реализуетio.Readerа такжеio.Writerинтерфейс,io.ReaderЧтение потока сжатых файлов, вывод распакованных данных,io.WriterРеализовать входные необработанные данные и записать на входio.Writer.

Для достижения сжатых потоков данных кажется, что вам нужно использоватьio.Writerинтерфейс, но этот ввод и выводio.Writer, что может быть сложно, потому что мы читаем файл и получаемio.Reader, ввод интерфейса http такжеio.Reader,похоже,нет интерфейса,который можно использовать напрямую,неужели нет возможности его реализовать?Нет,мы можем его сами инкапсулировать.Следующая инкапсуляцияlzoМетод сжатия потока данных:

package lzo

import (
	"bytes"
	"io"

	"github.com/cyberdelia/lzo"
)

type Reader struct {
	r    io.Reader
	rb   []byte
	buff *bytes.Buffer
	lzo  *lzo.Writer
	err  error
}

func NewReader(r io.Reader) *Reader {
	z := &Reader{
		r:    r,
		rb:   make([]byte, 256*1024),
		buff: bytes.NewBuffer(make([]byte, 0, 256*1024)),
	}

	z.lzo, _ = lzo.NewWriterLevel(z.buff, lzo.BestSpeed)
	return z
}

func (z *Reader) compress() {
	if z.err != nil {
		return
	}

	var nr, nw int
	nr, z.err = z.r.Read(z.rb)
	if z.err == io.EOF {
		if err := z.lzo.Close(); err != nil {
			z.err = err
		}
	}

	if nr > 0 {
		nw, z.err = z.lzo.Write(z.rb[:nr])
		if z.err == nil && nr != nw {
			z.err = io.ErrShortWrite
		}
	}
}

func (z *Reader) Read(p []byte) (n int, err error) {
	if z.err != nil {
		return 0, z.err
	}

	if z.buff.Len() <= 0 {
		z.compress()
	}

	n, err = z.buff.Read(p)
	if err == io.EOF {
		err = nil
	} else if err != nil {
		z.err = err
	}

	return
}

func (z *Reader) Reset(r io.Reader) {
	z.r = r
	z.buff.Reset()
	z.err = nil
	z.lzo, _ = lzo.NewWriterLevel(z.buff, lzo.BestSpeed)
}

Эта библиотека будет постоянно потреблять 512 КБ памяти, что не очень много. Нам нужно создать буфер чтения и буфер сжатия, оба имеют размер 256 КБ. Фактическому сжатому буферу 256 КБ не нужно. сжатые данные будут больше, чем исходные данные.Small, учитывая, что пространство не очень велико, непосредственно выделяйте 256 КБ, чтобы избежать выделения во время выполнения.

Принцип реализации - когда http вводится изio.Reader(На самом деле это то, что мы инкапсулировали вышеlzoбиблиотека), при чтении данных эта библиотека проверяет, пуст ли буфер сжатия, если он пуст, то она будет считывать 256k данных из файла и сжимать их в буфер сжатия, а затем читать данные из буфера сжатия в httpio.Reader, если в буфере сжатия есть данные, прочитать сжатые данные непосредственно из буфера сжатия.

Это не является потокобезопасным и выделяет фиксированный буфер размером 512 КБ, поэтому также обеспечиваетResetметод повторного использования этого объекта, чтобы избежать повторного выделения памяти, но необходимо обеспечитьlzoДоступ к экземплярам объекта может получить только одна горутина, это можно сделать с помощьюsync.PoolДля обеспечения следующего кода я использую другой метод для обеспечения.

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"

	".../pkg/aliyun_oss"
	".../pkg/lzo"
)

func main() {
	var oss *aliyun_oss.AliyunOSS
	files := os.Args[1:]
	if len(files) < 1 {
		fmt.Println("请输入要上传的文件")
		os.Exit(1)
	}

	fmt.Printf("待备份文件数量:%d\n", len(files))

	startTime := time.Now()
	defer func() {
		fmt.Printf("共耗时:%s\n", time.Now().Sub(startTime).String())
	}()

	var wg sync.WaitGroup
	n := 4
	c := make(chan string)

	// 压缩日志
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func() {
			defer wg.Done()
			var compress *lzo.Reader

			for file := range c {
				r, err := os.Open(file)
				if err != nil {
					panic(err)
				}

				if compress == nil {
					compress = lzo.NewReader(r)
				} else {
					compress.Reset(r)
				}

				name := filepath.Base(file)
				err = oss.PutObject("tmp/"+name+"1.lzo", compress)
				r.Close()
				if err != nil {
					panic(err)
				}
			}
		}()
	}

	for _, file := range files {
		c <- file
	}

	close(c)
	wg.Wait()
}

Программа выделяет фиксированныйcompress, когда файл нужно сжать, определяется, создавать его или сбрасывать для достижения эффекта мультиплексирования.

Запуск программы выводит:

待备份文件数量:336
共耗时 18m20.162441931s

Фактическое время на 28% выше, чем до оптимизации, а фактический проходiostatАнализ команд также показал, что потребление ресурсов также значительно улучшилось.iostat -m -x 5 10000Команда собирает данные на каждом этапе.

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          15.72    0.00    6.58   74.10    0.00    3.60

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00    0.00    0.00   0.00   0.00
vdb               3.80     3.40 1374.20    1.20 86484.00    18.40   125.79   121.57   87.24   87.32    1.00   0.73 100.00

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          26.69    0.00    8.42   64.27    0.00    0.62

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.00     0.20  426.80    0.80  9084.80     4.00    42.51     2.69    6.29    6.30    1.00   0.63  26.92
vdb               1.80     0.00 1092.60    0.00 72306.40     0.00   132.36   122.06  108.45  108.45    0.00   0.92 100.02

пройти черезiostatнашел толькоr_await,w_awaitполностью оптимизирован,iowaitЗначительное улучшение, более короткое время выполнения, более эффективное и меньшее время для воздействия на ввод-вывод.

Проблемы, возникающие при оптимизации

первый, кто нашелlzoБиблиотека алгоритмов для тестирования, чтобы убедиться, что сжатие и распаковка не вызывают проблем, иlzopСовместимость с командами.

В этот период было установлено, что соотношение данных при использовании сжатыхlzopСжатых данных намного больше.Прочитав реализацию исходного кода, я не обнаружил никаких проблем.Я попытался настроить размер буфера и обнаружил, что размер сгенерированного сжатого файла значительно улучшился.

Это открытие меня очень смутило.Какого размера соответствующий буфер?Я могу только посмотреть на него.lzopЯ обнаружил, что размер сжатого блока lzop по умолчанию составляет 256 КБ, а максимальный размер блока, поддерживаемый фактическим алгоритмом lzo, составляет 256 КБ, поэтому реализацияlzoАлгоритм упаковки заключается в создании буфера размером 256 КБ, размер этого буфера равен размеру сжатого блока, рекомендуется не настраивать его при использовании.

Суммировать

После запуска программы загрузка заняла почти полминуты до примерно десяти секунд (эффективность самого языка Go также очень помогает), и нагрузка была значительно улучшена.

До оптимизации при каждом запуске бэкапа лога ЦП часто рвался в таблицу.После оптимизации ЦП увеличился на 20% при бэкапе, что может спокойно справиться с проблемой расширения бизнеса.

Тесты проводились на простаивающей машине, на самом рабочем сервере.w_awaitИх будет около 20. Если вы используете твердотельный накопитель, режим полного дуплекса, чтение и запись разделены, то оптимизируйте егоw_awaitПомощь бизнесу очень велика, и канал записи бизнес-лога не будет заблокирован.

Конечно, наш сервер представляет собой высокоскоростной облачный диск (механический диск), поскольку физические характеристики механического диска могут быть только полудуплексными, он либо читается, либо пишется, поэтому он оптимизирован.w_awaitЭто правда, что эффективность будет значительно повышена, но это все равно повлияет на написание бизнес-сервисов.

Перепечатано:

Автор этой статьи: Ци Инь (thinkeridea)

Ссылка на эту статью:blog.thinker idea.com/201906/go/from…

Заявление об авторских правах: все статьи в этом блоге используются, если не указано иное.Соглашение CC BY 4.0 CNсоглашение. Пожалуйста, укажите источник!