微服务拆分之道


作者| 修冶

背 景


微服务在最近几年大行其道,很多公司的研发人员都在考虑微服务架构,同时,随着 Docker 容器技术和自动化运维等相关技术发展,微服务变得更容易管理,这给了微服务架构良好的发展机会。

在做微服务的路上,拆分服务是个很热的话题。我们应该按照什么原则将现有的业务进行拆分?是否拆分得越细就越好?接下来一起谈谈服务拆分的策略和坚持的原则。

拆分目的是什么?


在介绍如何拆分之前,我们需要了解下拆分的目的是什么,这样才不会在后续的拆分过程中忘了最初的目的。

拆分的本质是为了将复杂的问题简单化,那么我们在单体架构阶段遇到了哪些复杂性问题呢?首先来回想下当初为什么选用了单体架构,在电商项目刚启动的时候,我们只希望能尽快地将项目搭建起来,方便将产品更早的投放市场进行快速验证。在开发初期,这种架构确实给开发和运维带来了很大的便捷,主要体现在:

- 开发简单直接,代码和项目集中式管理。
- 排查问题时只需要排查这个应用就可以了,更有针对性。
- 只需要维护一个工程,节省维护系统运行的人力成本。

但是随着功能越来越多,开发团队的规模越来越大,单体架构的缺陷慢慢体现出来,主要有以下几个方面:

在技术层面,数据库的连接数成为应用服务器扩容的瓶颈,因为连接 MySQL 的客户端数量是有限制的。

除此之外,单体架构增加了研发的成本抑制了研发效率的提升。比如公司的垂直电商系统团队会被按业务线拆分为不同的组。当如此多的小团队共同维护一套代码和一个系统时,在配合的过程中就会出现问题。不同的团队之间沟通少,假如一个团队需要一个发送短信的功能,那么有的研发同学会认为最快的方式不是询问其他团队是否有现成的,而是自己写一套,但是这种想法是不合适的,会造成功能服务的重复开发。由于代码部署在一起,每个人都向同一个代码库提交代码,代码冲突无法避免;同时功能之间耦合严重,可能你只是更改了很小的逻辑却导致其它功能不可用,从而在测试时需要对整体功能回归,延长了交付时间。模块之间互相依赖,一个小团队中的成员犯了一个错误,就可能会影响到其它团队维护的服务,对于整体系统稳定性影响很大。

最后,单体架构对于系统的运维也会有很大的影响。想象一下,在项目初期你的代码可能只有几千行,构建一次只需要一分钟,那么你可以很敏捷灵活地频繁上线变更修复问题。但是当你的系统扩充到几十万行甚至上百万行代码的时候,一次构建的过程包括编译、单元测试、打包和上传到正式环境,花费的时间可能达到十几分钟,并且任何小的修改,都需要构建整个项目,上线变更的过程非常不灵活。

而这些问题都可以通过微服务化拆分来解决。

为了方便你更好的理解这块,在此附上一份表格(内容来源:《持续演进的 Cloud Native:云原生架构下微服务最佳》一书),可以更直观地帮助你认识拆分的目的。

1.png

拆分时机应该如何决策?


产品初期,应该以单体架构优先。因为面对一个新的领域,对业务的理解很难在开始阶段就比较清晰,往往是经过一段时间之后,才能逐步稳定,如果拆分过早,导致边界拆分不合理或者拆的过细,反而会影响生产力。很多时候,从一个已有的单体架构中逐步划分服务,要比一开始就构建微服务简单得多。同时公司的产品并没有被市场验证过,有可能会失败,所以这个投入的风险也会比较高。

另外,在资源受限的情况下,采用微服务架构很多优势无法体现,性能上的劣势反而会比较明显。如下图所示。当业务复杂度达到一定程度后,微服务架构消耗的成本才会体现优势,并不是所有的场景都适合采用微服务架构,服务的划分应逐步进行,持续演进。产品初期,业务复杂度不高的时候,应该尽量采用单体架构。

2.png

随着公司的商业模式逐渐得到验证,且产品获得了市场的认可,为了能加快产品的迭代效率快速占领市场,公司开始引进更多的开发同学,这时系统的复杂度会变得越来越高,就出现单体应用和团队规模之间出现矛盾,研发效率不升反降。上图中的交叉点表明,业务已经达到了一定的复杂度,单体应用已经无法满足业务增长的需求,研发效率开始下降,而这时就是需要考虑进行服务拆分的时机点。这个点需要架构师去权衡。笔者所在的公司,是当团队规模达到百人的时候,才考虑进行服务化。

当我们清楚了什么时候进行拆分,就可以直接落地了吗?不是的,微服务拆分的落地还要提前准备好配套的基础设施,如服务描述、注册中心、服务框架、服务监控、服务追踪、服务治理等几大基本组件,以上每个组件缺一不可,每个组件展开又包括很多技术门槛,比如,容器技术、持续部署、DevOps 等相关概念,以及人才的储备和观念的变化。微服务不仅仅是技术的升级,更是开发方式、组织架构、开发观念的转变。

至此,何时进行微服务的拆分,整体总结如下:

1. 业务规模:业务模式得到市场的验证,需要进一步加快脚步快速占领市场,这时业务的规模变得越来越大,按产品生命周期来划分(导入期、成长期、成熟期、衰退期)这时一般在成长期阶段。如果是导入期,尽量采用单体架构。
1. 团队规模:一般是团队达到百人的时候。
1. 技术储备:领域驱动设计、注册中心、配置中心、日志系统、持续交付、监控系统、分布式定时任务、CAP 理论、分布式调用链、API 网关等等。
1. 人才储备:精通微服务落地经验的架构师及相应开发同学。
1. 研发效率:研发效率大幅下降,具体问题参加上面拆分目的里提到的。

拆分时应该坚守哪些指导原则?

1. 单一服务内部功能高内聚低耦合

也就是说每个服务只完成自己职责内的任务,对于不是自己职责的功能交给其它服务来完成。

2. 闭包原则( CCP )

微服务的闭包原则就是当我们需要改变一个微服务的时候,所有依赖都在这个微服务的组件内,不需要修改其他微服务。

3. 服务自治、接口隔离原则

尽量消除对其他服务的强依赖,这样可以降低沟通成本,提升服务稳定性。服务通过标准的接口隔离,隐藏内部实现细节。这使得服务可以独立开发、测试、部署、运行,以服务为单位持续交付。

4. 持续演进原则

在服务拆分的初期,你其实很难确定服务究竟要拆成什么样。从微服务这几个字来看,服务的粒度貌似应该足够小,但是服务多了也会带来问题,服务数量快速增长会带来架构复杂度急剧升高,开发、测试、运维等环节很难快速适应,会导致故障率大幅增加,可用性降低,非必要情况,应逐步划分,持续演进,避免服务数量的爆炸性增长,这等同于灰度发布的效果,先拿出几个不太重要的功能拆分出一个服务做试验,如果出现故障,则可以减少故障的影响范围。

5. 拆分的过程尽量避免影响产品的日常功能迭代

也就是说要一边做产品功能迭代,一边完成服务化拆分。比如优先剥离比较独立的边界服务( 如短信服务等 ),从非核心的服务出发减少拆分对现有业务的影响,也给团队一个练习、试错的机会。同时当两个服务存在依赖关系时优先拆分被依赖的服务。

6. 服务接口的定义要具备可扩展性

服务拆分之后,由于服务是以独立进程的方式部署,所以服务之间通信就不再是进程内部的方法调用而是跨进程的网络通信了。在这种通信模型下服务接口的定义要具备可扩展性,否则在服务变更时会造成意想不到的错误。比如微服务的接口因为升级把之前的三个参数改成了四个,上线后导致调用方大量报错,推荐做法服务接口的参数类型最好是封装类,这样如果增加参数就不必变更接口的签名,而只需要在类中添加字段就可以了

7. 避免环形依赖与双向依赖

尽量不要有服务之间的环形依赖或双向依赖,原因是存在这种情况说明我们的功能边界没有化分清楚或者有通用的功能没有下沉下来。

3.png

8. 阶段性合并

随着你对业务领域理解的逐渐深入或者业务本身逻辑发生了比较大的变化,亦或者之前的拆分没有考虑的很清楚,导致拆分后的服务边界变得越来越混乱,这时就要重新梳理领域边界,不断纠正拆分的合理性。

拆分的粒度是不是越细越好?


目前很多传统的单体应用再向微服务架构进行升级改造,如果拆分粒度太细会增加运维复杂度,粒度过大又起不到效果,那么改造过程中如何平衡拆分粒度呢?

4.png

1. 弓箭原理

平衡拆分粒度可以从两方面进行权衡,一是业务发展的复杂度,二是团队规模的人数。如上图,它就像弓箭一样,只有当业务复杂度和团队人数足够大的时候,射出的服务拆分粒度这把剑才会飞的更远,发挥出最大的威力。

比如说电商的商品服务,当我们把商品从大的单体里拆分出来的时候,就商品服务本身来讲,逻辑并没有足够复杂到 2 ~ 3 个人没法维护的地步,这时我们没有必要继续将商品服务拆的更细,但是随着业务的发展,商品的业务逻辑变的越来越复杂,可能同时服务公司的多个平台,此时你会发现商品服务本身面临的问题跟单体架构阶段面临的问题基本一样,这个阶段就需要我们将商品拆成更细粒度的服务,比如,库存服务、价格服务、类目服务、商品基础信息服务等等。

虽然业务复杂度已经满足了,如果公司此时没有足够的人力(招聘不及时或员工异动比较多),服务最好也不要拆分,拆分会因为人力的不足导致更多的问题,如研发效率大幅下降(一个开发负责与其不匹配数量的服务)。这里引申另外一个问题,一个微服务究竟需要几个开发维护是比较理性的?我引用下李云华老师在"从零开始学架构“ 中的一段经典论述,可以解决此问题。

2. 三个火枪手原则

为什么说是三个人分配一个服务是比较理性的?而不是 4 个,也不是 2 个呢?

首先,从系统规模来讲,3 个人负责开发一个系统,系统的复杂度刚好达到每个人都能全面理解整个系统,又能够进行分工的粒度;如果是 2 个人开发一个系统,系统的复杂度不够,开发人员可能觉得无法体现自己的技术实力;如果是 4 个甚至更多人开发一个系统,系统复杂度又会无法让开发人员对系统的细节都了解很深。

其次,从团队管理来说,3 个人可以形成一个稳定的备份,即使 1 个人休假或者调配到其他系统,剩余 2 个人还可以支撑;如果是 2 个人,抽调 1 个后剩余的 1 个人压力很大;如果是 1 个人,这就是单点了,团队没有备份,某些情况下是很危险的,假如这个人休假了,系统出问题了怎么办?

最后,从技术提升的角度来讲,3 个人的技术小组既能够形成有效的讨论,又能够快速达成一致意见;如果是 2 个人,可能会出现互相坚持自己的意见,或者 2 个人经验都不足导致设计缺陷;如果是 1 个人,由于没有人跟他进行技术讨论,很可能陷入思维盲区导致重大问题;如果是 4 个人或者更多,可能有的参与的人员并没有认真参与,只是完成任务而已。

“三个火枪手”的原则主要应用于微服务设计和开发阶段,如果微服务经过一段时间发展后已经比较稳定,处于维护期了,无须太多的开发,那么平均 1 个人维护 1 个微服务甚至几个微服务都可以。当然考虑到人员备份问题,每个微服务最好都安排 2 个人维护,每个人都可以维护多个微服务。

综上所诉,拆分粒度不是越细越好,粒度需要符合弓箭原理及三个火枪手原则。

拆分策略有哪些?


拆分策略可以按功能和非功能维度进行考虑,功能维度主要是划分清楚业务的边界,非功能维度主要考虑六点包括扩展性、复用性、高性能、高可用、安全性、异构性。接下来详细介绍下。

功能维度

功能维度主要是划分清楚业务边界,采用的主要设计方法可以利用 DDD(关于 DDD 的理论知识可以参考网上其它资料),DDD 的战略设计会建立领域模型,可以通过领域模型指导微服务的拆分,主要分四步进行:

- 第一步,找出领域实体和值对象等领域对象。
- 第二步,找出聚合根,根据实体、值对象与聚合根的依赖关系,建立聚合。
- 第三步,根据业务及语义边界等因素,定义限界上下文。
- 第四步,每一个限界上下文可以拆分为一个对应的微服务,但也要考虑一些非功能因素。

以电商的场景为例,交易链路划分的限界上下文如下图左半部分,根据一个限界上下文可以设计一个微服务,拆解出来的微服务如下图右侧部分。

5.png

2. 非功能维度

当我们按照功能维度进行拆分后,并不是就万事大吉了,大部分场景下,我们还需要加入其它维度进一步拆分,才能最终解决单体架构带来的问题。

- 扩展性:区分系统中变与不变的部分,不变的部分一般是成熟的、通用的服务功能,变的部分一般是改动比较多、满足业务迭代扩展性需要的功能,我们可以将不变的部分拆分出来,作为共用的服务,将变的部分独立出来满足个性化扩展需要。同时根据二八原则,系统中经常变动的部分大约只占 20%,而剩下的 80 % 基本不变或极少变化,这样的拆分也解决了发布频率过多而影响成熟服务稳定性的问题。
  • 复用性:不同的业务里或服务里经常会出现重复的功能,比如每个服务都有鉴权、限流、安全及日志监控等功能,可以将这些通过的功能拆分出来形成独立的服务,也就是微服务里面的 API 网关。在如,对于滴滴业务,有快车和顺风车业务,其中都涉及到了订单支付的功能,那么就可以将订单支付独立出来,作为通用服务服务好上层业务。如下图:


6.png
  • 高性能:将性能要求高或者性能压力大的模块拆分出来,避免性能压力大的服务影响其它服务。常见的拆分方式和具体的性能瓶颈有关,例如电商的抢购,性能压力最大的是入口的排队功能,可以将排队功能独立为一个服务。同时,我们也可以基于读写分离来拆分,比如电商的商品信息,在 App 端主要是商详有大量的读取操作,但是写入端商家中心访问量确很少。因此可以对流量较大或较为核心的服务做读写分离,拆分为两个服务发布,一个负责读,另外一个负责写。还有数据一致性是另一个基于性能维度拆分需要考虑的点,对于强一致的数据,属于强耦合,尽量放在同一个服务中(但是有时会因为各种原因需要进行拆分,那就需要有响应的机制进行保证),弱一致性通常可以拆分为不同的服务。


7.png

  • 高可用:将可靠性要求高的核心服务和可靠性要求低的非核心服务拆分开来,然后重点保证核心服务的高可用。具体拆分的时候,核心服务可以是一个也可以是多个,只要最终的服务数量满足“三个火枪手”的原则就可以。比如针对商家服务,可以拆分一个核心服务一个非核心服务,核心服务供交易服务访问,非核心提供给商家中心访问 。

  • 安全性:不同的服务可能对信息安全有不同的要求,因此把需要高度安全的服务拆分出来,进行区别部署,比如设置特定的 DMZ 区域对服务进行分区部署,可以更有针对性地满足信息安全的要求,也可以降低对防火墙等安全设备吞吐量、并发性等方面的要求,降低成本,提高效率。

  • 异构性:对于对开发语言种类有要求的业务场景,可以用不同的语言将其功能独立出来实现一个独立服务。


以上几种拆分方式不是多选一,而是可以根据实际情况自由排列组合。同时拆分不仅仅是架构上的调整,也意味着要在组织结构上做出相应的适应性优化,以确保拆分后的服务由相对独立的团队负责维护。

服务都拆了为什么还要合并?


古希腊哲学家赫拉克利特曾经说过:“人不能两次踏进同一条河流。”随着时间的流逝,任何事物的状态都会发生变化。线上系统同样如此,即使一个系统在不同时刻的状况也绝不会一模一样。现在拆分出来的服务粒度也许合适,但谁能保证这个粒度能够一直正确呢。

服务都拆了为什么还要合,就是要不断适应新的业务发展阶段,笔者这里做个类比看大家是否清晰,拆相当于我们开发代码,合相当于重构代码,为什么要重构呢,相信你肯定知道。微服务的合也是一样的道理,随着我们对应用程序领域的了解越来越深,它们可能会随着时间的推移而变化。例如,你可能会发现由于过多的进程间通信而导致特定的分解效率低下,导致你必须把一些服务组合在一起。

同时因为人员和服务数量的不匹配,导致的维护成本增加,也是导致服务合并的一个重要原因。例如,今年疫情的影响导致很多企业开始大量裁员,人员流失但是服务的数量确没有变,造成服务数量和人员的不平衡,一个开发同学同时要维护至少 5 个服务的开发,效率大幅下降。

那么如果微服务数量过多和资源不匹配,则可以考虑合并多个微服务到服务包,部署到一台服务器,这样可以节省服务运行时的基础资源消耗也降低了维护成本。需要注意的是,虽然服务包是运行在一个进程中,但是服务包内的服务依然要满足微服务定义,以便在未来某一天要重新拆开的时候可以很快就分离。服务合并到服务包示意图如下:

8.png

拆分过程中要注意的风险


1. 不打无准备之仗

开发团队是否具备足够的经验,能否驾驭微服务的技术栈,可能是第一个需要考虑的点。这里并不是要求团队必须具备完善的经验才能启动服务拆分,如果团队中有这方面的专家固然是最好的。如果没有,那可能就需要事先进行充分的技术论证和预演,至少不打无准备之仗。避免哪个简单就先拆哪个,哪个新业务要上了,先起一个服务再说。否则可能在一些分布式常见的问题上会踩坑,比如服务器资源不够、运维困难、服务之间调用混乱、调用重试、超时机制、分布式事务等等。

2. 不断纠正

我们需要承认我们的认知是有限的,只能基于目前的业务状态和有限的对未来的预测来制定出一个相对合适的拆分方案,而不是所谓的最优方案,任何方案都只能保证在当下提供了相对合适的粒度和划分原则,要时刻做好在未来的末一个时刻会变得不和时宜、需要再次调整的准备。因此随着业务的演进,需要我们重新审视服务的划分是否合理,如服务拆的太细,导致人员效率反而下降,故障的概率也大大增加,则需要重新划分好领域边界。

3. 要做行动派,而不是理论派

在具体怎么拆分上,也不要太纠结于是否合适,不动手怎么知道合不合适呢?如果拆了之后发现真的不合适,在重新调整就好了。你可能会说,重新调整成本比较高。但实际上这个问题的本质是有没有针对服务化架构搭建起一套完成的能力体系,比如服务治理平台、数据迁移工具、数据双写等等,如果有的话,重新调整的成本是不会太高的。
继续阅读 »

作者| 修冶

背 景


微服务在最近几年大行其道,很多公司的研发人员都在考虑微服务架构,同时,随着 Docker 容器技术和自动化运维等相关技术发展,微服务变得更容易管理,这给了微服务架构良好的发展机会。

在做微服务的路上,拆分服务是个很热的话题。我们应该按照什么原则将现有的业务进行拆分?是否拆分得越细就越好?接下来一起谈谈服务拆分的策略和坚持的原则。

拆分目的是什么?


在介绍如何拆分之前,我们需要了解下拆分的目的是什么,这样才不会在后续的拆分过程中忘了最初的目的。

拆分的本质是为了将复杂的问题简单化,那么我们在单体架构阶段遇到了哪些复杂性问题呢?首先来回想下当初为什么选用了单体架构,在电商项目刚启动的时候,我们只希望能尽快地将项目搭建起来,方便将产品更早的投放市场进行快速验证。在开发初期,这种架构确实给开发和运维带来了很大的便捷,主要体现在:

- 开发简单直接,代码和项目集中式管理。
- 排查问题时只需要排查这个应用就可以了,更有针对性。
- 只需要维护一个工程,节省维护系统运行的人力成本。

但是随着功能越来越多,开发团队的规模越来越大,单体架构的缺陷慢慢体现出来,主要有以下几个方面:

在技术层面,数据库的连接数成为应用服务器扩容的瓶颈,因为连接 MySQL 的客户端数量是有限制的。

除此之外,单体架构增加了研发的成本抑制了研发效率的提升。比如公司的垂直电商系统团队会被按业务线拆分为不同的组。当如此多的小团队共同维护一套代码和一个系统时,在配合的过程中就会出现问题。不同的团队之间沟通少,假如一个团队需要一个发送短信的功能,那么有的研发同学会认为最快的方式不是询问其他团队是否有现成的,而是自己写一套,但是这种想法是不合适的,会造成功能服务的重复开发。由于代码部署在一起,每个人都向同一个代码库提交代码,代码冲突无法避免;同时功能之间耦合严重,可能你只是更改了很小的逻辑却导致其它功能不可用,从而在测试时需要对整体功能回归,延长了交付时间。模块之间互相依赖,一个小团队中的成员犯了一个错误,就可能会影响到其它团队维护的服务,对于整体系统稳定性影响很大。

最后,单体架构对于系统的运维也会有很大的影响。想象一下,在项目初期你的代码可能只有几千行,构建一次只需要一分钟,那么你可以很敏捷灵活地频繁上线变更修复问题。但是当你的系统扩充到几十万行甚至上百万行代码的时候,一次构建的过程包括编译、单元测试、打包和上传到正式环境,花费的时间可能达到十几分钟,并且任何小的修改,都需要构建整个项目,上线变更的过程非常不灵活。

而这些问题都可以通过微服务化拆分来解决。

为了方便你更好的理解这块,在此附上一份表格(内容来源:《持续演进的 Cloud Native:云原生架构下微服务最佳》一书),可以更直观地帮助你认识拆分的目的。

1.png

拆分时机应该如何决策?


产品初期,应该以单体架构优先。因为面对一个新的领域,对业务的理解很难在开始阶段就比较清晰,往往是经过一段时间之后,才能逐步稳定,如果拆分过早,导致边界拆分不合理或者拆的过细,反而会影响生产力。很多时候,从一个已有的单体架构中逐步划分服务,要比一开始就构建微服务简单得多。同时公司的产品并没有被市场验证过,有可能会失败,所以这个投入的风险也会比较高。

另外,在资源受限的情况下,采用微服务架构很多优势无法体现,性能上的劣势反而会比较明显。如下图所示。当业务复杂度达到一定程度后,微服务架构消耗的成本才会体现优势,并不是所有的场景都适合采用微服务架构,服务的划分应逐步进行,持续演进。产品初期,业务复杂度不高的时候,应该尽量采用单体架构。

2.png

随着公司的商业模式逐渐得到验证,且产品获得了市场的认可,为了能加快产品的迭代效率快速占领市场,公司开始引进更多的开发同学,这时系统的复杂度会变得越来越高,就出现单体应用和团队规模之间出现矛盾,研发效率不升反降。上图中的交叉点表明,业务已经达到了一定的复杂度,单体应用已经无法满足业务增长的需求,研发效率开始下降,而这时就是需要考虑进行服务拆分的时机点。这个点需要架构师去权衡。笔者所在的公司,是当团队规模达到百人的时候,才考虑进行服务化。

当我们清楚了什么时候进行拆分,就可以直接落地了吗?不是的,微服务拆分的落地还要提前准备好配套的基础设施,如服务描述、注册中心、服务框架、服务监控、服务追踪、服务治理等几大基本组件,以上每个组件缺一不可,每个组件展开又包括很多技术门槛,比如,容器技术、持续部署、DevOps 等相关概念,以及人才的储备和观念的变化。微服务不仅仅是技术的升级,更是开发方式、组织架构、开发观念的转变。

至此,何时进行微服务的拆分,整体总结如下:

1. 业务规模:业务模式得到市场的验证,需要进一步加快脚步快速占领市场,这时业务的规模变得越来越大,按产品生命周期来划分(导入期、成长期、成熟期、衰退期)这时一般在成长期阶段。如果是导入期,尽量采用单体架构。
1. 团队规模:一般是团队达到百人的时候。
1. 技术储备:领域驱动设计、注册中心、配置中心、日志系统、持续交付、监控系统、分布式定时任务、CAP 理论、分布式调用链、API 网关等等。
1. 人才储备:精通微服务落地经验的架构师及相应开发同学。
1. 研发效率:研发效率大幅下降,具体问题参加上面拆分目的里提到的。

拆分时应该坚守哪些指导原则?

1. 单一服务内部功能高内聚低耦合

也就是说每个服务只完成自己职责内的任务,对于不是自己职责的功能交给其它服务来完成。

2. 闭包原则( CCP )

微服务的闭包原则就是当我们需要改变一个微服务的时候,所有依赖都在这个微服务的组件内,不需要修改其他微服务。

3. 服务自治、接口隔离原则

尽量消除对其他服务的强依赖,这样可以降低沟通成本,提升服务稳定性。服务通过标准的接口隔离,隐藏内部实现细节。这使得服务可以独立开发、测试、部署、运行,以服务为单位持续交付。

4. 持续演进原则

在服务拆分的初期,你其实很难确定服务究竟要拆成什么样。从微服务这几个字来看,服务的粒度貌似应该足够小,但是服务多了也会带来问题,服务数量快速增长会带来架构复杂度急剧升高,开发、测试、运维等环节很难快速适应,会导致故障率大幅增加,可用性降低,非必要情况,应逐步划分,持续演进,避免服务数量的爆炸性增长,这等同于灰度发布的效果,先拿出几个不太重要的功能拆分出一个服务做试验,如果出现故障,则可以减少故障的影响范围。

5. 拆分的过程尽量避免影响产品的日常功能迭代

也就是说要一边做产品功能迭代,一边完成服务化拆分。比如优先剥离比较独立的边界服务( 如短信服务等 ),从非核心的服务出发减少拆分对现有业务的影响,也给团队一个练习、试错的机会。同时当两个服务存在依赖关系时优先拆分被依赖的服务。

6. 服务接口的定义要具备可扩展性

服务拆分之后,由于服务是以独立进程的方式部署,所以服务之间通信就不再是进程内部的方法调用而是跨进程的网络通信了。在这种通信模型下服务接口的定义要具备可扩展性,否则在服务变更时会造成意想不到的错误。比如微服务的接口因为升级把之前的三个参数改成了四个,上线后导致调用方大量报错,推荐做法服务接口的参数类型最好是封装类,这样如果增加参数就不必变更接口的签名,而只需要在类中添加字段就可以了

7. 避免环形依赖与双向依赖

尽量不要有服务之间的环形依赖或双向依赖,原因是存在这种情况说明我们的功能边界没有化分清楚或者有通用的功能没有下沉下来。

3.png

8. 阶段性合并

随着你对业务领域理解的逐渐深入或者业务本身逻辑发生了比较大的变化,亦或者之前的拆分没有考虑的很清楚,导致拆分后的服务边界变得越来越混乱,这时就要重新梳理领域边界,不断纠正拆分的合理性。

拆分的粒度是不是越细越好?


目前很多传统的单体应用再向微服务架构进行升级改造,如果拆分粒度太细会增加运维复杂度,粒度过大又起不到效果,那么改造过程中如何平衡拆分粒度呢?

4.png

1. 弓箭原理

平衡拆分粒度可以从两方面进行权衡,一是业务发展的复杂度,二是团队规模的人数。如上图,它就像弓箭一样,只有当业务复杂度和团队人数足够大的时候,射出的服务拆分粒度这把剑才会飞的更远,发挥出最大的威力。

比如说电商的商品服务,当我们把商品从大的单体里拆分出来的时候,就商品服务本身来讲,逻辑并没有足够复杂到 2 ~ 3 个人没法维护的地步,这时我们没有必要继续将商品服务拆的更细,但是随着业务的发展,商品的业务逻辑变的越来越复杂,可能同时服务公司的多个平台,此时你会发现商品服务本身面临的问题跟单体架构阶段面临的问题基本一样,这个阶段就需要我们将商品拆成更细粒度的服务,比如,库存服务、价格服务、类目服务、商品基础信息服务等等。

虽然业务复杂度已经满足了,如果公司此时没有足够的人力(招聘不及时或员工异动比较多),服务最好也不要拆分,拆分会因为人力的不足导致更多的问题,如研发效率大幅下降(一个开发负责与其不匹配数量的服务)。这里引申另外一个问题,一个微服务究竟需要几个开发维护是比较理性的?我引用下李云华老师在"从零开始学架构“ 中的一段经典论述,可以解决此问题。

2. 三个火枪手原则

为什么说是三个人分配一个服务是比较理性的?而不是 4 个,也不是 2 个呢?

首先,从系统规模来讲,3 个人负责开发一个系统,系统的复杂度刚好达到每个人都能全面理解整个系统,又能够进行分工的粒度;如果是 2 个人开发一个系统,系统的复杂度不够,开发人员可能觉得无法体现自己的技术实力;如果是 4 个甚至更多人开发一个系统,系统复杂度又会无法让开发人员对系统的细节都了解很深。

其次,从团队管理来说,3 个人可以形成一个稳定的备份,即使 1 个人休假或者调配到其他系统,剩余 2 个人还可以支撑;如果是 2 个人,抽调 1 个后剩余的 1 个人压力很大;如果是 1 个人,这就是单点了,团队没有备份,某些情况下是很危险的,假如这个人休假了,系统出问题了怎么办?

最后,从技术提升的角度来讲,3 个人的技术小组既能够形成有效的讨论,又能够快速达成一致意见;如果是 2 个人,可能会出现互相坚持自己的意见,或者 2 个人经验都不足导致设计缺陷;如果是 1 个人,由于没有人跟他进行技术讨论,很可能陷入思维盲区导致重大问题;如果是 4 个人或者更多,可能有的参与的人员并没有认真参与,只是完成任务而已。

“三个火枪手”的原则主要应用于微服务设计和开发阶段,如果微服务经过一段时间发展后已经比较稳定,处于维护期了,无须太多的开发,那么平均 1 个人维护 1 个微服务甚至几个微服务都可以。当然考虑到人员备份问题,每个微服务最好都安排 2 个人维护,每个人都可以维护多个微服务。

综上所诉,拆分粒度不是越细越好,粒度需要符合弓箭原理及三个火枪手原则。

拆分策略有哪些?


拆分策略可以按功能和非功能维度进行考虑,功能维度主要是划分清楚业务的边界,非功能维度主要考虑六点包括扩展性、复用性、高性能、高可用、安全性、异构性。接下来详细介绍下。

功能维度

功能维度主要是划分清楚业务边界,采用的主要设计方法可以利用 DDD(关于 DDD 的理论知识可以参考网上其它资料),DDD 的战略设计会建立领域模型,可以通过领域模型指导微服务的拆分,主要分四步进行:

- 第一步,找出领域实体和值对象等领域对象。
- 第二步,找出聚合根,根据实体、值对象与聚合根的依赖关系,建立聚合。
- 第三步,根据业务及语义边界等因素,定义限界上下文。
- 第四步,每一个限界上下文可以拆分为一个对应的微服务,但也要考虑一些非功能因素。

以电商的场景为例,交易链路划分的限界上下文如下图左半部分,根据一个限界上下文可以设计一个微服务,拆解出来的微服务如下图右侧部分。

5.png

2. 非功能维度

当我们按照功能维度进行拆分后,并不是就万事大吉了,大部分场景下,我们还需要加入其它维度进一步拆分,才能最终解决单体架构带来的问题。

- 扩展性:区分系统中变与不变的部分,不变的部分一般是成熟的、通用的服务功能,变的部分一般是改动比较多、满足业务迭代扩展性需要的功能,我们可以将不变的部分拆分出来,作为共用的服务,将变的部分独立出来满足个性化扩展需要。同时根据二八原则,系统中经常变动的部分大约只占 20%,而剩下的 80 % 基本不变或极少变化,这样的拆分也解决了发布频率过多而影响成熟服务稳定性的问题。
  • 复用性:不同的业务里或服务里经常会出现重复的功能,比如每个服务都有鉴权、限流、安全及日志监控等功能,可以将这些通过的功能拆分出来形成独立的服务,也就是微服务里面的 API 网关。在如,对于滴滴业务,有快车和顺风车业务,其中都涉及到了订单支付的功能,那么就可以将订单支付独立出来,作为通用服务服务好上层业务。如下图:


6.png
  • 高性能:将性能要求高或者性能压力大的模块拆分出来,避免性能压力大的服务影响其它服务。常见的拆分方式和具体的性能瓶颈有关,例如电商的抢购,性能压力最大的是入口的排队功能,可以将排队功能独立为一个服务。同时,我们也可以基于读写分离来拆分,比如电商的商品信息,在 App 端主要是商详有大量的读取操作,但是写入端商家中心访问量确很少。因此可以对流量较大或较为核心的服务做读写分离,拆分为两个服务发布,一个负责读,另外一个负责写。还有数据一致性是另一个基于性能维度拆分需要考虑的点,对于强一致的数据,属于强耦合,尽量放在同一个服务中(但是有时会因为各种原因需要进行拆分,那就需要有响应的机制进行保证),弱一致性通常可以拆分为不同的服务。


7.png

  • 高可用:将可靠性要求高的核心服务和可靠性要求低的非核心服务拆分开来,然后重点保证核心服务的高可用。具体拆分的时候,核心服务可以是一个也可以是多个,只要最终的服务数量满足“三个火枪手”的原则就可以。比如针对商家服务,可以拆分一个核心服务一个非核心服务,核心服务供交易服务访问,非核心提供给商家中心访问 。

  • 安全性:不同的服务可能对信息安全有不同的要求,因此把需要高度安全的服务拆分出来,进行区别部署,比如设置特定的 DMZ 区域对服务进行分区部署,可以更有针对性地满足信息安全的要求,也可以降低对防火墙等安全设备吞吐量、并发性等方面的要求,降低成本,提高效率。

  • 异构性:对于对开发语言种类有要求的业务场景,可以用不同的语言将其功能独立出来实现一个独立服务。


以上几种拆分方式不是多选一,而是可以根据实际情况自由排列组合。同时拆分不仅仅是架构上的调整,也意味着要在组织结构上做出相应的适应性优化,以确保拆分后的服务由相对独立的团队负责维护。

服务都拆了为什么还要合并?


古希腊哲学家赫拉克利特曾经说过:“人不能两次踏进同一条河流。”随着时间的流逝,任何事物的状态都会发生变化。线上系统同样如此,即使一个系统在不同时刻的状况也绝不会一模一样。现在拆分出来的服务粒度也许合适,但谁能保证这个粒度能够一直正确呢。

服务都拆了为什么还要合,就是要不断适应新的业务发展阶段,笔者这里做个类比看大家是否清晰,拆相当于我们开发代码,合相当于重构代码,为什么要重构呢,相信你肯定知道。微服务的合也是一样的道理,随着我们对应用程序领域的了解越来越深,它们可能会随着时间的推移而变化。例如,你可能会发现由于过多的进程间通信而导致特定的分解效率低下,导致你必须把一些服务组合在一起。

同时因为人员和服务数量的不匹配,导致的维护成本增加,也是导致服务合并的一个重要原因。例如,今年疫情的影响导致很多企业开始大量裁员,人员流失但是服务的数量确没有变,造成服务数量和人员的不平衡,一个开发同学同时要维护至少 5 个服务的开发,效率大幅下降。

那么如果微服务数量过多和资源不匹配,则可以考虑合并多个微服务到服务包,部署到一台服务器,这样可以节省服务运行时的基础资源消耗也降低了维护成本。需要注意的是,虽然服务包是运行在一个进程中,但是服务包内的服务依然要满足微服务定义,以便在未来某一天要重新拆开的时候可以很快就分离。服务合并到服务包示意图如下:

8.png

拆分过程中要注意的风险


1. 不打无准备之仗

开发团队是否具备足够的经验,能否驾驭微服务的技术栈,可能是第一个需要考虑的点。这里并不是要求团队必须具备完善的经验才能启动服务拆分,如果团队中有这方面的专家固然是最好的。如果没有,那可能就需要事先进行充分的技术论证和预演,至少不打无准备之仗。避免哪个简单就先拆哪个,哪个新业务要上了,先起一个服务再说。否则可能在一些分布式常见的问题上会踩坑,比如服务器资源不够、运维困难、服务之间调用混乱、调用重试、超时机制、分布式事务等等。

2. 不断纠正

我们需要承认我们的认知是有限的,只能基于目前的业务状态和有限的对未来的预测来制定出一个相对合适的拆分方案,而不是所谓的最优方案,任何方案都只能保证在当下提供了相对合适的粒度和划分原则,要时刻做好在未来的末一个时刻会变得不和时宜、需要再次调整的准备。因此随着业务的演进,需要我们重新审视服务的划分是否合理,如服务拆的太细,导致人员效率反而下降,故障的概率也大大增加,则需要重新划分好领域边界。

3. 要做行动派,而不是理论派

在具体怎么拆分上,也不要太纠结于是否合适,不动手怎么知道合不合适呢?如果拆了之后发现真的不合适,在重新调整就好了。你可能会说,重新调整成本比较高。但实际上这个问题的本质是有没有针对服务化架构搭建起一套完成的能力体系,比如服务治理平台、数据迁移工具、数据双写等等,如果有的话,重新调整的成本是不会太高的。 收起阅读 »

Telltale:看 Netflix 如何简化应用程序监控体系


【编者的话】本文阐述了 Netflix 的系统监控实践:自研 Telltale,成功运行并监控着 Netflix 100 多个生产应用程序的运行状况。

难忘的经历

相信很多运维人都有过这样的经历:监控系统某个指标超过阈值,触发告警。大半夜里,你被紧急召唤。

半睁着眼,你满脸疑惑:“系统真出问题了吗,还是仅仅需要调整下告警?上一次有人调整我们的告警阈值是在什么时候?有没有可能是上游或者下游的服务出现了问题?”

鉴于这是一次非常重要的应用告警,因此你不得不从床上爬起来,迅速打开电脑,然后浏览监控仪表盘来追踪问题源头。

忙了半天,你还没确认这个告警是来自于系统的问题,但也意识到,从海量数据中寻找线索时,时间正在流逝。你必须尽快定位告警的原因,并祈祷系统稳定运行。

对我们的用户来讲,稳健的 Netflix 服务至关重要。当你坐下来看《养虎为患》时,你肯定希望它能顺利播放。

多年来,我们从经常在深夜被召唤的工程师那里了解到应用程序监控的痛点:

  • 过多的告警
  • 太多滚动浏览的仪表盘
  • 太多的配置
  • 过多的维护


Telltale

我们的流媒体团队需要一个全新的监控系统,可以让团队成员快速地诊断和修复问题;因为在系统告警的紧急情况下,每一秒都至关重要!

我们的 Node 团队 需要一个仅需一小撮人就能运维大型集群的系统。因此,我们构建了 Telltale。

1.png

Telltale 监控时间轴

Telltale 的特性如下:

汇集监控数据源,创建整体监控视图:Telltale 汇集了各种监控数据源,从而能创建关于应用程序运行状况的整体监控视图。

多维度判断应用程序的健康状况:Telltale 可以通过多个维度判断一个应用程序的健康情况,而无需根据单一指标频繁调整告警阈值。

及时告警:因为我们知道应用程序在什么情况下是正常的,所以能在应用程序有异常趋势时及时通知应用程序的所有者。

显示关键数据:指标是了解应用程序运行状态的关键。但很多时候,你拥有太多的指标、太多的图表以及太多的监控仪表盘。而 Telltale 仅显示应用程序中有用的相关数据及其上游和下游服务的数据。

用颜色区分问题的严重程度:我们使用不同的颜色来表示问题的严重程度(除选择颜色之外,还可以让 Telltale 显示不同的数字),以便运维人员一眼就能判断出应用程序的运行状况。

高亮提示:我们还会对一些监控事件进行高亮提示,比如局部区域的网络流量疏散及就近的 服务部署,这些信息对于全面了解服务的健康情况至关重要,尤其是在真正发生系统故障的情况下。

这就是我们的 Telltale 监控。它现已成功运行并提供监控服务,监控着 Netflix 100 多个生产应用程序的运行状况。

2.png

应用程序健康评估模型

微服务并非是孤立存在和运行的。它需要特定的依赖,与其他服务进行数据交互,甚至位于不同的 AWS 区域。

上面的调用图是一个相对简单的图,其中涉及许多服务,实际的调用链可能会更深更复杂。

一个应用程序是系统生态的一部分,它的运行状态可能会受到相关属性变化的微弱影响,也有可能会受到区域范围内某些事件的影响从而发生根本性改变。

canary 的启动可能会对应用程序产生一定影响。在一定程度上,上游或下游服务的部署同样也可以带来一定的影响。

Telltale 通过使用多个维度的数据源构建一个不断自我优化的模型来监控应用程序的健康度:
  • Atlas 时序指标
  • 区域网络流量疏散
  • Mantis 实时流数据
  • 基础架构变更事件
  • Canary 部署及使用
  • 上、下游服务的运行状况
  • 表征 QoE 的相关指标
  • 告警平台发出的报警


不同的数据源对应用程序健康度的影响权重不同。例如,与错误率增加相比,响应时间的增加对应用程序的影响要小很多。

错误代码有很多,但是某些特定的错误代码的影响要比其他错误代码的影响大。在服务下游部署 canary 可能不如在上游部署带来的效果明显。

区域网络流量转移意味着某个区域的网络流量降为零而另一个区域的网络流量会加倍。

你可以感受下不同的指标对于监控的影响。监控指标的具体含义决定了我们应该如何科学有效地使用它来进行监控。

在构建应用程序健康状况视图时,Telltale 考虑了所有这些因素。应用程序健康评估模型是 Telltale 的核心。

智能监控

每个服务运维人员都知道告警阈值调整的难度。将阈值设置得太低,你会收到大量虚假告警。

如果过度补偿并放宽告警阈值,就会错过重要的异常警告。这样导致的最终结果是对告警缺乏信任。Telltale 可以帮助你免除不断调整相关配置的繁琐工作。

通过提供准确的和严格管理的数据源,我们能让应用程序所有者的设置和配置过程变得更加容易。

这些数据源通过按照一定的组合应用到程序的配置中,以实现最常见的服务类型配置。

Telltale 可以自动追踪服务之间的依赖关系,以构建应用程序健康评估模型中的拓扑。

通过数据源管理以及拓扑监测,在不用付出很大的努力情况下就能使配置保持最新状态。那些需要手动实践的一些场景仍然支持手动配置和调整。

没有任何一个独立的算法可以适用我们所有的监控场景。因此,我们采用了混合算法,包括统计算法、基于规则的算法和机器学习算法。

不久后,我们将在 Netflix Tech Blog 上发表一篇针对我们监控算法的文章。

Telltale 还具有分析器,可用于趋势探测或内存泄漏监测。智能监控意味着我们的用户可以信赖我们的监控结果。

这表明故障发生时,用户能更快地定位和解决系统异常问题。

智能告警

智能监控必然会促进智能告警。当 Telltale 检测到应用程序中的运行异常时,就会产生异常事件。

团队可以选择通过 Slack、电子邮件或 PagerDuty(均由我们的内部告警系统提供支持)进行告警。

如果该异常问题是由上游或下游系统引起的,则 Telltale 的上下文感知路由会提醒服务对应的维护团队。

智能告警还意味着运维团队针对特定异常只会收到一个通知,也就是说,告警风暴已经成为过去式。

3.png

Slack 中的 Telltale 通知示例

在系统出现问题时,掌握准确的信息至关重要。我们的 Slack 告警程序还会启动一个包含有关事件上下文信息的线程,提供 Telltale 识别到的异常问题信息及问题产生的原因。

正确的上下文可以方便我们了解应用程序的当前状态,以便值班运维的工程师能有针对性的定位和修复问题。

异常告警事件会不断发展而且拥有自己的生命周期,因此及时更新事件状态至关重要。告警异常是好转了还是恶化了?是否要考虑新的监控信息或事件?

Telltale 在当前事件发生改变时会更新 Slack 线程。系统返回正常状态后,该线程将被标记为“已解决”,因此用户一眼就能知道哪些异常事件正在处理中,哪些异常事件已成功修复。

这些 Slack 线程不仅仅适用于 Telltale。团队还可以用它们来共享有关事件的其他数据,方便进一步观察、理论分析和讨论。

异常信息数据和讨论全部集中在一个线程中,方便达成针对当前异常的共识,有利于更快提出问题的解决方案以及异常事件的事后分析。

我们致力于提高 Telltale 告警的质量。一种方法是向我们的用户学习。因此,我们在 Slack 消息中提供了反馈按钮。

用户可以告诉我们以后某些情况不需要再发生告警,或提供某些告警不合理的原因。智能告警意味着用户可以信赖我们的告警。

4.png

在 Slack 的 Telltale 通知中描述异常详细信息的一个示例

为什么我的应用服务运行状态欠佳?各种类型的监控数据、应用程序相关知识以及跨多种服务数据的相关性,有助于 Telltale 检测分析应用程序运行健康度降低的原因。

这些原因包括实例异常、相关依赖的监测和部署异常、数据库异常或者网络流量高峰等。突出高亮显示这些可能的原因可以帮助运维人员节省大量宝贵的时间。

异常事件管理

5.png

Telltale 异常事件摘要的一个示例

当 Telltale 发送告警时,它还会创建一个快照,其中引用了不正常的监控信号数据。随着新监控信息的到来,会将其添加到此快照中。

这简化了团队的很多事后审查流程。当需要复查过去的异常问题时,“应用程序事件摘要”功能可以从各个方面显示当前的问题,包括一些关键指标,比如总停机时间和 MTTR(平均解决时间)。

我们希望帮助我们的团队了解更多的异常事件的模式,以便提高我们服务的整体可用性。

6.png

集群视图下将相似异常事件分组

部署监控

可以看出,Telltale 的应用程序健康评估模型及其智能监控功能非常强大,所以我们也会将其应用于安全部署方面。我们从开放源码交付平台 Spinnaker 开始测试。

随着 Spinnaker 逐渐推出新版本,我们使用 Telltale 连续监监控运行新版本实例的运行状态。

持续监控意味着新部署在问题出现时能自行停止并进行回滚操作。这意味着部署存在问题时的影响半径较小,持续时间更短。

7.png

持续优化

在复杂的系统中,运行微服务非常具有挑战性。Telltale 的智能监控和告警功能可以帮助我们运维人员提高系统可用性、降低运维人员的劳动强度并减少工作人员大半夜被叫醒的频率。

我们为 Telltale 做到的这些功能提升感到高兴。但是远没有结束,我们仍在不断探索新算法,以提高告警的准确性。

我们将在以后的 Netflix Tech Blog 文章中详细介绍我们的工作进展。我们仍然在对应用程序健康评估模型进行进一步评估和改进。

我们相信服务运行日志和跟踪数据中会包含更多有价值的信息,这样我们就能采集到更有用的指标数据。我们很期待与平台其他团队进行合作,共同开发这些新功能。

将新应用监控引入 Telltale 可以享受到很好的服务体验,但是无法很好的进行扩展,所以我们绝对可以优化和提高自服务的用户界面。

我们确信,有更好的启发式方法能帮助用户找出影响服务健康度的一些因素。Telltale 简化了应用程序的监控。

原文链接:https://mp.weixin.qq.com/s/ZnpYMy2rupJS9hHA5WWyew
继续阅读 »

【编者的话】本文阐述了 Netflix 的系统监控实践:自研 Telltale,成功运行并监控着 Netflix 100 多个生产应用程序的运行状况。

难忘的经历

相信很多运维人都有过这样的经历:监控系统某个指标超过阈值,触发告警。大半夜里,你被紧急召唤。

半睁着眼,你满脸疑惑:“系统真出问题了吗,还是仅仅需要调整下告警?上一次有人调整我们的告警阈值是在什么时候?有没有可能是上游或者下游的服务出现了问题?”

鉴于这是一次非常重要的应用告警,因此你不得不从床上爬起来,迅速打开电脑,然后浏览监控仪表盘来追踪问题源头。

忙了半天,你还没确认这个告警是来自于系统的问题,但也意识到,从海量数据中寻找线索时,时间正在流逝。你必须尽快定位告警的原因,并祈祷系统稳定运行。

对我们的用户来讲,稳健的 Netflix 服务至关重要。当你坐下来看《养虎为患》时,你肯定希望它能顺利播放。

多年来,我们从经常在深夜被召唤的工程师那里了解到应用程序监控的痛点:

  • 过多的告警
  • 太多滚动浏览的仪表盘
  • 太多的配置
  • 过多的维护


Telltale

我们的流媒体团队需要一个全新的监控系统,可以让团队成员快速地诊断和修复问题;因为在系统告警的紧急情况下,每一秒都至关重要!

我们的 Node 团队 需要一个仅需一小撮人就能运维大型集群的系统。因此,我们构建了 Telltale。

1.png

Telltale 监控时间轴

Telltale 的特性如下:

汇集监控数据源,创建整体监控视图:Telltale 汇集了各种监控数据源,从而能创建关于应用程序运行状况的整体监控视图。

多维度判断应用程序的健康状况:Telltale 可以通过多个维度判断一个应用程序的健康情况,而无需根据单一指标频繁调整告警阈值。

及时告警:因为我们知道应用程序在什么情况下是正常的,所以能在应用程序有异常趋势时及时通知应用程序的所有者。

显示关键数据:指标是了解应用程序运行状态的关键。但很多时候,你拥有太多的指标、太多的图表以及太多的监控仪表盘。而 Telltale 仅显示应用程序中有用的相关数据及其上游和下游服务的数据。

用颜色区分问题的严重程度:我们使用不同的颜色来表示问题的严重程度(除选择颜色之外,还可以让 Telltale 显示不同的数字),以便运维人员一眼就能判断出应用程序的运行状况。

高亮提示:我们还会对一些监控事件进行高亮提示,比如局部区域的网络流量疏散及就近的 服务部署,这些信息对于全面了解服务的健康情况至关重要,尤其是在真正发生系统故障的情况下。

这就是我们的 Telltale 监控。它现已成功运行并提供监控服务,监控着 Netflix 100 多个生产应用程序的运行状况。

2.png

应用程序健康评估模型

微服务并非是孤立存在和运行的。它需要特定的依赖,与其他服务进行数据交互,甚至位于不同的 AWS 区域。

上面的调用图是一个相对简单的图,其中涉及许多服务,实际的调用链可能会更深更复杂。

一个应用程序是系统生态的一部分,它的运行状态可能会受到相关属性变化的微弱影响,也有可能会受到区域范围内某些事件的影响从而发生根本性改变。

canary 的启动可能会对应用程序产生一定影响。在一定程度上,上游或下游服务的部署同样也可以带来一定的影响。

Telltale 通过使用多个维度的数据源构建一个不断自我优化的模型来监控应用程序的健康度:
  • Atlas 时序指标
  • 区域网络流量疏散
  • Mantis 实时流数据
  • 基础架构变更事件
  • Canary 部署及使用
  • 上、下游服务的运行状况
  • 表征 QoE 的相关指标
  • 告警平台发出的报警


不同的数据源对应用程序健康度的影响权重不同。例如,与错误率增加相比,响应时间的增加对应用程序的影响要小很多。

错误代码有很多,但是某些特定的错误代码的影响要比其他错误代码的影响大。在服务下游部署 canary 可能不如在上游部署带来的效果明显。

区域网络流量转移意味着某个区域的网络流量降为零而另一个区域的网络流量会加倍。

你可以感受下不同的指标对于监控的影响。监控指标的具体含义决定了我们应该如何科学有效地使用它来进行监控。

在构建应用程序健康状况视图时,Telltale 考虑了所有这些因素。应用程序健康评估模型是 Telltale 的核心。

智能监控

每个服务运维人员都知道告警阈值调整的难度。将阈值设置得太低,你会收到大量虚假告警。

如果过度补偿并放宽告警阈值,就会错过重要的异常警告。这样导致的最终结果是对告警缺乏信任。Telltale 可以帮助你免除不断调整相关配置的繁琐工作。

通过提供准确的和严格管理的数据源,我们能让应用程序所有者的设置和配置过程变得更加容易。

这些数据源通过按照一定的组合应用到程序的配置中,以实现最常见的服务类型配置。

Telltale 可以自动追踪服务之间的依赖关系,以构建应用程序健康评估模型中的拓扑。

通过数据源管理以及拓扑监测,在不用付出很大的努力情况下就能使配置保持最新状态。那些需要手动实践的一些场景仍然支持手动配置和调整。

没有任何一个独立的算法可以适用我们所有的监控场景。因此,我们采用了混合算法,包括统计算法、基于规则的算法和机器学习算法。

不久后,我们将在 Netflix Tech Blog 上发表一篇针对我们监控算法的文章。

Telltale 还具有分析器,可用于趋势探测或内存泄漏监测。智能监控意味着我们的用户可以信赖我们的监控结果。

这表明故障发生时,用户能更快地定位和解决系统异常问题。

智能告警

智能监控必然会促进智能告警。当 Telltale 检测到应用程序中的运行异常时,就会产生异常事件。

团队可以选择通过 Slack、电子邮件或 PagerDuty(均由我们的内部告警系统提供支持)进行告警。

如果该异常问题是由上游或下游系统引起的,则 Telltale 的上下文感知路由会提醒服务对应的维护团队。

智能告警还意味着运维团队针对特定异常只会收到一个通知,也就是说,告警风暴已经成为过去式。

3.png

Slack 中的 Telltale 通知示例

在系统出现问题时,掌握准确的信息至关重要。我们的 Slack 告警程序还会启动一个包含有关事件上下文信息的线程,提供 Telltale 识别到的异常问题信息及问题产生的原因。

正确的上下文可以方便我们了解应用程序的当前状态,以便值班运维的工程师能有针对性的定位和修复问题。

异常告警事件会不断发展而且拥有自己的生命周期,因此及时更新事件状态至关重要。告警异常是好转了还是恶化了?是否要考虑新的监控信息或事件?

Telltale 在当前事件发生改变时会更新 Slack 线程。系统返回正常状态后,该线程将被标记为“已解决”,因此用户一眼就能知道哪些异常事件正在处理中,哪些异常事件已成功修复。

这些 Slack 线程不仅仅适用于 Telltale。团队还可以用它们来共享有关事件的其他数据,方便进一步观察、理论分析和讨论。

异常信息数据和讨论全部集中在一个线程中,方便达成针对当前异常的共识,有利于更快提出问题的解决方案以及异常事件的事后分析。

我们致力于提高 Telltale 告警的质量。一种方法是向我们的用户学习。因此,我们在 Slack 消息中提供了反馈按钮。

用户可以告诉我们以后某些情况不需要再发生告警,或提供某些告警不合理的原因。智能告警意味着用户可以信赖我们的告警。

4.png

在 Slack 的 Telltale 通知中描述异常详细信息的一个示例

为什么我的应用服务运行状态欠佳?各种类型的监控数据、应用程序相关知识以及跨多种服务数据的相关性,有助于 Telltale 检测分析应用程序运行健康度降低的原因。

这些原因包括实例异常、相关依赖的监测和部署异常、数据库异常或者网络流量高峰等。突出高亮显示这些可能的原因可以帮助运维人员节省大量宝贵的时间。

异常事件管理

5.png

Telltale 异常事件摘要的一个示例

当 Telltale 发送告警时,它还会创建一个快照,其中引用了不正常的监控信号数据。随着新监控信息的到来,会将其添加到此快照中。

这简化了团队的很多事后审查流程。当需要复查过去的异常问题时,“应用程序事件摘要”功能可以从各个方面显示当前的问题,包括一些关键指标,比如总停机时间和 MTTR(平均解决时间)。

我们希望帮助我们的团队了解更多的异常事件的模式,以便提高我们服务的整体可用性。

6.png

集群视图下将相似异常事件分组

部署监控

可以看出,Telltale 的应用程序健康评估模型及其智能监控功能非常强大,所以我们也会将其应用于安全部署方面。我们从开放源码交付平台 Spinnaker 开始测试。

随着 Spinnaker 逐渐推出新版本,我们使用 Telltale 连续监监控运行新版本实例的运行状态。

持续监控意味着新部署在问题出现时能自行停止并进行回滚操作。这意味着部署存在问题时的影响半径较小,持续时间更短。

7.png

持续优化

在复杂的系统中,运行微服务非常具有挑战性。Telltale 的智能监控和告警功能可以帮助我们运维人员提高系统可用性、降低运维人员的劳动强度并减少工作人员大半夜被叫醒的频率。

我们为 Telltale 做到的这些功能提升感到高兴。但是远没有结束,我们仍在不断探索新算法,以提高告警的准确性。

我们将在以后的 Netflix Tech Blog 文章中详细介绍我们的工作进展。我们仍然在对应用程序健康评估模型进行进一步评估和改进。

我们相信服务运行日志和跟踪数据中会包含更多有价值的信息,这样我们就能采集到更有用的指标数据。我们很期待与平台其他团队进行合作,共同开发这些新功能。

将新应用监控引入 Telltale 可以享受到很好的服务体验,但是无法很好的进行扩展,所以我们绝对可以优化和提高自服务的用户界面。

我们确信,有更好的启发式方法能帮助用户找出影响服务健康度的一些因素。Telltale 简化了应用程序的监控。

原文链接:https://mp.weixin.qq.com/s/ZnpYMy2rupJS9hHA5WWyew 收起阅读 »

Cilium 首次集成国内云服务,阿里云 ENI 被纳入新版本特性


作者:清弦
阿里云技术专家,主要负责 ACK 容器网络设计与研发,阿里云开源 CNI 项目 Terway 主要维护者,Cilium Alibaba IPAM 负责人

背景


近期 Cilium 社区发布了 Cilium 1.10 正式版本,在这个版本中正式支持阿里云 ENI 模式,阿里云也是国内首家支持 Cilium 的云厂商。

1.png

Cilium 是一个基于 eBPF 的高性能容器网络项目,提供网络、可观测性、安全三方面的解决方案。

2.png

Cilium 本身支持 Overlay 网络模式部署在各种云平台或者自建的集群上,但是这种非云原生的网络模式会带来不小的性能损耗。阿里巴巴云原生容器服务团队向 Cilium 社区贡献了阿里云 ENI 模式,使得在阿里云上可以以云原生方式运行 Cilium 。

云原生容器服务团队贡献 PR
_https://github.com/cilium/cilium/pull/15160_
_https://github.com/cilium/cilium/pull/15512_

架构


AlibabaCloud Operator 是集群内的网络资源控制器,承担对网络资源(ENI、ENIIP)统一管理、分配工作。

3.png

Cilium agent 通过 list-watch 机制、CNI 请求对 Operator 分配的地址资源进行配置、管理。

这种架构将所有阿里云 OpenAPI 调用集中到 Operator 中,可以有效的进行 API 请求管理,避免大规模集群下 API 流控问题。

4.png

基于 Cilium 1.10 + 阿里云 ENI 的高性能云原生网络


Cilium 使用了 EBPF 内核技术对传统数据链路进行了优化,绕过了Conntrack 模块,对容器场景下网络性能有了非常大的提高。在阿里云上使用 Cilium 1.10 + 阿里云 ENI 模式有多种按照方式,请阅读 Cilium 社区的安装文档[1]。

为了使云上用户享受到更加出色的网络性能,阿里云自研的开源 CNI 插件 Terway [2] 与 Cilium 实现了更好的结合。Terway 支持使用阿里云的弹性网卡来实现的容器网络。使得容器网络和虚拟机网络在同一个网络平面,在不同主机之间容器网络通信时不会有封包等损失,不依赖于分布式路由也能让集群规模不受限于路由条目限制。目前,Terway IPvlan 模式已经深度集成 Cilium 。

使用 Terway IPvlan

使用 Terway 模式非常简单,在阿里云容器服务控制台,创建集群中选择网络插件 Terway ,并勾选 IPvlan 即可启用。

5.png

IPvlan + eBPF 性能对比:


测试环境:
  • 2 节点 ecs . g5ne . 4xlarge 机型
  • 对比测试


Terway 独占 ENI ( ipvs )
Terway 共享 ENI IPvlan ( ebpf )
Terway 共享 ENI veth ( ipvs )
Flanne l vxlan ( ipvs )

Netperf 性能对比 TCP_CRR

测试场景:使用 netperf 测试 Pod 间通讯

6.png

上图数字越大性能越好

通过测试,可以看到基于 IPvlan 的 Pod 网络延迟较低,在 TCP_CRR 的测试中性能指标和独占 ENI 模式相当。

wrk + nginx 性能对比

测试场景:采用 wrk 压测 nginx 的 Service 的方式,采用 100 字节的小页面模拟常见的集群中微服务通信。

7.png

上图数字越小性能越好

8.png

上图数字越大性能越好

Terway IPvlan 模式在 wrk- nginx 的短连接测试中相对于传统的 Terway veth 策略路由方式:
  • ClusterIP 吞吐增加 277% , 延迟降低 50%。


总结


随着 Kubernetes 已经成为容器调度的事实标准,企业上云的首选。容器网络做为应用的底层基础资源,得到越来越多的关注。

在阿里云上我们默认提供高性能的 Terway 网络插件 [3] 帮助用户充分使用云原生的网络资源。Cilium 作为社区新兴的容器网络方案,在可观测性、安全性上有许多出色的特性,本次增加的阿里云ENI模式,可以帮助 Cilium 的用户充分使用阿里云上的网络资源。我们也将继续与社区同行,推动高性能的云原生网络实现规模化落地。

安装文档:_https://docs.cilium.io/en/v1.1 ... -eni_

Terway: _https://www.alibabacloud.com/h ... .htm_
Terway 网络插件:_ https://help.aliyun.com/docume ... html_
继续阅读 »

作者:清弦
阿里云技术专家,主要负责 ACK 容器网络设计与研发,阿里云开源 CNI 项目 Terway 主要维护者,Cilium Alibaba IPAM 负责人

背景


近期 Cilium 社区发布了 Cilium 1.10 正式版本,在这个版本中正式支持阿里云 ENI 模式,阿里云也是国内首家支持 Cilium 的云厂商。

1.png

Cilium 是一个基于 eBPF 的高性能容器网络项目,提供网络、可观测性、安全三方面的解决方案。

2.png

Cilium 本身支持 Overlay 网络模式部署在各种云平台或者自建的集群上,但是这种非云原生的网络模式会带来不小的性能损耗。阿里巴巴云原生容器服务团队向 Cilium 社区贡献了阿里云 ENI 模式,使得在阿里云上可以以云原生方式运行 Cilium 。

云原生容器服务团队贡献 PR
_https://github.com/cilium/cilium/pull/15160_
_https://github.com/cilium/cilium/pull/15512_

架构


AlibabaCloud Operator 是集群内的网络资源控制器,承担对网络资源(ENI、ENIIP)统一管理、分配工作。

3.png

Cilium agent 通过 list-watch 机制、CNI 请求对 Operator 分配的地址资源进行配置、管理。

这种架构将所有阿里云 OpenAPI 调用集中到 Operator 中,可以有效的进行 API 请求管理,避免大规模集群下 API 流控问题。

4.png

基于 Cilium 1.10 + 阿里云 ENI 的高性能云原生网络


Cilium 使用了 EBPF 内核技术对传统数据链路进行了优化,绕过了Conntrack 模块,对容器场景下网络性能有了非常大的提高。在阿里云上使用 Cilium 1.10 + 阿里云 ENI 模式有多种按照方式,请阅读 Cilium 社区的安装文档[1]。

为了使云上用户享受到更加出色的网络性能,阿里云自研的开源 CNI 插件 Terway [2] 与 Cilium 实现了更好的结合。Terway 支持使用阿里云的弹性网卡来实现的容器网络。使得容器网络和虚拟机网络在同一个网络平面,在不同主机之间容器网络通信时不会有封包等损失,不依赖于分布式路由也能让集群规模不受限于路由条目限制。目前,Terway IPvlan 模式已经深度集成 Cilium 。

使用 Terway IPvlan

使用 Terway 模式非常简单,在阿里云容器服务控制台,创建集群中选择网络插件 Terway ,并勾选 IPvlan 即可启用。

5.png

IPvlan + eBPF 性能对比:


测试环境:
  • 2 节点 ecs . g5ne . 4xlarge 机型
  • 对比测试


Terway 独占 ENI ( ipvs )
Terway 共享 ENI IPvlan ( ebpf )
Terway 共享 ENI veth ( ipvs )
Flanne l vxlan ( ipvs )

Netperf 性能对比 TCP_CRR

测试场景:使用 netperf 测试 Pod 间通讯

6.png

上图数字越大性能越好

通过测试,可以看到基于 IPvlan 的 Pod 网络延迟较低,在 TCP_CRR 的测试中性能指标和独占 ENI 模式相当。

wrk + nginx 性能对比

测试场景:采用 wrk 压测 nginx 的 Service 的方式,采用 100 字节的小页面模拟常见的集群中微服务通信。

7.png

上图数字越小性能越好

8.png

上图数字越大性能越好

Terway IPvlan 模式在 wrk- nginx 的短连接测试中相对于传统的 Terway veth 策略路由方式:
  • ClusterIP 吞吐增加 277% , 延迟降低 50%。


总结


随着 Kubernetes 已经成为容器调度的事实标准,企业上云的首选。容器网络做为应用的底层基础资源,得到越来越多的关注。

在阿里云上我们默认提供高性能的 Terway 网络插件 [3] 帮助用户充分使用云原生的网络资源。Cilium 作为社区新兴的容器网络方案,在可观测性、安全性上有许多出色的特性,本次增加的阿里云ENI模式,可以帮助 Cilium 的用户充分使用阿里云上的网络资源。我们也将继续与社区同行,推动高性能的云原生网络实现规模化落地。

安装文档:_https://docs.cilium.io/en/v1.1 ... -eni_

Terway: _https://www.alibabacloud.com/h ... .htm_
Terway 网络插件:_ https://help.aliyun.com/docume ... html_ 收起阅读 »

微服务之间的最佳调用方式


在微服务架构中,需要调用很多服务才能完成一项功能。服务之间如何互相调用就变成微服务架构中的一个关键问题。服务调用有两种方式,一种是RPC方式,另一种是事件驱动(Event-driven)方式,也就是发消息方式。消息方式是松耦合方式,比紧耦合的RPC方式要优越,但RPC方式如果用在适合的场景也有它的一席之地。

耦合的种类

我们总在谈耦合,那么耦合到底意味着什么呢?
  • 时间耦合:客户端和服务端必须同时上线才能工作。发消息时,接受消息队列必须运行,但后台处理程序暂时不工作也不影响。
  • 容量耦合:客户端和服务端的处理容量必须匹配。发消息时,如果后台处理能力不足也不要紧,消息队列会起到缓冲的作用。
  • 接口耦合:RPC调用有函数标签,而消息队列只是一个消息。例如买了商品之后要调用发货服务,如果是发消息,那么就只需发送一个商品被买消息。
  • 发送方式耦合:RPC是点对点方式,需要知道对方是谁,它的好处是能够传回返回值。消息既可以点对点,也可以用广播的方式,这样减少了耦合,但也使返回值比较困难。


下面我们来逐一分析这些耦合的影响。 第一,时间耦合,对于多数应用来讲,你希望能马上得到回答,因此即使使用消息队列,后台也需要一直工作。第二,容量耦合,如果你对回复有时间要求,那么消息队列的缓冲功能作用不大,因为你希望及时响应。真正需要的是自动伸缩(Auto-scaling),它能自动调整服务端处理能力去匹配请求数量。第三和第四,接口耦合和发送方式耦合,这两个确实是RPC方式的软肋。

事件驱动(Event-Driven)方式

Martin Fowler把事件驱动分成四种方式(What do you mean by “Event-Driven”),简化之后本质上只有两种方式。 一种就是我们熟悉的的事件通知(Event Notification),另一种是事件溯源(Event Sourcing)。事件通知就是微服务之间不直接调用,而是通过发消息来进行合作。事件溯源有点像记账,它把所有的事件都记录下来,作为永久存储层,再在它的基础之上构建应用程序。实际上从应用的角度来讲,它们并不应该分属一类,它们的用途完全不同。事件通知是微服务的调用(或集成)方式,应该和RPC分在一起。事件溯源是一种存储数据的方式,应该和数据库分在一起。

事件通知(Event Notification)方式

让我们用具体的例子来看一下。在下面的例子中,有三个微服务,“Order Service”,“Customer Service” 和“Product Service”。
1.jpeg

先说读数据,假设要创建一个“Order”,在这个过程中需要读取“Customer”的数据和“Product”数据。如果用事件通知的方式就只能在“Order Service”本地也创建只读“Customer”和“Product”表,并把数据用消息的方式同步过来。

再说写数据,如果在创建一个“Order”时需要创建一个新的“Customer”或要修改“Customer”的信息,那么可以在界面上跳转到用户创建页面,然后在“Customer Service”创建用户之后再发”用户已创建“的消息,“Order Service”接到消息,更新本地“Customer”表。

这并不是一个很好的使用事件驱动的例子,因为事件驱动的优点就是不同的程序之间可以独立运行,没有绑定关系。但现在“Order Service”需要等待“Customer Service”创建完了之后才能继续运行,来完成整个创建“Order”的工作。主要是因为“Order”和“Customer”本身从逻辑上来讲就是紧耦合关系,没有“Customer”你是不能创建“Order”的。

在这种紧耦合的情况下,也可以使用RPC。你可以建立一个更高层级的管理程序来管理这些微服务之间的调用,这样“Order Service”就不必直接调用“Customer Service”了。当然它从本质上来讲并没有解除耦合,只是把耦合转移到了上一层,但至少现在“order Service”和“Customer Service”可以互不影响了。之所以不能根除这种紧耦合关系是因为它们在业务上是紧耦合的。

再举一个购物的例子。用户选好商品之后进行“Checkout”,生成“Order”,然后需要“payment”,再从“Inventory”取货,最后由“Shipment”发货,它们每一个都是微服务。这个例子用RPC方式和事件通知方式都可以完成。当用RPC方式时,由“Order”服务调用其他几个服务来完成整个功能。用事件通知方式时,“Checkout”服务完成之后发送“Order Placed”消息,“Payment”服务收到消息,接收用户付款,发送“Payment received”消息。“Inventory”服务收到消息,从仓库里取货,并发送“Goods fetched”消息。“Shipment”服务得到消息,发送货物,并发送“Goods shipped”消息。
2.png

对这个例子来讲,使用事件驱动是一个不错的选择,因为每个服务发消息之后它不需要任何反馈,这个消息由下一个模块接收来完成下一步动作,时间上的要求也比上一个要宽松。用事件驱动的好处是降低了耦合度,坏处是你现在不能在程序里找到整个购物过程的步骤。如果一个业务逻辑有它自己相对固定的流程和步骤,那么使用RPC或业务流程管理(BPM)能够更方便地管理这些流程。在这种情况下选哪种方案呢?在我看来好处和坏处是大致相当的。从技术上来讲要选事件驱动,从业务上来讲要选RPC。不过现在越来越多的人采用事件通知作为微服务的集成方式,它似乎已经成了微服务之间的标椎调用方式。

事件溯源(Event Sourcing)

这是一种具有颠覆性质的的设计,它把系统中所有的数据都以事件(Event)的方式记录下来,它的持久存储叫Event Store, 一般是建立在数据库或消息队列(例如Kafka)基础之上,并提供了对事件进行操作的接口,例如事件的读写和查询。事件溯源是由领域驱动设计(Domain-Driven Design)提出来的。DDD中有一个很重要的概念,有界上下文(Bounded Context),可以用有界上下文来划分微服务,每个有界上下文都可以是一个微服务。 下面是有界上下文的示例。下图中有两个服务“Sales”和“Support”。有界上下文的一个关键是如何处理共享成员, 在图中是“Customer”和“Product”。在不同的有界上下文中,共享成员的含义、用法以及他们的对象属性都会有些不同,DDD建议这些共享成员在各自的有界上下文中都分别建自己的类(包括数据库表),而不是共享。可以通过数据同步的手段来保持数据的一致性。下面还会详细讲解。
3.png

事件溯源是微服务的一种存储方式,它是微服务的内部实现细节。因此你可以决定哪些微服务采用事件溯源方式,哪些不采用,而不必所有的服务都变成事件溯源的。 通常整个应用程序只有一个Event Store, 不同的微服务都通过向Event Store发送和接受消息而互相通信。Event Store内部可以分成不同的stream(相当于消息队列中的Topic), 供不同的微服务中的领域实体(Domain Entity)使用。

事件溯源的一个短板是数据查询,它有两种方式来解决。第一种是直接对stream进行查询,这只适合stream比较小并且查询比较简单的情况。查询复杂的话,就要采用第二种方式,那就是建立一个只读数据库,把需要的数据放在库中进行查询。数据库中的数据通过监听Event Store中相关的事件来更新。

数据库存储方式只能保存当前状态,而事件溯源则存储了所有的历史状态,因而能根据需要回放到历史上任何一点的状态,具有很大优势。但它也不是一点问题都没有。第一,它的程序比较复杂,因为事件是一等公民,你必须把业务逻辑按照事件的方式整理出来,然后用事件来驱动程序。第二,如果你要想修改事件或事件的格式就比较麻烦,因为旧的事件已经存储在Event Store里了(事件就像日志,是只读的),没有办法再改。

由于事件溯源和事件通知表面上看起来很像,不少人都搞不清楚它们的区别。事件通知只是微服务的集成方式,程序内部是不使用事件溯源的,内部实现仍然是传统的数据库方式。只有当要与其他微服务集成时才会发消息。而在事件溯源中,事件是一等公民,可以不要数据库,全部数据都是按照事件的方式存储的。

虽然事件溯源的践行者有不同的意见,但有不少人都认为事件溯源不是微服务的集成方式,而是微服务的一种内部实现方式。因此,在一个系统中,可以某些微服务用事件溯源,另外一些微服务用数据库。当你要集成这些微服务时,你可以用事件通知的方式。注意现在有两种不同的事件需要区分开,一种是微服务的内部事件,是颗粒度比较细的,这种事件只发送到这个微服务的stream中,只被事件溯源使用。另一种是其他微服务也关心的,是颗粒度比较粗的,这种事件会放到另外一个或几个stream中,被多个微服务使用,是用来做服务之间集成的。这样做的好处是限制了事件的作用范围,减少了不相关事件对程序的干扰。详见:https://www.innoq.com/en/blog/ ... cing/

事件溯源出现已经很长时间了,虽然热度一直在上升(尤其是这两年),但总的来说非常缓慢,谈论的人不少,但生产环境使用的不多。究其原因就是应为它对现在的体系结构颠覆太大,需要更改数据存储结构和程序的工作方式,还是有一定风险的。另外,微服务已经形成了一整套体系,从程序部署,服务发现与注册,到监控,服务韧性(Service Resilience),它们基本上都是针对RPC的,虽然也支持消息,但成熟度就差多了,因此有不少工作还是要自己来做。有意思的是Kafka一直在推动它作为事件驱动的工具,也取得了很大的成功。但它却没有得到事件溯源圈内的认可(详见这里)。

多数事件溯源都使用一个叫evenstore的开源Event Store,或是基于某个数据库的Event Store,只有比较少的人用Kafka做Event Store。 但如果用Kafka实现事件通知就一点问题都没有。总的来说,对大多数公司来讲事件溯源是有一定挑战的,应用时需要找到合适的场景。如果你要尝试的话,可以先拿一个微服务试水。

虽然现在事件驱动还有些生涩,但从长远来讲,还是很看好它的。像其他全新的技术一样,事件溯源需要大规模的适用场景来推动。例如容器技术就是因为微服务的流行和推动,才走向主流。事件溯源以前的适用场景只限于记账和源代码库,局限性较大。区块链可能会成为它的下一个机遇,因为它用的也是事件溯源技术。另外AI今后会渗入到具体程序中,使程序具有学习功能。而RPC模式注定没有自适应功能。事件驱动本身就具有对事件进行反应的能力,这是自我学习的基础。因此,这项技术长远来讲定会大放异彩,但短期内(3-5年)大概不会成为主流。

RPC方式

RPC的方式就是远程函数调用,像RESTFul,gRPC,Dubbo都是这种方式。它一般是同步的,可以马上得到结果。在实际中,大多数应用都要求立刻得到结果,这时同步方式更有优势,代码也更简单。

服务网关(API Gateway)

熟悉微服务的人可能都知道服务网关(API Gateway)。当UI需要调用很多微服务时,它需要了解每个服务的接口,这个工作量很大。于是就用服务网关创建了一个Facade,把几个微服务封装起来,这样UI就只调用服务网关就可以了,不需要去对付每一个微服务。下面是API Gateway示例图:
4.jpg

服务网关(API Gateway)不是为了解决微服务之间调用的紧耦合问题,它主要是为了简化客户端的工作。其实它还可以用来降低函数之间的耦合度。 有了API Gateway之后,一旦服务接口修改,你可能只需要修改API Gateway, 而不必修改每个调用这个函数的客户端,这样就减少了程序的耦合性。

服务调用

可以借鉴API Gateway的思路来减少RPC调用的耦合度,例如把多个微服务组织起来形成一个完整功能的服务组合,并对外提供统一的服务接口。这种想法跟上面的API Gateway有些相似,都是把服务集中起来提供粗颗粒(Coarse Granular)服务,而不是细颗粒的服务(Fine Granular)。但这样建立的服务组合可能只适合一个程序使用,没有多少共享价值。因此如果有合适的场景就采用,否侧也不必强求。虽然我们不能降低RPC服务之间的耦合度,却可以减少这种紧耦合带来的影响。

降低紧耦合的影响

什么是紧耦合的主要问题呢?就是客户端和服务端的升级不同步。服务端总是先升级,客户端可能有很多,如果要求它们同时升级是不现实的。它们有各自的部署时间表,一般都会选择在下一次部署时顺带升级。

一般有两个办法可以解决这个问题:
  • 同时支持多个版本:这个工作量比较大,因此大多数公司都不会采用这种方式。
  • 服务端向后兼容:这是更通用的方式。例如你要加一个新功能或有些客户要求给原来的函数增加一个新的参数,但别的客户不需要这个参数。这时你只好新建一个函数,跟原来的功能差不多,只是多了一个参数。这样新旧客户的需求都能满足。它的好处是向后兼容(当然这取决于你使用的协议)。它的坏处是当以后新的客户来了,看到两个差不多的函数就糊涂了,不知道该用那个。而且时间越长越严重,你的服务端可能功能增加的不多,但相似的函数却越来越多,无法选择。


它的解决办法就是使用一个支持向后兼容的RPC协议,现在最好的就是Protobuf gRPC,尤其是在向后兼容上。它给每个服务定义了一个接口,这个接口是与编程语言无关的中性接口,然后你可以用工具生成各个语言的实现代码,供不同语言使用。函数定义的变量都有编号,变量可以是可选类型的,这样就比较好地解决了函数兼容的问题。就用上面的例子,当你要增加一个可选参数时,你就定义一个新的可选变量。由于它是可选的,原来的客户端不需要提供这个参数,因此不需要修改程序。而新的客户端可以提供这个参数。你只要在服务端能同时处理这两种情况就行了。这样服务端并没有增加新的函数,但用户的新需求满足了,而且还是向后兼容的。

微服务的数量有没有上限?

总的来说微服务的数量不要太多,不然会有比较重的运维负担。有一点需要明确的是微服务的流行不是因为技术上的创新,而是为了满足管理上的需要。单体程序大了之后,各个模块的部署时间要求不同,对服务器的优化要求也不同,而且团队人数众多,很难协调管理。把程序拆分成微服务之后,每个团队负责几个服务,就容易管理了,而且每个团队也可以按照自己的节奏进行创新,但它给运维带来了巨大的麻烦。所以在微服务刚出来时,我一直觉得它是一个退步,弊大于利。但由于管理上的问题没有其他解决方案,只有硬着头皮上了。值得庆幸的是微服务带来的麻烦都是可解的。直到后来,微服务建立了全套的自动化体系,从程序集成到部署,从全链路跟踪到日志,以及服务检测,服务发现和注册,这样才把微服务的工作量降了下来。虽然微服务在技术上一无是处,但它的流行还是大大推动了容器技术,服务网格(Service Mesh)和全链路跟踪等新技术的发展。不过它本身在技术上还是没有发现任何优势。。直到有一天,我意识到单体程序其实性能调试是很困难的(很难分离出瓶颈点),而微服务配置了全链路跟踪之后,能很快找到症结所在。看来微服务从技术来讲也不全是缺点,总算也有好的地方。但微服务的颗粒度不宜过细,否则工作量还是太大。

一般规模的公司十几个或几十个微服务都是可以承受的,但如果有几百个甚至上千个,那么绝不是一般公司可以管理的。尽管现有的工具已经很齐全了,而且与微服务有关的整个流程也已经基本上全部自动化了,但它还是会增加很多工作。Martin Fowler几年以前建议先从单体程序开始(详见:https://martinfowler.com/bliki/MonolithFirst.html),然后再逐步把功能拆分出去,变成一个个的微服务。但是后来有人反对这个建议,他也有些松口了。如果单体程序不是太大,这是个好主意。可以用数据额库表的数量来衡量程序的大小,我见过大的单体程序有几百张表,这就太多了,很难管理。正常情况下,一个微服务可以有两、三张表到五、六张表,一般不超过十张表。但如果要减少微服务数量的话,可以把这个标准放宽到不要超过二十张表。用这个做为大致的指标来创建微程序,如果使用一段时间之后还是觉得太大了,那么再逐渐拆分。当然,按照这个标准建立的服务更像是服务组合,而不是单个的微服务。不过它会为你减少工作量。只要不影响业务部门的创新进度,这是一个不错的方案。

到底应不应该选择微服务呢?如果单体程序已经没法管理了,那么你别无选择。如果没有管理上的问题,那么微服务带给你的只有问题和麻烦。其实,一般公司都没有太多选择,只能采用微服务,不过你可以选择建立比较少的微服务。如果还是没法决定,有一个折中的方案,“内部微服务设计”。

内部微服务设计

这种设计表面上看起来是一个单体程序,它只有一个源代码存储仓库,一个数据库,一个部署,但在程序内部可以按照微服务的思想来进行设计。它可以分成多个模块,每个模块是一个微服务,可以由不同的团队管理。
5.png

用这张图做例子。这个图里的每个圆角方块大致是一个微服务,但我们可以把它作为一个单体程序来设计,内部有五个微服务。每个模块都有自己的数据库表,它们都在一个数据库中,但模块之间不能跨数据库访问(不要建立模块之间数据库表的外键)。“User”(在Conference Management模块中)是一个共享的类,但在不同的模块中的名字不同,含义和用法也不同,成员也不一样(例如,在“Customer Service”里叫“Customer”)。DDD(Domain-Driven Design)建议不要共享这个类,而是在每一个有界上下文(模块)中都建一个新类,并拥有新的名字。虽然它们的数据库中的数据应该大致相同,但DDD建议每一个有界上下文中都建一个新表,它们之间再进行数据同步。

这个所谓的“内部微服务设计”其实就是DDD,但当时还没有微服务,因此外表看起来是单体程序,但内部已经是微服务的设计了。它的书在2003就出版了,当时就很有名。但它更偏重于业务逻辑的设计,践行起来也比较困难,因此大家谈论得很多,真正用的较少。直到十年之后,微服务出来之后,人们发现它其实内部就是微服务,而且微服务的设计需要用它的思想来指导,于是就又重新焕发了青春,而且这次更猛,已经到了每个谈论微服务的人都不得不谈论DDD的地步。不过一本软件书籍,在十年之后还能指导新技术的设计,非常令人钦佩。

这样设计的好处是它是一个单体程序,省去了多个微服务带来的部署、运维的麻烦。但它内部是按微服务设计的,如果以后要拆分成微服务会比较容易。至于什么时候拆分不是一个技术问题。如果负责这个单体程序的各个团队之间不能在部署时间表,服务器优化等方面达成一致,那么就需要拆分了。当然你也要应对随之而来的各种运维麻烦。内部微服务设计是一个折中的方案,如果你想试水微服务,但又不愿意冒太大风险时,这是一个不错的选择。

微服务的数据库设计也有很多内容,包括如何把服务从单体程序一步步里拆分出来请参见:https://blog.csdn.net/weixin_3 ... 34941

结论

微服务之间的调用有两种方式,RPC和事件驱动。事件驱动是更好的方式,因为它是松耦合的。但如果业务逻辑是紧耦合的,RPC方式也是可行的(它的好处是代码更简单),而且你还可以通过选取合适的协议(Protobuf gRPC)来降低这种紧耦合带来的危害。由于事件溯源和事件通知的相似性,很多人把两者弄混了,但它们实际上是完全不同的东西。微服务的数量不宜太多,可以先创建比较大的微服务(更像是服务组合)。如果你还是不能确定是否采用微服务架构,可以先从“内部微服务设计”开始,再逐渐拆分。

原文链接:https://blog.csdn.net/weixin_3 ... 62272,作者:倚天码农
继续阅读 »

在微服务架构中,需要调用很多服务才能完成一项功能。服务之间如何互相调用就变成微服务架构中的一个关键问题。服务调用有两种方式,一种是RPC方式,另一种是事件驱动(Event-driven)方式,也就是发消息方式。消息方式是松耦合方式,比紧耦合的RPC方式要优越,但RPC方式如果用在适合的场景也有它的一席之地。

耦合的种类

我们总在谈耦合,那么耦合到底意味着什么呢?
  • 时间耦合:客户端和服务端必须同时上线才能工作。发消息时,接受消息队列必须运行,但后台处理程序暂时不工作也不影响。
  • 容量耦合:客户端和服务端的处理容量必须匹配。发消息时,如果后台处理能力不足也不要紧,消息队列会起到缓冲的作用。
  • 接口耦合:RPC调用有函数标签,而消息队列只是一个消息。例如买了商品之后要调用发货服务,如果是发消息,那么就只需发送一个商品被买消息。
  • 发送方式耦合:RPC是点对点方式,需要知道对方是谁,它的好处是能够传回返回值。消息既可以点对点,也可以用广播的方式,这样减少了耦合,但也使返回值比较困难。


下面我们来逐一分析这些耦合的影响。 第一,时间耦合,对于多数应用来讲,你希望能马上得到回答,因此即使使用消息队列,后台也需要一直工作。第二,容量耦合,如果你对回复有时间要求,那么消息队列的缓冲功能作用不大,因为你希望及时响应。真正需要的是自动伸缩(Auto-scaling),它能自动调整服务端处理能力去匹配请求数量。第三和第四,接口耦合和发送方式耦合,这两个确实是RPC方式的软肋。

事件驱动(Event-Driven)方式

Martin Fowler把事件驱动分成四种方式(What do you mean by “Event-Driven”),简化之后本质上只有两种方式。 一种就是我们熟悉的的事件通知(Event Notification),另一种是事件溯源(Event Sourcing)。事件通知就是微服务之间不直接调用,而是通过发消息来进行合作。事件溯源有点像记账,它把所有的事件都记录下来,作为永久存储层,再在它的基础之上构建应用程序。实际上从应用的角度来讲,它们并不应该分属一类,它们的用途完全不同。事件通知是微服务的调用(或集成)方式,应该和RPC分在一起。事件溯源是一种存储数据的方式,应该和数据库分在一起。

事件通知(Event Notification)方式

让我们用具体的例子来看一下。在下面的例子中,有三个微服务,“Order Service”,“Customer Service” 和“Product Service”。
1.jpeg

先说读数据,假设要创建一个“Order”,在这个过程中需要读取“Customer”的数据和“Product”数据。如果用事件通知的方式就只能在“Order Service”本地也创建只读“Customer”和“Product”表,并把数据用消息的方式同步过来。

再说写数据,如果在创建一个“Order”时需要创建一个新的“Customer”或要修改“Customer”的信息,那么可以在界面上跳转到用户创建页面,然后在“Customer Service”创建用户之后再发”用户已创建“的消息,“Order Service”接到消息,更新本地“Customer”表。

这并不是一个很好的使用事件驱动的例子,因为事件驱动的优点就是不同的程序之间可以独立运行,没有绑定关系。但现在“Order Service”需要等待“Customer Service”创建完了之后才能继续运行,来完成整个创建“Order”的工作。主要是因为“Order”和“Customer”本身从逻辑上来讲就是紧耦合关系,没有“Customer”你是不能创建“Order”的。

在这种紧耦合的情况下,也可以使用RPC。你可以建立一个更高层级的管理程序来管理这些微服务之间的调用,这样“Order Service”就不必直接调用“Customer Service”了。当然它从本质上来讲并没有解除耦合,只是把耦合转移到了上一层,但至少现在“order Service”和“Customer Service”可以互不影响了。之所以不能根除这种紧耦合关系是因为它们在业务上是紧耦合的。

再举一个购物的例子。用户选好商品之后进行“Checkout”,生成“Order”,然后需要“payment”,再从“Inventory”取货,最后由“Shipment”发货,它们每一个都是微服务。这个例子用RPC方式和事件通知方式都可以完成。当用RPC方式时,由“Order”服务调用其他几个服务来完成整个功能。用事件通知方式时,“Checkout”服务完成之后发送“Order Placed”消息,“Payment”服务收到消息,接收用户付款,发送“Payment received”消息。“Inventory”服务收到消息,从仓库里取货,并发送“Goods fetched”消息。“Shipment”服务得到消息,发送货物,并发送“Goods shipped”消息。
2.png

对这个例子来讲,使用事件驱动是一个不错的选择,因为每个服务发消息之后它不需要任何反馈,这个消息由下一个模块接收来完成下一步动作,时间上的要求也比上一个要宽松。用事件驱动的好处是降低了耦合度,坏处是你现在不能在程序里找到整个购物过程的步骤。如果一个业务逻辑有它自己相对固定的流程和步骤,那么使用RPC或业务流程管理(BPM)能够更方便地管理这些流程。在这种情况下选哪种方案呢?在我看来好处和坏处是大致相当的。从技术上来讲要选事件驱动,从业务上来讲要选RPC。不过现在越来越多的人采用事件通知作为微服务的集成方式,它似乎已经成了微服务之间的标椎调用方式。

事件溯源(Event Sourcing)

这是一种具有颠覆性质的的设计,它把系统中所有的数据都以事件(Event)的方式记录下来,它的持久存储叫Event Store, 一般是建立在数据库或消息队列(例如Kafka)基础之上,并提供了对事件进行操作的接口,例如事件的读写和查询。事件溯源是由领域驱动设计(Domain-Driven Design)提出来的。DDD中有一个很重要的概念,有界上下文(Bounded Context),可以用有界上下文来划分微服务,每个有界上下文都可以是一个微服务。 下面是有界上下文的示例。下图中有两个服务“Sales”和“Support”。有界上下文的一个关键是如何处理共享成员, 在图中是“Customer”和“Product”。在不同的有界上下文中,共享成员的含义、用法以及他们的对象属性都会有些不同,DDD建议这些共享成员在各自的有界上下文中都分别建自己的类(包括数据库表),而不是共享。可以通过数据同步的手段来保持数据的一致性。下面还会详细讲解。
3.png

事件溯源是微服务的一种存储方式,它是微服务的内部实现细节。因此你可以决定哪些微服务采用事件溯源方式,哪些不采用,而不必所有的服务都变成事件溯源的。 通常整个应用程序只有一个Event Store, 不同的微服务都通过向Event Store发送和接受消息而互相通信。Event Store内部可以分成不同的stream(相当于消息队列中的Topic), 供不同的微服务中的领域实体(Domain Entity)使用。

事件溯源的一个短板是数据查询,它有两种方式来解决。第一种是直接对stream进行查询,这只适合stream比较小并且查询比较简单的情况。查询复杂的话,就要采用第二种方式,那就是建立一个只读数据库,把需要的数据放在库中进行查询。数据库中的数据通过监听Event Store中相关的事件来更新。

数据库存储方式只能保存当前状态,而事件溯源则存储了所有的历史状态,因而能根据需要回放到历史上任何一点的状态,具有很大优势。但它也不是一点问题都没有。第一,它的程序比较复杂,因为事件是一等公民,你必须把业务逻辑按照事件的方式整理出来,然后用事件来驱动程序。第二,如果你要想修改事件或事件的格式就比较麻烦,因为旧的事件已经存储在Event Store里了(事件就像日志,是只读的),没有办法再改。

由于事件溯源和事件通知表面上看起来很像,不少人都搞不清楚它们的区别。事件通知只是微服务的集成方式,程序内部是不使用事件溯源的,内部实现仍然是传统的数据库方式。只有当要与其他微服务集成时才会发消息。而在事件溯源中,事件是一等公民,可以不要数据库,全部数据都是按照事件的方式存储的。

虽然事件溯源的践行者有不同的意见,但有不少人都认为事件溯源不是微服务的集成方式,而是微服务的一种内部实现方式。因此,在一个系统中,可以某些微服务用事件溯源,另外一些微服务用数据库。当你要集成这些微服务时,你可以用事件通知的方式。注意现在有两种不同的事件需要区分开,一种是微服务的内部事件,是颗粒度比较细的,这种事件只发送到这个微服务的stream中,只被事件溯源使用。另一种是其他微服务也关心的,是颗粒度比较粗的,这种事件会放到另外一个或几个stream中,被多个微服务使用,是用来做服务之间集成的。这样做的好处是限制了事件的作用范围,减少了不相关事件对程序的干扰。详见:https://www.innoq.com/en/blog/ ... cing/

事件溯源出现已经很长时间了,虽然热度一直在上升(尤其是这两年),但总的来说非常缓慢,谈论的人不少,但生产环境使用的不多。究其原因就是应为它对现在的体系结构颠覆太大,需要更改数据存储结构和程序的工作方式,还是有一定风险的。另外,微服务已经形成了一整套体系,从程序部署,服务发现与注册,到监控,服务韧性(Service Resilience),它们基本上都是针对RPC的,虽然也支持消息,但成熟度就差多了,因此有不少工作还是要自己来做。有意思的是Kafka一直在推动它作为事件驱动的工具,也取得了很大的成功。但它却没有得到事件溯源圈内的认可(详见这里)。

多数事件溯源都使用一个叫evenstore的开源Event Store,或是基于某个数据库的Event Store,只有比较少的人用Kafka做Event Store。 但如果用Kafka实现事件通知就一点问题都没有。总的来说,对大多数公司来讲事件溯源是有一定挑战的,应用时需要找到合适的场景。如果你要尝试的话,可以先拿一个微服务试水。

虽然现在事件驱动还有些生涩,但从长远来讲,还是很看好它的。像其他全新的技术一样,事件溯源需要大规模的适用场景来推动。例如容器技术就是因为微服务的流行和推动,才走向主流。事件溯源以前的适用场景只限于记账和源代码库,局限性较大。区块链可能会成为它的下一个机遇,因为它用的也是事件溯源技术。另外AI今后会渗入到具体程序中,使程序具有学习功能。而RPC模式注定没有自适应功能。事件驱动本身就具有对事件进行反应的能力,这是自我学习的基础。因此,这项技术长远来讲定会大放异彩,但短期内(3-5年)大概不会成为主流。

RPC方式

RPC的方式就是远程函数调用,像RESTFul,gRPC,Dubbo都是这种方式。它一般是同步的,可以马上得到结果。在实际中,大多数应用都要求立刻得到结果,这时同步方式更有优势,代码也更简单。

服务网关(API Gateway)

熟悉微服务的人可能都知道服务网关(API Gateway)。当UI需要调用很多微服务时,它需要了解每个服务的接口,这个工作量很大。于是就用服务网关创建了一个Facade,把几个微服务封装起来,这样UI就只调用服务网关就可以了,不需要去对付每一个微服务。下面是API Gateway示例图:
4.jpg

服务网关(API Gateway)不是为了解决微服务之间调用的紧耦合问题,它主要是为了简化客户端的工作。其实它还可以用来降低函数之间的耦合度。 有了API Gateway之后,一旦服务接口修改,你可能只需要修改API Gateway, 而不必修改每个调用这个函数的客户端,这样就减少了程序的耦合性。

服务调用

可以借鉴API Gateway的思路来减少RPC调用的耦合度,例如把多个微服务组织起来形成一个完整功能的服务组合,并对外提供统一的服务接口。这种想法跟上面的API Gateway有些相似,都是把服务集中起来提供粗颗粒(Coarse Granular)服务,而不是细颗粒的服务(Fine Granular)。但这样建立的服务组合可能只适合一个程序使用,没有多少共享价值。因此如果有合适的场景就采用,否侧也不必强求。虽然我们不能降低RPC服务之间的耦合度,却可以减少这种紧耦合带来的影响。

降低紧耦合的影响

什么是紧耦合的主要问题呢?就是客户端和服务端的升级不同步。服务端总是先升级,客户端可能有很多,如果要求它们同时升级是不现实的。它们有各自的部署时间表,一般都会选择在下一次部署时顺带升级。

一般有两个办法可以解决这个问题:
  • 同时支持多个版本:这个工作量比较大,因此大多数公司都不会采用这种方式。
  • 服务端向后兼容:这是更通用的方式。例如你要加一个新功能或有些客户要求给原来的函数增加一个新的参数,但别的客户不需要这个参数。这时你只好新建一个函数,跟原来的功能差不多,只是多了一个参数。这样新旧客户的需求都能满足。它的好处是向后兼容(当然这取决于你使用的协议)。它的坏处是当以后新的客户来了,看到两个差不多的函数就糊涂了,不知道该用那个。而且时间越长越严重,你的服务端可能功能增加的不多,但相似的函数却越来越多,无法选择。


它的解决办法就是使用一个支持向后兼容的RPC协议,现在最好的就是Protobuf gRPC,尤其是在向后兼容上。它给每个服务定义了一个接口,这个接口是与编程语言无关的中性接口,然后你可以用工具生成各个语言的实现代码,供不同语言使用。函数定义的变量都有编号,变量可以是可选类型的,这样就比较好地解决了函数兼容的问题。就用上面的例子,当你要增加一个可选参数时,你就定义一个新的可选变量。由于它是可选的,原来的客户端不需要提供这个参数,因此不需要修改程序。而新的客户端可以提供这个参数。你只要在服务端能同时处理这两种情况就行了。这样服务端并没有增加新的函数,但用户的新需求满足了,而且还是向后兼容的。

微服务的数量有没有上限?

总的来说微服务的数量不要太多,不然会有比较重的运维负担。有一点需要明确的是微服务的流行不是因为技术上的创新,而是为了满足管理上的需要。单体程序大了之后,各个模块的部署时间要求不同,对服务器的优化要求也不同,而且团队人数众多,很难协调管理。把程序拆分成微服务之后,每个团队负责几个服务,就容易管理了,而且每个团队也可以按照自己的节奏进行创新,但它给运维带来了巨大的麻烦。所以在微服务刚出来时,我一直觉得它是一个退步,弊大于利。但由于管理上的问题没有其他解决方案,只有硬着头皮上了。值得庆幸的是微服务带来的麻烦都是可解的。直到后来,微服务建立了全套的自动化体系,从程序集成到部署,从全链路跟踪到日志,以及服务检测,服务发现和注册,这样才把微服务的工作量降了下来。虽然微服务在技术上一无是处,但它的流行还是大大推动了容器技术,服务网格(Service Mesh)和全链路跟踪等新技术的发展。不过它本身在技术上还是没有发现任何优势。。直到有一天,我意识到单体程序其实性能调试是很困难的(很难分离出瓶颈点),而微服务配置了全链路跟踪之后,能很快找到症结所在。看来微服务从技术来讲也不全是缺点,总算也有好的地方。但微服务的颗粒度不宜过细,否则工作量还是太大。

一般规模的公司十几个或几十个微服务都是可以承受的,但如果有几百个甚至上千个,那么绝不是一般公司可以管理的。尽管现有的工具已经很齐全了,而且与微服务有关的整个流程也已经基本上全部自动化了,但它还是会增加很多工作。Martin Fowler几年以前建议先从单体程序开始(详见:https://martinfowler.com/bliki/MonolithFirst.html),然后再逐步把功能拆分出去,变成一个个的微服务。但是后来有人反对这个建议,他也有些松口了。如果单体程序不是太大,这是个好主意。可以用数据额库表的数量来衡量程序的大小,我见过大的单体程序有几百张表,这就太多了,很难管理。正常情况下,一个微服务可以有两、三张表到五、六张表,一般不超过十张表。但如果要减少微服务数量的话,可以把这个标准放宽到不要超过二十张表。用这个做为大致的指标来创建微程序,如果使用一段时间之后还是觉得太大了,那么再逐渐拆分。当然,按照这个标准建立的服务更像是服务组合,而不是单个的微服务。不过它会为你减少工作量。只要不影响业务部门的创新进度,这是一个不错的方案。

到底应不应该选择微服务呢?如果单体程序已经没法管理了,那么你别无选择。如果没有管理上的问题,那么微服务带给你的只有问题和麻烦。其实,一般公司都没有太多选择,只能采用微服务,不过你可以选择建立比较少的微服务。如果还是没法决定,有一个折中的方案,“内部微服务设计”。

内部微服务设计

这种设计表面上看起来是一个单体程序,它只有一个源代码存储仓库,一个数据库,一个部署,但在程序内部可以按照微服务的思想来进行设计。它可以分成多个模块,每个模块是一个微服务,可以由不同的团队管理。
5.png

用这张图做例子。这个图里的每个圆角方块大致是一个微服务,但我们可以把它作为一个单体程序来设计,内部有五个微服务。每个模块都有自己的数据库表,它们都在一个数据库中,但模块之间不能跨数据库访问(不要建立模块之间数据库表的外键)。“User”(在Conference Management模块中)是一个共享的类,但在不同的模块中的名字不同,含义和用法也不同,成员也不一样(例如,在“Customer Service”里叫“Customer”)。DDD(Domain-Driven Design)建议不要共享这个类,而是在每一个有界上下文(模块)中都建一个新类,并拥有新的名字。虽然它们的数据库中的数据应该大致相同,但DDD建议每一个有界上下文中都建一个新表,它们之间再进行数据同步。

这个所谓的“内部微服务设计”其实就是DDD,但当时还没有微服务,因此外表看起来是单体程序,但内部已经是微服务的设计了。它的书在2003就出版了,当时就很有名。但它更偏重于业务逻辑的设计,践行起来也比较困难,因此大家谈论得很多,真正用的较少。直到十年之后,微服务出来之后,人们发现它其实内部就是微服务,而且微服务的设计需要用它的思想来指导,于是就又重新焕发了青春,而且这次更猛,已经到了每个谈论微服务的人都不得不谈论DDD的地步。不过一本软件书籍,在十年之后还能指导新技术的设计,非常令人钦佩。

这样设计的好处是它是一个单体程序,省去了多个微服务带来的部署、运维的麻烦。但它内部是按微服务设计的,如果以后要拆分成微服务会比较容易。至于什么时候拆分不是一个技术问题。如果负责这个单体程序的各个团队之间不能在部署时间表,服务器优化等方面达成一致,那么就需要拆分了。当然你也要应对随之而来的各种运维麻烦。内部微服务设计是一个折中的方案,如果你想试水微服务,但又不愿意冒太大风险时,这是一个不错的选择。

微服务的数据库设计也有很多内容,包括如何把服务从单体程序一步步里拆分出来请参见:https://blog.csdn.net/weixin_3 ... 34941

结论

微服务之间的调用有两种方式,RPC和事件驱动。事件驱动是更好的方式,因为它是松耦合的。但如果业务逻辑是紧耦合的,RPC方式也是可行的(它的好处是代码更简单),而且你还可以通过选取合适的协议(Protobuf gRPC)来降低这种紧耦合带来的危害。由于事件溯源和事件通知的相似性,很多人把两者弄混了,但它们实际上是完全不同的东西。微服务的数量不宜太多,可以先创建比较大的微服务(更像是服务组合)。如果你还是不能确定是否采用微服务架构,可以先从“内部微服务设计”开始,再逐渐拆分。

原文链接:https://blog.csdn.net/weixin_3 ... 62272,作者:倚天码农 收起阅读 »

从入门到上手:什么是K8S持久卷?


本文是介绍Kubernetes的基本概念的系列文章之一, 在第一篇文章中,我们简单介绍了持久卷(Persistent Volumes)。在本文中,我们将学习如何设置数据持久性,并将编写Kubernetes脚本以将我们的Pod连接到持久卷。在此示例中,将使用Azure文件存储(Azure File Storage)来存储来自我们MongoDB数据库的数据,但您可以使用任何类型的卷来实现相同的结果(例如Azure Disk,GCE持久磁盘,AWS弹性块存储等)。

如果你想全面了解K8S其他概念的话,可以先查看此前发布的文章。

图片

请注意:本文提供的脚本不限定于某个平台,因此您可以使用其他类型的云提供商或使用具有K3S的本地集群实践本教程。本文建议使用K3S,因为它非常轻,所有的依赖项被打包在单个二进制中包装大小小于100MB。它也是一种高可用的认证的Kubernetes发行版,用于在资源受限环境中的生产工作负载。想了解更多信息,请查看官方文档:

https://docs.rancher.cn/k3s/

前期准备

在开始本教程之前,请确保已安装Docker。同时安装Kubectl(如果没有,请访问以下链接安装:

https://kubernetes.io/docs/tas ... ndows

在kubectl Cheat Sheet.中可以找到整个本教程中使用的kubectl命令:

https://kubernetes.io/docs/ref ... heet/

本教程中,我们将使用Visual Studio Code,您也可以使用其他的编辑器。

Kubernetes持久卷可以解决什么问题?

图片

请记住,我们有一个节点(硬件设备或虚拟机)和在节点内部,我们有一个Pod(或多个Pod),在Pod中,我们有容器。Pod的状态是暂时的,所以他们神出鬼没(时常会被删除或重新调度等)。在这种情况下,如果你想在Pod被删除之后已经保存其中的数据,你需要数据移动到Pod外部。这样它就可以独立于任何Pod存在。此外部位置称为卷,它是存储系统的抽象。使用卷,您可以在多个Pod保持持久化状态。

什么时候使用持久卷

当容器开始被广泛应用时,它们旨在支持无状态工作负载,其持久性数据存储在其他地方。从那时起,人们做了很多努力以支持容器生态系统中的有状态应用。

每个项目都需要某种数据持久性,因此,您通常需要一个数据库来存储数据。但在简洁的设计中,你不想依赖具体的实现;您想写一个尽可能可以重复使用和独立于平台的应用程序。

一直以来,始终需要向应用程序隐藏存储实现的详细信息。但现在,在云原生应用的时代,云提供商创建了的环境中,想要访问数据的应用程序或用户需要与特定存储系统集成。例如,许多应用程序直接使用特定存储系统,诸如Amazon S3、AzureFile或块存储等,这造成了不健康的依赖。Kubernetes正在尝试通过创建一个名为持久卷的抽象来改变这一情况,它允许云原生应用程序连接到各种云存储系统,而无需与这些系统建立明确的依赖关系。这可以使云存储的消耗更加无缝和消除集成成本。它还可以更容易地迁移云并采用多云策略。

即使有时候,由于金钱,时间或人力等客观条件的限制,你需要做出一些妥协,将你的应用程序与特定的平台或提供商直接耦合,您应该尽量避免尽可能多的直接依赖项。从实际数据库实现中解耦应用程序的一种方法(还有其他解决方案,但这些解决方案更加复杂)是使用容器(和持久卷来防止数据丢失)。这样,您的应用程序将依赖于抽象而不是特定实现。

现在真正的问题是,我们是否应该总是使用带有持久性卷的容器化数据库,或者哪些存储系统类型不应该在容器中使用?

何时使用持久卷并没有通用的黄金法则,但作为起点,您应该考虑可扩展性和集群中节点丢失的处理。

根据可扩展性,我们可以有两种类型的存储系统:
  • 垂直伸缩——包括传统的RDMS解决方案,例如MySQL、PostgreSQL以及SQL Server
  • 水平伸缩——包括“NoSQL”解决方案,例如ElasticSearch或基于Hadoop的解决方案


像MySQL、Postgres、Microsoft SQL等垂直伸缩的解决方案不应进入容器。这些数据库平台需要高I / O、共享磁盘、块存储等,并且不能优雅地处理集群中的节点丢失,这通常发生在基于容器的生态系统中。

对于水平伸缩的应用程序(Elastic、Cassandra、Kafka等),您应该使用容器,因为它们可以承受数据库集群中的节点丢失,并且数据库应用程序可以独立地再平衡。

通常,您可以并且应该分布式数据库容器化,这些数据库使用冗余存储技术,可以承受数据库集群中的节点丢失(Elasticsearch是一个非常好的例子)。

Kubernetes持久卷的类型

我们可以根据其生命周期和配置方式对Kubernetes卷进行分类。

考虑到卷的生命周期,我们可以分为:
  • 临时卷,即与节点的生命周期紧密耦合(例如ExpertDir或HostPath),如果节点倒闭,则删除它们的截阵数量。
  • 持久卷,即长期存储,并且与Ppd或节点生命周期无关。这些可以是云卷(如gcePersistentDisk、awselasticBlockStore、AzureFile或AzureDisk),NFS(网络文件系统)或Persistent
    Volume Claim(一系列抽象来连接到底层云提供存储卷)。


根据卷的配置方式,我们可以分为:

  1. 直接访问

  2. 静态配置

  3. 动态配置


直接访问持久卷

图片

在这种情况下,Pod将直接与Volume耦合,因此它将知道存储系统(例如,Pod将与Azure存储帐户耦合)。该解决方案与云无关,它取决于具体实施而不是抽象。因此,如果可能的话尽量避免这样的解决方案。它唯一的优点是速度快,在Pod中创建Secret,并指定应使用的Secret和确切的存储类型。

创建Secret脚本如下:

apiVersion: v1
kind: Secret
metadata:
name: static-persistence-secret
type: Opaque
data:
azurestorageaccountname: "base64StorageAccountName"
azurestorageaccountkey: "base64StorageAccountKey"


在任何Kubernetes脚本中,在第2行我们指定了资源的类型。在这种情况下,我们称之为Secret。在第4行,我们给它一个名字(我们称之为静态,因为它是由管理员手动创建的,而不是自动生成的)。从Kubernetes的角度来看,Opaque类型意味着该Secret的内容(数据)是非结构化的(它可以包含任意键值对)。要了解有关Kubernetes Secrets的更多信息,可以参阅Secrets Design Document和ConfigureKubernetes Secrets。

https://github.com/kubernetes/ ... ts.md

https://kubernetes.io/docs/con ... cret/

在数据部分中,我们必须指定帐户名称(在Azure中,它是存储帐户的名称)和Access键(在Azure中,选择存储帐户下的“Settings ”,Access key)。别忘了两者应该使用Base64进行编码。

下一步是修改我们的Deployment脚本以使用卷(在这种情况下,卷是Azure File Storage)。

apiVersion: apps/v1
kind: Deployment
metadata:
name: user-db-deployment
spec:
selector:
matchLabels:
app: user-db-app
replicas: 1
template:
metadata:
labels:
app: user-db-app
spec:
containers:
- name: mongo
image: mongo:3.6.4
command:
- mongod
- "--bind_ip_all"
- "--directoryperdb"
ports:
- containerPort: 27017
volumeMounts:
- name: data
mountPath: /data/db
resources:
limits:
memory: "256Mi"
cpu: "500m"
volumes:
- name: data
azureFile:
secretName: static-persistence-secret
shareName: user-mongo-db
readOnly: false


我们可以发现,唯一的区别是,从第32行我们指定了使用的卷,给它一个名称并指定底层存储系统的确切详细信息。secretName必须是先前创建的Secret的名称。

Kubernetes存储类

要了解静态或动态配置,首先我们必须了解Kubernetes存储类。

通过StorageClass,管理员可以提供关于可用存储的配置文件或“类”。不同的类可能映射到不同服务质量级别,或备份策略或由集群管理员确定的任意策略。

例如,你可以有一个在HDD上存储数据的配置文件,命名为慢速存储,或一个在SSD上存储数据的配置文件,命名为快速存储。这些存储的类型由供应者确定。对于Azure,有两种提供者:AzureFile和AzureDisk(区别在于AzureFile可以与Read Wriite Many访问模式一起使用,而AzureDisk只支持Read Write Once访问,当您希望同时使用多个Pod时,这可能是不利因素)。您可以在此处了解有关不同类型的Storage Classes:

https://kubernetes.io/docs/con ... sses/

以下是Storage Class的脚本:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: azurefilestorage
provisioner: kubernetes.io/azure-file
parameters:
storageAccount: storageaccountname
reclaimPolicy: Retain
allowVolumeExpansion: true


Kubernetes预定义提供者属性的值(请参阅Kubernetes存储类)。保留回收策略意味着在我们删除PVC和PV之后,未清除实际存储介质。我们可以将其设置为删除和使用此设置,一旦删除PVC,它也会触发相应的PV以及实际存储介质(此处实际存储是Azure文件存储)的删除。

持久卷及Persistent Volume Claim

图片

Kubernetes对每一个传统的存储操作活动(供应/配置/附加)都有一个匹配的原语。持久卷是供应,存储类正在配置,并且持久卷Claim是附加的。

来自初始文档:

Persistent Volume(PV)是集群中的存储,它已由管理员配置或使用存储类动态配置。
Persistent Volume Claim(PVC)是用户存储的请求。它类似于Pod。Pod消耗节点资源与PVC消耗PV资源是类似的。Pod可以请求特定的资源级别(CPU和内存)。Claim可以请求特定的大小和访问模式(例如,它们可以安装一次读/写或多次只读)。
这意味着管理员将创建持久卷以指定Pod可以使用的存储大小、访问模式和存储类型。开发人员将创建Persistent Volume Claim,要求提供一个卷、访问权限和存储类型。这样一来,在“开发侧”和“运维侧”之间就有了明显的区分。开发人员负责要求必要的卷(PVC),运维人员负责准备和配置要求的卷(PV)。
静态和动态配置之间的差异是,如果没有持久卷和管理员手动创建的Secret,Kubernetes将尝试自动创建这些资源。


动态配置

在这里插入图片描述

在这种情况下,没有手动创建的持久卷和Secret,因此Kubernetes将尝试生成它们。Storage Class是必要的,我们将使用在前文中创建的Storage Class。

PersistentVolumeClaim的脚本如下所示:

apiVersion: v1
kind:Persistent Volume Claim
metadata:
name: persistent-volume-claim-mongo
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: azurefilestorage


以及我们更新的Deployment脚本:

apiVersion: apps/v1
kind: Deployment
metadata:
name: user-db-deployment
spec:
selector:
matchLabels:
app: user-db-app
replicas: 1
template:
metadata:
labels:
app: user-db-app
spec:
containers:
- name: mongo
image: mongo:3.6.4
command:
- mongod
- "--bind_ip_all"
- "--directoryperdb"
ports:
- containerPort: 27017
volumeMounts:
- name: data
mountPath: /data/db
resources:
limits:
memory: "256Mi"
cpu: "500m"
volumes:
- name: data
Persistent Volume Claim:
claimName: persistent-volume-claim-mongo


如你所见,在第34行中,我们通过名称引用了先前创建的PVC。在这种情况下,我们没有手动为它创建持久卷或Secret,因此它将自动创建。

这种方法的最重要的优势是您不必手动创建PV和Secret,而且Deployment是与云无关的。存储的底层细节不存在于Pod的spec中。但是也有一些缺点:您无法配置存储帐户或文件共享,因为它们是自动生成的,并且您无法重复使用PV或Secret ——它们将为每个新Claim重新生成。

静态配置

在这里插入图片描述

静态和动态配置之间的唯一区别是我们手动创建静态配置中的持久卷和Secret。这样,我们就可以完全控制在集群中创建的资源。

持久卷脚本如下:

apiVersion: v1
kind: PersistentVolume
metadata:
name: static-persistent-volume-mongo
labels:
storage: azurefile
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
storageClassName: azurefilestorage
azureFile:
secretName: static-persistence-secret
shareName: user-mongo-db
readOnly: false


重要的是,在第12行我们按名称引用Storage Class。此外,在第14行我们引用了Secret,用于访问底层存储系统。

本文更推荐这个解决方案,即使它需要更多的工作,但它是与云无关的(cloud-agnostic)。它还允许您应用有关角色(集群管理员与开发人员)的关注点分离,并让您控制命名和创建资源。

总结

在本文中,我们了解了如何使用Volume持久化数据和状态,并提出了三种不同的方法来设置系统,即为直接访问、动态配置和静态配置,并讨论了每个系统的优缺点。

作者简介


Czako Zoltan,一位经验丰富的全栈开发人员,在包括前端,后端,DevOps,物联网和人工智能等多个领域都拥有丰富的经验。
继续阅读 »

本文是介绍Kubernetes的基本概念的系列文章之一, 在第一篇文章中,我们简单介绍了持久卷(Persistent Volumes)。在本文中,我们将学习如何设置数据持久性,并将编写Kubernetes脚本以将我们的Pod连接到持久卷。在此示例中,将使用Azure文件存储(Azure File Storage)来存储来自我们MongoDB数据库的数据,但您可以使用任何类型的卷来实现相同的结果(例如Azure Disk,GCE持久磁盘,AWS弹性块存储等)。

如果你想全面了解K8S其他概念的话,可以先查看此前发布的文章。

图片

请注意:本文提供的脚本不限定于某个平台,因此您可以使用其他类型的云提供商或使用具有K3S的本地集群实践本教程。本文建议使用K3S,因为它非常轻,所有的依赖项被打包在单个二进制中包装大小小于100MB。它也是一种高可用的认证的Kubernetes发行版,用于在资源受限环境中的生产工作负载。想了解更多信息,请查看官方文档:

https://docs.rancher.cn/k3s/

前期准备

在开始本教程之前,请确保已安装Docker。同时安装Kubectl(如果没有,请访问以下链接安装:

https://kubernetes.io/docs/tas ... ndows

在kubectl Cheat Sheet.中可以找到整个本教程中使用的kubectl命令:

https://kubernetes.io/docs/ref ... heet/

本教程中,我们将使用Visual Studio Code,您也可以使用其他的编辑器。

Kubernetes持久卷可以解决什么问题?

图片

请记住,我们有一个节点(硬件设备或虚拟机)和在节点内部,我们有一个Pod(或多个Pod),在Pod中,我们有容器。Pod的状态是暂时的,所以他们神出鬼没(时常会被删除或重新调度等)。在这种情况下,如果你想在Pod被删除之后已经保存其中的数据,你需要数据移动到Pod外部。这样它就可以独立于任何Pod存在。此外部位置称为卷,它是存储系统的抽象。使用卷,您可以在多个Pod保持持久化状态。

什么时候使用持久卷

当容器开始被广泛应用时,它们旨在支持无状态工作负载,其持久性数据存储在其他地方。从那时起,人们做了很多努力以支持容器生态系统中的有状态应用。

每个项目都需要某种数据持久性,因此,您通常需要一个数据库来存储数据。但在简洁的设计中,你不想依赖具体的实现;您想写一个尽可能可以重复使用和独立于平台的应用程序。

一直以来,始终需要向应用程序隐藏存储实现的详细信息。但现在,在云原生应用的时代,云提供商创建了的环境中,想要访问数据的应用程序或用户需要与特定存储系统集成。例如,许多应用程序直接使用特定存储系统,诸如Amazon S3、AzureFile或块存储等,这造成了不健康的依赖。Kubernetes正在尝试通过创建一个名为持久卷的抽象来改变这一情况,它允许云原生应用程序连接到各种云存储系统,而无需与这些系统建立明确的依赖关系。这可以使云存储的消耗更加无缝和消除集成成本。它还可以更容易地迁移云并采用多云策略。

即使有时候,由于金钱,时间或人力等客观条件的限制,你需要做出一些妥协,将你的应用程序与特定的平台或提供商直接耦合,您应该尽量避免尽可能多的直接依赖项。从实际数据库实现中解耦应用程序的一种方法(还有其他解决方案,但这些解决方案更加复杂)是使用容器(和持久卷来防止数据丢失)。这样,您的应用程序将依赖于抽象而不是特定实现。

现在真正的问题是,我们是否应该总是使用带有持久性卷的容器化数据库,或者哪些存储系统类型不应该在容器中使用?

何时使用持久卷并没有通用的黄金法则,但作为起点,您应该考虑可扩展性和集群中节点丢失的处理。

根据可扩展性,我们可以有两种类型的存储系统:
  • 垂直伸缩——包括传统的RDMS解决方案,例如MySQL、PostgreSQL以及SQL Server
  • 水平伸缩——包括“NoSQL”解决方案,例如ElasticSearch或基于Hadoop的解决方案


像MySQL、Postgres、Microsoft SQL等垂直伸缩的解决方案不应进入容器。这些数据库平台需要高I / O、共享磁盘、块存储等,并且不能优雅地处理集群中的节点丢失,这通常发生在基于容器的生态系统中。

对于水平伸缩的应用程序(Elastic、Cassandra、Kafka等),您应该使用容器,因为它们可以承受数据库集群中的节点丢失,并且数据库应用程序可以独立地再平衡。

通常,您可以并且应该分布式数据库容器化,这些数据库使用冗余存储技术,可以承受数据库集群中的节点丢失(Elasticsearch是一个非常好的例子)。

Kubernetes持久卷的类型

我们可以根据其生命周期和配置方式对Kubernetes卷进行分类。

考虑到卷的生命周期,我们可以分为:
  • 临时卷,即与节点的生命周期紧密耦合(例如ExpertDir或HostPath),如果节点倒闭,则删除它们的截阵数量。
  • 持久卷,即长期存储,并且与Ppd或节点生命周期无关。这些可以是云卷(如gcePersistentDisk、awselasticBlockStore、AzureFile或AzureDisk),NFS(网络文件系统)或Persistent
    Volume Claim(一系列抽象来连接到底层云提供存储卷)。


根据卷的配置方式,我们可以分为:

  1. 直接访问

  2. 静态配置

  3. 动态配置


直接访问持久卷

图片

在这种情况下,Pod将直接与Volume耦合,因此它将知道存储系统(例如,Pod将与Azure存储帐户耦合)。该解决方案与云无关,它取决于具体实施而不是抽象。因此,如果可能的话尽量避免这样的解决方案。它唯一的优点是速度快,在Pod中创建Secret,并指定应使用的Secret和确切的存储类型。

创建Secret脚本如下:

apiVersion: v1
kind: Secret
metadata:
name: static-persistence-secret
type: Opaque
data:
azurestorageaccountname: "base64StorageAccountName"
azurestorageaccountkey: "base64StorageAccountKey"


在任何Kubernetes脚本中,在第2行我们指定了资源的类型。在这种情况下,我们称之为Secret。在第4行,我们给它一个名字(我们称之为静态,因为它是由管理员手动创建的,而不是自动生成的)。从Kubernetes的角度来看,Opaque类型意味着该Secret的内容(数据)是非结构化的(它可以包含任意键值对)。要了解有关Kubernetes Secrets的更多信息,可以参阅Secrets Design Document和ConfigureKubernetes Secrets。

https://github.com/kubernetes/ ... ts.md

https://kubernetes.io/docs/con ... cret/

在数据部分中,我们必须指定帐户名称(在Azure中,它是存储帐户的名称)和Access键(在Azure中,选择存储帐户下的“Settings ”,Access key)。别忘了两者应该使用Base64进行编码。

下一步是修改我们的Deployment脚本以使用卷(在这种情况下,卷是Azure File Storage)。

apiVersion: apps/v1
kind: Deployment
metadata:
name: user-db-deployment
spec:
selector:
matchLabels:
app: user-db-app
replicas: 1
template:
metadata:
labels:
app: user-db-app
spec:
containers:
- name: mongo
image: mongo:3.6.4
command:
- mongod
- "--bind_ip_all"
- "--directoryperdb"
ports:
- containerPort: 27017
volumeMounts:
- name: data
mountPath: /data/db
resources:
limits:
memory: "256Mi"
cpu: "500m"
volumes:
- name: data
azureFile:
secretName: static-persistence-secret
shareName: user-mongo-db
readOnly: false


我们可以发现,唯一的区别是,从第32行我们指定了使用的卷,给它一个名称并指定底层存储系统的确切详细信息。secretName必须是先前创建的Secret的名称。

Kubernetes存储类

要了解静态或动态配置,首先我们必须了解Kubernetes存储类。

通过StorageClass,管理员可以提供关于可用存储的配置文件或“类”。不同的类可能映射到不同服务质量级别,或备份策略或由集群管理员确定的任意策略。

例如,你可以有一个在HDD上存储数据的配置文件,命名为慢速存储,或一个在SSD上存储数据的配置文件,命名为快速存储。这些存储的类型由供应者确定。对于Azure,有两种提供者:AzureFile和AzureDisk(区别在于AzureFile可以与Read Wriite Many访问模式一起使用,而AzureDisk只支持Read Write Once访问,当您希望同时使用多个Pod时,这可能是不利因素)。您可以在此处了解有关不同类型的Storage Classes:

https://kubernetes.io/docs/con ... sses/

以下是Storage Class的脚本:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: azurefilestorage
provisioner: kubernetes.io/azure-file
parameters:
storageAccount: storageaccountname
reclaimPolicy: Retain
allowVolumeExpansion: true


Kubernetes预定义提供者属性的值(请参阅Kubernetes存储类)。保留回收策略意味着在我们删除PVC和PV之后,未清除实际存储介质。我们可以将其设置为删除和使用此设置,一旦删除PVC,它也会触发相应的PV以及实际存储介质(此处实际存储是Azure文件存储)的删除。

持久卷及Persistent Volume Claim

图片

Kubernetes对每一个传统的存储操作活动(供应/配置/附加)都有一个匹配的原语。持久卷是供应,存储类正在配置,并且持久卷Claim是附加的。

来自初始文档:

Persistent Volume(PV)是集群中的存储,它已由管理员配置或使用存储类动态配置。
Persistent Volume Claim(PVC)是用户存储的请求。它类似于Pod。Pod消耗节点资源与PVC消耗PV资源是类似的。Pod可以请求特定的资源级别(CPU和内存)。Claim可以请求特定的大小和访问模式(例如,它们可以安装一次读/写或多次只读)。
这意味着管理员将创建持久卷以指定Pod可以使用的存储大小、访问模式和存储类型。开发人员将创建Persistent Volume Claim,要求提供一个卷、访问权限和存储类型。这样一来,在“开发侧”和“运维侧”之间就有了明显的区分。开发人员负责要求必要的卷(PVC),运维人员负责准备和配置要求的卷(PV)。
静态和动态配置之间的差异是,如果没有持久卷和管理员手动创建的Secret,Kubernetes将尝试自动创建这些资源。


动态配置

在这里插入图片描述

在这种情况下,没有手动创建的持久卷和Secret,因此Kubernetes将尝试生成它们。Storage Class是必要的,我们将使用在前文中创建的Storage Class。

PersistentVolumeClaim的脚本如下所示:

apiVersion: v1
kind:Persistent Volume Claim
metadata:
name: persistent-volume-claim-mongo
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: azurefilestorage


以及我们更新的Deployment脚本:

apiVersion: apps/v1
kind: Deployment
metadata:
name: user-db-deployment
spec:
selector:
matchLabels:
app: user-db-app
replicas: 1
template:
metadata:
labels:
app: user-db-app
spec:
containers:
- name: mongo
image: mongo:3.6.4
command:
- mongod
- "--bind_ip_all"
- "--directoryperdb"
ports:
- containerPort: 27017
volumeMounts:
- name: data
mountPath: /data/db
resources:
limits:
memory: "256Mi"
cpu: "500m"
volumes:
- name: data
Persistent Volume Claim:
claimName: persistent-volume-claim-mongo


如你所见,在第34行中,我们通过名称引用了先前创建的PVC。在这种情况下,我们没有手动为它创建持久卷或Secret,因此它将自动创建。

这种方法的最重要的优势是您不必手动创建PV和Secret,而且Deployment是与云无关的。存储的底层细节不存在于Pod的spec中。但是也有一些缺点:您无法配置存储帐户或文件共享,因为它们是自动生成的,并且您无法重复使用PV或Secret ——它们将为每个新Claim重新生成。

静态配置

在这里插入图片描述

静态和动态配置之间的唯一区别是我们手动创建静态配置中的持久卷和Secret。这样,我们就可以完全控制在集群中创建的资源。

持久卷脚本如下:

apiVersion: v1
kind: PersistentVolume
metadata:
name: static-persistent-volume-mongo
labels:
storage: azurefile
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
storageClassName: azurefilestorage
azureFile:
secretName: static-persistence-secret
shareName: user-mongo-db
readOnly: false


重要的是,在第12行我们按名称引用Storage Class。此外,在第14行我们引用了Secret,用于访问底层存储系统。

本文更推荐这个解决方案,即使它需要更多的工作,但它是与云无关的(cloud-agnostic)。它还允许您应用有关角色(集群管理员与开发人员)的关注点分离,并让您控制命名和创建资源。

总结

在本文中,我们了解了如何使用Volume持久化数据和状态,并提出了三种不同的方法来设置系统,即为直接访问、动态配置和静态配置,并讨论了每个系统的优缺点。

作者简介


Czako Zoltan,一位经验丰富的全栈开发人员,在包括前端,后端,DevOps,物联网和人工智能等多个领域都拥有丰富的经验。
收起阅读 »

Qunar 容器化落地过程踩过的那些坑


背景

近几年,容器技术非常火爆,且日趋成熟,众多企业慢慢开始容器化建设,并在云原生技术方向上不断的探索和实践。基于这个大的趋势, 2020 年底 Qunar 也向云原生迈出了第一步——容器化。为了完成容器化这个目标,我们基础平台、TCDEV、Ops、数据组、业务线等多个团队一起协作配合,终于在公司内部把容器化成功落地。截止到今年六月初,我们生产环境已经接入了 150 多个应用,其余应用也在陆续接入中,这个成果还是蛮让人惊喜的。回顾整个容器化落地过程其实有不少技术难点的,在这篇文章中我会分享下我们遇到的几个问题和解决思路,希望对还在云原生路上探索的同学有些借鉴意义。

容器化方案架构简介

Qunar 在做容器化过程中,各个系统 Portal 平台、中间件、Ops 基础设施、监控等都做了相应的适配改造,改造后的架构矩阵如下图所示:
1.png

  • Portal:Qunar 的 PaaS 平台入口,提供 CI/CD 能力、资源管理、自助运维、应用画像、应用授权(db 授权、支付授权、应用间授权)等功能
  • 运维工具:提供应用的可观测性工具, 包括 Watcher(监控和报警)、Bistoury(Java 应用在线 debug)、QTrace(Tracing 系统)、Loki/ELK(提供实时日志/离线日志查看)
  • 中间件:应用用到的所有中间件,MQ、配置中心、分布式调度系统 QSchedule、Dubbo 、MySQL SDK等
  • 虚拟化集群:底层的 Kubernetes 和 OpenStack 集群
  • Noah:测试环境管理平台,支持应用 KVM/容器混合部署


在 Portal PaaS 平台中,一个应用的容器发布流具体程如下:
2.png

容器化落地过程中碰到的问题

如何兼容过去 KVM 的使用方式,并支持 preStart、preOnline hook 自定义脚本

KVM 场景中 hook 脚本使用场景介绍:
  • preStart hook:用户在这个脚本中会自定义命令,比如环境准备
  • preOnline hook:用户会定义一些数据预热操作等,这个动作需要在应用 checkurl 通过并且接入流量前执行


问题点:Kubernetes 原生只提供了 preStop、postStart 2 种 hook,它们的执行时机没有满足上述 2 个 KVM 场景下业务用到的 hook。

分析与解决过程:
  • preStart hook:在 ENTRYPOINT 中注入 preStart hook 阶段,容器启动过程中发现有自定义的 preStart 脚本则执行该脚本,至于这个脚本的位置目前规范是定义在代码指定目录下
  • preOnline hook:由于 preOnline 脚本执行时机是在应用 checkurl 通过后,而应用容器是单进程,所以在应用容器中执行这个是行不通的。而 postStart hook 的设计就是异步的,和应用容器的启动是解耦的, 所以我们初步的方案选择了 postStart hook 中这个事情。实施方案是 postStart hook执行后会不断轮询应用的健康状态,如果健康检测 checkurl 通过了, 则执行 preOnline 脚本。脚本成功后则进行上线操作,即在应用目录下创建 healthcheck.html 文件,OpenResty 和中间件发现这个文件后就会把流量接入到这个实例中


按照上面的方案,Pod 的组成设计如下:
3.png

发布过程缺少日志 定位问题困难

场景介绍:在容器发布过程中如果应用启动失败,我们通过 Kubernetes API 是拿不到实时的标准输入输出流,只能等到发布设置的超时阈值,这个过程中发布人员心里是很焦急的,因为不确定发生了什么。如下图所示,部署过程中应用的更新工作流中什么都看不到。
4.png

问题点:Kubernetes API 为什么拿不到标准输入输出。

分析与解决过程:
  • 通过 kubectl logs 查看当时的 Pod 日志,什么都没有拿到,超时时间过后才拿到。说明问题不在程序本身,而是在 Kubernetes 的机制上;
  • 查看 postStart Hook 的相关文档,有一段介绍提到了 postHook 如果执行时间长或者 hang 住,容器的状态也会hang 住,不会进入 running 状态, 看到这条信息,大概猜测到罪魁祸首就是这个 postStart hook 了。
    5.png


基于上面的猜测,把 postStart hook 去掉后测试,应用容器的标准输入可以实时拿到了。
  • 找到问题后,解决方法也就简单了,把 postStart hook 中实现的功能放到 sidecar 中就可以解决。至于 Sidecar 如何在应用容器的目录中创建 healthcheck.html 文件,就需要用到共享卷了。新的方案设计如下:
    6.png
  • 使用上述方案后,发布流程的标准输入输出、自定义 hook 脚本的输出、pod 事件等都是实时可见的了, 发布过程更透明了。


Java 应用在容器场景下如何支持远程 debug

KVM 场景 debug 介绍:在开发 Java 应用的过程中,通过远程 debug 可以快速排查定位问题,因此是开发人员必不可少的一个功能。

debug 具体流程:开发人员在 Noah 环境管理平台的界面点击开启 debug,Noah 会自动为该 Java 应用配置上 debug 选项,-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=127.0.0.1:50005,并重启该 Java 应用,之后开发人员就可以在 IDE 中配置远程 debug 并进入调试模式了。
7.png

容器场景的 debug方案:测试环境的 Java 应用默认开启 debug 模式,这样也避免了更改 debug 重建 Pod 的过程,速度从 KVM 的分钟级到现在的秒级。当用户想开启 debug 时,Noah 会调用 Kubernetes exec 接口执行 socat 相关命令进行端口映射转发,让开发人员可以通过 socat 开的代理连接到 java 应用的 debug 端口。

问题点:容器场景下在用户 debug 过程中,当请求走到了设置的断点后,debug 功能失效。

分析与解决过程:
  • 复现容器场景下 debug,观察该 Pod 的各项指标,发现 debug 功能失效的时候系统收到了一个 Liveness probe failed,kill Pod 的事件。根据这个事件可以判断出当时 Liveness check 失败,应用容器 才被 kill 的,应用容器重启代理进程也就随之消失了, debug 也就失效了。
  • 关于 debug 过程 checkurl 为什么失败的问题,特地找了 TCDEV 的同学咨询了下,得到的答案是 debug 时当请求走到断点时,整个 JVM 是 hang 住的,这个时候任何请求过来也会被 hang 住,当然也包括 checkurl,于是我们也特地在 KVM 场景和容器场景分布做了测试,结果也确实是这样的。
  • 其他同事也提了个临时解决方案,把断点的阻断级别改为线程级的,这样就不会阻断 checkurl 了,IDEA 中默认的选项是 Suspend All,改为 Suspend Thread 即可。不过这个也不是最优解,因为这个需要用户手工配置阻断级别,有认知学习成本。
    8.png
  • 回到最初的问题上,为什么容器场景下遇到这个问题, 而 KVM 没有,主要是因为容器场景 Kubernetes 提供了自愈能力,Kubernetes 会定时执行 Liveness check,当失败次数达到指定的阈值时 Kubernetes 会 kill 掉容器并重新拉起一个新的容器。
  • 那我们只好从 Kubernetes 的 Liveness 探针上着手了,探针默认支持 exec、TCP 、HttpGet 3种模式, 当前使用的是 HttpGet,这种方式只支持一个 url,无法满足这个场景需求。经过组内讨论, 最后大家决定用这个表达式 ( checkurl == 200) || (socat process && java process alive) 在作为应用的 Liveness 检测方式,当 debug 走到断点的时候, 应用容器就不会阻断了, 完美的解决了这个问题。


多集群方案 Rancher 2.5 可能存在性能问题

Rancher 使用场景介绍:最初我们是采用的 Rancher 作为多集群管理方案,主要是因为 Rancher 代码开源,对运维友好、可以统一入口等优点。我们使用的方式是每个应用单独一个 namespace , 这样方便资源隔离和权限设置。

问题点:随着接入应用的数量增多,Rancher 接口性能越来越慢,当 namespace 数量达到 3000 后,几个查询请求之间把 Rancher 服务打挂了。(可能我们使用有误?欢迎指正)

分析与解决:Ops 同学通过查看代码和资料分析确认这个是 Rancher 的 bug,并且短时间也没计划修复。后来 Ops 调研并对比了其他的多集群方案 KubeFed,KubeSphere 等,最终选择了 KubeSphere 作为集群方案。迁移到 KubeSphere 后就没有这些性能问题了。

最初我们是采用的 Rancher 作为多集群管理方案,主要是因为 Rancher 代码开源,对运维友好、可以统一入口等优点。我们使用的方式是每个应用单独一个 namespace,这样方便资源隔离和权限设置。

总结与展望

总结

以上就是我们落地容器化过程中遇到的几个问题与解决思路, 其中从 KVM 迁移到容器需要考虑用户使用习惯还有历史功能等因素,兼容和适配是难免的,总是要做取舍的。我们的云原生之路的探索刚刚迈出一小步,任重而道远,后续我们会持续关注云原生最新的技术趋势和最佳实践,并在我们 Qunar PaaS 平台落地,同时也会把我们遇到的一些问题分享出来,避免大家踩同样的坑。

展望

  • 集群稳定性治理,为了保障业务应用在 Kubernetes 集群的稳定运行,我们会关注更多的集群稳定性指标并做好闭环处理,同时也探索和落地混沌工程在容器场景下的实践
  • 提高资源利用率,目前业内某些大厂的 Kubernetes 集群资源利用率达到了 60%,我们目前离这个目标还有很大距离,下个阶段我们会在资源动态压缩、节点资源动态超卖等方向做探索和实践


作者:邹晟,2017 年加入 Qunar,目前在基础平台做 Portal PaaS 平台的运维和开发,近期一直在做容器化落地方面的工作。

原文链接:https://mp.weixin.qq.com/s/5bEORRFZqGFnKZShtxWxJA
继续阅读 »

背景

近几年,容器技术非常火爆,且日趋成熟,众多企业慢慢开始容器化建设,并在云原生技术方向上不断的探索和实践。基于这个大的趋势, 2020 年底 Qunar 也向云原生迈出了第一步——容器化。为了完成容器化这个目标,我们基础平台、TCDEV、Ops、数据组、业务线等多个团队一起协作配合,终于在公司内部把容器化成功落地。截止到今年六月初,我们生产环境已经接入了 150 多个应用,其余应用也在陆续接入中,这个成果还是蛮让人惊喜的。回顾整个容器化落地过程其实有不少技术难点的,在这篇文章中我会分享下我们遇到的几个问题和解决思路,希望对还在云原生路上探索的同学有些借鉴意义。

容器化方案架构简介

Qunar 在做容器化过程中,各个系统 Portal 平台、中间件、Ops 基础设施、监控等都做了相应的适配改造,改造后的架构矩阵如下图所示:
1.png

  • Portal:Qunar 的 PaaS 平台入口,提供 CI/CD 能力、资源管理、自助运维、应用画像、应用授权(db 授权、支付授权、应用间授权)等功能
  • 运维工具:提供应用的可观测性工具, 包括 Watcher(监控和报警)、Bistoury(Java 应用在线 debug)、QTrace(Tracing 系统)、Loki/ELK(提供实时日志/离线日志查看)
  • 中间件:应用用到的所有中间件,MQ、配置中心、分布式调度系统 QSchedule、Dubbo 、MySQL SDK等
  • 虚拟化集群:底层的 Kubernetes 和 OpenStack 集群
  • Noah:测试环境管理平台,支持应用 KVM/容器混合部署


在 Portal PaaS 平台中,一个应用的容器发布流具体程如下:
2.png

容器化落地过程中碰到的问题

如何兼容过去 KVM 的使用方式,并支持 preStart、preOnline hook 自定义脚本

KVM 场景中 hook 脚本使用场景介绍:
  • preStart hook:用户在这个脚本中会自定义命令,比如环境准备
  • preOnline hook:用户会定义一些数据预热操作等,这个动作需要在应用 checkurl 通过并且接入流量前执行


问题点:Kubernetes 原生只提供了 preStop、postStart 2 种 hook,它们的执行时机没有满足上述 2 个 KVM 场景下业务用到的 hook。

分析与解决过程:
  • preStart hook:在 ENTRYPOINT 中注入 preStart hook 阶段,容器启动过程中发现有自定义的 preStart 脚本则执行该脚本,至于这个脚本的位置目前规范是定义在代码指定目录下
  • preOnline hook:由于 preOnline 脚本执行时机是在应用 checkurl 通过后,而应用容器是单进程,所以在应用容器中执行这个是行不通的。而 postStart hook 的设计就是异步的,和应用容器的启动是解耦的, 所以我们初步的方案选择了 postStart hook 中这个事情。实施方案是 postStart hook执行后会不断轮询应用的健康状态,如果健康检测 checkurl 通过了, 则执行 preOnline 脚本。脚本成功后则进行上线操作,即在应用目录下创建 healthcheck.html 文件,OpenResty 和中间件发现这个文件后就会把流量接入到这个实例中


按照上面的方案,Pod 的组成设计如下:
3.png

发布过程缺少日志 定位问题困难

场景介绍:在容器发布过程中如果应用启动失败,我们通过 Kubernetes API 是拿不到实时的标准输入输出流,只能等到发布设置的超时阈值,这个过程中发布人员心里是很焦急的,因为不确定发生了什么。如下图所示,部署过程中应用的更新工作流中什么都看不到。
4.png

问题点:Kubernetes API 为什么拿不到标准输入输出。

分析与解决过程:
  • 通过 kubectl logs 查看当时的 Pod 日志,什么都没有拿到,超时时间过后才拿到。说明问题不在程序本身,而是在 Kubernetes 的机制上;
  • 查看 postStart Hook 的相关文档,有一段介绍提到了 postHook 如果执行时间长或者 hang 住,容器的状态也会hang 住,不会进入 running 状态, 看到这条信息,大概猜测到罪魁祸首就是这个 postStart hook 了。
    5.png


基于上面的猜测,把 postStart hook 去掉后测试,应用容器的标准输入可以实时拿到了。
  • 找到问题后,解决方法也就简单了,把 postStart hook 中实现的功能放到 sidecar 中就可以解决。至于 Sidecar 如何在应用容器的目录中创建 healthcheck.html 文件,就需要用到共享卷了。新的方案设计如下:
    6.png
  • 使用上述方案后,发布流程的标准输入输出、自定义 hook 脚本的输出、pod 事件等都是实时可见的了, 发布过程更透明了。


Java 应用在容器场景下如何支持远程 debug

KVM 场景 debug 介绍:在开发 Java 应用的过程中,通过远程 debug 可以快速排查定位问题,因此是开发人员必不可少的一个功能。

debug 具体流程:开发人员在 Noah 环境管理平台的界面点击开启 debug,Noah 会自动为该 Java 应用配置上 debug 选项,-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=127.0.0.1:50005,并重启该 Java 应用,之后开发人员就可以在 IDE 中配置远程 debug 并进入调试模式了。
7.png

容器场景的 debug方案:测试环境的 Java 应用默认开启 debug 模式,这样也避免了更改 debug 重建 Pod 的过程,速度从 KVM 的分钟级到现在的秒级。当用户想开启 debug 时,Noah 会调用 Kubernetes exec 接口执行 socat 相关命令进行端口映射转发,让开发人员可以通过 socat 开的代理连接到 java 应用的 debug 端口。

问题点:容器场景下在用户 debug 过程中,当请求走到了设置的断点后,debug 功能失效。

分析与解决过程:
  • 复现容器场景下 debug,观察该 Pod 的各项指标,发现 debug 功能失效的时候系统收到了一个 Liveness probe failed,kill Pod 的事件。根据这个事件可以判断出当时 Liveness check 失败,应用容器 才被 kill 的,应用容器重启代理进程也就随之消失了, debug 也就失效了。
  • 关于 debug 过程 checkurl 为什么失败的问题,特地找了 TCDEV 的同学咨询了下,得到的答案是 debug 时当请求走到断点时,整个 JVM 是 hang 住的,这个时候任何请求过来也会被 hang 住,当然也包括 checkurl,于是我们也特地在 KVM 场景和容器场景分布做了测试,结果也确实是这样的。
  • 其他同事也提了个临时解决方案,把断点的阻断级别改为线程级的,这样就不会阻断 checkurl 了,IDEA 中默认的选项是 Suspend All,改为 Suspend Thread 即可。不过这个也不是最优解,因为这个需要用户手工配置阻断级别,有认知学习成本。
    8.png
  • 回到最初的问题上,为什么容器场景下遇到这个问题, 而 KVM 没有,主要是因为容器场景 Kubernetes 提供了自愈能力,Kubernetes 会定时执行 Liveness check,当失败次数达到指定的阈值时 Kubernetes 会 kill 掉容器并重新拉起一个新的容器。
  • 那我们只好从 Kubernetes 的 Liveness 探针上着手了,探针默认支持 exec、TCP 、HttpGet 3种模式, 当前使用的是 HttpGet,这种方式只支持一个 url,无法满足这个场景需求。经过组内讨论, 最后大家决定用这个表达式 ( checkurl == 200) || (socat process && java process alive) 在作为应用的 Liveness 检测方式,当 debug 走到断点的时候, 应用容器就不会阻断了, 完美的解决了这个问题。


多集群方案 Rancher 2.5 可能存在性能问题

Rancher 使用场景介绍:最初我们是采用的 Rancher 作为多集群管理方案,主要是因为 Rancher 代码开源,对运维友好、可以统一入口等优点。我们使用的方式是每个应用单独一个 namespace , 这样方便资源隔离和权限设置。

问题点:随着接入应用的数量增多,Rancher 接口性能越来越慢,当 namespace 数量达到 3000 后,几个查询请求之间把 Rancher 服务打挂了。(可能我们使用有误?欢迎指正)

分析与解决:Ops 同学通过查看代码和资料分析确认这个是 Rancher 的 bug,并且短时间也没计划修复。后来 Ops 调研并对比了其他的多集群方案 KubeFed,KubeSphere 等,最终选择了 KubeSphere 作为集群方案。迁移到 KubeSphere 后就没有这些性能问题了。

最初我们是采用的 Rancher 作为多集群管理方案,主要是因为 Rancher 代码开源,对运维友好、可以统一入口等优点。我们使用的方式是每个应用单独一个 namespace,这样方便资源隔离和权限设置。

总结与展望

总结

以上就是我们落地容器化过程中遇到的几个问题与解决思路, 其中从 KVM 迁移到容器需要考虑用户使用习惯还有历史功能等因素,兼容和适配是难免的,总是要做取舍的。我们的云原生之路的探索刚刚迈出一小步,任重而道远,后续我们会持续关注云原生最新的技术趋势和最佳实践,并在我们 Qunar PaaS 平台落地,同时也会把我们遇到的一些问题分享出来,避免大家踩同样的坑。

展望

  • 集群稳定性治理,为了保障业务应用在 Kubernetes 集群的稳定运行,我们会关注更多的集群稳定性指标并做好闭环处理,同时也探索和落地混沌工程在容器场景下的实践
  • 提高资源利用率,目前业内某些大厂的 Kubernetes 集群资源利用率达到了 60%,我们目前离这个目标还有很大距离,下个阶段我们会在资源动态压缩、节点资源动态超卖等方向做探索和实践


作者:邹晟,2017 年加入 Qunar,目前在基础平台做 Portal PaaS 平台的运维和开发,近期一直在做容器化落地方面的工作。

原文链接:https://mp.weixin.qq.com/s/5bEORRFZqGFnKZShtxWxJA 收起阅读 »

etcd 3.5正式发布


2019年8月推出etcd 3.4时,我们主要关注存储后端改进、非投票成员与预投票等功能。在接下来的近两年中,etcd被越来越广泛地应用于各类关键任务集群及数据库程序当中,其功能集也随之变得愈发广泛且复杂。因此,提高项目稳定性与可靠性成为近期规划工作的重中之重。

今天,我们正式发布etcd 3.5。过去两年以来,我们完成了多轮迭代、修复了大量bug、确定了新的优化方向并着力培养相关生态系统。在此期间,etcd项目也成为云原生计算基金会(CNCF)的毕业项目。而此次发布的3.5版本,正是etcd社区不断进化以及众多艰难探索的集大成结果。

在本文中,我们将共同了解etcd 3.5中最为显著的变化,并展示项目的未来发展路线图。关于详尽的变更清单,请参阅CHANGELOG 3.5。若需了解更多更新内容,请在Twitter @etcdio上关注我们。最后,你可以点击此处获取etcd。

安全性

考虑到不少用户在使用etcd处理敏感数据,改善并维护安全水平自然成为我们的首要任务。为了全面了解etcd的安全态势,我们完成了一轮第三方安全审计:首份报告发布于2020年2月,我们借此发现并修复了多种极端案例及高严重性问题。关于更多详细信息,请参阅安全审计报告

为了遵循最高级别的安全最佳实践,etcd现采取一套安全发布流程,并使用静态分析工具运行自动化测试,包括errcheckineffassign等。

功能拓展

etcd面向结构化日志记录的迁移工作已经完成。现在etcd默认使用zap记录器,其中采用一项无反射、零分配JSON编码器。我们已经正式弃用基于反射的capnslog序列化记录工具。

etcd现可支持内置日志轮替,允许你配置轮替阈值、压缩算法等细则。关于更多详细信息,请参阅hexfusion@ of Red Hat’s code change

etcd现可针对高资源需求量请求发出更详尽的跟踪信息,例如:
{
"caller":"traceutil/trace.go:116",
"msg":"trace[123] range",
"detail":"{
range_begin:foo;
range_end:fooo; response_count:100000; response_revision:191496;}",
"duration":"132.449773ms",
"start":"...:32.611-0700",
"end":"...:32.744-0700",
"steps":[
"trace[123] step 'range keys from bolt db' (duration: 92.521911ms)",
"trace[123] step 'filter and sort the key-value pairs' (duration: 22.789099ms)"]} 

这项功能将为跨越多个etcd服务器组件的请求提供非常实用的生命周期管理信号。具体请参阅YoyinZyc@’s code change(由谷歌提供)

每套etcd集群都维护有自己的集群版本,具体版本由集群自身按法定人数投票决定。以往,为了防止发生不兼容的变更,我们并不支持对集群版本进行降级(例如将etcd由3.5降级至3.4次要版本)。

假定我们允许3.3版本的节点加入3.4版本的集群,并向领导者发送租用检查点的请求,但这项请求在etcd 3.4版本才刚刚出现。在3.3版本的节点收到租用检查点请求时,将无法处理这项未知申请(详见etcd服务器申请代码)。然而,人们可能不会使用这样的租用检查点功能,而是倾向于冒着影响兼容性的风险进行版本降级(将3.4版本降级至3.3)。为了确保此类回滚能够简单可靠地实现,我们添加了新的降级API,用于验证、启用及取消etcd版本降级操作。关于更多信息,请参阅YoyinZyc@ of Google’s code change

通过仲裁协议,etcd集群成员将与写入方拥有相同级别的一致性保障。但以往,成员清单调用将直接由服务器的本地数据负责提供,而这部分数据可能已经过时。现在,etcd能够以线性化方式保证为成员清单提供支持——如果服务器与仲裁端断开连接,则成员调用操作将失败。具体请参阅 jingyih@ of Google’s code change

gRPC网关端点现已在/v3/*下获得稳定保证。gRPC网关会生成一个HTTP API,使得基于etcd gRPC的HTTP/2协议也可通过HTTP/1进行访问,例如:
curl -X POST -L http://localhost:2379/v3/kv/put -d '{"key": "Zm9v", "value": "YmFy"}' 

etcd客户端现可使用最新的gRPC v1.32.0,但这需要使用新的导入路径"go.etcd.io/etcd/client/v3",并将均衡器实现迁移至上游。关于更多详细信息,请参阅ptabor@ of Google’s code change

Bug修复

etcd的可靠性与正确性至关重要。正因为如此,我们才将所有关键bug修复逆向移植至以往的etcd版本。下面来看我们在etcd 3.5开发过程中发现并修复的各项严重bug:
  1. 租约对象规程导致内存泄漏,解决方法是清除旧leader中的过期租约队列,详见:https://github.com/etcd-io/etcd/pull/11731
  2. 持续compact操作可能导致mvcc存储层死锁。详见:https://github.com/etcd-io/etcd/pull/11817
  3. etcd服务器重启后,存在多余的后端数据库打开操作并导致重载4000万个键耗费掉5分钟时间。解决方案将重启时间缩短为一半,详见:https://github.com/etcd-io/etcd/pull/11779
  4. 如果etcd在完成碎片整理之前崩溃,则下一次碎片整理操作可能读取到已经损坏的文件。解决方案是忽略并直接覆盖现有文件。具体请参阅:https://github.com/etcd-io/etcd/pull/11613
  5. 客户端取消watch时没有向服务器发出信号,因此可能导致所创建watcher外泄。解决方案是向服务器显式发送取消请求,具体参见:https://github.com/etcd-io/etcd/pull/11613


性能改进

作为etcd最重要的用户之一,Kubernetes会查询整个键空间以罗列并监控集群资源。每当在kube-apiserver反射器缓存中找不到资源时,即会发生这项范围查询(例如请求的etcd修订版已被压缩,详见kube-apiserver v1.21 code),由此导致etcd服务器过载并引发读取缓慢。在这种情况下,kube-apiserver跟踪会发出如下警告:
"List" url:/api/v1/pods,user-agent... (started: ...) (total time: 1.208s): Trace[...]: [1.208s] [1.204s] Writing http response done count:4346

而etcd给出的警告如下所示:
etcdserver: read-only range request key:"/registry/pods/" range_end:"/registry/pods0" revision:... range_response_count:500 size:291984 took too long (723.099118ms) to execute

我们对etcd堆配置文件进行了深入研究,并发现服务器警告记录器效率低下的主要原因,在于该记录器中存在冗余的编码操作,仅用于使用proto.Size调用计算范围响应的大小。在其影响下,大范围查询的堆分配开销高达60%,因此会导致在过载的etcd服务器中发生内存不足崩溃或OOM(参见图一)。我们优化了协议部级区消息大小的相关操作,使得峰值使用期间etcd的内存消耗量降低了50%(参见图二)。虽然具体变更只涉及少量代码,但如果没有多年以来广泛的测试与工作负载模拟,这样的性能改进根本不可能实现。具体请参阅:https://github.com/etcd-io/etcd/issues/12835https://github.com/etcd-io/etcd/pull/12871
1.png

图一:用于计算协议缓冲区消息响应大小的缓慢请求警告日志,其中记录了期间etcd堆的使用情况。有61%的堆被分配给proto.Size调用路径当中,此调用路径负责对消息内的所有键值进行编码以计算其大小。
2.png

图二:在替换proto.Size调用之前与之后,etcd堆的不同使用情况。经过优化,etcd服务器中proto.Size调用所点明禾的内存量降低了50%。

etcd 3.4版本通过复制事务缓冲区(而非在写入就并发读取之间共享)实现后端读取事务的完全并发。但这种缓冲机制不可避免地带来复制开销,并给写入密集型事务的性能产生了负面影响。这是因为创建并发读取事务需要互斥锁,而这会阻塞传入的写入事务。

etcd 3.5通过改进进一步提升了事务并发性水平。

  1. 如果事务中包含一项PUT(更新)操作,则此事务在读取与写入之间共享事务缓冲区(同3.4版本中的设定)以避免复制缓冲区。这种事务处理模式可通过etcd --experimental-txn-mode-write-with-shared-buffer=false禁用。

    基准测试结果表明,通过在创建写入事务时避免复制缓冲区,高写入率事务的吞吐量增长了2.7倍(详见图三与图四)。这有利于一切指向使用etcd事务的kube-apiserver的创建与更新调用。关于更多详细信息,请参阅:https://github.com/etcd-io/etcd/pull/12896
    3.png

    图三:具有高写入率的etcd事务率。顶部的值为读取与写入间的比例。第一项比率0.125代表每发生8次写入,对应1次读取。第二项比率0.25代表每发生1次读取,对应发生4次写入。右边栏的数值代表的是etcd/pull/12896事务前后吞吐量的反比。使用共享缓冲区写入方法,事务的吞吐量提升了2.7倍。
    4.png

    图四:具有高读取率的etcd事务率。顶部的值为读取与写入的比率。第一项比率4.0代表每1次写入对应4次读取。第二项比率8.0代表第1次写入对应8次读取。右边栏的数值代表的是etcd/pull/12896事务前后吞吐量的反比。使用共享缓冲区写入方法,事务的吞吐量提升了25%。

  2. etcd现在会缓存事务缓冲区以避免不必要的复制操作。这加快了并发读取事务的创建速度,借此将具有高读取率的事务速率提升了2.4倍(详见图五与图六)。具体参见:https://github.com/etcd-io/etcd/pull/12933
    5.png

    图五:具有高写入率的etcd事务率。顶部的值为读取与写入间的比率。第一项比率0.125代表每发生8次写入,对应1次读取。第二项比率0.25代表每发生1次读取,对应发生4次写入。右边栏的数值代表的是etcd/pull/12933事务前后吞吐量的反比。使用共享缓冲区写入方法,事务的吞吐量提升了1.4倍。
    6.png

    图六:具有高读取率的etcd事务率。顶部的值为读取与写入的比率。第一项比率4.0代表每1次写入对应4次读取。第二项比率8.0代表第1次写入对应8次读取。右边栏的数值代表的是etcd/pull/12933事务前后吞吐量的反比。使用共享缓冲区写入方法,事务的吞吐量提升了2.5倍。


监控功能

长期运行的负载测试表明,etcd服务器会掩盖Go语言的垃圾回收机制并歪曲实际内存使用情况。我们发现,使用Go 1.12的etcd服务器会变更运行时以使用Linux内核中的MADV_FREE,因此回收的内存没有被反映在常驻集大小或RSS指标当中。这使得etcd内存使用指标发生了意外且错误的静态化,因此无法体现Go垃圾回收机制的效果。为了解决这个监控问题,我们使用Go 1.16编译了etcd 3.5,其在Linux上默认为MADV_DONTNEED。关于更多详细信息,请参阅:https://github.com/golang/go/issues/42330
7.png

图七:在Go运行时设置为MADV_DONTNEED前后,etcd在执行范围查询时的内存使用情况。当使用GODEBUG=madvdontneed=1"或者Go 1.16及更高版本时,etcd服务器在CloudWatch mem_used指标或其他监控工具(例如top)中能够准确报告内存使用情况。

监控是一项用于支持可靠性与可观察性的基础服务。准确有力的监控将帮助各服务所有者充分理解当前状态,并确定问题报告的潜在原因。以此为基础,大家可以检测早期预警信号并诊断潜在问题。etcd能够创建出带有跟踪信息的服务器日志并发布Prometheus指标。

利用这部分信息,我们可以确定潜在的服务影响与发生原因。然而,当某条请求调用链跨越多种外部组件时(例如从kube-apiserver到etcd),我们就很难识别出问题。为了高效确定根本原因,我们使用OpenTelemetry添加了分布式跟踪支持:启用分布式跟踪机制后,etcd现在可使用OpenTelemetry生成跨RPC调用链的跟踪路径,由此轻松与周边生态系统相集成。详见图八,https://github.com/etcd-io/etcd/pull/12919https://github.com/etcd-io/etcd/issues/12460
8.png

图八:用于etcd的jaeger跟踪UI示例。

测试功能

分布式系统当中充满了微妙的极端状况。某些bug很可能只在特殊情况下出现,这意味着简单的单元测试根本无法覆盖实际使用情况。为此,etcd引入了集成、端到端以及故障注入测试,借此以更可靠、更高效的方式验证每一项变更。但随着扩展功能集的不断发展与膨胀,测试问题也开始快速规程,几乎耗尽我们的生产力。我们面临着种种艰难的任务,往往需要几个小时的调试才能找出引发问题的根本原因。为了提高测试质量,我们在新版本中实现了以下改进:


平台支持

etcd的提交前测试已经达成了快速与可靠两大重要目标,但问题在于其目前主要支持x86处理器。社区中有不少声音呼吁我们支持更多其他架构,例如ARM与s390x。为此,我们推出了GitHub自托管操作运行器,它会以统一的方式托管各类外部测试工具。使用GitHub操作运行器,etcd现在可以在基于ARM架构的AWS EC2实例(Graviton)上运行测试,从而正式支持ARM64(aarch64)平台。此外,我们还引入了一种新的多平台支持机制,并根据测试覆盖率对支持能力进行划分。关于更多详细信息,请参阅https://github.com/etcd-io/website/pull/273https://github.com/etcd-io/website/pull/273

开发者体验

为了更好地支持与外部项目的集成能力,etcd现在全面采用Go 1.16模块。这当然也带来了挑战,因为现有代码库使得迁移工作变得困难重重,也引发了关于社区是否乐于接纳的担忧。工具一直是etcd发展过程中的重要组成部分,因此我们需要一套更好的解决方案以支持我们的贡献者体验。

使用Go模块,我们得以将服务器与客户端代码之间建立起清晰边界,降低了对依赖项更新的变更管理难度,同时打造出一套可验证的构建系统、且不会对供应商的复杂代码库造成影响。有了这套可重现的构建解决方案,我们不再需要直接提供依赖项,借此将etcd代码库的体积削减了一半。具体请参见https://docs.google.com/docume ... /edithttps://github.com/etcd-io/etcd/pull/12279

为了更好地隔离依赖树,etcd命令行界面现在引入了新的管理工具etcdutl(请注意,不是etcdut-i-l),其子命令包括etcdutl snapshot与etcdutl defrag。此项变更与新的Go模块布局高度吻合:etcdctl仅依赖于客户端v3库,而etcdutl则可能依赖于etcd服务器端包,例如bolt及后端数据库代码。具体请参阅https://github.com/etcd-io/etcd/pull/12971

为了支持包容性命名计划,etcd项目将默认分支master重命名为main。此次变更将无缝进行,GitHub方面将负责处理必要的重新定向工作。

每一次etcd写入都会在Raft中产生一条附加消息,并将其同步至磁盘。但是这种持久性可能并不适合测试场景。为了解决这个问题,我们添加了etcd-unsafe-no-fsync标记以绕过Raft WAL的磁盘写入操作。具体请参阅https://github.com/etcd-io/etcd/pull/11946https://github.com/etcd-io/etcd/issues/11930

社区建设

etcd最终用户的多样性不断扩大:Cloudflare依靠etcd管理其数据中心,Grafana Cortex将其配置数据存储在etcd之内,Netflix Titus使用etcd管理其容器工作负载,而Tailscale则在etcd之上运行其控制平面。

我们还扩展了供应商贡献者规模。在etcd 3.5版本中,我们迎来两位新增核心维护者:来自谷歌、一直领导etcd社区会议与Kubernetes集成工作的Wenjia Zhang,以及同样来自谷歌、长期负责bug修复与代码库模块化工作的Piotr Tabor。贡献者的多样性是构建可持续、高人气开源项目并改善工作环境可管理性的必要前提。

关于更多详细信息,请参阅2021年CNCF etcd项目进程报告

新的etcd.io

自从etcd项目于2018年12月加入云原生计算基金会(CNCF)以来,我们已经将全部面向用户的文档重构为专用库etcd-io/website,并使用Hugo完成了对网站托管的现代化改造。这项迁移是一项艰巨的任务,需要长达数月的时间投入与维护人员间的相互沟通。这里要感谢lucperkins@、CNCF的chalin@以及nate-double-u@的大力支持,也感谢其他社区贡献者的积极协助。

发展路线图

流量过载会导致级联节点故障,为此,集群的扩展一直是项颇具挑战的工作,甚至可能削弱由仲裁丢失中恢复过来的能力。但考虑到大量关键任务系统已然建立在etcd基础之上,保护etcd免受过载影响无疑至关重要。我们将重新审视etcd节流功能,借此更好地缓解过度负载。目前,etcd项目拥有两项尚未决定的限速机制提案:https://docs.google.com/docume ... av4x7https://github.com/etcd-io/etcd/pull/12290

来自kube-apiserver的大范围查询仍是导致进程崩溃的最主要根源,其情况也相对难以预测。我们对此类工作负载的堆配置文件进行了研究,并发现etcd范围请求处理程序在整个响应发送至gRPC服务器前会解码并保存整个响应,这会额外增加37%不必要的堆分配负载。具体参见https://github.com/etcd-io/etcd/issues/12835

客户端代码中的分页范围调用并不能完全解决这个问题,因为其中还涉及额外的一致性考量。为了解决这个低效难题,etcd需要支持范围流。我们将重新审视https://github.com/etcd-io/etcd/pull/12343,但这项提案要求投入大量努力以在etcd乃至下游项目中全面引入新的语义变化。
9.png

图九:在罗列Kubernetes Pod范围查询期间的etcd使用情况。可以看到,37%的堆分配被用于在etcd mvcc rangeKeys中保存键值对以创建范围查询响应。

为了降低维护开销,我们完全弃用了etcd v2 API,转而使用性能更高且已经得到广泛采用的v3 API。通过etcd --experimental-enable-v2v3实现的v2存储转换层在此次3.5版本中仍处于实验阶段,并将在下个版本中被删除。关于更多细节,请参阅https://github.com/etcd-io/etcd/issues/12913

纵观整个发展历程,受到频度低、增量大以及发布自动化等现实条件的影响,etcd发布一直是项艰巨的任务。我们也将着手开发一套更易于社区访问的自动发布系统,敬请期待!

原文链接:Announcing etcd 3.5
继续阅读 »

2019年8月推出etcd 3.4时,我们主要关注存储后端改进、非投票成员与预投票等功能。在接下来的近两年中,etcd被越来越广泛地应用于各类关键任务集群及数据库程序当中,其功能集也随之变得愈发广泛且复杂。因此,提高项目稳定性与可靠性成为近期规划工作的重中之重。

今天,我们正式发布etcd 3.5。过去两年以来,我们完成了多轮迭代、修复了大量bug、确定了新的优化方向并着力培养相关生态系统。在此期间,etcd项目也成为云原生计算基金会(CNCF)的毕业项目。而此次发布的3.5版本,正是etcd社区不断进化以及众多艰难探索的集大成结果。

在本文中,我们将共同了解etcd 3.5中最为显著的变化,并展示项目的未来发展路线图。关于详尽的变更清单,请参阅CHANGELOG 3.5。若需了解更多更新内容,请在Twitter @etcdio上关注我们。最后,你可以点击此处获取etcd。

安全性

考虑到不少用户在使用etcd处理敏感数据,改善并维护安全水平自然成为我们的首要任务。为了全面了解etcd的安全态势,我们完成了一轮第三方安全审计:首份报告发布于2020年2月,我们借此发现并修复了多种极端案例及高严重性问题。关于更多详细信息,请参阅安全审计报告

为了遵循最高级别的安全最佳实践,etcd现采取一套安全发布流程,并使用静态分析工具运行自动化测试,包括errcheckineffassign等。

功能拓展

etcd面向结构化日志记录的迁移工作已经完成。现在etcd默认使用zap记录器,其中采用一项无反射、零分配JSON编码器。我们已经正式弃用基于反射的capnslog序列化记录工具。

etcd现可支持内置日志轮替,允许你配置轮替阈值、压缩算法等细则。关于更多详细信息,请参阅hexfusion@ of Red Hat’s code change

etcd现可针对高资源需求量请求发出更详尽的跟踪信息,例如:
{
"caller":"traceutil/trace.go:116",
"msg":"trace[123] range",
"detail":"{
range_begin:foo;
range_end:fooo; response_count:100000; response_revision:191496;}",
"duration":"132.449773ms",
"start":"...:32.611-0700",
"end":"...:32.744-0700",
"steps":[
"trace[123] step 'range keys from bolt db' (duration: 92.521911ms)",
"trace[123] step 'filter and sort the key-value pairs' (duration: 22.789099ms)"]} 

这项功能将为跨越多个etcd服务器组件的请求提供非常实用的生命周期管理信号。具体请参阅YoyinZyc@’s code change(由谷歌提供)

每套etcd集群都维护有自己的集群版本,具体版本由集群自身按法定人数投票决定。以往,为了防止发生不兼容的变更,我们并不支持对集群版本进行降级(例如将etcd由3.5降级至3.4次要版本)。

假定我们允许3.3版本的节点加入3.4版本的集群,并向领导者发送租用检查点的请求,但这项请求在etcd 3.4版本才刚刚出现。在3.3版本的节点收到租用检查点请求时,将无法处理这项未知申请(详见etcd服务器申请代码)。然而,人们可能不会使用这样的租用检查点功能,而是倾向于冒着影响兼容性的风险进行版本降级(将3.4版本降级至3.3)。为了确保此类回滚能够简单可靠地实现,我们添加了新的降级API,用于验证、启用及取消etcd版本降级操作。关于更多信息,请参阅YoyinZyc@ of Google’s code change

通过仲裁协议,etcd集群成员将与写入方拥有相同级别的一致性保障。但以往,成员清单调用将直接由服务器的本地数据负责提供,而这部分数据可能已经过时。现在,etcd能够以线性化方式保证为成员清单提供支持——如果服务器与仲裁端断开连接,则成员调用操作将失败。具体请参阅 jingyih@ of Google’s code change

gRPC网关端点现已在/v3/*下获得稳定保证。gRPC网关会生成一个HTTP API,使得基于etcd gRPC的HTTP/2协议也可通过HTTP/1进行访问,例如:
curl -X POST -L http://localhost:2379/v3/kv/put -d '{"key": "Zm9v", "value": "YmFy"}' 

etcd客户端现可使用最新的gRPC v1.32.0,但这需要使用新的导入路径"go.etcd.io/etcd/client/v3",并将均衡器实现迁移至上游。关于更多详细信息,请参阅ptabor@ of Google’s code change

Bug修复

etcd的可靠性与正确性至关重要。正因为如此,我们才将所有关键bug修复逆向移植至以往的etcd版本。下面来看我们在etcd 3.5开发过程中发现并修复的各项严重bug:
  1. 租约对象规程导致内存泄漏,解决方法是清除旧leader中的过期租约队列,详见:https://github.com/etcd-io/etcd/pull/11731
  2. 持续compact操作可能导致mvcc存储层死锁。详见:https://github.com/etcd-io/etcd/pull/11817
  3. etcd服务器重启后,存在多余的后端数据库打开操作并导致重载4000万个键耗费掉5分钟时间。解决方案将重启时间缩短为一半,详见:https://github.com/etcd-io/etcd/pull/11779
  4. 如果etcd在完成碎片整理之前崩溃,则下一次碎片整理操作可能读取到已经损坏的文件。解决方案是忽略并直接覆盖现有文件。具体请参阅:https://github.com/etcd-io/etcd/pull/11613
  5. 客户端取消watch时没有向服务器发出信号,因此可能导致所创建watcher外泄。解决方案是向服务器显式发送取消请求,具体参见:https://github.com/etcd-io/etcd/pull/11613


性能改进

作为etcd最重要的用户之一,Kubernetes会查询整个键空间以罗列并监控集群资源。每当在kube-apiserver反射器缓存中找不到资源时,即会发生这项范围查询(例如请求的etcd修订版已被压缩,详见kube-apiserver v1.21 code),由此导致etcd服务器过载并引发读取缓慢。在这种情况下,kube-apiserver跟踪会发出如下警告:
"List" url:/api/v1/pods,user-agent... (started: ...) (total time: 1.208s): Trace[...]: [1.208s] [1.204s] Writing http response done count:4346

而etcd给出的警告如下所示:
etcdserver: read-only range request key:"/registry/pods/" range_end:"/registry/pods0" revision:... range_response_count:500 size:291984 took too long (723.099118ms) to execute

我们对etcd堆配置文件进行了深入研究,并发现服务器警告记录器效率低下的主要原因,在于该记录器中存在冗余的编码操作,仅用于使用proto.Size调用计算范围响应的大小。在其影响下,大范围查询的堆分配开销高达60%,因此会导致在过载的etcd服务器中发生内存不足崩溃或OOM(参见图一)。我们优化了协议部级区消息大小的相关操作,使得峰值使用期间etcd的内存消耗量降低了50%(参见图二)。虽然具体变更只涉及少量代码,但如果没有多年以来广泛的测试与工作负载模拟,这样的性能改进根本不可能实现。具体请参阅:https://github.com/etcd-io/etcd/issues/12835https://github.com/etcd-io/etcd/pull/12871
1.png

图一:用于计算协议缓冲区消息响应大小的缓慢请求警告日志,其中记录了期间etcd堆的使用情况。有61%的堆被分配给proto.Size调用路径当中,此调用路径负责对消息内的所有键值进行编码以计算其大小。
2.png

图二:在替换proto.Size调用之前与之后,etcd堆的不同使用情况。经过优化,etcd服务器中proto.Size调用所点明禾的内存量降低了50%。

etcd 3.4版本通过复制事务缓冲区(而非在写入就并发读取之间共享)实现后端读取事务的完全并发。但这种缓冲机制不可避免地带来复制开销,并给写入密集型事务的性能产生了负面影响。这是因为创建并发读取事务需要互斥锁,而这会阻塞传入的写入事务。

etcd 3.5通过改进进一步提升了事务并发性水平。

  1. 如果事务中包含一项PUT(更新)操作,则此事务在读取与写入之间共享事务缓冲区(同3.4版本中的设定)以避免复制缓冲区。这种事务处理模式可通过etcd --experimental-txn-mode-write-with-shared-buffer=false禁用。

    基准测试结果表明,通过在创建写入事务时避免复制缓冲区,高写入率事务的吞吐量增长了2.7倍(详见图三与图四)。这有利于一切指向使用etcd事务的kube-apiserver的创建与更新调用。关于更多详细信息,请参阅:https://github.com/etcd-io/etcd/pull/12896
    3.png

    图三:具有高写入率的etcd事务率。顶部的值为读取与写入间的比例。第一项比率0.125代表每发生8次写入,对应1次读取。第二项比率0.25代表每发生1次读取,对应发生4次写入。右边栏的数值代表的是etcd/pull/12896事务前后吞吐量的反比。使用共享缓冲区写入方法,事务的吞吐量提升了2.7倍。
    4.png

    图四:具有高读取率的etcd事务率。顶部的值为读取与写入的比率。第一项比率4.0代表每1次写入对应4次读取。第二项比率8.0代表第1次写入对应8次读取。右边栏的数值代表的是etcd/pull/12896事务前后吞吐量的反比。使用共享缓冲区写入方法,事务的吞吐量提升了25%。

  2. etcd现在会缓存事务缓冲区以避免不必要的复制操作。这加快了并发读取事务的创建速度,借此将具有高读取率的事务速率提升了2.4倍(详见图五与图六)。具体参见:https://github.com/etcd-io/etcd/pull/12933
    5.png

    图五:具有高写入率的etcd事务率。顶部的值为读取与写入间的比率。第一项比率0.125代表每发生8次写入,对应1次读取。第二项比率0.25代表每发生1次读取,对应发生4次写入。右边栏的数值代表的是etcd/pull/12933事务前后吞吐量的反比。使用共享缓冲区写入方法,事务的吞吐量提升了1.4倍。
    6.png

    图六:具有高读取率的etcd事务率。顶部的值为读取与写入的比率。第一项比率4.0代表每1次写入对应4次读取。第二项比率8.0代表第1次写入对应8次读取。右边栏的数值代表的是etcd/pull/12933事务前后吞吐量的反比。使用共享缓冲区写入方法,事务的吞吐量提升了2.5倍。


监控功能

长期运行的负载测试表明,etcd服务器会掩盖Go语言的垃圾回收机制并歪曲实际内存使用情况。我们发现,使用Go 1.12的etcd服务器会变更运行时以使用Linux内核中的MADV_FREE,因此回收的内存没有被反映在常驻集大小或RSS指标当中。这使得etcd内存使用指标发生了意外且错误的静态化,因此无法体现Go垃圾回收机制的效果。为了解决这个监控问题,我们使用Go 1.16编译了etcd 3.5,其在Linux上默认为MADV_DONTNEED。关于更多详细信息,请参阅:https://github.com/golang/go/issues/42330
7.png

图七:在Go运行时设置为MADV_DONTNEED前后,etcd在执行范围查询时的内存使用情况。当使用GODEBUG=madvdontneed=1"或者Go 1.16及更高版本时,etcd服务器在CloudWatch mem_used指标或其他监控工具(例如top)中能够准确报告内存使用情况。

监控是一项用于支持可靠性与可观察性的基础服务。准确有力的监控将帮助各服务所有者充分理解当前状态,并确定问题报告的潜在原因。以此为基础,大家可以检测早期预警信号并诊断潜在问题。etcd能够创建出带有跟踪信息的服务器日志并发布Prometheus指标。

利用这部分信息,我们可以确定潜在的服务影响与发生原因。然而,当某条请求调用链跨越多种外部组件时(例如从kube-apiserver到etcd),我们就很难识别出问题。为了高效确定根本原因,我们使用OpenTelemetry添加了分布式跟踪支持:启用分布式跟踪机制后,etcd现在可使用OpenTelemetry生成跨RPC调用链的跟踪路径,由此轻松与周边生态系统相集成。详见图八,https://github.com/etcd-io/etcd/pull/12919https://github.com/etcd-io/etcd/issues/12460
8.png

图八:用于etcd的jaeger跟踪UI示例。

测试功能

分布式系统当中充满了微妙的极端状况。某些bug很可能只在特殊情况下出现,这意味着简单的单元测试根本无法覆盖实际使用情况。为此,etcd引入了集成、端到端以及故障注入测试,借此以更可靠、更高效的方式验证每一项变更。但随着扩展功能集的不断发展与膨胀,测试问题也开始快速规程,几乎耗尽我们的生产力。我们面临着种种艰难的任务,往往需要几个小时的调试才能找出引发问题的根本原因。为了提高测试质量,我们在新版本中实现了以下改进:


平台支持

etcd的提交前测试已经达成了快速与可靠两大重要目标,但问题在于其目前主要支持x86处理器。社区中有不少声音呼吁我们支持更多其他架构,例如ARM与s390x。为此,我们推出了GitHub自托管操作运行器,它会以统一的方式托管各类外部测试工具。使用GitHub操作运行器,etcd现在可以在基于ARM架构的AWS EC2实例(Graviton)上运行测试,从而正式支持ARM64(aarch64)平台。此外,我们还引入了一种新的多平台支持机制,并根据测试覆盖率对支持能力进行划分。关于更多详细信息,请参阅https://github.com/etcd-io/website/pull/273https://github.com/etcd-io/website/pull/273

开发者体验

为了更好地支持与外部项目的集成能力,etcd现在全面采用Go 1.16模块。这当然也带来了挑战,因为现有代码库使得迁移工作变得困难重重,也引发了关于社区是否乐于接纳的担忧。工具一直是etcd发展过程中的重要组成部分,因此我们需要一套更好的解决方案以支持我们的贡献者体验。

使用Go模块,我们得以将服务器与客户端代码之间建立起清晰边界,降低了对依赖项更新的变更管理难度,同时打造出一套可验证的构建系统、且不会对供应商的复杂代码库造成影响。有了这套可重现的构建解决方案,我们不再需要直接提供依赖项,借此将etcd代码库的体积削减了一半。具体请参见https://docs.google.com/docume ... /edithttps://github.com/etcd-io/etcd/pull/12279

为了更好地隔离依赖树,etcd命令行界面现在引入了新的管理工具etcdutl(请注意,不是etcdut-i-l),其子命令包括etcdutl snapshot与etcdutl defrag。此项变更与新的Go模块布局高度吻合:etcdctl仅依赖于客户端v3库,而etcdutl则可能依赖于etcd服务器端包,例如bolt及后端数据库代码。具体请参阅https://github.com/etcd-io/etcd/pull/12971

为了支持包容性命名计划,etcd项目将默认分支master重命名为main。此次变更将无缝进行,GitHub方面将负责处理必要的重新定向工作。

每一次etcd写入都会在Raft中产生一条附加消息,并将其同步至磁盘。但是这种持久性可能并不适合测试场景。为了解决这个问题,我们添加了etcd-unsafe-no-fsync标记以绕过Raft WAL的磁盘写入操作。具体请参阅https://github.com/etcd-io/etcd/pull/11946https://github.com/etcd-io/etcd/issues/11930

社区建设

etcd最终用户的多样性不断扩大:Cloudflare依靠etcd管理其数据中心,Grafana Cortex将其配置数据存储在etcd之内,Netflix Titus使用etcd管理其容器工作负载,而Tailscale则在etcd之上运行其控制平面。

我们还扩展了供应商贡献者规模。在etcd 3.5版本中,我们迎来两位新增核心维护者:来自谷歌、一直领导etcd社区会议与Kubernetes集成工作的Wenjia Zhang,以及同样来自谷歌、长期负责bug修复与代码库模块化工作的Piotr Tabor。贡献者的多样性是构建可持续、高人气开源项目并改善工作环境可管理性的必要前提。

关于更多详细信息,请参阅2021年CNCF etcd项目进程报告

新的etcd.io

自从etcd项目于2018年12月加入云原生计算基金会(CNCF)以来,我们已经将全部面向用户的文档重构为专用库etcd-io/website,并使用Hugo完成了对网站托管的现代化改造。这项迁移是一项艰巨的任务,需要长达数月的时间投入与维护人员间的相互沟通。这里要感谢lucperkins@、CNCF的chalin@以及nate-double-u@的大力支持,也感谢其他社区贡献者的积极协助。

发展路线图

流量过载会导致级联节点故障,为此,集群的扩展一直是项颇具挑战的工作,甚至可能削弱由仲裁丢失中恢复过来的能力。但考虑到大量关键任务系统已然建立在etcd基础之上,保护etcd免受过载影响无疑至关重要。我们将重新审视etcd节流功能,借此更好地缓解过度负载。目前,etcd项目拥有两项尚未决定的限速机制提案:https://docs.google.com/docume ... av4x7https://github.com/etcd-io/etcd/pull/12290

来自kube-apiserver的大范围查询仍是导致进程崩溃的最主要根源,其情况也相对难以预测。我们对此类工作负载的堆配置文件进行了研究,并发现etcd范围请求处理程序在整个响应发送至gRPC服务器前会解码并保存整个响应,这会额外增加37%不必要的堆分配负载。具体参见https://github.com/etcd-io/etcd/issues/12835

客户端代码中的分页范围调用并不能完全解决这个问题,因为其中还涉及额外的一致性考量。为了解决这个低效难题,etcd需要支持范围流。我们将重新审视https://github.com/etcd-io/etcd/pull/12343,但这项提案要求投入大量努力以在etcd乃至下游项目中全面引入新的语义变化。
9.png

图九:在罗列Kubernetes Pod范围查询期间的etcd使用情况。可以看到,37%的堆分配被用于在etcd mvcc rangeKeys中保存键值对以创建范围查询响应。

为了降低维护开销,我们完全弃用了etcd v2 API,转而使用性能更高且已经得到广泛采用的v3 API。通过etcd --experimental-enable-v2v3实现的v2存储转换层在此次3.5版本中仍处于实验阶段,并将在下个版本中被删除。关于更多细节,请参阅https://github.com/etcd-io/etcd/issues/12913

纵观整个发展历程,受到频度低、增量大以及发布自动化等现实条件的影响,etcd发布一直是项艰巨的任务。我们也将着手开发一套更易于社区访问的自动发布系统,敬请期待!

原文链接:Announcing etcd 3.5 收起阅读 »

浅谈大型分布式Web系统的架构演进


我们以Java Web为例,来搭建一个简单的电商系统,看看这个系统可以如何一步步演变。

该系统具备的功能:
  • 用户模块:用户注册和管理
  • 商品模块:商品展示和管理
  • 交易模块:创建交易和管理


阶段一:单机构建网站

网站的初期,我们经常会在单机上跑我们所有的程序和软件。此时我们使用一个容器,如TomcatJettyJboss,然后直接使用JSP/Servlet技术,或者使用一些开源的框架如Maven + Spring + Struts + HibernateMaven + Spring + Spring MVC + Mybatis。最后再选择一个数据库管理系统来存储数据,如MySQLSQL ServerOracle,然后通过JDBC进行数据库的连接和操作。

把以上的所有软件包括数据库、应用程序都装载同一台机器上,应用跑起来了,也算是一个小系统了。此时系统结果如下:
1.png

阶段二:应用服务器与数据库分离

随着网站的上线,访问量逐步上升,服务器的负载慢慢提高,在服务器还没有超载的时候,我们应该就要做好准备,提升网站的负载能力。假如我们代码层面已难以优化,在不提高单台机器的性能的情况下,采用增加机器是一个不错的方式,不仅可以有效地提高系统的负载能力,而且性价比高。

增加的机器用来做什么呢?此时我们可以把数据库服务器Web服务器拆分开来,这样不仅提高了单台机器的负载能力,也提高了容灾能力。

应用服务器与数据库分开后的架构如下图所示:
2.png

阶段三:应用服务器集群

随着访问量继续增加,单台应用服务器已经无法满足需求了。在假设数据库服务器没有压力的情况下,我们可以把应用服务器从一台变成了两台甚至多台,把用户的请求分散到不同的服务器中,从而提高负载能力。而多台应用服务器之间没有直接的交互,他们都是依赖数据库各自对外提供服务。著名的做故障切换的软件有KeepAlivedKeepAlived是一个类似于Layer3、4、7交换机制的软件,他不是某个具体软件故障切换的专属品,而是可以适用于各种软件的一款产品。KeepAlived配合上ipvsadm又可以做负载均衡,可谓是神器。

我们以增加了一台应用服务器为例,增加后的系统结构图如下:
3.png

系统演变到这里,将会出现下面四个问题
  1. 用户的请求由谁来转发到到具体的应用服务器?
  2. 有那些转发的算法和策略可以使用?
  3. 应用服务器如何返回用户的请求?
  4. 用户如果每次访问到的服务器不一样,那么如何维护session的一致性?


针对以上问题,常用的解决方案如下:

1、负载均衡的问题

一般以下有5种解决方案:

1、HTTP重定向

HTTP重定向就是应用层的请求转发。用户的请求其实已经到了HTTP重定向负载均衡服务器,服务器根据算法要求用户重定向,用户收到重定向请求后,再次请求真正的集群。
  • 优点:简单易用;
  • 缺点:性能较差。


2、DNS域名解析负载均衡

DNS域名解析负载均衡就是在用户请求DNS服务器,获取域名对应的IP地址时,DNS服务器直接给出负载均衡后的服务器IP。
  • 优点:交给DNS,不用我们去维护负载均衡服务器;
  • 缺点:当一个应用服务器挂了,不能及时通知DNS,而且DNS负载均衡的控制权在域名服务商那里,网站无法做更多的改善和更强大的管理。


3、反向代理服务器

在用户的请求到达反向代理服务器时(已经到达网站机房),由反向代理服务器根据算法转发到具体的服务器。常用的ApacheNginx都可以充当反向代理服务器。
  • 优点:部署简单;
  • 缺点:代理服务器可能成为性能的瓶颈,特别是一次上传大文件。


4、IP层负载均衡

在请求到达负载均衡器后,负载均衡器通过修改请求的目的IP地址,从而实现请求的转发,做到负载均衡。
  • 优点:性能更好;
  • 缺点:负载均衡器的宽带成为瓶颈。


5、数据链路层负载均衡

在请求到达负载均衡器后,负载均衡器通过修改请求的MAC地址,从而做到负载均衡,与IP负载均衡不一样的是,当请求访问完服务器之后,直接返回客户。而无需再经过负载均衡器。

2、集群调度转发算法

1、rr轮询调度算法

顾名思义,轮询分发请求。
  • 优点:实现简单
  • 缺点:不考虑每台服务器的处理能力


2、wrr加权调度算法

我们给每个服务器设置权值Weight,负载均衡调度器根据权值调度服务器,服务器被调用的次数跟权值成正比。
  • 优点:考虑了服务器处理能力的不同


3、sh原地址散列算法

提取用户IP,根据散列函数得出一个key,再根据静态映射表,查处对应的value,即目标服务器IP。过目标机器超负荷,则返回空。
  • 优点:实现同一个用户访问同一个服务器。


4、dh目标地址散列算法

原理同上,只是现在提取的是目标地址的IP来做哈希。
  • 优点:实现同一个用户访问同一个服务器。


5、lc最少连接算法

优先把请求转发给连接数少的服务器。
  • 优点:使得集群中各个服务器的负载更加均匀。


6、wlc加权最少连接算法

lc的基础上,为每台服务器加上权值。算法为:(活动连接数 * 256 + 非活动连接数) ÷ 权重,计算出来的值小的服务器优先被选择。
  • 优点:可以根据服务器的能力分配请求。


7、sed最短期望延迟算法

其实sed跟wlc类似,区别是不考虑非活动连接数。算法为:(活动连接数 +1 ) * 256 ÷ 权重,同样计算出来的值小的服务器优先被选择。

8、nq永不排队算法

改进的sed算法。我们想一下什么情况下才能“永不排队”,那就是服务器的连接数为0的时候,那么假如有服务器连接数为0,均衡器直接把请求转发给它,无需经过sed的计算。

9、LBLC基于局部性最少连接算法

负载均衡器根据请求的目的IP地址,找出该IP地址最近被使用的服务器,把请求转发之。若该服务器超载,最采用最少连接数算法。

10、LBLCR带复制的基于局部性最少连接算法

负载均衡器根据请求的目的IP地址,找出该IP地址最近使用的“服务器组”,注意,并不是具体某个服务器,然后采用最少连接数从该组中挑出具体的某台服务器出来,把请求转发之。若该服务器超载,那么根据最少连接数算法,在集群的非本服务器组的服务器中,找出一台服务器出来,加入本服务器组,然后把请求转发。

3、集群请求返回模式问题

1、NAT

负载均衡器接收用户的请求,转发给具体服务器,服务器处理完请求返回给均衡器,均衡器再重新返回给用户。

2、DR

负载均衡器接收用户的请求,转发给具体服务器,服务器出来玩请求后直接返回给用户。需要系统支持IP Tunneling协议,难以跨平台

3、TUN

同上,但无需IP Tunneling协议,跨平台性好,大部分系统都可以支持。

4、集群Session一致性问题

1、Session Sticky

Session sticky就是把同一个用户在某一个会话中的请求,都分配到固定的某一台服务器中,这样我们就不需要解决跨服务器的session问题了,常见的算法有ip_hash算法,即上面提到的两种散列算法。
  • 优点:实现简单;
  • 缺点:应用服务器重启则session消失。


2、Session Replication

Session replication就是在集群中复制session,使得每个服务器都保存有全部用户的session数据。
  • 优点:减轻负载均衡服务器的压力,不需要要实现ip_hasp算法来转发请求;
  • 缺点:复制时网络带宽开销大,访问量大的话Session占用内存大且浪费。


3、Session数据集中存储

Session数据集中存储就是利用数据库来存储session数据,实现了session和应用服务器的解耦。
  • 优点:相比Session replication的方案,集群间对于宽带和内存的压力大幅减少;
  • 缺点:需要维护存储Session的数据库。


4、Cookie Base

Cookie base就是把Session存在Cookie中,由浏览器来告诉应用服务器我的session是什么,同样实现了session和应用服务器的解耦。
  • 优点:实现简单,基本免维护。
  • 缺点:cookie长度限制,安全性低,带宽消耗。


值得一提的是:
  • Nginx目前支持的负载均衡算法有wrrsh(支持一致性哈希)、fair(lc)。但Nginx作为均衡器的话,还可以一同作为静态资源服务器
  • Keepalived + ipvsadm比较强大,目前支持的算法有:rrwrrlcwlclblcshdh
  • Keepalived支持集群模式有:NATDRTUN
  • Nginx本身并没有提供session同步的解决方案,而Apache则提供了session共享的支持。


解决了以上的问题之后,系统的结构如下:
4.png

阶段四:数据库读写分离化

上面我们总是假设数据库负载正常,但随着访问量的的提高,数据库的负载也在慢慢增大。那么可能有人马上就想到跟应用服务器一样,把数据库一份为二再负载均衡即可。

但对于数据库来说,并没有那么简单。假如我们简单的把数据库一分为二,然后对于数据库的请求,分别负载到A机器和B机器,那么显而易见会造成两台数据库数据不统一的问题。那么对于这种情况,我们可以先考虑使用读写分离主从复制的方式。

读写分离后的系统结构如下:
5.png

这个结构变化后也会带来两个问题:
  • 主从数据库之间数据同步问题。
  • 应用对于数据源的选择问题。


解决方案:
  • 使用MySQL自带的Master + Slave的方式实现主从复制
  • 采用第三方数据库中间件,例如MyCatMyCat是从Cobar发展而来的,而Cobar是阿里开源的数据库中间件,后来停止开发。MyCat是国内比较好的MySql开源数据库分库分表中间件。


阶段五:用搜索引擎缓解读库的压力

数据库做读库的话,常常对模糊查找力不从心,即使做了读写分离,这个问题还未能解决。以我们所举的交易网站为例,发布的商品存储在数据库中,用户最常使用的功能就是查找商品,尤其是根据商品的标题来查找对应的商品。对于这种需求,一般我们都是通过like功能来实现的,但是这种方式的代价非常大,而且结果非常不准确。此时我们可以使用搜索引擎倒排索引来完成。

搜索引擎具有的优点:它能够大大提高查询速度和搜索准确性。

引入搜索引擎的开销:
  • 带来大量的维护工作,我们需要自己实现索引的构建过程,设计全量/增加的构建方式来应对非实时与实时的查询需求。
  • 需要维护搜索引擎集群


搜索引擎并不能替代数据库,它解决了某些场景下的精准、快速、高效的“读”操作,是否引入搜索引擎,需要综合考虑整个系统的需求。

引入搜索引擎后的系统结构如下:
6.png

阶段六:用缓存缓解读库的压力

常用的缓存机制包括页面级缓存、应用数据缓存和数据库缓存。

应用层和数据库层的缓存

随着访问量的增加,逐渐出现了许多用户访问同一部分热门内容的情况,对于这些比较热门的内容,没必要每次都从数据库读取。我们可以使用缓存技术,例如可以使用Google的开源缓存技术Guava或者使用Memecahed作为应用层的缓存,也可以使用Redis作为数据库层的缓存。

另外,在某些场景下,关系型数据库并不是很适合,例如我想做一个“每日输入密码错误次数限制”的功能,思路大概是在用户登录时,如果登录错误,则记录下该用户的IP和错误次数,那么这个数据要放在哪里呢?假如放在内存中,那么显然会占用太大的内容;假如放在关系型数据库中,那么既要建立数据库表,还要简历对应的Java bean,还要写SQL等等。而分析一下我们要存储的数据,无非就是类似{ip:errorNumber}这样的key:value数据。对于这种数据,我们可以用NOSQL数据库来代替传统的关系型数据库。

页面缓存

除了数据缓存,还有页面缓存。比如使用HTML5localstroage或者Cookie。除了页面缓存带来的性能提升外,对于并发访问且页面置换频率小的页面,应尽量使用页面静态化技术。
  • 优点:减轻数据库的压力, 大幅度提高访问速度;
  • 缺点:需要维护缓存服务器,提高了编码的复杂性。


值得一提的是:

缓存集群的调度算法不同与上面提到的应用服务器和数据库。最好采用一致性哈希算,这样才能提高命中率

加入缓存后的系统结构如下:
7.png

阶段七:数据库水平拆分与垂直拆分

我们的网站演进到现在,交易、商品、用户的数据都还在同一个数据库中。尽管采取了增加缓存读写分离的方式,但随着数据库的压力继续增加,数据库数据量的瓶颈越来越突出,此时,我们可以有数据垂直拆分水平拆分两种选择。

数据垂直拆分

垂直拆分的意思是把数据库中不同的业务数据拆分到不同的数据库中,结合现在的例子,就是把交易、商品、用户的数据分开。

优点:
  • 解决了原来把所有业务放在一个数据库中的压力问题;
  • 可以根据业务的特点进行更多的优化。


缺点:
  • 需要维护多个数据库的状态一致性和数据同步。


问题:
  • 需要考虑原来跨业务的事务;
  • 跨数据库的Join


解决问题方案:
  • 应该在应用层尽量避免跨数据库的分布式事务,如果非要跨数据库,尽量在代码中控制。
  • 通过第三方中间件来解决,如上面提到的MyCatMyCat提供了丰富的跨库Join方案,详情可参考MyCat官方文档。


数据垂直拆分后的结构如下:
8.png

数据水平拆分

数据水平拆分就是把同一个表中的数据拆分到两个甚至多个数据库中。产生数据水平拆分的原因是某个业务的数据量或者更新量到达了单个数据库的瓶颈,这时就可以把这个表拆分到两个或更多个数据库中。

优点:
  • 如果能克服以上问题,那么我们将能够很好地对数据量及写入量增长的情况。


问题:
  • 访问用户信息的应用系统需要解决SQL路由的问题,因为现在用户信息分在了两个数据库中,需要在进行数据操作时了解需要操作的数据在哪里。
  • 主键 的处理也变得不同,例如原来自增字段,现在不能简单地继续使用。
  • 如果需要分页查询,那就更加麻烦。


解决问题方案:
  • 我们还是可以通过可以解决第三方中间件,如MyCatMyCat可以通过SQL解析模块对我们的SQL进行解析,再根据我们的配置,把请求转发到具体的某个数据库。 我们可以通过UUID保证唯一或自定义ID方案来解决。
  • MyCat也提供了丰富的分页查询方案,比如先从每个数据库做分页查询,再合并数据做一次分页查询等等。


数据水平拆分后的结构如下:
9.png

阶段八:应用的拆分

按微服务拆分应用

随着业务的发展,业务越来越多,应用越来越大。我们需要考虑如何避免让应用越来越臃肿。这就需要把应用拆开,从一个应用变为俩个甚至更多。还是以我们上面的例子,我们可以把用户、商品、交易拆分开。变成“用户、商品”和“用户,交易”两个子系统。  

拆分后的结构:
10.png

问题:

这样拆分后,可能会有一些相同的代码,如用户相关的代码,商品和交易都需要用户信息,所以在两个系统中都保留差不多的操作用户信息的代码。如何保证这些代码可以复用是一个需要解决的问题。

解决问题:

通过走服务化SOA的路线来解决频繁公共的服务。

走SOA服务化治理道路

为了解决上面拆分应用后所出现的问题,我们把公共的服务拆分出来,形成一种服务化的模式,简称SOA

采用服务化之后的系统结构:
11.png

优点:
  • 相同的代码不会散落在不同的应用中了,这些实现放在了各个服务中心,使代码得到更好的维护。
  • 我们把对数据库的交互业务放在了各个服务中心,让前端的Web应用更注重与浏览器交互的工作。


问题:

如何进行远程的服务调用?

解决方法:

可以通过下面的引入消息中间件来解决。

阶段九:引入消息中间件

随着网站的继续发展,的系统中可能出现不同语言开发的子模块和部署在不同平台的子系统。此时我们需要一个平台来传递可靠的,与平台和语言无关的数据,并且能够把负载均衡透明化,能在调用过程中收集分析调用数据,推测出网站的访问增长率等等一系列需求,对于网站应该如何成长做出预测。开源消息中间件有阿里的Dubbo,可以搭配Google开源的分布式程序协调服务ZooKeeper实现服务器的注册发现

引入消息中间件后的结构:
12.png

总结

以上的演变过程只是一个例子,并不适合所有的网站,实际中网站演进过程与自身业务和不同遇到的问题有密切的关系,没有固定的模式。只有认真的分析和不断地探究,才能发现适合自己网站的架构。

原文链接:https://juejin.cn/post/6844903639123771406,作者:零壹技术栈
继续阅读 »

我们以Java Web为例,来搭建一个简单的电商系统,看看这个系统可以如何一步步演变。

该系统具备的功能:
  • 用户模块:用户注册和管理
  • 商品模块:商品展示和管理
  • 交易模块:创建交易和管理


阶段一:单机构建网站

网站的初期,我们经常会在单机上跑我们所有的程序和软件。此时我们使用一个容器,如TomcatJettyJboss,然后直接使用JSP/Servlet技术,或者使用一些开源的框架如Maven + Spring + Struts + HibernateMaven + Spring + Spring MVC + Mybatis。最后再选择一个数据库管理系统来存储数据,如MySQLSQL ServerOracle,然后通过JDBC进行数据库的连接和操作。

把以上的所有软件包括数据库、应用程序都装载同一台机器上,应用跑起来了,也算是一个小系统了。此时系统结果如下:
1.png

阶段二:应用服务器与数据库分离

随着网站的上线,访问量逐步上升,服务器的负载慢慢提高,在服务器还没有超载的时候,我们应该就要做好准备,提升网站的负载能力。假如我们代码层面已难以优化,在不提高单台机器的性能的情况下,采用增加机器是一个不错的方式,不仅可以有效地提高系统的负载能力,而且性价比高。

增加的机器用来做什么呢?此时我们可以把数据库服务器Web服务器拆分开来,这样不仅提高了单台机器的负载能力,也提高了容灾能力。

应用服务器与数据库分开后的架构如下图所示:
2.png

阶段三:应用服务器集群

随着访问量继续增加,单台应用服务器已经无法满足需求了。在假设数据库服务器没有压力的情况下,我们可以把应用服务器从一台变成了两台甚至多台,把用户的请求分散到不同的服务器中,从而提高负载能力。而多台应用服务器之间没有直接的交互,他们都是依赖数据库各自对外提供服务。著名的做故障切换的软件有KeepAlivedKeepAlived是一个类似于Layer3、4、7交换机制的软件,他不是某个具体软件故障切换的专属品,而是可以适用于各种软件的一款产品。KeepAlived配合上ipvsadm又可以做负载均衡,可谓是神器。

我们以增加了一台应用服务器为例,增加后的系统结构图如下:
3.png

系统演变到这里,将会出现下面四个问题
  1. 用户的请求由谁来转发到到具体的应用服务器?
  2. 有那些转发的算法和策略可以使用?
  3. 应用服务器如何返回用户的请求?
  4. 用户如果每次访问到的服务器不一样,那么如何维护session的一致性?


针对以上问题,常用的解决方案如下:

1、负载均衡的问题

一般以下有5种解决方案:

1、HTTP重定向

HTTP重定向就是应用层的请求转发。用户的请求其实已经到了HTTP重定向负载均衡服务器,服务器根据算法要求用户重定向,用户收到重定向请求后,再次请求真正的集群。
  • 优点:简单易用;
  • 缺点:性能较差。


2、DNS域名解析负载均衡

DNS域名解析负载均衡就是在用户请求DNS服务器,获取域名对应的IP地址时,DNS服务器直接给出负载均衡后的服务器IP。
  • 优点:交给DNS,不用我们去维护负载均衡服务器;
  • 缺点:当一个应用服务器挂了,不能及时通知DNS,而且DNS负载均衡的控制权在域名服务商那里,网站无法做更多的改善和更强大的管理。


3、反向代理服务器

在用户的请求到达反向代理服务器时(已经到达网站机房),由反向代理服务器根据算法转发到具体的服务器。常用的ApacheNginx都可以充当反向代理服务器。
  • 优点:部署简单;
  • 缺点:代理服务器可能成为性能的瓶颈,特别是一次上传大文件。


4、IP层负载均衡

在请求到达负载均衡器后,负载均衡器通过修改请求的目的IP地址,从而实现请求的转发,做到负载均衡。
  • 优点:性能更好;
  • 缺点:负载均衡器的宽带成为瓶颈。


5、数据链路层负载均衡

在请求到达负载均衡器后,负载均衡器通过修改请求的MAC地址,从而做到负载均衡,与IP负载均衡不一样的是,当请求访问完服务器之后,直接返回客户。而无需再经过负载均衡器。

2、集群调度转发算法

1、rr轮询调度算法

顾名思义,轮询分发请求。
  • 优点:实现简单
  • 缺点:不考虑每台服务器的处理能力


2、wrr加权调度算法

我们给每个服务器设置权值Weight,负载均衡调度器根据权值调度服务器,服务器被调用的次数跟权值成正比。
  • 优点:考虑了服务器处理能力的不同


3、sh原地址散列算法

提取用户IP,根据散列函数得出一个key,再根据静态映射表,查处对应的value,即目标服务器IP。过目标机器超负荷,则返回空。
  • 优点:实现同一个用户访问同一个服务器。


4、dh目标地址散列算法

原理同上,只是现在提取的是目标地址的IP来做哈希。
  • 优点:实现同一个用户访问同一个服务器。


5、lc最少连接算法

优先把请求转发给连接数少的服务器。
  • 优点:使得集群中各个服务器的负载更加均匀。


6、wlc加权最少连接算法

lc的基础上,为每台服务器加上权值。算法为:(活动连接数 * 256 + 非活动连接数) ÷ 权重,计算出来的值小的服务器优先被选择。
  • 优点:可以根据服务器的能力分配请求。


7、sed最短期望延迟算法

其实sed跟wlc类似,区别是不考虑非活动连接数。算法为:(活动连接数 +1 ) * 256 ÷ 权重,同样计算出来的值小的服务器优先被选择。

8、nq永不排队算法

改进的sed算法。我们想一下什么情况下才能“永不排队”,那就是服务器的连接数为0的时候,那么假如有服务器连接数为0,均衡器直接把请求转发给它,无需经过sed的计算。

9、LBLC基于局部性最少连接算法

负载均衡器根据请求的目的IP地址,找出该IP地址最近被使用的服务器,把请求转发之。若该服务器超载,最采用最少连接数算法。

10、LBLCR带复制的基于局部性最少连接算法

负载均衡器根据请求的目的IP地址,找出该IP地址最近使用的“服务器组”,注意,并不是具体某个服务器,然后采用最少连接数从该组中挑出具体的某台服务器出来,把请求转发之。若该服务器超载,那么根据最少连接数算法,在集群的非本服务器组的服务器中,找出一台服务器出来,加入本服务器组,然后把请求转发。

3、集群请求返回模式问题

1、NAT

负载均衡器接收用户的请求,转发给具体服务器,服务器处理完请求返回给均衡器,均衡器再重新返回给用户。

2、DR

负载均衡器接收用户的请求,转发给具体服务器,服务器出来玩请求后直接返回给用户。需要系统支持IP Tunneling协议,难以跨平台

3、TUN

同上,但无需IP Tunneling协议,跨平台性好,大部分系统都可以支持。

4、集群Session一致性问题

1、Session Sticky

Session sticky就是把同一个用户在某一个会话中的请求,都分配到固定的某一台服务器中,这样我们就不需要解决跨服务器的session问题了,常见的算法有ip_hash算法,即上面提到的两种散列算法。
  • 优点:实现简单;
  • 缺点:应用服务器重启则session消失。


2、Session Replication

Session replication就是在集群中复制session,使得每个服务器都保存有全部用户的session数据。
  • 优点:减轻负载均衡服务器的压力,不需要要实现ip_hasp算法来转发请求;
  • 缺点:复制时网络带宽开销大,访问量大的话Session占用内存大且浪费。


3、Session数据集中存储

Session数据集中存储就是利用数据库来存储session数据,实现了session和应用服务器的解耦。
  • 优点:相比Session replication的方案,集群间对于宽带和内存的压力大幅减少;
  • 缺点:需要维护存储Session的数据库。


4、Cookie Base

Cookie base就是把Session存在Cookie中,由浏览器来告诉应用服务器我的session是什么,同样实现了session和应用服务器的解耦。
  • 优点:实现简单,基本免维护。
  • 缺点:cookie长度限制,安全性低,带宽消耗。


值得一提的是:
  • Nginx目前支持的负载均衡算法有wrrsh(支持一致性哈希)、fair(lc)。但Nginx作为均衡器的话,还可以一同作为静态资源服务器
  • Keepalived + ipvsadm比较强大,目前支持的算法有:rrwrrlcwlclblcshdh
  • Keepalived支持集群模式有:NATDRTUN
  • Nginx本身并没有提供session同步的解决方案,而Apache则提供了session共享的支持。


解决了以上的问题之后,系统的结构如下:
4.png

阶段四:数据库读写分离化

上面我们总是假设数据库负载正常,但随着访问量的的提高,数据库的负载也在慢慢增大。那么可能有人马上就想到跟应用服务器一样,把数据库一份为二再负载均衡即可。

但对于数据库来说,并没有那么简单。假如我们简单的把数据库一分为二,然后对于数据库的请求,分别负载到A机器和B机器,那么显而易见会造成两台数据库数据不统一的问题。那么对于这种情况,我们可以先考虑使用读写分离主从复制的方式。

读写分离后的系统结构如下:
5.png

这个结构变化后也会带来两个问题:
  • 主从数据库之间数据同步问题。
  • 应用对于数据源的选择问题。


解决方案:
  • 使用MySQL自带的Master + Slave的方式实现主从复制
  • 采用第三方数据库中间件,例如MyCatMyCat是从Cobar发展而来的,而Cobar是阿里开源的数据库中间件,后来停止开发。MyCat是国内比较好的MySql开源数据库分库分表中间件。


阶段五:用搜索引擎缓解读库的压力

数据库做读库的话,常常对模糊查找力不从心,即使做了读写分离,这个问题还未能解决。以我们所举的交易网站为例,发布的商品存储在数据库中,用户最常使用的功能就是查找商品,尤其是根据商品的标题来查找对应的商品。对于这种需求,一般我们都是通过like功能来实现的,但是这种方式的代价非常大,而且结果非常不准确。此时我们可以使用搜索引擎倒排索引来完成。

搜索引擎具有的优点:它能够大大提高查询速度和搜索准确性。

引入搜索引擎的开销:
  • 带来大量的维护工作,我们需要自己实现索引的构建过程,设计全量/增加的构建方式来应对非实时与实时的查询需求。
  • 需要维护搜索引擎集群


搜索引擎并不能替代数据库,它解决了某些场景下的精准、快速、高效的“读”操作,是否引入搜索引擎,需要综合考虑整个系统的需求。

引入搜索引擎后的系统结构如下:
6.png

阶段六:用缓存缓解读库的压力

常用的缓存机制包括页面级缓存、应用数据缓存和数据库缓存。

应用层和数据库层的缓存

随着访问量的增加,逐渐出现了许多用户访问同一部分热门内容的情况,对于这些比较热门的内容,没必要每次都从数据库读取。我们可以使用缓存技术,例如可以使用Google的开源缓存技术Guava或者使用Memecahed作为应用层的缓存,也可以使用Redis作为数据库层的缓存。

另外,在某些场景下,关系型数据库并不是很适合,例如我想做一个“每日输入密码错误次数限制”的功能,思路大概是在用户登录时,如果登录错误,则记录下该用户的IP和错误次数,那么这个数据要放在哪里呢?假如放在内存中,那么显然会占用太大的内容;假如放在关系型数据库中,那么既要建立数据库表,还要简历对应的Java bean,还要写SQL等等。而分析一下我们要存储的数据,无非就是类似{ip:errorNumber}这样的key:value数据。对于这种数据,我们可以用NOSQL数据库来代替传统的关系型数据库。

页面缓存

除了数据缓存,还有页面缓存。比如使用HTML5localstroage或者Cookie。除了页面缓存带来的性能提升外,对于并发访问且页面置换频率小的页面,应尽量使用页面静态化技术。
  • 优点:减轻数据库的压力, 大幅度提高访问速度;
  • 缺点:需要维护缓存服务器,提高了编码的复杂性。


值得一提的是:

缓存集群的调度算法不同与上面提到的应用服务器和数据库。最好采用一致性哈希算,这样才能提高命中率

加入缓存后的系统结构如下:
7.png

阶段七:数据库水平拆分与垂直拆分

我们的网站演进到现在,交易、商品、用户的数据都还在同一个数据库中。尽管采取了增加缓存读写分离的方式,但随着数据库的压力继续增加,数据库数据量的瓶颈越来越突出,此时,我们可以有数据垂直拆分水平拆分两种选择。

数据垂直拆分

垂直拆分的意思是把数据库中不同的业务数据拆分到不同的数据库中,结合现在的例子,就是把交易、商品、用户的数据分开。

优点:
  • 解决了原来把所有业务放在一个数据库中的压力问题;
  • 可以根据业务的特点进行更多的优化。


缺点:
  • 需要维护多个数据库的状态一致性和数据同步。


问题:
  • 需要考虑原来跨业务的事务;
  • 跨数据库的Join


解决问题方案:
  • 应该在应用层尽量避免跨数据库的分布式事务,如果非要跨数据库,尽量在代码中控制。
  • 通过第三方中间件来解决,如上面提到的MyCatMyCat提供了丰富的跨库Join方案,详情可参考MyCat官方文档。


数据垂直拆分后的结构如下:
8.png

数据水平拆分

数据水平拆分就是把同一个表中的数据拆分到两个甚至多个数据库中。产生数据水平拆分的原因是某个业务的数据量或者更新量到达了单个数据库的瓶颈,这时就可以把这个表拆分到两个或更多个数据库中。

优点:
  • 如果能克服以上问题,那么我们将能够很好地对数据量及写入量增长的情况。


问题:
  • 访问用户信息的应用系统需要解决SQL路由的问题,因为现在用户信息分在了两个数据库中,需要在进行数据操作时了解需要操作的数据在哪里。
  • 主键 的处理也变得不同,例如原来自增字段,现在不能简单地继续使用。
  • 如果需要分页查询,那就更加麻烦。


解决问题方案:
  • 我们还是可以通过可以解决第三方中间件,如MyCatMyCat可以通过SQL解析模块对我们的SQL进行解析,再根据我们的配置,把请求转发到具体的某个数据库。 我们可以通过UUID保证唯一或自定义ID方案来解决。
  • MyCat也提供了丰富的分页查询方案,比如先从每个数据库做分页查询,再合并数据做一次分页查询等等。


数据水平拆分后的结构如下:
9.png

阶段八:应用的拆分

按微服务拆分应用

随着业务的发展,业务越来越多,应用越来越大。我们需要考虑如何避免让应用越来越臃肿。这就需要把应用拆开,从一个应用变为俩个甚至更多。还是以我们上面的例子,我们可以把用户、商品、交易拆分开。变成“用户、商品”和“用户,交易”两个子系统。  

拆分后的结构:
10.png

问题:

这样拆分后,可能会有一些相同的代码,如用户相关的代码,商品和交易都需要用户信息,所以在两个系统中都保留差不多的操作用户信息的代码。如何保证这些代码可以复用是一个需要解决的问题。

解决问题:

通过走服务化SOA的路线来解决频繁公共的服务。

走SOA服务化治理道路

为了解决上面拆分应用后所出现的问题,我们把公共的服务拆分出来,形成一种服务化的模式,简称SOA

采用服务化之后的系统结构:
11.png

优点:
  • 相同的代码不会散落在不同的应用中了,这些实现放在了各个服务中心,使代码得到更好的维护。
  • 我们把对数据库的交互业务放在了各个服务中心,让前端的Web应用更注重与浏览器交互的工作。


问题:

如何进行远程的服务调用?

解决方法:

可以通过下面的引入消息中间件来解决。

阶段九:引入消息中间件

随着网站的继续发展,的系统中可能出现不同语言开发的子模块和部署在不同平台的子系统。此时我们需要一个平台来传递可靠的,与平台和语言无关的数据,并且能够把负载均衡透明化,能在调用过程中收集分析调用数据,推测出网站的访问增长率等等一系列需求,对于网站应该如何成长做出预测。开源消息中间件有阿里的Dubbo,可以搭配Google开源的分布式程序协调服务ZooKeeper实现服务器的注册发现

引入消息中间件后的结构:
12.png

总结

以上的演变过程只是一个例子,并不适合所有的网站,实际中网站演进过程与自身业务和不同遇到的问题有密切的关系,没有固定的模式。只有认真的分析和不断地探究,才能发现适合自己网站的架构。

原文链接:https://juejin.cn/post/6844903639123771406,作者:零壹技术栈 收起阅读 »

打造极致体验:字节跳动亿级 DAU 背后的音视频技术最佳实践


在短视频用户全球化,短视频产品及内容消费井喷式增长的今天,用户开始逐渐对视频体验有了越来越高的要求,这也意味着音视频技术的应用和探索需求越来越普遍。

6 月 26 日,火山引擎开发者社区将举办第三期 Meetup,本次活动邀请到了字节跳动的四位技术大咖,同大家一起聊聊亿级 DAU 短视频产品背后的音视频、直播、点播、RTC 等最佳技术实践,探讨如何打造极致的音视频用户体验,揭秘支撑抖音和西瓜等现象级产品背后的秘密“武器”。
分布式实验室_01副本.jpg

分布式实验室_02.jpg

分布式实验室_03.jpg

640.png

扫码进群获取直播链接、演讲 PPT,并和讲师交流。如遇问题,请添加社区小助手微信(ID:volcano-dev)

报名链接:https://www.bagevent.com/event ... echat
继续阅读 »

在短视频用户全球化,短视频产品及内容消费井喷式增长的今天,用户开始逐渐对视频体验有了越来越高的要求,这也意味着音视频技术的应用和探索需求越来越普遍。

6 月 26 日,火山引擎开发者社区将举办第三期 Meetup,本次活动邀请到了字节跳动的四位技术大咖,同大家一起聊聊亿级 DAU 短视频产品背后的音视频、直播、点播、RTC 等最佳技术实践,探讨如何打造极致的音视频用户体验,揭秘支撑抖音和西瓜等现象级产品背后的秘密“武器”。
分布式实验室_01副本.jpg

分布式实验室_02.jpg

分布式实验室_03.jpg

640.png

扫码进群获取直播链接、演讲 PPT,并和讲师交流。如遇问题,请添加社区小助手微信(ID:volcano-dev)

报名链接:https://www.bagevent.com/event ... echat 收起阅读 »

Prometheus 存储层的演进


Prometheus 是当下最流行的监控平台之一,它的主要职责是从各个目标节点中采集监控数据,后持久化到本地的时序数据库中,并向外部提供便捷的查询接口。本文尝试探讨 Prometheus 存储层的演进过程,信息源主要来自于 Prometheus 团队在历届 PromConf 上的分享。

时序数据库是 Promtheus 监控平台的一部分,在了解其存储层的演化过程之前,我们需要先了解时序数据库及其要解决的根本问题。

TSDB

时序数据库(Time Series Database,TSDB)是数据库大家庭中的一员,专门存储随时间变化的数据,如股票价格、传感器数据、机器状态监控等等。时序(Time Series)指的是某个变量随时间变化的所有历史,而样本(Sample)指的是历史中该变量的瞬时值:
1.jpeg

每个样本由时序标识时间戳数值 3 部分构成,其所属的时序就由一系列样本构成。由于时间是连续的,我们不可能、也没有必要记录时序在每个时刻的数值,因此采样间隔(Interval)也是时序的重要组成部分。采样间隔越小、样本总量越大、捕获细节越多;采样间隔越大、样本总量越小、遗漏细节越多。以服务器机器监控为例,通常采样间隔为 15 秒。

数据的高效查询离不开索引,对于时序数据而言,唯一的、天然的索引就是时间(戳)。因此通常时序数据库的存储层相比于关系型数据库要简单得多。仔细思考,你可能会发现时序数据在某种程度上就是键值数据的一个子集,因此键值数据库天然地可以作为时序数据的载体。通常一个时序数据库能容纳百万量级以上的时序数据,要从其中搜索到其中少量的几个时序也非易事,因此对时序本身建立高效的索引也很重要。

The Fundamental Problem of TSDBs

TSDB 要解决的基本问题,可以概括为下图:
2.jpeg

研究过存储引擎结构和性能优化的工程师都会知道,许多数据库的奇技淫巧都是在解决内存与磁盘的读写模式、性能的不匹配问题。

时序数据库也是数据库的一种,只要它想持久化,自然不能例外。但与键值数据库相比,时序数据库存储的数据有更特殊的读写特征,Björn Rabenstein 将称其为:Vertical writes, horizontal(-ish) reads(垂直写,水平读)。

图中每条横线就是一个时序,每个时序由按照(准)固定间隔采集的样本数据构成,通常在时序数据库中会有很多活跃时序,因此数据写入可以用一个垂直的窄方框表示,即每个时序都要写入新的样本数据;用户在查询时,通常会观察某个、某几个时序在某个时间段内的变化趋势,或对其进行聚合计算,因此数据读取可以用一个水平的方框表示。是谓“垂直写、水平读”。

Storage Layer of Prometheus

Prometheus 是为云原生环境中的数据监控而生,在其设计过程中至少需要考虑以下两个方面:

1、在云原生环境中,实例可能随时出现、消失,因此时序也可能随时出现或消失,即系统中存在大量时序,其中部分处于活跃状态,这会在多方面带来挑战:
  • 如何存储大量时序避免资源浪费
  • 如何定位被查询的少数几个时序


2、监控系统本身应该尽量少地依赖外部服务,否则外部服务失效将引发监控系统失效。

对于第 2 点,Prometheus 团队选择放弃集群,使用单机架构,并且在单机系统中使用本地 TSDB 做数据持久化,完全不依赖外部服务;第 1 点是需要存储、索引、查询引擎层合作解决的问题,在下文中我们将进一步分析存储层在其中的作用。Prometheus 存储层的演进可以分成 3 个阶段:
  • 1st Generation:Prototype
  • 2nd Generation:Prometheus V1
  • 3rd Generation:Prometheus V2


注意:本节只关注 Prometheus 时序数据的存储,不涉及索引、WAL 等其它数据的存储。

Data Model

尽管数据模型是存储层之上的抽象,理论上它不应该影响存储层的设计。但理解数据模型能够帮助我们更快地理解存储层。

在 Prometheus 中,每个时序实际上由多个标签(labels)标识,如:
api_http_requests_total{path="/users",status=200,method="GET",instance="10.111.201.26"} 

该时序的名字为 api_http_requests_total,标签为 path、status、method 和 instance,只有时序名字和标签键值完全相同的时序才是同一个时序。事实上,时序名字就是一个隐藏标签:
{__name__="api_http_requests_total",path="/users",status=200,method="GET",
instance="10.111.201.26"} 

对于用户来说,标签之间不存在先后顺序,用户可能关注:
  • 所有 API 调用的 status
  • 某个 path 调用的成功率、QPS
  • 某个实例、某个 path 调用的成功率
  • ……


1st Generation:Prototype

在 Prototype 阶段,Prometheus 直接利用开源的键值数据库(LevelDB)作为本地持久化存储,并采用与 BigTable 推荐的时序数据方案类似的 schema 设计:
3.jpeg

时序名称、标签(固定顺序)、时间戳拼接成每个样本的键,于是同一个时序的数据就能够连续存储在键值数据库中,提高范围查询的效率。但从图中可以看出,这种方式存储的键很长,尽管键值数据库内部会对数据进行压缩,但是在内存中这样存储数据很浪费空间,这无法满足项目的设计要求。Prometheus 希望在内存中压缩数据,使得内存中可以容纳更多活跃的时序数据,同时在磁盘中也能按类似的方式压缩编码,提高效率。时序数据比通用键值数据有更显著的特征。即使键值数据库能够压缩数据,但针对时序数据的特征,使用特殊的压缩算法能够取得更好的压缩率。因此在 Prototype 阶段,使用三方键值数据库的方案最终流产。

2nd Generation:Prometheus V1

Compression

Why Compression

假设监控系统的需求如下:
  • 500 万活跃时序
  • 30 秒采样间隔
  • 1 个月数据留存


那么经过计算可以得到具体的存储要求:
  • 平均每秒采集 166000 个样本
  • 存储样本总量为 4320 亿个样本


假设没有任何压缩,不算时序标识,每个样本需要 16 个字节存储空间 (时间戳 8 个字节、数值 8 个字节),整个系统的存储总量为 7 TB,假设数据需要留存 6 个月,则总量为 42 TB,那么如果能找到一种有效的方式压缩数据,就能在单机的内存和磁盘中存放更多、更长的时序数据。

Chunked Storage Abstraction

上文提到 TSDB 的根本问题是“垂直写,水平读”,每次采样都会需要为每个活跃时序写入一条样本数据,但如果每次为每个时序写入 16 个字节到 HDD/SSD 中,显然这对块存储设备十分不友好,效率低下。因此 Prometheus V2 将数据按固定长度切割相同大小的分段(Chunks),方便压缩、批量读写。

访问时序数据时,Prometheus 使用 3 层抽象,如下图所示:
4.jpeg

应用层使用 Series Iterator 顺序访问时序中的样本,而 Series Iterator 底下由一个个 Chunk Iterator 拼接而成,每个 Chunk Iterator 负责将压缩编码的时序数据解码返回。这样做的好处是,每个 Chunk 甚至可以使用完全不同的方式编码,方便开发团队尝试不同的编码方案。

Timestamp Compression:Double Delta

由于通常数据采样间隔是固定值,因此前后时间戳的差值几乎固定,如 15 s,30 s。但如果我们更近一步,只存储差值的差值,那么几乎不用再为新的时间戳花费额外的空间,这便是所谓的“Double Delta”。本质上,如果未来所有的采集时间戳都可以精准预测,那么每个新时间戳的信息熵为 0 bit。但现实并不完美,网络可能延迟、中断,实例可能遇到 GC、重启,采样间隔随时有可能波动:
5.jpeg

但这种波动的幅度有限,Prometheus 采用了和 FB 的内存时序数据库 Gorilla 类似的方式编码时间戳,详情可以参考 Gorilla) 以及 Björn Rabenstein 在 PromCon 2016 的演讲 PPT,细节比较琐碎,这里不赘述。

Value Compression

Prometheus 和 Gorilla 中的每个样本值都是 float64 类型。Gorilla 利用 float64 的二进制表示(IEEE754)将前后两个样本值 XOR 来寻找压缩的空间,能获得 1.37 bytes/sample 的压缩能力。Prometheus V2 采用的方式比较简单:
  • 如果可能的话,使用整型(8/16/32 位)存储,否则用 float32,最后实在不行就直接存储 float64
  • 如果数值增长得很规律,则不使用额外的空间存储


以上做法给 Prometheus V1 带来了 3.3 bytes/sample 的压缩能力。相比于为完全存储于内存中的 Gorilla 相比,这样的压缩能力对于 Prometheus 已经够用,但在 V2 中,Prometheus 也融合了 Gorilla 采用的压缩技术。

Chunk Encoding

Prometheus V1 将每个时序分割成大小为 1KB 的 chunks,如下图所示:
6.jpeg

在内存中保留着最近写入的 chunk,其中 head chunk 正在接收新的样本。每当一个 head chunk 写满 1KB 时,会立即被冻结,我们称之为完整的 chunk,从此刻开始该 chunk 中的数据就是不可变的(immutable),同时生成一个新的 head chunk 负责消化新的请求。每个完整的 chunk 会被尽快地持久化到磁盘中。内存中保存着每个时序最近被写入或被访问的 chunks,当 chunks 数量过多时,存储引擎会将超过的 chunks 通过 LRU 策略清出。

在 Prometheus V1 中,每个时序都会被存储到在一个独占的文件中,这也意味着大量的时序将产生大量的文件。存储引擎会定期地去检查磁盘中的时序文件,是否已经有 chunk 数据超过保留时间,如果有则将其删除(复制后删除)。

Prometheus 的查询引擎的查询过程必须完全在内存中进行。因此在执行之前,存储引擎需要将不在内存中的 chunks 预加载到内存中:
7.jpeg

如果在内存中的 chunks 持久化之前系统发生崩溃,则会产生数据丢失。为了减少数据丢失,Prometheus V1 还使用了额外的 checkpoint 文件,用于存储各个时序中尚未写入磁盘的 chunks:
8.jpeg

Prometheus V1 vs. Gorilla

正因为 Prometheus V1 与 Gorilla 的设计理念、需求有所不同,我们可以通过对比二者来理解其设计过程中使用不同决策的原因。
9.jpeg

3rd Generation:Prometheus V2

The Main Problem With 2nd Generation

Prometheus V1 中,每个时序数据对应一个磁盘文件的方式给系统带来了比较大的麻烦:
  • 由于在云原生环境下,会不断产生新的时序、废弃旧的时序(Series Churn),因此实际上存储层需要的文件数量远远高于活跃的时序数量。任其发展迟早会将文件系统的 inodes 消耗殆尽。而且一旦发生,恢复系统将异常麻烦。不仅如此,在新旧时序大量更迭时,由于旧时序数据尚未从内存中清出,系统的内存消耗量也会飙升,造成 OOM。
  • 即便使用 chunks 来批量读写数据,从整体上看,系统每秒钟仍要向磁盘写入数千个 chunks,造成 I/O 压力;如果通过增大每批写入的量来减少 I/O 次数,又将造成内存的压力。
  • 同时将所有时序文件保持打开状态很不合理,需要消耗大量的资源。如果在查询前后打开、关闭文件,又会增加查询的时延。
  • 当数据超过留存时间时需要删除相关的 chunks,这意味着每隔一段时间就要对数百万的文件执行一次删除数据操作,这个过程可能需要持续数小时。
  • 通过周期性地将未持久化的 chunks 写入 checkpoint 文件理论上确实可以减少数据丢失,但是如果执行数据恢复需要很长时间,那么实际上又错过了新的数据,还不如不恢复。


因此 Prometheus 的第三代存储引擎,主要改变就是放弃“一个时序对应一个文件”的设计理念。

Macro Design

第三代存储引擎在磁盘中的文件结构如下图所示:
10.jpeg

根目录下,顺序排列着编了号的 blocks,每个 block 中包含 index 和 chunk 文件夹,后者里面包含编了号的 chunks,每个 chunk 包含许多不同时序的样本数据。其中 index 文件中的信息可以帮我我们快速锁定时序的标签及其可能的取值,进而找到相关的时序和持有该时序样本数据的 chunks。值得注意的是,最新的 block 文件夹中还包含一个 wal 文件夹,后者将承担故障恢复的职责。

Many Little Databases

第三代存储引擎将所有时序数据按时间分片,即在时间维度上将数据划分成互不重叠的 blocks,如下图所示:
11.jpeg

每个 block 实际上就是一个小型数据库,内部存储着该时间窗口内的所有时序数据,因此它需要拥有自己的 index 和 chunks。除了最新的、正在接收新鲜数据的 block 之外,其它 blocks 都是不可变的。由于新数据的写入都在内存中,数据的写效率较高:
12.jpeg

为了防止数据丢失,所有新采集的数据都会被写入到 WAL 日志中,在系统恢复时能快速地将其中的数据恢复到内存中。在查询时,我们需要将查询发送到不同的 block 中,再将结果聚合。

按时间将数据分片赋予了存储引擎新的能力:
  • 当查询某个时间范围内的数据,我们可以直接忽略在时间范围外的 blocks
  • 写完一个 block 后,我们可以将轻易地其持久化到磁盘中,因为只涉及到少量几个文件的写入
  • 新的数据,也是最常被查询的数据会处在内存中,提高查询效率(第二代同样支持)
  • 每个 chunk 不再是固定的 1KB 大小,我们可以选择任意合适的大小,选择合适的压缩方式
  • 删除超过留存时间的数据变得异常简单,直接删除整个文件夹即可


mmap

第三代引擎将数百万的小文件合并成少量大文件,也让 mmap 成为可能。利用 mmap 将文件 I/O 、缓存管理交给操作系统,降低 OOM 发生的频率。

Compaction

在 Macro Design 中,我们将所有时序数据按时间切割成许多 blocks,当新写满的 block 持久化到磁盘后,相应的 WAL 文件也会被清除。写入数据时,我们希望每个 block 不要太大,比如 2 小时左右,来避免在内存中积累过多的数据。读取数据时,若查询涉及到多个时间段,就需要对许多个 block 分别执行查询,然后再合并结果。假如需要查询一周的数据,那么这个查询将涉及到 80 多个 blocks,降低数据读取的效率。

为了既能写得快,又能读得快,我们就得引入 compaction,后者将一个或多个 blocks 中的数据合并成一个更大的 block,在合并的过程中会自动丢弃被删除的数据、合并多个版本的数据、重新结构化 chunks 来优化查询效率,如下图所示:
13.jpeg

Retention

当数据超过留存时间时,删除旧数据非常容易:
14.jpeg

直接删除在边界之外的 block 文件夹即可。如果边界在某个 block 之内,则暂时将它留存,知道边界超出为止。当然,在 Compaction 中,我们会将旧的 blocks 合并成更大的 block;在 Retention 时,我们又希望能够粒度更小。所以 Compaction 与 Retention 的策略之间存在着一定的互斥关系。Prometheus 的系统参数可以对单个 block 的大小作出限制,来寻找二者之间的平衡。

看到这里,相信你已经发现了,这不就是 LSM Tree 吗?每个 block 就是按时间排序的 SSTable,内存中的 block 就是 MemTable。

Compression

第三代存储引擎融合了 Gorilla 的 XOR float encoding 方案,将压缩能力提升到 1-2 bytes/sample。具体方案可以概括为按顺序采用以下第一条适用的策略:
  1. Zero encoding:如果完全可预测,则无需额外空间
  2. Integer double-delta encoding:如果是整型,可以利用 double-delta 原理,将不等的前后间隔分成 6/13/20/33 bits 几种,来优化空间使用
  3. XOR float encoding:参考 Gorilla
  4. Direct encoding:直接存 float64


平均下来能取得 1.28 bytes/sample 的压缩能力。

原文链接:https://zhuanlan.zhihu.com/p/155719693
继续阅读 »

Prometheus 是当下最流行的监控平台之一,它的主要职责是从各个目标节点中采集监控数据,后持久化到本地的时序数据库中,并向外部提供便捷的查询接口。本文尝试探讨 Prometheus 存储层的演进过程,信息源主要来自于 Prometheus 团队在历届 PromConf 上的分享。

时序数据库是 Promtheus 监控平台的一部分,在了解其存储层的演化过程之前,我们需要先了解时序数据库及其要解决的根本问题。

TSDB

时序数据库(Time Series Database,TSDB)是数据库大家庭中的一员,专门存储随时间变化的数据,如股票价格、传感器数据、机器状态监控等等。时序(Time Series)指的是某个变量随时间变化的所有历史,而样本(Sample)指的是历史中该变量的瞬时值:
1.jpeg

每个样本由时序标识时间戳数值 3 部分构成,其所属的时序就由一系列样本构成。由于时间是连续的,我们不可能、也没有必要记录时序在每个时刻的数值,因此采样间隔(Interval)也是时序的重要组成部分。采样间隔越小、样本总量越大、捕获细节越多;采样间隔越大、样本总量越小、遗漏细节越多。以服务器机器监控为例,通常采样间隔为 15 秒。

数据的高效查询离不开索引,对于时序数据而言,唯一的、天然的索引就是时间(戳)。因此通常时序数据库的存储层相比于关系型数据库要简单得多。仔细思考,你可能会发现时序数据在某种程度上就是键值数据的一个子集,因此键值数据库天然地可以作为时序数据的载体。通常一个时序数据库能容纳百万量级以上的时序数据,要从其中搜索到其中少量的几个时序也非易事,因此对时序本身建立高效的索引也很重要。

The Fundamental Problem of TSDBs

TSDB 要解决的基本问题,可以概括为下图:
2.jpeg

研究过存储引擎结构和性能优化的工程师都会知道,许多数据库的奇技淫巧都是在解决内存与磁盘的读写模式、性能的不匹配问题。

时序数据库也是数据库的一种,只要它想持久化,自然不能例外。但与键值数据库相比,时序数据库存储的数据有更特殊的读写特征,Björn Rabenstein 将称其为:Vertical writes, horizontal(-ish) reads(垂直写,水平读)。

图中每条横线就是一个时序,每个时序由按照(准)固定间隔采集的样本数据构成,通常在时序数据库中会有很多活跃时序,因此数据写入可以用一个垂直的窄方框表示,即每个时序都要写入新的样本数据;用户在查询时,通常会观察某个、某几个时序在某个时间段内的变化趋势,或对其进行聚合计算,因此数据读取可以用一个水平的方框表示。是谓“垂直写、水平读”。

Storage Layer of Prometheus

Prometheus 是为云原生环境中的数据监控而生,在其设计过程中至少需要考虑以下两个方面:

1、在云原生环境中,实例可能随时出现、消失,因此时序也可能随时出现或消失,即系统中存在大量时序,其中部分处于活跃状态,这会在多方面带来挑战:
  • 如何存储大量时序避免资源浪费
  • 如何定位被查询的少数几个时序


2、监控系统本身应该尽量少地依赖外部服务,否则外部服务失效将引发监控系统失效。

对于第 2 点,Prometheus 团队选择放弃集群,使用单机架构,并且在单机系统中使用本地 TSDB 做数据持久化,完全不依赖外部服务;第 1 点是需要存储、索引、查询引擎层合作解决的问题,在下文中我们将进一步分析存储层在其中的作用。Prometheus 存储层的演进可以分成 3 个阶段:
  • 1st Generation:Prototype
  • 2nd Generation:Prometheus V1
  • 3rd Generation:Prometheus V2


注意:本节只关注 Prometheus 时序数据的存储,不涉及索引、WAL 等其它数据的存储。

Data Model

尽管数据模型是存储层之上的抽象,理论上它不应该影响存储层的设计。但理解数据模型能够帮助我们更快地理解存储层。

在 Prometheus 中,每个时序实际上由多个标签(labels)标识,如:
api_http_requests_total{path="/users",status=200,method="GET",instance="10.111.201.26"} 

该时序的名字为 api_http_requests_total,标签为 path、status、method 和 instance,只有时序名字和标签键值完全相同的时序才是同一个时序。事实上,时序名字就是一个隐藏标签:
{__name__="api_http_requests_total",path="/users",status=200,method="GET",
instance="10.111.201.26"} 

对于用户来说,标签之间不存在先后顺序,用户可能关注:
  • 所有 API 调用的 status
  • 某个 path 调用的成功率、QPS
  • 某个实例、某个 path 调用的成功率
  • ……


1st Generation:Prototype

在 Prototype 阶段,Prometheus 直接利用开源的键值数据库(LevelDB)作为本地持久化存储,并采用与 BigTable 推荐的时序数据方案类似的 schema 设计:
3.jpeg

时序名称、标签(固定顺序)、时间戳拼接成每个样本的键,于是同一个时序的数据就能够连续存储在键值数据库中,提高范围查询的效率。但从图中可以看出,这种方式存储的键很长,尽管键值数据库内部会对数据进行压缩,但是在内存中这样存储数据很浪费空间,这无法满足项目的设计要求。Prometheus 希望在内存中压缩数据,使得内存中可以容纳更多活跃的时序数据,同时在磁盘中也能按类似的方式压缩编码,提高效率。时序数据比通用键值数据有更显著的特征。即使键值数据库能够压缩数据,但针对时序数据的特征,使用特殊的压缩算法能够取得更好的压缩率。因此在 Prototype 阶段,使用三方键值数据库的方案最终流产。

2nd Generation:Prometheus V1

Compression

Why Compression

假设监控系统的需求如下:
  • 500 万活跃时序
  • 30 秒采样间隔
  • 1 个月数据留存


那么经过计算可以得到具体的存储要求:
  • 平均每秒采集 166000 个样本
  • 存储样本总量为 4320 亿个样本


假设没有任何压缩,不算时序标识,每个样本需要 16 个字节存储空间 (时间戳 8 个字节、数值 8 个字节),整个系统的存储总量为 7 TB,假设数据需要留存 6 个月,则总量为 42 TB,那么如果能找到一种有效的方式压缩数据,就能在单机的内存和磁盘中存放更多、更长的时序数据。

Chunked Storage Abstraction

上文提到 TSDB 的根本问题是“垂直写,水平读”,每次采样都会需要为每个活跃时序写入一条样本数据,但如果每次为每个时序写入 16 个字节到 HDD/SSD 中,显然这对块存储设备十分不友好,效率低下。因此 Prometheus V2 将数据按固定长度切割相同大小的分段(Chunks),方便压缩、批量读写。

访问时序数据时,Prometheus 使用 3 层抽象,如下图所示:
4.jpeg

应用层使用 Series Iterator 顺序访问时序中的样本,而 Series Iterator 底下由一个个 Chunk Iterator 拼接而成,每个 Chunk Iterator 负责将压缩编码的时序数据解码返回。这样做的好处是,每个 Chunk 甚至可以使用完全不同的方式编码,方便开发团队尝试不同的编码方案。

Timestamp Compression:Double Delta

由于通常数据采样间隔是固定值,因此前后时间戳的差值几乎固定,如 15 s,30 s。但如果我们更近一步,只存储差值的差值,那么几乎不用再为新的时间戳花费额外的空间,这便是所谓的“Double Delta”。本质上,如果未来所有的采集时间戳都可以精准预测,那么每个新时间戳的信息熵为 0 bit。但现实并不完美,网络可能延迟、中断,实例可能遇到 GC、重启,采样间隔随时有可能波动:
5.jpeg

但这种波动的幅度有限,Prometheus 采用了和 FB 的内存时序数据库 Gorilla 类似的方式编码时间戳,详情可以参考 Gorilla) 以及 Björn Rabenstein 在 PromCon 2016 的演讲 PPT,细节比较琐碎,这里不赘述。

Value Compression

Prometheus 和 Gorilla 中的每个样本值都是 float64 类型。Gorilla 利用 float64 的二进制表示(IEEE754)将前后两个样本值 XOR 来寻找压缩的空间,能获得 1.37 bytes/sample 的压缩能力。Prometheus V2 采用的方式比较简单:
  • 如果可能的话,使用整型(8/16/32 位)存储,否则用 float32,最后实在不行就直接存储 float64
  • 如果数值增长得很规律,则不使用额外的空间存储


以上做法给 Prometheus V1 带来了 3.3 bytes/sample 的压缩能力。相比于为完全存储于内存中的 Gorilla 相比,这样的压缩能力对于 Prometheus 已经够用,但在 V2 中,Prometheus 也融合了 Gorilla 采用的压缩技术。

Chunk Encoding

Prometheus V1 将每个时序分割成大小为 1KB 的 chunks,如下图所示:
6.jpeg

在内存中保留着最近写入的 chunk,其中 head chunk 正在接收新的样本。每当一个 head chunk 写满 1KB 时,会立即被冻结,我们称之为完整的 chunk,从此刻开始该 chunk 中的数据就是不可变的(immutable),同时生成一个新的 head chunk 负责消化新的请求。每个完整的 chunk 会被尽快地持久化到磁盘中。内存中保存着每个时序最近被写入或被访问的 chunks,当 chunks 数量过多时,存储引擎会将超过的 chunks 通过 LRU 策略清出。

在 Prometheus V1 中,每个时序都会被存储到在一个独占的文件中,这也意味着大量的时序将产生大量的文件。存储引擎会定期地去检查磁盘中的时序文件,是否已经有 chunk 数据超过保留时间,如果有则将其删除(复制后删除)。

Prometheus 的查询引擎的查询过程必须完全在内存中进行。因此在执行之前,存储引擎需要将不在内存中的 chunks 预加载到内存中:
7.jpeg

如果在内存中的 chunks 持久化之前系统发生崩溃,则会产生数据丢失。为了减少数据丢失,Prometheus V1 还使用了额外的 checkpoint 文件,用于存储各个时序中尚未写入磁盘的 chunks:
8.jpeg

Prometheus V1 vs. Gorilla

正因为 Prometheus V1 与 Gorilla 的设计理念、需求有所不同,我们可以通过对比二者来理解其设计过程中使用不同决策的原因。
9.jpeg

3rd Generation:Prometheus V2

The Main Problem With 2nd Generation

Prometheus V1 中,每个时序数据对应一个磁盘文件的方式给系统带来了比较大的麻烦:
  • 由于在云原生环境下,会不断产生新的时序、废弃旧的时序(Series Churn),因此实际上存储层需要的文件数量远远高于活跃的时序数量。任其发展迟早会将文件系统的 inodes 消耗殆尽。而且一旦发生,恢复系统将异常麻烦。不仅如此,在新旧时序大量更迭时,由于旧时序数据尚未从内存中清出,系统的内存消耗量也会飙升,造成 OOM。
  • 即便使用 chunks 来批量读写数据,从整体上看,系统每秒钟仍要向磁盘写入数千个 chunks,造成 I/O 压力;如果通过增大每批写入的量来减少 I/O 次数,又将造成内存的压力。
  • 同时将所有时序文件保持打开状态很不合理,需要消耗大量的资源。如果在查询前后打开、关闭文件,又会增加查询的时延。
  • 当数据超过留存时间时需要删除相关的 chunks,这意味着每隔一段时间就要对数百万的文件执行一次删除数据操作,这个过程可能需要持续数小时。
  • 通过周期性地将未持久化的 chunks 写入 checkpoint 文件理论上确实可以减少数据丢失,但是如果执行数据恢复需要很长时间,那么实际上又错过了新的数据,还不如不恢复。


因此 Prometheus 的第三代存储引擎,主要改变就是放弃“一个时序对应一个文件”的设计理念。

Macro Design

第三代存储引擎在磁盘中的文件结构如下图所示:
10.jpeg

根目录下,顺序排列着编了号的 blocks,每个 block 中包含 index 和 chunk 文件夹,后者里面包含编了号的 chunks,每个 chunk 包含许多不同时序的样本数据。其中 index 文件中的信息可以帮我我们快速锁定时序的标签及其可能的取值,进而找到相关的时序和持有该时序样本数据的 chunks。值得注意的是,最新的 block 文件夹中还包含一个 wal 文件夹,后者将承担故障恢复的职责。

Many Little Databases

第三代存储引擎将所有时序数据按时间分片,即在时间维度上将数据划分成互不重叠的 blocks,如下图所示:
11.jpeg

每个 block 实际上就是一个小型数据库,内部存储着该时间窗口内的所有时序数据,因此它需要拥有自己的 index 和 chunks。除了最新的、正在接收新鲜数据的 block 之外,其它 blocks 都是不可变的。由于新数据的写入都在内存中,数据的写效率较高:
12.jpeg

为了防止数据丢失,所有新采集的数据都会被写入到 WAL 日志中,在系统恢复时能快速地将其中的数据恢复到内存中。在查询时,我们需要将查询发送到不同的 block 中,再将结果聚合。

按时间将数据分片赋予了存储引擎新的能力:
  • 当查询某个时间范围内的数据,我们可以直接忽略在时间范围外的 blocks
  • 写完一个 block 后,我们可以将轻易地其持久化到磁盘中,因为只涉及到少量几个文件的写入
  • 新的数据,也是最常被查询的数据会处在内存中,提高查询效率(第二代同样支持)
  • 每个 chunk 不再是固定的 1KB 大小,我们可以选择任意合适的大小,选择合适的压缩方式
  • 删除超过留存时间的数据变得异常简单,直接删除整个文件夹即可


mmap

第三代引擎将数百万的小文件合并成少量大文件,也让 mmap 成为可能。利用 mmap 将文件 I/O 、缓存管理交给操作系统,降低 OOM 发生的频率。

Compaction

在 Macro Design 中,我们将所有时序数据按时间切割成许多 blocks,当新写满的 block 持久化到磁盘后,相应的 WAL 文件也会被清除。写入数据时,我们希望每个 block 不要太大,比如 2 小时左右,来避免在内存中积累过多的数据。读取数据时,若查询涉及到多个时间段,就需要对许多个 block 分别执行查询,然后再合并结果。假如需要查询一周的数据,那么这个查询将涉及到 80 多个 blocks,降低数据读取的效率。

为了既能写得快,又能读得快,我们就得引入 compaction,后者将一个或多个 blocks 中的数据合并成一个更大的 block,在合并的过程中会自动丢弃被删除的数据、合并多个版本的数据、重新结构化 chunks 来优化查询效率,如下图所示:
13.jpeg

Retention

当数据超过留存时间时,删除旧数据非常容易:
14.jpeg

直接删除在边界之外的 block 文件夹即可。如果边界在某个 block 之内,则暂时将它留存,知道边界超出为止。当然,在 Compaction 中,我们会将旧的 blocks 合并成更大的 block;在 Retention 时,我们又希望能够粒度更小。所以 Compaction 与 Retention 的策略之间存在着一定的互斥关系。Prometheus 的系统参数可以对单个 block 的大小作出限制,来寻找二者之间的平衡。

看到这里,相信你已经发现了,这不就是 LSM Tree 吗?每个 block 就是按时间排序的 SSTable,内存中的 block 就是 MemTable。

Compression

第三代存储引擎融合了 Gorilla 的 XOR float encoding 方案,将压缩能力提升到 1-2 bytes/sample。具体方案可以概括为按顺序采用以下第一条适用的策略:
  1. Zero encoding:如果完全可预测,则无需额外空间
  2. Integer double-delta encoding:如果是整型,可以利用 double-delta 原理,将不等的前后间隔分成 6/13/20/33 bits 几种,来优化空间使用
  3. XOR float encoding:参考 Gorilla
  4. Direct encoding:直接存 float64


平均下来能取得 1.28 bytes/sample 的压缩能力。

原文链接:https://zhuanlan.zhihu.com/p/155719693 收起阅读 »