AICon全球人工智能与机器学习技术大会9折特惠中,点击立减¥480! 了解详情
写点什么

Serverless 实战:通过 Serverless 架构实现监控告警

2020 年 4 月 01 日

Serverless 实战:通过 Serverless 架构实现监控告警

在实际生产中,我们经常需要做一些监控脚本来监控网站服务或者 API 服务是否可用。传统的方法是使用网站监控平台(例如 DNSPod 监控、360 网站服务监控,以及阿里云监控等),它们的原理是通过用户自己设置要监控的服务地址和监测的时间阈值,由监控平台定期发起请求对网站或服务的可用性进行判断。


这些方法很大众化,通用性很强,但也不是所有场景都适合。例如,如果我们的需求是监控网站状态码,不同区域的延时,并且通过监控得到的数据,设定一个阈值,一旦超过阈值就通过邮件等进行统治告警,目前大部分的监控平台是很难满足这些需求的,这时就需要定制开发一个监控工具。


Serverless 服务的一个重要应用场景就是运维、监控与告警,所以本文将会通过现有的 Serverless 平台,部署一个网站状态监控脚本,对目标网站的可用性进行监控告警。


Web 服务监控告警

针对 Web 服务,我们先设计一个简单的监控告警功能的流程:



在这个流程中,我们仅对网站的状态码进行监控,即返回的状态为 200,则判定网站可正常使用,否则进行告警:


# -*- coding: utf8 -*-import sslimport jsonimport smtplibimport urllib.requestfrom email.mime.text import MIMETextfrom email.header import Header
ssl._create_default_https_context = ssl._create_unverified_context

def sendEmail(content, to_user): sender = 'service@anycodes.cn' receivers = [to_user]
mail_msg = content message = MIMEText(mail_msg, 'html', 'utf-8') message['From'] = Header("网站监控", 'utf-8') message['To'] = Header("站长", 'utf-8')
subject = "网站监控告警" message['Subject'] = Header(subject, 'utf-8')
try: smtpObj = smtplib.SMTP_SSL("smtp.exmail.qq.com", 465) smtpObj.login('发送邮件的邮箱地址', '密码') smtpObj.sendmail(sender, receivers, message.as_string()) except smtplib.SMTPException as e: print(e)

def getStatusCode(url): return urllib.request.urlopen(url).getcode()

def main_handler(event, context): url = "http://www.anycodes.cn" if getStatusCode(url) == 200: print("您的网站%s可以访问!" % (url)) else: sendEmail("您的网站%s 不可以访问!" % (url), "接受人邮箱地址") return None
复制代码


通过 ServerlessFramework 可以部署,在部署的时候可以增加时间触发器:


MyWebMonitor:  component: "@serverless/tencent-scf"  inputs:    name: MyWebMonitor    codeUri: ./code    handler: index.main_handler    runtime: Python3.6    region: ap-guangzhou    description: 网站监控    memorySize: 64    timeout: 20    events:      - timer:          name: timer          parameters:            cronExpression: '*/5 * * * *'            enable: true
复制代码


在这里,timer 表示时间触发器,cronExpression是表达式:


创建定时触发器时,用户能够使用标准的 Cron 表达式的形式自定义何时触发。定时触发器现已推出秒级触发功能,为了兼容老的定时触发器,因此 Cron 表达式有两种写法。


Cron 表达式语法一(推荐)

Cron 表达式有七个必需字段,按空格分隔。



其中,每个字段都有相应的取值范围:



Cron 表达式语法二(不推荐)

Cron 表达式有五个必需字段,按空格分隔。



其中,每个字段都有相应的取值范围:



通配符


注意事项

在 Cron 表达式中的“日”和“星期”字段同时指定值时,两者为“或”关系,即两者的条件分别均生效。


示例

*/5 * * * * * * 表示每 5 秒触发一次


0 0 2 1 * * * 表示在每月的 1 日的凌晨 2 点触发


0 15 10 * * MON-FRI * 表示在周一到周五每天上午 10:15 触发


0 0 10,14,16 * * * * 表示在每天上午 10 点,下午 2 点,4 点触发


0 */30 9-17 * * * * 表示在每天上午 9 点到下午 5 点内每半小时触发


0 0 12 * * WED * 表示在每个星期三中午 12 点触发


因此,我们上面的代码可以认为是每 5 秒触发一次,当然,也可以根据网站监控密度,自定义设置触发的间隔时间。当我们网站服务不可用时,就可以收到告警:



这种网站监控方法比较简单,准确度可能会有问题,对于网站或服务的监控不能简单的看返回值,还要看链接耗时、下载耗时以及不同区域、不同运营商访问网站或者服务的延时信息等。


所以,我们需要对这个代码进行额外的更新与优化:


  1. 通过在线网速测试的网站,抓包获取不同地区不同运营商的请求特征;

  2. 编写爬虫程序,进行在线网速测试模块的编写;

  3. 集成到刚刚的项目中;


下面以站长工具网站中国内网站测速工具 为例,通过网页查阅相关信息。


对网站测速工具进行封装,例如:



通过对网页进行分析,获取请求特征,包括 Url,Form data,以及 Headers 等相关信息,其中该网站在使用不同监测点对网站进行请求时,是通过 Form data 中的 guid 的参数实现的,例如部分监测点的 guid:


广东佛山  电信  f403cdf2-27f8-4ccd-8f22-6f5a28a01309江苏宿迁  多线  74cb6a5c-b044-49d0-abee-bf42beb6ae05江苏常州  移动  5074fb13-4ab9-4f0a-87d9-f8ae51cb81c5浙江嘉兴  联通  ddfeba9f-a432-4b9a-b0a9-ef76e9499558
复制代码


此时,我们可以编写基本的爬虫代码,来对 Response 进行初步解析,以62a55a0e-387e-4d87-bf69-5e0c9dd6b983 江苏宿迁[电信]为例,编写代码:


import urllib.requestimport urllib.parse
url = "*某测速网站地址*"form_data = { 'guid': '62a55a0e-387e-4d87-bf69-5e0c9dd6b983', 'host': 'anycodes.cn', 'ishost': '1', 'encode': 'ECvBP9vjbuXRi0CVhnXAbufDNPDryYzO', 'checktype': '1',}headers = { 'Host': 'tool.chinaz.com', 'Origin': '*某测速网站地址*', 'Referer': '*某测速网站地址*', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36', 'X-Requested-With': 'XMLHttpRequest'}
print(urllib.request.urlopen( urllib.request.Request( url=url, data=urllib.parse.urlencode(form_data).encode('utf-8'), headers=headers )).read().decode("utf-8"))

复制代码


获得结果:


({  state: 1,  msg: '',  result: {    ip: '119.28.190.46',    httpstate: 200,    alltime: '212',    dnstime: '18',    conntime: '116',    downtime: '78',    filesize: '-',    downspeed: '4.72',    ipaddress: '新加坡新加坡',    headers: 'HTTP/1.1 200 OK br>Server: ...',    pagehtml: ''  }})

复制代码


在这个结果中,我们可以提取部分数据,例如江苏宿迁[电信]访问目标网站的基础数据:


总耗时:alltime:'212'链接耗时:conntime:'116'下载耗时:downtime:'78'

复制代码


此时,我们可以改造代码对更多的节点,进行测试:


江苏宿迁[电信]  总耗时:223  链接耗时:121  下载耗时:81广东佛山[电信]  总耗时:44  链接耗时:27  下载耗时:17广东惠州[电信]  总耗时:56  链接耗时:34  下载耗时:22广东深圳[电信]  总耗时:149  链接耗时:36  下载耗时:25浙江湖州[电信]  总耗时:3190  链接耗时:3115  下载耗时:75辽宁大连[电信]  总耗时:468  链接耗时:255  下载耗时:170江苏泰州[电信]  总耗时:180  链接耗时:104  下载耗时:69安徽合肥[电信]  总耗时:196  链接耗时:110  下载耗时:73...

复制代码


并对项目中的 index.py 进行代码修改:


# -*- coding: utf8 -*-import sslimport jsonimport reimport socketimport smtplibimport urllib.requestfrom email.mime.text import MIMETextfrom email.header import Header
socket.setdefaulttimeout(2.5)ssl._create_default_https_context = ssl._create_unverified_context
def getWebTime():
final_list = [] final_status = True
total_list = '''62a55a0e-387e-4d87-bf69-5e0c9dd6b983 江苏宿迁[电信] f403cdf2-27f8-4ccd-8f22-6f5a28a01309 广东佛山[电信] 5bea1430-f7c2-4146-88f4-17a7dc73a953 河南新乡[多线] 1f430ff0-eae9-413a-af2a-1c2a8986cff0 河南新乡[多线] ea551b59-2609-4ab4-89bc-14b2080f501a 河南新乡[多线] 2805fa9f-05ea-46bc-8ac0-1769b782bf52 黑龙江哈尔滨[联通] 722e28ca-dd02-4ccd-a134-f9d4218505a5 广东深圳[移动]8e7a403c-d998-4efa-b3d1-b67c0dfabc41 广东深圳[移动]'''
url = "*某测速网站地址*" for eve in total_list.split('\n'): id_data, node_name = eve.strip().split(" ") form_data = { 'guid': id_data, 'host': 'anycodes.cn', 'ishost': '1', 'encode': 'ECvBP9vjbuXRi0CVhnXAbufDNPDryYzO', 'checktype': '1', } headers = { 'Host': '*某测速网站地址*', 'Origin': '*某测速网站地址*', 'Referer': '*某测速网站地址*', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36', 'X-Requested-With': 'XMLHttpRequest' } try: result_data = urllib.request.urlopen( urllib.request.Request( url=url, data=urllib.parse.urlencode(form_data).encode('utf-8'), headers=headers ) ).read().decode("utf-8") try: alltime = re.findall("alltime:'(.*?)'", result_data)[0] conntime = re.findall("conntime:'(.*?)'", result_data)[0] downtime = re.findall("downtime:'(.*?)'", result_data)[0] final_string = "%s\t总耗时:%s\t链接耗时:%s\t下载耗时:%s" % (node_name, alltime, conntime, downtime) except: final_string = "%s链接异常!" % (node_name) final_status = False except: final_string = "%s链接超时!" % (node_name) final_status = False final_list.append(final_string) print(final_string) return (final_status,final_list)def sendEmail(content, to_user): sender = 'service@anycodes.cn' receivers = [to_user] mail_msg = content message = MIMEText(mail_msg, 'html', 'utf-8') message['From'] = Header("网站监控", 'utf-8') message['To'] = Header("站长", 'utf-8') subject = "网站监控告警" message['Subject'] = Header(subject, 'utf-8') try: smtpObj = smtplib.SMTP_SSL("smtp.exmail.qq.com", 465) smtpObj.login('service@anycodes.cn', '密码') smtpObj.sendmail(sender, receivers, message.as_string()) except smtplib.SMTPException: pass
def getStatusCode(url): return urllib.request.urlopen(url).getcode()
def main_handler(event, context): url = "http://www.anycodes.cn" final_status,final_list = getWebTime() if not final_status: sendEmail("您的网站%s的状态:<br>%s" % (url, "<br>".join(final_list)), "service@52exe.cn")

复制代码


由于本文是以学习为主,所以我们将节点列表进行缩减,只保留几个。通过部署,可得到结果:



告警的灵敏度和监控的频率,在实际生产过程中可以根据自己的需求进行调整。


云服务监控告警

前文,我们对网站状态以及健康等信息进行了监控与告警,在实际的生产运维中,还需要对服务进行监控,例如在使用 Hadoop、Spark 的时候对节点的健康进行监控,在使用 K8S 的时候对 API 网关、ETCD 等多维度的指标进行监控,在使用 Kafka 的时候,对数据积压量,以及 Topic、Consumer 等进行监控…


而这些服务的监控,往往不能通过简单的 URL 以及某些状态来进行判断。传统运维的做法是在额外的机器上设置一个定时任务,对相关的服务进行旁路监控。而在本文中,我们则通过 Serverless 技术,对云产品进行相关的监控与告警。


在使用云上的 Kafka 时,我们通常要看数据积压量,因为如果 Consumer 集群挂掉了,或者消费能力突然降低导致数据积压,很可能会对服务产生不可预估的影响,这个时候对 Kafka 的数据积压量进行监控告警,就显得额外重要。


本文以监控腾讯云的 Ckafka 为例进行实践,并通过多个云产品进行组合(包括云监控、Ckafka、云 API 以及云短信等)来实现短信告警、邮件告警以及企业微信告警功能。


首先,可以设计简单的流程图:



在开始项目之前,我们要准备一些基础的模块:


  • Kafka 数据积压量获取模块:


def GetSignature(param):    # 公共参数    param["SecretId"] = ""    param["Timestamp"] = int(time.time())    param["Nonce"] = random.randint(1, sys.maxsize)    param["Region"] = "ap-guangzhou"    # param["SignatureMethod"] = "HmacSHA256"    # 生成待签名字符串    sign_str = "GETckafka.api.qcloud.com/v2/index.php?"    sign_str += "&".join("%s=%s" % (k, param[k]) for k in sorted(param))    # 生成签名    secret_key = ""    if sys.version_info[0] > 2:        sign_str = bytes(sign_str, "utf-8")        secret_key = bytes(secret_key, "utf-8")    hashed = hmac.new(secret_key, sign_str, hashlib.sha1)    signature = binascii.b2a_base64(hashed.digest())[:-1]    if sys.version_info[0] > 2:        signature = signature.decode()    # 签名串编码    signature = urllib.parse.quote(signature)    return signature
def GetGroupOffsets(max_lag, phoneList): param = {} param["Action"] = "GetGroupOffsets" param["instanceId"] = "" param["group"] = "" signature = GetSignature(param) # 生成请求地址 param["Signature"] = signature url = "https://ckafka.api.qcloud.com/v2/index.php?Action=GetGroupOffsets&" url += "&".join("%s=%s" % (k, param[k]) for k in sorted(param)) req_attr = urllib.request.urlopen(url) res_data = req_attr.read().decode("utf-8") json_data = json.loads(res_data) for eve_topic in json_data['data']['topicList']: temp_lag = 0 result_list = [] for eve_partition in eve_topic["partitions"]: lag = eve_partition["lag"] temp_lag = temp_lag + lag if temp_lag > max_lag: result_list.append( { "topic": eve_topic["topic"], "lag": lag } ) print(result_list) if len(result_list)>0: KafkaLagRobot(result_list) KafkaLagSMS(result_list,phoneList)

复制代码


  • 接入企业微信机器人模块:


def KafkaLagRobot(content):    url = ""    data = {        "msgtype": "markdown",        "markdown": {            "content": content,        }    }    data = json.dumps(data).encode("utf-8")    req_attr = urllib.request.Request(url, data)    resp_attr = urllib.request.urlopen(req_attr)    return_msg = resp_attr.read().decode("utf-8")

复制代码


  • 接入腾讯云短信服务模块:


def KafkaLagSMS(infor, phone_list):    url = ""    strMobile = phone_list    strAppKey = ""    strRand = str(random.randint(1, sys.maxsize))    strTime = int(time.time())    strSign = "appkey=%s&random=%s&time=%s&mobile=%s" % (strAppKey, strRand, strTime, ",".join(strMobile))    sig = hashlib.sha256()    sig.update(strSign.encode("utf-8"))
phone_dict = [] for eve_phone in phone_list: phone_dict.append( { "mobile": eve_phone, "nationcode": "86" } ) data = { "ext": "", "extend": "", "params": [ infor, ], "sig": sig.hexdigest(), "sign": "你的sign", "tel": phone_dict, "time": strTime, "tpl_id": 你的模板id } data = json.dumps(data).encode("utf-8") req_attr = urllib.request.Request(url=url, data=data) resp_attr = urllib.request.urlopen(req_attr) return_msg = resp_attr.read().decode("utf-8")

复制代码


  • 发送邮件告警模块:


def sendEmail(content, to_user):    sender = 'service@anycodes.cn'    message = MIMEText(content, 'html', 'utf-8')    message['From'] = Header("监控", 'utf-8')    message['To'] = Header("站长", 'utf-8')    message['Subject'] = Header("告警", 'utf-8')    try:        smtpObj = smtplib.SMTP_SSL("smtp.exmail.qq.com", 465)        smtpObj.login('service@anycodes.cn', '密码')        smtpObj.sendmail(sender, [to_user], message.as_string())    except smtplib.SMTPException as e:        logging.debug(e)

复制代码


完成模块编写,和上面的方法一样,进行项目部署。部署成功之后进行测试,测试可看到功能可用:


  • 短信告警样式:



  • 企业微信告警样式:



总结

通过本文的实践,希望读者可以了解到 Serverless 相关产品在运维行业中的基本应用,尤其是监控告警的基本使用方法和初步灵感。设计一个网站监控程序实际上是一个很初级的入门场景,希望大家可以将更多的监控告警功与 Serverless 技术进行结合,例如监控自己的 MySQL 压力情况、监控已有服务器的数据指标等,通过对这些指标的监控告警,不仅仅可以让管理者及时发现服务的潜在风险,也可以通过一些自动化流程实现项目的自动化运维。


通过本场景实践,我们也可以对项目进行额外的优化或者应用在不同的领域以及场景中。例如,我们可以通过增加短信告警、微信告警、企业微信告警等多个维度,来确保相关人员可以及时收到告警信息;我们也可以通过监控某个小说网站、视频网站等,看到我们关注的小说或者视频的更新情况,便于追更等。


本章内容大部分资料来自图书《Serverless 架构:从原理、设计到项目实战》


作者介绍


刘宇,腾讯 Serverless 团队后台研发工程师。毕业于浙江大学,硕士研究生学历,曾在滴滴出行、腾讯科技做产品经理,本科开始有自主创业经历,是 Anycodes 在线编程的负责人(该软件累计下载量超 100 万次)。目前投身于 Serverless 架构研发,著书《Serverless 架构:从原理、设计到项目实战》,参与开发和维护多个 Serverless 组件,是活跃的 Serverless Framework 的贡献者,也曾多次公开演讲和分享 Serverless 相关技术与经验,致力于 Serverless 的落地与项目上云。


2020 年 4 月 01 日 09:234599

评论

发布
暂无评论
发现更多内容

Atlassian 最受欢迎的分析工具强势融入 Confluence Data Center!

Atlassian

DevOps 知识管理 Atlassian Jira Confluence

Redis 缓存的三大问题及其解决方案

xcbeyond

redis 缓存 6月日更

一文教你快速构建Mysql百万级测试数据

代码熬夜敲

网络安全 Web 信息安全 渗透测试

【译】编写整洁 React 代码的简单实践

KooFE前端团队

前端 React 6 月日更 整洁代码

Git使用

xujiangniao

全网首发!阿里p7私藏的ZooKeeper面试题万字总结,撸完加薪10k

java专业爱好者

Java zookeeper

相似度计算-句子

Qien Z.

nlp 6月日更 六月日更 tf-idf

6月份组队学习预热

IT蜗壳-Tango

6 月日更

我的程序员生涯(1)

胡途

程序员 职业生涯

JavaScript 代码逻辑判断的优化

编程三昧

JavaScript 前端 代码质量 代码优化 编程思想

Bzz节点挖矿系统搭建,Bzz分币系统源码

13823153121

自制文件系统 —— 02 开发者的福音,FUSE文件系统

奇伢云存储

Linux 文件系统 FUSE

让JavaScript在WebAssembly上快速运行

代码先生

JIT webassembly WASI

平衡计分卡- 战略落地的工具

石云升

创业 战略 职场经验 6月日更

戴着镣铐起舞的算法市场

脑极体

MySQL基础之五:其他过滤方式

打工人!

myslq 6月日更

Sprint Review != Demo——《Scrum指南》重读有感(4)

Bruce Talk

Scrum 敏捷 随笔 Agile

我的程序员生涯(2)

胡途

程序员 职业生涯

APISIX2.6微服务网关入门

菠萝吹雪—Code

架构实战营

【Vue2.x 源码学习】第七篇 - 阶段性梳理

Brave

源码 vue2 6月日更

【TcaplusDB知识库】TcaplusDB限制条件

tcaplus

数据库 TcaplusDB

这个 “少年黑客”,用黑科技守护独居老人

阿里云视频云

阿里云 计算机视觉 音视频 养老

Java程序员【面试】与【进阶】3个最佳学习方法

Java架构师迁哥

如何使用Tauri和Ember.js创建小型、快速和酷的桌面应用程序

代码先生

tauri ember.js desktop程序

El Camino de Santiago

escray

六月日更

网络攻防学习笔记 Day38

穿过生命散发芬芳

网络攻防 6月日更

5分钟速读之Rust权威指南(十八)

码生笔谈

rust 范型 trait

理解IM消息“可靠性”和“一致性”问题,以及解决方案探讨

JackJiang

即时通讯 IM 可靠消息最终一致

整数划分问题(详解 n > m 情况)

若尘

数据结构 六月日更

懵了!看了阿里p7大佬耗时三天三夜整理的String类,我居然不会

java专业爱好者

Java string

Kubernetes手记(4)- 命令入门

雪雷

六月日更

新晋管理者都会遇到的6个问题

新晋管理者都会遇到的6个问题

Serverless 实战:通过 Serverless 架构实现监控告警-InfoQ