有个文件,存储了两条准备用于给文件重命名的字符串和文件路径的数据,每行一条,名称和字符串以 , 间隔。
然后问题来了,通过读文件取出数据,然后获取文件路径并打包时,ZipArchive 的 addFile 总会有一个 false。
折腾半天,最后发现还是因为粗心大意,没过滤行尾的换行符……
一声叹息,掏出小本本默默记上一笔。
最后附上代码:
1 | <?php |

所以,不要怀疑,只要 addfile 返回了 false,肯定是路径有问题或者文件不存在。
文章作者: m-finder
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 M-finder!
相关推荐

2019-04-12
laravel 广播系统学习
看到广播系统,先想起了曾经虐过我的即时通讯。 虽然都是对 websocket 的应用,但是好像又有点区别,这里好好学习一下。 laravel 的广播与事件紧密相关,广播即对事件进行广播,因此在学习广播之前,要先阅读事件和监听器的相关文档。 配置老规矩,先来看配置文件 config/broadcasting.php 里边的配置选项: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960<?phpreturn [ /* |-------------------------------------------------------------------------- | Default Broadcaster |-------------------------------------------------------------------------- | | This option controls the default broadcaster that will be used by the | framework when an event needs to be broadcast. You may set this to | any of the connections defined in the "connections" array below. | | Supported: "pusher", "redis", "log", "null" | */ 'default' => env('BROADCAST_DRIVER', 'null'), /* |-------------------------------------------------------------------------- | Broadcast Connections |-------------------------------------------------------------------------- | | Here you may define all of the broadcast connections that will be used | to broadcast events to other systems or over websockets. Samples of | each available type of connection are provided inside this array. | */ 'connections' => [ 'pusher' => [ 'driver' => 'pusher', 'key' => env('PUSHER_APP_KEY'), 'secret' => env('PUSHER_APP_SECRET'), 'app_id' => env('PUSHER_APP_ID'), 'options' => [ 'cluster' => env('PUSHER_APP_CLUSTER'), 'encrypted' => true, ], ], 'redis' => [ 'driver' => 'redis', 'connection' => 'default', ], 'log' => [ 'driver' => 'log', ], 'null' => [ 'driver' => 'null', ], ],]; 默认情况下,laravel 提供了以上几种开箱即用的广播驱动器程序。 env 配置文件中,默认的驱动为 log,意味着客户端不会受到任何信息,只是会把要广播的消息写入 log 文件中,跟学习目标不符,就先以 pusher 展开学习吧。 我们就以发布新文章后推送给所有用户为例。 前期准备开始之前,必须要先注册 App\Providers\BroadcastServiceProvider,在 config/app.php 配置文件中的 providers 数组中取消对提供者的注释。 注册: [ pusher ] 然后把相关参数配置到 .env 文件。 安装组件:12composer require pusher/pusher-php-servernpm install --save laravel-echo pusher-js 添加文章模块,包含 migrate,controller,model,view 和 router 等内容。 新建事件:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051php artisan make:event NewArticleNotificationEvent// 事件内容:<?phpnamespace App\Events;use Illuminate\Queue\SerializesModels;use Illuminate\Broadcasting\Channel;use Illuminate\Broadcasting\PrivateChannel;use Illuminate\Foundation\Events\Dispatchable;use Illuminate\Broadcasting\InteractsWithSockets;use Illuminate\Contracts\Broadcasting\ShouldBroadcast;use App\Article;class NewArticleNotificationEvent implements ShouldBroadcast{ use Dispatchable, InteractsWithSockets, SerializesModels; private $article; /** * Create a new event instance. * * @return void */ public function __construct(Article $article) { $this->article = $article; } public function broadcastWith() { return [ 'title' => $this->article->title, 'content' => $this->article->content, 'author' => $this->article->user->name ]; } /** * Get the channels the event should broadcast on. * * @return \Illuminate\Broadcasting\Channel|array */ public function broadcastOn() { return new Channel('articles'); }} 触发事件在保存文章的控制器中触发事件: 123$data = array_merge($request->only(['title', 'content']), ['uid' => Auth::id()]);$article = Article::create($data);broadcast(new NewArticleNotificationEvent($article)); 前端监听文章列表用了vue组件,在这个组件中进行事件监听。 1234567891011121314151617181920212223242526272829303132333435<template> <div class="container"> <table class="table table-striped"> <tr> <th>ID</th> <th>Author</th> <th>Title</th> <th>Content</th> <th>Created At</th> </tr> <tr v-for="article in articles"> <td>{{article.id}}</td> <td>{{article.user.name}}</td> <td>{{article.title}}</td> <td>{{article.content}}</td> <td>{{article.created_at}}</td> </tr> </table> </div></template><script>export default { props: ['articles'], created() { Echo.channel('articles').listen('NewArticleNotificationEvent', (article) => { console.log(article); }) }}</script><style scoped></style> 写好后要在命令行执行 npm run watch-poll 实时编译文件。 测试写篇文章测试一下: 注意事项 不需要创建 channel 路由 不需要开启队列监听 如果没反应请先强制刷新浏览器

2023-05-11
php 国密 sm2 sm3 sm4 完整测试类
应用范围及描述 算法类型 国密算法 应用范围及描述 对称加密 SM1 128位数据加密,算法不公开,仅以IP核的形式存在于芯片中。智能IC卡、智能密码钥匙、加密卡、加密机。 非对称加密 SM2 被用来替换RSA算法。常用于身份认证,数据签名,密码交换,256位椭圆曲线。 完整性运算 SM3 256位数据摘要计算,相当于SHA256,数字签名及验证、消息认证码生成及验证、随机数生成 对称加密 SM4 128位数据加密,相当于AES(128) 相关代码php sm2 sm3 sm4 完整测试类,可拖入 laravel unit test 模块运行。基于扩展包 [ lpilp/guomi ] , sm2 与兴业银行有部分区别,sm4 已互通,未做招行验证。 sm2 密钥长度一般为 128 或 130 位,部分使用压缩密钥长度为 66,也就是将密钥分成 x、y,y是偶数就是02,y是奇数就是03,通过 x 可以算出 y。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290<?phpnamespace Tests\Unit;use FG\ASN1\ASNObject;use FG\ASN1\Exception\ParserException;use Mdanter\Ecc\Crypto\Signature\Signature;use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;use PHPUnit\Framework\TestCase;use Rtgm\sm\RtSm2;use Rtgm\sm\RtSm3;use Rtgm\sm\RtSm4;/** * 国密加密测试 * sm4 已与兴业银行调通 */class GmTest extends TestCase{ /** * 获取 sm2 * @return RtSm2 */ private function getSm2(): RtSm2 { return new RtSm2('base64'); } /** * 获取 sm3 * @return RtSm3 */ private function getSm3(): RtSm3 { return new RtSm3(); } /** * 获取 sm4 * @return RtSm4 */ private function getSm4(): RtSm4 { $privateKey = $this->getSm4PrivateKey(); return new RtSm4($privateKey); } /** * 获取 16进制 sm2 密钥 * 生成于工具站 https://www.lzltool.com/SM2 base64格式 * @return string */ private function getSm2PrivateKey(): string { return bin2hex(base64_decode('L8TbMByc+rQmKECWMBjnDQHrXrExqZKdl5S6sBbP07M=')); } /** * 获取 16进制 sm2 公钥 * 生成于工具站 https://www.lzltool.com/SM2 base64格式 * @return string */ private function getSm2PublicKey(): string { return bin2hex(base64_decode('BCxc4cDX1OQEpCD8O7wzPhTOljYg0uzfsMAEanCvYgBIj966+i5pgjwyIOtFSNWLWjoDzLmMJP9nf2cVmiH+aYI=')); } /** * sm2 数据格式化 * @param $dec * @return string */ private function sm2FormatHex($dec): string { $hex = gmp_strval(gmp_init($dec, 10), 16); $len = strlen($hex); if ($len == 64) { return $hex; } return $len < 64 ? str_pad($hex, 64, "0", STR_PAD_LEFT) : substr($hex, $len - 64, 64); } /** * 获取 16位 密钥 * @return bool|string */ private function getSm4PrivateKey(): bool|string { return base64_decode('NmQzZDQ2YTcxMmRjNGE0NQ=='); } /** * 获取待加密字符串 * @return string */ private function getDataStr(): string { return '{"bankCardNo":"6212028190240439021","certNo":"41052619700925136X","userName":"南瓜"}'; } /** * 拼接 sm2 待加密字符串 * @return bool|string */ private function getSm2SignStr(): bool|string { $params = json_decode($this->getDataStr(), true); $signStr = ''; if ($params != null) { ksort($params); foreach ($params as $k => $v) { $signStr .= "{$k}={$v}&"; } } return substr($signStr, 0, strlen($signStr) - 1); } public function test_sm2_sign() { $sm2 = $this->getSm2(); $signStr = $this->getSm2SignStr(); // 加密 $sign = $sm2->doSign($signStr, $this->getSm2PrivateKey()); $encryptStr = base64_decode($sign); try { $a = ASNObject::fromBinary($encryptStr)->getChildren(); } catch (ParserException $e) { $this->fail('加密失败: ' . $e->getMessage()); } $aa = $this->sm2FormatHex($a[0]->getContent()); $bb = $this->sm2FormatHex($a[1]->getContent()); $encryptStr = base64_encode(hex2bin($aa . $bb)); $this->assertNotEmpty($encryptStr); return $encryptStr; } public function test_sm2_verify_sign() { $sm2 = $this->getSm2(); $encryptSignStr = bin2hex(base64_decode($this->test_sm2_sign())); echo 'sm2 sign str: ', $encryptSignStr, PHP_EOL; $r = substr($encryptSignStr, 0, 64); $s = substr($encryptSignStr, 64, 64); $r = gmp_init($r, 16); $s = gmp_init($s, 16); $signature = new Signature($r, $s); $serializer = new DerSignatureSerializer(); $sign = base64_encode($serializer->serialize($signature)); $boolean = $sm2->verifySign($this->getSm2SignStr(), $sign, $this->getSm2PublicKey()) ?? false; echo $boolean ? 'sm2 验签通过' : 'sm2 验签失败', PHP_EOL; $this->assertTrue($boolean); } public function test_sm2_encrypt() { $sm2 = $this->getSm2(); // 压缩公钥 $key = $this->decompressPublicKey('0315edd9126410e9b94b83ee2bcdfeebe9166e84d7aad1b9d16fa923995d28e81f'); $encrypt = $sm2->doEncrypt($this->getDataStr(), $key); $this->assertNotEmpty($encrypt); return $encrypt; } public function test_sm2_decrypt() { $sm2 = $this->getSm2(); $encrypt = $this->test_sm2_encrypt(); $privateKey = 'bf5e3e47e5392a8cdba8e3f854db2d3f5e2c536235303a02898b58d085a8246a'; echo 'sm2 encrypt str: ', $encrypt, PHP_EOL; $decryptStr = $sm2->doDecrypt($encrypt, $privateKey); echo 'sm2 decrypt str: ', $decryptStr, PHP_EOL; $this->assertNotEmpty($decryptStr); $this->assertTrue($decryptStr === $this->getDataStr()); } /** * 获取未压缩公钥 * @param $compressedKey * @return string|null */ function decompressPublicKey($compressedKey): ?string { // 获取压缩标志和X坐标 $flag = substr($compressedKey, 0, 2); $x = substr($compressedKey, 2); // 将16进制字符串转换为大整数 $p = gmp_init('FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF', 16); $a = gmp_init('FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC', 16); $b = gmp_init('28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93', 16); $gx = gmp_init('32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1711D7AFB1B8B4E16', 16); $gy = gmp_init('BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0', 16); $n = gmp_init('FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123', 16); // 计算Y坐标 $x = gmp_init($x, 16); $alpha = gmp_powm($x, 3, $p); $beta = gmp_add(gmp_mod(gmp_mul($a, $x), $p), $b); $y2 = gmp_mod(gmp_add($alpha, $beta), $p); $y = gmp_powm($y2, gmp_div_q(gmp_add($p, 1), 4), $p); if ($flag == "02") { // 如果压缩标志为 02,则Y坐标为偶数 if (gmp_strval(gmp_mod($y, 2)) != "0") { $y = gmp_sub($p, $y); } return "04" . gmp_strval($x, 16) . str_pad(gmp_strval($y, 16), 64, "0", STR_PAD_LEFT); } if ($flag == "03") { // 如果压缩标志为 03,则Y坐标为奇数 if (gmp_strval(gmp_mod($y, 2)) != "1") { $y = gmp_sub($p, $y); } return "04" . gmp_strval($x, 16) . str_pad(gmp_strval($y, 16), 64, "0", STR_PAD_LEFT); } return null; } public function test_sm3() { $sm3 = $this->getSm3(); $signStr = $sm3->digest($this->getDataStr()); echo 'sm3 sign str: ', $signStr, PHP_EOL; $this->assertNotEmpty($signStr); } /** * 测试 byteArr to string * @return string */ public function test_sm4_iv() { $byteArr = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; $iv = call_user_func_array('pack', array_merge(['C*'], $byteArr)); $this->assertTrue(base64_encode($iv) === 'AAAAAAAAAAAAAAAAAAAAAA=='); return base64_encode($iv); } public function test_sm4_encrypt() { try { $iv = $this->test_sm4_iv(); $sm4 = $this->getSm4(); $encryptJsonStr = $sm4->encrypt($this->getDataStr(), 'sm4', base64_decode($iv)); } catch (\Exception $e) { $this->fail('加密异常:' . $e->getMessage()); } $encryptJsonStr = base64_encode(hex2bin($encryptJsonStr)); $this->assertNotEmpty($encryptJsonStr); return $encryptJsonStr; } public function test_sm4_decrypt() { try { $sm4 = $this->getSm4(); $encryptJsonStr = $this->test_sm4_encrypt(); echo "sm4 encrypt str: " . $encryptJsonStr, PHP_EOL; $decryptJsonStr = $sm4->decrypt(bin2hex(base64_decode($encryptJsonStr)), 'sm4', base64_decode($this->test_sm4_iv())); } catch (\Exception $e) { $this->fail('解密异常:' . $e->getMessage()); } echo "sm4 decrypt str: " . $decryptJsonStr, PHP_EOL; echo $decryptJsonStr === $this->getDataStr() ? 'sm4 数据一致' : 'sm4 数据不一致', PHP_EOL; $this->assertNotEmpty($decryptJsonStr); }}

2021-05-11
centos php7.4 安装记录
最近记性不太好,做个记录,防止下次忘了。 若果有遗漏,之后再补充。 更新1yum update && yum upgrade 安装 php7.4 的 yum 源12345yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpmyum -y install https://rpms.remirepo.net/enterprise/remi-release-7.rpmyum -y install yum-utils 安装 php 7.412yum-config-manager --enable remi-php74yum install php php-cli php-fpm php-mysqlnd php-zip php-devel php-gd php-mcrypt php-mbstring php-curl php-xml php-pear php-bcmath php-json 安装新版 mariadb先加个源: 1234567vim /etc/yum.repos.d/mariadb.repo[mariadb]name = MariaDBbaseurl = https://mirrors.tuna.tsinghua.edu.cn/mariadb/yum/10.5/centos7-amd64/gpgkey = https://mirrors.tuna.tsinghua.edu.cn/mariadb/yum/RPM-GPG-KEY-MariaDBgpgcheck = 1 安装服务: 1234yum clean allyum makecacheyum install mariadb-server 初始化密码: 敲回车的时候注意别把用户远程登录给禁了!!! 1mysql_secure_installation 安装 nginx1yum install nginx 启动服务1234systemctl start nginxsystemctl start phpsystemctl start php-fpmsystemctl start mariadb

2018-02-08
openssl_pkey_get_private 内存泄漏
从前,有一块使用 swoole_process 做多线程处理的程序,这个程序有一个奇怪的问题:每运行一个星期左右都会因为内存爆表而停掉。 因为爆表的时间关系,开发的小伙伴们都认为是这段使用 swoole_process 的程序有问题,几经更新改版,代码已经趋于完美,平稳的运行了几天后,小伙伴们终于都松了一口气,终于解决了这个问题。 然而,伴随着百年难遇的血月,这块程序还是在一个意想不到的时间又爆了一次,小伙伴们终于意识到,问题的根本并不在这里,想要找出并改掉这个问题,是何等艰难!但是他们知道,真相只有一个! 又经过几天的排查,凶手终于浮出水面! 呐,就是这孙子:openssl_pkey_get_private 抽出的测试代码: 123456789while(true){ $key = file_get_contents('mock_A.pem'); echo $m=memory_get_usage(),PHP_EOL; $getkey = openssl_pkey_get_private($key, ''); openssl_pkey_free($getkey); echo $mm = memory_get_usage(),PHP_EOL; echo "Before unset: ",$mm-$m ,PHP_EOL; sleep(3);} 结果: 审判结果:该阉的就赶紧阉了吧 行刑后长相: 12345678910$key = file_get_contents('mock_A.pem');$getkey = openssl_pkey_get_private($key, '');while(true){ echo $m=memory_get_usage(),PHP_EOL; openssl_pkey_free($getkey); echo $mm = memory_get_usage(),PHP_EOL; echo "Before unset: ",$mm-$m ,PHP_EOL; sleep(3);} 前线专家点评:纯属冤案,哪儿特么这么容易泄漏,都是因为自己写的不规范! 注:生成公私钥是需要消耗内存的,每次脚本执行结束后会释放掉,但是常驻内存的脚本没有释放的机会,所以每次循环调用都会造成内存增加,持续增加到一定量后系统也就崩了。所以解决办法是在循环开始之前就把密钥生成好。

2018-06-12
Laravel使用ftp传输文件时报错ftp_put() No data connection的解决
代码: 123456789101112131415161718192021222324252627282930313233<?php$file = "test.dat";$ftp_server="ftp.server.com";$ftp_user = "myname";$ftp_pass = "mypass";$destination_file = "test.dat";$cid=ftp_connect($ftp_server);if(!$cid) { exit("Could not connect to server: $ftp_server\n");}$login_result = ftp_login($cid, $ftp_user, $ftp_pass);if (!$login_result) { echo "FTP connection has failed!"; echo "Attempted to connect to $ftp_server for user $ftp_user"; exit;} else {echo "Connected to $ftp_server, for user $ftp_user";}$upload = ftp_put($cid, $destination_file, $file, FTP_BINARY);if (!$upload) { echo "Failed upload for $source_file to $ftp_server as $destination_file<br>"; echo "FTP upload has failed!";} else { echo "Uploaded $source_file to $ftp_server as $destination_file";}ftp_close($cid);?> 原因是没有定义ftp的主被动模式,true是被动模式: 1ftp_pasv($cid, true);

2025-01-24
系统异常崩溃实录
前言众所周不知,我在 24 年底入职了某连锁品牌的美甲公司,负责相关小程序和后台的开发与维护。 入职前了解到该项目最初由外包团队开发,并使用外包三件套:宝塔、TP、Mysql 进行部署和搭建,当时我心里就对它有了一个大概的印象,但是等我真正接手这个项目时,还是忍不住地两眼一黑,心头有一万头草泥马奔腾而过。 项目结构之混乱,方法定义之奇葩,没有一项不在挑战我认知的下限,我只能说,用屎山来形容这套代码都是在夸它,项目能平稳运行简直就是个奇迹。 哦,也不能算奇迹,因为这勾八玩意儿就没有一天是平稳的。 在告别了手动替换服务器代码并用 git 管理之后,我又先后经历了 CDN 欠费,小程序图片无法正常显示;SSL 证书过期,所有服务全部宕机;以及子项目域名过期却拿不到平台账号,最后只能换绑域名这样的种种混乱…… 在此之后,系统总算平稳运行了几天。 屎山的崩塌正当我撸起袖子,准备奋力重构这坨屎山时,年底到了,我又迎来了新一波的挑战。 1 月 16 号早上 10 点,我把手里刚开发完的小程序推送到正式版,之所以选在这个时间更新,是因为有些门店会营业到凌晨5点,10 前的使用量相对还少一些。 本来只是一个简单的更新操作,但是让我没想到的是,刚发版没一会儿,运营就突然在群里反馈说小程序卡,很卡,非常卡。 我被这消息搞得一头雾水,貌似刚才也没写 bug 吧? 掏出手机操作一下,发现确实不能正常使用,阿西吧,赶紧联系同事撤回了版本。 这回总好了吧?掏出手机一看,还是不正常,群里也还在不停反馈依然无法登录等各种问题,傻眼的我更傻眼了。 打开服务器上的宝塔面板,两眼又是一黑,负载已经飙到了 100%,cpu 也到干了 94.5%。 wtf?这可是三台配置都不算低的服务器,按照上家公司的业务量,这个配置估计只需要一台就足够了。 我赶紧打开 nginx 的 access 日志,结果发现啥也没有,因为配置文件里压根儿就没有记录日志的代码。 再一问同事,之前服务器硬盘被日志塞爆过,没人清理,后来索性就都关了。 我无语凝噎。只能硬着头皮继续分析,发现负载和 cpu 飙升的原因是有几个 php-fpm 进程占用特别高。 它们在干啥?为什么会飙这么高? 正当我们抓耳挠腮,掉了无数头发时,又在云服务器后台发现前一天 0 点也有同样的情况。 再往前一翻,清一色的 0 点和 10 点各项指标飞速飙升,0 点的飙升像座小山,10 点的飙升像座大山。 wō ní mǎ。 开始抓虫我慌了,开始怀疑起这个项目有异常的定时任务。 我的怀疑是有根据的,因为前一波开发的大聪明在调接口时,借助框架的事件驱动模块,用嵌套了两层的 http 接口调用一个所谓的 Task 接口,然后触发了 1000 多个 event…… 为什么要套两层接口,我不知道,为什么要这样写,我也不知道。 我只知道本地的接口被这玩意儿拖到了 6 秒一个请求,开发需求时我都是直接把这行代码注释的。 事情到了这个节骨眼上,我只能硬着头皮研究了一下这块代码,然后清除了里边 90% 左右的无用事件,然后又仔细检查了服务器的 crontab,emmm,就没有在 0 点和 10 点运行的任务。 查到这里事情还是没有大的进展,我的心态已经开始波动了,这屌系统崩就崩吧,这屌工作要不就干到今天算了吧?要不等过了年,我再出去找找其他工作? 正当我想要打开 boss 直骗,给自己发昏的大脑来上一套强制冷却时,系统又诡异地恢复了平静。 我挠着头,一边感叹自己对这个屌系统的认知还是太过于浅薄,一边思考为什么会出现这样的局面,这里边还有没有什么其他值得怀疑的对象。 这时候,运营又在群里反馈了其他问题,我看这一波飙升已经过去,下一波也还很遥远,也就先放下了这件事,先去处理群里的其他问题。 时间一晃到了下午,闲下来的我和同事又发现数据库有很多慢 sql,其中一个最夸张的甚至运行了 126 秒。 是它吧?一定是,要不然还能是谁呢? 我和同事马上开工,把系统里的慢 sql 都给优化了一遍,然后又给服务器装了 Atop 做监控,信心满满地等着 0 点时验证一波。 时间再次来到 0 点,服务器准时出现波动,但是只有 10 秒。 我先打开慢 sql 日志,完美,没有慢sql。再一看 Atop,10 分钟一次的记录完美避开的案发现场。 这样下去明天 10 点铁定得崩,那咋办?还有台闲置的服务器,加上去吧,看看后台,还有门店在使用系统,那只能等明天早上再加。 跟同事约好第二天早上 5 点半起来操作,我估摸这要不把 access 日志打开,看看涌进来的请求是不是都正常,因为这个时候已经开始怀疑是不是被人攻击了。 第二天一早,我们爬起来先把昨天推送失败的小程序再次更新,验证完之后又把新服务器加进了负载,忙完以后定了个 9 点多的闹铃,然后赶紧躺下又眯了一会儿。 不到 10 点,又被群里的消息吵醒,看完发现是前一天优化的 一条 sql 有问题,没有正常生成数据,快 10 点了,我们把问题先丢在一边,先盯着监控看服务器状态。 结果很快再次打脸,新增的服务器虽然也扛住了很多并发,但系统还是崩了,大量用户无法排队,大量门店也无法叫号,这次挣扎再次以失败告终。 我把 access 日志拉了出来,逐行分析了上千行日志,发现事情并没有那么简单,这些请求似乎都很正常,难道攻击我们的还是个高手,伪装成了正常用户? 我统计了每个接口的访问数量,又统计了每个 token 访问的接口数量,还解析了几个用户的 token。但是,这些东西还是没有任何帮助,我没有找到任何被攻击的证据。 事情再一次陷入僵局。 令人无语的真相时间再次来到 0 点,我们提前停下了所有定时任务,系统不出所料,还是全线飘红,不过我们好在知道了问题的根源是在数据库。 但是为什么会在 0 点和 10 点这两个时间涌入大量的请求,我们还是怀疑是不是有人在攻击我们,苦于没有证据,只能继续分析日志。 我们发现在 0 点的一瞬间,系统涌入大量的排队操作,其他调查均无成效的情况下,我们只能统计了 0 点后每家门店的排队情况,打算第二天联系门店核实一下这些排队是否真实,同事也把我们统计的表格发给了老板。 每个人的心头都像压了一座大山,怎么办,再不搞定这个年还能过好吗? 第二天浑浑噩噩地爬起床,灌下一杯超浓咖啡,我带着深深的忧虑再次上班。 路上,同事发来一张聊天截图,老板说,这些是正常的,很多门店就是 0 点开始放号,因为每个门店的排号是有限的,顾客只能在放号的一瞬间去抢,包括十点也是一样的情况。 我觉得天塌了,忍不住骂一声他喵的,合着我们起早贪黑辛苦调查,最后调查了个寂寞,系统宕机纯粹就是因为瞬时并发太大??? wtf!这个世界是真的魔幻啊,做个美甲还要 0 点起来抢号? 波折的解决之路时间一晃又到 0 点,我们白天把排队叫号相关的接口逐个优化,该加缓存的也都加上了缓存,同事也研究起数据库的从库和代理,等着 0 点验证一下能否扛住压力。 然后,我们卡在了第一步,读库连不上去,服务器方面,靠着手动切换负载,硬是扛住了这一波。 再一看日志,排号接口平均在 20 毫秒,叫号接口平均在 250 毫秒,但是读库的内存才使用了 17%,像极了你那个出工不出力天天在摸鱼的老油条同事。 阿西吧,这可不行。 读库可是按小时收费的,扛过了零点高峰,我们马上下了,准备第二天早上继续研究。 第二天一早又把读库配好,也让运营通知了门店错峰放号,我紧盯着监控,准备随时切换负载分配,同事那边也在联系服务器客服,让他们远程协助,解决读库不发力的问题。 但是很遗憾,10 点前读库的问题还是没能解决,服务器小崩了一下。 时间已经到了周日,距离放假还有一周,距离第一次触发这种情况已经过去一周还要多,再不解决,真就过不好这个年了呗。 虽然这是个从系统诞生就已经存在的问题,但在业务高峰期频繁触发实在打脸,老板拉我们开了个会,说以前的人是把服务器的流量降下来,听得我们目瞪狗呆,这是什么傻逼骚操作?降流量,咋不把服务关了呢。 一番拉扯后,我们决定给接口限流,10-10:30 期间,每分钟放出 100 个排号,超出就等下一波。 方法测完上线后,我们终于迎来了第一个好消息,读库能连上了。 当即选了一台服务器切了过去,然后进行了一波测试,发现读库还是有点慢,有点鸡肋。 同事再次提了工单,在对方的大力协助下,我们升级了读库和代理的配置,几乎是在一瞬间,主库的 cpu 睡着了。 0 点后,qps 一路狂飙到了 18000/s,服务器的负载虽然又到了 100%,但是 cpu 一直维持在 80% 左右,这件事总算成了。 我特么能睡个好觉了……