mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-03 09:56:38 -05:00
Merge bitcoin/bitcoin#26308: rpc/rest/zmq: reduce LOCK(cs_main) scope: ~6 times as many requests per second
d7f61e7d59
rpc: reduce LOCK(cs_main) scope in gettxoutproof (Andrew Toth)4d92b5aaba
rpc: reduce LOCK(cs_main) scope in GetUndoChecked and getblockstats (Andrew Toth)efd82aec8a
rpc: reduce LOCK(cs_main) scope in blockToJSON (Andrew Toth)f00808e932
rpc: reduce LOCK(cs_main) scope in GetBlockChecked and getblock (Andrew Toth)7d253c943f
zmq: remove LOCK(cs_main) from NotifyBlock (Andrew Toth)c75e3d2772
rest: reduce LOCK(cs_main) scope in rest_block (Andrew Toth) Pull request description: Picking up from #21006. After commitccd8ef65f9
it is no longer required to hold `cs_main` when calling `ReadBlockFromDisk`. This can be verified in `master` at https://github.com/bitcoin/bitcoin/blob/master/src/node/blockstorage.cpp#L755. Same can be seen for `UndoReadFromDisk` https://github.com/bitcoin/bitcoin/blob/master/src/node/blockstorage.cpp#L485. The first commit moves `ReadBlockFromDisk` outside the lock scope in `rest_block`, where we can see a huge performance improvement when fetching blocks with multiple threads. My test setup, on an Intel i7 with 8 cores (16 threads): 1. Start a fully synced bitcoind, with this `bitcoin.conf`: ``` rest=1 rpcthreads=16 rpcworkqueue=64 rpcuser=user rpcpassword=password ``` 2. Run ApacheBench: 10000 requests, 16 parallel threads, fetching block nr. 750000 in binary: ``` ab -n 10000 -c 16 "http://127.0.0.1:8332/rest/block/0000000000000000000592a974b1b9f087cb77628bb4a097d5c2c11b3476a58e.bin" ``` Time per request (mean) 183 ms on master 30 ms this branch So this can process 6.1 times as many requests, and saturates all the cores instead of keeping them partly idle waiting in the lock. With 8 threads the mean times were 90 ms on master and 19 ms on this branch, a speedup of 4.7x. Big thanks to martinus for finding this and the original PR. The second commit is from a suggestion on the original PR by jonatack to remove the unnecessary `LOCK(cs_main)` in the zmq notifier's `NotifyBlock`. I also found that this approach could be applied to rpcs `getblock` (including `verbosity=3`), `getblockstats`, and `gettxoutproof` with similar very good results. The above benchmarks steps need to be modified slightly for RPC. Run the following ApacheBench command with different request data in a file named `data.json`: ``` ab -p data.json -n 10000 -c 16 -A user:password "http://127.0.0.1:8332/" ``` For `getblock`, use the following in `data.json`: ``` {"jsonrpc": "1.0", "id": "curltest", "method": "getblock", "params": ["0000000000000000000592a974b1b9f087cb77628bb4a097d5c2c11b3476a58e"]} ``` master - 184 ms mean request time branch - 28 ms mean request time For `getblock` with verbosity level 3, use the following in `data.json`: ``` {"jsonrpc": "1.0", "id": "curltest", "method": "getblock", "params": ["0000000000000000000592a974b1b9f087cb77628bb4a097d5c2c11b3476a58e", 3]} ``` This verbosity level fetches an undo file from disk, so it benefits from this approach as well. However, a lot of time is spent serializing to JSON so the performance gain is not as severe. master - 818 ms mean request time branch - 505 ms mean request time For `getblockstats`, use the following in `data.json`: ``` {"jsonrpc": "1.0", "id": "curltest", "method": "getblockstats", "params": ["0000000000000000000592a974b1b9f087cb77628bb4a097d5c2c11b3476a58e", ["minfeerate","avgfeerate"]]} ``` This request used a lock on reading both a block and undo file, so the results are very good. master - 244 ms mean request time branch - 28 ms mean request time ACKs for top commit: MarcoFalke: re-ACKd7f61e7d59
💫 hebasto: ACKd7f61e7d59
, I have reviewed the code and it looks OK. Did not make benchmarking though. Tree-SHA512: 305ac945b4571c5f47646d4f0e78180d7a3d40b2f70ee43e4b3e00c96a465f6d0b9c750b8e85c89ed833e557e2cdb5896743f07ef90e4e53d4ad85452b545886
This commit is contained in:
commit
1801d8c3c9
4 changed files with 29 additions and 28 deletions
|
@ -305,8 +305,10 @@ static bool rest_block(const std::any& context,
|
|||
if (chainman.m_blockman.IsBlockPruned(pblockindex))
|
||||
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not available (pruned data)");
|
||||
|
||||
if (!ReadBlockFromDisk(block, pblockindex, chainman.GetParams().GetConsensus()))
|
||||
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
|
||||
}
|
||||
|
||||
if (!ReadBlockFromDisk(block, pblockindex, chainman.GetParams().GetConsensus())) {
|
||||
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
|
||||
}
|
||||
|
||||
switch (rf) {
|
||||
|
|
|
@ -181,7 +181,8 @@ UniValue blockToJSON(BlockManager& blockman, const CBlock& block, const CBlockIn
|
|||
case TxVerbosity::SHOW_DETAILS:
|
||||
case TxVerbosity::SHOW_DETAILS_AND_PREVOUT:
|
||||
CBlockUndo blockUndo;
|
||||
const bool have_undo{WITH_LOCK(::cs_main, return !blockman.IsBlockPruned(blockindex) && UndoReadFromDisk(blockUndo, blockindex))};
|
||||
const bool is_not_pruned{WITH_LOCK(::cs_main, return !blockman.IsBlockPruned(blockindex))};
|
||||
const bool have_undo{is_not_pruned && UndoReadFromDisk(blockUndo, blockindex)};
|
||||
|
||||
for (size_t i = 0; i < block.vtx.size(); ++i) {
|
||||
const CTransactionRef& tx = block.vtx.at(i);
|
||||
|
@ -579,34 +580,38 @@ static RPCHelpMan getblockheader()
|
|||
};
|
||||
}
|
||||
|
||||
static CBlock GetBlockChecked(BlockManager& blockman, const CBlockIndex* pblockindex) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
|
||||
static CBlock GetBlockChecked(BlockManager& blockman, const CBlockIndex* pblockindex)
|
||||
{
|
||||
AssertLockHeld(::cs_main);
|
||||
CBlock block;
|
||||
if (blockman.IsBlockPruned(pblockindex)) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
|
||||
{
|
||||
LOCK(cs_main);
|
||||
if (blockman.IsBlockPruned(pblockindex)) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
|
||||
}
|
||||
}
|
||||
|
||||
if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) {
|
||||
// Block not found on disk. This could be because we have the block
|
||||
// header in our index but not yet have the block or did not accept the
|
||||
// block.
|
||||
// block. Or if the block was pruned right after we released the lock above.
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk");
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
static CBlockUndo GetUndoChecked(BlockManager& blockman, const CBlockIndex* pblockindex) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
|
||||
static CBlockUndo GetUndoChecked(BlockManager& blockman, const CBlockIndex* pblockindex)
|
||||
{
|
||||
AssertLockHeld(::cs_main);
|
||||
CBlockUndo blockUndo;
|
||||
|
||||
// The Genesis block does not have undo data
|
||||
if (pblockindex->nHeight == 0) return blockUndo;
|
||||
|
||||
if (blockman.IsBlockPruned(pblockindex)) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Undo data not available (pruned data)");
|
||||
{
|
||||
LOCK(cs_main);
|
||||
if (blockman.IsBlockPruned(pblockindex)) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Undo data not available (pruned data)");
|
||||
}
|
||||
}
|
||||
|
||||
if (!UndoReadFromDisk(blockUndo, pblockindex)) {
|
||||
|
@ -721,7 +726,6 @@ static RPCHelpMan getblock()
|
|||
}
|
||||
}
|
||||
|
||||
CBlock block;
|
||||
const CBlockIndex* pblockindex;
|
||||
const CBlockIndex* tip;
|
||||
ChainstateManager& chainman = EnsureAnyChainman(request.context);
|
||||
|
@ -733,10 +737,10 @@ static RPCHelpMan getblock()
|
|||
if (!pblockindex) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
|
||||
}
|
||||
|
||||
block = GetBlockChecked(chainman.m_blockman, pblockindex);
|
||||
}
|
||||
|
||||
const CBlock block{GetBlockChecked(chainman.m_blockman, pblockindex)};
|
||||
|
||||
if (verbosity <= 0)
|
||||
{
|
||||
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
|
||||
|
@ -1797,7 +1801,6 @@ static RPCHelpMan getblockstats()
|
|||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
ChainstateManager& chainman = EnsureAnyChainman(request.context);
|
||||
LOCK(cs_main);
|
||||
const CBlockIndex& pindex{*CHECK_NONFATAL(ParseHashOrHeight(request.params[0], chainman))};
|
||||
|
||||
std::set<std::string> stats;
|
||||
|
|
|
@ -84,13 +84,13 @@ static RPCHelpMan gettxoutproof()
|
|||
g_txindex->BlockUntilSyncedToCurrentChain();
|
||||
}
|
||||
|
||||
LOCK(cs_main);
|
||||
|
||||
if (pblockindex == nullptr) {
|
||||
const CTransactionRef tx = GetTransaction(/*block_index=*/nullptr, /*mempool=*/nullptr, *setTxids.begin(), chainman.GetConsensus(), hashBlock);
|
||||
if (!tx || hashBlock.IsNull()) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not yet in block");
|
||||
}
|
||||
|
||||
LOCK(cs_main);
|
||||
pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock);
|
||||
if (!pblockindex) {
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR, "Transaction index corrupt");
|
||||
|
|
|
@ -248,18 +248,14 @@ bool CZMQPublishRawBlockNotifier::NotifyBlock(const CBlockIndex *pindex)
|
|||
|
||||
const Consensus::Params& consensusParams = Params().GetConsensus();
|
||||
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
|
||||
{
|
||||
LOCK(cs_main);
|
||||
CBlock block;
|
||||
if(!ReadBlockFromDisk(block, pindex, consensusParams))
|
||||
{
|
||||
zmqError("Can't read block from disk");
|
||||
return false;
|
||||
}
|
||||
|
||||
ss << block;
|
||||
CBlock block;
|
||||
if (!ReadBlockFromDisk(block, pindex, consensusParams)) {
|
||||
zmqError("Can't read block from disk");
|
||||
return false;
|
||||
}
|
||||
|
||||
ss << block;
|
||||
|
||||
return SendZmqMessage(MSG_RAWBLOCK, &(*ss.begin()), ss.size());
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue