-
Notifications
You must be signed in to change notification settings - Fork 65
PR #122: реализация переопределения хедеров и ее оптимизация.md
Я делал новую функциональность для 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().