欢迎来到「我是真的狗杂谈世界」,关注不迷路
略读
- CGI+同步阻塞方案异步任务方案有点重,但又不想放弃傻瓜方案的优点;
- 思考后将问题转换成:
- 提前返回响应后继续同步执行非重要任务;
- 顺序编写逻辑,延迟执行部分非重要任务;
- 解决这两个问题:
- 使用fastcgi_finish_request;
- 借鉴golang defer;
- 实现、效果和注意事项。
背景
团队技术背景
目前我们团队小组的技术情况如下:
- 以PHP作为开发语言开发Web类接口服务;
- 采用传统的Nginx+FPM模式运行服务;
- 我创建并维护了新的开发框架;
- 框架基于Slim v3.7,是小组之前依赖的,因考虑过渡成本暂时没改变。
场景问题
近期在一些项目中发现经常遇到业务接口有以下特点:
- 有主:一个接口中部分逻辑(比如下方栗子中的1/3/5,后续简称主逻辑)是需要保障处理成功并将结果反馈给调用方的;
- 有支:另一部分逻辑(比如下方栗子中的2/4,后续简称支逻辑)则可容忍(暂时)失败,甚至调用方并不关心结果或说感知不明显;
- 简单:大部分支逻辑比较简单,简单判断加上打文件日志、写条MySQL日志记录、发个HTTP请求等类;
- 混合:主支逻辑在代码编写顺序上往往是交叉混淆而非泾渭分明的。
关于代码编写顺序当然也可以特意把两部分分开,但这样并不符合常规开发同学实现的思路脉络,也不利于代码阅读理解和变量控制
举一个接口栗子(瞎编的):
- 【主】一个重要的扣除逻辑,成功才能继续;
- 【支】扣除失败则发送一条info级别的邮件消息,成功与否都行;
- 【主】一个重要的发货逻辑,成功才能继续;
- 【支】发货失败则发送一条error级别的邮件消息,发送失败则记录本地文件日志;
- 【主】组装结果并返回。
同步模型
先闭上眼睛一把梭顺序编写代码实现,同步执行流程如下(事实上一开始我真是这么一把梭实现的):
异步模型
- 遗憾的是有一天QA同学说你这个接口响应耗时太高了,压测时的表现更明显;
- 更遗憾的是线上居然还发生了2/4步骤DNS解析超时问题(后面发现是整个libcurl问题,不仅DNS解析,当然这是另一个话题了)
这种场景下传统常见的方案也许我们不会陌生——进程级别的异步任务方案!
- Laravel/Lumen也是采用这个方案的
- 我们组内另一位同学也为我们的开发框架支持了这种异步任务的方案,其实是可以直接采用的,只是它并不是本文的主角~
执行流程如下:
多线程、协程+异步IO调度模型
当然还有很多其他方案,不过也都比较复杂,且不利于保持CGI+同步阻塞这套模型给团队同学的门槛好处和对服务的稳定安全,所以也不是本文的主角~
特别是团队今年的主基调是质量、与效率,更加不想这个时候搞事情啦~
思路
上述异步模型其实是比较成熟的方案选择,只是它也存在着一些问题/弊端:
- 队列服务依赖:需要额外依赖一个队列服务,这也一定程度上依赖了其可用性、同时本身也是一条网络IO开销;
- 消费进程管理:需要独立的消费进程消费队列中的异步任务,独立消费进程也需要额外考虑其运行状态维护和控制;
- 处理链路增长:这个就不多说了,虽然这对于异步任务而言倒不算什么。
分析转换
那有没有(使用成本和运行效率上)轻量级又保留现有优势的方案呢?
思考分析:
- 既然仍然是采用同步阻塞方案,也就是说不去做串行改并行的优化,仍旧是原来的执行时间开销;
- 同时想要轻量级,那需要将队列服务、消费进程都干掉,只能仍旧由当前处理该请求的FPM处理进程来执行全部逻辑;
- 那能不能搞一个伪"异步"呢?让调用方在感知上提前结束,但该FPM处理进程仍旧会完成剩下逻辑。
问题转换:
- PHP在FPM运行模式下能不能让请求响应提前返回给调用方?
- 能不能顺序编写代码逻辑,但执行时将指定部分的代码逻辑块延迟到某个指定逻辑之后?(这是因为我需要考虑封装成方便大家使用的框架能力)
问题1解法:fastcgi_finish_request
很容易想到fastcgi_finish_request
,FPM正好又是FastCGI模式,可以使用
当然使用它是要注意一些点的,具体我放在本文最后了~
另外印象里Laravel/Lumen有个终结者中间件,好像也是实现了类似功能(先返回响应给调用方,再继续执行终结者中间件逻辑),于是打算去翻翻源码回忆验证一下其实现原理:
- 对这个有印象是因当年有同事使用Laravel时遇到通过框架提供的session修改和保存方法但是并没有生效的问题
- 协助排查时大概看到过框架对于session的操作都是在进程内存级别的,只在终结者中间件中才会将session完整覆盖到存储驱动中
- 问题出在同事一通逻辑处理后没有采用框架提供的response方法返回响应,直接echo然后exit,以至于没法走到框架后续返回响应和执行终结者中间件。。。
- 所以记忆犹新~~
通过Laravel源码验证也是通过fastcgi_finish_request来实现此功能的:
- 阅读理解为:kernel->handle后得到response,response->send中执行了header和body的设置输出后,执行了fastcgi_finish_request
- 当然它对其他运行模式也做了兼容,但是我暂时用不到
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Kernel::class);
$response =