### 2018 年 11 月 18 日 发布 说到应用性能,涉及到的方方面面实在是太多了,关于服务器优化和前端优化的文章网上很多,所以就不再累述了。本文仅抛砖引玉浅述下ThinkPHP`5.1`应用开发中(尤其是部署环境)可能涉及的一些性能优化手段和注意事项。 >[danger] 首先要强调一点:框架并不是应用性能的瓶颈,架构设计、数据库和人才是。框架在设计之初,出于通用性考虑,不会特意针对某个应用做深入优化,但提供了一些可能的手段和配置参数让你进行针对性的调优,下面就列举一些可能的优化手段,在开发的时候可以根据情况调整。 正确的性能优化步骤应该是:架构优化、数据库优化、代码优化。 ## 架构优化 架构优化涉及到技术、存储、网络、服务的选型和构架,尽量使用成熟和现代的开发架构和设计模式。前后端完全分离设计,便于前后端的独立优化,也更加便于测试工作。 如果你的应用遇到了性能瓶颈,这个时候要考虑的就是优化架构而不是优化代码本身,因为架构层面的优化效果往往是最显著的。 >[info] 架构的优化需要根据自身运营情况来调整,切忌不可按图索骥提前优化,反而容易得不偿失,导致技术成本提高甚至“负优化”。 ## 关闭调试模式 部署环境千万不要忘记关闭调试模式,这不仅仅是出于性能考虑,更多是基于安全因素。事实上,建议通过环境变量来配置关闭调试模式,这样部署后不需要更改任何配置文件。 因为调试模式影响日志记录信息、额外的调试信息和缓存失效,关闭调试模式能够带来一定的性能提升。 ## 使用单模块 使用多模块功能会增加文件的`I/O`开销和额外的配置及检查,如非必要在规划你的应用架构的时候尽量考虑使用单一模块,然后使用控制器分级来解决控制器过多的问题。 使用单一模块的性能优势,在部署到`swoole`的时候可以得到更加充分的体现,因为应用文件一旦启动服务,就会载入内存,而模块的相关文件则会每次请求重新加载。 ## 路由设计及优化 在定义路由规则的时候,不要使用数组方式,尽量使用方法注册路由,并且多使用路由分组(或者资源路由)。分组路由可以减少路由的匹配次数,从而提升路由性能。如果你有多个域名的不同路由,也要按域名规划使用路由。 尽可能设计在路由中进行当前路由的数据验证和权限检查等操作,一方面比较清晰,另外一方面可以尽量把验证操作提前,而不必等到控制器执行。 在分组比较多的情况下,开启路由的延迟解析。 ``` // 开启路由延迟解析 'url_lazy_route' => true, ``` 如果同一个分组下面有比较多的路由规则,建议合并路由规则。 ``` // 合并分组路由规则 'route_rule_merge' => true, ``` 对于`GET`请求的路由,可以设置路由的请求缓存。 ``` // 定义GET请求路由规则 并设置3600秒的缓存 Route::get('new/:id','News/read')->cache(3600); ``` 部署阶段,可以开启路由缓存。 ``` // 开启路由缓存(仅部署模式有效) 'route_check_cache' => true, ``` ## 查询优化 首先保持良好的开发习惯,了解[Db类和模型的正确使用姿势](https://blog.thinkphp.cn/810719),数据库本身的性能优化可以参考[MySQL性能优化的最佳21条经验](http://www.thinkphp.cn/topic/54536.html),下面主要是对框架中数据查询相关的优化策略。 ### 合理使用查询缓存 尽量减少每次请求的查询次数,并对实时性要求不高的数据查询合理规划数据查询缓存(优先考虑使用`Redis`缓存)。 ``` Blog::where('id', 10) ->cache(30) ->find(); ``` 如果使用了关联查询,`cache`方法只能用于主模型的数据缓存,但你可以使用`Cache`类的`remember`方法进行方便的数据缓存。 ``` $users = Cache::remember('users', function(){ return User::with('profile') ->where('status', 1) ->select(); },30); ``` ### 不要过于纠结查询次数 尽量减少查询次数是出于性能考虑,但不是必须,使用最少的查询不代表性能就一定是最高。一个复杂的`JOIN`查询性能不见得有两次简单的查询高,而使用简单的查询反而更清晰易懂,并且更方便进行数据查询缓存。 ### 正确使用模型关联 不要总是以为模型的性能一定比`Db`类低,框架的ORM查询设计经过了较为合理的优化,正确使用模型一样可以有出色的性能,而且比`Db`查询要方便很多。 尤其是对于一些复杂的设计来说使用模型关联显得比直接用Db更加简单,例如使用关联预载入查询就可以避免`N+1`查询问题。 ``` User::with(['profile','book'])->select(); ``` 如果用`Db`类自己实现的话,费时费力,性能还不一定优。 ### 大量数据处理优化 对于大量数据的处理操作,使用`chunk`分批处理方法。 ``` User::chunk(100, function($users) { foreach ($users as $user) { // 处理数据 } }); ``` 对于内存开销比较大的应用,在做大量数据查询和处理的时候,使用`cursor`方法,可以利用PHP的生成器特性,减少内存占用。 ``` $cursor = User::cursor(); foreach($cursor as $user){ // 处理数据 } ``` 你会发现用户数据不论是1万还是10万级别,内存开销并没有大的变化。 >[danger] 涉及到对大量数据的处理,包括数据迁移、批量更新,尽量使用命令行指令运行,否则会因为超时而中断。 ### 善用数据集方法避免多次查询 可以通过数据集的方法完成的子集或者排序操作不要再次查询,例如: ``` // 模型查询返回数据集对象 $users = User::select(); // 按照用户的成绩由高到低排序 $list1 = $users->order('score', 'desc'); // 筛选成绩在90分以上的用户 $list2 = $users->where('score', '>=', 90); ``` ### 字段缓存 利用下面指令在部署后生成字段缓存,可以减少每次数据表的字段查询开销。 ``` php think optimize:schema ``` 更多用法可以参考官方手册的[数据字段缓存](https://www.kancloud.cn/manual/thinkphp5_1/354145)。 >[danger] 注意:一旦数据库的表结构发生变化,必须重新生成。 ## 配置和公共文件缓存 每次在应用初始化或者模块初始化的时候会有一定的`I/O`开销,如果已经开启`OpCache`的话对性能影响甚微,如果比较在意的也可以通过命令行指令生成配置缓存(包括相关的公共文件和各种定义文件)。 生成应用配置缓存: ``` php think optimize:config ``` 生成模块配置缓存: ``` php think optimize:config index ``` >[danger] 注意:一旦配置或者公共文件发生变化,必须重新生成。 ## 生成类库映射 类库映射可以提升类库的自动加载性能,使用下面的指令可以生成系统类库和应用类库的类库映射(包括`extend`目录下的类库)。 ``` php think optimize:autoload ``` `vendor`目录下的类库可以使用`composer`的`dump-autoload`指令优化加载性能。 ``` composer dump-autoload -o ``` 该命令把 `PSR-0` 和 `PSR-4` 转换为一个类映射表,来提高类的加载速度。