在LNMP架构中,通常都会开启nginx日志记录$upstream_response_time请求响应时间,而忽略到phpfpm日志可以提供更详尽的分析:

; The access log file
; Default: not set
;access.log = log/$pool.access.log

; The access log format.
; The following syntax is allowed
;  %%: the '%' character
;  %C: %CPU used by the request
;      it can accept the following format:
;      - %{user}C for user CPU only
;      - %{system}C for system CPU only
;      - %{total}C  for user + system CPU (default)
;  %d: time taken to serve the request
;      it can accept the following format:
;      - %{seconds}d (default)
;      - %{miliseconds}d
;      - %{mili}d
;      - %{microseconds}d
;      - %{micro}d
;  %e: an environment variable (same as $_ENV or $_SERVER)
;      it must be associated with embraces to specify the name of the env
;      variable. Some exemples:
;      - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e
;      - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e
;  %f: script filename
;  %l: content-length of the request (for POST request only)
;  %m: request method
;  %M: peak of memory allocated by PHP
;      it can accept the following format:
;      - %{bytes}M (default)
;      - %{kilobytes}M
;      - %{kilo}M
;      - %{megabytes}M
;      - %{mega}M
;  %n: pool name
;  %o: output header
;      it must be associated with embraces to specify the name of the header:
;      - %{Content-Type}o
;      - %{X-Powered-By}o
;      - %{Transfert-Encoding}o
;      - ....
;  %p: PID of the child that serviced the request
;  %P: PID of the parent of the child that serviced the request
;  %q: the query string
;  %Q: the '?' character if query string exists
;  %r: the request URI (without the query string, see %q and %Q)
;  %R: remote IP address
;  %s: status (response code)
;  %t: server time the request was received
;      it can accept a strftime(3) format:
;      %d/%b/%Y:%H:%M:%S %z (default)
;  %T: time the log has been written (the request has finished)
;      it can accept a strftime(3) format:
;      %d/%b/%Y:%H:%M:%S %z (default)
;  %u: remote user
;
; Default: "%R - %u %t \"%m %r\" %s"
;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"

划重点:
%d: 响应时间
%C: CPU处理时间占响应时间的百分比
%M: 峰值内存占用

通常一个请求,我们可以轻易的知道他的总响应时长,但是这个总时长里多少是CPU,多少是iowait是不知道的,有如下测试脚本

// test.php
// cpu
for($i=0; $i<10000000;$i++) {
  pow(2,32);
}
// iowait
$timeout = stream_context_create(['http' => ['timeout'=> 1]]);
file_get_contents('http://www.google.com/', 0, $timeout);

在shell环境,我们可以使用time命令获得进程的cpu时间占用

time php test.php
real    0m1.751s
user    0m0.724s
sys     0m0.024s

iowait时长可以用总时间相减得到:iowait = real – user + sys
上例中iowait = 1.751-0.724-0.024 = 1.003s 和我们设置的timeout几乎一致
实际上,如果dns未缓存,这里的iowait时长还会额外增加dns查询的实际,一定大于1秒

如此一来,通过记录%d和%C就能帮助方便我们轻松区分cpu和iowait接口。

启用日志:

access.log = /var/log/$pool.access.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"
curl http://localhost/test.php

tail /var/log/www.access.log
127.0.0.1 -  29/Oct/2017:23:39:34 +0800 "GET /test.php" 200 /index.php 2649.988 2048 40.00%

这条日志记录了test.php总时长为2649.988毫秒,其中仅有40%的时长是cpu消耗的,那么这个请求的%iowait就是60%

需要注意的是,%CPU有可能大于100%,这是为什么呢?
查看php源代码:
sapi/fpm/fpm/fpm_request.c
在每次请求开始(fpm_request_reading_headers)和结束(fpm_request_end),使用times获得当前进程cpu时间,tms结构体如下

struct tms {
   clock_t tms_utime;  /* user time */
   clock_t tms_stime;  /* system time */
   clock_t tms_cutime; /* user time of children */
   clock_t tms_cstime; /* system time of children */
};

最后写入日志sapi/fpm/fpm/fpm_log.c,cpu时长是包含子进程时长的
如果想获得主进程的iowait时间,应该是:总时长-用户空间时间-系统空间时间(不包括子进程时间)
可以给sapi/fpm/fpm/fpm_log.c打一个patch,获得原始的cpu时间

#ifdef HAVE_TIMES
case 'C': /* %CPU */
    if (format[0] == '\0' || !strcasecmp(format, "total")) {
        if (!test) {
            tms_total = proc.last_request_cpu.tms_utime + proc.last_request_cpu.tms_stime + proc.last_request_cpu.tms_cutime + proc.last_request_cpu.tms_cstime;
        }
    } else if (!strcasecmp(format, "user")) {
        if (!test) {
            tms_total = proc.last_request_cpu.tms_utime + proc.last_request_cpu.tms_cutime;
        }
    } else if (!strcasecmp(format, "system")) {
        if (!test) {
            tms_total = proc.last_request_cpu.tms_stime + proc.last_request_cpu.tms_cstime;
        }
    } else if (!strcasecmp(format, "_utime")) {
        if (!test) {
            tms_total = proc.last_request_cpu.tms_utime;
        }
    } else if (!strcasecmp(format, "_cutime")) {
        if (!test) {
            tms_total = proc.last_request_cpu.tms_cutime;
        }
    } else if (!strcasecmp(format, "_stime")) {
        if (!test) {
            tms_total = proc.last_request_cpu.tms_stime;
        }
    } else if (!strcasecmp(format, "_cstime")) {
        if (!test) {
            tms_total = proc.last_request_cpu.tms_cstime;
        }
    } else {
        zlog(ZLOG_WARNING, "only 'total', 'user' or 'system' are allowed as a modifier for %%%c ('%s')", *s, format);
        return -1;
    }

    if (!test) {
        if ( format[0] == '_' ) {
            len2 = snprintf(b, FPM_LOG_BUFFER - len, "%f", tms_total / fpm_scoreboard_get_tick() * 1000.);
        } else {
            len2 = snprintf(b, FPM_LOG_BUFFER - len, "%.2f", tms_total / fpm_scoreboard_get_tick() / (proc.cpu_duration.tv_sec + proc.cpu_duration.tv_usec / 1000000.) * 100.);
        }
    }
    format[0] = '\0';
    break;
#endif

#1、删除旧版本

apt-get remove php-common

#2、下载php7

wget http://cn2.php.net/get/php-7.1.10.tar.bz2/from/this/mirror
tar xvf mirror
cd php-7.1.10

#3、编译配置

./configure --prefix=/usr --with-config-file-path=/etc/php/7.1/cli/ --with-config-file-scan-dir=/etc/php/7.1/cli/conf.d --enable-mbstring --enable-zip --enable-bcmath --enable-pcntl --enable-ftp --enable-exif --enable-calendar --enable-sysvmsg --enable-sysvsem --enable-sysvshm --enable-wddx --with-curl --with-mcrypt --with-iconv --with-gmp --with-pspell --with-gd --with-jpeg-dir=/usr --with-png-dir=/usr --with-zlib-dir=/usr --with-xpm-dir=/usr --with-freetype-dir=/usr --enable-gd-native-ttf --enable-gd-jis-conv --with-openssl --with-pdo-mysql=/usr --with-gettext=/usr --with-zlib=/usr --with-bz2=/usr --enable-fpm --with-pcre-jit --with-pcre-regex --with-pear --config-cache --sysconfdir=/etc

#4、测试编译有没有异常

make
make test

#5、安装

sudo make install

#6、配置文件

cp php.ini-production /etc/php.ini
cp sapi/fpm/php-fpm.conf /etc/php-fpm.conf
cp sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm
mkdir /etc/php-fpm.d/
cp sapi/fpm/www.conf /etc/php-fpm.d/

#7、安装pear

curl -O http://pear.php.net/go-pear.phar
sudo php -d detect_unicode=0 go-pear.phar

#8、安装扩展

pecl install yaf
pecl install mongodb
pecl install redis
pecl install msgpack

#9、安装composer

curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/bin/

使用微信内嵌浏览器编辑文本字符,又遇到了老罗发现的:“笨拙的拇指”操作“缩微的字符”的问题,
文本框内长按呼出编辑菜单[粘贴、选择]的位置往往不符合预期,而一但进入插入状态(“|”光标闪烁),就无法于选中位置呼出编辑菜单粘贴!

问题:控制不了粘贴位置
由于安全问题,js是无法read系统剪贴板的,
于是通过一个空input,长按呼出编辑菜单【粘贴】,然后copy文本插入到原文本框sectionStart位置




BigFont






tips:
一、W3C剪贴板规范:
https://www.w3.org/TR/2013/WD-clipboard-apis-20130411/
二、clipboardjs
实现【复制】,支持微信内嵌浏览器!
https://clipboardjs.com
三、PC端还可以捕获Ctrl+c/v事件
https://stackoverflow.com/questions/2787669/get-html-from-clipboard-in-javascript/

The Definitive Guide to Copying and Pasting in JavaScript

今天思勇问我怎么配线上防火墙,就是只开80/443,那ssh要不要开?
一般方案都是走堡垒机,其实问题的关键不是ssh能不能开,而是按需打开,不能一直在那里listen,引火烧身。

之前我一直用linode日本的服务器,22端口时不时被墙,只能通过web访问服务器,于是使用了mosh的方案。

通常mosh需要先建立一个ssh连接,然后远程执行mosh-server命令创建一个udp-server,监听60000-61000端口

./mosh-server
#MOSH CONNECT  
MOSH CONNECT 60000 E97yTo4vcW6Wk+oiXa2TaQ

在客户端使用远程获得的port和key就可以建立一个udp的mosh连接

#MOSH_KEY= mosh-client  
MOSH_KEY=E97yTo4vcW6Wk+oiXa2TaQ mosh-client 172.104.33.236 60000

由于ssh端口被封了,不能直接通过ssh建立mosh-server,获取连接port和key
那就直接通过http来获得port/key了:
1、登陆终端启动一个mosh-daemon,监听任务队列 brpop MOSH:conn
2、浏览器访问 mosh-connect.php, 新增连接任务 lpush MOSH:conn $session_id, 并等待执行结果 BRPOP MOSH:conn:$session
3、mosh-daemon 执行命令 popen(‘/usr/bin/mosh-server 2>&1’, ‘r’) 获得连接信息并返回给客户端 lpush MOSH:conn:$session $port,$key

mosh-daemon.sh

#!/usr/bin/env php
<?php
while(1) {
    try {
        $redis = new Redis();
        $redis->connect('127.0.0.1');

        $res = $redis->blPop('MOSH:conn', 60);
        if ( !empty($res) ) {
            list(, $session_id) = $res;
            $mosh = popen('/usr/bin/mosh-server 2>&1', 'r');
            $result = fread($mosh, 1024);
            if ( preg_match('/MOSH CONNECT (\d+) (\S+)/', $result, $matches) ) {
                list(, $port, $key) = $matches;
                $redis->lPush("MOSH:conn:$session_id", "$port,$key");
            }
        }
    } catch (Exception $e) {}
}

mosh-connect.php

<?php
$redis = new Redis();
$redis->connect('127.0.0.1');

$session_id = uniqid();
$redis->lPush('MOSH:conn', $session_id);

list(, $result) = $redis->blPop('MOSH:conn:'.$session_id, 60);
list($port, $key) = explode(',', $result);

$ip = $_SERVER["SERVER_ADDR"];
echo "<pre>";
echo "$ip\n$port\n$key\n";
echo "MOSH_KEY=$key mosh-client $ip $port";

人民币是中国人民银行的货币,
比特币就是世界比特币银行的唯一货币。

我们来看看这个比特币银行都能开展哪些业务:

  1. 开户
    比特币银行开户不需要身份证;
    任何人可以开无限个账户;没有账户管理费;没有最低存款额;
    但每个账户对应一个唯一的密码,只有密码才能控制账户。所以:
    不能改密码!
    忘记密码,不能挂失!
    没有任何人能够冻结你的账户,因为他没有密码!
    任何人无需密码都能看到一个账户有多少钱,查询转账汇款记录,但并不知道谁拥有这个账户!
  2. 存款
    1. 没钱怎么存款?
      到交易所购买比特币;
      挖矿获得比特币;
    2. 转账到您的比特币私人账户
  3. 转账汇款
    只需要输入收款人账户和汇款金额;
    一旦汇款成功,款项无法追回;
    10~60分钟到账;
    普通汇款无需手续费;
    快速转账需要手续费;费率不定,给得越多越能确保10分钟到账!
    支持第三方担保支付(支付宝);
  4. 贷款
    本行无法开展此项业务,因为你的账户里面的资金只属于知道密码的人,不能被银行挪用贷款。所以本银行绝不会因为挤兑破产!


function confirm($msg, $expert='yes|y') {
	$input = propmt($msg);
	return in_array(trim($input), explode('|',$expert));
}

function propmt($msg) {
	echo $msg;
	$handle = fopen("php://stdin","r");
	return fgets($handle);
}

if ( confirm('are you sure? (yes/no)') ) {
    echo 'delete!';
}

Agent.php

pcntl_signal(SIGCHLD, SIG_IGN);

require 'iRedis.php';

$hostname = trim(`hostname`);
$redis = new iRedis('127.0.0.1');
$redis->conn || die;

$redis2 = new iRedis('127.0.0.1');
$redis2->conn || die;

$client_list = $redis->client('setname', $hostname);
$redis->subscribe('cmd');
while ( !feof($redis->conn) ) {
        $argv = $redis->read_reply($redis->conn);
        if ( $argv === null ) {
                echo "timeout\n";
                if ( $redis->ping() ) {
                        continue;
                } else {
                        exit;
                }
        } else {
                $pid = pcntl_fork();
                if ($pid == -1) {
                        die('could not fork');
                } else if ($pid) {
                } else {
                        echo "exec\n";
                        $handle = popen($argv[2], "r");
                        while ( !feof($handle) ) {
                                if ( $response = fread($handle, 8192) ) {
                                        $redis2->publish("resp:$hostname", $response);
                                }
                        }
                        exit;
                }
        }
}

Console.php

require 'iRedis.php';
$redis = new iRedis('127.0.0.1');
$redis2 = new iRedis('127.0.0.1');

if ( empty($argv[1]) ) die("no command!\n");

$cmd = $argv[1];
if ( isset($argv[2]) ) parse_str($argv[2], $opt);

if ( !isset($opt['line']) ) $opt['line'] = "\n";

$redis->psubscribe('resp*');
$redis2->publish('cmd', $cmd." 2>&1");

$last_host = $host = '';
$hosts = array();
while ( !feof($redis->conn) ) {
        $argv = $redis->read_reply($redis->conn);
        $host = substr($argv[2],5);
        if( !array_key_exists($host, $hosts) ) $hosts[$host] = count($hosts)+1;
        if ( $argv !== null ) {
                if ( $last_host !== $host ) echo "\n\33[1;41m",str_pad($hosts[$host],3,'0',STR_PAD_LEFT)," ",$host,"\33[0m",$opt['line'];
                echo trim($argv[3]);
        }
        $last_host = $host;
}

iRedis.php

class iRedis {
        var $host;
        var $pass;
        var $port;
        var $flags;
        var $conn;

        function __construct($host=null, $port=6379) {
                if ( !empty($host) ) {
                        $this->connect($host, $port);
                }
        }

        function pconnect($host, $port=6379) {
                return $this->connect($host, $port, STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT);
        }

        function connect($host, $port=6379, $flags=STREAM_CLIENT_CONNECT) {
                $this->host  = $host;
                $this->port  = $port;
                $this->flags = $flags;

                $this->conn  = stream_socket_client("tcp://$host:$port", $errno, $errstr, 10, $flags);
                if (!$this->conn) {
                        echo "$errstr ($errno)
\n"; return false; } if ( !empty($this->pass) ) { $this->auth($pass); } return $this->conn; } function auth($pass) { $this->pass = $pass; return $this->__call('auth', array($pass)); } function reconnect() { if ( $this->connect($this->host, $this->port, $this->flags) ) { if ( !empty($this->pass) ) { return $this->auth($this->pass); } return true; } return false; } function close() { fclose($this->conn); } function do_cmd($cmd) { if ( fwrite($this->conn, $cmd) === false ) { if ( !$this->reconnect() ) { return false; } return $this->do_cmd($cmd); } return $this->read_reply($this->conn); } function __call($name, $args) { array_unshift($args, $name); $argc = count($args); $cmd = '*'.$argc."\r\n"; foreach ( $args as $arg ) { $cmd .= '$'.strlen($arg)."\r\n".$arg."\r\n"; } return $this->do_cmd($cmd); } function read_reply($fd) { $argc = 1; $argv = array(); while ( $line = fgets($fd) ) { $val = substr($line,1,-2); switch ($line{0}) { case '+': case '-': case ':': $argv[] = $val; break; case '*': $argc = $val; continue 2; case '$': $len = $val; if ( $len > 0 ) { $buffer = ''; while ( $len > 0 && $chunk = fread($fd, $len) ) { $buffer .= $chunk; $len -= strlen($chunk); } $argv[] = $buffer; fread($fd,2); // -- ignore CRLF } else { $argv[] = null; } break; } if ( count($argv) == $argc ) { return $argc > 1 ? $argv : $argv[0]; } } } }

原则:

一个子进程死了,先变成僵尸,等待父进程收尸

父进程必须是子进程的亲爹,爷爷就不行了

如果亲爹死的早:

  1. 自动过继给init当儿子,由init收尸

如果亲爹还活着:

  1. 声明放弃抚养权,由init收尸
    pcntl_signal(SIGCHLD, SIG_IGN);
    一旦声明放弃,就无法获取子进程退出状态码:
    pcntl_waitpid(返回错误码-1)
    proc_open:proc_get_status/proc_close (返回错误码-1)
    但还有后悔药 pcntl_signal(SIGCHLD, SIG_DFL) 恢复接收信号
  2. 一个负责任的好爹,亲自收尸
    pcntl_waitpid 

    1. 阻塞等待所有子进程
      while( $pid_exited = pcntl_waitpid(-1, $status) ) {
      }
    2. 注册信号处理句柄
      pcntl_signal(SIGCHLD, function($signo){
      $pid_exited = pcntl_waitpid(-1, $status);
      });
  3. 一个不负责任的爹,活着却不管不问
    儿子死了就变成了僵尸,想当僵尸还是很难的:)

 

  1. 运行环境
    1. 使用最新的PHP版本 + 20%
      5.5 > 5.4 > 5.3
    2. 使用更快的OPCache缓存 + 20%
      ZendOpcache > APC
    3. 使用Nginx+PHP-FPM 而不是 Apache+mod_php
      1. Nginx
        1. workers进程数等于CPU核数
          worker_processes =  [CPU cores];
        2. 设置cpu亲密性,每个worker对于一个cpu
          worker_cpu_affinity 01 10; #双核
        3. 使用 limit_zone阻止恶意访问
          limit_req_zone  $binary_remote_addr  zone=qps1:1m   rate=3r/s;
          location /delay {
              limit_req   zone=qps1  burst=5;
          }
        4. 静态文件缓存
          location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
              expires 24h;
              log_not_found off;
          }
      2. PHP-FPM
        1. 打开error_log+slow_log,帮助你监控,迅速定位错误
          slowlog = /var/log/php-fpm.$pool.slow
          request_slowlog_timeout = 1
          php_admin_value[error_log] = /var/log/php-fpm.$pool.error
        2. 使用进程池
          PHP支持使用进程池,比如将网站前端和管理后台分别设置两个进程池:
          [fontend]
          # 前端:并发请求多,限制内存占用,限制单请求执行时长
          max_children=100;memory_limit=16M;request_terminate_timeout=15
          [backend]
          # 后端:并发请求少,占用内存大,不限制执行时间
          max_children=10;memory_limit=128M;request_terminate_timeout=0
        3. 设置合理的进程数
          取决于你的PHP程序是IO密集型还是CPU密集型,可使用htop观察;
          IO密集型:100/%MEM
          * 限制内存 php_admin_value[memory_limit] = 32M
          CPU密集型:100/%CPU
          pm = dynamic
          pm.max_children = ?
        4. 设置PHP最长执行时间
          PHP-FPM一个请求独占一个进程,如果不设置超时时间,慢请求会迅速占满所有fpm进程
          request_terminate_timeout = 15
    4. PHP的mysql扩展使用Mysqlnd驱动,而不是libmysql
      ./configure –with-mysql=mysqlnd –with-mysqli=mysqlnd –with-pdo-mysql=mysqlnd
    5. MySQL
      1. 打开慢日志
        log_slow_queries = /var/log/mysql/mysql-slow.log
        long_query_time = 1
      2. Innodb 设置
        innodb_buffer_pool_size = 1G
        innodb_log_file_size = 256M
        innodb_log_buffer_size = 4M
        innodb_flush_log_at_trx_commit = 2
        innodb_thread_concurrency = 8
        innodb_flush_method = O_DIRECT
  2. PHP
    1. 优化原则
      抓大放小(总执行时长=执行时长*执行次数)
      * 安装xhprof+XHGui;进入Hardest Hit ,按Total Wall Time(总执行时长)排序
    2. CPU
      * 监测代码执行前后的,脚本使用内存变化 

      1. 大加载
        1. 使用大数组作为配置文件
          使用hidef 一次性载入
        2. 载入不需要的类/库文件
          lazyload,按需加载库文件
      2. 反序列化
        1. 使用更快的序列化/反序列化方法
          explode > msgpack > igbinary > json> serialize
        2. 避免反序列化大对象(同大加载)
          保持小对象的序列化/反序列化,不要缓存大数据,检测方法同上
      3. 使用简单的数据/对象结构
        例如:避免对大数据集使用Active Record
    3. IOwait
      1. 架构设计上,避免单点,可水平扩展
        异步解决不了可扩展性问题,总执行时长还是一样多
      2. 设置请求timeout
        避免一个服务延迟,拖累整个页面请求
      3. 合并请求  curl/sql/redis/memcache
        * PHPIO: 循环执行IO函数,记录代码路径重复次数,例如:
        foreach ($users as $uid) {
        mysql_query(‘SELECT * FROM usr WHERE uid = $uid’)
        }
        =>
        mysql_query(‘SELECT * FROM usr WHERE uid IN (‘.implode(‘,’,$users).’)’);

自从换了美国主机后,Wordpress后台打开就奇慢无比,调试过程:

  1. xhprof
    访问http://www.hemono.com/wp-admin/?_profile=1,结果啥也没记录下来:(
    通常的profile工具是注册一个shutdown函数,最后一次性的将结果写入磁盘;遇到这样的超时程序,需要能够实时的写入profile到存储设备,todo:让phpio支持实时记录
  2. Strace
    php-fpm进程比较多,如果使用strace-f 跟踪父进程id的话,输出的内容太杂乱,还是新建一个单独pid的测试环境:

    1. php-fpm
      新建一个debug-pool:/etc/php5/fpm/pool.d/debug.conf
      [debug]
      listen = 127.0.0.1:9001
      pm = static
      pm.max_children = 1
    2. nginx
      新建一个debug站点:
      listen:8080;
      fastcgi_pass 127.0.0.1:9001;
    3. 在htop里面找到pool-debug的进程id
      strace -s 1024 -p [debug-pool-pid]
    4. 然后在浏览器中访问http://localhost:8080,终端里面应该可以看到php-fpm的系统调用
      看了半天,发现程序在不断的执行SQL,但是strace无法定位到出错的PHP代码段,到是可以把strace显示的SQL拿到源代码目录去grep一下;
      在htop看到mysqld的cpu占用老高,把慢日志打开再说。
  3. 打开慢日志
    1. php-fpm 慢日志 /etc/php5/fpm/pool.d/debug.conf
      slowlog = /var/log/php-fpm.slow.$pool.log
      request_slowlog_timeout = 1 

      [09-Oct-2013 10:51:36]  [pool debug] pid 12263
      script_filename = /var/www/hemono.com/wp-admin/index.php
      [0x00007fc9e145ee28] mysql_query() /var/www/hemono.com/wp-includes/wp-db.php:1098
      [0x00007fc9e145ecc8] query() /var/www/hemono.com/wp-includes/wp-db.php:1379
      [0x00007fc9e145eb48] get_results() /var/www/hemono.com/wp-admin/includes/dashboard.php:612
      [0x00007fffc11dc080] wp_dashboard_recent_comments() unknown:0
      [0x00007fc9e145e8f0] call_user_func() /var/www/hemono.com/wp-admin/includes/template.php:963
      [0x00007fc9e145e7a0] do_meta_boxes() /var/www/hemono.com/wp-admin/includes/dashboard.php:206
      [0x00007fc9e145e688] wp_dashboard() /var/www/hemono.com/wp-admin/index.php:63
    2. mysql慢日志 /etc/mysql/my.cnf
      log_slow_queries        = /var/log/mysql/mysql-slow.log
      long_query_time = 1 

      # Query_time: 1.010872  Lock_time: 0.000149 Rows_sent: 50  Rows_examined: 78376
      SELECT * FROM wp_comments c LEFT JOIN wp_posts p ON c.comment_post_ID = p.ID WHERE p.post_status != 'trash' ORDER BY c.comment_date_gmt DESC LIMIT 0, 50;
      SELECT * FROM wp_comments c LEFT JOIN wp_posts p ON c.comment_post_ID = p.ID WHERE p.post_status != 'trash' ORDER BY c.comment_date_gmt DESC LIMIT 50, 50;
      SELECT * FROM wp_comments c LEFT JOIN wp_posts p ON c.comment_post_ID = p.ID WHERE p.post_status != 'trash' ORDER BY c.comment_date_gmt DESC LIMIT 100, 50;
      ……
    3. 最后还是靠慢日志定位到问题:
      /wp-admin/includes/dashboard.php

      function wp_dashboard_recent_comments() {
              global $wpdb;
      
              if ( current_user_can('edit_posts') )
                      $allowed_states = array('0', '1');
              else
                      $allowed_states = array('1');
      
              // Select all comment types and filter out spam later for better query performance.
              $comments = array();
              $start = 0;
      
              $widgets = get_option( 'dashboard_widget_options' );
              $total_items = isset( $widgets['dashboard_recent_comments'] ) && isset( $widgets['dashboard_recent_comments']['items'] )
                      ? absint( $widgets['dashboard_recent_comments']['items'] ) : 5;
              // 由于垃圾评论35000多条,导致迟迟不能获得5条有效的评论,只需加个条件 c.`comment_approved` != 'spam' 过滤掉spam评论
      +        while ( count( $comments ) < 5 && $possible = $wpdb->get_results( "SELECT * FROM $wpdb->comments c LEFT JOIN $wpdb->posts p ON c.comment_post_ID = p.ID WHERE c.`comment_approved` != 'spam' AND p.post_status != 'trash' ORDER BY c.comment_date_gmt DESC LIMIT $start, 50" ) ) {
      -        while ( count( $comments ) < 5 && $possible = $wpdb->get_results( "SELECT * FROM $wpdb->comments c LEFT JOIN $wpdb->posts p ON c.comment_post_ID = p.ID WHERE p.post_status != 'trash' ORDER BY c.comment_date_gmt DESC LIMIT $start, 50" ) ) {
      
                      foreach ( $possible as $comment ) {
                              if ( count( $comments ) >= $total_items )
                                      break;
                              if ( in_array( $comment->comment_approved, $allowed_states ) && current_user_can( 'read_post', $comment->comment_post_ID ) )
                                      $comments[] = $comment;
                      }
                      break;
                      $start = $start + 50;
              }