<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Gogap</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="http://gogap.cn/"/>
  <updated>2017-09-21T06:54:16.000Z</updated>
  <id>http://gogap.cn/</id>
  
  <author>
    <name>Gogap</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Go1.7+中的BCE(Bounds Check Elimination)</title>
    <link href="http://gogap.cn/2017/09/21/go-1-7-bce/"/>
    <id>http://gogap.cn/2017/09/21/go-1-7-bce/</id>
    <published>2017-09-21T06:44:49.000Z</published>
    <updated>2017-09-21T06:54:16.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>自go1.7+，我们可以在编译时开启对有潜在slice越界访问风险的语句进行提示。</p></blockquote><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">f1</span><span class="params">(s []<span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line">_ = s[<span class="number">0</span>] <span class="comment">// line 5: bounds check </span></span><br><span class="line">_ = s[<span class="number">1</span>] <span class="comment">// line 6: bounds check </span></span><br><span class="line">_ = s[<span class="number">2</span>] <span class="comment">// line 7: bounds check </span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此处代码并未对slice的使用进行边界校验，容易发生危险，因为 <code>s []int</code> 尺寸未知。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">go build -gcflags=&quot;-d=ssa/check_bce/debug=1&quot; main.go</span><br><span class="line"></span><br><span class="line"># command-line-arguments</span><br><span class="line">./main.go:14:5: Found IsInBounds</span><br><span class="line">./main.go:15:6: Found IsInBounds</span><br><span class="line">./main.go:16:7: Found IsInBounds</span><br></pre></td></tr></table></figure><p>当我们把上面的代码修改为:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">f1</span><span class="params">(s []<span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(s) &lt; <span class="number">3</span> &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">_ = s[<span class="number">0</span>] <span class="comment">// line 5: bounds check</span></span><br><span class="line">_ = s[<span class="number">1</span>] <span class="comment">// line 6: bounds check</span></span><br><span class="line">_ = s[<span class="number">2</span>] <span class="comment">// line 7: bounds check</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再执行刚才的命令，就不会再提示有越界的可能了</p><p>原文地址(需梯子):</p><p><a href="http://www.tapirgames.com/blog/go-1.7-bce" target="_blank" rel="noopener">http://www.tapirgames.com/blog/go-1.7-bce</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;自go1.7+，我们可以在编译时开启对有潜在slice越界访问风险的语句进行提示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure class=&quot;highlight go&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>Go语言如何在没有实现功能的情况下写出完善的单元测试代码</title>
    <link href="http://gogap.cn/2017/09/20/go-mock-test/"/>
    <id>http://gogap.cn/2017/09/20/go-mock-test/</id>
    <published>2017-09-20T09:08:34.000Z</published>
    <updated>2017-09-20T09:13:50.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>最近在研究用Go写一个自己的解释型语言，有一本书叫《Writing An Interpreter In Go》, 作者在讲解如何编写解释器的时候，都是从写一个<code>_test.go</code>开始的，也就是说作者习惯于先写单元测试，以测试驱动开发，其实这是一个非常好的习惯，不过，作者在写<code>_test.go</code>文件的时候，都是先假设这个结构体、函数已经存在了，并且没有把关键的对象抽象成接口，因此，作者在运行<code>go test</code>的时候，是无法完成测试的，因为连编译都过不了，必须一边完善代码，一边重复运行<code>go test</code>，一直到完成开发。</p><p>基于这种开发模式下，其实我更期望能有一个Mock实现，写测试代码的时候畅通无阻，即使是没有实现，也能把各个测试用例覆盖到，当真实的实现完成后，我们只需要把mock实现替换成真实的实现就好了。</p><p>这么做还带来另一个好处，如果公司有SDET岗位，则可以直接让测试人员编写单元测试，开发任务和测试任务可以并行。</p><hr><h2 id="gomock-框架"><a href="#gomock-框架" class="headerlink" title="gomock 框架"></a>gomock 框架</h2><p>昨天闲着没事逛了逛 <a href="https://github.com/golang" target="_blank" rel="noopener">https://github.com/golang</a>, 发现了一个非常有意思的框架: <a href="https://github.com/golang/mock" target="_blank" rel="noopener">gomock</a>, 官方的描述是，这是一个mocking framework, 在使用上也很简单，大致的步骤如下：</p><p>1、定义一个待实现的接口.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> MyInterface <span class="keyword">interface</span> &#123;</span><br><span class="line">  SomeMethod(x <span class="keyword">int64</span>, y <span class="keyword">string</span>)</span><br><span class="line">  GetSomething() <span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2、使用<code>mockgen</code>生成mock代码.</p><p>3、测试:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestMyThing</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    mockCtrl := gomock.NewController(t)</span><br><span class="line">    <span class="keyword">defer</span> mockCtrl.Finish()</span><br><span class="line">    </span><br><span class="line">    mockObj := something.NewMockMyInterface(mockCtrl)</span><br><span class="line">    mockObj.EXPECT().SomeMethod(<span class="number">4</span>, <span class="string">"blah"</span>)</span><br><span class="line">    mockObj.EXPECT().GetSomething.Return(<span class="string">"haha"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看了这个步骤，想必大家应该猜到了，mocking framework 使用的是根据你的接口定义来自动生成一个Mock实现，我们还可以往这个实现里注入数据。</p><h3 id="期望接口调用参数"><a href="#期望接口调用参数" class="headerlink" title="期望接口调用参数"></a>期望接口调用参数</h3><p>我们可以通过 <code>.EXPECT()</code> 为这个mock对象注入期望值</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mockObj.EXPECT().SomeMethod(<span class="number">4</span>, <span class="string">"blah"</span>)</span><br></pre></td></tr></table></figure><ul><li>测试通过:</li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mockObj.SomeMethod(<span class="number">4</span>, <span class="string">"blah"</span>)</span><br></pre></td></tr></table></figure><ul><li>测试不通过:</li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mockObj.SomeMethod(<span class="number">5</span>, <span class="string">"bldah"</span>) <span class="comment">// 此处调用时直接会抛出错误</span></span><br></pre></td></tr></table></figure><p>对于参数类型的期望，在调用这个Mock函数的时候会直接抛错异常</p><h3 id="期望返回值"><a href="#期望返回值" class="headerlink" title="期望返回值"></a>期望返回值</h3><p><code>.EXPECT()</code> 同样可以为某个函数注入返回值</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mockObj.EXPECT().GetSomething().Return(<span class="string">"haha"</span>)</span><br></pre></td></tr></table></figure><ul><li>测试通过:</li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> <span class="string">"haha"</span> == mockObj.GetSomething() &#123;</span><br><span class="line">    <span class="comment">// -&gt; 执行到这里</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><ul><li>测试不通过:</li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> <span class="string">"haha"</span> == mockObj.GetSomething() &#123;</span><br><span class="line">    <span class="comment">// -&gt; 不会执行到这里</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>功能强的还不止这个，如果我们在测试一个循环，希望的是每次调用 <code>GetSomething()</code> 都返回不同的值，该怎么办？</p><p>答案很简单，依次调用</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">gomock.InOrder(</span><br><span class="line">    mockObj.EXPECT().GetSomething().Return(<span class="string">"A"</span>),</span><br><span class="line">    mockObj.EXPECT().GetSomething().Return(<span class="string">"B"</span>),</span><br><span class="line">    mockObj.EXPECT().GetSomething().Return(<span class="string">"C"</span>),</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>接下来，让我们来实战一下吧。</p><hr><h2 id="gomock-实战"><a href="#gomock-实战" class="headerlink" title="gomock 实战"></a>gomock 实战</h2><p>我们以《Writing An Interpreter In Go》这本书中的 <code>monkey</code> 语言的 <code>lexer</code> 作为例子</p><p>我们看一下 <code>monkey</code> 的目录结构：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">monkey&gt; tree .</span><br><span class="line">.</span><br><span class="line">├── lexer</span><br><span class="line">│   ├── lexer.go</span><br><span class="line">│   ├── lexer_test.go</span><br><span class="line">│   └── mock_lexer</span><br><span class="line">│       └── mock_lexer.go</span><br><span class="line">└── token</span><br><span class="line">    └── token.go</span><br></pre></td></tr></table></figure><h3 id="Lexer和Token"><a href="#Lexer和Token" class="headerlink" title="Lexer和Token"></a>Lexer和Token</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">type Lexer interface &#123;</span><br><span class="line">NextToken() token.Token</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>lexer的功能很简单，每次调用<code>NextToken()</code>，都是返回下一个<code>Token</code><br><code>Token</code>的结构<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">type TokenType string</span><br><span class="line"></span><br><span class="line">type Token struct &#123;</span><br><span class="line">Type    TokenType   // 类型</span><br><span class="line">Literal string      // 内容</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>比如下面的<code>go</code>语句</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> a = <span class="number">1</span></span><br></pre></td></tr></table></figure><p>lexter 在调用三次<code>NextToken()</code>后会得到三个Token, 依次是:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Token&#123;VAR, var&#125;</span><br><span class="line">Token&#123;IDENT, a&#125;</span><br><span class="line">Token&#123;INT, 1&#125;</span><br></pre></td></tr></table></figure><h3 id="测试思路"><a href="#测试思路" class="headerlink" title="测试思路"></a>测试思路</h3><p>其实测试方法就是：给定一段代码，用Lexer解析后，能得到指定顺序的Token，而<code>gomock</code>是完全可以实现的。</p><h3 id="使用gomock"><a href="#使用gomock" class="headerlink" title="使用gomock"></a>使用gomock</h3><h4 id="安装gomock"><a href="#安装gomock" class="headerlink" title="安装gomock"></a>安装gomock</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">go get github.com/golang/mock/gomock</span><br><span class="line">go get github.com/golang/mock/mockgen</span><br></pre></td></tr></table></figure><h4 id="生成mock代码"><a href="#生成mock代码" class="headerlink" title="生成mock代码"></a>生成mock代码</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mockgen -<span class="built_in">source</span> lexer.go -destination  mock_lexer/mock_lexer.go</span><br></pre></td></tr></table></figure><h3 id="编写lexer-test-go"><a href="#编写lexer-test-go" class="headerlink" title="编写lexer_test.go"></a>编写lexer_test.go</h3><h4 id="测试数据"><a href="#测试数据" class="headerlink" title="测试数据"></a>测试数据</h4><p>input: 输入的语句<br>tokens: 期望的Token</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">getTestData</span><span class="params">()</span> <span class="params">(input <span class="keyword">string</span>, tokens []token.Token)</span></span> &#123;</span><br><span class="line">input = <span class="string">`</span></span><br><span class="line"><span class="string">let five = 5;</span></span><br><span class="line"><span class="string">let ten = 10;</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line">tokens = []token.Token&#123;</span><br><span class="line">&#123;token.LET, <span class="string">"let"</span>&#125;,</span><br><span class="line">&#123;token.IDENT, <span class="string">"five"</span>&#125;,</span><br><span class="line">&#123;token.ASSIGN, <span class="string">"="</span>&#125;,</span><br><span class="line">&#123;token.INT, <span class="string">"5"</span>&#125;,</span><br><span class="line">&#123;token.SEMICOLON, <span class="string">";"</span>&#125;,</span><br><span class="line">&#123;token.LET, <span class="string">"let"</span>&#125;,</span><br><span class="line">&#123;token.IDENT, <span class="string">"ten"</span>&#125;,</span><br><span class="line">&#123;token.ASSIGN, <span class="string">"="</span>&#125;,</span><br><span class="line">&#123;token.INT, <span class="string">"10"</span>&#125;,</span><br><span class="line">&#123;token.SEMICOLON, <span class="string">";"</span>&#125;,</span><br><span class="line">&#123;token.EOF, <span class="string">""</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="生成一个真实的MonkeyLexer实例"><a href="#生成一个真实的MonkeyLexer实例" class="headerlink" title="生成一个真实的MonkeyLexer实例"></a>生成一个真实的MonkeyLexer实例</h4><p>当然，这里我们没有实现，所以返回是<code>nil</code></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newMonkeyLexer</span><span class="params">(input <span class="keyword">string</span>, excepts []token.Token, t *testing.T)</span> <span class="params">(l Lexer, deferFN <span class="keyword">func</span>()</span>)</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;&#125;</span><br><span class="line"><span class="comment">// return NewMonkeyLexer(input), func() &#123;&#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="构建MockLexer实例"><a href="#构建MockLexer实例" class="headerlink" title="构建MockLexer实例"></a>构建MockLexer实例</h4><p>由于没写完真正的lexer, 那么我们就开始Mock吧</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newMockLexer</span><span class="params">(input <span class="keyword">string</span>, excepts []token.Token, t *testing.T)</span> <span class="params">(l Lexer, deferFN <span class="keyword">func</span>()</span>)</span> &#123;</span><br><span class="line"></span><br><span class="line">ctrl := gomock.NewController(t)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 生成一个Mock实例</span></span><br><span class="line">mockLexter := mock_lexer.NewMockLexer(ctrl)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 将期望值一次传递给 NextToken()</span></span><br><span class="line">    <span class="comment">// 每次调用 NextToken() 也会依次获得期望值</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(excepts); i++ &#123;</span><br><span class="line">mockLexter.EXPECT().NextToken().Return(excepts[i])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">l = mockLexter</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用于清理</span></span><br><span class="line">deferFN = <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123; ctrl.Finish() &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了方便在mock实例和真实实例之间进行切换，我们可以通过环境变量来控制当前的测试实例是什么，如果要使用mock进行测试，我们只需要在运行 <code>go test</code> 前执行:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&gt; export GO_MOCK_TEST=<span class="number">1</span></span><br></pre></td></tr></table></figure><p>或</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&gt; GO_MOCK_TEST=<span class="number">1</span> <span class="keyword">go</span> test -v</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newLexer</span><span class="params">(input <span class="keyword">string</span>, excepts []token.Token, t *testing.T)</span> <span class="params">(l Lexer, deferFN <span class="keyword">func</span>()</span>)</span> &#123;</span><br><span class="line">env := os.Getenv(<span class="string">"GO_MOCK_TEST"</span>)</span><br><span class="line"><span class="keyword">if</span> env == <span class="string">"1"</span> &#123;</span><br><span class="line">t.Log(<span class="string">"MOCK TEST ENABLED!!!"</span>)</span><br><span class="line"><span class="keyword">return</span> newMockLexer(input, excepts, t)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> newMonkeyLexer(input, excepts, t)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以下是真正的测试代码，在没真实实现<code>monkey lexer</code>的情况下，我们可以写测试代码了，而且如果运行 <code>go test -v</code> 也是能通过的。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestNextToken</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line"></span><br><span class="line">input, excepts := getTestData()</span><br><span class="line"></span><br><span class="line">l, fn := newLexer(input, excepts, t) <span class="comment">// 生成 Lexer 对象</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">defer</span> fn() <span class="comment">// 清理</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i, tt := <span class="keyword">range</span> excepts &#123;</span><br><span class="line">tok := l.NextToken()  <span class="comment">// 获取下一个Token</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> tok.Type != tt.Type &#123;</span><br><span class="line">t.Fatalf(<span class="string">"tests[%d] - tokentype wrong. expected=%q, got=%q"</span>,</span><br><span class="line">i, tt.Type, tok.Type)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> tok.Literal != tt.Literal &#123;</span><br><span class="line">t.Fatalf(<span class="string">"tests[%d] - literal wrong. expected=%q, got=%q"</span>,</span><br><span class="line">i, tt.Literal, tok.Literal)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>下载完整的代码：<br><a href="https://github.com/xujinzheng/monkey" target="_blank" rel="noopener">https://github.com/xujinzheng/monkey</a></p></blockquote><h3 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h3><h4 id="使用mock实例进行测试"><a href="#使用mock实例进行测试" class="headerlink" title="使用mock实例进行测试"></a>使用mock实例进行测试</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">monkey&gt; <span class="built_in">cd</span> lexer</span><br><span class="line">lexer&gt; GO_MOCK_TEST=1 go <span class="built_in">test</span> -v</span><br><span class="line"></span><br><span class="line">=== RUN   TestNextToken</span><br><span class="line">--- PASS: TestNextToken (0.00s)</span><br><span class="line">PASS</span><br><span class="line">ok  github.com/xujinzheng/monkey/lexer0.007s</span><br></pre></td></tr></table></figure><h4 id="使用真实实例测试"><a href="#使用真实实例测试" class="headerlink" title="使用真实实例测试"></a>使用真实实例测试</h4><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newMonkeyLexer</span><span class="params">(input <span class="keyword">string</span>, excepts []token.Token, t *testing.T)</span> <span class="params">(l Lexer, deferFN <span class="keyword">func</span>()</span>)</span> &#123;</span><br><span class="line"><span class="keyword">return</span> NewMonkeyLexer(input), <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们将测试数据修改一下，假设 <code>ten=666</code>, 但不修改期望值，让测试报错</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">input = <span class="string">`</span></span><br><span class="line"><span class="string">let five = 5;</span></span><br><span class="line"><span class="string">let ten = 666;</span></span><br><span class="line"><span class="string">`</span></span><br></pre></td></tr></table></figure><p>再次运行测试</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">monkey&gt; <span class="built_in">cd</span> lexer</span><br><span class="line">lexer&gt; go <span class="built_in">test</span> -v</span><br><span class="line"></span><br><span class="line">=== RUN   TestNextToken</span><br><span class="line">--- FAIL: TestNextToken (0.00s)</span><br><span class="line">lexer_test.go:52: tests[8] - literal wrong. expected=<span class="string">"10"</span>, got=<span class="string">"666"</span></span><br><span class="line">FAIL</span><br><span class="line"><span class="built_in">exit</span> status 1</span><br><span class="line">FAILgithub.com/xujinzheng/monkey/lexer0.008s</span><br></pre></td></tr></table></figure><p>这里就报错了，说明我们的真实实例实现得有问题，需要修复这个BUG</p><p><code>gomock</code> 的使用到这里就结束了，除了上面介绍到的一些功能，<code>gomock</code> 还有很多其他丰富的方法，大家可以去 <a href="https://godoc.org/github.com/golang/mock/gomock" target="_blank" rel="noopener">GoDoc</a> 获取更详细的接口信息。</p><p>欢迎关注我的微信公众账号: <code>DeepIn-z</code></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h2&gt;&lt;p&gt;最近在研究用Go写一个自己的解释型语言，有一本书叫《Writing An Interpreter In Go》, 作者在讲解如何编写解释器的
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>用Go语言写个外挂（上）</title>
    <link href="http://gogap.cn/2017/05/13/%E7%94%A8Go%E8%AF%AD%E8%A8%80%E5%86%99%E4%B8%AA%E5%A4%96%E6%8C%82%EF%BC%88%E4%B8%8A%EF%BC%89/"/>
    <id>http://gogap.cn/2017/05/13/用Go语言写个外挂（上）/</id>
    <published>2017-05-13T06:49:12.000Z</published>
    <updated>2017-09-19T07:11:02.000Z</updated>
    
    <content type="html"><![CDATA[<p>本人在一家互联网金融公司上班，对于一家互联网金融公司，最基本的功能就是客户入金和出金，而出金的稳定性是很重要的，出金不畅容易导致投资人恐慌，本文讲的是出金，出金接口我们对接的是招商银行的银企直联系统，那么银企直连系统是一个什么样的程序呢？</p><p><img src="/images/waigua/1.png" alt="image.png"></p><p>没错，这个程序是运行在Windows上的，并且需要插入USBKey才能正常工作，这就意味着，不能简单的使用命令行进行运维管理。</p><p>看到这里，做运维的同学的内心应该和我一样是崩溃的。。</p><p><img src="/images/waigua/2.png" alt="image.png"></p><p>跟大家解释一下，这个服务是做什么的，大家可以把这个程序当成是我们的业务系统和招商银行沟通的信使，所有出金操作、查询操作都是通过这个信使来完成。</p><p>由于各种未知的原因，比如网络不稳定，或者USBKey插入时间过长产生了一些莫名其妙的错误，那么就需要人工去重启一下服务或重新登录一下账号，而且，这个工作有时候是在夜间操作的，这相当于要24小时待命啊，虽然故障频率不高，但这根弦始终是崩着的，这简直就是在破坏我的幸福美好生活啊。</p><p><img src="/images/waigua/3.png" alt="image.png"></p><p>这种体力活的事情，我坚决不能干，所以一定要交给别人干。</p><p><img src="/images/waigua/4.png" alt="image.png"></p><p>别想多了，【别人】也只能是个外挂而已，谁都不喜欢干这种人肉体力活。</p><p>所以凭借着我18岁那年的开发经验，脑子里想到了 Windows 的消息模型，使用 <code>SendMessage</code> 给对应的窗体控件句柄发送特定的事件不就搞定了么，异常自动重启使用 <code>CreateProcess</code> 不就行了吗？</p><p>天真的我脑子里已经充满了 <code>SendMessage</code> 的语句</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">LRESULT WINAPI <span class="title">SendMessage</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">  _In_ HWND   hWnd,</span></span></span><br><span class="line"><span class="function"><span class="params">  _In_ UINT   Msg,</span></span></span><br><span class="line"><span class="function"><span class="params">  _In_ WPARAM wParam,</span></span></span><br><span class="line"><span class="function"><span class="params">  _In_ LPARAM lParam</span></span></span><br><span class="line"><span class="function"><span class="params">)</span></span>;</span><br></pre></td></tr></table></figure><p>有木有很熟悉的样子，惊不惊喜，开不开心？是不是感觉发送键盘点击事件、鼠标点击事件就OK了？</p><blockquote><p>后面会讲到，其实还需要很多工作才能完成一个比较完善可用的外挂软件，<code>SendMessage</code> 基本上只能解决一部分问题</p></blockquote><p>然而当我想完这些代码后，感觉还是太麻烦，因为按键精灵这类软件就能解决，为什么还要自己亲自操刀？不过最终放弃了这种念头，因为这是一个很重要的服务，说不定在未来会掌握好 <code>几千个亿</code> 的资金命运，如果安装了不明软件，资金安全如何得以保障？？？绝对不能这么草草的做这种决定，所以还是决定老老实实的撸代码了。。。</p><p>用什么语言是个问题，在Windows上可以使用 <code>C++</code> , <code>C#</code> 系列，而且<code>C#</code>我记得有一个automation框架可以完成类似的操作，不过本人最近这3年一直在使用 <code>golang</code>，前两种语言目前也只是偶尔用用的节奏，所以基本处于手生的状态，而 <code>golang</code> 本身也支持使用 <code>syscall</code> 来调用 <code>windows</code> 的 <code>DLL</code>（动态链接库），所以果断使用 <code>golang</code>,  因为这个外挂大部分的WinAPI都在 <code>user32.dll</code> 和 <code>kernel32.dll</code> 里，我们只需要能加载这几个<code>DLL</code> 就可以调用强大的 <code>WinAPI</code> 了</p><p><img src="/images/waigua/5.png" alt="image.png"></p><blockquote><p>大家可以使用 PE Explorer 查看一个DLL有哪些输出函数</p></blockquote><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> (</span><br><span class="line">    moduser32 = syscall.NewLazyDLL(<span class="string">"user32.dll"</span>)</span><br><span class="line">    procSendMessage = moduser32.NewProc(<span class="string">"SendMessageW"</span>)</span><br><span class="line">    procPostMessage = moduser32.NewProc(<span class="string">"PostMessageW"</span>)</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SendMessage</span><span class="params">(hwnd HWND, msg <span class="keyword">uint32</span>, wParam, lParam <span class="keyword">uintptr</span>)</span> <span class="title">uintptr</span></span> &#123;</span><br><span class="line">ret, _, _ := procSendMessage.Call(</span><br><span class="line"><span class="keyword">uintptr</span>(hwnd),</span><br><span class="line"><span class="keyword">uintptr</span>(msg),</span><br><span class="line">wParam,</span><br><span class="line">lParam)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> ret</span><br><span class="line">&#125;</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>大家可以看到，在这里我们使用的是<code>SendMessageW</code>，而不是<code>SendMessageA</code>，因为<code>go</code>语言底层调用<code>DLL</code>接口时，传入的是<code>utf16</code>，看看下面的代码就明白了</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SetWindowText</span><span class="params">(hwnd HWND, text <span class="keyword">string</span>)</span></span> &#123;</span><br><span class="line">procSetWindowText.Call(</span><br><span class="line"><span class="keyword">uintptr</span>(hwnd),</span><br><span class="line"><span class="keyword">uintptr</span>(unsafe.Pointer(syscall.StringToUTF16Ptr(text))))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是一个设置窗体标题的API，第一个参数是窗体句柄，第二个参数大家可以看到，是将go语言的字符串转换成UTF16格式，并获取其指针。</p><p>另外值得注意的是，如果我们编译出来的程序是<code>32位</code>的，那么尽量不要用来作为<code>64位</code>程序的外挂，因为有很多复杂一点的功能无法实现，后续会提到这个部分，<code>银企直连</code> 这个服务是32位的，因此我们的go语言也是安装的32位的，同时为了更好的编译测试，我的虚拟机装的是 <code>Win2008 R2 32位</code> 操作系统</p><p>那么我们应该如何向一个窗体发送消息呢？能不能先做实验，不写代码呢？答案是肯定的，我们先请出我们的神器，<code>Spy++</code></p><p><img src="/images/waigua/6.png" alt="image.png"></p><blockquote><p>将瞄准器拖拽到具体的窗口上，就会得到窗口的句柄，我们可以通过 FindWindowW 或 EnumChildWindows 来实现相同的功能</p></blockquote><p>银企直连正常工作需要两个步骤</p><ol><li>启动HTTP服务监听</li><li>登录</li></ol><p>我们先看看启动HTTP监听按钮</p><p><img src="/images/waigua/7.png" alt="image.png"></p><p>我们使用<code>spy++</code>抓到了这个ToolBar的句柄</p><p><img src="/images/waigua/8.png" alt="image.png"></p><p>然后用 <code>spy++</code> 向第一个按钮发送鼠标点击事件，那么就可以开启监听了</p><p><img src="/images/waigua/9.png" alt="image.png"></p><p>点击动作在Windows消息来看，是分为两个动作，一个是 <code>WM_LBUTTONDOWN</code> 而另一个是 <code>WM_LBUTTONUP</code> ，所以我们需要发送两次事件，当完成这两次发送后，我们可以看到下面的界面</p><p><img src="/images/waigua/10.png" alt="image.png"></p><p>没错，其实这里是一个坑，启动监听还不好好启动，非得弹出一个消息框，同时伴随着的是<code>spy++</code>卡死了，为什么呢？ 因为我们使用的是<code>SendMessage</code>，这是一个同步的过程，因为出现了消息框，所以<code>spy++</code>还未收到返回消息，所以就卡死了。当我们点击完 确认 按钮后就可以恢复了，当然我们也可以使用 <code>PostMessage</code> ，不过这个接口只适合不在乎执行结果的情况下执行。</p><p>好了，这里我们出现了第一个坑：有弹窗，我们的外挂需要自动识别，并且能够自动关闭弹窗。</p><p><img src="/images/waigua/11.png" alt="image.png"></p><p>OK, 我们继续，我们该开始登陆了</p><p><img src="/images/waigua/12.png" alt="image.png"></p><p>刚才我们 <code>SendMessage</code> 里的WPARAM是1，那么，这个按钮是4</p><p><img src="/images/waigua/13.png" alt="image.png"></p><p>继续使用 <code>spy++</code> 发送消息</p><p><img src="/images/waigua/14.png" alt="image.png"></p><p>模拟完发送，整个人一下子就不好了，因为这个按钮根本就没有反应，后面的两个参数你也不知道到底传什么好，就在陷入了整个困局的时候，发现我们其实可以通过快捷键 <code>ctrl+b</code> 完成监听， <code>ctrl+i</code> 进入登录界面</p><p><img src="/images/waigua/15.png" alt="image.png"></p><blockquote><p>此时未插入USBKey</p></blockquote><p>所以，我们需要使用另外一个API: <code>SendInput</code>, 包括后面的密码输入，也一样要使用这个API</p><p>我们看一下这个API的定义</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">UINT WINAPI <span class="title">SendInput</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">  _In_ UINT    nInputs, <span class="comment">// 按键数量</span></span></span></span><br><span class="line"><span class="function"><span class="params">  _In_ LPINPUT pInputs, <span class="comment">// 按键内容数组</span></span></span></span><br><span class="line"><span class="function"><span class="params">  _In_ <span class="keyword">int</span>     cbSize <span class="comment">// 数组内容结构体的尺寸</span></span></span></span><br><span class="line"><span class="function"><span class="params">)</span></span>;</span><br></pre></td></tr></table></figure><p>看上去很心塞，一堆参数。</p><p><img src="/images/waigua/16.png" alt="image.png"></p><p>由于本文讲解的是调研篇，我们此处假设<code>SendInput</code>可以完成快捷键的按键模拟，密码输入的按键模拟，实际上这个API确实是可以工作的，因为这个接口是真实的模拟键盘输入，不针对某个窗口句柄。</p><p>接下来我们会迎来第二个坑，如果USBKey正常工作，那么用户名里的的内容是自动填写好的，如图：</p><p><img src="/images/waigua/17.png" alt="image.png"></p><p>这个用户名是从USBKey里读出来的，读取是需要时间的，因此我们可以在这里不停的向这个文本框发送<code>WM_GETTEXT</code> 消息，拿到用户名，如果用户名是预期的数据，我们就认为此时USBKey是正常工作的，否则如果长时间用户名未成功加载，则说明USBKey工作异常，应该发送报警信息。</p><p><img src="/images/waigua/18.png" alt="image.png"></p><p><img src="/images/waigua/19.png" alt="image.png"></p><p><img src="/images/waigua/20.png" alt="image.png"></p><p>我们大概会得到如下几类错误</p><ul><li>密码错误</li><li>通讯故障</li><li>USBKey有问题</li></ul><p>对于密码错误这个问题，我们的外挂应该立即停止工作，因为密码输入次数超过限制，USBKey将会锁定，公司出金服务就挂了。。。。</p><p><img src="/images/waigua/21.png" alt="image.png"></p><blockquote><p>为什么会密码输入错误呢？因为很有可能在自动输入时，被其他程序干扰了一下<br>我们在代码中会尽量用 <code>SetForegroundWindow</code> 让窗口保持在最前面，成为激活状态</p></blockquote><p>那么对于通讯故障，解决的办法就只能是重新尝试了</p><p>剩下的问题，我个人认为发出报警，人工处理一下会比较合适。</p><p>此时迎来两个新问题，</p><ol><li>我们如何知道消息框里的内容是什么</li><li>我们如何知道外挂登录成功了呢？</li></ol><p>对于第一个问题，我们可以通过 <code>EnumChildWindows</code> 来遍历这个消息框的孩子句柄，然后通过 <code>GetWindowText</code>  就可以知道是什么内容了。</p><p>我们重点来讨论第二个问题</p><p>此处有两种解法:</p><ol><li>向招行发起查询请求，如果能查询到数据，说明登录成功</li><li>检查登陆信息里的内容</li></ol><p><img src="/images/waigua/22.png" alt="image.png"></p><blockquote><p>登陆信息列表</p></blockquote><p>为了提升难度，我们选择方案2 </p><p><img src="/images/waigua/23.png" alt="image.png"></p><p>这种方法是比较困难的，有困难，我们要解决，没有困难我们也要创造困难来解决。。。。</p><p>为什么难呢？</p><p>因为我们没办法通过<code>SendMessage</code> 发送 <code>WM_GETTEXT</code> 事件获取内容，但是我们可以通过 <code>LVM_GETITEMTEXT</code> 来获取 listview 的列表内容</p><p>BUT….. 跨进程这么拿是拿不到的，同时，不同位数的进程，也是拿不到数据的。</p><p>如何解决？</p><p>我们需要使用API <code>VirtualAllocEx</code> 向银企直联进程申请一块内存空间，用于我们的外挂进程和银企直联进行数据沟通，当我们发送 <code>LVM_GETITEMTEXT</code> 消息之前，我们需要把参数信息写到这个内存块里，然后再使用<code>SendMessage</code>，ListView的数据会写到这个内存块，最后我们通过 <code>ReadProcessMemory</code> 来读取获取到列表的数据</p><p>这里就是为什么<code>32位</code>不能读<code>64位</code>程序的内容的原因了，虽然我们可以使用<code>WriteProcessMemory</code> 和 <code>ReadProcessMemory</code> 来写入和读取进程内存里的数据，但是由于通过这种机制进行交互，指针大小是不同的，通过<code>SendMessage</code>指令虽然能执行成功，但是回写的数据内容会跑飞。</p><p><img src="/images/waigua/24.png" alt="image.png"></p><blockquote><p>箭头代表数据流向，所有的API调用都是在外挂这边完成的</p></blockquote><p>整个流程大概就是这样的，我们需要借助远程进程的内存块来做数据交互，但最后切记一定要使用<code>VirtualFreeEx</code> 释放掉不用的内存块。</p><blockquote><p>此处应该有总结：</p></blockquote><ol><li>使用模拟键盘的方法开启监听和进入到登录界面而非<code>SendMessage</code></li><li>通过远程申请内存块的方式获取登录结果内容</li><li>需要判断弹出消息框的内容，用以判断是否有异常，同时需要关闭这些消息窗口</li></ol><p>到此为止，关键的技术内容我们已经调研完了，下一篇内容我们会讲如何使用go语言实现一个真正可用的外挂。<br>我们先来预览几个外挂的截图吧：</p><p>外挂工作中…..</p><p><img src="/images/waigua/25.png" alt="image.png"></p><p>当发生稳定性异常时，会通过bearychat的Incoming服务发送报警</p><p><img src="/images/waigua/26.png" alt="image.png"></p><p><img src="/images/waigua/27.png" alt="image.png"></p><p>欢迎关注我的微信公众号：<strong>DeepIn-z</strong></p><p><img src="/images/waigua/28.png" alt="image.png"></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;本人在一家互联网金融公司上班，对于一家互联网金融公司，最基本的功能就是客户入金和出金，而出金的稳定性是很重要的，出金不畅容易导致投资人恐慌，本文讲的是出金，出金接口我们对接的是招商银行的银企直联系统，那么银企直连系统是一个什么样的程序呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>Micro - 服务间的通讯浅析</title>
    <link href="http://gogap.cn/2016/06/01/micro-research-node-communicate/"/>
    <id>http://gogap.cn/2016/06/01/micro-research-node-communicate/</id>
    <published>2016-06-01T07:52:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<h4 id="传输层-Transport"><a href="#传输层-Transport" class="headerlink" title="传输层 (Transport )"></a>传输层 (Transport )</h4><p>目前官方支持的Transport</p><ul><li>HTTP</li><li>NATS</li><li>Rabbitmq</li></ul><p>Transport的作用</p><ul><li>将传输的内容统一成如下格式，并序列化成</li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Message <span class="keyword">struct</span> &#123;</span><br><span class="line">Header <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span></span><br><span class="line">Body   []<span class="keyword">byte</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>服务 Node 和 Node 之间的消息传输</li></ul><h4 id="协议-Codec"><a href="#协议-Codec" class="headerlink" title="协议 (Codec)"></a>协议 (Codec)</h4><p>目前支持的协议</p><ul><li>JSON</li><li>PROTOBUF</li></ul><p>服务底层的协议格式</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Message <span class="keyword">struct</span> &#123;</span><br><span class="line">Header <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span></span><br><span class="line">Body   []<span class="keyword">byte</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Q-amp-A"><a href="#Q-amp-A" class="headerlink" title="Q&amp;A"></a>Q&amp;A</h3><ol><li><em>两个服务节点之间是否可以支持使用不同的 <code>Transport</code></em></li></ol><blockquote><p>不支持，因为传输层是需要进行数据序列化和反序列化的，同时节点通讯其实是Transport的Client和Server进行通讯。（Server部分可以使用martini, gin, echo 等框架实现，但是他们同属于HTTP范畴，所以可以认定为相同的Transport）</p></blockquote><ol><li><em>服务于服务之间是如何传输的，如果客户端使用某种协议时，服务端是如何响应并解码的？</em></li></ol><blockquote><p>Micro 服务是基于 Transport 进行的，Transport 可以有多种实现方式，例如 RabbitMQ，NSQ，HTTP，NTATS……，由于不同的Transport有自己的协议，例如HTTP的协议我们基本可以认为他会分为 Header 和 Body 两个部分，因此，HTTP Transport 的实现就会把请求里的HTTP Request Header 作为 Message 里的 Header， Body 也直接从请求里获取，也就正好填充了 Message 这个结构体，Micro Server 根据 Header 里的 Content-Type 来确定使用哪一种 Codec 来进行解码，目前只有JSON和 ProtoBuf，Codec 的作用是将Body里的数据解码到具体的服务的协议。</p></blockquote>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h4 id=&quot;传输层-Transport&quot;&gt;&lt;a href=&quot;#传输层-Transport&quot; class=&quot;headerlink&quot; title=&quot;传输层 (Transport )&quot;&gt;&lt;/a&gt;传输层 (Transport )&lt;/h4&gt;&lt;p&gt;目前官方支持的Transport&lt;/p&gt;
      
    
    </summary>
    
      <category term="微服务" scheme="http://gogap.cn/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
  <entry>
    <title>多环境下的配置管理方案</title>
    <link href="http://gogap.cn/2016/05/31/multi-env-config/"/>
    <id>http://gogap.cn/2016/05/31/multi-env-config/</id>
    <published>2016-05-31T05:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>在开发中，我们需要面对各种各样的环境，开发环境、测试环境、生产环境……</p><p>并且，各个环境的参数和配置各不相同，比如数据库连接，服务器配置等。我们怎样在不同环境中调用正确的配置？</p><h2 id="通过配置文件"><a href="#通过配置文件" class="headerlink" title="通过配置文件"></a>通过配置文件</h2><p>这是一种常见的思路，通过创建多个配置文件，但根据命名区分，比如开发环境为<code>develop-app.conf</code>，测试环境为<code>testing-app.conf</code>，生产环境为<code>production-app.conf</code></p><p>我们通过在系统中设置环境变量<code>export ENV_MODE=develop</code>等等。在读取配置文件时，根据环境变量读取响应的配置文件。</p><p>这个方式易于使用，深得大家喜爱。但这个方案在集群扩大的一定程度时，会遇到一下几个主要问题：</p><ul><li>假如有30~40个微服务需要连接数据库运行，这个量级在中小型团队中很常见了，如果我们需要更改数据库密码，我们不得不将数十个project逐个进行更新，非常不灵活。</li><li>代码与配置掺杂在一起，代码是许多开发人员都可以看到的，也很容易泄露，而生产环境的各种秘钥应该只有少数人有权限能看到。这对系统的安全有重大影响。</li></ul><ul><li>对于大量相同的配置（比如数据库配置），逻辑上我们应该存放在同一个地方，保证只有唯一可靠的数据来源。</li></ul><p>对于这些问题，我们认为配置应该集中化管理。</p><p>集中化管理带来以下好处：</p><ul><li>各个服务间相同的配置只需要维护一分数据，保证唯一性</li><li>各个环境的配置环境实现权限隔离，少数人拥有查看生产环境配置的权限</li><li>更改配置将变得简单，不影响服务本身</li></ul><p>最简单的方案就是存储在<code>redis</code>中。KV的存储方式天然适合关联配置文件。但要完整的使用整个方案，需要做一些准备。</p><h2 id="集中式配置管理"><a href="#集中式配置管理" class="headerlink" title="集中式配置管理"></a>集中式配置管理</h2><p>我们的基本思路是：将配置文件的值替换为占位符，在系统启动时，相应的工具将根据占位符到<code>redis</code>中查询到实际的值，替换回配置文件。</p><p>最初的配置文件是这样：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  &quot;database_host&quot;:&quot;127.0.0.1&quot;,</span><br><span class="line">  &quot;database_port&quot;:3306</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在我们的配置文件变成了这样：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"database_host"</span>:<span class="string">"&#123;&#123;redis_hget "</span>global.mysql<span class="string">" "</span>host<span class="string">"&#125;&#125;"</span>,</span><br><span class="line">  "database_port":&#123;&#123;redis_hget "global.mysql" "port"&#125;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="读取配置"><a href="#读取配置" class="headerlink" title="读取配置"></a>读取配置</h2><p>在启动时，我们通过这个工具：<a href="https://github.com/gogap/env_json" target="_blank" rel="noopener">https://github.com/gogap/env_json</a></p><p>这样读取配置文件</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    data, _ := ioutil.ReadFile(<span class="string">"./db.conf"</span>)</span><br><span class="line">    dbConf := DBConfig&#123;&#125;</span><br><span class="line">    <span class="keyword">if</span> err := env_json.Unmarshal(data, &amp;dbConf); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        fmt.Print(err)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(dbConf)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个工具，默认从<code>/etv/env_string.conf</code>读取<code>redis</code>的配置信息，当然你可以更改，更多细节参看说明文档。</p><p>在这个过程中，<code>env_json</code>首先会从<code>/etv/env_string.conf</code>读取到<code>redis</code>的配置信息。</p><p>典型的<code>/etv/env_string.conf</code>内容如下</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"><span class="string">"storages"</span>: [&#123;</span><br><span class="line">        <span class="string">"engine"</span>: <span class="string">"redis"</span>,</span><br><span class="line">        <span class="string">"options"</span>: &#123;</span><br><span class="line">            <span class="string">"db"</span>: <span class="number">0</span>,</span><br><span class="line">            <span class="string">"password"</span>: <span class="string">""</span>,</span><br><span class="line">            <span class="string">"pool_size"</span>: <span class="number">10</span>,</span><br><span class="line">            <span class="string">"address"</span>: <span class="string">"127.0.0.1:6379"</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>连接上<code>redis</code>后，以上面的例子来说，将执行<code>hget global.mysql host</code>以及<code>hget global.mysql port</code>，将取到的值通过模板替换，更新到配置文件中，得到一个正常的<code>json</code>文本，剩下的就是通过<code>json</code>库把<code>json</code>内容解码到结构体中。</p><p>到目前为止，我们实现了从<code>redis</code>中读取并替换配置，那么我们写入配置的时候呢？</p><p>假如我们有数十个服务，我们难道需要逐个去<code>redis</code>中设置吗？我们怎样把这个流程自动化？</p><h2 id="写入配置"><a href="#写入配置" class="headerlink" title="写入配置"></a>写入配置</h2><p>我们需要另一个工具：<a href="https://github.com/gogap/env_strings/tree/master/env_sync" target="_blank" rel="noopener">env_sync</a></p><p>我们存储配置文件其实是一个具体的git工程，比如开发环境是<code>develop_env</code>,生产环境是<code>production_env</code>，开发人员都可以编辑<code>develop_env</code>这个工程，少数人可以编辑<code>production_env</code>。</p><p>工程里的内容什么呢？</p><p>我们约定了这样的目录结构</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">develop_env</span><br><span class="line">global.mysql    //this is folder</span><br><span class="line">         data       //this is file</span><br><span class="line">    components.accounts</span><br><span class="line">     data</span><br></pre></td></tr></table></figure><p>在工程中，有一系列的文件夹，文件夹中有一个叫data的文件。这样的目录结构会被<a href="https://github.com/gogap/env_strings/tree/master/env_sync" target="_blank" rel="noopener">env_sync</a>识别到，并转化成一系列的<code>redis</code>命令。</p><p>假如<code>global.mysql</code>文件夹下的<code>data</code>文件内容是</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"host"</span>:<span class="string">"127.0.0.1"</span>,</span><br><span class="line">  <span class="attr">"port"</span>:<span class="number">3306</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>转化出来的命令是：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hset global.mysql host 127.0.0.1</span><br><span class="line">hset global.mysql port 3306</span><br></pre></td></tr></table></figure><p>此过程与读取过程正好相反，同样的，<a href="https://github.com/gogap/env_strings/tree/master/env_sync" target="_blank" rel="noopener">env_sync</a>也是从<code>/etc/env_strings.conf</code>读取配置信息。与读取工具保持了统一。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>整体来看我们需要做几个工作</p><ul><li><p>为各个环境维护一个配置文件project</p></li><li><p>安装env_sync，便于同步配置文件到redis</p></li><li><p>设置<code>/etc/env_strings.conf</code></p></li><li><p>更改读取配置文件的代码，兼容env_json</p><p>​</p></li></ul><p>再结合<a href="http://lubia.cn/2016/05/30/deploy-micro/" target="_blank" rel="noopener">自动化部署工具</a>，每次配置文件有更新时，我们就在线上环境自动同步到redis。</p><h2 id="更多"><a href="#更多" class="headerlink" title="更多"></a>更多</h2><p>还有一种需求时，配置文件会动态变化，而我们不想重启服务就读取到配置文件，那你需要<a href="https://github.com/gogap/redconf" target="_blank" rel="noopener">https://github.com/gogap/redconf</a></p><p>这个工具可以实现对redis中数据的检测，如果数据发生变化，会触发回调，应用可以得到变化前后的值。 </p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在开发中，我们需要面对各种各样的环境，开发环境、测试环境、生产环境……&lt;/p&gt;
&lt;p&gt;并且，各个环境的参数和配置各不相同，比如数据库连接，服务器配置等。我们怎样在不同环境中调用正确的配置？&lt;/p&gt;
&lt;h2 id=&quot;通过配置文件&quot;&gt;&lt;a href=&quot;#通过配置文件&quot; clas
      
    
    </summary>
    
    
      <category term="CI" scheme="http://gogap.cn/tags/CI/"/>
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
  <entry>
    <title>怎样自动化部署微服务</title>
    <link href="http://gogap.cn/2016/05/30/deploy-micro/"/>
    <id>http://gogap.cn/2016/05/30/deploy-micro/</id>
    <published>2016-05-30T05:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>这篇文章【不是翻译】，是讨论我们团队在实际开发和运维过程中，怎样基于gitlab的CI系统和supervisor，进行微服务的自动化部署。</p><h2 id="CI"><a href="#CI" class="headerlink" title="CI"></a>CI</h2><p>持续集成的重要性不用多说，能够显著提高开发、部署和运维效率，但非侵入式的持续集成架构是很难的，此处分享我们在小型的开发团队中采用的持续集成方案。</p><h3 id="Gitlab"><a href="#Gitlab" class="headerlink" title="Gitlab"></a>Gitlab</h3><p>我们采用自建gitlab进行代码版本管理，通过docker进行搭建极其容易。目前的gitlab CI系统已经非常完善，可以针对特定的代码分支执行持续集成任务。</p><p>怎样安装gitlab-ci-runner参考<a href="/2016/05/28/gitlab-ci-runner/">这篇文章</a></p><h3 id="Supervisor"><a href="#Supervisor" class="headerlink" title="Supervisor"></a>Supervisor</h3><p>测试环境采用supervisor进行进程监控，保证应用挂掉之后能重启，且能正常的杀掉老的进程。</p><h2 id="Example"><a href="#Example" class="headerlink" title="Example"></a>Example</h2><p>以目前实现的一个监控组件monitor作为示例，分享怎样实现持续集成。</p><p>monitor是一个标准的micro服务，假设代码存放目录为</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$GOPATH/src/git.domain.com/micro/monitor</span><br></pre></td></tr></table></figure><p>monitor的代码目录如下</p><p> <img src="/images/micro-monitor-code.jpg" alt="micro-monitor-code"></p><p>其中与部署相关的是两个文件</p><p><code>.gitlab-ci.yml</code>是一个名字固定的文件，gitlab会根据这个文件名，来找到这个文件，将其中的内容根据分支设定，发送给runner执行，比如我们的文件内容是:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">develop:</span><br><span class="line">    script:</span><br><span class="line">    - &apos;cd $&#123;GOPATH&#125;/src/git.xxx.com/micro/monitor  //进入代码目录</span><br><span class="line">    &amp;&amp; git pull &amp;&amp; git checkout develop &amp;&amp; go build  //更新代码、切换到develop分支、编译</span><br><span class="line">    &amp;&amp; mkdir -p logs  //创建logs文件夹</span><br><span class="line">    &amp;&amp; supervisorctl -c ../../supervisord.conf update  //更新supervisor配置文件</span><br><span class="line">    &amp;&amp; supervisorctl -c ../../supervisord.conf restart monitor //重启服务</span><br><span class="line">    tags:</span><br><span class="line">      - micro   //将任务分发给有micro这个tag的runner执行</span><br><span class="line">    only:</span><br><span class="line">      - develop   //只监听develop分支</span><br></pre></td></tr></table></figure><p>注意，文件里的GOPATH是一个变量，这个变量在gitlab后台的monitor工程中设置，它是全局的，不用每个工程都设置，在某个工程设置一次即可。</p><p>具体参考<a href="/2016/05/28/gitlab-ci-runner/">这篇文章</a></p><p><code>`supervisord.conf</code>是supervisor的配置文件，supervisor的安装等等内容请参考<a href="http://supervisord.org/" target="_blank" rel="noopener">官网</a></p><p>内容如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">[program:monitor]</span><br><span class="line">directory=%(here)s/micro/%(program_name)s</span><br><span class="line">environment=RUN_MODE=%(ENV_RUN_MODE)s</span><br><span class="line">command=%(here)s/micro/%(program_name)s/%(program_name)s</span><br><span class="line">process_name=%(program_name)s</span><br><span class="line">autorestart = true</span><br><span class="line">startretries = 3</span><br><span class="line">redirect_stderr=true</span><br><span class="line">stdout_logfile_maxbytes = 20MB</span><br><span class="line">stdout_logfile_backups = 20</span><br><span class="line">stdout_logfile=%(here)s/micro/%(program_name)s/logs/%(program_name)s.log</span><br></pre></td></tr></table></figure><p>内容很简单，就是进去某个设定好的目录，执行某个命令。</p><p>实际效果：</p><p><img src="/images/ci-01.jpg" alt="micro-monitor-code"></p><p><img src="/images/ci-02.jpg" alt="micro-monitor-code"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本质上就是gitlab+supervisor的组合，需要一些细节的设计，开发的项目需要增加两个文件。部署的服务器需要设计好路径结构，否则可能会找不到文件。实际操作过程中有疑问欢迎给我发邮件。自动化部署如果想要运行起来，还需要另一个方面的配合 — 不同环境的配置文件问题，本地环境、开发环境、测试环境、生产环境的配置大部分情况下都不一样，怎样智能的读取响应环境的配置文件？这个问题我们也有自己的解决方案，在接下来的文章中我会进一步说明。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这篇文章【不是翻译】，是讨论我们团队在实际开发和运维过程中，怎样基于gitlab的CI系统和supervisor，进行微服务的自动化部署。&lt;/p&gt;
&lt;h2 id=&quot;CI&quot;&gt;&lt;a href=&quot;#CI&quot; class=&quot;headerlink&quot; title=&quot;CI&quot;&gt;&lt;/a&gt;CI&lt;
      
    
    </summary>
    
      <category term="CI" scheme="http://gogap.cn/categories/CI/"/>
    
    
      <category term="CI" scheme="http://gogap.cn/tags/CI/"/>
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
  <entry>
    <title>怎样使用gitlab-ci-runner</title>
    <link href="http://gogap.cn/2016/05/28/gitlab-ci-runner/"/>
    <id>http://gogap.cn/2016/05/28/gitlab-ci-runner/</id>
    <published>2016-05-28T07:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>这篇文件简单介绍，怎样安装使用gitlab-ci-runner，执行持续集成任务。</p><h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>gitlab-ci-runner是gitlab官方出品的持续集成工具，简单来说就是当你的代码触发了某个持续集成任务，运行在主机上的gitlab-ci-runner就会执行预先设计好的脚本。比如我们设计好，某个项目的develop分支有更新时，发送一段脚本到runner，这段事先写好的脚本，主要工作是进入这个项目的目录，git pull，编译，重启。这样就你只是推送了代码，但已经实现了简单的自动化部署。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>官方的安装文档在这里，非常简单，因为runner是采用golang编写的，所以你本质上只是下载了一个可执行文件，没有任何依赖项。按照你的平台选择即可：</p><p><a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner" target="_blank" rel="noopener">https://gitlab.com/gitlab-org/gitlab-ci-multi-runner</a></p><p>不必多说。</p><p>注：gitlab-ci-multi-runner和gitlab-ci-runner就是一个东西，两个名字而已。</p><h2 id="连接"><a href="#连接" class="headerlink" title="连接"></a>连接</h2><p>安装好以后，运行起来的效果应该类似这样</p><p><img src="/images/gitlab-ci-multi-runner-01.jpg" alt=""></p><p>注意，接下来的命令不要使用sudo，在linux环境下，如果使用sudo，在执行任务时会带来权限上的问题。</p><h3 id="注册runner"><a href="#注册runner" class="headerlink" title="注册runner"></a>注册runner</h3><p>接下来执行<code>gitlab-ci-multi-runner register</code>，进入交互式的页面，依次输入各个参数即可</p><p><img src="/images/gitlab-ci-multi-runner-02.jpg" alt=""></p><h3 id="激活runner"><a href="#激活runner" class="headerlink" title="激活runner"></a>激活runner</h3><p>执行<code>gitlab-ci-multi-runner verify</code></p><p><img src="/images/gitlab-ci-multi-runner-03.jpg" alt=""></p><h3 id="运行runner"><a href="#运行runner" class="headerlink" title="运行runner"></a>运行runner</h3><p>执行<code>gitlab-ci-multi-runner run &amp;</code></p><p>此时runner就已经运行起来，等待着gitlab发送任务。</p><p>此时在gitlab后台的runner页面中应该可以看到绑定成功的runner</p><p> <img src="/images/gitlab-ci-multi-runner-04.jpg" alt=""></p><h2 id="为项目绑定runner"><a href="#为项目绑定runner" class="headerlink" title="为项目绑定runner"></a>为项目绑定runner</h2><p>在gitlab进入某个需要进行持续集成的项目目录，<code>setting &gt; runners</code></p><p>为这个项目绑定runner，<code>ENABLE FOR THIS PROJECT</code></p><p><img src="/images/gitlab-ci-multi-runner-05.jpg" alt=""></p><h2 id="设置runner变量"><a href="#设置runner变量" class="headerlink" title="设置runner变量"></a>设置runner变量</h2><p>在某个项目的<code>setting &gt; variables</code>中，设置全局变量，注意这里设置的变量，所有项目都可以读取到。</p><p><img src="/images/gitlab-ci-multi-runner-06.jpg" alt=""></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这篇文件简单介绍，怎样安装使用gitlab-ci-runner，执行持续集成任务。&lt;/p&gt;
&lt;h2 id=&quot;介绍&quot;&gt;&lt;a href=&quot;#介绍&quot; class=&quot;headerlink&quot; title=&quot;介绍&quot;&gt;&lt;/a&gt;介绍&lt;/h2&gt;&lt;p&gt;gitlab-ci-runner是gitl
      
    
    </summary>
    
      <category term="CI" scheme="http://gogap.cn/categories/CI/"/>
    
    
      <category term="CI" scheme="http://gogap.cn/tags/CI/"/>
    
  </entry>
  
  <entry>
    <title>怎样设计友好的API接口</title>
    <link href="http://gogap.cn/2016/05/22/functional-options-for-friendly-apis/"/>
    <id>http://gogap.cn/2016/05/22/functional-options-for-friendly-apis/</id>
    <published>2016-05-22T05:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p><em>这篇文章是Dave Cheney在2014年发表的，我认为在go语言的接口设计上，这篇文章起到了指明灯的作用，包括<a href="https://github.com/micro" target="_blank" rel="noopener">Micro</a>在内的框架，都使用了这种方式提供API。原文看<a href="http://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis" target="_blank" rel="noopener">这里</a></em></p><p>正文开始：</p><p>下面的内容是我的一次演示的文字版本，这是我在<a href="http://www.dotgo.eu/" target="_blank" rel="noopener">dotGo</a>上演讲的『Functional options for friendly APIs』，在这里已经编辑的可读了。</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-032414.png" alt=""></p><p>我想用一个故事作为开头。</p><p>在2014年的晚些时候，你的公司发布了一款革命性的分布式社交网络工具，很明智的，你选择了Go来开发你的产品。</p><p>你分配到的任务是编写极为重要的服务端组件，看起来可能像这样</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-17-212315.png" alt=""></p><p>这里有一些不可导出的字段需要初始化，通过一个goroutine运行起来，响应请求。</p><p>这个包有很简单的API，非常容易使用。</p><p>但有一个问题，当你发布了你的第一个版本后，新的需求不断的被提出来。</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-032706.png" alt=""></p><p>手机客户端经常是响应的很慢，甚至停止响应。你需要添加支持来对慢的客户端主动断开连接。</p><p>为了增加安全，新的需求是增加安全连接（TLS）。</p><p>然后，你的某些用户是在一个很小的服务器上运行服务，他们需要限制客户端数量的方式。</p><p>下面是想要对并发数进行限制。</p><p>不断的新需求…</p><p>限制你需要调整你的API来满足这一系列的新需求</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-032801.png" alt=""></p><p> 还需要考虑不同版本直接接口的兼容性问题。</p><p>实话说，谁用过这样的API？</p><p>谁编写过这样的API?</p><p>谁的代码以为依赖了这样的包，而不能正常使用了？</p><p>明显的这种解决方式是笨重而脆弱的，同时也不容易发现问题。</p><p>你的包的新用户，不知道哪些参数是可选的，哪些是必须的。</p><p>比如说，如果我想创建一个服务的实例作为测试，我需要提供一个真实的TLS证书吗，如果不需要，我需要在接口中提供什么？</p><p>如果我不关心最大连接数，或者最大并发数，我应该在参数中设置什么值，我应该使用0？0听起来是合理的，但这依赖于具体的接口是怎样实现的，这也许真的会导致并发数限制为0。</p><p>在我看来，这样写API是容易的，同时你把正确使用接口的责任抛给了使用者。</p><p>这个例子甚至代码写的很糟糕，文档也不友好，我想这示范了一个看起来华丽，其实很脆弱的API设计。</p><p>现在我们定位了问题，我们看看解决方案。</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-032912.png" alt=""></p><p>与其提供一个单独的接口处理多种情况，一种解决方案是提供一系列的接口。</p><p>用户按需调用即可。</p><p>但你很快会发现，提供如此大量的接口，很快会让你不堪重负。</p><p>让我们看看另一种方式。</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-033016.png" alt=""></p><p>一种非常简单的方式是提供一个配置结构体。</p><p>这有一些优势。</p><p>使用这种方式，如果有新的需求加入，在结构体中增加选项即可。对外的公共API仍然保持不变。这也能让文档更加友好、可读。</p><p>在结构体上注明这是<code>NewServer</code>的参数，文档上也很容易识别。</p><p>潜在的它也允许用户使用0作为参数的值。</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-033123.png" alt=""></p><p>但是这种模式并不完美。</p><p>对于默认值是有歧义的，特别是0的值如果有特别的含义。</p><p>比如在这里的配置结构中，如果<code>port</code>没有被设置，<code>NewServer</code>会监听8080端口。</p><p>但是这有一个负面影响，你也许想设置为0，然后服务端默认分配一个随机端口，但你设置的0与默认值是相同的。</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-033220.png" alt=""></p><p>大部分时候，你的API用户只是想使用你的默认值。</p><p>即使他们不想改变你的配置的任何内容，仍然不得不传入一些参数。</p><p>当你的用户读你的测试代码或者示例代码时，在想着怎样使用你的包，他们会看到这个魔幻的空字符串参数。</p><p>对我来说，这让我感觉很糟糕。</p><p>为什么你的API的用户需要传入一个空的值，只是简单的让你的函数满足声明需求？</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-033508.png" alt=""></p><p>一个常见的解决办法是传入一个结构体指针，这让调用者可以传入<code>nil</code>，而不用考虑空值的问题。</p><p>在我看来，这个方案有前面的示例中的所有问题，甚至让问题更复杂了。</p><p>首先，我们仍然需要在第二个参数传入点什么，但目前，这个参数可以是nil了，而且大部分时候，对于默认的使用者，它就是nil。</p><p>使用指针的方式，包的作者和使用者都会担心的是，他们引用了同一份数据，随时有可能在运行中这份数据被修改而发生突变。</p><p>我想设计精良的API不应该要求用户传递这些额外的参数，只是为了应对一些罕见的情况。</p><p>我认为我们，Go程序员，应该努力确保不要求用户传递一个<code>nil</code>作为参数。</p><p>如果我们想要传递配置信息时，这应该是自解释的，尽量的有表达性。</p><p>现在，我们怀着这样的理念，我讨论一下我认为更好的解决方案。</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-033605.png" alt=""></p><p>我们可以让API把不必须的参数作为一个变参。</p><p>不是传入nil，或者一些值为0的结构体，这种函数的设计发出了这样的信号：你不需要在config上传入任何参数。</p><p>在我看来这解决了两个问题。</p><p>首先，默认的调用方式变得简介命了。</p><p>其次，<code>NewServer</code>现在只接受config的值，不是指针，移除了<code>nil</code>和其他可能的参数，确保用户不会修改已经传入的参数。</p><p>我认为这个一个巨大的提升。</p><p>但我们深究一下，这仍然有问题。</p><p>明显对你的预期是提供最多一个config值，但这个参数是变参，实现的时候需要考虑用户传入多个参数的情况。</p><p>我们可以既能使用变参，同时也能提高我们的参数的表达性吗？</p><p>我认为这就是结局方案。</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-033713.png" alt=""></p><p>在这里我想要说清楚，函数式参数的想法是来自于Rob Pike的这篇文章：<a href="http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html" target="_blank" rel="noopener">Self referential functions and design</a> ，我鼓励每个人都去看看。</p><p>这种方式与上面的例子关键的不同在于，服务的定制化并不是通过传递参数实现的，而是通过函数来直接修改<code>server</code>的配置本身。</p><p>正如前面看到的，不传递变参让我们使用默认的方式。</p><p>当需要进行配置时，我们传递一个操作<code>server</code>的配置的函数。</p><p>上面的代码中，<code>timeout</code>这个函数是用于改变<code>server</code>的配置中的timeout字段。</p><p><img src="http://dave.cheney.net/wp-content/uploads/2014/10/Screenshot-from-2014-10-15-033937.png" alt=""></p><p>在<code>NewServer</code>的实现内部，直接应用这些函数即可。</p><p>在上面的代码中，我们调用了一个<code>net.Listener</code>，在server的示例中，我们使用了这个默认的listener。</p><p>然后，对于每个传入的option，我们都调用它，把我们的配置传入进去。</p><p>很明显，如果没有option传递进来，我们就使用的是默认的server.</p><p>使用这种方式，我们可以让API有这样的特性</p><ul><li>默认情况是实用的</li><li>高度可配置</li><li>配置可以不断增长</li><li>自解释的文档</li><li>对新的使用者很安全</li><li>不会要求传入一个nil的或者空值（只是为了让编译通过）</li></ul><p>全文完。</p><p><a href="https://github.com/micro" target="_blank" rel="noopener">Micro</a>在几乎所有接口中使用了这样的方式，比如要创建一个micro server的实例，开发者通过一个<a href="https://github.com/micro/go-micro/blob/master/server/options.go" target="_blank" rel="noopener">option.go</a>提供了所有可能的配置函数，当然你也可以自己实现。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;&lt;em&gt;这篇文章是Dave Cheney在2014年发表的，我认为在go语言的接口设计上，这篇文章起到了指明灯的作用，包括&lt;a href=&quot;https://github.com/micro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Micro&lt;/a&gt;在
      
    
    </summary>
    
      <category term="Go" scheme="http://gogap.cn/categories/Go/"/>
    
    
      <category term="Go" scheme="http://gogap.cn/tags/Go/"/>
    
  </entry>
  
  <entry>
    <title>使用Micro构建有弹性的、高容错的应用</title>
    <link href="http://gogap.cn/2016/05/15/resiliency/"/>
    <id>http://gogap.cn/2016/05/15/resiliency/</id>
    <published>2016-05-15T07:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>这是一系列介绍<a href="http://github.com/micro" target="_blank" rel="noopener">Micro</a>框架的文章的第七篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。</p><p>自发表上篇文章以来，已经有一段时间了，但我们仍然努力在为Micro添砖加瓦，现在确实需要还债了，让我们一次解决掉。</p><p>如果你想先了解一下 <a href="https://github.com/micro/micro" target="_blank" rel="noopener"><strong>Micro</strong></a>工具箱，阅读以前的文章即可。</p><p>构建分布式系统是很有挑战性的，这毫无疑问。虽然我们已经解决了很多工程上的问题，我们仍然重复的在构建许多模块。目前，由于我们开始了更高级别的抽象，虚拟机到容器技术，适应新的语言，作用于云计算，都对微服务提出了要求。总有一些事情需要我们不断的去学习，怎样构建高性能的、高容错的系统仍然是下一波的技术浪潮。</p><p>重复与创新之间的战争从未停止，但我们需要做一些事情，通过云计算、容器技术、微服务来缓解我们的痛苦。</p><h2 id="动机"><a href="#动机" class="headerlink" title="动机"></a>动机</h2><p>我们为什么这样做？为什么我们持续的重新构建同样的模块，为什么我们持续的尝试解决大规模、容错性和分布式系统的问题？</p><p>我脑海中出现的是『<em>bigger, stronger, faster</em>』，或者是『<em>speed, scale, agility</em>』，你可能经常从C级别的管理人员口中听到这个说法。但关键的是，确实存在这样的需求，需要我们构建更加高性能和要弹性的系统。</p><p>在互联网的早期，只有数千或者数万用户在线，随着时间的推移，我们看到开始加速，现在我们面对的是数十亿用户和数十亿的设备。我们需要学习怎样为目前的情况构建系统。</p><p>上一代的人也许记得<a href="http://www.kegel.com/c10k.html" target="_blank" rel="noopener">C10K problem</a>，我不确定我们现在处在什么阶段，但我想我们现在谈论的是百万级的并发。世界上最大的技术公司，在10年前就已经解决了这个问题，也有了模式来构建这样大规模的系统。但剩下的其他人仍然在学习。</p><p>像Amazon，Google，Microsoft现在提供给我们的云计算平台，对大规模部署是有益的。但我们仍然努力在搞清楚，怎样编写应用程序，可以高效的利用这些大规模的资源。你也许这些天听说了容器的编排，微服务、云计算很多了。工作在很多层面上推进着，当我们完全确定了我们的模式，确定了需要解决的问题，我们的Micro就能作为工业级的产品发布了，这还需要一段时间。</p><p>许多公司现在求助的问题是『我该怎样构建可扩展的、高容错的系统？』但目前对这些重要的问题，有帮助的回答是很少的。</p><p>我该怎样编写可扩展的、高容错的系统？</p><p>Micro看起来通过专注于微服务的必要的软件开发工具，定位了问题。我们会详细的谈谈怎样帮助你构建有弹性的、高容错的系统，我们从client端开始。</p><h2 id="客户端"><a href="#客户端" class="headerlink" title="客户端"></a>客户端</h2><p>客户端在go-micro中是用于发起请求的模块，如果你已经构建过微服务挥着SOA架构，你会知道重要的一部分时间和执行过程是花在调用其他服务获取相关信息上面。</p><p>然而在巨大的应用中，关注点主要在接受请求返回内容，在微服务世界中，更像是取回或者发布内容。</p><p>这里是精简过的go-micro中client的接口，有最重要的三个方法Call, Publish  和 Stream。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Client <span class="keyword">interface</span> &#123;</span><br><span class="line">Call(ctx context.Context, req Request, rsp <span class="keyword">interface</span>&#123;&#125;, opts ...CallOption) error</span><br><span class="line">Publish(ctx context.Context, p Publication, opts ...PublishOption) error</span><br><span class="line">Stream(ctx context.Context, req Request, opts ...CallOption) (Streamer, error)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Request <span class="keyword">interface</span> &#123;</span><br><span class="line">Service() <span class="keyword">string</span></span><br><span class="line">Method() <span class="keyword">string</span></span><br><span class="line">ContentType() <span class="keyword">string</span></span><br><span class="line">Request() <span class="keyword">interface</span>&#123;&#125;</span><br><span class="line">Stream() <span class="keyword">bool</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Call和Stream是用来做同步通信请求，Call返回一个单一的结果，而Stream是一个双向的流式连接，与另一个服务维持着，其中任何消息都可以发进来也可以发出去。Publish用于发布异步的消息，通过broker，但我们今天不讨论它。</p><p>客户端是怎样工作的，前面的文章已经讨论过了。翻看以前的文章即可。</p><p>我们只是特别的讨论一些重要的内部细节。</p><p>客户端使用RPC层，结合broker，codec，register，selector和transport来提供丰富的组合。分层的架构非常重要，所以我们可以把单个的组件进行分离，减少了整体的复杂性，也提供了插件化的能力。</p><h2 id="为什么客户端重要"><a href="#为什么客户端重要" class="headerlink" title="为什么客户端重要?"></a>为什么客户端重要?</h2><p>客户端本质上抽象了与服务端之间的有弹性的、高容错的通信过程。像另一个服务发起请求看起来是非常直接的，但有几种情况是可能潜在的发生失败的。</p><p>下面我们开始了解这些功能和他们是怎样运作的。</p><h2 id="服务发现"><a href="#服务发现" class="headerlink" title="服务发现"></a>服务发现</h2><p>在分布式系统中，服务因为各种原因会频繁的加入和脱离集群。网络隔离、机器故障、调度等等。我们并不真正想关心它们。</p><p>当我们像另一个服务发起请求，我们通过名字识别服务，并允许客户端通过服务发现获取到服务的一系列实例，得到各个实例的地址和端口。服务在启动时在服务发现中心进行注册，在退出时进行注销。</p><p><img src="https://blog.micro.mu/assets/images/discovery.png" alt=""></p><p>正如我们提到的，任何类型的问题都会出现在分布式系统，服务发现也不例外。所以我们依赖于经过严格测试的分布式服务发现系统，例如consul、etcd和zookeeper，使用它们存储服务的信息。</p><p>它们都使用基于Paxos的Raft算法来进行网络选举，这解决了我们的一致性问题。通过运行一个3到5个节点的集群，我们可以容忍大部分的系统故障，为客户端提供稳定可靠的服务。</p><h2 id="节点选择"><a href="#节点选择" class="headerlink" title="节点选择"></a>节点选择</h2><p>现在我们可靠的把服务名字解析到了一堆地址列表。我们怎样选择其中的一个进行调用呢?这就是go-micro中的selector发挥作用的地方。它基于register模块构建，提供负载均衡策略，比如轮询或者随机，也提供过滤、缓存和黑名单的功能。</p><p>这里是定义的接口：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Selector <span class="keyword">interface</span> &#123;</span><br><span class="line">Select(service <span class="keyword">string</span>, opts ...SelectOption) (Next, error)</span><br><span class="line">Mark(service <span class="keyword">string</span>, node *registry.Node, err error)</span><br><span class="line">Reset(service <span class="keyword">string</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Next <span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="params">(*registry.Node, error)</span></span></span><br><span class="line"><span class="function"><span class="title">type</span> <span class="title">Filter</span> <span class="title">func</span><span class="params">([]*registry.Service)</span> []*<span class="title">registry</span>.<span class="title">Service</span></span></span><br><span class="line"><span class="function"><span class="title">type</span> <span class="title">Strategy</span> <span class="title">func</span><span class="params">([]*registry.Service)</span> <span class="title">Next</span></span></span><br></pre></td></tr></table></figure><h2 id="负载策略"><a href="#负载策略" class="headerlink" title="负载策略"></a>负载策略</h2><p>当前的策略是非常简单直接的，当Selector被调用时，它从register获取到服务，然后创建一个Next函数，从节点池中选择出符合要求的节点。</p><p>客户端会调用这个Next函数，根据负载均衡策略，获取到下一个符合要求的节点，并发出请求。如果这个请求失败了，而且重试次数大于1，它会使用相同的程序，获取下一个节点，再次调用。</p><p>这里是有很多不同的策略的，比如轮询、随机、最少连接、权重等等。负载均衡策略对于分布式系统是必不可少的。</p><h2 id="缓存"><a href="#缓存" class="headerlink" title="缓存"></a>缓存</h2><p>虽然有一个可靠的服务发现系统是很好的，但每次请求都去查询一次并不高效。如果你想象一个大规模的系统，每个服务都这样做，很容易就会使服务发现系统过载。这会让整个系统不可用。</p><p>为了避免这种情况，我们可以使用缓存。大部分的服务发现系统提供了一个监听更新的机制，一般来说叫做Watcher。不是去轮询服务发现系统，而是等待事件发送给我们。go-micro 的Registry提供了Watch的概念。</p><p>我们已经编写了一个带缓存的selector，它把服务缓存在内存中。如果缓存中不存在时，它会去服务发现系统查找，缓存，并用于之后的请求。如果watch事件收到了，缓存模块会与register进行更新。</p><p>首先，通过移除服务查找，大大的提高了性能。这也提供了一定的容错，万一服务发现系统宕机了呢？我们仍然有一点偏执，害怕缓存由于节点发生故障而被污染了，所以节点都维持着合适的TTL。</p><h2 id="黑名单"><a href="#黑名单" class="headerlink" title="黑名单"></a>黑名单</h2><p>下面介绍一下黑名单，注意一下Selector的接口有Mark和Reset方法。我们不同真正的保证，注册进来的节点都是健康的，所以我们需要做黑名单。</p><p>任何一个请求发送之后，我们都会跟踪它的结果。如果这个服务的实例出现了多次失败，我们就可以大体上把这个节点加入黑名单，并过滤掉它。</p><p>节点在回到节点池之前会在黑名单中会存在一段时间，这是很严格的，如果这个节点失败了我们就需要移除掉它。这样我们可以持续的返回成功的请求，不会有任何延迟。</p><h2 id="超时与重试"><a href="#超时与重试" class="headerlink" title="超时与重试"></a>超时与重试</h2><p>Adrian Cockroft最近开始讨论在微服务架构中消失的组件，其中一个有意思的是，传统的超时和重试策略导致了雪崩效应。我建议你看看这个<a href="http://www.slideshare.net/adriancockcroft/microservices-whats-missing-oreilly-software-architecture-new-york#24" target="_blank" rel="noopener">演示</a>。这个演示把问题总结的特别好。</p><p><img src="https://blog.micro.mu/assets/images/timeouts.png" alt=""></p><p>Adrian 在上面描述的是一种常见的情况，一个缓慢的响应会导致超时，然后客户端会触发重试。这事实上是一个请求链路，这创造了一系列的新请求，而旧有的请求仍然在处理中。这样的配置失误会导致大量服务的过载，造成的调用失败是很难回滚的。</p><p>在微服务世界，我们需要重新想想，处理重试和超时的策略。Adrian继续讨论了潜在的解决方案。其中一种方式是超时之后，在新的节点上发起请求。</p><p><img src="https://blog.micro.mu/assets/images/good-timeouts.png" alt=""></p><p>在重试的这方面，我们已经在Micro中使用了。重试的次数可以进行配置，如果你调用了一个失败的节点，客户端会在新的节点发起重试。</p><p>超时经常被深思熟虑，但事实上经常从传统的静态超时设置开始。直到Adrian演示了他的想法，超时策略变得很清晰了。</p><p>预算型超时策略现在也内置在Micro中，让我们看看它是怎样工作的。</p><p>第一个调用设置了超时，每个调用链上的请求都会消耗整体的超时时间。如果时间为0了，我们就会停止请求或重试，并返回调用链。</p><p>按照Adrian提到的，提供动态的预算型超时是非常好的，避免了不必要的雪崩。</p><p>更远一点来说，下一步应该是移除任何类型静态的超时。服务的响应时间根据环境的不同，请求的不同是不同的。这应该是动态的SLA，根据当前的状态进行调整，但这些事会留在未来解决。</p><h2 id="连接池"><a href="#连接池" class="headerlink" title="连接池"></a>连接池</h2><p>连接池是构建可扩展系统的很重要部分，我们很快就看到了没有连接池的局限性，经常导致文件描述符数量达到限制，导致端口用尽。</p><p>目前有个进行中的<a href="https://github.com/micro/go-micro/pull/86" target="_blank" rel="noopener">PR</a>为go-micro增加了连接池，由于Micro插件化的特性，把连接池放在transport的上层很重要，这样HTTP，NATS，RabbitMQ等等，都会受益。</p><p>你也许会想，这是特定实现的，一些transport也许已经支持了。这是对的，不能总是保证在不同的transport下工作效果是一样的。通过把这个放置于上层，我们减少了transport模块的复杂性。</p><h2 id="其他"><a href="#其他" class="headerlink" title="其他?"></a>其他?</h2><p>确实有很多好用的东西是go-micro内置的，那么还有什么呢？我很高兴你这么问…</p><h2 id="服务版本"><a href="#服务版本" class="headerlink" title="服务版本"></a>服务版本</h2><p>我们有这个功能，这个功能在前面的文章也讨论过了。服务包括名字和版本，注册在服务发现系统。当一个服务从注册器中查询出来时，它的节点是按照版本分组的。这样一样，selector就可以根据版本，进行流量负载。</p><p><img src="https://blog.micro.mu/assets/images/selector.png" alt=""></p><h3 id="为什么版本很重要"><a href="#为什么版本很重要" class="headerlink" title="为什么版本很重要"></a>为什么版本很重要</h3><p>当我们发布新版本时，这非常重要，它可以确保所有事情运作正常，这样才能把所有服务进行升级。新版本可以被部署到一个小型的节点上，客户端会自动的分发一定比例的请求到这个新的节点。通过结合一些编排系统比如Kubernetes，你可以非常有信心的部署，一旦有任何问题也可以回滚。</p><h2 id="过滤"><a href="#过滤" class="headerlink" title="过滤"></a>过滤</h2><p>我们也有，selector是非常强大的，它有能力把过滤条件传递进去，对节点进行过滤。这在client端调用时可以传递参数。一些意见存在的过滤可以在<a href="https://github.com/micro/go-micro/blob/master/selector/filter.go" target="_blank" rel="noopener">这里</a>看到，比如metadata，endpoint和版本过滤。</p><h3 id="为什么过滤重要"><a href="#为什么过滤重要" class="headerlink" title="为什么过滤重要"></a>为什么过滤重要</h3><p>你也许有一些功能只在某些特定版本的服务上存在。需要将这些请求分发到这些特定版本的服务上。这是非常好的功能，特别是多个不同版本的服务在同时运行时。</p><p>另外一个有用的地方是，你想要根据地区对服务进行路由。通过设置数据中心的标签在服务上，你可以过滤出本地的节点。根据metadata进行过滤是非常强大的，希望有更多的应该能够把这个功能使用起来。</p><h2 id="插件化的架构"><a href="#插件化的架构" class="headerlink" title="插件化的架构"></a>插件化的架构</h2><p>Micro原生的插件化架构是你一次又一次听到的。这从设计的第一天就已经确定了。这是非常重要的，Micro提供模块来构建整个系统。有时候的运行会超出控制，但这些都可以改善。</p><h3 id="为什么插件化很重要"><a href="#为什么插件化很重要" class="headerlink" title="为什么插件化很重要?"></a>为什么插件化很重要?</h3><p>每个人对怎样构建分布式系统都有自己的想法，我们实际上是提供了一个方式，让人们能设计他们想要的解决方案。不仅如此，现在也有很多经过严格测试的工具，我们可以直接使用，而不是自己重写任何东西。</p><p>技术始终在进化，全新的、更好的工具每天都在出现。我们怎样避免止步不前，插件化的架构意味着我们可以使用目前的组件，未来也可以使用更好的组件进行替代。</p><h3 id="插件"><a href="#插件" class="headerlink" title="插件"></a>插件</h3><p>每个go-micro的特性都被设计成golang中的接口，通过这样做，我们可以实际上替换底层的实现，这几乎不需要进行代码改动。在大部分情况下，只需要简单的引用这个包，然后在启动时加入参数就可以了。</p><p>在<a href="https://github.com/micro/go-plugins" target="_blank" rel="noopener">go-plugins</a>有很多现成的插件可以使用。</p><p>go-micro目前提供了默认的consul作为服务发现系统，http作为transport，你也许会想要使用一些别的东西，或者实现自己的插件。我们已经有社区的贡献者分享了<a href="https://github.com/micro/go-plugins/tree/master/registry/kubernetes" target="_blank" rel="noopener">Kubernetes</a> 的注册插件和<a href="https://github.com/micro/go-plugins/pull/24" target="_blank" rel="noopener">Zookeeper</a>的注册插件。</p><h3 id="我怎样使用插件"><a href="#我怎样使用插件" class="headerlink" title="我怎样使用插件"></a>我怎样使用插件</h3><p>大部分时候，插件的使用类似这样：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># Import the plugin</span><br><span class="line"><span class="keyword">import</span> _ <span class="string">"github.com/micro/go-plugins/registry/etcd"</span></span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">go</span> run main.<span class="keyword">go</span> --registry=etcd --registry_address=<span class="number">10.0</span><span class="number">.0</span><span class="number">.1</span>:<span class="number">2379</span></span><br></pre></td></tr></table></figure><p>如果你想要看更多的细节，参考之前讨论 Micro on NATS的文章。</p><h2 id="wrappers"><a href="#wrappers" class="headerlink" title="wrappers"></a>wrappers</h2><p>客户端和服务端都支持中间件的概念，称为wrapper。通过支持中间件，我们可以增加在请求和返回的业务逻辑前面或者后面，添加自定义的逻辑。</p><p>中间件是很容易理解的概念，数以千计的库在使用它。在处理崩溃、限制并发、认证、日志、记录等场景下，很容易发现它的妙处。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"># Client Wrappers</span><br><span class="line"><span class="keyword">type</span> Wrapper <span class="function"><span class="keyword">func</span><span class="params">(Client)</span> <span class="title">Client</span></span></span><br><span class="line"><span class="function"><span class="title">type</span> <span class="title">StreamWrapper</span> <span class="title">func</span><span class="params">(Streamer)</span> <span class="title">Streamer</span></span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"># <span class="title">Server</span> <span class="title">Wrappers</span></span></span><br><span class="line"><span class="function"><span class="title">type</span> <span class="title">HandlerWrapper</span> <span class="title">func</span><span class="params">(HandlerFunc)</span> <span class="title">HandlerFunc</span></span></span><br><span class="line"><span class="function"><span class="title">type</span> <span class="title">SubscriberWrapper</span> <span class="title">func</span><span class="params">(SubscriberFunc)</span> <span class="title">SubscriberFunc</span></span></span><br><span class="line"><span class="function"><span class="title">type</span> <span class="title">StreamerWrapper</span> <span class="title">func</span><span class="params">(Streamer)</span> <span class="title">Streamer</span></span></span><br></pre></td></tr></table></figure><h2 id="我怎样使用Wrapper"><a href="#我怎样使用Wrapper" class="headerlink" title="我怎样使用Wrapper"></a>我怎样使用Wrapper</h2><p>这里是一个很直接的插件</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"github.com/micro/go-micro"</span></span><br><span class="line"><span class="string">"github.com/micro/go-plugins/wrapper/breaker/hystrix"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">service := micro.NewService(</span><br><span class="line">micro.Name(<span class="string">"myservice"</span>),</span><br><span class="line">micro.WrapClient(hystrix.NewClientWrapper()),</span><br><span class="line">)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>很容易对不对，我们发现很多公司在Micro上层，创建了自己的层级，用于初始化大部分默认的wrapper，所以所有的wrapper可以在同一个地方进行添加。</p><p>现在我们看看wrapper怎样让应用更有弹性，更能容错。</p><h2 id="circuit-breaker"><a href="#circuit-breaker" class="headerlink" title="circuit breaker"></a>circuit breaker</h2><p>在SOA或者微服务世界，一个单独的请求可能会调用多个服务。大部分情况下，聚合许多信息返回给调用者。在成功的情况下，它运行的很好，但一旦发生错误，很容易触发雪崩式的错误，除了重启整个系统，很难恢复。</p><p>我们部分的解决了这个问题，通过在客户端使用重试机制和黑名单。但在一些情况下，我们需要组织客户端发起这个请求。</p><p>这里是circuit breaker怎样起作用的</p><p><img src="https://blog.micro.mu/assets/images/circuit.png" alt=""></p><p>circuit breakers的理念非常直接，方法的执行是根据对失败的情况进行监控而进行封装的。当失败的情况达到一个阈值时，breaker开始起作用，任何未来的调用尝试都会返回错误，而不会调用实际的业务函数。在超时时间过了以后，进入一个半开状态。如果某个请求失败了，breaker会再次生效，如果成功了就会恢复到正常。</p><p>虽然内部的Micro客户端有一些容错特性，但我们不应该依赖它来解决所有问题。在wrapper中使用circuit breakers让我们受益很多。</p><h2 id="Rate-Limiting"><a href="#Rate-Limiting" class="headerlink" title="Rate Limiting"></a>Rate Limiting</h2><p>如果我们非常轻松的能响应世界上所有的请求，那就太好了，不过是在梦里。真实的世界不是这样工作的，执行一个查询需要消耗时间，资源的限制让我们只能响应一定数量的请求。</p><p>我们需要考虑限制发起请求的数量，或者限制并发响应的数量。这就是rate limiting发挥作用的地方。如果没有rate limiting，很容易会把资源耗尽，或者完全的让系统崩溃，让系统不能响应未来的任何请求。这经常是DDOS攻击的常见做法。</p><p>每个人都听说过，使用过或者实现过一些类型的rate limiting。这里有很多不同的算法，其中一种是<a href="https://en.wikipedia.org/wiki/Leaky_bucket" target="_blank" rel="noopener">Leaky Bucket</a> 算法，我们不会在这里展开，但值得一读。</p><p>我们可以使用Micro Wrapper和已经存在的库来使用这个函数，一个已经存在的库在<a href="https://github.com/micro/go-plugins/blob/master/wrapper/ratelimiter/ratelimit/ratelimit.go" target="_blank" rel="noopener">这里</a>。</p><p>我们实际上对YouTube实现的<a href="https://github.com/youtube/doorman" target="_blank" rel="noopener">Doorman</a>算法很感兴趣，一个全局的客户端rate limiter，我们也在寻求社区的其他实现。</p><h2 id="服务端"><a href="#服务端" class="headerlink" title="服务端"></a>服务端</h2><p>前面介绍了很多客户端的很多特性和使用方式，那么服务端呢，第一件事需要注意的是Micro在go-micro的API、CLI、Sidecar等等都使用了客户端，客户端的特性让整个架构都收益，但我们仍然需要在服务端解决一些问题。</p><p>在客户端，register用于发现服务，服务端进行注册。当一个服务的实例运行起来时，它在服务发现系统进行注册，在退出时进行注销，关键词是『gracefully』。</p><p><img src="https://blog.micro.mu/assets/images/register.png" alt=""></p><h2 id="处理错误"><a href="#处理错误" class="headerlink" title="处理错误"></a>处理错误</h2><p>在分布式环境中，我们都需要处理错误，我们需要容忍错误。register支持通过ttl来进行过期检查，一旦过期节点就是不健康的，底层的服务发现机制类型consul都支持这些功能。同时服务端也支持重新注册。这两者的结合意味着，节点可以在间隔时间内会重新注册，如果节点因为运行失败等等没有重新注册，register就会因为超时而认为节点不健康，将节点从register删除。</p><p>这种容错设计最先没有出现在go-micro中，但我们很快发现，在真实的世界中，因为服务的崩溃或其他原因程序退出时，并没有注销自己，所以需要这种ttl健康监测。</p><p>带来的影响就是，客户端需要处理一系列污染的请求。客户端也需要容错性，我们认为这样的功能设计排除了许多明显的问题。</p><h2 id="增加更多功能设计"><a href="#增加更多功能设计" class="headerlink" title="增加更多功能设计"></a>增加更多功能设计</h2><p>另一件需要注意的事情是，服务端也提供了能力来使用Wrapper和中间件，这意味着我们也可以做circuit breaking， rate limiting等其他一些特性。</p><p>服务端的这个功能故意的设计的简单，但插件化的特性可以让你自由扩展。</p><h2 id="客户端与Sidecar"><a href="#客户端与Sidecar" class="headerlink" title="客户端与Sidecar"></a>客户端与Sidecar</h2><p>大部分我们讨论的都是存在于go-micro库中，这对所有的golang使用者是很好的，但其他人在想，我怎样从这里收益呢。</p><p>在最开始，Micro就包含了<a href="https://github.com/micro/micro/tree/master/car" target="_blank" rel="noopener">Sidecar</a>的设计理念，这是一个HTTP的代理，所有的go-micro的功能都内置其中，所以不管你用哪种语言构建你的应用，你都可以收益于我们在上面的讨论。</p><p><img src="https://blog.micro.mu/assets/images/sidecar-rpc.png" alt=""></p><p>sidecar的设计模式并不是新东西，NetflixOSS有一个叫做<a href="https://github.com/Netflix/Prana" target="_blank" rel="noopener">Prana</a>的项目。Buoyant有一个叫<a href="https://linkerd.io/" target="_blank" rel="noopener">Linkerd</a>的项目。</p><p>Micro Sidecar使用了默认的go-micro客户端，如果你想使用其他功能，你可以添加参数，很容易的重新编译。我们会想办法在未来简化这个程序。</p><h2 id="还有更多"><a href="#还有更多" class="headerlink" title="还有更多"></a>还有更多</h2><p>这里讨论了许多<a href="https://github.com/micro/go-micro" target="_blank" rel="noopener">go-micro</a>的包和相关的工具，这些工具是很好的开始，但他们还不够。当你想要运行一个可扩展的、数以百计的微服务，处理数百万请求，仍然有许多问题需要解决。</p><h3 id="Platform"><a href="#Platform" class="headerlink" title="Platform"></a>Platform</h3><p>这是<a href="https://github.com/micro/go-platform" target="_blank" rel="noopener">go-platform</a>和<a href="https://github.com/micro/platform" target="_blank" rel="noopener">platform</a>发挥作用的地方了，micro解决了基础的组件，Platform则更进一步，解决运行可扩展的服务的更多问题。比如认证、分布式trace、同步锁、健康检查等等。</p><p>分布式系统需要一系列的工具用于提高容错性，Platform看起来会有帮助。通过提供一个分层的架构，我们可以在原始的核心工具上，构建任何自己需要的功能。</p><p>Platform仍然在早期，但Platform会解决大部分公司构建分布式平台时会遇到的问题。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>科技在快速的进化，云计算给了我们不受限制的扩展能力。设法与变化保持同步很难，构建一个可扩展的，高容错的系统在今天仍然具有很大的挑战。</p><p>但不应该用以前的方式解决问题，作为一个社区，我们可以互相帮助，适应这个新的环境，构建随着不断增长的需求而不断扩张的系统。</p><p>Micro在这个过程中看起来提供了一些帮助，通过提供工具，简化了构建和管理分布式系统。希望这个文章能示范我们处理这些问题的方式。</p><p>如果你想了解更多，请看这个<a href="https://blog.micro.mu/" target="_blank" rel="noopener">blog</a>，或者这个<a href="https://github.com/micro/micro" target="_blank" rel="noopener">repo</a>，Twitter可以关注<a href="https://twitter.com/microhq" target="_blank" rel="noopener">@MicroHQ</a>，Slack社区在[这里</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这是一系列介绍&lt;a href=&quot;http://github.com/micro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Micro&lt;/a&gt;框架的文章的第七篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。&lt;/p&gt;
&lt;p&gt;自发表上篇文章以
      
    
    </summary>
    
      <category term="微服务" scheme="http://gogap.cn/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
  <entry>
    <title>Micro Bot - 微服务中的ChatOps</title>
    <link href="http://gogap.cn/2016/04/25/the-micro-bot/"/>
    <id>http://gogap.cn/2016/04/25/the-micro-bot/</id>
    <published>2016-04-25T05:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>这是一系列介绍<a href="http://github.com/micro" target="_blank" rel="noopener">Micro</a>框架的文章的第六篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。</p><p>今天我想聊一下机器人。</p><h2 id="机器人-真的吗…"><a href="#机器人-真的吗…" class="headerlink" title="机器人?真的吗…"></a>机器人?真的吗…</h2><p>现在我知道你在想什么，现在有许多关于机器人的夸张说法。如果你对聊天机器人熟悉的话，你会知道这些都不是什么新说法，事实上最早的历史开始于<a href="https://en.wikipedia.org/wiki/ELIZA" target="_blank" rel="noopener">Eliza</a>。大众对机器人重新开始着迷，是因为我们发现了机器人有更多的功能，而不仅是简单的好玩。同时他们也提醒了我们下一代的人机交互接口会演变成什么样。</p><p>从工程师的思维来看，机器人不仅是为了交谈的目的，他们在执行任务的时候，超出想象的好用。大部分的我们已经对ChatOps很熟悉了。Github创造了这个概念，推出了他们的 <a href="https://hubot.github.com/" target="_blank" rel="noopener">Hubot</a>,这是一个聊天机器人，可以管理技术上和业务上的操作任务。</p><p>看看这篇Jesse Newland的演讲了解更多：<a href="https://www.youtube.com/watch?v=NST3u-GjjFw" target="_blank" rel="noopener">ChatOpts at GitHub</a></p><p>Hubot和机器人看起来已经证明了，在技术公司他们是非常有用的，他们在运维和自动化方面成为了好用的助手。现在通过HipChat或者Slack操控机器人来执行任务，而以前我们是手动的执行一些脚本，这明显要强大的多。这对整个团队带来的价值是显而易见的，每个人都能看到你在做的事情，已经事情的结果。</p><h2 id="Micro服务怎样与ChatOps结合起来"><a href="#Micro服务怎样与ChatOps结合起来" class="headerlink" title="Micro服务怎样与ChatOps结合起来?"></a>Micro服务怎样与ChatOps结合起来?</h2><p><a href="https://github.com/micro/micro" target="_blank" rel="noopener"><strong>Micro</strong></a>，这个微服务工具箱，包括了一系列的服务，提供了接入点连接你正在运行的系统。API，Web控制台，CLI等等。他们都能与你的服务进行交互，观察你的服务的运行环境。在过去的几个月，这已经变得很清楚了，机器人是另外一种接入点，用于与你的服务进行交互与观察你的服务，这也是Micro世界的第一等公民。</p><p>这样一来</p><p><img src="https://blog.micro.mu/assets/images/micro_bot.png" alt=""></p><p>首先我们要明确，Micro 机器人是处于非常早期的阶段，目前主要是通过CLI提供功能。我们现在不能说实现了ChatOps，但或许有一天可以呢…</p><p>Micro机器人包括了类似hubot的语法命令，已经一种实现的输入，比如Slack或者HipChat。这是粗糙的第一个版本，但我相信随着工作的投入，不久以后就能大大提供机器人的能力。希望社区也能加入进来。</p><p>Bot 包括了所有的CLI命令，也提供了Slack和HipChat的入口。我们的机器人目前运行在一个demo环境中，通过Micro Slack提供，在<a href="http://slack.micro.mu/" target="_blank" rel="noopener">这里</a>加入我们。</p><p>在最近的开发周期中，我们会看看增加一些入口，比如IRC,XMPP，让我们可以通过命令简单的管理运行中的微服务。如果你有新的入口或者命令需要添加，请提交PR，贡献者是非常欢迎的。目前的插件可以在这里看到：<a href="https://github.com/micro/go-plugins/tree/master/bot" target="_blank" rel="noopener">github.com/micro/go-plugins/bot</a></p><p>这确实是一个基础的框架，用于对Micro生态系统做可编程的机器人。整个工具箱拥有插件化的特性。让我们看看Inputs和Commands是怎样工作的。</p><h2 id="Inputs"><a href="#Inputs" class="headerlink" title="Inputs"></a>Inputs</h2><p>Inputs是micro机器人怎样连接hipchat,slack,irc,xmpp等等。我们目前已经实现了HipChat和Slack，应该覆盖了大部分的用户。</p><p>这里是Input的接口定义</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Input <span class="keyword">interface</span> &#123;</span><br><span class="line"><span class="comment">// Provide cli flags</span></span><br><span class="line">Flags() []cli.Flag</span><br><span class="line"><span class="comment">// Initialise input using cli context</span></span><br><span class="line">Init(*cli.Context) error</span><br><span class="line"><span class="comment">// Stream events from the input</span></span><br><span class="line">Stream() (Conn, error)</span><br><span class="line"><span class="comment">// Start the input</span></span><br><span class="line">Start() error</span><br><span class="line"><span class="comment">// Stop the input</span></span><br><span class="line">Stop() error</span><br><span class="line"><span class="comment">// name of the input</span></span><br><span class="line">String() <span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Input提供了方便的功能，用于添加你自己的命令行参数。<code>Flag()</code>这个方法会在初始化之前调用，任何自定义的参数会增加到全局参数列表里面。</p><p>在参数被解析之后，<code>Init()</code>会被调用，这样一来，这个入口的任何中间数据都会被初始化，一旦所有事情执行完成，机器人就会调用<code>Start（）</code>然后是<code>Stream（）</code>方法，用于与Input建立连接。</p><p>这是Stream方法返回的Conn接口</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Conn <span class="keyword">interface</span> &#123;</span><br><span class="line">Close() error</span><br><span class="line">Recv(*Event) error</span><br><span class="line">Send(*Event) error</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>机器人会持续的调用<code>Recv()</code>来监听事件。<code>Recv()</code>应该是一个阻塞的调用，否则我们会陷入死循环，耗尽CPU。一旦机器人处理完了事件，它会通过<code>Send()</code>返回一些结果。</p><p>Event是一个基础的类型，用户在机器人和入口之间通信。他可以让我们把不同的消息类型，封装成统一的格式。目前只有一个<code>TextEvent</code>类型，在未来我们会有更多。</p><p>机器人是不知道命令是来自于Slack,HipChat还是其他地方。它只知道收到了一个事件，然后需要执行它。这是一种很好的方式，用于把机器人和Input拆分开。</p><p>这里是Event类型</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Event <span class="keyword">struct</span> &#123;</span><br><span class="line">Type EventType</span><br><span class="line">From <span class="keyword">string</span></span><br><span class="line">To   <span class="keyword">string</span></span><br><span class="line">Data []<span class="keyword">byte</span></span><br><span class="line">Meta <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">interface</span>&#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Commands"><a href="#Commands" class="headerlink" title="Commands"></a>Commands</h2><p>commands是可以被机器人执行的函数。这很简单，它们存储在map中，key经过正则，它们会匹配上input接收到的事件。如果正则匹配上了某个事件，关联的函数就会被执行。命令的执行结果就会被发送回input。如果事件的From字段不为空，返回会被发送到To字段。你可以看到这是怎样让机器人直接的进行交流，不管任何地方，任何时候。</p><p>当前的Command的接口非常直接，但未来可能会更改，一旦我们遇到更复杂的情况。</p><p>command的接口：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Command <span class="keyword">interface</span> &#123;</span><br><span class="line"><span class="comment">// Executes the command with args passed in</span></span><br><span class="line">Exec(args ...<span class="keyword">string</span>) ([]<span class="keyword">byte</span>, error)</span><br><span class="line"><span class="comment">// Usage of the command</span></span><br><span class="line">Usage() <span class="keyword">string</span></span><br><span class="line"><span class="comment">// Description of the command</span></span><br><span class="line">Description() <span class="keyword">string</span></span><br><span class="line"><span class="comment">// Name of the command</span></span><br><span class="line">String() <span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里是一个Echo Command的示例</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Echo returns the same message</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Echo</span><span class="params">(ctx *cli.Context)</span> <span class="title">Command</span></span> &#123;</span><br><span class="line">usage := <span class="string">"echo [text]"</span></span><br><span class="line">desc := <span class="string">"Returns the [text]"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> NewCommand(<span class="string">"echo"</span>, usage, desc, <span class="function"><span class="keyword">func</span><span class="params">(args ...<span class="keyword">string</span>)</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(args) &lt; <span class="number">2</span> &#123;</span><br><span class="line"><span class="keyword">return</span> []<span class="keyword">byte</span>(<span class="string">"echo what?"</span>), <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> []<span class="keyword">byte</span>(strings.Join(args[<span class="number">1</span>:], <span class="string">" "</span>)), <span class="literal">nil</span></span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="其他"><a href="#其他" class="headerlink" title="其他?"></a>其他?</h2><p>只有Inputs和Commands是不够的。如果我们以后想要做些其他的操作呢？我们怎样持久化机器人的状态?双向的交流怎么样？而不是仅仅返回内容。</p><p>这必须要编译！</p><p>我们仍然处于构建这个机器人框架的早期，这是一个机会，讨论基础的接口应该是什么样的。</p><p>下一步是提供各种类型的接口。更严肃一点，我们需要一个<code>Stream</code>接口或者类似的。还需要<code>Input.Conn</code>,这样我们可以处理任何插件的事件流。</p><p>这潜在的让我们有能力实现同一时间接收多个input的事件流，因此我们可以从事件流中获取事件，处理后返回。</p><p>一个例子是，从Slack中接受到消息，查询micro的服务，最后发送一个总结性的邮件。</p><h2 id="怎样运行起来"><a href="#怎样运行起来" class="headerlink" title="怎样运行起来?"></a>怎样运行起来?</h2><p>micro机器人在你的环境中单独运行起来，就像其他某个服务一样。也会通过服务发现进行注册。<img src="https://blog.micro.mu/assets/images/bot.png" alt=""></p><h2 id="我怎样运行"><a href="#我怎样运行" class="headerlink" title="我怎样运行?"></a>我怎样运行?</h2><p>因为机器人就像运行一个其他服务一下，你首先需要启动服务发现机制，默认是consul</p><p>使用支持Slack的机器人</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">micro bot --inputs=slack --slack_token=SLACK_TOKEN</span><br></pre></td></tr></table></figure><p>以及HipChat</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">micro bot --inputs=hipchat --hipchat_username=XMPP_USERNAME --hipchat_password=XMPP_PASSWORD</span><br></pre></td></tr></table></figure><h2 id="运行中的机器人"><a href="#运行中的机器人" class="headerlink" title="运行中的机器人"></a>运行中的机器人</h2><p>这里有一些运行起来的机器人的截图，就像你看到的，它是一个CLI命令的复制。我们有一些额外的命令比如动画和地图。在这里可以看到<a href="https://github.com/micro/go-plugins" target="_blank" rel="noopener">github.com/micro/go-plugins</a> </p><p><img src="https://blog.micro.mu/assets/images/hipchat.png" alt=""></p><h2 id="增加新的Commands"><a href="#增加新的Commands" class="headerlink" title="增加新的Commands"></a>增加新的Commands</h2><p>Commands是一个可以被机器人执行的函数，通过字符进行匹配，类似其他的机器人比如Hubot</p><p>这里是怎样写一个简单的ping命令</p><h3 id="编写命令"><a href="#编写命令" class="headerlink" title="编写命令"></a>编写命令</h3><p>首先通过NewCommand创建一个命令，这个一个快速的方式，你也可以实现这个接口。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">"github.com/micro/micro/bot/command"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Ping</span><span class="params">()</span> <span class="title">command</span>.<span class="title">Command</span></span> &#123;</span><br><span class="line">usage := <span class="string">"ping"</span></span><br><span class="line">description := <span class="string">"Returns pong"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> command.NewCommand(<span class="string">"ping"</span>, usage, desc, <span class="function"><span class="keyword">func</span><span class="params">(args ...<span class="keyword">string</span>)</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> []<span class="keyword">byte</span>(<span class="string">"pong"</span>), <span class="literal">nil</span></span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="注册命令"><a href="#注册命令" class="headerlink" title="注册命令"></a>注册命令</h3><p>把命令添加到Commands map中，匹配的key需要被<a href="https://golang.org/pkg/regexp/#Match" target="_blank" rel="noopener">golang/regexp.Match</a>匹配。</p><p>这里我们只对ping命令作出响应</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">"github.com/micro/micro/bot/command"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">command.Commands[<span class="string">"^ping$"</span>] = Ping()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="连接命令"><a href="#连接命令" class="headerlink" title="连接命令"></a>连接命令</h3><p>在这里引入你的命令</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="string">"path/to/import"</span></span><br></pre></td></tr></table></figure><p>接下来进行编译</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cd github.com/micro/micro</span><br><span class="line"><span class="keyword">go</span> build -o micro main.<span class="keyword">go</span> link_input.<span class="keyword">go</span></span><br></pre></td></tr></table></figure><h2 id="下一步"><a href="#下一步" class="headerlink" title="下一步?"></a>下一步?</h2><p>我们要意识到微服务世界并不容易，它需要一系列的工具，还要进行观测。比如监控服务、分布式tracing、结构化日志，这都是重要的组成部分。</p><p>想象一个世界，机器人有能力感知分布式系统。当我们需要的时候，提供反馈给我们，而不是需要盯着控制台，处理一个个错误提示。你也许听说过NoOps?那么什么是BotOps?你不会被电话催促怎么样？常见的错误，都通过事先预定的程序处理怎么样？</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>机器人的革命只是刚刚开始，基础设施和自动化的世界正在改变，我们相信机器人会扮演一个重要的角色，最初是传统的ChatOps ，未来会走的更远。</p><p>机器人需要被看做第一等公民，跟配置管理、命令行、和API一样。我们只是把机器人加入到Micro的生态系统中来。</p><p>这仍然是处于早期，但不就的将来将会让我们满意。</p><p>如果你想了解更多，请看这个<a href="https://blog.micro.mu/" target="_blank" rel="noopener">blog</a>，或者这个<a href="https://github.com/micro/micro" target="_blank" rel="noopener">repo</a>，Twitter可以关注<a href="https://twitter.com/microhq" target="_blank" rel="noopener">@MicroHQ</a>，Slack社区在<a href="http://slack.micro.mu/" target="_blank" rel="noopener">这里</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这是一系列介绍&lt;a href=&quot;http://github.com/micro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Micro&lt;/a&gt;框架的文章的第六篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。&lt;/p&gt;
&lt;p&gt;今天我想聊一下机
      
    
    </summary>
    
      <category term="微服务" scheme="http://gogap.cn/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
  <entry>
    <title>Micro的架构与微服务的设计模式</title>
    <link href="http://gogap.cn/2016/04/18/micro-architecture/"/>
    <id>http://gogap.cn/2016/04/18/micro-architecture/</id>
    <published>2016-04-18T07:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>这是一系列介绍<a href="http://github.com/micro" target="_blank" rel="noopener">Micro</a>框架的文章的第五篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。</p><p>在过去的几个月，我们已经有了很多有关micro架构的疑问和微服务的设计模式的问题，今天我们讨论一下这两个话题。</p><h2 id="关于Micro"><a href="#关于Micro" class="headerlink" title="关于Micro"></a>关于Micro</h2><p><a href="https://github.com/micro/micro" target="_blank" rel="noopener"><strong>Micro</strong></a>是一个微服务工具箱，它有自己固有的设计模式，但插件化的架构可以让底层的实现很轻易的被替换。</p><p>micro专注于定位微服务构建过程中的最基本的需求，并通过精密的设计来满足这些需求。</p><p>查看过往的文章可以了解微服务的理念和Micro的特性。</p><h2 id="关于工具箱"><a href="#关于工具箱" class="headerlink" title="关于工具箱"></a>关于工具箱</h2><p><a href="https://github.com/micro/go-micro" target="_blank" rel="noopener"><strong>Go Micro</strong></a> 是一个用golang编写的，插件化的RPC框架。它提供了基础的库，比如服务发现、客户端负载均衡、编解码、同步异步通信等。</p><p><a href="https://github.com/micro/micro/tree/master/api" target="_blank" rel="noopener"><strong>Micro API</strong></a>是一个API网关，用于将外部的HTTP请求路由到内部的micro服务上。它有单一的接入点，可以通过反向代理或者http转换成RPC来访问。</p><p><a href="https://github.com/micro/micro/tree/master/web" target="_blank" rel="noopener"><strong>Micro Web</strong></a>是一个web仪表盘，也是作为micro web应用的反向代理。我们相信web应用也应该是一个微服务，在微服务世界里也应该是第一等公民。它表现的很像Micro API但也有单独的特性比如websocket</p><p><a href="https://github.com/micro/micro/tree/master/car" target="_blank" rel="noopener"><strong>Micro Sidecar</strong></a>使用http服务，提供了go-micro的所有特性。虽然我们喜欢golang来构建微服务，但你也许想使用其他语言。所以Sidecar提供了一种其他语言的应用接入Micro世界的方式。</p><p><a href="https://github.com/micro/micro/tree/master/cli" target="_blank" rel="noopener"><strong>Micro CLI</strong></a>是一个简单直接的命令行接口，用于与你的服务交互。他也可以使用你的Sidecar作为代理来连接服务。</p><p>上面是很简单的介绍，下面我们更加深入一些。</p><h2 id="RPC-REST-Proto…"><a href="#RPC-REST-Proto…" class="headerlink" title="RPC,REST,Proto…"></a>RPC,REST,Proto…</h2><p>第一件你想到的事情是，为什么是RPC，而不是REST? 在内部服务间的通信上，我们相信RPC是更合适的。或者更明确一点，RPC使用protobuf做编码，通过protobuf定义API。这种方式把两个需求结合起来了：一个需求是需要明确定义的API接口，另一个需求是高性能的消息编解码。RPC是非常直接的通信方式。</p><p>我们在这个选择上并不孤独。</p><p>Google是protobuf的创造者，在内部通过gRPC这个框架，大量的使用RPC调用。Hailo也从RPC/protobuf的组合中收益很多，不仅是系统性能，开发速度也提高很多。Uber选择开发自己的RPC框架，名字叫<a href="http://uber.github.io/tchannel/" target="_blank" rel="noopener">TChannel</a></p><p>个人而言我认为未来的API将会使用RPC进行构建，因为它们结构化的格式、高效的编解码提供了定义良好的API和高性能的通信。</p><h2 id="HTTP-to-RPC-API…"><a href="#HTTP-to-RPC-API…" class="headerlink" title="HTTP to RPC,API…"></a>HTTP to RPC,API…</h2><p>事实上，我们在web上RPC还有很长的路要走。在内部RPC的表现是完美的，但在面对外部请求比如网站、手机app的接口等等，就是另外一回事了。我们需要面对这个，这就是为什么Micro需要一个API网关，用来接受并转换http请求。</p><p>API网关在微服务架构中是一个常见的模式。它作为一个单一的接入点，外部世界的请求，通过它进行路由分发。它让HTTP API可以由背后的很多服务所组成。</p><p>micro的API网关使用路径到服务的解决方案，因此不同的请求路径，对应了不同的服务。比如/user =&gt; user api，/order =&gt; order api。</p><p>这里有一个例子。一个请求的路径是/comstomer/orders，这个请求会被转发到<strong>go.micro.api.customer</strong>这个服务，会使用 <strong>Customer.Orders</strong>.这个方法进行处理。</p><p><img src="https://blog.micro.mu/assets/images/api.png" alt=""></p><p>你也许会问，API服务到底是怎样的？我们下面来讨论一下不同类型的服务。</p><h2 id="服务的类型"><a href="#服务的类型" class="headerlink" title="服务的类型"></a>服务的类型</h2><p>微服务的关键理念就是业务的拆解，这是从unix的设计哲学中得到的启示：『doing one thing and doing it well』，因为这个原因，我们认为不同的服务需要有逻辑上和架构上的区别，以实现自己不同的任务。</p><p>我们知道这些理念并没有什么太大的新意，但在一些非常大而且成功的公司，它们的实践取得了成功。我们的目标是传播这些开发理念，并通过工具来进行指导。</p><p>目前我们定义了下面的几种服务。</p><h3 id="API"><a href="#API" class="headerlink" title="API"></a>API</h3><p>通过micro api运行，API 服务在你的架构中处于关键位置，大部分作用是接受外部世界的请求并分发到内部的服务上。你可以通过micro api提供的反向代理REST模式进行访问，也可以通过RPC接口进行访问。</p><h3 id="WEB"><a href="#WEB" class="headerlink" title="WEB"></a>WEB</h3><p>通过micro web运行，web服务专注于服务html请求，构建仪表盘。micro web反向代理http和websocket，目前只有这两种协议支持，未来也许会增加。</p><h3 id="SRV"><a href="#SRV" class="headerlink" title="SRV"></a>SRV</h3><p>这是后台的RPC服务，他们的目标是为你的系统提供核心的功能，大部分并不是公开的接口。你仍然可以通过micro api和micro web，使用/rpc接入点进行访问。这种接入方式直接使用go-micro的client进行调用。</p><p><img src="https://blog.micro.mu/assets/images/arch.png" alt=""></p><p>按照过去的经验，我们发现这样的架构设计非常强大。可以被扩展到数以百计的服务。通过把它整合到Micro架构中，我们发现它为微服务的开发提供了非常好的基础。</p><h2 id="Namespacing"><a href="#Namespacing" class="headerlink" title="Namespacing"></a>Namespacing</h2><p>你也许会想，怎样区分micro api或者micro web以及服务呢。我们通过命名空间进行拆分。通过命名的前缀，我们可以很清晰的看到，某个服务是哪种类型的。这很简单但很高效。</p><p>micro api也会把/customer这样的请求路径定位到<strong>go.micro.api.customer</strong>服务。</p><p>默认的命名空间是：</p><ul><li>API - go.micro.api</li><li>WEB - go.micro.web</li><li>SRV - go.micro.srv</li></ul><p>你应该把它设置成你的域名，比如com.example.api。这些都可以进行配置。</p><h2 id="同步和异步"><a href="#同步和异步" class="headerlink" title="同步和异步"></a>同步和异步</h2><p>你经常听说微服务是很灵活的模式。大多数来说，微服务是关于创造事件驱动的架构，以及设计通过异步通信的方式响应的服务。</p><p>Micro把异步通信作为微服务构建中的第一等公民。事件通过异步消息，可以被任何人消费并作出反应，搭建一个新服务不需要对系统的其他部分作出任何更改。这是一种强大的设计模式，因为这个原因，我们在go-micro中定义了<a href="https://godoc.org/github.com/micro/go-micro/broker#Broker" target="_blank" rel="noopener">Broker</a>接口。</p><p><img src="https://blog.micro.mu/assets/images/pub-sub.png" alt=""></p><p>异步和同步通信在Micro中是分离开的。 <a href="https://godoc.org/github.com/micro/go-micro/transport#Transport" target="_blank" rel="noopener">Transport</a> 接口用于构建服务之间的点对点的通信。go-micro中的client和server基于transport来进行请求和返回RPC调用，提供了双向的通信流。</p><p><img src="https://blog.micro.mu/assets/images/request-response.png" alt=""></p><p>在构建系统时，两种通信方式都应该使用，但关键是理解在什么场景下应该用什么类型的通信方式。在大部分情况下并没有好坏之分，我们需要权衡处理。</p><p>一个broker和异步通信的典型使用方式是这样：监听系统通过broker对服务的事件历史进行记录。</p><p><img src="https://blog.micro.mu/assets/images/audit.png" alt=""></p><p>在这个例子中，每个服务的每个API在被调用时，都会把事件上报到监听topic，监听系统会订阅这个topic，并把他们存储到时间序列的数据库中。在admin管理平台可以看到任何用户的操作历史。</p><p>如果我们通过同步通信做，监听系统直接面对巨大的请求数。如果监听系统宕机了，我们就丢失了这些数据。通过把这些事件发布到broker，我们可以异步的持久化这些数据。这是一种微服务中常见的事件驱动设计模型。</p><h2 id="我们怎样定义微服务"><a href="#我们怎样定义微服务" class="headerlink" title="我们怎样定义微服务?"></a>我们怎样定义微服务?</h2><p>我们已经讨论了很多Micro能为微服务提供的工具箱，也定义了服务的类型。但还没有真正讨论，到底什么是微服务。</p><p>微服务与其他应用的区别到底在哪里，微服务为什么叫微服务。</p><p>现在有很多不同的定义，但有两条适合大部分微服务系统。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Loosely coupled service oriented architecture with a bounded context</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">An approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms</span><br></pre></td></tr></table></figure><p>微服务的哲学与unix也类似</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Do one thing and do it well</span><br></pre></td></tr></table></figure><p>我们认为微服务是这样一种应用程序：专注于单一的业务，并通过明确定义的API对外提供服务。</p><p>看看我们在社交网络中怎样使用微服务：</p><p><img src="https://blog.micro.mu/assets/images/facebook.png" alt=""></p><p>其中一种是流行的MVC架构，在MVC世界中，每个实体代表了一个模型，模型又是作为数据库的抽象。模型之间也许有一对多或者多对多的关系。controller模块负责处理请求，接受model模块返回的数据，并把数据传输到view层，进行渲染，最后输出给用户。</p><p>在微服务架构中，面对同样的例子。每个模型实际上是一个服务，通过API进行服务间通信。用户请求，数据的集合以及渲染是通过一系列不同的web服务进行处理的。每个服务有自身的专注点，当我们需要增加一个新特性时，我们只需要把关联的服务进行修改，或者直接写一个新的服务。分离的理念提供了大规模开发的模式。</p><h2 id="版本"><a href="#版本" class="headerlink" title="版本"></a>版本</h2><p><img src="https://blog.micro.mu/assets/images/versioning.png" alt=""></p><p>开发真实世界的软件时，版本是非常重要的。在微服务世界里，严格的把API和业务逻辑分离到许多不同的服务上，因为这个原因，服务的版本控制是核心的工具的很重要的一部分。可以让我们在流量很大时也能进行升级。</p><p>在go-micro中，服务定义了名字和版本，<a href="https://godoc.org/github.com/micro/go-micro/registry#Registry" target="_blank" rel="noopener">Registry</a>模块返回服务的列表，根据版本把节点进行了区分。这里是service的接口定义。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Service <span class="keyword">struct</span> &#123;</span><br><span class="line">Name      <span class="keyword">string</span></span><br><span class="line">Version   <span class="keyword">string</span></span><br><span class="line">Metadata  <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span></span><br><span class="line">Endpoints []*Endpoint</span><br><span class="line">Nodes     []*Node</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>板块控制需要与<a href="https://godoc.org/github.com/micro/go-micro/selector#Selector" target="_blank" rel="noopener">Selector</a>结合起来，selector是客户端的负载均衡机制，通过selector的策略实现请求根据版本进行分发。</p><p>selector是非常强大的接口，我们根据不同的路由算法，比如随机、轮询、根据标签、响应时间等等。</p><p>通过使用默认的随机负载算法，再加上版本控制算法，我们就可以进行灰度发布。</p><p><img src="https://blog.micro.mu/assets/images/selector.png" alt=""></p><p>在未来，我们会尝试实现一个全局的负载策略，根据历史的趋势进行选择，可以根据版本，设置不同的百分比，并动态的为服务增加标签。</p><h2 id="大规模扩展"><a href="#大规模扩展" class="headerlink" title="大规模扩展"></a>大规模扩展</h2><p>上面的介绍的版本系统，是大规模扩展服务时的基本模式。register存储了服务的注册信息，我们通过selector实现了路由和负载均衡。</p><p>按照<code>doing one thing well</code>的理念，扩展架构也应该是简单、明确定义的API、分层次的架构。通过创造这些工具，我们可以构建更加可靠的系统，专注于更高级别的业务需求。</p><p>这是Micro编写的基础理念，也是我们希望微服务开发者遵循的理念。</p><p>当我们在生产环境部署应用时，我们就需要构建可扩展、高容错、高性能的应用。云计算让我们可以进行不受限制的扩展，但是没有任何东西会一直正常运行。事实上，在构建分布式系统中，怎样对待运行失败的服务是非常重要的一方面，你在构建你的系统时，需要好好考虑。</p><p>在云计算的世界，我们想要在数据中运行错误，甚至多个数据中心运行错误的情况下，也能正常提供服务。在过去我们讨论的是冷热备份，或者是灾难恢复计划。在今天，最先进的技术公司，在全世界不停歇的运作，每个程序都会有多个备份，运行在世界上不同的数据中心。</p><p>我们需要向google，facebook，netflix和Twitter学习，即使在数据中心运行失败时，也要对用户提供服务，在多个数据中心运行失败时，也需要尽快恢复。</p><p>Micro可以让你构建这样的应用，通过插件化的架构，我们可以为不同的分布式系统，实现不同的工具箱。</p><p>服务发现和注册器是Micro的关键模块，它们可以用于发现在数据中心中的一系列服务，Micro API可以用于路由和负载一系列的服务。</p><p><img src="https://blog.micro.mu/assets/images/regions.png" alt=""></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>希望这篇文章清晰的讲解了Micro的架构，以及怎样实现可扩展的微服务设计模式。微服务首先是一种软件设计模式，我们可以通过工具实现基础、核心的功能，同时也能灵活组合其他设计模式。</p><p>因为Micro是一个插件化的架构，它强大的能力，可以实现不同的设计模式，在不同的场景中都能使用。比如你构建一个视频流的服务，你也许需要基于http的点对点服务。如果你对性能不敏感，你也许需要使用消息队列比如NATS或RabbitMQ。</p><p>使用Micro这样的工具进行开发是非常让人兴奋的。</p><p>如果你想了解更多，请看这个<a href="https://blog.micro.mu/" target="_blank" rel="noopener">blog</a>，或者这个<a href="https://github.com/micro/micro" target="_blank" rel="noopener">repo</a>，Twitter可以关注<a href="https://twitter.com/microhq" target="_blank" rel="noopener">@MicroHQ</a>，Slack社区在<a href="http://slack.micro.mu/" target="_blank" rel="noopener">这里</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这是一系列介绍&lt;a href=&quot;http://github.com/micro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Micro&lt;/a&gt;框架的文章的第五篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。&lt;/p&gt;
&lt;p&gt;在过去的几个月，
      
    
    </summary>
    
      <category term="微服务" scheme="http://gogap.cn/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
  <entry>
    <title>基于消息队列NATS构建微服务</title>
    <link href="http://gogap.cn/2016/04/11/micro-on-nats/"/>
    <id>http://gogap.cn/2016/04/11/micro-on-nats/</id>
    <published>2016-04-11T07:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>这是一系列介绍<a href="http://github.com/micro" target="_blank" rel="noopener">Micro</a>框架的文章的第四篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。</p><p>这篇文章我们会讨论基于<a href="http://nats.io/" target="_blank" rel="noopener"><strong>NATS</strong></a>使用<a href="https://github.com/micro/micro" target="_blank" rel="noopener"><strong>Micro</strong></a>。讨论包括了服务发现，同步通信和异步通信。如果需要了解Micro，请翻看以前的文章。</p><h2 id="NATS是什么"><a href="#NATS是什么" class="headerlink" title="NATS是什么?"></a>NATS是什么?</h2><p><a href="http://nats.io/" target="_blank" rel="noopener"><strong>NATS</strong></a>是一个开源的消息系统，或者说消息队列。NATS的作者是Derek Collison， <a href="https://www.apcera.com/" target="_blank" rel="noopener">Apcera</a>的作者。它起源于VMWare，最开始是一个ruby的系统。后来使用golang进行重写，逐步的成为了一个高扩展性的高性能消息系统。</p><h2 id="为什么是NATS"><a href="#为什么是NATS" class="headerlink" title="为什么是NATS?"></a>为什么是NATS?</h2><p>为什么不是呢?过去我使用过很多消息队列，很明显NATS是鹤立鸡群的，在过去消息系统似乎成了企业的灵丹妙药了，结果是每个人参与的每个系统都离不开消息系统。导致了很多无效的承诺、高昂的研发投入，制造出比它解决的更多的麻烦。</p><p>NATS，相比之下，就十分专注，在难以置信的简洁之下，解决了性能问题，解决了高可用行问题。它的口号是『always on and available』，使用了一种『fire and forget』的消息模式。它简单、专注、轻量的特性使它在微服务的候选中脱颖而出。我们相信，在服务间的消息传递领域，它很快会变成主要的候选者。</p><p>NATS能提供什么?</p><ul><li><p>高性能、高扩展</p></li><li><p>高可用</p></li><li><p>极致的轻量级</p></li><li><p>一次部署</p><p>​</p></li></ul><p>NATS不支持什么？</p><ul><li>持久化</li><li>事务</li><li>Enhanced delivery modes</li><li>Enterprise queueing</li></ul><p>NATS是否适合Micro呢？我们接着讨论</p><h2 id="Micro-on-NATS"><a href="#Micro-on-NATS" class="headerlink" title="Micro on NATS"></a>Micro on NATS</h2><p><a href="https://github.com/micro/micro" target="_blank" rel="noopener"><strong>Micro</strong></a> 采用插件化的架构设计，用户可以替换底层的实现，而不更改任何底层的代码。每个<a href="https://github.com/micro/go-micro" target="_blank" rel="noopener"><strong>Go-Micro</strong></a>框架的底层模块定义了相应的接口， <a href="https://godoc.org/github.com/micro/go-micro/registry#Registry" target="_blank" rel="noopener">registry</a> 是作为服务发现，<a href="https://godoc.org/github.com/micro/go-micro/transport#Transport" target="_blank" rel="noopener">transport</a>作为同步通信， <a href="https://godoc.org/github.com/micro/go-micro/broker#Broker" target="_blank" rel="noopener">broker</a>作为异步通信。</p><p>为每个组件创建一个插件很简单，只需要实现这些组件的接口即可。我们会在未来的文章里详细讨论，怎样写插件，如果你想详细了解NATS的插件或者其他的类似etcd作为服务发现，kafka作异步通信，rabbitmq作同步通信，在这里可以看到： <a href="https://github.com/micro/go-plugins" target="_blank" rel="noopener">github.com/micro/go-plugins</a></p><p>Micro on NATS本质上是实现一系列的插件，将NATS消息系统整合到Micro的框架中来。通过为Go Micro提供不同的插件，我们可以创造出很多灵活的组合。</p><p>在实践过程中，不能一套系统打天下，Micro on NATS的可以灵活的定义各种模型。</p><p>下面我们会讨论一下NATS插件怎样在transport，broker和registry下工作。</p><h2 id="Transport"><a href="#Transport" class="headerlink" title="Transport"></a>Transport</h2><p><img src="https://blog.micro.mu/assets/images/request-response.png" alt=""></p><p>transport 是 go-micro定义的同步通信接口，它使用了泛型的语法描述，类似其他golang代码，比如<code>Liesten,Dial,Accept</code>。这些理念和模式在类似TCP和HTTP这样的同步通信中很容易理解，但是在消息系统中要怎样兼容呢？通信过程中，连接是与消息队列建立的，而不是服务本身。为了达到目的，我们使用消息系统中的topics和channels来创造虚连接。</p><p>它是怎样工作起来的？</p><p>一个服务使用<code>transport.Listen</code>来监听消息，这会与NATS之间建立连接。当<code>transport.Accept</code>被调用时，一个唯一的topic会被创建并订阅。这个唯一的topic地址会作为服务的地址，注册到go-micro的注册器中。每个收到的消息会被虚连接处理，如果一个连接已经存在，而且回复的地址是一样的，我们会简单的把消息积压在连接上。</p><p>客户端通过<code>transport.Dial</code>来连接服务端，其实它是建立了与NATS的连接，然后创建了唯一的topic并且订阅这个topic。这个topic用于接收服务端的返回。任何时候客户端发送消息到服务端时，它都会把回复地址设置成这个topic。(译注：服务端的地址是固定的，而客户端的每个请求，都会生成一个客户端地址，唯一的请求与唯一的地址实现一一关联)</p><p>当任何一边想要关闭连接时，简单的调用<code>transport.Close</code>就会断开与NATS的连接。</p><p><img src="https://blog.micro.mu/assets/images/nats-transport.png" alt=""></p><p>使用transport插件</p><p>引用插件</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="string">"github.com/micro/go-plugins/transport/nats"</span></span><br></pre></td></tr></table></figure><p>启动时传入参数</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">go</span> run main.<span class="keyword">go</span> --transport=nats --transport_address=<span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span>:<span class="number">4222</span></span><br></pre></td></tr></table></figure><p>或者直接在代码中创建</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">transport := nats.NewTransport()</span><br></pre></td></tr></table></figure><p>go-micro的transport接口：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Transport <span class="keyword">interface</span> &#123;</span><br><span class="line">    Dial(addr <span class="keyword">string</span>, opts ...DialOption) (Client, error)</span><br><span class="line">    Listen(addr <span class="keyword">string</span>, opts ...ListenOption) (Listener, error)</span><br><span class="line">    String() <span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Broker"><a href="#Broker" class="headerlink" title="Broker"></a>Broker</h2><p><img src="https://blog.micro.mu/assets/images/pub-sub.png" alt=""></p><p>broke是go-micro的异步通信接口，它定义了泛型的、高等级的通用接口。NATS本身作为消息系统，是可以作为消息broker的。这里只有一个警告：nats并不持久化消息。虽然在某些场景有风险，但我们相信NATS可以也应该用于go-micro的broker。持久化并不是必须的，它提供了高扩展的发布和订阅架构。</p><p>NATS通过Topics和Channels的理念，非常直接的实现了发布和订阅机制。这里并没有其他工作需要做。消息可以被发布到任何异步的fire and forget manner。通过NATS的Queue Group，订阅方可以通过channel的名字就行订阅。实现消息自动的分发的多个订阅者。</p><p><img src="https://blog.micro.mu/assets/images/nats-broker.png" alt=""></p><p>使用broker插件</p><p>引入包</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="string">"github.com/micro/go-plugins/broker/nats"</span></span><br></pre></td></tr></table></figure><p>(译注：注意此时引用的是broker中的nats包，上面引用的是transport中的nats包)</p><p>通过参数启动</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">go</span> run main.<span class="keyword">go</span> --broker=nats --broker_address=<span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span>:<span class="number">4222</span></span><br></pre></td></tr></table></figure><p>也可以直接启动</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">broker := nats.NewBroker()</span><br></pre></td></tr></table></figure><p>go-micro的broker需要实现的接口：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Broker <span class="keyword">interface</span> &#123;</span><br><span class="line">    Options() Options</span><br><span class="line">    Address() <span class="keyword">string</span></span><br><span class="line">    Connect() error</span><br><span class="line">    Disconnect() error</span><br><span class="line">    Init(...Option) error</span><br><span class="line">    Publish(<span class="keyword">string</span>, *Message, ...PublishOption) error</span><br><span class="line">    Subscribe(<span class="keyword">string</span>, Handler, ...SubscribeOption) (Subscriber, error)</span><br><span class="line">    String() <span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Register"><a href="#Register" class="headerlink" title="Register"></a>Register</h2><p><img src="https://blog.micro.mu/assets/images/service-discovery.png" alt=""></p><p>注册器是go-micro定义的服务发现接口，你也许想过。用消息系统做服务发现？这能工作吗？事实上它不仅能工作，还工作的很好。许多人在服务发现中使用消息系统，避免了不一致的发现机制。这是因为消息队列通过topics和channels实现了路由，Topics定义为服务的名字可以作为路由的key，订阅topics的服务自动实现了负载均衡。</p><p>Go-micro把服务发现和传输机制看做两个不同的领域，任何时候，客户端向服务发起请求，都会首先在注册器中根据服务的名字查询到正在运行的服务节点，然后客户端通过transport与节点进行通信。</p><p>一般来说，最常见的存储服务发现信息的方式是通过分布式key-value store，比如zookeeper、etcd等等。正如你了解到的，NATS闭市一个分布式的KV store，我们将做一下改动。。。</p><p>广播查询！</p><p>广播查询正如你想象的那样，服务都会监听一个特点的topic，任何需要服务发现信息的都需要订阅这个topic，然后对这个topic做一个反馈。</p><p>因为我们并不知道有多少服务正在运行，我们对响应时间设置一个上限。这是一个粗糙的服务发现机制，但因为NATS本身的高扩展性和高性能，它运行的反而非常好。它也间接的提高了过滤服务节点的响应时间。未来我们会试着提高底层的实现。</p><p>我们总结一下它是怎么工作的：</p><ol><li>创建一个reply topic并订阅</li><li>向广播topic发起查询，附带上reply topic</li><li>监听回复，超过限制时间就注销订阅</li><li>聚合响应并返回结果</li></ol><p><img src="https://blog.micro.mu/assets/images/nats-registry.png" alt=""></p><p>使用register插件</p><p>引入包</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> _ <span class="string">"github.com/micro/go-plugins/registry/nats"</span></span><br></pre></td></tr></table></figure><p>通过参数启动</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">go</span> run main.<span class="keyword">go</span> --registry=nats --registry_address=<span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span>:<span class="number">4222</span></span><br></pre></td></tr></table></figure><p>或者直接在代码中设置</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">registry := nats.NewRegistry()</span><br></pre></td></tr></table></figure><p>go-micro中的register接口</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Registry <span class="keyword">interface</span> &#123;</span><br><span class="line">    Register(*Service, ...RegisterOption) error</span><br><span class="line">    Deregister(*Service) error</span><br><span class="line">    GetService(<span class="keyword">string</span>) ([]*Service, error)</span><br><span class="line">    ListServices() ([]*Service, error)</span><br><span class="line">    Watch() (Watcher, error)</span><br><span class="line">    String() <span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="大规模在Micro中使用NATS"><a href="#大规模在Micro中使用NATS" class="headerlink" title="大规模在Micro中使用NATS"></a>大规模在Micro中使用NATS</h2><p>在上面的例子中，我们只是用了单个的NATS服务，但是在实际情况下，我们一般都是使用NATS集群来实现高可用和容错。你可以在<a href="http://nats.io/documentation/server/gnatsd-cluster/" target="_blank" rel="noopener">这里</a>看看更多NATS集群的知识。</p><p>Micro 在启动时可以接收一系列的参数，如环境变量等。如果直接使用client的库，也可以在创建时指定参数。</p><p>在目前云计算的架构下，我们过去的经验是，每个AZ都应该有集群。大部分的云计算公司，不同的AZ之间的延迟大概是3到5毫秒，集群的通信是没有任何问题的。当我们运行一个高可用的配置，你的系统必须要能在AZ宕机、甚至整个region崩溃时还能提供服务。我们不建议跨region部署集群。理想的高等级工具应该是用于管理多集群和多region系统。</p><p>Micro是一个极其灵活的微服务系统，它本身就被设计成运行在任何配置的任何地方。整个Micro世界是通过服务发现进行指导，服务的集群可以运行在机器集群中，AZ或者regions。再考虑到NATS集群，它可以让你构建高可用的服务。</p><p><img src="https://blog.micro.mu/assets/images/region.png" alt=""></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>NATS是一个高可用和高性能的消息系统，在我们的微服务生态系统中运行的很好。它与Micro配合的非常的好，我们可以把它用在register，transport，broker中，我们也已经全部实现了这些插件。</p><p>NATS在Micro中的使用只是一个示例，它展示了Micro高度的插件化架构，每个go-micro的包都可以被替换，底层不需要改动代码，上层也只需要改动非常少的代码。在未来我们还会看到更多的Micro on X，下一个可能是Micro on kubernetes。</p><p>希望这可以鼓励你来使用Micro on NATS,或者编写自己的插件并回馈给社区。</p><p>在这里看更多的NATS插件<a href="https://github.com/micro/go-plugins" target="_blank" rel="noopener">github.com/micro/go-plugins</a></p><p>如果你想了解更多，请看这个<a href="https://blog.micro.mu/" target="_blank" rel="noopener">blog</a>，或者这个<a href="https://github.com/micro/micro" target="_blank" rel="noopener">repo</a>，Twitter可以关注<a href="https://twitter.com/microhq" target="_blank" rel="noopener">@MicroHQ</a>，Slack社区在<a href="http://slack.micro.mu/" target="_blank" rel="noopener">这里</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这是一系列介绍&lt;a href=&quot;http://github.com/micro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Micro&lt;/a&gt;框架的文章的第四篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。&lt;/p&gt;
&lt;p&gt;这篇文章我们会讨
      
    
    </summary>
    
      <category term="微服务" scheme="http://gogap.cn/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
  <entry>
    <title>使用Go Micro编写微服务</title>
    <link href="http://gogap.cn/2016/03/28/go-micro/"/>
    <id>http://gogap.cn/2016/03/28/go-micro/</id>
    <published>2016-03-28T05:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>这是一系列介绍<a href="http://github.com/micro" target="_blank" rel="noopener">Micro</a>框架的文章的第三篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。</p><p>这是一个高等级的说明：怎样使用<a href="https://github.com/micro/go-micro" target="_blank" rel="noopener"><strong>go-micro</strong></a>来编写微服务，如果你想学习更多微服务的知识以及Micro的整体架构，参考以前的文章。</p><h2 id="什么是Go-Micro"><a href="#什么是Go-Micro" class="headerlink" title="什么是Go Micro?"></a>什么是Go Micro?</h2><p><a href="https://github.com/micro/go-micro" target="_blank" rel="noopener"><strong>Go Micro</strong></a>是一个插件化的基础框架，基于此可以构建微服务。Micro的设计哲学是『可插拔』的插件化架构。在架构之外，它默认实现了consul作为服务发现，通过http进行通信，通过protobuf和json进行编解码。我们一步步深入下去。</p><p>Go Micro是：</p><ol><li>一个用Golang编写的包</li><li>一系列插件化的接口定义</li><li>基于RPC</li></ol><p>Go Micro为下面的模块定义了接口：</p><ol><li>服务发现</li><li>编解码</li><li>服务端、客户端</li><li>订阅、发布消息</li></ol><p>更详细的说明可以在<a href="https://blog.micro.mu/2016/03/20/micro.html#go-micro" target="_blank" rel="noopener">这里</a>看到。</p><h2 id="为什么是Go-Micro"><a href="#为什么是Go-Micro" class="headerlink" title="为什么是Go Micro?"></a>为什么是Go Micro?</h2><p>Go Micro从一年多以前开始开发，最初只是个人需求，很快我发现这对那些编写微服务的程序员会有很大的价值。它基于我在不同的技术公司如google和hailo的开发经验编写而成。</p><p>就像前面提到的，Go Micro是一个golang编写的插件化架构，专注于提供底层的接口定义和基础工具。这些接口可以接纳各种实现。比如 <a href="https://godoc.org/github.com/micro/go-micro/registry" target="_blank" rel="noopener">Registry</a>接口定义了服务发现的接口，默认采用了consul作为服务发现的实现，但也可以采用其他实现比如etcd和zookeeper等，只要能满足接口，就可以使用。</p><p>插件化的架构意味着如果你想替换底层的实现，你不需要修改任何底层的代码。</p><h2 id="编写一个服务"><a href="#编写一个服务" class="headerlink" title="编写一个服务"></a>编写一个服务</h2><p>如果你想直接看代码，看这里：<a href="https://github.com/micro/go-micro/tree/master/examples/service" target="_blank" rel="noopener">examples/service</a></p><p>顶层的<a href="https://godoc.org/github.com/micro/go-micro#Service" target="_blank" rel="noopener">Service</a>接口是构建服务的主要组件。它把底层的各个包需要实现的接口，做了一次封装。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Service <span class="keyword">interface</span> &#123;</span><br><span class="line">    Init(...Option)</span><br><span class="line">    Options() Options</span><br><span class="line">    Client() client.Client</span><br><span class="line">    Server() server.Server</span><br><span class="line">    Run() error</span><br><span class="line">    String() <span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h3><p>一个服务可以这样创建<code>micro.NewService</code></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">"github.com/micro/go-micro"</span></span><br><span class="line"></span><br><span class="line">service := micro.NewService()</span><br></pre></td></tr></table></figure><p>参数可以在创建时传入</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">service := micro.NewService(</span><br><span class="line">micro.Name(<span class="string">"greeter"</span>),</span><br><span class="line">micro.Version(<span class="string">"latest"</span>),</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>所有可选的参数设置可以在<a href="https://godoc.org/github.com/micro/go-micro#Option" target="_blank" rel="noopener">这里</a>看到</p><p>Go Micro也提供了读取命令行的方式</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"github.com/micro/cli"</span></span><br><span class="line"><span class="string">"github.com/micro/go-micro"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">service := micro.NewService(</span><br><span class="line">micro.Flags(</span><br><span class="line">cli.StringFlag&#123;</span><br><span class="line">Name:  <span class="string">"environment"</span>,</span><br><span class="line">Usage: <span class="string">"The environment"</span>,</span><br><span class="line">&#125;,</span><br><span class="line">)</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>通过 <code>service.Init</code>来解析参数，附加的处理可以通过<code>micro.Action</code>解决</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">service.Init(</span><br><span class="line">micro.Action(<span class="function"><span class="keyword">func</span><span class="params">(c *cli.Context)</span></span> &#123;</span><br><span class="line">env := c.StringFlag(<span class="string">"environment"</span>)</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(env) &gt; <span class="number">0</span> &#123;</span><br><span class="line">fmt.Println(<span class="string">"Environment set to"</span>, env)</span><br><span class="line">&#125;</span><br><span class="line">&#125;),</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>Go Micro提供了提供了预定义的参数，也会被<code>service.Init</code>解析，<a href="https://godoc.org/github.com/micro/go-micro/cmd#pkg-variables" target="_blank" rel="noopener">这里</a>可以看到所有的flag</p><h3 id="定义API"><a href="#定义API" class="headerlink" title="定义API"></a>定义API</h3><p>我们使用protobuf文件来定义服务的API，这是一种方便且严格的定义方式，协议将会提供给服务端和客户端。下面是一个协议的例子：greeter.proto</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">syntax = <span class="string">"proto3"</span>;</span><br><span class="line"></span><br><span class="line">service Greeter &#123;</span><br><span class="line">rpc Hello(HelloRequest) returns (HelloResponse) &#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">message HelloRequest &#123;</span><br><span class="line"><span class="keyword">string</span> name = <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">message HelloResponse &#123;</span><br><span class="line"><span class="keyword">string</span> greeting = <span class="number">2</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里定义了一个服务叫做Greeter，它提供一个接口叫Hello，它接受HelloRequest的请求，返回HelloResponse。</p><h3 id="生成API接口"><a href="#生成API接口" class="headerlink" title="生成API接口"></a>生成API接口</h3><p>我们使用<code>protoc</code>和<code>proto-gen-go</code>这两个工具来生成代码，Go Micro也会生成客户端代码，减少工作量，这里需要使用我们fork并修改过的<a href="https://blog.micro.mu/2016/03/28/github.com/micro/protobuf" target="_blank" rel="noopener">github.com/micro/protobuf</a>，与原始版本的区别是，fork版本能生成客户端代码</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">go get github.com/micro/protobuf/&#123;proto,protoc-gen-go&#125;</span><br><span class="line">protoc --go_out=plugins=micro:. greeter.proto</span><br></pre></td></tr></table></figure><p>生成的代码可以在handler中引用相应的包进行使用，下面是生成的一部分代码</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> HelloRequest <span class="keyword">struct</span> &#123;</span><br><span class="line">Name <span class="keyword">string</span> <span class="string">`protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> HelloResponse <span class="keyword">struct</span> &#123;</span><br><span class="line">Greeting <span class="keyword">string</span> <span class="string">`protobuf:"bytes,2,opt,name=greeting" json:"greeting,omitempty"`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Client API for Greeter service</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> GreeterClient <span class="keyword">interface</span> &#123;</span><br><span class="line">Hello(ctx context.Context, in *HelloRequest, opts ...client.CallOption) (*HelloResponse, error)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> greeterClient <span class="keyword">struct</span> &#123;</span><br><span class="line">c           client.Client</span><br><span class="line">serviceName <span class="keyword">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewGreeterClient</span><span class="params">(serviceName <span class="keyword">string</span>, c client.Client)</span> <span class="title">GreeterClient</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> c == <span class="literal">nil</span> &#123;</span><br><span class="line">c = client.NewClient()</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(serviceName) == <span class="number">0</span> &#123;</span><br><span class="line">serviceName = <span class="string">"greeter"</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> &amp;greeterClient&#123;</span><br><span class="line">c:           c,</span><br><span class="line">serviceName: serviceName,</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *greeterClient)</span> <span class="title">Hello</span><span class="params">(ctx context.Context, in *HelloRequest, opts ...client.CallOption)</span> <span class="params">(*HelloResponse, error)</span></span> &#123;</span><br><span class="line">req := c.c.NewRequest(c.serviceName, <span class="string">"Greeter.Hello"</span>, in)</span><br><span class="line">out := <span class="built_in">new</span>(HelloResponse)</span><br><span class="line">err := c.c.Call(ctx, req, out, opts...)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> out, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Server API for Greeter service</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> GreeterHandler <span class="keyword">interface</span> &#123;</span><br><span class="line">Hello(context.Context, *HelloRequest, *HelloResponse) error</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">RegisterGreeterHandler</span><span class="params">(s server.Server, hdlr GreeterHandler)</span></span> &#123;</span><br><span class="line">s.Handle(s.NewHandler(&amp;Greeter&#123;hdlr&#125;))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="实现handler"><a href="#实现handler" class="headerlink" title="实现handler"></a>实现handler</h3><p>服务端需要注册handler来处理请求，一个handler是一个这样的方法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span><span class="params">(ctx context.Context, req <span class="keyword">interface</span>&#123;&#125;, rsp <span class="keyword">interface</span>&#123;&#125;)</span> <span class="title">error</span></span></span><br></pre></td></tr></table></figure><p>正如上面看到的，一个handler实现了API协议中定义的接口</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> GreeterHandler <span class="keyword">interface</span> &#123;</span><br><span class="line">        Hello(context.Context, *HelloRequest, *HelloResponse) error</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里是一个Greeter的handler实现</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> proto <span class="string">"github.com/micro/micro/examples/service/proto"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Greeter <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(g *Greeter)</span> <span class="title">Hello</span><span class="params">(ctx context.Context, req *proto.HelloRequest, rsp *proto.HelloResponse)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">rsp.Greeting = <span class="string">"Hello "</span> + req.Name</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>handler需要注册到某个服务</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">service := micro.NewService(</span><br><span class="line">micro.Name(<span class="string">"greeter"</span>),</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">proto.RegisterGreeterHandler(service.Server(), <span class="built_in">new</span>(Greeter))</span><br></pre></td></tr></table></figure><h3 id="运行服务"><a href="#运行服务" class="headerlink" title="运行服务"></a>运行服务</h3><p>服务可以直接调用<code>server.Run()</code>来运行，这会让服务监听一个随机端口，这个调用也会让服务将自身注册到注册器，当服务停止运行时，会在注册器注销自己。</p><h3 id="完整的服务端"><a href="#完整的服务端" class="headerlink" title="完整的服务端"></a>完整的服务端</h3><p>greeter.go</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">        <span class="string">"log"</span></span><br><span class="line"></span><br><span class="line">        <span class="string">"github.com/micro/go-micro"</span></span><br><span class="line">        proto <span class="string">"github.com/micro/go-micro/examples/service/proto"</span></span><br><span class="line"></span><br><span class="line">        <span class="string">"golang.org/x/net/context"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Greeter <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(g *Greeter)</span> <span class="title">Hello</span><span class="params">(ctx context.Context, req *proto.HelloRequest, rsp *proto.HelloResponse)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">        rsp.Greeting = <span class="string">"Hello "</span> + req.Name</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">        service := micro.NewService(</span><br><span class="line">                micro.Name(<span class="string">"greeter"</span>),</span><br><span class="line">                micro.Version(<span class="string">"latest"</span>),</span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line">        service.Init()</span><br><span class="line"></span><br><span class="line">        proto.RegisterGreeterHandler(service.Server(), <span class="built_in">new</span>(Greeter))</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> err := service.Run(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">                log.Fatal(err)</span><br><span class="line">        &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意，服务发现机制需要首先运行起来，这样服务才能注册到注册器中，才能被客户端发现，<a href="https://github.com/micro/go-micro#getting-started" target="_blank" rel="noopener">这里</a>有一个简单的介绍。</p><h2 id="编写客户端"><a href="#编写客户端" class="headerlink" title="编写客户端"></a>编写客户端</h2><p> <a href="https://godoc.org/github.com/micro/go-micro/client" target="_blank" rel="noopener">client</a> 包用于向服务端发起请求，当你创建一个服务，客户端可以调用的接口已经自动生成了</p><p>调用上面的服务可以用下面的客户端代码</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">/ create the greeter client using the service name and client</span><br><span class="line">greeter := proto.NewGreeterClient(<span class="string">"greeter"</span>, service.Client())</span><br><span class="line"></span><br><span class="line"><span class="comment">// request the Hello method on the Greeter handler</span></span><br><span class="line">rsp, err := greeter.Hello(context.TODO(), &amp;proto.HelloRequest&#123;</span><br><span class="line">Name: <span class="string">"John"</span>,</span><br><span class="line">&#125;)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">fmt.Println(err)</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fmt.Println(rsp.Greeter)</span><br></pre></td></tr></table></figure><p><code>proto.NewGreeterClient</code>就是我们刚才生成的代码，根据服务名称发送请求。</p><p>完整的示例可以在这里看到：<a href="https://github.com/micro/go-micro/tree/master/examples/service" target="_blank" rel="noopener">go-micro/examples/service</a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这篇文章是一个高等级的使用说明，介绍了怎样编写客户端和服务端代码，你可以在这里看到更多示例代码：<a href="https://github.com/micro" target="_blank" rel="noopener">github.com/micro</a></p><p>如果你想了解更多，请看这个<a href="https://blog.micro.mu/" target="_blank" rel="noopener">blog</a>，或者这个<a href="https://github.com/micro/micro" target="_blank" rel="noopener">repo</a>，Twitter可以关注<a href="https://twitter.com/microhq" target="_blank" rel="noopener">@MicroHQ</a>，Slack社区在<a href="http://slack.micro.mu/" target="_blank" rel="noopener">这里</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这是一系列介绍&lt;a href=&quot;http://github.com/micro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Micro&lt;/a&gt;框架的文章的第三篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。&lt;/p&gt;
&lt;p&gt;这是一个高等级的
      
    
    </summary>
    
      <category term="微服务" scheme="http://gogap.cn/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
  <entry>
    <title>Micro - 微服务工具箱</title>
    <link href="http://gogap.cn/2016/03/20/micro/"/>
    <id>http://gogap.cn/2016/03/20/micro/</id>
    <published>2016-03-20T01:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>这是一系列介绍<a href="http://github.com/micro" target="_blank" rel="noopener">Micro</a>框架的文章的第二篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。</p><p>现在你也许听到了这个新现象：微服务。如果你对此不熟悉也有兴趣学习，欢迎参考上一篇文章。</p><p>这篇文章我们将讨论Micro - 一个开源的微服务工具箱，Micro提供了核心的必须工具来构建和管理微服务。它包含了一系列由golang开发的库和工具，同时也通过Sidecar特性与其他语言兼容。</p><p>在我们开始了解Micro之前，我们讨论一下为什么我们要把时间花费在它上面。</p><h2 id="开发与部署"><a href="#开发与部署" class="headerlink" title="开发与部署"></a>开发与部署</h2><p>从我们过去在软件工程领域的经验可以很清晰的看到，我们有这样一种需求：专注于开发而不是部署。PasS的解决方案是可行的，类似AWS，Google和微软的公司，提供了功能丰富的平台，并快速的推进着容器技术（container）。所有的这些让我们点几下鼠标就能使用大规模计算服务。</p><p>新世界看着不错，你也许说这解决了你所有的问题，真的是这样吗?当我们能接触到大规模计算的能力时，仍然缺少工具来让我们发挥出大规模计算的优势。不仅如此，在这个新世界中，容器的生命周期变得更加短暂，在运行时调度中不断创建和销毁。</p><h2 id="规模的挑战"><a href="#规模的挑战" class="headerlink" title="规模的挑战"></a>规模的挑战</h2><p>另一个问题是，正如我们一次又一次看到的，我们一直是巨型架构的受害者。随着功能需求的增加，现在的趋势是在巨型系统上不断增加功能，直到不断增加的技术债务让我们回天乏术。除此以外，随着组织不断扩张工程师团队，开发者想要单独的进行代码开发，或者开发功能时不被其他人block,变得极其困难。</p><p>这是一种难以避免的需求：重新进行架构设计，使用SOA或者微服务架构。公司需要在研发上投入努力，在尝试和错误中学习。现在正需要这样一种工具来帮助我们构建可扩展系统，减少研发部门的阻碍，由在此领域有经验的人士为大家提供建议。</p><h2 id="了解Micro"><a href="#了解Micro" class="headerlink" title="了解Micro"></a>了解Micro</h2><p>在Micro中我们构建了一个微服务生态系统，包括用于开发的基本的工具、服务和解决方案。我们已经构建好了基础的工具，这个工具与整个项目同名，也叫<a href="http://github.com/micro/micro" target="_blank" rel="noopener">Micro</a>,这一工具让我们更容易构建可扩展的架构，提供效率。</p><p>让我们更深入的挖掘Micro的特性。</p><h2 id="Go-Micro"><a href="#Go-Micro" class="headerlink" title="Go Micro"></a>Go Micro</h2><p><a href="http://github.com/micro/go-micro" target="_blank" rel="noopener">Go Micro</a> 是一个golang编写的用于构建微服务的插件化的RPC框架。它实现了服务创建、服务发现、服务间通信需要的功能需求。任何优秀的微服务架构都需要解决这三个基础问题：服务发现、同步通信和异步通信。</p><p>Go Micro包括以下这些包和功能</p><ul><li><p>Registry：客户端的服务发现</p></li><li><p>Transport：同步通信</p></li><li><p>Broker：异步通信</p></li><li><p>Selector：节点过滤、负载均衡</p></li><li><p>Codec：消息编解码</p></li><li><p>Server：基于此库构建RPC服务端</p></li><li><p>Client：基于此库构建RPC客户端</p><p>​</p></li></ul><p>Go Micro跟其他工具最大的不同是它是插件化的架构，这让上面每个包的具体实现都可以切换出去。举个例子，默认的服务发现的机制是通过Consul，但是如果想切换成etcd或者zookeeper 或者任何你实现的方案，都是非常便利的。官方实现的插件可以在这个地址看到：[<a href="https://github.com/micro/go-plugins" target="_blank" rel="noopener">github.com/micro/go-plugins</a>]</p><p>插件化架构的最大好处是你可以选择你喜欢的平台来支撑微服务架构，但无需更改任何底层代码。Go Micro无需任何更改，只需要import你的插件，直接使用即可。</p><p>Go Micro是编写微服务的切入点，<a href="https://github.com/micro/go-micro" target="_blank" rel="noopener"><strong>readme</strong></a>提供了说明包括怎样编写、运行和查询一个服务。这个greeter示例可以参考：<a href="https://github.com/micro/micro/tree/master/examples/greeter" target="_blank" rel="noopener">micro/examples/greeter</a> ，更多的服务示例可以在这个工程看到： <a href="https://github.com/micro" target="_blank" rel="noopener">github.com/micro</a></p><h2 id="Sidecar"><a href="#Sidecar" class="headerlink" title="Sidecar"></a>Sidecar</h2><p>Go Micro提供了用Golang编写服务的方式，那么其他编程语言呢？我们怎样构建一个有兼容性的系统，让任何人都能受益于MIcro？虽然Micro是用golang编写的，我们提供了一个快速且方便的方式，让其他语言能够接入。</p><p> <a href="https://github.com/micro/micro/tree/master/car" target="_blank" rel="noopener"><strong>Sidecar</strong></a>是一个轻量级的组装服务，概念上来说就是将Micro的库提供的功能，依附于其他语言的主程序中。sidecar本质上是一个单独运行的服务，通过http提供接口，其他语言通过接口使用Go Micro提供的功能。</p><p>sidecar的特性：</p><ul><li>在服务发现系统进行注册</li><li>发现其他服务</li><li>与主程序进行健康检查</li><li>作为代理与RPC系统通信</li><li>通过websocket订阅</li></ul><p><img src="https://blog.micro.mu/assets/images/sidecar.png" alt=""></p><p>用ruby和python借助sidecar进行使用的例子可以在这里看到<a href="https://github.com/micro/micro/tree/master/examples/greeter" target="_blank" rel="noopener">micro/examples/greeter</a>，我们会提供更多示例，帮助理解sidecar的使用。</p><h2 id="API"><a href="#API" class="headerlink" title="API"></a>API</h2><p>服务之间的请求是非常简单直接的，但从外部接入就要复杂一些。具体的服务实例可能会崩溃，重新调度，并监听随机的端口。<a href="https://github.com/micro/micro/tree/master/api" target="_blank" rel="noopener"><strong>API</strong></a>这个组件提供了一个接入点，外部的服务可以通过这个API网关向内部的服务发起请求。</p><p>API提供了几种不同的请求方式</p><h3 id="rpc"><a href="#rpc" class="headerlink" title="/rpc"></a>/rpc</h3><p>每个单独的服务可以通过/rpc这个接入点进行访问，示例如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">curl \</span><br><span class="line">-d "service=go.micro.srv.greeter" \</span><br><span class="line">-d "method=Say.Hello" \</span><br><span class="line">-d "request=&#123;\"name\": \"John\"&#125;" \</span><br><span class="line">http://localhost:8080/rpc</span><br><span class="line"></span><br><span class="line">&#123;"msg":"Hello John"&#125;</span><br></pre></td></tr></table></figure><h3 id="api-Request"><a href="#api-Request" class="headerlink" title="api.Request"></a>api.Request</h3><p>API也可以通过约定好的URL格式，请求到内部的服务，这是API服务的一个强大功能。经过URL解析能将路径转换成实际的请求，示例如下</p><p>请求</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GET /greeter/say/hello?name=John</span><br></pre></td></tr></table></figure><p>将会处理成</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">service: go.micro.api.greeter (default namespace go.micro.api is applied)</span><br><span class="line">method: Say.Hello</span><br><span class="line">request &#123;</span><br><span class="line">"method": "GET",</span><br><span class="line">"path": "/greeter/say/hello",</span><br><span class="line">"get": &#123;</span><br><span class="line">"name": "John"</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里看看通过protobuf定义的这个接口</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">syntax = <span class="string">"proto3"</span>;</span><br><span class="line"></span><br><span class="line">message Pair &#123;</span><br><span class="line">optional <span class="keyword">string</span> key = <span class="number">1</span>;</span><br><span class="line">repeated <span class="keyword">string</span> values = <span class="number">2</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">message Request &#123;</span><br><span class="line">optional <span class="keyword">string</span> method = <span class="number">1</span>;   <span class="comment">// GET, POST, etc</span></span><br><span class="line">optional <span class="keyword">string</span> path = <span class="number">2</span>;     <span class="comment">// e.g /greeter/say/hello</span></span><br><span class="line"><span class="keyword">map</span>&lt;<span class="keyword">string</span>, Pair&gt; header = <span class="number">3</span>;</span><br><span class="line"><span class="keyword">map</span>&lt;<span class="keyword">string</span>, Pair&gt; get = <span class="number">4</span>;    <span class="comment">// The URI query params</span></span><br><span class="line"><span class="keyword">map</span>&lt;<span class="keyword">string</span>, Pair&gt; post = <span class="number">5</span>;   <span class="comment">// The post body params</span></span><br><span class="line">optional <span class="keyword">string</span> body = <span class="number">6</span>;     <span class="comment">// raw request body; if not application/x-www-form-urlencoded</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">message Response &#123;</span><br><span class="line">optional <span class="keyword">int32</span> statusCode = <span class="number">1</span>;</span><br><span class="line"><span class="keyword">map</span>&lt;<span class="keyword">string</span>, Pair&gt; header = <span class="number">2</span>;</span><br><span class="line">optional <span class="keyword">string</span> body = <span class="number">3</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用示例可以在这里看到：<a href="https://github.com/micro/micro/blob/master/examples/greeter/api/api.go" target="_blank" rel="noopener">Greeter API</a></p><h3 id="反向代理"><a href="#反向代理" class="headerlink" title="反向代理"></a>反向代理</h3><p>最后一个API服务提供的功能是反向代理。正如上面例子中提到的，API服务可以通过路径解析到具体的服务，通过添加参数<code>--api_handler=proxy</code> 我们就可以支持REST风格的请求。反向代理只需要简单的在运行时天机<code>--api_handler=proxy</code> 参数即可。</p><p>使用API构建RESTful 风格的API可以在这个例子中看到：<a href="https://github.com/micro/micro/tree/master/examples/greeter/api/go-restful" target="_blank" rel="noopener">micro/examples/greeter/api</a></p><h2 id="Web-UI"><a href="#Web-UI" class="headerlink" title="Web UI"></a>Web UI</h2><p>web UI 提供了一个简单的界面观察运行中的系统，也可以进行一些交互。它提供了类似API这样的反向代理功能，我们的『web代理』也可以把开发好的其他web应用接入到web UI中，web UI与API一样仍然通过路径解析实现与内部服务的通信，通过websocket我们可以实时了解运行中系统的情况</p><p><img src="https://blog.micro.mu/assets/images/web.png" alt=""></p><h2 id="CLI"><a href="#CLI" class="headerlink" title="CLI"></a>CLI</h2><p>CLI是一个命令行工具，让我们可以观察、交互和管理运行中的服务，当前的特性允许你查询服务注册，检查服务的健康情况，也可以对服务进行请求</p><p><img src="https://blog.micro.mu/assets/images/cli.png" alt=""></p><p>其他有意思的特性包括，CLI可以使用Sidecar作为代理，只需要简单的设置参数：<code>--proxy_address=example.proxy.com</code></p><h2 id="组装在一起"><a href="#组装在一起" class="headerlink" title="组装在一起"></a>组装在一起</h2><p>我们已经写了一个全功能的示例，整体的执行过程是这样的：</p><ol><li>HTTP GET请求到API服务，请求地址是：/greeter/say/hello with the query name=John</li><li>API服务将请求解析并转换成默认的服务形式，服务是go.micro.api.greeter，方法是 Say.Hello</li><li>API使用Go Micro，查询注册器中服务go.micro.api.greeter注册的所有节点，根据负载均衡算法，选择其中一个节点，发出请求</li><li>go.micro.api.greeter服务收到请求，解析到结构体，去注册器查询到go.micro.srv.greeter这个服务，发送请求</li><li>greet server处理完成后，返回相应的内容到greet api</li><li>greet api转换greet服务的响应内容到 api.Response，返回到API服务</li><li>API服务解析请求，返回HTTP请求</li></ol><p>整体流程如下：</p><p><img src="https://blog.micro.mu/assets/images/greeter.png" alt=""></p><p>有更复杂的例子，比如API服务请求多个服务，组装多个服务的返回内容。示例如下：<a href="https://github.com/micro/micro/tree/master/examples/greeter" target="_blank" rel="noopener">greeter service</a></p><h2 id="Demo"><a href="#Demo" class="headerlink" title="Demo"></a>Demo</h2><p>如果你想看看正在运行中的系统，在这个页面查看：<a href="http://web.micro.pm/" target="_blank" rel="noopener">web.micro.pm</a></p><p>我们运行了一个Micro在Kubernetes上，demo是开源的，你可以运行一下：<a href="https://github.com/micro/kubernetes" target="_blank" rel="noopener">github.com/micro/kubernetes</a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Micro提供了基础的工具用于编写和管理微服务，Go Micro包括了核心的必须功能：服务发现、客户端、服务端和订阅、发布。CLI可以让你与运行中的服务进行交互。Sidecar可以让你接入其他非Micro应用。API是一个单独的接入点来调用内部的服务。借助于插件化的接口，你可以灵活选择各种组件来提升你的微服务。</p><p>在Micro我们的目标是让开发人员在一开始就能应对大规模开发，提高工作效率。我们感觉Micro是最好的选择。随着时间推移，整个微服务生态系统功能会更加完善。</p><p>如果你想了解更多，请看这个<a href="https://blog.micro.mu/" target="_blank" rel="noopener">blog</a>，或者这个<a href="https://github.com/micro/micro" target="_blank" rel="noopener">repo</a>，Twitter可以关注<a href="https://twitter.com/microhq" target="_blank" rel="noopener">@MicroHQ</a>，Slack社区在<a href="http://slack.micro.mu/" target="_blank" rel="noopener">这里</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这是一系列介绍&lt;a href=&quot;http://github.com/micro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Micro&lt;/a&gt;框架的文章的第二篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。&lt;/p&gt;
&lt;p&gt;现在你也许听到了
      
    
    </summary>
    
      <category term="微服务" scheme="http://gogap.cn/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
  <entry>
    <title>Micro - 微服务生态系统</title>
    <link href="http://gogap.cn/2016/03/17/introduction/"/>
    <id>http://gogap.cn/2016/03/17/introduction/</id>
    <published>2016-03-17T09:21:35.000Z</published>
    <updated>2017-09-19T06:23:11.000Z</updated>
    
    <content type="html"><![CDATA[<p>这是一系列介绍<a href="http://github.com/micro" target="_blank" rel="noopener">Micro</a>框架的文章的第一篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。</p><p>让我们讨论一下软件开发的特性。</p><p>变化总是在进行中，我们越来越接近一个被技术和商业驱动的社会，维持竞争能力变得越来越困难，如果采用低效的平台、结构和代码，组织将会越来越低效。创立十年以上的技术公司正在经历扩张带来的技术痛苦，但大部分仍然采用旧有的技术解决新问题。</p><p>是时候把世界上最成功的公司的竞争优势分享给其他人了，现在我们讨论微服务，一种构建你的核心技术优势的手段。</p><h2 id="什么是微服务"><a href="#什么是微服务" class="headerlink" title="什么是微服务?"></a>什么是微服务?</h2><p>微服务是一种软件架构模式，用于将大型架构拆解成小型模块，服务之间使用灵活的协议进行通信，使各个服务专注于自身的业务。</p><p>用学院派的定义来说明微服务:</p><ul><li><p>Loosely coupled service oriented architecture with a bounded context</p></li><li><p>An approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms</p></li></ul><p>微服务并不是一个新的概念，这是一种新的服务架构模式，但早在unix中类似的理念就出现在进程和管道中。</p><p>微服务架构的哲学是这样的</p><ul><li>服务是很小的，单一的服务只做单一的业务，类似unix中的 『Do one thing and do it well』</li><li>应该适合进行自动化部署和测试，可以轻易的减轻运维和管理的负担</li><li>系统必须有很好的容错性，健壮性</li></ul><h2 id="为什么是微服务"><a href="#为什么是微服务" class="headerlink" title="为什么是微服务?"></a>为什么是微服务?</h2><p>随着组织的技术和人员扩张，庞大的代码已经越来越难以维护。我们都认为twitter会失败，因为他们尝试在现有的巨大的系统上不断进行产品需求的开发。微服务的理念让Twitter分解整个应用为很小服务，每个服务都被单个小型团队负责。每个团队都为整个系统负责，而每个服务又可以单独的进行部署。</p><p><a href="https://blog.micro.mu/assets/images/micro-service-architecture.png" target="_blank" rel="noopener"></a></p><p><img src="https://blog.micro.mu/assets/images/micro-service-architecture.png" alt="alt"></p><p>我们从第一手的经验知道了微服务让开发周期进行的更快，提高了生产力，构造了优秀的可扩展系统</p><p>我们看一下其中的一些好处：</p><ul><li>更容易进行开发：不同的团队根据不同的需求，管理好自己的服务即可</li><li>更容易理解：微服务很小，经常是1000行或者更少</li><li>更容易频繁的部署新版本：服务很容易独立的进行部署、扩展和管理</li><li>提高了错误的容忍度和错误隔离：单个服务的错误不会对其他服务造成影响</li><li>提高了执行的速度：团队独立的开发、部署和管理微服务将使需求实现的更快</li><li><p>服务可以重用：unix的设计理念影响了微服务，这让你可以复用很多服务</p><p>​</p></li></ul><h2 id="什么是Micro"><a href="#什么是Micro" class="headerlink" title="什么是Micro?"></a>什么是Micro?</h2><p>Micro是一个微服务的生态系统，专注于为当代科技驱动的企业提供产品、服务和解决方案。我们计划为企业提供微服务资源以提高企业的技术水平。从早期的产品原型到大规模产品的部署都有解决方案。</p><p>我们看到了行业的基本的转折点正在到来，摩尔定律在起作用，我们拥有了越来越多的能力，而我们并不能完全了解这些全新的能力，当前的工具和开发实践不能在新的领域再起作用。开发者没有获得工具来从庞大的代码系统转向更加高效的设计架构。大部分公司会经历到一个节点即大量的研发投入到庞大的系统中，但没有产生相应的产出。Netfix，Twitter等公司都经历了这些，结局都是构建自己的微服务平台。</p><p>我们的愿景是提供基础的工具让任何人都能受益于微服务，我们已经开始行动，基于开源的微服务工具包<a href="http://github.com/micro" target="_blank" rel="noopener">Micro</a> ，接下来将会有一系列的文件介绍各个工具。</p><p>如果你想了解更多，请看这个<a href="https://blog.micro.mu/" target="_blank" rel="noopener">blog</a>，或者这个<a href="https://github.com/micro/micro" target="_blank" rel="noopener">repo</a>，Twitter可以关注<a href="https://twitter.com/microhq" target="_blank" rel="noopener">@MicroHQ</a>，Slack社区在<a href="http://slack.micro.mu/" target="_blank" rel="noopener">这里</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;这是一系列介绍&lt;a href=&quot;http://github.com/micro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Micro&lt;/a&gt;框架的文章的第一篇，我将会把作者的博客翻译成中文，推广Micro这个微服务框架。&lt;/p&gt;
&lt;p&gt;让我们讨论一下软
      
    
    </summary>
    
      <category term="微服务" scheme="http://gogap.cn/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
    
      <category term="Micro" scheme="http://gogap.cn/tags/Micro/"/>
    
  </entry>
  
</feed>
