hana_shinのLinux技術ブログ

Linuxの技術情報を掲載しています。特にネットワークをメインに掲載していきます。

プライバシーポリシー

個人情報について
利用目的
当ブログ「hana_shinのLinux技術ブログ」では、お問い合わせの際に、お名前、メールアドレス等の個人情報をご登録いただいています。

これらの個人情報は、質問に対する回答や必要な情報をご連絡する場合に利用させていただくものです。個人情報をこの目的以外で利用することはございません。

また当ブログでは、スパム・荒らしへの対応として、コメントの際に使用された IPアドレスを記録しています。

これは、はてなブログの標準機能としてサポートされているものです。スパム・荒らしへの対応以外にこの IPアドレスを使用することはありません。


個人情報の保管
ユーザーの個人情報を正確かつ最新の状態に保ち、個人情報への不正アクセス・紛失・破損・改ざん・漏洩などを防止するため、セキュリティシステムの維持など必要な措置を講じて、個人情報の厳重な管理を行ないます。

個人情報の開示
【第三者への開示】

次の場合を除いて、ユーザーからいただいた個人情報を、第三者に開示することはありません。

提供者の同意がある場合
法令に基づく場合
不正行為やその他の違法行為を防ぐために個人情報の開示が必要となった場合
【本人への開示】

ご本人の個人情報の照会・修正・削除などをご希望される場合には、ご本人であることを確認の上、対応させていただきます。

広告の配信について
Google アドセンスなど
「hana_shinのLinux技術ブログ」は第三者配信の広告サービス「Google アドセンス」「A8.net」「もしもアフィリエイト」を利用しています。
広告配信事業者は、ユーザーの興味に応じた広告を表示するためにCookie(クッキー)を使用します。Cookieを使用することでユーザーのPCを識別できるようになりますが、ユーザー個人を特定できるものではありません。

Cookieを無効にするやGoogleアドセンスに関する詳細は「広告 – ポリシーと規約 – Google」をご覧ください。

Amazon アソシエイト
「hana_shinのLinux技術ブログ」は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイトプログラムである、Amazonアソシエイト・プログラムの参加者です。 Amazonのアソシエイトとして、「hana_shinのLinux技術ブログ」は適格販売により収入を得ています。

Amazonアソシエイトの個人情報の取扱方法についてはAmazonプライバシー規約をご覧ください。

アクセス解析ツール
「hana_shinのLinux技術ブログ」では、Googleが提供している分析ツールGoogle Analyticsを利用して、訪問者の行動を分析しています。Google Analytics のデータのプライバシーとセキュリティについてはコチラをご覧ください。

Google Analyticsはトラフィックデータの収集のためにCookieを使用しています。このトラフィックデータは匿名で収集されており、個人を特定するものではありません。

この機能はCookieを無効にすることで収集を拒否することが出来ますので、お使いのブラウザの設定をご確認ください。

免責事項
当ブログからリンクやバナーなどによって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。

当サイトのコンテンツ・情報につきまして、可能な限り正確な情報を掲載するよう努めておりますが、誤情報が入り込んだり、情報が古くなっていることもございます。

当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。

また、当サイトに掲載しているすべての記事は、予告なしに変更・削除されることがあります。 予めご了承下さい。

肖像権について
当ブログは著作権や肖像権の侵害を目的としたものではありません。著作権や肖像権に関して問題がございましたら、お問い合わせフォームよりご連絡ください。確認後、速やかに対応いたします。

著作権について
「hana_shinのLinux技術ブログ」に掲載されている情報についての著作権は放棄しておりません。掲載している文章や画像などにつきましては、無断転載することを禁止します。

当ブログからの引用に関しましては「引用元の明示」によって無償で引用頂けます。

ただし、全文転載はお断りいたしております。引用許可範囲についても、事前予告なくこれを変更する事があります。

プライバシーポリシーの変更について
「キラッとブログ」は、個人情報に関して適用される日本の法令を遵守するとともに、本ポリシーの内容を適宜見直しその改善に努めます。

修正された最新のプライバシーポリシーは常に本ページにて開示されます。


運営者:hana-shin

初出掲載:2019年05月05日

Harborのインストール方法

1 Harborとは?

Harborは、コンテナイメージを保存・管理するためのコンテナレジストリです。Docker Hubのような公開レジストリとは異なり、組織内に専用のレジストリを構築できるため、本番環境で使用するコンテナイメージを安全に一元管理できます。また、イメージの脆弱性スキャン、アクセス制御、イメージ署名、レプリケーションなど、コンテナを安全に運用するための機能を標準で提供しています。

2 検証環境

2.1 ネットワーク構成

検証環境は、VMware Workstation Pro上に構築した3台の仮想マシンでKubernetesクラスタを構成しています。各仮想マシンはブリッジ接続により、PCと同じネットワーク(192.168.1.0/24)に接続されています。

一方、10.244.0.0/16 は、CalicoのCNIプラグインによってクラスタ内に作成されるPodネットワークです。各Podにはこのアドレス帯からIPアドレスが割り当てられ、異なるノード上のPod同士が相互に通信する際に使用されます。Calicoは各ノードに経路を設定することで、Pod間通信を透過的に実現しています。

+--- control ---+    +--- worker1 ---+   +--- worker2 ---+
|AlmaLinux 10.2 |    |AlmaLinux 10.2 |   |AlmaLinux 10.2 |
|               |    |               |   |               |
|      Pod      |    |      Pod      |   |      Pod      |
|       |       |    |       |       |   |       |       |
| 10.244.x.0/24 |    | 10.244.y.0/24 |   | 10.244.z.0/24 |
| ------------- |    | ------------- |   | ------------- |
|               |    |               |   |               |
+-------+-------+    +-------+-------+   +-------+-------+
        |.19                 |.20                |.22
        |                    |                   |
        |   192.168.1.0/24   |                   |
+--------------------------------------------------------+
|        VMware Workstation Pro(Bridged networking)    |
+--------------------------------------------------------+
                             |
                    +---------------+
                    |               |
                    |       PC      |
                    |               |
                    +---------------+

それぞれの役割は以下のとおりです。
1台をコントロールノード、2台をワーカーノードとして使用します。

ホスト名 名称 役割
control コントロールノード クラスタ(control、worker1、worker2)の状態を管理し、Pod をどのノードで実行するかを決定するノード
worker1 ワーカーノード Pod を実行するノード
worker2 ワーカーノード Pod を実行するノード

2.2 ソフトウェアのバージョン

各ノードのAlmaLinuxバージョンは以下のとおりです。

[root@control ~]# cat /etc/redhat-release
AlmaLinux release 10.2 (Lavender Lion)

各ノードのカーネルバージョンは以下のとおりです。

[root@control ~]# uname -r
6.12.0-211.7.3.el10_2.x86_64

Kubernetesのバージョンは以下のとおりです。

[root@control ~]# kubectl version
Client Version: v1.36.2
Kustomize Version: v5.8.1
Server Version: v1.36.2

2.3 ノードのリソース

各ノードには4GBのメモリを割り当てています。

[root@control ~]#  free -h
               total        used        free      shared  buff/cache   available
Mem:           3.6Gi       1.3Gi       1.0Gi       5.8Mi       1.5Gi       2.3Gi
Swap:             0B          0B          0B

各ノードは 4コアのCPU(4 vCPU) を搭載しています。

[root@control ~]# lscpu -xe
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
  0    0      0    0 0:0:0:0          yes
  1    0      1    1 1:1:1:1          yes
  2    0      2    2 2:2:2:2          yes
  3    0      3    3 3:3:3:3          yes

lscpuコマンドの詳しい使い方は、以下のページをご覧ください。
hana-shin.hatenablog.com

3 事前準備

本検証ではCNI(Container Network Interface)にCalicoを使用しており、ノード間のPod通信にはIPIPカプセル化方式(tunl0インターフェース)を使用しています。firewalldを有効にした環境では、Calicoが使用するトンネルインターフェースやPodネットワークに対する通信がfirewalldによって許可されていない場合、Pod間通信が正常に行えず、Harborなどのアプリケーションが正常に動作しないことがあります。そのため、この後の手順(MetalLB、Harborのインストール)を正常に進めるため、事前に以下の設定を行います。3章の設定は全てのノード(コントロールノード、ワーカノード)で実施します。

(1) tunl0 の状態確認
tunl0が現在どのゾーンに属しているか確認します。no zoneと表示された場合、以降の設定が必要です。

[root@control ~]# firewall-cmd --get-zone-of-interface=tunl0
no zone

firewall-cmdコマンドの詳しい使い方は、以下のページをご覧ください。
hana-shin.hatenablog.com

(2) trustedゾーンへの追加
firewalldのtrustedゾーンは、そのゾーンに属する通信を許可する、最も信頼度の高いゾーンです。今回は、Calicoが使用するトンネルインターフェース(tunl0)と、Podネットワーク(10.244.0.0/16)からの通信を許可するため、これらをtrustedゾーンへ追加します。

tunl0インターフェースをtrustedゾーンへ追加します。

[root@control ~]# firewall-cmd --permanent --zone=trusted --add-interface=tunl0
success

Podネットワーク(10.244.0.0/16)をtrustedゾーンへ追加します。この設定により、Calicoが構成するPodネットワークの通信がfirewalldによって遮断されることを防ぎます。

[root@control ~]# firewall-cmd --permanent --zone=trusted --add-source=10.244.0.0/16
success

クラスタを構成するノードが接続されているネットワーク(192.168.1.0/24)をtrustedゾーンへ追加します。

[root@control ~]# firewall-cmd --permanent --zone=trusted --add-source=192.168.1.0/24
success

永続設定を反映するため、firewalldの設定をリロードします。

[root@control ~]# firewall-cmd --reload
success

(3) 設定確認
tunl0がtrustedゾーンに追加されたことを確認します。

[root@control ~]# firewall-cmd --get-zone-of-interface=tunl0
trusted

trustedゾーンの設定内容を確認します。interfacesにtunl0 が登録されていれば、設定は正しく反映されています。

[root@control ~]# firewall-cmd --zone=trusted --list-all
trusted (active)
  target: ACCEPT
  ingress-priority: 0
  egress-priority: 0
  icmp-block-inversion: no
  interfaces: tunl0
  sources: 10.244.0.0/16 192.168.1.0/24
  services:
  ports:
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

4 ストレージ(Local Path Provisioner)のインストール

Harborは、コンテナイメージやデータベースなどのデータを永続的に保存するために、PersistentVolumeClaim(PVC)を使用します。ここでは、PVCの要求に応じてPersistentVolume(PV)を動的に作成するLocal Path Provisionerをインストールします。Local Path Provisionerは、ノード上のローカルディスクを利用して、ストレージを提供します。

curl コマンドを使用して、Local Path Provisionerのマニフェストファイルをダウンロードします。

[root@control ~]# curl -LO https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.36/deploy/local-path-storage.yaml
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3852  100  3852    0     0   1046      0  0:00:03  0:00:03 --:--:--  1046

curl コマンドの詳しい使い方は、以下のページをご覧ください。
hana-shin.hatenablog.com


ダウンロードしたマニフェストのバックアップを作成します。

[root@control ~]# cp local-path-storage.yaml local-path-storage.yaml.org

テキストエディタを使って、StorageClassをクラスタのデフォルトのStorageClassとして使用するため、local-path-storage.yaml を編集します。

[root@control ~]# vi local-path-storage.yaml

diff コマンドを使用し、StorageClassをデフォルトとして指定するアノテーションが追加されたことを確認します。

[root@control ~]# diff -Nur local-path-storage.yaml.org local-path-storage.yaml
--- local-path-storage.yaml.org 2026-07-12 08:58:45.916523536 +0900
+++ local-path-storage.yaml     2026-07-12 09:00:47.781709321 +0900
@@ -116,6 +116,8 @@
 kind: StorageClass
 metadata:
   name: local-path
+  annotations:
+    storageclass.kubernetes.io/is-default-class: "true"
 provisioner: rancher.io/local-path
 volumeBindingMode: WaitForFirstConsumer
 reclaimPolicy: Delete

編集したマニフェストをKubernetesクラスタに適用し、Local Path Provisionerをインストールします。

[root@control ~]# kubectl apply -f local-path-storage.yaml
namespace/local-path-storage created
serviceaccount/local-path-provisioner-service-account created
role.rbac.authorization.k8s.io/local-path-provisioner-role created
clusterrole.rbac.authorization.k8s.io/local-path-provisioner-role created
rolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
clusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
deployment.apps/local-path-provisioner created
storageclass.storage.k8s.io/local-path created
configmap/local-path-config created

インストールが完了したら、Local Path ProvisionerのPodが正常に起動しているか確認します。STATUS が Running になっていればOKです。

[root@control ~]# kubectl get pods -n local-path-storage -o wide
NAME                                      READY   STATUS    RESTARTS   AGE   IP              NODE      NOMINATED NODE   READINESS GATES
local-path-provisioner-6d76485965-xq6s2   1/1     Running   0          24s   10.244.189.66   worker2   <none>           <none>

最後に、local-path がデフォルトのStorageClassとして正しく認識されているかを確認します。

[root@control ~]# kubectl get storageclass
NAME                   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  42s

5 MetalLBのインストール

オンプレミス(自宅・社内インフラなど)では、クラウド環境とは異なり、LoadBalancer型のServiceを作成しても外部IPアドレス(EXTERNAL-IP)が自動で割り当てられません。MetalLBは、オンプレミス環境でもLoadBalancer型Serviceに対して外部IPアドレスを割り当てることができるソフトウェアです。今回は、後ほどHarborへのアクセスに使うIngress Controller(ingress-nginx)を外部公開するためにMetalLBを導入します。

5.1 MetalLBのインストール

MetalLBのリポジトリを追加します。

[root@control ~]# helm repo add metallb https://metallb.github.io/metallb
"metallb" has been added to your repositories

helm コマンドの詳しい使い方は、以下のページをご覧ください。
hana-shin.hatenablog.com

最新のチャート(パッケージ)情報を取得するため、Helmリポジトリの情報を更新します。

[root@control ~]# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "metallb" chart repository
Update Complete. ?Happy Helming!?

Helmを使ってMetalLBをインストールします。--create-namespace を付与することで、専用のNamespace(metallb-system)の作成とインストールを同時に行います。STATUS: deployed となれば成功です。

[root@control ~]# helm install metallb metallb/metallb --namespace metallb-system --create-namespace
I0712 09:11:29.887926   36180 warnings.go:107] "Warning: unrecognized format \"cidr\""
NAME: metallb
LAST DEPLOYED: Sun Jul 12 09:11:29 2026
NAMESPACE: metallb-system
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
MetalLB is now running in the cluster.

Now you can configure it via its CRs. Please refer to the metallb official docs
on how to use the CRs.

helm list コマンドを使い、指定したNamespaceにMetalLBが正常にデプロイされている状態か確認します。

[root@control ~]# helm list -n metallb-system
NAME    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
metallb metallb-system  1               2026-07-12 09:11:29.313813192 +0900 JST deployed        metallb-0.16.1  v0.16.1

MetalLBを構成する各Podの状態を確認します。すべてのPodが Running になっていれば正常に動作しています。

[root@control ~]# kubectl get pods -n metallb-system
NAME                                             READY   STATUS    RESTARTS   AGE
metallb-controller-bc9cbb54b-xsts9               1/1     Running   0          2m26s
metallb-frr-k8s-fcdvp                            5/5     Running   0          2m26s
metallb-frr-k8s-jtg94                            5/5     Running   0          2m26s
metallb-frr-k8s-s282w                            5/5     Running   0          2m26s
metallb-frr-k8s-statuscleaner-75b695f48d-dvtnc   1/1     Running   0          2m26s
metallb-speaker-fs475                            1/1     Running   0          2m26s
metallb-speaker-w9p7t                            1/1     Running   0          2m26s
metallb-speaker-wh8fd                            1/1     Running   0          2m26s

MetalLBのWebhook Serviceが作成されていることを確認します。

[root@control ~]# kubectl get svc -n metallb-system
NAME                      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
frr-k8s-webhook-service   ClusterIP   10.96.250.168   <none>        443/TCP   4m12s
metallb-webhook-service   ClusterIP   10.111.129.74   <none>        443/TCP   4m12s

5.2 IPプールの設定

MetalLBがLoadBalancer型のServiceに対して、どのIPアドレスを割り当てるべきかを定義したマニフェストを作成します。 
- IPAddressPool:払い出すIPアドレスの範囲(今回は 192.168.1.200〜210)を指定します。
- L2Advertisement:定義したIPアドレスを、L2モード(ARP)を使ってネットワーク内に通知するための設定です。

[root@control ~]# vi metallb-pool.yaml
[root@control ~]# cat metallb-pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.1.200-192.168.1.210
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2advertisement
  namespace: metallb-system

作成したマニフェストファイルをクラスタに適用します。

[root@control ~]# kubectl apply -f metallb-pool.yaml
ipaddresspool.metallb.io/first-pool created
l2advertisement.metallb.io/l2advertisement created

設定したIPプール(IPAddressPool)が正しく認識され、指定したIPアドレス範囲が登録されているか確認します。

[root@control ~]# kubectl get ipaddresspool -n metallb-system
NAME         AUTO ASSIGN   AVOID BUGGY IPS   ADDRESSES
first-pool   true          false             ["192.168.1.200-192.168.1.210"]

同様に、L2モードの通知設定(L2Advertisement)が正しく登録されているか確認します。

[root@control ~]# kubectl get l2advertisement -n metallb-system
NAME              IPADDRESSPOOLS   IPADDRESSPOOL SELECTORS   INTERFACES
l2advertisement

5.3 動作確認

設定したIPプールから、実際にServiceへIPアドレスが自動で割り当てられるかを確認します。まずは動作確認用として、シンプルなNginxのDeploymentを作成(Podをデプロイ)します。

[root@control ~]# kubectl create deployment nginx-test --image=nginx
deployment.apps/nginx-test created

Deploymentの詳細は、以下のページをご覧ください。
hana-shin.hatenablog.com

作成したNginxのDeploymentを、LoadBalancer 型のServiceとして外部へ公開します。

[root@control ~]# kubectl expose deployment nginx-test --port=80 --type=LoadBalancer
service/nginx-test exposed

作成されたServiceのステータスを確認します。EXTERNAL-IP の欄に、先ほどプールとして指定した範囲内から 192.168.1.200 が自動的に割り当てられていることがわかります。

[root@control ~]# kubectl get svc nginx-test
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
nginx-test   LoadBalancer   10.110.52.162   192.168.1.200   80:31347/TCP   5s

curl コマンドを使い、割り当てられた外部IPアドレス(192.168.1.200)へHTTPリクエストを送信してみます。Serviceを介して背後のNginx Podへと正しくルーティングされ、Nginxから正常な応答が返ってきていることが確認できます。

[root@control ~]# curl http://192.168.1.200
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, nginx is successfully installed and working.
Further configuration is required for the web server, reverse proxy,
API gateway, load balancer, content cache, or other features.</p>

<p>For online documentation and support please refer to
<a href="https://nginx.org/">nginx.org</a>.<br/>
To engage with the community please visit
<a href="https://community.nginx.org/">community.nginx.org</a>.<br/>
For enterprise grade support, professional services, additional
security features and capabilities please refer to
<a href="https://f5.com/nginx">f5.com/nginx</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

動作確認が完了したため、テスト用に作成したServiceとDeploymentを削除して環境を元の状態に戻しておきます。

[root@control ~]# kubectl delete svc nginx-test
service "nginx-test" deleted from default namespace
[root@control ~]# kubectl delete deployment nginx-test
deployment.apps "nginx-test" deleted from default namespace

6 イングレスコントローラのインストール

ここでは、ingress-nginxというIngress Controllerをインストールします。
Ingressは、HTTP/HTTPSリクエストをホスト名やURLパスに応じてServiceへルーティングするためのルールを定義するオブジェクトです。しかし、Ingressを定義しただけではリクエストはルーティングされません。Ingressで定義したルールを読み取り、リバースプロキシとして動作し、リクエストを適切なServiceへ振り分ける役割を担うのがIngress Controllerです。

6.1 ingress-nginxのインストール

MetalLBのプールから払い出される、Ingress用のIP(例えば192.168.1.200)に対してホスト名を紐付けます。

  • 192.168.1.200 harbor.home.lab

ingress-nginxのHelmリポジトリを追加します。

[root@control ~]# helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
"ingress-nginx" has been added to your repositories

Helmリポジトリの情報を最新の状態に更新します。

[root@control ~]# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "metallb" chart repository
...Successfully got an update from the "ingress-nginx" chart repository
Update Complete. ?Happy Helming!?

ingress-nginxをインストールします。--set controller.service.type=LoadBalancerを指定することで、ingress-nginxのServiceがLoadBalancer型で作成され、MetalLBによって外部IPアドレスが自動的に割り当てられます。

[root@control ~]# helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.type=LoadBalancer
NAME: ingress-nginx
LAST DEPLOYED: Sun Jul 12 10:38:10 2026
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
The ingress-nginx controller has been installed.
It may take a few minutes for the load balancer IP to be available.
You can watch the status by running 'kubectl get service --namespace ingress-nginx ingress-nginx-controller --output wide --watch'

An example Ingress that makes use of the controller:
  apiVersion: networking.k8s.io/v1
  kind: Ingress
  metadata:
    name: example
    namespace: foo
  spec:
    ingressClassName: nginx
    rules:
      - host: www.example.com
        http:
          paths:
            - pathType: Prefix
              backend:
                service:
                  name: exampleService
                  port:
                    number: 80
              path: /
    # This section is only required if TLS is to be enabled for the Ingress
    tls:
      - hosts:
        - www.example.com
        secretName: example-tls

If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:

  apiVersion: v1
  kind: Secret
  metadata:
    name: example-tls
    namespace: foo
  data:
    tls.crt: <base64 encoded cert>
    tls.key: <base64 encoded key>
  type: kubernetes.io/tls

インストール後、Podの状態を確認します。

[root@control ~]# kubectl get pods -n ingress-nginx
NAME                                        READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-5cd9869bf8-m584m   1/1     Running   0          49s

インストール後、Serviceの状態を確認します。ServiceのEXTERNAL-IPにMetalLBのプール範囲内(192.168.1.200〜192.168.1.210)のIPアドレスが割り当てられていれば成功です。

[root@control ~]# kubectl get svc -n ingress-nginx
NAME                                 TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)                      AGE
ingress-nginx-controller             LoadBalancer   10.97.221.138   192.168.1.200   80:32423/TCP,443:32058/TCP   55s
ingress-nginx-controller-admission   ClusterIP      10.98.231.61    <none>          443/TCP                      55s

7 証明書の作成

ここでは、公開認証局(Root CA)の代わりに、コントロールノード上でプライベートCAを構築します。具体的には、作業用ディレクトリを作成し、CAで使用する秘密鍵とCA証明書を作成します。続いて、作成したプライベートCAを使用してHarborのサーバ証明書に署名します。作成する証明書は以下のとおりです。

  • PCへインポートするCA証明書
  • HarborのHTTPS通信で使用するサーバ証明書

7.1 プライベートCAの証明書の作成

作業用ディレクトリを作成し、cdコマンドでharbor-certsディレクトリへ移動します。

[root@control ~]# mkdir -p ~/harbor-certs 
[root@control ~]# cd harbor-certs/

プライベートCAで使用する秘密鍵を作成します。

[root@control test]# openssl genpkey -algorithm RSA -out ca.key -pkeyopt rsa_keygen_bits:4096
[root@control harbor-certs]#

作成した秘密鍵(ca.key)が正常に生成されていることを確認します。

[root@control harbor-certs]# ls -l
合計 4
-rw-------. 1 root root 3268  7月 14 10:49 ca.key

プライベートCAのCA証明書(ca.crt)を作成します。このCA証明書は、Harborのサーバ証明書の検証に使用します。なお、検証環境のため、有効期限は10年(3650日)に設定しています。

[root@control harbor-certs]# openssl req -x509 -new -noenc -key ca.key -sha256 -days 3650 -out ca.crt -subj "/C=JP/ST=Tokyo/L=Machida/O=HomeLab/OU=CA/CN=HomeLab Root CA"

作成したCA証明書(ca.crt)と秘密鍵(ca.key)が正常に生成されていることを確認します。

[root@control harbor-certs]# ls -l
合計 8
-rw-r--r--. 1 root root 2033  7月 14 10:50 ca.crt
-rw-------. 1 root root 3268  7月 14 10:49 ca.key

7.2 harborのサーバ証明書の作成

HarborとHTTPS通信を行うため、harbor.home.lab用のサーバー証明書を作成します。

まず、Harborで使用する4096ビットRSA秘密鍵を生成します。

[root@control harbor-certs]# openssl genpkey -algorithm RSA -out harbor.home.lab.key -pkeyopt rsa_keygen_bits:4096

作成したサーバー秘密鍵が生成されていることを確認します。

[root@control harbor-certs]# ls -l
合計 12
-rw-r--r--. 1 root root 2033  7月 14 10:50 ca.crt
-rw-------. 1 root root 3268  7月 14 10:49 ca.key
-rw-------. 1 root root 3272  7月 14 10:51 harbor.home.lab.key

続いて、秘密鍵を使用してCSR(Certificate Signing Request:証明書署名要求)を作成します。CSRには、証明書へ埋め込むサーバー名などの情報が含まれます。

[root@control harbor-certs]# openssl req -new -key harbor.home.lab.key \
  -out harbor.home.lab.csr \
  -subj "/C=JP/ST=Tokyo/L=Machida/O=HomeLab/OU=Harbor/CN=harbor.home.lab"

CSR(harbor.home.lab.csr)とサーバー秘密鍵(harbor.home.lab.key)が生成されていることを確認します。

[root@control harbor-certs]# ls -l
合計 16
-rw-r--r--. 1 root root 2033  7月 14 10:50 ca.crt
-rw-------. 1 root root 3268  7月 14 10:49 ca.key
-rw-r--r--. 1 root root 1704  7月 14 10:51 harbor.home.lab.csr
-rw-------. 1 root root 3272  7月 14 10:51 harbor.home.lab.key

サーバー証明書へSAN(Subject Alternative Name)を設定するための設定ファイルを作成します。Webブラウザでは、証明書のSANに接続先のホスト名やIPアドレスが登録されていることが必須となっています。

[root@control harbor-certs]# vi harbor.home.lab.ext
[root@control harbor-certs]# cat harbor.home.lab.ext
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = harbor.home.lab
IP.1 = 192.168.1.200

作成したCSRをプライベートCAの秘密鍵で署名し、Harborのサーバー証明書を発行します。検証環境のため、有効期限は1年(365日)に設定しています。

[root@control harbor-certs]# openssl x509 -req -in harbor.home.lab.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out harbor.home.lab.crt -days 365 -sha256 -extfile harbor.home.lab.ext
Certificate request self-signature ok
subject=C=JP, ST=Tokyo, L=Machida, O=HomeLab, OU=Harbor, CN=harbor.home.lab

サーバー証明書(harbor.home.lab.crt)、CSR、秘密鍵などのファイルが生成されていることを確認します。

[root@control harbor-certs]# ls -l
合計 28
-rw-r--r--. 1 root root 2033  7月 14 10:50 ca.crt
-rw-------. 1 root root 3268  7月 14 10:49 ca.key
-rw-r--r--. 1 root root   41  7月 14 10:52 ca.srl
-rw-r--r--. 1 root root 2126  7月 14 10:52 harbor.home.lab.crt
-rw-r--r--. 1 root root 1704  7月 14 10:51 harbor.home.lab.csr
-rw-r--r--. 1 root root  257  7月 14 10:52 harbor.home.lab.ext
-rw-------. 1 root root 3272  7月 14 10:51 harbor.home.lab.key

最後に、サーバー証明書へSANが正しく設定されていることを確認します。
>||
[root@control harbor-certs]# openssl x509 -in harbor.home.lab.crt -text -noout | grep -A2 "Subject Alternative Name"
            X509v3 Subject Alternative Name:
                DNS:harbor.home.lab, IP Address:192.168.1.200
            X509v3 Subject Key Identifier:

7.3 Kubernetes Secretへの登録

HarborのIngressからTLS証明書を利用できるよう、作成したサーバー証明書と秘密鍵をKubernetes Secretとして登録します。

[root@control harbor-certs]# kubectl create secret tls harbor-tls \
  --cert=harbor.home.lab.crt \
  --key=harbor.home.lab.key \
  -n harbor
secret/harbor-tls created
[root@control harbor-certs]#

ConfigMap/Secretの詳細は、以下のページをご覧ください。
hana-shin.hatenablog.com

作成したTLS Secretが登録されていることを確認します。

[root@control harbor-certs]# kubectl get secret harbor-tls -n harbor
NAME         TYPE                DATA   AGE
harbor-tls   kubernetes.io/tls   2      30s

8 PC側の作業

PCのブラウザから、コントロールノード上で稼働するHarborへHTTPSでアクセスします。このとき、ブラウザはHarborから送られてくるサーバ証明書を検証します。サーバ証明書の検証には、CA証明書に含まれる公開鍵が使用されます。そのため、事前にCA証明書をPCへインポートしておく必要があります。ここでは、CA証明書をWindowsへインポートする手順を説明します。

8.1 hostsファイルの編集

PCからHarborへホスト名でアクセスできるよう、Windowsのhostsファイル(C:\Windows\System32\drivers\etc\hosts)に以下の内容を追記します。

192.168.1.200  harbor.home.lab

8.2 CA証明書のインポート

コントロールノードで作成したCA証明書(ca.crt)をPCへ転送します。次に、転送したCA証明書をWindowsの証明書ストア(信頼されたルート証明機関)へインポートします。ChromeやMicrosoft EdgeはWindowsの証明書ストアを利用するため、この設定を行うことで、ブラウザからHarborへHTTPSでアクセスすることができるようになります。

まず、[Windows]キー + [R] を押して 「ファイル名を指定して実行」 を開きます。そして、「certmgr.msc」 と入力し、「OK」 をクリックします。

証明書マネージャー(Certmgr)が起動したら、「信頼されたルート証明機関」→「証明書」 を右クリックし、「すべてのタスク」→「インポート」 を選択します。その後、ウィザードに従って転送したCA証明書を選択し、Windowsへインポートします。

「次へ」をクリックする。

作成したCA証明書(ca.crt)を指定して「次へ」をクリックします。

「証明書をすべて次のストアに配置する(P)」から「信頼されたルート証明機関」を選択して「次へ」をクリックする。

「完了」をクリックする。

「はい」をクリックする

「正しくインポートされました」のポップアップ画面を確認する。

9 Harborのインストール

Harbor用のNamespaceを作成します。

[root@control ~]# kubectl create namespace harbor
namespace/harbor created

Harborのリポジトリを追加します。

[root@control ~]# helm repo add harbor https://helm.goharbor.io
"harbor" has been added to your repositories

リポジトリの情報を最新の状態に更新します。

[root@control ~]# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "metallb" chart repository
...Successfully got an update from the "ingress-nginx" chart repository
...Successfully got an update from the "harbor" chart repository
Update Complete. ?Happy Helming!?

追加したリポジトリを確認します。

[root@control ~]#  helm repo list
NAME            URL
metallb         https://metallb.github.io/metallb
ingress-nginx   https://kubernetes.github.io/ingress-nginx
harbor          https://helm.goharbor.io

helm search repoコマンドを実行して、リポジトリに登録されているチャートを確認します。実行結果から、harborチャートが利用可能であることを確認できます。

[root@control ~]# helm search repo harbor
NAME            CHART VERSION   APP VERSION     DESCRIPTION
harbor/harbor   1.19.1          2.15.1          An open source trusted cloud native registry th...

Harborインストール用の作業ディレクトリを作成します。

[root@control ~]#  mkdir -p ~/harbor-install

HarborはHelmチャートを使用してインストールします。Helmでは、values.yamlに設定を記述することで、デフォルト設定を変更できます。ここでは、Harborへアクセスするホスト名やTLSの設定、永続ストレージの設定、管理者パスワードなどを指定するため、values.yamlを作成します。

[root@control ~]# vi ~/harbor-install/values.yaml
[root@control ~]# cat ~/harbor-install/values.yaml
expose:
  type: ingress
  tls:
    enabled: true
    certSource: secret
    secret:
      secretName: "harbor-tls"
  ingress:
    hosts:
      core: harbor.home.lab
    className: "nginx"

externalURL: https://harbor.home.lab

persistence:
  enabled: true
  persistentVolumeClaim:
    registry:
      storageClass: "local-path"
      size: 20Gi
    jobservice:
      jobLog:
        storageClass: "local-path"
        size: 1Gi
    database:
      storageClass: "local-path"
      size: 5Gi
    redis:
      storageClass: "local-path"
      size: 1Gi
    trivy:
      storageClass: "local-path"
      size: 5Gi

harborAdminPassword: "Harbor12345"

Harborをインストールします。-fオプションで指定したvalues.yamlの内容(Ingress・TLS・永続化ストレージの設定)が反映されます。

[root@control ~]# helm install harbor harbor/harbor -n harbor -f ~/harbor-install/values.yaml
NAME: harbor
LAST DEPLOYED: Sun Jul 12 11:24:12 2026
NAMESPACE: harbor
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
Please wait for several minutes for Harbor deployment to complete.
Then you should be able to visit the Harbor portal at https://harbor.home.lab
For more details, please visit https://github.com/goharbor/harbor

Harborがインストールされていることを確認します。

[root@control ~]# helm list -n harbor
NAME    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
harbor  harbor          1               2026-07-12 11:24:12.830505033 +0900 JST deployed        harbor-1.19.1   2.15.1

HarborのPodが正常に起動したことを確認します。

[root@control ~]# kubectl get pods -n harbor
NAME                                 READY   STATUS    RESTARTS      AGE
harbor-core-767d58bf54-6kh8f         1/1     Running   0             2m32s
harbor-database-0                    1/1     Running   0             2m32s
harbor-jobservice-84b7976678-l88l5   1/1     Running   4 (80s ago)   2m32s
harbor-portal-5b6bc45d5c-wtwd6       1/1     Running   0             2m32s
harbor-redis-0                       1/1     Running   0             2m32s
harbor-registry-54f749db94-x2spw     2/2     Running   0             2m32s
harbor-trivy-0                       1/1     Running   0             2m32s

Harborでは、コンテナイメージやデータベース、Redisなどの永続データをPersistentVolumeClaim(PVC)に保存します。すべてのPVCが Bound になっていれば、PersistentVolumeが正常に割り当てられています。

[root@control ~]# kubectl get pvc -n harbor
NAME                              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
data-harbor-redis-0               Bound    pvc-b5442603-8404-481e-8b93-938faf3e785e   1Gi        RWO            local-path     <unset>                 3m7s
data-harbor-trivy-0               Bound    pvc-b5fe7ad0-3510-44cc-a733-cb84a8f17eea   5Gi        RWO            local-path     <unset>                 3m7s
database-data-harbor-database-0   Bound    pvc-49907738-2af0-4864-b698-81f58eba1839   1Gi        RWO            local-path     <unset>                 3m7s
harbor-jobservice                 Bound    pvc-d236097f-637c-4525-b4bb-ecf4c7f9ed5e   1Gi        RWO            local-path     <unset>                 3m7s
harbor-registry                   Bound    pvc-f9800439-4cb1-4cca-b57b-a80b3ae96771   5Gi        RWO            local-path     <unset>                 3m7s

Harbor用のIngressリソースが作成されていることを確認します。HOSTSにharbor.home.lab、ADDRESSにMetalLBから割り当てられたIPアドレスが表示されていれば、Ingressが正しく構成されています。

[root@control ~]# kubectl get ingress -n harbor
NAME             CLASS   HOSTS             ADDRESS         PORTS     AGE
harbor-ingress   nginx   harbor.home.lab   192.168.1.200   80, 443   34h

10 Harbor管理画面へのログイン

ブラウザで https://harbor.home.lab/ にアクセスします。
ログイン画面が表示されたら、ユーザー名に 「admin」、パスワードに 「Harbor12345」 を入力してログインします。パスワードには、values.yaml の harborAdminPassword に設定した値を使用します。

ログインに成功すると、以下のようにHarborの管理画面が表示されます。

Z 参考図書

今回の記事執筆にあたり参考にした図書は以下のものです。

単行本

電子書籍

Volume(emptyDir/hostPath/PV・PVC)の挙動を確かめてみた

1 Volumeとは

KubernetesのVolumeとは、Pod内のコンテナがデータを保存・共有するための仕組みです。通常、コンテナのルートファイルシステムは一時的であり、コンテナが再生成されるとデータは失われます。Volumeを利用することで、次のようなことが可能になります。

  • コンテナの再起動後もデータを保持できる(Volumeの種類による)
  • 同一Pod内の複数コンテナ間でデータを共有できる
  • ノード上のディスクや、NFS、クラウドストレージなどの外部ストレージをコンテナから利用できる

VolumeはPod単位で定義され、各コンテナにマウントして使用します。Volumeにはさまざまな種類があり、emptyDir や hostPath のようにPodに直接定義するものと、PersistentVolumeClaim(PVC) を介して PersistentVolume(PV) を利用するものがあります。Volumeの種類によっては、Podが削除・再作成された後もデータを保持できます。

(1) Volumeタイプ(Podから直接指定するもの)

種類 概要 用途
emptyDir Pod起動時に作成され、Pod削除時に消える一時領域 一時ファイル
hostPath ノードのローカルディレクトリを直接マウントする。ストレージ変更時はPodの定義修正が必要 デバッグや単一ノードでの検証用。Podが別ノードに移動するとデータが引き継げないため、本番環境での利用は非推奨
nfs NFSサーバのディレクトリをマウント 複数Pod間での共有ストレージ

(2) 永続ストレージを抽象化する仕組み(PV/PVC)
PodはPVCを指定するだけで、背後でどのストレージ(hostPathなのかNFSなのか等)が使われているかを意識せずにVolumeを利用できます。

  • PV(PersistentVolume):ストレージ実体(例:NFS, クラウドディスクなど)をKubernetesリソースとして定義したもの
  • PVC(PersistentVolumeClaim):利用者が必要な容量・アクセスモードを要求し、PodからPVを利用するための窓口

2 検証環境

2.1 ネットワーク構成

検証環境は3台の仮想マシンでKubernetesクラスタを構成しています。

+--- control ---+    +--- worker1 ---+   +--- worker2 ---+
|               |    |               |   |               |
|AlmaLinux 10.2 |    |AlmaLinux 10.2 |   |AlmaLinux 10.2 |
|               |    |               |   |               |
+-------+-------+    +-------+-------+   +-------+-------+
        |.2                  |.139               |.171
        |                    |                   |
        |                    |                   |
        |   192.168.1.0/24   |                   |
+--------------------------------------------------------+
|                           KVM                          |
+--------------------------------------------------------+

それぞれの役割は以下のとおりです。
1台をコントロールノード、2台をワーカーノードとして使用します。

ホスト名 名称 役割
control コントロールノード クラスタ(control、worker1、worker2)の状態を管理し、Pod をどのノードで実行するかを決定するノード
worker1 ワーカーノード Pod を実行するノード
worker2 ワーカーノード Pod を実行するノード

2.2 ソフトウェアのバージョン

各ノードのAlmaLinuxバージョンは以下のとおりです。

[root@control ~]# cat /etc/redhat-release
AlmaLinux release 10.2 (Lavender Lion)

各ノードのカーネルバージョンは以下のとおりです。

[root@control ~]# uname -r
6.12.0-211.7.3.el10_2.x86_64

Kubernetesのバージョンは以下のとおりです。

[root@control ~]# kubectl version
Client Version: v1.35.3
Kustomize Version: v5.7.1
Server Version: v1.35.3

2.3 ノードのリソース

各ノードには4GBのメモリを割り当てています。

[root@control ~]#  free -h
               total        used        free      shared  buff/cache   available
Mem:           3.6Gi       1.3Gi       1.0Gi       5.8Mi       1.5Gi       2.3Gi
Swap:             0B          0B          0B

各ノードは 4コアのCPU(4 vCPU) を搭載しています。

[root@control ~]# lscpu -xe
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
  0    0      0    0 0:0:0:0          yes
  1    0      1    1 1:1:1:1          yes
  2    0      2    2 2:2:2:2          yes
  3    0      3    3 3:3:3:3          yes

lscpuコマンドの詳しい使い方は、以下のページをご覧ください。

hana-shin.hatenablog.com

3 emptyDirの使い方

emptyDirは、Podが起動したときに作成され、Podが削除されると消える一時的なVolumeです。ここでは、以下について確認します。

  • Pod内のコンテナ間でファイル共有ができること
  • Podが削除されるとデータも削除されること

まず、YAMLファイルを作成します。このPodにはcontainer1(nginx)とcontainer2(busybox)という2つのコンテナが定義されており、両方が同じemptyDirボリュームshared-volumeを/dataにマウントしています。

[root@control ~]# vi emptydir.yaml
[root@control ~]# cat emptydir.yaml
apiVersion: v1
kind: Pod
metadata:
  name: emptydir-test
spec:
  containers:
  - name: container1
    image: nginx
    volumeMounts:
    - name: shared-volume
      mountPath: /data

  - name: container2
    image: busybox
    command: ["sleep", "3600"]
    volumeMounts:
    - name: shared-volume
      mountPath: /data

  volumes:
  - name: shared-volume
    emptyDir: {}
[root@control ~]#

YAMLファイルの内容をKubernetesに適用し、Podを作成します。

[root@control ~]# kubectl apply -f emptydir.yaml
pod/emptydir-test created

Podの状態を確認します。READY が「2/2」、STATUS が「Running」となっていることから、Pod内の2つのコンテナが両方とも正常に動作していることがわかります。

[root@control ~]# kubectl get pods -o wide
NAME            READY   STATUS    RESTARTS   AGE     IP              NODE      NOMINATED NODE   READINESS GATES
emptydir-test   2/2     Running   0          3m17s   10.244.189.65   worker2   <none>           <none>

container1(nginxコンテナ)にログインします。

[root@control ~]# kubectl exec -it emptydir-test -c container1 -- /bin/bash

ログインしたら、emptyDirにマウントされた /data にファイルを作成します。

root@emptydir-test:/# echo "hello" > /data/test.txt
root@emptydir-test:/# cat /data/test.txt
hello
root@emptydir-test:/#

container1から抜けます。

root@emptydir-test:/# exit
exit

次に、container2(busyboxコンテナ)にログインします。

[root@control ~]# kubectl exec -it emptydir-test -c container2 -- /bin/sh

ログインしたら、container1で作成したファイルを確認します。container1で作成したファイルと同じ内容のファイルが確認できるので、emptyDirでデータ共有できていることがわかります。

/ # cat /data/test.txt
hello
/ #

container2から抜けます。

/ # exit
[root@control ~]#

次に、コンテナで作成したファイルがノード上のどこに保存されているのかを確認します。

まず、Podの詳細情報を取得し、その中からUIDを抽出します。このUIDは、ノード上のディレクトリ名として使用されます。

[root@control ~]# kubectl get pod emptydir-test -o yaml | grep uid
  uid: 8d8f6017-c297-4fe7-ba80-3c0c21a75c6d
        uid: 0
        uid: 0

次に、kubeletが管理しているディレクトリ配下に移動し、emptyDirの実体を確認します。

[root@worker2 shared-volume]# pwd
/var/lib/kubelet/pods/8d8f6017-c297-4fe7-ba80-3c0c21a75c6d/volumes/kubernetes.io~empty-dir/shared-volume

このディレクトリ内に、コンテナ内で作成したファイルが存在するか確認します。

[root@worker2 shared-volume]# ls
test.txt

ファイルの内容を確認すると、コンテナ内で作成した内容と一致していることがわかります。このことから、コンテナ内の /data に作成したファイルは、実際にはノード上の /var/lib/kubelet/ 配下に保存されていることがわかります。

[root@worker2 shared-volume]# cat test.txt
hello

最後に、Podを削除します。

[root@control ~]# kubectl delete -f emptydir.yaml

Podを削除したあとに /var/lib/kubelet/pods 配下を確認すると、作成した emptyDir のデータも削除されていることがわかります。このことから、emptyDir は Pod のライフサイクルに紐づいた一時的な領域であり、Pod が起動している間だけ有効であることが確認できます。なお、コンテナが再起動した場合は Pod は存続しているため、emptyDir のデータは保持されます。一方、Pod が削除されると、emptyDir のデータも削除されます。

4 hostPathの使い方

hostPath確認用のYAMLファイルを作成します。
このYAMLでは、ワーカーノード上の /tmp/hostpath-data 配下のディレクトリをコンテナ内の /data にマウントします。hostPath はノード上のローカルディレクトリをマウントするため、データは「Pod」ではなく「ノード」に紐づきます。このため、同じノードに再スケジュールされた場合は、以前保存したファイルをそのまま参照できますが、異なるノードに Pod が移動した場合は、移動前に別ノード上で hostPath に保存したデータを参照できません。なお、type: DirectoryOrCreate を指定することで、対象のディレクトリが存在しない場合は自動的に作成されます。

[root@control ~]# vi hostpath.yaml
[root@control ~]# cat hostpath.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hostpath-test
spec:
  containers:
  - name: container1
    image: nginx
    volumeMounts:
    - name: shared-volume
      mountPath: /data

  - name: container2
    image: busybox
    command: ["sleep", "3600"]
    volumeMounts:
    - name: shared-volume
      mountPath: /data

  volumes:
  - name: shared-volume
    hostPath:
      path: /tmp/hostpath-data
      type: DirectoryOrCreate

ちなみに、「3 emptyDirの使い方」で作成したYAMLファイルとの差分は以下のとおりです。

[root@control ~]# diff -Nur emptydir.yaml hostpath.yaml
--- emptydir.yaml       2026-03-23 14:52:36.262665552 +0900
+++ hostpath.yaml       2026-03-23 15:07:49.462094598 +0900
@@ -1,7 +1,7 @@
 apiVersion: v1
 kind: Pod
 metadata:
-  name: emptydir-test
+  name: hostpath-test
 spec:
   containers:
   - name: container1
@@ -19,4 +19,6 @@

   volumes:
   - name: shared-volume
-    emptyDir: {}
+    hostPath:
+      path: /tmp/hostpath-data
+      type: DirectoryOrCreate

YAMLファイルを適用し、hostPathを利用するPodを作成します。

[root@control ~]# kubectl apply -f hostpath.yaml
pod/hostpath-test created

Podの状態や配置されたノードを確認します。READY が「2/2」、STATUS が「Running」となっていることから、2つのコンテナが正常に動作していることがわかります。

[root@control ~]# kubectl get pods -o wide
NAME            READY   STATUS    RESTARTS   AGE   IP              NODE      NOMINATED NODE   READINESS GATES
hostpath-test   2/2     Running   0          75s   10.244.189.67   worker2   <none>           <none>

container1(nginxコンテナ)にログインします。

[root@control ~]# kubectl exec -it hostpath-test -c container1 -- /bin/bash

hostPathでマウントされた /data 配下にファイルを作成し、内容を確認します。

root@hostpath-test:/# echo "hello" > /data/test.txt
root@hostpath-test:/# cat /data/test.txt
hello
root@hostpath-test:/# exit
exit

container2(busyboxコンテナ)にログインします。

[root@control ~]# kubectl exec -it hostpath-test -c container2 -- /bin/sh

container1で作成したファイルが参照できることを確認します。

/ # cat /data/test.txt
hello
/ # exit
[root@control ~]#

ワーカーノード上のディレクトリに、同じファイルが作成されていることを確認します。

[root@worker2 ~]# cat /tmp/hostpath-data/test.txt
hello

作成したPodを削除します。

[root@control ~]# kubectl delete -f hostpath.yaml
pod "hostpath-test" deleted from default namespace

Podが削除されていることを確認します。

[root@control ~]# kubectl get pods -o wide
No resources found in default namespace.

ノード上にファイルが残っていることを確認します。

[root@worker2 ~]# cat /tmp/hostpath-data/test.txt
hello

次の検証のため、作成したPodを削除します。

[root@control ~]# kubectl delete -f hostpath.yaml
pod "hostpath-test" deleted from default namespace

5 PV(PersistentVolume)/PVC(PersistentVolumeClaim)の使い方

PVおよびPVCは、永続ストレージを管理するための仕組みです。前章で説明したhostPathはストレージのパスが変更されるとすべてのPodの定義を修正する必要があるなど、運用面での課題があります。これに対し、PV/PVCを利用することでストレージを抽象化し、Podからストレージの詳細を切り離すことができます。PodはPVCのみを参照するため、ストレージの変更はPV側で吸収でき、Podの定義を変更する必要がありません。本章では、PV/PVCの基本的な動きを理解するため、あえてバックエンドにhostPathを使った構成で検証を行います。

種類 概要 作成者
PV ストレージ本体(データの保存領域) 管理者
PVC PVを使うための申請。PodとPVをつなぐ役割 利用者

(1) 管理者側(PVの作成)
PV(PersistentVolume)を定義するためのYAMLファイルを作成します。このYAMLでは、ノード上の /tmp/pv-data をストレージとして使用するPVを定義しています。

[root@control ~]# vi pv.yaml
[root@control ~]# cat pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-test
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /tmp/pv-data

YAMLファイルをKubernetesに適用し、PersistentVolumeを作成します。

[root@control ~]# kubectl apply -f pv.yaml
persistentvolume/pv-test created

作成したPVの状態を確認します。STATUS が Available となっており、まだどのPVCにもバインドされていないことがわかります。

[root@control ~]# kubectl get pv -o wide
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
pv-test   1Gi        RWO            Retain           Available                          <unset>                          59s   Filesystem

(2) 利用者側(PVCの作成)

次に、先ほど作成したPVを利用するためのPVC(PersistentVolumeClaim)を作成します。PVCでは、必要な容量やアクセスモードを指定します。

[root@control ~]# vi pvc.yaml
[root@control ~]# cat pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-test
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

YAMLファイルを適用してPVCを作成します。

[root@control ~]# kubectl apply -f pvc.yaml
persistentvolumeclaim/pvc-test created

PVCを作成すると、条件に合うPVが自動的に割り当てられます。PVとPVCの状態を確認し、Bound になっていることを確認します。

[root@control ~]# kubectl get pv,pvc -o wide
NAME                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
persistentvolume/pv-test   1Gi        RWO            Retain           Bound    default/pvc-test                  <unset>                          44m   Filesystem

NAME                             STATUS   VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE    VOLUMEMODE
persistentvolumeclaim/pvc-test   Bound    pv-test   1Gi        RWO                           <unset>                 114s   Filesystem

(3) 利用者側(Podの作成)
PVにhostPathを使っているため、実際のデータ保存場所は /tmp/pv-data になります。挙動としてはhostPathと同様ですが、PV/PVCによりストレージの場所をPodから隠蔽できる点が異なります。これにより、Podを変更せずにストレージを差し替えることが可能となります。

PVCを利用するPodを定義するYAMLファイルを作成します。このPodでは、pvc-test を /data にマウントして使用します。

[root@control ~]# vi pod.yaml
[root@control ~]# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pvc-test-pod
spec:
  containers:
  - name: busybox
    image: busybox
    command: ["sleep", "3600"]
    volumeMounts:
    - name: storage
      mountPath: /data

  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: pvc-test

YAMLファイルを適用し、PVCを利用するPodを作成します。

[root@control ~]# kubectl apply -f pod.yaml
pod/pvc-test-pod created

Podの状態を確認します。STATUS が Running となり、正常に起動していることがわかります。

[root@control ~]# kubectl get pods -o wide
NAME           READY   STATUS    RESTARTS   AGE   IP               NODE      NOMINATED NODE   READINESS GATES
pvc-test-pod   1/1     Running   0          19s   10.244.235.145   worker1   <none>           <none>

作成したPodにログインし、/data にファイルを作成します。

[root@control ~]# kubectl exec -it pvc-test-pod -- /bin/sh
/ # echo "hello world" > /data/test.txt
/ # cat /data/test.txt
hello world

Pod から抜けます。

/ # exit

ノード上の /tmp/pv-data を確認し、コンテナ内で作成したファイルが保存されていることを確認します。

[root@worker1 ~]# cat /tmp/pv-data/test.txt
hello world

次に、Podを削除してもデータが残ることを確認します。まずPodを削除します。

[root@control ~]# kubectl delete -f pod.yaml
pod "pvc-test-pod" deleted from default namespace

Podが削除されていることを確認します。

[root@control ~]# kubectl get pods
No resources found in default namespace.

同じ定義のPodを再度作成します。

[root@control ~]# kubectl apply -f pod.yaml
pod/pvc-test-pod created

再作成したPodが Running になっていることを確認します。

[root@control ~]# kubectl get pods -o wide
NAME           READY   STATUS    RESTARTS   AGE   IP               NODE      NOMINATED NODE   READINESS GATES
pvc-test-pod   1/1     Running   0          28s   10.244.235.146   worker1   <none>           <none>

再度Podにログインし、以前作成したファイルが残っていることを確認します。

[root@control ~]# kubectl exec -it pvc-test-pod -- /bin/sh
/ # cat /data/test.txt
hello world

今回の検証では、Podが再度同じ worker1 にスケジュールされたためデータを引き継げています。hostPathを使用したPVの場合、別ノードにスケジュールされるとデータは参照できなくなるため、実運用ではネットワークストレージ(NFSやクラウドの永続ディスクなど)や、NodeAffinityを設定したLocal Volumeが使われます。

最後に、検証で使用したPod、PVC、PVを削除してクリーンアップします。

[root@control ~]# kubectl delete -f pod.yaml
pod "pvc-test-pod" deleted from default namespace
[root@control ~]# kubectl delete -f pvc.yaml
persistentvolumeclaim "pvc-test" deleted from default namespace
[root@control ~]# kubectl delete -f pv.yaml
persistentvolume "pv-test" deleted

Z 参考図書

今回の記事執筆にあたり参考にした図書は以下のものです。

単行本

電子書籍

KubernetesのNamespaceを理解する~ Leaseオブジェクトはどのように更新されるのか

1 Namespaceとは

Namespaceは、単一のクラスタ内を論理的に分割する仕組みです。チームや環境(開発・本番など)ごとにリソースを分けて管理できるため、複数のチームでクラスタを共有する際に役立ちます。たとえば、チームAとチームBが同じクラスタを使用している場合、チームAは team-a、チームBは team-b といったNamespaceをそれぞれ利用することで、お互いのリソース名が衝突することなく独立して開発を進めることができます。なお、PodやServiceなどのリソースはNamespaceごとに分離されますが、Nodeやストレージ(PersistentVolume)などはクラスタ全体で共有されるリソースです。

2 検証環境

2.1 ネットワーク構成

検証環境は3台の仮想マシンでKubernetesクラスタを構成しています。

+--- control ---+    +--- worker1 ---+   +--- worker2 ---+
|               |    |               |   |               |
|AlmaLinux 10.2 |    |AlmaLinux 10.2 |   |AlmaLinux 10.2 |
|               |    |               |   |               |
+-------+-------+    +-------+-------+   +-------+-------+
        |.19                 |.20                |.22
        |                    |                   |
        |                    |                   |
        |   192.168.1.0/24   |                   |
+--------------------------------------------------------+
|                           KVM                          |
+--------------------------------------------------------+

それぞれの役割は以下のとおりです。
1台をコントロールノード、2台をワーカーノードとして使用します。

ホスト名 名称 役割
control コントロールノード クラスタ(control、worker1、worker2)の状態を管理し、Pod をどのノードで実行するかを決定するノード
worker1 ワーカーノード Pod を実行するノード
worker2 ワーカーノード Pod を実行するノード

2.2 ソフトウェアのバージョン

各ノードのAlmaLinuxバージョンは以下のとおりです。

[root@control ~]# cat /etc/redhat-release
AlmaLinux release 10.2 (Lavender Lion)

各ノードのカーネルバージョンは以下のとおりです。

[root@control ~]# uname -r
6.12.0-211.7.3.el10_2.x86_64

Kubernetesのバージョンは以下のとおりです。

[root@control ~]# kubectl version
Client Version: v1.35.3
Kustomize Version: v5.7.1
Server Version: v1.35.3

2.3 ノードのリソース

各ノードには4GBのメモリを割り当てています。

[root@control ~]#  free -h
               total        used        free      shared  buff/cache   available
Mem:           3.6Gi       1.3Gi       1.0Gi       5.8Mi       1.5Gi       2.3Gi
Swap:             0B          0B          0B

各ノードは 4コアのCPU(4 vCPU) を搭載しています。

[root@control ~]# lscpu -xe
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
  0    0      0    0 0:0:0:0          yes
  1    0      1    1 1:1:1:1          yes
  2    0      2    2 2:2:2:2          yes
  3    0      3    3 3:3:3:3          yes

lscpuコマンドの詳しい使い方は、以下のページをご覧ください。
hana-shin.hatenablog.com

3 Namespaceの使い方

3.1 Namespace一覧の確認方法

クラスター内のNamespace一覧を表示します。

[root@control ~]# kubectl get namespaces
NAME              STATUS   AGE
default           Active   10d
kube-node-lease   Active   10d
kube-public       Active   10d
kube-system       Active   10d
Namespace 説明
default Namespaceを指定しなかった場合に使われる、デフォルトのNamespace
kube-system Kubernetesシステムが作成するオブジェクトのためのNamespace
kube-public 未認証のクライアントも含め、全員が読み取り可能なNamespace。クラスター全体に公開したい情報を置く用途を想定しているが、公開は運用上の慣習であり必須ではない
kube-node-lease 各ノードに対応するLeaseオブジェクトを格納するNamespace。各ノードのkubeletがLeaseオブジェクトを定期的に更新し、コントロールプレーンはその更新状況を監視してノードの生存状態を確認する

kubernetes.io

3.2 Namespaceの作成・削除方法

team-aという名前のNamespaceを作成します。

[root@control ~]# kubectl create namespace team-a
namespace/team-a created

再度、クラスタに存在するNamespace一覧を表示します。team-a というNamespaceが新しく作成されたことが確認できます。

[root@control ~]# kubectl get namespaces
NAME              STATUS   AGE
default           Active   10d
kube-node-lease   Active   10d
kube-public       Active   10d
kube-system       Active   10d
team-a            Active   9s

team-aという名前のNamespaceを削除します。

[root@control ~]# kubectl delete namespaces team-a
namespace "team-a" deleted

再度、クラスタに存在するNamespaceの一覧を表示します。team-a というNamespaceが削除されたことが確認できます。

[root@control ~]# kubectl get namespaces
NAME              STATUS   AGE
default           Active   10d
kube-node-lease   Active   10d
kube-public       Active   10d
kube-system       Active   10d

3.3 指定したNamespaceにリソースを作成する方法

(1) リソースの作成
ここでは、team-a と team-b というNamespaceを作成し、それぞれでPodを起動してみます。

team-aという名前のNamespaceを作成します。

[root@control ~]# kubectl create namespace team-a
namespace/team-a created

team-bという名前のNamespaceを作成します。

[root@control ~]# kubectl create namespace team-b
namespace/team-b created

Namespace一覧を確認します。

[root@control ~]# kubectl get namespaces
NAME              STATUS   AGE
default           Active   11d
kube-node-lease   Active   11d
kube-public       Active   11d
kube-system       Active   11d
team-a            Active   87s
team-b            Active   85s

default のnamespaceでNginxのPodを起動します。

[root@control ~]# kubectl run nginx-default --image=nginx
pod/nginx-default created

team-a のnamespaceでNginxのPodを起動します。

[root@control ~]# kubectl run nginx-a --image=nginx --namespace=team-a
pod/nginx-a created

team-b のnamespaceでNginxのPodを起動します。

[root@control ~]# kubectl run nginx-b --image=nginx --namespace=team-b
pod/nginx-b created

defaultのnamespaceでNginxのPodが動作していることが確認できます。

[root@control ~]# kubectl get pods
NAME            READY   STATUS    RESTARTS   AGE
nginx-default   1/1     Running   0          65s

team-aという名前のnamespaceでNginxのPodが動作していることが確認できます。

[root@control ~]# kubectl get pods --namespace=team-a
NAME      READY   STATUS    RESTARTS   AGE
nginx-a   1/1     Running   0          45s

team-bという名前のnamespaceでNginxのPodが動作していることが確認できます。

[root@control ~]# kubectl get pods --namespace=team-b
NAME      READY   STATUS    RESTARTS   AGE
nginx-b   1/1     Running   0          37s

-A オプションを使用すると、すべてのNamespaceに存在するリソースを確認することができます。以下の例では、NginxのPodが default、team-a、team-b の各Namespaceで動作していることが確認できます。

[root@control ~]# kubectl get pods -A
NAMESPACE     NAME                                     READY   STATUS    RESTARTS       AGE
default       nginx-default                            1/1     Running   0              3m31s
kube-system   calico-kube-controllers-9dff488b-sxpdb   1/1     Running   13 (21m ago)   23d
kube-system   calico-node-7m6ph                        1/1     Running   13 (21m ago)   23d
kube-system   calico-node-vcmh5                        1/1     Running   13 (21m ago)   23d
kube-system   calico-node-w7ldj                        1/1     Running   13 (21m ago)   23d
kube-system   coredns-66869746d6-45g95                 1/1     Running   2 (21m ago)    2d23h
kube-system   coredns-66869746d6-zk5p6                 1/1     Running   2 (21m ago)    2d23h
kube-system   etcd-control                             1/1     Running   26 (21m ago)   23d
kube-system   kube-apiserver-control                   1/1     Running   25 (21m ago)   23d
kube-system   kube-controller-manager-control          1/1     Running   15 (21m ago)   23d
kube-system   kube-proxy-426t6                         1/1     Running   13 (21m ago)   23d
kube-system   kube-proxy-gr68g                         1/1     Running   13 (21m ago)   23d
kube-system   kube-proxy-krgq6                         1/1     Running   13 (21m ago)   23d
kube-system   kube-scheduler-control                   1/1     Running   15 (21m ago)   23d
team-a        nginx-a                                  1/1     Running   0              91s
team-b        nginx-b                                  1/1     Running   0              74s


(2) リソースの削除

Namespace team-a に存在するPodを削除します。

[root@control ~]# kubectl delete pod nginx-a -n team-a
pod "nginx-a" deleted from team-a namespace

Namespace team-b に存在するPodを削除します。

[root@control ~]# kubectl delete pod nginx-b -n team-b
pod "nginx-b" deleted from team-b namespace

default Namespaceに存在するPodを削除します。Namespaceを指定しない場合は、default が対象となります。

[root@control ~]# kubectl delete pod nginx
pod "nginx" deleted from default namespace

すべてのNamespaceのPod一覧を確認し、対象のPodが削除されていることを確認します。

[root@control ~]# kubectl get pod -A
NAMESPACE     NAME                                     READY   STATUS    RESTARTS       AGE
kube-system   calico-kube-controllers-9dff488b-sxpdb   1/1     Running   13 (50m ago)   23d
kube-system   calico-node-7m6ph                        1/1     Running   13 (49m ago)   23d
kube-system   calico-node-vcmh5                        1/1     Running   13 (50m ago)   23d
kube-system   calico-node-w7ldj                        1/1     Running   13 (49m ago)   23d
kube-system   coredns-66869746d6-45g95                 1/1     Running   2 (50m ago)    3d
kube-system   coredns-66869746d6-zk5p6                 1/1     Running   2 (49m ago)    3d
kube-system   etcd-control                             1/1     Running   26 (50m ago)   23d
kube-system   kube-apiserver-control                   1/1     Running   25 (50m ago)   23d
kube-system   kube-controller-manager-control          1/1     Running   15 (50m ago)   23d
kube-system   kube-proxy-426t6                         1/1     Running   13 (49m ago)   23d
kube-system   kube-proxy-gr68g                         1/1     Running   13 (50m ago)   23d
kube-system   kube-proxy-krgq6                         1/1     Running   13 (49m ago)   23d
kube-system   kube-scheduler-control                   1/1     Running   15 (50m ago)   23d

3.4 デフォルトNamespaceの確認・変更

現在のデフォルトNamespaceを確認します。この時点ではNamespaceが設定されていないため、何も表示されません(デフォルトでは default Namespaceが使用されます)。

[root@control ~]# kubectl config view --minify | grep namespace:
[root@control ~]#

デフォルトのNamespaceを team-a に変更します。

[root@control ~]# kubectl config set-context --current --namespace=team-a
Context "kubernetes-admin@kubernetes" modified.

再度、現在のデフォルトNamespaceを確認します。team-a に変更されていることが確認できます。

[root@control ~]# kubectl config view --minify | grep namespace:
    namespace: team-a

続いて、デフォルトのNamespaceを team-b に変更します。

[root@control ~]# kubectl config set-context --current --namespace=team-b
Context "kubernetes-admin@kubernetes" modified.

現在のデフォルトNamespaceを確認すると、team-b に変更されていることが確認できます。

[root@control ~]# kubectl config view --minify|grep namespace
    namespace: team-b

この状態で kubectl get pods を実行すると、デフォルトのNamespaceである team-b のPodが表示されます。

[root@control ~]# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
nginx-b   1/1     Running   0          11m

--namespace オプションを指定することで、他のNamespaceのPodを確認することもできます。まず、default NamespaceのPodを確認します。

[root@control ~]# kubectl get pods --namespace=default
NAME            READY   STATUS    RESTARTS   AGE
nginx-default   1/1     Running   0          12m

続いて、team-a NamespaceのPodを確認します。

[root@control ~]# kubectl get pods --namespace=team-a
NAME      READY   STATUS    RESTARTS   AGE
nginx-a   1/1     Running   0          12m

4 kube-node-leaseについて

kube-node-lease Namespaceの使用目的は、各ノードに対応するLeaseオブジェクトを格納することです。Leaseオブジェクトは、各ノードのkubeletによって定期的に更新され、ノードの生存監視(ハートビート)に使用されます。各ノードのkubeletは、自身に対応するLeaseオブジェクトが存在しない場合、Leaseオブジェクトを作成します。作成要求はkube-apiserver経由で送信され、Leaseオブジェクトはetcdに保存されます。その後、kubeletは一定間隔(デフォルトでは約10秒ごと)で、kube-apiserver経由でLeaseオブジェクトのspec.renewTimeを更新します。更新されたLeaseオブジェクトは、kube-apiserverによってetcdに保存されます。

万一、kubeletの停止やノード障害、ネットワーク障害などによりspec.renewTimeが更新されなくなると、コントロールプレーンはLeaseオブジェクトの更新が一定時間行われていないことを検知します。更新がleaseDurationSeconds(デフォルトでは40秒)を超えて途絶えると、Node ControllerはそのノードをNotReady状態と判断します。その後、その状態が継続すると、Node Controllerはそのノード上で動作しているPodを別の正常なノードへ再スケジュール(再配置)する処理を開始します。

kube-node-lease Namespaceに格納されているLeaseオブジェクトを確認します。ノードごとに1つのLeaseオブジェクトが存在し、Leaseオブジェクトの名前がノード名と同じであることがわかります。

[root@control ~]# kubectl get leases -n kube-node-lease
NAME      HOLDER    AGE
control   control   29d
worker1   worker1   29d
worker2   worker2   3d1h

次に、worker1ノードに対応するLeaseオブジェクトの内容を確認します。

[root@control ~]# kubectl get lease worker1 -n kube-node-lease -o yaml
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
  creationTimestamp: "2026-06-05T05:37:30Z"
  name: worker1
  namespace: kube-node-lease
  ownerReferences:
  - apiVersion: v1
    kind: Node
    name: worker1
    uid: 8d828898-9282-4acd-b556-ceb068c8ff08
  resourceVersion: "132079"
  uid: a8fa8344-4044-47ff-9d3d-b94713364b29
spec:
  holderIdentity: worker1
  leaseDurationSeconds: 40
  renewTime: "2026-07-04T12:47:54.251743Z"

renewTimeの更新間隔を確認するため、LeaseオブジェクトのrenewTimeを1秒ごとに表示するrenewtime.shを作成して実行しました。
実行結果を見ると、renewTimeが約10秒ごとに更新されていることがわかります。このrenewTimeは、worker1ノード上で動作するkubeletによって定期的に更新されます。

[root@control ~]#  ./renewtime.sh
09:22:58   renewTime: "2026-07-05T00:22:54.205486Z"
09:22:59   renewTime: "2026-07-05T00:22:54.205486Z"
09:23:00   renewTime: "2026-07-05T00:22:54.205486Z"
09:23:01   renewTime: "2026-07-05T00:22:54.205486Z"
09:23:02   renewTime: "2026-07-05T00:22:54.205486Z"
09:23:03   renewTime: "2026-07-05T00:22:54.205486Z"
09:23:04   renewTime: "2026-07-05T00:22:54.205486Z"
09:23:05   renewTime: "2026-07-05T00:23:04.473769Z"
09:23:06   renewTime: "2026-07-05T00:23:04.473769Z"
09:23:07   renewTime: "2026-07-05T00:23:04.473769Z"
09:23:08   renewTime: "2026-07-05T00:23:04.473769Z"
09:23:09   renewTime: "2026-07-05T00:23:04.473769Z"
09:23:10   renewTime: "2026-07-05T00:23:04.473769Z"
09:23:11   renewTime: "2026-07-05T00:23:04.473769Z"
09:23:12   renewTime: "2026-07-05T00:23:04.473769Z"
09:23:13   renewTime: "2026-07-05T00:23:04.473769Z"
09:23:14   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:15   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:16   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:17   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:19   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:20   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:21   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:22   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:23   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:24   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:25   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:26   renewTime: "2026-07-05T00:23:24.830596Z"

kubeletサービスを停止します。

[root@worker1 ~]# systemctl stop kubelet.service

kubeletサービスを停止すると、renewTimeが更新されなくなることが確認できます。これは、Leaseオブジェクトの更新をkubeletが行っていることを示しています。

[root@control ~]#  ./renewtime.sh
-snip-
09:23:23   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:24   renewTime: "2026-07-05T00:23:14.799886Z"
09:23:25   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:26   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:27   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:28   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:29   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:30   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:31   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:32   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:33   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:34   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:35   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:36   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:37   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:38   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:39   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:40   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:41   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:42   renewTime: "2026-07-05T00:23:24.830596Z"
09:23:44   renewTime: "2026-07-05T00:23:24.830596Z"

kubeletサービスを停止すると、LeaseオブジェクトのrenewTimeの更新が停止します。その後、一定時間が経過すると、kube-controller-managerが更新が止まったことを検知し、worker1ノードをNotReadyと判定します。

[root@control ~]# date;kubectl get nodes
2026年  7月  5日 日曜日 09:39:10 JST
NAME      STATUS   ROLES           AGE     VERSION
control   Ready    control-plane   29d     v1.35.5
worker1   Ready    <none>          29d     v1.35.5
worker2   Ready    <none>          3d13h   v1.35.5

[root@control ~]# date;kubectl get nodes
2026年  7月  5日 日曜日 09:39:16 JST
NAME      STATUS   ROLES           AGE     VERSION
control   Ready    control-plane   29d     v1.35.5
worker1   Ready    <none>          29d     v1.35.5
worker2   Ready    <none>          3d13h   v1.35.5

[root@control ~]# date;kubectl get nodes
2026年  7月  5日 日曜日 09:39:19 JST
NAME      STATUS     ROLES           AGE     VERSION
control   Ready      control-plane   29d     v1.35.5
worker1   NotReady   <none>          29d     v1.35.5
worker2   Ready      <none>          3d13h   v1.35.5

Z 参考図書

今回の記事執筆にあたり参考にした図書は以下のものです。

単行本

電子書籍

ConfigMap/Secretの使い方

1 ConfigMap/Secretとは

種別 概要
ConfigMap アプリケーションの設定を管理するKubernetesオブジェクトです。設定はコンテナイメージには含めず、外部リソースとして管理します。そのため、コンテナイメージを作り直すことなく設定を変更できます。なお、変更内容の反映方法は、ConfigMapの利用方法(Volumeとしてマウントするか、環境変数として参照するか)によって異なります。3章では、その違いを実機で確認します。
Secret アプリケーションの機密情報(パスワードやAPIキーなど)を管理するためのKubernetesオブジェクトです。機密情報はコンテナイメージの中には含めず、外部リソースとして管理します。そのため、コンテナイメージを作り直すことなく機密情報だけを変更できます。変更内容の反映方法は、Secretの利用方法(Volumeとしてマウントするか、環境変数として参照するか)によって異なります。Secretはbase64形式で保存されますが、暗号化されているわけではありません。

2 検証環境

2.1 ネットワーク構成

検証環境は3台の仮想マシンでKubernetesクラスタを構成しています。

+--- control ---+    +--- worker1 ---+   +--- worker2 ---+
|               |    |               |   |               |
|AlmaLinux 10.2 |    |AlmaLinux 10.2 |   |AlmaLinux 10.2 |
|               |    |               |   |               |
+-------+-------+    +-------+-------+   +-------+-------+
        |.19                 |.20                |.22
        |                    |                   |
        |                    |                   |
        |   192.168.1.0/24   |                   |
+--------------------------------------------------------+
|                           KVM                          |
+--------------------------------------------------------+

それぞれの役割は以下のとおりです。
1台をコントロールノード、2台をワーカーノードとして使用します。

ホスト名 名称 役割
control コントロールノード クラスタ(control、worker1、worker2)の状態を管理し、Pod をどのノードで実行するかを決定するノード
worker1 ワーカーノード Pod を実行するノード
worker2 ワーカーノード Pod を実行するノード

2.2 ソフトウェアのバージョン

各ノードのAlmaLinuxバージョンは以下のとおりです。

[root@control ~]# cat /etc/redhat-release
AlmaLinux release 10.2 (Lavender Lion)

各ノードのカーネルバージョンは以下のとおりです。

[root@control ~]# uname -r
6.12.0-211.7.3.el10_2.x86_64

Kubernetesのバージョンは以下のとおりです。

[root@control ~]# kubectl version
Client Version: v1.35.3
Kustomize Version: v5.7.1
Server Version: v1.35.3

2.3 ノードのリソース

各ノードには4GBのメモリを割り当てています。

[root@control ~]#  free -h
               total        used        free      shared  buff/cache   available
Mem:           3.6Gi       1.3Gi       1.0Gi       5.8Mi       1.5Gi       2.3Gi
Swap:             0B          0B          0B

各ノードは 4コアのCPU(4 vCPU) を搭載しています。

[root@control ~]# lscpu -xe
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
  0    0      0    0 0:0:0:0          yes
  1    0      1    1 1:1:1:1          yes
  2    0      2    2 2:2:2:2          yes
  3    0      3    3 3:3:3:3          yes

lscpuコマンドの詳しい使い方は、以下のページをご覧ください。
hana-shin.hatenablog.com

3 ConfigMapの利用方法

3.1 Volumeとしてマウントする

(1) ConfigMapを新規作成

ConfigMapのマニフェストファイルを作成します。ここでは、Webサーバ(nginx)が表示するHTMLファイル(index.html)をConfigMapに保存します。なお、マニフェストファイル中の|(リテラルブロックスケーラー)は、改行を保持したまま、複数行の文字列を値として記述できます。

[root@control ~]# vi configmap.yaml
[root@control ~]# cat configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-html
data:
  index.html: |
    good morning!
    good morning!

作成したマニフェストファイルをKubernetesクラスタに適用し、ConfigMapを作成します。

[root@control ~]# kubectl apply -f configmap.yaml
configmap/nginx-html created

ConfigMap(nginx-html)が作成されたことを確認します。

[root@control ~]# kubectl get configmaps
NAME               DATA   AGE
kube-root-ca.crt   1      24d
nginx-html         1      11s

ConfigMapをVolumeとしてPodにマウントするDeploymentのマニフェストファイルを作成します。

[root@control ~]# vi deploy-configmap.yaml
[root@control ~]# cat deploy-configmap.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
          readOnly: true
      volumes:
      - name: html-volume
        configMap:
          name: nginx-html

Deploymentを作成します。

[root@control ~]# kubectl apply -f deploy-configmap.yaml
deployment.apps/nginx-deploy created

Podが起動していることを確認します。

[root@control ~]# kubectl get pods -o wide
NAME                           READY   STATUS    RESTARTS   AGE   IP              NODE      NOMINATED NODE   READINESS GATES
nginx-deploy-757844654-wkcq4   1/1     Running   0          13s   10.244.189.68   worker2   <none>           <none>

ローカル環境からPodへアクセスするため、kubectl port-forwardを実行します。

[root@control ~]# kubectl port-forward nginx-deploy-757844654-wkcq4 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

別ターミナルを開いて、アクセス確認を行います。

[root@control ~]# curl http://localhost:8080
good morning!
good morning!

(2) ConfigMapの更新(Pod再起動なしで自動反映を確認)
ConfigMapのマニフェストファイルを「good morning!」から「good evening!」へ変更します。

[root@control ~]# vi configmap.yaml
[root@control ~]# cat configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-html
data:
  index.html: |
    good evening!
    good evening!

更新したマニフェストファイルをKubernetesクラスタに適用し、ConfigMapを更新します。新規作成時はcreatedと表示されましたが、既存のConfigMapを更新する場合はconfiguredと表示されます。

[root@control ~]# kubectl apply -f configmap.yaml
configmap/nginx-html configured

Podを再起動せず、kubectl port-forwardを実行したままの状態で、再度アクセスします。

[root@control ~]# curl http://localhost:8080
good evening!
good evening!

最初に起動したPodを再起動・再作成することなく、ConfigMapの変更内容が反映されたことを確認できました。ConfigMapをVolumeとしてマウントしている場合、Kubernetesは変更内容をPod内へ自動的に反映します。ただし、本検証環境では反映まで約1分かかりました。一方、ConfigMapを環境変数として参照している場合は、値は自動更新されず、変更を反映するにはPodの再起動が必要です。

(3) あと始末
検証で使用したDeploymentとConfigMapを削除し、環境をクリーンな状態に戻します。

作成したDeploymentを削除します。

[root@control ~]# kubectl delete -f deploy-configmap.yaml
deployment.apps "nginx-deploy" deleted from default namespace

作成したConfigMapを削除します。

[root@control ~]# kubectl delete -f configmap.yaml
configmap "nginx-html" deleted from default namespace

3.2 環境変数として参照する

(1) ConfigMapを新規作成

環境変数として利用するConfigMapを作成します。

[root@control ~]# vi configmap.yaml
[root@control ~]# cat configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  MESSAGE: "good morning!"

作成したマニフェストファイルをKubernetesクラスタに適用し、ConfigMapを作成します。

[root@control ~]# kubectl apply -f configmap.yaml
configmap/app-config created

ConfigMapが作成されたことを確認します。

[root@control ~]# kubectl get configmaps
NAME               DATA   AGE
app-config         1      13s
kube-root-ca.crt   1      27d

ConfigMapを環境変数として参照するDeploymentのマニフェストファイルを作成します。

[root@control ~]# vi deploy-configmap.yaml
[root@control ~]# cat deploy-configmap.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        env:
        - name: MESSAGE
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: MESSAGE

Deploymentを作成します。

[root@control ~]# kubectl apply -f deploy-configmap.yaml
deployment.apps/nginx-deploy created

Podが起動していることを確認します。

[root@control ~]# kubectl get pods -o wide
NAME                           READY   STATUS    RESTARTS   AGE   IP               NODE      NOMINATED NODE   READINESS GATES
nginx-deploy-89768bd6f-qnp4k   1/1     Running   0          57s   10.244.235.133   worker1   <none>           <none>

Podへログインします。

[root@control ~]# kubectl exec -it nginx-deploy-89768bd6f-qnp4k -- bash

ConfigMapの値が環境変数 MESSAGE として設定されていることを確認します。

root@nginx-deploy-89768bd6f-qnp4k:/# echo $MESSAGE
good morning!

(2) ConfigMapの更新
別のターミナルを開き、ConfigMapを更新します。

[root@control ~]# vi configmap.yaml
[root@control ~]# cat configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  MESSAGE: "good evening!"

更新したマニフェストファイルを適用し、ConfigMapを更新します。

[root@control ~]# kubectl apply -f configmap.yaml
configmap/app-config configured

ConfigMapを更新したあと、環境変数の値を確認します。

root@nginx-deploy-89768bd6f-qnp4k:/# date;echo $MESSAGE
Thu Jul  2 11:15:03 UTC 2026
good morning!

ConfigMapを更新しても、環境変数の値は変更されません。

root@nginx-deploy-89768bd6f-qnp4k:/# date;echo $MESSAGE
Thu Jul  2 11:17:05 UTC 2026
good morning!

コンテナからログアウトします。

root@nginx-deploy-89768bd6f-qnp4k:/# exit
exit
command terminated with exit code 127

(3) Deploymentを再起動して変更を反映する

Deploymentが存在することを確認します。

[root@control ~]# kubectl get deployments.apps
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deploy   1/1     1            1           14m

Deploymentをロールアウト再起動します。これにより、新しいPodが作成されます。

[root@control ~]# kubectl rollout restart deployment nginx-deploy
deployment.apps/nginx-deploy restarted

新しいPodが作成されたことを確認します。

[root@control ~]# kubectl get pods -o wide
NAME                            READY   STATUS    RESTARTS   AGE   IP              NODE      NOMINATED NODE   READINESS GATES
nginx-deploy-864c85b6d4-8tzdd   1/1     Running   0          17s   10.244.189.67   worker2   <none>           <none>

新しいPodへログインします。環境変数 MESSAGE の値が更新されていることを確認します。

[root@control ~]# kubectl exec -it nginx-deploy-864c85b6d4-8tzdd -- bash
root@nginx-deploy-864c85b6d4-8tzdd:/# echo $MESSAGE
good evening!

コンテナからログアウトします。

root@nginx-deploy-864c85b6d4-8tzdd:/# exit
exit
command terminated with exit code 130

(4) あと始末
検証で使用したDeploymentとConfigMapを削除し、環境をクリーンな状態に戻します。

作成したDeploymentを削除します。

[root@control ~]# kubectl delete -f deploy-configmap.yaml
deployment.apps "nginx-deploy" deleted from default namespace

作成したConfigMapを削除します。

[root@control ~]# kubectl delete -f configmap.yaml
configmap "app-config" deleted from default namespace

4 Secretの使い方

(1) Secretを新規作成

Secretを作成するためのマニフェストファイルを作成します。stringDataを使うことで、平文のまま値を記述できます(適用時に自動でBase64エンコードされます)。

[root@control ~]# vi secret.yaml
[root@control ~]# cat secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: nginx-html-secret
type: Opaque
stringData:
  index.html: |
    top secret!
    top secret!

作成したマニフェストファイルを適用し、Secretを作成します。

[root@control ~]# kubectl apply -f secret.yaml
secret/nginx-html-secret created

Secret(nginx-html-secret)が作成されたことを確認します。

[root@control ~]# kubectl get secrets
NAME                TYPE     DATA   AGE
nginx-html-secret   Opaque   1      3m54s

SecretをVolumeとしてマウントするDeploymentのマニフェストファイルを作成します。ConfigMapのときと同様に、mountPathはディレクトリとして指定し、subPathは使いません。

[root@control ~]# vi deploy-secret.yaml
[root@control ~]# cat deploy-secret.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
          readOnly: true
      volumes:
      - name: html-volume
        secret:
          secretName: nginx-html-secret

Deploymentを作成します。

[root@control ~]# kubectl apply -f deploy-secret.yaml
deployment.apps/nginx-deploy created

Podが起動していることを確認します。

[root@control ~]# kubectl get pods -o wide
NAME                            READY   STATUS    RESTARTS   AGE   IP               NODE      NOMINATED NODE   READINESS GATES
nginx-deploy-6b9bcfd675-nlkh4   1/1     Running   0          21s   10.244.235.167   worker1   <none>           <none>

ローカル環境からPodへアクセスするため、kubectl port-forwardを実行します。

[root@control ~]# kubectl port-forward nginx-deploy-6b9bcfd675-nlkh4 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

別ターミナルを開いて、アクセス確認を行います。

[root@control ~]# curl http://localhost:8080
top secret!
top secret!

Secretの中身がBase64でエンコードされていることも確認しておきます。

[root@control ~]# kubectl get secret nginx-html-secret -o yaml
apiVersion: v1
data:
  index.html: dG9wIHNlY3JldCEKdG9wIHNlY3JldCEK
kind: Secret
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"name":"nginx-html-secret","namespace":"default"},"stringData":{"index.html":"top secret!\ntop secret!\n"},"type":"Opaque"}
  creationTimestamp: "2026-06-30T04:38:24Z"
  name: nginx-html-secret
  namespace: default
  resourceVersion: "1429701"
  uid: 8ca32f54-7868-415f-98b3-591dbe312e88
type: Opaque

表示されたBase64文字列(dG9wIHNlY3JldCEKdG9wIHNlY3JldCEK)をデコードし、元の平文と一致することを確認します。

[root@control ~]# echo dG9wIHNlY3JldCEKdG9wIHNlY3JldCEK | base64 -d
top secret!
top secret!

(2) Secretの更新(Pod再起動なしで自動反映を確認)

Secretの内容を「top secret!」から「Confidential!」へ変更します。

[root@control ~]# vi secret.yaml
[root@control ~]# cat secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: nginx-html-secret
type: Opaque
stringData:
  index.html: |
    Confidential!
    Confidential!

更新したマニフェストファイルを適用します。

[root@control ~]# kubectl apply -f secret.yaml
secret/nginx-html-secret configured

Podを再起動せず、kubectl port-forwardも実行したままの状態で、再度アクセスして内容を確認します。

[root@control ~]# curl http://localhost:8080
Confidential!
Confidential!

最初に起動した Pod(再起動・再作成はしていません)に対して、Secret の変更内容が自動的に反映されたことを確認できました。Secret も ConfigMap と同様に、Volume としてマウントしている場合は、Pod 内のファイルへ変更が自動的に反映されます。なお、Secret を環境変数として参照している場合は、値の変更は自動的には反映されず、Pod の再起動(再作成)が必要になります。自動反映されるのは、Secret を Volume としてマウントしている場合のみです。

(3) あと始末
検証で使用したDeploymentとSecretを削除し、環境をクリーンな状態に戻します。

作成したDeploymentを削除します。

[root@control ~]# kubectl delete -f deploy-secret.yaml
deployment.apps "nginx-deploy" deleted from default namespace

作成したSecretを削除します。

[root@control ~]# kubectl delete -f secret.yaml
secret "nginx-html-secret" deleted from default namespace

Z 参考図書

今回の記事執筆にあたり参考にした図書は以下のものです。

単行本

電子書籍

cgroup v2 の使い方~pids コントローラのインタフェースファイル編

1 はじめに

cgroup(Control Groups)は、複数のプロセスをグループとして管理し、CPU、メモリ、I/O などのシステムリソースをグループ単位で制御・監視する仕組みです。cgroup では、プロセス管理を行うコア・インタフェースファイルと、リソース制御を行うコントローラ・インタフェースファイルを利用して、設定や状態確認を行います。

種類 ファイル名 役割
コア・インタフェースファイル cgroup. というプレフィックスから始まるファイル プロセスの所属管理(cgroup.procs)や、子 cgroup に割り当てるコントローラの制御(cgroup.subtree_control)など、cgroup 自体の共通管理を行う
コントローラ・インタフェースファイル cpu. や memory. など、制御対象のリソース名から始まるファイル CPU 使用率の制限やメモリ使用量の上限設定など、特定のリソースの監視・制御を行う

本ブログでは、コントローラ・インタフェースファイルのうち、プロセス数を制御する pids コントローラのインタフェースファイルについて説明します。

コア・インタフェースファイルについては、以下の記事を参照してください。

hana-shin.hatenablog.com

2 検証環境

2.1 ソフトウェアのバージョン

AlmaLinuxバージョンは以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 10.2 (Lavender Lion)

カーネルバージョンは以下のとおりです。

[root@server ~]# uname -r
6.12.0-211.7.3.el10_2.x86_64

2.2 ノードのリソース

3.5GBのメモリを割り当てています

[root@server ~]# free -h
               total        used        free      shared  buff/cache   available
Mem:           3.5Gi       522Mi       3.0Gi       9.1Mi       195Mi       3.0Gi
Swap:          1.0Gi          0B       1.0Gi

4コアのCPU(4 vCPU) を搭載しています。

[root@server ~]# lscpu -xe
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
  0    0      0    0 0:0:0:0          yes
  1    0      1    1 2:2:2:2          yes
  2    0      2    2 4:4:4:4          yes
  3    0      3    3 6:6:6:6          yes

lscpuコマンドの詳しい使い方は、以下のページをご覧ください。
hana-shin.hatenablog.com

3 pids コントローラのインタフェースファイル

test という名前の cgroup を作成します。

[root@server ~]# mkdir -p /sys/fs/cgroup/test

cgroup を作成すると、そのディレクトリ配下にコア・インタフェースファイル(cgroup.procs など)が自動的に生成されます。コア・インタフェースファイルについては、以下の記事を参照してください。

hana-shin.hatenablog.com

一方、cpu.* や pids.* などのコントローラ・インタフェースファイルは、無条件に生成されるわけではなく、親 cgroup の cgroup.subtree_control でそのコントローラが有効化されている場合にのみ生成されます。ルート(/sys/fs/cgroup)の cgroup.subtree_control を確認すると、pids コントローラがあらかじめ有効になっていることが分かります。

[root@server ~]# cat /sys/fs/cgroup/cgroup.subtree_control
cpu memory pids

今回作成した test cgroup はこのルートの直下に作成しているため、特別な操作をしなくても pids コントローラが有効な状態で引き継がれ、pids.* のコントローラ・インタフェースファイルが自動的に生成されます。

pids コントローラでは、プロセス数の確認や上限設定を行うために、以下のインタフェースファイルを使用します。

[root@server ~]# ls /sys/fs/cgroup/test/pids.*
/sys/fs/cgroup/test/pids.current  /sys/fs/cgroup/test/pids.events.local  /sys/fs/cgroup/test/pids.peak
/sys/fs/cgroup/test/pids.events   /sys/fs/cgroup/test/pids.max
ファイル名 役割
pids.current cgroup に所属しているプロセスの数を表す
pids.max cgroup 内で作成可能なプロセス数の上限を設定する。「max」と書き込むと無制限になる
pids.peak cgroup で過去に記録したプロセス数の最大値を表す
pids.events pids.max に達したことにより、新しいプロセスの作成が拒否された回数などのイベント情報を表す
pids.events.local 現在の cgroup 自身で発生したイベントを表す。子 cgroup で発生したイベントは含まれない

この章では、pids.current、pids.max、pids.peak、pids.events の4つのファイルを実際に操作しながら、それぞれの挙動を確認していきます。

3.1 実験

cgroup 内のプロセス数が上限(pids.max)に達すると、新しいプロセスを一切起動できなくなります。これは、状態確認用の cat や ls といった基本コマンドすら実行できなくなることを意味します。そのため、今回は制限の影響を受けない「確認用の別ターミナル」を同時に開いて実験を進めます。本記事では、root のターミナルと区別しやすいように確認用のターミナルは一般ユーザ(user1)でログインして使用します。以降は、操作用として root でログインしたターミナルと、状態確認用として一般ユーザ(user1)でログインしたターミナルの 2 つを使用します。

[user1@server ~]$

確認用ターミナルから、test cgroup に所属するプロセス数を確認します。まだ test cgroup にプロセスを移動していないため、test cgroup に所属するプロセス数(pids.current)の値は 0 です。

[user1@server ~]$ cat /sys/fs/cgroup/test/pids.current
0

test cgroup に移動する bash プロセスの PID を確認します。ここでは、bash プロセスの PID が 2563 であることが分かります。

[root@server ~]# echo $$
2563

実験の最後に、bash プロセスを元の cgroup に戻すため、現在 bash が所属している cgroup を確認します。ここでは、user.slice/user-0.slice/session-1.scope に所属していることが分かります。

[root@server ~]# cat /proc/$$/cgroup
0::/user.slice/user-0.slice/session-1.scope

bash プロセスを test cgroup に移動します。$$ は現在実行中の bash の PID を表しており、cgroup.procs に書き込むことで、bash プロセスを test cgroup に移動させることができます。

[root@server ~]# echo $$ > /sys/fs/cgroup/test/cgroup.procs

bashプロセスが所属するcgroupを確認すると、test cgroupに移動したことがわかります。

[root@server ~]# cat /proc/$$/cgroup
0::/test

bash プロセスがtest cgroup に移動したため、pids.current の値が 0 から 1 に変化したことが分かります。

[user1@server ~]$ cat /sys/fs/cgroup/test/pids.current
1

ここで、test cgroup に所属できる最大プロセス数を 3 に設定してみます。

[root@server ~]# echo 3 > /sys/fs/cgroup/test/pids.max

test cgroup に所属する bash プロセスから sleep プロセスを起動します。親プロセスである bash が test cgroup に所属しているため、子プロセスである sleep も自動的に test cgroup に所属します。

[root@server ~]# sleep 600 &
[1] 2606

プロセス数(pids.current)を確認すると、bash プロセスと sleep プロセスの合計 2 個になっていることが分かります。

[user1@server ~]$ cat /sys/fs/cgroup/test/pids.current
2

pkill コマンドを実行して sleep プロセスを終了します。

[root@server ~]# pkill sleep
[1]+  Terminated              sleep 600

sleep プロセスを終了したため、test cgroup に所属するプロセスが 2 個から 1 個に減少したことが確認できます。

[user1@server ~]$ cat /sys/fs/cgroup/test/pids.current
1

pids.peak は、その cgroup に所属していたプロセス数が過去に到達した最大値を表します。この実験では、bash、sleep、および sleep を終了するために起動した pkill プロセスが同時に存在したため、最大値は 3 となっています。

[user1@server ~]$ cat /sys/fs/cgroup/test/pids.peak
3

再度、test cgroup に所属する bash プロセスからsleepプロセスを2つ起動します。

[root@server ~]# sleep 600&
[1] 2616
[root@server ~]# sleep 800&
[2] 2617

プロセスの親子関係を確認します。別のターミナルで確認することで、test cgroup に新たなプロセスを作成せずに状態を確認できます。2 つの sleep プロセスは、いずれも test cgroup に所属する bash プロセス(PID: 2563)から起動していることが確認できます。

[user1@server ~]$ ps -C sleep -o comm,pid,ppid
COMMAND             PID    PPID
sleep              2616    2563
sleep              2617    2563

ps コマンドの詳しい使い方は、以下のページをご覧ください。

hana-shin.hatenablog.com

この時点でのプロセス数(pids.current)は、上限の 3 になります。

[user1@server ~]$ cat /sys/fs/cgroup/test/pids.current
3

test cgroupに所属する最大のプロセス数は 3 のままであることがわかります。

[user1@server ~]$ cat /sys/fs/cgroup/test/pids.peak
3

新たに sleep を1つ起動します。しかし、test cgroup に移動した bash プロセスと、bash から起動した2個の sleep プロセスの合計3個のプロセスが、すでに test cgroup に存在しています。そのため、新たに sleep を起動することができず、以下のエラーメッセージが出力されています。

[root@server ~]# sleep 900&
-bash: fork: retry: リソースが一時的に利用できません
-bash: fork: retry: リソースが一時的に利用できません
-bash: fork: retry: リソースが一時的に利用できません
^C-bash: fork: システムコール割り込み

pids.events を確認します。max 7 は、pids.max に設定した上限に達したため、新しいプロセスの作成が拒否された回数を表しています。今回の環境では、sleep 900 & を起動しようとした際に、bash が内部的に複数回 fork() を再試行したため、1 回のコマンド実行であっても拒否回数が 7 回として記録されています。

[user1@server ~]$ cat /sys/fs/cgroup/test/pids.events
max 7

次に、test cgroup に所属する bash プロセスから ls コマンドを実行してみます。この時点で test cgroup には、bash プロセスと 2 個の sleep プロセスの合計 3 個のプロセスがすでに存在しています。pids.max に設定した上限は 3 のため、新たに ls プロセスを生成しようとすると上限を超えてしまいます。そのため、bash は fork() に失敗し、以下のエラーメッセージが出力されます。

[root@server ~]# ls
-bash: fork: retry: リソースが一時的に利用できません
-bash: fork: retry: リソースが一時的に利用できません
-bash: fork: retry: リソースが一時的に利用できません
^C-bash: fork: システムコール割り込み

pids.events は、pids.max に設定した上限に達したことにより、新しいプロセスの作成が拒否された回数を記録するファイルです。本実験では、sleep や ls の起動に失敗した回数が max に累積されていることが確認できます。

[user1@server ~]$ cat /sys/fs/cgroup/test/pids.events
max 10

3.2 後始末

root で実行していたターミナルは、test cgroup のプロセス数が上限に達しているため、新たなコマンドを実行できません。そのため、まずは確認用のターミナル(user1)から sudo pkill sleep コマンドを実行して sleep プロセスを終了させ、test cgroup にプロセス数の空きを作ります。

[user1@server ~]$ sudo pkill sleep

sleep プロセスが終了したため、test cgroup に所属しているプロセスは bash プロセス(PID: 2563)のみ(計1個)となり、root のターミナルでも再びコマンドが実行できるようになります。

[user1@server ~]$ cat /sys/fs/cgroup/test/cgroup.procs
2563

実験前に確認しておいた元の cgroup に bash プロセスを戻します。これにより、bash プロセスは test cgroup から抜け、もとのセッション cgroup に再所属します。

[root@server ~]# echo $$ > /sys/fs/cgroup/user.slice/user-0.slice/session-1.scope/cgroup.procs

bash プロセスが所属する cgroup を確認すると、元の user.slice/user-0.slice/session-1.scope に戻っていることが確認できます。

[root@server ~]# cat /proc/$$/cgroup
0::/user.slice/user-0.slice/session-1.scope

【NUMA】CPUとメモリの配置がパフォーマンスに与える影響を実験で確かめる

1 はじめに

NUMA(Non-Uniform Memory Access)は、CPUとメモリを複数のノードに分割して構成するアーキテクチャです。

従来のUMA(Uniform Memory Access)では、すべてのCPUからメモリへのアクセス時間はほぼ均一でした。しかし、CPUコア数やメモリ容量の増加に伴い、単一の共有メモリを複数のCPUで利用する方式では、メモリ帯域や拡張性に限界が生じるようになりました。

そこで、CPUの近くにメモリを配置し、それらを1つのノードとして扱うNUMAアーキテクチャが採用されるようになりました。NUMAでは、CPUが同じノード内のメモリへアクセスする場合(ローカルアクセス)は高速ですが、別のノードのメモリへアクセスする場合(リモートアクセス)はノード間の接続を経由するため、一般的にレイテンシが増加します。

近年のLinuxでは、NUMA Automatic BalancingによってCPUとメモリの配置が自動的に最適化されるため、通常の運用ではNUMAを意識する場面は多くありません。しかし、性能問題の調査やアプリケーションのチューニングを行う際には、ローカルアクセスとリモートアクセスの違いや、NUMAがシステム性能に与える影響を理解しておくことが重要です。

本記事では、numactl や stress-ng、numastat などのツールを用いて、CPU とメモリの割り当てを意図的に制御しながら簡単な実験を行い、ローカルアクセスとリモートアクセスで動作や性能がどのように変化するかを確認します。

2 検証環境

2.1 ソフトウェアのバージョン

AlmaLinuxバージョンは以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 10.2 (Lavender Lion)

カーネルバージョンは以下のとおりです。

[root@server ~]# uname -r
6.12.0-211.22.1.el10_2.x86_64

2.2 ノードのリソース

68GiBのメモリを搭載しています。

[root@server ~]# free -h
               total        used        free      shared  buff/cache   available
Mem:            68Gi        14Gi        53Gi        49Mi       1.8Gi        54Gi
Swap:          1.0Gi       1.1Mi       1.0Gi

2.3 検証環境におけるNUMA構成

lscpu の実行結果から、このサーバは 1 つの CPU ソケット を搭載していることが分かります。一方で、NUMA ノード数は 2 となっているため、このシステムは 1 ソケット内に 2 つの NUMA ノードを持つ構成(NUMA ドメインが 2 つ) になっています。また、論理 CPU は合計 32 個 存在し、そのうち NUMA ノード 0 には CPU 0-3、8-11、16-19、24-27 が、NUMA ノード 1 には CPU 4-7、12-15、20-23、28-31 が割り当てられています。NUMA システムでは、各 NUMA ノードがそれぞれローカルメモリを持っており、同じ NUMA ノード内の CPU がローカルメモリへアクセスする場合は高速です。一方、別の NUMA ノードに接続されたメモリへアクセスする場合は、ノード間の接続を経由するためアクセスレイテンシがわずかに増加します。

[root@server ~]# lscpu | egrep 'CPU\(s\)|Socket|NUMA'
CPU(s):                                  32
On-line CPU(s) list:                     0-31
Socket(s):                               1
CPU(s) scaling MHz:                      32%
NUMA node(s):                            2
NUMA node0 CPU(s):                       0-3,8-11,16-19,24-27
NUMA node1 CPU(s):                       4-7,12-15,20-23,28-31

lscpuコマンドの詳しい使い方は、以下のページをご覧ください。
hana-shin.hatenablog.com

また、numactl --hardware コマンドを実行すると、NUMA ノードごとの CPU やメモリの割り当て状況を確認できます。

[root@server ~]# numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 8 9 10 11 16 17 18 19 24 25 26 27
node 0 size: 38262 MB
node 0 free: 33387 MB
node 1 cpus: 4 5 6 7 12 13 14 15 20 21 22 23 28 29 30 31
node 1 size: 32248 MB
node 1 free: 30255 MB
node distances:
node     0    1
   0:   10   11
   1:   11   10
  • Node 0 には論理 CPU 0,1,2,3,8,9,10,11,16,17,18,19,24,25,26,27 が所属していること
  • Node 1 には論理 CPU 4,5,6,7,12,13,14,15,20,21,22,23,28,29,30,31 が所属していること
  • Node 0 のメモリサイズは約 37GB、Node 1 のメモリサイズは約 32GB であること
  • node distancesは、同じノード内へのアクセス(0→0、1→1)が 10、別ノードへのアクセス(0→1、1→0)が 11 と、リモートアクセスの方がわずかに値が大きくなっています。距離の絶対値そのものに意味があるわけではなく、「同一ノードのメモリアクセスより、別ノードのメモリアクセスの方がコストが高い」という相対関係を示したものです。


ここまでの情報をまとめると、構成は次のように図示できます。本環境は 1 ソケット構成で、32 個の論理 CPU とメモリが 2 つの NUMA ノードに分割されています。

+----------------------------------------------+
|                 Socket 0                     |
|                                              |
| +------------------+ +------------------+    |
| |   NUMA Node 0    | |   NUMA Node 1    |    |
| |                  | |                  |    |
| | CPUs             | | CPUs             |    |
| | 0-3              | | 4-7              |    |
| | 8-11             | | 12-15            |    |
| | 16-19            | | 20-23            |    |
| | 24-27            | | 28-31            |    |
| |                  | |                  |    |
| | Node 0 Memory    | | Node 1 Memory    |    |
| +------------------+ +------------------+    |
|                                              |
+----------------------------------------------+

3 numactlコマンドの使い方

3.1 CPUとメモリを同じNUMAノード(Node 0)に割り当てる

numactl で Node 0 の CPU を使うように指定(--cpunodebind=0)し、メモリも Node 0 から割り当てるように指定(--membind=0)した上で、stress-ng で10GB のメモリを確保してそのまま待機(3600秒)します。

[root@server ~]# numactl --cpunodebind=0 --membind=0  stress-ng -k --vm 1 --vm-bytes 10G --vm-hang 0 -q&
[1] 6832

stress-ng コマンドの詳しい使い方は、以下のページをご覧ください。

hana-shin.hatenablog.com

実行後、ps コマンドで stress-ng プロセスの状態を確認します。
PSR 列の値から、表示されているすべての stress-ng プロセスが Node 0 に属する論理 CPU 上で動作していることがわかります。この環境では PSR に表示されている 26、16、1 いずれの論理 CPU も Node 0 に属しているため、stress-ng のプロセスはすべて Node 0 上で実行されています。

[root@server ~]#  ps -C stress-ng -o comm,pid,ppid,psr,%mem,%cpu,wchan
COMMAND             PID    PPID PSR %MEM %CPU WCHAN
stress-ng          6832    5607  26  0.0  0.0 do_wait
stress-ng          6833    6832  16  0.0  0.0 do_wait
stress-ng          6834    6833   1 14.5  3.3 hrtimer_nanosleep

ps コマンドの詳しい使い方は、以下のページをご覧ください。

hana-shin.hatenablog.com


続いて numastat コマンドでメモリの割り当て状況を確認します。--membind=0 を指定したため、約 10GB のメモリのほぼすべてが Node 0 に割り当てられていることが確認できます。この例では、CPU とメモリの両方が Node 0 に配置されており、同一 NUMA ノード内で処理が完結しています。

[root@server ~]# numastat 6834

Per-node process memory usage (in MBs) for PID 6834 (stress-ng)
                           Node 0          Node 1           Total
                  --------------- --------------- ---------------
Huge                         0.00            0.00            0.00
Heap                         0.04            0.00            0.04
Stack                        0.03            0.00            0.03
Private                  10241.64            0.53        10242.17
----------------  --------------- --------------- ---------------
Total                    10241.71            0.53        10242.25

確認が終わったらstress-ngを終了します。

[root@server ~]# pkill stress-ng
[root@server ~]#
[1]+  終了                  numactl --cpunodebind=0 --membind=0 stress-ng -k --vm 1 --vm-bytes 10G --vm-hang 0 -q
[root@server ~]#

3.2 CPUとメモリを同じNUMAノード(Node 1)に割り当てる

今度は、numactl で Node 1 の CPU を使うように指定(--cpunodebind=1)し、メモリも Node 1 から割り当てるように指定(--membind=1)した上で、stress-ng で約 20GB のメモリ負荷を発生させます。

[root@server ~]# numactl --cpunodebind=1 --membind=1 stress-ng -k --vm 1 --vm-bytes 20G --vm-hang 0 -q&
[1] 6809

実行後、ps コマンドで stress-ng プロセスの状態を確認します。
PSR 列の値から、表示されているすべての stress-ng プロセスが Node 1 に属する論理 CPU 上で動作していることがわかります。この環境では PSR に表示されている 12、29、14 いずれの論理 CPU も Node 1 に属しているため、stress-ng のプロセスはすべて Node 1 上で実行されています。

[root@server ~]# ps -C stress-ng -o comm,pid,ppid,psr,%mem,%cpu,wchan
COMMAND             PID    PPID PSR %MEM %CPU WCHAN
stress-ng          6809    5607  12  0.0  0.0 do_wait
stress-ng          6810    6809  29  0.0  0.0 do_wait
stress-ng          6811    6810  14 29.0  2.9 hrtimer_nanosleep

続いて numastat コマンドでメモリの割り当て状況を確認します。--membind=1 を指定したため、約 20GB のメモリのほぼすべてが Node 1 に割り当てられていることがわかります。この例でも、CPU とメモリが同じ Node 1 に配置されているため、ローカルメモリアクセスで処理が行われます。

[root@server ~]# numastat 6811

Per-node process memory usage (in MBs) for PID 6811 (stress-ng)
                           Node 0          Node 1           Total
                  --------------- --------------- ---------------
Huge                         0.00            0.00            0.00
Heap                         0.00            0.04            0.04
Stack                        0.00            0.03            0.03
Private                      0.56        20481.61        20482.17
----------------  --------------- --------------- ---------------
Total                        0.56        20481.68        20482.25

3.3 CPUとメモリを異なるNUMAノードに割り当てる

numactl で CPU は Node 0(--cpunodebind=0)、メモリは Node 1 (--membind=1)を使用するように指定した上で、stress-ng で約 30GB のメモリ負荷を発生させます。

[root@server ~]# numactl --cpunodebind=0 --membind=1  stress-ng -k --vm 1 --vm-bytes 30G --vm-hang 0 -q&
[1] 6845

PSR 列の値から、表示されているすべての stress-ng プロセスが Node 0 に属する論理 CPU 上で動作していることがわかります。この環境では PSR に表示されている 0、1、2 いずれの論理 CPU も Node 0 に属しているため、stress-ng のプロセスはすべて Node 0 上で実行されています。

[root@server ~]#  ps -C stress-ng -o comm,pid,ppid,psr,%mem,%cpu,wchan
COMMAND             PID    PPID PSR %MEM %CPU WCHAN
stress-ng          6845    5607   0  0.0  0.0 do_wait
stress-ng          6846    6845   1  0.0  0.0 do_wait
stress-ng          6847    6846   2 43.5  3.2 hrtimer_nanosleep

続いて numastat コマンドでメモリの割り当て状況を確認します。--membind=1 を指定しているため、約 30GB のメモリはほぼすべて Node 1 に割り当てられています。つまり、このプロセス群は Node 0 の CPU で実行されながら、Node 1 に配置されたメモリへアクセスしている状態です。このように CPU とメモリが異なるノードにある場合、ノード間の通信が発生するため、パフォーマンスに影響が出る場合があります。

[root@server ~]# numastat 6847

Per-node process memory usage (in MBs) for PID 6847 (stress-ng)
                           Node 0          Node 1           Total
                  --------------- --------------- ---------------
Huge                         0.00            0.00            0.00
Heap                         0.00            0.04            0.04
Stack                        0.00            0.03            0.03
Private                      0.55        30721.60        30722.15
----------------  --------------- --------------- ---------------
Total                        0.55        30721.67        30722.23

4 ローカルメモリアクセスとリモートメモリアクセスの性能比較

ここでは、CPU とメモリを同じ NUMA ノードに配置した場合(ローカルアクセス)と、異なる NUMA ノードに配置した場合(リモートアクセス)で、メモリ書き込み処理の実行時間を比較します。CPU は Node 0 に固定し、メモリのみを Node 0 または Node 1 に割り当てて計測を行います。

4.1 1回目

1回目の計測結果です。ローカルアクセスは2.408秒、リモートアクセスは2.714秒でした。

(1) ローカルアクセスの場合
CPUとメモリの両方をNode 0に割り当てて計測します。
なお、stress-ng コマンドの --vm-method オプションに write64nt を使用します。これは、CPUキャッシュをバイパスする Non-Temporal Store 命令を用いて、メモリへ順次書き込みを行う方式です。キャッシュを介さず、DRAMへ直接データを書き込みます。詳細については、stress-ng の man ページを参照してください。

[root@server ~]# time numactl --cpunodebind=0 --membind=0 stress-ng --vm 1 --vm-bytes 30G --vm-method write64nt --vm-keep --vm-ops 10000
stress-ng: info:  [7584] defaulting to a 1 day run per stressor
stress-ng: info:  [7584] dispatching hogs: 1 vm
stress-ng: info:  [7585] vm: using 30G per stressor instance (total 30G of 64.03G available memory)
stress-ng: info:  [7584] skipped: 0
stress-ng: info:  [7584] passed: 1: vm (1)
stress-ng: info:  [7584] failed: 0
stress-ng: info:  [7584] metrics untrustworthy: 0
stress-ng: info:  [7584] successful run completed in 2.37 secs

real    0m2.408s
user    0m0.469s
sys     0m1.931s

(2) リモートアクセスの場合
CPUはNode 0、メモリはNode 1に割り当てて計測します。

[root@server ~]# time numactl --cpunodebind=0 --membind=1 stress-ng --vm 1 --vm-bytes 30G --vm-method write64nt --vm-keep --vm-ops 10000
stress-ng: info:  [7587] defaulting to a 1 day run per stressor
stress-ng: info:  [7587] dispatching hogs: 1 vm
stress-ng: info:  [7588] vm: using 30G per stressor instance (total 30G of 63.70G available memory)
stress-ng: info:  [7587] skipped: 0
stress-ng: info:  [7587] passed: 1: vm (1)
stress-ng: info:  [7587] failed: 0
stress-ng: info:  [7587] metrics untrustworthy: 0
stress-ng: info:  [7587] successful run completed in 2.68 secs

real    0m2.714s
user    0m0.534s
sys     0m2.173s

4.2 2回目

2回目の計測です。ローカルアクセスが1.745秒、リモートアクセスが1.888秒でした。

(1) ローカルアクセスの場合
CPUとメモリの両方をNode 0に割り当てて計測します。

[root@server ~]# time numactl --cpunodebind=0 --membind=0 stress-ng --vm 1 --vm-bytes 30G --vm-method write64nt --vm-keep --vm-ops 10000
stress-ng: info:  [7592] defaulting to a 1 day run per stressor
stress-ng: info:  [7592] dispatching hogs: 1 vm
stress-ng: info:  [7593] vm: using 30G per stressor instance (total 30G of 63.65G available memory)
stress-ng: info:  [7592] skipped: 0
stress-ng: info:  [7592] passed: 1: vm (1)
stress-ng: info:  [7592] failed: 0
stress-ng: info:  [7592] metrics untrustworthy: 0
stress-ng: info:  [7592] successful run completed in 1.71 sec

real    0m1.745s
user    0m1.193s
sys     0m0.548s

(2) リモートアクセスの場合
CPUはNode 0、メモリはNode 1に割り当てて計測します。

[root@server ~]# time numactl --cpunodebind=0 --membind=1 stress-ng --vm 1 --vm-bytes 30G --vm-method write64nt --vm-keep --
vm-ops 10000
stress-ng: info:  [7595] defaulting to a 1 day run per stressor
stress-ng: info:  [7595] dispatching hogs: 1 vm
stress-ng: info:  [7596] vm: using 30G per stressor instance (total 30G of 63.37G available memory)
stress-ng: info:  [7595] skipped: 0
stress-ng: info:  [7595] passed: 1: vm (1)
stress-ng: info:  [7595] failed: 0
stress-ng: info:  [7595] metrics untrustworthy: 0
stress-ng: info:  [7595] successful run completed in 1.85 sec

real    0m1.888s
user    0m1.225s
sys     0m0.659s

4.3 3回目

3回目の計測です。ローカルアクセスが1.744秒、リモートアクセスが2.019秒でした。

(1) ローカルアクセスの場合

[root@server ~]# time numactl --cpunodebind=0 --membind=0 stress-ng --vm 1 --vm-bytes 30G --vm-method write64nt --vm-keep --vm-ops 10000
stress-ng: info:  [7598] defaulting to a 1 day run per stressor
stress-ng: info:  [7598] dispatching hogs: 1 vm
stress-ng: info:  [7599] vm: using 30G per stressor instance (total 30G of 63.12G available memory)
stress-ng: info:  [7598] skipped: 0
stress-ng: info:  [7598] passed: 1: vm (1)
stress-ng: info:  [7598] failed: 0
stress-ng: info:  [7598] metrics untrustworthy: 0
stress-ng: info:  [7598] successful run completed in 1.71 sec

real    0m1.744s
user    0m1.080s
sys     0m0.660s

(2) リモートアクセスの場合

[root@server ~]# time numactl --cpunodebind=0 --membind=1 stress-ng --vm 1 --vm-bytes 30G --vm-method write64nt --vm-keep --vm-ops 10000
stress-ng: info:  [7601] defaulting to a 1 day run per stressor
stress-ng: info:  [7601] dispatching hogs: 1 vm
stress-ng: info:  [7602] vm: using 30G per stressor instance (total 30G of 62.81G available memory)
stress-ng: info:  [7601] skipped: 0
stress-ng: info:  [7601] passed: 1: vm (1)
stress-ng: info:  [7601] failed: 0
stress-ng: info:  [7601] metrics untrustworthy: 0
stress-ng: info:  [7601] successful run completed in 1.98 sec

real    0m2.019s
user    0m1.309s
sys     0m0.705s

4.4 結果まとめ

3回の平均値を計算すると、以下のようになります。

アクセス形態 1回目 2回目 3回目
ローカルアクセス(CPU=Node0, Memory=Node0) 2.408秒 1.745秒 1.744秒
リモートアクセス(CPU=Node0, Memory=Node1) 2.714秒 1.888秒 2.019秒

3回の平均で比較すると、リモートアクセスはローカルアクセスより約12%遅い結果となりました。CPUとメモリを同じNUMAノードに配置することで、メモリアクセスのレイテンシを抑えられることが実測で確認できました。

アクセス形態 平均
ローカルアクセス 1.966秒
リモートアクセス 2.207秒

なお、今回の検証環境は1ソケット構成ですが、2ソケット以上の環境ではソケット間インターコネクトを経由してメモリアクセスを行うため、ローカルアクセスとリモートアクセスの性能差がさらに大きくなる可能性があります。

5 numastat によるローカルアクセスとリモートアクセスの確認

これまでの検証では、numastat を使用してプロセス単位のメモリ配置を確認しました。本節では、numastat が表示するシステム全体の統計情報を利用して、ローカルメモリアクセスとリモートメモリアクセスの発生状況を確認します。これらの統計情報は累積値として記録されるため、実験の前後で numastat の結果を取得し、その差分を比較します。

numastatコマンドで表示される統計情報の意味です。

項目 意味
numa_hit 要求したノードからメモリ確保に成功した回数
numa_miss 要求したノードで確保できず別ノードから確保した回数
numa_foreign 他ノードが本ノードのメモリを利用した回数
interleave_hit インターリーブポリシーで割り当てられた回数
local_node CPUが所属するNUMAノードのメモリへアクセスした回数
other_node CPUが所属するNUMAノード以外のメモリへアクセスした回数

5.1 ローカルメモリアクセスの確認

実験前の NUMA 統計情報を保存します。

[root@server ~]# numastat > before 

続いて、CPU とメモリをともに Node 0 に割り当てて、ローカルメモリアクセスが発生するように stress-ng を実行します。

[root@server ~]# time numactl --cpunodebind=0 --membind=0 stress-ng --vm 1 --vm-bytes 30G --vm-method write64nt --vm-keep --vm-ops 100

実行後の NUMA 統計情報を取得します。

[root@server ~]# numastat > after

実験前後の差分を比較します。

[root@server ~]# sdiff before  after
                           node0           node1                                           node0           node1
numa_hit                22562613        10209485              | numa_hit                22605783        10209650
numa_miss                      0           98174                numa_miss                      0           98174
numa_foreign               98174               0                numa_foreign               98174               0
interleave_hit              2373            2607                interleave_hit              2373            2607
local_node              22551066         9883314              | local_node              22594236         9883479
other_node                 11547          424345                other_node                 11547          424345

差分を確認すると、主に node0 の numa_hit と local_node が増加していることが分かります。これは、Node 0 上で実行されたプロセスが Node 0 のメモリを利用して処理を行っていることを示しています。

  • numa_hit

node0: 22605783 − 22562613 = 43,170 増加
node1: 10209650 − 10209485 = 165 増加

  • local_node

node0: 22594236 − 22551066 = 43,170 増加
node1: 9883479 − 9883314 = 165 増加

  • numa_miss, numa_foreign, interleave_hit, other_node は変化なし

5.2 リモートメモリアクセスの確認

次に、CPUは Node 0、メモリは Node 1 に割り当て、あえてリモートメモリアクセスを発生させます。

実験前の NUMA 統計情報を保存します。

[root@server ~]# numastat > before 

今度は --membind=1 を指定し、メモリを強制的に Node 1 へ割り当てて実行します。

[root@server ~]# time numactl --cpunodebind=0 --membind=1 stress-ng --vm 1 --vm-bytes 30G --vm-method write64nt --vm-keep --vm-ops 100

実行後の NUMA 統計情報を取得します。

[root@server ~]# numastat > after

実験前後の NUMA 統計情報を比較します。
差分を確認すると、Node 1 側の numa_hit が大きく増加していることから、メモリが Node 1 に割り当てられたことが分かります。また、other_node の値も大きく増加しており、Node 0 上の CPU が Node 1 のメモリへアクセスしていることが確認できます。

[root@server ~]# sdiff before  after
                           node0           node1                                           node0           node1
numa_hit                22605861        10210529              | numa_hit                22605862        10222677
numa_miss                      0           98174                numa_miss                      0           98174
numa_foreign               98174               0                numa_foreign               98174               0
interleave_hit              2373            2607                interleave_hit              2373            2607
local_node              22594314         9884358              | local_node              22594315         9884589
other_node                 11547          424345              | other_node                 11547          436262
  • numa_hit

node0: 1 増加
node1: 10222677-10210529 = 12,148 増加

  • local_node

node0: 1 増加
node1: 231 増加

  • other_node

node1: 436262-424345 = 11,917 増加

other_node は CPU が所属する NUMA ノード以外のメモリへアクセスした回数を示します。
今回の検証では CPU を Node 0 に固定し、メモリを Node 1 に割り当てています。そのため、Node 0 上の CPU が Node 1 のメモリへアクセスするリモートメモリアクセスが多数発生し、その結果として other_node が大きく増加したことが分かります。

5.3 結果のまとめ

ここまでの実験で、以下のことが検証できました。

  • ローカルアクセス時: local_node が増加する
  • リモートアクセス時: other_node が増加する

numastat のシステム統計をチェックすれば、サーバー全体でどちらのアクセスが多く発生しているかが一目で判断できます。
前節の性能測定では、リモートアクセスが発生すると実行時間が約12%も長くなる ことが分かりました。NUMA環境のパフォーマンスを最大限引き出すためには、CPUとメモリを同じノードにバランスよく配置し、リモートアクセス(other_node)をいかに減らすかが重要になります。

6 NUMA 配置の自動最適化

ここまでの章では、numactl を使用して CPU とメモリの割り当てを手動で制御し、ローカルアクセスとリモートアクセスの振る舞いを確認しました。しかし、実運用環境において、プロセスごとに CPU やメモリの配置を手動で調整し続けることは現実的ではありません。そのため Linux には、NUMA 配置を自動的に最適化するための仕組みがいくつか用意されています。代表的なものが以下の 2 つです。

仕組み 概要
numad ユーザー空間で動作するデーモン。システムの CPU 使用率やメモリ使用状況を監視し、CPU アフィニティやメモリ配置を調整することで、ローカルメモリアクセスの割合を高めることを目的とした仕組み
NUMA Automatic Balancing(Auto NUMA Balancing) Linux カーネルがメモリアクセスパターンを監視し、必要に応じてメモリページやタスクの配置を最適化する機能。ユーザー空間のデーモンに依存せず、カーネルレベルで動作します。Red Hat Enterprise Linux 7 以降ではデフォルトで有効となっています

docs.redhat.com

本ブログの検証環境では、numad は無効になっています。

[root@server ~]# systemctl is-enabled numad.service
disabled

一方、NUMA Automatic Balancing は有効になっていますので、本検証環境では NUMA 配置の最適化が自動的に行われる設定になっていることがわかります。

[root@server ~]# cat /proc/sys/kernel/numa_balancing
1

Z 参考記事

kernel-internals.org

cgroup v2 の使い方~インタフェースファイルの役割とプロセス・スレッド制御の実験

1 cgroupとは?

cgroup(Control Groups)は、複数のプロセスをひとつのグループとして管理し、グループ単位で CPU、メモリ、I/O などのシステムリソースの使用量を制御・監視する仕組みです。cgroup v2 は AlmaLinux 8 で導入され、AlmaLinux 9 以降ではデフォルトの cgroup として利用されています。

本記事で使用している環境では、cgroup v2 が使用されています。以下のコマンドを実行することで、使用されている cgroup のバージョンが v2 であることを確認できます。

[root@server ~]# mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot)

参考記事
gihyo.jp

2 検証環境

2.1 ソフトウェアのバージョン

AlmaLinuxバージョンは以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 10.2 (Lavender Lion)

カーネルバージョンは以下のとおりです。

[root@server ~]# uname -r
6.12.0-211.7.3.el10_2.x86_64

2.2 ノードのリソース

4GBのメモリを割り当てています。

[root@server ~]# free -h
               total        used        free      shared  buff/cache   available
Mem:           3.5Gi       522Mi       3.0Gi       9.1Mi       195Mi       3.0Gi
Swap:          1.0Gi          0B       1.0Gi

4コアのCPU(4 vCPU) を搭載しています。

[root@server ~]# lscpu -xe
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
  0    0      0    0 0:0:0:0          yes
  1    0      1    1 2:2:2:2          yes
  2    0      2    2 4:4:4:4          yes
  3    0      3    3 6:6:6:6          yes

lscpuコマンドの詳しい使い方は、以下のページをご覧ください。
hana-shin.hatenablog.com

3 インタフェースファイル

/sys/fs/cgroup/ 配下に任意の名前のディレクトリを作成することで、新しいcgroup を作成することができます。以下は、test という名前の cgroup を作成するときの例です。

[root@server ~]# mkdir /sys/fs/cgroup/test

作成したディレクトリ配下に自動生成されるファイルは、以下の 2 種類に分類されています。これらのファイルを介して cgroup の設定や状態確認を行います。

種類 ファイル名 役割
コア・インタフェースファイル cgroup. というプレフィックスから始まる プロセスの所属管理(cgroup.procs)や、子 cgroup へ割り当てるコントローラの制御(cgroup.subtree_control)など、cgroup そのものの共通管理を行う
コントローラ・インタフェースファイル cpu. や memory. など、制御対象のリソース名から始まる CPU の使用率制限やメモリの上限設定など、特定のリソースを制限・制御する


(1) コア・インタフェースファイル
コア・インタフェースファイルは、以下のように cgroup. で始まるファイルです。

[root@control ~]# ls /sys/fs/cgroup/test/cgroup.*
/sys/fs/cgroup/test/cgroup.controllers      /sys/fs/cgroup/test/cgroup.procs
/sys/fs/cgroup/test/cgroup.events           /sys/fs/cgroup/test/cgroup.stat
/sys/fs/cgroup/test/cgroup.freeze           /sys/fs/cgroup/test/cgroup.subtree_control
/sys/fs/cgroup/test/cgroup.kill             /sys/fs/cgroup/test/cgroup.threads
/sys/fs/cgroup/test/cgroup.max.depth        /sys/fs/cgroup/test/cgroup.type
/sys/fs/cgroup/test/cgroup.max.descendants
種類 概要
cgroup.controllers この cgroup で利用可能なコントローラ(cpu、memoryなど)の一覧を表示します
cgroup.procs この cgroup に所属するプロセスの PID を表示します。PID を書き込むと、そのプロセスを元の cgroup から、この cgroup に移動します
cgroup.threads この cgroup に所属するスレッドの TID を表示します
cgroup.max.depth 作成できる子 cgroup の最大階層数を表示または設定します
cgroup.max.descendants 作成できる子孫 cgroup の最大数を表示または設定します
cgroup.subtree_control 子 cgroup に対して有効化するコントローラを設定します
cgroup.stat cgroup の状態情報(子 cgroup 数など)を表示します

(2) コントローラ・インタフェースファイル

各コントローラには専用のインタフェースファイルが用意されており、コントローラ名で始まるファイルとして配置されています。たとえば、cpuset コントローラには以下のようなインタフェースファイルがあります。

[root@control ~]# ls /sys/fs/cgroup/test/cpuset.*
/sys/fs/cgroup/test/cpuset.cpus
/sys/fs/cgroup/test/cpuset.cpus.effective
/sys/fs/cgroup/test/cpuset.cpus.exclusive
/sys/fs/cgroup/test/cpuset.cpus.exclusive.effective
/sys/fs/cgroup/test/cpuset.cpus.partition
/sys/fs/cgroup/test/cpuset.mems
/sys/fs/cgroup/test/cpuset.mems.effective

4 親プロセス移動時の子プロセスの cgroup 所属

本章では、cgroup v2 におけるプロセスの移動と cgroup 継承の動作を検証します。具体的には、以下の2点を確認することを本章の目的とします。

(1) 既存の子プロセスへの影響:
bash から sleep プロセスを起動した状態で、親プロセスである bash を別の cgroup へ移動した際、すでに起動している子プロセス(sleep)の所属 cgroup が変化するかどうか?

(2) 新規子プロセスへの継承:
bash を別の cgroup へ移動した後、その bash から新たにプロセスを起動した際、子プロセスが親プロセス(bash)の所属する cgroup を継承するかどうか?

4.1 動作確認

/sys/fs/cgroup/ 配下に test という名前の cgroup を作成します。

[root@control ~]# mkdir /sys/fs/cgroup/test

現在のシェル自身のPIDを確認します。$$ は現在のシェル自身の PID(プロセスID)を格納している特殊変数です。以降の作業でこのプロセスを移動させるため、あらかじめ PID を確認しておきます。

[root@control ~]# echo $$
46324

現在の bash プロセスがどの cgroup に所属しているかを確認します。通常、ログイン直後はシステム(systemd)がログインセッションに対して自動的に割り当てた cgroup に所属しています。

[root@control ~]# cat /proc/$$/cgroup
0::/user.slice/user-0.slice/session-3.scope

sleep 600 を現在の bash の子プロセスとしてバックグラウンドで起動します。ここでは PID 62585 が割り当てられました。

[root@control ~]# sleep 600 &
[1] 62585

sleep 600 の親プロセスが bash (sleepの親プロセスのPID が46324)であることを確認します。

[root@control ~]# ps -C sleep -o comm,pid,ppid,args
COMMAND             PID    PPID COMMAND
sleep             62585   46324 sleep 600

bash の子プロセスとして起動した sleep 600 も、この時点では親プロセス(bash)と同じ初期 cgroup に自動的に所属していることを確認します。

[root@control ~]# cat /proc/62585/cgroup
0::/user.slice/user-0.slice/session-3.scope

bash 自身の PID を、先ほど作成した test ディレクトリ内の cgroup.procs に書き込みます。cgroup v2 では、この操作によって指定したプロセス単体のみが移動します。

[root@control ~]# echo $$ > /sys/fs/cgroup/test/cgroup.procs

bash が test cgroup へ移動されたことを確認します。

[root@control ~]# cat /proc/$$/cgroup
0::/test

親プロセス(bash)を test へ移動させても、移動前から存在していた子プロセス sleep 600 の所属 cgroup は変化していないことを確認します。cgroup v2 では、親を移動しても既存の子プロセスは追従しないことが分かります。

[root@control ~]# cat /proc/62585/cgroup
0::/user.slice/user-0.slice/session-3.scope

test cgroup の cgroup.procs を確認します。移動した bash(46324)のほかに、一時的に実行された cat コマンド自身(PID: 62961)など、その瞬間 test 配下で動いているプロセスのみが表示されます。

[root@control ~]# cat /sys/fs/cgroup/test/cgroup.procs
46324
62961

test cgroup に移動した bash から、新たに sleep 800 をバックグラウンドで起動します。

[root@control ~]# sleep 800&
[2] 63103

ps コマンドで確認すると、移動前に起動した sleep 600 と、移動後に起動した sleep 800 の双方が、同じ bash(PID: 46324)を親に持っていることが分かります。

[root@control ~]# ps -C sleep -o comm,pid,ppid,args
COMMAND             PID    PPID COMMAND
sleep             62585   46324 sleep 600
sleep             63103   46324 sleep 800

test cgroup のプロセス一覧を確認します。bash に加え、新たに起動した sleep 800 がこの cgroup に含まれていることが確認できます(63509 は cat コマンドの PID です)。

[root@control ~]# cat /sys/fs/cgroup/test/cgroup.procs
46324
63103
63509

bash 自身が現在も test cgroup に所属し続けていることを確認します。

[root@control ~]# cat /proc/$$/cgroup
0::/test

移動後の bash から起動した sleep 800 の cgroup を確認します。親プロセスの現在の cgroup をしっかりと継承し、test に所属していることが確認できました。移動前に起動した sleep 600 の結果と対比することで、cgroup 伝播のタイミングが明確になります。

[root@control ~]# cat /proc/63103/cgroup
0::/test

4.2 後始末

実験用に変更した環境を元の状態に戻します。(cgroup は内部にプロセスが残っていると削除できないため、プロセスを終了させるか元の cgroup に戻す必要があります)

[root@control ~]# jobs
[1]-  終了                  sleep 600
[2]+  終了                  sleep 800

実験用に起動した 2 つの sleep プロセス(ジョブ番号 %1 と %2)に終了シグナルを送り、プロセスを破棄します。

[root@control ~]# kill %1 %2

作成した test cgroup を削除できるようにするため、bash プロセス(PID: 46324)を実験開始前の初期 cgroup(session-3.scope)へ移動します。

[root@control ~]# echo 46324 > /sys/fs/cgroup/user.slice/user-0.slice/session-3.scope/cgroup.procs

bash の所属が最初のログインセッションの cgroup に正しく戻っていることを確認します。

[root@control ~]# cat /proc/$$/cgroup
0::/user.slice/user-0.slice/session-3.scope
[root@control ~]# rmdir /sys/fs/cgroup/test

4.3 実験の結果まとめ

  • cgroup の移動は指定したプロセス単体にのみ適用される:親プロセスを別の cgroup へ移動させても、すでに起動している子プロセスの cgroup は変わりません
  • 新規プロセスは生成時の親プロセスの cgroup を継承する:親プロセスの移動後に、新しく起動した子プロセスは、その時点の親と同じ cgroup を自動的に引き継ぎます。

5 コア・インタフェースファイルの使い方

5.1 cgroup.controllers

cgroup.controllers は、その cgroup において、利用可能なすべてのリソースコントローラ(CPUやメモリなどを制御する機能)の一覧を確認するための、読み取り専用のファイルです。

test という名前の cgroup ディレクトリを作成します。

[root@control ~]# mkdir /sys/fs/cgroup/test

作成した test ディレクトリ直下にある cgroup.controllers の中身を cat コマンドで表示してみます。出力結果を見ると、cpuset や cpu など、この cgroup で利用可能なリソースコントローラを確認することができます。

[root@control ~]# cat /sys/fs/cgroup/test/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc dmem

5.2 cgroup.kill

cgroup 内のすべてのプロセスを一括で強制終了(kill)するためのファイルです。「1」を書き込むことで、その cgroup に所属するすべてのプロセスに SIGKILL シグナルが送信されます。

4章の実験では、親プロセスを別の cgroup へ移動しても、すでに起動している子プロセスの cgroup は変化しない一方、新たに起動した子プロセスは親プロセスの cgroup を継承することを確認しました。そのため、本実験では最初にシェル(bash)を test cgroup へ移動してから sleep プロセスを起動します。これにより、起動した sleep プロセスも自動的に test cgroup に所属するため、後続の cgroup.kill の動作を同一 cgroup 内の複数プロセスに対して確認できます。

現在のシェルの PID を確認します。

[root@server ~]# echo $$
2657

現在のシェルを test cgroup に移動します。

[root@control ~]# echo $$ > /sys/fs/cgroup/test/cgroup.procs

シェルが test cgroup に所属していることがを確認します。

[root@control ~]# cat /proc/$$/cgroup
0::/test

続いて、シェルから 2 つの sleep プロセスを起動します。

[root@server ~]# sleep 600&
[1] 2734
[root@server ~]# sleep 800&
[2] 2735

起動した sleep プロセスを確認すると、いずれも現在のシェルを親プロセスとして実行されていることが分かります。

[root@server ~]# ps -C sleep -o comm,pid,ppid,args
COMMAND             PID    PPID COMMAND
sleep              2734    2657 sleep 600
sleep              2735    2657 sleep 800

PID 2734 の sleep プロセスの /proc/[PID]/cgroup を確認すると、test cgroup に所属していることが分かります。

[root@server ~]# cat /proc/2734/cgroup
0::/test

同様に、PID 2735 の sleep プロセスについても、test cgroup に所属していることが確認できます。

[root@server ~]# cat /proc/2735/cgroup
0::/test

別のターミナルから test cgroup に所属するプロセスを確認します。ここには、test cgroup に所属するシェルおよび sleep プロセスの PID が表示されています。

[root@server ~]# cat /sys/fs/cgroup/test/cgroup.procs
2657
2734
2735
2746

cgroup.kill に 1 を書き込むと、test cgroup に所属するすべてのプロセスへ SIGKILL が送信されます。

[root@control ~]# echo 1 > /sys/fs/cgroup/test/cgroup.kill
Connection to 192.168.122.2 closed.

別のターミナルから test cgroup の状態を確認します。

[root@control ~]# cat /sys/fs/cgroup/test/cgroup.procs
[root@control ~]#

5.3 cgroup.max.depth

cgroup の配下に、最大で何階層まで子 cgroup(子ディレクトリ)を作成できるかを制限するファイルです。デフォルトは「max(無制限)」です。値を 1 に指定すると1階層下まで、2 に指定すると2階層下の cgroup ディレクトリまで作成できるようになります。ここでは、値を 2 に指定して、階層数の制限がどのように行われるかを確認してみます。

まずは、今回の実験の起点となる test という名前の cgroup ディレクトリを作成します。

[root@control ~]# mkdir /sys/fs/cgroup/test

test ディレクトリ直下の cgroup.max.depth ファイルに 2 を書き込みます。これにより、test から数えて2階層下までしか新しい cgroup(ディレクトリ)を作れないように制限が課されます。

[root@control ~]# echo 2 > /sys/fs/cgroup/test/cgroup.max.depth

1階層目(test/test1)の作成:制限内(2階層まで)なので成功します

[root@control ~]# mkdir /sys/fs/cgroup/test/test1
[root@control ~]#

2階層目(test/test1/test2)の作成:制限内(2階層まで)なので成功します

[root@control ~]# mkdir /sys/fs/cgroup/test/test1/test2
[root@control ~]#

3階層目(test/test1/test2/test3)の作成:設定した「2階層」を超えるため、エラーになります

[root@control ~]# mkdir /sys/fs/cgroup/test/test1/test2/test3
mkdir: ディレクトリ `/sys/fs/cgroup/test/test1/test2/test3' を作成できません: リソース が一時的に利用できません

5.4 cgroup.subtree_control

cgroup.subtree_control は、「子 cgroup(下位の階層)に対して、どのリソースコントローラ(CPUやメモリ制限など)の利用を許可するか」を制御するためのファイルです。
親ディレクトリでの有効化・無効化によって、子ディレクトリ内のファイルがどのように変化するかを確認してみます。

親 cgroup の test ディレクトリを作成します。

[root@control ~]# mkdir /sys/fs/cgroup/test

親の下に、リソース制限の対象となる子 cgroup の test1 ディレクトリを作成します。

[root@control ~]# mkdir /sys/fs/cgroup/test/test1

初期状態で、親(test)が子(test1)に対して何かコントローラを有効化しているかを確認します。まだ何も書き込んでいないため、出力は空(設定なし)です。

[root@control ~]# cat /sys/fs/cgroup/test/cgroup.subtree_control
[root@control ~]#

親が何も有効化していない状態で、子(test1)の中にどのようなファイルがあるかを確認します。結果を見ると、cgroup. で始まるコア・インタフェースファイルと、自動的に生成される cpu.stat のみであり、リソース制限用のファイル(cpu.max など)は存在しません。

[root@control ~]# ls /sys/fs/cgroup/test/test1/
cgroup.controllers  cgroup.freeze  cgroup.max.depth        cgroup.procs  cgroup.subtree_control  cgroup.type  cpu.stat.local
cgroup.events       cgroup.kill    cgroup.max.descendants  cgroup.stat   cgroup.threads          cpu.stat

親(test)の cgroup.subtree_control に +cpu を書き込みます。これにより、子(test1)に対して CPU の制限機能の利用を許可します。

[root@control ~]# echo "+cpu" > /sys/fs/cgroup/test/cgroup.subtree_control

もう一度、子(test1)のディレクトリ内を確認します。先ほどは存在しなかった、CPU を実際に制限するための cpu.max や cpu.weight といった「コントローラ・インタフェースファイル」が自動的に生成されていることが分かります。

[root@control ~]# ls /sys/fs/cgroup/test/test1/
cgroup.controllers  cgroup.kill             cgroup.procs            cgroup.threads  cpu.max        cpu.stat.local  cpu.weight
cgroup.events       cgroup.max.depth        cgroup.stat             cgroup.type     cpu.max.burst  cpu.uclamp.max  cpu.weight.nice
cgroup.freeze       cgroup.max.descendants  cgroup.subtree_control  cpu.idle        cpu.stat       cpu.uclamp.min

今度は逆に、親(test)の cgroup.subtree_control に -cpu を書き込み、子(test1)への CPU 制限機能の許可を取り消します。

[root@control ~]# echo "-cpu" > /sys/fs/cgroup/test/cgroup.subtree_control
[root@control ~] #

最後に、再度子(test1)のディレクトリ内を確認します。親側で機能をオフにしたため、先ほどまで出現していた cpu.max などの制限用ファイルが削除され、初期状態(コアファイルと統計ファイルのみ)に戻っていることが確認できます。

[root@control ~]# ls /sys/fs/cgroup/test/test1/
cgroup.controllers  cgroup.freeze  cgroup.max.depth        cgroup.procs  cgroup.subtree_control  cgroup.type  cpu.stat.local
cgroup.events       cgroup.kill    cgroup.max.descendants  cgroup.stat   cgroup.threads          cpu.stat

5.5 cgroup.type

本節では、cgroup.type が domain および threaded の cgroup における動作の違いを確認します。domain 型ではプロセス単位で、threaded 型ではスレッド単位で cgroup を管理できます。本実験では CPU コントローラを利用し、それぞれの挙動を確認します。

5.5.1 事前準備

検証で使用するプログラムを作成します。学習用のサンプルプログラムのため、エラー処理は省略しています。

[root@control ~]# cat tp.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>

void* cpu_burner(void* arg) {
    int thread_num = *(int*)arg;

    pid_t tid = syscall(SYS_gettid);

    printf("[Thread %d] 起動完了 - スレッドID(TID): %d (CPUに負荷をかけています...)\n", thread_num, tid);
    free(arg);

    while (1) {
        // ここで無限ループさせることで、CPUコアを使い切ります
    }

    return NULL;
}

int main(int argc, char* argv[]) {

    int num_threads = atoi(argv[1]);

    printf("========================================\n");
    printf("メインプロセス(親PID): %d\n", getpid());
    printf("%d 個のスレッドを生成します...\n", num_threads);
    printf("========================================\n");

    pthread_t* threads = malloc(sizeof(pthread_t) * num_threads);

    for (int i = 0; i < num_threads; i++) {
        int* thread_arg = malloc(sizeof(int));
        *thread_arg = i + 1;

        pthread_create(&threads[i], NULL, cpu_burner, thread_arg);
    }

    printf("すべてのスレッドが起動しました。Ctrl+C で終了します。\n\n");

    for (int i = 0; i < num_threads; i++) {
        pthread_join(threads[i], NULL);
    }

    free(threads);
    return 0;
}

コンパイルします。

[root@control ~]# gcc -Wall -pthread -o tp tp.c

tpの動作確認をします。tpの引数は起動するスレッド数を指定します。

[root@control ~]# ./tp 2 &
[1] 194279
[root@control ~]# ========================================
メインプロセス(親PID): 194279
2 個のスレッドを生成します...
========================================
すべてのスレッドが起動しました。Ctrl+C で終了します。

[Thread 2] 起動完了 - スレッドID(TID): 194281 (CPUに負荷をかけています...)
[Thread 1] 起動完了 - スレッドID(TID): 194280 (CPUに負荷をかけています...)

ps コマンドを実行すると、メインスレッド(TID: 194279)は futex_do_wait で待機していることが分かります。これは、メインスレッドが子スレッドの終了を待っている状態であることを示しています。一方、子スレッド(TID: 194280、194281)は CPU を集中的に使用しており、それぞれ約 90% の CPU 使用率となっていることが確認できます。

[root@control ~]# ps -C tp -L -o comm,pid,tid,psr,%cpu,wchan
COMMAND             PID     TID PSR %CPU WCHAN
tp               194279  194279   0  0.0 futex_do_wait
tp               194279  194280   2 88.7 -
tp               194279  194281   0 87.3 -

TPを終了します。

[root@control ~]# pkill tp
[root@control ~]#
[1]+  Terminated              ./tp 2
5.5.2 プロセス制御(domainモードでの検証)

実験用cgroupを作成します。

[root@control ~]# mkdir -p /sys/fs/cgroup/test/domain

作成直後のcgroupのタイプは、デフォルトである通常モード(domain)であることを確認します。

[root@control ~]# cat /sys/fs/cgroup/test/domain/cgroup.type
domain

検証用のプログラム tp を4スレッド指定でバックグラウンド起動します(親PID: 112376)。

[root@control ~]# ./tp 4&
[1] 112375
[root@control ~]# ========================================
メインプロセス(親PID): 112376
4 個のスレッドを生成します...
========================================
[Thread 1] 起動完了 - スレッドID(TID): 112377 (CPUに負荷をかけています...)
[Thread 2] 起動完了 - スレッドID(TID): 112378 (CPUに負荷をかけています...)
[Thread 3] 起動完了 - スレッドID(TID): 112379 (CPUに負荷をかけています...)
[Thread 4] 起動完了 - スレッドID(TID): 112380 (CPUに負荷をかけています...)
すべてのスレッドが起動しました。Ctrl+C で終了します。

まだcgroupによる制限がかかっていないため、各高負荷スレッド(TID: 112377〜112380)がそれぞれ約80%のCPUを消費しています。

[root@control ~]# ps -C tp -L -o comm,pid,tid,ppid,%cpu
COMMAND             PID     TID    PPID %CPU
tp               112376  112376       1  0.0
tp               112376  112377       1 78.9
tp               112376  112378       1 79.0
tp               112376  112379       1 78.8
tp               112376  112380       1 80.3

プロセスのPID(112376)を cgroup.procs に書き込み、プロセス全体を domain グループへ移動します。

[root@control ~]# echo  112376 > /sys/fs/cgroup/test/domain/cgroup.procs

domain グループに所属しているPIDを確認します。

[root@control ~]# cat /sys/fs/cgroup/test/domain/cgroup.procs
112376

グループに移動しただけでは、まだCPUの上限制限を設定していないため、各スレッドの使用率に変化はありません。

[root@control ~]# ps -C tp -L -o comm,pid,tid,ppid,%cpu
COMMAND             PID     TID    PPID %CPU
tp               112376  112376       1  0.0
tp               112376  112377       1 78.2
tp               112376  112378       1 78.3
tp               112376  112379       1 78.4
tp               112376  112380       1 78.4

親階層で cpu コントローラを有効化します。

[root@control ~]# echo "+cpu" > /sys/fs/cgroup/test/cgroup.subtree_contro

domain グループ全体のCPU上限を 50%(0.5コア分) に制限します。

[root@control ~]# echo "50000 100000" > /sys/fs/cgroup/test/domain/cpu.max

CPU 制限設定後、4本の高負荷スレッドのCPU使用率がそれぞれ約14~15%程度まで低下していることが確認できます。この結果から、cpu.max に設定した上限はプロセス単位ではなく cgroup 全体に対して適用され、その枠内で各スレッドがCPU時間を共有していることが分かります。つまり、プロセスを domain cgroup に所属させた場合、同じプロセスに属する全スレッドが cgroup のCPU制限の対象となります。

[root@control ~]# ps -C tp -L -o comm,pid,tid,ppid,psr,%cpu
COMMAND             PID     TID    PPID PSR %CPU
tp               112376  112376       1   2  0.0
tp               112376  112377       1   3 14.5
tp               112376  112378       1   1 14.5
tp               112376  112379       1   0 14.5
tp               112376  112380       1   2 14.5

(2) あと始末
pkill tp により、tp プロセスおよびそのスレッドを終了します。

[root@control ~]# pkill tp

ps コマンドで確認すると、tp に該当するプロセスおよびスレッドが存在しないことが分かります。

[root@control ~]# ps -C tp -L -o comm,pid,tid,ppid,psr,%cpu
COMMAND             PID     TID    PPID PSR %CPU
[root@control ~]#

続いて、domain cgroup に所属するプロセスがなくなったことを確認します。

[root@control ~]# cat /sys/fs/cgroup/test/domain/cgroup.procs
[root@control ~]#

最後に、実験で作成した cgroup を削除します。cgroup v2 では、プロセスが所属している cgroup や子 cgroup を持つ cgroup は削除できません。今回のように、プロセスが存在せず、子 cgroup も存在しない状態であるため、rmdir による削除が正常に完了します。

[root@control ~]# rmdir /sys/fs/cgroup/test/domain
[root@control ~]# rmdir /sys/fs/cgroup/test
[root@control ~]#
5.5.3 スレッド制御

スレッドモード(threaded)を利用し、同じプロセスに属するスレッドを別々の子cgroupに分散させ、スレッド単位で異なるCPU制限を適用する検証を行います。

[root@control ~]# mkdir -p /sys/fs/cgroup/test/thread1
[root@control ~]# mkdir -p /sys/fs/cgroup/test/thread2

子グループ(thread1, thread2)のタイプを threaded に変更します。

[root@control ~]# echo threaded > /sys/fs/cgroup/test/thread1/cgroup.type
[root@control ~]# echo threaded > /sys/fs/cgroup/test/thread2/cgroup.type

これに伴い、親グループ(test)のタイプが、配下にスレッドモードを持つことを示す domain threaded へと自動的に変形します。

[root@control ~]# cat /sys/fs/cgroup/test/cgroup.type
domain threaded
[root@control ~]# cat /sys/fs/cgroup/test/thread1/cgroup.type
threaded
[root@control ~]# cat /sys/fs/cgroup/test/thread2/cgroup.type
threaded

親階層の cgroup.subtree_control で cpu コントローラを有効化し、子グループへリソースを分配できるようにします。

[root@control ~]# echo "+cpu" > /sys/fs/cgroup/test/cgroup.subtree_control

検証用TPを起動します。

[root@control ~]# ./tp 4&
[1] 201199
========================================
メインプロセス(親PID): 201199
4 個のスレッドを生成します...
========================================
[Thread 1] 起動完了 - スレッドID(TID): 201200 (CPUに負荷をかけています...)
[Thread 3] 起動完了 - スレッドID(TID): 201202 (CPUに負荷をかけています...)
[Thread 2] 起動完了 - スレッドID(TID): 201201 (CPUに負荷をかけています...)
すべてのスレッドが起動しました。Ctrl+C で終了します。

[root@control ~]# [Thread 4] 起動完了 - スレッドID(TID): 201203 (CPUに負荷をかけています...)

[root@control ~]#

4つの高負荷スレッドを持つプロセス(PID: 201199)を起動します。この段階では制限がないため、各スレッド(TID: 201200〜201203)がそれぞれ約78%のCPUを消費しています。

[root@control ~]# ps -C tp -L -o comm,pid,tid,psr,%cpu,wchan
COMMAND             PID     TID PSR %CPU WCHAN
tp               201199  201199   0  0.0 futex_do_wait
tp               201199  201200   3 77.2 -
tp               201199  201201   2 77.9 -
tp               201199  201202   0 78.6 -
tp               201199  201203   2 78.9 -

まず、プロセスの基点となるメインプロセス(PID: 201199)を親グループ(test)に所属させます。

[root@control ~]# echo 201199 > /sys/fs/cgroup/test/cgroup.procs
[root@control ~]# cat /proc/201199/cgroup
0::/test

cgroup.procs はプロセス単位の移動に使用します。一方、threaded cgroup では cgroup.threads を使用することで、同一プロセス内の個々のスレッドを別々の cgroup に配置できます。

[root@control ~]# echo 201200 > /sys/fs/cgroup/test/thread1/cgroup.threads
[root@control ~]# echo 201201 > /sys/fs/cgroup/test/thread1/cgroup.threads
[root@control ~]# echo 201202 > /sys/fs/cgroup/test/thread2/cgroup.threads
[root@control ~]# echo 201203 > /sys/fs/cgroup/test/thread2/cgroup.threads

各タスクの /proc/[TID]/cgroup を確認すると、同じプロセスに属していながら、各スレッドが異なるcgroup階層に割り振られていることが確認できます。

[root@control ~]# cat /proc/201199/cgroup
0::/test
[root@control ~]# cat /proc/201200/cgroup
0::/test/thread1
[root@control ~]# cat /proc/201201/cgroup
0::/test/thread1
[root@control ~]# cat /proc/201202/cgroup
0::/test/thread2
[root@control ~]# cat /proc/201203/cgroup
0::/test/thread2

スレッドを分散させた直後は、まだCPUの上限制限を設定していないため、各スレッドの使用率は高いまま維持されています。

[root@control ~]# ps -C tp -L -o comm,pid,tid,psr,%cpu,wchan
COMMAND             PID     TID PSR %CPU WCHAN
tp               201199  201199   0  0.0 futex_do_wait
tp               201199  201200   3 76.5 -
tp               201199  201201   0 76.2 -
tp               201199  201202   1 77.1 -
tp               201199  201203   1 76.8 -

thread1 グループの上限を 40%、thread2 グループの上限を 20% にそれぞれ個別に制限します。

[root@control ~]# echo "40000 100000" > /sys/fs/cgroup/test/thread1/cpu.max
[root@control ~]# echo "20000 100000" > /sys/fs/cgroup/test/thread2/cpu.max

thread1 に所属するスレッドは約20%ずつ、thread2 に所属するスレッドは約10%ずつの CPU 使用率となっています。これは、それぞれの cgroup に設定した CPU 上限(40%、20%)が同一 cgroup 内のスレッド間で分配された結果です。domain モードではプロセス単位で制御されますが、threaded モードではスレッド単位で異なるリソース制御を適用できることが確認できます。

[root@control ~]# ps -C tp -L -o comm,pid,tid,psr,%cpu,wchan
COMMAND             PID     TID PSR %CPU WCHAN
tp               201199  201199   0  0.0 futex_do_wait
tp               201199  201200   1 21.2 -
tp               201199  201201   3 21.2 -
tp               201199  201202   2 12.0 -
tp               201199  201203   1 12.0 -