<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>zhongfox (麦先生)</title>
    <link>https://ruby-china.org/zhongfox</link>
    <description>知识改变命运，可乐改变鸡翅</description>
    <language>en-us</language>
    <item>
      <title>Data Service 设计分享</title>
      <description>&lt;p&gt;Data Service 是我们团队对前后端数据进行管理的中间层，它是在我们进行前后端分离过程中产生的数据共享实践。Data Service 在团队中的广泛应用，显著提升了系统性能以及大家的开发效率。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="项目演进背景"&gt;项目演进背景&lt;/h2&gt;
&lt;p&gt;我们团队维护的主要业务是 PC 商品导购平台，该项目在过去几年，经历了从一个很小的 rails 单体项目，演进为一个综合的大型互联网系统。前后端分离正是出现在这个演进过程中的一次拆分。我们在展示层架构上的技术选型是 Node.js KOA 框架，主要期望达到的效果：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;将展示层代码迁移到 node 项目，前端同学直接管理 view 层。&lt;/li&gt;
&lt;li&gt;利用 node.js 扛直接用户压力，系统日常 pv 百万级，大促可能飙到千万级，node.js 的异步 IO 是一个不错的选择。&lt;/li&gt;
&lt;li&gt;KOA 利用了 ES6 的 generator, 可以使用同步的语法写出异步的操作，因此 javascript 的回调金字塔也不是问题。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/8361f8d8b8e2a7223b648c03f4e31515.png!large" title="" alt="前后端分离"&gt;&lt;/p&gt;

&lt;p&gt;虽然 node ES6 的语言表现力仍然远不及 ruby, 不过在以上的特定领域，这套架构还是取得了预期的效果。技术选型、项目管理都充满了权衡，就看当前阶段你更想要什么。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="问题"&gt;问题&lt;/h2&gt;
&lt;p&gt;在前后端分离过程中，我们面临最明显的问题是前端如何获取数据，摸索期我们有尝试过的交互方式有：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;node.js 直接查询 mysql&lt;/li&gt;
&lt;li&gt;ruby 提供 http 接口供 node.js 查询&lt;/li&gt;
&lt;li&gt;后端提供 thrift 接口供 node.js 查询&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nodejs + ORM"&gt;nodejs + ORM&lt;/h3&gt;
&lt;p&gt;先说 node 直接查询 mysql, 我们采用的 ORM 框架&lt;a href="http://sequelizejs.com" rel="nofollow" target="_blank" title=""&gt;sequelize&lt;/a&gt;, 我们都很熟悉 Rails 的 ORM 框架 Activerecord, 想着在 node.js 里用 ORM 也是 so easy, 不过实际使用时我们还是遇到了很多问题。&lt;/p&gt;

&lt;p&gt;关系型数据库管理的数据关系和程序真实操作的数据结构之间有较大的差异，关系型数据库的数据无法直接表示对象、列表、嵌套等结构，这种差异叫做&lt;a href="https://en.wikipedia.org/wiki/Object-relational_impedance_mismatch" rel="nofollow" target="_blank" title=""&gt;阻抗失衡&lt;/a&gt;(Object-relational impedance mismatch), ORM 正是对阻抗失衡进行结构转译的技术。不过 ORM 框架本身的问题在于性能，在使用 ORM 时，开发同学必须十分了解 mysql 结构，具体字段有没有索引，mysql 数据量有多大，会不会造成慢查询等。然而 node 项目的维护者主要是前端同学，他们在数据库的使用技术上有较大的缺口，最后前端同学要么逼疯要么逼成全栈。&lt;/p&gt;

&lt;p&gt;不仅前端不满意，后端同学对 sequelize 的语法也不满意，这主要也是受限于 javascript 和 ruby 的语言表现力的差异，就好比之前你熟悉了用锯子砍树，现在给你一把剪刀，你能满意吗？&lt;/p&gt;
&lt;h3 id="http/thrift"&gt;http/thrift&lt;/h3&gt;
&lt;p&gt;也有的数据查询采用 http 交互，ruby 项目变成一个 api 服务。这种实现有以下问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;http 数据传输效率不高，相对于 tpc, 处于应用层的 http 传输作为内部 RPC 有天然的缺陷：文本格式，元数据 (header) 比重太大，需要 dns 等消耗。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;http 不稳定，容易受网络抖动等影响，对于内部 RPC, 重试，缓存等都需要代码自己考虑。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;那么换成&lt;a href="https://thrift.apache.org/" rel="nofollow" target="_blank" title=""&gt;thrift&lt;/a&gt;如何？thrift 是跨语言的 RPC 框架，采用高效的二进制通讯协议，在应用场景上很适合这种多语言的系统，然后 thrift 的应用也有一些问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;前端对 thrift 不熟悉，学习成本。如果整个团队都是全栈那就没问题了。&lt;/li&gt;
&lt;li&gt;thrift 作为面向服务框架，只有 RPC 调用功能，没有提供 RPC 治理功能，如监控，统计等，这些需要自行实现。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因为缺乏标准，一时间，前后端的数据查询进入了群魔乱舞的阶段，mysql, http, thrift 同时存在，项目耦合严重，系统不稳定，频繁告警。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Data Service 设计"&gt;Data Service 设计&lt;/h2&gt;
&lt;p&gt;上面花了大量的篇幅来说明背景和问题，其实系统的优化往往是这样，如果真的清楚了系统的痛点，那么离解决问题并不远。怕的是没有认识到问题和系统可优化的空间。&lt;/p&gt;
&lt;h3 id="结合代码分析"&gt;结合代码分析&lt;/h3&gt;
&lt;p&gt;通过分析前后端项目的业务代码，我们发现在涉及数据查询的代码中，大家都在做这样一些判断和操作：&lt;/p&gt;

&lt;p&gt;Ruby 项目同学：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;通过什么方式提供给前端同学，如果是 http/thrift, 就需要去编写具体的接口，如果是 mysql, 就需要告诉前端同学数据结构，如何查询，有时还得帮前端同学写好 sql.&lt;/li&gt;
&lt;li&gt;后端要不要缓存，缓存到哪里，key 是什么，前端同学会进行缓存吗。&lt;/li&gt;
&lt;li&gt;前端同学需要什么样的数据格式。&lt;/li&gt;
&lt;li style="color:red;"&gt;将持久化数据的数据组合为前端需要的格式&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node.js 项目同学：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;从哪里获得后端数据，涉及的 ip, port, url, 参数等，是否之前有缓存。有的数据可能还是从多个后端获取。&lt;/li&gt;
&lt;li&gt;从后端获取数据是否成功，如果失败了是否要重试，或者是否有备份数据提供展示。&lt;/li&gt;
&lt;li&gt;获得的数据是否要缓存，缓存要缓存多久，缓存到哪里，缓存的 key 叫什么。&lt;/li&gt;
&lt;li style="color:red;"&gt;拿到业务数据后，如何展示。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;各项目有大量和业务无直接关系的控制代码。后端同学其实只关心「将持久化数据的数据组合为前端需要的格式」, 而对于维护 view 层的前端同学来说，他们其实只关心「拿到业务数据后，如何展示」, 而在 data service 出现之前，以上逻辑在 node 项目中随处可见，展示层关心了太多数据的逻辑。&lt;/p&gt;
&lt;h3 id="结合业务分析"&gt;结合业务分析&lt;/h3&gt;
&lt;p&gt;用 rails 的 REST 术语来说，电商平台最常见的 view 形式是 list 页面和 show 页面。list 代表一类资源的列表，如商品列表，评论列表，购买列表。show 页面代码一个具体的资源实例，如 id 为 100 的商品。以上两类抽象数据占据了前后端数据查询的绝大部分。因此我们把前后端的数据抽象为 2 种情况，实例型和关系型：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/073d74c753a7064af1375a9603ffa473.png!large" title="" alt="实例型和关系型"&gt;&lt;/p&gt;

&lt;p&gt;其中实例型数据代表一个资源实体，它和数据表中的一条记录一一对应，在查询时，需要提供一个资源标识和 id 标识，典型的 url 是&lt;code&gt;/:resource_name/:id&lt;/code&gt;, 如&lt;code&gt;/products/100&lt;/code&gt;;&lt;/p&gt;

&lt;p&gt;我们对关系型数据的定义是：除了实例型以外的数据。它代表一类相关资源，可能对应后端的一张表，也可能是多张表中的某些数据。在查询时，只需要提供一个资源标识，典型的 url 是&lt;code&gt;/:resource_name&lt;/code&gt;, 如&lt;code&gt;/products&lt;/code&gt;, 这样的定义有点类似 nosql 中的聚合关系。&lt;/p&gt;
&lt;h3 id="Show me the Code"&gt;Show me the Code&lt;/h3&gt;
&lt;p&gt;设：mysql 有 1 个表 products(商品):&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;price(单位是分)&lt;/th&gt;
&lt;th&gt;...&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;td&gt;Ruby 元编程&lt;/td&gt;
&lt;td&gt;6880&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;456&lt;/td&gt;
&lt;td&gt;深入浅出 Node.js&lt;/td&gt;
&lt;td&gt;6900&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Rails 中的 ActiveRecord 数据模型：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/product.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="nf"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;top_products_by_sales&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# 查询销量最高的count个商品&lt;/span&gt;
      &lt;span class="o"&gt;......&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果前端展示层需要 2 个页面：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;展示销量 top 10 的商品页面，同时显示所有商品的个数。&lt;/li&gt;
&lt;li&gt;在上面的页面中点击具体商品，进入商品详情页，详情页需要展示商品的 title 和价格，价格展示单位是元。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;来看看引入了 Data Service 后，Ruby 和 Node 项目中的需要增加的代码是怎么样的：&lt;/p&gt;

&lt;p&gt;ruby 项目代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/data_service/product.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataService::Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;DataService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json_attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;price&lt;/span&gt;
    &lt;span class="no"&gt;Util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fen_to_yuan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;


  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json_attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;top&lt;/span&gt;
      &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;top_products_by_sales&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;node.js 项目代码&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;dataService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_data_service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// 单实例型调用, 返回对象&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;dataService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; {id: 123, title: 'Ruby元编程', price: 68.8}&lt;/span&gt;

&lt;span class="c1"&gt;// 多实例型调用, 返回数组&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;dataService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;456&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; [{id: 123, title: 'Ruby元编程', price: 68.8},&lt;/span&gt;
&lt;span class="c1"&gt;//     {id: 456, title: '深入浅出Node.js', price: 69} ]&lt;/span&gt;

&lt;span class="c1"&gt;// 关系型调用, 以下2种方式完全相同&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;topProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;dataService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;topProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;dataService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; {&lt;/span&gt;
&lt;span class="c1"&gt;//      top: [{id: 123, title: 'Ruby元编程', price: 68.8}, {id: 456, title: '深入浅出Node.js', price: 69} ...],&lt;/span&gt;
&lt;span class="c1"&gt;//      count: 23&lt;/span&gt;
&lt;span class="c1"&gt;//    }&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上基本是就是这一次业务需求中，&lt;strong&gt;前后端 2 个项目在数据交互上需要添加的全部代码！&lt;/strong&gt; 在前后端同学商量好需要的数据定义后，后端同学只需要实现数据如何查询和组装，前端同学通过一句简单的 yield 后，可以把所有精力都放到如何展示数据 json 上。他们的代码都不涉及写接口，缓存，查询失败的逻辑，这些都是 Data Service 框架完成的事情。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Data Service 实现"&gt;Data Service 实现&lt;/h2&gt;
&lt;p&gt;Ruby 语言的 API 设计非常简洁优雅，不过在简洁设计的背后有大量复杂的实现作为支撑。DataService 的目标之一，也是希望能提供一套简单易用的 API, 隐藏大量在数据交互中重复逻辑，让前后端同学各司其职，把精力放在具体业务的实现上。&lt;/p&gt;

&lt;p&gt;Data Service 实现的通用功能包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;采用 redis 作为前后端的统一数据缓存，后端异步更新数据，前端直接查询的是 redis, 高效且稳定。&lt;/li&gt;
&lt;li&gt;Ruby 项目作为数据维护方，Data Service 维护了统一的 redis 数据结构，这是前后端数据交互的约定结构，对于新的业务逻辑，前后端同学不需要花太多时间去讨论数据结构，只需要明确需要哪些模型，是实例型还是关系型。&lt;/li&gt;
&lt;li&gt;提供了统一的容错机制 (http miss 接口), 解决如果缓存里没数据怎么办的问题。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/49fbd87298ba19c6045be79e8284e1d2.png!large" title="" alt="Data Service架构"&gt;&lt;/p&gt;
&lt;h3 id="Ruby 端DataService::Base实现"&gt;Ruby 端 DataService::Base 实现&lt;/h3&gt;
&lt;p&gt;我个人非常喜欢 Ruby 中「类也是对象」的设计，这似乎是一种能治愈强迫症的设计：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Person&lt;/span&gt;
  &lt;span class="c1"&gt;# 这个作用域定义的方法、配置等, 是针对Person实例, 比如张三, 李四&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="c1"&gt;# 这个作用域定义的方法、配置等, 是针对Person这个类对象的&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;个体和集合这样的关系，在 ORM 领域和 RESTful 领域也有相似的对应关系：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;实例对象           &amp;lt;--&amp;gt; 类对象
关系型数据库record &amp;lt;--&amp;gt; 关系型数据库table
show页面          &amp;lt;--&amp;gt; list页面
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而在 Data Service 的设计中，我也把所有的数据抽象为了个体/集合这样的关系，用&lt;code&gt;DataService::Base&lt;/code&gt;的子类代表一种实例型数据，用&lt;code&gt;DataService::Base&lt;/code&gt;的子类的&lt;code&gt;singleton_class&lt;/code&gt;代表一种关系型数据：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/5e2e7ce679fabaf64c0aa1790741ab08.png!large" title="" alt="DataService的实例与关系"&gt;&lt;/p&gt;

&lt;p&gt;在 Data Service 中，实例型数据的定位方式是&lt;code&gt;模型名称&lt;/code&gt;+&lt;code&gt;id&lt;/code&gt;, 关系型数据的定位方式也是类似，只是 id 是固定的字符串'relation', 关系型数据可以认为是一个特殊的实例型数据，就类似 Ruby 中 Class 是一个特殊的 Ruby 对象。&lt;/p&gt;

&lt;p&gt;这样设计的一个好处是统一 API, Ruby 中类也是一个对象，那么类对象和类的实例，会有一些相同的 API, 比如&lt;code&gt;Object#to_s&lt;/code&gt;; 而 Data Service 中实例型数据和关系型数据都有设置缓存时间的需求，因此也有相同的 API&lt;code&gt;expire_time&lt;/code&gt;. 其他相同的 API 还有&lt;code&gt;cache_key&lt;/code&gt;, &lt;code&gt;save&lt;/code&gt;等：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataService::Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;DataService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="c1"&gt;### begin 实例存储 ##################################################&lt;/span&gt;

  &lt;span class="c1"&gt;# 实例缓存存储时间&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;

  &lt;span class="c1"&gt;# 可选设置, 默认是当前的class对应的顶级ActiveRecord Model, 用于和数据模型关联&lt;/span&gt;
  &lt;span class="c1"&gt;# self.model_class = ::Product&lt;/span&gt;

  &lt;span class="c1"&gt;# 可选设置, 默认是当前的class的小写形式, 最后存到redis的将是 'data_service:#{cache_key}:#{model.id}'&lt;/span&gt;
  &lt;span class="c1"&gt;# self.cache_key = 'product'&lt;/span&gt;

  &lt;span class="c1"&gt;# 前端需要的数据内容, 每一个属性都需要一个对应的方法&lt;/span&gt;
  &lt;span class="c1"&gt;# 方法优先在DataService::Base对象上查找&lt;/span&gt;
  &lt;span class="c1"&gt;# 如果找不到会委托到对应的Activerecord模型上查找&lt;/span&gt;
  &lt;span class="c1"&gt;# 这个例子中, price在下方定义了, id和title将委托到对应的AD模型上&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json_attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;price&lt;/span&gt;
    &lt;span class="no"&gt;Util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fen_to_yuan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;## 当调用DataService::Product.new(some_product.id).save 会更新redis实例数据&lt;/span&gt;
  &lt;span class="c1"&gt;## 将在redis中存储'data_service:product:#{model.id}' 为 {id: ..., title: ..., price: ...}&lt;/span&gt;

  &lt;span class="c1"&gt;### end 实例存储#####################################################&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="c1"&gt;### begin 关系存储##################################################&lt;/span&gt;

    &lt;span class="c1"&gt;# 关系型数据有和实例数据同样的设置API:&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json_attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# 可选设置, 默认是当前的class对应的顶级ActiveRecord Model, 用于和数据模型关联&lt;/span&gt;
    &lt;span class="c1"&gt;# self.model_class = ::Product&lt;/span&gt;
    &lt;span class="c1"&gt;# 可选设置, 默认是当前的class的小写形式, 最后存到redis的将是 'data_service:#{cache_key}:relation'&lt;/span&gt;
    &lt;span class="c1"&gt;# self.cache_key = 'product'&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;top&lt;/span&gt;
      &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;top_products_by_sales&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;## 当调用DataService::Product.save 更新关系数据&lt;/span&gt;
    &lt;span class="c1"&gt;## 将在redis中存储'data_service:product:relation' 为 {top: [...], count: ...}&lt;/span&gt;

    &lt;span class="c1"&gt;### end 关系存储配置#################################################&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ruby 普通对象和 Ruby 类对象的统一 API 大部分来自模块&lt;code&gt;Kernel&lt;/code&gt;, Data Service 中的实例型和关系型的统一 API 同样来自一个模块&lt;code&gt;DataService::DataApi&lt;/code&gt;, 感兴趣的同学可以查阅第一版&lt;code&gt;DataService::Base&lt;/code&gt;实现：&lt;a href="https://gist.github.com/zhongfox/f6ef4684f72215f837f7731f001ec16f" rel="nofollow" target="_blank" title=""&gt;base.rb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;除了统一 API, &lt;code&gt;DataService::DataApi&lt;/code&gt;还实现了对声明的&lt;code&gt;json_attributes&lt;/code&gt;的实现查找，默认它会在对应的 Activerecord 模型上查找，但是你可以在&lt;code&gt;DataService::Base&lt;/code&gt;对象上进行覆盖，比如上例中，实例数据的属性&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;是在 Activerecord 模型的实例上获取的，&lt;code&gt;DataService::Base&lt;/code&gt;中的&lt;code&gt;price&lt;/code&gt;覆盖了 Activerecord 模型上默认的&lt;code&gt;price&lt;/code&gt;方法。&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="Node.js 端dataService.fetch实现"&gt;Node.js 端 dataService.fetch 实现&lt;/h3&gt;
&lt;p&gt;Node.js 端的实现比较简单，npm package dataService 对外 export 唯一方法 &lt;code&gt;fetch&lt;/code&gt;, 前端同学通过此方法获取实例型和关系型数据。&lt;/p&gt;

&lt;p&gt;该方法首先查询 redis, 前端同学无需关心 redis 的数据解析，fetch 会将数据解析为期望的 json. 如果 redis 里没有数据将会自动调用 Ruby 提供的 miss 接口，不过这些逻辑都是隐藏在 fetch 之下的，前端同学不用关心。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * 通过传入资源 id 数组, 获取对应的deal的属性
 *
 * @public
 * @param {Object} query 查询信息对象
 *                - model: 字符串, 必要参数, 表示数据模型, 按照约定, 应该全部小写
 *                - ids: 字符串, 数字, 数组, 资源标识
 * @param {Object} 可选参数
 *                - cacheOnly 如果为true, 表示只走缓存, 不会触发miss
 *                - ns 表示namespace
 * @returns {Array/Object} 对于批量查询, 返回数组, 对于单个查询, 返回单个对象
 * @throws {Error} dataService 错误: model为空
 * @throws {Error} dataService 错误: ids为空
 * @throws {RedisError} Redis 超时或者错误
 */&lt;/span&gt;
&lt;span class="nx"&gt;dataService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;......&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;&lt;h3 id="系统收益"&gt;系统收益&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;项目解耦&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;引入数据中间层，Data Service 是 mysql 数据到项目实际需要的数据结构之间的转译，类似单体项目中的 ORM 的作用。&lt;/p&gt;

&lt;p&gt;前端 View 层解除了对 mysql 和后端接口的强依赖，即使 mysql/后端服务在短时间内挂掉 (真实发生过多次), Data Service 也是有缓存数据可供前端使用 (View 展示正常，可能数据有点旧).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;约定大于配置&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data Service 提供了默认的数据定义，默认的 Data Service 模型和 Activerecord 模型的对应关系，默认的 redis 存储结构，默认的 miss 容错机制。前后开发同学只需要把精力放到每次的业务实现上，不用重复考虑和实现这些通用的功能，这就是约定的好处。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;缓存异步刷新，高效且稳定&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data Service 将数据写入缓存和前端读取缓存完全解耦，使得后端可以异步对缓存进行刷新，比如上面的 Product 实例型数据，后端同学可以在&lt;code&gt;Product.after_commit&lt;/code&gt;中调用&lt;code&gt;DataService::Product.new(product.id).save&lt;/code&gt;进行缓存刷新，也可以通过 rake 定时刷新。&lt;/p&gt;

&lt;p&gt;缓存异步刷新的好处使得前端始终有可用的数据，也不会出现雪崩现象，以下是某页面接入 Data Service 后的效果，&lt;strong&gt;TP99&lt;/strong&gt;[^注 1] 显著降低，&lt;strong&gt;可用性&lt;/strong&gt;[^注 2] 明显提高。&lt;/p&gt;

&lt;p&gt;&lt;img src="//zhongfox.github.io/nodeppt/tp99/jinrishangxin.png" alt="今日上新" title="今日上新"&gt;&lt;/p&gt;
&lt;h3 id="其他"&gt;其他&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;数据抽象为关系型数据和实例型数据，灵感来源于 Ruby 的「类也是对象」&lt;/li&gt;
&lt;li&gt;这是一套小成本的优化：核心代码仅几百行; Redis 在前后端中已经广泛使用，没有引入新的中间件和复杂的概念; 简单统一的 API、出色的性能让系统推广十分顺利。&lt;/li&gt;
&lt;li&gt;Data Service 项目目标是统一管理 view 层可缓存的数据，大部分系统都是读多写少。对于推荐系统、用户签到、购买等写逻辑的数据流需要另外处理。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;

&lt;p&gt;[^注 1]: TP99: 关键的性能指标，所有请求响应时间降序排列，去掉 1% 的最高耗时，剩下 99% 的请求中的耗时最大值。&lt;/p&gt;

&lt;p&gt;[^注 2]: 可用性：关键的性能指标，HTTP 状态码 4XX, 5XX 以及响应时间超过 5s, 属于不可用，剩下的所有请求属于可用请求。&lt;/p&gt;</description>
      <author>zhongfox</author>
      <pubDate>Mon, 20 Feb 2017 12:55:12 +0800</pubDate>
      <link>https://ruby-china.org/topics/32337</link>
      <guid>https://ruby-china.org/topics/32337</guid>
    </item>
    <item>
      <title>记一次 Rails 项目异常排查</title>
      <description>&lt;h2 id="背景"&gt;背景&lt;/h2&gt;
&lt;p&gt;rails 项目&lt;code&gt;data_service&lt;/code&gt;, 在线上部署了 2 台机器，两台都是 rake 的执行器，&lt;code&gt;LX-host&lt;/code&gt; 和 &lt;code&gt;YJ-host&lt;/code&gt;, 前者执行 rake 正常，但是后者执行 rake 总是报错：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bundle exec rake public:home_banner RAILS_ENV=production
rake aborted!
NoMethodError: undefined method `find_special_banner' for #&amp;lt;Class:0x007f1e1c96d728&amp;gt;
.../data_service/vendor/bundle/ruby/2.1.0/gems/activerecord-4.2.6/lib/active_record/dynamic_matchers.rb:26:in `method_missing'
.../data_service/lib/data_service/public/home_banner.rb:11:in `block in index_background'
......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发同学在线下一直没法重现，因此怀疑是线上机器的问题，请求运维协助排查机器差异，但是线上机器系统，环境几乎一模一样，排查无果。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="项目结构"&gt;项目结构&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;data_service&lt;/code&gt; 是一个比较常规的 rails 4 项目，文件还是比较多，我只列出涉及到的文件结构，这是事后诸葛，开始排查时并不清楚哪些文件是有关联的：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── app
│&amp;nbsp;&amp;nbsp; ├── models
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── home_banner.rb
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── st_superscript.rb
├── config
│&amp;nbsp;&amp;nbsp; ├── initializers
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── data_service_monitor_register.rb
├── lib
│&amp;nbsp;&amp;nbsp; ├── data_service
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── base.rb
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── public
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── home_banner.rb
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── st_superscript.rb
│&amp;nbsp;&amp;nbsp; ├── tasks
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── public.rake
│&amp;nbsp;&amp;nbsp; └── website
│&amp;nbsp;&amp;nbsp;     └── util
│&amp;nbsp;&amp;nbsp;         ├── image_uploadable.rb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;略了很多不相关的文件。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="排查过程"&gt;排查过程&lt;/h2&gt;
&lt;p&gt;因为开发线下没有重现，因此登陆线上堡垒机排查，为了不影响现有服务，我将项目复制了一份到/tmp 下。&lt;/p&gt;

&lt;p&gt;rake 的入口在&lt;code&gt;lib/tasks/public.rake&lt;/code&gt;, 代码很简单：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tasks/public.rake&lt;/span&gt;
&lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s1"&gt;'首页品牌广告'&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;:brand_banners&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:environment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;DataService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BrandBanners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的错误栈中可以看到异常抛出点在&lt;code&gt;lib/data_service/public/home_banner.rb&lt;/code&gt;, 涉及代码如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/data_service/public/home_banner.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataService::HomeBanner&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;DataService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
&lt;span class="o"&gt;......&lt;/span&gt;
&lt;span class="n"&gt;banner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HomeBanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_special_banner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;banner_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_type&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
&lt;span class="o"&gt;......&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题很明显，类 HomeBanner 没有找到&lt;code&gt;find_special_banner&lt;/code&gt;这个类方法，HomeBanner 定义在&lt;code&gt;app/models/home_banner.rb&lt;/code&gt;, 这个文件比较长，有 692 行，看了代码，其中的确定义了类方法&lt;code&gt;find_special_banner&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/home_banner.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeBanner&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Concerns&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ImageUploadable&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="nf"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_special_banner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;banner_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;......&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="nf"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="类是否一致: Object#object_id"&gt;类是否一致：&lt;code&gt;Object#object_id&lt;/code&gt;
&lt;/h4&gt;
&lt;p&gt;首先想到的是&lt;code&gt;lib/data_service/public/home_banner.rb&lt;/code&gt; 中使用的 HomeBanner 和&lt;code&gt;app/models/home_banner.rb&lt;/code&gt;里定义的 HomeBanner 不是同一个类，因为&lt;a href="https://zhongfox.github.io/2013/03/21/ruby-constant-lookup/" rel="nofollow" target="_blank" title=""&gt;ruby 常量查找&lt;/a&gt;有一个比较复杂的过程，经常会出现查找不正确的情况。&lt;/p&gt;

&lt;p&gt;Ruby 提供了&lt;code&gt;Object#object_id&lt;/code&gt; 可以作为对象的标识，因此先在以上 2 个文件中打印：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/data_service/public/home_banner.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataService::HomeBanner&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;DataService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"1111111: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;HomeBanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# 调试代码&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"2222222: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HomeBanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# 调试代码&lt;/span&gt;
  &lt;span class="n"&gt;banner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HomeBanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_special_banner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;banner_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_type&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/home_banner.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeBanner&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"3333333: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;HomeBanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# 调试代码&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Concerns&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ImageUploadable&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="nf"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_special_banner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;banner_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;......&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="nf"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3333333: 70249085971460
1111111: 70249085971460
2222222: 70249085971460
rake aborted!
NoMethodError: undefined method `find_special_banner' for #&amp;lt;Class:0x007fc847a15808&amp;gt;
......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果显示 2 个类的 id 一致，排除常量查找问题。&lt;/p&gt;
&lt;h4 id="检视单键方法: Object#singleton_methods(all=true)"&gt;检视单键方法：&lt;code&gt;Object#singleton_methods(all=true)&lt;/code&gt;
&lt;/h4&gt;
&lt;p&gt;直接问题是找不到类方法，那就到类方法的定义处去测试一下，ruby 的类方法就是类的单键方法，可以使用&lt;code&gt;Object#singleton_methods(all=true)&lt;/code&gt;进行检视：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/home_banner.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeBanner&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"3333333: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;HomeBanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# 调试代码&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Concerns&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ImageUploadable&lt;/span&gt;
&lt;span class="o"&gt;......&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="nf"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_special_banner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;banner_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;......&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"all HomeBanner class methods: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;HomeBanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;singleton_methods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# 调试代码&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="o"&gt;.......&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3333333: 69955224157780
rake aborted!
NoMethodError: undefined method `find_special_banner' for #&amp;lt;Class:0x007f3f70927ca8&amp;gt;
......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过非常意外的是，最下面的调试代码在 rake 报错前没有任何输出，但是上面一行调试代码却是有输出的。排查了一会，基本可以确定，rake 在加载 HomeBanner 这个类的中途发生了异常 (在定义类方法前), 但是这个异常被吃掉了，导致 HomeBanner 加载发生异常没有抛出。最终抛出的却是使用 HomeBanner 时缺乏类方法的错误。&lt;/p&gt;
&lt;h4 id="确定代码调用栈 Kernel#caller"&gt;确定代码调用栈 &lt;code&gt;Kernel#caller&lt;/code&gt;
&lt;/h4&gt;
&lt;p&gt;异常被 rescue 但是没有正确处理或者上报，是导致调试困难的常见原因，麻烦的是根本不知道异常是在哪里吃掉的，也就不知道在加载 HomeBanner 时发生了什么异常。&lt;/p&gt;

&lt;p&gt;还好强大的 Ruby 提供了&lt;code&gt;Kernel#caller&lt;/code&gt;, 可以输出代码的调用栈。从上面的排查可以看出，HomeBanner 的确是加载并执行了，只是在中途停止了，没有加载完，因此可以在文件头打印出调用栈，看看是哪行该死的代码吃掉了异常：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/home_banner.rb&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="nb"&gt;caller&lt;/span&gt; &lt;span class="c1"&gt;# 调试代码&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeBanner&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Concerns&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ImageUploadable&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="nf"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_special_banner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;banner_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;......&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="nf"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vendor/bundle/ruby/2.1.0/gems/activesupport-4.2.6/lib/active_support/dependencies.rb:274:in 'require'
vendor/bundle/ruby/2.1.0/gems/activesupport-4.2.6/lib/active_support/dependencies.rb:274:in 'block in require'
vendor/bundle/ruby/2.1.0/gems/activesupport-4.2.6/lib/active_support/dependencies.rb:240:in 'load_dependency'
vendor/bundle/ruby/2.1.0/gems/activesupport-4.2.6/lib/active_support/dependencies.rb:274:in 'require'
vendor/bundle/ruby/2.1.0/gems/activesupport-4.2.6/lib/active_support/dependencies.rb:360:in 'require_or_load'
vendor/bundle/ruby/2.1.0/gems/activesupport-4.2.6/lib/active_support/dependencies.rb:494:in 'load_missing_constant'
vendor/bundle/ruby/2.1.0/gems/activesupport-4.2.6/lib/active_support/dependencies.rb:184:in 'const_missing'
lib/data_service/base.rb:81:in 'const_get'
lib/data_service/base.rb:81:in 'inherited'
lib/data_service/public/home_banner.rb:1:in '&amp;lt;top (required)&amp;gt;'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到项目加载 HomeBanner 的入口在&lt;code&gt;lib/data_service/base.rb&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/data_service/base.rb&lt;/span&gt;
&lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subclass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;demodulize&lt;/span&gt;
&lt;span class="n"&gt;top_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;const_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的确有&lt;code&gt;rescue&lt;/code&gt;!, 这个需求是当获取不到 name 对应的 Activerecord 模型，就直接返回 nil. 只是这里结果并不是找不到 HomeBanner, 而是找到 HomeBanner 后，加载时，HomeBanner 里有异常。&lt;/p&gt;
&lt;h4 id="检视异常堆栈: Exception#backtrace"&gt;检视异常堆栈：&lt;code&gt;Exception#backtrace&lt;/code&gt;
&lt;/h4&gt;
&lt;p&gt;rake 加载 HomeBanner 会出错，但是这个异常在 rails server 或者 rails console 里并没有重现，因此直接的办法是在异常点检视异常信息，Ruby 异常类 Exception 提供了丰富的检视方法，简化的调试代码如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/data_service/base.rb&lt;/span&gt;
&lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subclass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;demodulize&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="n"&gt;top_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;const_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backtrace&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uninitialized constant Concerns::ImageUploadable
app/models/home_banner.rb:4:in `&amp;lt;class:HomeBanner&amp;gt;'
app/models/home_banner.rb:3:in `&amp;lt;top (required)&amp;gt;'
lib/data_service/base.rb:84:in `const_get'
lib/data_service/base.rb:84:in `inherited'
lib/data_service/public/home_banner.rb:1:in `&amp;lt;top (required)&amp;gt;'
config/initializers/data_service_monitor_register.rb:4:in `block (2 levels) in &amp;lt;top (required)&amp;gt;'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终发现是&lt;code&gt;app/models/home_banner.rb&lt;/code&gt; 里&lt;code&gt;include Concerns::ImageUploadable&lt;/code&gt; 无法找到这个模块！&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;app/models/concerns/&lt;/code&gt; 下找了一下，并没发现这个模块，最后通过 grep 发现这个文件躺在&lt;code&gt;lib/website/util/image_uploadable.rb&lt;/code&gt;, 这是违反「约定大于配置」的后果。&lt;/p&gt;
&lt;h4 id="猜测与验证"&gt;猜测与验证&lt;/h4&gt;
&lt;p&gt;机器&lt;code&gt;YJ-host&lt;/code&gt;的&lt;code&gt;app/models/home_banner.rb&lt;/code&gt;无法找到&lt;code&gt;lib/website/util/image_uploadable.rb&lt;/code&gt;, 但是机器 &lt;code&gt;LX-host&lt;/code&gt;是正常的，因此到正常的机器&lt;code&gt;LX-host&lt;/code&gt;中，再次使用&lt;code&gt;caller&lt;/code&gt;定位到&lt;code&gt;lib/website/util/image_uploadable.rb&lt;/code&gt; 在另一个 model &lt;code&gt;app/models/st_superscript.rb&lt;/code&gt; 中 require 了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/st_superscript.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/lib/website/util/image_uploadable.rb"&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StSuperscript&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一感觉是这里的 require 不同寻常，&lt;code&gt;image_uploadable.rb&lt;/code&gt;是上传文件的通用 concern, 但是单单在其中一个使用 model&lt;code&gt;app/models/st_superscript.rb&lt;/code&gt;中 require, &lt;code&gt;app/models/home_banner.rb&lt;/code&gt; 中并没有 require.&lt;/p&gt;

&lt;p&gt;到这里基本上可以猜测到，正常机器&lt;code&gt;LX-host&lt;/code&gt; 是先加载了&lt;code&gt;app/models/st_superscript.rb&lt;/code&gt; 然后再加载了&lt;code&gt;app/models/home_banner.rb&lt;/code&gt;, 依赖的 concern 是在前者中进行 require, 而出错的机器&lt;code&gt;YJ-host&lt;/code&gt;的加载顺序刚好相反，因此导致&lt;code&gt;app/models/home_banner.rb&lt;/code&gt;无法找到依赖 concern.&lt;/p&gt;

&lt;p&gt;在两台机器的&lt;code&gt;st_superscript.rb&lt;/code&gt;和&lt;code&gt;home_banner.rb&lt;/code&gt;分别增加调试代码，两台机器打印结果的顺序不同，证明了之前的猜测。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/13bc58f60ff5cdab761c78651fa8fb21.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h4 id="文件加载顺序"&gt;文件加载顺序&lt;/h4&gt;
&lt;p&gt;相同的代码，在几乎相同的机器上，文件加载顺序不一致，这个比较罕见，不过可能的原因也有很多，很多 shell 命令都不保证文件遍历的顺序。&lt;/p&gt;

&lt;p&gt;通过&lt;code&gt;caller&lt;/code&gt; 继续确定 2 个 model 的加载入口，在文件&lt;code&gt;config/initializers/data_service_monitor_register.rb&lt;/code&gt;里：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/data_service_monitor_register.rb&lt;/span&gt;
&lt;span class="no"&gt;DataService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_prepare&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;
  &lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'lib/data_service/public/*.rb'&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;......&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;lib/data_service/public&lt;/code&gt; 下的&lt;code&gt;home_banner.rb&lt;/code&gt; 和&lt;code&gt;st_superscript.rb&lt;/code&gt;  会分别加载&lt;code&gt;app/models&lt;/code&gt;下的同名文件。&lt;/p&gt;

&lt;p&gt;看到&lt;code&gt;Dir&lt;/code&gt;, 猜测很大的可能是这个 api 返回的文件列表顺序不一致，查看 api 文档：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dir[ string [, string ...] ] → array
Equivalent to calling Dir.glob([string,...],0).

......

glob( pattern, [flags] ) → matches
glob( pattern, [flags] ) { |filename| block } → nil
Expands pattern, which is an Array of patterns or a pattern String, and returns the results as matches or as arguments given to the block.

Note that this pattern is not a regexp, it’s closer to a shell glob. See File.fnmatch for the meaning of the flags parameter. Note that case sensitivity depends on your system (so File::FNM_CASEFOLD is ignored), as does the order in which the results are returned.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到的是，glob 并没有保证返回的顺序，返回顺序和机器，系统相关！&lt;/p&gt;

&lt;p&gt;还是不放心？在两台机器的 irb 分别执行一下：&lt;/p&gt;

&lt;p&gt;出错的机器：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Dir['lib/data_service/public/*.rb'].grep /st_superscript|home_banner/
=&amp;gt; [
  "lib/data_service/public/home_banner.rb",
   "lib/data_service/public/st_superscript.rb"
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正确机器：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Dir['lib/data_service/public/*.rb'].grep /st_superscript|home_banner/
=&amp;gt; [
   "lib/data_service/public/st_superscript.rb",
  "lib/data_service/public/home_banner.rb"
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺序的确不同！&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="修复"&gt;修复&lt;/h2&gt;
&lt;p&gt;通过排查过程，可以发现，代码至少违反了以下最佳实践：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;吃掉异常，屏蔽掉了底层异常的现象，导致出错后排查困难。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;违反了「约定大于配置」, Concern 模块&lt;code&gt;image_uploadable.rb&lt;/code&gt; 按照约定应该放到&lt;code&gt;app/models/concerns/&lt;/code&gt;, 这个目录属于&lt;code&gt;eager_load_paths&lt;/code&gt;, 目录内的模块会被自动 require,  但是项目却把&lt;code&gt;image_uploadable.rb&lt;/code&gt;放到了&lt;code&gt;lib/website/util/image_uploadable.rb&lt;/code&gt;, 因此需要在使用的地方去手动 require, 但是没处理好加载顺序。导致异常。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;项目在开发测试阶段间接地依赖了&lt;code&gt;Dir&lt;/code&gt;返回的文件顺序，虽然项目本意并不关心&lt;code&gt;lib/data_service/public/&lt;/code&gt;下的文件加载顺序，程序在线下开发和测试测试阶段能正常运行完全是一个「巧合」. 如果依赖文件加载顺序，应该自己在结果后面进行 sort.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;解决法办法很简单：把&lt;code&gt;image_uploadable.rb&lt;/code&gt; 移到它应该在的位置&lt;code&gt;app/models/concerns/image_uploadable.rb&lt;/code&gt;, 问题就解决了。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;你真的理解「约定大于配置」的原因吗？你真的尊重「约定大于配置」吗？&lt;/p&gt;

&lt;p&gt;违反 rails 的「约定」会带来不必要的代价，concern 不在它该在的位置，没法自动加载，需要在 model 写奇怪的代码进行手动 require.&lt;/p&gt;

&lt;p&gt;这个问题发生在一两个月前，导致若干 rake 异常，只有部分机器报错。开发排查，运维排查，开发运维扯皮，最后迫不得已上线调试，前后也花了将近 2 个小时。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开发应该对「奇怪」的代码有反感。&lt;/p&gt;

&lt;p&gt;我觉得这里项目里最奇怪的代码是在&lt;code&gt;app/models/st_superscript.rb&lt;/code&gt;里，手动 require 了大部分 model 都要使用公共 Concern &lt;code&gt;require "lib/website/util/image_uploadable.rb"&lt;/code&gt;, 这应该是在开发途中，违反「约定大于配置」的一个补丁，开发应该对这种奇怪的代码多去刨根问底。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;对涉及 IO 遍历的返回值，需要考虑一下顺序是否会影响最终结果，如果 api 文档没有保证顺序，那么就是不确定的，自己加上&lt;code&gt;sort&lt;/code&gt;是个不赖的办法。不要让你的代码依赖「巧合」运行。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ruby 强大元编程非常利于调试，要善加利用。&lt;/p&gt;

&lt;p&gt;调试前后用到的主要技巧：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;判断对象一致性：&lt;code&gt;Object#object_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;检视单键方法：&lt;code&gt;Object#singleton_methods&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;确定代码调用栈：&lt;code&gt;Kernel#caller&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;检视异常堆栈：&lt;code&gt;Exception#backtrace&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;调试还是需要一些合理猜测和验证。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;尊重结果，按图索骥。&lt;/p&gt;

&lt;p&gt;以后遇到类似 bug, 不要轻易说「你的环境有问题吧？」 「在我这里可是好的」, 因为这一定是「你」的问题。 &lt;a href="http://blog.codinghorror.com/the-first-rule-of-programming-its-always-your-fault/" rel="nofollow" target="_blank" title=""&gt;The First Rule of Programming: It's Always Your Fault&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>zhongfox</author>
      <pubDate>Fri, 20 Jan 2017 17:55:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/32181</link>
      <guid>https://ruby-china.org/topics/32181</guid>
    </item>
  </channel>
</rss>
