网络寻租

Programmer, Gamer, Hacker

Ruby内存泄漏调试方法

| Comments

只要是跑起来的服务器程序,都有可能遇到内存泄露问题,或大或小。 可以用简单的看门狗方法,内存增加到一定程度就重启; 但是重启只是隐藏问题,遇到严重的内存泄露,只能正视问题,想办法找到内存泄露的源点。 这里我整理了一下ruby语言的内存泄露查找方法,欢迎反馈。

基本思路是这样:等待内存泄露到一定程度,进程的内存里面会大量充斥着没有被释放的对象, 随机获取内存中的数据,就可以知道是什么对象泄露了,从而定位问题。

我们先开始一个实验。创建文件leak.rb:

1
2
3
4
5
s = []
1000000.times { s << "hello" }
while true
  sleep(1)
end

这个文件生成了太多没有释放的字符串,并且一直处于循环等待状态。

实际的应用程序代码比较多,不是那么明显就能发现内存泄露的代码,需要通过调试寻找线索。

首先我们要编译一个带有debug信息的ruby版本。参数加上-O0 -gO0是为了防止优化掉一些调试的符号表信息。 如果你用的是rvm,可以采用下面的脚本:

1
2
3
4
5
6
7
8
9
10
# 清空rvm编译环境参数
unset rvm_configure_env

# 编译一个单独的ruby版本,需要花费一定时间
rvm install 2.0.0-debug --debug -j 3 -- --enable-shared optflags="-O0 -ggdb" debugflags="-ggdb3"

# 采用并且检查设置
rvm use 2.0.0-debug
ruby -rrbconfig -e 'puts RbConfig::CONFIG["optflags"]'
# 结果应该带有:-O0 -ggdb

然后我们在这个环境中执行程序,实际程序不要忘记安装支持的gem:

1
ruby leak.rb

之后,我们需要调试这个程序。如果你在linux下面,请使用gdb, 如果在OSX下面,请使用编译ruby工具链中的debug工具, 在我的机器OSX上面是用clang来编译的,所以我采用的是lldb, 下面的例子以我的机器为准,gdb的命令其实也是一样的。

另外开一个终端,启动lldb,然后连接上跑起来的进程:

1
(lldb) attach 77226

上面改成你用ps aux|grep ruby找到的进程号。

attach做的事情就是在你调试进程里面开一个线程,这样就能够获得所有的内存信息, 同时也不影响程序正常运行(只要你保证线程安全)。

然后我们要知道进程内存消耗状况。在调试环境里面,我们可以执行C语言的函数, 其中rb_eval_string可以用来直接执行ruby代码。 我们首先需要做的是用ObjectSpace来遍历和列出所有ruby对象:

1
2
3
4
p rb_eval_string("GC.start")
p rb_eval_string("$db_objs = Hash.new 0")
p rb_eval_string("ObjectSpace.each_object {|o| $db_objs[o.class] += 1}")
p rb_eval_string("puts $db_objs.to_s")

列出来之前先要垃圾处理一下。因为ruby有解释器全局锁,执行上面的代码应该不会造成线程安全问题。 回到执行ruby leak.rb的终端,可以看到打印出来的结果。 如果是实际运行的程序,你可能需要开启一个文件,把结果打印进去,而不是打印到标准输出里面:

1
p rb_eval_string("File.write('sys.log', $db_objs.to_s)")

结果如下:

1
2
3
4
5
6
{
  String=>1005019,
  RubyVM::InstructionSequence=>577,
  Hash=>28,
  ...
}

发现String对象出奇地多,应该是内存泄露的主要组成部分。我们采样一下数据,看看是什么样的字符串:

1
2
3
p rb_eval_string("$db_strs = []")
p rb_eval_string("ObjectSpace.each_object.each_with_index {|o, i| $db_strs << o if o.class == String and i%1000==0}")
p rb_eval_string("puts $db_strs.sample(10).to_s")

结果:

1
["hello", "hello", "hello", "hello", "hello", "hello", "hello", "hello", "hello", "hello"]

根据这个信息,我们回到源代码里面,找到对应的部分,思考为什么没有释放这个字符串,从而解决内存泄露的问题。

我们甚至可以利用rb_eval_string来动态修改代码和解决bug,不过在这个例子里面没有办法删除掉造成内存泄露的s对象。如果你发现有方法,还请告诉我。

但是如果内存泄露发生在C语言部分,应该如何发现?这个留到下次再介绍。 还有就是如何调试生产环境的进程,这个也请等我研究清楚之后再分享给大家。

引用资料:

为什么我要每天写日记

| Comments

我们年轻的时候,可能是发自内心的写作冲动,亦或是老师的布置,或长或短,都写过一段时间的日记。 那个时候的日记,更像是心情日记,想到什么就写到什么,记录下青春期那复杂混乱又绚丽的内心世界。

很少有人能够把这个习惯保留到现在。坚持了一段时间,但翻动日记的频率,随着热情渐渐消退了。 不过,也会有新的际遇让人用新的方式重拾它。

我是2012年8月5日开始重新写日记的,基本上每天都会记录,一直坚持到现在一年有余。 在写日记之前很久,我就养成了每天记录工作和学习时间的习惯, 而开始记录日记,则是对工作学习记录的一个很好的补充。

我日记主要记以下内容:

首先,整理和记录今天发生的事。人记忆力有限,昨天的事情已经不是很清楚, 上个星期发生的事就算很重要,也模模糊糊,更别说去年的某个日期发生了什么事情了。 如果不赶快记录下发生了什么,以后想要回溯的话,基本无迹可寻。

写法基本上流水账即可,比如上午做了什么。遇到什么人,发生什么事情之类。 重要的不是文笔上的修炼,而是发生事情的梳理。

然后对今天的事情做出反思。有了前一个步骤的回顾,大致回忆想一天发生的事情了。 这个时候要思考:今天有什么是做得比较好的,什么地方可以改进,学到了什么东西。 每天进行反思是自我提升的一个重要手段。 如果你有伴侣,可以两个人交流一下,效果会更好。

最后给明天做计划。一般来说,第二天早上一起来就要开始忙了,晚上是整理明天该做什么事情的好时机。 可以回顾一下自己的阶段性目标,然后按照重要性紧急性来安排计划。

这几个项目都非常重要,因此是必须要做的事情,用日记的方式再合适不过了。

安排写日记的时间,我觉得可以在晚上忙完所有事情闲下来,但离睡觉有一段距离的时候。 这样有充足的时间来记录和整理,又不会困和累到写不动, 并且做完之后大脑活跃,可以去学习或者做其他事情。

我日记的记录方式,是采用emacs的org-mode,很技术宅的东西。 我建议大家用evernote。这种整理的笔记要用云服务来备份,并且加上索引方便以后搜索。

除了每天写日记以外,同样的,也要写周记,月记,年记。 日记的粒度有点小,还需要更大的时间粒度来做汇总和长期的规划。

如何让进程后台执行

| Comments

很多时候我们需要让一个进程在后台执行。

终端下面执行命令

比如我们要执行cmd,可以简单地在命令行运行cmd &

如果我们希望重定向输出到其他的地方,可以用:

cmd 2>&1 > run.log &

里面 2>&1 是把stderr重导向到stdout,然后> run.logstdout重导向到文件。

但是如果关闭当前终端,后台跑的进程还是会被退出掉,这个时候需要工具

daemon cmd

如果你是用ruby写的程序,也可以有一个Gem来帮你完成这个工作。

原理

后台化主要做的事情就是让生成的后台进程不被你的命令行以及调用者影响到, 具体需要做的步骤如下:

  • 首先是第一遍fork(),这样的目的是让新的进程不成为process group leader,后一步操作setsid()成功执行依赖这点。
  • setsid()让新的进程成为session group leader,这样发送给父进程process group的信号就不会影响到子进程。
  • 第二遍fork(),这样生成的进程不会是session group leader,不会重开终端(PGID和PID不同了)。
  • chdir("/"),把进程默认目录移动到root,这样进程可以和文件系统无关,当然也可以移动到后台进程管理的牡蛎里面去。
  • umask(0)限制后台进程的权限,主要是安全考虑,这一步可选。
  • close()文件描述符0,1,2,其实就是标准输入输出和错误,因为它们是从父进程中继承过来的。你也可以重新导向标准输出和错误用来做日志记录,重新导向标准输入用来做进程控制。

示例代码如下:

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
module Daemon
  extend self

  def run
    5.times.each do
      sleep 1
      puts Time.now
    end
  end

  def start
    Process.fork do
      Process.setsid
      Process.fork do
        Dir.chdir(File.expand_path("."))
        File.umask(0)

        $stdin = File.open('/dev/null')
        $stdout = File.open('s.log', 'w+')
        $stderr = File.open('/dev/null')
        self.run

        exit
      end
      exit
    end
  end
end

Daemon.start

保存成daemon.rb,然后执行ruby daemon.rb

我在上面的测试代码中,如果去掉setsid和第二遍fork,执行代码,关闭当前的终端,进程还是在后台正常执行。 所以我不是很清楚它们的具体影响,欢迎有知道的人帮忙指导一下。

更新疑问:

有朋友回复,setsid用来设置成新的session和process group,这样就不会被来自父进程的killpg等操作影响, 还有就是,第二遍fork是让进程不再是process group leader,这样不能重新获得一个终端。这个操作是可选的。

引用材料:

最简单的搜索引擎

| Comments

有一堆文本,我们需要能够根据一个或者好几个词语,搜索到含有这些词语的文本。 我们可以简单粗暴地用find .|xargs grep word的方式来这样做。 但是每次搜索都需要遍历全部文本,只是搜索一次可以承受,但是重复搜索的话就不能承受了。

处理这种任务,我们用到搜索引擎。可以大如google,也可以小到嵌入在浏览器里面的文本搜索功能。

Boolean model

一个简单的模型,叫Boolean model, 思路是这样的。

我们把要搜索的全体文本叫做corpus,一份文本叫做document,文本可以拆分成一个个的关键字,叫做terms。

为了能够搜索文本,我们需要对文本预处理,把document里面的字一个个拆出来,预处理一番,形成terms。

如果用布尔值来标示一个document是否存在一个terms,我们可以做出来一个矩阵:

1
2
3
4
5
        term1   term2   term3   ...
Document1     X       .       X
Document2     .       X       .
Document3     X       .       X
...

利用这个矩阵进行搜索,只要进行查表工作就可以了。

因为terms的数量远远大于Document数量和长度,这个矩阵是稀疏的。为了节省空间,矩阵可以采用list表示。 我们给document标示上ID:D1, D2, …, 还有,我们也需要记录一下term出现在所有document里面的次数(document frequency),列在term名字后面, 缓存用来进行后续的运算。

这样:

1
2
3
term1(2) -> D1, D3
term2(1) -> D2
term3(2) -> D1, D3

然后是具体的搜索工作。

搜索的语法我们可以支持AND和OR,比如term1 AND term2, 处理的方法就是获取上面term1和term2的document列表,然后求交集即可。 列表可以先排好序,这样交集操作消耗的时间,就和这两个列表元素的和相关。

搜索语句可以归并成(term or term) and term and ...这样的形式, 这样搜索语法的执行过程,就是每次取两个document列表,进行集合合并操作,一直到最后只剩下一个结果集合。

这个操作的性能,取决于所有操作中,每次集合操作中两个集合的大小, 而操作的顺序是可以变化的, 一种启发式算法优化就是按照集合大小升序来做交集操作,这样尽量让每次生成的新集合小一些。 这就是为什么要在前面记录document frequency。

总体的思路就是这样。实际实现的话,会有很多东西需要考虑的:

  • 把document拆分成terms,需要处理英文文本里面时态变化,缩写,同义词合并,中文文本就要处理分词的问题。
  • 要能够根据搜索结果进行排序,比如根据term在文本中出现的词频,term在文本中出现的顺序和区域等。
  • 数据结构的保存方法,如何支持动态增加文本和更新文本。
  • 搜索语法需要支持更多的语法,两个terms间距搜索,模糊查询。

更多关于这些问题的处理,还是去看教科书比较合适: Introduction to Information Retrieval

牛逼或怂逼

| Comments

不知啥时开始,举目皆是英雄。

他们拥有牛逼哄哄的能力,手握不敢想象的资源,创下改变世界的伟业, 地球跟随他们转动,历史就是他们的故事。

这世上又还有普通人。英雄多了,普通人的生活也被恩泽, 英雄开创出来的基业,里面一个个坑还要普通人去填的。 但凡有坑可占,普通人亦可吃得饱,穿得暖,有地方住,还有点小玩头。

英雄多了,普通人亦不值钱了, 那些坑已然都挖好,谁人来站没啥区别,换换也就铲子的事儿,犯不着滴多点油水。 真要耗油水抢地盘的地方不是蓝海就是红海,战刺刀的不是海盗就是海军陆战队, 要和他们争,也非得上英雄不可,没超能力的还是靠边站吧。

坑里面油水再少,也算过得下去,一家几口挤个公寓,交齐了月租也能其乐融融, 本朝政府虽说变扭难养,至少也留口气给你呼吸或者说话,比起上世纪的伟人时代好过活许多, 外面还有国内外的英雄们开疆扩土,坐着也能等着日子一天天变好。

但人本性还是矫情,自己小日子过得不行,还要发贱探头去外面比,一比就完了。 举目皆是英雄人英雄事,相较之下生活就粗鄙不堪,连带身边婆娘或汉子也俗气了,着实难过。 难过也就罢了,落下一病,名为不甘心, 发起病来容易抛妻弃子背井离乡去当英雄,结果底子弱没有超能力, 英雄当不成,成了狗熊,连自己的一某三分地都丢了。 只留得脑壳后面印下三个字:不后悔。

所以咋办? 要么牛逼,要么怂逼,不上不下还是认怂吧, 没有超能力就不要戴面罩当英雄,不然脸被打得亲妈都认不出来。

Rails定制报错页面

| Comments

需求

rails的默认报错处理,是返回public/400.htmlpublic/500.html的内容。 我们一般期望能够定制化它,根据用户登录或者状况返回一个动态渲染的页面。

我们一般希望能够定制:500服务器错误,400地址不存在。

解决方案

在你的app/controllers/application_controller.rbApplicationController里面用rescue_from捕捉他们,并且只在生产环境这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ApplicationController < ActionController::Base
  ...
  unless Rails.application.config.consider_all_requests_local
    rescue_from Exception, with: lambda { |exception|
      render_error 500, exception
      ExceptionNotifier::Notifier.exception_notification(request.env, exception).deliver
      true
    }
    rescue_from ActionController::UnknownController, ActiveRecord::RecordNotFound, with: lambda { |exception|
      render_error 404, exception
    }
  end
  ...

500报错需要能够通知管理员,上面的exception_notification是我利用exception_notificationgem2.6版本中发送报错信息邮件的功能来做到这件事。

然后加上render_error方法,用来渲染报错页面,你可以在这里做定制渲染:

1
2
3
4
5
6
def render_error(status, exception)
  respond_to do |format|
    format.html { render template: "errors/error_#{status}", layout: 'layouts/application', status: status }
    format.all { render nothing: true, status: status }
  end
end

还是有一个问题,无法捕捉ActionController::RoutingErrorAbstractController::ActionNotFound,需要在config/route.rb里面最后捕捉:

1
2
3
unless Rails.application.config.consider_all_requests_local
  match '*not_found', to: 'errors#error_404'
end

加上errors_controller.rb,里面:

1
2
3
4
5
class ErrorsController < ApplicationController
  def error_404
    @not_found_path = params[:not_found]
    render_error 404, nil
  end

资源引用:

如何设置PPTP VPN

| Comments

首先解释一下基本的概念。

我们平时上网的时候, 比如访问douban.com, 电脑做的操作是, 首先连接上douban.com的服务器, 然后告诉服务器, 需要获取豆瓣的网页。 然后服务器会返回首页信息, 自己的电脑接收到了之后, 通过浏览器显示出一张网页出来。

但是有的时候, 有些网站的网页(比如twitter)在传输过程中, 会在中间被政府屏蔽, 这样浏览器上面就会显示无法获取内容。

为了解决这个问题, 我们一般的解决方案就是, 利用一台可以获取网站内容的服务器(主要在国外, 不会在中间被政府屏蔽), 下载好网页, 然后加密一下(这样在中间政府就不知道你这个网页需要屏蔽了), 传输回自己的电脑。

简单画一下模型:

原先的方式:

网站服务器 < - 政府会在这里审查你看的东西 - > 我的电脑

现在的方式:

网站服务器 < - 这里是国外, 政府不审查 - > 中转服务器
中转服务器 < - 数据加密了, 政府看不懂 - > 我的电脑

能够实现这样功能的网络通讯方式有很多种, 这里我们提供一种最容易设置的方式: PPTP VPN。

VPN是一种网络虚拟技术, 简单地说, 就是你的电脑设置了VPN之后, 相当于和提供VPN的服务器在同一个局域网下面了, 你向网络发出的请求, 都通过这台服务器中转, 达到上面我们说需要实现的功能。

PPTP是一种实现VPN的方法, 具体原理就不多说了, 用它的主要原因是windows电脑都默认带有这种VPN通讯方式, 设置起来方便一些。 不过缺点是容易在中间被干扰, 不是很稳定。

设置方式

首先你需要购买或者要到一个PPTP代理服务器的账号和密码,购买的话请自行Google。

选择 开始 –> 控制面板, 点击网络和Internet连接, 在下面的页面点击: 创建一个到您的工作位置的网络连接。

选择虚拟专用网络连接(Virtual Private Network),

公司名称随便设置就好。

主机选择提供VPN服务的主机名称或者IP地址。

点击完成。

创建VPN完毕后, 需要设置一下。 回到网络和Internet连接页面, 点击网络连接。

你会看到刚刚建立的VPN网络, 右键点击属性:

安全面板, 点击高级, 点击设置, 点击允许这些协议, 扣掉 Microsoft CHAP, 只留下面MS-CHAP2。 一般的linux下面PPTP代理服务器只支持这个。

现在设置完毕了, 当你需要连接VPN的时候, 进入网络连接页面, 鼠标双击建立的网络。

会打开输入密码页面, 在这里输入用户名和密码,如果你不希望每次都输入, 勾选下面的保存密码功能:

如果一切正常, 点击连接之后, 就可以通过服务器访问国外网站了。 当你连接的时候, 可能会出现问题:

你可以重新再试几次, 但是如果一直有问题,那么应该是你电脑所在的网络会有一些问题,不支持PPTP VPN, 这个时候我也没有办法, 只能通过更加复杂的方式来翻一下墙, 或者换一个网络再试一下了。

项目计划预估

| Comments

进行一个项目开发项目,我们预期的状况是这样的:

需要实现一个例程,达到从A到B的一个操作。看起来很完美。

但是实际开工之后却发现变成了这样的状况:

大量的细节在项目开始的时候没有预估到,比如:

  • 中间过程中出乎意料的复杂度。
  • 各种发现的例外状况。
  • 需求变更下的返工。

这些状况构成了图片中其他的路径,工期的时间和路径的长度成等比关系。 然后要把产品做得好,具体的分支细分还会更多。

如果希望更快完成,我们可以尽量砍掉其中的路径:

  • 开始之前有原型开发,预估到技术复杂度以及预先处理掉;
  • 例外状况不予接受,或者交给人来处理;
  • 小规模迭代,应对需求变更;至少尽快处理一个可用版本。

Css实现双栏同等高度

| Comments

问题

我们用css做双栏的方法,一般是通过边栏float: right, 或者position: absolute; right: 0; top: 0来实现的。

但是这样存在一个问题,如果左右栏的底色不一样以及他们高度不一致, 会显示出来底下区块的颜色。那么底下区块应该用左栏的背景色,还是用右栏的背景色就很难说了, 因为如果用左栏颜色,右栏高度不够的时候颜色不对;用右栏颜色,左栏高度不够的时候颜色不对。

如图所示:

那么我们应该怎么解决这个问题呢?解法很巧妙,采用下面的布局:

继承关系:

1
2
3
4
5
.L1
  .L2
    .L3l
    .L3r
    .clearfix

这样,L2的高度是L3l和L3r中长的那个的高度, L3都透明,左栏的底色设置在L2上面,右栏的底色设置在L1上面, 这样无论左右栏的高度如何变化,左右栏的颜色都是正确的。

示例代码如下:

一种简单的前端框架写法

| Comments

现在网站基本上js漫天飞,一不小心就写得乱七八糟,得需要好好整理才行。 这里分享一个简易整理法,不需要很复杂就可以把js弄得非常清爽。

我们大多数的js操作,都是针对用户事件,做一个具体的反应, 比如说点击一个按钮弹出一个对话框,当用户提交表单的时候做一些验证之类。 这种用户操作模型已经被研究很多了,通用的抽象方式就是著名的MVC模型。 我们写js也可以这样抽象隔离,不过一般并不需要抽象出MVC里面的Model。 View就是html的页面,Controller就是针对事件做出的反应。

我们可以把一个逻辑相关的功能划分成一个Controller类,类里面记录相对应的一个页面区块, 列出需要监听的事件,把事件绑定到类方法上面,方法里面修改和更新区块中特定的元素。 一个轻量的前端框架Spine,提供了这样的一个Controller类来满足我们的需求。

例子

假设我们需要做一个简单的功能,页面上面一个按钮,点击一次按钮,页面上面显示的数量就加一。

页面部分包含了我们需要展示的东西:

1
2
3
4
<div class="counter">
  <a class="click">Click Me</a>
  <div class="result">0</div>
</div>

对应的javascript(用coffeescript写):

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
class CounterController extends Spine.Controller
    # 列出我们需要交互的页面元素
    elements:
        ".result": "result"
    # 绑定我们关心的事件到on_click
    events:
        "click a.click": "on_click"

    constructor: ->
        # 在Controller初始化的时候,会帮你做好上面的事件和元素绑定
        super
        # 初始化变量
        @count = 0

    # 处理事件
    on_click: ->
        @count += 1
        @update()

    # 更新页面
    update: ->
        @result.val(@count)

# 初始化
new CounterController(el: '.counter')

我一般会再做一个启动器,这样可以自动挂载,以及限定好作用域不超出对应的区块:

页面修改成:

1
2
3
<div data-controller="Counter">
  ...
</div>

增加代码:

1
2
3
4
5
6
7
8
9
10
11
init_controllers = ->
    $('[data-controller]').each ->
        init_controller($(this))

init_controller = (obj)->
    controller = obj.data('controller') + "Controller"
    data = obj.data()
    data.el = obj
    new window[controller](data)

$(init_controllers)

所有常规的js页面操作都可以抽象成一个Controller类, 按照这样的方法来组织js代码,复杂的js就能管理得井井有条, 并且只需要引入Spine(几十K左右),不需要复杂的框架代码和理解。 当然我们还可以进一步,自动绑定页面元素和数据,比如Angularjs。 不过我个人觉得,普通功能不需要做得那么复杂,用我上面的方法组织就已经OK了。