задний план
Прошлые проблемы с производительностью промежуточного программного обеспечения и текущие1-й конкурс производительности данных PolarDBВсе они связаны с файловыми операциями, и рациональное проектирование архитектуры и правильное ограничение скорости чтения и записи стали ключом к достижению лучших результатов в конкурентной борьбе. Во время конкурса я получил отзывы от нескольких читателей и друзей официального аккаунта, большинство из которых выразили такие затруднения: «Мне очень интересен конкурс, но я не знаю, как начать», «Я могу запустить результатов, но по сравнению с первым рядом. Конкурсанты, результаты отличаются более чем в 10 раз"... Чтобы позволить большему количеству читателей участвовать в подобных соревнованиях в будущем, я просто организую некоторые лучшие практики для файловых операций ввода-вывода без включая общий дизайн системной архитектуры.Я надеюсь, что введение этой статьи позволит вам с радостью участвовать в подобных задачах производительности в будущем.
Сортировка очков знаний
В этой статье основное внимание уделяется файловым операциям, связанным с Java. Для их понимания требуются некоторые предварительные условия, такие как PageCache, Mmap (отображение памяти), DirectByteBuffer (кэш вне кучи), последовательное чтение и запись, произвольное чтение и запись... полное понимание, Но, по крайней мере, знать, что они из себя представляют, потому что в этой статье в основном будут описаны эти точки знаний.
Знакомство с FileChannel и MMAP
Во-первых, наиболее важным моментом в соревновании типов файлового ввода-вывода является выбор способа чтения и записи файлов.Сколько видов файлового ввода-вывода существует в JAVA? Собственные методы чтения и записи можно условно разделить на три типа: обычный ввод-вывод, FileChannel (файловый канал) и MMAP (отображение памяти). Их тоже легко отличить, например FileWriter, FileReader существуют вjava.ioВ пакете они относятся к обычному IO; FileChannel существует в пакете java.nio, который является своего рода NIO, но обратите внимание, что NIO не обязательно означает неблокирующий, FileChannel здесь блокирующий; более особенным является последний MMAP, который представляет собой особый способ чтения и записи файлов, производных от FileChannel, вызывающий метод карты, который называется отображением памяти.
Способы использования FIleChannel:
FileChannel fileChannel = new RandomAccessFile(new File("db.data"), "rw").getChannel();
Как получить ММАП:
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, filechannel.size();
MappedByteBuffer — это операционный класс MMAP в JAVA.
Мы отказались от традиционного метода ввода-вывода для передачи байтов и сосредоточились на различии между двумя методами чтения и записи, FileChannel и MMAP.
FileChannel чтение и запись
// 写
byte[] data = new byte[4096];
long position = 1024L;
//指定 position 写入 4kb 的数据
fileChannel.write(ByteBuffer.wrap(data), position);
//从当前文件指针的位置写入 4kb 的数据
fileChannel.write(ByteBuffer.wrap(data));
// 读
ByteBuffer buffer = ByteBuffer.allocate(4096);
long position = 1024L;
//指定 position 读取 4kb 的数据
fileChannel.read(buffer,position);
//从当前文件指针的位置读取 4kb 的数据
fileChannel.read(buffer);
Большую часть времени FileChannel имеет дело с классом ByteBuffer. Вы можете понимать его как класс инкапсуляции byte[], который предоставляет богатый API для управления байтами. Студенты, которые не знакомы с ним, могут ознакомиться с его API. Стоит отметить, что оба метода записи и чтенияпотокобезопасностьДа, FileChannel внутренне передаетprivate final Object positionLock = new Object();
блокировки для контроля параллелизма.
Почему FileChannel быстрее обычного ввода-вывода? Это может быть неточно, потому что, если вы хотите использовать его правильно, FileChannel может показать свою фактическую производительность только тогда, когда он записывает целое число, кратное 4 КБ за раз.Это связано с тем, что FileChannel использует буфер памяти, такой как ByteBuffer, который позволяет нам быть очень эффективными, точно контролировать размер записываемого диска, чего нельзя достичь с помощью обычного ввода-вывода. 4кб обязательно быстрее? Это не строго, это в основном зависит от структуры диска вашей машины и зависит от операционной системы, файловой системы и ЦП.Например, диск в задаче производительности промежуточного программного обеспечения, по крайней мере, 64 КБ может быть записано за раз для достижения максимального количества операций ввода-вывода в секунду.
Однако диск PolarDB совершенно другой, можно сказать, что он чрезвычайно прочный, конкретная производительность не будет исследована, потому что игра все еще продолжается, но с навыками бенчмаркинга мы можем полностью ее измерить.
Еще один момент заключается в том, что FileChannel очень эффективен.Прежде чем представить это, я хотел бы задать вопрос: записывает ли FileChannel данные непосредственно в ByteBuffer на диск? Подумайте несколько секунд... ответ: НЕТ. Данные в ByteBuffer и данные на диске разделены слоем, который называется PageCache, то есть слоем кеша между пользовательской памятью и диском. Все мы знаем, что скорость дискового ввода-вывода и памяти ввода-вывода отличается на несколько порядков. Мы можем думать, что запись filechannel.write в PageCache предназначена для завершения операции удаления диска, но на самом деле операционная система, наконец, помогает нам завершить окончательную запись PageCache на диск.После понимания этой концепции вы должны понять, почему FileChannel предоставляет метод force(), используемый для уведомления операционной системы о необходимости своевременной очистки.
Точно так же, когда мы используем FileChannel для операций чтения, мы также проходим через три этапа: диск -> PageCache -> пользовательская память.Для ежедневных пользователей вы можете игнорировать PageCache, но в качестве претендента PageCache находится в категории. Его нельзя игнорировать. в процессе настройки. Мы не будем слишком много рассказывать здесь об операции чтения. Мы еще раз упомянем ее в следующем резюме. Здесь она используется для ознакомления с концепцией PageCache.
MMAP читать и писать
// 写
byte[] data = new byte[4];
int position = 8;
//从当前 mmap 指针的位置写入 4b 的数据
mappedByteBuffer.put(data);
//指定 position 写入 4b 的数据
MappedByteBuffer subBuffer = mappedByteBuffer.slice();
subBuffer.position(position);
subBuffer.put(data);
// 读
byte[] data = new byte[4];
int position = 8;
//从当前 mmap 指针的位置读取 4b 的数据
mappedByteBuffer.get(data);
//指定 position 读取 4b 的数据
MappedByteBuffer subBuffer = mappedByteBuffer.slice();
subBuffer.position(position);
subBuffer.get(data);
FileChannel достаточно мощный, что еще может сделать MappedByteBuffer? Пожалуйста, позвольте мне сначала немного продать и рассказать о мерах предосторожности при использовании MappedByteBuffer.
когда мы выполняемfileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1.5 * 1024 * 1024 * 1024);
После этого наблюдайте за изменениями на диске, у вас сразу получится файл 1.5G, но в это время содержимое файла все 0 (байт 0). Это соответствует китайскому описанию MMAP: файлы, отображаемые в памяти, любые операции, которые мы выполняем с MappedByteBuffer в памяти позже, будут окончательно сопоставлены с файлом,
mmap сопоставляет файл с виртуальной памятью в пользовательском пространстве, устраняя процесс копирования из буфера ядра в пользовательское пространство.Место в файле имеет соответствующий адрес в виртуальной памяти, и с файлом можно манипулировать как с оперативной памятью Ю поместил весь файл в память, но он не будет потреблять физическую память до фактического использования данных, и не будет операций по чтению и записи на диск.Только когда данные действительно используются, то есть когда изображение готово к рендерингу на экране, система управления виртуальной памятью VMS загружает соответствующие блоки данных с диска в физическую память для рендеринга в соответствии с механизмом загрузки при отказе страницы. Такой метод чтения и записи файлов сокращает объем копирования данных из кэша ядра в пространство пользователя и является очень эффективным.
Прочитав чуть более официальное описание, вам может быть немного любопытно, что такое MMAP: если существует такая мощная черная технология, в чем смысл существования FileChannel? И во многих статьях в Интернете говорится, что производительность MMAP-операций над большими файлами на порядок лучше, чем у FileChannel! Однако из того, что я знаю из игры, MMAP не является серебряной пулей для файлового ввода-вывода, это всего лишьСценарии, которые записывают очень небольшой объем данных за разОн может показать лишь немного лучшую производительность, чем FileChannel. Тогда я вам скажу то, что вас расстраивает, по крайней мере, использование MappedByteBuffer в JAVA — вещь очень хлопотная и болезненная, в основном проявляющаяся в трех моментах:
- При использовании MMAP необходимо указать размер карты памяти, а размер карты ограничен примерно 1,5 Г. Повторение карты приведет к восстановлению и перераспределению виртуальной памяти, что очень плохо для ситуации, когда размер файла неизвестен.
- MMAP использует виртуальную память. Как и PageCache, он управляется операционной системой для очистки диска. Хотя им можно управлять вручную с помощью force(), время не очень хорошее, и это будет головной болью в сценариях с небольшим объемом памяти.
- Проблема восстановления MMAP, когда MappedByteBuffer уже не нужен, можно вручную освободить занятую виртуальную память, но... способ очень странный.
public static void clean(MappedByteBuffer mappedByteBuffer) {
ByteBuffer buffer = mappedByteBuffer;
if (buffer == null || !buffer.isDirect() || buffer.capacity() == 0)
return;
invoke(invoke(viewed(buffer), "cleaner"), "clean");
}
private static Object invoke(final Object target, final String methodName, final Class<?>... args) {
return AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
try {
Method method = method(target, methodName, args);
method.setAccessible(true);
return method.invoke(target);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
});
}
private static Method method(Object target, String methodName, Class<?>[] args)
throws NoSuchMethodException {
try {
return target.getClass().getMethod(methodName, args);
} catch (NoSuchMethodException e) {
return target.getClass().getDeclaredMethod(methodName, args);
}
}
private static ByteBuffer viewed(ByteBuffer buffer) {
String methodName = "viewedBuffer";
Method[] methods = buffer.getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals("attachment")) {
methodName = "attachment";
break;
}
}
ByteBuffer viewedBuffer = (ByteBuffer) invoke(buffer, methodName);
if (viewedBuffer == null)
return buffer;
else
return viewed(viewedBuffer);
}
Да, вы не ошиблись, такой длинный код предназначен только для повторного использования MappedByteBuffer.
Поэтому я предлагаю сначала использовать FileChannel для завершения первоначальной отправки кода. В сценарии, когда для обновления диска необходимо использовать небольшой объем данных (например, несколько байтов), замените его реализацией MMAP. В других случаях сценарии, FileChannel может быть полностью охвачен (при условии, что вы понимаете, как правильно использовать FileChannel). Что касается того, почему MMAP работает лучше, чем FileChannel при записи небольшого количества данных за раз, я не нашел теоретической основы.Если у вас есть соответствующие подсказки, пожалуйста, оставьте сообщение. Согласно теоретическому анализу, FileChannel также выполняет запись в память, но у него на один процесс копирования буфера ядра и пользовательского пространства больше, чем у MMAP, поэтому в экстремальных сценариях MMAP работает лучше. Что касается того, является ли виртуальная память, выделенная MMAP, настоящим PageCache, то я думаю, что ее можно приблизительно понимать как PageCache.
Последовательное чтение быстрее случайного чтения, а последовательная запись быстрее случайной записи.
Независимо от того, есть ли у вас механический жесткий диск или твердотельный накопитель, этот вывод очевиден. Хотя причины этого не одинаковы, сегодня мы не будем обсуждать древний носитель данных механический жесткий диск. Основное внимание уделяется твердотельному накопителю. взгляните на это Почему произвольное чтение и запись медленнее, чем последовательное чтение и запись. Несмотря на то, что состав каждого SSD и файловой системы различен, наш сегодняшний анализ по-прежнему информативен.
Во-первых, что такое последовательное чтение, что такое случайное чтение, что такое последовательная запись и что такое случайная запись? Может, у нас и не было таких сомнений, когда мы впервые соприкоснулись с файловыми операциями ввода-вывода, но когда мы писали и писали, то начали сомневаться в собственном понимании.Интересно, переживали ли вы подобный этап. это на время. Итак, давайте посмотрим на два фрагмента кода:
Метод записи 1: 64 потока, пользователь использует атомарную переменную для записи положения указателя записи и одновременно записывает
ExecutorService executor = Executors.newFixedThreadPool(64);
AtomicLong wrotePosition = new AtomicLong(0);
for(int i=0;i<1024;i++){
final int index = i;
executor.execute(()->{
fileChannel.write(ByteBuffer.wrap(new byte[4*1024]),wrote.getAndAdd(4*1024));
})
}
Режим записи 2: добавьте блокировку для записи, чтобы обеспечить синхронизацию.
ExecutorService executor = Executors.newFixedThreadPool(64);
AtomicLong wrotePosition = new AtomicLong(0);
for(int i=0;i<1024;i++){
final int index = i;
executor.execute(()->{
write(new byte[4*1024]);
})
}
public synchronized void write(byte[] data){
fileChannel.write(ByteBuffer.wrap(new byte[4*1024]),wrote.getAndAdd(4*1024));
}
Ответ заключается в том, что второй метод считается последовательной записью, и то же самое верно для последовательного чтения. Для файловых операций блокировка не очень страшная вещь, страшно не синхронизировать запись/чтение! Некоторые люди спросят: "Разве внутри FileChannel уже нет positionLock для обеспечения потокобезопасности записи? Зачем вам нужно добавлять синхронизацию самостоятельно?" Почему это быстро? Если я отвечу на просторечии, то это многопоточная одновременная запись и отсутствие синхронизации, что приведет к дырам в файлах, и порядок ее выполнения может быть
Время 1: позиция записи thread1 [0 ~ 4096)
Время 2: позиция записи thread3 [8194~12288)
Время 2: позиция записи thread2 [4096~8194)
Так что это не совсем "последовательная запись". Но не беспокойтесь о снижении производительности, вызванном блокировкой, в следующем обзоре мы представим оптимизацию: уменьшите конфликт блокировок при многопоточном чтении и записи посредством сегментирования файлов.
Теперь, чтобы проанализировать принцип, почему последовательное чтение быстрее, чем случайное чтение? Почему последовательная запись быстрее случайной записи? Эти два сравнения на самом деле работают в одном направлении: PageCache, как мы упоминали ранее, представляет собой слой кеша, расположенный между буфером приложения (пользовательской памятью) и файлом на диске (диском).
Взяв в качестве примера последовательное чтение, когда пользователь инициирует fileChannel.read(4kb), на самом деле происходят две вещи.
- ОС загружает 16 КБ с диска в PageCache, это называется упреждающим чтением.
- Операция копирует 4 КБ из PageCache в память пользователя.
В итоге мы получили доступ к 4кб пользовательской памяти, почему последовательное чтение быстрое? Считается, что когда пользователь продолжает обращаться к следующему содержимому диска [4 КБ, 16 КБ], доступ к нему осуществляется напрямую из PageCache. Только представьте, когда вам нужно получить доступ к 16 КБ содержимого диска, будет ли это быстрее для 4 дисковых операций ввода-вывода или 1 дискового ввода-вывода + 4 операций ввода-вывода памяти? Ответ очевиден, все дело в оптимизации PageCache.
Глубокое размышление: если памяти мало, повлияет ли это на выделение PageCache? Как определить размер PageCache, это фиксированные 16кб? Могу ли я отслеживать обращения к PageCache? В каких сценариях произойдет сбой PageCache, и если да, то какие меры нам потребуются?
Я провожу простые самовопросы и ответы на свои вопросы, и логика, стоящая за этим, все еще требует от читателей тщательного изучения:
- Когда памяти мало, это повлияет на предварительное чтение PageCache.В реальных измерениях литературной поддержки обнаружено не было.
- PageCache динамически настраивается и может быть настроен с помощью системных параметров Linux.По умолчанию занимает 20% от общей памяти.
- GitHub.com/Брендан Грег…Инструмент на github может отслеживать PageCache
- Это интересный момент оптимизации.Если использование PageCache для кэширования не поддается контролю, как насчет самостоятельного предварительного чтения?
Принцип последовательной записи такой же, как и у последовательного чтения, оба из которых зависят от PageCache и оставлены на размышление читателю.
Прямая память (вне кучи) и память в куче
Память в куче использовалась в предыдущем примере кода FileChannel:ByteBuffer.allocate(4 * 1024)
, ByteBuffer предоставляет нам другой способ выделения памяти вне кучи:ByteBuffer.allocateDirect(4 * 1024)
. Это приводит к ряду вопросов: когда следует использовать память в куче, а когда — прямую память?
Я не буду тратить слишком много времени на объяснение этого, просто перейду сразу к сравнению:
Память в куче | память вне кучи | |
---|---|---|
низкоуровневая реализация | массив, память JVM | unsafe.allocateMemory(size) возвращает прямую память |
ограничение размера выделения | Память JVM, настроенная с помощью -Xms-Xmx, связана, а размер массива ограничен.Во время тестирования было обнаружено, что когда свободная память JVM превышает 1,5 ГБ, будет сообщено об ошибке, когда ByteBuffer.allocate( 900м) | Может быть ограничено с уровня JVM через параметр -XX:MaxDirectMemorySize, и ограничено виртуальной памятью машины (то есть физической памятью не точно) |
вывоз мусора | разумеется | Когда DirectByteBuffer больше не используется, срабатывает внутренний обработчик очистки.Чтобы быть в безопасности, вы можете рассмотреть возможность ручного перезапуска: ((DirectBuffer) buffer).cleaner().clean(); |
метод копирования | Режим пользователя Режим ядра | Состояние ядра |
Некоторые рекомендации по работе с памятью в куче и вне кучи:
- Когда вам нужно подать заявку на большой блок памяти, память в куче будет ограничена, и может быть выделена только память вне кучи.
- Память вне кучи подходит для объектов со средним или длительным временем жизни. (Если это объект с коротким жизненным циклом, он будет переработан во время YGC, и FGC не повлияет на производительность приложения для объектов с большим объемом памяти и длительным жизненным циклом).
- Операции прямого копирования файлов или операции ввода-вывода. Прямое использование памяти вне кучи может уменьшить потребление памяти, копируемой из пользовательской памяти в системную.
- В то же время, комбинация пул + память вне кучи также может использоваться для повторного использования памяти вне кучи для объектов с коротким жизненным циклом, но с участием операций ввода-вывода (этот метод используется в Netty). Во время игры старайтесь не появляться часто
new byte[]
, создание области памяти, а затем ее повторное использование также требует больших накладных расходов, используяThreadLocal<ByteBuffer>
иThreadLocal<byte[]>
Часто это приносит вам неожиданные сюрпризы~ - Потребление памяти вне кучи больше, чем потребление памяти в куче, поэтому при выделении памяти вне кучи максимально используйте ее повторно.
Черная магия: НЕБЕЗОПАСНО
public class UnsafeUtil {
public static final Unsafe UNSAFE;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Мы можем использовать черную магию UNSAFE для достижения многих невообразимых вещей.Я представлю здесь несколько моментов.
Реализовать прямую память и копирование памяти:
ByteBuffer buffer = ByteBuffer.allocateDirect(4 * 1024 * 1024);
long addresses = ((DirectBuffer) buffer).address();
byte[] data = new byte[4 * 1024 * 1024];
UNSAFE.copyMemory(data, 16, null, addresses, 4 * 1024 * 1024);
Метод copyMemory может реализовать копирование между памятью, независимо от того, находится ли она внутри или вне кучи, 1~2 параметра — исходная сторона, 3~4 — целевая сторона, а пятый параметр — размер копии. Если это массив байтов в куче, передать первый адрес массива и фиксированную константу смещения ARRAY_BYTE_BASE_OFFSET, равную 16; если это память вне кучи, передать null и смещение прямой памяти, которое можно передать ( (DirectBuffer)) .address(), чтобы получить его. Почему бы просто не скопировать, а использовать UNSAFE? Конечно, потому что это быстро! несовершеннолетний! Кроме того: MappedByteBuffer также может использовать UNSAFE для копирования, чтобы добиться эффекта записи/чтения диска.
Что касается UNSAFE и тех черных технологий, то о них можно узнать отдельно, поэтому я не буду здесь вдаваться в подробности.
файловый раздел
Как упоминалось ранее, нам нужно блокировать запись и чтение при последовательном чтении и записи, и я неоднократно подчеркивал, что блокировка — это не страшно, и файловые операции ввода-вывода не так уж зависят от многопоточности. Однако последовательное чтение и запись после блокировки не должны заполнять дисковый ввод-вывод, и теперь мощный ЦП системы нельзя не выжимать, верно? Мы можем использовать разбиение файлов, чтобы добиться эффекта убийства двух зайцев одним выстрелом: оно не только обеспечивает последовательное чтение и запись, но и уменьшает конфликты блокировок.
Тогда снова возникает вопрос, насколько целесообразно? Если файлов слишком много, конфликт блокировок будет уменьшен; если файлов слишком много, фрагментация будет слишком серьезной. такой обмен? Теоретического ответа нет, сравните все~
Direct IO
Наконец, давайте обсудим метод ввода-вывода, о котором никогда раньше не упоминалось, Direct IO, что, Java и эта штука? Блогер, ты мне врал? Как вы мне раньше сказали, что есть только три метода ввода-вывода! Не ругайте меня, строго говоря, JAVA не поддерживает это нативно, но это можно сделать, вызывая нативные методы через JNA/JNI. Из приведенного выше рисунка видно: Direct IO обходит PageCache, но, как мы упоминали ранее, PageCache — это хорошо, почему бы его не использовать? После тщательного изучения действительно есть несколько сценариев, в которых Direct IO может сыграть свою роль, да, это то, о чем мы не упоминали ранее:случайное чтение. При использовании методов ввода-вывода, таких как fileChannel.read(), которые запускают предварительное чтение PageCache, мы на самом деле не хотим, чтобы операционная система что-то делала для нас, если только мы действительно не наступим на удачу, случайные чтения могут ударить по PageCache, но Шансы можно представить. Хотя Линус бездумно распылил Direct IO, он по-прежнему имеет свою ценность в сценариях произвольного чтения, уменьшая накладные расходы от блочного IO Layed (приблизительно понимаемого как диск) до кэша страниц.
Сказав это, как Java использует Direct IO? Есть ли ограничения? Как упоминалось ранее, Java в настоящее время не поддерживает изначально, но некоторые люди из лучших побуждений упаковали библиотеку Java JNA и реализовали Direct IO Java, адрес github:GitHub.com/racetrack/Джей…
int bufferSize = 20 * 1024 * 1024;
DirectRandomAccessFile directFile = new DirectRandomAccessFile(new File("dio.data"), "rw", bufferSize);
for(int i= 0;i< bufferSize / 4096;i++){
byte[] buffer = new byte[4 * 1024];
directFile.read(buffer);
directFile.readFully(buffer);
}
directFile.close();
Но следует отметить, что,DIO поддерживается только в системах Linux.Итак, парень, пора приступать к установке линукса. Стоит отметить, что говорится, что после выпуска Jdk10 Direct IO будет поддерживаться изначально, давайте подождем и увидим!
Суммировать
Все вышеизложенное является опытом, полученным из личной практики, а некоторые выводы не подтверждены литературой, поэтому, если есть какие-то ошибки, прошу меня поправить. Что касается конкурентного анализа конкурса производительности данных PolarDB, я напишу отдельную статью после матча-реванша, чтобы проанализировать, как использовать эти точки оптимизации.Конечно, многие люди знают эти советы, и общая архитектура дизайна определяет конечный результат. понимание файлового ввода-вывода, операционных систем, файловых систем, процессоров и языковых функций. Несмотря на то, что JAVA не пользуется популярностью для таких задач производительности, она все же доставляет массу удовольствия.Я надеюсь, что знание этих файловых операций ввода-вывода поможет вам, и увидимся на следующем соревновании~
Добро пожаловать в мою общедоступную учетную запись WeChat: «Обмен технологиями Кирито». На любые вопросы по статье будут даны ответы, что позволит расширить обмен технологиями, связанными с Java.