用动态加载和代码动态生成提高应用灵活性
Why?
Web产品一旦上线,重启应用就会造成业务中断,对于实时性要求很高或者业务关联紧密的应用,重启程序是非常重的代价。
将代码对象序列化之后保存到存储内(比如redis, 关系数据库),在运行业务的时候通过制定的路由机制加载这部分业务。
对于线上应用,使用这种方式可以更加方便的部署新业务。
对于性能要求不是很高的场合,可以牺牲一些性能做代码动态装载。
更好的设计还是做到重启应用不影响业务,将业务状态同应用程序剥离。但这不是本篇文章讨论的范围。
下面说说具体一点的。
主要可以在以下场景做代码动态生成和加载。
1.使用配置文件管理表单和页面的生成以及表单校验,这样你的代码只需要写一份,在需要的时候根据配置文件动态生成和校验你的表单,在配置文件内存储表单的字段类型和验证需即可。(实现参考wtforms即可,只不过把配置文件变为可序列化的其他格式,比如json)
Q:代码注入问题
A:必须校验配置文件,并对配置文件的可执行部分进行转义和过滤,根据配置文件生成代码的时候必须注意%s这种会发生代码注入的部分。
2.将Python脚本序列化保存,在需要的时候编译,然后载入到内存. 代码存储在数据库或者文件系统,为了提高效率可以将编译好的code object放置到缓存,每次数据库更新的时候主动刷新缓存.
Q:代码作用域或者脚本出错的情况怎么处理?
A:使用局部作用域(比如将代码段包裹在一个函数内,这样就不会影响全局作用域,也很难修改外部namespace的数据),并用try…expect…包裹载入的脚本部分.对于出错的脚本直接提示错误就好,不要回报具体的错误而是将错误log下来以免泄露runtime 信息。
2.在数据API当中使用动态装载
提前在数据API外部做好鉴权等操作,类似于RPC式的调用,然后通过API返回数据。适合于需要和后端程序通信作为Proxy使用的场景。
以下是关于动态装载代码部分的范例
代码范例:
以下是简单的范例,实现的CodeLoader可以动态的装载和执行代码的指定部分(执行指定部分是一般插件的编写方式,仅允许运行指定方法并将接口通用化.)代码还算清晰吧,不需要太多说明,所以罗列如下.
from abc import ABCMeta, abstractmethod
import logging
import imp
import sys
import traceback
try:
import cPickle as pickle
except ImportError:
import pickle
logger = logging.getLogger("CodeLoader")
__all__ = (
'StorageBackendMixin',
'CacheBackendMixin',
'CodeLoader',
)
class StorageBackendMixin(object):
__metaclass__ = ABCMeta
@abstractmethod
def get(self, key, *args, **kwargs):
pass
@abstractmethod
def set(self, key, value, *args, **kwargs):
pass
class DummyStorageBackend(StorageBackendMixin):
"""
Just a example, do not apply it to productive application.
"""
_storage = {}
@classmethod
def get(cls, key):
return cls._storage.get(key)
@classmethod
def set(cls, key, value, *args, **kwargs):
cls._storage[key] = value
class CacheBackendMixin(object):
__metaclass__ = ABCMeta
@abstractmethod
def get(self, key):
pass
@abstractmethod
def set(self, key, value):
pass
@abstractmethod
def delete(self, key):
pass
class DummyCacheBackend(CacheBackendMixin):
"""
Just a example, do not apply it to productive application.
"""
_cached = {}
@classmethod
def get(cls, key):
return cls._cached.get(key)
@classmethod
def set(cls, key, value):
cls._cached[key] = value
@classmethod
def delete(cls, key):
if key in cls._cached:
del cls._cached[key]
class CodeLoader(object):
"""
Get a module object from script file or string.
"""
_module_prefix = "_dynamic_loaded_"
def __init__(self, name, storage_backend=DummyStorageBackend, cache_backend=DummyCacheBackend):
if not issubclass(storage_backend, StorageBackendMixin):
raise TypeError("storage_backend must be subclass of StorageBackendMixin")
if not issubclass(cache_backend, CacheBackendMixin):
raise TypeError("cache_backend must be subclass of CacheBackendMixin")
self.name = name
self._cache_prefix = 'Loader_{cache_backend_name}_'.format(cache_backend_name=cache_backend.__name__)
self._storage_backend = storage_backend
self._cache_backend = cache_backend
def create_module(self, fullname, code_script, save_key=None):
"""
:param fullname: module name, will be joined with prefix `dynamic_loaded.`
:param code_script: the utf-8 encoded bytes object(in python2, it is `str`)
:param save_key: save key that will be used by storage_backend, You can also run 'CodeLoader.load(save_key)' to
access the saved code object.
:type fullname: str or unicode
:type code_script: str
:return: module object if success, return None if fail with anything wrong with the code's runtime error.
"""
if not isinstance(code_script, str):
raise TypeError("code_script param must instance of str")
code = self._compile(fullname, code_script)
if code is None:
return None
mod = imp.new_module(fullname)
mod.__file__ = self._module_prefix + fullname
mod.__loader__ = self
mod.__package__ = ''
mod.__script__ = code_script
if save_key is None:
mod.__save_key__ = self._module_prefix + fullname
else:
mod.__save_key__ = save_key
try:
exec(code, mod.__dict__)
return mod
except:
exc_type, exc_value, traceback = sys.exc_info()
logger.warning(
"Bad codes, info listed below:\n"
"exec_type: %s\n exec_value: %s\n traceback: %s" % (exc_type, exc_value, traceback)
)
return None
def save(self, mod, cached=False, *args, **kwargs):
"""
Save mod's script to storage backend, is cached=True, cache_backend will be refreshed.
"""
self._storage_backend.set(mod.__save_key__, mod.__script__, *args, **kwargs)
if cached:
self._cache_backend.set(mod.__save_key__, pickle.dumps(mod))
else:
self._cache_backend.delete(mod.__save_key__)
def load(self, fullname, save_key=None, *args, **kwargs):
if save_key:
access_key = save_key
else:
access_key = self._module_prefix + fullname
result = self._cache_backend.get(access_key)
print result
if result:
return pickle.loads(result)
else:
result = self._storage_backend.get(access_key, *args, **kwargs)
return self.create_module(fullname, result, access_key)
def _compile(self, fullname, code_script):
"""
:type fullname: str or unicode
:type code_script: str
"""
try:
return compile(code_script, self._module_prefix + fullname, 'exec')
except SyntaxError:
exc_type, exc_value, tb = sys.exc_info()
traceback.print_exception(exc_type, exc_value, tb)
logger.warning("code script `%s` has SyntaxError:\n"
"The code:\n%s" % (fullname, code_script))
return None
—————补充—————–
考察了很多沙盒方案之后,发现现有的Python沙盒基本都无法满足安全性需求,所以在线修改项目代码这种操作还是不能暴露给普通用户,只能在上线添加功能的时候临时开启,从数据存储层级控制权限。
PyPy类沙盒太重了,无法在应用和动态代码需要同时运作的时候使用。
别的类别的sandbox成本太高,一套GAE模式的沙盒需要太多的定制,不是个人能完成的。
—————end——————