網路磁碟機與 UNC 路徑的陷阱 ── 業務應用程式處理檔案伺服器(共用資料夾)的實務
· Go Komura · 網路磁碟機, UNC 路徑, SMB, 檔案共用, Windows 服務, 檔案伺服器, C#, .NET, 網路, 缺陷調查, Windows 開發, 技術諮詢
「在開發機上明明可以運作,結果客戶環境卻回報『找不到 Z:\ 』」「一放上工作排程器,對共用資料夾的輸出就開始失敗」「改成 Windows 服務之後,就看不到檔案伺服器了」── 在業務應用程式的諮詢案例中,與檔案伺服器(共用資料夾)有關的麻煩堪稱經典中的經典。訂單資料的匯入、報表或 CSV 的輸出、監控設備吐出的檔案 ── 在地端的業務系統中,共用資料夾至今仍是現役的整合基礎設施。
麻煩的地方在於,這類問題大多不是「程式碼的錯誤」,而是根植於 Windows 本身的機制 ── 磁碟機代號與登入工作階段的關係、服務的執行帳戶與驗證、SMB 的連線管理。就算用偵錯器追蹤也找不到原因,時間就在「我的電腦重現不出來」中一點一滴流逝。
本文以實作共用資料夾檔案輸出・匯入・監控的業務應用程式開發者(WinForms/WPF/Windows 服務)為對象,從機制面整理網路磁碟機與 UNC 路徑的陷阱,並將實務上的定石整理成判斷表。
假設的環境
由於驗證相關的討論會因前提不同而答案不同,因此先在此明確列出假設的環境。
| 項目 | 假設 |
|---|---|
| OS | Windows 10 / 11、Windows Server(目前受支援的版本) |
| 共用通訊協定 | SMB(Windows 標準的檔案共用) |
| 驗證基礎架構 | 主要假設為 Active Directory 網域環境。若使用沒有網域的工作群組環境,或 NAS 專屬帳戶時,會逐一以「在工作群組環境中」的方式加以區分說明 |
| 應用程式的執行形態 | 桌面應用程式(WinForms / WPF)、Windows 服務、由工作排程器啟動的主控台應用程式 |
| 程式碼範例 | C# / .NET 6 以後(使用了 File.WriteAllBytesAsync,以及 is ... or ... 的模式比對) |
是網域環境還是工作群組環境,是本文中影響最大的分歧點。第 3 章的執行帳戶選項(電腦帳戶或 gMSA)必須要有網域才成立;沒有網域的環境,則會偏向第 4 章「明確傳遞認證資訊」的方向。請先確認自家公司屬於哪一種,再繼續閱讀。
本文中使用的縮寫
| 縮寫 | 讀法・正式名稱 | 一句話說明 |
|---|---|---|
| UNC 路徑 | Universal Naming Convention(通用命名慣例) | \\伺服器名稱\共用名稱\... 的格式。共用資料夾本來的位址 |
| SMB | Server Message Block | Windows 檔案共用所使用的通訊協定 |
| 登入工作階段(logon session) | ─ | 使用者登入時,或服務啟動時,OS 建立的「執行的脈絡」。磁碟機代號就是以此為單位管理(第 2 章) |
| Kerberos | ─ | Active Directory 網域的標準驗證協定。透過交換票證(ticket)確認身分 |
| SPN | Service Principal Name(服務主體名稱) | 在 Kerberos 中,用來唯一指定連線目標服務的名稱。用戶端會從連線目標的主機名稱組出 SPN 並要求票證(第 4 章) |
| gMSA | group Managed Service Account(群組受管理服務帳戶) | 密碼由 OS 自動產生・自動更新的網域服務帳戶(第 3 章)1 |
1. 先講結論
- 磁碟機代號(Z: 等)並非整個系統共用,而是以登入工作階段為單位。每個登入工作階段都會分配到一整套 A 到 Z 的磁碟機代號,因此不同使用者的行程、在不同登入工作階段中執行的服務,都看不到使用者所對應的磁碟機。2
- 啟用 UAC 的系統管理員,會建立一般權限與提升權限用的兩個登入工作階段,磁碟機對應(DosDevices 的符號連結)在各工作階段中是獨立的。因此才會發生「只有從提升權限的應用程式看不到 Z:」的現象。雖然存在透過
EnableLinkedConnections登錄機碼讓兩個工作階段共用的迴避方法,但 Microsoft 明確指出這是「可能使系統變得不安全,不受支援」的設定。34 - 從服務(或不同安全性內容的行程)存取遠端資源時,官方方針是使用 UNC 路徑(
\\server\share\...)。在服務內用net use或 WNet 系列 API 對應磁碟機代號的做法,基於認證外洩與服務之間互相干擾的考量,並不建議採用。2 - 在共用端要授予權限的對象,由服務的執行帳戶決定。LocalSystem 與 NetworkService 在網路上會以「電腦的認證資訊」身分進行驗證,LocalService 則會變成匿名認證,不適合用於共用存取。使用本機使用者帳戶的服務,無法存取網路資源。實務上的首選是網域帳戶,或是可以把密碼管理交給 OS 負責的 gMSA(group Managed Service Account,群組受管理服務帳戶)。不過 gMSA 的前提是要有網域環境,並準備好 KDS 根金鑰(第 3 章)。567819
- 無法用多組認證資訊同時連線到同一台伺服器。第二個連線會因錯誤 1219(
ERROR_SESSION_CREDENTIAL_CONFLICT)而失敗,這是規格(by design)。1011 - 共用資料夾「速度慢・會斷線・有時候根本不在」才是正常狀態。閒置的連線在預設情況下,伺服器端會在 15 分鐘後將其中斷(下次存取時會重新連線)。
File.Exists在權限不足或發生錯誤時也不會擲出例外,而是回傳false,因此無法區分「檔案不存在」與「連不到伺服器」。1213 - FileSystemWatcher 支援監控網路磁碟機與遠端電腦,但設計時要假設它會漏掉事件。緩衝區溢位時會遺失事件,而透過網路監控時,內部緩衝區上限會被限制在 64KB。併用輪詢(完整掃描)是實務上的定石。14
2. 磁碟機代號與 UNC 路徑的關係 ── Z: 是「你的登入工作階段」的東西
在檔案總管中把 \\fileserver\share 對應成 Z:,看起來就像整台機器都長出了一個「Z: 磁碟機」。這是最初的誤解。
官方文件的說明相當明確。磁碟機代號並非系統全域共用,而是以登入工作階段為單位,分配到一整套 A 到 Z 的代號。重新導向的磁碟機(網路磁碟機)無法在以不同使用者帳戶執行的行程之間共用,在其他登入工作階段中執行的服務,也無法存取在另一個工作階段中建立的磁碟機代號。2 系統是根據唯一識別登入工作階段的登入 SID 來管理磁碟機對應的。2
也就是說,Z: 只不過是「每個登入工作階段各有一套路徑的縮寫符號」,實體則永遠是 UNC 路徑。畫成圖如下所示。
flowchart TB
accTitle: 磁碟機代號與 UNC 路徑的關係
accDescr: 兩個登入工作階段各自擁有獨立的磁碟機代號集合,只有其中一個對應了 Z:,而共用資料夾的實體 UNC 路徑只有一個,兩個工作階段能否用 Z: 到達的結果並不相同
subgraph SA["登入工作階段 A ── 互動式登入的使用者"]
EXP["檔案總管<br/>從桌面啟動的應用程式"]
ZA["這個工作階段的磁碟機代號集合<br/>Z: 已對應到共用資料夾"]
EXP --- ZA
end
subgraph SB["登入工作階段 B ── 服務 / 工作排程器"]
SVC["Windows 服務<br/>排程執行的批次作業"]
ZB["這個工作階段的磁碟機代號集合<br/>Z: 尚未指派"]
SVC --- ZB
end
UNC["共用資料夾的實體<br/>UNC 路徑 ── fileserver01 的 share"]
ZA -->|"可透過 Z: 到達"| UNC
ZB -.->|"Z: 不存在,只有 UNC 路徑才能到達"| UNC
圖 1:磁碟機代號每個登入工作階段各有一套,而實體的 UNC 路徑只有一個
要注意的是,這種區隔並非「因為使用者不同」。即使把服務設定為以與工作階段 A 相同的使用者帳戶執行,系統仍然會為服務建立新的登入工作階段,因此工作階段 A 中建立的對應不會被繼承。2 「明明把服務的執行使用者設成跟自己一樣,卻還是找不到 Z:」這類諮詢,原因就出在把這一點搞混了。
從這裡開始,可以連鎖式地說明現場經常發生的症狀。
| 症狀 | 機制上的原因 |
|---|---|
| 用別的使用者執行就沒有 Z: | 磁碟機代號以登入工作階段為單位。看不到別人的對應2 |
| 用「以系統管理員身分執行」就沒有 Z: | UAC 會建立一般用與提升權限用兩個登入工作階段,對應不會共用3 |
| 放到工作排程器/服務上就沒有 Z: | 因為是在不同的登入工作階段中執行。即使把服務設定為使用者帳戶,系統仍會為服務建立新的登入工作階段2 |
UAC 的部分值得再深入了解一下。當屬於系統管理員群組的使用者登入時,系統會建立兩個相互連結的登入工作階段:一個是權限受限的權杖,另一個是完整的系統管理員權杖。磁碟機對應的實體,是把磁碟機代號與 UNC 路徑對應起來的符號連結物件(DosDevices),這是登入工作階段專屬的,不會在工作階段之間共用。3 登入指令碼是在一般權限那一側執行,所以從提升權限的行程看不到那個對應,道理就在這裡。
把 EnableLinkedConnections(HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System 下的 DWORD 值)設為 1,就會在兩個相互連結的工作階段中都寫入符號連結,這個症狀就會消失。不過官方文件明確指出:「這個迴避方法可能使系統變得不安全。Microsoft 不支援這個迴避方法。使用時請自行承擔風險。」4 在客戶環境的登錄檔中埋入不受支援的設定,作為業務應用程式的解決方案並不是好路子。在應用程式端以 UNC 路徑撰寫才是正道。資料夾重新導向的官方疑難排解文件中也提到「建議一律使用 UNC 路徑,而非磁碟機代號」。4
實作上很單純,只要設定檔中儲存的路徑用 UNC 表示即可。
// appsettings.json ── 用 UNC 而非磁碟機代號來表示
{
"FileTransfer": {
"IncomingDir": "\\\\fileserver01\\edi\\incoming",
"ProcessedDir": "\\\\fileserver01\\edi\\processed"
}
}
如果應用程式允許使用者在資料夾選擇對話方塊中選到 Z: 底下的路徑,那麼儲存時先正規化成 UNC,之後即使執行內容改變也不會出問題。從磁碟機代號轉換成 UNC,有一個現成的 API 叫做 WNetGetUniversalName。15
3. 從 Windows 服務存取共用資料夾 ── 必須用 UNC,以及「以誰的身分」存取
如前一章所述,服務無法使用對應的磁碟機。官方文件表示,服務(以及在不同安全性內容中執行的行程)應該以 UNC 名稱存取遠端資源,並明確不建議在服務內用 net use 或 WNet 系列 API,在執行階段對應磁碟機代號。文件所列出的理由包括:在相同內容中執行的其他服務也會看到這個對應、傳給 net use 的認證資訊可能外洩到服務邊界之外,以及多個服務嘗試建立相同的對應時,會因「已經連線」的錯誤而互相干擾。2
改用 UNC 路徑之後,接下來的問題就是驗證。服務究竟是「以誰的身分」存取檔案伺服器?這由執行帳戶決定,共用端該授予權限的對象也由此而定。
| 執行帳戶 | 網路上的身分 | 共用端的權限授予 | 判斷 |
|---|---|---|---|
| LocalSystem | 電腦的認證資訊5 | 若為網域環境,則對電腦帳戶(DOMAIN\MACHINE$)授予共用・NTFS 權限 | 可以運作,但權限過大。更換機器時要重新設定權限 |
| NetworkService | 電腦的認證資訊6 | 同上 | 本機權限最小,在網路上與 LocalSystem 是相同身分 |
| LocalService | 匿名認證資訊7 | 無從授予 | 不適合用於共用存取 |
| 本機使用者 | ─ | ─ | 無法存取網路資源8 |
| 網域使用者 | 該帳戶本身 | 授予該帳戶 | 實務上的標準做法。密碼變更的運維是課題 |
| gMSA | 該帳戶本身 | 授予該帳戶 | 密碼由 OS 自動管理(每 30 天自動變更)。若環境支援,是首選116 |
LocalSystem 與 NetworkService 會「以電腦的身分」連上網路,這是官方明文記載的規格。56 BITS 的文件中,把這個結果所衍生的實務注意事項直接寫了出來:「若來源檔案的 ACL 把存取權限限制給特定使用者帳戶,那麼(以電腦認證資訊驗證的)服務就會被拒絕存取」「系統帳戶不應該使用對應的磁碟機」。17
由此可以導出以下 3 項實務指引。
- 「改成服務後就被拒絕存取」的第一步排查,是確認執行帳戶。在桌面環境執行時,是以「你自己」的權限做存取檢查;而在服務中,則是以上表所列的身分做存取檢查。請針對該身分,同時確認共用的存取權限與 NTFS 的 ACL。
- 若要在網域環境中長期運維,執行帳戶就用網域帳戶或 gMSA。gMSA 由 OS 每 30 天自動更新一次 240 位元組的隨機密碼,因此可以從結構上消除「服務帳戶密碼過期,週一早上全部停擺」這類事故。16
- 在工作群組環境(沒有網域)中,電腦認證資訊的驗證方式無法成立,因此就輪到下一章介紹的明確傳遞認證資訊登場了。
gMSA 有其前提條件。雖然上面寫成「首選」,但它並不是想到就能馬上使用的東西。gMSA 原本是把單一伺服器用的 sMSA(standalone Managed Service Account)功能,擴充為可以跨多台伺服器使用,是一種提供自動密碼管理與簡化 SPN 管理的網域帳戶。1 導入前請先確認以下事項。
| 前提 | 內容 |
|---|---|
| Active Directory 網域 | gMSA 是網域的帳戶。在工作群組環境中不是可選項目。此外也不適用於 Windows Server 2012 之前的 Windows1 |
| KDS 根金鑰 | 網域控制站要產生 gMSA 的密碼,需要 Key Distribution Service(KDS)的根金鑰。整個樹系只需執行一次 Add-KdsRootKey -EffectiveImmediately 來建立9 |
| 建立後的等待時間 | 從根金鑰建立起算,最長需要 10 小時,要等到所有網域控制站的 AD 複寫都收斂完成,才能建立 gMSA。這是為了避免在所有 DC 都能回應 gMSA 要求之前就產生密碼的安全機制,「剛建立完馬上不能用」是規格如此9 |
| 管理端點 | 執行用來管理 gMSA 的 Windows PowerShell 命令,需要 64 位元架構19 |
| 加密類型 | gMSA 依賴 Kerberos 支援的加密類型。若主機端停用了 RC4,且沒有 msDS-SupportedEncryptionTypes 屬性,驗證必定失敗,因此建議 MSA 一律設定 AES1 |
這些都是需要與網域管理員協調的項目,因此若要提出以 gMSA 為前提的設計,順序上應先向基礎架構負責人確認上述 5 點。若無法確認,或是 10 小時的等待時間無法納入工程排程,那麼務實的做法是先用一般的網域帳戶搭建,並讓日後切換到 gMSA 只需要變更執行帳戶即可完成。
服務本身的建立方式(執行帳戶設定、復原選項、安全停止)已整理在「Windows 服務的建立與維運 ── 從工作排程器的取捨到 BackgroundService 服務化」中,而工作階段與登入的機制則整理在「如何理解 Windows 的工作階段隔離 ── Session 0・RDP・多使用者同時執行」中。
4. 認證資訊的處理 ── net use・認證管理員・錯誤 1219
若無法直接對執行帳戶本身授予共用端的權限(例如工作群組環境,或 NAS 採用專屬帳戶制的情況),就需要明確傳遞認證資訊來連線。主要有 3 種手段。
net use \\server\share /user:...── 在該登入工作階段中建立連線。用於互動式確認很方便,但如前一章所述,不建議在服務內執行。2- 認證管理員(
cmdkey) ── 用cmdkey /add:server /user:svc-file /pass:...先儲存起來,之後對該伺服器進行驗證時,就會自動使用已儲存的認證資訊。1819 儲存是以使用者設定檔為單位,因此若要在服務中使用,就必須在「服務執行帳戶的內容(context)下」進行登錄,這是一個容易踩到的陷阱。 - 從程式建立連線(
WNetAddConnection2) ── 可以指定用戶端的認證資訊,建立與網路資源之間的連線。官方文件也把它列為伺服器行程存取網路資源的策略之一。20 若設定為不指派磁碟機代號的「無裝置連線(deviceless connection)」,就能維持以 UNC 路徑存取。
而在這個領域中最有名的陷阱,就是錯誤 1219。
Multiple connections to a server or shared resource by the same user, using more than one user name, are not allowed. (ERROR_SESSION_CREDENTIAL_CONFLICT, 1219)11
無法從同一個登入工作階段,用不同的使用者名稱對同一台伺服器建立多個連線。如果想用「業務共用用自己的帳戶、系統整合用的共用用專用帳戶」這種方式,以兩種認證資訊連到同一台檔案伺服器,第二個連線就會因 1219 而失敗。官方文件明確指出這是「by design(規格如此)」,列出的迴避方法是「用 IP 位址連線」「建立另一個 DNS 別名再連線」── 也就是讓它看起來像是另一台伺服器的方法。10
在實務上,第一選擇是從一開始就避免對同一台伺服器分別使用多組認證資訊的架構。把整合用的帳戶統整成一個,對所有需要的共用都授予這個帳戶的權限。只有在有無法這麼做的特殊原因時,才用別名(alias)來分開路徑。
不過,別名並不是「在 DNS 加一行 CNAME 就結束了」這麼簡單。在 Kerberos 驗證的環境中,SMB 用戶端會嘗試用對應連線目標名稱的 SPN(服務主體名稱)進行驗證,因此若沒有替別名註冊 SPN,經由 CNAME 的存取本身就可能失敗。官方的疑難排解文件也把 SPN 缺失列為原因之一,並建議不要用 DNS 的 CNAME,而是用 netdom computername <伺服器名稱> /add:<別名> 把它設定為電腦名稱的別名。21 若要把經由別名的連線,當作 1219 的對策納入設計,務必先在目標環境中實際驗證是否可行。
另外,「服務想要以連線進來的用戶端使用者的權限存取檔案伺服器」這種進階需求,不屬於重複使用認證資訊的範疇,而是偽裝(impersonation)的領域。官方文件也建議與其讓服務抱著認證資訊不放,不如使用偽裝。2 偽裝的正確寫法,以及在遠端資源上需要額外考量的地方,請參考「正確處理 Windows 偽裝權杖 - 執行緒層級的權限借用與安全還原方式」。
診斷自己的環境 ── 該先執行的指令
到目前為止的內容,唯有能在出現症狀的環境中確認狀態,才會真正變成實務。排查的起點是:該行程究竟是以誰的身分在執行,擁有哪些連線。請在與症狀發生時相同的執行內容(context)中,執行以下 3 個指令。
| 指令 | 可以了解什麼 | 觀察重點 |
|---|---|---|
whoami |
現在自己是以誰的身分在執行(以 網域名稱\使用者名稱 的格式顯示) |
是否與預期的執行帳戶一致。若是服務,對應到第 3 章表格中的哪一列 |
net use(不帶參數) |
該登入工作階段擁有的網路連線清單22 | 應該看不到的 Z: 是否出現在清單中。若沒有,就能確定「該工作階段中不存在這個連線」 |
net use \\fileserver01\share |
指定連線的狀態22 | 是連線本身就無法建立,還是連線建立成功但被權限擋下 |
關鍵在於要在相同的內容(context)下執行。就算在自己的桌面上執行 net use,講的也不會是服務所擁有的連線。若想以服務的執行帳戶確認,最確實的做法是在工作排程器中,用相同的帳戶、相同的特殊權限設定,暫時登錄一個像 cmd /c "whoami > C:\temp\who.txt & net use >> C:\temp\who.txt" 這樣的工作,再檢查其輸出檔案。
在手邊重現錯誤 1219
1219 這個錯誤,與其看說明,不如自己動手重現一次,更容易理解。請在已加入網域的端點上,嘗試用不同的認證資訊連到同一台檔案伺服器上的兩個共用。
:: 1) 用自己的帳戶連線到第一個共用(密碼用 * 以提示輸入)
net use \\fileserver01\share1 * /user:CONTOSO\tanaka
:: 2) 嘗試用另一個帳戶,連線到同一台伺服器的另一個共用
net use \\fileserver01\share2 * /user:CONTOSO\svc-file
:: → 這裡就會回傳上面引用的錯誤 1219,無法連線
:: 收尾:切斷這個工作階段的所有網路連線
net use * /delete
重點在於,即使共用不同,只要伺服器相同就會衝突。第一個是 share1、第二個是 share2 都沒有關係,因為限制是以伺服器為單位。反過來說,如果先用 net use \\fileserver01\share1 /delete 切斷第一個連線,再建立第二個連線,就會成功 ── 也就是說,依照順序的不同,有時候會通過,所以會出現開發階段重現不出來、到了正式環境才第一次發生的討厭狀況。若要用 IP 位址或別名作為迴避方法,請別忘記前面提到的 SPN 前提條件。1021
5. 以「速度慢・會斷線・有時候不在」為前提來設計
用對待本機磁碟的感覺寫出來的程式碼,面對共用資料夾時一定會在某處出包。應該當作前提的現實有 3 項。
第一,連線斷開才是正常的。閒置狀態的連線,為了避免浪費伺服器資源,預設會在 15 分鐘逾時後斷開。這就是檔案總管中網路磁碟機圖示上出現紅色叉叉這個熟悉現象的成因,只要下次存取,就會迅速重新連線。12 也就是說,「不常存取的業務應用程式,每次存取時都要稍等一下」「監控工具大呼『斷線了!』但實際上沒有損害」這兩種情況,都是符合規格的行為。與其監控連線是否存在,不如以實際的 I/O 是否成功來判斷。
這個逾時時間是可以變更的。不過請注意,伺服器端與用戶端各有一個計時器,以較短的那一個為準。12
| 位置 | 設定 | 內容 |
|---|---|---|
| 伺服器端(指令) | net config server /autodisconnect:<分鐘數> |
到閒置切斷為止的分鐘數。最大值為 65,535。就算指定 0 也不會停用,反而會變成閒置數秒就切斷,這是個陷阱 |
| 伺服器端(指令) | net config server /autodisconnect:-1 |
停用 autodisconnect 功能本身 |
| 伺服器端(登錄檔) | HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\lanmanserver\parameters 下的 autodisconnect(REG_DWORD) |
只能變更逾時時間。用這個方法無法停用功能 |
| 用戶端(登錄檔) | HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\lanmanworkstation\parameters 下的 KeepConn(REG_DWORD,1~65535 秒) |
用戶端的閒置保持時間。預設為 600 秒(10 分鐘) |
最容易被忽略的是最後一列。用戶端的預設值是 10 分鐘,比伺服器端預設的 15 分鐘還短,因此就算只延長伺服器端,斷線的行為也不會改變。因為工作階段是以 autodisconnect 與 KeepConn 之中較短的那個值來切斷的。12
話雖如此,變更這些設定並非首選。斷線是符合規格的行為,下次存取時就會重新連線,通常不會造成實際損害。只有在遇到「舊應用程式一斷線就掛掉,又動不了」這類特殊情況時,才考慮調整數值,而且這是會影響到其他系統的設定,務必先與伺服器管理員取得共識後再進行。應用程式端應該朝「假設會斷線,並以重試因應」的方向設計,這才是正道。
第二,錯誤呈現的方式並不友善。File.Exists 無論是路徑不正確、權限不足,還是磁碟故障,都不會擲出例外,而是回傳 false。13 在本機環境中,「false 就代表不存在」幾乎不會造成困擾;但面對共用資料夾時,「檔案不存在」與「連不到伺服器/沒有權限」全都被壓縮成同一個 false,因此依 Exists 的結果做業務判斷分支的程式碼,會在網路故障時以「找不到目標」正常結束,這是一種令人討厭的損壞方式。不做存在確認、直接嘗試開啟並用例外來區分,對於共用資料夾這種對象而言,是更容易診斷的設計。
第三,對方有時候還不存在。機器剛開機時,服務的啟動有時會搶在網路準備完成之前,檔案伺服器那一端也可能正在重新啟動。持續連線(persistent connection)是在使用者登入時才會被還原的機制23,因此在不經過登入的服務世界裡,「一啟動就能看到共用」這件事沒有任何保證。正確的行為不是啟動時只做一次通訊確認,失敗就結束,而是一邊重試一邊等待。
把這 3 點納入考量後,寫入端的骨架就會變成這樣。
// 暫時性的網路錯誤就重試,業務錯誤則立即失敗
private static async Task WriteToShareAsync(string finalPath, byte[] content, CancellationToken ct)
{
var dir = Path.GetDirectoryName(finalPath)!;
var tempPath = Path.Combine(dir, $"~{Guid.NewGuid():N}.tmp");
try
{
for (var attempt = 1; ; attempt++)
{
try
{
await File.WriteAllBytesAsync(tempPath, content, ct);
File.Move(tempPath, finalPath); // 在同一目錄內 rename,公開「完成」狀態
return;
}
catch (IOException ex) when (attempt < 5 && IsRetryable(ex))
{
// 只針對暫時性的網路故障,以指數退避重試(有上限)
_logger.LogWarning(ex, "寫入共用資料夾失敗(第 {Attempt} 次)。將重試", attempt);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), ct);
}
}
}
catch
{
// 放棄並讓例外往外傳播時,盡力(best-effort)清除暫存檔
try { File.Delete(tempPath); } catch { /* 清除失敗就忽略 */ }
throw;
}
}
// IOException 不論是「網路斷線」「目標位置已有同名檔案」還是「磁碟已滿」,都會以相同型別擲出。
// 透過 HResult 的低 16 bit(Win32 錯誤碼),只挑出重試才有意義的暫時性故障
private static bool IsRetryable(IOException ex)
{
var win32 = ex.HResult & 0xFFFF;
return win32 is 53 // ERROR_BAD_NETPATH: 找不到網路路徑
or 59 // ERROR_UNEXP_NET_ERR: 未預期的網路錯誤
or 64 // ERROR_NETNAME_DELETED: 網路名稱已無法使用
or 121; // ERROR_SEM_TIMEOUT: 逾時(所謂的號誌逾時)
}
不要粗略地寫成「只要是 IOException 就重試」,這點也很重要。目標位置已有同名檔案、路徑太長、磁碟已滿之類不管重試幾次結果都不會變的失敗,也會以同樣的 IOException 送達,因此如果只靠例外的型別來判斷,就會把永久性的錯誤重試 5 次,白白等待退避時間。請像上面的範例一樣,用 Win32 錯誤碼(53/59/64/121 等,屬於共用存取斷線・逾時系列24),只篩選出「重試才有意義的失敗」。實務上比較務實的做法,是在正式環境的日誌中觀察到新的錯誤碼後,再逐步補進清單。
重點與其說是加上重試這件事本身,不如說是要搭配一套即使重試也很安全的交接協定(temp -> rename、idempotency)。若在寫入途中斷線,就會留下寫到一半的檔案。只要事先約定 final 名稱只會在「關閉後執行 rename」時才會出現,就能防止接收端讀到還沒烤熟的檔案這種事故發生。另外,行程當機或斷電時,程式碼內的清理動作本身不會執行,因此交接用的資料夾也應該一併準備「定期清除 ~*.tmp 中一段時間沒有更新的檔案」這種定期清潔機制,才不會因為垃圾檔案堆積而卡住。這套交接設計的整體樣貌,整理在「檔案整合的互斥控制基礎 - 檔案鎖與原子性 claim 的最佳實務」之中。
還有一個網路對象特有的注意事項。「失敗」這個結果,並不保證真的失敗了。若伺服器端完成 rename 後立刻斷線,可能會發生用戶端收到例外,但 final 名稱的檔案其實已經公開了這種狀態。若什麼都不考慮就直接重試,不是卡在「目標位置已有同名檔案」的錯誤,就是在接收端已經匯入第一個檔案的情況下,把同一份資料重複公開兩次。對策是,在重試 rename 階段的失敗之前,先確認並比對目標位置的狀態。若在 final 名稱中包含處理 ID(單據編號或執行用 GUID),就能判定「已經存在相同 ID 的 final 檔案 = 上次的 rename 其實已經成功」,以成功狀態結束;接收端也能利用 ID 重複來擋下重複匯入。
6. 透過 SMB 的互斥控制與 FileSystemWatcher 的可靠性
6.1. 不可過度信任鎖定
共用資料夾會同時被多個用戶端存取。用 FileShare.None 開啟的話,開啟期間的互斥即使透過 SMB 也能運作,但若設計上過度依賴「拿得到鎖就安全」的想法,在斷線導致控制代碼遺失時,或是混入不使用鎖定的對象(手動複製、其他系統)時,就會崩潰。互斥的本體不應該放在 OS 的鎖定上,而應該放在 temp -> rename・原子性 claim(從 incoming 到 processing 的 rename,只有贏家才處理)・idempotency 這種交接協定這一側。詳細的思考方式,請參考上面提到的互斥控制文章。
6.2. 在遠端共用中使用 FileSystemWatcher 的現實情況
官方文件明確指出,FileSystemWatcher 不僅支援本機,也支援監控網路磁碟機與遠端電腦上的檔案。14 使用它本身是正當的做法。不過在可靠性上有 2 個前提。
- 通知是透過緩衝區傳遞的,一旦溢位就會漏掉。若變更在短時間內集中發生,超過緩衝區大小的事件就會遺失。14
- 透過網路監控時,
InternalBufferSize的上限會被限制在 64KB。官方文件明文記載了這個比本機環境「塞不下更多」的限制。14
此外,在共用另一端發生伺服器重新啟動或斷線時,監控就這樣悄悄死掉、完全沒有通知傳來的症狀,在不具合調查(缺陷調查)的現場已經反覆見過。把遠端共用的 FileSystemWatcher 定位為「用來提早察覺的提示」,並以啟動時・發生錯誤時・定期的完整掃描(輪詢)作為真實來源,是實務上的結論。包含事件重複、順序錯亂的因應方式在內的設計模式,已在「FileSystemWatcher 的使用方法與注意事項 - 漏掉、重複通知、完成判定的陷阱」中詳述。
7. 實務的定石(判斷表)
把到目前為止的內容,整理成設計時容易猶豫不決的論點的判斷表。
| 論點 | 選項 | 判斷依據 |
|---|---|---|
| 路徑的持有方式 | 磁碟機代號 / UNC 路徑 | 應用程式處理的路徑一律用 UNC。磁碟機代號只當作使用者畫面上的便利表示2 |
| 對共用的寫入 | 直接寫入 / temp -> rename | 直接寫入無法保證「寫到一半的檔案不會被讀到」。以 final 名稱 = 完成的約定為基本原則 |
| 新檔案的偵測 | 單獨使用 FileSystemWatcher / 輪詢 / 併用 | 遠端共用要假設會漏掉事件。若間隔數分鐘可以接受,單純只用輪詢反而更簡單堅固。若需要即時性則併用14 |
| 服務的執行帳戶 | LocalSystem / 網域帳戶 / gMSA | 若有共用存取需求,就用網域帳戶或 gMSA。若環境支援 gMSA,連密碼運維都能一併消除1 |
| 需要額外認證資訊時 | 用程式碼呼叫 net use / WNetAddConnection2 / 事先用 cmdkey 登錄 | 不建議在服務內使用 net use。對同一台伺服器使用多組認證資訊會卡在 1219,因此要從設計階段就用帳戶集中或 DNS 別名來迴避210 |
| 大量用戶端直接存取 | 各端點直接連 UNC / 經由中介服務(API) | 當端點數量 × 認證資訊 × 同時存取已經管理不過來時,就把接觸共用的動作集中到一個服務,讓用戶端改用 API 溝通 |
最後一列補充說明一下。共用資料夾整合雖然簡便,但隨著存取來源增加,「誰用什麼權限」「誰跟誰會衝突」的管理難度會呈指數成長。超過某個規模之後,就把接觸檔案伺服器的行程集中成一個 Windows 服務,各用戶端改用 HTTP 或 gRPC 溝通。把共用資料夾從「系統之間的介面」降級為「該服務的內部實作」,是長期而言最容易維護的形態。
8. 總結
- 磁碟機代號只不過是以登入工作階段為單位的符號。別的使用者・提升權限的行程・服務看不到它,這是規格如此,應用程式應該用 UNC 路徑撰寫才是正道。
EnableLinkedConnections是不受支援的迴避方法。 - 從服務存取共用時,必須使用 UNC。以誰的身分驗證,由執行帳戶決定:LocalSystem/NetworkService 是電腦認證資訊,LocalService 是匿名。實務上會對網域帳戶或 gMSA 授予共用端的權限。
- 對同一台伺服器同時使用多組認證資訊連線,會因錯誤 1219 而失敗(規格如此)。應該從設計階段就用帳戶集中或 DNS 別名來迴避。
- 共用「速度慢・會斷線・有時候不在」才是正常狀態。閒置切斷(預設 15 分鐘)並不是異常,
File.Exists回傳的 false 也無法與網路故障區分。應該把重試、temp -> rename、idempotency 一起搭配導入。 - FileSystemWatcher 雖然支援遠端監控,但因為有緩衝區溢位導致的漏失,以及 64KB 的上限,併用完整掃描是實務上的定石。
- 猶豫不決時,請參考第 7 章的判斷表。當規模成長後,也請考慮把接觸共用的行程集中到中介服務的架構。
相關文章
- 檔案整合的互斥控制基礎 - 檔案鎖與原子性 claim 的最佳實務
- FileSystemWatcher 的使用方法與注意事項 - 漏掉、重複通知、完成判定的陷阱
- Windows 服務的建立與維運 ── 從工作排程器的取捨到 BackgroundService 服務化
- 如何理解 Windows 的工作階段隔離 ── Session 0・RDP・多使用者同時執行
- 正確處理 Windows 偽裝權杖 - 執行緒層級的權限借用與安全還原方式
相關諮詢領域
合同會社小村軟體提供透過共用資料夾進行檔案整合系統的設計・實作、伴隨 Windows 服務化而來的存取權・驗證相關架構檢討,以及「改成服務後就看不到共用」「只在特定環境下失敗」之類缺陷的調查。
參考連結
-
Microsoft Learn, Group Managed Service Accounts overview。說明 gMSA 是一種提供自動密碼管理與簡化 SPN 管理的網域帳戶,可以把密碼管理交給 Windows OS 負責。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Services and Redirected Drives。說明磁碟機代號並非系統全域共用,而是以登入工作階段為單位分配、服務無法存取其他工作階段的磁碟機代號、應使用 UNC 名稱、在服務內以 net use・WNet 函式對應磁碟機不建議採用的理由(認證資訊外洩、服務之間互相干擾等)、以使用者帳戶設定的服務也會建立新的登入工作階段,以及與其讓服務抱著認證資訊不放,不如建議採用用戶端偽裝。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12
-
Microsoft Learn, Mapped drives are not available from an elevated prompt when UAC is configured to Prompt for credentials。說明啟用 UAC 時會建立兩個相互連結的登入工作階段、磁碟機對應的實體是登入工作階段專屬的符號連結物件(DosDevices)、不會在工作階段之間共用,以及 EnableLinkedConnections 會強制將符號連結寫入兩個工作階段。 ↩ ↩2 ↩3
-
Microsoft Learn, Folder Redirection fails to apply when redirected to mapped drive letter, instead of UNC path。說明系統管理員登入時 LSA 會建立兩個存取權杖、磁碟機是以標準權杖對應、EnableLinkedConnections 登錄值的設定步驟,以及「可能使系統變得不安全,Microsoft 不支援」的警告,還有一律建議使用 UNC 路徑而非磁碟機代號。 ↩ ↩2 ↩3
-
Microsoft Learn, LocalSystem Account。說明 LocalSystem 在本機擁有廣泛的特殊權限,在網路上則會向遠端伺服器提出電腦的認證資訊。 ↩ ↩2 ↩3
-
Microsoft Learn, NetworkService Account。說明 NetworkService 在本機只擁有最低限度的特殊權限,在網路上則會向遠端伺服器提出電腦的認證資訊。 ↩ ↩2 ↩3
-
Microsoft Learn, LocalService Account。說明 LocalService 在網路上會提出匿名認證資訊。 ↩ ↩2
-
Microsoft Learn, About Service Logon Accounts。說明服務的登入帳戶會決定執行階段的安全性內容,以及在本機使用者帳戶的安全性內容下執行的服務,無法存取網路資源。 ↩ ↩2
-
Microsoft Learn, Create a Key Distribution Service (KDS) Root Key。說明網域控制站要產生 gMSA 密碼需要 KDS 根金鑰、以
Add-KdsRootKey -EffectiveImmediately建立、從建立起最長 10 小時內,要等到所有網域控制站的 AD 複寫收斂完成才能建立 gMSA(這是為了避免在所有 DC 都能回應 gMSA 要求之前就產生密碼的安全機制),以及執行 gMSA 的管理命令需要 64 位元架構。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, The network folder specified is currently mapped using a different user name and password error。說明對同一台伺服器嘗試用不同認證資訊建立多個連線時發生的錯誤是 by design(規格如此),以及迴避方法是改用 IP 位址連線,或建立另一個 DNS 別名。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, System Error Codes (1000-1299)。說明錯誤 1219(ERROR_SESSION_CREDENTIAL_CONFLICT)的定義與訊息。 ↩ ↩2
-
Microsoft Learn, Mapped drive connection to network share may be lost。說明閒置連線在預設情況下,經過 15 分鐘逾時後就會被切斷(autodisconnect);檔案總管的磁碟機圖示會顯示紅色叉叉,但只要一存取就會迅速重新連線;用
net config server /autodisconnect:<分鐘數>可以變更逾時時間,最大值為 65,535;指定0並不會停用該功能,反而會變成閒置數秒就切斷;用-1可以停用該功能;伺服器端登錄檔HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\lanmanserver\parameters下的autodisconnect,只能變更逾時時間,無法停用功能;用戶端登錄檔HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\lanmanworkstation\parameters下有KeepConn(REG_DWORD,1~65535 秒,預設 600 秒);以及工作階段是以 autodisconnect 與 KeepConn 之中較短的值來切斷。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, File.Exists(String) Method。說明在沒有讀取權限時不會擲出例外,而是回傳 false,以及在判斷是否存在的過程中發生任何錯誤時(路徑無效、磁碟故障、權限不足等)也會回傳 false。 ↩ ↩2
-
Microsoft Learn, FileSystemWatcher Class。說明支援監控本機電腦・網路磁碟機・遠端電腦上的檔案,緩衝區大小超過時可能會漏掉事件,以及透過網路監控時 InternalBufferSize 的最大值為 64KB。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, WNet Functions。說明 WNetGetUniversalName 是一個可以從以磁碟機為基礎的路徑,取得通用(UNC)格式名稱的函式。 ↩
-
Microsoft Learn, Secure group managed service accounts。說明 gMSA 的密碼是隨機產生的 240 位元組,由 OS 每 30 天自動變更一次,因此不需要管理員規劃密碼變更或讓服務停止。 ↩ ↩2
-
Microsoft Learn, Service Accounts and BITS。說明 LocalSystem/NetworkService 的網路驗證是以電腦認證資訊進行、LocalService 則以匿名認證資訊進行,ACL 限定給特定使用者帳戶時會被拒絕存取,以及系統帳戶不應該使用對應的磁碟機。 ↩
-
Microsoft Learn, cmdkey。說明可用 cmdkey 命令建立・列出・刪除已儲存的使用者名稱與密碼(認證資訊)。 ↩
-
Microsoft Learn, Credentials processes in Windows authentication。說明認證管理員會把認證資訊儲存在 Windows 認證資訊容器中,並在下次以後的驗證時自動提出。 ↩
-
Microsoft Learn, Client Access to Network Resources。說明伺服器行程存取網路資源的策略之一,是用指定用戶端認證資訊的 WNetAddConnection2 建立連線。 ↩
-
Microsoft Learn, SMB file server share access is unsuccessful through DNS CNAME alias。說明經由 CNAME 存取 SMB 失敗的原因之一,是別名沒有註冊對應的 SPN,並介紹了不使用 DNS 的 CNAME、改用 netdom computername 命令來定義別名的方法。 ↩ ↩2
-
Microsoft Learn, Net use。說明不帶參數執行可取得網路連線的清單、用
net use <裝置名稱>可顯示特定連線的資訊、密碼指定*可以用提示方式輸入、用/user可以指定不同的使用者名稱,以及/delete指定*會解除所有的網路連線。 ↩ ↩2 -
Microsoft Learn, Windows Networking Operations。說明持續連線(persistent connection)是一種在使用者登入時,由系統自動還原的網路連線。 ↩
-
Microsoft Learn, System Error Codes (0-499)。說明 ERROR_BAD_NETPATH(53)、ERROR_UNEXP_NET_ERR(59)、ERROR_NETNAME_DELETED(64)、ERROR_SEM_TIMEOUT(121)各自的定義。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
睡眠・休眠・Modern Standby 與長時間執行的應用程式 ── 用設計預防「半夜停止運轉」
本文將從 S3 睡眠、休眠、Modern Standby 的差異出發,整理長時間執行的 Windows 應用程式「早上一看才發現已經停止」的原因,並解說睡眠期間計時器與 TCP 連線的行為,以及透過 SetThreadExecutionState 進行抑止的做法。
自行開發的 Windows 應用程式被當成病毒處理時 ── Microsoft Defender 誤判的因應方式,以及與效能影響的相處之道
整理自行開發的 Windows 應用程式被 Microsoft Defender 誤判時的正規因應方式。從現代防毒機制的原理、向 Microsoft 回報誤判、從隔離還原,到排除設定的正確做法與風險,一併解說。
Arm 版 Windows 能執行業務應用程式嗎 ── x64 模擬(Prism)與原生 DLL・COM 的現實
本文為開發者・資訊系統部門解答「Arm 版 Windows 能執行業務應用程式嗎」這個問題,整理 x64 模擬(Prism)的運作原理、驅動程式等無法執行的層級、.NET 的 AnyCPU 與 P/Invoke 問題,以及 Arm 對應檢查清單。
MAX_PATH 與 Windows 路徑・檔案名稱的陷阱 ── 260 字元限制、保留名稱、結尾句點、大小寫
本文整理「找不到檔案」這類問題的常見原因──路徑與檔案名稱的限制。內容涵蓋 MAX_PATH=260 字元的組成、透過 LongPathsEnabled 啟用長路徑、CON 等保留名稱、結尾句點的正規化,直到 Path.Combine 的陷阱。
Windows 應用程式的工作列通知區常駐與 Toast 通知 —— NotifyIcon 的陷阱與 AppNotification 的選型
本文整理將業務用 Windows 應用程式常駐於工作列通知區,並透過 Toast 通知告知使用者的實作要點。內容涵蓋 NotifyIcon 的正確用法與「關閉後仍留在通知區」的設計、檔案總管重新啟動時的重新註冊、三種 Toast API(Windows App SDK Ap...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
故障調查 & 長期運行故障
整理間歇性故障、通訊診斷、長期運行當機、失敗路徑測試基礎的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
常見問題
整理諮詢這個主題時常見的問題。
- 為什麼從 Windows 服務無法存取網路磁碟機(Z:)?
- 因為磁碟機代號不是整個系統共用的,而是以登入工作階段為單位。使用者在檔案總管中對應的 Z:,只不過是屬於該使用者登入工作階段的符號,在不同登入工作階段中執行的服務看不到它。即使服務是以使用者帳戶執行,系統仍會為服務建立新的登入工作階段,因此就算帳戶相同,桌面端的對應也不會被繼承。從服務端應該以 \\server\share 的 UNC 路徑存取,這才是正確做法。
- 以 LocalSystem 執行的服務,該如何存取共用資料夾?
- LocalSystem 在網路上會以電腦的認證資訊進行驗證。若是網域環境,只要在共用端(共用的存取權限與 NTFS 的 ACL)對該機器的電腦帳戶(DOMAIN\MACHINE$)授予權限,就能存取。不過更換機器時就得重新設定權限,運維上不太方便,因此實務上建議把服務的執行帳戶設為網域帳戶或 gMSA(群組受管理服務帳戶),並對該帳戶授予共用端的權限。
- 可以用 FileSystemWatcher 監控共用資料夾上的檔案嗎?
- 可以使用,但請不要單獨信任它。FileSystemWatcher 支援監控網路磁碟機與遠端電腦,但由於變更通知是透過緩衝區傳遞,一旦通知集中發生,緩衝區就會溢位,導致漏掉事件。而且透過網路監控時,緩衝區上限會被限制在 64KB。應該把通知當作「觸發重新掃描的契機」,並且務必併用啟動時・發生錯誤時・定期的完整掃描(輪詢),這是實務上的定石。
- 要如何讓檔案整合對網路斷線更有韌性?
- 設計時要把「共用速度慢・會斷線・有時候不在」當作正常狀態。具體來說,寫入時先用暫存檔名寫,關閉後再重新命名以公開(temp -> rename);讀寫都用重試機制包起來,並區分暫時性的網路錯誤與業務錯誤。要注意 File.Exists 即使在無法存取的情況下也會回傳 false,因此無法區分「檔案不存在」與「連不到伺服器」;而讓處理即使重複執行也不會壞掉的 idempotency(冪等性)設計,則是最後一道防線。