程序人生

写优雅的程序,做优雅的人

怎样才是 ruby 中捕获异常的正确姿势?

| Comments

在开发和使用微服务间通信组件 SneakersPacker 过程中遇到异常处理的一些问题,通过解决这些问题让我对 ruby 中捕获异常和定义异常有新的认识。

当初遇到的问题是这样的:薄荷的一个子系统 status 中原本使用 http api call 获取 record 子系统的一些数据,应用 SneakersPacker 之后,把代码改成了 rpc call。刚开始的时候 SneakersPacker 还有一些问题,为了防止 rpc call 出状况,在代码中提供了一层保护机制,也就是当 SneakersPacker.remote_call 触发异常之后,通过 http api call 获取数据。代码很简单,如下所示:

1
2
3
4
5
  begin
    res = SneakersPacker.remote_call "record.get_simple_profile", params_data
  rescue
    res = HTTPProxy::Record.get "api/v1/users/simple_profile", params_data
  end

但是实际运行结果很让人诧异,remote_call 的确触发了异常,但是代码中的 rescue 并没有捕获到异常,而是直接导致程序 500 错误,大量异常记录在日志和监控系统中,让人百思不得其解。在那之前我以为孤立的 rescue 会捕获全部异常的,仔细研究网络上的一些文档后,发现并不是这样,rescue 其实只捕获 StandardError 类型的异常。再去查阅 SneakersPacker 的代码,发现原来报了很多的异常 RemoteCallTimeoutError 是这样定义的:

1
  class RemoteCallTimeoutError < Exception; end

RemoteCallTimeoutError 选择父类 Exception 是错误的,应该选择 StandardError,改成下面之后 RemoteCallTimeoutError 象期望的一样被捕获了。

1
  class RemoteCallTimeoutError < StandardError; end

为了说明 rescue 这个关键字的行为,我们看下面几个例子:

1
2
3
4
5
6
# 代码1
begin
  success_job
rescue
  fail_job
end
1
2
3
4
5
6
7
# 代码2
begin
  success_job
rescue => e
  fail_job
  puts e is #{e.inspect}"
end
1
2
3
4
5
6
7
# 代码3
begin
  success_job
rescue StandardError => e
  fail_job
  puts e is #{e.inspect}"
end
1
2
3
4
5
6
7
# 代码4
begin
  success_job
rescue Exception => e
  fail_job
  puts e is #{e.inspect}"
end

代码1,代码2和代码3的 rescue 都只捕获 StandardError 类别的异常,而代码4明显不同,代码4会捕获所有的异常。

一些直接继承自 Exception 而不是 StandardError 的异常如下,都是系统层级严重错误,不应该由应用捕获。

1
2
3
4
5
6
7
8
9
10
11
12
SystemStackError
NoMemoryError
SecurityError
ScriptError
  NotImplementedError
  LoadError
    Gem::LoadError
  SyntaxError
SignalException
  Interrupt
SystemExit
  Gem::SystemExitException

综上所述,总结一些 ruby 中捕获异常的原则(正确姿势)

  1. 最好的 rescue 姿势,捕获明确指定的异常,rescue OneError => e
  2. 次好的 rescue 姿势,捕获 StandardError,通过 rescue 或 rescue => e
  3. 千万千万不要 rescue Exception => e,除非真的知道自己在干嘛
  4. 自己定义的异常应该继承自 StandardError ,而不是 Exception。

end.

缓存可能让你的应用更慢 - 缓存使用的 N+1 问题

| Comments

缓存是提升系统性能非常有效的手段,常常起到立竿见影的效果,但是有时不恰当的使用不但起不到优化效果,反而可能让系统更慢。下面总结缓存使用过程中常见的一些陷阱。

大家应该比较熟悉数据库查询时的 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
n = 10000
n.times {|i| Rails.cache.write "user:#{i}:counter", i * i }

def read_by_batch(total, batch_size)
  array = []
  total.times do |i|
    if i + 1 % batch_size == 0
      Rails.cache.read_multi *array
      array = []
    else
      array << "user:#{i}:counter"
    end
  end
end

Benchmark.bm do |x|
  x.report "  1" do
    n.times do |i|
      Rails.cache.read "user:#{i}:counter"
    end
  end

  x.report " 10" do
    read_by_batch n, 10
  end

  x.report " 30" do
    read_by_batch n, 30
  end

  x.report " 50" do
    read_by_batch n, 50
  end

  x.report "100" do
    read_by_batch n, 100
  end
end

执行结果如下所示:

1
2
3
4
5
6
   user     system      total        real
  1  0.910000   0.210000   1.120000 (  1.316800)
 10  0.010000   0.000000   0.010000 (  0.013985)
 30  0.010000   0.000000   0.010000 (  0.009109)
 50  0.010000   0.000000   0.010000 (  0.009200)
100  0.040000   0.010000   0.050000 (  0.044768)

从结果中可以看到,分批读取(每次 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
  keys = user_ids.map { |user_id| "user:#{user_id}" }

  user_hash = Rails.cache.fetch_multi(*keys) do |key|
    User.find_by_id key.gsub('user:', '')
  end

总结:缓存虽然很快,但它毕竟也是一次 IO 操作,同样需要消耗一定时间,如果某一次特别大量读写缓存,很可能会以前性能问题,通过批量读取方式是解决该问题的有效手段。

ruby 中的 4 种相等性判断方法

| Comments

很早就知道 ruby 有 4 种相等性判断方法,分别是:“==”,“===”,“equal?” 和 “eql?”,平常程序中都有使用,但是感觉对其缺乏深入理解,今天读 rails 部分源码的时候拿捏不定其中一个判断的意思,于是趁机深入研究了一番,总算觉得比较清楚了,今天做一下笔记,以作备忘。

“==” 最常见的相等性判断

“==” 使用最频繁,它通常用于对象的值相等性(语义相等)判断,在 Object 的方法定义中,“==” 比较两个对象的 object_id 是否一致,通常子类都会重写覆盖这个方法,通过比较内部值来判断对象是否相等。

比如 ActiveRecord::Base 对 “==” 的定义

1
2
3
4
5
6
  def ==(comparison_object)
    super ||
      comparison_object.instance_of?(self.class) &&
      id.present? &&
      comparison_object.id == id
  end

通过 model 的 id 属性比较两个 ActiveRecord::Base 实例是否相等。

“===” 用于 case 语句的相容判断

“===” 主要用于 case 语句中对象的相容比较,看代码比较容易理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def what_is(obj)
  case obj
    when /abc/
      puts "include abc"
    when 3..5
      puts "in 3..5"
    when Symbol
      puts "It is a symbol"
    else
      puts "unkonwn"
  end
end

what_is("abcde") # =>  "include abc"
what_is(4)       # =>  "in 3..5"
what_is(:a)      # =>  "It is a symbol"
what_is(100)     # =>  "unknown"

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?” 相同对象判断

“equal?” 其实是最简单的,但是也是最容易让人搞混的判断。说它简单是因为这个方法的语义是比较两个对象是否相同(是否有相同的 object_id),Object 的方法适用所有对象,不应该对其重写覆盖。说它容易让人搞混,是因为 ruby 和 java 中 “==” 和 “equal?” 方法的语义正好是相反的,ruby 中 “equal?” 表示对象引用相同,而 java 表示对象值相同。

“eql?” 对象 hash 值判断

eql? 用于对象 hash 值判断,如果两个对象的 hash 值相等,就返回 true,否则返回 false。Object 的定义里,“eql?” 和 “==” 是等价的。通常可以把 “eql?” 看作比 “==” 更严格的相等,比如:

1
2
1 == 1.0     #=> true
1.eql? 1.0   #=> false

重构系统广播功能

| Comments

以前薄荷伙伴子系统的广播功能使用 redis 存储数据,随着时间推移数据积累,消耗内存十分严重,属于典型的 redis 误用。现在进行大幅重构,更改为 cache + storage 的存储方式,取消了 redis 存储。重构后,消耗资源大幅下降,性能上还有所改进。

先说说广播功能特性。广播功能主要用于薄荷向全体用法传递的消息(包括各种广告,告示和提醒等),当系统管理员在后台发出一条广播后,用户 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 存储,大幅减少内存使用,算法调整让常见操作速度更快,效果很棒。

Rails 中 mattr_accessor 一处文档错误

| Comments

发现错误

最近写一个 gem 的时候偶然接触到 Rails ActiveSupport 扩展 module 的 mattr_accessor 系列方法,包括 mattr_accessor、mattr_reader 和 mattr_writer。 记得以前探索 Rails 源代码的时候经常遇到 mattr_accessor 方法,当时并没有细究,这次碰巧要自己用到,所以仔细研究了其文档和实现源码,居然发现文档描述有明显的错误。

Rails 的官方文档中提到,mattr_accessor 用于为类属性定义类和实例对象两者的访问器,然后还提供一段示例代码演示其用法。

1
2
mattr_accessor(*syms, &blk) public
Defines both class and instance accessors for class attributes.

演示代码如下:

1
2
3
4
5
6
7
8
9
10
11
module HairColors
  mattr_accessor :hair_colors
end

class Person
  include HairColors
end

Person.hair_colors = [:brown, :black, :blonde, :red]
Person.hair_colors     # => [:brown, :black, :blonde, :red]
Person.new.hair_colors # => [:brown, :black, :blonde, :red]

当我运行该代码的时候,发现无法运行,报错在 Person.hair_colors 处,信息如下:

1
2
3
[5] pry(main)> Person.hair_colors = [:brown, :black, :blonde, :red]
NoMethodError: undefined method `hair_colors=' for Person:Class
from (pry):7:in `__pry__'

刚开始还有点不相信,分别在 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
  # 此处忽略注释
  def mattr_writer(*syms)
    options = syms.extract_options!
    syms.each do |sym|
      raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
      class_eval(<<-EOS, __FILE__, __LINE__ + 1)
        @@#{sym} = nil unless defined? @@#{sym}

        def self.#{sym}=(obj)
          @@#{sym} = obj
        end
      EOS

      unless options[:instance_writer] == false || options[:instance_accessor] == false
        class_eval(<<-EOS, __FILE__, __LINE__ + 1)
          def #{sym}=(obj)
            @@#{sym} = obj
          end
        EOS
      end
      send("#{sym}=", yield) if block_given?
    end
  end

  # 此处忽略注释
  def mattr_accessor(*syms, &blk)
    mattr_reader(*syms, &blk)
    mattr_writer(*syms, &blk)
  end

从中可以看到,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
  module Foo
    def method1
      puts "method1 in Foo"
    end

    def self.method2
      puts "method2 in Foo as class method"
    end
  end

  class Bar
    include Foo
  end

  Bar.new.method1  # => "method1 in Foo"

  Foo.method2 # => "method2 in Foo as class method"

  Bar.method2 # => NoMethodError: undefined method `method2' for #<Bar:0x007fdb0a121488>

原来 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
module HairColors
  extend ActiveSupport::Concern
  included do
    mattr_accessor :hair_colors
  end
end

class Person
  include HairColors
end

Person.hair_colors = [:brown, :black, :blonde, :red]
Person.hair_colors     # => [:brown, :black, :blonde, :red]
Person.new.hair_colors # => [:brown, :black, :blonde, :red]
HairColors.hair_colors # => undefined method `hair_colors' for HairColors:Module

它的一个问题是 HairColors.hair_colors 不可用了,但我发现 Rails 中大多是使用这种手法处理的,这种情况下估计不怎么要直接用到 HairColors.hair_colors 吧。

总结

Rails 官方文档中关于 mattr_accessor 的描述的确有问题,示例代码不能正确运行,而且还会误导使用者,通过仔细探索 Rails 的源代码,找到了问题的源头。我已经修正文档的错误,提交了 pull request,希望能够被接受。

使用 Redis 进行唯一计数的 3 种方法

| Comments

唯一计数是网站系统中十分常见的一个功能特性,例如网站需要统计每天访问的人数 unique visitor (也就是 UV)。计数问题很常见,但解决起来可能十分复杂:一是需要计数的量可能很大,比如大型的站点每天有数百万的人访问,数据量相当大;二是通常还希望扩展计数的维度,比如除了需要每天的 UV,还想知道每周或每月的 UV,这样导致计算十分复杂。

在关系数据库存储的系统里,实现唯一计数的方法就是 select count(distinct <item_id>),它十分简单,但是如果数据量很大,这个语句执行是很慢的。用关系数据库另外一个问题是插入数据性能也不高。

Redis 解决这类计数问题得心应手,相比关系数据库速度更快,消耗资源更少,甚至提供了 3 种不同的方法。

  • 1.基于 set

Redis 的 set 用于保存唯一的数据集合,通过它可以快速判断某一个元素是否存在于集合中,也可以快速计算某一个集合的元素个数,另外和可以合并集合到一个新的集合中。涉及的命令如下:

1
2
3
SISMEMBER key member  # 判断 member 是否存在
SADD key member  # 往集合中加入 member
SCARD key   # 获取集合元素个数 

基于 set 的方法简单有效,计数精确,适用面广,易于理解,它的缺点是消耗资源比较大(当然比起关系数据库是少很多的),如果元素个数很大(比如上亿的计数),消耗内存很恐怖。

  • 2.基于 bit

Redis 的 bit 可以用于实现比 set 内存高度压缩的计数,它通过一个 bit 1 或 0 来存储某个元素是否存在信息。例如网站唯一访客计数,可以把 user_id 作为 bit 的偏移量 offset,设置为 1 表示有访问,使用 1 MB的空间就可以存放 800 多万用户的一天访问计数情况。涉及的命令如下:

1
2
3
4
SETBIT key offset value  # 设置位信息
GETBIT key offset        # 获取位信息
BITCOUNT key [start end] # 计数
BITOP operation destkey key [key ...]  # 位图合并 

基于 bit 的方法比起 set 空间消耗小得多,但是它要求元素能否简单映射为位偏移,适用面窄了不少,另外它消耗的空间取决于最大偏移量,和计数值无关,如果最大偏移量很大,消耗内存也相当可观。

  • 3.基于 HyperLogLog

实现超大数据量精确的唯一计数都是比较困难的,但是如果只是近似的话,计算科学里有很多高效的算法,其中 HyperLogLog Counting 就是其中非常著名的算法,它可以仅仅使用 12 k左右的内存,实现上亿的唯一计数,而且误差控制在百分之一左右。涉及的命令如下:

1
2
PFADD key element [element ...]  # 加入元素
PFCOUNT key [key ...]   # 计数

这种计数方法真的很神奇,我也没有彻底弄明白,有兴趣可以深入研究相关文章。

redis 提供的这三种唯一计数方式各有优劣,可以充分满足不同情况下的计数要求。

Rails 中 MySQL 分区表使用的一个注意事项

| Comments

MySQL 的分区表是一种简单有效的处理极大数据表的特性,通过它可以使应用程序几乎很少改动就能达成对极大数据表的高效处理,但由于 Rails ActiveRecord 设计上一些惯例,可能导致一些数据处理不能利用分区表特性,反而变得很慢,在使用分区表过程中一定要多加注意。

下面以一个例子来说明。在 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
CREATE TABLE `diet_items` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `schedule_id` int(11) NOT NULL,
  `meals_order` int(11) NOT NULL,
  `food_id` int(11) DEFAULT NULL,
  ....
  KEY id (`id`),
  UNIQUE KEY `index_diet_items_on_schedule_id_and_id` (`schedule_id`,`id`)
)
PARTITION BY HASH (schedule_id)
PARTITIONS 60;

分表之后,所有查询 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 条件,从而避免了扫描全部的分区表,性能提升立竿见影。

如何解决 Rails 中同时修改冲突

| Comments

Rails 应用程序中操作冲突是一个常见问题,Rails 提供了简单有效的解决方法。

举一个实际的例子:我们的系统里有一个商店模块,商店中重要的一块是对产品信息的管理,比如运营人员常常会编辑产品的信息,包括产品标题,营销口号和价格等等。因为修改十分频繁,碰巧同时编辑提交修改的话,就会偶尔遇到修改丢失的问题,运营人员 A 修改产品标题,运营人员 B 修改价格,A 和 B 提交修改都提示修改成功,但是结果上只是 A 的修改结果生效,B 的修改被 A 的修改冲掉了。

仔细研究原因,发现是因为修改功能缺少操作冲突机制,而修改操作同时发生导致了问题。 如下图所示,A 和 B 同时从数据库中查询数据,在 web 页面中修改同样的数据,提交保存时是以 web 页面中提交的数据为准,从而导致 A 的修改把 B 的修改给覆盖了。

A和B修改冲突

Rails 的 乐观锁Optimistic Locking 是解决这个问题的有力工具,它的原理是在数据库表中增加一个字段(默认是 lock_version,可配置)记录数据的版本号,每个提交的修改都带上这个版本号,在真正 update 修改数据之前,先判断提交的 lock_version 数据和数据库中的是否一致,如果不一致,则认为发生数据冲突,将抛出 ActiveRecord::StaleObjectError 异常,这样程序就可以捕获这个异常,提醒用户发生了冲突,由用户去协调解决冲突。

相关示例代码如下所示:

1
2
3
4
5
6
7
8
9
# migration: add lock_version to products
add_column :products, :lock_version, :integer, defalut: 0

# update product with StaleObjectError checking
begin
  product.update(params[:product])
rescue ActiveRecord::StaleObjectError
  render 'confilct'
end

http 抓包分析工具 pproxy

| Comments

引言

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 如下所示:

Fiddler UI

pproxy UI 如下所示:

pproxy UI

使用

pproxy 使用比较简单,具体可以参考其使用说明 pproxy。 难得的是他的作者是中国人,所以说明都是中文的。

总结

pproxy 是一个开源的轻巧的 http 抓包分析工具,尤其适合 API http 请求分析,完全可以替换昂贵的收费工具,同时也可以探索其源码学习 GO 语言,学习 http 协议和分析方法。

避免误用 Redis

| Comments

Redis 是目前 NoSQL 领域的当红炸子鸡,它象一把瑞士军刀,小巧、锋利、实用,特别适合解决一些使用传统关系数据库难以解决的问题。但是 Redis 不是银弹,有很多适合它解决的问题,但是也有很多并不适合它解决的问题。另外,Redis 作为内存数据库,如果用在不适合的场合,对内存的消耗是很可观的,甚至会让系统难以承受。

我们可以对系统存储使用的数据以两种角度分类,一种是按数据的大小划分,分成大数据和小数据,另一种是按数据的冷热程度划分,分成冷数据和热数据,热数据是指读或写比较频繁的数据,反之则是冷数据。

可以举一些具体的例子来说明数据的大小和冷热属性。比如网站总的注册用户数,这明显是一个小而热的数据,小是因为这个数据只有一个值,热是因为注册用户数随时间变化很频繁。再比如,用户最新访问时间数据,这是一个量比较大,冷热不均的数据,大是数据的粒度是用户级别,每一个用户都有数据,如果有一千万用户,就意味着有一千万的数据,冷热不均是因为活跃用户的最新访问时间变化很频繁,但是可能有很大一部非活跃用户访问时间长时间不会发生变化。

大体而言,Redis 最适合处理的是小而热,而且是写频繁,或者读写都比较频繁的热数据。对于大而热的数据,如果其它方式很难解决问题,也可以考虑使用 Redis 解决,但是一定要非常谨慎,防止数据无限膨胀。原因如下:

首先,对于冷数据,无论大小,都不建议放在 Redis 中。Redis 数据要全部放在内存中,资源宝贵,把冷数据放在其中实在是一种浪费,冷数据放在普通的存储比如关系数据库中就好了。

其次,对于热数据,尤其是写频繁的热数据,如果量比较小,是最适合放到 Redis 中的。比如上面提到的网站总的注册用户数,就是典型的 Redis 用做计数器的例子。再比如论坛最新发表列表,最新报名列表,可以控制数量在几百到一千的规模,也是典型的 redis 做最新列表的使用方式。

另外,对于量比较大的热数据(或者冷热不均数据),使用 Redis 时一定要比较谨慎。这种类型数据很容易引起数据膨胀,导致 Redis 消耗内存巨大,让系统难以承受。薄荷的一个惨痛教训是把用户关注(以及被关注)数据放在 Redis 中,这是一种数据量极大,冷热很不均衡的数据,在几百万的用户级别就占用了近 10 GB左右内存,让 Redis 变得难以应付。应对这种类型的数据,可以用普通存储 + 缓存的方式。

如果用对了地方,比如在小而热的数据情形,Redis 表现很棒,如果用错了地方,Redis 也会带来昂贵的代价,所以使用时务必谨慎。