序
驗證碼,只要一點簡單的邏輯母市,就能避免泱泱腳本大軍的騷擾矾兜。但利劍往往是雙刃的损趋,并不是每個場景都適用患久,本文將通過記錄一起線上事故,來展示使用圖形驗證碼的代價浑槽,并討論如何應(yīng)對類似情況蒋失。
科普一個冷知識:驗證碼的英文是CAPTCHA,是Completely Automated Public Turing test to tell Computers and Humans Apart的縮寫桐玻,翻譯過來是“全自動區(qū)分計算機和人類的圖靈測試”(不是“雅木茶”)篙挽。
事故過程
一切都要從一個簡單的需求說起
公司新游戲即將上線,運營已經(jīng)激動地搓手手了镊靴,于是一次預(yù)約活動規(guī)劃了下來铣卡。就是那種輸入手機號和手機系統(tǒng)的預(yù)約活動链韭。這個需求太簡單了,開發(fā)也沒多想煮落,一梭子代碼就下去了敞峭。唯一一層防護是為了避免腳本刷接口,要求預(yù)約時要輸入一次圖形驗證碼蝉仇。
悲劇即將上演
倒計時3,2,1旋讹,活動開始。開始的時候一切順利轿衔,流量流入沉迹,預(yù)約數(shù)據(jù)也在有條不紊地入庫。不過一會兒害驹,客服就開始忙活了起來鞭呕,原來大量玩家反饋:本該顯示圖形驗證碼的地方現(xiàn)在正顯示著一個x。于是宛官,一句”你們公司用的是土豆服務(wù)器嗎“刷遍了微博貼吧琅拌。
開始思考
這是圖形驗證碼的一般做法
在img標簽的src填入生成驗證碼地址,該接口會在內(nèi)存中生成一張圖片摘刑。嚴謹一些的話进宝,還會在圖片上面打上干擾線和噪點。
將驗證碼存入Session后枷恕,返回圖片党晋。
用戶提交數(shù)據(jù),比較參數(shù)和Session值徐块。
天底下沒有理所當然的事
這本來是最正常不過的操作未玻,但仔細過一遍可以發(fā)現(xiàn),生成一張圖形驗證碼的代價是很大的胡控。
生成圖片扳剿,占用比普通操作更多的內(nèi)存
生成隨機數(shù)、噪點昼激、干擾線庇绽,需要生成隨機數(shù)
圖片傳輸,占用帶寬
驗證碼讀取\寫入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);
沒有使用框架瞧掺、組件,簡化加載過程凡傅。功能越強大辟狈,性能越受限。
為了減少隨機數(shù)生成,驗證碼僅四位哼转,背景色明未、字體色直接固定,也沒有加噪點壹蔓,加干擾線亚隅。
生成并輸出圖片后,銷毀資源庶溶。
不保存圖片到磁盤煮纵。
寫一個方法,模擬用戶看到驗證碼偏螺,輸入驗證碼的過程:
$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' => '',
];
單次調(diào)用
獲取驗證碼用了184ms行疏。來看看機器指標,恩套像,沒什么波瀾酿联,系統(tǒng)1分鐘平均負載是0.3。
什么是系統(tǒng)1分鐘平均負載夺巩?
1分鐘內(nèi)贞让,占用全部CPU算力的比例。
舉個栗子柳譬,如果機器是2核的喳张,那么滿負載時,最大值就是2美澳。上面的負載是0.3销部,意味著程序只用到了1個CPU約三分之一的算力。
這次來試試1000次
go-stress-testing-linux -c 1000 -n 1 -u {your url}
總耗時5s制跟,全部成功(HTTP狀態(tài)碼200)舅桩,再來看看指標,系統(tǒng)1分鐘平均負載上升至1雨膨,還好還好擂涛。
康康你的極限在哪里?
go-stress-testing-linux -c 10000 -n 1 -u {your url}
總耗時36s聊记,成功5007撒妈,失敗4993(HTTP狀態(tài)碼509),看看指標:
硬件資源并沒有消耗完呀甥雕,怎么會有失敗請求呢踩身?
原因很簡單胀茵,上面已經(jīng)告訴你啦社露,帶寬占滿啦。代碼生成的驗證碼圖片一張約1.7k琼娘,比起其他類型的數(shù)據(jù)已經(jīng)大很多了峭弟。所謂聚少成多附鸽,聚沙成塔,一個人的力量可能不算什么瞒瘸,但千萬個人的力量就絕對不可忽視坷备。
各個HTTP狀態(tài)碼表示詳見(http://zhaomaomao.net/article/1/13)。
得想個辦法讓所有請求都進去
要達到這個目的情臭,需要一點點改造:
把生成驗證碼的邏輯從controller移到Laravel的Command模塊省撑,這樣,就不會出現(xiàn)php的腳本超時啦俯在。
寫一個sh腳本啟動這些CMD竟秫,這樣,就繞過了web服務(wù)器跷乐。
#!/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肥败,看看指標:
處理5k個驗證碼生成,已經(jīng)要花20多分鐘了愕提,單從這點看已經(jīng)不能拿上正式服了馒稍。由于沒有經(jīng)過Nginx處理,內(nèi)存倒沒有飆高浅侨。
分析一波
由上面幾組數(shù)據(jù)可以看出纽谒,自己生成圖形驗證碼消耗資源從多到少依次為:網(wǎng)絡(luò)帶寬 > CPU > 內(nèi)存 > 磁盤讀寫。
筆者的虛擬機是2核如输,4G佛舱,3M帶寬,在Nginx各項超時時間5分鐘的情況下(這個時間已經(jīng)很長了)挨决,每秒也只能正常處理約140個驗證碼請求请祖。
怎么優(yōu)化呢
有人可能會問,蛤脖祈?這有什么可優(yōu)化的肆捕?就像上面說的, 天底下真的沒有理所當然的事情盖高。筆者在此拋磚引玉慎陵,獻個丑啦。
限制前端的刷新頻率
筆者自己就是這樣喻奥,當看不清驗證碼心煩意亂的時候席纽,就會瘋狂點擊刷新。驗證碼生成本身就需要一定時間撞蚕,結(jié)果刷出新的還沒來得及顯示润梯,就又進入了下一次生成的循環(huán)。
用js稍稍控制一下,在img圖片load完成前禁止刷新纺铭;刷新后稍稍等個幾秒寇钉,就可以避免這類情況。
把驗證碼生成的服務(wù)與主要邏輯服務(wù)分離
這和靜態(tài)資源與網(wǎng)站其他資源分離是一個道理舶赔。既然驗證碼圖片占帶寬扫倡,那就不走主要邏輯服務(wù)器的流量,這樣就可以增大主要邏輯服務(wù)器的吞吐量竟纳。
使用沒有圖片的驗證碼
例如把方塊拖動到最右邊啦撵溃,完成拼圖啦(這種也需要圖片,但只需要加載一次)锥累,把圖片旋轉(zhuǎn)到正面啦之類的征懈。
使用第三方驗證碼
能用錢解決的,都不是問題揩悄。
就到這里吧
希望本文或多或少對你有些許幫助卖哎。謝謝你的閱讀。再見删性。