Nova Kwok's Awesome Blog

Cloudflare Workers 初探——以 G2WW 作为例子转发 Grafana 报警到企业微信

在 2018 年 3 月 13 日,Cloudflare Blog 发布了一篇文章:「Everyone can now run JavaScript on Cloudflare with Workers」,标志着 Cloudflare 的 “Serverless” 平台的 “GA”,作为一个标榜 “Edge computing for everyone” 的产品,Cloudflare Workers 或许是我们能接触到的最 “亲民” 的 Serverless 产品。

什么是 Serverless

Wiki 对于 Serverless Computing 的解释是:

Serverless computing is a cloud computing execution model in which the cloud provider runs the server, and dynamically manages the allocation of machine resources. Pricing is based on the actual amount of resources consumed by an application, rather than on pre-purchased units of capacity.

我对于 Serverless 的理解是:

关于 Serverless ,个人认为一个最好的诠释应该是下图:

对于静态内容来说,可以直接通过 S3 (搭配 Cloudfront)进行输出,对于动态的请求来说,只需要自己写好业务逻辑,去请求后端的服务即可,然后,无需考虑自己服务器部署,维护等内容,且真正按量计费。

什么是 Cloudflare Workers

相比较传统的 Serverless 框架来说,Cloudflare Workers 更近了一步,传统的 Serverless 结构还是有一个中心化的部署(比如应用就部署在 us-west 区域),甚至某些厂商认为自己的 Managed Docker 也是 Serverless,非常迷惑,前面是服务商的 CDN,而 Cloudflare 的函数部分是全球化的,计算逻辑直接跑在边缘节点上,依托他们的全球 Anycast 网络。这一点类似 Amazon 的 Lambda@Edge

Cloudflare 对于自己的 Workers 是这么介绍的:

You write code. We handle the rest.

以及:

Deploy serverless code to data centers across 200 cities in 95 countries to give it exceptional performance and reliability.

相对比传统的业务逻辑,我们将应用部署在自己的服务器上,并使用 Cloudflare 进行 CDN 加速(或者减速),终端用户的所有请求(一般来说就是 HTTP 请求啦)会先通过 Cloudflare 的网络,然后再由 Cloudflare 走公网反向代理到自己的应用服务器上,等应用服务器完成处理后请求再通过 Cloudflare 的边缘节点重新返回给用户(这个过程称为:回源)。

这么一来有两个问题:

  1. 回源需要时间

对于一个源站在日本的服务器来说,如果一个欧洲用户访问,那么用户的访问请求会通过 Cloudflare 位于欧洲的服务器返回到日本的源站上,期间直接走公网,如果你对 Cloudflare 回源机制还不熟悉的话,可以参考我的博文「搭建 Cloudflare 背后的 IPv6 AnyCast 网络」,内部讲解了 Cloudflare 回源机制,以及 Cloudflare 的增值服务——Argo。

  1. 计算的压力全部落在了源站上

由于是一个中心化的源站(当然,如果你使用了负载均衡器等另说),来自不同地区的访客的计算压力会全部落在自己的架构上。

Cloudflare Workers 的出现可以解决以上两个问题,Workers 的工作模式是在 Cloudflare 的 200+ 个数据中心(边缘节点)上直接运行用户的 Javascript 代码,所有的计算全部在边缘进行,此外,为了保证 Stateful 的服务,Workers 还提供一个全球低延迟(利用 Cloudflare 内网)的 KV 键值对数据库用于边缘应用的读写。

除了这些以外,还有一些小特性值得关注:

Cloudflare Workers 效果

别的不多说,我们先给一个对比图,以下是 webp-sh/webp_server_go,的官网: https://webp.sh 的内容,我们观察两种不同的使用 Cloudflare 的方式:

Cloudflare 直接反向代理

结构如下,网站的静态内容放置在 GCP 的日本 Osaka,使用的 Premium 网络,即使用了「冷土豆路由」,保证服务器到访客之间尽可能多的在 Google 内网内,此外,前端加入 Cloudflare 反向代理。

我们从 ping 值会发现,由于 Cloudflare 有 Anycast,在海外部分 ping 的延迟符合 Cloudflare 的预期:

这是因为我们的 ICMP 包在 Cloudflare 的边缘节点就已经有了 echo,然而对于网页访问来说是 HTTP 的 GET 请求,这里评判用户看到网页速度的要素不只是 ping 值,还有 TTFB:

TTFB measures the duration from the user or client making an HTTP request to the first byte of the page being received by the client’s browser. This time is made up of the socket connection time, the time taken to send the HTTP request, and the time taken to get the first byte of the page. Although sometimes misunderstood as a post-DNS calculation, the original calculation of TTFB in networking always includes network latency in measuring the time it takes for a resource to begin loading.

可以看到,在日本地区 TTFB 最小(因为距离最短,日本访客 -> Cloudflare 日本节点 -> 日本源站),而在德国,TTFB 就大的多了。

Cloudflare Workers Site

这里直接使用了 Cloudflare Workers Site 的功能,将 webp.sh 的网页直接推到 Workers 的 KV 存储中,并使用一个临时域名进行展示,可以看到,由于直接从 Cloudflare 边缘节点进行输出(没有回源的开销),这个时候的 TTFB 就已经大幅度减少:

Cloudflare Workers 怎么用

相信看到上面的例子你应该已经大概明白了 Cloudflare Workers 是啥,以及它所能带来的好处了,现在我们看看 Cloudflare Workers 怎么用,可惜的是,目前在 Google 上搜索「Cloudflare Workers」相关除了看到 Sukka 的一篇「将 Hexo 部署到 Cloudflare Workers Site 上的趟坑记录」以外就只能看到大家都在用 EtherDream 写的 JSProxy 做代理翻墙…

我们来看看 Cloudflare Workers 怎么用吧。

安装 Wrangler

这一步其实比较简单,在安装好 NPM 后直接按照 Quick Start 安装就好:

npm install -g @cloudflare/wrangler

然后就可以 wrangler generate my-router-app 创建一个 APP 了,这里建议先通篇阅读一下上述的 Quick Start.

index.js

在安装好 APP 之后,你就有 开局一个 index.js 可以用了,内容一般是这样的:

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})
/**
 * Respond with hello worker text
 * @param {Request} request
 */
async function handleRequest(request) {
  return new Response('Hello worker!', {
    headers: { 'content-type': 'text/plain' },
  })
}

警告:由于我没有学过 Javascript ,以下纯属瞎掰,仅供娱乐使用

这里的 addEventListener 是整个程序的入口,目前 Cloudflare Workers 只支持 FetchEvent,这一种。

G2WW

好了,标题中我们提到了 G2WW,这里就以 G2WW 作为例子进行演示 Worker 的一个用法,我们知道 Grafana 的报警 Channel 中是没有「企业微信」的(但是有 DingDing,很奇怪),所以为了保证企业微信用户也可以通过 Grafana 进行报警,我们需要做一点改装。

企业微信的一个"接口"就是「机器人」,它支持 Webhook 调用,只需要按照他们的规范 POST 一个数据即可,他们的规范如下:

{
  "msgtype": "news",
  "news": {
    "articles": [
      {
        "title": "这里是标题",
        "description": "你的服务掉线了",
        "url": "https://status.xxx.xxx/grafana/xxx",
        "picurl": "https://kongbu.de.diaoxian.tupian/1.jpg"
      }
    ]
  }
}

Grafana 支持一个 Webhook 报警,Example 如下:

{
  "dashboardId":1,
  "evalMatches":[
    {
      "value":1,
      "metric":"Count",
      "tags":{}
    }
  ],
  "imageUrl":"https://grafana.com/static/assets/img/blog/mixed_styles.png",
  "message":"Notification Message",
  "orgId":1,
  "panelId":2,
  "ruleId":1,
  "ruleName":"Panel Title alert",
  "ruleUrl":"http://localhost:3000/d/hZ7BuVbWz/test-dashboard?fullscreen\u0026edit\u0026tab=alert\u0026panelId=2\u0026orgId=1",
  "state":"alerting",
  "tags":{
    "tag name":"tag value"
  },
  "title":"[Alerting] Panel Title alert"
}

那么我们的需求就很明确了,就是需要将 Grafana Webhook 的报警内容提取需要的字段并转发到企业微信的接口上即可,该怎么做呢?

首先我们需要区分请求是 GET 请求还是 POST 请求:

addEventListener('fetch', event => {
  const { request } = event
 if (request.method === 'POST') {
    return event.respondWith(postWeChatUrl(request))
  } else if (request.method === 'GET') {
    return event.respondWith(new Response(banner_content, {
      headers: { 'content-type': 'text/html' },
    }))
  }
})

对于 POST 请求,我们对内容进行处理,并转发到企业微信的 API 地址上:

async function postWeChatUrl(request) {
  var key = request.url.replace(/^https:\/\/.*?\//gi, "")

  // 这里我们拼接一下,对于访问 https://g2ww-serverless.nova.moe/xxxxxx-xxxxxx 的请求就自动发到 https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxx-xxxxxx 下
  var wechat_work_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + key

  var json_obj = await request.json()

  // 重新构造 JSON
  var template = 
  {
		"msgtype": "news",
		"news": {
		  "articles": [
			  {
				"title": json_obj['title'],
				"description": json_obj['message'],
				"url": json_obj['ruleUrl'],
				"picurl": json_obj['imageUrl']
			  }
		  ]
		}
  }

  const init = {
    body: JSON.stringify(template),
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
  }

  // 发出请求
  const response = await fetch(wechat_work_url, init)

  // 准备结果
  const results = await gatherResponse(response)
  return new Response(results, init)

}

最后处理一下返回值并返回给客户端就可以了:

async function gatherResponse(response) {
  const { headers } = response
  const contentType = headers.get('content-type') || ''
  if (contentType.includes('application/json')) {
    return JSON.stringify(await response.json())
  } else {
    return await response.text()
  }
}

然后一个 Serverless 的 G2WW 就这么做好了,非常简单是不是?

我们把代码推到 Cloudflare 上:

wrangler publish

然后配置一下 Grafana 的报警 Channel 并看看效果如何:

配置个地址,图片中使用了 https://g2ww.nova.moe/<key>,其实可以使用 https://g2ww-serverless.nova.moe/<key>,然后点一下 「Send Test」。

一个可用的成品 Demo + 使用方法: https://g2ww-serverless.nova.moe

当然,这个只是一个 Hack,最终还是希望 Grafana 可以直接原生支持企业微信,我已经给他们提了一个 PR Add a new notifier : WeChat Work(企業微信) #26109,希望他们能快点合并吧。

尾声

以上通过一个简单的例子大致走了一遍 Cloudflare Workers 的基本用法,当然,Cloudflare Workers 的玩法绝对不止这么点,尤其是在 KV 的加持下,一定会有更多的有意思的项目兴起,比如他们的「Built with Workers」就有很多的优秀的项目,从各个角度发觉 Cloudflare Workers 的用法。

想到之前在某论坛上看到的一句话:

想法太多,我们却很少停下来看看真正的需求是什么,并对自己的技能进行提升和学习,一直忙于低头走路,以至于越走越累了。

除了已有的项目以外,我们还能利用它来做点什么呢,我想这是一个值得思考的问题。

References

  1. Alert notifications | Grafana Labs
  2. Everyone can now run JavaScript on Cloudflare with Workers
  3. Cloudflare Workers
  4. FetchEvent
  5. Time to first byte

#Chinese #Cloudflare