Download as pdf or txt
Download as pdf or txt
You are on page 1of 76

目录

把阿里巴巴的核心系统搬到云上,架构上的
挑战与演进是什么? 7
看完这篇你就知道什么是无服务器架构了
18
当心“中间件” 23
我们是怎么做到每小时推送百万级通知的
31
DDD:架构思想的旧瓶新酒 37
我们为什么用 gRPC 取代了 Kafka 43
大数据容器化,头部玩家尝到了甜头? 46
完整微服务化示例 50

1
CONTENTS / 目录
热点 | Hot
把阿里巴巴的核心系统搬到云上,架构上的挑战与演进是什么?

理论派 | Theory
看完这篇你就知道什么是无服务器架构了

推荐文章 | Article
当心“中间件”

我们是怎么做到每小时推送百万级通知的

观点 | Opinion
DDD:架构思想的旧瓶新酒

我们为什么用 gRPC 取代了 Kafka

特别专栏 | Column
大数据容器化,头部玩家尝到了甜头?

完整微服务化示例:使用 Apache ServiceComb 进行微服务开发、


容器化、弹性伸缩

架构师
2020 年 1 月刊

本期主编 张之栋 提供反馈 feedback@geekbang.com


流程编辑 丁晓昀 商务合作 hezuo@geekbang.org
发行人 霍泰稳 内容合作 editors@geekbang.com

2
专题 |卷首语
Topic

前端热词中的趋势:
Flutter、WebAssembly、Serverless……

作者 张之栋

2019 年 的 大 前 端 领 域 虽 然 并 没 有 出 现 什 么 颠 覆 性 技 术, 但 是 从
Flutter、WebAssembly、Serverless 等技术的火爆程度来看,大前端正呈现
出一种融合的趋势。

Flutter
Flutter 是谷歌的移动 UI 框架,可以快速在 iOS 和 Android 上构建
高质量的原生用户界面。然而,在今年的谷歌 I/O 大会上,谷歌公布了
Flutter 实现 Web 访问、提供自定义图像分类模型的新特性,并介绍了
Flutter 在桌面系统及嵌入式设备中的应用与未来的发展。从此,Flutter 项
目不再是一套单纯的移动框架,而是成为了一款可在 Web、桌面、移动以
及其他各类设备平台上运用的多平台框架。
Flutter 之所以如此流行,在于它的三个特点:精美、高效开发、开放。
Flutter 具有丰富的 Widget 库、 Material Design 和 Cupertino 风格的系
统库、组合式的 API 、像素级的控制力可以使开发者便捷地构建精美的应
用。
Flutter 特有的 Dart 语言是为数不多同时支持 JIT 和 AOT 编译的语言。
开发期使用 JIT 编译,支持广受欢迎的热重载功能,开发者可以像 PS 图
片一样来开发应用,开发效率高。发布后 Flutter 使用 AOT 编译, Dart 代
码最终被编译成 ARM 汇编指令,运行快速。
Flutter 是开源项目,其整个的开发工作流都是完全遵循开源项目的运
作来完成的。

3
InfoQ 架构师 2020 年 1 月

WebAssembly
WebAssembly 是由 Google、Microsoft、Mozilla、Apple 等几家大公司
合作发起的一个关于面向 Web 的通用二进制和文本格式的项目。
具体来讲,WebAssembly 是一种新的字节码格式,旨在成为高级语言
的编译目标,目前可以使用 C、C++、Rust、Go、Java、C# 等编译器(未
来还有更多)来创建 wasm 模块。
wasm 模块以二进制的格式发送到浏览器,并在专有虚拟机上执行,
与 JavaScript 虚拟机共享内存和线程等资源。
WebAssembly 处于编译阶段的中间码环节,它能做到像 Java 字节码
一样,一次编译,处处运行,具有跨平台特性。与此同时,作为中间码
的 WebAssembly 能够省略编译前端的步骤,开发者可以将现有的用 C、
C++ 等语言编写的库直接编译成 WebAssembly 并运行到浏览器上,这与
JavaScript 的实时编译相比,确实性能优势显著。
除此之外,WebAssembly 还引入了 GC / DOM / Web API 等一系列特
性,DOM 和 Web API 很重要,这意味着 WebAssembly 可直接和 html 以
及浏览器进行交互,从技术角度来看可以完全取代 JavaScript。

Serverless
Serverless 译作无服务器,虽然目前还没有一个具体的定义,但在行
业内,Serverless 的解读主要有这样 2 种:Serverless 架构和 Serverless 产品。
Serverless 架构完全依托于云厂商或云平台提供产品完成系统的组织
及构建,用户无需关注支撑应用服务运行的主机,只需关注于系统架构、
业务开发、业务支撑运维上。
Serverless 产品则是指无需理解、管理服务器,按需使用,按使用付
费的产品,其中包含了存储、计算等多种类型的产品,而典型的计算产品
则是云函数形态。
Serverless 被认作是云计算发展的未来方向,尤其是在前端研发领域,
使用 Node 开发云函数,可以让前端工程师更加专注于业务逻辑,实现全
栈工程师的角色转变。而且,从目前 Serverless 的实践情况来说,它可以
为前端开发带来 2 点好处:节约成本和提升研发交付速度。
对于初创公司来说,采用函数计算的 Serverless 不仅可以节省大量的

4
专题 |卷首语
Topic

资金,还会拥有更多的时间去开发业务功能。而且,Serverless 的服务供
应商,往往会提供一系列的配套服务。这种情况下,使用某一个云服务,
就和调用某一系统中自带的 API 一样简单,这样就又可以节省很多的开发
成本。
简而言之,Serverless 底层架构做的事情越多,业务层面需要关注的
架构和运维工作就越少。
而且,在 Serverless 的模式下,全栈开发的工作模式会执行得更加顺
畅,开发者不需要在架构和技术栈上花费过多精力,Runtime 和语言也没
有强制依赖,这样完全面向业务的模式,使得整个项目的研发交付速度得
到提升。

其他:BFF、中台……
除了以上的 3 个热词外,前端领域还有很多火热的技术,如:BFF、中台··
也许这些乍看起来似乎与前端并无太大关系,但在前端的发展中,它
们却又不可避免,就像一种瓶颈,遏制着前端能力的延伸。
换句话说,前端做到一定阶段会变得越来越聚焦,随着资产(代码)
的积累,很多东西也都会以自动化的方式呈现,再加上这些火热技术的迭
代发展,前端领域碎片化的部分将不断的融合在一起,并借助这种融合,
进一步推动大前端时代的到来。

5
6
专题
热点| |Topic
Hot

把阿里巴巴的核心系统搬到云上,架构上的
挑战与演进是什么?
作者 张瓅玶

阿里巴巴核心系统作为全球最大规模、峰值性能要求最高的电商交易
系统,在 2018 年之前只通过混合云弹性上云方式,为双 11 节约大量成本。
直到 2019 年,阿里巴巴实现了核心交易系统全面上云并经历了双 11 峰值
的考验。

核心系统上云之路
工程师时常把我们的系统用飞机来做比喻,乘客则是上面承载的业务。
云也是一架这样的载客飞机,作为基础平台承载着千万家企业的业务。今
年阿里巴巴实现了核心系统 100% 上云,这个过程实际上走了几年才达到
今天的进展,而且这还不是结束,也只是阿里巴巴上云的一个开始。
阿里巴巴集团自身业务体量巨大,支撑其的互联网技术体系任务也非
常繁重,再加上核心电商业务系统的复杂度,对技术带来的挑战可想而知。
用王坚博士的话说,核心系统上云让阿里巴巴和客户真正坐上了同一架
飞机。从 in-house 的基础设施、定制化的平台能力,到通用的云平台,从
cloud hosting 到 cloud native,这个过程面临着巨大的挑战,同时也是阿里
巴巴自身和阿里云的架构演进升级的历程。

7
InfoQ 架构师 2020 年 1 月

阿里巴巴的核心交易系统涉及到包括天猫、淘宝、河马、菜鸟、聚划
算、咸鱼、飞猪等一系列业务,其背后的核心电商系统的架构演进经历了
单机房架构、同城双机房架构再到目前的中心同城容灾,三地多单元多活
架构。软件也分为应用、微服务 / 中间件和数据库。
阿里巴巴的上云步骤一共分为三个阶段:
第一阶段:在 2015 年之前未上云,全部采用内部的基础设施。

第二阶段:2015 开始,双 11 期间单元化的交易应用开始通过弹性使


用云资源,实现成本节约的目标(注 : 图中上云单元规模和实际上云规模
不成比例)。

第三阶段:2018 年的 12 月,CTO 行癫决定阿里巴巴启动全面上云,


随后组建了以毕玄为上云总架构师的架构组,确定了上云的方案和步骤。
2019 年经过 1 年的努力,终于在双 11 前实现了核心系统的全面上云。这
一年核心电商的中心和单元业务,包括数据库、中间件等组件,实现了全
面上云和使用云的服务。通过弹性运化,以及离在线混部(图中未标识)

8
专题
热点| |Topic
Hot

等能力,使大促成本持续下降,到 2018 年,大促新增成本比前一年下降


17%,比早期方案下降 3/4。
这一年核心电商的中心和单元业务,包括数据库、中间件等组件,实
现了全面上云和使用云的服务。全面上云也不只是将机器搬到云上,更重
要的是 replatforming,集团的技术栈和云产品融合,应用通过神龙服务器
+ 容器和 K8s 上云,数据库接入 PolarDB,中间件采用云中间件和消息产品,
负载均衡采用云 SLB 产品等。

云已经成为了基础设施,无论是电商公司还是其他行业,都可以用云
去做更多事情。

云化架构
以全面上云的 2019 年为例,2019 年双 11 的实测,集群的规模超过
百万容器,单容器集群节点数量过万,数据库的峰值超过 54 万笔每秒,
对应 8700 万查询每秒,而实时计算每秒峰值处理消息超过 25 亿条,消息
系统 RocketMQ 峰值处理了超过每秒 1.5 亿条消息。

9
InfoQ 架构师 2020 年 1 月

这些数据背后所代表的,就是上云过程中形成的巨大挑战。针对这些
挑战,阿里巴巴集团从服务器、存储、网络、数据中心等基础设施方面做
了针对性的应对。

自研神龙服务器
核 心 系 统 全 面 上 云 决 定 采 用 了 神 龙 服 务 器。 神 龙 服 务 器 自 研 了
HyperVisor 虚拟化卡来替代软件虚拟化,从而实现无性能损耗的虚拟化架
构。其特点在于:
• 高性能:去掉了虚拟化带来的 8% 的性能损耗;
• 支持二次虚拟化:使多样虚拟化技术 (Kata, Firecracker 等 ) 的探索
和创新成为可能。

在阿里巴巴上云过程中,双 11 期间压力测试显示,高负载压力下的
电商应用,实现 30% 的 QPS 上升,而 rt 也有明显下降,长尾 rt 下降尤其
明显。同时,云化的神龙服务器,促进了运维管理的自动化和在线率水平
的提升。阿里巴巴认为,神龙是容器的最佳载体,神龙 + 服务是无服务器

10
专题
热点| |Topic
Hot

化基础设施的最佳载体。
存储方面,走向了全面云化存储,也即全面存储计算分离。
上云也带来了大规模使用云存储产品:盘古(盘古 2.0),实现了集
团业务的更大规模的存储计算分离。存储计算分离即业务逻辑执行在计算
集群上面,存储部署在存储集群上面。计算和存储集群之间通过高速网络
连接。随着数据处理对存储需求和计算需求在规模、速度、容量和成本等
维度的不断变化,计算与存储分离可以最大限度地解耦并使这两类不同的
关键资源相对独立地扩展和演进,获得更好的弹性、资源效率,同时可以
让应用更容易的获得分布式存储的可靠性。
上云过程中,盘古 2.0 的升级也带来更好的 io 长尾延迟的稳定性,通
过慢盘黑名单、backup read、动态 timeout 等关键技术大幅度的改进了长
尾延迟。

网络:高速混合云
原有的集团安全域,由现有的集团自建网络为主体逐渐转变为以云上
集团的虚拟网络为主体,以 VPC 的方式实现网络隔离混合云网络:为了
实现集团网络与云上 VPC 内业务单元的互通,采用了云专线产品方案,
组成了混合云网络。云专线方案中的虚拟网络网关(xGW 集群),采用
硬件化 HGW 集群。

数据中心:自建网络迁移上云
数据中心自建网络迁移到 VPC,在上云过程中,实现了云 VPC 最大
规模提升 4 倍。安全组性能大幅度优化,企业级安全组最大容量提升 25 倍。
在公司内部,各业务自建的网络之间是相互独立的。随着全站云化,网络

11
InfoQ 架构师 2020 年 1 月

安全域的形态也随之发生变化,TB 级别的云上云下网络流量对穿,从软
件实现的 XGW 升级到软硬结合的 HGW,单节点性能提升 20 倍。

另外,值得指出的是,资源、账号和权限体系对接互通是上云的重要
环节。

上云架构未来演进:云原生
上云已成为趋势,但是核心系统上云只是下一个开始。
企业上云今天已经成为广泛接受的必然趋势,Rightscale state of the
cloud report 2019 显示,94% 企业已经在使用云,其中公有云 91%,on
prem 70%。企业的数字化转型的过程中,利用云的能力的过程也分为不
同的阶段,一般来说也会是走过和阿里上云类似的过程:首先是弹性使用
云的资源,实现部分业务上云,Cloud-hosting。在此过程中,一般是非核
心系统使用云资源。然后涉及到核心系统的云化,这里发生的变化不仅仅
是上云的应用的数量,更是底层基础设施整体使用云平台的能力的过程。
在阿里巴巴看来,未来是云原生化的。

12
专题
热点| |Topic
Hot

什么是云原生?从技术角度讲,云原生技术是一系列的应用构建和运
维最佳实践的集合。云原生技术的生命力在于它聚焦于给用户带来价值,
这些价值分为几个方面:
1. 容器和资源的编排,如 K8s、Container,带来的运维效率提升,
和资源利用率的提升。中心化的编排可以很好的充分编排资源降
低企业成本。
2. 分布式系统的可以弹性扩展的能力 Scalability 以及可靠性。尽管
互联网技术发展了几十年,到今天,分布式、可扩展、可靠的系统,
仍然是很难构建的。也得益于云原生领域开放技术和云的快速发
展,一切正在变得越来越容易。
3. 开放治理和开放的技术,改变了云厂商和用户之间的信赖关系,
越来越多的企业信赖开放标准的云服务。同时云原生也降低了迁
云的成本。
4. 云原生技术所倡导的持续交付,聚焦于企业真正关注的价值,即
迭代创新的速度,time to market。

13
InfoQ 架构师 2020 年 1 月

从上云视角看,云原生(Cloud native)化是最大化使用云的能力,
使企业聚焦于业务的实践。
为什么这么说?我们以阿里核心系统演进为例说明云原生化和使用云
的能力的关系。
阿里巴巴的应用上云前仍然存在应用和基础设施耦合问题,由于采用
自建软件基础设施,配置管理、发布升级、监控观测、流量治理等与业务
应用耦合在一起,对于运维效率、研发演进效率和稳定性都带来了挑战。
我们在上云过程中看到,实现标准的云基础设施和业务应用的全面解耦,

将会带来全面的研发运维效率提升。
那么,使用 in-house 自建基础设施就一定不行吗?
阿里巴巴集团的基础设施也是由专门的团队维护的,也在一定意义上
实现了基础设施和应用的解耦。不是所有的 in-house 的基础设施就不云原
生。事实上,云原生技术的很多发源地,比如 Google 内部的基础设施很
好地实现了和应用的解耦并带来了业界领先的研发运维效率。
但是一般来说,由于内部开发容易忽视基础设施和应用的边界而实现
了过多的非标功能或者倾向于采用更快速落地的方案,这些容易带来基础
设施和应用的更多耦合,不利于长期的演进和效率。例如阿里的容器化虽
然带来了应用发布的标准化的优势,但是在容器化过程中我们采用了富容
器方案加快容器化进程,使容器支持了很多虚拟机使用模式(启停、原地
更新等),带来了容器的可迁移性比较差,容器运行生命周期可变带来运
维成本等。因此运维效率、稳定性和业务的演进效率,在这种耦合中,都
受到了不同程度损失。所以云原生化的关键路径,是实现应用和基础设施

14
专题
热点| |Topic
Hot

的解耦,并且通过采用标准化的云产品方式来支持。
那么基础设施和业务的边界应该在哪里?
阿里巴巴认为边界是在不断的变化过程中,真正的判断标准是业务关
注的边界而非架构边界,基础设施应无须业务持续关注和维护。例如,使
用了容器服务,是否能让业务无须关注底层资源?也未必,取决于资源的
弹性能力是否有充分的支持(否则业务仍需时刻关注流量和资源压力),
也取决于可观测性(否则问题的排查仍需关注底层环境)的能力。因此,
能够让业务无须持续关注的基础设施本身是一次重要技术演进。

无服务器化的基础设施,具有以下三个特点:

阿里核心系统的云原生化演进
阿里巴巴集团的核心系统的云原生架构演进,将继续朝着基础设施解
耦,业务研发运维效率提升,成本下降的整体目标推进。具体来说,围绕
几个重点的方向工作正在展开:

15
InfoQ 架构师 2020 年 1 月

节点托管的 K8s 服务
实现节点运行环境全托管的标准 K8s 基础设施,实现资源和节点运
行环境解耦:通过全托管的节点计算资源,业务无须管理服务器降低运维
成本。今年双 11 集团实现了上海单元的 ASI 升级,带来发布扩容效率提升、
运行时更稳定容器自愈率提升的效果。未来一年将实现核心系统整体 ASI
化。

Service Mesh 化
实现网络、流量管理配置下沉基础设施,与应用充分解耦。Mesh 化
带来的价值:
• 软件基础设施和业务解耦,各自独立演进;
• 全链路精准流量控制和资源动态隔离;
• 提供对应用透明的云安全特性(安全特性解耦)。

应用和基础设施全面解耦

16
专题
热点| |Topic
Hot

阿里巴巴集团核心系统通过云原生化,实现应用基础设施全面解耦,
将有效解决此前存在的运维、研发效率及可迁移性、稳定性风险,这也是
云原生带来的技术赋能。像下图所表示的,应用的关注对象,从繁杂的基
础设施耦合组件,到只需要关于业务逻辑本身。

应用交付的标准化:OAM
今天应用的交付仍然面临挑战:目前云上进行应用管理,需要面对的
是差异性的云基础设施能力和多样化的运行环境 , 需要分别对接和管理,
如 SLB、日志、网络环境、后端依赖等。
今年,阿里云和微软云 Azure 联合发布了一个全新的项目,叫做
Open Application Model OAM:开放应用模型。是业界第一个云原生应用
标准定义与架构模型。在这个模型下,应用的开发人员、运维人员和支持
OAM 模型的平台层,就可以通过一个标准的 OAM 应用描述来进行协作。

通过上云,最大化的使阿里巴巴的业务使用云的技术,通过技术架构
的演进使业务更好的聚焦于自身的发展而无须关于通用底层技术,使业务
研发效率提升,迭代速度更快是技术人的真正目标。正如阿里巴巴集团上
云项目的代号所说的,“云创未来”,通过技术创造新的价值和机会,通过
技术创新带来更好的未来。

作者介绍
张瓅玶,花名谷朴,阿里云原生应用平台集群管理团队负责人,研究员。
2017 年加入阿里巴巴。之前在 Google 的基础设施事业群的集群管理部门
工作了 5 年多,并领导了资源管理和优化团队,负责 Borg 以及基础存储
资源的优化,负责了 FlexBorg, Autoscaling 等多个产品。加入 Google 前在
加州大学伯克利分校从事智能系统的研究工作。

17
InfoQ 架构师 2020 年 1 月

看完这篇你就知道什么是无服务器架构了

作者 Systango 译者 无明

Gartner 最近的一份报告表明,到 2020 年,全球将有 20% 的企业部


署无服务器架构。
这说明无服务器架构不只是一个流行语,而是一种众所周知的云计算
趋势,并且已经在软件世界掀起一场革命。大型厂商(如亚马逊、微软和
谷歌)已经在无服务器架构领域重资投入,追赶革命的浪潮。
与其名字相反,无服务器架构实际上并没有把服务器去掉。那么,究
竟什么是无服务器架构?

什么是无服务器架构?
无服务器架构是指应用程序使用第三方 Function 和服务,但不需要管
理服务器。无服务器架构主要包含了两个方面:
• FaaS(Function as a Service,Function 即 服 务): 包 含 服 务 器 端
业 务 逻 辑 的 无 状 态 Function。 这 些 Function 运 行 在 独 立 的 容 器
里,基于事件驱动,并由第三方厂商托管,如 AWS Lambda 或者
Azure Functions。
• BaaS(Backend as a Service,后端即服务):使用第三方服务(如
Firebase、Auth0)来达成目的。使用 BaaS 的应用程序通常是富客
户端应用程序,如 SPA 或移动 App。客户端负责处理大部分的业
务逻辑,其他部分则依赖外部服务,如认证、数据库、用户管理,
等等。
无服务器架构包含了 BaaS 和 FaaS,不过这篇文章着重关注 FaaS。

18
专题
理论派 | Topic
| Theory

无服务器架构的特点
• 不需要管理服务器;
• 无状态;
• 自动伸缩;
• 没有运营成本;
• 成本由事件驱动;
• 处理第一个事件需要一些启动时间;
• 因为运行时小,所以具有较高的安全性。
• 无服务器的生命周期
下图描述了一个 Function 的生命周期。

无服务器应用程序架构示例

假设有一个简单的线上汽车拍卖应用程序,用户可以登录并出价,拍
卖时间结束时价高者得。
传统上,架构里会包含一个部署了应用程序和前端的单体服务器。

上述架构采用的是瘦客户端方式,所有的业务逻辑(如认证、回话管

19
InfoQ 架构师 2020 年 1 月

理、车辆管理等)都部署在服务器端。
那么,在一个无状态的微服务架构中,这个应用程序又会是什么样子?

原来的单体应用程序被拆分成了多个服务器端组件。
• 认证 Function:这是一个用于管理用户认证(登录)的 Function
(Function,FaaS)。
• 车辆管理服务:一个处理与车辆相关操作的微服务,如列出车辆、
查看车辆信息、比较车辆,等等。这个服务可以使用任意的语言
或框架来开发,它与数据库通信,并且独立运行。
• 车辆出价 Function:这是另外一个 Function,也与数据库通信,录
入用户出价记录。
• API 网关:所有服务的入口点和反向代理。来自客户端的请求会
先到达网关,网关根据路由规则将请求重定向到特定的服务。
在将服务拆分成微服务或 FaaS 时,需要考虑到业务逻辑、负载、规
模等方面的因素。
上述的例子描述了无服务器架构和基于无服务器架构设计微服务时的
大致过程。

无服务器架构与 PaaS
平台即服务(Platform as a Service)是另一个不需要开发人员管理服
务器(包括硬件和软件)的架构莫斯。正因为如此,开发人员容易把无服
务器架构和 PaaS 混为一谈。接下来,我们来看一看它们之间的相似点和

20
专题
理论派 | Topic
| Theory

不同点。

相似点
• 开发人员不需要管理服务器。
• 开发人员只要关注应用程序代码本身。

不同点
• PaaS 提供了更为可控的部署方式,而无服务器的部署则更为严格。
• 无服务器架构可以自动伸缩,而 PaaS 的伸缩需要进行配置。
• 无服务器架构的成本是由事件驱动的,而 PaaS 是固定的。
• PaaS 应用程序在部署之后会一直运行,并马上开始处理请求,而
无服务器需要等待第一个事件,具体取决于事件的发生频率。

用例
无服务器的应用应该不仅限于某个领域、业务或架构。在进行应用程
序架构时,你需要考虑多个因素,这些因素同样适用于无服务器架构。
• 成本——无服务器架构非常节约成本,具体取决于实际的负载。
• 服务器管理——无服务器架构可以极大地降低用于管理服务器的
运营成本。
• 伸缩——无服务器架构可以自动伸缩。
• 响应时间——FaaS 需要一些初始化时间。如果负载很小(比如一
个小时只有一个事件),每个请求都会经历冷启动,导致整体响
应变慢。
更快的发布周期——因为这些 Function 都是很小的单位,发布周期就
变得很短。
以下是一些常见的用例
• Web 应用程序;
• 批处理和调度;
• 移动和 IOT 后端;
• 聊天机器人。

21
InfoQ 架构师 2020 年 1 月

为什么要采用无服务器架构?
使用无服务器架构和 FaaS 有以下这些好处。
• 减少服务器管理成本;
• 减少运营成本;
• 自动伸缩;
• 比不间断运行的服务更安全;
• 成本由请求或事件数来决定;
• 更简单的打包和部署流程;
• 缩短发布周期;
• 开箱即用的监控。

无服务器架构的限制
与其他任何一种技术架构一样,无服务器架构也存在同样的限制。
• 启动延迟;
• 厂商锁定,对服务器缺乏控制;
• 性能优化局限于代码内部;
• 执行时间限制(AWS Lambda 的执行时间限制为 15 分钟);
• 成本不可预测;
• 开发环境和生产环境不一样;
• 测试和调试更为复杂。

总结
无服务器架构是一种架构风格,通过 FaaS 将业务逻辑从长期运行的
组件中移到临时的 Function 里。它可以解决很多架构和运营问题,简化开
发者和运维人员的工作。
与其他解决方案一样,无服务器架构并不是银弹。它无法直接取代现
有的组件,在决定是否要采用无服务器架构之前需要先分析一下自己的业
务和技术需求,通盘考虑各种优点和缺点。

22
专题
推荐文章 | Topic
| Article

当心“中间件”

作者 Richard Marmorstein 译者 刘雅梦

“给一个小男孩一把锤子,他就会发现他遇到的每件事都需要锤击。”
对于“中间件”,我们从来没有真正停下来思考过它的利弊。这似乎是一件“正确”
的事情:框架希望我做的事情,我就照做了。本文通过 HTTP API 探讨了“中
间件”使用的利弊。

“给一个小男孩一把锤子,他就会发现他遇到的每件事都需要锤击。”
——Abraham Kaplan
当你编写一个 HTTP API 时,通常会描述两种行为:
• 应用于特定路由的行为。
• 应用于所有或多个路由的行为。

一个好主意:控制器和模型
在我所见过的应用程序中,应用于特定路由的行为通常划分为“控制器”
和一个或多个“模型”。理想情况下,控制器是“瘦”的,本身不会做太多工作。
它的任务是将请求所描述的动作从 HTTP “语言”转换为模型“语言”。
为什么分成“模型”和“控制器”是一个好主意呢?因为受约束的数据比
不受约束的数据更容易推导。
这相关的一个经典例子是编译器的阶段,所以让我们稍微探讨一下这
个类比。简单编译器的前两个阶段是词法分析器(lexer)和解析器(parser),
词法分析器获取完全不受约束的数据(字节流)并发出已知的标识,如
QUOTATION_MARK 或 LEFT_PAREN 或 LITERAL “foo”,而解析则是获
取这些标识流并生成语法树。将表示 if 语句的语法树转换为字节码是很

23
InfoQ 架构师 2020 年 1 月

简单的。但将表示 if 语句的任意字节流直接转换为字节码就不那么简单
了……
在这个类比中,HTTP 请求就像是不受约束的字节流。它们有一些结
构,但是它们的主体可以包含任意字节(对任意的 JSON 进行编码),它
们的 header 可以是任意字符串。我们不想在任意请求的操作上表达业务
逻辑。用“Accounts”、“Posts”或任何领域对象来表示业务逻辑要自然得多。
因此,在我看来,控制器的工作类似于词法分析器 / 解析器。它的工作是
采取一个不受约束的数据结构来描述一个动作,并将其转换为一种更受约
束的形式(例如,“对 account 对象的 .update 方法的调用,随之有一条包
含了“email address”和“bio”字符串的记录)。
这种类比的奇怪之处在于,虽然词法分析器 / 解析器返回了它们从字
节流生成的语法树,但是 HTTP 控制器通常不会返回对应于其输入 HTTP
请求的模型方法调用的表示(当然它可以实现……但这种想法就是另一篇
博客文章了),而是直接执行。不过,这应该对咱们这个类比没什么影响。

一个有争议的想法:中间件
不过,控制器通常只会涉及到应用于单一路由的行为。根据我的经验,
应用于多个路由的行为往往被组织成一系列“中间件”或“中间件堆栈”。这
是一个坏主意,因为把控制器放在模型前面是一个好主意。也就是说,中
间件操作的是非常不受约束的数据结构(HTTP 请求和响应),而不是易
于推导和组合的受约束的数据结构。
虽然我假设我们对中间件都比较熟悉,但还是在此做个简单介绍吧:
• 将 HTTP 请求和(正在进行的)HTTP 响应作为参数
• 没有有意义的返回值
因此,操作必须通过修改请求或响应对象、修改全局状态、引发一些
副作用或抛出错误来进行。
我们需要抛弃在模型 / 控制器架构中使用的关于尝试操作受约束数据
的知识。对于“中间件”,在路由之前,HTTP 请求是无处不在的!
如果我们的中间件描绘简单、独立的操作,我仍然认为它是一种糟糕
的表达方式,但这在大多数情况下还是好的。当操作变得复杂且相互依赖
时,麻烦就开始了。

24
专题
推荐文章 | Topic
| Article

例如,如下这些操作可以称为简单操作:
1. 速率限制为每个 IP 每分钟 100 个请求。
2. 如果请求缺少有效的授权 header,则返回 401
3. 所有传入请求的 10% 记录日志
在 Express 中以中间件的形式进行编码,如下所示(代码仅用于演示,
请不要尝试运行它)
const rateLimitingMiddleware = async (req, res) => {
const ip = req.headers['ip']
db.incrementNRequests(ip)
if (await db.nRequestsSince(Date.now() - 60000, ip) > 100) {
return res.send(423)
}
}

const authorizationMiddleware = async (req, res) => {


const account = await db.accountByAuthorization(req.
headers['authorization'])
if (!account) { return res.send(401) }
}

const loggingMiddleware = async (req, res) => {


if (Math.random() <= .1) {
console.log(`request received ${req.method} ${req.path}\n${req.
body}`)
}
}

app.use([
rateLimitingMiddleware,
authorizationMiddleware,
loggingMiddleware
].map(
// Not important, quick and dirty plumbing to make express play
nice with
// async/await
(f) => (req, res, next) =>
f(req, res)
.then(() => next())
.catch(err => next(err))
))

25
InfoQ 架构师 2020 年 1 月

我所提倡的大致是这样的:
const shouldRateLimit = async (ip) => {
return await db.nRequestsSince(Date.now() - 60000, ip) < 100
}

const isAuthorizationValid = async (authorization) => {


return !!await db.accountByAuthorization(authorization)
}

const emitLog = (method, path, body) => {


if (Math.random() < .1) {
console.log(`request received ${method} ${path}\n${body}`)
}
}

const mw = async (req, res) => {


const {ip, authorization} = req.headers
const {method, path, body} = req

if (await shouldRateLimit(ip)) {
return res.send(423)
}

if (!await isAuthorizationValid(authorization)) {
return res.send(401)
}

emitLog(method, path, body)


}

app.use((req, res, next) => {


// async/await plumbing
mw(req, res).then(() => next()).catch(err => next(err))
})
我没有将每个操作注册为自己的中间件,并依赖 Express 按顺序调用
它们,传入不受约束的请求和响应对象,而是将每个操作作为函数来编写,
将其约束输入声明为参数,并将其结果描述为返回值。然后我注册了一个
中间件,负责将 HTTP“翻译”成这些操作的更受约束的语言(并执行它们)。
我相信,它可以类比为“瘦控制器”。
在这个简单的例子中,我的方法并没有明显的优势。所以让我们来引
入一些复杂的情况吧。

26
专题
推荐文章 | Topic
| Article

假设有一些新的需求
1. 有些请求来自“管理员”。
2. 来自管理员的请求 100% 都应该被记录下来(这样调试就更容易了)
3. 管理请求也不应该受到速率限制。
最简单的方法是在记录日志时进行查找和检查,并限制中间件的速率。
const rateLimitingMiddleware = async (req, res) => {
const account = await db.accountByAuthorization(req.
headers['authorization'])
if (account.isAdmin()) {
return
}
const ip = req.headers['ip']
db.incrementNRequests(ip)
if (await db.nRequestsSince(Date.now() - 60000, ip) > 100) {
return res.send(423)
}
}

const loggingMiddleware = async (req, res) => {


const account = await db.accountByAuthorization(req.
headers['authorization'])
if (account.isAdmin() || Math.random() <= .1) {
console.log(`request received ${req.method} ${req.path}\n${req.
body}`)
}
}
但这并不能令人满意。只调用一次 db.accountByAuthorization,避免
来来回回访问三次数据库,不是更好吗?中间件不能产生返回值,也不能
接受其他中间件产生的参数值,因此必须通过修改请求(或响应)对象来
实现,如下所示:
const authorizationMiddleware = async (req, res) => {
const account = await db.accountByAuthorization(req.
headers['authorization'])
if (!account) { return res.send(401) }
req.isAdmin = account.isAdmin()
}

const rateLimitingMiddleware = async (req, res) => {


if (req.isAdmin) return

27
InfoQ 架构师 2020 年 1 月

const ip = req.headers['ip']
db.incrementNRequests(ip)
if (await db.nRequestsSince(Date.now() - 60000, ip) > 100) {
return res.send(423)
}
}

const loggingMiddleware = async (req, res) => {


if (req.isAdmin || Math.random() <= .1) {
console.log(`request received ${req.method} ${req.path}\n${req.
body}`)
}
}
这应该会让我们在道德上感到不安。首先,修改是不好的,或者至
少在最近它已经过时了(在我看来,这是正确的)。其次,isAdmin 与
HTTP 请求没有任何关系,因此将它偷放到一个声称代表 HTTP 请求的对
象上似乎也不太合适。
此外,还有一个实际问题。代码被破坏了。rateLimitingMiddleware 现
在隐式地依赖于 authorizationMiddleware,在 authorizationMiddleware 运行
之后它就会运行。在我修复该问题并将 authorizationMiddleware 放在第一
位之前,将不能正确地免除对管理员的速率限制。
如果没有中间件,那会是什么样子的呢?(好吧,只有一个……)
const shouldRateLimit = async (ip, account) => {
return !account.isAdmin() &&
await db.nRequestsSince(Date.now() - 60000, ip) < 100
}

const authorizedAccount = async (authorization) => {


return await db.accountByAuthorization(authorization)
}

const emitLog = (method, path, body, account) => {


if (account.isAdmin()) { return }
if (Math.random() < .1) {
console.log(`request received ${method} ${path}\n${body}`)
}
}

const mw = async (req, res) => {


const {ip, authorization} = req.headers

28
专题
推荐文章 | Topic
| Article

const {method, path, body} = req

const account = authorizedAccount(authorization)


if (!account) { return res.send(401) }

if (await shouldRateLimit(ip, account)) {


return res.send(423)
}

emitLog(method, path, body, account)


}
这里,如下写法包含有类似的bug:
if (await shouldRateLimit(ip, account)) {
...
}
const account = authorizedAccount(authorization)
bug 在哪呢? account 变量在使用之前需要先定义,这样可以避免异
常抛出。如果我们不这样做,ESLint 将捕获异常。同样地,这也可以通过
定义具有约束参数和返回值的函数来实现。在无约束的“请求”对象(任意
属性的“抓包”)方面,静态分析帮不上你多大的忙。
我希望这个例子能够说服你,或者与你使用中间件的经验产生共鸣,
尽管我例子中的问题仍然非常轻微。但在实际应用程序中,情况会变得更
糟,尤其是当你将更多的复杂性添加到组合中时,如管理员能够充当其他
帐户、资源级别的速率限制和 IP 限制、功能标志等等。

黑暗从何而来?
希望我已经让你相信中间件是糟糕的了,或者至少认识到它很容易被
误用。但如果它们是如此糟糕,它们又怎么会如此受欢迎呢?
我曾写过一些欠考虑的中间件,对我来说,我认为它归根结底是“锤
子定律”。正如开篇所述:“给一个小男孩一把锤子,他就会发现他遇到的
每件事都需要锤击。”中间件就是锤子,而我就是那个小男孩。
这些 Web 框架(Express、Rack、Laravel 等)强调了“中间件”的概念。
我知道在请求到达路由器之前,我需要对它们执行一系列操作。我看到“中
间件”似乎是为了这个目的。我从来没有真正停下来思考过它的利弊。这
似乎是一件“正确”的事情:框架希望我去做什么,我就做了什么。
我认为还有一种模糊的感觉,那就是用框架希望的方式能解决问题也

29
InfoQ 架构师 2020 年 1 月

是好的,因为如果你这样做了,也许就可以更好地利用框架提供的其他特
性。根据我的经验,这种希望很少能实现。
在其他情况下,我也会陷入这种思维。例如,当我想跨多个 CI 作业
重用代码时,我使用了 Jenkins 共享库。我写了 }[&%ing Groovy(一种我
讨厌的语言)来做这个。如果我不知道“Jenkins 共享库”存在的话,我应该
做些什么,我应该怎么办。仅仅是用我想用的任何编程语言来编写操作(在
这种情况下,可能是用 Bash 进行编程),并使它们可以通过 shell 在 CI
作业上调用。
所以更广泛的教训是,试着通过你自己的思维意识到这种趋势。使用
工具,但别让工具利用你。尤其是如果你是一个更有经验的程序员,并且
按照工具“想要”的方式使用它似乎不怎么正确时,那它可能就真的不正确。
使用函数,将它们需要的东西作为参数,并将其结果放在返回值中。
如果可以的话,编写编译器之类的应用程序,这也是一个深刻的教训。

30
专题
推荐文章 | Topic
| Article

我们是怎么做到每小时推送百万级通知的
作者:Soham Kamani 译者:无明

推 送 通 知 是 让 用 户 立 即 接 收 到 事 件 的 一 个 非 常 有 效 的 工 具。 在
Gojek,我们每天需要处理 300 多万个订单,跨 20 多款产品。
可以想象的是,我们每天推送的通知数量有多大——大概每小时 1
百万个。这篇文章将介绍我们在处理如此体量的推送通知时所面临的挑战,
以及我们的解决方案。
体量大还只是其中的一个方面,在 Gojek,我们还需要面对一些独有
的问题。

多个应用程序
Gojek 不 只 有 一 个 App, 除 了 用 户 App, 我 们 还 有 GoLife、 司 机
App、商家 App,还有服务商 App。

当我们的系统要推送通知,要么是推给某个用户的 App(例如,推给
GoLife 的通知不会被推到 Gojek 上),要么是推给所有的 App(例如促销
通知)。
我们的系统需要足够灵活,能够在广播和单独推送之间自由地选择。

31
InfoQ 架构师 2020 年 1 月

多个通知服务提供商
因为我们的用户 App 需要支持 iOS 和 Android 两个平台,所以也需
要支持多个通知系统。
Android 平台我们使用了 FCM(Firebase Cloud Messaging)和 GCM
(Google Cloud Messaging),iOS 平 台 我 们 使 用 了 APNS(Apple Push
Notification Service)。

每一个通知服务提供商都为不同的 App 提供了不同的 API 秘钥和令


牌。例如,GoLife 和 Gojek 使用的 FCM API 秘钥就不一样。

一个用户多个设备
我们允许一个用户同时登录多个设备,所以通知需要被推送给用户已
登录的所有设备上,这就存在之前的两个问题:
• 用户可以在单个设备上登录多个 App(Gojek 和 GoLife);
• 用户可能会登录多个设备,每个设备需要使用不同的通知服务提
供商。例如,用户可以在 Android 设备和 iOS 设备上登录 Gojek。

多个需要推送通知的服务
Gojek 采用了微服务架构,我们想要让每个服务都能推送通知,不需
要操心多设备和多服务提供商问题。

32
专题
推荐文章 | Topic
| Article

为了解决上述问题,并尽可能保
持 API 简单,我们的通知系统被分为
三个组件:
• 通知服务器——提供通知推
送 API, 并 将 通 知 推 送 到 作
业队列中;
• 令牌存储——保存已登录用
户的设备和设备令牌数据;
• 通知处理器——处理作业队
列中的消息,并将消息发送
给通知服务提供商。
每个组件都解决了上述的一部分
问题,接下来,我们来深入介绍这些
组件。

令牌存储
用户在登录 App 后,App 会使用设备令牌和 App ID 调用令牌存储
API。
在用户退出时,这些记录会被删除。
令牌存储用于决定向用户的哪些设备推送通知。

通知服务器
这是 HTTP 服务器,提供用于推送通知的 API。

33
InfoQ 架构师 2020 年 1 月

为了简单起见,API 要求把用户 ID 和 App ID 放在 HTTP 头部,把通


知信息放在请求体里:
POST http://<base_url>/notification
user_id: <user_id>
application_id: <application_id>
{
"payload": {},
"title": "You driver is here",
"message": "Please meet your driver at the pickup point"
}
服务器从令牌存储获取所有的用户设备信息,然后为每个用户设备安
排一个调度作业。
通知服务器为系统提供了外部接口,需要推送通知的服务只要通过用
户 ID 来调用它的 API,剩下的事情由通知服务负责处理。

作业队列
我们使用 RabbitMQ 作为作业队列,并为每一种 App ID 和通知类型
创建了单独的队列。

34
专题
推荐文章 | Topic
| Article

分配单独的队列是很重要的,因为我们要为每一种 App 和通知类型


做好故障隔离。例如,如果 com.gojek.app 的 FCM 令牌过期,就不会影响
到 com.gojek.life 或者 com.gojek.driver.bike 的作业。

通知处理器
处理器进程从作业队列里拉取消息,把它们发送给对应的通知服务提
供商。
为了保持代码简单,并能够支持不同的服务提供商,我们定义了统一
接口:
type PushService interface {
Push(ctx context.Context, m PushRequest) (PushResponse, error)
}
Push 方法接收一个请求对象,并返回一个响应对象。
请求对象包含了与接收方和通知(比如过期时间、标题和文本)有关
的信息。
type PushRequest struct {
DeviceID string
Title string
Message string
Payload map[string]interface{}
//其他参数
}
响应消息里包含了通知是否发送成功的信息:
type PushResponse struct {
Success bool
ErrorMsg string
}
然后为不同的服务提供商实现接口。例如,FCM 和 APNS 对应的实
现看起来像下面这样:
type FCMProvider struct {
//配置信息,比如API令牌和URL端点
}
func (p *FCMProvider) Push(ctx context.Context, m queue.Message)
(notification.PushResponse, error) {
//发送通知给FCM服务器
}
type APNSProvider struct {

35
InfoQ 架构师 2020 年 1 月

//配置信息,比如API令牌和URL端点
}
func (p *APNSProvider) Push(ctx context.Context, m queue.Message)
(notification.PushResponse, error) {
//发送通知给APNS服务器
}
通知处理器负责选择对应的通知服务提供商,并将消息发送给它们。

结论
在面对这些挑战时,我们找出其中的一些常用模式,把它们抽离成不
同的服务,将一个相对复杂的问题变成了一系列简单且易于管理的服务。
每当一个核心逻辑需要不同的实现时,我们就把它抽离成单独的服务:
• 多个设备问题通过令牌服务来解决;
• 多个 App 问题通过统一的通知服务器接口来解决;
• 多个通知服务提供商通过单独的作业队列和通知处理器来解决。
最终,我们构建了一个每小时能够处理 1 百多万个推送通知的系统。

36
观点专题 | Topic
| Opinion

DDD:架构思想的旧瓶新酒
作者 小智

DDD 和 DSL、DCI 的 关 系 是 什 么? 开 发 团 队 为 何 需 要 DDD ? 它


与微服务与中台又有着怎样的联系?目前业界实践 DDD 最大的问题是
什么? 11 月 30 日,在由 ThoughtWorks 举办的领域驱动设计峰会 DDD-
China 2019 上,InfoQ 记者带着这些问题对中兴通讯资深软件架构师张晓
龙进行了采访。

DDD、DSL 和 DCI
DDD 概念最早提出于 2004 年,作为一种软件开发的指导思想,DDD
对软件开发带来了诸多可能与方向,张晓龙认为 DDD 为软件开发带来的
好处主要有以下几点:
• 首先,最大好处就是所有参与者围绕一个统一一致的领域模型工
作,传统的分析模型和设计模型不再割裂,不管是做设计、做分
析还是写代码、写文档,脑海中所构建的画面都是一致的。
• 第二,DDD 是一个软件开发过程,它显式地把领域和设计放到了
软件开发的核心,软件人员和业务人员被受到同样的重视,他们
合作来构建领域模型,使得软件的交付质量更高且维护成本更低;
• 第三,DDD 提出的分层架构,有效分离了业务复杂度和技术复杂
度,凸显了领域模型,使得领域层的代码和领域模型保持高度一致;
• 第四,统一语言非常重要,每个概念在各自的上下文中是清晰的
无歧义的,同时要控制领域模型的复杂度,于是 DDD 在战略上提
出了分离子域(问题域空间)和拆分 BC(解决方案空间)的模式,

37
InfoQ 架构师 2020 年 1 月

BC 间通过 Context Mapping 来集成;


• 第五,DDD 在战术层面提出了很多模式(聚合,实体,值对象,服务,
工厂,仓储),对领域模型中的元素进行了分类,并给出了每类
元素在领域模型中的职责和特征,降低了领域模型的构建成本。
张晓龙此前曾在 DDD-China 峰会和 ArchSummit 全球架构师峰会上分
别 做 过《 当 DDD 遇 上 DSL(Domain-Specific Language)》、《 当 DDD
遇 上 DCI(Data,Context, Interactive)》 的 演 讲, 在 他 看 来,DDD 和
DSL、DCI 之间存在极强的关联性。
DDD 和 DSL 的融合有三点:
• 面向领域;
• 模型的组装方式;
• 分层架构演进。
DSL 可以看作是在领域模型之上的一层外壳,可以显著增强领域模
型的能力。它的价值主要有两个,一是提升了开发人员的生产力,二是增
进了开发人员与领域专家的沟通。举个例子:想让 BA 负责流程契约的设
计,该流程契约是一个活文档,可以跑测试,而 BA 不熟悉宿主语言。于是,
我们设计了一种外部 DSL 来专门描述流程契约,对 BA 非常友好,学习
成本也很低(不超过 5 分钟就可以学会),最后发现 BA 很快就广泛使
用了起来。外部 DSL 并不一定要定义新文法,我们直接复用了 plantUML
文法,安装该插件可以自动生成序列图,非常棒!对于外部 DSL,需要
自己实现一个解析器将 DSL 文法解析成语法树,再根据语法树生成语义
模型。语义模型可以看作领域模型(严格的讲语义模型是领域模型的子集),
外部 DSL 就是对领域模型的一种组装方式。
DCI 的作用主要体现在两方面:
首先,DCI 助力 DDD 战术设计:
1. 显式地对 ROLE 建模,解决了贫血模型与充血模型之争;
2. 一个聚合可以支持哪些 ROLE,一个 ROLE 可以由哪些聚合扮演,
一个场景下哪些聚合要扮演哪些角色;
3. 当 Aggregate 内部实体行为比较多时可以嵌套使用 DCI 来拆分和
组合;

38
观点专题 | Topic
| Opinion

其次,DCI 助力 DDD 代码落地:


1. 对 象 就 是 Data,Client 为 Context, 对 象 在 Client 中 的 行 为 就 是
ROLE。
2. 根据正交设计原则得到小类(素材库),根据多重继承(only
C++)或依赖注入来组合素材,不管是行为类还是数据类,都按
Role 的方式来组合,对像仅仅组合 Role 并注入依赖;
3. 小类大对象:类作为一种模块化手段,遵循高内聚,低耦合,让
软件易于应对变化;对象作为一种领域对象的的直接映射,解决
了过多的类带来的可理解性问题,让领域可以指导设计,设计真
正反映领域;领域对象需要真正意义上的生命周期管理。
张晓龙认为,DCI 对一些开发人员的影响可能比 DDD 和 DSL 还大,
因为开发人员每天都在不断倒腾代码,想让代码的组合性更强,以便快速
应对需求的变化。

开发团队真的需要 DDD
DDD 思想贯穿了整个软件开发的生命周期,包括对需求的分析、建模、
架构、设计,和最终的代码实现,甚至对代码的测试与重构。代码是业务
的核心资产,不管是否特性团队,开发团队肯定是代码的编写者和守护者。
对于开发团队而言,需要关注以下几点:
• 首先是统一语言,让团队成员可以做到无障碍的沟通,不管是什
么角色都能基于同样的画面进行讨论;
• 其次是团队中各个角色都围绕领域模型开展工作;
• 第三是代码物理设计容易标准化,比如说在分层设计时,基础设
施层怎么设计,应用层怎么设计,DTO 应该放在哪儿,领域层中
各个建模元素如何组织?
更进一步,在分层架构里,应用层更加关注横切面的东西,比如说要
上报一个告警,要给用户发送一个 Email,这些最好都集中放到应用层里面。
但触发是在领域层发生的,应用层怎么知道?通过领域事件来实现依赖反
转,即应用层订阅领域事件,领域层发布领域事件。
在中兴通讯,核心业务属于通信行业,DDD 的应用场景跟互联网企
业有着很大差别:

39
InfoQ 架构师 2020 年 1 月

• 嵌入式软件;
• 兼业务复杂性和技术复杂性;
• 软件规模大,功能复杂,特性交叉;
• 高质量,高性能,高可靠等要求。
张晓龙举例提到,中兴通讯在开发团队中实践 DDD 的经验具体而言
有以下几点:
1. 领域专家下团队,和团队一起交流和协作;
2. 教练指导,开展战训营,定期 review;
3. 架构、设计、编码和工程实践:(1)DCI,DSL,正交设计,组
合式设计;(2)编码规范和纪律;(3)嵌入式 C/C++ 最佳实践;
(4)软件工程能力:开发者测试,小步安全流畅的重构,持续交
付流水线,每日 Code Review。

DDD 与微服务
DDD 概念提出距今已经有 15 年的历史,前十年时间都一直处于不
温不火的状态,而在最近几年才开始大行其道。张晓龙表示,中兴通讯
在 2012-2015 年期间也有过一些成功的案例,但对于整个业界来说了解的
人并不多。他拿 DDD-China 峰会举例解释:这次峰会的参会者有 500 人
的规模,而我们假设峰会在 2015 年之前举办的话,估计参会者不会超过
100 人。因此,我们可以断定是微服务的热风让人们重新发现了领域驱动
设计的价值。
微服务架构从提出以来一直没有很好的理论支撑如何合理地划分服务
边界,人们常常为服务要划分多大而争吵不休。而 DDD 被发现恰好可以
弥补微服务的营养不良:(1)服务最大不要大过一个 BC,否则服务内可
能会存在有歧义的领域概念;(2)服务最小不要小过一个聚合,否则会
(3)服务间最好通过 Domain Event 来进行交互,
引入分布式事务的复杂度;
这样可以让服务保持松耦合。微服务和 DDD 的结合,让微服务架构看起
来似乎更加稳健了。
“微服务就像是 DDD 的心上人,使得 DDD 真正焕发起了青春。”张
晓龙这样解释。
对于业界目前流行的中台概念,张晓龙同样也有自己的看法:

40
观点专题 | Topic
| Opinion

中台和 DDD 不是同一个层面的东西,不能为了把它们联系在一起,


而强行找相似点。中台实际上就是多条业务线的共同需求,比如对于滴滴
公司来说,快车、专车和出租车等业务都是微服务架构,这些业务的很多
服务是相似的,考虑将这些服务从各个前台下沉到统一的平台,这个平台
就是中台。中台要考虑各个前台的需求,所以复杂性变高了。
中台是一种企业级的架构模式,从企业全局整体视角来看架构全貌,
而 DDD 是一种主流的软件开发方法,用来应对软件的核心复杂性。中台
架构可以看作是微服务架构的延伸和发展,服务复杂性很高,所以更需要
用 DDD 的方式去设计和建模,但二者之间并不是相同层面的概念。

DDD 的困局
最近几年 DDD 的火爆也给业界开发团队带来了一些迷思,为什么我
的 DDD 推行不下去?为什么我的 DDD 做起来总是跟敏捷一样,最后都
变了味?
张晓龙总结了 DDD 目前面临的几大困局:
• 首先是领域案例面比较窄。目前业界的 DDD 实践案例并不多,而
且很多案例是偏向互联网领域的,对于工业领域、嵌入式领域和
操作系统领域基本没有涉及;
• 第二,DDD 书籍非常少,而且大多数书籍是以 Java 或 C# 写的。
如果开发团队用的是 C、C++、Python 或 Go 语言,基本没有可参
考的书籍,难度也就更大一些(尤其是 C 和 C++);
• 第三,各个巨头公司,比如 Google,微软,BAT 等,很少组织、
参与或赞助 DDD 峰会,没有形成引导作用,业界自然也就少有跟
随效应;
• 第四,开发团队要么找不到领域专家,要么领域专家无法与开发
团队长时间保持沟通,导致实践中出现偏差;
• 第五,DDD 落地有一定的门槛,对开发者的技能和素质都有较高
的要求。
针对以上几大困局,张晓龙也给出了自己的解决方案:
• 培训 OOA、OOD 和 OOP 的基本知识,并实战演练,不断弥补与
高手的 gap ;

41
InfoQ 架构师 2020 年 1 月

• 领域专家和团队一起工作,确保大家头脑中的画面是一致的;
• DDD 建模要有文档交付物,并和代码同步演进,以便对代码不熟
悉的人员也能看到并理解领域驱动设计成果的全貌。
软件开发没有银弹,DDD 也不是万能的。如果开发团队真的决定用
DDD 的思想指导软件开发,就一定要跟随时代的脚步,吃透 DDD 这个旧
瓶里装的新酒。

42
观点专题 | Topic
| Opinion

我们为什么用 gRPC 取代了 Kafka

作者 Stephanie Sherriff 译者 无明

本文作者的技术团队第一个 App 高度依赖 Kafka,她希望这个 App


能够支持审计,具有很高的稳定性,从长远看,随着用户量的增长也能够
轻松地处理高负载。但 Kafka 同样带来了基础设施、系统维护和支持方面
的成本问题,最终他们选择了用 gRPC 取代了 Kafka。值得说明的是,二
者的技术选型并没有明确的优劣之分,有的只是业务场景和需求所带来的
取舍。

保持简单的技术选型更重要
程序员职业生涯的大部分时间都用于维护和修复已有的系统。运气好
的话,如果你加入了一家初创公司,就有机会从头开始构建全新的系统。
这种兴奋是无可言喻的,因为你有了一个可以“重新来过”的机会,终于可
以把旧系统那些让你感到反胃的东西扔掉了。
你开始思考可以使用哪些新技术,有些是你已经尝试过的,有些是你
在某篇文章上刚刚看到的,有些是你在之前项目中用得很成功的。也就是
在这个时候,你感到了稍许的轻松,起码在一段时间之内,你或者你的团
队会一直维护这个新系统。
于是你开始想:这个跟这篇文章的标题又有什么关系?我为什么讨厌
Kafka ?我其实不讨厌 Kafka,或者更确切地说,是有那么一点点。从使
用体验看,Kafka 并没有给我留下非常好的印象,而且我们也只是在用它
解决原本就不存在的问题。

43
InfoQ 架构师 2020 年 1 月

不 管 你 有 没 有 在 遵 循 KISS(Keep It Simple Stupid, 保 持 简 单) 或


YAGNI(You Aren’t Gonna Need It,你其实不需要它)原则,又或者你只
是想尝试一下,我都希望你能够从我们犯过的错误中学到一些东西,并在
开始下一个项目时意识到,保持简单是多么的重要。

背景
我们的第一个 App 高度依赖 Kafka,因为我们希望我们的 App 能够
支持审计,具有很高的稳定性,从长远看,随着用户量的增长也能够轻松
地处理高负载。
但 Kafka 却让整件事情变复杂了。我们的核心系统是一个投资系统,
大多数时候不会直接与用户发生交互,所以我们没有必要创建一个高负载
系统来支持用户。我们的团队本来就不大,时间又紧,在发布第一个版本
时没有必要花费额外的开销。大多数情况下,真正到了需要处理大流量的
时候,你可能已经把系统都重写过了。而如果能够走到这一步,说明离成
功不远了。
到了这个时候,被忽略的往往是基础设施、系统维护和支持方面的成
本问题。

问题
采用新技术需要时间
每一种技术都有它自己的特点和正确的“打开”方式。如果你团队里没
有人熟悉一项新技术,那么很可能在一开始不知道怎样做才是对的。等你
知道该怎么做的时候,可能已经需要重新来过了。
即使你团队里有人熟悉这项新技术,要让其他人都熟悉起来也是需要
时间的。有时候采用新技术是势在必行的,但你要让整个团队都知道,并
一起讨论这样做的必要性,另外也要注意不要在短时间内引入太多新技术。

技术栈越简单就越容易上手
对于新手来说,简单且在业内随处可见的技术相比复杂的技术栈更容
易上手。吃透一家公司的业务逻辑本来就需要很长时间,如果在技术栈层
面再加入额外的复杂性,那么开发团队的产出必然会打折扣。在初创公司,
一方面人员流动率很高,一方面公司又想快速成长,这种情况就会越加严
重。

44
观点专题 | Topic
| Opinion

方便查找问题
调试生产环境的应用程序通常不是件容易的事情。如果应用程序本身
很复杂,就会带来更多的问题。
如果团队里只有一个人知道怎么处理这些问题,那他就要一直盯着。
如果这个人生病或者离开公司,对于团队来说就是一个严重的打击。而对
于这个人来说,老是处理这些问题也是件很枯燥的事情,因为他也想做一
些不一样的事情。
另一个是处理问题需要很长的时间,这可能会导致用户不开心,特别
是如果你是一个新手,很可能会让用户暴跳如雷,然后留下不好的评论,
或者跟他们的朋友说你的坏话。

方便维护和修改
简单的应用程序不仅开发起来容易,修复、更新和添加新特性也很容
易。有时候,对一个复杂的遗留应用程序进行更新通常会被列为“高难度”
的难题,因为没有人心里有底,不敢去碰它,所以迟迟不敢动手。所以说,
没事不要去摸老虎的屁股,小心它回头过来咬你。

我们是如何解决这些问题的
我的解决方案是改用 gPRC ,并把 Kafka 从我们的技术栈中移掉。至
于我们是怎么做的就说来话长了,我们做了很多规划,整个团队花了三到
四周的时间。我们是幸运的,因为公司给了我们足够的时间来偿还这笔巨
额技术债务。
改造给我们带来了很可观的好处。新手上手的时间缩短了,他们跟支
持工程师在一起一两个礼拜就能够对公司的业务有所了解。团队成员可以
轮流提供支持,因为每个人对代码多多少少都了解一些,至少知道如何搜
索日志,找到问题的根源。支持 SLA 时间大幅下降,轮流值班也限制了
知识孤岛的形成,支持人员也不再感到无聊,因为他们每次做的事情可能
不一样。另外,我们的服务器数量减少了 50%。
每家公司都有自己的问题要解决,有时候采用复杂的技术架构也是在
所难免的。但你需要确保采用技术是为了解决问题,而不是制造问题。代
码越少,bug 就越少。

45
InfoQ 架构师 2020 年 1 月

大数据容器化,头部玩家尝到了甜头?

大数据的需求热度,从来都是这个时代的风口浪尖。然而由于大数据
系统的复杂性,一度导致业界大数据已死的各种声音质疑不断。尤其当
MapR 被 HPE 收购,Cloudera 公司股票持续断崖式下跌,叫衰之声进而进
一步放大。
其实,大数据的需求一直在,只是传统的大数据实现系统需要考虑重
新构建,而容器依靠其自身标准化、一次构建随处运行的能力,非常适合
用于大数据系统的构建和管理。容器技术当前正是那只火遍全球的当红辣
子鸡。

华为云 BigData Pro 大数据解决方案荣获行业年度金奖


12 月 3 日晚,2019 年度中国数据与存储峰会年度颁奖典礼上,华为
云 BigData Pro 大数据解决方案荣获“2019 年度大数据产品金奖”,再一次
展示了华为云在大数据领域的不凡实力。中国数据与存储峰会(DSS)是
国内顶级的数据与存储领域技术盛会,其颁发的奖项颇具含金量,在十多
年间见证了国内数据存储技术和行业的迅猛发展。此次评选范围涉及私有
云大数据、公有云大数据、大数据软件、大数据解决方案等多个领域和维
度。本次华为云 BigData Pro 能一举拿下该奖项,也是实至名归。

大数据容器化,大势所趋
目前已经有大量的大数据系统原生支持 on Kubernetes,例如 Spark 官
方版本,从 2.3 开始,就可以无需任何修改直接跑在 K8s 上。不仅如此,
“更好地在 K8s 上运行”已成为后续版本演进的的重大策略, K8s 对大数据
系统的重要性可见一斑。

46
特别专栏专题 | Topic
| Column

队友已在加速,你感受到了么
鉴于容器技术对大数据具有良好助推作用,已经有不少技术嗅觉敏锐
的头部玩家在此尝到了甜头。
短短几年间,行业已涌现出多个正面案例,中国联通的容器化大
数据平台已实践,京东在使用 Kubernetes 管理大数据中心,网易基于
Kubernetes 和 Docker 构建构建了猛犸大数据平台,茄子科技直接将大数
据任务大量在生产环境跑在 K8s 之上,华为云 DLI 服务容器化,阿里云
Flink on K8s 等。不言而喻,容器技术助力大数据分析,已广泛应用于各
行各业,与其艰难维护自身的庞大的大数据系统,停下来看看业内的趋势、
思考当前的应对措施方为良策。
BigData on K8s 最直接的优势并非性能提升,而是成本下降。
1. 高利用率的资源调度平台。原来分散在多个集群中的业务,可以
合并到统一的集群中,加上长任务短任务混部及不同业务高峰时
间的削峰填谷,来最大化提升集群资源利用率。
2. 统一的技术栈。原有的 Yarn 调度,节点管理技术,与当下宇宙标
准 K8s 集群调度系统,目标是一样的。但是维护 2 种技术栈,就
得增加研发人力成本,统一的基础设施技术栈,降成本效果明显。
3. 容器自动化能力。标准化是推动 IT 技术持续发展的原动力之一。
容器技术本身理念就是一次构建,随处运行,这个与标准化理念
是一致的。通过容器技术的标准化实施,并整合容器生态,建立
运维系统。可以很好的降低业务系统的运维成本,甚至运维工具
本身的构建和使用成本。

47
InfoQ 架构师 2020 年 1 月

容器 + 存算分离,要速度也要成本
当前的大数据计算将计算和存储结合的模式,是分布式架构构建的一
种尝试。但是当社区修改 HDFS 以支持 Hadoop 3.0 的 ErasureCode(纠删码)
时,即接受了:不再支持就近读取的策略。它代表了一种新趋势:为了适
应不同场景,存储空间和算力配比应该是灵活的,可以分别独立构建。
IDC 中国报告指出:
“解耦计算和存储在大数据部署中被证明是有用的,
它提供了更高的资源利用率,更高的灵活性和更低的成本。”这一论断与
很多企业正在进行的大数据架构变革不谋而合。

同时,伴随着容器技术的成熟及在各行业深入广泛的应用,企业愈发
意识到容器技术的优势能很好解决大数据平台当前所遭遇的困境。容器以
其更小颗粒度的算力分配、更轻量和快捷的部署方式、灵活的任务调度等
特点,可以进一步提升资源利用率,并轻松应对大批量任务并发时的算力
扩容。

鲲鹏之上,火山助力
华为云自主研发的鲲鹏处理器,具备多核高并发能力可为用户提供包
括裸金属服务器、云服务器、容器和 Serverless 在内的多种粒度的算力,
大数据分布式场景性能可大幅提升。
其中鲲鹏大数据容器,具有极致弹性的调度能力,可以每秒发放
1000 容器,减少资源弹性等待时间,提升计算效率。而裸金属容器技术,
由于大幅降低虚拟化开销,可更进一步提升服务器执行业务的利用率。采
用 Serverless 模式的容器集群,可以很好地支持按需弹性无限扩展,用来
执行 Spark 大数据任务,轻松处理 PB 级数据作业。

48
特别专栏专题 | Topic
| Column

Volcano 项目是华为容器团队开源的一款 K8s 增强型调度器。初衷是


为了解决原生 K8s 不支持 Gang Scheduling 问题,后来由于 AI 和大数据等
业务领域也开始对 K8s 有极大诉求,团队成员通过总结具体场景实践经验,
将其打造成有价值的技术产品,并贡献社区。
Volcano 通过高性能的调度算法,达到更高的容器调度速度。同时,
自带的多种算法插件,可以极大地提升集群资源利用率。此外,Volcano
也补齐了 K8s 原生调度器与 Yarn 调度器间的 Gap,例如资源的队列管理
(Queue)能力等,大大提升了容器大数据技术的性能。

容器技术为华为云 BigData Pro 解决方案助力


BigData Pro 是业界首个鲲鹏大数据解决方案,该方案采用基于公有
云的存算分离架构,以可无限弹性扩容的鲲鹏算力作为计算资源,以支持
原生多协议的 OBS 对象存储服务为统一的存储数据湖,提供“存算分离、
极致弹性、极致高效”的全新公有云大数据解决方案,大幅提升了大数据
集群的资源利用率,能有效应对当前大数据行业存在的瓶颈,帮助企业应
对 5G+ 云 + 智能时代的全新挑战,实现企业智能化转型升级。

49
InfoQ 架构师 2020 年 1 月

完整微服务化示例:
使用 Apache ServiceComb 进行微服务开
发、容器化、弹性伸缩

微服务架构作为新兴领域的架构模式,已步入产品化形态,与容器化、
集群等一起成为了当下热点。而微服务、Docker、kubernetes 之间的关系,
究竟这三者之间是什么样的关系,分别能在微服务领域发挥什么作用,却
常给入门的读者和用户带来些许迷茫感。
本文使用一个简单的普适性的微服务示例,从业务场景入手,到微服
务架构设计、实现、容器化、集群部署、压测、弹性伸缩、资源控制,端
到端以最直白的方式演示了这三者的关系,会给读者带来不一样的真切的
理念体验和感受,增强对系列概念的理解。

PART 1 普适性微服务化示例
为了读者能更容易了解 ServiceComb 微服务框架的功能以及如何用其
快速开发微服务,所以提供大家耳熟能详的例子,降低学习曲线的同时,
增加趣味性,加深理解。
本文中假设我们成立了一家科研公司,处理复杂的数学运算,以及尖
端生物科技研究,并为用户提供如下服务:
• 黄金分割数列计算
• 蜜蜂繁殖规律 ( 计算每只雄蜂 / 雌蜂的祖先数量 )
但是我们如何将公司的这些强大运算能力提供给我们的消费者呢?
首先我们通过认证服务保障公司的计算资源没有被滥用,同时我们对

50
特别专栏专题 | Topic
| Column

外提供 Rest 服务让用户来进行访问。下面的视频展示具体的服务验证调


用的情况。

业务场景
让我们先对业务场景进行总结分析
1. 为了公司持续发展,我们需要对用户消费的运算能力收费,所以
我们聘用了门卫认证用户,避免不法分子混入。
2. 为了提供足够的黄金分割数量运算能力,我们需要雇佣相应的技
工。
3. 为了持续研究蜜蜂繁殖规律,公司建立了自己的蜂场,需要相应
的养蜂人进行管理研究。
4. 为了平衡技工、养蜂人、和门卫的工作量和时间,我们建立了告
示栏机制,让当前有闲暇的人员发布自己的联系方式,以便我们
能及时联系技能匹配的人员以服务到来的用户。
5. 因为运算能力成本高昂,我们将运算项目进行了归档,以便未来
有相同请求时,我们能直接查询项目归档,节省公司运算成本。
6. 面对上述复杂的场景,我们又聘用了部门经理来管理公司成员和
设施
7. 最后,当公司日益壮大,用户数量暴涨时,我们还需要招聘更多
技工、养蜂人、和门卫,所以增加了人力资源部门

公司结构 ( 系统架构 )

51
InfoQ 架构师 2020 年 1 月

到现在业务场景已经比较清晰,我们把上述职务部门和设施画成公司
组织结构图。
现在公司组织结构已经完整,让我们着手搭建相应部门。

技工 (Worker)
因为技工最为简单,对其他部门人员依赖最少,我们首先搭建这个部
门。

黄金分割运算服务
技工的主要工作是提供黄金分割数列计算服务,当用户需要知道第 n
个黄金分割数时,技工以最快的速度计算出数值并返回给用户。我们可以
把这个工作简化为如下数学方程 :
value = fibo(n)
在暂时不考虑性能的情况下,我们可以迅速实现黄金分割数列的计算。
interface FibonacciService {
long term(int n);
}
@Service
class FibonacciServiceImpl implements FibonacciService {
@Override
public long term(int n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
}
return term(n - 1) + term(n - 2);
}
}

技工服务端点
黄金分割数量运算已经实现,现在我们需要将服务提供给用户,首先
我们定义端点接口:
public interface FibonacciEndpoint {
long term(int n);
}
引入 ServiceComb 依赖:
<dependency>

52
特别专栏专题 | Topic
| Column

<groupId>org.apache.servicecomb</groupId>
<artifactId>spring-boot-starter-provider</artifactId>
</dependency>
接下来我们同时暴露黄金分割运算服务的 Restful 和 RPC 端点:
@RestSchema(schemaId = "fibonacciRestEndpoint")
@RequestMapping("/fibonacci")
@Controller
public class FibonacciRestEndpoint implements FibonacciEndpoint {
private final FibonacciService fibonacciService;
@Autowired
FibonacciRestEndpoint(FibonacciService fibonacciService) {
this.fibonacciService = fibonacciService;
}
@Override
@RequestMapping(value = "/term", method = RequestMethod.GET)
@ResponseBody
public long term(int n) {
return fibonacciService.term(n);
}
}
@RpcSchema(schemaId = "fibonacciRpcEndpoint")
public class FibonacciRpcEndpoint implements FibonacciEndpoint {
private final FibonacciService fibonacciService;
@Autowired
public FibonacciRpcEndpoint(FibonacciService fibonacciService) {
this.fibonacciService = fibonacciService;
}
@Override
public long term(int n) {
return fibonacciService.term(n);
}
}
这里用 @RestSchema 和 @RpcSchema 注释两个端点后,ServiceComb
会自动生成对应的服务端点契约,根据如下 microsevice.yaml 配置端点端
口,并将契约和服务一起注册到 Service Center(https://github.com/apache/
servicecomb-service-center):
# all interconnected microservices must belong to an application wth
the same ID
APPLICATION_ID: company
service_description:
# name of the declaring microservice

53
InfoQ 架构师 2020 年 1 月

name: worker
version: 0.0.1
# service center address
cse:
service:
registry:
address: http://sc.servicecomb.io:30100
highway:
address: 0.0.0.0:7070
rest:
address: 0.0.0.0:8080
最后,提供技工服务应用启动入口,并加上 @EnableServiceComb 注
释启用 ServiceComb :
@SpringBootApplication
@EnableServiceComb
public class WorkerApplication {
public static void main(String[] args) {
SpringApplication.run(WorkerApplication.class, args);
}
}

告示栏 (Bulletin Board)


告示栏提供为门卫、技工和养蜂人注册联系方式的设施,同时经理和
养蜂人可通过此设施查询注册方的联系方式,以方便匹配能力的提供和消
费。
Service Center 提供契约和服务注册、发现功能,而且校验服务提供
方 和 消 费 方 的 契 约 是 否 匹 配, 我 们 可 以 下 载 (https://github.com/apache/
servicecomb-service-center/releases) 编译好的版本直接运行。

养蜂人 (Beekeeper)
养蜂人研究蜜蜂繁殖规律,计算每只蜜蜂 ( 雄蜂 / 雌蜂 ) 的祖先数量。
因为蜜蜂繁殖规律和黄金分割数列相关,所以养蜂人同时消费技工提供的
计算服务。
研究表明,雄蜂 (Drone) 由未受精卵孵化而生,只有母亲;而雌蜂
(Queen) 由受精卵孵化而生,既有母又有父。
Credit: Dave Cushman’s website
参考下图,蜜蜂的某一代祖先数量符合黄金分割数列的模型,由此我

54
特别专栏专题 | Topic
| Column

们可以很快实现服务功能。

蜜蜂繁殖规律研究服务
首先我们定义黄金数列运算接口:
public interface FibonacciCalculator {
long term(int n);
}
接下来定义并实现蜜蜂繁殖规律研究服务 :
interface BeekeeperService {
long ancestorsOfDroneAt(int generation);
long ancestorsOfQueenAt(int generation);
}
class BeekeeperServiceImpl implements BeekeeperService {
private final FibonacciCalculator fibonacciCalculator;
BeekeeperServiceImpl(FibonacciCalculator fibonacciCalculator) {
this.fibonacciCalculator = fibonacciCalculator;
}
@Override
public long ancestorsOfDroneAt(int generation) {
if (generation <= 0) {
return 0;
}
return fibonacciCalculator.term(generation + 1);
}
@Override
public long ancestorsOfQueenAt(int generation) {

55
InfoQ 架构师 2020 年 1 月

if (generation <= 0) {
return 0;
}
return fibonacciCalculator.term(generation + 2);
}
}
这 里 我 们 用 到 之 前 定 义 的 FibonacciCalculator 接 口, 并 希 望 通 过
这 个 接 口 远 程 调 用 技 工 服 务 端 点。@RpcReference 注 释 能 帮 助 我 们
自 动 从 Service Center 中 获 取 microserviceName = "worker", schemaId
= "fibonacciRpcEndpoint" , 即 服 务 名 为 worker 已 经 schema ID 为
fibonacciRpcEndpoint 的端点:
@Configuration
class BeekeeperConfig {
@RpcReference(microserviceName = "worker", schemaId =
"fibonacciRpcEndpoint")
private FibonacciCalculator fibonacciCalculator;
@Bean
BeekeeperService beekeeperService() {
return new BeekeeperServiceImpl(fibonacciCalculator);
}
}
我们在技工一节已定义好对应的服务名和 schema ID 端点,通过上面
的配置,ServiceComb 会自动将远程技工服务实例和 FibonacciCalculator
绑定在一起。

养蜂人服务端点
与上一节技工服务相似,我们在这里也需要提供养蜂人服务端点,让
用户可以进行调用:
@RestSchema(schemaId = "beekeeperRestEndpoint")
@RequestMapping("/rest")
@Controller
public class BeekeeperController {
private static final Logger logger = LoggerFactory.
getLogger(BeekeeperController.class);
private final BeekeeperService beekeeperService;
@Autowired
BeekeeperController(BeekeeperService beekeeperService) {
this.beekeeperService = beekeeperService;
}
@RequestMapping(value = "/drone/ancestors/{generation}", method =

56
特别专栏专题 | Topic
| Column

GET, produces = APPLICATION_JSON_UTF8_VALUE)


@ResponseBody
public Ancestor ancestorsOfDrone(@PathVariable int generation) {
logger.info(
"Received request to find the number of ancestors of drone at
generation {}",
generation);
return new Ancestor(beekeeperService.ancestorsOfDroneAt(generati
on));
}
@RequestMapping(value = "/queen/ancestors/{generation}", method =
GET, produces = APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Ancestor ancestorsOfQueen(@PathVariable int generation) {
logger.info(
"Received request to find the number of ancestors of queen at
generation {}",
generation);
return new Ancestor(beekeeperService.ancestorsOfQueenAt(generati
on));
}
}
class Ancestor {
private long ancestors;
Ancestor() {
}
Ancestor(long ancestors) {
this.ancestors = ancestors;
}
public long getAncestors() {
return ancestors;
}
}
因为养蜂人需要消费技工提供的服务,所以其 microservice.yaml 配置
稍有不同:
# all interconnected microservices must belong to an application wth
the same ID
APPLICATION_ID: company
service_description:
# name of the declaring microservice
name: beekeeper
version: 0.0.1
cse:

57
InfoQ 架构师 2020 年 1 月

service:
registry:
address: http://sc.servicecomb.io:30100
rest:
address: 0.0.0.0:8090
handler:
chain:
Consumer:
default: bizkeeper-consumer,loadbalance
references:
# this one below must refer to the microservice name it
communicates with
worker:
version-rule: 0.0.1
这里我们需要定义 cse.references.worker.version-rule ,让配置名称中
指向技工服务名 worker ,并匹配其版本号。
最后定义养蜂人服务应用入口:
@SpringBootApplication
@EnableServiceComb
public class BeekeeperApplication {
public static void main(String[] args) {
SpringApplication.run(BeekeeperApplication.class, args);
}
}
门卫 (Doorman)
门卫为公司提供安全保障,屏蔽非合法用户,防止其骗取免费服务,
甚至伤害技工和养蜂人。

门卫认证服务
认证功能我们采用 JSON Web Token (JWT) 的机制,具体实现超出了
这篇文章的范围,细节大家可以查看 github 上 workshop 的 doorman 模块
代码。
认证服务的接口如下,authenticate 方法根据用户名和密码查询确认用
户存在,并返回对应 JWT token。用户登录后的每次请求都需要带上返回
的 JWT token,而 validate 方法将验证 token 以确认其有效。
public interface AuthenticationService {
String authenticate(String username, String password);
String validate(String token);

58
特别专栏专题 | Topic
| Column

门卫认证服务端点
与前两节的 Rest 服务端点相似,我们加上 @RestSchema 注释,以便
ServiceComb 自动配置端点、生成契约并注册服务。
@RestSchema(schemaId = "authenticationRestEndpoint")
@Controller
@RequestMapping("/rest")
public class AuthenticationController {
private static final Logger logger = LoggerFactory.getLogger(Authen
ticationController.class);
static final String USERNAME = "username";
static final String PASSWORD = "password";
static final String TOKEN = "token";
private final AuthenticationService authenticationService;
@Autowired
AuthenticationController(AuthenticationService
authenticationService) {
this.authenticationService = authenticationService;
}
@RequestMapping(value = "/login", method = POST, produces = TEXT_
PLAIN_VALUE)
public ResponseEntity<String> login(
@RequestParam(USERNAME) String username,
@RequestParam(PASSWORD) String password) {
logger.info("Received login request from user {}", username);
String token = authenticationService.authenticate(username,
password);
HttpHeaders headers = new HttpHeaders();
headers.add(AUTHORIZATION, TOKEN_PREFIX + token);
logger.info("Authenticated user {} successfully", username);
return new ResponseEntity<>("Welcome, " + username, headers,
OK);
}
@RequestMapping(value = "/validate", method = POST, consumes =
APPLICATION_JSON_UTF8_VALUE, produces = TEXT_PLAIN_VALUE)
@ResponseBody
public String validate(@RequestBody Token token) {
logger.info("Received validation request of token {}", token);
return authenticationService.validate(token.getToken());
}
}
class Token {

59
InfoQ 架构师 2020 年 1 月

private String token;


Token() {
}
Token(String token) {
this.token = token;
}
public String getToken() {
return token;
}
@Override
public String toString() {
return "Token{" +
"token='" + token + '\'' +
'}';
}
}
同样,我们需要提供服务应用启动入口以及 microservice.yaml:
@SpringBootApplication
@EnableServiceComb
public class DoormanApplication {
public static void main(String[] args) {
SpringApplication.run(DoormanApplication.class, args);
}
}
# all interconnected microservices must belong to an application wth
the same ID
APPLICATION_ID: company
service_description:
# name of the declaring microservice
name: doorman
version: 0.0.1
cse:
service:
registry:
address: http://sc.servicecomb.io:30100
rest:
address: 0.0.0.0:9090
经理 (Manager)
为了管理所有人员和设施,经理作为用户唯一接口人,主要功能有:
联系门卫认证用户,保护技工和养蜂人,以免非法用户骗取服务并逃
避服务费用

60
特别专栏专题 | Topic
| Column

联系能力相符的技工和养蜂人,平衡工作量,避免单个人员工作超载
管理项目归档,避免重复工作,保证公司收益最大化。
由于经理责任重大,我们选取了业界有名的 Netflix Zuul 作为候选人
并加以培训,提升其能力,以保证其能胜任该职位。
首先我们引入依赖:
<dependency>
<groupId>org.apache.servicecomb</groupId>
<artifactId>spring-boot-starter-discovery</artifactId>
</dependency>
用户认证服务
当用户发送非登录请求时,我们首先需要验证用户合法,在如下服务
中,我们通过告示栏获取门卫联系方式,然后发送用户 token 给门卫进行
认证。
ServiceComb 提供了相应 RestTemplate 实现查询 Service Center 中的服
务注册信息,只需在地址中以如下格式包含被调用的服务名
cse://doorman/path/to/rest/endpoint
ServiceComb 将自动查询对应服务并发送请求到地址中的服务端点。
@Service
public class AuthenticationService {
private static final Logger logger = LoggerFactory.
getLogger(AuthenticationService.class);
private static final String DOORMAN_ADDRESS = "cse://doorman";
private final RestTemplate restTemplate;
AuthenticationService() {
this.restTemplate = RestTemplateBuilder.create();
this.restTemplate.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse clientHttpResponse) throws
IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse clientHttpResponse)
throws IOException {
}

61
InfoQ 架构师 2020 年 1 月

});
}
@HystrixCommand(fallbackMethod = "timeout")
public ResponseEntity<String> validate(String token) {
logger.info("Validating token {}", token);
ResponseEntity<String> responseEntity = restTemplate.
postForEntity(
DOORMAN_ADDRESS + "/rest/validate",
validationRequest(token),
String.class
);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
logger.warn("No such user found with token {}", token);
}
logger.info("Validated request of token {} to be user {}",
token, responseEntity.getBody());
return responseEntity;
}
private ResponseEntity<String> timeout(String token) {
logger.warn("Request to validate token {} timed out", token);
return new ResponseEntity<>(REQUEST_TIMEOUT);
}
private HttpEntity<Token> validationRequest(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return new HttpEntity<>(new Token(token), headers);
}
}

请求过滤
接 下 来 我 们 提 供 ZuulFilter 实 现 过 滤 用 户 请 求, 调 用
authenticationService.validate(token) 认证用户 token。若用户合法则路由用
户请求到对应服务,否则返回 403 forbidden。
@Component
class AuthenticationAwareFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(Authen
ticationAwareFilter.class);
private static final String LOGIN_PATH = "/login";
private final AuthenticationService authenticationService;
private final PathExtractor pathExtractor;
@Autowired
AuthenticationAwareFilter(

62
特别专栏专题 | Topic
| Column

AuthenticationService authenticationService,
PathExtractor pathExtractor) {
this.authenticationService = authenticationService;
this.pathExtractor = pathExtractor;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
String path = pathExtractor.path(RequestContext.
getCurrentContext());
logger.info("Received request with query path: {}", path);
return !path.endsWith(LOGIN_PATH);
}
@Override
public Object run() {
filter();
return null;
}
private void filter() {
RequestContext context = RequestContext.getCurrentContext();
if (doesNotContainToken(context)) {
logger.warn("No token found in request header");
rejectRequest(context);
} else {
String token = token(context);
ResponseEntity<String> responseEntity = authenticationService.
validate(token);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
logger.warn("Unauthorized token {} and request rejected",
token);
rejectRequest(context);
} else {
logger.info("Token {} validated", token);
}
}
}
private void rejectRequest(RequestContext context) {

63
InfoQ 架构师 2020 年 1 月

context.setResponseStatusCode(SC_FORBIDDEN);
context.setSendZuulResponse(false);
}
private boolean doesNotContainToken(RequestContext context) {
return authorizationHeader(context) == null
|| !authorizationHeader(context).startsWith(TOKEN_PREFIX);
}
private String token(RequestContext context) {
return authorizationHeader(context).replace(TOKEN_PREFIX, "");
}
private String authorizationHeader(RequestContext context) {
return context.getRequest().getHeader(AUTHORIZATION);
}
}
最后提供服务应用入口:
@SpringBootApplication
@EnableCircuitBreaker
@EnableZuulProxy
@EnableDiscoveryClient
@EnableServiceComb
public class ManagerApplication {
public static void main(String[] args) {
SpringApplication.run(ManagerApplication.class, args);
}
}
application.yaml中定义路由规则:
zuul:
routes:
doorman:
serviceId: doorman
sensitiveHeaders:
worker:
serviceId: worker
beekeeper:
serviceId: beekeeper
# disable netflix eurkea since it's not used for service discovery
ribbon:
eureka:
enabled: false
microservice.yaml 中定义服务中心地址:
APPLICATION_ID: company
service_description:

64
特别专栏专题 | Topic
| Column

name: manager
version: 0.0.1
cse:
service:
registry:
address: http://sc.servicecomb.io:30100

项目归档 (Project Archive)


经理在每次用户请求后将项目进行归档,如果将来有内容相同的请求
到达,经理可以就近获取结果,不必再购买技工和养蜂人提供的计算服务,
节省公司开支。
对于归档功能的实现,我们采用了 Spring Cache Abstraction,具体细
节超出了这篇文章的范围,大家如果有兴趣可以查看 github 上 workshop
的 manager 模块代码。

人力资源 (Human Resource)


人力资源从运维层面保证服务的可靠性,主要功能有
• 弹性伸缩,以保证用户请求量超过技工或养蜂人处理能力后,招
聘更多技工或养蜂人加入项目;当请求量回落后,裁剪技工或养
蜂人以节省公司开支
• 健康检查,以保证技工或养蜂人告病时,能有替补接手任务
• 滚动升级,以保证项目需要新技能时,能替换、培训技工或养蜂人,
不中断接收用户请求
人力资源的功能需要云平台提供支持,在后续的文章中会跟大家介绍,
我们如何在华为云上轻松实现这些功能。

微服务化小结
至此,我们用一个公司的组织结构作为例子,给大家介绍了微服务的
完整架构,以及如何使用微服务框架 ServiceComb 快速开发微服务,以及
服务间互通、契约认证。
Workshop demo 项 目 也 包 含 大 量 完 整 易 懂 的 测 试 代 码, 以 及 使 用
docker 集成微服务,模拟生存环境,同时应用 Travis(https://travis-ci.org/)
搭建持续集成环境,体现 DevOps 在微服务开发中的实践。希望能对大家
有所帮助。

65
InfoQ 架构师 2020 年 1 月

PART 2 容器化并集群部署
现 在,github 上 已 经 提 供 了 在 kubernetes 集 群 上 一 键 式 部 署 的 功
能。本文将着重讲解相应的 yaml 文件和服务间通信,这对于开发者基于
Company 模型进行微服务开发并且部署到云上将会有所帮助。

一键部署
Run Company on Kubernetes Cluster 提供了详细的使用方法,读者只
需通过以下 3 条指令,就可将 company 在 kubernetes 集群上部署起来,
git clone https://github.com/ServiceComb/ServiceComb-Company-
WorkShop.git
cd ServiceComb-Company-WorkShop/kubernetes/
bash start.sh

Yaml 文件解读
以作者的实际环境为例:
root@zenlin:~/src/LinuxCon-Beijing-WorkShop/kubernetes# kubectl get
pod -owide
NAME READY STATUS
RESTARTS AGE IP NODE
company-beekeeper-3737555734-48sxf 1/1 Running 0
17s 10.244.2.49 zenlinnode2
company-bulletin-board-4113647782-th91w 1/1 Running 0
17s 10.244.1.53 zenlinnode1
company-doorman-3391375245-g0p8c 1/1 Running 0
17s 10.244.1.55 zenlinnode1
company-manager-454733969-0c1g8 1/1 Running 0
16s 10.244.2.50 zenlinnode2
company-worker-1085546725-x7zl4 1/1 Running 0
17s 10.244.1.54 zenlinnode1
zipkin-508217170-0khr3 1/1 Running 0
17s 10.244.2.48 zenlinnode2
可以看到,一共启动了 6 个 pod,分别为,公司经理(company-manager)、
门 卫(company-doorman)、 公 告 栏(company-bulletin-board)、 技 工
(company-worker)、养蜂人(company-beekeeper)、调用链跟踪(zipkin),
K8S 集群分别为他们分配对应的集群 IP。
root@zenlin:~/src/LinuxCon-Beijing-WorkShop/kubernetes# kubectl get
svc -owide
NAME CLUSTER-IP EXTERNAL-IP PORT(S)
AGE SELECTOR

66
特别专栏专题 | Topic
| Column

company-bulletin-board 10.99.70.46 <none> 30100/TCP


12m io.kompose.service=company-bulletin-board
company-manager 10.100.61.227 <nodes> 8083:30301/
TCP 12m io.kompose.service=company-manager
zipkin 10.104.92.198 <none> 9411/TCP
12m io.kompose.service=zipkin
仅 启 动 了 3 个 service, 调 用 链 跟 踪(zipkin)、 公 告 栏(company-
bulletin-board)以及经理(company-manager),这是因为,调用链跟踪
和公告栏需要在集群内被其他服务通过域名来调用,而经理需要作为对外
作为网关,统一暴露服务端口。
查看 company-bulletin-board-service.yaml 文件,
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
io.kompose.service: company-bulletin-board
name: company-bulletin-board
spec:
ports:
- name: "30100"
port: 30100
targetPort: 30100
selector:
io.kompose.service: company-bulletin-board
status:
loadBalancer: {}
该文件定义了公告栏对应的 service,给 service 定义了 name、port 和
targetPort,这样通过 kubectl expose 创建的 service 会在集群内具备 DNS
能力,在其他服务刚启动还未注册到公告栏(服务注册发现中心)时,就
是使用该能力来访问到公告栏并注册服务的。
对于 label 和 selector 的作用,在一个 service 启动多个 pod 的场景下
将会非常有用,当某个 pod 崩溃时,服务的 selector 将会自动将死亡的
pod 从 endpoints 中移除,并且选择新的 pod 加入到 endpoints 中。
查看 company-worker-deployment.yaml 文件,
apiVersion: extensions/v1beta1
kind: Deployment
metadata:

67
InfoQ 架构师 2020 年 1 月

creationTimestamp: null
labels:
io.kompose.service: company-worker
name: company-worker
spec:
replicas: 1
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
io.kompose.service: company-worker
spec:
containers:
- env:
- name: ARTIFACT_ID
value: worker
- name: JAVA_OPTS
value: -Dcse.service.registry.address=http://company-
bulletin-board:30100 -Dservicecomb.tracing.collector.adress=http://
zipkin:9411
image: servicecomb/worker:0.0.1-SNAPSHOT
name: company-worker
ports:
- containerPort: 7070
- containerPort: 8080
resources: {}
restartPolicy: Always
status: {}
该 yaml 文件定义了副本数为 1(replicas: 1)的 pod,可以通过修改该
副本数控制所需启动的 pod 的副本数量(当然也可以使用 K8S 的弹性伸
缩能力去实现按需动态水平伸缩,弹性伸缩部分将在后面的博文中提供)。
前面我们提到过 company-bulletin-board 具备了 DNS 的能力,故现在可
以通过该 Deployment 中的 env 传递 cse.service.registry.address 的值给 pod
内 的 服 务 使 用, 如:-Dcse.service.registry.address=http://company-bulletin-
board:30100,kube-dns(https://github.com/kubernetes/kubernetes/blob/master/
cluster/addons/dns/README.md) 将会自动解析该 servicename。
对于 kubernetes 如何实现服务间通信,可以阅读 connect-applications-
service。
其他的 deployment.yaml 以及 service.yaml 都跟以上大同小异,唯一例

68
特别专栏专题 | Topic
| Column

外的是 company-manager 服务,我们可以看到在 company-manager-service.


yaml 中看到定义了 nodePort,这将使能 company-manager 对集群外部提供
公网 IP 和服务端口,如下:
spec:
ports:
- name: "8083"
port: 8083
targetPort: 8080
nodePort: 30301
protocol: TCP
type: NodePort
可以通过以下方法获得公网 IP 和服务端口:
kubectl get svc company-manager -o yaml | grep ExternalIP -C 1
kubectl get svc company-manager -o yaml | grep nodePort -C 1
接下来你就可以使用公网 IP 和服务端口访问已经部署好的 company
了, 在 github.com/ServiceComb/ServiceComb-Company-WorkShop/
kubernetes 上详细提供了通过在集群内访问和集群外访问的方法。

模型归纳
通过详细阅读所有的 deployment.yaml 和 service.yaml,可以整理出以
下的模型:

69
InfoQ 架构师 2020 年 1 月

另外,经典的航空订票系统 Acmeair 也已经支持在 kubernetes 上一


键式部署基于 ServiceComb 框架开发的版本,点击访问 Run Acmeair on
Kubernetes(https://github.com/WillemJiang/acmeair/tree/master/kubernetes) 获
取。

PART 3 弹性伸缩
本小节将继续在 K8S 上演示使用 K8S 的弹性伸缩能力进行 Company
示例的按需精细化资源控制,以此体验微服务化给大家带来的好处。

环境准备
K8S 环境准备:
为使 K8S 具备弹性伸缩能力,需要先在 K8S 中安装监控器 Heapster
和 Grafana:
具体读者踩了坑后更新的 heapster 的安装脚本作者放在:heapster,
可直接获取下载获取,需要调整一个参数,后直接运行 kube.sh 脚本进行
安装。
vi LinuxCon-Beijing-WorkShop/kubernetes/heapster/deploy/kube-config/
influxdb/heapster.yaml
spec:
replicas: 1
template:
metadata:
labels:
task: monitoring
k8s-app: heapster
spec:
serviceAccountName: heapster
containers:
- name: heapster
image: gcr.io/google_containers/heapster-amd64:v1.4.1
imagePullPolicy: IfNotPresent
command:
- /heapster
#集群内安装直接使用kubernetes
- --source=kubernetes
#集群外安装请直接将下面的服务地址替换为k8s api server地址
# - --source=kubernetes:http://10.229.43.65:6443?inClusterCon
fig=false

70
特别专栏专题 | Topic
| Column

- --sink=influxdb:http://monitoring-influxdb:8086
启动 Company:
下载 Comany 支持弹性伸缩的代码:
git clone https://github.com/ServiceComb/ServiceComb-Company-
WorkShop.git
cd LinuxCon-Beijing-WorkShop/kubernetes/
bash start-autoscale.sh
在 Company 的 deployment.yaml 中,增加了如下限定资源的字段,这
将限制每个 pod 被限制在 200mill-core(1000 毫 core == 1 core) 的 cpu 使用
率以内。
resources:
limits:
cpu: 200m
在 start-autoscale.sh 中, 对 每 个 deployment 创 建 HPA(pod 水 平 弹 性
伸缩器 ) 资源,限定每个 pod 的副本数弹性伸缩时控制在 1 到 10 之间,
并 限 定 每 个 pod 的 cpu 占 用 率 小 于 50%, 结 合 前 面 限 定 了 200mcore,
故,每个 pod 的的平均 cpu 占用率会被 HPA 通过弹性伸缩能力控制在
100mcore 以内。
# Create Horizontal Pod Autoscaler
kubectl autoscale deployment zipkin --cpu-percent=50 --min=1 --max=10
kubectl autoscale deployment company-bulletin-board --cpu-percent=50
--min=1 --max=10
kubectl autoscale deployment company-worker --cpu-percent=50 --min=1
--max=10
kubectl autoscale deployment company-doorman --cpu-percent=50 --min=1
--max=10
kubectl autoscale deployment company-manager --cpu-percent=50 --min=1
--max=10
kubectl autoscale deployment company-beekeeper --cpu-percent=50
--min=1 --max=10
当运行 start-autoscale.sh 之后,具备弹性伸缩器的 company 已经被创
建,可通过下面指令进行 HPA 的查询:
kubectl get hpa
启动压测:
export $HOST=<heapster-ip>:<heapster-port>
bash LinuxCon-Beijing-WorkShop/kubernetes/stress-test.sh
该脚本不断循环执行 1s 内向 Company 请求计算 fibonacci 数值 200 次,

71
InfoQ 架构师 2020 年 1 月

对 Company 造成请求压力:
FIBONA_NUM=`curl -s -H "Authorization: $Authorization" -XGET
"http://$HOST/worker/fibonacci/term?n=6"`
测试过程与结果
分别查看 HPA 状态以及 Grafana,如下:

图 1 启动阶段

图 2 启动阶段

图 3 过程

图 4 结果

72
特别专栏专题 | Topic
| Column

图 5 结果

从以上过程可以分析出,以下几点:
1. 压力主要集中在 company-manager 这个 pod 上,K8S 的 autoscaler
通过弹性增加该 pod 的副本数量,最终达到目标:每个 pod 的
cpu 占 用 率 低 于 限 定 值 的 50%( 图 5,Usage default company-
manager/Request default company-manager = 192/600 约等于图 4 中
的 33%),并保持稳定。
2. 在弹性伸缩过程中,在还没稳定前可能造成丢包,如图 3。
3. Company 启动会导致系统资源负载暂时性加大,故 Grafana 上看
到的 cpu 占用率曲线会呈现波峰状,但随着系统稳定运行后,
HPA 会按照系统的稳定资源消耗准确找到匹配的副本数。图 3 中
副本数已超过实际所需 3 个,但随着系统稳定,最终还是稳定维
持在 3 个副本。
4. 在 HPA 以及 Grafana 可以看到缩放和报告数据都会有延迟,按照
官方文档说法,只有在最近 3 分钟内没有重新缩放的情况下,才
会进行放大。从最后一次重新缩放,缩小比例将等待 5 分钟。而且,
只有在 avg/ Target 降低到 0.9 以下或者增加到 1.1 以上(10%容差)
的情况下,才可能会进行缩放。
以上,就是本次对 Compan 示例弹性伸缩的全过程,Martin Fowler 在
2014 年 3 月的文章中提到 :
微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服
务组成。系统中的各个微服务可被独立部署,各个微服务之间是松
耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。
在所有情况下,每个任务代表着一个小的业务能力。
国内实践微服务的先行者王磊先生也在《微服务架构与实践》一书中
进行了全面论述。

73
InfoQ 架构师 2020 年 1 月

Company 使用 ServiceComb 进行微服务化改造后,具备了微服务的属


性,故可以对单个负载较大的 company-manager 这个微服务进行精细化的
控制,达到按需的目的,相比传统单体架构来讲,这将大大帮助准确有效
地化解应用瓶颈,提高资源的利用效率。
本文转载自微服务开源项目 Apache ServiceComb 官网博客:
• http://servicecomb.incubator.apache.org/cn/docs/linuxcon-workshop-
demo/
• http://servicecomb.incubator.apache.org/cn/docs/company-on-
kubernetes/
• http://servicecomb.incubator.apache.org/cn/docs/autoscale-on-
company/

74
75
76

You might also like