记录一起图形验证码引起的线上事故

记录一起图形验证码引起的线上事故

技术杂谈小彩虹2021-08-18 0:13:00190A+A-

验证码,只要一点简单的逻辑,就能避免泱泱脚本大军的骚扰。但利剑往往是双刃的,并不是每个场景都适用,本文将通过记录一起线上事故,来展示使用图形验证码的代价,并讨论如何应对类似情况。

科普一个冷知识:验证码的英文是CAPTCHA,是Completely Automated Public Turing test to tell Computers and Humans Apart的缩写,翻译过来是“全自动区分计算机和人类的图灵测试”(不是“雅木茶”)。

事故过程

一切都要从一个简单的需求说起

公司新游戏即将上线,运营已经激动地搓手手了,于是一次预约活动规划了下来。就是那种输入手机号和手机系统的预约活动。这个需求太简单了,开发也没多想,一梭子代码就下去了。唯一一层防护是为了避免脚本刷接口,要求预约时要输入一次图形验证码。

悲剧即将上演

倒计时3,2,1,活动开始。开始的时候一切顺利,流量流入,预约数据也在有条不紊地入库。不过一会儿,客服就开始忙活了起来,原来大量玩家反馈:本该显示图形验证码的地方现在正显示着一个x。于是,一句”你们公司用的是土豆服务器吗“刷遍了微博贴吧。

开始思考
这是图形验证码的一般做法
  • 在img标签的src填入生成验证码地址,该接口会在内存中生成一张图片。严谨一些的话,还会在图片上面打上干扰线和噪点。
  • 将验证码存入Session后,返回图片。
  • 用户提交数据,比较参数和Session值。
天底下没有理所当然的事

这本来是最正常不过的操作,但仔细过一遍可以发现,生成一张图形验证码的代价是很大的。

  • 生成图片,占用比普通操作更多的内存
  • 生成随机数、噪点、干扰线,需要生成随机数
  • 图片传输,占用带宽
  • 验证码读取\写入Session,需要读写磁盘
实践一下
代码

写一个简单的验证码生成方法:

$str = "23456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVW";
$len = strlen($str) - 1;
$code = '';
for ($i = 0; $i < 4; $i++) {
    $code .= $str[mt_rand(0, $len)];
}

$img = imagecreatetruecolor(100, 30);
imagefilledrectangle($img, 0, 0, 100, 30, imagecolorallocate($img, 255, 255, 255));
imagefttext($img, 20, mt_rand(-5, 5), 10, 25, imagecolorallocate($img, 0, 0, 0), '{your font path}', $code);

$_SESSION['captcha'] = $code;

header("Cache-Control: no-cache");
header("Content-type: image/png;charset=utf-8");
imagepng($img);
imagedestroy($img);
  • 没有使用框架、组件,简化加载过程。功能越强大,性能越受限。
  • 为了减少随机数生成,验证码仅四位,背景色、字体色直接固定,也没有加噪点,加干扰线。
  • 生成并输出图片后,销毁资源。
  • 不保存图片到磁盘。

写一个方法,模拟用户看到验证码,输入验证码的过程:

$input = $_POST['verify'] ?? '';
if ($input === '') {
    return [
        'code' => 0,
        'msg' => 'verify empty',
    ];
}
$code = $_SESSION['captcha'] ?? '';
if ($code === '') {
    return [
        'code' => 0,
        'msg' => 'verify not exist',
    ];
}
if (0 !== strcasecmp($input, $code)) {
    return [
        'code' => 0,
        'msg' => 'verify failed',
    ];
}
unset($_SERVER['captcha']);
return [
    'code' => 1,
    'msg' => '',
];
单次调用

获取验证码用了184ms。来看看机器指标,恩,没什么波澜,系统1分钟平均负载是0.3。

什么是系统1分钟平均负载?

1分钟内,占用全部CPU算力的比例。

举个栗子,如果机器是2核的,那么满负载时,最大值就是2。上面的负载是0.3,意味着程序只用到了1个CPU约三分之一的算力。

这次来试试1000次
go-stress-testing-linux -c 1000 -n 1 -u {your url}

总耗时5s,全部成功(HTTP状态码200),再来看看指标,系统1分钟平均负载上升至1,还好还好。

康康你的极限在哪里?
go-stress-testing-linux -c 10000 -n 1 -u {your url}

总耗时36s,成功5007,失败4993(HTTP状态码509),看看指标:

1.png

硬件资源并没有消耗完呀,怎么会有失败请求呢?

原因很简单,上面已经告诉你啦,带宽占满啦。代码生成的验证码图片一张约1.7k,比起其他类型的数据已经大很多了。所谓聚少成多,聚沙成塔,一个人的力量可能不算什么,但千万个人的力量就绝对不可忽视。

各个HTTP状态码表示详见(zhaomaomao.net/article/1/1…

得想个办法让所有请求都进去

要达到这个目的,需要一点点改造:

  • 把生成验证码的逻辑从controller移到Laravel的Command模块,这样,就不会出现php的脚本超时啦。
  • 写一个sh脚本启动这些CMD,这样,就绕过了web服务器。
#!/bin/bash
start_at=`date +%s`
for((i=1;i<=5000;i++));
do
php artisan {your command} > /dev/null;
done
end_at=`date +%s`
echo $[end_at-$start_at]

总耗时1219s,看看指标:

2.png

处理5k个验证码生成,已经要花20多分钟了,单从这点看已经不能拿上正式服了。由于没有经过Nginx处理,内存倒没有飙高。

分析一波

由上面几组数据可以看出,自己生成图形验证码消耗资源从多到少依次为:网络带宽 > CPU > 内存 > 磁盘读写。

笔者的虚拟机是2核,4G,3M带宽,在Nginx各项超时时间5分钟的情况下(这个时间已经很长了),每秒也只能正常处理约140个验证码请求。

怎么优化呢

有人可能会问,蛤?这有什么可优化的?就像上面说的, 天底下真的没有理所当然的事情。笔者在此抛砖引玉,献个丑啦。

限制前端的刷新频率

笔者自己就是这样,当看不清验证码心烦意乱的时候,就会疯狂点击刷新。验证码生成本身就需要一定时间,结果刷出新的还没来得及显示,就又进入了下一次生成的循环。

3.png

用js稍稍控制一下,在img图片load完成前禁止刷新;刷新后稍稍等个几秒,就可以避免这类情况。

把验证码生成的服务与主要逻辑服务分离

这和静态资源与网站其他资源分离是一个道理。既然验证码图片占带宽,那就不走主要逻辑服务器的流量,这样就可以增大主要逻辑服务器的吞吐量。

使用没有图片的验证码

例如把方块拖动到最右边啦,完成拼图啦(这种也需要图片,但只需要加载一次),把图片旋转到正面啦之类的。

使用第三方验证码

能用钱解决的,都不是问题。

就到这里吧

希望本文或多或少对你有些许帮助。谢谢你的阅读。再见。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们