项目总结-美食类社交app的架构设计

很早之前就想写这一篇东西,或者说是自己的总结吧。项目虽然融资失败了,作为技术者来讲,心里的失落是难免的,不过在项目的这段时间,技术架构上也积累了不少东西,有必要对自己一个交代。

先来讲讲这个项目整体上用到的一些技术和框架:

1)使用Ruby on Rails开发,rails基于3.2版本,兼容性上升级到4也是无压力,ruby使用2.0以上版本。

2)数据库使用mongodb,由于用到了aggregate,需要2.4以上版本。
因为mongodb对于地理位置的优势,这是一个不错的技术选型。

3)数据库还使用了redis,用来持久化一些计数或者关联比较弱的数据,
例如,某一道菜喜欢的人数,某一个用户follow的用户的动态,
这些数据是可以延迟更新或者允许丢失一部分也不会造成脏数据。

另外redis还用在缓存一些数据,这部分数据不能放在memcache中,因为是不允许丢失的,
例如某一道菜所有喜欢它的用户的id,已数组的形式保存在redis中。

最后redis还用在了队列系统中。

4)数据库还使用了memcache,用来做缓存一些简单的数据和页面,例如用户的个人主页,用户的积分页等等。

5)使用了sidekiq,用来处理异步处理耗时比较大的请求,例如发推送,切割图片,同步相关数据等等。

6)使用了solr,用来检索门店,
10万级别的门店数据,如果用mongodb,即使加索引,
对于一些模糊匹配还是会非常的慢,
全文检索可以解决速度的问题。(幸好solr也支持地理位置检索)

在服务器架构上,两台应用服务器负载均衡,一台服务器做nginx反向代理,三台mongodb使用replica set模式,两台redis,两台memcache,架构图如下:(比较简单)

关于follow和followers的问题

这个我想是每个做社交都不可避免的。随之而来的问题是,不一定像twitter或者微博那么的数据量大,但是核心会员有个一两万粉丝还是肯定的,这一两万粉丝的动态推送估计就够呛了。

我们知道这里无外乎两种模型,“推”和“拉”,推的问题是每次都需要推很多数据,就算用后台队列也不一定可以很快的到达,并且非常占用系统资源,如果用户不去看,推都推了,更是资源的浪费。拉的问题是每次用户访问的时候去拉数据,资源不会浪费,可是对于关注的多的用户,那会是灾难性的。

我这边的做法是“推”和“拉”结合起来,系统对于每个用户会做一次区分,当发现这个用户关注的人数在一定比较少的范围,例如100个以内,或者用户自从注册以后很长一段时间都没有访问,例如一个月都没有登录过,那么我就认为这个用户是“死用户”,对于死用户,我就采用拉的模式,因为就算他登录,因为数据量不大,也不会有太大的问题。相对于那些活跃用户,我用推模型,因为对于一个网站来讲,确实只有20%的属于活跃用户。

当然,在这个基础上,还可以更加深入下去,例如增加缓存,或者更好的来判断用户的权重,不过因为优化后没有遇到更大的问题,就先这样了,毕竟过度优化是万恶之源。

要做就做健壮的应用

首先,这里应用包括web网站,手机app等等。

健壮的应用应该是可维护的,可自动恢复,可持续更新,可跟踪日志,可及时反馈。

先来看看目前项目的方式:

项目代码 + 自动部署

仅仅这样远远不够,存在的问题:

1)服务器如果重启,如何做到项目自动开始运行,而不是手动再部署一遍

2)正式环境下项目日志会每天不断增长,不利于以后日志的分析

3)正式环境下项目如果自己挂了,如何自动重启,如何给出通知

4)数据库如何自动备份

5)部署的时候如何自动测试

所以健壮的应用应该在项目一开始就解决上面的问题

目前我是这么做的

1) 使用脚本来启动应用服务,脚本放在服务器的自启动里面

2)使用log_rotation按天切割日志

3)使用monit自检查应用进程,如果挂了自行重启,并且给出邮件通知

4)使用<a href="https://github.com/meskyanichi/backup">backup</a>自动备份数据库

5)使用Git hooks在部署到production的时候自动测试

另外上述的功能在一个deploy的期间,自动完成。

至此,这样才是一个健壮的上线应用。

更改默认的mysql和mongodb数据dbpath

修改mysql

sudo /etc/init.d/mysql stop
sudo cp -R -p /var/lib/mysql /newpath
sudo chown mysql:mysql /newpath
vi /etc/mysql/my.cnf
#####Look for lines beginning with /var/lib/mysql. Change /var/lib/mysql in the lines with the new path.
sudo /etc/init.d/mysql restart

修改mongodb

sudo service mongod stop
vi /etc/mongodb.conf

####
Change this:
dbpath=/var/lib/mongo

With:
dbpath=/home/myuser/data/mongo
###

cp -ra /var/lib/mongo  /home/myuser/data/mongo

chown mongodb:mongodb /home/myuser/data/mongo

sudo service mongod start

502 bad gateway error when deploy rails app with dokku on digitalocean

Follow this how-to-use-the-dokku-one-click-digitalocean-image-to-run-a-ruby-on-rails-app guide, i got nginx error – ’502 bad gateway’.

How to solve?

first, check app logs to see what happen.

dokku logs <your-app-name>

I got DATABASE_URL error. it seems dokku didn’t link my app with PostgreSQL.

Checkout the PostgreSQL database_url:

dokku postgresql:info <your-app-name>

Host: xxxxxx
Port: 49154
User: 'root'
Password: '*********'
Database: 'db'

Url: 'postgres://root:*******@xxxxx:49154/db'

now link this url with my app:

dokku postgresql:link <your-app-name> <your-db-name>

or

dokku config:set <your-app-name> DATABASE_URL='postgres://root:*******@xxxxx:49154/db'

git push again, got db:migration error!

rake manually according to this gist:

docker run -i -t app/mg /bin/bash

cd app/
export HOME=/app
for file in /app/.profile.d/*; do source $file; done
hash -r
cd /app
RAILS_ENV=production rake db:migrate

DONE! push again, it works.

[mongodb]tcmalloc: large alloc out of memory, printing stack and exiting

最近Mongodb会经常突然挂掉,检查日志发现如下的错误:

tcmalloc: large alloc 2061584302080 bytes == (nil) @
Tue Nov 26 17:45:04.539 out of memory, printing stack and exiting:
0xdddd81 0x6cfb4e 0x121021d 0xafcc1f 0xaf815f 0xaf8d1d 0xaf8e0f 0xaf52ae 0xaf53c9 0xb1eb11 0x8ab6a2 0x8d78ca 0x8d951d 0x8daa72 0xa80970 0xa8523c 0x9f9079 0x9fa5a3 0x6e8b88 0xdca34e
 ./mongod(_ZN5mongo15printStackTraceERSo+0x21) [0xdddd81]
 ./mongod(_ZN5mongo14my_new_handlerEv+0x3e) [0x6cfb4e]
 ./mongod(_Znwm+0x6d) [0x121021d]
 ./mongod(_ZNSt6vectorIN5mongo18DocumentSourceSort9KeyAndDocESaIS2_EE7reserveEm+0x6f) [0xafcc1f]
 ./mongod(_ZN5mongo18DocumentSourceSort12populateTopKEv+0x6f) [0xaf815f]
 ./mongod(_ZN5mongo18DocumentSourceSort8populateEv+0x2d) [0xaf8d1d]
 ./mongod(_ZN5mongo18DocumentSourceSort3eofEv+0xf) [0xaf8e0f]
 ./mongod(_ZN5mongo18DocumentSourceSkip7skipperEv+0x6e) [0xaf52ae]
 ./mongod(_ZN5mongo18DocumentSourceSkip3eofEv+0x9) [0xaf53c9]
 ./mongod(_ZN5mongo8Pipeline3runERNS_14BSONObjBuilderERSs+0x1b1) [0xb1eb11]
 ./mongod(_ZN5mongo15PipelineCommand3runERKSsRNS_7BSONObjEiRSsRNS_14BSONObjBuilderEb+0x132) [0x8ab6a2]
 ./mongod(_ZN5mongo12_execCommandEPNS_7CommandERKSsRNS_7BSONObjEiRSsRNS_14BSONObjBuilderEb+0x3a) [0x8d78ca]
 ./mongod(_ZN5mongo7Command11execCommandEPS0_RNS_6ClientEiPKcRNS_7BSONObjERNS_14BSONObjBuilderEb+0x71d) [0x8d951d]
 ./mongod(_ZN5mongo12_runCommandsEPKcRNS_7BSONObjERNS_11_BufBuilderINS_16TrivialAllocatorEEERNS_14BSONObjBuilderEbi+0x5f2) [0x8daa72]
 ./mongod(_ZN5mongo11runCommandsEPKcRNS_7BSONObjERNS_5CurOpERNS_11_BufBuilderINS_16TrivialAllocatorEEERNS_14BSONObjBuilderEbi+0x40) [0xa80970]
 ./mongod(_ZN5mongo8runQueryERNS_7MessageERNS_12QueryMessageERNS_5CurOpES1_+0xd7c) [0xa8523c]
 ./mongod() [0x9f9079]
 ./mongod(_ZN5mongo16assembleResponseERNS_7MessageERNS_10DbResponseERKNS_11HostAndPortE+0x383) [0x9fa5a3]
 ./mongod(_ZN5mongo16MyMessageHandler7processERNS_7MessageEPNS_21AbstractMessagingPortEPNS_9LastErrorE+0x98) [0x6e8b88]
 ./mongod(_ZN5mongo17PortMessageServer17handleIncomingMsgEPv+0x42e) [0xdca34e]

内存溢出了,一开始我以为是有些排序没有加索引,后来一些地方加了索引后还是会出现,细想了下,如果是没加索引的话,是不会让整个Mongodb宕机的。

后来在Mongodb的issue上查到了这样一条提交的bug清单SERVER-10136

原来aggregation如果传递一个$skip特别大的值的时候,就会内存溢出。我看到这个bug已经被修复了,不过是在2.5.2版本,最新的稳定版是2.4.8。所以我们需要在自己的应用程序里面控制,让$skip的值不要超出总长。

[mongodb]exception: $concat only supports strings, not NumberInt32

今天在用mongodb操作aggregation的时候出现这个问题,我是想格式化日期,例如”2013-10-17 04:41:37 UTC”变成”10月17日”,

'fdate' => { '$concat' => ['$date.month', '月', '$date.day', '日'] }

出现 exception: $concat only supports strings, not NumberInt32

原来$concat只能操作字符串,不支持数字类型,解决办法是用$substr

$date形如’2013-10-13 11:17:18 UTC’

'fdate' => { '$concat' => [ {$substr=>['$date', 5, 2]}, '月', {$substr=>['$date', 8, 2]}, '日'] }

$substr接受两个参数,一个是字符串的起点,一个是截取的字符串的长度。