精通 Storm(三)
原文:
zh.annas-archive.org/md5/5A2D98C1AAE9E2E2F9D015883F441239译者:飞龙
第十一章:使用 Storm 进行 Apache 日志处理
在上一章中,我们介绍了如何将 Storm 与 Redis、HBase、Esper 和 Elasticsearch 集成。
在本章中,我们将介绍 Storm 最流行的用例,即日志处理。
本章涵盖以下主要部分:
-
Apache 日志处理元素
-
安装 Logstash
-
配置 Logstash 以将 Apache 日志生成到 Kafka
-
拆分 Apache 日志文件
-
计算国家名称、操作系统类型和浏览器类型
-
识别网站的搜索关键词
-
持久化处理数据
-
Kafka spout 和定义拓扑
-
部署拓扑
-
将数据存储到 Elasticsearch 并生成报告
Apache 日志处理元素
日志处理正在成为每个组织的必需品,因为他们需要从日志数据中收集业务信息。在本章中,我们基本上是在讨论如何使用 Logstash、Kafka、Storm 和 Elasticsearch 来处理 Apache 日志数据,以收集业务信息。
以下图示了我们在本章中开发的所有元素:
图 11.1:日志处理拓扑
使用 Logstash 在 Kafka 中生成 Apache 日志
如第八章中所解释的,Storm 和 Kafka 的集成,Kafka 是一个分布式消息队列,可以与 Storm 很好地集成。在本节中,我们将向您展示如何使用 Logstash 来读取 Apache 日志文件并将其发布到 Kafka 集群中。我们假设您已经运行了 Kafka 集群。Kafka 集群的安装步骤在第八章中概述。
安装 Logstash
在继续安装 Logstash 之前,我们将回答以下问题:什么是 Logstash?为什么我们要使用 Logstash?
什么是 Logstash?
Logstash 是一个用于收集、过滤/解析和发送数据以供将来使用的工具。收集、解析和发送分为三个部分,称为输入、过滤器和输出:
-
input部分用于从外部来源读取数据。常见的输入来源是文件、TCP 端口、Kafka 等。
-
filter部分用于解析数据。
-
output部分用于将数据发送到某些外部来源。常见的外部来源是 Kafka、Elasticsearch、TCP 等。
为什么我们要使用 Logstash?
在 Storm 开始实际处理之前,我们需要实时读取日志数据并将其存储到 Kafka 中。我们使用 Logstash 是因为它非常成熟地读取日志文件并将日志数据推送到 Kafka 中。
安装 Logstash
在安装 Logstash 之前,我们应该在 Linux 服务器上安装 JDK 1.8,因为我们将使用 Logstash 5.4.1,而 JDK 1.8 是此版本的最低要求。以下是安装 Logstash 的步骤:
-
从
artifacts.elastic.co/downloads/logstash/logstash-5.4.1.zip下载 Logstash 5.4.1。 -
将设置复制到所有你想要发布到 Kafka 的 Apache 日志的机器上。
-
通过运行以下命令提取设置:
> unzip logstash-5.4.1.zip
Logstash 的配置
现在,我们将定义 Logstash 配置来消耗 Apache 日志并将其存储到 Kafka 中。
创建一个logstash.conf文件并添加以下行:
input {
file {
path => "PATH_TO_APACHE_LOG"
start_position => "beginning"
}
}
output {
kafka {
topic_id => "TOPIC_NAME"
bootstrap_servers => "KAFKA_IP:KAFKA_PORT"
}
}
我们应该更改前述配置中的以下参数:
-
TOPIC_NAME:替换为您要用于存储 Apache 日志的 Kafka 主题 -
KAFKA_IP和KAFKA_PORT:指定所有 Kafka 节点的逗号分隔列表 -
PATH_TO_APACHE_LOG:Logstash 机器上 Apache 日志文件的位置
转到 Logstash 主目录并执行以下命令以开始读取日志并发布到 Kafka:
$ bin/logstash agent -f logstash.conf
现在,实时日志数据正在进入 Kafka 主题。在下一节中,我们将编写 Storm 拓扑来消费日志数据,处理并将处理数据存储到数据库中。
为什么在 Logstash 和 Storm 之间使用 Kafka?
众所周知,Storm 提供了可靠的消息处理,这意味着每条消息进入 Storm 拓扑都将至少被处理一次。在 Storm 中,数据丢失只可能发生在 spout 端,如果 Storm spout 的处理能力小于 Logstash 的生产能力。因此,为了避免数据在 Storm spout 端丢失,我们通常会将数据发布到消息队列(Kafka),Storm spout 将使用消息队列作为数据源。
分割 Apache 日志行
现在,我们正在创建一个新的拓扑,它将使用KafkaSpout spout 从 Kafka 中读取数据。在本节中,我们将编写一个ApacheLogSplitter bolt,它具有从 Apache 日志行中提取 IP、状态码、引用来源、发送的字节数等信息的逻辑。由于这是一个新的拓扑,我们必须首先创建新项目。
-
创建一个新的 Maven 项目,
groupId为com.stormadvance,artifactId为logprocessing。 -
在
pom.xml文件中添加以下依赖项:
<dependency>
<groupId>org.apache.storm</groupId>
<artifactId>storm-core</artifactId>
<version>1.0.2</version>
<scope>provided</scope>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>15.0</version>
</dependency>
- 我们将在
com.stormadvance.logprocessing包中创建ApacheLogSplitter类。这个类包含了从 Apache 日志行中提取不同元素(如 IP、引用来源、用户代理等)的逻辑。
/**
* This class contains logic to Parse an Apache log file with Regular
* Expressions
*/
public class ApacheLogSplitter {
public Map<String,Object> logSplitter(String apacheLog) {
String logEntryLine = apacheLog;
// Regex pattern to split fetch the different properties from log lines.
String logEntryPattern = "^([\\d.]+) (\\S+) (\\S+) \\[([\\w-:/]+\\s[+\\-]\\d{4})\\] \"(.+?)\" (\\d{3}) (\\d+) \"([^\"]+)\" \"([^\"]+)\"";
Pattern p = Pattern.compile(logEntryPattern);
Matcher matcher = p.matcher(logEntryLine);
Map<String,Object> logMap = new HashMap<String, Object>();
if (!matcher.matches() || 9 != matcher.groupCount()) {
System.err.println("Bad log entry (or problem with RE?):");
System.err.println(logEntryLine);
return logMap;
}
// set the ip, dateTime, request, etc into map.
logMap.put("ip", matcher.group(1));
logMap.put("dateTime", matcher.group(4));
logMap.put("request", matcher.group(5));
logMap.put("response", matcher.group(6));
logMap.put("bytesSent", matcher.group(7));
logMap.put("referrer", matcher.group(8));
logMap.put("useragent", matcher.group(9));
return logMap;
}
logSplitter(String apacheLog)方法的输入是:
98.83.179.51 - - [18/May/2011:19:35:08 -0700] \"GET /css/main.css HTTP/1.1\" 200 1837 \"http://www.safesand.com/information.htm\" \"Mozilla/5.0 (Windows NT 6.0; WOW64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1\"
logSplitter(String apacheLog)方法的输出是:
{response=200, referrer=http://www.safesand.com/information.htm, bytesSent=1837, useragent=Mozilla/5.0 (Windows NT 6.0; WOW64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1, dateTime=18/May/2011:19:35:08 -0700, request=GET /css/main.css HTTP/1.1, ip=98.83.179.51}
- 现在我们将在
com.stormadvance.logprocessing包中创建ApacheLogSplitterBolt类。ApacheLogSplitterBolt扩展了org.apache.storm.topology.base.BaseBasicBolt类,并将ApacheLogSplitter类生成的字段集传递给拓扑中的下一个 bolt。以下是ApacheLogSplitterBolt类的源代码:
/**
*
* This class call the ApacheLogSplitter class and pass the set of fields (ip,
* referrer, user-agent, etc) to next bolt in Topology.
*/
public class ApacheLogSplitterBolt extends BaseBasicBolt {
private static final long serialVersionUID = 1L;
// Create the instance of ApacheLogSplitter class.
private static final ApacheLogSplitter apacheLogSplitter = new ApacheLogSplitter();
private static final List<String> LOG_ELEMENTS = new ArrayList<String>();
static {
LOG_ELEMENTS.add("ip");
LOG_ELEMENTS.add("dateTime");
LOG_ELEMENTS.add("request");
LOG_ELEMENTS.add("response");
LOG_ELEMENTS.add("bytesSent");
LOG_ELEMENTS.add("referrer");
LOG_ELEMENTS.add("useragent");
}
public void execute(Tuple input, BasicOutputCollector collector) {
// Get the Apache log from the tuple
String log = input.getString(0);
if (StringUtils.isBlank(log)) {
// ignore blank lines
return;
}
// call the logSplitter(String apachelog) method of ApacheLogSplitter
// class.
Map<String, Object> logMap = apacheLogSplitter.logSplitter(log);
List<Object> logdata = new ArrayList<Object>();
for (String element : LOG_ELEMENTS) {
logdata.add(logMap.get(element));
}
// emits set of fields (ip, referrer, user-agent, bytesSent, etc)
collector.emit(logdata);
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// specify the name of output fields.
declarer.declare(new Fields("ip", "dateTime", "request", "response",
"bytesSent", "referrer", "useragent"));
}
}
ApacheLogSplitterBolt类的输出包含七个字段。这些字段是ip,dateTime,request,response,bytesSent,referrer和useragent。
从日志文件中识别国家、操作系统类型和浏览器类型
本节解释了如何通过分析 Apache 日志行来计算用户国家名称、操作系统类型和浏览器类型。通过识别国家名称,我们可以轻松地确定我们网站受到更多关注的地点以及我们受到较少关注的地点。让我们执行以下步骤来计算 Apache 日志文件中的国家名称、操作系统和浏览器:
- 我们使用开源的
geoip库来从 IP 地址计算国家名称。在pom.xml文件中添加以下依赖项:
<dependency>
<groupId>org.geomind</groupId>
<artifactId>geoip</artifactId>
<version>1.2.8</version>
</dependency>
- 在
pom.xml文件中添加以下存储库:
<repository>
<id>geoip</id>
<url>http://snambi.github.com/maven/</url>
</repository>
- 我们将在
com.stormadvance.logprocessing包中创建IpToCountryConverter类。这个类包含了带有GeoLiteCity.dat文件位置作为输入的参数化构造函数。你可以在logprocessing项目的资源文件夹中找到GeoLiteCity.dat文件。GeoLiteCity.dat文件的位置在所有 Storm 节点中必须相同。GeoLiteCity.dat文件是我们用来从 IP 地址计算国家名称的数据库。以下是IpToCountryConverter类的源代码:
/**
* This class contains logic to calculate the country name from IP address
*
*/
public class IpToCountryConverter {
private static LookupService cl = null;
/**
* An parameterised constructor which would take the location of
* GeoLiteCity.dat file as input.
*
* @param pathTOGeoLiteCityFile
*/
public IpToCountryConverter(String pathTOGeoLiteCityFile) {
try {
cl = new LookupService("pathTOGeoLiteCityFile",
LookupService.GEOIP_MEMORY_CACHE);
} catch (Exception exception) {
throw new RuntimeException(
"Error occurs while initializing IpToCountryConverter class : ");
}
}
/**
* This method takes ip address an input and convert it into country name.
*
* @param ip
* @return
*/
public String ipToCountry (String ip) {
Location location = cl.getLocation(ip);
if (location == null) {
return "NA";
}
if (location.countryName == null) {
return "NA";
}
return location.countryName;
}
}
-
现在从
code.google.com/p/ndt/source/browse/branches/applet_91/Applet/src/main/java/edu/internet2/ndt/UserAgentTools.java?r=856下载UserAgentTools类。这个类包含了从用户代理中计算操作系统和浏览器类型的逻辑。你也可以在logprocessing项目中找到UserAgentTools类。 -
让我们在
com.stormadvance.logprocessing包中编写UserInformationGetterBolt类。这个 bolt 使用UserAgentTools和IpToCountryConverter类来计算国家名称、操作系统和浏览器。
/**
* This class use the IpToCountryConverter and UserAgentTools class to calculate
* the country, os and browser from log line.
*
*/
public class UserInformationGetterBolt extends BaseRichBolt {
private static final long serialVersionUID = 1L;
private IpToCountryConverter ipToCountryConverter = null;
private UserAgentTools userAgentTools = null;
public OutputCollector collector;
private String pathTOGeoLiteCityFile;
public UserInformationGetterBolt(String pathTOGeoLiteCityFile) {
// set the path of GeoLiteCity.dat file.
this.pathTOGeoLiteCityFile = pathTOGeoLiteCityFile;
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("ip", "dateTime", "request", "response",
"bytesSent", "referrer", "useragent", "country", "browser",
"os"));
}
public void prepare(Map stormConf, TopologyContext context,
OutputCollector collector) {
this.collector = collector;
this.ipToCountryConverter = new IpToCountryConverter(
this.pathTOGeoLiteCityFile);
this.userAgentTools = new UserAgentTools();
}
public void execute(Tuple input) {
String ip = input.getStringByField("ip").toString();
// calculate the country from ip
Object country = ipToCountryConverter.ipToCountry(ip);
// calculate the browser from useragent.
Object browser = userAgentTools.getBrowser(input.getStringByField(
"useragent").toString())[1];
// calculate the os from useragent.
Object os = userAgentTools.getOS(input.getStringByField("useragent")
.toString())[1];
collector.emit(new Values(input.getString(0), input.getString(1), input
.getString(2), input.getString(3), input.getString(4), input
.getString(5), input.getString(6), country, browser, os));
}
}
UserInformationGetterBolt类的输出包含 10 个字段。这些字段是ip、dateTime、request、response、bytesSent、referrer、useragent、country、browser和os。
计算搜索关键词
本节解释了如何从引荐 URL 计算搜索关键词。假设引荐 URL 是www.google.co.in/#q=learning+storm。我们将把这个引荐 URL 传递给一个类,这个类的输出将是learning storm。通过识别搜索关键词,我们可以轻松地确定用户搜索关键词以到达我们的网站。让我们执行以下步骤来计算引荐 URL 中的关键词:
- 我们在
com.stormadvance.logprocessing包中创建一个KeywordGenerator类。这个类包含从引荐 URL 生成搜索关键词的逻辑。以下是KeywordGenerator类的源代码:
/**
* This class takes referrer URL as input, analyze the URL and return search
* keyword as output.
*
*/
public class KeywordGenerator {
public String getKeyword(String referer) {
String[] temp;
Pattern pat = Pattern.compile("[?&#]q=([^&]+)");
Matcher m = pat.matcher(referer);
if (m.find()) {
String searchTerm = null;
searchTerm = m.group(1);
temp = searchTerm.split("\\+");
searchTerm = temp[0];
for (int i = 1; i < temp.length; i++) {
searchTerm = searchTerm + " " + temp[i];
}
return searchTerm;
} else {
pat = Pattern.compile("[?&#]p=([^&]+)");
m = pat.matcher(referer);
if (m.find()) {
String searchTerm = null;
searchTerm = m.group(1);
temp = searchTerm.split("\\+");
searchTerm = temp[0];
for (int i = 1; i < temp.length; i++) {
searchTerm = searchTerm + " " + temp[i];
}
return searchTerm;
} else {
//
pat = Pattern.compile("[?&#]query=([^&]+)");
m = pat.matcher(referer);
if (m.find()) {
String searchTerm = null;
searchTerm = m.group(1);
temp = searchTerm.split("\\+");
searchTerm = temp[0];
for (int i = 1; i < temp.length; i++) {
searchTerm = searchTerm + " " + temp[i];
}
return searchTerm;
} else {
return "NA";
}
}
}
}
}
-
如果
KeywordGenerator类的输入是:in.search.yahoo.com/search;_ylt=AqH0NZe1hgPCzVap0PdKk7GuitIF?p=india+live+score&toggle=1&cop=mss&ei=UTF-8&fr=yfp-t-704 -
然后,
KeywordGenerator类的输出是:
india live score
- 我们在
com.stormadvance.logprocessing包中创建一个KeyWordIdentifierBolt类。这个类调用KeywordGenerator来从引荐 URL 生成关键词。以下是KeyWordIdentifierBolt类的源代码:
/**
* This class use the KeywordGenerator class to generate the search keyword from
* referrer URL.
*
*/
public class KeyWordIdentifierBolt extends BaseRichBolt {
private static final long serialVersionUID = 1L;
private KeywordGenerator keywordGenerator = null;
public OutputCollector collector;
public KeyWordIdentifierBolt() {
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("ip", "dateTime", "request", "response",
"bytesSent", "referrer", "useragent", "country", "browser",
"os", "keyword"));
}
public void prepare(Map stormConf, TopologyContext context,
OutputCollector collector) {
this.collector = collector;
this.keywordGenerator = new KeywordGenerator();
}
public void execute(Tuple input) {
String referrer = input.getStringByField("referrer").toString();
// call the getKeyword(String referrer) method KeywordGenerator class to
// generate the search keyword.
Object keyword = keywordGenerator.getKeyword(referrer);
// emits all the field emitted by previous bolt + keyword
collector.emit(new Values(input.getString(0), input.getString(1), input
.getString(2), input.getString(3), input.getString(4), input
.getString(5), input.getString(6), input.getString(7), input
.getString(8), input.getString(9), keyword));
}
}
KeyWordIdentifierBolt类的输出包含 11 个字段。这些字段是ip、dateTime、request、response、bytesSent、referrer、useragent、country、browser、os和keyword。
持久化处理数据
本节将解释如何将处理数据持久化到数据存储中。我们在日志处理用例中使用 MySQL 作为数据存储。我假设您已经在您的 centOS 机器上安装了 MySQL,或者您可以按照www.rackspace.com/knowledge_center/article/installing-mysql-server-on-centos上的博客来安装 MySQL。让我们执行以下步骤将记录持久化到 MySQL 中:
- 将以下依赖项添加到
pom.xml:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
- 我们在
com.stormadvance.logprocessing包中创建一个MySQLConnection类。这个类包含getMySQLConnection(String ip, String database, String user, String password)方法,该方法返回 MySQL 连接。以下是MySQLConnection类的源代码:
/**
*
* This class return the MySQL connection.
*/
public class MySQLConnection {
private static Connection connect = null;
/**
* This method return the MySQL connection.
*
* @param ip
* ip of MySQL server
* @param database
* name of database
* @param user
* name of user
* @param password
* password of given user
* @return MySQL connection
*/
public static Connection getMySQLConnection(String ip, String database, String user, String password) {
try {
// this will load the MySQL driver, each DB has its own driver
Class.forName("com.mysql.jdbc.Driver");
// setup the connection with the DB.
connect = DriverManager
.getConnection("jdbc:mysql://"+ip+"/"+database+"?"
+ "user="+user+"&password="+password+"");
return connect;
} catch (Exception e) {
throw new RuntimeException("Error occurs while get mysql connection : ");
}
}
}
- 现在,我们在
com.stormadvance.logprocessing包中创建一个MySQLDump类。这个类有一个带参数的构造函数,它以 MySQL 的服务器 ip、数据库名称、用户和密码作为参数。这个类调用MySQLConnection类的getMySQLConnection(ip,database,user,password)方法来获取 MySQL 连接。MySQLDump类包含persistRecord(Tuple tuple)记录方法,这个方法将输入元组持久化到 MySQL 中。以下是MySQLDump类的源代码:
/**
* This class contains logic to persist record into MySQL database.
*
*/
public class MySQLDump {
/**
* Name of database you want to connect
*/
private String database;
/**
* Name of MySQL user
*/
private String user;
/**
* IP of MySQL server
*/
private String ip;
/**
* Password of MySQL server
*/
private String password;
public MySQLDump(String ip, String database, String user, String password) {
this.ip = ip;
this.database = database;
this.user = user;
this.password = password;
}
/**
* Get the MySQL connection
*/
private Connection connect = MySQLConnection.getMySQLConnection(ip,database,user,password);
private PreparedStatement preparedStatement = null;
/**
* Persist input tuple.
* @param tuple
*/
public void persistRecord(Tuple tuple) {
try {
// preparedStatements can use variables and are more efficient
preparedStatement = connect
.prepareStatement("insert into apachelog values (default, ?, ?, ?,?, ?, ?, ?, ? , ?, ?, ?)");
preparedStatement.setString(1, tuple.getStringByField("ip"));
preparedStatement.setString(2, tuple.getStringByField("dateTime"));
preparedStatement.setString(3, tuple.getStringByField("request"));
preparedStatement.setString(4, tuple.getStringByField("response"));
preparedStatement.setString(5, tuple.getStringByField("bytesSent"));
preparedStatement.setString(6, tuple.getStringByField("referrer"));
preparedStatement.setString(7, tuple.getStringByField("useragent"));
preparedStatement.setString(8, tuple.getStringByField("country"));
preparedStatement.setString(9, tuple.getStringByField("browser"));
preparedStatement.setString(10, tuple.getStringByField("os"));
preparedStatement.setString(11, tuple.getStringByField("keyword"));
// Insert record
preparedStatement.executeUpdate();
} catch (Exception e) {
throw new RuntimeException(
"Error occurs while persisting records in mysql : ");
} finally {
// close prepared statement
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (Exception exception) {
System.out
.println("Error occurs while closing PreparedStatement : ");
}
}
}
}
public void close() {
try {
connect.close();
}catch(Exception exception) {
System.out.println("Error occurs while clossing the connection");
}
}
}
- 让我们在
com.stormadvance.logprocessing包中创建一个PersistenceBolt类。这个类实现了org.apache.storm.topology.IBasicBolt。这个类调用MySQLDump类的persistRecord(Tuple tuple)方法来将记录/事件持久化到 MySQL。以下是PersistenceBolt类的源代码:
/**
* This Bolt call the getConnectionn(....) method of MySQLDump class to persist
* the record into MySQL database.
*
* @author Admin
*
*/
public class PersistenceBolt implements IBasicBolt {
private MySQLDump mySQLDump = null;
private static final long serialVersionUID = 1L;
/**
* Name of database you want to connect
*/
private String database;
/**
* Name of MySQL user
*/
private String user;
/**
* IP of MySQL server
*/
private String ip;
/**
* Password of MySQL server
*/
private String password;
public PersistenceBolt(String ip, String database, String user,
String password) {
this.ip = ip;
this.database = database;
this.user = user;
this.password = password;
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
}
public Map<String, Object> getComponentConfiguration() {
return null;
}
public void prepare(Map stormConf, TopologyContext context) {
// create the instance of MySQLDump(....) class.
mySQLDump = new MySQLDump(ip, database, user, password);
}
/**
* This method call the persistRecord(input) method of MySQLDump class to
* persist record into MySQL.
*/
public void execute(Tuple input, BasicOutputCollector collector) {
System.out.println("Input tuple : " + input);
mySQLDump.persistRecord(input);
}
public void cleanup() {
// Close the connection
mySQLDump.close();
}
}
在本节中,我们已经介绍了如何将输入元组插入数据存储中。
Kafka spout 和定义拓扑
本节将解释如何从 Kafka 主题中读取 Apache 日志。本节还定义了将在前面各节中创建的所有 bolt 链接在一起的LogProcessingTopology。让我们执行以下步骤来消费来自 Kafka 的数据并定义拓扑:
- 在
pom.xml文件中添加以下 Kafka 的依赖和仓库:
<dependency>
<groupId>org.apache.storm</groupId>
<artifactId>storm-kafka</artifactId>
<version>1.0.2</version>
<exclusions>
<exclusion>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.10</artifactId>
<version>0.9.0.1</version>
<exclusions>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
</dependency>
- 在
pom.xml文件中添加以下build插件。这将让我们使用 Maven 执行LogProcessingTopology:
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-
dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass></mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<executable>java</executable>
<includeProjectDependencies>true</includeProjectDependencies>
<includePluginDependencies>false</includePluginDependencies>
<classpathScope>compile</classpathScope>
<mainClass>${main.class}</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>
- 在
com.stormadvance.logprocessing包中创建一个LogProcessingTopology类。该类使用org.apache.storm.topology.TopologyBuilder类来定义拓扑。以下是LogProcessingTopology类的源代码及解释:
public class LogProcessingTopology {
public static void main(String[] args) throws Exception {
// zookeeper hosts for the Kafka cluster
BrokerHosts zkHosts = new ZkHosts("ZK:2183");
// Create the KafkaSpout configuartion
// Second argument is the topic name
// Third argument is the zookeepr root for Kafka
// Fourth argument is consumer group id
SpoutConfig kafkaConfig = new SpoutConfig(zkHosts, "apache_log", "",
"id2");
// Specify that the Kafka messages are String
kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme());
// We want to consume all the first messages in the topic everytime
// we run the topology to help in debugging. In production, this
// property should be false
kafkaConfig.startOffsetTime = kafka.api.OffsetRequest
.EarliestTime();
// Now we create the topology
TopologyBuilder builder = new TopologyBuilder();
// set the Kafka spout class
builder.setSpout("KafkaSpout", new KafkaSpout(kafkaConfig), 2);
// set the LogSplitter, IpToCountry, Keyword and PersistenceBolt bolts
// class.
builder.setBolt("LogSplitter", new ApacheLogSplitterBolt(), 1)
.globalGrouping("KafkaSpout");
builder.setBolt(
"IpToCountry",
new UserInformationGetterBolt(
args[0]), 1)
.globalGrouping("LogSplitter");
builder.setBolt("Keyword", new KeyWordIdentifierBolt(), 1)
.globalGrouping("IpToCountry");
builder.setBolt("PersistenceBolt",
new PersistenceBolt(args[1], args[2], args[3], args[4]),
1).globalGrouping("Keyword");
if (args.length == 6) {
// Run the topology on remote cluster.
Config conf = new Config();
conf.setNumWorkers(4);
try {
StormSubmitter.submitTopology(args[4], conf,
builder.createTopology());
} catch (AlreadyAliveException alreadyAliveException) {
System.out.println(alreadyAliveException);
} catch (InvalidTopologyException invalidTopologyException) {
System.out.println(invalidTopologyException);
}
} else {
// create an instance of LocalCluster class for executing topology
// in local mode.
LocalCluster cluster = new LocalCluster();
Config conf = new Config();
conf.setDebug(true);
// Submit topology for execution
cluster.submitTopology("KafkaToplogy1", conf,
builder.createTopology());
try {
// Wait for sometime before exiting
System.out
.println("**********************Waiting to consume from kafka");
Thread.sleep(100000);
System.out.println("Stopping the sleep thread");
} catch (Exception exception) {
System.out
.println("******************Thread interrupted exception : "
+ exception);
}
// kill the KafkaTopology
cluster.killTopology("KafkaToplogy1");
// shutdown the storm test cluster
cluster.shutdown();
}
}
}
本节介绍了如何将不同类型的 bolt 链接成拓扑。我们还介绍了如何从 Kafka 消费数据。在下一节中,我们将解释如何部署拓扑。
部署拓扑
本节将解释如何部署LogProcessingTopology。执行以下步骤:
- 在 MySQL 控制台上执行以下命令定义数据库架构:
mysql> create database apachelog;
mysql> use apachelog;
mysql> create table apachelog(
id INT NOT NULL AUTO_INCREMENT,
ip VARCHAR(100) NOT NULL,
dateTime VARCHAR(200) NOT NULL,
request VARCHAR(100) NOT NULL,
response VARCHAR(200) NOT NULL,
bytesSent VARCHAR(200) NOT NULL,
referrer VARCHAR(500) NOT NULL,
useragent VARCHAR(500) NOT NULL,
country VARCHAR(200) NOT NULL,
browser VARCHAR(200) NOT NULL,
os VARCHAR(200) NOT NULL,
keyword VARCHAR(200) NOT NULL,
PRIMARY KEY (id)
);
-
我假设您已经通过 Logstash 在
apache_log主题上产生了一些数据。 -
进入项目主目录并运行以下命令构建项目:
> mvn clean install -DskipTests
- 执行以下命令以在本地模式下启动日志处理拓扑:
> java -cp target/logprocessing-0.0.1-SNAPSHOT-jar-with-dependencies.jar:$STORM_HOME/storm-core-0.9.0.1.jar:$STORM_HOME/lib/* com.stormadvance.logprocessing.LogProcessingTopology path/to/GeoLiteCity.dat localhost apachelog root root
- 现在,进入 MySQL 控制台,检查
apachelog表中的行:
mysql> select * from apachelog limit 2
-> ;
+----+----------------+--------------------------+----------------+----------+-----------+-----------------------------------------+-----------------------------------------------------------------------------------------+---------------+----------------+-------+---------+
| id | ip | dateTime | request | response | bytesSent | referrer | useragent | country | browser | os | keyword |
+----+----------------+--------------------------+----------------+----------+-----------+-----------------------------------------+-----------------------------------------------------------------------------------------+---------------+----------------+-------+---------+
| 1 | 24.25.135.19 | 1-01-2011:06:20:31 -0500 | GET / HTTP/1.1 | 200 | 864 | http://www.adeveloper.com/resource.html | Mozilla/5.0 (Windows; U; Windows NT 5.1; hu-HU; rv:1.7.12) Gecko/20050919 Firefox/1.0.7 | United States | Gecko(Firefox) | WinXP | NA |
| 2 | 180.183.50.208 | 1-01-2011:06:20:31 -0500 | GET / HTTP/1.1 | 200 | 864 | http://www.adeveloper.com/resource.html | Mozilla/5.0 (Windows; U; Windows NT 5.1; hu-HU; rv:1.7.12) Gecko/20050919 Firefox/1.0.7 | Thailand | Gecko(Firefox) | WinXP | NA |
+----+----------------+--------------------------+----------------+----------+-----------+-----------------------------------------+-----------------------------------------------------------------------------------------+---------------+----------------+-------+---------+
在本节中,我们介绍了如何部署日志处理拓扑。下一节将解释如何从 MySQL 中存储的数据生成统计信息。
MySQL 查询
本节将解释如何分析或查询存储数据以生成一些统计信息。我们将涵盖以下内容:
-
计算每个国家的页面点击量
-
计算每个浏览器的数量
-
计算每个操作系统的数量
计算每个国家的页面点击量
在 MySQL 控制台上运行以下命令,计算每个国家的页面点击量:
mysql> select country, count(*) from apachelog group by country;
+---------------------------+----------+
| country | count(*) |
+---------------------------+----------+
| Asia/Pacific Region | 9 |
| Belarus | 12 |
| Belgium | 12 |
| Bosnia and Herzegovina | 12 |
| Brazil | 36 |
| Bulgaria | 12 |
| Canada | 218 |
| Europe | 24 |
| France | 44 |
| Germany | 48 |
| Greece | 12 |
| Hungary | 12 |
| India | 144 |
| Indonesia | 60 |
| Iran, Islamic Republic of | 12 |
| Italy | 24 |
| Japan | 12 |
| Malaysia | 12 |
| Mexico | 36 |
| NA | 10 |
| Nepal | 24 |
| Netherlands | 164 |
| Nigeria | 24 |
| Puerto Rico | 72 |
| Russian Federation | 60 |
| Singapore | 165 |
| Spain | 48 |
| Sri Lanka | 12 |
| Switzerland | 7 |
| Taiwan | 12 |
| Thailand | 12 |
| Ukraine | 12 |
| United Kingdom | 48 |
| United States | 5367 |
| Vietnam | 12 |
| Virgin Islands, U.S. | 129 |
+---------------------------+----------+
36 rows in set (0.08 sec)
计算每个浏览器的数量
在 MySQL 控制台上运行以下命令,计算每个浏览器的数量:
mysql> select browser, count(*) from apachelog group by browser;
+----------------+----------+
| browser | count(*) |
+----------------+----------+
| Gecko(Firefox) | 6929 |
+----------------+----------+
1 row in set (0.00 sec)
计算每个操作系统的数量
在 MySQL 控制台上运行以下命令,计算每个操作系统的数量:
mysql> select os,count(*) from apachelog group by os;
+-------+----------+
| os | count(*) |
+-------+----------+
| WinXP | 6929 |
+-------+----------+
1 row in set (0.00 sec)
总结
在本章中,我们向您介绍了如何处理 Apache 日志文件,如何通过分析日志文件识别 IP 的国家名称,如何通过分析日志文件识别用户操作系统和浏览器,以及如何通过分析引荐字段识别搜索关键字。
在下一章中,我们将学习如何通过 Storm 解决机器学习问题。
第十二章:Twitter 推文收集和机器学习
在上一章中,我们介绍了如何使用 Storm 和 Kafka 创建日志处理应用程序。
在本章中,我们将涵盖 Storm 机器学习的另一个重要用例。
本章涵盖的主要主题如下:
-
探索机器学习
-
使用 Kafka 生产者将推文存储在 Kafka 集群中
-
使用 Kafka Spout 从 Kafka 读取数据
-
使用 Storm Bolt 来过滤推文
-
使用 Storm Bolt 来计算推文的情感
-
拓扑的部署
探索机器学习
机器学习是应用计算机科学的一个分支,在这个分支中,我们基于现有的可供分析的数据构建真实世界现象的模型,然后使用该模型,预测模型以前从未见过的数据的某些特征。机器学习已经成为实时应用程序非常重要的组成部分,因为需要实时做出决策。
从图形上看,机器学习的过程可以用以下图示表示:
从数据构建模型的过程在机器学习术语中称为训练。训练可以实时在数据流上进行,也可以在历史数据上进行。当训练实时进行时,模型随着数据的变化而随时间演变。这种学习被称为在线学习,当模型定期更新,通过在新数据集上运行训练算法时,被称为离线学习。
当我们谈论 Storm 上的机器学习时,往往我们谈论的是在线学习算法。
以下是机器学习的一些真实应用:
-
在线广告优化
-
新文章聚类
-
垃圾邮件检测
-
计算机视觉
-
情感分析
Twitter 情感分析
我们将情感用例分为两部分:
-
从 Twitter 收集推文并将其存储在 Kafka 中
-
从 Kafka 读取数据,计算情感,并将其存储在 HDFS 中
使用 Kafka 生产者将推文存储在 Kafka 集群中
在本节中,我们将介绍如何使用 Twitter 流 API 从 Twitter 中获取推文。我们还将介绍如何将获取的推文存储在 Kafka 中,以便通过 Storm 进行后续处理。
我们假设您已经拥有 Twitter 账户,并且为您的应用程序生成了消费者密钥和访问令牌。您可以参考:bdthemes.com/support/knowledge-base/generate-api-key-consumer-token-access-key-twitter-oauth/ 生成消费者密钥和访问令牌。请按照以下步骤进行:
-
使用
groupId为com.stormadvance和artifactId为kafka_producer_twitter创建一个新的 maven 项目。 -
将以下依赖项添加到
pom.xml文件中。我们正在向pom.xml添加 Kafka 和 Twitter 流 Maven 依赖项,以支持 Kafka 生产者和从 Twitter 流式传输推文。
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.10</artifactId>
<version>0.9.0.1</version>
<exclusions>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.0-beta9</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-1.2-api</artifactId>
<version>2.0-beta9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.twitter4j/twitter4j-stream -->
<dependency>
<groupId>org.twitter4j</groupId>
<artifactId>twitter4j-stream</artifactId>
<version>4.0.6</version>
</dependency>
</dependencies>
- 现在,我们需要创建一个名为
TwitterData的类,其中包含从 Twitter 获取/流式传输数据并将其发布到 Kafka 集群的代码。我们假设您已经有一个运行中的 Kafka 集群和在 Kafka 集群中创建的twitterData主题。有关 Kafka 集群的安装和创建 Kafka 主题的信息,请参阅第八章,Storm 和 Kafka 的集成。
该类包含twitter4j.conf.ConfigurationBuilder类的一个实例;我们需要在配置中设置访问令牌和消费者密钥,如源代码中所述。
twitter4j.StatusListener类在onStatus()方法中返回推文的连续流。我们在onStatus()方法中使用 Kafka Producer 代码来发布推文到 Kafka。以下是TwitterData类的源代码:
public class TwitterData {
/** The actual Twitter stream. It's set up to collect raw JSON data */
private TwitterStream twitterStream;
static String consumerKeyStr = "r1wFskT3q";
static String consumerSecretStr = "fBbmp71HKbqalpizIwwwkBpKC";
static String accessTokenStr = "298FPfE16frABXMcRIn7aUSSnNneMEPrUuZ";
static String accessTokenSecretStr = "1LMNZZIfrAimpD004QilV1pH3PYTvM";
public void start() {
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.setOAuthConsumerKey(consumerKeyStr);
cb.setOAuthConsumerSecret(consumerSecretStr);
cb.setOAuthAccessToken(accessTokenStr);
cb.setOAuthAccessTokenSecret(accessTokenSecretStr);
cb.setJSONStoreEnabled(true);
cb.setIncludeEntitiesEnabled(true);
// instance of TwitterStreamFactory
twitterStream = new TwitterStreamFactory(cb.build()).getInstance();
final Producer<String, String> producer = new KafkaProducer<String, String>(
getProducerConfig());
// topicDetails
// new CreateTopic("127.0.0.1:2181").createTopic("twitterData", 2, 1);
/** Twitter listener **/
StatusListener listener = new StatusListener() {
public void onStatus(Status status) {
ProducerRecord<String, String> data = new ProducerRecord<String, String>(
"twitterData", DataObjectFactory.getRawJSON(status));
// send the data to kafka
producer.send(data);
}
public void onException(Exception arg0) {
System.out.println(arg0);
}
public void onDeletionNotice(StatusDeletionNotice arg0) {
}
public void onScrubGeo(long arg0, long arg1) {
}
public void onStallWarning(StallWarning arg0) {
}
public void onTrackLimitationNotice(int arg0) {
}
};
/** Bind the listener **/
twitterStream.addListener(listener);
/** GOGOGO **/
twitterStream.sample();
}
private Properties getProducerConfig() {
Properties props = new Properties();
// List of kafka borkers. Complete list of brokers is not required as
// the producer will auto discover the rest of the brokers.
props.put("bootstrap.servers", "localhost:9092");
props.put("batch.size", 1);
// Serializer used for sending data to kafka. Since we are sending
// string,
// we are using StringSerializer.
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("producer.type", "sync");
return props;
}
public static void main(String[] args) throws InterruptedException {
new TwitterData().start();
}
在执行TwitterData类之前,请使用有效的 Kafka 属性。
在执行上述类之后,用户将在 Kafka 中获得 Twitter 推文的实时流。在下一节中,我们将介绍如何使用 Storm 来计算收集到的推文的情感。
Kafka spout,情感 bolt 和 HDFS bolt
在本节中,我们将编写/配置一个 Kafka spout 来消费来自 Kafka 集群的推文。我们将使用开源的 Storm spout 连接器来从 Kafka 消费数据:
-
使用
groupID为com.stormadvance和artifactId为Kafka_twitter_topology创建一个新的 maven 项目。 -
将以下 maven 依赖项添加到
pom.xml文件中:
<dependencies>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.2.0</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.2.0</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Dependency for Storm-Kafka spout -->
<dependency>
<groupId>org.apache.storm</groupId>
<artifactId>storm-kafka</artifactId>
<version>1.0.2</version>
<exclusions>
<exclusion>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.10</artifactId>
<version>0.9.0.1</version>
<exclusions>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.storm</groupId>
<artifactId>storm-core</artifactId>
<version>1.0.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>clojars.org</id>
<url>http://clojars.org/repo</url>
</repository>
</repositories>
- 在
com.stormadvance.Kafka_twitter_topology.topology包内创建一个StormHDFSTopology类,并添加以下依赖项以指定 Kafka spout 从twitterData主题中消费数据:
BrokerHosts zkHosts = new ZkHosts("localhost:2181");
// Create the KafkaSpout configuartion
// Second argument is the topic name
// Third argument is the zookeeper root for Kafka
// Fourth argument is consumer group id
SpoutConfig kafkaConfig = new SpoutConfig(zkHosts, "twitterData", "",
"id7");
// Specify that the kafka messages are String
kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme());
// We want to consume all the first messages in the topic everytime
// we run the topology to help in debugging. In production, this
// property should be false
kafkaConfig.startOffsetTime = kafka.api.OffsetRequest
.EarliestTime();
// Now we create the topology
TopologyBuilder builder = new TopologyBuilder();
// set the kafka spout class
builder.setSpout("KafkaSpout", new KafkaSpout(kafkaConfig), 1);
- 在
com.stormadvance.Kafka_twitter_topology.bolt包内创建一个JSONParsingBolt类,以从 Twitter 接收的 JSON 推文中提取推文文本:
public class JSONParsingBolt extends BaseRichBolt implements Serializable{
private OutputCollector collector;
public void prepare(Map stormConf, TopologyContext context,
OutputCollector collector) {
this.collector = collector;
}
public void execute(Tuple input) {
try {
String tweet = input.getString(0);
Map<String, Object> map = new ObjectMapper().readValue(tweet, Map.class);
collector.emit("stream1",new Values(tweet));
collector.emit("stream2",new Values(map.get("text")));
this.collector.ack(input);
} catch (Exception exception) {
exception.printStackTrace();
this.collector.fail(input);
}
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declareStream("stream1",new Fields("tweet"));
declarer.declareStream("stream2",new Fields("text"));
}
}
- 在
com.stormadvance.Kafka_twitter_topology.sentiments包内创建一个SentimentBolt类,以创建每条推文的情感。我们使用字典文件来查找推文中使用的词语是积极的还是消极的,并计算整条推文的情感。以下是该类的源代码:
public final class SentimentBolt extends BaseRichBolt {
private static final Logger LOGGER = LoggerFactory
.getLogger(SentimentBolt.class);
private static final long serialVersionUID = -5094673458112825122L;
private OutputCollector collector;
private String path;
public SentimentBolt(String path) {
this.path = path;
}
private Map<String, Integer> afinnSentimentMap = new HashMap<String, Integer>();
public final void prepare(final Map map,
final TopologyContext topologyContext,
final OutputCollector collector) {
this.collector = collector;
// Bolt will read the AFINN Sentiment file [which is in the classpath]
// and stores the key, value pairs to a Map.
try {
BufferedReader br = new BufferedReader(new FileReader(path));
String line;
while ((line = br.readLine()) != null) {
String[] tabSplit = line.split("\t");
afinnSentimentMap.put(tabSplit[0],
Integer.parseInt(tabSplit[1]));
}
br.close();
} catch (final IOException ioException) {
LOGGER.error(ioException.getMessage(), ioException);
ioException.printStackTrace();
System.exit(1);
}
}
public final void declareOutputFields(
final OutputFieldsDeclarer outputFieldsDeclarer) {
outputFieldsDeclarer.declare(new Fields("tweet","sentiment"));
}
public final void execute(final Tuple input) {
try {
final String tweet = (String) input.getValueByField("text");
final int sentimentCurrentTweet = getSentimentOfTweet(tweet);
collector.emit(new Values(tweet,sentimentCurrentTweet));
this.collector.ack(input);
}catch(Exception exception) {
exception.printStackTrace();
this.collector.fail(input);
}
}
/**
* Gets the sentiment of the current tweet.
*
* @param status
* -- Status Object.
* @return sentiment of the current tweet.
*/
private final int getSentimentOfTweet(final String text) {
// Remove all punctuation and new line chars in the tweet.
final String tweet = text.replaceAll("\\p{Punct}|\\n", " ")
.toLowerCase();
// Splitting the tweet on empty space.
final Iterable<String> words = Splitter.on(' ').trimResults()
.omitEmptyStrings().split(tweet);
int sentimentOfCurrentTweet = 0;
// Loop thru all the wordsd and find the sentiment of this tweet.
for (final String word : words) {
if (afinnSentimentMap.containsKey(word)) {
sentimentOfCurrentTweet += afinnSentimentMap.get(word);
}
}
LOGGER.debug("Tweet : Sentiment {} ==> {}", tweet,
sentimentOfCurrentTweet);
return sentimentOfCurrentTweet;
}
}
- 我们需要将情感存储在 HDFS 中以生成图表或特征分析。接下来,在
StormHDFSTopology类中添加以下代码以链接 spout 和 bolts:
// use "|" instead of "," for field delimiter
RecordFormat format = new DelimitedRecordFormat()
.withFieldDelimiter(",");
// sync the filesystem after every 1k tuples
SyncPolicy syncPolicy = new CountSyncPolicy(1000);
// rotate files when they reach 5MB
FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(5.0f,
Units.MB);
FileNameFormat fileNameFormatSentiment = new DefaultFileNameFormat()
.withPath("/sentiment-tweet/");
HdfsBolt hdfsBolt2 = new HdfsBolt().withFsUrl("hdfs://127.0.0.1:8020")
.withFileNameFormat(fileNameFormatSentiment).withRecordFormat(format)
.withRotationPolicy(rotationPolicy).withSyncPolicy(syncPolicy);
//builder.setBolt("HDFSBolt", hdfsBolt).shuffleGrouping("KafkaSpout");
builder.setBolt("json", new JSONParsingBolt()).shuffleGrouping("KafkaSpout");
//
builder.setBolt("sentiment", new SentimentBolt("/home/centos/Desktop/workspace/storm_twitter/src/main/resources/AFINN-111.txt")).shuffleGrouping("json","stream2");
//
builder.setBolt("HDFS2", hdfsBolt2).shuffleGrouping("sentiment");
- 以下是
StormHDFSTopology类的完整代码:
public class StormHDFSTopology {
public static void main(String[] args) {
// zookeeper hosts for the Kafka cluster
BrokerHosts zkHosts = new ZkHosts("localhost:2181");
// Create the KafkaSpout configuartion
// Second argument is the topic name
// Third argument is the zookeeper root for Kafka
// Fourth argument is consumer group id
SpoutConfig kafkaConfig = new SpoutConfig(zkHosts, "twitterData", "",
"id7");
// Specify that the kafka messages are String
kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme());
// We want to consume all the first messages in the topic everytime
// we run the topology to help in debugging. In production, this
// property should be false
kafkaConfig.startOffsetTime = kafka.api.OffsetRequest
.EarliestTime();
// Now we create the topology
TopologyBuilder builder = new TopologyBuilder();
// set the kafka spout class
builder.setSpout("KafkaSpout", new KafkaSpout(kafkaConfig), 1);
// use "|" instead of "," for field delimiter
RecordFormat format = new DelimitedRecordFormat()
.withFieldDelimiter(",");
// sync the filesystem after every 1k tuples
SyncPolicy syncPolicy = new CountSyncPolicy(1000);
// rotate files when they reach 5MB
FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(5.0f,
Units.MB);
FileNameFormat fileNameFormatSentiment = new DefaultFileNameFormat()
.withPath("/sentiment-tweet/");
HdfsBolt hdfsBolt2 = new HdfsBolt().withFsUrl("hdfs://127.0.0.1:8020")
.withFileNameFormat(fileNameFormatSentiment).withRecordFormat(format)
.withRotationPolicy(rotationPolicy).withSyncPolicy(syncPolicy);
//builder.setBolt("HDFSBolt", hdfsBolt).shuffleGrouping("KafkaSpout");
builder.setBolt("json", new JSONParsingBolt()).shuffleGrouping("KafkaSpout");
//
builder.setBolt("sentiment", new SentimentBolt("/home/centos/Desktop/workspace/storm_twitter/src/main/resources/AFINN-111.txt")).shuffleGrouping("json","stream2");
//
builder.setBolt("HDFS2", hdfsBolt2).shuffleGrouping("sentiment");
// create an instance of LocalCluster class for executing topology in
// local mode.
LocalCluster cluster = new LocalCluster();
Config conf = new Config();
// Submit topology for execution
cluster.submitTopology("KafkaToplogy", conf, builder.createTopology());
try {
// Wait for some time before exiting
System.out.println("Waiting to consume from kafka");
Thread.sleep(6000000);
} catch (Exception exception) {
System.out.println("Thread interrupted exception : " + exception);
}
// kill the KafkaTopology
cluster.killTopology("KafkaToplogy");
// shut down the storm test cluster
cluster.shutdown();
}
}
- 现在,我们可以为整个项目创建 JAR 并根据本书中的第二章,Storm 部署、拓扑开发和拓扑选项中定义的方式部署到 Storm 集群。
总结
在本节中,我们介绍了如何使用 Twitter 流 API 读取 Twitter 推文,如何处理推文以计算输入 JSON 记录中的推文文本,计算推文的情感,并将最终输出存储在 HDFS 中。
通过这一点,我们来到了本书的结尾。在本书的过程中,我们已经从开始使用 Apache Storm 迈出了一大步,发展成为了真实世界应用程序的开发者。在这里,我们想总结一下我们所学到的一切。
我们向您介绍了 Storm 的基本概念和组件,并介绍了如何在本地和集群模式下编写和部署/运行拓扑。我们还介绍了 Storm 的基本命令,并介绍了如何在运行时修改 Storm 拓扑的并行性。我们还专门介绍了监控 Storm 的整个章节,这在开发过程中经常被忽视,但是对于任何生产环境来说都是至关重要的部分。您还了解了 Trident,这是低级 Storm API 的抽象,可用于开发更复杂的拓扑并维护应用程序状态。
没有任何企业应用程序可以仅使用一种技术开发,因此我们的下一步是看看如何将 Storm 与其他大数据工具和技术集成。我们看到了 Storm 与 Kafka、Hadoop、HBase 和 Redis 的特定实现。大多数大数据应用程序使用 Ganglia 作为集中监控工具,因此我们还介绍了如何通过 JMX 和 Ganglia 监控 Storm 集群。
你还学习了有关使用各种模式将不同数据源与 Storm 集成的知识。最后,在第十一章《使用 Storm 进行 Apache 日志处理》和本章中,我们实施了两个 Apache Storm 中的案例研究,这可以作为开发更复杂应用程序的起点。
我们希望阅读本书对你来说是一次富有成效的旅程,并且你对 Storm 以及一般实时流处理应用程序开发的各个方面有了基本的了解。Apache Storm 正在成为流处理的事实标准,我们希望本书能够成为你启动构建实时流处理应用程序的激动人心旅程的催化剂。