6.常见问题回答

直接统一回复常见问题,例如是不是模仿celery

6.1 你干嘛要写这个框架?和celery 、rq有什么区别?

你干嘛要写这个框架?和celery 、rq有什么区别?是不是完全重复造轮子为了装x?

见第二章的解释,有接近20种优势。
celery 从性能、用户编码需要的代码量、用户使用难度 各方面都远远差于此框架。
可以使用例子中的场景代码进行了严格的控制变量法实际运行对比验证。

6.2 为什么包的名字这么长?

为什么包的名字这么长,为什么不学celery把包名取成 花菜 茄子什么的?

答: 为了直接表达框架的意思。现在代码在ide都能补全,名字长没关系。
生产消费模式不是celery专利,是通用常见的编程思想,不是必须用水果取名。

6.3 框架是使用什么序列化协议来序列化消息的。

   答:框架默认使用json。并且不提供序列化方式选择,有且只能用json序列化。json消息可读性很强,远超其他序列化方式。
   默认使用json来序列化和反序列化消息。所以推送的消息必须是简单的,不要把一个自定义类型的对象作为消费函数的入参,
   json键的值必须是简单类型,例如 数字 字符串 数组 字典这种。不可以是不可被json序列化的python自定义类型的对象。
   
   用json序列化已经满足所有场景了,picke序列化更强,但仍然有一些自定义类型的对象的实例属性由于是一个不可被序列化
   的东西,picke解决不了,这种东西例如self.r = Redis(),而redis对象又包括threding.Lock类型的属性 ,不可以被pike序列化

   就算能序列化的对象也是要用一串很长的东西来。
   用pike来序列化复杂嵌套属性类型的对象,不仅会导致中间件要存储很大的东西传输效率会降低,在编码和解码也会消耗更多的cpu。如果框架支持了pike序列化,会让使用者养成不好的粗暴习惯。
   想消费函数传redis对象作为入参,这种完全可以使用json来解决,例如指定ip 和端口,在消费函数内部来使用redis。所以用json一定可以满足一切传参场景。
   
   如果json不能满足你的消费任务的序列化,那不是框架的问题,一定是你代码设计的问题。所以没有预留不同种类序列化方式的扩展,
   也不打算准备加入其他序列化方式。

6.4 框架如何实现定时?

答:使用的是定时发布任务,那么就能定时消费任务了。导入fsdf_background_scheduler然后添加定时发布任务。

FsdfBackgroundScheduler继承自 apscheduler 的 BackgroundScheduler,定时方式可以百度 apscheduler

6.5 为什么强调是函数调度框架不是类调度框架,不是方法调度框架?

为什么强调是函数调度框架不是类调度框架,不是方法调度框架?你代码里面使用了类,是不是和此框架水火不容了?

问的是consuming_function的值能不能是一个类或者一个实例方法。

   答:一切对类的调用最后都是体现在对方法的调用。这个问题莫名其妙。
   celery rq huery 框架都是针对函数。
   调度函数而不是类是因为:
   1)类实例化时候构造方法要传参,类的公有方法也要传参,这样就不确定要把中间件里面的参数哪些传给构造方法哪些传给普通方法了。
      见5.8
   2) 这种分布式一般要求是幂等的,传啥参数有固定的结果,函数是无依赖状态的。类是封装的带有状态,方法依赖了对象的实例属性。
   3) 比如例子的add方法是一个是实例方法,看起来好像传个y的值就可以,实际是add要接受两个入参,一个是self,一个是y。如果把self推到消息队列,那就不好玩了。
      对象的序列化浪费磁盘空间,浪费网速传输大体积消息,浪费cpu 序列化和反序列化。所以此框架的入参已近说明了,
      仅仅支持能够被json序列化的东西,像普通的自定义类型的对象就不能被json序列化了。
       celery也是这样的,演示的例子也是用函数(也可以是静态方法),而不是类或者实例方法,
       这不是刻意要和celery一样,原因已经说了,自己好好体会好好想想原因吧。
   
   框架如何调用你代码里面的类。
   假设你的代码是:
   class A():
      def __init__(x):
          self.x = x
       
      def add(self,y):
          return self.x + y
   
   那么你不能 a =A(1) ; a.add.push(2),因为self也是入参之一,不能只发布y,要吧a对象(self)也发布进来。
   add(2)的结果是不确定的,他是受到a对象的x属性的影响的,如果x的属性是100,那么a.add(2)的结果是102.
   如果框架对实例方法,自动发布对象本身作为第一个入参到中间件,那么就需要采用pickle序列化,picke序列化对象,
   消耗的cpu很大,占用的消息体积也很大,而且相当一大部分的对象压根无法支持pickle序列化。
   无法支持序列化的对象我举个例子,
   
import pickle
import threading
import redis

class CannotPickleObject:
   def __init__(self):
       self._lock = threading.Lock()


class CannotPickleObject2:
   def __init__(self):
       self._redis = redis.Redis()

print(pickle.dumps(CannotPickleObject())) # 报错,因为lock对象无法pickle
print(pickle.dumps(CannotPickleObject2())) # 报错,因为redis客户端对象也有一个属性是lock对象。

以上这两个对象如果你想实例化,那就是天方夜谭,不可能绝对不可能。
真实场景下,一个类的对象包含了很多属性,而属性指向另一个对象,另一个对象的属性指向下一个对象,
只要其中某一个属性的对象不可pickle序列化,那么此对象就无法pickle序列化。
pickle序列化并不是全能的,所以经常才出现python在win下的多进程启动报错,
因为windows开多进程需要序列化入参,但复杂的入参,例如不是简单的数字 字母,而是一个自定义对象,
万一这个对象无法序列化,那么win上启动多进程就会直接报错。

        
所以如果为了调度上面的class A的add方法,你需要再写一个函数
def your_task(x,y):
   return  A(x).add(y)
然后把这个your_task函数传给框架就可以了。所以此框架和你在项目里面写类不是冲突的,
本人是100%推崇oop编程,非常鲜明的反对极端面向过程编程写代码,但是此框架鼓励你写函数而不是类+实例方法。
框架能支持@staticmethod装饰的静态方法,不支持实例方法,因为静态方法的第一个入参不是self。
   
   
如果对以上为什么不支持实例方法解释还是无法搞明白,主要是说明没静下心来仔细想想,
如果是你设计框架,你会怎么让框架支持实例方法?

statckflow上提问,celery为什么不支持实例方法加@task
https://stackoverflow.com/questions/39490052/how-to-make-any-method-from-view-model-as-celery-task

celery的作者的回答是:

You can create tasks out of methods. The bad thing about this is that the object itself gets passed around 
(because the state of the object in worker has to be same as the state of the caller) 
in order for it to be called, so you lose some flexibility. So your object has to be pickled every 
time, which is why I am against this solution. Of course this concerns only class methods, s
tatic methods have no such problem.

Another solution, which I like, is to create separate tasks.py or class based tasks and call the methods 
from within them. This way, you will have FULL control over Analytics object within your worker.

这段英文的意思和我上面解释的完全一样。所以主要是你没仔细思考想想为什么不支持实例方法。
 

6.6 是怎么调度一个函数的。

    答:基本原理如下
    
    def add(a,b):
        print(a + b)
        
    从消息中间件里面取出参数{"a":1,"b":2}
    然后使用  add(**{"a":1,"b":2}),就是这样运行函数的。

6.7 框架适用哪些场景?

     答:分布式 、并发、 控频、断点接续运行、定时、指定时间不运行、
         消费确认、重试指定次数、重新入队、超时杀死、计算消费次数速度、预估消费时间、
         函数运行日志记录、任务过滤、任务过期丢弃等数十种功能。
        
         只需要其中的某一种功能就可以使用这。即使不用分布式,也可以使用python内置queue对象。
         这就是给函数添加几十项控制的超级装饰器。是快速写代码的生产力保障。
         
         适合一切耗时的函数,不管是cpu密集型 还是io密集型。
         
       不适合的场景主要是:
          比如你的函数非常简单,仅仅只需要1微妙 几十纳秒就能完成运行,比如做两数之和,print一下hello,这种就不是分需要使用这种框架了,
          如果没有解耦的需求,直接调用这样的简单函数她不香吗,还加个消息队列在中间,那是多此一举。
          

6.8 怎么引入使用这个框架?门槛高不高?

 答:先写自己的函数(类)来实现业务逻辑需求,不需要思考怎么导入框架。
     写好函数后把 函数和队列名字绑定传给消费框架就可以了。一行代码就能启动分布式消费。
     在你的函数上面加@task_deco装饰器,执行 your_function.conusme() 就能自动消费。
     所以即使你不想用这个框架了,你写的your_function函数代码并没有作废。
     所以不管是引入这个框架 、废弃使用这个框架、 换成celery框架,你项目的99%行 的业务代码都还是有用的,并没有成为废物。
     别的框架如flask换django,scrapy换spider,代码形式就成了废物。

6.9 怎么写框架?

 答: 需要学习真oop和36种设计模式。唯有oop编程思想和设计模式,才能持续设计开发出新的好用的包甚至框架。
     如果有不信这句话的,你觉得可以使用纯函数编程,使用0个类来实现这样的框架。
     
     如果完全不理会设计模式,实现threding gevent evenlet 3种并发模式,加上10种中间件类型,实现分布式消费流程,
     需要反复复制粘贴扣字30次。代码绝对比你这个多。例如基于nsq消息队列实现任务队列框架,加空格只用了80行。
     如果完全反对oop,需要多复制好几千行来实现。

     例如没听说设计模式的人,在写完rabbitmq版本后写redis版本,肯定十有八九是在rabbitmq版本写完后,把整个所有文件夹,
     全盘复制粘贴,然后在里面扣字母修改,把有关rabbitmq操作的全部扣字眼修改成redis。如果某个逻辑需要修改,
     要在两个地方都修改,更别说这是10几种中间件,改一次逻辑需要修改10几次。
     我接手维护得老项目很多,这种写法的编程思维的是特别常见的,主要是从来没听说设计模式4个字造成的,
     在我没主动学习设计模式之前,我也肯定会是这么写代码的。
     
     
     只要按照36种设计模式里面的oop4步转化公式思维写代码三个月,光就代码层面而言,写代码的速度、流畅度、可维护性
     不会比三年经验的老程序员差,顶多是老程序员的数据库 中间件种类掌握的多一点而已,这个有机会接触只要花时间就能追赶上,
     但是编程思维层次,如果没觉悟到,可不是那么容易转变的,包括有些科班大学学过java的也没这种意识,
     非科班的只要牢牢抓住把设计模式 oop思维放在第一重要位置,写出来的代码就会比科班好,
     不能光学 if else 字典 列表 的基本语法,以前我看python pdf资料时候,资料里面经常会有两章以上讲到类,
     我非常头疼,一看到这里的章节,就直接跳过结束学习了,现在我也许只会特意去看这些章节,
     然后看资料里面有没有把最本质的特点讲述好,从而让用户知道为什么要使用oop,而不是讲下类的语法,这样导致用户还是不会去使用的。
     
     
     你来写完包括完成10种中间件和3种并发模式,并且预留消息中间件的扩展。
     然后我们来和此框架 比较 实现框架难度上、 实现框架的代码行数上、 用户调用的难度上 这些方面。

6.10 框架能做什么

答:你在你的函数里面写什么,框架就是自动并发做什么。
框架在你的函数上加了自动使用消息队列、分布式、自动多进程+多线程(协程)超高并发、qps控频、自动重试。
只是增加了稳定性、扩展性、并发,但做什么任务是你的函数里面的代码目的决定的。

只要是你代码涉及到了使用并发,涉及到了手动调用线程或线程池或asyncio,那么就可以使用此框架,
使你的代码本身里面就不需要亲自操作任何线程 协程 asyncio了。

不需要使用此框架的场景是函数不需要消耗cpu也不需要消耗io,例如print("hello"),如果1微秒就能完成的任务不需要使用此框架。

6.11 日志的颜色不好看或者觉得太绚丽刺瞎眼,想要调整。


一 、关于日志颜色是使用的 \033实现的,控制台日志颜色不光是颜色代码决定的,最主要还是和ide的自身配色主题有关系,
同一个颜色代码,在pycahrm的十几个控制台颜色主题中,表现的都不一样。
所以代码一运行时候就已经能提示用户怎么设置优化控制台颜色了,文这个问题说明完全没看控制台的提示。
"""
1)使用pycharm时候,建议重新自定义设置pycharm的console里面的主题颜色。
   设置方式为 打开pycharm的 file -> settings -> Editor -> Color Scheme -> Console Colors 选择monokai,
   并重新修改自定义6个颜色,设置Blue为1585FF,Cyan为06B8B8,Green 为 05A53F,Magenta为 ff1cd5,red为FF0207,yellow为FFB009。         
2)使用xshell或finashell工具连接linux也可以自定义主题颜色,默认使用shell连接工具的颜色也可以。

颜色效果如连接 https://i.niupic.com/images/2020/11/04/8WZf.png

在当前项目根目录的 nb_log_config.py 中可以修改当get_logger方法不传参时后的默认日志行为。
"""



二、关于日志太绚丽,你觉得不需要背景色块,在当前项目根目录的 nb_log_config.py 中可以设置
DISPLAY_BACKGROUD_COLOR_IN_CONSOLE = False  # 在控制台是否显示彩色块状的日志。为False则不使用大块的背景颜色。

6.12 是不是抄袭模仿 celery

答:有20种优势,例如celery不支持asyncio、celery的控频严重不精确,光抄袭解决不了。
我到现在也只能通过实际运行来达到了解推车celery的目的,并不能直接默读代码就搞懂。
celery的层层继承,特别是层层组合,又没多少类型提示,说能精通里面每一行源码的人,多数是高估自己自信过头了。

celery的代码太魔幻,不运行想默读就看懂是不可能的,不信的人可以把自己关在小黑屋不吃不喝把celery源码背诵3个月,
然后3个月后 试试默写能不能写出来实现里面的兼容 多种中间件 + 多种并发模式 + 几十种控制方式的框架。

这是从一个乞丐版精简框架衍生的,加上36种设计模式付诸实践。

此框架运行print hello函数, 性能强过celery 20倍以上(测试每秒消费次数,具体看我的性能对比项目)。
此框架支持的中间件比celery多
此框架引用方式和celery完全不一样,完全不依赖任何特定的项目结构,celery门槛很高。
此框架和celery没有关系,没有受到celery启发,也不可能找出与celery连续3行一模一样的代码。
这个是从原来项目代码里面大量重复while 1:redis.blpop()  发散扩展的。

这个和celery唯一有相同点是,都是生产者 消费者 + 消息队列中间件的模式,这种生产消费的编程思想或者叫想法不是celery的专利。
包括我们现在java框架实时处理数据的,其实也就是生产者 消费者加kfaka中间件封装的,难道java人员开发框架时候也是需要模仿一下python celery源码或者思想吗。
任何人都有资格开发封装生产者消费者模式的框架,生产者 消费者模式不是celery专利。生产消费模式很容易想到,不是什么高深的架构思想,不需要受到celery的启发才能开发。

6.13 使用此框架时候,在一个python项目中如何连接多个相同种类的消息队列中间件ip地址

这个问题是问一个项目中,有些脚本要连接 192.168.0.1的redis ,有些脚本要连接192.168.0.2的redis,但框架配置文件只有一个,如何解决?

例如目录结构是
your_proj/
      distributed_frame_config.py   (此文件是第一次启动任意消费脚本后自动生成的,用户按需修改配置)
      dira/a_consumer.py  (此脚本中启动funa函数消费)
      dirb/b_consumer.py   (此脚本中启动funb函数消费)
      
如果funa函数要连接 192.168.0.1的redis,funb函数要连接192.168.0.2的redis,有两种解决方式

第一种是在启动消费的脚本,脚本里面手动调用 patch_frame_config()函数来设置各种中间件的值

第二种是 把 distributed_frame_config.py  分别复制到dira和dirb文件夹.
这种就会自动优先使用 a_consumer.py和b_consumer.py同文件夹层级的配置了,
而非是自动优先读取python项目根目录的配置文件,这个是利用了python语言的import 模块导入优先级机制。

6.14 什么是确认消费?为什么框架总是强调确认消费?

发布端:

from scripxx  import fun

for i in range(10):
    fun.push(i)

消费端:

import time
from function_scheduling_distributed_framework import task_deco

@task_deco('test_confirm')
def fun(x):
    print(f'开始处理 {x}')
    time.sleep(120)
    print(f'处理完成 {x}')

fun.consume()
启动消费脚本后,任意时刻随意强制反复关闭重启消费代码,只要函数没有完整的执行完成,函数参数就不会丢失。达到了消息万无一失。
具体的那些中间件消费者支持消费确认,具体见 3.1 介绍。
实现了4种redis消息队列中间件,其中有3种是确认消费的。

确认消费很重要,如果你自己写个简单粗暴的 while 1:redis.blpop()的脚本,你以为是可以断点接续呢,
在多线程并发执行函数时候,大量的消息会丢的很惨。导致虽然是断点接续但你不敢随意重启。

6.15 如何等待队列中的消息全部消费完成

如果有这种需求需要等待消费完成,使用 wait_for_possible_has_finish_all_tasks()

f.consume()
f.wait_for_possible_has_finish_all_tasks(minutes=3)  # 框架提供阻塞方法,直至队列任务全部消费完成,才会运行到下一行。
print("over")   # 如果不加上面那一行,这个会迅速打印over

6.16 框架支不支持函数上加两个装饰器?

如图所示,消费函数上支持两个装饰器,加100个装饰器都可以。

注意事项:
1、task_deco需要放在最上面。
2、不要忘了给装饰器内部函数加上 wraps(f),否则如果使用 fun.multi_process_consume(2)多进程方式无法启动消费。
 wraps是将 被修饰的函数(wrapped) 的一些属性值赋值给 修饰器函数(wrapper)

../_images/img_20.pngimg_20.png

6.17 嫌框架日志记录太详细?

日志是了解当前框架正在运行什么的好手段,不然用户懵逼不知道背后在发生执行什么。
@task_deco 装饰器设置 log_level=20 或logging.INFO,就不会再记录框架正在运行什么函数了。
如图再装饰器加上 log_level=20后,框架以后就再也不会记录框架正在运行什么函数入参结果是什么了。

../_images/img_31.pngimg_31.png

6.18 框架与你项目依赖的三方包版本不一致冲突?

用户完全可以自由选择任何三方包版本。例如你的 sqlalchemy pymongo等等与框架需要的版本不一致,你完全可以自由选择任何版本。
我开发时候实现了很多种中间件,没有时间长期对每一种中间件三方包的每个发布版本都做兼容测试,所以我固定死了。
用户完全可以选择自己的三方包版本,大胆点,等报错了再说,不出错怎么进步,不要怕代码报错,请大胆点升级你想用的版本。

如果是用requirements.txt方式自动安装三方包,我建议你在文件中第一行写上 function_scheduling_distributed_framework,之后再写其它包
这样就能使用你喜欢的版本覆盖此框架依赖的版本了。
等用的时候报错了再说,提isuu我做兼容适配。一般不会报错的大胆点。