在第一版里我着重的讲述了如何利用storm1.0带的窗口机制来做pv统计,而在(二)里面,我是基于(一)的升级,首先我还是来描述一下业务场景:
随着业务高峰期的到来,kafka中计算数据的qps急速增加,而计算资源的紧张导致cpu也直线飙升,所以计算延迟,服务down掉的情况时有发生,这时通过对每一个bolt执行时间的监控,发现了问题,(一)的数据落地在mysql中,存储的性能和开销太大,不适合于实时计算场景的数据落地,于是(二)里面,我们做了升级,重写了个Redis版本的。
核心业务逻辑不变,用Redis版本的好处,就是大多数情况,Redis可以帮助我们做数据的统一,即便是在多线程的情况下,我们也不再需要利用merge的方式去计算数据,完全可以多线程去写同一个key,大大节省了计算的资源,还有就是Redis提供了了排序的机制可以帮助我们去做段时间排行,也大大释放了计算服务器的资源。计算的逻辑不多说,这里只介绍一下,Redis版本测试过程中的两个坑:
1.利用每5s的窗口做计算,直接卡死
对于流计算场景和nosql数据库交互,我觉得一定是规约好可以接受的时间片段而进行批量提交,这个应该是和db交互的原则,如果在流计算场景按条为单位提交,那流量洪峰的时候就可以把db搞down,所以一定要严格控制和db交互的次数,机遇这样的思路,我就利用storm1.0的时间窗口,每5s产生一个时间窗口,然后对这个窗口的数据进行解析,存储。结果在测试过程中,直接卡死,目前我测试的窗口,只有按条数来计算的窗口好用,按时间计算的窗口均卡死,我们通过日志打点来看我每5s计算的时间,在单个窗口内的时间计算远远小于5s,但是卡死,由于时间关系,我还没有扒一下storm1.0窗口的源码,各位见谅,以后一定补上这个问题原因,所以,我尝试用了storm老版本自带的心跳机制去计算5s的数据,结果没有任何问题。如下:
/***
* bolt级的定时TickTime配置
* @return
*/
@Override
public Map<String, Object> getComponentConfiguration() {
Config conf = new Config();
//设置storm心跳tickTuple的时间频率=60s
conf.put(conf.TOPOLOGY_TICK_TUPLE_FREQ_SECS, 5);
return conf;
}
这块以后做storm窗口计算的小伙伴可以也踩一踩坑。
2.Redis数据指标设计问题
由于我们的前端页面和展现方式已经确定了,所以我们只能机遇前端展示的逻辑和业务层面的逻辑去设计Redis的数据key,最终的问题是,正常redis提供的api有很多没有批量提交,比如如果设计的key是在一个hash结构中的话,可以批量提交,效率很高,但是由于,我们的主key包含业务数据和时间片段数据,就导致,我们每5秒的数据是分散在不同hash key上的,无法利用正常的API去提交,最后,我采用了redis管道的方式去提交,这个地方介绍一个Redis Pipline的机制:
Redis使用的是客户端-服务器(CS)模型和请求/响应协议的TCP服务器。这意味着通常情况下一个请求会遵循以下步骤:
- 客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。
- 服务端处理命令,并将结果返回给客户端。
Redis客户端与Redis服务器之间使用TCP协议进行连接,一个客户端可以通过一个socket连接发起多个请求命令。每个请求命令发出后client通常会阻塞并等待redis服务器处理,redis处理完请求命令后会将结果通过响应报文返回给client,因此当执行多条命令的时候都需要等待上一条命令执行完毕才能执行。
由于通信会有网络延迟,假如client和server之间的包传输时间需要0.125秒。那么上面的三个命令6个报文至少需要0.75秒才能完成。这样即使redis每秒能处理100个命令,而我们的client也只能一秒钟发出四个命令。这显然没有充分利用 redis的处理能力。
而管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline通过减少客户端与redis的通信次数来实现降低往返延时时间,而且Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。 Pipeline 的默认的同步的个数为53个,也就是说arges中累加到53条数据时会把数据提交。
遂,我就采用管道的方式批量往里边写数据,开发了一下管道批量写的API,上一个代码:
public void putValueByPipline(Map<String,String> map)
{
Pool pool = this.redisPool.getPool();
Jedis jedis = (Jedis) pool.getResource();
Pipeline pip = null;
try
{
pip = jedis.pipelined();
pip.select(db_index);
for (Map.Entry<String,String> entry:map.entrySet()) {
pip.set(entry.getKey(),entry.getValue());
}
pip.sync();
}catch(Exception e)
{
jedis.close();
}
finally {
jedis.close();
}
}
3.Redis pipline提交堵死
我们业务高峰时候大概需要一次性利用管道往Redis写10w+的数据,发现Redis大多数情况下,就被我们写死了,机遇Redis的单线程机制,管道提交的量特别大导致堵死的通道。后来基于这个问题,我们我们做了按批次提交管道,500条一提,问题得以解决。