A blog system based on Python 2.7 and Redis.
Progress
It's under heavy development currently.
Don't use it in production environment right now since it might be changed frequently.
Installation
Doodle requires Python 2.7 and Redis. It has been tested in OS X 10.8 ~ 10.11 and Ubuntu 15.10.
- Clone or download code:
$ git clone https://github.com/keakon/Doodle.git
- Put your own config files under the private directory. (optional)
$ cd Doodle
$ mkdir private
- Install dependencies:
$ sudo pip install virtualenv
$ virtualenv .
$ bin/pip install cython
$ bin/pip install -r requirements.txt
- Install pycurl on macOS:
$ brew install openssl
$ export LDFLAGS="-L/usr/local/opt/openssl/lib"
$ export CPPFLAGS="-I/usr/local/opt/openssl/include"
$ export PYCURL_SSL_LIBRARY=openssl
$ bin/pip install -r requirements.txt
Usage
$ redis-server &
$ bin/python -m doodle.main
Then you can open http://0.0.0.0:8080 to check it.
from https://github.com/keakon/Doodle
-----
准备将Doodle移植到Linux vps
Doodle 2 将只能在 Linux 平台(VPS 或独立服务器)上运行了,老版本将只在即日起的一年内提供迁移(到 Doodle 2 或 WordPress)、修正 bug 和指导性的帮助,不再添加新功能了。
预计第一步将支持的是 Ubuntu 12.04+,因为开发是在 OS X 10.8 上,所以肯定也是支持的,只是依赖的工具 / 包 / 库我也记不清(找不到干净的系统)。Windows 上的开发者就抱歉了,支持这个实在头大。
虽说计划是最低支持 128 MB 内存的 VPS,不过看到 DigitalOcean 512 MB + 20G SSD 的 VPS 才卖 $5 每月,也支持按小时扣费,网速和口碑好像还行,所以想先玩玩。有闲钱赞助的,可以通过这个链接购买,累计消费 $10 (例如购买 2 个月最低配置的 VPS)后,我会收到 $10 的佣金,先谢了。发现几个优惠码,不知何时失效,有需要的自便:SSDBEAR10(免费 $10,需要绑定信用卡或 PayPal)、HELLOSF(免费 $5,无需绑定)。不会用的可以在填写支付方式时,找到 Promo Code,把优惠码贴进去,成功就会提示。
接下来是我临时想到的一些替代的解决方案,可能会有变化:
- 开发语言:Python 2.7.x。
2.6 及以下因缺少某些语法和标准库,写起来不爽;3.x 各种不兼容,你懂的。
至于 JavaScript 和 Bash 之类的就不提了。 - Web 框架:Tornado 3.x。
Tornado 我用得比较熟,异步虽然大部分情况下没用,但 AsyncHTTPClient 很有用(前天刚写了个工具,需要并发访问几百个 URL 来做统计,几秒钟就全部返回了)。比较头疼的是 StaticFileHandler,规则很奇葩,有空得自己重写下,或者交给 Nginx。
以前用的 YUI 只适合 GAE。Django 不会用,太复杂。Flask 的 routing 不喜欢,开发 YUI 时就说过了。Bottle 和 web.py 的性能确实赶不上 Tornado(自测,场景并不适用于所有情况,例如阻塞严重的情况)。 - 数据库:Redis 2.6+。
你没看错。最近拿 Redis 做了个目标千万用户的应用,感觉一台服务器就绰绰有余了。
开发起来很有挑战性和新鲜感,比用 SQLAlchemy + MySQL 更舒服,虽说大部分时间花在了实现 ORM 上。
至于内存占用,如果不算复杂的功能,预计本站现有的数据只需要不到 50 MB 的内存(也许多算了一倍),比 MySQL 也没差,还不需要缓存。
缺点有: ORM 和事务的结合比较难做,复杂的关系和查询不好实现,需要写出详细的实现文档。不过实现一个博客系统,好像也没什么困难,后面会提到如何实现。 - 安装:Buildout。
很容易设置依赖,还自带 virtualenv 功能。
不过有些 C 库 / 工具还是得手动安装和配置,小白用户也得自己动手了。 - 部署:Fabric + Git + Supervisor + Nginx。
Gunicorn 虽然用起来更简单,但如果要对 Tornado 的 HTTPServer 进行一些自定义设置,貌似比较难弄。 - 计划任务:crontab。
好像也就干些备份数据库的事,好在 Redis 的备份很简单,复制下数据文件即可。 - 任务队列:RQ、Beanstalkd,或者自己拿 Redis 发明轮子吧。
不过大部分情况都能用 Tornado 的 IOLoop.add_timeout(),所以有需要再去调研吧。 - 发送邮件:smtplib 模拟登录,或者 Amazon SES。
感觉自己搭建邮件服务器太难维护,且容易被当成垃圾邮件。 - 错误日志:Sentry。
之前拿 Sentry + SQLite 来查错误日志,感觉很卡。翻了下文档发现支持 Redis,准备试试。小内存的可以不启用,直接使用文件记录吧。 - 用户认证:Google OAuth 2.0。
新浪微博和 Twitter 拿不到邮箱,对我来说没用。腾讯微博注册开发者还要验证,我就懒得弄了。
其实拿不到邮箱也能做,但会把用户系统搞得很复杂,而且回复时还不能邮件通知。
计划增加的功能:
- 支持 Markdown。
虽说自己不用,不过既然这么流行,还是支持下吧…… - 支持全文搜索。
基本想法是用结巴分词提取关键词,存入 sorted set,关键词作为 key,出现频率作为 score,文章 ID 作为 member。标题中的关键字和分类、标签名可以作为较大的权重。还得允许手动设置关键词,并作为一个很大的权重。
为了避免占用内存过多,还需要设置 stop words;限制每篇文章的关键词个数和关键词的文章数好像也有必要。
当然结巴分词本身就很占内存,所以应该做成一个独立的服务或脚本,甚至调用 HTTP 接口。
至于英文分词,还没想好用啥,反正比中文简单。 - 提供 REST API。
其实不是为了外部调用,主要是想把网页的主体部分做成用户无关的,方便 CDN 进行缓存,然后再用 AJAX 加载用户相关的部分。 - 支持 archive 页面。
其实不加也一样,主要有些搜索引擎老去尝试访问,看错误日志看得烦。 - 支持按页数分页。
只是为了看上去美观些……
数据库设计:
这里主要考虑可行性,太细节的就忽略了。我也假设你已清楚老版本 Doodle 的数据结构,否则请参考源码。
- MaxID:存储在 hash 里,field 为类型名,value 为该类型的最大 ID。可以用 HINCRBY MaxID kind 1 拿到下一个 ID。
- Article:存储在 hash 里,field 为 ID,value 用 JSON 编码。
- ArticleURL:存储在 hash 里,field 为 URL,value 为 ID。允许给一篇文章设置多个相对链接。
- ArticleHitCount:存储在 hash 里,field 为 ID,value 为 count。
- PublicArticlePublishTime / PrivateArticlePublishTime:存储在 sorted set 里,score 为文章发布时间(按当地时间存储为 %Y%m%d%H%M%S),member 为文章 ID。
按十篇文章一页来算,第五页可以这样获取:ZREVRANGEBYSCORE ArticlePublishTime +inf 0 LIMIT 40 10。
要获取 2013 年 5 月的所有文章可以这样:ZREVRANGEBYSCORE ArticlePublishTime 20130500000000 (20130600000000。
要获取这篇文章的前一篇文章可以这样:ZREVRANGEBYSCORE ArticlePublishTime (20130415043212 0 LIMIT 0 1。
而下一篇文章则可以这样获取:ZRANGEBYSCORE ArticlePublishTime (20130415043212 +inf LIMIT 0 1。
要删除或隐藏一篇文章,可以用:ZREM ArticlePublishTime article_id。
如果由于文章修改了 URL,而导致老的 URL 在 ArticleURL 中找不到,但日期没变,且每天不会发布多于一篇的文章的话,还能重定向到那天的第一篇文章去,避免 404。
缺点:这种方式只能精确到秒,不过正常人不会在一秒内发两篇文章吧;而且貌似精确到分,去掉世纪都行(反正我没打算写到 2100 年)。此外,用 timestamp 也可节约空间,还不用担心时区的问题,不过计算起来稍微复杂些。 - ArticleUpdateTime:存储同上。但因为只提供给 feed,只需要最新的 10 篇文章,所以保存时还需要删除多余的:ZREMRANGEBYRANK ArticleUpdateTime 10 -1。
- Comment:同 Article。不过老版本的 Doodle 里,评论的 ID 不是全局唯一的,迁移过来时需要转换成唯一的。可以按时间顺序生成,记下对应关系,然后替换评论引用中的 ID。
- CommentsOfArticle:存储在 list 里,key 为 CommentsOfArticle:article_id,element 为评论 ID。
还可以直接把评论作为 element 存储到 CommentsOfArticle 里,这样连 Comment 类都不需要了。但删除评论时就不好操作了,而前者可这样删除:LREM CommentsOfArticle:article_id 0 comment_id。
按十条评论一页来算,第五页可以这样获取:LRANGE CommentsOfArticle:article_id 40 49。 - LatestComments:存储在 list 里,element 为评论 ID。因为只提供给 feed,只需要最新的 10 条评论,所以保存时还需要删除多余的:LTRIM LatestComments 0 9。
- Category:存储在 sorted set 里,key 为 Category:[parent_path:]category_name,score 为文章发布时间,member 为文章 ID。只存储公开的文章即可。
要查询一个分类的路径,可以用:EXISTS Category:category_name;查不到时再尝试:KEYS Category:*:category_name。
要查询一个分类的子类,可以用:KEYS Category:category_path:*。
要查询一个分类及其子类的文章,可以先查出它及其子类的 key,然后合并结果集:ZUNIONSTORE ArticlesOfCategory:category_name categories_count category_key1 [category_key2 ...] AGGREGATE MAX。这个值可以保留着当做缓存,需要更新时再重新生成。
要查询一个分类的父类,可以用:KEYS Category:*:category_name。或者拿到其路径,然后 split(':') 即可。
要修改一篇文章的分类,可以用:ZREM Category:old_category_path article_id 和 ZADD Category:new_category_path article_id。
和老版本的 Doodle 的区别在于,之前的分类路径用逗号隔开,现在要用冒号。
因为 KEYS 命令是 O(N) 的,所以似乎缓存一下比较好。如果当前 DB 里 key 比较多的话(目前看来,正常的博客只有 Subscriber 会占用大量 keys),可以放在单独的 DB 里(遇到性能问题,再用 MOVE 命令移动也可);或者在 hash 中保存 name 和 path 的对应关系,然后在应用程序里遍历匹配。还可考虑用一个 hash 或多个 set 存储分类的关系,占用的内存稍多,查询需要递归,修改、合并和移动分类时操作很复杂,且由于访问的 key 会多一倍,性能不一定好。 - CategoryPath:存储在 hash 里,field 为 name,value 为 path。避免查询分类路径时的开销。
- ArticlesOfCategory:存储在 sorted set 里,key 为 ArticlesOfCategory:category_name,score 为文章发布时间,member 为文章 ID。
- Tag:存储在 sorted set 里,key 为 Tag:tag_name,score 为文章发布时间,member 为文章 ID。
获取 Tag 的文章数可以用 ZCARD 命令,批量获取时可以用 pipeline。 - TagCount:存储在 hash 里,field 为 tag name,value 为 count。一来用于缓存文章数,二来避免用 KEYS 获取所有标签名(可以用 HKEYS 替代,N 会小很多,也没有匹配的开销。)
- User:存储在 hash 里,field 为 email,value 用 JSON 编码。
- Subscriber:存储在 string 里,key 为 Subscriber:user_agent[:ip],value 为 count,过期时间为一天后。
获取当天的订阅者可以用:KEYS Subscriber:* 和 MGET subscriber_key1 [subscriber_key2 ...],然后 sum 一下返回值即可。这个值最好缓存下,过期时间一小时以上亦可。
因为 KEYS 命令是 O(N) 的,长度还不定,所以可能会被攻击。这种情况应该屏蔽攻击者,并限制 key 的数量。单独放在一个 DB 里也能提高性能,因为无需匹配 key,获取所有的即可。
还可以搭配一个 sorted set,score 为过期时间,member 为 user_agent[:ip]。可以很容易地找出过期的订阅者,删掉并计算未过期的。不过因为有缓存,查询次数一般远小于写入次数,而 ZADD 也并不是 O(1) 的操作,所以我倾向于使用第一种实现。
Yeah,完美实现所有功能,妥妥的。好吧,其实写这篇文章前,我还不确定是否要舍弃一些功能。
不过奉劝一句:别滥用 Redis。复杂的关系还是用关系数据库吧,不然处理数据时很绕;虽说即使用关系数据库还是很难懂……
from https://www.keakon.net/2013/05/19/%E5%87%86%E5%A4%87%E5%B0%86Doodle%E7%A7%BB%E6%A4%8D%E5%88%B0Linux
No comments:
Post a Comment