База данных о постоянстве (MySQL/MongoDB/Cassandra/BigTable/BigData) Vs Массив без сохранения (PHP/PYTHON)

Насколько полезно использовать Python/PHP Nonpersistent array для хранения 6 ГБ + данных с 800+ миллионами строк в ОЗУ, вместо использования MySQL/MongoDB/Cassandra/База данных BigTable/BigData (база данных Persistence), когда дело доходит до скорости/задержки при простом выполнении запроса?

Например, поиск одного имени в 800+ миллионах строк за 1 секунду: возможно ли это? Кто-нибудь имеет опыт работы с набором данных более 1-2 миллиардов строк и получением результата в течение 1 секунды для простого поискового запроса?

Есть ли лучшая, проверенная методология для работы с миллиардами строк?

Ответы

Ответ 1

Он должен быть очень большим, примерно на 4-5 порядков быстрее. База данных хранит записи в блоках размером 4 КБ (обычно) и должна приносить каждый такой блок в память, для которого требуется несколько миллисекунд. Разделите размер вашего стола на 4KB и получите изображение. Напротив, соответствующие времена для данных в памяти обычно наносекунды. Нет сомнений в том, что память быстрее, реальный вопрос заключается в том, что у вас достаточно памяти и как долго вы можете хранить свои данные там.

Тем не менее, это выше для запроса select * from table. Если вы хотите select * from table where name=something, вы можете создать индекс для имени, чтобы в базе данных не нужно было сканировать весь файл, и результаты должны быть намного лучше, вероятно, очень удобными для практического использования.

Ответ 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 или с любой другой базой данных.

Ответ 3

  • Вы все еще можете воспользоваться поиском на основе ОЗУ и по-прежнему иметь дополнительные функции, которые предоставляют специализированные базы данных по сравнению с обычным хэшмапом/массивом в ОЗУ.

  • Ваша задача с помощью поиска на основе ram быстрее искать и избегать сетевых накладных расходов. Однако оба они могут быть достигнуты путем размещения базы данных локально, или сеть даже не будет накладными расходами для небольших данных, таких как имена.

  • По методу массива ОЗУ устойчивость приложений уменьшается, так как у вас есть единая точка отказа, нет простого моментального снимка, т.е. вам придется выполнять несколько утечки данных каждый раз, когда вы изменение приложения или перезапуск, и вы всегда будете ограничены одним шаблоном запросов и, возможно, не сможете развиваться в будущем.

  • Аналогично хорошие альтернативы с разумно сопоставимой пропускной способностью могут быть redis в конфигурации кластера или master-slave или на аэродинамическом диске на SSD. Вы получаете преимущества постоянных снимков, высокой пропускной способности, распределения и устойчивости с помощью осколков/кластеризации, т.е. 1/8 данных в 8 случаях, так что нет единственной точки отказа.