Neptune and Gremlin - wild CPU utilization

Viewed 143

Issue

I'm seeing extremely high CPU utilization when making [what seems like] a fairly common ask to a graph database. In fact, the utilization is so large that Amazon Neptune seems to "tap out" when I execute multiple of the queries in rapid succession / simultaneously.

I've experimented with even the largest (db.r5d.24xlarge) instance, which costs $14k/ month (), and I still see the general behavior -- memory is fine, CPU utilization goes wild.

Further, the query isn't exactly quick (~5 seconds), so I'm kind of getting the worst of both worlds...

CPU spike

Ask

Can someone help me understand what I can do to address this / review my query? My hope was, just as a relational database can handle thousands of concurrent requests, Amazon Neptune could do similar.

I suspect I might be using .limit() in the wrong spot. That said, the results produced are correct. If the limit() is moved up before the .and(), I'm concerned I'd end up with suboptimal results, since -- due to the until() clauses -- it could just be paths that stopped w/o making it to the toPortId.

Update: I’m wondering if it might make sense to add a timeLimit() clause to the repeat() clause … anecdotally, after some testing, it seems like I’m paying a significant time penalty to rigorously verify that there are, in fact, no routes to certain places.

Detail

  • I'm new to Gremlin / Neptune / graph databases in general, so it's possible/likely that I'm either doing something wrong (or have something incorrectly configured) -or- I overestimated the ability of these tools to handle queries.

  • My use-case requires that I "qualify" (or disqualify) a large number of candidate destinations -- that is:

Starting at ORIGIN (fromPortId) are there / what are the paths to CANDIDATE (toPortId) for which the duration properties sums to less than LIMIT (travelTimeBudget)?

Offending Query

const result = await g.withSack(INITIAL_CHECKIN_PENALTY)
    .V(fromPortId)
    .repeat(
        (__.outE().hasLabel('HAS_VOYAGE_TO').sack(operator.sum).by('duration'))
        .sack(operator.sum).by(__.constant(LAYEROVER_PENALTY))
        .inV()
        .simplePath()
    )
    .until(
            __.or(
                __.has('code', toPortId), 
                __.sack().is(gte(travelTimeBudget)), 
                __.loops().is(maxHops))
    )
    .and(
        __.has('code', toPortId), 
        __.sack().is(lte(travelTimeBudget))
    )
    .limit(4)
    .order().by(__.sack(), order.asc)
    .local(
        __.union(
            __.path().by('code').by('duration'), 
            __.sack()
       ).fold()
    )
    .local(
        __.unfold().unfold().fold()
    )
    .toList()
    .then(data => {
        return data
    })
    .catch(error => {
        console.log('ERROR', error);
        return false
    });

Execution

  • Database: Amazon Neptune (db.r5.large)
  • Querying from / runtime: AWS Lambda running Node.js
  • Graph
    • 5,300 vertices (Port)
    • 42,000 edges (HAS_VOYAGE_TO)

Data Model

Note: For this issue, I'm only executing the "upper plane" / top half.

data model

1 Answers

To address the CPU discussion, perhaps a small explanation of the Neptune instance architecture will be beneficial. Each Neptune instance has a worker thread pool. The number of workers in the pool is twice the number of vCPU the instance has. That controls how many queries (maximum) can be running concurrently on a given instance. Each instance also has a query queue that will hold queries sent to the instance until a worker becomes available. For a somewhat complex query, against a small instance type (like the db.r5.large), seeing 20 to 30% CPU utilization is not unexpected. A significant portion of the instance memory is used to cache graph data locally in a most recently used fashion. The remaining memory is allocated to the worker threads for query execution. So larger instances have (a) more workers (b) more memory for caching graph data locally and (c) more memory for use during query execution.

Without having the data it's hard to attempt to modify your query and test it, but it may well be possible to optimize various parts of it to improve its overall efficiency.

As there are a lot of topics that we could discuss here, and I am more than happy to do that, it might be easiest, if you are able, to open an AWS support case and ask that the support engineer create a ticket and have it assigned to me. I will be happy to then get on the phone with you and we can spend some time discussing your use case if that would help.

If you are not able to open a support case, we can connect other ways. I'm easy to find on LinkedIn if you prefer to reach out that way.

I very much want to help you with this and your other questions, but I feel the most expeditious way might be for us to get on the phone.

I'm also happy to keep discussing here of course.

Related