python热更新原理_Python功能点实现:数据热更新
关键词:热更新 | 热重载 | 定时更新 | 即时更新 | 缓存 | functools | cachetools | LRU | TTL
假设应用需要加载一个配置文件config.txt,一般的做法类似于:
with open('config.txt') as f:
parameters = f.read()
接下来parameters中存储的数据就可以被其他代码使用,但是这样写的话程序每次启动后,数据是固定死的,无法动态地自我更新,每次要修改配置/模型只能重启整个应用。
本文中热更新的意思是在应用运行时内,从外部(如文件、数据库、REST API)中获得数据并更新应用内的Python对象。应用场景一般是应用作为服务(有对外的API),需要在不重启的前提下更新自己的配置参数或者算法模型。
热更新可以分为两种:定时更新(periodic update)和即时更新(on-call update),前者周期性地、主动地执行更新,后者则是被动地等待,直到接收到某种来自应用外部的信号才会执行更新。本文将通过内存缓存(Memory Cache)和装饰器(Decorator)技术来实现两种热更新。
内存缓存
先来说明一下内存缓存。缓存中的数据一般以键值对(key-value pair)的形式存在,value中放数据本身,key中放数据的某种描述名。缓存的容量决定了其最大可容纳的数据条数,当容量已满时再向缓存中存入新的数据,缓存就会采取开始清理行为:清除掉已存的部分数据,从而为新数据腾地方。清理的策略(判定何时需要清理、具体如何清理数据)有很多种,决定了缓存的不同类型。
Python中缓存常被写成装饰器的形式,缓存数据的key里放的是被装饰原函数的参数值组合(key的生成方法可以不同,后面还会提到),value里放的则是原函数的返回值。这样当函数被调用时,程序会先去缓存数据里找是不是已经有相同的参数值,如果有就直接返回已缓存的返回值,不重复进行原函数内的计算。
注意缓存的使用有一个隐含前提:函数本身是无状态的。假如函数内引用了全局变量,或者存在闭包,那同样的参数值不一定必然计算出相同的返回值。这样缓存的返回值和实际期望的返回值就不一定一致了。
定时热更新
定时更新的实现使用了来源于第三方库cachetools的TTLCache,TTL(Time-to-Live)指存在时长策略。这种缓存为每一条存入的数据记录其存在的时长。每次调用都会检查是否存在超过某个设定时长阈值的数据,如果有就会开始清理行为:所有超时数据都会被清除掉;如果没有超时数据,缓存将会换用LRU策略,使缓存不超出容量大小(下一部分会提到LRU的具体策略)。
当我们将TTL缓存的容量设为1时、且用于加载数据的原函数参数不变的情况下,逻辑就变成了定时更新:
未超过时长:缓存保留,每次调用都使用缓存数据
超过时长,缓存清空(只有一条数据),程序重新计算(在这里即重新加载数据)
示例代码如下(运行需要安装cachetools,并在文首链接里下载完整的项目):
import time
import cachetools
from utils import change_conf_file
ROTATE = 5
@cachetools.cached(cachetools.TTLCache(1, ROTATE))
def reload():
print('Cache cleared, reloading config...')
with open('config.txt') as f:
parameters = f.read()
return parameters
class Model():
def log(self):
self.model = reload()
print(self.model)
if __name__ == '__main__':
# Reload automatically every [ROTATE] seconds
model = Model()
while True:
time.sleep(2)
change_conf_file() # change data
model.log()
即时热更新
即时更新的实现使用了来源于Python内置库functools的lru_cache,LRU(Least Recently Used)指最少使用策略。这种缓存为每一条存入的数据记录其被使用的次数,每次调用都会检查缓存大小是否超出容量,如超出就会开始清理行为:会从使用次数最少的数据开始清理,直到缓存大小处于容量以内。
当我们将LRU缓存的容量设为1、且用于加载数据的原函数参数不变的情况下,原函数只有在第一次被调用时才会发生计算,之后调用都会直接返回缓存中的数据,到这里与一般的读取效果上并无区别。当我们需要热更新数据的时候,只需要主动清空缓存。如下例中Getter.getModel.cache_clear()。其中Getter.getModel是装饰后的函数,其中带有用于清理缓存的函数cache_clear()。有了这个扳机,我们只需要额外开发一个API(比如REST下的GET)来触发它,这样通过外部即时call API就可以进行热更新了。
示例代码如下(运行需要在文首链接里下载完整的项目):
import time
from functools import lru_cache
from utils import change_conf_file
class Getter:
@staticmethod
@lru_cache(1)
def getModel():
with open('config.txt') as f:
model = f.read()
return model
class Model():
def log(self):
self.model = Getter.getModel()
print(self.model)
if __name__ == '__main__':
# Reload only when cache_clear() is called
model = Model()
while True:
model.log()
time.sleep(2)
change_conf_file() # change data
Getter.getModel.cache_clear()
print('Cache cleared, reloading config...')
model.log()
这里补充一个细节,上面的示例中被缓存装饰器装饰的原函数getModel是一个无参数的函数,这种情况下lru_cache是如何运作的呢?lru_cache的实现中使用函数functools._make_key来生成缓存的key。在Python中,当原函数无参数时,默认参数args可认为是空元组(),可选参数可认为是空字典{},在这种情况下生成的key将会是空列表[](注意列表是不可hash的,不可直接作为字典的key,functools里的实际数据结构较为复杂,这里没有深入)。上一部分提到的第三方库cachetools实现了类似的方法keys.typedkey,两者生成的key存在区别,但是结合其他方法,行为在大部分情况是一样的,包括本文中的无参数函数情况。
import time
from functools import _make_key
from cachetools.keys import typedkey
if __name__ == '__main__':
print(_make_key((), {}, False)) # []
print(typedkey((), {}, False)) # ((), {}, , )
扩展问题
多线程情况:本文的热更新方法均基于缓存,而由于缓存涉及到读写操作,在多线程环境下我们需要考虑其正确性。functools.lru_cache和cachetools.TTLCache里均有使用到锁的机制,再考虑到Python的GIL锁,本文所述的热更新在线程安全上应该算是有保障的,但目前未经试验无法完全下断言。更正:functools.lru_cache的文档中提到多线程环境下hit和miss的计数只是近似值,而Cachetool的文档中则明确提到cachetools.TTLCache这样的类是非线程安全的,需要额外提供锁以实现同步。最开始写这篇文章的时候我对GIL的理解有误,按我现在的理解,GIL宽泛地讲只会阻止多线程调用多个CPU,但同一个CPU下的多线程仍然是有效的(不然还要多线程干嘛),所以同步的问题还是要考虑清除。
import:假如我们在一个模块(module)里更新model,而另一个模块import这个model,那么当原模块的model热更新后,import得到的model并不会更新,这种行为可能与Python自身的module cache有关。要实现所有module的热更新,能考虑的一个办法是让数据自己成为一个模块,使其变得可以在模块之间共享。
吐槽:这篇写得我浑身无力啊,本来觉得很简单,就平时用的小东西拿来拎拎清,没想到越拎越深...很多时候我们很happy是因为我们站在冰山的最上面,不用面对水下的魔鬼细节...作为搞技术的,我们还是不能光看脸,也要多盯裆(≖_≖)✧
python热更新原理_Python功能点实现:数据热更新相关推荐
- python程序的原理_Python程序的执行原理(转)
1. 过程概述 Python先把代码(.py文件)编译成字节码,交给字节码虚拟机,然后虚拟机一条一条执行字节码指令,从而完成程序的执行. 2. 字节码 字节码在Python虚拟机程序里对应的是PyCo ...
- 简述python程序执行原理_Python程序的执行原理(1)
test.py的指令序列 func函数的指令序列 第一列表示以下几个指令在py文件中的行号; 第二列是该指令在指令序列co_code里的偏移量; 第三列是指令opcode的名称,分为有操作数和无操作数 ...
- python解析器原理_Python程序运行原理图文解析
本文研究的主要是Python程序运行原理,具体介绍如下. 编译型语言(C语言为例) 动态型语言 一个程序是如何运行起来的?比如下面的代码 #othermodule.py def add(a, b): ...
- python闭包实现原理_Python 闭包详解
在函数编程中经常用到闭包.闭包是什么,它是怎么产生的及用来解决什么问题呢.给出字面的定义先:闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)(想想Erlang的外层函数传入一个 ...
- js更新数组对象_7 种Vue 数据已更新而页面没有更新的情况及深化总结(收藏)
作者:前端1943 链接:https://segmentfault.com/a/1190000022772025 如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做 ...
- python系统工作原理_Python之路——堡垒机原理及其简单实现
1 堡垒机基本概述 其从功能上讲,它综合了核心系统运维和安全审计管控两大主干功能,从技术实现上讲,通过切断终端计算机对网络和服务器资源的直接访问,而采用协议代理的方式,接管了终端计算机对网络和服务器的 ...
- python修饰器原理_Python修饰器的函数式编程
Python的修饰器的英文名叫Decorator,当你看到这个英文名的时候,你可能会把其跟Design Pattern里的Decorator搞混了,其实这是完全不同的两个东西.虽然好像,他们要干的事都 ...
- python 编码解码原理_Python JSON编解码方式原理详解
这篇文章主要介绍了Python JSON编解码方式原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 概念 JSON(JavaScript Ob ...
- 用python画画的原理_python来画画第二弹!
下载W3Cschool手机App,0基础随时随地学编程导语 分享一波最近Get的一种python画画的方法. 让我们愉快地开始吧! 相关文件 密码: 9nei 开发工具 Python版本:3.6.4 ...
最新文章
- python进阶书籍的推荐 知乎-知乎看了很多推荐,最终选了这本Python入门
- JVM 调优实战--使用jstack分析线程执行情况
- 过滤设置_深圳外置鱼池过滤器零售_杰蒙尼鱼池过滤器
- 操作集锦【牛客网】 牛客练习赛60
- DFS应用——寻找欧拉回路
- 结构专业规范大全_1.2万篇 建筑行业规范大全套!速来!
- 用示例说明BitMap索引的效率要优于B-Tree索引
- 腾讯手机管家筹划“出海”
- CentOS(八)--crontab命令的使用方法
- Hive设置参数-指定引擎-队列
- 产品经理眼中的供应链、物流与采购管理
- php 姓氏表,PHP拆分姓名中的姓氏和名字函数
- oc基础-OC基础-类的使用
- (转)“版本上线延时”问题与对策的探讨
- 数据库 ER图 EER图(鸭蹼图) freedgo绘图工具
- 后缀–ize_动词后加ize的后缀有什么作用
- Python求方阵的逆矩阵与求非方阵的伪逆矩阵
- C# 实验三 面向对象程序设计(一)
- python---之scipy.ndimage.measurements.label
- 猪八戒让网赚国人再也不用看外国人…