Статьи

Увеличение пропускной способности сети с несколькими нотариусами

Вам нужна сеть Corda с очень высокой пропускной способностью? Выровнялась ли пропускная способность сети? Вы уже вытеснили всю производительность, которую могли, из других областей? Если вы ответили «да» на эти вопросы, у меня может быть некоторая полезная информация для вас. Я перечислил эти вопросы, чтобы уменьшить вероятность преждевременной оптимизации сети / приложения Corda. Переключение на использование нескольких нотариусов будет иметь заметное влияние на производительность, только если это одна из самых медленных частей, участвующих в обработке запросов / транзакций. Весьма вероятно, что необходимо улучшить другие области, прежде чем использовать несколько нотариусов.

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

На момент написания статьи текущие версии Corda:

  • Открытый исходный код 3.3
  • Предприятие 3.2

Зачем мне это делать?

Итак. Давайте действительно разберемся, почему вы хотите использовать несколько нотариусов. Диаграммы делают это лучше всего, поэтому давайте использовать один:

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

Упрощенный вид сети с одним нотариусом

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

Как уже упоминалось во введении. Это становится проблемой, когда скорость отправки транзакций нотариусу становится очень высокой. Как только эта точка будет достигнута, нотариус начнет отставать. Поскольку это не может проверить состояния в транзакциях достаточно быстро. Если производительность важна для сети, это хорошая область для изучения.

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

1
private fun notary(): Party = serviceHub.networkMapCache.notaryIdentities.first()

Переключение на несколько нотариусов

Переход от сети, основанной на одном нотариусе, к дизайну, который состоит из многих, по сути, требует двух вещей:

  • Более одного нотариуса в сети.
  • Алгоритм выбора нотариуса для отправки транзакции.

Кроме того, выбранный нотариус для транзакции затем ссылается на будущие транзакции, если потребляет состояния. Если вы окажетесь в ситуации, когда вводятся состояния ввода от разных нотариусов, вы должны выполнить транзакцию с изменением нотариуса. Я расскажу об этой теме позже.

Ниже описано, как можно изменить предыдущий дизайн, чтобы использовать несколько нотариусов:

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

Упрощенный вид сети с несколькими нотариусами

Лучшее в этой диаграмме то, что она показывает, как просто добавить еще одного нотариуса в сеть и перераспределить нагрузку между ними. Ничто не мешает нам добавлять все больше и больше нотариусов в сеть. Но будет момент, когда добавление еще не приведет к увеличению производительности. Это продолжает возвращаться к тому, что я упоминал ранее. Добавление большего количества нотариусов увеличит пропускную способность только тогда, когда сами нотариусы достигнут насыщения.

Выбор нотариуса для эмиссионных операций

Ниже приведен возможный алгоритм выбора нотариуса:

01
02
03
04
05
06
07
08
09
10
private fun transaction(): TransactionBuilder =
  TransactionBuilder(notary()).apply {
    addOutputState(message, MessageContract.CONTRACT_ID)
    addCommand(Send(), message.participants.map(Party::owningKey))
  }
 
private fun notary(): Party {
  val index = message.type.hashCode() % serviceHub.networkMapCache.notaryIdentities.size
  return serviceHub.networkMapCache.notaryIdentities.single { it.name.organisation == "Notary-$index" }
}

В этом примере транзакция выбирает нотариуса для использования на основе hashCode одного из свойств входного состояния и количества нотариусов в сети.

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

Выбор нотариуса при потреблении штатов от одного и того же нотариуса

Это красиво и просто … Если все входные состояния ссылаются на одного и того же нотариуса. Ниже показано, как это выглядит (в этом примере используется только один вход … потому что мне лень писать другую версию):

1
2
3
4
5
6
7
8
private fun transaction(response: MessageState): TransactionBuilder =
  TransactionBuilder(notary()).apply {
    addInputState(message)
    addOutputState(response, MessageContract.CONTRACT_ID)
    addCommand(Reply(), response.participants.map(Party::owningKey))
  }
 
private fun notary(): Party = message.state.notary

Как видите, все транзакции извлекают нотариуса, который связан с состоянием ввода и использует его для себя. Эта информация может быть извлечена, потому что message является StateAndRef и доступ к его свойству state вернет TransactionState . Следуя этому формату. Создание новых транзакций, которые потребляют состояние и производят несколько выходных данных, просто. Этот формат также действителен для нескольких состояний ввода. Если и только если они все ссылаются на одного и того же нотариуса.

Итак … Со всем этим поговорим о входных состояниях с разными нотариусами. Я, наверное, должен обсудить это дальше.

Выбор нотариуса при потреблении штатов от разных нотариусов

Здесь мы должны быть осторожны, иначе мы увидим ошибки, подобные приведенной ниже:

1
java.lang.IllegalArgumentException: Input state requires notary "O=Notary-1, L=London, C=GB" which does not match the transaction notary "O=Notary-0, L=London, C=GB".

Ошибка показывает, что состояние ввода не имеет тот же нотариус, что и транзакция, которая его содержит.

Чтобы исправить эту ошибку, нам нужно использовать транзакцию по нотариальному изменению. Согласно документам:

«Поток, который будет использоваться для изменения нотариуса штата. Это необходимо, поскольку все входные состояния транзакции должны указывать на одного и того же нотариуса ».

Я хотел поместить это там, на всякий случай, если вы думаете, что я лжец!

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Suspendable
private fun notaryChange(
  message: StateAndRef<MessageState>,
  notary: Party
): StateAndRef<MessageState> =
  if (message.state.notary != notary) {
    subFlow(
      NotaryChangeFlow(
        message,
        notary
      )
    )
  } else {
    message
  }

Я уверен, что вы можете понять, что происходит, но чтобы почувствовать себя умнее … Я расскажу вам. message представляет состояние ввода, а notary — это нотариус, который будет использовать новая транзакция. Если нотариусы одинаковы, то состояние можно вернуть, так как с этим ничего не нужно делать. Если они действительно разные, вызовите NotaryChangeFlow который принимает два аргумента, переданных в исходную функцию. Это вернет новый StateAndRef который затем возвращается из функции.

StateAndRef возвращенный из этой функции, может быть затем помещен в транзакцию.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Suspendable
private fun transaction(): TransactionBuilder {
  val messages = getMessageStates()
  val notary = notary()
  return TransactionBuilder(notary).apply {
    messages.forEach {
      addInputState(notaryChange(it, notary))
    }
    addCommand(
      Delete(),
      (messages.flatMap { it.state.data.participants }.toSet() + ourIdentity).map(Party::owningKey)
    )
  }
}
 
@Suspendable
private fun notaryChange(
  message: StateAndRef<MessageState>,
  notary: Party
): StateAndRef<MessageState> =
  if (message.state.notary != notary) {
    subFlow(
      NotaryChangeFlow(
        message,
        notary
      )
    )
  } else {
    message
  }
 
// however you want to choose your specific Notary
private fun notary(): Party =
  serviceHub.networkMapCache.notaryIdentities.single { it.name.organisation == "Notary-1" }

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

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

1
2
3
4
5
private fun notary(messages: List<StateAndRef<MessageState>>): Party =
  messages.map { it.state.notary }
    .groupingBy { it }
    .eachCount()
    .maxBy { (_, size) -> size }?.key ?: throw IllegalStateException("No Notary found")

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

Вывод

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

Последний комментарий я добавлю сюда. По мере увеличения внутренней производительности нотариусов потребность в подобной архитектуре будет уменьшаться. Может даже наступить момент, когда один нотариус сможет полностью обрабатывать большое количество входящих запросов. Это область, за которой нужно следить, поскольку Corda продолжает улучшать свою общую производительность.

Код, используемый в этом посте, можно найти на моем GitHub .

Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Увеличение пропускной способности сети с несколькими нотариусами

Мнения, высказанные участниками Java Code Geeks, являются их собственными.