(接上文《架構設計:系統間通訊(31)——其餘消息中間件及場景應用(下1)》)javascript
方案一併非最好的半侵入式方案,卻容易理解架構師的設計意圖:至少作到業務級隔離。方案一最大的優勢在於日誌採集邏輯和業務處理邏輯彼此隔離,當業務邏輯發生變化的時候,並不會影響日誌採集邏輯。html
可是咱們能爲方案一列舉的問題卻能夠遠遠多於方案一的優勢:前端
須要爲不一樣開發語言分別提供客戶端API包。上文中咱們介紹的示例使用JAVA語言,因而 事件/日誌採集系統 就要提供JAVA語言的客戶端API包。若是須要集成 事件/日誌採集系統 的業務系統,都是您公司內各個業務團隊開發的,那麼這個問題還算不上大問題——至少您能夠知道優先開發哪一種語言的客戶端,也知道須要開發有幾種有限的語言;但若是您想將 這個採集系統發佈成共享軟件,或者上市進行售賣,那麼這個問題將限制您產品的快速發展起來。java
因爲 事件/日誌採集系統 的客戶端代碼須要在業務系統中進行編碼集成。因此API包的升級也是一個問題:重大的API包升級可能就會形成以前版本的不兼容問題,致使業務系統從新更改採集系統的調用代碼。一樣,若是全部業務系統都在您公司內部,那麼這個問題也不大。可是記住,您的目標是要將系統產品化。程序員
雖然在業務系統中,能夠經過良好的代碼結構將業務邏輯和日誌採集邏輯進行隔離,可是日誌採集的處理過程終歸集成於業務系統中,或多或少會影響業務系統的處理過程。例如:當消息生產者速度減緩時,可能就會影響到業務系統的處理效率;當待發送的消息在業務系統端大量堆積時,這些消息就會佔用本該由業務數據使用的系統內存。web
看來,咱們須要另外一種半侵入的解決方案來解決這些問題。spring
第二種解決方案中,咱們只要求業務系統在頁面上加載一段JavaScript代碼,就能夠完成業務系統的事件/日誌採集工做。事件/日誌數據經過HTTP協議,跨域傳輸到事件/日誌採集系統。chrome
HTTP協議的優點在於它是一個業內普遍使用的協議,下到剛從學校畢業的應屆生上到有20年開發經驗的資深工程師,都會運用這個協議。其次,這個協議與編程語言無關,您的業務系統不管是使用JVM虛擬機系列的語言進行開發,仍是使用PHP進行開發,或是使用NodeJS進行開發又或者其它開發語言進行開發。只要您須要在瀏覽器上呈現操做頁面,就會涉及到HTTP協議。編程
在業務系統的頁面集成JavaScript腳本實現對訪問日誌的採集的方式,實際上也有必定侷限性:若是您須要採集的事件不是針對頁面訪問進行的(例如採集業務服務器在設定的定時執行器中,進行了多少訂單費用結算),那麼這種方案二的方式就不太適用。還好,根據上文中提到的統計需求,咱們須要統計的剛好是商品訂單的訪問狀況和商品價格走勢的訪問狀況。跨域
方案二和方案二的負載層設計徹底不同。在方案一中,因爲業務系統中集成了消息隊列的生產者端,因此它的負載層徹底由Kafka Brokers中的分區(partition)完成。可是在方案二中,因爲業務系統向採集系統發送消息的方式是經過HTTP協議完成,因此採集系統的負載層須要進行相應的調整:
上圖是一個典型的基於HTTP協議的負載均衡方案。在個人另外一篇博文《架構設計:負載均衡層設計方案(7)——LVS + Keepalived + Nginx安裝及配置》中對這個方案有詳細的介紹,這裏就再也不進行贅述了。若是您還以爲負載層太薄弱,還能夠在其之上再加入DNS輪詢等技術。
第二種解決方案中,在事件/日誌採集系統內部咱們仍是使用了Apache Kafka MQ技術,在採集系統內部進行消息的發送和接受。在一些讀者看來,消息已經經過HTTP協議從外部業務系統(更確切來講是從業務系統用戶的瀏覽器端)傳輸到了採集系統內部,那麼在採集系統內部只須要完成對這些原始日誌的存儲(或者送入及時分析系統)就好了,爲何還須要在採集系統內部採用消息隊列機制呢?
考慮一下這種狀況,當集成了採集系統的各個業務系統忽然出現訪問洪峯,產生大量的日誌數據時。若是採集系統內部沒有任何緩存機制,就會讓採集系統編程整個架構中的處理瓶頸。要知道,不管您在採集系統內部採用哪種適當的持久化存儲方案,都會消耗較多的處理時間。因此在方案二中,採集系統內部使用MQ隊列就是出於緩存消息的目的。
固然您也能夠去掉MQ,換成其餘的方案緩存來不及處理的日誌消息,但必定要有這樣的緩存機制。由於處理單條日誌數據,採集系統通常會消耗比業務系統多的時間,畢竟業務系統只負責發送日誌數據。
那麼結合負載均衡層的調整和已有的Kafka消息隊列的方案,咱們就能夠畫出方案二中完整的系統架構圖了:
在本方案中,業務系統經過呈如今瀏覽器上的頁面,集成JavaScript腳本向採集系統發送HTTP請求。可是業務系統和採集統極可能使用不一樣的域名(實際狀況是做爲事件/日誌採集系統的架構師,您不可能控制業務系統的域名)。
如上圖所示,跨域的狀況下業務系統的頁面不能經過瀏覽器端的XMLHttpRequest對象向工做在另一個域的採集系統發送HTTP請求。爲了解決這個問題,咱們須要找到一種在瀏覽器端可以完成HTTP跨域調用的方法。
好在靠譜的程序員們爲咱們提供了不少過往經驗解決這個問題:proxy、Flash、iframe、Jsonp、CORS等等。這裏咱們根據採集系統的技術需求,介紹兩種可使用的解決辦法:iframe和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", "*"); ......
...... 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標籤,實際上就是避免在瀏覽器端使用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屬性。
因爲解決方案二中有不少技術點都和解決方案一相同,例如都使用了Apache Kafka MQ,都會使用Spring進行支撐,而且都不會影響消息消費者使用「適當的存儲方案」進行存儲。因此在本小節介紹方案二的代碼時,咱們只會給出那些不同的,可以體現方案二工做特色的代碼,其餘部分的代碼就再也不贅述了。
爲了便於第三方業務系統的集成,採集系統所提供的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進行替換。
說完了採集系統爲業務系統提供的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消息生產者的設置。
按照解決方案二的設計思路,完成設計的日誌/事件採集系統,是能夠做爲一款產品對公衆開放了。既然要開放系統就涉及到各個用戶的權限問題:至少應該保證用戶A集成採集系統的業務系統是一個可用的業務系統,應該保證每個用戶只能在採集平臺上看到他本身的業務系統的統計信息。
採集系統能夠提供業務系統註冊功能,全部要使用採集系統的業務系統都首先須要經過註冊頁面進行註冊。註冊成功後,採集系統將會爲這個業務系統生成一個惟一校驗碼。在進行日誌採集時,只有校驗碼對應的業務系統和業務系統所註冊的域名徹底一致,採集系統纔會認爲本次數據有效。
事件/日誌採集系統架構設計的另外一個重點問題,就是要保證事件/採集系統可以在多個業務系統同時出現流量洪峯的狀況下,也能正常的進行日誌統計,而且不影響各個業務系統的正常工做——您不可能要求使用採集系統的各個業務日均PV不能超過XXXXX的最大閥值。
除了上文提到的採用一款高吞吐量MQ做用於採集系統內部,在流量洪峯時堆積消息消費者還將來得及處理的日誌消息之外(這也是方案二中依然要使用MQ組件的緣由)。您還能夠進一步在Kafka分區上作進行文章,例如爲每個業務系統建立獨立的Topic,並視用戶購買的服務套餐狀況設置不一樣的分區規模。您還須要爲整個日誌採集系統安排40%左右的閒置資源,以便再出現流量洪峯的狀況下,能夠快速升級每一個物理節點的性能或者加入新的服務節點——雲化的服務器是一個不錯的選擇。
須要注意的是:Apache Kafka中Topic所擁有的分區數量一旦建立就不能改變的缺點會限制它的橫向擴容潛力。因此若是真的要設計一款超大型,對多個高數據流量的業務系統進行徹底開放的採集系統,其中是否仍是採用Apache Kafka做爲核心消息傳遞手段就須要再進行慎重考慮了。
實際上若是您已經看過筆者三個專欄中的全部文章,那麼分佈式系統中最關鍵的幾個問題都已經有過介紹了(除了數據一致性問題和數據恢復問題):服務節點發現方法、服務協調和選舉規則、網絡IO模型、緩存和異步處理。那麼爲何不本身寫一個知足技術需求的MQ呢?另外,阿里的開源項目RocketMQ也是一個不錯的選擇哦。
和解決方案一相比,在解決方案二中的消息消費者代碼,包括其中調用的「合適的存儲方案」都不須要作任何的變化。日誌系統爲業務系統提供的HTTP調用接口是爲了保證各類業務系統的調用兼容性;繼續在日誌系統內部使用MQ是爲了保證日誌系統不會成爲任何外部系統的調用瓶頸。這樣,在解決方案二中就進一步優化了解決方案一中遺留的設計問題。
相似方案二這樣,在瀏覽頁面嵌入JavaScript代碼進行訪問日誌採集的典型應用之一就是百度推出的「百度站長統計工具」(http://tongji.baidu.com/)。要使用這個統計產品,首先您須要註冊一個用戶信息,而且告知統計工具您須要統計的業務系統的工做域名。
接下來百度統計工具就會爲您生成一段JavaScript代碼,而且帶有校驗信息。以下圖所示:
實際上,若是您仔細閱讀以上生成的代碼,就會發現這段代碼主要作的事情是:「經過這段代碼生成另外一個JavaScript引用標籤」。最後您只須要在您的業務系統頁面上,加入這段JavaScript代碼就好了。
上圖是「百度站長工具」的統計結果樣例。