libvirt 中,job 机制用于处理和跟踪针对虚拟机域(domain)的长时间操作,如迁移、快照、保存,热插拔等。job 机制的主要目的是确保在同一时间只有一个长时间操作可以执行,从而避免竞争条件和不一致性问题。
一、libvirt job基本概念
libvirt 的 job 机制主要包括以下几个方面:job 类型、job 锁、job 状态、job 事件和 job 控制。
job 类型:libvirt 定义了多种 job 类型,以区分不同的长时间操作。例如,VIR_DOMAIN_JOB_BOUNDED 表示有时间限制的操作,VIR_DOMAIN_JOB_UNBOUNDED 表示无时间限制的操作,VIR_DOMAIN_JOB_MIGRATION 表示虚拟机迁移操作等。不同类型的 job 可能具有不同的优先级和执行策略。
job 锁:为了确保在同一时间只有一个长时间操作可以执行,libvirt 为每个虚拟机域提供了一个 job 锁。当客户端请求执行某个长时间操作时,libvirtd 需要先获取该域的 job 锁。job 锁可以防止多个客户端同时执行相互冲突的操作,从而避免竞争条件和不一致性问题。
job 状态:libvirt 为每个虚拟机域维护了一个 job 状态,用于跟踪当前正在执行的长时间操作。客户端可以通过调用 virDomainGetJobInfo 函数来查询虚拟机域的 job 状态。job 状态包括了操作类型、进度、剩余时间等信息,有助于客户端了解操作的执行情况。
job 事件:libvirt 支持通过事件通知机制来报告 job 的状态变化。客户端可以注册事件回调函数,以便在 job 开始、结束或失败时收到通知。这样,客户端可以实时了解 job 的执行情况,以便采取相应的措施。
job 控制:libvirt 提供了一些 API 函数,用于控制长时间操作的执行。例如,客户端可以调用 virDomainAbortJob 函数来取消正在执行的操作,或调用 virDomainMigrateSetMaxDowntime 函数来设置迁移操作的最大停机时间。这些控制功能可以帮助客户端更好地管理长时间操作。
二、libvirt job源码分析
libvirt中通过qemuDomainObjBeginJob和qemuDomainObjEndJob配对使用,建立长时间操作虚拟机域job任务队列关系。
1.qemuDomainJob类型
destroy vm,挂起vm,修改vm状态(热插拔),终止任务,job嵌套等
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中有两种lock,全局锁和对象锁;
(1)全局锁(vm大锁):全局锁用于保护整个虚拟机管理系统的状态。当客户端请求执行某个操作时,libvirtd首先需要获取全局锁。全局锁确保了在同一时间只有一个操作可以修改虚拟机管理系统的状态,从而避免了竞争条件和不一致性问题。全局锁的作用范围较大,涵盖了整个虚拟机管理系统。
(2)对象锁:对象锁用于保护特定的虚拟机对象(如域、网络、存储池等)。当客户端请求执行针对某个对象的操作时,libvirtd需要先获取该对象的锁。对象锁可以提高libvirt的并发性能,因为它允许多个客户端同时操作不同的虚拟机对象。对象锁的作用范围较小,仅针对特定的虚拟机对象。
job锁:job锁是一种特殊类型的对象锁,用于保护虚拟机域(domain)的长时间操作,如迁移、快照、保存,热插拔等。job锁确保了在同一时间只有一个长时间操作可以执行,从而避免了竞争条件和不一致性问题。job锁的作用范围介于全局锁和对象锁之间,针对特定的虚拟机域和长时间操作。
vm并发操作场景下,如果所有的api都通过拿vm大锁来保证数据一致性,那会严重影响一些api(如查询类操作)的体验,比如有些生命周期操作很耗时,此时并发去查询就会卡住,故引入libvirtd job机制期望domain某些关键操作(迁移、快照、保存,热插拔)能得到成功保障,同时不影响查询类api对虚拟化的访问。
三、案例分析
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)
热迁移失败原因,迁移job拿不到vm的job锁,该锁被 QEMU_JOB_QUERY类型的job(qemuDispatchDomainMonitorCommand)持有。
Libvirtd job lock未释放期间,管理vm生命周期,migrate、hotplug/hotunplug操作均不能正常执行。
现有libvirtd健康check,定期(120s)通过virsh list返回值确保libvirtd状态,正常返回,libvirtd正常;异常返回,重启libvirtd。
2.现网出现获取不到job lock原因
持锁job hung住,引发后续操作失败,以磁盘热拔为例,某次插热拔失败,vs调用libvirt api一直发生重试,但是未处理热拔失败原因(vm内部未响应拔插事件,vm未响应的原因很多,具体问题需要具体分析),直到某天执行migrate/拔插拔/开关机失败才发现该问题。
处理方法:
(1)重启vm;(2)找到job hung住原因,让该job正常运行释放job lock;