地址:https://www.tuicool.com/articles/eim2Iv2
最近公司落地Flume日志采集着实反复了好久,简单记录一下性能优化的核心思路。
初始配置所有batch size、transaction size都是1000,channel的capactiy是10000。
最初我是按Memory Channel做压测,Taildir的source采集增量日志,Memory Channel缓冲数据,Kafka Sink发送数据。
这里面的瓶颈是Kafka Sink,因为Kafka Sink是单线程同步发送,网络延迟就会导致吞吐上不去,大概10MB+的一个吞吐就封顶了。
翻看了官方文档,打算试验一下sink group来实现多个kafka sink同时发送,结果性能仍旧10MB+。
分析原理,原来sink group仍旧是个单线程sink,相当于多个kafka sink的代理而已,仅仅实现了轮转负载均衡功能。
一个kafka sink的发送延迟高,轮转压根没有意义。
于是琢磨如何实现多线程跑多个Kafka Sink,于是仍旧使用1个Memory Channel,配置对应3个Kafka Sink,结果带宽可以升高到30MB的样子,但是极不稳定,来回跳跃。
此时发现Memory Channel的填充率接近90%+,应该是因为容量经常塞满导致的流水线阻塞,通过增加memory channel的capacity到10万,batch size和transaction size增加到1万,吞吐提升到60MB~80MB+,填充率小于10%,已经满足需求。
在transaction size=1000的情况下memory channel被填满,而transaction size=1万的情况下memory channel就不会被填满,其实是通过增加channel批处理的包大小,降低了channel访问的频次,解决的是memory channel的锁瓶颈。
同时,这个优化思路也带来了问题,更大的memory channel capacity带来了更大的数据丢失风险,因为宕机时memory channel里缓冲的数据都会丢失。
实现多个memory channel轮转,每个memory channel由一个kafka sink消费。
这样做目的有2个:
实现该功能需要自己开发channel selector插件,实现source流量的轮转分发,可以翻看我之前写的博客。
同事要求使用file channel,保障队列中数据的可靠性,但是经过测试发现吞吐只能跑到10MB+,上述所说优化手段均无效。
更换SSD盘也没有带来任何提升,File channel自身填充率极低。
个人怀疑瓶颈在File Channel自身,其事务的提交效率太低,阻塞了source的投递动作,无论如何增加channel数量也无济于事,因为source是单线程的,轮转发往多个File Channel的速度仍旧等于单个File Channel速度,导致后续Sink没有足够数据消费,吞吐无法提升。
从FileChannel代码来看,磁盘读写的相关代码全部被加锁处理:
synchronized FlumeEventPointer put(ByteBuffer buffer) throws IOException {
if (encryptor != null) {
buffer = ByteBuffer.wrap(encryptor.encrypt(buffer.array()));
}
Pair pair = write(buffer);
return new FlumeEventPointer(pair.getLeft(), pair.getRight());
}
synchronized void take(ByteBuffer buffer) throws IOException {
if (encryptor != null) {
buffer = ByteBuffer.wrap(encryptor.encrypt(buffer.array()));
}
write(buffer);
}
synchronized void rollback(ByteBuffer buffer) throws IOException {
if (encryptor != null) {
buffer = ByteBuffer.wrap(encryptor.encrypt(buffer.array()));
}
write(buffer);
}
synchronized void commit(ByteBuffer buffer) throws IOException {
if (encryptor != null) {
buffer = ByteBuffer.wrap(encryptor.encrypt(buffer.array()));
}
write(buffer);
dirty = true;
lastCommitPosition = position();
}
另外,日志文件的sync刷盘策略分为两种选项,一种是每次提交事务都刷新,另外一个是定时线程刷新(下面是定时线程):
syncExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
sync();
} catch (Throwable ex) {
LOG.error("Data file, " + getFile().toString() + " could not " +
"be synced to disk due to an error.", ex);
}
}
}, fsyncInterval, fsyncInterval, TimeUnit.SECONDS);
而这个sync()刷盘操作同样被锁保护的,会占用大量的锁时间:
/**
* Sync the underlying log file to disk. Expensive call,
* should be used only on commits. If a sync has already happened after
* the last commit, this method is a no-op
*
* @throws IOException
* @throws LogFileRetryableIOException - if this log file is closed.
*/
synchronized void sync() throws IOException {
if (!fsyncPerTransaction && !dirty) {
if (LOG.isDebugEnabled()) {
LOG.debug(
"No events written to file, " + getFile().toString() +
" in last " + fsyncInterval + " or since last commit.");
}
return;
}
if (!isOpen()) {
throw new LogFileRetryableIOException("File closed " + file);
}
if (lastSyncPosition < lastCommitPosition) {
getFileChannel().force(false);
lastSyncPosition = position();
syncCount++;
dirty = false;
}
}
降低sync()的调用频率,理论上可以降低锁占用时间,让出更多的锁时间给put与take操作。
flume可以配置这些参数,只是官方文档里并没有说明:
public static final String FSYNC_PER_TXN = "fsyncPerTransaction";
public static final boolean DEFAULT_FSYNC_PRE_TXN = true;
public static final String FSYNC_INTERVAL = "fsyncInterval";
public static final int DEFAULT_FSYNC_INTERVAL = 5; // seconds.
默认是每个事务都sync,可想而知put事务的性能有多差了,但是这样数据是最可靠的,总之只能折衷。
后续会补充一下相关的测试,通过配置定时刷新取代每次刷新,确认是否有相关提升效果。
关于File Channel瓶颈,有同学有JAVA调优经验的可以具体看一下FileChannel提交一个batch的时间花费。
因为提交批次属于顺序写盘行为,理论不应该过慢,但可能因为消费批次端锁内随机访问磁盘,导致锁占用时间过长,进而影响了生产端的投递batch耗时。