歡迎閱讀daxnet的新博客:一個基於Microsoft Azure、ASP.NET Core和Docker的博客系統


2008年11月,我在博客園開通了個人帳號,並在博客園發表了自己的第一篇博客。當然,我寫博客也不是從2008年才開始的,在更早時候,也在CSDN和系統分析員協會(之后名為“希賽網”)個人空間發布過一些與編程和開發相關的文章。從入行到現在,我至始至終樂於與網友分享自己的所學所得,希望會有更多的同我一樣的業內朋友能夠在事業上取得成功,也算是為我們的軟件事業貢獻自己的一份力量吧,這也是我在博客園建博客時候的願景:專業、求是、解惑。因此,我在撰寫博客文章的時候,都是以客觀嚴謹的態度來闡述技術知識,並盡可能地以更好的內容組織形式來提高文章的可讀性,同時盡可能地回答網友的提問。有很多博客園的粉絲跟我提過意見,有的說我的博客更新太慢,也有的說我有些系列文章有爛尾現象,對於粉絲們的提問,我只有一個回答,那就是業余時間太有限,我沒有辦法憑靠自己一個人的力量,在有限的業余時間里,在保證文章品質的前提下,為社區提供越來越多的支持。在這一點上,我選擇的是寧缺毋濫:寧可發布周期變長,也不希望把沒有質量的文章分享出來。另一方面,我也發布了很多開源項目,有些項目屬於我自己一些個人小工具的代碼備份,也有一些項目,比如ApworksByteart RetailWeText,甚至是我的新博客daxnet.me的源代碼項目daxnet-blog,都在我的Github Repo里。說實話我真的沒有時間把每個項目中的細節技術以博客的方式一一介紹清楚,因此,一般我基本完成了自己比較滿意的開源項目時,我都會寫一篇博客來介紹項目的內容和所使用的技術,同時引導讀者直接克隆我的項目代碼進行參閱,或者直接folk(不用太擔心許可協議問題,除了Raspkate項目之外,其它絕大部分項目都是MIT或者Apache的許可協議)。總而言之,不管形式如何,我始終沒有放棄過最初的願景。

也是出於這樣的堅持,我希望能夠更好地組織我的博客文章,甚至是其它的一些原創作品,以更為集中和高效的方式為讀者提供更好的學習交流體驗,一直以來我都想過搭建屬於自己的博客服務,我也經過了很多嘗試。早在2012年,我使用Word Press在一個國外站點建立過博客系統,可是后來因為國外服務供應商的原因,網站沒能繼續維持下去,之后我也經過好幾次的嘗試,包括使用BlogEngine.NET等開源項目,可是也都沒能做好。出於對技術的熱衷與追求,這一次,我終於下定決心,使用自己所學的知識,依托微軟的.NET平台,開發並部署了我自己的新博客系統:【http://daxnet.me】。

站點功能

首先簡要介紹一下目前的站點功能吧。右圖就是本站的主頁效果,我做得很簡潔,沒有用太多花哨的圖片,也沒有用走馬燈。明眼人一看就知道這是基於ASP.NET MVC而開發的Web應用程序,使用了Bootstrap。不錯,基本答對!需要強調的是,這個博客站點image以及后端的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提供的這些服務都很贊,我沒選不是說它們不好用,而是出於自己的實際情況考慮:

  1. 某些服務的學習成本
  2. 經濟成本
  3. 暫時沒必要做到99.99999%的高可用率
  4. 即使應用掛了,恢復的成本很小:數據完全不需要恢復,托管的SQL Database、Blob Storage會保證我的數據不丟失,應用程序恢復也很簡單:重新運行Docker容器就完事兒

OK,從整體架構上看,我的選擇即是如此而已,這樣的選擇當然不一定完全正確,但我覺得至少合適,僅供參考。下面附上本站點的整體架構圖。

image

作幾點注解:

  1. 三台VM位於同一個Virtual Network的subnet中,每台VM的虛擬網卡上都套有獨立的Network Security Group(NSG),在NSG上設置了Inbound/Outbound Endpoints,嚴格限制了端口訪問的IP地址。三台VM之間使用subnet IP地址訪問
  2. Windows Server 2012 VM宿主了Jenkins Master,以及Seq日志服務。它向公網暴露8080端口和5342端口,分別用於訪問Jenkins服務和Seq管理界面
  3. 第一台Ubuntu VM運行了Jenkins Slave,它不向公網暴露任何端口,僅向Jenkins Master機器暴露22端口,用於Jenkins Slave Agent的執行調度
  4. 第二台Ubuntu VM運行了博客系統的兩個Docker容器:前端應用程序blog-web和后端RESTful API服務程序blog-service。web通過子網IP地址訪問service,VM僅向公網暴露80端口,后台service無法從公網訪問
  5. 兩個Docker容器所運行的應用(blog-web和blog-service)都可以訪問托管的Azure SQL database、Azure Storage blob和SendGrid Account服務
  6. 整個部署的拓撲結構有可能不太合理,比如沒有做負載均衡,沒有使用托管的應用宿主服務(比如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,對我來說,這樣選擇的原因有以下幾個:

  1. 相對而言對ASP.NET MVC比較熟悉,更容易盡快完成開發任務
  2. 本身站點邏輯不是太復雜,暫時沒有必要使用這些前端框架
  3. 打算體驗一下ASP.NET Core的新特性

當然,為了實現一些特定的功能,我還是選用了一些開源代碼和框架,現給大家大致介紹一下。

關於首頁的分頁實現

首頁實現了博客文章的服務端分頁,每次僅向服務器請求有限量的數據。分頁控件是自己寫的一套算法實現的,並套用了Bootstrap的pager樣式,實現了響應式用戶體驗。分頁控件使用了ASP.NET Core MVC中新的Tag Helper技術,從算法上根據每頁的大小和總博客數量,對頁號進行分段處理,使得整個分頁功能有個很好的用戶體驗。

image

關於驗證碼生成

驗證碼的生成在經典的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,博客園也是使用的這個庫,不過我用的可能不是最新版本。

關於回復中的時間信息

在每篇博客文章后面會顯示網友的回復內容。這些內容會顯示回復時間與當前時間的關系信息,比如:

image

上圖顯示這則回復內容是發表於25天前的。可別小看了這個部分的實現,我是采用了一個叫做Humanizer的庫。這個庫很有意思,它能提供一些非常實用的API,比如給它一個英文名詞,它可以返回復數形式;給它一個日期,它能返回一個更貼近人類自然語言的表述。它還有很多其它的有趣的功能,大家可以去了解一下。

關於博客發布的MetaWeblog API

博客系統支持使用Windows Live Writer發布博客,它通過Shawn Wildermuth提供的WilderMinds.MetaWeblog實現了MetaWeblog API。通過Windows Live Writer可以直接將站點添加到帳號中:

image

基本上前端所使用的一些技術和第三方框架就如上所述。下面來看看后台的一些技術選型。

后台

數據庫與數據訪問組件

正如上所述,新博客系統后台使用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語句。下面的代碼正是這個框架的使用代碼:

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。

image

API文檔

不多說,Swagger。具體實現方式我也會在另外的文章中介紹。

image

緩存

暫時未使用緩存,下一步會增加。

好了,整個博客的架構以及前后端技術大概就介紹這么多,如果要深入技術實踐的每一個細節,我想,估計幾個系列文章都講不完。還是如本文最開始的時候所述,博客代碼開源,大家可以學習交流。今后我仍然會爭取多寫一些文章來介紹相關技術。

我還會繼續在博客園發表博客嗎?

當然會!博客園一直是我與大家交流的主要場所,將來也是。可以理解,為了向大家提供更多高質量的“干貨”,博客園對博主們所發文章都會有一些限制,博客主題行文也會有一些約束。作為我本人來說,在博客這種形式下,我或許應該可以以更多的方式來表現我的技術生涯,甚至是自己的一些對生活中事物的思考,這或許對他人的技術發展也會是一種啟發,在獲得大家的反饋和回復以后,我也能繼續提高自己。與這些相關的內容,我會發表在自己的博客中,當然,我想,我自己的博客仍然會以技術類文章為主吧。

目前這個新博客顯示了我曾經在博客園發表的博客(當然只是為了充數,使得主頁不顯得那么單調,所有圖片中還是保留博客園的鏈接)。我打算給這個新博客定下三個月的試運營階段,這個過程准備考察一下系統的運行狀況,並總結一下微軟Azure雲的使用心得,當然最重要的是衡量一下自己能否支付得起運營的這筆開銷。整個試運營階段我還會繼續往系統加入更多功能。

如果運營失敗,也請大家多多包涵,權當是我為社區多貢獻了一個開源項目吧。

總結

本文首先闡述了我對社區貢獻的一些實際情況,並由此引出我自己全手工打造的基於ASP.NET Core實現的博客系統;接下來介紹了這個系統的整體架構和部署,以及前后端的一些技術選型;最后對大家可能提出的問題進行了簡要解答。馬上又要進入新的一年了,也快到了自己MVP Renew的時間,無論Renew是否成功(去年貢獻量感覺不是太高),我仍將繼續堅持為社區多做貢獻,真正做到“專業、求是、解惑”。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM