我想谈谈在网络上很少看到的内容,但对我来说是健康应用的关键:处理失败的任务。
我假设你此时已经理解了后台队列的工作原理。如果你不熟悉,这里或许是一个好的起点。
处理失败的任务是那种很容易在有很多其他事情要做和工单要完成的时候忽略的事情。你可能甚至认为这不是你(可能确实不是)应该关心的事情。但我认为,这是团队中所有开发人员都应该意识到并且时刻思考的事情。
因此,在这篇文章中,我将讨论一些处理失败任务的技巧和窍门,但不仅限于它们失败之后,还包括它们在失败过程中。这篇文章将使用Laravel队列系统的例子,但主要概念很容易应用于任何编程语言/框架。
任何可能出错的事情都会出错
你知道关于墨菲定律吗?所以你知道事情会出错,所以尽量接受这一点。你的代码可能已经完美运行了几个月,但某一天,你不得不集成的那个丑陋的API在没有预先通知的情况下改变了,或者AWS出了问题,或者[插入你随机的失败原因]。
事实是,有些意想不到的事情将要发生。因此,处理你知道如何处理的事情,并在不知道如何处理的情况下确保你“失败得更好”。失败得更好的关键之一是事情发生后有足够的信息来调试发生了什么。通常,日志记录是处理这类事情的最佳工具。
让我们通过一个简单的代码示例来更好地说明这一点
<?php
class UploadImageJob implements ShouldQueue
{
public function __construct($path)
{
$this->path = $path;
}
public function handle(UploaderClient $client)
{
try {
$client->upload($this->path);
} catch (RateLimitException $exception) {
$this->release($exception->getRetryAfter());
} catch (Exception $exception) {
Log::critical('[UploadImage] Unkown error when trying to upload image', [
'error' => $exception->getMessage(),
'path' => $this->path,
]);
$this->fail($exception);
}
}
}
在上面的示例中,我们特别知道如何处理API中的速率限制异常,通过在我们的速率限制过期后将工作放回队列中。我们还在捕获可能发生的通用错误,并应用一些良好的日志记录信息,以便我们可以在以后调试发生了什么。
重试提高成功率
在上面的示例中,工作可能失败的原因之一是,我们使用的图像上传服务出现了故障。因此,如果你的工作不是时间敏感的,实施某种指数退避策略可能是个好主意。
在Laravel中,这可以通过尝试次数和retryAfter
方法来实现。
<?php
public function retryAfter()
{
return now()->addMinutes($this->attempts() * 5);
}
这里,重要的是要注意你的队列工作者和/或你的工作最大尝试次数。另一种方法是直接调用release
方法。
<?php
class UploadImageJob implements ShouldQueue
{
public function __construct($path)
{
$this->path = $path;
}
public function handle(UploaderClient $client)
{
try {
$client->upload($this->path);
} catch (Exception $exception) {
$this->release($this->attempts() * 5);
}
}
}
需要注意的是,这种方法可能不是每个工作的正确方法。所以请注意这一点。
保持失败的作业表干净
Cloudfare发生了故障,基本影响了整个互联网,你的大多数作业都失败了。当然,在故障期间,你无法做什么,但你(应当)总能回来重新运行失败的排队作业。它们都在(至少应该)存储在失败的作业表中。
如果你不清理一次,下次再发生时你可能不会再这么做,因为你对表中一些旧记录可能造成的影响并不完全确定。如此下去,直到你有5万个失败的作业在表中,你只能选择删除一切或假装你没有记得那个表。
所以,保持你的失败作业干净。为此,我有一些建议来避免在保持表清洁的过程中出现的问题。
有一些事情你不需要或不能重新运行
在某些情况下,重新运行一些失败的作业可能会产生比好的更多的伤害。所以,在重试失败的作业时要注意这一点。
例如,你不希望重新运行处理已经处理的交易的作业,或者在3个月后发送“您的订单已交付”的通知并不真正有意义。
Laravel并没有一个非常很好的批量重试或清除特定作业的方法,所以我写了这个包,在基本级别添加了这一功能,这可能对你有所帮助。
所以,我的建议是移除你不需要的,重新运行你想要的。这让我想到了下一个要点。
尽可能使你的作业幂等
幂等是一个时髦的词。但基本上,你想要确保如果同一个作业运行多次,它不会为相同的事情多次向你的客户提供费用,例如。
这种思维方式在通过作业有效载荷更新数据库记录时的另一个重要的情况是。我最近从事的这个系统接收数以千计的有效载荷来创建/更新/删除文档。有时,由于各种原因,这些作业会失败。但如果我们将来一天再运行这些作业,文档的新更新可能已经执行,具有更新的数据。所以在这种情况下,我实施了一种如下的逻辑
<?php
class UpdateDocumentJob implements ShouldQueue
{
public function __construct($document, $payload)
{
$this->document = $document;
$this-> payload = $payload;
}
public function handle()
{
if ($this->document->updated_at > $this->payload->updated_at) {
Log::info("[UpdateDocumentJob] Not updating document because document last updated is greater than the payload last updated", [
'document' => $this->document->id,
'document_updated_at' => $this->document-> updated_at,
'payload_updated_at' => $this->payload->updated_at,
]);
return;
}
}
}
忽略缺失的模型
<blockquote>Illuminate\Database\Eloquent\ModelNotFoundException: No query results for model</blockquote>在你的Laravel生活中,你是否遇到过这些情况?在队列任务中,这种情况可能会发生,因为Laravel在发送/检索任务到/从队列时会序列化和反序列化你的模型。这正是SerializesModels
特性所做的。
<?php
class SendArticleUpdatedWebhookJob implements ShouldQueue
{
use SerializesModels;
public $deleteWhenMissingModels = true;
public function __construct(Article $article)
{
$this->article = $article;
}
}
如果Laravel尝试反序列化你的模型(在幕后,它基本上会尝试通过findOrFail
从数据库再次获取它),并且模型已被删除,将会抛出ModelNotFoundException
。如果你不在意你的任务中的这个错误,你可以在你的任务中简单地设置deleteWhenMissingModels
属性,然后Laravel将忽略丢失的模型,不会将任务发送到失败的作业表。
这虽然可能是一个边缘情况,但是根据你处理的工作量,确实有助于保持事情整洁。
日志记录
队列任务以异步方式运行,你最有效率的工具是日志记录,以了解发生了什么(即使是在成功的情况下)。当你知道你正在处理的情况时,在某个地方编写日志消息时,这有时可能会显得有些愚蠢,但我不知道我有多多次因此受到教训。
<span class="text-highlight">你的本地数据和测试数据并不等于你的生产数据。</span>记得“无论什么可能出错,最终都会出错”的部分吗?有时你记录的简单事情可能会直接帮你节省数小时的调试时间。
我发现非常有用的是,使用所有不同的可用日志级别,并根据这些级别设置一个好的警报系统。
最后一件事情,这更多的是与你的整个堆栈相关,那就是拥有一个集中的日志记录场所,你可以搜索并设置警报。你可能已经注意到,我喜欢用作业名称作为“命名空间”来前缀我的日志条目。这对于搜索日志、创建独立的日志流和警报非常有用。
这就是我全部的分享,希望如果你一路看下来,这在某种程度上对你有所帮助。
driesvints, thinkverse, jingfengshi, noplanman, romankm, nunomaduro, ktanaug21 喜欢了这篇文章