The incident
dist-01 is the distribution router for a floor of the building. It
has one uplink: bond0, two members, 802.3ad to an MLAG pair, and
seven VLAN sub-interfaces on top of it carrying the OSPF adjacency to
the core, a VRRP pair with its partner router, DHCP relay, the remote
syslog feed and four user VLANs.
Several times a week, for between forty and ninety seconds, all of it stops. It has been happening for about a month.
The tickets read like four separate faults:
- The core team has an OSPF adjacency alert on
bond0.20. - The VRRP pair on
bond0.30transitions, both routers log it, and the service team has been chasing a “VRRP instability” ticket. - Users on two VLANs report brief drops; DHCP renewals fail during the window.
- The logging platform has a gap-detection alert firing on this router’s feed.
Four teams, four tickets, four investigations, and the only thing the four have in common is that they clear before anyone can look. The bond-state alert fires and resolves inside the same polling interval often enough that half the events are not even recorded.
The switch team has been asked twice and has answered the same way
twice: both switches are healthy, the MLAG peer-link is healthy, the
port-channel to dist-01 has not gone down, and one member port
bounces occasionally.
Evidence
vyos@dist-01:~$ sudo cat /proc/net/bonding/bond0Bonding Mode: IEEE 802.3ad Dynamic Link Aggregation
LACP active: on
LACP rate: slow
Min links: 2
Slave Interface: eth1
MII Status: up
Speed: 1000 Mbps
Duplex: full
Link Failure Count: 0
Aggregator ID: 1
Slave Interface: eth2
MII Status: up
Speed: 1000 Mbps
Duplex: full
Link Failure Count: 23
Aggregator ID: 1Illustrative output
vyos@dist-01:~$ show configuration commands | match bondingset interfaces bonding bond0 mode '802.3ad'
set interfaces bonding bond0 hash-policy 'layer3+4'
set interfaces bonding bond0 min-links '2'
set interfaces bonding bond0 member interface 'eth1'
set interfaces bonding bond0 member interface 'eth2'
set interfaces bonding bond0 mtu '9000'Illustrative output
vyos@dist-01:~$ journalctl -k --since -7d | grep bond0bond0: link status definitely down for interface eth2
bond0: link status definitely up for interface eth2
bond0: link status definitely down for interface eth2
bond0: link status definitely up for interface eth2Illustrative output
This is the capture taken from the console during an event, and it is the piece nobody had until an engineer happened to be logged in:
vyos@dist-01:~$ ip link show bond0
5: bond0: <NO-CARRIER,BROADCAST,MULTICAST,MASTER,UP> mtu 9000 state DOWN
vyos@dist-01:~$ ip link show bond0.20
9: bond0.20@bond0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 9000 state LOWERLAYERDOWN
vyos@dist-01:~$ ip link show eth1
3: eth1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 9000 state UP
And the per-member counters:
vyos@dist-01:~$ ip -s link show eth2
4: eth2: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 9000 state UP
RX: bytes packets errors dropped missed mcast
TX: bytes packets errors dropped carrier collsns
0 0 23 0
The carrier column on the transmit line counts carrier-loss events on
that member. On eth1 the same column reads zero.
Work the evidence before reading on
Four facts have to be true at once, and only one explanation makes them all true.
- Four services that share no software, no protocol and no configuration failed in the same second. Repeatedly.
- The switch never lost the port-channel. Their statement is not a deflection; their side genuinely stayed up.
- During the event,
eth1was up with a synchronised partner, andbond0had no carrier anyway. - Between events everything reads healthy, except two numbers that nobody had compared: a Link Failure Count of 23 on one member and 0 on the other.
Fact 3 is the one to sit with. A two-member 802.3ad bond with one healthy member should be a bond running at half capacity. Before continuing: what would have to be configured for a bond with a live, aggregated member to refuse to assert carrier - and where in the evidence above is it?
Root cause
1. The trigger is ordinary and is not the incident
eth2’s physical path is unreliable. Link Failure Count 23 against 0
on eth1, a non-zero carrier counter, and a kernel log that records
the member’s link dropping and returning, always the same member.
That is a cable, an optic, or a switch port that is being
error-disabled and auto-recovered - and it is a fault the bond exists
to absorb. On a healthy redundant uplink this costs half the aggregate
bandwidth for the duration and nothing else. Nobody would have opened
four tickets.
2. The incident is min-links 2 on a two-member bond
The bonding driver asserts carrier on the master only while the number
of aggregated members is at least min-links. Below that, the master
goes NO-CARRIER regardless of how healthy the remaining members are.
On a bond with two members, min-links 2 therefore means: the bond
is up only while both members are up. There is no degraded state. The
first member fault is a total loss of the uplink.
That single node explains every symptom:
- Why four unrelated services fail together. Every
bond0.NVLAN sub-interface is stacked onbond0, and a VLAN interface follows its lower device’s carrier. When the master goes NO-CARRIER, all seven sub-interfaces go LOWERLAYERDOWN in the same instant. OSPF, VRRP, DHCP relay and syslog did not fail; the thing they were all standing on went away. - Why the switch team saw nothing. From the switch side one member port bounced and the port-channel stayed up on the other. The switch has no visibility into the host’s carrier policy, and nothing on its side went down. Both reports were accurate and they described different systems.
- Why it looks fine afterwards.
min-linksis only visible when it bites. Between events the bond has two aggregated members, is above the floor, and reads perfectly healthy.
3. Why the outage lasts far longer than the bounce
The member is removed from the aggregator as soon as the driver sees
the link go down, which the MII poll catches quickly. Coming back is
not symmetric: the member has to complete an LACP exchange and reach
the synchronised, collecting and distributing state before the driver
counts it as aggregated again, and only then does the member count
cross back over min-links and carrier return.
That asymmetry is why a sub-second physical bounce produces a
forty-to-ninety-second service outage. It is also the part of the
outage that lacp-rate influences - slow exchanges LACPDUs on a
30-second period against fast’s one second. Measure the rejoin time
on your own bond before assuming a number for it; the point here is
the shape, which is that recovery is bounded by protocol negotiation
and not by the cable.
4. Why the intent was reasonable and the outcome was not
min-links 2 was almost certainly set to express “this bond needs
both of its links”. That is a true statement about capacity, and
min-links is the wrong place to put it. What the node actually says
is “if you cannot have both links, have none” - and on the single
uplink of a distribution router, “none” means the router is
disconnected.
Resolution
- Confirm the floor before changing it.
show configuration commands | match bondingmust showmin-linksand the member count together - the value is only meaningful relative to how many members exist. - Confirm you have a path to the router that does not cross
bond0. This change may bounce the bond on commit, and the console or out-of-band path is what you will be using if it does. - Apply the floor change on its own:
configure,set interfaces bonding bond0 min-links 1,compareto confirm exactly one line,commit,save. Change nothing else in this commit. - Confirm the bond is up with both members aggregated, matching Aggregator IDs and a partner MAC present on each member.
- Give the switch team the timestamps of the Link Failure Count transitions from the kernel log and ask specifically whether that port was error-disabled and auto-recovered, and for what reason. If it was not, the optic and the patch are the next things to replace.
- Leave
lacp-ratealone for now. If the rejoin time you measure during verification is unacceptable, change it as a separate, later commit so you can attribute the improvement. - Record in the bond description what the floor is and why, so the next reviewer sees the intent next to the value.
If the change cannot be made now, hold deliberately rather than by
default: shift the downstream services to the standby router by VRRP
priority, take dist-01 out of the forwarding path, and fix both
findings in a window. Give the hold a named owner and an end time. An
undecided hold on a router that drops every service four times a week
is not a decision, it is the incident continuing.
Verification
- Fail a member on purpose, in a window. Disable the switch port for
eth2, orset interfaces ethernet eth2 disableand commit. ip link show bond0stays UP with carrier, and does not report NO-CARRIER. This is the check that can fail, and it is the only one that actually tests the change.- Every
bond0.Nsub-interface stays up. Confirm at least the one carrying the OSPF adjacency and the one carrying VRRP, by name, rather than assuming they follow. - The OSPF adjacency on
bond0.20stays full through the whole test, and the VRRP pair onbond0.30does not transition. cat /proc/net/bonding/bond0shows one aggregated member and a bond that is still running.- Re-enable the member and time how long it takes to rejoin. Confirm both members show the same Aggregator ID and the same partner MAC afterwards, and write the rejoin time in the change record - it is the number that decides whether
lacp-rate fastis worth a second change. - Record the current Link Failure Count for
eth2, then re-read it after a period longer than the longest gap between events you have observed. The trigger was intermittent, so a quiet afternoon proves nothing about whether the physical fault is fixed. - Confirm the four original tickets stop. The proof that the diagnosis was right is that the OSPF, VRRP, DHCP and syslog alerts all stop together, because they were always one alert wearing four names.
Prevention
- Write
min-linksdown as a failure policy, not as a capacity number, and put the reason in the interface description. On a two-member bond any value above 1 deletes the redundancy the second member exists to provide. The legitimate uses are bonds wide enough that running on one member is worse than running on none, and they are rare enough to deserve a comment. - Alert on the leading indicators, not on the outage. Per-member Link Failure Count deltas and the count of aggregated members were both available for a month while the only alert anybody had cleared itself before a human could read it. An alert that resolves inside one polling interval is not monitoring, it is a rumour.
- Test the failure the redundancy is for. Pull one member in a window at build time, and again after any change to the bond. Redundancy that has never been exercised is a design document, not a property of the system.
- Keep the switch side in the interface inventory: switch hostname, port, aggregation mode, LACP rate. Half of an 802.3ad bond is configured by a team you have to phone, and the phone call goes better when both sides are describing the same ports.
- Read simultaneity as a stack question. When services that share no code and no configuration fail in the same second, look for the lowest layer they share. Four teams spent a month investigating four protocols that were all standing on one carrier bit.
- Separate the trigger from the amplifier in the write-up as well as in the fix. Closing this incident with “replaced a faulty optic” leaves the next flapping member free to do exactly the same thing.
Related material
vyos-x-01-lacp-concept— LACP negotiation, actor and partner, fast versus slow rate, and whatmin-linksdoes.vyos-x-04-bond-failure-modes— the eight production bond failure modes and the evidence each one leaves.vyos-x-06-bond-validation— the host-side and switch-side validation command set used throughout this scenario.vyos-x-03-switch-dependencies— the switch half of the bond, and why the inventory needs to name it.