寫程式,難免會遇到需要做成系統服務的需求。Windows 下寫系統服務需要實現一些特定的介面,做起來有一定難度,所以不少程式採用了 近似的備選方案 —— 做成帶系統工作列圖示的桌面應用。但是,服務之所以是服務,就在於他有一個非常重要的特點:可以開機自啟動,而且不需要使用者登入。要不然每次重啟還得人工去登入,是件多麼辛苦的事情。Windows 當然是可以設定自動登入的,但如果是託管伺服器,你真放心自動登入嗎?
而 Linux 下面似乎就要方便得多,大概不需要 GUI 持續執行的程式都可以做成服務。
1. 在 Windows 中做服務
先說 Windows。如果你還在用 Windows XP,那我們就此別過 ……
1.1. Windows Service Wrapper
[Windows Service Wrapper] 是全稱。其簡稱 WinSW 的知名度可能更高一些。
WinSW 基於 .NET Framework 4.6.1 和 .NET 5 實現,所以至少需要 Windows 7 SP1 / Windows Server 2008 R2 SP1 才可以使用。它可以把任意 Windows 程式封裝成 Windows 服務,你所需要做的,只是寫個配置檔案,然後用 WinSW 註冊一個 Windows 服務即可。WinSW 下載下來是個獨立的可執行檔案,使用前需要寫一個與可執行檔名同名但副檔名是 .xml
的配置檔案置於同一目錄下。
舉例來說,Nginx 本身並沒有提供註冊成 Windows 服務的能力,如果需要註冊成 Windows 服務,就可以用 WinSW 來封裝一下。把下載的 WinSW 可執行檔案改名為 winsw.exe
(隨便改成什麼名字都行,配置檔名按相同的名稱建立即可),放在 nginx 的主目錄下面,建立配置檔案之後的目錄結構大概是這樣:
[-] nginx
|-- conf
|-- ...(其他 nginx 的目錄或檔案)
|-- nginx.exe
|-- winsw.exe
`-- winsw.xml
winsw.xml
中的配置內容如下,看註釋就能理解。
<service>
<!-- 配置服務名稱 nginx-service,顯示名稱 Nginx Service,以及服務描述 -->
<id>nginx-service</id>
<name>Nginx Service</name>
<description>Nginx Service</description>
<!-- 服務執行的工作目錄,給絕對路徑 -->
<workingdirectory>C:\Local\Nginx</workingdirectory>
<!-- 服務可執行檔案,給絕對路徑 -->
<executable>C:\Local\Nginx\nginx.exe</executable>
<!-- 停止服務的可執行檔案 -->
<stopexecutable>C:\Local\Nginx\nginx.exe</stopexecutable>
<!-- 停止服務的引數 -->
<stoparguments>-s stop</stoparguments>
<priority>Normal</priority>
<stoptimeout>15 sec</stoptimeout>
<stopparentprocessfirst>false</stopparentprocessfirst>
<!-- 配置服務型別是「自動」啟動 -->
<startmode>Automatic</startmode>
<waithint>15 sec</waithint>
<sleeptime>1 sec</sleeptime>
<!-- 將服務的控制檯輸出(標準輸出/錯誤輸出)寫入日誌 -->
<!-- 其中 %BASE% 是指 winsw.exe 所在目錄 -->
<!-- 參考:https://github.com/winsw/winsw/blob/master/doc/loggingAndErrorReporting.md -->
<logpath>%BASE%\logs</logpath>
<log mode="roll-by-time">
<pattern>yyyyMMdd</pattern>
</log>
</service>
這個配置建立了名為 nginx-service
的 Windows 服務,它在 Windows 的「服務 (services.msc
)」顯示名稱為 Nginx Service
。啟動服務的時候直接執行 nginx.exe
來啟動,這是一個會執行佔用控制檯的程式;而停止服務則是執行 nginx.exe -s stop
,可執行程式和引數分別配置在 <stopexecutable>
和 <stoparguments>
中 —— 由此不難推斷,如果啟動服務需要引數,是配置在 <arguments>
中的。
詳細的配置可以在 github 庫裡的 XML configuratoin file 中查到,也可以查到一些示例。
配置完成之後執行 winsw.exe install
即可安裝為 Windows 服務。安裝完成之後可以使用 winsw.exe start
命令啟動服務,也可以去 Windows 的服務管理器啟動,或者使用 net start
命令來啟動。github 庫首頁的 Usage 部分有完整的命令說明。
1.2. 用 .NET Framework/Core/5 自己寫一個
用 .NET 寫個服務還是比較容易的,因為有現成的包(元件)可以用:NuGet Gallery | Microsoft.Extensions.Hosting.WindowsServices,官方出品。它至少需要依賴兩個包:
- NuGet Gallery | Microsoft.Extensions.Hosting
- NuGet Gallery | Microsoft.Extensions.Hosting.Abstractions
在引入元件之後,只需要少量程式碼就可以讓當前 .NET 的 Console Application 成為一個支援 Windows 服務介面的服務程式。
// Program.cs
class Program {
static async Task<int> Main(string[] args) {
await CreateHostBuilder(args).Build().RunAsync();
}
public static IHostBuilder CreateHostBuilder(string[] args) {
return Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) => {
services.AddHostedService<DaemonService>();
})
.UseWindowsService();
}
}
注意到 AddHostedService<DaemonServce>
,這裡的 DaemonServce
是一個自己實現的服務業務類,命名自由,但需要從 Microsoft.Extensions.Hosting.BackgroundService
繼承
class DaemonService : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
// TODO 提供服務內容的程式碼
}
}
服務的業務程式碼通常都是持續執行,或者監聽的程式碼。如果是計劃性/週期性的任務,可以考慮使用 Quartz 來實現。
程式完成後可以使用 Windows 提供的 sc
命令來註冊/登出服務。假設生成的程式是 MyService.exe
,那麼註冊、配置和啟動服務的命令如下:
sc create "my-service" binPath="C:\MyService\MyService.exe --service"
sc config "my-service" start= auto
sc start "my-service"
注意:binPath
中應該給絕對路徑。
2. 在 Ubuntu 中做 Systemd 服務
Linux 下服務種類比較多,最近主要是用 Ubuntu,所以做 Ubuntu 下的 Systemd 服務。
假設我們寫了一個 .NET 5 的 ASP.NET 應用,放在 /app/my-web/
,主檔案是 MyWeb.dll
。如果用命令列啟動這個 Web 應該應該是
cd /app/my-web
dotnet MyWeb.dll
注意:需要提前準備好 .NET 5 的執行環境,可參考在 Ubuntu 上安裝 .NET - .NET | Microsoft Docs。
接下來是寫 Systemd 服務配置。配置檔名起為 my-web.service
,放在 /etc/systemd/system
目錄下。內容(含註釋)如下:
[Unit]
# 服務說明
Description=My Web Application
# 在啟動網路服務之後啟動
After=network.target
[Service]
# 總是重啟(無論什麼原因結束都會立即重啟)
Restart=always
RestartSec=10
# 工作目錄
WorkingDirectory=/app/my-web
# 啟動服務的命令
ExecStart=/usr/bin/dotnet MyWeb.dll
# 通過殺主程式來結束服務
ExecStop=/bin/kill -HUP $MAINPID
TimeoutStopSec=5
KillMode=mixed
SyslogIdentifier=my-web
# 指定執行此服務的使用者,涉及到目錄訪問許可權等問題
User=james
[Install]
WantedBy=multi-user.target
配置完之後還不能馬上啟動服務,需要 systemd 重新載入配置,然後才啟動服務:
sudo systemctl daemon-reload
sudo systemctl start my-web
順便,再介紹一下,如果想在內容釋出之後自動重啟,需要加兩個配置檔案,一個 .path
監控變化,一個 .service
來重啟 my-web
:
restart-my-web.path
[Path]
# 監控主檔案 MyWeb.dll 的變動,如果有變動會觸發 restart-my-web.service 啟動
PathModified=/app/my-web/MyWeb.dll
[Install]
WantedBy=multi-user.target
restart-my-web.service
[Unit]
Description=My Web Restarter
After=network.target
[Service]
Type=oneshot
# 防抖,60 秒內只啟動 1 次
ExecStartPre=/bin/sleep 60
# 重啟 my-web.service
ExecStart=/bin/systemctl restart my-web.service
[Install]
WantedBy=multi-user.target
3. 小小的總結一下
做服務並不難,上面唯一的一個需要寫程式碼的方式,還是開箱即用的元件實現的。但話說回來,做服務不難,做服務的設計還是有不少事情需要考慮。比如
- 如何監控服務的狀態?—— 程式監控、心跳檢查……
- 如何分析服務中出現的錯誤?—— 系統日誌
- 如何提供 GUI 來對服務進行管理?—— Web 或其他 UI 跟服務程式進行互動(程式通訊、管理 API 等)
- ……
既然做服務不難,那就不要太糾結如何“做”(提供)服務,還是多糾結糾結如何做好(設計)服務吧。