AcapellaDB Network Transport comparation test¶
Цели бенчмарков¶
- Сравнить скорость GRPC и Aeron RPC
- Понять, какая часть Aeron RPC потребляет больше всего CPU
- Узнать максимальную скорость передачи сообщений по Aeron
Конфигурация тестовых машин¶
Для тестов использовались две машины со следующими характеристиками:
- 72-ядерный CPU
- 64 ГБ ОЗУ
- Сеть 10 Gbit Ethernet
Aeron¶
Для начала, оценим скорость передачи простых сообщений через Aeron без сериализации и RPC. Сообщения отправляются с максимальной скоростью, ограниченной только back-pressure.
код отправки сообщений
// msgSize - размер сообщения val pub = aeron.addPublication("aeron:udp?endpoint=$serverAddress", 10) val buffer = UnsafeBuffer(BufferUtil.allocateDirectAligned(1024 * 1024, BitUtil.CACHE_LINE_LENGTH)) ... while (true) { pub.offer(buffer, 0, msgSize) }
код получения сообщений
val counter = AtomicLong() // счётчик полученных байт val sub = aeron.addSubscription("aeron:udp?endpoint=$serverAddress", 10) val asm = FragmentAssembler { buffer, offset, length, header -> counter.addAndGet(length.toLong()) } ... while (true) { sub.poll(asm, 1024) }
Результаты замеров:
| размер сообщения | пропускная способность, байт/сек | пропускная способность, сообщений/сек |
|---|---|---|
| 1 | 3116441 | 3116441 |
| 2 | 6940044 | 3470022 |
| 4 | 25040070 | 6260018 |
| 8 | 22913552 | 2864194 |
| 16 | 68332646 | 4270790 |
| 32 | 165270332 | 5164698 |
| 64 | 234009030 | 3656391 |
| 128 | 269632089 | 2106501 |
| 256 | 319087334 | 1246435 |
| 512 | 315183411 | 615593 |
| 1024 | 316454604 | 309038 |
| 2048 | 207172812 | 101159 |
| 4096 | 229573427 | 56048 |
| 8192 | 327012352 | 39919 |
| 16384 | 260198923 | 15881 |
| 32768 | 235457740 | 7186 |
| 65536 | 390627328 | 5961 |
| 131072 | 317285990 | 2421 |
lifehack
Если мелко откройте : "open image in new tab" (правый клик)
Максимальная пропускная способность - примерно 400 Мбайт/сек.
Максимальное количество сообщений - примерно 6 млн/сек.
Aeron + Serialization¶
Добавим сериализацию и десериализацию сообщений с помощью библиотеки Protobuf. Они выполняются в потоках отправки и приёма сообщений соответственно.
protobuf-описание сообщения
message PingRequest { bytes data = 1; }
код отправки сообщений
// msgSize - размер сообщения val pub = aeron.addPublication("aeron:udp?endpoint=$serverAddress", 10) val buffer = UnsafeBuffer(BufferUtil.allocateDirectAligned(1024 * 1024, BitUtil.CACHE_LINE_LENGTH)) val output = DirectBufferOutputStream() val request = Ping.PingRequest .newBuilder() .setData(ByteString.copyFrom(ByteArray(msgSize))) .build() ... while (true) { output.wrap(buffer) request.writeTo(output) pub.offer(buffer, 0, output.position()) }
код получения сообщений
val counter = AtomicLong() // счётчик полученных байт val sub = aeron.addSubscription("aeron:udp?endpoint=$serverAddress", 10) val input = DirectBufferInputStream() val asm = FragmentAssembler { buffer, offset, length, header -> input.wrap(buffer, offset, length) val request = Ping.PingRequest .newBuilder() .mergeFrom(input) .build() counter.addAndGet(request.data.size().toLong()) } ... while (true) { sub.poll(asm, 1024) }
Результаты замеров:
| размер сообщения | пропускная способность, байт/сек | пропускная способность, сообщений/сек |
|---|---|---|
| 1 | 399005 | 399005 |
| 2 | 872933 | 436467 |
| 4 | 1753412 | 438353 |
| 8 | 3533239 | 441655 |
| 16 | 6549750 | 409359 |
| 32 | 10079913 | 314997 |
| 64 | 20378009 | 318406 |
| 128 | 32360908 | 252820 |
| 256 | 55729945 | 217695 |
| 512 | 77656166 | 151672 |
| 1024 | 157514547 | 153823 |
| 2048 | 164054016 | 80105 |
| 4096 | 224154419 | 54725 |
| 8192 | 276282572 | 33726 |
| 16384 | 337874124 | 20622 |
| 32768 | 318396825 | 9717 |
| 65536 | 359071744 | 5479 |
| 131072 | 320903577 | 2448 |
Максимальная пропускная способность - примерно 350 Мбайт/сек.
Максимальное количество сообщений - примерно 440 тысяч/сек.
Количество сообщений в секунду сократилось на порядок из-за накладных расходов на сериализацию. При этом, большие сообшения почти не страдают из-за этого оверхеда, поэтому всё ещё получается передавать большой объём данных.
Aeron + RPC¶
Добавим к передаче сериализованных сообщений простую реализацию RPC. Сериализация и десериализация, так же как и в прошлом тесте, выполняются в потоках оправки и получения сообщений, но теперь они через очередь пересылают сообщения в поток RPC.
protobuf-описание сообщений
message PingRequest { bytes data = 1; } message PingResponse { bytes data = 1; }
код отправки запросов
class RpcRequest(val id: Long, val payload: MessageLite) val pub = aeron.addPublication("aeron:udp?endpoint=$serverAddress", 10) val buffer = UnsafeBuffer(BufferUtil.allocateDirectAligned(1024 * 1024, BitUtil.CACHE_LINE_LENGTH)) val output = DirectBufferOutputStream() val messages = OneToOneConcurrentArrayQueue<RpcRequest>(1024) ... while (true) { messages.drain { msg -> buffer.putLong(0, msg.id) output.wrap(buffer, BitUtils.SIZE_OF_LONG, buffer.capacity()) msg.payload.writeTo(output) pub.offer(buffer, 0, BitUtils.SIZE_OF_LONG + output.position()) } }
код получения запросов
class RpcRequest(val id: Long, val payload: MessageLite) val counter = AtomicLong() val sub = aeron.addSubscription("aeron:udp?endpoint=$serverAddress", 10) val input = DirectBufferInputStream() val messages = OneToOneConcurrentArrayQueue<RpcRequest>(1024) val asm = FragmentAssembler { buffer, offset, length, header -> val id = buffer.getLong(offset) input.wrap(buffer, offset + BitUtils.SIZE_OF_LONG, length - BitUtils.SIZE_OF_LONG) val payload = Ping.PingRequest .newBuilder() .mergeFrom(input) .build() messages.offer(RpcRequest(id, payload)) } ... while (true) { sub.poll(asm, 1024) }
код RPC клиента и сервера
interface PingApi { @RPC(1) suspend fun ping(request: Ping.PingRequest): Ping.PingResponse } // counter - счётчик полученных байт class PingImpl(private val counter: AtomicLong) : PingApi { override suspend fun ping(request: Ping.PingRequest): Ping.PingResponse { counter.addAndGet(request.data.size().toLong()) return Ping.PingResponse .newBuilder() .setData(request.data) .build() } } // concurrency - количество конкурентных запросов suspend fun client(ctx: CoroutineContext, node: RpcNode) { val api = node.create<PingApi>() val client = api[serverAddress] for (c in 1..concurrency) { launch(ctx) { while (true) { withTimeoutOrNull(1000) { client.ping(request) } } } } }
Отправлять запросы без ожиданя ответа здесь уже не получится, поэтому появляется ещё один параметр - количество конкурентных запросов, для начала посмотрим, как он влияет на производительность:
| количество конкурентных запросов | пропускная способность, сообщений/сек |
|---|---|
| 1 | 17743 |
| 2 | 32962 |
| 4 | 75879 |
| 8 | 120853 |
| 16 | 152122 |
| 32 | 223092 |
| 64 | 332447 |
| 128 | 343200 |
| 256 | 328979 |
| 512 | 366588 |
| 1024 | 270144 |
Из результатов теста видно, что примерно после 64 конкурентных запрсов не происходит увеличения пропускной сопосбности, поэтому будем проводить следующий тест именно с таким значением этого параметра.
Результаты замеров в зависимости от размера сообщения:
| размер сообщения | пропускная способность, байт/сек | пропускная способность, сообщений/сек |
|---|---|---|
| 1 | 328979 | 328979 |
| 2 | 726566 | 363283 |
| 4 | 1092852 | 273213 |
| 8 | 2705092 | 338137 |
| 16 | 5768800 | 360550 |
| 32 | 13337392 | 416794 |
| 64 | 21619884 | 337811 |
| 128 | 33036044 | 258094 |
| 256 | 24751488 | 96686 |
| 512 | 71553843 | 139754 |
| 1024 | 149788160 | 146278 |
| 2048 | 118082969 | 57658 |
| 4096 | 228886118 | 55880 |
| 8192 | 326871449 | 39901 |
| 16384 | 308343603 | 18820 |
| 32768 | 377795379 | 11529 |
| 65536 | 312875417 | 4774 |
| 131072 | 342517350 | 2613 |
Максимальная пропускная способность - примерно 350 Мбайт/сек.
Максимальное количество сообщений - примерно 400 тысяч/сек.
По сравнению с предыдущим тестом сериализации, производительность уменьшилась незначительно. Можно сказать, что больше всего времени занимает сериалиазация сообщений.
GRPC¶
Для сравнения, возьмём популярную библиотеку GRPC, которая позволяет удалённо вызывать методы между разными языками. Она использует HTTP/2 для передачи сообщений и protobuf для сериализации.
protobuf-описание сообщений и сервиса
message PingRequest { bytes data = 1; } message PingResponse { bytes data = 1; } service PingApi { rpc Ping(PingRequest) returns (PingResponse) {} }
код RPC клиента и сервера
// counter - счётчик полученных байт class PingImpl(private val counter: AtomicLong) : PingApiImplBase() { override fun ping(request: Ping.PingRequest, response: StreamObserver<Ping.PingResponse>) { counter.addAndGet(request.data.size().toLong()) response.onNext(Ping.PingResponse .newBuilder() .setData(request.data) .build()) response.onCompleted() } } // concurrency - количество конкурентных запросов suspend fun client() { val channel = ManagedChannelBuilder .forAddress(serverHost, serverPort) .usePlaintext(true) .build() val stub = PingApiGrpc.newFutureStub(channel) for (c in 1..concurrency) { launch { while (true) { withTimeoutOrNull(1000) { stub.ping(request) } } } } }
Точно так же для начала необходимо найти оптимальное количество конкурентных запросов:
| количество конкурентных запросов | пропускная способность, сообщений/сек |
|---|---|
| 1 | 3812 |
| 2 | 6049 |
| 4 | 12329 |
| 8 | 15378 |
| 16 | 22354 |
| 32 | 23757 |
| 64 | 34033 |
| 128 | 44330 |
| 256 | 51589 |
| 512 | 49648 |
| 1024 | 43088 |
Из результатов теста видно, что примерно после 128 конкурентных запрсов не происходит увеличения пропускной сопосбности, поэтому будем проводить следующий тест именно с таким значением этого параметра.
Результаты замеров в зависимости от размера сообщения:
| размер сообщения | пропускная способность, байт/сек | пропускная способность, сообщений/сек |
|---|---|---|
| 1 | 40378 | 40378 |
| 2 | 83335 | 41668 |
| 4 | 172504 | 43126 |
| 8 | 317909 | 39739 |
| 16 | 659072 | 41192 |
| 32 | 1258892 | 39340 |
| 64 | 2681286 | 41895 |
| 128 | 4803801 | 37530 |
| 256 | 9913420 | 38724 |
| 512 | 19149414 | 37401 |
| 1024 | 36178022 | 35330 |
| 2048 | 62747443 | 30638 |
| 4096 | 107170611 | 26165 |
| 8192 | 175803596 | 21460 |
| 16384 | 202763468 | 12376 |
| 32768 | 253322854 | 7731 |
| 65536 | 310103244 | 4732 |
| 131072 | 356371660 | 2719 |
По результатам этих тестов можно сказать, что на небольших сообщениях Aeron быстрее GRPC почти на порядок, но с увеличением размера сообщения разница становится не такой значительной, при максимальных размерах сообщения её практически нет. Это объясняется увеличением отношения полезной нагрузки к затратам на пересылку и сериализацию сообщенния. Можно сказать, что для передачи больших сообщений нет разницы, какой транспорт использовать.
Сравнительные графики¶
Далее представлены графики, более наглядно показывающие производительность различных вариантов.
Выводы¶
- Aeron быстрее GRPC на небольших сообщениях, до 128 байт, примерно на порядок.
- С ростом размера сообщений, начиная от 1-2 Кбайт, разница между транспортами начинает исчезать.
- Больше всего процессорного времени уходит на сериализацию сообщений.
- Учитывая пункты 1 и 2, эффективный батчинг сообщений на уровне приложения может сильно сократить расходы на удалённые вызовы.
- Максимальная пропускная способность Aeron полученная в этих тестах - 400 Мбайт/сек или 3.2 Гбит/сек от максимально возможных 10 Гбит/сек.