🐳 Container-aware GOMAXPROCS: Go наконец-то видит лимиты контейнеров
Пост в официальном блоге разработчиков Go, где они подробно рассказывают суть проблемы и как устроено недавнее техническое решение.
Go Team решили давнюю проблему совместимости с контейнерами. До версии 1.25 Go определял все ядра хоста, не учитывая CPU limits контейнера. Теперь GOMAXPROCS выставляется в соответствии с лимитами автоматически.
Суть проблемы:
- Контейнеру выделено 2 CPU на машине с 128 ядрами
- Go видит 128 ядер, создаёт кучу тредов
- Linux throttling тормозит приложение каждые 100ms
- Tail latency растёт
Что изменилось в 1.25:
- Go читает cgroup CPU limits и ставит GOMAXPROCS по ним
- Динамически обновляется при изменении лимитов
- Работает из коробки — просто обновите go.mod
- CPU requests игнорируются (только limits)
————
Наконец-то.. Проблеме много лет, и всё это время приходилось костылить через uber/automaxprocs или ENV-переменные. А теперь оно работает из коробки, как и должно было уже давно.
P.S. В Java эту проблему решили ещё в 2017 году😩
#go1_25 #go_official #kubernetes #docker
Пост в официальном блоге разработчиков Go, где они подробно рассказывают суть проблемы и как устроено недавнее техническое решение.
Go Team решили давнюю проблему совместимости с контейнерами. До версии 1.25 Go определял все ядра хоста, не учитывая CPU limits контейнера. Теперь GOMAXPROCS выставляется в соответствии с лимитами автоматически.
Суть проблемы:
- Контейнеру выделено 2 CPU на машине с 128 ядрами
- Go видит 128 ядер, создаёт кучу тредов
- Linux throttling тормозит приложение каждые 100ms
- Tail latency растёт
Что изменилось в 1.25:
- Go читает cgroup CPU limits и ставит GOMAXPROCS по ним
- Динамически обновляется при изменении лимитов
- Работает из коробки — просто обновите go.mod
- CPU requests игнорируются (только limits)
————
Наконец-то.. Проблеме много лет, и всё это время приходилось костылить через uber/automaxprocs или ENV-переменные. А теперь оно работает из коробки, как и должно было уже давно.
P.S. В Java эту проблему решили ещё в 2017 году
#go1_25 #go_official #kubernetes #docker
Please open Telegram to view this post
VIEW IN TELEGRAM
go.dev
Container-aware GOMAXPROCS - The Go Programming Language
New GOMAXPROCS defaults in Go 1.25 improve behavior in containers.
❤20🔥18🤯2🤔1
Николай Тузов
https://youtu.be/fHuJNsZPCJ0
Этого ролика вам точно будет достаточно для полного понимания нововведений, вне зависимости от вашего опыта
Подробно обсудили, что нового в новой версии, зачем это нужно, как оно устроено, в сложных местах делали ликбез для лучшего понимания изменений.
Дима был очень хорош, такого подробного и глубокого разбора вы больше нигде не увидите, а с комментариями Глеба оно ещё круче, очень рекомендую.
#gogetpodcast #podcast
Please open Telegram to view this post
VIEW IN TELEGRAM
YouTube
Go v1.25 — Глубокий разбор всех изменений | GoGetPodcast №17
Подробно обсудили - что добавилось в новой версии Go 1.25, зачем всё это нужно, как с этим работать.
Go 1.25 Release Notes: https://tip.golang.org/doc/go1.25
Состав:
- Николай Тузов
- Глеб Яльчик
- Дмтрий Матрёничев, ведёт шикарный ТГ-канал по Go: http…
Go 1.25 Release Notes: https://tip.golang.org/doc/go1.25
Состав:
- Николай Тузов
- Глеб Яльчик
- Дмтрий Матрёничев, ведёт шикарный ТГ-канал по Go: http…
🔥20👍6❤3
Forwarded from Go Update
🏗️ gogrep — инструмент для семантического поиска внутри вашей кодовой базы. 🏗️
Наверняка много кому приходилось сталкиваться с ситуацией «надо по коду найти вызовы по определенному паттерну». Чаще всего для этого мы используем регулярные выражения (grep, ag, rg). Однако у них всех есть один минус — они интерпретируют файлы как текст, а не как код. Из-за этого в результаты поиска попадают как нужные нам места вызова, так и комментарии, участки текста и прочая.
Решение этой проблемы: семантический поиск. Это когда утилита разбивает файл на синтаксическое дерево и производит поиск уже по самому дереву. Приведу простой пример:
Здесь мы явно говорим: найди все вызовы, у которых слева есть идентификатор
Документация по gogrep доступна тут. С описанием синтаксиса немного сложнее: большую (и лучшую) часть информации по мэтчингу и по фильтрам можно найти в тестах. Сама тулза является часть куда более мощной тулзы go-ruleguard (которая кстати входит в golangci-lint).
За обе утилиты огромнейшее спасибо Искандеру Шарипову.
Наверняка много кому приходилось сталкиваться с ситуацией «надо по коду найти вызовы по определенному паттерну». Чаще всего для этого мы используем регулярные выражения (grep, ag, rg). Однако у них всех есть один минус — они интерпретируют файлы как текст, а не как код. Из-за этого в результаты поиска попадают как нужные нам места вызова, так и комментарии, участки текста и прочая.
Решение этой проблемы: семантический поиск. Это когда утилита разбивает файл на синтаксическое дерево и производит поиск уже по самому дереву. Приведу простой пример:
~/mws/api > gogrep . 'ptr.Get($_)'
Здесь мы явно говорим: найди все вызовы, у которых слева есть идентификатор
ptr
а внутри вызова идет только один аргумент (не важно выражение, их сумма, или переменная). Таким образом мы идентифицируем именно конкретные места которые будут частью компилируемого кода.Документация по gogrep доступна тут. С описанием синтаксиса немного сложнее: большую (и лучшую) часть информации по мэтчингу и по фильтрам можно найти в тестах. Сама тулза является часть куда более мощной тулзы go-ruleguard (которая кстати входит в golangci-lint).
За обе утилиты огромнейшее спасибо Искандеру Шарипову.
GitHub
GitHub - quasilyte/gogrep: Syntax-aware Go code search, based on the mvdan/gogrep
Syntax-aware Go code search, based on the mvdan/gogrep - quasilyte/gogrep
🔥11👍1
Go Update
🚀 Расширение функции new для создания указателя на значения 🚀 Отличные новости! Предложение, обсуждение которого которого длится уже больше четырех лет и которое выдвинул сам Роб Пайк, наконец-то подходит к принятию. В чем суть: есть у нас встроенная функция…
Наконец-то можно будет выкинуть из проектов мой "любимый" lo.ToPtr()
Правда остаётся ещё lo.FromPtr(), но надежда уже есть.
Если что, мне жутко не нравится идея тащить в проект samber/lo, но не всегда удаётся с этим бороться. При этом, я даже не знаю что хуже — тащить этого монстра ради пары строк кода, или же желание использовать ещё больше его функционала😩
К слову, в предыдущем выпуске подкаста тема `lo` очень подробно обсуждалась в самом конце. Советую послушать, если нужны аргументы.
Правда остаётся ещё lo.FromPtr(), но надежда уже есть.
Если что, мне жутко не нравится идея тащить в проект samber/lo, но не всегда удаётся с этим бороться. При этом, я даже не знаю что хуже — тащить этого монстра ради пары строк кода, или же желание использовать ещё больше его функционала
К слову, в предыдущем выпуске подкаста тема `lo` очень подробно обсуждалась в самом конце. Советую послушать, если нужны аргументы.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥3❤2
Forwarded from Go Update
✔️ errors.AsType — типобезопастная замена errors.As ✔️
Тем временем, в 1.26 нас (вероятно) ждет еще одно приятное изменение: дженерики наконец доберутся до пакета errors.
Все изменение проще описать тремя строчками кода. В 1.25 у нас вот так:
А в 1.26 можно будет вот так:
Вроде и небольшое изменение, но оно ведет, как минимум, к двум положительным вещам:
• Зона видимости типизированной ошибки во многих участках у нас теперь будет меньше, а значит меньше захламляется пространство имен и снижается необходимость думать над правильным именем для ошибки.
• В отличии от errors.As, который вторым аргументом принимал
Кстати, причина по которой сигнатура текущей функции выглядит как
компиляцию не пройдет. А причина в том, что интерфейсы у нас это отдельная сущность которая существует не только во время компиляции, но и во время выполнения.
Тем временем, в 1.26 нас (вероятно) ждет еще одно приятное изменение: дженерики наконец доберутся до пакета errors.
Все изменение проще описать тремя строчками кода. В 1.25 у нас вот так:
var pe *fs.PathError
if errors.As(err, &pe) {
fmt.Println("Failed at path:", pe.Path)
}
А в 1.26 можно будет вот так:
if pe, ok := errors.AsType[*fs.PathError](err); ok {
fmt.Println("Failed at path:", pe.Path)
}
Вроде и небольшое изменение, но оно ведет, как минимум, к двум положительным вещам:
• Зона видимости типизированной ошибки во многих участках у нас теперь будет меньше, а значит меньше захламляется пространство имен и снижается необходимость думать над правильным именем для ошибки.
• В отличии от errors.As, который вторым аргументом принимал
any
, новая функция принимает только тех, кто реализует интерфейс error
. Несмотря на то, что у нас есть проверка внутри go vet
проверяющая второй аргумент у As
, всегда приятнее когда компилятор может самостоятельно поймать ошибку на этапе сборки приложения.Кстати, причина по которой сигнатура текущей функции выглядит как
As(err error, target any) bool
заключается в том, что указатель на интерфейс и указатель на тип реализующий интерфейс для компилятора две несовместимые конструкции. Иначе говоря, вот такой код
func As(err error, target *error) bool {
panic("unimplemented")
}
…
pe *fs.PathError
if As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
}
…
компиляцию не пройдет. А причина в том, что интерфейсы у нас это отдельная сущность которая существует не только во время компиляции, но и во время выполнения.
GitHub
errors: AsType (As with type parameters) · Issue #51945 · golang/go
Currently in 1.18 and before, when using the errors.As method, an error type you would like to write into must be predeclared before calling the function. For example: var myErr *MyCustomError if e...
🔥30❤3
Forwarded from go-with-me
В предыдущем посте мы немного напутали с определением такого многопоточного паттерна как Fan Out
На деле мы показывали Tee, который распространяет одно и то же значение V из канала-источника на N каналов-потребителей
Отличие Fan Out от Tee в том, что на N каналов распространяются разные значения из одного канала-источника. Тобишь, воркеры тянут значения из одного канала, борясь за них насмерть
Лирическое отступление закончено, наша совесть чиста, а сегодняшняя тема будет посвящена Fan In
Этот паттерн является обратным для Fan-Out. Мы собираем данные из нескольких каналов-источников и направляем их в один общий канал-потребитель
1. Default
Итак, что мы имеем?
— Есть воркеры — они кладут значения в N каналов и являются продьюсерами
— Каждый из этих N каналов будет получать значения от своего продьюсера. Назовем такие каналы "стоковыми"
— Есть один общий канал out, туда будет нужно отправить все значения из стоковых каналов
— Для этого мы запускаем N потоков, каждый из которых слушает свой стоковый канал, куда кладет значения продьюсер и редиректит все значения в out
Playground пример
Но что же будет, если какой-то наш продьюсер потух и больше не шлет никаких значений, а передаггый контекст не отменен? Как бы нам понять, что воркер не является активным и перезапустить его? — в этом нам поможет такой механизм как "Heartbeats"
Heartbeat — это регулярное сообщение от продьюсера/воркера, подтверждающее, что он жив и работает
2. Heartbeats
Приступим к рассмотрению этого чуда!
Основная идея проста:
— Имеем структуру, которая хранит в себе стоковый канал, используемый как пайп между воркером и стоком, и канал "сердцебиений"
— Функция Supervise ответственна за отслеживание "сердцебиений" и перезапуск воркера при их отсутствии по TTL
— Функция FanIn принимает на вход стоковые каналы и возвращает результирующий канал, из которого можно читать данные
Всмотримся в наши функции поподробнее
2.1. FanIn
— Не отклоняемся от цели: выкачиваем данные из стоковых каналов и перекладываем в out, реагируя на контекст и неблокирующе отправляя "сердцебиение" нашему супервизору, который пристально наблюдает за нашими воркерами
— WaitGroup здесь так же используется для того, чтобы дождаться конца работы наших стоков и отдать управление основному потоку
после дренажа всех "живых" значений
2.2 Supervise
— Создаем стоковый канал и канал "сердцебиений", агрегируем эти значения в структуре Source и возвращаем ее
— В отдельном потоке запускаем нашу рутину по отслеживанию и перезапуску воркеров
2.2.1 Смотрим на внутренности запущенного потока внутри Supervise
— Изначально происходит создание дочернего контекста с отменой для нашего воркера. Этот контекст будет рулить в тот момент, когда наш TTL пройдет и надо будет потушить воркера
— Создаем ticker, который будет слать ивенты, семантически значащие следующее: "в нашего воркера стреляли и он упал в лужу на..."
После получения ивента мы отменяем контекст и воркер окончательно "задыхается в луже"
— Первично запускаем работягу в отдельном потоке
— Если ловим ивент от тикера: производим отмену контекста, переназначаем этот же контекст и функцию отмены, сбрасываем таймер, и запускаем нового воркера в отдельном потоке
— В случае, когда из стокового потока нам пришло "сердцебиение" мы просто сбрасываем таймер и движемся дальше!
Стоит отметить, что воркеры должны "реагировать" на переданный контекст. Без этого мы получим утечку потоков и черт его знает, чем нам это грозит (профилированием и
устранением проблемы, которой можно было бы и избежать)
Таким образом, мы получаем более надежный Fan In, где все источники данных контролируемы и восстанавливаемы при зависаниях
Playground пример
Статью писали с Дашей: @dariasroom
Stay tuned
Please open Telegram to view this post
VIEW IN TELEGRAM
2👍10❤6🔥4🤯1
conc — удобные примитивы для конкурентного кода
https://github.com/sourcegraph/conc
⭐️ ~10.1k
Небольшая библиотека от Sourcegraph, которая предлагает более безопасные и удобные обёртки над стандартными инструментами конкурентности в Go.
Впервые встретил её на своей текущей работе, и в целом мне понравилось, поэтому грех не поделиться.
Что в ней есть интересного:
- conc.WaitGroup — альтернатива стандартному sync.WaitGroup: ловит паники в горутинах и возвращает их в Wait(), а также не даёт забыть вызвать Wait()
- pool — конкурентный пул задач с ограничением числа горутин (WithMaxGoroutines), сбором ошибок (WithErrors, WithFirstError) и отменой задач при ошибках (WithContext)
- stream — параллельное выполнение задач с сохранением порядка результатов (удобно, когда порядок важен, но хочется параллелизма)
- iter.Map / ForEach — упрощённые хелперы для конкурентной обработки слайсов
🟠 Библиотека пока pre-1.0 (последний релиз в феврале 2023), API может меняться.
#library #concurrency #goroutines
https://github.com/sourcegraph/conc
⭐️ ~10.1k
Небольшая библиотека от Sourcegraph, которая предлагает более безопасные и удобные обёртки над стандартными инструментами конкурентности в Go.
Впервые встретил её на своей текущей работе, и в целом мне понравилось, поэтому грех не поделиться.
Что в ней есть интересного:
- conc.WaitGroup — альтернатива стандартному sync.WaitGroup: ловит паники в горутинах и возвращает их в Wait(), а также не даёт забыть вызвать Wait()
- pool — конкурентный пул задач с ограничением числа горутин (WithMaxGoroutines), сбором ошибок (WithErrors, WithFirstError) и отменой задач при ошибках (WithContext)
- stream — параллельное выполнение задач с сохранением порядка результатов (удобно, когда порядок важен, но хочется параллелизма)
- iter.Map / ForEach — упрощённые хелперы для конкурентной обработки слайсов
#library #concurrency #goroutines
Please open Telegram to view this post
VIEW IN TELEGRAM
GitHub
GitHub - sourcegraph/conc: Better structured concurrency for go
Better structured concurrency for go. Contribute to sourcegraph/conc development by creating an account on GitHub.
❤15👍11🤯2
Как я пишу HTTP сервисы на Go спустя 13 лет
How I write HTTP services in Go after 13 years
Мэт Райер (Grafana, Go Time podcast) делится опытом, как он пишет HTTP-сервисы после 13 лет работы с Go.
Ключевая мысль статьи: не нужен ни фреймворк, ни DI-магия, всё решается стандартной библиотекой и явными зависимостями.
TL;DR:
-
- Все роуты в одном месте (
-
- Тесты поднимают сервис целиком (с
- Хэндлеры собираются фабриками, middleware пишутся обычными функциями func(h http.Handler) http.Handler — всё просто и прозрачно.
————
🟢 Что я могу сказать.. Такое ощущение, будто статью писал я сам. Хочу, чтобы каждый мой коллега прочитал её дважды ! Я устал регулярно объяснять и защищать все те простые вещи, о которых пишет автор.
Мне даже 13 лет на это не понадобилось, просто учителя были хорошие👴
Единственное, что вызывает вопросы — это «nil-зависимости» в тестах (автор иногда передаёт nil, если зависимость не используется). Я бы всё же предпочёл простые no-op фейки, чтобы не ловить паники внезапно, пусть даже в тестах — поверьте, фиксить тесты без должной гигиены та ещё морока. Пусть сегодня в конкретном кейсе дело не доходит до какой-то зависимости, но завтра дойдёт.
В остальном подход отличный: код простой и читаемый, тесты пишутся легко, нет магии. Отличная статья, особенно для тех, кто только ещё не набил руку в архитектуре сервисов на Go❤️
Для новичков must read, опытным товарищам тоже лишним не будет ознакомиться.
#article #http #architecture #english
How I write HTTP services in Go after 13 years
Мэт Райер (Grafana, Go Time podcast) делится опытом, как он пишет HTTP-сервисы после 13 лет работы с Go.
Ключевая мысль статьи: не нужен ни фреймворк, ни DI-магия, всё решается стандартной библиотекой и явными зависимостями.
TL;DR:
-
NewServer(...)
принимает все зависимости конструктором и возвращает http.Handler
. Да, список аргументов может быть длинным, и это нормально — зато всё явно.- Все роуты в одном месте (
routes.go
), никакой путаницы.-
main()
тонкий, реальная логика в run(ctx, ...)
— удобно и для тестов, и для graceful shutdown.- Тесты поднимают сервис целиком (с
/readyz
и прочим), а окружение передаётся как параметры функции, без обращения к глобальному состоянию (os.Getenv
, flag
, os.Stdin/Stdout
)- Хэндлеры собираются фабриками, middleware пишутся обычными функциями func(h http.Handler) http.Handler — всё просто и прозрачно.
————
Мне даже 13 лет на это не понадобилось, просто учителя были хорошие
Единственное, что вызывает вопросы — это «nil-зависимости» в тестах (автор иногда передаёт nil, если зависимость не используется). Я бы всё же предпочёл простые no-op фейки, чтобы не ловить паники внезапно, пусть даже в тестах — поверьте, фиксить тесты без должной гигиены та ещё морока. Пусть сегодня в конкретном кейсе дело не доходит до какой-то зависимости, но завтра дойдёт.
В остальном подход отличный: код простой и читаемый, тесты пишутся легко, нет магии. Отличная статья, особенно для тех, кто только ещё не набил руку в архитектуре сервисов на Go
Для новичков must read, опытным товарищам тоже лишним не будет ознакомиться.
#article #http #architecture #english
Please open Telegram to view this post
VIEW IN TELEGRAM
Grafana Labs
How I write HTTP services in Go after 13 years | Grafana Labs
Mat Ryer, principal engineer at Grafana Labs and host of the Go Time podcast, shares what he's learned from more than a dozen years of writing HTTP services in Go.
1🔥44❤8🤔7👍4
🔍 Утечка горутин в продакшене — разбор реального кейса
- Оригинальный пост
- Обсуждение на Hacker News
Автор рассказывает про классическую production-проблему: out of memory в k8s-подах посреди во время работы сервиса.
Спойлер: виноваты горутины, которые живут вечно.
Как искали проблему:
- Grafana показала растущее потребление памяти горутинами
- Подключили pprof и какое-то время мониторили результаты
- Нашли фабрику бесконечных горутин🙃
Виновник — функция конвертации канала:
Проблема:
Инструменты для поиска утечек:
- Grafana + Pyroscope — flame-графы памяти
- pprof с diff_base — сравнение снапшотов до/после
- goleak — для автоматического обнаружения в тестах
Главная мысль от автора: не конвертируйте каналы!
Используйте обычные паттерны работы с
————
Что ж, вот вам ещё одно напоминание о том, что Go хоть и простой язык, но конкурентность в нём кусается как везде. И go func() — это не fire-and-forget, как кажется новичкам, а обязательство следить за временем жизни горутины. Опытный разработчик 10 раз подумает перед тем как использовать эту конструкцию где бы то ни было.
#article #concurrency #english
- Оригинальный пост
- Обсуждение на Hacker News
Автор рассказывает про классическую production-проблему: out of memory в k8s-подах посреди во время работы сервиса.
Спойлер: виноваты горутины, которые живут вечно.
Как искали проблему:
- Grafana показала растущее потребление памяти горутинами
- Подключили pprof и какое-то время мониторили результаты
- Нашли фабрику бесконечных горутин
Виновник — функция конвертации канала:
func ToDoneInterface(done <-chan struct{}) <-chan interface{} {
interfaceStream := make(chan interface{})
go func() {
defer close(interfaceStream)
select {
case <-done: return
}
}()
return interfaceStream
}
Проблема:
select{}
ждёт сигнала из done
, но если контекст никогда не отменяется — горутина блокируется навсегда. defer close()
там есть, но он не поможет, если select никогда не вернётся.Инструменты для поиска утечек:
- Grafana + Pyroscope — flame-графы памяти
- pprof с diff_base — сравнение снапшотов до/после
- goleak — для автоматического обнаружения в тестах
Главная мысль от автора: не конвертируйте каналы!
Используйте обычные паттерны работы с
context.Context
и не забывайте вызывать .Done()
там, где нужно.————
Что ж, вот вам ещё одно напоминание о том, что Go хоть и простой язык, но конкурентность в нём кусается как везде. И go func() — это не fire-and-forget, как кажется новичкам, а обязательство следить за временем жизни горутины. Опытный разработчик 10 раз подумает перед тем как использовать эту конструкцию где бы то ни было.
#article #concurrency #english
Please open Telegram to view this post
VIEW IN TELEGRAM
👍25❤9🔥4
Насколько быстр Go? Симуляция миллионов частиц на смарт-ТВ
https://habr.com/ru/articles/953434/
Автор проверил производительность Go на практике — написал симуляцию миллионов частиц с мультиплеером, которая работает только на CPU, причём настолько легковесно, что запускается даже на смарт-телевизоре.
Задача: симулировать миллионы частиц, синхронизировать их между сотнями клиентов через WebSocket, но при этом не заставлять клиент ничего симулировать — только рисовать.
Подход как из GPU-программирования:
- Используется g-buffer из deferred shading: как там отвязывают количество полигонов от расчёта освещения, так и здесь количество частиц отвязывается от объёма передаваемых данных
- Сервер рендерит всё сам и отправляет клиентам готовые кадры-буферы
- Клиент просто рисует пиксели в canvas — работает везде, где есть браузер
- Стоимость передачи фиксирована разрешением экрана, а не числом частиц
Математика:
Для разрешения 1920×1080 при 1 бите на пиксель получается примерно 260 КБ на кадр. При 30 fps это около 7.8 МБ/с (≈ 62 Мбит/с) на одного клиента. На сервере с пропускной способностью 2.5 Гбит/с можно теоретически обслужить порядка 40 клиентов при Full HD, или 300+ клиентов при мобильных разрешениях (например, 640×360).
Оптимизации:
- Воркер-пулы вместо создания горутин в цикле — pprof показал, что создание горутин не бесплатное
- Lookup-таблицы для побитовых операций вместо прямой упаковки/распаковки — ускорило в разы
- Простой трюк с
- Двойная буферизация кадров, каналы для неблокирующей отправки, пул буферов
Результат: 2.5 миллиона частиц на 60 fps симуляции + 30 fps отправка клиентам. Работает на смарт-ТВ.
Попробовать можно тут: howfastisgo.dev
————
Статья очень крутая, рекомендую.
Автор честно признаётся в разочаровании: Go без SIMD в чистых вычислениях проигрывает даже JavaScript, не говоря уж про Rust (который на одном ядре обошёл Go на восьми).
Но Go и не позиционируется как язык для тяжёлых вычислений — он про сетевые сервисы и конкурентность.
Мне понравился подход с g-buffer'ом — изящное решение проблемы масштабирования. Дельта-кодирование с зигзаг-кодированием в итоге забросили из-за сложности кода, но это правильный выбор — иногда простота важнее оптимальности.
#article #performance #websocket
https://habr.com/ru/articles/953434/
Автор проверил производительность Go на практике — написал симуляцию миллионов частиц с мультиплеером, которая работает только на CPU, причём настолько легковесно, что запускается даже на смарт-телевизоре.
Задача: симулировать миллионы частиц, синхронизировать их между сотнями клиентов через WebSocket, но при этом не заставлять клиент ничего симулировать — только рисовать.
Подход как из GPU-программирования:
- Используется g-buffer из deferred shading: как там отвязывают количество полигонов от расчёта освещения, так и здесь количество частиц отвязывается от объёма передаваемых данных
- Сервер рендерит всё сам и отправляет клиентам готовые кадры-буферы
- Клиент просто рисует пиксели в canvas — работает везде, где есть браузер
- Стоимость передачи фиксирована разрешением экрана, а не числом частиц
Математика:
Для разрешения 1920×1080 при 1 бите на пиксель получается примерно 260 КБ на кадр. При 30 fps это около 7.8 МБ/с (≈ 62 Мбит/с) на одного клиента. На сервере с пропускной способностью 2.5 Гбит/с можно теоретически обслужить порядка 40 клиентов при Full HD, или 300+ клиентов при мобильных разрешениях (например, 640×360).
Оптимизации:
- Воркер-пулы вместо создания горутин в цикле — pprof показал, что создание горутин не бесплатное
- Lookup-таблицы для побитовых операций вместо прямой упаковки/распаковки — ускорило в разы
- Простой трюк с
p := &particles[i]
вместо particles[i].x
дал +30% за счёт меньшего числа bounds checks- Двойная буферизация кадров, каналы для неблокирующей отправки, пул буферов
Результат: 2.5 миллиона частиц на 60 fps симуляции + 30 fps отправка клиентам. Работает на смарт-ТВ.
Попробовать можно тут: howfastisgo.dev
————
Статья очень крутая, рекомендую.
Автор честно признаётся в разочаровании: Go без SIMD в чистых вычислениях проигрывает даже JavaScript, не говоря уж про Rust (который на одном ядре обошёл Go на восьми).
Но Go и не позиционируется как язык для тяжёлых вычислений — он про сетевые сервисы и конкурентность.
Мне понравился подход с g-buffer'ом — изящное решение проблемы масштабирования. Дельта-кодирование с зигзаг-кодированием в итоге забросили из-за сложности кода, но это правильный выбор — иногда простота важнее оптимальности.
#article #performance #websocket
1👍27🔥7❤5
if err != nil: почему мы (не) любим обработку ошибок в Go? Обзор предложений по её улучшению
https://habr.com/ru/companies/avito/articles/944824/
Павел Агалецкий из Авито в очередной раз поднимает вечную холиварную тему — обработку ошибок в Go.
Суть проблемы (а вдруг кто-то не в курсе?🙃 ):
Go не использует исключения (exceptions) — ошибки это просто значения, которые функции возвращают наравне с другими. Код выглядит многословно:
Сравнение с другими языками:
- Python — исключения есть, но из сигнатуры непонятно, выбрасывает метод исключение или нет
- Java — есть checked exceptions, но большинство функций в языке и библиотеках их не декларируют, т.к. это необязательно
- Rust — есть тип
Что было предложено за годы существования Go:
На GitHub 195 закрытых и 14 открытых proposal по улучшению обработки ошибок. Автор группирует их:
1. check/handle — специальные ключевые слова, чтобы обрабатывать ошибки единообразно
2. try/catch — как в других языках
3. Спецсимволы (? или !) — вместо ключевых слов
4. Упрощение if — тернарные операторы и подобное
Все отклонялись по схожим причинам:
- Можно частично реализовать в userland (через panic/recover + defer)
- Ломается при оборачивании ошибок (fmt.Errorf)
- Появляется неявный control flow
- Код становится менее читаемым
Последний proposal от Ian Lance Taylor:
Ян предложил писать знак вопроса после функций, возвращающих ошибки:
С возможностью обработки:
Реакция сообщества неоднозначная, больше против. Дизлайков в полтора раза больше лайков.
Финал истории:
Robert Griesemer опубликовал статью в блоге Go, где команда объявила, что они больше не будут рассматривать предложения по изменению обработки ошибок. Решили, что на этапе создания языка стоило подумать лучше, но сейчас менять поздно — будет только хуже.
————
Автор делает правильные выводы:
Ошибки как обычные значения — это нормально, явная сигнатура — это хорошо, отсутствие исключений — это прекрасно. Да, многословно, но явно. Да, занимает много места, но зато всё понятно. А современные IDE с autocomplete (особенно с LLM) и code folding помогают справляться с многословностью (у меня в IDE вообще давно настроен хоткей для создания error handling блока:
Я полностью согласен. Лучше писать чуть больше, но понимать что происходит, чем получить "волшебный" синтаксис, который будет работать неявно. Go Team приняли мудрое решение — оставить всё как есть. Язык не должен становиться зоопарком из разных способов сделать одно и то же.
Да, в первые годы работы с Go мне тоже хотелось всячески его "облагородить", изобретать разные способы "упрощения" работы с ошибками, но со временем это проходит🙃
P.S. Мне кажется, что все эти годы сообщество пытается запихнуть в Go механики из других языков, вместо того чтобы просто принять философию Go🤡
#article #error_handling #proposals
https://habr.com/ru/companies/avito/articles/944824/
Павел Агалецкий из Авито в очередной раз поднимает вечную холиварную тему — обработку ошибок в Go.
Суть проблемы (а вдруг кто-то не в курсе?
Go не использует исключения (exceptions) — ошибки это просто значения, которые функции возвращают наравне с другими. Код выглядит многословно:
func doSomething() error {
if err := one(); err != nil {
return err
}
if err := two(); err != nil {
return err
}
if err := three(); err != nil {
return err
}
return nil
}
Сравнение с другими языками:
- Python — исключения есть, но из сигнатуры непонятно, выбрасывает метод исключение или нет
- Java — есть checked exceptions, но большинство функций в языке и библиотеках их не декларируют, т.к. это необязательно
- Rust — есть тип
Result<T, E>
и оператор ?
для проброса ошибок (более компактно)Что было предложено за годы существования Go:
На GitHub 195 закрытых и 14 открытых proposal по улучшению обработки ошибок. Автор группирует их:
1. check/handle — специальные ключевые слова, чтобы обрабатывать ошибки единообразно
2. try/catch — как в других языках
3. Спецсимволы (? или !) — вместо ключевых слов
4. Упрощение if — тернарные операторы и подобное
Все отклонялись по схожим причинам:
- Можно частично реализовать в userland (через panic/recover + defer)
- Ломается при оборачивании ошибок (fmt.Errorf)
- Появляется неявный control flow
- Код становится менее читаемым
Последний proposal от Ian Lance Taylor:
Ян предложил писать знак вопроса после функций, возвращающих ошибки:
func doSomething(in Dto) (err error) {
validate(in) ?
one() ?
two() ?
three() ?
return nil
}
С возможностью обработки:
validate(in) ? {
return fmt.Errorf("validation failed: %w", err)
}
Реакция сообщества неоднозначная, больше против. Дизлайков в полтора раза больше лайков.
Финал истории:
Robert Griesemer опубликовал статью в блоге Go, где команда объявила, что они больше не будут рассматривать предложения по изменению обработки ошибок. Решили, что на этапе создания языка стоило подумать лучше, но сейчас менять поздно — будет только хуже.
————
Автор делает правильные выводы:
Ошибки как обычные значения — это нормально, явная сигнатура — это хорошо, отсутствие исключений — это прекрасно. Да, многословно, но явно. Да, занимает много места, но зато всё понятно. А современные IDE с autocomplete (особенно с LLM) и code folding помогают справляться с многословностью (у меня в IDE вообще давно настроен хоткей для создания error handling блока:
CMD + J
).Я полностью согласен. Лучше писать чуть больше, но понимать что происходит, чем получить "волшебный" синтаксис, который будет работать неявно. Go Team приняли мудрое решение — оставить всё как есть. Язык не должен становиться зоопарком из разных способов сделать одно и то же.
Да, в первые годы работы с Go мне тоже хотелось всячески его "облагородить", изобретать разные способы "упрощения" работы с ошибками, но со временем это проходит
P.S. Мне кажется, что все эти годы сообщество пытается запихнуть в Go механики из других языков, вместо того чтобы просто принять философию Go
#article #error_handling #proposals
Please open Telegram to view this post
VIEW IN TELEGRAM
❤35👍23
Forwarded from Николай Тузов
- Источник
- Перевод
Инженеры Cloudflare рассказали детективную историю о том, как они нашли редчайший баг в компиляторе Go, который проявлялся только на arm64 и только при стечении обстоятельств.
Заваривайте кофе или чаёк и устраивайтесь поудобнее, мы начинаем..
Начало истории:
Один из сервисов начал спорадически паниковать на arm64-машинах с ошибкой "traceback did not unwind completely" — признак повреждения стека. Решили, что это редкая проблема с памятью и не стали копать глубже.
Но паники продолжились.
Первая теория:
- Все критические паники происходили при раскрутке стека
- Коррелировали с recovered panic
- В коде было старое использование panic / recover для обработки ошибок
- Есть похожий баг на GitHub
Убрали panic / recover — паники прекратились. Вздохнули с облегчением.
Но через месяц паники вернулись:
Теперь до 30 в день. Без recovered panics вообще. Никакой корреляции с релизами, инфраструктурой, или положением Марса 🪐
Все крэши происходили в
(*unwinder).next
— при раскрутке стека. Два типа:1. Явная критическая ошибка от среды выполнения
2.
SIGSEGV
при разыменовании указателяЗаметили паттерн: все segfault'ы происходили при асинхронном вытеснении функции
NetlinkSocket.Receive
из библиотеки go-netlink.Что такое асинхронное вытеснение:
До Go 1.14 планировщик был кооперативным — горутины сами решали, когда отдать управление. С 1.14 появилось асинхронное вытеснение: если горутина работает больше 10ms, среда выполнения отправляет SIGURG и принудительно вызывает
asyncPreempt
.Прорыв:
Удалось получить дамп ядра и посмотреть в отладчике. Горутина была остановлена между двумя инструкциями в эпилоге функции:
nl_linux.go:779 0x555577cb287c ADD $80, RSP, RSP
nl_linux.go:779 0x555577cb2880 ADD $(16<<12), RSP, RSP
nl_linux.go:779 0x555577cb2884 RET
Вытеснение произошло между двумя ADD, которые корректируют указатель стека. Стек оказался в несогласованном состоянии!
Почему две инструкции ADD:
На arm64 непосредственный операнд в инструкции ADD — это 12 бит. Для больших значений компилятор разбивает на две операции:
ADD $x, RSP
и ADD $(y<<12), RSP
. Если прерывание происходит между ними, RSP указывает в середину стека, а не на его вершину.Простейшее воспроизведение:
//go:noinline
func big_stack(val int) int {
var big_buffer = make([]byte, 1 << 16)
// ... работа с буфером
}
func main() {
go func() {
for { runtime.GC() }
}()
for { _ = big_stack(1000) }
}
Функция со стековым кадром >64KB, GC в цикле для раскрутки стека, и бесконечные вызовы. Через пару минут — segfault!
Суть бага:
1. Асинхронное вытеснение между
ADD x, RSP
и ADD (y<<12), RSP
2. GC запускает раскрутку стека
3. Раскрутчик пытается прочитать родительский кадр по невалидному RSP
4. Крэш
Условие гонки в одну инструкцию. Невероятно редкий баг!
Исправление:
Для стеков >4KB компилятор теперь сначала строит смещение во временном регистре, потом делает одну атомарную операцию ADD. Вытеснение может произойти до или после, но никогда не во время.
Исправлено в go1.23.12, go1.24.6 и go1.25.0.
————
Очень люблю такие истории!
Месяцы расследования, работа с дампами ядра, изучение внутренностей среды выполнения и ассемблера. И в итоге — баг на уровне компилятора, который проявляется только при стечении обстоятельств: arm64 + большой стек + асинхронное вытеснение + GC
#compiler #debugging
Please open Telegram to view this post
VIEW IN TELEGRAM
The Cloudflare Blog
How we found a bug in Go's arm64 compiler
84 million requests a second means even rare bugs appear often. We'll reveal how we discovered a race condition in the Go arm64 compiler and got it fixed.
🔥38❤9👍6