<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>徐靖峰|个人博客</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://lexburner.github.io/"/>
  <updated>2021-03-28T09:14:47.774Z</updated>
  <id>https://lexburner.github.io/</id>
  
  <author>
    <name>徐靖峰</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Netty实现长连接服务的难点和优化点</title>
    <link href="https://lexburner.github.io/netty-persistent-connection-difficulty-and-improvement/"/>
    <id>https://lexburner.github.io/netty-persistent-connection-difficulty-and-improvement/</id>
    <published>2021-03-28T06:59:28.000Z</published>
    <updated>2021-03-28T09:14:47.774Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>转载自：<a href="https://www.dozer.cc/2014/12/netty-long-connection.html" target="_blank" rel="noopener">https://www.dozer.cc/2014/12/netty-long-connection.html</a></p><p>原文作者：dozer</p></blockquote><h2 id="推送服务"><a href="#推送服务" class="headerlink" title="推送服务"></a>推送服务</h2><p>还记得一年半前，做的一个项目需要用到 Android 推送服务。和 iOS 不同，Android 生态中没有统一的推送服务。Google 虽然有 <a href="http://zh.wikipedia.org/wiki/Google雲端通訊" target="_blank" rel="noopener">Google Cloud Messaging</a> ，但是连国外都没统一，更别说国内了，直接被墙。</p><p>所以之前在 Android 上做推送大部分只能靠轮询。而我们之前在技术调研的时候，搜到了 <a href="https://www.jpush.cn/" target="_blank" rel="noopener">jPush</a> 的博客，上面介绍了一些他们的技术特点，他们主要做的其实就是移动网络下的长连接服务。单机 50W-100W 的连接的确是吓我一跳！后来我们也采用了他们的免费方案，因为是一个受众面很小的产品，所以他们的免费版够我们用了。一年多下来，运作稳定，非常不错！</p><p>时隔两年，换了部门后，竟然接到了一项任务，优化公司自己的长连接服务端。</p><p>再次搜索网上技术资料后才发现，相关的很多难点都被攻破，网上也有了很多的总结文章，单机 50W-100W 的连接完全不是梦，其实人人都可以做到。但是光有连接还不够，QPS 也要一起上去。</p><p>所以，这篇文章就是汇总一下利用 Netty 实现长连接服务过程中的各种难点和可优化点。</p><a id="more"></a><h2 id="Netty-是什么"><a href="#Netty-是什么" class="headerlink" title="Netty 是什么"></a>Netty 是什么</h2><p>Netty: <a href="http://netty.io/" target="_blank" rel="noopener">http://netty.io/</a></p><blockquote><p>Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers &amp; clients.</p></blockquote><p>官方的解释最精准了，期中最吸引人的就是高性能了。但是很多人会有这样的疑问：直接用 NIO 实现的话，一定会更快吧？就像我直接手写 JDBC 虽然代码量大了点，但是一定比 iBatis 快！</p><p>但是，如果了解 Netty 后你才会发现，这个还真不一定！</p><p>利用 Netty 而不用 NIO 直接写的优势有这些：</p><ul><li>高性能高扩展的架构设计，大部分情况下你只需要关注业务而不需要关注架构</li><li><code>Zero-Copy</code> 技术尽量减少内存拷贝</li><li>为 Linux 实现 Native 版 Socket</li><li>写同一份代码，兼容 java 1.7 的 NIO2 和 1.7 之前版本的 NIO</li><li><code>Pooled Buffers</code> 大大减轻 <code>Buffer</code> 和释放 <code>Buffer</code> 的压力</li><li>……</li></ul><p>特性太多，大家可以去看一下《Netty in Action》这本书了解更多。</p><p>另外，Netty 源码是一本很好的教科书！大家在使用的过程中可以多看看它的源码，非常棒！</p><h2 id="瓶颈是什么"><a href="#瓶颈是什么" class="headerlink" title="瓶颈是什么"></a>瓶颈是什么</h2><p>想要做一个长链服务的话，最终的目标是什么？而它的瓶颈又是什么？</p><p>其实目标主要就两个：</p><ol><li>更多的连接</li><li>更高的 QPS</li></ol><p>所以，下面就针对这连个目标来说说他们的难点和注意点吧。</p><h2 id="更多的连接"><a href="#更多的连接" class="headerlink" title="更多的连接"></a>更多的连接</h2><h3 id="非阻塞-IO"><a href="#非阻塞-IO" class="headerlink" title="非阻塞 IO"></a>非阻塞 IO</h3><p>其实无论是用 Java NIO 还是用 Netty，达到百万连接都没有任何难度。因为它们都是非阻塞的 IO，不需要为每个连接创建一个线程了。</p><p>欲知详情，可以搜索一下<code>BIO</code>,<code>NIO</code>,<code>AIO</code>的相关知识点。</p><h3 id="Java-NIO-实现百万连接"><a href="#Java-NIO-实现百万连接" class="headerlink" title="Java NIO 实现百万连接"></a>Java NIO 实现百万连接</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><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></pre></td><td class="code"><pre><span class="line">ServerSocketChannel ssc = ServerSocketChannel.open();</span><br><span class="line">Selector sel = Selector.open();</span><br><span class="line"></span><br><span class="line">ssc.configureBlocking(false);</span><br><span class="line">ssc.socket().bind(new InetSocketAddress(8080));</span><br><span class="line">SelectionKey key = ssc.register(sel, SelectionKey.OP_ACCEPT);</span><br><span class="line"></span><br><span class="line">while(true) &#123;</span><br><span class="line">    sel.select();</span><br><span class="line">    Iterator it = sel.selectedKeys().iterator();</span><br><span class="line">    while(it.hasNext()) &#123;</span><br><span class="line">        SelectionKey skey = (SelectionKey)it.next();</span><br><span class="line">        it.remove();</span><br><span class="line">        if(skey.isAcceptable()) &#123;</span><br><span class="line">            ch = ssc.accept();</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>这段代码只会接受连过来的连接，不做任何操作，仅仅用来测试待机连接数极限。</p><p>大家可以看到这段代码是 NIO 的基本写法，没什么特别的。</p><h3 id="Netty-实现百万连接"><a href="#Netty-实现百万连接" class="headerlink" title="Netty 实现百万连接"></a>Netty 实现百万连接</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><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">NioEventLoopGroup bossGroup =  new NioEventLoopGroup();</span><br><span class="line">NioEventLoopGroup workerGroup= new NioEventLoopGroup();</span><br><span class="line">ServerBootstrap bootstrap = new ServerBootstrap();</span><br><span class="line">bootstrap.group(bossGroup, workerGroup);</span><br><span class="line"></span><br><span class="line">bootstrap.channel( NioServerSocketChannel.class);</span><br><span class="line"></span><br><span class="line">bootstrap.childHandler(new ChannelInitializer&lt;SocketChannel&gt;() &#123;</span><br><span class="line">    @Override protected void initChannel(SocketChannel ch) throws Exception &#123;</span><br><span class="line">        ChannelPipeline pipeline = ch.pipeline();</span><br><span class="line">        //todo: add handler</span><br><span class="line">    &#125;&#125;);</span><br><span class="line">bootstrap.bind(8080).sync();</span><br></pre></td></tr></table></figure><p>这段其实也是非常简单的 Netty 初始化代码。同样，为了实现百万连接根本没有什么特殊的地方。</p><h3 id="瓶颈到底在哪"><a href="#瓶颈到底在哪" class="headerlink" title="瓶颈到底在哪"></a>瓶颈到底在哪</h3><p>上面两种不同的实现都非常简单，没有任何难度，那有人肯定会问了：实现百万连接的瓶颈到底是什么？</p><p>其实只要 java 中用的是非阻塞 IO（NIO 和 AIO 都算），那么它们都可以用单线程来实现大量的 Socket 连接。 不会像 BIO 那样为每个连接创建一个线程，因为代码层面不会成为瓶颈。</p><p>其实真正的瓶颈是在 Linux 内核配置上，默认的配置会限制全局最大打开文件数(Max Open Files)还会限制进程数。 所以需要对 Linux 内核配置进行一定的修改才可以。</p><p>这个东西现在看似很简单，按照网上的配置改一下就行了，但是大家一定不知道第一个研究这个人有多难。</p><p>这里直接贴几篇文章，介绍了相关配置的修改方式：</p><p><a href="http://www.ideawu.net/blog/archives/740.html" target="_blank" rel="noopener">构建C1000K的服务器</a></p><p><a href="http://www.blogjava.net/yongboy/archive/2013/04/11/397677.html" target="_blank" rel="noopener">100万并发连接服务器笔记之1M并发连接目标达成</a></p><p><a href="http://www.linuxde.net/2013/08/15150.html" target="_blank" rel="noopener">淘宝技术分享 HTTP长连接200万尝试及调优</a></p><h3 id="如何验证"><a href="#如何验证" class="headerlink" title="如何验证"></a>如何验证</h3><p>让服务器支持百万连接一点也不难，我们当时很快就搞定了一个测试服务端，但是最大的问题是，我怎么去验证这个服务器可以支撑百万连接呢？</p><p>我们用 Netty 写了一个测试客户端，它同样用了非阻塞 IO ，所以不用开大量的线程。 但是一台机器上的端口数是有限制的，用<code>root</code>权限的话，最多也就 6W 多个连接了。 所以我们这里用 Netty 写一个客户端，用尽单机所有的连接吧。</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><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></pre></td><td class="code"><pre><span class="line">NioEventLoopGroup workerGroup =  new NioEventLoopGroup();</span><br><span class="line">Bootstrap b = new Bootstrap();</span><br><span class="line">b.group(workerGroup);</span><br><span class="line">b.channel( NioSocketChannel.class);</span><br><span class="line"></span><br><span class="line">b.handler(new ChannelInitializer&lt;SocketChannel&gt;() &#123;</span><br><span class="line">    @Override</span><br><span class="line">    public void initChannel(SocketChannel ch) throws Exception &#123;</span><br><span class="line">        ChannelPipeline pipeline = ch.pipeline();</span><br><span class="line">        //todo:add handler</span><br><span class="line">    &#125;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">for (int k = 0; k &lt; 60000; k++) &#123;</span><br><span class="line">    //请自行修改成服务端的IP</span><br><span class="line">    b.connect(127.0.0.1, 8080);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码同样很简单，只要连上就行了，不需要做任何其他的操作。</p><p>这样只要找到一台电脑启动这个程序即可。这里需要注意一点，客户端最好和服务端一样，修改一下 Linux 内核参数配置。 </p><h3 id="怎么去找那么多机器"><a href="#怎么去找那么多机器" class="headerlink" title="怎么去找那么多机器"></a>怎么去找那么多机器</h3><p>按照上面的做法，单机最多可以有 6W 的连接，百万连接起码需要17台机器！</p><p>如何才能突破这个限制呢？其实这个限制来自于网卡。 我们后来通过使用虚拟机，并且把虚拟机的虚拟网卡配置成了桥接模式解决了问题。</p><p>根据物理机内存大小，单个物理机起码可以跑4-5个虚拟机，所以最终百万连接只要4台物理机就够了。</p><h3 id="讨巧的做法"><a href="#讨巧的做法" class="headerlink" title="讨巧的做法"></a>讨巧的做法</h3><p>除了用虚拟机充分压榨机器资源外，还有一个非常讨巧的做法，这个做法也是我在验证过程中偶然发现的。</p><p>根据 TCP/IP 协议，任何一方发送<code>FIN</code>后就会启动正常的断开流程。而如果遇到网络瞬断的情况，连接并不会自动断开。</p><p>那我们是不是可以这样做？</p><ol><li>启动服务端，千万别设置 Socket 的<code>keep-alive</code>属性，默认是不设置的</li><li>用虚拟机连接服务器</li><li>强制关闭虚拟机</li><li>修改虚拟机网卡的 MAC 地址，重新启动并连接服务器</li><li>服务端接受新的连接，并保持之前的连接不断</li></ol><p>我们要验证的是服务端的极限，所以只要一直让服务端认为有那么多连接就行了，不是吗？</p><p>经过我们的试验后，这种方法和用真实的机器连接服务端的表现是一样的，因为服务端只是认为对方网络不好罢了，不会将你断开。</p><p>另外，禁用<code>keep-alive</code>是因为如果不禁用，Socket 连接会自动探测连接是否可用，如果不可用会强制断开。</p><h2 id="更高的-QPS"><a href="#更高的-QPS" class="headerlink" title="更高的 QPS"></a>更高的 QPS</h2><p>由于 NIO 和 Netty 都是非阻塞 IO，所以无论有多少连接，都只需要少量的线程即可。而且 QPS 不会因为连接数的增长而降低（在内存足够的前提下）。</p><p>而且 Netty 本身设计得足够好了，Netty 不是高 QPS 的瓶颈。那高 QPS 的瓶颈是什么？</p><p>是数据结构的设计！</p><h3 id="如何优化数据结构"><a href="#如何优化数据结构" class="headerlink" title="如何优化数据结构"></a>如何优化数据结构</h3><p>首先要熟悉各种数据结构的特点是必需的，但是在复杂的项目中，不是用了一个集合就可以搞定的，有时候往往是各种集合的组合使用。</p><p>既要做到高性能，还要做到一致性，还不能有死锁，这里难度真的不小…</p><p>我在这里总结的经验是，不要过早优化。优先考虑一致性，保证数据的准确，然后再去想办法优化性能。</p><p>因为一致性比性能重要得多，而且很多性能问题在量小和量大的时候，瓶颈完全会在不同的地方。 所以，我觉得最佳的做法是，编写过程中以一致性为主，性能为辅；代码完成后再去找那个 TOP1，然后去解决它！</p><h3 id="解决-CPU-瓶颈"><a href="#解决-CPU-瓶颈" class="headerlink" title="解决 CPU 瓶颈"></a>解决 CPU 瓶颈</h3><p>在做这个优化前，先在测试环境中去狠狠地压你的服务器，量小量大，天壤之别。</p><p>有了压力测试后，就需要用工具来发现性能瓶颈了！</p><p>我喜欢用的是 VisualVM，打开工具后看抽样器(Sample)，根据自用时间(Self Time (CPU))倒序，排名第一的就是你需要去优化的点了！</p><p>备注：Sample 和 Profiler 有什么区别？前者是抽样，数据不是最准但是不影响性能；后者是统计准确，但是非常影响性能。 如果你的程序非常耗 CPU，那么尽量用 Sample，否则开启 Profiler 后降低性能，反而会影响准确性。</p><p><img src="https://www.easemob.com/data/upload/ueditor/20191126/5ddc9959c2b1d.png" alt="sample"></p><p>还记得我们项目第一次发现的瓶颈竟然是<code>ConcurrentLinkedQueue</code>这个类中的<code>size()</code>方法。 量小的时候没有影响，但是<code>Queue</code>很大的时候，它每次都是从头统计总数的，而这个<code>size()</code>方法我们又是非常频繁地调用的，所以对性能产生了影响。</p><p><code>size()</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><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">public int size() &#123;</span><br><span class="line">    int count = 0;</span><br><span class="line">    for (Node&lt;E&gt; p = first(); p != null; p = succ(p))</span><br><span class="line">    if (p.item != null)</span><br><span class="line">    // Collection.size() spec says to max out</span><br><span class="line">    if (++count == Integer.MAX_VALUE)</span><br><span class="line">    break;</span><br><span class="line">    return count;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>后来我们通过额外使用一个<code>AtomicInteger</code>来计数，解决了问题。但是分离后岂不是做不到高一致性呢？ 没关系，我们的这部分代码关心最终一致性，所以只要保证最终一致就可以了。</p><p>总之，具体案例要具体分析，不同的业务要用不同的实现。</p><h3 id="解决-GC-瓶颈"><a href="#解决-GC-瓶颈" class="headerlink" title="解决 GC 瓶颈"></a>解决 GC 瓶颈</h3><p>GC 瓶颈也是 CPU 瓶颈的一部分，因为不合理的 GC 会大大影响 CPU 性能。</p><p>这里还是在用 VisualVM，但是你需要装一个插件：VisualGC</p><p><img src="https://www.easemob.com/data/upload/ueditor/20191126/5ddc99655e074.png" alt="GC"></p><p>有了这个插件后，你就可以直观的看到 GC 活动情况了。</p><p>按照我们的理解，在压测的时候，有大量的 New GC 是很正常的，因为有大量的对象在创建和销毁。</p><p>但是一开始有很多 Old GC 就有点说不过去了！</p><p>后来发现，在我们压测环境中，因为 Netty 的 QPS 和连接数关联不大，所以我们只连接了少量的连接。内存分配得也不是很多。</p><p>而 JVM 中，默认的新生代和老生代的比例是1:2，所以大量的老生代被浪费了，新生代不够用。</p><p>通过调整 <code>-XX:NewRatio</code> 后，Old GC 有了显著的降低。</p><p>但是，生产环境又不一样了，生产环境不会有那么大的 QPS，但是连接会很多，连接相关的对象存活时间非常长，所以生产环境更应该分配更多的老生代。</p><p>总之，GC 优化和 CPU 优化一样，也需要不断调整，不断优化，不是一蹴而就的。</p><h2 id="其他优化"><a href="#其他优化" class="headerlink" title="其他优化"></a>其他优化</h2><p>如果你已经完成了自己的程序，那么一定要看看《Netty in Action》作者的这个网站：<a href="http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html" target="_blank" rel="noopener">Netty Best Practices a.k.a Faster == Better</a>。</p><p>相信你会受益匪浅，经过里面提到的一些小小的优化后，我们的整体 QPS 提升了很多。</p><p>最后一点就是，java 1.7 比 java 1.6 性能高很多！因为 Netty 的编写风格是事件机制的，看似是 AIO。 可 java 1.6 是没有 AIO 的，java 1.7 是支持 AIO 的，所以如果用 java 1.7 的话，性能也会有显著提升。</p><h2 id="最后成果"><a href="#最后成果" class="headerlink" title="最后成果"></a>最后成果</h2><p>经过几周的不断压测和不断优化了，我们在一台16核、120G内存(JVM只分配8G)的机器上，用 java 1.6 达到了60万的连接和20万的QPS。</p><p>其实这还不是极限，JVM 只分配了8G内存，内存配置再大一点连接数还可以上去；</p><p>QPS 看似很高，System Load Average 很低，也就是说明瓶颈不在 CPU 也不在内存，那么应该是在 IO 了！ 上面的 Linux 配置是为了达到百万连接而配置的，并没有针对我们自己的业务场景去做优化。</p><p>因为目前性能完全够用，线上单机 QPS 最多才 1W，所以我们先把精力放在了其他地方。 相信后面我们还会去继续优化这块的性能，期待 QPS 能有更大的突破！</p>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;转载自：&lt;a href=&quot;https://www.dozer.cc/2014/12/netty-long-connection.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.dozer.cc/2014/12/netty-long-connection.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;原文作者：dozer&lt;/p&gt;
&lt;/blockquote&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;还记得一年半前，做的一个项目需要用到 Android 推送服务。和 iOS 不同，Android 生态中没有统一的推送服务。Google 虽然有 &lt;a href=&quot;http://zh.wikipedia.org/wiki/Google雲端通訊&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google Cloud Messaging&lt;/a&gt; ，但是连国外都没统一，更别说国内了，直接被墙。&lt;/p&gt;
&lt;p&gt;所以之前在 Android 上做推送大部分只能靠轮询。而我们之前在技术调研的时候，搜到了 &lt;a href=&quot;https://www.jpush.cn/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jPush&lt;/a&gt; 的博客，上面介绍了一些他们的技术特点，他们主要做的其实就是移动网络下的长连接服务。单机 50W-100W 的连接的确是吓我一跳！后来我们也采用了他们的免费方案，因为是一个受众面很小的产品，所以他们的免费版够我们用了。一年多下来，运作稳定，非常不错！&lt;/p&gt;
&lt;p&gt;时隔两年，换了部门后，竟然接到了一项任务，优化公司自己的长连接服务端。&lt;/p&gt;
&lt;p&gt;再次搜索网上技术资料后才发现，相关的很多难点都被攻破，网上也有了很多的总结文章，单机 50W-100W 的连接完全不是梦，其实人人都可以做到。但是光有连接还不够，QPS 也要一起上去。&lt;/p&gt;
&lt;p&gt;所以，这篇文章就是汇总一下利用 Netty 实现长连接服务过程中的各种难点和可优化点。&lt;/p&gt;
    
    </summary>
    
      <category term="RPC" scheme="https://lexburner.github.io/categories/RPC/"/>
    
    
      <category term="RPC" scheme="https://lexburner.github.io/tags/RPC/"/>
    
  </entry>
  
  <entry>
    <title>小白也能懂的 Nacos 服务模型介绍</title>
    <link href="https://lexburner.github.io/nacos-service-model/"/>
    <id>https://lexburner.github.io/nacos-service-model/</id>
    <published>2021-03-14T06:59:28.000Z</published>
    <updated>2021-03-28T09:14:37.531Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>按照目前市场上的主流使用场景，Nacos 被分成了两块功能：服务注册发现（Naming）和配置中心（Config）。在之前的文章中我介绍了 Nacos 配置中心的实现原理，今天这篇文章所介绍的内容则是与 Nacos 服务注册发现功能相关，来聊一聊 Nacos 的服务模型。</p><p>说到服务模型，其实需要区分视角，一是用户视角，一个内核视角。即 Nacos 用户视角看到的服务模型和 Nacos 开发者设计的内核模型可能是完全不一样的，而今天的文章，是站在用户视角观察的，旨在探讨 Nacos 服务发现的最佳实践。</p><a id="more"></a><h2 id="服务模型介绍"><a href="#服务模型介绍" class="headerlink" title="服务模型介绍"></a>服务模型介绍</h2><p>一般我在聊注册中心时，都会以 Zookeeper 为引子，这也是很多人最熟悉的注册中心。但如果你真的写过或看过使用 Zookeeper 作为注册中心的适配代码，会发现并不是那么容易，再加上注册中心涉及到的一致性原理，这就导致很多人对注册中心的第一印象是：这个东西好难！ 但归根到底是因为 Zookeeper 根本不是专门为注册中心而设计的，其提供的 API 以及内核设计，并没有预留出「服务模型」的概念，这就使得开发者需要自行设计一个模型，去填补 Zookeeper 和服务发现之间的鸿沟。</p><p>微服务架构逐渐深入人心后，Nacos、Consul、Eureka 等注册中心组件进入大众的视线。可以发现，这些”真正“的注册中心都有各自的「服务模型」，在使用上也更加的方便。</p><p>为什么要有「服务模型」？理论上，一个基础组件可以被塑造成任意的模样，如果你愿意，一个数据库也可以被设计成注册中心，这并不是”夸张“的修辞手法，在阿里还真有人这么干过。那么代价是什么呢？一定会在业务发展到一定体量后遇到瓶颈，一定会遇到某些极端 case 导致其无法正常工作，一定会导致其扩展性低下。正如刚学习数据结构时，同学们常见的一个疑问一样：为什么栈只能先进后出。不是所有开发都是中间件专家，所以 Nacos 设计了自己的「服务模型」，这虽然限制了使用者的”想象力“，但保障了使用者在正确地使用 Nacos。</p><p>花了一定的篇幅介绍 Nacos 为什么需要设计「服务模型」，再来看看实际的 Nacos 模型是个啥，其实没那么玄乎，一张图就能表达清楚：</p><p><img src="https://image.cnkirito.cn/image-20210314161429839.png" alt="服务模型"></p><p>与 Consul、Eureka 设计有别，Nacos 服务发现使用的领域模型是命名空间-分组-服务-集群-实例这样的多层结构。服务 Service 和实例 Instance 是核心模型，命名空间 Namespace 、分组 Group、集群 Cluster 则是在不同粒度实现了服务的隔离。</p><p>为了更好的理解两个核心模型：Service 和 Instance，我们以 Dubbo 和 SpringCloud 这两个已经适配了 Nacos 注册中心的微服务框架为例，介绍下二者是如何映射对应模型的。</p><ul><li>Dubbo。将接口三元组（接口名+分组名+版本号）映射为 Service，将实例 IP 和端口号定义为 Instance。一个典型的注册在 Nacos 中的 Dubbo 服务：<code>providers:com.alibaba.mse.EchoService:1.0.0:DUBBO</code></li><li>Spring Cloud。将应用名映射为 Service，将实例 IP 和端口号定义为 Instance。一个典型的注册在 Nacos 中的 Spring Cloud 服务：<code>helloApp</code></li></ul><p>下面我们将会更加详细地阐释 Nacos 提供的 API 和服务模型之间的关系。</p><!-- more --><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>需要部署一个 Nacos Server 用于测试，我这里选择直接在 <a href="https://mse.console.aliyun.com/" target="_blank" rel="noopener">https://mse.console.aliyun.com/</a> 购买一个 MSE 托管的 Nacos，读者们可以选择购买 MSE Nacos 或者自行搭建一个 Nacos Server。</p><p><img src="https://image.cnkirito.cn/image-20210314163510445.png" alt="MSE"></p><p>MSE Nacos 提供的可视化控制台，也可以帮助我们更好的理解 Nacos 的服务模型。下文的一些截图，均来自 MSE Nacos 的商业化控制台。</p><h2 id="快速开始"><a href="#快速开始" class="headerlink" title="快速开始"></a>快速开始</h2><p>先来实现一个最简单的服务注册与发现 demo。Nacos 支持从客户端注册服务实例和订阅服务，具体代码如下：</p><figure class="highlight java"><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">Properties properties = <span class="keyword">new</span> Properties();</span><br><span class="line">properties.setProperty(PropertyKeyConst.SERVER_ADDR, <span class="string">"mse-xxxx-p.nacos-ans.mse.aliyuncs.com:8848"</span>);</span><br><span class="line"></span><br><span class="line">String serviceName = <span class="string">"nacos.test.service.1"</span>;</span><br><span class="line">String instanceIp = InetAddress.getLocalHost().getHostAddress();</span><br><span class="line"><span class="keyword">int</span> instancePort = <span class="number">8080</span>;</span><br><span class="line"></span><br><span class="line">namingService.registerInstance(serviceName, instanceIp, instancePort);</span><br><span class="line"></span><br><span class="line">System.out.println(namingService.getAllInstances(serviceName));</span><br></pre></td></tr></table></figure><p>上述代码定义了一个 service：<code>nacos.test.service.1</code>；定义了一个 instance，以本机 host 为 IP 和 8080 为端口号，观察实际的注册情况：</p><p><img src="https://image.cnkirito.cn/image-20210314165514015.png" alt="服务信息"></p><p><img src="https://image.cnkirito.cn/image-20210314173532176.png" alt="实例信息"></p><p>并且控制台也打印出了服务的详情。至此一个最简单的 Nacos 服务发现 demo 就已经完成了。对一些细节稍作解释：</p><ul><li>属性 <code>PropertyKeyConst.SERVER_ADDR</code> 表示的是 Nacos 服务端的地址。</li><li>创建一个 NamingService 实例，客户端将为该实例创建单独的资源空间，包括缓存、线程池以及配置等。Nacos 客户端没有对该实例做单例的限制，请小心维护这个实例，以防新建多于预期的实例。</li><li>注册服务 <code>registerInstance</code> 使用了最简单的重载方法，只需要传入服务名、IP、端口就可以。</li></ul><p>上述的例子中，并没有出现 Namespace、Group、Cluster 等前文提及的服务模型，我会在下面一节详细介绍，这个例子主要是为了演示 Nacos 支持的一些缺省配置，其中 Service 和 Instance 是必不可少的，这也验证了前文提到的服务和实例是 Nacos 的一等公民。</p><p>通过截图我们可以发现缺省配置的默认值：</p><ul><li>Namespace：默认值是 public 或者空字符串，都可以代表默认命名空间。</li><li>Group：默认值是 DEFAULT_GROUP。</li><li>Cluster：默认值是 DEFAULT。</li></ul><h2 id="构建自定义实例"><a href="#构建自定义实例" class="headerlink" title="构建自定义实例"></a>构建自定义实例</h2><p>为了展现出 Nacos 服务模型的全貌，还需要介绍下实例相关的 API。例如我们希望注册的实例中，有一些能够被分配更多的流量；或者能够传入一些实例的元信息存储到 Nacos 服务端，例如 IP 所属的应用或者所在的机房，这样在客户端可以根据服务下挂载的实例元信息，来自定义负载均衡模式。Nacos 也提供了另外的注册实例接口，使得用户在注册实例时可以指定实例的属性：</p><figure class="highlight java"><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="comment">/**</span></span><br><span class="line"><span class="comment"> * register a instance to service with specified instance properties.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> serviceName name of service</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> groupName   group of service</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> instance    instance to register</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> NacosException nacos exception</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">registerInstance</span><span class="params">(String serviceName, String groupName, Instance instance)</span> <span class="keyword">throws</span> NacosException</span>;</span><br></pre></td></tr></table></figure><p>这个方法在注册实例时，可以传入一个 Instance 实例，它的属性如下：</p><figure class="highlight java"><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><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Instance</span> </span>&#123;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * unique id of this instance.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String instanceId;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * instance ip.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String ip;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * instance port.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> port;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * instance weight.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">double</span> weight = <span class="number">1.0</span>D;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * instance health status.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">boolean</span> healthy = <span class="keyword">true</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * If instance is enabled to accept request.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">boolean</span> enabled = <span class="keyword">true</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * If instance is ephemeral.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@since</span> 1.0.0</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">boolean</span> ephemeral = <span class="keyword">true</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * cluster information of instance.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String clusterName;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Service information of instance.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String serviceName;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * user extended attributes.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Map&lt;String, String&gt; metadata = <span class="keyword">new</span> HashMap&lt;String, String&gt;();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有一些字段可以望文生义，有一些则需要花些功夫专门去了解 Nacos 的设计，我这里挑选几个我认为重要的属性重点介绍下：</p><ul><li>healthy 实例健康状态。标识该实例是否健康，一般心跳健康检查会自动更新该字段。</li><li>enable 是否启用。它跟 healthy 区别在于，healthy 一般是由内核健康检查更新，而 enable 更多是业务语义偏多，可以完全根据业务场景操控。例如在 Dubbo 中，一般使用该字段标识某个实例 IP 的上下线状态。</li><li>ephemeral 临时实例还是持久化实例。非常关键的一个字段，需要对 Nacos 有较为深入的了解才能够理解该字段的含义。区别在于，心跳检测失败一定时间之后，实例是自动下线还是标记为不健康。一般在注册中心场景下，会使用临时实例。这样心跳检测失败之后，可以让消费者及时收到下线通知；而在 DNS 模式下，使用持久化实例较多。在《一文详解 Nacos 高可用特性》中我也介绍过，该字段还会影响到 Nacos 的一致性协议。</li><li>metadata 元数据。一个 map 结构，可以存储实例的自定义扩展信息，例如机房信息，路由标签，应用信息，权重信息等。</li></ul><p>这些信息在由服务提供者上报之后，由服务消费者获取，从而完成信息的传递。以下是一个完整的实例注册演示代码：</p><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line">Properties properties = <span class="keyword">new</span> Properties();</span><br><span class="line"><span class="comment">// 指定 Nacos Server 地址</span></span><br><span class="line">properties.setProperty(PropertyKeyConst.SERVER_ADDR, <span class="string">"mse-xxxx-p.nacos-ans.mse.aliyuncs.com:8848"</span>);</span><br><span class="line"><span class="comment">// 指定命名空间</span></span><br><span class="line">properties.setProperty(PropertyKeyConst.NAMESPACE, <span class="string">"9125571e-bf50-4260-9be5-18a3b2e3605b"</span>);</span><br><span class="line"></span><br><span class="line">NamingService namingService = NacosFactory.createNamingService(properties);</span><br><span class="line">String serviceName = <span class="string">"nacos.test.service.1"</span>;</span><br><span class="line">String group = <span class="string">"DEFAULT_GROUP"</span>;</span><br><span class="line">String clusterName = <span class="string">"cn-hangzhou"</span>;</span><br><span class="line">String instanceIp = InetAddress.getLocalHost().getHostAddress();</span><br><span class="line"><span class="keyword">int</span> instancePort = <span class="number">8080</span>;</span><br><span class="line">Instance instance = <span class="keyword">new</span> Instance();</span><br><span class="line"><span class="comment">// 指定集群名</span></span><br><span class="line">instance.setClusterName(clusterName);</span><br><span class="line">instance.setIp(instanceIp);</span><br><span class="line">instance.setPort(instancePort);</span><br><span class="line"><span class="comment">// 指定实例的元数据</span></span><br><span class="line">Map&lt;String, String&gt; metadata = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">metadata.put(<span class="string">"app"</span>, <span class="string">"nacos-demo"</span>);</span><br><span class="line">metadata.put(<span class="string">"site"</span>, <span class="string">"cn-hangzhou"</span>);</span><br><span class="line">metadata.put(<span class="string">"protocol"</span>, <span class="string">"1.3.3"</span>);</span><br><span class="line">instance.setMetadata(metadata);</span><br><span class="line"><span class="comment">// 指定服务名、分组和实例</span></span><br><span class="line">namingService.registerInstance(serviceName, group, instance);</span><br><span class="line"></span><br><span class="line">System.out.println(namingService.getAllInstances(serviceName));</span><br></pre></td></tr></table></figure><p><img src="https://image.cnkirito.cn/image-20210314173008765.png" alt="服务信息"></p><p><img src="https://image.cnkirito.cn/image-20210314173750457.png" alt="实例信息"></p><h2 id="构建自定义服务"><a href="#构建自定义服务" class="headerlink" title="构建自定义服务"></a>构建自定义服务</h2><p>除了实例之外，服务也可以自定义配置，Nacos 的服务随着实例的注册而存在，并随着所有实例的注销而消亡。不过目前 Nacos 对于自定义服务的支持不是很友好，除使用 OpenApi 可以修改服务的属性外，就只能使用注册实例时传入的服务属性来进行自定义配置。所以在实际的 Dubbo 和 SpringCloud 中，自定义服务一般较少使用，而自定义实例信息则相对常用。</p><p>Nacos 的服务与 Consul、Eureka 的模型都不同，Consul 与 Eureka的服务等同于 Nacos 的实例，每个实例有一个服务名属性，服务本身并不是一个单独的模型。Nacos 的设计在我看来更为合理，其认为服务本身也是具有数据存储需求的，例如作用于服务下所有实例的配置、权限控制等。实例的属性应当继承自服务的属性，实例级别可以覆盖服务级别。以下是服务的数据结构：</p><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Service name</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Protect threshold</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">float</span> protectThreshold = <span class="number">0.0F</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Application name of this service</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String app;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Service group which is meant to classify services into different sets.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String group;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Health check mode.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String healthCheckMode;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Map&lt;String, String&gt; metadata = <span class="keyword">new</span> HashMap&lt;String, String&gt;();</span><br></pre></td></tr></table></figure><p>在实际使用过程中，可以像快速开始章节中介绍的那样，仅仅使用 ServiceName 标记一个服务。</p><h2 id="服务隔离：Namespace-amp-Group-amp-Cluster"><a href="#服务隔离：Namespace-amp-Group-amp-Cluster" class="headerlink" title="服务隔离：Namespace&amp;Group&amp;Cluster"></a>服务隔离：Namespace&amp;Group&amp;Cluster</h2><p>出于篇幅考虑，这三个概念放到一起介绍。</p><p>襄王有意，神女无心。Nacos 提出了这几种隔离策略，目前看来只有 Namespace 在实际应用中使用较多，而 Group 和 Cluster 并没有被当回事。</p><p>Cluster 集群隔离在阿里巴巴内部使用的非常普遍。一个典型的场景是这个服务下的实例，需要配置多种健康检查方式，有一些实例使用 TCP 的健康检查方式，另外一些使用 HTTP 的健康检查方式。另一个场景是，服务下挂载的机器分属不同的环境，希望能够在某些情况下将某个环境的流量全部切走，这样可以通过集群隔离，来做到一次性切流。在 Nacos 2.0 中，也在有意的弱化集群的概念，毕竟开源还是要面向用户的，有些东西适合阿里，但不一定适合开源，等再往后演进，集群这个概念又有可能重新回到大家的视线中了，history will repeat itself。</p><p>Group 分组隔离的概念可以参考 Dubbo 的服务隔离策略，其也有一个分组。支持分组的扩展，用意当然是好的，实际使用上，也的确有一些公司会习惯使用分组来进行隔离。需要注意的一点是：Dubbo 注册三元组（接口名+分组+版本）时，其中 Dubbo 的分组是包含在 Nacos 的服务名中的，并不是映射成了 Nacos 的分组，一般 Nacos 注册的服务是默认注册到 DEFAULT_GROUP 分组的。</p><p>Namespace 命名空间隔离，我认为是 Nacos 一个比较好的设计。在实际场景中使用也比较普遍，一般用于多个环境的隔离，例如 daily，dev，test，uat，prod 等环境的隔离。特别是当环境非常多时，使用命名空间做逻辑隔离是一个比较节约成本的策略。但强烈建议大家仅仅在非线上环境使用 Namespace 进行隔离，例如多套测试环境可以共享一套 Nacos，而线上环境单独搭建另一套 Nacos 集群，以免线下测试流量干扰到线上环境。</p><h2 id="服务发现：推拉模型"><a href="#服务发现：推拉模型" class="headerlink" title="服务发现：推拉模型"></a>服务发现：推拉模型</h2><p>上面介绍完了 Nacos 服务发现的 5 大领域模型，最后一节，介绍下如何获取服务模型。</p><p>Nacos 的服务发现，有主动拉取和推送两种模式，这与一般的服务发现架构相同。以下是拉模型的相关接口：</p><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Get all instances of a service</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> serviceName name of service</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> A list of instance</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> NacosException</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function">List&lt;Instance&gt; <span class="title">getAllInstances</span><span class="params">(String serviceName)</span> <span class="keyword">throws</span> NacosException</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Get qualified instances of service</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> serviceName name of service</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> healthy     a flag to indicate returning healthy or unhealthy instances</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> A qualified list of instance</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> NacosException</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function">List&lt;Instance&gt; <span class="title">selectInstances</span><span class="params">(String serviceName, <span class="keyword">boolean</span> healthy)</span> <span class="keyword">throws</span> NacosException</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Select one healthy instance of service using predefined load balance strategy</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> serviceName name of service</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> qualified instance</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> NacosException</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function">Instance <span class="title">selectOneHealthyInstance</span><span class="params">(String serviceName)</span> <span class="keyword">throws</span> NacosException</span>;</span><br></pre></td></tr></table></figure><p>Nacos 提供了三个同步拉取服务的方法，一个是查询所有注册的实例，一个是只查询健康且上线的实例，还有一个是获取一个健康且上线的实例。一般情况下，订阅端并不关心不健康的实例或者权重设为 0 的实例，但是也不排除一些场景下，有一些运维或者管理的场景需要拿到所有的实例。细心的读者会注意到上述 Nacos 实例中有一个 weight 字段，便是作用在此处的<code>selectOneHealthyInstance</code>接口上，按照权重返回一个健康的实例。个人认为这个功能相对鸡肋，一般的 RPC 框架都有自身配套的负载均衡策略，很少会由注册中心 cover，事实上 Dubbo 和 Spring Cloud 都没有用到 Nacos 的这个接口。</p><p>除了主动查询实例列表，Nacos还提供订阅模式来感知服务下实例列表的变化，包括服务配置或者实例配置的变化。可以使用下面的接口来进行订阅或者取消订阅：</p><figure class="highlight java"><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="comment">/**</span></span><br><span class="line"><span class="comment"> * Subscribe service to receive events of instances alteration</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> serviceName name of service</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> listener    event listener</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> NacosException</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">subscribe</span><span class="params">(String serviceName, EventListener listener)</span> <span class="keyword">throws</span> NacosException</span>;</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Unsubscribe event listener of service</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> serviceName name of service</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> listener    event listener</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> NacosException</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unsubscribe</span><span class="params">(String serviceName, EventListener listener)</span> <span class="keyword">throws</span> NacosException</span>;</span><br></pre></td></tr></table></figure><p>在实际的服务发现中，订阅接口尤为重要。消费者启动时，一般会同步获取一次服务信息用于初始化，紧接着订阅服务，这样当服务发生上下线时，就可以感知变化了，从而实现服务发现。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Nacos 为了更好的实现服务发现，提供一套成熟的服务模型，其中重点需要关注的是 Namespace、Service 和 Instance，得益于这一套服务模型的抽象，以及对推拉模型的支持，Nacos 可以快速被微服务框架集成。</p><p>理解了 Nacos 的服务模型，也有利于我们了解 Nacos 背后的工作原理，从而确保我们正确地使用 Nacos。但 Nacos 提供的这些模型也不一定所有都需要用上，例如集群、分组、权重等概念，被实践证明是相对鸡肋的设计，在使用时，也需要根据自身业务特点去评估特性用量，不要盲目地为了使用技术而去用。</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;按照目前市场上的主流使用场景，Nacos 被分成了两块功能：服务注册发现（Naming）和配置中心（Config）。在之前的文章中我介绍了 Nacos 配置中心的实现原理，今天这篇文章所介绍的内容则是与 Nacos 服务注册发现功能相关，来聊一聊 Nacos 的服务模型。&lt;/p&gt;
&lt;p&gt;说到服务模型，其实需要区分视角，一是用户视角，一个内核视角。即 Nacos 用户视角看到的服务模型和 Nacos 开发者设计的内核模型可能是完全不一样的，而今天的文章，是站在用户视角观察的，旨在探讨 Nacos 服务发现的最佳实践。&lt;/p&gt;
    
    </summary>
    
      <category term="Nacos" scheme="https://lexburner.github.io/categories/Nacos/"/>
    
    
      <category term="Nacos" scheme="https://lexburner.github.io/tags/Nacos/"/>
    
  </entry>
  
  <entry>
    <title>谈谈中间件开发</title>
    <link href="https://lexburner.github.io/talk-about-middleware-develop/"/>
    <id>https://lexburner.github.io/talk-about-middleware-develop/</id>
    <published>2021-02-06T11:09:53.000Z</published>
    <updated>2021-03-21T08:26:45.462Z</updated>
    
    <content type="html"><![CDATA[<p>最近频繁地在跟实习生候选人打交道，每次新接触一个候选人，都要花上一定的时间去介绍我们团队，候选人问的最多的一个问题就是「中间件部门一般是干嘛的？」，恰好我之前也接触过一些想从事中间件开发的小伙伴，问过我「现在转行做中间件开发还来得及吗？」诸如此类的问题，索性就写一篇文章，聊聊我个人这些年做中间件开发的感受吧。</p><h2 id="什么是中间件开发？"><a href="#什么是中间件开发？" class="headerlink" title="什么是中间件开发？"></a>什么是中间件开发？</h2><p>我大四实习时，在一个 20 多人的软件开发团队第一次接触了中间件，当时项目的架构师引入了微博开源的 RPC 框架 Motan，借助于这个框架，我们迅速构建起了一个基于微服务架构的内部电商系统。接着在项目中，由于业务需求，我们又引入了 ActiveMQ…在这个阶段，我已经在使用中间件了，但似乎没有接触到中间件开发，更多的是使用层面上的接触。</p><p>我毕业后的第一份工作，公司有几百号研发，当时的 leader 看我对中间件比较感兴趣，有意把我分配在了基础架构团队，我第一次真正意义上成为了一名”中间件研发“，平时主要的工作，是基于开源的 Kong 和 Dubbo，进行一些内部的改造，以提供给业务团队更好地使用。这个阶段，做的事还是比较杂的，业务团队对某些中间件有定制化的需求，都需要去了解这个中间件，熟悉源码。这段时间，也是我成长最快的一个时期，我是在这个期间学会了 Docker、Neo4j、Kong 等以前从来没接触过的技术，并且更加深入地了解 Dubbo 这类 RPC 框架的原理。可能坐在我旁边的就是一个订单部门的同学，抛了一个功能点让我来改造。</p><p>现在，我供职于阿里云云原生中间件，相较于上一份中间件研发工作，阿里云这类互联网大公司，任意一个中间件都有少则数人，多则数十人负责，中间件部门和业务部门之间也有着明确的界限。在这里，中间件团队的职责可以细分为三个方向：</p><ol><li>中间件团队会被业务团队的需求所驱动，为集团内部提供定制化的解决方案，俗称「自研」。所以你可能并不了解到 HSF、Diamond 这些阿里内部的中间件。</li><li>中间件团队会从事开源，花费大量的精力提升中间件的极致性能，提升开源影响力，引领技术先进性。这部分中间件则比较为人所熟知，例如 Dubbo、Spring Cloud Alibaba、RocketMQ、Nacos。</li><li>中间件会在阿里云输出商业化产品，相比开源产品，提供更高的 SLA、更强大的功能、更友好的交互。这部分商业化产品诸如：微服务引擎 MSE、消息队列 RocketMQ、分布式应用链路追踪 ARMS。</li></ol><p>我的这三段经历，正好反应了不同规模的公司对中间件开发的不同需求。小公司使用中间件，例如 RPC、MQ、缓存等，基本是由业务开发人员自己维护的。但如果后台研发达到数百人，基本就会组建自己的中间件团队，或者选择使用阿里云等云厂商提供的中间件产品。</p><a id="more"></a><h2 id="中间件开发和业务开发的区别"><a href="#中间件开发和业务开发的区别" class="headerlink" title="中间件开发和业务开发的区别"></a>中间件开发和业务开发的区别</h2><p>在我看来，中间件开发和业务开发并没有什么高下之分，非要说区别的话，有点像游戏里面的不同转职，有人选择的是魔法师，有人选择的是战士。在职场的练级过程中，每个人的总技能点数是一样的，业务开发有点向全能战士的方向去发展，各个点都涉猎一点，但每个方向能够分配到技能点自然就少了；中间件开发就像《因为太怕痛，就全点防御力了》里面的主角，把技能点都分配到了一个方向。</p><p>假设你是在一个小公司工作，现阶段并没有专门的中间件团队，大家都是业务开发，此时我们做一个假设：公司即将成立一个中间件团队或者叫基础架构部，那么会是哪一类人容易被选中呢？一定是那些技术功底扎实，对中间件感兴趣，研究过源码的人。这个假设并非凭空捏造，很多互联网公司的中间件团队都是这么一点点壮大起来的。我想说什么呢？业务开发和中间件开发一开始并没有明确的界限，因此，不用顾忌你现在是不是在从事业务开发，只要你对中间件感兴趣，有过源码级别的研究，就可以成为一个中间件开发。</p><h2 id="中间件开发需要具备哪些素质？"><a href="#中间件开发需要具备哪些素质？" class="headerlink" title="中间件开发需要具备哪些素质？"></a>中间件开发需要具备哪些素质？</h2><p>越是大的公司，大的中间件团队，责任分工就越垂直。基本在大公司，一个中间件开发可能花几年时间在某一个垂直方向深耕。以下是一些常见的中间件方向，当然，这个分类在各个公司可能由于组织架构的原因，略有不同。</p><ol><li>微服务治理。例如 RPC 相关中间件，注册中心，配置中心，限流熔断，链路追踪等等。开源产品例如：Dubbo、SpringCloud、Nacos、Zookeeper、Sentinel、Hystrix、Zipkin。</li><li>消息队列。微服务一般强调的同步通信，消息队列单独列出来，主要是因为其异步的机制。开源产品例如：RocketMQ、Kafka</li><li>存储中间件。例如缓存，数据库等等，例如 Mysql、Redis。值得一提的是，由于存储相关的系统一般都非常复杂，特别是在分布式存储领域，体系更是繁杂，在阿里内部一般将数据库和缓存这种存储类型的产品当成是和中间件平级的存在，所以如果有人说 Mysql 和 Redis 不是中间件，也没有啥好争吵的。</li><li>存储 Proxy。典型的如 ShardingSphere。</li><li>网关。例如 Spring Cloud Gateway、Kong、Nginx。</li><li>ServiceMesh。Envoy、Istio 在阿里这边也被划分在中间件部门。</li></ol><p>其实可以发现，中间件其实并没有一个明确的定义，到底哪些开源产品可以是中间件，哪些又不是。</p><p>列举完这些典型的中间件，继续讨论这一节的主题，一个中间件开发者需要具备哪些素质？</p><ol><li>语言基础。从 Java 程序员的角度，基础通常就是：集合，并发，JVM，常用工具类。</li><li>操作系统基础。中间件开发人员经常和操作系统打交道，所以计算机基础也必不可少，我列举一些关键词，供各位参考<ul><li>文件 IO。例如 pageCache，mmap，direct IO 等概念。</li><li>进程线程。例如 green thread，协程等概念。</li><li>内存/CPU。例如 cgroup，cache line，bound core 等概念。</li></ul></li><li>网络基础。可以发现上述的每一个中间件都离不开网络通信，一定需要对 TCP 和 HTTP 的原理烂熟于心，框架层面需要熟悉 NIO、Netty、GRPC、HttpClient 等常用的网络框架/工具。</li><li>分布式相关知识。了解 CAP， paxos，raft，zab，2pc/3pc，base 等理论知识，例如我看到有一些应届生简历中的一个项目经历就很有意思：根据 MIT 课程 Lab 实现 Raft 协议的 POC。</li><li>源码阅读能力。我认为源码阅读能力是一个中间件开发者必备的素质，网上经常能看到各种源码分析文章，通过阅读开源中间件的源码，可以借鉴别人的设计理念，提升自身的编码水准。</li><li>保持技术热情，拥抱变化。中间件技术日新月异，可能一个打败一个中间件的不是同类的产品，而是整体的大环境，例如近几年云原生大火，所有中间件几乎都在拥抱变化，主动向 K8s 对齐，在这个大背景下，就需要中间件开发者拥有 K8s 的基础认知能力，熟悉 pod、service、deployment、statefulSet、operator 这些 K8s 的基本概念。</li></ol><h2 id="如何成为中间件开发"><a href="#如何成为中间件开发" class="headerlink" title="如何成为中间件开发"></a>如何成为中间件开发</h2><p>看完上述这些要求，可能会有一些同学开始咋舌了，但其实也没那么可怕，这跟最早学习 Java 基础是一样的，很多东西一开始没有接触过，觉得很难，但熟悉之后会发现，也就那么回事。</p><p>我的技术交流群中经常会有同学抱怨说，平时只能接触到 CRUD，根本接触不到这些”高大上“的技术。我想说的是，机会都是自己找的。我这里有几个切实可行的建议：</p><ol><li>参与开源社区的项目，贡献代码。了解一个中间件最好的方式就是贡献它，带着问题有的放矢地研究源码，是我比较推荐的方式，你所需要做的是寻找一个合适的 issue，解决它。不断重复这个过程，你其实就是一个中间件开发了！</li><li>多动手做实验。很多上面提到的中间件开发应用的素质都可以通过动手做实验的方式来学习，例如动手实现一个简易的 RPC 框架，实现一个 Raft 协议的 POC，通过 benchmark 对比 FileChannel 和 MMAP 的性能，相信我，这比看书、看视频、看博客有用的多的多。</li><li>参与中间件挑战赛。最早是阿里会举办一年一度的中间件性能挑战赛，后来也有一些其他公司如华为开始效仿这类比赛，参与这些挑战赛也可以积累非常多的经验，同时你还可以借着组队，结交非常多的朋友。</li></ol><p>对于上述的那些要求，我筛选了我之前写过的一些不需要有任何门槛就可以阅读的文章，可能会对你有所帮助：</p><ol><li><a href="http://mp.weixin.qq.com/s?__biz=MzI0NzEyODIyOA==&amp;mid=2247485597&amp;idx=1&amp;sn=b0bc9ae83be9fdb2d49f1ac1c876d990&amp;chksm=e9b58156dec20840ba511955dc70040c8f0f9d793a152c9da322ff785fecd786e1f9e326e72d&amp;token=1086156994&amp;lang=zh_CN#rd" target="_blank" rel="noopener"> 用了这么久配置中心，还不知道长轮询是什么？</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MzI0NzEyODIyOA==&amp;mid=2247484345&amp;idx=1&amp;sn=9ba0fb062ff6ca427710885d8a26bf0e&amp;chksm=e9b58a72dec203649e2f5e7a97a4ddfcb46b96645a218c5c1ffd56ca832a184bdef0a6ecc923&amp;token=1086156994&amp;lang=zh_CN#rd" target="_blank" rel="noopener">一文探讨堆外内存的监控与回收</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MzI0NzEyODIyOA==&amp;mid=2247484276&amp;idx=1&amp;sn=e9d4dd7495272565ef77219f34d65a87&amp;chksm=e9b58abfdec203a97013f0dff6acb7ac0e680b0fcc4f064b4dc38bada0a4e1d750a428e8ea1f&amp;token=1086156994&amp;lang=zh_CN#rd" target="_blank" rel="noopener">一种心跳，两种设计</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MzI0NzEyODIyOA==&amp;mid=2247484251&amp;idx=1&amp;sn=c037f75bb6172fbad86f5e9c5ed22a77&amp;chksm=e9b58a90dec203866bbe080b1ab795af4f19cae5815426fa40503e0ad01d3ec2a1aac94cb723&amp;token=1086156994&amp;lang=zh_CN#rd" target="_blank" rel="noopener">聊聊 TCP 长连接和心跳那些事</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MzI0NzEyODIyOA==&amp;mid=2247484211&amp;idx=1&amp;sn=1a2e597fe3a039a5925db5f000bad872&amp;chksm=e9b58af8dec203ee578e8903fa7df7abbedcb3f90592117a8885179cb64c72366481427df91d&amp;token=1086156994&amp;lang=zh_CN#rd" target="_blank" rel="noopener">文件 IO 操作的最佳实践</a></li><li><a href="http://mp.weixin.qq.com/s?__biz=MzI0NzEyODIyOA==&amp;mid=2247484043&amp;idx=1&amp;sn=cd9c9106b9ef43adc7212bac26d4d1de&amp;chksm=e9b58b40dec202560dab4bf84a91d7d5a3a560fea2057e5f3704c2efcc9bf7a2978586a8984c&amp;token=1086156994&amp;lang=zh_CN#rd" target="_blank" rel="noopener">以 Dubbo 为例，聊聊如何为开源项目做贡献</a></li></ol><h2 id="中间件欢迎你"><a href="#中间件欢迎你" class="headerlink" title="中间件欢迎你"></a>中间件欢迎你</h2><p>如果你是一个业务开发，想从事中间件方向的研发，正在纠结，那我想说：选择中间件，最合适的时间是毕业时，其次是现在，Why Not？</p><p>如果你是一个学生，正在找一份实习工作，我还是挺推荐你选择中间件方向的工作的，这个方向非常技术范，无论如何对你的成长都有帮助。最后，你应该猜到我要说什么了吧？如果你有意向从事中间件的开发，随时欢迎你与我【微信 id：xiayimiaoshenghua】咨询，我这里是【阿里云云原生中间件】团队，我们正在针对 22 届的学生进行春季实习的意向沟通。</p><p>团队介绍 &amp; JD 请戳阅读原文</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近频繁地在跟实习生候选人打交道，每次新接触一个候选人，都要花上一定的时间去介绍我们团队，候选人问的最多的一个问题就是「中间件部门一般是干嘛的？」，恰好我之前也接触过一些想从事中间件开发的小伙伴，问过我「现在转行做中间件开发还来得及吗？」诸如此类的问题，索性就写一篇文章，聊聊我个人这些年做中间件开发的感受吧。&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;我大四实习时，在一个 20 多人的软件开发团队第一次接触了中间件，当时项目的架构师引入了微博开源的 RPC 框架 Motan，借助于这个框架，我们迅速构建起了一个基于微服务架构的内部电商系统。接着在项目中，由于业务需求，我们又引入了 ActiveMQ…在这个阶段，我已经在使用中间件了，但似乎没有接触到中间件开发，更多的是使用层面上的接触。&lt;/p&gt;
&lt;p&gt;我毕业后的第一份工作，公司有几百号研发，当时的 leader 看我对中间件比较感兴趣，有意把我分配在了基础架构团队，我第一次真正意义上成为了一名”中间件研发“，平时主要的工作，是基于开源的 Kong 和 Dubbo，进行一些内部的改造，以提供给业务团队更好地使用。这个阶段，做的事还是比较杂的，业务团队对某些中间件有定制化的需求，都需要去了解这个中间件，熟悉源码。这段时间，也是我成长最快的一个时期，我是在这个期间学会了 Docker、Neo4j、Kong 等以前从来没接触过的技术，并且更加深入地了解 Dubbo 这类 RPC 框架的原理。可能坐在我旁边的就是一个订单部门的同学，抛了一个功能点让我来改造。&lt;/p&gt;
&lt;p&gt;现在，我供职于阿里云云原生中间件，相较于上一份中间件研发工作，阿里云这类互联网大公司，任意一个中间件都有少则数人，多则数十人负责，中间件部门和业务部门之间也有着明确的界限。在这里，中间件团队的职责可以细分为三个方向：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;中间件团队会被业务团队的需求所驱动，为集团内部提供定制化的解决方案，俗称「自研」。所以你可能并不了解到 HSF、Diamond 这些阿里内部的中间件。&lt;/li&gt;
&lt;li&gt;中间件团队会从事开源，花费大量的精力提升中间件的极致性能，提升开源影响力，引领技术先进性。这部分中间件则比较为人所熟知，例如 Dubbo、Spring Cloud Alibaba、RocketMQ、Nacos。&lt;/li&gt;
&lt;li&gt;中间件会在阿里云输出商业化产品，相比开源产品，提供更高的 SLA、更强大的功能、更友好的交互。这部分商业化产品诸如：微服务引擎 MSE、消息队列 RocketMQ、分布式应用链路追踪 ARMS。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我的这三段经历，正好反应了不同规模的公司对中间件开发的不同需求。小公司使用中间件，例如 RPC、MQ、缓存等，基本是由业务开发人员自己维护的。但如果后台研发达到数百人，基本就会组建自己的中间件团队，或者选择使用阿里云等云厂商提供的中间件产品。&lt;/p&gt;
    
    </summary>
    
      <category term="技术杂谈" scheme="https://lexburner.github.io/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    
    
      <category term="职场" scheme="https://lexburner.github.io/tags/%E8%81%8C%E5%9C%BA/"/>
    
  </entry>
  
  <entry>
    <title>Kirito 杭州买房记 | 纯小白向杭州购房攻略</title>
    <link href="https://lexburner.github.io/hangzhou-buy-house/"/>
    <id>https://lexburner.github.io/hangzhou-buy-house/</id>
    <published>2021-02-06T11:09:53.000Z</published>
    <updated>2021-03-21T08:26:32.230Z</updated>
    
    <content type="html"><![CDATA[<p>2021 年刚开年，解决了人生的一个大事，没错，就像标题里透露的那样，我在杭州买房啦。第一次有在杭州买房这个念头，还要说回 2020 年 6 月，当时和朋友们聚餐时偶然聊到了房子的话题，意外地发现竟然只有自己还没有在杭州买房，其中不乏有 96 年如此年轻的小伙子，自那次聚餐之后，我便开始关注起了杭州的楼市。</p><p>先说结果吧，我参与摇号的盘是在杭州市余杭区未来科技城的【天空之城】，摇号结果：</p><p><img src="https://image.cnkirito.cn/image-20210220132201842.png" alt="摇号结果"></p><p>这是个什么概念呢，一共 6181 户参与摇号，房子一共有 1014 套，大概有 1/6 的人算是摇中，而我是摇中的人里面第 2 顺位选房的。记得等待摇号结果的那个周日中午，等待结果通知的那最后一个小时，真的比高考都要紧张，所幸不负期许，人品可以说非常爆炸了，希望摇号这件事没有花光我今年所有运气。</p><p>预计在大年初七~初十的时间点还需要办理预审并交付首付，再往后还要办理按揭、网签等流程，还需要忙活一段时间才算尘埃落定。趁着过年这段时间比较空闲，所幸记录一下自己在杭州楼市的经历，这样也给那些刚来到杭州，想要定居于此的小白们一些参考。</p><a id="more"></a><h2 id="杭州楼市印象"><a href="#杭州楼市印象" class="headerlink" title="杭州楼市印象"></a>杭州楼市印象</h2><blockquote><p>如果我是一个记者，我就去跟拍一下摇号江湖的沉浮的人儿</p><p>一户来杭三年漂泊的大龄单身青年刚刚结婚成为无房户</p><p>一个忙着给家里老人办户口来倒腾名额的主妇</p><p>一个只能被认定为有房户（其实没房）绝望的 25 岁年轻人……</p><p>在这个秋天，在杭州魔幻的楼市里沉浮的经历。</p><p>– 阿里购房交流群内群友的感慨</p></blockquote><p>记忆拉回去年的 6 月份，那时我就是一个购房纯小白，我不清楚为什么那么多人热衷于讨论杭州的房地产，不清楚杭州政府出台的关于购房的政策，不清楚新房和二手房的区别，甚至不清楚原来在杭州买房子竟然还需要摇号。而现在，我已经对选房摇号流程烂熟于心，并且接触了贷款并了解了利率上浮、组合贷等相关知识。</p><p>聊聊我的三次摇号经历吧</p><table><thead><tr><th>楼盘名</th><th>楼盘位置</th><th>时间</th><th>摇号结果</th><th>心情</th></tr></thead><tbody><tr><td>富力中心</td><td>余杭区未来科技城</td><td>2020年6月下旬</td><td>3000 多号，彻底没戏</td><td>第一次摇号，虽然没摇中，但也没感觉</td></tr><tr><td>天空之城二期</td><td>余杭区未来科技城</td><td>2020年9月下旬</td><td>583 号，轮候</td><td>失落，差了 60 多号</td></tr><tr><td>天空之城三期</td><td>余杭区未来科技城</td><td>2021年2月上旬</td><td>2 号，摇中</td><td>boom！</td></tr></tbody></table><p>秉持着自住买房的原则，我基本只在余杭区附近 10 公里内选择了新盘进行摇号，没有往远的地方例如滨江区等地方考虑，也没有往特别贵的地方例如西湖区考虑。</p><p>这三次摇号经历，心情都是不一样的。第一次摇富力中心没中时，真的一点感觉都没有，心想着，反正机会还多的是；第二次摇天空之城二期时，已经明显感觉到杭州政府的购房政策开始发生变化了，具体哪些政策变化了我下面会详细介绍，限制了一部分人参与摇号，选房时由于差了 60 多号，虽然到我时还有房可选，但总价远超了我的预算，只能作罢；第三次摇天空之城三期时，政策又发生了变化，增加了 5 年限售的政策，所幸总算是摇到了。每一波新盘摇号，都有新的变化。</p><p>如果用一个词语描述我对杭州楼市的印象，【魔幻】二字一点不为过。</p><h2 id="杭州购房全流程"><a href="#杭州购房全流程" class="headerlink" title="杭州购房全流程"></a>杭州购房全流程</h2><p><img src="https://image.cnkirito.cn/杭州购房.svg" alt="杭州购房"></p><p>我下面的内容偏记录向，基本是对上图的诠释，如果有兴趣听我啰嗦几句，可以继续往下看。</p><h2 id="杭州楼市政策"><a href="#杭州楼市政策" class="headerlink" title="杭州楼市政策"></a>杭州楼市政策</h2><p>2018 年，我从南京来到了杭州，对于一个打工人来说，原本以为只是换一个地方打工罢了，但随着工作的稳定，并且我的女朋友也在杭州，于是便决定在杭州买房定居了，第一个念头当然就是落户了。我原户口并不是上海、北京这种稀有户口，换个城市落户对我而言没有什么损失，再加上杭州的落户政策是相对宽松的，所以在 2020 年初，我便把户口迁到了杭州。虽然目前在杭州交完 2 年社保也是能买房的，但政策这个东西说不好哪一天就变了，为了保险起见，户口迁的早总归不是坏事。</p><p>我在 2020 年 6 月才算正式开始了解杭州的楼市。刚开始啥都不懂，追着几个杭州买过房的朋友刨根问底，在这里也向他们表示下感谢。第一个摆在我面前的选项便是：新房（便宜需要摇号） or 二手房（贵不用摇号），那时也是我第一次感受到房地产的魅力，因为在杭州楼市流传着这么一句俗语：“买到新房，就是赚钱”，一方面杭州政府对新房会进行房价调控，直接导致倒挂周边二手房能够达到上万一平米，另一方面杭州目前有大量的新盘在建，杭州房地产市场正处于火热阶段，导致新盘市场极其火热。但这个事情并非只有利于杭州本地人或者打算在杭州定居的年轻人，凡是有利益的地方，就会吸引资本进场，一时间来杭州炒房的人变多了，一些红盘动辄出现万人摇的局面，直接让我们这些原本就不富裕的刚需家庭更是雪上加霜。</p><p>杭州政府当然也不会希望炒房客把房子抢去了，所以每隔一段时间都会出一些新政，核心目的就是为了贯彻“房住不炒”的思想。更早的不谈，就说说我这几次摇号经历的杭州楼市新政吧。</p><p>2020 年 6 月下旬，我的杭州第一次摇号，贡献给了富力中心这个盘，还算是政策宽松的时候：有杭州户口就能参与摇号。结果很惨烈，没有摇中。</p><p><img src="https://image.cnkirito.cn/image-20210217164833997.png" alt="富力中心"></p><p>2020 年 9 月下旬，参与了天空之城二期的摇号，已经明显感觉到了政策的变化，开始强调无房家庭这个概念了。无房家庭主要是两类人群，一类是字面意思结婚之后没有房子的家庭，一类是 30 岁以上单身的人士，就这样误杀了一类刚毕业没几年还没结婚的 30 岁以下年轻人。我周围有一些朋友在新政出来之后立马去领了证，这样才得以保住摇号资格。除了无房家庭，还有一类可以摇号的人群：人才，如果满足一定条件，是可以获得人才认证的，有了这个身份，也可以参与摇号。摇号结果是比较可惜的，天空之城二期房源数量较少，只有 520 套，我的号码是 583 套，正式选房时，只剩下一些 600 多万的一楼可选，当时的顾虑主要是预算不够，于是选择了弃选。</p><p><img src="https://image.cnkirito.cn/1EAC57BA-D91B-4D3B-8A9F-96716CAADDA1.png" alt="1EAC57BA-D91B-4D3B-8A9F-96716CAADDA1"></p><p>2021 年 2 月上旬，参与了天空之城三期的摇号，而这次政策又有了较大的变化，我作为小白，能感受到比较大的两个点是</p><ul><li>红盘分流。原先我同时关注了天空之城，紫璋台，中梁沐宸院这三个楼盘的，他们都满足我对自住宅的选房标准，如果一个个摇，我可以摇三次，但杭州市政府为了避免出现万人摇的局面，对于很多楼盘都是同一时间发了预售证，购房者只能选择一个楼盘进行报名。</li><li>5 年限售。对于红盘，中签概率低于 10% 的楼盘增加了 5 年限售的限制，避免了投资客入场。</li></ul><p>可能也是因为这些政策的原因，加上运气的成分，我终于在第三次摇号的时候摇中了。我是幸运的，一个同期摇中的朋友是摇了 20 多次才上车的。</p><p>关于杭州楼市政策，我想给同为小白想买房的读者分享下我的看法，参考上海和深圳的楼市政策，例如上海的积分落户政策，深圳的购房限制，在逐渐火热的杭州楼市，未来可能也会出现同样的局面，趁早落户比较稳妥。杭州楼市政策在每一波开盘潮来临之后，几乎都会出现调整，需要时刻关注对个人的影响。</p><h2 id="选房"><a href="#选房" class="headerlink" title="选房"></a>选房</h2><p>在老家住的房子都是父母那一辈人辛苦打拼来的，自己住着也挺习惯，没有想过选房有哪些标准，等到了自己想要买房时，才发现自己对于选房真是一无所知。</p><p>也有人问我，杭州房子现在多少钱一平？这当然也很难回答，因为同一时间段不同的开发商，不同的地段，不同的配套等因素必然会导致不同的房价，群众眼睛是雪亮的，一分钱一分货，从摇号人数就开始看出大家的倾向性。</p><p>我整理了一些关键的参考指标，算是这几个月对杭州购房的一些总结。</p><h3 id="开发商-物业"><a href="#开发商-物业" class="headerlink" title="开发商/物业"></a>开发商/物业</h3><p>开发商的资质决定了楼盘质量，需要重点关注。不同开发商专注的楼盘档次不同（高端改善、刚需改善、刚需），高端盘自然不是我讨论涉及到的内容，我在网上搜集了这么一张排行，对于刚需和刚改盘有一定参考意义。</p><p><img src="https://image.cnkirito.cn/image-20210217165446544.png" alt="开发商"></p><p>开发商和物业理论上来说应当是两家公司，但似乎很多开发商和物业都从属于一个集团，例如万科房地产和万科物业、绿城房地产和绿城物业，品牌开发商通过成立物业公司也是对业主长期进行品牌维护的一种方式。有一些小开放商甚至会出现跑路的问题，这样的例子也不是没有出现过。</p><h3 id="地理位置"><a href="#地理位置" class="headerlink" title="地理位置"></a>地理位置</h3><p>因为我工作靠近余杭区 EFC 欧美金融城，我女朋友在余杭区西溪，都是在未来科技城板块，综合考虑的话，未来科技城肯定是首选，其次周边 10 km 以内的楼盘都可以摇号。</p><p>整个杭州楼市我也就只关注了这么些板块，例如最热的当数未来科技城和亚运村。前者是因为阿里巴巴的带动，以及近几年头条、OPPO、VIVO、富士康等公司陆续进驻，坐拥高新科技产业园区，会为后面房屋增值以及整个板块的发展带来较高的增速；后者自然不用多说，杭州要举办亚运会，虽说你可能会吐槽说亚运会和房价有啥关系，但上一次人们这么吐槽是 G20 峰会，的确带动了杭州楼市的一波价格上涨。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_jpg/FgmIXy59eQATe0x4Jl5EXHgTmzbPFzPghNEicKicwyicOZV8VYMKNSwnAXbzLkKSZIjNS9dqClE4FBqPGh3k5UAZQ/640?wx_fmt=jpeg&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="地理位置"></p><p>其次，选择楼盘时，也需要关注附近是否有高架、污水厂、垃圾处理厂、发电站等不利因素，还记得我来杭第一个月租房又换房，就是因为受不了楼下垃圾车每天 8 点准时把我吵醒，买房是更为长期的一件事，自然得需要更加慎重。</p><p>有一些朋友讲究风水，会比较在意楼盘附近是否有运河和公墓，前者会带走运势，后者总感觉瘆得慌。</p><h3 id="房屋性质"><a href="#房屋性质" class="headerlink" title="房屋性质"></a>房屋性质</h3><p><img src="https://image.cnkirito.cn/C2679A14-09C6-4B43-B57D-BFF103B6E9E2.png" alt="贝壳找房"></p><p>真正的房屋性质分类有很多，分类方式也很严谨，但是对于小白来说，我觉得只需要搞懂几个常见的概念就行了，一图胜千言，上图是我从贝壳找房上的二手房信息中截取出来的图片，最左下角是阿里巴巴总部西溪园区，我挑选出了它周边的几个小区，就比较有代表性，可以从价格上明显对比出普通住宅、公寓、回迁房的区别。虽然楼盘之间仍然有其他很多因素影响着它们的价格，但房屋性质显然是众多因素中影响比重最大的一个。</p><p>当人们在讨论买房时，99.9% 的情况讨论的是普通住宅，公寓和回迁房等其他房屋性质的房产均有不同程度的不利因素，例如产权、落户、学区等问题，这里就不一一列举了。</p><h3 id="学校"><a href="#学校" class="headerlink" title="学校"></a>学校</h3><p>学区无疑是牵动所有购房者心的一个话题，提到学区房，几乎所有人一致的反应都是：贵。之前，我对学区房一直有误解，认为有的房子买了之后，孩子是没学上的，只有学区房才有学上。不知道有没有小白跟我有一样的误解，事实上，只要是住宅性质的房子（非公寓），你的孩子就一定有学上，只不过是学校的好坏罢了。那些所谓的学区房，学区自然是划分了好的小学和初中。</p><p>对于新房，有的楼盘会包含幼儿园、小学，甚至初中（这里指的是建筑，例如有教学楼和操场），但只有交房后，才能确定是哪个具体的教育集团或者是公办学校来接管（这里指的是师资力量）。对于小孩已经快要上学的家庭，新盘就不会是一个好的选择了，基本都流向了昂贵的二手学区房。</p><p>至于新房学区能否划分到好的学区，这个完全看人品，有一定赌的成分。但我了解到有的小区没有划分到重点小学的学区，家长去教育局闹的，真的是楼市体现出生活百态。</p><p>另外一个不得不面对的现实问题便是，余杭区的高中和主城区的高中学籍是不通的。这意味着如果在余杭区读完小学初中，中考成绩再好，也没法就读主城区的知名高中，这就使得很多在余杭工作的家长，不得不跑到西湖区购置“学区房”。这让我想到了北京西二旗那批程序员的孩子，硬生生把回龙观考成“学区房”的励志故事，但毕竟是小概率事件，有条件的家长还是会更加青睐西湖区。</p><p>再说说我个人吧，毕竟考虑下一代这件事还不是我的当务之急，还是安心搞定第一套力所能及的房子吧。至于 3~4 年后，学籍政策是否有变化，这谁也说不准。</p><h3 id="交通"><a href="#交通" class="headerlink" title="交通"></a>交通</h3><p>杭州是一座交通规划远赶不上城市发展的城市，我从所在的余杭区想进西湖区，大多数情况会选择打的，而上海给我的感觉是地铁能到达任何一个地方。利好的地方在于，最近 5 号线增加了延线，14 号线也在规划中了，最明显的感受便是目前我租的房子附近，正在修建地铁站，道路也进行了拓宽，正在往好的方向发展。</p><p>交通对于楼盘的意义重大，不用我赘述。在现如今的城市发展中，交通约等于地铁，很多开发商在宣传楼盘时，都会标榜自己是地铁房，例如我摇中的楼盘就是打着“地铁万科天空之城”这样的旗号宣传的。如果当下没有地铁，也需要关注下，是否有地铁在建的规划。很多投资客会选地铁规划附近房子，等地铁建成自然会带动一波。</p><p>除了地铁，还得聊下出租车&amp;网约车，不得不吐槽下到了 9 ~ 10 点，余杭这鬼地方是真的难打车，毕竟跟杭州市中心区域还是要差一截的。</p><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>例如户型、得房率、楼层高度、装修标准等等一般都是次要因素，他们可能不会直接影响购房，但也一定会在多个楼盘对比间占据一定的权重，我就不一一总结了。</p><h2 id="获取楼盘资讯"><a href="#获取楼盘资讯" class="headerlink" title="获取楼盘资讯"></a>获取楼盘资讯</h2><p>还记得我刚接触杭州楼市时两眼一抹黑的阶段，我向朋友问的最多的一个问题便是：你们都是从哪儿知道这么多杭州楼市的资讯的？我相信很多跟我一样的小白，一定也有同样的疑问，其实当你想要去融入这个圈子时，有很多问题都会逐渐解答开。</p><p>我最早是加入了公司内部的钉钉购房群，里面有很多久战楼市的大神，看着他们对杭州楼市谈笑风生，心生敬畏。同时我也接触到一些之前觉得生僻的词汇：倒挂、得房率、浮动利率等，一边潜水，一边 Google 这些概念，最后也能融入其中进行交流。你周围同事或者朋友组建的微信群和钉钉群是最有效的信息获取渠道之一。</p><p>再推荐一些购房常用的小工具吧，微信小程序有：小鸡选房，杭州房小团。</p><p><img src="https://image.cnkirito.cn/image-20210220010213516.png" alt="小鸡选房"></p><p>里面会有杭州各个地区的楼盘信息，以及楼盘信息的详细介绍，并且可以在其中联系到销售，一般销售的服务都会非常热情。</p><p>微信公众号有：杭州发布、小鸡评房、杭州房叔。我关注的也不多，因为周围能摇的盘就那么几个。等掌握了必要的筛选技能之后，自然就掌握了自我判断的能力了，所谓久病成良医，久摇成能手。我认识一些摇号摇了几十次的朋友，他们真的已经是对杭州各个楼盘的情况烂熟于心了，当然我还是祝愿大家能不成为这样的高手，早日摇到号。</p><p>当你确定要摇一个盘时，该楼盘的销售是领着你走完最后一公里的人，包括你摇号的资格满不满足，楼盘的详细信息介绍，需要准备什么材料，以及你所想要了解的任何细节信息，都可以咨询销售。你所要做的，仅仅是在整整登记时，将他标记为你的销售，这样就算对他最大的支持了。一个好的销售至关重要，销售会可以理解为你购房的导师，陪伴着你直到交房。</p><h2 id="摇号"><a href="#摇号" class="headerlink" title="摇号"></a>摇号</h2><p>如果要选择一个跟买房最挂钩的一个词，那一定是“摇号”。其实不仅仅是杭州，目前国内一二线城市基本都会涉及到摇号这个问题。简单理解下这个流程：假设房源数量为 500 套，参与摇号的登记人数为 5000 人，每个人完成登记之后会拿到一个登记号，从 1~5000 依次递增，登记截止后不日便会正式开始摇号，摇号的依据就是登记号了，摇号结果公开后，可以根据登记号查询到一个选房顺序号，选房顺序号才是最终真正的那个“号”，1~500 号的锦鲤状态是“摇中”，501~5000 号为轮候，轮候状态并不是意味着一定选不到房，出于众多原因，例如剩余房源预算不够，楼层户型不满足预期，许多靠后的号会弃选，红盘的弃选率相对较低，但轮候顺延 10 号一般也都还好选到房子。我当时摇天空之城二期时便是轮候顺延了 60 号，到我的时候还有一个 600w 的一楼洋房可以选，但奈何实在是超出预算太多，最终还是错过了。</p><p>摇号流程最关键的准备好登记材料，因为整个登记时间非常短，只有三天时间，其中比较关键的材料我认为有两个。</p><ul><li><p>征信报告。贷款买房需要征信报告，没有信用卡的小白，建议去开一张信用卡，周期大概在一周左右。有了信用卡就可以在网上查询到个人的征信报告了。</p></li><li><p>冻资证明。提前把资金从理财产品中取出，避免 T+1 赎回到账较慢影响冻资流程。一般冻资的金额是楼盘中最便宜那套房子的 30%。</p></li></ul><p>除此之外，杭州新政中，还有一个新的概念不得不提：无房家庭。首先问一个问题：一个本科刚毕业的小伙子，来到了杭州打拼，打拼了三年之后 27 岁，在杭州准备长期发展，但还至今未婚，请问这个小伙子是否满足“无房家庭”的认证呢？答案可能违背直觉，并不符合。政策规定只有同时单身 30 岁以上无房或者已婚无房才能认定为无房家庭。</p><p>除了限购政策外，大多数新开的楼盘都增加了无房家庭的限制，这些盘也被人们称呼为“红盘”，在没有无房家庭限制时，这些红盘在历史开盘过程中，甚至出现过万人摇的盛况。而如今不满足无房家庭的认定，是没有资格摇这些红盘的，以至于我周围不少朋友为了摇号而去领了证，才成为了无房家庭。楼市新政封杀炒房客的同时，也同时堵死了一些单身年轻人买房的路。</p><p>最后在摇号资格这个话题里面要介绍的是高层次人才。人才并不是杭州独有的政策，例如我呆过的南京，也有人才的说法，只不过各个城市对待人才的政策是不同的，例如杭州的高层次人才就比较吃香，可以参与红盘的摇号，并且能够提升摇中的概率，特别是不满 30 岁的无房人才，则更是受益了。至于高层次人才认定的标准，我这里给出传送门，大家可以自行评估自己是否满足条件：<a href="http://rc.zjhz.hrss.gov.cn/articles/detail/6679.html。" target="_blank" rel="noopener">http://rc.zjhz.hrss.gov.cn/articles/detail/6679.html。</a></p><h2 id="贷款"><a href="#贷款" class="headerlink" title="贷款"></a>贷款</h2><p>由于我在写这篇文章时，贷款还没有真正办理完，所以只能将我所了解的信息给大家陈述下，仅供参考。</p><p>同样是先说政策，我这篇文章偏小白向，所以大概率咱们都是第一套房的用户，首付比例为 30%，剩下的 70% 就需要我们向银行办理贷款了。如果是纯小白，这里面涉及的知识点就比较多了，但了解起来也不难，我就三个关键点介绍下：贷款类别、贷款利率、还款方式。</p><p><img src="https://image.cnkirito.cn/image-20210220012407743.png" alt="房贷计算器"></p><p>贷款类别主要关注两种即可：商业贷款和组合型贷款。在大城市工作的同学应该都知道公司有一个福利叫五险一金，其中的一金便是我们买房时有用的住房公积金。如果你的公司没有避税，并且交纳了比较高比例额度的公积金，那么恭喜你，在买房时可以喘一口气了，除了可以将平时的公积金提取出来还款之外，还可以用来做贷款，这就是组合贷款（商业贷款+公积金贷款）的优势。在房贷计算器中可以发现，公积金贷款的利率是要远低于商业贷款的利率的，全国统一的 3.25% 利率，真是太香了。但公积金贷款是有上限的，杭州个人公积金贷款最高只有 50w，家庭则为 100w，各个地区额度有所差异，房贷计算器底部也有一些其他城市的额度信息，可供参考。剩余贷款部分的大头，还是需要使用商业贷款，相对于公积金贷款，肯定要贵一点，毕竟银行也是要恰饭的，具体的利率则会因银行不同，而有所差异，需要自行找到房地产开发商所处的银行去了解。既然组合贷款可以使用公积金，小白可能会问了，为啥还有人会选择使用纯商贷呢？这里面的考虑主要基于政策，希望将公积金贷款用于第二套改善房使用，我就不展开介绍了。</p><p>贷款利率需要介绍的是 LPR 浮动利率这个概念。前面已经提到了商业贷款比公积金贷款要贵的事实，原因也很简单，前者是银行给你放的贷款，后者相当于是国家给你的福利。但银行给你定的利率自然不能太高，不然跟高利贷有什么区别，所以央行每隔一段时间会规定一个基准利率，银行自行参考这个基准利率进行浮动，当然大部分情况是上浮，例如 2021 年 1 月 1 日的基础利率是 4.90%，而大多数银行在办理组合贷时，给出的利率是 5.2%。当然，利率上浮还不是 LPR 浮动利率的核心，只是一个引子。在选择办理贷款是可以选择 LRP 浮动利率和固定利率，在本小白看来其实就是一场小赌局，每年都在变，能不能说得准今年就是一个最低点呢？这个选项一旦确定，以后就不能更改了。如何选择我就不介绍了，因为我也不懂，希望其他小白看到这里能够理解上浮的概念即可。</p><p>最后要聊的就是等额本息和等额本金了，其实这两点房贷计算器上的说明已经很清楚了。</p><ul><li><strong>等额本息还款：</strong>把按揭贷款的本金总额与利息总额相加，然后平均分摊到还款期限的每个月中。作为还款人，每个月还给银行固定金额，但每月还款额中的本金比重逐月递增、利息比重逐月递减。</li><li><strong>等额本金还款：</strong>将本金分摊到每个月内,同时付清上一交易日至本次还款日之间的利息。这种还款方式相对等额本息而言,总的利息支出较低,但是前期支付的本金和利息较多,还款负担逐月递减。</li></ul><p>看一下相同贷款金额的每月还款金额就可以了解二者的区别了，等额本息每个月还款金额一样，刚开始还款的金额少，总还款金额相比后者多；等额本金还款金额逐月递减，刚开始还款的金额多，总还款金额相关前者少。</p><p>我选择的是等额本息，主要原因是，70% 的贷款额度已经把杠杆加的足够高了，选择等额本金的话，刚开始还贷时每月还款压力会比较大。利用好首套房 30% 的首付去加杠杆，个人认为也是一个比较好的投资方式。另外，虽然整体还款总额变多了，但今天的 100 块，到了十年后，又能相当于多少块的购买力呢？</p><p>贷款资质的评定要求贷款人提供收入证明，月供要小于月收入的一半，在办理贷款需要量力而为，否则不满足条件，银行是不会贷给你的。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>对于刚接触杭州楼市，特别是刚毕业在杭州打拼了几年，还没有买到房的小伙伴，也不用着急，杭州楼市有点饥饿营销的感觉，总给人一种这波楼盘过后，再无新盘的感觉，但开发商其实一直在拿地建房，政府为了吸引人才，肯定也会制定政策优待人才的。</p><p>最后祝大家都成为摇号潮中的锦鲤。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;2021 年刚开年，解决了人生的一个大事，没错，就像标题里透露的那样，我在杭州买房啦。第一次有在杭州买房这个念头，还要说回 2020 年 6 月，当时和朋友们聚餐时偶然聊到了房子的话题，意外地发现竟然只有自己还没有在杭州买房，其中不乏有 96 年如此年轻的小伙子，自那次聚餐之后，我便开始关注起了杭州的楼市。&lt;/p&gt;
&lt;p&gt;先说结果吧，我参与摇号的盘是在杭州市余杭区未来科技城的【天空之城】，摇号结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://image.cnkirito.cn/image-20210220132201842.png&quot; alt=&quot;摇号结果&quot;&gt;&lt;/p&gt;
&lt;p&gt;这是个什么概念呢，一共 6181 户参与摇号，房子一共有 1014 套，大概有 1/6 的人算是摇中，而我是摇中的人里面第 2 顺位选房的。记得等待摇号结果的那个周日中午，等待结果通知的那最后一个小时，真的比高考都要紧张，所幸不负期许，人品可以说非常爆炸了，希望摇号这件事没有花光我今年所有运气。&lt;/p&gt;
&lt;p&gt;预计在大年初七~初十的时间点还需要办理预审并交付首付，再往后还要办理按揭、网签等流程，还需要忙活一段时间才算尘埃落定。趁着过年这段时间比较空闲，所幸记录一下自己在杭州楼市的经历，这样也给那些刚来到杭州，想要定居于此的小白们一些参考。&lt;/p&gt;
    
    </summary>
    
      <category term="技术杂谈" scheme="https://lexburner.github.io/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    
    
      <category term="买房" scheme="https://lexburner.github.io/tags/%E4%B9%B0%E6%88%BF/"/>
    
  </entry>
  
  <entry>
    <title>从校园到职场，聊聊实习这点事</title>
    <link href="https://lexburner.github.io/school-internship/"/>
    <id>https://lexburner.github.io/school-internship/</id>
    <published>2021-01-31T11:09:53.000Z</published>
    <updated>2021-02-01T04:00:28.792Z</updated>
    
    <content type="html"><![CDATA[<p>从我最近发的实习招聘文章就可以知道，我最近在忙春招的事了，本人也非常“荣幸”，担任了我们团队这次春招的负责人。陆陆续续沟通了很多学生，于是在这个周末抽出了一点时间，跟大家聊聊我对校园实习的一些看法。</p><a id="more"></a><p>就在我发完实习招聘的当天下午，一位同学就联系到了我，非常客气地称呼我为“老师”，从申请好友时的备注中得知其是一位名校的学生，希望找一份暑期实习的工作。由于是第一个加我微信的同学，我兴致很高地跟他沟通了番。</p><p>说实话，这位同学虽然学历不错，但交流下来，发现在编程实践经验上差了那么一点，但也能理解，毕竟是学生吗，可能还是以学习学校课本上的知识为主。由于我们是中间件岗位，我又继续跟他聊了下中间件，问他对中间件的了解有多少，他说没了解过，但我可以学，说到这儿，我眉头开始一紧，继续追问他，这你也敢投简历？他回答说，阿里一直是我梦寐以求的公司。一时语塞，让我联想到了当初面试阿里时的自己，貌似也说过同样的话。最后我草草结束了交流，还是将他的简历投递进了系统。</p><p>接触过这些大三/研二找实习的学生之后，我不禁回忆起了自己的经历。我大三的时候都没有意识到要找一份实习，甚至觉得公司招聘实习生，就是在招一些廉价劳动力，去干一些脏活累活。直到应届毕业之后，才了解到原来国内这些大公司，几乎不招聘应届生，要么是 3 年工作经验起步，要么是校招实习转正，几乎没有第三种情况！</p><p>很多学生没有实习计划，或者出于对工作的恐惧，或者出于对自身能力的不自信，又或者是对实习工资的不屑…也有一大部分同学，觉得春招太早了，希望留着机会等到秋招。我这里有一份据阿里巴巴某部门的公开数据：2020 年转正入职的校招生中 80% 来自于春招，20% 来自于秋招。「好书一本，明日在读」，我不推荐大家错过现在这个最好的时机。</p><p>换个角度思考，公司为什么招聘实习生？先说一个好消息，请大家相信我，国内互联网公司绝不会为了招聘廉价劳动力而希望你来参加实习，再说一个坏消息，也请大家相信我，这些公司不是福利机构，给大家提供实习机会，还给大家发工资，绝不是为了帮助学生出国留学，或者方便去 title 更响亮的公司入职。公司招聘实习生，是为了提前物色人才，在实习中培养你，希望你毕业后能够入职，持续补充公司的新鲜血液。</p><p>交流过程中，也有同学比较关心实习薪资的问题，通常还都是技术不错的同学，希望能够议价。以我的了解，通常国内大厂的实习工资都是固定的，不会特别高，本科 200，研究生 300 大概这样的标准，还有 2000/ 月的住房补贴。这里并没有贬低同学的意思，但需要认识到一个事实，即便你绩点 5.0、ACM 金牌、挑战赛国一，进入公司后，依旧需要熟悉开发配置环境，熟悉公司流程，熟悉团队所用的技术框架，再到真正的开发，实习这几个月真的很短。 实习期间能够独立完成完成 1 个特性开发已经算是了不起的存在了，公司还需要投入一个 P7 级别的技术专家 1 对 1 带。大可不必计较这几个月的薪资。</p><p>对于非名校或非科班的学生，也请不必避讳、不必吝惜你简历的投递。只需要你各类程序竞赛中有较高的名次，专业课上有排名靠前的成绩，有很好的博客积累，有开源项目的贡献经验，有权威机构的论文投递经验，照样可以获得公司的青睐。但也不能盲目乐观，相比名校专业对口的学生，他们真的更有优势，也比双非的同学有更早更多的积累，「没伞的孩子要学会奔跑」。也请同学们理解，我们需要一定的区分度，最终是希望找到可以一起共事的人。同样用一份公开数据说话给这些学生以信心，近几年均有 40% 的学生比例来自 TOP 23 之外的学校。</p><p>实习生刚起步，可以塑造成任意的形状，你在学校积累的那些经验不应该完全作为你实习的参考。例如我收到一份机器学习方向研究生的简历，有意向从事中间件的开发，起初我也是不理解，因为机器学习近几年也很火热，但交流过程中学生对于自身择业方向的思考说服了我，学生基于自身专业和意向城市中的互联网公司分布综合考虑，决定了选择中间件方向。有些道理大家都懂</p><ul><li>Java 更偏向于业务研发和中间件研发，C/C++ 更偏向于数据库研发，Go 更偏向于云原生研发，Python 更偏向于机器学习</li><li>北上杭有阿里；深圳有腾讯；广州有微信；北京有一大波互联网…</li></ul><p>语言和方向有绑定关系，城市和公司的选择有指导性，但人没有定型。借助于实习的机会，你可以测试下自己适合什么岗位，什么公司的价值观适合你，什么样的团队氛围适合你成长，这些东西老师和学校帮不了你，我这篇文章帮不了你，但是一次实习机会可以。千万不要以一个熟悉的课题方向，一个对你有影响力的学长，一份职业工资排行就决定了你的方向。很多机会放在你面前，选择一个靠谱的公司，一个你认为能够 cover 的方向即可，真巧，我们“阿里云云原生中间件”部门就十分欢迎你来。</p><p>如何准备实习简历？我交流下来发现很多学生是很优秀的，但缺少表达技巧，准备的简历却过于简单。前面我介绍了公司筛选候选学生的流程，学生需要做的是：</p><ol><li>突出项目经历：参加学校/公司 xx 项目，采用了 xx 技术，解决了 xx 难题</li><li>突出学习经历：xx 专业课成绩 xx，专业排名 top xx；获得 xx 奖学金</li><li>突出竞赛经历：参与了 xx 举办的 xx 竞赛，竞赛主要考验了学生 xx 的能力，取得了 xx/xx 的名次，收获了 xx 的技能；xx 网站解题量 xx。</li><li>突出开源经历：参与 xx 开源项目，该项目是 xx。我参与贡献了 xx 模块的代码，附带 issue/pr</li><li>突出优秀论文：攻克了 xx 领域的难题，并且在 xx 发表，突出影响力</li><li>突出博客积累：文章主要体现了我在 xx 领域的思考，以及学习、编码经验的积累，附上博客链接，uv 或阅读量</li></ol><p>如果你没有上述的任意一项，说明你的积累相比同时期的学生已经稍有落后了。但是此时，你仍然可以表现出对技术的追求，对技术的热情，大家都很乐意帮助那些努力上进的同学，面试官也希望在同学身上看到自驱力，按照经验，这类同学的成长速度也会很快。</p><p>最后，我当然也希望你有自己的思考，择业是一件非常私人的事，没有人可以替你做选择，综合考虑自身的学习经历，不要自恃过高，也不要妄自菲薄，非常期待能和你共事。如果你也恰好有意向从事中间件的开发，随时欢迎你与我【微信 id：xiayimiaoshenghua】咨询，我这里是【阿里云云原生中间件】团队，我们正在针对 22 届的学生进行春季实习的意向沟通。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;从我最近发的实习招聘文章就可以知道，我最近在忙春招的事了，本人也非常“荣幸”，担任了我们团队这次春招的负责人。陆陆续续沟通了很多学生，于是在这个周末抽出了一点时间，跟大家聊聊我对校园实习的一些看法。&lt;/p&gt;
    
    </summary>
    
      <category term="技术杂谈" scheme="https://lexburner.github.io/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    
    
      <category term="实习" scheme="https://lexburner.github.io/tags/%E5%AE%9E%E4%B9%A0/"/>
    
      <category term="职场" scheme="https://lexburner.github.io/tags/%E8%81%8C%E5%9C%BA/"/>
    
  </entry>
  
  <entry>
    <title>ACK 部署 Apache apisix-ingress-cotroller</title>
    <link href="https://lexburner.github.io/apisix-ingress/"/>
    <id>https://lexburner.github.io/apisix-ingress/</id>
    <published>2021-01-23T06:37:39.000Z</published>
    <updated>2021-01-28T18:08:02.334Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>Ingress 是 Kubernetes 中一个值得关注的模块，作为外部访问 Kubernetes 集群服务的入口，市面上已经有了多种 <a href="https://kubernetes.io/zh/docs/concepts/services-networking/ingress-controllers/" target="_blank" rel="noopener">Ingress controller</a> 的实现。国产实时、高性能的 API 网关 Apache APISIX 推出的 <a href="https://github.com/apache/apisix-ingress-controller" target="_blank" rel="noopener">Apache/apisix-ingress-controller</a> 就是其中一员，作为功能更加强大的 ingress 对外提供服务。笔者准备在阿里云 ACK 集群上部署测试。 </p><a id="more"></a><h2 id="主题描述"><a href="#主题描述" class="headerlink" title="主题描述"></a>主题描述</h2><p>本文主要介绍在阿里云 ACK 部署 apisix-ingress-controller，并且使用 httpbin 测试一个简单的场景。</p><h2 id="部署拓扑"><a href="#部署拓扑" class="headerlink" title="部署拓扑"></a>部署拓扑</h2><p><img src="https://mmbiz.qlogo.cn/mmbiz_png/3ej9lic1DDEGvUsfyfXJJicAQiajss6KjO7r3kK0DfKJwY90uGJlT5uZ3iajcicqialnDG1sZbTOLpBumgRVxyh9S5Lw/0?wx_fmt=png" alt="img"> </p><h2 id="依赖项"><a href="#依赖项" class="headerlink" title="依赖项"></a>依赖项</h2><p>阿里云的 ACK 集群 ；推荐最低配置：3个 master 节点：CPU 2核  内存 4G2个 worker 节点：CPU 4核  内存 8G</p><h2 id="安装步骤"><a href="#安装步骤" class="headerlink" title="安装步骤"></a>安装步骤</h2><h3 id="apisix-2-1-release"><a href="#apisix-2-1-release" class="headerlink" title="apisix 2.1 release"></a>apisix 2.1 release</h3><p>通过 helm 安装 apisix 2.1 release</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></pre></td><td class="code"><pre><span class="line">$ kubectl create ns apisix</span><br><span class="line">$ git clone https://github.com/apache/apisix-helm-chart.git</span><br><span class="line">$ cd ./apisix-helm-chart</span><br><span class="line">$ helm repo add bitnami https://charts.bitnami.com/bitnami</span><br><span class="line">$ helm dependency update ./chart/apisix</span><br><span class="line">$ helm install apisix ./chart/apisix \</span><br><span class="line">  --set gateway.type=LoadBalancer \</span><br><span class="line">  --set allow.ipList=&quot;&#123;0.0.0.0/0&#125;&quot; \</span><br><span class="line">  --namespace apisix</span><br></pre></td></tr></table></figure><blockquote><p>tips: etcd 安装时指定 PVC， PVC 在阿里云部署时，需要指定 PV 为云盘， 请在 PVC 的 annotations 中增加：volume.beta.kubernetes.io/storage-class: alicloud-disk-ssd。(关于 PVC 和 PV 的关系请参考<a href="https://kubernetes.io/zh/docs/concepts/storage/persistent-volumes/" target="_blank" rel="noopener">这里</a>)</p></blockquote><h3 id="apisix-ingress-controller"><a href="#apisix-ingress-controller" class="headerlink" title="apisix-ingress-controller"></a>apisix-ingress-controller</h3><p>通过 helm 安装 apisix-ingress-controller</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></pre></td><td class="code"><pre><span class="line">$ git clone https://github.com/apache/apisix-ingress-controller.git</span><br><span class="line">$ cd ./apisix-ingress-controller</span><br><span class="line">$ helm install ingress-apisix-base -n apisix ./charts/base</span><br><span class="line">$ helm install ingress-apisix ./charts/ingress-apisix \</span><br><span class="line">  --set ingressController.image.tag=dev \</span><br><span class="line">  --set ingressController.config.apisix.baseURL=http://apisix-admin:9180/apisix/admin \</span><br><span class="line">  --set ingressController.config.apisix.adminKey=edd1c9f034335f136f87ad84b625c8f1 \</span><br><span class="line">  --namespace apisix</span><br></pre></td></tr></table></figure><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><h3 id="检查集群是否部署成功"><a href="#检查集群是否部署成功" class="headerlink" title="检查集群是否部署成功"></a>检查集群是否部署成功</h3><p><img src="https://uploader.shimo.im/f/LO0YnRdi4muxnhcy.png" alt="img"> </p><p><img src="https://uploader.shimo.im/f/wCBnC1p2enFZhmDl.png" alt="img"></p><h3 id="配置一个简单的路由做测试"><a href="#配置一个简单的路由做测试" class="headerlink" title="配置一个简单的路由做测试"></a>配置一个简单的路由做测试</h3><figure class="highlight yaml"><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="attr">apiVersion:</span> <span class="string">apisix.apache.org/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">ApisixRoute</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">httpbin-route</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">apisix</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">host:</span> <span class="string">httpbin.apisix.com</span></span><br><span class="line">    <span class="attr">http:</span></span><br><span class="line">      <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">backend:</span></span><br><span class="line">          <span class="attr">serviceName:</span> <span class="string">httpbin</span></span><br><span class="line">          <span class="attr">servicePort:</span> <span class="number">80</span></span><br><span class="line">        <span class="attr">path:</span> <span class="string">/hello*</span></span><br></pre></td></tr></table></figure><p>通过 apisix admin api 查看结果，发现路由已经正确配置。</p><p><img src="https://uploader.shimo.im/f/x0F4IbzvPkFIpbwK.png" alt="img"></p><figure class="highlight yaml"><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></pre></td><td class="code"><pre><span class="line"><span class="string">&#123;</span></span><br><span class="line">    <span class="attr">"action":</span> <span class="string">"get"</span><span class="string">,</span></span><br><span class="line">    <span class="attr">"count":</span> <span class="string">"2"</span><span class="string">,</span></span><br><span class="line">    <span class="attr">"header":</span> <span class="string">&#123;</span></span><br><span class="line">        <span class="attr">"revision":</span> <span class="string">"46"</span><span class="string">,</span></span><br><span class="line">        <span class="attr">"cluster_id":</span> <span class="string">"8320356269565269865"</span><span class="string">,</span></span><br><span class="line">        <span class="attr">"raft_term":</span> <span class="string">"2"</span><span class="string">,</span></span><br><span class="line">        <span class="attr">"member_id":</span> <span class="string">"3807956127770623265"</span></span><br><span class="line">    <span class="string">&#125;,</span></span><br><span class="line">    <span class="attr">"node":</span> <span class="string">&#123;</span></span><br><span class="line">        <span class="attr">"key":</span> <span class="string">"/apisix/upstreams"</span><span class="string">,</span></span><br><span class="line">        <span class="attr">"dir":</span> <span class="literal">true</span><span class="string">,</span></span><br><span class="line">        <span class="attr">"modifiedIndex":</span> <span class="number">27</span><span class="string">,</span></span><br><span class="line">        <span class="attr">"createdIndex":</span> <span class="number">3</span><span class="string">,</span></span><br><span class="line">        <span class="attr">"nodes":</span> <span class="string">[</span></span><br><span class="line">            <span class="string">&#123;</span></span><br><span class="line">                <span class="attr">"key":</span> <span class="string">"/apisix/upstreams/00000000000000000041"</span><span class="string">,</span></span><br><span class="line">                <span class="attr">"modifiedIndex":</span> <span class="number">42</span><span class="string">,</span></span><br><span class="line">                <span class="attr">"value":</span> <span class="string">&#123;</span></span><br><span class="line">                    <span class="attr">"nodes":</span> <span class="string">&#123;</span></span><br><span class="line">                        <span class="attr">"172.20.1.12:80":</span> <span class="number">100</span></span><br><span class="line">                    <span class="string">&#125;,</span></span><br><span class="line">                    <span class="attr">"type":</span> <span class="string">"roundrobin"</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">"pass_host":</span> <span class="string">"pass"</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">"hash_on":</span> <span class="string">"vars"</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">"desc":</span> <span class="string">"apisix_httpbin_80"</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">"create_time":</span> <span class="number">1608561159</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">"update_time":</span> <span class="number">1608561159</span></span><br><span class="line">                <span class="string">&#125;,</span></span><br><span class="line">                <span class="attr">"createdIndex":</span> <span class="number">42</span></span><br><span class="line">            <span class="string">&#125;</span></span><br><span class="line">        <span class="string">]</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="扩容-httpbin"><a href="#扩容-httpbin" class="headerlink" title="扩容 httpbin"></a>扩容 httpbin</h3><p><img src="https://uploader.shimo.im/f/oiZhhGJCFm2CVBkN.png" alt="img"></p><p>查看 k8s 中 httpbin</p><p><img src="https://uploader.shimo.im/f/L0RmaE5s6W9rX7qZ.png!thumbnail" alt="img"></p><p>查看 apisix 中 httpbin upstream</p><p><img src="https://uploader.shimo.im/f/1GlbOwZThCKJ8bwL.png" alt="img"></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><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="comment">// 格式化后</span></span><br><span class="line">&#123;</span><br><span class="line">    ...</span><br><span class="line">        "nodes": &#123;</span><br><span class="line">            "172.20.1.12:80": 100,</span><br><span class="line">            "172.20.0.198:80": 100,</span><br><span class="line">            "172.20.0.197:80": 100</span><br><span class="line">        &#125;,</span><br><span class="line">        "id": "00000000000000000041",</span><br><span class="line">        "key": "/apisix/upstreams/00000000000000000041",</span><br><span class="line">        "desc": "apisix_httpbin_80",</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p> 本文在 ACK 集群环境依次安装了 Etcd、 Apache APISIX、Apache apisix-ingress-controller，并且使用 httpbin 服务验证 ingress 的基本配置功能，通过 CRD 配置了路由，检测了后端服务在扩缩容时服务注册发现机制。 </p><p>另外值得一提的是 apisix-ingress-controller 可以完整的支持 Apache APISIX 提供的所有插件，甚至是自定义插件。功能丰富且扩展能力强，是一款不错的 Ingress 项目。</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;Ingress 是 Kubernetes 中一个值得关注的模块，作为外部访问 Kubernetes 集群服务的入口，市面上已经有了多种 &lt;a href=&quot;https://kubernetes.io/zh/docs/concepts/services-networking/ingress-controllers/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ingress controller&lt;/a&gt; 的实现。国产实时、高性能的 API 网关 Apache APISIX 推出的 &lt;a href=&quot;https://github.com/apache/apisix-ingress-controller&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Apache/apisix-ingress-controller&lt;/a&gt; 就是其中一员，作为功能更加强大的 ingress 对外提供服务。笔者准备在阿里云 ACK 集群上部署测试。 &lt;/p&gt;
    
    </summary>
    
      <category term="Apisix" scheme="https://lexburner.github.io/categories/Apisix/"/>
    
    
      <category term="Apisix" scheme="https://lexburner.github.io/tags/Apisix/"/>
    
  </entry>
  
  <entry>
    <title>用了这么久配置中心，还不知道长轮询是什么？</title>
    <link href="https://lexburner.github.io/nacos-and-longpolling/"/>
    <id>https://lexburner.github.io/nacos-and-longpolling/</id>
    <published>2021-01-23T06:37:39.000Z</published>
    <updated>2021-01-28T18:06:45.788Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>传统的静态配置方式想要修改某个配置时，必须重新启动一次应用，如果是数据库连接串的变更，那可能还容易接受一些，但如果变更的是一些运行时实时感知的配置，如某个功能项的开关，重启应用就显得有点大动干戈了。配置中心正是为了解决此类问题应运而生的，特别是在微服务架构体系中，更倾向于使用配置中心来统一管理配置。</p><p>配置中心最核心的能力就是配置的动态推送，常见的配置中心如 Nacos、Apollo 等都实现了这样的能力。在早期接触配置中心时，我就很好奇，配置中心是如何做到服务端感知配置变化实时推送给客户端的，在没有研究过配置中心的实现原理之前，我一度认为配置中心是通过<strong>长连接</strong>来做到配置推送的。事实上，目前比较流行的两款配置中心：Nacos 和 Apollo 恰恰都没有使用<strong>长连接</strong>，而是使用的<strong>长轮询</strong>。本文便是介绍一下长轮询这种听起来好像已经是上个世纪的技术，老戏新唱，看看能不能品出别样的韵味。文中会有代码示例，呈现一个简易的配置监听流程。</p><a id="more"></a><h2 id="数据交互模式"><a href="#数据交互模式" class="headerlink" title="数据交互模式"></a>数据交互模式</h2><p>众所周知，数据交互有两种模式：Push（推模式）和 Pull（拉模式）。</p><p>推模式指的是客户端与服务端建立好网络长连接，服务方有相关数据，直接通过长连接通道推送到客户端。其优点是及时，一旦有数据变更，客户端立马能感知到；另外对客户端来说逻辑简单，不需要关心有无数据这些逻辑处理。缺点是不知道客户端的数据消费能力，可能导致数据积压在客户端，来不及处理。</p><p>拉模式指的是客户端主动向服务端发出请求，拉取相关数据。其优点是此过程由客户端发起请求，故不存在推模式中数据积压的问题。缺点是可能不够及时，对客户端来说需要考虑数据拉取相关逻辑，何时去拉，拉的频率怎么控制等等。</p><h2 id="长轮询与轮询"><a href="#长轮询与轮询" class="headerlink" title="长轮询与轮询"></a>长轮询与轮询</h2><p>在开头，重点介绍一下长轮询（Long Polling）和轮询（Polling）的区别，两者都是拉模式的实现。</p><p>「轮询」是指不管服务端数据有无更新，客户端每隔定长时间请求拉取一次数据，可能有更新数据返回，也可能什么都没有。配置中心如果使用「轮询」实现动态推送，会有以下问题：</p><ul><li>推送延迟。客户端每隔 5s 拉取一次配置，若配置变更发生在第 6s，则配置推送的延迟会达到 4s。</li><li>服务端压力。配置一般不会发生变化，频繁的轮询会给服务端造成很大的压力。</li><li>推送延迟和服务端压力无法中和。降低轮询的间隔，延迟降低，压力增加；增加轮询的间隔，压力降低，延迟增高。</li></ul><p>「长轮询」则不存在上述的问题。客户端发起长轮询，如果服务端的数据没有发生变更，会 hold 住请求，直到服务端的数据发生变化，或者等待一定时间超时才会返回。返回后，客户端又会立即再次发起下一次长轮询。配置中心使用「长轮询」如何解决「轮询」遇到的问题也就显而易见了：</p><ul><li>推送延迟。服务端数据发生变更后，长轮询结束，立刻返回响应给客户端。</li><li>服务端压力。长轮询的间隔期一般很长，例如 30s、60s，并且服务端 hold 住连接不会消耗太多服务端资源。</li></ul><p>以 Nacos 为例的长轮询流程如下：</p><p><img src="https://image.cnkirito.cn/image-20210124145858668.png" alt="nacos long polling"></p><p>可能有人会有疑问，为什么一次长轮询需要等待一定时间超时，超时后又发起长轮询，为什么不让服务端一直 hold 住？主要有两个层面的考虑，一是连接稳定性的考虑，长轮询在传输层本质上还是走的 TCP 协议，如果服务端假死、fullgc 等异常问题，或者是重启等常规操作，长轮询没有应用层的心跳机制，仅仅依靠 TCP 层的心跳保活很难确保可用性，所以一次长轮询设置一定的超时时间也是在确保可用性。除此之外，在配置中心场景，还有一定的业务需求需要这么设计。在配置中心的使用过程中，用户可能随时新增配置监听，而在此之前，长轮询可能已经发出，新增的配置监听无法包含在旧的长轮询中，所以在配置中心的设计中，一般会在一次长轮询结束后，将新增的配置监听给捎带上，而如果长轮询没有超时时间，只要配置一直不发生变化，响应就无法返回，新增的配置也就没法设置监听了。</p><h2 id="配置中心长轮询设计"><a href="#配置中心长轮询设计" class="headerlink" title="配置中心长轮询设计"></a>配置中心长轮询设计</h2><p>上文的图中，介绍了长轮询的流程，本节会详解配置中心长轮询的设计细节。</p><ul><li><p>客户端发起长轮询</p><p>客户端发起一个 HTTP 请求，请求信息包含配置中心的地址，以及监听的 dataId（本文出于简化说明的考虑，认为 dataId 是定位配置的唯一键）。若配置没有发生变化，客户端与服务端之间一直处于连接状态。</p></li><li><p>服务端监听数据变化</p><p>服务端会维护 dataId 和长轮询的映射关系，如果配置发生变化，服务端会找到对应的连接，为响应写入更新后的配置内容。如果超时内配置未发生变化，服务端找到对应的超时长轮询连接，写入 304 响应。</p><blockquote><p>304 在 HTTP 响应码中代表“未改变”，并不代表错误。比较契合长轮询时，配置未发生变更的场景。</p></blockquote></li><li><p>客户端接收长轮询响应</p><p>首先查看响应码是 200 还是 304，以判断配置是否变更，做出相应的回调。之后再次发起下一次长轮询。</p></li><li><p>服务端设置配置写入的接入点</p><p>主要用配置控制台和 client 发布配置，触发配置变更</p></li></ul><p>这几点便是配置中心实现长轮询的核心步骤，也是指导下面章节代码实现的关键。但在编码之前，仍有一些其他的注意点需要实现阐明。</p><p>配置中心往往是为分布式的集群提供服务的，而每个机器上部署的应用，又会有多个 dataId 需要监听，实例级别 * 配置数是一个不小的数字，配置中心服务端维护这些 dataId 的长轮询连接显然不能用线程一一对应，否则会导致服务端线程数爆炸式增长。一个 Tomcat 也就 200 个线程，长轮询也不应该阻塞 Tomcat 的业务线程，所以需要配置中心在实现长轮询时，往往采用异步响应的方式来实现。而比较方便实现异步 HTTP 的常见手段便是 Servlet3.0 提供的 AsyncContext 机制。</p><blockquote><p>Servlet3.0 并不是一个特别新的规范，它跟 Java 6 是同一时期的产物。例如 SpringBoot 内嵌的 Tomcat 很早就支持了 Servlet3.0，你无需担心 AsyncContext 机制不起作用。</p></blockquote><p>SpringMVC 实现了 DeferredResult 和 Servlet3.0 提供的 AsyncContext 其实没有多大区别，我并没有深入研究过两个实现背后的源码，但从使用层面上来看，AsyncContext 更加的灵活，例如其可以自定义响应码，而 DeferredResult 在上层做了封装，可以快速的帮助开发者实现一个异步响应，但没法细粒度地控制响应。所以下文的示例中，我选择了 AsyncContext。</p><h2 id="配置中心长轮询实现"><a href="#配置中心长轮询实现" class="headerlink" title="配置中心长轮询实现"></a>配置中心长轮询实现</h2><h3 id="客户端实现"><a href="#客户端实现" class="headerlink" title="客户端实现"></a>客户端实现</h3><figure class="highlight java"><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><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf</span>4j</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ConfigClient</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> CloseableHttpClient httpClient;</span><br><span class="line">    <span class="keyword">private</span> RequestConfig requestConfig;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">ConfigClient</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.httpClient = HttpClientBuilder.create().build();</span><br><span class="line">        <span class="comment">// ① httpClient 客户端超时时间要大于长轮询约定的超时时间</span></span><br><span class="line">        <span class="keyword">this</span>.requestConfig = RequestConfig.custom().setSocketTimeout(<span class="number">40000</span>).build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@SneakyThrows</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">longPolling</span><span class="params">(String url, String dataId)</span> </span>&#123;</span><br><span class="line">        String endpoint = url + <span class="string">"?dataId="</span> + dataId;</span><br><span class="line">        HttpGet request = <span class="keyword">new</span> HttpGet(endpoint);</span><br><span class="line">        CloseableHttpResponse response = httpClient.execute(request);</span><br><span class="line">        <span class="keyword">switch</span> (response.getStatusLine().getStatusCode()) &#123;</span><br><span class="line">            <span class="keyword">case</span> <span class="number">200</span>: &#123;</span><br><span class="line">                BufferedReader rd = <span class="keyword">new</span> BufferedReader(<span class="keyword">new</span> InputStreamReader(response.getEntity()</span><br><span class="line">                    .getContent()));</span><br><span class="line">                StringBuilder result = <span class="keyword">new</span> StringBuilder();</span><br><span class="line">                String line;</span><br><span class="line">                <span class="keyword">while</span> ((line = rd.readLine()) != <span class="keyword">null</span>) &#123;</span><br><span class="line">                    result.append(line);</span><br><span class="line">                &#125;</span><br><span class="line">                response.close();</span><br><span class="line">                String configInfo = result.toString();</span><br><span class="line">                log.info(<span class="string">"dataId: [&#123;&#125;] changed, receive configInfo: &#123;&#125;"</span>, dataId, configInfo);</span><br><span class="line">                longPolling(url, dataId);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// ② 304 响应码标记配置未变更</span></span><br><span class="line">            <span class="keyword">case</span> <span class="number">304</span>: &#123;</span><br><span class="line">                log.info(<span class="string">"longPolling dataId: [&#123;&#125;] once finished, configInfo is unchanged, longPolling again"</span>, dataId);</span><br><span class="line">                longPolling(url, dataId);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">default</span>: &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"unExcepted HTTP status code"</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// httpClient 会打印很多 debug 日志，关闭掉</span></span><br><span class="line">        Logger logger = (Logger)LoggerFactory.getLogger(<span class="string">"org.apache.http"</span>);</span><br><span class="line">        logger.setLevel(Level.INFO);</span><br><span class="line">        logger.setAdditive(<span class="keyword">false</span>);</span><br><span class="line"></span><br><span class="line">        ConfigClient configClient = <span class="keyword">new</span> ConfigClient();</span><br><span class="line">        <span class="comment">// ③ 对 dataId: user 进行配置监听 </span></span><br><span class="line">        configClient.longPolling(<span class="string">"http://127.0.0.1:8080/listener"</span>, <span class="string">"user"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主要有三个注意点：</p><ul><li><code>RequestConfig.custom().setSocketTimeout(40000).build()</code> 。httpClient 客户端超时时间要大于长轮询约定的超时时间。很好理解，不然还没等服务端返回，客户端会自行断开 HTTP 连接。</li><li><code>response.getStatusLine().getStatusCode() == 304</code> 。前文介绍过，约定使用 304 响应码来标识配置未发生变更，客户端继续发起长轮询。</li><li><code>configClient.longPolling(&quot;http://127.0.0.1:8080/listener&quot;, &quot;user&quot;)</code>。在示例中，我们处于简单考虑，仅仅启动一个客户端，对单一的 dataId：user 进行监听（注意，需要先启动 server 端）。</li></ul><h3 id="服务端实现"><a href="#服务端实现" class="headerlink" title="服务端实现"></a>服务端实现</h3><figure class="highlight java"><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><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@Slf</span>4j</span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ConfigServer</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Data</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">AsyncTask</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 长轮询请求的上下文，包含请求和响应体</span></span><br><span class="line">        <span class="keyword">private</span> AsyncContext asyncContext;</span><br><span class="line">        <span class="comment">// 超时标记</span></span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">boolean</span> timeout;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">AsyncTask</span><span class="params">(AsyncContext asyncContext, <span class="keyword">boolean</span> timeout)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.asyncContext = asyncContext;</span><br><span class="line">            <span class="keyword">this</span>.timeout = timeout;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// guava 提供的多值 Map，一个 key 可以对应多个 value</span></span><br><span class="line">    <span class="keyword">private</span> Multimap&lt;String, AsyncTask&gt; dataIdContext = Multimaps.synchronizedSetMultimap(HashMultimap.create());</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> ThreadFactory threadFactory = <span class="keyword">new</span> ThreadFactoryBuilder().setNameFormat(<span class="string">"longPolling-timeout-checker-%d"</span>)</span><br><span class="line">        .build();</span><br><span class="line">    <span class="keyword">private</span> ScheduledExecutorService timeoutChecker = <span class="keyword">new</span> ScheduledThreadPoolExecutor(<span class="number">1</span>, threadFactory);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 配置监听接入点</span></span><br><span class="line">    <span class="meta">@RequestMapping</span>(<span class="string">"/listener"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addListener</span><span class="params">(HttpServletRequest request, HttpServletResponse response)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        String dataId = request.getParameter(<span class="string">"dataId"</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 开启异步</span></span><br><span class="line">        AsyncContext asyncContext = request.startAsync(request, response);</span><br><span class="line">        AsyncTask asyncTask = <span class="keyword">new</span> AsyncTask(asyncContext, <span class="keyword">true</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 维护 dataId 和异步请求上下文的关联</span></span><br><span class="line">        dataIdContext.put(dataId, asyncTask);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 启动定时器，30s 后写入 304 响应</span></span><br><span class="line">        timeoutChecker.schedule(() -&gt; &#123;</span><br><span class="line">            <span class="keyword">if</span> (asyncTask.isTimeout()) &#123;</span><br><span class="line">                dataIdContext.remove(dataId, asyncTask);</span><br><span class="line">                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);</span><br><span class="line">                asyncContext.complete();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;, <span class="number">30000</span>, TimeUnit.MILLISECONDS);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 配置发布接入点</span></span><br><span class="line">    <span class="meta">@RequestMapping</span>(<span class="string">"/publishConfig"</span>)</span><br><span class="line">    <span class="meta">@SneakyThrows</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">publishConfig</span><span class="params">(String dataId, String configInfo)</span> </span>&#123;</span><br><span class="line">        log.info(<span class="string">"publish configInfo dataId: [&#123;&#125;], configInfo: &#123;&#125;"</span>, dataId, configInfo);</span><br><span class="line">        Collection&lt;AsyncTask&gt; asyncTasks = dataIdContext.removeAll(dataId);</span><br><span class="line">        <span class="keyword">for</span> (AsyncTask asyncTask : asyncTasks) &#123;</span><br><span class="line">            asyncTask.setTimeout(<span class="keyword">false</span>);</span><br><span class="line">            HttpServletResponse response = (HttpServletResponse)asyncTask.getAsyncContext().getResponse();</span><br><span class="line">            response.setStatus(HttpServletResponse.SC_OK);</span><br><span class="line">            response.getWriter().println(configInfo);</span><br><span class="line">            asyncTask.getAsyncContext().complete();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">"success"</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SpringApplication.run(ConfigServer<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对上述实现的一些说明：</p><p><code>@RequestMapping(&quot;/listener&quot;)</code> ，配置监听接入点，也是长轮询的入口。在获取 dataId 之后，使用 <code>request.startAsync</code> 将请求设置为异步，这样在方法结束后，不会占用 Tomcat 的线程池。</p><p>接着 <code>dataIdContext.put(dataId, asyncTask)</code> 会将 dataId 和异步请求上下文给关联起来，方便配置发布时，拿到对应的上下文。注意这里使用了一个 guava 提供的数据结构 <code>Multimap&lt;String, AsyncTask&gt; dataIdContext</code> ，它是一个多值 Map，一个 key 可以对应多个 value，你也可以理解为 <code>Map&lt;String,List&lt;AsyncTask&gt;&gt;</code> ，但使用 <code>Multimap</code> 维护起来可以更方便地处理一些并发逻辑。至于为什么会有多值，很好理解，因为配置中心的 Server 端会接受来自多个客户端对同一个 dataId 的监听。</p><p><code>timeoutChecker.schedule()</code> 启动定时器，30s 后写入 304 响应。再结合之前客户端的逻辑，接收到 304 之后，会重新发起长轮询，形成一个循环。</p><p><code>@RequestMapping(&quot;/publishConfig&quot;)</code> ，配置发布的入口。配置变更后，根据 dataId 一次拿出所有的长轮询，为之写入变更的响应，同时不要忘记取消定时任务。至此，完成了一个配置变更后推送的流程。</p><h3 id="启动配置监听"><a href="#启动配置监听" class="headerlink" title="启动配置监听"></a>启动配置监听</h3><p>先启动 ConfigServer，再启动 ConfigClient。客户端打印长轮询的日志如下：</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">22:18:09.185 [main] INFO moe.cnkirito.demo.ConfigClient - longPolling dataId: [user] once finished, configInfo is unchanged, longPolling again</span><br><span class="line">22:18:39.197 [main] INFO moe.cnkirito.demo.ConfigClient - longPolling dataId: [user] once finished, configInfo is unchanged, longPolling again</span><br></pre></td></tr></table></figure><p>发布一条配置，<code>curl -X GET &quot;localhost:8080/publishConfig?dataId=user&amp;configInfo=helloworld&quot;</code></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">2021-01-24 22:18:50.801  INFO 73301 --- [nio-8080-exec-6] moe.cnkirito.demo.ConfigServer           : publish configInfo dataId: [user], configInfo: helloworld</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">22:18:50.806 [main] INFO moe.cnkirito.demo.ConfigClient - dataId: [user] changed, receive configInfo: helloworld</span><br></pre></td></tr></table></figure><h2 id="实现细节思考"><a href="#实现细节思考" class="headerlink" title="实现细节思考"></a>实现细节思考</h2><h3 id="为什么需要定时器返回-304"><a href="#为什么需要定时器返回-304" class="headerlink" title="为什么需要定时器返回 304"></a>为什么需要定时器返回 304</h3><p>上述的实现中，服务端采用了一个定时器，在配置未发生变更时，定时返回 304，客户端接收到 304 之后，重新发起长轮询。在前文，已经解释过了为什么需要超时后重新发起长轮询，而不是由服务端一直 hold，直到配置变更再返回，但可能有读者还会有疑问，为什么不由客户端控制超时，服务端去除掉定时器，这样客户端超时后重新发起下一次长轮询，这样的设计不是更简单吗？无论是 Nacos 还是 Apollo 都有这样的定时器，而不是靠客户端控制超时，这样做主要有两点考虑：</p><ul><li>和真正的客户端超时区分开。</li><li>仅仅使用异常（Exception）来表达异常流，而不应该用异常来表达正常的业务流。304 不是超时异常，而是长轮询中配置未变更的一种正常流程，不应该使用超时异常来表达。</li></ul><p>客户端超时需要单独配置，且需要比服务端长轮询的超时要长。正如上述的 demo 中客户端超时设置的是 40s，服务端判断一次长轮询超时是 30s。这两个值在 Nacos 中默认是 30s 和 29.5s，在 Apollo 中默认是是 90s 和 60s。 </p><h3 id="长轮询包含多组-dataId"><a href="#长轮询包含多组-dataId" class="headerlink" title="长轮询包含多组 dataId"></a>长轮询包含多组 dataId</h3><p>在上述的 demo 中，一个 dataId 会发起一次长轮询，在实际配置中心的设计中肯定不能这样设计，一般的优化方式是，一批 dataId 组成一个组批量包含在一个长轮询任务中。在 Nacos 中，按照 3000 个 dataId 为一组包装成一个长轮询任务。</p><h2 id="长轮询和长连接"><a href="#长轮询和长连接" class="headerlink" title="长轮询和长连接"></a>长轮询和长连接</h2><p>讲完实现细节，本文最核心的部分已经介绍完了。再回到最前面提到的数据交互模式上提到的推模型和拉模型，其实在写这篇文章时，我曾经问过交流群中的小伙伴们“配置中心实现动态推送的原理”，他们中绝大多数人认为是长连接的推模型。然而事实上，主流的配置中心几乎都是使用了本文介绍的长轮询方案，这又是为什么呢？</p><p>我也翻阅了不少博客，显然他们给出的理由并不能说服我，我尝试着从自己的角度分析了一下这个既定的事实。</p><ol><li>长轮询实现起来比较容易，完全依赖于 HTTP 便可以实现全部逻辑，而 HTTP 是最能够被大众接受的通信方式。</li><li>长轮询使用 HTTP，便于多语言客户端的编写，大多数语言都有 HTTP 的客户端。</li></ol><p>那么长连接是不是真的就不适合用于配置中心场景呢？有人可能会认为维护一条长连接会消耗大量资源，而长轮询可以提升系统的吞吐量，而在配置中心场景，这一假设并没有实际的压测数据能够论证，benchmark everything！please~</p><p>另外，翻阅了一下 Nacos 2.0 的 milestone，我发现了一个有意思的规划，Nacos 的注册中心（目前是短轮询 + udp 推送）和配置中心（目前是长轮询）都有计划改造为长连接模式。</p><p>再回过头来看，长轮询实现已经将配置中心这个组件支撑的足够好了，替换成长连接，一定需要找到合适的理由才行。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>本文介绍了长轮询、轮询、长连接这几种数据交互模型的差异性。</li><li>分析了 Nacos 和 Apollo 等主流配置中心均是通过长轮询的方式实现配置的实时推送的。实时感知建立在客户端拉的基础上，因为本质上还是通过 HTTP 进行的数据交互，之所以有“推”的感觉，是因为服务端 hold 住了客户端的响应体，并且在配置变更后主动写入了返回 response 对象再进行返回。</li><li>通过一个简单的 demo，实现了长轮询配置实时推送的过程演示，本文的 demo 示例存放在：<a href="https://github.com/lexburner/longPolling-demo" target="_blank" rel="noopener">https://github.com/lexburner/longPolling-demo</a></li></ol>]]></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;传统的静态配置方式想要修改某个配置时，必须重新启动一次应用，如果是数据库连接串的变更，那可能还容易接受一些，但如果变更的是一些运行时实时感知的配置，如某个功能项的开关，重启应用就显得有点大动干戈了。配置中心正是为了解决此类问题应运而生的，特别是在微服务架构体系中，更倾向于使用配置中心来统一管理配置。&lt;/p&gt;
&lt;p&gt;配置中心最核心的能力就是配置的动态推送，常见的配置中心如 Nacos、Apollo 等都实现了这样的能力。在早期接触配置中心时，我就很好奇，配置中心是如何做到服务端感知配置变化实时推送给客户端的，在没有研究过配置中心的实现原理之前，我一度认为配置中心是通过&lt;strong&gt;长连接&lt;/strong&gt;来做到配置推送的。事实上，目前比较流行的两款配置中心：Nacos 和 Apollo 恰恰都没有使用&lt;strong&gt;长连接&lt;/strong&gt;，而是使用的&lt;strong&gt;长轮询&lt;/strong&gt;。本文便是介绍一下长轮询这种听起来好像已经是上个世纪的技术，老戏新唱，看看能不能品出别样的韵味。文中会有代码示例，呈现一个简易的配置监听流程。&lt;/p&gt;
    
    </summary>
    
      <category term="Nacos" scheme="https://lexburner.github.io/categories/Nacos/"/>
    
    
      <category term="Nacos" scheme="https://lexburner.github.io/tags/Nacos/"/>
    
  </entry>
  
  <entry>
    <title>Dubbo 基础教程：使用 Nacos 实现服务注册与发现</title>
    <link href="https://lexburner.github.io/dubbo-nacos-registry/"/>
    <id>https://lexburner.github.io/dubbo-nacos-registry/</id>
    <published>2021-01-16T09:30:22.000Z</published>
    <updated>2021-01-16T18:34:31.211Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是-Nacos"><a href="#什么是-Nacos" class="headerlink" title="什么是 Nacos"></a>什么是 Nacos</h2><p><a href="https://github.com/alibaba/nacos" target="_blank" rel="noopener">Nacos</a> 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集，帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。</p><p>在接下里的教程中，将使用 Nacos 作为微服务架构中的注册中心，替代 ZooKeeper 传统方案。</p><a id="more"></a><h2 id="安装-Nacos"><a href="#安装-Nacos" class="headerlink" title="安装 Nacos"></a>安装 Nacos</h2><p>下载地址：<a href="https://github.com/alibaba/nacos/releases" target="_blank" rel="noopener">https://github.com/alibaba/nacos/releases</a><br>本文版本：<a href="https://github.com/alibaba/nacos/releases/tag/1.4.1" target="_blank" rel="noopener">1.4.1</a></p><p>下载完成之后，解压。根据不同平台，执行不同命令，启动单机版 Nacos 服务：</p><ul><li>Linux/Unix/Mac：<code>sh startup.sh -m standalone</code></li><li>Windows：<code>cmd startup.cmd -m standalone</code></li></ul><blockquote><p><code>startup.sh</code> 脚本位于 Nacos 解压后的 bin 目录下。</p></blockquote><p>启动完成之后，访问：<code>http://127.0.0.1:8848/nacos/</code>，使用默认的用户名和密码：<code>nacos/nacos</code> 可以进入 Nacos 的服务管理页面。</p><p><img src="https://image.cnkirito.cn/image-20210116152123133.png" alt="Nacos 控制台"></p><h2 id="构建-Dubbo-应用接入-Nacos-注册中心"><a href="#构建-Dubbo-应用接入-Nacos-注册中心" class="headerlink" title="构建 Dubbo 应用接入 Nacos 注册中心"></a>构建 Dubbo 应用接入 Nacos 注册中心</h2><p>在完成了 Nacos 安装和启动之后，下面我们就可以编写两个应用（服务提供者与服务消费者）来验证服务的注册与发现了。</p><h3 id="定义接口契约"><a href="#定义接口契约" class="headerlink" title="定义接口契约"></a>定义接口契约</h3><p>第一步：创建一个 maven 项目，命名为：<code>dubbo-nacos-api</code>。</p><figure class="highlight xml"><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="tag">&lt;<span class="name">artifactId</span>&gt;</span>dubbo-nacos-api<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">groupId</span>&gt;</span>moe.cnkirito<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">version</span>&gt;</span>1.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br></pre></td></tr></table></figure><p>第二步：定义服务提供者和服务消费者公用的 Java 接口</p><figure class="highlight java"><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">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">HelloService</span> </span>&#123;</span><br><span class="line">    <span class="function">String <span class="title">hello</span><span class="params">(String name)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Dubbo 的服务提供者和服务消费者一般会共同引用相同的接口，凭借接口达成调用的契约。</p><h3 id="服务提供者"><a href="#服务提供者" class="headerlink" title="服务提供者"></a>服务提供者</h3><p><strong>第一步</strong>：创建一个 Dubbo 应用，命名为：<code>dubbo-nacos-provider</code>。</p><p><strong>第二步</strong>：编辑 <code>pom.xml</code>，加入必要的依赖配置：</p><figure class="highlight xml"><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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>dubbo-nacos-provider<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>moe.cnkirito<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="tag">&lt;<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.0.6.RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-web<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.dubbo<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>dubbo-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.7.8<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.nacos<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>nacos-client<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.4.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>moe.cnkirito<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>dubbo-nacos-api<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br></pre></td></tr></table></figure><ul><li><code>dubbo-spring-boot-starter</code>：Dubbo 应用可以使用 api 配置、xml 配置、SpringBoot 自动配置，推荐使用 dubbo-spring-boot-starter 提供的自动装配机制构建 Dubbo 应用。</li><li><code>nacos-client</code>：Nacos 提供的 Java 客户端，一般需要显式指定版本，推荐使用和 nacos-server 配套的客户端版本，以确保没有兼容性问题</li><li><code>dubbo-nacos-api</code>：接口契约</li></ul><p>第三步：创建应用并定义服务提供者</p><figure class="highlight java"><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="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DubboProvider</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SpringApplication.run(DubboProvider<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><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="meta">@DubboService</span>(version = <span class="string">"1.0.0"</span>, group = <span class="string">"DUBBO"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">HelloServiceImpl</span> <span class="keyword">implements</span> <span class="title">HelloService</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">hello</span><span class="params">(String name)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">"hello "</span> + name;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>内容非常简单，<code>@DubboService</code> 注解是高版本 Dubbo 定义的新注解，用于服务提供者的暴露。一般我们定义 Dubbo 提供者时倾向于明确指定 <code>version</code> 和 <code>group</code>，而不是留空，Dubbo 会根据 <code>interfaceName</code>、<code>version</code>、<code>group</code> 的三元组唯一确定一个服务。</p><p>第四步：配置 Dubbo 服务提供者，定义 <code>application.yaml</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><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></pre></td><td class="code"><pre><span class="line">server:</span><br><span class="line">  port: 8080</span><br><span class="line"></span><br><span class="line">dubbo:</span><br><span class="line">  scan:</span><br><span class="line">    base-packages: moe.cnkirito.demo</span><br><span class="line">  application:</span><br><span class="line">    name: dubbo-nacos-provider</span><br><span class="line">  protocol:</span><br><span class="line">    name: dubbo</span><br><span class="line">    port: 20880</span><br><span class="line">  registry:</span><br><span class="line">    address: nacos://127.0.0.1:8848</span><br><span class="line">  config-center:</span><br><span class="line">    address: nacos://127.0.0.1:8848</span><br><span class="line">  metadata-report:</span><br><span class="line">    address: nacos://127.0.0.1:8848</span><br></pre></td></tr></table></figure><ul><li><code>dubbo.scan.base-packages</code>：配置 @DubboService 等 Dubbo 注解的包扫描路径</li><li><code>dubbo.application.name</code>：Dubbo 的应用名，建议配置，Dubbo 越来越推崇应用级别的服务治理。</li><li><code>dubbo.protocol.name</code> 和 <code>dubbo.protocol.port</code>：Dubbo 的协议配置，默认值为 dubbo 和 20880，这里配置出来主要是为了提醒大家，Dubbo 服务提供者会占用掉 <code>dubbo.protocol.port</code> 配置的端口号，当一个主机上启动多个服务提供者时，除了需要修改 <code>server.port</code> 外还需要修改 <code>dubbo.protocol.port</code> 的值 </li><li><code>dubbo.registry.address</code> 、<code>dubbo.config-center.address</code> 和 <code>dubbo.metadata-report.address</code>：Dubbo 注册中心、配置中心、元数据中心的配置地址，同时指向 Naocs。关于三个中心的介绍可以参考<a href="https://www.cnkirito.moe/dubbo27-features/" target="_blank" rel="noopener">《Dubbo2.7 三大新特性详解》</a>。</li></ul><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">[DUBBO] Register: dubbo://192.168.0.105:20880/moe.cnkirito.api.HelloService?anyhost=true&amp;application=dubbo-nacos-provider&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;group=DUBBO&amp;interface=moe.cnkirito.api.HelloService&amp;metadata-type=remote&amp;methods=hello&amp;pid=3885&amp;release=2.7.8&amp;revision=1.0.0&amp;side=provider&amp;timestamp=1610790598864&amp;version=1.0.0, dubbo version: 2.7.8, current host: 192.168.0.105</span><br></pre></td></tr></table></figure><p>我们可以访问 Nacos 的管理页面 <a href="http://127.0.0.1:8848/nacos/" target="_blank" rel="noopener">http://127.0.0.1:8848/nacos/</a> 来查看服务列表，此时可以看到如下内容：</p><p><img src="https://image.cnkirito.cn/image-20210116175213815.png" alt="服务列表"></p><p>点击详情，可以查看实例级别的信息</p><p><img src="https://image.cnkirito.cn/image-20210116175324746.png" alt="实例列表"></p><h3 id="服务消费者"><a href="#服务消费者" class="headerlink" title="服务消费者"></a>服务消费者</h3><p>接下来实现一个服务消费者来消费上面的服务</p><p><strong>第一步</strong>：创建一个 Dubbo 应用，命名为：<code>dubbo-nacos-consumer</code></p><p>第二步：编辑 pom.xml 中的依赖内容，与上面服务提供者内容一致</p><p><strong>第三步</strong>：创建应用并实现服务消费者</p><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DubboConsumer</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@DubboReference</span>(version = <span class="string">"1.0.0"</span>, group = <span class="string">"DUBBO"</span>)</span><br><span class="line">    <span class="keyword">private</span> HelloService helloService;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SpringApplication.run(DubboConsumer<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RequestMapping</span>(<span class="string">"/hello"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">hello</span><span class="params">(String name)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> helloService.hello(name);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>@DubboReference</code> 与 <code>@DubboService</code> 与成对出现，用于配置服务消费者。需要指定和服务提供者相同的 <code>version</code> 和 <code>group</code>。</p><p>第四步：配置 Dubbo 服务消费者，定义 <code>application.yaml</code>：</p><figure class="highlight yaml"><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="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">8081</span></span><br><span class="line"></span><br><span class="line"><span class="attr">dubbo:</span></span><br><span class="line">  <span class="attr">scan:</span></span><br><span class="line">    <span class="attr">base-packages:</span> <span class="string">moe.cnkirito.demo</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">dubbo-nacos-consumer</span></span><br><span class="line">  <span class="attr">registry:</span></span><br><span class="line">    <span class="attr">address:</span> <span class="string">nacos://127.0.0.1:8848</span></span><br><span class="line">  <span class="attr">config-center:</span></span><br><span class="line">    <span class="attr">address:</span> <span class="string">nacos://127.0.0.1:8848</span></span><br><span class="line">  <span class="attr">metadata-report:</span></span><br><span class="line">    <span class="attr">address:</span> <span class="string">nacos://127.0.0.1:8848</span></span><br></pre></td></tr></table></figure><p>和服务提供者配置的差异主要在于这里不用配置 protocol 暴露端口号了，因为消费者不会占用一个端口。但在实际开发中，一个业务应用往往既是服务提供者又是服务消费者，所以往往都需要配置 protocol。</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">2021-01-16 18:13:04.714  INFO 4811 --- [ncesChangeEvent] o.a.dubbo.registry.nacos.NacosRegistry   :  [DUBBO] Notify urls for subscribe url consumer://192.168.0.105/moe.cnkirito.api.HelloService?application=dubbo-nacos-consumer&amp;category=providers,configurators,routers&amp;dubbo=2.0.2&amp;group=DUBBO&amp;init=false&amp;interface=moe.cnkirito.api.HelloService&amp;metadata-type=remote&amp;methods=hello&amp;pid=4811&amp;qos.enable=false&amp;release=2.7.8&amp;revision=1.0.0&amp;side=consumer&amp;sticky=false&amp;timestamp=1610791940776&amp;version=1.0.0</span><br></pre></td></tr></table></figure><p>我们可以访问 Nacos 的管理页面 <a href="http://127.0.0.1:8848/nacos/" target="_blank" rel="noopener">http://127.0.0.1:8848/nacos/</a> 来查看服务消费者列表，此时可以看到如下内容：</p><p><img src="https://image.cnkirito.cn/image-20210116182820391.png" alt="消费者列表"></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></pre></td><td class="code"><pre><span class="line">$curl &quot;localhost:8081/hello?name=kirito&quot;</span><br><span class="line">hello kirito</span><br></pre></td></tr></table></figure><h2 id="常见错误"><a href="#常见错误" class="headerlink" title="常见错误"></a>常见错误</h2><ol><li><p>Caused by: java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtils</p><p>Dubbo 源码依赖了 common-lang3，如果项目中没有引入过该依赖，需要手动加上该依赖</p><figure class="highlight xml"><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="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.commons<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>commons-lang3<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.9<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></li></ol><p><strong>欢迎关注我的微信公众号：「Kirito 的技术分享」，关于文章的任何疑问都会得到回复，带来更多 Java 相关的技术分享。</strong></p><p><img src="https://image.cnkirito.cn/qrcode_for_gh_c06057be7960_258%20%281%29.jpg" alt="关注微信公众号"></p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;什么是-Nacos&quot;&gt;&lt;a href=&quot;#什么是-Nacos&quot; class=&quot;headerlink&quot; title=&quot;什么是 Nacos&quot;&gt;&lt;/a&gt;什么是 Nacos&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/alibaba/nacos&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Nacos&lt;/a&gt; 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集，帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。&lt;/p&gt;
&lt;p&gt;在接下里的教程中，将使用 Nacos 作为微服务架构中的注册中心，替代 ZooKeeper 传统方案。&lt;/p&gt;
    
    </summary>
    
      <category term="Dubbo" scheme="https://lexburner.github.io/categories/Dubbo/"/>
    
    
      <category term="Dubbo" scheme="https://lexburner.github.io/tags/Dubbo/"/>
    
      <category term="Nacos" scheme="https://lexburner.github.io/tags/Nacos/"/>
    
  </entry>
  
  <entry>
    <title>Service 层需要实现接口吗</title>
    <link href="https://lexburner.github.io/does-service-module-need-interface/"/>
    <id>https://lexburner.github.io/does-service-module-need-interface/</id>
    <published>2021-01-03T01:41:22.000Z</published>
    <updated>2021-01-16T11:25:16.435Z</updated>
    
    <content type="html"><![CDATA[<p>前几天看技术交流群的话题，又刷到了「Service 层和 Dao 层真的有必要每个类都加上接口吗？」这个问题，之前简单回答了一波，给出的观点是「看情况」</p><p>现在结合我参与的项目以及阅读的一些项目源码来看，如果<strong>项目中使用了像 Spring 这样的依赖注入框架，那可以不用接口</strong>！</p><p>先来说说为什么使用了依赖注入框架以后，可以不使用接口。</p><a id="more"></a><p>我整理了支持 Service 层和 Dao 层需要加上接口的理由，总结下来就这么三个：</p><ul><li>可以在尚未实现具体 Service 逻辑的情况下编写上层代码，如 Controller 对 Service 的调用</li><li>Spring 默认是基于动态代理实现 AOP 的，动态代理需要接口</li><li>可以对 Service 进行多实现</li></ul><p>实际上，这三个理由都站不住脚！</p><p>先说说第一个理由：「上层可以在下层逻辑没有实现的情况下进行编码」！很典型的面向接口编程，对层与层之间进行了解耦，看起来好像没有问题。</p><p>这种开发方式适合不同模块之间是由不同的人或项目组开发的，因为沟通的成本比较大。同时避免由于项目组之间开发进度的差异而相互影响。</p><p>不过让我们回想一下，在一般项目开发里面，有多少项目组是按层来切分开发任务的呢？实际上，大部分的项目都是按照功能划分的。即使是现在前后端分离的情况，单纯的后端开发也是按照功能模块进行任务划分，即一个人负责从 Controller 层到 DAO 层的完整逻辑处理。在这种情况下，每一层都先定义一个接口，再去实现逻辑，除了增加了开发人员的工作量（当然，如果代码量计入工作量的话，那开发人员应该也不是太排斥接口的！），实际没有任何用处。</p><p>如果开发人员想在下层逻辑没有完成的情况下，先开发上层逻辑，可以先编写下层类的空方法来先完成上层的逻辑。</p><p>这里推荐一个个人比较喜欢的开发流程，自上向下的编码流程：</p><ul><li>先在 Controller 层编写逻辑，遇到需要委托 Service 调用的地方，直接先写出调用代码。</li><li>优先完成 Controller 层的流程</li><li>然后使用 IDE 的自动补全，对刚才调用下层的代码生成对应的类和方法，在里面添加 TODO</li><li>等所有的类和方法都补全了，再基于 TODO，按照上面的流程去一个个的完善逻辑。</li><li>此方法可以使你对业务流程有比较好的理解。</li></ul><p>对于第二个理由，就完全不成立了。Spring 默认是基于动态代理的，不过通过配置是可以使用 CGLib 来实现 AOP。CGLib 是不需要接口的。</p><p>最后一个理由是「可以对 Service 进行多实现」。这个理由不充分，或者说没有考虑场景。实际上在大多数情况下是不需要多实现，或者说可以使用其它方式替代基于接口的多实现。</p><p>另外，对于很多使用了接口的项目，项目结构也是有待商榷的！下面，我们结合项目结构来说明。</p><p>一般项目结构都是按层来划分的，如下所示：</p><ul><li>Controller</li><li>Service</li><li>Dao</li></ul><p>对于不需要多实现的情况，也就不需要接口了。上面的项目结构即可满足要求。</p><p>对于需要多实现的情况，无论是现在需要，还是后面需要。这种情况下，看起来好像是需要接口。此时的项目结构看起来像这样：</p><ul><li><p>Controller</p></li><li><p>Service</p></li><li><ul><li>— 接口在一个包中</li><li>impl — 实现在另一个包里</li></ul></li><li><p>Dao</p></li></ul><p>对于上面的结构，我们来考虑多实现的情况下，该怎么处理？</p><p>第一种方式，是在 Service 中新增一个包，在里面编写新的逻辑，然后修改配置文件，将新实现作为注入对象。</p><ul><li><p>Controller</p></li><li><p>Service</p></li><li><ul><li>—- 接口在一个包中</li><li>impl —实现在另一个包里</li><li>impl2 —新实现在另一个包里</li></ul></li><li><p>Dao</p></li></ul><p>第二种方式，是新增一个 Service 模块，在里面编写新的逻辑（注意这里的包和原来 Service 的包不能相同，或者包相同，但是类名不同，否则无法创建类。因为在加载时需要同时加载两个 Service 模块，如果包名和类名都相同，两个模块的类全限定名就是一样的了！），然后修改配置文件，将新逻辑作为注入对象。</p><ul><li><p>Controller</p></li><li><p>Service</p></li><li><ul><li>—- 接口在一个包中</li><li>impl —实现在另一个包里</li></ul></li><li><p>Service2</p></li><li><ul><li>impl2 —新实现在另一个包里</li></ul></li><li><p>Dao</p></li></ul><p>相对而言，实际第一种方式相对更简单一点，只需要关注包层面。而第二种方式需要关注模块和包两个层面。另外，实际这两种方式都导致了项目中包含了不需要的逻辑代码。因为老逻辑都会被打进包里。</p><p>不过，从结构上来看，实际方式二的结构要比方式一的结构更清晰，因为从模块上能区分逻辑。</p><p>那有没有办法来结合两者的优点呢？答案是肯定的，而且操作起来也不复杂！</p><p>首先将接口和实现独立开，作为一个独立的模块：</p><ul><li><p>Controller</p></li><li><p>Service — 接口模块</p></li><li><ul><li>ServiceImpl</li><li>impl —实现在另一个包里</li></ul></li><li><p>ServiceImpl2</p></li><li><ul><li>impl2 —新实现在另一个包里</li></ul></li><li><p>Dao</p></li></ul><p>其次，调整打包配置，ServiceImpl 和 ServiceImpl2 二选一。既然 ServiceImpl 和 ServiceImpl2 是二选一，那 ServiceImpl 和ServiceImpl2 的包结构就可以相同。包结构相同了，那调整了依赖以后，依赖注入相关的配置就不需要调整了。调整后，项目结构看起来像这样：</p><ul><li><p>Controller</p></li><li><p>Service — 接口模块</p></li><li><ul><li>ServiceImpl</li><li>impl —实现在另一个包</li></ul></li><li><p>ServiceImpl2</p></li><li><ul><li>impl —新实现和老实现在相同的包中</li></ul></li><li><p>Dao</p></li></ul><p>现在，ServiceImpl 和 ServiceImpl2 模块中的包结构、类名都是一样的。那我们还需要接口模块吗？</p><p>假设，我们把Service接口模块去掉，结构变成了如下所示：</p><ul><li>Controller</li><li>Service1 — 老实现</li><li>Service2 — 新实现</li><li>Dao</li></ul><p>单纯的通过调整模块依赖，是否能实现 Service 的多实现？答案显而易见吧？</p><p>上面给出了不使用接口的理由。不过不使用接口并不是完全没有缺点的，主要问题就是在进行多实现的时候，没有一个强接口规范。即不能通过实现接口，借助 IDE 快速生成框架代码。对于没有实现的接口，IDE 也能给出错误提醒。</p><p>一个不太优雅的解决是，将原来的模块里的代码拷贝一份到新模块中，基于老代码来实现新的逻辑。</p><p>所以，如果一个项目需要多实现、且多实现数量较多（不过一般项目不会有多个实现的），则推荐使用接口。否则不需要使用接口。</p><p>本文针对「Service 层是否需要接口」这个问题，指出需要接口的理由的问题。以及个人对这个问题的观点，希望在评论区写出自己的理解 ！</p><blockquote><p>链接：<a href="https://urlify.cn/Vjua2e" target="_blank" rel="noopener">https://urlify.cn/Vjua2e</a></p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;前几天看技术交流群的话题，又刷到了「Service 层和 Dao 层真的有必要每个类都加上接口吗？」这个问题，之前简单回答了一波，给出的观点是「看情况」&lt;/p&gt;
&lt;p&gt;现在结合我参与的项目以及阅读的一些项目源码来看，如果&lt;strong&gt;项目中使用了像 Spring 这样的依赖注入框架，那可以不用接口&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;先来说说为什么使用了依赖注入框架以后，可以不使用接口。&lt;/p&gt;
    
    </summary>
    
      <category term="技术杂谈" scheme="https://lexburner.github.io/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    
    
      <category term="技术杂谈" scheme="https://lexburner.github.io/tags/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    
  </entry>
  
  <entry>
    <title>Spring Cloud 终于改了，为什么要用日期来做版本号？</title>
    <link href="https://lexburner.github.io/calendar-versioning/"/>
    <id>https://lexburner.github.io/calendar-versioning/</id>
    <published>2020-12-22T20:01:22.000Z</published>
    <updated>2021-01-16T11:25:16.426Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Spring-Cloud-终于改了"><a href="#Spring-Cloud-终于改了" class="headerlink" title="Spring Cloud 终于改了"></a>Spring Cloud 终于改了</h2><p>最近 Spring Cloud 把版本号从 A 到 Z 的伦敦地铁站，改成用日期命名了。</p><ul><li><a href="https://spring.io/blog/2020/04/17/spring-cloud-2020-0-0-m1-released" target="_blank" rel="noopener">https://spring.io/blog/2020/04/17/spring-cloud-2020-0-0-m1-released</a></li></ul><p>也就是从 <code>Greenwich.SR6</code>, <code>Hoxton.SR9</code> 这样的风格改成了 <code>2020.0.0</code> 的形式。广大人民终于不用为 Spring Cloud 的版本号烦恼了。</p><p>Spring Cloud 推广不力，固然有自身复杂的原因，版本号太复杂也是一个坑。</p><p>以日期为版本号，即所谓的 <code>Calendar Versioning</code>，可以参考这个网站：</p><ul><li><a href="https://calver.org/overview_zhcn.html" target="_blank" rel="noopener">https://calver.org/overview_zhcn.html</a></li></ul><a id="more"></a><h2 id="何时使用-CalVer"><a href="#何时使用-CalVer" class="headerlink" title="何时使用 CalVer"></a>何时使用 CalVer</h2><p>如果你和很多素不相识的人协同开发某个项目，那么使用一个严谨的版本命名方式是一个合适的选择，恰巧 CalVer 就是选择之一。</p><ul><li><p>该项目是否具有较大或不断变化的范围？</p></li><li><ul><li>大型系统和框架，如 Ubuntu 和 Twisted。</li><li>没有实际边界的实用工具集合，如 Boltons。</li></ul></li><li><p>该项目是否对时间敏感？是否有其他的外部变化驱动项目新版本的发布？</p></li><li><ul><li>业务需求，例如 Ubuntu 的支持计划。</li><li>安全更新，例如 certifi 对证书更新的需求。</li><li>政治变化，例如 pytz 对时区变化的处理。</li></ul></li></ul><p>如果你对这些问题中的任何一个回答是肯定的，CalVer 都可以成为你项目的有力选择。</p><p>但上面这些理由我觉得都不够充分。</p><p><strong>在我看来最重要的理由是：以日期为版本号，让依赖库的开发方和下游依赖方达成了默契。</strong></p><h2 id="阿里巴巴的实践"><a href="#阿里巴巴的实践" class="headerlink" title="阿里巴巴的实践"></a>阿里巴巴的实践</h2><p>Pandora 是阿里巴巴内部的隔离容器。在 14 年时，Pandora 包版本号是这样子的：</p><ul><li>2_1_0_3 , 2_1_0_4_10-LOG</li></ul><p>后面改为 Pandora 版本 + 日期</p><ul><li>2_2_140825, 2_2_140905</li></ul><p>但实际上应用方并不关心 Pandora 的版本，所以改成了现在的风格：</p><ul><li>2020-04-release-fix , 2020-10-release</li></ul><p>好处是：</p><ol><li><p>按时间节点推动升级</p><p>电商的业务都是时间为关键节点的，比如 618/双 11。中间件和应用方达成了一个默契：到关键时间点，业务方使用中间件推出的稳定版本，如果出了事故那么就是中间件的锅。不升级，则是业务方自己的锅。</p></li><li><p>推动升级的阻力变小</p><p>当业务方遇到问题时，一看版本号是  1 年多前的，很自然就会想到升级。</p></li><li><p>依赖提供方要按时间保持更新</p><p>维护人员本身要不断发版本证明自己的生命力。下游用户也可以根据时间选择是否要切换到其它的新技术路线上去了。</p></li></ol><p>对于一些总体的依赖，比如公司内部的 maven bom，都建议使用时间做日期。</p><p>比如 Spring 2.5.6 版本，大部分开发都知道它是比较旧的依赖，但不会有太大的动力去管。</p><p>但是如果你说，这是 12 年前的代码（绝大部分开发还没毕业），那么开发人员就知道很容易会出现不兼容的问题，他自己就知道应该要升级了。</p><p><strong>以时间为版本号，既是对用户的承诺，也是对开发者自己的鞭策。</strong></p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;Spring-Cloud-终于改了&quot;&gt;&lt;a href=&quot;#Spring-Cloud-终于改了&quot; class=&quot;headerlink&quot; title=&quot;Spring Cloud 终于改了&quot;&gt;&lt;/a&gt;Spring Cloud 终于改了&lt;/h2&gt;&lt;p&gt;最近 Spring Cloud 把版本号从 A 到 Z 的伦敦地铁站，改成用日期命名了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/blog/2020/04/17/spring-cloud-2020-0-0-m1-released&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://spring.io/blog/2020/04/17/spring-cloud-2020-0-0-m1-released&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是从 &lt;code&gt;Greenwich.SR6&lt;/code&gt;, &lt;code&gt;Hoxton.SR9&lt;/code&gt; 这样的风格改成了 &lt;code&gt;2020.0.0&lt;/code&gt; 的形式。广大人民终于不用为 Spring Cloud 的版本号烦恼了。&lt;/p&gt;
&lt;p&gt;Spring Cloud 推广不力，固然有自身复杂的原因，版本号太复杂也是一个坑。&lt;/p&gt;
&lt;p&gt;以日期为版本号，即所谓的 &lt;code&gt;Calendar Versioning&lt;/code&gt;，可以参考这个网站：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://calver.org/overview_zhcn.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://calver.org/overview_zhcn.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
    </summary>
    
      <category term="Spring Cloud" scheme="https://lexburner.github.io/categories/Spring-Cloud/"/>
    
    
      <category term="Spring Cloud" scheme="https://lexburner.github.io/tags/Spring-Cloud/"/>
    
  </entry>
  
  <entry>
    <title>Nacos 集群部署模式最佳实践</title>
    <link href="https://lexburner.github.io/nacos-cluster-mode/"/>
    <id>https://lexburner.github.io/nacos-cluster-mode/</id>
    <published>2020-12-22T20:01:22.000Z</published>
    <updated>2021-01-16T11:25:16.479Z</updated>
    
    <content type="html"><![CDATA[<h2 id="1-前言"><a href="#1-前言" class="headerlink" title="1 前言"></a>1 前言</h2><p>Nacos 支持两种部署模式：单机模式和集群模式。在实践中，我们往往习惯用单机模式快速构建一个 Nacos 开发/测试环境，而在生产中，出于高可用的考虑，一定需要使用 Nacos 集群部署模式。我的上一篇文章《一文详解 Nacos 高可用特性》提到了 Nacos 为高可用做了非常多的特性支持，而这些高可用特性大多数都依赖于集群部署模式。这篇模式文章便是给大家介绍一下，在实践中可以被采用的几种集群部署模式，无论你是希望自行搭建 Nacos，还是希望对 MSE 商业版 Nacos 有一个更加深刻的理解，我都很乐意跟你分享下面的内容。</p><p>由于篇幅限制，本文不会介绍如何将一个多节点的 Nacos 集群启动起来，主要介绍的是一个多节点的 Nacos 集群启动之后，我们的应用如何很好地连接到 Nacos 集群，即客户端视角。这中间我们会引入一些其他组件以解决一些问题，本文标题也可以叫做《Nacos 接入点最佳实践》。我将会介绍以下三种方案：直连模式、 VIP 模式和地址服务器模式，并对它们进行对比。</p><a id="more"></a><h2 id="2-直连模式"><a href="#2-直连模式" class="headerlink" title="2 直连模式"></a>2 直连模式</h2><p>直连模式是部署上最简单，也是最容易理解的一种模式</p><p><img src="http://image.cnkirito.cn/image-20201224024616439.png" alt="直连模式"></p><p>采用直连模式后，典型的开发场景配置如下：</p><h3 id="nacos-client-配置"><a href="#nacos-client-配置" class="headerlink" title="nacos-client 配置"></a>nacos-client 配置</h3><figure class="highlight java"><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">Properties properties = <span class="keyword">new</span> Properties();</span><br><span class="line">properties.setProperty(PropertyKeyConst.SERVER_ADDR, <span class="string">"192.168.0.1:8848,192.168.0.2:8848,192.168.0.3:8848"</span>);</span><br><span class="line">NamingService namingService = NacosFactory.createNamingService(properties);</span><br></pre></td></tr></table></figure><p>注意这里的 PropertyKeyConst.SERVER_ADDR 的字面量是：<code>serverAddr</code></p><h3 id="Dubbo-配置"><a href="#Dubbo-配置" class="headerlink" title="Dubbo 配置"></a>Dubbo 配置</h3><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">dubbo.registry.address=192.168.0.1:8848,192.168.0.2:8848,192.168.0.3:8848</span><br><span class="line">dubbo.registry.protocol=nacos</span><br></pre></td></tr></table></figure><p>如果有一天，Nacos 的 IP 变了，例如扩缩容，机器置换，集群迁移等场景，所有的应用都需要修改，这样的方式并不灵活。所以这种模式并不是生产推荐的模式。</p><h3 id="模式分析"><a href="#模式分析" class="headerlink" title="模式分析"></a>模式分析</h3><ul><li>高可用性。集群本身的扩缩容必须要改动业务代码才能被感知到，出现节点故障需要紧急下线、紧急扩容等场景，让业务修改代码是不现实的，不符合高可用的原则。</li><li>可伸缩性。同上，可伸缩性不友好。</li><li>架构简单，用户理解成本低</li><li>没有引入额外的组件，没有新增组件的运维成本</li></ul><h2 id="3-VIP-模式"><a href="#3-VIP-模式" class="headerlink" title="3 VIP 模式"></a>3 VIP 模式</h2><p>VIP（Virtual IP） 模式可以很好的解决直连模式 IP 变化所带来的应用批量修改的问题。什么是 VIP 呢？</p><p><img src="http://image.cnkirito.cn/1567916375212-f3fd5df3-1cc6-4304-aaee-c7bb564e3b79.png" alt="VIP"></p><ul><li>Real Server：处理实际请求的后端服务器节点。</li><li>Director Server：指的是负载均衡器节点，负责接收客户端请求，并转发给 RS。</li><li><strong>VIP：Virtual IP，DS 用于和客户端通信的 IP 地址，作为客户端请求的目标 IP 地址。</strong></li><li>DIP：Directors IP，DS 用于和内部 RS 通信的 IP 地址。</li><li>RIP：Real IP，后端服务器的 IP 地址。</li><li>CIP：Client IP，客户端的 IP 地址。</li></ul><p>我这里介绍时并没有用【负载均衡模式】，而是用了【VIP 模式】，主要是为了跟 Nacos 官方文档保持一致。事实上，VIP 的叫法在阿里内部比较流行，所以在开源 Nacos 时也被习惯性的带了出去。</p><p><img src="http://image.cnkirito.cn/image-20201224025005872.png" alt="VIP 模式"></p><p>VIP 帮助 Nacos Client 屏蔽了后端 RIP，相对于 RIP 而言，VIP 很少会发生变化。以扩容场景为例，只需要让 VIP 感知到即可，Nacos Client 只需要关注 VIP，避免了扩容引起的代码改造。</p><p>只要是具备负载均衡能力的组件，均可以实现 VIP 模式，例如开源的 Nginx 以及阿里云负载均衡 SLB。</p><p>采用 VIP 模式后，代码不需要感知 RIP，典型的开发场景配置如下：</p><h3 id="nacos-client-配置-1"><a href="#nacos-client-配置-1" class="headerlink" title="nacos-client 配置"></a>nacos-client 配置</h3><figure class="highlight java"><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">Properties properties = <span class="keyword">new</span> Properties();</span><br><span class="line">properties.setProperty(PropertyKeyConst.SERVER_ADDR, <span class="string">"&#123;VIP&#125;:8848"</span>);</span><br><span class="line">NamingService namingService = NacosFactory.createNamingService(properties);</span><br></pre></td></tr></table></figure><h3 id="Dubbo-配置-1"><a href="#Dubbo-配置-1" class="headerlink" title="Dubbo 配置"></a>Dubbo 配置</h3><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">dubbo.registry.address=&#123;VIP&#125;:8848</span><br><span class="line">dubbo.registry.protocol=nacos</span><br></pre></td></tr></table></figure><h3 id="域名配置"><a href="#域名配置" class="headerlink" title="域名配置"></a>域名配置</h3><p>VIP 模式和直连模式都不具备可读性，所以在实际生产中，往往还会给 VIP 挂载一个域名。</p><p>域名背后甚至可以挂载 2 个 VIP 用作高可用，路由到想同的 rs；同时域名的存在也让 VIP 的置换变得更加灵活，当其中一台出现问题后，域名的 DNS 解析只会路由到另外一个正常的 VIP 上，为平滑置换预留了足够的余地。</p><blockquote><p>tips：一个域名可以绑定多个 A 记录，一个 A 记录对应一个 IPv4 类型的 VIP，DNS 域名服务器了对多个 A 记录会有负载均衡策略和健康检查机制</p></blockquote><p>VIP 模式的最终生产高可用版架构便产生了：</p><p><img src="http://image.cnkirito.cn/image-20201225013353540.png" alt="域名 VIP 模式"></p><p>典型的开发场景配置只需要将 VIP 替换为域名即可</p><h3 id="nacos-client-配置-2"><a href="#nacos-client-配置-2" class="headerlink" title="nacos-client 配置"></a>nacos-client 配置</h3><figure class="highlight java"><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">Properties properties = <span class="keyword">new</span> Properties();</span><br><span class="line">properties.setProperty(PropertyKeyConst.SERVER_ADDR, <span class="string">"mse-abc123qwe-nacos.mse.aliyuncs.com:8848"</span>);</span><br><span class="line">NamingService namingService = NacosFactory.createNamingService(properties);</span><br></pre></td></tr></table></figure><h3 id="Dubbo-配置-2"><a href="#Dubbo-配置-2" class="headerlink" title="Dubbo 配置"></a>Dubbo 配置</h3><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">dubbo.registry.address=mse-abc123qwe-nacos.mse.aliyuncs.com:8848</span><br><span class="line">dubbo.registry.protocol=nacos</span><br></pre></td></tr></table></figure><h3 id="模式分析-1"><a href="#模式分析-1" class="headerlink" title="模式分析"></a>模式分析</h3><ul><li>高可用性。域名的可用性需要由 DNS 域名服务器负责，可用性保障较高；VIP 需要由高可用的负责均衡组件支持，且流量经过负载均衡转发，对 VIP 的实现有较高可用性的要求。</li><li>可伸缩性。水平扩缩容时，只需要让 VIP 感知即可，可伸缩性好。</li><li>依赖了域名解析系统和负载均衡系统，生产部署时，需要有配套设施的支持。</li></ul><h2 id="4-地址服务器模式"><a href="#4-地址服务器模式" class="headerlink" title="4 地址服务器模式"></a>4 地址服务器模式</h2><h3 id="地址服务器介绍"><a href="#地址服务器介绍" class="headerlink" title="地址服务器介绍"></a>地址服务器介绍</h3><p>说起地址服务器，可能大家对这个词会感到陌生，因为地址服务器的概念主要在阿里内部比较普及，也是阿里中间件使用的最广的一种地址寻址模式。但是在开源领域，鲜有人会提及，但对于 Nacos 部署模式而言，地址服务器模式是除了 VIP 模式之外，另外一个生产可用的推荐部署方式。</p><p>地址服务器是什么？顾名思义，是用来寻址地址的服务器，发送一个请求，返回一串地址列表。尽管在阿里内部使用的真实地址服务器比这复杂一些，但下图这个简单交互逻辑，几乎涵盖了地址服务器 90% 的内容。</p><p><img src="http://image.cnkirito.cn/image-20201225015919479.png" alt="地址服务器原理"></p><p>实现一个简易版本的地址服务器并不困难，推荐使用 nginx 搭建一个静态文件服务器管理地址， 当然你可以使用 Java！</p><figure class="highlight java"><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="meta">@Controller</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AddressServerController</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RequestMapping</span>(<span class="string">"/nacos/serverlist"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> ResponseEntity&lt;String&gt; <span class="title">serverlist</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> ResponseEntity.ok().</span><br><span class="line">            header(<span class="string">"Content-Type"</span>, <span class="string">"text/plain"</span>).</span><br><span class="line">            body(<span class="string">"192.168.0.1:8848\r\n"</span> +</span><br><span class="line">                    <span class="string">"192.168.0.2:8848\r\n"</span> +</span><br><span class="line">                    <span class="string">"192.168.0.3:8848\r\n"</span></span><br><span class="line">            );</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用地址服务器可以完成集群地址和客户端配置的解耦，解决直连模式中无法动态感知集群节点变化的问题。客户端根据地址服务器返回的列表，随后采取直连模式连接；并且在客户端启动后，会启动一个定时器，轮询感知 AddressServer 的变化，进而及时更新地址列表。</p><p>并且地址服务器建议配置域名，增加可读性。所以最后的部署交互架构是这样的：</p><p><img src="http://image.cnkirito.cn/image-20201225025419171.png" alt="地址服务器部署架构"></p><p>熟悉 RPC 的朋友看到这里应该能够很好地对 VIP 模式和地址服务器模式做一个类比。</p><ul><li>VIP 模式是 DNS 类的服务端负载均衡技术</li><li>地址服务器是类似服务发现机制的客户端负载均衡技术</li></ul><p>nacos-client 的源码专门适配了地址服务器模式，我们只需要配置好 addressServer 的 endpoint 即可</p><h3 id="nacos-client-配置-3"><a href="#nacos-client-配置-3" class="headerlink" title="nacos-client 配置"></a>nacos-client 配置</h3><figure class="highlight java"><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">Properties properties = <span class="keyword">new</span> Properties();</span><br><span class="line">properties.setProperty(PropertyKeyConst.ENDPOINT, <span class="string">"&#123;addressServerDomain&#125;"</span>);</span><br><span class="line">properties.setProperty(PropertyKeyConst.ENDPOINT_PORT, <span class="string">"8080"</span>);</span><br><span class="line">NamingService namingService = NacosFactory.createNamingService(properties);</span><br></pre></td></tr></table></figure><p>注意，这里 PropertyKeyConst.ENDPOINT 的字面量是：<code>endpoint</code> ，配置的是地址服务器的地址。</p><h3 id="Dubbo-配置-3"><a href="#Dubbo-配置-3" class="headerlink" title="Dubbo 配置"></a>Dubbo 配置</h3><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">dubbo.registry.address=0.0.0.0?endpoint=127.0.0.1&amp;endpointPort=8080</span><br><span class="line">dubbo.registry.protocol=nacos</span><br></pre></td></tr></table></figure><p>dubbo.registry.address 的 url 可以任意填写，因为当 serverAddr 和 endpoint 同时存在时，默认是优先从地址服务器去选址的。</p><p>此时，只需要把真实的 Nacos Server IP 配置到地址服务器中即可。</p><blockquote><p>Dubbo 通过 url 的 kv 属性将值透传给 Nacos 创建 Nacos-Client。Dubbo + Nacos 使用地址服务器模式时，建议 Dubbo 版本 &gt;= 2.7.4，nacos-client 版本 &gt;= 1.0.1</p></blockquote><h3 id="模式分析-2"><a href="#模式分析-2" class="headerlink" title="模式分析"></a>模式分析</h3><ul><li>高可用性。域名的可用性需要由 DNS 域名服务器负责，可用性保障较高；地址服务器的职责单一，有较高的可用性；运行时 Client 直连 Nacos Server 节点，可用性靠 nacos-sdk 保障。</li><li>可伸缩性。水平扩缩容时，只需要让地址服务器感知即可，可伸缩性好。</li><li>依赖了域名解析系统和地址服务器，生产部署时，需要有配套设施的支持。</li></ul><h2 id="5-部署模式对比"><a href="#5-部署模式对比" class="headerlink" title="5 部署模式对比"></a>5 部署模式对比</h2><table><thead><tr><th></th><th>直连模式</th><th>VIP 模式</th><th>地址服务器模式</th></tr></thead><tbody><tr><td>转发模式</td><td>直连</td><td>代理（网络多一跳）</td><td>直连</td></tr><tr><td>高可用</td><td>弱，代码配置不灵活，节点故障时无法批量变更</td><td>强</td><td>强</td></tr><tr><td>可伸缩性</td><td>弱</td><td>强</td><td>强</td></tr><tr><td>部署成本</td><td>无</td><td>负载均衡组件运维成本高</td><td>地址服务器运维成本低</td></tr><tr><td>负载均衡模式</td><td>nacos-sdk 客户端负载均衡</td><td>负载均衡组件提供负载均衡能力</td><td>nacos-sdk 客户端负载均衡</td></tr><tr><td>开源接受度</td><td>高</td><td>高</td><td>低，地址服务器模式在开源领域不太普遍</td></tr><tr><td>企业级能力</td><td>不方便</td><td>灵活</td><td>灵活</td></tr><tr><td>跨网络</td><td>内网环境，平坦网络</td><td>VIP 模式灵活地支持反向代理、安全组、ACL 等特性，可以很好的工作在内/外网环境中，使得应用服务器和 Nacos Server 可以部署在不同的网络环境中，借助 VIP 打通</td><td>内网环境，平坦网络</td></tr><tr><td>推荐使用环境</td><td>开发测试环境</td><td>生产环境，云环境</td><td>生产环境</td></tr></tbody></table><p>Nacos 这款开源产品很好地支持了地址服务器这种模式，所以无论是大、中、小型公司在自建 Nacos 时，都可以选择地址服务器模式去构建生产高可用的 Nacos 集群，地址服务器组件相对而言维护简单，Nginx，Java 构建的 Web 服务器均可以轻松实现一个地址服务器。使用地址服务器后，nacos-client 与 nacos-server 之间仍然是直连访问，所以可以很好的运作在平坦网络下。</p><p>VIP 模式同样推荐在自建场景使用，但运维成本相对地址服务器还是要高一些，可以根据自己公司的运维体系评估。经过了 VIP 的转发，有利有弊。弊端比较明显，网络多了一跳，对于内网环境这样的平坦网络而言，是不必要的；优势也同样明显，大公司往往环境比较复杂，数据中心之间有网络隔离，应用和中间件可能部署在不同的网络环境中，借助于 VIP 可以很好地做网络打通，并且基于 VIP 可以很好实现安全组、ACL 等特性，更符合企业级诉求。</p><p>当然，组合使用地址服务器 + VIP 也是可以的，可以充分的融合两者的优势：</p><p><img src="http://image.cnkirito.cn/image-20201225133001525.png" alt="组合模式"></p><h2 id="6-MSE-Nacos-的实践"><a href="#6-MSE-Nacos-的实践" class="headerlink" title="6 MSE Nacos 的实践"></a>6 MSE Nacos 的实践</h2><p>上述场景主要介绍了三种模式的具体部署方案，以及自建 Nacos 场景如何做到高可用，最后要介绍的是阿里云环境 MSE 是如何部署的。</p><p>MSE（微服务引擎）提供了 Nacos 注册中心中心的全托管能力，除了要做上述提到的高可用、可伸缩、易用性，还要考虑以下的因素：</p><ul><li>开源接受度。避免给用户带来太多理解成本，尽量做到对标开源，这样用户接受度才会高。</li><li>网络隔离。MSE 提供的是 BaaS 化的能力，Nacos Server 部署在云产品 VPC，与用户 VPC 是隔离的，需要解决网络隔离问题。</li><li>网络安全。MSE Nacos 是独享模式，网络上租户隔离是最基本的要求。除此之外企业级用户会对 MSE Nacos 提出安全组/ACL 控制的诉求，这些都需要考量。</li></ul><p>综上，MSE Nacos 最终采用的是域名 + SLB 的 VIP 模式。</p><p><img src="http://image.cnkirito.cn/image-20201225135839176.png" alt="MSE 部署模式"></p><p>MSE Nacos 提供两个域名，其中公网域名可以用做本地开发测试，或者自建环境、混合云等场景的接入点，内网域名用做阿里云生产环境接入点。公网域名有带宽限制，需要在集群创建时根据场景选择合适的带宽，而内网域名则没有带宽限制。公网域名请注意添加 IP 访问白名单。</p><h2 id="7-总结"><a href="#7-总结" class="headerlink" title="7 总结"></a>7 总结</h2><p>本文介绍了 Nacos 的三种部署模式，并就高可用、可伸缩、易用性等方面对各个模式进行了介绍，并对自建 Nacos 场景的部署选型进行了分析，同时介绍了 MSE Nacos 企业版的部署架构，对云环境部署 Nacos 进行了补充。</p><p>文章提及的三种模式其实也都是中间件组件常见的部署模式，不仅仅 Nacos，例如 Redis、DB 等场景，同样有参考价值。</p><p>本文提及了地址服务器这个可能在开源领域不太常见的组件，在阿里内部则用的非常普遍。</p><p>另外，Nacos 本身也提供 addressServer 模块，出于篇幅考虑没有在本文中提及，后续我会单独整理一篇文章介绍，感兴趣的同学可以自行参考 Nacos 官方文档和官方博客中的内容。</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;1-前言&quot;&gt;&lt;a href=&quot;#1-前言&quot; class=&quot;headerlink&quot; title=&quot;1 前言&quot;&gt;&lt;/a&gt;1 前言&lt;/h2&gt;&lt;p&gt;Nacos 支持两种部署模式：单机模式和集群模式。在实践中，我们往往习惯用单机模式快速构建一个 Nacos 开发/测试环境，而在生产中，出于高可用的考虑，一定需要使用 Nacos 集群部署模式。我的上一篇文章《一文详解 Nacos 高可用特性》提到了 Nacos 为高可用做了非常多的特性支持，而这些高可用特性大多数都依赖于集群部署模式。这篇模式文章便是给大家介绍一下，在实践中可以被采用的几种集群部署模式，无论你是希望自行搭建 Nacos，还是希望对 MSE 商业版 Nacos 有一个更加深刻的理解，我都很乐意跟你分享下面的内容。&lt;/p&gt;
&lt;p&gt;由于篇幅限制，本文不会介绍如何将一个多节点的 Nacos 集群启动起来，主要介绍的是一个多节点的 Nacos 集群启动之后，我们的应用如何很好地连接到 Nacos 集群，即客户端视角。这中间我们会引入一些其他组件以解决一些问题，本文标题也可以叫做《Nacos 接入点最佳实践》。我将会介绍以下三种方案：直连模式、 VIP 模式和地址服务器模式，并对它们进行对比。&lt;/p&gt;
    
    </summary>
    
      <category term="Nacos" scheme="https://lexburner.github.io/categories/Nacos/"/>
    
    
      <category term="Nacos" scheme="https://lexburner.github.io/tags/Nacos/"/>
    
  </entry>
  
  <entry>
    <title>一文详解 Nacos 高可用特性</title>
    <link href="https://lexburner.github.io/nacos-high-available/"/>
    <id>https://lexburner.github.io/nacos-high-available/</id>
    <published>2020-12-18T07:38:25.000Z</published>
    <updated>2021-01-16T11:25:16.480Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>服务注册发现是一个经久不衰的话题，Dubbo 早期开源时默认的注册中心 Zookeeper 最早进入人们的视线，并且在很长一段时间里，人们将注册中心和 Zookeeper 划上了等号，可能 Zookeeper 的设计者都没有想到这款产品对微服务领域造成了如此深厚的影响，直到 SpringCloud 开始流行，其自带的 Eureka 进入了人们的视野，人们这才意识到原来注册中心还可以有其他的选择。再到后来，热衷于开源的阿里把目光也聚焦在了注册中心这个领域，Nacos 横空出世。</p><p><img src="http://image.cnkirito.cn/image-20201220140934757.png" alt="注册中心"></p><p>Kirito 在做注册中心选型时的思考：曾经我没得选，现在我只想选择一个好的注册中心，它最好是开源的，这样开放透明，有自我的掌控力；不仅要开源，它还要有活跃的社区，以确保特性演进能够满足日益增长的业务需求，出现问题也能及时修复；最好…它的功能还要很强大，除了满足注册服务、推送服务外，还要有完善的微服务体系中所需的功能；最重要的，它还要稳定，最好有大厂的实际使用场景背书，证明这是一个经得起实战考验的产品；当然，云原生特性，安全特性也是很重要的…</p><p>似乎 Kirito 对注册中心的要求实在是太高了，但这些五花八门的注册中心呈现在用户眼前，总是免不了一番比较。正如上面所言，功能特性、成熟度、可用性、用户体验度、云原生特性、安全都是可以拉出来做比较的话题。今天这篇文章重点介绍的是 Nacos 在可用性上体现，希望借助于这篇文章，能够让你对 Nacos 有一个更加深刻的认识。</p><a id="more"></a><h2 id="高可用介绍"><a href="#高可用介绍" class="headerlink" title="高可用介绍"></a>高可用介绍</h2><p>当我们在聊高可用时，我们在聊什么？</p><ul><li>系统可用性达到 99.99%</li><li>在分布式系统中，部分节点宕机，依旧不影响系统整体运行</li><li>服务端集群化部署多个节点</li></ul><p>这些都可以认为是高可用，而我今天介绍的 Nacos 高可用，则是一些 Nacos 为了提升系统稳定性而采取的一系列手段。Nacos 的高可用不仅仅存在于服务端，同时它也存在于客户端，以及一些与可用性相关的功能特性中。这些点组装起来，共同构成了 Nacos 的高可用。</p><h2 id="客户端重试"><a href="#客户端重试" class="headerlink" title="客户端重试"></a>客户端重试</h2><p>先统一一下语义，在微服务架构中一般会有三个角色：Consumer、Provider 和 Registry，在今天注册中心的主题中，Registry 是 nacos-server，而 Consumer 和 Provider 都是 nacos-client。</p><p>在生产环境，我们往往需要搭建 Nacos 集群，在 Dubbo 也需要显式地配置上集群地址：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dubbo:registry</span> <span class="attr">protocol</span>=<span class="string">"nacos"</span> <span class="attr">address</span>=<span class="string">"192.168.0.1:8848,192.168.0.2:8848,192.168.0.3:8848"</span>/&gt;</span></span><br></pre></td></tr></table></figure><p>当其中一台机器宕机时，为了不影响整体运行，客户端会存在重试机制</p><p><img src="http://image.cnkirito.cn/image-20201220151901675.png" alt="轮询 server"></p><p>逻辑非常简单，拿到地址列表，在请求成功之前逐个尝试，直到成功为止。</p><p>该可用性保证存在于 nacos-client 端。</p><h2 id="一致性协议-distro"><a href="#一致性协议-distro" class="headerlink" title="一致性协议 distro"></a>一致性协议 distro</h2><p>首先给各位读者打个强心剂，不用看到”一致性协议“这几个字就被劝退，本节不会探讨一致性协议的实现过程，而是重点介绍其余高可用相关的特性。有的文章介绍 Nacos 的一致性模型是 AP + CP，这么说很容易让人误解，其实 Nacos 并不是支持两种一致性模型，也并不是支持两种模型的切换，介绍一致性模型之前，需要先了解到 Nacos 中的两个概念：临时服务和持久化服务。</p><ul><li>临时服务（Ephemeral）：临时服务健康检查失败后会从列表中删除，常用于服务注册发现场景。</li><li>持久化服务（Persistent）：持久化服务健康检查失败后会被标记成不健康，常用于 DNS 场景。</li></ul><p>临时服务使用的是 Nacos 为服务注册发现场景定制化的私有协议 distro，其一致性模型是 AP；而持久化服务使用的是 raft 协议，其一致性模型是 CP。所以以后不要再说 Nacos 是 AP + CP 了，更建议加上服务节点状态或者使用场景的约束。</p><p>distro 协议与高可用有什么关系呢？上一节我们提到 nacos-server 节点宕机后，客户端会重试，但少了一个前提，即 nacos-server 少了一个节点后依旧可以正常工作。Nacos 这种有状态的应用和一般无状态的 Web 应用不同，并不是说只要存活一个节点就可以对外提供服务的，需要分 case 讨论，这与其一致性协议的设计有关。distro 协议的工作流程如下：</p><ul><li>Nacos 启动时首先从其他远程节点同步全部数据</li><li>Nacos 每个节点是平等的都可以处理写入请求，同时把新数据同步到其他节点</li><li>每个节点只负责部分数据，定时发送自己负责数据校验值到其他节点来保持数据一致性</li></ul><p><img src="https://image.cnkirito.cn/image-20201220204422195.png" alt="image-20201220204422195"></p><p>如上图所示，每个节点服务一部分服务的写入，但每个节点都可以接收到写入请求，这时就存在两种写情况：</p><ol><li>当该节点接收到属于该节点负责的服务时，直接写入。</li><li>当该节点接收到不属于该节点负责的服务时，将在集群内部路由，转发给对应的节点，从而完成写入。</li></ol><p>读取操作则不需要路由，因为集群中的各个节点会同步服务状态，每个节点都会有一份最新的服务数据。</p><p>而当节点发生宕机后，原本该节点负责的一部分服务的写入任务会转移到其他节点，从而保证 Nacos 集群整体的可用性。</p><p><img src="https://image.cnkirito.cn/image-20201220204436097.png" alt="image-20201220204436097"></p><p>一个比较复杂的情况是，节点没有宕机，但是出现了网络分区，即下图所示：</p><p><img src="https://image.cnkirito.cn/image-20201220204446690.png" alt="image-20201220204446690"></p><p>这个情况会损害可用性，客户端会表现为有时候服务存在有时候服务不存在。</p><p>综上，Nacos 的 distro 一致性协议可以保证在大多数情况下，集群中的机器宕机后依旧不损害整体的可用性。该可用性保证存在于 nacos-server 端。</p><h2 id="本地缓存文件-Failover-机制"><a href="#本地缓存文件-Failover-机制" class="headerlink" title="本地缓存文件 Failover 机制"></a>本地缓存文件 Failover 机制</h2><p>注册中心发生故障最坏的一个情况是整个 Server 端宕机，这时候 Nacos 依旧有高可用机制做兜底。</p><p>一道经典的 Dubbo 面试题：当 Dubbo 应用运行时，Nacos 注册中心宕机，会不会影响 RPC 调用。这个题目大多数应该都能回答出来，因为 Dubbo 内存里面是存了一份地址的，一方面这样的设计是为了性能，因为不可能每次 RPC 调用时都读取一次注册中心，另一面，这也起到了可用性的保障（尽管可能 Dubbo 设计者并没有考虑这个因素）。</p><p>那如果，我在此基础上再出一道 Dubbo 面试题：Nacos 注册中心宕机，Dubbo 应用发生重启，会不会影响 RPC 调用。如果了解了 Nacos 的 Failover 机制，应当得到和上一题同样的回答：不会。</p><p>Nacos 存在本地文件缓存机制，nacos-client 在接收到 nacos-server 的服务推送之后，会在内存中保存一份，随后会落盘存储一份快照。snapshot 默认的存储路径为：{USER_HOME}/nacos/naming/ 中</p><p><img src="https://image.cnkirito.cn/image-20201220165548542.png" alt="Nacos snapshot 文件目录"></p><p>这份文件有两种价值，一是用来排查服务端是否正常推送了服务；二是当客户端加载服务时，如果无法从服务端拉取到数据，会默认从本地文件中加载。</p><blockquote><p>前提是构建 NacosNaming 时传入了该参数：namingLoadCacheAtStart=true</p><p>Dubbo 2.7.4 及以上版本支持该 Nacos 参数；开启该参数的方式：dubbo.registry.address=nacos://127.0.0.1:8848?namingLoadCacheAtStart=true</p></blockquote><p>在生产环境，推荐开启该参数，以避免注册中心宕机后，导致服务不可用的稳定，在服务注册发现场景，可用性和一致性 trade off 时，我们大多数时候会优先考虑可用性。</p><p>细心的读者还注意到 {USER_HOME}/nacos/naming/{namespace} 下除了缓存文件之外还有一个 failover 文件夹，里面存放着和 snapshot 一致的文件夹。这是 Nacos 的另一个 failover 机制，snapshot 是按照某个历史时刻的服务快照恢复恢复，而 failover 中的服务可以人为修改，以应对一些极端场景。</p><p>该可用性保证存在于 nacos-client 端。</p><h2 id="心跳同步服务"><a href="#心跳同步服务" class="headerlink" title="心跳同步服务"></a>心跳同步服务</h2><p>心跳机制一般广泛存在于分布式通信领域，用于确认存活状态。一般心跳请求和普通请求的设计是有差异的，心跳请求一般被设计的足够精简，这样在定时探测时可以尽可能避免性能下降。而在 Nacos 中，处于可用性的考虑，一个心跳报文包含了全部的服务信息，这样相比仅仅发送探测信息降低了吞吐量，而提升了可用性，怎么理解呢？考虑以下的两种场景：</p><ul><li>nacos-server 节点全部宕机，服务数据全部丢失。nacos-server 即使恢复运作，也无法恢复出服务，而心跳包含全部内容可以在心跳期间就恢复出服务，保证可用性。</li><li>nacos-server 出现网络分区。由于心跳可以创建服务，从而在极端网络故障下，依旧保证基础的可用性。</li></ul><p>以下是对心跳同步服务的测试，使用阿里云 MSE 提供 Nacos 集群进行测试</p><p><img src="https://image.cnkirito.cn/image-20201220173117425.png" alt></p><p>调用 OpenApi：<code>curl -X &quot;DELETE mse-xxx-p.nacos-ans.mse.aliyuncs.com:8848/nacos/v1/ns/service?serviceName=providers:com.alibaba.edas.boot.EchoService:1.0.0:DUBBO&amp;groupName=DEFAULT_GROUP&quot;</code> 依次删除各个服务</p><p><img src="https://image.cnkirito.cn/image-20201220173637589.png" alt></p><p>过 5s 后刷新，服务又再次被注册了上来，符合我们对心跳注册服务的预期。</p><h2 id="集群部署模式高可用"><a href="#集群部署模式高可用" class="headerlink" title="集群部署模式高可用"></a>集群部署模式高可用</h2><p>最后给大家分享的 Nacos 高可用特性来自于其部署架构。</p><h3 id="节点数量"><a href="#节点数量" class="headerlink" title="节点数量"></a>节点数量</h3><p>我们知道在生产集群中肯定不能以单机模式运行 Nacos，那么第一个问题便是：我应该部署几台机器？前面我们提到 Nacos 有两个一致性协议：distro 和 raft，distro 协议不会有脑裂问题，所以理论来说，节点数大于等于 2 即可；raft 协议的投票选举机制则建议是 2n+1 个节点。综合来看，选择 3 个节点是起码的，其次处于吞吐量和更高可用性的考量，可以选择 5 个，7 个，甚至 9 个节点的集群。</p><h3 id="多可用区部署"><a href="#多可用区部署" class="headerlink" title="多可用区部署"></a>多可用区部署</h3><p>组成集群的 Nacos 节点，应该尽可能考虑两个因素：</p><ol><li>各个节点之间的网络时延不能很高，否则会影响数据同步</li><li>各个节点所处机房、可用区应当尽可能分散，以避免单点故障</li></ol><p>以阿里云的 ECS 为例，选择同一个 Region 的不同可用区就是一个很好的实践</p><h3 id="部署模式"><a href="#部署模式" class="headerlink" title="部署模式"></a>部署模式</h3><p>主要分为 K8s 部署和 ECS 部署两种模式。</p><p>ECS 部署的优点在于简单，购买三台机器即可搭建集群，如果你熟练 Nacos 集群部署的话，这不是难事，但无法解决运维问题，如果 Nacos 某个节点出现 OOM 或者磁盘问题，很难迅速摘除，无法实现自运维。</p><p>K8s 部署的有点在于云原生运维能力强，可以在节点宕机后实现自恢复，保障 Nacos 的平稳运行。前面提到过，Nacos 和无状态的 Web 应用不同，它是一个有状态的应用，所以在 K8s 中部署，往往要借助于 StatefulSet 和 Operator 等组件才能实现 Nacos 集群的部署和运维。</p><h2 id="MSE-Nacos-的高可用最佳实践"><a href="#MSE-Nacos-的高可用最佳实践" class="headerlink" title="MSE Nacos 的高可用最佳实践"></a>MSE Nacos 的高可用最佳实践</h2><p>阿里云 MSE（微服务引擎）提供了 Nacos 集群的托管能力，实现了集群部署模式的高可用。</p><ul><li>当创建多个节点的集群时，系统会默认分配在不同可用区。同时，这对于用户来说又是透明的，用户只需要关心 Nacos 的功能即可，MSE 替用户兜底可用性。</li><li>MSE 底层使用 K8s 运维模式部署 Nacos。历史上出现过用户误用 Nacos 导致部分节点宕机的问题，但借助于 K8s 的自运维模式，宕机节点迅速被拉起，以至于用户可能都没有意识到自己发生宕机。</li></ul><p>下面模拟一个节点宕机的场景，来看看 K8s 如何实现自恢复。</p><p>一个三节点的 Nacos 集群：</p><p><img src="https://image.cnkirito.cn/image-20201220180757477.png" alt="正常状态"></p><p>执行 <code>kubectl delete pod mse-7654c960-1605278296312-reg-center-0-2</code> 以模拟部分节点宕机的场景。</p><p><img src="https://image.cnkirito.cn/image-20201220181056737.png" alt="恢复中"></p><p>大概 2 分钟后，节点恢复，并且角色发生了转换，Leader 从杀死的 2 号节点转给 1 号节点</p><p><img src="https://image.cnkirito.cn/image-20201220181330884.png" alt="恢复后 leader 重选"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文从多个角度出发，总结了一下 Nacos 是如何保障高可用的。高可用特性绝不是靠服务端多部署几个节点就可以获得的，而是要结合客户端使用方式、服务端部署模式、使用场景综合来考虑的一件事。</p><p>特别是在服务注册发现场景，Nacos 为可用性做了非常多的努力，而这些保障，Zookeeper 是不一定有的。在做注册中心选型时，可用性保障上，Nacos 绝对是优秀的。</p><p><img src="https://www.cnkirito.moe/css/images/wechat_public.jpg" alt="微信公众号"></p><p><em>「技术分享」某种程度上，是让作者和读者，不那么孤独的东西。欢迎关注我的微信公众号：「Kirito的技术分享」</em></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;服务注册发现是一个经久不衰的话题，Dubbo 早期开源时默认的注册中心 Zookeeper 最早进入人们的视线，并且在很长一段时间里，人们将注册中心和 Zookeeper 划上了等号，可能 Zookeeper 的设计者都没有想到这款产品对微服务领域造成了如此深厚的影响，直到 SpringCloud 开始流行，其自带的 Eureka 进入了人们的视野，人们这才意识到原来注册中心还可以有其他的选择。再到后来，热衷于开源的阿里把目光也聚焦在了注册中心这个领域，Nacos 横空出世。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://image.cnkirito.cn/image-20201220140934757.png&quot; alt=&quot;注册中心&quot;&gt;&lt;/p&gt;
&lt;p&gt;Kirito 在做注册中心选型时的思考：曾经我没得选，现在我只想选择一个好的注册中心，它最好是开源的，这样开放透明，有自我的掌控力；不仅要开源，它还要有活跃的社区，以确保特性演进能够满足日益增长的业务需求，出现问题也能及时修复；最好…它的功能还要很强大，除了满足注册服务、推送服务外，还要有完善的微服务体系中所需的功能；最重要的，它还要稳定，最好有大厂的实际使用场景背书，证明这是一个经得起实战考验的产品；当然，云原生特性，安全特性也是很重要的…&lt;/p&gt;
&lt;p&gt;似乎 Kirito 对注册中心的要求实在是太高了，但这些五花八门的注册中心呈现在用户眼前，总是免不了一番比较。正如上面所言，功能特性、成熟度、可用性、用户体验度、云原生特性、安全都是可以拉出来做比较的话题。今天这篇文章重点介绍的是 Nacos 在可用性上体现，希望借助于这篇文章，能够让你对 Nacos 有一个更加深刻的认识。&lt;/p&gt;
    
    </summary>
    
      <category term="Nacos" scheme="https://lexburner.github.io/categories/Nacos/"/>
    
    
      <category term="Nacos" scheme="https://lexburner.github.io/tags/Nacos/"/>
    
  </entry>
  
  <entry>
    <title>鱼和熊掌兼得：同时使用 JPA 和 Mybatis</title>
    <link href="https://lexburner.github.io/jpa-and-mybatis/"/>
    <id>https://lexburner.github.io/jpa-and-mybatis/</id>
    <published>2020-11-12T04:30:01.000Z</published>
    <updated>2021-01-16T11:25:16.470Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>JPA 和 Mybatis 的争论由来已久，还记得在 2 年前我就在 spring4all 社区就两者孰优孰劣的话题发表了观点，我当时是力挺 JPA 的，这当然跟自己对 JPA 熟悉程度有关，但也有深层次的原因，便是 JPA 的设计理念契合了领域驱动设计的思想，可以很好地指导我们设计数据库交互接口。这两年工作中，逐渐接触了一些使用 Mybatis 的项目，也对其有了一定新的认知。都说认知是一个螺旋上升的过程，随着经验的累积，人们会轻易推翻过去，到了两年后的今天，我也有了新的观点。本文不是为了告诉你 JPA 和 Mybatis 到底谁更好，而是尝试求同存异，甚至是在项目中同时使用 JPA 和 Mybatis。什么？要同时使用两个 ORM 框架，有这个必要吗？别急着吐槽我，希望看完本文后，你也可以考虑在某些场合下同时使用这两个框架。</p><p>ps. 本文讨论的 JPA 特指 spring-data-jpa。</p><a id="more"></a><h2 id="建模"><a href="#建模" class="headerlink" title="建模"></a>建模</h2><figure class="highlight java"><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="meta">@Entity</span></span><br><span class="line"><span class="meta">@Table</span>(name = <span class="string">"t_order"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Order</span> </span>&#123;</span><br><span class="line">  </span><br><span class="line">  <span class="meta">@Id</span></span><br><span class="line">  <span class="keyword">private</span> String oid;</span><br><span class="line">  </span><br><span class="line">  <span class="meta">@Embedded</span></span><br><span class="line">  <span class="keyword">private</span> CustomerVo customer;</span><br><span class="line">  </span><br><span class="line">  <span class="meta">@OneToMany</span>(cascade = &#123;CascadeType.ALL&#125;, orphanRemoval = <span class="keyword">true</span>, fetch = FetchType.LAZY, mappedBy = <span class="string">"order"</span>)</span><br><span class="line">  <span class="keyword">private</span> List&lt;OrderItem&gt; orderItems;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>JPA 最大的特点是 sqlless，如上述的实体定义，便将数据库的表和 Java 中的类型关联起来了，JPA 可以做到根据 @Entity 注解，自动创建表结构；基于这个实体实现的 Repository 接口，又使得 JPA 用户可以很方便地实现数据的 CRUD。所以，使用 JPA 的项目，人们很少会提到”数据库设计“，人们更关心的是领域建模，而不是数据建模。</p><figure class="highlight xml"><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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">generatorConfiguration</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">context</span> <span class="attr">id</span>=<span class="string">"my"</span> <span class="attr">targetRuntime</span>=<span class="string">"MyBatis3"</span>&gt;</span></span><br><span class="line">      </span><br><span class="line">      <span class="tag">&lt;<span class="name">jdbcConnection</span> <span class="attr">driverClass</span>=<span class="string">"com.mysql.jdbc.Driver"</span> <span class="attr">connectionURL</span>=<span class="string">""</span></span></span><br><span class="line"><span class="tag">           <span class="attr">userId</span>=<span class="string">""</span> <span class="attr">password</span>=<span class="string">""</span>/&gt;</span></span><br><span class="line"></span><br><span class="line">      <span class="tag">&lt;<span class="name">javaModelGenerator</span> <span class="attr">targetPackage</span>=<span class="string">""</span> <span class="attr">targetProject</span>=<span class="string">""</span> /&gt;</span></span><br><span class="line"></span><br><span class="line">      <span class="tag">&lt;<span class="name">sqlMapGenerator</span> <span class="attr">targetPackage</span>=<span class="string">""</span> <span class="attr">targetProject</span>=<span class="string">""</span> /&gt;</span></span><br><span class="line"></span><br><span class="line">      <span class="tag">&lt;<span class="name">javaClientGenerator</span> <span class="attr">targetPackage</span>=<span class="string">"moe.cnkirito.demo.mapper"</span> /&gt;</span></span><br><span class="line">      </span><br><span class="line">      <span class="tag">&lt;<span class="name">table</span> <span class="attr">tableName</span>=<span class="string">"t_order"</span> <span class="attr">domainObjectName</span>=<span class="string">"Order"</span> /&gt;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;/<span class="name">context</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">generatorConfiguration</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Mybatis 用户更多使用的是逆向工程，例如 mybatis-generator 插件根据如上的 xml 配置，便可以直接将表结构转译成 mapper 文件和实体文件。</p><p>code first 和 table first 从结果来看是没有区别的，差异的是过程，所以设计良好的系统，并不会仅仅因为这个差异而高下立判，但从指导性来看，无疑设计系统时，更应该考虑的是实体和实体，实体和值对象的关联，领域边界的划分，而不是首先着眼于数据库表结构的设计。</p><p>建模角度来看，JPA 的领域建模思想更胜一筹。</p><h2 id="数据更新"><a href="#数据更新" class="headerlink" title="数据更新"></a>数据更新</h2><p>聊数据库自然离不开 CRUD，先来看增删改这些数据更新操作，来看看两个框架一般的习惯是什么。</p><p>JPA 推崇的数据更新只有一种范式，分成三步：</p><ol><li>先 findOne 映射成实体</li><li>内存内修改实体</li><li>实体整体 save</li></ol><p>你可能会反驳我说，@Query 也存在 nativeQuery 和 JPQL 的用法，但这并不是主流用法。JPA 特别强调”整体 save“的思想，这与领域驱动设计所强调的有状态密不可分，即其认为，修改不应该是针对于某一个字段：”update table set a=b where colomonA=xx“ ，而应该反映成实体的变化，save 则代表了实体状态最终的持久化。</p><p>先 find 后 save 显然也适用于 Mybatis，而 Mybatis 的灵活性，使得其数据更新方式更加地百花齐放。路人甲可以认为 JPA 墨守成规不懂变通，认为 Mybatis 不羁放纵爱自由；路人乙也可以认为 JPA 格式规范易维护，Mybatis 不成方圆。这点不多加评判，留后人说。</p><p>从个人习惯来说，我还是偏爱先 find 后整体 save 这种习惯的，不是说这是 JPA 的专利，Mybatis 不具备；而是 JPA 的强制性，让我有了这个习惯。 </p><p>数据更新角度来看，JPA 强制使用 find+save，mybatis 也可以做到这一点，胜者：无。</p><h2 id="数据查询"><a href="#数据查询" class="headerlink" title="数据查询"></a>数据查询</h2><p>JPA 提供的查询方式主要分为两种</p><ol><li>简单查询：findBy + 属性名</li><li>复杂查询：JpaSpecificationExecutor</li></ol><p>简单查询在一些简单的业务场景下提供了非常大的便捷性，findBy + 属性名可以自动转译成 sql，试问如果可以少写代码，有谁不愿意呢？</p><p>复杂查询则是 JPA 为了解决复杂的查询场景，提供的解决方案，硬是把数据库的一些聚合函数，连接操作，转换成了 Java 的方法，虽然做到了 sqlless，但写出来的代码又臭又长，也不见得有多么的易读易维护。这算是我最不喜欢 JPA 的一个地方了，但要解决复杂查询，又别无他法。</p><p>而 Mybatis 可以执行任意的查询 sql，灵活性是 JPA 比不了的。数据库小白搜索的最多的两个问题：</p><ol><li>数据库分页怎么做</li><li><p>条件查询怎么做</p><p>Mybatis 都可以轻松的解决。</p></li></ol><p>千万不要否认复杂查询：如聚合查询、Join 查询的场景。令一个 JPA 用户抓狂的最简单方式，就是给他一个复杂查询的 case。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> a,b,c,<span class="keyword">sum</span>(a) <span class="keyword">where</span> a=xx <span class="keyword">and</span> d=xx <span class="keyword">group</span> <span class="keyword">by</span> a,b,c;</span><br></pre></td></tr></table></figure><p>来吧，展示。可能 JPA 的确可以完成上述 sql 的转义，但要知道不是所有开发都是 JPA 专家，没人关心你用 JPA 解决了多么复杂的查询语句，更多的人关心地是，能不能下班前把这个复杂查询搞定，早点回家。</p><p>在回到复杂数据查询需求本身的来分析下。我们假设需求是合理的，毕竟项目的复杂性难以估计，可能有 1000 个数据查询需求 JPA 都可以很方便的实现，但就是有那么 10 几个复杂查询 JPA hold 不住。这个时候你只能乖乖地去写 sql 了，如果这个时候又出现一个条件查询的场景，出现了 if else 意味着连 @Query 都用不了，完全退化成了 JdbcTemplate 的时代。</p><p>那为什么不使用 Mybatis 呢？Mybatis 使用者从来没有纠结过复杂查询，它简直就是为之而生的。</p><p>如今很多 Mybatis 的插件，也可以帮助使用者快速的生成基础方法，虽然仍然需要写 sql，但是这对于开发者来说，并不是一件难事。</p><p>不要质疑高并发下，JOIN 操作和聚合函数存在的可能性，数据查询场景下，Mybatis 完胜。</p><h2 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h2><p>本质上 ORM 框架并没有性能的区分度，因为最终都是转换成 sql 交给数据库引擎去执行，ORM 层面那层性能损耗几乎可以忽略不计。</p><p>但从实际出发，Mybatis 提供给了开发者更高的 sql 自由度，所以在一些需要 sql 调优的场景下会更加灵活。</p><h2 id="可维护性"><a href="#可维护性" class="headerlink" title="可维护性"></a>可维护性</h2><p>前面我们提到 JPA 相比 Mybatis 丧失了 sql 的自由度，凡事必有 trade off，从另一个层面上来看，其提供了高层次的抽象，尝试用统一的模型去解决数据层面的问题。sqlless 同时也屏蔽了数据库的实现，屏蔽了数据库高低版本的兼容性问题，这对可能存在的数据库迁移以及数据库升级提供了很大的便捷性。</p><h2 id="同时使用两者"><a href="#同时使用两者" class="headerlink" title="同时使用两者"></a>同时使用两者</h2><p>其他细节我就不做分析了，相信还有很多点可以拿过来做对比，但我相信主要的点上文都应该有所提及了。进行以上维度的对比并不是我写这篇文章的初衷，更多地是想从实际开发角度出发，为大家使用这两个框架提供一些参考建议。</p><p>在大多数场景下，我习惯使用 JPA，例如设计领域对象时，得益于 JPA 的正向模型，我会优先考虑实体和值对象的关联性以及领域上下文的边界，而不用过多关注如何去设计表结构；在增删改和简单查询场景下，JPA 提供的 API 已经是刻在我 DNA 里面的范式了，使用起来非常的舒服。</p><p>在复杂查询场景下，例如</p><ol><li>包含不存在领域关联的 join 查询</li><li>包含多个聚合函数的复杂查询</li><li>其他 JPA 较难实现的查询</li></ol><p>我会选择使用 Mybatis，有点将 Mybatis 当做数据库视图生成器的意味。坚定不移的 JPA 拥趸者可能会质疑这些场景的存在的真实性，会质疑是不是设计的漏洞，但按照经验来看，哪怕是短期方案，这些场景也是客观存在的，所以听我一言，尝试拥抱一下 Mybatis 吧。</p><p>随着各类存储中间件的流行，例如 mongodb、ES，取代了数据库的一部分地位，重新思考下，本质上都是在用专业的工具解决特定场景的问题，最终目的都是为了解放生产力。数据库作为最古老，最基础的存储组件，的确承载了很多它本不应该承受的东西，那又何必让一个工具或者一个框架成为限制我们想象力的沟壑呢？</p><p>两个框架其实都不重，在 springboot 的加持下，引入几行配置就可以实现两者共存了。</p><p>我自己在最近的项目中便同时使用了两者，遵循的便是本文前面聊到的这些规范，我也推荐给你，不妨试试。</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;JPA 和 Mybatis 的争论由来已久，还记得在 2 年前我就在 spring4all 社区就两者孰优孰劣的话题发表了观点，我当时是力挺 JPA 的，这当然跟自己对 JPA 熟悉程度有关，但也有深层次的原因，便是 JPA 的设计理念契合了领域驱动设计的思想，可以很好地指导我们设计数据库交互接口。这两年工作中，逐渐接触了一些使用 Mybatis 的项目，也对其有了一定新的认知。都说认知是一个螺旋上升的过程，随着经验的累积，人们会轻易推翻过去，到了两年后的今天，我也有了新的观点。本文不是为了告诉你 JPA 和 Mybatis 到底谁更好，而是尝试求同存异，甚至是在项目中同时使用 JPA 和 Mybatis。什么？要同时使用两个 ORM 框架，有这个必要吗？别急着吐槽我，希望看完本文后，你也可以考虑在某些场合下同时使用这两个框架。&lt;/p&gt;
&lt;p&gt;ps. 本文讨论的 JPA 特指 spring-data-jpa。&lt;/p&gt;
    
    </summary>
    
      <category term="JAVA" scheme="https://lexburner.github.io/categories/JAVA/"/>
    
    
      <category term="JAVA" scheme="https://lexburner.github.io/tags/JAVA/"/>
    
  </entry>
  
  <entry>
    <title>聊聊算法在面试中的地位</title>
    <link href="https://lexburner.github.io/talk-about-algorithm/"/>
    <id>https://lexburner.github.io/talk-about-algorithm/</id>
    <published>2020-10-08T07:43:47.000Z</published>
    <updated>2021-01-16T11:25:16.505Z</updated>
    
    <content type="html"><![CDATA[<p>前段时间，有一位好友找到我，向我打听阿里社招笔试是否看重算法题的考察，我给予了肯定的答复。他表现的有些沮丧，表示自己工程底子很扎实，框架源码也研究地很透彻，唯独算法能力不行，leetcode 上的简单题做起来都有点吃力。以至于面试一些公司时，基本都是前几面和面试官聊工程，相聊甚欢，一到笔试就 GG。鉴于我个人在学生时代有过 ACM 经历，对算法还是相当感冒的，个人算法能力不算出众，也不算弱，最好成绩是省赛金牌，区域赛铜牌（主要还是抱得队友的大腿），后来实在是写不动 C++ 了，中途转了 Java，借这个机会跟大家聊一聊，分享下个人对算法的一些认识。</p><a id="more"></a><p>我发现很多人有的一个观念是刷算法并不能很好地帮助他工作，他们中有些人是有了很多学校或者公司的项目经验，有些则是在数据库、RPC、大数据等某个垂直领域有了比较长时间的沉淀，他们会觉得刻意地刷算法题比较偏门，没有太大的价值。一方面有些人会比较自信，不认为需要靠算法来证明自己的价值，另一方面，有些人会认为刷算法题是应届生面试才需要考察的技能，对于社招来说，公司应该更注重考察项目经验和系统设计层面的技能。以我个人经验来看，面试互联网公司时，算法题几乎都是必考的一个环节，从公司的考察点出发，就可以佐证出，算法不重要这个观点的确是有待商榷的。还有一些人轻视算法，是觉得只有大厂才看重算法，一些小公司的面试根本不 care 算法，而且特别是像我文章开头提到那个朋友一样的人，有着比较强的工程能力，我相信在面试中一定可以凭借着这个优势，赢得面试官的好感，那我不妨再反问一句，为什么要让算法成为你的软肋呢？</p><p>我已经表露了我对面试中算法重要程度的态度，而且我也认为面试中考察算法能力是非常重要的一环。在公司里做项目，我们往往需要花费数个月去落地，而面试中完成算法题最多只限制在半小时内，虽然时间区间不同，但本质上都是在考察一个人在一个固定的时间内完成某个任务的能力。读题考察了候选人的理解能力，期间我会与候选人沟通，以确保他正确理解的题意，并且在码字之前，我会要求对方先讲解题思路，这考察了沟通能力，有的候选人可能没有经历过刷题训练，缺少一些常见的算法思维，但经过提示后，如果能快速地完成 coding，在笔试中或许也能够通过。所以你看，其实考察算法题其实和也是借此考验了你的工作能力，它要求你在短短的半个小时之内做到 Buf Free，一定程度上这比做工程更难，因为没有人为你测试，而你要想通过这一环节，是需要额外花费精力去训练的。</p><p>虽然我认为面试中算法很重要，推荐大家准备面试时多去刷刷题，但我也确实抵制一些偏题、怪题。以我的刷题经验和工作经验结合来看，推荐的难度为 leetcode 简单、中等题，ACM 铜牌、银牌题，仅供参考。记得有一次瞄了一眼阿里的校招在线笔试题，具体是哪个部门不清楚，那个难度估计得是地狱难度了，这类情况仅仅是小概率会发生，至少在我们大部门不会出现特别难的算法题。</p><p>很多人说面试造火箭，入职拧螺丝，以此来讽刺面试中算法面是不必要的，我是不赞同的。抛开面试，算法能力也的确是工作中帮助了我。简单举几个例子吧，我通过算法题接触到了欧拉函数、GCD 等数论知识，让我可以非常好地理解 RSA 加密的原理和实现过程，而 RSA 加密是很有可能在工程中被使用到的一种非对称加密方式；通过解决常见的数据结构类算法题，我了解到了跳表的实现，这方便了我去理解 Redis 的 Set 结构；熟练地解决贪心和 DP 等问题，也潜移默化地影响着我在工程项目中的代码逻辑。</p><p>字节跳动可以说是业内有名的看重算法面的公司了，但鉴于本人并不了解实际的情况，只能跟大家聊聊阿里的算法面试。分成两部分：实习生面试和社招面试。我这里的经验主要都是基于我所了解的情况，在阿里其他部门（非阿里云）可能情况就不一样了。先说实习生面试吧，算法主要考察的是简单题，主要以贪心、数据结构、模拟为主，可以说非常友好了，主要考验学生对于基础知识的掌握程度，但也要求候选人能够在较短时间内完成，否则很难在整体面试中获得 A 评价。而社招，算法面试的地位肯定是要低于工程能力的考核的，但是对于能不能发 offer 又起着决定性的作用，等于说，即使你的工作履历很 match 岗位，工程经验也很丰富，但算法面一塌糊涂，往往用人部门只能忍痛割爱了。</p><p>如果你正在准备面试，我是建议准备下算法，刷一些题目找下手感，leetcode 和各种在线 OJ 都是不错的选择，B 站也有很多视频，具体的刷题列表，我这儿没准备，相信你可以在网上找到很多的。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;前段时间，有一位好友找到我，向我打听阿里社招笔试是否看重算法题的考察，我给予了肯定的答复。他表现的有些沮丧，表示自己工程底子很扎实，框架源码也研究地很透彻，唯独算法能力不行，leetcode 上的简单题做起来都有点吃力。以至于面试一些公司时，基本都是前几面和面试官聊工程，相聊甚欢，一到笔试就 GG。鉴于我个人在学生时代有过 ACM 经历，对算法还是相当感冒的，个人算法能力不算出众，也不算弱，最好成绩是省赛金牌，区域赛铜牌（主要还是抱得队友的大腿），后来实在是写不动 C++ 了，中途转了 Java，借这个机会跟大家聊一聊，分享下个人对算法的一些认识。&lt;/p&gt;
    
    </summary>
    
      <category term="技术杂谈" scheme="https://lexburner.github.io/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    
    
      <category term="招聘" scheme="https://lexburner.github.io/tags/%E6%8B%9B%E8%81%98/"/>
    
      <category term="算法" scheme="https://lexburner.github.io/tags/%E7%AE%97%E6%B3%95/"/>
    
  </entry>
  
  <entry>
    <title>gson 替换 fastjson 引发的线上问题分析</title>
    <link href="https://lexburner.github.io/serialize-practice/"/>
    <id>https://lexburner.github.io/serialize-practice/</id>
    <published>2020-09-15T10:37:56.000Z</published>
    <updated>2021-01-16T11:25:16.492Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>Json 序列化框架存在的安全漏洞一直以来都是程序员们挂在嘴边调侃的一个话题，尤其是这两年 fastjson 由于被针对性研究，更是频频地的报出漏洞，出个漏洞不要紧，可安全团队总是用邮件催着线上应用要进行依赖升级，这可就要命了，我相信很多小伙伴也是不胜其苦，考虑了使用其他序列化框架替换 fastjson。这不，最近我们就有一个项目将 fastjson 替换为了 gson，引发了一个线上的问题。分享下这次的经历，以免大家踩到同样的坑，在此警示大家，规范千万条，安全第一条，升级不规范，线上两行泪。</p><h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>线上一个非常简单的逻辑，将对象序列化成 fastjson，再使用 HTTP 请求将字符串发送出去。原本工作的好好的，在将 fastjson 替换为 gson 之后，竟然引发了线上的 OOM。经过内存 dump 分析，发现竟然发送了一个 400 M+ 的报文，由于 HTTP 工具没有做发送大小的校验，强行进行了传输，直接导致了线上服务整体不可用。</p><a id="more"></a><h2 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h2><p>为什么同样是 Json 序列化，fastjson 没出过问题，而换成 gson 之后立马就暴露了呢？通过分析内存 dump 的数据，发现很多字段的值都是重复的，再结合我们业务数据的特点，一下子定位到了问题 – gson 序列化重复对象存在严重的缺陷。</p><p>直接用一个简单的例子，来说明当时的问题。模拟线上的数据特性，使用 <code>List&lt;Foo&gt;</code> 添加进同一个引用对象</p><figure class="highlight java"><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">Foo foo = <span class="keyword">new</span> Foo();</span><br><span class="line">Bar bar = <span class="keyword">new</span> Bar();</span><br><span class="line">List&lt;Foo&gt; foos = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">int</span> i=<span class="number">0</span>;i&lt;<span class="number">3</span>;i++)&#123;</span><br><span class="line">    foos.add(foo);</span><br><span class="line">&#125;</span><br><span class="line">bar.setFoos(foos);</span><br><span class="line"></span><br><span class="line">Gson gson = <span class="keyword">new</span> Gson();</span><br><span class="line">String gsonStr = gson.toJson(bar);</span><br><span class="line">System.out.println(gsonStr);</span><br><span class="line"></span><br><span class="line">String fastjsonStr = JSON.toJSONString(bar);</span><br><span class="line">System.out.println(fastjsonStr);</span><br></pre></td></tr></table></figure><p>观察打印结果：</p><p>gson：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;<span class="attr">"foos"</span>:[&#123;<span class="attr">"a"</span>:<span class="string">"aaaaa"</span>&#125;,&#123;<span class="attr">"a"</span>:<span class="string">"aaaaa"</span>&#125;,&#123;<span class="attr">"a"</span>:<span class="string">"aaaaa"</span>&#125;]&#125;</span><br></pre></td></tr></table></figure><p>fastjson：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;<span class="attr">"foos"</span>:[&#123;<span class="attr">"a"</span>:<span class="string">"aaaaa"</span>&#125;,&#123;<span class="attr">"$ref"</span>:<span class="string">"$.foos[0]"</span>&#125;,&#123;<span class="attr">"$ref"</span>:<span class="string">"$.foos[0]"</span>&#125;]&#125;</span><br></pre></td></tr></table></figure><p>可以发现 gson 处理重复对象，是对每个对象都进行了序列化，而 fastjson 处理重复对象，是将除第一个对象外的其他对象使用引用符号 <code>$ref</code> 进行了标记。</p><p>当单个重复对象的数量非常多，以及单个对象的提交较大时，两种不同的序列化策略会导致一个质变，我们不妨来针对特殊的场景进行下对比。 </p><h2 id="压缩比测试"><a href="#压缩比测试" class="headerlink" title="压缩比测试"></a>压缩比测试</h2><ul><li><p>序列化对象：包含大量的属性。以模拟线上的业务数据。</p></li><li><p>重复次数：200。即 List 中包含 200 个同一引用的对象，以模拟线上复杂的对象结构，扩大差异性。</p></li><li>序列化方式：gson、fastjson、Java、Hessian2。额外引入了 Java 和 Hessian2 的对照组，方便我们了解各个序列化框架在这个特殊场景下的表现。</li><li>主要观察各个序列化方式压缩后的字节大小，因为这关系到网络传输时的大小；次要观察反序列后 List 中还是不是同一个对象</li></ul><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException, ClassNotFoundException </span>&#123;</span><br><span class="line">        Foo foo = <span class="keyword">new</span> Foo();</span><br><span class="line">        Bar bar = <span class="keyword">new</span> Bar();</span><br><span class="line">        List&lt;Foo&gt; foos = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">int</span> i=<span class="number">0</span>;i&lt;<span class="number">200</span>;i++)&#123;</span><br><span class="line">            foos.add(foo);</span><br><span class="line">        &#125;</span><br><span class="line">        bar.setFoos(foos);</span><br><span class="line">        <span class="comment">// gson</span></span><br><span class="line">        Gson gson = <span class="keyword">new</span> Gson();</span><br><span class="line">        String gsonStr = gson.toJson(bar);</span><br><span class="line">        System.out.println(gsonStr.length());</span><br><span class="line">        Bar gsonBar = gson.fromJson(fastjsonStr, Bar<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">        System.out.println(gsonBar.getFoos().get(<span class="number">0</span>) == gsonBar.getFoos().get(<span class="number">1</span>));  </span><br><span class="line">        <span class="comment">// fastjson</span></span><br><span class="line">        String fastjsonStr = JSON.toJSONString(bar);</span><br><span class="line">        System.out.println(fastjsonStr.length());</span><br><span class="line">        Bar fastjsonBar = JSON.parseObject(fastjsonStr, Bar<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">        System.out.println(fastjsonBar.getFoos().get(<span class="number">0</span>) == fastjsonBar.getFoos().get(<span class="number">1</span>));</span><br><span class="line"><span class="comment">// java</span></span><br><span class="line">        ByteArrayOutputStream byteArrayOutputStream = <span class="keyword">new</span> ByteArrayOutputStream();</span><br><span class="line">        ObjectOutputStream oos = <span class="keyword">new</span> ObjectOutputStream(byteArrayOutputStream);</span><br><span class="line">        oos.writeObject(bar);</span><br><span class="line">        oos.close();</span><br><span class="line">        System.out.println(byteArrayOutputStream.toByteArray().length);</span><br><span class="line">        ObjectInputStream ois = <span class="keyword">new</span> ObjectInputStream(<span class="keyword">new</span> ByteArrayInputStream(byteArrayOutputStream.toByteArray()));</span><br><span class="line">        Bar javaBar = (Bar) ois.readObject();</span><br><span class="line">        ois.close();</span><br><span class="line">        System.out.println(javaBar.getFoos().get(<span class="number">0</span>) == javaBar.getFoos().get(<span class="number">1</span>));</span><br><span class="line">        <span class="comment">// hessian2</span></span><br><span class="line">        ByteArrayOutputStream hessian2Baos = <span class="keyword">new</span> ByteArrayOutputStream();</span><br><span class="line">        Hessian2Output hessian2Output = <span class="keyword">new</span> Hessian2Output(hessian2Baos);</span><br><span class="line">        hessian2Output.writeObject(bar);</span><br><span class="line">        hessian2Output.close();</span><br><span class="line">        System.out.println(hessian2Baos.toByteArray().length);</span><br><span class="line">        ByteArrayInputStream hessian2Bais = <span class="keyword">new</span> ByteArrayInputStream(hessian2Baos.toByteArray());</span><br><span class="line">        Hessian2Input hessian2Input = <span class="keyword">new</span> Hessian2Input(hessian2Bais);</span><br><span class="line">        Bar hessian2Bar = (Bar) hessian2Input.readObject();</span><br><span class="line">        hessian2Input.close();</span><br><span class="line">        System.out.println(hessian2Bar.getFoos().get(<span class="number">0</span>) == hessian2Bar.getFoos().get(<span class="number">1</span>));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出结果：</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><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></pre></td><td class="code"><pre><span class="line">gson:</span><br><span class="line">62810</span><br><span class="line">false</span><br><span class="line"></span><br><span class="line">fastjson:</span><br><span class="line">4503</span><br><span class="line">true</span><br><span class="line"></span><br><span class="line">Java:</span><br><span class="line">1540</span><br><span class="line">true</span><br><span class="line"></span><br><span class="line">Hessian2:</span><br><span class="line">686</span><br><span class="line">true</span><br></pre></td></tr></table></figure><p>结论分析：由于单个对象序列化后的体积较大，采用引用表示的方式可以很好的缩小体积，可以发现 gson 并没有采取这种序列化优化策略，导致体积膨胀。甚至一贯不被看好的 Java 序列化都比其优秀的多，而 Hessian2 更是夸张，直接比 gson 优化了 2个数量级。并且反序列化后，gson 并不能将原本是同一引用的对象还原回去，而其他的序列化框架均可以实现这一点。</p><h2 id="吞吐量测试"><a href="#吞吐量测试" class="headerlink" title="吞吐量测试"></a>吞吐量测试</h2><p>除了关注序列化之后数据量的大小，各个序列化的吞吐量也是我们关心的一个点。使用基准测试可以精准地测试出各个序列化方式的吞吐量。</p><figure class="highlight java"><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><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@BenchmarkMode</span>(&#123;Mode.Throughput&#125;)</span><br><span class="line"><span class="meta">@State</span>(Scope.Benchmark)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MicroBenchmark</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Bar bar;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Setup</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">prepare</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        Foo foo = <span class="keyword">new</span> Foo();</span><br><span class="line">        Bar bar = <span class="keyword">new</span> Bar();</span><br><span class="line">        List&lt;Foo&gt; foos = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">int</span> i=<span class="number">0</span>;i&lt;<span class="number">200</span>;i++)&#123;</span><br><span class="line">            foos.add(foo);</span><br><span class="line">        &#125;</span><br><span class="line">        bar.setFoos(foos);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    Gson gson = <span class="keyword">new</span> Gson();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Benchmark</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">gson</span><span class="params">()</span></span>&#123;</span><br><span class="line">        String gsonStr = gson.toJson(bar);</span><br><span class="line">        gson.fromJson(gsonStr, Bar<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Benchmark</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">fastjson</span><span class="params">()</span></span>&#123;</span><br><span class="line">        String fastjsonStr = JSON.toJSONString(bar);</span><br><span class="line">        JSON.parseObject(fastjsonStr, Bar<span class="class">.<span class="keyword">class</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Benchmark</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">java</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        ByteArrayOutputStream byteArrayOutputStream = <span class="keyword">new</span> ByteArrayOutputStream();</span><br><span class="line">        ObjectOutputStream oos = <span class="keyword">new</span> ObjectOutputStream(byteArrayOutputStream);</span><br><span class="line">        oos.writeObject(bar);</span><br><span class="line">        oos.close();</span><br><span class="line"></span><br><span class="line">        ObjectInputStream ois = <span class="keyword">new</span> ObjectInputStream(<span class="keyword">new</span> ByteArrayInputStream(byteArrayOutputStream.toByteArray()));</span><br><span class="line">        Bar javaBar = (Bar) ois.readObject();</span><br><span class="line">        ois.close();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Benchmark</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">hessian2</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        ByteArrayOutputStream hessian2Baos = <span class="keyword">new</span> ByteArrayOutputStream();</span><br><span class="line">        Hessian2Output hessian2Output = <span class="keyword">new</span> Hessian2Output(hessian2Baos);</span><br><span class="line">        hessian2Output.writeObject(bar);</span><br><span class="line">        hessian2Output.close();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        ByteArrayInputStream hessian2Bais = <span class="keyword">new</span> ByteArrayInputStream(hessian2Baos.toByteArray());</span><br><span class="line">        Hessian2Input hessian2Input = <span class="keyword">new</span> Hessian2Input(hessian2Bais);</span><br><span class="line">        Bar hessian2Bar = (Bar) hessian2Input.readObject();</span><br><span class="line">        hessian2Input.close();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> RunnerException </span>&#123;</span><br><span class="line">        Options opt = <span class="keyword">new</span> OptionsBuilder()</span><br><span class="line">            .include(MicroBenchmark<span class="class">.<span class="keyword">class</span>.<span class="title">getSimpleName</span>())</span></span><br><span class="line"><span class="class">            .<span class="title">build</span>()</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">new</span> Runner(opt).run();</span><br><span class="line">    &#125;</span><br><span class="line"></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><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">Benchmark                 Mode  Cnt        Score         Error  Units</span><br><span class="line">MicroBenchmark.fastjson  thrpt   25  6724809.416 ± 1542197.448  ops/s</span><br><span class="line">MicroBenchmark.gson      thrpt   25  1508825.440 ±  194148.657  ops/s</span><br><span class="line">MicroBenchmark.hessian2  thrpt   25   758643.567 ±  239754.709  ops/s</span><br><span class="line">MicroBenchmark.java      thrpt   25   734624.615 ±   66892.728  ops/s</span><br></pre></td></tr></table></figure><p>是不是有点出乎意料，fastjson 竟然独领风骚，文本类序列化的吞吐量相比二进制序列化的吞吐量要高出一个数量级，分别是每秒百万级和每秒十万级的吞吐量。</p><h2 id="整体测试结论"><a href="#整体测试结论" class="headerlink" title="整体测试结论"></a>整体测试结论</h2><ul><li>fastjson 序列化过后带有 $ 的引用标记也能够被 gson 正确的反序列化，但笔者并没有找到让 gson 序列化时转换成引用的配置</li><li>fastjson、hessian、java 均支持循环引用的解析；gson 不支持</li><li>fastjson 可以设置 DisableCircularReferenceDetect，关闭循环引用和重复引用的检测</li><li>gson 反序列化之前的同一个引用的对象，在经历了序列化再反序列化回来之后，不会被认为是同一个对象，可能会导致内存对象数量的膨胀；而 fastjson、java、hessian2 等序列化方式由于记录的是引用标记，不存在该问题</li><li>以笔者的测试 case 为例，hessian2 具有非常强大的序列化压缩比，适合大报文序列化后供网络传输的场景使用</li><li>以笔者的测试 case 为例，fastjson 具有非常高的吞吐量，对得起它的 fast，适合需要高吞吐的场景使用</li><li>序列化还需要考虑到是否支持循环引用，是否支持循环对象优化，是否支持枚举类型、集合、数组、子类、多态、内部类、泛型等综合场景，以及是否支持可视化等比较的场景，增删字段后的兼容性等等特性。综合来看，笔者比较推荐 hessian2 和 fastjson 两种序列化方式</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>大家都知道 fastjson 为了快，做了相对一些较为 hack 的逻辑，这也导致其漏洞较多，但我认为编码都是在 trade off 之中进行的，如果有一个完美的框架，那其他竞品框架早就不会存在了。笔者对各个序列化框架的研究也不深，可能你会说 jackson 更加优秀，我只能说能解决你的场景遇到的问题，那就是合适的框架。</p><p>最后，想要替换序列化框架时一定要慎重，了解清楚替代框架的特性，可能原先框架解决的问题，新的框架不一定能很好的 cover。</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;Json 序列化框架存在的安全漏洞一直以来都是程序员们挂在嘴边调侃的一个话题，尤其是这两年 fastjson 由于被针对性研究，更是频频地的报出漏洞，出个漏洞不要紧，可安全团队总是用邮件催着线上应用要进行依赖升级，这可就要命了，我相信很多小伙伴也是不胜其苦，考虑了使用其他序列化框架替换 fastjson。这不，最近我们就有一个项目将 fastjson 替换为了 gson，引发了一个线上的问题。分享下这次的经历，以免大家踩到同样的坑，在此警示大家，规范千万条，安全第一条，升级不规范，线上两行泪。&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;线上一个非常简单的逻辑，将对象序列化成 fastjson，再使用 HTTP 请求将字符串发送出去。原本工作的好好的，在将 fastjson 替换为 gson 之后，竟然引发了线上的 OOM。经过内存 dump 分析，发现竟然发送了一个 400 M+ 的报文，由于 HTTP 工具没有做发送大小的校验，强行进行了传输，直接导致了线上服务整体不可用。&lt;/p&gt;
    
    </summary>
    
      <category term="JAVA" scheme="https://lexburner.github.io/categories/JAVA/"/>
    
    
      <category term="JAVA" scheme="https://lexburner.github.io/tags/JAVA/"/>
    
  </entry>
  
  <entry>
    <title>三种思维，让我脱胎换骨</title>
    <link href="https://lexburner.github.io/three-thinking-ways/"/>
    <id>https://lexburner.github.io/three-thinking-ways/</id>
    <published>2020-08-30T13:04:52.000Z</published>
    <updated>2021-01-16T11:25:16.511Z</updated>
    
    <content type="html"><![CDATA[<h2 id="闭环思维"><a href="#闭环思维" class="headerlink" title="闭环思维"></a>闭环思维</h2><p><strong>闭环思维简而言之就是，如果别人发起一件事，你不管做的如何，最后都要闭环到这个发起者。</strong></p><p>我曾经就经历过这样的事：</p><p>我让一个实习生写一个问卷。 </p><p>两个小时过去了，没有反馈，三个小时过去了还是没有反馈，直到中午吃饭的时候，我碰到了他。</p><p>我问他，写完了吗？他说写完了。 </p><p>我说，那你怎么没发给我看，他说，写完直接发出去了。 </p><p><strong>你会对这个人怎么评价？不靠谱。</strong></p><p><strong>有闭环思维的人是怎么做呢？</strong></p><p>第一，如果他完成了，那么就要及时反馈，并且说一下当时的情景。 </p><p>第二，如果他没完成，也要及时反馈，是哪里遇到困难了，需不需要帮助。</p><p>以上无论哪一种情况，都有一个闭环，那就是由你本人发起又回到你的身上。</p><p><strong>不管做得如何，一定要给出一个反馈，形成闭环，有太多有才华的人就死在这一环节了。</strong></p><p>这件事我也没怪他，想想曾经的自己，也是一个“反馈黑洞”，这事交给我了，我最后给你弄完了就行了，老反馈多麻烦啊。最主要的是，减少反馈就多了偷懒的机会，自以为蒙混过关，其实已经被打上了不靠谱的标签。</p><p><strong>过去有句话说的分非常好：凡事有交代，件件有着落，事事有回应。这就是闭环思维。</strong></p><h2 id="成长型思维"><a href="#成长型思维" class="headerlink" title="成长型思维"></a>成长型思维</h2><p>成长型思维的人面对挑战与失败总是能够迎难而上；相反，成长型思维对应的另一种思维是固定型思维，他们面对挑战与失败总会下意识地回避。</p><p>曾经的我是一个不折不扣的固定型思维者：</p><p>小学时候觉得英语难，逃避，高中英语没及过格，大四才过四级；</p><p>中学时候因为一次物理考试没考好，对物理失去了兴趣，大学物理考了3次才过；</p><p>高中时候因为被喜欢的女生拒绝了，失去了追求异性的勇气，此后一段时间不敢和女生说话；</p><p>工作之后，领导交代一件事，我第一反应就是这个我不会，我做不了；</p><p>……</p><p>为什么会这样？</p><p><strong>归根结底是因为我害怕失败，害怕被否定</strong>，错误的把别人对某件事的否定当成了对自己这个人的否定。而不被否定的唯一方法就是不去做那件事。所以自己的能力圈就越来越小，进步的脚步也戛然而止。</p><p>后来我转变态度，我承认自己害怕失败，但我不服，越怕什么我就越做什么。</p><p>当初不是觉得自己学不好英语吗？我就去考研，虽然没考上，但我英语分数却不低，而且明显感觉英语并没有那么难学。</p><p>当初不是怕物理吗？我就去读物理相关的读物，我竟然还觉得很有意思，经常给人科普，别人经常会说：你怎么什么都知道啊？</p><p>当初不是不敢跟女生说话吗？我就疯狂和女生聊，线下搭讪，线上社交软件，虽然聊死了很多女生，但我却练就了和任何女生都能聊得来的能力。</p><p>……</p><p>做完之后我发现，失败是一件再正常不过的事，你能把它当成一座挡住你的大山，就可以做一个勇敢的登山人。</p><h2 id="批判性思维"><a href="#批判性思维" class="headerlink" title="批判性思维"></a>批判性思维</h2><p>所谓批判性思维，并不是指：</p><p>凡事先推翻立论，从反面去证明别人是错的；</p><p>以怀疑论者的思维方式，认为谁都是错的，谁说的都不在理；</p><p>传说中的杠精？怼天怼地怼空气。</p><p><strong>而是对自己的批判</strong></p><p>我们无法避免遇事时，总会以自己为标准去评判事物的正确性。这是与生俱来的惰性思维，却也总能通过自身的努力去降低偏见。</p><p>凡事要习惯回过头来三思。比如某个人和你讲一件事，你第一感觉可能觉得他完全在胡说八道，但是，一定要想第二遍，是否我错了，他对了？这一遍思考，一定不能假设自己是对的；</p><p>如果又想了第二遍，还是觉得自己对，对方错，</p><p>要想第三遍，是否是我的境界不够，不能理解他？为什么要想第三遍呢？<strong>因为任何一个想要精进的人，都要承认自己的无知，挑战自己的固有观念，拥抱新的观点。这就是批判性思维的真谛。</strong></p><p>对于自身的批判，最实用的，就是每日对自身的复盘。推敲一天内的工作生活是否符合自己的目标状态，是否还有遗漏或可以改进的地方。</p><p>作者：风茧<br>链接：<a href="https://www.zhihu.com/question/23913984/answer/754533449" target="_blank" rel="noopener">https://www.zhihu.com/question/23913984/answer/754533449</a></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;&lt;strong&gt;闭环思维简而言之就是，如果别人发起一件事，你不管做的如何，最后都要闭环到这个发起者。&lt;/strong&gt;&lt;/p
      
    
    </summary>
    
      <category term="随笔" scheme="https://lexburner.github.io/categories/%E9%9A%8F%E7%AC%94/"/>
    
    
      <category term="思维方式" scheme="https://lexburner.github.io/tags/%E6%80%9D%E7%BB%B4%E6%96%B9%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>lambda 表达式导致 Arthas 无法 redefine 的问题</title>
    <link href="https://lexburner.github.io/arthas-lambda-redefine/"/>
    <id>https://lexburner.github.io/arthas-lambda-redefine/</id>
    <published>2020-08-26T13:04:52.000Z</published>
    <updated>2021-01-16T11:25:16.421Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>原文出处：<a href="https://m.jb51.net/article/188155.htm" target="_blank" rel="noopener">https://m.jb51.net/article/188155.htm</a></p><p>作者：鲁严波</p></blockquote><p>这篇文章主要介绍了 lambda 表达式导致 Arthas 无法 redefine 的问题,本文通过图文实例相结合给大家介绍的非常详细，对大家的学习或工作具有一定的参考借鉴价值，需要的朋友可以参考下。</p><p>通过 arthas 的 redefine 命令，可以做到不用重新发布，就可以改变程序行为。</p><p>但是用多了，发现很多时候，我们就改了几行代码，甚至有的时候就添加了一行日志，就无法 redefine 了。提示：</p><blockquote><p>redefine error! java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method</p></blockquote><a id="more"></a><p><img src="https://img.jbzj.com/file_images/article/202006/202065165602467.png?202055165825" alt="img"></p><p>它提示我们新增加方法，那我们就看看是不是新增加了方法。通过 javap 来查看定义的方法：</p><p>这是老的类：</p><p><img src="https://img.jbzj.com/file_images/article/202006/202065165602468.jpg?202055165825" alt="img"></p><p>这是新的类：</p><p><img src="https://img.jbzj.com/file_images/article/202006/202065165602469.jpg?202055165825" alt="img"></p><p>对比之后发现，新的类，即本地编译的类，其中的 lambda 对应的方法名都是 lambda$getAllCity$0 这样的，最后的编号是从 0 开始的。</p><p>而旧的类，即现在在运行的类，其中的同一个 lambda 的方法名是 lambda$getAllCity$121，最后的编号是一个非常大的数字。</p><p>在仔细对比下，发现是 jdk 的版本问题，不同的 jdk 版本对与 lamdba 的处理可能不一致。</p><p>具体来说，线上编译的 jdk 版本是 1.8.0_66-b17， 而本地是 1.8.0_222-b10，而这两个版本对 lambda 对应的方法命名是不一样的。</p><p>首先，为了调试方便，写一个最小复现用例来看看：</p><figure class="highlight java"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Compile.java</span></span><br><span class="line"><span class="comment">// 编译LamdbaTest1.java和LamdbaTest2.java</span></span><br><span class="line"><span class="keyword">import</span> javax.tools.*;</span><br><span class="line"><span class="keyword">import</span> java.io.File;</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Compile</span> </span>&#123;</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">  String path1 = <span class="string">"/path/to/LamdbaTest1.java"</span>;</span><br><span class="line">  String path2 = <span class="string">"/path/to/LamdbaTest2.java"</span>;</span><br><span class="line">  JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();</span><br><span class="line">  DiagnosticCollector diagnostics = <span class="keyword">new</span> DiagnosticCollector();</span><br><span class="line">  StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(diagnostics, <span class="keyword">null</span>, <span class="keyword">null</span>);</span><br><span class="line">  Iterable&lt;? extends JavaFileObject&gt; compilationUnits = fileManager.getJavaFileObjects(</span><br><span class="line">    <span class="keyword">new</span> File(path1),</span><br><span class="line">    <span class="keyword">new</span> File(path2)</span><br><span class="line">  );</span><br><span class="line">  JavaCompiler.CompilationTask task = javaCompiler.getTask(<span class="keyword">null</span>, fileManager, diagnostics, <span class="keyword">null</span>, <span class="keyword">null</span>,</span><br><span class="line">    compilationUnits);</span><br><span class="line">  <span class="keyword">boolean</span> success = task.call();</span><br><span class="line">  System.out.println(success);</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//LamdbaTest1.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">LamdbaTest1</span> </span>&#123;</span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">test</span><span class="params">(Runnable runnable)</span> </span>&#123;</span><br><span class="line">  runnable.run();</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">()</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">  test(() -&gt; &#123;</span><br><span class="line">   System.out.println(<span class="number">11</span>);</span><br><span class="line">  &#125;);</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//LamdbaTest2.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">LamdbaTest2</span> </span>&#123;</span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">test</span><span class="params">(Runnable runnable)</span> </span>&#123;</span><br><span class="line">  runnable.run();</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">()</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">  test(() -&gt; &#123;</span><br><span class="line">   System.out.println(<span class="number">22</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>使用 1.8.0_222-b10（新版本 jdk）跑完了之后，发现 LamdbaTest2 中的 lambda 方法是：</p><p><img src="https://img.jbzj.com/file_images/article/202006/202065165602470.png?202055165825" alt="img"></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> lambda$main$<span class="number">0</span>();</span><br></pre></td></tr></table></figure><p>而换版本 1.8.0_66-b17（旧版本 jdk）之后，lambda 的方法就成了：</p><p><img src="https://img.jbzj.com/file_images/article/202006/202065165602471.jpg?202055165825" alt="img"></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> lambda$main$<span class="number">1</span>();</span><br></pre></td></tr></table></figure><p>多尝试几个文件同时编译，我们就可以发现：对于旧版本的 javac，末尾这个数字是全局递增的，50 个类有 100 个 lambda，那最后一个 lambda 的编号就是 99；而新的版本是每个类重新计数的，和总共多少个类没有关系。</p><p>确认了问题之后，接下来就是不断的打断点、重试了。后来发现不同版本的 javac 逻辑确实不同。</p><p>首先，查看 jdk 源码可以知道，lambda 的方法名都是：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lambda$&lt;methodname&gt;$&lt;lambdaCount&gt;</span><br></pre></td></tr></table></figure><p>代码见 <code>LambdaToMethod.java</code></p><p>不同的地方在于： 新版本的 javac，在处理一个新的类的时候，会保存上一个 lambdaCount，后续再恢复：</p><p><img src="https://img.jbzj.com/file_images/article/202006/202065165602472.jpg?202055165825" alt="img"></p><p>而旧版本则没有这个逻辑：</p><p><img src="https://img.jbzj.com/file_images/article/202006/202065165602473.jpg?202055165825" alt="img"></p><p>这就说明旧版本的编译器确实是 lambda 全局编号的。</p><p>那，问题来了，这个行为是从哪个版本变掉的呢？</p><p>对比之后发现这个变更是 jdk8u74-b02 引入的。对应的 bug 是 <a href="https://bugs.openjdk.java.net/browse/JDK-8067422，基本上就是每个类内的" target="_blank" rel="noopener">https://bugs.openjdk.java.net/browse/JDK-8067422，基本上就是每个类内的</a> lambda 单独编号，确保编译顺序不会影响 lambda 的方法名字。</p><p>所以，解决方案很简单，升级编译环境的 jdk 版本就好。</p><p>非常巧合的是，前两天为了更好的适配 Docker 运行环境（通俗的讲，就是在容器内获取到 docker 的 cpu 配额，而不是物理机器的 cpu 数量），我找运维添加了一个新的j dk 版本 1.8.0_231-b11，这样只需要直接将编译环境的 jdk 版本切换到 8u231 就行！</p>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;原文出处：&lt;a href=&quot;https://m.jb51.net/article/188155.htm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://m.jb51.net/article/188155.htm&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;作者：鲁严波&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这篇文章主要介绍了 lambda 表达式导致 Arthas 无法 redefine 的问题,本文通过图文实例相结合给大家介绍的非常详细，对大家的学习或工作具有一定的参考借鉴价值，需要的朋友可以参考下。&lt;/p&gt;
&lt;p&gt;通过 arthas 的 redefine 命令，可以做到不用重新发布，就可以改变程序行为。&lt;/p&gt;
&lt;p&gt;但是用多了，发现很多时候，我们就改了几行代码，甚至有的时候就添加了一行日志，就无法 redefine 了。提示：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;redefine error! java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
      <category term="Arthas" scheme="https://lexburner.github.io/categories/Arthas/"/>
    
    
      <category term="Arthas" scheme="https://lexburner.github.io/tags/Arthas/"/>
    
  </entry>
  
  <entry>
    <title>Dubbo 迈向云原生的里程碑 | 应用级服务发现</title>
    <link href="https://lexburner.github.io/dubbo-app-pubsub/"/>
    <id>https://lexburner.github.io/dubbo-app-pubsub/</id>
    <published>2020-06-15T16:33:50.000Z</published>
    <updated>2021-01-16T11:25:16.438Z</updated>
    
    <content type="html"><![CDATA[<h2 id="1-概述"><a href="#1-概述" class="headerlink" title="1 概述"></a>1 概述</h2><p>社区版本 Dubbo 从 2.7.5 版本开始，新引入了一种基于应用粒度的服务发现机制，这是 Dubbo 为适配云原生基础设施的一步重要探索。版本发布到现在已有近半年时间，经过这段时间的探索与总结，我们对这套机制的可行性与稳定性有了更全面、深入的认识；同时在 Dubbo 3.0 的规划也在全面进行中，如何让应用级服务发现成为未来下一代服务框架 Dubbo 3.0 的基础服务模型，解决云原生、规模化微服务集群扩容与可伸缩性问题，也已经成为我们当前工作的重点。</p><p>既然这套新机制如此重要，那它到底是怎么工作的，今天我们就来详细解读一下。在最开始的社区版本，我们给这个机制取了一个神秘的名字 - 服务自省，下文将进一步解释这个名字的由来，并引用服务自省代指这套应用级服务发现机制。</p><p>熟悉 Dubbo 开发者应该都知道，一直以来都是面向 RPC 方法去定义服务的，并且这也是 Dubbo 开发友好性、治理功能强的基础。既然如此，那我们为什么还要定义个应用粒度的服务发现机制那？这个机制到底是怎么工作的？它与当前机制的区别是什么？它能给我们带来哪些好处那？对适配云原生、性能提升又有哪些帮助？</p><p>带着所有的这些问题，我们开始本文的讲解。</p><a id="more"></a><h2 id="2-服务自省是什么？"><a href="#2-服务自省是什么？" class="headerlink" title="2 服务自省是什么？"></a>2 服务自省是什么？</h2><p>首先，我们先来解释文章开篇提到的问题：</p><ul><li>应用粒度服务发现是到底是一种怎样的模型，它与当前的 Dubbo 服务发现模型的区别是什么？</li><li>我们为什么叫它服务自省？</li></ul><p>所谓“应用/实例粒度” 或者“RPC 服务粒度”强调的是一种地址发现的数据组织格式。</p><p><img src="https://image.cnkirito.cn/46ddc6ead81d44fe1c68fb189a687f3e3fa60101.png" alt="1"></p><p>以 Dubbo 当前的地址发现数据格式为例，它是“RPC 服务粒度”的，它是以 RPC 服务作为 key，以实例列表作为 value 来组织数据的：</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></pre></td><td class="code"><pre><span class="line">&quot;RPC Service1&quot;: [</span><br><span class="line">  &#123;&quot;name&quot;:&quot;instance1&quot;, &quot;ip&quot;:&quot;127.0.0.1&quot;, &quot;metadata&quot;:&#123;&quot;timeout&quot;:1000&#125;&#125;,</span><br><span class="line">  &#123;&quot;name&quot;:&quot;instance2&quot;, &quot;ip&quot;:&quot;127.0.0.1&quot;, &quot;metadata&quot;:&#123;&quot;timeout&quot;:2000&#125;&#125;,</span><br><span class="line">  &#123;&quot;name&quot;:&quot;instance3&quot;, &quot;ip&quot;:&quot;127.0.0.1&quot;, &quot;metadata&quot;:&#123;&quot;timeout&quot;:3000&#125;&#125;,</span><br><span class="line">]</span><br><span class="line">&quot;RPC Service2&quot;: [Instance list of RPC Service2],</span><br><span class="line">&quot;RPC ServiceN&quot;: [Instance list of RPC ServiceN]</span><br></pre></td></tr></table></figure><p>而我们新引入的“应用粒度的服务发现”，它以应用名（Application）作为 key，以这个应用部署的一组实例（Instance）列表作为 value。这带来两点不同：</p><ol><li>数据映射关系变了，从 RPC Service -&gt; Instance 变为 Application -&gt; Instance</li><li>数据变少了，注册中心没有了 RPC Service 及其相关配置信息</li></ol><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">&quot;application1&quot;: [</span><br><span class="line">  &#123;&quot;name&quot;:&quot;instance1&quot;, &quot;ip&quot;:&quot;127.0.0.1&quot;, &quot;metadata&quot;:&#123;&#125;&#125;,</span><br><span class="line">  &#123;&quot;name&quot;:&quot;instance2&quot;, &quot;ip&quot;:&quot;127.0.0.1&quot;, &quot;metadata&quot;:&#123;&#125;&#125;,</span><br><span class="line">  &#123;&quot;name&quot;:&quot;instanceN&quot;, &quot;ip&quot;:&quot;127.0.0.1&quot;, &quot;metadata&quot;:&#123;&#125;&#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>要进一步理解新模型带来的变化，我们看一下应用与 RPC 服务间的关系，显而易见的，1 个应用内可能会定义 n 个 RPC Service。因此 Dubbo 之前的服务发现粒度更细，在注册中心产生的数据条目也会更多（与 RPC 服务成正比），同时也存在一定的数据冗余。</p><p>简单理解了应用级服务发现的基本机制，接着解释它为什么会被叫做“服务自省”？其实这还是得从它的工作原理说起，上面我们提到，应用粒度服务发现的数据模型有几个以下明显变化：数据中心的数据量少了，RPC 服务相关的数据在注册中心没有了，现在只有 application - instance 这两个层级的数据。为了保证这部分缺少的 RPC 服务数据仍然能被 Consumer 端正确的感知，我们在 Consumer 和 Provider 间建立了一条单独的通信通道：<strong>Consumer 和 Provider 两两之间通过特定端口交换信息</strong>，我们把这种 Provider 自己主动暴露自身信息的行为认为是一种内省机制，因此从这个角度出发，我们把整个机制命名为：服务自省。</p><h2 id="3-为什么需要服务自省？"><a href="#3-为什么需要服务自省？" class="headerlink" title="3 为什么需要服务自省？"></a>3 为什么需要服务自省？</h2><p>上面讲服务自省的大概原理的时候也提到了它给注册中心带来的几点不同，这几点不同体现在 Dubbo 框架侧（甚至整个微服务体系中），有以下优势：</p><ol><li>与业界主流微服务模型对齐，比如 SpringCloud、Kubernetes Native Service等。</li><li>提升性能与可伸缩性。注册中心数据的重新组织（减少），能最大幅度的减轻注册中心的存储、推送压力，进而减少 Dubbo Consumer 侧的地址计算压力；集群规模也开始变得可预测、可评估（与 RPC 接口数量无关，只与实例部署规模相关）。</li></ol><h3 id="3-1-对齐主流微服务模型"><a href="#3-1-对齐主流微服务模型" class="headerlink" title="3.1 对齐主流微服务模型"></a>3.1 对齐主流微服务模型</h3><p>自动、透明的实例地址发现（负载均衡）是所有微服务框架需要解决的事情，这能让后端的部署结构对上游微服务透明，上游服务只需要从收到的地址列表中选取一个，发起调用就可以了。要实现以上目标，涉及两个关键点的自动同步：</p><ul><li>实例地址，服务消费方需要知道地址以建立链接</li><li>RPC 方法定义，服务消费方需要知道 RPC 服务的具体定义，不论服务类型是 rest 或 rmi 等。</li></ul><p><img src="https://image.cnkirito.cn/de21f1ab3366f3c1ecaa648f0ad6e0d84aac7440.png" alt="2"></p><p>对于 RPC 实例间借助注册中心的数据同步，REST 定义了一套非常有意思的成熟度模型，感兴趣的朋友可以参考这里的链接 <a href="https://yq.aliyun.com/go/articleRenderRedirect?url=https%3A%2F%2Fwww.martinfowler.com%2Farticles%2FrichardsonMaturityModel.html" target="_blank" rel="noopener">https://www.martinfowler.com/articles/richardsonMaturityModel.html</a>， 按照文章中的 4 级成熟度定义，Dubbo 当前基于接口粒度的模型可以对应到 L4 级别。</p><p>接下来，我们看看 Dubbo、SpringCloud 以及 Kubernetes 分别是怎么围绕自动化的实例地址发现这个目标设计的。</p><p><strong>1. Spring Cloud</strong></p><p>Spring Cloud 通过注册中心只同步了应用与实例地址，消费方可以基于实例地址与服务提供方建立链接，但是消费方对于如何发起 http 调用（SpringCloud 基于 rest 通信）一无所知，比如对方有哪些 http endpoint，需要传入哪些参数等。</p><p>RPC 服务这部分信息目前都是通过线下约定或离线的管理系统来协商的。这种架构的优缺点总结如下。<br>优势：部署结构清晰、地址推送量小；<br>缺点：地址订阅需要指定应用名， provider 应用变更（拆分）需消费端感知；RPC 调用无法全自动同步。</p><p><img src="https://image.cnkirito.cn/51d71432a1411e85c0f4ed483c5a6739bcd7909c.png" alt="3"></p><p><strong>2. Dubbo</strong></p><p>Dubbo 通过注册中心同时同步了实例地址和 RPC 方法，因此其能实现 RPC 过程的自动同步，面向 RPC 编程、面向 RPC 治理，对后端应用的拆分消费端无感知，其缺点则是地址推送数量变大，和 RPC 方法成正比。</p><p><img src="https://image.cnkirito.cn/470ae02261cec1b1884737bd703222507027d184.png" alt="4"></p><p><strong>3. Dubbo + Kubernetes</strong></p><p>Dubbo 要支持 Kubernetes native service，相比之前自建注册中心的服务发现体系来说，在工作机制上主要有两点变化：</p><ul><li>服务注册由平台接管，provider 不再需要关心服务注册</li><li>consumer 端服务发现将是 Dubbo 关注的重点，通过对接平台层的 API-Server、DNS 等，Dubbo client 可以通过一个 <a href="https://yq.aliyun.com/go/articleRenderRedirect?url=https%3A%2F%2Fkubernetes.io%2Fdocs%2Fconcepts%2Fservices-networking%2Fservice%2F" target="_blank" rel="noopener">Service Name</a>（通常对应到 Application Name）查询到一组 <a href>Endpoints</a>（一组运行 provider 的 pod），通过将 Endpoints 映射到 Dubbo 内部地址列表，以驱动 Dubbo 内置的负载均衡机制工作。</li></ul><blockquote><p>Kubernetes Service 作为一个抽象概念，怎么映射到 Dubbo 是一个值得讨论的点</p><ul><li><p>Service Name - &gt; Application Name，Dubbo 应用和 Kubernetes 服务一一对应，对于微服务运维和建设环节透明，与开发阶段解耦。</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><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&gt;   apiVersion: v1</span><br><span class="line">&gt;   kind: Service</span><br><span class="line">&gt;   metadata:</span><br><span class="line">&gt;     name: provider-app-name</span><br><span class="line">&gt;   spec:</span><br><span class="line">&gt;     selector:</span><br><span class="line">&gt;   app: provider-app-name</span><br><span class="line">&gt;     ports:</span><br><span class="line">&gt;   - protocol: TCP</span><br><span class="line">&gt;   port: </span><br><span class="line">&gt;   targetPort: 9376</span><br><span class="line">&gt;</span><br></pre></td></tr></table></figure></li></ul></blockquote><blockquote><ul><li><p>Service Name - &gt; Dubbo RPC Service，Kubernetes 要维护调度的服务与应用内建 RPC 服务绑定，维护的服务数量变多。</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><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></pre></td><td class="code"><pre><span class="line">&gt;   ---</span><br><span class="line">&gt;   apiVersion: v1</span><br><span class="line">&gt;   kind: Service</span><br><span class="line">&gt;   metadata:</span><br><span class="line">&gt;     name: rpc-service-1</span><br><span class="line">&gt;   spec:</span><br><span class="line">&gt;     selector:</span><br><span class="line">&gt;       app: provider-app-name</span><br><span class="line">&gt;     ports: ##</span><br><span class="line">&gt;   ...</span><br><span class="line">&gt;   ---</span><br><span class="line">&gt;   apiVersion: v1</span><br><span class="line">&gt;   kind: Service</span><br><span class="line">&gt;   metadata:</span><br><span class="line">&gt;     name: rpc-service-2</span><br><span class="line">&gt;   spec:</span><br><span class="line">&gt;     selector:</span><br><span class="line">&gt;       app: provider-app-name</span><br><span class="line">&gt;     ports: ##</span><br><span class="line">&gt;   ...</span><br><span class="line">&gt;   ---</span><br><span class="line">&gt;   apiVersion: v1</span><br><span class="line">&gt;   kind: Service</span><br><span class="line">&gt;   metadata:</span><br><span class="line">&gt;     name: rpc-service-N</span><br><span class="line">&gt;   spec:</span><br><span class="line">&gt;     selector:</span><br><span class="line">&gt;       app: provider-app-name</span><br><span class="line">&gt;     ports: ##</span><br><span class="line">&gt;   ...</span><br><span class="line">&gt;</span><br></pre></td></tr></table></figure></li></ul></blockquote><p><img src="https://image.cnkirito.cn/b986d8df9f93eeb6d985e37604225dd68984f4db.png" alt="5"></p><p>结合以上几种不同微服务框架模型的分析，我们可以发现，Dubbo 与 SpringCloud、Kubernetes 等不同产品在微服务的抽象定义上还是存在很大不同的。SpringCloud 和 Kubernetes 在微服务的模型抽象上还是比较接近的，两者基本都只关心实例地址的同步，如果我们去关心其他的一些服务框架产品，会发现它们绝大多数也是这么设计的；</p><blockquote><p>即 REST 成熟度模型中的 L3 级别。</p></blockquote><p>对比起来 Dubbo 则相对是比较特殊的存在，更多的是从 RPC 服务的粒度去设计的。</p><blockquote><p>对应 REST 成熟度模型中的 L4 级别。</p></blockquote><p>如我们上面针对每种模型做了详细的分析，每种模型都有其优势和不足。而我们最初决定 Dubbo 要做出改变，往其他的微服务发现模型上的对齐，是我们最早在确定 Dubbo 的云原生方案时，我们发现要让 Dubbo 去支持 Kubernetes Native Service，模型对齐是一个基础条件；另一点是来自用户侧对 Dubbo 场景化的一些工程实践的需求，得益于 Dubbo 对多注册、多协议能力的支持，使得 Dubbo 联通不同的微服务体系成为可能，而服务发现模型的不一致成为其中的一个障碍，这部分的场景描述请参见以下文章：<a href="https://yq.aliyun.com/go/articleRenderRedirect?url=https%3A%2F%2Fwww.atatech.org%2Farticles%2F157719" target="_blank" rel="noopener">https://www.atatech.org/articles/157719</a></p><h3 id="3-2-更大规模的微服务集群-解决性能瓶颈"><a href="#3-2-更大规模的微服务集群-解决性能瓶颈" class="headerlink" title="3.2 更大规模的微服务集群 - 解决性能瓶颈"></a>3.2 更大规模的微服务集群 - 解决性能瓶颈</h3><p>这部分涉及到和注册中心、配置中心的交互，关于不同模型下注册中心数据的变化，之前原理部分我们简单分析过。为更直观的对比服务模型变更带来的推送效率提升，我们来通过一个示例看一下不同模型注册中心的对比：</p><p><img src="https://image.cnkirito.cn/c275bcf54f023eeac0e63d90e5068a6ec2686893.png" alt="6"></p><p>图中左边是微服务框架的一个典型工作流程，Provider 和 Consumer 通过注册中心实现自动化的地址通知。其中，Provider 实例的信息如图中表格所示：</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">应用 DEMO 包含三个接口 DemoService 1 2 3，当前实例的 ip 地址为 10.210.134.30。</span><br></pre></td></tr></table></figure><ul><li>对于 Spring Cloud 和 Kubernetes 模型，注册中心只会存储一条 <code>DEMO - 10.210.134.30+metadata</code> 的数据；</li><li>对于老的 Dubbo 模型，注册中心存储了三条接口粒度的数据，分别对应三个接口 DemoService 1 2 3，并且很多的址数据都是重复的；</li></ul><p>可以总结出，基于应用粒度的模型所存储和推送的数据量是和应用、实例数成正比的，只有当我们的应用数增多或应用的实例数增长时，地址推送压力才会上涨。<br>而对于基于接口粒度的模型，数据量是和接口数量正相关的，鉴于一个应用通常发布多个接口的现状，这个数量级本身比应用粒度是要乘以倍数的；另外一个关键点在于，接口粒度导致的集群规模评估的不透明，相对于实i例、应用增长都通常是在运维侧的规划之中，接口的定义更多的是业务侧的内部行为，往往可以绕过评估给集群带来压力。</p><p>以 Consumer 端服务订阅举例，根据我对社区部分 Dubbo 中大规模头部用户的粗略统计，根据受统计公司的实际场景，一个 Consumer 应用要消费（订阅）的 Provier 应用数量往往要超过 10 个，而具体到其要消费（订阅）的的接口数量则通常要达到 30 个，平均情况下 Consumer 订阅的 3 个接口来自同一个 Provider 应用，如此计算下来，如果以应用粒度为地址通知和选址基本单位，则平均地址推送和计算量将下降 60% 还要多，<br>而在极端情况下，也就是当 Consumer 端消费的接口更多的来自同一个应用时，这个地址推送与内存消耗的占用将会进一步得到降低，甚至可以超过 80% 以上。</p><p>一个典型的几段场景即是 Dubbo 体系中的网关型应用，有些网关应用消费（订阅）达 100+ 应用，而消费（订阅）的服务有 1000+ ，平均有 10 个接口来自同一个应用，如果我们把地址推送和计算的粒度改为应用，则地址推送量从原来的 n <em>1000 变为 n</em> 100，地址数量降低可达近 90%。</p><h2 id="工作原理"><a href="#工作原理" class="headerlink" title="工作原理"></a>工作原理</h2><h3 id="设计原则"><a href="#设计原则" class="headerlink" title="设计原则"></a>设计原则</h3><p>上面一节我们从<strong>服务模型</strong>及<strong>支撑大规模集群</strong>的角度分别给出了 Dubbo 往应用级服务发现靠拢的好处或原因，但这么做的同时接口粒度的服务治理能力还是要继续保留，这是 Dubbo 框架编程模型易用性、服务治理能力优势的基础。<br>以下是我认为我们做服务模型迁移仍要坚持的设计原则</p><ul><li>新的服务发现模型要实现对原有 Dubbo 消费端开发者的无感知迁移，即 Dubbo 继续面向 RPC 服务编程、面向 RPC 服务治理，做到对用户侧完全无感知。</li><li>建立 Consumer 与 Provider 间的自动化 RPC 服务元数据协调机制，解决传统微服务模型无法同步 RPC 级接口配置的缺点。</li></ul><h3 id="基本原理详解"><a href="#基本原理详解" class="headerlink" title="基本原理详解"></a>基本原理详解</h3><p>应用级服务发现作为一种新的服务发现机制，和以前 Dubbo 基于 RPC 服务粒度的服务发现在核心流程上基本上是一致的：即服务提供者往注册中心注册地址信息，服务消费者从注册中心拉取&amp;订阅地址信息。</p><p>这里主要的不同有以下两点：</p><h4 id="注册中心数据以“应用-实例列表”格式组织，不再包含-RPC-服务信息"><a href="#注册中心数据以“应用-实例列表”格式组织，不再包含-RPC-服务信息" class="headerlink" title="注册中心数据以“应用 - 实例列表”格式组织，不再包含 RPC 服务信息"></a>注册中心数据以“应用 - 实例列表”格式组织，不再包含 RPC 服务信息</h4><p><img src="https://image.cnkirito.cn/82c0d6c6222e4f582a275a70ff52332dcb5dba20.png" alt="7"></p><p>以下是每个 Instance metadata 的示例数据，总的原则是 metadata 只包含当前 instance 节点相关的信息，不涉及 RPC 服务粒度的信息。</p><p>总体信息概括如下：实例地址、实例各种环境标、metadata service 元数据、其他少量必要属性。</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><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">&#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"provider-app-name"</span>,</span><br><span class="line">    <span class="attr">"id"</span>: <span class="string">"192.168.0.102:20880"</span>,</span><br><span class="line">    <span class="attr">"address"</span>: <span class="string">"192.168.0.102"</span>,</span><br><span class="line">    <span class="attr">"port"</span>: <span class="number">20880</span>,</span><br><span class="line">    <span class="attr">"sslPort"</span>: <span class="literal">null</span>,</span><br><span class="line">    <span class="attr">"payload"</span>: &#123;</span><br><span class="line">        <span class="attr">"id"</span>: <span class="literal">null</span>,</span><br><span class="line">        <span class="attr">"name"</span>: <span class="string">"provider-app-name"</span>,</span><br><span class="line">        <span class="attr">"metadata"</span>: &#123;</span><br><span class="line">            <span class="attr">"metadataService"</span>: <span class="string">"&#123;\"dubbo\":&#123;\"version\":\"1.0.0\",\"dubbo\":\"2.0.2\",\"release\":\"2.7.5\",\"port\":\"20881\"&#125;&#125;"</span>,</span><br><span class="line">            <span class="attr">"endpoints"</span>: <span class="string">"[&#123;\"port\":20880,\"protocol\":\"dubbo\"&#125;]"</span>,</span><br><span class="line">            <span class="attr">"storage-type"</span>: <span class="string">"local"</span>,</span><br><span class="line">            <span class="attr">"revision"</span>: <span class="string">"6785535733750099598"</span>,</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"registrationTimeUTC"</span>: <span class="number">1583461240877</span>,</span><br><span class="line">    <span class="attr">"serviceType"</span>: <span class="string">"DYNAMIC"</span>,</span><br><span class="line">    <span class="attr">"uriSpec"</span>: <span class="literal">null</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Client-–-Server-自行协商-RPC-方法信息"><a href="#Client-–-Server-自行协商-RPC-方法信息" class="headerlink" title="Client – Server 自行协商 RPC 方法信息"></a>Client – Server 自行协商 RPC 方法信息</h4><p>在注册中心不再同步 RPC 服务信息后，服务自省在服务消费端和提供端之间建立了一条内置的 RPC 服务信息协商机制，这也是“服务自省”这个名字的由来。服务端实例会暴露一个预定义的 MetadataService RPC 服务，消费端通过调用 MetadataService 获取每个实例 RPC 方法相关的配置信息。</p><p><img src="https://image.cnkirito.cn/89e96f895f76912a7c2d1a6e8b44643176f3c431.png" alt="8"></p><p>当前 MetadataService 返回的数据格式如下，</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><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line">  <span class="string">"dubbo://192.168.0.102:20880/org.apache.dubbo.demo.DemoService?anyhost=true&amp;application=demo-provider&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;interface=org.apache.dubbo.demo.DemoService&amp;methods=sayHello&amp;pid=9585&amp;release=2.7.5&amp;side=provider&amp;timestamp=1583469714314"</span>, </span><br><span class="line"> <span class="string">"dubbo://192.168.0.102:20880/org.apache.dubbo.demo.HelloService?anyhost=true&amp;application=demo-provider&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;interface=org.apache.dubbo.demo.DemoService&amp;methods=sayHello&amp;pid=9585&amp;release=2.7.5&amp;side=provider&amp;timestamp=1583469714314"</span>,</span><br><span class="line">  <span class="string">"dubbo://192.168.0.102:20880/org.apache.dubbo.demo.WorldService?anyhost=true&amp;application=demo-provider&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;interface=org.apache.dubbo.demo.DemoService&amp;methods=sayHello&amp;pid=9585&amp;release=2.7.5&amp;side=provider&amp;timestamp=1583469714314"</span></span><br><span class="line">]</span><br></pre></td></tr></table></figure><blockquote><p>熟悉 Dubbo 基于 RPC 服务粒度的服务发现模型的开发者应该能看出来，服务自省机制机制将以前注册中心传递的 URL 一拆为二：</p><ul><li>一部分和实例相关的数据继续保留在注册中心，如 ip、port、机器标识等。</li><li>另一部分和 RPC 方法相关的数据从注册中心移除，转而通过 MetadataService 暴露给消费端。</li></ul><p><strong>理想情况下是能达到数据按照实例、RPC 服务严格区分开来，但明显可以看到以上实现版本还存在一些数据冗余，有些也数据还未合理划分。尤其是 MetadataService 部分，其返回的数据还只是简单的 URL 列表组装，这些 URL其实是包含了全量的数据。</strong></p></blockquote><p>以下是服务自省的一个完整工作流程图，详细描述了服务注册、服务发现、MetadataService、RPC 调用间的协作流程。</p><p><img src="https://image.cnkirito.cn/e8c286b5e79d9c085431840d3ee57537ece95d2e.png" alt="9"></p><ul><li>服务提供者启动，首先解析应用定义的“普通服务”并依次注册为 RPC 服务，紧接着注册内建的 MetadataService 服务，最后打开 TCP 监听端口。</li><li>启动完成后，将实例信息注册到注册中心（仅限 ip、port 等实例相关数据），提供者启动完成。</li><li>服务消费者启动，首先依据其要“消费的 provider 应用名”到注册中心查询地址列表，并完成订阅（以实现后续地址变更自动通知）。</li><li>消费端拿到地址列表后，紧接着对 MetadataService 发起调用，返回结果中包含了所有应用定义的“普通服务”及其相关配置信息。</li><li>至此，消费者可以接收外部流量，并对提供者发起 Dubbo RPC 调用</li></ul><blockquote><p>在以上流程中，我们只考虑了一切顺利的情况，但在更详细的设计或编码实现中，我们还需要严格约定一些异常场景下的框架行为。比如，如果消费者 MetadataService 调用失败，则在重试知道成功之前，消费者将不可以接收外部流量。</p></blockquote><h3 id="服务自省中的关键机制"><a href="#服务自省中的关键机制" class="headerlink" title="服务自省中的关键机制"></a>服务自省中的关键机制</h3><h4 id="元数据同步机制"><a href="#元数据同步机制" class="headerlink" title="元数据同步机制"></a>元数据同步机制</h4><p>Client 与 Server 间在收到地址推送后的配置同步是服务自省的关键环节，目前针对元数据同步有两种具体的可选方案，分别是：</p><ul><li>内建 MetadataService。</li><li>独立的元数据中心，通过中细化的元数据集群协调数据。</li></ul><p><strong>1. 内建 MetadataService</strong><br>MetadataService 通过标准的 Dubbo 协议暴露，根据查询条件，会将内存中符合条件的“普通服务”配置返回给消费者。这一步发生在消费端选址和调用前。</p><p><strong>元数据中心</strong><br>复用 2.7 版本中引入的元数据中心，provider 实例启动后，会尝试将内部的 RPC 服务组织成元数据的格式到元数据中心，而 consumer 则在每次收到注册中心推送更新后，主动查询元数据中心。</p><blockquote><p>注意 consumer 端查询元数据中心的时机，是等到注册中心的地址更新通知之后。也就是通过注册中心下发的数据，我们能明确的知道何时某个实例的元数据被更新了，此时才需要去查元数据中心。</p></blockquote><p><img src="https://image.cnkirito.cn/7a2c72bc9f5e541e8a155a01b303d4c77590a65a.png" alt="10"></p><h4 id="RPC-服务-lt-gt-应用映射关系"><a href="#RPC-服务-lt-gt-应用映射关系" class="headerlink" title="RPC 服务 &lt; - &gt; 应用映射关系"></a>RPC 服务 &lt; - &gt; 应用映射关系</h4><p>回顾上文讲到的注册中心关于“应用 - 实例列表”结构的数据组织形式，这个变动目前对开发者并不是完全透明的，业务开发侧会感知到查询/订阅地址列表的机制的变化。具体来说，相比以往我们基于 RPC 服务来检索地址，现在 consumer 需要通过指定 provider 应用名才能实现地址查询或订阅。</p><p>老的 Consumer 开发与配置示例：</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">&lt;!-- 框架直接通过 RPC Service 1/2/N 去注册中心查询或订阅地址列表 --&gt;</span><br><span class="line">&lt;dubbo:registry address=&quot;zookeeper://127.0.0.1:2181&quot;/&gt;</span><br><span class="line">&lt;dubbo:reference interface=&quot;RPC Service 1&quot; /&gt;</span><br><span class="line">&lt;dubbo:reference interface=&quot;RPC Service 2&quot; /&gt;</span><br><span class="line">&lt;dubbo:reference interface=&quot;RPC Service N&quot; /&gt;</span><br></pre></td></tr></table></figure><p>新的 Consumer 开发与配置示例：</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">&lt;!-- 框架需要通过额外的 provided-by=&quot;provider-app-x&quot; 才能在注册中心查询或订阅到地址列表 --&gt;</span><br><span class="line">&lt;dubbo:registry address=&quot;zookeeper://127.0.0.1:2181?registry-type=service&quot;/&gt;</span><br><span class="line">&lt;dubbo:reference interface=&quot;RPC Service 1&quot; provided-by=&quot;provider-app-x&quot;/&gt;</span><br><span class="line">&lt;dubbo:reference interface=&quot;RPC Service 2&quot; provided-by=&quot;provider-app-x&quot; /&gt;</span><br><span class="line">&lt;dubbo:reference interface=&quot;RPC Service N&quot; provided-by=&quot;provider-app-y&quot; /&gt;</span><br></pre></td></tr></table></figure><p>以上指定 provider 应用名的方式是 Spring Cloud 当前的做法，需要 consumer 端的开发者显示指定其要消费的 provider 应用。</p><p>以上问题的根源在于注册中心不知道任何 RPC 服务相关的信息，因此只能通过应用名来查询。</p><p>为了使整个开发流程对老的 Dubbo 用户更透明，同时避免指定 provider 对可扩展性带来的影响（参见下方说明），我们设计了一套 <code>RPC 服务到应用名</code>的映射关系，以尝试在 consumer 自动完成 RPC 服务到 provider 应用名的转换。</p><p><img src="https://image.cnkirito.cn/4a92841e861c49424a2635dfb695e58ea78f70ee.png" alt="11"></p><blockquote><p>Dubbo 之所以选择建立一套“接口-应用”的映射关系，主要是考虑到 service - app 映射关系的不确定性。一个典型的场景即是应用/服务拆分，如上面提到的配置<code>，PC Service 2 是定义于 provider-app-x 中的一个服务，未来它随时可能会被开发者分拆到另外一个新的应用如 provider-app-x-1 中，这个拆分要被所有的 PC Service 2 消费方感知到，并对应用进行修改升级，如改为</code>，这样的升级成本不可否认还是挺高的。<br>到底是 Dubbo 框架帮助开发者透明的解决这个问题，还是交由开发者自己去解决，当然这只是个策略选择问题，并且 Dubbo 2.7.5+ 版本目前是都提供了的。其实我个人更倾向于交由业务开发者通过组织上的约束来做，这样也可进一步降低 Dubbo 框架的复杂度，提升运行态的稳定性。</p></blockquote><h2 id="总结与展望"><a href="#总结与展望" class="headerlink" title="总结与展望"></a>总结与展望</h2><p>应用级服务发现机制是 Dubbo 面向云原生走出的重要一步，它帮 Dubbo 打通了与其他微服务体系之间在地址发现层面的鸿沟，也成为 Dubbo 适配 Kubernetes Native Service 等基础设施的基础。我们期望 Dubbo 在新模型基础上，能继续保留在编程易用性、服务治理能力等方面强大的优势。但是我们也应该看到应用粒度的模型一方面带来了新的复杂性，需要我们继续去优化与增强；另一方面，除了地址存储与推送之外，应用粒度在帮助 Dubbo 选址层面也有进一步挖掘的潜力。</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;1-概述&quot;&gt;&lt;a href=&quot;#1-概述&quot; class=&quot;headerlink&quot; title=&quot;1 概述&quot;&gt;&lt;/a&gt;1 概述&lt;/h2&gt;&lt;p&gt;社区版本 Dubbo 从 2.7.5 版本开始，新引入了一种基于应用粒度的服务发现机制，这是 Dubbo 为适配云原生基础设施的一步重要探索。版本发布到现在已有近半年时间，经过这段时间的探索与总结，我们对这套机制的可行性与稳定性有了更全面、深入的认识；同时在 Dubbo 3.0 的规划也在全面进行中，如何让应用级服务发现成为未来下一代服务框架 Dubbo 3.0 的基础服务模型，解决云原生、规模化微服务集群扩容与可伸缩性问题，也已经成为我们当前工作的重点。&lt;/p&gt;
&lt;p&gt;既然这套新机制如此重要，那它到底是怎么工作的，今天我们就来详细解读一下。在最开始的社区版本，我们给这个机制取了一个神秘的名字 - 服务自省，下文将进一步解释这个名字的由来，并引用服务自省代指这套应用级服务发现机制。&lt;/p&gt;
&lt;p&gt;熟悉 Dubbo 开发者应该都知道，一直以来都是面向 RPC 方法去定义服务的，并且这也是 Dubbo 开发友好性、治理功能强的基础。既然如此，那我们为什么还要定义个应用粒度的服务发现机制那？这个机制到底是怎么工作的？它与当前机制的区别是什么？它能给我们带来哪些好处那？对适配云原生、性能提升又有哪些帮助？&lt;/p&gt;
&lt;p&gt;带着所有的这些问题，我们开始本文的讲解。&lt;/p&gt;
    
    </summary>
    
      <category term="RPC" scheme="https://lexburner.github.io/categories/RPC/"/>
    
    
      <category term="DUBBO" scheme="https://lexburner.github.io/tags/DUBBO/"/>
    
  </entry>
  
  <entry>
    <title>平滑迁移 Dubbo 服务的思考</title>
    <link href="https://lexburner.github.io/dubbo-migration/"/>
    <id>https://lexburner.github.io/dubbo-migration/</id>
    <published>2020-05-30T15:17:38.000Z</published>
    <updated>2021-01-16T11:25:16.442Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>近日，有报道称在 HashCorp 的商业软件试用协议上发现，旗下所有商业产品禁止在中国境内使用、部署、安装，这其中就包含了 Terraform, Consul, Vagrant 等众多知名软件，其中 Consul 是一个在微服务领域的开源软件，可以用于做注册发现、配置管理等场景。</p><p>该新闻在国内发酵后，有人在 Twitter上咨询了HashCorp 公司的创始人，得到的回复是影响的软件仅限于 Vault 这款加密软件，目前 HashCorp 公司的官方网站上已经更新了相关的条款，明确了受影响的产品仅限 Vault 这一款产品。</p><h2 id="Consul-开源版是否收到影响？"><a href="#Consul-开源版是否收到影响？" class="headerlink" title="Consul 开源版是否收到影响？"></a>Consul 开源版是否收到影响？</h2><p>上面的条款里只提到了商业软件，那么开源的 Consul 是否受到影响呢？在 Github 的 Consul 仓库上，可以得知项目的 license 是 <code>Mozilla Public License 2.0</code> ，这款许可证在 Apache 官网上是 <code>Category B</code> , 属于 <code>Weak Copy Left</code> 许可，那么它有哪些特点呢？</p><p><img src="https://image.cnkirito.cn/1590754178611.png" alt="License"></p><ol><li>任何可以使用，复制，修改，重新分发该代码，包括商业目的使用。</li><li>如果修改了 MPL 协议许可下的源码，再重新发布这部分源码的话，必须保留原来 MPL 许可证不得更换。</li><li>如果基于该项目衍生出更大的项目，那么这部分工作可以使用新许可证的方式进行分发，只要没有修改原来 MPL 许可下的代码。（这也是为什么 Apache 项目的分发的源码中可以包含 MPL 协议下二进制文件的原因）</li></ol><p>可以看到，MPL 通常被认为是介于 Apache License 和 GPL/LGPL 之间的一个折中方案。相对于 Apache License，MPL 2.0 要求修改了源码必须保持相同协议；相对于 GPL/LGPL, MPL 2.0 可以商用，同时衍生的作品在一定条件下也可以更换许可证类型。</p><p>总体来看的话，开源版 Consul 无论是私用还是商用都是不受限制的。但这也可能是一个警钟，如果对 Consul 还是有所顾忌的话，如何替代掉它呢？</p><p>在微服务领域，Consul 主要被用来做充当注册中心和配置中心，例如 Dubbo 和 SpringCloud 都有对应的支持。本文便以这个事为一个引子，介绍如何平滑地迁移 Dubbo 服务，达到替换注册中心的效果。</p><a id="more"></a><h2 id="平滑迁移服务的定义和意义"><a href="#平滑迁移服务的定义和意义" class="headerlink" title="平滑迁移服务的定义和意义"></a>平滑迁移服务的定义和意义</h2><p>如果 Dubbo 应用已经部署到生产环境并处于正常运行状态中，此时想将应用的注册中心替换，那么在迁移过程中，保证业务的平稳运行不中断一定是第一要义。我们将保证应用运行不中断，并最终达成注册中心替换的过程称为平滑迁移。可以类比为给飞行中的飞机替换引擎，在项目升级、框架调整等很多时候，现状和终态之间往往都有一个过度方案。</p><ul><li><p>平滑迁移可以避免终态方案一次性上线后出现和原有方案的不兼容性，规避了整体回归的风险</p></li><li><p>没有哪个互联网公司可以承担的起：“自 xx 至 xx，系统维护一小时，期间服务将无法提供，请广大用户谅解” 这种停机升级方案。</p></li></ul><h2 id="平滑迁移过程"><a href="#平滑迁移过程" class="headerlink" title="平滑迁移过程"></a>平滑迁移过程</h2><p>说到注册中心迁移，可能很多人第一时间都能想到双注册双订阅这种方案</p><blockquote><p>双注册和双订阅迁移方案是指在应用迁移时同时接入两个注册中心（原有注册中心和新注册中心）以保证已迁移的应用和未迁移的应用之间的相互调用。</p></blockquote><p>以 Consul 迁移到 Nacos 为例：</p><p>在迁移态下，一共有两种应用类型：未迁移应用，迁移中应用。我们所说的双注册双订阅都是指的【迁移中应用】。明白下面几个点，平滑迁移的过程一下子就清晰了：</p><ul><li>【未迁移应用】不做任何改动</li><li>为了让【未迁移应用】调用到【迁移中应用】，要求【迁移中应用】不仅要将数据写到 Nacos，还要写回旧的 Consul，这是双注册</li><li><p>为了让【迁移中应用】调用到【未迁移应用】，要求【迁移中应用】不仅要订阅 Nacos 的数据，还要监听旧的 Consul，这是双订阅</p></li><li><p>当所有应用变成【迁移中应用】时，旧的 Consul 就可以光荣下岗了，至此平滑迁移完成。</p></li></ul><p>在这个过程中，还可以灵活的变换一些规则，例如在迁移中后期，大部分应用在 Nacos 中已经有服务了，可以切换双订阅为单订阅，以验证迁移情况。并且在真实场景下，还会并存配置中心、元数据中心的迁移，过程会更加复杂。</p><h2 id="Dubbo-平滑迁移方案-–-多注册中心"><a href="#Dubbo-平滑迁移方案-–-多注册中心" class="headerlink" title="Dubbo 平滑迁移方案 – 多注册中心"></a>Dubbo 平滑迁移方案 – 多注册中心</h2><p>Dubbo 多注册中心配置文档地址：<a href="http://dubbo.apache.org/zh-cn/docs/user/demos/multi-registry.html" target="_blank" rel="noopener">http://dubbo.apache.org/zh-cn/docs/user/demos/multi-registry.html</a></p><p>本文的完整代码示例将会在文末提供，其中 Consul 注册中心搭建在本地，而 Nacos 注册中心使用的是阿里云的云产品：微服务引擎 MSE，其可以提供托管的 Nacos/Zookeeper/Eureka 等集群。</p><p>Dubbo 支持多注册中心的配置，这就为我们平滑迁移提供了很多的便利性。在使用 dubbo-spring-boot-starter 时，只需要增加如下的配置，即可配置多注册中心：</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">dubbo.registries.first.protocol=consul</span><br><span class="line">dubbo.registries.first.address=localhost:8500</span><br><span class="line"></span><br><span class="line">dubbo.registries.second.protocol=nacos</span><br><span class="line">dubbo.registries.second.address=mse-kirito-p.nacos-ans.mse.aliyuncs.com:8848</span><br></pre></td></tr></table></figure><p>在 Consul 控制台可以看到服务已经注册成功：</p><p><img src="https://image.cnkirito.cn/222.png" alt="Consul"></p><p>在 MSE 控制台可以看到 Nacos 服务也已经注册成功</p><p><img src="https://image.cnkirito.cn/111.png" alt="MSE Nacos"></p><p>并且，服务调用一切正常。你可能回想：前面讲了一堆，你告诉我改了两行配置就是平滑迁移了？我还是得好好纠正下这种想法，改代码从来都是最轻松的事，难的是在迁移中，时刻观察业务状况，确保服务不因为迁移有损。除此之外，还需要注意的是，Dubbo 自带的多注册中心方案因为框架实现的问题，存在一定的缺陷。</p><h2 id="Dubbo-多注册中心的缺陷"><a href="#Dubbo-多注册中心的缺陷" class="headerlink" title="Dubbo 多注册中心的缺陷"></a>Dubbo 多注册中心的缺陷</h2><p>在 Dubbo 的实现中，多个注册中心的地址是隔离的，地址不会融合。也就是说，当消费者如下配置后：</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">dubbo.registries.first.protocol=consul</span><br><span class="line">dubbo.registries.first.address=localhost:8500</span><br><span class="line"></span><br><span class="line">dubbo.registries.second.protocol=nacos</span><br><span class="line">dubbo.registries.second.address=mse-kirito-p.nacos-ans.mse.aliyuncs.com:8848</span><br></pre></td></tr></table></figure><p>会永远优先从 Consul 中读取服务地址，除非 Consul 中没有服务，才会尝试从 Nacos 中读取，顺序取决于配置文件中注册中心声明的先后。这可能不符合大多数人对多注册中心的直观认知，但没办法，Dubbo 就是这么设计的，我也尝试猜想了几个这么设计的可能性：</p><ul><li>多个注册中心没有感知到对方存在的必要，所以只能串行读取多个注册中心</li><li>Dubbo 本身模型不支持注册中心聚合，除非专门搞一个 AggregationRegistry 代理多个注册中心实现</li><li>多个注册地址的 equals 方案难以确定，官方没有给出契约规范，即 ip 和 port 相同就可以认为同一个地址吗？</li><li>Dubbo 的多注册中心的设计并不只是为了适配平滑迁移方案，其他场景可能恰恰希望使用这种串行读取的策略</li></ul><p>为了让读者有一个直观的感受，我用文末的 demo 进行了测试，让服务提供者 A1（端口号 12346） 只注册到 Nacos，服务提供者 A2（端口号为 12345） 只注册到 Consul，消费者 B 双订阅 Nacos 和 Consul。如下图所示，在测试初期，可以发现，稳定调用到 A1；期间，我手动 kill 了 A1，图中也清晰地打印出了一条地址下线通知，之后稳定调用到 A2。</p><p><img src="https://image.cnkirito.cn/image-20200531012739193.png" alt="multi-registry"></p><p>这样的缺陷，会导致我们在平滑迁移过程中无法对未迁移应用和迁移中应用进行充分的测试。</p><h2 id="Dubbo-平滑迁移方案-–-注册中心聚合"><a href="#Dubbo-平滑迁移方案-–-注册中心聚合" class="headerlink" title="Dubbo 平滑迁移方案 – 注册中心聚合"></a>Dubbo 平滑迁移方案 – 注册中心聚合</h2><p>注册中心聚合这个词其实是我自己想的，因为 Dubbo 官方文档并没有直接给出这种方案，而是由阿里云的微服务商业化 EDAS 团队提供的开源实现（ps，没错，就是我所在的团队啦）。其基本思路就是前文提到的，聚合多个注册中心的地址。使用方式也同样简单</p><p>引入依赖：</p><figure class="highlight xml"><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="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.edas<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>edas-dubbo-migration-bom<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.6.5.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>增加配置：</p><p>在 <code>application.properties</code> 中添加注册中心的地址。</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">dubbo.registry.address = edas-migration://30.5.124.15:9999?service-registry=consul://localhost:8500,nacos://mse-kirito-p.nacos-ans.mse.aliyuncs.com:8848&amp;reference-registry=consul://localhost:8500,nacos://mse-kirito-p.nacos-ans.mse.aliyuncs.com:8848</span><br></pre></td></tr></table></figure><p><strong>说明</strong> 如果是非 Spring Boot 应用，在 dubbo.properties 或者对应的 Spring 配置文件中配置。</p><ul><li><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">edas-migration://30.5.124.15:9999</span><br></pre></td></tr></table></figure><p>多注册中心的头部信息。可以不做更改，ip 和 port 可以任意填写，主要是为了兼容 Dubbo 对 ip 和 port 的校验。启动时，如果日志级别是 WARN 及以下，可能会抛一个 WARN 的日志，可以忽略。</p></li><li><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">service-registry</span><br></pre></td></tr></table></figure><p>服务注册的注册中心地址。写入多个注册中心地址。每个注册中心都是标准的 Dubbo 注册中心格式；多个用<code>,</code>分隔。</p></li><li><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">reference-registry</span><br></pre></td></tr></table></figure><p>服务订阅的注册中心地址。每个注册中心都是标准的 Dubbo 注册中心格式；多个用<code>,</code>分隔。</p></li></ul><p>验证该方案：</p><p><img src="https://image.cnkirito.cn/image-20200531015410936.png" alt="migration"></p><p>已经变成了随机调用，解决了多注册中心的缺陷。</p><p>迁移完成后，建议删除原注册中心的配置和迁移过程专用的依赖<code>edas-dubbo-migration-bom</code>，在业务量较小的时间分批重启应用。<code>edas-dubbo-migration-bom</code> 是一个迁移专用的依赖，虽然长期使用对您业务的稳定性没有影响，但其并不会跟随 Dubbo 的版本进行升级，为避免今后框架升级过程中出现兼容问题，推荐您在迁移完毕后清理掉，然后在业务量较小的时间分批重启应用。</p><blockquote><p>说明：edas-dubbo-migration-bom 目前的 release 版本只支持 Dubbo 2.6，我在文末的代码中提供了 2.7 的支持，预计很快两个版本都会贡献给 Dubbo 开源社区。</p></blockquote><h2 id="彩蛋：阿里云微服务引擎-MSE-重磅升级，上线微服务治理中心"><a href="#彩蛋：阿里云微服务引擎-MSE-重磅升级，上线微服务治理中心" class="headerlink" title="彩蛋：阿里云微服务引擎 MSE 重磅升级，上线微服务治理中心"></a>彩蛋：阿里云微服务引擎 MSE 重磅升级，上线微服务治理中心</h2><p>微服务治理中心是一个面向开源微服务框架微服务中心，通过 Java Agent 技术使得您的应用无需修改任何代码和配置，即可享有阿里云提供的微服务治理能力。 已经上线的功能包含 服务查询、无损下线、服务鉴权、离群实例摘除、标签路由。</p><p>微服务治理中心具有如下优势：功能强大，覆盖和增强了开源的治理功能，还提供差异化的功能。零成本接入，支持近五年的 Spring Cloud 和 Dubbo 版本，无需修改任何代码和配置。易被集成，阿里云容器服务用户只需在应用市场安装 微服务中心对应的 pilot ，并修改部署时的配置即可接入。</p><p>微服务中心尤其适合以下场景</p><ul><li>解决应用发布时影响业务的问题。如果您的应用在发布新版本的时候，此应用的服务消费者仍旧调用已经下线的节点，出现业务有损，数据不一致的情况。这时候您需要使用 微服务治理中心，微服务治理中心提供的无损下线功能能够实现服务消费者及时感知服务提供者下线情况，保持业务连续无损。容器服务 K8s 集群的应用在接入 微服务治理中心后，您无需再额外对应用进行任何配置、也无需在 MSC 控制台进行任何操作，即可实现 Dubbo 和 Spring Cloud 流量的无损下线。</li><li>满足应用调用中权限控制的需求。当您的某个微服务应用有权限控制要求，不希望其它所有应用都能调用。比如优惠券部门的优惠券查询接口是默认内部的部门都是可以调用的，但是优惠券发放接口只允许特定的部门的应用才可以调用。这时候您需要使用 微服务治理中心，微服务治理中心提供的服务鉴权功能，既能够对整个应用做一些权限控制，也能对应用中的某个接口和 URL 进行权限控制，满足您不同场景下的权限控制需求</li><li>解决不健康实例影响业务对问题，当节点出现 Full GC、网络分区、机器异常等问题时，这种情况下会导致调用此应用的流量出现异常、影响业务。但是运维人员又很难及时发现问题，且无法判断应该采取何种措施，如重启或者单纯地等待应用恢复。这时候您需要使用微服务治理中心，微服务治理中心 提供的离群实例摘除功能，能够根据您配置的规则自动摘服务调用列表中不健康的应用实例，以免异常的节点影响您的业务。同时还能自动地探测实例是否恢复并恢复流量，以及将实例异常信息触发监控报警，保护您的业务，提升稳定性。</li></ul><p><img src="https://image.cnkirito.cn/1590756935648.png" alt="MSC"></p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><p>本文测试代码地址：<a href="https://github.com/lexburner/dubbo-migration" target="_blank" rel="noopener">https://github.com/lexburner/dubbo-migration</a></p><p><img src="https://www.cnkirito.moe/css/images/wechat_public.jpg" alt="img"></p><p><em>「技术分享」<strong>某种程度上，是让作者和读者，不那么孤独的东西。</strong>欢迎关注我的微信公众号：<strong>「</strong>Kirito的技术分享」</em></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;近日，有报道称在 HashCorp 的商业软件试用协议上发现，旗下所有商业产品禁止在中国境内使用、部署、安装，这其中就包含了 Terraform, Consul, Vagrant 等众多知名软件，其中 Consul 是一个在微服务领域的开源软件，可以用于做注册发现、配置管理等场景。&lt;/p&gt;
&lt;p&gt;该新闻在国内发酵后，有人在 Twitter上咨询了HashCorp 公司的创始人，得到的回复是影响的软件仅限于 Vault 这款加密软件，目前 HashCorp 公司的官方网站上已经更新了相关的条款，明确了受影响的产品仅限 Vault 这一款产品。&lt;/p&gt;
&lt;h2 id=&quot;Consul-开源版是否收到影响？&quot;&gt;&lt;a href=&quot;#Consul-开源版是否收到影响？&quot; class=&quot;headerlink&quot; title=&quot;Consul 开源版是否收到影响？&quot;&gt;&lt;/a&gt;Consul 开源版是否收到影响？&lt;/h2&gt;&lt;p&gt;上面的条款里只提到了商业软件，那么开源的 Consul 是否受到影响呢？在 Github 的 Consul 仓库上，可以得知项目的 license 是 &lt;code&gt;Mozilla Public License 2.0&lt;/code&gt; ，这款许可证在 Apache 官网上是 &lt;code&gt;Category B&lt;/code&gt; , 属于 &lt;code&gt;Weak Copy Left&lt;/code&gt; 许可，那么它有哪些特点呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://image.cnkirito.cn/1590754178611.png&quot; alt=&quot;License&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任何可以使用，复制，修改，重新分发该代码，包括商业目的使用。&lt;/li&gt;
&lt;li&gt;如果修改了 MPL 协议许可下的源码，再重新发布这部分源码的话，必须保留原来 MPL 许可证不得更换。&lt;/li&gt;
&lt;li&gt;如果基于该项目衍生出更大的项目，那么这部分工作可以使用新许可证的方式进行分发，只要没有修改原来 MPL 许可下的代码。（这也是为什么 Apache 项目的分发的源码中可以包含 MPL 协议下二进制文件的原因）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以看到，MPL 通常被认为是介于 Apache License 和 GPL/LGPL 之间的一个折中方案。相对于 Apache License，MPL 2.0 要求修改了源码必须保持相同协议；相对于 GPL/LGPL, MPL 2.0 可以商用，同时衍生的作品在一定条件下也可以更换许可证类型。&lt;/p&gt;
&lt;p&gt;总体来看的话，开源版 Consul 无论是私用还是商用都是不受限制的。但这也可能是一个警钟，如果对 Consul 还是有所顾忌的话，如何替代掉它呢？&lt;/p&gt;
&lt;p&gt;在微服务领域，Consul 主要被用来做充当注册中心和配置中心，例如 Dubbo 和 SpringCloud 都有对应的支持。本文便以这个事为一个引子，介绍如何平滑地迁移 Dubbo 服务，达到替换注册中心的效果。&lt;/p&gt;
    
    </summary>
    
      <category term="RPC" scheme="https://lexburner.github.io/categories/RPC/"/>
    
    
      <category term="Dubbo" scheme="https://lexburner.github.io/tags/Dubbo/"/>
    
  </entry>
  
  <entry>
    <title>Arthas | 追踪线上耗时方法</title>
    <link href="https://lexburner.github.io/arthas-trace/"/>
    <id>https://lexburner.github.io/arthas-trace/</id>
    <published>2020-04-09T12:49:30.000Z</published>
    <updated>2021-01-16T11:25:16.424Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文是 Arthas 系列文章的第三篇。</p><p>本文主要介绍 <code>trace</code> 指令，用于定位两种类型的问题：</p><ol><li>线上服务 RT 比较高，但没有打印日志，无法确定具体是哪个方法比较耗时</li><li>线上服务出现异常，需要追踪到方法的堆栈</li></ol><a id="more"></a><h2 id="模拟线上耗时方法"><a href="#模拟线上耗时方法" class="headerlink" title="模拟线上耗时方法"></a>模拟线上耗时方法</h2><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文以 Dubbo 线程池满异常作为引子，介绍了线程类问题该如何分析，以及如何通过 Arthas 快速诊断线程问题。有了 Arthas，基本不再需要 jstack 将 16 进制转来转去了，大大提升了诊断速度。</p><h2 id="Arthas-钉钉交流群"><a href="#Arthas-钉钉交流群" class="headerlink" title="Arthas 钉钉交流群"></a>Arthas 钉钉交流群</h2><p><img src="https://alibaba.github.io/arthas/_images/dingding_qr.jpg" alt="_images/dingding_qr.jpg"></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;本文是 Arthas 系列文章的第三篇。&lt;/p&gt;
&lt;p&gt;本文主要介绍 &lt;code&gt;trace&lt;/code&gt; 指令，用于定位两种类型的问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线上服务 RT 比较高，但没有打印日志，无法确定具体是哪个方法比较耗时&lt;/li&gt;
&lt;li&gt;线上服务出现异常，需要追踪到方法的堆栈&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
      <category term="Arthas" scheme="https://lexburner.github.io/categories/Arthas/"/>
    
    
      <category term="Arthas" scheme="https://lexburner.github.io/tags/Arthas/"/>
    
  </entry>
  
</feed>
