凡事只要有可能出错,那就一定会出错。
关于墨菲定律的一些说法可以参考。
马斯克的SpaceX(只有几十个员工)则是另一个很好的反向例子参考,至少说明了如下几点:
以现有载人飞船搭载的星载计算机和控制器举例,单个控制器价格为500万人民币左右,单个控制器价格为500万人民币左右,一共14个系统,为了追求高可靠性,每个系统1+1备份,一共28个控制器,成本总计约1.4亿人民币!
而SpaceX的龙飞船主控系统的芯片组仅用了2.6万人民币,成本相差5348倍!
启示:未来只需要很少的人,很低的成本,就足以完成一些顶级的事情。某种程度上The Fewer, The Better!
回到地面,本篇讨论如何主键构建一个高可靠的决策(控制)方法,目的是先解决一个中等复杂的系统的监控和debug过程。
假象一个场景:
链条大致是这样的:
用户 <----[公网]----> Flask <-------[RabbitMQ]--------->[Pika消息消费]
从一个完整的服务考虑,Flask(蓝图模式)至少应该具备几部分:
其中应用逻辑应该是其中最关键、也最灵活的部分,本次只讨论这个。应用部分大致会牵涉到一下内容:
设计/制造原则
1 先确定能确定的基本组件,按规则-图的方式去组织。
2 假设基本组件是不可靠的,需要增加追踪,控制和冗余。
3 优化基本组件(基本组件本身也是由更基本的组件构成的),直到其客观稳定性达到6个9以上。
4 通过冗余设计和控制方法,使系统的可靠性达到足够多的9。
我们还是先从可能出错的角度出发切入(毕竟火烧眉毛很烦人)。简单来说,是采用类似日志的方法去记录,但是不是简单的日志(服务器那么多日志,被使用的有多少?)。
这个是我自己编写的类,几个出发点,或者说假设点如下:
- 主程序(ll_mainstream)和子程序(ll_funcstream)。假设有一个主流程和若干子流程,例如某个用户点击了某个链接,为了完成这个工作所执行的若干个视图函数就是主程序(views1->view2…)。每个视图函数又有若干个处理过程叫子程序(func1 -> func2)。
- 调试模式和批量模式。批量模式不保留数据,只记录函数的路径(如果失败就是出错了);调试模式记录input, output的具体数据。
- 全局号(gid)和路径(gpath)。全局号也可以认为是用户的id,gpath则是保存文件的具体路径,因为全部使用python,所以使用pkl统一保存。
使用方法大致如下, 只要在调用函数的方法上略做调整就可以了(多少我还是偏函数式编程的习惯):
some_log = LogicLog(xxx,xxx)
-> res1 = some_log.log(func1, args1, kwargs1)
-> ...
-> some_log.save()
我们可以理解整体操作过程是网状的(Mesh), 当然我们一般会拆解为(有向无环图,DAG),然后某一次操作的过程就是一个链条。LogicLog要做的是记录这个链条发生的事,而整体的情况则是把链条的数据整合成图,然后通过算法来分析、控制。
以下算是V1版本,先做一些基础性的东西。初始化类的时候先看看当前的gid在当前文件夹下是否已经有日志了。
class LogicLog():
def __init__(self, gid, gpath='./', debug=False, keep_max=10, logname='ll_mainstream', logmode='justerror'):
self.gid = str(gid).replace(' ', '').replace('_', '')
self.logfile_name = logname + '_' + \
str(gid).replace(' ', '').replace('_', '')
self.logfile_path = amend_path_slash(gpath)
# 是否已有LogicLog文件
log_dict = tryload_pkl(self.logfile_name, cur_path=self.logfile_path)
if log_dict is None:
self.log_dict = {
}
self.log_dict['loglist'] = []
self.log_dict['logsummary'] = {
}
# 上一次的debug字典(不一定错)
self.log_dict['last_debug_data_dict'] = None
# 上一个错误的debug字典
self.log_dict['last_debug_wrong_data_dict'] = None
else:
self.log_dict = log_dict
# 根据keep_max修剪长度
self.log_dict['loglist'] = self.log_dict['loglist'][-keep_max:]
# 增加当前-步骤字典
self.step_dict = {
}
# 是否调试
self.is_debug = debug
其中的log函数负责把执行的信息记录下来,如果结果不符合规范,记录时会自动修改。约定的输出(输入)规范为
{
'status':True/False,'msg', 'meta':{
}, 'data':{
} }
以下是log函数:
def log(self, f, *args, **kwargs):
# 基础信息 I在执行函数之前
print('it works')
print('function name', f.__name__)
print('args', args)
print('kwargs', kwargs)
cur_timestamp = int(time.time())
cur_gid = self.gid
# II 执行函数
try:
res = f(*args, **kwargs)
except:
res = None
cur_finished_timestamp = int(time.time())
# III 对数据结果进行修饰
# 修正输出格式 {'status':True/False,'msg', 'meta'{}, 'data' }
# new_res 都是可被json dumps的简单对象
try:
if res.get('status'):
is_formatted_result = True
else:
is_formatted_result = False
except:
is_formatted_result = False
# 如果结果没有status字段,那么就认为是不规范的,结果,进行重新打包,数据部分(data)可能不是可json化的数据,所以放到最后决定是否保存
if not is_formatted_result:
new_res = {
}
if res:
new_res['status'] = True
new_res['msg'] = 'ok'
new_res['meta'] = {
}
# new_res['data'] = res
else:
new_res['status'] = False
new_res['msg'] = 'error'
new_res['meta'] = {
}
# new_res['data'] = res
else:
new_res = res
# 这一步是增加一个函数调用(成功/失败)的统计
if new_res.get('status'):
if self.log_dict['logsummary'].get(f.__name__) is None:
self.log_dict['logsummary'][f.__name__] = {
}
self.log_dict['logsummary'][f.__name__]['success'] = 1
self.log_dict['logsummary'][f.__name__]['error'] = 0
else:
self.log_dict['logsummary'][f.__name__]['success'] += 1
else:
if self.log_dict['logsummary'].get(f.__name__) is None:
self.log_dict['logsummary'][f.__name__] = {
}
self.log_dict['logsummary'][f.__name__]['success'] = 0
self.log_dict['logsummary'][f.__name__]['error'] = 1
else:
self.log_dict['logsummary'][f.__name__]['error'] += 1
# 将元数据保存
new_res['meta']['ts'] = cur_timestamp
new_res['meta']['tsdone'] = cur_finished_timestamp
new_res['meta']['gid'] = cur_gid
new_res['meta']['function'] = f.__name__
# 将结果保存到临时的step_dict
# 获取步骤号
if self.step_dict:
new_id = int(get_current_max_code(
self.step_dict, key_or_num='num')) + 1
else:
new_id = 1
# 如果是调试模式,那么会存详细的参数和数据
if self.is_debug:
new_res['data'] = {
}
new_res['data']['input'] = {
}
new_res['data']['input']['args'] = pickle.dumps(args)
new_res['data']['input']['kwargs'] = pickle.dumps(kwargs)
new_res['data']['output'] = pickle.dumps(res)
# 结果保存到新步骤
new_step = 'step' + str(new_id)
self.step_dict[new_step] = new_res
# 为debug模式保留
self.log_dict['last_debug_data_dict'] = new_res
if not new_res['status']:
self.log_dict['last_debug_wrong_data_dict'] = new_res
return res
# 通过get_last_wrong_data载入数据
def get_last_wrong_data(self):
if self.log_dict['last_debug_wrong_data_dict'] is not None:
res = copy.deepcopy(self.log_dict['last_debug_wrong_data_dict'])
res['data']['input']['args'] = pickle.loads(res['data']['input']['args'] )
res['data']['input']['kwargs'] = pickle.loads(res['data']['input']['kwargs'] )
res['data']['output'] = pickle.loads(res['data']['output'] )
else:
dm.color_print('没有错误日志')
res = None
return res
来个helloworld(第一次执行)
import DataManipulation as dm
import pandas as pd
gid= 3
ll = dm.LogicLog(gid)
def hello(x):
print(x)
return True
x1 = ll.log(hello, 'world')
ll.save()
--->
In [3]: x1 = ll.log(hello, 'world')
it works
function name hello
args ('world',)
kwargs {
}
world
In [4]: ll.save()
data save to pickle: ./ll_mainstream_3.pkl
In [5]: ll.step_dict
Out[5]:
{
'step1': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599288576, 'gid': '3', 'function': 'hello'}}}
再来个helloworld(第二次执行)
# --- 第二次
ll = dm.LogicLog(gid)
def hello1(x):
dm.color_print('hello1: %s' % str(x))
return True
x1 = ll.log(hello,'world1')
x2 = ll.log(hello1,'world2')
ll.save()
--- 结果
it works
function name hello
args ('world1',)
kwargs {
}
world1
it works
function name hello1
args ('world2',)
kwargs {
}
hello1: world2
data save to pickle: ./ll_mainstream_3.pkl
#看一下当前的ll - 总体函数的调用情况
ll.log_dict['logsummary']
In [11]: ll.log_dict['logsummary']
Out[11]: {
'hello': {
'success': 2, 'error': 0}, 'hello1': {
'success': 1, 'error': 0}}
#看一下当前的ll - 调用的历史情况
ll.log_dict['loglist']
In [12]: ll.log_dict['loglist']
...:
Out[12]:
[{
'step1': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599288576, 'gid': '3', 'function': 'hello'}}},
{
'step1': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599291067, 'gid': '3', 'function': 'hello'}},
'step2': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599291067, 'gid': '3', 'function': 'hello1'}}}]
# 目前我们做了两次操作
In [13]: len(ll.log_dict['loglist'])
Out[13]: 2
# 第一次只调用了hello
In [14]: ll.log_dict['loglist'][0]
...:
Out[14]:
{
'step1': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599288576, 'gid': '3', 'function': 'hello'}}}
# 第二次依次调用了hello,hello1
In [15]: ll.log_dict['loglist'][1]
...:
Out[15]:
{
'step1': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599291067, 'gid': '3', 'function': 'hello'}},
'step2': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599291067, 'gid': '3', 'function': 'hello1'}}}
接下来我们再做一次,但是这次我们是进入debug模式,并且故意输错参数
ll = LogicLog(gid, debug=True)
x1 = ll.log(hello, x1='i am wrong')
x2 = ll.log(hello1,('world3'))
ll.save()
--- 可以看到第三次的hello函数出错了(status:false)
In [32]: ll.log_dict['loglist']
Out[32]:
[{
'step1': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599293551,
'tsdone': 1599293551,
'gid': '3',
'function': 'hello'}}},
{
'step1': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599293564,
'tsdone': 1599293564,
'gid': '3',
'function': 'hello'}},
'step2': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599293564,
'tsdone': 1599293564,
'gid': '3',
'function': 'hello1'}}},
{
'step1': {
'status': False,
'msg': 'error',
'meta': {
'ts': 1599293594,
'tsdone': 1599293594,
'gid': '3',
'function': 'hello'},
'data': {
'input': {
'args': b'\x80\x03).',
'kwargs': b'\x80\x03}q\x00X\x02\x00\x00\x00x1q\x01X\n\x00\x00\x00i am wrongq\x02s.'},
'output': b'\x80\x03N.'}},
'step2': {
'status': True,
'msg': 'ok',
'meta': {
'ts': 1599293594,
'tsdone': 1599293594,
'gid': '3',
'function': 'hello1'},
'data': {
'input': {
'args': b'\x80\x03X\x06\x00\x00\x00world3q\x00\x85q\x01.',
'kwargs': b'\x80\x03}q\x00.'},
'output': b'\x80\x03\x88.'}}}]
# ---- 可以看到一共有三次路径日志
In [33]: len(ll.log_dict['loglist'])
Out[33]: 3
# --- 最近的一次错误日志
In [34]: ll.last_debug_wrong_data_dict
Out[34]:
{
'status': False,
'msg': 'error',
'meta': {
'ts': 1599293594,
'tsdone': 1599293594,
'gid': '3',
'function': 'hello'},
'data': {
'input': {
'args': b'\x80\x03).',
'kwargs': b'\x80\x03}q\x00X\x02\x00\x00\x00x1q\x01X\n\x00\x00\x00i am wrongq\x02s.'},
'output': b'\x80\x03N.'}}
# --- 获取最近一次的数据
In [43]: ll = LogicLog(gid, debug=True)
In [44]: ll.get_last_wrong_data()
Out[44]:
{
'status': False,
'msg': 'error',
'meta': {
'ts': 1599295468,
'tsdone': 1599295468,
'gid': '3',
'function': 'hello'},
'data': {
'input': {
'args': (), 'kwargs': {
'x1': 'i am wrong'}},
'output': None}}
可以看到,LogicLog 虽然保存了很多有用的信息,但是没考虑依赖,这要把之前函数字典的内容拉回来,重新整理。
简单来说是这样:
这个放在另一篇讲。