django开发总结——Gitshell

2014-01-08 16:35:49 +08:00
 gitshell
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
6924 次点击
所在节点    Python
4 条回复
zjwzszh
2014-01-08 16:42:06 +08:00
从推广的角度看,这样做真是太糟糕了!—— 直接原文复制粘帖,没有一点没感,实在看不下去。 万幸下面有文章原文链接。。原文看着倒是赏心悦目

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

所以,摘要+链接,或者只有链接都可以
gitshell
2014-01-08 18:01:35 +08:00
@zjwzszh
@dorentus 收到建议,感谢
RIcter
2014-01-08 21:23:58 +08:00
收藏之

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/96173

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX