Criar surtos eficientes com as etapas mergeV() e mergeE() do Gremlin. - Amazon Neptune

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

Criar surtos eficientes com as etapas mergeV() e mergeE() do Gremlin.

Um upsert (ou inserção condicional) reutilizará um vértice ou uma borda se já existir ou criará um desses elementos se não existir. Upserts eficientes podem fazer uma diferença significativa no desempenho das consultas do Gremlin.

Os upserts permitem que você escreva operações de inserção idempotentes: não importa quantas vezes você execute essa operação, o resultado geral é o mesmo. Isso é útil em cenários de gravação altamente simultâneos em que modificações simultâneas na mesma parte do grafo podem forçar a reversão de uma ou mais transações com ConcurrentModificationException, exigindo novas tentativas.

Por exemplo, a consulta a seguir inverte um vértice usando o Map fornecido para primeiro tentar encontrar um vértice com um T.id de "v-1". Se esse vértice for encontrado, ele será gerado. Se não for encontrado, um vértice com esse id e a propriedade será criado por meio da cláusula onCreate.

g.mergeV([(id):'v-1']). option(onCreate, [(label): 'PERSON', 'email': 'person-1@example.org'])

Agrupar upserts em lote para melhorar o throughput

Para cenários de gravação de throughput, é possível encadear as etapas mergeV() e mergeE() para aplicar upserts em vértices e bordas em lote. O agrupamento em lote reduz a sobrecarga transacional de aplicar upserts a um grande número de vértices e bordas. Depois, é possível melhorar ainda mais o throughput aplicando upserts às solicitações em lote paralelamente usando vários clientes.

Como regra, recomendamos aplicar upserts a cerca de duzentos registros por solicitação em lote. Registro é um único rótulo ou propriedade de vértice ou borda. Um vértice com um único rótulo e quatro propriedades, por exemplo, cria cinco registros. Uma borda com um rótulo e uma única propriedade cria dois registros. Se você quiser aplicar upserts a lotes de vértices, cada um com um único rótulo e quatro propriedades, deverá começar com um tamanho de lote de quarenta, porque 200 / (1 + 4) = 40.

É possível experimentar o tamanho do lote. Um valor de duzentos registros por lote é um bom ponto de partida, mas o tamanho ideal pode ser maior ou menor, dependendo da workload. Observe, no entanto, que o Neptune pode limitar o número total de etapas do Gremlin por solicitação. Esse limite não está documentado, mas, por segurança, tente garantir que suas solicitações não contenham mais de 1.500 etapas do Gremlin. O Neptune pode rejeitar grandes solicitações em lote com mais de 1.500 etapas.

Para aumentar o throughput, é possível inverter lotes em paralelo usando vários clientes (consulte Criação de gravações eficientes com multi-thread do Gremlin). O número de clientes deve ser igual ao número de threads de trabalho na sua instância do Neptune Writer, que normalmente é 2 vezes o número vCPUs do servidor. Por exemplo, uma r5.8xlarge instância tem 32 vCPUs e 64 threads de trabalho. Para cenários de gravação de throughput usando um r5.8xlarge, você usaria 64 clientes gravando upserts em lote no Neptune em paralelo.

Cada cliente deve enviar uma solicitação em lote e aguardar a conclusão da solicitação antes de enviar outra solicitação. Embora os vários clientes funcionem paralelamente, cada cliente individual envia solicitações em série. Isso garante que o servidor receba um fluxo constante de solicitações que ocupem todos os threads de operador sem inundar a fila de solicitações do lado do servidor (consulte Dimensionar instâncias de banco de dados em um cluster de banco de dados do Neptune).

Tentar evitar etapas que gerem vários percursos

Quando uma etapa do Gremlin é executada, ela pega um percurso de entrada e emite um ou mais percursos de saída. O número de percursos emitidos por uma etapa determina o número de vezes que a próxima etapa é executada.

Normalmente, ao realizar operações em lote, é recomendável que cada operação, como o vértice de upsert A, seja executada uma vez, para que a sequência de operações seja a seguinte: vértice de upsert A, depois vértice de upsert B, vértice de upsert C, etc. Desde que uma etapa crie ou modifique somente um elemento, ela emite somente um percurso, e as etapas que representam a próxima operação serão executadas somente uma vez. Se, por outro lado, uma operação criar ou modificar mais de um elemento, ela emitirá vários percursos o que, por sua vez, faz com que as etapas subsequentes sejam executadas várias vezes, uma vez por percurso emitido. Isso pode fazer com que o banco de dados execute trabalho adicional desnecessário e, em alguns casos, pode ocasionar a criação de vértices, bordas ou valores de propriedades adicionais indesejados.

Um exemplo de como as coisas podem dar errado é uma consulta como g.V().addV(). Essa consulta simples adiciona um vértice para cada vértice encontrado no grafo, porque V() emite um percurso para cada vértice no grafo e cada um desses percursos aciona uma chamada para addV().

Consulte Misturar upserts e inserções para saber como lidar com operações que podem emitir vários percursos.

Aplicar upserts a vértices

A etapa mergeV() foi projetada especificamente para aplicar upserts a vértices. Ela usa como argumento um Map que representa elementos correspondentes aos vértices existentes no grafo e, se algum elemento não for encontrado, usará esse Map para criar um vértice. A etapa também permite alterar o comportamento no caso de uma criação ou uma correspondência, em que o modulador option() pode ser aplicado com tokens Merge.onCreate e Merge.onMatch para controlar esses respectivos comportamentos. Consulte a documentação de TinkerPop referência para obter mais informações sobre como usar essa etapa.

É possível usar um ID de vértice para determinar se existe um vértice específico. Essa é a abordagem preferida, porque o Neptune otimiza upserts para casos de uso altamente simultâneos. IDs Por exemplo, a seguinte consulta criará um vértice com um ID específico, se ele ainda não existir, ou o reutilizará se existir:

g.mergeV([(T.id): 'v-1']). option(onCreate, [(T.label): 'PERSON', email: 'person-1@example.org', age: 21]). option(onMatch, [age: 22]). id()

Observe que essa consulta termina com uma etapa id(). Embora não seja estritamente necessário para aplicar upserts ao vértice, uma etapa id() até o final de uma consulta de upsert garante que o servidor não serialize todas as propriedades do vértice de volta para o cliente, o que ajuda a reduzir o custo de bloqueio da consulta.

Você também pode usar uma propriedade de vértice para identificar um vértice:

g.mergeV([email: 'person-1@example.org']). option(onCreate, [(T.label): 'PERSON', age: 21]). option(onMatch, [age: 22]). id()

Se possível, use seu próprio usuário fornecido IDs para criar vértices e use-os IDs para determinar se existe um vértice durante uma operação de upsert. Isso permite que o Neptune otimize os upserts. Um upsert baseado em ID pode ser significativamente mais eficiente do que um upsert baseado em propriedade quando modificações simultâneas são comuns.

Encadear upserts de vértice

É possível encadear upserts de vértice para inseri-los em um lote:

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .id()

Você também pode usar esta sintaxe mergeV():

g.mergeV([(T.id): 'v-1', (T.label): 'PERSON', email: 'person-1@example.org']). mergeV([(T.id): 'v-2', (T.label): 'PERSON', email: 'person-2@example.org']). mergeV([(T.id): 'v-3', (T.label): 'PERSON', email: 'person-3@example.org'])

No entanto, como essa forma de consulta inclui elementos nos critérios de pesquisa que são supérfluos para a pesquisa básica por id, ela não é tão eficiente quanto a consulta anterior.

Aplicar upserts a bordas

A etapa mergeE() foi projetada especificamente para aplicar upserts a bordas. Ela usa como argumento um Map que representa elementos correspondentes às bordas existentes no grafo e, se algum elemento não for encontrado, usará esse Map para criar uma borda. A etapa também permite alterar o comportamento no caso de uma criação ou uma correspondência, em que o modulador option() pode ser aplicado com tokens Merge.onCreate e Merge.onMatch para controlar esses respectivos comportamentos. Consulte a documentação de TinkerPop referência para obter mais informações sobre como usar essa etapa.

Você pode usar IDs a borda para elevar as bordas da mesma forma que você eleva os vértices usando vértices personalizados. IDs Novamente, essa é a abordagem preferencial porque permite que o Neptune otimize a consulta. Por exemplo, a consulta a seguir cria uma borda com base em seu ID de borda, se ela ainda não existir, ou a reutiliza se existir. A consulta também usa os Direction.to vértices IDs dos Direction.from e se precisar criar uma nova aresta:

g.mergeE([(T.id): 'e-1']). option(onCreate, [(from): 'v-1', (to): 'v-2', weight: 1.0]). option(onMatch, [weight: 0.5]). id()

Observe que essa consulta termina com uma etapa id(). Embora não seja estritamente necessário para aplicar upserts à borda, adicionar uma etapa id() até o final de uma consulta de upsert garante que o servidor não serialize todas as propriedades da borda de volta para o cliente, o que ajuda a reduzir o custo de bloqueio da consulta.

Muitos aplicativos usam vértices personalizadosIDs, mas deixam Neptune gerar borda. IDs Se você não sabe o ID de uma aresta, mas conhece o to vértice from eIDs, você pode usar esse tipo de consulta para inverter uma aresta:

g.mergeE([(from): 'v-1', (to): 'v-2', (T.label): 'KNOWS']). id()

Todos os vértices referenciados por mergeE() devem existir para que a etapa crie a borda.

Encadear upserts de borda

Assim como acontece com os upserts de vértice, é fácil encadear etapas mergeE() para solicitações em lote:

g.mergeE([(from): 'v-1', (to): 'v-2', (T.label): 'KNOWS']). mergeE([(from): 'v-2', (to): 'v-3', (T.label): 'KNOWS']). mergeE([(from): 'v-3', (to): 'v-4', (T.label): 'KNOWS']). id()

Combinar upserts de vértice e borda

Às vezes, convém aplicar upserts aos vértices e às bordas que os conectam. Você pode misturar os exemplos de lote apresentados aqui. O seguinte exemplo aplica upserts a três vértices e duas bordas:

g.mergeV([(id):'v-1']). option(onCreate, [(label): 'PERSON', 'email': 'person-1@example.org']). mergeV([(id):'v-2']). option(onCreate, [(label): 'PERSON', 'email': 'person-2@example.org']). mergeV([(id):'v-3']). option(onCreate, [(label): 'PERSON', 'email': 'person-3@example.org']). mergeE([(from): 'v-1', (to): 'v-2', (T.label): 'KNOWS']). mergeE([(from): 'v-2', (to): 'v-3', (T.label): 'KNOWS']). id()

Misturar upserts e inserções

Às vezes, convém aplicar upserts aos vértices e às bordas que os conectam. Você pode misturar os exemplos de lote apresentados aqui. O seguinte exemplo aplica upserts a três vértices e duas bordas:

Os upserts normalmente avançam um elemento por vez. Se você seguir os padrões de upsert apresentados aqui, cada operação de upsert emitirá um único percurso, o que faz com que a operação subsequente seja executada apenas uma vez.

No entanto, às vezes convém misturar upserts com inserções. Esse pode ser o caso, por exemplo, caso você use bordas para representar instâncias de ações ou eventos. Uma solicitação pode usar upserts para garantir que todos os vértices necessários existam e, depois, usar inserções para adicionar bordas. Com solicitações desse tipo, preste atenção ao número potencial de percursos emitidos por cada operação.

Considere o seguinte exemplo, que mistura upserts e inserções para adicionar bordas que representam eventos no grafo:

// Fully optimized, but inserts too many edges g.mergeV([(id):'v-1']). option(onCreate, [(label): 'PERSON', 'email': 'person-1@example.org']). mergeV([(id):'v-2']). option(onCreate, [(label): 'PERSON', 'email': 'person-2@example.org']). mergeV([(id):'v-3']). option(onCreate, [(label): 'PERSON', 'email': 'person-3@example.org']). mergeV([(T.id): 'c-1', (T.label): 'CITY', name: 'city-1']). V('p-1', 'p-2'). addE('FOLLOWED').to(V('p-1')). V('p-1', 'p-2', 'p-3'). addE('VISITED').to(V('c-1')). id()

A consulta deve inserir 5 arestas: 2 FOLLOWED arestas e 3 VISITED arestas. No entanto, a consulta conforme escrita insere 8 bordas: 2 FOLLOWED e 6VISITED. A razão para isso é que a operação que insere FOLLOWED as 2 arestas emite 2 travessas, fazendo com que a operação de inserção subsequente, que insere 3 bordas, seja executada duas vezes.

A correção é adicionar uma etapa fold() após cada operação que possa emitir mais de um percurso:

g.mergeV([(T.id): 'v-1', (T.label): 'PERSON', email: 'person-1@example.org']). mergeV([(T.id): 'v-2', (T.label): 'PERSON', email: 'person-2@example.org']). mergeV([(T.id): 'v-3', (T.label): 'PERSON', email: 'person-3@example.org']). mergeV([(T.id): 'c-1', (T.label): 'CITY', name: 'city-1']). V('p-1', 'p-2'). addE('FOLLOWED'). to(V('p-1')). fold(). V('p-1', 'p-2', 'p-3'). addE('VISITED'). to(V('c-1')). id()

Aqui, inserimos uma fold() etapa após a operação que insere FOLLOWED bordas. Isso ocasiona um único percurso, o que faz com que a operação subsequente seja executada apenas uma vez.

A desvantagem dessa abordagem é que a consulta agora não está totalmente otimizada, porque fold() não está otimizado. A operação de inserção após fold() agora também não será otimizada.

Se você precisar usar fold() para reduzir o número de percursos em nome das etapas subsequentes, tente ordenar suas operações de forma que as mais baratas ocupem a parte não otimizada da consulta.

Definindo a cardinalidade

A cardinalidade padrão para propriedades de vértice em Netuno está definida, o que significa que, ao usar mergeV (), todos os valores fornecidos no mapa receberão essa cardinalidade. Para usar a cardinalidade única, você deve ser explícito em seu uso. A partir da TinkerPop versão 3.7.0, há uma nova sintaxe que permite que a cardinalidade seja fornecida como parte do mapa, conforme mostrado no exemplo a seguir:

g.mergeV([(T.id): 1234]). option(onMatch, ['age': single(20), 'name': single('alice'), 'city': set('miami')])

Como alternativa, você pode definir a cardinalidade como padrão para isso da option seguinte maneira:

// age and name are set to single cardinality by default g.mergeV([(T.id): 1234]). option(onMatch, ['age': 22, 'name': 'alice', 'city': set('boston')], single)

Há menos opções para definir a cardinalidade mergeV() antes da versão 3.7.0. A abordagem geral é voltar à property() etapa da seguinte forma:

g.mergeV([(T.id): '1234']). option(onMatch, sideEffect(property(single,'age', 20). property(set,'city','miami')).constant([:]))
nota

Essa abordagem só funcionará mergeV() quando for usada com uma etapa inicial. Portanto, você não seria capaz de encadear mergeV() em uma única travessia, pois a primeira etapa mergeV() após a etapa inicial que usa essa sintaxe produzirá um erro caso o percurso de entrada seja um elemento gráfico. Nesse caso, você gostaria de dividir suas mergeV() chamadas em várias solicitações, onde cada uma pode ser uma etapa inicial.