使用NVMe云盤(pán)多重掛載及Reservation實(shí)現(xiàn)應(yīng)用間的數(shù)據(jù)共享
支持NVMe(Non-Volatile Memory Express)協(xié)議的ESSD云盤(pán)稱為NVMe云盤(pán)。NVMe云盤(pán)支持多重掛載能力,最多可以同時(shí)掛載到16個(gè)ECS實(shí)例上;同時(shí)也基于多重掛載實(shí)現(xiàn)了符合NVMe協(xié)議規(guī)范的Reservation功能。這些特性可以幫助您實(shí)現(xiàn)應(yīng)用的多個(gè)副本間的數(shù)據(jù)共享以提升數(shù)據(jù)讀寫(xiě)性能。本文通過(guò)簡(jiǎn)單的示例介紹如何在ACK集群中使用NVMe云盤(pán)多重掛載及Reservation功能。
閱讀前提示
為了讓您更好地使用NVMe云盤(pán)多重掛載及Reservation功能,建議您在閱讀本文檔之前,了解以下內(nèi)容:
NVMe協(xié)議介紹,請(qǐng)參見(jiàn)NVMe協(xié)議概述。
NVMe云盤(pán)介紹,請(qǐng)參見(jiàn)NVMe云盤(pán)概述。
應(yīng)用場(chǎng)景
云盤(pán)多重掛載主要有以下應(yīng)用場(chǎng)景:
NVMe最簡(jiǎn)單的應(yīng)用場(chǎng)景為數(shù)據(jù)共享,當(dāng)數(shù)據(jù)被寫(xiě)入云盤(pán)后,其他節(jié)點(diǎn)均可以訪問(wèn)該數(shù)據(jù),從而有效節(jié)省成本并提升讀寫(xiě)性能。例如,在云上容器鏡像場(chǎng)景,同一套系統(tǒng)的鏡像通常相似,因此多個(gè)不同實(shí)例可以讀取加載同一份鏡像。
業(yè)務(wù)高可用是共享盤(pán)最常見(jiàn)的應(yīng)用場(chǎng)景之一。傳統(tǒng)基于SAN的數(shù)據(jù)庫(kù),例如Oracle RAC、SAP HANA以及云原生高可用數(shù)據(jù)庫(kù)等場(chǎng)景中,實(shí)際業(yè)務(wù)使用過(guò)程中可能存在單點(diǎn)故障,確保故障情況下業(yè)務(wù)連續(xù)性是高可用系統(tǒng)的核心能力,在云上存儲(chǔ)和網(wǎng)絡(luò)具備極高的可用性。而計(jì)算節(jié)點(diǎn)則經(jīng)常受斷電、宕機(jī)、硬件故障等影響,所以業(yè)務(wù)通常搭建主備模式解決計(jì)算的高可用問(wèn)題。
例如數(shù)據(jù)庫(kù)場(chǎng)景,當(dāng)主庫(kù)故障時(shí)迅速切換到備庫(kù)對(duì)外提供服務(wù),實(shí)例切換后,可以通過(guò)NVMe PR命令釋放舊實(shí)例的寫(xiě)入權(quán)限,從而確保舊實(shí)例不再寫(xiě)入數(shù)據(jù)確保數(shù)據(jù)一致性。如圖所示,故障轉(zhuǎn)移流程說(shuō)明如下:
說(shuō)明PR(PersistentReservation)屬于NVMe協(xié)議的一部分,PR可精確地控制某個(gè)云盤(pán)的讀寫(xiě)權(quán)限,從而確保計(jì)算端按照預(yù)期寫(xiě)入數(shù)據(jù)。更多信息,請(qǐng)參見(jiàn)NVMe PR協(xié)議。
數(shù)據(jù)庫(kù)主實(shí)例1故障,導(dǎo)致業(yè)務(wù)停止。
下發(fā)NVMe PR命令,禁止數(shù)據(jù)庫(kù)實(shí)例1繼續(xù)寫(xiě)入數(shù)據(jù),允許數(shù)據(jù)庫(kù)實(shí)例2寫(xiě)入數(shù)據(jù)。
數(shù)據(jù)庫(kù)實(shí)例2通過(guò)日志回放等方式恢復(fù)到和數(shù)據(jù)庫(kù)實(shí)例1一致的狀態(tài)。
切換數(shù)據(jù)庫(kù)實(shí)例2為主實(shí)例,繼續(xù)對(duì)外提供服務(wù)。
開(kāi)啟多重掛載功能的云盤(pán)具備較高的IOPS和吞吐性能,可以為其他中低速的存儲(chǔ)系統(tǒng)提供性能加速能力。例如數(shù)據(jù)湖場(chǎng)景,數(shù)據(jù)湖通常基于OSS搭建,可同時(shí)被多個(gè)客戶端訪問(wèn),同時(shí)具備較高的順序讀吞吐和追加寫(xiě)吞吐能力,但是其順序讀寫(xiě)吞吐和延遲較差,其隨機(jī)讀寫(xiě)性能較差。通過(guò)在計(jì)算節(jié)點(diǎn)上掛載高速云盤(pán)作為緩存,可以極大地提升數(shù)據(jù)湖等場(chǎng)景的訪問(wèn)性能。
在分布式機(jī)器學(xué)習(xí)訓(xùn)練中,將樣本標(biāo)注寫(xiě)入后,會(huì)將數(shù)據(jù)集分割成小塊分發(fā)到多個(gè)計(jì)算節(jié)點(diǎn)上并行處理。云盤(pán)多重掛載使得每個(gè)計(jì)算節(jié)點(diǎn)都能直接訪問(wèn)共享的存儲(chǔ)資源,無(wú)需通過(guò)網(wǎng)絡(luò)頻繁傳輸數(shù)據(jù),減少了數(shù)據(jù)傳輸?shù)难舆t,從而加速了模型訓(xùn)練過(guò)程。云盤(pán)的高性能與多重掛載功能相結(jié)合,為機(jī)器學(xué)習(xí)場(chǎng)景提供了一個(gè)高效、靈活的存儲(chǔ)解決方案,特別是針對(duì)需要高速數(shù)據(jù)訪問(wèn)和處理的大規(guī)模模型訓(xùn)練任務(wù),能夠顯著提升整個(gè)機(jī)器學(xué)習(xí)流程的效率和效果。
使用限制
單個(gè)NVMe云盤(pán)支持同時(shí)掛載到同一可用區(qū)內(nèi)的最多16個(gè)ECS實(shí)例。
ACK僅支持通過(guò)volumeDevices的方式掛載可從多個(gè)節(jié)點(diǎn)讀寫(xiě)的云盤(pán),即不能通過(guò)文件系統(tǒng)訪問(wèn)。
更多使用限制,請(qǐng)參見(jiàn)多重掛載使用限制。
前提條件
已創(chuàng)建ACK托管集群,且集群為1.20及以上版本。具體操作,請(qǐng)參見(jiàn)創(chuàng)建ACK托管集群。
已安裝csi-plugin和csi-provisioner組件,且組件為v1.24.10-7ae4421-aliyun及以上版本。關(guān)于csi-plugin和csi-provisioner組件的升級(jí)操作,請(qǐng)參見(jiàn)管理CSI組件。
集群至少包含2個(gè)在同一可用區(qū)的且支持使用多重掛載功能的節(jié)點(diǎn),支持的實(shí)例規(guī)格族詳見(jiàn)多重掛載使用限制。
已準(zhǔn)備好業(yè)務(wù)應(yīng)用且符合以下要求,然后將應(yīng)用打包至容器鏡像中用于在ACK集群中部署。
應(yīng)用支持同時(shí)從多個(gè)副本中訪問(wèn)同一云盤(pán)中的數(shù)據(jù)。
應(yīng)用能自行通過(guò)標(biāo)準(zhǔn)的NVMe Reservation等功能確保數(shù)據(jù)的一致性。
計(jì)費(fèi)說(shuō)明
云盤(pán)多重掛載功能不會(huì)產(chǎn)生額外費(fèi)用,支持NVMe協(xié)議的相關(guān)資源仍保持各資源原有的計(jì)費(fèi)方式。關(guān)于云盤(pán)相關(guān)計(jì)費(fèi)的更多信息,請(qǐng)參見(jiàn)計(jì)費(fèi)說(shuō)明。
應(yīng)用示例
本文使用下方應(yīng)用示例的源代碼和Dockerfile,將其構(gòu)建后上傳至鏡像倉(cāng)庫(kù)以便后續(xù)在集群中部署。該示例應(yīng)用中的多個(gè)副本共同管理一個(gè)租約,但僅有一個(gè)副本持有該租約。若該副本無(wú)法正常工作,其他副本將自動(dòng)搶占該租約。編寫(xiě)應(yīng)用注意事項(xiàng)如下:
示例中使用
O_DIRECT
打開(kāi)塊設(shè)備進(jìn)行讀寫(xiě),避免任何緩存對(duì)測(cè)試的影響。示例中使用Linux內(nèi)核提供的Reservation簡(jiǎn)化接口,應(yīng)用也可使用以下兩種方法執(zhí)行與Reservation相關(guān)的命令,以下方法需要特權(quán)。
C代碼:
ioctl(fd, NVME_IOCTL_IO_CMD, &cmd);
命令行工具:
nvme-cli
關(guān)于NVMe Reservation功能的詳細(xì)信息,請(qǐng)參見(jiàn)NVMe Specification。
#define _GNU_SOURCE
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/pr.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
const char *disk_device = "/dev/data-disk";
uint64_t magic = 0x4745D0C5CD9A2FA4;
void panic(const char *restrict format, ...) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
exit(EXIT_FAILURE);
}
struct lease {
uint64_t magic;
struct timespec acquire_time;
char holder[64];
};
volatile bool shutdown = false;
void on_term(int signum) {
shutdown = true;
}
struct lease *lease;
const size_t lease_alloc_size = 512;
void acquire_lease(int disk_fd) {
int ret;
struct pr_registration pr_reg = {
.new_key = magic,
.flags = PR_FL_IGNORE_KEY,
};
ret = ioctl(disk_fd, IOC_PR_REGISTER, &pr_reg);
if (ret != 0)
panic("failed to register (%d): %s\n", ret, strerror(errno));
struct pr_preempt pr_pre = {
.old_key = magic,
.new_key = magic,
.type = PR_WRITE_EXCLUSIVE,
};
ret = ioctl(disk_fd, IOC_PR_PREEMPT, &pr_pre);
if (ret != 0)
panic("failed to preempt (%d): %s\n", ret, strerror(errno));
// register again in case we preempted ourselves
ret = ioctl(disk_fd, IOC_PR_REGISTER, &pr_reg);
if (ret != 0)
panic("failed to register (%d): %s\n", ret, strerror(errno));
fprintf(stderr, "Register as key %lx\n", magic);
struct pr_reservation pr_rev = {
.key = magic,
.type = PR_WRITE_EXCLUSIVE,
};
ret = ioctl(disk_fd, IOC_PR_RESERVE, &pr_rev);
if (ret != 0)
panic("failed to reserve (%d): %s\n", ret, strerror(errno));
lease->magic = magic;
gethostname(lease->holder, sizeof(lease->holder));
while (!shutdown) {
clock_gettime(CLOCK_MONOTONIC, &lease->acquire_time);
ret = pwrite(disk_fd, lease, lease_alloc_size, 0);
if (ret < 0)
panic("failed to write lease: %s\n", strerror(errno));
fprintf(stderr, "Refreshed lease\n");
sleep(5);
}
}
int timespec_compare(const struct timespec *a, const struct timespec *b) {
if (a->tv_sec < b->tv_sec)
return -1;
if (a->tv_sec > b->tv_sec)
return 1;
if (a->tv_nsec < b->tv_nsec)
return -1;
if (a->tv_nsec > b->tv_nsec)
return 1;
return 0;
}
int main() {
assert(lease_alloc_size >= sizeof(struct lease));
lease = aligned_alloc(512, lease_alloc_size);
if (lease == NULL)
panic("failed to allocate memory\n");
// char *reg_key_str = getenv("REG_KEY");
// if (reg_key_str == NULL)
// panic("REG_KEY env not specified");
// uint64_t reg_key = atoll(reg_key_str) | (magic << 32);
// fprintf(stderr, "Will register as key %lx", reg_key);
int disk_fd = open(disk_device, O_RDWR|O_DIRECT);
if (disk_fd < 0)
panic("failed to open disk: %s\n", strerror(errno));
// setup signal handler
struct sigaction sa = {
.sa_handler = on_term,
};
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
struct timespec last_active_local;
struct timespec last_active_remote;
int ret = pread(disk_fd, lease, lease_alloc_size, 0);
if (ret < 0)
panic("failed to read lease: %s\n", strerror(errno));
if (lease->magic != magic) {
// new disk, no lease
acquire_lease(disk_fd);
} else {
// someone else has the lease
while (!shutdown) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (timespec_compare(&lease->acquire_time, &last_active_remote)) {
fprintf(stderr, "Remote %s refreshed lease\n", lease->holder);
last_active_remote = lease->acquire_time;
last_active_local = now;
} else if (now.tv_sec - last_active_local.tv_sec > 20) {
// remote is dead
fprintf(stderr, "Remote is dead, preempting\n");
acquire_lease(disk_fd);
break;
}
sleep(5);
int ret = pread(disk_fd, lease, lease_alloc_size, 0);
if (ret < 0)
panic("failed to read lease: %s\n", strerror(errno));
}
}
close(disk_fd);
}
#!/bin/bash
set -e
DISK_DEVICE="/dev/data-disk"
MAGIC=0x4745D0C5CD9A2FA4
SHUTDOWN=0
trap "SHUTDOWN=1" SIGINT SIGTERM
function acquire_lease() {
# racqa:
# 0: aquire
# 1: preempt
# rtype:
# 1: write exclusive
nvme resv-register $DISK_DEVICE --iekey --nrkey=$MAGIC
nvme resv-acquire $DISK_DEVICE --racqa=1 --rtype=1 --prkey=$MAGIC --crkey=$MAGIC
# register again in case we preempted ourselves
nvme resv-register $DISK_DEVICE --iekey --nrkey=$MAGIC
nvme resv-acquire $DISK_DEVICE --racqa=0 --rtype=1 --prkey=$MAGIC --crkey=$MAGIC
while [[ $SHUTDOWN -eq 0 ]]; do
echo "$MAGIC $(date +%s) $HOSTNAME" | dd of=$DISK_DEVICE bs=512 count=1 oflag=direct status=none
echo "Refreshed lease"
sleep 5
done
}
LEASE=$(dd if=$DISK_DEVICE bs=512 count=1 iflag=direct status=none)
if [[ $LEASE != $MAGIC* ]]; then
# new disk, no lease
acquire_lease
else
last_active_remote=-1
last_active_local=-1
while [[ $SHUTDOWN -eq 0 ]]; do
now=$(date +%s)
read -r magic timestamp holder < <(echo $LEASE)
if [ "$last_active_remote" != "$timestamp" ]; then
echo "Remote $holder refreshed the lease"
last_active_remote=$timestamp
last_active_local=$now
elif (($now - $last_active_local > 10)); then
echo "Remote is dead, preempting"
acquire_lease
break
fi
sleep 5
LEASE=$(dd if=$DISK_DEVICE bs=512 count=1 iflag=direct status=none)
done
fi
下文部署所用YAML文件僅適用于C語(yǔ)言版本,Bash版本部署時(shí)需要在YAML中為容器授權(quán):
securityContext:
capabilities:
add: ["SYS_ADMIN"]
C語(yǔ)言版本的Dockerfile:
# syntax=docker/dockerfile:1.4
FROM buildpack-deps:bookworm as builder
COPY lease.c /usr/src/nvme-resv/
RUN gcc -o /lease -O2 -Wall /usr/src/nvme-resv/lease.c
FROM debian:bookworm-slim
COPY --from=builder --link /lease /usr/local/bin/lease
ENTRYPOINT ["/usr/local/bin/lease"]
Bash版本的Dockerfile:
# syntax=docker/dockerfile:1.4
FROM debian:bookworm-slim
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
rm -f /etc/apt/apt.conf.d/docker-clean && \
apt-get update && \
apt-get install -y nvme-cli
COPY --link lease.sh /usr/local/bin/lease
ENTRYPOINT ["/usr/local/bin/lease"]
步驟一:部署應(yīng)用并配置多重掛載
創(chuàng)建名為alicloud-disk-shared的StorageClass,并開(kāi)啟云盤(pán)的多重掛載功能。
創(chuàng)建名為data-disk的PVC,并設(shè)置accessModes
為ReadWriteMany
;volumeMode
為Block
。
創(chuàng)建名為lease-test的StatefulSet應(yīng)用,使用本文應(yīng)用示例的鏡像。
使用以下內(nèi)容,創(chuàng)建lease.yaml文件。
請(qǐng)將以下YAML中容器鏡像地址替換為您實(shí)際應(yīng)用的鏡像地址。
重要由于NVMe Reservation在節(jié)點(diǎn)維度生效,同一節(jié)點(diǎn)上的多個(gè)Pod可能會(huì)互相干擾,所以本示例中通過(guò)
podAntiAffinity
以避免多個(gè)Pod調(diào)度到同一個(gè)節(jié)點(diǎn)上。如果您的集群中包括其他不使用NVMe協(xié)議的節(jié)點(diǎn),您需要自行設(shè)置親和性,以確保將Pod調(diào)度到使用NVMe協(xié)議的節(jié)點(diǎn)上。
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: alicloud-disk-shared parameters: type: cloud_essd multiAttach: "true" provisioner: diskplugin.csi.alibabacloud.com reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: data-disk spec: accessModes: [ "ReadWriteMany" ] storageClassName: alicloud-disk-shared volumeMode: Block resources: requests: storage: 20Gi --- apiVersion: apps/v1 kind: StatefulSet metadata: name: lease-test spec: replicas: 2 serviceName: lease-test selector: matchLabels: app: lease-test template: metadata: labels: app: lease-test spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - lease-test topologyKey: "kubernetes.io/hostname" containers: - name: lease image: <IMAGE OF APP> # 替換為您應(yīng)用的鏡像地址。 volumeDevices: - name: data-disk devicePath: /dev/data-disk volumes: - name: data-disk persistentVolumeClaim: claimName: data-disk
參數(shù)
使用多重掛載功能配置說(shuō)明
普通掛載配置說(shuō)明
StorageClass
parameters.multiAttach
設(shè)置為true,以開(kāi)啟云盤(pán)的多重掛載功能。
無(wú)需配置
PVC
accessModes
ReadWriteMany
ReadWriteOnce
volumeMode
Block
Filesystem
存儲(chǔ)卷掛載方式
volumeDevices:直接通過(guò)塊設(shè)備訪問(wèn)云盤(pán)中的數(shù)據(jù)。
volumeMounts:主要用于掛載文件系統(tǒng)類型的Volume。
執(zhí)行以下命令,部署應(yīng)用。
kubectl apply -f lease.yaml
步驟二:驗(yàn)證多重掛載及Reservation效果
為了確保NVMe云盤(pán)的數(shù)據(jù)一致性,您可以在應(yīng)用中通過(guò)Reservation控制讀寫(xiě)權(quán)限,如果一個(gè)Pod進(jìn)行寫(xiě)操作,其他Pod就只能進(jìn)行讀操作。
多個(gè)節(jié)點(diǎn)可讀寫(xiě)同一個(gè)云盤(pán)
執(zhí)行以下命令,查看Pod日志。
kubectl logs -l app=lease-test --prefix -f
預(yù)期輸出:
[pod/lease-test-0/lease] Register as key 4745d0c5cd9a2fa4
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-1/lease] Remote lease-test-0 refreshed lease
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-1/lease] Remote lease-test-0 refreshed lease
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-1/lease] Remote lease-test-0 refreshed lease
[pod/lease-test-0/lease] Refreshed lease
[pod/lease-test-1/lease] Remote lease-test-0 refreshed lease
預(yù)期輸出表明,Pod lease-test-1可以即時(shí)讀取到Pod lease-test-0的寫(xiě)入的內(nèi)容。
NVMe Reservation創(chuàng)建成功
執(zhí)行以下命令,獲取云盤(pán)ID。
kubectl get pvc data-disk -ojsonpath='{.spec.volumeName}'
登錄兩個(gè)節(jié)點(diǎn)中的任意一個(gè)節(jié)點(diǎn),執(zhí)行以下命令確認(rèn)NVMe Reservation是否創(chuàng)建成功。
請(qǐng)?zhí)鎿Q以下代碼中
2zxxxxxxxxxxx
為您上一步獲取到的云盤(pán)ID中d-
之后的內(nèi)容。nvme resv-report -c 1 /dev/disk/by-id/nvme-Alibaba_Cloud_Elastic_Block_Storage_2zxxxxxxxxxxx
預(yù)期輸出:
NVME Reservation status: gen : 3 rtype : 1 regctl : 1 ptpls : 1 regctlext[0] : cntlid : ffff rcsts : 1 rkey : 4745d0c5cd9a2fa4 hostid : 4297c540000daf4a4*****
預(yù)期輸出表明,NVMe Reservation已創(chuàng)建成功。
通過(guò)Reservation可阻斷異常節(jié)點(diǎn)的寫(xiě)入IO
登錄Pod lease-test-0所在的節(jié)點(diǎn)上執(zhí)行以下命令,暫停該進(jìn)程用于模擬故障場(chǎng)景。
pkill -STOP -f /usr/local/bin/lease
等待30秒后,執(zhí)行以下命令,再次查看日志。
kubectl logs -l app=lease-test --prefix -f
預(yù)期輸出:
[pod/lease-test-1/lease] Remote lease-test-0 refreshed lease [pod/lease-test-1/lease] Remote is dead, preempting [pod/lease-test-1/lease] Register as key 4745d0c5cd9a2fa4 [pod/lease-test-1/lease] Refreshed lease [pod/lease-test-1/lease] Refreshed lease [pod/lease-test-1/lease] Refreshed lease
預(yù)期輸出表明,此時(shí)Pod lease-test-1已接管,持有租約成為服務(wù)的主節(jié)點(diǎn)。
再次登錄Pod lease-test-0所在的節(jié)點(diǎn)上執(zhí)行以下命令,恢復(fù)之前暫停的進(jìn)程。
pkill -CONT -f /usr/local/bin/lease
執(zhí)行以下命令,再次查看日志。
kubectl logs -l app=lease-test --prefix -f
預(yù)期輸出:
[pod/lease-test-0/lease] failed to write lease: Invalid exchange
預(yù)期輸出表明,Pod lease-test-0將無(wú)法再寫(xiě)入該云盤(pán),容器lease自動(dòng)重啟。說(shuō)明其寫(xiě)入IO的操作已成功被Reservation阻斷。
相關(guān)文檔
如果您的NVMe云盤(pán)空間不滿足要求或磁盤(pán)已滿,請(qǐng)參見(jiàn)擴(kuò)容云盤(pán)存儲(chǔ)卷。