Skip to content

Commit 4bb097b

Browse files
cskiralyfjl
andauthored
eth, p2p: improve dial speed by pre-fetching dial candidates (#31944)
This PR improves the speed of Disc/v4 and Disc/v5 based discovery by adding a prefetch buffer to discovery sources, eliminating slowdowns due to timeouts and rate mismatch between the two processes. Since we now want to filter the discv4 nodes iterator, it is being removed from the default discovery mix in p2p.Server. To keep backwards-compatibility, the default unfiltered discovery iterator will be utilized by the server when no protocol-specific discovery is configured. --------- Signed-off-by: Csaba Kiraly <csaba.kiraly@gmail.com> Co-authored-by: Felix Lange <fjl@twurst.com>
1 parent d675721 commit 4bb097b

File tree

3 files changed

+211
-20
lines changed

3 files changed

+211
-20
lines changed

eth/backend.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package eth
1919

2020
import (
21+
"context"
2122
"encoding/json"
2223
"fmt"
2324
"math/big"
@@ -62,6 +63,26 @@ import (
6263
gethversion "github.com/ethereum/go-ethereum/version"
6364
)
6465

66+
const (
67+
// This is the fairness knob for the discovery mixer. When looking for peers, we'll
68+
// wait this long for a single source of candidates before moving on and trying other
69+
// sources. If this timeout expires, the source will be skipped in this round, but it
70+
// will continue to fetch in the background and will have a chance with a new timeout
71+
// in the next rounds, giving it overall more time but a proportionally smaller share.
72+
// We expect a normal source to produce ~10 candidates per second.
73+
discmixTimeout = 100 * time.Millisecond
74+
75+
// discoveryPrefetchBuffer is the number of peers to pre-fetch from a discovery
76+
// source. It is useful to avoid the negative effects of potential longer timeouts
77+
// in the discovery, keeping dial progress while waiting for the next batch of
78+
// candidates.
79+
discoveryPrefetchBuffer = 32
80+
81+
// maxParallelENRRequests is the maximum number of parallel ENR requests that can be
82+
// performed by a disc/v4 source.
83+
maxParallelENRRequests = 16
84+
)
85+
6586
// Config contains the configuration options of the ETH protocol.
6687
// Deprecated: use ethconfig.Config instead.
6788
type Config = ethconfig.Config
@@ -176,7 +197,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
176197
networkID: networkID,
177198
gasPrice: config.Miner.GasPrice,
178199
p2pServer: stack.Server(),
179-
discmix: enode.NewFairMix(0),
200+
discmix: enode.NewFairMix(discmixTimeout),
180201
shutdownTracker: shutdowncheck.NewShutdownTracker(chainDb),
181202
}
182203
bcVersion := rawdb.ReadDatabaseVersion(chainDb)
@@ -494,10 +515,27 @@ func (s *Ethereum) setupDiscovery() error {
494515
s.discmix.AddSource(iter)
495516
}
496517

518+
// Add DHT nodes from discv4.
519+
if s.p2pServer.DiscoveryV4() != nil {
520+
iter := s.p2pServer.DiscoveryV4().RandomNodes()
521+
resolverFunc := func(ctx context.Context, enr *enode.Node) *enode.Node {
522+
// RequestENR does not yet support context. It will simply time out.
523+
// If the ENR can't be resolved, RequestENR will return nil. We don't
524+
// care about the specific error here, so we ignore it.
525+
nn, _ := s.p2pServer.DiscoveryV4().RequestENR(enr)
526+
return nn
527+
}
528+
iter = enode.AsyncFilter(iter, resolverFunc, maxParallelENRRequests)
529+
iter = enode.Filter(iter, eth.NewNodeFilter(s.blockchain))
530+
iter = enode.NewBufferIter(iter, discoveryPrefetchBuffer)
531+
s.discmix.AddSource(iter)
532+
}
533+
497534
// Add DHT nodes from discv5.
498535
if s.p2pServer.DiscoveryV5() != nil {
499536
filter := eth.NewNodeFilter(s.blockchain)
500537
iter := enode.Filter(s.p2pServer.DiscoveryV5().RandomNodes(), filter)
538+
iter = enode.NewBufferIter(iter, discoveryPrefetchBuffer)
501539
s.discmix.AddSource(iter)
502540
}
503541

p2p/enode/iter.go

Lines changed: 156 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package enode
1818

1919
import (
20+
"context"
2021
"sync"
2122
"time"
2223
)
@@ -59,6 +60,11 @@ func (it sourceIter) NodeSource() string {
5960
return it.name
6061
}
6162

63+
type iteratorItem struct {
64+
n *Node
65+
source string
66+
}
67+
6268
// ReadNodes reads at most n nodes from the given iterator. The return value contains no
6369
// duplicates and no nil values. To prevent looping indefinitely for small repeating node
6470
// sequences, this function calls Next at most n times.
@@ -152,6 +158,149 @@ func (f *filterIter) Next() bool {
152158
return false
153159
}
154160

161+
// asyncFilterIter wraps an iterator such that Next only returns nodes for which
162+
// the 'check' function returns a (possibly modified) node.
163+
type asyncFilterIter struct {
164+
it SourceIterator // the iterator to filter
165+
slots chan struct{} // the slots for parallel checking
166+
passed chan iteratorItem // channel to collect passed nodes
167+
cur iteratorItem // buffer to serve the Node call
168+
cancel context.CancelFunc
169+
closeOnce sync.Once
170+
}
171+
172+
type AsyncFilterFunc func(context.Context, *Node) *Node
173+
174+
// AsyncFilter creates an iterator which checks nodes in parallel.
175+
// The 'check' function is called on multiple goroutines to filter each node
176+
// from the upstream iterator. When check returns nil, the node will be skipped.
177+
// It can also return a new node to be returned by the iterator instead of the .
178+
func AsyncFilter(it Iterator, check AsyncFilterFunc, workers int) Iterator {
179+
f := &asyncFilterIter{
180+
it: ensureSourceIter(it),
181+
slots: make(chan struct{}, workers+1),
182+
passed: make(chan iteratorItem),
183+
}
184+
for range cap(f.slots) {
185+
f.slots <- struct{}{}
186+
}
187+
ctx, cancel := context.WithCancel(context.Background())
188+
f.cancel = cancel
189+
190+
go func() {
191+
select {
192+
case <-ctx.Done():
193+
return
194+
case <-f.slots:
195+
}
196+
// read from the iterator and start checking nodes in parallel
197+
// when a node is checked, it will be sent to the passed channel
198+
// and the slot will be released
199+
for f.it.Next() {
200+
node := f.it.Node()
201+
nodeSource := f.it.NodeSource()
202+
203+
// check the node async, in a separate goroutine
204+
<-f.slots
205+
go func() {
206+
if nn := check(ctx, node); nn != nil {
207+
item := iteratorItem{nn, nodeSource}
208+
select {
209+
case f.passed <- item:
210+
case <-ctx.Done(): // bale out if downstream is already closed and not calling Next
211+
}
212+
}
213+
f.slots <- struct{}{}
214+
}()
215+
}
216+
// the iterator has ended
217+
f.slots <- struct{}{}
218+
}()
219+
220+
return f
221+
}
222+
223+
// Next blocks until a node is available or the iterator is closed.
224+
func (f *asyncFilterIter) Next() bool {
225+
var ok bool
226+
f.cur, ok = <-f.passed
227+
return ok
228+
}
229+
230+
// Node returns the current node.
231+
func (f *asyncFilterIter) Node() *Node {
232+
return f.cur.n
233+
}
234+
235+
// NodeSource implements IteratorSource.
236+
func (f *asyncFilterIter) NodeSource() string {
237+
return f.cur.source
238+
}
239+
240+
// Close ends the iterator, also closing the wrapped iterator.
241+
func (f *asyncFilterIter) Close() {
242+
f.closeOnce.Do(func() {
243+
f.it.Close()
244+
f.cancel()
245+
for range cap(f.slots) {
246+
<-f.slots
247+
}
248+
close(f.slots)
249+
close(f.passed)
250+
})
251+
}
252+
253+
// bufferIter wraps an iterator and buffers the nodes it returns.
254+
// The buffer is pre-filled with the given size from the wrapped iterator.
255+
type bufferIter struct {
256+
it SourceIterator
257+
buffer chan iteratorItem
258+
head iteratorItem
259+
closeOnce sync.Once
260+
}
261+
262+
// NewBufferIter creates a new pre-fetch buffer of a given size.
263+
func NewBufferIter(it Iterator, size int) Iterator {
264+
b := bufferIter{
265+
it: ensureSourceIter(it),
266+
buffer: make(chan iteratorItem, size),
267+
}
268+
269+
go func() {
270+
// if the wrapped iterator ends, the buffer content will still be served.
271+
defer close(b.buffer)
272+
// If instead the bufferIterator is closed, we bail out of the loop.
273+
for b.it.Next() {
274+
item := iteratorItem{b.it.Node(), b.it.NodeSource()}
275+
b.buffer <- item
276+
}
277+
}()
278+
return &b
279+
}
280+
281+
func (b *bufferIter) Next() bool {
282+
var ok bool
283+
b.head, ok = <-b.buffer
284+
return ok
285+
}
286+
287+
func (b *bufferIter) Node() *Node {
288+
return b.head.n
289+
}
290+
291+
func (b *bufferIter) NodeSource() string {
292+
return b.head.source
293+
}
294+
295+
func (b *bufferIter) Close() {
296+
b.closeOnce.Do(func() {
297+
b.it.Close()
298+
// Drain buffer and wait for the goroutine to end.
299+
for range b.buffer {
300+
}
301+
})
302+
}
303+
155304
// FairMix aggregates multiple node iterators. The mixer itself is an iterator which ends
156305
// only when Close is called. Source iterators added via AddSource are removed from the
157306
// mix when they end.
@@ -164,9 +313,9 @@ func (f *filterIter) Next() bool {
164313
// It's safe to call AddSource and Close concurrently with Next.
165314
type FairMix struct {
166315
wg sync.WaitGroup
167-
fromAny chan mixItem
316+
fromAny chan iteratorItem
168317
timeout time.Duration
169-
cur mixItem
318+
cur iteratorItem
170319

171320
mu sync.Mutex
172321
closed chan struct{}
@@ -176,15 +325,10 @@ type FairMix struct {
176325

177326
type mixSource struct {
178327
it SourceIterator
179-
next chan mixItem
328+
next chan iteratorItem
180329
timeout time.Duration
181330
}
182331

183-
type mixItem struct {
184-
n *Node
185-
source string
186-
}
187-
188332
// NewFairMix creates a mixer.
189333
//
190334
// The timeout specifies how long the mixer will wait for the next fairly-chosen source
@@ -193,7 +337,7 @@ type mixItem struct {
193337
// timeout makes the mixer completely fair.
194338
func NewFairMix(timeout time.Duration) *FairMix {
195339
m := &FairMix{
196-
fromAny: make(chan mixItem),
340+
fromAny: make(chan iteratorItem),
197341
closed: make(chan struct{}),
198342
timeout: timeout,
199343
}
@@ -211,7 +355,7 @@ func (m *FairMix) AddSource(it Iterator) {
211355
m.wg.Add(1)
212356
source := &mixSource{
213357
it: ensureSourceIter(it),
214-
next: make(chan mixItem),
358+
next: make(chan iteratorItem),
215359
timeout: m.timeout,
216360
}
217361
m.sources = append(m.sources, source)
@@ -239,7 +383,7 @@ func (m *FairMix) Close() {
239383

240384
// Next returns a node from a random source.
241385
func (m *FairMix) Next() bool {
242-
m.cur = mixItem{}
386+
m.cur = iteratorItem{}
243387

244388
for {
245389
source := m.pickSource()
@@ -327,7 +471,7 @@ func (m *FairMix) runSource(closed chan struct{}, s *mixSource) {
327471
defer m.wg.Done()
328472
defer close(s.next)
329473
for s.it.Next() {
330-
item := mixItem{s.it.Node(), s.it.NodeSource()}
474+
item := iteratorItem{s.it.Node(), s.it.NodeSource()}
331475
select {
332476
case s.next <- item:
333477
case m.fromAny <- item:

p2p/server.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ import (
4545
const (
4646
defaultDialTimeout = 15 * time.Second
4747

48-
// This is the fairness knob for the discovery mixer. When looking for peers, we'll
49-
// wait this long for a single source of candidates before moving on and trying other
50-
// sources.
51-
discmixTimeout = 5 * time.Second
52-
5348
// Connectivity defaults.
5449
defaultMaxPendingPeers = 50
5550
defaultDialRatio = 3
@@ -447,7 +442,9 @@ func (srv *Server) setupLocalNode() error {
447442
}
448443

449444
func (srv *Server) setupDiscovery() error {
450-
srv.discmix = enode.NewFairMix(discmixTimeout)
445+
// Set up the discovery source mixer. Here, we don't care about the
446+
// fairness of the mix, it's just for putting the
447+
srv.discmix = enode.NewFairMix(0)
451448

452449
// Don't listen on UDP endpoint if DHT is disabled.
453450
if srv.NoDiscovery {
@@ -483,7 +480,6 @@ func (srv *Server) setupDiscovery() error {
483480
return err
484481
}
485482
srv.discv4 = ntab
486-
srv.discmix.AddSource(ntab.RandomNodes())
487483
}
488484
if srv.Config.DiscoveryV5 {
489485
cfg := discover.Config{
@@ -506,6 +502,19 @@ func (srv *Server) setupDiscovery() error {
506502
added[proto.Name] = true
507503
}
508504
}
505+
506+
// Set up default non-protocol-specific discovery feeds if no protocol
507+
// has configured discovery.
508+
if len(added) == 0 {
509+
if srv.discv4 != nil {
510+
it := srv.discv4.RandomNodes()
511+
srv.discmix.AddSource(enode.WithSourceName("discv4-default", it))
512+
}
513+
if srv.discv5 != nil {
514+
it := srv.discv5.RandomNodes()
515+
srv.discmix.AddSource(enode.WithSourceName("discv5-default", it))
516+
}
517+
}
509518
return nil
510519
}
511520

0 commit comments

Comments
 (0)