最近在做PHP接口的性能优化,在排查性能问题和优化的过程中总结了一些心得,分享给大家。
性能问题排查
首先,做性能优化要先进行性问题排查,即排查PHP接口的代码实现中那一块执行比较慢。
排查方法一
笔者使用的ThinkPHP框架中自带了G方法可以很方便的获取某个区间的运行时间和内存占用情况。例如:
G(‘begin’);
// …其他代码段
G(‘end’);
// …也许这里还有其他代码
// 进行统计区间
echo G(‘begin’,‘end’).‘s’;//输出代码运行时间
echo G(‘begin’,‘end’,‘m’).‘kb’;//输出内存开销统计(单位为kb)
通过对不同代码片段的运行时间统计与接口总运行时间的对比,可以分析出哪块代码的运行存在性能问题,然后再针对性的排查。
排查方法二
使用PHP性能分析工具来辅助定位。目前市面上有很多PHP性能分析工具,老牌的调试工具XDebug,以及Facebook开源的Xhprof。以Xhprof举例,配置成功后,可以记录和生成函数的调用链图,包含每个函数的请求次数、阻塞时间、CPU时间和内存使用情况。
图中标记为红色或黄色的区域就是需要重点关注的地方。
性能问题解决
通常在工具的辅助下,性能问题排查相对来说还是比较简单的。比较有挑战的是如何解决这些性能问题。笔者在解决性能问题的过程中也做了一些解决方式的总结:
一. 代码层面
代码的风格因人而异,实现方式也非常灵活。两个人实现同样的查询列表功能,代码写法都会不同。
以下几点是我个人总结的代码性能避坑指南:
1. 使用PHP7+版本。PHP7发布的时候号称比PHP5.6快两倍,而最新的PHP 7.3比PHP 5.6快3倍!所以升级PHP版本的收益还是很大的。 1. 尽量减少循环嵌套写法。循环嵌套增加所带来的运行消耗是指数级的。例如第一层循环执行100次,第二层循环执行100次,第三层循环执行100次,那么最终循环的运行次数就是100100100 = 一百万次。所以要尽量减少循环的嵌套。 1. 尽量减少数据库操作,例如避免在循环中进行数据库查询。数据库操作是比较消耗性能的动作,如果可以尽量少的做数据库操作,性能会有很大的提升。 1. 尽量将对象的声明写在循环外部以减少声明次数。 1. 使用unset()及时释放不使用的变量尤其是大数组,以便释放内存。 1. 方法尽量静态化。静态方法在程序开始时生成内存,实例方法在程序运行中生成内存,所以静态方法可以直接调用,实例方法要先成生实例,通过实例调用方法,静态速度很快,但是多了会占内存。 1. 尽量少用正则表达式。正则表达式的开销大,使用起来简单,但是性能低。因为正则表达式需要回溯;正则表达式越长,回溯的开销越大,优化正则表达式是需要技术水平的,正则技术不达标,不要乱用正则。 1. 使用单引号。在php中,单引号和双引号是有区别的,作为一种习惯字符串我都用单引号,因为它无需编译。
二. 数据库层面
1. sql查询优化
排查问题过程中,会发现很多性能的瓶颈在于复杂sql的查询太慢。我们可以使用‘EXPLAIN ’关键字来分析复杂sql的运行慢的原因。将‘EXPLAIN’加在sql语句开始的位置,就能分析出这条SQL语句的执行计划,查看该SQL语句有没有使用索引,有没有做全表扫描等等。例如:
EXPLAIN select * from person where dept_id =(select did from dept where dname =‘python’);
概要描述:
- **id**: 查询序列号,代表执行的先后顺序。- **select_type**:表示查询的类型。例子中的primary和subquery分别代表最外层查询和子查询。- **table**:显示这一步所访问数据库中表名称。- **type**:表示查询计划的连接类型。例子中的ALL代表MySQL将遍历全表以找到匹配的行,没有使用任何索引。这时就说明需要加索引进行优化了。性能由好到差:null > system/const > eq_ref > ref > ref_or_null >index_merge > range > index > all;如果有类型是 ALL 时,表示预计会进行全表扫描(full table scan)。通常全表扫描的代价是比较大的,建议创建适当的索引,通过索引检索避免全表扫描。- **possible_keys**:指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出。- **key**:显示MySQL实际决定使用的键(索引),必然包含在possible_keys中。- **key_len**:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。不损失精确性的情况下,长度越短越好。- **ref**:列与索引的比较,表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。- **rows**:估算出结果集行数,表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数。- **Extra**:该列包含MySQL解决查询的详细信息。如果有出现 Using temporary 或者 Using filesort 则要多加关注:- **Using temporary**,表示需要创建临时表以满足需求,通常是因为GROUP BY的列没有索引,或者GROUP BY和ORDER BY的列不一样,也需要创建临时表,建议添加适当的索引。- **Using filesort**,表示无法利用索引完成排序,也有可能是因为多表连接时,排序字段不是驱动表中的字段,因此也没办法利用索引完成排序,建议添加适当的索引。- **Using where**,通常是因为全表扫描或全索引扫描时(type 列显示为 ALL 或 index),又加上了WHERE条件,建议添加适当的索引。
2. 分库分表
基本思想就要把一个数据库切分成多个部分放到不同的数据库(server)上,从而缓解单一数据库的性能问题。
要做分库分表之前,首先要思考一个问题:MySQL 单表数据达到多少时才需要考虑分库分表?网上流传的文章有的说几千万行,有的说几百万行,都没有一个准确的数字。阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
由于笔者所要优化的数据库单表还达不到这个量级,所以没有进行分库分表的实际操作。但是笔者又深入探索了一个问题:这个500W是怎么推算出来的?
事实上,这个数值和实际记录的条数无关,而与 MySQL 的配置以及机器的硬件有关。因为,MySQL 为了提高性能,会将表的索引装载到内存中。InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还有具体的表结构的设计有关,最终导致的问题都是内存限制。所以,如果增加硬件配置,可能会带来立竿见影的性能提升。
三. 硬件配置
这种方式不用多介绍了。线上压力顶不住的时候,通常都会立即加机器加配置来抗。久而久之,很多开发人员对加机器加配置产生了依赖。虽然增加硬件配置是最简单粗暴的解决方案,但是作为有追求的技术人员,还是要多从代码层面和数据库层面做深度优化。所以这条解决方案笔者放在最后推荐大家使用。
Q