一、项目内容深度分析
1. 项目内容概览
- 电信大数据通话信息实时读写定位系统,即是为了解决实时产生的海量通话记录的实时读写与随机定位的问题,该系统涵盖了数据的产生,数据的收集,数据的处理及入库,内容包括以下四个模块:
- 电话通话记录的产生模块
- 电话通话记录的收集模块
- 电话通话记录的处理及入库模块
- 读写定位系统的前端平台模块
2.数据的大致流向分析
- 首先由电话通话记录的产生模块源源不断地产生数据,模拟在交换机上源源不断产生的电话通话日志,这些日志会被追加存储在一个特定的文件中,然后启动Flume监控该文件进行收集,收集后的通话记录数据将会进入Kafa中进行缓存,缓存后的电话通话记录会被HBase进行消费,此步会对数据进行处理,包括RowKey的生成及盐析处理避免热点问题等,将处理后的数据插入HBase的表,然后会在Hive中建立一张映射HBase的外部表,因为HBase是NoSQL,无法通过SQL语句进行查询信息,但是经过Hive的建模则可以直接通过类SQL语句进行查询信息了,然后开启hiveserver2服务,最后构建SSM平台,连接hiveserver2服务进行交互。
3. 涉及的知识难点分析
- HBase中RowKey的设计。这是本项目非常关键的一个步骤,假设现在HBase中用以存储通话记录的表有100个region,这些region分布在整个HBase的集群中,现在存在一个问题,当源源不断的通话记录被插入到HBase中若是不对RowKey进行设计则很有可能造成热点问题,使得某些RegionServer被高频访问,造成集群资源利用不充分。其次也不能随机将通话记录放入到各个region中,这样的话,同一个人同一时间段的通话记录被打散,查询起来就会跨多个region,效率低下。因此RowKey的设计是一个难点。
- 被叫记录的实时插入。当产生一个主叫记录时也必然会产生一个被叫记录,但是数据的收集处只有主叫记录,因此需要在插入一条主叫信息时实时插入一条被叫的通话记录,这就需要使用HBase的协处理器来完成,在检测到是通话记录表有插入操作且是主叫插入时会触发插入一条对应的被叫通话记录。在查询某个用户的通话记录时,应该要把该用户的主叫及被叫都查询出来。
- 合理在Hive中建立HBase的外部表。在外部执行某些条件查询时,使用HBase不好查询,因为HBase是非关系型数据库,因此不能使用SQL语句进行查询,需要使用Hive对HBase的通话记录表建模,创建一张外部表关联HBase的表,然后使用Hive进行查询。
- 本次项目会涉及HBase的过滤器的使用、协处理器的设计与使用、RowKey的设计等三大HBase的经典需求,以及Hive与HBase的交互,如何建立外部表关联HBase的表以及如何通过启动hivserver2这个交互式查询引擎与web服务对接进行实时查询。
二、项目所用到的框架清单
-
要完成该项目,需要确保你的虚拟机(或服务器)已经安装了以下大数据框架:
- Hadoop集群,本次演示使用的Hadoop版本是hadoop-2.6.0-cdh5.7.0
- HBase集群,本次演示使用的HBase版本是hbase-1.2.0-cdh5.7.0
- ZooKeeper集群,本次演示使用的Zookeeper版本是zookeeper-3.4.5-cdh5.7.0
- Kafka集群,本次演示使用的Kafka版本是kafka_2.11-1.0.0
- Flume,本次演示使用的Flume版本是apache-flume-1.9.0-bin
- Hive,本次演示使用的Hive版本是apache-hive-2.1.0-bin
前端构建查询系统使用了SSM框架:
- Spring,本次演示使用的Spring版本是4.3.3.RELEASE
- Spring MVC,本次演示使用的Spring MVC版本是4.3.3.RELEASE
- MyBatis,本次演示使用的MyBatis版本是3.2.1
- MySQL,本次演示使用的MySQL版本是5.6.26
本次项目使用IEDA2018作为开发工具。
三、项目实战代码
1. 后端开发
-
1. 构建工程项目模块
打开IDEA创建普通的Java工程,取名为CallLogsSystem,并添加Maven支持,然后在本工程下添加新的模块:
按照以上步骤,依次添加CallLogGenModule、CallLogWeb、CallCoprosser、KafkaConsumerModule四个模块,添加完成的模块如图:
-
2.开发通话记录生成模块
进入CallLogGenModule模块,添加Maven支持,目的是为了方便使用Maven自带的打包工具进行打包。在该模块的src下创建如下目录路径:
在包com.project.callloggenmodule下创建APP类,该类用于生成通话记录,这些通话 日志是最原始的记录:
APP类代码如下:/** * 主呼叫记录生成类 * 模拟一个主动呼叫的电话 */ public class App { //初始化工作 public static Map<String,String> allCallers = new HashMap(); public static List<String> phoneNumber = new ArrayList(); static { //加载所有用户号码和姓名信息 allCallers.put("15810092493", "萧邦主"); allCallers.put("18000696806", "赵贺彪"); allCallers.put("15151889601", "张倩"); allCallers.put("13269361119", "王世昌"); allCallers.put("15032293356", "张涛"); allCallers.put("17731088562", "张阳"); allCallers.put("15338595369", "李进全"); allCallers.put("15733218050", "杜泽文"); allCallers.put("15614201525", "任宗阳"); allCallers.put("15778423030", "梁鹏"); allCallers.put("18641241020", "郭美彤"); allCallers.put("15732648446", "刘飞飞"); allCallers.put("13341109505", "段光星"); allCallers.put("13560190665", "唐会华"); allCallers.put("18301589432", "杨力谋"); allCallers.put("13520404983", "温海英"); allCallers.put("18332562075", "朱尚宽"); allCallers.put("15902136987", "张翔"); allCallers.put("13801358247", "杨超凡"); allCallers.put("15975500987", "何潮辉"); allCallers.put("13013685036", "庄银泳"); allCallers.put("15019933667", "萧金辉"); allCallers.put("18301930136", "黄海锋"); //加载所有用户号码到此集合 phoneNumber.addAll(allCallers.keySet()); } public static void main(String[] args) throws Exception{ while (true){ if(args.length != 1){ System.err.println("You should input a path to save the logs"); System.exit(1); } // 第一个参数即是存储电话号码的文件 genCallLog(args[0]); } } public static void genCallLog (String logFile) throws Exception{ Random r = new Random(); //文件输出流,追加写入文件 FileWriter writer = new FileWriter(logFile,true); //产生一个主叫 String callerNumber = phoneNumber.get(r.nextInt(phoneNumber.size())); String callerName = allCallers.get(callerNumber); String calleeNumber; String calleeName; //产生一个被叫 while (true) { calleeNumber = phoneNumber.get(r.nextInt(phoneNumber.size())); //主叫与被叫默认不为同一个人 if (!calleeNumber.equals(callerNumber)) { calleeName = allCallers.get(calleeNumber); break; } } //通话时间生成 int year = r.nextInt(2) == 0 ? 2018 : 2019; int month = r.nextInt(12); int day = r.nextInt(getDay(month)) + 1; int hour = r.nextInt(24); int min = r.nextInt(60); int sec = r.nextInt(60); //使用日期类格式化时间 Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.YEAR, year); calendar.set(Calendar.MONTH, month); calendar.set(Calendar.DAY_OF_MONTH, day); calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, min); calendar.set(Calendar.SECOND, sec); Date timeDate = calendar.getTime(); SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyy/MM/dd hh:mm:ss"); String time = sdf.format(timeDate); //通话时长 DecimalFormat df = new DecimalFormat(); df.applyPattern("000"); // 默认通话时间范围[0,1800)秒 int dur = r.nextInt(60 * 30); String duration = df.format(dur); //通话记录 String log = callerNumber + "," + calleeNumber + "," + time + "," + duration; //将通话记录写入到外部存储系统 writer.write(log + "\r\n"); writer.flush(); //休眠200ms Thread.sleep(200); } // 根据月份返回该月最多的天数 public static int getDay(int month){ if(month == 2) return 28; if(month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) return 31; return 30; } }
-
3.开发通话记录收集模块
该模块是本项目核心项目之一,对RowKey的设计在本模块中完成。开发本模块之前,首先需要在HBase中建立一张表存储通话记录:
进入HBase的终端内,执行命令create 'ns1:calllog','f1'
,在名字空间ns1中创建一张calllog表,列族为f1。
然后进入KafkaConsumerModule模块下,并为该目录添加Maven支持,本模块完整的Maven依赖如下:
在resources目录下创建project.properties文件,该文件是一个该模块下的全局配置文件,用于该模块对Kafka,HBase等的配置:<repositories> <repository> <id>cloudera</id> <url>https://repository.cloudera.com/artifactory/cloudera-repos</url> </repository> </repositories> <dependencies> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka_2.11</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-client</artifactId> <version>1.2.0-cdh5.7.0</version> </dependency> </dependencies> <build> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.0.0</version> </plugin> <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging --> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.20.1</version> </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> </plugins> </pluginManagement> <!-- <sourceDirectory>src/main/scala</sourceDirectory> <testSourceDirectory>src/test/scala</testSourceDirectory>--> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-eclipse-plugin</artifactId> <configuration> <downloadSources>true</downloadSources> <buildcommands> <buildcommand>ch.epfl.lamp.sdt.core.scalabuilder</buildcommand> </buildcommands> <additionalProjectnatures> <projectnature>ch.epfl.lamp.sdt.core.scalanature</projectnature> </additionalProjectnatures> <classpathContainers> <classpathContainer>org.eclipse.jdt.launching.JRE_CONTAINER</classpathContainer> <classpathContainer>ch.epfl.lamp.sdt.launching.SCALA_CONTAINER</classpathContainer> </classpathContainers> </configuration> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> </build>
project.properties内容如下,注意要将某些配置如集群的IP等改为符合你的配置:
在java目录下创建类PropertiesUtil,用于解析配置文件,代码如下:#----- kafka配置 ----# #zk服务器 zookeeper.connect=hadoop00:2181 #消费者组 group.id=g1 zookeeper.session.timeout.ms=500 zookeeper.sync.time.ms=250 auto.commit.interval.ms=1000 #消费偏移量,从头消费smallest auto.offset.reset=smallest #主题 kafka.topic=calllog #----- HBase配置 ------# hbase.zookeeper.quorum=hadoop00:2181 hbase.tablename=ns1:calllog partition.num=100 #---- 其他配置 ----# #呼叫标志位,1代表主叫,0代表被叫 callerflag=1 calleeflag=0
接着在java目录下创建类HbaseDao用于处理通话记录,主要负责三个工作:计算该通话记录存储的region,产生RowKey,数据入库。HbaseDao代码如下:/** * 配置文件工具类 */ public class PropertiesUtil { //单例模式 private PropertiesUtil(){ } //静态配置对象 public static Properties props = null; //初始化对象 static { InputStream in = ClassLoader.getSystemResourceAsStream("project.properties"); props = new Properties(); try { props.load(in); } catch (IOException e) { e.printStackTrace(); } } //获得配置属性 public static String getProperty(String key){ return props.getProperty(key); } }
/** * 访问Hbase */ public class HbaseDao { private Table table = null; private TableName tableName = null; // 是否是主叫标志位 private String flag = null; // HBase的分区个数 private int partitonsNum = 0; //初始化 public HbaseDao(){ Configuration conf = HBaseConfiguration.create(); conf.set("hbase.zookeeper.quorum",PropertiesUtil.getProperty("hbase.zookeeper.quorum")); try { Connection con = ConnectionFactory.createConnection(conf); tableName = TableName.valueOf(PropertiesUtil.getProperty("hbase.tablename")); table = con.getTable(tableName); flag = PropertiesUtil.getProperty("callerflag"); partitonsNum = Integer.parseInt(PropertiesUtil.getProperty("partition.num")); } catch (IOException e) { e.printStackTrace(); } } //将通话记录插入到HBase,设计RowKey public void put(String log){ String[] arr = log.split(","); String caller = arr[0]; String callee = arr[1]; String callDate = arr[2]; //对日期格式化 String callDateFormat = callDate.replace("/",""); //删除/ callDateFormat = callDateFormat.replace(" ",""); //删除空格 callDateFormat = callDateFormat.replace(":",""); //删除: String duration = arr[3]; // 根据主叫号码以及呼叫日期生成哈希码 // 可以确保同一个用户的同一个月的通话记录会被放在同一个region中 // 不同用户或者同一用户的不同月份的通话记录尽可能发散存储,避免出现热点 String hashCode = getHashCode(caller,callDateFormat); // 拼接HBase的RowKey String rowKey = hashCode + "," + caller + "," + callDateFormat + "," + flag + "," + callee +