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 的方式来启动。