Низкая производительность html/ шаблона в Go lang, любой обходной путь?
Я тестирую (с loader.io) этот тип кода в Go, чтобы создать массив из 100 элементов вместе с некоторыми другими базовыми переменными и проанализировать их все в шаблоне:
package main
import (
"html/template"
"net/http"
)
var templates map[string]*template.Template
// Load templates on program initialisation
func init() {
if templates == nil {
templates = make(map[string]*template.Template)
}
templates["index.html"] = template.Must(template.ParseFiles("index.html"))
}
func handler(w http.ResponseWriter, r *http.Request) {
type Post struct {
Id int
Title, Content string
}
var Posts [100]Post
// Fill posts
for i := 0; i < 100; i++ {
Posts[i] = Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
}
type Page struct {
Title, Subtitle string
Posts [100]Post
}
var p Page
p.Title = "Index Page of My Super Blog"
p.Subtitle = "A blog about everything"
p.Posts = Posts
tmpl := templates["index.html"]
tmpl.ExecuteTemplate(w, "index.html", p)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8888", nil)
}
Мой тест с Loader использует 5k одновременных соединений в секунду в течение 1 минуты. Проблема в том, что через несколько секунд после запуска теста я получаю высокую среднюю задержку (почти 10 с), и в результате 5000 успешных ответов, и тест останавливается, потому что он достигает 50% ошибок (тайм-ауты).
На той же машине PHP дает 50к +.
Я понимаю, что это не проблема производительности Go, но, вероятно, что-то связано с HTML / шаблона. Go может легко управлять достаточно сложными вычислениями намного быстрее, чем что-либо вроде PHP, но когда дело доходит до анализа данных в шаблоне, почему это так ужасно?
Есть обходные пути, или, возможно, я просто делаю это неправильно (я новичок в Go)?
PS На самом деле даже с 1 предметом все точно так же... 5-6к и останавливается после огромного количества таймаутов. Но это, вероятно, потому что массив с постами остается той же длины.
Мой шаблон кода (index.html):
{{ .Title }}
{{ .Subtitle }}
{{ range .Posts }}
{{ .Title }}
{{ .Content }}
{{ end }}
Вот результат профилирования github.com/pkg/profile:
root@Test:~# go tool pprof app /tmp/profile311243501/cpu.pprof
Possible precedence issue with control flow operator at /usr/lib/go/pkg/tool/linux_amd64/pprof line 3008.
Welcome to pprof! For help, type 'help'.
(pprof) top10
Total: 2054 samples
97 4.7% 4.7% 726 35.3% reflect.Value.call
89 4.3% 9.1% 278 13.5% runtime.mallocgc
85 4.1% 13.2% 86 4.2% syscall.Syscall
66 3.2% 16.4% 75 3.7% runtime.MSpan_Sweep
58 2.8% 19.2% 1842 89.7% text/template.(*state).walk
54 2.6% 21.9% 928 45.2% text/template.(*state).evalCall
51 2.5% 24.3% 53 2.6% settype
47 2.3% 26.6% 47 2.3% runtime.stringiter2
44 2.1% 28.8% 149 7.3% runtime.makeslice
40 1.9% 30.7% 223 10.9% text/template.(*state).evalField
Это результаты профилирования после уточнения кода (как предложено в ответе icza):
root@Test:~# go tool pprof app /tmp/profile501566907/cpu.pprof
Possible precedence issue with control flow operator at /usr/lib/go/pkg/tool/linux_amd64/pprof line 3008.
Welcome to pprof! For help, type 'help'.
(pprof) top10
Total: 2811 samples
137 4.9% 4.9% 442 15.7% runtime.mallocgc
126 4.5% 9.4% 999 35.5% reflect.Value.call
113 4.0% 13.4% 115 4.1% syscall.Syscall
110 3.9% 17.3% 122 4.3% runtime.MSpan_Sweep
102 3.6% 20.9% 2561 91.1% text/template.(*state).walk
74 2.6% 23.6% 337 12.0% text/template.(*state).evalField
68 2.4% 26.0% 72 2.6% settype
66 2.3% 28.3% 1279 45.5% text/template.(*state).evalCall
65 2.3% 30.6% 226 8.0% runtime.makeslice
57 2.0% 32.7% 57 2.0% runtime.stringiter2
(pprof)
6 ответов
Есть две основные причины, по которым эквивалентное приложение использует html/template
медленнее, чем вариант PHP.
Прежде всего html/template
обеспечивает больше функциональности, чем PHP. Основное отличие состоит в том, что html/template
будет автоматически экранировать переменные, используя правильные правила экранирования (HTML, JS, CSS и т. д.), в зависимости от их расположения в итоговом HTML-выводе (что я считаю довольно классным!).
во-вторых html/template
при рендеринге кода интенсивно используются отражения и методы с переменным числом аргументов, и они не так быстры, как статически скомпилированный код.
Под капотом следующий шаблон
{{ .Title }}
{{ .Subtitle }}
{{ range .Posts }}
{{ .Title }}
{{ .Content }}
{{ end }}
превращается в нечто подобное
{{ .Title | html_template_htmlescaper }}
{{ .Subtitle | html_template_htmlescaper }}
{{ range .Posts }}
{{ .Title | html_template_htmlescaper }}
{{ .Content | html_template_htmlescaper }}
{{ end }}
призвание html_template_htmlescaper
использование отражения в цикле убивает производительность.
Сказав все, что это микро-эталон html/template
не должен использоваться, чтобы решить, использовать ли Go или нет. Когда вы добавляете код для работы с базой данных в обработчик запросов, я подозреваю, что время визуализации шаблона вряд ли будет заметно.
Также я почти уверен, что со временем html/template
Пакет станет быстрее.
Если в реальном приложении вы найдете, что html/template
является узким местом, все еще можно переключиться на text/template
и снабдить его уже сбежавшими данными.
Вы работаете с массивами и структурами, которые не являются указателями, а также не являются дескрипторами (например, срезами, картами или каналами). Поэтому их передача всегда создает копию значения, а присвоение значения массива переменной копирует все элементы. Это медленно и дает огромный объем работы для GC.
Также вы используете только 1 ядро процессора. Чтобы использовать больше, добавьте это в свой main()
функция:
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8888", nil))
}
Изменить: это было только в случае до Go 1.5. С Go 1.5 runtime.NumCPU()
по умолчанию.
Ваш код
var Posts [100]Post
Массив с пространством для 100 Post
с выделяется.
Posts[i] = Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
Вы создаете Post
значение с составным литералом, то это значение копируется в i
й элемент в массиве. (Резервный)
var p Page
Это создает переменную типа Page
, Это struct
, так что его память выделена, которая также содержит поле Posts [100]Post
так еще один массив 100
элементы выделяются.
p.Posts = Posts
Это копии 100
элементы (сто структур)!
tmpl.ExecuteTemplate(w, "index.html", p)
Это создает копию p
(который имеет тип Page
), поэтому еще один массив 100
сообщения созданы и элементы из p
копируются, а затем передаются ExecuteTemplate()
,
И с тех пор Page.Posts
это массив, скорее всего, когда он обрабатывается (повторяется в шаблонизаторе), копия будет сделана из каждого элемента (не проверено - не проверено).
Предложение по более эффективному коду
Некоторые вещи для ускорения вашего кода:
func handler(w http.ResponseWriter, r *http.Request) {
type Post struct {
Id int
Title, Content string
}
Posts := make([]*Post, 100) // A slice of pointers
// Fill posts
for i := range Posts {
// Initialize pointers: just copies the address of the created struct value
Posts[i]= &Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
}
type Page struct {
Title, Subtitle string
Posts []*Post // "Just" a slice type (it's a descriptor)
}
// Create a page, only the Posts slice descriptor is copied
p := Page{"Index Page of My Super Blog", "A blog about everything", Posts}
tmpl := templates["index.html"]
// Only pass the address of p
// Although since Page.Posts is now just a slice, passing by value would also be OK
tmpl.ExecuteTemplate(w, "index.html", &p)
}
Пожалуйста, проверьте этот код и сообщите свои результаты.
html/template
медленный, потому что он использует отражение, которое еще не оптимизировано для скорости.
Попробуйте быстрый шаблон как обходной путь медленного html/template
, В настоящее время quicktemplate
более чем в 20 раз быстрее, чем html/template
в соответствии с тестом из его исходного кода.
PHP не отвечает на 5000 запросов одновременно. Запросы мультиплексируются в несколько процессов для последовательного выполнения. Это позволяет более эффективно использовать как процессор, так и память. 5000 одновременных соединений может иметь смысл для брокера сообщений или аналогичного, выполняющего ограниченную обработку небольших фрагментов данных, но это не имеет особого смысла для любого сервиса, выполняющего реальный ввод-вывод или обработку. Если ваше приложение Go не находится за прокси-сервером какого-либо типа, который будет ограничивать количество одновременных запросов, вы можете сделать это самостоятельно, возможно, в начале вашего обработчика, используя буферизованный канал или группу ожидания, например https://blakemesdag.com/blog/2014/11/12/limiting-go-concurrency/.
На сайте goTemplateBenchmark есть тестовый шаблон. Лично я считаю, что Hero лучше всего сочетает в себе эффективность и удобочитаемость.
Типизированные строки — ваш друг, если вы хотите ускорить
html/template
. Иногда бывает полезно предварительно отрендерить повторяющиеся HTML-фрагменты.
Предполагая, что большая часть времени тратится на рендеринг этих 100 объектов Post, может иметь смысл выполнить их предварительный рендеринг.