ARTICLE

SaltStack任务管理实战:从cron定时到多机编排

考试通知 · 政策解读 · 开班计划

发布时间:2026/10/12 12:54:12来源:迅启考通分类:考试资讯

文章详情

以下为资讯详情页模板:正文区域由后台内容渲染,图片与正文将自动替换为对应文章内容。

文章配图
SaltStack任务管理实战:从cron定时到多机编排接手一批新机器的时候我最怕的就是夜里有变更。白天业务跑着谁也不敢动凌晨两点没人盯着要么靠人肉定闹钟爬起来敲命令要么靠脑子硬记明天要记得把那批调度任务清掉。用了Saltstack之后这个痛点算是彻底解决了。任务管理这一块本质上就是把什么时候执行、在哪执行、执行什么、执行完怎么办这套逻辑全部交给Saltstack去编排我只需要把规则写清楚。这篇教程是Saltstack入门到精通系列的第八篇也是我认为从会用Saltstack到用好Saltstack最关键的一步。前面几篇讲了配置管理、pillar、grains、state这些东西但它们更像是存量资源的描述。任务管理解决的是增量动作的执行——什么时候触发、怎么排队、怎么做多机协同。这篇文章我会从最常用的cron定时任务讲起再到一次性延迟任务、多机编排最后聊几个我实际踩过的坑尽量把思路捋清楚。1. 从被闹钟支配到被任务表支配Saltstack任务管理到底管什么先说个概念上的梳理不然很容易把几个东西混在一起。Saltstack里跟任务相关的组件实际上有四个层面第一个是执行模块Execution Modules这就是你平时在命令行里敲的salt * cmd.run uptime、salt * pkg.install vim这类即时命令。它解决的是我现在就要做一件事没有计划性跑完就结束。第二个是状态模块State Modules也就是state.sls文件里描述的东西解决的是我希望目标机器最终处于什么状态。它本身不是任务但它可以被任务系统调用。比如你写了一个ntp.sls描述必须安装ntp、必须启动ntpd那这个sls就可以作为一个任务包被定时调度。第三个是调度器Scheduler也就是schedule配置可以配置在master上也可以配置在minion上。它解决的是到了什么时间点自动去跑某个任务。这是任务管理的核心执行引擎。第四个是编排Orchestrate用salt-run state.orchestrate来跑解决的是多个minion之间、多个状态之间谁先谁后、失败怎么办这种多主机协同问题。我用一个类比给你理清楚执行模块是手动挡你想动就动一下状态模块是自动驾驶它只管最终到达什么状态调度器是闹钟到点就叫你去干某件事编排是总指挥它告诉所有车按什么顺序走、哪个路口先放行。这一篇的重点会放在调度器和编排上因为这两个才是真正的任务管理。状态模块和执行模块只是被任务系统拿来用的工具前面几篇已经讲了不少。为什么任务管理这么重要因为运维工作里大部分事故都出在人忘了执行或者执行顺序错了。比如你改了防火墙规则但你忘了30分钟后有个批量任务要跑再比如你给一批机器升级了内核但你忘了另一个编排任务还在等着重启服务。靠脑子记这些迟早出事。把任务交给系统去按计划执行出问题才有据可查。2. 定时任务怎么配Saltstack的cron状态模块实战2.1 最简单的cron.sls长什么样大多数人的定时任务需求用Saltstack的cron状态就能搞定。先看一个最基础的例子。假设我要在所有的web服务器上每天凌晨3点清理一次临时目录我会这么写# /srv/salt/clean-tmp.sls clean-tmp-job: cron.present: - name: find /tmp -type f -mtime 3 -exec rm -f {} \; - hour: 3 - minute: 0然后执行salt * state.apply clean-tmpSaltstack会在每台minion的crontab里自动加一行。这里有几个细节我要特别强调name字段是完整命令不是注释。很多人第一次写会以为name只是给任务起个名字实际上它是真正要执行的命令行。如果你写过crontab就知道crontab里只能写一条命令这里同理。hour和minute是必填的。不写的话Saltstack会使用默认值*也就是每分钟跑一次。我第一次用的时候就没注意结果一个清理脚本每分钟跑一次跑了半小时把临时目录删了个干净顺带把一个服务运行需要的临时socket文件也删了线上告警一片。更稳妥的写法是明确指定day、month这些参数不要嫌啰嗦clean-tmp-job: cron.present: - name: find /tmp -type f -mtime 3 -exec rm -f {} \; - minute: 0 - hour: 3 - day: * - month: * - weekday: *2.2 环境变量是隐形杀手有个问题很坑通过Saltstack配置的cron任务执行时的PATH跟你在终端里手动执行命令时的PATH完全不一样。crontab默认的PATH极其精简通常只有/usr/bin:/bin。所以你在命令行里能跑通的命令比如python3、/usr/local/bin/python3到了cron里可能直接报command not found。解决办法有两个二选一方案A在命令里写绝对路径。比如clean-tmp-job: cron.present: - name: /usr/local/bin/find /tmp -type f -mtime 3 -exec /usr/bin/rm -f {} \;方案B在cron条目里显式定义PATH。Saltstack的cron状态支持env参数clean-tmp-job: cron.present: - name: find /tmp -type f -mtime 3 -exec rm -f {} \; - hour: 3 - minute: 0 - env: - PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin我个人的习惯是优先用方案B因为一个任务可能要调用好几个命令每个都写绝对路径太啰嗦而且容易漏。在env里把PATH设置好下面随便写命令名就行。2.3 删除定时任务比你想的要小心删cron的写法是这样的clean-tmp-job: cron.absent: - name: find /tmp -type f -mtime 3 -exec rm -f {} \;注意cron.absent匹配的是命令内容不是你用cron.present时写的那个名字。所以你要删除某个定时任务必须把之前cron.present里的完整命令原封不动抄过来。用Jinja渲染的时候要特别小心别让变量在渲染前后产生变化否则absent会匹配不上。还有个更隐蔽的坑如果你在一个sls里同时写了cron.present和cron.absent比如一个sls文件里包含多个机器的任务配置Saltstack会按顺序执行。如果后面某个逻辑被触发去absent一个不存在的任务它不会报错只是返回unchanged。这个特性可以用来做覆盖式清理但要注意别把本该存在的任务误删了。2.4 通过Jinja管理多环境的cron差异到了多环境场景cron的写法就很有讲究了。比如生产环境的清理任务是凌晨3点测试环境的清理任务是凌晨5点。写死一个时间是不行的你也不想复制粘贴两个sls文件。更好的做法是结合pillar# /srv/salt/clean-tmp.sls {% set clean_hour pillar.get(clean_tmp_hour, 3) %} {% set clean_minute pillar.get(clean_tmp_minute, 0) %} clean-tmp-job: cron.present: - name: find /tmp -type f -mtime 3 -exec rm -f {} \; - hour: {{ clean_hour }} - minute: {{ clean_minute }}然后在pillar里为不同环境设置不同的值。这样一套sls走天下不同环境只有pillar的差异。任务管理最怕的就是每个环境的配置散落在不同地方用pillar统一管理是根治之道。3. 一次性任务和延迟任务不用盯着时钟等执行3.1 场景5分钟后我要跑一个脚本但不想手动等有时候你不需要一个循环往复的cron任务你只是想5分钟后执行一次这个脚本。Saltstack的schedule可以做到而且可以做到只在内存里生效不写进crontab。配置方式很直接写在minion配置文件里或者写在state里下发。我还是推荐写在state里下发这样方便版本管理。比如# /srv/salt/delayed-clean.sls schedule-delayed-clean: schedule.present: - name: delayed_clean - function: cmd.run - args: find /tmp -type f -mtime 3 -exec rm -f {} \; - when: 03:30 - once: True这里有几个参数必须讲清楚function是执行模块的函数名比如cmd.run、test.ping、state.apply。args是传给这个函数的参数。如果你想调用state.apply还要用kwarg传参比如schedule-run-clean-tmp: schedule.present: - name: clean_tmp_state - function: state.apply - args: clean-tmp - when: 04:00 - once: Truewhen是指定当天的某个时间点执行格式是HH:MM。once: True是关键表示这个任务只执行一次执行完之后Saltstack会把它标记为完成。不过注意这个一次是相对于minion进程生命周期而言的。如果minion重启了这个schedule任务还留在配置里可能再次触发。所以真正的一次性任务我建议跑完之后用schedule.disable或者schedule.delete主动清掉别只依赖once。3.2 schedule任务的闭环清理做一个真正的一次性任务我习惯这么写两步第一步配置里先声明# /srv/salt/oneoff-check.sls schedule-oneoff: schedule.present: - name: oneoff_check - function: cmd.run - args: /usr/local/bin/check_disk.sh - when: 23:00 - once: True第二步在脚本或者另一个state里跑完之后调用清理salt-call schedule.delete oneoff_check为什么非要加这一步因为我遇到过minion意外重启然后这个一次性任务在新进程起来之后又跑了一遍导致重复执行的问题。加了schedule.delete之后管你进程怎么重启任务表里压根就没有这条自然也就不会触发。3.3 schedule放在master还是minion这个选择很多人纠结。我的结论是放在minion每个minion自己管自己的schedule适合跟业务相关的任务比如每台机器上的清理任务、日志轮转。即使master挂了minion上的任务照样跑故障域隔离得比较好。放在mastermaster统一管理所有minion的任务适合跟全局相关的任务比如批量升级、批量重启。master感知的是全局状态可以统一调控。个别场景需要在master上配置schedule然后通过state.apply让所有minion去执行。但说实话我更多是在/etc/salt/master.d/schedule.conf里配置那种master自己发起的runner任务而不是minion任务。绝大多数minion任务我都放在minion端或者通过state下发。4. 任务编排Orchestrate让一批机器按你的剧本行动4.1 什么是Orchestrate它解决了什么假设我要做一次全量更新先关停应用层服务再更新依赖包然后重启服务最后验证健康检查。这是有严格顺序的。如果我用salt * state.apply一把梭所有机器同时执行那应用层服务可能还没停止依赖包就被替换了跑出来的结果完全不可控。这时候就需要编排。Saltstack的编排是通过master上的state.orchestraterunner来执行的。它和普通state的区别在于普通state是minion自己执行自己那份sls编排是在master上由master统一下发指令控制一批minion执行不同的sls。看一个最简单的编排例子# /srv/salt/orch/rolling-update.sls stop-app: salt.state: - tgt: web-app - tgt_type: grain - sls: app.service.stop update-packages: salt.state: - tgt: web-app - tgt_type: grain - sls: app.packages.update - require: - salt: stop-app start-app: salt.state: - tgt: web-app - tgt_type: grain - sls: app.service.start - require: - salt: update-packages health-check: salt.function: - tgt: monitor-server - tgt_type: grain - name: cmd.run - arg: - curl -f http://web-app/healthz - require: - salt: start-app执行方式salt-run state.orchestrate orch.rolling-update这个编排文件里salt.state表示调用某个minion组去执行某个statesalt.function表示调用某个minion组去执行某个执行模块。require的语义和state里一模一样就是前置依赖。4.2 编排的执行顺序和失败控制编排执行时同级的salt.state默认是并行还是串行的答案是在同一个编排sls里同级的state默认并行执行只有用require、watch这些requisite建立依赖关系才会等待前一个完成再继续。所以你在写编排的时候一定要显式写清楚依赖。我见过不少新手写编排每个salt.state都没有requisite结果一跑所有机器同时乱套然后还回头怀疑Saltstack编排是不是不生效。不是不生效是你没告诉它按什么顺序走。失败处理是另一个大坑。state.orchestrate默认在某个state失败后不会停止整个编排而是继续跑后面的。这在依赖关系清晰的时候问题不大因为前一个失败了后一个require前一个所以后一个也会失败退出。但如果有些步骤是独立执行的它失败了你可能没注意到。想要一个任务失败就完全终止的效果有两个选择在state.orchestrate里加-o参数比如salt-run state.orchestrate orch.rolling-update -o failhard遇到第一个失败就直接停止。在编排sls里把所有后续步骤都用require链挂在第一个任务下面。这是更细的控制但写起来稍麻烦。我推荐的做法是核心的关键步骤用failhard非关键的可选步骤比如更新监控探针这种即使失败也不影响主干的任务就不挂require让它们失败也能过去。4.3 把前一个任务的结果传给下一个任务编排里经常要用到上一个任务的返回值。比如你先获取了某台机器的IP列表然后要把它传给后面的部署任务。Saltstack的state.orchestrate里可以用conf.get这种runner来读取前一个的任务结果。实际开发中我更习惯用salt-run的另一个利器——runner模块手动编排而不只是依赖state写死。比如salt-run state.orchestrate orch.get_ips salt-run state.orchestrate orch.deploy_app前半段先跑完拿IP后半段直接用之前写好的state文件结合pillar把IP列表传进去。这种分段编排的做法虽然多了一步手动触发但排查问题的时候特别清晰每个人都能看明白到底卡在哪一步。4.4 编排里的目标定位tgt和tgt_type编排里的tgt和tgt_type用来指定目标机器tgt_type支持glob、grain、pillar等。我的建议是尽量用grain或者pillar作为目标匹配维度少用主机名glob。因为主机名容易随扩容变化而这台机器是不是web-app角色这种信息通过grain或pillar表达更稳定。举个例子deploy-code: salt.state: - tgt: role:web - tgt_type: grain - sls: app.deploy这样不管web节点怎么扩容缩容只要新节点打上了role: web这个grain编排就能自动覆盖到它。任务管理如果只能管理存量机器价值就少了一大半能自动适应增量机器才是真本事。5. 状态模块里的任务管理细节触发器与条件控制5.1 watch和onchanges你改配置我才执行动作任务管理不只是定时跑还包括被特定事件触发后去跑特定动作。Saltstack的state里有几个触发机制我实际用得最多的是watch和onchanges。watch和require最大的区别是require只看前一个任务是否成功执行而watch还多看一层前一个任务是否发生了变化。如果前一个任务的执行结果是unchanged没有实际改动那么watch后面的任务不会执行。这个语义非常有用。举一个典型场景# /srv/salt/nginx.sls nginx-install: pkg.installed: - name: nginx nginx-config: file.managed: - name: /etc/nginx/nginx.conf - source: salt://nginx/files/nginx.conf - require: - pkg: nginx-install nginx-reload: service.running: - name: nginx - watch: - file: nginx-config这里nginx-reload这个service任务只有nginx-config这个文件发生了变化比如内容更新了才会重启nginx。如果配置文件没变watch就不触发reload。这就是配置驱动重载的核心机制。5.2 如果你想无论变没变都要跑一次怎么办有一种情况很尴尬你确实希望每次执行都触发一个动作哪怕文件内容没变。比如你写了一个脚本希望每次salt * state.apply都跑一次。那你不要用watch直接用cmd.run的默认行为即可。我遇到过一个需求每次apply这个state都要把当前机器的磁盘信息上报一次。这种可以直接写report-disk: cmd.run: - name: /usr/local/bin/report_disk_usage.sh没有任何requisite所以每次apply都会执行。这时候反而要注意一点cmd.run默认只有在命令返回值变化时才认为状态发生了变化。如果你在脚本里没有输出cmd.run可能会认为没有变化而跳过。解决办法是加一句输出#!/bin/bash echo report disk usage at $(date) df -h只要命令有stdout输出Saltstack就会认为状态发生了改变。这是个很实用的小技巧不然你会发现任务根本没执行啊但实际上是执行了只是结果判定为unchanged被忽略掉了。5.3 testTrue任务管理最重要的安全阀不管你的任务是定时任务还是编排任务在下发之前一定要先跑一遍test模式。Saltstack的state支持salt * state.apply clean-tmp testTrue这个东西会模拟整个执行过程显示每个步骤如果真正执行会做什么但不会真正改动系统。在任务管理里testTrue还有一个额外好处它可以校验你的cron配置、schedule配置、watch依赖关系是否解析正常。如果Jinja渲染有错误、requisite引用了不存在的state IDtestTrue都会直接报错不会等到真正执行时才炸出来。我自己写任何新的sls铁打不动的流程是先state.apply xxx testTrue看返回的预执行结果。再state.apply xxx正式执行。再看一遍返回的最终结果确认有没有意外。6. 自定义执行模块与任务结果处理6.1 写一个自己的执行模块任务管理到后面必然会出现一些官网模块满足不了的需求。比如你要封装一个复杂的部署脚本不想把它写成一长串命令行。这时候就需要自定义执行模块。在Saltstack里自定义执行模块放在salt://_modules/目录下。你写一个Python文件比如deploy_tools.py# /srv/salt/_modules/deploy_tools.py def ping_services(service_list): ret {} for svc in service_list: ret[svc] __salt__[service.status](svc) return ret def backup_mysql(db_name): ret __salt__[cmd.run_all]( mysqldump --single-transaction -u root {} /backup/{}.sql.format(db_name, db_name) ) if ret[retcode] ! 0: return {success: False, message: ret[stderr]} return {success: True, message: backup done}写完存好之后还要做一步同步操作salt * saltutil.sync_modules同步之后就可以直接当普通执行模块用了salt * deploy_tools.backup_mysql mydb如果这个模块要在state里被引用也是直接写deploy_tools.backup_mysql即可。模块里的__salt__字典是Saltstack给每个执行模块内置的胶水引用可以直接调用其他模块的函数比如cmd.run_all、service.status。这个机制非常方便相当于你是站在Saltstack所有官方模块的肩膀上写自己的业务逻辑。6.2 任务执行后的结果收集任务跑完了结果怎么看命令行直接返回是一回事但如果任务是在凌晨3点由schedule自动触发的你总不可能凌晨3点爬起来盯着终端看。三个思路可以组合使用第一个思路用returner把结果送回数据库或消息队列。Saltstack的returner模块支持把执行结果发到MySQL、Redis、MongoDB等。比如我可以写一个简单的returner把结果写到某个数据库表# /srv/salt/_returners/db_returner.py def returner(ret): # 假设有配置好的数据库连接方式 conn __salt__[config.get](db_conn) # 把ret[return]的结果写入数据库 # ret 里面包含 id, fun, return, success, jid 等字段 pass配置好并sync_returners后执行任务时加--return db_returner结果就会自动入库。第二个思路用salt-run的runner。在master上执行salt-run jobs.lookup_jid 201612312359000000可以查询某个历史任务的详细返回。第三个思路也是最简单的就是让任务结果输出到一个文件。我经常在cron/schedule的命令里追加一句 /var/log/mytask.log 21然后在master上用salt * cmd.run tail -n 100 /var/log/mytask.log去批量查看。这个做法虽然土但排查效率极高——所有机器的结果都在一个固定的地方直接grep就行。很多时候运维不必非要搞得多花哨能最快定位问题的方式就是好方式。6.3 任务失败的通知机制任务跑失败了得让人知道。我的做法是通过cmd.run返回码判断配合一个失败才发消息的执行模块。比如写一个自定义模块def notify_slack(message): # 调用企业微信/钉钉/邮件 webhook __salt__[http.query]( os.environ.get(ALERT_URL), methodPOST, datajson.dumps({text: message}) ) return True然后每次schedule任务我在命令后面加上如果非0退出码就调通知。但更推荐的做法是把通知逻辑也写进自定义执行模块里把业务逻辑和告警逻辑放在一起def run_and_alert(cmd, alert_group): ret __salt__[cmd.run_all](cmd, python_shellTrue) if ret[retcode] ! 0: __salt__[alert.notify](task failed on {}: {}.format(__grains__[id], ret[stderr])) return ret这样不管你是手动执行、cron调度、schedule调度还是编排调用失败之后都会自动走告警通道不用额外维护一套告警规则。7. 几个高频坑我踩过之后总结的排错经验7.1 坑一cron任务执行了但命令本身报错crontab里看不到日志先说现象你配置了cron任务crontab -l 能看到时间到了它也确实跑了但你在系统日志里看不到任何输出。原因是cron默认把输出通过邮件发走如果机器没装邮件系统输出就直接丢了。尤其是cron.present默认不会给你加日志重定向。解决办法很简单在cron.present的命令后面显式加重定向clean-tmp-job: cron.present: - name: echo clean-tmp start $(date) /var/log/clean-tmp.log 21 find /tmp -type f -mtime 3 -exec rm -f {} \; /var/log/clean-tmp.log 21日志是一个运维人员的命根子。任务跑了没、跑成功没、输出是什么全得靠日志别指望系统帮你保留。7.2 坑二重名任务导致Jinja渲染后的cron出现重复这是一个我印象很深的坑。我有两个sls因为命名不规范都用sync-cert-job这个名字。结果两个sls同时apply到一台机器上Saltstack在渲染crontab时发现两条完全不同的命令但名字一样。它不会报警而是把后渲染的覆盖掉先渲染的。最后我的证书同步任务莫名消失排查半天才发现是两个sls里的ID冲突了。在Saltstack里state的ID是全局标识一个ID只能对应一个state声明。尤其是cron这种写外部的任务ID冲突导致的结果就是先建的任务被后建的悄悄替代。所以命名一定要加上区分业务含义的前缀比如sync-cert-from-vault-job: cron.present: ...不要用sync-cert-job这种到处都可能重复的通用名字。7.3 坑三schedule任务在minion重启后没了如果schedule配置不是写在minion配置文件里而是通过salt * state.apply schedule_config这种方式下发那minion重启之后这个schedule任务会不会丢答案是要看配置来源是否持久化。如果schedule写在/etc/salt/minion.d/*.conf里minion启动时会重新加载如果只是通过salt-call schedule.present设置的内存态minion重启后就不在了。为了稳妥我给所有schedule任务都配套了一个state文件作为持久化配置源# /srv/salt/schedule/custom.sls schedule-custom-jobs: schedule.present: - name: disk_clean - function: state.apply - args: disk_clean - when: 02:30这样即使minion重启只要执行过一次state.apply schedule/custom任务就在如果新装了一台minion初始部署时也会把这个state带上schedule就自动建立。运维的可靠性从来不是靠运行时手动敲一下来保证的而是靠声明式配置重复执行不会错来保证的。7.4 坑四编排跑了半天发现第一步就错了还继续往下跑这个问题我在前面提过一嘴但因为它太重要值得单独拿出来讲。我在早期做批量升级的时候编排文件里第一步是停止应用第二步是备份数据第三步才是升级包。当时第一步stop-app由于目标匹配错误实际上根本没有匹配到任何机器但它返回的是unchanged这种中性状态不是失败。结果后续的备份、升级全部在应用还在运行的情况下执行备份出来的数据完全不可用。我盯着编排结果看了半天还觉得每一步都成功了直到第二天业务方反馈数据有问题才反应过来。这里的根因有两个一是目标匹配写错导致空跑二是编排状态里unchanged不等于正常需要人工确认。我现在会在编排第一步后面加一个检查步骤专门确认目标机器数量符合预期check-target-count: salt.function: - tgt: role:web - tgt_type: grain - name: test.ping - failhard: Truetest.ping如果匹配不到机器会返回失败。再用failhard: True把这个检查设成关键节点一旦没有目标机器整个编排立刻终止避免后面白白执行一堆无效操作。这个习惯帮我挡住过好几次事故。8. 我总结的一套任务管理写法和流程最后梳理一下我目前最常用、也最稳定的一套任务管理实践给你做一个参考。任务分三类管理周期性任务比如清理临时文件、日志轮转、证书检查——用cron.present写进sls配合pillar管理时间和开关。一次性延迟任务比如半小时后重启服务——用schedule.present显式配once: True并在任务里加清理逻辑避免重启后重复触发。多机协同任务比如滚动更新、批量重启、跨机器依赖操作——用state.orchestrate写清楚requisite依赖关键步骤加failhard。每个任务在写完之后强制走四步检查第一步state.apply task testTrue确认渲染结果和执行计划正确。第二步检查目标匹配是否正确比如state.apply task testTrue返回的机器列表是不是你预期的那几台。第三步正式执行观察返回结果里的success和comment字段。第四步确认日志文件是否产生了预期内容没有就去看系统日志和盐的日志。任务命名上我用的是三段式业务名-动作名-类型。比如nginx-config-reload-job、mysql-backup-schedule、app-rolling-update-orch。命名清晰的好处是当你需要cron.absent或者排查任务冲突时一眼就能从ID里分辨出职责。环境变量和安全永远不要依赖任务运行环境自带的东西能写绝对路径就写绝对路径需要在任务里用的密钥通过pillar传别硬编码在sls里。备份先行凡是涉及数据迁移、服务重启、包升级这类有破坏性的任务我会在编排文件里强制加一个备份步骤并且用require把后面所有任务都挂在备份步骤之下。宁可多花几分钟备份也不要让整批机器陷入不可回滚的境地。Saltstack的任务管理归根到底做的是一件从人到机器的信任转移把执行计划写清楚、验证充分、交给系统去跑。定期检查任务是必要的但这份工作留给人的应该是指挥和决策而不是时刻盯着屏幕等结果。有了这套体系我能把精力放在更重要的业务架构上也建议你从手边最简单的定时清理任务开始先跑通一个完整的闭环慢慢再把编排、告警、结果收集都加进来。
特别提醒:考试时间、报名批次等安排如有调整,以河南省应急管理厅及官方考点最新通知为准。

最新新闻

看完这篇文章,想了解更多?

报考条件、材料清单、最近批次,顾问一次帮你理清,别自己摸索。