openlang

GOMAXPROCS в контейнере теперь следует лимиту CPU

До Go 1.25 GOMAXPROCS по умолчанию равнялся числу логических CPU машины. Контейнер с лимитом в два CPU на хосте с 32 логическими CPU получал 32. Это 32 P, которые гоняют горутины при квоте в два CPU на период, так что процесс съедает квоту в начале периода, а остаток каждого периода простаивает из-за троттлинга.

В Go 1.25 значение по умолчанию на Linux поменялось. Рантайм читает лимит CPU из cgroup и берёт его, если он меньше числа CPU. Дробный лимит округляется вверх. Ниже 2 значение по умолчанию не опускается, кроме случая, когда у самой машины CPU меньше двух. Ещё рантайм периодически перечитывает лимит, так что изменение на ходу тоже подхватится.

Новое поведение отключается тремя способами. Переменная окружения GOMAXPROCS, вызов runtime.GOMAXPROCS со значением или строка go с версией 1.24 и ниже в go.mod, потому что для таких версий по умолчанию действует containermaxprocs=0. И смотрит рантайм только на лимит. Запрос CPU без лимита ничего не меняет. В терминах Kubernetes это limits.cpu, а под, у которого задан только requests.cpu, остаётся со старым значением по умолчанию.

Для проверки у меня программа, которая печатает две строки.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Println("GOMAXPROCS", runtime.GOMAXPROCS(0))
	fmt.Println("NumCPU", runtime.NumCPU())
}

Собираю с таким go.mod, чтобы действовало новое значение по умолчанию.

module probe

go 1.25

NumCPU - это число, которое раньше бралось по умолчанию. Если в контейнере с лимитом ниже числа CPU обе строки совпадают, мешает что-то из трёх пунктов выше.

Так что число, за которым я слежу, упало с 32 готовых к работе P до двух.

Разговор продолжается 6

Следуя доводам
  1. Ссылка на ответ

    Кажется, большинство сервисов может просто удалить то, чем они выставляли это вручную при старте. До 1.25 обычно брали маленькую библиотеку, которая читала файлы cgroup и вызывала runtime.GOMAXPROCS с результатом. Вызов со значением теперь ещё и выключает периодическое обновление, так что если его оставить, новое поведение теряется ни за что.

    Ещё стоит поискать GOMAXPROCS, выставленный в файле развёртывания. Эффект тот же. Когда оба убраны, хватит напечатать при старте runtime.GOMAXPROCS(0), это только чтение, и будет видно, что выбрал рантайм.

  2. Ссылка на ответ

    --cpuset-cpus для этого не тот флаг. Под docker run --cpuset-cpus=0 программа из исходного поста печатает 1 и 1, что с go 1.25 в go.mod, что со строкой go, возвращённой на 1.24.

    Cpuset сужает маску привязки к CPU, а Go давно учитывает эту маску, так что и старое значение по умолчанию уже давало 1. Нижняя граница 2 не действует, когда в самой маске один CPU. Новый код касается квоты, а её задаёт --cpus. Дальше прогон с --cpus, а старая строка go остаётся в списке у Vadim, пока он что-нибудь не покажет.

  3. Ссылка на ответ

    Не уверен, что число, прочитанное обратно, что-то доказывает. Ладно, оно показывает, что рантайм увидел лимит. Но убрать хотелось троттлинг, а его cgroup считает сам, в cpu.stat. Кто-нибудь сравнивал nr_throttled до и после под одной и той же нагрузкой, или мы все просто печатаем GOMAXPROCS?

  4. Ссылка на ответ

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

    grep -E '^(nr_periods|nr_throttled|throttled_usec)' \
      /sys/fs/cgroup/cpu.stat

    Это cgroup v2 изнутри контейнера. nr_throttled, делённое на nr_periods, даёт долю периодов, упёршихся в квоту. Сравнивать надо приращения, итоговые значения копятся с момента создания cgroup.

  5. Ссылка на ответ

    С этим grep вокруг прогона с --cpus=2 я бы ждал, что с go 1.25 счётчик nr_throttled упадёт, но не до нуля. Два P не могут исполнять Go-код больше чем на два CPU, но потоки без P, вроде sysmon, всё равно тратят квоту, а квота применяется по периодам. Крутящийся цикл на каждое ядро на VM с восемью CPU, один прогон со старой строкой go и один с новой, и это будет видно по приращениям.

Другие стороны того же вопроса

  1. try против catch unreachable - это сравнение двух разных программ

    try и catch unreachable обещают разное, поэтому, замеряя одно против другого, мы сравниваем две разные программы. Обе версии…

    Ответы 4zig
  2. Нужен ли этой очереди сохранённый счётчик?

    Вызывающий код спрашивает у этой очереди только List.length items > 8, зато спрашивает постоянно. Очередь представлена одним…

    Ответы 4ocaml
  3. Какое время сборки вы приводите?

    Каждая тема про разбиение крейтов заканчивается тем, что кто-то выкладывает одно время сборки. Чисел два, и меняются они в ра…

    Ответы 2rust