uriDB网站的可扩展的技术栈

技术流网站从0到1

目录 [−]

  1. 背景
  2. 产品部署环境
    1. 云主机
    2. 负载均衡
  3. 前端
  4. 爬虫
  5. 后台
  6. 数据库
  7. 搜索
  8. 移动端
  9. 运维
  10. 参考文章

背景

uriDB本身不生产干货,uriDB技术流网站只是大自然的搬运工。
Hacker News诞生依赖,已经有多个中文技术头条的网站了,比如开发者头条极客头条,为什么还要做这样一个雷同的头条网站呢?

有两个原因:
一是我想做一个分类头条的网站,按照技术领域对文章进行分类,这样只对前端感兴趣的同学可以只跟踪最新的前端文章。 同时uriDB只会筛选最新的技术干货,不会将问答,闲聊等技术层次低的文章收录。
二是这么多年来,我涉及的领域包括后台,大数据,前端和移动端的技术也是我感兴趣的领域。心中那份对技术的持久的热情,促使我将多年的技术积累以某种具体的形式呈现出来,籍此展示并能持久的进行技术架构的演化。

因此,uriDB技术流网站也就孵化出来了。虽然目前的访问量比较少,但是看的用户数和访问量在逐步的提升,也是一件令人欣慰的事。至少,这个网站收集的干货也为那些执着学习的同学带来些许的便利和技能提升。

与其说uriDB类似Hacker News网站, 还不说说它类似今日头条, 只不是今日头条上全是新闻类的内容,而uriDB上全是技术干货。今日头条会将目标网站上的内容抓去过来进行重新排版,更加适合阅读。我也抓去了目标文章的内容,却没有进行重新排版显示,主要是考虑到了版权的问题,还是老老实实的做Hacker News一样的转发。

这个网站是2015年国庆节期间开始启动的,也是作为我的side project在维护。我会时不时的将我的新的想法,技术灵感应用于这个网站上。

产品部署环境

云主机

国内的云服务器包括:阿里云 腾讯云 美团云 青云 华为云 天翼云 西部数码 Linkcloud
国外的如 AWS Azure Digital Ocean linnode

云服务器之间的比较大家可以具体的搜索。
我使用 DigitalOcean的云主机作为服务器, 主要考虑价格便宜,而是使用ssd作硬盘,速度好,可以选用新加坡的机房。

负载均衡

使用CentOS 7.1做所有的节点的操作系统。
nginx 1.8.0作为负载均衡。为了实现高可用性,采用keepalived实现, 比如文章Building a Highly-Available Load Balancer with Nginx and Keepalived on CentOS。这样当一台Load balancer宕机的时候,它的功能转移到另外一台Load balancer上。

Keepalived的作用是检测服务器的健康状态,在所有可能出现单点故障的地方为其提供高可用。如果有一台服务器死机,或工作出现故障,Keepalived将检测到,并将有故障的服务器从系统中剔除,当服务器工作正常后Keepalived自动将服务器加入到服务器群中,这些工作全部自动完成,不需要人工干涉,需要人工做的只是修复故障的服务器。keepalived的核心是vrrp,它是通过脚本来调用服务的,所以在keepalived的使用中,仅需关心两点:配置文件(/etc/keepalived/keepalived.conf)和服务脚本(/etc/rc.d/init.d/keepalived)

N年前我在Motorola工作的时候,使用的是一个商业的高可用方案,后来基于HAProxy+keepalived方式实现高可用,应用于视频节目的播放产品中。这里我们使用Nginx作为负载均衡器,所以配置Nginx+Keepalived作为双主高可用负载均衡器。

参考文档中列出了此方案的配置方法的一些介绍文章,文档内容基本类似,读者可以选择查看。
商业版的NGINX Plus提供了Keepalived的集成。

国内厂商使用 Tengine也不少,配置keepalived一样。

使用Golang开发了整个服务器后台程序和爬虫。自2000年开始我用.NET做了三年的开发,2003年以后一致用Java做开发,2015年使用Scala做大数据和高性能服务器的开发。所以最熟悉的开发语言还是Java和Scala。
但是,我想挑战一下自己。Scala代码的优雅(这里指代码本身,不谈论性能等其它方面),面向对象和函数式编程是我的思维得到了极大的扩展。通过对Go语言的学习,我也深深被它的简洁和编译的本地代码所吸引,所以业余将主要精力放在了Go语言的学习和实践上,也尝试为开源项目提供贡献

虽然Java生态圈的库和框架多如牛毛,Go相关的高质量库也不断的涌现。Go在中国也相当的,国内的一些厂商也在应用Golang,如七牛,360等。本身我对Go实现一个高性能的服务器架构不持怀疑态度。

上面一句话是假的。既然决定采用Go作为主开发语言,必然在选型的时候做一些性能的测试。实际的测试结果也表明Go的性能不错。尽管Web Frameworks Benchmark 2015的测试中Go排名19,低于Java实现的Netty,undertow,Servlet, C++/C的实现等,我还是觉得Go的潜力无限。

另一个值得关注的语言是Mozilla主导的rust,但是能否成气候还有待观察。

前端

网站的前端主要采用当前流行的单页程序设计。采用AJAX进行数据的拉取。
2014年在Thistech主要采用ember.js进行开发,积累了一些前端框架经验,所以对ember.js的评价有好有坏。考虑到uriDB网站的形式,动态交互的逻辑不多(搜索,加载),没必要重度使用前端框架如AngularJS, Ember.js, Backbone.js等。

所以最后使用jquery的ajax调用访问服务器的Restful API,以及jquery相关的插件。
前端的css框架采用定制的bootstrap,主要以红色色调为主,因为快到冬天了,红红火火,比较温暖,而没有采用蓝色系,尽管蓝色系的bootstrap也准备好了。

尽然通过jquery动态加载数据,页面基本上就是一个模版,这样动静分离,可以将静态页面cache住,减少服务器的压力。
相关的javascript和css进行了合并,并且进行了压缩。
在nginx出配置了gzip特性,超过1k的响应会被压缩。
将js,css,图片分流到另外一个域名上 http://static.uridb.com。
前端的图片采用动态加载的方式,只有滚动到显示时才进行加载,从而避免页面加载时间过长。

更多的前端优化请参看: 前端性能优化指南

爬虫

爬虫运行在单独的服务器上,它会定时的到指定网站抓取最新的文章列表,这里对网站的请求使用了我的一个开源项目: goreq,它简化了http client的操作。
它基于RSS或者goquery进行分析列表,得到候选文章。
但是不止于此,它还会基于候选文章列表访问每个抓取的文章,获取文章的元数据和正文。
基于这些信息,它会生成文章的元数据,并进行自动化的分类。
当前的分类还是一个简单的根据关键词的分类,所以有时候会造成误判, 比如一篇《Go, Go, GO,让我们开始用Java编程》可能会被分类成Golang文章,尽管它属于Java栏目的。

爬虫应该能处理超时以及意外情况(不规整的html),我们可以容忍一条文章的损失,但是必须保证整个爬虫程序不会垮掉。

充分利用Go interface,我们实现了简洁的爬虫程序,并且可以很方便的添加新的网站源。

通过crontab进行抓取,每天会抓取几次,每次的抓取时间少于10分钟。

后台

使用bone作为http Multiplexer,而没有采用其它流行的Go web框架如: Gorilla, 谢工的Beego等,主要还是考虑到简单和性能。
依照bone官方的测试:

1
2
3
4
5
6
- BenchmarkBoneMux 10000000 118 ns/op
- BenchmarkZeusMux 100000 144 ns/op
- BenchmarkHttpRouterMux 10000000 134 ns/op
- BenchmarkNetHttpMux 3000000 580 ns/op
- BenchmarkGorillaMux 300000 3333 ns/op
- BenchmarkGorillaPatMux 1000000 1889 ns/op

它的性能比Gorilla好太多了。使用它开发也非常的简单快捷。

后台是无状态的服务设计。Session需要在所有的节点共享。
使用 gorilla/sessions实现session的管理,并且使用mongostore将session存储在MongoDB中。
也有其它的session存储方式, 如MySQL, Redis等:

1
2
3
4
5
6
7
8
9
10
11
12
13
* [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB
* [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt
* [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase
* [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - Dynamodb on AWS
* [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache
* [github.com/hnakamur/gaesessions](https://github.com/hnakamur/gaesessions) - Memcache on GAE
* [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB
* [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL
* [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL
* [github.com/boj/redistore](https://github.com/boj/redistore) - Redis
* [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB
* [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak
* [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite

得益于Go http库的设计,可以实现很精巧的请求拦截, 比如进行权限检查,日志输出和Panic处理等。比如下面的panic的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func panicCover(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
defer func() {
if r := recover(); r != nil {
glog.Error("Recovered in handlers", r, stack.Callers(3))
http.Error(w, "Error", http.StatusInternalServerError)
}
}()
f(w, req)
}
}
mux.Get("/abc", panicCover(http.HandlerFunc(oneHandler)))

基于golang template可以实现网站的模板化处理,将布局layout和组件widget分开,这样可以公用header, footer,菜单,边栏等。
Restful API提供JSON数据,将从Mongo或者缓存中的数据返回给前端。

使用mgo作为数据访问层, mgo对于session/连接池的管理官方还缺乏一个详细而精确的说明,这里使用了基于请求的session管理:Best practice to maintain a mgo session

uriDB提供了微博,QQ,Github,Linkedin,Google的OAuth2登录,x/oauth2并不能完全cover所有的oauth2的认证,因为每个厂家都有自己的方言,所以实现的时候针对每个厂家做了底层的处理。
同时还提供了注册服务器帐号的功能。
为了避免机器人注册和登录,提供了验证码的功能,基于dchest/captcha
我们需要考虑多节点的情况,填写验证码后请求可能会被提交到另外一个节点,所以验证码需要在节点中共享,所以实现了一个基于memcached的store。

考虑uriDB站点的情况, 操作相对较少,大部分的操作还是,所以很适合用缓存来减少数据库的压力。使用memcached缓存文章的查询,极大地减少数据库的压力。

uriDB还提供了搜索的功能,详情见搜索那一节。

另外,还设计了管理员的简单平台,可以对文章进行审核,对用户进行管理,对文章进行修改。还提供了命令行的工具。
但是这些基本上不会用到,而是采用了slack机器人的方式,随时随地对网站进行管理: 使用Go开发一个 Slack 运维机器人

日志采用glog,简单好用。

数据库

数据库的选择倒不是一个很艰难的选择,无论mysql还是MongoDB,cassandra都能胜任。
基本上现在每天收集的文章不到100篇,一年也才3万多篇, 10年也才30万。对数据库的存储压力不大。
所以这里选用了我近两年一直使用的Mongo。
Mongo数据的备份开始参照: Mongodb 定期备份

如果数据库的压力增大,我会考虑将当前的MongoDB单例迁移到Replica Set,如果性能再不济,将其迁移到cluster模式,通过shard方式分担数据库的压力。我想基本不会达到这样的压力的。

但是数据库的备份是至关重要的,一旦机器宕机,或者遭受攻击,或者运维误操作,必须能恢复回来,否则哭也来不及。

搜索

当前的搜索基于Mongo的查询,搜索字段都建立了索引。但是在数据量大的时候,分页查询会是一个瓶颈,我想这对于实现过大数据分页的读者来说并不陌生。当offset很大的时候, 因为尽管你会skip这些offset取得limit条数据,也会搜索这些所有的数据,越往后查询越慢。
杨卫华(TimYang)有一篇很好的总结文章:为什么超长列表数据的翻页技术实现复杂

所以一般实现扶梯方式,只提供上一页下一页的功能,并不能直接跳转到n页。这样就可以通过最后一条或者第一条的偏移,获取下一页或者上一页的数据。
另外还要根据多字段查询,都建立索引对数据库的性能也有影响。

总的来说,在大数据量的情况下,查询并不是一件容易的事。

现在正在做的一件事是使用Elasticsearch做索引服务器,可以很好的解决查询的问题。Elasticsearch已经在很多大公司得到广泛的应用,绝对是值得使用的做内容索引的产品。

最简单的方式,将Mongo数据库的数据导入到Elasticsearch中: 基于Golang将MongoDB的数据同步到Elasticsearch。但是我不会采用这种方式,而是采用一种近实时的索引架构。

我索引的内容就是文章的元数据,元数据插入的时候只有两种:爬虫插入,读者提交/管理员更改/删除。所以在数据插入/更改/删除的时候将操作放入到nsq中,Elasticsearch服务器读取到文章的变动,实时更新索引服务器。

使用nsq可以很好的实现异步的操作,将文章的变动和索引服务器的操作交给不同的服务器进行处理。
而且nsq也会作为uriDB异步服务的基础框架。

移动端

事实上,我先实现的Android版的移动端, 叫"技术快报", 如果你在一些应用商店搜索"Android 技术快报"就能搜到它。

因为当时还没有客户端,所以爬虫代码就在手机端实现的,出于时间的考虑,只能拉取很少的文章资源。

新的版本将直接从 http://uridb.com 拉取数据,并提供按照栏目进行浏览的方式,基本上会类似网易新闻这样的客户端。

运维

Elasticsearch、Logstash和Kibana会是日志分析的三剑客。
当前的运维还很薄弱,服务器的监控,软件的重启和升级都靠手工,这些都是有待加强的地方。

当前实现了一个slack机器人,这样我就可以通过聊天室让机器人完成一些网站管理的工作,现在感觉超方便,我甚至可以在炒菜的时候得到网站的消息,也可以在睡觉前查看服务器的状态。

参考文章

  1. http://www.tokiwinter.com/building-a-highly-available-load-balancer-with-nginx-and-keepalived-on-centos/
  2. https://www.digitalocean.com/community/tutorials/how-to-set-up-highly-available-web-servers-with-keepalived-and-floating-ips-on-ubuntu-14-04
  3. http://seanlook.com/2015/05/18/nginx-keepalived-ha/
  4. http://nmshuishui.blog.51cto.com/1850554/1405484
  5. http://isux.tencent.com/h5-performance.html
  6. https://developer.yahoo.com/performance/rules.html
  7. https://github.com/PuerkitoBio/goquery
  8. https://github.com/smallnest/goreq
  9. https://github.com/go-zoo/bone
  10. http://github.com/gorilla/sessions
  11. http://github.com/kidstuff/mongostore
  12. https://labix.org/mgo
  13. http://stackoverflow.com/questions/26574594/best-practice-to-maintain-a-mgo-session
  14. https://godoc.org/golang.org/x/oauth2
  15. https://github.com/dchest/captcha
  16. https://github.com/golang/glog
  17. https://github.com/bradfitz/gomemcache/memcache
  18. http://colobu.com/2015/10/27/mongodb-backup/
  19. http://colobu.com/2015/10/27/Sync-Transformed-Data-from-MongoDB-to-Elasticsearch/
  20. https://github.com/nsqio/nsq
  21. https://www.gitbook.com/book/fuxiaopang/learnelasticsearch/details
  22. http://kibana.logstash.es/
  23. https://www.gitbook.com/book/chenryn/logstash-best-practice/details
  24. https://www.gitbook.com/book/looly/elasticsearch-the-definitive-guide-cn/details