A Full Stack RESTful API Web Service for Q&A
[TOC]
Nginx, uWSGI, Daphne, and Supervisor
in progress
-
Install
Elasticsearch-2.4.6
# download $ wget https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.4.6/elasticsearch-2.4.6.tar.gz # unzip $ tar -xf elasticsearch-2.4.6.tar.gz # run $ ./elasticsearch-2.4.6/bin/elasticsearch # to test elasticsearch is running properly $ curl http://localhost:9200
-
Install
Redis
# install $ wget http://download.redis.io/redis-stable.tar.gz $ tar xvzf redis-stable.tar.gz $ cd redis-stable $ make # run $ redis-server
-
Install dependencies
$ pip install -r requirements.txt
-
Run
$ python3 manage.py runserver
- Nginx: 高性能的代理服务, 接受到客户端发送过来的 HTTP 请求和 WebSocket 请求, 响应静态文件请求和转发动态请求
- WSGI: 用在 Python Web 框架编写的应用程序与 Web 服务器之间的规范 (本例就是 Django 与 uWSGI 之间)
- uWSGI: 是一个 Web 服务器, 它实现了 WSGI/uwsgi/HTTP 等协议, 勇于接受Nginx 转发的动态请求并处理后发给 Python 应用程序
- uwsgi: 是 uWSGI 服务器实现的独有协议, 用于 Nginx 服务与 uWSGI 服务的通信规范
- 最上方有"发表动态"按钮,每页显示 20 条动态
- 动态下有点赞和评论按钮
- 每条动态除内容外要显示用户头像, 昵称, 发表时间, 赞和评论数量, 用户互动后能自动更新数量
- 对于登录用户发表的动态,右上角显示删除按钮
- Generic Detail
- DetailView
- Generic Edit
- CreateView
- DeleteView
- FormView
- UpdateView
- Generic List
- ListView
Django ListView 继承关系图
MRO (Method Resolution Order), 定义了 Python 中多继承存在的情况下, 解释器查找函数解析的具体顺序 (C3 线性算法)
django-contrib-comment
实现评论功能markdownx
实现 markdown 实时预览python-slugify
创建文章 url 链接后缀django-taggit
创建文章中可以用逗号分隔的标签
Django ContentTypes 是由 Django 框架义工的一个核心功能, 对当前项目中所有基于 Django 驱动的 model 提供更高层次的抽象接口
ContentType 通用类型 - GenericRelation (通用外键关联)
这样就只需要在 Vote
这个 class 里面设置好 content_type
以及 object_id
, 同时设置 vote = GenericForeignKey()
就可以免去在不同的表中 (问题, 动态, 评论) 重复创建 vote 表.
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
# 在 Django 项目中找到对应的表
content_type = models.ForeignKey(ContentType, models.CASCADE)
object_id = models.IntegerField()
content_object = GenericForeignKey()
- 发送: Ajax POST 请求
- 接收: Websocket 请求
WebSocket 允许服务端主动地向客户端发送数据, 在 WebSocket 协议中, 客户端浏览器和服务器只需一次握手就可以创建持久性的连接, 并在浏览器与服务器之间进行双向的数据传输 (全双工通信)
优点:
- 支持双向通信, 实时性好
- 数据格式轻量, 性能开销小, 通信高效
- 支持扩展
缺点:
- 浏览器是否支持不确定
- 长连接对后端处理业务代码稳定性要求更高, 后端推送相对复杂
- 组件比较少
WebSocket 请求头中重要的字段
- Connection 和 Upgrade: 表示客户端发起的是 Websocket 请求, 先用 HTTP 协议发送 GET 请求, 然后 Upgrade 成 WebSocket 协议
- Sec-Websocket-Version: 客户端使用的 WebSocket 协议版本号, 服务端会确认是否支持该版本
- Sec-Websocket-Key: 一个 Base64 的随机编码值, 浏览器随机生成, 用于升级 request
WebSocket 响应头中重要的字段
- HTTP/1.1 101 Switching Protocols: 切换协议, WebSocket 协议通过 HTTP 协议来建立运输的 TCP 链接, statuscode 101
- Connection 和 Upgrade: 表示服务端返回 WebSocket 响应
- Sec-WebSocket-Accept: 表示服务端接受了客户端的请求, 由 Sec-Websocket-Key 计算得来
需要解决的问题
- 如何分别路由的 HTTP 请求和 WebSocket 请求
- 如何兼容 Django 认证系统
- 如何接受, 推送 WebSocket 消息
- 如何通过 ORM 保存和获取数据
所以我们使用 Django-Channels
为 Django 提供异步扩展的库, 通常主要用来提供 WebSocket 支持和后台任务
一共三层
- 第一层 - 接口服务器 (Interface Server): 负责对协议进行解析, 将不同协议进行解析, 将不同协议分发到不同的 Channel
- 频道层 (Channel Layer): 可以是 FIFO 队列, 通常使用 Redis
- 消费者 (Consumer): 接受处理消息
- WSGI (Python Web Server Gateway Interface): 为 Python 语言定义的 Web 服务器和 Web 应用程序或框架之间的一种简单而通用的接口
- Nginx + uWSGI + Django/Flask (HTTP / HTTP 2)
- ASGI (Asynchronous Server Gateway Interface): 异步服务网关接口, 一个介于网络协议服务和 Python 应用之间的标准接口, 能够处理多种通用的协议类型. 如 HTTP HTTP2 和 WebSocket
- Nginx + Daphne + Django/Flask (HTTP / HTTP 2)
- WSGI 和 ASGI 区别: WSGI 是基于 HTTP 协议模式, 不支持 WebSocket, 而 ASGI 就是为了支持 Python 常用的 WSGI 所不支持的协议标准. ASGI 是 WSGI 的扩展, 同时可以通过 asyincio 异步运行
当其它用户与我有如下互动时能接收到通知:
- 赞了我的状态
- 评论了我的动态或文章
- 收藏了我的文章
- 回答了我的提问
- 接受了我的回答
- 回复了我的评论
所以我们可以创建一个通知处理器, 来更方便的管理处理通知
刘看山 点赞了 我的 文章
actor verb recipient action_object
使用 django-comments
信号机制实现文章评论, 同时可以实现解耦
一共有 3 种信号机制可以使用
comment_will_be_posted
comment_was_posted
: send just after the commetn is savedcomment_was_flagged
同步的 pub + sub (观察者模式)
- Elasticsearch: 分布式可扩展的实时搜索和分析引擎
- 实时分析的分布式搜索引擎
- 分布式实时文件存储, 并将没一个字段编入索引, 使其可以被搜索
- 可以扩展到上百台服务器, 处理 PB 级别的结构化或非结构化数据
- ES 服务 -> 索引库(可以是多个) -> 类型(多种) -> 文档(行) -> 字段
- "面向文档的数据库"
- django-haystack: 为 Django 项目提供了模块化的搜索
为各个模型类创建索引库: 先开启 Elasticsearch 服务 (切换至非 root 用户)
python manage.py rebuild_index
同时可以使用 django-haystack
的实时信号处理器, 会实时更新索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# django-compressor
# ------------------------------------------------------------------------------
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_ENABLED
COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True)
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL
COMPRESS_URL = STATIC_URL
python manage.py compress --force
使用场景: 注册用户后要等邮件发送完成后才能跳转页面
分布式任务队列
消息中间件 (Broker): 任务调度队列, 接受生产者发送过来的任务
Django 里使用 Celery 异步发送邮件
缓存结构
Session 缓存
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
使用 cached_db
仅仅使用 cache
, 关机就没了
缓存文章创建页面
@method_decorator(cache_page(60 * 60), name='get')
class ArticleCreateView(LoginRequiredMixin, CreateView):
pass
缓存导航栏
{% cache 3600 navbar request.user.username %}
<nav>...</nav>
{% endcache %}
- 设置数据库持久连接
- 合理的创建索引
- 减少 SQL 语句的执行次数
- 仅获取所需要的字段数据
- 使用批量创建, 更新和删除; 不随意对结果排序
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60)
当第一次 request 传过来, 执行 sql 语句, 会去连接数据库, 但是在之后 60s 以内, 不会断开数据库连接. 但是也不能太久
索引会占据磁盘空间
主键, 外键, 唯一键会自动创建索引
如何添加索引?
db_index=True
添加索引的一些经验
- 频繁出现在
where
条件子句的字段 - 经常被用来分组
group by
或者排序order by
的字段 - 用于连接的列(主键/外键)上建立索引
index_together
- 在经常存取的多个列上简历复合索引, 但要注意复合索引的建立顺序要按照使用的频度来确定
方法
select_related()
适用于 ForeighKey 或者 OneToOneFieldprefetch_related()
适用于 ManyToManyField 或者 GenericForeignkey
defer()
不需要获取的字段on()
需要获取的字段
bulk_create()