CLOVER🍀

That was when it all began.

JavaがDockerコンテナ内でどのようにCPU数、メモリサイズを取得しているのかを調べてみる

これは、なにをしたくて書いたもの?

現在のJavaは、コンテナ環境下ではホスト側ではなくコンテナにリソース制限がかけられていればそちらの値を見るように
なっています。

これはどこの値を見ているのかな?というのを確認してみたくなりまして。

なお、自分にはcgroupに関する知識はほぼありません。あくまで、Javaがどこの情報を見ているか?という観点で
追っています。

JDK-8146115

Javaも以前はホスト側のCPU数やメモリサイズを参照していたのですが、JDK-8146115(およびそのバックポート)が
入ってからはコンテナに割り当てられたCPU数やメモリサイズを見るようになりました。

https://bugs.openjdk.java.net/browse/JDK-8146115

Java 10以降、Java 8については8u191以降で対応しています。

デフォルトでこの機能は有効になっていて、明示的に無効にしたい場合は-XX:-UseContainerSupportを指定すれば
OKです。
また、CPU数については-XX:ActiveProcessorCount=[CPU数]でオーバーライドすることもできます。

今回は、JDK-8146115等の対応で、Javaがどのような情報を参照してCPU数やメモリサイズを取得しているのかを
調べてみました。

環境

確認環境は、こちらです。

$ docker version
Client: Docker Engine - Community
 Version:           20.10.12
 API version:       1.41
 Go version:        go1.16.12
 Git commit:        e91ed57
 Built:             Mon Dec 13 11:45:33 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.12
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.12
  Git commit:       459d0df
  Built:            Mon Dec 13 11:43:42 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.12
  GitCommit:        7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc:
  Version:          1.0.2
  GitCommit:        v1.0.2-0-g52b36a2
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

ホスト側はUbuntu Linux 20.04 LTS。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
Codename:       focal


$ uname -srvmpio
Linux 5.4.0-91-generic #102-Ubuntu SMP Fri Nov 5 16:31:28 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

CPU数は8個、メモリは16Gです。

$ grep ^processor /proc/cpuinfo
processor       : 0
processor       : 1
processor       : 2
processor       : 3
processor       : 4
processor       : 5
processor       : 6
processor       : 7


$ head -n 1 /proc/meminfo
MemTotal:       16306452 kB

確認には、Eclipse TemurinのDockerイメージを使うことにします。

DockerHub / eclipse-temurin

今回利用するバージョンは、こちら。

$ docker container run -it --rm --name java eclipse-temurin:17-jdk-focal bash
# java --version
openjdk 17.0.1 2021-10-19
OpenJDK Runtime Environment Temurin-17.0.1+12 (build 17.0.1+12)
OpenJDK 64-Bit Server VM Temurin-17.0.1+12 (build 17.0.1+12, mixed mode, sharing)

Dockerコンテナのリソース制限について

まず、Dockerコンテナでリソース制限する方法を見ていきましょう。

Runtime options with Memory, CPUs, and GPUs | Docker Documentation

メモリに関しては、--memoryでコンテナが使えるメモリを制限できます。

Runtime options with Memory, CPUs, and GPUs / Memory

CPUに関しては、--cpu-periodで指定した時間あたりのCPU使用時間の上限を、--cpu-quotaで指定します。
この値とホスト側のCPU数から、コンテナ側で利用できるCPU数が決まります。 なのですが、ドキュメントでも勧められているように--cpusでCPU数で指定するのがわかりやすいです。
--cpusは、--cpu-periodと--cpu-quotaをDocker側に計算させるような意味になります。

--cpu-sharesでは、優先度をコントロールします。

Runtime options with Memory, CPUs, and GPUs / CPU

また、コンテナに割り当てるCPUを具体的に指定するには、--cpuset-cpusを使用します。

そもそも、コンテナ内はどうなっているのか?

そもそもコンテナ内ではどうなっているのか、ちょっと確認してみましょう。

特になにも制限せずにコンテナを起動してみます。

$ docker container run -it --rm --name java eclipse-temurin:17-jdk-focal bash

コンテナ内で、CPUやメモリの情報を見てみます。

# grep ^processor /proc/cpuinfo
processor       : 0
processor       : 1
processor       : 2
processor       : 3
processor       : 4
processor       : 5
processor       : 6
processor       : 7


#  head -n 1 /proc/meminfo
MemTotal:       16306452 kB

ホスト側と差がないですね。

Javaで見ても同様です。

# jshell
Jan 04, 2022 5:06:09 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 17.0.1
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 8

jshell> ((com.sun.management.OperatingSystemMXBean)java.lang.management.ManagementFactory.getOperatingSystemMXBean()).getAvailableProcessors()
$2 ==> 8

jshell> ((com.sun.management.OperatingSystemMXBean)java.lang.management.ManagementFactory.getOperatingSystemMXBean()).getTotalMemorySize()
$3 ==> 16697806848

docker container inspectで見ても、特に値は指定されていません。

$ docker container inspect java | jq '.[].HostConfig' | grep -iE 'cpu|memory' | grep -v Kernel
  "CpuShares": 0,
  "Memory": 0,
  "NanoCpus": 0,
  "CpuPeriod": 0,
  "CpuQuota": 0,
  "CpuRealtimePeriod": 0,
  "CpuRealtimeRuntime": 0,
  "CpusetCpus": "",
  "CpusetMems": "",
  "MemoryReservation": 0,
  "MemorySwap": 0,
  "MemorySwappiness": null,
  "CpuCount": 0,
  "CpuPercent": 0,

次に、CPU 2つ、メモリ2Gに制限してコンテナを起動してみます。

$ docker container run -it --rm --name java --cpus 2 --memory 2G eclipse-temurin:17-jdk-focal bash

/procで見える情報には、変化がありません。ホスト側の情報が見えたままです。

# grep ^processor /proc/cpuinfo
processor       : 0
processor       : 1
processor       : 2
processor       : 3
processor       : 4
processor       : 5
processor       : 6
processor       : 7


# head -n 1 /proc/meminfo
MemTotal:       16306452 kB

Java側では、この制限を認識できています。

# jshell
Jan 04, 2022 5:05:11 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 17.0.1
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 2

jshell> ((com.sun.management.OperatingSystemMXBean)java.lang.management.ManagementFactory.getOperatingSystemMXBean()).getAvailableProcessors()
$2 ==> 2

jshell> ((com.sun.management.OperatingSystemMXBean)java.lang.management.ManagementFactory.getOperatingSystemMXBean()).getTotalMemorySize()
$3 ==> 2147483648

docker container inspectすると、制限が入っていることは確認できます。

$ docker container inspect java | jq '.[].HostConfig' | grep -iE 'cpu|memory' | grep -v Kernel
  "CpuShares": 0,
  "Memory": 2147483648,
  "NanoCpus": 2000000000,
  "CpuPeriod": 0,
  "CpuQuota": 0,
  "CpuRealtimePeriod": 0,
  "CpuRealtimeRuntime": 0,
  "CpusetCpus": "",
  "CpusetMems": "",
  "MemoryReservation": 0,
  "MemorySwap": -1,
  "MemorySwappiness": null,
  "CpuCount": 0,
  "CpuPercent": 0,

というわけで、特に考慮しないままとDockerコンテナでリソース制限をしても、ホスト側のリソースの情報を
見てしまうことがわかります。

JDK-8146115での対応内容を確認する

ここで、JDK-8146115ではどのようにしてCPU数やメモリサイズを算出するのか見ていきたいと思います。

https://bugs.openjdk.java.net/browse/JDK-8146115

答えは、issue内に書いてあります。

CPU数は、以下の要領で算出します。

  • cpu_quota / cpu_period
    • 補足) cpu_quotaが設定されている(-1でない場合)に有効
  • コンテナにcpu_sharesが指定されている場合は、cpu_shares / 1024

Number of CPUs

Use a combination of number_of_cpus() and cpu_sets() in order to determine how many processors are available to the process and adjust the JVMs os::active_processor_count appropriately. The number_of_cpus() will be calculated based on the cpu_quota() and cpu_period() using this formula: number_of_cpus() = cpu_quota() / cpu_period(). If cpu_shares has been setup for the container, the number_of_cpus() will be calculated based on cpu_shares()/1024. 1024 is the default and standard unit for calculating relative cpu usage in cloud based container management software.

Also add a new VM flag (-XX:ActiveProcessorCount=xx) that allows the number of CPUs to be overridden. This flag will be honored even if UseContainerSupport is not enabled.

メモリの総量については、cgroupファイルシステムのmemory_limitを使用して取得します。

Total available memory

Use the memory_limit() value from the cgroup file system to initialize the os::physical_memory() value in the VM. This value will propagate to all other parts of the Java runtime.

使用しているメモリについては、OSの利用可能なメモリ(os::available_memory)からmemory_usage_in_bytesを
引いて算出します。

Memory usage

Use memory_usage_in_bytes() for providing os::available_memory() by subtracting the usage from the total available memory allocated to the container.

cgroupが出てきましたね。

関連するカーネルのドキュメントとOpenJDKのソースコード

このあたりの話題で、関連するカーネルのドキュメントはこちらです。

cgroupはv1とv2があります。

OpenJDKのcgroup v1、v2にそれぞれ対応していそうですが、cgroupSubsystem_linux.cppでその振り分けを行って
います。

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/cgroupSubsystem_linux.cpp

cgroup v1についてはこちら。

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp

cgroup v2についてはこちら。

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp

コンテナ内のどこの情報を見ているのかは、cgroup v1であればcgroupV1Subsystem_linux.cppを、
cgroup v2であればcgroupV2Subsystem_linux.cppを見ればわかるようになっています。

たとえば、cgroup v1のcpu_quotaとcpu_periodは、cpu.cfs_quota_usとcpu.cfs_period_usです。

/* cpu_quota
 *
 * Return the number of microseconds per period
 * process is guaranteed to run.
 *
 * return:
 *    quota time in microseconds
 *    -1 for no quota
 *    OSCONTAINER_ERROR for not supported
 */
int CgroupV1Subsystem::cpu_quota() {
  GET_CONTAINER_INFO(int, _cpu->controller(), "/cpu.cfs_quota_us",
                     "CPU Quota is: %d", "%d", quota);
  return quota;
}

int CgroupV1Subsystem::cpu_period() {
  GET_CONTAINER_INFO(int, _cpu->controller(), "/cpu.cfs_period_us",
                     "CPU Period is: %d", "%d", period);
  return period;
}

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp#L204-L224

controllerという部分がわかりませんね。これはまた後で。

ちなみに、利用可能なCPU数の算出方法はcgroupSubsystem_linux.cppにコメントとしても書かれています。

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/cgroupSubsystem_linux.cpp#L406-L441

また、DockerのCPU制限のところで、CFSスケジューラーという言葉が出てくるのですが、

Specify the CPU CFS scheduler period, which is used alongside --cpu-quota.

Runtime options with Memory, CPUs, and GPUs / CPU

これはこちらのことですね。

CFS Scheduler — The Linux Kernel documentation

自分の環境を確認する

ここで、自分の手元の環境がcgroup v1なのかcgroup v2なのかを確認したいと思います。

mountで見るとよいみたいなのですが、cgroup v1とcgroup v2の両方が入っています。

$ mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)

これは、cgroup v1がデフォルトではマウントされ、そうならなかったものがcgroup v2でマウントされているようなのですが。

A cgroup v2 controller is available only if it is not currently in use via a mount against a cgroup v1 hierarchy. Or, to put things another way, it is not possible to employ the same controller against both a v1 hierarchy and the unified v2 hierarchy.

cgroups(7) - Linux manual page

実質、自分の環境ではcgroup v1ですね。

ちなみに、コンテナ内で確認すると完全にcgroup v1になっています。

$ docker container run -it --rm --name java eclipse-temurin:17-jdk-focal bash -c 'mount | grep cgroup'
tmpfs on /sys/fs/cgroup type tmpfs (rw,nosuid,nodev,noexec,relatime,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (ro,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/rdma type cgroup (ro,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/blkio type cgroup (ro,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (ro,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (ro,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/freezer type cgroup (ro,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/devices type cgroup (ro,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (ro,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/pids type cgroup (ro,nosuid,nodev,noexec,relatime,pids)

cgroup v1でコンテナに割り当てられたリソースを確認する

ここから先は、手元の環境(cgroup v1)で確認していきたいと思います。

再度、CPUを2つ、メモリを2Gに制限したコンテナに入ってみます。

$ docker container run -it --rm --name java --cpus 2 --memory 2G eclipse-temurin:17-jdk-focal bash

自身のcgroupの情報は、/proc/self/cgroupで確認できます。

# cat /proc/self/cgroup
12:pids:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
11:net_cls,net_prio:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
10:cpuset:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
9:memory:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
8:devices:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
7:freezer:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
6:perf_event:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
5:cpu,cpuacct:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
4:hugetlb:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
3:blkio:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
2:rdma:/
1:name=systemd:/docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba
0::/system.slice/containerd.service

2列目にあるmemoryやcpu,cpuacctというのは、サブシステムを表しています。

そして、/proc/self/mountinfoを見ると、各サブシステムがどこにあるのかがわかります。

# grep docker /proc/self/mountinfo
1646 1569 0:135 / / rw,relatime master:726 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/GJRR3TKMSGCOQ6S7DE34V67ZDU:/var/lib/docker/overlay2/l/VSPKK6AJ62IU6LXW7CUURU5NBL:/var/lib/docker/overlay2/l/NM6ZKZ76GIHXKGJFFADB3UAEB7:/var/lib/docker/overlay2/l/L4PQYHUEGPQIFE5VDUMPZK3ZJ2:/var/lib/docker/overlay2/l/RXIPGFSFBRXJUBRP7FGTUIN5Y2,upperdir=/var/lib/docker/overlay2/dbf0492bd7fdc22e4755c6364b451cdc69acdb306cbda5f7c7a6897ed0262180/diff,workdir=/var/lib/docker/overlay2/dbf0492bd7fdc22e4755c6364b451cdc69acdb306cbda5f7c7a6897ed0262180/work,xino=off
1658 1657 0:30 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd
1660 1657 0:35 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,blkio
1661 1657 0:36 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,hugetlb
1662 1657 0:37 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,cpu,cpuacct
1663 1657 0:38 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,perf_event
1664 1657 0:39 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,freezer
1665 1657 0:40 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,devices
1666 1657 0:41 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,memory
1667 1657 0:42 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,cpuset
1668 1657 0:43 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,net_cls,net_prio
1669 1657 0:44 /docker/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,pids
1672 1646 8:8 /var/lib/docker/containers/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sda8 rw,errors=remount-ro
1673 1646 8:8 /var/lib/docker/containers/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba/hostname /etc/hostname rw,relatime - ext4 /dev/sda8 rw,errors=remount-ro
1674 1646 8:8 /var/lib/docker/containers/435086c1708f59eaae929124b129e64b2a93adea24c6cb570ed99323064d4dba/hosts /etc/hosts rw,relatime - ext4 /dev/sda8 rw,errors=remount-ro

たとえば、memoryサブシステムであれば/sys/fs/cgroup/memory、cpu,cpuacctサブシステムであれば
/sys/fs/cgroup/cpu,cpuacctを参照すればよいことがわかります。

実際、OpenJDKでもこれらの情報は見ているようです。

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/cgroupSubsystem_linux.cpp#L44-L46

ここで、CPU数はcpu_quotaをcpu_periodで割れば算出できるということでした。そして、cgroup v1のcpu_quotaと
cpu_periodは、cpu.cfs_quota_usとcpu.cfs_period_usでした。

というわけで、コンテナ内でそれぞれの値を確認します。

# cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us
200000


# cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us
100000

200000 / 100000なので、2ですね。コンテナに割り当てたCPU数と一致しました。

コンテナ自体が認識しているCPUは、ホスト側の8つ全部のようですが、これをスケジューリングして実質2個に
している感じでしょうか。

# cat /sys/fs/cgroup/cpuset/cpuset.cpus
0-7

メモリについては、/sys/fs/cgroup/memory/memory.limit_in_bytesを見ます。

# cat /sys/fs/cgroup/memory/memory.limit_in_bytes
2147483648

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp#L104-L127

これは、Javaで見た結果とも、docker container inspectで見た結果とも一致しますね。

jshell> ((com.sun.management.OperatingSystemMXBean)java.lang.management.ManagementFactory.getOperatingSystemMXBean()).getTotalMemorySize()
$3 ==> 2147483648


$ docker container inspect java | jq '.[].HostConfig' | grep -iE 'cpu|memory' | grep -v Kernel
  "CpuShares": 0,
  "Memory": 2147483648,

〜省略〜

現在のメモリ使用量なら、/sys/fs/cgroup/memory/memory.usage_in_bytesみたいです。

# cat /sys/fs/cgroup/memory/memory.usage_in_bytes
4104192

ちなみに、cpu_sharesについては/sys/fs/cgroup/cpu,cpuacct/cpu.sharesを参照します。今回はここは調整しませんが。

# cat /sys/fs/cgroup/cpu,cpuacct/cpu.shares
1024

cpuset-cpusを指定すると?

--cpuset-cpusを使い、CPUを2個固定で割り当ててみます。

$ docker container run -it --rm --name java --cpuset-cpus 0,1 eclipse-temurin:17-jdk-focal bash

cpu.cfs_quota_usが-1なので、こちらは無効のようです。

# cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us
-1


# cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us
100000

それでもJava側は認識できています。

# jshell
Jan 04, 2022 6:05:44 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 17.0.1
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 2

jshell> ((com.sun.management.OperatingSystemMXBean)java.lang.management.ManagementFactory.getOperatingSystemMXBean()).getAvailableProcessors()
$2 ==> 2

/sys/fs/cgroup/cpuset/cpuset.cpusを見ると、確かに2個割り当てられています。

# cat /sys/fs/cgroup/cpuset/cpuset.cpus
0-1

これはどうやっているかというと、sched_getaffinityの結果を見ています。

sched_getaffinity(2) - Linux man page

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/os_linux.cpp#L4678-L4685

--cpuset-cpusの場合は、コンテナ内のプロセスに対してCPUアフィニティで実現しているようです。

確認用のコード。
※sysconfおよび_SC_NPROCESSORS_CONFが登場する理由は、後で出てきます

print_processors_count.c

#define _GNU_SOURCE 
#include <stdio.h>
#include <unistd.h>
#include <sched.h>
#include <stdlib.h>

int main()
{
  printf("available processors = %lu\n", sysconf(_SC_NPROCESSORS_CONF));

  cpu_set_t cpu_set;
  CPU_ZERO(&cpu_set);

  sched_getaffinity(0, sizeof(cpu_set), &cpu_set);
  printf("number of cpus: %d\n", CPU_COUNT(&cpu_set));
}

これをビルドしてコンテナ内に送り込み、

$ gcc print_processors_count.c -o print_processors_count
$ docker container cp print_processors_count java:/

コンテナ内で実行すると以下の結果になります。

# /print_processors_count
available processors = 8
number of cpus: 2

認識しているCPU数は8ですが、sched_getaffinityで得られた結果は2になっています。

docker container inspectでは、こうなりました。

$ docker container inspect java | jq '.[].HostConfig' | grep -iE 'cpu|memory' | grep -v Kernel
  "CpuShares": 0,
  "Memory": 0,
  "NanoCpus": 0,
  "CpuPeriod": 0,
  "CpuQuota": 0,
  "CpuRealtimePeriod": 0,
  "CpuRealtimeRuntime": 0,
  "CpusetCpus": "0,1",
  "CpusetMems": "",
  "MemoryReservation": 0,
  "MemorySwap": 0,
  "MemorySwappiness": null,
  "CpuCount": 0,
  "CpuPercent": 0,

同じ情報をホスト側で参照すると?

これらの情報を、ホスト側で参照するとどうなっているんでしょう?

$ cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us
-1


$ cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us
100000


$ cat /sys/fs/cgroup/memory/memory.limit_in_bytes
9223372036854771712


$ cat /sys/fs/cgroup/cpu,cpuacct/cpu.shares
1024

/sys/fs/cgroup/memory/memory.limit_in_bytesの値がすごいことになっています…。

ちなみに、コンテナにリソース制限を入れずにコンテナ内で同じ情報を確認すると、同様の結果になりました。

コンテナ対応を無効にした場合は?そもそも、元はどこを参照していた?

cgroupの情報を参照する処理は、-XX:-UseContainerSupportを指定すると行わずに飛ばしてしまいます。

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/osContainer_linux.cpp#L53-L56

というか、そもそも元はどこの情報を見ていたんでしょうね?

sysconfみたいです。CPU数は_SC_NPROCESSORS_CONFから、メモリは_SC_PHYS_PAGESと_SC_PAGESIZEから。

https://github.com/openjdk/jdk17u/blob/jdk-17.0.1%2B12/src/hotspot/os/linux/os_linux.cpp#L364-L379

sysconf(3) - Linux manual page

まとめ

Javaがどうやってコンテナ側のCPU数やメモリサイズを取得しているのか、興味があったので調べてみました。

一応、情報はだいたい揃った気がしますが、cgroupに対する理解が全然ないので、本当に情報が並んでいるだけです。

もうちょっと追いたい気もするのですが、いったんここまでで…。

でも、いろいろ勉強になりました。

コンテナに割り当てられたリソースを参照したり、その情報に合わせて動作するようなプログラムは、このあたりの
情報を意識しないといけないということになるんですね。

参考

第3回 Linuxカーネルのコンテナ機能[2] ─cgroupとは?(その1):LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術|gihyo.jp … 技術評論社

第4回 Linuxカーネルのコンテナ機能[3] ─cgroupとは?(その2):LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術|gihyo.jp … 技術評論社

第5回 Linuxカーネルのコンテナ機能[4] ─cgroupとは?(その3):LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術|gihyo.jp … 技術評論社