| <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Apache Dubbo – 源码分析</title><link>https://dubbo.apache.org/zh-cn/blog/java/codeanalysis/</link><description>Recent content in 源码分析 on Apache Dubbo</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><atom:link href="https://dubbo.apache.org/zh-cn/blog/java/codeanalysis/index.xml" rel="self" type="application/rss+xml"/><item><title>Blog: dubbo-metrics 指标模块源码浅析</title><link>https://dubbo.apache.org/zh-cn/blog/java/codeanalysis/metrics/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://dubbo.apache.org/zh-cn/blog/java/codeanalysis/metrics/</guid><description/></item><item><title>Blog: Dubbo 3.0.8 源码解析</title><link>https://dubbo.apache.org/zh-cn/blog/java/codeanalysis/3.0.8/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://dubbo.apache.org/zh-cn/blog/java/codeanalysis/3.0.8/</guid><description/></item><item><title>Blog: Dubbo 3 中的三层配置隔离</title><link>https://dubbo.apache.org/zh-cn/blog/1/01/01/dubbo-3-%E4%B8%AD%E7%9A%84%E4%B8%89%E5%B1%82%E9%85%8D%E7%BD%AE%E9%9A%94%E7%A6%BB/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://dubbo.apache.org/zh-cn/blog/1/01/01/dubbo-3-%E4%B8%AD%E7%9A%84%E4%B8%89%E5%B1%82%E9%85%8D%E7%BD%AE%E9%9A%94%E7%A6%BB/</guid><description> |
| <h2 id="models提供的隔离">Models提供的隔离</h2> |
| <p>Dubbo目前提供了三个级别上的隔离:JVM级别、应用级别、服务(模块)级别,从而实现各个级别上的生命周期及配置信息的单独管理。这三个层次上的隔离由 FrameworkModel、ApplicationModel 和 ModuleModel 及它们对应的 Config 来完成。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/models.png" alt="image"></p> |
| <ul> |
| <li> |
| <p>FrameworkModel :Dubbo 框架的顶级模型,表示 Dubbo 框架的全局运行环境,适配多应用混合部署的场景,降低资源成本。</p> |
| <p>如:假设我们有一个在线教育平台,平台下有多个租户,而我们希望使这些租户的服务部署在同一个 JVM 上以节省资源,但它们之间可能使用不同的注册中心、监控设施、协议等,因此我们可以为每个租户分配一个 FrameWorkModel 实例来实现这种隔离。</p> |
| <p>FrameworkModel负责管理整个Dubbo框架的各种全局配置、元数据以及默认配置。</p> |
| </li> |
| <li> |
| <p>ApplicationModel :应用程序级别模型,表示一个 Dubbo 应用(通常为一个 SpringApplication)。适配单JVM多应用场景,通常结合热发布使用,降低热发布对整个应用的影响范围。</p> |
| <p>如,以上的在线教育平台有多个子系统,如课程管理、学生管理,而每个子系统都是独立的 Spring 应用,在同个 JVM 中运行,共享一个 FrameworkModel,也会共享 Framework 级别的默认配置和资源:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#586e75">//课程管理应用</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">@SpringBootApplication</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#268bd2">class</span> <span style="color:#268bd2">ClassApplication</span> { |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">private</span> <span style="color:#268bd2">static</span> ApplicationModel classAppModel; |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">public</span> <span style="color:#268bd2">static</span> <span style="color:#dc322f">void</span> <span style="color:#268bd2">main</span>(String<span style="color:#719e07">[]</span> args) { |
| </span></span><span style="display:flex;"><span> classAppModel |
| </span></span><span style="display:flex;"><span> .getApplicationConfigManager() |
| </span></span><span style="display:flex;"><span> .addRegistry(<span style="color:#719e07">new</span> RegistryConfig(REGISTRY_URL_CLASS)); |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//...其它设置</span> |
| </span></span><span style="display:flex;"><span> SpringApplication.run(ClassApplication.class, args); |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">public</span> <span style="color:#268bd2">static</span> <span style="color:#dc322f">void</span> <span style="color:#268bd2">setApplicationModel</span>(ApplicationModel classAppModel){ |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">this</span>.classAppModel <span style="color:#719e07">=</span> classAppModel; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#586e75">//学生管理应用</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">@SpringBootApplication</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#268bd2">class</span> <span style="color:#268bd2">StudentApplication</span> { |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">private</span> <span style="color:#268bd2">static</span> ApplicationModel studentAppModel; |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">public</span> <span style="color:#268bd2">static</span> <span style="color:#dc322f">void</span> <span style="color:#268bd2">main</span>(String<span style="color:#719e07">[]</span> args) { |
| </span></span><span style="display:flex;"><span> studentAppModel |
| </span></span><span style="display:flex;"><span> .getApplicationConfigManager() |
| </span></span><span style="display:flex;"><span> .addRegistry(<span style="color:#719e07">new</span> RegistryConfig(REGISTRY_URL_STUDENT)); |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//...其它设置</span> |
| </span></span><span style="display:flex;"><span> SpringApplication.run(StudentApplication.class, args); |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">public</span> <span style="color:#268bd2">static</span> <span style="color:#dc322f">void</span> <span style="color:#268bd2">setApplicationModel</span>(ApplicationModel studentAppModel){ |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">this</span>.studentAppModel <span style="color:#719e07">=</span> studentAppModel; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#586e75">//单机发布多应用</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#268bd2">class</span> <span style="color:#268bd2">MultiContextLauncher</span> { |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">private</span> <span style="color:#268bd2">static</span> FrameworkModel frameworkModel <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> FrameworkModel(); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">public</span> <span style="color:#268bd2">static</span> <span style="color:#dc322f">void</span> <span style="color:#268bd2">main</span>(String<span style="color:#719e07">[]</span> args) { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//使用同一个FrameworkModel</span> |
| </span></span><span style="display:flex;"><span> ClassApplication.setApplicationModel(frameworkModel.newApplication()); |
| </span></span><span style="display:flex;"><span> StudenAppication.setApplicationModel(frameworkModel.newApplication()); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">new</span> SpringApplicationBuilder(ClassApplication.class) |
| </span></span><span style="display:flex;"><span> .run(args); |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">new</span> SpringApplicationBuilder(StudentApplication.class.class) |
| </span></span><span style="display:flex;"><span> .run(args); |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><p>由此,我们可以重新部署单个应用与其对应的ApplicationModel,实现热发布,而不会影响到另一个应用。</p> |
| </li> |
| <li> |
| <p>ModuleModel :模块级别模型,表示应用下的子模块。适配一个应用下的多个模块(容器)。</p> |
| <p>如,上述的课程管理子系统内,新增课程和发布课程两个业务模块可能使用不同的spring容器。我们可以为每个spirng容器提供一个ModuleModel实例来管理、隔离模块级别的配置和资源:</p> |
| </li> |
| </ul> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#268bd2">@Configuration</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">@ComponentScan</span>(<span style="color:#2aa198">&#34;com.example.demo.class&#34;</span>) |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#268bd2">class</span> <span style="color:#268bd2">ClassManageConfig</span> { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">// 课程管理模块配置</span> |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#268bd2">@Configuration</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">@ComponentScan</span>(<span style="color:#2aa198">&#34;com.example.demo.student&#34;</span>) |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#268bd2">class</span> <span style="color:#268bd2">ClassPublishConfig</span> { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">// 课程发布模块配置</span> |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#268bd2">class</span> <span style="color:#268bd2">EducationPlatformApplication</span> { |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">public</span> <span style="color:#268bd2">static</span> <span style="color:#dc322f">void</span> <span style="color:#268bd2">main</span>(String<span style="color:#719e07">[]</span> args) { |
| </span></span><span style="display:flex;"><span> FrameworkModel frameworkModel <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> FrameworkModel(); |
| </span></span><span style="display:flex;"><span> ApplicationModel applicationModel <span style="color:#719e07">=</span> frameworkModel.newApplication(); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">// 创建课程管理模块,使用自己的ApplicationContext与ModuleModel</span> |
| </span></span><span style="display:flex;"><span> ModuleModel classManageModuleModel <span style="color:#719e07">=</span> applicationModel.newModule(); |
| </span></span><span style="display:flex;"><span> AnnotationConfigApplicationContext classManageContext <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> AnnotationConfigApplicationContext(classManageModuleModel); |
| </span></span><span style="display:flex;"><span> classManageContext.register(ClassManagementConfig.class); |
| </span></span><span style="display:flex;"><span> classManageContext.refresh(); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">// 创建学生管理模块,使用自己的ApplicationContext与ModuleModel</span> |
| </span></span><span style="display:flex;"><span> ModuleModel classPublishModuleModel <span style="color:#719e07">=</span> applicationModel.newModule(); |
| </span></span><span style="display:flex;"><span> AnnotationConfigApplicationContext classPublishContext <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> AnnotationConfigApplicationContext(classPublishModuleModel); |
| </span></span><span style="display:flex;"><span> classPublishContext.register(ClassPublishConfig.class); |
| </span></span><span style="display:flex;"><span> classPublishContext.refresh(); |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><p>三层的模型设计让Dubbo在处理复杂业务场景时具有更强的适应性和可扩展性。</p> |
| <p> 以下是一个以传统方式(而非 DubboBootstrap)启动 Dubbo 的 Demo,可以帮助我们了解 FrameworkModel、ApplicationModel 和 ModuleModel 之间的组织关系。</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#268bd2">private</span> <span style="color:#268bd2">static</span> <span style="color:#dc322f">void</span> <span style="color:#268bd2">startWithExportNew</span>() <span style="color:#268bd2">throws</span> InterruptedException { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//创建三个层级的Model</span> |
| </span></span><span style="display:flex;"><span> FrameworkModel frameworkModel <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> FrameworkModel(); |
| </span></span><span style="display:flex;"><span> ApplicationModel applicationModel <span style="color:#719e07">=</span> frameworkModel.newApplication(); |
| </span></span><span style="display:flex;"><span> ModuleModel moduleModel <span style="color:#719e07">=</span> applicationModel.newModule(); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//提供配置中心、元数据中心、协议的配置</span> |
| </span></span><span style="display:flex;"><span> RegistryConfig registryConfig <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> RegistryConfig(REGISTRY_URL); |
| </span></span><span style="display:flex;"><span> MetadataReportConfig metadataReportConfig <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> MetadataReportConfig(METADATA_REPORT_URL); |
| </span></span><span style="display:flex;"><span> ProtocolConfig protocolConfig <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> ProtocolConfig(CommonConstants.DUBBO, <span style="color:#719e07">-</span>1); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//如果之后需要获取注册中心或元数据中心,需要设置id,之后通过它们的Id获取(适配多注册中心)</span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">final</span> String registryId <span style="color:#719e07">=</span> <span style="color:#2aa198">&#34;registry-1&#34;</span>; |
| </span></span><span style="display:flex;"><span> registryConfig.setId(registryId); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//Model的配置管理通过对应的ConfigManager进行</span> |
| </span></span><span style="display:flex;"><span> ConfigManager appConfigManager <span style="color:#719e07">=</span> applicationModel.getApplicationConfigManager(); |
| </span></span><span style="display:flex;"><span> appConfigManager.setApplication(<span style="color:#719e07">new</span> ApplicationConfig(<span style="color:#2aa198">&#34;dubbo-demo-api-provider-app-1&#34;</span>)); |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//提供应用层级的配置中心、元数据中心、协议默认设置</span> |
| </span></span><span style="display:flex;"><span> appConfigManager.addRegistry(registryConfig); |
| </span></span><span style="display:flex;"><span> appConfigManager.addMetadataReport(metadataReportConfig); |
| </span></span><span style="display:flex;"><span> appConfigManager.addProtocol(protocolConfig); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> ModuleConfigManager moduleConfigManager <span style="color:#719e07">=</span> moduleModel.getConfigManager(); |
| </span></span><span style="display:flex;"><span> moduleConfigManager.setModule(<span style="color:#719e07">new</span> ModuleConfig(<span style="color:#2aa198">&#34;dubbo-demo-api-provider-app-1-module-1&#34;</span>)); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> ServiceConfig<span style="color:#719e07">&lt;</span>DemoService<span style="color:#719e07">&gt;</span> serviceConfig <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> ServiceConfig<span style="color:#719e07">&lt;&gt;</span>(); |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//设置该ServiceConfig对应的ModuleModel</span> |
| </span></span><span style="display:flex;"><span> serviceConfig.setScopeModel(moduleModel); |
| </span></span><span style="display:flex;"><span> serviceConfig.setProtocol(protocolConfig); |
| </span></span><span style="display:flex;"><span> serviceConfig.setInterface(DemoService.class); |
| </span></span><span style="display:flex;"><span> serviceConfig.setRef(<span style="color:#719e07">new</span> DemoServiceImpl()); |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//为ModuleModel添加ServiceConfig</span> |
| </span></span><span style="display:flex;"><span> moduleConfigManager.addConfig(serviceConfig); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> serviceConfig.export(); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">new</span> CountDownLatch(1).await(); |
| </span></span><span style="display:flex;"><span> } |
| </span></span></code></pre></div><ul> |
| <li>ApplicationModel 由 FrameWorkModel 创建,ModuleModel 由 ApplicationModel 创建,由此完成它们之间层级关系的引用。</li> |
| <li>ServiceConfig 和 ReferenceConfig 需要设置其所属的 ScopeModel(实际只能为 ModuleModel )。然后,还需将它们设置到对应的ModuleModel 中。</li> |
| </ul></description></item><item><title>Blog: Dubbo 3 之 Triple 流控反压原理解析</title><link>https://dubbo.apache.org/zh-cn/blog/2022/12/28/dubbo-3-%E4%B9%8B-triple-%E6%B5%81%E6%8E%A7%E5%8F%8D%E5%8E%8B%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/</link><pubDate>Wed, 28 Dec 2022 00:00:00 +0000</pubDate><guid>https://dubbo.apache.org/zh-cn/blog/2022/12/28/dubbo-3-%E4%B9%8B-triple-%E6%B5%81%E6%8E%A7%E5%8F%8D%E5%8E%8B%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/</guid><description> |
| <p>Triple 是 Dubbo 3 提出的基于 HTTP2 的开放协议, |
| 旨在解决 Dubbo 2 私有协议带来的互通性问题。 |
| Triple 基于 HTTP/2 定制自己的流控,支持通过特定的异常通知客户端业务层服务端负载高情况, |
| 保护了服务端被大流量击垮,提高系统高可用能力。</p> |
| <h2 id="一流控反压现状">一、流控反压现状</h2> |
| <p>客户端和服务器端在接收数据的时候有一个缓冲区来临时存储数据, |
| 但是缓冲区的大小是有限制的,所以有可能会出现缓冲区溢出的情况, |
| HTTP 通过流控保护数据溢出丢失风险。</p> |
| <h3 id="1http1-流控">1、HTTP/1 流控</h3> |
| <p>在 HTTP/1.1 中,流量的控制依赖的是底层TCP协议,在客户端和服务器端建立连接的时候, |
| 会使用系统默认的设置来建立缓冲区。在数据进行通信的时候,会告诉对方它的接收窗口的大小, |
| 这个接收窗口就是缓冲区中剩余的可用空间。如果接收窗口大小为零,则说明接收方缓冲区已满, |
| 则发送方将不再发送数据,直到客户端清除其内部缓冲区,然后请求恢复数据传输。</p> |
| <h3 id="2http2-流控">2、HTTP/2 流控</h3> |
| <p>HTTP/2 使用了多路复用机制,一个TCP连接可以有多个 HTTP/2 连接, |
| 故在 HTTP/2 中,有更加精细的流控制机制,允许服务端实现自己数据流和连接级的流控制。 |
| 服务端与客户端第一次连接时,会通过发送 <code>HTTP/2 SettingsFrame</code>设置初始化的流控窗口大小, |
| 用于 <code>Stream</code> 级别流控,默认为 65,535 字节。 |
| 定好流控窗口后,每次客户端发送数据就会减少流控窗口的大小, |
| 服务端收到数据后会发送窗口更新包(<code>WINDOW_UPDATE frame</code>)通知客户端更新窗口。 |
| 客户端收到窗口更新包后就会增加对应值的流控窗口,从而达到动态控制的目的。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/28/triple/1.png" alt="1.png"></p> |
| <h2 id="二triple流控反压">二、Triple流控反压</h2> |
| <p>Netty 基于 HTTP/2 实现了基础的流控,当服务端负载过高,客户端发送窗口为 0 时, |
| 新增请求就无法被发送出去,会在缓存到客户端待发送请求队列中,缓存数据过大, |
| 就会造成客户端内存溢出,影响业务程序。</p> |
| <p>Triple 基于 Netty 实现了 HTTP/2 协议,通过 <code>HTTP/2 FlowController</code>接口统一封装, |
| 在实现分为进站(inbound)和出站(outbound)两个维度的实现。 |
| Triple 在 inbound 流量上使用了 Netty 的默认流控实现, |
| 在 outbound 上实现了自己流控,基于服务端负载, |
| 将服务端流量压力透传到客户端业务层,实现客户端的业务反压,暂停业务继续发送请求, |
| 保护服务端不被大流量击垮。</p> |
| <h3 id="1连接初始化">1、连接初始化</h3> |
| <p>Triple在初次建立连接时,通过 <code>TripleHttpProtocol</code> 初始化 HTTP/2 配置, |
| 默认流控窗口 <code>DEFAULT_WINDOW_INIT_SIZE = MIB_8</code>, |
| 并在服务端和客户端加入自己的 outbound 流控接口。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/28/triple/2.png" alt="2.png"></p> |
| <h3 id="2inbound流控">2、Inbound流控</h3> |
| <p>Inbound 流量会通过 <code>DefaultHttpLocalFlowController</code> 的 <code>consumeBytes</code> 方法实现流控窗口更新与发送。</p> |
| <h4 id="1-入口传入http-流与更新数据大小">1) 入口传入HTTP 流与更新数据大小</h4> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/28/triple/3.png" alt="3.png"></p> |
| <h4 id="2-找到对应连接实现数据消费">2) 找到对应连接实现数据消费</h4> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/28/triple/4.png" alt="4.png"></p> |
| <h4 id="3-更新流控窗口">3) 更新流控窗口</h4> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/28/triple/5.png" alt="5.png"></p> |
| <h4 id="4-发送流控更新数据包window_update">4) 发送流控更新数据包(window_update)</h4> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/28/triple/6.png" alt="6.png"></p> |
| <h3 id="3outbound流控">3、Outbound流控</h3> |
| <p>Outbound 通过 Triple 自己的流控实现 <code>TriHttpRemoteFlowController</code>, |
| 将服务端压力反馈到业务层,保护服务端被大流量击垮。</p> |
| <h4 id="1-发送数据时判断是否还有窗口">1) 发送数据时判断是否还有窗口</h4> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/28/triple/7.png" alt="7.png"></p> |
| <h4 id="2-窗口为0时抛出特定异常">2) 窗口为0时抛出特定异常</h4> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/28/triple/8.png" alt="8.png"></p> |
| <h4 id="3-反馈客户端流控异常">3) 反馈客户端流控异常</h4> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/28/triple/9.png" alt="9.png"></p> |
| <h3 id="4总结">4、总结</h3> |
| <p>Triple 通过将底层客户端发送窗口为 0 场景封装为特定流控异常, |
| 透传至客户端上层业务,阻止客户端业务继续数据发送, |
| 有效的保护了服务端被大流量击垮和客户端的内存溢出的问题。</p> |
| <h2 id="三未来展望">三、未来展望</h2> |
| <p>目前 Triple 已经基本实现了流控反压能力,未来我们将深度联动业务, |
| 基于业务负载自适应调整反压流控, |
| 一是在 inbound 上将流控窗口包发送时机调整到服务端业务处理完成后, |
| 二是在 outbound 流量上关联客户端业务层,动态调整客户端发送速率。 |
| 从而实现基于服务端业务负载动态反压流控机制。</p></description></item><item><title>Blog: Triple 协议支持 Java 异常回传的设计与实现</title><link>https://dubbo.apache.org/zh-cn/blog/2022/12/19/triple-%E5%8D%8F%E8%AE%AE%E6%94%AF%E6%8C%81-java-%E5%BC%82%E5%B8%B8%E5%9B%9E%E4%BC%A0%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/</link><pubDate>Mon, 19 Dec 2022 00:00:00 +0000</pubDate><guid>https://dubbo.apache.org/zh-cn/blog/2022/12/19/triple-%E5%8D%8F%E8%AE%AE%E6%94%AF%E6%8C%81-java-%E5%BC%82%E5%B8%B8%E5%9B%9E%E4%BC%A0%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/</guid><description> |
| <h2 id="背景">背景</h2> |
| <p>在一些业务场景, 往往需要自定义异常来满足特定的业务, 主流用法是在catch里抛出异常, 例如:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#dc322f">void</span> <span style="color:#268bd2">deal</span>() { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">try</span>{ |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//doSomething </span> |
| </span></span><span style="display:flex;"><span> ... |
| </span></span><span style="display:flex;"><span> } <span style="color:#719e07">catch</span>(IGreeterException e) { |
| </span></span><span style="display:flex;"><span> ... |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">throw</span> e; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><p>或者通过ExceptionBuilder,把相关的异常对象返回给consumer:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span>provider.send(<span style="color:#719e07">new</span> ExceptionBuilders.IGreeterExceptionBuilder() |
| </span></span><span style="display:flex;"><span> .setDescription(&#39;异常描述信息&#39;); |
| </span></span></code></pre></div><p>在抛出异常后, 通过捕获和instanceof来判断特定的异常, 然后做相应的业务处理,例如:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#719e07">try</span> { |
| </span></span><span style="display:flex;"><span> greeterProxy.echo(REQUEST_MSG); |
| </span></span><span style="display:flex;"><span>} <span style="color:#719e07">catch</span> (IGreeterException e) { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//做相应的处理</span> |
| </span></span><span style="display:flex;"><span> ... |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><p>在 Dubbo 2.x 版本,可以通过上述方法来捕获 Provider 端的异常。 |
| 而随着云原生时代的到来, Dubbo 也开启了 3.0 的里程碑。</p> |
| <p>Dubbo 3.0 的一个很重要的目标就是全面拥抱云原生, |
| 在 3.0 的许多特性中,很重要的一个改动就是支持<strong>新的一代Rpc协议Triple</strong>。</p> |
| <p>Triple 协议基于 HTTP 2.0 进行构建,对网关的穿透性强,<strong>兼容 gRPC</strong>, |
| 提供 Request Response、Request Streaming、Response Streaming、 |
| Bi-directional Streaming 等通信模型; |
| 从 Triple 协议开始,Dubbo 还支持基于 IDL 的服务定义。</p> |
| <p>采用 Triple 协议的用户可以在 provider 端生成用户定义的异常信息, |
| 记录异常产生的堆栈,triple 协议可保证将用户在客户端获取到异常的message。</p> |
| <p>Triple 的回传异常会在 <code>AbstractInvoker</code> 的 <code>waitForResultIfSync</code> |
| 中把异常信息堆栈统一封装成 <code>RpcException</code>, |
| 所有来自 Provider 端的异常都会被封装成 <code>RpcException</code> 类型并抛出, |
| 这会导致用户无法根据特定的异常类型捕获来自 Provider 的异常, |
| 只能通过捕获 RpcException 异常来返回信息, |
| 且 Provider 携带的异常 message 也无法回传,只能获取打印的堆栈信息:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span> <span style="color:#719e07">try</span> { |
| </span></span><span style="display:flex;"><span> greeterProxy.echo(REQUEST_MSG); |
| </span></span><span style="display:flex;"><span> } <span style="color:#719e07">catch</span> (RpcException e) { |
| </span></span><span style="display:flex;"><span> e.printStackTrace(); |
| </span></span><span style="display:flex;"><span> } |
| </span></span></code></pre></div><p>自定义异常信息在社区中的呼声也比较高, |
| 因此本次改动将支持自定义异常的功能, 使得服务端能抛出自定义异常后被客户端捕获到。</p> |
| <h2 id="dubbo异常处理简介">Dubbo异常处理简介</h2> |
| <p>我们从Consumer的角度看一下一次Triple协议 Unary请求的大致流程:</p> |
| <p>Dubbo Consumer 从 Spring 容器中获取 bean 时获取到的是一个代理接口, |
| 在调用接口的方法时会通过代理类远程调用接口并返回结果。</p> |
| <p>Dubbo提供的代理工厂类是 <code>ProxyFactory</code>,通过 SPI 机制默认实现的是 <code>JavassistProxyFactory</code>, |
| <code>JavassistProxyFactory</code> 创建了一个继承自 <code>AbstractProxyInvoker</code> 类的匿名对象, |
| 并重写了抽象方法 <code>doInvoke</code>。 |
| 重写后的 <code>doInvoke</code> 只是将调用请求转发给了 <code>Wrapper</code> 类的 <code>invokeMethod</code> 方法, |
| 并生成 <code>invokeMethod</code> 方法代码和其他一些方法代码。</p> |
| <p>代码生成完毕后,通过 <code>Javassist</code> 生成 <code>Class</code> 对象, |
| 最后再通过反射创建 <code>Wrapper</code> 实例,随后通过 <code>InvokerInvocationHandler</code> -&gt; <code>InvocationUtil</code> -&gt; <code>AbstractInvoker</code> -&gt; 具体实现类发送请求到Provider端。</p> |
| <p>Provider 进行相应的业务处理后返回相应的结果给 Consumer 端,来自 Provider 端的结果会被封装成 <code>AsyncResult</code> ,在 <code>AbstractInvoker</code> 的具体实现类里, |
| 接受到来自 Provider 的响应之后会调用 <code>appResponse</code> 到 <code>recreate</code> 方法,若 <code>appResponse</code> 里包含异常, |
| 则会抛出给用户,大体流程如下:</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/19/triple/1.jpeg" alt="1.jpeg"></p> |
| <p>上述的异常处理相关环节是在 Consumer 端,在 Provider 端则是由 <code>org.apache.dubbo.rpc.filter.ExceptionFilter</code> 进行处理, |
| 它是一系列责任链 Filter 中的一环,专门用来处理异常。</p> |
| <p>Dubbo 在 Provider 端的异常会在封装进 <code>appResponse</code> 中。下面的流程图揭示了 <code>ExceptionFilter</code> 源码的异常处理流程:</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/19/triple/2.jpeg" alt="2.jpeg"></p> |
| <p>而当 <code>appResponse</code> 回到了 Consumer 端,会在 <code>InvocationUtil</code> 里调用 <code>AppResponse</code> 的 <code>recreate</code> 方法抛出异常, |
| 最终可以在 Consumer 端捕获:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#268bd2">public</span> Object <span style="color:#268bd2">recreate</span>() <span style="color:#268bd2">throws</span> Throwable { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> (exception <span style="color:#719e07">!=</span> <span style="color:#cb4b16">null</span>) { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">try</span> { |
| </span></span><span style="display:flex;"><span> Object stackTrace <span style="color:#719e07">=</span> exception.getStackTrace(); |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> (stackTrace <span style="color:#719e07">==</span> <span style="color:#cb4b16">null</span>) { |
| </span></span><span style="display:flex;"><span> exception.setStackTrace(<span style="color:#719e07">new</span> StackTraceElement<span style="color:#719e07">[</span>0<span style="color:#719e07">]</span>); |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> } <span style="color:#719e07">catch</span> (Exception e) { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">// ignore</span> |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">throw</span> exception; |
| </span></span><span style="display:flex;"><span>} |
| </span></span><span style="display:flex;"><span><span style="color:#719e07">return</span> result; |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><h2 id="triple-通信原理">Triple 通信原理</h2> |
| <p>在上一节中,我们已经介绍了 Dubbo 在 Consumer 端大致发送数据的流程, |
| 可以看到最终依靠的是 <code>AbstractInvoker</code> 的实现类来发送数据。 |
| 在 Triple 协议中,<code>AbstractInvoker</code> 的具体实现类是 <code>TripleInvoker</code> , |
| <code>TripleInvoker</code> 在发送前会启动监听器,监听来自 Provider 端的响应结果, |
| 并调用 <code>ClientCallToObserverAdapter</code> 的 <code>onNext</code> 方法发送消息, |
| 最终会在底层封装成 Netty 请求发送数据。</p> |
| <p>在正式的请求发起前,TripleServer 会注册 <code>TripleHttp2FrameServerHandler</code>, |
| 它继承自 Netty 的 <code>ChannelDuplexHandler</code>, |
| 其作用是会在 <code>channelRead</code> 方法中不断读取 Header 和 Data 信息并解析, |
| 经过层层调用, |
| 会在 <code>AbstractServerCall</code> 的 <code>onMessage</code> 方法里把来自 consumer 的信息流进行反序列化, |
| 并最终由交由 <code>ServerCallToObserverAdapter</code> 的 <code>invoke</code> 方法进行处理。</p> |
| <p>在 <code>invoke</code> 方法中,根据 consumer 请求的数据调用服务端相应的方法,并异步等待结果;' |
| 若服务端抛出异常,则调用 <code>onError</code> 方法进行处理, |
| 否则,调用 <code>onReturn</code> 方法返回正常的结果,大致代码逻辑如下:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#dc322f">void</span> <span style="color:#268bd2">invoke</span>() { |
| </span></span><span style="display:flex;"><span> ... |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">try</span> { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//调用invoke方法请求服务</span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">final</span> Result response <span style="color:#719e07">=</span> invoker.invoke(invocation); |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//异步等待结果</span> |
| </span></span><span style="display:flex;"><span> response.whenCompleteWithContext((r, t) <span style="color:#719e07">-&gt;</span> { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//若异常不为空</span> |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> (t <span style="color:#719e07">!=</span> <span style="color:#cb4b16">null</span>) { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//调用方法过程出现异常,调用onError方法处理</span> |
| </span></span><span style="display:flex;"><span> responseObserver.onError(t); |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">return</span>; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> (response.hasException()) { |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//调用onReturn方法处理业务异常</span> |
| </span></span><span style="display:flex;"><span> onReturn(response.getException()); |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">return</span>; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> ... |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">//正常返回结果</span> |
| </span></span><span style="display:flex;"><span> onReturn(r.getValue()); |
| </span></span><span style="display:flex;"><span> }); |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> ... |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><p>大体流程如下:</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/19/triple/3.jpeg" alt="3.jpeg"></p> |
| <h2 id="实现版本">实现版本</h2> |
| <p>了解了上述原理,我们就可以进行相应的改造了, |
| 能让 consumer 端捕获异常的<strong>关键在于把异常对象以及异常信息序列化后再发送给consumer端</strong>。 |
| 常见的序列化协议很多,例如 Dubbo/HSF 默认的 hessian2 序列化; |
| 还有使用广泛的 JSON 序列化;以及 gRPC 原生支持的 protobuf(PB) 序列化等等。 |
| Triple协议因为兼容grpc的原因,默认采用 Protobuf 进行序列化。 |
| 上述提到的这三种典型的序列化方案作用类似,但在实现和开发中略有不同。 |
| PB 不可由序列化后的字节流直接生成内存对象, |
| 而 Hessian 和 JSON 都是可以的。后两者反序列化的过程不依赖“二方包”, |
| 其序列化和反序列化的代码由 proto 文件相同,只要客户端和服务端用相同的 proto 文件进行通信, |
| 就可以构造出通信双方可解析的结构。</p> |
| <p>单一的 protobuf 无法序列化异常信息, |
| 因此我们采用 <code>Wrapper + PB</code> 的形式进行序列化异常信息, |
| 抽象出一个 <code>TripleExceptionWrapperUtils</code> 用于序列化异常, |
| 并在 <code>trailer</code> 中采用 <code>TripleExceptionWrapperUtils</code> 序列化异常,大致代码流程如下:</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/19/triple/4.jpeg" alt="4.jpeg"></p> |
| <p>上面的实现方案看似非常合理,已经能把 Provider 端的异常对象和信息回传, |
| 并在 Consumer 端进行捕获。但仔细想想还是有问题的: |
| 通常在 HTTP2 为基础的通信协议里会对 header 大小做一定的限制, |
| 太大的header size 会导致性能退化严重,为了保证性能, |
| 往往以 HTTP2 为基础的协议在建立连接的时候是要协商最大 header size 的, |
| 超过后会发送失败。对于 Triple 协议来说,在设计之初就是基于 HTTP 2.0, |
| 能无缝兼容 Grpc,而 Grpc header 头部只有 8KB 大小, |
| 异常对象大小可能超过限制,从而丢失异常信息; |
| 且多一个 header 携带序列化的异常信息意味着用户能加的 header 数量会减少, |
| 挤占了其他 header 所能占用的空间。</p> |
| <p>经过讨论,考虑将异常信息放置在 Body,将序列化后的异常从 trailer 挪至 body, |
| 采用 <code>TripleWrapper + protobuf</code> 进行序列化,把相关的异常信息序列化后回传。 |
| 社区围绕这个问题进行了一系列的争论,读者也可尝试先思考一下:</p> |
| <p>1.在 body 中携带回传的异常信息,其对应HTTP header状态码该设置为多少?</p> |
| <p>2.基于 http2 构建的协议,按照主流的 grpc 实现方案,相关的错误信息放在 <code>trailer</code>,理论上不存在body,上层协议也需要保持语义一致性,若此时在payload回传异常对象,且grpc并没有支持在Body回传序列化对象的功能, 会不会破坏Http和grpc协议的语义?从这个角度出发,异常信息更应该放在trailer里。</p> |
| <p>3.作为开源社区,不能一味满足用户的需求,非标准化的用法注定是会被淘汰的,应该尽量避免更改 Protobuf的语义,是否在Wrapper层去支持序列化异常就能满足需求?</p> |
| <p>首先回答第二、三个问题:HTTP 协议并没有约定在状态码非 2xx 的时候不能返回 body,返回之后是否读取取决于用户。grpc 采用protobuf进行序列化,所以无法返回 exception;且try catch机制为java独有,其他语言并没有对应的需求,但Grpc暂时不支持的功能并一定是unimplemented,Dubbo的设计目标之一是希望能和主流协议甚至架构进行对齐,但对于用户合理的需求也希望能进行一定程度的修改。且从throw本身的语义出发,throw 的数据不只是一个 error message,序列化的异常信息带有业务属性,根据这个角度,更不应该采用类似trailer的设计。至于单一的Wrapper层,也没办法和grpc进行互通。至于Http header状态码设置为200,因为其返回的异常信息已经带有一定的业务属性,不再是单纯的error,这个设计也与grpc保持一致,未来考虑网关采集可以增加新的triple-status。</p> |
| <p>更改后的版本只需在异常不为空时返回相关的异常信息,采用 <code>TripleWrapper + Protobuf</code> 进行序列化异常信息,并在consumer端进行解析和反序列化,大体流程如下:</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/2022/12/19/triple/5.jpeg" alt="5.jpeg"></p> |
| <h2 id="总结">总结</h2> |
| <p>通过对 Dubbo 3.0 新增自定义异常的版本迭代中可以看出,尽管只能新增一个小小的特性,流程下并不复杂,但由于要考虑互通、兼容和协议的设计理念,因此思考和讨论的时间可能比写代码的时间更多。</p></description></item><item><title>Blog: 浅析 Dubbo 3.0 中接口级地址推送性能的优化</title><link>https://dubbo.apache.org/zh-cn/blog/2022/06/23/%E6%B5%85%E6%9E%90-dubbo-3.0-%E4%B8%AD%E6%8E%A5%E5%8F%A3%E7%BA%A7%E5%9C%B0%E5%9D%80%E6%8E%A8%E9%80%81%E6%80%A7%E8%83%BD%E7%9A%84%E4%BC%98%E5%8C%96/</link><pubDate>Thu, 23 Jun 2022 00:00:00 +0000</pubDate><guid>https://dubbo.apache.org/zh-cn/blog/2022/06/23/%E6%B5%85%E6%9E%90-dubbo-3.0-%E4%B8%AD%E6%8E%A5%E5%8F%A3%E7%BA%A7%E5%9C%B0%E5%9D%80%E6%8E%A8%E9%80%81%E6%80%A7%E8%83%BD%E7%9A%84%E4%BC%98%E5%8C%96/</guid><description> |
| <h2 id="url-简介">URL 简介</h2> |
| <p>在阐述地址推送性能的具体优化之前,我们有必要先了解一下与之息息相关的内容 &mdash; URL。</p> |
| <h3 id="定义">定义</h3> |
| <p>在不谈及 dubbo 时,我们大多数人对 URL 这个概念并不会感到陌生。统一资源定位器 (<a href="https://www.ietf.org/rfc/rfc1738.txt">RFC1738</a>――Uniform Resource Locators (URL))应该是最广为人知的一个 RFC 规范,它的定义也非常简单。</p> |
| <blockquote> |
| <p>因特网上的可用资源可以用简单字符串来表示,该文档就是描述了这种字符串的语法和语 义。而这些字符串则被称为:“统一资源定位器”(URL)</p> |
| </blockquote> |
| <p><strong>一个标准的 URL 格式</strong>至多可以包含如下的几个部分</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>protocol://username:password@host:port/path?key=value&amp;key=value |
| </span></span></code></pre></div><p><strong>一些典型 URL</strong></p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>http://www.facebook.com/friends?param1=value1&amp;amp;param2=value2 |
| </span></span><span style="display:flex;"><span>https://username:password@10.20.130.230:8080/list?version=1.0.0 |
| </span></span><span style="display:flex;"><span>ftp://username:password@192.168.1.7:21/1/read.txt |
| </span></span></code></pre></div><p>当然,也有一些<strong>不太符合常规的 URL</strong>,也被归类到了 URL 之中</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>192.168.1.3:20880 |
| </span></span><span style="display:flex;"><span>url protocol = null, url host = 192.168.1.3, port = 20880, url path = null |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>file:///home/user1/router.js?type=script |
| </span></span><span style="display:flex;"><span>url protocol = file, url host = null, url path = home/user1/router.js |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>file://home/user1/router.js?type=script&lt;br&gt; |
| </span></span><span style="display:flex;"><span>url protocol = file, url host = home, url path = user1/router.js |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>file:///D:/1/router.js?type=script |
| </span></span><span style="display:flex;"><span>url protocol = file, url host = null, url path = D:/1/router.js |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>file:/D:/1/router.js?type=script |
| </span></span><span style="display:flex;"><span>同上 file:///D:/1/router.js?type=script |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>/home/user1/router.js?type=script |
| </span></span><span style="display:flex;"><span>url protocol = null, url host = null, url path = home/user1/router.js |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>home/user1/router.js?type=script |
| </span></span><span style="display:flex;"><span>url protocol = null, url host = home, url path = user1/router.js |
| </span></span></code></pre></div><h3 id="dubbo-中的-url">Dubbo 中的 URL</h3> |
| <p>在 dubbo 中,也使用了类似的 URL,主要用于在各个扩展点之间传递数据,组成此 URL 对象的具体参数如下:</p> |
| <ul> |
| <li> |
| <p>protocol:一般是 dubbo 中的各种协议 如:dubbo thrift http zk</p> |
| </li> |
| <li> |
| <p>username/password:用户名/密码</p> |
| </li> |
| <li> |
| <p>host/port:主机/端口</p> |
| </li> |
| <li> |
| <p>path:接口名称</p> |
| </li> |
| <li> |
| <p>parameters:参数键值对</p> |
| <p><strong>一些典型的 Dubbo URL</strong></p> |
| </li> |
| </ul> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000 |
| </span></span><span style="display:flex;"><span>描述一个 dubbo 协议的服务 |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-consumer&amp;dubbo=2.0.2&amp;interface=org.apache.dubbo.registry.RegistryService&amp;pid=1214&amp;qos.port=33333&amp;timestamp=1545721981946 |
| </span></span><span style="display:flex;"><span>描述一个 zookeeper 注册中心 |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>consumer://30.5.120.217/org.apache.dubbo.demo.DemoService?application=demo-consumer&amp;category=consumers&amp;check=false&amp;dubbo=2.0.2&amp;interface=org.apache.dubbo.demo.DemoService&amp;methods=sayHello&amp;pid=1209&amp;qos.port=33333&amp;side=consumer&amp;timestamp=1545721827784 |
| </span></span><span style="display:flex;"><span>描述一个消费者 |
| </span></span></code></pre></div><p>可以说,任意的一个领域中的一个实现都可以认为是一类 URL,dubbo 使用 URL 来统一描述了元数据,配置信息,贯穿在整个框架之中。</p> |
| <h2 id="dubbo-27">Dubbo 2.7</h2> |
| <h3 id="url-结构">URL 结构</h3> |
| <p>在 Dubbo 2.7 中,URL 的结构非常简单,一个类就涵盖了所有内容,如下图所示。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/url-perf-tuning-1.png" alt="Dubbo2 URL类图.png"></p> |
| <h3 id="地址推送模型">地址推送模型</h3> |
| <p>接下来我们再来看看 Dubbo 2.7 中的地址推送模型方案,主要性能问题由下列过程引起。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/url-perf-tuning-2.png" alt="Dubbo2 地址推送模型.png"></p> |
| <p>上图中主要的流程为 |
| 1、用户新增/删除DemoService的某个具体Provider实例(常见于扩容缩容、网络波动等原因) |
| 2、ZooKeeper将DemoService下所有实例推送给Consumer端 |
| 3、Consumer端根据Zookeeper推送的数据重新全量生成URL |
| 根据该方案可以看出在Provider实例数量较小时,Consumer端的影响比较小,但当某个接口有大量Provider实例时,便会有大量不必要的URL创建过程。 |
| 而Dubbo 3.0中则主要针对上述推送流程进行了一系列的优化,接下来我们便对其进行具体的讲解。</p> |
| <h2 id="dubbo-30">Dubbo 3.0</h2> |
| <h3 id="url-结构-1">URL 结构</h3> |
| <p>当然,地址推送模型的优化依然离不开 URL 的优化,下图是Dubbo 3.0中优化地址推送模型的过程中使用的新的URL结构。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/url-perf-tuning-3.png" alt="Dubbo3 URL类图.png"></p> |
| <p>根据上图我们可以看出,在 Dubbo 2.7 的 URL 中的几个重要属性在 Dubbo 3.0 中已经不存在了,取而代之的是 URLAddress 和 URLParam 两个类。原来的 parameters 属性被移动到了 URLParam 中的 params,其他的属性则移动到了 URLAddress 及其子类中。 |
| 再来介绍 URL 新增的 3 个子类,其中 InstanceAddressURL 属于应用级接口地址,本篇章中不做介绍。 |
| 而 ServiceConfigURL 及 ServiceAddressURL 主要的差别就是,ServiceConfigURL 是程序读取配置文件时生成的 URL。而 ServiceAddressURL 则是注册中心推送一些信息(如 providers)过来时生成的 URL。 |
| 在这里我们顺便提一下为什么会有 DubboServiceAddressURL 这个子类,按照目前的结构来看,ServiceAddressURL 只有这一个子类,所以完全可以将他们两个的属性全都放到 ServiceAddressURL 中,那么为什么还要有这个子类呢?其实是 Dubbo 3.0 为了兼容 HSF 框架所设计的,抽象出了一个 ServiceAddressURL,而 HSF 框架则可以继承这个类,使用 HSFServiceAddressURL,当然,这个类目前没有体现出来,所以此处我们简单一提,不过多讲解。 |
| 那么,我们接下来就讨论一下 Dubbo 3.0 为什么要改为此种数据结构,并且该结构和地址推送模型的优化有何关联性吧!</p> |
| <h3 id="地址推送模型的优化">地址推送模型的优化</h3> |
| <h4 id="url-结构上的优化">URL 结构上的优化</h4> |
| <p>我们在上小节中的类图里看到虽然原来的属性都被移到了 URLAddress 和 URLParam 里,但是 URL 的子类依然多了几个属性,这几个属性自然也是为了优化而新增的,那么这里就讲讲这几个属性的作用。 |
| <strong>ServiceConfigURL</strong>:这个子类中新增了 attribute 这个属性,这个属性主要是针对 URLParam 的 params 做了冗余,仅仅只是将 value 的类型从 String 改为了 Object,减少了代码中每次获取 parameters 的格式转换消耗。 |
| <strong>ServiceAddressURL</strong>:这个子类及其对应的其他子类中则新增了 overrideURL 和 consumerURL 属性。其中 consumerURL 是针对 consumer 端的配置信息,overrideURL 则是在 Dubbo Admin 上进行动态配置时写入的值,当我们调用 URL 的 getParameter() 方法时,优先级为 <code>overrideURL &gt; consumerURL &gt; urlParam</code>。在 Dubbo 2.7 时,动态配置属性会替换 URL 中的属性,及当你有大量 URL 时消耗也是不可忽视的,而此处的 overrideURL 则避免了这种消耗,因为所有 URL 都会共同使用同一个对象。</p> |
| <h4 id="多级缓存">多级缓存</h4> |
| <p>缓存是 Dubbo 3.0 在 URL 上做的优化的重点,同时这部分也是直接针对地址推送模型所做的优化,那么接下来我们就开始来介绍一下多级缓存的具体实现。 |
| 首先,多级缓存主要体现在 CacheableFailbackRegistry 这个类之中,它直接继承于 FailbackRegistry,以 Zookeeper 为例,我们看看 Dubbo 2.7 和 Dubbo 3.0 继承结构的区别。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/url-perf-tuning-4.png" alt="Dubbo3 CacheableFailbackRegistry缓存.png"></p> |
| <p>可以看到在 CacheableFailbackRegistry 缓存中,我们新增了 3 个缓存属性 <code>stringAddress</code>,<code>stringParam</code> 和 <code>stringUrls</code>。我们通过下图来描述这 3 个缓存的具体使用场景。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/url-perf-tuning-5.png" alt="多级缓存.png"></p> |
| <p>在该方案下,我们使用了 3 个纬度的缓存数据(URL 字符串缓存、URL 地址缓存、URL 参数缓存),这样一来,在大部分情况下都能有效利用到缓存中的数据,减少了 Zookeeper 重复通知的消耗。</p> |
| <h4 id="延迟通知">延迟通知</h4> |
| <p>除了上面提到的优化之外,其实另外还有两个小小的优化。 |
| 第一个是解析 URL 时可以直接使用编码后的 URL 字符串字节进行解析,而在 Dubbo 2.7 中,所有编码后的 URL 字符串都需要经过解码才可以正常解析为 URL 对象。该方式也直接减少了 URL 解码过程的开销。 |
| 第二个则是 URL 变更后的通知机制增加了延迟,下图以Zookeeper为例讲解了实现细节。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/url-perf-tuning-6.png" alt="延迟通知.png"></p> |
| <p>在该方案中,当 Consumer 接收 Zookeeper 的变更通知后会主动休眠一段时间,而这段时间内的变更在休眠结束后只会保留最后一次变更,Consumer 便会使用最后一次变更来进行监听实例的更新,以此方法来减少大量 URL 的创建开销。</p> |
| <h4 id="字符串重用">字符串重用</h4> |
| <p>在旧版本实现中,不同的 URL 中属性相同的字符串会存储在堆内不同的地址中,如 protocol、path 等,当有大量 provider 的情况下,Consumer 端的堆内会存在大量的重复字符串,导致内存利用率低下,所以此处提供了另一个优化方式,即字符串重用。 |
| 而它的实现方式也非常的简单,让我们来看看对应的代码片段。</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#268bd2">class</span> <span style="color:#268bd2">URLItemCache</span> { |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">private</span> <span style="color:#268bd2">static</span> <span style="color:#268bd2">final</span> Map<span style="color:#719e07">&lt;</span>String, String<span style="color:#719e07">&gt;</span> PATH_CACHE <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> LRUCache<span style="color:#719e07">&lt;&gt;</span>(10000); |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">private</span> <span style="color:#268bd2">static</span> <span style="color:#268bd2">final</span> Map<span style="color:#719e07">&lt;</span>String, String<span style="color:#719e07">&gt;</span> PROTOCOL_CACHE <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> ConcurrentHashMap<span style="color:#719e07">&lt;&gt;</span>(); |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">// 省略无关代码片段</span> |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">public</span> <span style="color:#268bd2">static</span> String <span style="color:#268bd2">checkProtocol</span>(String _protocol) { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> (_protocol <span style="color:#719e07">==</span> <span style="color:#cb4b16">null</span>) { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">return</span> _protocol; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> String cachedProtocol <span style="color:#719e07">=</span> PROTOCOL_CACHE.putIfAbsent(_protocol, _protocol); |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> (cachedProtocol <span style="color:#719e07">!=</span> <span style="color:#cb4b16">null</span>) { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">return</span> cachedProtocol; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">return</span> _protocol; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">public</span> <span style="color:#268bd2">static</span> String <span style="color:#268bd2">checkPath</span>(String _path) { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> (_path <span style="color:#719e07">==</span> <span style="color:#cb4b16">null</span>) { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">return</span> _path; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> String cachedPath <span style="color:#719e07">=</span> PATH_CACHE.putIfAbsent(_path, _path); |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> (cachedPath <span style="color:#719e07">!=</span> <span style="color:#cb4b16">null</span>) { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">return</span> cachedPath; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">return</span> _path; |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><p>由如上代码片段可以得知,字符串重用即为简单地使用了 Map 来存储对应的缓存值,当你使用了相同的字符串时,便会从 Map 中获取早已存在的对象返回给调用方,由此便可以减少堆内存中重复的字符串数以达到优化的效果。</p> |
| <h3 id="优化结果">优化结果</h3> |
| <p>这里优化结果我引用了<a href="https://zhuanlan.zhihu.com/p/345626851">《Dubbo 3.0 前瞻:服务发现支持百万集群,带来可伸缩微服务架构》</a>这篇文章中的两副图来说明,下图模拟了在<strong>220万</strong>个 Provider 接口的情况下,接口数据不断变更导致的 Consumer 端的消耗,我们看到整个 Consumer 端几乎被 Full GC 占满了,严重影响了性能。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/url-perf-tuning-7.png" alt="Dubbo2 接口级地址模型.png"></p> |
| <p>那么我们再来看看 Dubbo 3.0 中对 URL 进行优化后同一个环境下的压测结果,如下图所示。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/url-perf-tuning-8.png" alt="Dubbo3 接口级地址模型.png"></p> |
| <p>我们明显可以看到 Full GC 的频率减少到了只有 3 次,大大提升了性能。当然,该文章中还有其他方面的对比,此处便不一一引用了,感兴趣的读者可以自行去阅读该文章。</p></description></item><item><title>Blog: Dubbo3 应用级服务发现</title><link>https://dubbo.apache.org/zh-cn/blog/2021/06/02/dubbo3-%E5%BA%94%E7%94%A8%E7%BA%A7%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0/</link><pubDate>Wed, 02 Jun 2021 00:00:00 +0000</pubDate><guid>https://dubbo.apache.org/zh-cn/blog/2021/06/02/dubbo3-%E5%BA%94%E7%94%A8%E7%BA%A7%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0/</guid><description> |
| <h2 id="1-服务发现service-discovery-概述">1 服务发现(Service Discovery) 概述</h2> |
| <p>从 Internet 刚开始兴起,如何动态感知后端服务的地址变化就是一个必须要面对的问题,为此人们定义了 DNS 协议,基于此协议,调用方只需要记住由固定字符串组成的域名,就能轻松完成对后端服务的访问,而不用担心流量最终会访问到哪些机器 IP,因为有代理组件会基于 DNS 地址解析后的地址列表,将流量透明的、均匀的分发到不同的后端机器上。</p> |
| <p>在使用微服务构建复杂的分布式系统时,如何感知 backend 服务实例的动态上下线,也是微服务框架最需要关心并解决的问题之一。业界将这个问题称之为 - 微服务的地址发现(Service Discovery),业界比较有代表性的微服务框架如 SpringCloud、Dubbo 等都抽象了强大的动态地址发现能力,并且为了满足微服务业务场景的需求,绝大多数框架的地址发现都是基于自己设计的一套机制来实现,因此在能力、灵活性上都要比传统 DNS 丰富得多。如 SpringCloud 中常用的 Eureka, Dubbo 中常用的 Zookeeper、Nacos 等,这些注册中心实现不止能够传递地址(IP + Port),还包括一些微服务的 Metadata 信息,如实例序列化类型、实例方法列表、各个方法级的定制化配置等。</p> |
| <p>下图是微服务中 Service Discovery 的基本工作原理图,微服务体系中的实例大概可分为三种角色:服务提供者(Provider)、服务消费者(Consumer)和注册中心(Registry)。而不同框架实现间最主要的区别就体现在注册中心数据的组织:地址如何组织、以什么粒度组织、除地址外还同步哪些数据?</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-1.png" alt="img1"></p> |
| <p>我们今天这篇文章就是围绕这三个角色展开,重点看下 Dubbo 中对于服务发现方案的设计,包括之前老的服务发现方案的优势和缺点,以及 Dubbo 3.0 中正在设计、开发中的全新的<strong>面向应用粒度的地址发现方案</strong>,我们期待这个新的方案能做到:</p> |
| <ul> |
| <li><strong>支持几十万/上百万级集群实例的地址发现</strong></li> |
| <li><strong>与不同的微服务体系(如 Spring Cloud)实现在地址发现层面的互通</strong></li> |
| </ul> |
| <h2 id="2-dubbo-地址发现机制解析">2 Dubbo 地址发现机制解析</h2> |
| <p>我们先以一个 DEMO 应用为例,来快速的看一下 Dubbo “接口粒度”服务发现与“应用粒度”服务发现体现出来的区别。这里我们重点关注 Provider 实例是如何向注册中心注册的,并且,为了体现注册中心数据量变化,我们观察的是两个 Provider 实例的场景。</p> |
| <p><strong>应用 DEMO 提供的服务列表如下:</strong></p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:service</span> interface=<span style="color:#2aa198">&#34;org.apache.dubbo.samples.basic.api.DemoService&#34;</span> ref=<span style="color:#2aa198">&#34;demoService&#34;</span><span style="color:#268bd2">/&gt;</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:service</span> interface=<span style="color:#2aa198">&#34;org.apache.dubbo.samples.basic.api.GreetingService&#34;</span> ref=<span style="color:#2aa198">&#34;greetingService&#34;</span><span style="color:#268bd2">/&gt;</span> |
| </span></span></code></pre></div><p>我们示例注册中心实现采用的是 Zookeeper ,启动 192.168.0.103 和 192.168.0.104 两个实例后,以下是两种模式下注册中心的实际数据</p> |
| <p><strong>1. “接口粒度” 服务发现</strong></p> |
| <p>192.168.0.103 实例注册的数据</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>dubbo://192.168.0.103:20880/org.apache.dubbo.samples.basic.api.DemoService?anyhost=true&amp;application=demo-provider&amp;default=true&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;interface=org.apache.dubbo.samples.basic.api.DemoService&amp;methods=testVoid,sayHello&amp;pid=995&amp;release=2.7.7&amp;side=provider&amp;timestamp=1596988171266 |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>dubbo://192.168.0.103:20880/org.apache.dubbo.samples.basic.api.GreetingService?anyhost=true&amp;application=demo-provider&amp;default=true&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;interface=org.apache.dubbo.samples.basic.api.GreetingService&amp;methods=greeting&amp;pid=995&amp;release=2.7.7&amp;side=provider&amp;timestamp=1596988170816 |
| </span></span></code></pre></div><p>192.168.0.104 实例注册的数据</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>dubbo://192.168.0.104:20880/org.apache.dubbo.samples.basic.api.DemoService?anyhost=true&amp;application=demo-provider&amp;default=true&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;interface=org.apache.dubbo.samples.basic.api.DemoService&amp;methods=testVoid,sayHello&amp;pid=995&amp;release=2.7.7&amp;side=provider&amp;timestamp=1596988171266 |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>dubbo://192.168.0.104:20880/org.apache.dubbo.samples.basic.api.GreetingService?anyhost=true&amp;application=demo-provider&amp;default=true&amp;deprecated=false&amp;dubbo=2.0.2&amp;dynamic=true&amp;generic=false&amp;interface=org.apache.dubbo.samples.basic.api.GreetingService&amp;methods=greeting&amp;pid=995&amp;release=2.7.7&amp;side=provider&amp;timestamp=1596988170816 |
| </span></span></code></pre></div><p><strong>2. “应用粒度” 服务发现</strong></p> |
| <p>192.168.0.103 与 192.168.0.104 两个实例共享一份注册中心数据,如下:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;name&#34;</span>: <span style="color:#2aa198">&#34;demo-provider&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;id&#34;</span>: <span style="color:#2aa198">&#34;192.168.0.103:20880&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;address&#34;</span>: <span style="color:#2aa198">&#34;192.168.0.103&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;port&#34;</span>: <span style="color:#2aa198">20880</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;metadata&#34;</span>: { |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;dubbo.endpoints&#34;</span>: <span style="color:#2aa198">&#34;[{\&#34;port\&#34;:20880,\&#34;protocol\&#34;:\&#34;dubbo\&#34;}]&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;dubbo.metadata.storage-type&#34;</span>: <span style="color:#2aa198">&#34;local&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;dubbo.revision&#34;</span>: <span style="color:#2aa198">&#34;6785535733750099598&#34;</span> |
| </span></span><span style="display:flex;"><span> }, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;time&#34;</span>: <span style="color:#2aa198">1583461240877</span> |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;name&#34;</span>: <span style="color:#2aa198">&#34;demo-provider&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;id&#34;</span>: <span style="color:#2aa198">&#34;192.168.0.104:20880&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;address&#34;</span>: <span style="color:#2aa198">&#34;192.168.0.104&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;port&#34;</span>: <span style="color:#2aa198">20880</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;metadata&#34;</span>: { |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;dubbo.endpoints&#34;</span>: <span style="color:#2aa198">&#34;[{\&#34;port\&#34;:20880,\&#34;protocol\&#34;:\&#34;dubbo\&#34;}]&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;dubbo.metadata.storage-type&#34;</span>: <span style="color:#2aa198">&#34;local&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;dubbo.revision&#34;</span>: <span style="color:#2aa198">&#34;7829635812370099387&#34;</span> |
| </span></span><span style="display:flex;"><span> }, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;time&#34;</span>: <span style="color:#2aa198">1583461240877</span> |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><p>对比以上两种不同粒度的服务发现模式,从 “接口粒度” 升级到 “应用粒度” 后我们可以总结出最大的区别是:注册中心数据量不再与接口数成正比,不论应用提供有多少接口,注册中心只有一条实例数据。</p> |
| <p>那么接下来我们详细看下这个变化给 Dubbo 带来了哪些好处。</p> |
| <h2 id="3-dubbo-应用级服务发现的意义">3 Dubbo 应用级服务发现的意义</h2> |
| <p>我们先说结论,应用级服务发现给 Dubbo 带来以下优势:</p> |
| <ol> |
| <li>与业界主流微服务模型对齐,比如 SpringCloud、Kubernetes Native Service等。</li> |
| <li>提升性能与可伸缩性。注册中心数据的重新组织(减少),能最大幅度的减轻注册中心的存储、推送压力,进而减少 Dubbo Consumer 侧的地址计算压力;集群规模也开始变得可预测、可评估(与 RPC 接口数量无关,只与实例部署规模相关)。</li> |
| </ol> |
| <h3 id="31-对齐主流微服务模型">3.1 对齐主流微服务模型</h3> |
| <p>自动、透明的实例地址发现(负载均衡)是所有微服务框架需要解决的事情,这能让后端的部署结构对上游微服务透明,上游服务只需要从收到的地址列表中选取一个,发起调用就可以了。要实现以上目标,涉及两个关键点的自动同步:</p> |
| <ul> |
| <li>实例地址,服务消费方需要知道地址以建立连接</li> |
| <li>RPC 方法定义,服务消费方需要知道 RPC 服务的具体定义,不论服务类型是 rest 或 rmi 等。</li> |
| </ul> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-2.png" alt="img2"></p> |
| <p>对于 RPC 实例间借助注册中心的数据同步,REST 定义了一套非常有意思的成熟度模型,感兴趣的朋友可以参考这里的链接 <a href="https://www.martinfowler.com/articles/richardsonMaturityModel.html">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 服务这部分信息目前都是通过线下约定或离线的管理系统来协商的。这种架构的优缺点总结如下。 |
| 优势:部署结构清晰、地址推送量小; |
| 缺点:地址订阅需要指定应用名, provider 应用变更(拆分)需消费端感知;RPC 调用无法全自动同步。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-3.png" alt="img3"></p> |
| <p><strong>2. Dubbo</strong></p> |
| <p>Dubbo 通过注册中心同时同步了实例地址和 RPC 方法,因此其能实现 RPC 过程的自动同步,面向 RPC 编程、面向 RPC 治理,对后端应用的拆分消费端无感知,其缺点则是地址推送数量变大,和 RPC 方法成正比。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-4.png" alt="img4"></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://kubernetes.io/docs/concepts/services-networking/service/">Service Name</a>(通常对应到 Application Name)查询到一组 Endpoints(一组运行 provider 的 pod),通过将 Endpoints 映射到 Dubbo 内部地址列表,以驱动 Dubbo 内置的负载均衡机制工作。</li> |
| </ul> |
| <blockquote> |
| <p>Kubernetes Service 作为一个抽象概念,怎么映射到 Dubbo 是一个值得讨论的点</p> |
| <ul> |
| <li>Service Name - &gt; Application Name,Dubbo 应用和 Kubernetes 服务一一对应,对于微服务运维和建设环节透明,与开发阶段解耦。</li> |
| </ul> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#268bd2">apiVersion</span>: v1 |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">kind</span>: Service |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">metadata</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">name</span>: provider-app-name |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">spec</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">selector</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">app</span>: provider-app-name |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">ports</span>: |
| </span></span><span style="display:flex;"><span> - <span style="color:#268bd2">protocol</span>: TCP |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">port</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">targetPort</span>: <span style="color:#2aa198">9376</span> |
| </span></span></code></pre></div><ul> |
| <li>Service Name - &gt; Dubbo RPC Service,Kubernetes 要维护调度的服务与应用内建 RPC 服务绑定,维护的服务数量变多。</li> |
| </ul> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>--- |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">apiVersion</span>: v1 |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">kind</span>: Service |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">metadata</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">name</span>: rpc-service-1 |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">spec</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">selector</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">app</span>: provider-app-name |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">ports</span>: <span style="color:#586e75">##</span> |
| </span></span><span style="display:flex;"><span>... |
| </span></span><span style="display:flex;"><span>--- |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">apiVersion</span>: v1 |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">kind</span>: Service |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">metadata</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">name</span>: rpc-service-2 |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">spec</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">selector</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">app</span>: provider-app-name |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">ports</span>: <span style="color:#586e75">##</span> |
| </span></span><span style="display:flex;"><span>... |
| </span></span><span style="display:flex;"><span>--- |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">apiVersion</span>: v1 |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">kind</span>: Service |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">metadata</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">name</span>: rpc-service-N |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">spec</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">selector</span>: |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">app</span>: provider-app-name |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">ports</span>: <span style="color:#586e75">##</span> |
| </span></span><span style="display:flex;"><span>... |
| </span></span></code></pre></div></blockquote> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-5.png" alt="img5"></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://developer.aliyun.com/article/740260">Dubbo 如何成为连接异构微服务体系的最佳服务开发框架</a></p> |
| <h3 id="32-更大规模的微服务集群---解决性能瓶颈">3.2 更大规模的微服务集群 - 解决性能瓶颈</h3> |
| <p>这部分涉及到和注册中心、配置中心的交互,关于不同模型下注册中心数据的变化,之前原理部分我们简单分析过。为更直观的对比服务模型变更带来的推送效率提升,我们来通过一个示例看一下不同模型注册中心的对比:</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-6.png" alt="img6"></p> |
| <p>图中左边是微服务框架的一个典型工作流程,Provider 和 Consumer 通过注册中心实现自动化的地址通知。其中,Provider 实例的信息如图中表格所示: |
| 应用 DEMO 包含三个接口 DemoService 1 2 3,当前实例的 ip 地址为 10.210.134.30。</p> |
| <ul> |
| <li>对于 Spring Cloud 和 Kubernetes 模型,注册中心只会存储一条 <code>DEMO - 10.210.134.30+metadata</code> 的数据;</li> |
| <li>对于老的 Dubbo 模型,注册中心存储了三条接口粒度的数据,分别对应三个接口 DemoService 1 2 3,并且很多的址数据都是重复的;</li> |
| </ul> |
| <p>可以总结出,基于应用粒度的模型所存储和推送的数据量是和应用、实例数成正比的,只有当我们的应用数增多或应用的实例数增长时,地址推送压力才会上涨。 |
| 而对于基于接口粒度的模型,数据量是和接口数量正相关的,鉴于一个应用通常发布多个接口的现状,这个数量级本身比应用粒度是要乘以倍数的;另外一个关键点在于,接口粒度导致的集群规模评估的不透明,相对于实例、应用增长都通常是在运维侧的规划之中,接口的定义更多的是业务侧的内部行为,往往可以绕过评估给集群带来压力。</p> |
| <p>以 Consumer 端服务订阅举例,根据我对社区部分 Dubbo 中大规模头部用户的粗略统计,根据受统计公司的实际场景,一个 Consumer 应用要消费(订阅)的 Provier 应用数量往往要超过 10 个,而具体到其要消费(订阅)的的接口数量则通常要达到 30 个,平均情况下 Consumer 订阅的 3 个接口来自同一个 Provider 应用,如此计算下来,如果以应用粒度为地址通知和选址基本单位,则平均地址推送和计算量将下降 60% 还要多, |
| 而在极端情况下,也就是当 Consumer 端消费的接口更多的来自同一个应用时,这个地址推送与内存消耗的占用将会进一步得到降低,甚至可以超过 80% 以上。</p> |
| <p>一个典型的极端场景即是 Dubbo 体系中的网关型应用,有些网关应用消费(订阅)达 100+ 应用,而消费(订阅)的服务有 1000+ ,平均有 10 个接口来自同一个应用,如果我们把地址推送和计算的粒度改为应用,则地址推送量从原来的 n * 1000 变为 n * 100,地址数量降低可达近 90%。</p> |
| <h2 id="4-应用级服务发现工作原理">4 应用级服务发现工作原理</h2> |
| <h3 id="41-设计原则">4.1 设计原则</h3> |
| <p>上面一节我们从<strong>服务模型</strong>及<strong>支撑大规模集群</strong>的角度分别给出了 Dubbo 往应用级服务发现靠拢的好处和原因,但这么做的同时接口粒度的服务治理能力还是要继续保留,这是 Dubbo 框架编程模型易用性、服务治理能力优势的基础。 |
| 以下是我认为我们做服务模型迁移仍要坚持的设计原则</p> |
| <ul> |
| <li>新的服务发现模型要实现对原有 Dubbo 消费端开发者的无感知迁移,即 Dubbo 继续面向 RPC 服务编程、面向 RPC 服务治理,做到对用户侧完全无感知。</li> |
| <li>建立 Consumer 与 Provider 间的自动化 RPC 服务元数据协调机制,解决传统微服务模型无法同步 RPC 级接口配置的缺点。</li> |
| </ul> |
| <h3 id="42-基本原理详解">4.2 基本原理详解</h3> |
| <p>应用级服务发现作为一种新的服务发现机制,和以前 Dubbo 基于 RPC 服务粒度的服务发现在核心流程上基本上是一致的:即服务提供者往注册中心注册地址信息,服务消费者从注册中心拉取&amp;订阅地址信息。</p> |
| <p>这里主要的不同有以下两点:</p> |
| <h4 id="421-注册中心数据以应用---实例列表格式组织不再包含-rpc-服务信息">4.2.1 注册中心数据以“应用 - 实例列表”格式组织,不再包含 RPC 服务信息</h4> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-7.png" alt="img7"></p> |
| <p>以下是每个 Instance metadata 的示例数据,总的原则是 metadata 只包含当前 instance 节点相关的信息,不涉及 RPC 服务粒度的信息。</p> |
| <p>总体信息概括如下:实例地址、实例各种环境标、metadata service 元数据、其他少量必要属性。</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;name&#34;</span>: <span style="color:#2aa198">&#34;provider-app-name&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;id&#34;</span>: <span style="color:#2aa198">&#34;192.168.0.102:20880&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;address&#34;</span>: <span style="color:#2aa198">&#34;192.168.0.102&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;port&#34;</span>: <span style="color:#2aa198">20880</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;sslPort&#34;</span>: <span style="color:#cb4b16">null</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;payload&#34;</span>: { |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;id&#34;</span>: <span style="color:#cb4b16">null</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;name&#34;</span>: <span style="color:#2aa198">&#34;provider-app-name&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;metadata&#34;</span>: { |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;metadataService&#34;</span>: <span style="color:#2aa198">&#34;{\&#34;dubbo\&#34;:{\&#34;version\&#34;:\&#34;1.0.0\&#34;,\&#34;dubbo\&#34;:\&#34;2.0.2\&#34;,\&#34;release\&#34;:\&#34;2.7.5\&#34;,\&#34;port\&#34;:\&#34;20881\&#34;}}&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;endpoints&#34;</span>: <span style="color:#2aa198">&#34;[{\&#34;port\&#34;:20880,\&#34;protocol\&#34;:\&#34;dubbo\&#34;}]&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;storage-type&#34;</span>: <span style="color:#2aa198">&#34;local&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;revision&#34;</span>: <span style="color:#2aa198">&#34;6785535733750099598&#34;</span>, |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> }, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;registrationTimeUTC&#34;</span>: <span style="color:#2aa198">1583461240877</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;serviceType&#34;</span>: <span style="color:#2aa198">&#34;DYNAMIC&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#268bd2">&#34;uriSpec&#34;</span>: <span style="color:#cb4b16">null</span> |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><h4 id="422-client--server-自行协商-rpc-方法信息">4.2.2 Client – Server 自行协商 RPC 方法信息</h4> |
| <p>在注册中心不再同步 RPC 服务信息后,服务自省在服务消费端和提供端之间建立了一条内置的 RPC 服务信息协商机制,这也是“服务自省”这个名字的由来。服务端实例会暴露一个预定义的 MetadataService RPC 服务,消费端通过调用 MetadataService 获取每个实例 RPC 方法相关的配置信息。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-8.png" alt="img8"></p> |
| <p>当前 MetadataService 返回的数据格式如下,</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>[ |
| </span></span><span style="display:flex;"><span> <span style="color:#2aa198">&#34;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&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#2aa198">&#34;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&#34;</span>, |
| </span></span><span style="display:flex;"><span> <span style="color:#2aa198">&#34;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&#34;</span> |
| </span></span><span style="display:flex;"><span>] |
| </span></span></code></pre></div><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://dubbo.apache.org/imgs/blog/service-discovery-9.png" alt="img9"></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="43-服务自省中的关键机制">4.3 服务自省中的关键机制</h3> |
| <h4 id="431-元数据同步机制">4.3.1 元数据同步机制</h4> |
| <p>Client 与 Server 间在收到地址推送后的配置同步是服务自省的关键环节,目前针对元数据同步有两种具体的可选方案,分别是:</p> |
| <ul> |
| <li>内建 MetadataService。</li> |
| <li>独立的元数据中心,通过中心化的元数据集群协调数据。</li> |
| </ul> |
| <p><strong>1. 内建 MetadataService</strong> |
| MetadataService 通过标准的 Dubbo 协议暴露,根据查询条件,会将内存中符合条件的“普通服务”配置返回给消费者。这一步发生在消费端选址和调用前。</p> |
| <p><strong>2. 元数据中心</strong> |
| 复用 2.7 版本中引入的元数据中心,provider 实例启动后,会尝试将内部的 RPC 服务组织成元数据的格式同步到元数据中心,而 consumer 则在每次收到注册中心推送更新后,主动查询元数据中心。</p> |
| <blockquote> |
| <p>注意 consumer 端查询元数据中心的时机,是等到注册中心的地址更新通知之后。也就是通过注册中心下发的数据,我们能明确的知道何时某个实例的元数据被更新了,此时才需要去查元数据中心。</p> |
| </blockquote> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-10.png" alt="img10"></p> |
| <h4 id="432-rpc-服务-----应用映射关系">4.3.2 RPC 服务 &lt; - &gt; 应用映射关系</h4> |
| <p>回顾上文讲到的注册中心关于“应用 - 实例列表”结构的数据组织形式,这个变动目前对开发者并不是完全透明的,业务开发侧会感知到查询/订阅地址列表的机制的变化。具体来说,相比以往我们基于 RPC 服务来检索地址,现在 consumer 需要通过指定 provider 应用名才能实现地址查询或订阅。</p> |
| <p>老的 Consumer 开发与配置示例:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#586e75">&lt;!-- 框架直接通过 RPC Service 1/2/N 去注册中心查询或订阅地址列表 --&gt;</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:registry</span> address=<span style="color:#2aa198">&#34;zookeeper://127.0.0.1:2181&#34;</span><span style="color:#268bd2">/&gt;</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:reference</span> interface=<span style="color:#2aa198">&#34;RPC Service 1&#34;</span> <span style="color:#268bd2">/&gt;</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:reference</span> interface=<span style="color:#2aa198">&#34;RPC Service 2&#34;</span> <span style="color:#268bd2">/&gt;</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:reference</span> interface=<span style="color:#2aa198">&#34;RPC Service N&#34;</span> <span style="color:#268bd2">/&gt;</span> |
| </span></span></code></pre></div><p>新的 Consumer 开发与配置示例:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#586e75">&lt;!-- 框架需要通过额外的 provided-by=&#34;provider-app-x&#34; 才能在注册中心查询或订阅到地址列表 --&gt;</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:registry</span> address=<span style="color:#2aa198">&#34;zookeeper://127.0.0.1:2181?registry-type=service&#34;</span><span style="color:#268bd2">/&gt;</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:reference</span> interface=<span style="color:#2aa198">&#34;RPC Service 1&#34;</span> provided-by=<span style="color:#2aa198">&#34;provider-app-x&#34;</span><span style="color:#268bd2">/&gt;</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:reference</span> interface=<span style="color:#2aa198">&#34;RPC Service 2&#34;</span> provided-by=<span style="color:#2aa198">&#34;provider-app-x&#34;</span> <span style="color:#268bd2">/&gt;</span> |
| </span></span><span style="display:flex;"><span><span style="color:#268bd2">&lt;dubbo:reference</span> interface=<span style="color:#2aa198">&#34;RPC Service N&#34;</span> provided-by=<span style="color:#2aa198">&#34;provider-app-y&#34;</span> <span style="color:#268bd2">/&gt;</span> |
| </span></span></code></pre></div><p>以上指定 provider 应用名的方式是 Spring Cloud 当前的做法,需要 consumer 端的开发者显示指定其要消费的 provider 应用。</p> |
| <p>以上问题的根源在于注册中心不知道任何 RPC 服务相关的信息,因此只能通过应用名来查询。</p> |
| <p>为了使整个开发流程对老的 Dubbo 用户更透明,同时避免指定 provider 对可扩展性带来的影响(参见下方说明),我们设计了一套 <code>RPC 服务到应用名</code>的映射关系,以尝试在 consumer 端自动完成 RPC 服务到 provider 应用名的转换。</p> |
| <p><img src="https://dubbo.apache.org/imgs/blog/service-discovery-11.png" alt="img11"></p> |
| <blockquote> |
| <p>Dubbo 之所以选择建立一套“接口-应用”的映射关系,主要是考虑到 service - app 映射关系的不确定性。一个典型的场景即是应用/服务拆分,如上面提到的配置<code>&lt;dubbo:reference interface=&quot;RPC Service 2&quot; provided-by=&quot;provider-app-x&quot; /&gt;</code>,PC Service 2 是定义于 provider-app-x 中的一个服务,未来它随时可能会被开发者分拆到另外一个新的应用如 provider-app-x-1 中,这个拆分要被所有的 PC Service 2 消费方感知到,并对应用进行修改升级,如改为<code>&lt;dubbo:reference interface=&quot;RPC Service 2&quot; provided-by=&quot;provider-app-x-1&quot; /&gt;</code>,这样的升级成本不可否认还是挺高的。 |
| 到底是 Dubbo 框架帮助开发者透明的解决这个问题,还是交由开发者自己去解决,当然这只是个策略选择问题,并且 Dubbo 2.7.5+ 版本目前是都提供了的。其实我个人更倾向于交由业务开发者通过组织上的约束来做,这样也可进一步降低 Dubbo 框架的复杂度,提升运行态的稳定性。</p> |
| </blockquote> |
| <h2 id="5-总结与展望">5 总结与展望</h2> |
| <p>应用级服务发现机制是 Dubbo 面向云原生走出的重要一步,它帮 Dubbo 打通了与其他微服务体系之间在地址发现层面的鸿沟,也成为 Dubbo 适配 Kubernetes Native Service 等基础设施的基础。我们期望 Dubbo 在新模型基础上,能继续保留在编程易用性、服务治理能力等方面强大的优势。但是我们也应该看到应用粒度的模型一方面带来了新的复杂性,需要我们继续去优化与增强;另一方面,除了地址存储与推送之外,应用粒度在帮助 Dubbo 选址层面也有进一步挖掘的潜力。</p></description></item><item><title>Blog: Dubbo 中的 URL 统一模型</title><link>https://dubbo.apache.org/zh-cn/blog/2019/10/17/dubbo-%E4%B8%AD%E7%9A%84-url-%E7%BB%9F%E4%B8%80%E6%A8%A1%E5%9E%8B/</link><pubDate>Thu, 17 Oct 2019 00:00:00 +0000</pubDate><guid>https://dubbo.apache.org/zh-cn/blog/2019/10/17/dubbo-%E4%B8%AD%E7%9A%84-url-%E7%BB%9F%E4%B8%80%E6%A8%A1%E5%9E%8B/</guid><description> |
| <h3 id="定义">定义</h3> |
| <p>在不谈及 dubbo 时,我们大多数人对 URL 这个概念并不会感到陌生。统一资源定位器 (<a href="https://www.ietf.org/rfc/rfc1738.txt">RFC1738</a>――Uniform Resource Locators (URL))应该是最广为人知的一个 RFC 规范,它的定义也非常简单</p> |
| <blockquote> |
| <p>因特网上的可用资源可以用简单字符串来表示,该文档就是描述了这种字符串的语法和语 |
| 义。而这些字符串则被称为:“统一资源定位器”(URL)</p> |
| </blockquote> |
| <p><strong>一个标准的 URL 格式</strong>至多可以包含如下的几个部分</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>protocol://username:password@host:port/path?key=value&amp;key=value |
| </span></span></code></pre></div><p><strong>一些典型 URL</strong></p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>http://www.facebook.com/friends?param1=value1&amp;amp;param2=value2 |
| </span></span><span style="display:flex;"><span>https://username:password@10.20.130.230:8080/list?version=1.0.0 |
| </span></span><span style="display:flex;"><span>ftp://username:password@192.168.1.7:21/1/read.txt |
| </span></span></code></pre></div><p>当然,也有一些<strong>不太符合常规的 URL</strong>,也被归类到了 URL 之中</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>192.168.1.3:20880 |
| </span></span><span style="display:flex;"><span>url protocol = null, url host = 192.168.1.3, port = 20880, url path = null |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>file:///home/user1/router.js?type=script |
| </span></span><span style="display:flex;"><span>url protocol = file, url host = null, url path = home/user1/router.js |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>file://home/user1/router.js?type=script&lt;br&gt; |
| </span></span><span style="display:flex;"><span>url protocol = file, url host = home, url path = user1/router.js |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>file:///D:/1/router.js?type=script |
| </span></span><span style="display:flex;"><span>url protocol = file, url host = null, url path = D:/1/router.js |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>file:/D:/1/router.js?type=script |
| </span></span><span style="display:flex;"><span>同上 file:///D:/1/router.js?type=script |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>/home/user1/router.js?type=script |
| </span></span><span style="display:flex;"><span>url protocol = null, url host = null, url path = home/user1/router.js |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>home/user1/router.js?type=script |
| </span></span><span style="display:flex;"><span>url protocol = null, url host = home, url path = user1/router.js |
| </span></span></code></pre></div><h3 id="dubbo-中的-url">Dubbo 中的 URL</h3> |
| <p>在 dubbo 中,也使用了类似的 URL,主要用于在各个扩展点之间传递数据,组成此 URL 对象的具体参数如下:</p> |
| <ul> |
| <li>protocol:一般是 dubbo 中的各种协议 如:dubbo thrift http zk</li> |
| <li>username/password:用户名/密码</li> |
| <li>host/port:主机/端口</li> |
| <li>path:接口名称</li> |
| <li>parameters:参数键值对</li> |
| </ul> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#268bd2">public</span> <span style="color:#268bd2">URL</span>(String protocol, String username, String password, String host, <span style="color:#dc322f">int</span> port, String path, Map<span style="color:#719e07">&lt;</span>String, String<span style="color:#719e07">&gt;</span> parameters) { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> ((username <span style="color:#719e07">==</span> <span style="color:#cb4b16">null</span> <span style="color:#719e07">||</span> username.length() <span style="color:#719e07">==</span> 0) |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">&amp;&amp;</span> password <span style="color:#719e07">!=</span> <span style="color:#cb4b16">null</span> <span style="color:#719e07">&amp;&amp;</span> password.length() <span style="color:#719e07">&gt;</span> 0) { |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">throw</span> <span style="color:#719e07">new</span> IllegalArgumentException(<span style="color:#2aa198">&#34;Invalid url, password without username!&#34;</span>); |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">this</span>.protocol <span style="color:#719e07">=</span> protocol; |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">this</span>.username <span style="color:#719e07">=</span> username; |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">this</span>.password <span style="color:#719e07">=</span> password; |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">this</span>.host <span style="color:#719e07">=</span> host; |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">this</span>.port <span style="color:#719e07">=</span> (port <span style="color:#719e07">&lt;</span> 0 <span style="color:#719e07">?</span> 0 : port); |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">this</span>.path <span style="color:#719e07">=</span> path; |
| </span></span><span style="display:flex;"><span> <span style="color:#586e75">// trim the beginning &#34;/&#34;</span> |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">while</span>(path <span style="color:#719e07">!=</span> <span style="color:#cb4b16">null</span> <span style="color:#719e07">&amp;&amp;</span> path.startsWith(<span style="color:#2aa198">&#34;/&#34;</span>)) { |
| </span></span><span style="display:flex;"><span> path <span style="color:#719e07">=</span> path.substring(1); |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">if</span> (parameters <span style="color:#719e07">==</span> <span style="color:#cb4b16">null</span>) { |
| </span></span><span style="display:flex;"><span> parameters <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> HashMap<span style="color:#719e07">&lt;</span>String, String<span style="color:#719e07">&gt;</span>(); |
| </span></span><span style="display:flex;"><span> } <span style="color:#719e07">else</span> { |
| </span></span><span style="display:flex;"><span> parameters <span style="color:#719e07">=</span> <span style="color:#719e07">new</span> HashMap<span style="color:#719e07">&lt;</span>String, String<span style="color:#719e07">&gt;</span>(parameters); |
| </span></span><span style="display:flex;"><span> } |
| </span></span><span style="display:flex;"><span> <span style="color:#719e07">this</span>.parameters <span style="color:#719e07">=</span> Collections.unmodifiableMap(parameters); |
| </span></span><span style="display:flex;"><span>} |
| </span></span></code></pre></div><p>可以看出,dubbo 认为 protocol,username,passwored,host,port,path 是主要的 URL 参数,其他键值对存放在 parameters 之中。</p> |
| <p><strong>一些典型的 Dubbo URL</strong></p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000 |
| </span></span><span style="display:flex;"><span>描述一个 dubbo 协议的服务 |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-consumer&amp;dubbo=2.0.2&amp;interface=org.apache.dubbo.registry.RegistryService&amp;pid=1214&amp;qos.port=33333&amp;timestamp=1545721981946 |
| </span></span><span style="display:flex;"><span>描述一个 zookeeper 注册中心 |
| </span></span><span style="display:flex;"><span> |
| </span></span><span style="display:flex;"><span>consumer://30.5.120.217/org.apache.dubbo.demo.DemoService?application=demo-consumer&amp;category=consumers&amp;check=false&amp;dubbo=2.0.2&amp;interface=org.apache.dubbo.demo.DemoService&amp;methods=sayHello&amp;pid=1209&amp;qos.port=33333&amp;side=consumer&amp;timestamp=1545721827784 |
| </span></span><span style="display:flex;"><span>描述一个消费者 |
| </span></span></code></pre></div><p>可以说,任意的一个领域中的一个实现都可以认为是一类 URL,dubbo 使用 URL 来统一描述了元数据,配置信息,贯穿在整个框架之中。</p> |
| <h3 id="url-相关的生命周期">URL 相关的生命周期</h3> |
| <h4 id="rpc调用">RPC调用</h4> |
| <p>从地址发现的视角,URL 代表了一条可用的 provider 实例地址,除了地址信息之外还有相关的配置信息,这些配置信息是层次化的,有 provider 侧指定的配置值、consumer 侧指定的配置值、接口级别的配置值、方法级别的配置值等。这些 URL 配置值将直接影响消费端的 RPC 调用行为。</p> |
| <p>以 timeout 为例,下图显示了配置的查找顺序,其它 retries, loadbalance, actives 等类似:</p> |
| <ul> |
| <li>方法级优先,接口级次之,全局配置再次之。</li> |
| <li>如果级别一样,则消费方优先,提供方次之。</li> |
| </ul> |
| <p>其中,服务提供方配置,通过 URL 经由注册中心传递给消费方。</p> |
| <p><img src="https://dubbo.apache.org/imgs/user/dubbo-config-override.jpg" alt="dubbo-config-override"></p> |
| <p>(建议由服务提供方设置超时,因为一个方法需要执行多长时间,服务提供方更清楚,如果一个消费方同时引用多个服务,就不需要关心每个服务的超时设置)。</p> |
| <p>理论上 ReferenceConfig 中除了<code>interface</code>这一项,其他所有配置项都可以缺省不配置,框架会自动使用ConsumerConfig,ServiceConfig, ProviderConfig等提供的缺省配置。</p> |
| <h4 id="解析服务">解析服务</h4> |
| <p>基于 dubbo.jar 内的 <code>META-INF/spring.handlers</code> 配置,Spring 在遇到 dubbo 名称空间时,会回调 <code>DubboNamespaceHandler</code>。</p> |
| <p>所有 dubbo 的标签,都统一用 <code>DubboBeanDefinitionParser</code> 进行解析,基于一对一属性映射,将 XML 标签解析为 Bean 对象。</p> |
| <p>在 <code>ServiceConfig.export()</code> 或 <code>ReferenceConfig.get()</code> 初始化时,将 Bean 对象转换 URL 格式,所有 Bean 属性转成 URL 的参数。</p> |
| <p>然后将 URL 传给协议扩展点,基于扩展点自适应机制,根据 URL 的协议头,进行不同协议的服务暴露或引用。</p> |
| <h4 id="暴露服务">暴露服务</h4> |
| <p><strong>1. 只暴露服务端口:</strong></p> |
| <p>在没有注册中心,直接暴露提供者的情况下,<code>ServiceConfig</code> 解析出的 URL 的格式为:<code>dubbo://service-host/com.foo.FooService?version=1.0.0</code>。</p> |
| <p>基于扩展点自适应机制,通过 URL 的 <code>dubbo://</code> 协议头识别,直接调用 <code>DubboProtocol</code>的 <code>export()</code> 方法,打开服务端口。</p> |
| <p><strong>2. 向注册中心暴露服务:</strong></p> |
| <p>在有注册中心,需要注册提供者地址的情况下,<code>ServiceConfig</code> 解析出的 URL 的格式为: <code>registry://registry-host/org.apache.dubbo.registry.RegistryService?export=URL.encode(&quot;dubbo://service-host/com.foo.FooService?version=1.0.0&quot;)</code>,</p> |
| <p>基于扩展点自适应机制,通过 URL 的 <code>registry://</code> 协议头识别,就会调用 <code>RegistryProtocol</code> 的 <code>export()</code> 方法,将 <code>export</code> 参数中的提供者 URL,先注册到注册中心。</p> |
| <p>再重新传给 <code>Protocol</code> 扩展点进行暴露: <code>dubbo://service-host/com.foo.FooService?version=1.0.0</code>,然后基于扩展点自适应机制,通过提供者 URL 的 <code>dubbo://</code> 协议头识别,就会调用 <code>DubboProtocol</code> 的 <code>export()</code> 方法,打开服务端口。</p> |
| <h4 id="引用服务">引用服务</h4> |
| <p><strong>1. 直连引用服务:</strong></p> |
| <p>在没有注册中心,直连提供者的情况下,<code>ReferenceConfig</code> 解析出的 URL 的格式为:<code>dubbo://service-host/com.foo.FooService?version=1.0.0</code>。</p> |
| <p>基于扩展点自适应机制,通过 URL 的 <code>dubbo://</code> 协议头识别,直接调用 <code>DubboProtocol</code> 的 <code>refer()</code> 方法,返回提供者引用。</p> |
| <p><strong>2. 从注册中心发现引用服务:</strong></p> |
| <p>在有注册中心,通过注册中心发现提供者地址的情况下,<code>ReferenceConfig</code> 解析出的 URL 的格式为:<code>registry://registry-host/org.apache.dubbo.registry.RegistryService?refer=URL.encode(&quot;consumer://consumer-host/com.foo.FooService?version=1.0.0&quot;)</code>。</p> |
| <p>基于扩展点自适应机制,通过 URL 的 <code>registry://</code> 协议头识别,就会调用 <code>RegistryProtocol</code> 的 <code>refer()</code> 方法,基于 <code>refer</code> 参数中的条件,查询提供者 URL,如: <code>dubbo://service-host/com.foo.FooService?version=1.0.0</code>。</p> |
| <p>基于扩展点自适应机制,通过提供者 URL 的 <code>dubbo://</code> 协议头识别,就会调用 <code>DubboProtocol</code> 的 <code>refer()</code> 方法,得到提供者引用。</p> |
| <p>然后 <code>RegistryProtocol</code> 将多个提供者引用,通过 <code>Cluster</code> 扩展点,伪装成单个提供者引用返回。</p> |
| <h3 id="url-统一模型的意义">URL 统一模型的意义</h3> |
| <p>对于 dubbo 中的 URL,有人理解为配置总线,有人理解为统一配置模型,说法虽然不同,但都是在表达一个意思,这样的 URL 在 dubbo 中被当做是<a href="https://dubbo.apache.org/zh-cn/docsv2.7/dev/contract/">公共契约</a>,所有扩展点参数都包含 URL 参数,URL 作为上下文信息贯穿整个扩展点设计体系。</p> |
| <p>在没有 URL 之前,只能以字符串传递参数,不停的解析和拼装,导致相同类型的接口,参数时而 Map, 时而 Parameters 类包装:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span>export(String url) |
| </span></span><span style="display:flex;"><span>createExporter(String host, <span style="color:#dc322f">int</span> port, Parameters params) |
| </span></span></code></pre></div><p>使用 URL 一致性模型:</p> |
| <div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span>export(URL url) |
| </span></span><span style="display:flex;"><span>createExporter(URL url) |
| </span></span></code></pre></div><p>在最新的 dubbo 代码中,我们可以看到大量使用 URL 来进行上下文之间信息的传递,这样的好处是显而易见的:</p> |
| <ol> |
| <li>使得代码编写者和阅读者能够将一系列的参数联系起来,进而形成规范,使得代码易写,易读。</li> |
| <li>可扩展性强,URL 相当于参数的集合(相当于一个 Map),他所表达的含义比单个参数更丰富,当我们在扩展代码时,可以将新的参数追加到 URL 之中,而不需要改变入参,返参的结构。</li> |
| <li>统一模型,它位于 org.apache.dubbo.common 包中,各个扩展模块都可以使用它作为参数的表达形式,简化了概念,降低了代码的理解成本。</li> |
| </ol> |
| <p>如果你能够理解 final 契约和 restful 契约,那我相信你会很好地理解 URL 契约。契约的好处我还是啰嗦一句:大家都这么做,就形成了默契,沟通是一件很麻烦的事,统一 URL 模型可以省去很多沟通成本,这边是 URL 统一模型存在的意义。</p></description></item></channel></rss> |