標籤:ken table 方便 clear 開始 ice 情境 otf 入行
2008年11月,我在部落格園開通了個人帳號,並在部落格園發表了自己的第一篇部落格。當然,我寫部落格也不是從2008年才開始的,在更早時候,也在CSDN和系統分析員協會(之後名為“希賽網”)個人空間發布過一些與編程和開發相關的文章。從入行到現在,我至始至終樂於與網友分享自己的所學所得,希望會有更多的同我一樣的業內朋友能夠在事業上取得成功,也算是為我們的軟體事業貢獻自己的一份力量吧,這也是我在部落格園建部落格時候的願景:專業、求是、解惑。因此,我在撰寫部落格文章的時候,都是以客觀嚴謹的態度來闡述技術知識,並儘可能地以更好的內容組織形式來提高文章的可讀性,同時儘可能地回答網友的提問。有很多部落格園的粉絲跟我提過意見,有的說我的部落格更新太慢,也有的說我有些系列文章有爛尾現象,對於粉絲們的提問,我只有一個回答,那就是業餘時間太有限,我沒有辦法憑靠自己一個人的力量,在有限的業餘時間裡,在保證文章品質的前提下,為社區提供越來越多的支援。在這一點上,我選擇的是寧缺毋濫:寧可發布周期變長,也不希望把沒有品質的文章分享出來。另一方面,我也發布了很多開源項目,有些項目屬於我自己一些個人小工具的代碼備份,也有一些項目,比如Apworks、Byteart Retail、WeText,甚至是我的新部落格daxnet.me的原始碼項目daxnet-blog,都在我的Github Repo裡。說實話我真的沒有時間把每個項目中的細節技術以部落格的方式一一介紹清楚,因此,一般我基本完成了自己比較滿意的開源項目時,我都會寫一篇部落格來介紹項目的內容和所使用的技術,同時引導讀者直接複製我的項目代碼進行參閱,或者直接folk(不用太擔心許可協議問題,除了Raspkate項目之外,其它絕大部分項目都是MIT或者Apache的許可協議)。總而言之,不管形式如何,我始終沒有放棄過最初的願景。
也是出於這樣的堅持,我希望能夠更好地組織我的部落格文章,甚至是其它的一些原創作品,以更為集中和高效的方式為讀者提供更好的學習交流體驗,一直以來我都想過搭建屬於自己的部落格服務,我也經過了很多嘗試。早在2012年,我使用Word Press在一個國外網站建立過部落格系統,可是後來因為國外服務供應商的原因,網站沒能繼續維持下去,之後我也經過好幾次的嘗試,包括使用BlogEngine.NET等開源項目,可是也都沒能做好。出於對技術的熱衷與追求,這一次,我終於下定決心,使用自己所學的知識,依託微軟的.NET平台,開發並部署了我自己的新部落格系統:【http://daxnet.me】。
網站功能
首先簡要介紹一下目前的網站功能吧。右圖就是本站的首頁效果,我做得很簡潔,沒有用太多花哨的圖片,也沒有用走馬燈。明眼人一看就知道這是基於ASP.NET MVC而開發的Web應用程式,使用了Bootstrap。不錯,基本答對!需要強調的是,這個部落格網站以及後端的RESTful服務,全部都是基於ASP.NET Core完成的,.NET Core運行時版本為1.1.0,運行在Docker容器中。哎,說著說著又到技術上了,功能還沒介紹完呢。說到功能,目前功能很簡單:首頁列出了我自己原創或者翻譯的所有文章,讀者可以註冊使用者帳號,註冊使用者可以發表評論,也可以在使用者管理頁面中更改自己的暱稱。好了,目前功能就這麼多,別看功能少,我可是前前後後陸陸續續花了2個月的時間,才做到目前這個樣子。當然,我會繼續更新這個網站,讓它的功能變得更加完善。
提到ASP.NET Core,有沒有吊起你的技術胃口呢?不用著急,接下來我就介紹一下整個網站中各部分的技術選型,看完後,或許你會知道為什麼我花了2個月的業餘時間,才整出來這麼個簡單的玩意兒。
網站技術介紹整體架構
整個網站所採用的所有基礎設施全部運行在微軟雲(Windows Azure)中,使用了部分託管資源,以及一些非託管的Azure VM。大致情況如下:
- 圖片儲存服務:由Azure Blob Storage Service託管
- 資料庫系統:由Azure SQL Database託管(未啟用Geo-Replication,因為沒錢)
- 郵件服務:由Azure SendGrid Account託管(Pricing Tier為F1,每月可以免費發送25000封郵件)
- 應用伺服器:基於Azure構建的Ubuntu 16.04.1 LTS虛擬機器,運行了兩個Docker容器:blog-web和blog-service,分別託管前端Web網站和後端RESTful服務。後端RESTful API服務沒有做任何認證和授權,Web網站通過內部子網訪問RESTful API服務,Docker容器運行在非託管環境中
- 持續整合系統:Jenkins,基於Azure構建的Windows Server 2012 R2一台(Master),和一台Ubuntu 16.04.1 LTS(Slave)。網站的前端和後端都在後者(Ubuntu)中完成編譯、打包以及Docker鏡像的發布,實現了一步到位的部署方式
- 程式碼程式庫:Github
有人會問:為什麼使用了非託管的Azure VM環境運行應用系統?我也考慮過這個問題,理論上講,雲端式的系統架構最好選用託管的PaaS服務,這樣不僅可以得到純天然的高可用性(包括災備,比如AWS的跨AZ部署,某些服務跨地區的可用性,以及負載平衡),而且還可以得到專業的支援人員。只有當存在老系統向雲遷移的需求,並需要迎合老系統的特定運行環境要求時,才考慮使用IaaS服務。雖然虛擬機器等這些資源是由Azure負責建立並啟動並執行,在這一層面Azure可以保證虛機的可用性,但虛機內部啟動並執行任何程式的狀態,以及所使用的資料,Azure等雲端服務是無從得知的,對這部分東西的監控也會變得很麻煩。出於安全考慮,通常雲端服務供應商是不會,也不應該獲得類似虛機內部的客戶程式的運行資料的,使用虛擬機器服務所產生的程式運行風險,客戶需要自己承擔。這也就是著名的責任共擔原則。
看起來用虛擬機器運行應用不是太靠譜嘛,然而我卻選擇這麼使用了。有幾個原因:
- 為何不使用Azure Web App?一方面Jenkins做自動化部署,直接把編譯好的應用推送到Azure Web App中好像不是太順手,要寫一些PowerShell的代碼,可是我的編譯系統是Linux,不過現在已經有Linux版的PowerShell了,而且Azure SDK Command Line Interface也有Linux版,所以這個理由有點牽強,更合理的解釋是:勞資不會!另一方面,我沒有在服務端做認證和授權,僅通過子網向外界提供服務,所以我希望我的Web App也運行在子網內部,然後向外暴露80連接埠供外界訪問。這樣一來,Azure Web App又如何部署到我自己的子網內?這是一個技術問題,我相信一定有解決方案,但是我也沒太多時間和精力去細究如何?,自己的第一反應也無非是將前後端全部部署在Azure Web App中,然後開啟後端的認證機制。但這樣做又要花一些額外的工夫。好吧,還是這個理由:勞資不會
- 為何不使用Azure Container Service?Azure Container Service會在你指定的Resource Group(資源群組)中建立一整套網路部署,包括好幾台虛擬機器、公網IP、兩個負載平衡器等等,我想你一定知道我為什麼沒有選擇Azure Container Service了,原因就是:勞資沒錢
理由夠充分吧?微軟Windows Azure提供的這些服務都很贊,我沒選不是說它們不好用,而是出於自己的實際情況考慮:
- 某些服務的學習成本
- 經濟成本
- 暫時沒必要做到99.99999%的高可用率
- 即使應用掛了,恢複的成本很小:資料完全不需要恢複,託管的SQL Database、Blob Storage會保證我的資料不丟失,應用程式恢複也很簡單:重新運行Docker容器就完事兒
OK,從整體架構上看,我的選擇即是如此而已,這樣的選擇當然不一定完全正確,但我覺得至少合適,僅供參考。下面附上本網站的整體架構圖。
作幾點註解:
- 三台VM位於同一個Virtual Network的subnet中,每台VM的虛擬網卡上都套有獨立的Network Security Group(NSG),在NSG上設定了Inbound/Outbound Endpoints,嚴格限制了連接埠訪問的IP地址。三台VM之間使用subnet IP地址訪問
- Windows Server 2012 VM宿主了Jenkins Master,以及SeqLog Service。它向公網暴露8080連接埠和5342連接埠,分別用於訪問Jenkins服務和Seq管理介面
- 第一台Ubuntu VM運行了Jenkins Slave,它不向公網暴露任何連接埠,僅向Jenkins Master機器暴露22連接埠,用於Jenkins Slave Agent的執行調度
- 第二台Ubuntu VM運行了部落格系統的兩個Docker容器:前端應用程式blog-web和後端RESTful API服務程式blog-service。web通過子網IP地址訪問service,VM僅向公網暴露80連接埠,後台service無法從公網訪問
- 兩個Docker容器所啟動並執行應用(blog-web和blog-service)都可以訪問託管的Azure SQL database、Azure Storage blob和SendGrid Account服務
- 整個部署的拓撲結構有可能不太合理,比如沒有做負載平衡,沒有使用託管的應用宿主服務(比如Azure Web App、Container Service等),沒有使用Scaleset。因為目前沒必要而且沒錢
接下來,回到代碼上,我向大家介紹一些架構的技術選型,以及幾個ASP.NET Core可用的開源庫項目。
前端
如今的前端技術日新月異,各種Javascript的架構和JSX的技術,使得前端開發變得更加方便高效,所獲得的使用者體驗也變得越來越好。例如Angular JS(包括1和2兩個版本)、React + Redux、Knockout.JS、Backbone等等。在實際項目中,我們也運用了這其中絕大部分技術,然而,在我的這個部落格系統中,我沒有使用單頁面應用的解決方案,而是繼續使用前端Razor+後端C#代碼的方式,對啦,這就是ASP.NET Core MVC!我沒有使用任何MVVM的架構,只是簡單地使用了Bootstrap和jQuery,對我來說,這樣選擇的原因有以下幾個:
- 相對而言對ASP.NET MVC比較熟悉,更容易儘快完成開發工作單位
- 本身網站邏輯不是太複雜,暫時沒有必要使用這些前端架構
- 打算體驗一下ASP.NET Core的新特性
當然,為了實現一些特定的功能,我還是選用了一些開原始碼和架構,現給大家大致介紹一下。
關於首頁的分頁實現
首頁實現了部落格文章的服務端分頁,每次僅向伺服器請求有限量的資料。分頁控制項是自己寫的一套演算法實現的,並套用了Bootstrap的pager樣式,實現了響應式使用者體驗。分頁控制項使用了ASP.NET Core MVC中新的Tag Helper技術,從演算法上根據每頁的大小和總部落格數量,對頁號進行分段處理,使得整個分頁功能有個很好的使用者體驗。
關於驗證碼產生
驗證碼的產生在經典的ASP.NET應用程式中能夠非常容易地實現。經典ASP.NET應用程式基底於Full .NET Framework,運行於Windows的IIS上,依賴於Windows的圖形庫,可以很方便地產生圖片。然而,ASP.NET Core應用程式則完全不同,為了實現跨平台,就無法使用System.Drawing命名空間下的類型(當然你可以指定你的ASP.NET Core應用程式使用net45,但是這樣無法跨平台)。在這裡我使用了CoreCompact.System.Drawing這個庫,可以通過nuget搜尋到 。它會依賴於Microsoft.Win32.Primitives庫,這個庫定義了一些與Drawing相關的資料結構,但是沒有提供任何圖形庫的實現。有興趣的讀者不妨一試。
關於回複編輯器
沒什麼好說的,使用了著名的CKEditor作為編輯器,當然,我選擇性地啟用/禁用了某些功能。
關於部落格文章中的代碼高亮
使用了著名的Alex Gorbatchev的SyntaxHighlighter,部落格園也是使用的這個庫,不過我用的可能不是最新版本。
關於回複中的時間資訊
在每篇部落格文章後面會顯示網友的回複內容。這些內容會顯示回複時間與目前時間的關係資訊,比如:
顯示這則回複內容是發表於25天前的。可別小看了這個部分的實現,我是採用了一個叫做Humanizer的庫。這個庫很有意思,它能提供一些非常實用的API,比如給它一個英文名詞,它可以返回複數形式;給它一個日期,它能返回一個更貼近人類自然語言的表述。它還有很多其它的有趣的功能,大家可以去瞭解一下。
關於部落格發布的MetaWeblog API
部落格系統支援使用Windows Live Writer發布部落格,它通過Shawn Wildermuth提供的WilderMinds.MetaWeblog實現了MetaWeblog API。通過Windows Live Writer可以直接將網站添加到帳號中:
基本上前端所使用的一些技術和第三方架構就如上所述。下面來看看背景一些技術選型。
後台資料庫與資料訪問組件
正如上所述,新部落格系統後台使用Azure SQL Database,也就是託管的SQL Server關係型資料庫。為什麼選擇SQL Server而不選擇MongoDB等目前流行的NoSQL方案?作為一個部落格網站,我沒有找到選擇NoSQL的理由,Azure上也有託管的MongoDB服務,僅管它是委託由Bitnami負責營運的。另一方面,雖然我選擇了Azure SQL Database,但我沒有使用任何第三方的資料訪問架構,沒有使用ORM,包括目前流行的Dapper。沒有選擇ORM的理由,一方面感覺ORM在這個情境裡還是太重,另一方面,截止我進行技術選型時,Entity Framework Core無法滿足我的需求,至少它無法從領域模型的角度去支援多對多的映射。那為何又沒有選擇Dapper呢?主要原因還是一樣:無法滿足我的需求。原生的Dapper類庫需要寫一些SQL指令碼,雖然輕量了,但失去了對代碼重構的支援,Dapper.Contrib增加了一些更友好的API,但仍然無法滿足自己的需求。
幾番思考,我決定自己寫一個小架構,既可以支援自己定義的簡單領域模型,又可以支援基於Lambda的文法、支援資料庫事務、支援非同步API、支援多種類型的關係型資料庫。這個小架構的代碼位於DaxnetBlog.Common.Storage命名空間下,使用了一些非常巧妙的技巧,比如,開發人員可以使用Lambda運算式來定義查詢條件,架構會通過ExpressionVisitor(訪問者模式)將Lambda運算式轉換成SQL語句。下面的代碼正是這個架構的使用代碼:
| 12345678910111213141516171819 |
var rowsAffected = await this.storage.ExecuteAsync(async (connection, transaction, cancellationToken) =>{ var account = (await this.accountStore.SelectAsync(connection, acct => acct.UserName == userName, transaction: transaction, cancellationToken: cancellationToken)).FirstOrDefault(); if (account == null) { throw new ServiceException(HttpStatusCode.NotFound, Reason.EntityNotFound, $"未能找到帳號名稱為{userName}的使用者帳號。"); } account.DateLastLogin = DateTime.UtcNow; return await this.accountStore.UpdateAsync(account, connection, acct => acct.UserName == userName, new Expression<Func<Account, object>>[] { acct => acct.DateLastLogin }, transaction, cancellationToken);}); |
這段代碼用於更新指定帳號名稱的使用者的登入時間,代碼中沒有穿插SQL語句,而是使用Lambda運算式進行表述。代碼中storage對象指代關係型資料庫的實體,而accountStore則表示對某種實體(在此處是帳號實體)的儲存,有點像領域驅動設計中的Repository的概念。這樣的設計是為了實現職責分離:accountStore不會依賴於storage(也就是關係型資料庫類型)的實現。
日誌
無論是前端還是後端,我都使用了Serilog作為日誌架構,並將日誌推送到Seq系統。具體做法我會在另外的部落格文章中詳細介紹,在此就不多介紹了。就是本部落格的日誌輸出,為了省錢,在Docker容器啟動時,通過環境變數將記錄層級設定為Warning。
API文檔
不多說,Swagger。具體實現方式我也會在另外的文章中介紹。
緩衝
暫時未使用緩衝,下一步會增加。
好了,整個部落格的架構以及前後端技術大概就介紹這麼多,如果要深入技術實踐的每一個細節,我想,估計幾個系列文章都講不完。還是如本文最開始的時候所述,部落格代碼開源,大家可以學習交流。今後我仍然會爭取多寫一些文章來介紹相關技術。
我還會繼續在部落格園發表部落格嗎?
當然會!部落格園一直是我與大家交流的主要場所,將來也是。可以理解,為了向大家提供更多高品質的“乾貨”,部落格園對博主們所發文章都會有一些限制,部落客題行文也會有一些約束。作為我本人來說,在部落格這種形式下,我或許應該可以以更多的方式來表現我的技術生涯,甚至是自己的一些對生活中事物的思考,這或許對他人的技術發展也會是一種啟發,在獲得大家的反饋和回複以後,我也能繼續提高自己。與這些相關的內容,我會發表在自己的部落格中,當然,我想,我自己的部落格仍然會以技術類文章為主吧。
目前這個新部落格顯示了我曾經在部落格園發表的部落格(當然只是為了充數,使得首頁不顯得那麼單調,所有圖片中還是保留部落格園的連結)。我打算給這個新部落格定下三個月的試運營階段,這個過程準備考察一下系統的健全狀態,並總結一下微軟Azure雲的使用心得,當然最重要的是衡量一下自己能否支付得起運營的這筆開銷。整個試運營階段我還會繼續往系統加入更多功能。
如果運營失敗,也請大家多多包涵,權當是我為社區多貢獻了一個開源項目吧。
總結
本文首先闡述了我對社區貢獻的一些實際情況,並由此引出我自己全手工打造的基於ASP.NET Core實現的部落格系統;接下來介紹了這個系統的整體架構和部署,以及前後端的一些技術選型;最後對大家可能提出的問題進行了簡要解答。馬上又要進入新的一年了,也快到了自己MVP Renew的時間,無論Renew是否成功(去年貢獻量感覺不是太高),我仍將繼續堅持為社區多做貢獻,真正做到“專業、求是、解惑”。
基於Microsoft Azure、ASP.NET Core和Docker的部落格系統