21 | 系统架构:每秒1万次请求的系统要做服务化拆分吗?
通过前面几个篇章的内容,你已经从数据库、缓存和消息队列的角度对自己的垂直电商系统
在性能、可用性和扩展性上做了优化。
现在,你的系统运行稳定,好评不断,每天高峰期的流量,已经达到了 10000/s 请求,
DAU 也涨到了几十万。CEO 非常高兴,打算继续完善产品功能,以便进行新一轮的运营推
广,争取在下个双十一可以将 DAU 冲击过百万。 这时,你开始考虑,怎么通过技术上的
优化改造,来支撑更高的并发流量,比如支撑过百万的 DAU。
于是,你重新审视了自己的系统架构,分析系统中有哪些可以优化的点。

目前来看,工程的部署方式还是采用一体化架构,也就是说所有的功能模块,比方说电商系
统中的订单模块、用户模块、支付模块、物流模块等等,都被打包到一个大的 Web 工程
中,然后部署在应用服务器上。
你隐约觉得这样的部署方式可能存在问题,于是,你 Google 了一下,发现当系统发展到
一定阶段,都要做微服务化的拆分,你也看到淘宝的“五彩石”项目,对于淘宝整体架构的
扩展性,带来的巨大影响。这一切让你心驰神往。
但是有一个问题一直萦绕在你的心里:究竟是什么促使我们将一体化架构,拆分成微服务化
架构?是不是说系统的整体 QPS 到了 1 万,或者到了 2 万,就一定要做微服务化拆分呢?
一体化架构的痛点
先来回想一下,你当初为什么选用了一体化架构。
在电商项目刚刚启动的时候,你只是希望能够尽量快地将项目搭建起来,方便将产品更早地
投放市场,快速完成验证。
在系统开发的初期,这种架构确实给你的开发运维,带来了很大的便捷,主要体现在:
开发简单直接,代码和项目集中式管理;只需要维护一个工程,节省维护系统运行的人力成本;
排查问题的时候,只需要排查这个应用进程就可以了,目标性强。
但随着功能越来越复杂,开发团队规模越来越大,你慢慢感受到了一体化架构的一些缺陷,
这主要体现在以下几个方面。
首先,在技术层面上,数据库连接数可能成为系统的瓶颈。
在第 7 讲中我提到,数据库的连接是比较重的一类资源,不仅连接过程比较耗时,而且
连接 MySQL 的客户端数量有限制,最多可以设置为 16384(在实际的项目中,可以依据
实际业务来调整)。
这个数字看着很大,但是因为你的系统是按照一体化架构部署的,在部署结构上没有分层,
应用服务器直接连接数据库,那么当前端请求量增加,部署的应用服务器扩容,数据库的连
接数也会大增,给你举个例子。
我之前维护的一个系统中,数据库的最大连接数设置为 8000,应用服务器部署在虚拟机
上,数量大概是 50 个,每个服务器会和数据库建立 30 个连接,但是数据库的连接数,却
远远大于 30 * 50 = 1500。
因为你不仅要支撑来自客户端的外网流量,还要部署单独的应用服务,支撑来自其它部门的
内网调用,也要部署队列处理机,处理来自消息队列的消息,这些服务也都是与数据库直接
连接的,林林总总加起来,在高峰期的时候,数据库的连接数要接近 3400。
所以,一旦遇到一些大的运营推广活动,服务器就要扩容,数据库连接数也随之增加,基本
上就会处在最大连接数的边缘。这就像一颗定时炸弹,随时都会影响服务的稳定。
第二点,一体化架构增加了研发的成本,抑制了研发效率的提升。
《人月神话》中曾经提到:一个团队内部沟通成本,和人员数量 n 有关,约等于 n(n-
1)/2,也就是说随着团队人员的增加,沟通的成本呈指数级增长,一个 100 人的团队,
需要沟通的渠道大概是 100(100-1)/2 = 4950。那么为了减少沟通成本,我们一般会
把团队拆分成若干个小团队,每个小团队 5~7 人,负责一部分功能模块的开发和维护。比方说,你的垂直电商系统团队就会被拆分为用户组、订单组、支付组、商品组等等。当如
此多的小团队共同维护一套代码,和一个系统时,在配合时就会出现问题。
不同的团队之间沟通少,假如一个团队需要一个发送短信的功能,那么有的研发同学会认为
最快的方式,不是询问其他团队是否有现成的,而是自己写一套,但是这种想法是不合适
的,这样一来就会造成功能服务的重复开发。
由于代码部署在一起,每个人都向同一个代码库提交代码,代码冲突无法避免;同时,功能
之间耦合严重,可能你只是更改了很小的逻辑,却导致其它功能不可用,从而在测试时需要
对整体功能回归,延长了交付时间。
模块之间互相依赖,一个小团队中的成员犯了一个错误,就可能会影响到,其它团队维护的
服务,对于整体系统稳定性影响很大。
第三点,一体化架构对于系统的运维也会有很大的影响。
想象一下,在项目初期,你的代码可能只有几千行,构建一次只需要一分钟,那么你可以很
敏捷灵活地频繁上线变更修复问题。但是当你的系统扩充到几十万行,甚至上百万行代码的
时候,一次构建的过程,包括编译、单元测试、打包和上传到正式环境,花费的时间可能达
到十几分钟,并且,任何小的修改,都需要构建整个项目,上线变更的过程非常不灵活。
而我说的这些问题,都可以通过微服务化拆分来解决。
如何使用微服务化解决这些痛点
之前,我在做一个社区业务的时候,开始采用的架构也是一体化的架构,数据库已经做了垂
直分库,分出了用户库、内容库和互动库,并且已经将工程拆分了业务池,拆分成了用户
池、内容池和互动池。
当前端的请求量越来越大时,我们发现,无论哪个业务池子,用户模块都是请求量最大的模
块儿,用户库也是请求量最大的数据库。这很好理解,无论是内容还是互动,都会查询用户
库获取用户数据,所以,即使我们做了业务池的拆分,但实际上,每一个业务池子都需要连
接用户库,并且请求量都很大,这就造成了用户库的连接数比其它都要多一些,容易成为系
统的瓶颈。

那么我们怎么解决这个问题呢?
其实,可以把与用户相关的逻辑,部署成一个单独的服务,其它无论是用户池、内容池还是
互动池,都连接这个服务来获取和更改用户信息,那么也就是说,只有这个服务可以连接用
户库,其它的业务池都不直连用户库获取数据。
由于这个服务只处理和用户相关的逻辑,所以,不需要部署太多的实例就可以承担流量,这
样就可以有效地控制用户库的连接数,提升了系统的可扩展性。那么如此一来,我们也可以
将内容和互动相关的逻辑,都独立出来,形成内容服务和互动服务,这样,我们就通过按照
业务做横向拆分的方式,解决了数据库层面的扩展性问题。

再比如,我们在做社区业务的时候,会有多个模块需要使用地理位置服务,将 IP 信息或者
经纬度信息,转换为城市信息。比如,推荐内容的时候,可以结合用户的城市信息,做附近
内容的推荐;展示内容信息的时候,也需要展示城市信息等等。
那么,如果每一个模块都实现这么一套逻辑就会导致代码不够重用。因此,我们可以把将
IP 信息或者经纬度信息,转换为城市信息,包装成单独的服务供其它模块调用,也就是,
我们可以将与业务无关的公用服务抽取出来,下沉成单独的服务。
按照以上两种拆分方式将系统拆分之后,每一个服务的功能内聚,维护人员职责明确,增加
了新的功能只需要测试自己的服务就可以了,而一旦服务出了问题,也可以通过服务熔断、
降级的方式减少对于其他服务的影响(我会在第 34 讲中系统地讲解)。
另外,由于每个服务都只是原有系统的子集,代码行数相比原有系统要小很多,构建速度上
也会有比较大的提升。
当然,微服务化之后,原有单一系统被拆分成多个子服务,无论在开发,还是运维上都会引
入额外的问题,那么这些问题是什么? 我们将如何解决呢?下一节课,我会带你来了解。
课程小结
本节课,我主要带你了解了,实际业务中会基于什么样的考虑,对系统做微服务化拆分,其
实,系统的 QPS 并不是决定性的因素。影响的因素,我归纳为以下几点:
系统中,使用的资源出现扩展性问题,尤其是数据库的连接数出现瓶颈;
大团队共同维护一套代码,带来研发效率的降低,和研发成本的提升;
系统部署成本越来越高。
从中你应该有所感悟:在架构演进的初期和中期,性能、可用性、可扩展性是我们追求的主
要目标,高性能和高可用给用户带来更好的使用体验,扩展性可以方便我们支撑更大量级的
并发。但是当系统做的越来越大,团队成员越来越多,我们就不得不考虑成本了。
这里面的“成本”有着复杂的含义,它不仅代表购买服务器的费用,还包括研发团队,内部
的开发成本,沟通成本以及运维成本等等,甚至有些时候,成本会成为架构设计中的决定性
因素。比方说,你做一个直播系统,在架构设计时除了要关注起播速度,还需要关注 CDN 成本;
再比如作为团队 Leader,你在日常开发中除了要推进正常的功能需求开发,也要考虑完善
工具链建设,提高工程师的研发效率,降低研发成本。
这很好理解,如果在一个 100 个人的团队中,你的工具为每个人每天节省了 10 分钟,那
么加起来就是接近 17 小时,差不多增加了 2 个人工作时间。而正是基于提升扩展性和降低
成本的考虑,我们最终走上了微服务化的道路。
22 | 微服务架构:微服务化后,系统架构要如何改造?
上一节课,我带你了解了,单体架构向微服务化架构演进的原因,你应该了解到,当系统依
赖资源的扩展性出现问题,或者是一体化架构带来的研发成本、部署成本变得难以接受时,
我们会考虑对整体系统,做微服务化拆分。
微服务化之后,垂直电商系统的架构会将变成下面这样:

在这个架构中,我们将用户、订单和商品相关的逻辑,抽取成服务独立的部署,原本的
Web 工程和队列处理程序,将不再直接依赖缓存和数据库,而是通过调用服务接口,查询
存储中的信息。
有了构思和期望之后,为了将服务化拆分尽快落地,你们决定抽调主力研发同学,共同制定
拆分计划。但是细致讨论后发现,虽然对服务拆分有了大致的方向,可还是有很多疑问,比
如:
服务拆分时要遵循哪些原则?
服务的边界如何确定?服务的粒度是怎样呢?
在服务化之后,会遇到哪些问题呢?我们又将如何来解决?
当然,你也许想知道,微服务拆分的具体操作过程和步骤是怎样的,但是这部分内容涉及的
知识点比较多,不太可能在一次课程中,把全部内容涵盖到。而且《DDD 实战课》中,已
经侧重讲解了微服务化拆分的具体过程,你可以借鉴。上面这三点内容,会影响服务化拆分的效果,但在实际的项目中,经常被大部分人忽略,所
以是我们本节课的重点内容。而我希望你能把本节课的内容和自身的业务结合起来体会,思
考业务服务化拆分的方式和方法。
微服务拆分的原则
之前,你维护的一体化架构,就像是一个大的蜘蛛网,不同功能模块,错综复杂地交织在一
起,方法之间调用关系非常的复杂,导致你修复了一个 Bug,可能会引起另外多个 Bug,
整体的维护成本非常高。同时,数据库较弱的扩展性,也限制了服务的扩展能力
出于上述考虑,你要对架构做拆分。但拆分并不像听上去那么简单,这其实就是将整体工
程,重构甚至重写的过程。你需要将代码,拆分到若干个子工程里面,再将这些子工程,通
过一些通信方式组装起来,这对架构是很大的调整,需要跨多个团队协调完成。
所以在开始拆分之前,你需要明确几个拆分的原则,否则就会事倍功半,甚至对整体项目产
生不利的影响。
原则一,做到单一服务内部功能的高内聚,和低耦合。也就是说,每个服务只完成自己职责
之内的任务,对于不是自己职责的功能,交给其它服务来完成。说起来你可能觉得理所当
然,对这一点不屑一顾,但很多人在实际开发中,经常会出现一些问题。
比如,我之前的项目中, 有用户服务和内容服务,用户信息中有“是否为认证用户”字
段。组内有个同学在内容服务里有这么一段逻辑:如果用户认证字段等于 1,代表是认证用
户,那么就把内容权重提升。问题是,判断用户是否为认证用户的逻辑,应该内聚在用户服
务内部,而不应该由内容服务判断,否则认证的逻辑一旦变更,内容服务也需要一同跟着变
更,这就不满足高内聚、低耦合的要求了。所幸,我们在 Review 代码时,及时发现了这个
问题,并在服务上线之前修复了它。
原则二,你需要关注服务拆分的粒度,先粗略拆分,再逐渐细化。在服务拆分的初期,你其
实很难确定,服务究竟要拆分成什么样。但是,从“微服务”这几个字来看,服务的粒度貌
似应该足够小,甚至有“一方法一服务”的说法。不过,服务多了也会带来问题,像是服务
个数的增加会增加运维的成本。再比如,原本一次请求只需要调用进程内的多个方法,现在
则需要跨网络调用多个 RPC 服务,在性能上肯定会有所下降。所以我推荐的做法是:拆分初期可以把服务粒度拆的粗一些,后面随着团队对于业务和微服
务理解的加深,再考虑把服务粒度细化。比如说,对于一个社区系统来说,你可以先把和用
户关系相关的业务逻辑,都拆分到用户关系服务中,之后,再把比如黑名单的逻辑独立成黑
名单服务。
原则三,拆分的过程,要尽量避免影响产品的日常功能迭代,也就是说,要一边做产品功能
迭代,一边完成服务化拆分。
还是拿我之前维护的一个项目举例。我曾经在竞品对手快速发展的时期做了服务的拆分,拆
分的方式是停掉所有业务开发,全盘推翻重构,结果错失了产品发展的最佳机会,最终败给
了竞争对手。因此,我们的拆分只能在现有一体化系统的基础上,不断剥离业务独立部署,
剥离的顺序,你可以参考以下几点:
1. 优先剥离比较独立的边界服务(比如短信服务、地理位置服务),从非核心的服务出
发,减少拆分对现有业务的影响,也给团队一个练习、试错的机会;
2. 当两个服务存在依赖关系时,优先拆分被依赖的服务。比方说,内容服务依赖于用户服
务获取用户的基本信息,那么如果先把内容服务拆分出来,内容服务就会依赖于一体化架构
中的用户模块,这样还是无法保证内容服务的快速部署能力。
所以正确的做法是,你要理清服务之间的调用关系,比如说,内容服务会依赖用户服务获取
用户信息,互动服务会依赖内容服务,所以要按照先用户服务,再内容服务,最后互动服务
的顺序来进行拆分。
原则四,服务接口的定义要具备可扩展性。服务拆分之后,由于服务是以独立进程的方式部
署,所以服务之间通信,就不再是进程内部的方法调用,而是跨进程的网络通信了。在这种
通信模型下需要注意,服务接口的定义要具备可扩展性,否则在服务变更时,会造成意想不
到的错误。
在之前的项目中,某一个微服务的接口有三个参数,在一次业务需求开发中,组内的一个同
学将这个接口的参数调整为了四个,接口被调用的地方也做了修改,结果上线这个服务后,
却不断报错,无奈只能回滚。想必你明白了,这是因为这个接口先上线后,参数变更成了四个,但是调用方还未变更,还
是在调用三个参数的接口,那就肯定会报错了。所以,服务接口的参数类型最好是封装类,
这样如果增加参数,就不必变更接口的签名,而只需要在类中添加字段即就可以了。
微服务化带来的问题和解决思路
那么,依据这些原则,将系统做微服务拆分之后,是不是就可以一劳永逸,解决所有问题了
呢?当然不是。
微服务化只是一种架构手段,有效拆分后,可以帮助实现服务的敏捷开发和部署。但是,由
于将原本一体化架构的应用,拆分成了,多个通过网络通信的分布式服务,为了在分布式环
境下,协调多个服务正常运行,就必然引入一定的复杂度,这些复杂度主要体现在以下几个
方面:
1. 服务接口的调用,不再是同一进程内的方法调用,而是跨进程的网络调用,这会增加接
口响应时间的增加。此时,我们就要选择高效的服务调用框架,同时,接口调用方需要知道
服务部署在哪些机器的哪个端口上,这些信息需要存储在一个分布式一致性的存储中,于是
就需要引入服务注册中心,这一点,是我在 24 讲会提到的内容。不过在这里我想强调的
是,注册中心管理的是服务完整的生命周期,包括对于服务存活状态的检测。
2. 多个服务之间有着错综复杂的依赖关系。一个服务会依赖多个其它服务,也会被多个服
务所依赖,那么一旦被依赖的服务的性能出现问题,产生大量的慢请求,就会导致依赖服务
的工作线程池中的线程被占满,那么依赖的服务也会出现性能问题。接下来,问题就会沿着
依赖网,逐步向上蔓延,直到整个系统出现故障为止。
为了避免这种情况的发生,我们需要引入服务治理体系,针对出问题的服务,采用熔断、降
级、限流、超时控制的方法,使得问题被限制在单一服务中,保护服务网络中的其它服务不
受影响。
3. 服务拆分到多个进程后,一条请求的调用链路上,涉及多个服务,那么一旦这个请求的
响应时间增长,或者是出现错误,我们就很难知道,是哪一个服务出现的问题。另外,整体
系统一旦出现故障,很可能外在的表现是所有服务在同一时间都出现了问题,你在问题定位
时,很难确认哪一个服务是源头,这就需要引入分布式追踪工具,以及更细致的服务端监控
报表。我在 25 讲和 30 讲会详细的剖析这个内容,在这里我想强调的是,监控报表关注的是,依
赖服务和资源的宏观性能表现;分布式追踪关注的是,单一慢请求中的性能瓶颈分析,两者
需要结合起来帮助你来排查问题。
以上这些微服务化后,在开发方面引入的问题,就是接下来,“分布式服务篇”和“维护
篇”的主要讨论内容。
总的来说,微服务化是一个很大的话题,在微服务开发和维护时,你也许会在很短时间就把
微服务拆分完成,但是你可能会花相当长的时间来完善服务治理体系。接下来的内容,会涉
及一些常用微服务中间件的原理,和使用方式,你可以使用以下的方式更好地理解后面的内
容:
快速完成中间件的部署运行,建立对它感性的认识;
阅读它的文档中,基本原理和架构设计部分;
必要时,阅读它的源码,加深对它的理解,这样可以帮助你在维护你的微服务时,排查
中间件引起的故障和解决性能问题。
课程小结
本节课,为了能够指导你更好地进行服务化的拆分,我带你了解了,微服务化拆分的原则,
内容比较清晰。在这里,我想延伸一些内容:
1.“康威定律”提到,设计系统的组织,其产生的设计等同于组织间的沟通结构。通俗一点
说,就是你的团队组织结构是什么样的,你的架构就会长成什么样。
如果你的团队分为服务端开发团队,DBA 团队,运维团队,测试团队,那么你的架构就是
一体化的,所有的团队共同为一个大系统负责,团队内成员众多,沟通成本就会很高;而如
果你想实现微服务化的架构,那么你的团队也要按照业务边界拆分,每一个模块由一个自治
的小团队负责,这个小团队里面有开发、测试、运维和 DBA,这样沟通就只发生在这个小
团队内部,沟通的成本就会明显降低。
2. 微服务化的一个目标是减少研发的成本,其中也包括沟通的成本,所以小团队内部成员
不宜过多。按照亚马逊 CEO,贝佐斯的“两个披萨”的理论,如果两个披萨不够你的团队吃,那么你
的团队就太大了,需要拆分,所以一个小团队包括开发、运维、测试以 6~8 个人为最佳;
3. 如果你的团队人数不多,还没有做好微服务化的准备,而你又感觉到研发和部署的成本
确实比较高,那么一个折中的方案是,你可以优先做工程的拆分。
比如说,如果你使用的是 Java 语言,你可以依据业务的边界,将代码拆分到不同的子工程
中,然后子工程之间以 jar 包的方式依赖,这样每个子工程代码量减少,可以减少打包时
间;并且子工程代码内部,可以做到高内聚低耦合,一定程度上减少研发的成本,也不失为
一个不错的保守策略。
23 | RPC框架:10万QPS下如何实现毫秒级的服务调用?
在21 讲和22 讲中,你的团队已经决定对垂直电商系统做服务化拆分,以便解决扩展
性和研发成本高的问题。与此同时,你们在不断学习的过程中还发现,系统做了服务化拆分
之后,会引入一些新的问题,这些问题我在上节课提到过,归纳起来主要是两点:
服务拆分单独部署后,引入的服务跨网络通信的问题;
在拆分成多个小服务之后,服务如何治理的问题。
如果想要解决这两方面问题,你需要了解,微服务化所需要的中间件的基本原理,和使用技
巧,那么本节课,我会带你掌握,解决第一点问题的核心组件:RPC 框架。来思考这样一个场景:你的垂直电商系统的 QPS 已经达到了每秒 2 万次,在做了服务化拆
分之后,由于我们把业务逻辑,都拆分到了单独部署的服务中,那么假设你在完成一次完整
的请求时,需要调用 4~5 次服务,计算下来,RPC 服务需要承载大概每秒 10 万次的请
求。那么,你该如何设计 RPC 框架,来承载如此大的请求量呢?你要做的是:
选择合适的网络模型,有针对性地调整网络参数,以优化网络传输性能;
选择合适的序列化方式,以提升封包、解包的性能。
接下来,我从原理出发,让你对于 RPC 有一个理性的认识,这样你在设计 RPC 框架时,
就可以清晰地知道自己的设计目标是什么了。
你所知道的 RPC
说到 RPC(Remote Procedure Call,远程过程调用),你不会陌生,它指的是通过网
络,调用另一台计算机上部署服务的技术。
而 RPC 框架就封装了网络调用的细节,让你像调用本地服务一样,调用远程部署的服务。
你也许觉得只有像 Dubbo、Grpc、Thrift 这些新兴的框架才算是 RPC 框架,其实严格来
说,你很早之前就接触到与 RPC 相关的技术了。
比如,Java 原生就有一套远程调用框架叫做 RMI(Remote Method Invocation), 它
可以让 Java 程序通过网络,调用另一台机器上的 Java 对象的方法。它是一种远程调用的
方法,也是 J2EE 时代大名鼎鼎的 EJB 的实现基础。
时至今日,你仍然可以通过 Spring 的“RmiServiceExporter”将 Spring 管理的 bean 暴
露成一个 RMI 的服务,从而继续使用 RMI 来实现跨进程的方法调用。之所以 RMI 没有像
Dubbo,Grpc 一样大火,是因为它存在着一些缺陷:
RMI 使用专为 Java 远程对象定制的协议 JRMP(Java Remote Messaging Protocol)
进行通信,这限制了它的通信双方,只能是 Java 语言的程序,无法实现跨语言通信;
RMI 使用 Java 原生的对象序列化方式,生成的字节数组空间较大,效率很差。
另一个你可能听过的技术是 Web Service,它也可以认为是 RPC 的一种实现方式。它的
优势是,使用 HTTP+SOAP 协议,保证了调用可以跨语言,跨平台。只要你支持 HTTP 协议,可以解析 XML,那么就能够使用 Web Service。在我来看,它由于使用 XML 封装数
据,数据包大,性能还是比较差。
借上面几个例子,我主要是想告诉你,RPC 并不是互联网时代的产物,也不是服务化之后
才衍生出来的技术,而是一种规范,只要是封装了网络调用的细节,能够实现远程调用其他
服务,就可以算作是一种 RPC 技术了。
那么你的垂直电商项目在使用 RPC 框架之后,会产生什么变化呢?
在我来看,在性能上的变化是不可忽视的,我给你举个例子。 比方说,你的电商系统中,
商品详情页面需要商品数据、评论数据还有店铺数据,如果在一体化的架构中,你只需要从
商品库,评论库和店铺库获取数据就可以了,不考虑缓存的情况下有三次网络请求。
但是,如果独立出商品服务、评论服务和店铺服务之后,那么就需要分别调用这三个服务,
而这三个服务又会分别调用各自的数据库,这就是六次网络请求。如果你服务拆分的更细粒
度,那么多出的网络调用就会越多,请求的延迟就会更长,而这就是你为了提升系统的扩展
性,在性能上所付出的代价。

那么,我们要如果优化 RPC 的性能,从而尽量减少网络调用,对于性能的影响呢?在这
里,你首先需要了解一次 RPC 的调用都经过了哪些步骤,因为这样,你才可以针对这些步
骤中可能存在的性能瓶颈点提出优化方案。
步骤如下:
在一次 RPC 调用过程中,客户端首先会将调用的类名、方法名、参数名、参数值等信
息,序列化成二进制流;
然后客户端将二进制流,通过网络发送给服务端;服务端接收到二进制流之后,将它反序列化,得到需要调用的类名、方法名、参数名和
参数值,再通过动态代理的方式,调用对应的方法得到返回值;
服务端将返回值序列化,再通过网络发送给客户端;
客户端对结果反序列化之后,就可以得到调用的结果了。

从这张图中你可以看到,有网络传输的过程,也有将请求序列化和反序列化的过程, 所
以,如果要提升 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 过程比喻成烧水倒水的过程,等待资源(就是烧水的过程),使用资源(就
是倒水的过程):
如果你站在炤台边上一直等着(等待资源)水烧开,然后倒水(使用资源),那么就是
同步阻塞 I/O;
如果你偷点儿懒,在烧水的时候躺在沙发上看会儿电视(不再时时刻刻等待资源),但
是还是要时不时的去看看水开了没有,一旦水开了,马上去倒水(使用资源),那么这
就是同步非阻塞 I/O;
如果你想要洗澡,需要同时烧好多壶水,那你就在看电视的间隙去看看哪壶水开了(等
待多个资源),哪一壶开了就先倒哪一壶,这样就加快了烧水的速度,这就是同步多路
I/O 复用;
不过你发现自己总是跑厨房去看水开了没,太累了,于是你考虑给你的水壶加一个报警
器(信号),只要水开了就马上去倒水,这就是信号驱动 I/O;最后一种就高级了,你发明了一个智能水壶,在水烧好后自动就可以把水倒好,这就是
异步 I/O。
这五种 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 算法,他期望:
如果是连续的小数据包,大小没有一个 MSS(Maximum Segment
Size,最大分段大小),并且还没有收到之前发送的数据包的 Ack 信息,那么这些小数
据包就会在发送端暂存起来,直到小数据包累积到一个 MSS,或者收到一个 Ack 为止。
这原本是为了减少不必要的网络传输,但是如果接收端开启了 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 都是需要引入 IDL(Interface description language)的,也
就是需要按照约定的语法写一个 IDL 文件,然后通过特定的编译器将它转换成各语言对应
的代码,从而实现跨语言的特点。Thrift 是 Facebook 开源的高性能的序列化协议,也是一个轻量级的 RPC 框架;
Protobuf 是谷歌开源的序列化协议。它们的共同特点是,无论在空间上还是时间上都有着
很高的性能,缺点就是由于 IDL 存在带来一些使用上的不方便。
那么,你要如何选择这几种序列化协议呢?这里我给你几点建议:
如果对于性能要求不高,在传输数据占用带宽不大的场景下,可以使用 JSON 作为序列
化协议;
如果对于性能要求比较高,那么使用 Thrift 或者 Protobuf 都可以。而 Thrift 提供了配
套的 RPC 框架,所以想要一体化的解决方案,你可以优先考虑 Thrift;
在一些存储的场景下,比如说你的缓存中存储的数据占用空间较大,那么你可以考虑使
用 Protobuf 替换 JSON,作为存储数据的序列化方式。
课程小结
为了优化 RPC 框架的性能,本节课,我带你了解了网络 I/O 模型和序列化方式的选择,它
们是实现高并发 RPC 框架的要素,总结起来有三个要点:
1. 选择高性能的 I/O 模型,这里我推荐使用同步多路 I/O 复用模型;
2. 调试网络参数,这里面有一些经验值的推荐。比如将 tcp_nodelay 设置为 true,也有一
些参数需要在运行中来调试,比如接受缓冲区和发送缓冲区的大小,客户端连接请求缓冲队
列的大小(back log)等等;
3. 序列化协议依据具体业务来选择。如果对性能要求不高,可以选择 JSON,否则可以从
Thrift 和 Protobuf 中选择其一。
在学习本节课的过程中,我建议你阅读一下,成熟的 RPC 框架的源代码。比如,阿里开源
的 Dubbo,微博的 Motan 等等,理解它们的实现原理和细节,这样你会更有信心维护好
你的微服务系统;同时,你也可以从优秀的代码中,学习到代码设计的技巧,比如说
Dubbo 对于 RPC 的抽象,SPI 扩展点的设计,这样可以有助你提升代码能力。
当然了,本节课我不仅仅想让你了解 RPC 框架实现的一些原理,更想让你了解在做网络编
程时,需要考虑哪些关键点,这样你在设计此类型的系统时,就会有一些考虑的方向和思路
24 | 注册中心:分布式系统如何寻址?
上一节课,我带你了解了 RPC 框架实现中的一些关键的点,你通过 RPC 框架,能够解决
服务之间,跨网络通信的问题,这就完成了微服务化改造的基础。
但是在服务拆分之后,你需要维护更多的细粒度的服务,而你需要面对的第一个问题就是,
如何让 RPC 客户端知道服务端部署的地址,这就是我们今天要讲到的,服务注册与发现的
问题。
你所知道的服务发现服务注册和发现不是一个新的概念,你在之前的实际项目中也一定了解过,只是你可能没怎
么注意罢了。比如说,你知道 Nginx 是一个反向代理组件,那么 Nginx 需要知道,应用服
务器的地址是什么,这样才能够将流量透传到应用服务器上,这就是服务发现的过程。
那么 Nginx 是怎么实现的呢?它是把应用服务器的地址配置在了文件中。
这固然是一种解决的思路,实际上,我在早期的项目中也是这么做的。那时,项目刚刚做了
服务化拆分,RPC 服务端的地址,就是配置在了客户端的代码中,不过,这样做之后出现
了几个问题:
首先在紧急扩容的时候,就需要修改客户端配置后,重启所有的客户端进程,操作时间
比较长;
其次,一旦某一个服务器出现故障时,也需要修改所有客户端配置后重启,无法快速修
复,更无法做到自动恢复;
最后,RPC 服务端上线无法做到提前摘除流量,这样在重启服务端的时候,客户端发往
被重启服务端的请求还没有返回,会造成慢请求甚至请求失败。
因此,我们考虑使用注册中心来解决这些问题。
目前业界有很多可供你来选择的注册中心组件,比如说老派的 ZooKeeper,Kubernetes
使用的 ETCD,阿里的微服务注册中心 Nacos,Spring Cloud 的 Eureka 等等。
这些注册中心的基本功能有两点:
其一是提供了服务地址的存储;
其二是当存储内容发生变化时,可以将变更的内容推送给客户端。
第二个功能是我们使用注册中心的主要原因。因为无论是,当我们需要紧急扩容,还是在服
务器发生故障时,需要快速摘除节点,都不用重启服务器就可以实现了。使用了注册中心组
件之后,RPC 的通信过程就变成了下面这个样子:

从图中,你可以看到一个完整的,服务注册和发现的过程:
客户端会与注册中心建立连接,并且告诉注册中心,它对哪一组服务感兴趣;
服务端向注册中心注册服务后,注册中心会将最新的服务注册信息通知给客户端;
客户端拿到服务端的地址之后就可以向服务端发起调用请求了。
从这个过程中可以看出,有了注册中心之后,服务节点的增加和减少对于客户端就是透明
的。这样,除了可以实现不重启客户端,就能动态地变更服务节点以外,还可以实现优雅关
闭的功能。
优雅关闭是你在系统研发过程中,必须要考虑的问题。因为如果暴力地停止服务,那么已经
发送给服务端的请求,来不及处理服务就被杀掉了,就会造成这部分请求失败,服务就会有
波动。所以,服务在退出的时候,都需要先停掉流量,再停止服务,这样服务的关闭才会更
平滑,比如说,消息队列处理器就是要将所有,已经从消息队列中读出的消息,处理完之后
才能退出。
对于 RPC 服务来说,我们可以先将 RPC 服务从注册中心的服务列表中删除掉,然后观察
RPC 服务端没有流量之后,再将服务端停掉。有了优雅关闭之后,RPC 服务端再重启的时
候,就会减少对客户端的影响。在这个过程中,服务的上线和下线是由服务端主动向注册中心注册、和取消注册来实现的,
这在正常的流程中是没有问题的。可是,如果某一个服务端意外故障,比如说机器掉电,网
络不通等情况,服务端就没有办法向注册中心通信,将自己从服务列表中删除,那么客户端
也就不会得到通知,它就会继续向一个故障的服务端发起请求,也就会有错误发生了。那这
种情况如何来避免呢?其实,这种情况是一个服务状态管理的问题。
服务状态管理如何来做
针对上面我提到的问题,我们一般会有两种解决思路。
第一种思路是主动探测,方法是这样的:
你的 RPC 服务要打开一个端口,然后由注册中心每隔一段时间(比如 30 秒)探测这些端
口是否可用,如果可用就认为服务仍然是正常的,否则就可以认为服务不可用,那么注册中
心就可以把服务从列表里面删除了。

微博早期的注册中心就是采用这种方式,但是后面出现的两个问题,让我们不得不对它做改
造。
第一个问题是:所有的 RPC 服务端都需要,开放一个统一的端口给注册中心探测,那时候
还没有容器化,一台物理机上会混合部署很多的服务,你需要开放的端口很可能已经被占
用,这样会造成 RPC 服务启动失败。还有一个问题是:如果 RPC 服务端部署的实例比较多,那么每次探测的成本也会比较高,
探测的时间也比较长,这样当一个服务不可用时,可能会有一段时间的延迟,才会被注册中
心探测到。
因此,我们后面把它改造成了心跳模式。
这也是大部分注册中心提供的,检测连接上来的 RPC 服务端是否存活的方式,比如
Eureka、ZooKeeper,在我来看,这种心跳机制可以这样实现:
注册中心为每一个连接上来的 RPC 服务节点,记录最近续约的时间,RPC 服务节点在启动
注册到注册中心后,就按照一定的时间间隔(比如 30 秒),向注册中心发送心跳包。注册
中心在接受到心跳包之后,会更新这个节点的最近续约时间。然后,注册中心会启动一个定
时器,定期检测当前时间和节点,最近续约时间的差值,如果达到一个阈值(比如说 90
秒),那么认为这个服务节点不可用。

在实际的使用中,心跳机制相比主动探测的机制,适用范围更广,如果你的服务也需要检测
是否存活,那么也可以考虑使用心跳机制来检测。
接着说回来,有了心跳机制之后,注册中心就可以管理注册的服务节点的状态了,也让你的
注册中心成为了整体服务最重要的组件,因为一旦它出现问题或者代码出现 Bug,那么很
可能会导致整个集群的故障,给你举一个真实的案例。在我之前的一个项目中,工程是以“混合云”的方式部署的,也就是一部分节点部署在自建
机房中,一部分节点部署在云服务器上,每一个机房都部署了自研的一套注册中心,每套注
册中心中都保存了全部节点的数据。
这套自研的注册中心使用 Redis 作为最终的存储,而在自建机房和云服务器上的注册中
心,共用同一套 Redis 存储资源。由于“混合云”还处在测试阶段,所以,所有的流量还
都在自建机房,自建机房和云服务器之前的专线带宽还比较小,部署结构如下:

在测试的过程中,系统运行稳定,但是某一天早上五点,我突然发现,所有的服务节点都被
摘除了,客户端因为拿不到服务端的节点地址列表全部调用失败,整体服务宕机。经过排查
我发现,云服务器上部署的注册中心,竟然将所有的服务节点全部删除了!进一步排查之
后,原来是自研注册中心出现了 Bug。
在正常的情况下,无论是自建机房,还是云服务器上的服务节点,都会向各自机房的注册中
心注册地址信息,并且发送心跳。而这些地址信息,以及服务的最近续约时间,都是存储在
Redis 主库中,各自机房的注册中心,会读各自机房的从库来获取最近续约时间,从而判断
服务节点是否有效。
Redis 的主从同步数据是通过专线来传输的,出现故障之前,专线带宽被占满,导致主从同
步延迟。这样一来,云上部署的 Redis 从库中存储的最近续约时间,就没有得到及时更
新,随着主从同步延迟越发严重,最终,云上部署的注册中心发现了,当前时间与最近续约
时间的差值,超过了摘除的阈值,所以将所有的节点摘除,从而导致了故障。有了这次惨痛的教训,我们给注册中心增加了保护的策略:如果摘除的节点占到了服务集群
节点数的 40%,就停止摘除服务节点,并且给服务的开发同学和,运维同学报警处理(这
个阈值百分比可以调整,保证了一定的灵活性)。
据我所知,Eureka 也采用了类似的策略,来避免服务节点被过度摘除,导致服务集群不足
以承担流量的问题。如果你使用的是 ZooKeeper 或者 ETCD 这种无保护策略的分布式一致
性组件,那你可以考虑在客户端,实现保护策略的逻辑,比如说当摘除的节点超过一定比例
时,你在 RPC 客户端就不再处理变更通知,你可以依据自己的实际情况来实现。
除此之外,在实际项目中,我们还发现注册中心另一个重要的问题就是“通知风暴”。你想
一想,变更一个服务的一个节点,会产生多少条推送消息?假如你的服务有 100 个调用
者,有 100 个节点,那么变更一个节点会推送 100 * 100 = 10000 个节点的数据。那么如
果多个服务集群同时上线或者发生波动时,注册中心推送的消息就会更多,会严重占用机器
的带宽资源,这就是我所说的“通知风暴”。那么怎么解决这个问题呢?你可以从以下几个
方面来思考:
首先,要控制一组注册中心管理的服务集群的规模,具体限制多少没有统一的标准,你
需要结合你的业务以及注册中心的选型来考虑,主要考察的指标就是注册中心服务器的
峰值带宽;
其次,你也可以通过扩容注册中心节点的方式来解决;
再次,你可以规范一下对于注册中心的使用方式,如果只是变更某一个节点,那么只需
要通知这个节点的变更信息即可;
最后,如果是自建的注册中心,你也可以在其中加入一些保护策略,比如说如果通知的
消息量达到某一个阈值就停止变更通知。
其实,服务的注册和发现,归根结底是服务治理中的一环,服务治理(service
governance),其实更直白的翻译应该是服务的管理,也就是解决多个服务节点,组成集
群的时候,产生的一些复杂的问题。为了帮助你理解,我来做个简单的比喻。
你可以把集群看作是一个微型的城市,把道路看做是组成集群的服务,把行走在道路上的车
当做是流量,那么服务治理就是对于整个城市道路的管理。如果你新建了一条街道(相当于启动了一个新的服务节点),那么就要通知所有的车辆(流
量)有新的道路可以走了;你关闭了一条街道,你也要通知所有车辆不要从这条路走了,这
就是服务的注册和发现。
我们在道路上安装监控,监视每条道路的流量情况,这就是服务的监控。
道路一旦出现拥堵或者道路需要维修,那么就需要暂时封闭这条道路,由城市来统一调度车
辆,走不堵的道路,这就是熔断以及引流。
道路之间纵横交错四通八达,一旦在某条道路上出现拥堵,但是又发现这条道路从头堵到
尾,说明事故并不是发生在这条道路上,那么就需要从整体链路上来排查事故究竟处在哪个
位置,这就是分布式追踪。
不同道路上的车辆有多有少,那么就需要有一个警察来疏导,在某一个时间走哪一条路会比
较快,这就是负载均衡。
而这些问题,我会在后面的课程中针对性地讲解。
课程小结
本节课,我带你了解了在微服务架构中,注册中心是如何实现服务的注册和发现的,以及在
实现中遇到的一些坑,除此之外,我还带你了解了服务治理的含义,以及后续我们会讲到的
一些技术点。在这节课中,我想让你明确的重点如下:
注册中心可以让我们动态地,变更 RPC 服务的节点信息,对于动态扩缩容,故障快速恢
复,以及服务的优雅关闭都有重要的意义;
心跳机制是一种常见的探测服务状态的方式,你在实际的项目中也可以考虑使用;
我们需要对注册中心中管理的节点提供一些保护策略,避免节点被过度摘除导致的服务
不可用。
你看,注册中心虽然是一种简单易懂的分布式组件,但是它在整体架构中的位置至关重要,
不容忽视。同时,在它的设计方案中,也蕴含了一些系统设计的技巧,比如上,面提到的服
务状态检测的方式,还有上面提到的优雅关闭的方式,了解注册中心的原理,会给你之后的
研发工作提供一些思路。
25 | 分布式Trace:横跨几十个分布式组件的慢请求要如何排查?
经过前面几节课的学习,你的垂直电商系统在引入 RPC 框架,和注册中心之后已经完成基
本的服务化拆分了,系统架构也有了改变:

现在,你的系统运行平稳,老板很高兴,你也安心了很多。而且你认为,在经过了服务化拆
分之后,服务的可扩展性增强了很多,可以通过横向扩展服务节点的方式,进行平滑地扩容
了,对于应对峰值流量也更有信心了。
但是这时出现了问题:你通过监控发现,系统的核心下单接口在晚高峰的时候,会有少量的
慢请求,用户也投诉在 APP 上下单时,等待的时间比较长。而下单的过程可能会调用多个
RPC 服务,或者使用多个资源,一时之间,你很难快速判断,究竟是哪个服务或者资源出
了问题,从而导致整体流程变慢,于是,你和你的团队开始想办法如何排查这个问题。
一体化架构中的慢请求排查如何做
因为在分布式环境下,请求要在多个服务之间调用,所以对于慢请求问题的排查会更困难,
我们不妨从简单的入手,先看看在一体化架构中,是如何排查这个慢请求的问题的。
最简单的思路是:打印下单操作的每一个步骤的耗时情况,然后通过比较这些耗时的数据,
找到延迟最高的一步,然后再来看看这个步骤要如何的优化。如果有必要的话,你还需要针
对步骤中的子步骤,再增加日志来继续排查,简单的代码就像下面这样:

这是最简单的实现方式,打印出日志后,我们可以登录到机器上,搜索关键词来查看每个步
骤的耗时情况。
虽然这个方式比较简单,但你可能很快就会遇到问题:由于同时会有多个下单请求并行处
理,所以,这些下单请求的每个步骤的耗时日志,是相互穿插打印的。你无法知道这些日
志,哪些是来自于同一个请求,也就不能很直观地看到,某一次请求耗时最多的步骤是哪一
步了。那么,你要如何把单次请求,每个步骤的耗时情况串起来呢?
一个简单的思路是:给同一个请求的每一行日志,增加一个相同的标记。这样,只要拿到这
个标记就可以查询到这个请求链路上,所有步骤的耗时了,我们把这个标记叫做
requestId,我们可以在程序的入口处生成一个 requestId,然后把它放在线程的上下文
中,这样就可以在需要时,随时从线程上下文中获取到 requestId 了。简单的代码实现就
像下面这样:

Version:0.9 StartHTML:0000000105 EndHTML:0000008359 StartFragment:0000000141 EndFragment:0000008319
有了 requestId,你就可以清晰地了解一个调用链路上的耗时分布情况了。
于是,你给你的代码增加了大量的日志,来排查下单操作缓慢的问题。很快, 你发现是某
一个数据库查询慢了才导致了下单缓慢,然后你优化了数据库索引,问题最终得到了解决。
正当你要松一口气的时候,问题接踵而至:又有用户反馈某些商品业务打开缓慢;商城首页
打开缓慢。你开始焦头烂额地给代码中增加耗时日志,而这时你意识到,每次排查一个接口
就需要增加日志、重启服务,这并不是一个好的办法,于是你开始思考解决的方案。
其实,从我的经验出发来说,一个接口响应时间慢,一般是出在跨网络的调用上,比如说请
求数据库、缓存或者依赖的第三方服务。所以,我们只需要针对这些调用的客户端类,做切
面编程,通过插入一些代码打印它们的耗时就好了。
说到切面编程(AOP)你应该并不陌生,它是面向对象编程的一种延伸,可以在不修改源
代码的前提下,给应用程序添加功能,比如说鉴权,打印日志等等。如果你对切面编程的概
念理解的还不透彻,那我给你做个比喻,帮你理解一下。
这就像开发人员在向代码仓库提交代码后,他需要对代码编译、构建、执行单元测试用例,
以保证提交的代码是没有问题的。但是,如果每个人提交了代码都做这么多事儿,无疑会对
开发同学造成比较大的负担,那么你可以配置一个持续集成的流程,在提交代码之后,自动
帮你完成这些操作,这个持续集成的流程就可以认为是一个切面。
一般来说,切面编程的实现分为两类:
一类是静态代理,典型的代表是 AspectJ,它的特点是在编译期做切面代码注入;
另一类是动态代理,典型的代表是 Spring AOP,它的特点是在运行期做切面代码注
入。
这两者有什么差别呢?以 Java 为例,源代码 Java 文件先被 Java 编译器,编译成 Class 文
件,然后 Java 虚拟机将 Class 装载进来之后,进行必要的验证和初始化后就可以运行了。静态代理是在编译期插入代码,增加了编译的时间,给你的直观感觉就是启动的时间变长
了,但是一旦在编译期插入代码完毕之后,在运行期就基本对于性能没有影响。
而动态代理不会去修改生成的 Class 文件,而是会在运行期生成一个代理对象,这个代理
对象对源对象做了字节码增强,来完成切面所要执行的操作。由于在运行期需要生成代理对
象,所以动态代理的性能要比静态代理要差。
我们做切面的原因,是想生成一些调试的日志,所以我们期望尽量减少对于原先接口性能的
影响。因此,我推荐采用静态代理的方式,实现切面编程。
如果你的系统中需要增加切面,来做一些校验、限流或者日志打印的工作,我也建议你考虑
使用静态代理的方式,使用 AspectJ 做切面的简单代码实现就像下面这样:

这样,你就在你的系统的每个接口中,打印出了所有访问数据库、缓存、外部接口的耗时情
况,一次请求可能要打印十几条日志,如果你的电商系统的 QPS 是 10000 的话,就是每
秒钟会产生十几万条日志,对于磁盘 I/O 的负载是巨大的,那么这时,你就要考虑如何减
少日志的数量。
你可以考虑对请求做采样,采样的方式也简单,比如你想采样 10% 的日志,那么你可以只
打印“requestId%10==0”的请求。
有了这些日志之后,当给你一个 requestId 的时候,你发现自己并不能确定这个请求到了
哪一台服务器上,所以你不得不登陆所有的服务器,去搜索这个 requestId 才能定位请
求。这样无疑会增加问题排查的时间。
你可以考虑的解决思路是:把日志不打印到本地文件中,而是发送到消息队列里,再由消息
处理程序写入到集中存储中,比如 Elasticsearch。这样,你在排查问题的时候,只需要拿
着 requestId 到 Elasticsearch 中查找相关的记录就好了。在加入消息队列和
Elasticsearch 之后,我们这个排查程序的架构图也会有所改变:

我来总结一下,为了排查单次请求响应时间长的原因,我们主要做了哪些事情:
1. 在记录打点日志时,我们使用 requestId 将日志串起来,这样方便比较一次请求中的多
个步骤的耗时情况;
2. 我们使用静态代理的方式做切面编程,避免在业务代码中,加入大量打印耗时的日志的
代码,减少了对于代码的侵入性,同时编译期的代码注入可以减少;3. 我们增加了日志采样率,避免全量日志的打印;
4. 最后为了避免在排查问题时,需要到多台服务器上搜索日志,我们使用消息队列,将日
志集中起来放在了 Elasticsearch 中。
如何来做分布式 Trace
你可能会问:题目既然是“分布式 Trace:横跨几十个分布式组件的慢请求要如何排
查?”,那么我为什么要花费大量的篇幅,来说明在一体化架构中如何排查问题呢?这是因
为在分布式环境下,你基本上也是依据上面,我提到的这几点来构建分布式追踪的中间件
的。
在一体化架构中,单次请求的所有的耗时日志,都被记录在一台服务器上,而在微服务的场
景下,单次请求可能跨越多个 RPC 服务,这就造成了,单次的请求的日志会分布在多个服
务器上。
当然,你也可以通过 requestId 将多个服务器上的日志串起来,但是仅仅依靠 requestId
很难表达清楚服务之间的调用关系,所以从日志中,就无法了解服务之间是谁在调用谁。因
此,我们采用 traceId + spanId 这两个数据维度来记录服务之间的调用关系(这里
traceId 就是 requestId),也就是使用 traceId 串起单次请求,用 spanId 记录每一次
RPC 调用。说起来可能比较抽象,我给你举一个具体的例子。
比如,你的请求从用户端过来,先到达 A 服务,A 服务会分别调用 B 和 C 服务,B 服务又
会调用 D 和 E 服务。

我来给你讲讲图中的内容:
用户到 A 服务之后会初始化一个 traceId 为 100,spanId 为 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 中间件,那么一定要提供一个开关,方便在线上随时将日志打印关闭;如果使用
开源的组件,可以开始设置一个较低的日志采样率,观察系统性能情况再调整到一个合适的
数值。
课程小结
本节课我带你了解了在一体化架构和服务化架构中,你要如何排查单次慢请求中,究竟哪一
个步骤是瓶颈,这里你需要了解的主要有以下几个重点:
服务的追踪的需求主要有两点,一点对代码要无侵入,你可以使用切面编程的方式来解
决;另一点是性能上要低损耗,我建议你采用静态代理和日志采样的方式,来尽量减少
追踪日志对于系统性能的影响;
无论是单体系统还是服务化架构,无论是服务追踪还是业务问题排查,你都需要在日志
中增加 requestId,这样可以将你的日志串起来,给你呈现一个完整的问题场景。如果
requestId 可以在客户端上生成,在请求业务接口的时候传递给服务端,那么就可以把
客户端的日志体系也整合进来,对于问题的排查帮助更大。
其实,分布式追踪系统不是一项新的技术,而是若干项已有技术的整合,在实现上并不复
杂,却能够帮助你实现跨进程调用链展示、服务依赖分析,在性能优化和问题排查方面提供
数据上的支持。所以,在微服务化过程中,它是一个必选项,无论是采用 Zipkin,Jaeger
这样的开源解决方案,还是团队内自研,你都应该在微服务化完成之前,尽快让它发挥应有
的价值。
26 | 负载均衡:怎样提升系统的横向扩展能力?
在基础篇中,我提到了高并发系统设计的三个通用方法:缓存、异步和横向扩展,到目前为
止,你接触到了缓存的使用姿势,也了解了,如何使用消息队列异步处理业务逻辑,那么本
节课,我将带你了解一下,如何提升系统的横向扩展能力。
在之前的课程中,我也提到过提升系统横向扩展能力的一些案例。比如,08 讲提到,可
以通过部署多个从库的方式,来提升数据库的扩展能力,从而提升数据库的查询性能,那么
就需要借助组件,将查询数据库的请求,按照一些既定的策略分配到多个从库上,这是负载
均衡服务器所起的作用,而我们一般使用 DNS 服务器来承担这个角色。不过在实际的工作中,你经常使用的负载均衡的组件应该算是 Nginx,它的作用是承接前
端的 HTTP 请求,然后将它们按照多种策略,分发给后端的多个业务服务器上。这样,我
们可以随时通过扩容业务服务器的方式,来抵挡突发的流量高峰。与 DNS 不同的是,
Nginx 可以在域名和请求 URL 地址的层面做更细致的流量分配,也提供更复杂的负载均衡
策略。
你可能会想到,在微服务架构中,我们也会启动多个服务节点,来承接从用户端到应用服务
器的请求,自然会需要一个负载均衡服务器,作为流量的入口,实现流量的分发。那么在微
服务架构中,如何使用负载均衡服务器呢?
在回答这些问题之前,我先带你了解一下,常见的负载均衡服务器都有哪几类,因为这样,
你就可以依据不同类型负载均衡服务器的特点做选择了。
负载均衡服务器的种类
负载均衡的含义是:将负载(访问的请求)“均衡”地分配到多个处理节点上。这样可以减
少单个处理节点的请求量,提升整体系统的性能。
同时,负载均衡服务器作为流量入口,可以对请求方屏蔽服务节点的部署细节,实现对于业
务方无感知的扩容。它就像交通警察,不断地疏散交通,将汽车引入合适的道路上。
而在我看来,负载均衡服务大体上可以分为两大类:一类是代理类的负载均衡服务;另一类
是客户端负载均衡服务。
代理类的负载均衡服务,以单独的服务方式部署,所有的请求都要先经过负载均衡服务,在
负载均衡服务中,选出一个合适的服务节点后,再由负载均衡服务,调用这个服务节点来实
现流量的分发。

由于这类服务需要承担全量的请求,所以对于性能的要求极高。代理类的负载均衡服务有很
多开源实现,比较著名的有 LVS,Nginx 等等。LVS 在 OSI 网络模型中的第四层,传输层
工作,所以 LVS 又可以称为四层负载;而 Nginx 运行在 OSI 网络模型中的第七层,应用
层,所以又可以称它为七层负载(你可以回顾一下02 讲的内容)。
在项目的架构中,我们一般会同时部署 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 客户端中。
它一般和客户端应用,部署在一个进程中,提供多种选择节点的策略,最终为客户端应用提
供一个最佳的,可用的服务端节点。这类服务一般会结合注册中心来使用,注册中心提供服
务节点的完整列表,客户端拿到列表之后使用负载均衡服务的策略选取一个合适的节点,然
后将请求发到这个节点上。

了解负载均衡服务的分类,是你学习负载均衡服务的第一步,接下来,你需要掌握负载均衡
策略,这样一来,你在实际工作中,配置负载均衡服务的时候,可以对原理有更深刻的了
解。
常见的负载均衡策略有哪些
负载均衡策略从大体上来看可以分为两类:一类是静态策略,也就是说负载均衡服务器在选择服务节点时,不会参考后端服务的实
际运行的状态。
一类是动态策略,也就是说负载均衡服务器会依据后端服务的一些负载特性,来决定要
选择哪一个服务节点。
常见的静态策略有几种,其中使用最广泛的是轮询的策略(RoundRobin,RR),这种策
略会记录上次请求后端服务的地址或者序号,然后在请求时,按照服务列表的顺序,请求下
一个后端服务节点。伪代码如下:
1 AtomicInteger lastCounter = getLastCounter();// 获取上次请求的服务节点的序号
2 List<String> serverList = getServerList(); // 获取服务列表
3 int currentIndex = lastCounter.addAndGet(); // 增加序列号
4 if(currentIndex >= serverList.size()) {
5 currentIndex = 0;
6 }
7 setLastCounter(currentIndex);
8 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 讲中,我带你了解到,在微服务化架构中,服务节点会定期地向注册中心发送心跳
包,这样注册中心就能够知晓服务节点是否故障,也就可以确认传递给负载均衡服务的节
点,一定是可用的。
但对于 Nginx 来说,我们要如何保证配置的服务节点是可用的呢?
这就要感谢淘宝开源的 Nginx 模块nginx_upstream_check_module了,这个模块可以
让 Nginx 定期地探测后端服务的一个指定的接口,然后根据返回的状态码,来判断服务是否还存活。当探测不存活的次数达到一定阈值时,就自动将这个后端服务从负载均衡服务器
中摘除。它的配置样例如下:
Nginx 按照上面的方式配置之后,你的业务服务器也要实现一个“/health_check”的接
口,在这个接口中返回的 HTTP 状态码,这个返回的状态码可以存储在配置中心中,这样
在变更状态码时,就不需要重启服务了(配置中心在第 33 节课中会讲到)。
节点检测的功能,还能够帮助我们实现 Web 服务的优雅关闭。在 24 讲中介绍注册中心
时,我曾经提到,服务的优雅关闭需要先切除流量再关闭服务,使用了注册中心之后,就可
以先从注册中心中摘除节点,再重启服务,以便达到优雅关闭的目的。那么 Web 服务要如
何实现优雅关闭呢?接下来,我来给你了解一下,有了节点检测功能之后,服务是如何启动
和关闭的。
在服务刚刚启动时,可以初始化默认的 HTTP 状态码是 500,这样 Nginx 就不会很快将这
个服务节点标记为可用,也就可以等待服务中,依赖的资源初始化完成,避免服务初始启动
时的波动。
在完全初始化之后,再将 HTTP 状态码变更为 200,Nginx 经过两次探测后,就会标记服
务为可用。在服务关闭时,也应该先将 HTTP 状态码变更为 500,等待 Nginx 探测将服务
标记为不可用后,前端的流量也就不会继续发往这个服务节点。在等待服务正在处理的请求
全部处理完毕之后,再对服务做重启,可以避免直接重启导致正在处理的请求失败的问题。
这是启动和关闭线上 Web 服务时的标准姿势,你可以在项目中参考使用。
课程小结
本节课,我带你了解了与负载均衡服务相关的一些知识点,以及在实际工作中的运用技巧。
我想强调几个重点:
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=t
check_http_send "GET /health_check HTTP/1.0\r\n\r\n"; // 检测 URL
check_http_expect_alive http_2xx; // 检测返回状态码为 200 时认为检测成功
}网站负载均衡服务的部署,是以 LVS 承接入口流量,在应用服务器之前,部署 Nginx 做
细化的流量分发,和故障节点检测。当然,如果你的网站的并发不高,也可以考虑不引
入 LVS。
负载均衡的策略可以优先选择动态策略,保证请求发送到性能最优的节点上;如果没有
合适的动态策略,那么可以选择轮询的策略,让请求平均分配到所有的服务节点上。
Nginx 可以引入 nginx_upstream_check_module,对后端服务做定期的存活检测,后
端的服务节点在重启时,也要秉承着“先切流量后重启”的原则,尽量减少节点重启对
于整体系统的影响。
你可能会认为,像 Nginx、LVS 应该是运维所关心的组件,作为开发人员不用操心维护。
不过通过今天的学习你应该可以看到:负载均衡服务是提升系统扩展性,和性能的重要组
件,在高并发系统设计中,它发挥的作用是无法替代的。理解它的原理,掌握使用它的正确
姿势,应该是每一个后端开发同学的必修课。
27 | API网关:系统的门面要如何做呢?
到目前为止,你的垂直电商系统在经过微服务化拆分之后,已经运行了一段时间了,系统的
扩展性得到了很大的提升,也能够比较平稳地度过高峰期的流量了。
不过最近你发现,随着自己的电商网站知名度越来越高,系统迎来了一些“不速之客”,在
凌晨的时候,系统中的搜索商品和用户接口的调用量,会有激剧的上升,持续一段时间之后
又回归正常。
这些搜索请求有一个共同特征是,来自固定的几台设备。当你在搜索服务上加一个针对设备
ID 的限流功能之后,凌晨的高峰搜索请求不见了。但是不久之后,用户服务也出现了大量爬取用户信息的请求,商品接口出现了大量爬取商品信息的请求。你不得不在这两个服务上
也增加一样的限流策略。
但是这样会有一个问题:不同的三个服务上使用同一种策略,在代码上会有冗余,无法做到
重用,如果其他服务上也出现类似的问题,还要通过拷贝代码来实现,肯定是不行的。
不过作为 Java 程序员,你很容易想到:将限流的功能独立成一个单独的 jar 包,给这三个
服务来引用。不过你忽略了一种情况,那就是你的电商团队使用的除了 Java,还有 PHP 和
Golang 等多种语言。
用多种语言开发的服务是没有办法使用 jar 包,来实现限流功能的,这时你需要引入 API
网关。
API 网关起到的作用(904)
API 网关(API Gateway)不是一个开源组件,而是一种架构模式,它是将一些服务共有的
功能整合在一起,独立部署为单独的一层,用来解决一些服务治理的问题。你可以把它看作
系统的边界,它可以对出入系统的流量做统一的管控。
在我看来,API 网关可以分为两类:一类叫做入口网关,一类叫做出口网关。
入口网关是我们经常使用的网关种类,它部署在负载均衡服务器和应用服务器之间,主要有
几方面的作用。
1. 它提供客户端一个统一的接入地址,API 网关可以将用户的请求动态路由到不同的业务
服务上,并且做一些必要的协议转换工作。在你的系统中,你部署的微服务对外暴露的
协议可能不同:有些提供的是 HTTP 服务;有些已经完成 RPC 改造,对外暴露 RPC 服
务;有些遗留系统可能还暴露的是 Web Service 服务。API 网关可以对客户端屏蔽这些
服务的部署地址,以及协议的细节,给客户端的调用带来很大的便捷。
2. 另一方面,在 API 网关中,我们可以植入一些服务治理的策略,比如服务的熔断、降
级,流量控制和分流等等(关于服务降级和流量控制的细节,我会在后面的课程中具体
讲解,在这里,你只要知道它们可以在 API 网关中实现就可以了)。
3. 再有,客户端的认证和授权的实现,也可以放在 API 网关中。你要知道,不同类型的客
户端使用的认证方式是不同的。在我之前项目中,手机 APP 使用 Oauth 协议认证,HTML5 端和 Web 端使用 Cookie 认证,内部服务使用自研的 Token 认证方式。这些
认证方式在 API 网关上,可以得到统一处理,应用服务不需要了解认证的细节。
4. 另外,API 网关还可以做一些与黑白名单相关的事情,比如针对设备 ID、用户 IP、用户
ID 等维度的黑白名单。
5. 最后,在 API 网关中也可以做一些日志记录的事情,比如记录 HTTP 请求的访问日志,
我在25 讲中讲述分布式追踪系统时,提到的标记一次请求的 requestId,也可以在网关
中来生成。

出口网关就没有这么丰富的功能和作用了。我们在系统开发中,会依赖很多外部的第三方系
统,比如典型的例子:第三方账户登录、使用第三方工具支付等等。我们可以在应用服务器
和第三方系统之间,部署出口网关,在出口网关中,对调用外部的 API 做统一的认证、授
权,审计以及访问控制。

我花一定的篇幅去讲 API 网关起到的作用,主要是想让你了解,API 网关可以解决什么样
的实际问题,这样,当你在面对这些问题时,你就会有解决的思路,不会手足无措了。
API 网关要如何实现
了解 API 网关的作用之后,所以接下来,我们来看看 API 网关在实现中需要关注哪些点,
以及常见的开源 API 网关有哪些,这样,你在实际工作中,无论是考虑自研 API 网关还是
使用开源的实现都会比较自如了。
在实现一个 API 网关时,你首先要考虑的是它的性能。这很好理解,API 入口网关承担从
客户端的所有流量。假如业务服务处理时间是 10ms,而 API 网关的耗时在 1ms,那么相
当于每个接口的响应时间都要增加 10%,这对于性能的影响无疑是巨大的。而提升 API 网
关性能的关键还是在 I/O 模型上(我在23 讲中详细讲到过),这里只是举一个例子来说
明 I/O 模型对于性能的影响。
Netfix 开源的 API 网关 Zuul,在 1.0 版本的时候使用的是同步阻塞 I/O 模型,整体系统其
实就是一个 servlet,在接收到用户的请求,然后执行在网关中配置的认证、协议转换等逻
辑之后,调用后端的服务获取数据返回给用户。
而在 Zuul2.0 中,Netfix 团队将 servlet 改造成了一个 netty server(netty 服务),采用
I/O 多路复用的模型处理接入的 I/O 请求,并且将之前同步阻塞调用后端服务的方式,改造成使用 netty client(netty 客户端)非阻塞调用的方式。改造之后,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
了。

另外还需要注意的一点是,为了提升网关对于请求的并行处理能力,我们一般会使用线程池
来并行的执行请求。不过,这就带来一个问题:如果商品服务出现问题,造成响应缓慢,那
么调用商品服务的线程就会被阻塞无法释放,久而久之,线程池中的线程就会被商品服务所
占据,那么其他服务也会受到级联的影响。因此,我们需要针对不同的服务做线程隔离,或
者保护。在我看来有两种思路:
如果你后端的服务拆分得不多,可以针对不同的服务,采用不同的线程池,这样商品服
务的故障就不会影响到支付服务和用户服务了;
在线程池内部可以针对不同的服务,甚至不同的接口做线程的保护。比如说,线程池的
最大线程数是 1000,那么可以给每个服务设置一个最多可以使用的配额。
一般来说,服务的执行时间应该在毫秒级别,线程被使用后会很快被释放,回到线程池给后
续请求使用,同时处于执行中的线程数量不会很多,对服务或者接口设置线程的配额,不会
影响到正常的执行。可是一旦发生故障,某个接口或者服务的响应时间变长,造成线程数暴
涨,但是因为有配额的限制,也就不会影响到其他的接口或者服务了。你在实际应用中也可以将这两种方式结合,比如说针对不同的服务使用不同的线程池,在线
程池内部针对不同的接口设置配额。
以上就是实现 API 网关的一些关键的点,你如果要自研 API 网关服务的话可以参考借鉴。
另外 API 网关也有很多开源的实现,目前使用比较广泛的有以下几个:
Kong是在 Nginx 中运行的 Lua 程序。得益于 Nginx 的性能优势,Kong 相比于其它
的开源 API 网关来说,性能方面是最好的。由于大中型公司对于 Nginx 运维能力都比较
强,所以选择 Kong 作为 API 网关,无论是在性能还是在运维的把控力上,都是比较好
的选择;
Zuul是 Spring Cloud 全家桶中的成员,如果你已经使用了 Spring Cloud 中的其他
组件,那么也可以考虑使用 Zuul 和它们无缝集成。不过,Zuul1 因为采用同步阻塞模
型,所以在性能上并不是很高效,而 Zuul2 推出时间不长,难免会有坑。但是 Zuul 的
代码简单易懂,可以很好的把控,并且你的系统的量级很可能达不到 Netfix 这样的级
别,所以对于 Java 技术栈的团队,使用 Zuul 也是一个不错的选择;
Tyk是一种 Go 语言实现的轻量级 API 网关,有着丰富的插件资源,对于 Go 语言栈
的团队来说,也是一种不错的选择。
那么你要考虑的是,这些开源项目适不适合作为 AIP 网关供自己使用。而接下来,我以电
商系统为例,带你将 API 网关引入我们的系统之中。
如何在你的系统中引入 API 网关呢?
目前为止,我们的电商系统已经经过了服务化改造,在服务层和客户端之间有一层薄薄的
Web 层,这个 Web 层做的事情主要有两方面:
一方面是对服务层接口数据的聚合。比如,商品详情页的接口,可能会聚合服务层中,获取
商品信息、用户信息、店铺信息以及用户评论等多个服务接口的数据;
另一方面 Web 层还需要将 HTTP 请求转换为 RPC 请求,并且对前端的流量做一些限制,
对于某些请求添加设备 ID 的黑名单等等。
因此,我们在做改造的时候,可以先将 API 网关从 Web 层中独立出来,将协议转换、限
流、黑白名单等事情,挪到 API 网关中来处理,形成独立的入口网关层;而针对服务接口数据聚合的操作,一般有两种解决思路:
1. 再独立出一组网关专门做服务聚合、超时控制方面的事情,我们一般把前一种网关叫做
流量网关,后一种可以叫做业务网关;
2. 抽取独立的服务层,专门做接口聚合的操作。这样服务层就大概分为原子服务层和聚合
服务层。
我认为,接口数据聚合是业务操作,与其放在通用的网关层来实现,不如放在更贴近业务的
服务层来实现,所以,我更倾向于第二种方案。

同时,我们可以在系统和第三方支付服务,以及登陆服务之间部署出口网关服务。原先,你
会在拆分出来的支付服务中,完成对于第三方支付接口所需要数据的加密、签名等操作,再
调用第三方支付接口,完成支付请求。现在,你把对数据的加密、签名的操作放在出口网关
中,这样一来,支付服务只需要调用出口网关的统一支付接口就可以了。
在引入了 API 网关之后,我们的系统架构就变成了下面这样:

课程小结
本节课我带你了解了 API 网关在系统中的作用,在实现中的一些关键的点,以及如何将
API 网关引入你的系统,我想强调的重点如下:
1. API 网关分为入口网关和出口网关两类,入口网关作用很多,可以隔离客户端和微服
务,从中提供协议转换、安全策略、认证、限流、熔断等功能。出口网关主要是为调用
第三方服务提供统一的出口,在其中可以对调用外部的 API 做统一的认证、授权,审计
以及访问控制;
2. API 网关的实现重点在于性能和扩展性,你可以使用多路 I/O 复用模型和线程池并发处
理,来提升网关性能,使用责任链模式来提升网关的扩展性;
3. API 网关中的线程池,可以针对不同的接口或者服务做隔离和保护,这样可以提升网关
的可用性;
4. API 网关可以替代原本系统中的 Web 层,将 Web 层中的协议转换、认证、限流等功能
挪入到 API 网关中,将服务聚合的逻辑下沉到服务层。API 网关可以为 API 的调用提供便捷,也可以为将一些服务治理的功能独立出来,达到复
用的目的,虽然在性能上可能会有一些损耗,但是一般来说,使用成熟的开源 API 网关组
件,这些损耗都是可以接受的。所以,当你的微服务系统越来越复杂时,你可以考虑使用
API 网关作为整体系统的门面。
28 | 多机房部署:跨地域的分布式系统如何做?
来想象这样一个场景:你的垂直电商系统部署的 IDC 机房,在某一天发布了公告说,机房
会在第二天凌晨做一次网络设备的割接,在割接过程中会不定时出现瞬间,或短时间网络中
断。
机房网络的中断,肯定会对业务造成不利的影响,即使割接的时间在凌晨(业务的低峰
期),作为技术负责人的你,也要尽量思考方案来规避隔离的影响。然而不幸的是,在现有
的技术架构下,电商业务全都部署在一个 IDC 机房中,你并没有好的解决办法。而 IDC 机房的可用性问题是整个系统的阿喀琉斯之踵,一旦 IDC 机房像一些大厂一样,出
现很严重的问题,就会对整体服务的可用性造成严重的影响。比如:
2016 年 7 月,北京联通整顿旗下 40 多个 IDC 机房中,不规范的接入情况,大批不合规接
入均被断网,这一举动致使脉脉当时使用的蓝汛机房受到影响,脉脉宕机长达 15 个小时,
著名的 A 站甚至宕机超过 48 个小时,损失可想而知。
而目前,单一机房部署的架构特点,决定了你的系统可用性受制于机房的可用性,也就是机
房掌控了系统的生命线。所以,你开始思考,如何通过架构的改造,来进一步提升系统的可
用性。在网上搜索解决方案和学习一些大厂的经验后,你发现“多机房部署”可以解决这个
问题。
多机房部署的难点是什么
多机房部署的含义是:在不同的 IDC 机房中,部署多套服务,这些服务共享同一份业务数
据,并且都可以承接来自用户的流量。
这样,当其中某一个机房出现网络故障、火灾,甚至整个城市发生地震、洪水等大的不可抗
的灾难时,你可以随时将用户的流量切换到其它地域的机房中,从而保证系统可以不间断地
持续运行。这种架构听起来非常美好,但是在实现上却是非常复杂和困难的,那么它复杂在
哪儿呢?
假如我们有两个机房 A 和 B 都部署了应用服务,数据库的主库和从库部署在 A 机房,那么
机房 B 的应用如何访问到数据呢?有两种思路。
一个思路是直接跨机房读取 A 机房的从库:

另一个思路是在机房 B 部署一个从库,跨机房同步主库的数据,然后机房 B 的应用就可以
读取这个从库的数据了:

无论是哪一种思路,都涉及到跨机房的数据传输,这就对机房之间延迟情况有比较高的要求
了。而机房之间的延迟,和机房之间的距离息息相关,你可以记住几个数字:
1. 北京同地双机房之间的专线延迟一般在 1ms~3ms。
这个延迟会造成怎样的影响呢?要知道,我们的接口响应时间需要控制在 200ms 之内,而
一个接口可能会调用几次第三方 HTTP 服务,或者 RPC 服务。如果这些服务部署在异地机
房,那么接口响应时间就会增加几毫秒,是可以接受的。
一次接口可能会涉及几次的数据库写入,那么如果数据库主库在异地机房,那么接口的响应
时间也会因为写入异地机房的主库,增加几毫秒到十几毫秒,也是可以接受的。
但是,接口读取缓存和数据库的数量,可能会达到十几次甚至几十次,那么这就会增加几十
毫秒甚至上百毫秒的延迟,就不能接受了。
2. 国内异地双机房之间的专线延迟会在 50ms 之内。
具体的延迟数据依据距离的不同而不同。比如,北京到天津的专线延迟,会在 10ms 之
内;而北京到上海的延迟就会提高到接近 30ms;如果想要在北京和广州部署双机房,那么延迟就会到达 50ms 了。在这个延迟数据下,要想保证接口的响应时间在 200ms 之内,就
要尽量减少跨机房的服务调用,更要避免跨机房的数据库和缓存操作了。
3. 如果你的业务是国际化的服务,需要部署跨国的双机房,那么机房之间的延迟就更高
了,依据各大云厂商的数据来看,比如,从国内想要访问部署在美国西海岸的服务,这个延
迟会在 100ms~200ms 左右。在这个延迟下,就要避免数据跨机房同步调用,而只做异步
的数据同步。
如果你正在考虑多机房部署的架构,那么这些数字都是至关重要的基础数据,你需要牢牢记
住,避免出现跨机房访问数据造成性能衰减问题。
机房之间的数据延迟,在客观上是存在的,你没有办法改变,你可以做的,就是尽量避免数
据延迟对于接口响应时间的影响。那么在数据延迟下,你要如何设计多机房部署的方案呢?
逐步迭代多机房部署方案
1. 同城双活
制定多机房部署的方案不是一蹴而就的,而是不断迭代发展的。我在上面提到,同城机房之
间的延时在 1ms~3ms 左右,对于跨机房调用的容忍度比较高,所以,这种同城双活的方
案复杂度会比较低。
但是,它只能做到机房级别的容灾,无法做到城市级别的容灾。不过,相比于城市发生地
震、洪水等自然灾害来说,机房网络故障、掉电出现的概率要大的多。所以,如果你的系统
不需要考虑城市级别的容灾,一般做到同城双活就足够了。那么,同城双活的方案要如何设
计呢?
假设这样的场景:你在北京有 A 和 B 两个机房,A 是联通的机房,B 是电信的机房,机房
之间以专线连接,方案设计时,核心思想是,尽量避免跨机房的调用。具体方案如下:
首先,数据库的主库可以部署在一个机房中,比如部署在 A 机房中,那么 A 和 B 机房数
据都会被写入到 A 机房中。然后,在 A、B 两个机房中各部署一个从库,通过主从复制
的方式,从主库中同步数据,这样双机房的查询请求可以查询本机房的从库。一旦 A 机
房发生故障,可以通过主从切换的方式,将 B 机房的从库提升为主库,达到容灾的目
的。缓存也可以部署在两个机房中,查询请求也读取本机房的缓存,如果缓存中数据不存
在,就穿透到本机房的从库中,加载数据。数据的更新可以更新双机房中的数据,保证
数据的一致性。
不同机房的 RPC 服务会向注册中心,注册不同的服务组,而不同机房的 RPC 客户端,
也就是 Web 服务,只订阅同机房的 RPC 服务组,这样就可以实现 RPC 调用尽量发生
在本机房内,避免跨机房的 RPC 调用。

你的系统肯定会依赖公司内的其他服务,比如审核,搜索等服务,如果这些服务也是双机房
部署的,那么也需要尽量保证只调用本机房的服务,降低调用的延迟。
使用了同城双活架构之后,可以实现机房级别的容灾,服务的部署也能够突破单一机房的限
制,但是,还是会存在跨机房写数据的问题,不过鉴于写数据的请求量不高,所以在性能上
是可以容忍的。
2. 异地多活
上面这个方案,足够应对你目前的需要,但是,你的业务是不断发展的,如果有朝一日,你
的电商系统的流量达到了京东或者淘宝的级别,那么你就要考虑,即使机房所在的城市发生
重大的自然灾害,也要保证系统的可用性。而这时,你需要采用异地多活的方案(据我所
知,阿里和饿了么采用的都是异地多活的方案)。
在考虑异地多活方案时,你首先要考虑异地机房的部署位置。它部署的不能太近,否则发生
自然灾害时,很可能会波及。所以,如果你的主机房在北京,那么异地机房就尽量不要建设
在天津,而是可以选择上海、广州这样距离较远的位置。但这就会造成更高的数据传输延
迟,同城双活中,使用的跨机房写数据库的方案,就不合适了。所以,在数据写入时,你要保证只写本机房的数据存储服务,再采取数据同步的方案,将数
据同步到异地机房中。一般来说,数据同步的方案有两种:
一种基于存储系统的主从复制,比如 MySQL 和 Redis。也就是在一个机房部署主库,
在异地机房部署从库,两者同步主从复制, 实现数据的同步。
另一种是基于消息队列的方式。一个机房产生写入请求后,会写一条消息到消息队列,
另一个机房的应用消费这条消息后,再执行业务处理逻辑,写入到存储服务中。
我建议你,采用两种同步相结合的方式,比如,你可以基于消息的方式,同步缓存的数据、
HBase 数据等。然后基于存储,主从复制同步 MySQL、Redis 等数据。
无论是采取哪种方案,数据从一个机房,传输到另一个机房都会有延迟,所以,你需要尽量
保证用户在读取自己的数据时,读取数据主库所在的机房。为了达到这一点,你需要对用户
做分片,让一个用户每次的读写都尽量在同一个机房中。同时,在数据读取和服务调用时,
也要尽量调用本机房的服务。这里有一个场景:假如在电商系统中,用户 A 要查看所有订
单的信息,而这些订单中,店铺的信息和卖家的信息很可能是存储在异地机房中,那么你应
该优先保证服务调用,和数据读取在本机房中进行,即使读取的是跨机房从库的数据,会有
一些延迟,也是可以接受的。

课程小结
本节课,为了提升系统的可用性和稳定性,我带你探讨了多机房部署的难点,以及同城双机
房和异地多活的部署架构,在这里,我想强调几个重点:不同机房的数据传输延迟,是造成多机房部署困难的主要原因,你需要知道,同城多机
房的延迟一般在 1ms~3ms,异地机房的延迟在 50ms 以下,而跨国机房的延迟在
200ms 以下。
同城多机房方案可以允许有跨机房数据写入的发生,但是数据的读取,和服务的调用应
该尽量保证在同一个机房中。
异地多活方案则应该避免跨机房同步的数据写入和读取,而是采取异步的方式,将数据
从一个机房同步到另一个机房。
多机房部署是一个业务发展到一定规模,对于机房容灾有需求时,才会考虑的方案,能不做
则尽量不要做。一旦你的团队决定做多机房部署,那么同城双活已经能够满足你的需求了,
这个方案相比异地多活要简单很多。而在业界,很少有公司,能够搭建一套真正的异步多活
架构,这是因为这套架构在实现时过于复杂,所以,轻易不要尝试。
总之,架构需要依据系统的量级和对可用性、性能、扩展性的要求,不断演进和调整,盲目
地追求架构的“先进性”只能造成方案的复杂,增加运维成本,从而给你的系统维护带来不
便。
29 | Service Mesh:如何屏蔽服务化系统的服务治理细节?
在分布式服务篇的前几节课程中,我带你了解了在微服务化过程中,要使用哪些中间件解决
服务之间通信和服务治理的问题,其中就包括:
用 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(边车)”,服务之间的通
信也从之前的,客户端和服务端直连,变成了下面这种形式:

在这种形式下,RPC 客户端将数据包先发送给,与自身同主机部署的 Sidecar,在 Sidecar
中经过服务发现、负载均衡、服务路由、流量控制之后,再将数据发往指定服务节点的
Sidecar,在服务节点的 Sidecar 中,经过记录访问日志、记录分布式追踪日志、限流之
后,再将数据发送给 RPC 服务端。
这种方式,可以把业务代码和服务治理的策略隔离开,将服务治理策略下沉,让它成为独立
的基础模块。这样一来,不仅可以实现跨语言,服务治理策略的复用,还能对这些 Sidecar
做统一的管理。
目前,业界提及最多的 Service Mesh 方案当属istio, 它的玩法是这样的:

它将组件分为数据平面和控制平面,数据平面就是我提到的 Sidecar(Istio 使用Envoy
作为 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 中默认有五条链,你可以把这五条链,当作
数据包流转过程中的五个步骤,依次为 PREROUTING,INPUT,FORWARD,OUTPUT
和 POSTROUTING。数据包传输的大体流程如下:

从图中可以看到,数据包以 PREROUTING 链作为入口,当数据包目的地为本机时,它们
也都会流经到 OUTPUT 链。所以,我们可以在这两个链上,增加一些规则,将数据包重定
向。我以 Istio 为例,带你看看如何使用 iptables 实现流量转发。
在 Istio 中,有一个叫做“istio-iptables.sh”的脚本,这个脚本在 Sidecar 被初始化的时
候执行,主要是设置一些 iptables 规则。
我摘录了一些关键点来说明一下:


Iptables 方式的优势在于,对于业务完全透明,业务甚至不知道有 Sidecar 存在,这样会
减少业务接入的时间。不过,它也有缺陷,那就是它是在高并发下,性能上会有损耗,因此
国内大厂采用了另外一种方式:轻量级客户端。
在这种方式下,RPC 客户端会通过配置的方式,知道 Sidecar 的部署端口,然后通过一个
轻量级客户端,将调用服务的请求发送给 Sidecar,Sidecar 在转发请求之前,先执行一些
服务治理的策略,比如说,从注册中心中,查询到服务节点信息并且缓存起来,然后从服务
节点中,使用某种负载均衡的策略选出一个节点等等。
请求被发送到服务端的 Sidecar 上后,然后在服务端记录访问日志,和分布式追踪日志,
再把请求转发到真正的服务节点上。当然,服务节点在启动时,会委托服务端 Sidecar,向
注册中心注册节点,Sidecar 也就知道了真正服务节点部署的端口是多少。整个请求过程如
图所示:

当然,除了 iptables 和轻量级客户端两种方式外,目前在探索的方案还有Cilium,这个
方案可以从 Socket 层面实现请求的转发,也就可以避免 iptables 方式在性能上的损耗。
在这几种方案中,我建议你使用轻量级客户端的方式,这样虽然会有一些改造成本,但是却
在实现上最简单,可以快速的让 Service Mesh 在你的项目中落地。
当然,无论采用哪种方式,你都可以实现将 Sidecar 部署到,客户端和服务端的调用链路
上,让它代理进出流量,这样,你就可以使用运行在 Sidecar 中的服务治理的策略了。至
于这些策略我在前面的课程中都带你了解过(你可以回顾 23 至 26 讲的课程),这里就不
再赘述了。
与此同时,我也建议你了解目前业界一些开源的 Service Mesh 框架,这样在选择方案时可
以多一些选择。目前在开源领域比较成熟的 Service Mesh 框架有下面几个,你可以通过阅
读它们的文档来深入了解,作为本节课的引申阅读。
1. Istio 这个框架在业界最为著名,它提出了数据平面和控制平面的概念,是 Service
Mesh 的先驱,缺陷就是刚才提到的 Mixer 的性能问题。
2. Linkerd 是第一代的 Service Mesh,使用 Scala 语言编写,其劣势就是内存的占用。
3. SOFAMesh 是蚂蚁金服开源的 Service Mesh 组件,在蚂蚁金服已经有大规模落地的
经验。
课程小结
本节课,为了解决跨语言场景下,服务治理策略的复用问题,我带你了解了什么是 Service
Mesh 以及如何在实际项目中落地,你需要的重点内容如下:1.Service Mesh 分为数据平面和控制平面。数据平面主要负责数据的传输;控制平面用来
控制服务治理策略的植入。出于性能的考虑,一般会把服务治理策略植入到数据平面中,控
制平面负责服务治理策略数据的下发。
2.Sidecar 的植入方式目前主要有两种实现方式,一种是使用 iptables 实现流量的劫持;另
一种是通过轻量级客户端来实现流量转发。
目前,在一些大厂中,比如微博、蚂蚁金服,Service Mesh 已经开始在实际项目中大量的
落地实践,而我建议你持续关注这项技术。它本身是一种将业务与通信基础设施分离的技
术,如果你的业务上遇到多语言环境下,服务治理的困境,如果你的遗留服务,需要快速植
入服务治理策略,如果你想要将你在服务治理方面积累的经验,快速地与其他团队共享,那
么 Service Mesh 就是你的一个不错的选择。

610

被折叠的 条评论
为什么被折叠?



