Thread (1 message) 1 message, 1 author, 2025-03-19

Re: tc: network egress frozen during qdisc update with debug kernel

From: "Paul E. McKenney" <paulmck@kernel.org>
Date: 2025-03-19 15:04:52
Also in: rcu

On Wed, Mar 19, 2025 at 07:56:40AM -0700, Breno Leitao wrote:
On Wed, Mar 19, 2025 at 03:41:37PM +0100, Eric Dumazet wrote:
quoted
On Wed, Mar 19, 2025 at 2:09 PM Breno Leitao [off-list ref] wrote:
quoted
Hello,

I am experiencing an issue with upstream kernel when compiled with debug
capabilities. They are CONFIG_DEBUG_NET, CONFIG_KASAN, and
CONFIG_LOCKDEP plus a few others. You can find the full configuration at
....

Basically when running a `tc replace`, it takes 13-20 seconds to finish:

        # time /usr/sbin/tc qdisc replace dev eth0 root handle 0x1234: mq
        real    0m13.195s
        user    0m0.001s
        sys     0m2.746s

While this is running, the machine loses network access completely. The
machine's network becomes inaccessible for 13 seconds above, which is far
from
ideal.

Upon investigation, I found that the host is getting stuck in the following
call path:

        __qdisc_destroy
        mq_attach
        qdisc_graft
        tc_modify_qdisc
        rtnetlink_rcv_msg
        netlink_rcv_skb
        netlink_unicast
        netlink_sendmsg

The big offender here is rtnetlink_rcv_msg(), which is called with
rtnl_lock
in the follow path:

        static int tc_modify_qdisc() {
                ...
                netdev_lock_ops(dev);
                err = __tc_modify_qdisc(skb, n, extack, dev, tca, tcm,
&replay);
                netdev_unlock_ops(dev);
                ...
        }

So, the rtnl_lock is held for 13 seconds in the case above. I also
traced that __qdisc_destroy() is called once per NIC queue, totalling
a total of 250 calls for the cards I am using.

Ftrace output:

        # perf ftrace --graph-opts depth=100,tail,noirqs -G
rtnetlink_rcv_msg   /usr/sbin/tc qdisc replace dev eth0 root handle 0x1: mq
| grep \\$
        7) $ 4335849 us  |        } /* mq_init */
        7) $ 4339715 us  |      } /* qdisc_create */
        11) $ 15844438 us |        } /* mq_attach */
        11) $ 16129620 us |      } /* qdisc_graft */
        11) $ 20469368 us |    } /* tc_modify_qdisc */
        11) $ 20470448 us |  } /* rtnetlink_rcv_msg */

        In this case, the rtnetlink_rcv_msg() took 20 seconds, and, while
it
        was running, the NIC was not being able to send any packet

Going one step further, this matches what I described above:

        # perf ftrace --graph-opts depth=100,tail,noirqs -G
rtnetlink_rcv_msg   /usr/sbin/tc qdisc replace dev eth0 root handle 0x1: mq
| grep "\\@\|\\$"

        7) $ 4335849 us  |        } /* mq_init */
        7) $ 4339715 us  |      } /* qdisc_create */
        14) @ 210619.0 us |                      } /* schedule */
        14) @ 210621.3 us |                    } /* schedule_timeout */
        14) @ 210654.0 us |                  } /*
wait_for_completion_state */
        14) @ 210716.7 us |                } /* __wait_rcu_gp */
        14) @ 210719.4 us |              } /* synchronize_rcu_normal */
        14) @ 210742.5 us |            } /* synchronize_rcu */
        14) @ 144455.7 us |            } /* __qdisc_destroy */
        14) @ 144458.6 us |          } /* qdisc_put */
        <snip>
        2) @ 131083.6 us |                        } /* schedule */
        2) @ 131086.5 us |                      } /* schedule_timeout */
        2) @ 131129.6 us |                    } /*
wait_for_completion_state */
        2) @ 131227.6 us |                  } /* __wait_rcu_gp */
        2) @ 131231.0 us |                } /* synchronize_rcu_normal */
        2) @ 131242.6 us |              } /* synchronize_rcu */
        2) @ 152162.7 us |            } /* __qdisc_destroy */
        2) @ 152165.7 us |          } /* qdisc_put */
        11) $ 15844438 us |        } /* mq_attach */
        11) $ 16129620 us |      } /* qdisc_graft */
        11) $ 20469368 us |    } /* tc_modify_qdisc */
        11) $ 20470448 us |  } /* rtnetlink_rcv_msg */

From the stack trace, it appears that most of the time is spent waiting
for the
RCU grace period to free the qdisc (!?):

        static void __qdisc_destroy(struct Qdisc *qdisc)
        {
                if (ops->destroy)
                        ops->destroy(qdisc);

                call_rcu(&qdisc->rcu, qdisc_free_cb);
call_rcu() is asynchronous, this is very different from synchronize_rcu().
That is a good point. The offender is synchronize_rcu() is here.
Should that be synchronize_net()?

							Thanx, Paul
quoted
quoted
        }

So, from my newbie PoV, the issue can be summarized as follows:

        netdev_lock_ops(dev);
        __tc_modify_qdisc()
          qdisc_graft()
            for (i = 0; i <  255; i++)
              qdisc_put()
                ____qdisc_destroy()
                  call_rcu()
              }

Questions:

1) I assume the egress traffic is blocked because we are modifying the
   qdisc, which makes sense. How is this achieved? Is it related to
   rtnl_lock?

2) Would it be beneficial to attempt qdisc_put() outside of the critical
   section (rtnl_lock?) to prevent this freeze?
It is unclear to me why you have syncrhonize_rcu() calls.
This is coming from:

	__qdisc_destroy() {
		lockdep_unregister_key(&qdisc->root_lock_key) {
			...
			/* Wait until is_dynamic_key() has finished accessing k->hash_entry. */
			synchronize_rcu();
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help