(接上文《架构设计:系统间通讯(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代码就好了。
上图是“百度站长工具”的统计结果样例。