Начните с рефакторинга
Это должно начаться с оптимизации рефакторинга.
Недавно я реконструировал функцию маршрутизации.Поскольку маршрутизация более сложная и требования сильно меняются, я хочу передать责任链模式
чтобы восстановить, как раз в это времяSentinel-Go
См. соответствующий исходный код в .
Самым большим преимуществом модели цепочки ответственности является то, что вы можете гибко подключать и отключать возможности маршрутизации для каждого запроса, например:
Эта реализация будет идти на каждый запрос new
Из всей цепочки ответственности можно предвидеть, что объекты будут создаваться и уничтожаться часто.
Объединение объектов в пул не рекомендуется для Java, если только создание объекта не является особенно трудоемким, например连接对象
, в противном случае конкуренция за блокировку между потоками определенно дороже, чем прямое выделение памяти~
Но Go отличается, он имеет встроенныйsync.Pool
Совместная модель планирования (GMP) может просто избежать такого конфликта блокировок.
Все знают, что пул объектов в Go очень мощный. Конкретный принцип не является предметом этой статьи, и его нельзя четко объяснить в одном или двух предложениях. У меня есть возможность написать еще одну статью, чтобы подробно описать его ~
Но теория есть теория, это мул или лошадь, вы должны вытащить его на прогулку, чтобы узнать, действительно ли он классный~
Время ожидания истекло!
Для тестирования такой производительности Benchmark, безусловно, лучший выбор, поэтому я написал два примера для сравнения: непосредственно новые объекты и использование sync.Pool для объединения объектов.
func BenchmarkPooledObject(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
object := pool.Get().(*MyObject)
Consume(object)
// 用完了放回对象池
object.Reset()
pool.Put(object)
}
})
}
func BenchmarkNewObject(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
object := &MyObject{
Name: "hello",
Age: 2,
}
Consume(object)
}
})
}
Затем эти тестовые параметры
go test -bench=. -cpu=4 -count=2 -benchtime=10s
Вышли следующие результаты.Похоже, что прямой Новый объект быстрее, что не согласуется с теорией!
BenchmarkPooledObject-4 1000000000 6.25 ns/op
BenchmarkNewObject-4 1000000000 0.374 ns/op
Поэтому я подумал, что-то не так с моим методом тестирования?
Технология пула может сократить потребление создания и уничтожения объектов. Большая часть этого выигрывает от сокращения количества сборщиков мусора. Я запускал только 10 секунд и еще не запускал сборщик мусора?
Итак, я проверил, когда Go запускает GC, и получил следующий ответ:
- Активный вызов
runtime.GC
для запуска - Существует два типа пассивных триггеров:
- Если триггера нет более 2 минут, GC принудительно срабатывает
- Когда память увеличивается до определенного процента, запускаем сборщик мусора, например, при начальном размере кучи 4 МБ, при росте памяти на 25%, то есть до 5 МБ, запускается сборщик мусора.
Очевидно, что активный запуск не подходит, а пассивный запуск не может подтвердить темп роста, его можно добиться только принудительным запуском GC через 2 минуты, поэтому я удлинил базовое время теста и изменил его на-benchtime=150s
.
После казни я пошел заварить чай и сходил в туалет... Спустя долгое время казнь наконец закончилась, и результат был такой:
*** Test killed with quit: ran too long (11m0s).
Выполнение не удалось, и оно выполнялось 11 минут~
Я искал эту ошибку.В Интернете было сказано, что юнит-тесты и бенчмарки Go имеют тайм-ауты.По умолчанию 10 минут, которые можно пройти-timeout
модифицировать.
Но дело не в этом, дело в том, почему я поставил 150с, а прошло 11 минут?
Никаких секретов под исходным кодом
Интуиция подсказывает мне, что это непросто, либо я ошибаюсь, либо Go неправ~ К счастью, Go является открытым исходным кодом, и под исходным кодом нет никаких секретов.
После отладки и проверки кода я сначала нашел этот код
func (b *B) runN(n int) {
benchmarkLock.Lock()
defer benchmarkLock.Unlock()
defer b.runCleanup(normalPanic)
// 注意看这里,帮我们GC了
runtime.GC()
b.raceErrors = -race.Errors()
b.N = n
b.parallelism = 1
// 重置计时器
b.ResetTimer()
// 开始计时
b.StartTimer()
// 执行 benchmark 方法
b.benchFunc(b)
// 停止计时
b.StopTimer()
b.previousN = n
b.previousDuration = b.duration
b.raceErrors += race.Errors()
if b.raceErrors > 0 {
b.Errorf("race detected during execution of benchmark")
}
}
Этот код выполняет метод Benchmark, который мы определили однажды, а n — это параметр, переданный в определенный нами метод.*testing.B
Свойство в структуре.
И его время вычисления также очень разумно, оно вычисляет только время для выполнения определяемого нами метода, то есть-benchtime
Время — это только время выполнения функции, а время, затраченное фреймворком Benchmark, не учитывается.
Более разумно, чтобы фреймворк также запускал для нас GC перед выполнением метода, то есть только мусор памяти, сгенерированный при выполнении нашей функции, учитывался в нашем времени Benchmark, что очень строго.
Но это не имеет никакого отношения к провалу нашего исполнения~
Но, с одной стороны, общее время выполнения Бенчмарка должно быть больше-benchtime
установить время.
Это действительно так? Я провел две серии экспериментов и нарушил это правило:
go test -bench=. -cpu=4 -count=1 -benchtime=5s
BenchmarkPooledObject-4 793896368 7.65 ns/op
BenchmarkNewObject-4 1000000000 0.378 ns/op
PASS
ok all-in-one/go-in-one/samples/object_pool 7.890s
go test -bench=. -cpu=4 -count=1 -benchtime=10s
BenchmarkPooledObject-4 1000000000 7.16 ns/op
BenchmarkNewObject-4 1000000000 0.376 ns/op
PASS
ok all-in-one/go-in-one/samples/object_pool 8.508s
Вторая группа настроена на выполнение 10с, но общее время теста всего 8.508с.Это очень странно.Еще более странно время выполнения второго столбца результатов теста.Все они 1000000000.Неужели это такое совпадение?
С сомнениями я нашел этот основной код Benchmark:
func (b *B) launch() {
...
// 标注①
if b.benchTime.n > 0 {
// We already ran a single iteration in run1.
// If -benchtime=1x was requested, use that result.
if b.benchTime.n > 1 {
b.runN(b.benchTime.n)
}
} else {
d := b.benchTime.d
// 标注②
for n := int64(1); !b.failed && b.duration < d && n < 1e9; {
last := n
goalns := d.Nanoseconds()
prevIters := int64(b.N)
prevns := b.duration.Nanoseconds()
if prevns <= 0 {
prevns = 1
}
// 标注③
n = goalns * prevIters / prevns
// Run more iterations than we think we'll need (1.2x).
// 标注④
n += n / 5
// Don't grow too fast in case we had timing errors previously.
// 标注⑤
n = min(n, 100*last)
// Be sure to run at least one more than last time.
// 标注⑥
n = max(n, last+1)
// Don't run more than 1e9 times. (This also keeps n in int range on 32 bit platforms.)
// 标注⑦
n = min(n, 1e9)
// 标注⑧
b.runN(int(n))
}
}
b.result = BenchmarkResult{b.N, b.duration, b.bytes, b.netAllocs, b.netBytes, b.extra}
}
Сердечники помечены серийными номерами, вот пояснение:
Этикетка①: Go's Benchmark реализует два параметра, количество исполнений и лимит времени выполнения, я использую время выполнения, вы также можете использовать-benchtime=1000x
чтобы указать, что требуется 1000 тестов.
Этикетка ②: Вот условие для оценки того, достаточно ли времени, когда установлен лимит времени выполнения.Вы можете видеть, что помимо оценки времени, есть такжеn < 1e9
Предел, то есть максимальное количество выполнений1e9
, что равно 1000000000, что объясняет приведенную выше путаницу, почему время выполнения все еще меньше, чем установленное время работы. Поскольку Go ограничивает максимальное количество выполнений до 1e9, это не так много, как установлено, и существует верхний предел.
Отметьте от ③ до ⑧:
Откуда Go знает, сколько n нужно взять, если время как раз соответствует нашим настройкам?benchtime
? Ответ - искушение!
n Начать испытание с 1, и после 1 выполнения оценить n по времени выполнения.n = goalns * prevIters / prevns
, это формула оценки, targetns — заданное время выполнения (в наносекундах), prevIters — количество последних выполнений, а prevns — время последнего выполнения (в наносекундах).
В соответствии с последним временем выполнения и общим временем выполнения целевого параметра рассчитайте количество раз, которое необходимо выполнить, что, вероятно, будет таким:
目标执行次数 = 执行目标时间 / (上次执行时间 / 上次执行次数)
Упростите, чтобы получить:
目标执行次数 = 执行目标时间 * 上次执行次数 / 上次执行时间
, это не формула выше~
Для расчета целевого времени выполнения n в исходном коде также выполняется некоторая другая обработка:
- Отметка ④: Пусть фактическое количество выполнений будет примерно равно количеству запланированных исполнений.
1.2
Времена, разве это не немного смущает, если целевое время не достигнуто? Просто беги немного дольше - Примечание ⑤: Вы не можете позволить n расти слишком быстро. Установите максимальную скорость роста в 100 раз. Когда n растет слишком быстро, тестируемый метод должен иметь очень короткое время выполнения, и ошибка может быть большой. Медленный рост — это хорошо. уровень правды
- Примечание ⑥: n не может стоять на месте, независимо от того, как вы получаете +1
- Примечание ⑦: n должен установить верхний предел 1e9, чтобы предотвратить переполнение в 32-битных системах.
Принцип выполнения Go Benchmark примерно выяснен, но ответ, который нам нужен, еще не появился.
Затем я сделал отладку точек останова в Benchmark.
прежде всего-benchtime=10s
Найдите, что предварительный рост n равен 1, 100, 10000, 1000000, 100000000, 1000000000 и, наконец, n равен 1000000000.
Это показывает, что наш метод выполнения занимает очень короткое время, а количество выполнений достигло верхнего предела.
посмотри снова-benchtime=150s
, который начинается нормально:
n рост равен 1, 100, 10000, 1000000, 100000000, но с последним есть проблема:
n оказалось отрицательным числом! Видимо это перелив.
n = targetns * prevIters / prevns Эта формула вызовет переполнение n, когда целевое время выполнения (goalns) велико, а время выполнения тестового метода (prevns) очень мало!
Каковы последствия переполнения?
Назадn = min(n, 100*last)
отрицательное число, но естьn = max(n, last+1)
Гарантировано, поэтому n все еще увеличивается, но очень медленно, только +1 каждый раз, поэтому последовательность n последующих испытаний равна 100000001, 100000002, 100000003....
Это затрудняет достижение n верхнего предела 1e9, а общее время выполнения трудно достичь установленного ожидаемого времени, поэтому тестовая программа будет продолжать работать до тех пор, пока не истечет время ожидания!
Это наверное баг, да?
Автор, написавший эту логику Benchamrk, добавил верхний предел числа выполнений 1e9 с учетом переполнения, но не учел переполнение n в процессе расчета.
Я чувствую, что это должно быть ошибкой, но не совсем уверен.
В Интернете не было найдено соответствующего отчета об ошибке, поэтому я пошел к официальному лицу Go, чтобы сообщить о проблеме и соответствующем коде исправления.Из-за сложного и длительного процесса разработки Go, когда эта статья была опубликована, официальный представитель не указал четко была ли это ошибка или что-то еще.
Если будет последующий официальный ответ или другие изменения, я сообщу вам снова~
Ищите и следите за общедоступной учетной записью WeChat «мастер ошибок», обмен внутренними технологиями, проектирование архитектуры, оптимизация производительности, чтение исходного кода, устранение неполадок и практика.