この記事はNTTコムウェア Advent Calendar 2024 6日目の記事です。
NTTコムウェアの東です。
社内では技術支援・ナレッジ蓄積を行う後方部隊的な部署で特にKubernetesやコンテナ界隈を担当し、オープンソースを活用したクラウドネイティブ系技術の普及展開を目論んでいます。
皆さん、Kubernetes(k8s)でのバックアップについてどのようにお考えでしょうか?
クラウドネイティブ/k8s界隈では、なるべくSPOFを排除したいという理念のもと、結果整合性を受け入れることで可用性とスケーラビリティを確保したり、ノード/コンテナを使い捨て(家畜)として扱うことなど、可用性を高くする工夫が取り入れやすくなっています。
またほとんどのクラウドサービスで、データ領域(オブジェクトストレージ、ブロックストレージ、ファイルシステム)について、基盤側で冗長化が施されており、単なるHDDを単体で使用する際に比べ、耐久性が高くなっています。
とはいえ、故障が発生する確率を0にすることはできません。諸行無常。形あるものはいつか壊れるのです。故障はクリスマスや年末年始も関係なく襲ってきます。
そこで、k8sにおいてもバックアップが大切になってきます。
Veleroはk8s上に存在するオブジェクトやPersistentVolumeClaim(PVC)/PersistentVolume(PV)に関してバックアップおよびリストアするためにオープンソースで開発されているツールです。
k8s上でVeleroを稼働させると、k8s上に存在するリソースやPVC/PVを自動で検出し、それらを外部のオブジェクトストレージにコピーまたはスナップショットを取得します。
リストアを別のk8sクラスタ1に行うことができるので、バックアップのみならずディザスタリカバリやk8sクラスタ移行などに応用することもできます。
Veleroは比較的ドキュメントが豊富で基本的な使用方法はすぐに理解できると思います。
本稿はタイトルでも示した通り、Veleroの応用的なナレッジを目指し、バックアップにかかる時間をなるべく短縮する方法について紹介したいと思います。そのため、基本的な使用方法やインストール手順などの紹介は(紙面の都合もあり)必要最小限に留めます。
Veleroによるバックアップ/リストアの時間について、支配的なのはなんといってもPVのデータの扱いです2。
というわけで本稿では、PVのデータにフォーカスし、これをいかに早くバックアップ/リストアするかのチューニングポイントを紹介していきます。
またチューニングを応用し、実際に8倍ほど高速化 する例をハンズオン形式でも紹介します。
以降、バックアップ/リストアの詳細な動作を理論面で紹介したのち、具体的なチューニングポイントおよび実例を挙げていきます。
このうち理論面については特にk8s/Veleroが稼働する基盤環境には依らないかたちで紹介しますが、具体的なチューニングポイントおよび実例についてはAWS上のEKSを題材として紹介します。
AWS(EKS)以外の環境においては若干あてはまらない場合もあるかと思いますが、理論面をご理解のうえ適宜読み替えていただくことで参考にはなるかと思います
Veleroの機能と本稿の主眼
PVのバックアップ/リストアのチューニングは一言でいえば、「データコピーをいかに早く実行するか」に尽きます。
一方で、Veleroはバックアップ/リストア対象のPVの種類に応じ、以下4つの機能を有します。それぞれで、データのコピー先や経路が異なるため、チューニングポイントも様々です。
-
Velero Native スナップショット
- 動作:ストレージのスナップショットをVeleroが内包するドライバで取得する
- 対象:Amazon EBS、Azure Manages Disks、Google Persistent Disks 等のストレージを k8s in-tree ドライバで取り扱う場合が該当
-
CSIスナップショット
- 動作:ストレージのスナップショットをCSIドライバで取得する
- 対象:Amazon EBS、Azure Manages Disks、Google Persistent Disks 等のストレージを CSI ドライバで取り扱う場合が該当
-
CSIスナップショットデータムーブ
- 動作:ストレージのスナップショットをCSIドライバで取得し、その中身をオブジェクトストレージへコピーする
- 対象:Amazon EBS、Azure Manages Disks、Google Persistent Disks 等のストレージを CSI ドライバで取り扱う場合が該当
- "対象"が上記"CSIスナップショット"と同じですが、こちらはデータをオブジェクトストレージへコピーするため、スナップショットが直接参照できない場所(地理的に離れている等。例えば他のクラウド事業者やオンプレなど。AWSの場合、他リージョンや他アカウントが該当。)でも、オブジェクトストレージが参照できればリストアができる、という違いがあります。(一般にオブジェクトストレージの方が参照できる範囲は広く、また、コピーも容易です。)
-
File System Backup(FSB)
- 動作:ストレージの中身を(直接)オブジェクトストレージへコピーする
- 対象:Amazon EFS、AzureFile、NFS、k8s の emptyDir、local などスナップショットに対応しないほぼすべてのストレージが該当
本稿では上記のうち、最も仕組み・経路が複雑な「CSIスナップショットデータムーブ」に絞ってチューニングポイントを紹介します。
CSIスナップショットデータムーブでのチューニングポイントは、他の機能を用いる際にも応用できるものが多々あるので、各チューニングポイント紹介の際に適宜補足していきます。
CSIスナップショットに対応したボリュームの実例として、以降、AWSのAmazon EBSをPVとして用いて説明します。
CSIスナップショットデータムーブの仕組み
CSIスナップショットデータムーブを用いた際のバックアップ/リストアの流れを大まかに説明し、それを踏まえたチューニングポイントを列挙します。
バックアップ
まず対象のPVに紐づくボリュームのスナップショットをCSIドライバを介して取得します。
次に取得したスナップショットから、新しいボリュームを作成し、それをVeleroが管理するアップロード作業用PodにPVとしてアタッチします。
アップロード作業用Podは、アタッチされたボリュームの中身をkopiaと呼ばれるファイルレベルのバックアップツール3を用い、オブジェクトストレージにアップロードします。このアップロード処理はdatauploadリソースというカスタムリソースで管理されます。
リストア
Veleroにより管理されるダウンロード作業用Podが作成され、それに新規PV(空)をアタッチされます。
その中でダウンロード作業用Podがオブジェクトストレージからデータをダウンロードし、新規PVにデータを書き戻します。このダウンロード処理はdatadownloadリソースというカスタムリソースで管理されます。
書き戻しが終わると、その新規PVを(別途Veleroによりバックアップされたマニフェストからリストアされた)アプリケーションPodにアタッチし直します。
バックアップ/リストアの動作の概要図を以下に示します。
なお、図ではS3バケットのデータを別リージョンにコピーしています(グレー矢印)が、これについてはVeleroは機能を有さないため、必要な場合は別途クラウド側の機能で実施してください。
図中にバックアップ/リストアの動作(データの流れ)を踏まえた、チューニングポイントを記載しました。
- No.1 事前のスナップショット取得
- No.2 バックアップ/リストア対象のPVCのストレージタイプ高速化
- No.3 Workerノードのタイプ高速化
- No.4 S3へのVPC Endpoint
- No.5 kopiaキャッシュ領域高速化
以下、それぞれのチューニングポイントについて詳細を示します。
チューニングポイント
No.1 事前のスナップショット取得
これは、Veleroによるバックアップに伴い取得されるスナップショットを、先に手動で取得しておく、というテクニックです。
とはいえ、ここで手動で取得するスナップショットをこの後Veleroが直接使用するわけではありません。
しかしながら、Amazon EBSでは同じボリュームに対して取得した複数のスナップショットは、ユーザが意識せずとも自動的に増分バックアップになるため、あらかじめ手動で取得しておくことで、その後Veleroでの取得時にはデータ量を増加分のみに抑えられるのです。
このチューニングはバックアップ時にのみ有効です。リストア時には実施する必要はありません(実施できません)。
Azureの場合でも、Velero導入時のオプションに「incremental: "true"」を付与することで、増分スナップショットを有効化できるようなので、本テクニックが効く可能性があると思います。(すみません。実機未確認です。)
No.2 バックアップ/リストア対象のPVCのストレージタイプ高速化
Veleroによりデータがオブジェクトストレージへアップロードされる際のスループットをなるべく向上させるため、バックアップ/リストア対象のPVCのストレージタイプをなるべく高速なものにしておきます。
Veleroはアップロードに際し(当然ですが)ストレージの中身を全て読み込むため、読み込み速度を上げておくことでアップロード作業を高速にできます。
Amazon EBSおよびaws-ebs-csi-driverは、ボリュームタイプのオンラインでの変更に対応しているため、バックアップ取得時にのみ一時的にタイプを(高速なものに)変更することもできます。ただし、ボリュームタイプのオンライン変更は6時間以上の間隔を空ける必要がある点にご注意ください。
このチューニングはバックアップ/リストアの両者で有効です。また、FSB機能利用時も有効です。
No.3 Workerノードのタイプ高速化
Workerノードのタイプを、EBS帯域およびNW帯域がなるべく大きいものにしておきます。
これは、Veleroによりデータがオブジェクトストレージへアップロードされる際のスループットをなるべく向上させるためです。
バックアップ対象のPVからのデータ読み取り速度がEBS帯域により制限される点と、(ネットワークを介した)S3へのアップロードがNW帯域に制限されるため、これらをなるべく大きくしておくのです。
帯域の大きいものを選ぶと、自然にCPU/メモリに関しても増強されるため、これも性能向上に寄与します。
このチューニングはバックアップ/リストアの両者で有効です。また、FSB機能利用時も有効です。
No.4 S3へのVPC Endpoint
S3へのアクセスを高速化するため、VPC Endpointを作成します。
これは、Veleroに限らず、AWSでS3を用いる際の一般的な性能向上策の1つです。
特にS3の場合は無償のゲートウェイ型が選択できるので料金面を心配する必要もなく、使わない手はありません。
このチューニングはバックアップ/リストアの両者で有効です。また、FSB機能利用時も有効です。
No.5 kopiaキャッシュ領域高速化
Workerノードの/(root)領域となっているEBSのストレージタイプをなるべく高速なものにしておきます。
これは、Veleroが管理するダウンロード作業用PodがS3からデータを書き戻す際に、ダウンロードしたデータを一時的にコンテナ内の領域にキャッシュするため、キャッシュの書き込みがボトルネックになるのを防ぐためです。
キャッシュを配置するコンテナ内の領域の実体はWorkerノードの/(root)領域である4ため、ここのI/O性能を上げるのです。
(No.2と同様に)Amazon EBSは、ボリュームタイプのオンラインでの変更に対応しているため、バックアップ取得時にのみ一時的にタイプを(高速なものに)変更することもできます。ただし、ボリュームタイプのオンライン変更は6時間以上の間隔を空ける必要がある点にご注意ください。
このチューニングはリストア時にのみ有効です。FSB機能利用時も有効です。
実践例
ここからは、実際にデモアプリを構築し、バックアップ/リストアを実行し、かかった時間を測定してみます。
具体的には以下図のようなデモアプリをデプロイし、これまでに示したチューニング前後でどのくらい違ってくるかを確認します。
なお、チューニング前後で選択しているボリュームやインスタンスのタイプ等はあくまで例ですので、深い意味はないです。
動作させる上で前提となる環境を紹介します。
バージョン等の詳細は筆者が使用したものですが、必ずこうでなければならないわけではありません。
-
EKSクラスタが構築済みであること。
- EKS v1.30
- リージョン:ap-northeast-1
- EKS v1.30
-
EKSに以下のアドオンが導入済みであること。
-
EKSおよびAWSを操作するためのインスタンス(踏み台)を構築済みであること。
-
EKSにVeleroが構築済みであること。
- Velero v1.15.0
- Helmチャート v8.0.0 で構築
- Velero v1.15.0
チューニング無し
まず比較対象となるチューニング無しでの一連の流れを説明します。
なお、本稿では紙面の都合上、EKSやVeleroの基本的な構築・操作手順は割愛し、構築・操作に必要な設定ファイルのうちチューニングに関わる部分の記載のみに留めます。
Workerノード準備
筆者はEKS構築にeksctlを用いたため、EKS構成を定義するClusterConfig.yamlに以下2つのノードグループ定義を追記します。
ここでは"チューニング無し"ということで、あえてスペックの低いタイプ(m4.xlarge)でEKSにノードグループを作成(追加)します。
/(root)領域のボリュームタイプもgp3とはしていますが、IOPSやスループットはデフォルトのままです。
なお、チューニングには直接関連しませんが筆者は、複数のWorkerノードが確実に各AZに分散配置されるよう、タイプは同じですがあえてノードグループを分けAZを明示しています。
・・・略・・・
managedNodeGroups:
・・・略・・・
- name: m4xlarge-1a
instanceType: m4.xlarge
desiredCapacity: 1
minSize: 0
maxSize: 10
privateNetworking: true
volumeEncrypted: true
volumeType: gp3
availabilityZones: [ "ap-northeast-1a" ]
- name: m4xlarge-1c
instanceType: m4.xlarge
desiredCapacity: 1
minSize: 0
maxSize: 10
privateNetworking: true
volumeEncrypted: true
volumeType: gp3
availabilityZones: [ "ap-northeast-1c" ]
上記を追記したClusterConfig.yamlでノードグループを追加します。
$ eksctl create nodegroup --config-file=ClusterConfig.yaml
ストレージクラス/ボリュームスナップショットクラス準備
ストレージクラスおよびボリュームスナップショットクラスを以下の通り定義します。
ストレージクラスはボリュームタイプをgp3としていますが、IOPSやスループットはデフォルトのままです。
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
annotations:
storageclass.kubernetes.io/is-default-class: "true"
name: ebs-sc
provisioner: ebs.csi.aws.com
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
encrypted: "true"
type: gp3
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
labels:
velero.io/csi-volumesnapshot-class: "true"
name: csi-aws-vsc
deletionPolicy: Delete
driver: ebs.csi.aws.com
Velero準備
筆者はVeleroを以下のvalues.yamlを用いHelmチャートで構築しました。
configuration:
uploaderType: kopia
backupStorageLocation:
- name: default
bucket: <S3バケット名>
config:
region: <リージョン名>
provider: aws
accessMode: ReadWrite
volumeSnapshotLocation:
- name: default
provider: aws
config:
region: <リージョン名>
features: EnableCSI
extraEnvVars:
TZ: "Asia/Tokyo"
extraArgs:
- --backup-repository-configmap=velero-backup-repository-configmap #★1
credentials:
useSecret: false
deployNodeAgent: true
initContainers:
- name: velero-plugin-for-aws
image: velero/velero-plugin-for-aws:v1.11.0
volumeMounts:
- mountPath: /target
name: plugins
serviceAccount:
server:
create: false
name: velero-server
nodeAgent:
extraEnvVars:
TZ: "Asia/Tokyo"
extraArgs:
- --node-agent-configmap=velero-node-agent-configmap #★2
- --data-mover-prepare-timeout=120m
- --resource-timeout=120m
configMaps:
backup-repository-configmap: #★1'
labels: {}
data:
kopia: |
{
"cacheLimitMB": 71680
}
node-agent-configmap: #★2'
labels: {}
data:
node-agent-configmap.json: |+
{
"loadAffinity": [
{
"nodeSelector": {
"matchLabels": {
"node.kubernetes.io/instance-type": "m4.xlarge"
}
}
}
]
}
AWSサービスへのアクセス権限付与にはこちらを参考にIAM Roles for Service Accounts(IRSA)を使用するよう設定しています。(別途IAMポリシーおよびiamserviceaccount作成済み)
VeleroでのIRSA使用に関し、v1.13以上でバックアップ/リストア時間が1時間を超えるとトークンのタイムアウトが発生し失敗する、というissueが報告されています。2024/11/29時点でopenのため、根本的な解決には至っていないようです。
このissueは、IRSAを用いずアクセスキーを用いる構成にすることで回避できると思われます。
本稿主眼であるチューニングに関連するのは、以下★1,1',2,2'の部分です。
-
★1, 1' backup-repository-configmap関連
ダウンロード作業用Podがダウンロードしたデータをキャッシュする領域の使用量の上限を70GBに設定するConfigMapの作成およびConfigMap名の設定をしています。
"チューニング"とはちょっとズレるのでが、これを設定しないとVelero(kopia)はキャッシュ領域を際限無く使用するため、リストアするデータ量によってはWorkerノードの/(root)領域が枯渇してしまうため設定しています。
なお、70GBという値は、Workerノード作成時にボリュームサイズを指定しなかった場合のデフォルトである80GBから、OSやコンテナイメージによる使用分を引いた残りをすべて使用させる、という考え方で算出しました。
70GBでなければならないわけではないため、適宜Workerノードの残ディスク容量から逆算してください。 -
★2, 2' node-agent-configmap関連
バックアップ時にVeleroが管理するアップロード作業用Podを作成するWorkerノードを指定するための設定をConfigMapで施しています。
ここでは"チューニング無し"ということで、あえてスペックの低いタイプ(m4.xlarge)を使用するようにしています。
上記★1,1',2,2'で生成されるConfigMap名には"velero-"という接頭語が付与されます。
上記★1,1',2,2'の設定はいずれもVelero v1.15からの新機能です。
以下コマンドでVeleroをデプロイします。
$ helm install velero vmware-tanzu/velero \
--namespace velero \
--version 8.0.0 \
-f values.yaml
本稿では、Velero(が生成するPod)に対し、CPU/メモリの上限(RequestsとLimits)を設定しませんでしたが、必要に応じ他のPod等への影響を加味した上限を設定することをお勧めします。
Velero構築にHelmチャートを使用する場合、コンポーネントごとにresources:設定が分かれている点に留意してください。(本稿でダウンロード/アップロード作業Podと呼んでいるものに関係するのは"node-agent"の方です。)
デモアプリ準備
以下StatefulSetをデモアプリとします。
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-app
namespace: test
spec:
replicas: 2
selector:
matchLabels:
app: my-app
serviceName: my-app
template:
metadata:
labels:
app: my-app
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: "node.kubernetes.io/instance-type"
operator: In
values:
- "m4.xlarge" #★1
containers:
- name: my-app
image: busybox:latest
command:
- "/bin/sh"
args:
- "-c"
- "sleep inf"
volumeMounts:
- name: pv1
mountPath: "/mnt/pv1"
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: my-app
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pv1
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 25Gi
storageClassName: ebs-sc #★2
Podが2つ起動し、永遠にsleepし続け、特に何もしないものです。
それぞれのPodに25GiBのPVをアタッチしています。
チューニングに関連するのは、★1~3の部分です。
-
★1
デモアプリが稼働するWorkerノードのタイプを指定しています。
ここでは"チューニング無し"ということで、あえてスペックの低いタイプ(m4.xlarge)を使用するようにしています。 -
★2, 3
PVが使用するストレージクラスを指定しています。
さきほど作成したebs-sc(特にチューニング無し)を使用します。
$ kubectl apply -f my-app.yaml
$ kubectl get -n test pod,pvc
NAME READY STATUS RESTARTS AGE
pod/my-app-0 1/1 Running 0 45s
pod/my-app-1 1/1 Running 0 29s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/pv1-my-app-0 Bound pvc-f4fca5d4-9de3-4037-8857-8f5905414502 100Gi RWO ebs-sc <unset> 45s
persistentvolumeclaim/pv1-my-app-1 Bound pvc-6605fd2d-df94-45ef-ae70-0150714ae57f 100Gi RWO ebs-sc <unset> 29s
Pod起動後、アタッチしたPV内に、以下スクリプトで1GB×25個のダミーデータを生成しておきます。
#!/bin/bash
NS="test"
POD="my-app"
DISK=pv1
SEQ=25
COUNT=1000
for p in 0 1
do
for i in $( seq 1 $SEQ )
do
echo "pod: ${POD}-${p} , gen data /mnt/${DISK}/file${i} ..."
kubectl --context=${CONT} -n ${NS} exec -it ${POD}-${p} -- dd if=/dev/urandom of=/mnt/${DISK}/file${i} bs=1M count=${COUNT} oflag=direct
done
done
$ kubectl -n test exec -it my-app-0 -- du -sh /mnt/pv1/
24.4G /mnt/pv1/
$ kubectl -n test exec -it my-app-1 -- du -sh /mnt/pv1/
24.4G /mnt/pv1/
バックアップ
以下コマンドでVeleroによるバックアップを開始します。
各引数の意味は以下の通りです。
-
--snapshot-move-data
この引数により「CSIスナップショットデータムーブ」を有効化しています。 -
--include-namespaces test
バックアップ対象のネームスペースを(デモアプリをデプロイした)testと指定しています。 -
--csi-snapshot-timeout 6000m --item-operation-timeout 6000m
バックアップ処理のタイムアウト時間をちょっと長め(100時間)に設定しています。
前者がCSIスナップショット取得のタイムアウトでデフォルト10分、後者がその他操作(データアップロード)のタイムアウトでデフォルト4時間です。
$ velero create backup myapp-tune-off \
--snapshot-move-data \
--include-namespaces test \
--csi-snapshot-timeout 6000m \
--item-operation-timeout 6000m
バックアップを開始すると、まずVeleroによりVolumeSnapshotリソースが作成され、スナップショットの作成が開始されます。ただし、VolumeSnapshotリソースはVelero動作が次のフェーズに移ると、veleroネームスペースに移動し、バックアップが完了すると削除されます。
$ kubectl -n test get volumesnapshot
NAME READYTOUSE SOURCEPVC SOURCESNAPSHOTCONTENT RESTORESIZE SNAPSHOTCLASS SNAPSHOTCONTENT CREATIONTIME AGE
velero-pv1-my-app-0-qrs9l false pv1-my-app-0 25Gi csi-aws-vsc snapcontent-bb410d8d-c7e8-4388-a68a-48e1607b0f80 85s 86s
velero-pv1-my-app-1-vgc4j false pv1-my-app-1 25Gi csi-aws-vsc snapcontent-33edd93a-a7d6-4383-89c4-d820843b50bf 80s 81s
スナップショット完了までにはデータ量に応じた時間がかかります。
スナップショット取得はsnapshot-controllerの以下ログで確認できます。ここでは、34分ほどかかりました。
$ kubectl -n kube-system logs snapshot-controller-xxxxxxxxx-xxxxx
・・・
I1127 04:48:10.339757 1 snapshot_controller.go:645] createSnapshotContent: Creating content for snapshot test/velero-pv1-my-app-0-qrs9l through the plugin ...
I1127 04:48:15.493025 1 snapshot_controller.go:645] createSnapshotContent: Creating content for snapshot test/velero-pv1-my-app-1-vgc4j through the plugin ...
・・・
I1127 05:22:25.728909 1 event.go:377] Event(v1.ObjectReference{Kind:"VolumeSnapshot", Namespace:"test", Name:"velero-pv1-my-app-0-qrs9l", UID:"bb410d8d-c7e8-4388-a68a-48e1607b0f80", APIVersion:"snapshot.storage.k8s.io/v1", ResourceVersion:"69729114", FieldPath:""}): type: 'Normal' reason: 'SnapshotReady' Snapshot test/velero-pv1-my-app-0-qrs9l is ready to use.
I1127 05:22:30.027185 1 event.go:377] Event(v1.ObjectReference{Kind:"VolumeSnapshot", Namespace:"test", Name:"velero-pv1-my-app-1-vgc4j", UID:"33edd93a-a7d6-4383-89c4-d820843b50bf", APIVersion:"snapshot.storage.k8s.io/v1", ResourceVersion:"69729164", FieldPath:""}): type: 'Normal' reason: 'SnapshotReady' Snapshot test/velero-pv1-my-app-1-vgc4j is ready to use.
・・・
スナップショット取得が完了すると、それを基に新しいボリュームが作成され、データのS3へのアップロードが開始されます。アップロード作業の進捗は、datauploadリソースで確認できます。
$ kubectl get -n velero dataupload
NAME STATUS STARTED BYTES DONE TOTAL BYTES STORAGE LOCATION AGE NODE
myapp-tune-off-c8dxm Completed 78m 22336569344 26214400000 default 112m ip-192-168-101-246.ap-northeast-1.compute.internal
myapp-tune-off-hcc8q Completed 78m 21659385856 26214400000 default 112m ip-192-168-58-61.ap-northeast-1.compute.internal
"チューニング無し"の環境ではバックアップは約62分(3706秒)で完了しました。
$ velero backup get myapp-tune-off
NAME STATUS ERRORS WARNINGS CREATED EXPIRES STORAGE LOCATION SELECTOR
myapp-tune-off Completed 0 0 2024-11-27 13:48:09 +0900 JST 28d default <none>
$ velero backup describe --details myapp-tune-off
Name: myapp-tune-off
Namespace: velero
・・・
Phase: Completed
・・・
Started: 2024-11-27 13:48:09 +0900 JST
Completed: 2024-11-27 14:49:55 +0900 JST
・・・
リストア
続いて、リストア時間を確認します。
#先ほどチューニングポイントを示した図では、S3データを他リージョンへコピーしてリストアする構成を描きましたが、ここでは簡単のため同一リージョン/k8sクラスタにリストアします。
-
--from-backup myapp-tune-off
どのバックアップを起点としてリストアを行うかを指定します。
さきほど取得したバックアップ名"myapp-tune-off"を指定します。 -
--item-operation-timeout 6000m
タイムアウト時間をちょっと長め(100時間)に設定しています。 -
--namespace-mappings test:test-r
リストア時に元のネームスペース"test"を、"test-r"に書き変えたかたちでリストアします。
これにより、元のアプリをそのままにリストアできるため、本稿のような動作確認には便利です。
なお、以下コマンドでは"リストア名"について省略しています。その場合、リストア名はバックアップ名にリストア開始日時を付与したもの(バックアップ名-YYYYMMDDHHMISS)になります。
$ velero create restore \
--from-backup myapp-tune-off \
--item-operation-timeout 6000m \
--namespace-mappings test:test-r
リストアが開始されると、まず各マニフェストのリストアが行われ、アプリのPod等がVeleroにより作成(リストア)されます。
ですが、PVを持ったPodの場合、Veleroによる書き戻しが完了するまではPodは起動せずPendingとなります。
$ kubectl -n test-r get pod,pvc
NAME READY STATUS RESTARTS AGE
pod/my-app-0 0/1 Pending 0 93s
pod/my-app-1 0/1 Pending 0 93s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/pv1-my-app-0 Pending ebs-sc <unset> 93s
persistentvolumeclaim/pv1-my-app-1 Pending ebs-sc <unset> 93s
Veleroによる書き戻しの進捗状況は、datadownloadリソースで確認できます。
$ kubectl -n velero get datadownload
NAME STATUS STARTED BYTES DONE TOTAL BYTES STORAGE LOCATION AGE NODE
myapp-tune-off-20241127154405-rvs2d InProgress 19s 875626496 26214400000 default 38s ip-192-168-58-61.ap-northeast-1.compute.internal
myapp-tune-off-20241127154405-tmm5b InProgress 15s 871596032 26214400000 default 38s ip-192-168-101-246.ap-northeast-1.compute.internal
"チューニング無し"の環境ではリストアは約12分(719秒)で完了しました。
$ velero restore get myapp-tune-off-20241127154405
NAME BACKUP STATUS STARTED COMPLETED ERRORS WARNINGS CREATED
SELECTOR
myapp-tune-off-20241127154405 myapp-tune-off Completed 2024-11-27 15:44:06 +0900 JST 2024-11-27 15:56:05 +0900 JST 0 1 2024-11-27 15:44:06 +0900 JST <none>
$ velero restore describe --details myapp-tune-off-20241127154405
Name: myapp-tune-off-20241127154405
Namespace: velero
・・・
Phase: Completed
・・・
Started: 2024-11-27 15:44:06 +0900 JST
Completed: 2024-11-27 15:56:05 +0900 JST
・・・
チューニング有り
続いて、チューニング有りでの一連の流れを説明します。
前述の「チューニング無し」との差分のみにフォーカスし記載していきます。
VPC Endpoint作成
チューニングポイントNo.4に対応する手順として、公式ドキュメント等を参考に、EKSをデプロイしたVPCにS3用のエンドポイントを作成します。
Workerノード準備
"チューニング無し"ではインスタンスタイプを"m4.xlarge"としましたが、これを"c6i.4xlarge"に変更します(★1)。
また、/(root)領域としてアタッチするEBSボリュームのスループットを312MB/sに変更しています(★2)。
(IOPSについてはデフォルトの3000のままとしています。)
・・・
managedNodeGroups:
・・・
- name: c6i4xlarge-1a
instanceType: c6i.4xlarge #★1
desiredCapacity: 1
minSize: 0
maxSize: 10
privateNetworking: true
volumeEncrypted: true
volumeType: gp3
availabilityZones: [ "ap-northeast-1a" ]
volumeThroughput: 312 #★2
- name: c6i4xlarge-1c
instanceType: c6i.4xlarge #★1
desiredCapacity: 1
minSize: 0
maxSize: 10
privateNetworking: true
volumeEncrypted: true
volumeType: gp3
availabilityZones: [ "ap-northeast-1c" ]
volumeThroughput: 312 #★2
変更前後のdiffは以下の通りです。(折りたたんでいます)
diff
@@ -66,8 +66,8 @@
privateNetworking: true
volumeEncrypted: true
-- name: m4xlarge-1a
- instanceType: m4.xlarge
+- name: c6i4xlarge-1a
+ instanceType: c6i.4xlarge
desiredCapacity: 1
minSize: 0
maxSize: 10
@@ -75,9 +75,10 @@
volumeEncrypted: true
volumeType: gp3
availabilityZones: [ "ap-northeast-1a" ]
+ volumeThroughput: 312
-- name: m4xlarge-1c
- instanceType: m4.xlarge
+- name: c6i4xlarge-1c
+ instanceType: c6i.4xlarge
desiredCapacity: 1
minSize: 0
maxSize: 10
@@ -85,3 +86,4 @@
volumeEncrypted: true
volumeType: gp3
availabilityZones: [ "ap-northeast-1c" ]
+ volumeThroughput: 312
変更(追記)したClusterConfig.yamlでノードグループを追加します。
$ eksctl create nodegroup --config-file=ClusterConfig.yaml
ストレージクラス/ボリュームスナップショットクラス準備
"チューニング有り"用のストレージクラス"ebs-sc-tune"を、以下マニフェストで新たに作成します。
EBSボリュームのスループットを312MB/sに変更しています(★1)。
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-sc-tune
provisioner: ebs.csi.aws.com
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
encrypted: "true"
type: gp3
throughput: "312" #★1
変更前後のdiffは以下の通りです。(折りたたんでいます)
diff
@@ -2,9 +2,7 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
- annotations:
- storageclass.kubernetes.io/is-default-class: "true"
- name: ebs-sc
+ name: ebs-sc-tune
provisioner: ebs.csi.aws.com
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
@@ -12,4 +10,6 @@
parameters:
encrypted: "true"
type: gp3
+ throughput: "312"
ボリュームスナップショットクラスに変更はありません。
Velero準備
"チューニング無し"で使用したHelm用のvalues.yamlで定義したConfigMap"velero-node-agent-configmap"の内容を、(先ほど作成した)性能の高いインスタンスタイプを使用するよう変更します。
ConfigMapは直接書き換えられないので、一旦削除し、新たにデプロイします。
その後、変更を反映するため、(Veleroの各ノードでのアップロード/ダウンロード等の実処理を担う)node-agentを再起動します。
## Configmap"velero-node-agent-configmap"を一旦削除
$ kubectl -n velero delete configmap velero-node-agent-configmap
## 新たなConfigMap定義を作成
$ cat << EOF > velero-node-agent-configmap-tune.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: velero-node-agent-configmap
namespace: velero
data:
node-agent-configmap.json: |
{
"loadAffinity": [
{
"nodeSelector": {
"matchLabels": {
"node.kubernetes.io/instance-type": "c6i.4xlarge"
}
}
}
]
}
EOF
## 新たなConfigMapをデプロイ
$ kubectl apply -f velero-node-agent-configmap-tune.yaml
## node-agentを再起動
$ kubectl -n velero rollout restart daemonset node-agent
デモアプリ準備
デモアプリについて、以下の3点を変更し、デプロイします。
- ネームスペースをtest-tuneに変更
- 配置先ノード(Node Affinity)を高スペックなもの(c6i.4xlarge)に変更
- PVのストレージクラスを"ebs-sc-tune"に変更
@@ -2,13 +2,13 @@
apiVersion: v1
kind: Namespace
metadata:
- name: test
+ name: test-tune
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-app
- namespace: test
+ namespace: test-tune
spec:
replicas: 2
selector:
@@ -29,7 +29,7 @@
- key: "node.kubernetes.io/instance-type"
operator: In
values:
- - "m4.xlarge"
+ - "c6i.4xlarge"
containers:
- name: my-app
image: busybox:latest
@@ -61,7 +61,7 @@
resources:
requests:
storage: 25Gi
- storageClassName: ebs-sc
+ storageClassName: ebs-sc-tune
先ほどと同様に、Pod起動後、アタッチしたPV内に1GB×25個のダミーデータを生成しておきます。
バックアップ
チューニングNo.1に対応する手順として、事前にPVのスナップショットを取得します。
スナップショットは(Veleroも使用する)CSIのスナップショット機能を用いると簡単です。
以下の各VolumeSnapshotリソースをデプロイすることで、スナップショットが取得されます。
## VolumeSnapshotリソース(yaml)定義
$ cat << EOF > volumesnapshots.yaml
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: pv1-my-app-0-snapshot
namespace: test-tune
labels:
created-for: "preparation-for-backup-with-velero"
spec:
volumeSnapshotClassName: csi-aws-vsc
source:
persistentVolumeClaimName: pv1-my-app-0
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: pv1-my-app-1-snapshot
namespace: test-tune
labels:
created-for: "preparation-for-backup-with-velero"
spec:
volumeSnapshotClassName: csi-aws-vsc
source:
persistentVolumeClaimName: pv1-my-app-1
EOF
## VolumeSnapshotリソースデプロイ
$ kubectl apply -f volumesnapshots.yaml
CSIを用いず、クラウドのAPIを直接利用してもかまいません。
例えばAmazon EBSの場合、以下のようなコマンドでもスナップショットが取得できます。
$ aws ec2 create-snapshot --volume-id <ボリュームID>
ボリュームIDは、(k8sの)PVのIDではなく、(AWSが管理の)EBSボリュームのIDである点に留意してください。
PVの.spec.csi.volumeHandleの値で確認できます。(以下vol-09133bf242b6a4ff7がボリュームID)
$ kubectl get pv pvc-6605fd2d-df94-45ef-ae70-0150714ae57f -o json | jq -r '.spec.csi.volumeHandle'
vol-09133bf242b6a4ff7
VolumeSnapshotリソースのreadyToUse欄がtrueになること、または各クラウドの管理コンソール上で完了を確認してください。
$ kubectl -n test-tune get VolumeSnapshot
なお、スナップショットの完了までにはデータ量に応じた時間がかかります。
が、この作業はVeleroによるバックアップ作業とは完全に切り離されており、かつ、あくまで後の(Veleroによる)スナップショット取得時の差分を最小化する目的であるためアプリケーションに全く影響を与えずにバックグラウンドで実行できるというのがミソです。
ちなみに筆者環境では、"チューニング無し"では34分程度かかったスナップショット取得が、チューニング後は3分程度で完了しました。本稿では主眼にしていませんが、EBSのボリュームタイプ/性能がスナップショット取得時間にも影響するようです。
スナップショット取得時間はsnapshot-controllerの以下ログで確認しました。
$ kubectl -n kube-system logs snapshot-controller-xxxxxxxxx-xxxxx
・・・
I1127 07:25:01.098612 1 snapshot_controller.go:645] createSnapshotContent: Creating content for snapshot test-tune/pv1-my-app-0-snapshot through the plugin ...
I1127 07:25:01.120896 1 snapshot_controller.go:645] createSnapshotContent: Creating content for snapshot test-tune/pv1-my-app-1-snapshot through the plugin ...
・・・
I1127 07:29:22.274013 1 event.go:377] Event(v1.ObjectReference{Kind:"VolumeSnapshot", Namespace:"test-tune", Name:"pv1-my-app-0-snapshot", UID:"5b312c95-c8b2-4fd1-81ab-4aaaa15d0dce", APIVersion:"snapshot.storage.k8s.io/v1", ResourceVersion:"69782586", FieldPath:""}): type: 'Normal' reason: 'SnapshotReady' Snapshot test-tune/pv1-my-app-0-snapshot is ready to use.
I1127 07:29:22.294625 1 event.go:377] Event(v1.ObjectReference{Kind:"VolumeSnapshot", Namespace:"test-tune", Name:"pv1-my-app-1-snapshot", UID:"30871aa1-3534-4885-bf6d-1e6406245b86", APIVersion:"snapshot.storage.k8s.io/v1", ResourceVersion:"69782584", FieldPath:""}): type: 'Normal' reason: 'SnapshotReady' Snapshot test-tune/pv1-my-app-1-snapshot is ready to use.
・・・
スナップショットが完了したら速やかにバックアップを実行します。
コマンドはバックアップ名以外、"チューニング無し"と同じです。
$ velero create backup myapp-tune-on \
--snapshot-move-data \
--include-namespaces test-tune \
--csi-snapshot-timeout 6000m \
--item-operation-timeout 6000m
バックアップを開始すると、まずVeleroによりVolumeSnapshotリソースが作成され、スナップショットの作成が開始されます。
が、スナップショットはすでに先ほど手で作成済みなので、(差分が多くない限り)すぐに完了します。
この場合、1分程度で完了しました。
$ kubectl -n kube-system logs snapshot-controller-xxxxxxxxx-xxxxx
・・・
I1127 07:32:12.266727 1 snapshot_controller.go:645] createSnapshotContent: Creating content for snapshot test-tune/velero-pv1-my-app-0-cwrx4 through the plugin ...
I1127 07:32:17.397906 1 snapshot_controller.go:645] createSnapshotContent: Creating content for snapshot test-tune/velero-pv1-my-app-1-44hzs through the plugin ...
・・・
I1127 07:33:20.231330 1 event.go:377] Event(v1.ObjectReference{Kind:"VolumeSnapshot", Namespace:"test-tune", Name:"velero-pv1-my-app-0-cwrx4", UID:"a2bd236f-daed-44e5-9805-e642c9bafb77", APIVersion:"snapshot.storage.k8s.io/v1", ResourceVersion:"69785318", FieldPath:""}): type: 'Normal' reason: 'SnapshotReady' Snapshot test-tune/velero-pv1-my-app-0-cwrx4 is ready to use.
I1127 07:33:24.804995 1 event.go:377] Event(v1.ObjectReference{Kind:"VolumeSnapshot", Namespace:"test-tune", Name:"velero-pv1-my-app-1-44hzs", UID:"8095dab2-4356-40d7-8015-ebcb8d88d783", APIVersion:"snapshot.storage.k8s.io/v1", ResourceVersion:"69785371", FieldPath:""}): type: 'Normal' reason: 'SnapshotReady' Snapshot test-tune/velero-pv1-my-app-1-44hzs is ready to use.
・・・
VolumeSnapshotは以下の通り、先ほど手で作成したものとVeleroにより作成したものの両者が表示されます。
$ kubect get -n test-tune volumesnapshot
NAME READYTOUSE SOURCEPVC SOURCESNAPSHOTCONTENT RESTORESIZE SNAPSHOTCLASS SNAPSHOTCONTENT CREATIONTIME AGE
pv1-my-app-0-snapshot true pv1-my-app-0 25Gi csi-aws-vsc snapcontent-5b312c95-c8b2-4fd1-81ab-4aaaa15d0dce 8m6s
8m7s
pv1-my-app-1-snapshot true pv1-my-app-1 25Gi csi-aws-vsc snapcontent-30871aa1-3534-4885-bf6d-1e6406245b86 8m6s
8m7s
velero-pv1-my-app-0-cwrx4 false pv1-my-app-0 25Gi csi-aws-vsc snapcontent-a2bd236f-daed-44e5-9805-e642c9bafb77 55s
56s
velero-pv1-my-app-1-44hzs false pv1-my-app-1 25Gi csi-aws-vsc snapcontent-8095dab2-4356-40d7-8015-ebcb8d88d783 51s
51s
スナップショット取得が完了すると、それを基に新しいボリュームが作成され、データのS3へのアップロードが開始されます。
アップロード作業の進捗は、datauploadリソースで確認できます。
$ kubectl get -n velero dataupload
NAME STATUS STARTED BYTES DONE TOTAL BYTES STORAGE LOCATION AGE NODE
myapp-tune-on-5djz2 InProgress 22s 1199964160 26214400000 default 98s ip-192-168-2-155.ap-northeast-1.compute.internal
myapp-tune-on-rhns8 InProgress 26s 2461466624 26214400000 default 103s ip-192-168-119-84.ap-northeast-1.compute.internal
"チューニング有り"の環境ではバックアップは約4分(214秒)で完了しました。
$ velero backup get myapp-tune-on
NAME STATUS ERRORS WARNINGS CREATED EXPIRES STORAGE LOCATION SELECTOR
myapp-tune-on Completed 0 0 2024-11-27 16:32:11 +0900 JST 29d default <none>
$ velero backup describe --details myapp-tune-on
Name: myapp-tune-on
・・・
Phase: Completed
・・・
Started: 2024-11-27 16:32:11 +0900 JST
Completed: 2024-11-27 16:35:45 +0900 JST
・・・
リストア
続いて、リストア時間を確認します。
元とするバックアップ名およびネームスペースが異なる以外、コマンドは"チューニング無し"と同じです。
$ velero create restore \
--from-backup myapp-tune-on \
--item-operation-timeout 6000m \
--namespace-mappings test-tune:test-tune-r
"チューニング無し"と同様に、まず各マニフェストのリストアが行われ、アプリのPod等がVeleroにより作成(リストア)されます。
$ kubectl -n test-tune-r get pod,pvc
NAME READY STATUS RESTARTS AGE
pod/my-app-0 0/1 Pending 0 34s
pod/my-app-1 0/1 Pending 0 33s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/pv1-my-app-0 Pending ebs-sc-tune <unset> 34s
persistentvolumeclaim/pv1-my-app-1 Pending ebs-sc-tune <unset> 34s
Veleroによる書き戻しの進捗状況は、datadownloadリソースで確認できます。
$ kubectl -n velero get datadownload
NAME STATUS STARTED BYTES DONE TOTAL BYTES STORAGE LOCATION AGE NODE
myapp-tune-on-20241127164031-4cr66 InProgress 23s 8561721344 26214400000 default 45s ip-192-168-2-155.ap-northeast-1.compute.internal
myapp-tune-on-20241127164031-wjtkc InProgress 27s 8712224768 26214400000 default 45s ip-192-168-119-84.ap-northeast-1.compute.internal
"チューニング有り"の環境ではリストアは約2分(123秒)で完了しました。
$ velero restore get myapp-tune-on-20241127164031
NAME BACKUP STATUS STARTED COMPLETED ERRORS WARNINGS CREATED
SELECTOR
myapp-tune-on-20241127164031 myapp-tune-on Completed 2024-11-27 16:40:32 +0900 JST 2024-11-27 16:42:35 +0900 JST 0 2 2024-11-27 16:40:32 +0900
JST <none>
$ velero restore describe --details myapp-tune-on-20241127164031
Name: myapp-tune-on-20241127164031
・・・
Phase: Completed
・・・
Started: 2024-11-27 16:40:32 +0900 JST
Completed: 2024-11-27 16:42:35 +0900 JST
・・・
結果まとめ
チューニング有無とバックアップ/リストア時間をまとめると以下の通りとなりました。
操作 | チューニング前 | チューニング後 |
---|---|---|
バックアップ | 3706秒(61.8分) | 430秒(7.2分) |
リストア | 719秒(12.0分) | 123秒(2.1分) |
バックアップについて、スナップショット取得にかかった時間とアップロードにかかった時間の内訳は以下の通りでした。
操作 | チューニング前 | チューニング後 |
---|---|---|
スナップショット (バックアップ開始~スナップショット取得完了) |
2060秒(34.3分) | 216秒(3.6分) |
アップロード (スナップショット取得完了~バックアップ完了) |
1646秒(27.4分) | 214秒(3.6分) |
バックアップで8.6倍、リストアで5.8倍高速化することができました。
ボリュームやインスタンスタイプを増強しただけの効果が出たかたちです。
※チューニング前後でディスク/NW/CPU/メモリの使用量がどうなったかは、本稿末尾に参考として記載します。詳細を知りたい方はそちらも参照してみてください。
さいごに
というわけで、無事にチューニングによりVeleroの動作を爆速にすることができました。
ボリュームやインスタンスタイプを高速なものにすると料金は高くなりますが、その分時間を節約できるということで、タイムisマネーです。
今回はチューニングということで、いろいろな手順を紹介しましたが、Veleroでのバックアップ/リストア自体はとても簡単(ワンコマンド)であることも感じていただけたかと思います。
皆様も是非Veleroを活用し、より堅牢にk8sを運用し心安らかにクリスマス&年末をお過ごしください。
※本稿に記載されている製品名、サービス名は、各団体の商標または登録商標です。
参考:バックアップ/リストア時のリソース消費
以下、参考情報として、ディスク/NW/CPU/メモリのそれぞれについて、実行中の推移を紹介しながら、チューニングの効果を確認していきます。
なお、Workerノードのグラフ(ディスク/ネットワーク)については、アップロード/ダウンロード処理が2つのノードで並列に実行されたため2ノード分収集しましたが、各ノードで傾向に大きな差は無いため、片方のみ掲載します。
バックアップ
- WorkerノードのディスクI/O
- チューニング無し
- チューニング有り
- チューニング無しでは15MB/sあたりで不安定な挙動5でしたが、チューニング有りでは最大250MB/sとなり、短時間で処理を完了できました
- チューニング無しでバックアップ開始(13:48)から35分程度は0で推移していますが、これはAWS側でのスナップショット取得を待っているためです
- チューニング無し
-
WorkerノードのネットワークI/O
-
アップロード作業PodのCPU/メモリ
本稿では、Velero(が生成するPod)に対し、CPU/メモリの上限(RequestsとLimits)を設定しませんでしたが、必要に応じ他のPod等への影響を加味した上限を設定することをお勧めします。
Velero構築にHelmチャートを使用する場合、コンポーネントごとにresources:設定が分かれている点に留意してください。(本稿でダウンロード/アップロード作業Podと呼んでいるものに関係するのは"node-agent"の方です。)
リストア
-
WorkerノードのディスクI/O
-
WorkerノードのネットワークI/O
-
ダウンロード作業PodのCPU/メモリ
-
Workerノードのディスク使用量
-
別のリージョン、別のアカウント等、バックアップデータ(≒オブジェクトストレージの中身)にアクセスできる範囲であればリストア可能です。クラウドサービスをまたがってリストアしたケースもあるようです。 ↩
-
Pod等を定義するマニフェストは単なるテキストでサイズがそれほど大きくないためPVに比べバックアップ/リストア時間にはほとんど影響しません。 ↩
-
Veleroはkopiaを内包しており、意識せずともVeleroが内部的にkopiaを利用しています。 ↩
-
Velero v1.14まではダウンロード作業用Pod=DeamonSetで稼働のnode-agentであったため、これにエフェメラルボリュームをアタッチすることでキャッシュ領域にPVを使用することができました。が、Velero v1.15でアーキテクチャが変更され、ダウンロード作業用Podが都度直接作成されるものになり、このPodにspec.nodeNameが付与されているがためこの制限に引っかかり、volumeBindingModeがWaitForFirstConsumerであるストレージクラスのPVをアタッチできなくなりました。 ↩
-
Amazon EBSではスナップショットから新規作成したボリュームは裏側でS3からの書き戻し(事前処理)が完了するまでレイテンシーが増加するためこのような挙動になったものと思われます。 ↩