B/S 類項目改善的一些建議

要分享的議題

  1. 性能提高:在訪問量逐漸增大的同時,如何增大單臺服務器的 PV2 上限,增長 TPS3
  2. RESTful:相較於傳統的 SOAP1,RESTful 風格架構有哪些優勢?作法有哪些區別?
  3. 微服務:隨着企業愈來愈大,系統會愈來愈大,愈來愈難維護,如何在保證「穩」的同時,還保證有小企業的「靈活」?

簡要的介紹

性能提高

最經常使用的性能提升方式能夠經過使用服務器的集羣來解決,簡單粗暴的理解就是增長銀行櫃員的數量。可是,一味的只考慮從服務端提供性能,並非聰明的作法 —— 應該講求性價比。固然,核心必須是提升服務器的 TPS,即在最短的時間內給最多的客戶提供服務。服務器集羣能夠大幅提高總體的性能,可是咱們要討論的是如何提高單臺服務器的性能。javascript

  1. 服務器的壓力主要來源於三個方面:CPU、網絡和磁盤 IO。磁盤做爲最容易達到瓶頸的一方,必須想辦法減小 IO 操做。數據庫做爲數據持久性存儲、磁盤開銷的大戶,這裏主要就是要減小或合併數據庫操做。
  2. 系統的流暢性取決於服務端和客戶端的良好配合。網站類的項目,充分利用瀏覽器資源,不只能下降服務器壓力,還能提供更好地客戶體驗。現代化的瀏覽器,通常都符合 RFC26164 規範。其中很重要的報頭有:ETagLast-Modified 報頭 —— 瀏覽器的緩存設置開關,能夠最大限度的利用客戶端資源。

RESTful 架構風格

比如面向過程編程和麪向對象編程,這二者並無明確的界限。在適當的地方用適當風格的架構,重點是物盡其用。但 RESTful 做爲新興的風格,必然有其優點:html

  1. 在 RESTful 架構中,關注點在於資源。每一個都有一個地址,資源自己就是方法調用的目標。方法列表對全部資源都是同樣的。這些方法都是標準方法,包括 HTTP GET、POST、PUT、DELETE,還可能包括 PATCH、HEADER 和 OPTIONS。其指導思想是遠端提供了一系列資源,客戶端須要下載、展示、編輯和提交更改,重點放在本地。
  2. 在 RPC 架構中,關注點在於方法。在客戶端看來,就是在客戶端組合條件,而後在服務器中執行,最終再反饋給客戶端。其指導思想是隱藏實現細節,或者關聯其它 RPC 服務運算,重點放在了服務器。

微服務和單體式應用

現代化的單體式應用,一般採用模塊化的方式,圍繞核心模塊並行開發。最終他們須要聯合測試,部署成一個單體式的應用:C# 會部署成 IIS 的一個網站,Java 會打包成 War 格式部署 Tomcat 上。java

隨着時間的推移,單體式在應對愈來愈多的新需求後,會變得愈來愈大。更不幸的是,由於公司資源和需求的不對等,許多倉促應對的代碼會添加到應用中。這些代碼在短時間內不會出現問題,可是修正 Bug 和正常的新功能添加會變得愈來愈困難,由於一般會涉及到多個模塊,牽一髮而動全身。此時就是單體式應用的瓶頸期,會考慮拆分紅多個子系統。固然,這將會再維持一段時間,直到再出現類似的問題。ios

許多公司,好比 Amazon、eBay 和 NetFlix,經過採用微處理結構模式解決了上述問題。其思路不是開發一個巨大的單體式的應用,而是將應用分解爲小的、互相鏈接的微服務。web

一個微服務通常完成某個特定的功能,好比下單管理、客戶管理等等。每個微服務都有本身的業務邏輯和適配器。一些微服務還會發布 Api 給其它微服務和應用客戶端使用。其它微服務完成一個 Web UI。正則表達式

性能提高

  1. 數據庫靜態化:數據庫不包含運算邏輯,全部運算邏輯在程序內完成。
  2. 減小外部 IO:使用數據緩存、合併數據庫操做、讀寫分離。
  3. 異步化:對於非必須的方法,異步執行使其不影響當前邏輯。
  4. 子系統拆分:拆分長時間運行的邏輯爲 Windows 服務或 Job 。

一個栗子:電子商務系統,下單操做起始涉及到了對多個模塊的調用。可是用戶下單的時候,並不關心這些,只要獲得一個下單成功的結果就能夠了。咱們能夠分析一下:系統首先要對用戶提交的信息有效性校驗,再就是業務數據準確性校驗,最後提交到數據庫。一個成功的電商系統,前二者必須能在很短的時間內完成,而且在秒殺特賣這種場景時不會形成數據庫的崩潰。數據庫

基於以上兩點,咱們分析下如何優化秒殺特賣這種場景下的操做流程。編程

  1. 服務端對信息有效性的校驗,操做頻率最密集、速度要最快,因此不該該涉及除內存運算以外的操做,好比:Redis 和數據庫讀寫、TCP/HTTP 遠程調用等。
  2. 業務性數據校驗,關聯模塊不少、速度要求較快,因此不該涉及慢速的 IO 操做,好比:數據庫讀寫、HTTP 遠程調用等。
  3. 寫數據庫頻繁,在較短的時間內給數據庫形成很大的持續壓力、速度要求很快,因此這裏能夠採用當即反饋,稍後寫入的方式執行。

數據庫靜態化

數據庫的操做都是有鎖的:Select 語句發佈共享鎖5,Insert、Update 和 Delete 發佈排它鎖6。因此說在操做同一張表的前提下,數據庫操做都是串行7的。api

基於以上考慮,讓數據庫只作存儲容器,不負責運算纔是正途。正是由於數據庫的操做是串行的,在大併發量寫入時,任何一點的提高都是要爭取的,因此這裏要把運算的任務提到程序中執行瀏覽器

  1. 存儲過程由於把程序邏輯放在數據庫,通常來講確定包含運算任務,考慮通常開發的水平不能保證先用臨時表存儲預先計算好的數據(能作到也太繁瑣了,很容易出現異常),最後再統一執行,因此首先要摒棄包含數據庫寫入類的存儲過程
  2. 程序內的運算,若是是在開啓事務後仍然存在,也要算入數據庫的運算任務。由於數據庫事務開啓後,獨佔的串行已經開始了,程序的運算時間不只佔用了程序的運算時間,還佔用了數據庫事務的開啓時長,在本質上並無減小數據庫事務的開啓時長。嚴格來算的話,這種作法甚至還不如上一條的作法優化。

因此,真正的數據庫靜態化是:首先在程序內運算,產生數據庫要執行的 SQL 寫入語句和參數,持續運算直到產生全部的數據庫寫入命令;再開啓數據庫事務,按照先進先出原則順序連續執行數據庫寫入命令(此段時間內不能包含其它非數據庫運算)。只有這樣,才能保證命令的執行都是靜態化的寫入,而且鎖定數據庫的時間最短,保證最大化的下降數據庫壓力。

另一個,正是由於數據庫的操做是串行的,因此在執行數據庫寫入的狀況下,是不能讀取的,要避免出現髒讀,數據庫的讀寫分離就頗有必要了。創建從庫,由主庫負責寫入,從庫負責讀取,將數據庫的壓力均分到多臺上。

數據庫的讀寫分離要注意:剛寫入數據庫的數據,同步到從庫須要 2 到 3 秒的時間,須要在業務上更改流程,以便於在用戶檢索時數據已同步。

減小外部 IO

因爲磁盤的限制,其讀寫速度和內存不成比例,因此這裏是第二個可能出現瓶頸的地方。能夠考慮將配置信息預先讀取到緩存的方式解決。

由於數據庫是依託於硬盤而存在的,因此數據庫的讀寫相對於有效性驗證和業務驗證來講,是時間消耗大戶。在單純的考慮數據庫寫入的狀況下,能夠從系統內剝離訂單的數據庫寫入業務。一來能夠省掉無心義等待數據庫寫入的時間;二來能夠減小 CPU 時間片的佔用,將時間

另外一種是數據庫的讀寫操做,在一個數據庫事務內,是不能有第二個數據庫執行相同的操做的。考慮到數據靜態化中的介紹,數據庫事務開啓後,程序內的運算其實也是數據庫的運算時間的。此時能夠考慮推遲數據庫事務的開啓時間:首先在程序內運算產生要執行的數據庫命令,再開啓數據庫事務,在連續的時間內執行批量執行數據庫事務。即合併數據庫操做到一次數據庫執行中。

異步化

在開發的過程當中,不可避免的要和其關聯的模塊交互,而這些交互並不會對當前的業務邏輯產生影響。這種操做就應該改爲異步的方式。在數據庫交互的過程當中,若是用戶不須要等待數據庫的返回值,還能夠將數據庫執行異步化,在最短的時間內反饋執行結果。

一個栗子:用戶下單的過程當中,提交訂單的操做,其實並不關心提交成功失敗,只是在後續跳轉到訂單詳情的時候纔會看到訂單詳情。這個流程就能夠將數據庫執行異步化,在服務器接收到用戶的提交請求時,能夠在校驗數據後,直接反饋提交成功的響應給用戶。接下來,經過異步隊列的方式,保存到數據庫。客戶端在接收到提交成功的反饋後,提示用戶提交成功,可是不給出訂單的任何信息。用戶只有主動點擊了查看訂單列表,纔會執行數據庫查詢。這個時間差足夠系統處理訂單的真正提交操做了。

這樣作最直接的好處是提升了網站的響應速度,優化了用戶體驗,在提高服務器 TPS 的同時,尚未提高數據庫的壓力。在秒殺特賣時,可以最大限度的避免超賣的狀況。

子系統拆分

繼續上面的例子,數據庫的提交訂單操做,和網站並無多少的關係。此時就能夠考慮到將這一部分拆分出來,作成一個 Windows 服務,二者經過消息隊列的方式通信。隊列的串行讀取正好符合了數據庫的串行執行,在高峯時段也沒有超過數據庫的極限,形成宕機的狀況。在超過服務極限的狀況下,處理慢比不能處理老是要好的。

從操做系統上來講,系統調度針對每一個進程都是平等的。此處將數據庫執行操做從網站拆分出來,減小了數據庫操做對 CPU 時間片的佔用,側面提高了網站的服務能力。

RESTful 架構風格與 SOAP 架構風格

  1. 屬性路由:使用漸進式的 URI 替代傳統平板式的方法名稱 URI。
  2. 客戶端緩存報頭:使用 ETagLast-Modified 報頭減輕服務器壓力。
  3. CRUD:使用 GET、POST、PUT 和 DELETE 方法區分數據庫 CRUD 操做。

屬性路由

第一個 WebApi 版本使用的是基於公約的路由。在該類型的路由中, 你能夠定義一個或多個被參數化字符串的模版。當這個框架接收到一個請求時,它匹配一個 URI 到路由模版。

基於公約的路由的一個優點就是:這個模版被定義在一個單獨的地方,路由規則一致的被應用於全部的控制器。不幸的是,基於公約的路由是很難支持確切的URI模式,而這個確切的 URI 模式在 RESTful Api 中是很廣泛的。好比,資源常常包含子資源:客戶下了訂單,電影有演員,書有做者等等,它是很天然的建立這些 URI 來反應這些關係:

/customers/3/orders

這種類型的 URI 在基於公約的路由下是比較難實現的。儘管它能作到,可是若是你有許多控制器或者不少資源類型時,不能很好的被擴展。但對於屬性路由,它是很容易的爲這個 URI 定義一個路由,你能夠簡單的添加一個屬性到控制器的動做上:

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomerId(int customerId) { ... }

方便的 Api 版本控制

有時候咱們須要開發一個功能的新版本,可是並不想對現有的功能產生影響,好比:api/v1/productsapi/v2/products 能夠被路由到不一樣的控制器。在開發階段就作出較好了區分,而且當新的版本正式商用後,也能夠方便的對 V1 版本的控制器過時或停用。

重載 URI 片斷

在下面的例子中,12306 表示一個特定的車票,而 notravelled 表示未出行的車票集合。

/tickets/12306
/tickets/notravelled

經過天然語義,人們能夠很容易的理解這些 URI 的含義,可是基於公約的方式並不能很方便的解決這個問題。

路由約束

屬性路由添加了公約路由時代所沒有的約束特性,可讓你在路由模版中限制參數被匹配。常規的語法是 {parameter:constraint},例如:

[Route("users/{id:int}"]
public User GetUserById(int id) { ... }

[Route("users/{name}"]
public User GetUserByName(string name) { ... }

若是 URI 的 id 片斷是一個 int 類型的,那麼第一個路由將會被選擇,不然第二個路由將會被選擇。屬性路由約定特殊規則的路由優先匹配,最後才匹配沒有任何約束的路由。注意不要出現兩種可能的匹配,不然會出現多匹配的問題,好比:

[Route("{id:int}")]
public string Get(int id)

[Route("{id:decimal}")]
public string Get(decimal id)

這裏須要注意的是,WebApi 框架有一個 Bug,不支持小數點,好比:/values/v1/8.3 將不會被解析成 decimal 類型。

下面是被支持的約束列表:

約束 描述 用法演示
bool 類型匹配(Boolean 類型)
datetime 類型匹配(DateTime 類型) {x:datetime8}
decimal 類型匹配(Decimal 類型) {x:decimal9}
double 類型匹配(64 位浮點數) {x:double9}
float 類型匹配(32 位浮點數)
guid 類型匹配(Guid)
int 類型匹配(32 位整數)
long 類型匹配(64 位整數)
alpha 字符組成(必須由拉丁字母組成)
regex 字符組成(必須與指定的正則表達式匹配)
max 值範圍(小於或等於指定的最大值)
min 值範圍(大於或等於指定的最小值)
range 值範圍(在指定的最小值和最大值之間)
maxlength 字符串最大長度(小於或等於指定的長度)
minlength 字符串最小長度(大於或等於指定的長度)
length 字符串長度(等於指定的長度或者長度在指定的範圍內)

客戶端緩存報頭

基礎知識

什麼是 Last-Modified

在瀏覽器第一次請求某一個 URL 時,服務器端的返回狀態會是 200,內容是你請求的資源,同時有一個 Last-Modified 的屬性標記此文件在服務期端最後被修改的時間,格式相似這樣:

Last-Modified: Fri, 12 May 2006 18:53:33 GMT

客戶端第二次請求此 URL 時,根據 HTTP 協議的規定,瀏覽器會向服務器傳送 If-Modified-Since 報頭,詢問該時間以後文件是否有被修改過:

If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT

若是服務器端的資源沒有變化,則自動返回 HTTP 304(Not Modified)狀態碼,內容爲空,這樣就節省了傳輸數據量。當服務器端代碼發生改變或者重啓服務器時,則從新發出資源,返回和第一次請求時相似。從而保證不向客戶端重複發出資源,也保證當服務器有變化時,客戶端可以獲得最新的資源。

什麼是 ETag

HTTP 協議規格說明定義 ETag被請求變量的實體值;另外一種說法是,ETag 是一個能夠與 Web 資源關聯的記號(Token):典型的 Web 資源能夠一個 HTML 頁,但也多是 JSON 或 XML 文檔。服務器單獨負責判斷記號是什麼及其含義,並在 HTTP 響應頭中將其傳送到客戶端,如下是服務器端返回的格式:

ETag: W/"9e10cdada3f741f6b0802ee31179837d"

客戶端的查詢更新格式是這樣的:

If-None-Match: W/"9e10cdada3f741f6b0802ee31179837d"

若是 ETag 沒改變,則返回狀態碼 304 內容不返回,這也和 Last-Modified 同樣。本人測試 ETag 主要在斷點下載時比較有用。

Last-ModifiedETag 如何幫助提升性能?

聰明的開發者會把 Last-ModifiedETag 跟請求的 HTTP 報頭一塊兒使用,這樣可利用客戶端(例如瀏覽器)的緩存。由於服務器首先產生 Last-Modified/ETag 標記,服務器可在稍後使用它來判斷頁面是否已經被修改。本質上,客戶端經過將該記號傳回服務器要求服務器驗證其(客戶端)緩存。過程以下:

  1. 客戶端請求一個頁面(A)。
  2. 服務器返回頁面A,並在給A加上一個 Last-Modified/ETag
  3. 客戶端展示該頁面,並將頁面連同 Last-Modified/ETag 一塊兒緩存。
  4. 客戶再次請求頁面A,並將上次請求時服務器返回的 Last-Modified/ETag 一塊兒傳遞給服務器。
  5. 服務器檢查該 Last-ModifiedETag,並判斷出該頁面自上次客戶端請求以後還未被修改,直接返回狀態碼 304 和一個空的響應體。

這裏的客戶端通常指瀏覽器,經過編程方式使用的客戶端,通常不會處理這兩個 HTTP 請求頭。

微服務

目前不作深刻討論。


  1. SOAP(Simple Object Access Protocol)簡單對象訪問協議,是交換數據的一種協議規範,是一種輕量的、簡單的、基於XML(標準通用標記語言下的一個子集)的協議,它被設計成在WEB上交換結構化的和固化的信息。 

  2. PV:(Page View)即頁面瀏覽量,一般是衡量一個網絡新聞頻道或網站甚至一條網絡新聞的主要指標。網頁瀏覽數是評價網站流量最經常使用的指標之一,簡稱爲 PV。監測網站 PV 的變化趨勢和分析其變化緣由是不少站長按期要作的工做。Page Views 中的 Page 通常是指普通的 HTML 網頁,也包含 PHP、JSP 等動態產生的 HTML 內容。來自瀏覽器的一次 HTML 內容請求會被看做一個 PV,逐漸累計成爲 PV 總數。 

  3. TPS:(Transaction Per Second)每秒鐘系統可以處理的交易或事務的數量。它是衡量系統處理能力的重要指標。TPS 是 LoadRunner 中重要的性能參數指標。 

  4. RFC2616:目前該規範已有部分更新。 

  5. 共享鎖:相似於讀寫鎖中的讀鎖。能夠多個一塊兒讀,可是排斥寫鎖。只有讀鎖釋放後,才能進入寫鎖。 

  6. 排它鎖:相似於讀寫鎖中的寫鎖。只能一個寫,其他的操做都必須等待,直到當前寫鎖釋放後。 

  7. 串行:同一時間只容許一個線程操做,其他線程只能等待完成後,才能繼續執行操做。 

  8. datetime 類型的約束,若是採用 / 作分隔符,必須放在最後一個,而且採用 * 前導:{*x:datetime}。目前,也只有這種寫法能夠跨多個 URI 段。 

  9. decimaldouble 兩種數字類型,若是包含小數點將不能被正常解析,目前能夠算 WebApi 框架的一個 Bug 。 

相關文章
相關標籤/搜索