V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
gitshell
V2EX  ›  Python

django开发总结——Gitshell

  •  
  •   gitshell · 2014-01-08 16:35:49 +08:00 · 6952 次点击
    这是一个创建于 4002 天前的主题,其中的信息可能已经有所发展或是发生改变。
    django是流行的web开发框架,使用优雅的python语言。以下内容是使用django开发gitshell的经验总结,需要对django,python有一定的基础,对于入门,请看这里 The Django Book 中文版 。

    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'

    编码统一
    我的建议是在现代操作系统上,全部使用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'中文内容'

    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频繁不再使用。
    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。减少最大可能的丢失。
    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

    异步事件
    异步事件使用 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 格式做序列化,简单,并且跨平台。
    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,
    },
    }
    }

    除此之外,写一个全局的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 可以自由发挥,一个常见的例子就是每分钟超出一定数量的异常发生,那么就可以发送异常监控报警了,这对一个生产环境的系统很重要。
    安全相关
    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

    生产环境配置

    推荐生产环境使用 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 的方式来启动。

    有其他问题,联系 admin AT gitshell.com

    http://gitshell.1kapp.com/?p=169
    第 1 条附言  ·  2014-01-08 18:07:58 +08:00
    这是开发Gitshell过程中的一个总结,本来想方便阅读贴了原文,不过这里的格式支持并不好,欢迎大家直接在博客阅读,谢谢 http://gitshell.1kapp.com/?p=169
    4 条回复    1970-01-01 08:00:00 +08:00
    zjwzszh
        1
    zjwzszh  
       2014-01-08 16:42:06 +08:00   ❤️ 1
    从推广的角度看,这样做真是太糟糕了!—— 直接原文复制粘帖,没有一点没感,实在看不下去。 万幸下面有文章原文链接。。原文看着倒是赏心悦目

    阁下好歹给一个全文提炼,或者一个简述。这样才能吸引人吧-。-
    dorentus
        2
    dorentus  
       2014-01-08 17:22:31 +08:00   ❤️ 1
    这里其实是鼓励直接贴链接的

    所以,摘要+链接,或者只有链接都可以
    gitshell
        3
    gitshell  
    OP
       2014-01-08 18:01:35 +08:00   ❤️ 1
    @zjwzszh
    @dorentus 收到建议,感谢
    RIcter
        4
    RIcter  
       2014-01-08 21:23:58 +08:00
    收藏之
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1267 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 17:51 · PVG 01:51 · LAX 09:51 · JFK 12:51
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.