tech| 技术分享: 脚本慢如何优化?

date: 2018-10-16 15:38:04
title: tech| 技术分享: 脚本慢如何优化?

业务上有很多功能通过后台脚本运行, 有时会遇到 脚本还没跑完, 又要加班了 这种情况, 这篇聊聊脚本优化提速的话题.

优化的 2 个大方向:

  • 物力
  • 人力

先上干货, 聊聊物力, 怎么想办法发挥出计算机的性能, 物力优化常见的有 2 方面:

  • 多进程
  • 多协程

在继续下面的内容之前, 请确保你熟悉这些基础知识:

  • unix系统架构图
  • 系统调用和库函数
  • 用户态和内核态
  • 程序是如何执行的
  • 进程 线程 协程

这篇文章非常不错, 推荐阅读

编程基础知识: https://mp.weixin.qq.com/s/nxdFeLGGQLgBcy5zq5rsvw

进程 线程 协程

  • 什么是进程

进程就是运行着的程序.

// test.php
<?php
sleep(100);

查看进程:

/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
  • 什么是线程

线程是操作系统的最小执行单位, 真正干活的不是进程, 而是进程中的线程

  • 什么是协程

非常形象的说法: 用户态线程. 为什么协程比线程快, 下面还会讲到.

思考一个问题: 使用协程的时候, 到底是谁在干活?

多进程

基础实现

fork 系统调用

#include <unistd.h>
#include <stdio.h>
int main ()
{
    pid_t fpid; //fpid表示fork函数返回的值
    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");//对某些人来说中文看着更直白。
        count++;
    }
    else {
        printf("i am the parent process, my process id is %d/n",getpid());
        printf("我是孩子他爹/n");
        count++;
    }
    printf("统计结果是: %d/n",count);
    return 0;
}

运行结果:

i am the child process, my process id is 5574
我是爹的儿子
统计结果是: 1
i am the parent process, my process id is 5573
我是孩子他爹
统计结果是: 1

是不是很神奇, if-else 代码块都执行了

PHP 中多进程相关扩展: pcntl/posix

更多 PHP 中多进程编程的知识: rango blog

更简单的方式

swoole 的协程池, 轻松开启多进程:

// 进程数
$pool = new \Swoole\Process\Pool($workNum);
$pool->on('workerStart', function ($pool, $workerId) {
    // 业务逻辑
});
$pool->start();

具体实例:

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) {
                    // 业务代码
                }
            }
            /** @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();
}

多协程

协程为什么快

  • 用户态 内核态
  • cpu密集型 IO密集型
$n = 4;
// 普通版
for ($i = 0; $i < $n; $i++) {
    sleep(1);
    echo microtime(true) . ": hello $i \n";
};
// 单协程
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";
    });
};

推荐这篇文章对协程的解读: swoole| swoole 协程初体验

swoole 现在支持原生 mysql/redis 无缝切换到协程

业务中的一次实践, 更新现有手机号的运营商信息:

for ($i=0; $i< 500; $i++) {
    go(function () use ($i, $sms_job_id){
        // 取模进行任务分片
        $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']);
            // 判断用户手机号运营商
            $m->ops_type = Helper::getMobileOperator($row['phone']);
            $m->save();
        }
    });
}

怎么把任务拆成多个

上面出现过的案例:

  • sql 中 取模 / 分段(id>:id limit 1000)
  • 消息队列
rabbitmq_queue

物力

为何会优先划分 人力/物力 这 2 个范畴呢? 因为绝大多数场景, 并不需要去榨干计算机性能. 反而是编程时没有采取一些 最佳实践 或者一些 失误 导致程序运行的效果 比较糟糕.

开发规范

历史总是惊人的相似, 积累一些开发规范, 可以少踩一些坑

案例分享: 变通思路

《聊斋志异》手稿本卷三《驱怪》篇末,有“异史氏曰:黄狸黑狸,得鼠者雄”!

deng_cat

比如上面手机运营商的例子, 开了 500 协程还是很慢, 3w 数据超过 10 分钟才执行完, 还能更快一些么? 可以, 业务中直接读取的本地文件.

out of memory

有个定时脚本从 mongo 中取数据进行处理, 循环到数据处理完, 上线后执行一段时间报错: out of memory

  • ini_set('memory_limit', '512m'); 调整脚本运行内存限制, 执行一段时间, 依旧报错
  • memory_get_usage() / memory_get_peak_usage() 添加日志, 记录脚本内存使用, 定位问题, 发现每条需要处理的数据超过 20m
  • gc_collect_cycles() / unset() 添加强制 gc, 主动回收变量, 有效果, 但是内存依旧会持续增长
  • 0 22 * * * -> * 22 * * * + limit 50 原脚本每天 22 点执行一次, 改成每天 22 点每分钟执行一次, 每次只处理 50 条数据

connection gone away

有个统计脚本统计比较复杂, 需要统计三层数据: 查询出第一层数据, 统计后, 拿获取的数据再去查询, 查询后再统计, 比如 一定维度的用户 + 这些用户相关的订单 + 这些订单相关的账单, 测试时发现脚本运行一段时间后报错 connection gone away

关于连接超时:

  • Navicat 中的 keepalive
  • mysql 中 timeout 相关配置 SHOW VARIABLES LIKE '%timeout%';
navicat_keepalive

代码中:

  • 最简单的例子, mysqli_ping()
// 进程长时间运行时的长连接
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 重新连接数据库
    * @param string $db
    * @return \yii\db\Connection 返回数据库操作句柄
    */
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();
    }
}
  • 给报错的代码打上补丁(伪代码)
// 获取指定用户
// 可能在这里断开连接, 打上补丁
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);
    }
}

问题依旧存在, 只是变成了: 补丁要打在哪里

  • 现实中的代码比这个更复杂, 给每个 db 连接的地方都打补丁?
  • 直接改动框架底层, 风险怎么评估?

那问题怎么解决呢? 同样的套路: limit 数据量 + 多次执行.

写在最后

提升技能的过程中, 不妨多掌握一些 套路:

  • 积累/制定 相应技术的开发规范: 历史总是惊人的相似
  • 有些知识知道了(理解了)就很简单, 没那么玄乎
  • 《聊斋志异》手稿本卷三《驱怪》篇末,有“异史氏曰:黄狸黑狸,得鼠者雄”!

推荐阅读:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容