java.lang.ArrayIndexOutOfBoundsException:256,jeromq 0.3.6版本

时间:2017-11-02 21:00:29

标签: java multithreading thread-safety zeromq jeromq

我在多线程环境中使用Jeromq,如下所示。下面是我的代码,其中SocketManager的构造函数首先连接到所有可用的套接字,然后将它们放在liveSocketsByDatacenter方法的connectToZMQSockets地图中。之后,我在同一个构造函数中启动一个后台线程,该构造函数每30秒运行一次并调用updateLiveSockets方法来ping liveSocketsByDatacenter map中已存在的所有套接字并更新liveSocketsByDatacenter映射这些插座是否存活。

并且多个读取器线程同时调用getNextSocket()方法以获取下一个可用的套接字,然后我们使用该套接字在其上发送数据。所以我的问题是我们在多线程环境中正确使用Jeromq吗?因为在我们尝试将数据发送到该实时套接字时,我们刚刚在生产环境中看到了这个堆栈跟踪的异常,所以我不确定它是否是一个bug或其他东西?

java.lang.ArrayIndexOutOfBoundsException: 256
at zmq.YQueue.push(YQueue.java:97)
at zmq.YPipe.write(YPipe.java:47)
at zmq.Pipe.write(Pipe.java:232)
at zmq.LB.send(LB.java:83)
at zmq.Push.xsend(Push.java:48)
at zmq.SocketBase.send(SocketBase.java:590)
at org.zeromq.ZMQ$Socket.send(ZMQ.java:1271)
at org.zeromq.ZFrame.send(ZFrame.java:131)
at org.zeromq.ZFrame.sendAndKeep(ZFrame.java:146)
at org.zeromq.ZMsg.send(ZMsg.java:191)
at org.zeromq.ZMsg.send(ZMsg.java:163)

以下是我的代码:

public class SocketManager {
    private static final Random random = new Random();
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private final Map<Datacenters, List<SocketHolder>> liveSocketsByDatacenter = new ConcurrentHashMap<>();
    private final ZContext ctx = new ZContext();

    private static class Holder {
        private static final SocketManager instance = new SocketManager();
    }

    public static SocketManager getInstance() {
        return Holder.instance;
    }

    private SocketManager() {
      connectToZMQSockets();
      scheduler.scheduleAtFixedRate(this::updateLiveSockets, 30, 30, TimeUnit.SECONDS);
    }

    // during startup, making a connection and populate once
    private void connectToZMQSockets() {
      Map<Datacenters, List<String>> socketsByDatacenter = Utils.SERVERS;
      for (Map.Entry<Datacenters, List<String>> entry : socketsByDatacenter.entrySet()) {
        List<SocketHolder> addedColoSockets = connect(entry.getValue(), ZMQ.PUSH);
        liveSocketsByDatacenter.put(entry.getKey(), addedColoSockets);
      }
    }

    private List<SocketHolder> connect(List<String> addresses, int socketType) {
        List<SocketHolder> socketList = new ArrayList<>();
        for (String address : addresses) {
          try {
            Socket client = ctx.createSocket(socketType);
            // Set random identity to make tracing easier
            String identity = String.format("%04X-%04X", random.nextInt(), random.nextInt());
            client.setIdentity(identity.getBytes(ZMQ.CHARSET));
            client.setTCPKeepAlive(1);
            client.setSendTimeOut(7);
            client.setLinger(0);
            client.connect(address);

            SocketHolder zmq = new SocketHolder(client, ctx, address, true);
            socketList.add(zmq);
          } catch (Exception ex) {
            // log error
          }
        }
        return socketList;
    }

    // this method will be called by multiple threads concurrently to get the next live socket
    // is there any concurrency or thread safety issue or race condition here?
    public Optional<SocketHolder> getNextSocket() {
      for (Datacenters dc : Datacenters.getOrderedDatacenters()) {
        Optional<SocketHolder> liveSocket = getLiveSocket(liveSocketsByDatacenter.get(dc));
        if (liveSocket.isPresent()) {
          return liveSocket;
        }
      }
      return Optional.absent();
    }

    private Optional<SocketHolder> getLiveSocket(final List<SocketHolder> listOfEndPoints) {
      if (!CollectionUtils.isEmpty(listOfEndPoints)) {
        // The list of live sockets
        List<SocketHolder> liveOnly = new ArrayList<>(listOfEndPoints.size());
        for (SocketHolder obj : listOfEndPoints) {
          if (obj.isLive()) {
            liveOnly.add(obj);
          }
        }
        if (!liveOnly.isEmpty()) {
          // The list is not empty so we shuffle it an return the first element
          return Optional.of(liveOnly.get(random.nextInt(liveOnly.size()))); // just pick one
        }
      }
      return Optional.absent();
    }

    // runs every 30 seconds to ping all the socket to make sure whether they are alive or not
    private void updateLiveSockets() {
      Map<Datacenters, List<String>> socketsByDatacenter = Utils.SERVERS;

      for (Map.Entry<Datacenters, List<String>> entry : socketsByDatacenter.entrySet()) {
        List<SocketHolder> liveSockets = liveSocketsByDatacenter.get(entry.getKey());
        List<SocketHolder> liveUpdatedSockets = new ArrayList<>();
        for (SocketHolder liveSocket : liveSockets) { // LINE A
          Socket socket = liveSocket.getSocket();
          String endpoint = liveSocket.getEndpoint();
          Map<byte[], byte[]> holder = populateMap();
          Message message = new Message(holder, Partition.COMMAND);

          // pinging to see whether a socket is live or not
          boolean status = SendToSocket.getInstance().execute(message.getAdd(), holder, socket);
          boolean isLive = (status) ? true : false;

          SocketHolder zmq = new SocketHolder(socket, liveSocket.getContext(), endpoint, isLive);
          liveUpdatedSockets.add(zmq);
        }
        liveSocketsByDatacenter.put(entry.getKey(), Collections.unmodifiableList(liveUpdatedSockets));
      }
    }
}

以下是我如何从多个读者线程中同时使用getNextSocket()类的SocketManager方法:

// this method will be called from multiple threads
public boolean sendAsync(final long addr, final byte[] reco) {
  Optional<SocketHolder> liveSockets = SocketManager.getInstance().getNextSocket();
  return sendAsync(addr, reco, liveSockets.get().getSocket(), false);
}

public boolean sendAsync(final long addr, final byte[] reco, final Socket socket,
    final boolean messageA) {
  ZMsg msg = new ZMsg();
  msg.add(reco);
  boolean sent = msg.send(socket);
  msg.destroy();
  retryHolder.put(addr, reco);
  return sent;
}

  public boolean send(final long address, final byte[] encodedRecords, final Socket socket) {
    boolean sent = sendAsync(address, encodedRecords, socket, true);
    // if the record was sent successfully, then only sleep for timeout period
    if (sent) {
      try {
        TimeUnit.MILLISECONDS.sleep(500);
      } catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
      }
    }
    // ...
    return sent;
  } 

我认为这不正确。似乎getNextSocket()可以将0MQ socket返回thread A。同时,计时器线程可以访问相同的0MQ socket来ping它。在这种情况下,thread A和计时器线程正在改变相同的0MQ socket,这将导致问题。那么解决这个问题的最佳和有效方法是什么?

注意: SocketHolder是一个不可变的类

更新

我刚刚注意到同一问题发生在我的另一个包含ArrayIndexOutOfBoundsException的邮箱上,但这次是"YQueue"文件中的71行号。唯一一致的是256总是。所以应该有一些与256相关的东西,我无法弄清楚256这是什么?

java.lang.ArrayIndexOutOfBoundsException: 256
    at zmq.YQueue.backPos(YQueue.java:71)
    at zmq.YPipe.write(YPipe.java:51)
    at zmq.Pipe.write(Pipe.java:232)
    at zmq.LB.send(LB.java:83)
    at zmq.Push.xsend(Push.java:48)
    at zmq.SocketBase.send(SocketBase.java:590)
    at org.zeromq.ZMQ$Socket.send(ZMQ.java:1271)
    at org.zeromq.ZFrame.send(ZFrame.java:131)
    at org.zeromq.ZFrame.sendAndKeep(ZFrame.java:146)
    at org.zeromq.ZMsg.send(ZMsg.java:191)
    at org.zeromq.ZMsg.send(ZMsg.java:163)

1 个答案:

答案 0 :(得分:2)

事实#0:根据定义,ZeroMQ不是线程安全的

虽然ZeroMQ文档和Pieter HINTJENS&#39;优秀的书&#34; Code Connected。第1卷&#34;不要忘记在可能的情况下提醒这个事实,不时出现在线程中返回甚至共享ZeroMQ套接字实例的想法。当然,班级实例&#39;方法可以提供这几乎&#34;隐藏&#34;在他们的内部方法和属性内部,但正确的设计努力应该防止任何这样的副作用,没有例外,没有任何借口。

如果定量事实得到合理支持,共享可能是zmq.Context()的一个共同实例的一种方式,但是一个清晰的分布式系统设计可能存在于一个真正的多代理方案中,每个代理都运行它自己的Context()引擎,根据配置和性能偏好的各种组合进行了微调。

那么解决这个问题的最佳和有效方法是什么?

永远不要共享ZeroMQ套接字。从来没有,确实。即使最新的发展开始承诺在不久的将来这方面的变化。使用共享污染任何高性能,低延迟的分布式系统设计是一个坏习惯。分享一切都不是这个领域的最佳设计原则。

是的,我可以看到我们不应该在线程之间共享套接字,而是在我的代码中 你认为解决这个问题的最佳方法是什么?

是的,解决此问题的最佳和有效方法是永远不要共享ZeroMQ套接字。

这意味着永远不会返回任何对象,其属性是ZeroMQ套接字(您主动构建并以 .connect(){...} 类方法大量返回。在您的情况下,所有类方法似乎保持 private ,这可能会导致允许&#34;其他线程&#34;触及类私有套接字实例的问题,但原理相同必须在所有属性级别上进行认可,以便有效。最后,这个&#34;融合&#34;获取快捷方式并被违反 的 public static SocketManager getInstance() 下,
它可以提供任何外部提问者直接访问共享ZeroMQ套接字的类私有实例。

如果某些文档几乎在每一章都明确警告不要分享内容,那么就不应该分享这些内容。

因此,重新设计方法,以便SocketManager获得更多功能,因为它的类方法将执行嵌入的必备功能,以便明确防止任何外部世界线程触及不可共享的实例,如ZeroMQ出版物中所述。

接下来是资源清单:您的代码似乎每30秒重新检查一次所有DataInter-of-Interest中的状态。这实际上每分钟创建两个新的List对象。虽然你可以推测让垃圾收集器整理掉所有的捶打,但是没有从任何地方进一步引用,这对于ZeroMQ相关的对象来说不是一个好主意,嵌入在你之前重新检查的List-s中运行。 ZeroMQ对象仍然从Zcontext() - ZeroMQ Context() - 核心工厂实例化的I / O线程内部引用,它们也可以被视为ZeroMQ套接字库存资源管理器。因此,所有 new 创建的套接字实例不仅从java侧获得外部句柄,而且还从{{内部获取内部句柄1}}。到现在为止还挺好。但是,在代码中的任何地方都看不到的是任何方法,它可以取消委托对象实例中的任何和所有ZeroMQ套接字,这些套接字已从(Z)Context()侧解除关联,但仍然从的 java - 侧。分配资源的显式资源退役是公平的设计方面的做法,资源越多,限制或受到限制。对于{&#34; cheap&#34; | &#34;昂贵&#34; - 这种资源管理处理的维护成本(ZeroMQ套接字实例作为一些轻量级&#34;消耗品/一次性使用非常昂贵&#34; ......但这是另一个故事)。

因此,添加一组适当的资源 - 重用/资源 - 拆解方法,这将使(Z)Context() - 创建的套接字的总量回到您的控制责任之下(您的代码负责如何 new -domain-of-resources-control中的许多套接字处理程序可能会被创建,并且必须保持被管理 - 无论是否有意识。)

有人可能反对可能会有一些&#34;承诺&#34;从自动检测和(可能很好的延期)垃圾收集,但是,你的代码仍负责适当的资源管理,即使LMAX人员如果依赖&#34;承诺&#34;来自标准gc。你的问题比LMAX顶级性能不得不与之斗争更糟糕。您的代码(目前为止已发布)对 (Z)Context() .close() ZeroMQ相关资源完全没有任何帮助。在生态系统内部,这是一种直接不可能的做法,具有不受控制的(分布式需求)消费。 你必须保护你的船免于超载你知道它可以安全处理的限制并动态卸载每个箱子,在对面的海岸上没有收件人&#34;。 / p>

这是船长(您的代码设计师)责任。

没有明确告知最低级别(ZeroMQ .term() - 楼层)的库存管理负责人,有些箱子要卸载,问题仍然存在。标准 Context() -chain-of-command将不会自动执行此操作&#34;,无论&#34; promises&#34;可能看起来会这样,但事实并非如此。因此,明确您的ZeroMQ资源管理,评估返回代码,从订购这些步骤开始,并适当处理在代码显式控制下执行这些资源管理操作所产生的任何和所有异常。

降低(如果不是最低可实现的最低值)资源利用率 - 信封和更高(如果不是最高可实现的)性能是这项工作的一个好处对。 LMAX人员做得非常好,除了标准的java&#34; promises&#34;之外,所以人们可以从最好的成绩中学习。

宣布的呼叫签名与使用的签名似乎不匹配:
虽然我在这一点上可能是错的,因为我的大多数设计工作都不在多态调用接口中,签名中似乎存在不匹配,发布为:

gc

和实际的方法调用,通过以下方式调用private List<SocketHolder> connect( Datacenters dc, // 1-st List<String> addresses, // 2-nd int socketType // 3-rd ) { ... /* implementation */ } 方法:

connectToZMQSockets()