文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

GitHub的最大亮点就是可以通过版本管理客户端工具访问GitHub上的Git资源库. 去年,我们重构了Subversion桥大部分的基础架构.

起因

Subversion桥的主要作用就是映射Git提交与Subversion的版本号. 这个映射确保了我们看到的资源库是一致的.git在提交的时候,这个映射也暴露在客户的SVN属性上.例如,你可以看到SVN版本号2504对应Git提交的 2837f28, 对应的文件是phantomjs.

第 1 段(可获 2 积分)
$ svn propget --revprop -r 2504 git-commit https://github.com/ariya/phantomjs
2837f28c739f823f2eff061c8e41cf47654b8016

在最开始开发 Subversion 桥的时候,我们选择在仓库目录中存储可序列化的 Ruby 数据库的映射数据。由于共存的映射目标和映射信息,这的确可行。

这种方式也有一些缺点。必须在工具中对映射文件特殊对待。例如备份脚本不能简单的 git clone 一个仓库。由于我们的架构在不断的发展,这种在 Git 仓库中保存临时文件的特殊做法越来越不切实际。

第 2 段(可获 2 积分)

解决方案

在2015年, 我们努力将Subversion映射迁移到Git资源库的对象数据库中. 这种方式使得映射信息在资源库中做了共享,这也意味着不需要对映射相关的数据做特殊处理. 导致的直接结果就是映射数据变成了普通的Git提交:

$ git show refs/__gh__/svn/v4
Author: Vitaly Slobodin <Vitallium@users.noreply.github.com>
Date:   Sat Dec 19 10:15:10 2015 +0300

    ---
    yaml
    ---
    r: 2504
    b: refs/heads/gh-pages
    c: 2837f28c739f823f2eff061c8e41cf47654b8016
    h: refs/heads/master
第 3 段(可获 2 积分)

除了将映射数据迁移到Git作为我们的目标外,还得确保奇偶校验的特性,并对终端用户的使用性能不产生影响.

为了让新旧映射做无缝切换,我们使用了 Scientist 库,并发的执行新旧映射.

使用Scientist库

Scientist库已经完成了两个或两个以上的实现, 通过它输入相同的内容 然后将线上的输出结果做比较.这样才能帮助确保新实现的功能与原有的功能是一致的.测试可以完成一部分工作,但在复杂的系统中,在给定的工作时间内,完成指定的测试范围并不容易.

第 4 段(可获 2 积分)

这个项目的第一步就是提取MsgpackMapping类.这个新类封装了SVN桥需要的所有的存储信息.它是一个接口,允许我们重新实现,用于支持新Git的映射.

MsgpackMapping类是一个相当大的接口(包含30个方法),由于新旧映射的实现是不同的,所以一次更换一个方法是行不通的.有了Scientist库,我们就可以增量实现,并逐步完善新的实现.新的实现与旧的实现逻辑需要保持一致.我们可以比较每个新实现方法的准确度与性能.

 

第 5 段(可获 2 积分)

接下来我们创建了一个新的 GitMapping 类,不提供任何新方法只是为了匹配 MsgpackMapping 中的方法.

class GitMapping
  # Returns current svn revision.
  def current_version; end

  # Returns a list of paths and the svn revisions where they were modified.
  def path_history(ref, path); end

  # Returns the git commit sha for an svn revision at a specific git ref.
  def sha_for_ref_version(ref, version); end

  # Updates the svn revision and git mapping.
  def update_mapping; end

  # ...
end

然后创建 ScientificMapping 类并使用 Scientist 来运行这个实验。这个类让我们可以启用和停用使用。

第 6 段(可获 2 积分)
class ScientificMapping
  # Include Scientist's API for running experiments.
  include ::Scientist

  # Original mapping class passed into the initialize method.
  attr_reader :msgpack_mapping

  # New mapping class passed into the initialize method.
  attr_reader :git_mapping

  # Class method for enabling experiments per method. See examples below.
  def self.experimental_methods(*names)
    names.each do |name|
      define_method(name) do |*args|
        science name.to_s do |experiment|
          experiment.context :args => args
          experiment.use { msgpack_mapping.send(name, *args) }
          experiment.try { git_mapping.send(name, *args) }
          experiment.run_if { run_experiment?(experiment) }
        end
      end
    end
  end

  # Enable the current_version and update_mapping method experiments.
  experimental_methods(
    :current_version,
    :update_mapping
  )

  # Class method for disabling experiments per method. See examples below.
  def self.disabled_experiments(*methods)
    extend Forwardable
    def_delegators :msgpack_mapping, *methods
  end

  # Disable the path_history and sha_for_ref_version method experiments.
  disabled_experiments(
    :path_history,
    :sha_for_ref_version
  )

  # Enable or disable experiments based on specifics to this instance of the
  # experiment. In the SvnApp::Experiment class below are examples of broader
  # ways to determine whether an experiment is run.
  def run_experiment?(experiment); end
end

 

第 7 段(可获 2 积分)

最后我们创建了一个类来做实验。这个类控制这个实验运行的频度,并记录实验的结果:

# Tell Scientist how to create a new experiment record.
Scientist::Experiment.module_eval do
  def self.new(name)
    ::SvnApp::Experiment.new(name)
  end
end

class SvnApp
  class Experiment
    include ::Scientist::Experiment

    # Experiment name passed into initialize method. For example:
    # - current_version
    # - path_history
    attr_reader :name

    # Override Scientist's default implementation to only run experiments a
    # certain percentage of the time.
    def enabled?
      percent_enabled > 0 && rand(100) < percent_enabled
    end

    # Only run experiments 10% of the time. In our case this method had a couple
    # of conditionals and returned different percentages based on the situation.
    def percent_enabled
      10
    end

    # This is the scientist method that you use to do something with the results
    # of the experiment. In our case we logged result metrics to statsd, and
    # mismatches and slow candidates to our raw log and error reporting systems.
    def publish(result)
      $stats.increment "experiments.#{name}.total"

      if result.mismatched?
        $stats.increment "experiments.#{name}.mismatch"
        log_mismatch result
      end

      result.observations.each do |observation|
        $stats.timing "experiments.#{name}.#{observation.name}", observation.duration

        if observation.raised?
          $stats.increment "experiments.#{name}.#{observation.name}_raised"
        end
      end

      result.candidates.each do |candidate|
        if candidate.duration > 10.0 # 10 seconds
          log_slow_candidate result, candidate
        end
      end
    end

    # ...
  end
end

 

第 8 段(可获 2 积分)

上面的代码需要注意的是我们怎么保存结果的.我们使用Statsd来计算结果.最开始我们将详细的实验信息保存在日志文件中,后来我们把它转存在异常报事系统中. 这些对Subversion桥来说都不是新加的.它们已经被使用了很长时间,并且我们也有查询它们的好工具.这凸显了使用Scientist库的好处:你会怎样存储结果它并没有做任何假设.例如,在GitHub上的其它应用,使用Redis或MySQL存储Scientist的执行结果.

第 9 段(可获 2 积分)

配置好Scientist以后,该我们新的映射类出场了,开始实现每一个映射方法.

实现过程

我们实现 GitMapping 类的整个过程中做的改动并不多.然而,我们经常一次只改动一小块,以便我们可以更快的做回退操作.我们的实现宗旨是:

  1. 在本地先写一个与现有单元类似的实现,然后再进行集成测试.
  2. 启用ScientificMapping类的实验.
  3. 发布到生产环境,然后观察图表与不匹配时系统产生的异常报告.
  4. 尝试将不匹配数据复制到开发环境,然后添加一个单元/集成测试用于覆盖这个场景.
  5. 确保新的测试通过.
  6. 重复步骤 3-5 直到线上环境未出现不匹配.
第 10 段(可获 2 积分)

我们严重依赖于各种图表、日志以及脚本,用来识别错误用于识别错误匹配、性能测试,以加强我们的反馈环节。

图表和日志

最开始我们有一个面板汇总了各种不匹配、性能和响应时间数据,以确保我们的试验不会对用户造成负面的影响。

science dashboard

通过了解一般意义上的事情的进行方式,我们可以从原生日志以及错误报告系统中挖掘到一些不匹配的信息。

jonmagicjonmagic

/splunk -1h production app=svnapp at=mismatch | tail 5

hubotHubot

2015-08-12T23:24:21-07:00 experiment=path_version control_value=1358 candidate_value=11 args='[1358, "branches"]'
2015-08-12T22:59:54-07:00 experiment=sha_for_ref_version control_value=67ac52c329bd04c29e84099a45bf8b763e181557 candidate_value=d561c00c2695fa46d93b5a8e88eeb10b5b256e39 args='["refs/heads/master", 20]'
2015-08-12T19:38:29-07:00 experiment=sha_for_ref_version control_value=nil candidate_value=8555c1e64383c286592f84cb7bf16e3a370a4358 args="[nil, nil]"
2015-08-12T18:32:31-07:00 experiment=path_version control_value=283 candidate_value=287 args='[287, "branches/master"]'
2015-08-12T17:37:34-07:00 experiment=commit_at control_value=67ac52c329bd04c29e84099a45bf8b763e181557 candidate_value=d561c00c2695fa46d93b5a8e88eeb10b5b256e39 args=[20]
第 11 段(可获 2 积分)

这是我们异常报告系统中一个不匹配的展示图.

haystack needle

随着项目的推进,不匹配的问题出现的越来越少,慢慢地我们更多的关注于性能,为此我们增加了一个新的图板,将每个方法的活动图画出来,它可以很直观的看出方法是如何执行与流转.这个新的图表可以帮助我们跟踪性能回归测试数据.

experiment dashboard

在变更实现的过程中我使用了一些缓存策略,用于修复性能问题.

脚本

很快地复制成为了开发环境中的瓶颈.每个资源库都呈现为特殊的雪花状.一组提交内容,包含不同的提交者,不同的国家与地区,不同的Git版本.每个资源库都有一个使用不同版本的Subversion桥创建的映射文件. 但在我们的资源库中很难复现这些bug. 因此我们添加了 script/clone. 用脚本克隆资源库,并收集它产生的msgpack映射文件复本.它允许我们在本地复制、测试并调试问题.

第 12 段(可获 2 积分)
$ script/clone ariya/phantomjs
Cloning into bare repository repositories/ariya/phantomjs.git...
remote: Counting objects: 76355, done.
remote: Total 76355 (delta 0), reused 0 (delta 0), pack-reused 76355
Receiving objects: 100% (76355/76355), 138.71 MiB | 6.05 MiB/s, done.
Resolving deltas: 100% (39951/39951), done.
Checking connectivity... done.
x svn.history.msgpack

下一个脚本用来在应用初始化 MsgpackMapping, GitMapping, 和 ScientificMapping 类实例完成后打开控制台。这不像手工方式打开会花很长时间。但我们要经常做这个操作,在长时间运行中可节省时间。它同时也让对象的构造保持一致。

第 13 段(可获 2 积分)
$ script/console ariya/phantomjs
repo            = #<SvnApp::Repo:0x007fa0baa83358>
msgpack_mapping = #<MsgpackMapping:0x007fa0baa831a0>
git_mapping     = #<GitMapping:0x007fa0baa913b8>
science_mapping = #<ScientificMapping:0x007fa0baa90c10>
irb(main):001:0> msgpack_mapping.current_version
=> 2435
irb(main):002:0> git_mapping.current_version
=> 2435

随着对性能调优的关注度不断提升,我们增加了 script/benchmark 脚本来帮助我们在开发中快速的迭代单个仓库,而无需发布到产品环境中并等待漫长的性能数据收集过程。

第 14 段(可获 2 积分)
$ script/benchmark ariya/phantomjs
--------------------------------
repositories/ariya/phantomjs.git
--------------------------------

level: 1, current_version: 2435, ref: refs/heads/master, file_path: src/qt/src/3rdparty/webkit/Source/JavaScriptCore/runtime/PutPropertySlot.h, sha: bb3df8057037aa3e49dd1818ee73967e2ea72487
Benchmarking branches_at(2330)
msgpack: 7.378ms  => ["refs/heads/1.0", "refs/heads/1.1", ...
git:           6.147ms  => ["refs/heads/1.0", "refs/heads/1.1", ...
Benchmarking refs_at(2330)
msgpack: 3.399ms  => ["refs/heads/1.0", "refs/heads/1.1", ...
    git:       2.736ms  => ["refs/heads/1.0", "refs/heads/1.1", ...
Benchmarking source_branch("refs/heads/master")
msgpack: 3.427ms  => nil
    git: 0.218ms  => nil
Benchmarking tags_at(2330)
msgpack: 3.401ms  => ["refs/tags/1.0.0", "refs/tags/1.1.0"...
    git:       3.814ms  => ["refs/tags/1.0.0", "refs/tags/1.1.0"...

 

第 15 段(可获 2 积分)

结束语

最后,我们可以交换出msgpack基础映射文件,用于新Git支持成千上万用户的产品映射. 我们的 Git架构组也可以在没有Subversion映射文件的情况下持续改进.

了解更多关于如何使用Scientist 请阅读  @jesseplusplus写的Scientist: 再次测量,一次剪裁  和 @vmg快速推进并修复决问题  .

第 16 段(可获 2 积分)

文章评论