Ir para o conteúdo

Fazendo downgrade do MongoDB 8.0 para 7.0

Jean da Silva
Published date:
5 min read

Em releases mais recentes do MongoDB, depois da 7.0, o downgrade, que antes era direto e tinha processo documentado, virou incógnita quando a Mongo Inc. publicou isto: Downgrade 7.0 to 6.0

O procedimento ficou ainda mais enxuto no MongoDB 8.0: Downgrade 8.0 to 7.0

Só isso nas release notes. Resumindo:

Daí a pergunta:

“Mas, mas o processo de downgrade ainda é possível?”

Sim, com ressalvas. Detalho abaixo. Primeiro, como funciona no MongoDB 8.0.

rs0 [direct: primary] admin> db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )
{
  featureCompatibilityVersion: { version: '8.0' },
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1739388689, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('Ufa9JQOJi8bbBFs0VPuQw1wZhi4=', 0),
      keyId: Long('7470593229067386887')
    }
  },
  operationTime: Timestamp({ t: 1739388689, i: 1 })
}

Como está documentado, upgrade e downgrade passam por 4 fases para mudar a versão, tudo via FCV.

Com isso em mente, as fases internas do downgrade em um Replica Set:

Fase 1: Início da transição

A primeira fase sinaliza ao cluster que o downgrade começou, via setFeatureCompatibilityVersion:

db.adminCommand({setFeatureCompatibilityVersion: "7.0", confirm: true})

Com o comando emitido, o Mongo transiciona para requestedVersion atualizando o documento local de FCV para kDowngradingFrom_X_To_Y. Etapa rápida, deve sempre passar. O Mongo pega lock exclusivo e bloqueia outra mudança de FCV em paralelo:

// Only allow one instance of setFeatureCompatibilityVersion to run at a time.
Lock::ExclusiveLock setFCVCommandLock(opCtx, commandMutex);

No mongo.log, a transição aparece mais ou menos assim:

{"t":{"$date":"2025-07-08T14:24:47.608-03:00"},"s":"I",  "c":"-",        "id":6744300, "ctx":"conn47","msg":"setFeatureCompatibilityVersion command called","attr":{"upgradeOrDowngrade":"downgrade","serverType":"replica set/maintenance mode","fromVersion":"8.0","toVersion":"7.0"}}
[...]
{"t":{"$date":"2025-07-08T14:24:47.608-03:00"},"s":"I", "c":"REPL", "id":20459, "ctx":"conn47","msg":"Setting featureCompatibilityVersion","attr":{"currentVersion":"8.0","newVersion":"downgrading from 8.0 to 7.0"}}
[...]
{"t":{"$date":"2025-07-08T14:24:47.618-03:00"},"s":"I", "c":"-", "id":6744301, "ctx":"conn47","msg":"setFeatureCompatibilityVersion has set the FCV to the transitional state","attr":{"upgradeOrDowngrade":"downgrade","serverType":"replica set/maintenance mode","fromVersion":"8.0","toVersion":"7.0"}}

Nessa fase o Mongo também aborta index builders incompatíveis e atualiza o wire protocol interno.

Fase 2: Validação e preparação

Aqui o Mongo checa o downgrade via _prepareToDowngrade e funções como _prepareToDowngradeActions, tudo antes de mexer em metadados, sem feature exclusiva do 8.0 rodando. Ainda não há limpeza de metadados nesta fase.

_prepareToUpgradeActions(opCtx, requestedVersion, changeTimestamp);

        {
            // Take the global lock in S mode to create a barrier for operations taking the global
            // IX or X locks. This ensures that either:
            //   - The global IX/X locked operation will start after the FCV change, see the
            //     upgrading to the latest FCV and act accordingly.
            //   - The global IX/X locked operation began prior to the FCV change, is acting on that
            //     assumption and will finish before upgrade procedures begin right after this.
            Lock::GlobalLock lk(opCtx, MODE_S);
        }
uassert(ErrorCodes::Error(549181),
                "Failing downgrade due to 'failDowngrading' failpoint set",
                !failDowngrading.shouldFail());
        hangWhileDowngrading.pauseWhileSet(opCtx);

        // This helper function is for any uasserts for users to clean up user collections. Uasserts
        // for users to change settings or wait for settings to change should also happen here.
        // These uasserts happen before the internal server downgrade cleanup. The code in this
        // helper function is required to be idempotent in case the node crashes or downgrade fails
        // in a way that the user has to run setFCV again. The code added/modified in this helper
        // function should not leave the server in an inconsistent state if the actions in this
        // function failed part way through.
        // This helper function can only fail with some transient error that can be retried (like
        // InterruptedDueToReplStateChange) or ErrorCode::CannotDowngrade. The uasserts added to
        // this helper function can only have the CannotDowngrade error code indicating that the
        // user must manually clean up some user data in order to retry the FCV downgrade.
        _userCollectionsUassertsForDowngrade(opCtx, requestedVersion);

É aqui que o MongoDB barra o downgrade se encontrar feature do 8.0 que o 7.0 não suporta. Antes de tentar, revise a versão e veja se o ambiente usa alguma delas:

Pra mostrar a falha no prepare, habilitamos featureFlagRecordIdsReplicated:

/mongod --replSet replset --dbpath /replset/rs1/db --logpath /replset/rs1/mongod.log --port 8083 --fork --keyFile /keyfile --wiredTigerCacheSizeGB 1 --setParameter featureFlagRecordIdsReplicated=true

No 8.0, essa feature mantém RecordId consistente entre membros do replica set. RecordIds são IDs internos do storage engine; com recordIdsReplicated ligado, o primário inclui RecordIds no oplog (campo rid) e força secundários a usar os mesmos RecordIds pros mesmos documentos. O 7.0 não entende essa mudança no protocolo de replicação; downgrade fica inviável. Exemplo:

replset [direct: primary] blogpost> db.createCollection("test_recordids", {
    recordIdsReplicated: true
})

replset [direct: primary] blogpost> db.test_recordids.insertMany([
...     { name: "Alice", age: 25, city: "New York" },
...     { name: "Bob", age: 30, city: "London" },
...     { name: "Charlie", age: 35, city: "Tokyo" }
... ])
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('686ea382fd1484cd608b1fbd'),
    '1': ObjectId('686ea382fd1484cd608b1fbe'),
    '2': ObjectId('686ea382fd1484cd608b1fbf')
  }
}
replset [direct: primary] local> db.oplog.rs.find({"ns": "blogpost.test_recordids","op": "i"},{'o._id':1 ,rid: 1})
[
  { o: { _id: ObjectId('686ea382fd1484cd608b1fbd') }, rid: Long('1') },
  { o: { _id: ObjectId('686ea382fd1484cd608b1fbe') }, rid: Long('2') },
  { o: { _id: ObjectId('686ea382fd1484cd608b1fbf') }, rid: Long('3') }
]

-- SECONDARY --
use local
replset [direct: secondary] local> db.oplog.rs.find({"ns": "blogpost.test_recordids","op": "i"},{'o._id':1 ,rid: 1})
[
  { o: { _id: ObjectId('686ea382fd1484cd608b1fbd') }, rid: Long('1') },
  { o: { _id: ObjectId('686ea382fd1484cd608b1fbe') }, rid: Long('2') },
  { o: { _id: ObjectId('686ea382fd1484cd608b1fbf') }, rid: Long('3') }
]

Cada documento replica com o rid que veio do PRIMARY. Em collection normal, a replicação é assim:

replset [direct: primary] blogpost> db.test_regular.insertMany([
...     { name: "Alice", age: 25, city: "New York" },
...     { name: "Bob", age: 30, city: "London" },
...     { name: "Charlie", age: 35, city: "Tokyo" }
... ])
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('686ea5ad15ad32da8c8b1fbd'),
    '1': ObjectId('686ea5ad15ad32da8c8b1fbe'),
    '2': ObjectId('686ea5ad15ad32da8c8b1fbf')
  }
}

replset [direct: primary] local> db.oplog.rs.find({"ns": "blogpost.test_regular","op": "i"},{'o._id':1 ,rid: 1})
[
  { o: { _id: ObjectId('686ea5ad15ad32da8c8b1fbd') } },
  { o: { _id: ObjectId('686ea5ad15ad32da8c8b1fbe') } },
  { o: { _id: ObjectId('686ea5ad15ad32da8c8b1fbf') } }
]

Sem rid. Tentando o downgrade, a prepare phase devolve:

replset [direct: primary] blogpost> db.adminCommand( { setFeatureCompatibilityVersion: "7.0", confirm: true } )
MongoServerError[CannotDowngrade]: Cannot downgrade the cluster when there are collections with 'recordIdsReplicated' enabled. Please unset the option or drop the collection(s) before downgrading. First detected collection with 'recordIdsReplicated' enabled: blogpost.test_recordids (UUID: 0ed7067b-8384-4e08-b66e-f0a781405ea6).
{"t":{"$date":"2025-07-09T02:06:40.869-03:00"},"s":"I",  "c":"-",        "id":6744301, "ctx":"conn42","msg":"setFeatureCompatibilityVersion has set the FCV to the transitional state","attr":{"upgradeOrDowngrade":"downgrade","serverType":"replica set/maintenance mode","fromVersion":"8.0","toVersion":"7.0"}}
{"t":{"$date":"2025-07-09T02:06:40.870-03:00"},"s":"D1", "c":"ASSERT",   "id":23074,   "ctx":"conn42","msg":"User assertion","attr":{"error":"CannotDowngrade: Cannot downgrade the cluster when there are collections with 'recordIdsReplicated' enabled. Please unset the option or drop the collection(s) before downgrading. First detected collection with 'recordIdsReplicated' enabled: blogpost.test_recordids (UUID: 0ed7067b-8384-4e08-b66e-f0a781405ea6).","file":"src/mongo/db/commands/set_feature_compatibility_version_command.cpp","line":1265}}
{"t":{"$date":"2025-07-09T02:06:40.870-03:00"},"s":"D3", "c":"ASSERT",   "id":4892201, "ctx":"conn42","msg":"Internal assertion","attr":{"error":{"code":332,"codeName":"CannotDowngrade","errmsg":"Cannot downgrade the cluster when there are collections with 'recordIdsReplicated' enabled. Please unset the option or drop the collection(s) before downgrading. First detected collection with 'recordIdsReplicated' enabled: blogpost.test_recordids (UUID: 0ed7067b-8384-4e08-b66e-f0a781405ea6)."},"location":"{fileName:\"src/mongo/db/service_entry_point_common.cpp\", line:1449, functionName:\"_handleError\"}"}}
{"t":{"$date":"2025-07-09T02:06:40.870-03:00"},"s":"D3", "c":"ASSERT",   "id":4892201, "ctx":"conn42","msg":"Internal assertion","attr":{"error":{"code":332,"codeName":"CannotDowngrade","errmsg":"Cannot downgrade the cluster when there are collections with 'recordIdsReplicated' enabled. Please unset the option or drop the collection(s) before downgrading. First detected collection with 'recordIdsReplicated' enabled: blogpost.test_recordids (UUID: 0ed7067b-8384-4e08-b66e-f0a781405ea6)."},"location":"{fileName:\"src/mongo/db/service_entry_point_common.cpp\", line:730, functionName:\"run\"}"}}
{"t":{"$date":"2025-07-09T02:06:40.870-03:00"},"s":"D1", "c":"COMMAND",  "id":21962,   "ctx":"conn42","msg":"Assertion while executing command","attr":{"command":"setFeatureCompatibilityVersion","db":"admin","commandArgs":{"setFeatureCompatibilityVersion":"7.0","confirm":true,"lsid":{"id":{"$uuid":"88d019cc-8649-4963-9e7c-1297c79d45de"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1752037588,"i":1}},"signature":{"hash":{"$binary":{"base64":"rK0IXY19pYAiE/3DaS3Rw4rRhm8=","subType":"0"}},"keyId":7519659317708980230}},"$readPreference":{"mode":"primaryPreferred"},"$db":"admin"},"error":"CannotDowngrade: Cannot downgrade the cluster when there are collections with 'recordIdsReplicated' enabled. Please unset the option or drop the collection(s) before downgrading. First detected collection with 'recordIdsReplicated' enabled: blogpost.test_recordids (UUID: 0ed7067b-8384-4e08-b66e-f0a781405ea6)."}}
{"t":{"$date":"2025-07-09T02:06:40.870-03:00"},"s":"I",  "c":"COMMAND",  "id":51803,   "ctx":"conn42","msg":"Slow query","attr":{"type":"command","isFromUserConnection":true,"ns":"admin.$cmd","collectionType":"admin","appName":"mongosh 2.5.3","command":{"setFeatureCompatibilityVersion":"7.0","confirm":true,"lsid":{"id":{"$uuid":"88d019cc-8649-4963-9e7c-1297c79d45de"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1752037588,"i":1}},"signature":{"hash":{"$binary":{"base64":"rK0IXY19pYAiE/3DaS3Rw4rRhm8=","subType":"0"}},"keyId":7519659317708980230}},"$readPreference":{"mode":"primaryPreferred"},"$db":"admin"},"numYields":0,"ok":0,"errMsg":"Cannot downgrade the cluster when there are collections with 'recordIdsReplicated' enabled. Please unset the option or drop the collection(s) before downgrading. First detected collection with 'recordIdsReplicated' enabled: blogpost.test_recordids (UUID: 0ed7067b-8384-4e08-b66e-f0a781405ea6).","errName":"CannotDowngrade","errCode":332,"reslen":509,"locks":{"MultiDocumentTransactionsBarrier":{"acquireCount":{"R":1}},"ReplicationStateTransition":{"acquireCount":{"w":10}},"Global":{"acquireCount":{"r":8,"w":3,"R":1}},"Database":{"acquireCount":{"r":6,"w":3}},"Collection":{"acquireCount":{"r":4,"w":3}},"Mutex":{"acquireCount":{"W":2}}},"flowControl":{"acquireCount":3},"readConcern":{"level":"local","provenance":"implicitDefault"},"writeConcern":{"w":"majority","wtimeout":0,"provenance":"implicitDefault"},"storage":{},"cpuNanos":2236818,"remote":"127.0.0.1:60690","protocol":"op_msg","queues":{"execution":{"admissions":11},"ingress":{"admissions":2}},"workingMillis":7,"durationMillis":7}}

Passou a Fase 2, entra a Fase 3: limpeza de metadados internos do servidor, o downgrade de fato.

O Mongo remove metadados específicos do 8.0 e ajusta o que precisa pra rodar sob restrições do 7.0.

// Set the isCleaningServerMetadata field to true. This prohibits the downgrading to
// upgrading transition until the isCleaningServerMetadata is unset when we successfully
// finish the FCV downgrade and transition to the DOWNGRADED state.
{
const auto fcvChangeRegion(FeatureCompatibilityVersion::enterFCVChangeRegion(opCtx));
FeatureCompatibilityVersion::updateFeatureCompatibilityVersionDocument(
opCtx,
actualVersion,
requestedVersion,
isFromConfigServer,
changeTimestamp,
true /* setTargetVersion */,
true /* setIsCleaningServerMetadata*/);
}

A flag impede o cluster de voltar pra upgrade no meio da limpeza. Downgrade precisa terminar de uma vez.

Outra peça central: _internalServerCleanupForDowngrade:

// This helper function is for any internal server downgrade cleanup, such as dropping
// collections or aborting. This cleanup will happen after user collection downgrade
// cleanup.
// The code in this helper function is required to be IDEMPOTENT and RETRYABLE in case the
// node crashes or downgrade fails in a way that the user has to run setFCV again. It cannot
// fail for a non-retryable reason since at this point user data has already been cleaned
// up.
// It also MUST be able to be rolled back. This is because we cannot guarantee the safety of
// any server metadata that is not replicated in the event of a rollback.
//
// This helper function can only fail with some transient error that can be retried
// (like InterruptedDueToReplStateChange), ManualInterventionRequired, or fasserts. For
// any non-retryable error in this helper function, it should error either with an
// uassert with ManualInterventionRequired as the error code (indicating a server bug
// but that all the data is consistent on disk and for reads/writes) or with an fassert
// (indicating a server bug and that the data is corrupted). ManualInterventionRequired
// and fasserts are errors that are not expected to occur in practice, but if they did,
// they would turn into a Support case.
_internalServerCleanupForDowngrade(
opCtx, getTransitionFCVInfo(fcvSnapshot.getVersion()).from, requestedVersion);

Três requisitos dessa cleanup function:

  1. IDEMPOTENT: pode rodar de novo sem quebrar
  2. RETRYABLE: recupera de falha transitória
  3. ROLLBACK SAFE: dá pra desfazer se precisar

No log, Fase 3 aparece assim:

{"t":{"$date":"2025-07-09T15:00:42.458-03:00"},"s":"I", "c":"WRITE", "id":51803, "ctx":"conn616","msg":"Slow query","attr":{"type":"update","isFromUserConnection":true,"ns":"admin.system.version","collectionType":"system","appName":"mongosh 2.5.3","command":{"q":{"_id":"featureCompatibilityVersion"},"u":{"_id":"featureCompatibilityVersion","version":"7.0","targetVersion":"7.0","previousVersion":"8.0","isCleaningServerMetadata":true},"multi":false,"upsert":true},"planSummary":"IDHACK","planningTimeMicros":26,"totalOplogSlotDurationMicros":103,"keysExamined":1,"docsExamined":1,"nMatched":1,"nModified":1,"nUpserted":0,"keysInserted":0,"keysDeleted":0,"numYields":0,"locks":{"MultiDocumentTransactionsBarrier":{"acquireCount":{"R":1}},"ReplicationStateTransition":{"acquireCount":{"w":18}},"Global":{"acquireCount":{"r":15,"w":4,"R":1}},"Database":{"acquireCount":{"r":11,"w":4}},"Collection":{"acquireCount":{"r":25,"w":4}},"Mutex":{"acquireCount":{"W":3}},"oplog":{"acquireCount":{"r":1}}},"flowControl":{"acquireCount":4},"readConcern":{"level":"local","provenance":"implicitDefault"},"storage":{"data":{"txnBytesDirty":889}},"cpuNanos":403960,"remote":"127.0.0.1:47132","queues":{"execution":{"admissions":19},"ingress":{"admissions":3}},"workingMillis":0,"durationMillis":0}}

Documento de FCV com isCleaningServerMetadata:true; MongoDB roda cleanup de cluster parameters.

Downgrade não tem _finalizeDowngrade separado; upgrade tem _finalizeUpgrade. Downgrade fecha com um update atômico no documento.

Um update atômico leva o FCV do estado transitório pra versão final:

{
  "_id": "featureCompatibilityVersion",
  "version": "7.0",
  "targetVersion": "7.0",
  "previousVersion": "8.0",
  "isCleaningServerMetadata": true
}
{
  "_id": "featureCompatibilityVersion",
  "version": "7.0"
}

No código-fonte, isso fica em SetFeatureCompatibilityVersionCommand:

// Complete transition by updating the local FCV document to the fully upgraded or
            // downgraded requestedVersion.
            const auto fcvChangeRegion(FeatureCompatibilityVersion::enterFCVChangeRegion(opCtx));

            uassert(ErrorCodes::Error(6794601),
                    "Failing downgrade due to 'failBeforeUpdatingFcvDoc' failpoint set",
                    !failBeforeUpdatingFcvDoc.shouldFail());

            hangBeforeUpdatingFcvDoc.pauseWhileSet();

            FeatureCompatibilityVersion::updateFeatureCompatibilityVersionDocument(
                opCtx,
                serverGlobalParams.featureCompatibility.acquireFCVSnapshot().getVersion(),
                requestedVersion,
                isFromConfigServer,
                changeTimestamp,
                false /* setTargetVersion */,
                false /* setIsCleaningServerMetadata */);
        }

Deu certo, o mongo.log registra algo nesta linha:

{"t":{"$date":"2025-07-09T02:00:11.129-03:00"},"s":"I", "c":"-", "id":6744302, "ctx":"conn50","msg":"setFeatureCompatibilityVersion succeeded","attr":{"upgradeOrDowngrade":"downgrade","serverType":"replica set/maintenance mode","fromVersion":"8.0","toVersion":"7.0"}}

E os binários?

Aqui fica nebuloso. FCV está bem detalhado no código, docs internas e em parte na doc pública. Downgrade de binário quase não aparece. Só cavando o código, que também fala pouco. Diferença de arquitetura: FCV tem fases e validação explícitas; trocar binário é trial-and-error. Ao substituir os binários, antes mesmo do FCV, rola checagem automática de compatibilidade dos arquivos:

void WiredTigerKVEngine::_openWiredTiger(const std::string& path, const std::string& wtOpenConfig) {
    // MongoDB 4.4 will always run in compatibility version 10.0.
    std::string configStr = wtOpenConfig + ",compatibility=(require_min=\"10.0.0\")";
    auto wtEventHandler = _eventHandler.getWtEventHandler();

    int ret = wiredtiger_open(path.c_str(), wtEventHandler, configStr.c_str(), &_conn);
    if (!ret) {
        _fileVersion = {WiredTigerFileVersion::StartupVersion::IS_44_FCV_44};
        return;
    }

    if (_eventHandler.isWtIncompatible()) {
        // WT 4.4+ will refuse to startup on datafiles left behind by 4.0 and earlier. This behavior
        // is enforced outside of `require_min`. This condition is detected via a specific error
        // message from WiredTiger.
        if (_inRepairMode) {
            // In case this process was started with `--repair`, remove the "repair incomplete"
            // file.
            StorageRepairObserver::get(getGlobalServiceContext())->onRepairDone(nullptr);
        }
        LOGV2_FATAL_NOTRACE(
            4671205,
            "This version of MongoDB is too recent to start up on the existing data files. "
            "Try MongoDB 4.2 or earlier.");
    }
[...]

Na prática, trial-and-error acontece em todo startup. O Mongo valida se os data files batem com aquele binário.

Conclusão

Nada te impede de fazer downgrade. O procedimento oficial sumiu, mas dá pra olhar os antigos, tipo 6.0 to 5.0. A doc desencoraja downgrade de binário na Community, mas o FCV por baixo é o que faz o trabalho pesado na troca de versão. O processo é bem estruturado. Duas opções na prática:

Dois fatores na decisão: tem contrato de suporte ou não.

Backup/restore decente, teste antes, Replica Set/Shard bem montado. Isso corta risco, inclusive na troca de binário. Entenda o fluxo, respeite as travas do FCV e teste no seu ambiente antes. Comentários abertos. See ya!

Próximo
Benchmarking MongoDB do 3.6 ao 8.0 - Parte 2