微服务与中间件排查 列表全部文章
微服务与中间件排查

ES 写入全线 403:磁盘刚到 95%,整个集群就“只读”了——水位线三兄弟与 read_only_allow_delete

日志写入突然全部失败,报 index read-only / allow delete (api),集群还变了 yellow。顺着 _cluster/health、_cat/allocation、allocation/explain 一路查到磁盘水位线,讲清 85/90/95 三道线、解除只读的正确姿势,以及分片过多这个隐形推手。

更多

那是一个很普通的周二晚上,我刚点完外卖。钉钉“叮”了一声:日志平台写入失败率 100%。紧接着是业务群里的一句“Kibana 里怎么查不到今天的日志了?”,再然后是同事的“在吗”。我看了一眼外卖软件:骑手还有 2.3 公里。看来今晚这顿饭是要凉着吃了。

这类问题的特点是:报错信息其实把答案写脸上了,但很多人第一反应是重启。ES 这次说得很直白——index read-only / allow delete (api),翻译成人话就是“我这块盘快满了,为了保命,现在只许删、不许写”。这篇把整个排查过程、水位线的原理、几个新老版本的坑,还有“为什么盘明明还有空间也会触发”一并记下来,既当备忘录,也给以后凌晨被叫醒的自己抄作业。

一、现象:写入全挂,读还活着

先说我们的环境:一个 3 数据节点的 ES 集群,应用日志经 Logstash(早期是 Filebeat 直连)灌进去,按天建索引,Kibana 查询。出事当晚,应用侧和 Logstash 侧同时出现下面的报错:

[2026-09-22T21:14:03,412][WARN ][logstash.outputs.elasticsearch] Retrying failed action with response code: 403
({"type"=>"cluster_block_exception",
  "reason"=>"index [app-log-2026.09.22] blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];"})

Java 业务侧如果直接用 RestHighLevelClient 写审计数据,抛出来的是这样的:

ElasticsearchStatusException[Elasticsearch exception [type=cluster_block_exception,
reason=index [audit-2026.09] blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];]]
    at org.elasticsearch.rest.BytesRestResponse.errorFromXContent(BytesRestResponse.java:187)
    at org.elasticsearch.client.RestHighLevelClient.parseEntity(RestHighLevelClient.java:1911)
    ...

这条报错里有三个关键信息,我习惯逐个拆开看:

  • cluster_block_exception:不是索引不存在、不是映射冲突,是被“块(block)”挡住了。ES 里有一类索引级别的 block,分别是 read_only、read_only_allow_delete、read(禁读)、write(禁写)、metadata(禁改元数据)。
  • FORBIDDEN/12:12 这个编号对应的正是 index read-only / allow delete (api),也就是 index.blocks.read_only_allow_delete。
  • HTTP 状态码 403,而不是 5xx:说明集群本身是健康活着的,是它主动拒绝,不是它坏了。

“先保留现场再动手”这条原则在这里同样适用。ES 的 block 是自动加上去的,如果我一上来就手动把 block 清掉,磁盘还没腾出来,30 秒后它会再次被加回去(后面讲原理)。所以第一件事不是清 block,而是把现场的事实记下来:磁盘用量、哪些节点超线、哪些分片没分配。这些命令下面一条一条来。

二、先弄懂机制:磁盘水位线三兄弟

ES 有一个叫 DiskThresholdMonitor 的组件,每隔一段时间(cluster.info.update.interval,默认 30 秒)由主节点去收集各数据节点的磁盘使用情况,然后对照三条水位线做决策。官方文档给出的默认值是这样的:

水位线默认值触发后 ES 做什么
cluster.routing.allocation.disk.watermark.low85%不再往这个节点分配新的副本分片(新建索引的分片也会避开它);已有分片不动
cluster.routing.allocation.disk.watermark.high90%尝试把这个节点上的分片迁走(relocate)到还有空间的节点
cluster.routing.allocation.disk.watermark.flood_stage95%对在该节点上有分片的索引加上 index.blocks.read_only_allow_delete:只许删除,不许写入

补充几个细节,都是我踩坑之后回头查文档才搞清楚的:

  • 这三个值既可以是百分比(85%),也可以是剩余空间的绝对值(如 50gb)。注意语义:百分比表示“已使用”,绝对值表示“至少还要剩多少”。两种写法不能混着用。
  • 官方文档里还提到了 max headroom:在没有显式配置水位线的情况下,低/高/洪水位分别有 200GB/150GB/100GB 的“剩余空间上限”,也就是说盘特别大(比如 10TB)的时候,不会傻等到用满 95% 才报警。这是较新版本的行为,老版本没有,看文档时要注意你的版本。
  • flood_stage 是“节点粒度”触发、“索引粒度”生效:只要一个节点超了 95%,凡是在这个节点上有分片的索引都会被加 block。所以一个节点满盘,可能牵连一大片索引,甚至整个集群的写入都受影响。
  • block 的释放:Elasticsearch 7.4 之前不会自动解除,要人工 PUT _settings 把它置为 null;7.4 起,当该节点磁盘使用率降到高水位线以下(注意是 high,不是 flood_stage),ES 会自动把 block 清掉。官方文档原话的意思也是“磁盘使用率降到 high watermark 以下时,索引块自动释放”。

为什么设计成“只读但允许删除”?因为磁盘满了要救命,第一件事就是删旧索引腾空间。如果整个索引被禁读禁写,你连删都删不掉,那才叫真正的死局。Lucene 段合并(merge)还会临时占用额外空间,磁盘 100% 时可能直接损坏索引,所以 ES 选择在 95% 就提前拉闸。

三、排查路线图

先看一下故障是怎么一步步发生的,这张图我贴在了值班手册里:

数据持续写入日志/审计按天建索引 ≥ 85% low不再分配新分片 ≥ 90% high尝试迁走分片 ≥ 95% flood索引加只读块 写入 403读取仍正常 ↓ 排查顺序(自上而下,每一步都能用 curl 完成) ① _cluster/healthstatus / unassigned ② _cat/allocation哪个节点盘超线 ③ _cat/shards谁 UNASSIGNED、为什么 ④ allocation/explain拿到决策原文 ⑤ 腾空间再解除只读 顺序不能反:先腾空间,再解 block;否则 30 秒后 ES 会重新把 block 加回来

四、动手:五步定位

下面所有命令都可以直接在 Kibana Dev Tools 里执行,也可以在任意一台能通到 ES 的机器上用 curl。这里统一用 demo.example.com:9200 代替真实地址,需要认证的话自己加 -u user:pass(别把密码直接敲进 shell 历史,用 --netrc 或环境变量)。

第 1 步:集群健康——先看整体

$ curl -s 'http://demo.example.com:9200/_cluster/health?pretty'
{
  "cluster_name" : "prod-log",
  "status" : "yellow",
  "timed_out" : false,
  "number_of_nodes" : 3,
  "number_of_data_nodes" : 3,
  "active_primary_shards" : 412,
  "active_shards" : 790,
  "relocating_shards" : 2,
  "initializing_shards" : 0,
  "unassigned_shards" : 34,
  "delayed_unassigned_shards" : 0,
  "number_of_pending_tasks" : 0,
  "active_shards_percent_as_number" : 95.87378640776699
}

怎么读:

  • status: yellow:所有主分片都分配了,但有副本分片没分配上。这通常是低水位线在起作用——节点超过 85%,副本不再往上放。如果是 red,说明有主分片未分配,那就严重了(新建索引时所有节点都过了低水位线,主分片也放不下,就会 red)。
  • unassigned_shards: 34:34 个分片没有落脚点,具体是谁,下一步查。
  • relocating_shards: 2:ES 正在尽力把分片从 90% 以上的节点往外搬。搬不动的原因往往是别的节点也超线了。

提醒一句:yellow 不一定是“出事了”,单节点集群天然 yellow(副本没地方放)。但配合“写入 403”,就要往磁盘上想了。

第 2 步:_cat/allocation——哪个节点在“满”

$ curl -s 'http://demo.example.com:9200/_cat/allocation?v'
shards disk.indices disk.used disk.avail disk.total disk.percent host        ip          node
   268      812.4gb   946.1gb     53.9gb        1tb           94 10.0.1.11   10.0.1.11   es-data-1
   262      781.9gb   921.7gb     78.3gb        1tb           92 10.0.1.12   10.0.1.12   es-data-2
   260      590.2gb   735.0gb    265.0gb        1tb           73 10.0.1.13   10.0.1.13   es-data-3
    34                                                                                  UNASSIGNED

这张表是整个排查里信息量最大的一张。逐列看:

  • disk.indices 是 ES 索引占的空间,disk.used 是整块盘已用空间。es-data-1 的两者差了 130GB多——这 130GB 不是 ES 索引,可能是日志、core dump、旧的快照目录、别人偷偷放的备份。后面我们就是在这里发现了猫腻。
  • disk.percent 前两个节点都在 92-94%,已经越过 high 水位线,正处于“想搬又没地方搬”的阶段;只要 es-data-1 再涨一点,越过 95%,flood_stage 就会拉闸。
  • 第三个节点才 73%,说明数据分布不均。ES 是按“分片数量”均衡的,不是按磁盘大小,一个 5GB 的分片和一个 80GB 的分片在它眼里是等价的(新版本加入了磁盘因素,但不是完全均衡)。
  • UNASSIGNED 34:与 health 的数字对上了。

如果想在命令行里快速看到“有没有 block”,可以查设置:

$ curl -s 'http://demo.example.com:9200/_all/_settings?filter_path=*.settings.index.blocks.read_only_allow_delete&pretty'
{
  "app-log-2026.09.21" : {
    "settings" : {
      "index" : {
        "blocks" : {
          "read_only_allow_delete" : "true"
        }
      }
    }
  },
  "app-log-2026.09.22" : {
    "settings" : { "index" : { "blocks" : { "read_only_allow_delete" : "true" } } }
  }
}

能看到具体哪些索引被加了 block,和报错里的索引名是吻合的。

第 3 步:_cat/shards——谁没分配,为什么

$ curl -s 'http://demo.example.com:9200/_cat/shards?v&h=index,shard,prirep,state,unassigned.reason,node&s=state' | grep -v STARTED | head
index                shard prirep state      unassigned.reason node
app-log-2026.09.22   0     r      UNASSIGNED INDEX_CREATED
app-log-2026.09.22   1     r      UNASSIGNED INDEX_CREATED
app-log-2026.09.22   2     r      UNASSIGNED INDEX_CREATED
audit-2026.09        3     r      UNASSIGNED NODE_LEFT
...

几点说明:

  • prirep 是 p(主)或 r(副)。这里全是 r,佐证了“集群 yellow 而不是 red”。
  • unassigned.reason 是“当初为什么变成未分配”,INDEX_CREATED(新建索引还没分配)、NODE_LEFT(节点离开)、ALLOCATION_FAILED(分配失败)、CLUSTER_RECOVERED 等等。注意这只是“历史原因”,不是“现在为什么分配不上”,后者要看 explain。
  • grep -v STARTED 是我偷懒,也可以直接 _cat/shards?h=...&s=state 排序,UNASSIGNED 会排到前面。

第 4 步:allocation/explain——拿到决策原文

不带参数的 GET _cluster/allocation/explain 会自动挑一个未分配的分片来解释;也可以指定:

$ curl -s -X GET 'http://demo.example.com:9200/_cluster/allocation/explain?pretty' \
  -H 'Content-Type: application/json' -d '
{ "index": "app-log-2026.09.22", "shard": 0, "primary": false }'
{
  "index" : "app-log-2026.09.22",
  "shard" : 0,
  "primary" : false,
  "current_state" : "unassigned",
  "unassigned_info" : {
    "reason" : "INDEX_CREATED",
    "at" : "2026-09-22T00:00:04.115Z",
    "last_allocation_status" : "no_attempt"
  },
  "can_allocate" : "no",
  "allocate_explanation" : "cannot allocate because allocation is not permitted to any of the nodes",
  "node_allocation_decisions" : [
    {
      "node_name" : "es-data-1",
      "node_decision" : "no",
      "deciders" : [
        {
          "decider" : "disk_threshold",
          "decision" : "NO",
          "explanation" : "the node is above the high watermark cluster setting [cluster.routing.allocation.disk.watermark.high=90%], having less than the minimum required [102.4gb] free space, actual free space [53.9gb]; ..."
        }
      ]
    },
    {
      "node_name" : "es-data-2",
      "node_decision" : "no",
      "deciders" : [
        { "decider" : "disk_threshold", "decision" : "NO",
          "explanation" : "the node is above the low watermark cluster setting [cluster.routing.allocation.disk.watermark.low=85%] ..." }
      ]
    },
    {
      "node_name" : "es-data-3",
      "node_decision" : "no",
      "deciders" : [
        { "decider" : "same_shard", "decision" : "NO",
          "explanation" : "a copy of this shard is already allocated to this node [[app-log-2026.09.22][0], node[...], [P], s[STARTED], ...]" }
      ]
    }
  ]
}

这份输出是最有说服力的证据,读法:

  • can_allocate: no + allocate_explanation:结论。
  • node_allocation_decisions:对每一个节点的裁决。三个节点三种“NO”,每一个都有 decider(裁决器)名字,这里出现了两种:disk_threshold(磁盘水位线)和 same_shard(同一节点不能放同一个分片的两份拷贝)。
  • data-1、data-2 因为磁盘超过水位线而被拒;data-3 磁盘有空间,但主分片就在它上面,副本不能和主放一起。于是副本无处可去。
  • explanation 的文本里能看到实际配置值和实际空闲值,非常直观。(如果你的输出里 explanation 后面是“…having less than the minimum required [xx] free space, actual free space [xx]”,就是磁盘水位线没跑了。)

提示:加上 ?include_disk_info=true 可以在响应里带上各节点磁盘和各分片大小,?include_yes_decisions=true 可以看到“同意”的裁决,排除“到底是哪个 decider 挡住了”时很有用。

第 5 步:找出“谁吃了盘”

$ curl -s 'http://demo.example.com:9200/_cat/indices?v&s=store.size:desc&h=index,pri,rep,docs.count,store.size,pri.store.size' | head -8
index                pri rep docs.count store.size pri.store.size
app-log-2026.09.20    5   1  412883901    186.2gb         93.1gb
app-log-2026.09.19    5   1  398112034    179.8gb         89.9gb
app-log-2026.09.21    5   1  405227719    177.4gb         88.7gb
app-log-2026.09.18    5   1  390338811    170.1gb         85.0gb
...
$ curl -s 'http://demo.example.com:9200/_cat/indices?h=index&s=index' | wc -l
417

结论出来了。我们的日志索引留了 90 天没删:当初说好“保留 15 天”,但清理脚本三个月前因为一次升级就悄悄失效了,谁也没发现。417 个索引,每个 5 主 1 副,加起来分片数达到 800 多个,比它们该有的数量翻了好几倍。同时,es-data-1 上多出来的 130GB 是一份忘在数据盘上的 heapdump 和一个快照仓库目录——这就是前面 disk.indices 与 disk.used 对不上的原因。

五、根因:三个“没人管”叠在一起

回头复盘,这次不是 ES 的问题,而是三个“没人管”叠加:

  1. 索引生命周期没人管:按天建索引,没有 ILM,也没有可靠的定时清理。数据只增不减,磁盘是被时间慢慢吃光的。
  2. 磁盘监控没人管:我们监控了 ES 的 cluster status,但没有单独监控数据节点磁盘使用率,也没有 85% 的预警。等 status 变黄,已经是低水位线后好几天,再等到写入 403,就是 flood_stage 了。
  3. 分片设计没人管:每天一个索引,5 主 1 副,每个 90GB+,看着还行;但同时有一堆小索引(每月一个的审计、监控指标)也被默认配置成 5 个主分片,只有几百 MB。一个集群 800 个分片,其中大半是小分片,白白占用堆内存、元数据和线程池,也让 ES 均衡时更“抖”。

再补一个很多人忽略的点:ES 判断磁盘用量,依据的是“整块盘”,不是“ES 目录”。所以把日志、dump、备份放在同一块盘上,等于给 ES 埋雷。官方文档在排查页里也提到了“非 Elasticsearch 文件占用同一卷”这类常见原因。

官方“分片大小”指南里有几条建议值得抄下来:分片大小建议在 10GB~50GB,单分片文档数不要超过约 2 亿;每个节点的非冻结分片数默认上限是 1000(cluster.max_shards_per_node);“搜索一千个 50MB 的分片,代价远高于搜索一个 50GB 的分片”。对着我们这个集群一比,小分片过多的毛病一目了然。

六、止血:先腾空间,再解除只读

1)腾空间(顺序很重要)

最快的办法是删旧索引——flood_stage 下依然允许删除,这也是 allow delete 的意义。先确认保留策略,然后:

# 先预览,通配符很危险;确认无误再删
$ curl -s 'http://demo.example.com:9200/_cat/indices/app-log-2026.06.*?h=index,store.size&s=index' | head -3
app-log-2026.06.01   161.7gb
app-log-2026.06.02   158.3gb
app-log-2026.06.03   160.2gb

$ curl -s -X DELETE 'http://demo.example.com:9200/app-log-2026.06.01'
{"acknowledged":true}

一点提醒:默认情况下集群设置 action.destructive_requires_name 在 ES 8 起是 true,也就是不能用通配符或 _all 删除索引,逼着你写全名,这是官方对“手滑”的善意。7.x 默认是 false,请自己格外小心,我一般用循环一条一条删,先在 shell 里 echo 再执行。

删完之后,盯着 _cat/allocation?v,看 disk.percent 是否掉到 90% 以下(7.4+ 的自动解除条件)。如果暂时删不出足够空间,可以考虑:

  • 把不重要索引的副本数临时降到 0(PUT app-log-*/_settings {"index.number_of_replicas":0}),立即释放一半空间;代价是失去冗余和读扩展,事后要恢复;
  • 清理数据盘上的非 ES 文件(dump、旧日志、快照缓存);
  • 紧急情况下临时抬高水位线,但这是拆东墙补西墙,只是给自己争取几十分钟:
$ curl -s -X PUT 'http://demo.example.com:9200/_cluster/settings' -H 'Content-Type: application/json' -d '
{
  "transient": {
    "cluster.routing.allocation.disk.watermark.low": "90%",
    "cluster.routing.allocation.disk.watermark.high": "95%",
    "cluster.routing.allocation.disk.watermark.flood_stage": "97%"
  }
}'
{"acknowledged":true,"persistent":{},"transient":{...}}

注意水位线之间要保持 low < high < flood 的顺序,否则会被拒。事后一定记得改回:把它们设置为 null 恢复默认(官方排查页的写法用的是 persistent 设置,我的习惯是临时的操作用 transient,忘了也会在整集群重启后失效;较新版本已不推荐 transient,选哪种取决于你的版本与团队约定,总之操作要记录,事后要还原)。

2)解除只读块

如果你的版本是 7.4 及以上,且磁盘已降到高水位线以下,稍等一个 cluster.info.update.interval(默认 30 秒),block 会自动消失。可以直接查:

$ curl -s 'http://demo.example.com:9200/_all/_settings?filter_path=*.settings.index.blocks&pretty'
{ }

空对象,说明 block 已经没有了。如果是旧版本(比如 6.x、7.3 以下),或者你不想等,需要手动解除:

$ curl -s -X PUT 'http://demo.example.com:9200/_all/_settings' \
  -H 'Content-Type: application/json' -d '
{ "index.blocks.read_only_allow_delete": null }'
{"acknowledged":true}

只针对某个索引,把 _all 换成索引名或 app-log-* 即可。再强调一次顺序:磁盘还在 95% 以上时手动解除,30 秒后 DiskThresholdMonitor 又会把它加回去,你会得到一个“解了又封、封了又解”的循环,非常折磨人。

验证写入恢复:

$ curl -s -X POST 'http://demo.example.com:9200/app-log-2026.09.22/_doc' \
  -H 'Content-Type: application/json' -d '{"msg":"hello after recovery","@timestamp":"2026-09-22T22:01:00Z"}'
{"_index":"app-log-2026.09.22","_id":"kX2s...","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},...}

Logstash 那边的重试机制会把积压的批次陆续补写进去;Filebeat 会从注册表记录的位置续传,理论上日志不丢——前提是它们的队列没被撑爆。(这也是我们后来给 Logstash 开持久化队列 queue.type: persisted 的原因。)

七、根治:让它不再半夜叫醒我

1)用 ILM 管理生命周期,别再手写脚本

把“按天建索引 + 定时删除脚本”换成 ILM(Index Lifecycle Management)+ Data Stream 或 rollover 别名。示意策略:热阶段按大小/时长滚动,过了 15 天直接删除:

$ curl -s -X PUT 'http://demo.example.com:9200/_ilm/policy/app-log-policy' \
  -H 'Content-Type: application/json' -d '
{
  "policy": {
    "phases": {
      "hot":    { "actions": { "rollover": { "max_primary_shard_size": "50gb", "max_age": "1d" } } },
      "warm":   { "min_age": "3d",  "actions": { "shrink": { "number_of_shards": 1 }, "forcemerge": { "max_num_segments": 1 } } },
      "delete": { "min_age": "15d", "actions": { "delete": {} } }
    }
  }
}'
{"acknowledged":true}

几点说明:max_primary_shard_size 让分片大小落在 10-50GB 的健康区间;warm 阶段的 shrink 把旧索引压成 1 个主分片,能大幅降低分片总数;delete 阶段兜底保证数据不会无限增长。冷热分层(hot/warm/cold 节点角色 + index.routing.allocation.require.data 或者数据层 data_hot/data_warm)可以把旧数据放在便宜的大容量磁盘上,热节点只承载最近几天的写入。

2)控制分片数量

  • 小索引(每天不到几个 GB)就用 1 个主分片,不要沿用 7.x 之前默认的 5 个(7.0 起默认已经改为 1 主 1 副);
  • 把“每天一个索引”改为“每周/每月一个”或 rollover 按大小滚动;
  • 定期用 GET _cat/shards?h=index,prirep,store&s=store 看一下小分片是否泛滥;
  • 留意 cluster.max_shards_per_node,触顶时创建新索引会直接失败,那是另一种事故了。

3)监控与告警:在 85% 就喊,别等 95%

磁盘告警要“分层”:

  • 数据节点数据盘使用率 > 80% 预警、> 85% 告警、> 90% 紧急(对应 low / high 水位线之前留出反应时间);
  • 按增长速率预测“几天后满”,比固定阈值更有用(Prometheus 的 predict_linear 就是干这个的);
  • 监控 _cluster/health 的 unassigned_shards,大于 0 持续 10 分钟就告警;
  • 日志里 DiskThresholdMonitor 的 WARN,如 low disk watermark [85%] exceeded on ...、flood stage disk watermark [95%] exceeded on ...,也可以作为关键字告警;
  • ES 数据盘独占,别和应用日志、dump 混用同一挂载点。

八、复盘

做法为什么
看到 FORBIDDEN/12 先查磁盘,不要先重启这是 ES 的自我保护,重启不释放空间,block 还会回来
先腾空间,再解 block(7.4+ 可以等它自动解)磁盘没降下去就手动解,30 秒后会被再次加上
_cat/allocation 里对比 disk.indices 与 disk.used差值大说明盘里有非 ES 文件,这是很隐蔽的空间黑洞
用 allocation/explain 读 decider 原文“分片没分配”的原因有十几种,磁盘只是其一,别靠猜
ILM 管理生命周期,delete 阶段兜底手写清理脚本会悄悄失效,而没人发现
分片 10-50GB,小索引用 1 个主分片过度分片浪费堆内存,还让均衡更抖动
80%/85%/90% 三档磁盘告警 + 增长预测在 ES 自己动手之前,人先动手
临时抬高水位线要记录并还原它只是延期,不是修复

一句话备忘:写入 403 + FORBIDDEN/12 → 磁盘过了 95% → _cat/allocation 找满盘节点 → allocation/explain 读原因 → 删旧索引腾空间 → 降到 90% 以下(7.4+ 自动解除,旧版本 PUT _all/_settings 置 null)→ 上 ILM 和磁盘告警。

最后说句掏心窝子的:ES 这个 95% 的“拉闸”,其实是个负责任的保险丝。它就像一位嘴硬心软的门卫:“盘满了?今天谁也别写,只准搬东西出去。”我当时气得想骂它,回头一看,是我三个月前写的清理脚本先罢工的。至于那份饭,我端起来的时候,已经凉得像 ES 的 flood_stage——不,比它还凉。

参考资料

— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人