故事背景这段时间在做一个nginx+uwsgi+python的项目。有一种需求,就是在服务运行的过程中可以更改配置并生效,可以理解为Hotreload。以前,这些配置都是硬编码在项目配置文件中的基础配置,通常是python项目中的config.py文件。现在配置更改使用开源的apollo作为管理终端,python需要使用客户端连接apollo。先看一个常见的python后台使用uwsgi配置:test@python:~/app$catuwsgi.ini[uwsgi]module=appwsgi-file=app.pymaster=trueprocesses=4#多个工作进程enable-threads=true#允许多线程#lazy-apps=true#稍后再说http=:3000die-on-term=truepidfile=./uwsgi.pidchdir=/home/test/appdisable-logging=truelog-maxsize=5000000daemonize=/home/test/app/log.log这是python代码的演示app.py:fromflaskimportFlask,jsonify,来自阿波罗的请求importConfigcf=Config("test","application")print("-----------key------------")print(cf.SQLALCHEMY_TRACK_MODIFICATIONS)#尝试获取一些配置print(cf.LOG_NAME)print("--------key---------")app=Flask(__name__)@app.route('/')defhello_world():key=request.values.get('key')new=getattr(cf,key)#尝试获取配置实时返回njsonify({'data':new,'apo':cf.apo.get_value(key),"my":cf.SQLALCHEMY_POOL_SIZE})application=app#foruwsgi.iniif__name__=="__main__":app.run(port=5000)看看这个配置启动后的效果:test@python:~/app$ps-ef|grepuwsgi.initest162241014:36?00:00:00uwsgi--iniuwsgi.initest1622516224014:36?00:00:00uwsgi--iniuwsgi.initest1622616224014:36?00:00:00uwsgi--iniuwsgi.initest1622716224014:36?uwsgi--iniuwsgi.initest1622816224014:36?00:00:00uwsgi--iniuwsgi。initest1622916224014:36?00:00:00uwsgi--iniuwsgi。39pts/4800:00:00grep--color=autouwsgi.ini然后apollo后台每次改配置都会出问题我尝试了apollo开源说明中推荐的三个python客户端,发现实现方法类似,主要是启动守护线程长链接拉取服务器接口,当服务器发生变化时,可以访问该接口,然后触发守护线程的动作去更新缓存和本地文件。它说本地文件已更新。为什么缓存没有更新?带着疑惑去看了这三个开源库的issues,然后找到了uwsgi+django项目中配置的apollo获取不到最新的apollo数据。好吧,这似乎是一个普遍的问题。经验证猜想,其他语言没有类似问题。它可能是python的一个特性吗?我们先做一个手册。多进程尝试:1.执行pythonapp.py2。修改app.py3中的端口号。执行pythonapp.py4。重复2,35。注意打印的log6。尝试访问设置的端口curl"127.0.0.1:3000"7.修改apollo的配置8.查看日志,然后执行curl"127.0.0.1:3000",查看获取到的配置是否是最新的。然后发现没有问题,每个实例都是可以访问到最新的,并且在日志中打印了更新缓存和localfile的日志。那么python的问题就排除了。下面重点介绍uwsgi的配置。网上一搜,乱七八糟。一般最好搜索一下官方文档,比如HereisaquickstarttothePython/WSGIapplication,然后你会在左边看到一个关于Python线程的通知。额,是不是我没有加enable-threads=true?立即尝试添加,效果还是不行,然后继续阅读文档,翻遍目录,直到看到这个优雅的重载艺术,文档中的一些关键语句摘录如下:PreforkingVSlazy-appsVSlazyThisisuWSGI项目的有争议的选择之一。默认情况下,uWSGI是在一个进程中加载??整个应用程序,然后在应用程序加载完成后多次fork()自身。这是一种常见的Unix模式,它可能会大大减少应用程序的内存使用,允许很多有趣的技巧,并且在某些语言中,可能会让你很烦。尽管名声在外,uWSGI是作为一个Perl应用程序服务器诞生的(它不叫uWSGI,也不是开源的),而preforking在Perl世界中通常是一种祝福。然而,对于许多其他语言、平台和框架而言,情况并非如此,因此在开始处理uWSGI之前,您应该选择如何管理堆栈中的fork()。从“优雅重新加载”的角度来看,preforking大大提高了速度:只加载一次你的应用程序,生成额外的worker会非常快。避免堆栈中的每个worker都访问磁盘可以减少启动时间,特别是对于花费大量时间访问磁盘以查找模块的框架或语言。不幸的是,preforking方法会强制您在修改代码时重新加载整个堆栈,而不仅仅是重新加载工作人员。除此之外,您的应用程序可能需要预分叉,或者由于它的开发方式,可能会因此完全崩溃。相反,惰性应用程序模式会为每个工作人员加载一次您的应用程序。它将占用大约O(n)负载(其中n是工作人员的数量),很可能会消耗更多内存,但会在更加一致和干净的环境中运行。请记住:lazy-apps与lazy不同,前者只是指示uWSGI为每个worker加载一次应用程序,而后者更具侵略性(通常不鼓励),因为它改变了很多内部默认行为。好像是默认配置导致了多进程多线程。uwsgi加载第一个完整的工作后,processes中配置的其余工作通过fork来完成。查看uwsgi的启动日志,你会发现确实如此。只加载一个app,每次操作只有一个守护线程在监控和打印日志,为什么fork不是一个完整的服务呢?这是关于unixfork的原理和实现。在unix/linux操作系统中,提供了一个fork()系统函数,它具有这些特点:0.fork()函数用于从一个已有的进程中创建一个新进程,这个新进程称为“子进程”。创建子进程的进程称为“父进程”。使用fork()函数得到的子进程是父进程的副本。子进程完整复制父进程的资源,包括进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述等。字符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等信息,子进程与父进程的区别在于进程号、资源使用和定时器。1.普通函数调用,调用一次,返回一次,但是fork()调用一次,返回两次。因为操作系统会自动复制当前进程(父进程)(子进程),然后分别在父进程和子进程中返回。2、子进程一直返回0,父进程返回子进程的ID。3.一个父进程可以fork()多个子进程。因此,父进程需要记录每个子进程的ID,子进程只需要调用getppid()获取父进程的id即可。getpid()可以得到当前进程id4。父进程和子进程的执行顺序是无规律的,完全取决于操作系统的调度算法。5、如果父进程有多个线程,是否会复制父进程的多个线程?实际上,创建子进程时只有一个线程,就是调用fork()函数的线程。也就是说,uwsgi在fork进程时(不区分进程和线程),只会复制当前正在执行的app线程,而不会fork出app线程初始化过程中产生的守护线程apollo-client。那么解决方法就简单了,配置lazy-apps=true即可,每个fork都是一个真正完整的app进程,包括app线程和apollo-client线程。如果我没说清楚,可以参考这里Cautionoususeofforkinmulti-threaded多线程进程的坑(转)然后自然而然地想到,既然缓存是独立于各个进程的,那么干脆去掉缓存再使用localfile,完成多进程共享配置的功能也很简单粗暴,每次访问配置都做文件IO操作。如果不是访问量大的服务,可以这样做。让我们谈谈其他解决方案。在apollo客户端中使用cache在线程中重构cache缓存的存储方式,比如切换到Redis,同样的IO操作比每次直接通过http查询apollo配置界面要好。如果是远程redis-server,网络延迟不容忽视。然后考虑本地redis或者使用uWSGI缓存框架,在应用程序中使用缓存API访问Cache,你可以通过缓存API访问你的实例或者远程实例中的各种缓存。目前,公开了以下函数(每种语言的命名可能与标准略有不同):cache_get(key[,cache])cache_set(key,value[,expires,cache])cache_update(key,value[,expires,cache])cache_exists(key[,cache])cache_del(key[,cache])cache_clear([cache])如果调用此缓存API的语言/平台区分字符串和字节(例如Python3和Java),那么您必须假设键是字符串,值是字节(或者在java下,字节数组)。否则,键和值都是编码中性字符串,因为在内部,缓存值和缓存键是简单的二进制blob。expires参数(默认为0,表示禁用)是对象将过期(并在未设置purge_lru时被缓存清道夫删除,见下文)之前的秒数,缓存参数是所谓的“魔法”identifier”,其语法为嗯,到这里这个问题已经解决了一半。为什么说一半,因为这些配置是普通配置,不是类似mysql、redis的配置信息。这些配置修改配置后不会重新生成实例,所以没有如何使用最新的mysql或redis配置,那怎么办呢?让我们谈谈超载服务。如何优化重载服务的重启服务?命令重启uwsgi服务,然后监听线程的监听函数。实现如下,pid_path为uwsgi启动后生成的pid文件地址。简单粗暴但有效。#重载uwsgidefrelaod_uwsgi(pid_path):"""Option1"""print("------------relaod_uwsgi----------------")val=os.system('uwsgi--reload{}'.format(pid_path))print(val)ifval:print("Restartmayhaveencounteredproblems...")另一种方式py-auto-reloadargument:Requiredparameterparser:uwsgi_opt_set_intflags:UWSGI_OPT_THREADS|UWSGI_OPT_MASTERhelp:Monitorpythonmodulemtimetotriggerreload(useonlyduringdevelopment)py-autoreloadargument:必需的参数解析器-reloadargument:必需的参数解析器:uwsgi_opt_set_intflags:UWSGI_OPT_THREADS|UWSGI_OPT_MASTERhelp:监控python模块mtime以仅在开发中触发重新加载(使用)python-autoreloadargument:必需的参数解析器:uwsgi_opt_set_intflags:UWSGI_OPT_THREADS|UWSGI_OPT_MASTERhelp:monitorpythonmodulemtimetotriggerreload(仅在开发时使用)py-auto-reload-ignorargument:需要的参数parser:uwsgi_opt_add_string_listflags:UWSGI_OPT_THREADS|UWSGI_OPT_MASTERhelp:在自动重载扫描时忽略指定的模块(可以指定多次)这些配置是为了监控特定的文件使uwsgi服务过载,那么我们只需要将localfile的名字改成py结尾即可,几乎没有问题。抛开一些东西最后想说说一些私人物品,人类不可能想象出意识范围之外的东西,比如做梦。梦里的东西,一定是日常生活中拼凑而成,伪装成的,密码都是一样的。其创新也。这里有一个矿坑贡献的python客户端demo,主要代码在apollo-client-python。我把里面的http请求改成使用requests,然后做了一点浅层包装。欢迎大家star!本篇笔记也存档在这里python-mini,欢迎大家star!最后:不要盲目复制粘贴!动动脑筋,尝试调整显示的配置以满足您的需要,或创建新的配置。每个应用程序和系统都互不相同。在做出选择之前进行试验。上面这句话不是我说的,是uwsgi文档说的。
