1. 背景
服务后台实时收集千万级别在线终端、全国近400个城市的出租车、手机和pad等移动终端的位置点gps信息,然后根据gps所在城市区域,持久化并推送分发给不同的订阅用户。
其业务逻辑图如下:
1.1 需求特征
a 实时性(gps点本身具有实时性的特征,例如打车服务,需要周边实时出租车位置信息)
b 数据量大(全国实时gps点数据规模 T级别/per day,高峰期时达到1G/min)
1.2 推送方式选择
数据推送方式通常有两种类型:
a Pull方式,这种方式服务端开发相对简单,可以采用缓存+httpserver的方式解决;
b Push方式,这种方式通常满足实时性的需求,对服务端而言逻辑相对复杂,需要维持大并发的连接和发送
由于实时性和大数据量需求的特征,所以系统采用"Push+长连接方式"进行推送。当然实现一套支持实时海量数据和客户推送的系统,需要解决的关键技术问题有很多: 如分布式集群,集群的failover和balancer能力,集群节点的配置管理等等。本系统借鉴hadoop的RPC模块,实现一套订阅发布实时推送服务,下面主要说说为了提高单节点的并发和吞吐量的些trick.
2. 架构图
3. 性能优化
3.1 异步数据发送
异步发送逻辑如下:
private int channelIO(WritableByteChannel writeCh, ByteBuffer buf)
throws IOException {
int originalLimit = buf.limit();
int initialRemaining = buf.remaining();
int ret = 0;
while (buf.remaining() > 0) {
try {
int ioSize = Math.min(buf.remaining(), NIO_BUFFER_LIMIT);
buf.limit(buf.position() + ioSize);
ret = writeCh.write(buf);
if (ret < ioSize) {
break;
}
} finally {
buf.limit(originalLimit);
}
}
int nBytes = initialRemaining - buf.remaining();
return (nBytes > 0) ? nBytes : ret;
}
这里主要有两优化点:①为防止待写的数据量过大导致独占线程时间片过长,在8行代码对ByteBuffer进行了分片发送(尽管tcp尽量避免数据分片和组包),②在11行代码,当通道没法写完数据时,应让出线程,立刻返回注册到selector,待下次writeCh通道变成writable可写状态时,再进行channelIO写操作(这是nio与bio的最大区别)。
3.2 用多selector机制,分离网络读写操作
ReadSelector负责监听用户的请求和鉴权响应,若用户请求为合法,则把相应连接注册给WriteSelector;WriteSelector负责将接收的实时gps点数据推送给已鉴权成功的注册用户连接
3.3 使用多selector机制,进行异步写数据
可以根据客户端的端口hash到不同的Selector上去执行写的操作,如下:
private Responder selectResponder(int remotePort){
int index = Math.abs(remotePort % responderCount);
return responders[index];
}
4. 容错健壮性
ResponseQueue,来维护待发送数据集。只有数据队列中存在数据时,就将相应连接注册到WriteSelector。如下图:
这里主要用到两个trick:
4.1 避免服务数据堆积
maxAllowedQueueSize。如果数据批次队列大于maxAllowedQueueSize,则直接丢弃,避免数据无上限增长,如下代码:
void doRespond(Call call) throws IOException {
try {
synchronized (call.connection.responseQueue) {
if (call.connection.responseQueue.size() < maxAllowedQueueSize) {
call.connection.responseQueue.addLast(call);
if (call.connection.responseQueue.size() == 1) {
processResponse(call.connection.responseQueue, true);
}
} else {
logger.warn(
"incoming data discarded from connection {}",
call.connection);
}
}
} catch (NullPointerException e) {
logger.error(e.getMessage(), e);
}
}
4.2 定期扫描和关闭坏掉的连接资源
这里的坏掉是指数据在一段时间内一直停留在连接connection的数据队列里,则认为该连接已失效而直接清理队列数据和关闭相应连接。代码如下:
private void doPurge(Call call, long now) throws IOException {
if(call.connection == null || call.connection.responseQueue == null){
return ;
}
LinkedList<Call> responseQueue = call.connection.responseQueue;
synchronized (responseQueue) {
Iterator<Call> iter = responseQueue.listIterator(0);
while (iter.hasNext()) {
call = iter.next();
if (now > call.timestamp + PURGE_INTERVAL) {
logger.info("dalay of current connection {} exceeds 10 mins",call.connection);
closeConnection(call.connection);
}
}
}
}
希望对有类似需求的网友能提供些参考和讨论。