tendermint, PEX

PEX handles peer exchange, finding more peers and building the p2p network.

PEX is a reactor working together with Switch.

Reactor

type Reactor struct {
	p2p.BaseReactor

	book              AddrBook
	config            *ReactorConfig
	ensurePeersPeriod time.Duration // TODO: should go in the config

	// maps to prevent abuse
	requestsSent         *cmap.CMap // ID->struct{}: unanswered send requests
	lastReceivedRequests *cmap.CMap // ID->time.Time: last time peer requested from us

	seedAddrs []*p2p.NetAddress

	attemptsToDial sync.Map // address (string) -> {number of attempts (int), last time dialed (time.Time)}

	// seed/crawled mode fields
	crawlPeerInfos map[p2p.ID]crawlPeerInfo
}

// NewReactor creates new PEX reactor.
func NewReactor(b AddrBook, config *ReactorConfig) *Reactor {
	r := &Reactor{
		book:                 b,
		config:               config,
		ensurePeersPeriod:    defaultEnsurePeersPeriod,
		requestsSent:         cmap.NewCMap(),
		lastReceivedRequests: cmap.NewCMap(),
		crawlPeerInfos:       make(map[p2p.ID]crawlPeerInfo),
	}
	r.BaseReactor = *p2p.NewBaseReactor("Reactor", r)
	return r
}

Config

// ReactorConfig holds reactor specific configuration data.
type ReactorConfig struct {
	// Seed/Crawler mode
	SeedMode bool

	// We want seeds to only advertise good peers. Therefore they should wait at
	// least as long as we expect it to take for a peer to become good before
	// disconnecting.
	SeedDisconnectWaitPeriod time.Duration

	// Maximum pause when redialing a persistent peer (if zero, exponential backoff is used)
	PersistentPeersMaxDialPeriod time.Duration

	// Seeds is a list of addresses reactor may use
	// if it can't connect to peers in the addrbook.
	Seeds []string
}

Channels & Messages

	// PexChannel is a channel for PEX messages
	PexChannel = byte(0x00)	
/*
A pexRequestMessage requests additional peer addresses.
*/
type pexRequestMessage struct {
}
/*
A message with announced peer addresses.
*/
type pexAddrsMessage struct {
	Addrs []*p2p.NetAddress
}

Start

If working on seed mode, PEX starts crawlPeersRoutine(), or ensurePeersRoutine().

// OnStart implements BaseService
func (r *Reactor) OnStart() error {
	err := r.book.Start()
	if err != nil && err != service.ErrAlreadyStarted {
		return err
	}

	numOnline, seedAddrs, err := r.checkSeeds()
	if err != nil {
		return err
	} else if numOnline == 0 && r.book.Empty() {
		return errors.New("address book is empty and couldn't resolve any seed nodes")
	}

	r.seedAddrs = seedAddrs

	// Check if this node should run
	// in seed/crawler mode
	if r.config.SeedMode {
		go r.crawlPeersRoutine()
	} else {
		go r.ensurePeersRoutine()
	}
	return nil
}

crawlPeersRoutine()

It is the Seed Mode.


// Explores the network searching for more peers. (continuous)
// Seed/Crawler Mode causes this node to quickly disconnect
// from peers, except other seed nodes.
func (r *Reactor) crawlPeersRoutine() {
	// If we have any seed nodes, consult them first
	if len(r.seedAddrs) > 0 {
		r.dialSeeds()
	} else {
		// Do an initial crawl
		r.crawlPeers(r.book.GetSelection())
	}

	// Fire periodically
	ticker := time.NewTicker(crawlPeerPeriod)

	for {
		select {
		case <-ticker.C:
			r.attemptDisconnects()
			r.crawlPeers(r.book.GetSelection())
			r.cleanupCrawlPeerInfos()
		case <-r.Quit():
			return
		}
	}
}

ensurePeersRoutine()

It picks up some addresses from AddrBook then dials them. Besides, it will pick a random peer from Switch peer list and then request for more peer addresses. At last, if it is not dialing any peer, it trys dialing the seed node.

// Ensures that sufficient peers are connected. (continuous)
func (r *Reactor) ensurePeersRoutine() {
	var (
		seed   = rand.NewRand()
		jitter = seed.Int63n(r.ensurePeersPeriod.Nanoseconds())
	)

	// Randomize first round of communication to avoid thundering herd.
	// If no peers are present directly start connecting so we guarantee swift
	// setup with the help of configured seeds.
	if r.nodeHasSomePeersOrDialingAny() {
		time.Sleep(time.Duration(jitter))
	}

	// fire once immediately.
	// ensures we dial the seeds right away if the book is empty
	r.ensurePeers()

	// fire periodically
	ticker := time.NewTicker(r.ensurePeersPeriod)
	for {
		select {
		case <-ticker.C:
			r.ensurePeers()
		case <-r.Quit():
			ticker.Stop()
			return
		}
	}
}

AddPeer & RemovePeer

PEX would requests more peers if need when a new outbound peer is added to Switch. As for an inbound peer, PEX adds it and probably asks it for more peer if it is ensured to be trustworthy.


// AddPeer implements Reactor by adding peer to the address book (if inbound)
// or by requesting more addresses (if outbound).
func (r *Reactor) AddPeer(p Peer) {
	if p.IsOutbound() {
		// For outbound peers, the address is already in the books -
		// either via DialPeersAsync or r.Receive.
		// Ask it for more peers if we need.
		if r.book.NeedMoreAddrs() {
			r.RequestAddrs(p)
		}
	} else {
		// inbound peer is its own source
		addr, err := p.NodeInfo().NetAddress()
		if err != nil {
			r.Logger.Error("Failed to get peer NetAddress", "err", err, "peer", p)
			return
		}

		// Make it explicit that addr and src are the same for an inbound peer.
		src := addr

		// add to book. dont RequestAddrs right away because
		// we don't trust inbound as much - let ensurePeersRoutine handle it.
		err = r.book.AddAddress(addr, src)
		r.logErrAddrBook(err)
	}
}

// RemovePeer implements Reactor by resetting peer's requests info.
func (r *Reactor) RemovePeer(p Peer, reason interface{}) {
	id := string(p.ID())
	r.requestsSent.Delete(id)
	r.lastReceivedRequests.Delete(id)
}

Receive

It handles pexRequestMessage and pexAddrsMessage.

pexRequestMessage

A Seed node doesn’t take too many connections with peers.

If we are running at Seed Mode and the message comes from an inbound peer, we select some addresses from AddrBook and send back with pexAddrsMessage. After all, we disconnect the peer.

Otherwise, we enforce a minimum amount of time between requests, then send back some random addresses wiht pexAddrsMessage.

pexAddrsMessage

We simly check if there is an open request, and delete it if so, then add peer address to AddrBook. At the same time, if any address matches with the known seed node, we start a connection towards the seed node immediately. This is to find out more peers as soon as possible.

dialPeer

A helper function to dial the address. It has some strategy to make dialing and mark the address if dialing failed.


func (r *Reactor) dialPeer(addr *p2p.NetAddress) error {
	attempts, lastDialed := r.dialAttemptsInfo(addr)
	if !r.Switch.IsPeerPersistent(addr) && attempts > maxAttemptsToDial {
		// TODO(melekes): have a blacklist in the addrbook with peers whom we've
		// failed to connect to. Then we can clean up attemptsToDial, which acts as
		// a blacklist currently.
		// https://github.com/tendermint/tendermint/issues/3572
		r.book.MarkBad(addr)
		return errMaxAttemptsToDial{}
	}

	// exponential backoff if it's not our first attempt to dial given address
	if attempts > 0 {
		jitterSeconds := time.Duration(tmrand.Float64() * float64(time.Second)) // 1s == (1e9 ns)
		backoffDuration := jitterSeconds + ((1 << uint(attempts)) * time.Second)
		backoffDuration = r.maxBackoffDurationForPeer(addr, backoffDuration)
		sinceLastDialed := time.Since(lastDialed)
		if sinceLastDialed < backoffDuration {
			return errTooEarlyToDial{backoffDuration, lastDialed}
		}
	}

	err := r.Switch.DialPeerWithAddress(addr)
	if err != nil {
		if _, ok := err.(p2p.ErrCurrentlyDialingOrExistingAddress); ok {
			return err
		}

		markAddrInBookBasedOnErr(addr, r.book, err)
		switch err.(type) {
		case p2p.ErrSwitchAuthenticationFailure:
			// NOTE: addr is removed from addrbook in markAddrInBookBasedOnErr
			r.attemptsToDial.Delete(addr.DialString())
		default:
			r.attemptsToDial.Store(addr.DialString(), _attemptsToDial{attempts + 1, time.Now()})
		}
		return errors.Wrapf(err, "dialing failed (attempts: %d)", attempts+1)
	}

	// cleanup any history
	r.attemptsToDial.Delete(addr.DialString())
	return nil
}

AddrBook

PEX manages peers via AddrBook interface. AddrBook is also a Service which follows the lifecycle of a service model.

It maintains the ‘new’ and ‘old’ address bucket, holding all addresses.


// AddrBook is an address book used for tracking peers
// so we can gossip about them to others and select
// peers to dial.
// TODO: break this up?
type AddrBook interface {
	service.Service

	// Add our own addresses so we don't later add ourselves
	AddOurAddress(*p2p.NetAddress)
	// Check if it is our address
	OurAddress(*p2p.NetAddress) bool

	AddPrivateIDs([]string)

	// Add and remove an address
	AddAddress(addr *p2p.NetAddress, src *p2p.NetAddress) error
	RemoveAddress(*p2p.NetAddress)

	// Check if the address is in the book
	HasAddress(*p2p.NetAddress) bool

	// Do we need more peers?
	NeedMoreAddrs() bool
	// Is Address Book Empty? Answer should not depend on being in your own
	// address book, or private peers
	Empty() bool

	// Pick an address to dial
	PickAddress(biasTowardsNewAddrs int) *p2p.NetAddress

	// Mark address
	MarkGood(p2p.ID)
	MarkAttempt(*p2p.NetAddress)
	MarkBad(*p2p.NetAddress)

	IsGood(*p2p.NetAddress) bool

	// Send a selection of addresses to peers
	GetSelection() []*p2p.NetAddress
	// Send a selection of addresses with bias
	GetSelectionWithBias(biasTowardsNewAddrs int) []*p2p.NetAddress

	Size() int

	// Persist to disk
	Save()
}

// addrBook - concurrency safe peer address manager.
// Implements AddrBook.
type addrBook struct {
	service.BaseService

	// accessed concurrently
	mtx        sync.Mutex
	rand       *tmrand.Rand
	ourAddrs   map[string]struct{}
	privateIDs map[p2p.ID]struct{}
	addrLookup map[p2p.ID]*knownAddress // new & old
	bucketsOld []map[string]*knownAddress
	bucketsNew []map[string]*knownAddress
	nOld       int
	nNew       int

	// immutable after creation
	filePath          string
	key               string // random prefix for bucket placement
	routabilityStrict bool

	wg sync.WaitGroup
}

Initialization

AddrBook is created in NewNode(), set to Switch, and then passed to PEX.

// NewAddrBook creates a new address book.
// Use Start to begin processing asynchronous address updates.
func NewAddrBook(filePath string, routabilityStrict bool) *addrBook {
	am := &addrBook{
		rand:              tmrand.NewRand(),
		ourAddrs:          make(map[string]struct{}),
		privateIDs:        make(map[p2p.ID]struct{}),
		addrLookup:        make(map[p2p.ID]*knownAddress),
		filePath:          filePath,
		routabilityStrict: routabilityStrict,
	}
	am.init()
	am.BaseService = *service.NewBaseService(nil, "AddrBook", am)
	return am
}

Start

Started by PEX start(), it will load address from file addrbook.json, then start a go routine to update the file in a fixed interval - 2m by default.

// OnStart implements Service.
func (a *addrBook) OnStart() error {
	if err := a.BaseService.OnStart(); err != nil {
		return err
	}
	a.loadFromFile(a.filePath)

	// wg.Add to ensure that any invocation of .Wait()
	// later on will wait for saveRoutine to terminate.
	a.wg.Add(1)
	go a.saveRoutine()

	return nil
}
github.com/tendermint/tendermint/p2p/pex.(*addrBook).OnStart at addrbook.go:140
github.com/tendermint/tendermint/libs/service.(*BaseService).Start at service.go:139
github.com/tendermint/tendermint/p2p/pex.(*Reactor).OnStart at pex_reactor.go:146
github.com/tendermint/tendermint/libs/service.(*BaseService).Start at service.go:139
github.com/tendermint/tendermint/p2p.(*Switch).OnStart at switch.go:227
github.com/tendermint/tendermint/libs/service.(*BaseService).Start at service.go:139
github.com/tendermint/tendermint/node.(*Node).OnStart at node.go:791
github.com/tendermint/tendermint/libs/service.(*BaseService).Start at service.go:139
github.com/hyperledger/burrow/core.TendermintLauncher.func1 at processes.go:165
github.com/hyperledger/burrow/core.(*Kernel).Boot at kernel.go:259
github.com/hyperledger/burrow/cmd/burrow/commands.Start.func1.1 at start.go:31
github.com/jawher/mow.cli/internal/flow.(*Step).callDo at flow.go:55
github.com/jawher/mow.cli/internal/flow.(*Step).Run at flow.go:25
github.com/jawher/mow.cli/internal/flow.(*Step).Run at flow.go:29
github.com/jawher/mow.cli/internal/flow.(*Step).Run at flow.go:29
github.com/jawher/mow.cli/internal/flow.(*Step).Run at flow.go:29
github.com/jawher/mow%2ecli.(*Cmd).parse at commands.go:681
github.com/jawher/mow%2ecli.(*Cmd).parse at commands.go:695
github.com/jawher/mow%2ecli.(*Cli).parse at cli.go:76
github.com/jawher/mow%2ecli.(*Cli).Run at cli.go:105
main.main at main.go:15
runtime.main at proc.go:203
runtime.goexit at asm_amd64.s:1357
 - Async stack trace
runtime.rt0_go at asm_amd64.s:220
func (a *addrBook) saveRoutine() {
	defer a.wg.Done()

	saveFileTicker := time.NewTicker(dumpAddressInterval)
out:
	for {
		select {
		case <-saveFileTicker.C:
			a.saveToFile(a.filePath)
		case <-a.Quit():
			break out
		}
	}
	saveFileTicker.Stop()
	a.saveToFile(a.filePath)
}

Basic Operations

  • AddAddress, add address to a “new” bucket
  • RemoveAddress, removes the address from the book
  • HasAddress
  • NeedMoreAddrs, number of known addresses < needAddressThreshold (1000)
  • PickAddress, picks an address to connect to. The address is picked randomly from an old or new bucket according to the biasTowardsNewAddrs argument
  • MarkGood, marks the peer as good and/ moves it into an “old” bucket
  • MarkAttempt, marks that an attempt was made to connect to the address
  • MarkBad, currently remove the address
  • GetSelection, select some addresses randomly
  • GetSelectionWithBias, select some addresses randomly according to the biasTowardsNewAddrs argument
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章