AWS provides four different storage options for your Kubernetes cluster: EBS, EFS, FSx for Lustre, and Amazon File Cache. Each of these CSI drivers has different performance characteristics, depending on your workload. This post quantifies those performance differences using the flexible I/O tester FIO.

Note: For an overview of the different CSI options available on AWS, see Picking the right AWS CSI driver for your Kubernetes application.

Before we start, please note that these results come with multiple caveats, and you should absolutely test performance on your own workloads before making any final decisions. For example, a workload with many random reads is typically latency sensitive and would benefit from higher IOPS, whereas a workload consisting of mostly streaming reads is typically throughput sensitive and would benefit from higher bandwidth and at large I/O sizes with relatively lower IOPS.

With that said, let’s get on with the test!

Cluster Setup

To run this test, I created an EKS cluster using the eksctl tool. Make sure to setup IAM roles for service accounts. This capability sets up an admission controller in EKS that injects AWS session credentials into Pods so that they can access AWS services. I used this to grant access to the correct AWS services to the CSI drivers during the test.

After provisioning an EKS cluster, I installed each of the CSI drivers according to the documentation provided. Mostly this involved creating the correct service accounts and installing the driver using either a Helm chart or Kubernetes manifest.

Because AWS File Cache uses Lustre under the hood, I compared the following drivers.

Test Setup

I set a storage class for each CSI driver to specify the storage type to provision for the test.

EBS Storage Class

I created two EBS storage classes. One for provisioning io2 volumes and another for provisioning gp3 volumes. Both storage classes were configured to use 50 IOPS per GB of storage.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: aws-csi-driver-benchmark-ebs-io2-sc  # can change io2 to gp3
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: io2  # can change io2 to gp3
  iopsPerGB: "50"
  encrypted: "true"

EFS Storage Class

For EFS, I manually created an Elastic File System and recorded the file system identifier.

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: aws-csi-driver-benchmark-efs-sc
provisioner: efs.csi.aws.com
parameters:
  provisioningMode: efs-ap
  fileSystemId: <your-file-system-id-here>
  directoryPerms: "700"
  basePath: "/dynamic_provisioner" # optional
  subPathPattern: "${.PVC.namespace}/${.PVC.name}" # optional

FSx Volume Claim

Lastly, I used the SCRATCH_1 an SSD storage type for Lustre.

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: aws-csi-driver-benchmark-fsx-sc
provisioner: fsx.csi.aws.com
parameters:
  subnetId: <lustre-subnet-id>
  securityGroupIds: <lustre-security-groups>
  deploymentType: SCRATCH_1
  storageType: SSD

Then, I configured a Kubernetes Job to mount a PersistentVolumeClaim, using the different storage classes specifications.

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: aws-csi-driver-benchmark-pvc
spec:
  storageClassName: aws-csi-driver-benchmark-efs-sc
  # storageClassName: aws-csi-driver-benchmark-ebs-gp3-sc
  # storageClassName: aws-csi-driver-benchmark-ebs-io2-sc
  # storageClassName: aws-csi-driver-benchmark-fsx-sc
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 60Gi
---
apiVersion: batch/v1
kind: Job
metadata:
  name: aws-csi-driver-benchmark-job
spec:
  template:
    spec:
      containers:
      - name: aws-csi-driver-benchmark
        image: <aws-account>.dkr.ecr.<aws-region>.amazonaws.com/sookocheff/benchmark:main
        imagePullPolicy: Always
        env:
          - name: MOUNTPOINT
            value: /data
        volumeMounts:
        - name: aws-csi-driver-benchmark-pv
          mountPath: /data
      restartPolicy: Never
      volumes:
      - name: aws-csi-driver-benchmark-pv
        persistentVolumeClaim:
          claimName: aws-csi-driver-benchmark-pvc
  backoffLimit: 4

The Dockerfile to execute was adapted from the DBench project that executes the following script on Pod entry.

#!/usr/bin/env sh
set -e

if [ -z $MOUNTPOINT ]; then
    MOUNTPOINT=/tmp
fi

if [ -z $FIO_SIZE ]; then
    FIO_SIZE=2G
fi

if [ -z $FIO_OFFSET_INCREMENT ]; then
    FIO_OFFSET_INCREMENT=500M
fi

if [ -z $FIO_DIRECT ]; then
    FIO_DIRECT=0
fi

echo Working dir: $MOUNTPOINT
echo

echo Testing Read IOPS...
READ_IOPS=$(fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=$FIO_DIRECT --gtod_reduce=1 --name=read_iops --filename=$MOUNTPOINT/fiotest --bs=4K --iodepth=64 --size=$FIO_SIZE --readwrite=randread --time_based --ramp_time=2s --runtime=15s)
echo "$READ_IOPS"
READ_IOPS_VAL=$(echo "$READ_IOPS"|grep -E 'read ?:'|grep -Eoi 'IOPS=[0-9k.]+'|cut -d'=' -f2)
echo
echo

echo Testing Write IOPS...
WRITE_IOPS=$(fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=$FIO_DIRECT --gtod_reduce=1 --name=write_iops --filename=$MOUNTPOINT/fiotest --bs=4K --iodepth=64 --size=$FIO_SIZE --readwrite=randwrite --time_based --ramp_time=2s --runtime=15s)
echo "$WRITE_IOPS"
WRITE_IOPS_VAL=$(echo "$WRITE_IOPS"|grep -E 'write:'|grep -Eoi 'IOPS=[0-9k.]+'|cut -d'=' -f2)
echo
echo

echo Testing Read Bandwidth...
READ_BW=$(fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=$FIO_DIRECT --gtod_reduce=1 --name=read_bw --filename=$MOUNTPOINT/fiotest --bs=128K --iodepth=64 --size=$FIO_SIZE --readwrite=randread --time_based --ramp_time=2s --runtime=15s)
echo "$READ_BW"
READ_BW_VAL=$(echo "$READ_BW"|grep -E 'read ?:'|grep -Eoi 'BW=[0-9GMKiBs/.]+'|cut -d'=' -f2)
echo
echo

echo Testing Write Bandwidth...
WRITE_BW=$(fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=$FIO_DIRECT --gtod_reduce=1 --name=write_bw --filename=$MOUNTPOINT/fiotest --bs=128K --iodepth=64 --size=$FIO_SIZE --readwrite=randwrite --time_based --ramp_time=2s --runtime=15s)
echo "$WRITE_BW"
WRITE_BW_VAL=$(echo "$WRITE_BW"|grep -E 'write:'|grep -Eoi 'BW=[0-9GMKiBs/.]+'|cut -d'=' -f2)
echo
echo

echo Testing Read Latency...
READ_LATENCY=$(fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=$FIO_DIRECT --name=read_latency --filename=$MOUNTPOINT/fiotest --bs=4K --iodepth=4 --size=$FIO_SIZE --readwrite=randread --time_based --ramp_time=2s --runtime=15s)
echo "$READ_LATENCY"
READ_LATENCY_VAL=$(echo "$READ_LATENCY"|grep ' lat.*avg'|grep -Eoi 'avg=[0-9.]+'|cut -d'=' -f2)
echo
echo

echo Testing Write Latency...
WRITE_LATENCY=$(fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=$FIO_DIRECT --name=write_latency --filename=$MOUNTPOINT/fiotest --bs=4K --iodepth=4 --size=$FIO_SIZE --readwrite=randwrite --time_based --ramp_time=2s --runtime=15s)
echo "$WRITE_LATENCY"
WRITE_LATENCY_VAL=$(echo "$WRITE_LATENCY"|grep ' lat.*avg'|grep -Eoi 'avg=[0-9.]+'|cut -d'=' -f2)
echo
echo

echo Testing Read Sequential Speed...
READ_SEQ=$(fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=$FIO_DIRECT --gtod_reduce=1 --name=read_seq --filename=$MOUNTPOINT/fiotest --bs=1M --iodepth=16 --size=$FIO_SIZE --readwrite=read --time_based --ramp_time=2s --runtime=15s --thread --numjobs=4 --offset_increment=$FIO_OFFSET_INCREMENT)
echo "$READ_SEQ"
READ_SEQ_VAL=$(echo "$READ_SEQ"|grep -E 'READ:'|grep -Eoi '(aggrb|bw)=[0-9GMKiBs/.]+'|cut -d'=' -f2)
echo
echo

echo Testing Write Sequential Speed...
WRITE_SEQ=$(fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=$FIO_DIRECT --gtod_reduce=1 --name=write_seq --filename=$MOUNTPOINT/fiotest --bs=1M --iodepth=16 --size=$FIO_SIZE --readwrite=write --time_based --ramp_time=2s --runtime=15s --thread --numjobs=4 --offset_increment=$FIO_OFFSET_INCREMENT)
echo "$WRITE_SEQ"
WRITE_SEQ_VAL=$(echo "$WRITE_SEQ"|grep -E 'WRITE:'|grep -Eoi '(aggrb|bw)=[0-9GMKiBs/.]+'|cut -d'=' -f2)
echo
echo

echo Testing Read/Write Mixed...
RW_MIX=$(fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=$FIO_DIRECT --gtod_reduce=1 --name=rw_mix --filename=$MOUNTPOINT/fiotest --bs=4k --iodepth=64 --size=$FIO_SIZE --readwrite=randrw --rwmixread=75 --time_based --ramp_time=2s --runtime=15s)
echo "$RW_MIX"
RW_MIX_R_IOPS=$(echo "$RW_MIX"|grep -E 'read ?:'|grep -Eoi 'IOPS=[0-9k.]+'|cut -d'=' -f2)
RW_MIX_W_IOPS=$(echo "$RW_MIX"|grep -E 'write:'|grep -Eoi 'IOPS=[0-9k.]+'|cut -d'=' -f2)
echo
echo

echo All tests complete.
echo
echo ==================
echo = Benchmark Summary =
echo ==================
echo "Random Read/Write IOPS: $READ_IOPS_VAL/$WRITE_IOPS_VAL. BW: $READ_BW_VAL / $WRITE_BW_VAL"
echo "Average Latency (usec) Read/Write: $READ_LATENCY_VAL/$WRITE_LATENCY_VAL"
echo "Sequential Read/Write: $READ_SEQ_VAL / $WRITE_SEQ_VAL"
echo "Mixed Random Read/Write IOPS: $RW_MIX_R_IOPS/$RW_MIX_W_IOPS"

rm $MOUNTPOINT/fiotest
exit 0

Results

As expected, EBS volumes offer the highest performance, with io2 offering the best bandwidth, lowest latency, and most IOPS performance. More interesting is the different between AWS FSx for Lustre and Elastic File System (EFS). EFS and FSx have similar bandwidth. EFS has better IOPS performance, but worse overall latency than FSx.

CSI Driver Benchmarks

The results appear to show that, depending on your workload, you will get similar or better performance using EFS than using FSx for Lustre.

Random Read/Write IOPS

Random read IOPS was tested using the following configuration:

fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=read_iops --filename=$MOUNTPOINT/fiotest --bs=4K --iodepth=64 --size=$FIO_SIZE --readwrite=randread --time_based --ramp_time=2s --runtime=15s

Random write IOPS with the following:

fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=1  --gtod_reduce=1 --name=write_iops --filename=$MOUNTPOINT/fiotest --bs=4K --iodepth=64 --size=$FIO_SIZE --readwrite=randwrite --time_based --ramp_time=2s --runtime=15s

EBS consistently has the highest IOPS performance, followed by EFS and FSx.

DriverRandom ReadRandom Write
EBS gp31640016700
EBS io21960019600
EFS103005220
FSx for Lustre13171179
Random Read/Write IOPS

Mixed Random Read/Write IOPS

Mixed random read/write IOPS was measured with:

fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=rw_mix --filename=$MOUNTPOINT/fiotest --bs=4k --iodepth=64 --size=$FIO_SIZE --readwrite=randrw --rwmixread=75 --time_based --ramp_time=2s --runtime=15s
DriverMixed Random ReadMixed Random Write
EBS gp3124004175
EBS io2148004885
EFS54191785
FSx for Lustre951325
Mixed Random Read/Write IOPS

Random Read/Write Bandwidth

Random read/write bandwidth was tested using the following configuration:

fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=read_bw --filename=$MOUNTPOINT/fiotest --bs=128K --iodepth=64 --size=$FIO_SIZE --readwrite=randread --time_based --ramp_time=2s --runtime=15s
fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=write_bw --filename=$MOUNTPOINT/fiotest --bs=128K --iodepth=64 --size=$FIO_SIZE --readwrite=randwrite --time_based --ramp_time=2s --runtime=15s

EFS and EBS with gp3 volumes perform similarly, with EBS io2 volumes significantly faster.

DriverRandom Read (MiB/s)Random Write (MiB/s)
EBS gp3114106
EBS io2542501
EFS14499.3
FSx for Lustre81.81771.647
Random Read/Write Bandwidth

Sequential Read/Write Bandwidth

Sequential read was tested with the following configuration.

fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=read_seq --filename=$MOUNTPOINT/fiotest --bs=1M --iodepth=16 --size=$FIO_SIZE --readwrite=read --time_based --ramp_time=2s --runtime=15s --thread --numjobs=4 --offset_increment=$FIO_OFFSET_INCREMENT

And sequential write with:

fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=write_seq --filename=$MOUNTPOINT/fiotest --bs=1M --iodepth=16 --size=$FIO_SIZE --readwrite=write --time_based --ramp_time=2s --runtime=15s --thread --numjobs=4 --offset_increment=$FIO_OFFSET_INCREMENT

The results show that EBS io2 volumes have substantially higher bandwidth than other options.

DriverSequential Read (MiB/s)Sequential Write (MiB/s)
EBS gp3121115
EBS io2554523
EFS157101
FSx for Lustre30.91955.0742
Sequential Read/Write Bandwidth

Read/Write Latency

Read latency was tested with:

fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=1 --name=read_latency --filename=$MOUNTPOINT/fiotest --bs=4K --iodepth=4 --size=$FIO_SIZE --readwrite=randread --time_based --ramp_time=2s --runtime=15s

And write latency with:

fio --randrepeat=0 --verify=0 --ioengine=libaio --direct=1 --name=write_latency --filename=$MOUNTPOINT/fiotest --bs=4K --iodepth=4 --size=$FIO_SIZE --readwrite=randwrite --time_based --ramp_time=2s --runtime=15s

EBS has significantly lower latency both EFS and FSx, which makes sense because EFS and FSx both require larger network hops to access data.

DriverAverage Read Latency (usec)Average Write Latency (usec)
EBS gp3478.42680.48
EBS io2207.72261.23
EFS2736.4310118.31
FSx for Lustre2945.063412.69
Read/Write Latency