Эта статья проясняет, от принципа до применения системы ввода-вывода Java.

Java
Эта статья проясняет, от принципа до применения системы ввода-вывода Java.

В этой статье представлен принцип работы ввода-вывода операционной системы, проектирование ввода-вывода Java, базовое использование, распространенные методы и реализации высокопроизводительного ввода-вывода в проектах с открытым исходным кодом, а также подробное понимание способа высокопроизводительного ввода-вывода.

Базовые концепты

Прежде чем представить принципы ввода-вывода, давайте рассмотрим несколько основных понятий:

  • (1) Операционная система и ядро

Операционная система: системное программное обеспечение, управляющее аппаратными и программными ресурсами компьютера.ядро: основное программное обеспечение операционной системы, отвечающее за управление системными процессами, памятью, драйверами устройств, файлами и сетевыми системами и т. д., а также предоставление приложениям услуг безопасного доступа к компьютерному оборудованию.

  • 2 Пространство ядра и пространство пользователя

Для того, чтобы пользовательские процессы не могли напрямую работать с ядром и обеспечить безопасность ядра, операционная система делит адресное пространство памяти на две части:Ядро-пространство, используемый программой ядраПользовательское пространство, для использования пользовательскими процессами В целях безопасности пространство ядра и пространство пользователя изолированы, даже если пользовательская программа выйдет из строя, ядро ​​не пострадает.

  • 3 поток данных

Данные в компьютере основаны на передаче сигналов высокого и низкого напряжения во времени.Эти сигналы данных являются непрерывными и имеют фиксированное направление передачи, подобно потоку воды в водопроводе, поэтому концепция абстрактного потока данных ( поток ввода-вывода):Относится к упорядоченному набору байтов с началом и концом.,

Абстрактная роль потока данных:Реализовать развязку программной логики от базового оборудования., путем введения потока данных в качестве уровня абстракции между программой и аппаратным устройством, программирования для общего интерфейса ввода и вывода потока данных, а не конкретных характеристик оборудования, программа и базовое оборудование могут быть независимо и гибко заменены и расширены

Как работает ввод-вывод

1 дисковый ввод-вывод

Типичный диск ввода-вывода для чтения и записи работает следующим образом:

tips:DMA: Полное название Direct Memory Access, представляющее собой механизм, позволяющий периферийным устройствам (аппаратным подсистемам) напрямую обращаться к основной памяти системы. Основываясь на методе доступа DMA, передача данных между основной памятью системы и аппаратным устройством может сохранить все планирование ЦП.

Стоит отметить, что:

  • Операции чтения и записи реализованы на основе системных вызовов
  • Операции чтения и записи проходят через пользовательский буфер, буфер ядра, и процесс приложения не может напрямую управлять диском.
  • Когда процесс приложения читает, он должен блокироваться до тех пор, пока данные не будут прочитаны.

2 сетевых ввода/вывода

Вот введение в самую классическую модель блокирующего ввода-вывода:

tips:recvfrom, функция получения данных через сокет

Стоит отметить, что:

  • Операции чтения и записи сетевого ввода-вывода проходят через пользовательские буферы, буферы Sokcet.
  • Поток сервера блокируется от вызова recvfrom до тех пор, пока он не вернется с готовой дейтаграммой.После успешного завершения recvfrom поток начинает обрабатывать дейтаграмму.

Дизайн ввода/вывода Java

1 классификация ввода/вывода

Поток данных воплощается и реализуется на языке Java.Как правило, поток данных Java связан со следующими моментами:

  • (1) Направление потокаизвне в программу, называемуювходной поток; из программы наружу, называетсявыходной поток

  • (2) Единица данных потокаПрограмма использует байты как минимальную единицу данных для чтения и записи, называемуюбайтовый поток, с символами в качестве минимальной единицы данных для чтения и записи, называемойпоток символов

  • (3) Функциональные роли потоков

Поток чтения/записи данных с/на определенное устройство ввода-вывода (например, диск, сеть) или объект хранения (например, массив памяти), называемыйузел потока; Подключите и инкапсулируйте существующий поток и реализуйте функцию чтения/записи данных через инкапсулированный поток, который называетсяпоток процесса(или фильтровать поток);

2 интерфейс ввода/вывода

В пакете java.io есть куча классов операций ввода-вывода, их легко понять, когда вы новичок, на самом деле, есть правила, если внимательно их соблюдать: Эти классы операций ввода-вывода находятся вНа основе наследования 4 базовых абстрактных потоков, либо потока узла, либо потока обработки.

2.1 Четыре основных абстрактных потока

Пакет java.io содержит все классы, необходимые для потокового ввода-вывода.В пакете java.io есть четыре основных абстрактных потока, которые имеют дело с потоками байтов и потоками символов соответственно:

  • InputStream
  • OutputStream
  • Reader
  • Writer

2.2 Узловой поток

Имя класса ввода/вывода потока узла состоит из типа потока узла + типа абстрактного потока.Обычные типы узлов:

  • файл
  • Конвейерный конвейер связи между потоками внутри процесса
  • ByteArray/CharArray (массив байтов/массив символов)
  • StringBuffer/String (буфер строки/строка)

Создание потока узлов обычно передается в источнике данных в конструкторе, например:

FileReader reader = new FileReader(new File("file.txt"));
FileWriter writer = new FileWriter(new File("file.txt"));

2.3 Потоки обработки

Имя класса для обработки потокового ввода-вывода состоит из функции, инкапсулирующей существующий поток, и абстрактного типа потока.

  • буфер: Предоставляет функцию буферизации для данных, считываемых и записываемых потоком узла, и данные могут считываться и записываться пакетами на основе буфера для повышения эффективности. Распространенными являются BufferedInputStream, BufferedOutputStream.
  • Преобразование потока байтов в поток символов: Реализовано InputStreamReader, OutputStreamWriter
  • Преобразование потока байтов в данные базового типа: Здесь основные типы данных, такие как int, long, short, реализованы DataInputStream и DataOutputStream.
  • Преобразование потока байтов в экземпляр объекта: используется для реализации сериализации объектов, реализуемой ObjectInputStream и ObjectOutputStream.

Шаблон адаптера/декоратора применяется к потоку обработки, который преобразует/расширяет существующий поток.Создание потока обработки обычно передается в существующем потоке узла или потоке обработки в конструкторе:

FileOutputStream fileOutputStream = new FileOutputStream("file.txt");
// 扩展提供缓冲写
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
 // 扩展提供提供基本数据类型写
DataOutputStream out = new DataOutputStream(bufferedOutputStream);

3 Java NIO

3.1 Проблемы со стандартным вводом-выводом

Java NIO (новый ввод-вывод) — это API-интерфейс ввода-вывода (начиная с Java 1.4), который может заменить стандартный API-интерфейс ввода-вывода Java. Java NIO обеспечивает другой способ работы с вводом-выводом по сравнению со стандартным вводом-выводом. для решения стандартного ввода-вывода имеет следующие проблемы:

  • (1) Несколько копий данных

Стандартная обработка ввода-вывода, чтобы завершить полное чтение и запись данных, по крайней мере, необходимо прочитать из базового оборудования в пространство ядра, затем прочитать в пользовательский файл, записать из пользовательского пространства в пространство ядра, а затем записать в базовое оборудование

Кроме того, когда нижний уровень выполняет системные вызовы ввода-вывода с помощью таких функций, как запись и чтение, ему необходимо передать данные в буфер, где находятся данные.начальный адрес и длинаИз-за существования JVM GC позиция объекта в куче часто перемещается, и параметр адреса, передаваемый в системную функцию после перемещения, не является реальным адресом буфера.

Это может привести к ошибкам чтения и записи.Чтобы решить вышеуказанные проблемы, при использовании стандартного ввода-вывода для выполнения системных вызовов будет вызвано дополнительное копирование данных: копирование данных из кучи JVM в непрерывную пространственную память за пределами куча (память вне кучи)

Таким образом, всего создается 6 копий данных, а эффективность выполнения низкая.

  • (2) Блокировка операций

В традиционной обработке сетевого ввода-вывода такие операции, как запрос на установление соединения (подключение), чтение данных сетевого ввода-вывода (чтение) и отправка данных (отправка), блокируют поток.

// 等待连接
Socket socket = serverSocket.accept();

// 连接已建立,读取请求消息
StringBuilder req = new StringBuilder();
byte[] recvByteBuf = new byte[1024];
int len;
while ((len = socket.getInputStream().read(recvByteBuf)) != -1) {
	req.append(new String(recvByteBuf, 0, len, StandardCharsets.UTF_8));
}

// 写入返回消息
socket.getOutputStream().write(("server response msg".getBytes()));
socket.shutdownOutput();

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

Чтобы реализовать одновременный ответ сервера, каждое соединение должно обрабатываться независимым потоком.Когда количество одновременных запросов велико, для поддержания соединения слишком велики накладные расходы на память и переключение потоков.

3.2 Buffer

Три основных компонента ядра Java NIO: Buffer (буфер), Channel (канал), Selector.

Buffer предоставляет байтовые буферы, обычно используемые для операций ввода/вывода.Обычные буферы включают ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer, соответствующие базовым типам данных: byte, char, double, float, int, long, short, ниже в основном используется наиболее часто используемый ByteBuffer в качестве примера.Нижний уровень буфера поддерживает кучу Java (HeapByteBuffer) или память вне кучи (DirectByteBuffer).

память вне кучиЭто относится к памяти, соответствующей памяти кучи, а объекты памяти выделяются в память вне кучи JVM.Эта память напрямую управляется операционной системой (а не виртуальной машиной. По сравнению с памятью в куче, преимущества использования памяти вне кучи в операциях ввода-вывода в:

  • Нет необходимости восстанавливать линию JVM GC, уменьшая использование ресурсов потока GC.
  • Когда система ввода-вывода вызывает, непосредственно управляйте памятью вне кучи, что может сохранить копию памяти вне кучи и памяти в куче.

Выделение и освобождение базовой памяти вне кучи ByteBuffer основано на функциях malloc и free.Внешний метод allocateDirect может применяться для выделения памяти вне кучи и возврата объекта DirectByteBuffer, наследующего класс ByteBuffer:

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

Восстановление памяти вне кучи основано на переменной-члене класса Cleaner класса DirectByteBuffer, который предоставляет чистый метод, который можно использовать для активной утилизации.Большая часть памяти вне кучи в Netty определяет наличие Cleaner путем записи и активно вызывает метод clean для восстановления; Кроме того, когда объект DirectByteBuffer является сборщиком мусора, соответствующая память вне кучи также освобождается.

tips: для параметра JVM не рекомендуется устанавливать -XX:+DisableExplicitGC, поскольку некоторые фреймворки, использующие Java NIO (например, Netty), будут активно вызывать System.gc() при ненормальном исчерпании памяти, запускать полный сборщик мусора и перерабатывать Объект DirectByteBuffer как рециркулируемая куча Механизм последней гарантии для внешней памяти.Установка этого параметра приведет к тому, что в этом случае внешняя память не будет очищаться.

Память вне кучи основана на переменной-члене класса DirectByteBuffer базового класса ByteBuffer: объект Cleaner, объект Cleaner будет выполнять unsafe.freeMemory(address) в соответствующее время, тем самым освобождая эту память вне кучи.

Под буфером можно понимать набор основных типов данных, массив с непрерывными адресами хранения, поддерживает операции чтения и записи, соответствует режиму чтения и записи и сохраняет текущее состояние положения этих данных через несколько переменных: емкость, положение, предел:

  • вместимость Общая длина массива буферов
  • position положение следующего элемента данных для работы
  • позиция следующего нерабочего элемента в массиве лимитных буферов: limit

3.3 Channel

Концепцию канала можно сравнить с объектами потока ввода-вывода.Операции ввода-вывода в NIO в основном основаны на канале: Данные, считанные из канала: создайте буфер, затем запросите у канала чтение данных. Запись данных из канала: создать буфер, заполнить данные, запросить канал для записи данных

Каналы и потоки очень похожи со следующими отличиями:

  • Каналы можно читать и записывать, в то время как стандартные потоки ввода-вывода являются однонаправленными.
  • Канал можно читать и записывать асинхронно.Стандартные потоки ввода-вывода требуют, чтобы потоки блокировались и ждали завершения операций чтения и записи.
  • Канал всегда основан на буфере Чтение и запись буфера

Реализация наиболее важных каналов в Java NIO:

  • FileChannel: используется для чтения и записи данных файлов.Метод, предоставляемый FileChannel, может уменьшить количество копий данных чтения и записи файлов, которые будут представлены позже.
  • DatagramChannel: чтение и запись данных для UDP
  • SocketChannel: используется для чтения и записи данных TCP, представляя клиентское соединение.
  • ServerSocketChannel: слушайте запросы TCP-подключения, каждый запрос создает SocketChannel, обычно используемый для сервера.

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

FileInputStream fileInputStream = new FileInputStream("test.txt");
FileChannel channel = fileInputStream.channel();

tips: FileChannel может работать только в режиме блокировки, а ввод-вывод асинхронной обработки файлов был добавлен в JDK 1.7 java.nio.channels.AsynchronousFileChannel.

// server socket channel:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 9091));

while (true) {
	SocketChannel socketChannel = serverSocketChannel.accept();
	ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
	int readBytes = socketChannel.read(buffer);
	if (readBytes > 0) {
		// 从写数据到buffer翻转为从buffer读数据
		buffer.flip();
		byte[] bytes = new byte[buffer.remaining()];
		buffer.get(bytes);
		String body = new String(bytes, StandardCharsets.UTF_8);
		System.out.println("server 收到:" + body);
	}
}

3.4 Selector

Селектор (selector), который является одним из основных компонентов Java NIO, используется для проверки того, доступно ли состояние одного или нескольких каналов NIO для чтения и записи. Реализуйте один поток для управления несколькими каналами, то есть вы можете управлять несколькими сетевыми подключениями.

Ядро Selector основано на функции мультиплексирования ввода-вывода, предоставляемой операционной системой. Один поток может одновременно отслеживать несколько дескрипторов соединения. программа для выполнения соответствующих операций чтения и записи. Существуют различные реализации, такие как выбор, опрос, epoll и т. д.

Основной принцип работы Java NIO Selector заключается в следующем:

  • (1) Инициализировать объект Selector, серверный объект ServerSocketChannel
  • (2) Зарегистрируйте событие принятия сокета ServerSocketChannel с помощью Selector.
  • (3) Поток заблокирован в selector.select(), когда клиент запрашивает сервер, поток выходит из блокировки
  • (4) Получить все события готовности на основе селектора.В это время событие принятия сокета получается первым, а событие события готовности для чтения данных клиента SocketChannel регистрируется с помощью селектора.
  • (5) Поток снова блокируется в selector.select(), когда данные о клиентском соединении готовы, доступны для чтения.
  • (6) Прочитайте данные запроса клиента на основе ByteBuffer, затем запишите данные ответа и закройте канал.

Пример следующий, полный исполняемый код выложен на github (GitHub.com/Это ты/Это...):

Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9091));
// 配置通道为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 注册服务端的socket-accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
	// selector.select()会一直阻塞,直到有channel相关操作就绪
	selector.select();
	// SelectionKey关联的channel都有就绪事件
	Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();

	while (keyIterator.hasNext()) {
		SelectionKey key = keyIterator.next();
		// 服务端socket-accept
		if (key.isAcceptable()) {
			// 获取客户端连接的channel
			SocketChannel clientSocketChannel = serverSocketChannel.accept();
			// 设置为非阻塞模式
			clientSocketChannel.configureBlocking(false);
			// 注册监听该客户端channel可读事件,并为channel关联新分配的buffer
			clientSocketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
		}

		// channel可读
		if (key.isReadable()) {
			SocketChannel socketChannel = (SocketChannel) key.channel();
			ByteBuffer buf = (ByteBuffer) key.attachment();

			int bytesRead;
			StringBuilder reqMsg = new StringBuilder();
			while ((bytesRead = socketChannel.read(buf)) > 0) {
				// 从buf写模式切换为读模式
				buf.flip();
				int bufRemain = buf.remaining();
				byte[] bytes = new byte[bufRemain];
				buf.get(bytes, 0, bytesRead);
				// 这里当数据包大于byteBuffer长度,有可能有粘包/拆包问题
				reqMsg.append(new String(bytes, StandardCharsets.UTF_8));
				buf.clear();
			}
			System.out.println("服务端收到报文:" + reqMsg.toString());
			if (bytesRead == -1) {
				byte[] bytes = "[这是服务回的报文的报文]".getBytes(StandardCharsets.UTF_8);

				int length;
				for (int offset = 0; offset < bytes.length; offset += length) {
					length = Math.min(buf.capacity(), bytes.length - offset);
					buf.clear();
					buf.put(bytes, offset, length);
					buf.flip();
					socketChannel.write(buf);
				}
				socketChannel.close();
			}
		}
		// Selector不会自己从已selectedKeys中移除SelectionKey实例
		// 必须在处理完通道时自己移除 下次该channel变成就绪时,Selector会再次将其放入selectedKeys中
		keyIterator.remove();
	}
}

tips: Java NIO реализует высокопроизводительный сетевой ввод-вывод на основе Selector. Он громоздкий в использовании и неудобный в использовании. Как правило, в отрасли используется оптимизация пакетов на основе Java NIO и расширяется инфраструктура Netty с богатыми функциями для достижения элегантной реализации.

Оптимизация высокопроизводительного ввода/вывода

Далее представлена ​​оптимизация высокопроизводительного ввода-вывода в сочетании с популярными в отрасли проектами с открытым исходным кодом.

1 нулевая копия

Технология нулевого копирования используется для сокращения или даже полного исключения ненужных копий процессора при чтении и записи данных, уменьшения использования пропускной способности памяти и повышения эффективности выполнения.Существует несколько различных принципов реализации нулевого копирования.Обычные проекты с открытым исходным кодом описаны ниже. -копировать реализацию

1.1 Нулевая копия Кафки

Kafka предоставляется на основе ядра Linux 2.1, а улучшенная функция отправки файла в ядре 2.4 + DMA Gather Copy, предоставляемая аппаратным обеспечением, обеспечивает нулевое копирование и передачу файлов через сокеты.

Функция завершает передачу файла через системный вызов, уменьшая переключение режима исходного режима чтения/записи. При этом копия данных уменьшается, а подробный процесс отправки файла выглядит следующим образом:

Основной процесс выглядит следующим образом:

  • (1) Пользовательский процесс инициирует системный вызов sendfile
  • (2) Ядро копирует данные файла с диска в буфер ядра на основе копирования DMA.
  • (3) Ядро копирует информацию описания файла (дескриптор файла, длину данных) из буфера ядра в буфер сокета.
  • (4) Ядро копирует данные буфера ядра на сетевую карту на основе информации об описании файла в буфере сокета и функции Gather Copy, предоставляемой оборудованием DMA.
  • (5) Системный вызов sendfile пользовательского процесса завершается и возвращается

По сравнению с традиционным методом ввода-вывода, метод sendfile + DMA Gather Copy обеспечивает нулевое копирование, количество копий данных уменьшается с 4 до 2, количество системных вызовов уменьшается с 2 до 1, а количество пользовательских процессов переключение контекста изменено с 4 раз DMA Copy на 2 раза, что значительно повышает эффективность обработки

Нижний уровень Kafka основан на transferTo FileChannel в пакете java.nio:

public abstract long transferTo(long position, long count, WritableByteChannel target)

TransferTo отправляет файл, связанный с FileChannel, в указанный канал.Когда потребитель потребляет данные, сервер Kafka отправляет данные сообщения в файле в SocketChannel на основе FileChannel

1.2 Нулевая копия RocketMQ

RocketMQ реализует нулевое копирование на основе mmap + write: mmap() может сопоставлять адрес буфера в ядре с буфером в пользовательском пространстве для реализации совместного использования данных, устраняя необходимость копировать данные из буфера ядра в пользовательский буфер.

tmp_buf = mmap(file, len); 
write(socket, tmp_buf, len);

Основной процесс mmap + write для достижения нулевого копирования выглядит следующим образом:

  • (1) Пользовательский процесс инициирует системный вызов mmap к ядру.
  • (2) Выполнить сопоставление адресов памяти между буфером чтения пространства ядра пользовательского процесса и буфером пользовательского пространства.
  • (3) Ядро копирует данные файла с диска в буфер ядра на основе копирования DMA.
  • (4) Системный вызов mmap пользовательского процесса завершается и возвращается
  • (5) Пользовательский процесс инициирует системный вызов записи в ядро.
  • (6) Ядро копирует данные из буфера ядра в буфер сокета на основе копирования ЦП.
  • (7) Ядро копирует данные из буфера сокета на сетевую карту на основе копирования DMA.
  • (8) Системный вызов записи пользовательского процесса завершается и возвращается

Логика хранения и загрузки сообщений в RocketMQ на основе mmap написана в org.apache.rocketmq.store.MappedFile, внутренняя реализация основана на java.nio.MappedByteBuffer, предоставленном nio, а метод map на основе FileChannel получает mmap буфер:

// 初始化
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);

При запросе сообщения CommitLog на основе смещения pos mappedByteBuffer запрашивается размер данных:

public SelectMappedBufferResult selectMappedBuffer(int pos, int size) {
	int readPosition = getReadPosition();
	// ...各种安全校验
    
	// 返回mappedByteBuffer视图
	ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
	byteBuffer.position(pos);
	ByteBuffer byteBufferNew = byteBuffer.slice();
	byteBufferNew.limit(size);
	return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
}

советы: механизм transientStorePoolEnableЧасть памяти Java NIO mmap не является резидентной памятью и может быть заменена памятью подкачки (виртуальной памятью).Чтобы повысить производительность отправки сообщений, RocketMQ вводит механизм блокировки памяти, который сопоставляет файл CommitLog, который необходимо недавно работал в памяти и обеспечивает функцию блокировки памяти, которая гарантирует, что эти файлы всегда существуют в памяти. Параметр управления этого механизма — transientStorePoolEnable

Поэтому есть 2 способа сохранить данные MappedFile в кисть CommitLog:

  • 1 Включите transientStorePoolEnable: запись байтового буфера памяти (writeBuffer) -> фиксация (фиксация) из байтового буфера памяти (writeBuffer) в файловый канал (fileChannel) -> файловый канал (fileChannel) -> сброс на диск
  • 2 TransientStorePoolEnable не включен: запись байтового буфера сопоставленного файла (mappedByteBuffer) -> байтовый буфер сопоставленного файла (mappedByteBuffer) -> сброс на диск

RocketMQ реализует нулевое копирование на основе mmap+write, что подходит для сохранения данных и передачи небольших файлов, таких как сообщения бизнес-уровня. Kafka основан на методе отправки файла с нулевым копированием, который подходит для сохранения данных и передачи больших блоков файлов с высокой пропускной способностью, таких как сообщения системного журнала.

tips:Индексный файл Kafka использует метод mmap + write, а сеть отправки файлов данных использует метод sendfile.

1.3 Нулевая копия Netty

В Netty есть два типа нулевого копирования:

  • 1 На основе нулевого копирования, реализованного операционной системой, нижний уровень основан на методе TransferTo класса FileChannel.
  • 2 На основе оптимизации операций на уровне Java объект кэша массива (ByteBuf) упаковывается и оптимизируется. Путем создания представления данных для данных ByteBuf он поддерживает слияние и сегментацию объектов ByteBuf. Когда нижний уровень сохраняет только одно хранилище данных, это уменьшает ненужные копии.

2 Мультиплексирование

После оптимизации инкапсуляции функций Java NIO в Netty реализация кода мультиплексирования ввода-вывода стала намного элегантнее:

// 创建mainReactor
NioEventLoopGroup boosGroup = new NioEventLoopGroup();
// 创建工作线程组
NioEventLoopGroup workerGroup = new NioEventLoopGroup();

final ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap 
	 // 组装NioEventLoopGroup 
	.group(boosGroup, workerGroup)
	 // 设置channel类型为NIO类型
	.channel(NioServerSocketChannel.class)
	// 设置连接配置参数
	.option(ChannelOption.SO_BACKLOG, 1024)
	.childOption(ChannelOption.SO_KEEPALIVE, true)
	.childOption(ChannelOption.TCP_NODELAY, true)
	// 配置入站、出站事件handler
	.childHandler(new ChannelInitializer<NioSocketChannel>() {
		@Override
		protected void initChannel(NioSocketChannel ch) {
			// 配置入站、出站事件channel
			ch.pipeline().addLast(...);
			ch.pipeline().addLast(...);
		}
	});

// 绑定端口
int port = 8080;
serverBootstrap.bind(port).addListener(future -> {
    if (future.isSuccess()) {
        System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
    } else {
        System.err.println("端口[" + port + "]绑定失败!");
    }
});

3 Кэш страницы (PageCache)

Кэш страниц (Page Cache) — это кеш файлов операционной системы, который используется для сокращения операций ввода-вывода на диске.В страницах содержимое представляет собой физический блок на диске, и кеш страниц может помочь.Скорость последовательного чтения и записи файлов программой практически близка к скорости чтения и записи памяти., основная причина в том, что ОС использует механизм PageCache для оптимизации производительности операций чтения и записи:

Политика чтения кэша страниц: Когда процесс инициирует операцию чтения (например, процесс инициирует системный вызов read()), он сначала проверяет, есть ли требуемые данные в кэше страниц:

  • Если это так, откажитесь от доступа к диску и читайте прямо из кеша страниц.
  • Если нет, ядро ​​планирует операцию блочного ввода-вывода для чтения данных с диска, чтения следующих нескольких страниц (не менее одной страницы, обычно трех страниц), а затем помещения данных в страницу в кэше.

Политика записи кэша страниц: Когда процесс инициирует системный вызов записи для записи данных в файл, они сначала записываются в кэш страниц, а затем метод возвращается. На данный момент данные еще не сохранены в файл, Linux только помечает эту страницу данных в кэше страниц как «грязную» и добавляет ее в список грязных страниц.

Затем поток обратной записи флешера периодически записывает страницы из связанного списка грязных страниц на диск, чтобы данные на диске согласовывались с памятью, и, наконец, очищает флаг «грязных». Грязные страницы записываются обратно на диск в следующих трех случаях:

  • Свободная память ниже определенного порога
  • Грязные страницы находятся в памяти выше определенного порога
  • Когда пользовательский процесс вызывает системные вызовы sync() и fsync()

В RocketMQ логическая очередь потребления ConsumeQueue хранит меньше данных и читается последовательно. Благодаря эффекту предварительного чтения механизма кэширования страниц производительность чтения файлов Consume Queue почти близка к памяти чтения, даже когда сообщения накапливаются. влияют на производительность.Предусмотрены две стратегии сброса сообщений:

  • Синхронная очистка: после того, как сообщение фактически сохраняется на диске, сторона брокера RocketMQ фактически возвращает успешный ответ ACK стороне производителя.
  • Асинхронная очистка может в полной мере использовать преимущества PageCache операционной системы.Пока сообщение записывается в PageCache, отправителю может быть возвращен успешный ACK. Сброс сообщений осуществляется путем отправки фонового асинхронного потока, что снижает задержку чтения и записи и повышает производительность и пропускную способность MQ.

Kafka также использует кеш страниц для достижения высокой производительности при чтении и записи сообщений, которые не будут здесь подробно описываться.

Ссылаться на

"Глубокое понимание ядра Linux - Даниэль П.Бовет"

Наклейки Netty по обучению грамотности в работе с памятью вне кучи Java - Jiangnan Baiyi

Java НИО? Хватит это читать! - Чжу Сяоси

Процесс хранения сообщений RocketMQ — Чжао Кун

Статья для понимания архитектуры модели Netty - caison

Более интересно, добро пожаловать, чтобы обратить внимание на общедоступную архитектуру распределенной системы