Skip to content

Commit 1801d8c

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#26308: rpc/rest/zmq: reduce LOCK(cs_main) scope: ~6 times as many requests per second
d7f61e7 rpc: reduce LOCK(cs_main) scope in gettxoutproof (Andrew Toth) 4d92b5a rpc: reduce LOCK(cs_main) scope in GetUndoChecked and getblockstats (Andrew Toth) efd82ae rpc: reduce LOCK(cs_main) scope in blockToJSON (Andrew Toth) f00808e rpc: reduce LOCK(cs_main) scope in GetBlockChecked and getblock (Andrew Toth) 7d253c9 zmq: remove LOCK(cs_main) from NotifyBlock (Andrew Toth) c75e3d2 rest: reduce LOCK(cs_main) scope in rest_block (Andrew Toth) Pull request description: Picking up from #21006. After commit bitcoin/bitcoin@ccd8ef6 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-ACK d7f61e7 💫 hebasto: ACK d7f61e7, I have reviewed the code and it looks OK. Did not make benchmarking though. Tree-SHA512: 305ac945b4571c5f47646d4f0e78180d7a3d40b2f70ee43e4b3e00c96a465f6d0b9c750b8e85c89ed833e557e2cdb5896743f07ef90e4e53d4ad85452b545886
2 parents a653f4b + d7f61e7 commit 1801d8c

File tree

4 files changed

+29
-28
lines changed

4 files changed

+29
-28
lines changed

src/rest.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,10 @@ static bool rest_block(const std::any& context,
305305
if (chainman.m_blockman.IsBlockPruned(pblockindex))
306306
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not available (pruned data)");
307307

308-
if (!ReadBlockFromDisk(block, pblockindex, chainman.GetParams().GetConsensus()))
309-
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
308+
}
309+
310+
if (!ReadBlockFromDisk(block, pblockindex, chainman.GetParams().GetConsensus())) {
311+
return RESTERR(req, HTTP_NOT_FOUND, hashStr + " not found");
310312
}
311313

312314
switch (rf) {

src/rpc/blockchain.cpp

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ UniValue blockToJSON(BlockManager& blockman, const CBlock& block, const CBlockIn
181181
case TxVerbosity::SHOW_DETAILS:
182182
case TxVerbosity::SHOW_DETAILS_AND_PREVOUT:
183183
CBlockUndo blockUndo;
184-
const bool have_undo{WITH_LOCK(::cs_main, return !blockman.IsBlockPruned(blockindex) && UndoReadFromDisk(blockUndo, blockindex))};
184+
const bool is_not_pruned{WITH_LOCK(::cs_main, return !blockman.IsBlockPruned(blockindex))};
185+
const bool have_undo{is_not_pruned && UndoReadFromDisk(blockUndo, blockindex)};
185186

186187
for (size_t i = 0; i < block.vtx.size(); ++i) {
187188
const CTransactionRef& tx = block.vtx.at(i);
@@ -579,34 +580,38 @@ static RPCHelpMan getblockheader()
579580
};
580581
}
581582

582-
static CBlock GetBlockChecked(BlockManager& blockman, const CBlockIndex* pblockindex) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
583+
static CBlock GetBlockChecked(BlockManager& blockman, const CBlockIndex* pblockindex)
583584
{
584-
AssertLockHeld(::cs_main);
585585
CBlock block;
586-
if (blockman.IsBlockPruned(pblockindex)) {
587-
throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
586+
{
587+
LOCK(cs_main);
588+
if (blockman.IsBlockPruned(pblockindex)) {
589+
throw JSONRPCError(RPC_MISC_ERROR, "Block not available (pruned data)");
590+
}
588591
}
589592

590593
if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) {
591594
// Block not found on disk. This could be because we have the block
592595
// header in our index but not yet have the block or did not accept the
593-
// block.
596+
// block. Or if the block was pruned right after we released the lock above.
594597
throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk");
595598
}
596599

597600
return block;
598601
}
599602

600-
static CBlockUndo GetUndoChecked(BlockManager& blockman, const CBlockIndex* pblockindex) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
603+
static CBlockUndo GetUndoChecked(BlockManager& blockman, const CBlockIndex* pblockindex)
601604
{
602-
AssertLockHeld(::cs_main);
603605
CBlockUndo blockUndo;
604606

605607
// The Genesis block does not have undo data
606608
if (pblockindex->nHeight == 0) return blockUndo;
607609

608-
if (blockman.IsBlockPruned(pblockindex)) {
609-
throw JSONRPCError(RPC_MISC_ERROR, "Undo data not available (pruned data)");
610+
{
611+
LOCK(cs_main);
612+
if (blockman.IsBlockPruned(pblockindex)) {
613+
throw JSONRPCError(RPC_MISC_ERROR, "Undo data not available (pruned data)");
614+
}
610615
}
611616

612617
if (!UndoReadFromDisk(blockUndo, pblockindex)) {
@@ -721,7 +726,6 @@ static RPCHelpMan getblock()
721726
}
722727
}
723728

724-
CBlock block;
725729
const CBlockIndex* pblockindex;
726730
const CBlockIndex* tip;
727731
ChainstateManager& chainman = EnsureAnyChainman(request.context);
@@ -733,10 +737,10 @@ static RPCHelpMan getblock()
733737
if (!pblockindex) {
734738
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
735739
}
736-
737-
block = GetBlockChecked(chainman.m_blockman, pblockindex);
738740
}
739741

742+
const CBlock block{GetBlockChecked(chainman.m_blockman, pblockindex)};
743+
740744
if (verbosity <= 0)
741745
{
742746
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
@@ -1797,7 +1801,6 @@ static RPCHelpMan getblockstats()
17971801
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
17981802
{
17991803
ChainstateManager& chainman = EnsureAnyChainman(request.context);
1800-
LOCK(cs_main);
18011804
const CBlockIndex& pindex{*CHECK_NONFATAL(ParseHashOrHeight(request.params[0], chainman))};
18021805

18031806
std::set<std::string> stats;

src/rpc/txoutproof.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ static RPCHelpMan gettxoutproof()
8484
g_txindex->BlockUntilSyncedToCurrentChain();
8585
}
8686

87-
LOCK(cs_main);
88-
8987
if (pblockindex == nullptr) {
9088
const CTransactionRef tx = GetTransaction(/*block_index=*/nullptr, /*mempool=*/nullptr, *setTxids.begin(), chainman.GetConsensus(), hashBlock);
9189
if (!tx || hashBlock.IsNull()) {
9290
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not yet in block");
9391
}
92+
93+
LOCK(cs_main);
9494
pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock);
9595
if (!pblockindex) {
9696
throw JSONRPCError(RPC_INTERNAL_ERROR, "Transaction index corrupt");

src/zmq/zmqpublishnotifier.cpp

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -248,18 +248,14 @@ bool CZMQPublishRawBlockNotifier::NotifyBlock(const CBlockIndex *pindex)
248248

249249
const Consensus::Params& consensusParams = Params().GetConsensus();
250250
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
251-
{
252-
LOCK(cs_main);
253-
CBlock block;
254-
if(!ReadBlockFromDisk(block, pindex, consensusParams))
255-
{
256-
zmqError("Can't read block from disk");
257-
return false;
258-
}
259-
260-
ss << block;
251+
CBlock block;
252+
if (!ReadBlockFromDisk(block, pindex, consensusParams)) {
253+
zmqError("Can't read block from disk");
254+
return false;
261255
}
262256

257+
ss << block;
258+
263259
return SendZmqMessage(MSG_RAWBLOCK, &(*ss.begin()), ss.size());
264260
}
265261

0 commit comments

Comments
 (0)