
很多現(xiàn)在用 .NET Core 寫服務的年輕朋友可能已經(jīng)不太能理解“跨平臺”這件事為什么值得單獨寫一篇文章。默認不就是dotnet build一下然后丟到 Linux 容器里跑嗎但如果你經(jīng)歷過.NET Framework4.x 時代或者曾經(jīng)被客戶要求在 CentOS 上部署一個老 ASP.NET 項目而勸退過就會知道“ .NET Core 跨平臺”這幾個字背后其實是整個 .NET 生態(tài)跟歷史遺留包袱打了一場多年的戰(zhàn)爭。這篇是系列上篇不急著講 CoreCLR 和依賴注入那些事先聊歷史的枷鎖為什么當年的 .NET 天生就長在 Windows 上為什么跨平臺這么難又是什么力量把它一步步撬開的。適合看這篇的人大概是兩類一類是剛接觸 .NET Core、想知道“為什么以前不行、現(xiàn)在行”的新人另一類是還在維護老 .NET Framework 項目、想上云或者遷移到 Linux 的開發(fā)者。前者能少走彎路后者能看清坑在哪。只要你動手做過一次跨平臺遷移對這些歷史包袱的體感就會完全不同。1. 從“Windows的唯一答案”說起.NET 的基因里刻著什么1.1 .NET 出生時的定位為 Windows 而生的“下一代開發(fā)平臺”很多人現(xiàn)在回頭看 .NET 1.0容易下意識拿它跟 Java 對比覺得“都是托管運行時為什么 Java 一開始就跨平臺.NET 不跨”。這其實是用今天的視角美化歷史。2002 年 .NET Framework 1.0 發(fā)布時它的定位根本不是“跨平臺”而是微軟押注 Windows 生態(tài)的下一代開發(fā)模型。那時候微軟的主要對手是 Java 和 Sun 的整套體系微軟要做的是把開發(fā)者留在 Windows 上讓 Windows 成為唯一合適的宿主機。這不是我事后總結出來的而是從 .NET 早期的架構細節(jié)里能直接看出來的。CLR 在很大程度上被設計成操作系統(tǒng)的一部分而不是一個獨立運行的應用層運行時。.NET Framework 的很多組件是跟著 Windows 補丁一起分發(fā)的Windows XP、Vista、7、8、10 都內(nèi)置了特定版本的 .NET Framework。這種綁定帶來的好處是開發(fā)者在部署時很省事系統(tǒng)自帶就能跑壞處是運行時本身無法脫離操作系統(tǒng)獨立進化。尤其到了 Windows 8 時代.NET Framework 4.5 幾乎就是 Windows 的一個系統(tǒng)組件你想換版本得看操作系統(tǒng)臉色。更麻煩的是.NET 的整個框架庫并不是空中樓閣。BCL 里大量功能的實現(xiàn)直接依賴 Win32 API。比如你寫一句File.ReadAllText底層很可能就是一次CreateFile加ReadFileRegistry.GetValue聽名字就知道只有 Windows 注冊表才有。一個運行時如果能在 Linux 上跑 JIT充其量只是“肌肉”過來了“內(nèi)臟”還全是 Windows 的。這就是當時所有跨平臺嘗試面臨的第一個結構性障礙框架代碼比運行時代碼更不平臺無關。1.2 組成 .NET 的幾根“綁定繩”P/Invoke、COM、WinForms 與 IIS如果只講“底層調用了 Win32”還是太抽象。具體一點.NET Framework 的跨平臺障礙實際上是幾個大塊頭綁在一起的P/Invoke 機制本身不是問題問題在于它調用的大多是 Windows 專屬 API。連System.IO、System.Diagnostics.EventLog、System.ServiceProcess這種基礎命名空間都長著 Windows 的臉。COM 互操作是 .NET 早期的重要賣點但 COM 幾乎等于 Windows 的代名詞。當年大量企業(yè)組件、Office 自動化、WMI 管理接口全依賴 COM這套生態(tài)天然排斥非 Windows 平臺。WinForms 和 WPF 是桌面 UI 的兩大王牌但它們分別依賴 User32、GDI 和 DirectX 渲染。把這些搬到 Linux 上等于把 Windows 的窗口管理器也搬過去工程量巨大。ASP.NET WebForms 和早期 ASP.NET MVC 的宿主是 IIS。IIS 本身是 Windows 服務請求管線、進程模型、身份認證都和 Windows 賬戶體系深度耦合。就算 CLR 能跨平臺Web 框架這層也過不去。這些綁定不是某一天設計出來的而是一層層疊加出來的。我見過不少朋友在 .NET Framework 項目里用ComponentDispatcher、HwndSource、RegistryKey這些類寫的時候沒覺得有什么問題一旦想換平臺才發(fā)現(xiàn)這些 API 的“老家”都在 Windows 的系統(tǒng) DLL 里??缙脚_不是編譯一次換個后綴名那么簡單它意味著整個框架層的“地基”都要換。2. 掙脫的先行者Mono 把“不可能”拉到“可能”2.1 Mono 的起點ECMA 標準是那把鑰匙在 .NET Core 之前“.NET 跨平臺”這個命題不是沒人做過最著名的就是 Mono。2001 年Ximian 發(fā)起 Mono 項目目標是在 Linux 上提供一個獨立于微軟實現(xiàn)的 .NET 運行時。它之所以能啟動關鍵不是因為微軟大發(fā)慈悲而是因為微軟把 C# 語言和公共語言基礎架構CLI提交到了 ECMA也就是后來的 ECMA-334 和 ECMA-335。ECMA 標準公開之后第三方依據(jù)標準文檔實現(xiàn)自己的 CLR 和編譯器就成了合法且可行的事。Mono 的意義在當年非常大。它讓 Linux 桌面應用有能力使用 C# 開發(fā)GNOME 桌面里好幾個核心工具就是 Mono 寫的。后來的 Unity 游戲引擎也是把 Mono 作為腳本運行時帶到了無數(shù)游戲里??梢哉fMono 是第一個證明“托管代碼可以脫離微軟親兒子運行環(huán)境”的項目。它提前給社區(qū)埋下了一顆種子原來 .NET 的江湖不是只有微軟一家。但 Mono 也背負著沉重的歷史包袱。它要兼容的是 .NET Framework 的龐雜 API而這些 API 里有一大堆是 Windows 專屬的。Mono 團隊選擇用各種方式“補兼容”能實現(xiàn)的實現(xiàn)不能實現(xiàn)的就拋異常或者給個空殼。這種“盡力兼容”的思路在小項目里很奏效碰到大型企業(yè)項目就力不從心。我們今天回頭看Mono 不是輸在技術不行而是輸在“它要復刻的對象本身就是一座 Windows 專屬的大廈”。2.2 Mono 的歷史貢獻讓跨平臺從“絕對不行”變成“看情況行”如果說 .NET Framework 的歷史枷鎖是把鎖鏈完全焊死在 Windows 上那 Mono 就是拿鑿子把鎖鏈敲松了一點。至少從它開始社區(qū)明確知道 .NET 是可以用另一個運行時跑起來的只是兼容層會受傷。Mono 能跑通不少控制臺程序、類庫、網(wǎng)絡服務但 WPF、WCF 服務端、完整 IIS 管線這些大家都繞不過去。實踐中最常見的問題是開發(fā)機 Windows 上編譯好好的 Web 項目放到 Mono 環(huán)境里因為某個System.Drawing底層依賴跑不起來或者是數(shù)據(jù)庫驅動、加密組件內(nèi)部走了 P/InvokeLinux 上沒有對應的 native 庫整個服務直接崩掉。我當時排查過不少這類問題最后多半都是繞路換 API而不是真正解決問題。所以說Mono 打破了“不可能”的絕對化但它并沒有打通一條系統(tǒng)性的官方路徑。它更像是江湖俠客靠個人武藝開路真正的官方隊要等到很多年后 .NET Core 出現(xiàn)才進場。3. .NET Core 誕生的導火索云計算的倒逼與老后端的拖累3.1 2014 年的微軟轉身為什么要親手拆掉 Windows 綁定Mono 奮斗了十幾年微軟一直按兵不動但到了 2014 年前后形勢變了。云計算興起之后服務器端的大量工作負載從 Windows Server 轉向 Linux企業(yè)采購開始更多依賴開源技術棧。如果微軟繼續(xù)把 .NET 鎖在 Windows 上那 .NET 在服務器端的市場份額只會越來越小。阿里、亞馬遜、谷歌的云上跑著海量 Linux 實例開發(fā)者想用 .NET 都沒地方放這是很現(xiàn)實的問題。所以微軟做了一系列轉向開放 .NET 官方實現(xiàn)源碼、成立 .NET Foundation、把 Roslyn 編譯器、CoreCLR、CoreFX 放到 GitHub 上。這一步的意義很直接微軟從“把 .NET 當作 Windows 生態(tài)的一部分”轉變成“把 .NET 當作微軟云上一種主流開發(fā)工具”。工具可以跨 Windows 和 Linux才能真正適應混合云、多平臺部署的節(jié)奏。這在當時被看作“微軟擁抱開源”的標志性事件但在我看來本質是商業(yè)利益驅動的架構松綁。這件事給開發(fā)者的沖擊很大。尤其對中小企業(yè)來說以前要跑 .NET 網(wǎng)站必須買 Windows Server 授權運維成本比 Linux 高不少。很多創(chuàng)業(yè)團隊選型時直接把 .NET 排除掉不是語言不好而是平臺成本太高。.NET Core 開源并跨平臺之后同樣一套代碼可以部署到便宜得多的 Linux VM 或容器里這才是它能重新獲得關注的根本原因。3.2 老框架的結構性難題安裝式更新、GAC 和 IIS 的綁架既然要重寫一個跨平臺運行時就得先清算老框架的歷史包袱。.NET Framework 最典型的問題有三個每一個都夠讓人頭疼。第一是“機器級安裝”模型。.NET Framework 的運行時是操作系統(tǒng)級別安裝的升級也是系統(tǒng)級升級。你在自己機器上裝一個新版 .NET 補丁可能會影響同一臺機器上的所有老應用。企業(yè)環(huán)境里不同項目依賴不同 .NET 修補版本的情況非常常見一個應用升級導致另一個應用掛掉簡直是運維噩夢。第二是 GAC也就是全局程序集緩存。程序集裝進 GAC 之后所有應用共享看起來省空間實際上版本沖突讓人頭大。你想讓兩個應用用不同版本的同一個第三方庫GAC 模式下很容易互相踩。這種共享依賴的更新方式放到現(xiàn)代容器化部署里完全行不通。第三是 System.Web 與 IIS 的深度耦合。老 ASP.NET 的請求生命周期是嵌在 IIS 里的進程管理、線程調度、管道事件全跟 IIS 綁在一起。IIS 是 Windows 專屬服務所以雖然理論上 CLR 有移植可能只要 Web 框架這層不動跨平臺就是空談。想從根上解決只能把 Web 框架也拆掉重寫這就是為什么 ASP.NET Core 沒有沿用 System.Web 的老模型。我印象特別深的是 .NET Framework 時代部署網(wǎng)站發(fā)布完往往還要在 IIS 管理工具里手動配應用程序池、權限、身份認證。而到了 .NET Coredotnet run起來了Kestrel 自己監(jiān)聽端口想折騰就套一層 Nginx跨平臺自然就順了。這背后不是運氣而是結構設計變了。4. 拆掉“鎖鏈”的核心.NET Core 背后的架構手術4.1 從“單一大包”到“模塊組件”CoreCLR 與 CoreFX.NET Core 能跨平臺根本原因是微軟沒有再走“一個大而全的 Framework”老路而是把運行時和基礎庫拆成可獨立分發(fā)的模塊。CoreCLR 負責 JIT、GC、類型系統(tǒng)這些運行時核心CoreFX 提供System.*命名空間下的大部分基礎庫實現(xiàn)兩者都能隨應用一起發(fā)布不依賴操作系統(tǒng)預裝。這套模型的直接好處是你的應用可以自己帶一份運行時部署到任意支持的操作系統(tǒng)上。不需要在目標機器上先安裝特定版本的 .NET Framework也不用怕系統(tǒng)補丁把運行時改掉。這種“app-local runtime”的模式在 Node.js、Go 和 Python 虛擬環(huán)境里已經(jīng)非常成熟.NET Core 把它吸收過來等于把歷史包袱甩掉一大半。同時模塊化也讓平臺專屬 API 顯形了。像 Windows 注冊表、事件日志、服務控制被抽到獨立的 NuGet 包里只有目標平臺是 Windows 時才會使用??缙脚_代碼默認不引用這些包這就從源頭上避免了“我明明寫跨平臺項目卻順手調了個 Win32 函數(shù)”的尷尬。4.2 .NET Standard 與兼容層老項目遷移的“體檢清單”對老 .NET Framework 項目來說直接跳到 .NET Core 不現(xiàn)實所以微軟還搞了一個 .NET Standard 作為中間契約。簡單理解它就是一套跨平臺 API 的公共子集。如果你寫的庫只用到 .NET Standard 里的 API那么這個庫可以被 .NET Framework、.NET Core、Xamarin 等多個運行時共用。這不是魔法而是把歷史 API 里真正平臺無關的部分提煉出來做成一個大家都認的“普通話”。當然.NET Standard 不可能解決所有兼容問題。老項目里常見的 Windows 專屬依賴表格里列的比較清楚歷史包袱跨平臺影響.NET Core 后的處理WinForms / WPF僅限 Windows 桌面在 .NET Core 3.0 中繼續(xù)支持但仍是 Windows-onlySystem.Web / ASP.NET WebForms依賴 IIS 管線不兼容需要改用 ASP.NET CoreAppDomain 動態(tài)加載平臺相關能力過大用 AssemblyLoadContext 替代更可控Windows 注冊表 / 服務 / 事件日志調用 Windows 專屬 API獨立 NuGet 包僅 Windows 啟用System.DrawingGDI 底層 Windows 化跨平臺場景建議用 ImageSharp 等替代老項目遷移的時候我建議先把所有項目依賴用 API Portability Analyzer 掃一遍看看到底有沒有踩不能跨平臺的 API。這一步能省掉后面大量部署時才發(fā)現(xiàn)問題的痛苦。別指望 .NET Standard 幫你“自動跨平臺”它只是給你劃了一條安全線越線的事還得自己處理。4.3 服務器層解綁Kestrel 替代 IIS跨平臺的最后一根鐵鏈在服務器層。老 ASP.NET 的宿主是 IISIIS 又是 Windows 專屬所以早期換平臺根本無從談起。ASP.NET Core 直接把 Kestrel 作為內(nèi)置的跨平臺 Web 服務器Kestrel 是純托管實現(xiàn)的可以在 Windows、Linux、macOS 上直接監(jiān)聽端口處理 HTTP 請求。生產(chǎn)環(huán)境里你覺得 Kestrel 裸奔不放心就可以在前面放 Nginx 或 Apache 做反向代理靜態(tài)文件、負載均衡、HTTPS 終結這些都可以交給代理層。這個變化讓 .NET 真正進入了 Linux 部署的主流陣營。我身邊好幾個開源項目包括之前折騰過的那個“跨平臺音樂管理系統(tǒng) v2.0 源碼”后端就是用 ASP.NET Core 寫的跑在 Linux 小主機上用 Nginx 反代配合 SQLite 存儲穩(wěn)定性和部署體驗都很好。放到五年前這種組合是不可能想象的。所以為什么說歷史枷鎖被拆掉了因為從運行時到基礎庫再到 Web 服務器三層全面換血。底層不再是 Windows 系統(tǒng)調用的“附庸”服務器層也不再被 IIS 獨占。剩下的第三方庫生態(tài)中真正卡你跨平臺的已經(jīng)不多大多是能用替代方案繞過去的。5. 給老項目遷移者的三條大實話最后不做什么宏大總結就說我在實際折騰遷移過程中最真實的幾條感受也許比抽象地講歷史更有用。第一條不要指望自動化工具“一鍵遷移”。就算用了 .NET Upgrade Assistant老項目里的 Windows 專屬依賴還是得自己一個個決定是替換、抽象還是放棄。特別是System.Web相關的代碼越早重寫越省心留到后面只會更痛。第二條盡量把跨平臺邊界畫在“項目依賴”而不是“代碼技巧”上。我見過很多項目寫著寫著就從 NuGet 里引了Microsoft.Win32.Registry原因只是某臺 Windows 機器上原來用了注冊表做配置。到了 Linux這個依賴就得換。最好的做法是開始設計時就明確哪些功能是 Windows-only用接口隔離出來別讓它混進核心業(yè)務邏輯。第三條體驗一次 Linux 容器部署比讀十篇文章都管用。拿一個 ASP.NET Core 的空項目放到 Docker 的mcr.microsoft.com/dotnet/aspnet鏡像里跑起來再掛個 Nginx 反代你會真正體會到跨平臺的紅利在哪。容器化加上自包含發(fā)布之后.NET 應用和 Go、Node 的應用已經(jīng)沒有本質區(qū)別構建一次到處運行。我個人反而很喜歡這段歷史。它不是“微軟突然就跨平臺了”的爽文而是先被 Windows 綁了十幾年被 Mono 撬開一道縫被云計算逼到墻角最后才靠拆掉原有架構換來的自由。下篇我會繼續(xù)聊聊真正的“自由之路”CoreCLR 從零開始的設計取舍以及跨平臺之后 .NET 生態(tài)失去了什么、得到了什么。如果你恰好也在糾結老項目的去處歡迎先把歷史看懂再動手。