date: 2018-10-16 15:38:04
title: tech| 技術(shù)分享: 腳本慢如何優(yōu)化?
業(yè)務(wù)上有很多功能通過(guò)后臺(tái)腳本運(yùn)行, 有時(shí)會(huì)遇到 腳本還沒(méi)跑完, 又要加班了 這種情況, 這篇聊聊腳本優(yōu)化提速的話題.
優(yōu)化的 2 個(gè)大方向:
- 物力
- 人力
先上干貨, 聊聊物力, 怎么想辦法發(fā)揮出計(jì)算機(jī)的性能, 物力優(yōu)化常見(jiàn)的有 2 方面:
- 多進(jìn)程
- 多協(xié)程
在繼續(xù)下面的內(nèi)容之前, 請(qǐng)確保你熟悉這些基礎(chǔ)知識(shí):
- unix系統(tǒng)架構(gòu)圖
- 系統(tǒng)調(diào)用和庫(kù)函數(shù)
- 用戶態(tài)和內(nèi)核態(tài)
- 程序是如何執(zhí)行的
- 進(jìn)程 線程 協(xié)程
這篇文章非常不錯(cuò), 推薦閱讀
編程基礎(chǔ)知識(shí): https://mp.weixin.qq.com/s/nxdFeLGGQLgBcy5zq5rsvw
進(jìn)程 線程 協(xié)程
- 什么是進(jìn)程
進(jìn)程就是運(yùn)行著的程序.
// test.php
<?php
sleep(100);
查看進(jìn)程:
/var/www/coding/php # php test.php &
/var/www/coding/php # ps aux
PID USER TIME COMMAND
156 root 0:00 php test.php
157 root 0:00 ps aux
- 什么是線程
線程是操作系統(tǒng)的最小執(zhí)行單位, 真正干活的不是進(jìn)程, 而是進(jìn)程中的線程
- 什么是協(xié)程
非常形象的說(shuō)法: 用戶態(tài)線程. 為什么協(xié)程比線程快, 下面還會(huì)講到.
思考一個(gè)問(wèn)題: 使用協(xié)程的時(shí)候, 到底是誰(shuí)在干活?
多進(jìn)程
基礎(chǔ)實(shí)現(xiàn)
#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid; //fpid表示fork函數(shù)返回的值
int count=0;
fpid=fork();
if (fpid < 0)
printf("error in fork!");
else if (fpid == 0) {
printf("i am the child process, my process id is %d/n",getpid());
printf("我是爹的兒子/n");//對(duì)某些人來(lái)說(shuō)中文看著更直白堰氓。
count++;
}
else {
printf("i am the parent process, my process id is %d/n",getpid());
printf("我是孩子他爹/n");
count++;
}
printf("統(tǒng)計(jì)結(jié)果是: %d/n",count);
return 0;
}
運(yùn)行結(jié)果:
i am the child process, my process id is 5574
我是爹的兒子
統(tǒng)計(jì)結(jié)果是: 1
i am the parent process, my process id is 5573
我是孩子他爹
統(tǒng)計(jì)結(jié)果是: 1
是不是很神奇, if-else 代碼塊都執(zhí)行了
PHP 中多進(jìn)程相關(guān)擴(kuò)展: pcntl/posix
更多 PHP 中多進(jìn)程編程的知識(shí): rango blog
更簡(jiǎn)單的方式
swoole 的協(xié)程池, 輕松開(kāi)啟多進(jìn)程:
// 進(jìn)程數(shù)
$pool = new \Swoole\Process\Pool($workNum);
$pool->on('workerStart', function ($pool, $workerId) {
// 業(yè)務(wù)邏輯
});
$pool->start();
具體實(shí)例:
public function actionOpinionMq($workNum = 1)
{
$pool = new Pool($workNum);
$pool->on('workerStart', function ($pool, $workerId) {
$callback = function (AMQPMessage $msg) {
$msgBody = $msg->body;
$this->info($msgBody);
$msgBody = json_decode($msgBody, true);
if (isset($msgBody['id'])) {
$row = Yii::$app->getDb()->createCommand("SELECT id,content FROM opinion WHERE source_id='{$msgBody['id']}'")->queryOne();
if ($row) {
// 業(yè)務(wù)代碼
}
}
/** @var AMQPChannel $ch */
$ch = $msg->delivery_info['channel'];
$ch->basic_ack($msg->delivery_info['delivery_tag']);
};
$connection = new AMQPStreamConnection('rabbitmq', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('crawl-opinion', false, false, false, false);
$channel->basic_qos(null, 1, null);
$channel->basic_consume('crawl-opinion', '', false, false, false, false, $callback);
while (count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$connection->close();
});
$pool->start();
}
多協(xié)程
協(xié)程為什么快
- 用戶態(tài) 內(nèi)核態(tài)
- cpu密集型 IO密集型
$n = 4;
// 普通版
for ($i = 0; $i < $n; $i++) {
sleep(1);
echo microtime(true) . ": hello $i \n";
};
// 單協(xié)程
go(function () use ($n) {
for ($i = 0; $i < $n; $i++) {
Co::sleep(1);
echo microtime(true) . ": hello $i \n";
};
});
for ($i = 0; $i < $n; $i++) {
go(function () use ($i) {
Co::sleep(1);
// sleep(1);
echo microtime(true) . ": hello $i \n";
});
};
推薦這篇文章對(duì)協(xié)程的解讀: swoole| swoole 協(xié)程初體驗(yàn)
swoole 現(xiàn)在支持原生 mysql/redis 無(wú)縫切換到協(xié)程
業(yè)務(wù)中的一次實(shí)踐, 更新現(xiàn)有手機(jī)號(hào)的運(yùn)營(yíng)商信息:
for ($i=0; $i< 500; $i++) {
go(function () use ($i, $sms_job_id){
// 取模進(jìn)行任務(wù)分片
$sql = "SELECT id,phone FROM sms_job_phone WHERE sms_job_id=$sms_job_id AND ops_type=0 and id%500={$i} LIMIT 500";
$rows = static::getDb()->createCommand($sql)->queryAll();
foreach ($rows as $row) {
echo $row['id'], "\n";
$m = static::findOne($row['id']);
// 判斷用戶手機(jī)號(hào)運(yùn)營(yíng)商
$m->ops_type = Helper::getMobileOperator($row['phone']);
$m->save();
}
});
}
怎么把任務(wù)拆成多個(gè)
上面出現(xiàn)過(guò)的案例:
- sql 中 取模 / 分段(
id>:id limit 1000
) - 消息隊(duì)列
物力
為何會(huì)優(yōu)先劃分 人力/物力 這 2 個(gè)范疇呢? 因?yàn)榻^大多數(shù)場(chǎng)景, 并不需要去榨干計(jì)算機(jī)性能. 反而是編程時(shí)沒(méi)有采取一些 最佳實(shí)踐 或者一些 失誤 導(dǎo)致程序運(yùn)行的效果 比較糟糕.
開(kāi)發(fā)規(guī)范
歷史總是驚人的相似, 積累一些開(kāi)發(fā)規(guī)范, 可以少踩一些坑
案例分享: 變通思路
《聊齋志異》手稿本卷三《驅(qū)怪》篇末站楚,有“異史氏曰:黃貍黑貍单起,得鼠者雄”!
比如上面手機(jī)運(yùn)營(yíng)商的例子, 開(kāi)了 500 協(xié)程還是很慢, 3w 數(shù)據(jù)超過(guò) 10 分鐘才執(zhí)行完, 還能更快一些么? 可以, 業(yè)務(wù)中直接讀取的本地文件.
out of memory
有個(gè)定時(shí)腳本從 mongo 中取數(shù)據(jù)進(jìn)行處理, 循環(huán)到數(shù)據(jù)處理完, 上線后執(zhí)行一段時(shí)間報(bào)錯(cuò): out of memory
-
ini_set('memory_limit', '512m');
調(diào)整腳本運(yùn)行內(nèi)存限制, 執(zhí)行一段時(shí)間, 依舊報(bào)錯(cuò) -
memory_get_usage() / memory_get_peak_usage()
添加日志, 記錄腳本內(nèi)存使用, 定位問(wèn)題, 發(fā)現(xiàn)每條需要處理的數(shù)據(jù)超過(guò) 20m -
gc_collect_cycles() / unset()
添加強(qiáng)制 gc, 主動(dòng)回收變量, 有效果, 但是內(nèi)存依舊會(huì)持續(xù)增長(zhǎng) -
0 22 * * *
->* 22 * * *
+limit 50
原腳本每天 22 點(diǎn)執(zhí)行一次, 改成每天 22 點(diǎn)每分鐘執(zhí)行一次, 每次只處理 50 條數(shù)據(jù)
connection gone away
有個(gè)統(tǒng)計(jì)腳本統(tǒng)計(jì)比較復(fù)雜, 需要統(tǒng)計(jì)三層數(shù)據(jù): 查詢出第一層數(shù)據(jù), 統(tǒng)計(jì)后, 拿獲取的數(shù)據(jù)再去查詢, 查詢后再統(tǒng)計(jì), 比如 一定維度的用戶 + 這些用戶相關(guān)的訂單 + 這些訂單相關(guān)的賬單, 測(cè)試時(shí)發(fā)現(xiàn)腳本運(yùn)行一段時(shí)間后報(bào)錯(cuò) connection gone away
關(guān)于連接超時(shí):
- Navicat 中的 keepalive
- mysql 中 timeout 相關(guān)配置
SHOW VARIABLES LIKE '%timeout%';
代碼中:
- 最簡(jiǎn)單的例子,
mysqli_ping()
// 進(jìn)程長(zhǎng)時(shí)間運(yùn)行時(shí)的長(zhǎng)連接
if ($this->_linkr && mysqli_ping($this->_linkr)) {
$this->_link = $this->_linkr;
return true;
}
$this->_linkr = $this->_connect($host);
- yii 框架中
vendor/yiisoft/yii2/db/Connection.php
/**
* Returns a value indicating whether the DB connection is established.
* @return bool whether the DB connection is established
*/
public function getIsActive()
{
return $this->pdo !== null;
}
common/helpers/GlobalHelper.php
/**
* connect db 重新連接數(shù)據(jù)庫(kù)
* @param string $db
* @return \yii\db\Connection 返回?cái)?shù)據(jù)庫(kù)操作句柄
*/
public static function connectDb($db = 'db') {
$db_handle = ($db instanceof \yii\db\Connection) ? $db : \yii::$app->$db;
if (empty($db_handle)) {
return false;
}
try {
return $db_handle->createCommand("select 1")->queryScalar();
}
catch (\Exception $e){
$db_handle->close();
$db_handle->open();
}
}
- 給報(bào)錯(cuò)的代碼打上補(bǔ)丁(偽代碼)
// 獲取指定用戶
// 可能在這里斷開(kāi)連接, 打上補(bǔ)丁
GlobalHelper::connectDb();
$data1 = $db->execute($sql_user);
foreach ($data1 as $v1) {
$data2 = $db->execute($sql_order);
foreach ($data2 as $v2) {
$data3 = $db->execute($sql_bill);
}
}
問(wèn)題依舊存在, 只是變成了: 補(bǔ)丁要打在哪里
- 現(xiàn)實(shí)中的代碼比這個(gè)更復(fù)雜, 給每個(gè) db 連接的地方都打補(bǔ)丁?
- 直接改動(dòng)框架底層, 風(fēng)險(xiǎn)怎么評(píng)估?
那問(wèn)題怎么解決呢? 同樣的套路: limit 數(shù)據(jù)量 + 多次執(zhí)行.
寫在最后
提升技能的過(guò)程中, 不妨多掌握一些 套路:
- 積累/制定 相應(yīng)技術(shù)的開(kāi)發(fā)規(guī)范: 歷史總是驚人的相似
- 有些知識(shí)知道了(理解了)就很簡(jiǎn)單, 沒(méi)那么玄乎
- 《聊齋志異》手稿本卷三《驅(qū)怪》篇末蛀柴,有“異史氏曰:黃貍黑貍螃概,得鼠者雄”!
推薦閱讀:
- 編程基礎(chǔ)知識(shí)
- 多進(jìn)程編程 - fork 系統(tǒng)調(diào)用
- 服務(wù)器編程 - rango blog
- 推薦這篇文章對(duì)協(xié)程的解讀: swoole| swoole 協(xié)程初體驗(yàn)