libvirt では、ジョブ メカニズムは、移行、スナップショット、保存、ホットプラグなど、仮想マシン ドメイン (ドメイン) 上の長期的な操作を処理および追跡するために使用されます。ジョブ メカニズムの主な目的は、同時に実行できる長時間実行オペレーションが 1 つだけであることを保証し、それによって競合状態や不整合の問題を回避することです。
1. libvirt ジョブの基本概念
libvirt のジョブ メカニズムには主に、ジョブ タイプ、ジョブ ロック、ジョブ ステータス、ジョブ イベント、およびジョブ制御の側面が含まれます。
ジョブ タイプ: libvirt は、さまざまな長期操作を区別するために複数のジョブ タイプを定義します。たとえば、VIR_DOMAIN_JOB_BOUNDED は時間制限のある操作を表し、VIR_DOMAIN_JOB_UNBOUNDED は無制限の時間制限のある操作を表し、VIR_DOMAIN_JOB_MIGRATION は仮想マシンの移行操作を表します。ジョブの種類が異なれば、優先順位や実行戦略も異なる場合があります。
ジョブ ロック: 同時に実行できる長期操作が 1 つだけであることを保証するために、libvirt は仮想マシン ドメインごとにジョブ ロックを提供します。クライアントが長期操作の実行を要求した場合、libvirtd は最初にドメインのジョブ ロックを取得する必要があります。ジョブ ロックにより、複数のクライアントが競合する操作を同時に実行することがなくなり、競合状態や不整合の問題が回避されます。
ジョブ ステータス: libvirt は、現在実行されている長期的な操作を追跡するために、各仮想マシン ドメインのジョブ ステータスを維持します。クライアントは、virDomainGetJobInfo 関数を呼び出すことによって、仮想マシン ドメインのジョブ ステータスをクエリできます。ジョブのステータスには、操作の種類、進行状況、残り時間などの情報が含まれており、クライアントが操作の実行状況を把握するのに役立ちます。
ジョブ イベント: libvirt は、イベント通知メカニズムを介したジョブ ステータス変更のレポートをサポートします。クライアントはイベント コールバック関数を登録して、ジョブの開始、終了、または失敗時に通知を受け取ることができます。これにより、クライアントはジョブの実行状況をリアルタイムに把握し、適切な対応を行うことができます。
ジョブ制御: libvirt は、長期的な操作の実行を制御するためのいくつかの API 関数を提供します。たとえば、クライアントは virDomainAbortJob 関数を呼び出して進行中の操作をキャンセルしたり、virDomainMigrateSetMaxDowntime 関数を呼び出して移行操作の最大ダウンタイムを設定したりできます。これらの制御は、クライアントが長時間実行される操作をより適切に管理するのに役立ちます。
2. libvirt ジョブのソース コード分析
libvirt では、qemuDomainObjBeginJob と qemuDomainObjEndJob がペアになって、長期運用の仮想マシン ドメイン ジョブ タスク キュー関係が確立されます。
1.qemuDomainJob タイプ
VM の破棄、VM の一時停止、VM ステータスの変更 (ホットプラグ)、タスクの終了、ジョブのネストなど。
typedef enum {
QEMU_JOB_NONE = 0, /* Always set to 0 for easy if (jobActive) conditions */
QEMU_JOB_QUERY, /* Doesn't change any state */
QEMU_JOB_DESTROY, /* Destroys the domain (cannot be masked out) */
QEMU_JOB_SUSPEND, /* Suspends (stops vCPUs) the domain */
QEMU_JOB_MODIFY, /* May change state */
QEMU_JOB_ABORT, /* Abort current async job */
QEMU_JOB_MIGRATION_OP, /* Operation influencing outgoing migration */
/* The following two items must always be the last items before JOB_LAST */
QEMU_JOB_ASYNC, /* Asynchronous job */
QEMU_JOB_ASYNC_NESTED, /* Normal job within an async job */
QEMU_JOB_LAST
} qemuDomainJob;
2.qemuDomainObjBeginJob –>qemuDomainObjBeginJobInternal の分析
int qemuDomainObjBeginJob(virQEMUDriverPtr driver,
virDomainObjPtr obj,
qemuDomainJob job)
{
if (qemuDomainObjBeginJobInternal(driver, obj, job,
QEMU_ASYNC_JOB_NONE) < 0)
return -1;
else
return 0;
}
/*
* obj must be locked before calling
*/
static int ATTRIBUTE_NONNULL(1)
qemuDomainObjBeginJobInternal(virQEMUDriverPtr driver,
virDomainObjPtr obj,
qemuDomainJob job,
qemuDomainAsyncJob asyncJob)
{
qemuDomainObjPrivatePtr priv = obj->privateData;
unsigned long long now;
unsigned long long then;
bool nested = job == QEMU_JOB_ASYNC_NESTED;
bool async = job == QEMU_JOB_ASYNC;
virQEMUDriverConfigPtr cfg = virQEMUDriverGetConfig(driver);
const char *blocker = NULL;
int ret = -1;
unsigned long long duration = 0;
unsigned long long asyncDuration = 0;
const char *jobStr;
/*首先会判断job是否异步job*/
if (async)
jobStr = qemuDomainAsyncJobTypeToString(asyncJob);
else
jobStr = qemuDomainJobTypeToString(job);
VIR_DEBUG("Starting %s: %s (vm=%p name=%s, current job=%s async=%s)",
async ? "async job" : "job", jobStr, obj, obj->def->name,
qemuDomainJobTypeToString(priv->job.active),
qemuDomainAsyncJobTypeToString(priv->job.asyncJob));
/*获取当前时间*/
if (virTimeMillisNow(&now) < 0) {
virObjectUnref(cfg);
return -1;
}
/*更新domain jobs_queued*/
priv->jobs_queued++;
/*设置job预期处理时间30s*/
/*Give up waiting for mutex after 30 seconds */
/*#define QEMU_JOB_WAIT_TIME (1000ull * 30)*/
then = now + QEMU_JOB_WAIT_TIME;
retry:
/*如果虚拟机设置的最多等待job个数,且当前等待超过最大值后,新插入job直接失败*/
if (cfg->maxQueuedJobs &&
priv->jobs_queued > cfg->maxQueuedJobs) {
goto error;
}
/*当新job不是QEMU_JOB_ASYNC_NESTED,且和其他异步job冲突时,新job需要等待完成*/
while (!nested && !qemuDomainNestedJobAllowed(priv, job)) {
VIR_DEBUG("Waiting for async job (vm=%p name=%s)", obj, obj->def->name);
if (virCondWaitUntil(&priv->job.asyncCond, &obj->parent.lock, then) < 0)
goto error;
}
/*如果当前有正在执行的非异步job,其他任何job都要等待,再次while循环是因为只有同步才会更新priv->job.active*/
while (priv->job.active) {
VIR_DEBUG("Waiting for job (vm=%p name=%s)", obj, obj->def->name);
if (virCondWaitUntil(&priv->job.cond, &obj->parent.lock, then) < 0)
goto error;
}
/* No job is active but a new async job could have been started while obj
* was unlocked, so we need to recheck it. */
/*检查是不是新的异步job已经提前进入队列*/
if (!nested && !qemuDomainNestedJobAllowed(priv, job))
goto retry;
/*重置同步job信息*/
qemuDomainObjResetJob(priv);
ignore_value(virTimeMillisNow(&now));
if (job != QEMU_JOB_ASYNC) {
/*处理非异步job*/
VIR_DEBUG("Started job: %s (async=%s vm=%p name=%s)",
qemuDomainJobTypeToString(job),
qemuDomainAsyncJobTypeToString(priv->job.asyncJob),
obj, obj->def->name);
priv->job.active = job;
/*获取当前线程id*/
priv->job.owner = virThreadSelfID();
/*获取当前线程执行的job*/
priv->job.ownerAPI = virThreadJobGet();
/*设置当前job执行的开始时间*/
priv->job.started = now;
} else {
VIR_DEBUG("Started async job: %s (vm=%p name=%s)",
qemuDomainAsyncJobTypeToString(asyncJob),
obj, obj->def->name);
/*重置异步job信息*/
qemuDomainObjResetAsyncJob(priv);
if (VIR_ALLOC(priv->job.current) < 0)
goto cleanup;
priv->job.asyncJob = asyncJob;
/*获取当前线程id*/
priv->job.asyncOwner = virThreadSelfID();
/*获取当前线程执行的job*/
priv->job.asyncOwnerAPI = virThreadJobGet();
/*设置异步job执行的开始时间*/
priv->job.asyncStarted = now;
priv->job.current->started = now;
}
if (qemuDomainTrackJob(job))
qemuDomainObjSaveJob(driver, obj);
virObjectUnref(cfg);
return 0;
error:
ignore_value(virTimeMillisNow(&now));
if (priv->job.active && priv->job.started)
duration = now - priv->job.started;
if (priv->job.asyncJob && priv->job.asyncStarted)
asyncDuration = now - priv->job.asyncStarted;
VIR_WARN("Cannot start job (%s, %s) for domain %s; "
"current job is (%s, %s) owned by (%llu %s, %llu %s) "
"for (%llus, %llus)",
qemuDomainJobTypeToString(job),
qemuDomainAsyncJobTypeToString(asyncJob),
obj->def->name,
qemuDomainJobTypeToString(priv->job.active),
qemuDomainAsyncJobTypeToString(priv->job.asyncJob),
priv->job.owner, NULLSTR(priv->job.ownerAPI),
priv->job.asyncOwner, NULLSTR(priv->job.asyncOwnerAPI),
duration / 1000, asyncDuration / 1000);
if (nested || qemuDomainNestedJobAllowed(priv, job))
blocker = priv->job.ownerAPI;
else
blocker = priv->job.asyncOwnerAPI;
ret = -1;
1./*error的处理,virCondWaitUntil等待超时以后,就会走向error,计算job占有lock的时常*/
if (errno == ETIMEDOUT) {
if (blocker) {
virReportError(VIR_ERR_OPERATION_TIMEOUT,
_("cannot acquire state change lock (held by %s)"),
blocker);
} else {
virReportError(VIR_ERR_OPERATION_TIMEOUT, "%s",
_("cannot acquire state change lock"));
}
ret = -2;
2./*当前等在job数大于设置的maxQueuedJobs*/
} else if (cfg->maxQueuedJobs &&
priv->jobs_queued > cfg->maxQueuedJobs) {
if (blocker) {
virReportError(VIR_ERR_OPERATION_FAILED,
_("cannot acquire state change lock (held by %s) "
"due to max_queued limit"),
blocker);
} else {
virReportError(VIR_ERR_OPERATION_FAILED, "%s",
_("cannot acquire state change lock "
"due to max_queued limit"));
}
ret = -2;
3./*其他异常场景*/
} else {
virReportSystemError(errno, "%s", _("cannot acquire job mutex"));
}
cleanup:
priv->jobs_queued--;
virObjectUnref(cfg);
return ret;
}
3.qemuDomainObjEndJob 分析
void
qemuDomainObjEndJob(virQEMUDriverPtr driver, virDomainObjPtr obj)
{
qemuDomainObjPrivatePtr priv = obj->privateData;
qemuDomainJob job = priv->job.active;
/*jobs计数器减一*/
priv->jobs_queued--;
VIR_DEBUG("Stopping job: %s (async=%s vm=%p name=%s)",
qemuDomainJobTypeToString(job),
qemuDomainAsyncJobTypeToString(priv->job.asyncJob),
obj, obj->def->name);
/*重置job信息*/
qemuDomainObjResetJob(priv);
if (qemuDomainTrackJob(job))
qemuDomainObjSaveJob(driver, obj);
/*发信号唤醒其他使用virCondWaitUntil等待的job*/
virCondSignal(&priv->job.cond);
}
libvirt job机制lock一种特殊类型的对象锁,用于保护虚拟机域(domain)的长时间操作,如迁移、快照、保存等。job锁确保了在同一时间只有一个长时间操作可以执行,从而避免了竞争条件和不一致性问题。job锁的作用范围介于全局锁和对象锁之间,针对特定的虚拟机域和长时间操作。
libvirt には、グローバル ロックとオブジェクト ロックの 2 種類のロックがあります;
(1) グローバル ロック (vm big ロック): グローバル ロックは、仮想マシン管理システム全体の状態を保護するために使用されます。クライアントが操作の実行を要求すると、libvirtd はまずグローバル ロックを取得する必要があります。グローバル ロックにより、一度に 1 つの操作のみが仮想マシン管理システムの状態を変更できるようになり、競合状態や不整合の問題が回避されます。グローバル ロックの範囲はより広く、仮想マシン管理システム全体がカバーされます。
(2) オブジェクト ロック: オブジェクト ロックは、特定の仮想マシン オブジェクト (ドメイン、ネットワーク、ストレージ プールなど) を保護するために使用されます。クライアントがオブジェクトに対する操作の実行を要求すると、libvirtd は最初にオブジェクトのロックを取得する必要があります。オブジェクト ロックにより、複数のクライアントが異なる仮想マシン オブジェクトを同時に操作できるようになるため、libvirt の同時実行パフォーマンスを向上させることができます。オブジェクト ロックのスコープは小さく、特定の仮想マシン オブジェクトのみをターゲットとします。
ジョブ ロック: ジョブ ロックは特別なタイプのオブジェクト ロックで、移行、スナップショット、保存、ホット スワップなど、仮想マシン ドメイン (ドメイン) の長期的な操作を保護するために使用されます。ジョブ ロックにより、一度に 1 つの長時間実行オペレーションのみを実行できるようになり、競合状態や不整合の問題が回避されます。ジョブ ロックの範囲はグローバル ロックとオブジェクト ロックの間にあり、特定の仮想マシン ドメインと長期操作を対象としています。
VM の同時操作シナリオでは、データの一貫性を確保するためにすべての API が大きな VM ロックを使用すると、一部の API (クエリ操作など) のエクスペリエンスに重大な影響を及ぼします。たとえば、一部のライフ サイクル操作は非常に時間がかかり、今回は、同時実行性によってクエリがスタックしてしまうため、クエリ API の仮想化へのアクセスに影響を与えることなく、ドメインの特定の主要な操作 (移行、スナップショット、保存、ホットプラグ) が正常に保証されることを期待して、libvirtd ジョブ メカニズムが導入されました。
3. 事例分析
1. ライブマイグレーション/ホットプラグ障害分析
2023-04-05 03:35:18.033+0800: 134423: warning : qemuDomainObjBeginJobInternal:3879 : Cannot start job (query, none) for domain 90ba372a-8d3d-416a-93bc-217051612e8d; current job is (query, none) owned by (134425 qemuDispatchDomainMonitorCommand, 0 <null>) for (147s, 0s)
2023-04-05 03:35:18.033+0800: 134423: error : qemuDomainObjBeginJobInternal:3891 : Timed out during operation: cannot acquire state change lock (held by qemuDispatchDomainMonitorCommand)
ライブ マイグレーションが失敗する理由は、移行ジョブが VM のジョブ ロックを取得できないことです。ロックは、QEMU_JOB_QUERY タイプのジョブ (qemuDispatchDomainMonitorCommand) によって保持されています。
Libvirtd ジョブのロックが解除されていない間は、VM のライフサイクルが管理され、移行やホットプラグ/ホットアンプラグの操作が正常に実行できなくなります。
既存の libvirtd ヘルスチェックは定期的 (120 秒) で virsh list の戻り値を通じて libvirtd の状態を確認し、正常に戻った場合は libvirtd が正常であることを示し、異常な場合は libvirtd を再起動します。
2. ライブネットワーク上でジョブロックが取得できない理由
ロック保持ジョブがハングし、後続の操作が失敗します。ディスクのホットプラグを例にとります。特定のホットプラグが失敗します。VS は libvirt API を呼び出し、再試行が繰り返されますが、ホットプラグが失敗する理由は次のとおりです。処理されません (VM は内部的にプラグイン イベントに応答しません。VM が応答しない理由は多数あり、特定の問題には詳細な分析が必要です)。問題は、ある日移行/切断/電源が実行されるまで発見されませんでした。 -on が失敗しました。
解決策:
(1) VM を再起動します; (2) ジョブがハングした理由を特定し、ジョブを通常どおり実行してジョブのロックを解除します。