📣 极限科技诚招搜索运维工程师(Elasticsearch/Easysearch)- 全职/北京 👉 : 立即申请加入

适用版本: 6.8-8.11

1. 错误异常的基本描述 #

cannot delete snapshot from a readonly repository 是 Elasticsearch 在删除快照时抛出的仓库级异常。它发生在删除流程的最前端——Elasticsearch 在真正执行任何删除动作之前,会先检查目标仓库是否为只读;如果是,则立即抛出 RepositoryException,不会继续后续的元数据更新或 blob 删除。

这意味着该错误不是偶发性故障,而是仓库策略与操作语义之间的明确冲突:删除快照需要更新 index-N 元数据文件、移除不再被引用的 blob,这些都属于写操作,而只读仓库在设计与实现上都不允许此类操作。

常见现象 #

  • 调用 DELETE /_snapshot/{repository}/{snapshot} 后立即返回 500,响应体中包含 repository_exceptioncannot delete snapshot from a readonly repository
  • 同一个仓库可以正常执行 GET /_snapshot/{repository}/_all 查看快照列表,甚至可以成功执行 POST /_snapshot/{repository}/_restore 恢复数据,但任何删除操作都会被拒绝。
  • 经常出现在灾备恢复仓库、跨集群共享仓库或对等复制场景中,仓库被设计为只读,但运维人员或 SLM 策略仍尝试在其上执行删除。
  • 当使用快照生命周期管理(SLM)或第三方备份工具时,如果策略配置错误,可能会反复自动触发该错误,导致大量失败日志。
  • 在某些版本的 Elasticsearch 中,如果 readonly 是通过底层存储权限(而非仓库配置)实现的,错误信息可能还伴随 AccessDeniedExceptionIOException

典型报错与异常栈 #

该错误在 REST API 响应和服务器日志中的表现形式如下:

REST API 响应示例:

{
  "error": {
    "root_cause": [
      {
        "type": "repository_exception",
        "reason": "[my_backup_repo] cannot delete snapshot from a readonly repository"
      }
    ],
    "type": "repository_exception",
    "reason": "[my_backup_repo] cannot delete snapshot from a readonly repository"
  },
  "status": 500
}

服务器日志(主节点)示例:

[2026-03-15T10:23:45,123][WARN ][o.e.s.SnapshotsService   ] [node-1] [my_backup_repo] failed to delete snapshot [snapshot_2026_03_15]
org.elasticsearch.repositories.RepositoryException: [my_backup_repo] cannot delete snapshot from a readonly repository
    at org.elasticsearch.repositories.blobstore.BlobStoreRepository.deleteSnapshots(BlobStoreRepository.java:XXX)
    at org.elasticsearch.snapshots.SnapshotsService.deleteSnapshot(SnapshotsService.java:XXX)
    at org.elasticsearch.snapshots.SnapshotsService.access$XXX(SnapshotsService.java:XXX)
    at org.elasticsearch.snapshots.SnapshotsService$2.onFailure(SnapshotsService.java:XXX)
    at org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.onFailure(ThreadContext.java:XXX)
    at org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:XXX)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:XXX)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:XXX)
    at java.lang.Thread.run(Thread.java:XXX)

当 SLM 策略触发删除时可能出现的日志:

[2026-03-15T02:00:00,000][ERROR][o.e.s.SnapshotLifecycleTask] [node-1] failed to execute snapshot lifecycle policy [daily-snapshots]
org.elasticsearch.repositories.RepositoryException: [my_backup_repo] cannot delete snapshot from a readonly repository
    at org.elasticsearch.repositories.blobstore.BlobStoreRepository.deleteSnapshots(BlobStoreRepository.java:XXX)
    ...

2. 为什么会发生这个错误 #

这个错误的根因非常直接:删除快照是一个写操作,而目标仓库被配置为只读。Elasticsearch 的 BlobStoreRepositorydeleteSnapshots(...) 方法入口处通过 isReadOnly() 检查强制拦截所有写操作。

// BlobStoreRepository.java 中的核心检查逻辑
if (isReadOnly()) {
    listener.onFailure(
        new RepositoryException(metadata.name(), "cannot delete snapshot from a readonly repository")
    );
    return;
}

只要 isReadOnly() 返回 true,无论调用方是谁(用户手动删除、SLM 自动策略、ILM 生命周期),删除请求都会被拒绝。

常见原因 #

  1. 仓库配置中明确设置了 readonly: true 这是最常见的原因。仓库在创建或更新时被显式标记为只读,例如:

    {
      "type": "fs",
      "settings": {
        "location": "/mnt/backups/es",
        "readonly": true
      }
    }
    
  2. 仓库用于跨集群恢复或灾备共享 在灾备场景中,源集群向仓库写入快照,目标集群只负责恢复。目标集群的仓库通常被配置为只读,以防止恢复集群意外修改备份数据。如果运维人员在目标集群上执行删除操作,就会触发此错误。

  3. 底层对象存储或文件系统权限限制 即使仓库配置中没有 readonly: true,如果底层存储(如 S3、GCS、Azure Blob 或 NFS)的 IAM 策略或文件系统权限只允许读取,Elasticsearch 在尝试写入或删除时仍会失败。在某些版本中,这种权限问题可能会以 cannot delete snapshot from a readonly repository 的形式表现出来,或者伴随底层的 AccessDeniedException

  4. 运维误操作:删除请求发到了只读副本仓库 在一些多集群架构中,可能存在一个可写的主仓库和多个只读的副本仓库。如果删除请求被错误地发送到了副本仓库,就会触发此错误。

  5. SLM 或 ILM 策略配置错误 快照生命周期管理(SLM)策略中配置了自动删除过期快照,但策略绑定的仓库是只读的。这种情况下,SLM 会定期尝试删除快照并持续失败。

3. 如何排查这个异常 #

建议按以下顺序系统性排查:

排查步骤 #

步骤 1:确认仓库的只读状态

curl -X GET "http://localhost:9200/_snapshot/my_backup_repo?pretty"

重点关注返回结果中的 settings.readonly 字段:

{
  "my_backup_repo": {
    "type": "fs",
    "settings": {
      "location": "/mnt/backups/es",
      "readonly": "true"
    }
  }
}

如果 readonlytrue(注意:JSON 中可能是布尔值 true 或字符串 "true",取决于版本和配置方式),则确认仓库确实是只读的。

步骤 2:确认报错发生的时间点和触发来源

检查主节点日志,确认错误是手动操作触发还是自动任务触发:

# 在主节点上搜索相关日志
grep -r "cannot delete snapshot from a readonly repository" /var/log/elasticsearch/

如果是 SLM 触发,日志中通常会出现 SnapshotLifecycleTaskslm 相关关键字;如果是手动操作,日志中会显示具体的 REST 请求来源。

步骤 3:检查仓库中的快照列表

curl -X GET "http://localhost:9200/_snapshot/my_backup_repo/_all?pretty"

确认目标快照确实存在,以及仓库中还有哪些其他快照。这有助于判断仓库的实际用途——如果仓库中包含大量历史快照且从未被删除过,很可能它就是设计成只读的。

步骤 4:检查 SLM 策略配置(如果适用)

curl -X GET "http://localhost:9200/_slm/policy?pretty"

查看是否有 SLM 策略绑定了当前只读仓库,并且配置了 retention 规则尝试自动删除过期快照:

{
  "daily-snapshots": {
    "policy": {
      "name": "<daily-{now/d}>",
      "repository": "my_backup_repo",
      "retention": {
        "expire_after": "7d",
        "min_count": 1
      }
    }
  }
}

如果 repository 指向的是只读仓库,SLM 的保留策略将无法执行删除,从而反复报错。

步骤 5:验证仓库的实际可用性

curl -X POST "http://localhost:9200/_snapshot/my_backup_repo/_verify?pretty"

如果仓库配置或底层存储有问题,_verify 会返回节点级别的详细错误信息。

步骤 6:检查底层存储权限(对象存储场景)

如果使用 S3、GCS 等对象存储,需要确认 IAM 策略或访问密钥是否具备删除权限。以 S3 为例,所需的权限至少包括:

  • s3:DeleteObject
  • s3:PutObject(用于更新 index-N 文件)
  • s3:GetObject
  • s3:ListBucket

排查时需要注意的问题 #

  • 不要混淆"仓库只读"和"索引只读"index.blocks.write 是索引级别的只读设置,与仓库级别的 readonly 完全无关。
  • readonly 可以通过多种方式生效:除了仓库配置中的 readonly: true,底层存储权限、文件系统挂载选项(如 ro)、NFS 导出设置等都可能导致只读行为。
  • SLM 错误可能被忽略:SLM 任务失败不会阻止新快照的创建,因此这个错误可能在日志中存在很久才被发现。
  • 多集群共享仓库时要特别小心:如果一个仓库被多个集群挂载,只有一个集群应该对它拥有写权限,其他集群必须是只读的。

4. 如何解决这个错误 #

常用修复思路 #

根据排查结果,选择以下修复方案之一:

方案 A:如果删除请求发错了仓库 #

如果当前仓库本来就是只读的(例如用于恢复的仓库),删除操作应该发送到可写的主仓库。检查是否有对应的可写仓库:

# 列出所有仓库,找到可写的那个
curl -X GET "http://localhost:9200/_snapshot?pretty"

然后在正确的仓库上执行删除:

curl -X DELETE "http://localhost:9200/_snapshot/correct_writable_repo/snapshot_2026_03_31?pretty"

方案 B:如果仓库确实需要支持删除,修正 readonly 配置 #

如果业务上确定该仓库应该允许删除操作,可以将 readonly 设置为 false

curl -X PUT "http://localhost:9200/_snapshot/my_backup_repo?pretty" \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "fs",
    "settings": {
      "location": "/mnt/backups/es",
      "readonly": false
    }
  }'

修改后,验证仓库是否恢复正常:

# 重新验证仓库
curl -X POST "http://localhost:9200/_snapshot/my_backup_repo/_verify?pretty"

# 尝试删除快照
curl -X DELETE "http://localhost:9200/_snapshot/my_backup_repo/snapshot_2026_03_31?pretty"

重要提醒:修改 readonly 之前,必须同时确认:

  1. 底层存储权限确实允许写入和删除(对象存储的 IAM 策略、文件系统权限等)
  2. 仓库的用途确实应该是可写的,而不是设计成只读恢复仓库
  3. 如果仓库被多个集群共享,只有一个集群应该执行写操作

方案 C:如果 SLM 策略配置错误 #

修正 SLM 策略,确保它使用的是可写仓库:

curl -X PUT "http://localhost:9200/_slm/policy/daily-snapshots?pretty" \
  -H 'Content-Type: application/json' \
  -d '{
    "schedule": "0 1 1 * * ?",
    "name": "<daily-{now/d}>",
    "repository": "writable_backup_repo",
    "retention": {
      "expire_after": "7d",
      "min_count": 1
    }
  }'

方案 D:如果底层存储权限不足 #

以 S3 为例,更新 IAM 策略以包含删除权限:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::my-elasticsearch-backup",
        "arn:aws:s3:::my-elasticsearch-backup/*"
      ]
    }
  ]
}

后续注意事项与推荐建议 #

  • 为仓库明确标注用途:在仓库名称或元数据中标注其角色,例如 prod_backup_rw(可写备份仓库)和 prod_backup_ro(只读恢复仓库),避免运维人员混淆。
  • 在 SLM 策略中检查仓库可写性:创建或更新 SLM 策略时,验证目标仓库是否支持删除操作,尤其是配置了 retention 规则时。
  • 对高风险操作增加前置检查:在执行删除快照的自动化脚本中,先调用 GET /_snapshot/{repository} 检查 readonly 状态,如果为 true 则提前报错而不是盲目重试。
  • 定期审计仓库配置:随着集群扩容和架构调整,仓库的读写角色可能会发生变化,建议定期通过 GET /_snapshot 审计所有仓库的配置。
  • 区分主仓库和副本仓库:在跨集群备份架构中,明确哪些仓库是可写的(用于创建快照),哪些仓库是只读的(用于恢复),避免角色混乱。

借助 INFINI 产品提升排障效率 #

INFINI Console #

INFINI Console 是一款企业级 Elasticsearch 管理平台,在快照仓库管理场景下可以提供以下帮助:

  • 仓库状态可视化:在 Console 的快照管理界面中,可以清晰地看到每个仓库的配置、只读状态、快照列表和健康状态,无需手动调用 API 逐个检查。
  • 操作审计与追踪:Console 会记录所有快照相关的操作历史,包括删除失败的请求。当出现 cannot delete snapshot from a readonly repository 错误时,可以通过审计日志快速定位是哪位用户或哪个自动化任务触发了错误的删除请求。
  • SLM 策略可视化与管理:Console 提供图形化的 SLM 策略管理界面,可以直观地看到每个策略绑定的仓库、保留规则和最近的执行状态,帮助快速发现配置错误的策略。
  • 多集群统一视图:如果您的架构中有多个 Elasticsearch 集群共享快照仓库,Console 可以提供统一的跨集群视图,帮助您迅速判断哪个集群应该对仓库执行写操作,哪个集群应该只做恢复。

INFINI Gateway #

INFINI Gateway 是 Elasticsearch 的高性能应用网关,在快照仓库异常处理场景下具有以下优势:

  • 请求审计与重写:Gateway 可以记录所有到达 Elasticsearch 的快照相关请求(/_snapshot/*)。如果发现删除请求频繁发往只读仓库,可以通过 Gateway 的审计日志快速定位请求来源(IP、应用、用户)。
  • 请求阻断与保护:可以在 Gateway 层配置规则,自动拦截发往只读仓库的删除请求并返回更友好的错误信息,避免请求到达 Elasticsearch 后失败。例如,可以配置规则识别 DELETE /_snapshot/{readonly_repo}/* 模式的请求并提前拒绝。
  • 流量监控与告警:Gateway 可以监控快照操作的流量模式,当检测到针对只读仓库的频繁删除失败时,可以触发告警通知运维人员。
  • 跨集群请求路由:在复杂的多集群架构中,Gateway 可以根据仓库名称自动将请求路由到正确的集群。例如,所有针对 readonly_repo 的写操作可以被自动路由到拥有可写仓库的集群,从而避免只读错误。

5. 小结 #

cannot delete snapshot from a readonly repository 的本质是仓库策略与操作语义的冲突——删除快照是写操作,而目标仓库被配置为只读。这个错误在入口检查阶段就被拦截,与快照内容、集群状态或网络无关。

处理这个错误的最有效方式是:

  1. 先确认仓库的 readonly 状态和使用目的
  2. 判断删除请求是否发错了仓库
  3. 如果仓库本来就应该可写,修正 readonly 配置并同步修复底层存储权限
  4. 如果仓库本来就是只读的,调整操作路径或 SLM 策略

通过将仓库的读写职责划分清楚,并在运维平台中建立可观测性,这类错误通常可以完全避免。结合 INFINI Console 和 INFINI Gateway,可以进一步提升快照管理的可靠性和排障效率。

相关错误 #

附:日志上下文 #

以下是 Elasticsearch 源码中触发该异常的核心代码片段(来自 BlobStoreRepository.java):

@Override
public void deleteSnapshots(
    Collection<SnapshotId> snapshotIds,
    long repositoryStateId,
    Version repositoryMetaVersion,
    SnapshotDeleteListener listener
) {
    // 入口检查:如果仓库是只读的,直接拒绝删除操作
    if (isReadOnly()) {
        listener.onFailure(
            new RepositoryException(metadata.name(), "cannot delete snapshot from a readonly repository")
        );
        return;
    }

    // 只有仓库可写时,才会继续执行以下删除逻辑:
    // 1. 列出仓库根目录的 blob
    // 2. 读取当前仓库元数据(RepositoryData)
    // 3. 计算需要删除的快照引用
    // 4. 更新 index-N 元数据文件
    // 5. 删除不再被引用的 blob
    threadPool.executor(ThreadPool.Names.SNAPSHOT).execute(new AbstractRunnable() {
        @Override
        protected void doRun() throws Exception {
            final Map<String, BlobMetadata> rootBlobs = blobContainer().listBlobs();
            final RepositoryData repositoryData = safeRepositoryData(repositoryStateId, rootBlobs);
            // ... 删除逻辑 ...
        }

        @Override
        public void onFailure(Exception e) {
            listener.onFailure(
                new RepositoryException(metadata.name(), "failed to delete snapshots " + snapshotIds, e)
            );
        }
    });
}

从源码可以看出,isReadOnly() 检查位于 deleteSnapshots 方法的最前端,任何删除请求在到达实际删除逻辑之前就会被拦截。这也是为什么这个错误的报错信息如此明确且不包含更复杂的原因链。