垃圾回收

简称GC。顾名思义,就是废物利用的意思。
说垃圾回收机制之前,先接触一下内存泄露。

内存泄露

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

C语言的垃圾回收机制

如果用过C语言,那么申请内存的方式是malloc或者是calloc,然后你用完这个内存后,一定不要忘了用free函数去释放掉,这就是手动垃圾回收,一般都是使用这种方式。

PHP的自动垃圾回收机制

我们都知道PHP是C语言实现的。你想想如何用C语言实现对一个变量的统计及释放?C语言是如何实现一个变量,从声明开始到没人使用了,就把这个变量所占的内存释放掉(被垃圾回收)。

PHP进行内存管理的核心算法一共两项:
一是引用计数,二是写时拷贝

声明一个PHP变量的时候,C语言就在底层生成了一个叫做zval的struct,如下:

zval {
    string "a" // 变量名为a
    value zend_value // 变量的值,联合体
    type string // 变量是字符串类型
}

zval struct结构体

  1. PHP $a 的变量名
  2. PHP $a 的变量类型
  3. PHP 变量 $a 的zend_value联合体
    如果给变量赋值了,比如"hello world",那么C语言就在底层再生成一个叫做zend_value的union
zend_value {
    string "hello world" // 值内容
    refcount 1 // 引用计数
}

zend_value联合体

  1. 保存PHP $a 的变量值
  2. 记录PHP $a 变量的引用计数

何为引用计数?

$a = "hello world";
echo xdebug_debug_zval('a'); // refcount=1
$b = $a; // $b引用变量$a
echo xdebug_debug_zval('a'); // refcount=2
$c = $a; // $c引用变量$a
echo xdebug_debug_zval('a'); // refcount=3
unset($c); // 删除了$c的引用
echo xdebug_debug_zval('a'); // refcount=2

何为拷贝复制?

$a = 'hello';
$b = $a; // $a赋值给$b的时候,并没有复制值
echo xdebug_debug_zval('a'); // $a的引用计数为2
$a = 'world'; // 当修改$a的值的时候,不得己进行复制,避免牵扯到$b
echo xdebug_debug_zval('a'); // $a的引用计数为1

垃圾回收机制:

当一个zval在被unset时,或者从一个函数中运行完毕出来(局部变量)等很多地方,都会产生zval与zend_value发生断开的行为,这个时候zend引擎需要检测的就是zend_value的refcount是否为0,如果为0则直接KO free空出内容来。如果zend_value的refcount不为0,这个value就不能被释放,但是也不代表这个zend_value是清白的,因为此zend_value依然可能是垃圾。

  1. 当php变量的refcount=0时,变量$a就会被垃圾回收
  2. 当php变量的refcount>0时,变量$a也可能被认为是垃圾

什么情况会导致zend_value的refcount不为0,但是zend_value确是垃圾

$arr = [1];
$arr[] = &$arr;
unset($arr);

这种情况下,zend_value不会释放,但也不能放过它,不然会产生内存泄露,所以这会儿zend_value会被扔到一个叫做垃圾回收堆中,然后zend引擎会依次对垃圾回收堆中的这些zend_value进行二次检测,检测是不是由于上述两种情况造成的refcount为1但是自身却没有人在使用了,一旦确认是上述两种情况造成的,那么就会将zend_value彻底抹掉释放内存。

垃圾回收发生在什么时候

  1. FPM运行完毕后,一定会GC
  2. 运行过程中也会GC,内存都是即用即释放的

优化硬件

如果你需要庞大的数据库表(>2G),应考虑使用64位的硬件结构。因为MYSQL内部使用大量64位整数,64位的CPU将提供更好的性能。
对于大数据库,优化次序一般是 RAM、硬盘、CPU。
更多的内存通过将最常用的键码页面存放在内存中可以加速键码的更新。
如果不用事务安全(Transaction Safe)的表或有大表并且想避免长文件检查,一台UPS就能够在电源故障时让系统安全关闭。
对于数据库存放在一个专用的服务器系统,应考虑1G的以太网。延迟与吞吐量同样重要。

优化磁盘

为系统、程序和临时文件配备专用磁盘,如果确是进行很多修改工作,将更新日志和事务日志放在专用磁盘上。
低寻道时间对数据库磁盘非常重要。对于大表,你可以估计你将需要:
log(行数)/log(索引块长度/3*2/(键码长度+数据指针长度))+1 次寻道才能找到一行。
对于有50W行的表,索引Mediun int类型的列,需要:
log(500000)/log(1024/3*2/(3+2))+1 = 4次寻道。
上述索引需要 50000073/2=5.2M的空间。实际上,大多数块将被缓存,所以大概只需要1-2次寻道。
然而对于写入,你将需要4次寻道请求来找到在哪里存放新键码,而且一般要2次寻道来更新索引并写入一行。
对于非常大的数据库,你的应用将受到磁盘寻道速度限制,随着数据量的增加呈N log N数据集递增。
将数据库和表分在不同的磁盘上,在MYSQL中,你可以为此而使用符号链接。
条列磁盘(RAID 0)将提高读和写的吞吐量。带镜像的条列(RAID 0+1)将更安全并提高数据读的吞吐量。写入的吞吐量将有所降低。不要对临时文件或可以很容易重建的数据所在的磁盘使用镜像或RAID(除RAID 0)。
在Linux上,在引导时对磁盘使用命令hdparm -m16 -d1 以启用同时读写多个扇区和DMA功能。这可以将响应时间提高5%-50%。
在Linux上,用async和noatime挂载磁盘。对于某些特定应用,可以对某些特定表使用内存磁盘,但通常不需要。

优化操作系统

不要交换区。如果内存不足,则增加更多的内存或配置你的系统使用较少内存。
不要使用NFS磁盘。会有NFS锁定问题。
增加系统和MYSQL服务器的打开文件数量。(在safe_mysqld脚本中加入 ulimit -n #)
增加系统的进程和线程数量。
如果你有相对较少的大表,告诉文件系统不要将文件打碎在不同的磁道上(Solaris)。
使用支持大文件的文件系统(Solaris)。
选择使用哪种文件系统。在Linux上的Reiserfs对于打开、读写都非常快。文件检查只需要几秒钟。

优化应用

使用持续的连接
缓存应用中的数据以减少SQL服务器负载
不要查询应用中不需要的列
不要使用select * from table
测试应用程序的所有部分,但将大部分精力放在可能最坏的合理负载下的测试整体应用,通过模块化的方式进行来快速找到瓶颈。
如果在一个批处理中进行大量修改,使用lock tables。例如将多个update或delete集中在一起。

优化MYSQLD

挑选编译器和编译选项。
为你的系统寻找最好的启动选项。
多用EXPLAIN SELECT/SHOW VARIABLES/SHOW STATUS/SHOW PROCESSLIST。
优化表格式。
维护表(myisamchk、check table、optimize table)
使用MYSQL扩展让一切快速完成
避免使用表级或列级的GRANT

编译和安装MYSQL

通过为你的系统挑选可能最好的编译器,通常可以获得10%-30%的性能提升。
在Linux/Intel平台上,用pgcc编译MYsql。
对于一种特定的平台,使用MYSQL参考手册上的推荐优化项。
一般对特定CPU的原生编译器(如Sparc的Sun Workshop)应该比gcc提供更好的性能,但并不总是这样。
用你将使用的字符集编译MYSQL。
静态编译生成mysqld的执行文件(用--with-mysqld-ldflags=all-static)并用strip sql/mysqld整理最终的执行文件。
注意,既然MySQL不使用C++扩展,不带扩展支持编译MySQL将赢得巨大的性能提高。
如果操作系统支持原生线程,使用原生线程(而不用mit-pthreads)。
用MySQL基准测试来测试最终的二进制代码。

维护

如果可能,偶尔运行一下 OPTIMIZE table,这对大量更新的变长行非常重要。
偶尔用 myisamchk -a 更新一下表中的键码分布统计。记住在做之前关掉MySQL。
如果有碎片文件,可能值得将所有文件复制到另一个磁盘上,清除原来的磁盘并拷回文件。
如果遇到问题,用myisamchk或CHECK table检查表。

重要的MySQL启动项

back_log 如果需要大量新连接,修改它
thread_cache_size 如果需要大量新连接,修改它
key_buffer_size 索引页池,可以设置很大
bdb_cache_size BDB表使用的记录和键码的高速缓存
table_cache 如果有很多的表和并发连接,修改它
delay_key_write 如果需要缓存所有键码写入,设置它
log_show_queries 找出需要花费大量时间的查询
max_heap_table_size 用于 group by
sort_buffer 用于 order by 和 group by
myisam_sort_buffer_size 用于 repair table
join_buffer_size 在进行无键码的连接时使用

优化表

缓存

索引

优化SQL

git reset --hard <commit_id>

git push origin HEAD --force

根据–soft –mixed –hard,会对working tree和index和HEAD进行重置:

git reset –mixed:不带任何参数的git reset,即时这种方式,它回退到某个版本,只保留源码,回退commit和index信息
git reset –soft:回退到某个版本,只回退了commit的信息,不会恢复到index file一级。如果还要提交,直接commit即可
git reset –hard:彻底回退到某个版本,本地的源码也会变为上一个版本的内容

Queue是一个先进先出的集合,在工作中经常会使用到。

但是难免会遇到这样一种情况:一个用户或某几个用户推了太多的任务在队列前排,其他新的用户可能只有很少一部分任务却得不到执行,在服务器资源有限、消费者执行速度有限的情况下,对后面的用户造成使用体验很差的情况(有的人推荐使用高低速队列分组,然而事实却并不能提供一个合理的阙值来区分任务组)。

例:用户 a,b,c,d,e 的任务以 A,B,C,D,E 来标记,他们各自推送了 8,9,4,5,2 条任务,正常的执行流程为(换行为更容易看清楚)

A A A A A A A A ->
B B B B B B B B B->
C C C C ->
D D D D D ->
E E

用户E虽然只有两条任务待处理,但是需要等待很久。

对此我们的解决方案是:修改队列执行顺序逻辑,以用户进行分组,在相同资源情况下,给所有用户一致和公平的体验。

A B C D E ->
A B C D E ->
A B C D ->
A B C D ->
A B D ->
A B ->
A B ->
A B ->
B

over,不想往后看的可以去拿代码了:
基于Yii2-Queue的Redis驱动修改


以下是源码分析

我们目前项目中使用的是Yii2-Queue,源码地址为https://github.com/yiisoft/yii2-queue,目前版本2.0.1。以下是对Yii2-Queue的Redis驱动执行流程分析及优化过程:

在程序中注册\yii\queue\redis\Queue,然后推送任务的调用方式:

Yii::$app->queue->push(new SomeJob());

跟踪得到push()方法继承于src/Queue.php中,建立任务调用所注册驱动程序的pushMessage($message, $ttr, $delay, $priority)方法,Redis驱动的代码在src/drivers/Queue.php中。

push源码注解:

    protected function pushMessage($message, $ttr, $delay, $priority)
    {
        // Redis驱动不支持作业优先级 如果有传值则抛出错误
        if ($priority !== null) {
            throw new NotSupportedException('Job priority is not supported in the driver.');
        }
        // 取到上条 message_id + 1 ,作为本条任务的ID
        $id = $this->redis->incr("$this->channel.message_id");
        // 以新ID将本条任务格存储到hash表 messages 中
        $this->redis->hset("$this->channel.messages", $id, "$ttr;$message");
        if (!$delay) {
            // 如果不需要等待执行 则将任务ID推到hash表 waiting 中
            $this->redis->lpush("$this->channel.waiting", $id);
        } else {
            // 如果需要等待执行 则推送到有序集合 delayed 中
            $this->redis->zadd("$this->channel.delayed", time() + $delay, $id);
        }
        // 返回任务ID
        return $id;
    }

reserve源码注解:

    /**
     * @param int $wait timeout
     * @return array|null payload
     */
    protected function reserve($wait)
    {
        // 将延迟和保留的作业移动到等待列表中 并锁定一秒
        if ($this->redis->set("$this->channel.moving_lock", true, 'NX', 'EX', 1)) {
            $this->moveExpired("$this->channel.delayed");
            $this->moveExpired("$this->channel.reserved");
        }
        // Find a new waiting message
        $id = null;
        if (!$wait) {
            // 从等待列表取1条任务ID待执行
            $id = $this->redis->rpop("$this->channel.waiting");
        } elseif ($result = $this->redis->brpop("$this->channel.waiting", $wait)) {
            $id = $result[1];
        }
        if (!$id) {
            return null;
        }
        // 根据任务ID取出任务
        $payload = $this->redis->hget("$this->channel.messages", $id);
        list($ttr, $message) = explode(';', $payload, 2);
        // 加入到作业列表
        $this->redis->zadd("$this->channel.reserved", time() + $ttr, $id);
        $attempt = $this->redis->hincrby("$this->channel.attempts", $id, 1);
        return [$id, $message, $ttr, $attempt];
    }

以上是关键的两段代码,经过修改逻辑如下:

    /**
     * @inheritdoc
     */
    protected function pushMessage($message, $ttr, $delay, $priority, $group)
    {
        if ($priority !== null) {
            throw new NotSupportedException('Job priority is not supported in the driver.');
        }

        $id = $this->redis->incr("$this->channel.message_id");
        $this->redis->hset("$this->channel.messages", $id, "$ttr;$message");
        if (!$delay) {
            $group ?
                $this->redis->lpush("$this->channel.waiting.$group", $id) :
                $this->redis->lpush("$this->channel.waiting", $id);
        } else {
            $this->redis->zadd("$this->channel.delayed", time() + $delay, $id);
        }

        return $id;
    }
    /**
     * @param int $wait timeout
     * @return array|null payload
     */
    protected function reserve($wait)
    {
        // Moves delayed and reserved jobs into waiting list with lock for one second
        if ($this->redis->set("$this->channel.moving_lock", true, 'NX', 'EX', 1)) {
            $this->moveExpired("$this->channel.delayed");
            $this->moveExpired("$this->channel.reserved");
        }

        // 如果列表为空  则遍历所有的子队列 每个拿一条数据放到列表
        if(!$this->redis->llen("$this->channel.waiting")){
            $keys = $this->redis->keys("$this->channel.waiting.*");
            foreach ($keys as $key){
                $id = $this->redis->rpop($key);
                $this->redis->lpush("$this->channel.waiting", $id);
            }
        }

        // Find a new waiting message
        $id = null;
        if (!$wait) {
            $id = $this->redis->rpop("$this->channel.waiting");
        } elseif ($result = $this->redis->brpop("$this->channel.waiting", $wait)) {
            $id = $result[1];
        }
        if (!$id) {
            return null;
        }

        $payload = $this->redis->hget("$this->channel.messages", $id);
        list($ttr, $message) = explode(';', $payload, 2);
        $this->redis->zadd("$this->channel.reserved", time() + $ttr, $id);
        $attempt = $this->redis->hincrby("$this->channel.attempts", $id, 1);

        return [$id, $message, $ttr, $attempt];
    }