要分享的議題
- 性能提高:在訪問量逐漸增大的同時,如何增大單臺服務器的 PV2 上限,增長 TPS3 ?
- RESTful:相較於傳統的 SOAP1,RESTful 風格架構有哪些優勢?作法有哪些區別?
- 微服務:隨着企業愈來愈大,系統會愈來愈大,愈來愈難維護,如何在保證「穩」的同時,還保證有小企業的「靈活」?
簡要的介紹
性能提高
最經常使用的性能提升方式能夠經過使用服務器的集羣來解決,簡單粗暴的理解就是增長銀行櫃員的數量。可是,一味的只考慮從服務端提供性能,並非聰明的作法 —— 應該講求性價比。固然,核心必須是提升服務器的 TPS,即在最短的時間內給最多的客戶提供服務。服務器集羣能夠大幅提高總體的性能,可是咱們要討論的是如何提高單臺服務器的性能。javascript
- 服務器的壓力主要來源於三個方面:CPU、網絡和磁盤 IO。磁盤做爲最容易達到瓶頸的一方,必須想辦法減小 IO 操做。數據庫做爲數據持久性存儲、磁盤開銷的大戶,這裏主要就是要減小或合併數據庫操做。
- 系統的流暢性取決於服務端和客戶端的良好配合。網站類的項目,充分利用瀏覽器資源,不只能下降服務器壓力,還能提供更好地客戶體驗。現代化的瀏覽器,通常都符合 RFC26164 規範。其中很重要的報頭有:ETag 和 Last-Modified 報頭 —— 瀏覽器的緩存設置開關,能夠最大限度的利用客戶端資源。
RESTful 架構風格
比如面向過程編程和麪向對象編程,這二者並無明確的界限。在適當的地方用適當風格的架構,重點是物盡其用。但 RESTful 做爲新興的風格,必然有其優點:html
- 在 RESTful 架構中,關注點在於資源。每一個都有一個地址,資源自己就是方法調用的目標。方法列表對全部資源都是同樣的。這些方法都是標準方法,包括 HTTP GET、POST、PUT、DELETE,還可能包括 PATCH、HEADER 和 OPTIONS。其指導思想是遠端提供了一系列資源,客戶端須要下載、展示、編輯和提交更改,重點放在本地。
- 在 RPC 架構中,關注點在於方法。在客戶端看來,就是在客戶端組合條件,而後在服務器中執行,最終再反饋給客戶端。其指導思想是隱藏實現細節,或者關聯其它 RPC 服務運算,重點放在了服務器。
微服務和單體式應用
現代化的單體式應用,一般採用模塊化的方式,圍繞核心模塊並行開發。最終他們須要聯合測試,部署成一個單體式的應用:C# 會部署成 IIS 的一個網站,Java 會打包成 War 格式部署 Tomcat 上。java
隨着時間的推移,單體式在應對愈來愈多的新需求後,會變得愈來愈大。更不幸的是,由於公司資源和需求的不對等,許多倉促應對的代碼會添加到應用中。這些代碼在短時間內不會出現問題,可是修正 Bug 和正常的新功能添加會變得愈來愈困難,由於一般會涉及到多個模塊,牽一髮而動全身。此時就是單體式應用的瓶頸期,會考慮拆分紅多個子系統。固然,這將會再維持一段時間,直到再出現類似的問題。ios
許多公司,好比 Amazon、eBay 和 NetFlix,經過採用微處理結構模式解決了上述問題。其思路不是開發一個巨大的單體式的應用,而是將應用分解爲小的、互相鏈接的微服務。web
一個微服務通常完成某個特定的功能,好比下單管理、客戶管理等等。每個微服務都有本身的業務邏輯和適配器。一些微服務還會發布 Api 給其它微服務和應用客戶端使用。其它微服務完成一個 Web UI。正則表達式
性能提高
- 數據庫靜態化:數據庫不包含運算邏輯,全部運算邏輯在程序內完成。
- 減小外部 IO:使用數據緩存、合併數據庫操做、讀寫分離。
- 異步化:對於非必須的方法,異步執行使其不影響當前邏輯。
- 子系統拆分:拆分長時間運行的邏輯爲 Windows 服務或 Job 。
一個栗子:電子商務系統,下單操做起始涉及到了對多個模塊的調用。可是用戶下單的時候,並不關心這些,只要獲得一個下單成功的結果就能夠了。咱們能夠分析一下:系統首先要對用戶提交的信息有效性校驗,再就是業務數據準確性校驗,最後提交到數據庫。一個成功的電商系統,前二者必須能在很短的時間內完成,而且在秒殺特賣這種場景時不會形成數據庫的崩潰。數據庫
基於以上兩點,咱們分析下如何優化秒殺特賣這種場景下的操做流程。編程
- 服務端對信息有效性的校驗,操做頻率最密集、速度要最快,因此不該該涉及除內存運算以外的操做,好比:Redis 和數據庫讀寫、TCP/HTTP 遠程調用等。
- 業務性數據校驗,關聯模塊不少、速度要求較快,因此不該涉及慢速的 IO 操做,好比:數據庫讀寫、HTTP 遠程調用等。
- 寫數據庫頻繁,在較短的時間內給數據庫形成很大的持續壓力、速度要求很快,因此這裏能夠採用當即反饋,稍後寫入的方式執行。
數據庫靜態化
數據庫的操做都是有鎖的:Select 語句發佈共享鎖5,Insert、Update 和 Delete 發佈排它鎖6。因此說在操做同一張表的前提下,數據庫操做都是串行7的。api
基於以上考慮,讓數據庫只作存儲容器,不負責運算纔是正途。正是由於數據庫的操做是串行的,在大併發量寫入時,任何一點的提高都是要爭取的,因此這裏要把運算的任務提到程序中執行。瀏覽器
- 存儲過程由於把程序邏輯放在數據庫,通常來講確定包含運算任務,考慮通常開發的水平不能保證先用臨時表存儲預先計算好的數據(能作到也太繁瑣了,很容易出現異常),最後再統一執行,因此首先要摒棄包含數據庫寫入類的存儲過程。
- 程序內的運算,若是是在開啓事務後仍然存在,也要算入數據庫的運算任務。由於數據庫事務開啓後,獨佔的串行已經開始了,程序的運算時間不只佔用了程序的運算時間,還佔用了數據庫事務的開啓時長,在本質上並無減小數據庫事務的開啓時長。嚴格來算的話,這種作法甚至還不如上一條的作法優化。
因此,真正的數據庫靜態化是:首先在程序內運算,產生數據庫要執行的 SQL 寫入語句和參數,持續運算直到產生全部的數據庫寫入命令;再開啓數據庫事務,按照先進先出原則順序連續執行數據庫寫入命令(此段時間內不能包含其它非數據庫運算)。只有這樣,才能保證命令的執行都是靜態化的寫入,而且鎖定數據庫的時間最短,保證最大化的下降數據庫壓力。
另一個,正是由於數據庫的操做是串行的,因此在執行數據庫寫入的狀況下,是不能讀取的,要避免出現髒讀,數據庫的讀寫分離就頗有必要了。創建從庫,由主庫負責寫入,從庫負責讀取,將數據庫的壓力均分到多臺上。
數據庫的讀寫分離要注意:剛寫入數據庫的數據,同步到從庫須要 2 到 3 秒的時間,須要在業務上更改流程,以便於在用戶檢索時數據已同步。
減小外部 IO
因爲磁盤的限制,其讀寫速度和內存不成比例,因此這裏是第二個可能出現瓶頸的地方。能夠考慮將配置信息預先讀取到緩存的方式解決。
由於數據庫是依託於硬盤而存在的,因此數據庫的讀寫相對於有效性驗證和業務驗證來講,是時間消耗大戶。在單純的考慮數據庫寫入的狀況下,能夠從系統內剝離訂單的數據庫寫入業務。一來能夠省掉無心義等待數據庫寫入的時間;二來能夠減小 CPU 時間片的佔用,將時間
另外一種是數據庫的讀寫操做,在一個數據庫事務內,是不能有第二個數據庫執行相同的操做的。考慮到數據靜態化中的介紹,數據庫事務開啓後,程序內的運算其實也是數據庫的運算時間的。此時能夠考慮推遲數據庫事務的開啓時間:首先在程序內運算產生要執行的數據庫命令,再開啓數據庫事務,在連續的時間內執行批量執行數據庫事務。即合併數據庫操做到一次數據庫執行中。
異步化
在開發的過程當中,不可避免的要和其關聯的模塊交互,而這些交互並不會對當前的業務邏輯產生影響。這種操做就應該改爲異步的方式。在數據庫交互的過程當中,若是用戶不須要等待數據庫的返回值,還能夠將數據庫執行異步化,在最短的時間內反饋執行結果。
一個栗子:用戶下單的過程當中,提交訂單的操做,其實並不關心提交成功失敗,只是在後續跳轉到訂單詳情的時候纔會看到訂單詳情。這個流程就能夠將數據庫執行異步化,在服務器接收到用戶的提交請求時,能夠在校驗數據後,直接反饋提交成功的響應給用戶。接下來,經過異步隊列的方式,保存到數據庫。客戶端在接收到提交成功的反饋後,提示用戶提交成功,可是不給出訂單的任何信息。用戶只有主動點擊了查看訂單列表,纔會執行數據庫查詢。這個時間差足夠系統處理訂單的真正提交操做了。
這樣作最直接的好處是提升了網站的響應速度,優化了用戶體驗,在提高服務器 TPS 的同時,尚未提高數據庫的壓力。在秒殺特賣時,可以最大限度的避免超賣的狀況。
子系統拆分
繼續上面的例子,數據庫的提交訂單操做,和網站並無多少的關係。此時就能夠考慮到將這一部分拆分出來,作成一個 Windows 服務,二者經過消息隊列的方式通信。隊列的串行讀取正好符合了數據庫的串行執行,在高峯時段也沒有超過數據庫的極限,形成宕機的狀況。在超過服務極限的狀況下,處理慢比不能處理老是要好的。
從操做系統上來講,系統調度針對每一個進程都是平等的。此處將數據庫執行操做從網站拆分出來,減小了數據庫操做對 CPU 時間片的佔用,側面提高了網站的服務能力。
RESTful 架構風格與 SOAP 架構風格
- 屬性路由:使用漸進式的 URI 替代傳統平板式的方法名稱 URI。
- 客戶端緩存報頭:使用 ETag 和 Last-Modified 報頭減輕服務器壓力。
- 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/products
和 api/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-Modified
和 ETag
如何幫助提升性能?
聰明的開發者會把 Last-Modified
和 ETag
跟請求的 HTTP 報頭一塊兒使用,這樣可利用客戶端(例如瀏覽器)的緩存。由於服務器首先產生 Last-Modified
/ETag
標記,服務器可在稍後使用它來判斷頁面是否已經被修改。本質上,客戶端經過將該記號傳回服務器要求服務器驗證其(客戶端)緩存。過程以下:
- 客戶端請求一個頁面(A)。
- 服務器返回頁面A,並在給A加上一個
Last-Modified
/ETag
。 - 客戶端展示該頁面,並將頁面連同
Last-Modified
/ETag
一塊兒緩存。 - 客戶再次請求頁面A,並將上次請求時服務器返回的
Last-Modified
/ETag
一塊兒傳遞給服務器。 - 服務器檢查該
Last-Modified
或ETag
,並判斷出該頁面自上次客戶端請求以後還未被修改,直接返回狀態碼 304 和一個空的響應體。
這裏的客戶端通常指瀏覽器,經過編程方式使用的客戶端,通常不會處理這兩個 HTTP 請求頭。
微服務
目前不作深刻討論。
-
SOAP(Simple Object Access Protocol)簡單對象訪問協議,是交換數據的一種協議規範,是一種輕量的、簡單的、基於XML(標準通用標記語言下的一個子集)的協議,它被設計成在WEB上交換結構化的和固化的信息。 ↩
-
PV:(Page View)即頁面瀏覽量,一般是衡量一個網絡新聞頻道或網站甚至一條網絡新聞的主要指標。網頁瀏覽數是評價網站流量最經常使用的指標之一,簡稱爲 PV。監測網站 PV 的變化趨勢和分析其變化緣由是不少站長按期要作的工做。Page Views 中的 Page 通常是指普通的 HTML 網頁,也包含 PHP、JSP 等動態產生的 HTML 內容。來自瀏覽器的一次 HTML 內容請求會被看做一個 PV,逐漸累計成爲 PV 總數。 ↩
-
TPS:(Transaction Per Second)每秒鐘系統可以處理的交易或事務的數量。它是衡量系統處理能力的重要指標。TPS 是 LoadRunner 中重要的性能參數指標。 ↩
-
RFC2616:目前該規範已有部分更新。 ↩
-
共享鎖:相似於讀寫鎖中的讀鎖。能夠多個一塊兒讀,可是排斥寫鎖。只有讀鎖釋放後,才能進入寫鎖。 ↩
-
排它鎖:相似於讀寫鎖中的寫鎖。只能一個寫,其他的操做都必須等待,直到當前寫鎖釋放後。 ↩
-
串行:同一時間只容許一個線程操做,其他線程只能等待完成後,才能繼續執行操做。 ↩
-
datetime 類型的約束,若是採用
/
作分隔符,必須放在最後一個,而且採用*
前導:{*x:datetime}
。目前,也只有這種寫法能夠跨多個 URI 段。 ↩ -
decimal
和double
兩種數字類型,若是包含小數點將不能被正常解析,目前能夠算 WebApi 框架的一個 Bug 。 ↩