tech| 技術(shù)分享: 腳本慢如何優(yōu)化?

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)

fork 系統(tǒng)調(diào)用

#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ì)列
rabbitmq_queue

物力

為何會(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ū)怪》篇末站楚,有“異史氏曰:黃貍黑貍单起,得鼠者雄”!

deng_cat

比如上面手機(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%';
navicat_keepalive

代碼中:

  • 最簡(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ū)怪》篇末蛀柴,有“異史氏曰:黃貍黑貍螃概,得鼠者雄”!

推薦閱讀:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸽疾,一起剝皮案震驚了整個(gè)濱河市吊洼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌制肮,老刑警劉巖冒窍,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異豺鼻,居然都是意外死亡综液,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門儒飒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谬莹,“玉大人,你說(shuō)我怎么就攤上這事桩了「矫保” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵井誉,是天一觀的道長(zhǎng)蕉扮。 經(jīng)常有香客問(wèn)我,道長(zhǎng)颗圣,這世上最難降的妖魔是什么慢显? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任爪模,我火速辦了婚禮欠啤,結(jié)果婚禮上荚藻,老公的妹妹穿的比我還像新娘。我一直安慰自己洁段,他們只是感情好应狱,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著祠丝,像睡著了一般疾呻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上写半,一...
    開(kāi)封第一講書(shū)人閱讀 49,929評(píng)論 1 290
  • 那天岸蜗,我揣著相機(jī)與錄音,去河邊找鬼叠蝇。 笑死璃岳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悔捶。 我是一名探鬼主播铃慷,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蜕该!你這毒婦竟也來(lái)了犁柜?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤堂淡,失蹤者是張志新(化名)和其女友劉穎馋缅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體绢淀,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萤悴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了更啄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稚疹。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖祭务,靈堂內(nèi)的尸體忽然破棺而出内狗,到底是詐尸還是另有隱情,我是刑警寧澤义锥,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布柳沙,位于F島的核電站,受9級(jí)特大地震影響拌倍,放射性物質(zhì)發(fā)生泄漏赂鲤。R本人自食惡果不足惜噪径,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望数初。 院中可真熱鬧找爱,春花似錦、人聲如沸泡孩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)仑鸥。三九已至吮播,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間眼俊,已是汗流浹背意狠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疮胖,地道東北人环戈。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像获列,于是被迫代替她去往敵國(guó)和親谷市。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容