Skip to content

Broker enters an infinite loop in ManagedLedgerImpl.asyncReadEntries and consumes 100%+ CPU #8229

Description

@lhotari

Pulsar Broker 2.6.1 enters an infinite loop between these stack frames:

org.apache.bookkeeper.mledger.impl.OpReadEntry.checkReadCompletion
org.apache.bookkeeper.mledger.impl.ManagedLedgerImpl.internalReadFromLedger
org.apache.bookkeeper.mledger.impl.ManagedLedgerImpl.asyncReadEntries
org.apache.bookkeeper.mledger.impl.OpReadEntry.lambda$checkReadCompletion$1

To Reproduce
There aren't currently known steps to reproduce the issue in an isolated way. This happens in a load test on a 3 broker, 3 bookie, 3 zk cluster running in Openshift/k8s. The load test covers 7500 Pulsar topics which are read with short-living readers, there aren't any subscriptions. The topics are created in a namespace with retention policy of 10080 minutes. Each topic contains up to 5 messages.

Additional context
Pulsar Slack thread with lots of details at https://apache-pulsar.slack.com/archives/C5Z4T36F7/p1602159831227900

Possible reason

This is the first few lines of code in OpRead.checkReadCompletion method:

    void checkReadCompletion() {
        if (entries.size() < count && cursor.hasMoreEntries()) {
            // We still have more entries to read from the next ledger, schedule a new async operation
            if (nextReadPosition.getLedgerId() != readPosition.getLedgerId()) {
                cursor.ledger.startReadOperationOnLedger(nextReadPosition, OpReadEntry.this);
            }

The usage of cursor.hasMoreEntries() seems invalid here.

In the table of data extracted from the heap dump by using Eclipse MAT OQL, I can see that OpRead.readPosition.ledgerId != OpRead.cursor.readPosition.ledgerId and OpRead.nextReadPosition.ledgerId != OpRead.cursor.ledger.lastConfirmedEntry.ledgerId (cursor write position) .

image

Therefore, when cursor.hasMoreEntries() is called, it should take OpRead.readPosition.getLedgerId() into account. Currently the ledgerId is ignored and in the case of when cursor really has more entries in a different ledgerId, it will lead to an infinite loop.

The code in checkReadCompletion has another problem. The call to cursor.ledger.startReadOperationOnLedger might call opReadEntry.readEntriesFailed.

PositionImpl startReadOperationOnLedger(PositionImpl position, OpReadEntry opReadEntry) {
        Long ledgerId = ledgers.ceilingKey(position.getLedgerId());
        if (null == ledgerId) {
            opReadEntry.readEntriesFailed(new ManagedLedgerException.NoMoreEntriesToReadException("The ceilingKey(K key) method is used to return the " +
                    "least key greater than or equal to the given key, or null if there is no such key"), null);
        }

The problem here is that there isn't currently a way to signal this back to checkReadCompletion method and the execution will continue calling startReadOperationOnLedger again later on in the same checkReadCompletion method call.

Possibly related issue
#8078 has a similar stack trace in a screenshot of a threaddump. That also has a symptom of high CPU consumption.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugThe PR fixed a bug or issue reported a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions