关于这个系列
这个项目实录系列是记录Mproxy项目的整个开发流程。项目最终的目标是开发一套代理服务器的API。这个系列中会记录项目的需求、设计、验证、实现、升级等等,包括设计决策的依据,开发过程中的各种坑。希望和大家共同交流,一起进步。
项目的源码我会同步更新到GitHub,项目地址:https://github.com/mrbcy/Mproxy。
系列地址:
今日计划
本来想着要在代理服务器的收集系统里面加一个内建的web页面,这样就可以通过浏览器查看系统的运行状态。
后来想了想,我的主要目的是能够确保系统在正确的运行就可以了。要达到这个目标并不是一定要内建web页面。要确保系统的正常运行只要卡主两个关键指标就可以了。
第1个指标是可用的代理服务器数量。这个是重中之重,只要这个指标没问题,其他可以不用那么紧张。
第2个指标是last_validate_time中的最晚时间。如果这个时间离现在很远,表示系统很久没有重新验证代理服务器了,也就证明目前的可用代理服务器数量是不可靠的。
针对这两个指标,提出了如下的监测方案。
- 如果发现可用的代理服务器数量少于4000,就运行爬虫进行爬取。这个检查2小时执行1次。
- 如果last_validate_time中的最晚时间距今超过24小时,发送短信提醒。这个检查6小时执行1次。
- 如果发现可用的代理服务器数量少于2000,发送短信提醒。这个检查2小时执行1次。
- 如果在执行上述查询的过程中抛出异常,也发送短信提醒。
所以,今天的任务就是完成监测器的开发,并且微调之前的系统各个参数。为真正在ubuntu集群上的部署做好准备。
技术验证
还是有两个技术验证工作要做。第一个是通过Python执行其他的Python程序;第二个是用Python发送短信。
用Python执行其他的Python程序
原来以为这个很难,结果很简单,哈哈。
首先写一个test.bat文件,代码如下:
cd /d "D:\软件编程学习\Mproxy\代码\Spiders\kuaidaili"
scrapy crawl kuaidaili
看到路径里面有中文,则test.bat文件的编码必须跟cmd命令窗口的编码格式一致,否则会找不到路径。我这里的编码是GBK。
然后编写调用bat的代码:
#-*- coding: utf-8 -*-
import os
def func():
os.system('test.bat')
if __name__ == '__main__':
func()
使用Python发送短信
我这边选用了阿里大于的短信接口。主要优点是使用淘宝用户名直接登录,很方便,还有Python的SDK可以直接用。而且毕竟大厂出品,应该有保障。
在使用之前要完成应用创建,签名审批和短信模板审批几个流程。具体就不说了,基本都能过。
在申请的时候一定要申请合适的短信模板。经过反复测试,变量长度最多为15个字符(中英文都算1个)。
在申请的时候一定要申请合适的短信模板。经过反复测试,变量长度最多为15个字符(中英文都算1个)。
在申请的时候一定要申请合适的短信模板。经过反复测试,变量长度最多为15个字符(中英文都算1个)。
发送短信的API文档可以参考这里:https://api.alidayu.com/doc2/apiDetail.htm?spm=a3142.8062968.3.1.2fJlnI&apiId=25450
代码如下:
#-*- coding: utf-8 -*-
import top.api
def func():
url = " gw.api.taobao.com"
req = top.api.AlibabaAliqinFcSmsNumSendRequest(url, port=80)
req.set_app_info(top.appinfo(appkey, secret))
req.extend = "123456"
req.sms_type = "normal"
req.sms_free_sign_name = "阿里大于"
req.sms_param = "{\"code\":\"1234\",\"product\":\"alidayu\"}"
req.rec_num = "13000000000"
req.sms_template_code = "SMS_585014"
try:
resp = req.getResponse()
print(resp)
except Exception, e:
print(e)
if __name__ == '__main__':
func()
真实运行时需要把appkey,secret等信息填成系统分配过来的值。
这里有点坑,我申请的短信模板是这样的:
系统运行异常,请处理。${alarm_info}
本来想发一条这样的短信:
系统运行异常,请处理。Mproxy系统提示,代理服务器数量已经不足2000
然后反复的收到isv.PARAM_LENGTH_LIMIT的异常,就是说变量长度限制。
所以我们在申请的时候一定要申请合适的短信模板。经过反复测试,变量长度最多为15个字符(中英文都算1个)。
配置文件工具类
为了后续维护的方便和上传GitHub时能不泄露我的关键信息,必须把发送短信关键的数据放到配置文件里,然后提供一个配置文件的工具类供其他代码使用。
#-*- coding: utf-8 -*-
import ConfigParser
import os
class ConfigLoader:
def __init__(self):
# get the project path
dir_name = "Monitor" + os.sep
thePath = os.getcwdu()
if thePath.find(dir_name) > 0:
thePath = thePath[:thePath.find(dir_name) + len(dir_name)]
else:
thePath += os.sep
print thePath
self.cp = ConfigParser.SafeConfigParser()
self.cp.read(thePath + 'monitor.cfg')
def get_app_key(self):
return self.cp.get('sms','appkey')
def get_secret_key(self):
return self.cp.get('sms','secret')
def get_sign_name(self):
return self.cp.get('sms','sign_name')
def get_sms_template_code(self):
return self.cp.get('sms','sms_template_code')
def get_phone_num(self):
return self.cp.get('sms','phone_num')
def get_mysql_host(self):
return self.cp.get('mysql','host')
def get_mysql_port(self):
return int(self.cp.get('mysql','port'))
def get_mysql_user(self):
return self.cp.get('mysql','user')
def get_mysql_pwd(self):
return self.cp.get('mysql','password')
def get_mysql_db_name(self):
return self.cp.get('mysql','db_name')
配置文件的结构如下,具体内容请自行填写。
[sms]
appkey =
secret =
sign_name =
sms_template_code =
phone_num =
[mysql]
host = amaster
port = 3306
user =
password =
db_name = mproxy
感觉经过了几个版本的迭代,classloader终于写得在各个路径下以及在Windows和Linux下都能用了。稍后把这个代码更新到其他组件上去。
封装数据库的查询工具类
接下来使用三层架构来封装需要查询数据库的工具类。这里从dispatcher中把dbpool,domain包都复制过来待用。
dao的代码如下:
#-*- coding: utf-8 -*-
import traceback
from dao.proxystatus import ProxyStatus
from dbpool.poolutil import PoolUtil
from domain.proxydaoitem import ProxyDaoItem
class ProxyDao:
def find_proxy_last_validate_time(self):
try:
conn = PoolUtil.pool.connection()
cur = conn.cursor()
sql = "select * from proxy_list order by last_validate_time desc limit 1"
count = cur.execute(sql)
last_validate_time = None
if count != 0:
data = cur.fetchone()
last_validate_time = data[5]
cur.close()
conn.close()
return last_validate_time
except Exception as e:
return None
def get_avaliable_proxy_count(self):
try:
conn = PoolUtil.pool.connection()
cur = conn.cursor()
sql = "select count(*) from proxy_list where status = %s"
count = cur.execute(sql,ProxyStatus.AVAILABLE)
result = None
if count != 0:
data = cur.fetchone()
result = int(data[0])
cur.close()
conn.close()
return result
except Exception as e:
return 0
然后写dao的单元测试:
#-*- coding: utf-8 -*-
import datetime
from dao.proxydao import ProxyDao
from dao.proxystatus import ProxyStatus
from domain.proxydaoitem import ProxyDaoItem
proxy_dao = ProxyDao()
def test_get_proxy_count():
global proxy_dao
print proxy_dao.get_avaliable_proxy_count()
def test_get_last_validate_time():
global proxy_dao
last_validate_time = proxy_dao.find_proxy_last_validate_time()
print type(last_validate_time)
print last_validate_time
if __name__ == '__main__':
test_get_last_validate_time()
service就是在dao上包了一层,所以也不用写单元测试了。
#-*- coding: utf-8 -*-
from dao.proxydao import ProxyDao
class ProxyService():
def __init__(self):
self.proxy_dao = ProxyDao()
def find_proxy_last_validate_time(self):
return self.proxy_dao.find_proxy_last_validate_time()
def get_avaliable_proxy_count(self):
return self.proxy_dao.get_avaliable_proxy_count()
封装发送短信工具类
#-*- coding: utf-8 -*-
import traceback
import top
from conf.configloader import ConfigLoader
class SmsUtil:
conf_loader = ConfigLoader()
@classmethod
def send_sms(cls,system_name,exception_name,key_prompt):
url = "gw.api.taobao.com"
appkey = cls.conf_loader.get_app_key()
secret = cls.conf_loader.get_secret_key()
req = top.api.AlibabaAliqinFcSmsNumSendRequest(url)
req.set_app_info(top.appinfo(appkey, secret))
req.sms_type = "normal"
req.sms_free_sign_name = cls.conf_loader.get_sign_name()
req.sms_param = """{"system_name":"%s","exception_name":"%s","key_prompt":"%s"}""" % (system_name,exception_name,key_prompt)
req.rec_num = cls.conf_loader.get_phone_num()
req.sms_template_code = cls.conf_loader.get_sms_template_code()
try:
resp = req.getResponse()
print(resp)
except Exception as e:
traceback.print_exc()
这里后续还可以改进,如果发送短信报异常,可以再发送邮件,让运维人员知道短信接口也有问题。
封装检查任务线程类
检查可用代理服务器数量的线程类
#-*- coding: utf-8 -*-
import os
import threading
import time
import traceback
from conf.configloader import ConfigLoader
from service.proxyservice import ProxyService
from sms.smsutil import SmsUtil
class AvailableCountTask(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.proxy_service = ProxyService()
self.conf_loader = ConfigLoader()
def run(self):
while True:
count = self.proxy_service.get_avaliable_proxy_count()
if count < 4500:
try:
# start spiders
exit_code = os.system(self.conf_loader.get_start_kuaidaili_command())
print exit_code
if exit_code != 0:
SmsUtil.send_sms('Mproxy', '快代理爬虫运行出错', '无')
time.sleep(5)
exit_code = os.system(self.conf_loader.get_start_xicidaili_command())
print exit_code
if exit_code != 0:
SmsUtil.send_sms('Mproxy', '西刺代理爬虫运行出错', '无')
except Exception as e:
traceback.print_exc()
SmsUtil.send_sms('Mproxy','启动爬虫出错','无')
elif count < 2000:
SmsUtil.send_sms('Mproxy', '代理服务器数量不足', str(count))
time.sleep(60*60*2)
这里还对爬虫的代码进行了一点修改,如果启动过程出错(一般是连接Kafka集群失败)就以退出码1退出,让监测器能够知道确实出错了。
检查验证时间的任务类
#-*- coding: utf-8 -*-
import os
import threading
import time
import traceback
import datetime
from service.proxyservice import ProxyService
from sms.smsutil import SmsUtil
class ValidateCheckTask(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.proxy_service = ProxyService()
def run(self):
while True:
last_validate_time = self.proxy_service.find_proxy_last_validate_time()
now = datetime.datetime.now()
seconds = (now - last_validate_time).total_seconds()
if seconds > 12 * 60 * 60:
SmsUtil.send_sms('Mproxy', '超过12小时未执行验证', '无')
time.sleep(60*60*6)
写监控器的代码
剩下的部分很简单,只要把两个任务启动起来就行了。
#-*- coding: utf-8 -*-
from sms.smsutil import SmsUtil
from task.availablecounttask import AvailableCountTask
from task.validatechecktask import ValidateCheckTask
def func():
validate_check_task = ValidateCheckTask()
validate_check_task.start()
available_count_check_task = AvailableCountTask()
available_count_check_task.start()
# SmsUtil.send_sms('Mproxy','代理服务器数量不足','200')
if __name__ == '__main__':
func()
封装邮件发送的工具类
直接在网上找到了代码http://www.cnblogs.com/xiaowuyi/archive/2012/03/17/2404015.html,用一下吧。
#-*- coding: utf-8 -*-
import smtplib
from email.mime.text import MIMEText
from conf.configloader import ConfigLoader
class EmailUtil:
conf_loader = ConfigLoader()
@classmethod
def send_email(cls,sms_content,exception_info):
mailto_list = cls.conf_loader.get_mail_to_list()
mail_host = "smtp.qq.com"
mail_user = cls.conf_loader.get_mail_username()
mail_pass = cls.conf_loader.get_mail_password()
mail_postfix = "qq.com"
sub = "Mproxy短信接口异常"
content = "尊敬的管理员您好,您收到这封邮件是因为我们的短信接口出现了问题,无法向运维人员发送短信。" \
"\n\n尝试发送的短信内容如下:\n%s \n\n发送过程中出现的异常为:\n%s" % (sms_content,exception_info)
me = "Mproxy" + "<" + mail_user + "@" + mail_postfix + ">"
msg = MIMEText(content, _subtype='plain', _charset='utf-8')
msg['Subject'] = sub
msg['From'] = me
msg['To'] = ";".join(mailto_list)
try:
server = smtplib.SMTP_SSL("smtp.qq.com", 465)
server.connect(mail_host)
server.login(mail_user, mail_pass)
server.sendmail(me, mailto_list, msg.as_string())
server.quit()
except Exception, e:
print str(e)
然后修改一下短信发送工具类,如果发送短信出错,就发送邮件。
#-*- coding: utf-8 -*-
import traceback
import top
from conf.configloader import ConfigLoader
from mail.emailutil import EmailUtil
from sms.tracebackcontainer import TracebackContainer
class SmsUtil:
conf_loader = ConfigLoader()
@classmethod
def send_sms(cls,system_name,exception_name,key_prompt):
url = "gw.api.taobao.com"
appkey = cls.conf_loader.get_app_key()
secret = cls.conf_loader.get_secret_key()
req = top.api.AlibabaAliqinFcSmsNumSendRequest(url)
req.set_app_info(top.appinfo(appkey, secret))
req.sms_type = "normal"
req.sms_free_sign_name = cls.conf_loader.get_sign_name()
req.sms_param = """{"system_name":"%s","exception_name":"%s","key_prompt":"%s"}""" % (system_name,exception_name,key_prompt)
req.rec_num = cls.conf_loader.get_phone_num()
req.sms_template_code = cls.conf_loader.get_sms_template_code()
try:
resp = req.getResponse()
print(resp)
except Exception as e:
traceback_container = TracebackContainer()
traceback.print_exc(file=traceback_container)
sms_content = "%s运行异常:%s,关键参数:%s,请尽快处理" % (system_name,exception_name,key_prompt)
EmailUtil.send_email(sms_content, traceback_container.message)
这里额外定义了一个类来收集traceback的打印信息。
#-*- coding: utf-8 -*-
class TracebackContainer:
def __init__(self):
self.message = ""
def write(self, str):
'''
把traceback信息存储必须的函数
'''
self.message += str
小结
到现在来说,应该是能够保证收集系统稳定运行了。下面真的要部署到Linux环境下面去了,再把文档补一补,差的太多了。