[PHP]细说Opcache

Opcache原理

从Operate Code开始

  • 当解释器完成对脚本代码的分析后,便将它们生成可以直接运行的中间代码,也称为操作码。
    • 流程:读取文件 => 扫描词典和表达式 => 解析文件 => 创建Opcode
  • 生产环境PHP的两种运行模式
    • PHP执行流程:
      • module_init => request_init => php_execute_script(Opcode) => request_shutdown => module_shutdown
    • PHP-FPM:PHP-FPM是对于FastCGI 协议的具体实现。FastCGI是Web服务器和处理程序之间通信的一种协议, 是CGI的一种改进方案,FastCGI像是一个常驻(long-lived)型的CGI, 它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理。
      • 生命周期:一直在request_init到request_shutdown之间重复执行,Opcache在服务启动时一直生效。
    • PHP-CLI:PHP在命令行下运行的接口。
      • 生命周期:从module_init直接执行到module_shutdown,执行一次,进程结束Opcache失效。
  • Opcache在一个生命周期内缓存内容
    • 存储Opcode(避免重复编译,减少CPU和内存开销)
    • 存储Interned String(php请求生命周期中不释放的String:变量名、类名、方法名、字符串、注释等)
  • Opcache的一些其他原则
    • 基于先到先得的原则进行缓存,不使用诸如最近最少使用(LRU)之类的驱逐策略。
    • 每当达到最大内存消耗或最大加速文件时,Opcache 就会尝试重新启动缓存。
    • 如果 Opcache 中浪费的内存没有超过 max_wasted_percentage,则 Opcache 将不会重启,对于新的请求Opcache失效。
      • wasted memory:被改变的php脚本占用的内存。
        • 理想状况永远为0。
        • 改变的php脚本在opcache中生成一个新条目,但旧条目不会被释放。
        • 手动清空wasted memory只能重启PHP-FPM或使用PHP函数opcache_reset()。
  • Opcache对性能的影响。
    • 如果性能瓶颈不在于CPU和内存,而在于I/O操作,比如数据库查询带来的磁盘I/O开销,那么Opcode cache的性能提升是非常有限的。
    • 刚启动或缓存被清空时,如果服务器流量大可能会导致 thundering herd problem 或缓存猛击,许多请求同时生成相同的缓存条目。
  • PHP函数opcache_get_status 统计内存消耗信息 单位Byte”memory_usage”: {
       “used_memory”: 91647160,
       “free_memory”: 440537232,
       “wasted_memory”: 4686520,
       “current_wasted_percentage”: 0.8729323744773865
    }
    计算 memory_consumption 的公式为: ( used_memory + free_memory + wasted_memory) / 1048576

Opcache配置

配置示例

zend_extension=opcache.so                    # 添加扩展
opcache.enable=1                             # 是否开启opcache
opcache.enable_cli=0                         # 是否在cli下开启opcache
opcache.max_wasted_percentage=5              # 浪费内存上限 单位百分比 超出后清空全部缓存
opcache.memory_consumption=512               # 共享内存大小 单位是MB 
opcache.interned_strings_buffer=128          # 存储字符串 单位是MB 原理是内存驻留
opcache.max_accelerated_files=100000         # 哈希表中存储文件数量上限 最小200 最大1000000
opcache.validate_timestamps=0                # 是否根据时间戳检查文件变更 0 revalidate_freq无效
opcache.revalidate_freq=0                    # 根据时间戳检查文件变更的周期 单位秒 0不检查
opcache.save_comments=0                      # 缓存里是否加载注释 如框架依赖注释无法正常运行
opcache.fast_shutdown=1                      # 快速停止续发事件 一次释放请求变量全部内存 php7.2后无效
opcache.file_cache=/tmp                      # 启用文件缓存 设置缓存路径

生产环境部署

配置文件

  • 仅在PHP-FPM模式下启用Opcache。
  • Opcache不设置过期时间。
  • php.ini中配置preload(用于缓存预热)
    • opcache.preload=/var/www/wonjia/preload.php
    • 注意:一台服务器上有多个php项目时,可能会引起未知错误

运维

  • 避免在流量高峰期发布代码。
  • 发版时清空opcache,二选一,倾向于重启PHP-FPM。
    • 调用每台服务器的opcache_clear.php脚本,无法预热。
    • 重启每台服务器的PHP-FPM。
  • jenkins增加功能(每台服务器)监控opcache状态、重启opcache、重启PHP-FPM。

代码

  • api和pc都增加preload.php脚本,require_once文件使用绝对路径。
  • api增加opcache_clear.php脚本,用于清空opcache。
  • api增加opcache_status.php脚本,用于监控opcache。

[PHP][设计模式]装饰器模式

装饰器模式是一个常见的设计模式,经典使用场景是商品销售。

下面就是一个典型的装饰器模式,用PHP语言描述。


/**
 * Interface IComponent 组件对象接口
 * Author wonjia
 */
interface IComponent {
    public function getName();
    public function getPrice();
}

/**
 * Class Car 待装饰对象
 */
class Car implements IComponent {
    private $_name;
    private $_price;

    /**
     * Person constructor. 构造方法
     *
     * @param $name 汽车的名称和价格
     */
    public function __construct($name, $price) {
        $this->_name = $name;
        $this->_price = $price;
    }

    public function getName() {
        return $this->_name;
    }

    public function getPrice() {
        return $this->_price;
    }
}

/**
 * Class Extra 所有装饰器父类 额外产品
 */
class Extra implements IComponent {
    protected $component;

    /**
     * 接收装饰对象
     *
     * @param IComponent $component
     */
    public function decorate(IComponent $component) {
        $this->component = $component;
    }

    public function getName() {
        if(!empty($this->component)) {
            return $this->component->getName();
        }
    }

    public function getPrice() {
        if(!empty($this->component)) {
            return $this->component->getPrice();
        }
    }
}

/* 下面为具体装饰器类 */
/**
 * Class baoXian 保险
 */
class baoXian extends Extra{
    public function getName() {
        return parent::getName() . " 保险 ";
    }

    public function getPrice() {
        return parent::getPrice() * 1.1;
    }
}

/**
 * Class jiaoDian 脚垫
 */
class jiaoDian extends Extra {
    public function getName() {
        return parent::getName() . " 脚垫 ";
    }

    public function getPrice() {
        return parent::getPrice() + 500;
    }
}

/**
 * Class jingHuaQi 净化器
 */
class jingHuaQi extends Extra {
    public function getName() {
        return parent::getName() . " 净化器 ";
    }

    public function getPrice() {
        return parent::getPrice() + 1200;
    }
}

/**
 * Class mieHuoQi 灭火器
 */
class mieHuoQi extends Extra {
    public function getName() {
        return parent::getName() . " 灭火器 ";
    }

    public function getPrice() {
        return parent::getPrice() + 200;
    }
}


/**
 * Class Client
 */
class Client {
    public static function order() {

        //昂科威
        $car1 = new Car('昂科威', 230000);
        $baoXian  = new baoXian();
        $jiaoDian = new jiaoDian();

        $baoXian->decorate($car1);
        $jiaoDian->decorate($baoXian);

        echo $jiaoDian->getName();
        echo $jiaoDian->getPrice() . '元';
        echo '<br />';

        //GL8
        $car2 = new Car('GL8', 310000);

        $jingHuaQi = new jingHuaQi();
        $mieHuoQi   = new mieHuoQi();

        $jingHuaQi->decorate($car2);
        $mieHuoQi->decorate($jingHuaQi);

        echo $mieHuoQi->getName();
        echo $mieHuoQi->getPrice() . '元';
        echo '<br />';
    }
}

Client::order();

[PHP]求最小公倍数

根据最小公倍数的公式,要先求出最大公约数。

最大公约数由欧几里德法求出。


/**
 * 求两个整数的最小公倍数
 * @param $a int
 * @param $b int
 * @return int/bool
 * @author wonjia
 */
function minMultiple($a, $b) {
    if(!is_int($a) || !is_int($b)){
        return false;
    }

    if($a==0 || $b==0){
        return 0;
    }

    //取绝对值
    $a = abs($a);
    $b = abs($b);

    if($a == $b){
        return $a;
    }

    //欧几里得法 求最大公约数
    $max = max($a, $b);
    $min = min($a, $b);
    while(1){
        $max = $max % $min;
        if($max === 0){
            $maxDiviso = $min;
            break;
        }
        //两个值互相交换
        $max = $max + $min;
        $min = $max - $min;
        $max = $max - $min;
    }

    //根据最大公约数求最小公倍数
    $minMultiple = $a * $b / $maxDiviso;

    return $minMultiple;
}

$a = 12;
$b = 8;
$c = minMultiple($a, $b);
echo "{$a}和{$b}的最小公倍数是{$c} <br>";

[校园怪谈][之一]厕所的最后一个格子

马帅在北江一中读高二,是寄宿生。男生宿舍楼的厕所靠近里边楼梯,外边有一圈水池子供大家洗漱,再往里走才是厕所。厕所靠门的一面墙都是小便池,对面是蹲便,一共五个格子。大概是因为怕同学在里面玩手机的缘故,蹲便的格子没有装门。

暑假学校把一楼宿舍改成了仓库,这学期马帅他们搬到了二楼。之前二楼住的是高三的学生,北江一中各个年级的同学相互间不太友好,所以马帅他们没去过一楼往上的楼层。不知道为什么,二楼厕所最里面的那个格子很脏,打扫卫生的老大爷往往只是胡乱地清扫一下那个格子。奇怪的是学校每次检查卫生,对宿舍二楼男厕所的卫生问题从来没发表过意见。

这天晚上马帅有点拉肚子,晚上熄灯之后躲在被窝里玩了一局游戏,肚子就开始疼了起来。马帅很想去厕所,但想想现在已经是晚上十二点多了,万一被宿管老师抓住可不管是不是拉肚子,被训一顿是难免的,严重还要请家长。五分钟后,马帅一手捂着肚子蹑手蹑脚地出现在宿舍门口,还好是住在下铺,如果住在上铺的话就真的来不及了。门被推开一条缝,为了不被宿管老师发现,马帅蹲下身子,从靠近地面的地方把头伸出去左右看了看。走廊里没人。马帅冲出宿舍,一头蹿进斜对面的厕所里。进了厕所,马帅本能地往里边走,但看了看脏兮兮的最后一个格子,马帅皱了皱眉头就蹲在了倒数第二个格子里。就在马帅刚刚解决完问题之后,他听见走廊里响起了由远及近的脚步声。脚步声到了厕所的门口就突然消失了,马帅以为是查寝的老师有些紧张起来。一阵凉风吹过脸庞,马帅感到后背有些发凉,这层厕所特有的味道仿佛开始让人窒息起来。借着外面路灯从窗户射进来的光,马帅大气也不敢出一下,眼睛死死地盯着格子外边的小便池。厕所外边一点动静都没有,不像是老师,马帅又开始疑神疑鬼起来。或许是错觉,马帅看到自己的影子晃动了一下,就像烛火照出来似的。没来由地一股恐惧弥漫在马帅的心里,就这样在寂静中又过了几秒钟,马帅胡乱地擦了擦屁股提起裤子就冲出了厕所,好在厕所门口并没有遇见老师。站在走廊上马帅回过头看了一眼自己刚才蹲过的格子,格子里黑漆漆的,只有格子外的地面上散落着窗外透过来的一点光。下一秒马帅眼角的余光移到最后一个格子,宿管老师正蹲在那里叼着烟微笑地看着他。