Например, поиск одного имени в 800+ миллионах строк за 1 секунду: возможно ли это? Кто-нибудь имеет опыт работы с набором данных более 1-2 миллиардов строк и получением результата в течение 1 секунды для простого поискового запроса?
Ответ 2
4 байта (int) * 1_000_000_000 ~ 4 Гб
4 байта (int) * 1_000_000_000/64 байта = 62500000 раз (для кеша L1)
4 байта (int) * 1_000_000_000/64 байта = 62500000 раз (для кеша L2)
Взятая задержка, которую все должны знать для основной памяти 100 нс отсюда, мы получаем 100 с.
Если весь внутренний кеш L1 (64 байта для Intel), он близок к 31,25 мс.
Но до этого есть также кеши L2/L3 (одинаковый размер строки) - 218,75 мс.
Вы можете видеть, что читать 1 Мб последовательно (другими словами, это лучший случай), поэтому для 4 Гб это 4024 * 250 мкс = 1006000 мкс ~ = 1 с.
У SSD-диска меньше латентности, но это не так просто. Существует исследование (возможно, истекшее сейчас) показало, что большинство дисков SSD, которые доступны для всех, не могут выдерживать действительно очень высокие нагрузки (причины - они терпят неудачу и более интересны - у них есть собственный сборщик мусора, который может добавить большие задержка). Но также есть решения, адаптируемые к среде SSD-дисков, например, Aerospike, и, как правило, SSD быстрее, чем жесткий диск.
Просто чтобы понять. На типичном ноутбуке (my: intel core i5, x64, 16Gb RAM) мне нужно около 580 мс до 875 мс для вычисления длинной суммы за 1 миллиард элементов int.
Я также могу видеть скорость Clickhouse от 300 Мбит/с до 354,66 Мбит/с для вычисления суммы на столбце Int32 на моем SSD.
(обратите внимание, что сумма в обоих случаях не имеет смысла, из-за переполнения типа)
Конечно, у нас также есть CUDA как вариант или даже простая потоковая передача (предположим, что несколько потоков будут вычислять сумму, мы можем легко скопировать).
Итак... Что мы можем сделать?
Существует два типа масштабирования: вертикальный и горизонтальный. Большинство баз данных предпочитают горизонтальное масштабирование, я полагаю, причина проста.
Горизонтальное масштабирование проще, чем вертикальное. Для вертикального масштабирования вам нужны люди (или вы должны иметь по своему усмотрению) очень хороший опыт в разных областях.
Например, из моей жизни я должен много знать о архитектурах Java/HotSpot/OS/Множество технологий/фреймворков/БД для написания или понимания преимуществ различных решений при создании высокопроизводительных приложений/алгоритмов.
И это только начало, есть гораздо более сложные эксперты, чем я.
Другие базы данных используют вертикальное масштабирование, более точно они используют специальные оптимизации для определенных сценариев/запросов.
Все решения являются компромиссом между различными операциями.
Например, для проблемы Top N Vertica и друид имеют конкретные реализации, которые решают именно эту задачу.
В Cassandra, чтобы сделать все выбор быстро, вы должны создать несколько таблиц или несколько представлений для одной таблицы с различным представлением, эффективным для конкретного запроса, конечно, тратя больше места для хранения из-за дублирования данных.
Одна из самых больших проблем, которые даже вы можете прочитать 1 миллиард строк за одну секунду - вы не можете писать одновременно в одной и той же таблице.
Другими словами, главная проблема - трудно удовлетворить все пользовательские запросы для всех пользовательских задач одновременно.
Есть ли лучшая, проверенная методология для работы с миллиардами строк?
Некоторые примеры:
- ОЗУ с комбинацией файлов с отображением памяти (для хранения служебных данных): когда вы используете файлы с отображением памяти (или файлы с произвольным доступом), вы можете хранить больше данных и с хорошими дисками, получать высокие скорости чтения/записи.
- Индексированные сегменты памяти: идея разделяет ваши данные по индексу, который будет ассоциирован с сегментом, и выполняет запрос внутри сегментов без обработки всех данных.
- Специальные хранилища задач: когда вы знаете свои данные и требования, вы можете создать хранилище, которое будет очень эффективным для него, но не для других (в вашем случае "нахождение одного имени" вы можете индексировать и разбивать данные на буквы, префикс и т.д.);
- Выполнять сложные вычисления в C/С++, иногда это может быть быстрее.:) Это немного смешно, но настоящие истории. Через мир рта С++ более сложна для программирования и поддержки, но проще писать быстрое приложение на С++, если у вас достаточно опыта;
- Дублирование данных (хранить данные разными способами для разных запросов);
- Генерация кода (генерировать код "на лету", который будет оптимизирован для каждой конкретной задачи);
- Многопоточность, конечно: выполните одну задачу в нескольких потоках, если вы можете эффективно использовать ресурсы процессора;
- Кэширование, конечно: кэширование resuls на основе дисков /RAM/network (я имею в виду внешние серверы кэшей);
В некоторых случаях использование собственного решения может быть более дорогостоящим (и эффективным), а затем обычным. В некоторых случаях это не...
Сравнение строк относительно сложно, поэтому, полагаю, вам нужно начать с вычисления того, сколько времени вам нужно сравнить с двумя строками.
Этот простой пример показывает, сколько времени нам нужно сравнить с двумя строками в Java.
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Threads(1)
public class StringEquals {
@Param({"0", "5", "10"})
int prefix;
String theSamePart, theSamePartQuery;
@Setup(Level.Invocation)
public void setData() {
String value = String.valueOf(ThreadLocalRandom.current().nextInt());
theSamePart = prefix > 0 ? value.substring(Math.min(prefix, value.length())) : value;
value = String.valueOf(ThreadLocalRandom.current().nextInt());
theSamePartQuery = prefix > 0 ? theSamePart + value.substring(Math.min(prefix, value.length())) : value;
}
@Benchmark
public boolean equals(StringEquals stringEquals) {
return stringEquals.theSamePart.equals(stringEquals.theSamePartQuery);
}
public static void main(String[] args) throws Exception {
new Runner(new OptionsBuilder()
.include(StringEquals.class.getSimpleName())
.measurementIterations(10)
.warmupIterations(10)
.build()).run();
}
}
Результаты:
Benchmark (prefix) Mode Cnt Score Error Units
StringEquals.equals 0 sample 3482270 0,047 ± 0,011 us/op
StringEquals.equals:equals·p0.00 0 sample 0,022 us/op
StringEquals.equals:equals·p0.50 0 sample 0,035 us/op
StringEquals.equals:equals·p0.90 0 sample 0,049 us/op
StringEquals.equals:equals·p0.95 0 sample 0,058 us/op
StringEquals.equals:equals·p0.99 0 sample 0,076 us/op
StringEquals.equals:equals·p0.999 0 sample 0,198 us/op
StringEquals.equals:equals·p0.9999 0 sample 8,636 us/op
StringEquals.equals:equals·p1.00 0 sample 9519,104 us/op
StringEquals.equals 5 sample 2686616 0,037 ± 0,003 us/op
StringEquals.equals:equals·p0.00 5 sample 0,021 us/op
StringEquals.equals:equals·p0.50 5 sample 0,028 us/op
StringEquals.equals:equals·p0.90 5 sample 0,044 us/op
StringEquals.equals:equals·p0.95 5 sample 0,048 us/op
StringEquals.equals:equals·p0.99 5 sample 0,060 us/op
StringEquals.equals:equals·p0.999 5 sample 0,238 us/op
StringEquals.equals:equals·p0.9999 5 sample 8,677 us/op
StringEquals.equals:equals·p1.00 5 sample 1935,360 us/op
StringEquals.equals 10 sample 2989681 0,039 ± 0,001 us/op
StringEquals.equals:equals·p0.00 10 sample 0,021 us/op
StringEquals.equals:equals·p0.50 10 sample 0,030 us/op
StringEquals.equals:equals·p0.90 10 sample 0,049 us/op
StringEquals.equals:equals·p0.95 10 sample 0,056 us/op
StringEquals.equals:equals·p0.99 10 sample 0,074 us/op
StringEquals.equals:equals·p0.999 10 sample 0,222 us/op
StringEquals.equals:equals·p0.9999 10 sample 8,576 us/op
StringEquals.equals:equals·p1.00 10 sample 325,632 us/op
Предположим, что вам нужны строки 1_000_000_000, вам нужно примерно 8_000_000_000 us = 8000 с для обработки 1 миллиарда строк в 99,99% случаях.
В отличие от этого мы могли бы попытаться сделать это параллельно:
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.*;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Threads(1)
public class SearchBillionForkJoin {
static final int availableProcessors = 4; // Runtime.getRuntime().availableProcessors()
static final int size = 10_000_000, bucketSize = size / availableProcessors;
static final int handlersCount = availableProcessors;
@Param({"50", "100"})
int spinner;
String[] a;
Callable<Integer>[] callables;
ForkJoinTask<Integer>[] tasks;
QueryHolder queryHolder;
@Setup(Level.Trial)
public void setup() {
callables = new Callable[handlersCount];
queryHolder = new QueryHolder();
a = new String[size];
for (int i = 0; i < callables.length; ++i) {
switch (i) {
case 0:
callables[i] = createForBucket(queryHolder, a, 0, bucketSize);
break;
case 1:
callables[i] = createForBucket(queryHolder, a, bucketSize, bucketSize * 2);
break;
case 2:
callables[i] = createForBucket(queryHolder, a, bucketSize * 2, bucketSize * 3);
break;
case 3:
callables[i] = createForBucket(queryHolder, a, bucketSize * 3, size);;
break;
}
}
tasks = new ForkJoinTask[handlersCount];
}
@Setup(Level.Invocation)
public void setData() {
for (int i = 0; i < a.length; ++i) {
a[i] = String.valueOf(ThreadLocalRandom.current().nextInt());
}
queryHolder.query = String.valueOf(ThreadLocalRandom.current().nextInt());
}
@Benchmark
public Integer forkJoinPoolWithoutCopy() {
try {
for (int i = 0; i < tasks.length; ++i) {
tasks[i] = ForkJoinPool.commonPool().submit(callables[i]);
}
Integer position = -1;
boolean findMore = true;
head:
while(position == -1 && findMore) {
findMore = false;
for (int i = 0; i < tasks.length; ++i) {
if (tasks[i].isDone() && !tasks[i].isCancelled()) {
final Integer value = tasks[i].get();
if (value > -1) {
position = value;
for (int j = 0; j < tasks.length; ++j) {
if (j != i && !tasks[j].isDone()) {
tasks[j].cancel(true);
}
}
break head;
}
} else {
findMore = true;
}
}
int counter = spinner;
while (counter > 0) --counter;
}
return position;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {
new Runner(new OptionsBuilder()
.include(SearchBillionForkJoin.class.getSimpleName())
.jvmArgs("-Xmx10G")
.measurementIterations(10)
.warmupIterations(10)
.build()).run();
}
static boolean isDone(ForkJoinTask[] tasks) {
for (int i = 0; i < tasks.length; ++i) {
if (!tasks[i].isDone()) {
return false;
}
}
return true;
}
static Callable<Integer> createForBucket(QueryHolder queryHolder, String[] a, int start, int end) {
return new Callable<Integer>() {
@Override
public Integer call() throws Exception {
for (int j = start; j < end; ++j) {
if (queryHolder.query.equals(a[j])) {
return j;
}
}
return -1;
}
};
}
static class QueryHolder {
String query = null;
}
}
Я использую 10_000_000 и 4 потока (для 4 процессорных ядер), потому что у меня недостаточно памяти для него.
Результаты по-прежнему не подходят.
Benchmark (spinner) Mode Cnt Score Error Units
SearchBillionForkJoin.forkJoinPoolWithoutCopy 50 sample 166 47,136 ± 1,989 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.00 50 sample 5,521 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.50 50 sample 47,055 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.90 50 sample 54,788 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.95 50 sample 56,653 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.99 50 sample 61,352 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.999 50 sample 63,635 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.9999 50 sample 63,635 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p1.00 50 sample 63,635 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy 100 sample 162 51,288 ± 4,031 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.00 100 sample 5,448 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.50 100 sample 49,840 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.90 100 sample 67,030 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.95 100 sample 90,505 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.99 100 sample 110,920 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.999 100 sample 121,242 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p0.9999 100 sample 121,242 ms/op
SearchBillionForkJoin.forkJoinPoolWithoutCopy:forkJoinPoolWithoutCopy·p1.00 100 sample 121,242 ms/op
Другими словами 63 635 мс * 100 = 6363,5 мс = 6 с.
Эти результаты могут быть улучшены, например, если вы можете использовать аффинные блокировки (один полный процессор в потоке). Но может быть слишком сложным.
Попробуйте использовать сегменты, чтобы показать идею:
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Threads(1)
public class SearchInMapBillionForkJoin {
static final int availableProcessors = 8; // Runtime.getRuntime().availableProcessors()
static final int size = 10_000_000, bucketSize = size / availableProcessors;
static final int handlersCount = availableProcessors;
Map<Integer, List<StringWithIndex>> strings;
QueryHolder queryHolder;
ForkJoinTask<Integer>[] tasks;
Callable<Integer>[] callables;
@Param({"50", "100"})
int spinner;
@Setup(Level.Trial)
public void setup() throws Exception {
queryHolder = new QueryHolder();
strings = new ConcurrentHashMap<>();
tasks = new ForkJoinTask[handlersCount];
callables = new Callable[handlersCount];
setData();
}
public void setData() throws Exception {
final int callableBucket = size / handlersCount;
for (int i = 0; i < handlersCount; ++i) {
callables[i] = createGenerateForBucket(strings, callableBucket);
tasks[i] = ForkJoinPool.commonPool().submit(callables[i]);
}
while(!isDone(tasks)) {
int counter = spinner;
while (counter > 0) --counter;
}
Map<Integer, Integer> distribution = new HashMap<>();
for (List<StringWithIndex> stringWithIndices : strings.values()) {
distribution.compute(stringWithIndices.size(), (key, value) -> value == null ? 1 : value + 1);
}
int maxListSize = 0;
for (int i = 0; i < handlersCount; ++i) {
Integer max = tasks[i].get();
if (max > maxListSize) {
maxListSize = max;
}
}
System.out.println("maxListSize = " + maxListSize);
System.out.println("list size distribution = " + distribution);
System.out.println("map size = " + strings.size());
distribution = null;
queryHolder.query = String.valueOf(ThreadLocalRandom.current().nextInt());
}
@Benchmark
public Integer findInSegment() {
final String query = this.queryHolder.query;
final Integer hashCode = query.hashCode();
final Map<Integer, List<StringWithIndex>> strings = this.strings;
if (strings.containsKey(hashCode)) {
List<StringWithIndex> values = strings.get(hashCode);
if (!values.isEmpty()) {
final int valuesSize = values.size();
if (valuesSize > 100_000) {
final int bucketSize = valuesSize / handlersCount;
callables[0] = createSearchForBucket(query, values, 0, bucketSize);
callables[1] = createSearchForBucket(query, values, bucketSize, bucketSize * 2);
callables[2] = createSearchForBucket(query, values, bucketSize * 2, bucketSize * 3);
callables[3] = createSearchForBucket(query, values, bucketSize * 3, values.size());
try {
for (int i = 0; i < callables.length; ++i) {
tasks[i] = ForkJoinPool.commonPool().submit(callables[i]);
}
Integer position = -1;
boolean findMore = true;
head:
while (position == -1 && findMore) {
findMore = false;
for (int i = 0; i < tasks.length; ++i) {
if (tasks[i].isDone() && !tasks[i].isCancelled()) {
final Integer value = tasks[i].get();
if (value > -1) {
position = value;
for (int j = 0; j < tasks.length; ++j) {
if (j != i && !tasks[j].isDone()) {
tasks[j].cancel(true);
}
}
break head;
}
} else {
findMore = true;
}
}
int counter = spinner;
while (counter > 0) --counter;
}
return position;
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
for (StringWithIndex stringWithIndex : values) {
if (query.equals(stringWithIndex.value)) {
return stringWithIndex.index;
}
}
}
}
}
return -1;
}
public static void main(String[] args) throws Exception {
new Runner(new OptionsBuilder()
.include(SearchInMapBillionForkJoin.class.getSimpleName())
.jvmArgs("-Xmx6G")
.measurementIterations(10)
.warmupIterations(10)
.build()).run();
}
static class StringWithIndex implements Comparable<StringWithIndex> {
final int index;
final String value;
public StringWithIndex(int index, String value) {
this.index = index;
this.value = value;
}
@Override
public int compareTo(StringWithIndex o) {
int a = this.value.compareTo(o.value);
if (a == 0) {
return Integer.compare(this.index, o.index);
}
return a;
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof StringWithIndex) {
return this.value.equals(((StringWithIndex) obj).value);
}
return false;
}
}
static class QueryHolder {
String query = null;
}
static Callable<Integer> createSearchForBucket(String query, List<StringWithIndex> values, int start, int end) {
return new Callable<Integer>() {
@Override
public Integer call() throws Exception {
for (int j = start; j < end; ++j) {
StringWithIndex stringWithIndex = values.get(j);
if (query.equals(stringWithIndex.value)) {
return stringWithIndex.index;
}
}
return -1;
}
};
}
static Callable<Integer> createGenerateForBucket(Map<Integer, List<StringWithIndex>> strings,
int count) {
return new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int maxListSize = 0;
for (int i = 0; i < count; ++i) {
String value = String.valueOf(ThreadLocalRandom.current().nextInt());
List<StringWithIndex> values = strings.computeIfAbsent(value.hashCode(), k -> new ArrayList<>());
values.add(new StringWithIndex(i, value));
if (values.size() > maxListSize) {
maxListSize = values.size();
}
}
return maxListSize;
}
};
}
static boolean isDone(ForkJoinTask[] tasks) {
for (int i = 0; i < tasks.length; ++i) {
if (!tasks[i].isDone()) {
return false;
}
}
return true;
}
}
Результаты:
Benchmark (spinner) Mode Cnt Score Error Units
SearchInMapBillionForkJoin.findInSegment 50 sample 5164328 ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.00 50 sample ≈ 10⁻⁵ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.50 50 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.90 50 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.95 50 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.99 50 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.999 50 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.9999 50 sample 0.009 ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p1.00 50 sample 18.973 ms/op
SearchInMapBillionForkJoin.findInSegment 100 sample 4642775 ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.00 100 sample ≈ 10⁻⁵ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.50 100 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.90 100 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.95 100 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.99 100 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.999 100 sample ≈ 10⁻⁴ ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p0.9999 100 sample 0.005 ms/op
SearchInMapBillionForkJoin.findInSegment:findInSegment·p1.00 100 sample 0.038 ms/op
Прежде чем делать какие-либо глобальные выводы, хорошо знать некоторую критику для этого примера:
- из-за артефакта контрольных данных имеет очень хорошее распределение среди list'sizes: один пример: maxListSize = 3, распределение размера списка = {1 = 9954167, 2 = 22843, 3 = 49}, размер карты = 9977059. maxListSize для всех итераций было всего 4.
- в результате мы никогда не заходим внутрь "if (valuesSize > 100_000)" branch;
- более того, в большинстве случаев мы, вероятно, не заходим внутрь "} else {for (StringWithIndex stringWithIndex: values) {", из-за "if (stringings.containsKey(hashCode))" условие;
- этот тест, в отличие от предыдущих тестов, работал на разных ПК (8 бит, 32 Гб оперативной памяти, amd64);
Здесь вы можете получить представление о том, что проверка есть ключ на карте (или сегменте памяти), очевидно, лучше, чем просматривать все данные. Эта тема очень широка. Есть много людей, которые работают с производительностью и могут сказать, что "Оптимизация производительности - это бесконечный процесс".:)
Я также должен напомнить, что "Предварительная оптимизация плохая", и от меня добавьте, что это не значит, что вы должны проектировать свою систему, не думая, иррационально.
Отказ от ответственности:
Вся эта информация может быть неправильной. Он предназначен только для информационных целей и не может быть включен в какой-либо договор. Прежде чем использовать его для производственных сценариев, вы должны проверить самостоятельно. И вы не должны использовать эту информацию в производственном коде, ссылается на меня. Я не несу ответственности за возможную потерю денег. Вся эта информация не относится к компаниям, где я когда-либо работал. Я не связан ни с одной из MySQL/MongoDB/Cassandra/BigTable/BigData, а также Apache Ignite/Hazelcast/Vertica/Clickhouse/Aerospike или с любой другой базой данных.