PHP 8.5发布有一段时间了,我们团队也在生产环境逐步升级。这篇文章,我想分享一下PHP 8.5的核心新特性,以及我们是如何利用这些新特性来优化架构、提升高可用和高并发能力的。

我们的业务是一个日活百万级的Web应用,对性能和稳定性要求很高。从PHP 8.1升级到8.5的过程中,我们做了大量的测试和优化,也踩了不少坑。希望这些经验能帮到正在考虑升级的团队。

这篇文章不是简单的新特性罗列,而是结合我们的架构实践,讲讲每个新特性能解决什么问题,在高并发场景下怎么用。

PHP 8.5核心新特性概览

先快速过一下PHP 8.5的核心新特性。

第一个是属性钩子。这个特性允许你为类的属性定义get和set钩子,在属性被访问或赋值时执行自定义逻辑。这让我们可以更优雅地实现属性的访问控制和计算逻辑,不需要再写大量的getter和setter方法。

第二个是异步改进。PHP 8.5在异步方面有了进一步的增强,Fiber的性能有提升,异步IO的支持也更完善。虽然PHP的异步生态还不如Node.js成熟,但对于IO密集型的场景,已经能发挥不小的作用。

第三个是JIT编译器的优化。PHP 8.1引入了JIT,8.5对JIT做了进一步优化,特别是在CPU密集型计算场景下,性能提升明显。对于算法计算、图像处理、数据压缩等场景,JIT能带来显著的性能提升。

第四个是类型系统的增强。PHP 8.5增加了更精细的类型声明,比如更完善的联合类型、交集类型、可空类型的改进。类型系统的增强能让代码更安全,IDE的提示也更准确。

第五个是性能优化。PHP 8.5在底层做了很多性能优化,比如内存管理的改进、字符串处理的优化、数组操作的加速。整体性能比8.1有10%到20%的提升,这个提升是免费的,升级就能享受到。

第六个是新的数组函数和字符串函数。8.5增加了一些方便的内置函数,比如arrayfirst、arraylast、str_contains的增强等。这些虽然是小改进,但能让代码更简洁。

下面详细说说我们在架构中是怎么用这些新特性的。

属性钩子在领域模型中的应用

属性钩子是我们用得最多的新特性之一。在升级之前,我们的领域模型里有大量的getter和setter方法,代码很冗长。比如一个User类,可能有几十个属性,每个属性都有get和set方法,类文件动辄几百行。

用了属性钩子之后,代码简洁了很多。我们可以直接在属性声明时定义访问逻辑,不需要单独写方法。比如对于email属性,我们可以在set钩子里做格式校验;对于fullName属性,我们可以在get钩子里动态拼接firstName和lastName。

这样做的好处是,代码更简洁,可读性更好。而且属性钩子是语言层面的支持,性能比手动写getter和setter更好,因为不需要额外的方法调用开销。

在高并发场景下,属性钩子的性能优势更明显。我们的API接口经常需要返回大量的领域对象,每个对象的属性访问都要调用getter方法,这些方法调用累积起来的开销不小。用了属性钩子之后,属性访问的开销降低了,整体响应时间有改善。

不过属性钩子也有一些需要注意的地方。比如不要在set钩子里写太复杂的逻辑,不然会影响性能。还有属性钩子不能和显式的getter、setter混用,不然会混乱。我们的做法是,新代码统一用属性钩子,旧代码逐步重构。

异步在IO密集型场景的应用

我们的业务中有很多IO密集型的场景,比如调用第三方API、查询数据库、读写缓存。在PHP 8.5之前,这些操作都是同步阻塞的,一个请求里如果有多个IO操作,就得一个一个等,响应时间很长。

用了PHP 8.5的异步特性之后,我们可以把多个IO操作并行执行,大幅缩短响应时间。比如一个商品详情页接口,需要调用商品服务、库存服务、评论服务、推荐服务。以前是串行调用,总耗时是各个服务耗时的总和。现在用Fiber把这些调用并行化,总耗时变成了最慢的那个服务的耗时,响应时间缩短了一半以上。

我们的做法是,用Fiber封装异步操作,然后用一个调度器来管理多个Fiber的执行。对于简单的并行调用,我们封装了一个工具函数,可以很方便地并行执行多个闭包。

不过PHP的异步生态还不够成熟,很多库还不支持异步。比如我们用的数据库客户端,目前还是同步的。所以我们主要在调用外部HTTP API的场景下用异步,数据库操作还是用同步连接池。

还有一点要注意,异步编程的复杂度比同步高很多,错误处理和调试都更困难。我们的原则是,只在性能瓶颈明确的地方用异步,不要为了用而用。大部分场景下,同步编程已经够用了,而且更简单、更易维护。

JIT在CPU密集型场景的应用

JIT编译器是PHP 8.0引入的,8.5做了进一步优化。我们的业务中有一些CPU密集型的场景,比如图片处理、数据统计、报表生成。这些场景以前是PHP的弱项,通常需要用扩展或者外部服务来处理。

升级到PHP 8.5并开启JIT之后,这些CPU密集型操作的性能有了明显提升。比如我们的图片水印处理,开启JIT之后,处理速度提升了30%左右。报表生成中的大量数据计算,速度也提升了20%以上。

JIT的开启也很简单,在php.ini里配置opcache.jitbuffersize和opcache.jit就行。我们用的是tracing模式,这个模式会跟踪代码的执行,对热点代码进行JIT编译,效果最好。

不过JIT也不是万能的。对于典型的Web应用,大部分时间花在IO等待上,JIT的提升不明显。JIT主要对CPU密集型的代码有效。如果你的应用是IO密集型的,JIT可能带来不了多少提升。

还有一点,开启JIT会增加内存使用,因为JIT编译后的代码需要存在内存里。我们的经验是,给JIT分配128M的缓冲区就够了,太大了浪费内存,太小了会频繁回收。

类型系统在大型项目中的价值

PHP 8.5的类型系统又增强了,这对于大型项目来说非常有价值。

我们的代码库有几百万行代码,维护起来很困难。很多bug都是因为类型不明确导致的,比如传了一个null给期望字符串的函数,或者数组的结构不对。有了更完善的类型系统,这些问题在开发阶段就能被发现。

我们在新项目中全面启用了严格类型模式,所有的方法参数和返回值都有类型声明,类的属性也都有类型。配合静态分析工具,能在代码提交之前就发现很多潜在的bug。

类型系统的另一个好处是,IDE的代码提示更准确了。以前调用一个方法,不知道参数应该传什么类型,返回什么类型,现在有了类型声明,IDE能准确提示,开发效率提升不少。

在高并发场景下,类型系统也有帮助。因为类型明确了,PHP引擎可以做更多的优化,比如类型专门化、方法内联等。虽然单个请求的提升不明显,但在高并发下,累积起来的性能提升还是可观的。

当然,全面启用类型系统需要一个过程。我们是从新代码开始,逐步向旧代码推广。对于历史代码,我们用静态分析工具找出类型问题,然后逐步修复。这个过程虽然慢,但值得做。

高可用架构设计

除了语言层面的新特性,我们在架构层面也做了很多优化,来保证高可用和高并发。

第一个是无状态设计。我们的应用服务器都是无状态的,任何请求都可以发到任何一台服务器。这样做的好处是,某台服务器挂了,流量可以自动切换到其他服务器,不影响用户。会话信息存在Redis里,不存在本地。

第二个是多活部署。我们在多个可用区部署了应用,每个可用区都有完整的服务栈。正常情况下,流量按权重分配到各个可用区;某个可用区出问题时,流量自动切到其他可用区。这样即使整个可用区挂了,服务也不会中断。

第三个是服务降级和熔断。对于非核心功能,我们做了降级处理。比如推荐服务挂了,就显示默认推荐,而不是让整个页面报错。对于外部依赖,我们用了熔断器,当依赖服务失败率超过阈值时,自动熔断,避免雪崩效应。

第四个是限流和排队。对于突发流量,我们做了限流保护。超过限流阈值的请求,进入排队系统,异步处理。这样可以避免系统被突发流量打垮,保证核心功能的可用性。

第五个是健康检查和自动恢复。我们有完善的健康检查机制,定时检测各个服务的健康状态。发现异常时,自动告警,并且尝试自动恢复,比如重启服务、切换流量。大部分故障都能在无人干预的情况下自动恢复。

高并发优化实践

在高并发优化方面,我们也积累了一些经验。

第一个是缓存策略。我们用了多级缓存,浏览器缓存、CDN缓存、应用层缓存、Redis缓存。能缓存的都缓存,尽量减少对数据库的直接访问。对于热点数据,我们用本地缓存加Redis的二级缓存,进一步降低延迟。

第二个是数据库优化。数据库是高并发下最容易成为瓶颈的地方。我们做了读写分离,写操作走主库,读操作走从库。对于大表,做了分库分表。慢查询定期优化,该加索引的加索引,该改SQL的改SQL。

第三个是队列异步化。对于非实时的操作,比如发邮件、发短信、生成报表,都放到消息队列里异步处理。这样可以快速响应用户请求,把耗时的操作放到后台。

第四个是连接池。数据库连接、Redis连接、HTTP连接,都用了连接池。避免每次请求都新建连接,减少连接建立的开销。连接池的大小要根据实际情况调优,太小了会等待,太大了会浪费资源。

第五个是压力测试和容量规划。每次大促之前,我们都会做压力测试,找出系统的瓶颈,提前优化。同时做好容量规划,根据预估的流量准备足够的服务器资源。有备无患,才能在高并发下保持稳定。

升级过程中的坑

最后说说升级到PHP 8.5过程中遇到的坑。

第一个坑是扩展兼容性。有些PHP扩展还不支持8.5,升级之后这些扩展就用不了了。我们升级之前,先检查了所有用到的扩展,确认都有8.5的版本。对于不支持的扩展,要么找替代方案,要么等扩展更新。

第二个坑是废弃特性。PHP 8.5废弃了一些旧的特性,比如某些旧的函数、某些语法写法。我们的代码库里有一些历史代码用了这些特性,升级之后报错。我们用静态分析工具扫描了所有代码,找出不兼容的地方,逐一修复。

第三个坑是性能回退。升级之后,我们发现某些接口的性能反而下降了。排查之后发现,是因为JIT的配置不对,某些代码在JIT编译之后反而变慢了。调整了JIT的配置之后,性能恢复了。所以升级之后一定要做全面的性能测试,不能想当然地认为新版本一定更快。

第四个坑是测试覆盖。升级之前,我们以为测试覆盖得不错,结果上线之后还是出了一些问题。主要是一些边缘场景没有覆盖到。所以升级之前,一定要做充分的回归测试,特别是核心业务流程。

我们的升级策略是灰度发布。先在测试环境跑一段时间,然后在生产环境选一小部分流量切到PHP 8.5,观察没有问题之后再逐步扩大。整个升级过程持续了一个多月,虽然慢,但很稳。

写在最后

PHP 8.5是一个很扎实的版本,在性能、类型安全、语法特性上都有进步。对于还在使用旧版本的团队,我建议尽早升级。升级带来的不仅是性能提升,还有代码质量和开发效率的提升。

但升级不是目的,用好新特性、优化架构、提升系统的高可用和高并发能力,才是目的。语言只是工具,架构和工程实践才是关键。

希望这篇文章能给正在使用PHP的朋友一些参考。如果有不同的观点或者补充,欢迎交流。

最后用一句话来结束这篇文章:技术选型没有银弹,适合自己业务的才是最好的。

愿每一个PHPer,都能在自己的业务场景中,写出高性能、高可用的代码。