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


要分享的議題

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

簡要的介紹

性能提升

最常用的性能提高方式可以通過使用服務器的集群來解決,簡單粗暴的理解就是增加銀行櫃員的數量。但是,一味的只考慮從服務端提供性能,並不是聰明的做法 —— 應該講求性價比。當然,核心必須是提高服務器的 TPS,即在最短的時間內給最多的客戶提供服務。服務器集群可以大幅提升整體的性能,但是我們要討論的是如何提升單台服務器的性能。

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

RESTful 架構風格

好比面向過程編程和面向對象編程,這兩者並沒有明確的界限。在適當的地方用適當風格的架構,重點是物盡其用。但 RESTful 作為新興的風格,必然有其優勢:

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

微服務和單體式應用

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

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

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

一個微服務一般完成某個特定的功能,比如下單管理、客戶管理等等。每一個微服務都有自己的業務邏輯和適配器。一些微服務還會發布 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的。

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

  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 。 


免責聲明!

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



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