Skip to content

PR #122: реализация переопределения хедеров и ее оптимизация.md

Timur Torubarov edited this page Apr 10, 2019 · 1 revision

Я делал новую функциональность для raw ридера патронов - перезаписывание заголовков в патронах через конфиг, без генерации патронов заново.

Задачка сходу выглядела как простая штука - в провайдере данных почитать конфиг, подекодить запрос из патронов, перезаписать заголовки.

Но, так как данные упражнения производятся на каждый фактический запрос из пушки, мы решили исследовать производительность изменений (особенно волновала работа с памятью, потому что работа GC останавливает работу пушки. И чем чаще и дольше по времени она будет проходить, тем менее стабильной и производительной будет работа пушки).

Первое что я сделал - написал бенчмарк для оригинального варианта (его не было), чтобы была отправная точка.

func BenchmarkRawDecode(b *testing.B) {
	for i := 0; i < b.N; i++ {
		decodeRequest([]byte(benchTestRequest))
	}
}

Запускаем, смотрим что у нас есть на данный момент, записывая профайлинг в файлы.

go test --bench=BenchmarkRawDecode -benchmem -cpuprofile=cpu0.out -memprofile=mem0.out
pkg: github.com/yandex/pandora/components/phttp/ammo/simple/raw
BenchmarkRawDecode-8   	  200000	      9393 ns/op	    5154 B/op	      11 allocs/op

Теперь посмотрим что в профайлинге памяти

$ go tool pprof --alloc_space mem0.out
(pprof) top
Showing nodes accounting for 997.16MB, 99.53% of 1001.83MB total
Dropped 40 nodes (cum <= 5.01MB)
Showing top 10 nodes out of 12
      flat  flat%   sum%        cum   cum%
  818.62MB 81.71% 81.71%   818.62MB 81.71%  bufio.NewReaderSize
   85.02MB  8.49% 90.20%    85.02MB  8.49%  net/textproto.(*Reader).ReadMIMEHeader
   50.51MB  5.04% 95.24%   157.54MB 15.73%  net/http.readRequest
      19MB  1.90% 97.14%       19MB  1.90%  net/url.parse
      13MB  1.30% 98.44%  1000.16MB 99.83%  github.com/yandex/pandora/components/phttp/ammo/simple/raw.BenchmarkRawDecode
      11MB  1.10% 99.53%       11MB  1.10%  bytes.NewReader
         0     0% 99.53%   818.62MB 81.71%  bufio.NewReader
         0     0% 99.53%   987.16MB 98.54%  github.com/yandex/pandora/components/phttp/ammo/simple/raw.decodeRequest
         0     0% 99.53%   157.54MB 15.73%  net/http.ReadRequest
         0     0% 99.53%       19MB  1.90%  net/url.ParseRequestURI

и процессора

(pprof) top10
Showing nodes accounting for 1470ms, 44.82% of 3280ms total
Dropped 54 nodes (cum <= 16.40ms)
Showing top 10 nodes out of 155
      flat  flat%   sum%        cum   cum%
     430ms 13.11% 13.11%      770ms 23.48%  runtime.scanobject
     250ms  7.62% 20.73%      250ms  7.62%  runtime.futex
     150ms  4.57% 25.30%      440ms 13.41%  runtime.sweepone
     130ms  3.96% 29.27%      130ms  3.96%  runtime.memclrNoHeapPointers
     120ms  3.66% 32.93%      150ms  4.57%  runtime.findObject
     100ms  3.05% 35.98%      800ms 24.39%  runtime.mallocgc

24.39% времени тратится на сборку мусора, 99.83% выделенной памяти приходится на наш бенчмарк, 98.54% из которых - на метод decodeRequest. Смотрим подробности про метод:

(pprof) list decodeRequest
Total: 1001.83MB
ROUTINE ======================== github.com/yandex/pandora/components/phttp/ammo/simple/raw.decodeRequest in /home/ttorubarov/testssss/pandora/components/phttp/ammo/simple/raw/decoder.go
         0   987.16MB (flat, cum) 98.54% of Total
         .          .     18:	}
         .          .     19:	return
         .          .     20:}
         .          .     21:
         .          .     22:func decodeRequest(reqString []byte) (req *http.Request, err error) {
         .   987.16MB     23:	req, err = http.ReadRequest(bufio.NewReader(bytes.NewReader(reqString)))
         .          .     24:	if err != nil {
         .          .     25:		return
         .          .     26:	}
         .          .     27:	if req.Host != "" {
         .          .     28:		req.URL.Host = req.Host

bytes.NewReader создаёт обёртку in-memory reader для работы вокруг слайса байт с текстовыми данными запроса Далее bufio.NewReader - обёртка, для буферизированной работы с I/O И, наконец, http.ReadRequest, который парсит данные и создаёт объект запроса. Ничего криминального.

Так как моей задачей было не оптимизировать текущий метод, а по-быстрому допилить нужное мне, я начал решать свою задачку.

И сходу и в лоб я её решил так, как привык на питоне - сделал словарик из данных конфига (map[string][string]), положил в неё key-value прочитанные заголовки из конфига, перезаписывал их на этапе формирования объекта http.Request.

Напишем метод, который будет делать то, что нам нужно (читать конфиг, парсить все строки вида "[Host: yourhost.tld]", разбивать на ключ и значение и добавлять/переписывать HTTP заголовки в сформированном объекте запроса.

func decodeConfigHeader(req *http.Request, header string) error {
	line := []byte(header)
	if len(line) < 3 || line[0] != '[' || line[len(line)-1] != ']' {
		return errors.New("header line should be like '[key: value]")
	}
	line = line[1 : len(line)-1]
	colonIdx := bytes.IndexByte(line, ':')
	if colonIdx < 0 {
		return errors.New("missing colon")
	}
	key := string(bytes.TrimSpace(line[:colonIdx]))
	value := string(bytes.TrimSpace(line[colonIdx+1:]))
	req.Header.Set(key, value)
	return nil
}

Напишем бенчмарк, который будет его использовать.

func BenchmarkRawWithHeadersDecode(b *testing.B) {
	for i := 0; i < b.N; i++ {
		req, _ := decodeRequest([]byte(benchTestRequest))
		for _, header := range benchTestConfigHeaders {
			decodeConfigHeader(req, header)
		}
	}
}

Запускаем

$ go test --bench=BenchmarkRawWithHeadersDecode -benchmem -cpuprofile=cpu1.out -memprofile=mem1.out
pkg: github.com/yandex/pandora/components/phttp/ammo/simple/raw
BenchmarkRawWithHeadersDecode-8   	  100000	     11220 ns/op	    5218 B/op	      17 allocs/op

11220 ns/op 5218 B/op 17 allocs/op

Стало медленнее на операцию/сек почти на 17%, хотя существенных изменений не должно быть, к тому же сильно добавилось allocs/op. Смотрим в профайлинг cpu:

(pprof) list BenchmarkRawWithHeadersDecode
Total: 1.67s
ROUTINE ======================== github.com/yandex/pandora/components/phttp/ammo/simple/raw.BenchmarkRawWithHeadersDecode in /home/ttorubarov/testssss/pandora/components/phttp/ammo/simple/raw/decoder_bench_test.go
      10ms      1.02s (flat, cum) 61.08% of Total
         .          .     23:	}
         .          .     24:}
         .          .     25:
         .          .     26:func BenchmarkRawWithHeadersDecode(b *testing.B) {
         .          .     27:	for i := 0; i < b.N; i++ {
         .      840ms     28:		req, _ := decodeRequest([]byte(benchTestRequest))
      10ms       10ms     29:		for _, header := range benchTestConfigHeaders {
         .      170ms     30:			decodeConfigHeader(req, header)
         .          .     31:		}
         .          .     32:	}
         .          .     33:}

170 мс тратится на декодинг хеадеров по сравнению с декодингом запроса. На что?

(pprof) list decodeConfigHeader
Total: 1.67s
ROUTINE ======================== github.com/yandex/pandora/components/phttp/ammo/simple/raw.decodeConfigHeader in /home/ttorubarov/testssss/pandora/components/phttp/ammo/simple/raw/decoder.go
         0      170ms (flat, cum) 10.18% of Total
         .          .     39:	line = line[1 : len(line)-1]
         .          .     40:	colonIdx := bytes.IndexByte(line, ':')
         .          .     41:	if colonIdx < 0 {
         .          .     42:		return errors.New("missing colon")
         .          .     43:	}
         .       70ms     44:	key := string(bytes.TrimSpace(line[:colonIdx]))
         .       60ms     45:	value := string(bytes.TrimSpace(line[colonIdx+1:]))
         .       40ms     46:	req.Header.Set(key, value)
         .          .     47:	return nil
         .          .     48:}

70+60ms на то, чтобы сделать trim в строке и дополнительно 40, чтобы сделать Header.Set(). Но ведь у нас заголовки читаются один раз, зачем делать trim на каждый запрос?

Работает, но всё надо переделать :) Перепишем так, чтобы заголовки конфига декодились (и делали операцию TrimSpace) только один раз, а потом только читались из готовой структуры.

Размахиваем напильником над кодом, запускаем.

$ go test --bench=Benchmark -benchmem -cpuprofile=cpu2.out -memprofile=mem2.out
pkg: github.com/yandex/pandora/components/phttp/ammo/simple/raw
BenchmarkRawDecoderWithHeaders-8   	  200000	      9765 ns/op	    5170 B/op	      12 allocs/op
(pprof) list BenchmarkRawDecoderWithHeaders
ROUTINE ======================== github.com/yandex/pandora/components/phttp/ammo/simple/raw.BenchmarkRawDecoderWithHeaders in /home/ttorubarov/testssss/pandora/components/phttp/ammo/simple/raw/decoder_bench_test.go
      20ms      1.66s (flat, cum) 29.02% of Total
         .          .     28:func BenchmarkRawDecoderWithHeaders(b *testing.B) {
         .          .     29:	b.StopTimer()
         .          .     30:	decodedHTTPConfigHeaders, _ := decodeHTTPConfigHeaders(benchTestConfigHeaders)
         .          .     31:	b.StartTimer()
         .          .     32:	for i := 0; i < b.N; i++ {
         .      1.56s     33:		req, _ := decodeRequest([]byte(benchTestRequest))
      10ms       10ms     34:		for _, header := range decodedHTTPConfigHeaders {
         .          .     35:			if header.key == "Host" {
      10ms       10ms     36:				req.URL.Host = header.value
         .          .     37:			} else {
         .       80ms     38:				req.Header.Set(header.key, header.value)
         .          .     39:			}
         .          .     40:		}
         .          .     41:	}
         .          .     42:}

9393 ns/op vs 9765 ns/op

80ms vs 1560ms

около 4%-5%, бОльшая часть которых тратится на req.Header.Set().

Clone this wiki locally