这台单节点 OpenSearch 存放 qa、stg、preprod 几个非生产环境的日志。OpenSearch 是 3.5.0,用 deb 包安装、由 systemd 管理,跑在系统的 OpenJDK 21 上;虚拟机 4 vCPU、31 GiB 内存、512 GB 单盘,堆 16 GiB(-Xms16g -Xmx16g)。主机层只有 node_exporter 向 Prometheus 上报系统指标。巡检看了主机内存、OpenSearch 自带的插件和告警、集群健康状态和服务日志。

OpenSearch 报告的内存使用率#

OpenSearch 自己报告的内存使用率是 98%。这个值按 total 减 free 计算(见 OsStats.java),free 取自 JVM 的 getFreeMemorySize(见 OsProbe.java),Page Cache 被算成了已用。

OpenSearch 在 64 位 JVM 上默认用 hybridfs 存储类型,按文件类型分别用 mmap 或 NIO 读取 Lucene 段文件(见 IndexModule.java 里的 defaultStoreType 和 index.store.hybrid.nio.extensions)。两种方式读过的文件数据都缓存在操作系统的 Page Cache 里,不占 JVM heap。

判断内存够不够要看 /proc/meminfo 里的 MemAvailable。按内核文档的定义,它估算的是不发生 swap 时还能给新程序用多少内存,由 MemFree、可回收的 slab、文件 LRU 链表的大小和各 zone 的低水位算出;Cached 是从磁盘读过的文件的缓存,也就是 Page Cache(见 内核的 /proc 文档)。这台机器的 MemAvailable 约 10.8 GiB;OpenSearch 进程常驻 18.2 GiB,其中匿名内存 16.2 GiB、文件映射 2.0 GiB,它所在的 cgroup 里另有约 4.8 GiB 页缓存。98% 这个数不说明内存紧张;以后给内存配告警,按 1 - MemAvailable / MemTotal 计算。

堆设为 16 GiB,约为物理内存的一半,和安装文档的建议一致(见 安装文档)。jvm.options 里开了 -XX:+AlwaysPreTouch,JVM 启动时就把堆的每一页都访问一遍(见 java 命令文档),所以 16 GiB 从启动起就全部常驻。节点这次启动以来(约 8 天)的 GC 日志里,GC 后的存活数据最高约 2.8 GiB,p99 约 2.6 GiB。下次重启时可以把堆减到 8 GiB,腾出约 8 GiB 内存,存活数据仍有约 2.9 倍的余量。

OpenSearch 进程的 swap 占用#

按进程的 smaps_rollup 统计,OpenSearch 有 1.05 GiB 在 swap 里,整机 2 GiB 的 swapfile 基本用满。这台机器的 vm.swappiness 是默认值 60。这个参数表示 swap 和文件分页的相对 IO 开销,取值 0 到 200,设为 100 时内核对 Page Cache 和匿名页施加同样的回收压力(见 内核的 vm sysctl 文档)。JVM heap 属于匿名页,堆又从启动起全部常驻,长期没被访问的堆页在内存回收时就会被换出去。

在线就能做的一步是把 vm.swappiness 调到 1。Elasticsearch 的内存配置文档给的就是这个值,它降低内核换出内存的倾向,正常情况下不会再发生 swap(见 Configure swappiness)。下次重启时,可以再在 opensearch.yml 里设置 bootstrap.memory_lock: true,让 OpenSearch 启动时锁定内存(见 configuration and system settings)。这还需要在 systemd 里给 OpenSearch 加上 LimitMEMLOCK=infinity,这台机器现在的限制只有 8 MiB(见 OpenSearch 博客的 memory locking 一文)。安装文档对生产集群的要求也是关闭 swap(同上安装文档)。

Performance Analyzer 的 agent 没有运行#

OpenSearch 内部的 JVM、线程池和分片状态没有被采集。Prometheus 只抓 node_exporter 的主机指标,机器上也没有 opensearch-exporter、Metricbeat 这类采集 OpenSearch 指标的程序。

OpenSearch 自带的 Performance Analyzer(PA)由插件和 agent 两部分组成:插件在每个节点上采集指标,临时存放在 /dev/shm,负载高时最多用到 1 GB;agent 需要单独启动(tarball 安装用 performance-analyzer-agent-cli),在 9600 端口提供 REST API,RCA(Root Cause Analysis)也由它运行。2.0 及以后的版本默认安装 PA 插件(见 PA 文档)。

这台机器的配置里 PA 和 RCA 都开着,但 9600 端口没有监听,agent 没有运行。PA 插件的 5 个采集线程(pa-collectors)却一直在跑,节点这次启动的 7.9 天里累计用了约 2.5 CPU·小时,占 OpenSearch CPU 的 1.5%。

不打算用 PA 的话,按 PA 文档的步骤先关 RCA、再关 PA,文档给出的理由是节省内存(同上 PA 文档):

POST _plugins/_performanceanalyzer/rca/cluster/config
{"enabled": false}

POST _plugins/_performanceanalyzer/cluster/config
{"enabled": false}

需要引擎指标的话,可以改装 opensearch-prometheus-exporter 插件,让现有的 Prometheus 一并采集集群状态和节点的 JVM、文件系统、断路器等指标。它要装在每个被采集的节点上,插件版本要和 OpenSearch 版本对应(见仓库的 README 和兼容性表)。

Alerting 里没有基础设施监视器#

Alerting 的通知通道由 Notifications 插件管理,支持 Slack、Microsoft Teams、自定义 webhook 等(见 Notifications 文档)。这台机器上已经配好了 3 个 Discord webhook 通道,都处于启用状态;Alerting 里却只有两条监视器,都是已禁用的业务指标监控。

集群状态、磁盘和 heap 都没有告警。Alerting 的 per cluster metrics monitor 可以直接查 _cluster/health、_nodes/stats 这类 API,文档的例子里就有「集群健康变成 yellow 或 red 时告警」(见 per cluster metrics monitors)。可以复用现有的 Discord 通道先补三条。第一条是集群状态变成 red,或者 yellow 持续一段时间。第二条是磁盘用量接近 85%,这是磁盘低水位的默认值,超过后 OpenSearch 不再往这个节点分配分片(见 cluster settings)。第三条是 JVM heap 持续偏高。

默认 1 副本导致的 yellow#

集群健康状态一直是 yellow,有 1,357 个未分配的副本分片。这台机器有 1,456 个索引,各有 1 个主分片;业务日志索引大多没有索引模板,副本数用的是默认值。

OpenSearch 会把副本分片分配到和主分片不同的节点上(见 入门文档)。索引没有显式设置副本数时,index.number_of_replicas 取 cluster.default_number_of_replicas 的值,默认是 1(见 index settings 文档)。只有一个节点时,这些副本无处分配,集群就一直是 yellow,健康状态也没法当告警条件用。

主副分片合计 2,813 个,超过了 cluster.max_shards_per_node 的默认值 1000,这个上限按主分片和副本分片的总数计算(同上 cluster settings)。集群设置里它被调到了 5000,是当初为了绕开上限临时调高的。

处理办法是把集群默认副本数设为 0。ISM 的历史索引有自己的副本设置 plugins.index_state_management.history.number_of_replicas,默认是 1,集群默认值管不到它,要一起设为 0(见 ISM settings):

PUT _cluster/settings
{
  "persistent": {
    "cluster.default_number_of_replicas": 0,
    "plugins.index_state_management.history.number_of_replicas": 0
  }
}

存量索引可以用 _cat/indices?health=yellow 列出来,分批把副本数改成 0。索引模板和 ISM rollover 用的模板里如果显式写了 number_of_replicas,会覆盖集群默认值,也要一起改。改完之后,预计 1,357 个未分配分片清零,集群转为 green。

Log4j2 双写的服务日志#

/var/log/opensearch 下同时有 opensearch_server.json 和 opensearch.log。这是默认的 log4j2.properties:根 logger 同时挂着 rolling 和 rolling_old 两个 appender,前者用 JSON 格式写 <cluster_name>_server.json,后者用纯文本写 <cluster_name>.log,都按 128 MB 滚动,滚出去的文件压缩成 .gz(见 log4j2.properties)。

两份的内容相同。当前写入速度是 JSON 约 9.4 KB/s、纯文本约 4.4 KB/s,合计每天约 1.1 GiB(未压缩)。机器上没有程序读 JSON 那一份,去掉 rolling 这个 appender 以及 rootLogger 对它的引用,服务日志的写入就从约 13.8 KB/s 降到 4.4 KB/s;改 log4j2.properties 需要重启一次。

日志量大还有内容上的原因。最近 20 万行里 96% 是 ISM 的 INFO,ISM 每跑一次任务就打 4 行,来自下面三个 logger。这三个 logger 可以在线调成 WARN(见 OpenSearch 日志文档),服务日志的行数预计减少约 96%:

PUT _cluster/settings
{
  "persistent": {
    "logger.org.opensearch.jobscheduler.transport.PluginClient": "WARN",
    "logger.org.opensearch.jobscheduler.scheduler.JobScheduler": "WARN",
    "logger.org.opensearch.indexmanagement.indexstatemanagement.ManagedIndexRunner": "WARN"
  }
}

总结#

六个问题里,两个在内存上。OpenSearch 自报的 98% 把 Page Cache 算成了已用,按 MemAvailable 看整机还有约 10.8 GiB 可用;OpenSearch 有 1.05 GiB 的堆页在 swap 里,先把 swappiness 调到 1,重启时再开 memory_lock,堆也可以借这次重启从 16 GiB 减到 8 GiB。两个在监控上。PA 的 agent 没有运行,采集线程仍在消耗 CPU,不用就关掉,要引擎指标就改装 Prometheus exporter;Alerting 里没有基础设施监视器,先补集群状态、磁盘和 heap 三条。另外两个出在单节点沿用的默认配置上。默认 1 副本让集群一直 yellow,集群默认值和 ISM 历史索引的副本数都要改成 0;Log4j2 默认同时写两份服务日志,去掉 JSON 那一份,再把 ISM 的三个 logger 调成 WARN。