作者归档:admin

PHP缓存之APC-简介、存储结构和操作

APC简介

APC,全称是Alternative PHP Cache,官方翻译叫”可选PHP缓存”。它为我们提供了缓存和优化PHP的中间代码的框架。 APC的缓存分两部分:系统缓存和用户数据缓存。

  • 系统缓存 它是指APC把PHP文件源码的编译结果缓存起来,然后在每次调用时先对比时间标记。如果未过期,则使用缓存的中间代码运行。默认缓存 3600s(一小时)。但是这样仍会浪费大量CPU时间。因此可以在php.ini中设置system缓存为永不过期(apc.ttl=0)。不过如果这样设置,改运php代码后需要重启WEB服务器。目前使用较多的是指此类缓存。
  • 用户数据缓存 缓存由用户在编写PHP代码时用apc_store和apc_fetch函数操作读取、写入的。如果数据量不大的话,可以一试。如果数据量大,使用类似memcache此类的更加专著的内存缓存方案会更好。

在APC中我们也可以享受APC带来的缓存大文件上传进度的特性,需要在php.ini中将apc.rfc1867设为1,并且在表单中加一个隐藏域 APC_UPLOAD_PROGRESS,这个域的值可以随机生成一个hash,以确保唯一。之前的一篇文章PHP文件上传进度的实现原理中有对此更为细致的说明。

APC与PHP内核的交互

APC是作为一个扩展添加到PHP体系中的。因此,按照PHP的扩展规范,它会有PHP_MINIT_FUNCTION、PHP_MSHUTDOWN_FUNCTION、PHP_RINIT_FUNCTION、PHP_RSHUTDOWN_FUNCTION等宏定义的函数。在PHP_MINIT_FUNCTION(apc)中有调用apc_module_init中,并且在此函数中通过重新给zend_compile_file赋值以替换系统自带的编译文件过程,从而将APC自带的功能和相关数据结构插入到整个PHP的体系中。

这里会有一个问题,如果出现多个zend_compile_file的替换操作呢?在实际使用过程,这种情况会经常出现,比如当我们使用xdebug扩展时,又使用了apc,此时PHP是怎么处理的呢?不管是哪个扩展,在使用zend_compile_file替换时,都会有一个自己的compile_file函数(替换用),还有一个作用域在当前扩展的,一个旧的编译函数:old_compile_file。相当于每个扩展当中都保留了一个对于前一个编译函数的引用,形成一个单向链表。并且,所有最终的op_array都是在新的zend_compile_file中通过old_compile_file生成,即都会沿着这条单向链表,将编译的最终过程传递到PHP的zend_compile_file实现。在传递过程中,每经过一个节点,这些节点都会增加一些属于自己的数据结构,以实现特定的需求。

APC内部存储结构

在APC内部,对于系统缓存和用户缓存分别是以两个全局变量存储,从代码逻辑层面就隔离了两种缓存,当然,这两种存储的实现过程和数据结构是一样的,它们都是apc_cache_t类型,如下:

 
    /* {{{ struct definition: apc_cache_t */
    struct apc_cache_t {
        void* shmaddr;                共享缓存的本地进程地址
        cache_header_t* header;       缓存头,存储在共享内存中
        slot_t** slots;               缓存的槽数组,存储在共享内存中
        int num_slots;                存储在缓存中的槽个数
        int gc_ttl;                   GC列表中槽的最大生存时间
        int ttl;                      如果对槽的访问时间大于这个TTL,需要则移除这个槽
        apc_expunge_cb_t expunge_cb;  /* cache specific expunge callback to free up sma memory */
        uint has_lock;                为可能存在的造成同一进程递归锁而存在的标记 /* flag for possible recursive locks within the same process */
    };
    /* }}} */

对于一个缓存,apc_cache_t类型的变量是其入口,它包含了这个缓存的一些全局信息。每个缓存都会有多个缓存槽,包含在slots字段中,slots的个数包含在num_slots字段,槽的过程时间控制在于ttl字段。对于用户缓存和系统缓存,默认情况下系统缓存数量为1000,实际上APC创建了1031个,也就是说默认情况下APC最少可以缓存1031个文件的中间代码。当然这个值还需要考虑内存大小,计算slot的key后的分布等等。更多的关于缓存的统计信息存储在header字段中,header字段结构为cache_header_t,如下:

 
struct cache_header_t {
        apc_lck_t lock;             读写锁,独占阻塞缓存锁
        apc_lck_t wrlock;           写锁,为防止缓存爆满
        unsigned long num_hits;     缓存命中数
        unsigned long num_misses;   缓存未命中数
        unsigned long num_inserts;  插入缓存总次数
        unsigned long expunges;     清除的总次数
        slot_t* deleted_list;       指向被清除的槽的链表
        time_t start_time;          以上计数器被重置的时间
        zend_bool busy;             当apc在忙于清除缓存时告诉客户端此时状态的标记
        int num_entries;            统计的实体数
        size_t mem_size;            统计的被用于缓存的内存大小
        apc_keyid_t lastkey;        用户缓存最后一写入的key
    };

一个缓存包含多个slots,每个slot都是一个slot结构体的变量,其结构如下:

 
    struct slot_t {
        apc_cache_key_t key;        槽的key
        apc_cache_entry_t* value;   槽的值
        slot_t* next;               链表中的下一个槽
        unsigned long num_hits;     这个bucket的命中数/* number of hits to this bucket */
        time_t creation_time;       槽的初始化时间
        time_t deletion_time;       槽从缓存被移除的时间 /* time slot was removed from cache */
        time_t access_time;         槽的最后一次被访问的时间
    };

每个槽包含一个key,以apc_cache_key_t结构体存储;包含一个值,以apc_cache_entry_t结构体存储。如下:

 
    typedef struct apc_cache_key_t apc_cache_key_t;
    struct apc_cache_key_t {
        apc_cache_key_data_t data;
        unsigned long h;              /* pre-computed hash value */
        time_t mtime;                 /* the mtime of this cached entry */
        unsigned char type;
        unsigned char md5[16];        /* md5 hash of the source file */
    };

结构说明如下:

  • data字段 apc_cache_key_data_t类型,一个联合体,存储key的关联信息,比如对于系统缓存,其可能会存储文件的路径或OS的文件device/inode;对于用户缓存可能会存储用户给定的标识或标识长度。
  • h字段 文件完整路径或用户给定的标识的hash值,使用的hash算法为PHP自带的time33算法;或者文件所在device和inode的和
  • mtime字段 缓存实体的修改时间
  • type字段 APC_CACHE_KEY_USER:用户缓存; APC_CACHE_KEY_FPFILE:系统缓存(有完整路径); APC_CACHE_KEY_FILE: 系统缓存(需要查找文件)
  • md5字段 文件内容的MD5值,这个字段与前面四个字段不同,它是可选项,可以通过配置文件的apc.file_md5启用或禁用。并且这个值是在初始化实体时创建的。看到这里源文件的md5值,想起之前做过一个关于MySQL数据表中访问路径查询的优化,开始时通过直接查询路径字段,在数据量达到一定级别时,出现了就算走索引还是会很慢的情况,各种方案测试后,采用了以新增一个关于访问路径的md5值查询解决。

除了入口,APC在最终的数据存储上对于系统缓存和用户缓存也做了区分,在_apc_cache_entry_value_t分别对应file和user。

 
    typedef union _apc_cache_entry_value_t {
        struct {            
            char *filename; /* absolute path to source file */
            zend_op_array* op_array;     存储中间代码的op_array
            apc_function_t* functions; /* array of apc_function_t's */
            apc_class_t* classes; /* array of apc_class_t's */
            long halt_offset; /* value of __COMPILER_HALT_OFFSET__ for the file */
        } file;                         file结构体 系统缓存所用空间,包括文件名,,
        struct {
            char *info;
            int info_len;
            zval *val;
            unsigned int ttl;           过期时间
        } user;                         ser结构体 用户缓存所用空间
    } apc_cache_entry_value_t;

如图所示:

APC缓存存储结构

APC缓存存储结构

初始化

在APC扩展的模块初始化函数(PHP_MINIT_FUNCTION(apc))中,APC会调用apc_module_init函数初始化缓存所需要的全局变量,如系统缓存则调用apc_cache_create创建缓存全局变量apce_cache,默认情况下会分配1031个slot所需要的内存空间,用户缓存也会调用同样的方法创建缓存,存储在另一个全局变量apc_user_cache,默认情况下会分配4099个内存空间。这里分配的空间的个数都是素数,在APC的代码中有一个针对不同数量的素数表primes(在apc_cache.c文件)。素数的计算是直接遍历素数表,找到表中第一个比需要分配的个数大的素数。

缓存key生成规则

APC的缓存中的每个slot都会有一个key,key是 apc_cache_key_t结构体类型,除了key相关的属性,关键是h字段的生成。 h字段决定了此元素落于slots数组的哪一个位置。对于用户缓存和系统缓存,其生成规则不同。

  • 用户缓存通过apc_cache_make_user_key函数生成key。通过用户传递进来的key字符串,依赖PHP内核中的hash函数(PHP的hashtable所使用的hash函数:zend_inline_hash_func),生成h值。
  • 系统缓存通过apc_cache_make_file_key函数生成key。通过APC的配置项apc.stat的开关来区别对待不同的方案。在打开的情况下,即 apc.stat= On 时,如果被更新则自动重新编译和缓存编译后的内容。此时的h值是文件的device和inode相加所得的值。在关闭的情况下,即apc.stat=off时,当文件被修改后,如果要使更新的内容生效,则必须重启Web服务器。此时h值是根据文件的路径地址生成,并且这里的路径是绝对路径。即使你是使用的相对路径,也会查找PG(include_path)定位文件,以取得绝对路径,所以使用绝对路径会跳过检查,可以提高代码的效率。

添加缓存过程

以用户缓存为例,apc_add函数用于给APC缓存中添加内容。如果key参数为字符串中,APC会根据此字符串生成key,如果key参数为数组,APC会遍历整个数组,生成key。根据这些key,APC会调用_apc_store将值存储到缓存中。由于这是用户缓存,当前使用的缓存为apc_user_cache。执行写入操作的是apc_cache_make_user_entry函数,其最终调用apc_cache_user_insert执行遍历查询和写入操作。与此对应,系统缓存使用apc_cache_insert执行写入操作,其最终都会调用_apc_cache_insert。

不管是用户缓存还是系统缓存,大体的执行过程类似,步骤如下:

  1. 通过求余操作,定位当前key的在slots数组中的位置: cache->slots[key.h % cache->num_slots];
  2. 在定位到slots数组中的位置后,遍历当前key对应的slot链表,如果存在slot的key和要写入的key匹配或slot过期,清除当前slot。
  3. 在最后一个slot的后面插入新的slot。

几个 PHP 的“魔术常量”

PHP向它运行的任何脚本提供了大量的预定义常量。
不过很多常量都是由不同的扩展库定义的,只有在加载了这些扩展库时才会出现,或者动态加载后,或者在编译时已经包括进去了。

有七个魔术常量它们的值随着它们在代码中的位置改变而改变。例如 __LINE__ 的值就依赖于它在脚本中所处的行来决定。这些特殊的常量不区分大小写。在手册中这几个变量的简单说明如下:
几个 PHP 的“魔术常量”

名称 说明
__LINE__ 文件中的当前行号。
__FILE__ 文件的完整路径和文件名。如果用在被包含文件中,则返回被包含的文件名。自 PHP 4.0.2 起,__FILE__ 总是包含一个绝对路径(如果是符号连接,则是解析后的绝对路径),而在此之前的版本有时会包含一个相对路径。
__DIR__ 文件所在的目录。如果用在被包括文件中,则返回被包括的文件所在的目录。它等价于 dirname(__FILE__)。除非是根目录,否则目录中名不包括末尾的斜杠。(PHP 5.3.0中新增) =
__FUNCTION__ 函数名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该函数被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。
__CLASS__ 类的名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该类被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。
__METHOD__ 类的方法名(PHP 5.0.0 新加)。返回该方法被定义时的名字(区分大小写)。
__NAMESPACE__ 当前命名空间的名称(大小写敏感)。这个常量是在编译时定义的(PHP 5.3.0 新增)

在写这篇文章时,由于把思维局限在语法解析和运行时赋值,导致寻找了好久都没有发现实现原因。还是网上的一篇文章将我们的思维找回到词法分析。PHP内核会在词法解析时将这些相对静态的内容赋值给这些变量,而不是在运行时执行的分析。如下PHP代码:

<?PHP
echo __LINE__;
function demo() {
	echo __FUNCTION__;
}
demo();

其实PHP已经在词法解析时将这些常量换成了对应的值,以上的代码可以看成如下的PHP代码:

<?PHP
echo 2;
function demo() {
	echo "demo";
}
demo();

如果我们使用VLD扩展查看以上的两段代码生成的中间代码,你会发现其结果是一样的。

前面我们有说PHP是在词法分析时做的赋值替换操作,以__FUNCTION__为例,在Zend/zend_language_scanner.l文件中,__FUNCTION__是一个需要分析的元标记(token):

<ST_IN_SCRIPTING>"__FUNCTION__" {
	char *func_name = NULL;
 
	if (CG(active_op_array)) {
		func_name = CG(active_op_array)->function_name;
	}
 
	if (!func_name) {
		func_name = "";
	}
	zendlval->value.str.len = strlen(func_name);
	zendlval->value.str.val = estrndup(func_name, zendlval->value.str.len);
	zendlval->type = IS_STRING;
	return T_FUNC_C;
}

就是这里,当当前中间代码处于一个函数中时,则将当前函数名赋值给zendlval,如果没有,则将空字符串赋值给zendlval(因此在顶级作用域名中直接打印__FUNCTION__会输出空格)。这个值在语法解析时会直接赋值给返回值。这样我们就在生成的中间代码中看到了这些常量的位置都已经赋值好了。

和__FUNCTION__类似,在其附近的位置,上面表格中的其它常量也进行了类似的操作。在PHP5.4中增加了对于trait类的常量定义:__TRAIT__。

这些常量其实相当于一个常量模板,或者说是一个占位符,在词法解析时这些模板或占位符就被替换成实际的值

转岗一年总结

去年的这个月,我转岗了,不再是一个把代码写好,写干净的纯粹程序员。有几分失落,有几分彷徨,虽然有些无所适从,但是还是做了下去,自己选的路,得自己走下去。

一年了,整整一年了。过程中有一些过错,也有一些收获,而最近团队也发生了一些事情,并因这些事情被菊爷严肃的批评了(批评的非常好)。于是就有了今天这篇文章,以此反思自己一年的工作历程。

当面临一个机会,选择向左走 OR 向右走?想想自己适合什么?最终要的是什么?得失之间,做了决定,如此,一路走下去。

从招聘开始

招聘是一个识人的过程,对于一个新手,还是一个曾经专注于技术的新手,这时自己对于技术的偏执就彻底的显示出来了,希望自己的团队成员会是专业的人员,希望自己的团队中能进来一些高手,一句话:技术好才是真的好。依着这样的标准,各种面试,发现自己进了一个死循环,如一些爱情故事: ”爱我的人我不爱,我爱的人不爱我“。被打击后,尝试着降低自己的标准……

但是这一年来招的人最后留下来超过一年的并不多,什么原因?与团队文化不符?没有给他带来所要的东西?或者根本与团队融合不了。新成员与团队的融合和飞机飞行一样,起飞的时候都很难,但还是努力的冲向天空,遇到气流,还会有一些颠簸 …… 最后,不到终点不能换乘别的航班。是什么让他们选择换了别一趟航班?

反思自己这一年招聘历程,离开的人都是我的识人水平有问题,根本就没有认清你想要的是什么样的人,或者说什么样的人适应你的团队。认识到在招聘面试过程中需要关注的第一条:一个将要进入团队的人必须需要符合你的公司文化和团队文化,如果这个都不符合,再好的技术也没有用,能力越强,对于整个团队的破坏性就越大。也体会到书上说的:面试并不是一个几个小时就可以做完的活动,这是一个持续的动作,当一个新人通过正常的面试流程到了你的团队,作为团队leader,你就要开始你的实战面试,在三个月的试用期,一定要强烈跟进新人的状态和与团队的整合进度,当发现新人不可用,或确实与团队不符合时,一定要立即做出处理,否则会对整个团队产生影响,至于换成什么人那是下一个问题。

新人来了后,确实没有花费太多的心思,任凭他们自己适应这个团队,这个需要自省的地方!老大说任何一个团队成员的成长都会付出许多的精力和心血,部门和公司都还小,还没有绝对优势的薪酬和福利、还没有绝对的精神领袖来吸引人,更多的是尽力给团队的成员创造好的成长环境,提供一个相当公平、朝气的工作氛围,提供犯错的机会。

在人和事中往返

转岗后,更多的时间是在做项目经理,各种流程和文档,当然还有编码,虽然这已经比较少了。一段时间后,项目经理也没有做了,将项目直接交给团队成员负责,而自己只做一些代码review和进度check操作。忽然发现自己离实际的项目越来越远,对一些业务已经不太了解了。直到有一天,老大在月度总结后问我,你上个月都干嘛了,才发现部门的项目自己一个也没参加,而自己也不知道自己在忙些什么!终于,让自己迷茫了。每天忙忙碌碌,却不知为何而忙!

一个leader,始终需要关注人和事。向上走,人更关注些,向下走,事更关注些。人不好,事也不会做得好。前面已经说了如果让一个人成为团队的一员,这是团队搭建的水平;而当一个人成为团队成员后,如何让其可以安心留下来,这是团队管理的水平。一个leader既要有团队搭建的能力也要有团队管理的能力。对于团队管理我只能说我还没有入门,还在找属于我自己的门。但是我会秉承一些原则:

  • 发现并留住可自我成长的人才
  • 对事不对人
  • 可以犯错,错不过三

余世维在《经理人五项修炼》中说,一个领导需要做三件事:第一,思考你的战略,第二,计划你的工作,第三,教育好你的员工。我们还没到这个层次,但是计划和成员管理还是需要做的。

“凡事豫则立,不豫则废。“。在工作的过程中,计划是必不可少的环节,每周都会有计划,团队的计划、团队成员的计划,个人的计划,都会去做,按既定的目标前行。一周复一周,周计划,周总结,以周为时间单位粒度的确认,多是按惯例去执行,却没有自己的章法和思路。基于此,以《一页纸项目管理》中的项目进度表为蓝本,进行简化,去掉与部门实际不符的内容,增加人员维度的考量,以项目为一个整体,以每周五为检查点,实现部门事务的整体跟进。这与实际的项目管理工作没有太多的关系,项目管理还是基于公司的项目管理系统。

对于团队成员,菊爷说:”因为你是一个领导,所以你必须对每一个手下有着灵敏的触觉!他们的一举一动,他们的思维反应,他们的耐心躁动,他们的激进疲软,你必须在第一时间做到心中有数,而不是等不相关的其他人发现后,你才后知后觉甚至还发出质问为何有这样的情况发生!你必须有这个触觉跟担当。”。于此,需要做到心如明镜,何其困难。团队管理的细化,具体到每个成员,他们的诉求是什么?他们的性格是什么?他们最近家里是否有发生什么事情?有小孩子了?家里老人过来了?……

leader应该具备的三气

才气
技术人以技术为本,技术是基础,不能落下。
文艺的气息,能写文章,能写PPT,以在众人面前良好的表述自己的想法和思路。
能够及时的解决问题,遇到难点能让人想到你可以解决。
这就是你的才气。才气的体现是得让人说你行,并且说你行的人得行。

霸气
这是一种气场,是在知晓如何去做、为何去做之后所带来的胸有成竹。
遇到问题和争论能够拍板,具有话语权,说一不二,一口吐沫能砸一个坑
团队成员能够信服你。

大气
眼界高,能够从更长的时间维度和更大的空间维度看问题。
大局观好,不能局限于一个部门一小块业务去看问题。
心胸宽广,大肚能容天下可容之事。

除此之外,还需要有较强的沟通能力、执行力、积极的反馈以及以身作则的态度等等。

最后的感恩

一年了。
感谢那些伤害了我的人,是他们让我成长;
感谢那些帮助了我的人,是他们让我感到了温暖;
感谢给我试错机会的人,是他们让我能够遇到这些人这些事;
感谢家里的人,回家的温暖让疲惫的心有一个可以休息的港湾。