踩过的那些技术坑
此贴记录日常工作中踩过的技术坑
开源软件: RocksDB 版本: rocksdbjni-5.14.2.jar 问题: org.rocksdb.RocksIterator 的反直觉设计与常见的 java.util.Iterator 不同 细节描述: 在 Lealone-Plugins 项目中集成 RocksDB 时,为了给上层查询数据,提供了一个 RocksdbStorageMapCursor 类,它基于 RocksIterator 实现了 java.util.Iterator 接口,java.util.Iterator 的常规使用方式是先判断 hasNext(),如果为true再调用 next() 取到当前值,而 RocksIterator 是判断 isValid() 为true时就要把当前值取好,调用 next() 已经转到下一行了,所以如果像下面这样实现,不但会漏掉数据,还会导致 RocksDB 崩溃。
@Override
public boolean hasNext() {
return iterator.isValid();
}
@Override
public byte[] next() {
iterator.next();
return iterator.key();
}
解决办法: 像 RocksdbStorageMapCursor 类那样,在实现 hasNext() 方法时,判断 isValid() 为true后就把当前值取好,然后实现 next() 时还是要调用 iterator.next(),但是返回的是在 hasNext() 方法中事先取好的数据。
无责任吐槽: 这问题的排查和修复让我昨晚工作到凌晨3点,RocksDB 是C/C++系的,从这问题看出,提供的 Java API 并没有很好的考虑 Java 开发人员的日常习惯,特别是跟 Java 标准库不同的反直觉设计很容易让开发人员踩到坑。
开源软件: JDK/java.nio.channels 包 版本: 1.8或其他版本 问题: 如果执行 Selector.select 的线程阻塞了,其他执行 SelectableChannel.register 的线程也会阻塞 细节描述: 例如下面的代码会按 step 1到4 的顺序输出,而不是 step 1、step 2、step 4、step 3
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
System.out.println("step 1");
new Thread(() -> {
try {
System.out.println("step 2");
selector.select(3000);
System.out.println("step 3");
} catch (Exception e) {
}
}).start();
Thread.sleep(1000);
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_CONNECT); // 直到select结束时才会被执行
System.out.println("step 4");
}
这是因为执行 Selector.select 的线程阻塞了,主线程执行到 SelectableChannel.register 时也会被阻塞, 原因是因为执行 Selector.select 时会执行到 sun.nio.ch.SelectorImpl.lockAndDoSelect(long) 方法, 它的代码如下:
private int lockAndDoSelect(long timeout) throws IOException {
synchronized (this) {
if (!isOpen())
throw new ClosedSelectorException();
synchronized (publicKeys) {
synchronized (publicSelectedKeys) {
return doSelect(timeout);
}
}
}
}
最关键的是 synchronized (publicKeys) 这一行,占有了publicKeys 相关的锁后,执行 doSelect 的过程中就算线程被阻塞了,线程占用的锁依然没有释放。当主线程执行到 SelectableChannel.register 时会调用到 sun.nio.ch.SelectorImpl.register(AbstractSelectableChannel, int, Object) 它的代码如下:
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
synchronized (publicKeys) {
implRegister(k);
}
k.interestOps(ops);
return k;
}
最关键的也是 synchronized (publicKeys) ,主线程因为无法获得 publicKeys 相关的锁所以就卡在那里了,直到执行 Selector.select 的线程超时为止。
解决办法: 调用 Selector.wakeup 方法可以唤醒被 Selector.select 阻塞的线程,从而释放 publicKeys 锁,只要释放publicKeys 锁了,其他线程只要抢到锁就可以继续运行。这里有个参考例子: ChannelRegisterBlockingTest
无责任吐槽: 这是用 NIO 实现客户端事件循环线程时碰到的问题,NIO 是标准库中非常难用很容易踩坑的一个库,光看文档还不够,常常需要去跟踪代码流程才能明白不同 API 实现之间有什么影响,如果用 NIO 实现服务器端,有时还会碰到当客户端连接异常关闭后,导致服务器端的事件循环线程进入无 select 超时的状态, CPU 负载高到离谱。
刚刚又踩了 NIO 的一个坑,排查了快两小时,找了各种原因才发现服务器端收到的字节数总是少于客户端要发送的,结果是因为调用了 java.nio.channels.SocketChannel.write 方法时,只是简单的channel.write(ByteBuffer) 了,这样调用可能只写了部分数据,要像这样用:
while (buffer.hasRemaining())
channel.write(buffer);
为什么要这样设计?我都给你传一个ByteBuffer了,里面有pos,有limit,要写多少数据不会自己算吗?!
沉没好久了
做 Java 开发,是否有必要用 NIO 实现自己的网络客户端或网络服务器?
简而言之,99%以上的场景都是没必要的。
用 NIO 实现一个成熟稳定的高性能的网络应用程序是一件很繁琐很容易出错的事,Java 开源社区已经有像 Apache MINA 、Netty 和 Vert.x 这样的网络框架或工具库了,直接拿了用就好了。
在 Lealone-Plugins 的网络层插件中就包含对 MINA、Netty 和 Vertx 的支持,不过我最近倒是心血来潮自己用 NIO 实现了一个用于 Lealone 数据库的网络客户端和网络服务器。
之所以自己造了一个,是因为我在开发 Lealone 的过程中常常要快速启动一些测试程序,启动速度大于 1.5 秒就无法忍受了,Vertx 的启动速度最慢,在我的机器上经常要 3 秒左右,Netty 要两秒多,MINA 要接近两秒,为什么慢,这两个代码例子中的注释给出了一些原因: Netty例子 、Vertx例子
当然,用 NIO 开发程序时,除了上面说到的,还有挺多的,今天在做性能测试时,又对一个非常诡异的问题排查了大半天,发现是 NIO、协议、多线程并发同时引起的终极难搞问题,最后只需要调整两三行代码的顺序!!! 代码在此
最后,Lealone 启动速度是快了,小于1秒,总算是个安慰!:)
NIO太多坑了.
排查 drill 1.15.0 和 hadoop 2.7.4 问题的一个案例:
在windows7下,最初跑drill的测试用例时一开始就报这个错但是没理: Area [xxxxxx] must be writable and executable for application user 然后我直接把RemoteFunctionRegistry.createArea里面的检查代码注释掉,临时解决了这个问题。
今天我下了drill的分发包来跑,执行sqlline.bat时又报这个错,我仔细看了看xxxxxx目录的权限都是可写可执行的呀为啥还报错。 我不得不去掉注释调试了代码才发现RemoteFunctionRegistry.createArea里面执行fileStatus.getOwner()时得出xxxxxx目录的Owner是Administrators,而ImpersonationUtil.getProcessUserName()是Administrator,少个s,调式ImpersonationUtil.getProcessUserName()的代码又跟到hadoop相关的UserGroupInformation.getLoginUser(),hadoop默认又取jdk标准库返回的Administrator,所以就出错了。
还好UserGroupInformation.getLoginUser()允许通过设置环境变量HADOOP_PROXY_USER来指定代理用户名,把它设成Administrators就解决了。
之所以fileStatus.getOwner()返回Administrators,我跟踪了代码,发现在org.apache.hadoop.fs.RawLocalFileSystem.DeprecatedRawLocalFileStatus.loadPermissionInfo() 里面它用了winutils.exe ls -F xxxxxx命令得到了一个"drwx------|1|BUILTIN\Administrators..."这样的字符串,然后Owner就变成了Administrators,也就是它取了管理员用户组去了。