ARTICLE DETAIL

资讯详情

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

Sentinel告警接入企业微信钉钉:Webhook实时通知方案实践

Sentinel告警接入企业微信钉钉:Webhook实时通知方案实践 最近我花了一晚上把 Sentinel 的告警通知给接进了企业微信和钉钉群触发限流熔断的时候群机器人直接把资源名、异常类型、QPS、阈值这些关键信息全部抛出来再也不用盯着控制台刷监控了。这套东西本身不复杂核心就是 WebhookSentinel 负责触发事件应用侧捕获异常或监听规则变更组装成 Markdown 消息POST 到企业微信/钉钉机器人的地址上群里的值班同学第一时间就能看到。如果你手头的项目正在用 Sentinel又苦于限流被触发时没有实时感知这篇内容应该能直接给你省下不少排查时间。我会从机器人创建、Sentinel 客户端改造、消息封装、防抖和重试这几个环节一步步讲最后附上我踩过的几个坑和排查思路。1. 告警这件事值得单独折腾一下1.1 Sentinel 的默认能力离“告警”还差一步Sentinel 是阿里开源的流量治理组件主打限流、熔断降级和系统负载保护。它的默认行为很明确规则命中后抛BlockException同时会在本地打印sentinel-block.log日志实时状态也会在 Dashboard 的监控页面上展示。但这离真正的“告警”还有一段距离——日志是事后翻的控制台是主动去盯的线上应用真正被限流的时候开发和运维往往只能等用户投诉进来才知道出事了。我接手的几个核心服务都是 Sentinel 的老用户了限流规则配了一大堆但告警通知完全是空白。偶尔有接口被刷爆QPS 把阈值撞穿业务方反馈“怎么突然请求大量失败”我们才后知后觉地去查日志发现FlowException已经刷了上千行。这个反应链路实在太长所以这次下定决心把告警补齐。1.2 为什么要通过 Webhook 而不是把消息系统做重先说结论Webhook 是性价比最高的轻量方案。你只需要一个 HTTP POST 接口把 JSON 消息体推过去IM 群里的机器人就会帮你把内容展示出来。相比自建消息推送平台、接入短信服务或者搭建 Prometheus Alertmanager 全套监控链路Webhook 几乎零成本不引入额外存储也不用运维额外组件适合绝大多数中小团队和业务类系统。有人可能会说直接上 Prometheus Alertmanager 不是更专业确实如果你们已经有完整的可观测性体系那走 Alertmanager 或夜莺这类平台统一出告警是正道。但很多项目的体量并没有那么大硬塞一套监控体系进来反而增加维护成本。Sentinel 的告警通过 Webhook 直连企业微信/钉钉群属于“四两拨千斤”的做法规则命中即推送延迟基本在秒级够用且好维护。2. 先把两个 IM 的机器人 Webhook 摸透2.1 企业微信群机器人三分钟拿到 Webhook 地址企业微信的群机器人配置路径很直接进入目标群打开群设置找到“群机器人”点击“添加机器人”选一个新建或者用已有的就能拿到一个 Webhook 地址。地址格式类似https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key你的唯一标识这里要提醒两点。第一机器人名称建好之后是可以改的建议直接命名为“Sentinel告警机器人”这样在群里一眼能认出来。第二企业微信机器人的安全策略主要是“关键词”和“IP白名单”如果你在创建机器人时配置了关键词那么推送的文本或 Markdown 内容里必须包含至少一个关键词否则消息会被静默丢弃接口返回成功但群里永远看不到内容。这个坑我后面会展开说。发送消息时只需要以application/json的格式 POST 到上面这个地址。企业微信支持text、markdown、news等多种消息类型告警场景下用markdown最合适加粗、高亮、链接都能渲染信息层级一目了然。2.2 钉钉自定义机器人加签才是真正的门槛钉钉的入口在群里的“智能群助手” - “添加机器人” - “自定义”。创建时有三种安全设置自定义关键词、加签、IP地址段。关键词和 IP 限制跟企业微信类似而加签是钉钉特有的也是很多人第一次接就卡住的地方。加签的逻辑是把当前毫秒级时间戳加上换行符再加上机器人密钥拼成一个字符串然后用 HMAC-SHA256 算法计算出签名做 Base64 编码再对签名结果做 URL 编码最后把timestamp和sign拼到 Webhook 地址的 query 参数上。注意几个细节时间戳必须是毫秒级和 URL 上的timestamp参数保持一致钉钉会校验时间偏差超出后直接报sign not match。拼接格式是timestamp \n secret反了或者漏了换行符都会签名失败。签名做完之后必须URLEncoder.encode一下否则 Base64 里的、/、会让签名解析出问题。加签方式的 Webhook 地址类似https://oapi.dingtalk.com/robot/send?access_token你的tokentimestamp1700000000000signxxxxx2.3 Webhook 消息格式速查与差异对比下面是我实际封装消息时整理的对照表两家机器人虽然都是 Markdown 消息但字段名有些不同平台消息类型请求体结构返回成功标志企业微信markdown{msgtype: markdown, markdown: {content: ...}}{errcode:0,errmsg:ok}钉钉markdown{msgtype: markdown, markdown: {title: ..., text: ...}}{errcode:0,errmsg:ok}这里最容易踩的坑是企业微信的 Markdown 字段叫content钉钉的 Markdown 字段叫text并且钉钉还要求有一个title字段。如果你把企业微信的请求体原封不动搬到钉钉接口会返回错误群里面什么也不显示。另外两家对 Markdown 语法支持也有细微区别比如企业微信对所有人的写法支持得更纯粹钉钉在 Markdown 文本里需要特定语法所以我建议告警内容里不要依赖 功能直接靠内容本身说清楚问题更稳妥。3. Sentinel 告警方案设计从触发到通知的全链路3.1 方案一业务代码里手动处理 BlockException最朴素的做法是在业务代码捕获BlockException。用 Sentinel 的原生 APItry (Entry entry SphU.entry(queryOrder)) { // 业务逻辑 return orderService.query(...); } catch (BlockException ex) { notifyClient.send(ex, queryOrder); return fallbackResult(); }这样说白了就是把告警逻辑硬编码在业务方法里。优点是简单直接但缺点也很明显项目里受 Sentinel 保护的资源可能有几十上百个每个都手动 catch代码会非常啰嗦而且很容易漏掉某几个资源的告警。我只建议在个别核心接口上临时用这种方式不适合作为统一方案。3.2 方案二用 AOP 切面统一拦截推荐如果项目中大量使用了SentinelResource注解那么用一个 AOP 切面把资源方法的调用包起来捕获到异常时统一发告警是目前侵入性最小、扩展性最好的方式。切面代码如下Aspect Component public class SentinelAlertAspect { private final WebhookNotifier webhookNotifier; public SentinelAlertAspect(WebhookNotifier webhookNotifier) { this.webhookNotifier webhookNotifier; } Around(annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)) public Object handleSentinelBlock(ProceedingJoinPoint joinPoint) throws Throwable { try { return joinPoint.proceed(); } catch (BlockException e) { MethodSignature signature (MethodSignature) joinPoint.getSignature(); String resource signature.getName(); webhookNotifier.send(buildMessage(e, resource)); throw e; } } }这里有几个点需要注意。第一切点表达式中的annotation写法要求方法上明确加了SentinelResource注解如果你的团队习惯用SphU.entry原生 API切面就帮不上忙需要回到方案一或者做一层自定义封装。第二切面捕获到BlockException之后必须重新抛出去否则会破坏 Sentinel 原本的降级语义。第三理论上joinPoint.proceed()抛出的异常不一定是BlockException所以 catch 范围要精确别把业务异常也当成限流告警发出去。我推荐这个方案还有一个原因告警逻辑完全收敛在切面里后续想调整消息模板、加静默期、做告警聚合都只需要改这一个类其他业务代码完全不用动。3.3 方案三监听规则变更把“动作”也通知出去限流本身会触发告警但规则被人改了之后最好也能在群里留个痕。Sentinel 的规则加载机制支持注册监听器当规则通过控制台或数据源推送到客户端时监听器会收到回调。利用这一点可以把规则变更记录一起推到群里。下面给出一段监听流控规则变更的代码示例Component public class FlowRuleChangeListener implements InitializingBean { Override public void afterPropertiesSet() { FlowRuleManager.getProperty().addListener(new PropertyListenerListFlowRule() { Override public void configLoad(ListFlowRule value) { notify(规则加载, value); } Override public void configUpdate(ListFlowRule value) { notify(规则更新, value); } }); } private void notify(String action, ListFlowRule rules) { // 组装规则数量、资源名和阈值推送到 Webhook } }这段代码看起来不复杂却是我后来在运维中最依赖的一个功能。因为限流规则经常被临时调整如果没有变更留痕过几天根本想不起来哪个资源当时为什么放宽了阈值。规则变更提醒和限流触发提醒配合起来告警群里才能还原完整的“发生了什么”和“为什么发生”。3.4 为什么不建议走 Sentinel 日志采集这条路有的人不想动 Java 代码想着 Sentinel 本来会写sentinel-block.log日志那我用 Filebeat 采集日志转发到 Logstash 再解析最后调用 Webhook不是更解耦吗理论上完全可行但我不建议一上来就这么搞。原因有三第一日志解析有延迟尤其是大流量秒杀场景下日志文件瞬间膨胀采集器处理不过来告警会严重滞后第二告警需要的是结构化数据日志里虽然包含了时间、资源名、异常类型但每次推送前你还得清洗字段链路越长越容易出差错第三直接抓日志拿不到“哪些规则被变更”这类控制面事件。所以日志采集方案更适合那些有现成日志平台、实在不愿意动代码的团队而不是通用首选。4. Webhook 通知模块的完整实现4.1 配置文件与实体为了让通知模块不写死在代码里我用 Spring Boot 的配置文件管理各环境的机器人地址和密钥。以钉钉为例alert: dingtalk: webhook: https://oapi.dingtalk.com/robot/send?access_tokenxxx secret: SECxxxxxx enabled: true wecom: webhook: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxx enabled: true对应的配置实体Component ConfigurationProperties(prefix alert) Data public class AlertProperties { private DingTalk dingtalk new DingTalk(); private WeCom wecom new WeCom(); Data public static class DingTalk { private String webhook; private String secret; private boolean enabled; } Data public static class WeCom { private String webhook; private boolean enabled; } }用ConfigurationProperties的好处是后续加字段不需要改注入点而且多个环境可以配不同的值。生产环境的 Webhook 地址属于敏感信息建议放在环境变量或配置中心里不要直接提交到 Git 仓库这个我会在最后的安全部分再提。4.2 企业微信/钉钉通知发送器实现发送器的核心就是构建 JSON 字符串、发送 HTTP POST、解析返回结果。我直接用 Spring 的RestTemplate实现并发场景下单例够用。先看企业微信发送器Component public class WeComNotifier { Resource private AlertProperties alertProperties; public void send(String markdownContent) { if (!alertProperties.getWecom().isEnabled()) { return; } MapString, Object message new HashMap(); message.put(msgtype, markdown); MapString, String markdown new HashMap(); markdown.put(content, markdownContent); message.put(markdown, markdown); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityMapString, Object entity new HttpEntity(message, headers); // 企业微信机器人每分钟限制20条失败重试只做2次 try { String body restTemplate.postForObject(alertProperties.getWecom().getWebhook(), entity, String.class); log.info(wecom alert response: {}, body); } catch (Exception e) { log.error(wecom alert failed, e); } } }钉钉发送器需要额外完成加签逻辑Component public class DingTalkNotifier { Resource private AlertProperties alertProperties; public void send(String markdownContent) { if (!alertProperties.getDingtalk().isEnabled()) { return; } MapString, Object message new HashMap(); message.put(msgtype, markdown); MapString, String markdown new HashMap(); markdown.put(title, Sentinel 告警); markdown.put(text, markdownContent); message.put(markdown, markdown); String webhookUrl buildSignedUrl(alertProperties.getDingtalk().getWebhook(), alertProperties.getDingtalk().getSecret()); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityMapString, Object entity new HttpEntity(message, headers); try { String body restTemplate.postForObject(webhookUrl, entity, String.class); log.info(dingtalk alert response: {}, body); } catch (Exception e) { log.error(dingtalk alert failed, e); } } private String buildSignedUrl(String webhook, String secret) { Long timestamp System.currentTimeMillis(); String stringToSign timestamp \n secret; try { Mac mac Mac.getInstance(HmacSHA256); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HmacSHA256)); byte[] signData mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); String sign URLEncoder.encode(Base64.getEncoder().encodeToString(signData), UTF-8); return String.format(%s%stimestamp%dsign%s, webhook, webhook.contains(?) ? : ?, timestamp, sign); } catch (Exception e) { throw new IllegalStateException(dingtalk sign failed, e); } } }拼接 URL 的时候要用contains(?)判断连接符号我见过有人写死而 access_token 的 URL 本身带?写死就翻车了。4.3 消息模板设计群里一眼能看懂发生了什么告警消息最忌讳光秃秃一行字“接口被限流了”。我打磨后的模板长这样### Sentinel 告警 - **应用名**: order-service - **环境**: prod - **资源**: queryOrder - **异常类型**: 流控 FlowException - **当前QPS**: 325 - **配置阈值**: 100 - **触发时间**: 2024-12-20 14:32:11 请检查该接口是否存在异常流量或确认是否需要临时调整规则。这个模板在企业微信和钉钉里都是标准的 Markdown 渲染关键信息用加粗标出便于值班同学第一时间扫到重点。实现时不需要把模板硬编码在代码里可以用字符串模板拼接public AlertMessage buildMessage(BlockException ex, String resource) { long count FlowRuleManager.getFlowRules().stream() .filter(r - r.getResource().equals(resource)) .mapToLong(r - r.getCount().longValue()) .findFirst().orElse(0L); return String.format(### Sentinel 告警\n\n- **应用名**: %s\n- **资源**: %s\n- **异常类型**: %s\n- **配置阈值**: %d\n- **触发时间**: %s, appName, resource, ex.getClass().getSimpleName(), count, now()); }这里要注意把应用名、环境这类基础信息放在配置里不要每个告警消息里写死。不同环境用同一个告警群时环境字段非常关键否则一个测试环境的误报能骗走所有人。4.4 异步发送、重试与防抖告警发送不能阻塞业务线程。如果 Webhook 网络超时一两秒接口响应可能因此变慢所以发送动作一定要异步化。我用了 Spring 的Async注解配一个独立线程池Async(alertExecutor) public void sendAlertAsync(AlertMessage message) { notifier.send(message); }线程池参数不用很大核心线程 2、最大线程 4、队列容量 1000 足够告警消息量通常不大但峰值时不能拖垮业务线程。重试逻辑我做得比较简单发送失败后延迟 1 秒重试一次最多重试两次再失败就只写日志。因为告警消息本身就强调实时性超过 3 秒再发积累的意义就小了很多。真正需要重点防的是告警风暴。假设某个接口突然被流量打爆Sentinel 熔断后大概率是持续一段时间内每个请求都触发异常如果不做限制机器人会把群消息刷到爆炸。因此我在通知模块里加了一个按资源维度的全局防抖同一个资源在 60 秒内只发一条告警。private final ConcurrentHashMapString, Long lastNotifyTime new ConcurrentHashMap(); private boolean shouldNotify(String resource) { long now System.currentTimeMillis(); Long last lastNotifyTime.putIfAbsent(resource, now); if (last null || now - last 60_000L) { lastNotifyTime.put(resource, now); return true; } return false; }60 秒的静默期是试出来比较舒服的节奏既能感知问题存在又不会刷屏。如果团队要求更严格可以把间隔缩小到 30 秒或者设计一个基于时间窗口的最大消息数。5. 实测记录模拟限流看群里怎么报警5.1 准备一个容易触发规则的测试接口我在本地起了一个 Spring Boot 服务引用了sentinel-core、sentinel-annotation-aspectj和sentinel-transport-simple-http为了方便直接用 Dashboard 人工配流控规则。业务接口很简单SentinelResource(value queryOrder) GetMapping(/api/demo) public String demo() { return ok; }然后在 Dashboard 里给queryOrder配置一条流控规则阈值设成 1也就是每秒最多 1 个请求超过的直接拦掉。阈值设这么低就是为了方便压测触发。5.2 压测触发限流观察告警消息我用ab模拟并发请求ab -n 200 -c 20 http://localhost:8080/api/demo200 个请求阈值 1必然大量触发限流。此时再看企业微信告警群已经收到了第一条 Markdown 消息资源名、异常类型、阈值、触发时间都在。过了 1 秒因为防抖生效不会再有第二条直到 60 秒静默期过后如果还在压测才会有下一条消息。而钉钉群也同时收到了同等内容的告警说明两个发送器都正常工作。实际测下来从请求触发BlockException到群里收到消息延迟基本在 300 到 500 毫秒内。这中间主要开销是 HTTP POST 的建连和消息渲染对于告警来说已经足够实时。5.3 消息没到我把排查过程也记录下来第一次测试的时候企业微信群里怎么等都收不到消息但接口返回的errcode确实是 0。这个现象很容易迷惑人。后来我才意识到企业微信创建的机器人如果勾选了“关键词”发送内容必须包含对应关键词。我当时填的关键词是“告警”可是测试时模板里写的是“Sentinel Alert”一个关键词都不匹配于是消息被静默丢弃。改成在消息里强制拼接“告警”两个字之后就正常了。钉钉那边也出过一次问题现象是接口返回errcode: 310000提示sign not match。排查后发现是服务器时间比真实时间快了一分多钟导致时间戳校验不通过。解决方法是让容器走 NTP 时间同步然后在代码里再做一次时钟偏移补偿。如果你们服务跑在虚拟机或 Docker 里非常建议先检查时间同步。6. 常见问题与避坑清单6.1 机器人不推送的常见原因我整理了一个速查表按排查顺序排好了遇到“群里没消息”直接对照着试现象可能原因排查和解决接口返回 errcode0但群里无消息企业微信机器人配置了关键词消息不含关键词在群机器人设置里确认关键词在消息模板中强制带上钉钉返回 sign not match加签时间戳不一致或签名算法写错检查服务器时间替换为真实时间戳后重新生成签名返回 errcode 非 0Webhook 地址被篡改或拼接错误打印完整 URL 检查 query 参数是否重复或缺失群里偶尔有消息经常没有触发了机器人每分钟 20 条限流降低发送频率加重防抖逻辑按资源聚合消息消息内容显示乱码Markdown 字段混用了双平台格式检查是不是把钉钉的 text 写到了企业微信的 content 上6.2 告警风暴怎么控制告警风暴是我接手后最先解决的问题之一。一开始没有防抖逻辑瞬间的流量高峰直接把群里刷了上百条消息手机震个不停。后来加了按资源 60 秒静默和滑动窗口统计问题才缓解下来。如果你有多个服务共用一个告警群建议把服务名或者环境作为第一级维度的防抖 key再加资源名作为第二级维度这样“同一个服务的同一个资源”在窗口期内只发一条其他资源不受影响。另外一个有效手段是给不同级别告警设置不同告警频率限流。比如规则变更通知永远实时发而限流触发通知 60 秒最多一条做到“百川入海各有阀门”群里才能真正安静下来。6.3 内网环境访问公网 Webhook 的问题有些公司的服务部署在封闭内网不能直接访问公网 Webhook 地址。这种情况下需要确认是否有统一的 HTTP 出口代理。我的做法是把代理地址配到RestTemplate的SimpleClientHttpRequestFactory里这样应用就可以通过代理转发到企业微信或钉钉。如果公司链路管控更严格建议走内部消息网关由网关统一转发到外网这样密钥也不用下发到每个服务上。6.4 Webhook 地址泄露了怎么办Webhook 地址本质上等于一个可以向群内发消息的凭证。一旦泄露到外部别人就可以恶意往你们告警群里灌垃圾消息。所以我有几个习惯机器人能设 IP 白名单就一定设密钥和 Webhook 地址不要提交到 Git 仓库运维通过环境变量注入如果发现群里出现不明消息第一时间去群机器人设置里删除并重建机器人。钉钉的 secret 和 access_token 也需要一并对换只换一个等于没换。另外从代码层面说日志输出时不要打印完整 Webhook 地址和签名避免日志平台泄露。我曾经在 Debug 日志里打过一次签名 URL直接被日志采集系统收集到了 ELK后来整改时才清掉这个教训还是挺深刻的。这个告警体系上线之后我再也没有在半夜被用户反馈叫醒过。真要给这套方案排个优先级我的建议是先做 AOP 统一捕获限流异常再做防抖和规则变更通知最后才是优化消息模板和对接第二个 IM 平台。Sentinel 本身已经解决了“保护”的问题Webhook 解决了“感知”的问题两者配合起来限流这件事才真正闭环了。
返回列表