You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by jp...@apache.org on 2018/09/28 00:00:42 UTC
[mesos] 02/04: Added a test for XFS persistent volume isolation.
This is an automated email from the ASF dual-hosted git repository.
jpeach pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mesos.git
commit 16f0aebaaee80e43039f7893fc98c08843260c4e
Author: James Peach <jp...@apache.org>
AuthorDate: Thu Sep 27 16:18:01 2018 -0700
Added a test for XFS persistent volume isolation.
Added a test to ensure that the `disk/xfs` isolator can isolate
and manage project IDs on a persistent volume.
Review: https://reviews.apache.org/r/68402/
---
src/tests/containerizer/xfs_quota_tests.cpp | 329 +++++++++++++++++++++++++++-
1 file changed, 328 insertions(+), 1 deletion(-)
diff --git a/src/tests/containerizer/xfs_quota_tests.cpp b/src/tests/containerizer/xfs_quota_tests.cpp
index 4043154..4e6ba95 100644
--- a/src/tests/containerizer/xfs_quota_tests.cpp
+++ b/src/tests/containerizer/xfs_quota_tests.cpp
@@ -21,6 +21,8 @@
#include <gmock/gmock.h>
+#include <google/protobuf/util/message_differencer.h>
+
#include <mesos/mesos.hpp>
#include <mesos/resources.hpp>
@@ -96,6 +98,8 @@ public:
const Option<std::string>& _mkfsOptions = None())
: mountOptions(_mountOptions), mkfsOptions(_mkfsOptions) {}
+ static constexpr int DISK_SIZE_MB = 40;
+
virtual void SetUp()
{
MesosTest::SetUp();
@@ -107,7 +111,7 @@ public:
string mntPath = path::join(base.get(), "mnt");
ASSERT_SOME(os::mkdir(mntPath));
- ASSERT_SOME(mkfile(devPath, Megabytes(40)));
+ ASSERT_SOME(mkfile(devPath, Megabytes(DISK_SIZE_MB)));
// Get an unused loop device.
Try<string> loop = mkloop();
@@ -250,6 +254,9 @@ public:
};
+constexpr int ROOT_XFS_TestBase::DISK_SIZE_MB;
+
+
// ROOT_XFS_QuotaTest is our standard fixture that sets up a
// XFS filesystem on loopback with project quotas enabled.
class ROOT_XFS_QuotaTest : public ROOT_XFS_TestBase
@@ -498,6 +505,326 @@ TEST_F(ROOT_XFS_QuotaTest, DiskUsageExceedsQuota)
}
+// Verify that a task may not exceed quota on a persistent volume. We run
+// a task that writes to a persistent volume and verify that it fails with
+// an I/O error. After destroying the volume, make sure that the project
+// ID metrics are updated consistently.
+TEST_F(ROOT_XFS_QuotaTest, VolumeUsageExceedsQuota)
+{
+ FrameworkInfo frameworkInfo = DEFAULT_FRAMEWORK_INFO;
+ frameworkInfo.set_roles(0, DEFAULT_TEST_ROLE);
+
+ Try<Owned<cluster::Master>> master = StartMaster();
+ ASSERT_SOME(master);
+
+ slave::Flags flags = CreateSlaveFlags();
+ flags.gc_delay = Seconds(10);
+ flags.disk_watch_interval = Seconds(10);
+ flags.resources = strings::format(
+ "disk(%s):%d", DEFAULT_TEST_ROLE, DISK_SIZE_MB).get();
+
+ Owned<MasterDetector> detector = master.get()->createDetector();
+ Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
+ ASSERT_SOME(slave);
+
+ MockScheduler sched;
+ MesosSchedulerDriver driver(
+ &sched, frameworkInfo, master.get()->pid, DEFAULT_CREDENTIAL);
+
+ EXPECT_CALL(sched, registered(&driver, _, _));
+
+ Future<vector<Offer>> offers;
+ EXPECT_CALL(sched, resourceOffers(&driver, _))
+ .WillOnce(FutureArg<1>(&offers))
+ .WillRepeatedly(DeclineOffers()); // Ignore subsequent offers.
+
+ driver.start();
+
+ AWAIT_READY(offers);
+ ASSERT_FALSE(offers->empty());
+
+ // Create a task that requests a 1 MB persistent volume but attempts
+ // to use 2MB.
+ Resources volume = createPersistentVolume(
+ Megabytes(1),
+ DEFAULT_TEST_ROLE,
+ "id1",
+ "volume_path",
+ None(),
+ None(),
+ frameworkInfo.principal());
+
+ // We intentionally request a sandbox that is much bigger (16MB) than
+ // the file the task writes (2MB) to the persistent volume (1MB). This
+ // makes sure that the quota is indeed enforced on the persistent volume.
+ Resources taskResources = Resources::parse(strings::format(
+ "cpus:1;mem:32;disk(%s):16", DEFAULT_TEST_ROLE).get()).get() + volume;
+
+ TaskInfo task = createTask(
+ offers->at(0).slave_id(),
+ taskResources,
+ "dd if=/dev/zero of=volume_path/file bs=1048576 count=2 && sleep 1000");
+
+ Future<TaskStatus> startingStatus;
+ Future<TaskStatus> runningStatus;
+ Future<TaskStatus> failedStatus;
+ EXPECT_CALL(sched, statusUpdate(&driver, _))
+ .WillOnce(FutureArg<1>(&startingStatus))
+ .WillOnce(FutureArg<1>(&runningStatus))
+ .WillOnce(FutureArg<1>(&failedStatus));
+
+ Future<Nothing> terminateExecutor =
+ FUTURE_DISPATCH(_, &Slave::executorTerminated);
+
+ Future<Nothing> gc =
+ FUTURE_DISPATCH(_, &GarbageCollectorProcess::prune);
+
+ // Create the volume and launch the task.
+ driver.acceptOffers(
+ {offers->at(0).id()}, {CREATE(volume), LAUNCH({task})});
+
+ AWAIT_READY(startingStatus);
+ EXPECT_EQ(task.task_id(), startingStatus->task_id());
+ EXPECT_EQ(TASK_STARTING, startingStatus->state());
+
+ AWAIT_READY(runningStatus);
+ EXPECT_EQ(task.task_id(), runningStatus->task_id());
+ EXPECT_EQ(TASK_RUNNING, runningStatus->state());
+
+ AWAIT_READY(failedStatus);
+ EXPECT_EQ(task.task_id(), failedStatus->task_id());
+ EXPECT_EQ(TASK_FAILED, failedStatus->state());
+
+ // Unlike the 'disk/du' isolator, the reason for task failure
+ // should be that dd got an IO error.
+ EXPECT_EQ(TaskStatus::SOURCE_EXECUTOR, failedStatus->source());
+ EXPECT_EQ("Command exited with status 1", failedStatus->message());
+
+ AWAIT_READY(terminateExecutor);
+
+ // Wait for new offers for the DESTROY.
+ EXPECT_CALL(sched, resourceOffers(&driver, _))
+ .WillOnce(FutureArg<1>(&offers))
+ .WillRepeatedly(Return()); // Ignore subsequent offers.
+
+ Future<ApplyOperationMessage> apply =
+ FUTURE_PROTOBUF(ApplyOperationMessage(), _, slave.get()->pid);
+
+ AWAIT_READY(offers);
+ ASSERT_FALSE(offers->empty());
+
+ // Destroy the volume.
+ driver.acceptOffers(
+ {offers->at(0).id()}, {DESTROY(volume)});
+
+ AWAIT_READY(apply);
+
+ // Advance the clock to trigger sandbox GC.
+ Clock::pause();
+ Clock::advance(flags.gc_delay);
+ Clock::settle();
+ Clock::resume();
+
+ AWAIT_READY(gc);
+
+ // Advance the clock to trigger the project ID usage check.
+ Clock::pause();
+ Clock::advance(flags.disk_watch_interval);
+ Clock::settle();
+ Clock::resume();
+
+ // We should have reclaimed the project IDs for both the sandbox and the
+ // persistent volume.
+ JSON::Object metrics = Metrics();
+ EXPECT_EQ(
+ metrics.at<JSON::Number>("containerizer/mesos/disk/project_ids_total")
+ ->as<int>(),
+ metrics.at<JSON::Number>("containerizer/mesos/disk/project_ids_free")
+ ->as<int>());
+
+ driver.stop();
+ driver.join();
+}
+
+
+// Verify that a task may not exceed quota on a persistent volume. We run a
+// task that writes to a persistent volume and verify that the containerizer
+// kills it when it exceeds the quota. After destroying the volume, make
+// sure that the project ID metrics are updated consistently.
+TEST_F(ROOT_XFS_QuotaTest, VolumeUsageExceedsQuotaWithKill)
+{
+ FrameworkInfo frameworkInfo = DEFAULT_FRAMEWORK_INFO;
+ frameworkInfo.set_roles(0, DEFAULT_TEST_ROLE);
+
+ Try<Owned<cluster::Master>> master = StartMaster();
+ ASSERT_SOME(master);
+
+ slave::Flags flags = CreateSlaveFlags();
+ flags.gc_delay = Seconds(10);
+ flags.disk_watch_interval = Seconds(10);
+ flags.resources = strings::format(
+ "disk(%s):%d", DEFAULT_TEST_ROLE, DISK_SIZE_MB).get();
+
+ // Enable killing containers on disk quota violations.
+ flags.xfs_kill_containers = true;
+
+ // Tune the watch interval down so that the isolator will detect
+ // the quota violation as soon as possible.
+ flags.container_disk_watch_interval = Milliseconds(1);
+
+ Owned<MasterDetector> detector = master.get()->createDetector();
+ Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), flags);
+ ASSERT_SOME(slave);
+
+ MockScheduler sched;
+ MesosSchedulerDriver driver(
+ &sched, frameworkInfo, master.get()->pid, DEFAULT_CREDENTIAL);
+
+ EXPECT_CALL(sched, registered(&driver, _, _));
+
+ Future<vector<Offer>> offers;
+ EXPECT_CALL(sched, resourceOffers(&driver, _))
+ .WillOnce(FutureArg<1>(&offers))
+ .WillRepeatedly(DeclineOffers()); // Ignore subsequent offers.
+
+ driver.start();
+
+ AWAIT_READY(offers);
+ ASSERT_FALSE(offers->empty());
+
+ // We create 2 persistent volumes, but only write to one so that we can
+ // test that the container limitation reports the correct volume in the
+ // task resource limitation.
+ Resources volume1 = createPersistentVolume(
+ Megabytes(1),
+ DEFAULT_TEST_ROLE,
+ "id1",
+ "volume_path_1",
+ None(),
+ None(),
+ frameworkInfo.principal());
+
+ Resources volume2 = createPersistentVolume(
+ Megabytes(1),
+ DEFAULT_TEST_ROLE,
+ "id2",
+ "volume_path_2",
+ None(),
+ None(),
+ frameworkInfo.principal());
+
+ // We intentionally request a sandbox that is much bigger (16MB) than
+ // the file the task writes (2MB) to the persistent volume (1MB). This
+ // makes sure that the quota is indeed enforced on the persistent volume.
+ Resources taskResources = Resources::parse(strings::format(
+ "cpus:1;mem:32;disk(%s):16", DEFAULT_TEST_ROLE).get()).get();
+
+ TaskInfo task = createTask(
+ offers->at(0).slave_id(),
+ taskResources + volume1 + volume2,
+ "dd if=/dev/zero of=volume_path_1/file bs=1048576 count=2 && "
+ "sleep 100000");
+
+ Future<TaskStatus> startingStatus;
+ Future<TaskStatus> runningStatus;
+ Future<TaskStatus> killedStatus;
+ EXPECT_CALL(sched, statusUpdate(&driver, _))
+ .WillOnce(FutureArg<1>(&startingStatus))
+ .WillOnce(FutureArg<1>(&runningStatus))
+ .WillOnce(FutureArg<1>(&killedStatus));
+
+ Future<Nothing> terminateExecutor =
+ FUTURE_DISPATCH(_, &Slave::executorTerminated);
+
+ Future<Nothing> gc =
+ FUTURE_DISPATCH(_, &GarbageCollectorProcess::prune);
+
+ // Create the volume and launch the task.
+ driver.acceptOffers(
+ {offers->at(0).id()},
+ {CREATE(volume1), CREATE(volume2), LAUNCH({task})});
+
+ AWAIT_READY(startingStatus);
+ EXPECT_EQ(task.task_id(), startingStatus->task_id());
+ EXPECT_EQ(TASK_STARTING, startingStatus->state());
+
+ AWAIT_READY(runningStatus);
+ EXPECT_EQ(task.task_id(), runningStatus->task_id());
+ EXPECT_EQ(TASK_RUNNING, runningStatus->state());
+
+ AWAIT_READY(killedStatus);
+ EXPECT_EQ(task.task_id(), killedStatus->task_id());
+ EXPECT_EQ(TASK_FAILED, killedStatus->state());
+
+ EXPECT_EQ(TaskStatus::SOURCE_SLAVE, killedStatus->source());
+ EXPECT_EQ(
+ TaskStatus::REASON_CONTAINER_LIMITATION_DISK, killedStatus->reason());
+
+ // Verify that the `TASK_KILLED` status includes the correct persistent
+ // volume disk resource in its limitation.
+ ASSERT_TRUE(killedStatus->has_limitation())
+ << JSON::protobuf(killedStatus.get());
+
+ ASSERT_EQ(1, killedStatus->limitation().resources().size())
+ << JSON::protobuf(killedStatus.get());
+
+ ASSERT_TRUE(killedStatus->limitation().resources().Get(0).has_disk())
+ << JSON::protobuf(killedStatus.get());
+
+ EXPECT_TRUE(google::protobuf::util::MessageDifferencer::Equals(
+ killedStatus->limitation().resources().Get(0).disk(),
+ Resource(*volume1.begin()).disk()))
+ << "Limitation contained disk "
+ << killedStatus->limitation().resources().Get(0).disk().DebugString()
+ << ", wanted disk " << Resource(*volume1.begin()).disk().DebugString();
+
+ AWAIT_READY(terminateExecutor);
+
+ // Wait for new offers for the DESTROY.
+ EXPECT_CALL(sched, resourceOffers(&driver, _))
+ .WillOnce(FutureArg<1>(&offers))
+ .WillRepeatedly(Return()); // Ignore subsequent offers.
+
+ Future<ApplyOperationMessage> apply =
+ FUTURE_PROTOBUF(ApplyOperationMessage(), _, slave.get()->pid);
+
+ AWAIT_READY(offers);
+ ASSERT_FALSE(offers->empty());
+
+ // Destroy the volumes.
+ driver.acceptOffers(
+ {offers->at(0).id()}, {DESTROY(volume1), DESTROY(volume2)});
+
+ AWAIT_READY(apply);
+
+ // Advance the clock to trigger sandbox GC.
+ Clock::pause();
+ Clock::advance(flags.gc_delay);
+ Clock::settle();
+ Clock::resume();
+
+ AWAIT_READY(gc);
+
+ // Advance the clock to trigger the project ID usage check.
+ Clock::pause();
+ Clock::advance(flags.disk_watch_interval);
+ Clock::settle();
+ Clock::resume();
+
+ // We should have reclaimed the project IDs for both the sandbox and the
+ // persistent volumes.
+ JSON::Object metrics = Metrics();
+ EXPECT_EQ(
+ metrics.at<JSON::Number>("containerizer/mesos/disk/project_ids_total")
+ ->as<int>(),
+ metrics.at<JSON::Number>("containerizer/mesos/disk/project_ids_free")
+ ->as<int>());
+
+ driver.stop();
+ driver.join();
+}
+
+
// This is the same logic as DiskUsageExceedsQuota except we turn off disk quota
// enforcement, so exceeding the quota should be allowed.
TEST_F(ROOT_XFS_QuotaTest, DiskUsageExceedsQuotaNoEnforce)