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,111 @@
<audio id="audio" title="21 | 系统架构每秒1万次请求的系统要做服务化拆分吗" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0a/a9/0a7d759bfd8b7dedbb7def477b5676a9.mp3"></audio>
你好,我是唐扬。
通过前面几个篇章的内容,你已经从数据库、缓存和消息队列的角度对自己的垂直电商系统在性能、可用性和扩展性上做了优化。
现在你的系统运行稳定好评不断每天高峰期的流量已经达到了10000/s请求DAU也涨到了几十万。CEO非常高兴打算继续完善产品功能进行新一轮的运营推广争取在下个双十一可以将DAU冲击过百万。这时你开始考虑怎么通过技术上的优化改造支撑更高的并发流量比如支撑过百万的DAU。
于是你重新审视了自己的系统架构,分析系统中有哪些可以优化的点。
<img src="https://static001.geekbang.org/resource/image/61/e7/612173bc83b332bef201e4ad7056f5e7.jpg" alt="">
目前来看工程的部署方式还是采用一体化架构。也就是说所有的功能模块比如电商系统中的订单模块、用户模块、支付模块、物流模块等等都被打包到一个大的Web工程中然后部署在应用服务器上。
你隐约觉得这样的部署方式可能存在问题于是Google了一下后发现当系统发展到一定阶段都要做微服务化的拆分你也看到淘宝的“五彩石”项目对于淘宝整体架构的扩展性带来的巨大影响。这一切让你心驰神往。
但是有一个问题一直萦绕在你的心里究竟是什么促使我们将一体化架构拆分成微服务化架构是不是说系统的整体QPS到了1万或者到了2万就一定要做微服务化拆分呢
## 一体化架构的痛点
先来回想一下你当初为什么选用了一体化架构。
在电商项目刚刚启动的时候,你只是希望能够尽量快地将项目搭建起来,方便将产品更早地投放市场快速完成验证。
在系统开发的初期,这种架构确实给你的开发运维带来了很大的便捷,主要体现在:
- 开发简单直接,代码和项目集中式管理;
- 只需要维护一个工程,节省维护系统运行的人力成本;
- 排查问题的时候,只需要排查这个应用进程就可以了,目标性强。
但随着功能越来越复杂,开发团队规模越来越大,你慢慢感受到了一体化架构的一些缺陷,这主要体现在以下几个方面。
在技术层面上,数据库连接数可能成为系统的瓶颈。
在[第7讲](https://time.geekbang.org/column/article/144796)中我提到数据库的连接是比较重的一类资源不仅连接过程比较耗时而且连接MySQL的客户端数量有限制最多可以设置为16384在实际的项目中可以依据实际业务来调整
这个数字看着很大,但是因为你的系统是按照一体化架构部署的,在部署结构上没有分层,应用服务器直接连接数据库,那么当前端请求量增加,部署的应用服务器扩容,数据库的连接数也会大增,给你举个例子。
**我之前维护的一个系统中**数据库的最大连接数设置为8000应用服务器部署在虚拟机上数量大概是50个每个服务器会和数据库建立30个连接但是数据库的连接数却远远大于30 * 50 = 1500。
因为你不仅要支撑来自客户端的外网流量还要部署单独的应用服务支撑来自其它部门的内网调用也要部署队列处理机处理来自消息队列的消息这些服务也都是与数据库直接连接的林林总总加起来在高峰期的时候数据库的连接数要接近3400。
所以一旦遇到一些大的运营推广活动服务器就要扩容,数据库连接数也随之增加,基本上就会处在最大连接数的边缘。这就像一颗定时炸弹,随时都会影响服务的稳定。
除此之外,一体化架构增加了研发的成本抑制了研发效率的提升。
>
《人月神话》中曾经提到一个团队内部沟通成本和人员数量n有关约等于n(n-1)/2也就是说随着团队人员的增加沟通的成本呈指数级增长一个100人的团队需要沟通的渠道大概是100100-1/2 = 4950。为了减少沟通成本我们一般会把团队拆分成若干个小团队每个小团队57人负责一部分功能模块的开发和维护。
比如你的垂直电商系统团队就会被拆分为用户组、订单组、支付组、商品组等等。当如此多的小团队共同维护一套代码和一个系统时,在配合的过程中就会出现问题。
不同的团队之间沟通少,假如一个团队需要一个发送短信的功能,那么有的研发同学会认为最快的方式不是询问其他团队是否有现成的而是自己写一套,但是这种想法是不合适的,会造成功能服务的重复开发。
由于代码部署在一起,每个人都向同一个代码库提交代码,代码冲突无法避免;同时功能之间耦合严重,可能你只是更改了很小的逻辑却导致其它功能不可用,从而在测试时需要对整体功能回归,延长了交付时间。
模块之间互相依赖,一个小团队中的成员犯了一个错误,就可能会影响到其它团队维护的服务,对于整体系统稳定性影响很大。
最后,一体化架构对于系统的运维也会有很大的影响。
想象一下,在项目初期你的代码可能只有几千行,构建一次只需要一分钟,那么你可以很敏捷灵活地频繁上线变更修复问题。但是当你的系统扩充到几十万行甚至上百万行代码的时候,一次构建的过程包括编译、单元测试、打包和上传到正式环境,花费的时间可能达到十几分钟,并且任何小的修改,都需要构建整个项目,上线变更的过程非常不灵活。
而我说的这些问题都可以通过微服务化拆分来解决。
## 如何使用微服务化解决这些痛点
之前我在做一个社区业务的时候,开始采用的架构也是一体化的架构,数据库已经做了垂直分库,分出了用户库、内容库和互动库,并且已经将工程拆分了业务池,拆分成了用户池、内容池和互动池。
当前端的请求量越来越大时,我们发现无论哪个业务池子用户模块都是请求量最大的模块儿,用户库也是请求量最大的数据库。这很好理解,无论是内容还是互动都会查询用户库获取用户数据,所以虽然我们做了业务池的拆分,但实际上每一个业务池子都需要连接用户库并且请求量都很大,这就造成了用户库的连接数比其它都要多一些,容易成为系统的瓶颈。
<img src="https://static001.geekbang.org/resource/image/94/11/9417a969ce19be3e70841b8d51cf8011.jpg" alt="">
**那么我们怎么解决这个问题呢?**
其实可以把与用户相关的逻辑部署成一个单独的服务,其它无论是用户池、内容池还是互动池都连接这个服务来获取和更改用户信息,也就是说只有这个服务可以连接用户库,其它的业务池都不直连用户库获取数据。
由于这个服务只处理和用户相关的逻辑,所以不需要部署太多的实例就可以承担流量,这样就可以有效地控制用户库的连接数,提升了系统的可扩展性。那么如此一来,我们也可以将内容和互动相关的逻辑都独立出来,形成内容服务和互动服务,这样我们就通过**按照业务做横向拆分**的方式解决了数据库层面的扩展性问题。
<img src="https://static001.geekbang.org/resource/image/89/f9/897bcb5e27c6492484b625fc06599ff9.jpg" alt="">
再比如我们在做社区业务的时候会有多个模块需要使用地理位置服务将IP信息或者经纬度信息转换为城市信息。比如推荐内容的时候可以结合用户的城市信息做附近内容的推荐展示内容信息的时候也需要展示城市信息等等。
那么如果每一个模块都实现这么一套逻辑就会导致代码不够重用。因此我们可以把将IP信息或者经纬度信息转换为城市信息包装成单独的服务供其它模块调用也就是**我们可以将与业务无关的公用服务抽取出来,下沉成单独的服务。**
按照以上两种拆分方式将系统拆分之后每一个服务的功能内聚维护人员职责明确增加了新的功能只需要测试自己的服务就可以了而一旦服务出了问题也可以通过服务熔断、降级的方式减少对于其他服务的影响我会在第34讲中系统地讲解
另外由于每个服务都只是原有系统的子集,代码行数相比原有系统要小很多,构建速度上也会有比较大的提升。
微服务化之后,原有单一系统被拆分成多个子服务,无论在开发还是运维上都会引入额外的问题,那么这些问题是什么?我们将如何解决呢?下一节课,我会带你来了解。
## 课程小结
本节课我主要带你了解了实际业务中会基于什么样的考虑对系统做微服务化拆分其实系统的QPS并不是决定性的因素。影响的因素我归纳为以下几点
- 系统中使用的资源出现扩展性问题,尤其是数据库的连接数出现瓶颈;
- 大团队共同维护一套代码,带来研发效率的降低和研发成本的提升;
- 系统部署成本越来越高。
**从中你应该有所感悟:**在架构演进的初期和中期,性能、可用性、可扩展性是我们追求的主要目标,高性能和高可用给用户带来更好的使用体验,扩展性可以方便我们支撑更大量级的并发。但是当系统做的越来越大,团队成员越来越多,我们就不得不考虑成本了。
这里面的“成本”有着复杂的含义,它不仅代表购买服务器的费用,还包括研发团队,内部的开发成本,沟通成本以及运维成本等等,甚至有些时候,成本会成为架构设计中的决定性因素。
比方说你做一个直播系统在架构设计时除了要关注起播速度还需要关注CDN成本再比如作为团队Leader你在日常开发中除了要推进正常的功能需求开发也要考虑完善工具链建设提高工程师的研发效率降低研发成本。
这很好理解如果在一个100个人的团队中你的工具为每个人每天节省了10分钟那么加起来就是接近17小时差不多增加了2个人工作时间。而正是基于提升扩展性和降低成本的考虑我们最终走上了微服务化的道路。
## 一课一思
在实际的项目中,你可能已经将系统拆分成独立的服务部署了,那么在一开始,你在开发和运维的过程中是遇到了哪些问题促使你走上了微服务化的道路呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,99 @@
<audio id="audio" title="22 | 微服务架构:微服务化后系统架构要如何改造?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0a/90/0aac3a0e033290051ebf5d42a9ba7d90.mp3"></audio>
你好,我是唐扬。
上一节课,我带你了解了单体架构向微服务化架构演进的原因,你应该了解到当系统依赖资源的扩展性出现问题,或者是一体化架构带来的研发成本、部署成本变得难以接受时,我们会考虑对整体系统做微服务化拆分。
微服务化之后垂直电商系统的架构将会变成下面这样:
<img src="https://static001.geekbang.org/resource/image/1d/e9/1d5f1212017c6c22818e413ab74f88e9.jpg" alt="">
在这个架构中我们将用户、订单和商品相关的逻辑抽取成服务独立的部署原本的Web工程和队列处理程序将不再直接依赖缓存和数据库而是通过调用服务接口查询存储中的信息。
有了构思和期望之后,为了将服务化拆分尽快落地,你们决定抽调主力研发同学共同制定拆分计划。但是仔细讨论后你们发现,虽然对服务拆分有了大致的方向可还是有很多疑问,比如:
- 服务拆分时要遵循哪些原则?
- 服务的边界如何确定?服务的粒度是怎样的?
- 在服务化之后会遇到哪些问题呢?我们又将如何来解决?
当然你也许想知道微服务拆分的具体操作过程和步骤是怎样的但是这部分内容涉及的知识点比较多不太可能在一次课程中把全部内容涵盖到。而且《DDD实战课》中已经侧重讲解了微服务化拆分的具体过程你可以借鉴。
而上面这三点内容会影响服务化拆分的效果,但在实际的项目中经常被大部分人忽略,所以是我们本节课的重点内容。我希望你能把本节课的内容和自身的业务结合起来体会,思考业务服务化拆分的方式和方法。
## 微服务拆分的原则
之前你维护的一体化架构就像是一个大的蜘蛛网不同功能模块错综复杂地交织在一起方法之间调用关系非常的复杂导致你修复了一个Bug可能会引起另外多个Bug整体的维护成本非常高。同时数据库较弱的扩展性也限制了服务的扩展能力
**出于上述考虑,**你要对架构做拆分。但拆分并不像听上去那么简单,这其实就是将整体工程重构甚至重写的过程。你需要将代码拆分到若干个子工程里面,再将这些子工程通过一些通信方式组装起来,这对架构是很大的调整,需要跨多个团队协作完成。
所以在开始拆分之前你需要明确几个拆分的原则,否则就会事倍功半甚至对整体项目产生不利的影响。
**原则一,做到单一服务内部功能的高内聚和低耦合。**也就是说每个服务只完成自己职责之内的任务,对于不是自己职责的功能交给其它服务来完成。说起来你可能觉得理所当然对这一点不屑一顾,但很多人在实际开发中,经常会出现一些问题。
比如我之前的项目中有用户服务和内容服务用户信息中有“是否为认证用户”字段。组内有个同学在内容服务里有这么一段逻辑如果用户认证字段等于1代表是认证用户那么就把内容权重提升。问题是判断用户是否为认证用户的逻辑应该内聚在用户服务内部而不应该由内容服务判断否则认证的逻辑一旦变更内容服务也需要一同跟着变更这就不满足高内聚、低耦合的要求了。所幸我们在Review代码时及时发现了这个问题并在服务上线之前修复了它。
**原则二,你需要关注服务拆分的粒度,先粗略拆分再逐渐细化。**在服务拆分的初期你其实很难确定服务究竟要拆分成什么样。但是从“微服务”这几个字来看服务的粒度貌似应该足够小甚至有“一方法一服务”的说法。不过服务多了也会带来问题像是服务个数的增加会增加运维的成本。再比如原本一次请求只需要调用进程内的多个方法现在则需要跨网络调用多个RPC服务在性能上肯定会有所下降。
**所以我推荐的做法是:**拆分初期可以把服务粒度拆得粗一些,后面随着团队对于业务和微服务理解的加深,再考虑把服务粒度细化。比如对于一个社区系统来说,你可以先把和用户关系相关的业务逻辑,都拆分到用户关系服务中,之后,再把比如黑名单的逻辑独立成黑名单服务。
**原则三,拆分的过程,要尽量避免影响产品的日常功能迭代。**也就是说,要一边做产品功能迭代,一边完成服务化拆分。
**还是拿我之前维护的一个项目举例。**我曾经在竞品对手快速发展的时期做了服务的拆分,拆分的方式是停掉所有业务开发全盘推翻重构,结果错失了产品发展的最佳机会,最终败给了竞争对手。因此,我们的拆分只能在现有一体化系统的基础上不断剥离业务独立部署,**剥离的顺序你可以参考以下几点:**
1.优先剥离比较独立的边界服务(比如短信服务、地理位置服务),从非核心的服务出发减少拆分对现有业务的影响,也给团队一个练习、试错的机会;
2.当两个服务存在依赖关系时优先拆分被依赖的服务。比如内容服务依赖于用户服务获取用户的基本信息,那么如果先把内容服务拆分出来,内容服务就会依赖于一体化架构中的用户模块,这样还是无法保证内容服务的快速部署能力。
所以正确的做法是理清服务之间的调用关系,比如内容服务会依赖用户服务获取用户信息,互动服务会依赖内容服务,所以要按照先用户服务再内容服务,最后互动服务的顺序来进行拆分。
**原则四,服务接口的定义要具备可扩展性。**服务拆分之后,由于服务是以独立进程的方式部署,所以服务之间通信就不再是进程内部的方法调用而是跨进程的网络通信了。在这种通信模型下服务接口的定义要具备可扩展性,否则在服务变更时会造成意想不到的错误。
**在之前的项目中,**某一个微服务的接口有三个参数,在一次业务需求开发中,组内的一个同学将这个接口的参数调整为了四个,接口被调用的地方也做了修改,结果上线这个服务后却不断报错,无奈只能回滚。
这是因为这个接口先上线后参数变更成了四个,但是调用方还未变更还是在调用三个参数的接口,那就肯定会报错了。所以服务接口的参数类型最好是封装类,这样如果增加参数就不必变更接口的签名,而只需要在类中添加字段就可以了。
## 微服务化带来的问题和解决思路
那么依据这些原则将系统做微服务拆分之后,是不是就可以一劳永逸解决所有问题了呢?当然不是。
微服务化只是一种架构手段,有效拆分后可以帮助实现服务的敏捷开发和部署。但是由于将原本一体化架构的应用拆分成了多个通过网络通信的分布式服务,为了在分布式环境下协调多个服务正常运行,就必然引入一定的复杂度,这些复杂度主要体现在以下几个方面:
1.服务接口的调用不再是同一进程内的方法调用而是跨进程的网络调用,这会增加接口响应时间的增加。此时我们就要选择高效的服务调用框架,同时接口调用方需要知道服务部署在哪些机器的哪个端口上,这些信息需要存储在一个分布式一致性的存储中,**于是就需要引入服务注册中心,**这一点是我在24讲会提到的内容。不过在这里我想强调的是注册中心管理的是服务完整的生命周期包括对于服务存活状态的检测。
2.多个服务之间有着错综复杂的依赖关系。一个服务会依赖多个其它服务也会被多个服务所依赖,那么一旦被依赖的服务的性能出现问题产生大量的慢请求,就会导致依赖服务的工作线程池中的线程被占满,依赖的服务也会出现性能问题。接下来问题就会沿着依赖网逐步向上蔓延,直到整个系统出现故障为止。
为了避免发生这种情况,我们需要引入服务治理体系针对出问题的服务采用熔断、降级、限流、超时控制的方法,使问题被限制在单一服务中,保护服务网络中的其它服务不受影响。
3.服务拆分到多个进程后,一条请求的调用链路上涉及多个服务,那么一旦这个请求的响应时间增长或者是出现错误,我们就很难知道是哪一个服务出现的问题。另外,整体系统一旦出现故障,很可能外在的表现是所有服务在同一时间都出现了问题,你在问题定位时很难确认哪一个服务是源头,**这就需要引入分布式追踪工具,以及更细致的服务端监控报表。**
我在25讲和30讲会详细地剖析这个内容**在这里我想强调的是,**监控报表关注的是依赖服务和资源的宏观性能表现;分布式追踪关注的是单一慢请求中的性能瓶颈分析,两者需要结合起来帮助你来排查问题。
以上这些微服务化后在开发方面引入的问题,就是接下来“分布式服务篇”和“维护篇”的主要讨论内容。
总的来说,微服务化是一个很大的话题,在微服务开发和维护时,你也许会在很短时间就把微服务拆分完成,但是你可能会花相当长的时间来完善服务治理体系。接下来的内容会涉及一些常用微服务中间件的原理和使用方式,你可以使用以下的方式更好地理解后面的内容:
- 快速完成中间件的部署运行,建立对它感性的认识;
- 阅读它的文档中基本原理和架构设计部分;
- 必要时阅读它的源码,加深对它的理解,这样可以帮助你在维护你的微服务时排查中间件引起的故障和解决性能问题。
## 课程小结
本节课,为了能够指导你更好地进行服务化的拆分,我带你了解了微服务化拆分的原则,内容比较清晰。在这里我想延伸一些内容:
1.“康威定律”提到设计系统的组织其产生的设计等同于组织间的沟通结构。通俗一点说,就是你的团队组织结构是什么样的你的架构就会长成什么样。
如果你的团队分为服务端开发团队、DBA团队、运维团队、测试团队那么你的架构就是一体化的所有的团队共同为一个大系统负责团队内成员众多沟通成本就会很高而如果你想实现微服务化的架构**那么你的团队也要按照业务边界拆分,**每一个模块由一个自治的小团队负责这个小团队里面有开发、测试、运维和DBA这样沟通就只发生在这个小团队内部沟通的成本就会明显降低。
2.微服务化的一个目标是减少研发的成本,其中也包括沟通的成本,所以小团队内部成员不宜过多。
按照亚马逊CEO贝佐斯的“两个披萨”的理论如果两个披萨不够你的团队吃那么你的团队就太大了需要拆分所以一个小团队包括开发、运维、测试以68个人为最佳
3.如果你的团队人数不多还没有做好微服务化的准备,而你又感觉到研发和部署的成本确实比较高,那么一个折中的方案是**你可以优先做工程的拆分。**
比如你使用的是Java语言你可以依据业务的边界将代码拆分到不同的子工程中然后子工程之间以jar包的方式依赖这样每个子工程代码量减少可以减少打包时间并且子工程代码内部可以做到高内聚低耦合一定程度上减少研发的成本也不失为一个不错的保守策略。
## 一课一思
结合你在实际微服务改造中的经验,可以和我说说你在微服务拆分后都遇到了哪些问题吗?你是如何解决的呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,185 @@
<audio id="audio" title="23 | RPC框架10万QPS下如何实现毫秒级的服务调用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/04/27/04e5304777e7ffe7a5b3860ab864f727.mp3"></audio>
你好,我是唐扬。
在[21讲](https://time.geekbang.org/column/article/164025)和[22讲](https://time.geekbang.org/column/article/164710)中,你的团队已经决定对垂直电商系统做服务化拆分,以便解决扩展性和研发成本高的问题。与此同时,你们在不断学习的过程中还发现系统做了服务化拆分之后会引入一些新的问题,这些问题我在上节课提到过,归纳起来主要是两点:
- 服务拆分单独部署后,引入的服务跨网络通信的问题;
- 在拆分成多个小服务之后,服务如何治理的问题。
如果想要解决这两方面问题,你需要了解微服务化所需要的中间件的基本原理和使用技巧,那么本节课,我会带你掌握解决第一个问题的核心组件:**RPC框架。**
**来思考这样一个场景:**你的垂直电商系统的QPS已经达到了每秒2万次在做了服务化拆分之后由于我们把业务逻辑都拆分到了单独部署的服务中那么假设你在完成一次完整的请求时需要调用45次服务计算下来RPC服务需要承载大概每秒10万次的请求。而你该如何设计RPC框架承载如此大的请求量呢我建议你
- 选择合适的网络模型,有针对性地调整网络参数优化网络传输性能;
- 选择合适的序列化方式,以提升封包、解包的性能。
接下来我从原理出发让你对于RPC有一个理性的认识这样你在设计RPC框架时就可以清晰地知道自己的设计目标是什么了。
## 你所知道的RPC
说到RPCRemote Procedure Call远程过程调用你不会陌生它指的是通过网络调用另一台计算机上部署服务的技术。
而RPC框架就封装了网络调用的细节让你像调用本地服务一样调用远程部署的服务。你也许觉得只有像Dubbo、Grpc、Thrift这些新兴的框架才算是RPC框架**其实严格来说你很早之前就接触到与RPC相关的技术了。**
比如Java原生就有一套远程调用框架叫做RMIRemote Method Invocation它可以让Java程序通过网络调用另一台机器上的Java对象的方法。它是一种远程调用的方法也是J2EE时代大名鼎鼎的EJB的实现基础。
时至今日你仍然可以通过Spring的“RmiServiceExporter”将Spring管理的bean暴露成一个RMI的服务从而继续使用RMI来实现跨进程的方法调用。之所以RMI没有像Dubbo、Grpc一样大火**是因为它存在着一些缺陷:**
<li>
RMI使用专为Java远程对象定制的协议JRMPJava Remote Messaging Protocol进行通信这限制了它的通信双方只能是Java语言的程序无法实现跨语言通信
</li>
<li>
RMI使用Java原生的对象序列化方式生成的字节数组空间较大效率很差。
</li>
**另一个你可能听过的技术是Web Service**它也可以认为是RPC的一种实现方式。它的优势是使用HTTP+SOAP协议保证了调用可以跨语言、跨平台。只要你支持HTTP协议可以解析XML那么就能够使用Web Service。在我看来由于它使用XML封装数据数据包大性能还是比较差。
**借上面几个例子我主要是想告诉你,**RPC并不是互联网时代的产物也不是服务化之后才衍生出来的技术而是一种规范只要是封装了网络调用的细节能够实现远程调用其他服务就可以算作是一种RPC技术了。
**那么你的垂直电商项目在使用RPC框架之后会产生什么变化呢**
在我看来,在性能上的变化是不可忽视的,**我给你举个例子。** 比方说,你的电商系统中商品详情页面需要商品数据、评论数据还有店铺数据,如果在一体化的架构中,你只需要从商品库、评论库和店铺库获取数据就可以了,不考虑缓存的情况下有三次网络请求。
但是如果独立出商品服务、评论服务和店铺服务之后,那么就需要分别调用这三个服务,而这三个服务又会分别调用各自的数据库,这就是六次网络请求。如果你服务拆分得更细粒度,那么多出的网络调用就会越多,请求的延迟就会更长,而这就是你为了提升系统的扩展性在性能上所付出的代价。
<img src="https://static001.geekbang.org/resource/image/1d/ce/1dba9b34e2973ec185b353becfc64fce.jpg" alt="">
那么我们要如何优化RPC的性能从而尽量减少网络调用对于性能的影响呢在这里你首先需要了解一次RPC的调用都经过了哪些步骤因为这样你才可以针对这些步骤中可能存在的性能瓶颈点提出优化方案。**步骤如下:**
<li>
在一次RPC调用过程中客户端首先会将调用的类名、方法名、参数名、参数值等信息序列化成二进制流
</li>
<li>
然后客户端将二进制流通过网络发送给服务端;
</li>
<li>
服务端接收到二进制流之后将它反序列化,得到需要调用的类名、方法名、参数名和参数值,再通过动态代理的方式调用对应的方法得到返回值;
</li>
<li>
服务端将返回值序列化,再通过网络发送给客户端;
</li>
<li>
客户端对结果反序列化之后,就可以得到调用的结果了。
</li>
**过程图如下:**
<img src="https://static001.geekbang.org/resource/image/f9/3e/f98bd80af8a4e7258251db1084e0383e.jpg" alt="">
从这张图中你可以看到网络传输的过程,将请求序列化和反序列化的过程, 所以如果要提升RPC框架的性能需要从**网络传输和序列化**两方面来优化。
## 如何提升网络传输性能
在网络传输优化中你首先要做的是选择一种高性能的I/O模型。所谓I/O模型就是我们处理I/O的方式。而一般单次I/O请求会分为两个阶段每个阶段对于I/O的处理方式是不同的。
**首先I/O会经历一个等待资源的阶段**比方说等待网络传输数据可用在这个过程中我们对I/O会有两种处理方式
- 阻塞。指的是在数据不可用时I/O请求一直阻塞直到数据返回
- 非阻塞。指的是数据不可用时I/O请求立即返回直到被通知资源可用为止。
**然后是使用资源的阶段,**比如说从网络上接收到数据,并且拷贝到应用程序的缓冲区里面。在这个阶段我们也会有两种处理方式:
- 同步处理。指的是I/O请求在读取或者写入数据时会阻塞直到读取或者写入数据完成
- 异步处理。指的是I/O请求在读取或者写入数据时立即返回当操作系统处理完成I/O请求并且将数据拷贝到用户提供的缓冲区后再通知应用I/O请求执行完成。
将这两个阶段的四种处理方式做一些排列组合再做一些补充就得到了我们常见的五种I/O模型
- 同步阻塞I/O
- 同步非阻塞I/O
- 同步多路I/O复用
- 信号驱动I/O
- 异步I/O。
你需要理解这五种I/O模型的区别和特点不过在理解上你可能会有些难度所以我来做个比喻方便你理解。
我们把I/O过程比喻成烧水倒水的过程等待资源就是烧水的过程使用资源就是倒水的过程
<li>
如果你站在灶台边上一直等着等待资源水烧开然后倒水使用资源那么就是同步阻塞I/O
</li>
<li>
如果你偷点儿懒在烧水的时候躺在沙发上看会儿电视不再时时刻刻等待资源但是还是要时不时地去看看水开了没有一旦水开了马上去倒水使用资源那么这就是同步非阻塞I/O
</li>
<li>
如果你想要洗澡需要同时烧好多壶水那你就在看电视的间隙去看看哪壶水开了等待多个资源哪一壶开了就先倒哪一壶这样就加快了烧水的速度这就是同步多路I/O复用
</li>
<li>
不过你发现自己总是跑厨房去看水开了没太累了于是你考虑给你的水壶加一个报警器信号只要水开了就马上去倒水这就是信号驱动I/O
</li>
<li>
最后一种就高级了你发明了一个智能水壶在水烧好后自动就可以把水倒好这就是异步I/O。
</li>
这五种I/O模型中最被广泛使用的是**多路I/O复用**Linux系统中的select、epoll等系统调用都是支持多路I/O复用模型的Java中的高性能网络框架Netty默认也是使用这种模型。你可以选择它。
选择好了一种高性能的I/O模型是不是就能实现数据在网络上的高效传输呢其实并没有那么简单网络性能的调优涉及很多方面**其中不可忽视的一项就是网络参数的调优,**接下来我带你了解其中一个典型例子。当然你可以结合网络基础知识以及成熟RPC框架比如Dubbo的源码来深入了解网络参数调优的方方面面。
**在之前的项目中,**我的团队曾经写过一个简单的RPC通信框架。在进行测试的时候发现远程调用一个空业务逻辑的方法时平均响应时间居然可以到几十毫秒这明显不符合我们的预期在我们看来运行一个空的方法应该在1毫秒之内可以返回。于是我先在测试的时候使用tcpdump抓了包发现一次请求的ACK包竟然要经过40ms才返回。在网上google了一下原因发现和一个叫做tcp_nodelay的参数有关。**这个参数是什么作用呢?**
TCP协议的包头有20字节IP协议的包头也有20字节如果仅仅传输1字节的数据在网络上传输的就有20 + 20 + 1 = 41字节其中真正有用的数据只有1个字节这对效率和带宽是极大的浪费。所以在1984年的时候John Nagle提出了以他的名字命名的Nagle`s算法他希望
>
<p>如果是连续的小数据包大小没有一个MSSMaximum Segment<br>
Size最大分段大小并且还没有收到之前发送的数据包的Ack信息那么这些小数据包就会在发送端暂存起来直到小数据包累积到一个MSS或者收到一个Ack为止。</p>
这原本是为了减少不必要的网络传输但是如果接收端开启了DelayedACK延迟ACK的发送这样可以合并多个ACK提升网络传输效率那就会发生发送端发送第一个数据包后接收端没有返回ACK这时发送端发送了第二个数据包因为Nagle`s算法的存在并且第一个发送包的ACK还没有返回所以第二个包会暂存起来。而DelayedACK的超时时间默认是40ms所以一旦到了40ms接收端回给发送端ACK那么发送端才会发送第二个包这样就增加了延迟。
**解决的方式非常简单:**只要在Socket上开启tcp_nodelay就好了这个参数关闭了Nagle`s算法这样发送端就不需要等到上一个发送包的ACK返回直接发送新的数据包就好了。这对于强网络交互的场景来说非常的适用基本上如果你要自己实现一套网络框架tcp_nodelay这个参数最好是要开启的。
## 选择合适的序列化方式
**在对网络数据传输完成调优之后,另外一个需要关注的点就是数据的序列化和反序列化。**通常所说的序列化是将传输对象转换成二进制串的过程,而反序列化则是相反的动作,是将二进制串转换成对象的过程。
从上面的RPC调用过程中你可以看到一次RPC调用需要经历两次数据序列化的过程和两次数据反序列化的过程可见它们对于RPC的性能影响是很大的**那么我们在选择序列化方式的时候需要考虑哪些因素呢?**
首先需要考虑的肯定是性能嘛,性能包括时间上的开销和空间上的开销,时间上的开销就是序列化和反序列化的速度,这是显而易见需要重点考虑的,而空间上的开销则是序列化后的二进制串的大小,过大的二进制串也会占据传输带宽影响传输效率。
除去性能之外我们需要考虑的是它是否可以跨语言、跨平台这一点也非常重要因为一般的公司的技术体系都不是单一的使用的语言也不是单一的那么如果你的RPC框架中传输的数据只能被一种语言解析这无疑限制了框架的使用。
另外,扩展性也是一个需要考虑的重点问题。你想想,如果对象增加了一个字段就会造成传输协议的不兼容,导致服务调用失败,这会是多么可怕的事情。
综合上面的几个考虑点,在我看来,**我们的序列化备选方案主要有以下几种:**
首先是大家熟知的JSON它起源于JavaScript是一种最广泛使用的序列化协议它的优势简单易用同时在性能上相比XML有比较大的优势。
另外的Thrift和Protobuf都是需要引入IDLInterface description language也就是需要按照约定的语法写一个IDL文件然后通过特定的编译器将它转换成各语言对应的代码从而实现跨语言的特点。
**Thrift**是Facebook开源的高性能的序列化协议也是一个轻量级的RPC框架**Protobuf**是谷歌开源的序列化协议。它们的共同特点是无论在空间上还是时间上都有着很高的性能缺点就是由于IDL存在带来一些使用上的不方便。
那么你要如何选择这几种序列化协议呢?**这里我给你几点建议:**
<li>
如果对于性能要求不高在传输数据占用带宽不大的场景下可以使用JSON作为序列化协议
</li>
<li>
如果对于性能要求比较高那么使用Thrift或者Protobuf都可以。而Thrift提供了配套的RPC框架所以想要一体化的解决方案你可以优先考虑Thrift
</li>
<li>
在一些存储的场景下比如说你的缓存中存储的数据占用空间较大那么你可以考虑使用Protobuf替换JSON作为存储数据的序列化方式。
</li>
## 课程小结
为了优化RPC框架的性能本节课我带你了解了网络I/O模型和序列化方式的选择它们是实现高并发RPC框架的要素总结起来有三个要点
<li>
选择高性能的I/O模型这里我推荐使用同步多路I/O复用模型
</li>
<li>
调试网络参数这里面有一些经验值的推荐。比如将tcp_nodelay设置为true也有一些参数需要在运行中来调试比如接受缓冲区和发送缓冲区的大小客户端连接请求缓冲队列的大小back log等等
</li>
<li>
序列化协议依据具体业务来选择。如果对性能要求不高可以选择JSON否则可以从Thrift和Protobuf中选择其一。
</li>
在学习本节课的过程中我建议你阅读一下成熟的RPC框架的源代码。比如阿里开源的Dubbo、微博的Motan等等理解它们的实现原理和细节这样你会更有信心维护好你的微服务系统同时你也可以从优秀的代码中学习到代码设计的技巧比如说Dubbo对于RPC的抽象SPI扩展点的设计这样可以有助你提升代码能力。
当然了本节课我不仅仅想让你了解RPC框架实现的一些原理更想让你了解在做网络编程时需要考虑哪些关键点这样你在设计此类型的系统时就会有一些考虑的方向和思路了。
## 一课一思
你在实际的工作中可能已经使用过一些RPC框架那么结合你的实际经验可以和我说说在RPC框架使用过程中遇到了哪些问题吗又是如何排查和解决的呢欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,129 @@
<audio id="audio" title="24 | 注册中心:分布式系统如何寻址?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ff/75/ff4cff2c475ab622e886c8688ba27275.mp3"></audio>
你好,我是唐扬。
上一节课我带你了解了RPC框架实现中的一些关键的点你通过RPC框架能够解决服务之间跨网络通信的问题这就完成了微服务化改造的基础。
但是在服务拆分之后你需要维护更多的细粒度的服务而你需要面对的第一个问题就是如何让RPC客户端知道服务端部署的地址。这就是我们今天要讲到的服务注册与发现的问题。
## 你所知道的服务发现
服务注册和发现不是一个新的概念你在之前的实际项目中也一定了解过只是你可能没怎么注意罢了。比如说你知道Nginx是一个反向代理组件那么Nginx需要知道应用服务器的地址是什么这样才能够将流量透传到应用服务器上**这就是服务发现的过程。**
**那么Nginx是怎么实现的呢**它是把应用服务器的地址配置在了文件中。
这固然是一种解决的思路实际上我在早期的项目中也是这么做的。那时项目刚刚做了服务化拆分RPC服务端的地址就是配置在了客户端的代码中不过这样做之后出现了几个问题
- 首先在紧急扩容的时候,就需要修改客户端配置后,重启所有的客户端进程,操作时间比较长;
- 其次,一旦某一个服务器出现故障时,也需要修改所有客户端配置后重启,无法快速修复,更无法做到自动恢复;
- 最后RPC服务端上线无法做到提前摘除流量这样在重启服务端的时候客户端发往被重启服务端的请求还没有返回会造成慢请求甚至请求失败。
因此,我们考虑使用**注册中心**来解决这些问题。
目前业界有很多可供你来选择的注册中心组件比如说老派的ZooKeeper、Kubernetes使用的ETCD、阿里的微服务注册中心Nacos、Spring Cloud的Eureka等等。
这些注册中心的基本功能有两点:
- 其一是提供了服务地址的存储;
- 其二是当存储内容发生变化时,可以将变更的内容推送给客户端。
第二个功能是我们使用注册中心的主要原因。因为无论是当我们需要紧急扩容还是在服务器发生故障时需要快速摘除节点都不用重启服务器就可以实现了。使用了注册中心组件之后RPC的通信过程就变成了下面这个样子
<img src="https://static001.geekbang.org/resource/image/3e/13/3ee52d302f77bf5e61b244094d754d13.jpg" alt="">
从图中你可以看到一个完整的服务注册和发现的过程:
- 客户端会与注册中心建立连接,并且告诉注册中心,它对哪一组服务感兴趣;
- 服务端向注册中心注册服务后,注册中心会将最新的服务注册信息通知给客户端;
- 客户端拿到服务端的地址之后就可以向服务端发起调用请求了。
从这个过程中可以看出,有了注册中心之后,服务节点的增加和减少对于客户端就是透明的。这样除了可以实现不重启客户端就能动态地变更服务节点以外,还可以**实现优雅关闭的功能。**
优雅关闭是你在系统研发过程中必须要考虑的问题。因为如果暴力地停止服务,那么已经发送给服务端的请求,来不及处理服务就被删掉了,就会造成这部分请求失败,服务就会有波动。所以服务在退出的时候,都需要先停掉流量再停止服务,这样服务的关闭才会更平滑。比如,消息队列处理器就是要将所有已经从消息队列中读出的消息,处理完之后才能退出。
**对于RPC服务来说**我们可以先将RPC服务从注册中心的服务列表中删除掉然后观察RPC服务端没有流量之后再将服务端停掉。有了优雅关闭之后RPC服务端再重启的时候就会减少对客户端的影响。
在这个过程中,服务的上线和下线是由服务端主动向注册中心注册和取消注册来实现的,这在正常的流程中是没有问题的。**可是,如果某一个服务端意外故障,**比如说机器掉电,网络不通等情况,服务端就没有办法向注册中心通信,将自己从服务列表中删除,那么客户端也就不会得到通知,它就会继续向一个故障的服务端发起请求,也就会有错误发生了。那这种情况如何来避免呢?其实,这种情况是一个服务状态管理的问题。
## 服务状态管理如何来做
针对上面我提到的问题,**我们一般会有两种解决思路。**
第一种思路是主动探测,**方法是这样的:**
你的RPC服务要打开一个端口然后由注册中心每隔一段时间比如30秒探测这些端口是否可用如果可用就认为服务仍然是正常的否则就可以认为服务不可用那么注册中心就可以把服务从列表里面删除了。
<img src="https://static001.geekbang.org/resource/image/be/a9/be3edc8206ef630c54e14f429746dea9.jpg" alt="">
微博早期的注册中心就是采用这种方式,但是后面出现的两个问题,让我们不得不对它做改造。
**第一个问题是:**所有的RPC服务端都需要开放一个统一的端口给注册中心探测那时候还没有容器化一台物理机上会混合部署很多的服务你需要开放的端口很可能已经被占用这样会造成RPC服务启动失败。
**还有一个问题是:**如果RPC服务端部署的实例比较多那么每次探测的成本也会比较高探测的时间也比较长这样当一个服务不可用时可能会有一段时间的延迟才会被注册中心探测到。
**因此,我们后面把它改造成了心跳模式。**
这也是大部分注册中心提供的检测连接上来的RPC服务端是否存活的方式比如Eureka、ZooKeeper**在我来看,这种心跳机制可以这样实现:**
注册中心为每一个连接上来的RPC服务节点记录最近续约的时间RPC服务节点在启动注册到注册中心后就按照一定的时间间隔比如30秒向注册中心发送心跳包。注册中心在接收到心跳包之后会更新这个节点的最近续约时间。然后注册中心会启动一个定时器定期检测当前时间和节点最近续约时间的差值如果达到一个阈值比如说90秒那么认为这个服务节点不可用。
<img src="https://static001.geekbang.org/resource/image/76/99/768494782e63e9dcddd464cb6bdd7e99.jpg" alt="">
**在实际的使用中,**心跳机制相比主动探测的机制,适用范围更广,如果你的服务也需要检测是否存活,那么也可以考虑使用心跳机制来检测。
**接着说回来,**有了心跳机制之后注册中心就可以管理注册的服务节点的状态了也让你的注册中心成为了整体服务最重要的组件因为一旦它出现问题或者代码出现Bug那么很可能会导致整个集群的故障给你举一个真实的案例。
**在我之前的一个项目中,**工程是以“混合云”的方式部署的,也就是一部分节点部署在自建机房中,一部分节点部署在云服务器上,每一个机房都部署了自研的一套注册中心,每套注册中心中都保存了全部节点的数据。
这套自研的注册中心使用Redis作为最终的存储而在自建机房和云服务器上的注册中心共用同一套Redis存储资源。由于“混合云”还处在测试阶段所以所有的流量还都在自建机房自建机房和云服务器之前的专线带宽还比较小部署结构如下
<img src="https://static001.geekbang.org/resource/image/b3/09/b31fa6bc6b383675a80917e7491be209.jpg" alt="">
在测试的过程中系统运行稳定,但是某一天早上五点,我突然发现,所有的服务节点都被摘除了,客户端因为拿不到服务端的节点地址列表全部调用失败,整体服务宕机。经过排查我发现,云服务器上部署的注册中心竟然将所有的服务节点全部删除了!进一步排查之后,**原来是自研注册中心出现了Bug。**
在正常的情况下无论是自建机房还是云服务器上的服务节点都会向各自机房的注册中心注册地址信息并且发送心跳。而这些地址信息以及服务的最近续约时间都是存储在Redis主库中各自机房的注册中心会读各自机房的从库来获取最近续约时间从而判断服务节点是否有效。
Redis的主从同步数据是通过专线来传输的出现故障之前专线带宽被占满导致主从同步延迟。这样一来云上部署的Redis从库中存储的最近续约时间就没有得到及时更新随着主从同步延迟越发严重最终云上部署的注册中心发现了当前时间与最近续约时间的差值超过了摘除的阈值所以将所有的节点摘除从而导致了故障。
有了这次惨痛的教训,**我们给注册中心增加了保护的策略:**如果摘除的节点占到了服务集群节点数的40%,就停止摘除服务节点,并且给服务的开发同学和运维同学报警处理(这个阈值百分比可以调整,保证了一定的灵活性)。
**据我所知,**Eureka也采用了类似的策略来避免服务节点被过度摘除导致服务集群不足以承担流量的问题。如果你使用的是ZooKeeper或者ETCD这种无保护策略的分布式一致性组件那你可以考虑在客户端实现保护策略的逻辑比如说当摘除的节点超过一定比例时你在RPC客户端就不再处理变更通知你可以依据自己的实际情况来实现。
除此之外在实际项目中我们还发现注册中心另一个重要的问题就是“通知风暴”。你想一想变更一个服务的一个节点会产生多少条推送消息假如你的服务有100个调用者有100个节点那么变更一个节点会推送100 * 100 = 10000个节点的数据。那么如果多个服务集群同时上线或者发生波动时注册中心推送的消息就会更多会严重占用机器的带宽资源这就是我所说的“通知风暴”。**那么怎么解决这个问题呢?**你可以从以下几个方面来思考:
- 首先,要控制一组注册中心管理的服务集群的规模,具体限制多少没有统一的标准,你需要结合你的业务以及注册中心的选型来考虑,主要考察的指标就是注册中心服务器的峰值带宽;
- 其次,你也可以通过扩容注册中心节点的方式来解决;
- 再次,你可以规范一下对于注册中心的使用方式,如果只是变更某一个节点,那么只需要通知这个节点的变更信息即可;
- 最后,如果是自建的注册中心,你也可以在其中加入一些保护策略,比如说如果通知的消息量达到某一个阈值就停止变更通知。
其实,服务的注册和发现归根结底是服务治理中的一环,**服务治理service governance**其实更直白的翻译应该是服务的管理,也就是解决多个服务节点组成集群的时候产生的一些复杂的问题。为了帮助你理解,**我来做个简单的比喻。**
你可以把集群看作是一个微型的城市,把道路看作是组成集群的服务,把行走在道路上的车看作是流量,那么服务治理就是对于整个城市道路的管理。
如果你新建了一条街道(相当于启动了一个新的服务节点),那么就要通知所有的车辆(流量)有新的道路可以走了;你关闭了一条街道,你也要通知所有车辆不要从这条路走了,**这就是服务的注册和发现。**
我们在道路上安装监控,监视每条道路的流量情况,**这就是服务的监控。**
道路一旦出现拥堵或者道路需要维修,那么就需要暂时封闭这条道路,由城市来统一调度车辆,走不堵的道路,**这就是熔断以及引流。**
道路之间纵横交错四通八达,一旦在某条道路上出现拥堵,但是又发现这条道路从头堵到尾,说明事故并不是发生在这条道路上,那么就需要从整体链路上来排查事故究竟处在哪个位置,**这就是分布式追踪。**
不同道路上的车辆有多有少,那么就需要有一个警察来疏导,在某一个时间走哪一条路会比较快,**这就是负载均衡。**
而这些问题,我会在后面的课程中针对性地讲解。
## 课程小结
本节课,我带你了解了在微服务架构中,注册中心是如何实现服务的注册和发现的,以及在实现中遇到的一些坑,除此之外,我还带你了解了服务治理的含义,以及后续我们会讲到的一些技术点。在这节课中,我想让你明确的重点如下:
- 注册中心可以让我们动态地变更RPC服务的节点信息对于动态扩缩容故障快速恢复以及服务的优雅关闭都有重要的意义
- 心跳机制是一种常见的探测服务状态的方式,你在实际的项目中也可以考虑使用;
- 我们需要对注册中心中管理的节点提供一些保护策略,避免节点被过度摘除导致的服务不可用。
你看,注册中心虽然是一种简单易懂的分布式组件,但是它在整体架构中的位置至关重要,不容忽视。同时,在它的设计方案中,也蕴含了一些系统设计的技巧,比如上面提到的服务状态检测的方式,还有上面提到的优雅关闭的方式,了解注册中心的原理,会给你之后的研发工作提供一些思路。
## 思考时间
结合你的实际经验,和我说说你们在服务化框架中使用的什么注册中心?当初是基于什么样的考虑来做选型的呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,193 @@
<audio id="audio" title="25 | 分布式Trace横跨几十个分布式组件的慢请求要如何排查" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a6/77/a6415ffdb0078c41a442e5ed30932677.mp3"></audio>
你好,我是唐扬。
经过前面几节课的学习你的垂直电商系统在引入RPC框架和注册中心之后已经完成基本的服务化拆分了系统架构也有了改变
<img src="https://static001.geekbang.org/resource/image/ac/4e/ac71f706f791e6f8d31d30513657534e.jpg" alt="">
现在,你的系统运行平稳,老板很高兴,你也安心了很多。而且你认为,在经过了服务化拆分之后,服务的可扩展性增强了很多,可以通过横向扩展服务节点的方式平滑地扩容了,对于应对峰值流量也更有信心了。
**但是这时出现了问题:**你通过监控发现系统的核心下单接口在晚高峰的时候会有少量的慢请求用户也投诉在APP上下单时等待的时间比较长。而下单的过程可能会调用多个RPC服务或者使用多个资源一时之间你很难快速判断究竟是哪个服务或者资源出了问题从而导致整体流程变慢。**于是你和你的团队开始想办法如何排查这个问题。**
## 一体化架构中的慢请求排查如何做
因为在分布式环境下,请求要在多个服务之间调用,所以对于慢请求问题的排查会更困难,**我们不妨从简单的入手,**先看看在一体化架构中是如何排查这个慢请求的问题的。
最简单的思路是:打印下单操作的每一个步骤的耗时情况,然后通过比较这些耗时的数据,找到延迟最高的一步,然后再来看看这个步骤要如何优化。如果有必要的话,你还需要针对步骤中的子步骤,再增加日志来继续排查,**简单的代码就像下面这样:**
```
long start = System.currentTimeMillis();
processA();
Logs.info(&quot;process A cost &quot; + (System.currentTimeMillis() - start));//打印A步骤的耗时
start = System.currentTimeMillis();
processB();
Logs.info(&quot;process B cost &quot; + (System.currentTimeMillis() - start));//打印B步骤的耗时
start = System.currentTimeMillis();
processC();
Logs.info(&quot;process C cost &quot; + (System.currentTimeMillis() - start));//打印C步骤的耗时
```
这是最简单的实现方式,打印出日志后,我们可以登录到机器上搜索关键词来查看每个步骤的耗时情况。
**虽然这个方式比较简单,但你可能很快就会遇到问题:**由于同时会有多个下单请求并行处理,所以,这些下单请求的每个步骤的耗时日志是相互穿插打印的。你无法知道这些日志哪些是来自于同一个请求,也就不能很直观地看到某一次请求耗时最多的步骤是哪一步了。那么你要如何把单次请求,每个步骤的耗时情况串起来呢?
**一个简单的思路是:**给同一个请求的每一行日志增加一个相同的标记。这样只要拿到这个标记就可以查询到这个请求链路上所有步骤的耗时了我们把这个标记叫做requestId我们可以在程序的入口处生成一个requestId然后把它放在线程的上下文中这样就可以在需要时随时从线程上下文中获取到requestId了。简单的代码实现就像下面这样
```
String requestId = UUID.randomUUID().toString();
ThreadLocal&lt;String&gt; tl = new ThreadLocal&lt;String&gt;(){
@Override
protected String initialValue() {
return requestId;
}
}; //requestId存储在线程上下文中
long start = System.currentTimeMillis();
processA();
Logs.info(&quot;rid : &quot; + tl.get() + &quot;, process A cost &quot; + (System.currentTimeMillis() - start)); // 日志中增加requestId
start = System.currentTimeMillis();
processB();
Logs.info(&quot;rid : &quot; + tl.get() + &quot;, process B cost &quot; + (System.currentTimeMillis() - start));
start = System.currentTimeMillis();
processC();
Logs.info(&quot;rid : &quot; + tl.get() + &quot;, process C cost &quot; + (System.currentTimeMillis() - start));
```
有了requestId你就可以清晰地了解一个调用链路上的耗时分布情况了。
于是,你给你的代码增加了大量的日志来排查下单操作缓慢的问题。**很快,** 你发现是某一个数据库查询慢了才导致了下单缓慢,然后你优化了数据库索引,问题最终得到了解决。
**正当你要松一口气的时候,问题接踵而至:**又有用户反馈某些商品业务打开缓慢;商城首页打开缓慢。你开始焦头烂额地给代码中增加耗时日志,而这时你意识到,每次排查一个接口就需要增加日志、重启服务,**这并不是一个好的办法,于是你开始思考解决的方案。**
**其实,从我的经验出发来说,**一个接口响应时间慢,一般是出在跨网络的调用上,比如说请求数据库、缓存或者依赖的第三方服务。所以,我们只需要针对这些调用的客户端类做切面编程,通过插入一些代码打印它们的耗时就好了。
说到切面编程AOP你应该并不陌生它是面向对象编程的一种延伸可以在不修改源代码的前提下给应用程序添加功能比如说鉴权、打印日志等等。如果你对切面编程的概念理解得还不透彻那我给你做个比喻**帮你理解一下。**
这就像开发人员在向代码仓库提交代码后,他需要对代码编译、构建、执行单元测试用例以保证提交的代码是没有问题的。但是如果每个人提交了代码都做这么多事儿,无疑会对开发同学造成比较大的负担,那么你可以配置一个持续集成的流程,在提交代码之后自动帮你完成这些操作,这个持续集成的流程就可以认为是一个切面。
**一般来说,切面编程的实现分为两类:**
<li>
一类是静态代理典型的代表是AspectJ它的特点是在编译期做切面代码注入
</li>
<li>
另一类是动态代理典型的代表是Spring AOP它的特点是在运行期做切面代码注入。
</li>
**这两者有什么差别呢?**以Java为例源代码Java文件先被Java编译器编译成Class文件然后Java虚拟机将Class装载进来之后进行必要的验证和初始化后就可以运行了。
静态代理是在编译期插入代码,增加了编译的时间,给你的直观感觉就是启动的时间变长了,但是一旦在编译期插入代码完毕之后在运行期就基本对于性能没有影响。
而动态代理不会去修改生成的Class文件而是会在运行期生成一个代理对象这个代理对象对源对象做了字节码增强来完成切面所要执行的操作。由于在运行期需要生成代理对象所以动态代理的性能要比静态代理要差。
我们做切面的原因,是想生成一些调试的日志,所以我们希望尽量减少对于原先接口性能的影响。**因此,我推荐采用静态代理的方式,实现切面编程。**
如果你的系统中需要增加切面来做一些校验、限流或者日志打印的工作,**我也建议你考虑使用静态代理的方式,**使用AspectJ做切面的简单代码实现就像下面这样
```
@Aspect
public class Tracer {
@Around(value = &quot;execution(public methodsig)&quot;, argNames = &quot;pjp&quot;) //execution内替换要做切面的方法签名
public Object trace(ProceedingJoinPoint pjp) throws Throwable {
TraceContext traceCtx = TraceContext.get(); //获取追踪上下文,上下文的初始化可以在程序入口处
String requestId = reqCtx.getRequestId(); //获取requestId
String sig = pjp.getSignature().toShortString(); //获取方法签名
boolean isSuccessful = false;
String errorMsg = &quot;&quot;;
Object result = null;
long start = System.currentTimeMillis();
try {
result = pjp.proceed();
isSuccessful = true;
return result;
} catch (Throwable t) {
isSuccessful = false;
errorMsg = t.getMessage();
return result;
} finally {
long elapseTime = System.currentTimeMillis() - start;
Logs.info(&quot;rid : &quot; + requestId + &quot;, start time: &quot; + start + &quot;, elapseTime: &quot; + elapseTime + &quot;, sig: &quot; + sig + &quot;, isSuccessful: &quot; + isSuccessful + &quot;, errorMsg: &quot; + errorMsg );
}
}
}
```
这样你就在你的系统的每个接口中打印出了所有访问数据库、缓存、外部接口的耗时情况一次请求可能要打印十几条日志如果你的电商系统的QPS是10000的话就是每秒钟会产生十几万条日志对于磁盘I/O的负载是巨大的**那么这时,你就要考虑如何减少日志的数量。**
**你可以考虑对请求做采样,**采样的方式也简单比如你想采样10%的日志那么你可以只打印“requestId%10==0”的请求。
有了这些日志之后当给你一个requestId的时候你发现自己并不能确定这个请求到了哪一台服务器上所以你不得不登录所有的服务器去搜索这个requestId才能定位请求。**这样无疑会增加问题排查的时间。**
**你可以考虑的解决思路是:**把日志不打印到本地文件中而是发送到消息队列里再由消息处理程序写入到集中存储中比如Elasticsearch。这样你在排查问题的时候只需要拿着requestId到Elasticsearch中查找相关的记录就好了。在加入消息队列和Elasticsearch之后我们这个排查程序的架构图也会有所改变
<img src="https://static001.geekbang.org/resource/image/ae/7a/ae25d911a438dc8ca1adb816595a787a.jpg" alt="">
我来总结一下,为了排查单次请求响应时间长的原因,我们主要做了哪些事情:
<li>
在记录打点日志时我们使用requestId将日志串起来这样方便比较一次请求中的多个步骤的耗时情况
</li>
<li>
我们使用静态代理的方式做切面编程,避免在业务代码中,加入大量打印耗时的日志的代码,减少了对于代码的侵入性,同时编译期的代码注入可以减少;
</li>
<li>
我们增加了日志采样率,避免全量日志的打印;
</li>
<li>
最后为了避免在排查问题时需要到多台服务器上搜索日志我们使用消息队列将日志集中起来放在了Elasticsearch中。
</li>
## 如何来做分布式Trace
你可能会问题目既然是“分布式Trace横跨几十个分布式组件的慢请求要如何排查那么我为什么要花费大量的篇幅来说明在一体化架构中如何排查问题呢**这是因为在分布式环境下,**你基本上也是依据上面我提到的这几点来构建分布式追踪的中间件的。
在一体化架构中单次请求的所有的耗时日志都被记录在一台服务器上而在微服务的场景下单次请求可能跨越多个RPC服务这就造成了单次的请求的日志会分布在多个服务器上。
当然你也可以通过requestId将多个服务器上的日志串起来但是仅仅依靠requestId很难表达清楚服务之间的调用关系所以从日志中就无法了解服务之间是谁在调用谁。因此我们采用traceId + spanId这两个数据维度来记录服务之间的调用关系这里traceId就是requestId也就是使用traceId串起单次请求用spanId记录每一次RPC调用。**说起来可能比较抽象,我给你举一个具体的例子。**
比如你的请求从用户端过来先到达A服务A服务会分别调用B和C服务B服务又会调用D和E服务。
<img src="https://static001.geekbang.org/resource/image/ba/10/ba9e63bbcccd910df41b75b925ad5910.jpg" alt="">
我来给你讲讲图中的内容:
- 用户到A服务之后会初始化一个traceId为100spanId为1
- A服务调用B服务时traceId不变而spanId用1.1标识代表上一级的spanId是1这一级的调用次序是1
- A调用C服务时traceId依然不变spanId则变为了1.2代表上一级的spanId还是1而调用次序则变成了2以此类推。
通过这种方式,我们可以在日志中清晰地看出服务的调用关系是如何的,方便在后续计算中调整日志顺序,打印出完整的调用链路。
**那么spanId是何时生成的又是如何传递的呢**这部分内容可以算作一个延伸点能够帮你了解分布式Trace中间件的实现原理。
首先A服务在发起RPC请求服务B前先从线程上下文中获取当前的traceId和spanId然后依据上面的逻辑生成本次RPC调用的spanId再将spanId和traceId序列化后装配到请求体中发送给服务方B。
服务方B获取请求后从请求体中反序列化出spanId和traceId同时设置到线程上下文中以便给下次RPC调用使用。在服务B调用完成返回响应前计算出服务B的执行时间发送给消息队列。
当然在服务B中你依然可以使用切面编程的方式得到所有调用的数据库、缓存、HTTP服务的响应时间只是在发送给消息队列的时候要加上当前线程上下文中的spanId和traceId。
这样无论是数据库等资源的响应时间还是RPC服务的响应时间就都汇总到了消息队列中在经过一些处理之后最终被写入到Elasticsearch中以便给开发和运维同学查询使用。
而在这里你大概率会遇到的问题还是性能的问题也就是因为引入了分布式追踪中间件导致对于磁盘I/O和网络I/O的影响**而我给你的“避坑”指南就是:**如果你是自研的分布式Trace中间件那么一定要提供一个开关方便在线上随时将日志打印关闭如果使用开源的组件可以开始设置一个较低的日志采样率观察系统性能情况再调整到一个合适的数值。
## 课程小结
本节课我带你了解了在一体化架构和服务化架构中,你要如何排查单次慢请求中,究竟哪一个步骤是瓶颈,这里你需要了解的主要有以下几个重点:
<li>
服务的追踪的需求主要有两点,一点对代码要无侵入,你可以使用切面编程的方式来解决;另一点是性能上要低损耗,我建议你采用静态代理和日志采样的方式,来尽量减少追踪日志对于系统性能的影响;
</li>
<li>
无论是单体系统还是服务化架构无论是服务追踪还是业务问题排查你都需要在日志中增加requestId这样可以将你的日志串起来给你呈现一个完整的问题场景。如果requestId可以在客户端上生成在请求业务接口的时候传递给服务端那么就可以把客户端的日志体系也整合进来对于问题的排查帮助更大。
</li>
其实分布式追踪系统不是一项新的技术而是若干项已有技术的整合在实现上并不复杂却能够帮助你实现跨进程调用链展示、服务依赖分析在性能优化和问题排查方面提供数据上的支持。所以在微服务化过程中它是一个必选项无论是采用ZipkinJaeger这样的开源解决方案还是团队内自研你都应该在微服务化完成之前尽快让它发挥应有的价值。
## 一课一思
你在项目中是否接入过分布式追踪系统呢?在使用过程中它帮助你排查了哪些问题呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,138 @@
<audio id="audio" title="26 | 负载均衡:怎样提升系统的横向扩展能力?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/41/97/41cd57c9507d7247577c22492f3e1d97.mp3"></audio>
你好,我是唐扬。
在基础篇中,我提到了高并发系统设计的三个通用方法:缓存、异步和横向扩展。到目前为止,你接触到了缓存的使用姿势,也了解了如何使用消息队列异步处理业务逻辑。那么本节课,我将带你了解一下如何提升系统的横向扩展能力。
在之前的课程中,我也提到过提升系统横向扩展能力的一些案例。比如,[08讲](https://time.geekbang.org/column/article/145095)提到可以通过部署多个从库的方式来提升数据库的扩展能力从而提升数据库的查询性能那么就需要借助组件将查询数据库的请求按照一些既定的策略分配到多个从库上这是负载均衡服务器所起的作用而我们一般使用DNS服务器来承担这个角色。
不过在实际的工作中你经常使用的负载均衡的组件应该算是Nginx它的作用是承接前端的HTTP请求然后将它们按照多种策略分发给后端的多个业务服务器上。这样我们可以随时通过扩容业务服务器的方式来抵挡突发的流量高峰。与DNS不同的是Nginx可以在域名和请求URL地址的层面做更细致的流量分配也提供更复杂的负载均衡策略。
你可能会想到,在微服务架构中我们也会启动多个服务节点承接从用户端到应用服务器的请求,自然会需要一个负载均衡服务器作为流量的入口,实现流量的分发。那么在微服务架构中,如何使用负载均衡服务器呢?
在回答这些问题之前,我先带你了解一下常见的负载均衡服务器都有哪几类,因为这样,你就可以根据不同类型负载均衡服务器的特点做选择了。
## 负载均衡服务器的种类
**负载均衡的含义是:**将负载(访问的请求)“均衡”地分配到多个处理节点上。这样可以减少单个处理节点的请求量,提升整体系统的性能。
同时,负载均衡服务器作为流量入口,可以对请求方屏蔽服务节点的部署细节,实现对于业务方无感知的扩容。它就像交通警察,不断地疏散交通,将汽车引入合适的道路上。
**而在我看来,**负载均衡服务大体上可以分为两大类:一类是代理类的负载均衡服务;另一类是客户端负载均衡服务。
代理类的负载均衡服务以单独的服务方式部署,所有的请求都要先经过负载均衡服务,在负载均衡服务中选出一个合适的服务节点后,再由负载均衡服务调用这个服务节点来实现流量的分发。
<img src="https://static001.geekbang.org/resource/image/7a/c9/7a76b0b7c6e3fc4b60cfcda8dbd93dc9.jpg" alt="">
由于这类服务需要承担全量的请求所以对于性能的要求极高。代理类的负载均衡服务有很多开源实现比较著名的有LVS、Nginx等等。LVS在OSI网络模型中的第四层传输层工作所以LVS又可以称为四层负载而Nginx运行在OSI网络模型中的第七层应用层所以又可以称它为七层负载你可以回顾一下[02讲](https://time.geekbang.org/column/article/138331)的内容)。
在项目的架构中我们一般会同时部署LVS和Nginx来做HTTP应用服务的负载均衡。也就是说在入口处部署LVS将流量分发到多个Nginx服务器上再由Nginx服务器分发到应用服务器上**为什么这么做呢?**
主要和LVS和Nginx的特点有关LVS是在网络栈的四层做请求包的转发请求包转发之后由客户端和后端服务直接建立连接后续的响应包不会再经过LVS服务器所以相比Nginx性能会更高也能够承担更高的并发。
可LVS缺陷是工作在四层而请求的URL是七层的概念不能针对URL做更细致的请求分发而且LVS也没有提供探测后端服务是否存活的机制而Nginx虽然比LVS的性能差很多但也可以承担每秒几万次的请求并且它在配置上更加灵活还可以感知后端服务是否出现问题。
因此LVS适合在入口处承担大流量的请求分发而Nginx要部署在业务服务器之前做更细维度的请求分发。**我给你的建议是,**如果你的QPS在十万以内那么可以考虑不引入LVS而直接使用Nginx作为唯一的负载均衡服务器这样少维护一个组件也会减少系统的维护成本。
不过这两个负载均衡服务适用于普通的Web服务对于微服务架构来说它们是不合适的。因为微服务架构中的服务节点存储在注册中心里使用LVS就很难和注册中心交互获取全量的服务节点列表。另外一般微服务架构中使用的是RPC协议而不是HTTP协议所以Nginx也不能满足要求。
**所以我们会使用另一类的负载均衡服务客户端负载均衡服务也就是把负载均衡的服务内嵌在RPC客户端中。**
它一般和客户端应用部署在一个进程中,提供多种选择节点的策略,最终为客户端应用提供一个最佳的、可用的服务端节点。这类服务一般会结合注册中心来使用,注册中心提供服务节点的完整列表,客户端拿到列表之后使用负载均衡服务的策略选取一个合适的节点,然后将请求发到这个节点上。
<img src="https://static001.geekbang.org/resource/image/53/c1/539f9fd7196c3c0b17eba55584d4c6c1.jpg" alt="">
了解负载均衡服务的分类是你学习负载均衡服务的第一步,接下来,你需要掌握负载均衡策略,这样一来,你在实际工作中配置负载均衡服务的时候,可以对原理有更深刻的了解。
## 常见的负载均衡策略有哪些
负载均衡策略从大体上来看可以分为两类:
- 一类是静态策略,也就是说负载均衡服务器在选择服务节点时,不会参考后端服务的实际运行的状态;
- 一类是动态策略,也就是说负载均衡服务器会依据后端服务的一些负载特性,来决定要选择哪一个服务节点。
常见的静态策略有几种,其中使用最广泛的是**轮询的策略RoundRobinRR**这种策略会记录上次请求后端服务的地址或者序号,然后在请求时按照服务列表的顺序,请求下一个后端服务节点。伪代码如下:
```
AtomicInteger lastCounter = getLastCounter();//获取上次请求的服务节点的序号
List&lt;String&gt; serverList = getServerList(); // 获取服务列表
int currentIndex = lastCounter.addAndGet(); //增加序列号
if(currentIndex &gt;= serverList.size()) {
currentIndex = 0;
}
setLastCounter(currentIndex);
return serverList.get(currentIndex);
```
它其实是一种通用的策略基本上大部分的负载均衡服务器都支持。轮询的策略可以做到将请求尽量平均地分配到所有服务节点上但是它没有考虑服务节点的具体配置情况。比如你有三个服务节点其中一个服务节点的配置是8核8G另外两个节点的配置是4核4G那么如果使用轮询的方式来平均分配请求的话8核8G的节点分到的请求数量和4核4G的一样多就不能发挥性能上的优势了
所以我们考虑给节点加上权重值比如给8核8G的机器配置权重为2那么就会给它分配双倍的流量**这种策略就是带有权重的轮询策略。**
除了这两种策略之外,目前开源的负载均衡服务还提供了很多静态策略:
- Nginx提供了ip_hash和url_hash算法
- LVS提供了按照请求的源地址和目的地址做Hash的策略
- Dubbo也提供了随机选取策略以及一致性Hash的策略。
**但是在我看来,**轮询和带有权重的轮询策略能够将请求尽量平均地分配到后端服务节点上也就能够做到对于负载的均衡分配。在没有更好的动态策略之前应该优先使用这两种策略比如Nginx就会优先使用轮询的策略。
而目前开源的负载均衡服务中,也会提供一些动态策略,我强调一下它们的原理。
在负载均衡服务器上会收集对后端服务的调用信息,比如从负载均衡端到后端服务的活跃连接数,或者是调用的响应时间,然后从中选择连接数最少的服务,或者响应时间最短的后端服务。**我举几个具体的例子:**
- Dubbo提供的LeastAcive策略就是优先选择活跃连接数最少的服务
- Spring Cloud全家桶中的Ribbon提供了WeightedResponseTimeRule是使用响应时间给每个服务节点计算一个权重然后依据这个权重来给调用方分配服务节点。
**这些策略的思考点**是从调用方的角度出发,选择负载最小、资源最空闲的服务来调用,以期望能得到更高的服务调用性能,也就能最大化地使用服务器的空闲资源,请求也会响应得更迅速。**所以我建议你,**在实际开发中,优先考虑使用动态的策略。
到目前为止,你已经可以根据上面的分析,选择适合自己的负载均衡策略,并选择一个最优的服务节点。**那么问题来了:**你怎么保证选择出来的这个节点,一定是一个可以正常服务的节点呢?如果你采用的是轮询的策略,选择出来的是一个故障节点又要怎么办呢?所以,为了降低请求被分配到一个故障节点的几率,有些负载均衡服务器还提供了对服务节点的故障检测功能。
## 如何检测节点是否故障
[24讲](https://time.geekbang.org/column/article/167151)中,我带你了解到在微服务化架构中,服务节点会定期地向注册中心发送心跳包,这样注册中心就能够知晓服务节点是否故障,也就可以确认传递给负载均衡服务的节点一定是可用的。
但对于Nginx来说**我们要如何保证配置的服务节点是可用的呢?**
这就要感谢淘宝开源的Nginx模块[nginx_upstream_check_module](https://github.com/yaoweibin/nginx_upstream_check_module)了这个模块可以让Nginx定期地探测后端服务的一个指定的接口然后根据返回的状态码来判断服务是否还存活。当探测不存活的次数达到一定阈值时就自动将这个后端服务从负载均衡服务器中摘除。**它的配置样例如下:**
```
upstream server {
server 192.168.1.1:8080;
server 192.168.1.2:8080;
check interval=3000 rise=2 fall=5 timeout=1000 type=http default_down=true;//检测间隔为3秒检测超时时间是1秒使用http协议。如果连续失败次数达到5次就认为服务不可用如果连续成功次数达到2次则认为服务可用。后端服务刚启动时状态是不可用的
check_http_send &quot;GET /health_check HTTP/1.0\r\n\r\n&quot;; //检测URL
check_http_expect_alive http_2xx; //检测返回状态码为200时认为检测成功
}
```
Nginx按照上面的方式配置之后你的业务服务器也要实现一个“/health_check”的接口在这个接口中返回的HTTP状态码这个返回的状态码可以存储在配置中心中这样在变更状态码时就不需要重启服务了配置中心在第33节课中会讲到
节点检测的功能还能够帮助我们实现Web服务的优雅关闭。在[24讲](https://time.geekbang.org/column/article/167151)中介绍注册中心时我曾经提到服务的优雅关闭需要先切除流量再关闭服务使用了注册中心之后就可以先从注册中心中摘除节点再重启服务以便达到优雅关闭的目的。那么Web服务要如何实现优雅关闭呢接下来我们了解一下有了节点检测功能之后服务是如何启动和关闭的。
**在服务刚刚启动时,**可以初始化默认的HTTP状态码是500这样Nginx就不会很快将这个服务节点标记为可用也就可以等待服务中依赖的资源初始化完成避免服务初始启动时的波动。
**在完全初始化之后,**再将HTTP状态码变更为200Nginx经过两次探测后就会标记服务为可用。在服务关闭时也应该先将HTTP状态码变更为500等待Nginx探测将服务标记为不可用后前端的流量也就不会继续发往这个服务节点。在等待服务正在处理的请求全部处理完毕之后再对服务做重启可以避免直接重启导致正在处理的请求失败的问题。**这是启动和关闭线上Web服务时的标准姿势你可以在项目中参考使用。**
## 课程小结
本节课,我带你了解了与负载均衡服务相关的一些知识点,以及在实际工作中的运用技巧。我想强调几个重点:
<li>
网站负载均衡服务的部署是以LVS承接入口流量在应用服务器之前部署Nginx做细化的流量分发和故障节点检测。当然如果你的网站的并发不高也可以考虑不引入LVS。
</li>
<li>
负载均衡的策略可以优先选择动态策略,保证请求发送到性能最优的节点上;如果没有合适的动态策略,那么可以选择轮询的策略,让请求平均分配到所有的服务节点上。
</li>
<li>
Nginx可以引入nginx_upstream_check_module对后端服务做定期的存活检测后端的服务节点在重启时也要秉承着“先切流量后重启”的原则尽量减少节点重启对于整体系统的影响。
</li>
你可能会认为像Nginx、LVS应该是运维所关心的组件作为开发人员不用操心维护。**不过通过今天的学习你应该可以看到:**负载均衡服务是提升系统扩展性和性能的重要组件,在高并发系统设计中,它发挥的作用是无法替代的。理解它的原理,掌握使用它的正确姿势,应该是每一个后端开发同学的必修课。
## 一课一思
在实际的工作中,你一定也用过很多的负载均衡的服务和组件,那么在使用过程中你遇到过哪些问题呢,有哪些注意的点呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,146 @@
<audio id="audio" title="27 | API网关系统的门面要如何做呢" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b9/6f/b99f4616c5983f007ace270156ec696f.mp3"></audio>
你好,我是唐扬。
到目前为止,你的垂直电商系统在经过微服务化拆分之后已经运行了一段时间了,系统的扩展性得到了很大的提升,也能够比较平稳地度过高峰期的流量了。
不过最近你发现,随着自己的电商网站知名度越来越高,系统迎来了一些“不速之客”,在凌晨的时候,系统中的搜索商品和用户接口的调用量会急剧上升,持续一段时间之后又回归正常。
**这些搜索请求有一个共同特征是来自固定的几台设备。**当你在搜索服务上加一个针对设备ID的限流功能之后凌晨的高峰搜索请求不见了。但是不久之后用户服务也出现了大量爬取用户信息的请求商品接口出现了大量爬取商品信息的请求。你不得不在这两个服务上也增加一样的限流策略。
**但是这样会有一个问题:**不同的三个服务上使用同一种策略,在代码上会有冗余,无法做到重用,如果其他服务上也出现类似的问题,还要通过拷贝代码来实现,肯定是不行的。
不过作为Java程序员**你很容易想到:**将限流的功能独立成一个单独的jar包给这三个服务来引用。不过你忽略了一种情况那就是你的电商团队使用的除了Java还有PHP和Go等多种语言。
用多种语言开发的服务是没有办法使用jar包来实现限流功能的**这时你需要引入API网关。**
## API网关起到的作用
API网关API Gateway不是一个开源组件而是一种架构模式它是将一些服务共有的功能整合在一起独立部署为单独的一层用来解决一些服务治理的问题。你可以把它看作系统的边界它可以对出入系统的流量做统一的管控。
在我看来API网关可以分为两类**一类叫做入口网关,一类叫做出口网关。**
入口网关是我们经常使用的网关种类,它部署在负载均衡服务器和应用服务器之间,**主要有几方面的作用。**
<li>
它提供客户端一个统一的接入地址API网关可以将用户的请求动态路由到不同的业务服务上并且做一些必要的协议转换工作。**在你的系统中,你部署的微服务对外暴露的协议可能不同:**有些提供的是HTTP服务有些已经完成RPC改造对外暴露RPC服务有些遗留系统可能还暴露的是Web Service服务。API网关可以对客户端屏蔽这些服务的部署地址以及协议的细节给客户端的调用带来很大的便捷。
</li>
<li>
另一方面在API网关中我们可以植入一些服务治理的策略比如服务的熔断、降级、流量控制和分流等等关于服务降级和流量控制的细节我会在后面的课程中具体讲解在这里你只要知道它们可以在API网关中实现就可以了
</li>
<li>
再有客户端的认证和授权的实现也可以放在API网关中。你要知道不同类型的客户端使用的认证方式是不同的。**在我之前项目中,**手机APP使用Oauth协议认证HTML5端和Web端使用Cookie认证内部服务使用自研的Token认证方式。这些认证方式在API网关上可以得到统一处理应用服务不需要了解认证的细节。
</li>
<li>
另外API网关还可以做一些与黑白名单相关的事情比如针对设备ID、用户IP、用户ID等维度的黑白名单。
</li>
<li>
最后在API网关中也可以做一些日志记录的事情比如记录HTTP请求的访问日志我在[25讲](https://time.geekbang.org/column/article/167979)中讲述分布式追踪系统时提到的标记一次请求的requestId也可以在网关中来生成。
</li>
<img src="https://static001.geekbang.org/resource/image/e7/25/e7fef913472514fb01f4c8ee112d0325.jpg" alt="">
**出口网关就没有这么丰富的功能和作用了。**我们在系统开发中会依赖很多外部的第三方系统典型的例子第三方账户登录、使用第三方工具支付等等。我们可以在应用服务器和第三方系统之间部署出口网关在出口网关中对调用外部的API做统一的认证、授权、审计以及访问控制。
<img src="https://static001.geekbang.org/resource/image/cd/63/cd4174a43b289b0538811293a93daf63.jpg" alt="">
我花一定的篇幅去讲API网关起到的作用主要是想让你了解API网关可以解决什么样的实际问题。这样一来当你在面对这些问题时你就会有解决的思路不会手足无措了。
## API网关要如何实现
了解API网关的作用之后接下来我们来看看API网关在实现中需要关注哪些点以及常见的开源API网关有哪些这样你在实际工作中无论是考虑自研API网关还是使用开源的实现都会比较自如了。
在实现一个API网关时你首先要考虑的是它的性能。这很好理解API入口网关承担从客户端的所有流量。假如业务服务处理时间是10ms而API网关的耗时在1ms那么相当于每个接口的响应时间都要增加10%这对于性能的影响无疑是巨大的。而提升API网关性能的关键还是在I/O模型上我在[23讲](https://time.geekbang.org/column/article/165765)中详细讲到过这里只是举一个例子来说明I/O模型对于性能的影响。
Netfix开源的API网关Zuul在1.0版本的时候使用的是同步阻塞I/O模型整体系统其实就是一个servlet在接收到用户的请求然后执行在网关中配置的认证、协议转换等逻辑之后调用后端的服务获取数据返回给用户。
而在Zuul2.0中Netfix团队将servlet改造成了一个netty servernetty服务采用I/O多路复用的模型处理接入的I/O请求并且将之前同步阻塞调用后端服务的方式改造成使用netty clientnetty客户端非阻塞调用的方式。改造之后Netfix团队经过测试发现性能提升了20%左右。
除此之外API网关中执行的动作有些是可以预先定义好的比如黑白名单的设置接口动态路由有些则是需要业务方依据自身业务来定义。**所以API网关的设计要注意扩展性**也就是你可以随时在网关的执行链路上增加一些逻辑,也可以随时下掉一些逻辑(也就是所谓的热插拔)。
所以一般来说我们可以把每一个操作定义为一个filter过滤器然后使用“责任链模式”将这些filter串起来。责任链可以动态地组织这些filter解耦filter之间的关系无论是增加还是减少filter都不会对其他的filter有任何的影响。
**Zuul就是采用责任链模式**Zuul1中将filter定义为三类pre routing filter路由前过滤器、routing filter路由过滤器和after routing filter路由后过滤器。每一个filter定义了执行的顺序在filter注册时会按照顺序插入到filter chain过滤器链中。这样Zuul在接收到请求时就会按照顺序依次执行插入到filter chain中的filter了。
<img src="https://static001.geekbang.org/resource/image/a1/88/a1c11d4059e55b0521dd0cf19cf73488.jpg" alt="">
**另外还需要注意的一点是,**为了提升网关对于请求的并行处理能力,我们一般会使用线程池来并行的执行请求。**不过,这就带来一个问题:**如果商品服务出现问题造成响应缓慢,那么调用商品服务的线程就会被阻塞无法释放,久而久之,线程池中的线程就会被商品服务所占据,那么其他服务也会受到级联的影响。因此,我们需要针对不同的服务做线程隔离或者保护。**在我看来有两种思路:**
<li>
如果你后端的服务拆分得不多,可以针对不同的服务,采用不同的线程池,这样商品服务的故障就不会影响到支付服务和用户服务了;
</li>
<li>
在线程池内部可以针对不同的服务甚至不同的接口做线程的保护。比如说线程池的最大线程数是1000那么可以给每个服务设置一个最多可以使用的配额。
</li>
一般来说,服务的执行时间应该在毫秒级别,线程被使用后会很快被释放回到线程池给后续请求使用,同时处于执行中的线程数量不会很多,对服务或者接口设置线程的配额不会影响到正常的执行。可是一旦发生故障,某个接口或者服务的响应时间变长,造成线程数暴涨,但是因为有配额的限制,也就不会影响到其他的接口或者服务了。
**你在实际应用中也可以将这两种方式结合,**比如说针对不同的服务使用不同的线程池,在线程池内部针对不同的接口设置配额。
以上就是实现API网关的一些关键的点你如果要自研API网关服务的话可以参考借鉴。另外API网关也有很多开源的实现目前使用比较广泛的有以下几个
<li>
[Kong](https://konghq.com/faqs/)是在Nginx中运行的Lua程序。得益于Nginx的性能优势Kong相比于其它的开源API网关来说性能方面是最好的。由于大中型公司对于Nginx运维能力都比较强所以选择Kong作为API网关无论是在性能还是在运维的把控力上都是比较好的选择
</li>
<li>
[Zuul](https://github.com/Netflix/zuul)是Spring Cloud全家桶中的成员如果你已经使用了Spring Cloud中的其他组件那么也可以考虑使用Zuul和它们无缝集成。不过Zuul1因为采用同步阻塞模型所以在性能上并不是很高效而Zuul2推出时间不长难免会有坑。但是Zuul的代码简单易懂可以很好地把控并且你的系统的量级很可能达不到Netfix这样的级别所以对于Java技术栈的团队使用Zuul也是一个不错的选择
</li>
<li>
[Tyk](https://tyk.io/)是一种Go语言实现的轻量级API网关有着丰富的插件资源对于Go语言栈的团队来说也是一种不错的选择。
</li>
**那么你要考虑的是,**这些开源项目适不适合作为API网关供自己使用。而接下来我以电商系统为例带你将API网关引入我们的系统之中。
## 如何在你的系统中引入API网关
目前为止我们的电商系统已经经过了服务化改造在服务层和客户端之间有一层薄薄的Web层**这个Web层做的事情主要有两方面**
一方面是对服务层接口数据的聚合。比如,商品详情页的接口可能会聚合服务层中,获取商品信息、用户信息、店铺信息以及用户评论等多个服务接口的数据;
另一方面Web层还需要将HTTP请求转换为RPC请求并且对前端的流量做一些限制对于某些请求添加设备ID的黑名单等等。
因此我们在做改造的时候可以先将API网关从Web层中独立出来将协议转换、限流、黑白名单等事情挪到API网关中来处理形成独立的入口网关层
而针对服务接口数据聚合的操作,**一般有两种解决思路:**
<li>
再独立出一组网关专门做服务聚合、超时控制方面的事情,我们一般把前一种网关叫做流量网关,后一种可以叫做业务网关;
</li>
<li>
抽取独立的服务层,专门做接口聚合的操作。这样服务层就大概分为原子服务层和聚合服务层。
</li>
我认为,接口数据聚合是业务操作,与其放在通用的网关层来实现,不如放在更贴近业务的服务层来实现,**所以,我更倾向于第二种方案。**
<img src="https://static001.geekbang.org/resource/image/ab/f2/ab701c40ed8229606a4bf90db327c2f2.jpg" alt="">
同时,我们可以在系统和第三方支付服务,以及登陆服务之间部署出口网关服务。原先,你会在拆分出来的支付服务中完成对于第三方支付接口所需要数据的加密、签名等操作,再调用第三方支付接口完成支付请求。现在,你把对数据的加密、签名的操作放在出口网关中,这样一来,支付服务只需要调用出口网关的统一支付接口就可以了。
在引入了API网关之后我们的系统架构就变成了下面这样
<img src="https://static001.geekbang.org/resource/image/76/da/766076d1193755a50a325e744bc452da.jpg" alt="">
## 课程小结
本节课我带你了解了API网关在系统中的作用在实现中的一些关键的点以及如何将API网关引入你的系统**我想强调的重点如下:**
<li>
API网关分为入口网关和出口网关两类入口网关作用很多可以隔离客户端和微服务从中提供协议转换、安全策略、认证、限流、熔断等功能。出口网关主要是为调用第三方服务提供统一的出口在其中可以对调用外部的API做统一的认证、授权、审计以及访问控制
</li>
<li>
API网关的实现重点在于性能和扩展性你可以使用多路I/O复用模型和线程池并发处理来提升网关性能使用责任链模式来提升网关的扩展性
</li>
<li>
API网关中的线程池可以针对不同的接口或者服务做隔离和保护这样可以提升网关的可用性
</li>
<li>
API网关可以替代原本系统中的Web层将Web层中的协议转换、认证、限流等功能挪入到API网关中将服务聚合的逻辑下沉到服务层。
</li>
API网关可以为API的调用提供便捷也可以为将一些服务治理的功能独立出来达到复用的目的虽然在性能上可能会有一些损耗**但是一般来说,**使用成熟的开源API网关组件这些损耗都是可以接受的。所以当你的微服务系统越来越复杂时你可以考虑使用API网关作为整体系统的门面。
## 一课一思
你的项目中是否有使用API网关呢你在使用API网关的时候遇到过什么样的问题吗欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,120 @@
<audio id="audio" title="28 | 多机房部署:跨地域的分布式系统如何做?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b7/79/b7235af1280411e5d0fe1e299058d479.mp3"></audio>
你好,我是唐扬。
**来想象这样一个场景:**你的垂直电商系统部署的IDC机房在某一天发布了公告说机房会在第二天凌晨做一次网络设备的割接在割接过程中会不定时出现瞬间或短时间网络中断。
机房网络的中断肯定会对业务造成不利的影响即使割接的时间在凌晨业务的低峰期作为技术负责人的你也要尽量思考方案来规避隔离的影响。然而不幸的是在现有的技术架构下电商业务全都部署在一个IDC机房中你并没有好的解决办法。
而IDC机房的可用性问题是整个系统的阿喀琉斯之踵一旦IDC机房像一些大厂一样出现很严重的问题就会对整体服务的可用性造成严重的影响。比如
2016年7月北京联通整顿旗下40多个IDC机房中不规范的接入情况大批不合规接入均被断网这一举动致使脉脉当时使用的蓝汛机房受到影响脉脉宕机长达15个小时著名的A站甚至宕机超过48个小时损失可想而知。
而目前,单一机房部署的架构特点决定了你的系统可用性受制于机房的可用性,也就是机房掌控了系统的生命线。所以,你开始思考如何通过架构的改造进一步提升系统的可用性。在网上搜索解决方案和学习一些大厂的经验后,你发现“多机房部署”可以解决这个问题。
## 多机房部署的难点是什么
**多机房部署的含义是:**在不同的IDC机房中部署多套服务这些服务共享同一份业务数据并且都可以承接来自用户的流量。
这样,当其中某一个机房出现网络故障、火灾,甚至整个城市发生地震、洪水等大的不可抗的灾难时,你可以随时将用户的流量切换到其它地域的机房中,从而保证系统可以不间断地持续运行。这种架构听起来非常美好,但是在实现上却是非常复杂和困难的,那么它复杂在哪儿呢?
假如我们有两个机房A和B都部署了应用服务数据库的主库和从库部署在A机房那么机房B的应用如何访问到数据呢有两种思路。
**一个思路是直接跨机房读取A机房的从库**
<img src="https://static001.geekbang.org/resource/image/72/b9/72938f06f3193b7bd30223d188475bb9.jpg" alt="">
**另一个思路是在机房B部署一个从库跨机房同步主库的数据然后机房B的应用就可以读取这个从库的数据了**
<img src="https://static001.geekbang.org/resource/image/49/4d/4924474ef8379137c6effe923a19e04d.jpg" alt="">
无论是哪一种思路,**都涉及到跨机房的数据传输,**这就对机房之间延迟情况有比较高的要求了。而机房之间的延迟和机房之间的距离息息相关,你可以记住几个数字。
1.北京同地双机房之间的专线延迟一般在1ms~3ms。
这个延迟会造成怎样的影响呢要知道我们的接口响应时间需要控制在200ms之内而一个接口可能会调用几次第三方HTTP服务或者RPC服务。如果这些服务部署在异地机房那么接口响应时间就会增加几毫秒是可以接受的。
一次接口可能会涉及几次的数据库写入,那么如果数据库主库在异地机房,那么接口的响应时间也会因为写入异地机房的主库,增加几毫秒到十几毫秒,也是可以接受的。
但是,接口读取缓存和数据库的数量可能会达到十几次甚至几十次,那么这就会增加几十毫秒甚至上百毫秒的延迟,就不能接受了。
2.国内异地双机房之间的专线延迟会在50ms之内。
具体的延迟数据依据距离的不同而不同。比如北京到天津的专线延迟会在10ms之内而北京到上海的延迟就会提高到接近30ms如果想要在北京和广州部署双机房那么延迟就会到达50ms了。在这个延迟数据下要想保证接口的响应时间在200ms之内就要尽量减少跨机房的服务调用更要避免跨机房的数据库和缓存操作了。
3.如果你的业务是国际化的服务需要部署跨国的双机房那么机房之间的延迟就更高了依据各大云厂商的数据来看比如从国内想要访问部署在美国西海岸的服务这个延迟会在100ms~200ms左右。在这个延迟下就要避免数据跨机房同步调用而只做异步的数据同步。
如果你正在考虑多机房部署的架构,那么这些数字都是至关重要的基础数据,**你需要牢牢记住,避免出现跨机房访问数据造成性能衰减问题。**
机房之间的数据延迟在客观上是存在的,你没有办法改变。你可以做的,就是尽量避免数据延迟对于接口响应时间的影响。那么在数据延迟下,你要如何设计多机房部署的方案呢?
## 逐步迭代多机房部署方案
### 1.同城双活
制定多机房部署的方案不是一蹴而就的而是不断迭代发展的。我在上面提到同城机房之间的延时在1ms~3ms左右对于跨机房调用的容忍度比较高所以这种同城双活的方案复杂度会比较低。
但是,它只能做到机房级别的容灾,无法做到城市级别的容灾。不过,相比于城市发生地震、洪水等自然灾害来说,机房网络故障、掉电出现的概率要大得多。所以,如果你的系统不需要考虑城市级别的容灾,一般做到同城双活就足够了。那么,同城双活的方案要如何设计呢?
**假设这样的场景:**你在北京有A和B两个机房A是联通的机房B是电信的机房机房之间以专线连接方案设计时核心思想是尽量避免跨机房的调用。具体方案如下。
<li>
首先数据库的主库可以部署在一个机房中比如部署在A机房中那么A和B机房数据都会被写入到A机房中。然后在A、B两个机房中各部署一个从库通过主从复制的方式从主库中同步数据这样双机房的查询请求可以查询本机房的从库。一旦A机房发生故障可以通过主从切换的方式将B机房的从库提升为主库达到容灾的目的。
</li>
<li>
缓存也可以部署在两个机房中,查询请求也读取本机房的缓存,如果缓存中数据不存在,就穿透到本机房的从库中加载数据。数据的更新可以更新双机房中的数据,保证数据的一致性。
</li>
<li>
不同机房的RPC服务会向注册中心注册不同的服务组而不同机房的RPC客户端也就是Web服务只订阅同机房的RPC服务组这样就可以实现RPC调用尽量发生在本机房内避免跨机房的RPC调用。
</li>
<img src="https://static001.geekbang.org/resource/image/c7/86/c7a4a321ba02cf3ff8c65e9d5bb99686.jpg" alt="">
你的系统肯定会依赖公司内的其他服务,比如审核、搜索等服务,如果这些服务也是双机房部署的,那么也需要尽量保证只调用本机房的服务,降低调用的延迟。
使用了同城双活架构之后,可以实现机房级别的容灾,服务的部署也能够突破单一机房的限制。但是,还是会存在跨机房写数据的问题,不过由于写数据的请求量不高,所以在性能上是可以容忍的。
### 2.异地多活
上面这个方案足够应对你目前的需要,但是,你的业务是不断发展的,如果有朝一日,你的电商系统的流量达到了京东或者淘宝的级别,那么你就要考虑即使机房所在的城市发生重大的自然灾害,也要保证系统的可用性。**而这时,你需要采用异地多活的方案**(据我所知,阿里和饿了么采用的都是异地多活的方案)。
在考虑异地多活方案时,你首先要考虑异地机房的部署位置。它部署的不能太近,否则发生自然灾害时,很可能会波及。所以,如果你的主机房在北京,那么异地机房就尽量不要建设在天津,而是可以选择上海、广州这样距离较远的位置。但这就会造成更高的数据传输延迟,同城双活中,使用的跨机房写数据库的方案,就不合适了。
所以,在数据写入时,你要保证只写本机房的数据存储服务再采取数据同步的方案,将数据同步到异地机房中。**一般来说,数据同步的方案有两种:**
<li>
一种基于存储系统的主从复制比如MySQL和Redis。也就是在一个机房部署主库在异地机房部署从库两者同步主从复制实现数据的同步。
</li>
<li>
另一种是基于消息队列的方式。一个机房产生写入请求后,会写一条消息到消息队列,另一个机房的应用消费这条消息后再执行业务处理逻辑,写入到存储服务中。
</li>
**我建议你**采用两种同步相结合的方式比如你可以基于消息的方式同步缓存的数据、HBase数据等。然后基于存储主从复制同步MySQL、Redis等数据。
无论是采取哪种方案,数据从一个机房传输到另一个机房都会有延迟,所以,你需要尽量保证用户在读取自己的数据时,读取数据主库所在的机房。为了达到这一点,你需要对用户做分片,让一个用户每次的读写都尽量在同一个机房中。同时,在数据读取和服务调用时,也要尽量调用本机房的服务。**这里有一个场景:**假如在电商系统中用户A要查看所有订单的信息而这些订单中店铺的信息和卖家的信息很可能是存储在异地机房中那么你应该优先保证服务调用和数据读取在本机房中进行即使读取的是跨机房从库的数据会有一些延迟也是可以接受的。
<img src="https://static001.geekbang.org/resource/image/01/73/0138791e6164ea89380f262467820173.jpg" alt="">
## 课程小结
本节课,为了提升系统的可用性和稳定性,我带你探讨了多机房部署的难点以及同城双机房和异地多活的部署架构,**在这里,我想强调几个重点:**
<li>
不同机房的数据传输延迟是造成多机房部署困难的主要原因你需要知道同城多机房的延迟一般在1ms~3ms异地机房的延迟在50ms以下而跨国机房的延迟在200ms以下。
</li>
<li>
同城多机房方案可以允许有跨机房数据写入的发生,但是数据的读取和服务的调用应该尽量保证在同一个机房中。
</li>
<li>
异地多活方案则应该避免跨机房同步的数据写入和读取,而是采取异步的方式,将数据从一个机房同步到另一个机房。
</li>
多机房部署是一个业务发展到一定规模,对于机房容灾有需求时才会考虑的方案,能不做则尽量不要做。一旦你的团队决定做多机房部署,那么同城双活已经能够满足你的需求了,这个方案相比异地多活要简单很多。而在业界,很少有公司能够搭建一套真正的异步多活架构,这是因为这套架构在实现时过于复杂,**所以,轻易不要尝试。**
总之,架构需要依据系统的量级和对可用性、性能、扩展性的要求,不断演进和调整,盲目地追求架构的“先进性”只能造成方案的复杂,增加运维成本,从而给你的系统维护带来不便。
## 一课一思
在实际项目中,你在遇到怎样量级的情况下,才会考虑使用多机房部署的方案呢?在实施的过程中踩到了哪些坑呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,144 @@
<audio id="audio" title="29 | Service Mesh如何屏蔽服务化系统的服务治理细节" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7e/02/7e78b53b2c44e886e5415fd57c337d02.mp3"></audio>
你好,我是唐扬。
在分布式服务篇的前几节课程中,我带你了解了在微服务化过程中,要使用哪些中间件解决服务之间通信和服务治理的问题,其中就包括:
- 用RPC框架解决服务通信的问题
- 用注册中心解决服务注册和发现的问题;
- 使用分布式Trace中间件排查跨服务调用慢请求
- 使用负载均衡服务器,解决服务扩展性的问题;
- 在API网关中植入服务熔断、降级和流控等服务治理的策略。
经历了这几环之后你的垂直电商系统基本上已经完成了微服务化拆分的改造。不过目前来看你的系统使用的语言还是以Java为主之前提到的服务治理的策略和服务之间通信协议也是使用Java语言来实现的。
**那么这会存在一个问题:**一旦你的团队中有若干个小团队开始尝试使用Go或者PHP来开发新的微服务那么在微服务化过程中一定会受到挑战。
## 跨语言体系带来的挑战
其实一个公司的不同团队使用不同的开发语言是比较常见的。比如微博的主要开发语言是Java和PHP近几年也有一些使用Go开发的系统。而使用不同的语言开发出来的微服务**在相互调用时会存在两方面的挑战:**
一方面,服务之间的通信协议上,要对多语言友好,要想实现跨语言调用,关键点是选择合适的序列化方式。**我给你举一个例子。**
比如你用Java开发一个RPC服务使用的是Java原生的序列化方式。这种序列化方式对于其它语言并不友好那么你使用其它语言调用这个RPC服务时就很难解析序列化之后的二进制流。**所以我建议你,**在选择序列化协议时考虑序列化协议是否对多语言友好比如你可以选择Protobuf、Thrift这样一来跨语言服务调用的问题就可以很容易地解决了。
另一方面使用新语言开发的微服务无法使用之前积累的服务治理的策略。比如说RPC客户端在使用注册中心订阅服务的时候为了避免每次RPC调用都要与注册中心交互一般会在RPC客户端缓存节点的数据。如果注册中心中的服务节点发生了变更那么RPC客户端的节点缓存会得到通知并且变更缓存数据。
而且为了减少注册中心的访问压力我们在RPC客户端上一般会考虑使用多级缓存内存缓存和文件缓存来保证节点缓存的可用性。而这些策略在开始的时候都是使用Java语言来实现的并且封装在注册中心客户端里提供给RPC客户端使用。如果更换了新的语言这些逻辑就都要使用新的语言实现一套。
除此之外,负载均衡、熔断降级、流量控制、打印分布式追踪日志等等,这些服务治理的策略都需要重新实现,而使用其它语言重新实现这些策略无疑会带来巨大的工作量,也是中间件研发中一个很大的痛点。
那么,你要如何屏蔽服务化架构中服务治理的细节,或者说,**如何让服务治理的策略在多语言之间复用呢?**
可以考虑将服务治理的细节从RPC客户端中拆分出来形成一个代理层单独部署。这个代理层可以使用单一的语言实现所有的流量都经过代理层来使用其中的服务治理策略。这是一种“关注点分离”的实现方式**也是Service Mesh的核心思想。**
## Service Mesh是如何工作的
### 1. 什么是Service Mesh
Service Mesh主要处理服务之间的通信它的主要实现形式就是在应用程序同主机上部署一个代理程序。一般来讲我们将这个代理程序称为“Sidecar边车服务之间的通信也从之前的客户端和服务端直连变成了下面这种形式
<img src="https://static001.geekbang.org/resource/image/83/1b/833f4c8daea04104dfa5566715642c1b.jpg" alt="">
在这种形式下RPC客户端将数据包先发送给与自身同主机部署的Sidecar在Sidecar中经过服务发现、负载均衡、服务路由、流量控制之后再将数据发往指定服务节点的Sidecar在服务节点的Sidecar中经过记录访问日志、记录分布式追踪日志、限流之后再将数据发送给RPC服务端。
这种方式可以把业务代码和服务治理的策略隔离开将服务治理策略下沉让它成为独立的基础模块。这样一来不仅可以实现跨语言服务治理策略的复用还能对这些Sidecar做统一的管理。
**目前业界提及最多的Service Mesh方案当属[Istio](https://istio.io)** 它的玩法是这样的:
<img src="https://static001.geekbang.org/resource/image/60/64/604415b5d99ca176baf1c628d0677c64.jpg" alt="">
它将组件分为数据平面和控制平面数据平面就是我提到的SidecarIstio使用[Envoy](https://www.envoyproxy.io/)作为Sidecar的实现。控制平面主要负责服务治理策略的执行在Istio中主要分为Mixer、Pilot和Istio-auth三部分。
你可以先不了解每一部分的作用,只知道它们共同构成了服务治理体系就可以了。
然而在Istio中每次请求都需要经过控制平面也就是说每次请求都需要跨网络地调用Mixer这会极大地影响性能。
因此国内大厂开源出来的Service Mesh方案中一般只借鉴Istio的数据平面和控制平面的思路然后将服务治理策略做到了Sidecar中控制平面只负责策略的下发这样就不需要每次请求都经过控制平面性能上会改善很多。
### 2. 如何将流量转发到Sidecar中
在Service Mesh的实现中一个主要的问题是如何尽量无感知地引入Sidecar作为网络代理。也就是说无论是数据流入还是数据流出时都要将数据包重定向到Sidecar的端口上。实现思路一般有两种
第一种使用iptables的方式来实现流量透明的转发而Istio就默认了使用iptables来实现数据包的转发。为了能更清晰地说明流量转发的原理我们先简单地回顾一下什么是iptables。
Iptables是 Linux 内核中,防火墙软件 Netfilter 的管理工具它位于用户空间可以控制Netfilter实现地址转换的功能。在iptables中默认有五条链你可以把这五条链当作数据包流转过程中的五个步骤依次为PREROUTINGINPUTFORWARDOUTPUT和POSTROUTING。数据包传输的大体流程如下
<img src="https://static001.geekbang.org/resource/image/0e/8e/0e432f5623f7c1528341d2459b949a8e.jpg" alt="">
从图中可以看到数据包以PREROUTING链作为入口当数据包目的地为本机时它们也都会流经到OUTPUT链。所以我们可以在这两个链上增加一些规则将数据包重定向。我以Istio为例带你看看如何使用iptables实现流量转发。
在Istio中有一个叫做“istio-iptables.sh”的脚本这个脚本在Sidecar被初始化的时候执行主要是设置一些iptables规则。
我摘录了一些关键点来说明一下:
```
//流出流量处理
iptables -t nat -N ISTIO_REDIRECT //增加ISTIO_REDIRECT链处理流出流量
iptables -t nat -A ISTIO_REDIRECT -p tcp -j REDIRECT --to-port &quot;${PROXY_PORT}&quot; // 重定向流量到Sidecar的端口上
iptables -t nat -N ISTIO_OUTPUT // 增加ISTIO_OUTPUT链处理流出流量
iptables -t nat -A OUTPUT -p tcp -j ISTIO_OUTPUT// 将OUTPUT链的流量重定向到ISTIO_OUTPUT链上
for uid in ${PROXY_UID}; do
iptables -t nat -A ISTIO_OUTPUT -m owner --uid-owner &quot;${uid}&quot; -j RETURN //Sidecar本身的流量不转发
done
for gid in ${PROXY_GID}; do
iptables -t nat -A ISTIO_OUTPUT -m owner --gid-owner &quot;${gid}&quot; -j RETURN //Sidecar本身的流量不转发
done
iptables -t nat -A ISTIO_OUTPUT -j ISTIO_REDIRECT //将ISTIO_OUTPUT链的流量转发到ISTIO_REDIRECT
//流入流量处理
iptables -t nat -N ISTIO_IN_REDIRECT //增加ISTIO_IN_REDIRECT链处理流入流量
iptables -t nat -A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-port &quot;${PROXY_PORT}&quot; // 将流入流量重定向到Sidecar端口
iptables -t ${table} -N ISTIO_INBOUND //增加ISTIO_INBOUND链处理流入流量
iptables -t ${table} -A PREROUTING -p tcp -j ISTIO_INBOUND //将PREROUTING的流量重定向到ISTIO_INBOUND链
iptables -t nat -A ISTIO_INBOUND -p tcp --dport &quot;${port}&quot; -j ISTIO_IN_REDIRECT //将ISTIO_INBOUND链上指定目的端口的流量重定向到ISTIO_IN_REDIRECT链
```
假设服务的节点部署在9080端口上Sidecar开发的端口是15001那么流入流量的流向如下
<img src="https://static001.geekbang.org/resource/image/01/24/014a530acbcac3f8b57635627a22e924.jpg" alt="">
流出流量的流量图如下:
<img src="https://static001.geekbang.org/resource/image/43/55/43ee298a3f01c0de5d3ee0c5c96ea455.jpg" alt="">
**Iptables方式的优势在于对业务完全透明**业务甚至不知道有Sidecar存在这样会减少业务接入的时间。不过它也有缺陷那就是它是在高并发下性能上会有损耗因此国内大厂采用了另外一种方式轻量级客户端。
在这种方式下RPC客户端会通过配置的方式知道Sidecar的部署端口然后通过一个轻量级客户端将调用服务的请求发送给Sidecar。Sidecar在转发请求之前先执行一些服务治理的策略比如从注册中心中查询到服务节点信息并且缓存起来然后从服务节点中使用某种负载均衡的策略选出一个节点等等。
请求被发送到服务端的Sidecar上后然后在服务端记录访问日志和分布式追踪日志再把请求转发到真正的服务节点上。当然服务节点在启动时会委托服务端Sidecar向注册中心注册节点Sidecar也就知道了真正服务节点部署的端口是多少。整个请求过程如图所示
<img src="https://static001.geekbang.org/resource/image/ea/1d/ea37c1e8374d2c67b3348b566fb8921d.jpg" alt="">
当然除了iptables和轻量级客户端两种方式外目前在探索的方案还有[Cilium](https://github.com/cilium/cilium)。这个方案可以从Socket层面实现请求的转发也就可以避免iptables方式在性能上的损耗。**在这几种方案中,我建议你使用轻量级客户端的方式,**这样虽然会有一些改造成本但是却在实现上最简单可以快速让Service Mesh在你的项目中落地。
当然无论采用哪种方式你都可以实现将Sidecar部署到客户端和服务端的调用链路上让它代理进出流量。这样你就可以使用运行在Sidecar中的服务治理的策略了。至于这些策略我在前面的课程中都带你了解过你可以回顾23至26讲的课程这里就不再赘述了。
与此同时我也建议你了解目前业界一些开源的Service Mesh框架这样在选择方案时可以多一些选择。目前在开源领域比较成熟的Service Mesh框架有下面几个你可以通过阅读它们的文档来深入了解作为本节课的引申阅读。
<li>
[Istio](https://istio.io/) 这个框架在业界最为著名它提出了数据平面和控制平面的概念是Service Mesh的先驱缺陷就是刚才提到的Mixer的性能问题。
</li>
<li>
[Linkerd](https://linkerd.io/) 是第一代的Service Mesh使用Scala语言编写其劣势就是内存的占用。
</li>
<li>
[SOFAMesh](https://github.com/sofastack/sofa-mesh) 是蚂蚁金服开源的Service Mesh组件在蚂蚁金服已经有大规模落地的经验。
</li>
## 课程小结
本节课为了解决跨语言场景下服务治理策略的复用问题我带你了解了什么是Service Mesh以及如何在实际项目中落地你需要的重点内容如下
1.Service Mesh分为数据平面和控制平面。数据平面主要负责数据的传输控制平面用来控制服务治理策略的植入。出于性能的考虑一般会把服务治理策略植入到数据平面中控制平面负责服务治理策略数据的下发。
2.Sidecar的植入方式目前主要有两种实现方式一种是使用iptables实现流量的劫持另一种是通过轻量级客户端来实现流量转发。
目前在一些大厂中比如微博、蚂蚁金服Service Mesh已经开始在实际项目中大量的落地实践而我建议你持续关注这项技术。它本身是一种将业务与通信基础设施分离的技术如果你的业务上遇到多语言环境下服务治理的困境如果你的遗留服务需要快速植入服务治理策略如果你想要将你在服务治理方面积累的经验快速地与其他团队共享那么Service Mesh就是一个不错的选择。
## 一课一思
你在实际的工作中是否使用过Service Mesh解决过跨语言的服务治理的问题呢在使用的过程中是否踩到过坑呢欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。