後臺程式在處理繁重的任務時,呼叫外部程式非同步執行的簡單實現

bananaplan發表於2020-10-21

在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函式中特意提到的:

If a program is started with this function, in order for it to continue running in the background, the output of the program must be redirected to a file or another output stream. Failing to do so will cause PHP to hang until the execution of the program ends.

意思就是,為了讓外部程式在後臺執行,這個外部程式的輸出【指標準輸出【像 Python中的 print,PHP中的 echo 和 Java中的 System.out.print】和標準錯誤】必須重定向到一個檔案或另一個輸出流中。否則,宿主程式可能會掛起,等待外部程式執行完畢後,才會終止結束他的生命週期。

所以,文章開頭提到的命令中的 > /dev/null 2>&1,就是用來重定向標準輸出和標準錯誤,將其寫入 /dev/null 檔案的,以使得宿主程式在呼叫外部程式,讓其後臺執行後,自己會立刻執行後續程式碼,直到結束,可以很快的結束自己的生命週期,而此時,外部程式,還正在默默的努力執行中。

當我在寫這篇文章之前,還特意查了一下,在Java下用Runtime.exec()呼叫外部程式的實現方式,發現有篇文章提到了這樣一點:

意思也是要將外部程式的輸出重定向出來,這與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 &');

參考文章

相關文章