From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:04:34
At this moment, there is only one type of next-hop group: an mpath group.
Mpath groups implement the hash-threshold algorithm, described in RFC
2992[1].
To select a next hop, hash-threshold algorithm first assigns a range of
hashes to each next hop in the group, and then selects the next hop by
comparing the SKB hash with the individual ranges. When a next hop is
removed from the group, the ranges are recomputed, which leads to
reassignment of parts of hash space from one next hop to another. RFC 2992
illustrates it thus:
+-------+-------+-------+-------+-------+
| 1 | 2 | 3 | 4 | 5 |
+-------+-+-----+---+---+-----+-+-------+
| 1 | 2 | 4 | 5 |
+---------+---------+---------+---------+
Before and after deletion of next hop 3
under the hash-threshold algorithm.
Note how next hop 2 gave up part of the hash space in favor of next hop 1,
and 4 in favor of 5. While there will usually be some overlap between the
previous and the new distribution, some traffic flows change the next hop
that they resolve to.
If a multipath group is used for load-balancing between multiple servers,
this hash space reassignment causes an issue that packets from a single
flow suddenly end up arriving at a server that does not expect them, which
may lead to TCP reset.
If a multipath group is used for load-balancing among available paths to
the same server, the issue is that different latencies and reordering along
the way causes the packets to arrive in the wrong order.
Resilient hashing is a technique to address the above problem. Resilient
next-hop group has another layer of indirection between the group itself
and its constituent next hops: a hash table. The selection algorithm uses a
straightforward modulo operation on the SKB hash to choose a hash table
bucket, then reads the next hop that this bucket contains, and forwards
traffic there.
This indirection brings an important feature. In the hash-threshold
algorithm, the range of hashes associated with a next hop must be
continuous. With a hash table, mapping between the hash table buckets and
the individual next hops is arbitrary. Therefore when a next hop is deleted
the buckets that held it are simply reassigned to other next hops:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1|1|1|1|2|2|2|2|3|3|3|3|4|4|4|4|5|5|5|5|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
v v v v
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1|1|1|1|2|2|2|2|1|2|4|5|4|4|4|4|5|5|5|5|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Before and after deletion of next hop 3
under the resilient hashing algorithm.
When weights of next hops in a group are altered, it may be possible to
choose a subset of buckets that are currently not used for forwarding
traffic, and use those to satisfy the new next-hop distribution demands,
keeping the "busy" buckets intact. This way, established flows are ideally
kept being forwarded to the same endpoints through the same paths as before
the next-hop group change.
This patch set adds the implementation of resilient next-hop groups.
In a nutshell, the algorithm works as follows. Each next hop has a number
of buckets that it wants to have, according to its weight and the number of
buckets in the hash table. In case of an event that might cause bucket
allocation change, the numbers for individual next hops are updated,
similarly to how ranges are updated for mpath group next hops. Following
that, a new "upkeep" algorithm runs, and for idle buckets that belong to a
next hop that is currently occupying more buckets than it wants (it is
"overweight"), it migrates the buckets to one of the next hops that has
fewer buckets than it wants (it is "underweight"). If, after this, there
are still underweight next hops, another upkeep run is scheduled to a
future time.
Chances are there are not enough "idle" buckets to satisfy the new demands.
The algorithm has knobs to select both what it means for a bucket to be
idle, and for whether and when to forcefully migrate buckets if there keeps
being an insufficient number of idle ones.
To illustrate the usage, consider the following commands:
# ip nexthop add id 1 via 192.0.2.2 dev dummy1
# ip nexthop add id 2 via 192.0.2.3 dev dummy1
# ip nexthop add id 10 group 1/2 type resilient \
buckets 8 idle_timer 60 unbalanced_timer 300
The last command creates a resilient next-hop group. It will have 8
buckets, each bucket will be considered idle when no traffic hits it for at
least 60 seconds, and if the table remains out of balance for 300 seconds,
it will be forcefully brought into balance.
If not present in netlink message, the idle timer defaults to 120 seconds,
and there is no unbalanced timer, meaning the group may remain unbalanced
indefinitely. The value of 120 is the default in Cumulus implementation of
resilient next-hop groups. To a degree the default is arbitrary, the only
value that certainly does not make sense is 0. Therefore going with an
existing deployed implementation is reasonable.
Unbalanced time, i.e. how long since the last time that all nexthops had as
many buckets as they should according to their weights, is reported when
the group is dumped:
# ip nexthop show id 10
id 10 group 1/2 type resilient buckets 8 idle_timer 60 unbalanced_timer 300 unbalanced_time 0
When replacing next hops or changing weights, if one does not specify some
parameters, their value is left as it was:
# ip nexthop replace id 10 group 1,2/2 type resilient
# ip nexthop show id 10
id 10 group 1,2/2 type resilient buckets 8 idle_timer 60 unbalanced_timer 300 unbalanced_time 0
It is also possible to do a dump of individual buckets (and now you know
why there were only 8 of them in the example above):
# ip nexthop bucket show id 10
id 10 index 0 idle_time 5.59 nhid 1
id 10 index 1 idle_time 5.59 nhid 1
id 10 index 2 idle_time 8.74 nhid 2
id 10 index 3 idle_time 8.74 nhid 2
id 10 index 4 idle_time 8.74 nhid 1
id 10 index 5 idle_time 8.74 nhid 1
id 10 index 6 idle_time 8.74 nhid 1
id 10 index 7 idle_time 8.74 nhid 1
Note the two buckets that have a shorter idle time. Those are the ones that
were migrated after the nexthop replace command to satisfy the new demand
that nexthop 1 be given 6 buckets instead of 4.
The patchset proceeds as follows:
- Patches #1 and #2 are small refactoring patches.
- Patch #3 adds a new flag to struct nh_group, is_multipath. This flag is
meant to be set for all nexthop groups that in general have several
nexthops from which they choose, and avoids a more expensive dispatch
based on reading several flags, one for each nexthop group type.
- Patch #4 contains defines of new UAPI attributes and the new next-hop
group type. At this point, the nexthop code is made to bounce the new
type. As the resilient hashing code is gradually added in the following
patch sets, it will remain dead. The last patch will make it accessible.
This patch also adds a suite of new messages related to next hop buckets.
This approach was taken instead of overloading the information on the
existing RTM_{NEW,DEL,GET}NEXTHOP messages for the following reasons.
First, a next-hop group can contain a large number of next-hop buckets
(4k is not unheard of). This imposes limits on the amount of information
that can be encoded for each next-hop bucket given a netlink message is
limited to 64k bytes.
Second, while RTM_NEWNEXTHOPBUCKET is only used for notifications at this
point, in the future it can be extended to provide user space with
control over next-hop buckets configuration.
- Patch #5 contains the meat of the resilient next-hop group support.
- Patches #6 and #7 implement support for notifications towards the
drivers.
- Patch #8 adds an interface for the drivers to report resilient hash
table bucket activity. Drivers will be able to report through this
interface whether traffic is hitting a given bucket.
- Patch #9 adds an interface for the drivers to report whether a given
hash table bucket is offloaded or trapping traffic.
- In patches #10, #11, #12 and #13, UAPI is implemented. This includes all
the code necessary for creation of resilient groups, bucket dumping and
getting, and bucket migration notifications.
- In patch #14 the next-hop groups are finally made available.
The overall plan is to contribute approximately the following patchsets:
1) Nexthop policy refactoring (already pushed)
2) Preparations for resilient next-hop groups (already pushed)
3) Implementation of resilient next-hop groups (this patchset)
4) Netdevsim offload plus a suite of selftests
5) Preparations for mlxsw offload of resilient next-hop groups
6) mlxsw offload including selftests
Interested parties can look at the current state of the code at [2] and
[3].
[1] https://tools.ietf.org/html/rfc2992
[2] https://github.com/idosch/linux/commits/submit/res_integ_v1
[3] https://github.com/idosch/iproute2/commits/submit/res_v1
v1 (changes since RFC):
- Patch #3:
- This patch is new
- Patches #4-#13:
- u32 -> u16 for bucket counts / indices
- Patch #5:
- set the new flag is_multipath for resilient groups
Ido Schimmel (4):
nexthop: Add netlink defines and enumerators for resilient NH groups
nexthop: Add data structures for resilient group notifications
nexthop: Allow setting "offload" and "trap" indication of nexthop
buckets
nexthop: Allow reporting activity of nexthop buckets
Petr Machata (10):
nexthop: Pass nh_config to replace_nexthop()
nexthop: __nh_notifier_single_info_init(): Make nh_info an argument
nexthop: Add a dedicated flag for multipath next-hop groups
nexthop: Add implementation of resilient next-hop groups
nexthop: Implement notifiers for resilient nexthop groups
nexthop: Add netlink handlers for resilient nexthop groups
nexthop: Add netlink handlers for bucket dump
nexthop: Add netlink handlers for bucket get
nexthop: Notify userspace about bucket migrations
nexthop: Enable resilient next-hop groups
include/net/nexthop.h | 72 +-
include/uapi/linux/nexthop.h | 43 +
include/uapi/linux/rtnetlink.h | 7 +
net/ipv4/nexthop.c | 1524 ++++++++++++++++++++++++++++++--
security/selinux/nlmsgtab.c | 5 +-
5 files changed, 1597 insertions(+), 54 deletions(-)
--
2.26.2
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:04:34
Currently, replace assumes that the new group that is given is a
fully-formed object. But mpath groups really only have one attribute, and
that is the constituent next hop configuration. This may not be universally
true. From the usability perspective, it is desirable to allow the replace
operation to adjust just the constituent next hop configuration and leave
the group attributes as such intact.
But the object that keeps track of whether an attribute was or was not
given is the nh_config object, not the next hop or next-hop group. To allow
(selective) attribute updates during NH group replacement, propagate `cfg'
to replace_nexthop() and further to replace_nexthop_grp().
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
net/ipv4/nexthop.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
@@ -1319,7 +1320,7 @@ static int replace_nexthop(struct net *net, struct nexthop *old,}if(old->is_group)-err=replace_nexthop_grp(net,old,new,extack);+err=replace_nexthop_grp(net,old,new,cfg,extack);elseerr=replace_nexthop_single(net,old,new,extack);
@@ -1361,7 +1362,7 @@ static int insert_nexthop(struct net *net, struct nexthop *new_nh,}elseif(new_id>nh->id){pp=&next->rb_right;}elseif(replace){-rc=replace_nexthop(net,nh,new_nh,extack);+rc=replace_nexthop(net,nh,new_nh,cfg,extack);if(!rc){new_nh=nh;/* send notification with old nh */replace_notify=1;
From: David Ahern <hidden> Date: 2021-03-11 15:21:06
On 3/10/21 8:02 AM, Petr Machata wrote:
Currently, replace assumes that the new group that is given is a
fully-formed object. But mpath groups really only have one attribute, and
that is the constituent next hop configuration. This may not be universally
true. From the usability perspective, it is desirable to allow the replace
operation to adjust just the constituent next hop configuration and leave
the group attributes as such intact.
But the object that keeps track of whether an attribute was or was not
given is the nh_config object, not the next hop or next-hop group. To allow
(selective) attribute updates during NH group replacement, propagate `cfg'
to replace_nexthop() and further to replace_nexthop_grp().
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
net/ipv4/nexthop.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:04:34
The cited function currently uses rtnl_dereference() to get nh_info from a
handed-in nexthop. However, under the resilient hashing scheme, this
function will not always be called under RTNL, sometimes the mutual
exclusion will be achieved differently. Therefore move the nh_info
extraction from the function to its callers to make it possible to use a
different synchronization guarantee.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
net/ipv4/nexthop.c | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
From: David Ahern <hidden> Date: 2021-03-11 15:22:43
On 3/10/21 8:02 AM, Petr Machata wrote:
The cited function currently uses rtnl_dereference() to get nh_info from a
handed-in nexthop. However, under the resilient hashing scheme, this
function will not always be called under RTNL, sometimes the mutual
exclusion will be achieved differently. Therefore move the nh_info
extraction from the function to its callers to make it possible to use a
different synchronization guarantee.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
net/ipv4/nexthop.c | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:05
From: Ido Schimmel <idosch@nvidia.com>
- RTM_NEWNEXTHOP et.al. that handle resilient groups will have a new nested
attribute, NHA_RES_GROUP, whose elements are attributes NHA_RES_GROUP_*.
- RTM_NEWNEXTHOPBUCKET et.al. is a suite of new messages that will
currently serve only for dumping of individual buckets of resilient next
hop groups. For nexthop group buckets, these messages will carry a nested
attribute NHA_RES_BUCKET, whose elements are attributes NHA_RES_BUCKET_*.
There are several reasons why a new suite of messages is created for
nexthop buckets instead of overloading the information on the existing
RTM_{NEW,DEL,GET}NEXTHOP messages.
First, a nexthop group can contain a large number of nexthop buckets (4k
is not unheard of). This imposes limits on the amount of information that
can be encoded for each nexthop bucket given a netlink message is limited
to 64k bytes.
Second, while RTM_NEWNEXTHOPBUCKET is only used for notifications at
this point, in the future it can be extended to provide user space with
control over nexthop buckets configuration.
- The new group type is NEXTHOP_GRP_TYPE_RES. Note that nexthop code is
adjusted to bounce groups with that type for now.
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Petr Machata <petrm@nvidia.com>
Signed-off-by: Petr Machata <petrm@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
include/uapi/linux/nexthop.h | 43 ++++++++++++++++++++++++++++++++++
include/uapi/linux/rtnetlink.h | 7 ++++++
net/ipv4/nexthop.c | 2 ++
security/selinux/nlmsgtab.c | 5 +++-
4 files changed, 56 insertions(+), 1 deletion(-)
@@ -22,6 +22,7 @@ struct nexthop_grp {enum{NEXTHOP_GRP_TYPE_MPATH,/* default type if not specified */+NEXTHOP_GRP_TYPE_RES,/* resilient nexthop group */__NEXTHOP_GRP_TYPE_MAX,};
@@ -52,8 +53,50 @@ enum {NHA_FDB,/* flag; nexthop belongs to a bridge fdb *//* if NHA_FDB is added, OIF, BLACKHOLE, ENCAP cannot be set */+/* nested; resilient nexthop group attributes */+NHA_RES_GROUP,+/* nested; nexthop bucket attributes */+NHA_RES_BUCKET,+__NHA_MAX,};#define NHA_MAX (__NHA_MAX - 1)++enum{+NHA_RES_GROUP_UNSPEC,+/* Pad attribute for 64-bit alignment. */+NHA_RES_GROUP_PAD=NHA_RES_GROUP_UNSPEC,++/* u16; number of nexthop buckets in a resilient nexthop group */+NHA_RES_GROUP_BUCKETS,+/* clock_t as u32; nexthop bucket idle timer (per-group) */+NHA_RES_GROUP_IDLE_TIMER,+/* clock_t as u32; nexthop unbalanced timer */+NHA_RES_GROUP_UNBALANCED_TIMER,+/* clock_t as u64; nexthop unbalanced time */+NHA_RES_GROUP_UNBALANCED_TIME,++__NHA_RES_GROUP_MAX,+};++#define NHA_RES_GROUP_MAX (__NHA_RES_GROUP_MAX - 1)++enum{+NHA_RES_BUCKET_UNSPEC,+/* Pad attribute for 64-bit alignment. */+NHA_RES_BUCKET_PAD=NHA_RES_BUCKET_UNSPEC,++/* u16; nexthop bucket index */+NHA_RES_BUCKET_INDEX,+/* clock_t as u64; nexthop bucket idle time */+NHA_RES_BUCKET_IDLE_TIME,+/* u32; nexthop id assigned to the nexthop bucket */+NHA_RES_BUCKET_NH_ID,++__NHA_RES_BUCKET_MAX,+};++#define NHA_RES_BUCKET_MAX (__NHA_RES_BUCKET_MAX - 1)+#endif
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:06
With the introduction of resilient nexthop groups, there will be two types
of multipath groups: the current hash-threshold "mpath" ones, and resilient
groups. Both are multipath, but to determine the fact, the system needs to
consider two flags. This might prove costly in the datapath. Therefore,
introduce a new flag, that should be set for next-hop groups that have more
than one nexthop, and should be considered multipath.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- This patch is new
include/net/nexthop.h | 7 ++++---
net/ipv4/nexthop.c | 5 ++++-
2 files changed, 8 insertions(+), 4 deletions(-)
From: David Ahern <hidden> Date: 2021-03-11 15:29:38
On 3/10/21 8:02 AM, Petr Machata wrote:
With the introduction of resilient nexthop groups, there will be two types
of multipath groups: the current hash-threshold "mpath" ones, and resilient
groups. Both are multipath, but to determine the fact, the system needs to
consider two flags. This might prove costly in the datapath. Therefore,
introduce a new flag, that should be set for next-hop groups that have more
than one nexthop, and should be considered multipath.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- This patch is new
include/net/nexthop.h | 7 ++++---
net/ipv4/nexthop.c | 5 ++++-
2 files changed, 8 insertions(+), 4 deletions(-)
This patch looks good:
Reviewed-by: David Ahern <dsahern@kernel.org>
@@ -80,6 +80,7 @@ struct nh_grp_entry {structnh_group{structnh_group*spare;/* spare group for removals */u16num_nh;+boolis_multipath;boolmpath;
It would be good to rename the existing type 'mpath' to something else.
You have 'resilient' as a group type later, so maybe rename this one to
hash or hash_threshold.
@@ -80,6 +80,7 @@ struct nh_grp_entry {structnh_group{structnh_group*spare;/* spare group for removals */u16num_nh;+boolis_multipath;boolmpath;
It would be good to rename the existing type 'mpath' to something else.
You have 'resilient' as a group type later, so maybe rename this one to
hash or hash_threshold.
@@ -80,6 +80,7 @@ struct nh_grp_entry {structnh_group{structnh_group*spare;/* spare group for removals */u16num_nh;+boolis_multipath;boolmpath;
It would be good to rename the existing type 'mpath' to something else.
You have 'resilient' as a group type later, so maybe rename this one to
hash or hash_threshold.
All right, I'll send a follow-up with that.
I'm fine with the rename being a followup after this patch set or as the
last patch in this set.
@@ -80,6 +80,7 @@ struct nh_grp_entry {structnh_group{structnh_group*spare;/* spare group for removals */u16num_nh;+boolis_multipath;boolmpath;
It would be good to rename the existing type 'mpath' to something else.
You have 'resilient' as a group type later, so maybe rename this one to
hash or hash_threshold.
All right, I'll send a follow-up with that.
I'm fine with the rename being a followup after this patch set or as the
last patch in this set.
I looked at this, it's more than just this struct field. There is a
whole number of functions with mpath in their name to reflect that they
are for the hash-threshold algorithm. (And then some where the "mpath"
reflects is_multipath assumption.)
So I'll send this separately, and have it go through our regression.
It's still trivialish renaming, but a fair amount thereof.
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:06
From: Ido Schimmel <idosch@nvidia.com>
The kernel periodically checks the idle time of nexthop buckets to
determine if they are idle and can be re-populated with a new nexthop.
When the resilient nexthop group is offloaded to hardware, the kernel
will not see activity on nexthop buckets unless it is reported from
hardware.
Add a function that can be periodically called by device drivers to
report activity on nexthop buckets after querying it from the underlying
device.
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Petr Machata <petrm@nvidia.com>
Signed-off-by: Petr Machata <petrm@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
include/net/nexthop.h | 2 ++
net/ipv4/nexthop.c | 35 +++++++++++++++++++++++++++++++++++
2 files changed, 37 insertions(+)
@@ -222,6 +222,8 @@ int unregister_nexthop_notifier(struct net *net, struct notifier_block *nb);voidnexthop_set_hw_flags(structnet*net,u32id,booloffload,booltrap);voidnexthop_bucket_set_hw_flags(structnet*net,u32id,u16bucket_index,booloffload,booltrap);+voidnexthop_res_grp_activity_update(structnet*net,u32id,u16num_buckets,+unsignedlong*activity);/* caller is holding rcu or rtnl; no reference taken to nexthop */structnexthop*nexthop_find_by_id(structnet*net,u32id);
@@ -3106,6 +3106,41 @@ void nexthop_bucket_set_hw_flags(struct net *net, u32 id, u16 bucket_index,}EXPORT_SYMBOL(nexthop_bucket_set_hw_flags);+voidnexthop_res_grp_activity_update(structnet*net,u32id,u16num_buckets,+unsignedlong*activity)+{+structnh_res_table*res_table;+structnexthop*nexthop;+structnh_group*nhg;+u16i;++rcu_read_lock();++nexthop=nexthop_find_by_id(net,id);+if(!nexthop||!nexthop->is_group)+gotoout;++nhg=rcu_dereference(nexthop->nh_grp);+if(!nhg->resilient)+gotoout;++/* Instead of silently ignoring some buckets, demand that the sizes+*bethesame.+*/+res_table=rcu_dereference(nhg->res_table);+if(num_buckets!=res_table->num_nh_buckets)+gotoout;++for(i=0;i<num_buckets;i++){+if(test_bit(i,activity))+nh_res_bucket_set_busy(&res_table->nh_buckets[i]);+}++out:+rcu_read_unlock();+}+EXPORT_SYMBOL(nexthop_res_grp_activity_update);+staticvoid__net_exitnexthop_net_exit(structnet*net){rtnl_lock();
From: David Ahern <hidden> Date: 2021-03-11 16:06:53
On 3/10/21 8:03 AM, Petr Machata wrote:
From: Ido Schimmel <idosch@nvidia.com>
The kernel periodically checks the idle time of nexthop buckets to
determine if they are idle and can be re-populated with a new nexthop.
When the resilient nexthop group is offloaded to hardware, the kernel
will not see activity on nexthop buckets unless it is reported from
hardware.
Add a function that can be periodically called by device drivers to
report activity on nexthop buckets after querying it from the underlying
device.
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Petr Machata <petrm@nvidia.com>
Signed-off-by: Petr Machata <petrm@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
include/net/nexthop.h | 2 ++
net/ipv4/nexthop.c | 35 +++++++++++++++++++++++++++++++++++
2 files changed, 37 insertions(+)
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:06
From: Ido Schimmel <idosch@nvidia.com>
Add data structures that will be used for in-kernel notifications about
addition / deletion of a resilient nexthop group and about changes to a
hash bucket within a resilient group.
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Petr Machata <petrm@nvidia.com>
Signed-off-by: Petr Machata <petrm@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
include/net/nexthop.h | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
From: David Ahern <hidden> Date: 2021-03-11 15:56:10
On 3/10/21 8:02 AM, Petr Machata wrote:
From: Ido Schimmel <idosch@nvidia.com>
Add data structures that will be used for in-kernel notifications about
addition / deletion of a resilient nexthop group and about changes to a
hash bucket within a resilient group.
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Petr Machata <petrm@nvidia.com>
Signed-off-by: Petr Machata <petrm@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:07
From: Ido Schimmel <idosch@nvidia.com>
Add a function that can be called by device drivers to set "offload" or
"trap" indication on nexthop buckets following nexthop notifications and
other changes such as a neighbour becoming invalid.
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Petr Machata <petrm@nvidia.com>
Signed-off-by: Petr Machata <petrm@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
include/net/nexthop.h | 2 ++
net/ipv4/nexthop.c | 34 ++++++++++++++++++++++++++++++++++
2 files changed, 36 insertions(+)
@@ -220,6 +220,8 @@ int register_nexthop_notifier(struct net *net, struct notifier_block *nb,structnetlink_ext_ack*extack);intunregister_nexthop_notifier(structnet*net,structnotifier_block*nb);voidnexthop_set_hw_flags(structnet*net,u32id,booloffload,booltrap);+voidnexthop_bucket_set_hw_flags(structnet*net,u32id,u16bucket_index,+booloffload,booltrap);/* caller is holding rcu or rtnl; no reference taken to nexthop */structnexthop*nexthop_find_by_id(structnet*net,u32id);
From: David Ahern <hidden> Date: 2021-03-11 16:06:53
On 3/10/21 8:02 AM, Petr Machata wrote:
From: Ido Schimmel <idosch@nvidia.com>
Add a function that can be called by device drivers to set "offload" or
"trap" indication on nexthop buckets following nexthop notifications and
other changes such as a neighbour becoming invalid.
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Petr Machata <petrm@nvidia.com>
Signed-off-by: Petr Machata <petrm@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
include/net/nexthop.h | 2 ++
net/ipv4/nexthop.c | 34 ++++++++++++++++++++++++++++++++++
2 files changed, 36 insertions(+)
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:07
At this moment, there is only one type of next-hop group: an mpath group,
which implements the hash-threshold algorithm.
To select a next hop, hash-threshold algorithm first assigns a range of
hashes to each next hop in the group, and then selects the next hop by
comparing the SKB hash with the individual ranges. When a next hop is
removed from the group, the ranges are recomputed, which leads to
reassignment of parts of hash space from one next hop to another. While
there will usually be some overlap between the previous and the new
distribution, some traffic flows change the next hop that they resolve to.
That causes problems e.g. as established TCP connections are reset, because
the traffic is forwarded to a server that is not familiar with the
connection.
Resilient hashing is a technique to address the above problem. Resilient
next-hop group has another layer of indirection between the group itself
and its constituent next hops: a hash table. The selection algorithm uses a
straightforward modulo operation to choose a hash bucket, and then reads
the next hop that this bucket contains, and forwards traffic there.
This indirection brings an important feature. In the hash-threshold
algorithm, the range of hashes associated with a next hop must be
continuous. With a hash table, mapping between the hash table buckets and
the individual next hops is arbitrary. Therefore when a next hop is deleted
the buckets that held it are simply reassigned to other next hops. When
weights of next hops in a group are altered, it may be possible to choose a
subset of buckets that are currently not used for forwarding traffic, and
use those to satisfy the new next-hop distribution demands, keeping the
"busy" buckets intact. This way, established flows are ideally kept being
forwarded to the same endpoints through the same paths as before the
next-hop group change.
In a nutshell, the algorithm works as follows. Each next hop has a number
of buckets that it wants to have, according to its weight and the number of
buckets in the hash table. In case of an event that might cause bucket
allocation change, the numbers for individual next hops are updated,
similarly to how ranges are updated for mpath group next hops. Following
that, a new "upkeep" algorithm runs, and for idle buckets that belong to a
next hop that is currently occupying more buckets than it wants (it is
"overweight"), it migrates the buckets to one of the next hops that has
fewer buckets than it wants (it is "underweight"). If, after this, there
are still underweight next hops, another upkeep run is scheduled to a
future time.
Chances are there are not enough "idle" buckets to satisfy the new demands.
The algorithm has knobs to select both what it means for a bucket to be
idle, and for whether and when to forcefully migrate buckets if there keeps
being an insufficient number of idle buckets.
There are three users of the resilient data structures.
- The forwarding code accesses them under RCU, and does not modify them
except for updating the time a selected bucket was last used.
- Netlink code, running under RTNL, which may modify the data.
- The delayed upkeep code, which may modify the data. This runs unlocked,
and mutual exclusion between the RTNL code and the delayed upkeep is
maintained by canceling the delayed work synchronously before the RTNL
code touches anything. Later it restarts the delayed work if necessary.
The RTNL code has to implement next-hop group replacement, next hop
removal, etc. For removal, the mpath code uses a neat trick of having a
backup next hop group structure, doing the necessary changes offline, and
then RCU-swapping them in. However, the hash tables for resilient hashing
are about an order of magnitude larger than the groups themselves (the size
might be e.g. 4K entries), and it was felt that keeping two of them is an
overkill. Both the primary next-hop group and the spare therefore use the
same resilient table, and writers are careful to keep all references valid
for the forwarding code. The hash table references next-hop group entries
from the next-hop group that is currently in the primary role (i.e. not
spare). During the transition from primary to spare, the table references a
mix of both the primary group and the spare. When a next hop is deleted,
the corresponding buckets are not set to NULL, but instead marked as empty,
so that the pointer is valid and can be used by the forwarding code. The
buckets are then migrated to a new next-hop group entry during upkeep. The
only times that the hash table is invalid is the very beginning and very
end of its lifetime. Between those points, it is always kept valid.
This patch introduces the core support code itself. It does not handle
notifications towards drivers, which are kept as if the group were an mpath
one. It does not handle netlink either. The only bit currently exposed to
user space is the new next-hop group type, and that is currently bounced.
There is therefore no way to actually access this code.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
- set the new flag is_multipath for resilient groups
include/net/nexthop.h | 42 ++++
net/ipv4/nexthop.c | 517 ++++++++++++++++++++++++++++++++++++++++--
2 files changed, 546 insertions(+), 13 deletions(-)
@@ -63,6 +69,32 @@ struct nh_info {};};+structnh_res_bucket{+structnh_grp_entry__rcu*nh_entry;+atomic_long_tused_time;+unsignedlongmigrated_time;+booloccupied;+u8nh_flags;+};++structnh_res_table{+structnet*net;+u32nhg_id;+structdelayed_workupkeep_dw;++/* List of NHGEs that have too few buckets ("uw" for underweight).+*Reclaimedbucketswillbegiventoentriesinthislist.+*/+structlist_headuw_nh_entries;+unsignedlongunbalanced_since;++u32idle_timer;+u32unbalanced_timer;++u16num_nh_buckets;+structnh_res_bucketnh_buckets[];+};+structnh_grp_entry{structnexthop*nh;u8weight;
@@ -71,6 +103,13 @@ struct nh_grp_entry {struct{atomic_tupper_bound;}mpath;+struct{+/* Member on uw_nh_entries. */+structlist_headuw_nh_entry;++u16count_buckets;+u16wants_buckets;+}res;};structlist_headnh_list;
@@ -183,6 +183,30 @@ static int call_nexthop_notifiers(struct net *net,returnnotifier_to_errno(err);}+/* There are three users of RES_TABLE, and NHs etc. referenced from there:+*+*1)acollectionofcallbacksforNHmaintenance.Thisoperatesunder+*RTNL,+*2)thedelayedworkthatgraduallybalancestheresilienttable,+*3)andnexthop_select_path(),operatingunderRCU.+*+*BoththedelayedworkandtheRTNLblockarewriters,andneedto+*maintainmutualexclusion.Sincethereareonlytwoandwell-known+*writersforeachtable,theRTNLcodecanmakesureithasexclusive+*accessthus:+*+*-HavetheDWoperatewithoutlocking;+*-synchronouslycanceltheDW;+*-dothewriting;+*-ifthewritewasnotactuallyadelete,callupkeep,whichschedules+*DWagainifnecessary.+*+*ThefunctionsthatarealwayscalledfromtheRTNLcontextuse+*rtnl_dereference().ThefunctionsthatcanalsobecalledfromtheDWdo+*arawdereferenceandrelyontheabovemutualexclusionscheme.+*/+#define nh_res_dereference(p) (rcu_dereference_raw(p))+staticintcall_nexthop_notifier(structnotifier_block*nb,structnet*net,enumnexthop_event_typeevent_type,structnexthop*nh,
@@ -347,6 +398,13 @@ static u32 nh_find_unused_id(struct net *net)return0;}+staticvoidnh_res_time_set_deadline(unsignedlongnext_time,+unsignedlong*deadline)+{+if(time_before(next_time,*deadline))+*deadline=next_time;+}+staticintnla_put_nh_group(structsk_buff*skb,structnh_group*nhg){structnexthop_grp*p;
@@ -540,20 +598,62 @@ static void nexthop_notify(int event, struct nexthop *nh, struct nl_info *info)rtnl_set_sk_err(info->nl_net,RTNLGRP_NEXTHOP,err);}+staticunsignedlongnh_res_bucket_used_time(conststructnh_res_bucket*bucket)+{+return(unsignedlong)atomic_long_read(&bucket->used_time);+}++staticunsignedlong+nh_res_bucket_idle_point(conststructnh_res_table*res_table,+conststructnh_res_bucket*bucket,+unsignedlongnow)+{+unsignedlongtime=nh_res_bucket_used_time(bucket);++/* Bucket was not used since it was migrated. The idle time is now. */+if(time==bucket->migrated_time)+returnnow;++returntime+res_table->idle_timer;+}++staticunsignedlong+nh_res_table_unb_point(conststructnh_res_table*res_table)+{+returnres_table->unbalanced_since+res_table->unbalanced_timer;+}++staticvoidnh_res_bucket_set_idle(conststructnh_res_table*res_table,+structnh_res_bucket*bucket)+{+unsignedlongnow=jiffies;++atomic_long_set(&bucket->used_time,(long)now);+bucket->migrated_time=now;+}++staticvoidnh_res_bucket_set_busy(structnh_res_bucket*bucket)+{+atomic_long_set(&bucket->used_time,(long)jiffies);+}+staticboolvalid_group_nh(structnexthop*nh,unsignedintnpaths,bool*is_fdb,structnetlink_ext_ack*extack){if(nh->is_group){structnh_group*nhg=rtnl_dereference(nh->nh_grp);-/* nested multipath (group within a group) is not-*supported-*/+/* Nesting groups within groups is not supported. */if(nhg->mpath){NL_SET_ERR_MSG(extack,"Multipath group can not be a nexthop within a group");returnfalse;}+if(nhg->resilient){+NL_SET_ERR_MSG(extack,+"Resilient group can not be a nexthop within a group");+returnfalse;+}*is_fdb=nhg->fdb_nh;}else{structnh_info*nhi=rtnl_dereference(nh->nh_info);
@@ -734,6 +834,22 @@ static struct nexthop *nexthop_select_path_mp(struct nh_group *nhg, int hash)returnrc;}+staticstructnexthop*nexthop_select_path_res(structnh_group*nhg,inthash)+{+structnh_res_table*res_table=rcu_dereference(nhg->res_table);+u16bucket_index=hash%res_table->num_nh_buckets;+structnh_res_bucket*bucket;+structnh_grp_entry*nhge;++/* nexthop_select_path() is expected to return a non-NULL value, so+*skipprotocolvalidationandjusthandoutwhateverthereis.+*/+bucket=&res_table->nh_buckets[bucket_index];+nh_res_bucket_set_busy(bucket);+nhge=rcu_dereference(bucket->nh_entry);+returnnhge->nh;+}+structnexthop*nexthop_select_path(structnexthop*nh,inthash){structnh_group*nhg;
@@ -926,7 +1044,289 @@ static int fib_check_nh_list(struct nexthop *old, struct nexthop *new,return0;}-staticvoidnh_group_rebalance(structnh_group*nhg)+staticboolnh_res_nhge_is_balanced(conststructnh_grp_entry*nhge)+{+returnnhge->res.count_buckets==nhge->res.wants_buckets;+}++staticboolnh_res_nhge_is_ow(conststructnh_grp_entry*nhge)+{+returnnhge->res.count_buckets>nhge->res.wants_buckets;+}++staticboolnh_res_nhge_is_uw(conststructnh_grp_entry*nhge)+{+returnnhge->res.count_buckets<nhge->res.wants_buckets;+}++staticboolnh_res_table_is_balanced(conststructnh_res_table*res_table)+{+returnlist_empty(&res_table->uw_nh_entries);+}++staticvoidnh_res_bucket_unset_nh(structnh_res_bucket*bucket)+{+structnh_grp_entry*nhge;++if(bucket->occupied){+nhge=nh_res_dereference(bucket->nh_entry);+nhge->res.count_buckets--;+bucket->occupied=false;+}+}++staticvoidnh_res_bucket_set_nh(structnh_res_bucket*bucket,+structnh_grp_entry*nhge)+{+nh_res_bucket_unset_nh(bucket);++bucket->occupied=true;+rcu_assign_pointer(bucket->nh_entry,nhge);+nhge->res.count_buckets++;+}++staticboolnh_res_bucket_should_migrate(structnh_res_table*res_table,+structnh_res_bucket*bucket,+unsignedlong*deadline,bool*force)+{+unsignedlongnow=jiffies;+structnh_grp_entry*nhge;+unsignedlongidle_point;++if(!bucket->occupied){+/* The bucket is not occupied, its NHGE pointer is either+*NULLorobsolete.We_haveto_migrate:setforce.+*/+*force=true;+returntrue;+}++nhge=nh_res_dereference(bucket->nh_entry);++/* If the bucket is populated by an underweight or balanced+*nexthop,donotmigrate.+*/+if(!nh_res_nhge_is_ow(nhge))+returnfalse;++/* At this point we know that the bucket is populated with an+*overweightnexthop.Itneedstobemigratedtoanewnexthopif+*theidletimerofunbalancedtimerexpired.+*/++idle_point=nh_res_bucket_idle_point(res_table,bucket,now);+if(time_after_eq(now,idle_point)){+/* The bucket is idle. We _can_ migrate: unset force. */+*force=false;+returntrue;+}++/* Unbalanced timer of 0 means "never force". */+if(res_table->unbalanced_timer){+unsignedlongunb_point;++unb_point=nh_res_table_unb_point(res_table);+if(time_after(now,unb_point)){+/* The bucket is not idle, but the unbalanced timer+*expired.We_can_migrate,butsetforceanyway,+*sothatdriversknowtoignoreactivityreports+*fromtheHW.+*/+*force=true;+returntrue;+}++nh_res_time_set_deadline(unb_point,deadline);+}++nh_res_time_set_deadline(idle_point,deadline);+returnfalse;+}++staticboolnh_res_bucket_migrate(structnh_res_table*res_table,+u16bucket_index,boolforce)+{+structnh_res_bucket*bucket=&res_table->nh_buckets[bucket_index];+structnh_grp_entry*new_nhge;++new_nhge=list_first_entry_or_null(&res_table->uw_nh_entries,+structnh_grp_entry,+res.uw_nh_entry);+if(WARN_ON_ONCE(!new_nhge))+/* If this function is called, "bucket" is either not+*occupied,oritbelongstoanexthopthatis+*overweight.Ineithercase,thereoughttobea+*correspondingunderweightnexthop.+*/+returnfalse;++nh_res_bucket_set_nh(bucket,new_nhge);+nh_res_bucket_set_idle(res_table,bucket);++if(nh_res_nhge_is_balanced(new_nhge))+list_del(&new_nhge->res.uw_nh_entry);+returntrue;+}++#define NH_RES_UPKEEP_DW_MINIMUM_INTERVAL (HZ / 2)++staticvoidnh_res_table_upkeep(structnh_res_table*res_table)+{+unsignedlongnow=jiffies;+unsignedlongdeadline;+u16i;++/* Deadline is the next time that upkeep should be run. It is the+*earliesttimeatwhichoneofthebucketsmightbemigrated.+*Startatthemostpessimisticestimate:eitherunbalanced_timer+*fromnow,orifthereisnone,idle_timerfromnow.Foreach+*encounteredtimepoint,callnh_res_time_set_deadline()to+*refinetheestimate.+*/+if(res_table->unbalanced_timer)+deadline=now+res_table->unbalanced_timer;+else+deadline=now+res_table->idle_timer;++for(i=0;i<res_table->num_nh_buckets;i++){+structnh_res_bucket*bucket=&res_table->nh_buckets[i];+boolforce;++if(nh_res_bucket_should_migrate(res_table,bucket,+&deadline,&force)){+if(!nh_res_bucket_migrate(res_table,i,force)){+unsignedlongidle_point;++/* A driver can override the migration+*decisioniftheHWreportsthatthe+*bucketisactuallynotidle.Therefore+*remarkthebucketasbusyagainand+*updatethedeadline.+*/+nh_res_bucket_set_busy(bucket);+idle_point=nh_res_bucket_idle_point(res_table,+bucket,+now);+nh_res_time_set_deadline(idle_point,&deadline);+}+}+}++/* If the group is still unbalanced, schedule the next upkeep to+*eitherthedeadlinecomputedabove,ortheminimumdeadline,+*whichevercomeslater.+*/+if(!nh_res_table_is_balanced(res_table)){+unsignedlongnow=jiffies;+unsignedlongmin_deadline;++min_deadline=now+NH_RES_UPKEEP_DW_MINIMUM_INTERVAL;+if(time_before(deadline,min_deadline))+deadline=min_deadline;++queue_delayed_work(system_power_efficient_wq,+&res_table->upkeep_dw,deadline-now);+}+}++staticvoidnh_res_table_upkeep_dw(structwork_struct*work)+{+structdelayed_work*dw=to_delayed_work(work);+structnh_res_table*res_table;++res_table=container_of(dw,structnh_res_table,upkeep_dw);+nh_res_table_upkeep(res_table);+}++staticvoidnh_res_table_cancel_upkeep(structnh_res_table*res_table)+{+cancel_delayed_work_sync(&res_table->upkeep_dw);+}++staticvoidnh_res_group_rebalance(structnh_group*nhg,+structnh_res_table*res_table)+{+intprev_upper_bound=0;+inttotal=0;+intw=0;+inti;++INIT_LIST_HEAD(&res_table->uw_nh_entries);++for(i=0;i<nhg->num_nh;++i)+total+=nhg->nh_entries[i].weight;++for(i=0;i<nhg->num_nh;++i){+structnh_grp_entry*nhge=&nhg->nh_entries[i];+intupper_bound;++w+=nhge->weight;+upper_bound=DIV_ROUND_CLOSEST(res_table->num_nh_buckets*w,+total);+nhge->res.wants_buckets=upper_bound-prev_upper_bound;+prev_upper_bound=upper_bound;++if(nh_res_nhge_is_uw(nhge)){+if(list_empty(&res_table->uw_nh_entries))+res_table->unbalanced_since=jiffies;+list_add(&nhge->res.uw_nh_entry,+&res_table->uw_nh_entries);+}+}+}++/* Migrate buckets in res_table so that they reference NHGE's from NHG with+*therightNHID.SetthosebucketsthatdonothaveacorrespondingNHGE+*entryinNHGasnotoccupied.+*/+staticvoidnh_res_table_migrate_buckets(structnh_res_table*res_table,+structnh_group*nhg)+{+u16i;++for(i=0;i<res_table->num_nh_buckets;i++){+structnh_res_bucket*bucket=&res_table->nh_buckets[i];+u32id=rtnl_dereference(bucket->nh_entry)->nh->id;+boolfound=false;+intj;++for(j=0;j<nhg->num_nh;j++){+structnh_grp_entry*nhge=&nhg->nh_entries[j];++if(nhge->nh->id==id){+nh_res_bucket_set_nh(bucket,nhge);+found=true;+break;+}+}++if(!found)+nh_res_bucket_unset_nh(bucket);+}+}++staticvoidreplace_nexthop_grp_res(structnh_group*oldg,+structnh_group*newg)+{+/* For NH group replacement, the new NHG might only have a stub+*hashtablewith0buckets,becausethenumberofbucketswasnot+*specified.ForNHremoval,oldgandnewgbothreferencethesame+*res_table.Soinanycase,inthefollowing,wewanttowork+*witholdg->res_table.+*/+structnh_res_table*old_res_table=rtnl_dereference(oldg->res_table);+unsignedlongprev_unbalanced_since=old_res_table->unbalanced_since;+boolprev_has_uw=!list_empty(&old_res_table->uw_nh_entries);++nh_res_table_cancel_upkeep(old_res_table);+nh_res_table_migrate_buckets(old_res_table,newg);+nh_res_group_rebalance(newg,old_res_table);+if(prev_has_uw&&!list_empty(&old_res_table->uw_nh_entries))+old_res_table->unbalanced_since=prev_unbalanced_since;+nh_res_table_upkeep(old_res_table);+}++staticvoidnh_mp_group_rebalance(structnh_group*nhg){inttotal=0;intw=0;
@@ -1035,6 +1441,11 @@ static void remove_nexthop_group(struct nexthop *nh, struct nl_info *nlinfo)list_del_init(&nhge->nh_list);}++if(nhg->resilient){+res_table=rtnl_dereference(nhg->res_table);+nh_res_table_cancel_upkeep(res_table);+}}/* not called for nexthop replace */
@@ -1113,6 +1524,9 @@ static int replace_nexthop_grp(struct net *net, struct nexthop *old,structnexthop*new,conststructnh_config*cfg,structnetlink_ext_ack*extack){+structnh_res_table*tmp_table=NULL;+structnh_res_table*new_res_table;+structnh_res_table*old_res_table;structnh_group*oldg,*newg;inti,err;
@@ -1121,19 +1535,57 @@ static int replace_nexthop_grp(struct net *net, struct nexthop *old,return-EINVAL;}-err=call_nexthop_notifiers(net,NEXTHOP_EVENT_REPLACE,new,extack);-if(err)-returnerr;-oldg=rtnl_dereference(old->nh_grp);newg=rtnl_dereference(new->nh_grp);+if(newg->mpath!=oldg->mpath){+NL_SET_ERR_MSG(extack,"Can not replace a nexthop group with one of a different type.");+return-EINVAL;+}++if(newg->mpath){+err=call_nexthop_notifiers(net,NEXTHOP_EVENT_REPLACE,new,+extack);+if(err)+returnerr;+}elseif(newg->resilient){+new_res_table=rtnl_dereference(newg->res_table);+old_res_table=rtnl_dereference(oldg->res_table);++/* Accept if num_nh_buckets was not given, but if it was+*given,demandthatthevaluebecorrect.+*/+if(cfg->nh_grp_res_has_num_buckets&&+cfg->nh_grp_res_num_buckets!=+old_res_table->num_nh_buckets){+NL_SET_ERR_MSG(extack,"Can not change number of buckets of a resilient nexthop group.");+return-EINVAL;+}++if(cfg->nh_grp_res_has_idle_timer)+old_res_table->idle_timer=cfg->nh_grp_res_idle_timer;+if(cfg->nh_grp_res_has_unbalanced_timer)+old_res_table->unbalanced_timer=+cfg->nh_grp_res_unbalanced_timer;++replace_nexthop_grp_res(oldg,newg);++tmp_table=new_res_table;+rcu_assign_pointer(newg->res_table,old_res_table);+rcu_assign_pointer(newg->spare->res_table,old_res_table);+}+/* update parents - used by nexthop code for cleanup */for(i=0;i<newg->num_nh;i++)newg->nh_entries[i].nh_parent=old;rcu_assign_pointer(old->nh_grp,newg);+if(newg->resilient){+rcu_assign_pointer(oldg->res_table,tmp_table);+rcu_assign_pointer(oldg->spare->res_table,tmp_table);+}+for(i=0;i<oldg->num_nh;i++)oldg->nh_entries[i].nh_parent=new;
@@ -1383,6 +1835,27 @@ static int insert_nexthop(struct net *net, struct nexthop *new_nh,gotoout;}+if(new_nh->is_group){+structnh_group*nhg=rtnl_dereference(new_nh->nh_grp);+structnh_res_table*res_table;++if(nhg->resilient){+res_table=rtnl_dereference(nhg->res_table);++/* Not passing the number of buckets is OK when+*replacing,butnotwhencreatinganewgroup.+*/+if(!cfg->nh_grp_res_has_num_buckets){+NL_SET_ERR_MSG(extack,"Number of buckets not specified for nexthop group insertion");+rc=-EINVAL;+gotoout;+}++nh_res_group_rebalance(nhg,res_table);+nh_res_table_upkeep(res_table);+}+}+rb_link_node_rcu(&new_nh->rb_node,parent,pp);rb_insert_color(&new_nh->rb_node,root);
@@ -1445,6 +1918,7 @@ static struct nexthop *nexthop_create_group(struct net *net,u16num_nh=nla_len(grps_attr)/sizeof(*entry);structnh_group*nhg;structnexthop*nh;+interr;inti;if(WARN_ON(!num_nh))
@@ -1476,8 +1950,10 @@ static struct nexthop *nexthop_create_group(struct net *net,structnh_info*nhi;nhe=nexthop_find_by_id(net,entry[i].id);-if(!nexthop_get(nhe))+if(!nexthop_get(nhe)){+err=-ENOENT;gotoout_no_nh;+}nhi=rtnl_dereference(nhe->nh_info);if(nhi->family==AF_INET)
@@ -1493,13 +1969,28 @@ static struct nexthop *nexthop_create_group(struct net *net,nhg->mpath=1;nhg->is_multipath=true;}elseif(cfg->nh_grp_type==NEXTHOP_GRP_TYPE_RES){+structnh_res_table*res_table;++/* Bounce resilient groups for now. */+err=-EINVAL;gotoout_no_nh;++res_table=nexthop_res_table_alloc(net,cfg->nh_id,cfg);+if(!res_table){+err=-ENOMEM;+gotoout_no_nh;+}++rcu_assign_pointer(nhg->spare->res_table,res_table);+rcu_assign_pointer(nhg->res_table,res_table);+nhg->resilient=true;+nhg->is_multipath=true;}-WARN_ON_ONCE(nhg->mpath!=1);+WARN_ON_ONCE(nhg->mpath+nhg->resilient!=1);if(nhg->mpath)-nh_group_rebalance(nhg);+nh_mp_group_rebalance(nhg);if(cfg->nh_fdb)nhg->fdb_nh=1;
@@ -1518,7 +2009,7 @@ static struct nexthop *nexthop_create_group(struct net *net,kfree(nhg);kfree(nh);-returnERR_PTR(-ENOENT);+returnERR_PTR(err);}staticintnh_create_ipv4(structnet*net,structnexthop*nh,
From: David Ahern <hidden> Date: 2021-03-11 15:49:00
On 3/10/21 8:02 AM, Petr Machata wrote:
At this moment, there is only one type of next-hop group: an mpath group,
which implements the hash-threshold algorithm.
To select a next hop, hash-threshold algorithm first assigns a range of
hashes to each next hop in the group, and then selects the next hop by
comparing the SKB hash with the individual ranges. When a next hop is
removed from the group, the ranges are recomputed, which leads to
reassignment of parts of hash space from one next hop to another. While
there will usually be some overlap between the previous and the new
distribution, some traffic flows change the next hop that they resolve to.
That causes problems e.g. as established TCP connections are reset, because
the traffic is forwarded to a server that is not familiar with the
connection.
Resilient hashing is a technique to address the above problem. Resilient
next-hop group has another layer of indirection between the group itself
and its constituent next hops: a hash table. The selection algorithm uses a
straightforward modulo operation to choose a hash bucket, and then reads
the next hop that this bucket contains, and forwards traffic there.
This indirection brings an important feature. In the hash-threshold
algorithm, the range of hashes associated with a next hop must be
continuous. With a hash table, mapping between the hash table buckets and
the individual next hops is arbitrary. Therefore when a next hop is deleted
the buckets that held it are simply reassigned to other next hops. When
weights of next hops in a group are altered, it may be possible to choose a
subset of buckets that are currently not used for forwarding traffic, and
use those to satisfy the new next-hop distribution demands, keeping the
"busy" buckets intact. This way, established flows are ideally kept being
forwarded to the same endpoints through the same paths as before the
next-hop group change.
In a nutshell, the algorithm works as follows. Each next hop has a number
of buckets that it wants to have, according to its weight and the number of
buckets in the hash table. In case of an event that might cause bucket
allocation change, the numbers for individual next hops are updated,
similarly to how ranges are updated for mpath group next hops. Following
that, a new "upkeep" algorithm runs, and for idle buckets that belong to a
next hop that is currently occupying more buckets than it wants (it is
"overweight"), it migrates the buckets to one of the next hops that has
fewer buckets than it wants (it is "underweight"). If, after this, there
are still underweight next hops, another upkeep run is scheduled to a
future time.
Chances are there are not enough "idle" buckets to satisfy the new demands.
The algorithm has knobs to select both what it means for a bucket to be
idle, and for whether and when to forcefully migrate buckets if there keeps
being an insufficient number of idle buckets.
There are three users of the resilient data structures.
- The forwarding code accesses them under RCU, and does not modify them
except for updating the time a selected bucket was last used.
- Netlink code, running under RTNL, which may modify the data.
- The delayed upkeep code, which may modify the data. This runs unlocked,
and mutual exclusion between the RTNL code and the delayed upkeep is
maintained by canceling the delayed work synchronously before the RTNL
code touches anything. Later it restarts the delayed work if necessary.
The RTNL code has to implement next-hop group replacement, next hop
removal, etc. For removal, the mpath code uses a neat trick of having a
backup next hop group structure, doing the necessary changes offline, and
then RCU-swapping them in. However, the hash tables for resilient hashing
are about an order of magnitude larger than the groups themselves (the size
might be e.g. 4K entries), and it was felt that keeping two of them is an
overkill. Both the primary next-hop group and the spare therefore use the
same resilient table, and writers are careful to keep all references valid
for the forwarding code. The hash table references next-hop group entries
from the next-hop group that is currently in the primary role (i.e. not
spare). During the transition from primary to spare, the table references a
mix of both the primary group and the spare. When a next hop is deleted,
the corresponding buckets are not set to NULL, but instead marked as empty,
so that the pointer is valid and can be used by the forwarding code. The
buckets are then migrated to a new next-hop group entry during upkeep. The
only times that the hash table is invalid is the very beginning and very
end of its lifetime. Between those points, it is always kept valid.
This patch introduces the core support code itself. It does not handle
notifications towards drivers, which are kept as if the group were an mpath
one. It does not handle netlink either. The only bit currently exposed to
user space is the new next-hop group type, and that is currently bounced.
There is therefore no way to actually access this code.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Thanks for the detailed documentation around exclusion expectations.
Reviewed-by: David Ahern <dsahern@kernel.org>
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:07
Implement the following notifications towards drivers:
- NEXTHOP_EVENT_REPLACE, when a resilient nexthop group is created.
- NEXTHOP_EVENT_BUCKET_REPLACE any time there is a change in assignment of
next hops to hash table buckets. That includes replacements, deletions,
and delayed upkeep cycles. Some bucket notifications can be vetoed by the
driver, to make it possible to propagate bucket busy-ness flags from the
HW back to the algorithm. Some are however forced, e.g. if a next hop is
deleted, all buckets that use this next hop simply must be migrated,
whether the HW wishes so or not.
- NEXTHOP_EVENT_RES_TABLE_PRE_REPLACE, before a resilient nexthop group is
replaced. Usually the driver will get the bucket notifications as well,
and could veto those. But in some cases, a bucket may not be migrated
immediately, but during delayed upkeep, and that is too late to roll the
transaction back. This notification allows the driver to take a look and
veto the new proposed group up front, before anything is committed.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
net/ipv4/nexthop.c | 320 +++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 308 insertions(+), 12 deletions(-)
@@ -183,6 +218,107 @@ static int call_nexthop_notifiers(struct net *net,returnnotifier_to_errno(err);}+staticint+nh_notifier_res_bucket_idle_timer_get(conststructnh_notifier_info*info,+boolforce,unsignedint*p_idle_timer_ms)+{+structnh_res_table*res_table;+structnh_group*nhg;+structnexthop*nh;+interr=0;++/* When 'force' is false, nexthop bucket replacement is performed+*becausethebucketwasdeemedtobeidle.Inthiscase,capable+*listenerscanchoosetoperformanatomicreplacement:Thebucketis+*onlyreplacedifitisinactive.However,iftheidletimerinterval+*issmallerthantheintervalinwhichalistenerisquerying+*buckets'activityfromthedevice,thenatomicreplacementshould+*notbetried.Passtheidletimervaluetolisteners,sothatthey+*coulddeterminewhichtypeofreplacementtoperform.+*/+if(force){+*p_idle_timer_ms=0;+return0;+}++rcu_read_lock();++nh=nexthop_find_by_id(info->net,info->id);+if(!nh){+err=-EINVAL;+gotoout;+}++nhg=rcu_dereference(nh->nh_grp);+res_table=rcu_dereference(nhg->res_table);+*p_idle_timer_ms=jiffies_to_msecs(res_table->idle_timer);++out:+rcu_read_unlock();++returnerr;+}++staticintnh_notifier_res_bucket_info_init(structnh_notifier_info*info,+u16bucket_index,boolforce,+structnh_info*oldi,+structnh_info*newi)+{+unsignedintidle_timer_ms;+interr;++err=nh_notifier_res_bucket_idle_timer_get(info,force,+&idle_timer_ms);+if(err)+returnerr;++info->type=NH_NOTIFIER_INFO_TYPE_RES_BUCKET;+info->nh_res_bucket=kzalloc(sizeof(*info->nh_res_bucket),+GFP_KERNEL);+if(!info->nh_res_bucket)+return-ENOMEM;++info->nh_res_bucket->bucket_index=bucket_index;+info->nh_res_bucket->idle_timer_ms=idle_timer_ms;+info->nh_res_bucket->force=force;+__nh_notifier_single_info_init(&info->nh_res_bucket->old_nh,oldi);+__nh_notifier_single_info_init(&info->nh_res_bucket->new_nh,newi);+return0;+}++staticvoidnh_notifier_res_bucket_info_fini(structnh_notifier_info*info)+{+kfree(info->nh_res_bucket);+}++staticint__call_nexthop_res_bucket_notifiers(structnet*net,u32nhg_id,+u16bucket_index,boolforce,+structnh_info*oldi,+structnh_info*newi,+structnetlink_ext_ack*extack)+{+structnh_notifier_infoinfo={+.net=net,+.extack=extack,+.id=nhg_id,+};+interr;++if(nexthop_notifiers_is_empty(net))+return0;++err=nh_notifier_res_bucket_info_init(&info,bucket_index,force,+oldi,newi);+if(err)+returnerr;++err=blocking_notifier_call_chain(&net->nexthop.notifier_chain,+NEXTHOP_EVENT_BUCKET_REPLACE,&info);+nh_notifier_res_bucket_info_fini(&info);++returnnotifier_to_errno(err);+}+/* There are three users of RES_TABLE, and NHs etc. referenced from there:**1)acollectionofcallbacksforNHmaintenance.Thisoperatesunder
@@ -207,6 +343,53 @@ static int call_nexthop_notifiers(struct net *net,*/#define nh_res_dereference(p) (rcu_dereference_raw(p))+staticintcall_nexthop_res_bucket_notifiers(structnet*net,u32nhg_id,+u16bucket_index,boolforce,+structnexthop*old_nh,+structnexthop*new_nh,+structnetlink_ext_ack*extack)+{+structnh_info*oldi=nh_res_dereference(old_nh->nh_info);+structnh_info*newi=nh_res_dereference(new_nh->nh_info);++return__call_nexthop_res_bucket_notifiers(net,nhg_id,bucket_index,+force,oldi,newi,extack);+}++staticintcall_nexthop_res_table_notifiers(structnet*net,structnexthop*nh,+structnetlink_ext_ack*extack)+{+structnh_notifier_infoinfo={+.net=net,+.extack=extack,+};+structnh_group*nhg;+interr;++ASSERT_RTNL();++if(nexthop_notifiers_is_empty(net))+return0;++/* At this point, the nexthop buckets are still not populated. Only+*emitanotificationwiththelogicalnexthops,sothatalistener+*couldpotentiallyvetoitincaseofunsupportedconfiguration.+*/+nhg=rtnl_dereference(nh->nh_grp);+err=nh_notifier_mp_info_init(&info,nhg);+if(err){+NL_SET_ERR_MSG(extack,"Failed to initialize nexthop notifier info");+returnerr;+}++err=blocking_notifier_call_chain(&net->nexthop.notifier_chain,+NEXTHOP_EVENT_RES_TABLE_PRE_REPLACE,+&info);+kfree(info.nh_grp);++returnnotifier_to_errno(err);+}+staticintcall_nexthop_notifier(structnotifier_block*nb,structnet*net,enumnexthop_event_typeevent_type,structnexthop*nh,
@@ -1160,6 +1345,28 @@ static bool nh_res_bucket_migrate(struct nh_res_table *res_table,*/returnfalse;+if(notify){+structnh_grp_entry*old_nhge;++old_nhge=nh_res_dereference(bucket->nh_entry);+err=call_nexthop_res_bucket_notifiers(res_table->net,+res_table->nhg_id,+bucket_index,force,+old_nhge->nh,+new_nhge->nh,&extack);+if(err){+pr_err_ratelimited("%s\n",extack._msg);+if(!force)+returnfalse;+/* It is not possible to veto a forced replacement, so+*justclearthehardwareflagsfromthenexthop+*buckettoindicatetouserspacethatthisbucketis+*notcorrectlypopulatedinhardware.+*/+bucket->nh_flags&=~(RTNH_F_OFFLOAD|RTNH_F_TRAP);+}+}+nh_res_bucket_set_nh(bucket,new_nhge);nh_res_bucket_set_idle(res_table,bucket);
@@ -1194,7 +1401,8 @@ static void nh_res_table_upkeep(struct nh_res_table *res_table)if(nh_res_bucket_should_migrate(res_table,bucket,&deadline,&force)){-if(!nh_res_bucket_migrate(res_table,i,force)){+if(!nh_res_bucket_migrate(res_table,i,notify,+force)){unsignedlongidle_point;/* A driver can override the migration
@@ -1407,9 +1615,15 @@ static void remove_nh_grp_entry(struct net *net, struct nh_grp_entry *nhge,list_del(&nhge->nh_list);nexthop_put(nhge->nh);-err=call_nexthop_notifiers(net,NEXTHOP_EVENT_REPLACE,nhp,&extack);-if(err)-pr_err("%s\n",extack._msg);+/* Removal of a NH from a resilient group is notified through+*bucketnotifications.+*/+if(newg->mpath){+err=call_nexthop_notifiers(net,NEXTHOP_EVENT_REPLACE,nhp,+&extack);+if(err)+pr_err("%s\n",extack._msg);+}if(nlinfo)nexthop_notify(RTM_NEWNEXTHOP,nhp,nlinfo);
@@ -1562,6 +1776,16 @@ static int replace_nexthop_grp(struct net *net, struct nexthop *old,return-EINVAL;}+/* Emit a pre-replace notification so that listeners could veto+*apotentiallyunsupportedconfiguration.Otherwise,+*individualbucketreplacementnotificationswouldneedtobe+*vetoed,whichissomethingthatshouldonlyhappenifthe+*bucketiscurrentlyactive.+*/+err=call_nexthop_res_table_notifiers(net,new,extack);+if(err)+returnerr;+if(cfg->nh_grp_res_has_idle_timer)old_res_table->idle_timer=cfg->nh_grp_res_idle_timer;if(cfg->nh_grp_res_has_unbalanced_timer)
@@ -1653,8 +1942,8 @@ static int replace_nexthop_single(struct net *net, struct nexthop *old,list_for_each_entry(nhge,&old->grp_list,nh_list){structnexthop*nhp=nhge->nh_parent;-err=call_nexthop_notifiers(net,NEXTHOP_EVENT_REPLACE,nhp,-extack);+err=replace_nexthop_single_notify(net,nhp,old,oldi,newi,+extack);if(err)gotoerr_notify;}
@@ -1684,7 +1973,7 @@ static int replace_nexthop_single(struct net *net, struct nexthop *old,list_for_each_entry_continue_reverse(nhge,&old->grp_list,nh_list){structnexthop*nhp=nhge->nh_parent;-call_nexthop_notifiers(net,NEXTHOP_EVENT_REPLACE,nhp,extack);+replace_nexthop_single_notify(net,nhp,old,newi,oldi,NULL);}call_nexthop_notifiers(net,NEXTHOP_EVENT_REPLACE,old,extack);returnerr;
@@ -1852,13 +2141,20 @@ static int insert_nexthop(struct net *net, struct nexthop *new_nh,}nh_res_group_rebalance(nhg,res_table);-nh_res_table_upkeep(res_table);++/* Do not send bucket notifications, we do full+*notificationbelow.+*/+nh_res_table_upkeep(res_table,false);}}rb_link_node_rcu(&new_nh->rb_node,parent,pp);rb_insert_color(&new_nh->rb_node,root);+/* The initial insertion is a full notification for mpath as well+*asresilientgroups.+*/rc=call_nexthop_notifiers(net,NEXTHOP_EVENT_REPLACE,new_nh,extack);if(rc)rb_erase(&new_nh->rb_node,&net->nexthop.rb_root);
From: David Ahern <hidden> Date: 2021-03-11 15:58:18
On 3/10/21 8:02 AM, Petr Machata wrote:
Implement the following notifications towards drivers:
- NEXTHOP_EVENT_REPLACE, when a resilient nexthop group is created.
- NEXTHOP_EVENT_BUCKET_REPLACE any time there is a change in assignment of
next hops to hash table buckets. That includes replacements, deletions,
and delayed upkeep cycles. Some bucket notifications can be vetoed by the
driver, to make it possible to propagate bucket busy-ness flags from the
HW back to the algorithm. Some are however forced, e.g. if a next hop is
deleted, all buckets that use this next hop simply must be migrated,
whether the HW wishes so or not.
- NEXTHOP_EVENT_RES_TABLE_PRE_REPLACE, before a resilient nexthop group is
replaced. Usually the driver will get the bucket notifications as well,
and could veto those. But in some cases, a bucket may not be migrated
immediately, but during delayed upkeep, and that is too late to roll the
transaction back. This notification allows the driver to take a look and
veto the new proposed group up front, before anything is committed.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
@@ -937,8 +1000,14 @@ static int nh_check_attr_group(struct net *net,for(i=NHA_GROUP_TYPE+1;i<tb_size;++i){if(!tb[i])continue;-if(i==NHA_FDB)+switch(i){+caseNHA_FDB:continue;+caseNHA_RES_GROUP:+if(nh_grp_type==NEXTHOP_GRP_TYPE_RES)+continue;+break;+}NL_SET_ERR_MSG(extack,"No other attributes can be set in nexthop groups");return-EINVAL;
@@ -2475,6 +2544,70 @@ static struct nexthop *nexthop_add(struct net *net, struct nh_config *cfg,returnnh;}+staticintrtm_nh_get_timer(structnlattr*attr,unsignedlongfallback,+unsignedlong*timer_p,bool*has_p,+structnetlink_ext_ack*extack)+{+unsignedlongtimer;+u32value;++if(!attr){+*timer_p=fallback;+*has_p=false;+return0;+}++value=nla_get_u32(attr);+timer=clock_t_to_jiffies(value);+if(timer==~0UL){+NL_SET_ERR_MSG(extack,"Timer value too large");+return-EINVAL;+}++*timer_p=timer;+*has_p=true;+return0;+}++staticintrtm_to_nh_config_grp_res(structnlattr*res,structnh_config*cfg,+structnetlink_ext_ack*extack)+{+structnlattr*tb[ARRAY_SIZE(rtm_nh_res_policy_new)]={};+interr;++if(res){+err=nla_parse_nested(tb,+ARRAY_SIZE(rtm_nh_res_policy_new)-1,+res,rtm_nh_res_policy_new,extack);+if(err<0)+returnerr;+}++if(tb[NHA_RES_GROUP_BUCKETS]){+cfg->nh_grp_res_num_buckets=+nla_get_u16(tb[NHA_RES_GROUP_BUCKETS]);+cfg->nh_grp_res_has_num_buckets=true;+if(!cfg->nh_grp_res_num_buckets){+NL_SET_ERR_MSG(extack,"Number of buckets needs to be non-0");+return-EINVAL;+}+}++err=rtm_nh_get_timer(tb[NHA_RES_GROUP_IDLE_TIMER],+NH_RES_DEFAULT_IDLE_TIMER,+&cfg->nh_grp_res_idle_timer,+&cfg->nh_grp_res_has_idle_timer,+extack);+if(err)+returnerr;++returnrtm_nh_get_timer(tb[NHA_RES_GROUP_UNBALANCED_TIMER],+NH_RES_DEFAULT_UNBALANCED_TIMER,+&cfg->nh_grp_res_unbalanced_timer,+&cfg->nh_grp_res_has_unbalanced_timer,+extack);+}+staticintrtm_to_nh_config(structnet*net,structsk_buff*skb,structnlmsghdr*nlh,structnh_config*cfg,structnetlink_ext_ack*extack)
@@ -2553,7 +2686,14 @@ static int rtm_to_nh_config(struct net *net, struct sk_buff *skb,NL_SET_ERR_MSG(extack,"Invalid group type");gotoout;}-err=nh_check_attr_group(net,tb,ARRAY_SIZE(tb),extack);+err=nh_check_attr_group(net,tb,ARRAY_SIZE(tb),+cfg->nh_grp_type,extack);+if(err)+gotoout;++if(cfg->nh_grp_type==NEXTHOP_GRP_TYPE_RES)+err=rtm_to_nh_config_grp_res(tb[NHA_RES_GROUP],+cfg,extack);/* no other attributes should be set */gotoout;
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:07
Allow getting (but not setting) individual buckets to inspect the next hop
mapped therein, idle time, and flags.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
net/ipv4/nexthop.c | 110 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 109 insertions(+), 1 deletion(-)
@@ -3381,6 +3390,105 @@ static int rtm_dump_nexthop_bucket(struct sk_buff *skb,returnerr;}+staticintnh_valid_get_bucket_req_res_bucket(structnlattr*res,+u16*bucket_index,+structnetlink_ext_ack*extack)+{+structnlattr*tb[ARRAY_SIZE(rtm_nh_res_bucket_policy_get)];+interr;++err=nla_parse_nested(tb,ARRAY_SIZE(rtm_nh_res_bucket_policy_get)-1,+res,rtm_nh_res_bucket_policy_get,extack);+if(err<0)+returnerr;++if(!tb[NHA_RES_BUCKET_INDEX]){+NL_SET_ERR_MSG(extack,"Bucket index is missing");+return-EINVAL;+}++*bucket_index=nla_get_u16(tb[NHA_RES_BUCKET_INDEX]);+return0;+}++staticintnh_valid_get_bucket_req(conststructnlmsghdr*nlh,+u32*id,u16*bucket_index,+structnetlink_ext_ack*extack)+{+structnlattr*tb[ARRAY_SIZE(rtm_nh_policy_get_bucket)];+interr;++err=nlmsg_parse(nlh,sizeof(structnhmsg),tb,+ARRAY_SIZE(rtm_nh_policy_get_bucket)-1,+rtm_nh_policy_get_bucket,extack);+if(err<0)+returnerr;++err=__nh_valid_get_del_req(nlh,tb,id,extack);+if(err)+returnerr;++if(!tb[NHA_RES_BUCKET]){+NL_SET_ERR_MSG(extack,"Bucket information is missing");+return-EINVAL;+}++err=nh_valid_get_bucket_req_res_bucket(tb[NHA_RES_BUCKET],+bucket_index,extack);+if(err)+returnerr;++return0;+}++/* rtnl */+staticintrtm_get_nexthop_bucket(structsk_buff*in_skb,structnlmsghdr*nlh,+structnetlink_ext_ack*extack)+{+structnet*net=sock_net(in_skb->sk);+structnh_res_table*res_table;+structsk_buff*skb=NULL;+structnh_group*nhg;+structnexthop*nh;+u16bucket_index;+interr;+u32id;++err=nh_valid_get_bucket_req(nlh,&id,&bucket_index,extack);+if(err)+returnerr;++nh=nexthop_find_group_resilient(net,id,extack);+if(IS_ERR(nh))+returnPTR_ERR(nh);++nhg=rtnl_dereference(nh->nh_grp);+res_table=rtnl_dereference(nhg->res_table);+if(bucket_index>=res_table->num_nh_buckets){+NL_SET_ERR_MSG(extack,"Bucket index out of bounds");+return-ENOENT;+}++skb=alloc_skb(NLMSG_GOODSIZE,GFP_KERNEL);+if(!skb)+return-ENOBUFS;++err=nh_fill_res_bucket(skb,nh,&res_table->nh_buckets[bucket_index],+bucket_index,RTM_NEWNEXTHOPBUCKET,+NETLINK_CB(in_skb).portid,nlh->nlmsg_seq,+0,extack);+if(err<0){+WARN_ON(err==-EMSGSIZE);+gotoerrout_free;+}++returnrtnl_unicast(skb,net,NETLINK_CB(in_skb).portid);++errout_free:+kfree_skb(skb);+returnerr;+}+staticvoidnexthop_sync_mtu(structnet_device*dev,u32orig_mtu){unsignedinthash=nh_dev_hashfn(dev->ifindex);
@@ -3604,7 +3712,7 @@ static int __init nexthop_init(void)rtnl_register(PF_INET6,RTM_NEWNEXTHOP,rtm_new_nexthop,NULL,0);rtnl_register(PF_INET6,RTM_GETNEXTHOP,NULL,rtm_dump_nexthop,0);-rtnl_register(PF_UNSPEC,RTM_GETNEXTHOPBUCKET,NULL,+rtnl_register(PF_UNSPEC,RTM_GETNEXTHOPBUCKET,rtm_get_nexthop_bucket,rtm_dump_nexthop_bucket,0);return0;
From: David Ahern <hidden> Date: 2021-03-11 16:20:29
On 3/10/21 8:03 AM, Petr Machata wrote:
Allow getting (but not setting) individual buckets to inspect the next hop
mapped therein, idle time, and flags.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
net/ipv4/nexthop.c | 110 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 109 insertions(+), 1 deletion(-)
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:07
Nexthop replacements et.al. are notified through netlink, but if a delayed
work migrates buckets on the background, userspace will stay oblivious.
Notify these as RTM_NEWNEXTHOPBUCKET events.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
net/ipv4/nexthop.c | 45 +++++++++++++++++++++++++++++++++++++++------
1 file changed, 39 insertions(+), 6 deletions(-)
@@ -2288,7 +2321,7 @@ static int insert_nexthop(struct net *net, struct nexthop *new_nh,/* Do not send bucket notifications, we do full*notificationbelow.*/-nh_res_table_upkeep(res_table,false);+nh_res_table_upkeep(res_table,false,false);}}
From: David Ahern <hidden> Date: 2021-03-11 16:21:00
On 3/10/21 8:03 AM, Petr Machata wrote:
Nexthop replacements et.al. are notified through netlink, but if a delayed
work migrates buckets on the background, userspace will stay oblivious.
Notify these as RTM_NEWNEXTHOPBUCKET events.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
net/ipv4/nexthop.c | 45 +++++++++++++++++++++++++++++++++++++++------
1 file changed, 39 insertions(+), 6 deletions(-)
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:07
Add a dump handler for resilient next hop buckets. When next-hop group ID
is given, it walks buckets of that group, otherwise it walks buckets of all
groups. It then dumps the buckets whose next hops match the given filtering
criteria.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
net/ipv4/nexthop.c | 283 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 283 insertions(+)
@@ -3101,6 +3168,219 @@ static int rtm_dump_nexthop(struct sk_buff *skb, struct netlink_callback *cb)returnerr;}+staticstructnexthop*+nexthop_find_group_resilient(structnet*net,u32id,+structnetlink_ext_ack*extack)+{+structnh_group*nhg;+structnexthop*nh;++nh=nexthop_find_by_id(net,id);+if(!nh)+returnERR_PTR(-ENOENT);++if(!nh->is_group){+NL_SET_ERR_MSG(extack,"Not a nexthop group");+returnERR_PTR(-EINVAL);+}++nhg=rtnl_dereference(nh->nh_grp);+if(!nhg->resilient){+NL_SET_ERR_MSG(extack,"Nexthop group not of type resilient");+returnERR_PTR(-EINVAL);+}++returnnh;+}++staticintnh_valid_dump_nhid(structnlattr*attr,u32*nh_id_p,+structnetlink_ext_ack*extack)+{+u32idx;++if(attr){+idx=nla_get_u32(attr);+if(!idx){+NL_SET_ERR_MSG(extack,"Invalid nexthop id");+return-EINVAL;+}+*nh_id_p=idx;+}else{+*nh_id_p=0;+}++return0;+}++staticintnh_valid_dump_bucket_req(conststructnlmsghdr*nlh,+structnh_dump_filter*filter,+structnetlink_callback*cb)+{+structnlattr*res_tb[ARRAY_SIZE(rtm_nh_res_bucket_policy_dump)];+structnlattr*tb[ARRAY_SIZE(rtm_nh_policy_dump_bucket)];+interr;++err=nlmsg_parse(nlh,sizeof(structnhmsg),tb,+ARRAY_SIZE(rtm_nh_policy_dump_bucket)-1,+rtm_nh_policy_dump_bucket,NULL);+if(err<0)+returnerr;++err=nh_valid_dump_nhid(tb[NHA_ID],&filter->nh_id,cb->extack);+if(err)+returnerr;++if(tb[NHA_RES_BUCKET]){+size_tmax=ARRAY_SIZE(rtm_nh_res_bucket_policy_dump)-1;++err=nla_parse_nested(res_tb,max,+tb[NHA_RES_BUCKET],+rtm_nh_res_bucket_policy_dump,+cb->extack);+if(err<0)+returnerr;++err=nh_valid_dump_nhid(res_tb[NHA_RES_BUCKET_NH_ID],+&filter->res_bucket_nh_id,+cb->extack);+if(err)+returnerr;+}++return__nh_valid_dump_req(nlh,tb,filter,cb->extack);+}++structrtm_dump_res_bucket_ctx{+structrtm_dump_nh_ctxnh;+u16bucket_index;+u32done_nh_idx;/* 1 + the index of the last fully processed NH. */+};++staticstructrtm_dump_res_bucket_ctx*+rtm_dump_res_bucket_ctx(structnetlink_callback*cb)+{+structrtm_dump_res_bucket_ctx*ctx=(void*)cb->ctx;++BUILD_BUG_ON(sizeof(*ctx)>sizeof(cb->ctx));+returnctx;+}++structrtm_dump_nexthop_bucket_data{+structrtm_dump_res_bucket_ctx*ctx;+structnh_dump_filterfilter;+};++staticintrtm_dump_nexthop_bucket_nh(structsk_buff*skb,+structnetlink_callback*cb,+structnexthop*nh,+structrtm_dump_nexthop_bucket_data*dd)+{+u32portid=NETLINK_CB(cb->skb).portid;+structnhmsg*nhm=nlmsg_data(cb->nlh);+structnh_res_table*res_table;+structnh_group*nhg;+u16bucket_index;+interr;++if(dd->ctx->nh.idx<dd->ctx->done_nh_idx)+return0;++nhg=rtnl_dereference(nh->nh_grp);+res_table=rtnl_dereference(nhg->res_table);+for(bucket_index=dd->ctx->bucket_index;+bucket_index<res_table->num_nh_buckets;+bucket_index++){+structnh_res_bucket*bucket;+structnh_grp_entry*nhge;++bucket=&res_table->nh_buckets[bucket_index];+nhge=rtnl_dereference(bucket->nh_entry);+if(nh_dump_filtered(nhge->nh,&dd->filter,nhm->nh_family))+continue;++if(dd->filter.res_bucket_nh_id&&+dd->filter.res_bucket_nh_id!=nhge->nh->id)+continue;++err=nh_fill_res_bucket(skb,nh,bucket,bucket_index,+RTM_NEWNEXTHOPBUCKET,portid,+cb->nlh->nlmsg_seq,NLM_F_MULTI,+cb->extack);+if(err<0){+if(likely(skb->len))+gotoout;+gotoout_err;+}+}++dd->ctx->done_nh_idx=dd->ctx->nh.idx+1;+bucket_index=0;++out:+err=skb->len;+out_err:+dd->ctx->bucket_index=bucket_index;+returnerr;+}++staticintrtm_dump_nexthop_bucket_cb(structsk_buff*skb,+structnetlink_callback*cb,+structnexthop*nh,void*data)+{+structrtm_dump_nexthop_bucket_data*dd=data;+structnh_group*nhg;++if(!nh->is_group)+return0;++nhg=rtnl_dereference(nh->nh_grp);+if(!nhg->resilient)+return0;++returnrtm_dump_nexthop_bucket_nh(skb,cb,nh,dd);+}++/* rtnl */+staticintrtm_dump_nexthop_bucket(structsk_buff*skb,+structnetlink_callback*cb)+{+structrtm_dump_res_bucket_ctx*ctx=rtm_dump_res_bucket_ctx(cb);+structrtm_dump_nexthop_bucket_datadd={.ctx=ctx};+structnet*net=sock_net(skb->sk);+structnexthop*nh;+interr;++err=nh_valid_dump_bucket_req(cb->nlh,&dd.filter,cb);+if(err)+returnerr;++if(dd.filter.nh_id){+nh=nexthop_find_group_resilient(net,dd.filter.nh_id,+cb->extack);+if(IS_ERR(nh))+returnPTR_ERR(nh);+err=rtm_dump_nexthop_bucket_nh(skb,cb,nh,&dd);+}else{+structrb_root*root=&net->nexthop.rb_root;++err=rtm_dump_walk_nexthops(skb,cb,root,&ctx->nh,+&rtm_dump_nexthop_bucket_cb,&dd);+}++if(err<0){+if(likely(skb->len))+gotoout;+gotoout_err;+}++out:+err=skb->len;+out_err:+cb->seq=net->nexthop.seq;+nl_dump_check_consistent(cb,nlmsg_hdr(skb));+returnerr;+}+staticvoidnexthop_sync_mtu(structnet_device*dev,u32orig_mtu){unsignedinthash=nh_dev_hashfn(dev->ifindex);
@@ -3324,6 +3604,9 @@ static int __init nexthop_init(void)rtnl_register(PF_INET6,RTM_NEWNEXTHOP,rtm_new_nexthop,NULL,0);rtnl_register(PF_INET6,RTM_GETNEXTHOP,NULL,rtm_dump_nexthop,0);+rtnl_register(PF_UNSPEC,RTM_GETNEXTHOPBUCKET,NULL,+rtm_dump_nexthop_bucket,0);+return0;}subsys_initcall(nexthop_init);
From: David Ahern <hidden> Date: 2021-03-11 16:18:20
On 3/10/21 8:03 AM, Petr Machata wrote:
Add a dump handler for resilient next hop buckets. When next-hop group ID
is given, it walks buckets of that group, otherwise it walks buckets of all
groups. It then dumps the buckets whose next hops match the given filtering
criteria.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
Notes:
v1 (changes since RFC):
- u32 -> u16 for bucket counts / indices
From: Petr Machata <petrm@nvidia.com> Date: 2021-03-10 15:05:37
Now that all the code is in place, stop rejecting requests to create
resilient next-hop groups.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
net/ipv4/nexthop.c | 4 ----
1 file changed, 4 deletions(-)
From: David Ahern <hidden> Date: 2021-03-11 16:21:33
On 3/10/21 8:03 AM, Petr Machata wrote:
Now that all the code is in place, stop rejecting requests to create
resilient next-hop groups.
Signed-off-by: Petr Machata <petrm@nvidia.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
---
net/ipv4/nexthop.c | 4 ----
1 file changed, 4 deletions(-)
From: David Ahern <hidden> Date: 2021-03-11 16:23:41
On 3/10/21 8:02 AM, Petr Machata wrote:
At this moment, there is only one type of next-hop group: an mpath group.
Mpath groups implement the hash-threshold algorithm, described in RFC
2992[1].
To select a next hop, hash-threshold algorithm first assigns a range of
hashes to each next hop in the group, and then selects the next hop by
comparing the SKB hash with the individual ranges. When a next hop is
removed from the group, the ranges are recomputed, which leads to
reassignment of parts of hash space from one next hop to another. RFC 2992
illustrates it thus:
+-------+-------+-------+-------+-------+
| 1 | 2 | 3 | 4 | 5 |
+-------+-+-----+---+---+-----+-+-------+
| 1 | 2 | 4 | 5 |
+---------+---------+---------+---------+
Before and after deletion of next hop 3
under the hash-threshold algorithm.
Note how next hop 2 gave up part of the hash space in favor of next hop 1,
and 4 in favor of 5. While there will usually be some overlap between the
previous and the new distribution, some traffic flows change the next hop
that they resolve to.
If a multipath group is used for load-balancing between multiple servers,
this hash space reassignment causes an issue that packets from a single
flow suddenly end up arriving at a server that does not expect them, which
may lead to TCP reset.
If a multipath group is used for load-balancing among available paths to
the same server, the issue is that different latencies and reordering along
the way causes the packets to arrive in the wrong order.
Resilient hashing is a technique to address the above problem. Resilient
next-hop group has another layer of indirection between the group itself
and its constituent next hops: a hash table. The selection algorithm uses a
straightforward modulo operation on the SKB hash to choose a hash table
bucket, then reads the next hop that this bucket contains, and forwards
traffic there.
This indirection brings an important feature. In the hash-threshold
algorithm, the range of hashes associated with a next hop must be
continuous. With a hash table, mapping between the hash table buckets and
the individual next hops is arbitrary. Therefore when a next hop is deleted
the buckets that held it are simply reassigned to other next hops:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1|1|1|1|2|2|2|2|3|3|3|3|4|4|4|4|5|5|5|5|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
v v v v
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1|1|1|1|2|2|2|2|1|2|4|5|4|4|4|4|5|5|5|5|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Before and after deletion of next hop 3
under the resilient hashing algorithm.
When weights of next hops in a group are altered, it may be possible to
choose a subset of buckets that are currently not used for forwarding
traffic, and use those to satisfy the new next-hop distribution demands,
keeping the "busy" buckets intact. This way, established flows are ideally
kept being forwarded to the same endpoints through the same paths as before
the next-hop group change.
This patch set adds the implementation of resilient next-hop groups.
In a nutshell, the algorithm works as follows. Each next hop has a number
of buckets that it wants to have, according to its weight and the number of
buckets in the hash table. In case of an event that might cause bucket
allocation change, the numbers for individual next hops are updated,
similarly to how ranges are updated for mpath group next hops. Following
that, a new "upkeep" algorithm runs, and for idle buckets that belong to a
next hop that is currently occupying more buckets than it wants (it is
"overweight"), it migrates the buckets to one of the next hops that has
fewer buckets than it wants (it is "underweight"). If, after this, there
are still underweight next hops, another upkeep run is scheduled to a
future time.
Chances are there are not enough "idle" buckets to satisfy the new demands.
The algorithm has knobs to select both what it means for a bucket to be
idle, and for whether and when to forcefully migrate buckets if there keeps
being an insufficient number of idle ones.
To illustrate the usage, consider the following commands:
# ip nexthop add id 1 via 192.0.2.2 dev dummy1
# ip nexthop add id 2 via 192.0.2.3 dev dummy1
# ip nexthop add id 10 group 1/2 type resilient \
buckets 8 idle_timer 60 unbalanced_timer 300
The last command creates a resilient next-hop group. It will have 8
buckets, each bucket will be considered idle when no traffic hits it for at
least 60 seconds, and if the table remains out of balance for 300 seconds,
it will be forcefully brought into balance.
If not present in netlink message, the idle timer defaults to 120 seconds,
and there is no unbalanced timer, meaning the group may remain unbalanced
indefinitely. The value of 120 is the default in Cumulus implementation of
resilient next-hop groups. To a degree the default is arbitrary, the only
value that certainly does not make sense is 0. Therefore going with an
existing deployed implementation is reasonable.
Unbalanced time, i.e. how long since the last time that all nexthops had as
many buckets as they should according to their weights, is reported when
the group is dumped:
# ip nexthop show id 10
id 10 group 1/2 type resilient buckets 8 idle_timer 60 unbalanced_timer 300 unbalanced_time 0
When replacing next hops or changing weights, if one does not specify some
parameters, their value is left as it was:
# ip nexthop replace id 10 group 1,2/2 type resilient
# ip nexthop show id 10
id 10 group 1,2/2 type resilient buckets 8 idle_timer 60 unbalanced_timer 300 unbalanced_time 0
It is also possible to do a dump of individual buckets (and now you know
why there were only 8 of them in the example above):
# ip nexthop bucket show id 10
id 10 index 0 idle_time 5.59 nhid 1
id 10 index 1 idle_time 5.59 nhid 1
id 10 index 2 idle_time 8.74 nhid 2
id 10 index 3 idle_time 8.74 nhid 2
id 10 index 4 idle_time 8.74 nhid 1
id 10 index 5 idle_time 8.74 nhid 1
id 10 index 6 idle_time 8.74 nhid 1
id 10 index 7 idle_time 8.74 nhid 1
Note the two buckets that have a shorter idle time. Those are the ones that
were migrated after the nexthop replace command to satisfy the new demand
that nexthop 1 be given 6 buckets instead of 4.
The patchset proceeds as follows:
- Patches #1 and #2 are small refactoring patches.
- Patch #3 adds a new flag to struct nh_group, is_multipath. This flag is
meant to be set for all nexthop groups that in general have several
nexthops from which they choose, and avoids a more expensive dispatch
based on reading several flags, one for each nexthop group type.
- Patch #4 contains defines of new UAPI attributes and the new next-hop
group type. At this point, the nexthop code is made to bounce the new
type. As the resilient hashing code is gradually added in the following
patch sets, it will remain dead. The last patch will make it accessible.
This patch also adds a suite of new messages related to next hop buckets.
This approach was taken instead of overloading the information on the
existing RTM_{NEW,DEL,GET}NEXTHOP messages for the following reasons.
First, a next-hop group can contain a large number of next-hop buckets
(4k is not unheard of). This imposes limits on the amount of information
that can be encoded for each next-hop bucket given a netlink message is
limited to 64k bytes.
Second, while RTM_NEWNEXTHOPBUCKET is only used for notifications at this
point, in the future it can be extended to provide user space with
control over next-hop buckets configuration.
- Patch #5 contains the meat of the resilient next-hop group support.
- Patches #6 and #7 implement support for notifications towards the
drivers.
- Patch #8 adds an interface for the drivers to report resilient hash
table bucket activity. Drivers will be able to report through this
interface whether traffic is hitting a given bucket.
- Patch #9 adds an interface for the drivers to report whether a given
hash table bucket is offloaded or trapping traffic.
- In patches #10, #11, #12 and #13, UAPI is implemented. This includes all
the code necessary for creation of resilient groups, bucket dumping and
getting, and bucket migration notifications.
- In patch #14 the next-hop groups are finally made available.
The overall plan is to contribute approximately the following patchsets:
1) Nexthop policy refactoring (already pushed)
2) Preparations for resilient next-hop groups (already pushed)
3) Implementation of resilient next-hop groups (this patchset)
4) Netdevsim offload plus a suite of selftests
5) Preparations for mlxsw offload of resilient next-hop groups
6) mlxsw offload including selftests
Interested parties can look at the current state of the code at [2] and
[3].
[1] https://tools.ietf.org/html/rfc2992
[2] https://github.com/idosch/linux/commits/submit/res_integ_v1
[3] https://github.com/idosch/iproute2/commits/submit/res_v1
well done and well documented. Thanks for the attention to detail there.
When you get to the end of the sets, it would be good to submit
documentation for resilient multipath under Documentation/networking