django开发总结

2013-01-08 21:53 木头lbj 5415

django是流行的web开发框架,使用优雅的python语言。以下内容是使用django开发gitshell的经验总结,需要对django,python有一定的基础,对于入门,请看这里 The Django Book 中文版

  1. URL 设计
    django 认为 URL 是有语义的,URL 也要优雅,遵循人类的自然语言,可以实现一些类似 RESTful 接口。在 django 的官方文档上面:A clean, elegant URL scheme is an important detail in a high-quality Web application。事实上,优美的 url 设计对 seo 也是非常友好的。比如下面的登录注册,找回密码相关操作:

    from django.conf.urls.defaults import patterns, include, url
     
    urlpatterns = patterns('gitshell',
        url(r'^login/?$', 'gsuser.views.login'),
        url(r'^logout/?$', 'gsuser.views.logout'),
        url(r'^join/?(\w+)?/?$', 'gsuser.views.join'),
        url(r'^resetpassword/?(\w+)?/?$', 'gsuser.views.resetpassword'),
    )
    handler404 = 'gitshell.help.views.error'
    handler500 = 'gitshell.help.views.error'


  2. 编码统一
    我的建议是在现代操作系统上,全部使用UTF-8编码,从操作系统到数据库到django,还有其他所有组件,这能减少很多编码的问题。
    确定 > locale 输出编码是 *.UTF-8
    django settings.py 里面:

    TIME_ZONE = 'Asia/Shanghai' LANGUAGE_CODE = 'zh_CN' DEFAULT_CHARSET = 'UTF-8'

    这里讲和django相关的东西,说到mysql主要是考虑编码的问题,当然也建议mysql使用新版本,innodb引擎:

    [client]
    default-character-set = utf8 
    [mysqld]
    init_connect = 'SET collation_connection = utf8_general_ci'
    init_connect = 'SET NAMES utf8'
    character-set-server = utf8
    collation-server = utf8_general_ci 
    [mysql]
    default-character-set = utf8

    另外在python代码里面,添加coding,使用中文内容的时候添加unicode标识

    # -*- coding: utf-8 -*- 
    var = u'中文内容'


  3. cache 机制
    gitshell 对内存用的非常重度,最大化的减少db的压力,关于使用的内存策略,这里简单说一下,以后可以单独成为一篇文章。
    大多数的系统都是读多于写,能否利用好内存是一个系统能不能面对多并发,多流量的关键部分。
    此外,一些系统具有“分片”的特征,最明显的就是crm系统,每一个更新操作都是在具体公司下面,下面提供一个思路:
    1)对于可以“分片”的数据库表结构,所有的请求都附带“分片ID”,比如具体公司ID
    2)使用版本号的概念,比如具体公司ID 1 的版本号就是 “company_id_1″ -> 1000
    3)所有的sql语句抽象为sql_id,包含参数,那么数据库请求就是先查看key为 “company_id_1_” + version + ‘_’ + sql_id 的缓存是否存在,比如 ‘company_id_1_100_sql_id’,如果cache存在,直接返回数据,否则取数据库然后放到cache。
    4)对于更新操作,cache key version自增,比如上面的 1000 自增为 1001,之前的所有缓存自动不再使用,等待废弃。
    5)监听 save() 接口,从中激发更新操作。
    6)对于直接 get_by_id,可以做一些针对化的cache,因为使用主键id来访问的情况非常频繁。
    监听 save() 接口,使用 django event 机制:

    def da_post_save(mobject):
        table = mobject._meta.db_table
        if not hasattr(mobject, 'id'):
            return False
        id_key = __get_idkey(table, mobject.id)
        cache.delete(id_key)
        if table in table_ptkey_field:
            ptkey_field = table_ptkey_field[table]
            ptkey_value = getattr(mobject, ptkey_field)
            version = __get_current_version()
            cache.set(__get_verkey(table, ptkey_value), version)
        return True
    def __cache_version_update(sender, **kwargs):
        da_post_save(kwargs['instance'])
    post_save.connect(__cache_version_update)

    注意,这种缓存方式不适合于非常频繁更新的操作,会导致memcache的item频繁不再使用。


  4. redis 配置
    gitshell 对 redis 的使用非常谨慎的,redis 虽然好,但是内存大户,所以需要使用redis的情况下才使用,比如排名,feed,前N最大最小列表,以下脚本测试redis占用量:

    #!/usr/bin/python
    import redis
    import random
     
    def main():
        feed_redis = redis.Redis('localhost', 6379, 3)
        for i in range(0, 1000):
            for ftype in ['r', 'u', 'wu', 'bwu', 'wr', 'c']:
                key = '%s:%s' % (ftype, i + 10000) 
                for j in range(0, 100):
                    value = random.randint(0, 1000000)
                    feed_redis.zadd(key, value, value+1)
     
    if __name__ == '__main__':
        main()
    100000 的 sorted key 大概需要 150M 内存,假如你有 10 万个用户呢?

    把所有的 redis 相关操作封装成为方法,在一个 python class 里面,减少 redis 的滥用。
    redis 设计上全在内存使用,才能发挥最大优势,但是内存是易逝性存储,需要使用 M-S 做分发和复制。
    建议起两个实例,主从复制,主redis使用内存结构,从redis使用Append-only,appendfsync everysec。减少最大可能的丢失。


  5. decorator 做权限控制
    这个地方和 1 URL 设计 息息相关,decorator 可以拦截所有的请求,针对请求做指定事情。
    gitshell 所有仓库都是使用 /username/reponame/ 的方式,相对 URL 都是

    url(r'^(\w+)/(\w+)/issues/', 'repo.views.issues_show'),
    对应方法:
    @repo_permission_check
    def issues_show(request, user_name, repo_name):
        pass
    对于仓库是否可见的权限,repo_permission_check 就是 decorator 控制:

    使用这样的机制能使权限控制统一和优雅,减少离散粒度控制出现的失误

    from django.http import Http404
    from django.http import HttpResponseRedirect
    from gitshell.repo.models import RepoManager
     
    def repo_permission_check(function):
     
        def wrap(request, *args, **kwargs):
            if len(args) >= 2:
                user_name = args[0]
                repo_name = args[1]
                repo = RepoManager.get_repo_by_name(user_name, repo_name)
                if repo is None:
                    return HttpResponseRedirect('/help/error/')
                # half private, code is keep
                if repo.auth_type == 2:
                    if not RepoManager.is_repo_member(repo, request.user):
                        return HttpResponseRedirect('/help/error/')
            return function(request, *args, **kwargs)
        wrap.__doc__=function.__doc__
        wrap.__name__=function.__name__
     
        return wrap


  6. 异步事件
    异步事件使用 beanstalkd,beanstalkd 是一个非常小巧,依赖少的事件后台服务。默认是在内存中,如果需要持久状态,使用 -b 参数,这样就能持久的写在文件里,防止忽然的机器故障丢失数据。
    在 ubuntu 里,使用
    > sudo apt-get install beanstalkd
    python client 使用 beanstalkc
    多个 tube 的使用,如果有多个队列,为了能每个后台程序管理对应的队列,使用tube:

    from gitshell.settings import BEANSTALK_HOST, BEANSTALK_PORT
    class EventManager():
     
        @classmethod
        def sendevent(self, tube, event):
            beanstalk = beanstalkc.Connection(host=BEANSTALK_HOST, port=BEANSTALK_PORT)
            self.switch(beanstalk, tube)
            beanstalk.put(event)
     
        @classmethod
        def switch(self, beanstalk, tube):
            beanstalk.use(tube)
            beanstalk.watch(tube)
            beanstalk.ignore('default')
     
        @classmethod
        def send_stop_event(self, tube):
            stop_event = {'type': -1}
            self.sendevent(tube, json.dumps(stop_event))
     
        # ======== send event ========
        @classmethod
        def send_fork_event(self, from_repo_id, to_repo_id):
            fork_event = {'type': 0, 'from_repo_id': from_repo_id, 'to_repo_id': to_repo_id}
            self.sendevent(FORK_TUBE_NAME, json.dumps(fork_event))
    beanstalk 的事件建议使用 json 格式做序列化,简单,并且跨平台。


  7. logging 以及监控
    logging 是系统健壮的有效保证,呃,系统挂了,什么日志都没有??
    django 通过 logging 来记录所有的日志,settings.py 配置如下:

    LOGGING = {
        'version': 1,
        'disable_existing_loggers': False,
        'handlers': {
            'mail_admins': {
                'level': 'ERROR',
                'class': 'django.utils.log.AdminEmailHandler',
            },
            'file': {
                'level': 'INFO',
                'class': 'logging.FileHandler',
                'filename': '/opt/run/var/log/gitshell.8001.log',
            },
        },
        'loggers': {
            'gitshell': {
                'handlers': ['file'],
                'level': 'INFO',
                'propagate': True,
            },
            'django.request': {
                'handlers': ['mail_admins'],
                'level': 'ERROR',
                'propagate': True,
            },
        }
    }


  8. 除此之外,写一个全局的middleware来捕获所有的exception,一个登录用户一定时间内(比如30分钟)最多访问请求(比如1000)限制控制:

    class ExceptionLoggingMiddleware(object):
        def process_exception(self, request, exception):
            logger = logging.getLogger('gitshell')
            logger.error(traceback.format_exc())
            return None
    class UserAccessLimitMiddleware(object):
        def process_request(self, request):
            path = request.path
            if path.startswith('/help/') or path.startswith('/captcha/'):
                return
            if request.user.is_authenticated():
                user_id = request.user.id
                key = '%s:%s' % (ACL_KEY, user_id)
                value = cache.get(key)
                if value is None:
                    cache.add(key, 1, ACCESS_WITH_IN_TIME)
                    return
                if value > MAX_ACCESS_TIME:
                    return HttpResponseRedirect(OUT_OF_AccessLimit_URL)
                cache.incr(key)
    settings.py:
    MIDDLEWARE_CLASSES = (
        'gitshell.gsuser.middleware.UserAccessLimitMiddleware',
        'gitshell.gsuser.middleware.ExceptionLoggingMiddleware',
    )

    MIDDLEWARE 可以自由发挥,一个常见的例子就是每分钟超出一定数量的异常发生,那么就可以发送异常监控报警了,这对一个生产环境的系统很重要。

  9. 安全相关
    django 对安全非常重视,假如你使用 POST 请求,你会发现 django 要求 csrfmiddlewaretoken 参数,在 html 代码如下:

    {csrfmiddlewaretoken: '{{ csrf_token }}'}

    为了减少 csrf 攻击,看起来简单粗暴,是吧?
    正是因为这样,我才推荐所有的ajax通过 POST 请求,你甚至可以通过 @require_http_methods(["POST"]) 来强制要求 POST 请求,这样 ajax 必须附带 csrfmiddlewaretoken 参数。
    另一个安全问题是 xss,随着 ajax 使用越来越多,这个问题越来越容易被忽视,gitshell 使用统一的 json 序列化方法来防止 xss 攻击:

    import json
    import functools
    from django.utils.html import escape
    from django.http import HttpResponse, HttpResponseRedirect, Http404
     
    def json_httpResponse(o):
        return HttpResponse(json_escape_dumps(o), mimetype='application/json')
     
    def json_escape_dumps(o):
        json.encoder.encode_basestring = encoder
        json.encoder.encode_basestring_ascii = encoder
        return json.dumps(o)
     
    def encoder(o, _encoder=json.encoder.encode_basestring):
        if isinstance(o, basestring):
            o = escape(o)
        return _encoder(o)

    iptables 也是必须的,简单的 iptables 策略就是只开放对外端口:

    *filter
    :INPUT ACCEPT [0:0]
    :FORWARD ACCEPT [0:0]
    :OUTPUT ACCEPT [0:0]
    -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
    -A INPUT -p icmp -j ACCEPT
    -A INPUT -i lo -j ACCEPT
    -A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT
    -A INPUT -j REJECT --reject-with icmp-host-prohibited
    -A FORWARD -j REJECT --reject-with icmp-host-prohibited
    COMMIT


  10. 生产环境配置

  11. 推荐生产环境使用 nginx + uwsgi,nginx 配置

    http {
        upstream uwsgicluster {
            server 127.0.0.1:8001;
            server 127.0.0.1:8002;
        }
        server {
            location / {
                include        uwsgi_params;
                uwsgi_pass     uwsgicluster;
            }
        }
    }

    uwsgi 配置文件:

    [uwsgi]
    socket = :8001
    protocol = uwsgi
    processes = 3
    harakiri = 30
    daemonize = /opt/run/var/log/uwsgi.8001.daemonize.log
    listen = 4096
    master = true
    max-requests = 2500
    pidfile = /opt/run/var/uwsgi.8001.pid
    uid = git
    gid = git
    limit-as = 512
    limit-post = 3145728
    no-orphans = true
    post-buffering = 4096
    logto = /opt/run/var/log/uwsgi.8001.log
    log-slow = 800
    log-5xx = true
    log-big = 102400
    disable-logging = true
    chdir = /opt/app/8001
    pyhom = /opt/app/8001
    pythonpath = /opt/app/8001
    env = DJANGO_SETTINGS_MODULE=gitshell.settings
    module = gitshell.wsgi:application

    使用 /opt/bin/uwsgi –ini 的方式来启动。


相关阅读