Spark Streaming+kafka+spring boot+elasticsearch實時項目(canal)

在本次實驗中,利用spark、elasticsearch、kafka等相關框架搭建一個實時計算系統。

具體流程如下圖所示,

  • 用戶訪問對應服務,由nginx服務器進行負載均衡訪問具體的主機上的服務,訪問過程中將產生用戶具體的操作日誌,該操作日誌將由具體服務發送保存到Kafka集羣(或者可以寫到具體文件,可以通過Flume對日誌文件進行採集,發送到Kafka集羣)。
  • 數據緩存到kafka集羣后,利用Spark Streaming對Kafka進行具體時間間隔的消費(批處理),對消費的數據進行業務去重,計算,加工,完成後,將數據寫到Mysql數據庫或者ES(用於對數據的檢索和分析)。
  • 數據保存到ES後,編寫Spring boot程序,將es中數據讀取,並按照一定的業務邏輯進行處理,將需求數據以json格式返回。在本次實驗中,編寫的改spring boot程序主要用於發佈接口,由另外一個前端程序請求該接口,返回相應數據,當然也可以寫到一個web工程中,本次例程中主要是偏向於基礎。
  • 另外一個web工程訪問具體業務接口,返回json數據,解析響應數據,利用echart.js繪製相應圖表,並設置時間間隔進行請求,實時更新圖表內容。

注:另外,還可以通過canal監控對應的業務數據,對更改的業務數據進行抓取,發送給kafka。主要利用的是mysql的主從備份的原理,將canal僞裝成一臺mysql slave服務器,從主節點請求數據。
在這裏插入圖片描述

一、環境搭建

集羣搭建可以參考

三臺虛擬機,分別爲hadoop1、hadoop2、hadoop3,本次例程中使用的是centos 6.8。

分配的內存爲:(當然內存足夠可以多分配)

主機 內存 處理器
hadoop1 4G 2
hadoop2 2G 1
hadoop3 2G 1
  • hadoop集羣,(可選,方便查看具體job 日誌)hadoop版本 hadoop-2.7.2
  • zookeeper集羣,版本:zookeeper-3.4.10
  • kafka集羣,版本 kafka_2.11-0.11.0.2
  • spark集羣(可選),版本spark-2.1.1-bin-hadoop2.7 將項目部署到集羣上可以考慮搭建spark集羣,測試則不需要,在idea測試即可。
  • elasticsearch集羣,版本 elasticsearch-6.6.0 ,可以再安裝一個es的可視化平臺,kibana 版本kibana-6.6.0-linux-x86_64
  • redis 可單機可集羣,版本redis-5.0.6
  • nginx

二、項目搭建

如下圖所示,爲本項目的功能文件目錄結構。

  • canal模塊爲利用canal API將mysql數據庫修改的數據發送到kafka集羣。
  • common模塊 是公用的依賴和工具類。
  • dw-chart模塊是web項目,負責向對應接口請求數據,並繪製圖表,前端展示。
  • export2ES模塊 (可忽略),將hive數據導入到es。
  • logger模塊,是用戶請求的對應服務的spring boot工程,負責將用戶操作日誌發送給kafka。
  • mock模塊,是模擬用戶操作日誌,負責向logger模塊發起請求。
  • publisher模塊,spring boot功能,負責發佈訪問接口,由dw-chart請求相應數據。
  • realtime模塊,spark streaming計算,負責消費kafka數據,並保存到es中。
  • sql文件夾中 是對應的order_info 模擬生成數據的存儲過程和部分模擬數據,用於cannal監控,和統計銷售額。

在這裏插入圖片描述

三、分析過程

  • kafka集羣中topic有以下三個,GMALL_STARTUP(用於統計每日活躍度)、GMALL_EVENT(暫時未使用)、GMALL_ORDER(用於統計銷售額)。
  • es集羣中index有以下三個,gmall_dau(保存計算每日活躍度的結果數據)、gmall_order(保存計算後的銷售額數據)、gmall_sale_detail(保存從hive中導入到es的數據)。

日誌數據格式如下,一條json數據表示用戶做的一次操作,當type爲startup爲登錄,可以記錄當前app的每日活躍度。

{
    "area": "guangdong",   //地址
    "uid": "186",          
    "itemid": 17,          //主題id
    "npgid": 14,
    "evid": "addCart",     //時間id
    "os": "andriod",       //用戶操作系統
    "pgid": 43,
    "appid": "gmall_hcx",    //appid
    "mid": "mid_74",         //用戶唯一id
    "type": "event",         //用戶操作類型
    "ts": 1574325528404      //時間戳
}

mysql中的order_info表中數據如下,記錄着用戶下單產生的業務數據,由canal監控mysql數據庫的這個表的變化,並將數據寫入kafka集羣中,便於之後統計銷售額。

在這裏插入圖片描述

以下爲spark streaming代碼,進行每日活躍度的統計。首先從kafka中讀取數據爲inputDstream,再將輸入流轉換爲泛型爲具體樣例類的輸入流。利用redis對數據進行去重,因爲統計用戶活躍度,當一個用戶多次登錄後,只取這個用戶的一次有效登錄記錄。利用redis去重後,還需要考慮到當一個批次讀取的數據中有重複數據時,redis未能去重,則需要再對過濾後的數據進一步去重,去重思路是將想用mid的數據分爲同一組,即一個用戶的登錄記錄分爲一組,只取其中一條作爲有效數據,其餘的去除。

val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("dau_app")
    val ssc = new StreamingContext(sparkConf,Seconds(5))

    val inputDstream: InputDStream[ConsumerRecord[String, String]] = MyKafkaUtil.getKafkaStream(GmallConstant.KAFKA_TOPIC_STARTUP,ssc)

    //轉換操作
    val startuplogStream: DStream[Startuplog] = inputDstream.map {
      record =>
        val jsonStr: String = record.value()
        val startuplog: Startuplog = JSON.parseObject(jsonStr, classOf[Startuplog])
        val date = new Date(startuplog.ts)
        val DateStr: String = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(date)
        val splits: Array[String] = DateStr.split(" ")
        startuplog.logDate = splits(0)
        startuplog.logHour = splits(1).split(":")(0)
        startuplog.logHourMinute = splits(1)
        startuplog
    }
    //利用redis進行去重過濾
    val filteredDstream: DStream[Startuplog] = startuplogStream.transform {
      rdd =>
        //driver  週期性執行
        val curdate: String = new SimpleDateFormat("yyyy-MM-dd").format(new Date())
        val jedis: Jedis = RedisUtil.getJedisClient
        val key = "dau:" + curdate
        val dauSet: util.Set[String] = jedis.smembers(key)
        val dauBC: Broadcast[util.Set[String]] = ssc.sparkContext.broadcast(dauSet)
        val filteredRDD: RDD[Startuplog] = rdd.filter {
          startuplog =>
            //executor
            val dauSet: util.Set[String] = dauBC.value
            !dauSet.contains(startuplog.mid)
        }
        filteredRDD
    }
    val groupbyMidDstram: DStream[(String, Iterable[Startuplog])] = filteredDstream.map {
      startiplog => (startiplog.mid, startiplog)
    }.groupByKey()
    //去重思路,把相同mid的數據分成一組,每組取一個
    val distinctDstream: DStream[Startuplog] = groupbyMidDstram.flatMap {
      case (mid, startuplogItr) =>
        startuplogItr.take(1)
    }
    //保存到redis中
    distinctDstream.foreachRDD{rdd=>
      //driver
      //redis   type  set
      //key  dau:2019-06-03  value:mids
      rdd.foreachPartition{startuplogItr =>
        //executor
        val jedis: Jedis = RedisUtil.getJedisClient
        val list: List[Startuplog] = startuplogItr.toList
        for (startuplog<- list){
          val key = "dau:" + startuplog.logDate
          val value = startuplog.mid
          jedis.sadd(key,value)
          println(startuplog)
        }
        MyEsUtil.indexBulk(GmallConstant.ES_INDEX_DAU,list)
        jedis.close()
      }
    }
    ssc.start()
    ssc.awaitTermination()

以下爲canal API 部分代碼,負責監聽mysql數據庫的order_info表的數據變化,將改變後的數據發送到kafka集羣。

CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress("hadoop1", 11111), "example", "", "");
        while (true){
            //連接、訂閱表、獲取數據
            canalConnector.connect();
            canalConnector.subscribe("gmall.order_info");
            Message message = canalConnector.get(100);
            int size = message.getEntries().size();
            if (size == 0){
                try {
                    System.out.println("no Data...");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                for (CanalEntry.Entry entry : message.getEntries()) {

                    //判斷時間類型,只處理行變化業務
                    if (entry.getEntryType().equals(CanalEntry.EntryType.ROWDATA)){
                        //將數據集進行反序列化
                        ByteString storeValue = entry.getStoreValue();
                        CanalEntry.RowChange rowChange = null;
                        try {
                             rowChange = CanalEntry.RowChange.parseFrom(storeValue);

                        } catch (InvalidProtocolBufferException e) {
                            e.printStackTrace();
                        }
                        // 獲取行集
                        List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
                        //操作類
                        CanalEntry.EventType eventType = rowChange.getEventType();
                        //表名
                        String tableName = entry.getHeader().getTableName();
                        CanalHandler.handle(tableName,eventType,rowDatasList);
                    }
                }
            }
        }

四、項目運行

1、首先啓動zookeeper集羣和kafka集羣、nginx。

nginx配置文件內容如下:

#user  nobody;
worker_processes  1;
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#pid        logs/nginx.pid;
events {
    worker_connections  1024;
}
http {
    upstream logserver{
        server   hadoop1:8080  weight=1;
        server   hadoop2:8080  weight=1;
        server   hadoop3:8080  weight=1;
}
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    #tcp_nopush     on;
    #keepalive_timeout  0;
    keepalive_timeout  65;
    #gzip  on;

    server {
        listen       80;
        server_name  logserver;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;
        location / {
            root   html;
            index  index.html index.htm;
            proxy_pass http://logserver;
            proxy_connect_timeout 10;
        }
        #error_page  404              /404.html;
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

zookeeper配置文件內容如下:

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.

server.1=hadoop1:2888:3888
server.2=hadoop2:2888:3888
server.3=hadoop3:2888:3888
dataDir=/home/hadoop/zookeeper-3.4.10/zkData
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the 
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

kafka集羣配置主節點配置文件內容如下:(slave節點的配置文件內容也需要修改,具體可參考網上內容)

boker.id=0
zookeeper.connect=hadoop1:2181,hadoop2:2181,hadoop3:2181
listeners=PLAINTEXT://hadoop1:9092
advertised.listeners=PLAINTEXT://hadoop1:9092
delete.topic.enable=true   #用於刪除topic

2、將logger模塊打包上傳到三臺虛擬機,並每臺都啓動。

可以在hadoop1主機編寫一個啓動腳本來啓動三臺主機的服務。腳本內容如下,具體需要修改java路徑和jar包路徑。

#!/bin/bash
JAVA_BIN=/home/hadoop/jdk1.8/bin/java
PROJECT=gmall
APPNAME=logger-0.0.1-SNAPSHOT.jar
SERVER_PORT=8080

case $1 in
"start")
{
  for i in hadoop1 hadoop2 hadoop3
  do
  echo "=======啓動日誌服務:$i"
  ssh $i "$JAVA_BIN -Xms32m -Xmx64m -jar gmall/$APPNAME --server.port=$SERVER_PORT >/home/hadoop/gmall/boot.log 2>&1 &"
  done
};;

"stop")
{
 for i in hadoop1 hadoop2 hadoop3
  do 
   echo "=========關閉日誌服務:$i=========="
   ssh $i "ps -ef | grep $APPNAME | grep -v grep | awk '{print \$2}' |xargs kill" >/dev/null 2>&1 &
  done
};;

esac

3、啓動JsonMocker程序,發送請求到nginx服務器,訪問三臺主機的具體服務,並將日誌保存到kafka集羣中。(可以直接在IDEA中啓動,發送請求,看到終端輸出200返回結果,並kafka對應topic有數據即成功)

在這裏插入圖片描述

4、啓動spark streaming程序 DauApp,從kafka讀取數據進行計算處理,並將結果保存到es中。(可以直接在IDEA中啓動,通過查看es-head或kibana查詢有數據 來查看,如果有數據即成功)

在這裏插入圖片描述

5、啓動發佈接口spring-boot程序,讀取es中數據,按照對應的業務邏輯處理數據,並以json形式返回。(可以在IDEA中啓動,也可以打包部署到集羣,瀏覽器訪問對應接口地址,返回json數據即成功)

在這裏插入圖片描述

6、啓動前端展示web項目,通過請求對應接口,得到返回的json數據,將數據解析後利用echart繪製圖表。(可以在IDEA中啓動,也可以打包部署到集羣,瀏覽器輸入地址後,看到對應圖表,並且圖表按照規律時間變化及成功)

在這裏插入圖片描述

效果圖:(每日活躍度完成顯示,顯示昨天和今天兩天的對比圖)

在這裏插入圖片描述

7、銷售額統計部分,首先需要配置canal,監聽對應的mysql,canal的配置文件內容如下,啓動canal bin/startup.sh

conf/example/instance.properties 主要配置slaveId和mysql地址,還有canal的用戶和密碼,這個需要在mysql中配置一個用戶和密碼,用於canal操作mysql中的表。

#################################################
## mysql serverId , v1.0.26+ will autoGen 
canal.instance.mysql.slaveId=3

# enable gtid use true/false
canal.instance.gtidon=false

# position info
canal.instance.master.address=hadoop1:3306
canal.instance.master.journal.name=
canal.instance.master.position=
canal.instance.master.timestamp=
canal.instance.master.gtid=

# rds oss binlog
canal.instance.rds.accesskey=
canal.instance.rds.secretkey=
canal.instance.rds.instanceId=

# table meta tsdb info
canal.instance.tsdb.enable=true
#canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb
#canal.instance.tsdb.dbUsername=canal
#canal.instance.tsdb.dbPassword=canal

#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =
#canal.instance.standby.gtid=

# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
canal.instance.defaultDatabaseName =test
# enable druid Decrypt database password
canal.instance.enableDruid=false
#canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==

# table regex
canal.instance.filter.regex=.*\\..*
# table black regex
canal.instance.filter.black.regex=

# mq config
canal.mq.topic=example
canal.mq.partition=0
# hash partition config
#canal.mq.partitionsNum=3
#canal.mq.partitionHash=mytest.person:id,mytest.role:id
#################################################

8、啓動canal API程序,將mysql業務表的修改數據保存到kafka對應topic,啓動程序後,需要利用sql文件夾中的sql腳本,在對應mysql數據庫中創建存儲過程和表,並利用存儲過程修改order_info表,此時canal監聽到數據發生改變,就會讀取bin文件,將數據發送到kafka集羣。

利用下列存儲過程修改表中數據,具體含義可查看存儲過程。

call init_data(varchar do_date_string, int order_incr_num, int user_incr_num, tinyint if_truncate);
call init_data('2019-11-22', 10, 5, false)

9、啓動spark streaming程序的orderApp,讀取kafka數據,並進行處理後保存到es對應index中。(可以直接在IDEA中運行,查看es中idnex中有數據增加即成功)

在這裏插入圖片描述

10、啓動publisher模塊和dw-chart模塊,輸入訪問地址,可以查看到以下效果圖。當然也可以通過kibana的圖表工具繪製對應的圖,如下第二張圖所示,設置對應的index和字段後也可以查看到自己需要的圖。

在這裏插入圖片描述
在這裏插入圖片描述

五、總結

本次的例程主要是針對基礎,完成一個完整的從數據模擬、數據採集到傳輸、計算、結果展示的流程。這樣的一個簡單實時系統還有很多需要完善的地方,也有很多更優選擇,可以在後期完善,該例程用於記錄學習過程,也希望能幫到想學習大數據的同學。

完整工程github:https://github.com/HeCCXX/gmall-parent.git

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章