前言

项目介绍在线视频: https://www.bilibili.com/video/BV1zv41157yY

本案例是一个专注于flink动态规则计算的项目,核心技术组件涉及flink、hbase、clickhouse、drools等 项目可根据各类个性化需求进行二次开发后,直接用于实时运营,实时风控、交通监控等场景的线上生产 项目完整视频教程和资料代码等,

兵马未动,粮草先行

巧妇难为无米之炊,本运营系统的开发,离不开用户行为的数据记录;
各类网站、app等互联网产品上,用户的行为记录数据,往往称作“行为日志”,或曰“埋点日志”,亦或曰“流量日志”,亦或曰“点击流日志”,皆类同矣;

日志数据埋点介绍

埋点技术,通常在前端系统进行埋点

  • 如html页面,采用js埋点
  • app端则可以用android api代码埋点
  • 微信小程序,使用微信小程序的js api代码埋点

埋点代码,主要的技术手段是对界面元素进行函数绑定,当用户浏览,点击,滑动等动作发生时,绑定函数会被触发,从而获取到用户的行为信息,并通过HTTP发给后端的日志服务器进行收集存储(日志文件);

埋点技术,在业内通常又分为特定事件埋点,全埋点(或称无埋点)方案

  • 特定事件埋点,是针对特定按钮上的特定操作行为,进行专门函数的绑定,获取到的信息有针对性,比如捕获的用户行为是一个什么事件,拥有哪些业务属性等;
  • 全埋点(无埋点),则通常是对整个界面上的所有元素进行统一函数的绑定,这样一来,用户的所有行为都将被捕获,但是获得的数据相对来说没有那么具备针对性,往往是产生,用户操作的元素类型,元素id,元素标题等通用信息;

本项目侧重数据平台开发,因此,前端埋点技术具体内容不在此展开;

日志数据字段说明

本公司的埋点日志数据,整体结构为一个嵌套json串,如下所示:

{
	"account": "Vz54E9Ya",
	"appId": "cn.doitedu.app1",
	"appVersion": "3.4",
	"carrier": "中国移动",
	"deviceId": "WEISLD0235S0934OL",
	"deviceType": "MI-6",
    "ip": "24.93.136.175",
	"latitude": 42.09287620431088,
	"longitude": 79.42106825764643,
	"netType": "WIFI",
	"osName": "android",
	"osVersion": "6.5",
	"releaseChannel": "豌豆荚",
	"resolution": "1024*768",
	"sessionId": "SE18329583458",
	"timeStamp": 1594534406220
	"eventId": "productView",
	"properties": {
		"pageId": "646",
		"productId": "157",
		"refType": "4",
		"refUrl": "805",
		"title": "爱得堡 男靴中高帮马丁靴秋冬雪地靴 H1878 复古黄 40码",
		"url": "https://item.jd.com/36506691363.html",
		"utm_campain": "4",
		"utm_loctype": "1",
		"utm_source": "10"
	}	
}

其中主要有如下内容:

  • 用户终端属性(公共属性)
  • 事件类型(eventId)
  • 事件详情(properties,map结构)

行为明细数据入kafka

  • 用模拟器生成数据写入kafka
/**
 * @author 涛哥
 * @nick_name "deep as the sea"
 * @date 2021-03-25
 * @desc 压测数据模拟器
 * {
 * "account":"157","appId":"cn.do",
 * "appVersion":"3.0","carrier":"中国移动",
 * "deviceId":"157","deviceType":"mi10",
 * "eventId":"thumbUp","ip":"152.33.68.90",
 * "latitude":56.29385792,"longitude":120.329857234,
 * "netType":"wifi","osName":"android","osVersion":"9.0",
 * "properties":{"productId":"10","pageId":"10"},
 * "releaseChannel":"小米应用",
 * "resolution":"1024*2048",
 * "sessionId":"sessionid157",
 * "timeStamp":1616142957968
 * }
 *
 */
public class GenLogData {

    public static void main(String[] args) throws InterruptedException {

        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "hdp01:9092,hdp02:9092,hdp03:9092");
        props.put("acks", "all");// acks=0 配置适用于实现非常高的吞吐量 , acks=all 这是最安全的模式
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        KafkaProducer<String, String> producer = new KafkaProducer<>(props);

        String[] events = {"pageView", "addCart", "collect", "thumbUp", "share", "search", "adShow", "adClick", "fetchCoupon", "useCoupon", "register"};

        for(int i=0;i<10;i++){
            new Thread(
                    new Runnable() {
                        @Override
                        public void run() {
                            while (true) {
                                LogBean bean = new LogBean();
                                // 决定人数,3个就可能有1000人
                                String account = StringUtils.leftPad(RandomStringUtils.randomNumeric(2),4,"0");
                                bean.setAccount(account);

                                String eventId = events[RandomUtils.nextInt(0, events.length)];
                                bean.setEventId(eventId);
                                bean.setTimeStamp(System.currentTimeMillis());
                                bean.setAppId("cn.do");
                                bean.setAppVersion("3.0");
                                bean.setCarrier("中国移动");
                                bean.setDeviceId(account);
                                bean.setDeviceType("mi10");
                                bean.setIp("152.33.68.90");
                                bean.setLatitude(56.29385792);
                                bean.setLongitude(120.329857234);
                                bean.setSessionId("sessionid" + account);
                                bean.setResolution("1024*2048");
                                bean.setReleaseChannel("小米应用");
                                bean.setNetType("wifi");
                                bean.setOsName("android");
                                bean.setOsVersion("9.0");

                                HashMap<String, String> properties = new HashMap<>();
                                // 页面数量可能达到10种
                                properties.put("pageId", RandomUtils.nextInt(1, 11) + "");
                                if (eventId.equals("addCart") || eventId.equals("collect") 
                                      || eventId.equals("share") || eventId.equals("thumbUp")) {
                                    properties.put("productId", RandomUtils.nextInt(1, 11) + "");
                                }
                                if (eventId.equals("fetchCoupon") || eventId.equals("useCoupon")) {
                                    properties.put("couponId", RandomUtils.nextInt(1, 11) + "");
                                }
                                if (eventId.equals("adShow") || eventId.equals("adClick")) {
                                    properties.put("adId", RandomUtils.nextInt(1, 11) + "");
                                }

                                if (eventId.equals("search")) {
                                    properties.put("keyword", RandomStringUtils.randomNumeric(1));
                                }

                                bean.setProperties(properties);

                                producer.send(new ProducerRecord<String, String>("app_log", JSON.toJSONString(bean)));

                                try {
                                    // 20ms相当于每秒2k条,相当于2w人同时在线
                                    // 当前配置情况下,完全hold住没有任何反压延迟
                                    // 内存占用约6G(原因,缓存没有清除机制,内存一直在增长)
                                    Thread.sleep(RandomUtils.nextInt(1000,4000));
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }

                            }
                        }
                    }
            ).start();
        }
    }
}