This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
<audio id="audio" title="11 | 服务发布和引用的实践" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f0/c6/f0c610b7b21be428960db2513fba9cc6.mp3"></audio>
在[专栏第4期](http://time.geekbang.org/column/article/14425)我给你讲解了服务发布和引用常见的三种方式Restful API、XML配置以及IDL文件。今天我将以XML配置方式为例给你讲解服务发布和引用的具体实践以及可能会遇到的问题。
首先我们一起来看下XML配置方式服务发布和引用的具体流程是什么<!-- [[[read_end]]] -->样的。
## XML配置方式的服务发布和引用流程
**1. 服务提供者定义接口**
服务提供者发布服务之前首先要定义接口声明接口名、传递参数以及返回值类型然后把接口打包成JAR包发布出去。
比如下面这段代码声明了接口UserLastStatusService包含两个方法getLastStatusId和getLastStatusIds传递参数一个是long值、一个是long数组返回值一个是long值、一个是map。
```
package com.weibo.api.common.status.service;
public interface UserLastStatusService {
* @param uids
* @return
*/
public long getLastStatusId(long uid);
/**
*
* @param uids
* @return
*/
public Map&lt;Long, Long&gt; getLastStatusIds(long[] uids);
}
```
**2. 服务提供者发布接口**
服务提供者发布的接口是通过在服务发布配置文件中定义接口来实现的。
下面我以一个具体的服务发布配置文件user-last-status.xml来给你讲解它定义了要发布的接口userLastStatusLocalService对外暴露的协议是Motan协议端口是8882。并且针对两个方法getLastStatusId和getLastStatusIds通过requestTimeout=&quot;300&quot;单独定义了超时时间是300ms通过retries=&quot;0&quot;单独定义了调用失败后重试次数为0也就是不重试。
```
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;
xsi:schemaLocation=&quot;http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
&quot;&gt;
&lt;motan:service ref=&quot;userLastStatusLocalService&quot;
requestTimeout=&quot;50&quot; retries=&quot;2&quot; interface=&quot;com.weibo.api.common.status.service.UserLastStatusService&quot;
basicService=&quot;serviceBasicConfig&quot; export=&quot;motan:8882&quot;&gt;
&lt;motan:method name=&quot;getLastStatusId&quot; requestTimeout=&quot;300&quot;
retries=&quot;0&quot; /&gt;
&lt;motan:method name=&quot;getLastStatusIds&quot; requestTimeout=&quot;300&quot;
retries=&quot;0&quot; /&gt;
&lt;/motan:service&gt;
&lt;/beans&gt;
```
然后服务发布者在进程启动的时候会加载配置文件user-last-status.xml把接口对外暴露出去。
**3. 服务消费者引用接口**
服务消费者引用接口是通过在服务引用配置文件中定义要引用的接口并把包含接口定义的JAR包引入到代码依赖中。
下面我再以一个具体的服务引用配置文件user-last-status-client.xml来给你讲解它定义服务消费者引用了接口commonUserLastStatusService接口通信协议是Motan。
```
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;
xsi:schemaLocation=&quot;http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
&quot;&gt;
&lt;motan:protocol name=&quot;motan&quot; default=&quot;true&quot; loadbalance=&quot;${service.loadbalance.name}&quot; /&gt;
&lt;motan:basicReferer id=&quot;userLastStatusServiceClientBasicConfig&quot;
protocol=&quot;motan&quot; /&gt;
&lt;!-- 导出接口 --&gt;
&lt;motan:referer id=&quot;commonUserLastStatusService&quot; interface=&quot;com.weibo.api.common.status.service.UserLastStatusService&quot;
basicReferer=&quot;userLastStatusServiceClientBasicConfig&quot; /&gt;
&lt;/beans&gt;
```
然后服务消费者在进程启动时会加载配置文件user-last-status-client.xml来完成服务引用。
上面所讲的服务发布和引用流程看似比较简单,但在实际使用过程中,还是有很多坑的,比如在实际项目中经常会遇到这个问题:一个服务包含了多个接口,可能有上行接口也可能有下行接口,每个接口都有超时控制以及是否重试等配置,如果有多个服务消费者引用这个服务,是不是每个服务消费者都必须在服务引用配置文件中定义?
你可以先思考一下这个问题,联系自己的实践经验,是否有理想的解决方案呢?
## 服务发布和引用的那些坑
根据我的项目经验,在一个服务被多个服务消费者引用的情况下,由于业务经验的参差不齐,可能不同的服务消费者对服务的认知水平不一,比如某个服务可能调用超时了,最好可以重试来提供调用成功率。但可能有的服务消费者会忽视这一点,并没有在服务引用配置文件中配置接口调用超时重试的次数,**因此最好是可以在服务发布的配置文件中预定义好类似超时重试次数**,即使服务消费者没有在服务引用配置文件中定义,也能继承服务提供者的定义。这就是下面要讲的服务发布预定义配置。
**1. 服务发布预定义配置**
以下面的服务发布配置文件server.xml为例它提供了一个服务contentSliceRPCService并且明确了其中三个方法的调用超时时间为500ms以及超时重试次数为3。
```
&lt;motan:service ref=&quot;contentSliceRPCService&quot; interface=&quot;cn.sina.api.data.service.ContentSliceRPCService&quot;
basicService=&quot;serviceBasicConfig&quot; export=&quot;motan:8882&quot; &gt;
&lt;motan:method name=&quot;saveContent&quot; requestTimeout=&quot;500&quot;
retries=&quot;3&quot; /&gt;
&lt;motan:method name=&quot;deleteContent&quot; requestTimeout=&quot;500&quot;
retries=&quot;3&quot; /&gt;
&lt;motan:method name=&quot;updateContent&quot; requestTimeout=&quot;500&quot;
retries=&quot;3&quot; /&gt;
&lt;/motan:service&gt;
```
假设服务引用的配置文件client.xml的内容如下那么服务消费者就会默认继承服务发布配置文件中设置的方法调用的超时时间以及超时重试次数。
```
&lt;motan:referer id=&quot;contentSliceRPCService&quot; interface=&quot;cn.sina.api.data.service.ContentSliceRPCService&quot; basicReferer=&quot;contentSliceClientBasicConfig&quot; &gt;
&lt;/motan:referer&gt;
```
通过服务发布预定义配置可以解决多个服务消费者引用服务可能带来的配置复杂的问题,这样是不是最优的解决方案呢?
实际上我还遇到过另外一种极端情况,一个服务提供者发布的服务有上百个方法,并且每个方法都有各自的超时时间、重试次数等信息。服务消费者引用服务时,完全继承了服务发布预定义的各项配置。这种情况下,服务提供者所发布服务的详细配置信息都需要存储在注册中心中,这样服务消费者才能在实际引用时从服务发布预定义配置中继承各种配置。
这里就存在一种风险当服务提供者发生节点变更尤其是在网络频繁抖动的情况下所有的服务消费者都会从注册中心拉取最新的服务节点信息就包括了服务发布配置中预定的各项接口信息这个信息不加限制的话可能达到1M以上如果同时有上百个服务消费者从注册中心拉取服务节点信息在注册中心机器部署为百兆带宽的情况下很有可能会导致网络带宽打满的情况发生。
面对这种情况,**最好的办法是把服务发布端的详细服务配置信息转移到服务引用端**,这样的话注册中心中就不需要存储服务提供者发布的详细服务配置信息了。这就是下面要讲的服务引用定义配置。
**2. 服务引用定义配置**
以下面的服务发布配置文件为例它详细定义了服务userInfoService的各个方法的配置信息比如超时时间和重试次数等。
```
&lt;motan:service ref=&quot;userInfoService&quot; requestTimeout=&quot;50&quot; retries=&quot;2&quot; interface=&quot;cn.sina.api.user.service.UserInfoService&quot; basicService=&quot;serviceBasicConfig&quot;&gt;
&lt;motan:method name=&quot;addUserInfo&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;updateUserPortrait&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;modifyUserInfo&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;addUserTags&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;delUserTags&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;processUserCacheByNewMyTriggerQ&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;modifyObjectUserInfo&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;addObjectUserInfo&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;updateObjectUserPortrait&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;updateObjectManager&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;add&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;deleteObjectManager&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;getUserAttr&quot; requestTimeout=&quot;300&quot; retries=&quot;1&quot; /&gt;
&lt;motan:method name=&quot;getUserAttrList&quot; requestTimeout=&quot;300&quot; retries=&quot;1&quot; /&gt;
&lt;motan:method name=&quot;getAllUserAttr&quot; requestTimeout=&quot;300&quot; retries=&quot;1&quot; /&gt;
&lt;motan:method name=&quot;getUserAttr2&quot; requestTimeout=&quot;300&quot; retries=&quot;1&quot; /&gt;
&lt;/motan:service&gt;
```
可以像下面一样把服务userInfoService的详细配置信息转移到服务引用配置文件中。
```
&lt;motan:referer id=&quot;userInfoService&quot; interface=&quot;cn.sina.api.user.service.UserInfoService&quot; basicReferer=&quot;userClientBasicConfig&quot;&gt;
&lt;motan:method name=&quot;addUserInfo&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;updateUserPortrait&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;modifyUserInfo&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;addUserTags&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;delUserTags&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;processUserCacheByNewMyTriggerQ&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;modifyObjectUserInfo&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;addObjectUserInfo&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;updateObjectUserPortrait&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;updateObjectManager&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;add&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;deleteObjectManager&quot; requestTimeout=&quot;300&quot; retries=&quot;0&quot;/&gt;
&lt;motan:method name=&quot;getUserAttr&quot; requestTimeout=&quot;300&quot; retries=&quot;1&quot; /&gt;
&lt;motan:method name=&quot;getUserAttrList&quot; requestTimeout=&quot;300&quot; retries=&quot;1&quot; /&gt;
&lt;motan:method name=&quot;getAllUserAttr&quot; requestTimeout=&quot;300&quot; retries=&quot;1&quot; /&gt;
&lt;motan:method name=&quot;getUserAttr2&quot; requestTimeout=&quot;300&quot; retries=&quot;1&quot; /&gt;
&lt;/motan:referer&gt;
```
这样的话,服务发布配置文件可以简化为下面这段代码,是不是信息精简了许多。
```
&lt;motan:service ref=&quot;userInfoService&quot; requestTimeout=&quot;50&quot; retries=&quot;2&quot; interface=&quot;cn.sina.api.user.service.UserInfoService&quot; basicService=&quot;serviceBasicConfig&quot;&gt;
&lt;/motan:service&gt;
```
在进行类似的服务详细信息配置,由服务发布配置文件迁移到服务引用配置文件的过程时,尤其要注意迁移步骤问题,这就是接下来我要给你讲的服务配置升级问题。
**3. 服务配置升级**
实际项目中,我就经历过一次服务配置升级的过程。由于引用服务的服务消费者众多,并且涉及多个部门,升级步骤就显得异常重要,通常可以按照下面步骤操作。
<li>
各个服务消费者在服务引用配置文件中添加服务详细信息。
</li>
<li>
服务提供者升级两台服务器,在服务发布配置文件中删除服务详细信息,并观察是否所有的服务消费者引用时都包含服务详细信息。
</li>
<li>
如果都包含,说明所有服务消费者均完成升级,那么服务提供者就可以删除服务发布配置中的服务详细信息。
</li>
<li>
如果有不包含服务详细信息的服务消费者,排查出相应的业务方进行升级,直至所有业务方完成升级。
</li>
## 总结
今天我给你介绍了XML配置方式的服务发布和引用的具体流程简单来说就是服务提供者定义好接口并且在服务发布配置文件中配置要发布的接口名在进程启动时加载服务发布配置文件就可以对外提供服务了。而服务消费者通过在服务引用配置文件中定义相同的接口名并且在服务引用配置文件中配置要引用的接口名在进程启动时加载服务引用配置文件就可以引用服务了。
在业务具体实践过程中可能会遇到引用服务的服务消费者众多,对业务的敏感度参差不齐的问题,所以在服务发布的时候,最好预定义好接口的各种配置。在服务规模不大,业务比较简单的时候,这样做比较合适。但是对于复杂业务,虽然服务发布时预定义好接口的各种配置,但在引用的服务消费者众多且同时访问的时候,可能会引起网络风暴。这种情况下,比较保险的方式是,把接口的各种配置放在服务引用配置文件里。
在进行服务配置升级过程时,要考虑好步骤,在所有服务消费者完成升级之前,服务提供者还不能把服务的详细信息去掉,否则可能会导致没有升级的服务消费者引用异常。
## 思考题
如果你在实际项目中采用过XML配置的服务发布和应用方式是否还遇到过其他问题你是如何解决的呢
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,170 @@
<audio id="audio" title="12 | 如何将注册中心落地?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a3/21/a3663e5b9518f7745b29201777d28f21.mp3"></audio>
专栏第5期我给你讲了服务注册和发现的原理这里面的核心是服务提供者、服务消费者和注册中心这三个概念以及它们之间的交互关系。你可以先回顾一下这几个关键的知识点如果有不清楚的地方建议你先返回[第5期](http://time.geekbang.org/column/article/14603)复习一下,再开始今天的学习。
掌握了服务注册和发现的原理之后,我们就需要考虑如何把注册中心落地实现。结合前面所讲的服务注册与发现的流程,在落地注册中心的过程中,我们需要解决一系列的问题,包括如何存储服务信息、如何注册节点、如何反注册、如何查询节点信息以及如何订阅服务变更等。这些问题你都知道如何解决吗?如果还没答案,没关系,下面我来给你一一讲解。
## 注册中心如何存储服务信息
注册中心既然是用来存储服务信息的,那么服务信息都包含哪些内容呢?
根据我的实践经验服务信息除了包含节点信息IP和端口号以外还包含其他一些信息比如请求失败时重试的次数、请求结果是否压缩等信息。因此服务信息通常用JSON字符串来存储包含多个字段每个字段代表不同的含义。
除此之外,服务一般会分成多个不同的分组,每个分组的目的不同。一般来说有下面几种分组方式。
<li>
核心与非核心,从业务的核心程度来分。
</li>
<li>
机房,从机房的维度来分。
</li>
<li>
线上环境与测试环境,从业务场景维度来区分。
</li>
所以注册中心存储的服务信息一般包含三部分内容:分组、服务名以及节点信息,节点信息又包括节点地址和节点其他信息。从注册中心中获取的信息结构大致如下图所示。
<img src="https://static001.geekbang.org/resource/image/fa/d8/faef4be8f5d42f5e67f300d4b20158d8.png" alt="">
具体存储的时候,一般是按照“服务-分组-节点信息”三层结构来存储可以用下图来描述。Service代表服务的具体分组Cluster代表服务的接口名节点信息用KV存储。
<img src="https://static001.geekbang.org/resource/image/1f/46/1f8da1497fdf18d75bdcc13315ddcc46.jpg" alt="">
搞清楚了注册中心存储服务信息的原理后,再来看下注册中心具体是如何工作的,包括四个流程。
<li>
服务提供者注册流程。
</li>
<li>
服务提供者反注册流程。
</li>
<li>
服务消费者查询流程。
</li>
<li>
服务消费者订阅变更流程。
</li>
接下来,我来给你具体讲解上面四个流程的实现方式。
## 注册中心是如何工作的
**1. 如何注册节点**
知道了服务的节点信息如何存储之后,服务注册流程是怎么样的呢?可以用下面这张流程图来描述。
<img src="https://static001.geekbang.org/resource/image/26/e6/26719c9b542a6dc0a8e9e4656489c2e6.png" alt="">
根据我的经验,服务注册流程主要有下面几个步骤:
<li>
首先查看要注册的节点是否在白名单内?如果不在就抛出异常,在的话继续下一步。
</li>
<li>
其次要查看注册的Cluster服务的接口名是否存在如果不存在就抛出异常存在的话继续下一步。
</li>
<li>
然后要检查Service服务的分组是否存在如果不存在则抛出异常存在的话继续下一步。
</li>
<li>
最后将节点信息添加到对应的Service和Cluster下面的存储中。
</li>
**2. 如何反注册**
再来看下服务提供者节点反注册的流程,可以用下面这张流程图来描述。
<img src="https://static001.geekbang.org/resource/image/6d/ff/6d49ee407b74881ebaded01ef6181fff.png" alt="">
根据我的经验,节点反注册流程主要包含下面几个步骤:
<li>
查看Service服务的分组是否存在不存在就抛出异常存在就继续下一步。
</li>
<li>
查看Cluster服务的接口名是否存在不存在就抛出异常存在就继续下一步。
</li>
<li>
删除存储中Service和Cluster下对应的节点信息。
</li>
<li>
更新Cluster的sign值。
</li>
**3. 如何查询节点信息**
关于服务消费者是如何从注册中心查询服务提供者的节点信息,可以用下面这张流程图来描述。
<img src="https://static001.geekbang.org/resource/image/2a/85/2af5590e0a6f57c51905522b81263085.png" alt="">
服务消费者查询节点信息主要分为下面几个步骤:
<li>
首先从localcache本机内存中查找如果没有就继续下一步。这里为什么服务消费者要把服务信息存在本机内存呢主要是因为服务节点信息并不总是时刻变化的并不需要每一次服务调用都要调用注册中心获取最新的节点信息只需要在本机内存中保留最新的服务提供者的节点列表就可以。
</li>
<li>
接着从snapshot本地快照中查找如果没有就继续下一步。这里为什么服务消费者要在本地磁盘存储一份服务提供者的节点信息的快照呢这是因为服务消费者同注册中心之间的网络不一定总是可靠的服务消费者重启时本机内存中还不存在服务提供者的节点信息如果此时调用注册中心失败那么服务消费者就拿不到服务节点信息了也就没法调用了。本地快照就是为了防止这种情况的发生即使服务消费者重启后请求注册中心失败依然可以读取本地快照获取到服务节点信息。
</li>
**4. 如何订阅服务变更**
最后看下,服务消费者如何订阅服务提供者的变更信息呢?可以用下面这张流程图来描述。
<img src="https://static001.geekbang.org/resource/image/d6/f1/d6db90f412fbc9b89a0cf94cda474af1.png" alt="">
主要分为下面几个步骤:
<li>
服务消费者从注册中心获取了服务的信息后就订阅了服务的变化会在本地保留Cluster的sign值。
</li>
<li>
服务消费者每隔一段时间调用getSign()函数从注册中心获取服务端该Cluster的sign值并与本地保留的sign值做对比如果不一致就从服务端拉取新的节点信息并更新localcache和snapshot。
</li>
这里小结一下,以上就是服务注册和反注册、服务查询和服务订阅变更的基本流程。除此之外,我再给你讲下我在实际项目实践中,实现服务注册与发现时遇到的几个问题,希望能给你帮助。
## 注册与发现的几个问题
**1. 多注册中心**
理论上对于一个服务消费者来说,同一个注册中心交互是最简单的。但是不可避免的是,服务消费者可能订阅了多个服务,多个服务可能是由多个业务部门提供的,而且每个业务部门都有自己的注册中心,提供的服务只在自己的注册中心里有记录。这样的话,就要求服务消费者要具备在启动时,能够从多个注册中心订阅服务的能力。
根据我的经验,还有一种情况是,一个服务提供者提供了某个服务,可能作为静态服务对外提供,有可能又作为动态服务对外提供,这两个服务部署在不同的注册中心,所以要求服务提供者在启动的时候,要能够同时向多个注册中心注册服务。
也就是说,对于服务消费者来说,要能够同时从多个注册中心订阅服务;对于服务提供者来说,要能够同时向多个注册中心注册服务。
**2. 并行订阅服务**
通常一个服务消费者订阅了不止一个服务,在我经历的一个项目中,一个服务消费者订阅了几十个不同的服务,每个服务都有自己的方法列表以及节点列表。服务消费者在服务启动时,会加载订阅的服务配置,调用注册中心的订阅接口,获取每个服务的节点列表并初始化连接。
最开始我们采用了串行订阅的方式,每订阅一个服务,服务消费者调用一次注册中心的订阅接口,获取这个服务的节点列表并初始化连接,总共需要执行几十次这样的过程。在某些服务节点的初始化连接过程中,出现连接超时的情况,后续所有的服务节点的初始化连接都需要等待它完成,导致服务消费者启动变慢,最后耗费了将近五分钟时间来完成所有服务节点的初始化连接过程。
后来我们改成了并行订阅的方式每订阅一个服务就单独用一个线程来处理这样的话即使遇到个别服务节点连接超时其他服务节点的初始化连接也不受影响最慢也就是这个服务节点的初始化连接耗费的时间最终所有服务节点的初始化连接耗时控制在了30秒以内。
**3. 批量反注册服务**
通常一个服务提供者节点提供不止一个服务,所以注册和反注册都需要多次调用注册中心。在与注册中心的多次交互中,可能由于网络抖动、注册中心集群异常等原因,导致个别调用失败。对于注册中心来说,偶发的注册调用失败对服务调用基本没有影响,其结果顶多就是某一个服务少了一个可用的节点。但偶发的反注册调用失败会导致不可用的节点残留在注册中心中,变成“僵尸节点”,但服务消费者端还会把它当成“活节点”,继续发起调用,最终导致调用失败。
以前我们的业务中经常遇到这个问题,需要定时去清理注册中心中的“僵尸节点”。后来我们通过优化反注册逻辑,对于下线机器、节点销毁的场景,通过调用注册中心提供的批量反注册接口,一次调用就可以把该节点上提供的所有服务同时反注册掉,从而避免了“僵尸节点”的出现。
**4. 服务变更信息增量更新**
服务消费者端启动时除了会查询订阅服务的可用节点列表做初始化连接还会订阅服务的变更每隔一段时间从注册中心获取最新的服务节点信息标记sign并与本地保存的sign值作比对如果不一样就会调用注册中心获取最新的服务节点信息。
一般情况下,按照这个过程是没问题的,但是在网络频繁抖动时,服务提供者上报给注册中心的心跳可能会一会儿失败一会儿成功,这时候注册中心就会频繁更新服务的可用节点信息,导致服务消费者频繁从注册中心拉取最新的服务可用节点信息,严重时可能产生网络风暴,导致注册中心带宽被打满。
为了减少服务消费者从注册中心中拉取的服务可用节点信息的数据量,这个时候可以通过增量更新的方式,注册中心只返回变化的那部分节点信息,尤其在只有少数节点信息变更时,此举可以大大减少服务消费者从注册中心拉取的数据量,从而最大程度避免产生网络风暴。
## 总结
今天我给你讲解了在注册中心实际使用过程中,服务注册、服务反注册、服务订阅和服务变更的实现方式,并列举了几个我在服务注册与发现的过程中遇到的典型问题。
而针对这些异常情况,我都给出了对应的解决方案,这些方案都是经过实际业务验证的,对于大部分中小团队在应用场景面临的问题,应该足以应对。
## 思考题
你的团队在使用注册中心时,是否有遇到过上面这些问题呢?我给出的解决方案是否可以解决你们的问题呢?
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,129 @@
<audio id="audio" title="13 | 开源服务注册中心如何选型?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/88/e4/88a5c64d8188a623afdd9cf8b572d4e4.mp3"></audio>
上一期我给你讲了服务注册中心的落地实践,以及在实际应用中可能会遇到的问题和对应的解决方案。关于注册中心,如果你的团队有足够的人才和技术储备,可以选择自己研发注册中心。但对于大多数中小规模团队来说,我的建议是最好使用业界开源的、应用比较成熟的注册中心解决方案,把精力投入到业务架构的改造中,不要自己造轮子。
当下主流的服务注册与发现的解决方案,主要有两种:
<li>
应用内注册与发现注册中心提供服务端和客户端的SDK业务应用通过引入注册中心提供的SDK通过SDK与注册中心交互来实现服务的注册和发现。
</li>
<li>
应用外注册与发现业务应用本身不需要通过SDK与注册中心打交道而是通过其他方式与注册中心交互间接完成服务注册与发现。
</li>
下面我会用两个业界使用比较成熟的注册中心开源实现,来讲解下应用内和应用外两种解决方案的不同之处。
## 两种典型的注册中心实现
**1. 应用内**
采用应用内注册与发现的方式最典型的案例要属Netflix开源的Eureka官方架构图如下。
<img src="https://static001.geekbang.org/resource/image/d2/1c/d220f8970c8d7a4f4ea4677ec2cbd61c.jpg" alt="" /><br />
[https://github.com/Netflix/eureka/raw/master/images/eureka_architecture.png](https://github.com/Netflix/eureka/raw/master/images/eureka_architecture.png)
对着这张图我来介绍下Eureka的架构它主要由三个重要的组件组成
<li>
Eureka Server注册中心的服务端实现了服务信息注册、存储以及查询等功能。
</li>
<li>
服务端的Eureka Client集成在服务端的注册中心SDK服务提供者通过调用SDK实现服务注册、反注册等功能。
</li>
<li>
客户端的Eureka Client集成在客户端的注册中心SDK服务消费者通过调用SDK实现服务订阅、服务更新等功能。
</li>
**2. 应用外**
采用应用外方式实现服务注册和发现最典型的案例是开源注册中心Consul它的架构图如下。
<img src="https://static001.geekbang.org/resource/image/da/3f/da82d0cba1c49252e1ae48f91fcb543f.png" alt="" /><br />
[https://technologyconversations.files.wordpress.com/2015/09/etcd-registrator-confd2.png](https://technologyconversations.files.wordpress.com/2015/09/etcd-registrator-confd2.png)
通过这张架构图可以看出来使用Consul实现应用外服务注册和发现主要依靠三个重要的组件
<li>
Consul注册中心的服务端实现服务注册信息的存储并提供注册和发现服务。
</li>
<li>
[Registrator](https://github.com/gliderlabs/registrator)一个开源的第三方服务管理器项目它通过监听服务部署的Docker实例是否存活来负责服务提供者的注册和销毁。
</li>
<li>
[Consul Template](https://github.com/hashicorp/consul-template)定时从注册中心服务端获取最新的服务提供者节点列表并刷新LB配置比如Nginx的upstream这样服务消费者就通过访问Nginx就可以获取最新的服务提供者信息。
</li>
对比小结一下这两种解决方案的不同之处在于应用场景应用内的解决方案一般适用于服务提供者和服务消费者同属于一个技术体系应用外的解决方案一般适合服务提供者和服务消费者采用了不同技术体系的业务场景比如服务提供者提供的是C++服务而服务消费者是一个Java应用这时候采用应用外的解决方案就不依赖于具体一个技术体系。同时对于容器化后的云应用来说一般不适合采用应用内SDK的解决方案因为这样会侵入业务而应用外的解决方案正好能够解决这个问题。
## 注册中心选型要考虑的两个问题
在选择注册中心解决方案的时候,除了要考虑是采用应用内注册还是应用外注册的方式以外,还有两个最值得关注的问题,一个是高可用性,一个是数据一致性,下面我来给你详细解释下为什么。
**1. 高可用性**
注册中心作为服务提供者和服务消费者之间沟通的纽带,它的高可用性十分重要。试想,如果注册中心不可用了,那么服务提供者就无法对外暴露自己的服务,而服务消费者也无法知道自己想要调用的服务的具体地址,后果将不堪设想。
根据我过往的实践经验,实现高可用性的方法主要有两种:
<li>
集群部署,顾名思义就是通过部署多个实例组成集群来保证高可用性,这样的话即使有部分机器宕机,将访问迁移到正常的机器上就可以保证服务的正常访问。
</li>
<li>
多IDC部署就是部署在不止一个机房这样能保证即使一个机房因为断电或者光缆被挖断等不可抗力因素不可用时仍然可以通过把请求迁移到其他机房来保证服务的正常访问。
</li>
我们以Consul为例来看看它是如何通过这两种方法来保证注册中心的高可用性。
从下面的官方架构图中你可以看到一方面在每个数据中心DATACENTER内都有多个注册中心Server节点可供访问另一方面还可以部署在多个数据中心来保证多机房高可用性。
<img src="https://static001.geekbang.org/resource/image/c0/ab/c0661d7687e29927fdcecc0f140fb5ab.png" alt="" /><br />
[https://www.consul.io/assets/images/consul-arch-420ce04a.png](https://www.consul.io/assets/images/consul-arch-420ce04a.png)
**2. 数据一致性**
为了保证注册中心的高可用性,注册中心的部署往往都采用集群部署,并且还通常部署在不止一个数据中心,这样的话就会引出另一个问题,多个数据中心之间如何保证数据一致?如何确保访问数据中心中任何一台机器都能得到正确的数据?
这里就涉及分布式系统中著名的CAP理论即同时满足一致性、可用性、分区容错性这三者是不可能的其中CConsistency代表一致性AAvailability代表可用性PPartition Tolerance代表分区容错性。
为什么说CAP三者不能被同时满足的呢
你可以想象在一个分布式系统里面,包含了多个节点,节点之间通过网络连通在一起。正常情况下,通过网络,从一个节点可以访问任何别的节点上的数据。
但是有可能出现网络故障,导致整个网络被分成了互不连通的区域,这就叫作分区。一旦出现分区,那么一个区域内的节点就没法访问其他节点上的数据了,最好的办法是把数据复制到其他区域内的节点,这样即使出现分区,也能访问任意区域内节点上的数据,这就是分区容错性。
但是把数据复制到多个节点就可能出现数据不一致的情况,这就是一致性。要保证一致,就必须等待所有节点上的数据都更新成功才可用,这就是可用性。
总的来说,就是数据节点越多,分区容错性越高,但数据一致性越难保证。为了保证数据一致性,又会带来可用性的问题。
而注册中心一般采用分布式集群部署也面临着CAP的问题根据CAP不能同时满足所以不同的注册中心解决方案选择的方向也就不同大致可分为两种。
<li>
CP型注册中心牺牲可用性来保证数据强一致性最典型的例子就是ZooKeeperetcdConsul了。ZooKeeper集群内只有一个Leader而且在Leader无法使用的时候通过Paxos算法选举出一个新的Leader。这个Leader的目的就是保证写信息的时候只向这个Leader写入Leader会同步信息到Followers这个过程就可以保证数据的强一致性。但如果多个ZooKeeper之间网络出现问题造成出现多个Leader发生脑裂的话注册中心就不可用了。而etcd和Consul集群内都是通过raft协议来保证强一致性如果出现脑裂的话 注册中心也不可用。
</li>
<li>
AP型注册中心牺牲一致性来保证可用性最典型的例子就是Eureka了。对比下ZookeeperEureka不用选举一个Leader每个Eureka服务器单独保存服务注册地址因此有可能出现数据信息不一致的情况。但是当网络出现问题的时候每台服务器都可以完成独立的服务。
</li>
而对于注册中心来说最主要的功能是服务的注册和发现在网络出现问题的时候可用性的需求要远远高于数据一致性。即使因为数据不一致注册中心内引入了不可用的服务节点也可以通过其他措施来避免比如客户端的快速失败机制等只要实现最终一致性对于注册中心来说就足够了。因此选择AP型注册中心一般更加合适。
## 总结
总的来说,在选择开源注册中心解决方案的时候,要看业务的具体场景。
<li>
如果你的业务体系都采用Java语言的话Netflix开源的Eureka是一个不错的选择并且它作为服务注册与发现解决方案能够最大程度的保证可用性即使出现了网络问题导致不同节点间数据不一致你仍然能够访问Eureka获取数据。
</li>
<li>
如果你的业务体系语言比较复杂Eureka也提供了Sidecar的解决方案也可以考虑使用Consul它支持了多种语言接入包括Go、Python、PHP、Scala、JavaErlang、Ruby、Node.js、.NET、Perl等。
</li>
<li>
如果你的业务已经是云原生的应用可以考虑使用Consul搭配Registrator和Consul Template来实现应用外的服务注册与发现。
</li>
## 思考题
针对你的业务场景,如果要选择一种开源注册中心实现的话,你觉得哪种方案更适合?
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,216 @@
<audio id="audio" title="14 | 开源RPC框架如何选型" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/80/55/804a04f7e6330e35ada75fb3fe121655.mp3"></audio>
[专栏第6期](http://time.geekbang.org/column/article/15092)我给你讲解了RPC远程调用的原理简单回顾一下一个完整的RPC框架主要有三部分组成通信框架、通信协议、序列化和反序列化格式。根据我的经验想要开发一个完整的RPC框架并且应用到线上生产环境至少需要投入三个人力半年以上的时间。这对于大部分中小团队来说人力成本和时间成本都是不可接受的所以我建议还是选择开源的RPC框架比较合适。
那么业界应用比较广泛的开源RPC框架有哪些呢
简单划分的话,主要分为两类:一类是跟某种特定语言平台绑定的,另一类是与语言无关即跨语言平台的。
跟语言平台绑定的开源RPC框架主要有下面几种。
<li>
Dubbo国内最早开源的RPC框架由阿里巴巴公司开发并于2011年末对外开源仅支持Java语言。
</li>
<li>
Motan微博内部使用的RPC框架于2016年对外开源仅支持Java语言。
</li>
<li>
Tars腾讯内部使用的RPC框架于2017年对外开源仅支持C++语言。
</li>
<li>
Spring Cloud国外Pivotal公司2014年对外开源的RPC框架仅支持Java语言最近几年生态发展得比较好是比较火的RPC框架。
</li>
而跨语言平台的开源RPC框架主要有以下几种。
<li>
gRPCGoogle于2015年对外开源的跨语言RPC框架支持常用的C++、Java、Python、Go、Ruby、PHP、Android Java、Objective-C等多种语言。
</li>
<li>
Thrift最初是由Facebook开发的内部系统跨语言的RPC框架2007年贡献给了Apache基金成为Apache开源项目之一支持常用的C++、Java、PHP、Python、Ruby、Erlang等多种语言。
</li>
所以很明显如果你的业务场景仅仅局限于一种语言的话可以选择跟语言绑定的RPC框架中的一种如果涉及多个语言平台之间的相互调用就应该选择跨语言平台的RPC框架。
针对每一种RPC框架它们具体有何区别该如何选择呢接下来我就从每个框架的实现角度来具体给你讲解。当你知道了他们的具体实现也就能知道他们的优缺点以及适用场景了。
## 限定语言平台的开源RPC框架
**1. Dubbo**
先来聊聊DubboDubbo可以说是国内开源最早的RPC框架了目前只支持Java语言它的架构可以用下面这张图展示。
<img src="https://static001.geekbang.org/resource/image/71/f3/7114e779d5e8a20ad9986b8ebc52f2f3.jpg" alt="" /><br />
(图片来源:[https://dubbo.incubator.apache.org/docs/zh-cn/dev/sources/images/dubbo-relation.jpg](https://dubbo.incubator.apache.org/docs/zh-cn/dev/sources/images/dubbo-relation.jpg)
从图中你能看到Dubbo的架构主要包含四个角色其中Consumer是服务消费者Provider是服务提供者Registry是注册中心Monitor是监控系统。
具体的交互流程是Consumer一端通过注册中心获取到Provider节点后通过Dubbo的客户端SDK与Provider建立连接并发起调用。Provider一端通过Dubbo的服务端SDK接收到Consumer的请求处理后再把结果返回给Consumer。
可以看出服务消费者和服务提供者都需要引入Dubbo的SDK才来完成RPC调用因为Dubbo本身是采用Java语言实现的所以要求服务消费者和服务提供者也都必须采用Java语言实现才可以应用。
我们再来看下Dubbo的调用框架是如何实现的。
<li>
通信框架方面Dubbo默认采用了Netty作为通信框架。
</li>
<li>
通信协议方面Dubbo除了支持私有的Dubbo协议外还支持RMI协议、Hession协议、HTTP协议、Thrift协议等。
</li>
<li>
序列化格式方面Dubbo支持多种序列化格式比如Dubbo、Hession、JSON、Kryo、FST等。
</li>
**2. Motan**
Motan是国内另外一个比较有名的开源的RPC框架同样也只支持Java语言实现它的架构可以用下面这张图描述。
<img src="https://static001.geekbang.org/resource/image/08/19/08044dcbdbaaedb30222695be29bc119.jpg" alt="" /><br />
(图片来源:[https://github.com/weibocom/motan/wiki/media/14612352579675.jpg](https://github.com/weibocom/motan/wiki/media/14612352579675.jpg)
Motan与Dubbo的架构类似都需要在Client端服务消费者和Server端服务提供者引入SDK其中Motan框架主要包含下面几个功能模块。
<li>
register用来和注册中心交互包括注册服务、订阅服务、服务变更通知、服务心跳发送等功能。Server端会在系统初始化时通过register模块注册服务Client端会在系统初始化时通过register模块订阅到具体提供服务的Server列表当Server列表发生变更时也由register模块通知Client。
</li>
<li>
protocol用来进行RPC服务的描述和RPC服务的配置管理这一层还可以添加不同功能的filter用来完成统计、并发限制等功能。
</li>
<li>
serialize将RPC请求中的参数、结果等对象进行序列化与反序列化即进行对象与字节流的互相转换默认使用对Java更友好的Hessian 2进行序列化。
</li>
<li>
transport用来进行远程通信默认使用Netty NIO的TCP长链接方式。
</li>
<li>
clusterClient端使用的模块cluster是一组可用的Server在逻辑上的封装包含若干可以提供RPC服务的Server实际请求时会根据不同的高可用与负载均衡策略选择一个可用的Server发起远程调用。
</li>
**3. Tars**
Tars是腾讯根据内部多年使用微服务架构的实践总结而成的开源项目仅支持C++语言,它的架构图如下。
<img src="https://static001.geekbang.org/resource/image/e2/98/e207486467e03ded669380f39aadf098.png" alt="" /><br />
(图片来源:[https://github.com/TarsCloud/Tars/blob/master/docs/images/tars_jiaohu.png](https://github.com/TarsCloud/Tars/blob/master/docs/images/tars_jiaohu.png)
Tars的架构交互主要包括以下几个流程
<li>
服务发布流程在web系统上传server的发布包到patch上传成功后在web上提交发布server请求由registry服务传达到node然后node拉取server的发布包到本地拉起server服务。
</li>
<li>
管理命令流程web系统上的可以提交管理server服务命令请求由registry服务传达到node服务然后由node向server发送管理命令。
</li>
<li>
心跳上报流程server服务运行后会定期上报心跳到nodenode然后把服务心跳信息上报到registry服务由registry进行统一管理。
</li>
<li>
信息上报流程server服务运行后会定期上报统计信息到stat打印远程日志到log定期上报属性信息到prop、上报异常信息到notify、从config拉取服务配置信息。
</li>
<li>
client访问server流程client可以通过server的对象名Obj间接访问serverclient会从registry上拉取server的路由信息如IP、Port信息然后根据具体的业务特性同步或者异步TCP或者UDP方式访问server当然client也可以通过IP/Port直接访问server
</li>
**4. Spring Cloud**
Spring Cloud是为了解决微服务架构中服务治理而提供的一系列功能的开发框架它是完全基于Spring Boot进行开发的Spring Cloud利用Spring Boot特性整合了开源行业中优秀的组件整体对外提供了一套在微服务架构中服务治理的解决方案。因为Spring Boot是用Java语言编写的所以目前Spring Cloud也只支持Java语言平台它的架构图可以用下面这张图来描述。
<img src="https://static001.geekbang.org/resource/image/d7/01/d71df127a2c40acf06b3fba6deb42501.png" alt="" /><br />
(图片来源:[http://www.hyhblog.cn/wp-content/uploads/2018/07/Arch-Design-Spring-Cloud-1024x576.png](http://www.hyhblog.cn/wp-content/uploads/2018/07/Arch-Design-Spring-Cloud-1024x576.png)
由此可见Spring Cloud微服务架构是由多个组件一起组成的各个组件的交互流程如下。
<li>
请求统一通过API网关Zuul来访问内部服务先经过Token进行安全认证。
</li>
<li>
通过安全认证后网关Zuul从注册中心Eureka获取可用服务节点列表。
</li>
<li>
从可用服务节点中选取一个可用节点,然后把请求分发到这个节点。
</li>
<li>
整个请求过程中Hystrix组件负责处理服务超时熔断Turbine组件负责监控服务间的调用和熔断相关指标Sleuth组件负责调用链监控ELK负责日志分析。
</li>
**5. 对比选型**
介绍完这4种限定语言的开源RPC框架后我们该如何选择呢
很显然如果你的语言平台是C++那么只能选择Tars而如果是Java的话可以选择Dubbo、Motan或者Spring Cloud。这时你又要问了它们三个又该如何抉择呢
仔细分析可以看出Spring Cloud不仅提供了基本的RPC框架功能还提供了服务注册组件、配置中心组件、负载均衡组件、断路器组件、分布式消息追踪组件等一系列组件也难怪被技术圈的人称之为“Spring Cloud全家桶”。如果你不想自己实现以上这些功能那么Spring Cloud基本可以满足你的全部需求。而Dubbo、Motan基本上只提供了最基础的RPC框架的功能其他微服务组件都需要自己去实现。
不过由于Spring Cloud的RPC通信采用了HTTP协议相比Dubbo和Motan所采用的私有协议来说在高并发的通信场景下性能相对要差一些所以对性能有苛刻要求的情况下可以考虑Dubbo和Motan。
## 跨语言平台的开源RPC框架
**1. gRPC**
先来看下gRPC它的原理是通过IDLInterface Definition Language文件定义服务接口的参数和返回值类型然后通过代码生成程序生成服务端和客户端的具体实现代码这样在gRPC里客户端应用可以像调用本地对象一样调用另一台服务器上对应的方法。
<img src="https://static001.geekbang.org/resource/image/d9/f9/d9acfb00d5e98adbd65306e6a4e761f9.png" alt="" /><br />
(图片来源:[https://grpc.io/img/landing-2.svg](https://grpc.io/img/landing-2.svg)
它的主要特性包括三个方面。
<li>
通信协议采用了HTTP/2因为HTTP/2提供了连接复用、双向流、服务器推送、请求优先级、首部压缩等机制所以在通信过程中可以节省带宽、降低TCP连接次数、节省CPU尤其对于移动端应用来说可以帮助延长电池寿命。
</li>
<li>
IDL使用了[ProtoBuf](https://developers.google.com/protocol-buffers/docs/overview)ProtoBuf是由Google开发的一种数据序列化协议它的压缩和传输效率极高语法也简单所以被广泛应用在数据存储和通信协议上。
</li>
<li>
多语言支持,能够基于多种语言自动生成对应语言的客户端和服务端的代码。
</li>
**2. Thrift**
再来看下ThriftThrift是一种轻量级的跨语言RPC通信方案支持多达25种编程语言。为了支持多种语言跟gRPC一样Thrift也有一套自己的接口定义语言IDL可以通过代码生成器生成各种编程语言的Client端和Server端的SDK代码这样就保证了不同语言之间可以相互通信。它的架构图可以用下图来描述。
<img src="https://static001.geekbang.org/resource/image/d5/62/d5c959122758f1915d6ae4f89247e062.png" alt="" /><br />
(图片来源:[https://github.com/apache/thrift/raw/master/doc/images/thrift-layers.png](https://github.com/apache/thrift/raw/master/doc/images/thrift-layers.png)
从这张图上可以看出Thrift RPC框架的特性。
<li>
支持多种序列化格式如Binary、Compact、JSON、Multiplexed等。
</li>
<li>
支持多种通信方式如Socket、Framed、File、Memory、zlib等。
</li>
<li>
服务端支持多种处理方式如Simple 、Thread Pool、Non-Blocking等。
</li>
**3. 对比选型**
那么涉及跨语言的服务调用场景到底该选择gRPC还是Thrift呢
从成熟度上来讲Thrift因为诞生的时间要早于gRPC所以使用的范围要高于gRPC在HBase、Hadoop、Scribe、Cassandra等许多开源组件中都得到了广泛地应用。而且Thrift支持多达25种语言这要比gRPC支持的语言更多所以如果遇到gRPC不支持的语言场景下选择Thrift更合适。
但gRPC作为后起之秀因为采用了HTTP/2作为通信协议、ProtoBuf作为数据序列化格式在移动端设备的应用以及对传输带宽比较敏感的场景下具有很大的优势而且开发文档丰富根据ProtoBuf文件生成的代码要比Thrift更简洁一些从使用难易程度上更占优势所以如果使用的语言平台gRPC支持的话建议还是采用gRPC比较好。
## 总结
以上就是我对几种使用最广泛的开源RPC框架的选型建议也是基于它们目前现状所作出的判断从长远来看支持多语言是RPC框架未来的发展趋势。正是基于此判断各个RPC框架都提供了Sidecar组件来支持多语言平台之间的RPC调用。
<li>
Dubbo在去年年底又重启了维护并且宣称要引入Sidecar组件来构建[Dubbo Mesh](https://yq.aliyun.com/articles/604030)提供多语言支持。
</li>
<li>
Motan也在去年对外开源了其内部的Sidecar组件[Motan-go](https://github.com/weibocom/motan-go)目前支持PHP、Java语言之间的相互调用。
</li>
<li>
Spring Cloud也提供了Sidecar组件[spring-cloud-netflix-sideca](https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-sidecar)可以让其他语言也可以使用Spring Cloud的组件。
</li>
所以未来语言不会成为使用上面这几种RPC框架的约束而gRPC和Thrift虽然支持跨语言的RPC调用但是因为它们只提供了最基本的RPC框架功能缺乏一系列配套的服务化组件和服务治理功能的支撑所以使用它们作为跨语言调用的RPC框架就需要自己考虑注册中心、熔断、限流、监控、分布式追踪等功能的实现不过好在大多数功能都有开源实现可以直接采用。
## 思考题
同样是支持跨语言的RPC调用你觉得gRPC这类的跨语言服务框架和Motan-go这类的Sidecar方案有什么区别在使用过程中都需要注意什么
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,250 @@
<audio id="audio" title="15 | 如何搭建一个可靠的监控系统?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9f/36/9fa7120729f9428ad00b0839f29f2936.mp3"></audio>
[专栏第7期](http://time.geekbang.org/column/article/15109)我给你讲解了监控系统的实现原理,先来简单回顾一下,一个监控系统的组成主要涉及四个环节:数据收集、数据传输、数据处理和数据展示。不同的监控系统实现方案,在这四个环节所使用的技术方案不同,适合的业务场景也不一样。
目前,比较流行的开源监控系统实现方案主要有两种:以[ELK](https://www.elastic.co/cn/)为代表的集中式日志解决方案,以及[Graphite](http://graphite.readthedocs.io/en/latest/index.html)、[TICK](https://www.influxdata.com/time-series-platform/)和[Prometheus](https://prometheus.io/)等为代表的时序数据库解决方案。接下来我就以这几个常见的监控系统实现方案,谈谈它们的实现原理,分别适用于什么场景,以及具体该如何做技术选型。
## ELK
ELK是Elasticsearch、Logstash、Kibana三个开源软件产品首字母的缩写它们三个通常配合使用所以被称为ELK Stack它的架构可以用下面的图片来描述。
<img src="https://static001.geekbang.org/resource/image/cd/9a/cd8d76c0ab3a17bf16c19f973e92bb9a.png" alt="" /><br />
(图片来源:[https://cdn-images-1.medium.com/max/1600/1*mwSvtVy_qGz0nTjaYbvwpw.png](https://cdn-images-1.medium.com/max/1600/1*mwSvtVy_qGz0nTjaYbvwpw.png)
这三个软件的功能也各不相同。
<li>
Logstash负责数据收集和传输它支持动态地从各种数据源收集数据并对数据进行过滤、分析、格式化等然后存储到指定的位置。
</li>
<li>
Elasticsearch负责数据处理它是一个开源分布式搜索和分析引擎具有可伸缩、高可靠和易管理等特点基于Apache Lucene构建能对大容量的数据进行接近实时的存储、搜索和分析操作通常被用作基础搜索引擎。
</li>
<li>
Kibana负责数据展示也是一个开源和免费的工具通常和Elasticsearch搭配使用对其中的数据进行搜索、分析并且以图表的方式展示。
</li>
这种架构因为需要在各个服务器上部署Logstash来从不同的数据源收集数据所以比较消耗CPU和内存资源容易造成服务器性能下降因此后来又在Elasticsearch、Logstash、Kibana之外引入了Beats作为数据收集器。相比于LogstashBeats所占系统的CPU和内存几乎可以忽略不计可以安装在每台服务器上做轻量型代理从成百上千或成千上万台机器向Logstash或者直接向Elasticsearch发送数据。
其中Beats支持多种数据源主要包括
<li>
Packetbeat用来收集网络流量数据。
</li>
<li>
Topbeat用来收集系统、进程的CPU和内存使用情况等数据。
</li>
<li>
Filebeat用来收集文件数据。
</li>
<li>
Winlogbeat用来收集Windows事件日志收据。
</li>
Beats将收集到的数据发送到Logstash经过Logstash解析、过滤后再将数据发送到Elasticsearch最后由Kibana展示架构就变成下面这张图里描述的了。
<img src="https://static001.geekbang.org/resource/image/e6/ef/e6ebce07db63a7f0d6e4c03cb458eaef.png" alt="" /><br />
(图片来源:[https://logz.io/wp-content/uploads/2018/08/image21-1024x328.png](https://logz.io/wp-content/uploads/2018/08/image21-1024x328.png)
## Graphite
Graphite的组成主要包括三部分Carbon、Whisper、Graphite-Web它的架构可以用下图来描述。
<li>
Carbon主要作用是接收被监控节点的连接收集各个指标的数据将这些数据写入carbon-cache并最终持久化到Whisper存储文件中去。
</li>
<li>
Whisper一个简单的时序数据库主要作用是存储时间序列数据可以按照不同的时间粒度来存储数据比如1分钟1个点、5分钟1个点、15分钟1个点三个精度来存储监控数据。
</li>
<li>
Graphite-Web一个Web App其主要功能绘制报表与展示即数据展示。为了保证Graphite-Web能及时绘制出图形Carbon在将数据写入Whisper存储的同时会在carbon-cache中同时写入一份数据Graphite-Web会先查询carbon-cache如果没有再查询Whisper存储。
</li>
<img src="https://static001.geekbang.org/resource/image/fc/11/fc78b78efe3c2cb52f478e61a9ebbc11.png" alt="" /><br />
(图片来源:[https://graphiteapp.org/img/architecture_diagram.png](https://graphiteapp.org/img/architecture_diagram.png)
也就是说Carbon负责数据处理Whisper负责数据存储Graphite-Web负责数据展示可见Graphite自身并不包含数据采集组件但可以接入[StatsD](https://github.com/etsy/statsd)等开源数据采集组件来采集数据再传送给Carbon。
其中Carbon对写入的数据格式有一定的要求比如
```
servers.www01.cpuUsage 42 1286269200
products.snake-oil.salesPerMinute 123 1286269200
[one minute passes]
servers.www01.cpuUsageUser 44 1286269260
products.snake-oil.salesPerMinute 119 1286269260
```
其中“servers.www01.cpuUsage 42 1286269200”是“key” + 空格分隔符 + “value + 时间戳”的数据格式“servers.www01.cpuUsage”是以“.”分割的key代表具体的路径信息“42”是具体的值“1286269200”是当前的Unix时间戳。
Graphite-Web对外提供了HTTP API可以查询某个key的数据以绘图展示查询方式如下。
```
http://graphite.example.com/render?target=servers.www01.cpuUsage&amp;
width=500&amp;height=300&amp;from=-24h
```
这个HTTP请求意思是查询key“servers.www01.cpuUsage”在过去24小时的数据并且要求返回500*300大小的数据图。
除此之外Graphite-Web还支持丰富的函数比如
```
target=sumSeries(products.*.salesPerMinute)
```
代表了查询匹配规则“products.*.salesPerMinute”的所有key的数据之和。
## TICK
TICK是Telegraf、InfluxDB、Chronograf、Kapacitor四个软件首字母的缩写是由InfluxData开发的一套开源监控工具栈因此也叫作TICK Stack它的架构可以看用下面这张图来描述。
<img src="https://static001.geekbang.org/resource/image/6e/35/6e5c85e68f0eff409f70f17f846d5335.png" alt="" /><br />
(图片来源:[https://2bjee8bvp8y263sjpl3xui1a-wpengine.netdna-ssl.com/wp-content/uploads/Tick-Stack-Complete.png](https://2bjee8bvp8y263sjpl3xui1a-wpengine.netdna-ssl.com/wp-content/uploads/Tick-Stack-Complete.png)
从这张图可以看出其中Telegraf负责数据收集InfluxDB负责数据存储Chronograf负责数据展示Kapacitor负责数据告警。
这里面InfluxDB对写入的数据格式要求如下。
```
&lt;measurement&gt;[,&lt;tag-key&gt;=&lt;tag-value&gt;...] &lt;field-key&gt;=&lt;field-value&gt;[,&lt;field2-key&gt;=&lt;field2-value&gt;...] [unix-nano-timestamp]
```
下面我用一个具体示例来说明它的格式。
```
cpu,host=serverA,region=us_west value=0.64 1434067467100293230
```
其中“cpu,host=serverA,region=us_west value=0.64 1434067467100293230”代表了host为serverA、region为us_west的服务器CPU的值是0.64时间戳是1434067467100293230时间精确到nano。
## Prometheus
还有一种比较有名的时间序数据库解决方案Prometheus它是一套开源的系统监控报警框架受Google的集群监控系统Borgmon启发由工作在SoundCloud的Google前员工在2012年创建后来作为社区开源项目进行开发并于2015年正式发布2016年正式加入CNCFCloud Native Computing Foundation成为受欢迎程度仅次于Kubernetes的项目它的架构可以用下图来描述。
<img src="https://static001.geekbang.org/resource/image/a4/71/a42d15cb006b41fecc82575b566dbc71.png" alt="" /><br />
(图片来源:[https://prometheus.io/assets/architecture.png](https://prometheus.io/assets/architecture.png)
从这张图可以看出Prometheus主要包含下面几个组件
<li>
Prometheus Server用于拉取metrics信息并将数据存储在时间序列数据库。
</li>
<li>
Jobs/exporters用于暴露已有的第三方服务的metrics给Prometheus Server比如StatsD、Graphite等负责数据收集。
</li>
<li>
Pushgateway主要用于短期jobs由于这类jobs存在时间短可能在Prometheus Server来拉取metrics信息之前就消失了所以这类的jobs可以直接向Prometheus Server推送它们的metrics信息。
</li>
<li>
Alertmanager用于数据报警。
</li>
<li>
Prometheus web UI负责数据展示。
</li>
它的工作流程大致是:
<li>
Prometheus Server定期从配置好的jobs或者exporters中拉取metrics信息或者接收来自Pushgateway发过来的metrics信息。
</li>
<li>
Prometheus Server把收集到的metrics信息存储到时间序列数据库中并运行已经定义好的alert.rules向Alertmanager推送警报。
</li>
<li>
Alertmanager根据配置文件对接收的警报进行处理发出告警。
</li>
<li>
通过Prometheus web UI进行可视化展示。
</li>
Prometheus存储数据也是用的时间序列数据库格式如下。
```
&lt;metric name&gt;{&lt;label name&gt;=&lt;label value&gt;, …}
```
比如下面这段代码代表了位于集群cluster 1上节点IP为1.1.1.1端口为80访问路径为“/a”的http请求的总数为100。
```
http_requests_total{instance=&quot;1.1.1.1:80&quot;,job=&quot;cluster1&quot;,location=&quot;/a&quot;} 100
```
讲到这里,四种监控系统的解决方案都已经介绍完了,接下来我们对比一下这四种方案,看看如何选型。
## 选型对比
我们从监控系统的四个环节来分别对比。
**1. 数据收集**
ELK是通过在每台服务器上部署Beats代理来采集数据Graphite本身没有收据采集组件需要配合使用开源收据采集组件比如StatsDTICK使用了Telegraf作为数据采集组件Prometheus通过jobs/exporters组件来获取StatsD等采集过来的metrics信息。
**2. 数据传输**
ELK是Beats采集的数据传输给Logstash经过Logstash清洗后再传输给ElasticsearchGraphite是通过第三方采集组件采集的数据传输给CarbonTICK是Telegraf采集的数据传输给InfluxDB而Prometheus是Prometheus Server隔一段时间定期去从jobs/exporters拉取数据。可见前三种都是采用“推数据”的方式而Prometheus是采取拉数据的方式因此Prometheus的解决方案对服务端的侵入最小不需要在服务端部署数据采集代理。
**3. 数据处理**
ELK可以对日志的任意字段索引适合多维度的数据查询在存储时间序列数据方面与时间序列数据库相比会有额外的性能和存储开销。除此之外时间序列数据库的几种解决方案都支持多种功能的数据查询处理功能也更强大。
- Graphite通过Graphite-Web支持正则表达式匹配、sumSeries求和、alias给监控项重新命名等函数功能同时还支持这些功能的组合比如下面这个表达式的意思是要查询所有匹配路径“stats.open.profile.*.API._comments_flow”的监控项之和并且把监控项重命名为Total QPS。
```
alias(sumSeries(stats.openapi.profile.*.API._comments_flow.total_count,&quot;Total QPS&quot;)
```
- InfluxDB通过类似SQL语言的InfluxQL能对监控数据进行复杂操作比如查询一分钟CPU的使用率用InfluxDB实现的示例是
```
SELECT 100 - usage_idel FROM &quot;autogen&quot;.&quot;cpu&quot; WHERE time &gt; now() - 1m and &quot;cpu&quot;='cpu0'
```
- Prometheus通过私有的PromQL查询语言如果要和上面InfluxDB实现同样的功能PromQL语句如下看起来更加简洁。
```
100 - (node_cpu{job=&quot;node&quot;,mode=&quot;idle&quot;}[1m])
```
**4. 数据展示**
Graphite、TICK和Prometheus自带的展示功能都比较弱界面也不好看不过好在它们都支持[Grafana](https://grafana.com/)来做数据展示。Grafana是一个开源的仪表盘工具它支持多种数据源比如Graphite、InfluxDB、Prometheus以及Elasticsearch等。ELK采用了Kibana做数据展示Kibana包含的数据展示功能比较强大但只支持Elasticsearch而且界面展示UI效果不如Grafana美观。
## 总结
以上几种监控系统实现方式,所采用的技术均为开源的,其中:
<li>
ELK的技术栈比较成熟应用范围也比较广除了可用作监控系统外还可以用作日志查询和分析。
</li>
<li>
Graphite是基于时间序列数据库存储的监控系统并且提供了功能强大的各种聚合函数比如sum、average、top5等可用于监控分析而且对外提供了API也可以接入其他图形化监控系统如Grafana。
</li>
<li>
TICK的核心在于其时间序列数据库InfluxDB的存储功能强大且支持类似SQL语言的复杂数据处理操作。
</li>
<li>
Prometheus的独特之处在于它采用了拉数据的方式对业务影响较小同时也采用了时间序列数据库存储而且支持独有的PromQL查询语言功能强大而且简洁。
</li>
从对实时性要求角度考虑时间序列数据库的实时性要好于ELK通常可以做到10s级别内的延迟如果对实时性敏感的话建议选择时间序列数据库解决方案。
从使用的灵活性角度考虑几种时间序列数据库的监控处理功能都要比ELK更加丰富使用更灵活也更现代化。
所以如果要搭建一套新的监控系统我建议可以考虑采用Graphite、TICK或者Prometheus其中之一。不过Graphite还需要搭配数据采集系统比如StatsD或者Collectd使用而且界面展示建议使用Grafana接入Graphite的数据源它的效果要比Graphite Web本身提供的界面美观很多。TICK提供了完整的监控系统框架包括从数据采集、数据传输、数据处理再到数据展示不过在数据展示方面同样也建议用Grafana替换掉TICK默认的数据展示组件Chronograf这样展示效果更好。Prometheus因为采用拉数据的方式所以对业务的侵入性最小比较适合Docker封装好的云原生应用比如Kubernetes默认就采用了Prometheus作为监控系统。
## 思考题
通过我今天的讲解你应该知道了Graphite、TICK以及Prometheus存储监控数据都采用了时间序列数据库它们在存储和性能上有什么不同之处吗
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,187 @@
<audio id="audio" title="16 | 如何搭建一套适合你的服务追踪系统?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/42/bb/428d718973ed04ace64bd3a52a06c3bb.mp3"></audio>
[专栏第8期](http://time.geekbang.org/column/article/15273)我给你讲了服务追踪系统的原理以及实现,简单回顾一下服务追踪系统的实现,主要包括三个部分。
<li>
埋点数据收集,负责在服务端进行埋点,来收集服务调用的上下文数据。
</li>
<li>
实时数据处理负责对收集到的链路信息按照traceId和spanId进行串联和存储。
</li>
<li>
数据链路展示,把处理后的服务调用数据,按照调用链的形式展示出来。
</li>
如果要自己从0开始实现一个服务追踪系统针对以上三个部分你都必须有相应的解决方案。首先你需要在业务代码的框架层开发调用拦截程序在调用的前后收集相关信息把信息传输给到一个统一的处理中心。然后处理中心需要实时处理收集到链路信息并按照traceId和spanId进行串联处理完以后再存到合适的存储中。最后还要能把存储中存储的信息以调用链路图或者调用拓扑图的形式对外展示。
可以想象这个技术难度以及开发工作量都不小,对于大部分中小业务团队来说,都十分具有挑战。不过幸运的是,业界已经有不少开源的服务追踪系统实现,并且应用范围也已经十分广泛,对大部分的中小业务团队来说,足以满足对服务追踪系统的需求。
业界比较有名的服务追踪系统实现有阿里的鹰眼、Twitter开源的OpenZipkin还有Naver开源的Pinpoint它们都是受Google发布的Dapper论文启发而实现的。其中阿里的鹰眼解决方案没有开源而且由于阿里需要处理数据量比较大所以鹰眼的定位相对定制化不一定适合中小规模的业务团队感兴趣的同学可以点击本期文章末尾“拓展阅读”进行学习。
下面我主要来介绍下开源实现方案OpenZipkin和Pinpoint再看看它们有什么区别。
## OpenZipkin
OpenZipkin是Twitter开源的服务追踪系统下面这张图展示了它的架构设计。
<img src="https://static001.geekbang.org/resource/image/69/33/699916c60cd31a2b8d7ab0335038cf33.png" alt="" /><br />
(图片来源:[https://zipkin.io/public/img/architecture-1.png](https://zipkin.io/public/img/architecture-1.png)
从图中看OpenZipkin主要由四个核心部分组成。
<li>
Collector负责收集探针Reporter埋点采集的数据经过验证处理并建立索引。
</li>
<li>
Storage存储服务调用的链路数据默认使用的是Cassandra是因为Twitter内部大量使用了Cassandra你也可以替换成Elasticsearch或者MySQL。
</li>
<li>
API将格式化和建立索引的链路数据以API的方式对外提供服务比如被UI调用。
</li>
<li>
UI以图形化的方式展示服务调用的链路数据。
</li>
它的工作原理可以用下面这张图来描述。
<img src="https://static001.geekbang.org/resource/image/4c/d9/4c036659e0d14176215686f1a1129ed9.png" alt="" /><br />
(图片来源:[https://zipkin.io/pages/architecture.html](https://zipkin.io/pages/architecture.html)
具体流程是通过在业务的HTTP Client前后引入服务追踪代码这样在HTTP方法“/foo”调用前生成trace信息TraceIdaa、SpanId6b、annotationGET /foo以及当前时刻的timestamp1483945573944000然后调用结果返回后记录下耗时duration之后再把这些trace信息和duration异步上传给Zipkin Collector。
## Pinpoint
Pinpoint是Naver开源的一款深度支持Java语言的服务追踪系统下面这张图是它的架构设计。
<img src="https://static001.geekbang.org/resource/image/d8/a4/d8b526a56b633c34364924a2d00905a4.png" alt="" /><br />
(图片来源:[http://naver.github.io/pinpoint/1.7.3/images/pinpoint-architecture.png](http://naver.github.io/pinpoint/1.7.3/images/pinpoint-architecture.png)
Pinpoint主要也由四个部分组成。
<li>
Pinpoint Agent通过Java字节码注入的方式来收集JVM中的调用数据通过UDP协议传递给Collector数据采用Thrift协议进行编码。
</li>
<li>
Pinpoint Collector收集Agent传过来的数据然后写到HBase Storgage。
</li>
<li>
HBase Storage采用HBase集群存储服务调用的链路信息。
</li>
<li>
Pinpoint Web UI通过Web UI展示服务调用的详细链路信息。
</li>
它的工作原理你可以看这张图。
<img src="https://static001.geekbang.org/resource/image/87/95/8730864e70d666267e40e1cc4d622195.png" alt="" /><br />
(图片来源:[http://naver.github.io/pinpoint/1.7.3/images/td_figure6.png](http://naver.github.io/pinpoint/1.7.3/images/td_figure6.png)
具体来看就是请求进入TomcatA然后生成TraceIdTomcatA^ TIME ^ 1、SpanId10、pSpanId-1代表是根请求接着TomatA调用TomcatB的hello方法TomcatB生成TraceIdTomcatA^ TIME ^1、新的SpanId20、pSpanId10代表是TomcatA的请求返回调用结果后将trace信息发给CollectorTomcatA收到调用结果后将trace信息也发给Collector。Collector把trace信息写入到HBase中Rowkey就是traceIdSpanId和pSpanId都是列。然后就可以通过UI查询调用链路信息了。
## 选型对比
根据我的经验,考察服务追踪系统主要从下面这几个方面。
**1. 埋点探针支持平台的广泛性**
OpenZipkin和Pinpoint都支持哪些语言平台呢
OpenZipkin提供了不同语言的Library不同语言实现时需要引入不同版本的Library。
官方提供了C#、Go、Java、JavaScript、Ruby、Scala、PHP等主流语言版本的Library而且开源社区还提供了更丰富的不同语言版本的Library详细的可以点击[这里](https://zipkin.io/pages/existing_instrumentations)查看而Pinpoint目前只支持Java语言。
所以从探针支持的语言平台广泛性上来看OpenZipkin比Pinpoint的使用范围要广而且开源社区很活跃生命力更强。
**2. 系统集成难易程度**
再来看下系统集成的难易程度。
以OpenZipkin的Java探针Brave为例它只提供了基本的操作API如果系统要想集成Brave必须在配置里手动里添加相应的配置文件并且增加trace业务代码。具体来讲就是你需要先修改工程的POM依赖以引入Brave相关的JAR包。
```
&lt;dependencyManagement&gt;
&lt;dependencies&gt;
&lt;dependency&gt;
&lt;groupId&gt;io.zipkin.brave&lt;/groupId&gt;
&lt;artifactId&gt;brave-bom&lt;/artifactId&gt;
&lt;version&gt;${brave.version}&lt;/version&gt;
&lt;type&gt;pom&lt;/type&gt;
&lt;scope&gt;import&lt;/scope&gt;
&lt;/dependency&gt;
&lt;/dependencies&gt;
&lt;/dependencyManagement&gt;
```
然后假如你想收集每一次HTTP调用的信息你就可以使用Brave在Apache Httpclient基础上封装的httpClient它会记录每一次HTTP调用的信息并上报给OpenZipkin。
```
httpclient =TracingHttpClientBuilder.create(tracing).build();
```
而Pinpoint是通过字节码注入的方式来实现拦截服务调用从而收集trace信息的所以不需要代码做任何改动。Java字节码注入的大致原理你可以参考下图。
<img src="https://static001.geekbang.org/resource/image/4a/75/4a27448c52515020c1f687e8e3567875.png" alt="" /><br />
(图片来源:[http://naver.github.io/pinpoint/1.7.3/images/td_figure3.png](http://naver.github.io/pinpoint/1.7.3/images/td_figure3.png)
我来解释一下就是JVM在加载class二进制文件时动态地修改加载的class文件在方法的前后执行拦截器的before()和after()方法在before()和after()方法里记录trace()信息。而应用不需要修改业务代码只需要在JVM启动时添加类似下面的启动参数就可以了。
```
-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar
-Dpinpoint.agentId=&lt;Agent's UniqueId&gt;
-Dpinpoint.applicationName=&lt;The name indicating a same service (AgentId collection)
```
所以从系统集成难易程度上看Pinpoint要比OpenZipkin简单。
**3. 调用链路数据的精确度**
从下面这张OpenZipkin的调用链路图可以看出OpenZipkin收集到的数据只到接口级别进一步的信息就没有了。
<img src="https://static001.geekbang.org/resource/image/33/23/33c924c5563be070416d8133e255af23.jpg" alt="" /><br />
(图片来源:[http://ovcjgn2x0.bkt.clouddn.com/zipkin-info.jpg](http://ovcjgn2x0.bkt.clouddn.com/zipkin-info.jpg)
再来看下Pinpoint因为Pinpoint采用了字节码注入的方式实现trace信息收集所以它能拿到的信息比OpenZipkin多得多。从下面这张图可以看出它不仅能够查看接口级别的链路调用信息还能深入到调用所关联的数据库信息。
<img src="https://static001.geekbang.org/resource/image/5f/3e/5f365d3c49cdb113bf6b08f5e3b36e3e.jpg" alt="" /><br />
(图片来源:[http://ovcjgn2x0.bkt.clouddn.com/pp-info.jpg](http://ovcjgn2x0.bkt.clouddn.com/pp-info.jpg)
同理在绘制链路拓扑图时OpenZipkin只能绘制服务与服务之间的调用链路拓扑图比如下面这张示意图。
<img src="https://static001.geekbang.org/resource/image/a7/e9/a7575c0826b77d236ddffe92d4d3c1e9.jpg" alt="" /><br />
(图片来源:[http://ovcjgn2x0.bkt.clouddn.com/zipdependency1.jpg](http://ovcjgn2x0.bkt.clouddn.com/zipdependency1.jpg)
而Pinpoint不仅能够绘制服务与服务之间还能绘制与DB之间的调用链路拓扑图比如下图。
<img src="https://static001.geekbang.org/resource/image/e5/1e/e59d46aa62e542246732ab9a985d281e.jpg" alt="" /><br />
(图片来源:[http://ovcjgn2x0.bkt.clouddn.com/ppreal.jpg](http://ovcjgn2x0.bkt.clouddn.com/ppreal.jpg)
所以从调用链路数据的精确度上看Pinpoint要比OpenZipkin精确得多。
## 总结
今天我给你讲解了两个开源服务追踪系统OpenZipkin和Pinpoint的具体实现并从埋点探针支持平台广泛性、系统集成难易程度、调用链路数据精确度三个方面对它们进行了对比。
从选型的角度来讲如果你的业务采用的是Java语言那么采用Pinpoint是个不错的选择因为它不需要业务改动一行代码就可以实现trace信息的收集。除此之外Pinpoint不仅能看到服务与服务之间的链路调用还能看到服务内部与资源层的链路调用功能更为强大如果你有这方面的需求Pinpoint正好能满足。
如果你的业务不是Java语言实现或者采用了多种语言那毫无疑问应该选择OpenZipkin并且由于其开源社区很活跃基本上各种语言平台都能找到对应的解决方案。不过想要使用OpenZipkin还需要做一些额外的代码开发工作以引入OpenZipkin提供的Library到你的系统中。
除了OpenZipkin和Pinpoint业界还有其他开源追踪系统实现比如Uber开源的Jaeger以及国内的一款开源服务追踪系统SkyWalking。不过由于目前应用范围不是很广这里就不详细介绍了感兴趣的同学可以点击“拓展阅读”自行学习。
## 思考题
OpenZipkin在探针采集完数据后有两种方式把数据传递给Collector一种是通过HTTP调用一种是基于MQ的异步通信方式比如使用RabbitMQ或者Kafka你觉得哪种方式更好一些为什么
欢迎你在留言区写下自己的思考,与我一起讨论。
**拓展阅读:**
阿里巴巴鹰眼:[http://ppt.geekbang.org/slide/download/939/595f4cdcb9d52.pdf/18](http://ppt.geekbang.org/slide/download/939/595f4cdcb9d52.pdf/18)
Jaeger[https://www.jaegertracing.io](https://www.jaegertracing.io)
SkyWalking[https://github.com/apache/incubator-skywalking](https://github.com/apache/incubator-skywalking)

View File

@@ -0,0 +1,69 @@
<audio id="audio" title="17 | 如何识别服务节点是否存活?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/1e/796f39abb2e5367d3d976d412aa2dc1e.mp3"></audio>
今天我要与你分享如何识别服务节点是否存活,这在服务治理中是十分重要的。在进入正题之前,你可以先复习一下[专栏第5期](http://time.geekbang.org/column/article/14603)我在讲解注册中心原理的时候以开源注册中心ZooKeeper为例描述了它是如何管理注册到注册中心的节点的存活的。
其实ZooKeeper判断注册中心节点存活的机制其实就是注册中心摘除机制服务消费者以注册中心中的数据为准当服务端节点有变更时注册中心就会把变更通知给服务消费者服务消费者就会调用注册中心来拉取最新的节点信息。
这种机制在大部分情况下都可以工作得很好,但是在网络频繁抖动时,服务提供者向注册中心汇报心跳信息可能会失败,如果在规定的时间内,注册中心都没有收到服务提供者的心跳信息,就会把这个节点从可用节点列表中移除。更糟糕的是,在服务池拥有上百个节点的的时候,每个节点都可能会被移除,导致注册中心可用节点的状态一直在变化,这个时候应该如何处理呢?
下面就结合我在实践中的经验,给你讲解几种解决方案。
## 心跳开关保护机制
在网络频繁抖动的情况下,注册中心中可用的节点会不断变化,这时候服务消费者会频繁收到服务提供者节点变更的信息,于是就不断地请求注册中心来拉取最新的可用服务节点信息。当有成百上千个服务消费者,同时请求注册中心获取最新的服务提供者的节点信息时,可能会把注册中心的带宽给占满,尤其是注册中心是百兆网卡的情况下。
所以针对这种情况,**需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息**。
我曾经就遇到过这种情况一个可行的解决方案就是给注册中心设置一个开关当开关打开时即使网络频繁抖动注册中心也不会通知所有的服务消费者有服务节点信息变更比如只给10%的服务消费者返回变更这样的话就能将注册中心的请求量减少到原来的1/10。
当然打开这个开关也是有一定代价的它会导致服务消费者感知最新的服务节点信息延迟原先可能在10s内就能感知到服务提供者节点信息的变更现在可能会延迟到几分钟所以在网络正常的情况下开关并不适合打开可以作为一个紧急措施在网络频繁抖动的时候才打开这个开关。
## 服务节点摘除保护机制
服务提供者在进程启动时会注册服务到注册中心并每隔一段时间汇报心跳给注册中心以标识自己的存活状态。如果隔了一段固定时间后服务提供者仍然没有汇报心跳给注册中心注册中心就会认为该节点已经处于“dead”状态于是从服务的可用节点信息中移除出去。
如果遇到网络问题,大批服务提供者节点汇报给注册中心的心跳信息都可能会传达失败,注册中心就会把它们都从可用节点列表中移除出去,造成剩下的可用节点难以承受所有的调用,引起“雪崩”。但是这种情况下,可能大部分服务提供者节点是可用的,仅仅因为网络原因无法汇报心跳给注册中心就被“无情”的摘除了。
**这个时候就需要根据实际业务的情况,设定一个阈值比例,即使遇到刚才说的这种情况,注册中心也不能摘除超过这个阈值比例的节点**
这个阈值比例可以根据实际业务的冗余度来确定我通常会把这个比例设定在20%就是说注册中心不能摘除超过20%的节点。因为大部分情况下,节点的变化不会这么频繁,只有在网络抖动或者业务明确要下线大批量节点的情况下才有可能发生。而业务明确要下线大批量节点的情况是可以预知的,这种情况下可以关闭阈值保护;而正常情况下,应该打开阈值保护,以防止网络抖动时,大批量可用的服务节点被摘除。
讲到这里,我们先小结一下。
心跳开关保护机制,是为了防止服务提供者节点频繁变更导致的服务消费者同时去注册中心获取最新服务节点信息;服务节点摘除保护机制,是为了防止服务提供者节点被大量摘除引起服务消费者可以调用的节点不足。
可见,无论是心跳开关保护机制还是服务节点摘除保护机制,都是因为注册中心里的节点信息是随时可能发生变化的,所以也可以把注册中心叫作动态注册中心。
那么是不是可以换个思路,**服务消费者并不严格以注册中心中的服务节点信息为准,而是更多的以服务消费者实际调用信息来判断服务提供者节点是否可用**。这就是下面我要讲的静态注册中心。
## 静态注册中心
前面讲过心跳机制能保证在服务提供者出现异常时,注册中心可以及时把不可用的服务提供者从可用节点列表中移除出去,正常情况下这是个很好的机制。
但是仔细思考一下,为什么不把这种心跳机制直接用在服务消费者端呢?
因为服务提供者是向服务消费者提供服务的,是否可用服务消费者应该比注册中心更清楚,因此可以直接在服务消费者端根据调用服务提供者是否成功来判定服务提供者是否可用。如果服务消费者调用某一个服务提供者节点连续失败超过一定次数,可以在本地内存中将这个节点标记为不可用。并且每隔一段固定时间,服务消费者都要向标记为不可用的节点发起保活探测,如果探测成功了,就将标记为不可用的节点再恢复为可用状态,重新发起调用。
这样的话,服务提供者节点就不需要向注册中心汇报心跳信息,注册中心中的服务节点信息也不会动态变化,也可以称之为静态注册中心。
从我的实践经历来看,一开始采用了动态注册中心,后来考虑到网络的复杂性,心跳机制不一定是可靠的,而后开始改为采用服务消费者端的保活机制,事实证明这种机制足以应对网络频繁抖动等复杂的场景。
当然静态注册中心中的服务节点信息并不是一直不变,当在业务上线或者运维人工增加或者删除服务节点这种预先感知的情况下,还是有必要去修改注册中心中的服务节点信息。
比如在业务上线过程中,需要把正在部署的服务节点从注册中心中移除,等到服务部署完毕,完全可用的时候,再加入到注册中心。还有就是在业务新增或者下线服务节点的时候,需要调用注册中心提供的接口,添加节点信息或者删除节点。这个时候静态注册中心有点退化到配置中心的意思,只不过这个时候配置中心里存储的不是某一项配置,而是某个服务的可用节点信息。
## 总结
今天我给你讲解了动态注册中心在实际线上业务运行时,如果遇到网络不可靠等因素,可能会带来的两个问题,一个是服务消费者同时并发访问注册中心获取最新服务信息导致注册中心带宽被打满;另一个是服务提供者节点被大量摘除导致服务消费者没有足够的节点可以调用。
这两个问题都是我在业务实践过程中遇到过的,我给出的两个解决方案:心跳开关保护机制和服务节点摘除保护机制都是在实践中应用过的,并且被证明是行之有效的。
而静态注册中心的思路,是在斟酌注册中心的本质之后,引入的另外一个解决方案,相比于动态注册中心更加简单,并且基于服务消费者本身调用来判断服务节点是否可用,更加直接也更加准确,尤其在注册中心或者网络出现问题的时候,这种方案基本不受影响。
## 思考题
在实际的微服务架构中注册中心主动心跳机制和客户端摘除机制可能会同时使用比如Spring Cloud就把这两种机制结合起来识别服务节点是否存活。如果注册中心没有收到某一个服务节点的心跳汇报而服务消费者又调用这个服务节点成功了你认为应该以哪个为准为什么
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,113 @@
<audio id="audio" title="18 | 如何使用负载均衡算法?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c9/c7/c9122bb6fb0c9e490bb3f2ee7b2a9dc7.mp3"></audio>
假设你订阅了一个别人的服务,从注册中心查询得到了这个服务的可用节点列表,而这个列表里包含了几十个节点,这个时候你该选择哪个节点发起调用呢?这就是今天我要给你讲解的关于客户端负载均衡算法的问题。
为什么要引入负载均衡算法呢?主要有两个原因:一个是要考虑调用的均匀性,也就是要让每个节点都接收到调用,发挥所有节点的作用;另一个是要考虑调用的性能,也就是哪个节点响应最快,优先调用哪个节点。
不同的负载均衡算法在这两个方面的考虑不同,下面我就来能给介绍常见的负载均衡算法及其应用场景。
## 常见的负载均衡算法
**1. 随机算法**
随机算法,顾名思义就是从可用的服务节点中,随机挑选一个节点来访问。
在实现时随机算法通常是通过生成一个随机数来实现比如服务有10个节点那么就每一次生成一个110之间的随机数假设生成的是2那么就访问编号为2的节点。
采用随机算法,在节点数量足够多,并且访问量比较大的情况下,各个节点被访问的概率是基本相同的。一个随机算法的代码实现,可以参考这个[示例](https://github.com/weibocom/motan/blob/master/motan-core/src/main/java/com/weibo/api/motan/cluster/loadbalance/RandomLoadBalance.java)。
**2. 轮询算法**
轮询算法,顾名思义就是按照固定的顺序,把可用的服务节点,挨个访问一次。
在实现时轮询算法通常是把所有可用节点放到一个数组里然后按照数组编号挨个访问。比如服务有10个节点放到数组里就是一个大小为10的数组这样的话就可以从序号为0的节点开始访问访问后序号自动加1下一次就会访问序号为1的节点以此类推。
轮询算法能够保证所有节点被访问到的概率是相同的。一个轮询算法的代码实现,可以参考这个[示例](https://github.com/weibocom/motan/blob/master/motan-core/src/main/java/com/weibo/api/motan/cluster/loadbalance/RoundRobinLoadBalance.java)。
**3. 加权轮询算法**
轮询算法能够保证所有节点被访问的概率相同,而加权轮询算法是在此基础上,给每个节点赋予一个权重,从而使每个节点被访问到的概率不同,权重大的节点被访问的概率就高,权重小的节点被访问的概率就小。
在实现时加权轮询算法是生成一个节点序列该序列里有n个节点n是所有节点的权重之和。在这个序列中每个节点出现的次数就是它的权重值。比如有三个节点a、b、c权重分别是3、2、1那么生成的序列就是{a、a、b、c、b、a}这样的话按照这个序列访问前6次请求就会分别访问节点a三次节点b两次节点c一次。从第7个请求开始又重新按照这个序列的顺序来访问节点。
在应用加权轮询算法的时候,根据我的经验,要尽可能保证生产的序列的均匀,如果生成的不均匀会造成节点访问失衡,比如刚才的例子,如果生成的序列是{a、a、a、b、b、c}就会导致前3次访问的节点都是a。一个加权轮询算法的代码实现可以参考这个[示例](https://github.com/weibocom/motan/blob/master/motan-core/src/main/java/com/weibo/api/motan/cluster/loadbalance/ConfigurableWeightLoadBalance.java)。
**4. 最少活跃连接算法**
最少活跃连接算法,顾名思义就是每一次访问都选择连接数最少的节点。因为不同节点处理请求的速度不同,使得同一个服务消费者同每一个节点的连接数都不相同。连接数大的节点,可以认为是处理请求慢,而连接数小的节点,可以认为是处理请求快。所以在挑选节点时,可以以连接数为依据,选择连接数最少的节点访问。
在实现时,需要记录跟每一个节点的连接数,这样在选择节点时,才能比较出连接数最小的节点。一个最少活跃连接算法的代码实现,可以参考这个[示例](https://github.com/weibocom/motan/blob/master/motan-core/src/main/java/com/weibo/api/motan/cluster/loadbalance/ActiveWeightLoadBalance.java)。
**5. 一致性hash算法**
一致性hash算法是通过某个hash函数把同一个来源的请求都映射到同一个节点上。一致性hash算法最大的特点就是同一个来源的请求只会映射到同一个节点上可以说是具有记忆功能。只有当这个节点不可用时请求才会被分配到相邻的可用节点上。
一个一致性hash算法的代码实现可以参考这个[示例](https://github.com/weibocom/motan/blob/master/motan-core/src/main/java/com/weibo/api/motan/cluster/loadbalance/ConsistentHashLoadBalance.java)。
## 负载均衡算法的使用场景
上面这五种负载均衡算法,具体在业务中该如何选择呢?根据我的经验,它们的各自应用场景如下:
<li>
随机算法:实现比较简单,在请求量远超可用服务节点数量的情况下,各个服务节点被访问的概率基本相同,主要应用在各个服务节点的性能差异不大的情况下。
</li>
<li>
轮询算法:跟随机算法类似,各个服务节点被访问的概率也基本相同,也主要应用在各个服务节点性能差异不大的情况下。
</li>
<li>
加权轮询算法:在轮询算法基础上的改进,可以通过给每个节点设置不同的权重来控制访问的概率,因此主要被用在服务节点性能差异比较大的情况。比如经常会出现一种情况,因为采购时间的不同,新的服务节点的性能往往要高于旧的节点,这个时候可以给新的节点设置更高的权重,让它承担更多的请求,充分发挥新节点的性能优势。
</li>
<li>
最少活跃连接算法:与加权轮询算法预先定义好每个节点的访问权重不同,采用最少活跃连接算法,客户端同服务端节点的连接数是在时刻变化的,理论上连接数越少代表此时服务端节点越空闲,选择最空闲的节点发起请求,能获取更快的响应速度。尤其在服务端节点性能差异较大,而又不好做到预先定义权重时,采用最少活跃连接算法是比较好的选择。
</li>
<li>
一致性hash算法因为它能够保证同一个客户端的请求始终访问同一个服务节点所以适合服务端节点处理不同客户端请求差异较大的场景。比如服务端缓存里保存着客户端的请求结果如果同一客户端一直访问一个服务节点那么就可以一直从缓存中获取数据。
</li>
这五种负载均衡算法是业界最常用的不光在RPC调用中被广泛采用在一些负载均衡组件比如Nginx中也有应用所以说是一种通用的负载均衡算法但是不是所有的业务场景都能很好解决呢
我曾经遇到过这种场景:
<li>
服务节点数量众多,且性能差异比较大;
</li>
<li>
服务节点列表经常发生变化,增加节点或者减少节点时有发生;
</li>
<li>
客户端和服务节点之间的网络情况比较复杂,有些在一个数据中心,有些不在一个数据中心需要跨网访问,而且网络经常延迟或者抖动。
</li>
显然无论是随机算法还是轮询算法第一个情况就不满足加权轮询算法需要预先配置服务节点的权重在节点列表经常变化的情况下不好维护所以也不适合。而最少活跃连接算法是从客户端自身维度去判断的在实际应用时并不能直接反映出服务节点的请求量大小尤其是在网络情况比较复杂的情况下并不能做到动态的把请求发送给最合适的服务节点。至于一致性hash算法显然不适合这种场景。
针对上面这种场景,有一种算法更加适合,这种算法就是自适应最优选择算法。
## 自适应最优选择算法
这种算法的主要思路是在客户端本地维护一份同每一个服务节点的性能统计快照并且每隔一段时间去更新这个快照。在发起请求时根据“二八原则”把服务节点分成两部分找出20%的那部分响应最慢的节点,然后降低权重。这样的话,客户端就能够实时的根据自身访问每个节点性能的快慢,动态调整访问最慢的那些节点的权重,来减少访问量,从而可以优化长尾请求。
由此可见自适应最优选择算法是对加权轮询算法的改良可以看作是一种动态加权轮询算法。它的实现关键之处就在于两点第一点是每隔一段时间获取客户端同每个服务节点之间调用的平均性能统计第二点是按照这个性能统计对服务节点进行排序对排在性能倒数20%的那部分节点赋予一个较低的权重,其余的节点赋予正常的权重。
在具体实现时针对第一点需要在内存中开辟一块空间记录客户端同每一个服务节点之间调用的平均性能并每隔一段固定时间去更新。这个更新的时间间隔不能太短太短的话很容易受瞬时的性能抖动影响导致统计变化太快没有参考性同时也不能太长太长的话时效性就会大打折扣效果不佳。根据我的经验1分钟的更新时间间隔是个比较合适的值。
针对第二点关键点是权重值的设定即使服务节点之间的性能差异较大也不适合把权重设置得差异太大这样会导致性能较好的节点与性能较差的节点之间调用量相差太大这样也不是一种合理的状态。在实际设定时可以设置20%性能较差的节点权重为3其余节点权重为5。
## 总结
今天我给你讲解了最常用的五种客户端负载均衡算法的原理以及适用场景,在业务实践的过程汇总,究竟采用哪种,需要根据实际情况来决定,并不是算法越复杂越好。
比如在一种简单的业务场景下有10个服务节点并且配置基本相同位于同一个数据中心此时客户端选择随机算法或者轮询算法既简单又高效并没有必要选择加权轮询算法或者最少活跃连接算法。
但在遇到前面提到的那种复杂业务场景下,服务节点数量众多,配置差异比较大,而且位于不同的数据中心,客户端与服务节点之间的网络情况也比较复杂,这个时候简单的负载均衡算法通常都难以应对,需要针对实际情况,选择更有针对性的负载均衡算法,比如自适应最优选择算法。
## 思考题
今天我给你讲的都属于软件层面的负载均衡算法它与F5这种硬件负载均衡器有什么不同呢
欢迎你在留言区写下自己的思考,与我一起讨论。
**扩展阅读:**
一致性hash算法是如何做到添加或者删除节点对整体请求的分布影响不大[https://www.codeproject.com/Articles/56138/Consistent-hashing](https://www.codeproject.com/Articles/56138/Consistent-hashing)

View File

@@ -0,0 +1,174 @@
<audio id="audio" title="19 | 如何使用服务路由?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/91/b4/9141d4a490f782e4d38ec2b367b21bb4.mp3"></audio>
专栏上一期我给你讲解了常用的客户端负载均衡算法它帮我们解决了服务消费者如何从众多可用的服务节点中选取一个最合适的节点发起调用的问题。但在业务中经常还会遇到这样的场景比如服务A部署在北京、上海、广州三个数据中心所有的服务节点按照所在的数据中心被分成了三组那么服务A的消费者在发起调用时该如何选择呢这就是今天我要给你讲解的服务路由的问题。
那么什么是服务路由呢?我的理解是**服务路由就是服务消费者在发起服务调用时,必须根据特定的规则来选择服务节点,从而满足某些特定的需求**。
那么服务路由都有哪些应用场景?具体都有哪些规则呢?
## 服务路由的应用场景
根据我的实践经验,服务路由主要有以下几种应用场景:
<li>
分组调用。一般来讲,为了保证服务的高可用性,实现异地多活的需求,一个服务往往不止部署在一个数据中心,而且出于节省成本等考虑,有些业务可能不仅在私有机房部署,还会采用公有云部署,甚至采用多家公有云部署。服务节点也会按照不同的数据中心分成不同的分组,这时对于服务消费者来说,选择哪一个分组调用,就必须有相应的路由规则。
</li>
<li>
灰度发布。在服务上线发布的过程中,一般需要先在一小部分规模的服务节点上先发布服务,然后验证功能是否正常。如果正常的话就继续扩大发布范围;如果不正常的话,就需要排查问题,解决问题后继续发布。这个过程就叫作灰度发布,也叫金丝雀部署。
</li>
<li>
流量切换。在业务线上运行过程中,经常会遇到一些不可抗力因素导致业务故障,比如某个机房的光缆被挖断,或者发生着火等事故导致整个机房的服务都不可用。这个时候就需要按照某个指令,能够把原来调用这个机房服务的流量切换到其他正常的机房。
</li>
<li>
读写分离。对于大多数互联网业务来说都是读多写少,所以在进行服务部署的时候,可以把读写分开部署,所有写接口可以部署在一起,而读接口部署在另外的节点上。
</li>
上面四种应用场景是实际业务中很常见的,服务路由可以通过各种规则来实现,那么服务路由都有哪些规则呢?
## 服务路由的规则
根据我的实践经验,服务路由主要有两种规则:一种是条件路由,一种是脚本路由。
**1. 条件路由**
条件路由是基于条件表达式的路由规则,以下面的条件路由为例,我来给你详细讲解下它的用法。
```
condition://0.0.0.0/dubbo.test.interfaces.TestService?category=routers&amp;dynamic=true&amp;priority=2&amp;enabled=true&amp;rule=&quot; + URL.encode(&quot; host = 10.20.153.10=&gt; host = 10.20.153.11&quot;)
```
这里面“condition://”代表了这是一段用条件表达式编写的路由规则,具体的规则是
```
host = 10.20.153.10 =&gt; host = 10.20.153.11
```
分隔符“=&gt;”前面是服务消费者的匹配条件后面是服务提供者的过滤条件。当服务消费者节点满足匹配条件时就对该服务消费者执行后面的过滤规则。那么上面这段表达式表达的意义就是IP为“10.20.153.10”的服务消费者都调用IP为“10.20.153.11”的服务提供者节点。
如果服务消费者的匹配条件为空,就表示对所有的服务消费者应用,就像下面的表达式一样。
```
=&gt; host = 10.20.153.11
```
如果服务提供者的过滤条件为空,就表示禁止服务消费者访问,就像下面的表达式一样。
```
host = 10.20.153.10=&gt;
```
下面我举一些Dubbo框架中的条件路由来给你讲解下条件路由的具体应用场景。
- 排除某个服务节点
```
=&gt; host != 172.22.3.91
```
一旦这条路由规则被应用到线上所有的服务消费者都不会访问IP为172.22.3.91的服务节点,这种路由规则一般应用在线上流量排除预发布机以及摘除某个故障节点的场景。
- 白名单和黑名单功能
```
host != 10.20.153.10,10.20.153.11 =&gt;
```
这条路由规则意思是除了IP为10.20.153.10和10.20.153.11的服务消费者可以发起服务调用以外,其他服务消费者都不可以,主要用于白名单访问逻辑,比如某个后台服务只允许特定的几台机器才可以访问,这样的话可以机器控制访问权限。
```
host = 10.20.153.10,10.20.153.11 =&gt;
```
同理这条路由规则意思是除了IP为10.20.153.10和10.20.153.11的服务消费者不能发起服务调用以外,其他服务消费者都可以,也就是实现了黑名单功能,比如线上经常会遇到某些调用方不管是出于有意还是无意的不合理调用,影响了服务的稳定性,这时候可以通过黑名单功能暂时予以封杀。
- 机房隔离
```
host = 172.22.3.* =&gt; host = 172.22.3.*
```
这条路由规则意思是IP网段为172.22.3.*的服务消费者才可以访问同网段的服务节点这种规则一般应用于服务部署在多个IDC理论上同一个IDC内的调用性能要比跨IDC调用性能要好应用这个规则是为了实现同IDC就近访问。
- 读写分离
```
method = find*,list*,get*,is* =&gt; host =172.22.3.94,172.22.3.95
method != find*,list*,get*,is* =&gt; host = 172.22.3.97,172.22.3.98
```
这条路由规则意思是find*、get*、is*等读方法调用IP为172.22.3.94和172.22.3.95的节点除此以外的写方法调用IP为172.22.3.97和172.22.3.98的节点。对于大部分互联网业务来说,往往读请求要远远大于写请求,而写请求的重要性往往要远远高于读请求,所以需要把读写请求进行分离,以避免读请求异常影响到写请求,这时候就可以应用这种规则。
**2. 脚本路由**
脚本路由是基于脚本语言的路由规则常用的脚本语言比如JavaScript、Groovy、JRuby等。以下面的脚本路由规则为例我来给你详细讲解它的用法。
```
&quot;script://0.0.0.0/com.foo.BarService?category=routers&amp;dynamic=false&amp;rule=&quot; + URL.encode(&quot;function route(invokers) { ... } (invokers)&quot;)
```
这里面“script://”就代表了这是一段脚本语言编写的路由规则具体规则定义在脚本语言的route方法实现里比如下面这段用JavaScript编写的route()方法表达的意思是只有IP为10.20.153.10的服务消费者可以发起服务调用。
```
function route(invokers){
var result = new java.util.ArrayList(invokers.size());
for(i =0; i &lt; invokers.size(); i ++){
if(&quot;10.20.153.10&quot;.equals(invokers.get(i).getUrl().getHost())){
result.add(invokers.get(i));
}
}
return result;
} (invokers);
```
既然服务路由是通过路由规则来实现的,那么服务消费者该如何获取路由规则呢?
## 服务路由的获取方式
根据我的实践经验,服务路由的获取方式主要有三种:
- 本地配置
顾名思义就是路由规则存储在服务消费者本地上。服务消费者发起调用时,从本地固定位置读取路由规则,然后按照路由规则选取一个服务节点发起调用。
- 配置中心管理
这种方式下,所有的服务消费者都从配置中心获取路由规则,由配置中心来统一管理。
- 动态下发
这种方式下,一般是运维人员或者开发人员,通过服务治理平台修改路由规则,服务治理平台调用配置中心接口,把修改后的路由规则持久化到配置中心。因为服务消费者订阅了路由规则的变更,于是就会从配置中心获取最新的路由规则,按照最新的路由规则来执行。
根据我的实践经验,上面三种方式实际使用时,还是有一定区别的。
一般来讲,服务路由最好是存储在配置中心中,由配置中心来统一管理。这样的话,所有的服务消费者就不需要在本地管理服务路由,因为大部分的服务消费者并不关心服务路由的问题,或者说也不需要去了解其中的细节。通过配置中心,统一给各个服务消费者下发统一的服务路由,节省了沟通和管理成本。
但也不排除某些服务消费者有特定的需求,需要定制自己的路由规则,这个时候就适合通过本地配置来定制。
而动态下发可以理解为一种高级功能,它能够动态地修改路由规则,在某些业务场景下十分有用。比如某个数据中心存在问题,需要把调用这个数据中心的服务消费者都切换到其他数据中心,这时就可以通过动态下发的方式,向配置中心下发一条路由规则,将所有调用这个数据中心的请求都迁移到别的地方。
当然,这三种方式也可以一起使用,这个时候服务消费者的判断优先级是本地配置&gt;动态下发&gt;配置中心管理。
## 总结
今天我给你讲解了服务路由的作用,简单来讲就是为了实现某些调用的特殊需求,比如分组调用、灰度发布、流量切换、读写分离等。在业务规模比较小的时候,可能所有的服务节点都部署在一起,也就不需要服务路由。但随着业务规模的扩大、服务节点增多,尤其是涉及多数据中心部署的情况,把服务节点按照数据中心进行分组,或者按照业务的核心程度进行分组,对提高服务的可用性是十分有用的。以微博业务为例,有的服务不仅进行了核心服务和非核心服务分组,还针对私有云和公有云所处的不同数据中心也进行了分组,这样的话就可以将服务之间的调用尽量都限定在同一个数据中心内部,最大限度避免跨数据中心的网络延迟、抖动等影响。
而服务路由具体是在本地配置,还是在配置中心统一管理,也是视具体业务需求而定的。如果没有定制化的需求,建议把路由规则都放到配置中心中统一存储管理。而动态下发路由规则对于服务治理十分有帮助,当数据中心出现故障的时候,可以实现动态切换流量,还可以摘除一些有故障的服务节点。
## 思考题
在实际业务场景中,经常有一类需求就是一个新功能在全量上线前,会圈一批用户优先适用,如果使用服务路由功能的话,你觉得可以怎么做?
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,85 @@
<audio id="audio" title="20 | 服务端出现故障时该如何应对?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/52/13/528f7ba5ae46462bf525f50bc4f8b213.mp3"></audio>
在专栏前面我讲过,单体应用改造成微服务的一个好处是可以减少故障影响范围,故障被局限在一个微服务系统本身,而不是整个单体应用都崩溃。那么具体到一个微服务系统,如果出现了故障,应该如何处理呢?
首先,我先来带你回顾一下微服务系统可能出现故障的种类,主要有三种故障。
<li>
集群故障。根据我的经验微服务系统一般都是集群部署的根据业务量大小而定集群规模从几台到甚至上万台都有可能。一旦某些代码出现bug可能整个集群都会发生故障不能提供对外提供服务。
</li>
<li>
单IDC故障。现在大多数互联网公司为了保证业务的高可用性往往业务部署在不止一个IDC。然而现实中时常会发生某个IDC的光缆因为道路施工被挖断导致整个IDC脱网。
</li>
<li>
单机故障。顾名思义就是集群中的个别机器出现故障,这种情况往往对全局没有太大影响,但会导致调用到故障机器上的请求都失败,影响整个系统的成功率。
</li>
在我的实践过程中,这三种故障都经常遇到,因此相应的处理手段也可谓驾轻就熟,下面就把我应对故障的实战经验分享给你,希望对你有所帮助。
## 集群故障
一般而言集群故障的产生原因不外乎有两种一种是代码bug所导致比如说某一段Java代码不断地分配大对象但没有及时回收导致JVM OOM退出另一种是突发的流量冲击超出了系统的最大承载能力比如“双11”这种购物活动电商系统会在零点一瞬间涌入大量流量超出系统的最大承载能力一下子就把整个系统给压垮了。
应付集群故障的思路,主要有两种:**限流**和**降级**。
**1. 限流**
顾名思义,限流就是限制流量,通常情况下,系统能够承载的流量根据集群规模的大小是固定的,可以称之为系统的最大容量。当真实流量超过了系统的最大容量后,就会导致系统响应变慢,服务调用出现大量超时,反映给用户的感觉就是卡顿、无响应。所以,应该根据系统的最大容量,给系统设置一个阈值,超过这个阈值的请求会被自动抛弃,这样的话可以最大限度地保证系统提供的服务正常。
除此之外,通常一个微服务系统会同时提供多个服务,每个服务在同一时刻的请求量也是不同的,很可能出现的一种情况就是,系统中某个服务的请求量突增,占用了系统中大部分资源,导致其他服务没有资源可用。因此,还要针对系统中每个服务的请求量也设置一个阈值,超过这个阈值的请求也要被自动抛弃,这样的话不至于因为一个服务影响了其他所有服务。
在实际项目中可以用两个指标来衡量服务的请求量一个是QPS即每秒请求量一个是工作线程数。不过QPS因为不同服务的响应快慢不同所以系统能够承载的QPS相差很大因此一般选择工作线程数来作为限流的指标给系统设置一个总的最大工作线程数以及单个服务的最大工作线程数这样的话无论是系统的总请求量过大导致整体工作线程数量达到最大工作线程数还是某个服务的请求量超过单个服务的最大工作线程数都会被限流以起到保护整个系统的作用。
**2. 降级**
什么是降级呢?在我看来,降级就是通过停止系统中的某些功能,来保证系统整体的可用性。降级可以说是一种被动防御的措施,为什么这么说呢?因为它一般是系统已经出现故障后所采取的一种止损措施。
那么降级一般是如何实现的呢?根据我的实践来看, 一种可行的方案是通过开关来实现。
具体来讲,就是在系统运行的内存中开辟一块区域,专门用于存储开关的状态,也就是开启还是关闭。并且需要监听某个端口,通过这个端口可以向系统下发命令,来改变内存中开关的状态。当开关开启时,业务的某一段逻辑就不再执行,而正常情况下,开关是关闭的状态。
开关一般用在两种地方,一种是新增的业务逻辑,因为新增的业务逻辑相对来说不成熟,往往具备一定的风险,所以需要加开关来控制新业务逻辑是否执行;另一种是依赖的服务或资源,因为依赖的服务或者资源不总是可靠的,所以最好是有开关能够控制是否对依赖服务或资源发起调用,来保证即使依赖出现问题,也能通过降级来避免影响。
在实际业务应用的时候,降级要按照对业务的影响程度进行分级,一般分为三级:一级降级是对业务影响最小的降级,在故障的情况下,首先执行一级降级,所以一级降级也可以设置成自动降级,不需要人为干预;二级降级是对业务有一定影响的降级,在故障的情况下,如果一级降级起不到多大作用的时候,可以人为采取措施,执行二级降级;三级降级是对业务有较大影响的降级,这种降级要么是对商业收入有重大影响,要么是对用户体验有重大影响,所以操作起来要非常谨慎,不在最后时刻一般不予采用。
## 单IDC故障
在现实情况下整个IDC脱网的事情时有发生多半是因为不可抗力比如机房着火、光缆被挖断等如果业务全部部署在这个IDC那就完全不可访问了所以国内大部分的互联网业务多采用多IDC部署。具体来说有的采用同城双活也就是在一个城市的两个IDC内部署有的采用异地多活一般是在两个城市的两个IDC内部署当然也有支付宝这种金融级别的应用采用了“三地五中心”部署这种部署成本显然高比两个IDC要高得多但可用性的保障要更高。
采用多IDC部署的最大好处就是当有一个IDC发生故障时可以把原来访问故障IDC的流量切换到正常的IDC来保证业务的正常访问。
流量切换的方式一般有两种一种是基于DNS解析的流量切换一种是基于RPC分组的流量切换。
**1. 基于DNS解析的流量切换**
基于DNS解析流量的切换一般是通过把请求访问域名解析的VIP从一个IDC切换到另外一个IDC。比如访问“[www.weibo.com](http://www.weibo.com)”正常情况下北方用户会解析到联通机房的VIP南方用户会解析到电信机房的VIP如果联通机房发生故障的话会把北方用户访问也解析到电信机房的VIP只不过此时网络延迟可能会变长。
**2. 基于RPC分组的流量切换**
对于一个服务来说如果是部署在多个IDC的话一般每个IDC就是一个分组。假如一个IDC出现故障那么原先路由到这个分组的流量就可以通过向配置中心下发命令把原先路由到这个分组的流量全部切换到别的分组这样的话就可以切换故障IDC的流量了。
## 单机故障
单机故障是发生概率最高的一种故障了,尤其对于业务量大的互联网应用来说,上万台机器的规模也是很常见的。这种情况下,发生单机故障的概率就很高了,这个时候只靠运维人肉处理显然不可行,所以就要求有某种手段来自动处理单机故障。
根据我的经验,处理单机故障一个有效的办法就是自动重启。具体来讲,你可以设置一个阈值,比如以某个接口的平均耗时为准,当监控单机上某个接口的平均耗时超过一定阈值时,就认为这台机器有问题,这个时候就需要把有问题的机器从线上集群中摘除掉,然后在重启服务后,重新加入到集群中。
不过这里要注意的是需要防止网络抖动造成的接口超时从而触发自动重启。一种方法是在收集单机接口耗时数据时多采集几个点比如每10s采集一个点采集5个点当5个点中有超过3个点的数据都超过设定的阈值范围才认为是真正的单机问题这时会触发自动重启策略。
除此之外为了防止某些特殊情况下短时间内被重启的单机过多造成整个服务池可用节点数太少最好是设置一个可重启的单机数量占整个集群的最大比例一般这个比例不要超过10%因为正常情况下不大可能有超过10%的单机都出现故障。
## 总结
今天我们探讨了微服务系统可能出现的三种故障集群故障、单IDC故障、单机故障并且针对这三种故障我给出了分别的解决方案包括降级、限流、流量切换以及自动重启。
在遇到实际的故障时往往多个手段是并用的比如在出现单IDC故障首先要快速切换流量到正常的IDC但此时可能正常IDC并不足以支撑两个IDC的流量所以这个时候首先要降级部分功能保证正常的IDC顺利支撑切换过来的流量。
而且要尽量让故障处理自动化这样可以大大减少故障影响的时间。因为一旦需要引入人为干预往往故障处理的时间都得是10分钟以上这对大部分用户敏感型业务的影响是巨大的如果能做到自动化故障处理的话可以将故障处理的时间降低到1分钟以内甚至秒级别这样的话对于用户的影响最小。
## 思考题
上面我提到为了避免单IDC故障导致服务不可用情况的发生服务需要采用多IDC部署这个时候就要求服务依赖的数据也需要存储在多个IDC内这样势必会带来数据一致性的问题你有什么解决方案吗
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,103 @@
<audio id="audio" title="21 | 服务调用失败时有哪些处理手段?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5d/04/5db0291ea9e011f79839aaafec12ad04.mp3"></audio>
通过前面的学习你应该可以理解,微服务相比于单体应用最大的不同之处在于,服务的调用从同一台机器内部的本地调用变成了不同机器之间的远程方法调用,但是这个过程也引入了两个不确定的因素。
一个是调用的执行是在服务提供者一端即使服务消费者本身是正常的服务提供者也可能由于诸如CPU、网络I/O、磁盘、内存、网卡等硬件原因导致调用失败还有可能由于本身程序执行问题比如GC暂停导致调用失败。
另一个不确定因素是调用发生在两台机器之间,所以要经过网络传输,而网络的复杂性是不可控的,网络丢包、延迟以及随时可能发生的瞬间抖动都有可能造成调用失败。
所以,单体应用改造为微服务架构后,要针对服务调用失败进行特殊处理。那具体来说有哪些处理手段呢?下面我就结合自己的实战经验,一起来聊聊服务调用失败都有哪些处理手段。
## 超时
首先你要知道的是,单体应用被改造成微服务架构后,一次用户调用可能会被拆分成多个系统之间的服务调用,任何一次服务调用如果发生问题都可能会导致最后用户调用失败。而且在微服务架构下,一个系统的问题会影响所有调用这个系统所提供服务的服务消费者,如果不加以控制,严重的话会引起整个系统雪崩。
所以在实际项目中针对服务调用都要设置一个超时时间以避免依赖的服务迟迟没有返回调用结果把服务消费者拖死。这其中超时时间的设定也是有讲究的不是越短越好因为太短可能会导致有些服务调用还没有来得及执行完就被丢弃了当然时间也不能太长太长有可能导致服务消费者被拖垮。根据我的经验找到比较合适的超时时间需要根据正常情况下服务提供者的服务水平来决定。具体来说就是按照服务提供者线上真实的服务水平取P999或者P9999的值也就是以99.9%或者99.99%的调用都在多少毫秒内返回为准。
## 重试
虽然设置超时时间可以起到及时止损的效果但是服务调用的结果毕竟是失败了而大部分情况下调用失败都是因为偶发的网络问题或者个别服务提供者节点有问题导致的如果能换个节点再次访问说不定就能成功。而且从概率论的角度来讲假如一次服务调用失败的概率为1%那么连续两次服务调用失败的概率就是0.01%失败率降低到原来的1%。
所以在实际服务调用时经常还要设置一个服务调用超时后的重试次数。假如某个服务调用的超时时间设置为100ms重试次数设置为1那么当服务调用超过100ms后服务消费者就会立即发起第二次服务调用而不会再等待第一次调用返回的结果了。
## 双发
正如我刚才讲的那样假如一次调用不成功的概率为1%那么连续两次调用都不成功的概率就是0.01%,根据这个推论,一个简单的提高服务调用成功率的办法就是每次服务消费者要发起服务调用的时候,都同时发起两次服务调用,一方面可以提高调用的成功率,另一方面两次服务调用哪个先返回就采用哪次的返回结果,平均响应时间也要比一次调用更快,这就是双发。
但是这样的话一次调用会给后端服务两倍的压力所要消耗的资源也是加倍的所以一般情况下这种“鲁莽”的双发是不可取的。我这里讲一个更为聪明的双发即“备份请求”Backup Requests它的大致思想是服务消费者发起一次服务调用后在给定的时间内如果没有返回请求结果那么服务消费者就立刻发起另一次服务调用。这里需要注意的是这个设定的时间通常要比超时时间短得多比如超时时间取的是P999那么备份请求时间取的可能是P99或者P90这是因为如果在P99或者P90的时间内调用还没有返回结果那么大概率可以认为这次请求属于慢请求了再次发起调用理论上返回要更快一些。
在实际线上服务运行时P999由于长尾请求时间较长的缘故可能要远远大于P99和P90。在我经历的一个项目中一个服务的P999是1s而P99只有200ms、P90只有50ms这样的话如果备份请求时间取的是P90那么第二次请求等待的时间只有50ms。不过这里需要注意的是备份请求要设置一个最大重试比例以避免在服务端出现问题的时大部分请求响应时间都会超过P90的值导致请求量几乎翻倍给服务提供者造成更大的压力。我的经验是这个最大重试比例可以设置成15%,一方面能尽量体现备份请求的优势,另一方面不会给服务提供者额外增加太大的压力。
## 熔断
前面讲得一些手段在服务提供者偶发异常时会十分管用,但是假如服务提供者出现故障,短时间内无法恢复时,无论是超时重试还是双发不但不能提高服务调用的成功率,反而会因为重试给服务提供者带来更大的压力,从而加剧故障。
针对这种情况,就需要服务消费者能够探测到服务提供者发生故障,并短时间内停止请求,给服务提供者故障恢复的时间,待服务提供者恢复后,再继续请求。这就好比一条电路,电流负载过高的话,保险丝就会熔断,以防止火灾的发生,所以这种手段就被叫作“熔断”。
首先我们先来简单了解一下熔断的工作原理。
简单来讲,熔断就是把客户端的每一次服务调用用断路器封装起来,通过断路器来监控每一次服务调用。如果某一段时间内,服务调用失败的次数达到一定阈值,那么断路器就会被触发,后续的服务调用就直接返回,也就不会再向服务提供者发起请求了。
再来看下面这张图,熔断之后,一旦服务提供者恢复之后,服务调用如何恢复呢?这就牵扯到熔断中断路器的几种状态。
<li>
Closed状态正常情况下断路器是处于关闭状态的偶发的调用失败也不影响。
</li>
<li>
Open状态当服务调用失败次数达到一定阈值时断路器就会处于开启状态后续的服务调用就直接返回不会向服务提供者发起请求。
</li>
<li>
Half Open状态当断路器开启后每隔一段时间会进入半打开状态这时候会向服务提供者发起探测调用以确定服务提供者是否恢复正常。如果调用成功了断路器就关闭如果没有成功断路器就继续保持开启状态并等待下一个周期重新进入半打开状态。
</li>
<img src="https://static001.geekbang.org/resource/image/ca/8a/caf39c417373e0261da5717d1442958a.png" alt="" /><br />
(图片来源:[https://martinfowler.com/bliki/images/circuitBreaker/state.png](https://martinfowler.com/bliki/images/circuitBreaker/state.png)
关于断路器的实现最经典也是使用最广泛的莫过于Netflix开源的Hystrix了下面我来给你介绍下Hystrix是如何实现断路器的。
Hystrix的断路器也包含三种状态关闭、打开、半打开。Hystrix会把每一次服务调用都用HystrixCommand封装起来它会实时记录每一次服务调用的状态包括成功、失败、超时还是被线程拒绝。当一段时间内服务调用的失败率高于设定的阈值后Hystrix的断路器就会进入进入打开状态新的服务调用就会直接返回不会向服务提供者发起调用。再等待设定的时间间隔后Hystrix的断路器又会进入半打开状态新的服务调用又可以重新发给服务提供者了如果一段时间内服务调用的失败率依然高于设定的阈值的话断路器会重新进入打开状态否则的话断路器会被重置为关闭状态。
其中决定断路器是否打开的失败率阈值可以通过下面这个参数来设定:
```
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
```
而决定断路器何时进入半打开的状态的时间间隔可以通过下面这个参数来设定:
```
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
```
断路器实现的关键就在于如何计算一段时间内服务调用的失败率那么Hystrix是如何做的呢
答案就是下图所示的滑动窗口算法,下面我来解释一下具体原理。
<img src="https://static001.geekbang.org/resource/image/ab/31/ab8715bb51480098449ffe5b44db9c31.png" alt="" /><br />
(图片来源:[https://raw.githubusercontent.com/wiki/Netflix/Hystrix/images/circuit-breaker-1280.png](https://raw.githubusercontent.com/wiki/Netflix/Hystrix/images/circuit-breaker-1280.png)
Hystrix通过滑动窗口来对数据进行统计默认情况下滑动窗口包含10个桶每个桶时间宽度为1秒每个桶内记录了这1秒内所有服务调用中成功的、失败的、超时的以及被线程拒绝的次数。当新的1秒到来时滑动窗口就会往前滑动丢弃掉最旧的1个桶把最新1个桶包含进来。
任意时刻Hystrix都会取滑动窗口内所有服务调用的失败率作为断路器开关状态的判断依据这10个桶内记录的所有失败的、超时的、被线程拒绝的调用次数之和除以总的调用次数就是滑动窗口内所有服务的调用的失败率。
## 总结
今天我给你讲解了微服务架构下服务调用失败的几种常见手段:超时、重试、双发以及熔断,实际使用时,具体选择哪种手段要根据具体业务情况来决定。
根据我的经验大部分的服务调用都需要设置超时时间以及重试次数当然对于非幂等的也就是同一个服务调用重复多次返回结果不一样的来说不可以重试比如大部分上行请求都是非幂等的。至于双发它是在重试基础上进行一定程度的优化减少了超时等待的时间对于长尾请求的场景十分有效。采用双发策略后服务调用的P999能大幅减少经过我的实践证明是提高服务调用成功率非常有效的手段。而熔断能很好地解决依赖服务故障引起的连锁反应对于线上存在大规模服务调用的情况是必不可少的尤其是对非关键路径的调用也就是说即使调用失败也对最终结果影响不大的情况下更加应该引入熔断。
## 思考题
Hystrix采用了线程池隔离的方式来实现不同的服务调用相互之间不影响你认为这种方式的优缺点有哪些
欢迎你在留言区写下自己的思考,与我一起讨论。
**拓展阅读:**
关于熔断的解释:[https://martinfowler.com/bliki/CircuitBreaker.html](https://martinfowler.com/bliki/CircuitBreaker.html)
Hystrix的使用方法[https://github.com/Netflix/Hystrix/wiki/How-To-Use](https://github.com/Netflix/Hystrix/wiki/How-To-Use)

View File

@@ -0,0 +1,147 @@
<audio id="audio" title="22 | 如何管理服务配置?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/83/26/83cf7fde61f71256086a5713fb0f8226.mp3"></audio>
在拆分为微服务架构前,曾经的单体应用只需要管理一套配置;而拆分为微服务后,每一个系统都有自己的配置,并且都各不相同,而且因为服务治理的需要,有些配置还需要能够动态改变,以达到动态降级、切流量、扩缩容等目的,这也是今天我要与你探讨的,在微服务架构下服务配置如何管理的问题。
## 本地配置
服务配置管理最简单的方案就是**把配置当作代码同等看待,随着应用程序代码一起发布**。比如下面这段代码用到了开源熔断框架Hystrix并且在代码里定义了几个配置一个是线程的超时时间是3000ms一个是熔断器触发的错误比率是60%。
```
@HystrixCommand(fallbackMethod = &quot;getDefaultProductInventoryByCode&quot;,
commandProperties = {
@HystrixProperty(name = &quot;execution.isolation.thread.timeoutInMilliseconds&quot;, value = &quot;3000&quot;),
@HystrixProperty(name = &quot;circuitBreaker.errorThresholdPercentage&quot;, value=&quot;60&quot;)
}
)
public Optional&lt;ProductInventoryResponse&gt; getProductInventoryByCode(String productCode)
{
....
}
```
还有一种方案就是**把配置都抽离到单独的配置文件当中,使配置与代码分离**,比如下面这段代码。
```
@HystrixCommand(commandKey = &quot;inventory-by-productcode&quot;, fallbackMethod = &quot;getDefaultProductInventoryByCode&quot;)
public Optional&lt;ProductInventoryResponse&gt; getProductInventoryByCode(String productCode)
{
...
}
```
相应的配置可以抽离到配置文件中,配置文件的内容如下:
```
hystrix.command.inventory-by-productcode.execution.isolation.thread.timeoutInMilliseconds=2000
hystrix.command.inventory-by-productcode.circuitBreaker.errorThresholdPercentage=60
```
无论是把配置定义在代码里,还是把配置从代码中抽离出来,都相当于把配置存在了应用程序的本地。这样做的话,如果需要修改配置,就需要重新走一遍代码或者配置的发布流程,在实际的线上业务当中,这是一个很重的操作,往往相当于一次上线发布过程,甚至更繁琐,需要更谨慎。
这时你自然会想,如果能有一个集中管理配置的地方,如果需要修改配置,只需要在这个地方修改一下,线上服务就自动从这个地方同步过去,不需要走代码或者配置的发布流程,不就简单多了吗?没错,这就是下面要讲的配置中心。
## 配置中心
配置中心的思路就是把服务的各种配置,如代码里配置的各种参数、服务降级的开关甚至依赖的资源等都在一个地方统一进行管理。服务启动时,可以自动从配置中心中拉取所需的配置,并且如果有配置变更的情况,同样可以自动从配置中心拉取最新的配置信息,服务无须重新发布。
具体来讲,配置中心一般包含下面几个功能:
<li>
配置注册功能
</li>
<li>
配置反注册功能
</li>
<li>
配置查看功能
</li>
<li>
配置变更订阅功能
</li>
接下来我来给你详细讲解下配置中心的功能是如何实现的。
**1. 配置存储结构**
如下图所示一般来讲配置中心存储配置是按照Group来存储的同一类配置放在一个Group下以K, V键值对存储。
<img src="https://static001.geekbang.org/resource/image/ac/4e/ac57322b53b16525e5c1c26b036e694e.jpg" alt="" />
**2. 配置注册**
配置中心对外提供接口/config/service?action=register来完成配置注册功能需要传递的参数包括配置对应的分组Group以及对应的Key、Value值。比如调用下面接口请求就会向配置项global.property中添加Key为reload.locations、Value为/data1/confs/system/reload.properties的配置。
```
curl &quot;http://ip:port/config/service?action=register&quot; -d &quot;group=global.property&amp;key=reload.locations&amp;value=/data1/confs/system/reload.properties&quot;
```
**3. 配置反注册**
配置中心对外提供接口config/service?action=unregister来完成配置反注册功能需要传递的参数包括配置对象的分组Group以及对应的Key。比如调用下面的接口请求就会从配置项global.property中把Key为reload.locations的配置删除。
```
curl &quot;http://ip:port/config/service?action=unregister&quot;-d &quot;group=global.property&amp;key=reload.locations&quot;
```
**4. 配置查看**
配置中心对外提供接口config/service?action=lookup来完成配置查看功能需要传递的参数包括配置对象的分组Group以及对应的Key。比如调用下面的接口请求就会返回配置项global.property中Key为reload.locations的配置值。
```
curl &quot;http://ip:port/config/service?action=lookup&amp;group=global.property&amp;key=reload.locations&quot;
```
**5. 配置变更订阅**
配置中心对外提供接口config/service?action=getSign来完成配置变更订阅接口客户端本地会保存一个配置对象的分组Group的sign值同时每隔一段时间去配置中心拉取该Group的sign值与本地保存的sign值做对比。一旦配置中心中的sign值与本地的sign值不同客户端就会从配置中心拉取最新的配置信息。比如调用下面的接口请求就会返回配置项global.property中Key为reload.locations的配置值。
```
curl &quot;http://ip:port/config/serviceaction=getSign&amp;group=global.property&quot;
```
讲到这里,你应该对配置中心的作用有所了解了,它可以便于我们管理服务的配置信息,并且如果要修改配置信息的话,只需要同配置中心交互就可以了,应用程序会通过订阅配置中心的配置,自动完成配置更新。那么实际业务中,有哪些场景应用配置中心比较合适呢?下面我就结合自己的经验,列举几个配置中心的典型应用场景,希望能给你一些启发。
<li>
资源服务化。对于大部分互联网业务来说在应用规模不大的时候所依赖的资源如Memcached缓存或者MCQ消息队列的数量也不多因此对应的资源的IP可以直接写在配置里。但是当业务规模发展到一定程度后所依赖的这些资源的数量也开始急剧膨胀。以微博的业务为例核心缓存Memcached就有上千台机器经常会遇到个别机器因为硬件故障而不可用这个时候如果采用的是本地配置的话就需要去更改本地配置把不可用的IP改成可用的IP然后发布新的配置这样的过程十分不便。但如果采用资源服务化的话把对应的缓存统统归结为一类配置然后如果有个别机器不可用的话只需要在配置中心把对应的IP换成可用的IP即可应用程序会自动同步到本机也无须发布。
</li>
<li>
业务动态降级。微服务架构下,拆分的服务越多,出现故障的概率就越大,因此需要有对应的服务治理手段,比如要具备动态降级能力,在依赖的服务出现故障的情况下,可以快速降级对这个服务的调用,从而保证不受影响。为此,服务消费者可以通过订阅依赖服务是否降级的配置,当依赖服务出现故障的时候,通过向配置中心下达指令,修改服务的配置为降级状态,这样服务消费者就可以订阅到配置的变更,从而降级对该服务的调用。
</li>
<li>
分组流量切换。前面我提到过为了保证异地多活以及本地机房调用一般服务提供者的部署会按照IDC维度进行部署每个IDC划分为一个分组这样的话如果一个IDC出现故障可以把故障IDC机房的调用切换到其他正常IDC。为此服务消费者可以通过订阅依赖服务的分组配置当依赖服务的分组配置发生变更时服务消费者就对应的把调用切换到新的分组从而实现分组流量切换。
</li>
## 开源配置中心与选型
讲到这里,你可以根据我前面对配置中心的讲解自己去实现一个配置中心,但其实对于大部分中小团队来说,目前业界已经开源的配置中心实现可以说功能已经十分完善了,并且经过很多公司实际线上业务的充分论证,能满足大多数业务的需求,所以我建议是尽量选择成熟的开源配置中心实现,那么有哪些开源的配置中心可以使用呢?下面我就简单介绍下三个典型的开源实现:
<li>
[Spring Cloud Config](https://github.com/spring-cloud/spring-cloud-config)。Spring Cloud中使用的配置中心组件只支持Java语言配置存储在git中变更配置也需要通过git操作如果配置中心有配置变更需要手动刷新。
</li>
<li>
[Disconf](https://github.com/knightliao/disconf)。百度开源的分布式配置管理平台只支持Java语言基于Zookeeper来实现配置变更实时推送给订阅的客户端并且可以通过统一的管理界面来修改配置中心的配置。
</li>
<li>
[Apollo](https://github.com/ctripcorp/apollo)。携程开源的分布式配置中心支持Java和.Net语言客户端和配置中心通过HTTP长连接实现实时推送并且有统一的管理界面来实现配置管理。
</li>
在实际选择的时候Spring Cloud Config作为配置中心的功能比较弱只能通过git命令操作而且变更配置的话还需要手动刷新如果不是采用Spring Cloud框架的话不建议选择。而Disconf和Apollo的功能都比较强大在国内许多互联网公司内部都有大量应用其中Apollo对Spring Boot的支持比较好如果应用本身采用的是Spring Boot开发的话集成Apollo会更容易一些。
## 总结
今天我给你讲解了微服务架构下如何使用配置中心对服务的配置进行管理,以及实际业务中可能用到的场景,最后给出了一些开源配置中心的解决方案。关于业务中是否需要用到配置中心,以及选择哪种配置中心,要根据实际情况而定,如果业务比较简单,配置比较少并且不经常变更的话,采用本地配置是最简单的方案,这样的话不需要额外引入配置中心组件;相反,如果业务比较复杂,配置多而且有动态修改配置的需求的话,强烈建议引入配置中心来进行管理,而且最好做到配置变更实时推送给客户端,并且可以通过统一的管理界面来管理配置,这样的话能极大地降低运维的复杂度,减少人为介入,从而提高效率。
## 思考题
在前面我讲到Zookeeper、Consul、etcd作为服务的注册中心时可以提供强一致性的服务发现功能那么它们能够作为配置中心吗为什么
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,135 @@
<audio id="audio" title="23 | 如何搭建微服务治理平台?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/73/a0/7363f3d040d420e75abd37fa69b696a0.mp3"></audio>
在学习今天的内容前,我们先来回顾下[专栏第9期](http://time.geekbang.org/column/article/18651)。我给你讲过单体应用改造为微服务架构后,服务调用从本地调用变成了远程方法调用后,面临的各种不确定因素变多了,一方面你需要能够监控各个服务的实时运行状态、服务调用的链路和拓扑图;另一方面你需要在出现故障时,能够快速定位故障的原因并可以通过诸如降级、限流、切流量、扩容等手段快速干预止损。这个时候就需要我今天要讲的微服务治理平台了。
那么微服务治理平台都具备哪些功能呢,具体该如何搭建一套微服务治理平台呢?
## 微服务治理平台的基本功能
你可能先会问,到底什么是微服务治理平台?根据我的理解,微服务治理平台就是**与服务打交道的统一入口**,无论是开发人员还是运维人员,都能通过这个平台对服务进行各种操作,比如开发人员可以通过这个平台对服务进行降级操作,运维人员可以通过这个平台对服务进行上下线操作,而不需要关心这个操作背后的具体实现。
接下来我就结合下面这张图,给你介绍一下一个微服务治理平台应该具备哪些基本功能。
<img src="https://static001.geekbang.org/resource/image/f5/5d/f5aa7ddbd2c0997839d3f292ea89975d.png" alt="" />
**1. 服务管理**
通过微服务治理平台,可以调用注册中心提供的各种管理接口来实现服务的管理。根据我的经验,服务管理一般包括以下几种操作:
<li>
服务上下线。当上线一个新服务的时候,可以通过调用注册中心的服务添加接口,新添加一个服务,同样要下线一个已有服务的时候,也可以通过调用注册中心的服务注销接口,删除一个服务。
</li>
<li>
节点添加/删除。当需要给服务新添加节点时候,可以通过调用注册中心的节点注册接口,来给服务新增加一个节点。而当有故障节点出现或者想临时下线一些节点时,可以通过调用注册中心的节点反注册接口,来删除节点。
</li>
<li>
服务查询。这个操作会调用注册中心的服务查询接口,可以查询当前注册中心里共注册了多少个服务,每个服务的详细信息。
</li>
<li>
服务节点查询。这个操作会调用注册中心的节点查询接口,来查询某个服务下一共有多少个节点。
</li>
**2. 服务治理**
通过微服务治理平台,可以调用配置中心提供的接口,动态地修改各种配置来实现服务的治理。根据我的经验,常用的服务治理手段包括以下几种:
<li>
限流。一般是在系统出现故障的时候,比如像微博因为热点突发事件的发生,可能会在短时间内流量翻几倍,超出系统的最大容量。这个时候就需要调用配置中心的接口,去修改非核心服务的限流阈值,从而减少非核心服务的调用,给核心服务留出充足的冗余度。
</li>
<li>
降级。跟限流一样,降级也是系统出现故障时的应对方案。要么是因为突发流量的到来,导致系统的容量不足,这时可以通过降级一些非核心业务,来增加系统的冗余度;要么是因为某些依赖服务的问题,导致系统被拖慢,这时可以降级对依赖服务的调用,避免被拖死。
</li>
<li>
切流量。通常为了服务的异地容灾考虑服务部署在不止一个IDC内。当某个IDC因为电缆被挖断、机房断电等不可抗力时需要把故障IDC的流量切换到其他正常IDC这时候可以调用配置中心的接口向所有订阅了故障IDC服务的消费者下发指令将流量统统切换到其他正常IDC从而避免服务消费者受影响。
</li>
**3. 服务监控**
微服务治理平台一般包括两个层面的监控。一个是整体监控比如服务依赖拓扑图将整个系统内服务间的调用关系和依赖关系进行可视化的展示一个是具体服务监控比如服务的QPS、AvgTime、P999等监控指标。其中整体监控可以使用服务追踪系统提供的服务依赖拓扑图而具体服务监控则可以通过Grafana等监控系统UI来展示。
**4. 问题定位**
微服务治理平台实现问题定位,可以从两个方面来进行。一个是宏观层面,即通过服务监控来发觉异常,比如某个服务的平均耗时异常导致调用失败;一个是微观层面,即通过服务追踪来具体定位一次用户请求失败具体是因为服务调用全链路的哪一层导致的。
**5. 日志查询**
微服务治理平台可以通过接入类似ELK的日志系统能够实时地查询某个用户的请求的详细信息或者某一类用户请求的数据统计。
**6. 服务运维**
微服务治理平台可以调用容器管理平台,来实现常见的运维操作。根据我的经验,服务运维主要包括下面几种操作:
<li>
发布部署。当服务有功能变更,需要重新发布部署的时候,可以调用容器管理平台分批按比例进行重新部署,然后发布到线上。
</li>
<li>
扩缩容。在流量增加或者减少的时候,需要相应地增加或者缩减服务在线上部署的实例,这时候可以调用容器管理平台来扩容或者缩容。
</li>
## 如何搭建微服务治理平台
微服务治理平台之所以能够实现上面所说的功能关键之处就在于它能够封装对微服务架构内的各个基础设施组件的调用从而对外提供统一的服务操作API而且还提供了可视化的界面以方便开发人员和运维人员操作。
根据我的经验一个微服务治理平台的组成主要包括三部分Web Portal层、API层以及数据存储DB层结合下面这张图我来详细讲解下每一层该如何实现。
<img src="https://static001.geekbang.org/resource/image/60/e6/6092c301cfe5dc69abd3825e8ceedbe6.png" alt="" />
**第一层Web Portal**。也就是微服务治理平台的前端展示层,一般包含以下几个功能界面:
- 服务管理界面,可以进行节点的操作,比如查询节点、删除节点。
<img src="https://static001.geekbang.org/resource/image/8a/49/8a21ed33fa1dc550dbc08c5cad993949.png" alt="" />
- 服务治理界面,可以进行服务治理操作,比如切流量、降级等,还可以查看操作记录。
<img src="https://static001.geekbang.org/resource/image/0b/84/0b2688db42e3c9b29f19d46eed0aae84.png" alt="" />
- 服务监控界面可以查看服务的详细信息比如QPS、AvgTime、耗时分布区间以及P999等。
<img src="https://static001.geekbang.org/resource/image/07/7b/078d10be60b8a6a804a709c7e896167b.png" alt="" />
- 服务运维界面,可以执行服务的扩缩容操作,还可以查看扩缩容的操作历史。
<img src="https://static001.geekbang.org/resource/image/be/52/be0ab5c9ba97efa0938a1eab10492a52.png" alt="" />
**第二层API**。也就是微服务治理平台的后端服务层这一层对应的需要提供Web Portal接口以调用对应的一般包含下面几个接口功能
<li>
添加服务接口。这个接口会调用注册中心提供的服务添加接口来新发布一个服务。
</li>
<li>
删除服务接口。这个接口会调用注册中心提供的服务注销接口来下线一个服务。
</li>
<li>
服务降级/限流/切流量接口。这几个接口会调用配置中心提供的配置修改接口,来修改对应服务的配置,然后订阅这个服务的消费者就会从配置中心拉取最新的配置,从而实现降级、限流以及流量切换。
</li>
<li>
服务扩缩容接口。这个接口会调用容器平台提供的扩缩容接口,来实现服务的实例添加和删除。
</li>
<li>
服务部署接口。这个接口会调用容器平台提供的上线部署接口,来实现服务的线上部署。
</li>
**第三层DB**。也就是微服务治理平台的数据存储层,因为微服务治理平台不仅需要调用其他组件提供的接口,还需要存储一些基本信息,主要分为以下几种:
<li>
用户权限。因为微服务治理平台的功能十分强大,所以要对用户的权限进行管理。一般可以分为可浏览、可更改以及管理员三个权限。而且还需要对可更改的权限进行细分,按照不同服务的负责人进行权限划分,一个人只能对它负责的服务的进行更改操作,而不能修改其他人负责的服务。
</li>
<li>
操作记录。用来记录下用户在平台上所进行的变更操作,比如降级记录、扩缩容记录、切流量记录等。
</li>
<li>
元数据。主要是用来把服务在各个系统中对应的记录映射到微服务治理平台中,统一进行管理。比如某个服务在监控系统里可能有个特殊标识,在注册中心里又使用了另外一个标识,为了统一就需要在微服务治理平台统一进行转换,然后进行数据串联。
</li>
## 总结
可以说一个微服务框架是否成熟,除了要看它是否具备服务治理能力,还要看是否有强大的微服务治理平台。因为微服务治理平台能够将多个系统整合在一起,无论是对开发还是运维来说,都能起到事半功倍的作用,这也是当前大部分开源微服务框架所欠缺的部分,所以对于大部分团队来说,都需要自己搭建微服务治理平台。不过好在微服务治理平台本身的架构并不复杂,你可以根据自己的实际需要,来决定微服务治理平台具备哪些功能。
## 思考题
除了我上面列举的一些功能以外,你觉得微服务治理平台还可以包含哪些功能?说说你的理由。
欢迎你在留言区写下自己的思考,与我一起讨论。

View File

@@ -0,0 +1,63 @@
<audio id="audio" title="24 | 微服务架构该如何落地?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/08/4e039bff5887ec8ca8d6b0d06ae93f08.mp3"></audio>
专栏前面的文章我给你讲解了微服务架构的各个组成部分,以及实践过程中可能遇到的问题和对应的解决方案,到这里你应该对微服务架构有了一个完整的认识。那么在实际项目中,如何让一个团队把我们所学的微服务架构落地呢?
今天我就结合自己的经验,定位在中小规模团队,谈谈微服务架构到底该如何落地。
## 组建合适的技术团队
经过我前面的讲解你应该认识到微服务架构相比于单体应用来说复杂度提升了很多这其中涉及很多组件比如注册中心、配置中心、RPC框架、监控系统、追踪系统、服务治理等每个组件都需要专门的人甚至专家把控才能hold住不然微服务架构的落地就相当于空中楼阁虚无缥缈。
所以想要落地微服务首先需要合适的人也就是组建一支合适的技术团队。你一定很容易想到是不是只有架构师适合做微服务架构的开发一定程度上这是合理的因为微服务架构所涉及的具体技术比如CAP理论、底层网络可靠性保证、Netty高并发框架等都对技术的深度要求比较高一般有经验的架构师才能掌握所以这个技术团队必须包含技术能力很强的架构师。但是还要考虑到微服务架构最后还是要落地到业务当中既要满足业务的需求也要防止一种情况的发生那就是全部由架构人员组成技术团队根据自己的设想脱离了实际的业务场景最后开发出来的架构中看不中用业务无法实际落地既打击了团队人员积极性又对业务没有实际价值劳民伤财。所以这支技术团队也**必须包含做业务懂业务的开发人员**,只有他们了解业务的实际痛点以及落地过程中的难点,这样才能保证最后设计出的微服务架构是贴合业务实际的,并且最后是能够实际落地的。
## 从一个案例入手
当你的团队决定要对业务进行微服务架构改造时,要避免一上来就妄想将整个业务进行服务化拆分、追求完美。这种想法是很危险的,一切的技术改造都应当以给业务创造价值为宗旨,所以业务的稳定性要放在第一位,切忌好高骛远。
正确的方法是**首先从众多业务中找到一个小的业务进行试点**,前期的技术方案以满足这个小的业务需求为准,力求先把这个小业务的微服务架构落地实施,从中发现各种问题并予以解决,然后才可以继续考虑更大规模的推广。这样的话,即使微服务架构的改造因为技术方案不成熟,对业务造成了影响,也只是局限在一个小的业务之中,不会对整体业务造成太大影响。否则的话,如果因为微服务架构的改造给业务带来灾难性的后果,在许多技术团队的决策者来看,可能微服务架构的所带来的种种好处也不足以抵消其带来的风险,最后整个微服务架构的改造可能就夭折了。
回想一下微博业务的微服务改造从2013年开始进行微服务架构的研发到2014年用户关系服务开始进行微服务改造再到2015年Feed业务开始进行微服务改造从几个服务上线后经过春晚流量的考验后逐步推广到上百个服务的上线整个过程持续了两年多时间。虽然周期比较长但是对于大流量的业务系统来说稳定性永远是在第一位的业务架构改造追求的是稳步推进中间可以有小的波折但对整体架构的演进方向不会产生影响。
## 做好技术取舍
**我在搭建微服务架构的时候,其实做的最多的工作就是技术取舍**。比如在开发RPC框架的时候是选择自研呢还是采用开源RPC框架呢如果自研的话目前团队系统的主要语言是Java那么RPC框架是只支持Java语言就可以了还是考虑到将来有可能需要支持其他语言呢
我的经验就是一切以业务的实际情况为准,只要满足当前的需求就好,切忌好高骛远,尤其是对于技术能力很强的开发者来说,很容易陷入对技术的完美追求,投入过多精力在架构的雕花工作上,而忽视了眼下业务最实际的需求。尤其是在团队技术人力紧张,开发周期短的时候,更需要集中力量去满足业务最迫切的需求。而对于架构的完善以及一些附加功能的追求,可以在后面业务落地后逐步进行完善。
以微博的服务化框架Motan为例因为微博平台的开发语言主要是Java所以最早Motan只支持Java语言。从2017年开始有了跨语言服务化调用的需求才在此基础上对架构进行了升级加入了对Go、PHP等语言的支持。而且在早期业务开始使用时只开发了最基本的几个核心组件如RPC框架、注册中心和配置中心以及简单的监控系统而服务追踪系统、服务治理平台这些高级的功能都没有后来随着重要业务进行微服务改造的越来越多不断补充技术人力才开始完善服务追踪系统以及服务治理平台。
除此之外,在做技术选型的时候,还要考虑到团队的实际掌控能力,尤其是对一些新技术方案的引入要尤其慎重。如果没有合适的人能够掌控这些技术,那么贸然引入新技术,一旦业务受影响时,如果没有人能有效干预,这对业务来说是灾难性的后果。
微博在做注册中心选型的时候没有选取当时很火的Zookeeper的一个重要原因就是它底层依赖的是HBase存储当时团队中还没有有经验的运维和开发人员但团队对Redis十分了解所以基于Redis存储自研了一套注册中心完全能够满足需求并且又没有引入技术不可控因素。
## 采用DevOps
微服务架构带来的不光是业务开发模式的改变,对测试和运维的影响也是根本性的。以往在单体应用架构时,开发只需要整体打包成一个服务,交给测试去做自动化测试、交给运维去部署发布就可以了。但是微服务架构下,一个单体应用被拆分成多个细的微服务,并且需要独自开发、测试和上线,如果继续按照之前的单体应用模式运维,那么测试和运维的工作量相当于成倍的增加。因此迫切需要对以往的开发、测试和运维模式进行升级,从我的经验来看,**最好的方案就是采用DevOps**,对微服务架构进行一站式开发、测试、上线和运维。
在单体应用架构下开发、测试和运维这三者角色的区分是十分比较明显的分属于不同的部门。而在微服务架构下由于服务被拆分得足够细每个服务都需要完成独立的开发、测试和运维工作有自己完整的生命周期所以需要将一个服务从代码开发、单元测试、集成测试以及服务发布都自动化起来。这样的话测试人员就可以从众多微服务的测试中解放出来着重进行自动化测试用例的维护运维人员也可以从众多微服务的上线发布工作中解放出来着重进行DevOps体系工具的建设。而每个服务的开发负责人需要对服务的整个生命周期负责无论是在代码检查阶段出现问题还是测试阶段和发布阶段出现问题都需要去解决。
## 统一微服务治理平台
以前我们只需要关心一个大的单体应用的健康状况,所以团队可以针对大的单体应用专门监控。但进行微服务改造后,拆分出几个甚至上百个服务之后,再靠传统的运维方案去管理,就会显得力不从心了。
而且微服务架构下会衍生出许多新的问题比如RPC调用超时、注册中心获取失败、服务容量不足等有些问题需要开发介入去定位分析而有些问题需要运维介入十分混乱。
微博在进行微服务改造初期,就面临着诸多问题,比如某一个微服务的容量不足了,需要进行扩容,而它所依赖的服务也需要进行扩容,但这种依赖关系只有业务的开发人员清楚,运维人员其实并不知晓详情。还有就是某个服务依赖的另一个服务出现故障,需要紧急降级,而此时如果运维人员操作的话并不知道哪个开关,虽然开发知晓,但开发实际上又没有线上服务器的操作权限。
所以,这时就迫切需要一个微服务治理平台,能够将微服务的服务治理以及各种运维操作都统一管理起来,并且打破开发和运维之间的隔阂,给予同样的权限,让服务的开发人员真正做到对自己的服务负责,不仅要对自己的服务情况了如指掌,还需要能对自己的服务进行治理和运维。
基于此,也就**需要开发和运维深入合作,发挥各自专业的特长,将微服务治理的功能以及之前运维系统的基础功能结合在一起,打造成“一站式”微服务治理平台**。
## 总结
今天我给你讲解了微服务架构如何在业务中进行落地总结来讲就是首先你必须组建一支合适的技术团队这其中不仅要包含资深的架构师还需要包含业务的开发者。在选择业务进行微服务架构改造时不能追大求全正确的做法应当是先以一个适当规模的业务进行微服务改造走完整个微服务架构落地的过程从而找出问题不断打磨到成熟可用的状态再推广到更多更重要的业务当中。在改造的过程中要做好技术取舍以团队人员的实际情况以及业务的实际需求为准绳切忌追新立异避免给业务引入不可控因素留下“架构债”。同时微服务架构的过程也是团队组织变革的过程传统意义上的开发、测试和运维明确的分割线会被打破出现一种DevOps工程师的角色他需要对服务全生命周期负责。为了做到这一点就需要一个统一的微服务治理平台融合服务治理和运维的各种功能。
实际上,每个团队都有各自不同的情况,但只要秉承上面这些基本准则,就可以走出一条适合自己团队的微服务架构路线出来,这其中没有高低之分,适合自己的才是最好的。
## 思考题
传统单体应用下进行测试只需要启动单体应用部署一个测试环境即可进行集成测试,但经过微服务改造后,一个功能依赖了多个微服务,每个微服务都有自己的测试环境,这个时候该如何进行集成测试呢?
欢迎你在留言区写下自己的思考,与我一起讨论。