Browse Source

bound mempool memory usage (#3248)

* bound mempool memory usage

Closes #3079

* rename SizeBytes to TxsTotalBytes

and other small fixes after Zarko's review

* rename MaxBytes to MaxTxsTotalBytes

* make ErrMempoolIsFull more informative

* expose mempool's txs_total_bytes via RPC

* test full response

* fixes after Ethan's review

* config: rename mempool.size to mempool.max_txs

https://github.com/tendermint/tendermint/pull/3248#discussion_r254034004

* test more cases

https://github.com/tendermint/tendermint/pull/3248#discussion_r254036532

* simplify test

* Revert "config: rename mempool.size to mempool.max_txs"

This reverts commit 39bfa36961.

* rename count back to n_txs

to make a change non-breaking

* rename max_txs_total_bytes to max_txs_bytes

* format code

* fix TestWALPeriodicSync

The test was sometimes failing due to processFlushTicks being called too
early. The solution is to call wal#Start later in the test.

* Apply suggestions from code review
pull/3348/head
Anton Kaliaev 6 years ago
committed by Ethan Buchman
parent
commit
41f91318e9
9 changed files with 180 additions and 44 deletions
  1. +3
    -0
      CHANGELOG_PENDING.md
  2. +14
    -9
      config/config.go
  3. +7
    -2
      config/toml.go
  4. +7
    -2
      docs/tendermint-core/configuration.md
  5. +40
    -7
      mempool/mempool.go
  6. +69
    -1
      mempool/mempool_test.go
  7. +9
    -3
      rpc/client/rpc_test.go
  8. +27
    -18
      rpc/core/mempool.go
  9. +4
    -2
      rpc/core/types/responses.go

+ 3
- 0
CHANGELOG_PENDING.md View File

@ -18,6 +18,9 @@ Special thanks to external contributors on this release:
* P2P Protocol * P2P Protocol
### FEATURES: ### FEATURES:
- [mempool] \#3079 bound mempool memory usage (`mempool.max_txs_bytes` is set to 1GB by default; see config.toml)
mempool's current `txs_total_bytes` is exposed via `total_bytes` field in
`/num_unconfirmed_txs` and `/unconfirmed_txs` RPC endpoints.
### IMPROVEMENTS: ### IMPROVEMENTS:


+ 14
- 9
config/config.go View File

@ -530,12 +530,13 @@ func DefaultFuzzConnConfig() *FuzzConnConfig {
// MempoolConfig defines the configuration options for the Tendermint mempool // MempoolConfig defines the configuration options for the Tendermint mempool
type MempoolConfig struct { type MempoolConfig struct {
RootDir string `mapstructure:"home"`
Recheck bool `mapstructure:"recheck"`
Broadcast bool `mapstructure:"broadcast"`
WalPath string `mapstructure:"wal_dir"`
Size int `mapstructure:"size"`
CacheSize int `mapstructure:"cache_size"`
RootDir string `mapstructure:"home"`
Recheck bool `mapstructure:"recheck"`
Broadcast bool `mapstructure:"broadcast"`
WalPath string `mapstructure:"wal_dir"`
Size int `mapstructure:"size"`
MaxTxsBytes int64 `mapstructure:"max_txs_bytes"`
CacheSize int `mapstructure:"cache_size"`
} }
// DefaultMempoolConfig returns a default configuration for the Tendermint mempool // DefaultMempoolConfig returns a default configuration for the Tendermint mempool
@ -544,10 +545,11 @@ func DefaultMempoolConfig() *MempoolConfig {
Recheck: true, Recheck: true,
Broadcast: true, Broadcast: true,
WalPath: "", WalPath: "",
// Each signature verification takes .5ms, size reduced until we implement
// Each signature verification takes .5ms, Size reduced until we implement
// ABCI Recheck // ABCI Recheck
Size: 5000,
CacheSize: 10000,
Size: 5000,
MaxTxsBytes: 1024 * 1024 * 1024, // 1GB
CacheSize: 10000,
} }
} }
@ -574,6 +576,9 @@ func (cfg *MempoolConfig) ValidateBasic() error {
if cfg.Size < 0 { if cfg.Size < 0 {
return errors.New("size can't be negative") return errors.New("size can't be negative")
} }
if cfg.MaxTxsBytes < 0 {
return errors.New("max_txs_bytes can't be negative")
}
if cfg.CacheSize < 0 { if cfg.CacheSize < 0 {
return errors.New("cache_size can't be negative") return errors.New("cache_size can't be negative")
} }


+ 7
- 2
config/toml.go View File

@ -237,10 +237,15 @@ recheck = {{ .Mempool.Recheck }}
broadcast = {{ .Mempool.Broadcast }} broadcast = {{ .Mempool.Broadcast }}
wal_dir = "{{ js .Mempool.WalPath }}" wal_dir = "{{ js .Mempool.WalPath }}"
# size of the mempool
# Maximum number of transactions in the mempool
size = {{ .Mempool.Size }} size = {{ .Mempool.Size }}
# size of the cache (used to filter transactions we saw earlier)
# Limit the total size of all txs in the mempool.
# This only accounts for raw transactions (e.g. given 1MB transactions and
# max_txs_bytes=5MB, mempool will only accept 5 transactions).
max_txs_bytes = {{ .Mempool.MaxTxsBytes }}
# Size of the cache (used to filter transactions we saw earlier) in transactions
cache_size = {{ .Mempool.CacheSize }} cache_size = {{ .Mempool.CacheSize }}
##### consensus configuration options ##### ##### consensus configuration options #####


+ 7
- 2
docs/tendermint-core/configuration.md View File

@ -183,10 +183,15 @@ recheck = true
broadcast = true broadcast = true
wal_dir = "" wal_dir = ""
# size of the mempool
# Maximum number of transactions in the mempool
size = 5000 size = 5000
# size of the cache (used to filter transactions we saw earlier)
# Limit the total size of all txs in the mempool.
# This only accounts for raw transactions (e.g. given 1MB transactions and
# max_txs_bytes=5MB, mempool will only accept 5 transactions).
max_txs_bytes = 1073741824
# Size of the cache (used to filter transactions we saw earlier) in transactions
cache_size = 10000 cache_size = 10000
##### consensus configuration options ##### ##### consensus configuration options #####


+ 40
- 7
mempool/mempool.go View File

@ -63,13 +63,26 @@ var (
// ErrTxInCache is returned to the client if we saw tx earlier // ErrTxInCache is returned to the client if we saw tx earlier
ErrTxInCache = errors.New("Tx already exists in cache") ErrTxInCache = errors.New("Tx already exists in cache")
// ErrMempoolIsFull means Tendermint & an application can't handle that much load
ErrMempoolIsFull = errors.New("Mempool is full")
// ErrTxTooLarge means the tx is too big to be sent in a message to other peers // ErrTxTooLarge means the tx is too big to be sent in a message to other peers
ErrTxTooLarge = fmt.Errorf("Tx too large. Max size is %d", maxTxSize) ErrTxTooLarge = fmt.Errorf("Tx too large. Max size is %d", maxTxSize)
) )
// ErrMempoolIsFull means Tendermint & an application can't handle that much load
type ErrMempoolIsFull struct {
numTxs int
maxTxs int
txsBytes int64
maxTxsBytes int64
}
func (e ErrMempoolIsFull) Error() string {
return fmt.Sprintf(
"Mempool is full: number of txs %d (max: %d), total txs bytes %d (max: %d)",
e.numTxs, e.maxTxs,
e.txsBytes, e.maxTxsBytes)
}
// ErrPreCheck is returned when tx is too big // ErrPreCheck is returned when tx is too big
type ErrPreCheck struct { type ErrPreCheck struct {
Reason error Reason error
@ -147,6 +160,9 @@ type Mempool struct {
preCheck PreCheckFunc preCheck PreCheckFunc
postCheck PostCheckFunc postCheck PostCheckFunc
// Atomic integers
txsBytes int64 // see TxsBytes
// Keep a cache of already-seen txs. // Keep a cache of already-seen txs.
// This reduces the pressure on the proxyApp. // This reduces the pressure on the proxyApp.
cache txCache cache txCache
@ -265,8 +281,13 @@ func (mem *Mempool) Size() int {
return mem.txs.Len() return mem.txs.Len()
} }
// Flushes the mempool connection to ensure async resCb calls are done e.g.
// from CheckTx.
// TxsBytes returns the total size of all txs in the mempool.
func (mem *Mempool) TxsBytes() int64 {
return atomic.LoadInt64(&mem.txsBytes)
}
// FlushAppConn flushes the mempool connection to ensure async resCb calls are
// done e.g. from CheckTx.
func (mem *Mempool) FlushAppConn() error { func (mem *Mempool) FlushAppConn() error {
return mem.proxyAppConn.FlushSync() return mem.proxyAppConn.FlushSync()
} }
@ -282,6 +303,8 @@ func (mem *Mempool) Flush() {
mem.txs.Remove(e) mem.txs.Remove(e)
e.DetachPrev() e.DetachPrev()
} }
_ = atomic.SwapInt64(&mem.txsBytes, 0)
} }
// TxsFront returns the first transaction in the ordered list for peer // TxsFront returns the first transaction in the ordered list for peer
@ -308,8 +331,15 @@ func (mem *Mempool) CheckTx(tx types.Tx, cb func(*abci.Response)) (err error) {
// use defer to unlock mutex because application (*local client*) might panic // use defer to unlock mutex because application (*local client*) might panic
defer mem.proxyMtx.Unlock() defer mem.proxyMtx.Unlock()
if mem.Size() >= mem.config.Size {
return ErrMempoolIsFull
var (
memSize = mem.Size()
txsBytes = mem.TxsBytes()
)
if memSize >= mem.config.Size ||
int64(len(tx))+txsBytes > mem.config.MaxTxsBytes {
return ErrMempoolIsFull{
memSize, mem.config.Size,
txsBytes, mem.config.MaxTxsBytes}
} }
// The size of the corresponding amino-encoded TxMessage // The size of the corresponding amino-encoded TxMessage
@ -383,6 +413,7 @@ func (mem *Mempool) resCbNormal(req *abci.Request, res *abci.Response) {
tx: tx, tx: tx,
} }
mem.txs.PushBack(memTx) mem.txs.PushBack(memTx)
atomic.AddInt64(&mem.txsBytes, int64(len(tx)))
mem.logger.Info("Added good transaction", mem.logger.Info("Added good transaction",
"tx", TxID(tx), "tx", TxID(tx),
"res", r, "res", r,
@ -424,6 +455,7 @@ func (mem *Mempool) resCbRecheck(req *abci.Request, res *abci.Response) {
// Tx became invalidated due to newly committed block. // Tx became invalidated due to newly committed block.
mem.logger.Info("Tx is no longer valid", "tx", TxID(tx), "res", r, "err", postCheckErr) mem.logger.Info("Tx is no longer valid", "tx", TxID(tx), "res", r, "err", postCheckErr)
mem.txs.Remove(mem.recheckCursor) mem.txs.Remove(mem.recheckCursor)
atomic.AddInt64(&mem.txsBytes, int64(-len(tx)))
mem.recheckCursor.DetachPrev() mem.recheckCursor.DetachPrev()
// remove from cache (it might be good later) // remove from cache (it might be good later)
@ -597,6 +629,7 @@ func (mem *Mempool) removeTxs(txs types.Txs) []types.Tx {
if _, ok := txsMap[string(memTx.tx)]; ok { if _, ok := txsMap[string(memTx.tx)]; ok {
// remove from clist // remove from clist
mem.txs.Remove(e) mem.txs.Remove(e)
atomic.AddInt64(&mem.txsBytes, int64(-len(memTx.tx)))
e.DetachPrev() e.DetachPrev()
// NOTE: we don't remove committed txs from the cache. // NOTE: we don't remove committed txs from the cache.


+ 69
- 1
mempool/mempool_test.go View File

@ -30,8 +30,10 @@ import (
type cleanupFunc func() type cleanupFunc func()
func newMempoolWithApp(cc proxy.ClientCreator) (*Mempool, cleanupFunc) { func newMempoolWithApp(cc proxy.ClientCreator) (*Mempool, cleanupFunc) {
config := cfg.ResetTestRoot("mempool_test")
return newMempoolWithAppAndConfig(cc, cfg.ResetTestRoot("mempool_test"))
}
func newMempoolWithAppAndConfig(cc proxy.ClientCreator, config *cfg.Config) (*Mempool, cleanupFunc) {
appConnMem, _ := cc.NewABCIClient() appConnMem, _ := cc.NewABCIClient()
appConnMem.SetLogger(log.TestingLogger().With("module", "abci-client", "connection", "mempool")) appConnMem.SetLogger(log.TestingLogger().With("module", "abci-client", "connection", "mempool"))
err := appConnMem.Start() err := appConnMem.Start()
@ -462,6 +464,72 @@ func TestMempoolMaxMsgSize(t *testing.T) {
} }
func TestMempoolTxsBytes(t *testing.T) {
app := kvstore.NewKVStoreApplication()
cc := proxy.NewLocalClientCreator(app)
config := cfg.ResetTestRoot("mempool_test")
config.Mempool.MaxTxsBytes = 10
mempool, cleanup := newMempoolWithAppAndConfig(cc, config)
defer cleanup()
// 1. zero by default
assert.EqualValues(t, 0, mempool.TxsBytes())
// 2. len(tx) after CheckTx
err := mempool.CheckTx([]byte{0x01}, nil)
require.NoError(t, err)
assert.EqualValues(t, 1, mempool.TxsBytes())
// 3. zero again after tx is removed by Update
mempool.Update(1, []types.Tx{[]byte{0x01}}, nil, nil)
assert.EqualValues(t, 0, mempool.TxsBytes())
// 4. zero after Flush
err = mempool.CheckTx([]byte{0x02, 0x03}, nil)
require.NoError(t, err)
assert.EqualValues(t, 2, mempool.TxsBytes())
mempool.Flush()
assert.EqualValues(t, 0, mempool.TxsBytes())
// 5. ErrMempoolIsFull is returned when/if MaxTxsBytes limit is reached.
err = mempool.CheckTx([]byte{0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}, nil)
require.NoError(t, err)
err = mempool.CheckTx([]byte{0x05}, nil)
if assert.Error(t, err) {
assert.IsType(t, ErrMempoolIsFull{}, err)
}
// 6. zero after tx is rechecked and removed due to not being valid anymore
app2 := counter.NewCounterApplication(true)
cc = proxy.NewLocalClientCreator(app2)
mempool, cleanup = newMempoolWithApp(cc)
defer cleanup()
txBytes := make([]byte, 8)
binary.BigEndian.PutUint64(txBytes, uint64(0))
err = mempool.CheckTx(txBytes, nil)
require.NoError(t, err)
assert.EqualValues(t, 8, mempool.TxsBytes())
appConnCon, _ := cc.NewABCIClient()
appConnCon.SetLogger(log.TestingLogger().With("module", "abci-client", "connection", "consensus"))
err = appConnCon.Start()
require.Nil(t, err)
defer appConnCon.Stop()
res, err := appConnCon.DeliverTxSync(txBytes)
require.NoError(t, err)
require.EqualValues(t, 0, res.Code)
res2, err := appConnCon.CommitSync()
require.NoError(t, err)
require.NotEmpty(t, res2.Data)
// Pretend like we committed nothing so txBytes gets rechecked and removed.
mempool.Update(1, []types.Tx{}, nil, nil)
assert.EqualValues(t, 0, mempool.TxsBytes())
}
func checksumIt(data []byte) string { func checksumIt(data []byte) string {
h := sha256.New() h := sha256.New()
h.Write(data) h.Write(data)


+ 9
- 3
rpc/client/rpc_test.go View File

@ -290,9 +290,13 @@ func TestUnconfirmedTxs(t *testing.T) {
for i, c := range GetClients() { for i, c := range GetClients() {
mc, ok := c.(client.MempoolClient) mc, ok := c.(client.MempoolClient)
require.True(t, ok, "%d", i) require.True(t, ok, "%d", i)
txs, err := mc.UnconfirmedTxs(1)
res, err := mc.UnconfirmedTxs(1)
require.Nil(t, err, "%d: %+v", i, err) require.Nil(t, err, "%d: %+v", i, err)
assert.Exactly(t, types.Txs{tx}, types.Txs(txs.Txs))
assert.Equal(t, 1, res.Count)
assert.Equal(t, 1, res.Total)
assert.Equal(t, mempool.TxsBytes(), res.TotalBytes)
assert.Exactly(t, types.Txs{tx}, types.Txs(res.Txs))
} }
mempool.Flush() mempool.Flush()
@ -311,7 +315,9 @@ func TestNumUnconfirmedTxs(t *testing.T) {
res, err := mc.NumUnconfirmedTxs() res, err := mc.NumUnconfirmedTxs()
require.Nil(t, err, "%d: %+v", i, err) require.Nil(t, err, "%d: %+v", i, err)
assert.Equal(t, mempoolSize, res.N)
assert.Equal(t, mempoolSize, res.Count)
assert.Equal(t, mempoolSize, res.Total)
assert.Equal(t, mempool.TxsBytes(), res.TotalBytes)
} }
mempool.Flush() mempool.Flush()


+ 27
- 18
rpc/core/mempool.go View File

@ -248,27 +248,32 @@ func BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) {
// //
// ```json // ```json
// { // {
// "error": "",
// "result": {
// "txs": [],
// "n_txs": "0"
// },
// "id": "",
// "jsonrpc": "2.0"
// }
// "result" : {
// "txs" : [],
// "total_bytes" : "0",
// "n_txs" : "0",
// "total" : "0"
// },
// "jsonrpc" : "2.0",
// "id" : ""
// }
// ```
// //
// ### Query Parameters // ### Query Parameters
// //
// | Parameter | Type | Default | Required | Description | // | Parameter | Type | Default | Required | Description |
// |-----------+------+---------+----------+--------------------------------------| // |-----------+------+---------+----------+--------------------------------------|
// | limit | int | 30 | false | Maximum number of entries (max: 100) | // | limit | int | 30 | false | Maximum number of entries (max: 100) |
// ```
func UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) { func UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) {
// reuse per_page validator // reuse per_page validator
limit = validatePerPage(limit) limit = validatePerPage(limit)
txs := mempool.ReapMaxTxs(limit) txs := mempool.ReapMaxTxs(limit)
return &ctypes.ResultUnconfirmedTxs{N: len(txs), Txs: txs}, nil
return &ctypes.ResultUnconfirmedTxs{
Count: len(txs),
Total: mempool.Size(),
TotalBytes: mempool.TxsBytes(),
Txs: txs}, nil
} }
// Get number of unconfirmed transactions. // Get number of unconfirmed transactions.
@ -291,15 +296,19 @@ func UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) {
// //
// ```json // ```json
// { // {
// "error": "",
// "result": {
// "txs": null,
// "n_txs": "0"
// },
// "id": "",
// "jsonrpc": "2.0"
// "jsonrpc" : "2.0",
// "id" : "",
// "result" : {
// "n_txs" : "0",
// "total_bytes" : "0",
// "txs" : null,
// "total" : "0"
// }
// } // }
// ``` // ```
func NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) { func NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) {
return &ctypes.ResultUnconfirmedTxs{N: mempool.Size()}, nil
return &ctypes.ResultUnconfirmedTxs{
Count: mempool.Size(),
Total: mempool.Size(),
TotalBytes: mempool.TxsBytes()}, nil
} }

+ 4
- 2
rpc/core/types/responses.go View File

@ -178,8 +178,10 @@ type ResultTxSearch struct {
// List of mempool txs // List of mempool txs
type ResultUnconfirmedTxs struct { type ResultUnconfirmedTxs struct {
N int `json:"n_txs"`
Txs []types.Tx `json:"txs"`
Count int `json:"n_txs"`
Total int `json:"total"`
TotalBytes int64 `json:"total_bytes"`
Txs []types.Tx `json:"txs"`
} }
// Info abci msg // Info abci msg


Loading…
Cancel
Save