网络寻租

Programmer, Gamer, Hacker

网站cache机制

| Comments

在网站开发中,cache机制是一个非常好用的性能提升方法。其实在其他领域,cache也有着广泛的应用。 我在这里整理一下自己在网站开发中使用cache的思路。

首先,网站的通讯模型并不复杂,所有的操作都是用户发起一个请求,然后服务器端回应请求返回数据。 中间经过了很多的层级,按照用户端到服务器端的距离,可以分为:

  • 用户端:用户在浏览器上面进行一个操作。
  • http通讯:浏览器根据用户操作,发起一个http请求并收到来自服务器的回复。
  • 页面渲染:网站服务器根据请求渲染页面返回用户。
  • 数据model:根据请求生成的对象模型和业务模型。
  • 数据库查询:服务器上面缓存的数据,提供数据持久化和访问服务。

同时,我们可以发现一些特性和规律:

  • 用户的访问符合2/8原理,大部分的访问集中在局部的功能和页面上面。
  • 上层访问下层资源的频率,也大致符合2/8原理。
  • 用户的响应时间取决于一次请求的深度。

我们可以根据这些特性,利用cache机制来优化整体访问延迟时间,以及优化服务器性能。

方法和注意点

短接

针对每个请求,越在上层返回,请求处理消耗的时间越少。所以如果要尽量提升性能,就要尽量短接请求。 我们可以用cache,预测结果,拆分请求的方式来减少反应时间。

  • cache:如果下面层级的数据没有更新,可以缓存这部分的数据,下次请求进来的时候返回,消除下层操作的时间成本和性能消耗。
  • 预测结果:针对结果确定的请求,可以先返回结果,后进行操作。 比如浏览器端用户点击关注按钮,可以先更新页面,然后ajax发起请求;服务器端直接返回成功的结果,后台异步再进行处理。
  • 拆分请求:如果一个请求有多步操作,那么可以先返回快的操作,然后再返回慢的操作。 比如先返回外框页面,通过ajax获得内部耗时区块信息;或者服务端多线程渲染不同的区块,最后合并返回给用户。

针对不同层级的cache短接方法:

  • 用户端:用js缓存数据,用户点击的时候,渲染对应的区块。
  • http:如果用户已经请求过这个页面,而这个页面也没有更改过,可以利用http的cache机制只返回http头,基本不消耗服务器资源以及砍掉服务器准备数据和渲染时间。
  • 页面渲染:如果页面其中一个区块的数据没有变更,直接返回上次渲染的页面。
  • 数据model和数据库查询:同上,数据没有变更,就可以返回上次生成的对象。

平衡

在进行cache设计的时候,我们需要平衡好开发成本,系统复杂度,性能,以及资源消耗, 要对做的事情,带来什么样的后果和收益心中有数,针对不同的策略进行权衡。

  • 页面渲染:如果一个页面包含有静态的部分和动态的部分,可以把他们拆分开来,缓存静态的部分,节省这部分的渲染时间。 但是如果这两块页面混杂起来,需要花费一些心思,比如用js来动态合并。由此给前端带来一定的复杂度。这部分的复杂度会带来更多的bug,更高的测试成本,以及未来改动更加困难。

  • caceh机制需要考虑过期的方式。如何应对嵌套cache?如果cache内部是根据多个对象渲染的, 是基于推还是基于拉的方式让cache过期?rails在这方面有比较成熟的解决方案

降维

如果cache的数据维度过多,会造成cache爆炸。比如说页面需要根据语言,用户,国家等来生成,各维护一个版本的cache的话就有点多了,需要想办法降低维度:

  • 合并重复的数据,比如合并一些重复的国家和语言。
  • 拆分静态和动态的数据,比如前端js动态更新页面来做多国语言和跟用户有关的内容。不过也需要权衡由此带来的复杂度。

同时也要考虑这些手段带来的复杂度是否能够把控。

热点优化

2/8原理:在消耗资源多的地方使力。比如在网站首页,列表的前几页等经常访问到的页面花大功夫, 而不要太花时间去优化很少被访问到的地方。

ORM cache

csdn robbin针对ORM cache优化非常有心得,可以学习一下。

cache服务器

cache的实现方式,可以用文件,内存,或者单独的cache服务器。 如果服务端采用多个进程来服务,最好采用一个cache服务器, 这样不会出现每个进程各自维护一份同样内容cache的状况。

Nested Set Model介绍

| Comments

问题

我们很多时候,需要在传统的关系型数据库中间实现树状结构, 比如说部门层级图,树状留言图等。

一般来说,针对树状结构的操作有:

  • 访问一个节点
  • 增加一个节点
  • 删除一个节点及其子项
  • 移动节点
  • 遍历一个节点及其子项

我们可以常用简单的数据结构实现,记录一个项目的父节点就可以了,比如:

Category: id integer, parent_id: integer, content: string

数据就是这样:

ID  |  PARENT_ID | content
 0  |    null    |   ...  
 1  |      0     |   ...  
 2  |      1     |   ...  
 3  |    null    |   ...  
 4  |      2     |   ...  
 5  |      4     |   ...  

这样的数据结构下面,遍历节点及其子项的操作需要用到迭代, 在关系型数据库中,基本上有多少层的子项,就要多少SQL查询, 为了保证一致性,需要用存储过程来实现这样的遍历, 一个是麻烦另外一个是也会有性能问题。 那么有什么好的解决方案呢?

Nested Set Model

我们可以换另外一种抽象方式来表示数据, 假设我们把树状图中的每个节点当做一个集合,父节点集合包含了所有的子节点集合, 那么我们可以把树转换成一个集合图:

然后,我们把所有的集合都当做一个数值的范围,父节点集合范围包含了所有子节点集合的范围, 那么集合图就会变成这样:

这样一个节点可以用范围的两个数值来表示,比如:

ID  |  PARENT_ID |  lft  |  rgt  |  content
 0  |    null    |   1   |  10   |  ...  
 1  |      0     |   2   |   9   |  ...  
 2  |      1     |   3   |   8   |  ...  
 3  |    null    |  11   |  12   |  ...  
 4  |      2     |   4   |   7   |  ...  
 5  |      4     |   5   |   6   |  ...  

其中,lft和rgt分别代表范围的左边距和右边距。

这样,我们可以通过这样的SQL来查询一个项目的所有子项:

1
2
3
4
5
select child.* from
    categories as child, categories as parent
where
    child.lft between parent.lft and parent.rft
    and parent.id = 1

一行SQL就可以解决问题,非常方便。不过带来的代价是添加和移动上面的复杂度。

具体实现

首先是如何初始化一棵树。做法很简单, 深度优先遍历这棵树,然后按顺序赋值,维护一个当前最大顺序即可。 ruby代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def init_tree root_nodes
  count = 1

  def set_value node
    node.lft = count

    for child in node.children
      count += 1
      set_value(child)
    end

    count += 1
    node.rgt = count
    node.save
  end

  for root_node in root_nodes
    set_value(root_node)
  end
end

添加一个节点,把父节点尾部rgt,以及之后的所有位置统统移后2位即可, 用一行SQL就可以实现:

1
2
3
4
5
update categories set
    rgt = rgt + 2,
    lft = lft + 2
where
    lft > 3

移动一个节点比较复杂,因为要把范围内的子项也一起移动走。 操作基本上就是把节点范围内的所有项目(包括自己)移动到目标位置, 然后把目标位置和当前位置之间的所有项目也更新到应该对应的位置上面。 具体实现比较复杂,你可以去看awesome_nested_set的具体实现

其他的操作非常简单就不给出示例了。

结论

采用nested set model可以很方便地实现树状结构, 具有很好的查询效率。不过在添加项目和移动项目上面有比较大的代价。 如果项目非常多,操作可能就不是很经济了。 我在想,应该有好的算法能够让每次更新的项目变得最少, 不知道大家是否知道有这样的算法?

使用到的工具和库

Rails下面有一个Gem叫awesome_nested_set来实现这种结构, 利用它来实现的一个树状评论Gem acts_as_commentable_with_threading 。如果你有兴趣,可以去看看具体的源码。

引用材料

如何支持视网膜屏幕

| Comments

最近网站版本更新,过程中发现有需要针对视网膜屏幕做优化, 不然普通的图片在rmbp或者new iPad上面看起来一片模糊,体验很不好。 调研了一下,发现有无数种方法,这里整理一下,欢迎大家拍砖。

css background解法

根据这里的方法,在CSS里面写:

1
2
3
4
.aClass {
    background: url(image.png);
    background: -webkit-image-set(url(image.png) 1x, url(image_2x.png) 2x);
}

这样需要把所有图片都放到css里面,比较复杂,需要设计一个预编译流程。

gem js解法

如果你用rails,这里有一个针对的gem可以用:retina_rails

它的方法是跑一个js脚本,检查是否是retina设备, 如果是的话,搜索所有页面上的img,然后替换成2倍分辨率的图片文件(在原先图片文件名后面加上@2x,当然也有办法自己设置)。

它首先需要页面加载好低分辨率的图片,然后计算大小并且限定,然后替换成高分辨率的图片。 页面的图片会首先模糊,然后变成清晰的,并且它依赖到了js,在js加载之后才会工作,不能直接展示好一个完美的页面。

rewrite解法

stackoverflow上面看到的一个有趣的解法, 用js判断是否是retina设备,然后在cookie里面加上是否是retina的标示,服务器端发现如果有这个参数的话,返回对应大小的图片。

这种解法很巧妙,不过需要服务器端支持,并且文件规则要统一,对于rails这种文件名带有tag的部署方式就不太好支持。

svg图片解法

图片全部都用svg,然后就没有模糊的问题了。不过有些浏览器不支持svg,以及不是所有图片都可以做成svg的。

缩小大图片解法

直接用一个2倍长宽的大图片,然后设置好大小成小尺寸。实现起来最简单, 但是问题是比较消耗带宽,以及大图片缩小的方式是浏览器的缩放算法,并不是点阵完美的。

其他网站用到的方法

上面是我看到的一些解决方法,然后我也去看了一下一些著名网站是如何解决这个问题的:

  • apple 图片地址用js获得然后显示,然后一部分图片没有针对retina优化。
  • v2ex 直接用2倍图片。
  • ruby-china 也直接用2倍图片。
  • github 图片全部放在css里面,好狠!
  • twitter 没有针对retina优化,图片是模糊的。
  • dribbble 也是用大图。
  • google 也是放在css里面。

我的解决方案

考虑到实现方式,带宽消耗等问题,我首先采用gem的方法,同时做出来了一些简单的修改。 img加上data-at2x,里面放置了retina图片的地址,然后js只针对所有设置这个属性的图片进行修改。

针对会造成图片模糊然后变清晰的状况,我考虑直接返回image是retina的版本。 第一次可以用这个js插件,然后设置cookie,然后服务器返回retina图片的版本。

考虑了上面这个复杂的解决方案,我还是放弃了,还是直接用2x图片吧,最简单。其实图片也不会大多少, 因为大多数图片还是区块比较多的,压缩算法已经很好地处理了这种状况了。

如何实现推荐类似文章功能

| Comments

我最近在调整我们网站GuruDigger的类似文章推荐功能。 原先的类似文章推荐是基于用户自己打的标签,而标签大家往往打得比较随意,推荐的内容比较少, 我需要采用更好的方式来处理这个问题。

整体思路

推荐文章可以用协同过滤(Collaborative Filtering), 不过要业务逻辑符合这个模式,以及数据量足够大才行,我还是用基于文章内容(Content Based)的算法来解决。

具体思路:

  • 首先是分词。文章需要拆分成单元为词的串,然后才有办法处理。
  • 然后从词串中获取能够代表文章内容意思的关键词。
  • 然后基于文章的关键词,来选择类似的文章。

这种方法,文章数据只被利用到了词和词出现频率的信息, 另外也可以通过基于语义的方式来分析出更多内容, 对应的学术领域叫做Topic Modeling, 我现在这个思路算是一个很简单的方法。

分词

采用mmseg的分词方法。原理看起来比较简单,大家可以稍微了解一下。 具体实现采用的库是针对ruby的绑定rmmseg-cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require 'rmmseg'
RMMSeg::Dictionary.load_dictionaries

def cn_word_split text
  algor = RMMSeg::Algorithm.new(text)
  out = []
  while tok = algor.next_token
    out << to_utf8(tok.text)
  end
  out
end

def to_utf8 text
  String.new(text).force_encoding('utf-8')
end

提取关键词

分词完毕之后,还需要初步筛掉一些对了解文章内容不需要的词,这种词叫做停用词, 我随便在网络上面下载了一份,大致能用,在这里下载

1
2
3
4
5
6
7
8
9
10
11
def stop_words
  File.read('chinese_stop_words.txt').split.sort
end

def cn_filter_stop_words words
  (words - stop_words).reject do |word|
    stop_words.map do |sw|
      word.include?(sw)
    end.any?
  end
end

对了,分词之前,还需要对文章预处理一下,去掉一些比较杂的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def cn_tokenize text
  out = text.downcase
  # Strip all HTML
  out = out.gsub(/<[^<>]+>/, ' ')
  # Strip all number
  out = out.gsub(/[0-9]+/, ' ')
  # strip all url
  out = out.gsub(/(http|https):\/\/[^\s]*/, ' ')

  out = cn_word_split(out)
  out = cn_filter_stop_words(out)
  out.reject do |word|
    word.length <= 1
  end
end

我采用tf-idf的方式给文章中所有词算好一个权重。 关于tf-idf的介绍,可见这里

我采用的是一个叫tf_idf的Gem, 同时我把文章title,tag等也当做词加入进文章内容中。为了表示title和tag的重要性, 出现在title和tag中的词,相当于出现在文章中的次数为系数k(我选取的是10和5)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def pre_token item
  tw = cn_tokenize(item.title)
  dw = cn_tokenize(item.desc)
  gw = item.taggings.map{|t| t.tag.name}
  tw * 10 + gw * 5 + dw
end

require 'tf_idf'

def caculate items
  corpus = items.map{|item| pre_token(item)}

  data = TfIdf.new(corpus).tf_idf
end

选择类似的文章

把文章抽象成一个在关键词作为维度的多维空间的向量,然后2篇文章的相似度就是它们之间的夹角。 这种方法叫余弦相似度。 按照公式

实现起来很简单:

1
2
3
4
5
6
7
8
9
10
11
  def similars data, d1
    data.each_with_index.map do |d2, i|
      same_keys = d1.keys & d2.keys
      score = same_keys.map{|k| d1[k] * d2[k]}.sum
      score /= (
        Math.sqrt(d1.values.map{|v| v**2}.sum) *
        Math.sqrt(d2.values.map{|v| v**2}.sum)
        )
      [score, i]
    end.sort.reverse
  end

上面函数返回的就是按照相似度排列的item列表了。 我考虑只展示前5个相似度比较高的,如果还是没有,选择相似度最高的2个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def filter_similars similars, i
  selected = []
  similars.each do |score, j|
    # escape same item
    next if j == i

    # max similar
    break if selected.length >= 5

    # max similar for score too low
    break if selected.length >= 2 and score < 0.1

    selected << [score, j]
  end
  selected
end

结论

稍微调整一下之后,列出来相似文章还是大致靠谱的。关于整体开发过程,还是有一些可以说的。

在调研上面花费了很多时间,走了弯路。这个领域我不熟悉,搜索资料,尝试各种其他方法上面,花费了不少的时间。 比如有一个python专门关于Topic Modeling的库叫gensim, 我打算用它,不过现在项目主要用ruby,我尝试互相调用,最后还是失败了,还是走回全部ruby的方法。

现在全部流程是批次的,每次有新的文章进来,都需要重新算,一次计算需要消耗我自己rMBP时间100秒左右, 计算开销还是很大的。不过现在网站压力不大,放到后台慢慢算。

关于推荐准确率,瓶颈主要在关键词获取,现在的方案中,分数比较高的词很多还是会不准确, 如果要提升的话,可能要用更多数据输入过滤掉更多的词,或者通过语义分析只提取名词, 不过现在得到的结果还行,优化的问题等真正需要面对的时候再说,先去做其它一些更重要的事情吧。

持续集成测试系统评估

| Comments

今天打算弄一个持续集成系统, 用来自动化测试我们GuruDigger的代码。选型和测试结果如下。

gitlab-ci

gitlab-ci和GitLab是一起的,安装过程非常复杂,需要创建系统用户等等,我安装失败就没有继续了。

cruisecontrol.rb

cruisecontrol.rb是thoughtworks的一个东西,安装还是比较简单的。

  • 首先把源代码下载下来。
  • 替换gemspec里面的rcov"simplecov-rcov", '0.2.3',因为rcov不支持1.9.x之后的版本。
  • bundle install
  • ./cruise start启动服务器。
  • 访问3333端口网站,或者用./cruise命令行进行操作。

不过这个项目看起来很老了,也没有什么更新,功能上面看起来也很简单,只是点击跑一下测试显示结果,不是很满足要求。

travis-ci

travis-ci它可以针对github的开源项目免费测试,针对私有项目就没有办法了,可以去下载安装源码,不过上面说还不稳定不推荐自己折腾。

cijoe

看起来使用比较简单,不过我死活没有跑起来。 更新也还是2年以前,放弃之。

jenkins

好像是比较受欢迎的CI系统, 安装非常简单,只要下载war文件,然后执行java -jar jenkins.war,之后访问8080端口网站即可。 不过添加测试用例的过程就有点复杂,我还没有深入。看起来jenkins是我需要的东西。(待续)

把书籍全部电子化

| Comments

最近,我开始实践完全的书籍电子化方案,渐渐把现在手头上拥有的实体书都出掉。 我认为现在的书籍电子化方案已经非常成熟,留存实体书,对于我来说,已经没有太多实际意义。 我同时也建议大家评估一下,看看要不要采用这种方案。

电子书的优点

首先,电子书不占空间。实体书需要书柜放置,如果住在大城市,书柜本身占用面积对应的房价, 不知道比书本身贵多少倍。书非常笨重,经常搬家的人应该知道在所有的东西里面, 书籍是单位体积下面最重的。我在电脑里面可以随随便便放上千本书,所占用的硬盘空间可以忽略不计。

电子书检索非常方便。当我需要寻找一本电子书的时候,我不需要去在书柜里面一本本翻, 我只需要利用操作系统的全局搜索功能,输入关键词即可。 如果我需要在一本书里面找一个我想要寻找的段落,不需要一页页地翻, 用电子书应用程序内置的搜索就轻松完成目标。

电子书方便携带。就算是最小的书,也比我的手机大很多, 我可以在手机里面放很多本电子书,在任何想看书的时候,都可以打开来看。

电子书有很好的软件辅助。可以内嵌字典,可以设置书签,可以拷贝内容, 比实体书方便很多。

不足之处

电子书有这些优点,比较起来也有一些缺点,不过对于我来说问题不大。这里一一说明。

电子书阅读吃力。这个应该说是对于电子书最广泛的批评了。很多人偏好实体书也主要是这个原因。 我现在看电子书有几种方式:在我的笔记本rMBP上面,iPad上面,以及我的手机上面。 在这些平台,我的眼睛没有出现任何问题。rMBP和new iPad采用retina屏幕,看东西非常舒服, 不会出现其他液晶屏看久了眼睛难受的状况。如果你没有这些设备,我建议你可以考虑买一台。

很多实体书没有电子版。这个问题的确不好办,这种状况也只能购买实体书了。 不过还有另外的解决办法。现在有很多扫描仪可以用,专门扫描全书的设备也有, 淘宝上面有很多书籍扫描服务和设备。 对于我来说,除了新书,基本没有发现找不到电子版的状况。好书一定有很多人需要,很多人需要一般就会有电子版放出来。

一些经验

电子书往往有多种格式,比如pdf,epub,mobi等,我比较喜欢epub,因为格式开放, 同一份电子书文件可以在电脑,手机上面阅读。pdf次之,因为我只能在电脑和iPad上面获得好的展示效果。 至于其他各种第三方的格式,使用不便我一般不搜集。

如果我听说了一本好书想要下载,我会直接用google搜索那本书的名字+下载, 一般都会有,主要放出来的地方是新浪爱问,或者各种专业领域论坛。 我一般看英文书,如果不是教科书和专业书籍(贵死了买不起), 我会选择直接购买。amazon.com购买英文电子书,卓越亚马逊购买中文电子书。

我会按照类型把书籍整理起来,放到对应的文件夹里面去,而全部的书籍文件夹, 我会采用dropbox同步,你可以用国内的坚果云服务。 这样这些资料不会丢失,并且可以很方便地分享给其他人。 最好你用一些全局搜索工具监控这个文件夹,方便需要的时候快速检索。

如何写邮件样式

| Comments

最近一段时间都在写邮件的样式,以及测试邮件在各种邮箱中的展示效果,搞得焦头烂额,痛定思痛,在这里整理获得的一些撞墙经验吧。

基本的原则

用table来控制架构

虽然现在css已经进化到可以用来画画的地步, 但是各种邮箱客户端往往各自为政,你调试得非常完美的邮件,在用户那边,被客户端蹂躏得惨不忍睹。 我们还是需要退而求其次,找一个各种客户端都支持得非常好的布局方案:利用table来布局。

用table的方式安排页面各个元素的方法,有着悠久的历史,虽然不优雅,但是能够解决问题。 删除掉各种在web端应用广泛的floatposition吧,勇敢回归table

inline css样式

邮件中带有css外链?邮件中含有<style>区块?很抱歉,在Gmail以及各种在线邮箱客户端中, 它们都会被当做嫌疑分子,有杀错没漏过地一律干掉。唯一可以通行的就只有嵌入在tag中的style属性了。 不过不要紧,你不需要手写这些,现在有很多第三方的工具来帮助你自动生成,比如premailer

注意可用的css样式

虽然style可以通行,但是广大客户端为了显示正常,还会过滤掉一些影响大局稳定的css属性, 比如Gmail就会杀掉position: absolute的样式,hotmail看float不顺眼。

还有就是客户端应用程序的邮件显示渲染系统一般进行了客制化,邮件的样式还是最好还是限制在css1/css2标准里面安全一些。

有一个各个客户端的css支持表格,聊胜于无。

text模式和html模式

按照需要, 一封邮件可以同时提供texthtml两份,用户可以选择自己喜欢的看。 如果你的用户期望看到text格式的邮件,最好提供一份。

显示图片和非显示图片

因为安全因素考虑,大多数邮件客户端都不默认显示邮件的图片,需要用户手动确认。 所以你的邮件必须要能够在这种情况下面显示得不至于太寒碜。

各种需要处理状况

上面这些都是总览,这里整理一些其他额外需要注意的状况。

gmail会根据你的文字宽度给你添加<wbr>的标签强制换行。我最后采用table布局的方式解决了这个问题。

不同平台下的不同浏览器会采用不同的字体。所以你也页面不要根据开发环境的字体预设宽度。

有些android手机(比如我的galaxy nexus)gmail客户端会限制页面的宽度,有些文字段落会压缩掉宽度,没有找到解决办法。

测试

邮件显示效果的测试,我觉得是最烦的一件事情了。维度太多。需要测试的有:

  • 不同的邮件客户端,thunderbird,outlook,foxmail,它们还有不同的版本。
  • 不同的在线邮箱在不同浏览器下面展示的效果。维度大概是在线邮箱系统数量乘以浏览器数量了。
  • 不同操作系统。主要是字体影响比较大。
  • 多平台,各种手机客户端,平板设备。现在是移动时代,很多人有在移动设备上读邮件的习惯。

全部测试一遍很不现实,只能根据用户特征把主流的一些环境测试一下。

经验

花费了那么多时间,这里整理一下主要的经验以供借鉴:

如果简单的邮件,只是改一份文件就好,如果模块复杂,还是需要根据区块拆分一下。 也可以用一些预处理的工具来让文件变得更易读。 我是用haml,sass,以及rails的render partial功能来把复杂邮件模块化的。

因为邮件需要不断地调整,最好养成完成一个部分的改动后就git commit一下,或者至少git add变动, 因为很容易在改动中造成错误。

要进行流程控制,每次修改完毕后都过一遍测试,总是会出现一个环境改动正确了,其他环境又出现展示问题的状况。

资料

保证电子邮件的送达率

| Comments

现在大多数网站都会发送各种各样的邮件,保证邮件送达率至关重要,这里整理一下我调研的一些结果。

要让邮件正常到达用户的收件箱,需要保证几点:

  • 让收件方验证邮件来源是非伪造的,需要设置PTRSPFDKIM
  • 让收件方认可发送者是可靠的。

如何让收件方验证邮件来源是非伪造的

PTR

ptr是DNS记录的一种类型,有一个用途在于反方向从IP推导到域名,比如:

dig -x 203.208.36.17

把DNS的MX类型记录指向到你邮件服务器的地址一般就可以了。

SPF

SPF(Sender Policy Framework)这是一套电子邮件验证系统,通过在DNS记录中添加一个TXT类型的记录,指定谁有权以域名的名义发送邮件。如果你没有设置这条记录,很容易就被当做非法邮件spam掉。

这里是配置方法,以及一个简单的例子:

v=spf1 a mx ptr include:spf.mtasv.net ~all

这一行主要说明spf.mtasv.net(postmark,一个邮件发送服务商)代发的邮件是被认可的。

如果你域名挂靠在bluehost上面,这里是对应的设置方法

设置完毕,等待生效后,可以通过这种方式验证是否设置成功:

dig txt your-domain.com

返回的信息需要带有你设置的SPF记录。

DKIM

邮件本身是纯文本,协议也没有防止伪造的部分,在发送的过程中很容易被篡改, DKIM(DomainKeys Identified Mail) 对邮件用私钥加密,同时公钥信息放在域名DNS上面,这样收件方就可以验证邮件的真实性。

如果你用ubuntu以及postfix,这里是设置教程,以及一个简略的安装过程整理:

  • 安装opendkim sudo aptitude install opendkim
  • 设置 /etc/opendkim.conf
  • 告诉postfix采用opendkim,设置/etc/postfix/main.cf
  • 启动和重启对应的服务。

原理很简单,运行opendkim进程服务器,然后postfix把它当做一个过滤器来用,设置完成之后,对于所有的邮件,都会加上一个DKIM-Signature,一个具体的例子:

DKIM-Signature: v=1; a=rsa-sha256; d=example.net; s=brisbane;
c=relaxed/simple; q=dns/txt; l=1234; t=1117574938; x=1118006938;
h=from:to:subject:date:keywords:keywords;
bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;
b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR

一些参数的含义:

  • b:具体的数字签名。
  • bh:body hash。
  • d:签名的域名。
  • s:selector。一个域名可能有多个邮件发送服务器,需要用一个selector来区分。

然后接收方就会去检查brisbane._domainkey.example.net的DNS记录,你可以通过这种方式来查看是否它设置正确:

dig txt brisbane._domainkey.example.net

内容格式是k=rsa\; p=MIGfMA0GCSqGSIb3DQEBA...

检查是否设置成功

发送一封邮件到gmail里面,这样检查信息:

出现mailed-by表示你SPF设置正确了,出现signed-by表示你DKIM设置正确了。

让收件方认可发送者是可靠的

上面是一些技术上面的部分,同时还需要在内容上面以及行为上面让收件方认可你:

  • 收件人需要认可邮件内容。你发送的东西对于用户是有信息含量的,如果用户看到你的邮件就标注spam,再多的设置也没有用。
  • 邮件中带有退订链接。当用户不想收到你的邮件的时候,可以点击退订,防止用户直接把你spam掉。
  • 控制发送频率。各个邮箱提供商会检查来自一个IP的邮件投递速率,如果你发送速度过于频繁,也容易进黑名单。

第三方的服务

设置了上面的这些东西,你的邮件还是有可能被spam,以及你也不清楚到底邮件的到达率有多少,我建议针对商业的网站,还是需要采用一些第三方的邮件发送服务。这里推荐几个:

采用了这些第三方服务之后,你需要对应地更新SPFDKIM。 同时如果可能,尽量选择独立IP的服务,这样不会因为其他人发spam影响到你的邮件送达率。

mailgun和postmark都会想办法保证你的邮件送达, 因此价格也比其他的邮件服务商要贵,但是还是物有所值的。

mailgun带有campaign服务, 你可以设置一个campaign,然后发送带有这个campaign ID的邮件,之后会统计出送达率,开启率,链接点击率等信息给你。

还有就是针对国内垃圾邮件乱飞的状况,很多国内的邮箱服务商会比较严格,造成国外的第三方邮件发送服务不好用的状况,比如:

  • mailgun发到QQ邮箱,新浪邮箱里面会被spam掉。
  • postmark和mailgun都发不了邮件到tom邮箱里面。

需要注意一下。

引用资料以及工具

命令行下使用全局代理

| Comments

最近Bitbucket的git ssh访问被墙了,如果设置电脑全局翻墙会很麻烦,我需要能够有针对一个terminal,一个命令的翻墙方式。

询问了友邻之后,有几个工具浮出水面:tsocks以及proxychains

调研了一下tsocks,2002年就不再更新了,osx下面编译没有成功,不过ubuntu的源里面是有的。下载下来发现,它需要修改一个设置文件/etc/tsocks.conf,比较麻烦,于是我放弃使用这个工具。

proxychains要好一些,github上面的页面有mac下面用homebrew的安装方法,ubuntu的源里面也有,使用起来也是一行代码可以搞定的。

首先需要跑一个socks5 proxy,使用:

$ ssh -fN -D 4321 some.example.com

然后设置参数执行命令就好了:

$ PROXYCHAINS_SOCKS5=4321 proxychains wget http://wikileaks.org/IMG/wlogo.png

调试rails Autolink

| Comments

今天遇到一个bug,在我们网站GuruDigger里面的留言或者私信中,会自动把网址转换成链接,比如:

我今天发现一个好的网站,它是:www.dreamore.com

而对于下面这种状况,就错误地把后面的内容(就是那个句号)全部加到链接里面去了。

我今天发现一个好的网站,它是:www.dreamore.com。

我首先判断一下这个问题是否是一个问题。在使用中,中文的逗号和句号都是参见的分割符,用户会很容易就使用这种方法。我觉得这个问题需要解决。

首先是界定问题。这个很明显是渲染链接出错。我采用的是rails_autolinkgem,因为在rails3.2中,原先的autolink功能被移除了。

我下载了它的代码库,找到具体做这件事的文件:lib/rails_autolink/helpers.rb,和对应的位置:

1
2
3
4
AUTO_LINK_RE = %r{
  (?: ((?:ed2k|ftp|http|https|irc|mailto|news|gopher|nntp|telnet|webcal|xmpp|callto|feed|svn|urn|aim|rsync|tag|ssh|sftp|rtsp|afs|file):)// | www\. )
  [^\s<]+
}x

它是利用正则来找到链接位置,然后替换的。在[^\s<]+里面,没有取消掉中文的分隔符,这样把后面的东西全部匹配掉了。

找到了问题,如何解决呢?我尝试去掉分隔符,把它替换成[^\s<\p{P}]+,但是发现.是应该被匹配到的,也被包含在了\p{P}里面,于是我要细分以下。 文档中有:

/\p{Pc}/ - 'Punctuation: Connector'
/\p{Pd}/ - 'Punctuation: Dash'
/\p{Ps}/ - 'Punctuation: Open'
/\p{Pe}/ - 'Punctuation: Close'
/\p{Pi}/ - 'Punctuation: Initial Quote'
/\p{Pf}/ - 'Punctuation: Final Quote'
/\p{Po}/ - 'Punctuation: Other'

然后我完全不清楚每一个组里面有什么符号,google也找不到解释,这个时候我只能去挖ruby源码了。 我用find | grep来找Punctuation,被我找到具体的定义位置在:enc/unicode/name2ctype.h里面。英文的句号被分组在Punctuation: Other中。

这条路走不通,我又回头好好思考了一下,我觉得这个需求并不是全局的,只是针对采用中文的用户有效。我放弃做一个全局的方案,然后提交Pull Request, 改成自己客制化一个repo算了。于是我fork了这个项目,然后把[^\s<]+改成[^\s<,。]+push上去,Gemfile里面换成gem 'rails_autolink', github: "halida/rails_autolink", 发布到服务器上面去,问题解决了。

但是问题并没有完全结束,我必须在以后的版本里面注意到采用了一个客制化的gem,可能会有各种问题出现。未来的地雷需要小心不要踩到。 这是一个简单的bug处理,上面是我debug的整个过程,体力活,是以为记,大家对于这个过程有什么看法吗?