Skip to content

Synthetic Topologies from DGG

2026-02-25T16:29:43Z by Showboat 0.6.1

Du et al. ("A Microservice Graph Generator with Production Characteristics", ICS 2025) built DGG, a generator that produces synthetic service dependency graphs matching production characteristics from Alibaba traces. The dgg2motel tool converts DGG's JSON output into motel topology YAML, enabling bulk testing of motel against a wide range of graph shapes.

This demo walks through converting a DGG call graph, inspecting the output, and running motel against the converted topology.

A DGG call graph

DGG outputs call graphs as JSON. Each graph has nodes (microservices with type labels) and edges (calls between services with protocol and multiplicity). Here is a sample graph from DGG's Alibaba trace corpus — a normal service that fans out to a memcached cache, a blackhole leaf, and a function that itself calls another cache.

cat > /tmp/dgg-sample.json << 'EOF'
{
    "nodes": [
        {"node": "USER", "label": "relay"},
        {"node": "MS_normal+2.1", "label": "normal"},
        {"node": "MS_Memcached.2", "label": "Memcached"},
        {"node": "MS_blackhole.1_func1", "label": "blackhole"},
        {"node": "MS_normal+2.1_func2", "label": "normal"},
        {"node": "MS_Memcached.1", "label": "Memcached"}
    ],
    "edges": [
        {"rpcid": "0", "um": "USER", "dm": "MS_normal+2.1", "time": 1, "compara": "http"},
        {"rpcid": "0.1", "um": "MS_normal+2.1", "dm": "MS_Memcached.2", "time": 1, "compara": "mc"},
        {"rpcid": "0.2", "um": "MS_normal+2.1", "dm": "MS_blackhole.1_func1", "time": 1, "compara": "rpc"},
        {"rpcid": "0.3", "um": "MS_normal+2.1", "dm": "MS_normal+2.1_func2", "time": 1, "compara": "rpc"},
        {"rpcid": "0.3.1", "um": "MS_normal+2.1_func2", "dm": "MS_Memcached.1", "time": 2, "compara": "mc"}
    ],
    "num": 82
}
EOF
echo 'wrote /tmp/dgg-sample.json'
wrote /tmp/dgg-sample.json

The USER node is the entry point — DGG always starts call graphs from USER. Node names encode the service type and instance: MS_normal+2.1 is a "normal" microservice, MS_Memcached.2 is a memcached instance. The _func2 suffix indicates a function within a service — DGG models services with multiple callable interfaces.

The rpcid field is a hierarchical trace identifier: 0.3.1 means the first child of the third child of the root call. The compara field is the communication protocol (http, rpc, mc for memcached). The time field is a call multiplicity — time: 2 on the edge from MS_normal+2.1_func2 to MS_Memcached.1 means two calls per invocation.

Converting to motel topology

The dgg2motel converter maps DGG's graph structure to motel's topology format. Nodes with a _funcN suffix become operations within their parent service; nodes without a suffix get a handle operation. Edge multiplicities become count on calls. Service type labels determine synthetic duration defaults: memcached gets 1ms, blackhole 5ms, relay 10ms, normal 20ms — all with proportional variance.

go run ./tools/dgg2motel -file /tmp/dgg-sample.json
version: 1

services:
  normal-2-1:
    operations:
      func2:
        duration: 20ms +/- 10ms
        calls:
          - target: memcached-1.handle
            count: 2
      handle:
        duration: 20ms +/- 10ms
        calls:
          - memcached-2.handle
          - blackhole-1.func1
          - normal-2-1.func2
  memcached-2:
    operations:
      handle:
        duration: 1ms +/- 500us
  blackhole-1:
    operations:
      func1:
        duration: 5ms +/- 2ms
  memcached-1:
    operations:
      handle:
        duration: 1ms +/- 500us

traffic:
  rate: 10/s

The converter grouped MS_normal+2.1 and MS_normal+2.1_func2 into a single normal-2-1 service with two operations: handle (the base) and func2. The time: 2 edge became count: 2 on the call from func2 to memcached-1.handle. The USER node was dropped — motel auto-detects root operations (those with no inbound calls).

Checking the converted topology

go run ./tools/dgg2motel -file /tmp/dgg-sample.json > /tmp/dgg-topo.yaml && build/motel check /tmp/dgg-topo.yaml
PASS  max-depth: 2 (limit: 10)
      path: normal-2-1.handle → normal-2-1.func2 → memcached-1.handle
      p50: 2  p95: 2  p99: 2  max: 2  (1000 samples)
PASS  max-fan-out: 3 (limit: 100)
      worst: normal-2-1.handle
      p50: 3  p95: 3  p99: 3  max: 3  (1000 samples)
PASS  max-spans: 6 static worst-case, 6 observed/1000 samples (limit: 10000)
      p50: 6  p95: 6  p99: 6  max: 6  (1000 samples)

The longest path is normal-2-1.handle → normal-2-1.func2 → memcached-1.handle at depth 2. Fan-out of 3 is at the root handle operation, which calls memcached-2, blackhole-1, and its own func2. Total spans per trace is 6: the root handle (1) + memcached-2 (1) + blackhole-1 func1 (1) + func2 (1) + 2x memcached-1 (2). No variance in the percentiles because this topology has no probabilistic calls or retries.

Running traces

build/motel run --stdout --duration 2s /tmp/dgg-topo.yaml 2>&1 > /dev/null | jq '{traces, spans, errors, spans_bounded}'
{
  "traces": 20,
  "spans": 120,
  "errors": 0,
  "spans_bounded": 0
}

20 traces, 120 spans — exactly 6 spans per trace as motel check predicted. No errors because no error rate was set. The converter produces clean topologies that motel runs without issue.

Bulk conversion

DGG's sample corpus contains 111 call graphs across 6 type clusters, derived from Alibaba production traces. The -dir flag converts an entire directory tree at once.

go run ./tools/dgg2motel -dir /tmp/dgg-samples/DGG_gen_cgs -out /tmp/dgg-topologies
converted 111 graphs, skipped 0

Testing the corpus

The included test_corpus.sh script runs motel check and motel run on every topology in a directory. It reports failures and timeouts separately.

./tools/dgg2motel/test_corpus.sh /tmp/dgg-topologies
testing 111 topologies from /tmp/dgg-topologies
motel: build/motel
duration: 1s


=== results ===
check: 111 pass, 0 fail
run:   111 pass, 0 fail, 0 timeout

Stress testing at higher rates

The RATE environment variable overrides the traffic rate in all topologies, enabling stress testing. At 1000 traces/s, the corpus exercises motel's engine across a range of graph shapes at production-like throughput.

RATE="1000/s" DURATION="2s" ./tools/dgg2motel/test_corpus.sh /tmp/dgg-topologies
testing 111 topologies from /tmp/dgg-topologies
motel: build/motel
duration: 2s


=== results ===
check: 111 pass, 0 fail
run:   111 pass, 0 fail, 0 timeout

Corpus shape distribution

for f in /tmp/dgg-topologies/20250109_150211/*/*.yaml; do
    build/motel check --samples 0 "$f" 2>/dev/null
done | awk '
/max-depth:/   { d=$3+0; depths[d]++ }
/max-fan-out:/ { f=$3+0; fans[f]++ }
/max-spans:/   { s=$3+0; spans[s]++ }
END {
    printf "depth:    "; for(i=0;i<=10;i++) if(depths[i]) printf "%dx%d  ", depths[i], i; print ""
    printf "fan-out:  "; for(i=0;i<=10;i++) if(fans[i]) printf "%dx%d  ", fans[i], i; print ""
    printf "spans:    "; for(i=0;i<=20;i++) if(spans[i]) printf "%dx%d  ", spans[i], i; print ""
}'
depth:    20x0  42x1  45x2  2x3  2x4  
fan-out:  20x0  23x1  21x2  13x3  16x4  16x5  1x7  1x8  
spans:    20x1  14x2  19x3  11x4  11x5  9x6  14x7  7x8  3x9  1x10  2x11  

The format is countxvalue — for example, 45x2 means 45 topologies have max depth 2. The corpus covers depths 0-4, fan-outs 0-8, and span counts 1-11. The 20 topologies at depth 0 and fan-out 0 are single-service graphs (type5 in DGG's clustering). The heaviest topologies have 11 spans per trace.

These are small by production standards — Alibaba's traces in the Du et al. paper reach depths of 15+ and hundreds of spans. The sample corpus represents clustered archetypes rather than the full distribution. The dgggen tool below fills that gap.

Generating a production-shaped corpus

The dgggen tool generates DGG-format JSON graphs with shape distributions drawn from the production measurements in the Du et al. paper: heavy-tailed graph sizes reaching hundreds of nodes, depths reaching 15+, hub services with high fan-out, and repeated calls with multiplicities into the hundreds. The corpus is stratified rather than frequency-matched — deep and wide graphs are oversampled so a corpus of 100 graphs still covers the tail. Output is deterministic for a given seed.

go run ./tools/dgggen -n 100 -seed 1 -out /tmp/dgg-prod
go run ./tools/dgg2motel -dir /tmp/dgg-prod -out /tmp/dgg-prod-topologies
generated 100 graphs in /tmp/dgg-prod
converted 100 graphs, skipped 0

The generated graphs exceed motel check's default limits by design — that is the point of a production-shaped corpus. The CHECK_ARGS environment variable passes raised limits through test_corpus.sh.

CHECK_ARGS="--max-depth 25 --max-fan-out 500" ./tools/dgg2motel/test_corpus.sh /tmp/dgg-prod-topologies
testing 100 topologies from /tmp/dgg-prod-topologies
motel: build/motel
duration: 1s


=== results ===
check: 100 pass, 0 fail
run:   100 pass, 0 fail, 0 timeout

Production-shaped corpus distribution

for f in /tmp/dgg-prod-topologies/*.yaml; do
    build/motel check --samples 0 --max-depth 25 --max-fan-out 500 "$f" 2>/dev/null
done | awk '
/max-depth:/   { d=$3+0; depths[d]++; if(d>maxd) maxd=d }
/max-fan-out:/ { f=$3+0; if(f>maxf) maxf=f }
/max-spans:/   { s=$3+0; if(s>maxs) maxs=s; sum+=s; n++ }
END {
    printf "depth:    "; for(i=0;i<=25;i++) if(depths[i]) printf "%dx%d  ", depths[i], i; print ""
    printf "max depth: %d   max fan-out: %d   max spans: %d   mean spans: %.0f\n", maxd, maxf, maxs, sum/n
}'
depth:    6x0  8x1  8x2  9x3  12x4  13x5  8x6  6x7  7x8  5x9  2x10  3x11  2x13  1x14  4x15  3x16  2x17  1x18  
max depth: 18   max fan-out: 460   max spans: 4196   mean spans: 509

Where the DGG sample corpus tops out at depth 4 and 11 spans, this corpus reaches depth 18, fan-out of 460 (a hub calling a memcached instance hundreds of times), and traces of over 4000 spans — the scale the Du et al. paper reports for Alibaba production traces. Every topology still passes motel check and runs cleanly, which is the property the corpus exists to verify.