ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Shell+Curl 调用短信 API:运维告警零依赖短信发送实战

Shell+Curl 调用短信 API:运维告警零依赖短信发送实战 凌晨两点被电话吵醒原因是某台服务器的磁盘写满了但监控平台设置的告警规则漏掉了这个分区日志直接在系统盘里涨到 100%网站白屏了半小时才被值班同事发现。那台机器上装的是精简版 Linux没有 Python3没有 PHP连 wget 都要看运气唯一确定存在的是 bash 和 curl。也就是从那次以后我彻底不再纠结“短信接口该怎么接”直接在 Shell 脚本里用 Curl 调用短信 API把这一套做成了所有临时告警场景的首选方案。这条路线对运维、对做自动化脚本的人、对需要在无图形环境里快速发通知的开发者都有参考价值。你不需要装任何第三方 SDK不需要编译不需要解决依赖地狱只要系统里有 bash 和 curl再拿一份短信平台给的 API 文档就能写出发短信的函数并集成到监控脚本、定时任务、部署脚本里。这篇内容就从选型逻辑讲起到真实可用的脚本再到我实际踩过的那些发送失败的坑一次性把“Shell Curl 调短信 API”这件事讲透。1. 为什么短信告警我首选 Shell Curl 而不是“正规军” SDK先聊点掏心窝的话。很多人一听“短信接口”第一反应是去装云厂商的官方 SDKPython 有Java 有Go 也有。但我这些年做运维和自动化脚本真实的感受是在告警场景里Shell Curl 赢在“可达性”。你要知道企业内网的机器并不都是配置齐整的。我碰到过跳板机只有最小化 CentOS 系统装 Python3 得找运维审批、过安全合规、甚至要配内部 pip 源一来一回半天就没了而告警这种东西往往是你半夜就要用的。SDK 的引入还带着一个隐藏成本——它能帮你处理签名和请求封装但它自身也有版本兼容问题尤其在老系统上OpenSSL 版本不匹配、glibc 太旧导致装不上哪个都能卡你一下。Curl 就不一样几乎每个 Linux 发行版都预装十几年老机器上也有。然后是最重要的排错效率。Curl 可以在命令行里天然调试一条curl -v就能看到 HTTP 请求的完整链路——DNS、TCP 握手、TLS 握手、发送 header、接收 body每一步都摆在眼前。SDK 一旦发不出去你要么翻源码要么开 debug 日志效率和命令行直接试完全是两个级别。当然我不是说 SDK 一无是处。如果你的业务场景是用户触发的高并发短信验证码每秒几百上千 QPS那必须用厂商 SDK甚至要上连接池和异步如果你只需要在磁盘告警、网站宕机、备份失败时收到一条短信通知一天最多几十条那你为 SDK 付出的所有环境成本都在浪费。告警场景的核心是“极简、可靠、立刻能跑”Shell Curl 恰好全部命中。2. 第一个能跑的脚本简版 REST 接口怎么用 Curl 调通2.1 拿到 API 文档后先盯四个要素接入一个短信平台我不管它的文档写得多花哨先找四样东西接口 URL、鉴权方式、Content-Type 要求、请求体参数。举个例子很多聚合短信平台提供的是非常传统的 REST 接口形如POST https://api.example-sms.com/v1/sms/send Header: Content-Type: application/json Authorization: Bearer YourAccessToken Body: { phone: 13800138000, templateId: SMS_123456, params: {\name\:\disk\} }这类接口人类友好度极高本身就是给 HTTP 客户端设计的用 Curl 调用几乎没有理解成本。2.2 一个直接可用的 send_sms 函数基于这种接口我会把发送逻辑封装成一个 Shell 函数放到所有脚本里都能source。函数设计有四个要点token 用全局变量而不是硬编码在函数里方便后续从配置文件加载必填参数做校验避免空手机号白花短信费curl 必须设置连接超时和总超时不能让脚本卡死最后打印 HTTP 状态码和接口返回体方便排查。代码我用的是长下面这样#!/usr/bin/env bash # 短信平台相关配置建议从外部配置文件加载 SMS_API_URLhttps://api.example-sms.com/v1/sms/send SMS_API_TOKENYourAccessToken send_sms() { local phone$1 local template_id$2 local params$3 if [[ -z $phone || -z $template_id ]]; then echo [ERROR] phone and template_id are required. 2 return 1 fi # 构造 JSON注意 params 本身是 JSON 字符串 local payload payload$(printf {phone:%s,templateId:%s,params:%s} \ $phone $template_id $params) local http_code local resp_body http_code$(curl -sS \ --connect-timeout 5 \ --max-time 10 \ -X POST \ -H Content-Type: application/json \ -H Authorization: Bearer ${SMS_API_TOKEN} \ -d $payload \ -w %{http_code} \ -o /tmp/sms_resp.$$ \ $SMS_API_URL 2/tmp/sms_curl_err.$$) local curl_rc$? resp_body$(cat /tmp/sms_resp.$$ 2/dev/null) if [[ $curl_rc -ne 0 ]]; then echo [ERROR] curl failed with code $curl_rc: $(cat /tmp/sms_curl_err.$$) 2 rm -f /tmp/sms_resp.$$ /tmp/sms_curl_err.$$ return 2 fi rm -f /tmp/sms_resp.$$ /tmp/sms_curl_err.$$ if [[ $http_code ! 200 ]]; then echo [ERROR] HTTP $http_code, body: $resp_body 2 return 3 fi echo [INFO] sms sent. http$http_code body$resp_body return 0 }拆解几个容易被忽略的点。-sS是静默模式加错误显示。只用-s的话curl 在 DNS 失败、连接被拒时不会输出任何错误你只能看到返回码非 0查起来全靠猜加上-S后错误会打到 stderr。--connect-timeout 5和--max-time 10是告警脚本的命根子——不设这两个参数的话curl 默认会傻等系统 TCP 超时那个时间是分钟级的告警链路会因此延迟很久。-w %{http_code}把 HTTP 状态码附加到输出-o把响应体写文件两者分拆后才能做到”我用状态码做判断、用响应体做日志“而不是混在一起再用正则抠。2.3 模板参数是 JSON 字符串最容易翻车的点上面函数里我特意把params定义为字符串因为它本身要往大 JSON 的“params”字段里塞。这里有个经典问题如果 params 里含中文比如{name:数据库}直接拼printf出来的 JSON 是合法 UTF-8绝大多数平台能正确解析。但如果 params 里含双引号或反斜杠就需要做转义。别用 sed 手工转——太脆。你可以在调用前用python3 -c import json,sys; print(json.dumps(sys.argv[1])) $raw_params转一次前提是机器上有 python3没有的话写一个最小化的转义函数把变成\把\变成\\把换行变成\n够用。顺带说一句对“params 传 JSON 字符串”这个设计我见过很多新手误把 JSON 直接铺平到根层级比如{phone:...,name:数据库}平台会报“模板参数缺失”。调试的时候看返回体就明白了但能提前知道的话能少走一段弯路。2.4 命令行五分钟验证法不要一上来就写完整脚本先确认接口本身能通。我用的是这个三步流程# 1. 用一条最简 curl 测接口连通性 curl -v --connect-timeout 5 --max-time 10 -X POST \ -H Content-Type: application/json \ -H Authorization: Bearer YourAccessToken \ -d {phone:13800138000,templateId:SMS_123456,params:{\name\:\test\}} \ https://api.example-sms.com/v1/sms/send # 2. 确认返回内容里业务 code 是否为成功值 # 3. 手机号收到测试短信说明全链路 OK命令行这一步能直接暴露 80% 的问题域名通不通、token 对不对、Content-Type 有没有按要求写、JSON 是否畸形。全部确认完再把命令封装成函数避免“函数写得很漂亮但底层就是个坏接口”的情况。2.5 嵌进监控脚本的真实例子函数写好后实战价值立刻体现。比如磁盘告警我可以写成一个 30 行的脚本#!/usr/bin/env bash source /etc/sms_notify/send_sms.sh THRESHOLD85 CURRENT$(df / | awk NR2 {print $5} | tr -d %) if [[ $CURRENT -ge $THRESHOLD ]]; then send_sms 13800138000 SMS_DISK_WARN {\partition\:\/\,\usage\:\$CURRENT\} fi这没什么高深的但胜在直观、低耦合。你要做网站异常检测、SSL 证书到期提醒、数据库备份完成通知全部是同一个套路检查条件满足后调用send_sms。3. 面对阿里云风格的 RPC 签名接口纯 Shell 怎么算签名3.1 RPC 接口为什么让 Shell 选手头疼现实中叫“阿里云短信 api 发不出去”的情况极多。阿里云短信发送走的是 RPC 风格接口核心流程是把所有请求参数按字典序排序拼接成规范化请求串然后用 AccessKey Secret 做 HMAC-SHA1 签名最后把签名放到请求参数里发出去。这套东西用 Python/Java SDK 封装后很简单但在纯 Shell 里要手动做 URL 编码、HMAC 计算、Base64 编码很多脚本看起来就头大。好在 Linux 自带的openssl命令可以完成 HMAC 和 Base64curl -G可以帮忙做 URL 参数拼接整体可行。3.2 用 openssl 造一个签名函数这里给出一个纯 Shell 的计算签名版本。核心思路是先准备好待签名字符串用openssl dgst -sha1 -hmac做 HMAC再用base64编码输出。注意阿里云早期 RPC 签名算法用的是 HMAC-SHA1如果你用的是新版 SDK 或按SignatureMethodHMAC-SHA256传参把-sha1换成-sha256就行。#!/usr/bin/env bash # 阿里云短信配置 ALIYUN_ACCESS_KEY_IDLTAI5tXXXXXXX ALIYUN_ACCESS_KEY_SECRETyour_secret ALIYUN_SIGN_NAME阿里云短信测试 # 短信签名 ALIYUN_TEMPLATE_CODESMS_123456 # 模板CODE ALIYUN_ENDPOINTdysmsapi.aliyuncs.com urlencode() { # 用 printf 的 % 转义注意保留字母数字和部分安全字符 local s$1 s$(printf %s $s | sed -e s/%/%25/g -e s/ /%20/g \ -e s//%2B/g -e s/\//%2F/g -e s/?/%3F/g -e s/#/%23/g \ -e s//%26/g -e s//%3D/g -e s//%40/g -e s/:/%3A/g) printf %s $s } aliyun_sms_signature() { local params_ordered$1 # 已经按 Key 字典序排列的 query string local sign_str sign_str$(printf POST%%2F%s $(urlencode $params_ordered)) printf %s $sign_str | openssl dgst -sha1 -hmac $ALIYUN_ACCESS_KEY_SECRET -binary | base64 }这里有个关键细节签名用的 Key 是AccessKeySecret 很多人都漏掉后面的 。另外阿里云要求对规范化请求串里的参数再次 URL 编码后拼进待签名字符串所以我在aliyun_sms_signature里对params_ordered又做了一次urlencode。3.3 用 curl -G 规避手写 URL 编码RPC 接口所有参数都是 query string如果直接拼 URL一个个手动编码很容易错。更好的做法是让 curl 自己编码——用-G配合多个--data-urlencodecurl 会帮你完成 URL 编码和参数拼接。以短信发送为例公共参数 业务参数的完整调用长这样build_aliyun_sms_request() { local phone$1 local template_param$2 # 例如 {name:Oracle} # 生成时间戳和随机数 local timestamp local nonce timestamp$(date -u %Y-%m-%dT%H:%M:%SZ) nonce$(date %s%N) local common_params( AccessKeyId$ALIYUN_ACCESS_KEY_ID ActionSendSms FormatJSON RegionIdcn-hangzhou SignatureMethodHMAC-SHA1 SignatureNonce$nonce SignatureVersion1.0 Timestamp$timestamp Version2017-05-25 PhoneNumbers$phone SignName$ALIYUN_SIGN_NAME TemplateCode$ALIYUN_TEMPLATE_CODE TemplateParam$template_param ) # 按字典序排序参数按 Key 排序也就是“”左边的字符串 local sorted_params sorted_params$(printf %s\n ${common_params[]} | sort) # 拼接成 keyvaluekeyvalue local query_string local item for item in $sorted_params; do if [[ -z $query_string ]]; then query_string$item else query_string$query_string$item fi done local signature signature$(aliyun_sms_signature $query_string) # 用 curl -G 构造最终请求--data-urlencode 会自动编码 curl -sS \ --connect-timeout 5 \ --max-time 10 \ -G \ --data-urlencode $query_string \ --data-urlencode Signature$signature \ https://$ALIYUN_ENDPOINT/ }curl -G的好处是你给它一个带的字符串它不会拆分而是把整串当作一个 keyvalue 对当多个--data-urlencode同时存在时curl 会用连接它们。这样签名里已经编码过的query_string和Signature参数都交给 curl 处理不需要手写完整的 URL。这个方法比我以前傻乎乎拼 URL 稳太多了。3.4 最小可运行样例手机号 模板 签名的完整测试整个逻辑串起来一次真实发送只需要三步先验四个公共参数的状态时间戳是不是 UTC、SignatureNonce 是否唯一再拼接排序签名最后 curl 发请求。通常返回 JSON 里会有一个Code字段OK表示成功像isv.SMS_SIGNATURE_ILLEGAL这种说明签名或模板审核没通过。3.5 壳层里的隐藏细节Timestamp必须是 UTC 时间date -u %Y-%m-%dT%H:%M:%SZ不少脚本死在这里。SignatureNonce每次请求必须唯一我用date %s%N生成纳秒时间戳重复概率极低。参数排序不是整体按行排序而是按“参数名”排序我上面的sort默认按整行排序刚好够用因为参数名AccessKeyId、Action这些首字母已经决定了相对顺序但如果你有A1和A10这种需要精确时建议用sort -t -k1,1限定按 key 排序。千万别在 TemplateParam 这个 JSON 里带多余空格签名串会含空格然后被编码成%20实际发送时会解析不出来。4. 脚本上线前的改造超时、重试、去重和日志一个都不能少4.1 告警脚本最怕“僵尸卡住”很多初版脚本是直接用上面最简单的函数跑一段时间后你就发现问题磁盘满了脚本调用 curl 发短信但网络抖动导致 curl 一直卡在等待响应告警没发出去脚本本身还占用了一个进程。所以我在所有 curl 调用里强制加两个参数--connect-timeout 5TCP 连接建立超时 5 秒和--max-time 10整个请求最多 10 秒。宁可偶尔因为超时漏发也绝不让脚本变成僵尸进程拖垮监控进程。4.2 --retry 到底用不用Curl 内置的--retry 3 --retry-delay 2看起来很香但用在短信接口上要非常小心。短信不是幂等操作——第二次调用就意味着再发一条短信如果接口其实已收到请求并成功下发只是响应在回程时超时你重试一次用户就收到两条同样的短信。对验证码场景这是灾难对告警场景虽然没那么严重但短信是要花钱的。我的策略是重试只针对“连接层面”的错误不重试“HTTP 层”的错误。具体做法很简单——第一次请求返回非 0 或者 HTTP 500/502/503 时sleep 2 秒后再发一次最多三次HTTP 200 但业务 code 失败则直接报警绝不重试。代码上就是在函数外面套一个 for 循环。4.3 HTTP 200 不等于发送成功这是最容易误导人的地方。许多短信平台即使在业务失败时也返回 HTTP 200因为 HTTP 层是通的业务错误被包在响应体的 code 字段里。比如阿里云返回{Message:InvalidTimeStamp.Expired,Code:InvalidTimeStamp.Expired}HTTP 状态码还是 200。所以我在判断成功与否时必须同时检查 HTTP 状态码和业务 code 字段。拿阿里云来说case $resp_body in *Code:OK) echo [INFO] send ok ;; *) echo [WARN] send failed: $resp_body ;; esac别用 grep 找OK就完事因为可能消息内容是NotOK要判断的是Code:OK这种精确片段。4.4 幂等去重避免告警风暴告警脚本第一次接入短信之后最常见的副作用就是告警风暴——磁盘使用率在阈值上下波动监控每五分钟跑一次一个小时内你能收到十几条“85% 了”“83% 了”“86% 了”。解决方案有两个层次在监控侧做当前状态没恢复就不重复发。脚本里维护一个状态文件比如/var/run/sms_disk_warn.lock当 usage 85 时如果 lock 文件存在就不再发送只有 usage 降到 80% 以下或脚本重启时删除 lock。在通知侧做对相同内容做 15 分钟窗口内的去重我一般用 Redis但 Shell 脚本里最简单的方案是记录上次发送时间比较时间差。4.5 日志留痕是事后追责的唯一依据短信发没发、发了几次、平台返回什么这事必须留日志。我会在 send_sms 函数里统一打一行结构化日志echo $(date %Y-%m-%d %H:%M:%S) [phone$phone][template$template_id][http$http_code][curl_rc$curl_rc][body$resp_body]写到/var/log/sms_sender.log这样哪天用户说“我没收到短信”你能直接翻出当时的请求和响应。比用户描述“好像有个短信但被我删了”靠谱一百倍。5. 线上排查实录短信发不出去的时候我按什么顺序查5.1 先看返回码再抓原始响应短信发不出去我很少直接怀疑平台挂了通常是我自己的问题。排查顺序是先看脚本日志里记录的 curl 退出码和 HTTP 状态码然后手工用curl -v跑一遍同样的命令抓原始响应。curl 退出码是线索宝库比如 6 是 couldnt resolve host7 是 failed to connect28 是 timeout35 是 SSL connect error。看到 28先查网络和防火墙看到 35查 TLS 版本和证书链。5.2 案例curl (56) Recv failure - Connection reset by peer我遇到过的典型场景是脚本在客户机房跑防火墙开了 80/443 出方向但实际访问短信平台域名时被中间设备干扰TCP 连接被重置curl 报 56。这时候curl -v能看到Recv failure: Connection reset by peer。解法不是加超时而是查出口代理策略或者换一个短信平台接入域名有些平台同时提供 IP 直连域名用于内网环境。这类问题里--resolve参数很实用——你可以在测试时直接把域名解析到指定 IP排除 DNS 被污染的问题。5.3 案例curl (23) Failure writing output to destination这个报错大部分人看到会懵因为发送短信 curl 明明没写什么大文件。原因通常是 curl 的输出管道断开了——比如你-o到/dev/full、管道给了一个提前关闭的进程、或者磁盘满了导致临时文件写不进去。在告警场景里最容易触发的是-o /tmp/sms_resp.$$但/tmp挂载满了。这是个很好笑的死锁你要告警磁盘满结果 curl 往 /tmp 写响应时因为磁盘满而失败。解决方法是把响应体输出到内存缓存或直接丢弃-o /dev/null只保留 HTTP 状态码用于判断。5.4 案例模板变量格式错误导致 isv.SMS_PARAM_ERROR模板变量是最隐蔽的坑。比如你的短信模板是“您的{name}设备发生告警”接口要求 params 传入{name:Web服务器}。问题在于有些平台要求传入的字符串必须转义成{\name\:\Web\}有些平台则要求直接传{name:Web}。同一个 JSONA 平台能解析B 平台报参数不合法。我的经验是先按接口文档给的示例原样传通了之后再测带空格和中文的情况如果带中文失败试一下平台是否要求 URL 编码后的 JSON。5.5 案例手机号格式和号段校验有些平台会校验手机号的号段106开头、86前缀、400开头的号码都可能被拒。Shell 脚本里加一行简单的正则校验能拦截掉大部分低级错误if ! [[ $phone ~ ^1[3-9][0-9]{9}$ ]]; then echo [ERROR] invalid phone: $phone return 4 fi别觉得这是小题大做我见过因为配置文件里手机号多了一个空格告警发到“不存在号码”上直到月底账单炸掉。5.6 我自己的排查清单顺序检查项命令/方法1curl 退出码echo $?对照 curl 错误码表2DNS 能否解析域名getent hosts api.xxx.com3TCP 能否连通timeout 5 bash -c cat /dev/null /dev/tcp/api.xxx.com/4434HTTP 状态码curl -sS -o /dev/null -w %{http_code}5业务 code查看响应体 JSON 的 Code/Message6时间戳与秒级同步date -u与平台服务器时间比对7签名串算法临时在脚本打印待签名字符串与文档手工推演对比6. 进阶从一条短信到一个通知中枢6.1 短信失败自动降级到其他通道短信通道稳定性并非 100%平台偶尔也会故障更常见的是你账户余额耗尽导致发送失败。但告警不能断。所以我在 send_sms 失败时会自动降级到备用通道——飞书/钉钉 Webhook 或者一个简单的 Telegram Bot。逻辑就是发送失败后的 catch 分支# 短信失败后尝试发钉钉机器人群 if send_sms ...; then echo ok else send_dingtalk 磁盘告警: / 分区使用率 91% fiShell 函数可以做到通道无感切换使用者只要调notify 标题 内容内部先试短信、再试 webhook。多一层保险深夜被叫醒的概率能低不少。6.2 配置外置不把 AccessKey 硬编码在脚本里AccessKey 直接写进脚本是安全大忌。我现在的做法是把敏感配置放到/etc/sms_notify/sms.env权限设成 600脚本里用source /etc/sms_notify/sms.env加载再配合set -a和set a控制导出范围。这样脚本本身可以放心分发到多台机器密钥只留在一台配置机上。6.3 和监控系统打通这套 Shell 函数不是我发完短信就弃用的玩具我现在把它放在监控体系的正中间。Zabbix 的自定义媒介类型可以直接调 Shell 脚本Prometheus 的 Alertmanager 可以在 webhook 里用脚本转发Cron 里更是随手一挂就是一套定时巡检。所有需要“人最终知道”的事件最后都会穿过这个 send_sms 函数。它的价值不只是发一条短信而是给整个监控链路提供了一个最简单、最可靠的“最后一公里”。我在实际项目里就是这么做的每个项目目录下放一个notify.sh里面定义 send_sms 和 send_dingtalk 两个函数所有告警脚本统一 source 它。后来团队里有新人接手报警脚本不需要理解短信签名算法只需要知道调notify 分区满了 dev/sda1 91%就能把消息送出去。这就是 Shell Curl 这条路的真正回报——三分钟接入、零依赖、任何人都会改。
返回列表