课程格子黑板报

Blog of CreatingEV Dev Team

Rails_server后边隐藏的真相

| Comments

第一部分 Rack

什么是rack

Rack 为开发 Ruby web 应用提供了一个最小的模块化和适应性接口。通过对 HTTP 请求与响应 的尽可能最简单的方式包装,它统一和提炼 Web 服务器 ,Web 框架,和之间的软件(所谓的中间件)的 API 为单一方法调用。

什么是rack app

A Rack application is any Ruby object that responds to the call method, takes a single hash parameter and returns an array containing the response status code, HTTP response headers and the response body as an array of strings.

意思就是,一个包含call(env)方法的对象就能做为一个Rack Web应用,参数env是一个hash,方法的返回值是一个列表,包含三个元素:HTTP状态码(200, 500等),HTTP响应头(Hash),HTTP响应内容(字符串数组)。

一些具体的例子

simplest.ru

1
2
3
4
# run with rackup
Simplest = proc { |env| ["200", {"Content-Type" => "text/plain"}, ["OK"]] }

run Simplest

redirect.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
require "rack"

class Redirect
  def self.call env
    if env["QUERY_STRING"] =~ /url=redirect/
      ["302", {"location" => "http://qhr.me"}, []]
    else
      ["200", {"location" => "text/plain"}, ["ok"]]
    end
  end
end

Rack::Server.new(app: Redirect, environment: "development").start

local_dir.rb

1
2
3
require "rack"

Rack::Server.new(app: Rack::Directory.new("~/")).start

更多的细节请大家参看http://guides.ruby-china.org/rails_on_rack.html这里就不在重复了。

第二部分 Server (Webrick举例)

webrick是ruby标准库中的一个webserver。

一些基础

SizedQueue

一个线程安全的队列,有大小限制。队列为空的时候,pop操作的线程会被阻塞。队列满的时候,push操作的线程会被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
q = SizedQueue.new 1

q.push 1

Thread.start {
    loop do
        puts q.pop
        sleep 10
    end
}


q.push 2
q.push 3

TCPServer

TCP/IP stream型连接的服务器端套接字的类。accept实例方法会受理客户端的连接请求, 返回已连接的TCPSocket的实例。

IO::select

多路复用IO。参数列表前三项为输入/输出/异常的IO(或者子类)的实例数组。第四个参数是timeout。第四个参数是timeout可以是整数、Float或nil(省略时的默认值)。指定为nil时,将会一直等到IO变成就绪状态。timeout时将返回nil,除此以外将返回一个包含3个元素的数组,这3个元素分别是等待输入/输出/异常的对象的数组(指定数组的子集)。

从rack开始

rack可以简单的理解成ruby frameword 和 webserver 之间的一个通用接口。一份基于rack开发的web服务可以使用rack支持的各种server来运行。rack中的所有server都具有一个叫做run的方法,这个是web server的入口。那么从rack/lib/rack/handler/webrick.rb中可以找到如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
def self.run(app, options={})
    environment  = ENV['RACK_ENV'] || 'development'
    default_host = environment == 'development' ? 'localhost' : '0.0.0.0'

    options[:BindAddress] = options.delete(:Host) || default_host
    options[:Port] ||= 8080
    options[:OutputBufferSize] = 5
    @server = ::WEBrick::HTTPServer.new(options)
    @server.mount "/", Rack::Handler::WEBrick, app
    yield @server  if block_given?
    @server.start
end

那么就从WEBrick::HTTPServer开始,看看mount和start方法是怎么工作的。

进入webrick

1
2
3
class HTTPServer < ::WEBrick::GenericServer
    ...
end

这里有必要说说GenericServer。 其中有两个只读的实例变量:listeners, tokens。 listeners是监听连接的socket数组。 tokens是最大连接数量(并发数量)。

start方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def start(&block)
  ...
      while @status == :Running
      ...
          if svrs = IO.select(@listeners, nil, nil, 2.0)
            @logger.debug(svrs.to_s)
            svrs[0].each{|svr|
              @tokens.pop          # blocks while no token is there.
              if sock = accept_client(svr)
                sock.do_not_reverse_lookup = config[:DoNotReverseLookup]
                th = start_thread(sock, &block)
                th[:WEBrickThread] = true
                thgroup.add(th)
              else
                @tokens.push(nil)
              end
            }
          end
      ...
      end
    ...
  }
end

start中,是一个循环。当没有请求的时候,主线程会被select阻塞。有请求的时候,针对每个输入就绪的socket,会通过调用socket的accept方法,来产生一个与客户端通信的新socket,而原来的socket依然在端口上监听。

针对每个与客户端通信的socket,webrick会创建一个线程(相关代码在start_thread中,稍后提及)来处理请求,这里@tokens的作用类似信号量,初始化server的时候,会把@tokens用nil填充满,只有能从@token获取到信号的时候,才可以创建线程,获取不到信号的时候,会阻塞主线程,以此控制并发数量。这里参见之前提到的SizedQueue。

每个请求的具体行为,就要继续查看start_thread了。

start_thread

这个方法中是一些异常和logger的处理,主要的一句是

1
2
3
4
5
def start_thread(sock, &block)
  ...
  block ? block.call(sock) : run(sock)
  ...
end

显而易见,run(sock)就是下个目标。

run

这个方法,就要回到::WEBrick::HTTPServer了。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def run(sock)
      while true
        res = HTTPResponse.new(@config)
        req = HTTPRequest.new(@config)
        server = self
        begin
          timeout = @config[:RequestTimeout]
          while timeout > 0
            break if IO.select([sock], nil, nil, 0.5)
            timeout = 0 if @status != :Running
            timeout -= 0.5
          end
          raise HTTPStatus::EOFError if timeout <= 0
          raise HTTPStatus::EOFError if sock.eof?
          req.parse(sock)
          res.request_method = req.request_method
          res.request_uri = req.request_uri
          res.request_http_version = req.http_version
          res.keep_alive = req.keep_alive?
          server = lookup_server(req) || self
          if callback = server[:RequestCallback]
            callback.call(req, res)
          elsif callback = server[:RequestHandler]
            msg = ":RequestHandler is deprecated, please use :RequestCallback"
            @logger.warn(msg)
            callback.call(req, res)
          end
          server.service(req, res)
        rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex
          res.set_error(ex)
        rescue HTTPStatus::Error => ex
          @logger.error(ex.message)
          res.set_error(ex)
        rescue HTTPStatus::Status => ex
          res.status = ex.code
        rescue StandardError => ex
          @logger.error(ex)
          res.set_error(ex, true)
        ensure
          if req.request_line
            if req.keep_alive? && res.keep_alive?
              req.fixup()
            end
            res.send_response(sock)
            server.access_log(@config, req, res)
          end
        end
        break if @http_version < "1.1"
        break unless req.keep_alive?
        break unless res.keep_alive?
      end
    end

req.parse

从socket读取请求报文,构造request实例。

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
39
40
41
42
43
44
45
46
47
def parse(socket=nil)
  @socket = socket
  begin
    @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : []
    @addr = socket.respond_to?(:addr) ? socket.addr : []
  rescue Errno::ENOTCONN
    raise HTTPStatus::EOFError
  end

  read_request_line(socket)
  if @http_version.major > 0
    read_header(socket)
    @header['cookie'].each{|cookie|
      @cookies += Cookie::parse(cookie)
    }
    @accept = HTTPUtils.parse_qvalues(self['accept'])
    @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset'])
    @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding'])
    @accept_language = HTTPUtils.parse_qvalues(self['accept-language'])
  end
  return if @request_method == "CONNECT"
  return if @unparsed_uri == "*"

  begin
    setup_forwarded_info
    @request_uri = parse_uri(@unparsed_uri)
    @path = HTTPUtils::unescape(@request_uri.path)
    @path = HTTPUtils::normalize_path(@path)
    @host = @request_uri.host
    @port = @request_uri.port
    @query_string = @request_uri.query
    @script_name = ""
    @path_info = @path.dup
  rescue
    raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'."
  end

  if /close/io =~ self["connection"]
    @keep_alive = false
  elsif /keep-alive/io =~ self["connection"]
    @keep_alive = true
  elsif @http_version < "1.1"
    @keep_alive = false
  else
    @keep_alive = true
  end
end

先是解析请求行,再是请求报文头部解析,最后确定keep_alive

回到run。

一般的使用情况下server都是self,lookup_server与virtual_hosts有关。server.service就是self.service,其中,找到了真正的servlet的实例,并调用实例的service方法。其中可以看看mount方法的作用:可以把不同的servlet mount不同的url上,形成一个路由表。

rack的webrick handler就是一个webrick servlet,并且复写了service这个方法。server.service(req, res)调用完毕,那么response的各个属性也就填好了,接着res.send_response(sock)会通过socket来发送数据。

一次聊天服务器性能调优经历分享

| Comments

导读:
这是我的一次性能调优经历的分享,我的本意不是分享我调优所获得的知识与经验,而是希望可以启发大家的思考。如天放所说,其实最有效完成一个目标的方式是用二分法,逐步找到问题的解决办法。但二分法的本质是趋利避害,是以目标为导向的方法,追求最短时间解决一个问题,追求最易解决问题的办法。二分法可能是高效地解决问题的好办法,但却不是学习的好方法。

“名人哲言”里有一句话,“上帝若為你關了一扇門,必然為你開另一扇門”,在这里,恰恰相反,“当你开了一扇门,你必然会关上另外一扇门”。当你用二分法解决了你的问题,并为此窃喜之时,或许被你关上的无数扇门了有你追求了许久的宝藏,这些宝藏一次次与你擦肩而过。从长远的角度来讲,二分法不见得是最高效的办法。

带着问题去学习,才是一个最高效的学习方式,在我们去完成我们的目标的时候,我们会碰到各种各样的问题,正是这些问题的存在,才让我们逐步减少因我们片段学习(我们的许多知识是通过google得到的)而产生的割裂性,逐渐完整化,系统化。而二分法在许多时候,会让我们规避掉这些问题,导致我们错失最好的学习机会。

在我的这次经验中,我获得了一些看起来很简单,但是却让我受益无穷的经验和知识宝藏,有了这些知识的夯实,我才能慢慢往更深的地方走去。或许可以这么说,二分法是一个解决问题的好办法,但不是一个提高知识深度的好办法。而提高你的知识深度,有时候,需要你违背二分法的原则,脑子缺根筋,在一条未知的路上反复探索,才能到达更深的地步。

理是这么个理,你需要有足够的智慧去区分什么时候需要用二分法,什么时候需要钻牛角尖。其实最简单的办法是看时间的要求,在时间紧张的时候,我们应该追求效率,在时间充裕的时候,我们应该追求深度,对问题穷追不舍。我觉得每个人都应该在快速的节奏下,停下来想一想,去深究一些问题,张弛有度才是一个健康的方式。这也是为什么我希望我们的开发节奏是一大一小穿插的方式进行的原因。(但看起来,大部分人都误用了小版本的用途,小版本最主要的目的是希望你能深究一些你碰到的难题,比如重构,比如UI效果,而现在大家却只用小版本来解决小bug…)

第二件,我想通过这次经历分享的是,面对未知原因的问题的时候,其实每个人都可以解决。用最笨的办法,用最小时候学过的做实验的办法。我们小时候做过许多化学实验,物理实验,老师通常会选一组参照物,最终通过与参照物对比的方式来得出我们的结论。所以我们碰到未知问题的时候,也同样的可以用这样的办法解决,但请注意最重要的一点,要保证参照物与实验对象只有唯一一点不同,这样结论才是正确的,否则实验没有任何意义。所以大胆假设,小心求证才显得如此重要。

有参照物的问题,每个人都有办法去解决,每个人都不要错过这样一个完美的学习的机会。

这次调优经历不见得是最高效,最好的方式,结果也不见得是最好的结果,但我想,希望我的这次抛砖引玉,可以给大家带来更多的思考。


ppt内容预览下载地址(可以用上一页,下一页翻页,这样就像ppt了):http://pan.baidu.com/s/1sjybu8X

Splitting a Fat Model Into Multiple Files

| Comments

起因

Rails的应用内,总有一些超级复杂的model,比如User。而正因是胖model,瘦controller这样的指导思想,会导致一些model臃肿不堪,有些model甚至会超过1000行,而一旦超过1000行,一个类的代码就变得难以维护了。所以一段时间内,我们为了不让我们的model太臃肿,我们只能不往User里面写代码,而把一些代码写在其他的model里面。比如写成如下的方式

#exam.rb
class Exam < ActiveRecord::Base
    def add_user(user)
    end
end

但这样的方式Exam.add_user(user)明显没有user.join_exam(exam)直观。

#user.rb
class User < ActiveRecord::Base
    def join_exam(exam)
    end
end

所以为了兼顾直观与保持model可维护性更高,我查找了一些资料,尝试为我们的model瘦身。

解决思路

一个广为流传的办法是Use concerns to keep your models manageable,这是dhh(Creator of Ruby on Rails)大神推荐的方式,已被列入Rails4的规范之中了(Rails4重大设计决策:“胖”Model用ActiveSupport::Concern瘦身)。

但使用concern的根本目的不是为了解决单个model的肥胖问题,而是为了让一些model通用的代码片段存在于一个合适的位置,比如commentable,这样但凡有用到comment的model,只要include commentable就可以实现评论相关的功能,这样的实现方式既简单又直观。

如果为model瘦身,主要的问题是,如何分离你的业务逻辑,因为单纯的将代码发在几个小文件中是没有什么意义的(为什么没有意义?因为会增加你查找的难度和理解的难度),这也是为什么7 Patterns to Refactor Fat ActiveRecord Models

“Any application with an app/concerns directory is concerning.”

的一个重要原因。

所以综合以上思路,最终实践得出了三个可以简化User model的办法

  • 为model抽象一些功能性concern,比如Authenticatable, Genderable。
  • 为model分离一些业务相关模块。比如user_post, 比如 user_event
  • 将特殊的功能模块抽象成class

实践

以下实践均以User model为例

为model抽象一些功能性concern

这样做符合concern本身的意图,比如将用户验证的相关操作和相关scope放在Authenticatable的module,这样User就拥有了可拔插的能力,如果想去掉相关的功能,只需要取消include就可以了。

所以,这部分的代码类似于

#app/models/concerns/authenticatable.rb
# -*- encoding : utf-8 -*-
module Authenticatable
  extend ActiveSupport::Concern
  included do
    def self.basic_auth(account, password)
    end
  end

  def has_password?
  end
end

为model分离一些业务相关模块

这样做是不太符合concern本身的意图的,但借用concern来表达我们的业务逻辑,也没有什么问题。但这样做最大的问题是,这部分的concern不应该和传统的concern混在一起,这样不同model的业务逻辑容易混乱(至少不好查找)。

所以,我们将这部分代码放在了app/models/concerns/user_concern/中,代码类似如下

#app/models/concerns/user_concern/post.rb
# -*- encoding : utf-8 -*-
module UserConcern
  module Post
    extend ActiveSupport::Concern

    included do
      has_many :posts, :dependent => :destroy
      has_many :other_posts, :dependent => :destroy
    end
  end
end

将特殊的功能模块抽象成class

这种办法最受7 Patterns to Refactor Fat ActiveRecord Models推崇,所以,可以借鉴一些这边文章提到的一些方法来抽象我们的代码。这部分代码类似于

#app/models/lib/edit_limit.rb
# -*- encoding : utf-8 -*-
class EditLimit
  NO_MODIFY_LIMIT_USERS_KEY = "no_modify_limit_users"

  MODIFY_LIMIT = 5
  MODIFY_LIMIT_KEY = "editable_times_%d"

  def initialize(user)
    @user = user
  end

  def self.no_modify_limit_users
    Rails.cache.fetch NO_MODIFY_LIMIT_USERS_KEY do
      Set.new
    end
  end

  def self.add_no_modify_limit_user(user)
    no_limit_user_ids = no_modify_limit_users

    no_limit_user_ids << user.id

    Rails.cache.write NO_MODIFY_LIMIT_USERS_KEY, no_limit_user_ids
    no_limit_user_ids
  end
end

最终,我们在user.rb的引用如下所示

#app/models/user.rb
class User < ActiveRecord::Base
  include Authenticatable
  include UserConcern::Post
end

总结

因为Rails本身的设计,对于一些不太复杂的逻辑与应用,实在无需煞费苦心去寻求瘦身与简化。因为符合Rails本身的设计思想在很大程度上就已经很好的分离了model,view,controller(MVC)三者的职责。而一些复杂的应用,在不考虑更好的可读性的前提下(比如将许多逻辑放置在关联表中),也不会形成太过肥胖的model。所以,只有一些人像我这样对可读性有些洁癖,又想让model保持简单的人,才需要类似的解决办法。

参考资料

一定会发生和一定不会发生的事情

| Comments

谈起这个话题,原因是我的一次愚蠢的尝试。我曾经在一个游戏中用虚拟积分玩过这样的游戏,我先用1个积分去投注,如果输了,我就翻倍去投注,直到我输光为止。而我不相信我能连输十盘,我觉得那是一个不可能发生的事情。结果很明显,我为了赚一个积分,把我的所有积分都输光了。所以从这点出发,我觉得有必要去正视什么是一定会发生的事情,什么是一定不会发生的事情,两者的边界是什么?

一定会发生的事情

赌博

以我上述的例子为准,假设你的人品爆发,碰到赌场了绝对公平的游戏,你赢的概率有50%。所以我们可以计算一下你连输10(或11次,下方是11次的概率)盘,输掉内裤的概率有多大。

连输11次的概率=m¹º=0.0009765625 其中m=0.5

所以,很明显你连输11盘的概率很低,换言之,你赢的概率达到了99.90%,这个概率达到了千足金的含金量。可问题是,这够稳固吗?这就意味着,你玩1000次,你就要输掉一次,只为了赚1块钱。

ok,你可以说假设你每天玩一次,1000天才会输掉一次,就是说快3年你才输掉一次。这看起来还挺好的。常胜将军。但问题关键是,这是赌博,赌博最重要的一点是趋利,是贪婪,没有人会满足一天只赚一块钱,说不定你一天就会玩上100盘,不到十天,你就输光了你的所有资本。

我称上述的例子为一定会发生的事情,哪怕它的概率达到了99.9%。

一定不会发生的事情

飞机失事

众所周知,飞机失事的概率是很低的,但到底是一个什么量级的数据呢?

Accident statistics的统计,平均340万次飞行才会有一次至少一人死亡的记录,所以全身而退的概率就等于

1-1/3400000 = 0.99999970588

这个比例就相当于,如果象梁朝伟一样,每天飞到巴黎去喂个鸽子,第二天飞回来(每天一次飞机)。那么9315年才能出现一次(至少一个人死亡)的飞机失事。

所以这个比例在我们看来是几乎不会出现的概率,我们暂时可以称之为一定不会发生的事情。

UUID(Universally Unique Identifier)

UUID是计算机上普遍使用的用于唯一标识的标识符。它一般是根据mac地址+时间戳+随机数生成的。

UUID的标准型式包含32个16进位数字,以连字号分为五段,形式为8-4-4-4-12的32个字符。示例:
550e8400-e29b-41d4-a716-446655440000

所以假设UUID是全随机的,那么它的概率是

16的32次方 = 3.4 x 10的38次方

所以,也就是说随机两次,重复一次的概率=1/ 3.4*10的38次方

那随机UUID的重复机率是多少呢?

随机产生的UUID(例如说由java.util.UUID类型产生的)的128个比特中,有122个比特是随机产生,4个比特在此版本('Randomly generated UUID')被使用,还有2个在其变体('Leach-Salz')中被使用。利用生日悖论,可计算出两笔UUID拥有相同值的机率约为   
p(n)\approx 1-e^{-n^2/{2 \cdot x}}
以下是以x=2122计算出n笔UUID后产生碰撞的机率:
n   机率
68,719,476,736 = 2的36次方   0.0000000000000004 (4 x 10-16)
2,199,023,255,552 = 2的41方    0.0000000000004 (4 x 10-13)
70,368,744,177,664 = 2的46方  0.0000000004 (4 x 10-10)
与被陨石击中的机率比较的话,已知一个人每年被陨石击中的机率估计为170亿分之1[1],也就是说机率大约是0.00000000006 (6 x 10-11),等同于在一年内置立数十兆笔UUID并发生一次重复。换句话说,每秒产生10亿笔UUID,100年后只产生一次重复的机率是50%。如果地球上每个人都各有6亿笔UUID,发生一次重复的机率是50%。
产生重复UUID并造成错误的情况非常低,是故大可不必考虑此问题。
机率也与乱数产生器的质量有关。若要避免重复机率提高,必须要使用奠基于密码学上的假乱数产生器来生成值才行。

所以,UUID的重复可以认为是一定不可能发生的事情。而这个概率相比飞机失事的概率来说,对我们来说,那真的是非常非常放心了。如果飞机失事的概率达到UUID重复的概率,那就没有空难了。

两者的边界

当我们去评估哪一些属于一定会发生,哪些是一定不会发生的时候,有两个指标。一个是单次发生的概率,第二个是你重复的次数。而我们去评估的时候,通常会以相对夸张的次数来评估最终发生的概率。

以飞机失事的概率来计算,假设一个人的寿命是90年,每天坐一班飞机。那么他在有生之年失事的概率<1%。虽然没有绝对安全,但相对更为复杂的生存环境,我觉得这已经是一个完美的概率了。

所以我觉得,飞机失事的概率为分界(1%的最终概率),远大于最终死亡概率的,那么它就可以认为是一定会发生的事情,而远小于最终死亡概率的,那么可以列为安全概率。而在这概率附近的,看你个人自己的看法啦。

参考资料

我们真正关心的事情

| Comments

如果你是位新员工,你可能会有一些困惑,我们没有代码规范,却要求你代码应该这么写,而不应该那么写,相同的,我们会建议你有些事情应该这样做,而不是那么做。你不知道到底哪些事情才是最重要的,在不知道优先级的情况下,你可能会犯一些错误,你会让你感到很沮丧。所以,从一个更严谨的角度,我应该把我们真正在乎的事情全都告诉你。 我们真正在乎的事情是:

  • 工作态度
  • 效率
  • 创造性

工作态度

认真严谨的工作态度

这件事情,我在之前提过,你可以参考an advise to the new staff,这是最最重要的事情,这是我们接下来要说的所有事情的前提。

我们没有程序员,只有工程师

希望你可以理解这句听上去有点绕口的话。程序员和工程师,在我的理解,两者最大的区别,前者只工作在代码开发阶段,而后者参与一件事情的全部(请以广义的工程师来理解,非片面的IT工程师)。而这点区别决定了,在这里,你不是对你的leader负责,而是对整个产品负责。所以需要要求你:

  • 希望你以用户的眼光来看待问题。不管是API接口还是App开发,请以用户的视角来看待问题。不要犯类似于给用户的接口却不关心排序这么低级的错误。
  • 在你的角度,尽你所能推进事情的进展。请记住你不是一个程序员,你可以推动相关人员在这件事情上的进度,(比如推动设计师完成急需的东西)。请不要置身事外,等待事情的进展。
  • 写代码之前先想清楚,先写spec梳理整个功能的开发以及后续。

在职责之外,也有属于你的权利

  • 你可以强烈坚持你的意见。(但在申诉无效之后,请服从团队的决定)
  • 关于对于产品的建议,你的建议会比其他人的更受重视。

效率

效率是团队竞争力的根基。围绕效率,有一系列我们推崇的工作方式

效率、专注——番茄工作法

专注能力是一种宝贵的资源,你只有在专注的时候,你才能用一些方法来提高你的效率。番茄工作法简易工具下载)可以帮助你集中注意力,提高效率。这是我们实践过的极为有效的一种办法,使用番茄工作法的还有一个好处是可以尽快帮你从糟糕的状态里走出来。

异步工作方式

异步工作方式保障了你好不容易积累起来的注意力不会被轻易打断。请参考be asynchronous

不要过度设计,用迭代的方法去做事情

实现最小可用原型,然后用迭代的办法去完善它。这是所有敏捷开发最重要的一点。而这一点,也体现在我们工作中的方方面面。它帮助我们训练自己如何在缺乏资源的情况下,完成一件最小可用的模型,同时,也帮我们学会去抽象和简化一件事情,而抽象是程序设计最重要的能力之一。

TDD(测试驱动开发)

没错,把TDD放在这里是因为它能帮我们提高效率。TDD可以帮我们

  • 检查代码逻辑,很难被测试的代码就是一个强烈的可以被重构的信号。
  • 减少人工检查的时间。
  • 提高交付的正确率,节约大家的时间。

重构

重构最重要的一点是可以嗅得到坏代码的味道,而坏代码是所有bug的温床,是阅读和理解代码的拦路虎。适当的及时的重构一些不合理的代码可以帮助减少bug,节约其他人理解代码的时间。

而之所以需要频繁的重构,是因为我们采用迭代的方法来编写我们的代码,而在迭代初期,适当的简化和抽象逻辑是为了更快地完成我们的最小可用模型。但我们在修改和完善我们的代码的时候,我们必须要及时的把一些不再合理的代码改写掉,使之更健壮。

创造性

创造性是一个老生常谈的话题,也是一个可遇而不可求的东西。这看起来是一个非常虚和无法落实的话题。每个个体的创造性是一个概率问题,但一个团队的创造性却是一个竞争力问题。而这一点是发展过程中,不可避免的。

所以,为了提高自己和团队的创造性,希望大家可以找到hacker的感觉。hacker不是指入侵和破坏,而是一种无边界的思维方式。希望大家有这方面的体验和训练。

其实,这篇东西里讲的东西非常之多,非常之大,每个单独的东西拿出来都是可以单独出书的。之所以简要罗列于此,是希望大家可以明白和认同什么才是我们真正关注的东西。

参考资料

BE ASYNCHRONOUS

| Comments

上次的团队分享中,跟大家分享了github的异步的工作方式。把大家的建议总结一下,并发起一个倡议。希望大家可以遵守并形成我们自己的异步的工作方式。

异步的工作方式的优点不言而喻,缩短等待(block)的时间,不会打扰其他人的工作。而这两点归根结底,会影响团队的工作效率,而效率,是一个团队,尤其是创业团队的命根子。

所以要想做到异步的工作方式,我们需要做到:

  1. 保障自己的成果输出质量。希望每个人都可以严格要求自己,对自己的输出成果负责。这其中包括
    • 服务端提供给API接口的正确性保障
    • App客户端提供给大家测试版本的基本正确性(减少明显错误的地方)
    • 设计输出给开发的切图的完整性与正确性
  2. 规范交付成果

    以API接口为例,在API接口可测试的同时,需要告诉App开发工程师,接口的url以及接口参数和调用说明。如果非绝对必要,请以异步的方式交付这些东西,比如email或者QQ的方式告诉相关的人员。

  3. 建立安静的工作氛围(强制大家以异步的方式来沟通)

    因为我们的工作环境是一个大开间,这样设计的原因是为了体现大家的平等,没有人应该有办公室,所有人包括天放在内都是平等的(虽然以后可能会改变,但改变不是为了体现特权,而是特殊原因,比如不应该被频繁打扰)。但大开间对工作效率是有害的(我在hackernews上见过类似的控诉),因为会有一些有意无意的打断和干扰,我相信这也是为什么天放设立效率室的原因。所以,为了减少大开间带来的弊端,我希望可以做到:

    • 安静的沟通方式,QQ为主,小声的讨论。如需要讨论激烈的话题,请到会议室讨论。(抱歉,在这一点上我做得非常不好,请大家监督,下不为例)
    • 不打扰带耳机的同事。如果你不希望被打扰,请带上耳机。同样的,你也不应该打扰带耳机的同事,哪怕他就坐在你旁边(你可以用QQ告知)。当然,真正紧急的事情除外。(但你要有能力区分哪些是真正紧急的事情)
    • 有些需要面对面沟通的事情,尽量放在scrum会议以及中午和晚上吃饭的时间附近。

差不多是以上几点。此外有一些技巧可以减少你被阻塞(block)的机会,以及在被阻塞(block)住之后,可以继续进行下去的办法。

  • 花一些时间在跟你的工作有交集的地方,比如App开发组,需要花一些时间在服务端的开发上,和设计的工作方式上,这样可以减少你被block的机会,以API接口为例,如果你真不清楚某个接口的参数,你可以直接去后台代码中查看,这样可以节省很多时间。

  • 培养处理block的能力。还是以App开发组为例,要有能力在没有接口和切图的前提下,去完成App的逻辑。这样在碰到block的地方,可以轻易跳过。

  • 两件事情并行的能力。虽然原则上你要一件件的事情去做,优质地完成一件事情之后,再去做另外一件事情。这样可以减少事情烂尾的风险。但真被block住的事情,希望你能有快速切换到另外一件事情的能力。这就要求你有良好的开发习惯。

    • 合适的commit粒度
    • 每件事情一个branch的开发原则
    • 了解和理解团队和你的优先级

希望和大家一起享受异步的工作方式。

参考资料

An Advise to the New Staff

| Comments

hi,新来的同学,欢迎加入我们的团队。

我们是一个甚少约束的公司,你可以自由选择自己的工作时间,自由选择自己的工作方式,在保证效率的前提下,你几乎可做任何尝试,比如站着办公,比如去不受打扰的房间一个人待着。(虽然除了我和天放,几乎没有人这样做过,但实际上我希望大家这么做,希望每个人都可以找到属于的自己的高效率的钥匙),甚至可以在家办公(你可以尝试,我尝试过,但效果不佳)。

我们在编程风格上和团队交流上都没有强制的建议。(比如所有成果必须文档化,在做之前必须写设计文档之类的)。但我们建议你可以遵从语言本身的编码规范,这样可以减少犯错的机会和降低交流的成本。

在团队的建设过程中,我们也逐渐形成了我们自己的一套工作流程。比如如何是用git来管理我们的代码,如何用git flow来管理我们的代码提交流程。(如果你不是很熟悉这些,没有关系,我们会认真跟你讲明白,帮你尽快适应)。还有比如用trello来管理和追踪我们的工作进度和工作安排。

希望你可以尽快适应上述一切(那只是一部分),那是属于我们团队的特质(traits)。你在适应的过程中,可能会遇到一些困难,习惯上的,新语言上的。希望困难不会让你感到无所适从,但不管如何,请谨记下面最重要的一点。

养成认真、严谨的工作态度,对自己的工作成果负责。

要做到上述要求,你需要做到:

  1. 在提交代码之前,认真检查自己的代码,没有任何多余无关的东西。
  2. 认真测试自己的输出成果,只有经过本地测试,内部测试之后,所有的东西才能发布在生产(production)环境。
  3. 希望你可以养成写单元测试的习惯,这样可以减少你犯错的机会。
  4. 不要重复犯低级错误。

总而言之,就是对自己的成果负责,不要让其他人去发现你的低级的错误。请记住,这是最最重要的一点,如果你多次犯了这样的错误,我们对你会是零容忍的态度。(原则的事情需要说在前面,希望这样的事情永远不会发生)至于其他,语言,习惯上的,你可以慢慢适应。

最后,希望你可以在这里找到快乐。(Be Happy here).

Support Emoji in Rails 3.2.14

| Comments

本文主要参考一片日文的文章Rails 3.2でiOS5の絵文字を扱う,实践并修改完成。让你的应用可以支持emoji,需要达到以下几点要求

  1. 数据库支持emoji(utf8mb4)
  2. rails的sql adapter可以支持以utf8mb4的方式访问数据库
  3. 传输过程中,utf8mb4的信息不会丢失

让MySQL数据库支持emoji(utf8mb4)的存储

MySQL 5.5.3以上已经支持了utf8mb4的字符集,所以如果你的MySQL是5.5.3以上,只需要在my.conf文件中按照如下的配置就可以支持utf8mb4的字符集了。

[client]
default-character-set = utf8mb4

[mysqld]
collation-server = utf8mb4_unicode_ci
character-set-server = utf8mb4
init_connect='SET NAMES utf8mb4'

在修改完my.conf配置之后,重启mysql,检查字符集是否已经更改,除了character_set_systemcharacter_set_filesystem之外,其他的字符集都需要变成utf8mb4类型。

mysql> show variables like 'char%';
+--------------------------+------------------------------------------------------+
| Variable_name            | Value                                                |
+--------------------------+------------------------------------------------------+
| character_set_client     | utf8mb4                                              |
| character_set_connection | utf8mb4                                              |
| character_set_database   | utf8mb4                                              |
| character_set_filesystem | binary                                               |
| character_set_results    | utf8mb4                                              |
| character_set_server     | utf8mb4                                              |
| character_set_system     | utf8                                                 |
| character_sets_dir       | /usr/local/Cellar/mysql/5.5.10/share/mysql/charsets/ |
+--------------------------+------------------------------------------------------+

为了支持之前用utf8创建的表同样支持emoji,你还需要将之前的表修改未utf8mb4字符集

alter table posts convert to character set utf8mb4 collate utf8mb4_unicode_ci;

rails支持emoji(utf8mb4)

rails对utf8bm4的支持主要通过sql adapter实现,mysql2的0.3.13版本已经支持了utf8mb4 encoding。主要在配置(database.yml)的时候加上以下配置即可

charset: utf8mb4
encoding: utf8mb4
collation: utf8mb4_unicode_ci

在做完上述工作之后你的rails其实已经支持了utf8mb4的存储与读取,但为了与之前的代码兼容,你还需要做下面的事情

处理MySQL index 767个字节的限制

因为mysql对index是有767个字节的限制的,在我们默认使用utf8编码的时候,如果我们定义如下的结构和index,是不会有问题的

add_column :users, :name, :string
add_index :users, :name

因为utf8最多占3个字节,而rails中创建string类型的字段时,默认用的是varchar(255)这样的数据库结构,所以255*3=765,没有超出767的限制。但我们把字符集改成了utf8mb4,所以一个字最多可以占用4个字节,那255*4就会超出767的字符限制了,所以我们没有办法使用上述的方式来创建字段和index了。

所以我们应该限定name的长度

add_column :users, :name, :string, :limit => 100
add_index :users, :name

但对于之前的系统,如果我们全部修改string类型的index,会是一个很大的工作量(主要是在正式服务器上的耗费时间会很长),所以我们可以有个折中的方案,只修改index的长度限制。

add_index :entries, [:user_id, :url],
  unique: true, length: { url: (191-4) }

所以你需要找遍所有的migration文件,通通加上长度的限制,这样才能避免一个新同学可以通过rake db:migrate

除此之外,你还需要修改rails系统的schema_migrations_table表的结构,打开activerecord(bundle open activerecord),修改lib/active_record/connection_adapters/abstract/schema_statements.rb的419行,换成下面的语句。

schema_migrations_table.column :version, :string, :null => false, :limit => 15

或者你可以像我一样,打个补丁。将以下内容写入config/initializers/active_record_schema_migrations_version.rb文件

require 'active_support/core_ext/array/wrap'
require 'active_support/deprecation/reporting'

module ActiveRecord
  module ConnectionAdapters # :nodoc:
    module SchemaStatements
            # Should not be called normally, but this operation is non-destructive.
      # The migrations module handles this automatically.
      def initialize_schema_migrations_table
        sm_table = ActiveRecord::Migrator.schema_migrations_table_name

        unless table_exists?(sm_table)
          create_table(sm_table, :id => false) do |schema_migrations_table|
            schema_migrations_table.column :version, :string, :null => false, :limit => 15
          end
          add_index sm_table, :version, :unique => true,
            :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"

          # Backwards-compatibility: if we find schema_info, assume we've
          # migrated up to that point:
          si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix

          if table_exists?(si_table)
            ActiveSupport::Deprecation.warn "Usage of the schema table `#{si_table}` is deprecated. Please switch to using `schema_migrations` table"

            old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i
            assume_migrated_upto_version(old_version)
            drop_table(si_table)
          end
        end
      end
    end
  end
end

修改JSON gem保障utf8mb4信息不丢失

Emoji and Rails JSON output issue文章描述,在未对json库做补丁之前,JSON会丢失掉一些信息。(我测试的结果是,未打补丁之前,确实emoji无法正确显示)

>> JSON({:a => "\360\237\230\204"}.to_json)
=> {"a"=>"\357\230\204"}

所以,针对这个问题的解决方案就是,把以下内容写入config/initializers/active_support_encoding.rb文件

module ActiveSupport::JSON::Encoding
    class << self
        def escape(string)
            if string.respond_to?(:force_encoding)
                string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY)
            end
            json = string.gsub(escape_regex) { |s| ESCAPED_CHARS[s] }
            json = %("#{json}")
            json.force_encoding(::Encoding::UTF_8) if json.respond_to?(:force_encoding)
            json
        end
    end
end

这样做完之后,你就可以支持emoji在json格式中的传输了。

Rails 3.2でiOS5の絵文字を扱う还给了另外一个解决办法,

module ActiveSupport
    module JSON
        module Encoding
          class << self
            def escape_with_json_gem(string)
              ::JSON.generate([string])[1..-2]
            end
            alias_method_chain :escape, :json_gem
          end
        end
    end
end

但据我测试,如果在不使用resque的时候,但在使用resque的时候,就会出现死循环的问题了。导致resque无法启动。如果你也使用了resque,你可以在escape_with_json_gem里面加入puts信息,来验证我说的话。很有可能是resque对JSON库的generate做了一些改动(我看了resque的代码,没有发现相关的细节,但resque确实动过JSON gem),导致escape与generate循环调用了。因为这个原因,我们使用这种办法来做JSON的补丁。

添加测试保证对utf8mb4的支持

为了确保我们上述的工作已经很好地支持了emoji,你最好可以在你的测试中加入类似的测试用例,来确保这一点。

  test "post emoji comment create" do
    post = FactoryGirl.create(:post)
    post :create, :format => :json, :comment => {:content => "😔"}
    assert_response_result(true)
    #测试json是否正确
    assert_equal "😔", @response_json["content"]
    #测试数据库是否正确保存与读取
    assert_equal "😔", Comments.first.content
  end

参考资料