Laravel 多租户应用中的单个共享队列工作者
Single shared queue worker in Laravel multi-tenant app
我正在构建一个多租户 Laravel 应用程序(在 Laravel 5.3 上),它允许每个租户针对任何受支持的 Laravel 设置拥有自己的一组配置。目前,这是通过使用我自己提供自定义配置加载器(覆盖默认值 Illuminate\Foundation\Bootstrap\LoadConfiguration
)的实现覆盖默认值 Laravel Application
来实现的。应用程序从 bootstrap 上的环境(PHP 的 $_ENV
或 .env
文件)检测当前租户,然后为检测到的租户加载适当的配置文件。
上述方法对 HTTP 和控制台内核都非常有效,其中每个内核 request/command 都有有限的生命周期,但我不确定如何处理队列工作程序。我希望所有租户都有一个队列工作人员,并且我已经实现了一个自定义队列连接器,以便在安排队列作业时添加额外的元数据,以便在工作人员收到租户时识别租户。
我寻求您帮助的部分是如何 运行 在隔离环境中对每个队列作业进行排序,我可以 bootstrap 使用我的自定义配置。
我看到的一些可能的解决方案是:
到 运行 自定义队列工作器 运行 作为守护进程并从队列中获取作业,但在单独的 PHP 中执行作业进程(通过 exec()
创建);作业执行后,工作人员收集结果(状态、异常等)并在父进程中完成作业(例如删除作业等)
与上面类似,但是运行作业在单独的PHP线程中,而不是使用RunKit Sandbox
[=的单独进程39=]
实施一个解决方案,在收到队列作业后"reboots" 应用程序(例如,重新加载当前租户的配置,重置任何已解决的依赖项等)
重要的是,我希望这个多租户作业的执行对作业本身是透明的,这样在多租户环境中没有设计为 运行 的作业(例如来自Laravel Scout 等第三方包无需任何修改即可处理。
关于如何解决这个问题有什么建议吗?
我们的情况差不多。这是我们的方法:
服务提供商
我们有一个名为 BootTenantServiceProvider
的 ServiceProvider,它在正常的 HTTP/Console 请求中引导租户。它期望存在一个名为 TENANT_ID
的环境变量。这样,它将加载所有适当的配置并设置特定的租户。
具有 __sleep() 和 __wakeup()
的特征
我们有一个 BootsTenant
特征,我们将在我们的队列作业中使用它,它看起来像这样:
trait BootsTenant
{
protected $tenantId;
/**
* Prepare the instance for serialization.
*
* @return array
*/
public function __sleep()
{
$this->tenantId = env('TENANT_ID');
return array_keys(get_object_vars($this));
}
/**
* Restore the ENV, and run the service provider
*/
public function __wakeup()
{
// We need to set the TENANT_ID env, and also force the BootTenantServiceProvider again
\Dotenv::makeMutable();
\Dotenv::setEnvironmentVariable('TENANT_ID', this->tenantId);
app()->register(BootTenantServiceProvider::class, [], true);
}
}
现在我们可以编写一个使用此特征的队列作业。当作业在队列上序列化时,__sleep()
方法将在本地存储 tenantId。反序列化时,__wakeup()
方法将恢复环境变量和 运行 服务提供商。
队列作业
我们的队列作业只需要使用这个特性:
class MyJob implements SelfHandling, ShouldQueue {
use BootsTenant;
protected $userId;
public function __construct($userId)
{
$this->userId = $userId;
}
public function handle()
{
// At this point the job has been unserialized from the queue,
// the trait __wakeup() method has restored the TENANT_ID
// and the service provider has set us all up!
$user = User::find($this->userId);
// Do something with $user
}
}
与 SerializesModels 冲突
Laravel 包含的 SerializesModels
特性提供了自己的 __sleep
和 __wakeup
方法。我还没有完全弄清楚如何让这两个特性一起工作,或者即使这是可能的。
现在我确保我从不在构造函数中提供完整的 Eloquent 模型。您可以在上面的示例作业中看到,我只将 ID 存储为 class 属性,而不是完整模型。我有 handle()
方法在队列 运行 时间内获取模型。那么我根本不需要 SerializesModels
特性。
使用queue:listen代替--daemon
您需要 运行 您的队列工作人员使用 queue:listen
而不是 queue:work --daemon
。前者为每个队列作业启动框架,后者将启动的框架加载到内存中。
至少,假设您的租户启动过程需要全新的框架启动,您需要执行此操作。如果您能够连续启动多个租户,干净地覆盖每个租户的配置,那么您可能能够逃脱 queue:work --daemon
就好了。
要扩展@jszobody 他的答案,请参阅 TenantAwareJob trait build by multi-tenant Laravel package。
这完全符合您的要求,在睡眠之前对您的租户进行编码,在醒来时启动您的租户。
此特性也适用于 SerializesModels
,因此您可以传递您的模型。
更新
因为 Laravel 6 这不再有效。 SerializeModels
特征覆盖 __serialize
和 __unserialize
functions.
新方法是注册服务提供者并挂接到队列。通过这种方式,您可以在处理之前添加有效负载数据并启动 Tentant。示例:
public function boot()
{
$this->app->extend('queue', function (QueueManager $queue) {
// Store tenant key and identifier on job payload when a tenant is identified.
$queue->createPayloadUsing(function () {
return ['tenant_id' => TenantManager::getInstance()->getTenant()->id];
});
// Resolve any tenant related meta data on job and allow resolving of tenant.
$queue->before(function (JobProcessing $jobProcessing) {
$tenantId = $jobProcessing->job->payload()['tenant_id'];
TenantManager::getInstance()->setTenantById($tenantId);
});
return $queue;
});
}
灵感来自 laravel-multitenancy and tanancy queue driver
我正在构建一个多租户 Laravel 应用程序(在 Laravel 5.3 上),它允许每个租户针对任何受支持的 Laravel 设置拥有自己的一组配置。目前,这是通过使用我自己提供自定义配置加载器(覆盖默认值 Illuminate\Foundation\Bootstrap\LoadConfiguration
)的实现覆盖默认值 Laravel Application
来实现的。应用程序从 bootstrap 上的环境(PHP 的 $_ENV
或 .env
文件)检测当前租户,然后为检测到的租户加载适当的配置文件。
上述方法对 HTTP 和控制台内核都非常有效,其中每个内核 request/command 都有有限的生命周期,但我不确定如何处理队列工作程序。我希望所有租户都有一个队列工作人员,并且我已经实现了一个自定义队列连接器,以便在安排队列作业时添加额外的元数据,以便在工作人员收到租户时识别租户。
我寻求您帮助的部分是如何 运行 在隔离环境中对每个队列作业进行排序,我可以 bootstrap 使用我的自定义配置。
我看到的一些可能的解决方案是:
到 运行 自定义队列工作器 运行 作为守护进程并从队列中获取作业,但在单独的 PHP 中执行作业进程(通过
exec()
创建);作业执行后,工作人员收集结果(状态、异常等)并在父进程中完成作业(例如删除作业等)与上面类似,但是运行作业在单独的PHP线程中,而不是使用
[=的单独进程39=]RunKit Sandbox
实施一个解决方案,在收到队列作业后"reboots" 应用程序(例如,重新加载当前租户的配置,重置任何已解决的依赖项等)
重要的是,我希望这个多租户作业的执行对作业本身是透明的,这样在多租户环境中没有设计为 运行 的作业(例如来自Laravel Scout 等第三方包无需任何修改即可处理。
关于如何解决这个问题有什么建议吗?
我们的情况差不多。这是我们的方法:
服务提供商
我们有一个名为 BootTenantServiceProvider
的 ServiceProvider,它在正常的 HTTP/Console 请求中引导租户。它期望存在一个名为 TENANT_ID
的环境变量。这样,它将加载所有适当的配置并设置特定的租户。
具有 __sleep() 和 __wakeup()
的特征我们有一个 BootsTenant
特征,我们将在我们的队列作业中使用它,它看起来像这样:
trait BootsTenant
{
protected $tenantId;
/**
* Prepare the instance for serialization.
*
* @return array
*/
public function __sleep()
{
$this->tenantId = env('TENANT_ID');
return array_keys(get_object_vars($this));
}
/**
* Restore the ENV, and run the service provider
*/
public function __wakeup()
{
// We need to set the TENANT_ID env, and also force the BootTenantServiceProvider again
\Dotenv::makeMutable();
\Dotenv::setEnvironmentVariable('TENANT_ID', this->tenantId);
app()->register(BootTenantServiceProvider::class, [], true);
}
}
现在我们可以编写一个使用此特征的队列作业。当作业在队列上序列化时,__sleep()
方法将在本地存储 tenantId。反序列化时,__wakeup()
方法将恢复环境变量和 运行 服务提供商。
队列作业
我们的队列作业只需要使用这个特性:
class MyJob implements SelfHandling, ShouldQueue {
use BootsTenant;
protected $userId;
public function __construct($userId)
{
$this->userId = $userId;
}
public function handle()
{
// At this point the job has been unserialized from the queue,
// the trait __wakeup() method has restored the TENANT_ID
// and the service provider has set us all up!
$user = User::find($this->userId);
// Do something with $user
}
}
与 SerializesModels 冲突
Laravel 包含的 SerializesModels
特性提供了自己的 __sleep
和 __wakeup
方法。我还没有完全弄清楚如何让这两个特性一起工作,或者即使这是可能的。
现在我确保我从不在构造函数中提供完整的 Eloquent 模型。您可以在上面的示例作业中看到,我只将 ID 存储为 class 属性,而不是完整模型。我有 handle()
方法在队列 运行 时间内获取模型。那么我根本不需要 SerializesModels
特性。
使用queue:listen代替--daemon
您需要 运行 您的队列工作人员使用 queue:listen
而不是 queue:work --daemon
。前者为每个队列作业启动框架,后者将启动的框架加载到内存中。
至少,假设您的租户启动过程需要全新的框架启动,您需要执行此操作。如果您能够连续启动多个租户,干净地覆盖每个租户的配置,那么您可能能够逃脱 queue:work --daemon
就好了。
要扩展@jszobody 他的答案,请参阅 TenantAwareJob trait build by multi-tenant Laravel package。
这完全符合您的要求,在睡眠之前对您的租户进行编码,在醒来时启动您的租户。
此特性也适用于 SerializesModels
,因此您可以传递您的模型。
更新
因为 Laravel 6 这不再有效。 SerializeModels
特征覆盖 __serialize
和 __unserialize
functions.
新方法是注册服务提供者并挂接到队列。通过这种方式,您可以在处理之前添加有效负载数据并启动 Tentant。示例:
public function boot()
{
$this->app->extend('queue', function (QueueManager $queue) {
// Store tenant key and identifier on job payload when a tenant is identified.
$queue->createPayloadUsing(function () {
return ['tenant_id' => TenantManager::getInstance()->getTenant()->id];
});
// Resolve any tenant related meta data on job and allow resolving of tenant.
$queue->before(function (JobProcessing $jobProcessing) {
$tenantId = $jobProcessing->job->payload()['tenant_id'];
TenantManager::getInstance()->setTenantById($tenantId);
});
return $queue;
});
}
灵感来自 laravel-multitenancy and tanancy queue driver