權聲明:歡迎轉載,但是看在我辛勤勞動的份上,請註明來源:http://blog.csdn.net/yinwenjie(未經允許嚴禁用於商業用途!) https://blog.csdn.net/yinwenjie/article/details/51459120
(接上文《架構設計:系統間通信(31)——其他消息中間件及場景應用(下1)》)
5-3、解決方案二:改進半侵入式方案
5-3-1、解決方法一的問題所在
方案一並不是最好的半侵入式方案,卻容易理解架構師的設計意圖:至少做到業務級隔離。方案一最大的優點在於日誌採集邏輯和業務處理邏輯彼此隔離,當業務邏輯發生變化的時候,並不會影響日誌採集邏輯。
但是我們能爲方案一列舉的問題卻可以遠遠多於方案一的優點:
需要爲不同開發語言分別提供客戶端API包。上文中我們介紹的示例使用JAVA語言,於是 事件/日誌採集系統 就要提供JAVA語言的客戶端API包。如果需要集成 事件/日誌採集系統 的業務系統,都是您公司內各個業務團隊開發的,那麼這個問題還算不上大問題——至少您可以知道優先開發哪種語言的客戶端,也知道需要開發有幾種有限的語言;但如果您想將 這個採集系統發佈成共享軟件,或者上市進行售賣,那麼這個問題將限制您產品的快速發展起來。
由於 事件/日誌採集系統 的客戶端代碼需要在業務系統中進行編碼集成。所以API包的升級也是一個問題:重大的API包升級可能就會造成之前版本的不兼容問題,導致業務系統重新更改採集系統的調用代碼。同樣,如果所有業務系統都在您公司內部,那麼這個問題也不大。但是記住,您的目標是要將系統產品化。
雖然在業務系統中,可以通過良好的代碼結構將業務邏輯和日誌採集邏輯進行隔離,但是日誌採集的處理過程終歸集成於業務系統中,或多或少會影響業務系統的處理過程。例如:當消息生產者速度減緩時,可能就會影響到業務系統的處理效率;當待發送的消息在業務系統端大量堆積時,這些消息就會佔用本該由業務數據使用的系統內存。
看來,我們需要另一種半侵入的解決方案來解決這些問題。
5-3-2、解決方法二的思路
第二種解決方案中,我們只要求業務系統在頁面上加載一段JavaScript代碼,就可以完成業務系統的事件/日誌採集工作。事件/日誌數據通過HTTP協議,跨域傳輸到事件/日誌採集系統。
HTTP協議的優勢在於它是一個業內廣泛使用的協議,下到剛從學校畢業的應屆生上到有20年開發經驗的資深工程師,都會運用這個協議。其次,這個協議與編程語言無關,您的業務系統無論是使用JVM虛擬機系列的語言進行開發,還是使用PHP進行開發,或是使用NodeJS進行開發又或者其它開發語言進行開發。只要您需要在瀏覽器上呈現操作頁面,就會涉及到HTTP協議。
在業務系統的頁面集成JavaScript腳本實現對訪問日誌的採集的方式,實際上也有一定侷限性:如果您需要採集的事件不是針對頁面訪問進行的(例如採集業務服務器在設定的定時執行器中,進行了多少訂單費用結算),那麼這種方案二的方式就不太適用。還好,根據上文中提到的統計需求,我們需要統計的恰好是商品訂單的訪問情況和商品價格走勢的訪問情況。
5-3-2-1 負載層設計
方案二和方案二的負載層設計完全不一樣。在方案一中,由於業務系統中集成了消息隊列的生產者端,所以它的負載層完全由Kafka Brokers中的分區(partition)完成。但是在方案二中,由於業務系統向採集系統發送消息的方式是通過HTTP協議完成,所以採集系統的負載層需要進行相應的調整:
上圖是一個典型的基於HTTP協議的負載均衡方案。在我的另一篇博文《架構設計:負載均衡層設計方案(7)——LVS + Keepalived + Nginx安裝及配置》中對這個方案有詳細的介紹,這裏就不再進行贅述了。如果您還覺得負載層太薄弱,還可以在其之上再加入DNS輪詢等技術。
5-3-2-2 爲什麼還要繼續使用MQ?
第二種解決方案中,在事件/日誌採集系統內部我們還是使用了Apache Kafka MQ技術,在採集系統內部進行消息的發送和接受。在一些讀者看來,消息已經通過HTTP協議從外部業務系統(更確切來說是從業務系統用戶的瀏覽器端)傳輸到了採集系統內部,那麼在採集系統內部只需要完成對這些原始日誌的存儲(或者送入及時分析系統)就行了,爲什麼還需要在採集系統內部採用消息隊列機制呢?
考慮一下這種情況,當集成了採集系統的各個業務系統突然出現訪問洪峯,產生大量的日誌數據時。如果採集系統內部沒有任何緩存機制,就會讓採集系統編程整個架構中的處理瓶頸。要知道,無論您在採集系統內部採用哪一種適當的持久化存儲方案,都會消耗較多的處理時間。所以在方案二中,採集系統內部使用MQ隊列就是出於緩存消息的目的。
當然您也可以去掉MQ,換成其他的方案緩存來不及處理的日誌消息,但一定要有這樣的緩存機制。因爲處理單條日誌數據,採集系統一般會消耗比業務系統多的時間,畢竟業務系統只負責發送日誌數據。
那麼結合負載均衡層的調整和已有的Kafka消息隊列的方案,我們就可以畫出方案二中完整的系統架構圖了:
5-3-2-3 跨域問題如何解決
在本方案中,業務系統通過呈現在瀏覽器上的頁面,集成JavaScript腳本向採集系統發送HTTP請求。但是業務系統和採集統很可能使用不同的域名(實際情況是作爲事件/日誌採集系統的架構師,您不可能控制業務系統的域名)。
如上圖所示,跨域的情況下業務系統的頁面不能通過瀏覽器端的XMLHttpRequest對象向工作在另外一個域的採集系統發送HTTP請求。爲了解決這個問題,我們需要找到一種在瀏覽器端能夠完成HTTP跨域調用的方法。
好在靠譜的程序員們爲我們提供了很多過往經驗解決這個問題:proxy、Flash、iframe、Jsonp、CORS等等。這裏我們根據採集系統的技術需求,介紹兩種可以使用的解決辦法:iframe和CORS。
CORS方式:
CORS是Cross-Origin Resource Sharing(跨源資源共享)的簡稱。這個跨域技術主要由瀏覽器提供支持。當瀏覽器檢查到XMLHttpRequest對象進行跨域調用時,CORS會首先允許本次調用,並且檢查對方響應的HTTP協議的返回信息。如果返回信息的Header中存在Access-Control-Allow-Origin屬性描述信息,並且允許調用域,那麼就認爲調用成功;否則瀏覽器會提示類似於:“No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin XXXXX is therefore not allowed access.”的錯誤。
由於CORS方式的跨域調用需要瀏覽器的支持,所以存在一個瀏覽器版本的支持問題。以下列表摘自CORS官網(http://enable-cors.org/)列舉了各種瀏覽器版本對CORS的支持情況:
上圖中紅色部分代表不支持CORS的瀏覽器版本、黃×××塊代表部分支持CORS的瀏覽器版本、綠×××塊代表完整支持CORS的瀏覽器版本。要使用CORS的支持也很簡單,只需要在目標域的服務端http協議header部分寫入“Access-Control-Allow-Origin”屬性,如下JAVA代碼所示:
允許任何域調用本域服務
...... response.setHeader("Access-Control-Allow-Origin", "*"); ......
允許XXXXX域調用本域服務
...... response.setHeader("Access-Control-Allow-Origin", "XXXXX"); ......
注意,如果您使用CORS方式,並且服務前存在類似Nginx一樣的HTTP代理服務
,那麼您需要在Nginx的配置中增加對Access-Control-Allow-Origin的支持,類似如下:
http { ...... add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Headers X-Requested-With; add_header Access-Control-Allow-Methods GET,POST,OPTIONS; ...... }
iframe標籤方式:
使用iframe標籤,實際上就是避免在瀏覽器端使用XMLHttpRequest對象。iframe標籤在各個版本的瀏覽器上基本上都沒有不支持的問題,只有部分瀏覽器對iframe標籤的屬性支持有一些不同。以下是一個使用iframe標籤調用另一域上服務的示例:
...... <iframe style="display: none" src="http://192.168.1.100:9090/templateSSHProject/showSomething"></iframe> ......
display屬性的作用是保證iframe標籤不會有展示效果出現在最終頁面上。使用iframe標籤進行跨域調用是有明顯缺點的:它會破壞前端開發人員既定的頁面佈局思路;如果不隱藏iframe標籤,還會破壞開發人員在書寫JavaScript腳本時的效果預判。
由於這兩種方式都有一些問題,所以在實際操作中可以兩種解決方法進行混用。首先判斷當前瀏覽器版本信息,如果瀏覽器版本支持CORS方式,則優先採用這種方式(畢竟這種方式不會改變頁面既有的html標籤佈局);如果瀏覽器版本不支持CORS方式,則使用iframe標籤方式。至於日誌服務器所提供HTTP的調用接口上,始終都向header增加Access-Control-Allow-Origin屬性。
5-4、解決方案二編碼示例
由於解決方案二中有很多技術點都和解決方案一相同,例如都使用了Apache Kafka MQ,都會使用Spring進行支撐,並且都不會影響消息消費者使用“適當的存儲方案”進行存儲。所以在本小節介紹方案二的代碼時,我們只會給出那些不一樣的,能夠體現方案二工作特點的代碼,其他部分的代碼就不再贅述了。
5-4-1、混合採用CORS和iframe
爲了便於第三方業務系統的集成,採集系統所提供的JavaScript代碼段應該儘量簡單,最好就只需要業務系統引用一個JavaScript文件就行了。如下代碼端所以:
// 業務系統在頁面上通過以下形式引用採集系統提供的腳本文件 ...... <script type="text/javascript" src="http://www.logsservice.com/analysis.js?34ab834ea98ee838ac76ed3986347546"></script> ......
以上代碼片段中“www.logsservice.com”就是採集系統所在的域名,analysis.js就是提供給各個業務系統進行嵌入的js文件,“34ab834ea98ee838ac76ed3986347546”是一段由採集系統的“註冊管理平臺”生成的第三方業務系統的校驗串,只有校驗串所綁定的域名和當前嵌入js文件頁面所在的域名相同時,採集系統才認爲本次採集數據有效。
以下爲“analysis.js”文件的腳本代碼示例:
var _supportchromeversion = ["47","48","49","50","51","52"]; // 首先,無論使用哪種方式向採集系統發送http數據,都需要得到頁面上引用本js文件時傳遞的校驗串encrypted // 這個encrypted參數含有相當的信息量 // 日誌服務通過這個encrypted驗證用戶權限,業務系統域名匹配等信息 var encrypted = null; var scripts = document.getElementsByTagName("script"); for (var index = 0; index < scripts.length; index++) { var script = scripts[index]; // 如果條件成立,說明找到了在頁面上本js文件的引用位置,並且有加密參數記錄 if (script.src.indexOf("js/analysis.js") >= 0 && script.src.indexOf("?") >= 0) { encrypted = script.src.split('?')[1]; } } // 如果沒有傳遞encrypted信息,則認爲是錯誤的js引用。不再進行處理 if(encrypted != null && encrypted != "") { // 確定當前瀏覽器是否支持CORS方式 var bowersInfos = getVersion(); var supportCors = false; // 在本示例中,我們只判斷了chrome瀏覽器的版本信息 // 其它瀏覽器版本的判斷原理相似 if(bowersInfos.browser == "chrome") { var currentVersionArray = bowersInfos.ver.split("."); var currentVersion = currentVersionArray[0]; if(contains(_supportchromeversion , currentVersion)) { supportCors = true; } } // ================= // 這裏可判斷其它瀏覽器的支持情況 // ================= // ===========================如果支持,則直接使用XMLHttpRequest發起請求 //時間戳是爲了防止 HTTP 304 var timestamp = new Date().getTime(); if(supportCors) { var req = createXmlHttpRequest(); var url = "http://127.0.0.1:9090/templateSSHProject/analysisSomething?encrypted=" + encrypted + "&" + timestamp; req.open("GET" , url , true); req.send(null); } // ===========================如果不支持,則使用iframe方式進行請求 else { var context = "<iframe style=\"display: none\" src=\"http://127.0.0.1:9090/templateSSHProject/analysisSomething?encrypted=" + encrypted + "&" + timestamp + "\"></iframe>"; document.write(context); } } // 獲取瀏覽器版本的方法 // 該方法經用於測試使用。包括的瀏覽器並不完整 function getVersion() { var Sys = {}; var ua = navigator.userAgent.toLowerCase(); var re =/(msie|firefox|chrome|opera|version).*?([\d.]+)/; var m = ua.match(re); Sys.browser = m[1].replace(/version/, "'safari"); Sys.ver = m[2]; return Sys; } //獲取XmlHttpRequest對象 function createXmlHttpRequest() { if(window.ActiveXObject) { return new ActiveXObject("Microsoft.XMLHTTP"); } else if(window.XMLHttpRequest) { return new XMLHttpRequest(); } } // 用於集合元素比較 function contains(collection, obj) { var index = collection.length; while (index--) { if (collection[index] === obj) { return true; } } return false; }
根據以上代碼片段,如果瀏覽器不支持CORS方式那麼腳本代碼將在頁面輸出一個iframe標籤,並通過這個iframe標籤完成跨域調用(當然這個標籤在頁面上是不可見的)。生成的iframe標籤如下所示:
如果瀏覽器支持CORS方式,那麼腳本代碼將創建XMLHttpRequest對象,並通過XMLHttpRequest對象完成跨域調用(IE下使用ActiveXObject)。
注意:爲了方便調試,以上實例代碼中使用了一個筆者本地可調試的url, 代替了“www.logsservice.com”。讀者可以根據自己的url進行替換。
5-4-2、採集系統生產者編碼
說完了採集系統爲業務系統提供的JavaScript腳本文件,我們再來說說採集系統的HTTP接口層代碼:
package templateSSHProject.controller; import java.io.PrintWriter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import test.interrupter.producer.ProducerService; /** * spring MVC組件搭建的http控制層 * @author yinwenjie */ @Controller @RequestMapping("/") public class AnalysisController { /** * 這裏就是消息生產者對象 * 其工作方式與方案一中的工作方式一致 */ @Autowired private ProducerService producerService; /** * 做一些分析動作 * @param request * @param response */ @RequestMapping("/analysisSomething") public void analysisSomething(HttpServletRequest request , HttpServletResponse response) { String param = request.getParameter("encrypted"); // 利用kafka生產者端發送消息 this.producerService.sendeMessage(param); System.out.println("public void sendeMessage(String message) : " + param); // 輸出相應信息,最關鍵的就是header中的設置 // 有沒有body信息,都沒有什麼關係 response.setHeader("Access-Control-Allow-Origin", "*"); response.setCharacterEncoding("utf-8"); response.setContentType("text/html; charset=UTF-8"); PrintWriter out = null; try { out = response.getWriter(); } catch (Exception e) { throw new RuntimeException(e); } out.print(""); } }
採集系統保持高吞吐量的其中一個關鍵在於,Web控制層中所使用的Apache Kafka消費者對象producerService能夠快速的將消息發送出去。可以沿用方案一中對Apache Kafka消息生產者的設置。
5-5 方案二中其他設計思考
保證日誌收集權限
按照解決方案二的設計思路,完成設計的日誌/事件採集系統,是可以作爲一款產品對公衆開放了。既然要開放系統就涉及到各個用戶的權限問題:至少應該保證用戶A集成採集系統的業務系統是一個可用的業務系統,應該保證每一個用戶只能在採集平臺上看到他自己的業務系統的統計信息。
採集系統可以提供業務系統註冊功能,所有要使用採集系統的業務系統都首先需要通過註冊頁面進行註冊。註冊成功後,採集系統將會爲這個業務系統生成一個唯一校驗碼。在進行日誌採集時,只有校驗碼對應的業務系統和業務系統所註冊的域名完全一致,採集系統纔會認爲本次數據有效。
卸掉流量洪峯
事件/日誌採集系統架構設計的另一個重點問題,就是要保證事件/採集系統能夠在多個業務系統同時出現流量洪峯的情況下,也能正常的進行日誌統計,並且不影響各個業務系統的正常工作——您不可能要求使用採集系統的各個業務日均PV不能超過XXXXX的最大閥值。
除了上文提到的採用一款高吞吐量MQ作用於採集系統內部,在流量洪峯時堆積消息消費者還未來得及處理的日誌消息以外(這也是方案二中依然要使用MQ組件的原因)。您還可以進一步在Kafka分區上做進行文章,例如爲每一個業務系統創建獨立的Topic,並視用戶購買的服務套餐情況設置不同的分區規模。您還需要爲整個日誌採集系統安排40%左右的閒置資源,以便再出現流量洪峯的情況下,可以快速升級每個物理節點的性能或者加入新的服務節點——雲化的服務器是一個不錯的選擇。
需要注意的是:Apache Kafka中Topic所擁有的分區數量一旦創建就不能改變的缺點會限制它的橫向擴容潛力。所以如果真的要設計一款超大型,對多個高數據流量的業務系統進行完全開放的採集系統,其中是否還是採用Apache Kafka作爲核心消息傳遞手段就需要再進行慎重考慮了。
實際上如果您已經看過筆者三個專欄中的所有文章,那麼分佈式系統中最關鍵的幾個問題都已經有過介紹了(除了數據一致性問題和數據恢復問題):服務節點發現方法、服務協調和選舉規則、網絡IO模型、緩存和異步處理。那麼爲什麼不自己寫一個滿足技術需求的MQ呢?另外,阿里的開源項目RocketMQ也是一個不錯的選擇哦。
沿用方案一的MQ的設計
和解決方案一相比,在解決方案二中的消息消費者代碼,包括其中調用的“合適的存儲方案”都不需要做任何的變化。日誌系統爲業務系統提供的HTTP調用接口是爲了保證各種業務系統的調用兼容性;繼續在日誌系統內部使用MQ是爲了保證日誌系統不會成爲任何外部系統的調用瓶頸。這樣,在解決方案二中就進一步優化了解決方案一中遺留的設計問題。
5-6 百度站長統計工具
類似方案二這樣,在瀏覽頁面嵌入JavaScript代碼進行訪問日誌採集的典型應用之一就是百度推出的“百度站長統計工具”(http://tongji.baidu.com/)。要使用這個統計產品,首先您需要註冊一個用戶信息,並且告知統計工具您需要統計的業務系統的工作域名。
接下來百度統計工具就會爲您生成一段JavaScript代碼,並且帶有校驗信息。如下圖所示:
實際上,如果您仔細閱讀以上生成的代碼,就會發現這段代碼主要做的事情是:“通過這段代碼生成另一個JavaScript引用標籤”。最後您只需要在您的業務系統頁面上,加入這段JavaScript代碼就行了。