用动态加载和代码动态生成提高应用灵活性

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——————

Last modification:August 14th, 2017 at 10:21 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment