mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
perf: batch NIO dispatch on project open (one HTTP per compute)
Project open used to create each link by issuing two NIO POSTs from the
controller to the compute — ~5000 HTTP round-trips for a 2500-link
topology, all funnelling through the single shared controller/compute
event loop and capping throughput near 12 links/s.
Replace it with a bulk path:
- UDPLink split into _prepare() (local: ports, peer addrs, link_data)
and _commit_nios() (dispatch). create() = prepare + commit (interactive).
- Link.add_node gains batch=True: attach both nodes without triggering
per-link NIO HTTP.
- compute: new POST /projects/{id}/nios/batch endpoint with a unified
_add_nio_binding dispatch across node types (docker/qemu/iou/vpcs/
builtin differ in signature).
- project.open: prepare all links locally, group NIO entries by compute,
send each compute a single /nios/batch, then finalise (wire node/port
refs, mark created, notify, apply marker defs) in parallel.
Cuts controller->compute HTTP from O(links) to O(computes). Test added
for the batch endpoint.
This commit is contained in:
7 files changed
+280
-29
No files matched your search
@@ -827,6 +827,81 @@ class Project:
|
||||
# a link should have 2 attached nodes, this can happen with corrupted projects
|
||||
await self.delete_link(link.id, force_delete=True)
|
||||
|
||||
async def _prepare_link_from_topology(self, link_data):
|
||||
"""
|
||||
Build a link locally from topology data WITHOUT dispatching NIOs to the
|
||||
computes. Returns ``(link, entries)`` where ``entries`` is the list of
|
||||
``(node, adapter_number, port_number, nio_data)`` tuples produced by
|
||||
``UDPLink._prepare()``, or ``None`` if the link is invalid/incomplete.
|
||||
|
||||
Used by the project-open bulk path so all NIOs can be sent in a single
|
||||
batch HTTP call per compute instead of one round-trip per link.
|
||||
"""
|
||||
|
||||
link = await self.add_link(link_id=link_data["link_id"])
|
||||
if "filters" in link_data:
|
||||
try:
|
||||
await link.update_filters(link_data["filters"])
|
||||
except ControllerError as e:
|
||||
log.warning("Dropping invalid filters on link %s: %s", link_data.get("link_id"), e)
|
||||
for name, marker in (link_data.get("markers") or {}).items():
|
||||
bpf = marker.get("bpf")
|
||||
if not bpf:
|
||||
log.warning("Dropping marker %s on link %s: missing bpf", name, link_data.get("link_id"))
|
||||
continue
|
||||
result = validate_bpf_syntax(bpf)
|
||||
if not result.get("valid"):
|
||||
log.warning(
|
||||
"Dropping marker %s on link %s: invalid BPF (%s)",
|
||||
name, link_data.get("link_id"), result.get("error")
|
||||
)
|
||||
continue
|
||||
link._markers[name] = {
|
||||
"bpf": bpf,
|
||||
"tag": marker.get("tag"),
|
||||
"enabled": marker.get("enabled", True),
|
||||
"color": marker.get("color"),
|
||||
"highlight_duration": marker.get("highlight_duration"),
|
||||
"capture_node_id": marker.get("capture_node_id"),
|
||||
"direction": marker.get("direction"),
|
||||
}
|
||||
if "link_style" in link_data:
|
||||
await link.update_link_style(link_data["link_style"])
|
||||
if "show_filters_icon" in link_data:
|
||||
await link.update_show_filters_icon(link_data["show_filters_icon"])
|
||||
for node_link in link_data.get("nodes", []):
|
||||
node = self.get_node(node_link["node_id"])
|
||||
port = node.get_port(node_link["adapter_number"], node_link["port_number"])
|
||||
if port is None:
|
||||
log.warning(
|
||||
"Port {}/{} for {} not found".format(
|
||||
node_link["adapter_number"], node_link["port_number"], node.name
|
||||
)
|
||||
)
|
||||
continue
|
||||
if port.link is not None:
|
||||
log.warning(
|
||||
"Port {}/{} is already connected to link ID {}".format(
|
||||
node_link["adapter_number"], node_link["port_number"], port.link.id
|
||||
)
|
||||
)
|
||||
continue
|
||||
# batch=True: attach the node without triggering per-link NIO HTTP
|
||||
await link.add_node(
|
||||
node,
|
||||
node_link["adapter_number"],
|
||||
node_link["port_number"],
|
||||
label=node_link.get("label"),
|
||||
dump=False,
|
||||
batch=True,
|
||||
)
|
||||
if len(link.nodes) != 2:
|
||||
# a link should have 2 attached nodes, this can happen with corrupted projects
|
||||
await self.delete_link(link.id, force_delete=True)
|
||||
return None
|
||||
entries = await link._prepare()
|
||||
return (link, entries)
|
||||
|
||||
@open_required
|
||||
async def add_link(self, link_id=None, dump=True):
|
||||
"""
|
||||
@@ -1648,13 +1723,62 @@ class Project:
|
||||
count = ports_per_compute.get(compute.id, 0)
|
||||
if count > 0:
|
||||
await self.preallocate_udp_ports_for_compute(compute, count)
|
||||
# Create links in parallel for improved performance
|
||||
pool = Pool(concurrency=100)
|
||||
for link_data in topology.get("links", []):
|
||||
if "link_id" not in link_data.keys():
|
||||
continue
|
||||
pool.append(self._create_link_from_topology_data, link_data)
|
||||
await pool.join()
|
||||
# Create links via the bulk path: build every link locally (no NIO
|
||||
# HTTP), then dispatch all NIOs to each compute in a single batch
|
||||
# request. This replaces one HTTP round-trip per link (~5000 for a
|
||||
# 2500-link topology) with one round-trip per compute.
|
||||
link_data_list = [d for d in topology.get("links", []) if "link_id" in d.keys()]
|
||||
sem = asyncio.Semaphore(100)
|
||||
|
||||
async def _prepare_one(data):
|
||||
async with sem:
|
||||
try:
|
||||
return await self._prepare_link_from_topology(data)
|
||||
except Exception as e:
|
||||
log.warning("Could not load link %s: %s", data.get("link_id"), e)
|
||||
return None
|
||||
|
||||
prepared = await asyncio.gather(*[_prepare_one(d) for d in link_data_list])
|
||||
valid = [p for p in prepared if p is not None]
|
||||
|
||||
# Group the prepared NIO entries by destination compute and send
|
||||
# each compute a single /nios/batch request.
|
||||
per_compute = {} # compute -> list of {node_id, adapter_number, port_number, nio}
|
||||
for link, entries in valid:
|
||||
for node, adapter_number, port_number, nio_data in entries:
|
||||
per_compute.setdefault(node.compute, []).append(
|
||||
{
|
||||
"node_id": node.id,
|
||||
"adapter_number": adapter_number,
|
||||
"port_number": port_number,
|
||||
"nio": nio_data,
|
||||
}
|
||||
)
|
||||
|
||||
async def _dispatch_batch(compute, nio_entries):
|
||||
await compute.post(
|
||||
f"/projects/{self._id}/nios/batch",
|
||||
data={"nios": nio_entries},
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
if per_compute:
|
||||
await asyncio.gather(
|
||||
*[_dispatch_batch(c, n) for c, n in per_compute.items()]
|
||||
)
|
||||
|
||||
# Finalise every link: wire node/port back-references, mark created,
|
||||
# notify clients, and apply project-level marker definitions.
|
||||
for link, _entries in valid:
|
||||
for n in link._nodes:
|
||||
n["node"].add_link(link)
|
||||
n["port"].link = link
|
||||
link._created = True
|
||||
self.emit_notification("link.created", link.asdict())
|
||||
if valid:
|
||||
await asyncio.gather(
|
||||
*[self.apply_defs_to_new_link(link) for link, _ in valid]
|
||||
)
|
||||
# Release any pre-allocated UDP ports that were not consumed by links
|
||||
for compute_id, ports in self._preallocated_udp_ports.items():
|
||||
if ports:
|
||||
|
||||
Reference in new issue
Block a user