在使用" executeAsync&#34 ;?时,如何限制写入请求到cassandra?

时间:2016-12-08 22:12:11

标签: java multithreading datastax-java-driver rate-limiting backpressure

我使用datastax java驱动程序3.1.0连接到cassandra集群,我的cassandra集群版本是2.0.10。我正在与QUORUM一致性写作异步。

  private final ExecutorService executorService = Executors.newFixedThreadPool(10);

  public void save(String process, int clientid, long deviceid) {
    String sql = "insert into storage (process, clientid, deviceid) values (?, ?, ?)";
    try {
      BoundStatement bs = CacheStatement.getInstance().getStatement(sql);
      bs.setConsistencyLevel(ConsistencyLevel.QUORUM);
      bs.setString(0, process);
      bs.setInt(1, clientid);
      bs.setLong(2, deviceid);

      ResultSetFuture future = session.executeAsync(bs);
      Futures.addCallback(future, new FutureCallback<ResultSet>() {
        @Override
        public void onSuccess(ResultSet result) {
          logger.logInfo("successfully written");
        }

        @Override
        public void onFailure(Throwable t) {
          logger.logError("error= ", t);
        }
      }, executorService);
    } catch (Exception ex) {
      logger.logError("error= ", ex);
    }
  }

我的上述保存方法将以非常快的速度从多个线程调用。

问题:

我希望将请求限制为executeAsync方法,该方法异步写入Cassandra。如果我以非常高的速度写入我的Cassandra集群可以处理,那么它将开始抛出错误,我希望我的所有写入都应成功进入cassandra而不会有任何损失。

我看到了这个post,其中解决方案是使用具有固定数量许可的Semaphore。但我不确定如何以及实施该方法的最佳方式是什么。我之前从未使用过Semaphor。这是逻辑。任何人都可以在我的代码上提供信号量基础的示例,或者如果有更好的方法/选项,那么请告诉我。

  

在编写dataloader程序的上下文中,您可以做一些事情   如下:

     
      
  • 为了简单起见,请使用信号量或其他具有固定数量许可的构造(这将是您的最大飞行数量   要求)。每当您使用executeAsync提交查询时,   获得许可证。你应该只需要1个线程(但可能需要)   获取获取的#cpu核心大小池   来自信号量的许可并执行查询。它会   在获得可用许可证之前阻止获取。
  •   
  • 使用Futures.addCallback从executeAsync返回的未来。回调应该在onSuccess和on中调用Sempahore.release()   onFailure案件。通过发布许可证,这应该允许你的线程   在步骤1中继续并提交下一个请求。
  •   

此外,我已经看到其他几个post,他们已经讨论过使用RingBufferGuava RateLimitter,哪一个更好,我应该使用?以下是我能想到的选项:

  • 使用信号量
  • 使用Ring Buffer
  • 使用番石榴速率限制器

任何人都可以帮我一个例子,说明我们如何限制请求或获取cassandra写入的背压并确保所有写入成功进入cassandra?

2 个答案:

答案 0 :(得分:8)

不是权威的答案,但可能会有所帮助。首先,你应该考虑当你的查询无法立即执行时你会做什么。无论您选择哪种速率限制,如果您获得的请求速度高于您写入Cassandra的速度,最终您的流程会因等待请求而被堵塞。在那一刻,你需要告诉你的客户暂时保留他们的请求(&#34;推回&#34;)。例如。如果他们是通过HTTP来的,那么响应状态将是429&#34; Too Many Requests&#34;。如果在同一进程中生成请求,则确定可接受的最长超时。那说如果Cassandra跟不上,那就是缩放(或调整)它的时候了。

也许在实施速率限制之前,在调用save方法(使用Thread.sleep(...))之前尝试并在线程中添加人为延迟是值得的,并查看它是否解决了您的问题或其他需要的东西。

从Cassandra查询返回错误背压。但您可以选择或实施RetryPolicy以确定何时重试失败的查询。

您也可以查看connection pool options(尤其是Monitoring and tuning the pool)。可以调整异步requests per connection的数量。然而文档说,对于Cassandra 2.x,这个参数上限为128,一个不应该改变它(虽然我试验了它)

使用Semaphore的实现看起来像

/* Share it among all threads or associate with a thread for per-thread limits
   Number of permits is to be tuned depending on acceptable load.
*/
final Semaphore queryPermits = new Semaphore(20); 


public void save(String process, int clientid, long deviceid) {
  ....
  queryPermits.acquire(); // Blocks until a permit is available

  ResultSetFuture future = session.executeAsync(bs);
  Futures.addCallback(future, new FutureCallback<ResultSet>() {
    @Override
    public void onSuccess(ResultSet result) {
      queryPermits.release();
      logger.logInfo("successfully written");
    }
    @Override
    public void onFailure(Throwable t) {
      queryPermits.release(); // Permit should be released in all cases.
      logger.logError("error= ", t);
    }
  }, executorService);
  ....
}

(在实际代码中我创建了一个包装器回调函数,它将释放许可证,然后调用包装方法)

Guava的RateLimiter类似于信号量,但在未充分利用期后允许临时突发,并根据时间限制请求(不是活动查询的总数)。

但是请求会因各种原因而失败,所以最好有一个计划如何重试它们(如果出现间歇性错误)。

在您的情况下可能不合适,但我尝试使用某个队列或缓冲区来排队请求(例如java.util.concurrent.ArrayBlockingQueue)。 &#34;缓冲区已满#34;意味着客户应该等待或放弃请求。缓冲区还将用于重新排队失败的请求。但是更公平的失败请求可能应该放在队列前面,以便首先重试它们。此外,当队列已满并且同时存在新的失败请求时,应该以某种方式处理该情况。然后,单线程工作者将从队列中选择请求并将其发送到Cassandra。因为它不应该做太多,所以它不太可能成为一个瓶颈。该工作人员还可以应用它自己的速率限制,例如基于com.google.common.util.concurrent.RateLimiter的时间安排。

如果有人想避免尽可能地丢失消息,他可以在Cassandra面前放置一个持久化的消息代理(例如Kafka)。这样,即使长时间停止使用Cassandra,传入的消息也可以存活。但是,我想,你的情况太过分了。

答案 1 :(得分:2)

只需使用阻塞队列就可以了。 期货是线程化的,回调(成功和失败)将充当消费者,无论你从哪里调用save方法都将充当生产者。

更好的方法是,将完整的请求本身放入队列中,然后逐一将其出列,并在每次出列时保存。

private final ExecutorService executorService = Executors.newFixedThreadPool(10);

public void save(String process, int clientid, long deviceid, BlockingQueue<Object> queue) {
    String sql = "insert into storage (process, clientid, deviceid) values (?, ?, ?)";
    try {
      BoundStatement bs = CacheStatement.getInstance().getStatement(sql);
      bs.setConsistencyLevel(ConsistencyLevel.QUORUM);
      bs.setString(0, process);
      bs.setInt(1, clientid);
      bs.setLong(2, deviceid);

      ResultSetFuture future = session.executeAsync(bs);
      Futures.addCallback(future, new FutureCallback<ResultSet>() {
        @Override
        public void onSuccess(ResultSet result) {
          logger.logInfo("successfully written");
          queue.take();
        }

        @Override
        public void onFailure(Throwable t) {
          logger.logError("error= ", t);
          queue.take();
        }
      }, executorService);
    } catch (Exception ex) {
      logger.logError("error= ", ex);
    }
}

public void invokeSaveInLoop(){
    Object dummyObj = new Object();
    BlockingQueue<Object> queue = new ArrayBlockingQueue<>(20);;
    for(int i=0; i< 1000; i++){
        save("process", clientid, deviceid, queue);
        queue.put(dummyObj);
    }
}

如果你想进一步检查群集中途加载

public static String getCurrentState(){    
StringBuilder response = new StringBuilder();
            response.append("Current Database Connection Status <br>\n ---------------------------------------------<br>\n");
            final LoadBalancingPolicy loadBalancingPolicy =
                    cluster.getConfiguration().getPolicies().getLoadBalancingPolicy();
            final PoolingOptions poolingOptions =
                    cluster.getConfiguration().getPoolingOptions();
            Session.State state = session.getState();
            for (Host host : state.getConnectedHosts()) {
                HostDistance distance = loadBalancingPolicy.distance(host);
                int connections = state.getOpenConnections(host);
                int inFlightQueries = state.getInFlightQueries(host);
                response.append(String.format("%s current connections=%d, max allowed connections=%d, current load=%d, max load=%d%n",
                                host, connections, poolingOptions.getMaxConnectionsPerHost(distance), inFlightQueries,
                                connections *
                                        poolingOptions.getMaxRequestsPerConnection(distance)))
                        .append("<br>\n");
            }
            return response.toString();
}