自从换了美国主机后,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;
              }

引言

emlog使用文件缓存,将数据序列化后存放到 content/cache/,如果你的blog只有几百篇日志,那么这个缓存还是有效果的,如果用emlog运营商业化应用,添加个上千篇日志,这个缓存设计就反倒成为拖累系统的累赘。

问题原因

来看看 include/lib/cache.php 是怎么读取缓存的:

function readCache($cacheName) {
	if ( $this->{$cacheName.'_cache'} == null ) {
		$cachefile = EMLOG_ROOT . '/content/cache/' . $cacheName . '.php';
		// 如果缓存文件不存在则自动生成缓存文件
		if (!is_file($cachefile) || filesize($cachefile) <= 0) {
				if (method_exists($this, 'mc_' . $cacheName)) {
						call_user_func(array($this, 'mc_' . $cacheName));
				}
		}
		if ($fp = fopen($cachefile, 'r')) {
				$data = fread($fp, filesize($cachefile));
				fclose($fp);
				$this->{$cacheName.'_cache'} = unserialize(str_replace("{$cacheName.'_cache'};
}

fopen文件,str_replace替换保护前缀,然后unserialize数据;
来看看一个8000条日志的blog,产生多少缓存数据:

ls -lhS content/cache/
total 17M
-rw-r--r-- 1 www-data www-data 3.9M Sep 28 23:21 logtags.php
-rw-r--r-- 1 www-data www-data 3.1M Sep 28 23:21 tags.php
-rw-r--r-- 1 www-data www-data 693K Sep 28 23:21 logsort.php
-rw-r--r-- 1 www-data www-data 434K Sep 28 23:21 logalias.php
-rw-r--r-- 1 www-data www-data 7.3K Sep 28 23:21 options.php
-rw-r--r-- 1 www-data www-data 2.7K Sep 28 23:21 record.php
-rw-r--r-- 1 www-data www-data 1.4K Sep 28 23:21 newtw.php
-rw-r--r-- 1 www-data www-data 1.1K Sep 28 23:21 comment.php
-rw-r--r-- 1 www-data www-data  971 Sep 28 23:21 sort.php
-rw-r--r-- 1 www-data www-data  935 Sep 28 23:21 logatts.php
-rw-r--r-- 1 www-data www-data  829 Sep 28 23:21 link.php
-rw-r--r-- 1 www-data www-data  691 Sep 28 23:21 newlog.php
-rw-r--r-- 1 www-data www-data  679 Sep 28 23:21 user.php
-rw-r--r-- 1 www-data www-data  673 Sep 28 23:21 sta.php
-rw-r--r-- 1 www-data www-data  592 Sep 28 23:21 navi.php

缓存文件最大达到了3.9M,反序列化时间长达300~500ms,所以说嘛,不会设计缓存,还不如老老实实用数据库SELECT;

优化

优化的方向?

  1. 更快的反序列化方法,比如json、igbinary、msgpack
  2. 对象持久化:hidef

我选用hidef持久化的方法,在php-fpm启动时一次性载入大数组,每次进程请求直接引用数据对象,无需反序列化。

  1. 安装hidef
    pecl install hidef
  2. 配置hidef
    extension=hidef.so
    [hidef]
    hidef.data_path=/var/www/emlog/content/cache
  3. 修改  include/lib/cache.php
            /**
             * 写入缓存
             */
            function cacheWrite ($cacheData, $cacheName) {
                    // 为hidef写入cache,文件后缀名必须为.data
                    @file_put_contents(EMLOG_ROOT . '/content/cache/' . $cacheName . '.data', $cacheData);
                   // 原写入缓存代码 从略 ……
            }
    
            /**
             * 读取缓存文件
             */
            function readCache($cacheName) {
                    if ( $this->{$cacheName.'_cache'} == null ) {
                            if ( strpos($_SERVER['REQUEST_URI'], 'admin') === false && // 管理后台不使用hidef加速
                                    $cacheVal = hidef_fetch($cacheName) ) { // hidef_fetch 获取缓存目录下对应 ***.data 的数据
                                    if ( in_array($cacheName, array('options')) ) {
                                            $cacheVal = $cacheVal->thaw();
                                    }
                                    $this->{$cacheName.'_cache'} = $cacheVal;
                            } else {
                                    // 原读取缓存代码 从略 ……
                            }
                    }
    
                    return $this->{$cacheName.'_cache'};
            }
  4. Crontab 监控缓存目录更新,自动重载php5-fpm
    * * * * * /usr/local/bin/php5-fpm-reload 

    #!/bin/sh
    # php5-fpm-reload
    md5_new="`ls -l /var/www/emlog/content/cache | md5sum | cut -d ' ' -f1`"
    md5_old="`cat /tmp/cache.md5`"
    
    if [ "$md5_new" != "$md5_old" ]; then
            service php5-fpm reload
            echo $md5_new > /tmp/cache.md5
    fi
  5. 由于hidef将数组作为FrozenArray引用对象保存,这个对象ReadOnly,支持key读取,foreach迭代,不能赋值,unset,不能使用数组函数array_XXX 操作,可以使用thaw()导出为标准Array; emlog大量用到array_search, 写个自定义函数替换掉:
    function _array_search( $needle, $haystack, $strict=false) {
            if ( is_array($haystack) ) return array_search($needle, $haystack, $strict);
            foreach ( $haystack as $_key => $_val ) {
                    $find = $strict ? ($_val === $needle) : ($_val == $needle);
                    if ($find) return $_key;
            }
    }

优化结果

优化前 优化后 Diff Diff%
Incl. Wall Time(microsec)
执行时间(毫秒)
376,113
376 毫秒
8,921
8毫秒
-367,192 -97.6%
Incl.
PeakMemUse(bytes)
峰值内存占用
67,770,496
64Mb
522,056
0.5Mb
-67,248,440 -99.2%

关键路径优化前/后的时间差

zpool create tank /dev/sdb1

rm -rf /var/lib/mysql

zfs create -o mountpoint=/var/lib/mysql tank/mysql

mysql_install_db

chcon -R -t mysqld_db_t /var/lib/mysql

/etc/my.conf
innodb_use_native_aio=false

mysql start

 

Oracle Solaris 管理:ZFS 文件系统
http://docs.oracle.com/cd/E26926_01/html/E25826/

ZFS in 30 minutes

SElinux 安全策略 引发的mysql数据库无法启动 http://blog.sina.com.cn/s/blog_53b13d950100w4yt.html
chcon -R -t mysqld_db_t /var/lib/mysql

使用tmpfs文件系统做MySQL tmpdir潜在的问题 http://www.linuxidc.com/Linux/2013-03/80696.htm
/etc/my.conf
innodb_use_native_aio=false

VPS编译PHP5.4报错,make:*** [ext/fileinfo/libmagic/apprentice.lo] Error 1,编译这个fileinfo扩展至少要700M内存。
https://bugs.php.net/bug.php?id=48809

解决方案:

  1. 禁用fileinfo扩展
    –disable-fileinfo
  2. 添加swap分区
  1. 进入一个目录
    cd /var/
  2. 获取256M的文件块
    dd if=/dev/zero of=swapfile bs=1024 count=262144
  3. 创建swap文件
    mkswap swapfile
  4. 激活swap文件
    swapon swapfile
  5. 查看一下swap是否正确
    swapon -s
  6. 加到fstab文件中让系统引导时自动启动
    vi /etc/fstab
    /var/swapfile swap swap defaults 0 0

 

一个TCP连接四元组,唯一确定一条TCP连接,只要IP四元组不冲突,就能创建连接。
client_ip:client_port – server_ip:server_port

可以创建多少连接?

php-fpm(192.168.1.100) → memcached(127.0.0.1:11211)
↓
mysql(192.168.1.200:3306)
  1. php-fpm(127.0.0.1) → memcached(127.0.0.1)
    由于连接本地环回地址,故而client_id=server_ip=127.0.0.1,只有client端口号可以在[1~65535]之间动态变化,
    IP四元组可变范围:127.0.0.1:[1~65535] – 127.0.0.1:[11211]
    【连接相同IP的一个服务端口】能建立65535-1条连接,因为源地址和目地址不能相等!127.0.0.1:11211 → 127.0.0.1:11211。
  2. php-fpm(192.168.1.100) → memcached(192.168.2.100)
    由于mysql在网段192.168.1.0,故而使用同网段IP与之建立连接,
    IP四元组可变范围:192.168.1.100:[1~65535] – 192.168.1.200:[3306]
    【连接不同IP的一个服务端口】能建立65535条连接。

连接数受限于:

  1. 客户端:动态端口范围(默认32768-61000)
    net.ipv4.ip_local_port_range = 1024 65535
  2. 服务端/客户端:最大文件描述符
    1. /proc/sys/fs/nr_open(linux 2.6.25 默认1000000)
      echo 2000000 > /proc/sys/fs/nr_open
    2. /etc/security/limits.conf
      admin    soft    nofile  2000000
      admin    hard    nofile  2000000

参考:

  1. tcp的65535个连接之迷
  2. http长连接200万尝试及调优

# Per-thread memory
$per_thread_buffers =  thread_stack + read_buffer_size + read_rnd_buffer_size + sort_buffer_size + join_buffer_size;
$total_per_thread_buffers = $per_thread_buffers * max_connections;

# Server-wide memory
$server_buffers = key_buffer_size + query_cache_size + MAX(tmp_table_size, max_heap_table_size)
                + innodb_buffer_pool_size + innodb_additional_mem_pool_size + innodb_log_buffer_size;

# Global memory
$total_possible_used_memory = $server_buffers + $total_per_thread_buffers;

Reference:mysqltuner.pl

  • max_connections
    允许的并行客户端连接数目。增大该值则增加mysqld 需要的文件描述符的数量。
  • read_buffer_size
    每个线程连续扫描时为扫描的每个表分配的缓冲区的大小(字节)。如果进行多次连续扫描,可能需要增加该值, 默认值为131072。
  • read_rnd_buffer_size
    当排序后按排序后的顺序读取行时,则通过该缓冲区读取行,避免搜索硬盘。将该变量设置为较大的值可以大大改进ORDER BY的性能。但是,这是为每个客户端分配的缓冲区,因此你不应将全局变量设置为较大的值。相反,只为需要运行大查询的客户端更改会话变量。
  • sort_buffer_size
    每个排序线程分配的缓冲区的大小。增加该值可以加快ORDER BY或GROUP BY操作。
  • thread_stack
    每个线程的堆栈大小。用crash-me测试检测出的许多限制取决于该值。 默认值足够大,可以满足普通操作。
  • join_buffer_size
    用于完全联接的缓冲区的大小(当不使用索引的时候使用联接操作)。一般情况获得快速联接的最好方法是添加索引。当增加索引时不可能通过增加join_buffer_size值来获得快速完全联接。将为两个表之间的每个完全联接分配联接缓冲区。对于多个表之间不使用索引的复杂联接,需要多联接缓冲区。
  • query_cache_size
    为缓存查询结果分配的内存的数量。默认值是0,即禁用查询缓存。请注意即使query_cache_type设置为0也将分配此数量的内存。
  • tmp_table_size
    如果内存内的临时表超过该值,MySQL自动将它转换为硬盘上的MyISAM表。如果你执行许多高级GROUP BY查询并且有大量内存,则可以增加tmp_table_size的值。
  • max_heap_table_size
    该变量设置MEMORY (HEAP)表可以增长到的最大空间大小。该变量用来计算MEMORY表的MAX_ROWS值。在已有的MEMORY表上设置该变量没有效果,除非用CREATE TABLE或TRUNCATE TABLE等语句重新创建表。
  • key_buffer_size
    MyISAM表的索引块分配了缓冲区,由所有线程共享。key_buffer_size是索引块缓冲区的大小。
  • innodb_buffer_pool_size
    InnoDB用来缓存它的数据和索引的内存缓冲区的大小。你把这个值设得越高,访问表中数据需要得磁盘I/O越少。在一个专用的数据库服务器上,你可以设置这个参数达机器物理内存大小的80%。尽管如此,还是不要把它设置得太大,因为对物理内存的竞争可能在操作系统上导致内存调度。
  • innodb_additional_mem_pool_size
    InnoDB用来存储数据目录信息&其它内部数据结构的内存池的大小。你应用程序里的表越多,你需要在这里分配越多的内存。如果InnoDB用光了这个池内的内存,InnoDB开始从操作系统分配内存,并且往MySQL错误日志写警告信息。默认值是1MB。
  • innodb_log_buffer_size
    InnoDB用来往磁盘上的日志文件写操作的缓冲区的大小。明智的值是从1MB到8MB。默认的是1MB。一个大的日志缓冲允许大型事务运行而不需要在事务提交之前往磁盘写日志。因此,如果你有大型事务,使日志缓冲区更大以节约磁盘I/O。

usage:
chvmid.sh old_id new_id

#!/bin/bash

let old=$1
let new=$2

mv /var/lib/vz/images/$old/vm-$old-disk-1.raw /var/lib/vz/images/$old/vm-$new-disk-1.raw
mv /var/lib/vz/images/$old /var/lib/vz/images/$new

sed -i "s/$old\/vm\-$old/$new\/vm\-$new/g" /etc/pve/nodes/$HOSTNAME/qemu-server/$old.conf
mv /etc/pve/nodes/$HOSTNAME/qemu-server/$old.conf /etc/pve/nodes/$HOSTNAME/qemu-server/$new.conf

Change VMID (KVM) in Proxmox 2.3

Dotdeb镜像使用帮助
使用说明

1. 添加下面两行到/etc/apt/sources.list

deb http://packages.dotdeb.org squeeze all
deb-src http://packages.dotdeb.org squeeze all

2. (可选) 如果你想在Debian Squeeze上安装PHP5.4的话,再添加下面这两行:

deb http://mirrors.ustc.edu.cn/dotdeb/packages.dotdeb.org squeeze-php54 all
deb-src http://mirrors.ustc.edu.cn/dotdeb/packages.dotdeb.org squeeze-php54 all

3. 然后导入合适的GnuPG key

wget http://www.dotdeb.org/dotdeb.gpg
cat dotdeb.gpg | sudo apt-key add –

4. 运行
apt-get update

# install software
apt-get install ttf-wqy-microhei
apt-get install libreoffice-core
apt-get install imagemagick

#download
http://sourceforge.net/projects/openkm/files/
#install to /opt/openkm-6.2.3-community/
./openkm-6.2.3-community-linux-x64-installer.run
# login
user = okmAdmin
pass = admin

# administration - Config
system.swftools.pdf2swf  = /opt/openkm-6.2.3-community/tomcat/bin/pdf2swf -T 9 -f ${fileIn} -o ${fileOut}
system.openoffice.path     = /usr/lib/libreoffice
system.imagemagick.convert = /usr/bin/convert

# start on boot
vi /etc/rc.local
/opt/openkm-6.2.3-community/tomcat/bin/startup.sh

# shutdown
/opt/openkm-6.2.3-community/tomcat/bin/shutdown.sh

#log
/opt/openkm-6.2.3-community/tomcat/logs/catalina.log

1. 准备一个标准crontab文件 ./crontab

# m h dom mon dow command
* * * * * date > /tmp/cron.date.run

2. crontab -e 将此cron.php脚本加入系统cron

* * * * * /usr/bin/php cron.php

3. cron.php 源码

// 从./crontab读取cron项,也可以从其他持久存储(mysql、redis)读取
$crontab = file('./crontab');
$now = $_SERVER['REQUEST_TIME'];

foreach ( $crontab as $cron ) {
	$slices = preg_split("/[\s]+/", $cron, 6);
	if( count($slices) !== 6 ) continue;
	
	$cmd       = array_pop($slices);
	$cron_time = implode(' ', $slices);
	$next_time = Crontab::parse($cron_time, $now);
	if ( $next_time !== $now ) continue;	

	$pid = pcntl_fork();
	if ($pid == -1) {
		die('could not fork');
	} else if ($pid) {
		// we are the parent
		pcntl_wait($status, WNOHANG); //Protect against Zombie children
	} else {
	     // we are the child
		`$cmd`;
		exit;
	}
}

/* https://github.com/jkonieczny/PHP-Crontab */
class Crontab {
   /**
	* Finds next execution time(stamp) parsin crontab syntax,
	* after given starting timestamp (or current time if ommited)
	*
	* @param string $_cron_string:
	*
	* 0 1 2 3 4
	* * * * * *
	* - - - - -
	* | | | | |
	* | | | | +----- day of week (0 - 6) (Sunday=0)
	* | | | +------- month (1 - 12)
	* | | +--------- day of month (1 - 31)
	* | +----------- hour (0 - 23)
	* +------------- min (0 - 59)
	* @param int $_after_timestamp timestamp [default=current timestamp]
	* @return int unix timestamp - next execution time will be greater
	* than given timestamp (defaults to the current timestamp)
	* @throws InvalidArgumentException
	*/
    public static function parse($_cron_string,$_after_timestamp=null)
    {
        if(!preg_match('/^((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)$/i',trim($_cron_string))){
            throw new InvalidArgumentException("Invalid cron string: ".$_cron_string);
        }
        if($_after_timestamp && !is_numeric($_after_timestamp)){
            throw new InvalidArgumentException("\$_after_timestamp must be a valid unix timestamp ($_after_timestamp given)");
        }
        $cron = preg_split("/[\s]+/i",trim($_cron_string));
        $start = empty($_after_timestamp)?time():$_after_timestamp;

        $date = array( 'minutes' =>self::_parseCronNumbers($cron[0],0,59),
                            'hours' =>self::_parseCronNumbers($cron[1],0,23),
                            'dom' =>self::_parseCronNumbers($cron[2],1,31),
                            'month' =>self::_parseCronNumbers($cron[3],1,12),
                            'dow' =>self::_parseCronNumbers($cron[4],0,6),
                        );
        // limited to time()+366 - no need to check more than 1year ahead
        for($i=0;$i<=60*60*24*366;$i+=60){
            if( in_array(intval(date('j',$start+$i)),$date['dom']) &&
                in_array(intval(date('n',$start+$i)),$date['month']) &&
                in_array(intval(date('w',$start+$i)),$date['dow']) &&
                in_array(intval(date('G',$start+$i)),$date['hours']) &&
                in_array(intval(date('i',$start+$i)),$date['minutes'])

                ){
                    return $start+$i;
            }
        }
        return null;
    }
	
    /**
	* get a single cron style notation and parse it into numeric value
	*
	* @param string $s cron string element
	* @param int $min minimum possible value
	* @param int $max maximum possible value
	* @return int parsed number
	*/
    protected static function _parseCronNumbers($s,$min,$max)
    {
        $result = array();

        $v = explode(',',$s);
        foreach($v as $vv){
            $vvv = explode('/',$vv);
            $step = empty($vvv[1])?1:$vvv[1];
            $vvvv = explode('-',$vvv[0]);
            $_min = count($vvvv)==2?$vvvv[0]:($vvv[0]=='*'?$min:$vvv[0]);
            $_max = count($vvvv)==2?$vvvv[1]:($vvv[0]=='*'?$max:$vvv[0]);

            for($i=$_min;$i<=$_max;$i+=$step){
                $result[$i]=intval($i);
            }
        }
        ksort($result);
        return $result;
    }
}