5 种编写可扩展 Laravel 代码的方法(赞助)
发布于 作者 Jack Ellis
您好,Laravel 新闻的读者,我是 Jack Ellis 和 Paul Jarvis,是 Fathom Analytics 的创始人。在我们深入了解具体内容之前,请允许我们自我介绍一下。我们运营着 Fathom Analytics,这是一个简单、注重隐私的分析平台,被那些关心访客隐私的具有前瞻性的网站所有者所使用。我们的应用程序使用 Laravel 构建并在 Laravel Vapor 上部署。Jack 也是 无服务器 Laravel 的创建者,这是一门掌握 Laravel Vapor 的课程,Paul 也是 独狼公司 的作者,这本书质疑传统的业务增长,以寻求对成功的更好定义。我们共同组成了 Fathom Analytics 团队。
Fathom Analytics 在整个 Laravel 社区中得到了广泛使用。我们的一些很棒的 Laravel 客户包括
- Matt Stauffer(Tighten 的合伙人)
- James Brooks(Laravel LLC 的开发人员和 Happy Dev FM 主持人)
- Dries Vints(Laravel LLC 的开发人员和 Laravel.io 的创始人)
- Jack McDade(Statamic 的创建者)
- Justin Jackson(Transistor 的联合创始人)
- Stefan Bauer(PingPing 的创始人)
以及许多其他。
以下内容不是我们在推销 Fathom。相反,它旨在帮助您成为一名更好的 Laravel 开发人员。我们唯一的宣传是:如果您需要简单的分析或认识需要这种分析的人,请试用 Fathom Analytics。
现在介绍部分已经结束,我(Jack)将介绍一些关于扩展的代码提示。我将重点关注代码,而不是基础设施。
做好数据库停机准备
当数据库离线时,应用程序前端通常也会随之离线,因为应用程序通常无法没有数据库而运行。但幕后会发生什么?当您回复愤怒的推文时,您的队列工作者仍在运行,却一无所获,并且可能会丢失所有作业数据。
在编写作业时,我们需要了解它们有时会失败。我们并不为此感到愤怒,我们明白这是作业的本质。假设我们使用 Redis 来处理队列,因为我们想要一个高度可扩展的东西,我们将设置我们的工作者
php artisan queue:work redis —tries=3 —delay=3
一切运行得都很完美。由于 Redis 的低延迟,我们的作业排队速度很快,我们的用户都很喜欢我们(没有愤怒的推文!)。
但是,假设我们的数据库始终可用就太天真了。
假设它离线了 20 分钟……我们的作业会怎样?由于 Redis 仍然在线,它们会继续运行。如果我们没有修改默认配置,它们会在 90 秒后重试,并且根据上面的代码,将进行 3 次尝试。在这些尝试之后,失败的作业会进入我们数据库中的 failed_jobs 表。等等,我们的数据库离线了……所以作业无法插入 failed_jobs 表。
我们可以采取以下措施来防止这种情况
try { // Check to see if the database is online DB::connection()->getPdo();} catch (\Exception $e) { // Push it back onto the Redis queue for 20 mins $this->release(1200);}
我们可以将这段代码放在一些作业中间件中运行,或者将其添加到作业的开头。冒着被称作“显而易见”的风险,让我解释一下它的作用。在它在作业中执行任何操作之前,它会检查数据库连接是否在线。如果没有,它会将作业释放一段时间(20 分钟)。如果您的设置是尝试作业 3 次,那么在头两次尝试中大约可以获得 40 分钟的时间。如果您的数据库在那段时间内没有恢复在线,那么,天哪,您遇到了更大的问题。
现在,您可能会认为 20 分钟的延迟很愚蠢。冷静一下,我还有另一种方法。将尝试次数设置为更高的值
php artisan queue:work redis --tries=15 --delay=3
然后使用这段代码
try { // Check to see if the database is online DB::connection()->getPdo();} catch (\Exception $e) { if ($this->attempts() <= 13) { $this->release(60); } else { $this->release(1200); }}
这样,您就可以两全其美。前 13 次尝试会导致 60 秒的延迟,这对于数据库出现短暂故障(离线 20 毫秒)非常有用,因为您的作业将很快完成,并且您还拥有 20 分钟的延迟,以便在数据库离线 15 分钟或更长时间时使用。这不是生产代码,这只是一篇用于 Laravel 新闻文章的概念代码,但这可以被修改、测试和完美地实现。所以,试一试吧。
假设所有外部服务都会在某个时间点离线
开发者有时会变得自满,不是吗?将作业抛到队列中,它会没事的。检查缓存,它会在线的。但是当这些组件离线时会发生什么?当然,如果您在作业内部运行这些组件,作业会失败/重试,您可以继续编码。但是,如果您在用户向您的应用程序发出 HTTP 请求时将作业排队或检查缓存,那么世界末日就到了,每个人都会受到伤害。但是,如果我们使用以下技术,我们可以成为快乐的人
// Adding Fault toleranceretry(20, function() use ($request) { dispatch(new JobThatUsesTheRequest($request));}, 200);
这里的妙处在于,我们将作业的排队操作重试 20 次,每次尝试之间延迟 200 毫秒。这是一种很好地吸收队列的任何短暂停机时间的做法。是的,它会增加用户响应时间,但,猜猜怎么了,请求得到了满足,那么谁是受害者呢?
虽然上面的方法对于 SQS 这样的高可用性、完全托管队列非常有效,但对于低可用性队列,您该怎么办?理想情况下,您不应该使用低可用性队列。如果您的老板或客户不允许您花更多钱来获得高可用性队列解决方案,那么以下代码可以帮到您
try { retry(20, function() use ($request) { dispatch(new JobThatUsesTheRequest($request)); }, 200);} catch (\Exception $e) { Mail::raw('Error with low-availability queue, increase budget please', function ($message) { $message->to('[email protected]'); $message->subject('Look what you did'); });}
好吧,这就是我会做的事情;)
使用更快的会话驱动程序
我在很多应用程序中看到的一个情况是,人们使用默认的会话驱动程序或他们的数据库。这在小规模情况下可以,但在规模化时不会带来最佳效果。更好的选择是使用内存中的存储,如 Redis。
在您执行任何操作之前,请设置 Redis,获取连接详细信息并设置相应的环境变量(这里只有这些内容,这不是“将 Redis 添加到 Laravel”的指南 :P)。
一旦一切设置就绪,打开 config/database.php
并向下滚动到 Redis 部分。复制默认条目并将键更改为“session”。然后将 database
值更改为 env(‘REDIS_SESSION_DB’, 3)
并为其添加一个环境变量。Redis 区域应该如下所示
'redis' => [ 'client' => env('REDIS_CLIENT', 'predis'), 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), ], 'default' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'password' => env('REDIS_PASSWORD', null), 'port' => env('REDIS_PORT', 6379), 'database' => env('REDIS_DB', 0), ], 'cache' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'password' => env('REDIS_PASSWORD', null), 'port' => env('REDIS_PORT', 6379), 'database' => env('REDIS_CACHE_DB', 1), ], 'session' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'password' => env('REDIS_PASSWORD', null), 'port' => env('REDIS_PORT', 6379), 'database' => env('REDIS_SESSION_DB', 3), ],],
现在,您需要确保在您的 .env 文件中存在以下变量
- SESSION_DRIVER=redis
- SESSION_CONNECTION=session
然后您就可以开始使用了。响应时间会大大减少。朋友,不谢。
不要浪费查询做无用功
让我们来看看一些在小规模下并不重要,但在你发展壮大后会变得越来越重要的东西:缓存你的查询。大多数人已经这样做了,这很棒,但很多人没有这样做。有些人缓存了错误的东西,而另一些人却恰到好处。还有一些人遇到了各种陈旧的缓存问题,他们可能会花费数小时来调试由缓存引起的问题。哎呀,我们都经历过。
那么,如果我们想过上一种幸福的生活,高效地利用我们的资源,我们该怎么做呢?
缓存静态数据
如果你的数据库中有一个表,比如 Countries,它很少被更新,你可以缓存它,而不会有任何陈旧的缓存问题。
$countries = Cache::remember(‘countries:all’, 86400, function() { return Country::orderBy(‘name’, ‘asc’)->get();});
我通常会选择 24 小时。虽然每天没有很多新国家出现,但仍然有可能某个国家会改名等。如果我们现实一点,你也可以缓存它一个星期。但为什么我们不使用 rememberForever 呢?我们可以。我只是更喜欢将 Redis 的驱逐策略设置为 "lru 选项"(这不是 Redis 课程,所以我们到此为止!)。
缓存动态数据
早在早期,我们很多人都避免缓存用户对象和其他内容。 "如果用户更改了他们的电子邮件,而缓存中的信息是错误的怎么办?"。天哪。但这并不一定如此。如果我们负起保持缓存更新的责任,就没有问题。在 Fathom Analytics 中,我们广泛地使用缓存来存储动态数据,并且我们使用观察者来确保缓存保持最新。
我们使用诸如 **Site::loadFromCache($id)** 之类的函数,然后,每当站点发生更改时,我们确保调用 **Site::updateCache($id, $site)**。当然,我们也使用 **Site::deleteFromCache($id)**。你只能想象我们节省了多少数据库调用,让我们永远不必担心数据库负载。
这对数据库的更新也非常有益。与其对模型执行 findOrFail,你只需检查缓存,然后运行更新即可。当你处理 100 个更新时,这种更改的影响可以忽略不计,但一旦你达到数十万甚至数百万,它就能产生很大的影响。
在你的命令中做更少的事情
我保证这是最后一个。另外,嘿,你已经读了 1500 个字的我的胡言乱语,我很感激。我很乐意收到你的来信,保罗也是,我们是在 Twitter 上的 @jackellis 和 @pjrvs。即使你讨厌这篇文章,也告诉我们你有多讨厌它。你知道他们是怎么说的:任何宣传都是好宣传。
我看到很多人做的一件事是尝试在他们的命令中做太多的事情。例如,他们可能会让他们的命令在每次执行时处理和发送电子邮件,或者他们会在各种数据上执行一些逻辑。这在小规模下是可以的,但是当你的数据增加或你开始发送更多电子邮件时会发生什么?你的命令将超时。你需要分解你的后台任务。求求你了。如果你不为自己做,那就为我们做。
在使用命令时,你应该使用它们来调度作业。作业可以在几毫秒内调度,并且处理可以在隔离状态下完成。这意味着你的命令不会超时或达到一些愚蠢的内存限制。当然,这并不总是这样,但在你处理将会扩展的数据负载时是相关的。
$userChunks->each(function ($users) { SendUsersAnEmail::dispatch($users);});
通过这样做,我们分解了我们的工作负载。我们可以将我们的用户分成 100 人一组,让我们的作业负责给他们发送电子邮件。想象一下,如果我们对 500,000 个用户执行此操作,我们已经从在单个命令中处理所有 500,000 个用户转变为在 5,000 个单个作业之间处理它们。好多了。显然,我们可以在一个作业中处理 100 多个用户,这只是一个例子。
正如我最喜欢的兔子曾经说过的那样......这就是全部了。如果你有任何问题,你可以随时在 Twitter 上联系我们。
现在我们已经完成了,我将再次推广几件事
- Fathom Analytics 用于简单、注重隐私的分析
- Serverless Laravel 用于掌握 Laravel Vapor
- Everybody Hurts, REM
***
非常感谢 Fathom Analytics 在本周赞助 Laravel News。