首页 / 资讯中心 / 文章详情

Ansible Facts深度解析:采集机制、性能优化与缓存避坑指南

Ansible Facts深度解析:采集机制、性能优化与缓存避坑指南 ★ FEATURED ARTICLE
最近帮团队接手一套20台机器的发布环境第一个让我头疼的现象是整个Playbook跑一次要近半小时而且卡顿的位置既不在应用包拉取也不在数据库迁移而是每个Play开头那个几乎没人关注的Gathering Facts环节。用-vvv跑一次看逐task耗时就原形毕露——光收集facts就吃掉了整个任务接近60%的时间。这个经历让我意识到很多做自动化的人对Ansible facts的理解还停留在能取主机名和内存容量的层面但实际生产环境里facts的收集机制、数据结构、裁剪方式和缓存策略每一项都直接影响执行速度和结果正确性。这篇我把facts从底层机制到实战操作完整梳理一遍最后聊聊实际项目中我踩过的变量覆盖和缓存坑。1. 先把机制搞清楚facts发生在哪一步为什么默认就收集1.1 一次真实耗时定位当时我做的第一件事不是优化代码而是确认时间到底花在哪。跑一条上线命令time ansible-playbook deploy.yml -f 20然后打开debug发现真正拖慢的task是没有名字的Gathering Facts。再用ansible -m setup单独测一台主机的收集耗时结果一台2C4G的云主机全量收集要3.8秒左右。20台机器并行跑光这里就耗掉了一分多钟。很多新手遇到这个问题会误以为网络慢、SSH握手慢其实都忽略了Ansible在每个Play执行前默认做的那次全量fact收集。后来我抽查了团队里其他十几套Playbook发现一个共性大部分人在写剧本时根本不会显式设置gather_facts导致Ansible按默认行为在每台目标主机上执行一次完整的setup模块收集。而真正被用到的fact往往只有两三个字段。这就像每次进门都让保安把你从头到脚扫描一遍只为了确认你今天带了门禁卡。1.2 Facts的本质目标主机运行时的状态快照Ansible facts简单说就是控制端从目标主机采集回来的一组运行时数据。它包含操作系统类型、发行版信息、内核版本、CPU架构、内存容量、磁盘分区、网卡和IP、挂载点、包管理器、虚拟化类型、当前用户等一系列信息。收集完成后这些数据会被存进变量ansible_facts里同时每个fact也会被注入为顶层变量比如ansible_os_family、ansible_hostname。要理解facts首先要分清它和inventory变量的区别。inventory变量是你在控制端声明的主观信息比如你可以把某台机器的分组写成web_servers、给它定义app_port: 8080。而facts是目标机执行后返回的客观信息Ansible没法靠读取inventory知道你机器实际装的是CentOS还是Ubuntu更没法知道内存到底是8G还是16G。所以一个合格的角色凡是涉及系统判断、环境分支的地方都应该优先用facts而不是inventory变量——因为你声明的可能错机器反馈的通常假不了。1.3 收集机制setup模块到远程节点的一次往返为什么facts会这么慢因为它不是一个简单的API调用而是一整套搬运执行流程。以最常见的SSH连接为例Ansible控制端在Play开始时会做以下事情把setup模块的Python源码连同参数打包通过SSH连接传输到目标主机的临时目录在目标主机上调用系统的Python解释器执行这段模块代码模块内部通过读取/proc、执行系统命令、调用dmidecode等方式收集信息把结果以JSON格式写回给控制端控制端解析返回内容把ansible_facts键下的数据注册为变量。全量收集之所以慢是因为第4步里包含了对硬件、磁盘、网络等多种子系统的探测很多操作在虚拟化环境里响应并不快。如果目标主机用的还是老版本Python、或者临时目录权限不对这一步还会出现各种附加延迟。所以理解facts的第一步就是意识到它本质上是一项在目标机器上跑一段采集程序的工作是有真实计算成本的。2. 解剖一只麻雀setup输出里的数据结构长什么样2.1 第一条命令把facts完整拉出来看想直观理解facts最好的方式是直接在命令行跑一次setup模块ansible node1 -m setup返回的是一大坨JSON几十上百个字段。直接看会很乱我习惯把它落盘再用jq处理ansible node1 -m setup node1_facts.json jq .ansible_facts node1_facts.json | head -200如果只想看某几个字段setup模块支持filter参数这里的匹配规则是glob风格ansible node1 -m setup -a filteransible_os_family ansible node1 -m setup -a filteransible_*注意filter参数匹配的是fact键名而不是路径层级。想过滤出所有网络相关的字段直接写filteransible_*或filteransible_default_ipv4都行但没法写filter*ipv4*这种模糊匹配就一定要把外层引号加上避免shell把通配符展开。2.2 常用字段与典型场景对照我整理了生产环境里最常用的一批fact附上了对应的典型用途方便你按图索骥Fact键名含义典型用途ansible_os_family系统家族RedHat / Debian / Suse等用when判断走哪套初始化逻辑ansible_distribution具体发行版Ubuntu、CentOS等区分yum/apt/dnf细节ansible_distribution_version发行版主版本号校验系统最低版本ansible_kernel内核版本判断是否需要重启ansible_hostname短主机名生成集群节点名称ansible_fqdn完整主机域名签发证书、写入DNSansible_default_ipv4默认路由网卡地址信息注册服务对外地址ansible_interfaces所有网卡列表判断是否存在多网卡ansible_memtotal_mb物理内存总量MB动态调整JVM或MySQL参数ansible_processor_vcpusCPU核数动态调整worker进程数量ansible_devices块设备结构判断磁盘容量和分区ansible_mounts挂载点列表磁盘空间检查、数据盘识别ansible_pkg_mgr包管理器类型决定调用apt / yum / dnf模块ansible_virtualization_type虚拟化类型kvm/xen/container区分物理机与容器环境ansible_local用户自定义facts读取业务自定义标签这些字段基本覆盖了日常自动化90%以上的判断需求。比如写一个通用的初始化角色第一步往往就是- name: 适配不同系统家族 include_tasks: setup_{{ ansible_facts[ansible_os_family] | lower }}.yml2.3 网络、磁盘和挂载点的深层结构很多初学的人以为ansible_default_ipv4就是个IP字符串实际它是个嵌套字典{ ansible_default_ipv4: { address: 192.168.1.20, gateway: 192.168.1.1, interface: eth0, macaddress: aa:bb:cc:dd:ee:ff, netmask: 255.255.255.0, network: 192.168.1.0, mtu: 1500 } }所以在模板里取IP要写ansible_facts[ansible_default_ipv4][address]而不是ansible_facts[ansible_default_ipv4]。磁盘结构类似ansible_devices下面以设备名分块再包含partitions、size、model等子字段。ansible_mounts则是一个列表每个元素带mount、device、fstype、size_total、size_available、options等字段非常适合做磁盘空间巡检。判断某个路径是否已被挂载我常用这种写法- name: 检查数据盘是否挂载到 /data assert: that: - ansible_facts[ansible_mounts] | selectattr(mount, equalto, /data) | list | length 0这种嵌套结构在写模板和条件判断时非常容易踩坑我的经验是先在本地把facts落盘用jq看清楚层级再动笔写引用。3. 性能阀门用gather_subset精准控制采集范围3.1 全量收集到底慢在哪我在一套云环境里测过几组数据单台2C4G的Linux主机不同采集范围耗时有明显差异收集范围实测耗时单台说明默认全量all3.5 ~ 4.5秒包含硬件、网络、虚拟化、系统服务等全部探测!all network子集0.4 ~ 0.8秒只保留网络相关fact和最小基础集!all!min接近0.1秒基本不返回任何有用fact耗时大头集中在硬件信息探测上。比如扫描本机所有块设备、读取SMART状态、解析DMI数据这些操作在云主机里要遍历虚拟设备文件响应并不快。如果只是一套应用发布Playbook最终只用到ansible_default_ipv4和ansible_os_family全量收集就是纯粹的浪费。3.2 gather_subset的取值规则与推荐组合Ansible的setup模块和Play关键字gather_subset都支持用all、min、hardware、network、virtual、ohai、facter这些子集名称来控制范围也支持用!前缀排除。一个关键细节是min子集无论什么组合都会默认被包含除非你显式写!min。所以!all并不是什么都不收而是不要全部只保留最小必要集和显式点名要的子集。生产环境里我比较常用的组合是- hosts: all gather_facts: true gather_subset: - !all - network这行配置的意思很明显我只要网络facts加最小基础facts。如果还需要操作系统信息把virtual或hardware加进去也行。比如- hosts: all gather_facts: true gather_subset: - !all - network - virtual这套组合对绝大多数Web服务发布场景足够了。但要注意如果你的角色依赖某个特定fact比如ansible_devices而你又把hardware子集排除了那任务运行时会直接报变量未定义而不是自动补采。3.3 关掉自动收集之后怎么补救还有一种更激进的做法某些性能敏感的场景直接把gather_facts: false让Play一个facts都不收。这适合那些根本不依赖目标主机信息的任务比如只做网络设备配置下发、只清理临时文件。但关闭之后如果中途需要facts不必把整个Play拆开重跑可以临时手动触发一次setup- hosts: all gather_facts: false tasks: - name: 手动收集系统facts ansible.builtin.setup: gather_subset: - !all - network这样只有当前这个task会去采集后续task照样能引用到fact。值得提醒的是手动触发setup时返回的facts默认不会自动写入缓存除非你配合set_fact ... cacheable: true或用缓存插件处理否则下一个Play还是要重新采。4. 自定义facts让机器自己上报业务状态4.1 facts.d目录最简单直接的自定义入口Ansible facts不只是系统内置的那些你完全可以让目标主机把自己的一些业务状态一起汇报上来。最轻量的做法是使用facts.d目录。默认路径是/etc/ansible/facts.d也可以在ansible.cfg里改[defaults] fact_path /etc/ansible/facts.d在这个目录下放.fact结尾的文件Ansible在收集时就会把它们解析进ansible_local这个命名空间。文件格式支持INI、JSON也可以是可执行脚本。比如我写一个静态的业务标签文件mkdir -p /etc/ansible/facts.d cat /etc/ansible/facts.d/biz.fact EOF [service_info] nameweb-01 zonecn-north-1 ownerplatform EOF在Playbook里用文件模块下发也可以- name: 写入业务标签fact ansible.builtin.copy: content: | [service_info] nameweb-01 zonecn-north-1 dest: /etc/ansible/facts.d/biz.fact notify: refresh facts收集完成后模板里就可以用ansible_local.biz.service_info.name这种方式读到自定义内容。注意文件名的后缀会被当作命名空间的第一个层级所以文件叫biz.fact访问路径就是ansible_local.biz。如果放一个可执行脚本Ansible收集时会运行它并把标准输出按JSON解析。比如#!/bin/bash echo {app_version: 1.3.0, ready: true}这种脚本型自定义fact非常适合让机器上报那些每次收集时动态计算的数据比如当前部署的应用版本。提醒一句要让脚本被执行文件必须带可执行权限否则Ansible会把它当静态文件解析然后报一个解析错误。4.2 自定义facts模块当逻辑复杂到脚本不好表达时facts.d适合轻量自定义但如果要动态读取一个进程状态、连接数据库查配置、或者根据环境做复杂计算自定义模块更合适。Ansible模块的本质就是一段在目标主机上执行的Python脚本只要它返回的JSON里带ansible_facts键里面的内容就会被并入该主机facts。一个最简的自定义fact模块长这样#!/usr/bin/python from ansible.module_utils.basic import AnsibleModule def main(): module AnsibleModule( argument_specdict( is_readydict(typebool, requiredFalse, defaultTrue) ) ) facts { custom_biz: { is_ready: module.params[is_ready], deploy_time: 2024-06-01 12:00:00, } } module.exit_json(ansible_factsfacts, changedFalse) if __name__ __main__: main()把它放到角色或项目的library/目录下Playbook里直接调用- name: 拉取业务状态facts custom_biz: is_ready: false之后就能用ansible_facts[custom_biz][is_ready]引用。这里有一个关键点虽然模块返回的是ansible_facts键但在Playbook里访问的时候自定义键还是被合并进统一的ansible_facts变量里而不是另起一个新的顶层变量。所以别写custom_biz.is_ready要写ansible_facts[custom_biz][is_ready]。4.3 怎么选型、怎么落地维护我的习惯是静态标签用facts.d动态指标用自定义模块。在不可变基础设施模式下更好的做法是把facts.d文件直接烧进系统镜像里这样每次新节点上线时就自带标签不需要额外task去创建。如果节点上线后要动态加标签那就得在Playbook里先创建文件、再用setup刷新一次否则本次Play引用的ansible_local是旧值。自定义模块的维护成本比facts.d高毕竟要写Python要考虑参数校验、异常处理、幂等性。但它的优点是可以复用、可以传参数、可以对接更复杂的数据源。小型环境优先facts.d别一上来就上模块过度设计也是成本。5. 缓存facts规模上去后的必修课5.1 三种缓存方案怎么选facts的收集既然这么贵自然要想办法复用。Ansible官方提供了几种fact缓存插件实际环境中最常用的是这三种方案是否需额外组件适用场景jsonfile无需额外服务简单可靠单控制端或共享存储时使用redis需要Redis服务和Python redis库多控制端共享、集中式缓存memory完全不需要只在当前ansible进程内有效跨playbook运行不持久单机跑Playbook的话jsonfile是最稳妥的选择没有任何外部依赖还能直接看缓存文件内容排查问题方便。如果公司有多个Jenkins或GitLab Runner同时跑Ansible务必要用同一份redis缓存否则每台控制端各缓存各的数据不一致效果还不如不缓存。5.2 jsonfile缓存配置与smart模式在ansible.cfg里加上[defaults] gathering smart fact_caching jsonfile fact_caching_connection /var/cache/ansible/facts fact_caching_timeout 86400fact_caching_connection在jsonfile模式下必须写成绝对路径Ansible会在该目录下按主机名生成文件。fact_caching_timeout的单位是秒86400就是24小时。gathering smart的意思很直接如果缓存里已经有该主机的facts且没到过期时间就直接使用缓存跳过重新收集如果缓存没有或已过期则老老实实重新采一遍。这台机器改过IP、升级过内核、换过分区之类的场景最好把缓存时间调短比如3600秒。因为smart模式不会去验证缓存里的数据是否还符合机器现状它只看时间戳。5.3 缓存失效的隐患与主动刷新手段缓存最大的隐患在于它存的是过去的真相。我见过一次典型的翻车某团队先把内核升级任务的缓存timeout设成了86400第二天安全组要求批量重启异常内核版本的主机Playbook通过ansible_kernel判断是否需要重启结果因为缓存里还是旧内核版本所有机器都被判断为无需处理。实际排查耗了不少时间最后发现是缓存没失效。处理这种问题我总结了三个手段临时清缓存重跑。jsonfile方案直接rm -rf /var/cache/ansible/facts/*redis方案就执行FLUSHDB注意只清对应db别把业务Redis数据冲了。在关键变更加一个显式刷新task。需要内核、IP、挂载信息做判断的Play不要依赖缓存在判断task之前加一个setup任务强制收集相关字段。把易变facts的使用和静态facts分离。静态信息操作系统发行版、CPU架构放心用缓存动态信息磁盘剩余空间、当前IP尽量每次实时采或者把timeout压到足够低。另外要提醒一点如果用了set_fact往facts里写值想让它进缓存得加cacheable: true参数否则这个值只在当前Play生命周期里有效下一轮重新缓存时又会消失。6. 几个我自己踩进去又爬出来的坑6.1 同名变量覆盖vars比facts嗓门大Ansible有一套完整的变量优先级规则从低到高大致是角色默认变量、inventory变量、play变量、task变量、set_fact、extra_vars。facts在里面的位置并不高大致在play变量之下。这意味着你如果在play vars里写了一个和facts同名的变量facts会被覆盖。我踩过的最典型的例子某次调试一套系统检查Playbook我为了快速测试某个when判断在vars里临时放了一句ansible_os_family: Debian结果现场所有CentOS机器全走了Debian分支。当时我第一反应是facts没采到折腾半天才发现是变量优先级在作怪。所以这里有几条红线不要用ansible_开头的名字声明普通变量不要为了方便测试在Play vars里写facts同名key排查fact问题时先怀疑变量覆盖再去怀疑采集失败。用debug输出vars看看最终生效值是什么比猜快得多。6.2 smart缓存让新内核变成了旧内核这个坑我在5.3里提过但值得单独展开。当时的情况是一批机器分批执行内核升级执行完的机器已经跑着新内核而Ansible控制端的facts缓存还没过期。后续Play里有一个根据ansible_kernel判断的task依赖缓存的老值直接把已升级的机器判定成未升级于是重复执行了下次才该做的步骤造成了不小的混乱。从那以后凡是涉及系统重启、内核版本、IP地址、磁盘容量这类跑一会儿就会变的信息我都不依赖长缓存。要么把缓存timeout压到非常短要么在关键任务前用setup的filter参数只刷新指定字段成本低得多- name: 只刷新内核version ansible.builtin.setup: filter: ansible_kernel6.3 ansible_facts与顶层ansible_*的双轨故事Ansible从2.5开始引导用户使用ansible_facts[xxx]的写法但为了兼容远古模板默认还会把每个fact注入成顶层变量也就是ansible_distribution、ansible_os_family这种形式。于是你经常会看到老角色里全用顶层变量新角色里全用ansible_facts[...]混着用也能跑。真正的问题出现在某些团队把inject_facts_as_vars设为False之后。这是ansible.cfg里的一个开关关掉后顶层ansible_*变量不再自动注入只有ansible_facts字典可用。如果你模板里全是老写法升级配置后一堆任务立刻报变量未定义。我的建议是统一写成ansible_facts[ansible_distribution]这种完整字典访问形式至少保证在inject_facts_as_vars关掉后还能正常工作。另外注意ansible_facts字典里的键名在绝大多数版本里保留了ansible_前缀也就是ansible_facts[ansible_os_family]而不是ansible_facts[os_family]。有些文档里写短键形式那多半是因为依赖了特定版本的兼容行为不建议作为通用写法。6.4 自定义fact不更新的问题还有一次是facts.d文件明明被改成了新值Playbook里引用的ansible_local.biz.app_version却还是旧的。排查流程走了一圈才发现问题出在缓存facts.d文件改了但控制端走的是smart缓存根本没有重新触发收集。这个坑和6.2本质上一样但更容易被忽略——因为认知里总觉得自定义fact是每次现场处理的没想到缓存这层会被截胡。这种情况我给团队定的规范是凡是更新facts.d文件或自定义fact模块逻辑的任务紧随其后必跟一个setup刷新并且确保刷新后的值不入长时间缓存。简单说自定义fact最怕的不是写错Python而是改了没生效这类由缓存和收集时机造成的隐性bug。如果排查时不确定机器上真实的自定义fact状态直接登到目标主机上手动跑一下那个脚本或者查看facts.d文件内容能快速定位问题到底出在远端数据还是控制端缓存。最后分享一个我一直在用的习惯每个项目的roles目录下放一个print_facts.yml专门用来打印和校验本次执行环境的关键facts配合debug模块输出ansible_facts的几个核心字段。这样不管是接手别人的Playbook还是排查自己半年没碰的旧项目第一件事永远是先看看机器到底是怎么被理解的而不是直接冲进业务逻辑里找bug。Ansible facts这套东西不复杂但值得花一晚上把它彻底理顺——它几乎是所有条件判断的地基地基歪了上面盖什么都是白搭。
阅读完成 · 觉得有帮助?
咨询建站