kinerja pemuatan rendah saat memasukkan banyak baris ke dalam Spanner menggunakan jdbc

Aug 19 2020

Latar belakang: Saya mencoba memuat file data berformat TSV (dibuang dari database MySQL) ke dalam tabel GCP Spanner.

  • perpustakaan klien: dependensi JDBC Spanner resmi v1.15.0
  • skema tabel: dua kolom tipe string dan sepuluh kolom tipe int
  • Instance GCP Spanner: dikonfigurasi sebagai nam6 multi-region dengan 5 node

Program pemuatan saya berjalan di VM GCP dan merupakan klien eksklusif yang mengakses instance Spanner. Komit otomatis diaktifkan. Penyisipan batch adalah satu-satunya operasi DML yang dijalankan oleh program saya dan ukuran batch sekitar 1500. Dalam setiap komit, ini sepenuhnya menggunakan batas mutasi, yaitu 20000. Dan pada saat yang sama, ukuran komit di bawah 5MB (nilainya dari dua kolom tipe string berukuran kecil). Baris dipartisi berdasarkan kolom pertama dari kunci utama sehingga setiap komit dapat dikirim ke beberapa partisi untuk kinerja yang lebih baik.

Dengan semua konfigurasi dan optimasi di atas, kecepatan penyisipan hanya sekitar 1k baris per detik. Ini benar-benar mengecewakan saya karena saya memiliki lebih dari 800 juta baris untuk disisipkan. Saya perhatikan bahwa dokumen resmi menyebutkan kira-kira. penulisan puncak (total QPS) adalah 1800 untuk instance Spanner multi-region.

Jadi saya punya dua pertanyaan di sini:

  1. Mempertimbangkan QPS tulis puncak yang rendah, apakah itu berarti GCP tidak mengharapkan atau tidak mendukung pelanggan untuk memigrasi kumpulan data besar ke instance Spanner multi-region?
  2. Saya melihat latensi baca yang tinggi dari pemantauan Spanner. Saya tidak punya permintaan baca. Dugaan saya adalah bahwa menulis baris Spanner perlu terlebih dahulu membaca dan memeriksa apakah ada baris dengan kunci utama yang sama. Jika tebakan saya benar, mengapa butuh banyak waktu? Jika tidak, dapatkah saya mendapatkan panduan tentang bagaimana operasi baca ini terjadi?

Jawaban

KnutOlavLoite Aug 19 2020 at 15:50

Tidak begitu jelas bagi saya bagaimana tepatnya Anda menyiapkan aplikasi klien yang memuat data. Kesan awal saya adalah bahwa aplikasi klien Anda mungkin tidak cukup menjalankan transaksi secara paralel. Anda biasanya dapat memasukkan secara signifikan lebih dari 1.000 baris / detik, tetapi Anda harus melakukan beberapa transaksi secara paralel, mungkin dari beberapa VM. Saya menggunakan contoh sederhana berikut untuk menguji throughput beban dari mesin lokal saya ke instance Spanner node tunggal , dan itu memberi saya throughput sekitar 1.500 baris / detik.

Penyiapan multi-node menggunakan aplikasi klien yang berjalan di satu atau beberapa VM di wilayah jaringan yang sama dengan instance Spanner Anda seharusnya dapat mencapai volume yang lebih tinggi dari itu.

import com.google.api.client.util.Base64;
import com.google.common.base.Stopwatch;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class TestJdbc {

  public static void main(String[] args) {
    final int threads = 512;
    ExecutorService executor = Executors.newFixedThreadPool(threads);
    watch = Stopwatch.createStarted();
    for (int i = 0; i < threads; i++) {
      executor.submit(new InsertRunnable());
    }
  }

  static final AtomicLong rowCount = new AtomicLong();
  static Stopwatch watch;

  static final class InsertRunnable implements Runnable {
    @Override
    public void run() {
      try (Connection connection =
          DriverManager.getConnection(
              "jdbc:cloudspanner:/projects/my-project/instances/my-instance/databases/my-db")) {
        while (true) {
          try (PreparedStatement ps =
              connection.prepareStatement("INSERT INTO Test (Id, Col1, Col2) VALUES (?, ?, ?)")) {
            for (int i = 0; i < 150; i++) {
              ps.setLong(1, rnd.nextLong());
              ps.setString(2, randomString(100));
              ps.setString(3, randomString(100));
              ps.addBatch();
              rowCount.incrementAndGet();
            }
            ps.executeBatch();
          }
          System.out.println("Rows inserted: " + rowCount);
          System.out.println("Rows/second: " + rowCount.get() / watch.elapsed(TimeUnit.SECONDS));
        }
      } catch (SQLException e) {
        throw new RuntimeException(e);
      }
    }

    private final Random rnd = new Random();

    private String randomString(int maxLength) {
      byte[] bytes = new byte[rnd.nextInt(maxLength / 2) + 1];
      rnd.nextBytes(bytes);
      return Base64.encodeBase64String(bytes);
    }
  }
}

Ada juga beberapa hal lain yang dapat Anda coba sesuaikan untuk mendapatkan hasil yang lebih baik:

  • Mengurangi jumlah baris per kelompok dapat menghasilkan hasil keseluruhan yang lebih baik.
  • Jika memungkinkan, menggunakan InsertOrUpdateobjek mutasi jauh lebih efisien daripada menggunakan pernyataan DML (lihat contoh di bawah).

Contoh penggunaan Mutationsebagai ganti DML:

import com.google.api.client.util.Base64;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.jdbc.CloudSpannerJdbcConnection;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class TestJdbc {

  public static void main(String[] args) {
    final int threads = 512;
    ExecutorService executor = Executors.newFixedThreadPool(threads);
    watch = Stopwatch.createStarted();
    for (int i = 0; i < threads; i++) {
      executor.submit(new InsertOrUpdateMutationRunnable());
    }
  }

  static final AtomicLong rowCount = new AtomicLong();
  static Stopwatch watch;

  static final class InsertOrUpdateMutationRunnable implements Runnable {
    @Override
    public void run() {
      try (Connection connection =
          DriverManager.getConnection(
              "jdbc:cloudspanner:/projects/my-project/instances/my-instance/databases/my-db")) {
        CloudSpannerJdbcConnection csConnection = connection.unwrap(CloudSpannerJdbcConnection.class);
        CloudSpannerJdbcConnection csConnection =
            connection.unwrap(CloudSpannerJdbcConnection.class);
        while (true) {
          ImmutableList.Builder<Mutation> builder = ImmutableList.builder();
          for (int i = 0; i < 150; i++) {
            builder.add(
                Mutation.newInsertOrUpdateBuilder("Test")
                    .set("Id")
                    .to(rnd.nextLong())
                    .set("Col1")
                    .to(randomString(100))
                    .set("Col2")
                    .to(randomString(100))
                    .build());
            rowCount.incrementAndGet();
          }
          csConnection.write(builder.build());
          System.out.println("Rows inserted: " + rowCount);
          System.out.println("Rows/second: " + rowCount.get() / watch.elapsed(TimeUnit.SECONDS));
        }
        }
      } catch (SQLException e) {
        throw new RuntimeException(e);
      }
    }

    private final Random rnd = new Random();

    private String randomString(int maxLength) {
      byte[] bytes = new byte[rnd.nextInt(maxLength / 2) + 1];
      rnd.nextBytes(bytes);
      return Base64.encodeBase64String(bytes);
    }
  }
}

Contoh sederhana di atas memberi saya throughput sekitar 35.000 baris / detik tanpa penyetelan lebih lanjut.

INFORMASI TAMBAHAN 2020-08-21 : Alasan objek mutasi lebih efisien daripada pernyataan DML (batch), adalah karena pernyataan DML diubah secara internal untuk membaca kueri oleh Cloud Spanner, yang kemudian digunakan untuk membuat mutasi. Konversi ini perlu dilakukan untuk setiap pernyataan DML dalam satu batch, yang berarti bahwa batch DML dengan 1.500 pernyataan penyisipan sederhana akan memicu 1.500 kueri baca (kecil) dan perlu dikonversi menjadi 1.500 mutasi. Ini kemungkinan besar juga merupakan alasan di balik latensi baca yang Anda lihat dalam pemantauan Anda.

Apakah Anda berkeberatan untuk berbagi beberapa informasi lebih lanjut tentang seperti apa tampilan aplikasi klien Anda dan berapa banyak contoh yang sedang Anda jalankan?

RedPandaCurios Aug 20 2020 at 23:30

Dengan lebih dari 800 juta baris untuk disisipkan, dan melihat bahwa Anda adalah programmer Java, dapatkah saya menyarankan menggunakan Beam di Dataflow?

The penulis spanner di Beam dirancang untuk menjadi seefisien mungkin dengan menulis nya - pengelompokan baris dengan kunci yang sama, dan batching mereka seperti yang Anda lakukan. Beam on Dataflow juga dapat menggunakan beberapa VM pekerja untuk menjalankan beberapa pembacaan file dan penulisan kunci pas secara paralel ...

Dengan contoh kunci pas multiregion, Anda seharusnya bisa mendapatkan kecepatan penyisipan sekitar 1800 baris per node per detik (lebih banyak jika baris kecil dan bertumpuk, seperti yang disarankan oleh balasan Knut) dan dengan 5 node kunci pas, Anda mungkin dapat memiliki antara 10 dan 20 utas importir berjalan secara paralel - baik menggunakan program importir Anda atau menggunakan Dataflow.

(pengungkapan: Saya adalah pengelola Beam SpannerIO)