[caption id="attachment_411" align="alignleft" width="334"] from: blog.docker.com[/caption] Microsoft 官方宣布能在 Linux 上面運行 .NET (v5) 應用程式.. 其實這已經不是什麼新聞了,去年 Microsoft TechEd 2014 North America 就正式的發布這個消息了,不過因為種種原因,去年看到這新聞的時候,只停留在 "看看" 的階段而已,直到現在時機成熟了才開始動手研究。一來是因為官方的開發工具 Visual Studio 2015 已經 RTM 了 (這麼大的軟體,實在不大想安裝 preview 版本).. 二來 Docker 用的很高興,NAS / Ubuntu Server 也都已經準備好,執行環境我也上手了.. 萬事俱備只欠東風,於是今天就趁周末,把 .NET Core 版的 "Hello World" 丟到 Docker 裡面執行的任務給搞定了 :D Microsoft 自從新任 CEO Satya Nadella 上任後,宣布了一連串改變,我覺得對未來影響最大的就是 .NET Open Source + Support Linux 這件事了。我認為這決策帶來的影響,遠遠大於 ASP.NET5 本身開發技術及架構上的改變 (例如 MVC6.. 動態編譯.. Dependency Injection 等等)。因為前者影響的是整個 .NET 生態的改變,可能會影響到將來大型應用部屬的架構決策,而後者影響的只是開發團隊,完全是不同層級的問題..。如果你的職責是 system architect, 那千萬別忽略這個改變.. 以後 "混搭" 風格的架構一定會越來越盛行,不管是在 windows server 上面,或是在 Linux server 上都是。 在這兩年內,另一個很快竄起的技術: Docker, 也是另一個關鍵。Docker 這才出來兩年就紅翻天的 Container 技術,這種東西實在太對 architect 的胃口了。過去 VM 的技術沒甚麼不好,不過最大的問題就是: 充分虛擬化之後,帶來的副作用是多一堆 "虛擬" 的機器要管理... 每個 VM 裡面都要裝一套 OS,不論是 Linux or Windows,都要花力氣去維護,要執行這些 OS 也要花費運算資源... 舉個慘痛的經歷,我就碰過在同一台實體機器上,上面的 10 台 VM 都開始掃毒,那狀況真的只能用 Orz 來形容... 上面的 APP 都還沒認真在跑,系統就被 OS + AntiVirus 給拖垮了... Docker 只虛擬化 Application 的執行環境,巧妙的避開了 VM 過度使用帶來的副作用... 舉例來說,如果你的應用規模不大,只要一台最低規格的 VM 就跑得動的話,你會為了架構考量把他分裝在三台 VM,實作三層式架構嗎? 應該沒人會這麼幹吧? 除非你要展示應用架構,或是可預見的將來會需要 scale out, 不然這樣搞只是自找麻煩而已。 但是現在用了 Docker 你就可以盡可能的採用你認為最理想的架構,分成幾個獨立的 container 可以維持架構上的正確性,同時也不必擔心架構帶來效能的折損。 因為這個原因,了解這種跨界的應用,對我來說遠比了解 ASP.NET5 本身 coding 帶來的改變還重要的多。裝了 Visual Studio 2015 之後,第一件事不是先寫看看 MVC6,而是先弄了個 Hello World, 試看看該怎樣丟到 Linux 上面跑.. 為了這件事,前置作業就花了快一個月... 有在 follow 我的 facebook 或是訂閱 blog 的就知道,之前我都在研究 Docker ,包括在 NAS 上用我理想的架構,來架設我自己的 blog.. 也弄了台差點要被扔掉的舊筆電,架了台研究用的 Ubuntu Server .. 接下來主角要上場了,就是 .NET CoreCLR !
這時 command prompt 已經變了,由原本的 chicken@localhost:~$ 變成 root@93462d92e941:/# , 代表 shell 已經啟動成功,且順利進去 conainer 內了,接下來我就可以把它當作 VM 開始大玩特玩..
原本只是想在 NAS 上簡單玩玩 Docker, 為了接下來的 ASP.NET 5 做準備.. 不過實在太好用,還沒開始做正事 (ASP.NET 5), 就先把原本的 BLOG 從 GoDaddy 的 hosting 搬回來放 NAS 了,順手也架了 reverse proxy ... 現在 NAS Docker 有正式用途了, 而 NAS 的運算能力又很有限 (我的是 DS-412+, CPU 只是 atom d2700, 雙核而已, 1GB RAM), 裝沒幾個就跑開始擔心了,於是就開始想另外搞一個可以隨便玩得 docker engine 環境.. 其實 PC 上弄個 VM 是很容易啦,windows 10 內建的 hyper-v 根本就不用花甚麼功夫, 不過我想弄個像 NAS 這樣省電不大需要去管它, 24hr 開著隨時都可以用.. 就把腦筋動到古董要丟掉的筆電.. 我老姊正好有台筆電要丟掉,找我幫他先把硬碟資料清掉她才敢丟...一切都來的太巧了! 於是這台就被我拿來大整修一番... 這台的規格是 Acer Aspire 5742Z:
前言: 先讓我講一點前情提要 XD,想看安裝步驟的請自己跳到後面... 在買這台 NAS 之前 (Synology DS-412+), 我是自己在家裡弄了台 PC, 裝了一堆硬碟充當個人用 file server, 同時順便在裡面架了自己常用的網站,包含這部落格的前身 (BlogEngine),還有自己用的 Visual SVN, 另外也架了 Linux VM 裝 Redmine 等等其他的東西.. 後來 PC 開始不聽話了,開始三不五時當機,心一橫,兩年多前就買了台 4Bays NAS 把 Server 換掉,頓時輕鬆許多... 只是 NAS 不比 Server, 慣用 Windows Server 的我一時找不到替代品,這些服務就一個一個搬家了。其中最重要的 BLOG,就搬到 GoDaddy 的免費 web hosting (有 IIS),繼續在上面掛著.. 用過 Synology NAS 的大概都知道,它的特色就是 DSM 很好用,也有提供很多 Package, 讓 NAS 加裝軟體就像手機逛 App Store 一樣簡單... 不過,DSM 的裝機量不比手機,很多套件要是沒有經過 Synology 包裝,要安裝就是件麻煩事了。就算有 Synology 官方的打包套件,更新或是維護也不比這些軟體的官方來的快。尤其是從小到大都是抱著 Microsoft 大腿的我更是不適應 @@ 直到幾個月前 Synology Release DSM 5.2, 正式在裡面搭載了 Docker ! 這真是天大的好消息.. :D Docker 是個好物,沒用過或沒聽過的可以參考這裡: What is docker ? 簡單的說,是另一種虛擬化的應用。他不像 VM 是將硬體虛擬化,所以不用在上面安裝 OS,只虛擬化執行環境... 啟動速度很快,兩三秒就可以啟動了,少掉 OS 這層,整個就很輕量化,架在 Docker 裡,跟原生地執行環境速度,CPU / RAM 資源的使用沒甚麼明顯差別... 我覺得 DSM 加入 Docker 真的是 Synology 最聰明的決定了,比 Q 牌直接導入正統的虛擬化技術還實用... 畢竟個人用的 NAS 都不會有太強大的運算資源,為了這樣去拚硬體配備就有點本末倒置了。Docker 這種輕量化的虛擬化技術,正好補足了這需求 講這麼多幹嘛? 因為 Docker 實在太熱門了,所以在 Docker Hub 上大概所有熱門的應用都有人包好了,煩惱的是同樣的東西太多人包了,有時還真不知該怎麼選 @@.. 我是有官方版的就盡量挑官方的來用。因為社群的差異太大了,因此從 Docker 上可以找到的選擇,遠比從 Synology Package Center 找到的又多又廣泛,我就開始一個一個替換的計畫... 計畫要安裝的有好幾個,包括 WordPress, Redmine, MYSQL, WebSVN, 還有為了方便發布這些服務,還想裝個 Reverse Proxy... 不過流水帳我就省了,這次的範例我就拿 WordPress 跟 Reverse Proxy 當案例,給有需要的人參考步驟就好。
NameVirtualHost *:80 <VirtualHost *:80> ServerName * DocumentRoot /var/services/web </VirtualHost> <VirtualHost *:80> ServerName columns.chicken-house.net DocumentRoot "/var/services/web/columns" ErrorDocument 403 "/webdefault/error.html" ErrorDocument 404 "/webdefault/error.html" ErrorDocument 500 "/webdefault/error.html" # start of (reverse proxy settings) ProxyPreserveHost On ProxyRequests Off <Location / > ProxyPass http://nas:8012/ ProxyPassReverse http://columns.chicken-house.net/ Order allow,deny Allow from all </Location> # end of (reverse proxy settings) </VirtualHost>完成後,用這個指令 restart apache (httpd), 讓設定生效:
httpd -k restart再用瀏覽器測試一下網址 http://columns.chicken-house.net, 應該就可以看到 wordpress 的內容了 ! 拿手機測試一下,關掉 wifi, 用 4G 連看看我自己的網站... 果然可以用正常的 URL 看到內容:
原本只是很單純的把大型檔案 (100mb 左右的 video) 放到 Azure Storage 的 BLOB 而已,結果效能一直不如預期般的理想。才又把過去的 thread 技巧搬出來用,結果又花了點時間改寫,用 async / await 的效果還更漂亮一點,於是就多了這篇文章 :D
其實這次要處理的問題很單純,就是 WEB 要從 Azure Storage Blob 讀取大型檔案,處理前端的認證授權之後,將檔案做編碼處理後直接從 Response 輸出。主要要解決的問題是效能過於糟糕… 透過層層的處理,效能 (3.5 Mbps) 跟直接從 Azure Storage 取得檔案 (7.3 Mbps) 相比只剩一半左右.. 過程中監控過 SERVER 的 CPU,頻寬等等,看來這些都不是效能的瓶頸。
為了簡化問題,我另外寫了個簡單的 Sample Code, 來呈現這問題。最後找出來的原因是,程式碼就是單純的跑 while loop, 不斷的把檔案內容讀進 buffer 並處理後,將 buffer 輸出。結果因為程式完全是 single thread 的處理方式,也沒有使用任何非同步的處理技巧,導致程式在讀取及處理時,輸出就暫停了,而在輸出時,讀取及處理的部份就暫停了,讓輸入及輸出的 I/O, 還有 CPU 都沒有達到滿載… 於是效能就打對折了。用時間軸表達,過程就如下圖:
這樣的設計方式,同一時間只能做一件事。若把上圖換成各種資源的使用率,會發現不論是 DISK、NETWORK、CPU等等資源,都沒有同時間保持忙碌。換句話說好像公司請了三個員工,可是同時間只有一個人在做事一樣,這樣的工作安排是很沒效率的。要改善的方法就是讓三個員工都保持忙碌,同時還能亂中有序,能彼此協調共同完成任務。
同樣的狀況應該很普遍吧? 不要說別人了,就連我自己都寫過很多這樣的 CODE … 光是 COPY 大型檔案,大家一定都是這樣寫的: 用個 while loop, 把來源檔讀進 buffer, buffer 滿了寫到目地檔,然後不斷重複這動作,直到整個檔案複製完成為止。這不是一模一樣的情況嗎? 只是大部份的人不會去考量如何加速這樣的動作而已…
我先把目前的CODE簡化一下,拿掉一些不相關的部份,單純的用 Read()
/ Process()
/ Write()
三個空的 method 代表執行這三部份的工作,執行過程需要的時間,就用 Task.Delay( 100 ) 來取代。經簡過後的 Code 如下:
經簡後的示意程式碼:
public class Program
{
static Stopwatch read_timer = new Stopwatch();
static Stopwatch proc_timer = new Stopwatch();
static Stopwatch write_timer = new Stopwatch();
static Stopwatch overall_timer = new Stopwatch();
public static void Main(string[] args)
{
overall_timer.Start();
for (int count = 0; count < 10; count++)
{
Read();
Process();
Write();
}
overall_timer.Stop();
Console.WriteLine("Total Time (over all): {0} ms", overall_timer.ElapsedMilliseconds);
Console.WriteLine("Total Read Time: {0} ms", read_timer.ElapsedMilliseconds);
Console.WriteLine("Total Process Time: {0} ms", proc_timer.ElapsedMilliseconds);
Console.WriteLine("Total Write Time: {0} ms", write_timer.ElapsedMilliseconds);
}
public static void Read()
{
read_timer.Start();
Task.Delay(200).Wait();
read_timer.Stop();
}
public static void Process()
{
proc_timer.Start();
Task.Delay(300).Wait();
proc_timer.Stop();
}
public static void Write()
{
write_timer.Start();
Task.Delay(500).Wait();
write_timer.Stop();
}
}
程式執行結果:
程式總共要花掉 10 秒鐘才執行完畢,由於完全沒有任何並行的處理,因此就是很簡單的 Read 花掉 2 秒,Process 花掉 3 秒,Write 則花掉 5 秒,加起來剛好就是總執行時間 10 秒。
回顧一下,過去寫過幾篇如何善用多執行緒來解決各種效能問題的文章,其中兩篇跟這次的案例有關:
其實這些方法的目的都一樣,都是透過各種執行緒的操作技巧,讓一件大型工作的不同部份,能夠重疊在一起。這樣的話,整體完成的時間就能縮短。不過,隨著 .NET Framework 一直發展,C# 5.0 提供的 Syntax Sugar 也越來越精彩,到了 .NET Framework 4.5 開始提供了 Async / Await 的語法,能夠大幅簡化非同步模式的設計工作。
非同步的程式設計,其實也是 multi-threading 的一種運用。簡單的說,它就是把要非同步執行的任務丟到另一條執行緒去執行,等到它執行結束後再回過頭來找它拿結果。只是為了這樣的一個動作,往往得寫上數十行程式碼,加上原本程式的結構被迫切的亂七八糟,過去往往非絕對必要,否則不會用這樣的模式。
這次我的目的,其實用前面那幾篇的技巧就能解決了。不過這次實作我想換個方法,都已經 2013 了,有 Async / Await 為何要丟著不用? 這次就用新方法來試看看。先用上面的時間軸那張圖,來看看改進後的程式執行狀況,應該是什麼樣子:
解釋一下這張圖: 橘色的部份代表是用非同步的方式呼叫的,呼叫後不會 BLOCK 原呼叫者,而是會立即 RETURN,兩邊同時進行。而圖中有個箭頭 + await
, 則代表第二個非同步呼叫 Write()
的動作,會等待前一個 Write()
完成後才會繼續。
Write()
跟下一次的 Read()
其實並無相依性,因此在開始 Write()
時,其實可以同時開始下一回的 Read()
, 因此時間軸上標計的執行順序就可以被壓縮,調整一下執行的順序,馬上得到大幅的效能改進。這次要改善的,就是把 Read() + Process()
跟 Write()
重疊在一起,預期會有一倍的效能提升。
想要瞭解 C# 的 async / await 該怎麼用,網路上的資源有很多,我習慣看官方的文件,有需要參考的可以看這幾篇:
Async / Await 的細節我就不多說了,簡單的說在 method 宣告加上 async 的話,代表它的傳回值會被改成 Task<>
, 而呼叫這個 method 會變成非同步的,一旦呼叫就會立刻 Return, 若需要這個 method 的執行結果,可用 await 等待,直到 method 已經執行完畢才會繼續…
廢話不多說,過程就沒啥好說的了,直接來看改好的程式碼跟執行結果:
改寫為非同步模式的 CODE:
public class Program
{
static Stopwatch read_timer = new Stopwatch();
static Stopwatch proc_timer = new Stopwatch();
static Stopwatch write_timer = new Stopwatch();
static Stopwatch overall_timer = new Stopwatch();
public static void Main(string[] args)
{
overall_timer.Start();
DoWork().Wait();
overall_timer.Stop();
Console.WriteLine("Total Time (over all): {0} ms", overall_timer.ElapsedMilliseconds);
Console.WriteLine("Total Read Time: {0} ms", read_timer.ElapsedMilliseconds);
Console.WriteLine("Total Process Time: {0} ms", proc_timer.ElapsedMilliseconds);
Console.WriteLine("Total Write Time: {0} ms", write_timer.ElapsedMilliseconds);
}
public static void Read()
{
read_timer.Start();
Task.Delay(200).Wait();
read_timer.Stop();
}
public static void Process()
{
proc_timer.Start();
Task.Delay(300).Wait();
proc_timer.Stop();
}
public static async Task Write()
{
write_timer.Start();
await Task.Delay(500);
write_timer.Stop();
}
private static async Task DoWork()
{
Task write_result = null;
for (int count = 0; count < 10; count++)
{
Read();
Process();
if (write_result != null) await write_result;
write_result = Write();
}
await write_result;
}
}
程式碼幾乎都沒有動,不過就是把 Write()
改寫為 Async 版本,同時在主程式 DoWork()
用 Task 形別,把 Write()
傳回的 Task
物件,保留到下一次呼叫 Write()
前,用 await 來確保上一個 Write()
已經完成。
改寫過的版本,程式碼很簡單易懂,90% 以上的程式碼結構,都跟原本同步的版本是一樣的,大幅維持了程式碼的可讀性,完全不像過去用了多執行緒或是非同步的版本,整個結構都被切的亂七八糟。看看程式的執行結果,果然跟預期的一樣,整體執行時間大約為 5 秒。多出來的 660 ms, 就是第一次的 Read() + Process()
, 跟最後一次的 Write()
是沒有重疊的,因此會多出 500 ms, 再加上一些執行的誤差及額外負擔,就是這 660ms 的來源了。
最後,來看一下效能的改善。在我實際的案例裡,Read 是受限於 VM 與 Storage 之間的頻寬,固定為 200Mbps, 而 Process 是受限於 VM 的 CPU 效能,也是固定可控制的, 最後 Write 則是受限於 client 到 VM 之間的頻寬,可能從 2Mbps ~ 20Mbps 不等,這會直接影響到到 Write 需要的時間。
不管是用 thread 或是 async ,都不是萬靈丹,主要還是看你的狀況適不適合用這方法解決。這次我的案例是用 async 的方式,將 Read / Write 閒置的時間重疊在一起,節省的時間就反應在整個工作完成的時間縮短了。因此兩者花費的時間差距如果過大,則就沒有效果了。
我簡單列了一張表,來表達這個關係。分別針對 client 端的頻寬,從 2Mbps ~ 200Mbps, 列出使用 async 改善前後的花費時間,及效能改善的幅度:
*200M | 100M | 80M | 50M | 20M | 10M | 5M | 2M | |
---|---|---|---|---|---|---|---|---|
原花費時間(ms) | 7000 | 9000 | 10000 | 13000 | 25000 | 45000 | 85000 | 205000 |
ASYNC花費時間(ms) | 5500 | 5500 | 5500 | 8500 | 20500 | 40500 | 80500 | 200500 |
效能改善% | 127.27% | 163.64% | 181.82% | 152.94% | 121.95% | 111.11% | 105.59% | 102.24% |
以執行時間來看,頻寬低於 80M 之後,改善的程度就固定下來了,隨著頻寬越來越低,WRITE 需要花費的時間越來越長,改善的幅度就越來越不明顯。同樣這些數據,換成改善的百分比,換成下一張圖:
改善幅度最好的地方,發生在 80Mbps, 這時正好是 Read() + Process()
的時間,正好跟 Write()
花費的時間一樣的地方。頻寬高於或低於這個地方,效果就開始打折扣了。通常改善幅度若低於 10%, 那就屬於 “無感” 的改善了。
簡單的下個結論,其實任何效能問題都很類似,能用 async 改善的效能問題,一定有這種模式存在: 整個程式執行過程中,有太多等待的狀況發生。不論是 IO 等待 CPU,或是 DISK IO 等待 NETWORK IO 等等,都屬此類。從外界能觀察到的狀況,就是幾個主要的硬體資源,如 Network, CPU, DISK, Memory 等等,都沒有明顯的負載過重,但是整體效能就是無法提升,大概就屬於這種模式了。找出流程能夠重新安排的地方後,剩下的就是如何善用這些技巧 (async),把它實作出來就結束了。
而 async / await, 處理這類問題,遠比 thread 來的有效率。就我看來,若需要大規模的平行處理,還是 thread 合適。但是像這次的案例,只是希望將片段的任務以非同步的模式進行,重點在精確的切割任務,同時要在特定的 timing 等待先前的任務完成,這時 async / await 會合適的多。
上篇花了一堆口水,說明各種 data layer 的設計方式,這回不噴口水了,直接來實作...。 開始前先說明一下我的期望,我先假設各位都用過 Entity Framework 或是這類的 solution, 我希望在 data context 這層,就能處理掉所有 隔離 tenancy data 的問題,也就是我可以用一般 application 的開發概念來開發 multi-tenancy 的 application。換另一個說法,我希望 在一個 storage 內,模擬出讓每個 tenancy 都有獨立的 storage 可運用的介面。 Web 的這部份也是一樣的概念,我希望在網址這層,邏輯上就讓每個 tenancy 有獨立的 URL (虛擬目錄)。有這種網址上自行客製的需求,當然是 非 MVC 不可,因此這次會在 MVC 的 Routing 上動手腳,除了 controller 及 action 之外,能夠再切出一層 client 出來,讓 application 也像 是能夠虛擬化一樣,每個用戶可擁有自己的 partition。 最後當然也希望 WEB 這邊的架構,能緊密的跟 DATA 這邊結合。讓開發人員就照一般 的方法就能快速開發出Multi-Tenancy Applicaiotn。寫這種系統,若規模不夠大就沒意思了,因此我初期就把目標鎖定 在 Windows Azure Storage + MVC4, 而應用程式就以常見的 "訂便當系統" 為案例。 訂便當系統,是個很適合開發成 Multi-Tenancy 模式的主題。原因有幾個:
越想越多,再想下去這個 POC 用的 prototype 就寫不完了,需求給各位讀者再去延伸,我這邊作 POC 就把需求收殮一下...。偷學一下 SCRUM 的 story 撰寫技巧,以下是我這次 POC 要實作的 stories:
好,看來一個設計良好的 DataContext, 可以省掉不少工夫。大部份的 coding logic, 都是在客戶的專區內運作的。我把整套系統稱作 "Hub”, 而每個客戶專區內的資料,就通稱為 "HubData”, 如會員資料,或是訂單等等。而其它非 HubData, 就是整個系統通用的資料。因此,我希望 Hub 用的 Data Context 能有這些功能:
OK,噴了三篇的口水,終於看到第一段程式碼了 Q_Q,HubDataContext 的 interface 看起來要像這樣:
使用它的 Code 要像這樣 (這段我就寫在 unit test 內…):
花了一點時間,總算把實作都寫出來,成功通過單元測試了。套句 Luddy Lee 前輩常說的話,寫雲端的程式測試跟佈署都比以前麻煩,因此做好完整的規劃跟測試就變的更重要了,單元測試是少不了的,請各位切記。過去吃了很多苦頭,更加證明 Luddy Lee 前輩講的話一點都沒錯....
最終的 HubDataContext 實作如下... 其實也沒幾行:
OK,第一步通過了,接下來我們開始來規劃一下 Data Schema …. Azure Table Storage 已經沒有所謂的 "schema” 這回事了,它完全就像 EF5 裡面的 code first 一樣,你的 Data Entity class 定義好,就可以跑了... 這些細節就不在這篇裡多做說明,各位有興趣可以參考其它的資料。既然都是 Code First 開發模式了,我就直接用 class diagram 來描述這些資料 (Entity) 之間的關係: