【问题标题】:Cassandra: How to insert a new wide row with good performance using CQLCassandra:如何使用 CQL 插入具有良好性能的新宽行
【发布时间】:2014-03-13 18:33:50
【问题描述】:

我正在评估 cassandra。我正在使用 datastax 驱动程序和 CQL。

我想存储一些具有以下内部结构的数据,其中每次更新的名称都不同。

+-------+-------+-------+-------+-------+-------+
|       | name1 | name2 | name3 | ...   | nameN |
| time  +-------+-------+-------+-------+-------+
|       | val1  | val2  | val3  | ...   | valN  |
+-------+-------+-------+-------|-------+-------+

所以时间应该是列键,名称应该是行键。我用来创建这个表的 CQL 语句是:

CREATE TABLE IF NOT EXISTS test.wide (
  time varchar,
  name varchar,
  value varchar,
  PRIMARY KEY (time,name))
  WITH COMPACT STORAGE

我希望架构是这种方式以便于查询。我还必须偶尔存储超过 65000 行的更新。所以使用 cassandra list/set/map 数据类型不是一个选项。

我必须能够每秒处理至少 1000 次宽行插入,并且名称/值对的数量虽然不同但很大 (~1000)。

问题如下:我编写了一个简单的基准测试,它执行 1000 个宽行插入,每个行插入 10000 个名称/值对。我使用 CQL 和 datastax 驱动程序的性能非常缓慢,而不使用 CQL(使用 astyanax)的版本在同一测试集群上具有良好的性能。

我已阅读此related question,并且在此问题的已接受答案中建议您应该能够通过使用批量准备语句,在 cassandra 2 中可用。

所以我尝试使用这些,但性能仍然很慢(对于在 localhost 上运行的小型三节点集群,每秒插入两次)。我是否遗漏了一些明显的东西,或者我必须使用较低级别的节俭 API? 我在 astyanax 中使用 ColumnListMutation 实现了相同的插入,我每秒插入大约 30 次。

如果我必须使用较低级别的 thrift API:

  • 它实际上已被弃用,还是因为它的级别较低而使用不方便?

  • 我能否使用 CQL 查询使用 thrift api 创建的表?

以下是 scala 中的自包含代码示例。它只是创建一个批处理语句,用于插入具有 10000 列的宽行并重复计算插入性能。

我使用了 BatchStatement 选项和一致性级别,但没有什么能让我获得更好的性能。

我唯一的解释是,尽管批处理由准备好的语句组成,但条目被一个接一个地添加到行中。


package cassandra

import com.datastax.driver.core._

object CassandraTestMinimized extends App {

  val keyspace = "test"
  val table = "wide"
  val tableName = s"$keyspace.$table"

  def createKeyspace = s"""
CREATE KEYSPACE IF NOT EXISTS ${keyspace}
WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }
"""

  def createWideTable = s"""
CREATE TABLE IF NOT EXISTS ${tableName} (
time varchar,
name varchar,
value varchar,
PRIMARY KEY (time,name))
WITH COMPACT STORAGE
"""

  def writeTimeNameValue(time: String) = s"""
INSERT INTO ${tableName} (time, name, value)
VALUES ('$time', ?, ?)
"""

  val cluster = Cluster.builder.addContactPoints("127.0.0.1").build
  val session = cluster.connect()

  session.execute(createKeyspace)
  session.execute(createWideTable)

  for(i<-0 until 1000) {
    val entries =
      for {
        i <- 0 until 10000
        name = i.toString
        value = name
      } yield name -> value
    val batchPreparedStatement = writeMap(i, entries)
    val t0 = System.nanoTime()
    session.execute(batchPreparedStatement)
    val dt = System.nanoTime() - t0
    println(i + " " + (dt/1.0e9))
  }

  def writeMap(time: Long, update: Seq[(String, String)]) : BatchStatement = {
    val template = session
      .prepare(writeTimeNameValue(time.toString))
      .setConsistencyLevel(ConsistencyLevel.ONE)
    val batch = new BatchStatement(BatchStatement.Type.UNLOGGED)
    for ((k, v) <- update)
      batch.add(template.bind(k, v))
    batch
  }
}

这里是 astyanax 代码(从 astyanax example 修改),它在本质上将相同的事情快 15 倍。请注意,这也不使用异步调用,因此这是一个公平的比较。这要求列族已经存在,因为我还没有弄清楚如何使用 astyanax 创建它,并且该示例没有任何用于创建列族的代码。

package cassandra;

import java.util.Iterator;

import com.netflix.astyanax.ColumnListMutation;
import com.netflix.astyanax.serializers.AsciiSerializer;
import com.netflix.astyanax.serializers.LongSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.astyanax.AstyanaxContext;
import com.netflix.astyanax.Keyspace;
import com.netflix.astyanax.MutationBatch;
import com.netflix.astyanax.connectionpool.NodeDiscoveryType;
import com.netflix.astyanax.connectionpool.OperationResult;
import com.netflix.astyanax.connectionpool.exceptions.ConnectionException;
import com.netflix.astyanax.connectionpool.impl.ConnectionPoolConfigurationImpl;
import com.netflix.astyanax.connectionpool.impl.CountingConnectionPoolMonitor;
import com.netflix.astyanax.impl.AstyanaxConfigurationImpl;
import com.netflix.astyanax.model.Column;
import com.netflix.astyanax.model.ColumnFamily;
import com.netflix.astyanax.model.ColumnList;
import com.netflix.astyanax.thrift.ThriftFamilyFactory;

public class AstClient {
    private static final Logger logger = LoggerFactory.getLogger(AstClient.class);

    private AstyanaxContext<Keyspace> context;
    private Keyspace keyspace;
    private ColumnFamily<Long, String> EMP_CF;
    private static final String EMP_CF_NAME = "employees2";

    public void init() {
        logger.debug("init()");

        context = new AstyanaxContext.Builder()
                .forCluster("Test Cluster")
                .forKeyspace("test1")
                .withAstyanaxConfiguration(new AstyanaxConfigurationImpl()
                        .setDiscoveryType(NodeDiscoveryType.RING_DESCRIBE)
                )
                .withConnectionPoolConfiguration(new ConnectionPoolConfigurationImpl("MyConnectionPool")
                        .setPort(9160)
                        .setMaxConnsPerHost(1)
                        .setSeeds("127.0.0.1:9160")
                )
                .withAstyanaxConfiguration(new AstyanaxConfigurationImpl()
                        .setCqlVersion("3.0.0")
                        .setTargetCassandraVersion("2.0.5"))
                .withConnectionPoolMonitor(new CountingConnectionPoolMonitor())
                .buildKeyspace(ThriftFamilyFactory.getInstance());

        context.start();
        keyspace = context.getClient();

        EMP_CF = ColumnFamily.newColumnFamily(
                EMP_CF_NAME,
                LongSerializer.get(),
                AsciiSerializer.get());
    }

    public void insert(long time) {
        MutationBatch m = keyspace.prepareMutationBatch();

        ColumnListMutation<String> x =
                m.withRow(EMP_CF, time);
        for(int i=0;i<10000;i++)
            x.putColumn(Integer.toString(i), Integer.toString(i));

        try {
            @SuppressWarnings("unused")
            Object result = m.execute();
        } catch (ConnectionException e) {
            logger.error("failed to write data to C*", e);
            throw new RuntimeException("failed to write data to C*", e);
        }
        logger.debug("insert ok");
    }

    public void createCF() {
    }

    public void read(long time) {
        OperationResult<ColumnList<String>> result;
        try {
            result = keyspace.prepareQuery(EMP_CF)
                    .getKey(time)
                    .execute();

            ColumnList<String> cols = result.getResult();
            // process data

            // a) iterate over columsn
            for (Iterator<Column<String>> i = cols.iterator(); i.hasNext(); ) {
                Column<String> c = i.next();
                String v = c.getStringValue();
                System.out.println(c.getName() + " " + v);
            }

        } catch (ConnectionException e) {
            logger.error("failed to read from C*", e);
            throw new RuntimeException("failed to read from C*", e);
        }
    }

    public static void main(String[] args) {
        AstClient c = new AstClient();
        c.init();
        long t00 = System.nanoTime();
        for(int i=0;i<1000;i++) {
            long t0 = System.nanoTime();
            c.insert(i);
            long dt = System.nanoTime() - t0;
            System.out.println((1.0e9/dt) + " " + i);
        }
        long dtt = System.nanoTime() - t00;

        c.read(0);
        System.out.println(dtt / 1e9);
    }

}

更新:我在cassandra-user 邮件列表中找到了这个帖子。在进行大的宽行插入时,CQL 似乎存在性能问题。有一张票 CASSANDRA-6737 来跟踪这个问题。

Update2:我已经试用了 CASSANDRA-6737 附带的补丁,我可以确认这个补丁完全解决了这个问题。感谢 DataStax 的 Sylvain Lebresne 如此迅速地解决了这个问题!

【问题讨论】:

    标签: performance cassandra cql3 datastax-java-driver


    【解决方案1】:

    您的代码中有一个错误,我认为这可以解释您所看到的许多性能问题:对于每个批次,您都要重新准备语句。准备一份声明并不是非常昂贵,但是像你这样做会增加很多延迟。您等待该语句准备好的时间是您不构建批处理的时间,而 Cassandra 不花费处理该批处理的时间。准备好的语句只需要准备一次,并且应该重复使用。

    我认为很多糟糕的性能可以解释为延迟问题。瓶颈很可能是您的应用程序代码,而不是 Cassandra。即使您只准备该语句一次,您仍然会花费大部分时间在应用程序中受 CPU 限制(构建大批量)或不做任何事情(等待网络和 Cassandra)。

    您可以做两件事:首先使用 CQL 驱动程序的异步 API 并在网络和 Cassandra 忙于您刚刚完成的那个时构建下一个批次;其次尝试运行多个线程做同样的事情。您必须试验的确切线程数取决于您拥有的内核数量以及您是在同一台机器上运行一个还是三个节点。

    在同一台机器上运行三节点集群会使集群比运行单个节点慢,而在不同机器上运行会更快。同样在同一台机器上运行应用程序并没有帮助。如果你想测试性能,要么只运行一个节点,要么在不同的机器上运行一个真实的集群。

    批处理可以为您提供额外的性能,但并非总是如此。它们可能导致您在测试代码中看到的那种问题:缓冲区膨胀。一旦批次变得太大,您的应用程序就会花费太多时间来构建它们,然后花费太多时间将它们推送到网络上,以及等待 Cassandra 处理它们的时间太多。您需要尝试批量大小,看看哪种效果最好(但要使用真正的集群,否则您将看不到网络的影响,当您的批量变得更大时,这将是一个重要因素)。

    如果您使用批处理,请使用压缩。压缩对大多数请求负载没有影响(响应是另一回事),但是当您发送大批量时,它会产生很大的不同。

    Cassandra 中的宽行写入没有什么特别之处。除某些例外情况外,架构不会更改处理写入所需的时间。我运行的应用程序每秒执行数以万计的非批处理混合宽行和非宽行写入。集群并不大,每个只有三个或四个 m1.xlarge EC2 节点。诀窍是永远不要在发送下一个请求之前等待返回(这并不意味着触发并忘记,只需以相同的异步方式处理响应)。延迟是性能杀手。

    【讨论】:

    • 我不认为构建批次是罪魁祸首。但请注意,为了确保我不测量构建批次的时间,时间测量只是批次的执行,而不是构建。我知道在 localhost 上运行三节点集群并不是最好的性能。我也知道我可以通过调整一些设置来获得一些改进。但这些都不能解释我在 localhost 上运行的相同三节点集群上使用 Thrift API 获得了 15 倍 更好的性能。
    • 我认为您测量错误。将测试代码重写为多线程,异步执行所有操作并测量几分钟内完成的写入次数。衡量一个批次的延迟并不能告诉您足够的实际性能。
    • 但是非多线程的非 CQL 版本怎么会表现得这么好呢?此外,究竟什么会导致 localhost 延迟一秒?我认为问题在于 CQL 并没有弄清楚所有插入都会影响同一个行键,而使用 astyanax API 我可以精确地指定我想要一个行键的大更新。
    • 您能否再解释一下您的实际测量值是多少?插入一批 10,000 个项目平均需要多长时间?如果你对ColumnListMutation 做同样的事情,并且只测量发送和接收对该操作的响应所花费的时间,那需要多长时间?您说“我必须能够每秒处理至少 1000 次宽行插入”,但也说“我仍然获得绝对糟糕的性能 [...] 每秒两次插入”。您的意思是每秒两批 10,000 个(这将是 20K 插入/秒,远高于您的目标)。你能澄清一下吗?
    • 我使用您的 CQL 在我的机器上运行了一个测试,在 Ruby 中使用 Ruby 驱动程序,我可以在大约 1.5 秒内运行一批 10,000 条准备好的语句写入,但我也可以运行 10,000 个异步请求约 1.5 秒。当您说您的 1000 批 10000 需要 545 秒时,您只是添加测试代码打印的测量值,还是整个运行?我假设是前者。
    【解决方案2】:

    你不是唯一经历过这种情况的人。不久前我写了一篇博客文章,更多地关注 CQL 和 thrift 之间的转换,但是有一些链接指向看到相同内容的人的邮件列表问题(宽行插入的性能问题是我最初调查的动机): http://thelastpickle.com/blog/2013/09/13/CQL3-to-Astyanax-Compatibility.html

    总而言之 - CQL 非常适合为刚接触 Cassandra 的人减轻键入和理解数据模型的负担。 DataStax 驱动程序编写得很好,并且包含许多有用的功能。

    但是,对于宽行插入,Thrift API 稍微快一点。 Netflix 博客并没有过多地讨论这个特定的用例。此外,只要人们在使用 Thrift API(很多人都在使用),它就不是遗留的。这是一个 ASF 项目,因此不是由任何单一供应商运行的。

    一般来说,对于任何基于 Cassandra 的应用程序,如果您找到一种方法来满足(或经常超过)工作负载的性能要求,请坚持下去。

    【讨论】:

      【解决方案3】:

      您可以尝试一些事情...在您的cassandra.yaml 中(这是 Cassandra 1.2.x,也许参数在 2.x 中的调用方式有所不同):

      • 禁用行缓存 (row_cache_size_in_mb: 0)
      • 在内存中的行溢出到磁盘之前增加内存限制 (min_memory_compaction_limit_in_mb),仅当您看到一些表明确实发生溢出的日志输出时才这样做
      • 确保正确配置 num_tokens / initial_token 值,以便行在节点间分布

      您可以尝试的其他方法:

      • 将集群中的所有节点 IP 提供给客户端,而不仅仅是一个
      • 为每个 Cassandra 节点提供更多 RAM
      • 尝试多线程运行测试
      • 如果您运行 Cassandra,请确保您有 JNA installed 并且正在使用中 在 Linux 上

      需要澄清的事情:

      • 您是否已通过nodetool 确认 3 个节点已分别找到 其他?
      • nodetool 对您的 3 个节点的负载分布有何看法?
      • 虚拟集群的物理主机对 CPU 和 I/O 使用情况?也许它只是被刷爆了?

      【讨论】:

      • 感谢调优提示。但我认为这三个节点工作正常。我确实运行了另一个刚刚插入大 blob 的测试,我得到了可以接受的性能。 Nodetool 的输出看起来也不错。我想我必须在一个节俭的 API 中实现相同的基准测试,看看它是如何执行的。
      • 我现在使用 Astyanax thrift API(通过 ColumnListMutation)实现了基本相同的功能,并且性能提高了 15 倍。我很乐意使用较低级别的 API。事实上,我更喜欢它而不是 CQL,因为它更接近物理数据布局。但似乎不鼓励和不推荐使用它。
      • @RüdigerKlaehn 根据 Astyanax 开发人员的说法,thrift API 稍微快一点techblog.netflix.com/2013/12/astyanax-update.html,所以这肯定有另一个原因。
      • 可能是我对准备好的语句做错了什么。但是什么?代码很简单。
      猜你喜欢
      • 2014-07-02
      • 2019-03-04
      • 1970-01-01
      • 2012-10-24
      • 2015-03-25
      • 2012-02-26
      • 2011-08-22
      • 1970-01-01
      • 2015-11-09
      相关资源
      最近更新 更多