Python 全栈系列16 - 逻辑全流程控制

说明

凡事只要有可能出错,那就一定会出错。

关于墨菲定律的一些说法可以参考。
马斯克的SpaceX(只有几十个员工)则是另一个很好的反向例子参考,至少说明了如下几点:

  1. 即使是航天飞行这种复杂情况也有可能通过某种方法(算法)达到极高的可靠度(6个9?)
  2. 自己动手做的东西,时间可能长一点,但是才真正拥有无限可能。

以现有载人飞船搭载的星载计算机和控制器举例,单个控制器价格为500万人民币左右,单个控制器价格为500万人民币左右,一共14个系统,为了追求高可靠性,每个系统1+1备份,一共28个控制器,成本总计约1.4亿人民币!
而SpaceX的龙飞船主控系统的芯片组仅用了2.6万人民币,成本相差5348倍!

启示:未来只需要很少的人,很低的成本,就足以完成一些顶级的事情。某种程度上The Fewer, The Better!

回到地面,本篇讨论如何主键构建一个高可靠的决策(控制)方法,目的是先解决一个中等复杂的系统的监控和debug过程。

假象一个场景:

  1. 有一台云服务器,上面运行了Flask,提供网络服务。
  2. 云服务器上有RabbitMQ,提供消息服务,Flask会与RabbitMQ通讯。
  3. 本地服务器上有消息消费者,与RabbitMQ 通讯,间接为Flask提供服务。

链条大致是这样的:

用户 <----[公网]----> Flask <-------[RabbitMQ]--------->[Pika消息消费]

1 内容

1.1 Flask的逻辑

从一个完整的服务考虑,Flask(蓝图模式)至少应该具备几部分:

  • 1 用户注册和认证
  • 2 (邮件)消息交互
  • 3 应用权限
  • 4 数据(库)存取
  • 5 应用逻辑

其中应用逻辑应该是其中最关键、也最灵活的部分,本次只讨论这个。应用部分大致会牵涉到一下内容:

  • 1 flask服务相关。例如服务提供了用户的状态,邮件以及消息的发送,文件路径,数据库连接(ORM对象),以及一些配置。
  • 2 通用函数包。例如Pandas, re等。
  • 3 自有函数包。例如DataManipulation,有些包是浅层逻辑的(例如只做文件读取),如果出错其实也很容易发现。有些则具有较深的逻辑层次,这部分在过去通常是个麻烦(Bless)
  • 4 逻辑处理以及跳转。有些是视图函数内容的,相对还清晰,有些则是埋在前端的超链,这部分内容过去通常需要联调(麻烦)

设计/制造原则

1 先确定能确定的基本组件,按规则-图的方式去组织。
2 假设基本组件是不可靠的,需要增加追踪,控制和冗余。
3 优化基本组件(基本组件本身也是由更基本的组件构成的),直到其客观稳定性达到6个9以上。
4 通过冗余设计和控制方法,使系统的可靠性达到足够多的9。

我们还是先从可能出错的角度出发切入(毕竟火烧眉毛很烦人)。简单来说,是采用类似日志的方法去记录,但是不是简单的日志(服务器那么多日志,被使用的有多少?)。

1.2 LogicLog(逻辑日志)

这个是我自己编写的类,几个出发点,或者说假设点如下:

  1. 主程序(ll_mainstream)和子程序(ll_funcstream)。假设有一个主流程和若干子流程,例如某个用户点击了某个链接,为了完成这个工作所执行的若干个视图函数就是主程序(views1->view2…)。每个视图函数又有若干个处理过程叫子程序(func1 -> func2)。
  2. 调试模式和批量模式。批量模式不保留数据,只记录函数的路径(如果失败就是出错了);调试模式记录input, output的具体数据。
  3. 全局号(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}}

2 链式函数(函数字典)

可以看到,LogicLog 虽然保存了很多有用的信息,但是没考虑依赖,这要把之前函数字典的内容拉回来,重新整理。

简单来说是这样:

  • 1 函数通过一个函数字典(Funcdict)管理。可以为每一个项目(类似LogicLog,由gid唯一指定),或者也可以做一个大的通用函数字典,这个字典主要是通过键值关联了需要操作的函数。
  • 2 输入字典(Inputdict)和参数字典(Paramdict)。输入字典是用户输入的部分(例如输入一张图片矩阵),参数字典是算法控制相关的部分(例如学习率)。参数字典也可以先存盘,用户的输入字典先不存,未来会存元数据。例如,会记录用户输入了一个100列的DataFrame,而不是里面的内容。
  • 3 用户在使用时知道依赖。通过一个字典列表把操作连起来,默认的假设是all(即每一步的执行都需要前一步为真)。这里和LogicLog配合起来就比较完美了。
  • 4 分支指定。进一步,用户可以通过msg指定如何分支,这样在循环执行时,每次都会判断接下来要去哪。例如True, msg1 指向函数1,而True, msg2指向函数2。这需要用户在函数输出时按照之前提的规范来,输出包含status, msg, meta,data的字典。

这个放在另一篇讲。

你可能感兴趣的:(全栈,python)