在Web应用的开发中,不论是网站还是服务接口,我们可能会遇到来自客户端的某个请求,而这请求的背后,隐藏着要执行的大量的繁重任务,如果我们在后台程序中,同步的进行处理,那么程序执行时间比较久,用户体验是糟糕的,甚至会导致502执行超时。针对这种情况,有很多成熟的解决方案【据我粗浅的认知,使用队列是一个较好的方案】,但实现起来稍显复杂和繁琐。如果我们对要异步执行的任务没有特别的要求【比如失败重试或异步任务执行完毕后的事件回调】,那么,可以用一种非常轻松的方式来简单实现:nohup 要执行的命令 > /dev/null 2>&1 &

如果,你看到这里,觉得这没什么新奇的,那说明你水平挺好,至少比我要好【发自真心】。也希望你在离开之前,说一说有没有啥更好的方法,分享一下。

应用场景

目前,我在两种具体的场景下,实际使用了这种异步执行任务的方法。

1、早前,做一个网站,数据是另一个同事从第三方采集的,采集下来的数据,需要导入到我的数据库中,于是,我在网站后台提供了一个功能,一个文件上传导入的按钮,当同事将他采集的数据,通过文件上传的方式,保存在服务器上的时候,后台程序会读取这个文件的内容,并基于里面的数据,进行一下必要的分析,最后将分析后的数据,通过SQL写入到数据库中。此过程执行的快与慢,取决于数据文件的大小,一个几千行数据的文件,最后可能要执行一分多钟。如果采用传统的同步执行,那么从文件上传 -> 数据分析 -> 写入数据库 这整个过程中,浏览器都在转圈圈,若是时间再长点,执行就超时了,前功尽弃。所以,这里我采用了异步执行任务的方式,在数据文件成功上传后,服务端直接响应回浏览器,显示一个“数据导入成功,正在进行处理”的提示,整个前端的交互就到此为止,后面数据分析和写入数据库,就交给另一个单独的进程去处理了。

2、就在前两天,我刚用这种方式,写了这样的功能。我们做了一个APP【用APICloud做的一个不入流的APP】,当用户在使用APP发布了一个内容后,我们需要调用百度AI的内容审核接口,对用户发布的文字和图片,进行自动审核,如果发现其中包含不良信息,则自动审核不过。而这个调用百度接口的过程,是略微有点耗时的,这主要取决于用户发布的内容中所包含图片的多少,图多自然百度接口处理响应的慢。同样的,如果用同步的方式,用户发布内容 -> 调用百度接口 -> 等待接口返回数据 -> 判断是否审核通过,太耗时了,这样在用户看来,就是内容发出去之后,等待了好几秒,甚至十几秒,最后才有反应,这用户体验就太差了。所以,可以做到当用户发布了内容后,立刻提示“发布成功,正在审核”的字样,而在几秒钟之后,用户就看到他刚刚发布的内容审核通过,并出现在内容列表中的时候,是多么自然的一个过程。

实现思路

所以,有的时候,异步的处理一下任务,还是很有必要的。既然咱们开头说到,在Web应用的开发中,那就跳不出Linux服务器,现如今除了.NET系的可能还会部署在Windows上【博客园貌似就不是】,其他的后端应用,基本都会部署在Linux上,而我们开头提到的实现方式,就是在Linux下的命令实现。

首先,要实现程序的异步执行,大概有两种方式:线程 和 进程【说大概,是因为听说有的语言还支持协程。嗯?什么鬼? -_-!!!】。像Java这种,支持线程的编程语言,异步执行可以用线程实现,也可以用进程实现【Runtime.exec()】;而像PHP这种,在默认情况下,是没有线程的,并且大家普遍也都不在PHP下使用线程,那么,这就只能通过其内部函数,调用外部进程,去实现异步任务的执行。

在PHP下,执行一个外部程序,并且要求这个外部程序是在后台运行,且不会让你的宿主程序等待挂起【宿主也就是执行调用外部命令的PHP程序】,有一点要特别注意,这是在官方手册的exec函数中特意提到的:

意思就是,为了让外部程序在后台运行,这个外部程序的输出【指标准输出【像 Python中的 print,PHP中的 echo 和 Java中的 System.out.print】和标准错误】必须重定向到一个文件或另一个输出流中。否则,宿主程序可能会挂起,等待外部程序执行完毕后,才会终止结束他的生命周期。

所以,文章开头提到的命令中的 > /dev/null 2>&1,就是用来重定向标准输出和标准错误,将其写入 /dev/null 文件的,以使得宿主程序在调用外部程序,让其后台运行后,自己会立刻执行后续代码,直到结束,可以很快的结束自己的生命周期,而此时,外部程序,还正在默默的努力运行中。

当我在写这篇文章之前,还特意查了一下,在Java下用Runtime.exec()调用外部程序的实现方式,发现有篇文章提到了这样一点:

后台程序在处理繁重的任务时,调用外部程序异步执行的简单实现-LMLPHP

意思也是要将外部程序的输出重定向出来,这与PHP官方手册中提到的注意事项,完全一致。

具体实现

下面,我们就来解释一下 nohup 要执行的命令 > /dev/null 2>&1 & 这条命令的含义。

首先,是 要执行的命令,比如我上文提到的,调用百度AI,进行内容审核,那么命令就像 php /www/wwwroot/app_service/artisan baidu:censor 文章ID 这样,我这里用的PHP的Laravel框架,至于你用什么语言,什么框架,怎么写这个 要执行的命令,也要视你的情况而定。

其次,要让一个程序,在后台运行,需要在命令后面加上 &【也就是末尾的 &】,以告诉系统,我要执行的命令,是一个需要后台运行的程序。

然后,为了防止我们的宿主程序等待挂起,我们需要重定向外部程序的输出,于是就加上了 > /dev/null 2>&1> /dev/null 是指将标准输出重定向到 /dev/null 文件,而后面的 2>&1 是指,将标准错误也重定向到跟前面标准输出一样的位置。而 /dev/null 是一个不存在的文件,所以 > /dev/null 2>&1 的整体意思是,这个外部程序执行时,他产生的所有标准输出和标准错误【就是报错信息】,统统不要保存,我不要看到。当然,如果你在调用外部程序后,发现没有按预期执行,可能是这个外部程序报错了,你可以将输出,重定向到一个真实的文件,以保存外部程序的输出信息,便于你排错。

最后,是 nohup。当你通过指定 & 让外部程序在后台运行后,如果此时你关闭、退出你的 terminal 终端【就是黑乎乎的命令行窗口】,那么此时你刚刚正在后台运行的外部程序,也会终止。为了避免这个问题,需要在开头加上 nohup,来告诉系统,关闭、退出终端时,别把我刚刚执行的外部程序的这个后台进程杀掉啊!!!

好了,具体实现要用到的命令,解释清楚了,那在各个语言中,如何实现呢?这个,我相信各个语言中,都有调用外部程序的方式,你可以自己研究下。我用PHP多一点,最后就贴一下PHP的实现方法:

exec('nohup php /www/wwwroot/app_service/artisan baidu:censor 文章ID > /dev/null 2>&1 &');【Laravel】

exec('nohup php /www/wwwroot/app_service/baidu_censor.php 文章ID > /dev/null 2>&1 &');

参考文章

10-21 19:33