当初遇到的问题是这样的:薄荷的一个子系统 status 中原本使用 http api call 获取 record 子系统的一些数据,应用 SneakersPacker 之后,把代码改成了 rpc call。刚开始的时候 SneakersPacker 还有一些问题,为了防止 rpc call 出状况,在代码中提供了一层保护机制,也就是当 SneakersPacker.remote_call 触发异常之后,通过 http api call 获取数据。代码很简单,如下所示:
1 2 3 4 5 |
|
但是实际运行结果很让人诧异,remote_call 的确触发了异常,但是代码中的 rescue 并没有捕获到异常,而是直接导致程序 500 错误,大量异常记录在日志和监控系统中,让人百思不得其解。在那之前我以为孤立的 rescue 会捕获全部异常的,仔细研究网络上的一些文档后,发现并不是这样,rescue 其实只捕获 StandardError 类型的异常。再去查阅 SneakersPacker 的代码,发现原来报了很多的异常 RemoteCallTimeoutError 是这样定义的:
1
|
|
RemoteCallTimeoutError 选择父类 Exception 是错误的,应该选择 StandardError,改成下面之后 RemoteCallTimeoutError 象期望的一样被捕获了。
1
|
|
为了说明 rescue 这个关键字的行为,我们看下面几个例子:
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 |
|
代码1,代码2和代码3的 rescue 都只捕获 StandardError 类别的异常,而代码4明显不同,代码4会捕获所有的异常。
一些直接继承自 Exception 而不是 StandardError 的异常如下,都是系统层级严重错误,不应该由应用捕获。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
综上所述,总结一些 ruby 中捕获异常的原则(正确姿势)
end.
]]>大家应该比较熟悉数据库查询时的 N+1 问题,在缓存中同样存在 N+1 问题。当应用中出现需要多次读取缓存的时候,虽然单次读取缓存速度很快,但是多次读取缓存累计时间相当可观,很可能会成为一个性能瓶颈。
直接给一个演示例子,生成 10000 个缓存对象 user:<i>:counter
存储整数,然后分别单次,批量读取缓存,统计每种方式消耗时间。
代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
执行结果如下所示:
1 2 3 4 5 6 |
|
从结果中可以看到,分批读取(每次 30~50时)速度很快,要比每次 1 个对象快 100 多倍,每次读取 10 个对象也要快 100 倍,批次并不是越大越好,每次读取 100 个速度比 10 个更慢。
在薄荷生产系统性能优化中,我们遇到过好几次类似的问题。例如有一个 api 需要返回多个存放缓存的用户资料,单个用户资料缓存读取时间接近 1 ms,50 个用户资料消耗接近 45 ms 时间,它导致这个 api 响应时间很长,把 50 次用户资料缓存读取放到一次批量读取后,缓存读取时间减少为 3 ms 左右,应用性能立即大幅提升。
为什么批量读取时间消耗大幅减少呢?因为每一次缓存读取过程有很多固定开销,包括加锁,系统(网络)调用等等,当使用批量读取时,这些固定开销统统节省了,而缓存服务器单次 key 查找和数据返回消耗时间差别不大,所以整体时间大幅减少。
当然,批量读取增加了应用的复杂度,如果应用性能没有问题,或者缓存读取次数很少,并没有必要改造成批量读取形式。
最后一点是,通常我们以 fetch 方法使用缓存对象,这时批量读取方法如下所示:
1 2 3 4 5 |
|
总结:缓存虽然很快,但它毕竟也是一次 IO 操作,同样需要消耗一定时间,如果某一次特别大量读写缓存,很可能会以前性能问题,通过批量读取方式是解决该问题的有效手段。
]]>“==” 使用最频繁,它通常用于对象的值相等性(语义相等)判断,在 Object 的方法定义中,“==” 比较两个对象的 object_id 是否一致,通常子类都会重写覆盖这个方法,通过比较内部值来判断对象是否相等。
比如 ActiveRecord::Base 对 “==” 的定义
1 2 3 4 5 6 |
|
通过 model 的 id 属性比较两个 ActiveRecord::Base 实例是否相等。
“===” 主要用于 case 语句中对象的相容比较,看代码比较容易理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
case 背后是拿每一个 when 后面的对象与 obj 进行 === 方法计算比较,比如上面的代码就是
分别求 /abc/.===(obj)
,(3..5).===(obj)
,Symbol.===(obj)
。
关键得看 ===
方法里如何定义,Class
类中,===
定义为 obj.is_a?(klass),所以 case 可以现实 obj 的类型判断。
特别要注意的是和其他相等判断不同 “===” 通常没法交换,也就是很可能
a.===(b) != b.===(a)
,比如 /abc/ === "abcd"
为 true,但 "abcd" === /abc/
为 false。
“equal?” 其实是最简单的,但是也是最容易让人搞混的判断。说它简单是因为这个方法的语义是比较两个对象是否相同(是否有相同的 object_id),Object 的方法适用所有对象,不应该对其重写覆盖。说它容易让人搞混,是因为 ruby 和 java 中 “==” 和 “equal?” 方法的语义正好是相反的,ruby 中 “equal?” 表示对象引用相同,而 java 表示对象值相同。
eql?
用于对象 hash 值判断,如果两个对象的 hash 值相等,就返回 true,否则返回 false。Object 的定义里,“eql?” 和 “==” 是等价的。通常可以把 “eql?” 看作比 “==” 更严格的相等,比如:
1 2 |
|
先说说广播功能特性。广播功能主要用于薄荷向全体用法传递的消息(包括各种广告,告示和提醒等),当系统管理员在后台发出一条广播后,用户 app 上的消息图标显示小红点,当用户点击图标进入广播列表查看消息内容后,小红点消除。
重构前,广播功能采用了 redis 存储方案,为每一位用户建立一个已读广播集合 set,该 set 中存放用户已读的广播 id。最常见的操作是获取某位用户未读广播和数量,其方法是:使用全体 广播 id 集合与已读广播 id 集合比较,差异部分就是未读广播,对其计数得到未读广播数量。
这种方法的问题有两个:一是 redis 消耗内存巨大,随着用户数增加和发送广播数量增加,内存一直累积;二是当广播数量增加到比较大的数量后,获取未读广播很慢。
对其重构如下:去除了 redis 存储,在数据库中存储每一个用户最新访问广播的时间 last_read_time,建立一个全体的广播列表缓存,广播列表只存放广播 id 和 created_time,列表根据 created_time 从新到旧排序。获取某个用户未读广播和其数量方法:把广播列表缓存的生成时间 created_time 和用户的 last_read_time 比较,比 last_read_time 大的属于未读广播,比 last_read_time 小的属于已读广播。
这次重构,去除 redis 存储,大幅减少内存使用,算法调整让常见操作速度更快,效果很棒。
]]>最近写一个 gem 的时候偶然接触到 Rails ActiveSupport 扩展 module 的 mattr_accessor 系列方法,包括 mattr_accessor、mattr_reader 和 mattr_writer。 记得以前探索 Rails 源代码的时候经常遇到 mattr_accessor 方法,当时并没有细究,这次碰巧要自己用到,所以仔细研究了其文档和实现源码,居然发现文档描述有明显的错误。
Rails 的官方文档中提到,mattr_accessor 用于为类属性定义类和实例对象两者的访问器,然后还提供一段示例代码演示其用法。
1 2 |
|
演示代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
当我运行该代码的时候,发现无法运行,报错在 Person.hair_colors
处,信息如下:
1 2 3 |
|
刚开始还有点不相信,分别在 Rails 4.1.8,4.2.0 和 3.2.x,Ruby 1.9.3,2.0.0 和 2.1.5 下运行, 都出现这个错误,这下确信文档描述应该是有问题的。我想把问题彻底搞清楚,于是仔细查看 Rails ActiveSupport 中相关的源代码,发现的确是文档描述的行为和程序实际行为不符。
mattr_accessor 系列方法的代码在 rails/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
文件中,相关的代码并不复杂,部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
从中可以看到,mattr_accessor 分别 call mattr_reader 和 mattr_writer,mattr_writer 的主要逻辑是定义类变量(class variable,命名为 @@#{sym}
),然后定义类方法 (见 def self.#{sym}=(obj)
)和普通实例方法(见 def #{sym}=(obj)
)。当 Person include HairColors
的时候,普通实例方法是 mix 进 Person 的,但类方法并不会被 mix 进 Person。可以用下面更简明的例子演示 include module 并不能 mix 类方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
原来 mattr_accessor 为 HairColors 生成的 def self.hair_colors
类方法不能 mix 进 Person,从而导致 Person.hair_colors 出错,但是 Person.new.hair_colors 能够正常运行的。
如果希望 Person 通过类方法和实例方法都能使用 hair_colors ,应该怎么做呢? 可以把 mattr_accessor 放在 included 钩子中执行,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
它的一个问题是 HairColors.hair_colors
不可用了,但我发现 Rails 中大多是使用这种手法处理的,这种情况下估计不怎么要直接用到 HairColors.hair_colors
吧。
Rails 官方文档中关于 mattr_accessor 的描述的确有问题,示例代码不能正确运行,而且还会误导使用者,通过仔细探索 Rails 的源代码,找到了问题的源头。我已经修正文档的错误,提交了 pull request
,希望能够被接受。
在关系数据库存储的系统里,实现唯一计数的方法就是 select count(distinct <item_id>)
,它十分简单,但是如果数据量很大,这个语句执行是很慢的。用关系数据库另外一个问题是插入数据性能也不高。
Redis 解决这类计数问题得心应手,相比关系数据库速度更快,消耗资源更少,甚至提供了 3 种不同的方法。
Redis 的 set 用于保存唯一的数据集合,通过它可以快速判断某一个元素是否存在于集合中,也可以快速计算某一个集合的元素个数,另外和可以合并集合到一个新的集合中。涉及的命令如下:
1 2 3 |
|
基于 set 的方法简单有效,计数精确,适用面广,易于理解,它的缺点是消耗资源比较大(当然比起关系数据库是少很多的),如果元素个数很大(比如上亿的计数),消耗内存很恐怖。
Redis 的 bit 可以用于实现比 set 内存高度压缩的计数,它通过一个 bit 1 或 0 来存储某个元素是否存在信息。例如网站唯一访客计数,可以把 user_id 作为 bit 的偏移量 offset,设置为 1 表示有访问,使用 1 MB的空间就可以存放 800 多万用户的一天访问计数情况。涉及的命令如下:
1 2 3 4 |
|
基于 bit 的方法比起 set 空间消耗小得多,但是它要求元素能否简单映射为位偏移,适用面窄了不少,另外它消耗的空间取决于最大偏移量,和计数值无关,如果最大偏移量很大,消耗内存也相当可观。
实现超大数据量精确的唯一计数都是比较困难的,但是如果只是近似的话,计算科学里有很多高效的算法,其中 HyperLogLog Counting 就是其中非常著名的算法,它可以仅仅使用 12 k左右的内存,实现上亿的唯一计数,而且误差控制在百分之一左右。涉及的命令如下:
1 2 |
|
这种计数方法真的很神奇,我也没有彻底弄明白,有兴趣可以深入研究相关文章。
redis 提供的这三种唯一计数方式各有优劣,可以充分满足不同情况下的计数要求。
]]>下面以一个例子来说明。在 light 系统中,有一张数据表是 diet_items, 主要字段是 id, schedule_id, meal_order food_id, weight, calory 等等,它的每一条记录表示为用户生成每日的减肥计划(减肥食谱 + 运动计划)中的一条饮食项,平均一条的计划有 10 多条数据,数据量非常大,预计每天生成数据会超过 100 万条,所以对其做了分表处理,根据 schedule_id hash 分成 60 张表,也就是数据将动态分到 60 张表中。分表后 diet_items 的建表语句如下所示:
1 2 3 4 5 6 7 8 9 10 11 |
|
分表之后,所有查询 diet_items 的地方都要求带上 schedule_id,比如获取某一个 schedule 的所有 diet_items,通过 schedule. diet_items,获取某一个 id 的 diet_item 也是通过 schedule.diet_items.find(id)
进行。生成 diet_item 也没有问题,因为生成 diet_item 都是通过 schedule.diet_items.build(data)
方式,在生成的时候都是带了 schedule_id 的。
观察 newrelic 日志,发现 diet_item 的 update 和 destroy 相关的请求特别慢,仔细分析后,发现这两种操作非常忙是由于 ActiveRecord 生成的 sql 并没有带 schedule_id 导致。
diet_item update 操作 ActiveRecord 生成的 sql 语句类似于 update diet_items set … where id = <id>
。
diet_item destroy 生成的语句类似于 delete diet_items where id = <id>
因为没有带 schedule_id,导致这两种语句都需要 mysql 扫描 60 张分区表才能够完成一个语句执行,非常慢!
知道原因之后就好办了,把原来的 update 和 destroy 调用改为自定义版本的 update 和 destroy 调用就可以了。
diet_item.update(attributes)
改成 DietItem.where(id: diet_item.id, schedule_id: diet_item.schedule_id).update_all(attributes)
diet_item.destroy
改成 DietItem.where(id: diet_item.id, schedule_id: diet_item.schedule_id).delete_all
这样生成的 sql 都带上 schedule_id 条件,从而避免了扫描全部的分区表,性能提升立竿见影。
]]>举一个实际的例子:我们的系统里有一个商店模块,商店中重要的一块是对产品信息的管理,比如运营人员常常会编辑产品的信息,包括产品标题,营销口号和价格等等。因为修改十分频繁,碰巧同时编辑提交修改的话,就会偶尔遇到修改丢失的问题,运营人员 A 修改产品标题,运营人员 B 修改价格,A 和 B 提交修改都提示修改成功,但是结果上只是 A 的修改结果生效,B 的修改被 A 的修改冲掉了。
仔细研究原因,发现是因为修改功能缺少操作冲突机制,而修改操作同时发生导致了问题。 如下图所示,A 和 B 同时从数据库中查询数据,在 web 页面中修改同样的数据,提交保存时是以 web 页面中提交的数据为准,从而导致 A 的修改把 B 的修改给覆盖了。
Rails 的 乐观锁Optimistic Locking 是解决这个问题的有力工具,它的原理是在数据库表中增加一个字段(默认是 lock_version,可配置)记录数据的版本号,每个提交的修改都带上这个版本号,在真正 update 修改数据之前,先判断提交的 lock_version 数据和数据库中的是否一致,如果不一致,则认为发生数据冲突,将抛出 ActiveRecord::StaleObjectError
异常,这样程序就可以捕获这个异常,提醒用户发生了冲突,由用户去协调解决冲突。
相关示例代码如下所示:
1 2 3 4 5 6 7 8 9 |
|
web 开发和 API 开发中难免要详细分析 http 请求和响应信息。web 开发的话,浏览器提供了便利的工具,比如 chrome 和 IE 都带了 develop tool,而 firefox 更是有十分强大的 firebug,可以让 http 请求的所有秘密一览无遗。目前是 app 大流行的时代,想要观察 app 中得 http 请求的秘密,浏览器的工具和插件都无能为力,有不少本地化的软件可以很好的解决这个问题,Windows 平台下有大名鼎鼎的 Fiddler 和 HttpWatch,Mac 平台下有 Charles。Charles 是一个收费软件,价格不菲要 $50。钱还不是关键问题,作为一名 geek,当然想更向往开源,轻量的解决方案了,无意中发现 pproxy,简单使用了一下,觉得相当棒,可以做绝佳的替代方案。
pproxy 和 Fiddler、Charles 最大的不同是,它是一个开源软件,使用 go 编写,代码托管在 github 上。因为开源,作为一名 geek,就可以通过阅读源代码对其工作机制一探究竟,如果发觉某些方面不能满足需求,可以直接向开发者提需求,也可以自己动手,丰衣足食,造福大众。
pproxy 的工作机制和本地化软件如 Fiddler、Charles 差别很大。Fiddler 和 Charles 是一个本地化软件,通常是安装在桌面电脑上,通过在桌面电脑建立 proxy,然后截获的 http 请求和响应数据,提供一个本地化的 UI 界面提供服务。而 pproxy 是一个服务端软件,通常安装在 Linux 服务器上(当然也是可以安装在桌面电脑上),在服务器上建立 proxy 截获 http 请求和响应数据,另外提供一个远程的基于 web (html5)的 UI 界面提供服务。
Fiddler UI 如下所示:
pproxy UI 如下所示:
pproxy 使用比较简单,具体可以参考其使用说明 pproxy。 难得的是他的作者是中国人,所以说明都是中文的。
pproxy 是一个开源的轻巧的 http 抓包分析工具,尤其适合 API http 请求分析,完全可以替换昂贵的收费工具,同时也可以探索其源码学习 GO 语言,学习 http 协议和分析方法。
]]>我们可以对系统存储使用的数据以两种角度分类,一种是按数据的大小划分,分成大数据和小数据,另一种是按数据的冷热程度划分,分成冷数据和热数据,热数据是指读或写比较频繁的数据,反之则是冷数据。
可以举一些具体的例子来说明数据的大小和冷热属性。比如网站总的注册用户数,这明显是一个小而热的数据,小是因为这个数据只有一个值,热是因为注册用户数随时间变化很频繁。再比如,用户最新访问时间数据,这是一个量比较大,冷热不均的数据,大是数据的粒度是用户级别,每一个用户都有数据,如果有一千万用户,就意味着有一千万的数据,冷热不均是因为活跃用户的最新访问时间变化很频繁,但是可能有很大一部非活跃用户访问时间长时间不会发生变化。
大体而言,Redis 最适合处理的是小而热,而且是写频繁,或者读写都比较频繁的热数据。对于大而热的数据,如果其它方式很难解决问题,也可以考虑使用 Redis 解决,但是一定要非常谨慎,防止数据无限膨胀。原因如下:
首先,对于冷数据,无论大小,都不建议放在 Redis 中。Redis 数据要全部放在内存中,资源宝贵,把冷数据放在其中实在是一种浪费,冷数据放在普通的存储比如关系数据库中就好了。
其次,对于热数据,尤其是写频繁的热数据,如果量比较小,是最适合放到 Redis 中的。比如上面提到的网站总的注册用户数,就是典型的 Redis 用做计数器的例子。再比如论坛最新发表列表,最新报名列表,可以控制数量在几百到一千的规模,也是典型的 redis 做最新列表的使用方式。
另外,对于量比较大的热数据(或者冷热不均数据),使用 Redis 时一定要比较谨慎。这种类型数据很容易引起数据膨胀,导致 Redis 消耗内存巨大,让系统难以承受。薄荷的一个惨痛教训是把用户关注(以及被关注)数据放在 Redis 中,这是一种数据量极大,冷热很不均衡的数据,在几百万的用户级别就占用了近 10 GB左右内存,让 Redis 变得难以应付。应对这种类型的数据,可以用普通存储 + 缓存的方式。
如果用对了地方,比如在小而热的数据情形,Redis 表现很棒,如果用错了地方,Redis 也会带来昂贵的代价,所以使用时务必谨慎。
]]>在 Git 对象模型中讲到,每一次 Git 提交,都会生成一个 commit 对象,这个 commit 对象有一个 SHA1 key,例如 4240c635214402420f4fd907c378f0d01a5b5d8e。简单的说,Git 的分支就是一个引用 reference,这个引用有一个名称(分支的名称),然后指向一个具体的 commit 对象(通过 commit 对象的 key),就是这么简单。
分支的数据保存在那里呢?都在目录 .git/refs
中,通常这个目录下有三个子目录,分别是 heads,tags 和 remotes。
我们可以看到 .git/refs/heads
下有一些文件,每一个文件表示一个分支,文件的名称就是分支的名称,文件的内容就是 commit 对象的 key。
例如,dig-git 项目里:
1 2 3 4 |
|
是不是很简单,知道了这个原理,我们甚至可以绕开 Git 命令,直接通过文件操作生成一个 branch。 例如:
1
|
|
通过上面的命令,在 .git/refs/heads
目录下生成一个 test2 文件,文件内容是一个 commit 对象的 key,这样就生成了一个 test2 分支。
标签 tag 和分支 branch 的本质是一样的,都是一个指向 commit 对象的引用,只不过标签 tag 的文件放在 .git/refs/tags
目录下而已。
如图所示,生成了 3 个对象,一个 commit 对象,一个 tree 对象,一个 blob 对象。图上蓝底是 commit 对象,灰底的是 tree 对象,白底的是 blob 对象,每个对象节点的标题是对象的 key (SHA 摘要)缩略表示。 对于 commit 对象,tree 内容表示这个 commit 对应根目录的 tree 对象,parent 表示父 commit 节点,通常commit 只有一个父节点,也可能没有(首次提交时 parent 为空),也可能有多个(合并节点),commit 对象还保存了 commit message 等信息。 对于 tree 对象,里面的内容包含了文件名,文件对应的 blob 对象的 key,或者是目录名和目录对应 tree 对象的 key。 对于 blob 对象,表示一个实际文件对象的内容,但不包括文件名,文件名是在 tree 对象里存的。
这个图怎么得到的呢?主要是两个命令:
* 通过 git log
命令获取最新 commit 的 key
* 通过 git cat-file -p <object key>
获取 key 对应 object 的内容,根据 object 里的内容,继续探索,就可以访问到所有关联 object.
因为 a.txt 文件已经修改,生成了一个新的 blob 对象,tree 对象和 commit 对象。如图所示,commit 对象之间是有关联的,新提交的 commit 对象的 parent 是上一次提交的 commit 对象。
如图所示,目录是有一个 tree 对象表示的,里面的内容指明了目录包含的文件或子目录。
0c5ca 对应的 commit 对象就是生成的分支 test1 中的。分支在 Git 中是一个非常轻量化的操作,建立分支甚至都不增加新的对象。
def18 就是合并后的 commit 对象。合并生成了一个新的commit ,这个 commit 的 parent 有两个,指向合并的两个原分支对应的 commit 上。
抱歉没有写得很详细,恐怕需要自己参照例子试试一看看,搞明白这些图,也就能搞明白整个 Git 对象模型机制了。
]]>服务器部署示意图如下所示:
端口转发方式要对中转服务器端口转发做设置,在 iptables 中配置例子如下:
1
|
|
端口转发实质上和 capistrano 没有直接关系,是在网络层面对中转服务器和内网实际部署服务器做了设置,让 capistrano 通过一个特定的端口与内网服务器通信,在 capistrano 看起来,内网服务器和中转服务器是一样的,只是端口不同而已。
网关中转方式下,部署客户机首先和中转服务器建立连接,中间服务器再和内网服务器建立连接,部署工作站和内网服务器通过这两个连接通信。网关中转方式通过 capistrano 设置 deploy via gateway 选项完成,不需要对中转服务器做转发设置。
需要注意的是,使用网关中转方式时,capistrano 2.x 和 capistrano 3.x 差异很大,两种并不兼容。
capistrano 2.x 设置很简单,只要设置 gateway 选项就行了,如下所示:
1 2 |
|
capistrano 2.x 的设置在 capistrano 3.x 下不能工作,原因是 cap3 对网络连接做了非常大得重构,原来一些特性使用接口有变化。 capistrano 3.x 设置如下:
1 2 3 4 5 |
|
总结:capistrano 可以通过简单的设置完成向内网服务器部署应用,有端口转发和网关中转两种方式,推荐使用网关中转方式。
]]>汇总计数是系统常见功能,比如网站通常需要统计注册用户数,网站总浏览次数等等。
使用 Redis 提供的基本数据类型就能实现汇总计数器,通过 incr
命令实现增加操作。
比如注册用户数,基本操作命令如下:
1 2 3 4 |
|
通常计数还要按时间统计,比如注册用户数需要按日统计,处理方法比较简单,把日期带入计数器 key 就可以。
还是注册用户计数的例子,基本操作命令如下:
1 2 3 4 5 6 7 |
|
为计数器设置一个 48 小时的过期时间是为了节省计数器占用空间,毕竟 redis 是内存数据库,可以在过期前执行一个任务把计数器存入关系数据库。
速度控制也是 Redis 一种常见的计数用途,比如有一个 API 服务,希望控制每一个 IP 每秒请求数不超过 10 次,可以用 IP 和 时间秒作为 key 设置一个计数器,实现控制,伪代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
有时候需要维护大量计数器,比如每一个论坛主题的查看数,比如每一个用户访问页面次数,因为论坛主题和用户基数可能很大,直接基于论坛主题或用户 ID 生成计数器的话,占用 Redis 资源还是相当可观的,这时可以用 Hash 数据类型压缩所需资源。
比如,对应论坛主题查看计数,可以由模式
1 2 |
|
转换为模式:
1 2 3 4 |
|
总结:利用 Redis 实现计数器,可以简单高效实现各种计数功能。
]]>在 《MongoDB 那些坑》中提到,在前台直接运行建立索引命令的话,将造成整个数据库阻塞,因此索引建议使用 background 的方式建立。但是这也会带来一定的问题,在 2.6 版本之前,在 secondary server 中即使使用 background 方式建立索引,secondary 还是会以 foreground 方式建立索引,它导致 secondary 同样引发数据库阻塞问题。2.6 版本修复了这个 Bug,2.6 版之后使用 background 方式建立索引时,真正转向后台运行了。
为了尽量降低建立索引对 MongoDB Server 的影响,有一种方法是把 MongoDB Server 转换成 standalone 模式后建立。具体做法如下:
首先把 secondary server 停止,在取消 --replSet
参数,并且更改 MongoDB port 之后重新启动 MongoDB,这时候 MongoDB 将进入 standalone 模式;
在 standalone 模式下运行命令 ensureIndex 建立索引,建议使用 foreground 方式运行;
建立索引完毕之后关闭 secondary server 按正常方式启动;
根据上述 1~3 的步骤轮流为 secondary 建立索引,最后把 primary server 临时转换为 secondary server,同样按 1~3 的方法建立索引,再把其转换为 primary server。
这种方式还是比较麻烦的,但可以把建立索引操作对 MongoDB 的影响降到最低,在有些情况下还是值得做的。
]]>薄荷是中国领先移动健康公司,主要产品“薄荷” 是中国最受欢迎的健康健美类 App 之一,用户已近千万,长期位于 App Store 健康健美榜前列,长期位于各大 Android 应用市场健康或生活类应用前列。
我叫谢文威,是薄荷联合创始人,我在 2006 年底和大学同学、上下铺的兄弟马海华一起建立了薄荷。在创立薄荷之前,我曾经做过多年的程序员,DBA,数据挖掘工程师和项目经理,我在薄荷主要负责技 术。薄荷是我第一次创业,走过很多弯路,可谓屡败屡战,所幸我们顽强的生存下来,现在还算有不错的进展。
薄荷一直专注女性健康生活领域,我们十分信奉法国一位哲学家卢梭的一条格言 “在所有的人类知识中,最重要而又最缺乏的是关于人的知识” ,我们相信随着大数据时代,数据化自我时代的到来,基于数据应用改善人的健康和美丽方向一定大有可为。
我们希望找到靠谱各领域工程师 ,一些要求如下:
我们注重能力包括:
薄荷的技术系统正处于从 数百万用户量到数千万量级 演进过程中,这里充满了机会和挑战,如果你是技术大牛,或者渴望快速成长为技术大牛,这里有供你发挥的舞台。如果想未来成为产品经理,或者自己创业,这里也将为你打下坚实的基础。
为了让你写出优雅的代码,我们当然为你提供优雅的工作环境。相比其它互联网公司,薄荷一个特点是女员工比例很高,男女比例是 1 比 2,我们有美女程序员,美女设计师,美女咨询师和美女产品经理 … 因为我们从事的是女性美丽相关的业务,我们的口号是:与美女一起共事,为美女服务!
年薪 10~25 万,上不封顶,取决于你的能力和业绩。目前公司处于快速发展阶段,公司已全面盈利,并且已有顶尖美元风投基金加入(晨兴,SIG,DCM和高通),未来充满无限想象。我们将把你视为伙伴,希望通过一起努力,能够提供远超你预期的回报。
如果您对我们的工作岗位感兴趣,请把您的简历或情况发邮件到vincent(at)boohee.com,最好能充分展示您的能力,谢谢!
坑爹指数:5星(最高5星)
MongoDB的锁机制和一般关系数据库如 MySQL(InnoDB), Oracle 有很大的差异,InnoDB 和 Oracle 能提供行级粒度锁,而 MongoDB 只能提供 库级粒度锁,这意味着当 MongoDB 一个写锁处于占用状态时,其它的读写操作都得干等。
初看起来库级锁在大并发环境下有严重的问题,但是 MongoDB 依然能够保持大并发量和高性能,这是因为 MongoDB 的锁粒度虽然很粗放,但是在锁处理机制和关系数据库锁有很大差异,主要表现在:
通常不出问题不等于没有问题,如果数据操作不当,依然会导致长时间占用写锁,比如下面提到的前台建索引操作,当出现这种情况的时候,整个数据库就处于完全阻塞状态,无法进行任何读写操作,情况十分严重。
解决问题的方法,尽量避免长时间占用写锁操作,如果有一些集合操作实在难以避免,可以考虑把这个集合放到一个单独的 MongoDB 库里,因为 MongoDB 不同库锁是相互隔离的,分离集合可以避免某一个集合操作引发全局阻塞问题。
坑爹指数:3星
上面提到了 MongoDB 库级锁的问题,建索引就是一个容易引起长时间写锁的问题,MongoDB 在前台建索引时需要占用一个写锁(而且不会临时放弃),如果集合的数据量很大,建索引通常要花比较长时间,特别容易引起问题。
解决的方法很简单,MongoDB 提供了两种建索引的访问,一种是 background 方式,不需要长时间占用写锁,另一种是非 background 方式,需要长时间占用锁。使用 background 方式就可以解决问题。 例如,为超大表 posts 建立索引, 千万不用使用
1
|
|
而应该使用
1
|
|
坑爹指数:5星
embed document 是 MongoDB 相比关系数据库差异明显的一个地方,可以在某一个 document 中嵌入其它子 document,这样可以在父子 document 保持在单一 collection 中,检索修改比较方便。
比如薄荷的应用情景中有一个 Group document,用户申请加入 Group 建模为 GroupRequest document,我们最初的时候使用 embed 方式把 GroupRequest 放置到 Group 中。 Ruby 代码如下所示(使用了 Mongoid ORM):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这个使用方式让我们掉到坑里了,差点就爬不出来,它导致有接近两周的时间系统问题,高峰时段常有几分钟的系统卡顿,最严重一次甚至引起 MongoDB 宕机。
仔细分析后,发现某些活跃的 Group 的 group_requests 增加(当有新申请时)和更改(当通过或拒绝用户申请时)异常频繁,而这些操作经常长时间占用写锁,导致整个数据库阻塞。原因是当有增加 group_request 操作时,Group 预分配的空间不够,需要重新分配空间(内存和硬盘都需要),耗时较长,另外 Group 上建的索引很多,移动 Group 位置导致大量索引更新操作也很耗时,综合起来引起了长时间占用锁问题。
解决问题的方法,说起来也简单,就是把 embed 关联更改成的普通外键关联,就是类似关系数据库的做法,这样 group_request 增加或修改都只发生在 GroupRequest 上,简单快速,避免长时间占用写锁问题。当关联对象的数据不固定或者经常发生变化时,一定要避免使用 embed 关联,不然会死的很惨。
坑爹指数:4星
MongoDB 的 Array 字段是比较独特的一个特性,它可以在单个 document 里存储一些简单的一对多关系。
薄荷有一个应用情景使用遇到严重的性能问题,直接上代码如下所示:
1 2 3 4 5 6 |
|
User 中通过一个 Array 类型字段 follower_user_ids 保存用户关注的人的 id,用户关注的人从 10个到 3000 个不等,变化是比较频繁的,和上面 embed 引发的问题类似,频繁的 follower_user_ids 增加修改操作导致大量长时间数据库写锁,从而引发 MongoDB 数据库性能急剧下降。
解决问题的方法:我们把 follower_user_ids 转移到了内存数据库 redis 中,避免了频繁更改 MongoDB 中的 User, 从而彻底解决问题。如果不使用 redis,也可以建立一个 UserFollower 集合,使用外键形式关联。
先列举上面几个坑吧,都是害人不浅的陷阱,使用 MongoDB 过程一定要多加注意,避免掉到坑里。
参考资料: * 1.MongoDB 锁机制详解 * 2.MongoDB 建立索引操作文档
]]>Redis 提供了两种持久化数据机制:
一种是 RDB 机制。这种方式是把当前整个 Redis 内存数据快照写到磁盘上。优点是比较简单,恢复快速。缺点是代价比较昂贵,尤其当数据量很大时,会严重影响到 Redis(照成 Redis 停顿),而且会极度消耗磁盘 IO。
另一种是 AppendOnly 机制。这种方式类似于 log,把数据操作命令全部存入 log 文件。优点是每段时间写入数据不是很大,可以设置很短的写入时间间隔(比如1秒钟)。缺点是 log 文件可能会远大于数据文件,通常会是数据文件大小的 3~5 倍,而且恢复的时间要远大于 RDB 方式的恢复时间。
这两种机制各有优劣,不过它们是可以结合起来使用的。在实际使用过程,通常还结合 Redis 的 slave 功能,做到对 Redis 影响更小,保护更充分。具体的做法是对 Redis 建立 master 和 slave,在 master 上根本不使用任何持久化机制,只在 slave 上建立结合 RDB 和 AppendOnly 的持久化机制。
]]>首先,需要一台内存很大的机器。Redis 是内存数据库,它需要把所有需求全部放在内存中,需要为之装下 12 GB的 Redis 实例,至少需要 12 GB 内存大小的机器,考虑的预留增长空间,一般需要 12 * 1.5 约 18 GB 内存。 另外还有一个考虑的因素是,Redis 进行硬盘数据存储时,fork 进程需要消耗同样大小的内存,因此一个 12GB 的 redis 实例需要 32 GB左右的内存比较合适,这对机器提出了很高的要求,常常难以满足。
然后,Redis 容易成为性能瓶颈。Redis 的并发模型是单进程单线程,它不能充分利用多核 CPU,在请求数很高,或者某一些请求处理比较慢时(比如一些大的数据排序),可能会成为系统的性能瓶颈。有方法可以缓解甚至这个问题,就是建立多个 Redis 实例,通过多个 Redis 连接来实现。
另外,单个巨大的 Redis 实例也会增加数据管理难度,因为这么大的数据量,无论是复制,备份操作都比较慢,容易对线上系统造成冲击。
因此,十分有必要把单个巨大的 Redis 实例分割成多个小的 Redis 实例。
使用 Redis 的复制机制,可以在线平滑处理 Redis 实例分割,几乎不会对系统有很大的影响。
分割的具体操作思路如下:
首先,规划 Redis 分割策略,通常是基于业务划分,比如薄荷伙伴是基于业务分成 timeline, user_relationship, other 3个 Redis 实例。规划好之后,需要根据规划结果对应用程序中 Redis 程序代码做修改,通常是有一个统一的 Redis 链接修改为多个 Redis 连接,不同业务使用不同的连接。
然后,通过 Redis 复制功能建立多个 Redis 副本,让不同 Redis 连接使用不同的 Redis 副本,在 Redis 副本中删除多余的数据。批量删除某个模式的 keys,可以使用下面的 shell 命令:
1
|
|
1
|
|
表示删除 user followers 数据。
Concern 主要用于解决 module mix 的依赖问题,同时简化 module mix 的步骤。 关于依赖问题,必须用一个例子才好说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
上面例子中 Host 为了 include Bar,必须得先 include Bar 依赖的 Foo,这是因为 Bar 在 include Foo 时,只是为 Bar extend method_injected_by_foo 方法,所以 Host 必须显式的 include Foo,才能够 extend method_injected_by_foo 方法。
使用 Concern 后,代码可以简写为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Bar 和 Foo 都 extend ActiveSupport::Concern
后,Host include Bar 已经不需要事先 mix Foo。
这是怎么做到的呢,看看 Concern 的代码,其实代码很少:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
关键部分是 append_features
方法,通过阅读 ruby 的文档和源码得知,ruby 在 include 一个 module 时,实际会触发两个方法,一个是 append_features,进行实际的 mixing 操作,包括增加常量,方法和变量到模块中,另外一个是 included 方法,也就是我们常用来作为 include 钩子的方法,默认的 included 是一个空方法,我们通过重载它使钩子起作用。
从代码可知,当一个模块 extend ActiveSupport::Concern
时,将产生 3 个影响:
变量为 @_dependencies,其值为空数组,表示依赖的模块,将在 append_features 中用到;
重载后行为有了很大变化,它的处理分两种情况:
一种是当它被一个有 @dependencies 实例变量的模块被 include 时,直接把自身加到 @dependencies 中,
比如当 Bar include Foo 时,将触发 Foo 的 append_features(base) 方法,此时 base 是 Bar,self 是 Foo,由于 Bar 已经 extend ActiveSupport::Concern
,Bar 的 @dependencies 有定义,所以直接把 Foo 加到 Bar 的 @dependencies 中,然后直接返回,没有立即执行 mixing 操作。
当 Host include Bar 时,将触发 Bar 的 append_features(base) 方法,此时
base 是 Host,self 是 Bar,Host 没有 extend ActiveSupport::Concern
,所以 Host 的 @dependencies 无定义,将执行下面的分支,首先 include Foo(通过 Bar 的 @dependencies 获得 ),然后 include Bar (通过 super),然后是后续操作。
included 的动作比较简单,如果以没有参数形式调用,将把 block 存放到 @included_block 变量中,@included_block 的 block 将在 append_features 方法中使用。
通过施加的这 3 个影响,ActiveSupport::Concern 完成了它的全部功能,非常简洁精练。
从中学到的东西,包括:
module 中 append_features 和 included 方法时 include 的核心;
可以通过覆盖这些方法改变操作的行为;
可以定义模块实例变量存储属于该模块的一些数据;