Spring Cloud

Spring Cloud

应用量化时代 | 微服务架构的服务治理之路

博云BoCloud 发表了文章 • 0 个评论 • 26 次浏览 • 2019-06-19 16:10 • 来自相关话题

【编者的话】微服务下一个“兵家必争”的场景。 技术随业务而生,业务载技术而行。 近些年来,伴随数字经济的发展,在众多企业的数字化转型之路上,云原生、DevOps、微服务、服 ...查看全部

【编者的话】微服务下一个“兵家必争”的场景。



技术随业务而生,业务载技术而行。



近些年来,伴随数字经济的发展,在众多企业的数字化转型之路上,云原生、DevOps、微服务、服务治理等成为行业内不断被探讨的新话题。人们在理解和接受这些新型概念的同时,也不断地思考其可能的落地形态。需求是创造发生的原动力,于是一批代表性的开源技术或者框架涌现而出:Kubernetes,Spring Cloud,Service Mesh,Serverless…… 它们炙手可热,大放异彩。然而在具体落地过程中,却举步维艰,磕磕绊绊。 如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态



本文试图结合企业业务的核心诉求,以应用形态发展历程为背景,帮助企业梳理应用面向云原生、微服务转型中涉及的各种服务治理问题,以及服务治理的发展趋势。





什么是服务治理?



服务治理(SOA governance),按照Anne Thomas Manes的定义是:企业为了确保事情顺利完成而实施的过程,包括最佳实践、架构原则、治理规程、规律以及其他决定性的因素。服务治理指的是用来管理SOA的采用和实现的过程。



由定义可知,服务治理关键因素在于:应用形态,数据采集,信息分析,管控策略,协议规范五个方面。用户群体只有从这五个层次出发,才能构建出符合企业规范与要求的服务治理平台,从而进一步创造商业价值。





“微观”塑形,服务一小再小



世界上唯一不变的是变化本身。

——By 斯宾塞.约翰逊



万理同此,纵观应用形态发展历程,从单机到网络、从单体到服务化、到微服务、到Serverless,再到未来,应用的形态随着业务驱动和技术演化,一直在不断变化。随之而来的是业务需求的复杂化与多样化,企业IT面临着大规模、高并发、应用快速创新等新难题,弹性与敏捷成为企业IT的迫切需求。



在IT行业内有两个“不成熟”的理论:第一,每增加一行代码就会带来N种风险;第二,任何问题都可以采取增加一层抽象的方式解决。因此面对企业IT复杂的环境,“小而精”逐渐取代“大而全”,成为构建企业服务的首选方式,这也导致软件设计原则中的“高内聚,低耦合”又开始成为不断被高调吟诵的主角,微服务理念因此大行其道。



微服务架构为业务单元可独立开发和独立部署,使服务具备灵活的动态处理机能,同时依赖高度抽象化的组件工具和多元化的通信机制,向用户屏蔽所有服务之间的通信细节的这种思想提供了最佳落地实践。微服务的出现有效地缩短了服务上线周期,并且允许企业快速响应客户反馈,为客户提供所期望的可靠服务。



然而随着企业业务的发展与扩张与微服务的深入,服务数量向不可控的规模增长,服务数量的爆发式增长,为服务管理以及线上治理带来了极大的挑战。服务治理应运而生,成为构建微服务架构系统的必备“良药”。





“量化”管控,服务无可遁形



数字永远不会说谎。



如今,微服务已经成为软件架构的实际指导思想,而以Docker和Kubernetes为代表的容器技术的延伸,也有效解决了微服务架构下多个服务单元的编排部署问题。然而,微服务架构下也隐藏着容易被忽视的风险:面临规模巨大的服务单元,如何对其进行有效合理的管控与治理?



服务治理领域开始被行业与用户所重视,期望能够获得有效的思维方式和技术手段,应对由于不断激增的服务单元带来的服务治理挑战。关于服务治理,我们看到的更多的是其功能集合:服务注册发现、服务配置、服务熔断、网关、负载均衡、服务跟踪、日志采集、监控平台等。但当我们抛开这些名词解释,重新审视服务治理的时候,这些名词并没有完整的解释我们的困惑:如何设置负载均衡策略?采集日志格式是什么?服务配置如何生效?服务跟踪如何进行精确定位?



显然单单通过这些功能名词无法满足我们构建服务治理平台的需求,但从这些功能中我们总结出一些规律与方法,我们将从功能场景的横向切面和技术手段的纵深层次,进行如何构建一个有效的服务治理平台的分析探讨。



首先,我们从服务治理功能场景的横向切面来看,其可以抽象为四个层面:量化,追踪,管控,规范。





量化


量化包括服务数据采集、数据过滤和数据聚合三个层次。数据采集进一步细分为业务数据和性能数据,业务数据主要包括方法响应周期、服务内资源消耗规模、业务异常检测、方法调用次数、服务运行日志等;性能数据包括服务间响应时长、服务整体资源消耗等。



服务本身需要依赖不同的特性,构建不同的agent,来搜集服务运行时产生的数据。数据过滤针对采集的数据按照一定的格式规范进一步加工处理,例如基于kafka对原始的日志数据进行标准化处理后,导入日志系统。



数据聚合需要对独立的服务数据进行聚合操作,例如服务调用链呈现。



通过服务量化能够清晰的记录服务运行时产生的所有数据,为服务跟踪呈现和服务管控策略制定并提供强有力的数据支撑。





追踪


追踪能够有效量化服务调用链路上发生的事情,具体来讲,可以划分为:服务间的链路跟踪和服务内部的方法调用链路跟踪。追踪的本质,不仅仅是为了呈现服务链路及服务路由信息,更重要的是呈现服务间请求,以及服务内部请求的响应延迟,异常反馈,能够快速定位服务以及服务内在代码存在的问题。





管控


管控依赖于量化采集的聚合数据。管控允许运维人员聚焦某个服务单元的运行时状态,为服务设定一定的控制策略,从而保证服务稳定可靠的运行。例如熔断策略,负载策略,流量控制,权限控制等。





规范


规范更多针对服务通信而言,例如通信协议规范,无论针对哪种协议,例如http,tcp,rpc等都能够提供相应的检测手段。与此同时,规范也能够清晰定义服务名称和管控策略,使得服务在不同环境之间进行迁移的时候,依旧平稳可靠。



综上所述,在服务单元遵循一定规范标准的前提下,基于服务单元数据量化、服务调用跟踪以及服务策略管控的方式,才能构建出符合要求的服务治理平台。



接下来,我们从纵深的角度考虑构建服务治理平台过程中涉及的技术理论基础。服务治理之所以困难,原因在于构建业务系统采用的技术栈成多元化的方式存在。从目前行业内采用的技术而言可以划分为三大学派:代码集成,agent探针,流量劫持





代码集成


代码集成往往需要业务开发人员的支持,在业务系统中嵌入数据采集代码,用来采集服务运行时服务产生的各种业务指标及性能指标,并将数据传输到云端治理平台。平台依据数据信息,通过配置动态下发,从而影响业务响应动态,完成服务治理功能。



优点:治理深入,端到端监控



缺点:维护繁琐,语言版本众多,影响业务性能





Agent探针


Agent探针是对代码集成的进一步提炼。Agent探针将需要集成的监控代码,高度提取、抽象、封装成可以独立集成的SDK,并且以“弱旁路”的方式与代码集成在一起,从而完成数据采集工作。云端治理平台,同样以采集的数据信息作为治理策略制定的依据,下发各种治理策略,从而达到服务治理功能。



优点:治理深入,端到端监控



缺点:语言版本众多,影响业务性能





流量劫持


流量劫持与前两者相比,与代码集成不同。它从网络通信作为切入点,以proxy的方式,代理业务单元所有的IN/OUT流量,并且proxy内部可以对请求数据进行一定的策略控制。从而完成服务通信的治理功能。



优点:无关语言差异性,维护简单



缺点:治理略浅,影响业务性能



综上所述,目前服务治理的技术栈或多或少都存在一些缺陷,在构建服务治理平台时往往需要采用结合的方式,才能做到物尽其才。





“百家争鸣”,谁能一统天下



竞争成就未来。



从目前行业发展来看,微服务奠定了服务构建的基础方式,容器引擎以及编排技术解决了服务编排上线的困惑,下一个“兵家必争”的场景必将在服务治理。那目前行业内又有哪些项目聚焦在服务治理领域?





Spring Cloud


Spring Cloud作为Spring社区的重要布局之一,在微服务落地伊始就逐渐发力,当下已经成为Java体系下微服务框架的代名词,Spring Cloud 以 Netfilx 全家桶作为初始化基础,为开发人员提供业务单元服务支撑框架的同时,也开发出一系列的服务治理SDK,供开发人员选用。在微服务发展背景下,SpringCloud可谓如日中天。





Dubbo


Dubbo原为阿里开源的 rpc 远程调用框架,初始设计初衷在于解决以 rpc 协议为标准的远程服务调用问题,随着阿里重启Dubbo,其也开始在服务治理领域发力,成为很多以rpc协议作为通信基础系统平台的首选。粗略而言,Dubbo和SpringCloud已成为Java体系下的服务治理“双枪”。





gRPC


gRPC与Dubbo类似,最初是由Google开源的一款远程服务调用框架。gRPC凭借HTTP/2和 RrotoBuf 服务定义方式以及多语言支持的特性,加之其易于定制与开发,能够方面开发人员进行快速扩展和灵活发挥,从而也成为众多用户的选择之一。





Service Mesh


Service Mesh的出现不在于它实现了多少功能,而是它彻底把业务单元与业务支撑体系分离,完整贯彻了“术业有专攻”的思想理念。它允许业务人员聚焦业务实现,不再关心服务治理相关的内容。通过与容器技术结合,下沉至基础设施,从通信协议的角度彻底接管业务通信交互过程,可谓微服务治理领域的后起之秀。



总而言之,服务治理的本质是针对业务与应用产生价值的收敛与反馈,只有不断地反馈和复盘才能构建出稳定、高效的应用形态。

API网关如何实现对服务下线实时感知

翔宇 发表了文章 • 0 个评论 • 248 次浏览 • 2019-06-04 15:50 • 来自相关话题

上篇文章《Eureka 缓存机制》介绍了Eureka的缓存机制,相信大家对Eureka有了进一步的了解,本文将详细介绍API网关如何实现服务下线的实时感知。 #一、前言 在基于云的微服务应用中,服务实例的网络位置都是动态分配的。而且 ...查看全部
上篇文章《Eureka 缓存机制》介绍了Eureka的缓存机制,相信大家对Eureka有了进一步的了解,本文将详细介绍API网关如何实现服务下线的实时感知。
#一、前言

在基于云的微服务应用中,服务实例的网络位置都是动态分配的。而且由于自动伸缩、故障和升级,服务实例会经常动态改变。因此,客户端代码需要使用更加复杂的服务发现机制。

目前服务发现主要有两种模式:客户端发现和服务端发现。

* 服务端发现:客户端通过负载均衡器向服务注册中心发起请求,负载均衡器查询服务注册中心,将每个请求路由到可用的服务实例上。
* 客户端发现:客户端负责决定可用服务实例的网络地址,并且在集群中对请求负载均衡, 客户端访问服务登记表,也就是一个可用服务的数据库,然后客户端使用一种负载均衡算法选择一个可用的服务实例然后发起请求。

客户端发现相对于服务端发现最大的区别是:客户端知道(缓存)可用服务注册表信息。如果Client端缓存没能从服务端及时更新的话,可能出现Client 与 服务端缓存数据不一致的情况。
#二、网关与Eureka结合使用

Netflix OSS 提供了一个客户端服务发现的好例子。Eureka Server 为注册中心,Zuul 相对于Eureka Server来说是Eureka Client,Zuul 会把 Eureka Server 端服务列表缓存到本地,并以定时任务的形式更新服务列表,同时 Zuul 通过本地列表发现其它服务,使用 Ribbon 实现客户端负载均衡。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
1.jpg

正常情况下,调用方对网关发起请求即刻能得到响应。但是当对生产者做缩容、下线、升级的情况下,由于Eureka这种多级缓存的设计结构和定时更新的机制,LoadBalance 端的服务列表B存在更新不及时的情况(由上篇文章《Eureka 缓存机制》可知,服务消费者最长感知时间将无限趋近240s),如果这时消费者对网关发起请求,LoadBalance 会对一个已经不存在的服务发起请求,请求是会超时的。
#三、解决方案

##实现思路

生产者下线后,最先得到感知的是 Eureka Server 中的 readWriteCacheMap,最后得到感知的是网关核心中的 LoadBalance。但是 loadBalance 对生产者的发现是在 loadBalance 本地维护的列表中。

所以要想达到网关对生产者下线的实时感知,可以这样做:首先生产者或者部署平台主动通知 Eureka Server,然后跳过 Eureka 多级缓存之间的更新时间,直接通知 Zuul 中的 Eureka Client,最后将 Eureka Client 中的服务列表更新到 Ribbon 中。

但是如果下线通知的逻辑代码放在生产者中,会造成代码污染、语言差异等问题。

借用一句名言:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。
2.jpg

Gateway-SynchSpeed 相当于一个代理服务,它对外提供REST API来负责响应调用方的下线请求,同时会将生产者的状态同步到 Eureka Server 和 网关核心,起着 状态同步 和 软事物 的作用。

思路:在生产者做 缩容、下线、升级 前,spider 平台(spider为容器管理平台)会主动通知 Gateway-SynchSpeed 某个生产者的某个实例要下线了,然后 Gateway-SynchSpeed 会通知 Eureka Server 生产者的某个实例下线了;如果Eureka Server 下线成功,Gateway-SynchSpeed 会直接通知 网关核心。

设计特点:

* 无侵入性、方便使用。不用关心调用方的基于何种语言实现,调用者只要对 Gateway-SynchSpeed 发起一个http rest请求即可,真正的实现逻辑不用侵入到调用方而是交给这个代理来实现。
* 原子性。调用方先在Eureka Server下线,然后在所有相关网关核心中下线为最小工作执行单元,Gateway-SynchSpeed 相当于一个"软事物",保证服务下线的某种程度上原子特性。

##实现步骤

3.jpg

步骤说明:

第一步:在生产者做 缩容、下线、升级 前,spider平台会以http请求的形式通知到 Gateway-SynchSpeed 服务,通知的粒度为服务实例所在的容器IP。

第二步:Gateway-SynchSpeed 接受到请求后,先校验IP的可用性,然后通知Eureka Server。

第三步:Eureka Server 将 Producer 置为失效状态,并返回处理结果(Eureka 下线形式分为两种,一种是直接从服务注册列表直接剔除,第二种是状态下线,即是将 Producer 的状态置为OUT_OF_SERVICE。 如果是以第一种形式下线,Spider平台发出下线请求后,不能保证Producer进程立刻被kill,如果这期间 Producer 还有心跳同步到 Eureka Server,服务会重新注册到 Eureka Server)。

第四步:Gateway-SynchSpeed 得到上一步结果,如果结果为成功,则执行下一步;反之,则停止。

第五步:Gateway-SynchSpeed 为Eureka Client。Gateway-SynchSpeed 通过 IP 到本地服务注册列表中得到 Producer 的 Application-Name。

第六步:Gateway-SynchSpeed 通过 Application-Name 到网关核心库中查询所有与下线服务相关的 网关组名字。

第七步:Gateway-SynchSpeed 通过 网关组名字 到本地服务列表中查找网关组下所有的服务地址 ipAddress(ip : port)。

第八步:Gateway-SynchSpeed 异步通知所有相关网关节点。

第九步:Gateway-Core 收到通知后,对 Producer 做状态下线,同时记录所有状态下线成功的实例信息到缓存 DownServiceCache 中。

第十步:Gateway-Core 更新本地 Ribbon 服务列表。
#四、补偿机制

Eureka 提供了一种安全保护机制。Eureka Client 从 Eureka Server 更新服务列表前,会校验相关Hash值是否改变(Client 服务列表被修改,hash值会改变),如果改变,更新方式会从增量更新变成全量更新,(由《Eureka 缓存机制》可知这30s内 readOnlyCacheMap 和 readWriteCacheMap 的数据可能存在差异),如果Client端缓存列表被readOnlyCacheMap 覆盖,最终会导致 Ribbon 端服务列表与 readWriteCacheMap 数据不一致。
4.jpg

针对 Eureka 这种机制,引入监听器 EurekaEventListener 作为补偿机制,它会监听 Eureka Client 全量拉取事件,对于缓存中未超过30s的服务,将其状态重新设置成 OUT_OF_SERVICE 。
#五、API安全设计

考虑到系统的安全性问题,如果被人恶意访问,可能会使生产者在Eureka Server中无故下线,导致消费者无法通过 Eureka Server 来发现生产者。

使用黑白名单做安全过滤,基本流程如下:

* 对 Gateway-Synchspeed 中设置白名单网段(IP网段)。
* 在 Gateway-Synchspeed 加入过滤器,对下线请求方进行IP校验,如果请求端IP在网段中,则放行;反之,过滤。

#六、日志回溯

由于 Gateway-SynchSpeed 和 Gateway-Core 是部署在 Docker 容器中,如果容器重启,会导致日志文件全部丢失。所以需要将 Gateway-SynchSpeed 和 Gateway-Core 中相关日志写入到 Elasticsearch ,最终由 Kibana 负责查询 Elasticsearch 的数据并以可视化的方式展现。
#七、代码片段展示

Gateway-SynchSpeed 做状态同步。
5.jpg

EurekaEventListener 处理缓存数据。
6.jpg

#八、 补充说明

目前网关实现对服务下线的实时感知中,使用的 Zuul 和 Eureka 版本为 Spring Cloud Zuul 1.3.6.RELEASE、Spring Cloud Eureka 1.4.4.RELEASE。

目前网关实现的是对网关下游服务的实时感知,而且需满足以下条件:

* 生产者需部署在 kubernetes 容器管理平台 。
* 生产者做正常的下线、升级或者缩容操作。如果是由于容器资源不足,导致服务异常宕机等非正常下线,不支持。

网关服务下线实时感知是网关对业务方提供的一种可选的解决方案,在 spider 平台中默认是没有开启此功能,是否开启此功能由业务方根据本身系统要求决定,具体如何配置可参考 API网关接入指南 中 《网关实时感知在spider上配置文档说明》。

原文链接:http://college.creditease.cn/detail/256

详解Eureka缓存机制

阿娇 发表了文章 • 0 个评论 • 279 次浏览 • 2019-06-04 14:45 • 来自相关话题

【编者的话】Eureka是Netflix开源的、用于实现服务注册和发现的服务。Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便。但是由于Eureka本身存在较多缓存,服务状态更新滞后,最常见的状况是:服务下 ...查看全部
【编者的话】Eureka是Netflix开源的、用于实现服务注册和发现的服务。Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便。但是由于Eureka本身存在较多缓存,服务状态更新滞后,最常见的状况是:服务下线后状态没有及时更新,服务消费者调用到已下线的服务导致请求失败。本文基于Spring Cloud Eureka 1.4.4.RELEASE,在默认region和zone的前提下,介绍Eureka的缓存机制。
#一、AP特性

从CAP理论看,Eureka是一个AP系统,优先保证可用性(A)和分区容错性(P),不保证强一致性(C),只保证最终一致性,因此在架构中设计了较多缓存。
1.jpg

Eureka高可用架构
#二、服务状态

Eureka服务状态enum类:com.netflix.appinfo.InstanceInfo.InstanceStatus
2.png

#三、Eureka Server

在Eureka高可用架构中,Eureka Server也可以作为Client向其他server注册,多节点相互注册组成Eureka集群,集群间相互视为peer。Eureka Client向Server注册、续约、更新状态时,接受节点更新自己的服务注册信息后,逐个同步至其他peer节点。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

【注意】如果server-A向server-B节点单向注册,则server-A视server-B为peer节点,server-A接受的数据会同步给server-B,但server-B接受的数据不会同步给server-A。
##缓存机制

Eureka Server存在三个变量:(registry、readWriteCacheMap、readOnlyCacheMap)保存服务注册信息,默认情况下定时任务每30s将readWriteCacheMap同步至readOnlyCacheMap,每60s清理超过90s未续约的节点,Eureka Client每30s从readOnlyCacheMap更新服务注册信息,而UI则从registry更新服务注册信息。
3.jpg

三级缓存:
4.jpg

缓存相关配置:
5.jpg

关键类:
6.jpg

#四、Eureka Client

Eureka Client存在两种角色:服务提供者和服务消费者,作为服务消费者一般配合Ribbon或Feign(Feign内部使用Ribbon)使用。Eureka Client启动后,作为服务提供者立即向Server注册,默认情况下每30s续约(renew);作为服务消费者立即向Server全量更新服务注册信息,默认情况下每30s增量更新服务注册信息;Ribbon延时1s向Client获取使用的服务注册信息,默认每30s更新使用的服务注册信息,只保存状态为UP的服务。

二级缓存:
7.jpg

缓存相关配置:
8.jpg

关键类:
9.jpg

#五、默认配置下服务消费者最长感知时间

10.jpg

考虑如下情况:

* 0s时服务未通知Eureka Client直接下线;
* 29s时第一次过期检查evict未超过90s;
* 89s时第二次过期检查evict未超过90s;
* 149s时第三次过期检查evict未续约时间超过了90s,故将该服务实例从registry和readWriteCacheMap中删除;
* 179s时定时任务从readWriteCacheMap更新至readOnlyCacheMap;
* 209s时Eureka Client从Eureka Server的readOnlyCacheMap更新;
* 239s时Ribbon从Eureka Client更新。

因此,极限情况下服务消费者最长感知时间将无限趋近240s。
11.jpg

#六、应对措施

服务注册中心在选择使用Eureka时说明已经接受了其优先保证可用性(A)和分区容错性(P)、不保证强一致性(C)的特点。如果需要优先保证强一致性(C),则应该考虑使用ZooKeeper等CP系统作为服务注册中心。分布式系统中一般配置多节点,单个节点服务上线的状态更新滞后并没有什么影响,这里主要考虑服务下线后状态更新滞后的应对措施。
##Eureka Server

1、缩短readOnlyCacheMap更新周期。缩短该定时任务周期可减少滞后时间。
eureka.server.responsecCacheUpdateIntervalMs: 10000  # Eureka Server readOnlyCacheMap更新周期

2、关闭readOnlyCacheMap。中小型系统可以考虑该方案,Eureka Client直接从readWriteCacheMap更新服务注册信息。
eureka.server.useReadOnlyResponseCache: false        # 是否使用readOnlyCacheMap

##Eureka Client

1、服务消费者使用容错机制。如Spring Cloud Retry和Hystrix,Ribbon、Feign、Zuul都可以配置Retry,服务消费者访问某个已下线节点时一般报ConnectTimeout,这时可以通过Retry机制重试下一个节点。

2、服务消费者缩短更新周期。Eureka Client和Ribbon二级缓存影响状态更新,缩短这两个定时任务周期可减少滞后时间,例如配置:
eureka.client.registryFetchIntervalSeconds: 5        # Eureka Client更新周期
ribbon.ServerListRefreshInterval: 2000 # Ribbon更新周期

3、服务提供者保证服务正常下线。服务下线时使用kill或kill -15命令,避免使用kill -9命令,kill或kill -15命令杀死进程时将触发Eureka Client的shutdown()方法,主动删除Server的registry和readWriteCacheMap中的注册信息,不必依赖Server的evict清除。

4、服务提供者延迟下线。服务下线之前先调用接口使Eureka Server中保存的服务状态为DOWN或OUT_OF_SERVICE后再下线,二者时间差根据缓存机制和配置决定,比如默认情况下调用接口后延迟90s再下线服务即可保证服务消费者不会调用已下线服务实例。
#七、网关实现服务下线实时感知

在软件工程中,没有一个问题是中间层解决不了的,而网关是服务提供者和服务消费者的中间层。以Spring Cloud Zuul网关为例,网关作为Eureka Client保存了服务注册信息,服务消费者通过网关将请求转发给服务提供者,只需要做到服务提供者下线时通知网关在自己保存的服务列表中使该服务失效。为了保持网关的独立性,可实现一个独立服务接收下线通知并协调网关集群。下篇文章将详细介绍网关如何实现服务下线实时感知,敬请期待!

原文链接:https://mp.weixin.qq.com/s/zwoIDzX8WouYVrBfMJMpJQ

Spring Cloud微服务如何设计异常处理机制?

齐达内 发表了文章 • 0 个评论 • 163 次浏览 • 2019-06-03 21:36 • 来自相关话题

【编者的话】今天和大家聊一下在采用Spring Cloud进行微服务架构设计时,微服务之间调用时异常处理机制应该如何设计的问题。我们知道在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向内部和外部提供相应的功能服务接口。面向外部提供的服务接口,会通 ...查看全部
【编者的话】今天和大家聊一下在采用Spring Cloud进行微服务架构设计时,微服务之间调用时异常处理机制应该如何设计的问题。我们知道在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向内部和外部提供相应的功能服务接口。面向外部提供的服务接口,会通过服务网关(如使用Zuul提供的apiGateway)面向公网提供服务,如给App客户端提供的用户登陆、注册等服务接口。

而面向内部的服务接口,则是在进行微服务拆分后由于各个微服务系统的边界划定问题所导致的功能逻辑分散,而需要微服务之间彼此提供内部调用接口,从而实现一个完整的功能逻辑,它是之前单体应用中本地代码接口调用的服务化升级拆分。例如,需要在团购系统中,从下单到完成一次支付,需要交易系统在调用订单系统完成下单后再调用支付系统,从而完成一次团购下单流程,这个时候由于交易系统、订单系统及支付系统是三个不同的微服务,所以为了完成这次用户订单,需要App调用交易系统提供的外部下单接口后,由交易系统以内部服务调用的方式再调用订单系统和支付系统,以完成整个交易流程。如下图所示:
1.png

这里需要说明的是,在基于Spring Cloud的微服务架构中,所有服务都是通过如Consul或Eureka这样的服务中间件来实现的服务注册与发现后来进行服务调用的,只是面向外部的服务接口会通过网关服务进行暴露,面向内部的服务接口则在服务网关进行屏蔽,避免直接暴露给公网。而内部微服务间的调用还是可以直接通过Consul或Eureka进行服务发现调用,这二者并不冲突,只是外部客户端是通过调用服务网关,服务网关通过Consul再具体路由到对应的微服务接口,而内部微服务则是直接通过Consul或者Eureka发现服务后直接进行调用。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#异常处理的差异

面向外部的服务接口,我们一般会将接口的报文形式以JSON的方式进行响应,除了正常的数据报文外,我们一般会在报文格式中冗余一个响应码和响应信息的字段,如正常的接口成功返回:
 {
"code" : "0",
"msg" : "success",
"data" : {
"userId" : "zhangsan",
"balance" : 5000
}
}

而如果出现异常或者错误,则会相应地返回错误码和错误信息,如:
 {
"code" : "-1",
"msg" : "请求参数错误",
"data" : null
}

在编写面向外部的服务接口时,服务端所有的异常处理我们都要进行相应地捕获,并在controller层映射成相应地错误码和错误信息,因为面向外部的是直接暴露给用户的,是需要进行比较友好的展示和提示的,即便系统出现了异常也要坚决向用户进行友好输出,千万不能输出代码级别的异常信息,否则用户会一头雾水。对于客户端而言,只需要按照约定的报文格式进行报文解析及逻辑处理即可,一般我们在开发中调用的第三方开放服务接口也都会进行类似的设计,错误码及错误信息分类得也是非常清晰!

而微服务间彼此的调用在异常处理方面,我们则是希望更直截了当一些,就像调用本地接口一样方便,在基于Spring Cloud的微服务体系中,微服务提供方会提供相应的客户端SDK代码,而客户端SDK代码则是通过FeignClient的方式进行服务调用,如:而微服务间彼此的调用在异常处理方面,我们则是希望更直截了当一些,就像调用本地接口一样方便,在基于Spring Cloud的微服务体系中,微服务提供方会提供相应的客户端SDK代码,而客户端SDK代码则是通过FeignClient的方式进行服务调用,如:
@FeignClient( value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class )
public interface OrderClient {
/[i] 订单(内) [/i]/
@RequestMapping( value = "/order/createOrder", method = RequestMethod.POST )
OrderCostDetailVo orderCost( @RequestParam(value = "orderId") String orderId,
@RequestParam(value = "userId") long userId,
@RequestParam(value = "orderType") String orderType,
@RequestParam(value = "orderCost") int orderCost,
@RequestParam(value = "currency") String currency,
@RequestParam(value = "tradeTime") String tradeTime )
}

而服务的调用方在拿到这样的SDK后就可以忽略具体的调用细节,实现像本地接口一样调用其他微服务的内部接口了,当然这个是FeignClient框架提供的功能,它内部会集成像Ribbon和Hystrix这样的框架来实现客户端服务调用的负载均衡和服务熔断功能(注解上会指定熔断触发后的处理代码类),由于本文的主题是讨论异常处理,这里暂时就不作展开了。

现在的问题是,虽然FeignClient向服务调用方提供了类似于本地代码调用的服务对接体验,但服务调用方却是不希望调用时发生错误的,即便发生错误,如何进行错误处理也是服务调用方希望知道的事情。另一方面,我们在设计内部接口时,又不希望将报文形式搞得类似于外部接口那样复杂,因为大多数场景下,我们是希望服务的调用方可以直截了的获取到数据,从而直接利用FeignClient客户端的封装,将其转化为本地对象使用。
@Data
@Builder
public class OrderCostDetailVo implements Serializable {
private String orderId;
private String userId;
private int status; /[i] 1:欠费状态;2:扣费成功 [/i]/
private int orderCost;
private String currency;
private int payCost;
private int oweCost;
public OrderCostDetailVo( String orderId, String userId, int status, int orderCost, String currency, int payCost,
int oweCost )
{
this.orderId = orderId;
this.userId = userId;
this.status = status;
this.orderCost = orderCost;
this.currency = currency;
this.payCost = payCost;
this.oweCost = oweCost;
}
}

如我们在把返回数据就是设计成了一个正常的VO/BO对象的这种形式,而不是向外部接口那么样额外设计错误码或者错误信息之类的字段,当然,也并不是说那样的设计方式不可以,只是感觉会让内部正常的逻辑调用,变得比较啰嗦和冗余,毕竟对于内部微服务调用来说,要么对,要么错,错了就Fallback逻辑就好了。

不过,话虽说如此,可毕竟服务是不可避免的会有异常情况的。如果内部服务在调用时发生了错误,调用方还是应该知道具体的错误信息的,只是这种错误信息的提示需要以异常的方式被集成了FeignClient的服务调用方捕获,并且不影响正常逻辑下的返回对象设计,也就是说我不想额外在每个对象中都增加两个冗余的错误信息字段,因为这样看起来不是那么优雅!

既然如此,那么应该如何设计呢?
#最佳实践设计

首先,无论是内部还是外部的微服务,在服务端我们都应该设计一个全局异常处理类,用来统一封装系统在抛出异常时面向调用方的返回信息。而实现这样一个机制,我们可以利用Spring提供的注解@ControllerAdvice来实现异常的全局拦截和统一处理功能。如:
@Slf4j
@RestController
@ControllerAdvice
public class GlobalExceptionHandler {
@Resource
MessageSource messageSource;
@ExceptionHandler( { org.springframework.web.bind.MissingServletRequestParameterException.class } )
@ResponseBody
public APIResponse processRequestParameterException( HttpServletRequest request,
HttpServletResponse response,
MissingServletRequestParameterException e )
{
response.setStatus( HttpStatus.FORBIDDEN.value() );
response.setContentType( "application/json;charset=UTF-8" );
APIResponse result = new APIResponse();
result.setCode( ApiResultStatus.BAD_REQUEST.getApiResultStatus() );
result.setMessage(
messageSource.getMessage( ApiResultStatus.BAD_REQUEST.getMessageResourceName(),
null, LocaleContextHolder.getLocale() ) + e.getParameterName() );
return(result);
}

@ExceptionHandler( Exception.class )
@ResponseBody
public APIResponse processDefaultException( HttpServletResponse response,
Exception e )
{
/[i] log.error("Server exception", e); [/i]/
response.setStatus( HttpStatus.INTERNAL_SERVER_ERROR.value() );
response.setContentType( "application/json;charset=UTF-8" );
APIResponse result = new APIResponse();
result.setCode( ApiResultStatus.INTERNAL_SERVER_ERROR.getApiResultStatus() );
result.setMessage( messageSource.getMessage( ApiResultStatus.INTERNAL_SERVER_ERROR.getMessageResourceName(), null,
LocaleContextHolder.getLocale() ) );
return(result);
}

@ExceptionHandler( ApiException.class )
@ResponseBody
public APIResponse processApiException( HttpServletResponse response,
ApiException e )
{
APIResponse result = new APIResponse();
response.setStatus( e.getApiResultStatus().getHttpStatus() );
response.setContentType( "application/json;charset=UTF-8" );
result.setCode( e.getApiResultStatus().getApiResultStatus() );
String message = messageSource.getMessage( e.getApiResultStatus().getMessageResourceName(),
null, LocaleContextHolder.getLocale() );
result.setMessage( message );
/[i] log.error("Knowned exception", e.getMessage(), e); [/i]/
return(result);
}

/**
* 内部微服务异常统一处理方法
*/
@ExceptionHandler( InternalApiException.class )
@ResponseBody
public APIResponse processMicroServiceException( HttpServletResponse response,
InternalApiException e )
{
response.setStatus( HttpStatus.OK.value() );
response.setContentType( "application/json;charset=UTF-8" );
APIResponse result = new APIResponse();
result.setCode( e.getCode() );
result.setMessage( e.getMessage() );
return(result);
}
}

如上述代码,我们在全局异常中针对内部统一异常及外部统一异常分别作了全局处理,这样只要服务接口抛出了这样的异常就会被全局处理类进行拦截并统一处理错误的返回信息。

理论上我们可以在这个全局异常处理类中,捕获处理服务接口业务层抛出的所有异常并统一响应,只是那样会让全局异常处理类变得非常臃肿,所以从最佳实践上考虑,我们一般会为内部和外部接口分别设计一个统一面向调用方的异常对象,如外部统一接口异常我们叫ApiException,而内部统一接口异常叫InternalApiException。这样,我们就需要在面向外部的服务接口controller层中,将所有的业务异常转换为ApiException;而在面向内部服务的controller层中将所有的业务异常转化为InternalApiException。如:
@RequestMapping( value = "/creatOrder", method = RequestMethod.POST )
public OrderCostDetailVo orderCost(
@RequestParam(value = "orderId") String orderId,
@RequestParam(value = "userId") long userId,
@RequestParam(value = "orderType") String orderType,
@RequestParam(value = "orderCost") int orderCost,
@RequestParam(value = "currency") String currency,
@RequestParam(value = "tradeTime") String tradeTime ) throws InternalApiException
{
OrderCostVo costVo = OrderCostVo.builder().orderId( orderId ).userId( userId ).busiId( busiId ).orderType( orderType )
.duration( duration ).bikeType( bikeType ).bikeNo( bikeNo ).cityId( cityId ).orderCost( orderCost )
.currency( currency ).strategyId( strategyId ).tradeTime( tradeTime ).countryName( countryName )
.build();
OrderCostDetailVo orderCostDetailVo;
try {
orderCostDetailVo = orderCostServiceImpl.orderCost( costVo );
return(orderCostDetailVo);
} catch ( VerifyDataException e ) {
log.error( e.toString() );
throw new InternalApiException( e.getCode(), e.getMessage() );
} catch ( RepeatDeductException e ) {
log.error( e.toString() );
throw new InternalApiException( e.getCode(), e.getMessage() );
}
}

如上面的内部服务接口的controller层中将所有的业务异常类型都统一转换成了内部服务统一异常对象InternalApiException了。这样全局异常处理类,就可以针对这个异常进行统一响应处理了。

对于外部服务调用方的处理就不多说了。而对于内部服务调用方而言,为了能够更加优雅和方便地实现异常处理,我们也需要在基于FeignClient的SDK代码中抛出统一内部服务异常对象,如:
@FeignClient( value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class )
public interface OrderClient {
/[i] 订单(内) [/i]/
@RequestMapping( value = "/order/createOrder", method = RequestMethod.POST )
OrderCostDetailVo orderCost( @RequestParam(value = "orderId") String orderId,
@RequestParam(value = "userId") long userId,
@RequestParam(value = "orderType") String orderType,
@RequestParam(value = "orderCost") int orderCost,
@RequestParam(value = "currency") String currency,
@RequestParam(value = "tradeTime") String tradeTime ) throws InternalApiException
};

这样在调用方进行调用时,就会强制要求调用方捕获这个异常,在正常情况下调用方不需要理会这个异常,像本地调用一样处理返回对象数据就可以了。在异常情况下,则会捕获到这个异常的信息,而这个异常信息则一般在服务端全局处理类中会被设计成一个带有错误码和错误信息的json数据,为了避免客户端额外编写这样的解析代码,FeignClient为我们提供了异常解码机制。如:
@Slf4j
@Configuration
public class FeignClientErrorDecoder implements feign.codec.ErrorDecoder {
private static final Gson gson = new Gson();
@Override
public Exception decode( String methodKey, Response response )
{
if ( response.status() != HttpStatus.OK.value() )
{
if ( response.status() == HttpStatus.SERVICE_UNAVAILABLE.value() )
{
String errorContent;
try {
errorContent = Util.toString( response.body().asReader() );
InternalApiException internalApiException = gson.fromJson( errorContent, InternalApiException.class );
return(internalApiException);
} catch ( IOException e ) {
log.error( "handle error exception" );
return(new InternalApiException( 500, "unknown error" ) );
}
}
}
return(new InternalApiException( 500, "unknown error" ) );
}
}

我们只需要在服务调用方增加这样一个FeignClient解码器,就可以在解码器中完成错误消息的转换。这样,我们在通过FeignClient调用微服务时就可以直接捕获到异常对象,从而实现向本地一样处理远程服务返回的异常对象了。

以上就是在利用Spring Cloud进行微服务拆分后关于异常处理机制的一点分享了,如有更好的方式,也欢迎大家给我留言!

作者:若丨寒
链接:https://www.jianshu.com/p/9fb7684bbeca

使用Spring Cloud和Docker构建微服务架构

老马 发表了文章 • 0 个评论 • 242 次浏览 • 2019-05-28 22:27 • 来自相关话题

【编者的话】如何使用Spring Boot、Spring Cloud、Docker和Netflix的一些开源工具来构建一个微服务架构。本文通过使用Spring Boot、Spring Cloud和Docker构建的概念型应用示例,提供了了解常见的微服务架构模式 ...查看全部
【编者的话】如何使用Spring Boot、Spring Cloud、Docker和Netflix的一些开源工具来构建一个微服务架构。本文通过使用Spring Boot、Spring Cloud和Docker构建的概念型应用示例,提供了了解常见的微服务架构模式的起点。

该代码可以在GitHub上获得,并且在Docker Hub上提供了镜像。您只需要一个命令即可启动整个系统。

我选择了一个老项目作为这个系统的基础,它的后端以前是单一应用。此应用提供了处理个人财务、整理收入开销、管理储蓄、分析统计和创建简单预测等功能。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#功能服务
整个应用分解为三个核心微服务。它们都是可以独立部署的应用,围绕着某些业务功能进行组织。
1.png


账户服务

包含一般用户输入逻辑和验证:收入/开销记录、储蓄和账户设置。
2.png


统计服务

计算主要的统计参数,并捕获每一个账户的时间序列。数据点包含基于货币和时间段正常化后的值。该数据可用于跟踪账户生命周期中的现金流量动态。
3.png


通知服务

存储用户的联系信息和通知设置(如提醒和备份频率)。安排工作人员从其它服务收集所需的信息并向订阅的客户发送电子邮件。
4.png


注意

* 每一个微服务拥有自己的数据库,因此没有办法绕过API直接访问持久数据。
* 在这个项目中,我使用MongoDB作为每一个服务的主数据库。拥有一个多种类持久化架构(polyglot persistence architecture)也是很有意义的。
* 服务间(Service-to-service)通信是非常简单的:微服务仅使用同步的REST API进行通信。现实中的系统的常见做法是使用互动风格的组合。例如,执行同步的GET请求检索数据,并通过消息代理(broker)使用异步方法执行创建/更新操作,以便解除服务和缓冲消息之间的耦合。然而,这带给我们是最终的一致性

#基础设施服务
分布式系统中常见的模式,可以帮助我们描述核心服务是怎样工作的。Spring Cloud提供了强大的工具,可以增强Spring Boot应用的行为来实现这些模式。我会简要介绍一下:
5.png

##配置服务
Spring Cloud Config是分布式系统的水平扩展集中式配置服务。它使用了当前支持的本地存储、Git和Subversion等可拔插存储库层(repository layer)。

在此项目中,我使用了native profile,它简单地从本地classpath下加载配置文件。您可以在配置服务资源中查看shared目录。现在,当通知服务请求它的配置时,配置服务将响应回shared/notification-service.yml和shared/application.yml(所有客户端应用之间共享)。

客户端使用

只需要使用sprng-cloud-starter-config依赖构建Spring Boot应用,自动配置将会完成其它工作。

现在您的应用中不需要任何嵌入的properties,只需要提供有应用名称和配置服务url的bootstrap.yml即可:
spring:
application:
name: notification-service
cloud:
config:
uri: http://config:8888
fail-fast: true

使用Spring Cloud Config,您可以动态更改应用配置

比如,EmailService bean使用了@RefreshScope注解。这意味着您可以更改电子邮件的内容和主题,而无需重新构建和重启通知服务应用。

首先,在配置服务器中更改必要的属性。然后,对通知服务执行刷新请求:curl -H "Authorization: Bearer #token#" -XPOST http://127.0.0.1:8000/notifications/refresh。

您也可以使用webhook来自动执行此过程

注意

* 动态刷新存在一些限制。@RefreshScope不能和@Configuraion类一同工作,并且不会作用于@Scheduled方法。
* fail-fast属性意味着如果Spring Boot应用无法连接到配置服务,将会立即启动失败。当您一起启动所有应用时,这非常有用。
* 下面有重要的安全提示

##授权服务
负责授权的部分被完全提取到单独的服务器,它为后端资源服务提供OAuth2令牌。授权服务器用于用户授权以及在周边内进行安全的机器间通信。

在此项目中,我使用密码凭据作为用户授权的授权类型(因为它仅由本地应用UI使用)和客户端凭据作为微服务授权的授权类型。

Spring Cloud Security提供了方便的注解和自动配置,使其在服务器端或者客户端都可以很容易地实现。您可以在文档中了解到更多信息,并在授权服务器代码中检查配置明细。

从客户端来看,一切都与传统的基于会话的授权完全相同。您可以从请求中检索Principal对象、检查用户角色和其它基于表达式访问控制和@PreAuthorize注解的内容。

PiggyMetrics(帐户服务、统计服务、通知服务和浏览器)中的每一个客户端都有一个范围:用于后台服务的服务器、用于浏览器展示的UI。所以我们也可以保护控制器避免受到外部访问,例如:
@PreAuthorize("#oauth2.hasScope('server')")
@RequestMapping(value = "accounts/{name}", method = RequestMethod.GET)
public List getStatisticsByAccountName(@PathVariable String name) {
return statisticsService.findByAccountName(name);
}

##API网关
您可以看到,有三个核心服务。它们向客户端暴露外部API。在现实系统中,这个数量可以非常快速地增长,同时整个系统将变得非常复杂。实际上,一个复杂页面的渲染可能涉及到数百个服务。

理论上,客户端可以直接向每个微服务直接发送请求。但是这种方式是存在挑战和限制的,如果需要知道所有端点的地址,分别对每一段信息执行http请求,将结果合并到客户端。另一个问题是,这不是web友好协议,可能只在后端使用。

通常一个更好的方法是使用API网关。它是系统的单个入口点,用于通过将请求路由到适当的后端服务或者通过调用多个后端服务并聚合结果来处理请求。此外,它还可以用于认证、insights、压力测试、金丝雀测试(canary testing)、服务迁移、静态响应处理和主动变换管理。

Netflix开源这样的边缘服务,现在用Spring Cloud,我们可以用一个@EnabledZuulProxy注解来启用它。在这个项目中,我使用Zuul存储静态内容(UI应用),并将请求路由到适当的微服务。以下是一个简单的基于前缀(prefix-based)路由的通知服务配置:
zuul:
routes:
notification-service:
path: /notifications/**
serviceId: notification-service
stripPrefix: false

这意味着所有以/notification开头的请求将被路由到通知服务。您可以看到,里面没有硬编码的地址。Zuul使用服务发现机制来定位通知服务实例以及断路器和负载均衡器,如下所述。
##服务发现
另一种常见的架构模式是服务发现。它允许自动检测服务实例的网络位置,由于自动扩展、故障和升级,它可能会动态分配地址。

服务发现的关键部分是注册。我使用Netflix Eureka进行这个项目,当客户端需要负责确定可以用的服务实例(使用注册服务器)的位置和跨平台的负载均衡请求时,Eureka就是客户端发现模式的一个很好的例子。

使用Spring Boot,您可以使用spring-cloud-starter-eureka-server依赖、@EnabledEurekaServer注解和简单的配置属性轻松构建Eureka注册中心(Eureka Registry)。

使用@EnabledDiscoveryClient注解和带有应用名称的bootstrap.yml来启用客户端支持:
spring:
application:
name: notification-service

现在,在应用启动时,它将向Eureka服务器注册并提供元数据,如主机和端口、健康指示器URL、主页等。Eureka接收来自从属于某服务的每个实例的心跳消息。如果心跳失败超过配置的时间表,该实例将从注册表中删除。

此外,Eureka还提供了一个简单的界面,您可以通过它来跟踪运行中的服务和可用实例的数量:http://localhost:8761
6.png

##负载均衡器、断路器和Http客户端
Netflix OSS提供了另一套很棒的工具。

Ribbon

Ribbon是一个客户端负载均衡器,可以很好地控制HTTP和TCP客户端的行为。与传统的负载均衡器相比,每次线上调用都不需要额外的跳跃——您可以直接联系所需的服务。

它与Spring Cloud和服务发现是集成在一起的,可开箱即用。Eureka客户端提供了可用服务器的动态列表,因此Ribbon可以在它们之间进行平衡。

Hystrix

Hystrix是断路器模式的一种实现,它可以通过网络访问依赖来控制延迟和故障。中心思想是在具有大量微服务的分布式环境中停止级联故障。这有助于快速失败并尽快恢复——自我修复在容错系统中是非常重要的。

除了断路器控制,在使用Hystrix,您可以添加一个备用方法,在主命令失败的情况下,该方法将被调用以获取默认值。

此外,Hystrix生成每个命令的执行结果和延迟的度量,我们可以用它来监视系统的行为

Feign

Feign是一个声明式HTTP客户端,能与Ribbon和Hystrix无缝集成。实际上,通过一个spring-cloud-starter-feign依赖和@EnabledFeignClients注解,您可以使用一整套负载均衡器、断路器和HTTP客户端,并附带一个合理的的默认配置。

以下是账户服务的示例:
@FeignClient(name = "statistics-service")
public interface StatisticsServiceClient {
@RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
void updateStatistics(@PathVariable("accountName") String accountName, Account account);
}


* 您需要的只是一个接口
* 您可以在Spring MVC控制器和Feign方法之间共享@RequestMapping部分
* 以上示例仅指定所需要的服务ID——statistics-service,这得益于Eureka的自动发现(但显然您可以使用特定的URL访问任何资源)。

##监控仪表盘
在这个项目配置中,Hystrix的每一个微服务都通过Spring Cloud Bus(通过AMQP broker)将指标推送到Turbine。监控项目只是一个使用了TurbineHystrix仪表盘的小型Spring Boot应用。

让我们看看系统行为在负载下:账户服务调用统计服务和它在一个变化的模拟延迟下的响应。响应超时阈值设置为1秒。
7.png

##日志分析
集中式日志记录在尝试查找分布式环境中的问题时非常有用。Elasticsearch、Logstash和Kibana技术栈可让您轻松搜索和分析您的日志、利用率和网络活动数据。在我的另一个项目中已经有现成的Docker配置。
##安全

高级安全配置已经超过了此概念性项目的范围。为了更真实地模拟真实系统,请考虑使用https和JCE密钥库来加密微服务密码和配置服务器的properties内容(有关详细信息,请参阅文档)。
#基础设施自动化

部署微服务比部署单一的应用的流程要复杂得多,因为它们相互依赖。拥有完全基础设置自动化是非常重要的。我们可以通过持续交付的方式获得以下好处:

* 随时发布软件的能力。
* 任何构建都可能最终成为一个发行版本。
* 构建工件(artifact)一次,根据需要进行部署。

这是一个简单的持续交付工作流程,在这个项目的实现:

在此配置中,Travis CI为每一个成功的Git推送创建了标记镜像。因此,每一个微服务在Docker Hub上的都会有一个latest镜像,而较旧的镜像则使用Git提交的哈希进行标记。如果有需要,可以轻松部署任何一个,并快速回滚。
8.png

#如何运行全部?
这真的很简单,我建议您尝试一下。请记住,您将要启动8个Spring Boot应用、4个MongoDB实例和RabbitMq。确保您的机器上有4GB的内存。您可以随时通过网关、注册中心、配置、认证服务和账户中心运行重要的服务。

运行之前

* 安装Docker和Docker Compose。
* 配置环境变量:CONFIG_SERVICE_PASSWORD, NOTIFICATION_SERVICE_PASSWORD, STATISTICS_SERVICE_PASSWORD, ACCOUNT_SERVICE_PASSWORD, MONGODB_PASSWORD

生产模式

在这种模式下,所有最新的镜像都将从Docker Hub上拉取。只需要复制docker-compose.yml并执行docker-compose up -d即可。

开发模式

如果您想自己构建镜像(例如,在代码中进行一些修改),您需要克隆所有仓库(repository)并使用Mavne构建工件(artifact)。然后,运行docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

docker-compose.dev.yml继承了docker-compose.yml,附带额外配置,可在本地构建镜像,并暴露所有容器端口以方便开发。

重要的端点(Endpoint)

* localhost:80 —— 网关
* localhost:8761 —— Eureka仪表盘
* localhost:9000 —— Hystrix仪表盘
* localhost:8989 —— Turbine stream(Hystrix仪表盘来源)
* localhost:15672 —— RabbitMq管理

注意

所有Spring Boot应用都需要运行配置服务器才能启动。得益于Spring Boot的fail-fast属性和docker-compsoe的restart:always选项,我们可以同时启动所有容器。这意味着所有依赖的容器将尝试重新启动,直到配置服务器启动运行为止。

此外,服务发现机制在所有应用启动后需要一段时间。在实例、Eureka服务器和客户端在其本地缓存中都具有相同的元数据之前,任何服务都不可用于客户端发现,因此可能需要3次心跳。默认的心跳周期为30秒。

原文链接:Microservice Architectures With Spring Cloud and Docker(翻译:Oopsguy

微服务网关实战——Spring Cloud Gateway

博云BoCloud 发表了文章 • 0 个评论 • 247 次浏览 • 2019-05-24 17:29 • 来自相关话题

作为Netflix Zuul的替代者,Spring Cloud Gateway是一款非常实用的微服务网关,在Spring Cloud微服务架构体系中发挥非常大的作用。本文对Spring Cloud Gateway常见使用场景进行了梳理,希望对微服务开发人员提供 ...查看全部
作为Netflix Zuul的替代者,Spring Cloud Gateway是一款非常实用的微服务网关,在Spring Cloud微服务架构体系中发挥非常大的作用。本文对Spring Cloud Gateway常见使用场景进行了梳理,希望对微服务开发人员提供一些帮助。




微服务网关SpringCloudGateway



1.概述

Spring cloud gateway是spring官方基于Spring 5.0、Spring Boot2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等。



2.核心概念

网关提供API全托管服务,丰富的API管理功能,辅助企业管理大规模的API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等贡呢。一般来说网关对外暴露的URL或者接口信息,我们统称为路由信息。如果研发过网关中间件或者使用过Zuul的人,会知道网关的核心是Filter以及Filter Chain(Filter责任链)。Sprig Cloud Gateway也具有路由和Filter的概念。下面介绍一下Spring Cloud Gateway中几个重要的概念。



路由。路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配

断言。Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于http request中的任何信息,比如请求头和参数等。

过滤器。一个标准的Spring webFilter。Spring cloud gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理




图片1.png





如上图所示,Spring cloudGateway发出请求。然后再由Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway web handler。Handler再通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。



快速入门


以Spring Boot框架开发为例,启动一个Gateway服务模块(以Consul作为注册中心),一个后端服务模块。client端请求经gateway服务把请求路由到后端服务。



前提条件:

  • Consul:版本1.5.0。
  • Spring bot:版本2.1.5。
  • Spring cloud:版本Greenwich.SR1。
  • Redis:版本5.0.5。
1.微服务开发这里以使用Spring Boot框架开发微服务为例,启动一个服务并注册到Consul。引入依赖:
    org.springframework.cloud    spring-cloud-starter-consul-discovery
注册服务到Consul,配置文件配置如下:
spring:  application:    name: service-consumer  cloud:    consul:      host: 127.0.0.1      port: 8500      discovery:        service-name: service-consumer
如下定义RestController,发布HTTP接口。
@RestController@RequestMapping("/user")public class UserController {    @Resource    private UserService userService;    @GetMapping(value = "/info")    public User info() {        return userService.info();    }
}

注:此为服务端配置,经Gateway把请求路由转发到该服务上。

2.网关配置创建一个Gateway服务,引入以下依赖:
    org.springframework.cloud    spring-cloud-starter-gateway    org.springframework.cloud    spring-cloud-starter-consul-discovery
启动类配置如下:
@SpringBootApplication@EnableDiscoveryClientpublic class GatewayApplication {    public static void main(String[] args) {        SpringApplication.run(GatewayApplication.class, args);    }
}Spring Cloud Gateway对client端请求起到路由功能,主要配置如下:
server:  port: 8098spring:  application:    name: service-gateway  cloud:    gateway:      discovery:        locator:          enabled: true             lower-case-service-id: true      consul:      host: 127.0.0.1 #注册gateway网关到consul      port: 8500      discovery:        service-name: service-gateway
此时使用http://localhost:8089/service-consumer/user/info访问服务,网关即可对服务进行路由转发,把请求转发到具体后端服务上。此时,url中使用的url前缀service-consumer,是后端服务在Consul注册的服务名称转为小写字母以后的字符串。 最佳实践 01Gateway网关配置本文第二部分开发规范中定义了网关进行路由转发的配置,除了上述配置方式还可以使用下面的方式进行配置:
gateway:      discovery:        locator:          enabled: true          lower-case-service-id: true      routes:      - id: service_consumer        uri: lb://service-consumer        predicates:        - Path= /consumer/**        filters:        - StripPrefix=1
在上面的配置中,配置了一个Path的predicat,将以/consumer/**开头的请求都会转发到uri为lb://service-consumer的地址上,lb://service-consumer(注册中心中服务的名称)即service-consumer服务的负载均衡地址,并用StripPrefix的filter 在转发之前将/consumer去掉。同时将spring.cloud.gateway.discovery.locator.enabled改为false,如果不改的话,之前的http://localhost:8081/service-consumer/user/info这样的请求地址也能正常访问,因为这时为每个服务创建了2个router。本文第二部分和本节一共讲述了两种配置方式,两种配置都可以实现请求路由转发的功能。参数spring.cloud.gateway.discovery.locator.enabled为true,表明Gateway开启服务注册和发现的功能,并且Spring Cloud Gateway自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务。spring.cloud.gateway.discovery.locator.lowerCaseServiceId是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了)。
gateway:      discovery:        locator:          enabled: true          lower-case-service-id: true
02Gateway跨域访问Spring Cloud Gateway还针对跨域访问做了设计,可以使用以下配置解决跨域访问问题:
spring:  cloud:    gateway:      globalcors:        corsConfigurations:          '[/**]':            allowedOrigins: "https://docs.spring.io"            allowedMethods:            - GET            allowHeaders:            - Content-Type
在上面的示例中,允许来自https://docs.spring.io的get请求进行访问,并且表明服务器允许请求头中携带字段Content-Type。03Gateway 过滤器Spring Cloud Gateway的filter生命周期不像Zuul那么丰富,它只有两个:“pre”和“post”:pre:这种过滤器在请求被路由之前调用。可以利用这个过滤器实现身份验证、在集群中选择请求的微服务、记录调试的信息。post:这种过滤器在路由到服务器之后执行。这种过滤器可用来为响应添加HTTP Header、统计信息和指标、响应从微服务发送给客户端等。Spring Cloud gateway的filter分为两种:GatewayFilter和Globalfilter。GlobalFilter会应用到所有的路由上,而Gatewayfilter将应用到单个路由或者一个分组的路由上。利用Gatewayfilter可以修改请求的http的请求或者是响应,或者根据请求或者响应做一些特殊的限制。更多时候可以利用Gatewayfilter做一些具体的路由配置。下面的配置是AddRequestParameter Gatewayfilter的相关配置。
spring:  application:    name: service-gateway  cloud:    gateway:     discovery:        locator:         enabled: true     routes:     - id: parameter_route      uri: http://localhost:8504/user/info      filters:      - AddRequestParameter=foo, bar      predicates:      - Method=GET
上述配置中指定了转发的地址,设置所有的GET方法都会自动添加foo=bar,当请求符合上述路由条件时,即可在后端服务上接收到Gateway网关添加的参数。另外再介绍一种比较常用的filter,即StripPrefix gateway filter。配置如下:
spring:  cloud:    gateway:      routes:      - id: stripprefixfilter        uri: lb://service-consumer        predicates:        - Path=/consumer/**        filters:        - StripPrefix=1
当client端使用http://localhost:8098/consumer/user/info路径进行请求时,如果根据上述进行配置Gateway会将请求转换为http://localhost:8098/service-consumer/user/info。以此作为前端请求的最终目的地。04Gateway请求匹配Gateway网关可以根据不同的方式进行匹配进而把请求分发到不同的后端服务上。通过header进行匹配,把请求分发到不同的服务上,配置如下:
spring:  cloud:    gateway:      routes:      - id: header_route        uri: http://baidu.com        predicates:        - Header=X-Request-Id, \d+
通过curl测试:curl http://localhost:8080 -H "X-Request-Id:666666",返回页面代码证明匹配成功。如果是以Host进行匹配,配置如下:
spring:  cloud:    gateway:      routes:      - id: host_route        uri: http://baidu.com        predicates:        - Host=**.baidu.com
通过curl http://localhost:8098 -H "Host: www.baidu.com"进行测试,返回页面代码即转发成功。可以通过POST、GET、PUT、DELTE等不同的方式进行路由:
spring:  cloud:    gateway:      routes:      - id: method_route        uri: http://baidu.com        predicates:        - Method=GET
通过 curl http://localhost:8098 进行测试,返回页面代码即表示成功。上述是单个匹配进行路由,如果把多个匹配合在一起进行路由,必须满足所有的路有条件才会进行路由转发。05Gateway熔断Spring Cloud Gateway也可以利用Hystrix的熔断特性,在流量过大时进行服务降级,同时项目中必须加上Hystrix的依赖。
  org.springframework.cloud  spring-cloud-starter-netflix-hystrix    
配置后,Gateway将使用fallbackcmd作为名称生成HystrixCommand对象进行熔断处理。如果想添加熔断后的回调内容,需要添加以下配置:
spring:  cloud:    gateway:      routes:      - id: hystrix_route        uri: lb://consumer-service        predicates:        - Path=/consumer/**        filters:        - name: Hystrix          args:            name: fallbackcmd            fallbackUri: forward:/fallback        - StripPrefix=1hystrix:    command:    fallbackcmd:      execution:        isolation:          thread:            timeoutInMilliseconds: 5000 #超时时间,若不设置超时时间则有可能无法触发熔断
上述配置中给出了熔断之后返回路径,因此,在Gateway服务模块添加/fallback路径,以作为服务熔断时的返回路径。
@RestControllerpublic class GatewayController {    @RequestMapping(value = "/fallback")    public String fallback(){        return "fallback nothing";    }
}fallbackUri: forward:/fallback配置了 fallback 时要会调的路径,当调用 Hystrix 的 fallback 被调用时,请求将转发到/fallback这个 URI,并以此路径的返回值作为返回结果。06Gateway重试路由器通过简单的配置,Spring Cloud Gateway就可以支持请求重试功能。
spring:  cloud:    gateway:      routes:      - id: header_route        uri: http://localhost:8504/user/info        predicates:        - Path=/user/**        filters:        - name: Retry          args:            retries: 3            status: 503        - StripPrefix=1
Retry GatewayFilter通过四个参数来控制重试机制,参数说明如下:
  • retries:重试次数,默认值是 3 次。
  • statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus。
  • methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod。
  • series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5个值。
使用上述配置进行测试,当后台服务不可用时,会在控制台看到请求三次的日志,证明此配置有效。07Gateway 限流操作Spring Cloud Gateway本身集成了限流操作,Gateway限流需要使用Redis,pom文件中添加Redis依赖:
    org.springframework.boot    spring-boot-starter-data-redis-reactive
配置文件中配置如下:
spring:  cloud:    gateway:      routes:      - id: rate_limit_route        uri: lb://service-consumer        predicates:        - Path=/user/**        filters:        - name: RequestRateLimiter          args:            key-resolver: "#{@hostAddrKeyResolver}"            redis-rate-limiter.replenishRate: 1            redis-rate-limiter.burstCapacity: 3        - StripPrefix=1    consul:      host: 127.0.0.1      port: 8500      discovery:        service-name: service-gateway        instance-id: service-gateway-233  redis:    host: localhost    port: 6379
在上面的配置问价中,配置了Redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
  • BurstCapacity:令牌桶的总容量。
  • replenishRate:令牌通每秒填充平均速率。
  • Key-resolver:用于限流的解析器的Bean对象的名字。它使用SpEL表达式#{@beanName}从Spring容器中获取bean对象。

注意:filter下的name必须是RequestRateLimiter。





Key-resolver参数后面的bean需要自己实现,然后注入到Spring容器中。KeyResolver需要实现resolve方法,比如根据ip进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。还可以根据uri限流,同hostname限流是一样的。例如以ip限流为例,在gateway模块中添加以下实现:



public class HostAddrKeyResolver implements KeyResolver {

@Override
public Mono resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}

public HostAddrKeyResolver hostAddrKeyResolver() {
return new HostAddrKeyResolver();
}
}


把该类注入到spring容器中:



@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}

@Bean
public HostAddrKeyResolver hostAddrKeyResolver(){
return new HostAddrKeyResolver();
}
}



基于上述配置,可以对请求基于ip的访问进行限流。



08

自定义Gatewayfilter



Spring Cloud Gateway内置了过滤器,能够满足很多场景的需求。当然,也可以自定义过滤器。在Spring Cloud Gateway自定义过滤器,过滤器需要实现GatewayFilter和Ordered这两个接口。

下面的例子实现了Gatewayfilter,它可以以log日志的形式记录每次请求耗费的时间,具体实现如下:



public class RequestTimeFilter implements GatewayFilter, Ordered {
private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
if (startTime != null) {
log.info("请求路径:"+exchange.getRequest().getURI().getRawPath() + "消耗时间: " + (System.currentTimeMillis() - startTime) + "ms");
}
})
);
}
@Override
public int getOrder() {
return 0;
}
}



上述代码中定义了自己实现的过滤器。Ordered的int getOrder()方法是来给过滤器定优先级的,值越大优先级越低。还有一个filter(ServerWebExchange exchange, GatewayFilterChain chain)方法,在该方法中,先记录了请求的开始时间,并保存在ServerWebExchange中,此处是一个“pre”类型的过滤器。然后再chain.filter()的内部类中的run()方法中相当于"post"过滤器,在此处打印了请求所消耗的时间。



接下来将该过滤器注册到router中,代码如下。



 @Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/user/**")
.filters(f -> f.filter(new RequestTimeFilter())
.addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
.uri("http://localhost:8504/user/info")
.order(0)
.id("customer_filter_router")
)
.build();
}



除了上述代码的方式配置我们自定义的过滤器的方式之外,也可以在application.yml文件中直接配置,这里不再赘述。



启动程序,通过curl http://localhost:8098/user/info控制台会打印出请求消耗时间,日志如下:



....
2019-05-22 15:13:31.221 INFO 19780 --- [ctor-http-nio-4] o.s.cloud.gateway.filter.GatewayFilter : 请求路径:/user/info消耗时间: 54ms
...
2019-05-22 16:46:23.785 INFO 29928 --- [ctor-http-nio-1] o.s.cloud.gateway.filter.GatewayFilter : 请求路径:/user/info3消耗时间: 5ms
....




09

自定义GlobalFilter


Spring Cloud Gateway根据作用范围分为GatewayFilter和GlobalFilter,二者区别如下:

GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上。

GlobalFilter:全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。





在上一小节中定义的是Gatewayfilter,下面实现的是Globalfilter:



public class TokenFilter implements GlobalFilter, Ordered {
Logger logger= LoggerFactory.getLogger( TokenFilter.class );
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (token == null || token.isEmpty()) {
logger.info( "token 为空,无法进行访问." );
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return 0;
}
}



上述代码实现了Globalfilter,具体逻辑是判断请求中是否含参数token,如果没有,则校验不通过,对所有请求都有效。如果含有token则转发到具体后端服务上,如果没有则校验不通过。



通过curl http://localhost:8098/user/info进行访问,因为路径中不含有参数token,则无法通过校验,打印日志如下:



2019-05-22 15:27:11.078  INFO 5956 --- [ctor-http-nio-1] com.song.gateway.TokenFilter             : token 为空,无法进行访问.
...




通过curl http://localhost:8098/user/info?token=123进行访问时,则可以获取到后端服务返回结果。



服务迁移之路 | Spring Cloud向Service Mesh转变

博云BoCloud 发表了文章 • 0 个评论 • 298 次浏览 • 2019-05-20 17:39 • 来自相关话题

导读 Spring Cloud基于Spring Boot开发,提供一套完整的微服务解决方案,具体包括服务注册与发现,配置中心,全链路监控,API网关,熔断器,远程调用框架,工具客户端等选项中立的开源组件,并且可以根据需求对部分组件进行 ...查看全部
导读

Spring Cloud基于Spring Boot开发,提供一套完整的微服务解决方案,具体包括服务注册与发现,配置中心,全链路监控,API网关,熔断器,远程调用框架,工具客户端等选项中立的开源组件,并且可以根据需求对部分组件进行扩展和替换。

Service Mesh,这里以Istio(目前Service Mesh具体落地实现的一种,且呼声最高)为例简要说明其功能。 Istio 有助于降低这些部署的复杂性,并减轻开发团队的压力。它是一个完全开源的服务网格,可以透明地分层到现有的分布式应用程序上。它也是一个平台,包括允许它集成到任何日志记录平台、遥测或策略系统的 API。Istio的多样化功能集使你能够成功高效地运行分布式微服务架构,并提供保护、连接和监控微服务的统一方法。如果你想和更多 Service Mesh 技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

从上面的简单介绍中,我们可以看出为什么会存在要把Spring Cloud体系的应用迁移到Service Mesh这样的需求,总结下来,有四方面的原因:

1. 功能重叠

来简单看一下他们的功能对比:



微信截图_20190520171338.png




从上面表格中可以看到,如果从功能层面考虑,Spring Cloud与Service Mesh在服务治理场景下,有相当大量的重叠功能,从这个层面而言,为Spring Cloud向Service Mesh迁移提供了一种潜在的可能性。



2. 服务容器化

在行业当前环境下,还有一个趋势,或者说是现状。越来越多的应用走在了通往应用容器化的道路上,或者在未来,容器化会成为应用部署的标准形态。而且无论哪种容器化运行环境,都天然支撑服务注册发现这一基本要求,这就导致Spring Cloud体系应用上容器的过程中,存在一定的功能重叠,有可能为后期的应用运维带来一定的影响,而Service Mesh恰恰需要依赖容器运行环境,同时弥补了容器环境所欠缺的内容(后续会具体分析)。



3. 术业有专攻

从软件设计角度出发,我们一直在追求松耦合的架构,也希望做到领域专攻。例如业务开发人员希望我只要关心业务逻辑即可,不需要关心链路跟踪,熔断,服务注册发现等支撑工具的服务;而平台支撑开发人员,则希望我的代码中不要包含任何业务相关的内容。而Service Mesh的出现,让这种情况成为可能。



4. 语言壁垒

目前而言Spring Cloud虽然提供了对众多协议的支持,但是受限于Java技术体系。这就要求应用需要在同一种语言下进行开发(这不一定是坏事儿),在某种情况下,不一定适用于一些工作场景。而从微服务设计考虑,不应该受限于某种语言,各个服务应该能够相互独立,大家需要的是遵循通信规范即可。而Service Mesh恰好可以消除服务间的语言壁垒,同时实现服务治理的能力。


基于以上四点原因,当下环境,除了部分大多已经提前走在了Service Mesh实践的道路上互联网大厂以外(例如蚂蚁金服的SOFASTACK),也有大部分企业已经开始接触Service Mesh,并且尝试把Spring Cloud构建的应用,迁移到Service Mesh中。



#Spring Cloud向Service Mesh的迁移方案



Spring Cloud向Service Mesh迁移,从我们考虑而言大体分为七个步骤,如图所示:



图片1.png




1. Spring Cloud架构解析

Spring Cloud架构解析的目的在于确定需要从当前的服务中去除与Service Mesh重叠的功能,为后续服务替换做准备。我们来看一个典型的Spring Cloud架构体系,如图所示:



图片2.png






从图中我们可以简要的分析出,一个基于Spring Cloud的微服务架构,主要包括四部分内容:服务网关,应用服务,外围支撑组件,服务管理控制台。



  • 服务网关
服务网关涵盖的功能包括路由,鉴权,限流,熔断,降级等对入站请求的统一拦截处理。具体可以进一步划分为外部网关(面向互联网)和内部网关(面向服务内部管理)。
  • 应用服务
应用服务是企业业务核心。应用服务内部由三部分内容构成:业务逻辑实现,外部组件交互SDK集成,服务内部运行监控集成。
  • 外围支撑组件
外围支撑组件,涵盖了应用服务依赖的工具,包括注册中心,配置中心,消息中心,安全中心,日志中心等。
  • 服务管理控制台
服务管理控制台面向服务运维或者运营人员,实现对应用服务运行状态的实时监控,以及根据情况需要能够动态玩成在线服务的管理和配置。



这里面哪些内容是我们可以拿掉或者说基于Service Mesh(以Istio为例)能力去做的?分析下来,可以替换的组件包括网关(gateway或者Zuul,由Ingress gateway或者egress替换),熔断器(hystrix,由SideCar替换),注册中心(Eureka及Eureka client,由Polit,SideCar替换),负责均衡(Ribbon,由SideCar替换),链路跟踪及其客户端(Pinpoint及Pinpoint client,由SideCar及Mixer替换)。这是我们在Spring Cloud解析中需要完成的目标:即确定需要删除或者替换的支撑模块。



2. 服务改造

服务单元改造的目的在于基于第一步的解析结果,完成依赖去除或者依赖替换。根据第一步的分析结果服务单元改造分为三步:

· 删除组件,包括网关,熔断器,注册中心,负载均衡,链路跟踪组件,同时删除对应client的SDK;
· 替换组件,采用httpClient的SDK支持http协议的远程调用(原来在Ribbon中),由原来基于注册中心的调用,转变成http直接调用;
· 配置信息变更,修改与删除组件管理的配置信息以及必要的组件交互代码(根据实际应用情况操作);

当然服务单元改造过程中,还会涉及到很多的细节问题,都需要根据应用特点进行处理,这里不做深入分析。



3. 服务容器化

服务容器化是目前应用部署的趋势所在。服务容器化本身有很多不同的方式,例如基于Jenkins的pipeline实现,基于docker-maven-plugin + dockerfile实现,当然还有很多不同的方式。这里以Spring Cloud一个demo服务通过docker-maven-plugin+dockerfile实现说明为例:



简易的一个服务的Dockerfile如下所示:


ROM openjdk:8-jre-alpine
ENV TZ=Asia/Shanghai \
SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
JAVA_OPTS="" \
JHIPSTER_SLEEP=0
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
CMD echo "The application will start in ${JHIPSTER_SLEEP}s..." && \
sleep ${JHIPSTER_SLEEP} && \
java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /app.jar
# java ${JAVA_OPTS} -Djava.security.egd=environment:/dev/./urandom -jar /app.@project.packaging@

EXPOSE 8080
ADD microservice-demo.jar /app.jar



文件中定义了服务端口以及运行命令。


Maven-docker-plugin的插件配置如下所示:



microservice-demo

......

com.spotify
docker-maven-plugin
1.2.0


build-image
package

build



tag-image
package

tag


${project.build.finalName}:${project.version}
${docker.registry.name}/${project.build.finalName}:${project.version}





${project.basedir}/src/main/docker
${project.build.finalName}:${project.version}


/
${project.build.directory}
${project.build.finalName}.${project.packaging}








通过增加docker-maven-plugin,在执行mvn package的时候可以加载Dockerfile,自动构建服务的容器镜像(需要说明的前提是本地安装docker运行环境,或者通过环境变量在开发工具中配置Docker的远程连接环境),从而完成服务容器化改造。



4. 容器环境构建

容器环境决定这Service Mesh的部署形态,这里不详细描述容器环境的部署过程。感兴趣的朋友,可以参考https://github.com/easzlab/kubeasz 开源项目,提供了Kubernetes基于ansible的自动化部署脚本。我们也建议选择Kubernetes来构建容器环境。这里说明容器环境构建的考虑因素:


· 集群部署方案
集群部署方案主要考虑多集群,跨数据中心,存储选择,网络方案,集群内部主机标签划分,集群内部网络地址规划等多方面因素。

· 集群规模
集群规模主要考虑etcd集群大小,集群内运行实例规模(用来配置ip范围段),集群高可用节点规模等因素。


基于以上两点来考虑容器化环境的部署方案,关键是合理规划,避免资源浪费。



5. Service Mesh环境构建

Service Mesh环境构建依赖于容器环境构建,主要考虑两个方面,以Isito为例:


· 部署插件
Istio部署插件需要根据需要的场景,考虑采用的插件完整性,例如prometheus,kiali,是否开启TLS等,具体安装选项可以参考https://preliminary.istio.io/zh/docs/reference/config/installation-options/。

· 跨集群部署
依据容器环境考虑是否需要支持Isito的跨集群部署方案.



6. 服务注入

服务注入用于将容器化的服务接入到Service Mesh的平台中,目前主要有两种方式。以Isito为例说明,主要包括自动注入和手动入住。选择手动注入的目的在于可以根据企业内部上线流程,对服务接入进行人为控制。而自动注入则能够更加快捷,方便。到此实际上已经完成服务迁移工作。



7. 服务管理控制台

由于Service Mesh目前而言,多是基于声明式的配置文件,达到服务治理的效果,因此无法实时传递执行结果。基于这种原因,需要一个独立的Service Mesh的管理控制台,一方面能够查看各个服务的运行状态以及策略执行情况,另外一方面能够支持服务运行过程中策略的动态配置管理。目前而言,可以在Isito安装过程中选择kiali作为一个控制台实现,当然未来也会有大量的企业提供专门的服务。


通过以上七个步骤,能够在一定程度上帮助企业应用,从Spring Cloud迁移到Service Mesh上,但迁移过程中必然存在不断踩坑的过程,需要根据应用特点,事前做好评估规划。



#迁移优缺点分析

Spring Cloud迁移到Service Mesh是不是百利而无一害呢?

首先,从容器化的环境出发,后续Knative,Kubernetes,Service Mesh必然会构建出一套相对完整的容器化PaaS解决方案,从而完成容器化PaaS支撑平台的构建。Service Mesh将为容器运行态提供保驾护航的作用。

其次,就目前Service Mesh的落地实现而言,对于一些特定需求的监测粒度有所欠缺,例如调用线程栈的监测(当然,从网络层考虑,或者不在Service Mesh的考虑范围之内),但是恰恰在很多服务治理场景的要求范围之中。我们也需要针对这种情况,考虑实现方案。

最后,大家一直诟病的性能和安全问题。目前已经有所加强,但是依然被吐槽。

整体而言,Spring Cloud是微服务实现服务治理平台的现状,而Service Mesh却是未来,当然也不能完全取而代之,毕竟设计思路和侧重点不同,是否迁移需要根据业务场景而定。


本文由博云研究院原创发表,转载请注明出处。

一张图了解Spring Cloud微服务架构

老马 发表了文章 • 0 个评论 • 600 次浏览 • 2019-04-26 20:48 • 来自相关话题

Spring Cloud作为当下主流的微服务框架,可以让我们更简单快捷地实现微服务架构。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置 ...查看全部
Spring Cloud作为当下主流的微服务框架,可以让我们更简单快捷地实现微服务架构。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。Spring Cloud中各个组件在微服务架构中扮演的角色如下图所示,黑线表示注释说明,蓝线由A指向B,表示B从A处获取服务。
6295401-8076ec880947ba79.png

Spring Cloud组成的微服务架构图

由上图所示微服务架构大致由上图的逻辑结构组成,其包括各种微服务、注册发现、服务网关、熔断器、统一配置、跟踪服务等。下面说说Spring Cloud中的组件分别充当其中的什么角色。

Fegin(接口调用):微服务之间通过Rest接口通讯,Spring Cloud提供Feign框架来支持Rest的调用,Feign使得不同进程的Rest接口调用得以用优雅的方式进行,这种优雅表现得就像同一个进程调用一样。

Netflix eureka(注册发现):微服务模式下,一个大的Web应用通常都被拆分为很多比较小的Web应用(服务),这个时候就需要有一个地方保存这些服务的相关信息,才能让各个小的应用彼此知道对方,这个时候就需要在注册中心进行注册。每个应用启动时向配置的注册中心注册自己的信息(IP地址,端口号, 服务名称等信息),注册中心将他们保存起来,服务间相互调用的时候,通过服务名称就可以到注册中心找到对应的服务信息,从而进行通讯。注册与发现服务为微服务之间的调用带来了方便,解决了硬编码的问题。服务间只通过对方的服务ID,而无需知道其IP和端口即可以获取对方方服务。

Ribbon(负载均衡):Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon,配置服务提供者的地址列表后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机等。当然,我们也可为Ribbon实现自定义的负载均衡算法。在Spring Cloud中,当Ribbon与Eureka配合使用时,Ribbon可自动从EurekaServer获取服务提供者的地址列表,并基于负载均衡算法,请求其中一个服务提供者的实例(为了服务的可靠性,一个微服务可能部署多个实例)。

Hystrix(熔断器):当服务提供者响应非常缓慢,那么消费者对提供者的请求就会被强制等待,直到提供者响应或超时。在高负载场景下,如果不做任何处理,此类问题可能会导致服务消费者的资源耗竭甚至整个系统的崩溃(雪崩效应)。Hystrix正是为了防止此类问题发生。Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。Hystrix主要通过以下几点实现延迟和容错。

* 包裹请求:使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用了设计模式中的“命令模式”。
* 跳闸机制:当某服务的错误率超过一定阈值时,Hystrix可以自动或者手动跳闸,停止请求该服务一段时间。
* 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。
* 监控:Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时和被拒绝的请求等。
* 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑可由开发人员指定。

Zuul(微服务网关):不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求。例如一个电影购票的手机APP,可能调用多个微服务的接口才能完成一次购票的业务流程,如果让客户端直接与各个微服务通信,会有以下的问题:

* 客户端会多次请求不同的微服务,增加了客户端的复杂性。
* 存在跨域请求,在一定场景下处理相对复杂。
* 认证复杂,每个服务都需要独立认证。
* 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将很难实施。
* 某些微服务可能使用了对防火墙/浏览器不友好的协议,直接访问时会有一定的困难。

以上问题可借助微服务网关解决。微服务网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过微服务网关。使用微服务网关后,微服务网关将封装应用程序的内部结构,客户端只用跟网关交互,而无须直接调用特定微服务的接口。这样,开发就可以得到简化。不仅如此,使用微服务网关还有以下优点:

* 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
* 易于认证。可在微服务网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
* 减少了客户端与各个微服务之间的交互次数。

Spring Cloud Bus( 统一配置服务):对于传统的单体应用,常使用配置文件管理所有配置。例如一个SpringBoot开发的单体应用,可将配置内容放在application.yml文件中。如果需要切换环境,可设置多个Profile,并在启动应用时指定spring.profiles.active={profile}。然而,在微服务架构中,微服务的配置管理一般有以下需求:

* 集中管理配置。一个使用微服务架构的应用系统可能会包含成百上千个微服务,因此集中管理配置是非常有必要的。
* 不同环境,不同配置。例如,数据源配置在不同的环境(开发、测试、预发布、生产等)中是不同的。
* 运行期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小或熔断阈值,并且在调整配置时不停止微服务。
* 配置修改后可自动更新。如配置内容发生变化,微服务能够自动更新配置。综上所述,对于微服务架构而言,一个通用的配置管理机制是必不可少的,常见做法是使用配置服务器管理配置。Spring Cloud Bus利用Git或SVN等管理配置、采用Kafka或者RabbitMQ等消息总线通知所有应用,从而实现配置的自动更新并且刷新所有微服务实例的配置。

Sleuth+ZipKin(跟踪服务):Sleuth和Zipkin结合使用可以通过图形化的界面查看微服务请求的延迟情况以及各个微服务的依赖情况。需要注意的是Spring Boot 2及以上不在支持Zipkin的自定义,需要到官方网站下载ZipKin相关的jar包。另外需要提一点的是Spring Boot Actuator,提供了很多监控端点如/actuator/info、/actuator/health、/acutator/refresh等,可以查看微服务的信息、健康状况、刷新配置等。

原文链接:一张图了解Spring Cloud微服务架构(作者:SimpleEasy)

从 Spring Cloud 看一个微服务框架的「五脏六腑」

齐达内 发表了文章 • 0 个评论 • 562 次浏览 • 2019-03-31 11:53 • 来自相关话题

Spring Cloud 是一个基于 Spring Boot 实现的微服务框架,它包含了实现微服务架构所需的各种组件。 注:Spring Boot 简单理解就是简化 Spring 项目的搭建、配置、组合的框架。因为与构建微服务本身没有 ...查看全部
Spring Cloud 是一个基于 Spring Boot 实现的微服务框架,它包含了实现微服务架构所需的各种组件。

注:Spring Boot 简单理解就是简化 Spring 项目的搭建、配置、组合的框架。因为与构建微服务本身没有直接关系,所以本文不对 Spring Boot 进行展开。另外本文有一些例子涉及到 Spring 和 Spring Boot,建议先了解一下 Spring 和 Spring Boot 再阅读本文。



本文的阅读对象主要是没有接触过服务架构,想对其有一个宏观的了解的同学。

本文将从 Spring Cloud 出发,分两小节讲述微服务框架的「五脏六腑」:

* 第一小节「服务架构」旨在说明的包括两点,一服务架构是什么及其必要性;二是服务架构的基本组成。为什么第一节写服务架构而不是微服务架构呢?原因主要是微服务架构本身与服务架构有着千丝万缕的关系,服务架构是微服务架构的根基。
* 第二小节「五脏六腑」则将结合 Spring Cloud 这个特例来介绍一个完整的微服务框架的组成。

#服务架构
为了方便理解,我先讲一个小故事(改编自一知乎答主):

Martin(微服务提出者也叫 Martin)刚来到公司时是一个基层员工,它上面有经理、老板,那个时候所有人都听老板的指挥。

但是过了两年,公司的人越来越多,原来的模式下整个公司的运作效率太低,管理也很混乱。

于是已经踏上中层岗位的 Martin 建议老板进行部门划分(服务化),专门的部门只做专门的事情(单一职责)。例如研发部门只做研发,人事部门只做招聘。

老板听取了 Martin 的意见,对公司的组织架构进行了调整。

有一天,Martin 发现公司的部门越来越多,各个部门并不能完全知道对方所做的事情,这对跨部门协作(服务调用)带来了困难。

行政部门会(注册中心)来记录所有的部门,每当有新的部门行政都会记录下来(服务注册),然后公布出来让所有部门知道(服务发现)。

在新的组织架构下,公司的效率逐步提高。老板也给 Martin 发了大量奖金作为奖励,Martin 从此赢取白富美走向了人生巅峰。

这是一个公司组织架构演变的故事,主要讲的是随着公司规模的扩大,组织从集中化管理到分布化管理的过程。

映射到我们的信息系统里来也是一样的,随着我们的系统越来越复杂,变得难以管理,也有人想到去拆分然后治理。在解决复杂问题上,分治可以说是一个屡试不爽的办法。

服务化即是拆解的一种手段。而上面圆括号里面的内容其实就对应了一个服务化架构的最小组成元素,分别是服务、服务调用、注册中心、服务注册、服务发现。有了这些基本的组成要素,就可以实现一个最简单的服务架构。
##面向服务的架构和微服务架构
面向服务的架构(SOA)和微服务架构是目前两种主流的服务化架构,都符合上面的例子,也有上面提到的所有组件。这两种服务架构有很多可以讲的,但是与本文的相关性不大,本文不做会过多展开,只简单介绍一下两者的区别。

准确地说微服务是去 ESB(企业服务总线)的 SOA。ESB 借鉴了计算机组成原理中的通信模型 —— 总线,所有需要和外部系统通信的系统,通过 ESB 进行标准化地转换从而消除协议、异构系统之间的差异,这样就可以利用现有的系统构建一个全新的松耦合的异构的分布式系统。微服务架构去掉 ESB,本质上是一种去中心化的思想。
#五脏六腑
##「心脏」
顺着上一节的思路,从最简单、最核心的问题出发,假设服务 A 要调用服务 B,会有什么问题?

* 服务在哪?(服务治理问题)
* 怎么调用?(服务调用问题)

这两个是最核心的问题,也是任何微服务框架首要解决的两个问题。

为了解决第一个问题 Spring Cloud 提供了 Eureka、ZooKeeper、Cloud Foundry、Consul 等服务治理框架的集成。它们的工作模式是将所有的微服务注册到一个 Server 上,然后通过心跳进行服务健康监测。这样服务 A 调用 B 时可以从注册中心拿到可用的服务 B 的地址、端口进行调用。

第二个服务调用有人可能认为就是一个简单的 HTTP 或者 RPC 调用,不是什么问题。但是在分布式的场景下,服务调用需要考虑的因素会更多。比如一个服务有多个实例,此时请求进来了交给谁处理,请求的负载怎么平衡到各个实例,都是比较棘手的问题。Spring Cloud 提供了两种服务调用的方式:一种是 Ribbon + restTemplate,另一种是 Feign。

其中 Ribbon 是基于 HTTP 和 TCP 客户端的负载均衡器,restTemplate 是 Spring 提供的 Restful 远程调用的模板,两者结合就可以达到远程调用的负载均衡。

而 Feign 是一个更加声明式的 HTTP 客户端,开发者可以像调用本地方法一样调用它,完全感觉不到是远程调用,结合 Ribbon 也可以做负载均衡。

既然两个问题都得到了解决,我们就用一个例子来进一步说明一下,例子包含了微服务中最基本的三个角色(注册中心、服务提供者、服务消费者):

注册中心

注解 @EnableEurekaServer 表示该 Spring Boot 应用是一个注册中心。
@EnableEurekaServer
@SpringBootApplication
public class EurekaserverApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaserverApplication.class, args);
}
}

eureka.client.registerWithEureka: false 和 fetchRegistry: false 来表明自己是一个 eureka server。
server:
port: 8080

eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/


service-hello 服务

注解 @EnableEurekaClient 表示他是一个 Eureka 客户端,它会在注册中心注册自己。

注解 @RestController 表示这是一个控制器,@RequestMapping("/hello") 表示匹配到请求 '/hello' 时会调用该方法进行响应。
@SpringBootApplication
@EnableEurekaClient
@RestController
public class ServiceHelloApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceHelloApplication.class, args);
}

@Value("${server.port}")
String port;
@RequestMapping("/hello")
public String home(@RequestParam String name) {
return "hello "+name+",i am from port:" +port;
}

}

注册中心的地址为 http://localhost:8080/eureka/,也就是上面我们定义的。服务名为 service-hello,将会被调用者使用。
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8080/eureka/
server:
port: 8081
spring:
application:
name: service-hello


服务消费者 service-ribbon

假设 service-ribbon 端口为 8082,当我们访问 http://localhost:8080/hello 时,HelloControler 接收到请求,并调用 HelloService 中的 helloService 方法,HelloService 中通过定义的 restTemplate 去调用 http://service-hello/hello。此处要注意的是 @LoadBalanced 注解,它表示启用负载均衡。
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceRibbonApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceRibbonApplication.class, args);
}

@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

}

@Service
public class HelloService {

@Autowired
RestTemplate restTemplate;

public String helloService(String name) {
return restTemplate.getForObject("http://service-hello/hello?name="+name,String.class);
}

}

@RestController
public class HelloControler {

@Autowired
HelloService helloService;

@RequestMapping(value = "/hello")
public String hello(@RequestParam String name){
return helloService.helloService(name);
}

}

至此其实一个微服务应用的雏形已经搭建出来了,服务治理、服务调用可以说是「五脏六腑」中的「心脏」。
##「心脏」的依托
接下来我们要进一步思考的是「五脏六腑」中其余的部分,因为少了它们人也是活不久的。下面通过一个问题或需求对应一个组件的方式进行介绍。

服务“雪崩”与断路器

由于网络等原因,服务并不能保证 100% 可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet 容器的线程资源会被消耗殆尽,导致服务瘫痪。

由于服务与服务之间存在依赖,故障会在调用链路上传播,导致整个微服务系统崩溃,这就是服务故障的“雪崩”效应。

为了解决这个问题,Spring Cloud 提供了对 Hystrix 断路器的集成,当服务调用失败的频次达到一定阈值,断路器将被开启,降级的策略可以开发者制定,一般是返回一个固定值。这样就能够避免连锁故障。

此外 Spring Cloud 还提供 Hystrix Dashboard 和 Hystrix Turbine,帮助我们进行监控和聚合监控。

服务暴露与路由网关

微服务中的服务很多,直接暴露给用户一是不安全,二是对用户不友好。因此在微服务和面向服务的架构中,通常会有一个路由网关的角色,来负责路由转发和过滤。对应到 Spring Cloud 中有 Zuul 和 Gateway 两个组件可用。

路由网关接收了所有的用户请求,有着很高的负载,因此它通常是一个集群。用户的请求会先经过一层负载均衡被发到路由网关。

服务配置与配置中心

在微服务应用中,服务数量巨多,而每个服务不同环境都有着不同的配置,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。需要注意的是此处的配置与注册中心注册的配置信息是两个概念,此处的配置是服务本身的一些配置信息,如下图:
1.png

Spring Cloud 提供了 Spring Cloud Config 组件,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程 Git 仓库中,帮助我们管理服务的配置信息。

信息同步与消息总线

前一个问题讲到了每个服务都有一些配置信息,那么配置信息更新了我们该怎么办,手动一个个去更新?当然不是,Spring Cloud 提供了 Spring Cloud Bus 组件,它通过轻量消息代理连接各个分布的节点。当配置信息更新的时候,我们只要更新一个节点的配置,这个更新就会被广播到这个分布式系统中。

问题定位与链路追踪

在微服务系统中,服务之间可以相互调用,因此我们一个请求可能会一条调用链,而整个系统会存在一张调用网,其中任意一个服务调用失败或网络超时都可能导致整个请求失败。因为调用关系的复杂,这给问题的定位造成了极大的困难,这也是必须提供服务链路追踪的原因。

Spring Cloud 为我们提供了 Spring Cloud Sleuth 组件,它能够跟进一个请求到底有哪些服务参与,参与的顺序是怎样的,从而达到每个请求的步骤清晰可见。借助服务链路追踪,我们可以快速定位问题。

至此,Spring Cloud 的所有基础组件都介绍完了。但是目前所有的组件介绍都是分散的,它们组合起来,完整的样子是什么样的?如下图:
2.jpg

偷懒偷了张图,图中漏掉了 Config Server 和链路追踪组件。但是结合上文的介绍,我们大致可以脑补出这两个东西在图中的位置。Config Server 是一个与所有服务相连的服务集群,链路追踪组件则集成在每个服务中。
#小结
服务治理为心脏,路由网关、消息中心、断路器、链路追踪、配置中心等为依托,构造了整个微服务框架的「五脏六腑」。当然,一个微服务系统远比本文所写的复杂得多,尤其是在不同的业务场景之下,因此想要更深入地了解它就需要我们不断地去实践。而作为前端,我了解这些内容一是为了更好地了解整个请求的流程,二是为了后续在 SOA 中接入 Node 子服务积累相关知识。

最后分享一句有趣的调侃 Spring 的话:在 Spring 中没有什么是一个注解解决不了的,如果有,那么就用两个注解。

原文链接:从 Spring Cloud 看一个微服务框架的「五脏六腑」

从技术演变的角度看互联网后台架构

Andy_Lee 发表了文章 • 0 个评论 • 1258 次浏览 • 2019-03-27 09:30 • 来自相关话题

本文是腾讯IEG内部做的一个面向后台开发新同学的课程,因为其他BG一些同学要求分享,所以发一下。 其实内容都是些常见开源组件的high level描述,比如Flask,Express框架,中间件的演化,Microservices的 ...查看全部
本文是腾讯IEG内部做的一个面向后台开发新同学的课程,因为其他BG一些同学要求分享,所以发一下。

其实内容都是些常见开源组件的high level描述,比如Flask,Express框架,中间件的演化,Microservices的概念,一些对NoSQL/Column based DB的概念介绍,Docker的一些简单概念等等。从单个概念来说,这只是一些科普。

但是为什么当时要开这门课呢?重点是我发现很多新入职的后台开发同学并不太清楚自己做的东西在现代互联网整体架构中处于一个什么样的角色,而在IEG内部则因为游戏开发和互联网开发的一些历史性差异,有些概念并不清晰。

拿中间件来说,很多Web application不用啥中间件一样可以跑很好,那么是不是都要上Redis?到底解决什么问题?中间件又存在什么问题?中台和中间件又是个什么关系?如果开个MQ就是中间件,微服务又是要做啥?

如果能从这十多年来互联网应用的整个Tech stack变化去看待backend architecture的一些改变,应该是一件有趣也有意思的事情。这是当时写这个PPT开课的初衷。

我不敢说我在这个PPT里面的一些私货概念就是对的,但是也算是个人这么多年的一些认知理解,抛砖引玉吧。

强调一点,这个PPT的初衷是希望从近十多年来不同时代不同热点下技术栈的变化来看看我们是如何从最早的PHP/ASP/JSP<=>MySQL这样的两层架构,一个阶段一个阶段演变到现在繁复的大数据、机器学习、消息驱动、微服务架构这样的体系,然后在针对其中比较重要的几个方面来给新入门后台开发的同学起个“提纲目录”的作用。如果要对每个方面都深入去谈,那肯定不是一两页PPT就能做到的事情。

下面我们开始。首先看第一页如下图:什么是System Design?什么是架构设计?为什么要谈架构设计?
1.jpg

之所以抛出这个问题,是因为平时常常听到两个互相矛盾的说法:一方面很多人爱说“架构师都是不干活夸夸其谈”,另一方面又有很多人苦恼限于日常业务需求开发,无法或者没有机会去从整体架构思考,不知道怎么成长为架构师。

上面PPT中很有趣的是第一句英文,翻译过来恰好可以反映了论坛上经常有人问的“如何学习架构”的问题:很多leader一来就是扔几本书(书名)给新同学,期望他们读完书就马上升级……这种一般都只会带来失望。

何为架构师?不写代码只画PPT?

不是的,架构师的基本职责是要在项目早期就能设计好基本的框架,这个框架能够确保团队成员顺利coding满足近期内业务需求的变化,又能为进一步的发展留出空间(所谓scalability),这即是所谓技术选型。如何确保选型正确?对于简单的应用,或者没有新意完全是实践过多次的相同方案,确实靠几页PPT足矣。但是对于新的领域新的复杂需求,这个需求未必都是业务需求,也包括根据团队自身特点(人员太多、太少、某些环节成员不熟悉需要剥离开)来进行新的设计,对现有技术重新分解组合,这时候就需要架构师自己编码实现原型并验证思路正确性。

要达到这样的目标难不难?难!但是现在不是2000年了,是2019年了,大量的框架(framework)、开源工具和各种best practice,其实都是在帮我们解决这件事情。而这些框架并不是凭空而来,而是在这十多年互联网的演化中因为要解决各种具体业务难点而一点一点积累进化而来。无论是从MySQL到MongoDB到Cassandra到Time Series DB,或者从Memcached到Redis,从Lucene到Solr到Elasticsearch,从离线批处理到Hadoop到Storm到Spark到Flink,技术不是突然出现的,总是站在前人的肩膀上不断演变的。而要能在浩如烟海的现代互联网技术栈中选择合适的来组装自己的方案,则需要对技术的来源和历史有一定的了解。否则就会出现一些新人张口ELK,闭口TensorFlow,然后一个简单的异步消息处理就会让他们张口结舌的现象。

20多年前的经典著作DesignPatterns中讲过学习设计模式的意义,放在这里非常经典:学习设计模式并不是要你学习一种新的技术或者编程语言,而是建立一种交流的共同语言和词汇,在方案设计时方便沟通,同时也帮助人们从更抽象的层次去分析问题本质,而不被一些实现的细枝末节所困扰。同时,当我们能把很多问题抽象出来之后,也能帮我们更深入更好地去了解现有系统-------这些意义,对于今天的后端系统设计来说,也仍然是正确的。

下图是我们要谈的几个主要方面。
2.jpg

上面的几个主题中,第一个后台架构的演化是自己从业十多年来,体会到的互联网技术架构的整体变迁。然后分成后台前端应用框架、Middleware和存储三大块谈一下,最后两节微服务和Docker则是给刚进入后台开发的同学做一些概念普及。其中个人觉得最有趣的,是第一部分后台架构的演化和第三部分的中间件,因为这两者是很好地反映了过去十多年互联网发展期间技术栈的变化,从LAMP到MEAN Stack,从各种繁复的中间层到渐渐统一的消息驱动+流处理,每个阶段的业界热点都相当有代表性。

当然,不是说Web框架、数据存储就不是热点了,姑且不说这几年Web前端的复杂化,光后端应用框架,Node的Express,Python的Django/Flask,Go在国内的盛行,都是相当有趣的。在数据存储领域,列存储和时序数据随着物联网的发展也是备受重视。但是篇幅所限,在这个课程中这些话题也就只能一带而过,因为这些与其说是技术的演变过程,不如说是不同的技术选型和方向了,比如说MySQL适合OLTP(Online Transaction Processing),而Cassandra/HBase等则适合OLAP(Online Analyical Processing),并不能说后者就优于前者。

下面我们先来看后台架构的演化。
3.jpg

严格说这是个很大的标题,从2000年到现在的故事太多了,我这里只能尽力而为从个人体验来分析。

首先是2008年以前,我把它称为网站时代。为什么这么说?因为那时候的后台开发就是写网站,而且通常是页面代码和后台数据逻辑一起写。你只要能写JSP/PHP/ASP来读写MySQL或者SQL Server,基本就能保证一份不错的工作了。
4.jpg

要强调一下,这种简单的两层结构并不能说就是落后。在现在各个企业、公司以及小团队的大量Web应用包括移动App的后端服务中,采用这种架构的不在少数,尤其是很多公司、学校、企业的内部服务,用这种架构已经足够了。

注意一个时间节点:2008。

当然,这个节点是我YY的。这个节点可以是2007,或者2006。这个时间段发生了两个影响到现在的事情:Google上市,Facebook开始推开。

我个人相信前者上市加上它发表的那三篇大数据paper影响了后来业界的技术方向,后者的火热则造成了社交成为业务热点。偏偏社交网站对大数据处理有着天然的需求,技术的积累和业务的需求就这么阴差阳错完美结合了起来,直接影响了大海那边后面的科技发展。

同时在中国,那个时候却是网络游戏MMO的黄金年代,对单机单服高并发实时交互的需求,远远压过了对海量数据Data mining的需要,在这个时间点,中美两边的互联网科技树发生了比较大的分叉。这倒是并没有优劣之说,只是业务场景的重要性导致了技能树的侧重。直到今天,单机(包括简单的多服务器方案)高并发、高QPS仍然也是国内业界所追求的目标,而在美国那边,这只是一个业务指标而已,更看重的是如何进行水平扩展(horizontal scaling)和分散压力。

国内和美国的科技树回到一条线上,大数据的业务需求和相关技术发展紧密结合起来,可能要到2014年左右,随着互联网创业的盛行,O2O业务对大数据实时处理、机器学习推荐提出了真正的需求时,才是国内业界首次出现技术驱动业务,算法驱动产品的现象,重新和美国湾区那边站在了一条线上,而这则是后话了。
5.jpg

到了2010年前后,Facebook在全球已经是现象级产品,当时微软直接放弃了Windows Live,就是为了避免在社交领域硬怼Facebook。八卦一下当时在美国湾区那边聚餐的时候,如果谁说他是Facebook的,那基本就是全场羡慕的焦点。

Facebook的崛起也带动了其他大量的社交网站开始出现,社交网站最大的特点就是频繁的用户搜索、推荐,当用户上亿的时候,这就是前面传统的两层架构无法处理的问题了。因此这就带动了中间件的发展。实际上在国外很少有人用中间件或者Middelware这个词,更多是探讨如何把各种Service集成在一起,像国内这样强行分成Frontend/Middleware/Storage的概念是没听人这么谈过的,后面中间件再说这问题。当时的一个惯例是用PHP做所谓的胶水语言(glue language),然后通过Hessian这些协议工具来把其他Java服务连接到一起。与此同时,为了提高访问速度,降低后端查询压力,Memcached/Redis也开始大量使用。基于Lucene的搜索(2010左右很多是自行开发)或者Solr也被用在用户搜索、推荐以及Typeahead这些场景中。

我记忆中在2012年之前消息队列的使用还不是太频繁,不像后来这么重要。当时常见的应该就是Beanstalkd/RabbitMQ,ZeroMQ其实我在湾区那边很少听人用,倒是后来回国后看到国内用的人还不少。Kafka在2011年已经出现了,有少部分公司开始用,不过还不是主流。
6.jpg

2013年之后就是大数据+云的时代了,如果大家回想一下,基本上国内也是差不多在2014年左右开始叫出了云+大数据的口号(2013年国内还在手游狂潮中...)。不谈国外,在中国那段时间就是互联网创业的时代,从千团大战到手游爆发到15年开始的O2O,业务的发展也带动了技术栈的飞速进步。左上角大致上也写了这个时代互联网业界的主要技术热点,实际上这也就是现在的热点。无论国内国外,绝大部分公司还并没有离开云+大数据这个时代。无论是大数据的实时处理、数据挖掘、推荐系统、Docker化,包括A/B测试,这些都是很多企业还正在努力全面解决的问题。

但是在少数站在业界技术顶端或者没有历史技术包袱的新兴公司,从某个角度上来说,他们已经开始在往下一个时代前进:机器学习AI驱动的时代
7.jpg

2018年开始,实际上可能是2017年中开始,AI驱动成了各大公司口号。上图是Facebook和Uber的机器学习平台使用情况,基本上已经全部进入业务核心。当然并不是说所有公司企业都要AI驱动,显然最近发生的波音737事件就说明该用传统的就该传统,别啥都往并不成熟的AI上堆。但另一方面,很多新兴公司的业务本身就是基于大数据或者算法的,因此他们在这个领域也往往走得比较激进。由于这个AI驱动还并没有一个很明确的定义和概念,还处于一种早期萌芽的阶段,在这里也就不多YY了。

互联网后台架构发展的简单过程就在这里讲得差不多了,然后我们快速谈一下Web开发框架。
8.png

首先在前面我提到,在后端架构中其实也有所谓的Frontend(前台)开发存在,一般来说这是指响应用户请求,实现具体业务逻辑的业务逻辑层。当然这么定义略微粗糙了些,很多中间存储、消息服务也会封装一些业务相关逻辑。总之Web开发框架往往就是为了更方便地实现这些业务逻辑而存在的。

前文提到在一段较长时间内,国内的技术热点是单机高并发高QPS,因此很多那个时代走过来的人会本能地质疑Web框架的性能,而更偏好TCP长链接甚至UDP协议。然而这往往是自寻烦恼,因为除开特别的强实时系统,无论是休闲手游、视频点播还是信息流,都已经是基于HTTP的了。
9.jpg

上图所提到的两个问题中,我想强调的是第一点:所有的业务,在能满足需求的情况下,首选HTTP协议进行数据交互。准确点说,首选JSON,使用Web API。

Why?这就是上图第一个问题所回答的:无状态、易调试易修改、一般没有80端口限制。

最为诟病的无非是性能,然而实际上对非实时应用,晚个半秒一秒不应该是大问题,要考虑的是水平扩展scalability,不是实时响应(因为前提就是非实时应用);其次实在不行你还有WebSocket可以用。
10.jpg

这一部分是简单列举了一下不同框架的使用,可以看出不同框架的概念其实差不多。重点是要注意到Middleware这个说法在Web Framework和后端架构中的意义不同。在Web Framework中是指具体处理GET/POST这些请求之前的一个通用处理(往往是链式调用),比如可以把鉴权、一些日志处理和请求记录放在这里。但在后端架构设计中的Middleware则是指类似消息队列、缓存这些在最终数据库之前的中间服务组件。
11.jpg

最后这里是想说Web Framework并不是包治百病,实际上那只是提供了基础功能的一个library,作为开发者则更多需要考虑如何定义配置文件,一些敏感参数如token、密码怎么传进来,开发环境和生产环境的配置如何自动切换,单元测试怎么搞,代码目录怎么组织。有时候我们可以用一些比如Yeoman之类的Scaffold工具来自动生成项目代码框架,或者类似Django这种也可能自动生成基本目录结构。

下面进入Middleware环节。Again,强调一下这里只是根据个人经验和感受谈谈演化过程。
12.png

13.jpg

这一页只是大致讲一下怎么定义中间件Middleware。说句题外话,在美国湾区那边提这个概念的很少,而阿里又特别喜欢说中间件,两者相互的交流非常头痛。湾区那边不少Google、Facebook还有Pinterest/Uber这些的朋友好几次都在群里问说啥叫中间件。

中间件这个概念很含糊,应该是阿里提出来的,对应于Middleware(不过似乎也不是完全对应),可能是因为早期Java的EJB那些概念里面比较强调Middleware这一点吧(个人猜的)。大致上,如果我们把Web后端分为直接处理用户请求的Frontend,最后对数据进行持久存储(persistant storage)这两块,那么中间对数据的所有处理环节都可以视为Middleware。
14.jpg

和中间件对应的另一个阿里发明的概念是中台。近一年多阿里的中台概念都相当引人注意,这里对中台不做太多描述。总体来说中台更多是偏向业务和组织架构划分,不能说是一个技术概念,也不是面向开发人员的。而中间件Middleware是标准的技术组件服务。

那么我们自然会有一个问题:为什么要用中间件?
15.jpg

谈到为什么要用Middlware,这里用推荐系统举例。

推荐系统,对数据少用户少的情况下,简单的MySQL即可,比如早期论坛的什么top 10热门话题啊,最多回复的话题啊,都可以视为简单的推荐,数据量又不大的情况下,直接select就可以了。

如果是用户推荐的话,用户量不大的情况下,也可以如法炮制,选择同一区域(城市)年龄相当的异性,最后随机挑几个给你,相信世纪佳缘之类的交友网站早期实现也就是类似的模式。
16.jpg

那么,如果用户量多了呢?每次都去搜数据库,同时在线用户又多,那对数据库的压力就巨大了。这时候就是引入缓存,Memcached、Redis就出现了。

简单的做法就是把搜索条件作为key,把结果作为value存入缓存。打个比方你可以把key存为 20:40:beijing:male(20到40岁之间北京的男性),然后把第一次搜索的结果全部打乱shuffle后,存前1000个,10分钟过期,再有人用类似条件搜索,就直接把缓存数据随机挑几个返回。放心,一般来说不会有人10分钟就把1000个用户的资料都看完了,中间偶有重复也没人在意(用世纪佳缘、百合网啥的时候看到过重复的吧)。

不过话又说回来,现代数据库,尤其是类似MongoDB/ES这些大量占用内存的NoSQL,已经对经常查询的数据做了缓存,在这之上再加cache,未必真的很有效,这需要case by case去分析了,总之盲目加cache也并不推荐。

加缓存是为了解决访问速度,减轻数据库压力,但是并不提高推荐精准度。如果我们要提高推荐效果呢?在2015年之前机器学习还没那么普及成熟的时候,我们怎么搞呢?
17.jpg

提高推荐效果,在机器学习之前有两种做法:

- 引入基于Lucene的搜索引擎,在搜索的同时通过定制方案实现scoring,比如我可以利用Lucene对用户的年龄、性别、地址等进行indexing,但是再返回结果时我再根据用户和查询者两人的具体信息进行关联,自定义返回的score(可以视为推荐相关系数)
- 采用离线批处理。固然可以用Hadoop,但是就太杀鸡用牛刀了。常见的是定时批处理任务,按某种规则划分用户群体,对每个群体再做全量计算后把推荐结果写入缓存。这种可以做很繁复准确的计算,虽然慢,但效果往往不错。这种做法也常用在手机游戏的PvP对战列表里面。

这些处理方法对社交网络/手游这类型的其实已经足够了,但是新的业务是不断出现的。随着Uber/滴滴/饿了么/美团这些需要实时处理数据的App崛起,作为一个司机,并不想你上线后过几分钟才有客人来吧,你希望你开到一个热点区域,一开机就马上接单。
18.jpg

所以这种对数据进行实时(近实时)处理的需求也带动了后端体系的大发展,Kafka/Spark等等流处理大行其道。这时候的后端体系就渐渐引入了消息驱动的模式,所谓消息驱动,就是对新的生产数据会有多个消费者,有的是满足实时计算的需求(比如司机信息需要立刻能够被快速检索到,又不能每次都做全量indexing,就需要用到Spark),有的只是为了数据分析,写入类似Cassandra这些数据库里,还有的可能是为了生成定时报表,写入到MySQL。

大数据的处理一直是业界热点领域。记得2015年硅谷一个朋友就是从一家小公司做PHP跳去另一家物联网公司做Spark相关的工作,之前还很担心玩不转,搞了两年就俨然业界大佬被Oracle挖去负责云平台。

Anyway,这时候对后端体系的要求是一方面能快速满足实时需求,另一方面又能满足各种耗时长的数据分析、Data lake存储等等,以及当时渐渐普及的机器学习模型(当时2015年初和几个朋友搞Startup,其中一个是Walmart Lab的机器学习专家,上来就一堆模型,啥数据和用户都还没有就把模型摆上来了,后来搞得非常头痛。当时没有Keras/PyTorch/tf这些,那堆模型是真心搞不太懂,但是又不敢扔,要靠那东西去包装拿投资的。)

但是我们再看上面的图,是不是感觉比较乱呢?各种系统的数据写来写去,是不是有点messy?当公司团队增多,系统复杂度越来越高的时候,我们该怎么梳理?
19.jpg

到了2017之后,前面千奇百怪的后端体系基本上都趋同了。Kafka的实时消息队列,Spark的流处理(当然现在也可以换成Flink,不过大部分应该还是Spark),然后后端的存储,基于Hive的数据分析查询,然后根据业务的模型训练平台。各个公司反正都差不多这一套,在具体细节上根据业务有所差异,或者有些实力强大的公司会把中间一些环节替换成自己的实现,不过不管怎么千变万化,整体思路基本都一致了。

这里可以看到机器学习和AI模型的引入。个人认为,Machine Learning的很大一个好处,是简化业务逻辑,简化后台流程,不然一套业务一套实现,各种数据和业务规则很难用一个整体的技术平台来完成。相比前面一页的后台架构,这一页要清晰许多,而且是一个DAG有向无环图的形式,数据流向很明确。我们在下面再来说这个机器学习对业务数据流程的简化。
20.jpg

在传统后端系统中,业务逻辑其实和数据是客观分离的,逻辑规则和数据之间并不存在客观联系,而是人为主观加入,并没形成闭环,如上图左上所示。而基于机器学习的平台,这个闭环就形成了,从业务数据->AI模型->业务逻辑->影响用户行为->新的业务数据这个流程是自给自足的。这在很多推荐系统中表现得很明显,通过用户行为数据训练模型,模型对页面信息流进行调整,从而影响用户行为,然后用新的用户行为数据再次调整模型。而在机器学习之前,这些观察工作是交给运营人员去手工猜测调整。

上图右边谈的是机器学习相关后台架构和传统Web后台的一些差别,重点是耗时太长,必须异步处理。因此消息驱动机制对机器学习后台是一个必须的设计。
21.jpg

这页是一些个人的感受,现代的后端数据处理越来越偏向于DAG的形态,Spark不说了,DAG是最大特色;神经网络本身也可以看作是一个DAG(RNN其实也可以看作无数个单向DNN的组合);TensorFlow也是强调其Graph是DAG,另外编程模式上,Reactive编程也很受追捧。

其实DAG的形态重点强调的就是数据本身是immutable(不可修改),只能transform后成为新的数据进入下一环。这个思维其实可以贯穿到现代后台系统设计的每个环节,比如Trakcing、Analytics、数据表设计、Microservice等等,但具体实施还是要case by case了。

无论如何,数据,数据的跟踪Tracking,数据的流向,是现代后台系统的核心问题,只有Dataflow和Data Pipeline清晰了,整个后台架构才会清楚。

数据库是个非常复杂的领域,在下面对几个基本常用的概念做一些介绍。注意一点是Graph database在这里没有提到,因为日常使用较少,相对来说Facebook提出的GraphQL倒是个有趣的概念,但也只是在传统DB上的一个概念封装。
22.png

23.jpg

上图是2018年12月初热门数据库的排名,我们可以看到关系数据库RDBMS和NOSQL数据库基本上平分秋色。而NoSQL中实际上又可以分为key-value storage(包括文档型)及Column based DB。
24.jpg

MySQL这个没啥好讲,大概提一下就是。有趣的是曾经看到一篇文章是AWS CTO谈的一些内容,其中印象深刻是:如果你的用户还不到100万,就别折腾了,无脑使用MySQL吧。

在2015年之前的一个趋势是不少公司使用MySQL作为数据存储,但是把indexing放在外部去做。这个思路最早似乎是Friendster提出的,后来Uber也模仿这种做法设计了自己的数据库Schemaless。然而随着PostgreSQL的普及(PostgreSQL支持对json的索引),这种做法是否还有意义就值得商榷了。
25.jpg

NoSQL最早的使用就是key-value的查找,典型的就是Redis。实际上后来的像MongoDB这些Documentbased DB也是类似的key value,只是它对Document中的内容又做了一次index(b-tree),用空间换时间来提供查找数据,这也是CS不变的思维。

MongoDB/Elasticsearch收到热捧主要是因为它们的Schemaless属性,也就是不需要提前定义数据格式,只要是json就存,还都能根据每个field搜索,这非常方便程序员快速出demo。但是实际上数据量大之后还是要规范数据结构,定义需要indexing的field的。
26.jpg

这里提一个比较好玩的开源Project NodeBB,这是个Node.js开发的论坛系统。在我前几年看到这个的时候它其实只支持Redis,然后当时因为一个项目把它改造了让他支持MySQL。去年再看的时候发现它同时支持了Redis/Postres/MongoDB,如果对比一下同样的功能他如何在这三种DB实现的,相信会很有帮助。

稍微谈谈列存储。常见MySQL你在select的时候其实往往会把整行都读出来,再在其中挑那么一两个你需要的属性,非常浪费。而MongoDB这些文件型DB,又不支持常见SQL。而列存储DB的好处就是快,不用把一行所有信息读出来,只是按列读取你需要的,对现在的大数据分析特别是OLAP(Online Analytical Processing)来说特别重要。然而据另外的说法,实际上像Casssandra/HBase这些并不是真正的列存储,而只是借用了一些概念。这个我也没深入去了解,有兴趣的同学可以自己研究研究。
27.jpg

28.jpg

列存储的一个重要领域是时序数据库,物联网用得多。其特色是大量写入,只增不改(不修改数据),但是读的次数相对于很少(想想物联网的特点,随时有数据写入,但是你不会随时都在看你家小米电器的状态。)

注意说Write/Read是正交的。这意思是每次写入是一次一行,而读是按列,加上又不会修改数据,因此各自都能保持极快的速度。

下面简单谈一下微服务,大部分直接看PPT就可以了,有几页略微谈一下个人思考。
29.jpg

30.jpg

31.jpg

32.jpg

33.jpg

上面这页说说,其实微服务所谓的服务发现/name service不要被忽悠觉得是多神奇的东西。最简单的Nginx/Apache这些都能做(域名转向,Proxy),或者你要写个name : address的对应关系到DB里面也完全可以,再配一个定时HealthCheck的服务,最简单的服务发现也就行了。

高级点用到ZooKeeper/etcd等等,或者Spring Cloud全家桶,那只是简化配置,原理都一样。从开发角度来看,微服务的开发并不是难点,难点是微服务的配置和部署。最近一段时间微服务部署也是业界热点,除了全家桶形态的Spring Cloud,也可以看看Istio这些开源工具。
34.jpg

上图主要大致对比一下,看看从早期的Spring到现在Spring Cloud的变化。想来用过Java Tomcat的朋友都能体会Java这一套Config based development的繁琐,开发的精力很多不是在业务代码上,往往会化不少精力去折腾配置文件。当然,Spring Cloud在这方面简化了不少,不过个人还是不太喜欢Java,搞很多复杂的设计模式,封装了又封装。
35.jpg

这里要说并不是微服务解决一切,热门的Python Django尽管有REST Framework,但是它实际上是一个典型的Monolithic体系。对很多核心业务,其实未必要拆开成微服务。

这两者是互补关系,不是替代关系。

下面的Docker我就不仔细谈了,PPT基本表达了我想表述的概念,主要意思是:

  • Docker能够简化部署,简化开发,能够在某种程度上让开发环境和产品环境尽量接近。
  • 不要担心Docker的性能,它不是虚拟机,可以看作在Server上运行的一个Process。

36.png

37.jpg

上图是描述Docker之前开发人员的常见开发环境,首先在自己机器上装一大堆服务,像MySQL,Redis,Tomcat啥的。也有直接在远程服务器安装环境后,多人共同登录远端开发,各自使用一个端口避免冲突……实际上这种土法炼钢的形态,在2019年的今天仍然在国内非常普及。

这种形态的后果就是在最后发布到生产环境时,不同开发人员会经历长时间的“联调”,各种端口、权限、脚本、环境设置在生产环境再来一遍…这也是过去运维人员的主要工作。
38.jpg

上一页提到的问题,并不是一定要Docker来解决。在这之前,虚拟机VM的出现,以及Vagrant这样的工具,都让开发环境的搭建多少轻松了一些。不过思路仍然是把VM作为一个独立服务器使用,只是因为快照、镜像和辅助工具,让环境的配置、统一和迁移更加简单快捷。
39.jpg

上图是对比程序运行在物理服务器、VM及Docker时的资源共享情况,可以看到运行在Docker的应用,并没有比并发运行在物理服务器上占用更多资源。

下图是简单的Docker使用,不做赘述。
40.jpg

41.jpg

这一页主要是强调Docker并不等同于虚拟机。虚拟机所占资源是独享的,比如你启动一个VM,分配2G内存,那么这个VM里不管是否运行程序都会占用2G内存。然而如果你启动一个Docker,里面运行一个简单Web服务,在不强制指定内存占用情况下,如果没有请求进入,没有额外占用内存,那么这个Docker服务对整机的内存占用几乎为0(当然仍然存在一些开销,但主要是根据该程序自身的运行状况而定)。
42.jpg

最后是Kubernetes,这里大概说说Host-Pod-Container的关系,一个Host可以是物理机或者VM,Pod不是一个Docker,而是可以看作有一个IP的……(不知道怎么形容),总之一个Pod可以包括多个Container(Docker),Pod之中的Container可以共享该Pod的资源(IP,storage等)。不过现实中似乎大多是一个Pod对一个Container。

对互联网一些热门概念和演变过程的一个很简略的描述就到这里。

原文链接:从技术演变的角度看互联网后台架构

详解Eureka缓存机制

阿娇 发表了文章 • 0 个评论 • 279 次浏览 • 2019-06-04 14:45 • 来自相关话题

【编者的话】Eureka是Netflix开源的、用于实现服务注册和发现的服务。Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便。但是由于Eureka本身存在较多缓存,服务状态更新滞后,最常见的状况是:服务下 ...查看全部
【编者的话】Eureka是Netflix开源的、用于实现服务注册和发现的服务。Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便。但是由于Eureka本身存在较多缓存,服务状态更新滞后,最常见的状况是:服务下线后状态没有及时更新,服务消费者调用到已下线的服务导致请求失败。本文基于Spring Cloud Eureka 1.4.4.RELEASE,在默认region和zone的前提下,介绍Eureka的缓存机制。
#一、AP特性

从CAP理论看,Eureka是一个AP系统,优先保证可用性(A)和分区容错性(P),不保证强一致性(C),只保证最终一致性,因此在架构中设计了较多缓存。
1.jpg

Eureka高可用架构
#二、服务状态

Eureka服务状态enum类:com.netflix.appinfo.InstanceInfo.InstanceStatus
2.png

#三、Eureka Server

在Eureka高可用架构中,Eureka Server也可以作为Client向其他server注册,多节点相互注册组成Eureka集群,集群间相互视为peer。Eureka Client向Server注册、续约、更新状态时,接受节点更新自己的服务注册信息后,逐个同步至其他peer节点。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

【注意】如果server-A向server-B节点单向注册,则server-A视server-B为peer节点,server-A接受的数据会同步给server-B,但server-B接受的数据不会同步给server-A。
##缓存机制

Eureka Server存在三个变量:(registry、readWriteCacheMap、readOnlyCacheMap)保存服务注册信息,默认情况下定时任务每30s将readWriteCacheMap同步至readOnlyCacheMap,每60s清理超过90s未续约的节点,Eureka Client每30s从readOnlyCacheMap更新服务注册信息,而UI则从registry更新服务注册信息。
3.jpg

三级缓存:
4.jpg

缓存相关配置:
5.jpg

关键类:
6.jpg

#四、Eureka Client

Eureka Client存在两种角色:服务提供者和服务消费者,作为服务消费者一般配合Ribbon或Feign(Feign内部使用Ribbon)使用。Eureka Client启动后,作为服务提供者立即向Server注册,默认情况下每30s续约(renew);作为服务消费者立即向Server全量更新服务注册信息,默认情况下每30s增量更新服务注册信息;Ribbon延时1s向Client获取使用的服务注册信息,默认每30s更新使用的服务注册信息,只保存状态为UP的服务。

二级缓存:
7.jpg

缓存相关配置:
8.jpg

关键类:
9.jpg

#五、默认配置下服务消费者最长感知时间

10.jpg

考虑如下情况:

* 0s时服务未通知Eureka Client直接下线;
* 29s时第一次过期检查evict未超过90s;
* 89s时第二次过期检查evict未超过90s;
* 149s时第三次过期检查evict未续约时间超过了90s,故将该服务实例从registry和readWriteCacheMap中删除;
* 179s时定时任务从readWriteCacheMap更新至readOnlyCacheMap;
* 209s时Eureka Client从Eureka Server的readOnlyCacheMap更新;
* 239s时Ribbon从Eureka Client更新。

因此,极限情况下服务消费者最长感知时间将无限趋近240s。
11.jpg

#六、应对措施

服务注册中心在选择使用Eureka时说明已经接受了其优先保证可用性(A)和分区容错性(P)、不保证强一致性(C)的特点。如果需要优先保证强一致性(C),则应该考虑使用ZooKeeper等CP系统作为服务注册中心。分布式系统中一般配置多节点,单个节点服务上线的状态更新滞后并没有什么影响,这里主要考虑服务下线后状态更新滞后的应对措施。
##Eureka Server

1、缩短readOnlyCacheMap更新周期。缩短该定时任务周期可减少滞后时间。
eureka.server.responsecCacheUpdateIntervalMs: 10000  # Eureka Server readOnlyCacheMap更新周期

2、关闭readOnlyCacheMap。中小型系统可以考虑该方案,Eureka Client直接从readWriteCacheMap更新服务注册信息。
eureka.server.useReadOnlyResponseCache: false        # 是否使用readOnlyCacheMap

##Eureka Client

1、服务消费者使用容错机制。如Spring Cloud Retry和Hystrix,Ribbon、Feign、Zuul都可以配置Retry,服务消费者访问某个已下线节点时一般报ConnectTimeout,这时可以通过Retry机制重试下一个节点。

2、服务消费者缩短更新周期。Eureka Client和Ribbon二级缓存影响状态更新,缩短这两个定时任务周期可减少滞后时间,例如配置:
eureka.client.registryFetchIntervalSeconds: 5        # Eureka Client更新周期
ribbon.ServerListRefreshInterval: 2000 # Ribbon更新周期

3、服务提供者保证服务正常下线。服务下线时使用kill或kill -15命令,避免使用kill -9命令,kill或kill -15命令杀死进程时将触发Eureka Client的shutdown()方法,主动删除Server的registry和readWriteCacheMap中的注册信息,不必依赖Server的evict清除。

4、服务提供者延迟下线。服务下线之前先调用接口使Eureka Server中保存的服务状态为DOWN或OUT_OF_SERVICE后再下线,二者时间差根据缓存机制和配置决定,比如默认情况下调用接口后延迟90s再下线服务即可保证服务消费者不会调用已下线服务实例。
#七、网关实现服务下线实时感知

在软件工程中,没有一个问题是中间层解决不了的,而网关是服务提供者和服务消费者的中间层。以Spring Cloud Zuul网关为例,网关作为Eureka Client保存了服务注册信息,服务消费者通过网关将请求转发给服务提供者,只需要做到服务提供者下线时通知网关在自己保存的服务列表中使该服务失效。为了保持网关的独立性,可实现一个独立服务接收下线通知并协调网关集群。下篇文章将详细介绍网关如何实现服务下线实时感知,敬请期待!

原文链接:https://mp.weixin.qq.com/s/zwoIDzX8WouYVrBfMJMpJQ

使用Spring Cloud和Docker构建微服务架构

老马 发表了文章 • 0 个评论 • 242 次浏览 • 2019-05-28 22:27 • 来自相关话题

【编者的话】如何使用Spring Boot、Spring Cloud、Docker和Netflix的一些开源工具来构建一个微服务架构。本文通过使用Spring Boot、Spring Cloud和Docker构建的概念型应用示例,提供了了解常见的微服务架构模式 ...查看全部
【编者的话】如何使用Spring Boot、Spring Cloud、Docker和Netflix的一些开源工具来构建一个微服务架构。本文通过使用Spring Boot、Spring Cloud和Docker构建的概念型应用示例,提供了了解常见的微服务架构模式的起点。

该代码可以在GitHub上获得,并且在Docker Hub上提供了镜像。您只需要一个命令即可启动整个系统。

我选择了一个老项目作为这个系统的基础,它的后端以前是单一应用。此应用提供了处理个人财务、整理收入开销、管理储蓄、分析统计和创建简单预测等功能。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#功能服务
整个应用分解为三个核心微服务。它们都是可以独立部署的应用,围绕着某些业务功能进行组织。
1.png


账户服务

包含一般用户输入逻辑和验证:收入/开销记录、储蓄和账户设置。
2.png


统计服务

计算主要的统计参数,并捕获每一个账户的时间序列。数据点包含基于货币和时间段正常化后的值。该数据可用于跟踪账户生命周期中的现金流量动态。
3.png


通知服务

存储用户的联系信息和通知设置(如提醒和备份频率)。安排工作人员从其它服务收集所需的信息并向订阅的客户发送电子邮件。
4.png


注意

* 每一个微服务拥有自己的数据库,因此没有办法绕过API直接访问持久数据。
* 在这个项目中,我使用MongoDB作为每一个服务的主数据库。拥有一个多种类持久化架构(polyglot persistence architecture)也是很有意义的。
* 服务间(Service-to-service)通信是非常简单的:微服务仅使用同步的REST API进行通信。现实中的系统的常见做法是使用互动风格的组合。例如,执行同步的GET请求检索数据,并通过消息代理(broker)使用异步方法执行创建/更新操作,以便解除服务和缓冲消息之间的耦合。然而,这带给我们是最终的一致性

#基础设施服务
分布式系统中常见的模式,可以帮助我们描述核心服务是怎样工作的。Spring Cloud提供了强大的工具,可以增强Spring Boot应用的行为来实现这些模式。我会简要介绍一下:
5.png

##配置服务
Spring Cloud Config是分布式系统的水平扩展集中式配置服务。它使用了当前支持的本地存储、Git和Subversion等可拔插存储库层(repository layer)。

在此项目中,我使用了native profile,它简单地从本地classpath下加载配置文件。您可以在配置服务资源中查看shared目录。现在,当通知服务请求它的配置时,配置服务将响应回shared/notification-service.yml和shared/application.yml(所有客户端应用之间共享)。

客户端使用

只需要使用sprng-cloud-starter-config依赖构建Spring Boot应用,自动配置将会完成其它工作。

现在您的应用中不需要任何嵌入的properties,只需要提供有应用名称和配置服务url的bootstrap.yml即可:
spring:
application:
name: notification-service
cloud:
config:
uri: http://config:8888
fail-fast: true

使用Spring Cloud Config,您可以动态更改应用配置

比如,EmailService bean使用了@RefreshScope注解。这意味着您可以更改电子邮件的内容和主题,而无需重新构建和重启通知服务应用。

首先,在配置服务器中更改必要的属性。然后,对通知服务执行刷新请求:curl -H "Authorization: Bearer #token#" -XPOST http://127.0.0.1:8000/notifications/refresh。

您也可以使用webhook来自动执行此过程

注意

* 动态刷新存在一些限制。@RefreshScope不能和@Configuraion类一同工作,并且不会作用于@Scheduled方法。
* fail-fast属性意味着如果Spring Boot应用无法连接到配置服务,将会立即启动失败。当您一起启动所有应用时,这非常有用。
* 下面有重要的安全提示

##授权服务
负责授权的部分被完全提取到单独的服务器,它为后端资源服务提供OAuth2令牌。授权服务器用于用户授权以及在周边内进行安全的机器间通信。

在此项目中,我使用密码凭据作为用户授权的授权类型(因为它仅由本地应用UI使用)和客户端凭据作为微服务授权的授权类型。

Spring Cloud Security提供了方便的注解和自动配置,使其在服务器端或者客户端都可以很容易地实现。您可以在文档中了解到更多信息,并在授权服务器代码中检查配置明细。

从客户端来看,一切都与传统的基于会话的授权完全相同。您可以从请求中检索Principal对象、检查用户角色和其它基于表达式访问控制和@PreAuthorize注解的内容。

PiggyMetrics(帐户服务、统计服务、通知服务和浏览器)中的每一个客户端都有一个范围:用于后台服务的服务器、用于浏览器展示的UI。所以我们也可以保护控制器避免受到外部访问,例如:
@PreAuthorize("#oauth2.hasScope('server')")
@RequestMapping(value = "accounts/{name}", method = RequestMethod.GET)
public List getStatisticsByAccountName(@PathVariable String name) {
return statisticsService.findByAccountName(name);
}

##API网关
您可以看到,有三个核心服务。它们向客户端暴露外部API。在现实系统中,这个数量可以非常快速地增长,同时整个系统将变得非常复杂。实际上,一个复杂页面的渲染可能涉及到数百个服务。

理论上,客户端可以直接向每个微服务直接发送请求。但是这种方式是存在挑战和限制的,如果需要知道所有端点的地址,分别对每一段信息执行http请求,将结果合并到客户端。另一个问题是,这不是web友好协议,可能只在后端使用。

通常一个更好的方法是使用API网关。它是系统的单个入口点,用于通过将请求路由到适当的后端服务或者通过调用多个后端服务并聚合结果来处理请求。此外,它还可以用于认证、insights、压力测试、金丝雀测试(canary testing)、服务迁移、静态响应处理和主动变换管理。

Netflix开源这样的边缘服务,现在用Spring Cloud,我们可以用一个@EnabledZuulProxy注解来启用它。在这个项目中,我使用Zuul存储静态内容(UI应用),并将请求路由到适当的微服务。以下是一个简单的基于前缀(prefix-based)路由的通知服务配置:
zuul:
routes:
notification-service:
path: /notifications/**
serviceId: notification-service
stripPrefix: false

这意味着所有以/notification开头的请求将被路由到通知服务。您可以看到,里面没有硬编码的地址。Zuul使用服务发现机制来定位通知服务实例以及断路器和负载均衡器,如下所述。
##服务发现
另一种常见的架构模式是服务发现。它允许自动检测服务实例的网络位置,由于自动扩展、故障和升级,它可能会动态分配地址。

服务发现的关键部分是注册。我使用Netflix Eureka进行这个项目,当客户端需要负责确定可以用的服务实例(使用注册服务器)的位置和跨平台的负载均衡请求时,Eureka就是客户端发现模式的一个很好的例子。

使用Spring Boot,您可以使用spring-cloud-starter-eureka-server依赖、@EnabledEurekaServer注解和简单的配置属性轻松构建Eureka注册中心(Eureka Registry)。

使用@EnabledDiscoveryClient注解和带有应用名称的bootstrap.yml来启用客户端支持:
spring:
application:
name: notification-service

现在,在应用启动时,它将向Eureka服务器注册并提供元数据,如主机和端口、健康指示器URL、主页等。Eureka接收来自从属于某服务的每个实例的心跳消息。如果心跳失败超过配置的时间表,该实例将从注册表中删除。

此外,Eureka还提供了一个简单的界面,您可以通过它来跟踪运行中的服务和可用实例的数量:http://localhost:8761
6.png

##负载均衡器、断路器和Http客户端
Netflix OSS提供了另一套很棒的工具。

Ribbon

Ribbon是一个客户端负载均衡器,可以很好地控制HTTP和TCP客户端的行为。与传统的负载均衡器相比,每次线上调用都不需要额外的跳跃——您可以直接联系所需的服务。

它与Spring Cloud和服务发现是集成在一起的,可开箱即用。Eureka客户端提供了可用服务器的动态列表,因此Ribbon可以在它们之间进行平衡。

Hystrix

Hystrix是断路器模式的一种实现,它可以通过网络访问依赖来控制延迟和故障。中心思想是在具有大量微服务的分布式环境中停止级联故障。这有助于快速失败并尽快恢复——自我修复在容错系统中是非常重要的。

除了断路器控制,在使用Hystrix,您可以添加一个备用方法,在主命令失败的情况下,该方法将被调用以获取默认值。

此外,Hystrix生成每个命令的执行结果和延迟的度量,我们可以用它来监视系统的行为

Feign

Feign是一个声明式HTTP客户端,能与Ribbon和Hystrix无缝集成。实际上,通过一个spring-cloud-starter-feign依赖和@EnabledFeignClients注解,您可以使用一整套负载均衡器、断路器和HTTP客户端,并附带一个合理的的默认配置。

以下是账户服务的示例:
@FeignClient(name = "statistics-service")
public interface StatisticsServiceClient {
@RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
void updateStatistics(@PathVariable("accountName") String accountName, Account account);
}


* 您需要的只是一个接口
* 您可以在Spring MVC控制器和Feign方法之间共享@RequestMapping部分
* 以上示例仅指定所需要的服务ID——statistics-service,这得益于Eureka的自动发现(但显然您可以使用特定的URL访问任何资源)。

##监控仪表盘
在这个项目配置中,Hystrix的每一个微服务都通过Spring Cloud Bus(通过AMQP broker)将指标推送到Turbine。监控项目只是一个使用了TurbineHystrix仪表盘的小型Spring Boot应用。

让我们看看系统行为在负载下:账户服务调用统计服务和它在一个变化的模拟延迟下的响应。响应超时阈值设置为1秒。
7.png

##日志分析
集中式日志记录在尝试查找分布式环境中的问题时非常有用。Elasticsearch、Logstash和Kibana技术栈可让您轻松搜索和分析您的日志、利用率和网络活动数据。在我的另一个项目中已经有现成的Docker配置。
##安全

高级安全配置已经超过了此概念性项目的范围。为了更真实地模拟真实系统,请考虑使用https和JCE密钥库来加密微服务密码和配置服务器的properties内容(有关详细信息,请参阅文档)。
#基础设施自动化

部署微服务比部署单一的应用的流程要复杂得多,因为它们相互依赖。拥有完全基础设置自动化是非常重要的。我们可以通过持续交付的方式获得以下好处:

* 随时发布软件的能力。
* 任何构建都可能最终成为一个发行版本。
* 构建工件(artifact)一次,根据需要进行部署。

这是一个简单的持续交付工作流程,在这个项目的实现:

在此配置中,Travis CI为每一个成功的Git推送创建了标记镜像。因此,每一个微服务在Docker Hub上的都会有一个latest镜像,而较旧的镜像则使用Git提交的哈希进行标记。如果有需要,可以轻松部署任何一个,并快速回滚。
8.png

#如何运行全部?
这真的很简单,我建议您尝试一下。请记住,您将要启动8个Spring Boot应用、4个MongoDB实例和RabbitMq。确保您的机器上有4GB的内存。您可以随时通过网关、注册中心、配置、认证服务和账户中心运行重要的服务。

运行之前

* 安装Docker和Docker Compose。
* 配置环境变量:CONFIG_SERVICE_PASSWORD, NOTIFICATION_SERVICE_PASSWORD, STATISTICS_SERVICE_PASSWORD, ACCOUNT_SERVICE_PASSWORD, MONGODB_PASSWORD

生产模式

在这种模式下,所有最新的镜像都将从Docker Hub上拉取。只需要复制docker-compose.yml并执行docker-compose up -d即可。

开发模式

如果您想自己构建镜像(例如,在代码中进行一些修改),您需要克隆所有仓库(repository)并使用Mavne构建工件(artifact)。然后,运行docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

docker-compose.dev.yml继承了docker-compose.yml,附带额外配置,可在本地构建镜像,并暴露所有容器端口以方便开发。

重要的端点(Endpoint)

* localhost:80 —— 网关
* localhost:8761 —— Eureka仪表盘
* localhost:9000 —— Hystrix仪表盘
* localhost:8989 —— Turbine stream(Hystrix仪表盘来源)
* localhost:15672 —— RabbitMq管理

注意

所有Spring Boot应用都需要运行配置服务器才能启动。得益于Spring Boot的fail-fast属性和docker-compsoe的restart:always选项,我们可以同时启动所有容器。这意味着所有依赖的容器将尝试重新启动,直到配置服务器启动运行为止。

此外,服务发现机制在所有应用启动后需要一段时间。在实例、Eureka服务器和客户端在其本地缓存中都具有相同的元数据之前,任何服务都不可用于客户端发现,因此可能需要3次心跳。默认的心跳周期为30秒。

原文链接:Microservice Architectures With Spring Cloud and Docker(翻译:Oopsguy

一张图了解Spring Cloud微服务架构

老马 发表了文章 • 0 个评论 • 600 次浏览 • 2019-04-26 20:48 • 来自相关话题

Spring Cloud作为当下主流的微服务框架,可以让我们更简单快捷地实现微服务架构。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置 ...查看全部
Spring Cloud作为当下主流的微服务框架,可以让我们更简单快捷地实现微服务架构。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。Spring Cloud中各个组件在微服务架构中扮演的角色如下图所示,黑线表示注释说明,蓝线由A指向B,表示B从A处获取服务。
6295401-8076ec880947ba79.png

Spring Cloud组成的微服务架构图

由上图所示微服务架构大致由上图的逻辑结构组成,其包括各种微服务、注册发现、服务网关、熔断器、统一配置、跟踪服务等。下面说说Spring Cloud中的组件分别充当其中的什么角色。

Fegin(接口调用):微服务之间通过Rest接口通讯,Spring Cloud提供Feign框架来支持Rest的调用,Feign使得不同进程的Rest接口调用得以用优雅的方式进行,这种优雅表现得就像同一个进程调用一样。

Netflix eureka(注册发现):微服务模式下,一个大的Web应用通常都被拆分为很多比较小的Web应用(服务),这个时候就需要有一个地方保存这些服务的相关信息,才能让各个小的应用彼此知道对方,这个时候就需要在注册中心进行注册。每个应用启动时向配置的注册中心注册自己的信息(IP地址,端口号, 服务名称等信息),注册中心将他们保存起来,服务间相互调用的时候,通过服务名称就可以到注册中心找到对应的服务信息,从而进行通讯。注册与发现服务为微服务之间的调用带来了方便,解决了硬编码的问题。服务间只通过对方的服务ID,而无需知道其IP和端口即可以获取对方方服务。

Ribbon(负载均衡):Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon,配置服务提供者的地址列表后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机等。当然,我们也可为Ribbon实现自定义的负载均衡算法。在Spring Cloud中,当Ribbon与Eureka配合使用时,Ribbon可自动从EurekaServer获取服务提供者的地址列表,并基于负载均衡算法,请求其中一个服务提供者的实例(为了服务的可靠性,一个微服务可能部署多个实例)。

Hystrix(熔断器):当服务提供者响应非常缓慢,那么消费者对提供者的请求就会被强制等待,直到提供者响应或超时。在高负载场景下,如果不做任何处理,此类问题可能会导致服务消费者的资源耗竭甚至整个系统的崩溃(雪崩效应)。Hystrix正是为了防止此类问题发生。Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。Hystrix主要通过以下几点实现延迟和容错。

* 包裹请求:使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用了设计模式中的“命令模式”。
* 跳闸机制:当某服务的错误率超过一定阈值时,Hystrix可以自动或者手动跳闸,停止请求该服务一段时间。
* 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。
* 监控:Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时和被拒绝的请求等。
* 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑可由开发人员指定。

Zuul(微服务网关):不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求。例如一个电影购票的手机APP,可能调用多个微服务的接口才能完成一次购票的业务流程,如果让客户端直接与各个微服务通信,会有以下的问题:

* 客户端会多次请求不同的微服务,增加了客户端的复杂性。
* 存在跨域请求,在一定场景下处理相对复杂。
* 认证复杂,每个服务都需要独立认证。
* 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将很难实施。
* 某些微服务可能使用了对防火墙/浏览器不友好的协议,直接访问时会有一定的困难。

以上问题可借助微服务网关解决。微服务网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过微服务网关。使用微服务网关后,微服务网关将封装应用程序的内部结构,客户端只用跟网关交互,而无须直接调用特定微服务的接口。这样,开发就可以得到简化。不仅如此,使用微服务网关还有以下优点:

* 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
* 易于认证。可在微服务网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
* 减少了客户端与各个微服务之间的交互次数。

Spring Cloud Bus( 统一配置服务):对于传统的单体应用,常使用配置文件管理所有配置。例如一个SpringBoot开发的单体应用,可将配置内容放在application.yml文件中。如果需要切换环境,可设置多个Profile,并在启动应用时指定spring.profiles.active={profile}。然而,在微服务架构中,微服务的配置管理一般有以下需求:

* 集中管理配置。一个使用微服务架构的应用系统可能会包含成百上千个微服务,因此集中管理配置是非常有必要的。
* 不同环境,不同配置。例如,数据源配置在不同的环境(开发、测试、预发布、生产等)中是不同的。
* 运行期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小或熔断阈值,并且在调整配置时不停止微服务。
* 配置修改后可自动更新。如配置内容发生变化,微服务能够自动更新配置。综上所述,对于微服务架构而言,一个通用的配置管理机制是必不可少的,常见做法是使用配置服务器管理配置。Spring Cloud Bus利用Git或SVN等管理配置、采用Kafka或者RabbitMQ等消息总线通知所有应用,从而实现配置的自动更新并且刷新所有微服务实例的配置。

Sleuth+ZipKin(跟踪服务):Sleuth和Zipkin结合使用可以通过图形化的界面查看微服务请求的延迟情况以及各个微服务的依赖情况。需要注意的是Spring Boot 2及以上不在支持Zipkin的自定义,需要到官方网站下载ZipKin相关的jar包。另外需要提一点的是Spring Boot Actuator,提供了很多监控端点如/actuator/info、/actuator/health、/acutator/refresh等,可以查看微服务的信息、健康状况、刷新配置等。

原文链接:一张图了解Spring Cloud微服务架构(作者:SimpleEasy)

从 Spring Cloud 看一个微服务框架的「五脏六腑」

齐达内 发表了文章 • 0 个评论 • 562 次浏览 • 2019-03-31 11:53 • 来自相关话题

Spring Cloud 是一个基于 Spring Boot 实现的微服务框架,它包含了实现微服务架构所需的各种组件。 注:Spring Boot 简单理解就是简化 Spring 项目的搭建、配置、组合的框架。因为与构建微服务本身没有 ...查看全部
Spring Cloud 是一个基于 Spring Boot 实现的微服务框架,它包含了实现微服务架构所需的各种组件。

注:Spring Boot 简单理解就是简化 Spring 项目的搭建、配置、组合的框架。因为与构建微服务本身没有直接关系,所以本文不对 Spring Boot 进行展开。另外本文有一些例子涉及到 Spring 和 Spring Boot,建议先了解一下 Spring 和 Spring Boot 再阅读本文。



本文的阅读对象主要是没有接触过服务架构,想对其有一个宏观的了解的同学。

本文将从 Spring Cloud 出发,分两小节讲述微服务框架的「五脏六腑」:

* 第一小节「服务架构」旨在说明的包括两点,一服务架构是什么及其必要性;二是服务架构的基本组成。为什么第一节写服务架构而不是微服务架构呢?原因主要是微服务架构本身与服务架构有着千丝万缕的关系,服务架构是微服务架构的根基。
* 第二小节「五脏六腑」则将结合 Spring Cloud 这个特例来介绍一个完整的微服务框架的组成。

#服务架构
为了方便理解,我先讲一个小故事(改编自一知乎答主):

Martin(微服务提出者也叫 Martin)刚来到公司时是一个基层员工,它上面有经理、老板,那个时候所有人都听老板的指挥。

但是过了两年,公司的人越来越多,原来的模式下整个公司的运作效率太低,管理也很混乱。

于是已经踏上中层岗位的 Martin 建议老板进行部门划分(服务化),专门的部门只做专门的事情(单一职责)。例如研发部门只做研发,人事部门只做招聘。

老板听取了 Martin 的意见,对公司的组织架构进行了调整。

有一天,Martin 发现公司的部门越来越多,各个部门并不能完全知道对方所做的事情,这对跨部门协作(服务调用)带来了困难。

行政部门会(注册中心)来记录所有的部门,每当有新的部门行政都会记录下来(服务注册),然后公布出来让所有部门知道(服务发现)。

在新的组织架构下,公司的效率逐步提高。老板也给 Martin 发了大量奖金作为奖励,Martin 从此赢取白富美走向了人生巅峰。

这是一个公司组织架构演变的故事,主要讲的是随着公司规模的扩大,组织从集中化管理到分布化管理的过程。

映射到我们的信息系统里来也是一样的,随着我们的系统越来越复杂,变得难以管理,也有人想到去拆分然后治理。在解决复杂问题上,分治可以说是一个屡试不爽的办法。

服务化即是拆解的一种手段。而上面圆括号里面的内容其实就对应了一个服务化架构的最小组成元素,分别是服务、服务调用、注册中心、服务注册、服务发现。有了这些基本的组成要素,就可以实现一个最简单的服务架构。
##面向服务的架构和微服务架构
面向服务的架构(SOA)和微服务架构是目前两种主流的服务化架构,都符合上面的例子,也有上面提到的所有组件。这两种服务架构有很多可以讲的,但是与本文的相关性不大,本文不做会过多展开,只简单介绍一下两者的区别。

准确地说微服务是去 ESB(企业服务总线)的 SOA。ESB 借鉴了计算机组成原理中的通信模型 —— 总线,所有需要和外部系统通信的系统,通过 ESB 进行标准化地转换从而消除协议、异构系统之间的差异,这样就可以利用现有的系统构建一个全新的松耦合的异构的分布式系统。微服务架构去掉 ESB,本质上是一种去中心化的思想。
#五脏六腑
##「心脏」
顺着上一节的思路,从最简单、最核心的问题出发,假设服务 A 要调用服务 B,会有什么问题?

* 服务在哪?(服务治理问题)
* 怎么调用?(服务调用问题)

这两个是最核心的问题,也是任何微服务框架首要解决的两个问题。

为了解决第一个问题 Spring Cloud 提供了 Eureka、ZooKeeper、Cloud Foundry、Consul 等服务治理框架的集成。它们的工作模式是将所有的微服务注册到一个 Server 上,然后通过心跳进行服务健康监测。这样服务 A 调用 B 时可以从注册中心拿到可用的服务 B 的地址、端口进行调用。

第二个服务调用有人可能认为就是一个简单的 HTTP 或者 RPC 调用,不是什么问题。但是在分布式的场景下,服务调用需要考虑的因素会更多。比如一个服务有多个实例,此时请求进来了交给谁处理,请求的负载怎么平衡到各个实例,都是比较棘手的问题。Spring Cloud 提供了两种服务调用的方式:一种是 Ribbon + restTemplate,另一种是 Feign。

其中 Ribbon 是基于 HTTP 和 TCP 客户端的负载均衡器,restTemplate 是 Spring 提供的 Restful 远程调用的模板,两者结合就可以达到远程调用的负载均衡。

而 Feign 是一个更加声明式的 HTTP 客户端,开发者可以像调用本地方法一样调用它,完全感觉不到是远程调用,结合 Ribbon 也可以做负载均衡。

既然两个问题都得到了解决,我们就用一个例子来进一步说明一下,例子包含了微服务中最基本的三个角色(注册中心、服务提供者、服务消费者):

注册中心

注解 @EnableEurekaServer 表示该 Spring Boot 应用是一个注册中心。
@EnableEurekaServer
@SpringBootApplication
public class EurekaserverApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaserverApplication.class, args);
}
}

eureka.client.registerWithEureka: false 和 fetchRegistry: false 来表明自己是一个 eureka server。
server:
port: 8080

eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/


service-hello 服务

注解 @EnableEurekaClient 表示他是一个 Eureka 客户端,它会在注册中心注册自己。

注解 @RestController 表示这是一个控制器,@RequestMapping("/hello") 表示匹配到请求 '/hello' 时会调用该方法进行响应。
@SpringBootApplication
@EnableEurekaClient
@RestController
public class ServiceHelloApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceHelloApplication.class, args);
}

@Value("${server.port}")
String port;
@RequestMapping("/hello")
public String home(@RequestParam String name) {
return "hello "+name+",i am from port:" +port;
}

}

注册中心的地址为 http://localhost:8080/eureka/,也就是上面我们定义的。服务名为 service-hello,将会被调用者使用。
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8080/eureka/
server:
port: 8081
spring:
application:
name: service-hello


服务消费者 service-ribbon

假设 service-ribbon 端口为 8082,当我们访问 http://localhost:8080/hello 时,HelloControler 接收到请求,并调用 HelloService 中的 helloService 方法,HelloService 中通过定义的 restTemplate 去调用 http://service-hello/hello。此处要注意的是 @LoadBalanced 注解,它表示启用负载均衡。
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceRibbonApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceRibbonApplication.class, args);
}

@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

}

@Service
public class HelloService {

@Autowired
RestTemplate restTemplate;

public String helloService(String name) {
return restTemplate.getForObject("http://service-hello/hello?name="+name,String.class);
}

}

@RestController
public class HelloControler {

@Autowired
HelloService helloService;

@RequestMapping(value = "/hello")
public String hello(@RequestParam String name){
return helloService.helloService(name);
}

}

至此其实一个微服务应用的雏形已经搭建出来了,服务治理、服务调用可以说是「五脏六腑」中的「心脏」。
##「心脏」的依托
接下来我们要进一步思考的是「五脏六腑」中其余的部分,因为少了它们人也是活不久的。下面通过一个问题或需求对应一个组件的方式进行介绍。

服务“雪崩”与断路器

由于网络等原因,服务并不能保证 100% 可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet 容器的线程资源会被消耗殆尽,导致服务瘫痪。

由于服务与服务之间存在依赖,故障会在调用链路上传播,导致整个微服务系统崩溃,这就是服务故障的“雪崩”效应。

为了解决这个问题,Spring Cloud 提供了对 Hystrix 断路器的集成,当服务调用失败的频次达到一定阈值,断路器将被开启,降级的策略可以开发者制定,一般是返回一个固定值。这样就能够避免连锁故障。

此外 Spring Cloud 还提供 Hystrix Dashboard 和 Hystrix Turbine,帮助我们进行监控和聚合监控。

服务暴露与路由网关

微服务中的服务很多,直接暴露给用户一是不安全,二是对用户不友好。因此在微服务和面向服务的架构中,通常会有一个路由网关的角色,来负责路由转发和过滤。对应到 Spring Cloud 中有 Zuul 和 Gateway 两个组件可用。

路由网关接收了所有的用户请求,有着很高的负载,因此它通常是一个集群。用户的请求会先经过一层负载均衡被发到路由网关。

服务配置与配置中心

在微服务应用中,服务数量巨多,而每个服务不同环境都有着不同的配置,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。需要注意的是此处的配置与注册中心注册的配置信息是两个概念,此处的配置是服务本身的一些配置信息,如下图:
1.png

Spring Cloud 提供了 Spring Cloud Config 组件,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程 Git 仓库中,帮助我们管理服务的配置信息。

信息同步与消息总线

前一个问题讲到了每个服务都有一些配置信息,那么配置信息更新了我们该怎么办,手动一个个去更新?当然不是,Spring Cloud 提供了 Spring Cloud Bus 组件,它通过轻量消息代理连接各个分布的节点。当配置信息更新的时候,我们只要更新一个节点的配置,这个更新就会被广播到这个分布式系统中。

问题定位与链路追踪

在微服务系统中,服务之间可以相互调用,因此我们一个请求可能会一条调用链,而整个系统会存在一张调用网,其中任意一个服务调用失败或网络超时都可能导致整个请求失败。因为调用关系的复杂,这给问题的定位造成了极大的困难,这也是必须提供服务链路追踪的原因。

Spring Cloud 为我们提供了 Spring Cloud Sleuth 组件,它能够跟进一个请求到底有哪些服务参与,参与的顺序是怎样的,从而达到每个请求的步骤清晰可见。借助服务链路追踪,我们可以快速定位问题。

至此,Spring Cloud 的所有基础组件都介绍完了。但是目前所有的组件介绍都是分散的,它们组合起来,完整的样子是什么样的?如下图:
2.jpg

偷懒偷了张图,图中漏掉了 Config Server 和链路追踪组件。但是结合上文的介绍,我们大致可以脑补出这两个东西在图中的位置。Config Server 是一个与所有服务相连的服务集群,链路追踪组件则集成在每个服务中。
#小结
服务治理为心脏,路由网关、消息中心、断路器、链路追踪、配置中心等为依托,构造了整个微服务框架的「五脏六腑」。当然,一个微服务系统远比本文所写的复杂得多,尤其是在不同的业务场景之下,因此想要更深入地了解它就需要我们不断地去实践。而作为前端,我了解这些内容一是为了更好地了解整个请求的流程,二是为了后续在 SOA 中接入 Node 子服务积累相关知识。

最后分享一句有趣的调侃 Spring 的话:在 Spring 中没有什么是一个注解解决不了的,如果有,那么就用两个注解。

原文链接:从 Spring Cloud 看一个微服务框架的「五脏六腑」

从技术演变的角度看互联网后台架构

Andy_Lee 发表了文章 • 0 个评论 • 1258 次浏览 • 2019-03-27 09:30 • 来自相关话题

本文是腾讯IEG内部做的一个面向后台开发新同学的课程,因为其他BG一些同学要求分享,所以发一下。 其实内容都是些常见开源组件的high level描述,比如Flask,Express框架,中间件的演化,Microservices的 ...查看全部
本文是腾讯IEG内部做的一个面向后台开发新同学的课程,因为其他BG一些同学要求分享,所以发一下。

其实内容都是些常见开源组件的high level描述,比如Flask,Express框架,中间件的演化,Microservices的概念,一些对NoSQL/Column based DB的概念介绍,Docker的一些简单概念等等。从单个概念来说,这只是一些科普。

但是为什么当时要开这门课呢?重点是我发现很多新入职的后台开发同学并不太清楚自己做的东西在现代互联网整体架构中处于一个什么样的角色,而在IEG内部则因为游戏开发和互联网开发的一些历史性差异,有些概念并不清晰。

拿中间件来说,很多Web application不用啥中间件一样可以跑很好,那么是不是都要上Redis?到底解决什么问题?中间件又存在什么问题?中台和中间件又是个什么关系?如果开个MQ就是中间件,微服务又是要做啥?

如果能从这十多年来互联网应用的整个Tech stack变化去看待backend architecture的一些改变,应该是一件有趣也有意思的事情。这是当时写这个PPT开课的初衷。

我不敢说我在这个PPT里面的一些私货概念就是对的,但是也算是个人这么多年的一些认知理解,抛砖引玉吧。

强调一点,这个PPT的初衷是希望从近十多年来不同时代不同热点下技术栈的变化来看看我们是如何从最早的PHP/ASP/JSP<=>MySQL这样的两层架构,一个阶段一个阶段演变到现在繁复的大数据、机器学习、消息驱动、微服务架构这样的体系,然后在针对其中比较重要的几个方面来给新入门后台开发的同学起个“提纲目录”的作用。如果要对每个方面都深入去谈,那肯定不是一两页PPT就能做到的事情。

下面我们开始。首先看第一页如下图:什么是System Design?什么是架构设计?为什么要谈架构设计?
1.jpg

之所以抛出这个问题,是因为平时常常听到两个互相矛盾的说法:一方面很多人爱说“架构师都是不干活夸夸其谈”,另一方面又有很多人苦恼限于日常业务需求开发,无法或者没有机会去从整体架构思考,不知道怎么成长为架构师。

上面PPT中很有趣的是第一句英文,翻译过来恰好可以反映了论坛上经常有人问的“如何学习架构”的问题:很多leader一来就是扔几本书(书名)给新同学,期望他们读完书就马上升级……这种一般都只会带来失望。

何为架构师?不写代码只画PPT?

不是的,架构师的基本职责是要在项目早期就能设计好基本的框架,这个框架能够确保团队成员顺利coding满足近期内业务需求的变化,又能为进一步的发展留出空间(所谓scalability),这即是所谓技术选型。如何确保选型正确?对于简单的应用,或者没有新意完全是实践过多次的相同方案,确实靠几页PPT足矣。但是对于新的领域新的复杂需求,这个需求未必都是业务需求,也包括根据团队自身特点(人员太多、太少、某些环节成员不熟悉需要剥离开)来进行新的设计,对现有技术重新分解组合,这时候就需要架构师自己编码实现原型并验证思路正确性。

要达到这样的目标难不难?难!但是现在不是2000年了,是2019年了,大量的框架(framework)、开源工具和各种best practice,其实都是在帮我们解决这件事情。而这些框架并不是凭空而来,而是在这十多年互联网的演化中因为要解决各种具体业务难点而一点一点积累进化而来。无论是从MySQL到MongoDB到Cassandra到Time Series DB,或者从Memcached到Redis,从Lucene到Solr到Elasticsearch,从离线批处理到Hadoop到Storm到Spark到Flink,技术不是突然出现的,总是站在前人的肩膀上不断演变的。而要能在浩如烟海的现代互联网技术栈中选择合适的来组装自己的方案,则需要对技术的来源和历史有一定的了解。否则就会出现一些新人张口ELK,闭口TensorFlow,然后一个简单的异步消息处理就会让他们张口结舌的现象。

20多年前的经典著作DesignPatterns中讲过学习设计模式的意义,放在这里非常经典:学习设计模式并不是要你学习一种新的技术或者编程语言,而是建立一种交流的共同语言和词汇,在方案设计时方便沟通,同时也帮助人们从更抽象的层次去分析问题本质,而不被一些实现的细枝末节所困扰。同时,当我们能把很多问题抽象出来之后,也能帮我们更深入更好地去了解现有系统-------这些意义,对于今天的后端系统设计来说,也仍然是正确的。

下图是我们要谈的几个主要方面。
2.jpg

上面的几个主题中,第一个后台架构的演化是自己从业十多年来,体会到的互联网技术架构的整体变迁。然后分成后台前端应用框架、Middleware和存储三大块谈一下,最后两节微服务和Docker则是给刚进入后台开发的同学做一些概念普及。其中个人觉得最有趣的,是第一部分后台架构的演化和第三部分的中间件,因为这两者是很好地反映了过去十多年互联网发展期间技术栈的变化,从LAMP到MEAN Stack,从各种繁复的中间层到渐渐统一的消息驱动+流处理,每个阶段的业界热点都相当有代表性。

当然,不是说Web框架、数据存储就不是热点了,姑且不说这几年Web前端的复杂化,光后端应用框架,Node的Express,Python的Django/Flask,Go在国内的盛行,都是相当有趣的。在数据存储领域,列存储和时序数据随着物联网的发展也是备受重视。但是篇幅所限,在这个课程中这些话题也就只能一带而过,因为这些与其说是技术的演变过程,不如说是不同的技术选型和方向了,比如说MySQL适合OLTP(Online Transaction Processing),而Cassandra/HBase等则适合OLAP(Online Analyical Processing),并不能说后者就优于前者。

下面我们先来看后台架构的演化。
3.jpg

严格说这是个很大的标题,从2000年到现在的故事太多了,我这里只能尽力而为从个人体验来分析。

首先是2008年以前,我把它称为网站时代。为什么这么说?因为那时候的后台开发就是写网站,而且通常是页面代码和后台数据逻辑一起写。你只要能写JSP/PHP/ASP来读写MySQL或者SQL Server,基本就能保证一份不错的工作了。
4.jpg

要强调一下,这种简单的两层结构并不能说就是落后。在现在各个企业、公司以及小团队的大量Web应用包括移动App的后端服务中,采用这种架构的不在少数,尤其是很多公司、学校、企业的内部服务,用这种架构已经足够了。

注意一个时间节点:2008。

当然,这个节点是我YY的。这个节点可以是2007,或者2006。这个时间段发生了两个影响到现在的事情:Google上市,Facebook开始推开。

我个人相信前者上市加上它发表的那三篇大数据paper影响了后来业界的技术方向,后者的火热则造成了社交成为业务热点。偏偏社交网站对大数据处理有着天然的需求,技术的积累和业务的需求就这么阴差阳错完美结合了起来,直接影响了大海那边后面的科技发展。

同时在中国,那个时候却是网络游戏MMO的黄金年代,对单机单服高并发实时交互的需求,远远压过了对海量数据Data mining的需要,在这个时间点,中美两边的互联网科技树发生了比较大的分叉。这倒是并没有优劣之说,只是业务场景的重要性导致了技能树的侧重。直到今天,单机(包括简单的多服务器方案)高并发、高QPS仍然也是国内业界所追求的目标,而在美国那边,这只是一个业务指标而已,更看重的是如何进行水平扩展(horizontal scaling)和分散压力。

国内和美国的科技树回到一条线上,大数据的业务需求和相关技术发展紧密结合起来,可能要到2014年左右,随着互联网创业的盛行,O2O业务对大数据实时处理、机器学习推荐提出了真正的需求时,才是国内业界首次出现技术驱动业务,算法驱动产品的现象,重新和美国湾区那边站在了一条线上,而这则是后话了。
5.jpg

到了2010年前后,Facebook在全球已经是现象级产品,当时微软直接放弃了Windows Live,就是为了避免在社交领域硬怼Facebook。八卦一下当时在美国湾区那边聚餐的时候,如果谁说他是Facebook的,那基本就是全场羡慕的焦点。

Facebook的崛起也带动了其他大量的社交网站开始出现,社交网站最大的特点就是频繁的用户搜索、推荐,当用户上亿的时候,这就是前面传统的两层架构无法处理的问题了。因此这就带动了中间件的发展。实际上在国外很少有人用中间件或者Middelware这个词,更多是探讨如何把各种Service集成在一起,像国内这样强行分成Frontend/Middleware/Storage的概念是没听人这么谈过的,后面中间件再说这问题。当时的一个惯例是用PHP做所谓的胶水语言(glue language),然后通过Hessian这些协议工具来把其他Java服务连接到一起。与此同时,为了提高访问速度,降低后端查询压力,Memcached/Redis也开始大量使用。基于Lucene的搜索(2010左右很多是自行开发)或者Solr也被用在用户搜索、推荐以及Typeahead这些场景中。

我记忆中在2012年之前消息队列的使用还不是太频繁,不像后来这么重要。当时常见的应该就是Beanstalkd/RabbitMQ,ZeroMQ其实我在湾区那边很少听人用,倒是后来回国后看到国内用的人还不少。Kafka在2011年已经出现了,有少部分公司开始用,不过还不是主流。
6.jpg

2013年之后就是大数据+云的时代了,如果大家回想一下,基本上国内也是差不多在2014年左右开始叫出了云+大数据的口号(2013年国内还在手游狂潮中...)。不谈国外,在中国那段时间就是互联网创业的时代,从千团大战到手游爆发到15年开始的O2O,业务的发展也带动了技术栈的飞速进步。左上角大致上也写了这个时代互联网业界的主要技术热点,实际上这也就是现在的热点。无论国内国外,绝大部分公司还并没有离开云+大数据这个时代。无论是大数据的实时处理、数据挖掘、推荐系统、Docker化,包括A/B测试,这些都是很多企业还正在努力全面解决的问题。

但是在少数站在业界技术顶端或者没有历史技术包袱的新兴公司,从某个角度上来说,他们已经开始在往下一个时代前进:机器学习AI驱动的时代
7.jpg

2018年开始,实际上可能是2017年中开始,AI驱动成了各大公司口号。上图是Facebook和Uber的机器学习平台使用情况,基本上已经全部进入业务核心。当然并不是说所有公司企业都要AI驱动,显然最近发生的波音737事件就说明该用传统的就该传统,别啥都往并不成熟的AI上堆。但另一方面,很多新兴公司的业务本身就是基于大数据或者算法的,因此他们在这个领域也往往走得比较激进。由于这个AI驱动还并没有一个很明确的定义和概念,还处于一种早期萌芽的阶段,在这里也就不多YY了。

互联网后台架构发展的简单过程就在这里讲得差不多了,然后我们快速谈一下Web开发框架。
8.png

首先在前面我提到,在后端架构中其实也有所谓的Frontend(前台)开发存在,一般来说这是指响应用户请求,实现具体业务逻辑的业务逻辑层。当然这么定义略微粗糙了些,很多中间存储、消息服务也会封装一些业务相关逻辑。总之Web开发框架往往就是为了更方便地实现这些业务逻辑而存在的。

前文提到在一段较长时间内,国内的技术热点是单机高并发高QPS,因此很多那个时代走过来的人会本能地质疑Web框架的性能,而更偏好TCP长链接甚至UDP协议。然而这往往是自寻烦恼,因为除开特别的强实时系统,无论是休闲手游、视频点播还是信息流,都已经是基于HTTP的了。
9.jpg

上图所提到的两个问题中,我想强调的是第一点:所有的业务,在能满足需求的情况下,首选HTTP协议进行数据交互。准确点说,首选JSON,使用Web API。

Why?这就是上图第一个问题所回答的:无状态、易调试易修改、一般没有80端口限制。

最为诟病的无非是性能,然而实际上对非实时应用,晚个半秒一秒不应该是大问题,要考虑的是水平扩展scalability,不是实时响应(因为前提就是非实时应用);其次实在不行你还有WebSocket可以用。
10.jpg

这一部分是简单列举了一下不同框架的使用,可以看出不同框架的概念其实差不多。重点是要注意到Middleware这个说法在Web Framework和后端架构中的意义不同。在Web Framework中是指具体处理GET/POST这些请求之前的一个通用处理(往往是链式调用),比如可以把鉴权、一些日志处理和请求记录放在这里。但在后端架构设计中的Middleware则是指类似消息队列、缓存这些在最终数据库之前的中间服务组件。
11.jpg

最后这里是想说Web Framework并不是包治百病,实际上那只是提供了基础功能的一个library,作为开发者则更多需要考虑如何定义配置文件,一些敏感参数如token、密码怎么传进来,开发环境和生产环境的配置如何自动切换,单元测试怎么搞,代码目录怎么组织。有时候我们可以用一些比如Yeoman之类的Scaffold工具来自动生成项目代码框架,或者类似Django这种也可能自动生成基本目录结构。

下面进入Middleware环节。Again,强调一下这里只是根据个人经验和感受谈谈演化过程。
12.png

13.jpg

这一页只是大致讲一下怎么定义中间件Middleware。说句题外话,在美国湾区那边提这个概念的很少,而阿里又特别喜欢说中间件,两者相互的交流非常头痛。湾区那边不少Google、Facebook还有Pinterest/Uber这些的朋友好几次都在群里问说啥叫中间件。

中间件这个概念很含糊,应该是阿里提出来的,对应于Middleware(不过似乎也不是完全对应),可能是因为早期Java的EJB那些概念里面比较强调Middleware这一点吧(个人猜的)。大致上,如果我们把Web后端分为直接处理用户请求的Frontend,最后对数据进行持久存储(persistant storage)这两块,那么中间对数据的所有处理环节都可以视为Middleware。
14.jpg

和中间件对应的另一个阿里发明的概念是中台。近一年多阿里的中台概念都相当引人注意,这里对中台不做太多描述。总体来说中台更多是偏向业务和组织架构划分,不能说是一个技术概念,也不是面向开发人员的。而中间件Middleware是标准的技术组件服务。

那么我们自然会有一个问题:为什么要用中间件?
15.jpg

谈到为什么要用Middlware,这里用推荐系统举例。

推荐系统,对数据少用户少的情况下,简单的MySQL即可,比如早期论坛的什么top 10热门话题啊,最多回复的话题啊,都可以视为简单的推荐,数据量又不大的情况下,直接select就可以了。

如果是用户推荐的话,用户量不大的情况下,也可以如法炮制,选择同一区域(城市)年龄相当的异性,最后随机挑几个给你,相信世纪佳缘之类的交友网站早期实现也就是类似的模式。
16.jpg

那么,如果用户量多了呢?每次都去搜数据库,同时在线用户又多,那对数据库的压力就巨大了。这时候就是引入缓存,Memcached、Redis就出现了。

简单的做法就是把搜索条件作为key,把结果作为value存入缓存。打个比方你可以把key存为 20:40:beijing:male(20到40岁之间北京的男性),然后把第一次搜索的结果全部打乱shuffle后,存前1000个,10分钟过期,再有人用类似条件搜索,就直接把缓存数据随机挑几个返回。放心,一般来说不会有人10分钟就把1000个用户的资料都看完了,中间偶有重复也没人在意(用世纪佳缘、百合网啥的时候看到过重复的吧)。

不过话又说回来,现代数据库,尤其是类似MongoDB/ES这些大量占用内存的NoSQL,已经对经常查询的数据做了缓存,在这之上再加cache,未必真的很有效,这需要case by case去分析了,总之盲目加cache也并不推荐。

加缓存是为了解决访问速度,减轻数据库压力,但是并不提高推荐精准度。如果我们要提高推荐效果呢?在2015年之前机器学习还没那么普及成熟的时候,我们怎么搞呢?
17.jpg

提高推荐效果,在机器学习之前有两种做法:

- 引入基于Lucene的搜索引擎,在搜索的同时通过定制方案实现scoring,比如我可以利用Lucene对用户的年龄、性别、地址等进行indexing,但是再返回结果时我再根据用户和查询者两人的具体信息进行关联,自定义返回的score(可以视为推荐相关系数)
- 采用离线批处理。固然可以用Hadoop,但是就太杀鸡用牛刀了。常见的是定时批处理任务,按某种规则划分用户群体,对每个群体再做全量计算后把推荐结果写入缓存。这种可以做很繁复准确的计算,虽然慢,但效果往往不错。这种做法也常用在手机游戏的PvP对战列表里面。

这些处理方法对社交网络/手游这类型的其实已经足够了,但是新的业务是不断出现的。随着Uber/滴滴/饿了么/美团这些需要实时处理数据的App崛起,作为一个司机,并不想你上线后过几分钟才有客人来吧,你希望你开到一个热点区域,一开机就马上接单。
18.jpg

所以这种对数据进行实时(近实时)处理的需求也带动了后端体系的大发展,Kafka/Spark等等流处理大行其道。这时候的后端体系就渐渐引入了消息驱动的模式,所谓消息驱动,就是对新的生产数据会有多个消费者,有的是满足实时计算的需求(比如司机信息需要立刻能够被快速检索到,又不能每次都做全量indexing,就需要用到Spark),有的只是为了数据分析,写入类似Cassandra这些数据库里,还有的可能是为了生成定时报表,写入到MySQL。

大数据的处理一直是业界热点领域。记得2015年硅谷一个朋友就是从一家小公司做PHP跳去另一家物联网公司做Spark相关的工作,之前还很担心玩不转,搞了两年就俨然业界大佬被Oracle挖去负责云平台。

Anyway,这时候对后端体系的要求是一方面能快速满足实时需求,另一方面又能满足各种耗时长的数据分析、Data lake存储等等,以及当时渐渐普及的机器学习模型(当时2015年初和几个朋友搞Startup,其中一个是Walmart Lab的机器学习专家,上来就一堆模型,啥数据和用户都还没有就把模型摆上来了,后来搞得非常头痛。当时没有Keras/PyTorch/tf这些,那堆模型是真心搞不太懂,但是又不敢扔,要靠那东西去包装拿投资的。)

但是我们再看上面的图,是不是感觉比较乱呢?各种系统的数据写来写去,是不是有点messy?当公司团队增多,系统复杂度越来越高的时候,我们该怎么梳理?
19.jpg

到了2017之后,前面千奇百怪的后端体系基本上都趋同了。Kafka的实时消息队列,Spark的流处理(当然现在也可以换成Flink,不过大部分应该还是Spark),然后后端的存储,基于Hive的数据分析查询,然后根据业务的模型训练平台。各个公司反正都差不多这一套,在具体细节上根据业务有所差异,或者有些实力强大的公司会把中间一些环节替换成自己的实现,不过不管怎么千变万化,整体思路基本都一致了。

这里可以看到机器学习和AI模型的引入。个人认为,Machine Learning的很大一个好处,是简化业务逻辑,简化后台流程,不然一套业务一套实现,各种数据和业务规则很难用一个整体的技术平台来完成。相比前面一页的后台架构,这一页要清晰许多,而且是一个DAG有向无环图的形式,数据流向很明确。我们在下面再来说这个机器学习对业务数据流程的简化。
20.jpg

在传统后端系统中,业务逻辑其实和数据是客观分离的,逻辑规则和数据之间并不存在客观联系,而是人为主观加入,并没形成闭环,如上图左上所示。而基于机器学习的平台,这个闭环就形成了,从业务数据->AI模型->业务逻辑->影响用户行为->新的业务数据这个流程是自给自足的。这在很多推荐系统中表现得很明显,通过用户行为数据训练模型,模型对页面信息流进行调整,从而影响用户行为,然后用新的用户行为数据再次调整模型。而在机器学习之前,这些观察工作是交给运营人员去手工猜测调整。

上图右边谈的是机器学习相关后台架构和传统Web后台的一些差别,重点是耗时太长,必须异步处理。因此消息驱动机制对机器学习后台是一个必须的设计。
21.jpg

这页是一些个人的感受,现代的后端数据处理越来越偏向于DAG的形态,Spark不说了,DAG是最大特色;神经网络本身也可以看作是一个DAG(RNN其实也可以看作无数个单向DNN的组合);TensorFlow也是强调其Graph是DAG,另外编程模式上,Reactive编程也很受追捧。

其实DAG的形态重点强调的就是数据本身是immutable(不可修改),只能transform后成为新的数据进入下一环。这个思维其实可以贯穿到现代后台系统设计的每个环节,比如Trakcing、Analytics、数据表设计、Microservice等等,但具体实施还是要case by case了。

无论如何,数据,数据的跟踪Tracking,数据的流向,是现代后台系统的核心问题,只有Dataflow和Data Pipeline清晰了,整个后台架构才会清楚。

数据库是个非常复杂的领域,在下面对几个基本常用的概念做一些介绍。注意一点是Graph database在这里没有提到,因为日常使用较少,相对来说Facebook提出的GraphQL倒是个有趣的概念,但也只是在传统DB上的一个概念封装。
22.png

23.jpg

上图是2018年12月初热门数据库的排名,我们可以看到关系数据库RDBMS和NOSQL数据库基本上平分秋色。而NoSQL中实际上又可以分为key-value storage(包括文档型)及Column based DB。
24.jpg

MySQL这个没啥好讲,大概提一下就是。有趣的是曾经看到一篇文章是AWS CTO谈的一些内容,其中印象深刻是:如果你的用户还不到100万,就别折腾了,无脑使用MySQL吧。

在2015年之前的一个趋势是不少公司使用MySQL作为数据存储,但是把indexing放在外部去做。这个思路最早似乎是Friendster提出的,后来Uber也模仿这种做法设计了自己的数据库Schemaless。然而随着PostgreSQL的普及(PostgreSQL支持对json的索引),这种做法是否还有意义就值得商榷了。
25.jpg

NoSQL最早的使用就是key-value的查找,典型的就是Redis。实际上后来的像MongoDB这些Documentbased DB也是类似的key value,只是它对Document中的内容又做了一次index(b-tree),用空间换时间来提供查找数据,这也是CS不变的思维。

MongoDB/Elasticsearch收到热捧主要是因为它们的Schemaless属性,也就是不需要提前定义数据格式,只要是json就存,还都能根据每个field搜索,这非常方便程序员快速出demo。但是实际上数据量大之后还是要规范数据结构,定义需要indexing的field的。
26.jpg

这里提一个比较好玩的开源Project NodeBB,这是个Node.js开发的论坛系统。在我前几年看到这个的时候它其实只支持Redis,然后当时因为一个项目把它改造了让他支持MySQL。去年再看的时候发现它同时支持了Redis/Postres/MongoDB,如果对比一下同样的功能他如何在这三种DB实现的,相信会很有帮助。

稍微谈谈列存储。常见MySQL你在select的时候其实往往会把整行都读出来,再在其中挑那么一两个你需要的属性,非常浪费。而MongoDB这些文件型DB,又不支持常见SQL。而列存储DB的好处就是快,不用把一行所有信息读出来,只是按列读取你需要的,对现在的大数据分析特别是OLAP(Online Analytical Processing)来说特别重要。然而据另外的说法,实际上像Casssandra/HBase这些并不是真正的列存储,而只是借用了一些概念。这个我也没深入去了解,有兴趣的同学可以自己研究研究。
27.jpg

28.jpg

列存储的一个重要领域是时序数据库,物联网用得多。其特色是大量写入,只增不改(不修改数据),但是读的次数相对于很少(想想物联网的特点,随时有数据写入,但是你不会随时都在看你家小米电器的状态。)

注意说Write/Read是正交的。这意思是每次写入是一次一行,而读是按列,加上又不会修改数据,因此各自都能保持极快的速度。

下面简单谈一下微服务,大部分直接看PPT就可以了,有几页略微谈一下个人思考。
29.jpg

30.jpg

31.jpg

32.jpg

33.jpg

上面这页说说,其实微服务所谓的服务发现/name service不要被忽悠觉得是多神奇的东西。最简单的Nginx/Apache这些都能做(域名转向,Proxy),或者你要写个name : address的对应关系到DB里面也完全可以,再配一个定时HealthCheck的服务,最简单的服务发现也就行了。

高级点用到ZooKeeper/etcd等等,或者Spring Cloud全家桶,那只是简化配置,原理都一样。从开发角度来看,微服务的开发并不是难点,难点是微服务的配置和部署。最近一段时间微服务部署也是业界热点,除了全家桶形态的Spring Cloud,也可以看看Istio这些开源工具。
34.jpg

上图主要大致对比一下,看看从早期的Spring到现在Spring Cloud的变化。想来用过Java Tomcat的朋友都能体会Java这一套Config based development的繁琐,开发的精力很多不是在业务代码上,往往会化不少精力去折腾配置文件。当然,Spring Cloud在这方面简化了不少,不过个人还是不太喜欢Java,搞很多复杂的设计模式,封装了又封装。
35.jpg

这里要说并不是微服务解决一切,热门的Python Django尽管有REST Framework,但是它实际上是一个典型的Monolithic体系。对很多核心业务,其实未必要拆开成微服务。

这两者是互补关系,不是替代关系。

下面的Docker我就不仔细谈了,PPT基本表达了我想表述的概念,主要意思是:

  • Docker能够简化部署,简化开发,能够在某种程度上让开发环境和产品环境尽量接近。
  • 不要担心Docker的性能,它不是虚拟机,可以看作在Server上运行的一个Process。

36.png

37.jpg

上图是描述Docker之前开发人员的常见开发环境,首先在自己机器上装一大堆服务,像MySQL,Redis,Tomcat啥的。也有直接在远程服务器安装环境后,多人共同登录远端开发,各自使用一个端口避免冲突……实际上这种土法炼钢的形态,在2019年的今天仍然在国内非常普及。

这种形态的后果就是在最后发布到生产环境时,不同开发人员会经历长时间的“联调”,各种端口、权限、脚本、环境设置在生产环境再来一遍…这也是过去运维人员的主要工作。
38.jpg

上一页提到的问题,并不是一定要Docker来解决。在这之前,虚拟机VM的出现,以及Vagrant这样的工具,都让开发环境的搭建多少轻松了一些。不过思路仍然是把VM作为一个独立服务器使用,只是因为快照、镜像和辅助工具,让环境的配置、统一和迁移更加简单快捷。
39.jpg

上图是对比程序运行在物理服务器、VM及Docker时的资源共享情况,可以看到运行在Docker的应用,并没有比并发运行在物理服务器上占用更多资源。

下图是简单的Docker使用,不做赘述。
40.jpg

41.jpg

这一页主要是强调Docker并不等同于虚拟机。虚拟机所占资源是独享的,比如你启动一个VM,分配2G内存,那么这个VM里不管是否运行程序都会占用2G内存。然而如果你启动一个Docker,里面运行一个简单Web服务,在不强制指定内存占用情况下,如果没有请求进入,没有额外占用内存,那么这个Docker服务对整机的内存占用几乎为0(当然仍然存在一些开销,但主要是根据该程序自身的运行状况而定)。
42.jpg

最后是Kubernetes,这里大概说说Host-Pod-Container的关系,一个Host可以是物理机或者VM,Pod不是一个Docker,而是可以看作有一个IP的……(不知道怎么形容),总之一个Pod可以包括多个Container(Docker),Pod之中的Container可以共享该Pod的资源(IP,storage等)。不过现实中似乎大多是一个Pod对一个Container。

对互联网一些热门概念和演变过程的一个很简略的描述就到这里。

原文链接:从技术演变的角度看互联网后台架构

基于Spring Cloud的微服务架构演变史

尼古拉斯 发表了文章 • 0 个评论 • 1271 次浏览 • 2019-02-22 22:23 • 来自相关话题

#导读 一段时期以来 “微服务架构 ”一直是一个热门词汇,各种技术类公众号或架构分享会议上,关于微服务架构的讨论和主题也都非常多。对于大部分初创互联网公司来说,早期的单体应用结构才是最合适的选择,只有当业务进入快速发展期,在系统压力、业务复杂度以 ...查看全部
#导读

一段时期以来 “微服务架构 ”一直是一个热门词汇,各种技术类公众号或架构分享会议上,关于微服务架构的讨论和主题也都非常多。对于大部分初创互联网公司来说,早期的单体应用结构才是最合适的选择,只有当业务进入快速发展期,在系统压力、业务复杂度以及人员扩展速度都快速上升的情况下,如何快速、稳妥有序的将整个互联网软件系统升级成微服务架构,以满足业务发展需要及技术组织的重新塑造,才是进行微服务架构的最主要的动力,否则空谈微服务架构是没有意义的。

而一旦决定将整个应用体系按照微服务架构体系进行升级,就需要有组织有计划的进行业务系统、基础架构、运维体系等多个方面的升级配套。而另一个比较尴尬的现实是,一般业务发展进入到需要进行微服务架构层面的时候,业务发展往往又都是非常迅猛的,这种业务快速发展和增长的压力往往又会给整个技术团队带来非常大的挑战,因为此时你需要取舍,是简单方案快速支撑呢?还是选择适当长远一点的方案?当然这种情况大部分是技术细节方面的问题,掌控的“度”大部分情况是掌握在具体的工程师手中。

而如何整体上确保应用体系及组织结构向微服务时代快速、有序的跨越,是一件十分考验团队能力以及架构管理水平的事。能做到80分已然算优秀了,因为这是有其客观规律的!

作者自身亲历了一个快速发展的互联网公司从单体应用~以Spring Cloud为技术栈的微服务架构体系的全过程。本文将主要从技术角度与大家探讨如何利用Spring Cloud进行微服务架构拆分,以及在这个过程中一点自己的思考。水平有限,不足之处还请包涵!
#系统架构演变概述

在公司业务初创时期,面对的主要问题是如何将一个想法变成实际的软件实现,在这个时候整个软件系统的架构并没有搞得那么复杂,为了快速迭代,整个软件系统就是由“App+后台服务”组成,而后台服务也只是从工程角度将应用进行Jar包的拆分。此时软件系统架构如下:
01.jpg

而此时整个软件系统的功能也比较简单,只有基本的用户、订单、支付等功能,并且由于业务流程没有那么复杂,这些功能基本耦合在一起。而随着App的受欢迎程度(作者所在的公司正好处于互联网热点),所以App下载量在2017年迅猛增长,在线注册人数此时也是蹭蹭往上涨。

随着流量的迅猛增长,此时整个后台服务的压力变得非常大,为了抗住压力只能不断的加机器,平行扩展后台服务节点。此时的部署架构如下:
2.png

通过这种方式,整个软件系统抗住了一波压力,然而系统往往还是会偶尔出点事故,特别是因为API中的某个接口性能问题导致整个服务不可用,因为这些接口都在一个JVM进程中,虽然此时部署了多个节点,但因为底层数据库、缓存系统都是一套,所以还是会出现一挂全挂的情况。

另一方面,随着业务的快速发展,以往相对简单的功能变得复杂起来,这些功能除了有用户看得见的、也会包括很多用户看不见的,就好像百度搜索,用户能看见的可能只是一个搜索框,但是实际上后台对应的服务可能是成百上千,如有些增长策略相关的功能:红包、分享拉新等。还有些如广告推荐相关的变现功能等。

此外,流量/业务的增长也意味着团队人数的迅速增长,如果此时大家开发各自的业务功能还是用一套服务代码,很难想象百十来号人,在同一个工程在叠加功能会是一个什么样的场景。所以如何划分业务边界、合理的进行团队配置也是一件十分迫切的事情了!

为了解决上述问题,适应业务、团队发展,架构团队决定进行微服务拆分。而要实施微服务架构,除了需要合理的划分业务模块边界外,也需要一整套完整的技术解决方案。

在技术方案的选择上,服务拆分治理的框架也是有很多,早期的有如WebService,近期的则有各种Rpc框架(如Dubbo、Thirft、Grpc)。而Spring Cloud则是基于Spring Boot提供的一整套微服务解决方案,因为技术栈比较新,并且各类组件的支撑也非常全面,所以Spring Cloud就成为了首选。

经过一系列的重构+扩展,整个系统架构最终形成了以APP为中心的一套微服务软件系统,结构如下:
3.jpg

到这里,整个软件系统就基于Spring Cloud初步完成了微服务体系的拆分。支付、订单、用户、广告等核心功能抽离成独立的微服务,与此同时各自微服务对应的数据库也按照服务边界进行了拆分。

在完成服务的拆分以后,原来功能逻辑之间的代码调用关系,转换成了服务间网络的调用关系,而各个微服务需要根据各自所承载的功能提供相应的服务,此时服务如何被其他服务发现并调用,就成了整个微服务体系中比较关键的部分,使用过Dubbo框架的同学知道,在Dubbo中服务的注册&发现是依赖于ZooKeeper实现的,而在Spring Cloud中我们是通过Consul来实现。另外在基于Spring Cloud的架构体系中,提供了配置中心(ConfigServer)来帮助各个微服务管理配置文件,而原本的API服务,随着各个功能的抽离,逐步演变成前置网关服务了。

聊到这里,基于Spring Cloud我们进行了微服务拆分,而在这个体系结构中,分别提到了Consul、ConfigServer、网关服务这几个关键组件,那么这几个关键组件具体是如何支撑这个庞大的服务体系的呢?
#SpringCloud关键组件

##Consul

Consul是一个开源的,使用Go语言开发的注册中心服务。它里面内置了服务发现与注册框架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心等多个方案。在Spring Cloud框架中还可以选择Eurke作为注册中心,这里之所以选择Consul主要原因在于Consul对异构的服务的支持,如:gRPC服务。

事实上,在后续的系统架构演进中,在某些服务模块进一步向子系统化拆分的过程中,就采用了gRPC作为子系统服务间的调用方式。例如,支付模块的继续扩张,对支付服务本身又进行了微服务架构的拆分,此时支付微服务内部就采用了gRPC的方式进行调用,而服务注册与发现本身则还是依赖于同一套Consul集群。

此时的系统架构演进如下:
4.jpg

原有微服务架构中的模块服务在规模达到一定程度或复杂性达到一定程度后,都会朝着独立的体系发展,从而将整个微服务的调用链路变的非常长,而从Consul的角度看,所有的服务又都是扁平的。

随着微服务规模的越来越大,Consul作为整个体系的核心服务组件,在整个体系中处于关键的位置,一旦Consul挂掉,所有的服务都将停止服务。那么Consul到底是什么样服务?其容灾机制又该如何设计呢?

要保证Consul服务的高可用,在生产环境Consul应该是一个集群(关于Consul集群的安装与配置可以参考网络资料),这是毫无疑问的。而在Consul集群中,存在两种角色:Server、Client,这两种角色与Consul集群上运行的应用服务并没有什么关系,只是基于Consul层面的一种角色划分。实际上,维持整个Consul集群状态信息的还是Server节点,与Dubbo中使用ZooKeeper实现注册中心一样,Consul集群中的各个Server节点也需要通过选举的方式(使用GOSSIP协议、Raft一致性算法,这里不做详细展开,在后面的文章中可以和大家单独讨论)来选举整个集群中的Leader节点来负责处理所有查询和事务,并向其他节点同步状态信息。

而Client角色则是相对无状态的,只是简单的代理转发RPC请求到Server节点,之所以存在Client节点主要是分担Server节点的压力,作一层缓冲而已,这主要是因为Server节点的数量不宜过多,因为Server节点越多也就意味着达成共识的过程越慢,节点间同步的代价也就越高。对于Server节点,一般建议3-5台,而Client节点则没有数量的限制,可以根据实际情况部署数千或数万台。事实上,这也只是一种策略,在现实的生产环境中,大部分应用只需要设置3~5台Server节点就够了,作者所在的公司一套生产集群中的Consul集群的节点配置就是5个Server节点,并没有额外再设置Client节点。

另外,在Consul集群中还有一个概念是Agent,事实上每个Server或Client都是一个consul agent,它是运行在Consul集群中每个成员上的一个守护进程,主要的作用是运行DNS或HTTP接口,并负责运行时检查和保持服务信息同步。我们在启动Consul集群的节点(Server或Client)时,都是通过Consul Agent的方式启动的。例如:
consul agent -server -bootstrap -syslog \
-ui \
-data-dir=/opt/consul/data \
-dns-port=53
-recursor=10.211.55.3
-config-dir=/opt/consul/conf \
-pid-file=/opt/consul/run/consul.pid \
-client=10.211.55.4 \
-bind=10.211.55.4 \
-node=consul-server01 \
-disable-host-node-id &

以实际的生产环境为例,Consul集群的部署结构示意图如下:
05.jpg

实际生产案例中并没有设置Client节点,而是通过5个Consul Server节点组成的集群,来服务整套生产集群的应用注册&发现。这里有细节需要了解下,实际上5个Consul Server节点的IP地址是不一样的,具体的服务在连接Consul集群进行服务注册与查询时应该连接Leader节点的IP,而问题是,如果Leader节点挂掉了,相应的应用服务节点,此时如何连接通过Raft选举产生的新Leader节点呢?难道手工切换IP不成?

显然手工切换IP的方式并不靠谱,而在生产实践中,Consul集群的各个节点实际上是在Consul Agent上运行DNS(如启动参数中红色字体部分),应用服务在连接Consul集群时的IP地址为DNS的IP,DNS会将地址解析映射到Leader节点对应的IP上,如果Leader节点挂掉,选举产生的新Leader节点会将自己的IP通知DNS服务,DNS更新映射关系,这一过程对各应用服务则是透明的。

通过以上分析,Consul是通过集群设计、Raft选举算法,Gossip协议等机制来确保Consul服务的稳定与高可用的。如果需要更高的容灾级别,也可以通过设计双数据中心的方式,来异地搭建两个Consul数据中心,组成一个异地灾备Consul服务集群,只是这样成本会更高,这就看具体是否真的需要了。
##ConfigServer(配置中心)

配置中心是对微服务应用配置进行管理的服务,例如数据库的配置、某些外部接口地址的配置等等。在Spring Cloud中ConfigServer是独立的服务组件,它与Consul一样也是整个微服务体系中比较关键的一个组件,所有的微服务应用都需要通过调用其服务,从而获取应用所需的配置信息。

随着微服务应用规模的扩大,整个ConfigServer节点的访问压力也会逐步增加,与此同时,各个微服务的各类配置也会越来越多,如何管理好这些配置文件以及它们的更新策略(确保不因生产配置随意改动而造成线上故障风险),以及搭建高可用的ConfigServer集群,也是确保微服务体系稳定很重要的一个方面。

在生产实践中,因为像Consul、ConfigServer这样的关键组件,需要搭建独立的集群,并且部署在物理机而不是容器里。在上一节介绍Consul的时候,我们是独立搭建了5个Consul Server节点。而ConfigServer因为主要是http配置文件访问服务,不涉及节点选举、一致性同步这样的操作,所以还是按照传统的方式搭建高可用配置中心。具体结构示意图如下:
06.jpg

我们可以单独通过Git来管理应用配置文件,正常来说由ConfigSeever直接通过网络拉取Git仓库的配置供服务获取就可以了,这样只要Git仓库配置更新,配置中心就能立刻感知到。但是这样做的不稳定之处,就在于Git本身是内网开发用的代码管理工具,如果让线上实时服务直接读取,很容易将Git仓库拉挂了,所以,我们在实际的运维过程中,是通过Git进行配置文件的版本控制,区分线上分支/master与功能开发分支/feature,并且在完成mr后还需要手工(通过发布平台触发)同步一遍配置,过程是将新的master分支的配置同步一份到各个ConfigServer节点所在主机的本地路径,这样ConfigServer服务节点就可以通过其本地目录获取配置文件,而不用多次调用网络获取配置文件了。

而另一方面,随着微服务越来越多,Git仓库中的配置文件数量也会越来越多。为了便于配置的管理,我们需要按照一定的组织方式来组织不同应用类型的配置。在早期所有的应用因为没有分类,所以导致上百个微服务的配置文件放在一个仓库目录,这样一来导致配置文件管理成本增加,另外一方面也会影响ConfigServer的性能,因为某个微服务用不到的配置也会被ConfigServer加载。

所以后期的实践是,按照配置的层次关系进行组织,将公司全局的项目配置抽象到顶层,由ConfigServer默认加载,而其他所有的微服务则按照应用类型进行分组(通过Git项目空间的方式分组),相同的应用放在一个组,然后这个组下单独设立一个名为Config的Git仓库来存放这个组下相关微服务的配置文件。层次结构如下:
7.png

这样应用加载配置的优先级就是“本地配置->common配置->组公共配置->项目配置”这样的顺序。例如某服务A,在项目工程的默认配置文件(“bootstrap.yml/application.yml”)中配置了参数A,同时也在本地项目配置“application-production.yml”配置了参数B,而与此同时,ConfigServer中的common仓库下的配置文件“application.yml/application-production.yml”又分别存在参数C、参数D,同时有个组叫“pay”,其下的默认配置文件“application.yml/application-production.yml”存在参数E、参数F,具体项目pay-api又存在配置文件“pay-api-production.yml”其覆盖了common仓库中参数C、参数D的值。那么此时如果该应用以“spring.profiles.active=production”的方式启动,那么其能获取到的配置参数(通过链接访问:http://{spring.cloud.config.uri}/pay-api-production.yml)就是A、B、C、D、E、F,其中C、D的参数值为pay-api-production.yml中最后覆盖的值。

而对于ConfigServer服务本身来说,需要按照这样的组织方式进行配置类型匹配,例如上述的例子中,假设还存在finance的配置仓库,而pay组下的服务访问配置中心的时候,是不需要finance空间下的配置文件的,所以ConfigServer可以不用加载。这里就需要在ConfigServer服务配置中进行一些配置。具体如下:
spring:
application:
name: @project.artifactId@
version: @project.version@
build: @buildNumber@
branch: @scmBranch@
cloud:
inetutils:
ignoredInterfaces:
- docker0
config:
server:
health.enabled: false
git:
uri: /opt/repos/config
searchPaths: 'common,{application}'
cloneOnStart: true
repos:
pay:
pattern: pay-*
cloneOnStart: true
uri: /opt/repos/example/config
searchPaths: 'common,{application}'
finance:
pattern: finance-*
cloneOnStart: true
uri: /opt/repos/finance/config
searchPaths: 'common,{application}'

通过在ConfigServer服务本身的application.yml本地配置中,设置其配置搜索方式,来实现这样的目的。
##网关服务&服务熔断&监控

通过上面两小节的内容,我们相对详细地介绍了基于Spring Cloud体系中比较关键的两个服务组件。然而在微服务架构体系中,还有很多关键的问题需要解决,例如,应用服务在Consul中部署了多个节点,那么调用方如何实现负载均衡?

关于这个问题,在传统的架构方案中是通过Nginx实现的,但是在前面介绍Consul的时候只提到了Consul的服务注册&发现、选举等机制,并没有提到Consul如何在实现服务调用的负载均衡。难道基于Spring Cloud的微服务体系中的应用服务都是单节点在提供服务,哪怕即使部署了多个服务节点?事实上,我们在服务消费方通过@EnableFeignClients注解开启调用,通过@FeignClient("user")注解进行服务调用时,就已经实现了负载均衡,为什么呢?因为,这个注解默认是会默认开启Robbin代理的,而Robbin是实现客户端负载均衡的一个组件,通过从Consul拉取服务节点信息,从而以轮询的方式转发客户端调用请求至不同的服务端节点来实现负载均衡。而这一切都是在消费端的进程内部通过代码的方式实现的。这种负载方式寄宿于消费端应用服务上,对消费端存在一定的代码侵入性,这是为什么后面会出现Service Mesh(服务网格)概念的原因之一,这里就不展开了,后面有机会再和大家交流。

另一需要解决的关键问题是服务熔断、限流等机制的实现,Spring Cloud通过集成Netflix的Hystrix框架来提供这种机制的支持,与负载均衡机制一样也是在消费端实现。由于篇幅的关系,这里也不展开了,在后面的文章中有机会再和大家交流。

此外还有Zuul组件来实现API网关服务,提供路由分发与过滤相关的功能。而其他辅助组件还有诸如Sleuth来实现分布式链路追踪、Bus实现消息总线、Dashboard实现监控仪表盘等。由于Spring Cloud的开源社区比较活跃,还有很多新的组件在不断的被集成进来,感兴趣的朋友可以持续关注下!
#微服务之运维形态

在微服务体系结构下,随着服务数量大量的增长,线上的部署&维护的工作量会变得非常大,而如果还采用原有的运维模式的话,就能难满足需要了。此时运维团队需要实施DevOps策略,开发自动化运维发布平台,打通产品、开发、测试、运维流程,关注研发效能。

另外一方面也需要推进容器化(Docker/Docker Swarm/Kubernetes)策略,这样才能快速对服务节点进行伸缩,这也是微服务体系下的必然要求。
#微服务泛滥问题

这里还需要注意一个问题,就是实施微服务架构后,如何在工程上管控微服务的问题。盲目的进行微服务的拆分也不是一件很合理的事情,因为这会导致整个服务调用链路变得深不可测,对问题排查造成难度,也浪费线上资源。
#重构问题

在早期单体架构方式向微服务架构的转变过程中,重构是一个非常好的方式,也是确保服务规范化,业务系统应用架构合理化的很重要的手段。但是,一般来说,在快速发展阶段也就意味着团队规模的迅速增长,短时间内如何让新的团队有事可做也是一件非常考验管理水平的事情,因为如果招了很多人,并且他们之间呈现一种过渡的竞争状态的话,就会出现让重构这件事变得有些功利的情况,从而导致重构不彻底、避重就轻,导致表象上看是很高大上的微服务架构,而业务系统实际上比较烂的情况。

另外,重构是在一定阶段后作出的重要决策,不仅仅是重新拆分,也是对业务系统的重新塑造,所以一定要考虑好应用软件的系统结构以及实施它们所需要付出的成本,切不可盲目!
#后记

基于Spring Cloud的微服务架构体系,通过集成各种开源组件来为整个体系服务支持,但是在负载均衡、熔断、流量控制的方面需要对服务消费端的业务进程进行侵入。所以很多人会认为这不是一件很好的事情,于是出现了Service Mesh(服务网格)的概念,Service Mesh的基本思路就是通过主机独立Proxy进行的部署来解耦业务系统进程,这个Proxy除了负责服务发现和负载均衡(不再需要单独的注册组件,如Consul)外,还负责动态路由、容错限流、监控度量和安全日志等功能。

而在具体的服务组件上目前主要是Google/IBM等大厂支持和推进的一个叫做Istio的Service Mesh标准化工作组。具体关于Service Mesh的知识,在后面的内容中再和大家交流。以上就是本文的全部内容,由于作者水平有限,还请多多包涵!

原文链接:https://mp.weixin.qq.com/s/NHVJCmVUXcAb_pAXxT8mHA

DockOne微信分享(一九〇):Spring Cloud Kubernetes容器化实践

李颖杰 发表了文章 • 0 个评论 • 1844 次浏览 • 2018-11-03 09:20 • 来自相关话题

【编者的话】随着公司业务量和产品线的增加,项目越来越多,普通运维系统架构对整个软件研发生命周期的管理越来越难,效率低下,难以统一管理。近年来Docker统一了容器标准,对于软件开发流程产生了深远的影响,Docker可以一次打包,处处运行。过去几年Kuberne ...查看全部
【编者的话】随着公司业务量和产品线的增加,项目越来越多,普通运维系统架构对整个软件研发生命周期的管理越来越难,效率低下,难以统一管理。近年来Docker统一了容器标准,对于软件开发流程产生了深远的影响,Docker可以一次打包,处处运行。过去几年Kubernetes平台发展日新月益,Kubernetes统一了容器排编王者的地位,我个人认为kubernetes可以说是对普通运维架构一次突破性的革命。

利用Kubrenets集群平台可以很方便的对容器服务进行集中管理,可以非常高效的对容器服务进行编排、调度、扩容、升级、回滚、监控、集中收集日志等,基本上把传统运维架构需要考虑的问题全部解决了,而DevOps容器化也是整个软件开发流程的必经之路,因此我们对现有老旧的运维平台进行替换,统一利用Kubernetes对所有业务进行管理。
#原有运维系统缺点

* 原有业务布署在虚拟机ECS、KVM上,脚本分散、日志分散、难于集中收集管理,监控不统一,CPU、内存、磁盘资源使用率低,运维效率极低,无法集中管理。
* 新业务布署需要开通新的虚拟机,需要单独定制监控,各种Crontab,配置脚本,效率低下,CI/CD Jenkins配置繁琐。

#Kubernetes容器化优势

* 利用Kubernetes容器平台namespaces对不同环境进行区分,建立不同dev、test、stage、prod环境,实现隔离。
* 通过容器化集中布署所有业务,实现一键布署所需环境业务。
* 统一集中监控报警所有容器服务异常状态。
* 统一集中收集所有服务日志至ELK集群,利用Kibana面板进行分类,方便开发查日志。
* 基于Kubernetes命令行二次开发,相关开发、测试人员直接操作容器。
* 基于RBAC对不同的环境授于不同的开发、测试访问Kubernetes权限,防止越权。
* 通过Jenkins统一CI/CD编译发布过程。
* 项目容器化后,整体服务器CPU、内存、磁盘、资源利用减少50%,运维效率提高60%,原来需要N个运维做的事,现在一个人即可搞定。

Kubernetes本身是一套分布式系统,要用好会遇到很多问题,不是说三天两头就能搞定,需要具备网络、Linux系统、存储,等各方面专业知识,在使用过程中我们也踩了不少坑,我们是基于二进制包的方式安装Kubernetes集群,我们Kubernetes集群版本为1.10,经过一段时间的实践,Kubernetes对于我们整个开发、测试、发布、运维流程帮助非常大,值得大力推广。
#网络方案选择

  1. Flannel是 CoreOS 团队针对 Kubernetes 设计的一个覆盖网络(Overlay Network)工具,所有节点通过flanneld节点服务同步路由,使用简单、方便、稳定,是Kubernetes入门首选。
  2. Calico是基于BGP协议的路由方案,支持ACL,部署复杂,出现问题难排查。
  3. Weave是基于UDP承载容器之间的数据包,并且可以完全自定义整个集群的网络拓扑,国内使用较少。
  4. Open vSwitch是一个生产质量的多层虚拟交换机,它旨在通过编程扩展实现大规模网络自动化,同时仍支持标准管理接口和协议,OpenShift-kubernetes平台和混合云使用比较多。

我们对各个网络组件进行过调研对比,网络方案选择的是flanneld-hostgw+ipvs,在Kubernetes 1.9之前是不支持IPVS的,kube-proxy负责所有SVC规则的同步,使用的iptables,一个service会产生N条iptables记录。如果SVC增加到上万条,iptables-svc同步会很慢,得几分钟,使用IPVS之后,所有节点的SVC由IPVS LVS来负载,更快、更稳定,而且简单方便,使用门槛低。host-gw会在所有节同步路由表,每个容器都分配了一个IP地址,可用于与同一主机上的其他容器进行通信。对于通过网络进行通信,容器与主机的IP地址绑定。flanneld host-gw性能接近Calico,相对来说Falnneld配置布署比Calico简单很多。顺便提下flanneld-vxlan这种方式,需要通过UDP封包解包,效率较低,适用于一些私有云对网络封包有限制,禁止路由表添加等有限制的平台。

Flanneld通过为每个容器提供可用于容器到容器通信的IP来解决问题。它使用数据包封装来创建跨越整个群集的虚拟覆盖网络。更具体地说,Flanneld为每个主机提供一个IP子网(默认为/ 24),Docker守护程序可以从中为每个主机分配IP。

Flannel使用etcd来存储虚拟IP和主机地址之间的映射。一个Flanneld守护进程在每台主机上运行,并负责维护etcd信息和路由数据包。

在此提一下,在使用flannled使用过程中遇到过严重bug,即租约失效,flanneld会shutdown节点网络组件,节点网络直接崩掉,解决办法是设置永久租期:https://coreos.com/flannel/docs/latest/reservations.html#reservations。
#传统业务迁移至Kubernetes遇到的问题和痛点,DevOps遇到的问题
使用Kubernetes会建立两套网络,服务之间调用通过service域名,默认网络、域名和现有物理网络是隔离的,开发,测试,运维无法像以前一样使用虚拟机,Postman IP+端口调试服务, 网络都不通,这些都是问题。

* Pod网络和物理网络不通,Windows办公电脑、Linux虚拟机上现有的业务和Kubernetes是隔离的。
* SVC网络和物理网络不通,Windows办公电脑、Linux虚拟机上现有的业务和Kubernetes是隔离的。
* SVC域名和物理网络不通,Windows办公电脑、Linux虚拟机上现有的业务和Kubernetes是隔离的。
* 原有Nginx配置太多的location路由规则,有的有几百层,不好迁移到ingress-nginx,ingress只支持简单的规则。
* SVC-NodePort访问,在所有Node上开启端口监听,占用Node节点端口资源,需要记住端口号。
* ingress-nginx http 80端口, 必需通过域名引入,http 80端口必需通过域名引入,原来简单nginx的location可以通过ingress引入。
* ingress-nginx tcp udp端口访问需要配置一个lb,很麻烦,要先规划好lb节点同样也需要访问lb端口。
* 原有业务不能停,继续运行,同时要能兼容Kubernetes环境,和Kubernetes集群内服务互相通讯调用,网络需要通。

传统虚拟机上布署服务我们只需要一个地址+端口直接访问调试各种服务,Kubernetes是否能做到不用改变用户使用习惯,无感知使用呢?答案是打通DevOps全链路,像虚拟机一样访部Kubernetes集群服务 , 我们打通Kubernetes网络和物理网络直通,物理网络的DNS域名调用Kubernetes DNS域名服务直接互访,所有服务互通。公司原有业务和现有Kubernetes集群无障碍互访。

配置一台Kubernetes Node节点机做路由转发,配置不需要太高,布署成路由器模式,所有外部访问Kubernetes集群流量都经该节点,本机IP:192.168.2.71。
vim /etc/sysctl.conf
net.ipv4.ip_forward = 1

设置全网路由通告,交换机或者Linux、Windows主机加上静态路由,打通网络。
route add -net 172.20.0.0 netmask 255.255.0.0 gw 192.168.2.71
route add -net 172.21.0.0 netmask 255.255.0.0 gw 192.168.2.71

增加DNS服务器代理,外部服务需要访问Kubernetes service域名,首先需要解析域名,Kubernetes服务只对集群内部开放,此时需要外部能调用KubeDNS 53号端口,所有办公电脑,业务都来请求KubeDNS肯定撑不住,事实上确实是撑不住,我们做过测试,此时需要配置不同的域名进行分流策略,公网域名走公网DNS,内部.svc.cluster.local走KubeDNS。

1、建立DNS代理服务器,ingress建立一个nginx-ingress服务反代KubeDNS,ingress-nginx绑定到DNS节点运行,在节点上监听DNS 53端口。
[root@master1 kube-dns-proxy-1.10]# cat tcp-services-configmap.yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: ingress-nginx
data:
53: "kube-system/kube-dns:53"
[root@master1 kube-dns-proxy-1.10]# cat udp-services-configmap.yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: udp-services
namespace: ingress-nginx
data:
53: "kube-system/kube-dns:53"
[root@master1 kube-dns-proxy-1.10]# cat ingress-nginx-deploy.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-ingress-controller-dns
namespace: ingress-nginx
spec:
replicas: 1
selector:
matchLabels:
app: ingress-nginx-dns
template:
metadata:
labels:
app: ingress-nginx-dns
annotations:
prometheus.io/port: '10254'
prometheus.io/scrape: 'true'
spec:
hostNetwork: true
serviceAccountName: nginx-ingress-serviceaccount
containers:
- name: nginx-ingress-controller-dns
image: registry-k8s.novalocal/public/nginx-ingress-controller:0.12.0
args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
# - --configmap=$(POD_NAMESPACE)/nginx-configuration
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
- --annotations-prefix=nginx.ingress.kubernetes.io
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
#- name: https
# containerPort: 443
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
nodeSelector:
node: dns

2、最简单快捷的方式是安装Dnsmasq,当然你也可以用Bind,PowerDNS,CoreDNS等改造,上游DNS配置为上一步骤增加nginx-ingress dns的地址,所有办公,业务电脑全部设置DNS为此机,dnsmasq.conf配置分流策略。
no-resolv
server=/local/192.168.1.97
server=114.114.114.114

完成以上步骤后Kubernetes pod网络、service网络、 service域名和办公网络,现有ECS、虚拟机完美融合,无缝访问,容器网络问题完美搞定。

Windows访问Kubernetes service畅通无组,开发测试,完美无缝对接。
WechatIMG470.png

1.png

#ingress-nginx服务入口接入
服务发布后最终对接的是用户,用户访问Kubernetes服务需要通过nginx或其它http服务器接入,对于服务接入我们同时使用两种不同的方案,取决于nginx location的复杂度,location规则简单的我们使用第一种方案,由于各种问题,location复杂我们使用第二种方案。

  1. client-------ingress-nginx-----upstream----podip,对于ingress-nginx官方使用的原始方案,先配置ingress规则路由,ingress对接不同的service-dns域名,ingress自动发现后端podip,通过upstream负载不同的后端podip,不同的域名路由到不同的Kubernetes后端podip,用户客户端访问流量会负载到不同的Pod上。

  1. client------nginx-------upstream------svc-----podip改造现有nginx兼容Kubernetes,对接Kubernetes service服务。对于nginx location规则过多,不能很好的兼容nginx-ingress导致使用Kubernetes非常困难,难以普及,在不变更现有nginx配置的情况下如何对接Kubernetes这是一个问题,经过前面网络打通的步骤我们所有网络的问题都已解决。现在只需改动很小部分即可兼容,由于Kubernetes podip是漂移的,IP总是会变的,nginx只能是对接SVC域名才能持久,但是nginx解析域名有个bug,只解析一次,如果在此期间删除了yaml,nginx会找不到后端svcip,所以这里要设置代理变量set $backend,设置resolver的DNS为代理DNS地址,设置解析域名时间和变量解决该问题。

location /tomcat/ {
resolver 192.168.1.97 valid=3600s;
set $backend "tomcat.dac-prod.svc.cluster.local";
error_log logs/dac_error.log error;
access_log logs/dac_access.log main;
proxy_set_header X-real-ip $remote_addr;
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_redirect off;
client_max_body_size 100M;
proxy_pass http://${backend}:9090;
}

大家可能担心Eureka和Kubernetes service有冲突,Spring Cloud本身自带服务发现Eureka,组件之间的调用通过Eureka注册调用,其实你直接布署就行了,Eureka和Service没任何冲突,和普通Java应用一样用。
#监控方案
目前使用的Kubernetes官方的Heapster,Monitoring-InfluxDB-Grafana +自定议脚本+自定义Grafana面板可以灵活报警。

监控面板按业务环境dev/test/stage/prod/对CPU/内存/网络等分类进行展示。

节点资源监控:
2.png

Pod CPU、内存、网络等监控:
3.png

监控脚本,可以很灵活跟据设定参数进行钉钉报警,报告有问题的Pod、Node,自动处理有问题的服务。
#!/bin/bash
#最大内存排除的node节点
exclude_node="node7|node1|node2|node3|master1"
exclude_pod="redis|kafka|mongo|zookeeper|Evicted|Completed"
#node使用的最大报警内存%比
node_mem_max="100"
#node最大使用cpu百分比
node_cpu_max="80"
#pod使用的最大报警内存MB
pod_mem_max="4096"
pod_top="5"
pod_top_cpu="10"
#pod的启动错误时间,单位为秒s
pod_error_m_time="120"
pyding="$HOME/k8s-dev/dingd-zabbix.python"
#pod的内存以及cpu的使用状态
pod_mem=$(/usr/local/bin/kubectl top pod --all-namespaces |sort -n -k4 )
#node的内存使用状态
node_status=$(/usr/local/bin/kubectl top node|egrep -v "${exclude_node}" |egrep -v "MEMORY%")
#pod的运行状态
pod_status=$(/usr/local/bin/kubectl get pod --all-namespaces -o wide|grep -v NAMESPACE)
#设定有问题的pod存取文件路径
alert_error_pod="/tmp/alert-error-pod.txt"
#设定最大内存占用节点上pod的文件列表路径
alert_list="/tmp/alert-mem-list.txt"
#监控cpu百分比文件输出路径
alert_node_cpu_list="/tmp/alert_node_cpu_list.txt"
#取node内存的百分比数字值
#node_pre_mem=$(echo "${node_mem}"|awk '{print $5}'|sed -e "s/%//g")
#监控node的内存百分比,列出占用内存最高的应用并重启top5应用
node_mem_mon () {
echo "${node_status}" |awk '{print $1,$5}'|sed -e "s/%//g" |while read node_name node_mem_status;do
#echo $node_name $node_mem_status
if [ "${node_mem_status}" -gt "${node_mem_max}" ];then
>${alert_list}
#找到该节点上的所有的pod名
find_pod=$(echo "${pod_status}"|egrep ${node_name}|awk '{print $2}')
#找到所有节点倒排序使用最大的内存的pod列表
for i in $(echo "${find_pod}");do
echo "${pod_mem}"|grep $i >>${alert_list}
done
date_time=`date +'%F-%T'`
echo -e "\n${node_name}最大内存超过 %${node_mem_max} 以下pod应用将被重启 ------------------\n"
cat ${alert_list}|sort -n -k 4|tail -${pod_top}
python ${pyding} "`echo -e "\n ${date_time} ${node_name}当前内存为${node_mem_status}%,最大内存超过 %${node_mem_max} 以下pod应用将被重启 ------------------\n" ;cat ${alert_list}|sort -n -k 4|egrep -v "$exclude_pod"|tail -${pod_top}` "
cat ${alert_list}|sort -n -k 4|egrep -v "$exclude_pod"|tail -${pod_top}|egrep -v "应用将被重启" | awk '{print "/usr/local/bin/kubectl delete pod "$2" -n "$1" " | "/bin/bash"}'
fi
done
}

钉钉报警图:
4.png

#Kubernetes集群yaml容器编排管理
Kubernetes通过yaml对容器进行管理,yaml配置编排文件是管理整个容器生命周期重要的一部份,管理好yaml非常重要。我开发了一套类似于Helm的模板的脚本框架,用于所有环境的yaml初始化工作 ,自己写脚本的好处就是可以灵活控制,比如哪个组件要挂载存储,共享卷,要配置私有hosts等,我可以一次性定制好,初始化时只需要init-yaml直接批量搞定,不需要每个yml单独去修改,之后就是kubectl create 直接用。

容器编排yaml文件按空间环境dev、test、stage、prod进行模板base分类,复制一套yaml模板即可生成其它各环境,容器编排按业务类型模块配置conf app-list。
[root@master1 config]# ls
public-dev_app_list.conf public-test-base.yml
public-dev-base.yml sms-test_app_list.conf
public-pretest_app_list.conf sms-test-base.yml
public-pretest-base.yml wbyh-dev_app_list.conf
public-stage_app_list.conf wbyh-dev-base.yml
public-stage-base.yml wbyh-stage_app_list.conf
public-test_app_list.conf wbyh-stage-base.yml

通过Kubernetes核心排编脚本进行init-yml初始化对应环境,生成所有Pod的yaml排编文件,每套环境可以生成环境对应的MySQL、Redis、Kafka、MongoDB等,直接启动即可调用。
[root@master1 k8s-dev]# ./k8s wbyh-stage init-yml
/root/k8s-dev/config
[root@master1 k8s-dev]# tree
wbyh-stage/
├── app
│ ├── dac-api-center
│ │ └── dac-api-center.yml
│ ├── dac-app-web
│ │ └── dac-app-web.yml
│ ├── dac-config-server
│ │ └── dac-config-server.yml
│ ├── dac-eureka-server
│ │ └── dac-eureka-server.yml
│ ├── dac-task
│ │ └── dac-task.yml
│ ├── dac-task-apply
│ │ └── dac-task-apply.yml
│ ├── dac-task-h5
│ │ └── dac-task-h5.yml
│ ├── dac-web
│ │ └── dac-web.yml
│ ├── dac-message-center
│ │ └── dac-message-center.yml
│ ├── dac-quartz-jfdata
│ │ └── dac-quartz-jfdata.yml
│ ├── dac-quartz-mach
│ │ └── dac-quartz-mach.yml
│ ├── dac-quartz-dac
│ │ └── dac-quartz-dac.yml
│ ├── dac-resources-center
│ │ └── dac-resources-center.yml
│ ├── dac-resources-item
│ │ └── dac-resources-item.yml
│ ├── dac-usercenter-web
│ │ └── dac-usercenter-web.yml
│ └── tomcat
│ └── tomcat.yml
└── stateful-sets
├── kafka
│ ├── 10kafka-config-0420yml
│ ├── 10kafka-config.yml
│ ├── 20dns.yml
│ └── 50kafka.yml
├── mongo
│ └── mongo-statefulset.yml
├── redis
│ ├── primary.yml
│ └── redis-configmap.yml
└── zookeeper
├── 10zookeeper-config.yml
├── 30service.yml
└── 50pzoo.yml

22 directories, 26 files

通过Kubernetes脚本调用kubectl可以直接批量创建该空间下所有服务。
[root@master1 k8s-dev]# ./k8s wbyh-stage create_all
/root/k8s-dev/config
configmap "dac-eureka-server-filebeat-config" created
service "dac-eureka-server" created
deployment.extensions "dac-eureka-server" created
configmap "dac-config-server-filebeat-config" created
service "dac-config-server" created
deployment.extensions "dac-config-server" created
configmap "tomcat-filebeat-config" created
service "tomcat" created
deployment.extensions "tomcat" created

所有代码存入GitLab做版本管理,即基础设施即代码。
    add svn-jar-version ll item

commit 29dc05530d839c826130eef81541ce96a155107b
Author: idea77
Date: Thu Sep 20 16:11:00 2018 +0800

mod ossfs to /Rollback/oss

commit 880bcd9483a6ee1f5ca440fef017b30ba7cd14fe
Author: idea77
Date: Wed Sep 19 16:57:43 2018 +0800

#存储方案
目前公司一部份应用挂载的卷为NFS,读写要求不高的可以配置NFS, 一部份要求比较高的用的Ceph,如MySQL、Kafka之类的就需要Ceph支撑,对于需要持久化的DB类型存储的管理用StorageClass存储类对接管理,很方便自动建立存储卷PV-PVC对接,共享卷类型可以直接挂载卷。

NFS配置需要在每个Node节点安装NFS-Utils,配置yml,注意CentOS 7低版本3.10内核的nfs-server有bug,导致服务器重启,升到4.0以上内核解决问题。
      - name: tomcat-img
nfs:
path: /home/k8s-nfs-data/dac-test-tomcat-img
server: 192.168.8.30

Ceph Kubernetes Node节点安装ceph-commo,配置StorageClass。
ceph-class.yaml
apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
name: ceph-db
provisioner: kubernetes.io/rbd
parameters:
monitors: 192.168.1.31:6789
adminId: admin
adminSecretName: ceph-secret
adminSecretNamespace: kube-system
pool: rbd
userId: admin
userSecretName: ceph-secret

#Jenkins CI/CD编译发布阶段
Jenkins CI/CD控制台完成整个jar包编译,Dockerfile编译、docker push、Kubernetes deployment镜像滚动升级功能。

Jenkins Manage and Assign Roles授权不同的开发、测试组不同的用户权限,隔离不同的项目编译发布权限。
5.png

目前没有完全用上流水线服务,完全流水线需要构建不报错,一报错也就无法完成,不是很灵活,构建jar包和发布docker-image是分开的,需要跟据公司业务来。

编译阶段我们做了钉钉通知,每个项目拉了自己的群,编译jar包是否成功整个组都有通知,同样update发布是否成功都有提示,群内可见。
6.png

目前我们Kubernetes容器启动分为两种架构:

  1. 容器发布后启动基础JDK镜像,Wget去http服务器下载对应目录编译好的jar包,然后启动,即无镜像模式,适合频繁发布类型的业务,push jar to oss有一部份业务是跑虚拟机,需要jar包,oss可以做共享。
  2. 容器发布按照标准的方式打image update-imae模式,适合出错及时回滚的业务,即编译dockerfile-push-docke-image-update-deployment。

build-$namespace通过空间变量名拟写对应脚本,基本是做一个通用模板base,复制生成对应项目的build.sh供Jenkins传参调用,每套环境有自己的基础镜像base,基础镜像就是打入JDK等一些私有的配置,编译的时候在基础镜像上加上jar包。
if [[ $MY_POD_NAMESPACE =~ -dev ]];then
#定义启动基础镜相
base_image="registry-k8s.novalocal/public/yh-centos7-jdk-1.8"
#定义APP镜像仓库地址
image_path="registry-k8s.novalocal/xl_public/$MY_POD_NAMESPACE/${APP}"
elif [[ $MY_POD_NAMESPACE =~ "-test" ]];then
#定义启动基础镜相
base_image="registry-k8s.novalocal/public/yh-centos7-jdk-1.8"
#定义APP镜像仓库地址
image_path="registry-k8s.novalocal/xl_public/$MY_POD_NAMESPACE/${APP}:${date_time}"
elif [[ $MY_POD_NAMESPACE =~ -stage ]];then
#定义启动基础镜相
base_image="registry-k8s.novalocal/xl_public/wbyh-base/centos7-jdk-1.8"
#定义idc镜相仓库路径
image_path="registry.cn-hangzhou-idc.com/xl_dac/wbyh-stage-${APP}:${date_time}"
vpc_image_path="registry-vpc.cn-hangzhou-idc.com/wbyh-stage-${APP}:${date_time}"
fi
#初始化dockerfile
init_dockerfile () {
#生成Dockerfile
cd /Rollback/build-docker/
echo "" >$MY_POD_NAMESPACE/${APP}/Dockerfile
#生成基础镜像地址
echo -e "${base_image}" >>${MY_POD_NAMESPACE}/${APP}/Dockerfile
#生成docker作者
echo -e "MAINTAINER idea77@qq.com" >>${MY_POD_NAMESPACE}/${APP}/Dockerfile
echo -e "USER root" >>${MY_POD_NAMESPACE}/${APP}/Dockerfile
#获取启动脚本
\cp -f start-sh/${MY_POD_NAMESPACE}-sh/${APP}.sh $MY_POD_NAMESPACE/${APP}/
echo -e "ADD ./${APP}.sh /home/deploy/" >>${MY_POD_NAMESPACE}/${APP}/Dockerfile
#添加 jar包到/home/deploy/
echo -e "${add_jar}" >>${MY_POD_NAMESPACE}/${APP}/Dockerfile
#暴露端口
echo -e "EXPOSE 9090" >>${MY_POD_NAMESPACE}/${APP}/Dockerfile
#添加docker入口启动文件
\cp -f start-sh/templates/docker-entrypoint.sh $MY_POD_NAMESPACE/${APP}/
echo -e "ADD ./docker-entrypoint.sh /docker-entrypoint.sh" >>$MY_POD_NAMESPACE/$APP/Dockerfile
echo -e "RUN chown -R deploy:deploy /home/deploy && chown -R deploy:deploy /docker-entrypoint.sh && ls -t --full /home/deploy " >>$MY_POD_NAMESPACE/$APP/Dockerfile
echo -e "USER deploy" >>$MY_POD_NAMESPACE/$APP/Dockerfile
echo -e 'ENTRYPOINT ["/docker-entrypoint.sh"]' >>$MY_POD_NAMESPACE/$APP/Dockerfile

if [[ ${MY_POD_NAMESPACE} =~ -prod ]];then
docker images |grep xl_prod|grep ${APP}|awk '{print $1":"$2}'|xargs docker rmi -f
else
docker images |grep min-test|grep ${APP}|awk '{print $1":"$2}'|xargs docker rmi -f
fi
name="${MY_POD_NAMESPACE},build ${image_path}-${svn_version}"
cd /Rollback/build-docker/$MY_POD_NAMESPACE/$APP/
docker build --no-cache -t ${image_path}-${svn_version} .
check
if [[ $MY_POD_NAMESPACE =~ -stage ]];then
#vpc专有镜相地址修改到yml文件
sed -i "s@registry-vpc.cn-hangzhou-idc.com/xl_public\(.*\)@${vpc_image_path}-${svn_version}@g" /home/deploy/k8s-dev/${MY_POD_NAMESPACE}/app/$APP/$APP.yml

elif [[ $MY_POD_NAMESPACE =~ -test ]];then
sed -i "s@registry-k8s.novalocal/xl_public/\(.*\)@${image_path}-${svn_version}@g" /home/deploy/k8s-dev/${MY_POD_NAMESPACE}/app/$APP/$APP.yml

fi

name="push ${APP}"
docker push ${image_path}-${svn_version}
check
}

Jenkins触发:
7.png

build-----push------updae-deployment-----image,整个过程是流水线形式,一次性连续完成,完成后通过机器人通知到各业务组,中间有任何问题,机器人会告诉我们在哪个阶段出错,很方便排查问题,镜像的版本号根据Git或SVN的版本号来获取,然后加上当前时间戳,在jar包编译阶段版本号会写入特定文件,Jenkins会跟据当前编译的版本生成对应的Docker镜像版本。
8.png

#Kubernetes日志方案
普通虚拟机日志分散,难管理,需要登陆虚拟机一个个查看,利用Kubernetes Pod多容器策略可以很方便帮我们收集管理日志,日志方案有几种。

  1. 应用打到docker stdout前台输出,Docker输出到/var/lib/containers,通过Filebeat、Fluentd、DaemonSet组件收集,这种对于小量日志还可以,大量日志性能很差,写入很慢。
  2. Pod挂载host-path把日志打到宿主机,宿主机启动Filebeat、Fluentd、DaemonSet收集,无法判断来自哪个容器,哪个Pod和namespace空间。
  3. Pod的yml中定义两个container,同时启动一个附加的Filebeat,两个container挂载一个共享卷来收集日志。

我们用第三种方案,通过一个附加容器Filebeat来收集所有日志,filebeat–kakfa–logstash–es,自定义编译Filebeat容器镜像,为Filebeat打上podip空间service名等标签,方便识别来自哪个容器,哪个namespace,配置config-map以及yaml。
filebeat----kafkacluster-----logstash----es
apiVersion: v1
kind: ConfigMap
metadata:
namespace: dac-prod
name: dac-config-server-filebeat-config
data:
filebeat.yml: |
filebeat.prospectors:
- input_type: log
fields:
namespace: dac-prod
service-name: dac-config-server
#pod-ip:
paths:
- "/mnt/*.log"
multiline:
pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}'
negate: true
match: after
#output.elasticsearch:
output.kafka:
hosts: ["10.31.222.108:9092", "10.31.222.109:9092", "10.31.222.110:9092"]
topic: applog
required_acks: 1
compression: gzip
# Available log levels are: critical, error, warning, info, debug
logging.level: info
---
apiVersion: v1
kind: Service
metadata:
name: dac-config-server
namespace: dac-prod
spec:
ports:
- port: 9090
name: http
selector:
app: dac-config-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dac-config-server
namespace: dac-prod
labels:
app: dac-config-server
spec:
replicas: 1
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
type: RollingUpdate
selector:
matchLabels:
app: dac-config-server
template:
metadata:
labels:
app: dac-config-server
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- dac-config-server
topologyKey: "kubernetes.io/hostname"
imagePullSecrets:
- name: myregistrykey
containers:
- image: registry-vpc.cn-hangzhou-idc.com/dac-prod-dac-config-server:v1
name: dac-config-server
imagePullPolicy: Always
resources:
limits:
cpu: 4000m
memory: 4096Mi
requests:
cpu: 150m
memory: 1024Mi
env:
- name: APP
value: dac-config-server
#public
- name: JAVA_OPTS
value: "-Xms4g -Xmx4g"
- name: CONTAINER_CORE_LIMIT
value: "4"
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: MY_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
readinessProbe:
tcpSocket:
port: 9090
initialDelaySeconds: 60
timeoutSeconds: 3
livenessProbe:
tcpSocket:
port: 9090
initialDelaySeconds: 60
timeoutSeconds: 3
ports:
- name: http
containerPort: 9090
volumeMounts:
#- name: opt-data
#mountPath: /home/deploy
- name: logs
mountPath: /home/deploy/logs
- name: host-time
mountPath: /etc/localtime
readOnly: true
- image: registry-vpc.cn-hangzhou-idc.com/dac_prod/filebeat:6.0.0
name: filebeat
imagePullPolicy: Always
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
volumeMounts:
- name: logs
mountPath: /mnt
- name: filebeat-conf
mountPath: /etc/filebeat
- name: host-time
mountPath: /etc/localtime
readOnly: true
nodeSelector:
node: public
volumes:
- name: logs
emptyDir: {}
- name: filebeat-conf
configMap:
name: dac-config-server-filebeat-config
#- name: opt-data
#nfs:
#path: /home/k8s-nfs-data/public-dev-base
#server: 10.10.1.30
- name: host-time
hostPath:
path: /etc/localtime

Filebeat收集日志打上关键字标签,namespace,svc,podip等。
9.png

Kibana集中日志展示,建立Dashboard分类,用户可以按namespce分类不同环境,过滤选择查看不同模块的应用日志。
10.png

#RBAC+二次开发Kubernetes脚本
简化kubectl 命令,提供给研发团队使用。实际上这里功能和Jenkins以及Kibana上是重复的,但是必需考虑到所有团队成员的使用感受,有人喜欢命令行,有人喜欢界面,简单好用就够。我打个比方,比如看日志,有人可能喜欢用命令行tail -f看日志,用grep过滤等,有人喜欢用Kibana看,那怎么办?于是就有了两种方案,喜欢用图形界面用Jenkins或Kibana,想用命令可以用命令操作,满足你一切需求。统一集中通过指定的机器提供给开发、测试、运维、使用,方便调试、排障。通过统一的入口可以直接对容器进行服务创建、扩容、重启、登陆、查看日志、查看Java启动参数等,方便整个团队沟通。

在这里我们通过Kubernetes RBAC授权身份认证,生成不同的证书configkey,授于不同项目组不同的管理权限,不同的项目组只有自己项目的权限。权限做了细分,不同研发、测试团队互不干扰。
11.png

[deploy@185 app]# k8s dac-test  get_all
NAME READY STATUS RESTARTS AGE IP NODE
accountant-3536198527-dtrc9 2/2 Running 0 21h 172.20.1.5 node3.k8s.novalocal
analyzer-1843296997-vz9nc 2/2 Running 0 21h 172.20.87.15 node5.k8s.novalocal
api-1260757537-gxrp2 2/2 Running 0 21h 172.20.71.6 k8s-monitor.novalocal
calculator-1151720239-pr69x 2/2 Running 0 21h 172.20.1.12 node3.k8s.novalocal
consul-0 1/1 Running 0 21h 172.20.87.3 node5.k8s.novalocal
dispatcher-2608806384-kp433 2/2 Running 0 21h 172.20.4.6 lb1.k8s.novalocal
geo-1318383076-c7th2 2/2 Running 0 5m 172.20.94.6 node6.k8s.novalocal
greeter-79754259-s3bs2 2/2 Running 0 21h 172.20.19.5 jenkins-master.k8s.novalocal
kafka-0 1/1 Running 0 21h 172.20.1.4 node3.k8s.novalocal
mqtt-0 1/1 Running 0 21h 172.20.94.15 node6.k8s.novalocal
mysql-0 2/2 Running 0 21h 172.20.47.7 elk-k8sdata.novalocal
pusher-2834145138-lfs21 2/2 Running 0 21h 172.20.19.6 jenkins-master.k8s.novalocal
recovery-261893050-70s3w 2/2 Running 0 21h 172.20.32.13 node4.k8s.novalocal
redis-0 1/1 Running 0 21h 172.20.4.5 lb1.k8s.novalocal
robot-1929938921-6lz6f 2/2 Running 0 21h 172.20.47.8 elk-k8sdata.novalocal
scheduler-3437011440-rsnj6 2/2 Running 0 21h 172.20.5.10 db.k8s.novalocal
valuation-2088176974-5kwbr 2/2 Running 0 21h 172.20.94.20 node6.k8s.novalocal
zookeeper-0 1/1 Running 0 21h 172.20.4.4 lb1.k8s.novalocal


注意,如何操作用户自己有权限的空间,必需填写default-namespace.conf
注意,当gitlab master分支有合并的时候,目前我们ci自动会构建编译最新的jar版本,推送至nexus仓库,k8s容器里的jar包可以指定更新

k8s init-yml #初始化生成用户自己本人的yml文件
k8s get_all #查看用户自己本人空间下的所有运行的容器
k8s create_all #创建用户自己本人所有服务
k8s delall_app #删除本人空间下所有app服务,除基础服务mysql、 consul、 kafka、 redis、 zookeeper、mqtt 以外的所有服务
k8s apply api #修改了用户自己本人yml配置文件,应用配置生效
k8s create api #用户自己本人空间下创建一个api服务
k8s delete api #用户自己本人空间下删除一个api服务
k8s scale api 2 #用户自己本人空间下把api服务扩容成2个pod
k8s login api #用户本人空间下登录api所在的docker容器

k8s logs api #用户自己本人空间用tail -f 命令的方式查看容器内/home/deploy/api/logs/api.log 的日志
k8s error-logs api #用户自己本人空间用tail -f 命令的方式查看容器内/home/deploy/api/logs/api.error.log 的日志
k8s clean api #如果编译出错,在用户自己本人空间用gradlew clean清理命令的方式清理编译
k8s push_jar #更新本人空间下所有容器的jar包版本,重启所有容器,默认拉取backend / push-envelope -git最终版本,该版本为合并编译成功后的最新版本号
k8s push_jar 20170927-1731 #选择指定的jar版本号20170927-1731 进行更新 ,重启所有容器
k8s reinit-mysql #重新更新所有容器jar版本后api无法启动,清空用户空间下的数据库,重新创建导入数据

批量操作
k8s scale api-geo 2 #在dev用户下把api和geo 扩容
k8s delete api-geo #在dev用户下删除api 和geo服务
k8s create api-geo #在dev用户下创建api和geo服务

所有人员通用命令,要操作某个用户的资源,必需先生成所需要的yml文件
但是必需指定第二个参数名dev test stage等。

k8s stage init-yml #初始化生成stage用户的yml文件 注意要操作stage用户的容器要先成配置文件
k8s test init-yml #初始化生成test空间的yml文件
k8s dev init-yml #初始化生成dev空间的yml文件
k8s dev get_all #查看dev用户空间下的所有运行的容器
k8s dev create_all #创建dev空间下所有服务
k8s dev delall_app #删除dev空间下的app服务,除基础服务mysql、 consul、 kafka、 redis、 zookeeper、mqtt 以外的所有服务
k8s dev apply api #修改了yml配置文件,应用配置生效
k8s dev create api #dev空间下创建一个api服务
k8s dev delete api #dev空间下删除一个api服务
k8s dev scale api 2 #dev空间下把api服务扩容成2个pod
k8s dev login api #dev空间下登录api所在的docker容器
k8s dev logs api #dev空间用tail -f 命令的方式查看容器内/home/deploy/api/logs/api.log 的日志
k8s dev error-logs api #dev空间用tail -f 命令的方式查看容器内/home/deploy/api/logs/api.error.log 的日志
k8s dev push_jar #更新dev空间下所有容器的jar包版本,重启所有容器,默认拉取backend /-git最终版本,该版本为合并编译成功后的最新版本号
k8s dev push_jar 20170927-1731 #选择指定的jar版本号20170927-1731 进行更新 ,重启所有容器
k8s dev clean api #如果编译出错,dev用户空间用gradlew clean清理命令的方式清理编译
k8s dev reinit-mysql #重新更新所有容器jar版本后api无法启动,清空dev空间下的数据库,重新创建导入数据

批量操作
k8s dev scale api-geo 2 #在dev空间把api和geo 扩容
k8s dev delete api-geo #在dev空间删除api 和geo服务
k8s dev create api-geo #在dev空间下创建api和geo服务


管理员专用命令,注意管理员第二个参数一定要填
k8s dev create_rsync #创建dev空间的rsync配置
k8s dev create_passwd #创建dev空间的解压密码下发密钥
k8s dev create rbac #创建dev空间的集群授权认证
k8s dev delete rbac #删除dev空间的集群授权认证
k8s dev delete_all #删除dev空间下所有服务

#Kubernetes集群规划和问题总结
1、集群资源规划request +limit+maxpods+eviction参数,需要计算好再配置,配置有问题可能导致资源利用不均衡,一部节点资源利用过高,一部节点资源利用过低。

2、Kubernetes Node节点一定要留有足够的磁盘空间,跟据Pod个数和image大小决定磁盘空间数。

3、JDK无法获取正确的CPU数,默认获取的是宿主机CPU,会致创建的线程数过多,系统崩溃,可以通过:https://github.com/obmarg/libsysconfcpus.git 解决。
if [ "x$CONTAINER_CORE_LIMIT" != "x" ]; then
LIBSYSCONFCPUS="$CONTAINER_CORE_LIMIT"
if [ ${LIBSYSCONFCPUS} -lt 2 ]; then
LIBSYSCONFCPUS=2
fi
export LIBSYSCONFCPUS
fi
export LD_PRELOAD="/usr/local/lib/libsysconfcpus.so:$LD_PRELOAD"

4、nfs-server一定要用async,充份利用缓存加快写入速度,注意内核版本bug。

5、应用产生的日志必需要设置轮转数和大小,防止过大日志撑暴宿主机磁盘。

6、发布版本越多,随着下载镜像版本越来越多,磁盘会撑爆,合理配置kubelet image gc参数,配置gc回收优化磁盘空间。

7、Docker CE以前的版本经常会出现Docker失控,使用过程中整个节点容器无法删除,无法创建,只能重启,对业务影响很大,建议全部更新到18-CE版本,和Kubernetes容性更好。

8、节点的亲和性和反亲和Affinity一定要提前规划好,为了达到高可用目的,多副本必需配置。

9、应用异常检测,跟据实际情况配置探针ReadinessProbe、LivenessProbe防止应用假死,Kubernetes提前剔除有问题的Pod容器。
#Q&A
Q:使用NFS有存在性能瓶颈或单点故障的问题吗,如何解决,对于持久化要求高的Redis应该采用哪种存储?

A:具体看你的规模数量,测试、开发环境,单节点NFS毫无压力,数据是先写到缓存内存,速度很快,我文章中的说的内核注意bug,没必要做高可用,公有云有NAS服务,不必担心,自建机房可以用drbd Keepalived vip。



Q:为什么网络没有使用Traefik,Spring Cloud的相关组件是怎么部署的,是用yaml文件还是使用Helm方式?

A:考虑到Traefik性能没有nginx好,所以用nginx,ymal是自己写的模板生成的,没有用Helm。我们正在调研,Eureka可以单独定制多个yml互相注册。与外部服务通过打通网络直通,通过SVC对接。



Q:请问下所有环境都在一个集群,压测怎么办?

A:压测只是对应用产生压力,你可以把需要压测的应用调度到不同的节点NodeSelecto隔离运行。



Q:对于局域网微信回调是如何做,没有公网IP?

A:打通网络之后,设置WIFI指向DNS为Kubernetes DNS,Service直接互通。



Q:Eureka注册时服务IP用的什么?

A:Kubernetes集群内会用的podip去注册。



Q:有状态应用的场景,使用容器部署与传统部署有啥区别,容器部署是否存在一些坑?

A:有状态容器创建后,尽量少动,少迁移,遇到过卡住,容器无法迁移或删除,重要的MySQL之类的建议放外部运行。



以上内容根据2018年10月30日晚微信群分享内容整理。分享人涂小刚,新浪爱问普惠科技容器平台负责人,负责Kubernetes容器平台的推广与建设。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

DockOne微信分享(一八五):Nepxion Discovery:Spring Cloud灰度发布神器

DarkForces. 发表了文章 • 0 个评论 • 1840 次浏览 • 2018-08-19 09:46 • 来自相关话题

【编者的话】Nepxion Discovery是一款对Spring Cloud服务注册发现和负载均衡的增强中间件,其功能包括灰度发布(包括切换发布和平滑发布),黑/白名单的IP地址过滤,限制注册,限制发现等,支持Eureka、Consul和Zookeeper, ...查看全部
【编者的话】Nepxion Discovery是一款对Spring Cloud服务注册发现和负载均衡的增强中间件,其功能包括灰度发布(包括切换发布和平滑发布),黑/白名单的IP地址过滤,限制注册,限制发现等,支持Eureka、Consul和Zookeeper,支持Spring Cloud Api Gateway(Finchley版)、Zuul网关和微服务的灰度发布,支持用户自定义和编程灰度路由策略,支持多数据源的数据库灰度发布等客户特色化灰度发布,支持Nacos和Redis为远程配置中心,同时兼容Spring Cloud Edgware版和Finchley版。
#现有Spring Cloud微服务痛点

  1. 如果你是运维负责人,是否会经常发现,你掌管的测试环境中的服务注册中心,被一些不负责的开发人员把他本地开发环境注册上来,造成测试人员测试失败。你希望可以把本地开发环境注册给屏蔽掉,不让注册。
  2. 如果你是运维负责人,生产环境的某个微服务集群下的某个实例,暂时出了问题,但又不希望它下线。你希望可以把该实例给屏蔽掉,暂时不让它被调用。
  3. 如果你是业务负责人,鉴于业务服务的快速迭代性,微服务集群下的实例发布不同的版本。你希望根据版本管理策略进行路由,提供给下游微服务区别调用,例如访问控制快速基于版本的不同而切换,例如在不同的版本之间进行流量调拨。
  4. 如果你是业务负责人,希望灰度发布功能可以基于业务场景特色定制,例如根据用户手机号进行不同服务器的路由。
  5. 如果你是DBA负责人,希望灰度发布功能可以基于数据库切换上。
  6. 如果你是测试负责人,希望对微服务做A/B测试,那么通过动态改变版本达到该目的。

#Spring Cloud微服务痛点场景表现
##黑/白名单的IP地址注册的过滤
开发环境的本地微服务(例如IP地址为172.16.0.8)不希望被注册到测试环境的服务注册发现中心,那么可以在配置中心维护一个黑/白名单的IP地址过滤(支持全局和局部的过滤)的规则。

我们可以通过提供一份黑/白名单达到该效果。
##最大注册数的限制的过滤
当某个微服务注册数目已经达到上限(例如10个),那么后面起来的微服务,将再也不能注册上去。
##黑/白名单的IP地址发现的过滤
开发环境的本地微服务(例如IP地址为172.16.0.8)已经注册到测试环境的服务注册发现中心,那么可以在配置中心维护一个黑/白名单的IP地址过滤(支持全局和局部的过滤)的规则,该本地微服务不会被其他测试环境的微服务所调用。

我们可以通过推送一份黑/白名单达到该效果。
##多版本访问的灰度控制
A服务调用B服务,而B服务有两个实例(B1、B2),虽然三者有相同的服务名,但功能上有差异,需求是在某个时刻,A服务只能调用B1,禁止调用B2。在此场景下,我们在application.properties里为B1维护一个版本为1.0,为B2维护一个版本为1.1。

我们可以通过推送A服务调用某个版本的B服务对应关系的配置,达到某种意义上的灰度控制,改变版本的时候,我们只需要再次推送即可。
##多版本权重的灰度控制
上述场景中,我们也可以通过配对不同版本的权重(流量比例),根据需求,A访问B的流量在B1和B2进行调拨。
##多数据源的数据库灰度控制
我们事先为微服务配置多套数据源,通过灰度发布实时切换数据源。
##动态改变微服务版本
在A/B测试中,通过动态改变版本,不重启微服务,达到访问版本的路径改变。

用户自定义和编程灰度路由策略,可以通过非常简单编程达到如下效果:

* 我们可以在网关上根据不同的Token查询到不同的用户,把请求路由到指定的服务器。
* 我们可以在服务上根据不同的业务参数,例如手机号或者身份证号,把请求路由到指定的服务器。

#Nepxion Discovery概念和功能介绍
Nepxion Discovery是一款对Spring Cloud服务注册发现和负载均衡的增强中间件,其功能包括灰度发布(包括切换发布和平滑发布),黑/白名单的IP地址过滤,限制注册,限制发现等,支持Eureka、Consul和Zookeeper,支持Spring Cloud Api Gateway(Finchley版)、Zuul网关和微服务的灰度发布,支持用户自定义和编程灰度路由策略,支持Nacos和Redis为远程配置中心,同时兼容Spring Cloud Edgware版和Finchley版。

Nepxion Discovery包含功能:

实现对基于Spring Cloud的微服务和Spring Cloud Api Gateway(F版)和Zuul网关的支持:

  1. 具有极大的灵活性——支持在任何环节做过滤控制和灰度发布。
  2. 具有极小的限制性——只要开启了服务注册发现,程序入口加了@EnableDiscoveryClient。
  3. 具有极强的可用性——当远程配置中心全部挂了,可以通过Rest方式进行灰度发布。

实现服务注册层面的控制:

  1. 基于黑/白名单的IP地址过滤机制禁止对相应的微服务进行注册。
  2. 基于最大注册数的限制微服务注册。一旦微服务集群下注册的实例数目已经达到上限,将禁止后续的微服务进行注册。

实现服务发现层面的控制:

  1. 基于黑/白名单的IP地址过滤机制禁止对相应的微服务被发现。
  2. 基于版本号配对,通过对消费端和提供端可访问版本对应关系的配置,在服务发现和负载均衡层面,进行多版本访问控制。
  3. 基于版本权重配对,通过对消费端和提供端版本权重(流量)对应关系的配置,在服务发现和负载均衡层面,进行多版本流量调拨访问控制。

实现灰度发布:

  1. 通过版本的动态改变,实现切换灰度发布。
  2. 通过版本访问规则的改变,实现切换灰度发布。
  3. 通过版本权重规则的改变,实现平滑灰度发布。

实现通过XML或者Json进行上述规则的定义。

实现通过事件总线机制(EventBus)的功能,实现发布/订阅功能:

  1. 对接远程配置中心,集成Nacos和Redis,异步接受远程配置中心主动推送规则信息,动态改变微服务的规则。
  2. 结合Spring Boot Actuator,异步接受Rest主动推送规则信息,动态改变微服务的规则,支持同步和异步推送两种方式。
  3. 结合Spring Boot Actuator,动态改变微服务的版本,支持同步和异步推送两种方式。
  4. 在服务注册层面的控制中,一旦禁止注册的条件触发,主动推送异步事件,以便使用者订阅。

实现通过Listener机制进行扩展:

  1. 使用者可以对服务注册发现核心事件进行监听,实现通过扩展,用户自定义和编程灰度路由策略。

实现通过扩展,用户自定义和编程灰度路由策略:

  1. 使用者可以实现跟业务有关的路由策略,根据业务参数的不同,负载均衡到不同的服务器。
  2. 使用者可以根据内置的版本路由策略+自定义策略,随心所欲的达到需要的路由功能。

实现支持Spring Boot Admin的集成。

实现支持未来扩展更多的服务注册中心。

实现控制平台微服务,支持对规则和版本集中管理、推送、更改和删除。

实现基于控制平台微服务的图形化的灰度发布功能。

上述文中提到切换灰度发布和平滑灰度发布,切换灰度发布即在灰度发布的时候,没有过渡过程,流量直接从旧版本切换到新版本;平滑灰度发布即在灰度发布的时候,有个过渡过程,可以根据实际情况,先给新版本分配低额流量,给旧版本分配高额流量,对新版本进行监测,如果没有问题,就继续把旧版的流量切换到新版本上
#Nepxion Discovery规则和策略介绍
现有Spring Cloud微服务可以方便引入Nepxion Discovery,代码零侵入,使用者只需要做如下简单的事情:

  1. 引入相关依赖到pom.xml。
  2. 在application.properties或者application.yml中,必须为微服务定义一个版本号(version),必须为微服务自定义一个组名(group)或者应用名(application)。
  3. 使用者只需要关注相关规则推送。可以采用如下方式之一:

* 通过远程配置中心推送规则
* 通过控制台界面推送规则
* 通过客户端工具(例如Postman)推送

规则示例解释:

基于黑/白名单的IP地址过滤机制禁止对相应的微服务进行注册








黑名单,表示a服务注册到服务注册发现中心,如果它的IP起始为172.16,那么禁止注册。

白名单,表示a服务注册到服务注册发现中心,如果它的IP起始为10.10,那么允许注册。

基于最大注册数的限制微服务注册





表示a服务注册到服务注册发现中心,当达到500个后,更多的实例将无法注册。

基于黑/白名单的IP地址过滤机制禁止对相应的微服务被发现。








黑名单,表示a服务的IP起始为172.16,那么禁止被发现。

白名单,表示a服务的IP起始为10.10,那么允许被发现。

服务发现的多版本灰度访问控制。






表示消费端服务a的1.0,允许访问提供端服务b的1.0版本。

表示消费端服务a的1.1,允许访问提供端服务b的1.1版本。

服务发现的多版本权重灰度访问控制。





表示消费端服务a访问提供端服务b的时候,提供端服务b的1.0版本提供90%的权重流量,1.1版本提供10%的权重流量。

简单的多数据源数据库灰度切换。



表示服务b有两个库的配置,分别是临时数据库(database的value为temp)和生产数据库(database的value为prod)。

上线后,一开始数据库指向临时数据库,对应value为temp,然后灰度发布的时候,改对应value为prod,即实现数据库的灰度发布。

全局架构图:
1.jpg

模块结构图:
2.jpg

#两个基于网关的灰度发布示例
两个基于网关的灰度发布示例,您可以研究更多的灰度发布策略。
##基于网关版本权重的灰度发布
灰度发布前:

  1. 网关不需要配置版本
  2. 网关->服务A(V1.0),网关配给服务A(V1.0)的100%权重(流量)

灰度发布中:

  1. 上线服务A(V1.1)
  2. 在网关层调拨10%权重(流量)给A(V1.1),给A(V1.0)的权重(流量)减少到90%
  3. 通过观测确认灰度有效,把A(V1.0)的权重(流量)全部切换到A(V1.1)

灰度发布后:

  1. 下线服务A(V1.0),灰度成功

##基于网关版本切换的灰度发布
灰度发布前:

  1. 假设当前生产环境,调用路径为网关(V1.0)->服务A(V1.0)
  2. 运维将发布新的生产环境,部署新服务集群,服务A(V1.1)
  3. 由于网关(1.0)并未指向服务A(V1.1),所以它们是不能被调用的

灰度发布中:

  1. 新增用作灰度发布的网关(V1.1),指向服务A(V1.1)
  2. 灰度网关(V1.1)发布到服务注册发现中心,但禁止被服务发现,网关外的调用进来无法负载均衡到网关(V1.1)上
  3. 在灰度网关(V1.1)->服务A(V1.1)这条调用路径做灰度测试
  4. 灰度测试成功后,把网关(V1.0)指向服务A(V1.1)

灰度发布后:

  1. 下线服务A(V1.0),灰度成功
2. 灰度网关(V1.1)可以不用下线,留作下次版本上线再次灰度发布
3. 如果您对新服务比较自信,可以更简化,可以不用灰度网关和灰度测试,当服务A(V1.1)上线后,原有网关直接指向服务A(V1.1),然后下线服务A(V1.0)

#户自定义和编程灰度路由策略
使用者可以实现跟业务有关的路由策略,根据业务参数的不同,负载均衡到不同的服务器。

  1. 基于服务的编程灰度路由,实现DiscoveryEnabledExtension,通过RequestContextHolder(获取来自网关的Header参数)和ServiceStrategyContext(获取来自RPC方式的方法参数)获取业务上下文参数,进行路由自定义

  1. 基于Zuul的编程灰度路由,实现DiscoveryEnabledExtension,通过Zuul自带的RequestContext(获取来自网关的Header参数)获取业务上下文参数,进行路由自定义

  1. 基于Spring Cloud Api Gateway的编程灰度路由,实现DiscoveryEnabledExtension,通过GatewayStrategyContext(获取来自网关的Header参数)获取业务上下文参数,进行路由自定义
举例如下:

基于Rest请求Header头部自带版本规则的实时动态路由:

场景举例:外部->网关->A服务,A服务有a1, a2两个实例,a1和a2是一模一样的两个服务,但a1和a2的区别是连的数据库是不同的。a1对接外部的民生银行通道,a2对接外部的浦发银行通道。那么外部Rest经过网关时,自动根据版本号路由到正确的服务上。
3.jpg

如图所示,当前从Postman发起的请求,当走到example-a服务的时候,只能走1.0版本,到example-b服务的时候,也是1.0版本,到example-c服务的时候,能路由到1.1和1.2版本,下面的打印出来的结果印证了这个路由方式。

加如下代码即可实现该功能:
@Bean
public MyDiscoveryEnabledExtension myDiscoveryEnabledExtension() {
return new MyDiscoveryEnabledExtension();
}

表示在网关层(以Zuul为例),编程灰度路由策略,如下代码,表示请求的Header中的token包含'abc',在负载均衡层面,对应的服务示例不会被负载均衡到。

代码截图:
// 实现了组合策略,版本路由策略+自定义策略
public class MyDiscoveryEnabledExtension implements DiscoveryEnabledExtension {
private static final Logger LOG = LoggerFactory.getLogger(MyDiscoveryEnabledExtension.class);

@Override
public boolean apply(Server server, Map metadata) {
// 对Rest调用传来的Header参数(例如Token)做策略
return applyFromHeader(server, metadata);
}

// 根据Rest调用传来的Header参数(例如Token),选取执行调用请求的服务实例
private boolean applyFromHeader(Server server, Map metadata) {
RequestContext context = RequestContext.getCurrentContext();
String token = context.getRequest().getHeader("token");
// String value = context.getRequest().getParameter("value");

String serviceId = server.getMetaInfo().getAppName().toLowerCase();

LOG.info("Zuul端负载均衡用户定制触发:serviceId={}, host={}, metadata={}, context={}", serviceId, server.toString(), metadata, context);

String filterToken = "abc";
if (StringUtils.isNotEmpty(token) && token.contains(filterToken)) {
LOG.info("过滤条件:当Token含有'{}'的时候,不能被Ribbon负载均衡到", filterToken);

return false;
}

return true;
}
}

表示在服务层,当服务名为discovery-springcloud-example-c,同时版本为1.0,同时参数value中包含'abc',三个条件同时满足的情况下,在负载均衡层面,对应的服务示例不会被负载均衡到。

代码截图:
// 实现了组合策略,版本路由策略+自定义策略
public class MyDiscoveryEnabledExtension implements DiscoveryEnabledExtension {
private static final Logger LOG = LoggerFactory.getLogger(MyDiscoveryEnabledExtension.class);

@Override
public boolean apply(Server server, Map metadata) {
// 对RPC调用传来的方法参数做策略
return applyFromMethd(server, metadata);
}

// 根据RPC调用传来的方法参数(例如接口名、方法名、参数名或参数值等),选取执行调用请求的服务实例

@SuppressWarnings("unchecked")
private boolean applyFromMethd(Server server, Map metadata) {
ServiceStrategyContext context = ServiceStrategyContext.getCurrentContext();
Map attributes = context.getAttributes();

String serviceId = server.getMetaInfo().getAppName().toLowerCase();
String version = metadata.get(DiscoveryConstant.VERSION);

LOG.info("Serivice端负载均衡用户定制触发:serviceId={}, host={}, metadata={}, context={}", serviceId, server.toString(), metadata, context);

String filterServiceId = "discovery-springcloud-example-b";
String filterVersion = "1.0";
String filterBusinessValue = "abc";
if (StringUtils.equals(serviceId, filterServiceId) && StringUtils.equals(version, filterVersion)) {
if (attributes.containsKey(ServiceStrategyConstant.PARAMETER_MAP)) {
Map parameterMap = (Map) attributes.get(ServiceStrategyConstant.PARAMETER_MAP);
String value = parameterMap.get("value").toString();
if (StringUtils.isNotEmpty(value) && value.contains(filterBusinessValue)) {
LOG.info("过滤条件:当serviceId={} && version={} && 业务参数含有'{}'的时候,不能被Ribbon负载均衡到", filterServiceId, filterVersion, filterBusinessValue);

return false;
}
}
}

return true;
}
}

使用者可以通过订阅业务参数的变化,实现特色化的灰度发布,例如,多数据源的数据库切换的灰度发布。

代码截图:
@EventBus
public class MySubscriber {
@Autowired
private PluginAdapter pluginAdapter;

@Subscribe
public void onCustomization(CustomizationEvent customizationEvent) {
CustomizationEntity customizationEntity = customizationEvent.getCustomizationEntity();
String serviceId = pluginAdapter.getServiceId();
if (customizationEntity != null) {
Map> customizationMap = customizationEntity.getCustomizationMap();
Map customizationParameter = customizationMap.get(serviceId);
// 根据customizationParameter的参数动态切换数据源
} else {
// 根据customizationParameter的参数动态切换数据源
}
}

@Subscribe
public void onRegisterFailure(RegisterFailureEvent registerFailureEvent) {
System.out.println("========== 注册失败, eventType=" + registerFailureEvent.getEventType() + ", eventDescription=" + registerFailureEvent.getEventDescription() + ", serviceId=" + registerFailureEvent.getServiceId() + ", host=" + registerFailureEvent.getHost() + ", port=" + registerFailureEvent.getPort());
}
}

#用户自定义灰度发布监听
用户实现服务注册的监听,当负服务注册的时候,用户会收到下面的事件。

代码截图:
public class MyRegisterListener extends AbstractRegisterListener {
@Override
public void onRegister(Registration registration) {
}

@Override
public void onDeregister(Registration registration) {
}

@Override
public void onSetStatus(Registration registration, String status) {
}

@Override
public void onClose() {
}
}

用户实现服务发现的监听,当负服务发现过滤的时候,用户会收到下面的事件。

代码截图:
public class MyDiscoveryListener extends AbstractDiscoveryListener {
@Override
public void onGetInstances(String serviceId, List instances) {
}

@Override
public void onGetServices(List services) {
}
}

用户实现负载均衡的监听,当负载均衡启动过滤的时候,用户会收到下面的事件。

代码截图:
public class MyLoadBalanceListener extends AbstractLoadBalanceListener {
@Override
public void onGetServers(String serviceId, List servers) {
}
}

用户实现对注册失败的监听,当黑名单激活的时候,会触发注册失败,那么用户会收到一个注册失败的事件。

代码截图:
@EventBus
public class MySubscriber {
@Subscribe
public void onRegisterFailure(RegisterFailureEvent registerFailureEvent) {
}
}

图形化灰度发布桌面程序:
8.jpg

9.png

具体操作不详细述说了,请看相关视频:

灰度发布-版本访问策略:

请访问http://www.iqiyi.com/w_19rzwzovrl.html,视频清晰度改成720P,然后最大化播放。

灰度发布-版本权重策略:

请访问http://www.iqiyi.com/w_19rzs9pll1.html,视频清晰度改成720P,然后最大化播放。

灰度发布-全链路策略:

请访问http://www.iqiyi.com/w_19s1e0zf95.html,视频清晰度改成720P,然后最大化播放。

#跟Spring Boot Admin整合
实现相关一系列的监控:
10.png

11.jpg

12.png


以上内容根据2018年8月14日晚微信群分享内容整理。分享人任浩军。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

DockOne微信分享(一八四):基于Spring Cloud的微服务容器化实践

DarkForces. 发表了文章 • 0 个评论 • 2975 次浏览 • 2018-08-13 00:01 • 来自相关话题

【编者的话】近几年,互联网飞速发展的同时,也推动了云计算、大数据、人工智能的快速落地,数据本身价值也得到提升。互联网发展对应用开发提出了更高要求。首先数据采集的量级和效率提高,传统的单体架构将出现瓶颈,其次是数据联通性的需求,对数据对接必须保证高性能、高安全、 ...查看全部
【编者的话】近几年,互联网飞速发展的同时,也推动了云计算、大数据、人工智能的快速落地,数据本身价值也得到提升。互联网发展对应用开发提出了更高要求。首先数据采集的量级和效率提高,传统的单体架构将出现瓶颈,其次是数据联通性的需求,对数据对接必须保证高性能、高安全、高标准。使用微服务架构恰好解决了大部分痛点。

本次主要介绍基于Spring Cloud构建微服务,以及配套的DevOps思路,并着重介绍在Docker容器里如何部署基于Spring Cloud的微服务。
#1、基于Spring Cloud构建微服务
历史总是惊人的相似,合久必分,分久必合。我们经历了“合”:单体架构(软)、计算能力超强的小型机(硬)到“分”:分布式架构的转变,后期可能会将分发挥到了极致(去中心化的分布式,如区块链),最后很可能再经历“合”:计算和存储能力超强的“智人”(集超级计算和存储一身的人工智能)。

单体架构也有自身优势,这里不做详细介绍,大家在做架构选型时可以根据公司组织架构和业务需求综合考虑。

Spring Cloud作为Java语言的微服务框架,它依赖于Spring Boot,是由Pivotal团队提供的全新Web框架。有快速开发、持续交付和容易部署等特点。Spring Cloud提供了开发分布式服务系统的一些常用组件,例如服务注册和发现、配置中心、熔断器、智能路由、微代理、控制总线、全局锁、分布式会话等。

先看看我们使用的一个架构:
图片1.png


  • 服务发现中心(Service Discovery):服务注册与发现组件(选用Erueka)
  • 监控面板(Monitor Dashboard):整合了熔断监控(选用Hystrix)、服务调用链路监控(选用Spring Cloud Sleuth)
  • 日志分析面板(Logging Analyses):基于efk构建的日志采集系统
  • 服务网关(Edge Server):服务网关组件(Zuul & Spring Cloud Security)
  • OAuth认证服务器(OAuth Authorization Server):授权认证模块(Spring Cloud security OAuth2)
  • 配置服务器(Configuration Server):配置中心(Spring Cloud Config & Spring Cloud Bus)
#2、基于微服务架构体系的DevOps思路DevOps带来的不仅是技术挑战,还受公司组织架构和文化影响,这里是从技术角度去实现的思路。先看两个微服务应用案例:##案例1:基于微服务架构的接口开发平台主要提供Restful API服务,服务方式多样,其中一个最简单的案例流程如下:
  • 首先企业通过申请账号、密码和需要调用的API
  • 平台针对申请企业创建可调用API的账号和密码,并将该企业调用端IP加入服务白名单,用户可在平台下载文档、SDK,并进行测试
  • 企业根据用户名、密码去获取token,并在请求header中加入申请的token调用接口
图片2.png
##案例2:基于微服务架构的应用开发Web应用开发采用前后端分离方式,前端采用AngularJS,后端仍是基于Spring Cloud的微服务,整套系统部署到容器云实现CI/CD,架构如下图所示:
图片3.png
微服务引入增加了团队配合、测试、运维等后续一系列操作的复杂度,必须考虑自动化,因此需要有一套CI/ CD方案应对:
图片4.png
大致流程:[list=1]
  • 研发完成本地开发和测试提交代码
  • 代码连同Dockerfile等构建文件一起push到GitLab(可以自己搭建)
  • 代码提交触发Jenkins自动构建
  • Jenkins调用单元测试、代码检查等测试任务,如果测试通过自动构建Docker镜像
  • Jenkins将构建好的镜像推送到私有镜像仓库(自己搭建Harbor镜像库)
  • Jenkins调用容器管理平台(我们使用的Rancher)的接口进行升级
  • 容器管理平台拉取镜像完成部署(测试环境or生产环境)
  • 说明:这里我们不仅使用了Docker,还选用容器编排工具构建了容器云平台,以方便我们快速实现CI/CD。大家可以根据自己情况选择,如Kubernetes、Rancher(Rancher 2.0以后底层使用的编排工具也是Kubernetes)等。#3、Spring Cloud基于Docker的实践我们使用Docker,主要因为以下4点:[list=1]
  • Docker提供一个简单、轻量的建模方式
  • Docker使团队职责的逻辑分离
  • 可以实现快速高效的开发生命周期
  • 团队使用面向服务的架构
  • 以下介绍如何构建Docker镜像和配置细节。##1. 项目目录Dockerfile及相关文件一般放到项目子目录,由开发维护。这种方式对研发提出了更高的技能要求,这也是近期全栈工程师比较火的一个原因。当然,具体协作方式还是根据公司情况定。以下是我们的项目目录:
    图片5.png
    ##2、编写Dockerfile(供参考)注:配合自动化工具使用效果更好。
    FROM harbor.jry.com/library/alpine-jdk-time:slimADD *.jar service.jarCOPY formFile /ENTRYPOINT [ "sh", "-c", "java -jar service.jar" ] 
    ##3、模块配置我们将Spring Cloud相关的配置放到了bootstrap.yml文件,需要注意的配置如下:
    图片6.png
    注意标红部分,服务要以IP地址方式注册到注册中心,否则注册中心无法做心跳检测,因为容器之间默认无法通过hostname通信。我们也可以搭建内部DNS方式解决此问题。服务发现中心建议配置成高可用(比如两个注册中心互相注册),也需要上图配置。##4、打包构建这里截取了Jenkins里的打包及构建命令:[list=1]
  • 打包
  • 图片7.png
    [list=1]
  • 构建
  • 图片8.png
    ##5、容器启动配置(docker-compose.yml)
    version: '2'services:  zipkin:    image: harbor.jry.com/zipkin:v1.0.1    environment:      eureka.client.service-url.defaultZone: http://discovery:8761/eureka/    stdin_open: true    tty: true     links:    - discovery2:discovery     volumes:    - /data/log:/target/log    ports:    - 9411:9411/tcp
      service-config:    image: harbor.jry.com/service-config:v1.0.1    hostname: config    environment:      eureka.client.service-url.defaultZone: http://discovery:8761/eureka/    stdin_open: true    tty: true    links:    - discovery2:discovery    volumes:    - /data/log:/target/log    ports:    - 8889:8889/tcp
      service-monitor:    image: harbor.jry.com/service-monitor:v1.0.1    environment:      eureka.client.service-url.defaultZone: http://discovery:8761/eureka/    stdin_open: true    tty: true    links:    - discovery1:discovery    volumes:    - /data/log:/target/log
      discovery2:    image: harbor.jry.com/service-discovery    environment:      eureka.client.service-url.defaultZone: http://discovery:8761/eureka/    stdin_open: true    tty: true    links:    - discovery1:discovery    volumes:    - /data/log:/target/log    ports:    - 8762:8761/tcp
      discovery1:    image: harbor.jry.com/service-discovery:v1.0.1    environment:      eureka.client.service-url.defaultZone: http://discovery:8761/eureka/    stdin_open: true    tty: true    links:    - discovery2:discovery    volumes:    - /data/log:/target/log
      daas-monitor:    image: harbor.jry.com/daas-monitor:v1.0.1    environment:      eureka.client.service-url.defaultZone: http://discovery:8761/eureka/    stdin_open: true    tty: true    links:    - discovery1:discovery    volumes:    - /data/log:/target/log    ports:    - 9080:9080/tcp
      proxy-server:    image: harbor.jry.com/service-proxy:v1.0.1    environment:      eureka.client.serviceUrl.defaultZone: http://discovery:8761/eureka/    stdin_open: true    links:    - discovery1:discovery    tty: true    links:    - oauth2-server:oauth    volumes:    - /data/log:/target/log    ports:    - 8111:8443/tcp
      oauth2-server:    image: harbor.jry.com/oauth2-cg:v1.0.1    environment:      eureka.client.serviceUrl.defaultZone: http://discovery:8761/eureka/    stdin_open: true    links:    - discovery2:discovery    volumes:    - /data/log:/target/log    tty: true    ports:    - 9999:9999/tcp
    说明:
    • Zipkin:服务调用链路监控
    • service-config:配置中心服务
    • service-monitor:断路监控服务
    • discovery1, discovery2:高可用的服务发现中心
    • daas-monitor:自研的监控面板,整合其他监控服务
    • proxy-server:服务网关
    • oauth2-server:OAuth认证服务器
    注:[list=1]
  • 虽然使用的配置文件,但维护起来还是很麻烦,所以建议使用编排工具,一般会提供部分DevOps工具集
  • 上述各服务模块可以根据实际情况启动多个相同容器,以保证高可用
  • 以上各服务模块做了磁盘映射,主要为采集日志,这里我们使用的EFK,时间关系暂不展开。

  • #Q&A
    Q:WSO2的API Manager会将所有流量都统一到他这里进出,如何解决统一API网关的大流量问题?

    A:API Manager是可以拆开的,分开部署,比如调用你接口走网关模块,可以做高可用。当然不拆开也可以做高可用,前面加负载均衡。



    Q:Eureka在生产上的Kubernetes中是如何做到高可用和动态扩容的?

    A :好问题,Eureka我们目前是指定数量,可以结合脚本(或代码)做动态扩容和高可用。目前我们Hadoop就是结合代码做到的。



    Q:服务之间二阶段事务控制一般怎么做?或者说有没有现成工具或代码?服务间用http靠谱还是其他协议靠谱?

    A:这个网上有一些思路比如补偿的方式,还有就是通过流数据库(Kafka),事件驱动的方式。我们目前正在尝试这种方式。

    未标题-1.jpg



    以上内容根据2018年7月31日晚微信群分享内容整理。分享人刘凯,现就职于中金云金融(北京)大数据科技股份有限公司,负责应用架构设计及大数据平台建设。2015年开始实践微服务架构,参与基于Dubbo的微服务搭建,并实现部分源码优化、实现安卓端SDK,2016年参与公司区块链应用预研,同年开始进行基于Spring Cloud的微服务架构实践,2017年作为团队核心人员完成容器云搭建、大数据平台搭建及实践、DevOps工具集优化。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

    Nepxion Discovery:Spring Cloud微服务版本灰度发布新神器

    Andy_Lee 发表了文章 • 0 个评论 • 3527 次浏览 • 2018-07-23 23:10 • 来自相关话题

    项目地址:https://github.com/Nepxion/Discovery 强烈建议star、fork该项目,该项目可以作为学习改造Spring Cloud组件的案例项目。 Nepxion Disc ...查看全部
    项目地址:https://github.com/Nepxion/Discovery

    强烈建议star、fork该项目,该项目可以作为学习改造Spring Cloud组件的案例项目。

    Nepxion Discovery是一款对Spring Cloud的服务注册发现的增强中间件,其功能包括多版本灰度发布,黑/白名单的IP地址过滤,限制注册等,支持Eureka、Consul和Zookeeper。现有的Spring Cloud微服务可以方便引入该插件,代码零侵入,使用者只需要做如下简单的事情:

    * 引入相关Plugin Starter依赖到pom.xml
    * 必须为微服务定义一个版本号(version),在application.properties或者yaml的metadata里
    * 必须为微服务自定义一个便于为微服务归类的Key,例如组名(group)或者应用名(application),在application.properties或者yaml的metadata里,便于远程配置中心推送和灰度界面分析
    * 使用者只需要关注相关规则推送。可以采用如下方式之一:

    * 通过远程配置中心推送规则
    * 通过控制台界面推送规则
    * 通过客户端工具(例如Postman)推送推测

    #Quick Start
    图形化演示操作:

    * 请访问http://www.iqiyi.com/w_19rzwzovrl.html,视频清晰度改成720P,然后最大化播放。
    * 请访问https://pan.baidu.com/s/1eq_N56VbgSCaTXYQ5aKqiA,获取更清晰的视频 Alt text Alt text。
    1.jpg

    2.jpg


    更多教程和示例查看最下面的“示例演示”。
    #痛点
    现有Spring Cloud的痛点:

    * 如果你是运维负责人,是否会经常发现,你掌管的测试环境中的服务注册中心,被一些不负责的开发人员把他本地开发环境注册上来,造成测试人员测试失败。你希望可以把本地开发环境注册给屏蔽掉,不让注册
    * 如果你是运维负责人,生产环境的某个微服务集群下的某个实例,暂时出了问题,但又不希望它下线。你希望可以把该实例给屏蔽掉,暂时不让它被调用
    * 如果你是业务负责人,鉴于业务服务的快速迭代性,微服务集群下的实例发布不同的版本。你希望根据版本管理策略进行路由,提供给下游微服务区别调用,达到多版本灰度访问控制
    * 如果你是测试负责人,希望对微服务做A/B测试,那么通过动态改变版本达到该目的

    #简介
    实现对基于Spring Cloud的微服务和Zuul网关的支持:

    * 具有极大灵活性——支持在任何环节做过滤控制和版本灰度发布。
    * 具有极小限制性——只要开启了服务注册发现,程序入口加了@EnableDiscoveryClient。

    实现服务注册层面的控制:

    * 基于黑/白名单的IP地址过滤机制禁止对相应的微服务进行注册。
    * 基于最大注册数的限制微服务注册。一旦微服务集群下注册的实例数目已经达到上限,将禁止后续的微服务进行注册。

    实现服务发现层面的控制

    * 基于黑/白名单的IP地址过滤机制禁止对相应的微服务被发现。
    基于版本配对,通过对消费端和提供端可访问版本对应关系的配置,在服务发现和负载均衡层面,进行多版本访问控制。

    实现灰度发布:

    * 通过规则改变,实现灰度发布。
    * 通过版本切换,实现灰度发布。

    实现通过XML进行上述规则的定义。

    实现通过事件总线机制(EventBus)的功能,实现发布/订阅功能:

    * 对接远程配置中心,默认集成阿里巴巴的Nacos,异步接受远程配置中心主动推送规则信息,动态改变微服务的规则。
    * 结合Spring Boot Actuator,异步接受Rest主动推送规则信息,动态改变微服务的规则。
    * 结合Spring Boot Actuator,动态改变微服务的版本。
    * 在服务注册层面的控制中,一旦禁止注册的条件触发,主动推送异步事件,以便使用者订阅。

    实现通过Listener机制进行扩展:

    * 使用者可以自定义更多的规则过滤条件
    * 使用者可以对服务注册发现核心事件进行监听

    实现支持Spring Boot Actuator和Swagger集成。

    实现独立控制台,支持对规则和版本集中管理,未来考虑界面实现。

    实现支持未来扩展更多的服务注册中心。

    实现图形化的灰度发布功能。
    #名词解释
    IP地址,即根据微服务上报的它所在机器的IP地址。本系统内部强制以IP地址上报,禁止HostName上报,杜绝Spring Cloud应用在Docker或者Kubernetes部署时候出现问题。

    本地版本,即初始化读取本地配置文件获取的版本,也可以是第一次读取远程配置中心获取的版本。本地版本和初始版本是同一个概念。

    动态版本,即灰度发布时的版本。动态版本和灰度版本是同一个概念。

    本地规则,即初始化读取本地配置文件获取的规则,也可以是第一次读取远程配置中心获取的规则。本地规则和初始规则是同一个概念。

    动态规则,即灰度发布时的规则。动态规则和灰度规则是同一个概念。

    事件总线,即基于Google Guava的EventBus构建的组件。在使用上,通过事件总线推送动态版本和动态规则的时候,前者只支持异步,后者支持异步和同步。

    远程配置中心,即可以存储规则配置XML格式的配置中心,可以包括不限于Nacos,Apollo,DisConf,Spring Cloud Config。

    配置(Config)和规则(Rule),在本系统中属于同一个概念,例如更新配置,即更新规则,例如远程配置中心存储的配置,即规则XML。
    #场景
    黑/白名单的IP地址注册的过滤:

    * 开发环境的本地微服务(例如IP地址为172.16.0.8)不希望被注册到测试环境的服务注册发现中心,那么可以在配置中心维护一个黑/白名单的IP地址过滤(支持全局和局部的过滤)的规则。
    * 我们可以通过提供一份黑/白名单达到该效果。

    最大注册数的限制的过滤:

    * 当某个微服务注册数目已经达到上限(例如10个),那么后面起来的微服务,将再也不能注册上去。

    黑/白名单的IP地址发现的过滤:

    * 开发环境的本地微服务(例如IP地址为172.16.0.8)已经注册到测试环境的服务注册发现中心,那么可以在配置中心维护一个黑/白名单的IP地址过滤(支持全局和局部的过滤)的规则,该本地微服务不会被其他测试环境的微服务所调用
    * 我们可以通过推送一份黑/白名单达到该效果

    多版本灰度访问控制:

    * A服务调用B服务,而B服务有两个实例(B1、B2),虽然三者相同的服务名,但功能上有差异,需求是在某个时刻,A服务只能调用B1,禁止调用B2。在此场景下,我们在application.properties里为B1维护一个版本为1.0,为B2维护一个版本为1.1。
    * 我们可以通过推送A服务调用某个版本的B服务对应关系的配置,达到某种意义上的灰度控制,切换版本的时候,我们只需要再次推送即可。

    动态改变微服务版本:

    * 在A/B测试中,通过动态改变版本,不重启微服务,达到访问版本的路径改变。

    #架构
    简单描述一下,本系统的核心模块“基于版本控制的灰度发布”,从网关(Zuul)开始的灰度发布操作过程。
    ##灰度发布前

    * 假设当前生产环境,调用路径为网关(V1.0)->服务A(V1.0)->服务B(V1.0)。
    * 运维将发布新的生产环境,部署新服务集群,服务A(V1.1),服务B(V1.1)。
    * 由于网关(1.0)并未指向服务A(V1.1),服务B(V1.1),所以它们是不能被调用的。

    ##灰度发布中

    * 新增用作灰度发布的网关(V1.1),指向服务A(V1.1)->服务B(V1.1)。
    * 灰度网关(V1.1)发布到服务注册发现中心,但禁止被服务发现,网关外的调用进来无法负载均衡到网关(V1.1)上。
    * 在灰度网关(V1.1)->服务A(V1.1)->服务B(V1.1)这条调用路径做灰度测试。
    * 灰度测试成功后,把网关(V1.0)指向服务A(V1.1)->服务B(V1.1)。

    ##灰度发布后

    * 下线服务A(V1.0),服务B(V1.0),灰度成功。
    * 灰度网关(V1.1)可以不用下线,留作下次版本上线再次灰度发布。

    架构图:
    3.jpg

    #兼容
    ##版本兼容情况

    * Spring Cloud F版,请采用4.x.x版本,具体代码参考master分支。
    * Spring Cloud C版、D版和E版,请采用3.x.x版本,具体代码参考Edgware分支。
    * 4.x.x版本由于Swagger和Spring Boot 2.x.x版本的Actuator用法有冲突,故暂时不支持Endpoint功能,其他功能和3.x.x版本一致。

    ##中间件兼容情况

    Consul:

    * Spring Cloud F版,最好采用Consul的1.2.1服务器版本(或者更高),从https://releases.hashicorp.com/consul/1.2.1/获取。
    * Spring Cloud C版、D版和E版,必须采用Consul的0.9.3服务器版本(或者更低),从https://releases.hashicorp.com/consul/0.9.3/获取。

    Zookeeper:

    * Spring Cloud F版,必须采用Zookeeper的3.5.x服务器版本(或者更高)。
    * Spring Cloud C版、D版和E版,最好采用Zookeeper的3.5.0以下服务器版本(或者更低)。

    Eureka:

    * 跟Spring Cloud版本保持一致。

    #依赖
    微服务选择相应的插件引入,最后一个如需对接Nacos远程配置中心,则引入:

    com.nepxion
    discovery-plugin-starter-eureka
    ${discovery.plugin.version}



    com.nepxion
    discovery-plugin-starter-consul
    ${discovery.plugin.version}



    com.nepxion
    discovery-plugin-starter-zookeeper
    ${discovery.plugin.version}



    com.nepxion
    discovery-plugin-config-center-extension-nacos
    ${discovery.plugin.version}

    独立控制台引入,最后一个如需对接Nacos远程配置中心,则引入

    com.nepxion
    discovery-console-starter
    ${discovery.plugin.version}



    com.nepxion
    discovery-console-extension-nacos
    ${discovery.plugin.version}

    #工程
    b1.png

    #规则和策略
    ##规则示例
    请不要被吓到,我只是把注释写的很详细而已,里面配置没几行:















































    ##多版本灰度规则策略
    版本策略介绍:

    1、标准配置,举例如下:

    表示消费端1.0版本,允许访问提供端1.0和1.1版本。

    2、版本值不配置,举例如下:

    表示消费端任何版本,允许访问提供端1.0和1.1版本。

    表示消费端1.0版本,允许访问提供端任何版本。

    表示消费端任何版本,允许访问提供端任何版本。

    3、版本值空字符串,举例如下:

    表示消费端任何版本,允许访问提供端1.0和1.1版本。

    表示消费端1.0版本,允许访问提供端任何版本。

    表示消费端任何版本,允许访问提供端任何版本。

    4、版本对应关系未定义,默认消费端任何版本,允许访问提供端任何版本。

    特殊情况处理,在使用上需要极力避免该情况发生:

    1. 消费端的application.properties未定义版本号,则该消费端可以访问提供端任何版本。
    2. 提供端的application.properties未定义版本号,当消费端在xml里不做任何版本配置,才可以访问该提供端。

    ##动态改变规则策略
    微服务启动的时候,由于规则(例如:rule.xml)已经配置在本地,使用者希望改变一下规则,而不重启微服务,达到规则的改变。

    * 规则分为本地规则和动态规则。
    * 本地规则是通过在本地规则(例如:rule.xml)文件定义的,也可以从远程配置中心获取,在微服务启动的时候读取。
    * 动态规则是通过POST方式动态设置,或者由远程配置中心推送设置。
    * 规则初始化的时候,如果接入了远程配置中心,先读取远程规则,如果不存在,再读取本地规则文件。
    * 多规则灰度获取规则的时候,先获取动态规则,如果不存在,再获取本地规则。

    ##动态改变版本策略
    微服务启动的时候,由于版本已经写死在application.properties里,使用者希望改变一下版本,而不重启微服务,达到访问版本的路径改变。

    * 版本分为本地版本和动态版本。
    * 本地版本是通过在application.properties里配置的,在微服务启动的时候读取。
    * 动态版本是通过POST方式动态设置。
    * 多版本灰度获取版本值的时候,先获取动态版本,如果不存在,再获取本地版本。

    ##黑/白名单的IP地址注册的过滤策略
    微服务启动的时候,禁止指定的IP地址注册到服务注册发现中心。支持黑/白名单,白名单表示只允许指定IP地址前缀注册,黑名单表示不允许指定IP地址前缀注册。

    * 全局过滤,指注册到服务注册发现中心的所有微服务,只有IP地址包含在全局过滤字段的前缀中,都允许注册(对于白名单而言),或者不允许注册(对于黑名单而言)。
    * 局部过滤,指专门针对某个微服务而言,那么真正的过滤条件是全局过滤+局部过滤结合在一起。

    ##最大注册数的限制的过滤策略
    微服务启动的时候,一旦微服务集群下注册的实例数目已经达到上限(可配置),将禁止后续的微服务进行注册。

    * 全局配置值,只下面配置所有的微服务集群,最多能注册多少个。
    * 局部配置值,指专门针对某个微服务而言,那么该值如存在,全局配置值失效

    ##黑/白名单的IP地址发现的过滤策略
    微服务启动的时候,禁止指定的IP地址被服务发现。它使用的方式和“黑/白名单的IP地址注册的过滤”一致。
    ##版本属性字段定义策略
    不同的服务注册发现组件对应的版本配置值。
    # Eureka config
    eureka.instance.metadataMap.version=1.0
    eureka.instance.metadataMap.group=xxx-service-group

    # 奇葩的Consul配置(参考https://springcloud.cc/spring-cloud-consul.html - 元数据和Consul标签)
    # Consul config(多个值用“,”分隔,例如version=1.0,value=abc)
    spring.cloud.consul.discovery.tags=version=1.0,group=xxx-service-group

    # Zookeeper config
    spring.cloud.zookeeper.discovery.metadata.version=1.0
    spring.cloud.zookeeper.discovery.metadata.group=xxx-service-group

    ##功能开关策略
    # Plugin config
    # 开启和关闭服务注册层面的控制。一旦关闭,服务注册的黑/白名单过滤功能将失效,最大注册数的限制过滤功能将失效。缺失则默认为true
    spring.application.register.control.enabled=true
    # 开启和关闭服务发现层面的控制。一旦关闭,服务多版本调用的控制功能将失效,动态屏蔽指定IP地址的服务实例被发现的功能将失效。缺失则默认为true
    spring.application.discovery.control.enabled=true
    # 开启和关闭通过Rest方式对规则配置的控制和推送。一旦关闭,只能通过远程配置中心来控制和推送。缺失则默认为true
    spring.application.config.rest.control.enabled=true
    # 本地规则文件的路径,支持两种方式:classpath:rule.xml - 规则文件放在resources目录下,便于打包进jar;file:rule.xml - 规则文件放在工程根目录下,放置在外部便于修改。缺失则默认为不装载本地规则
    spring.application.config.path=classpath:rule.xml
    # 为微服务归类的Key,一般通过group字段来归类,例如eureka.instance.metadataMap.group=xxx-group或者eureka.instance.metadataMap.application=xxx-application。缺失则默认为group
    # spring.application.group.key=group
    # spring.application.group.key=application


    #配置中心
    跟远程配置中心整合。

    本系统默认跟Nacos集成,如何安装使用,请参考https://github.com/alibaba/nacos。使用者也可以跟携程Apollo,百度DisConf等远程配置中心整合,实现规则读取和订阅。

    * 拉取配置,参考discovery-plugin-config-center-extension-nacos工程。
    * 推送配置,参考discovery-console-extension-nacos工程。

    #管理中心
    PORT端口号为server.port或者management.port都可以(management.port开放只支持3.x.x版本)。

    配置接口、版本接口、路由接口参考Swagger界面,如下图:
    4.jpg

    #独立控制台
    为UI提供相关接口,包括:

    * 一系列批量功能。
    * 跟Nacos集成,实现配置推送和清除。

    PORT端口号为server.port或者management.port都可以(management.port开放只支持3.x.x版本)。
    ##控制台接口
    参考Swagger界面,如下图:
    5.jpg

    #扩展和自定义更多规则或者监听
    使用者可以继承如下类:

    * AbstractRegisterListener,实现服务注册的扩展和监听,用法参考discovery-springcloud-example下MyRegisterListener。
    * AbstractDiscoveryListener,实现服务发现的扩展和监听,用法参考discovery-springcloud-example下MyDiscoveryListener。注意,在Consul下,同时会触发service和management两个实例的事件,需要区别判断,如下图。
    * AbstractLoadBalanceListener,实现负载均衡的扩展和监听,用法参考discovery-springcloud-example下MyLoadBalanceListener。

    集成了健康检查的Consul控制台。
    6.jpg

    #示例演示
    ##场景描述
    本例将模拟一个较为复杂的场景,如下图:
    7.jpg

    系统部署情况:

    * 网关Zuul集群部署了1个
    * 微服务集群部署了3个,分别是A服务集群、B服务集群、C服务集群,分别对应的实例数为2、2、3

    微服务集群的调用关系为网关Zuul->服务A->服务B->服务C。

    系统调用关系:

    * 网关Zuul的1.0版本只能调用服务A的1.0版本,网关Zuul的1.1版本只能调用服务A的1.1版本。
    * 服务A的1.0版本只能调用服务B的1.0版本,服务A的1.1版本只能调用服务B的1.1版本。
    * 服务B的1.0版本只能调用服务C的1.0和1.1版本,服务B的1.1版本只能调用服务C的1.2版本。

    用规则来表述上述关系:



















    上述微服务分别见discovery-springcloud-example字样的8个DiscoveryApplication,分别对应各自的application.properties。这8个应用,对应的版本和端口号如下表:
    b2.png

    独立控制台见discovery-springcloud-example-console,对应的版本和端口号如下表:
    b3.png

    ##开始演示

    启动服务注册发现中心,默认是Eureka。可供选择的有Eureka,Zuul,Zookeeper。Eureka,请启动discovery-springcloud-example-eureka下的应用,后两者自行安装服务器。

    根据上面选择的服务注册发现中心,对示例下的discovery-springcloud-example/pom.xml进行组件切换。

    com.nepxion
    discovery-plugin-starter-eureka


    ${discovery.plugin.version}

    根据上面选择的服务注册发现中心,对控制台下的discovery-springcloud-example-console/pom.xml进行组件切换切换。

    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client



    ##服务注册过滤的操作演示
    黑/白名单的IP地址注册的过滤:

    * 在rule.xml把本地IP地址写入到相应地方。
    * 启动DiscoveryApplicationA1.java。
    * 抛出禁止注册的异常,即本地服务受限于黑名单的IP地址列表,不会注册到服务注册发现中心;白名单操作也是如此,不过逻辑刚好相反。

    最大注册数的限制的过滤:

    * 在rule.xml修改最大注册数为0。
    * 启动DiscoveryApplicationA1.java。
    * 抛出禁止注册的异常,即本地服务受限于最大注册数,不会注册到服务注册发现中心。

    黑/白名单的IP地址发现的过滤:

    * 在rule.xml把本地IP地址写入到相应地方。
    * 启动DiscoveryApplicationA1.java和DiscoveryApplicationB1.java、DiscoveryApplicationB2.java。
    * 你会发现A服务无法获取B服务的任何实例,即B服务受限于黑名单的IP地址列表,不会被A服务的发现;白名单操作也是如此,不过逻辑刚好相反。

    ##服务发现和负载均衡控制的操作演示
    基于图形化方式的多版本灰度访问控制:

    * 请访问http://www.iqiyi.com/w_19s07thtsh.html,视频清晰度改成720P,然后最大化播放。
    * 请访问https://pan.baidu.com/s/1eq_N56VbgSCaTXYQ5aKqiA,获取更清晰的视频。

    ##基于Rest方式的多版本灰度访问控制
    基于服务的操作过程和效果:

    启动discovery-springcloud-example下7个DiscoveryApplication(除去Zuul),无先后顺序,等待全部启动完毕。

    下面URL的端口号,可以是服务端口号,也可以是管理端口号。

    通过版本切换,达到灰度访问控制,针对A服务:

    1.1 通过Postman或者浏览器,执行POST http://localhost:1100/routes,填入discovery-springcloud-example-b;discovery-springcloud-example-c,查看路由路径,如图1,可以看到符合预期的调用路径。

    1.2 通过Postman或者浏览器,执行POST http://localhost:1100/version/update,填入1.1,动态把服务A的版本从1.0切换到1.1。

    1.3 通过Postman或者浏览器,再执行第一步操作,如图2,可以看到符合预期的调用路径,通过版本切换,灰度访问控制成功。

    通过规则改变,达到灰度访问控制,针对B服务:

    2.1 通过Postman或者浏览器,执行POST http://localhost:1200/config/update-sync,发送新的规则XML(内容见下面)。

    2.2 通过Postman或者浏览器,执行POST http://localhost:1201/config/update-sync,发送新的规则XML(内容见下面)。

    2.3 上述操作也可以通过独立控制台,进行批量更新,见图5。操作的逻辑:B服务的所有版本都只能访问C服务3.0版本,而本例中C服务3.0版本是不存在的,意味着这么做B服务不能访问C服务。

    2.4 重复1.1步骤,发现调用路径只有A服务->B服务,如图3,通过规则改变,灰度访问控制成功。

    负载均衡的灰度测试:

    3.1 通过Postman或者浏览器,执行POST http://localhost:1100/invoke,这是example内置的访问路径示例(通过Feign实现)。

    3.2 重复“通过版本切换,达到灰度访问控制”或者“通过规则改变,达到灰度访问控制”操作,查看Ribbon负载均衡的灰度结果,如图4。

    上述操作,都是单次操作,如需要批量操作,可通过“独立控制台”接口,它集成批量操作和推送到远程配置中心的功能,可以取代上面的某些调用方式。

    其它更多操作,请参考“配置中心”、“管理中心”和“独立控制台”。

    新XML规则:








    图 1:
    8.jpg

    图 2:
    9.jpg

    图 3:
    10.jpg

    图 4:
    11.jpg

    图 5:
    12.jpg


    基于网关的操作过程和效果:

    * 在上面基础上,启动discovery-springcloud-example下DiscoveryApplicationZuul。
    * 因为Zuul是一种特殊的微服务,所有操作过程跟上面完全一致。

    图 6:
    13.jpg


    图7
    14.jpg

    spring cloud 微服务部署到docker上内存就占用很大, 本地占用内存很正常

    回复

    styshoo 回复了问题 • 3 人关注 • 3 个回复 • 3259 次浏览 • 2018-01-11 11:46 • 来自相关话题

    一个spring cloud的java容器限制多大的内存比较好

    回复

    请叫我小路飞 发起了问题 • 1 人关注 • 0 个回复 • 3143 次浏览 • 2017-05-23 19:48 • 来自相关话题

    应用量化时代 | 微服务架构的服务治理之路

    博云BoCloud 发表了文章 • 0 个评论 • 26 次浏览 • 2019-06-19 16:10 • 来自相关话题

    【编者的话】微服务下一个“兵家必争”的场景。 技术随业务而生,业务载技术而行。 近些年来,伴随数字经济的发展,在众多企业的数字化转型之路上,云原生、DevOps、微服务、服 ...查看全部

    【编者的话】微服务下一个“兵家必争”的场景。



    技术随业务而生,业务载技术而行。



    近些年来,伴随数字经济的发展,在众多企业的数字化转型之路上,云原生、DevOps、微服务、服务治理等成为行业内不断被探讨的新话题。人们在理解和接受这些新型概念的同时,也不断地思考其可能的落地形态。需求是创造发生的原动力,于是一批代表性的开源技术或者框架涌现而出:Kubernetes,Spring Cloud,Service Mesh,Serverless…… 它们炙手可热,大放异彩。然而在具体落地过程中,却举步维艰,磕磕绊绊。 如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态



    本文试图结合企业业务的核心诉求,以应用形态发展历程为背景,帮助企业梳理应用面向云原生、微服务转型中涉及的各种服务治理问题,以及服务治理的发展趋势。





    什么是服务治理?



    服务治理(SOA governance),按照Anne Thomas Manes的定义是:企业为了确保事情顺利完成而实施的过程,包括最佳实践、架构原则、治理规程、规律以及其他决定性的因素。服务治理指的是用来管理SOA的采用和实现的过程。



    由定义可知,服务治理关键因素在于:应用形态,数据采集,信息分析,管控策略,协议规范五个方面。用户群体只有从这五个层次出发,才能构建出符合企业规范与要求的服务治理平台,从而进一步创造商业价值。





    “微观”塑形,服务一小再小



    世界上唯一不变的是变化本身。

    ——By 斯宾塞.约翰逊



    万理同此,纵观应用形态发展历程,从单机到网络、从单体到服务化、到微服务、到Serverless,再到未来,应用的形态随着业务驱动和技术演化,一直在不断变化。随之而来的是业务需求的复杂化与多样化,企业IT面临着大规模、高并发、应用快速创新等新难题,弹性与敏捷成为企业IT的迫切需求。



    在IT行业内有两个“不成熟”的理论:第一,每增加一行代码就会带来N种风险;第二,任何问题都可以采取增加一层抽象的方式解决。因此面对企业IT复杂的环境,“小而精”逐渐取代“大而全”,成为构建企业服务的首选方式,这也导致软件设计原则中的“高内聚,低耦合”又开始成为不断被高调吟诵的主角,微服务理念因此大行其道。



    微服务架构为业务单元可独立开发和独立部署,使服务具备灵活的动态处理机能,同时依赖高度抽象化的组件工具和多元化的通信机制,向用户屏蔽所有服务之间的通信细节的这种思想提供了最佳落地实践。微服务的出现有效地缩短了服务上线周期,并且允许企业快速响应客户反馈,为客户提供所期望的可靠服务。



    然而随着企业业务的发展与扩张与微服务的深入,服务数量向不可控的规模增长,服务数量的爆发式增长,为服务管理以及线上治理带来了极大的挑战。服务治理应运而生,成为构建微服务架构系统的必备“良药”。





    “量化”管控,服务无可遁形



    数字永远不会说谎。



    如今,微服务已经成为软件架构的实际指导思想,而以Docker和Kubernetes为代表的容器技术的延伸,也有效解决了微服务架构下多个服务单元的编排部署问题。然而,微服务架构下也隐藏着容易被忽视的风险:面临规模巨大的服务单元,如何对其进行有效合理的管控与治理?



    服务治理领域开始被行业与用户所重视,期望能够获得有效的思维方式和技术手段,应对由于不断激增的服务单元带来的服务治理挑战。关于服务治理,我们看到的更多的是其功能集合:服务注册发现、服务配置、服务熔断、网关、负载均衡、服务跟踪、日志采集、监控平台等。但当我们抛开这些名词解释,重新审视服务治理的时候,这些名词并没有完整的解释我们的困惑:如何设置负载均衡策略?采集日志格式是什么?服务配置如何生效?服务跟踪如何进行精确定位?



    显然单单通过这些功能名词无法满足我们构建服务治理平台的需求,但从这些功能中我们总结出一些规律与方法,我们将从功能场景的横向切面和技术手段的纵深层次,进行如何构建一个有效的服务治理平台的分析探讨。



    首先,我们从服务治理功能场景的横向切面来看,其可以抽象为四个层面:量化,追踪,管控,规范。





    量化


    量化包括服务数据采集、数据过滤和数据聚合三个层次。数据采集进一步细分为业务数据和性能数据,业务数据主要包括方法响应周期、服务内资源消耗规模、业务异常检测、方法调用次数、服务运行日志等;性能数据包括服务间响应时长、服务整体资源消耗等。



    服务本身需要依赖不同的特性,构建不同的agent,来搜集服务运行时产生的数据。数据过滤针对采集的数据按照一定的格式规范进一步加工处理,例如基于kafka对原始的日志数据进行标准化处理后,导入日志系统。



    数据聚合需要对独立的服务数据进行聚合操作,例如服务调用链呈现。



    通过服务量化能够清晰的记录服务运行时产生的所有数据,为服务跟踪呈现和服务管控策略制定并提供强有力的数据支撑。





    追踪


    追踪能够有效量化服务调用链路上发生的事情,具体来讲,可以划分为:服务间的链路跟踪和服务内部的方法调用链路跟踪。追踪的本质,不仅仅是为了呈现服务链路及服务路由信息,更重要的是呈现服务间请求,以及服务内部请求的响应延迟,异常反馈,能够快速定位服务以及服务内在代码存在的问题。





    管控


    管控依赖于量化采集的聚合数据。管控允许运维人员聚焦某个服务单元的运行时状态,为服务设定一定的控制策略,从而保证服务稳定可靠的运行。例如熔断策略,负载策略,流量控制,权限控制等。





    规范


    规范更多针对服务通信而言,例如通信协议规范,无论针对哪种协议,例如http,tcp,rpc等都能够提供相应的检测手段。与此同时,规范也能够清晰定义服务名称和管控策略,使得服务在不同环境之间进行迁移的时候,依旧平稳可靠。



    综上所述,在服务单元遵循一定规范标准的前提下,基于服务单元数据量化、服务调用跟踪以及服务策略管控的方式,才能构建出符合要求的服务治理平台。



    接下来,我们从纵深的角度考虑构建服务治理平台过程中涉及的技术理论基础。服务治理之所以困难,原因在于构建业务系统采用的技术栈成多元化的方式存在。从目前行业内采用的技术而言可以划分为三大学派:代码集成,agent探针,流量劫持





    代码集成


    代码集成往往需要业务开发人员的支持,在业务系统中嵌入数据采集代码,用来采集服务运行时服务产生的各种业务指标及性能指标,并将数据传输到云端治理平台。平台依据数据信息,通过配置动态下发,从而影响业务响应动态,完成服务治理功能。



    优点:治理深入,端到端监控



    缺点:维护繁琐,语言版本众多,影响业务性能





    Agent探针


    Agent探针是对代码集成的进一步提炼。Agent探针将需要集成的监控代码,高度提取、抽象、封装成可以独立集成的SDK,并且以“弱旁路”的方式与代码集成在一起,从而完成数据采集工作。云端治理平台,同样以采集的数据信息作为治理策略制定的依据,下发各种治理策略,从而达到服务治理功能。



    优点:治理深入,端到端监控



    缺点:语言版本众多,影响业务性能





    流量劫持


    流量劫持与前两者相比,与代码集成不同。它从网络通信作为切入点,以proxy的方式,代理业务单元所有的IN/OUT流量,并且proxy内部可以对请求数据进行一定的策略控制。从而完成服务通信的治理功能。



    优点:无关语言差异性,维护简单



    缺点:治理略浅,影响业务性能



    综上所述,目前服务治理的技术栈或多或少都存在一些缺陷,在构建服务治理平台时往往需要采用结合的方式,才能做到物尽其才。





    “百家争鸣”,谁能一统天下



    竞争成就未来。



    从目前行业发展来看,微服务奠定了服务构建的基础方式,容器引擎以及编排技术解决了服务编排上线的困惑,下一个“兵家必争”的场景必将在服务治理。那目前行业内又有哪些项目聚焦在服务治理领域?





    Spring Cloud


    Spring Cloud作为Spring社区的重要布局之一,在微服务落地伊始就逐渐发力,当下已经成为Java体系下微服务框架的代名词,Spring Cloud 以 Netfilx 全家桶作为初始化基础,为开发人员提供业务单元服务支撑框架的同时,也开发出一系列的服务治理SDK,供开发人员选用。在微服务发展背景下,SpringCloud可谓如日中天。





    Dubbo


    Dubbo原为阿里开源的 rpc 远程调用框架,初始设计初衷在于解决以 rpc 协议为标准的远程服务调用问题,随着阿里重启Dubbo,其也开始在服务治理领域发力,成为很多以rpc协议作为通信基础系统平台的首选。粗略而言,Dubbo和SpringCloud已成为Java体系下的服务治理“双枪”。





    gRPC


    gRPC与Dubbo类似,最初是由Google开源的一款远程服务调用框架。gRPC凭借HTTP/2和 RrotoBuf 服务定义方式以及多语言支持的特性,加之其易于定制与开发,能够方面开发人员进行快速扩展和灵活发挥,从而也成为众多用户的选择之一。





    Service Mesh


    Service Mesh的出现不在于它实现了多少功能,而是它彻底把业务单元与业务支撑体系分离,完整贯彻了“术业有专攻”的思想理念。它允许业务人员聚焦业务实现,不再关心服务治理相关的内容。通过与容器技术结合,下沉至基础设施,从通信协议的角度彻底接管业务通信交互过程,可谓微服务治理领域的后起之秀。



    总而言之,服务治理的本质是针对业务与应用产生价值的收敛与反馈,只有不断地反馈和复盘才能构建出稳定、高效的应用形态。

    API网关如何实现对服务下线实时感知

    翔宇 发表了文章 • 0 个评论 • 248 次浏览 • 2019-06-04 15:50 • 来自相关话题

    上篇文章《Eureka 缓存机制》介绍了Eureka的缓存机制,相信大家对Eureka有了进一步的了解,本文将详细介绍API网关如何实现服务下线的实时感知。 #一、前言 在基于云的微服务应用中,服务实例的网络位置都是动态分配的。而且 ...查看全部
    上篇文章《Eureka 缓存机制》介绍了Eureka的缓存机制,相信大家对Eureka有了进一步的了解,本文将详细介绍API网关如何实现服务下线的实时感知。
    #一、前言

    在基于云的微服务应用中,服务实例的网络位置都是动态分配的。而且由于自动伸缩、故障和升级,服务实例会经常动态改变。因此,客户端代码需要使用更加复杂的服务发现机制。

    目前服务发现主要有两种模式:客户端发现和服务端发现。

    * 服务端发现:客户端通过负载均衡器向服务注册中心发起请求,负载均衡器查询服务注册中心,将每个请求路由到可用的服务实例上。
    * 客户端发现:客户端负责决定可用服务实例的网络地址,并且在集群中对请求负载均衡, 客户端访问服务登记表,也就是一个可用服务的数据库,然后客户端使用一种负载均衡算法选择一个可用的服务实例然后发起请求。

    客户端发现相对于服务端发现最大的区别是:客户端知道(缓存)可用服务注册表信息。如果Client端缓存没能从服务端及时更新的话,可能出现Client 与 服务端缓存数据不一致的情况。
    #二、网关与Eureka结合使用

    Netflix OSS 提供了一个客户端服务发现的好例子。Eureka Server 为注册中心,Zuul 相对于Eureka Server来说是Eureka Client,Zuul 会把 Eureka Server 端服务列表缓存到本地,并以定时任务的形式更新服务列表,同时 Zuul 通过本地列表发现其它服务,使用 Ribbon 实现客户端负载均衡。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
    1.jpg

    正常情况下,调用方对网关发起请求即刻能得到响应。但是当对生产者做缩容、下线、升级的情况下,由于Eureka这种多级缓存的设计结构和定时更新的机制,LoadBalance 端的服务列表B存在更新不及时的情况(由上篇文章《Eureka 缓存机制》可知,服务消费者最长感知时间将无限趋近240s),如果这时消费者对网关发起请求,LoadBalance 会对一个已经不存在的服务发起请求,请求是会超时的。
    #三、解决方案

    ##实现思路

    生产者下线后,最先得到感知的是 Eureka Server 中的 readWriteCacheMap,最后得到感知的是网关核心中的 LoadBalance。但是 loadBalance 对生产者的发现是在 loadBalance 本地维护的列表中。

    所以要想达到网关对生产者下线的实时感知,可以这样做:首先生产者或者部署平台主动通知 Eureka Server,然后跳过 Eureka 多级缓存之间的更新时间,直接通知 Zuul 中的 Eureka Client,最后将 Eureka Client 中的服务列表更新到 Ribbon 中。

    但是如果下线通知的逻辑代码放在生产者中,会造成代码污染、语言差异等问题。

    借用一句名言:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。
    2.jpg

    Gateway-SynchSpeed 相当于一个代理服务,它对外提供REST API来负责响应调用方的下线请求,同时会将生产者的状态同步到 Eureka Server 和 网关核心,起着 状态同步 和 软事物 的作用。

    思路:在生产者做 缩容、下线、升级 前,spider 平台(spider为容器管理平台)会主动通知 Gateway-SynchSpeed 某个生产者的某个实例要下线了,然后 Gateway-SynchSpeed 会通知 Eureka Server 生产者的某个实例下线了;如果Eureka Server 下线成功,Gateway-SynchSpeed 会直接通知 网关核心。

    设计特点:

    * 无侵入性、方便使用。不用关心调用方的基于何种语言实现,调用者只要对 Gateway-SynchSpeed 发起一个http rest请求即可,真正的实现逻辑不用侵入到调用方而是交给这个代理来实现。
    * 原子性。调用方先在Eureka Server下线,然后在所有相关网关核心中下线为最小工作执行单元,Gateway-SynchSpeed 相当于一个"软事物",保证服务下线的某种程度上原子特性。

    ##实现步骤

    3.jpg

    步骤说明:

    第一步:在生产者做 缩容、下线、升级 前,spider平台会以http请求的形式通知到 Gateway-SynchSpeed 服务,通知的粒度为服务实例所在的容器IP。

    第二步:Gateway-SynchSpeed 接受到请求后,先校验IP的可用性,然后通知Eureka Server。

    第三步:Eureka Server 将 Producer 置为失效状态,并返回处理结果(Eureka 下线形式分为两种,一种是直接从服务注册列表直接剔除,第二种是状态下线,即是将 Producer 的状态置为OUT_OF_SERVICE。 如果是以第一种形式下线,Spider平台发出下线请求后,不能保证Producer进程立刻被kill,如果这期间 Producer 还有心跳同步到 Eureka Server,服务会重新注册到 Eureka Server)。

    第四步:Gateway-SynchSpeed 得到上一步结果,如果结果为成功,则执行下一步;反之,则停止。

    第五步:Gateway-SynchSpeed 为Eureka Client。Gateway-SynchSpeed 通过 IP 到本地服务注册列表中得到 Producer 的 Application-Name。

    第六步:Gateway-SynchSpeed 通过 Application-Name 到网关核心库中查询所有与下线服务相关的 网关组名字。

    第七步:Gateway-SynchSpeed 通过 网关组名字 到本地服务列表中查找网关组下所有的服务地址 ipAddress(ip : port)。

    第八步:Gateway-SynchSpeed 异步通知所有相关网关节点。

    第九步:Gateway-Core 收到通知后,对 Producer 做状态下线,同时记录所有状态下线成功的实例信息到缓存 DownServiceCache 中。

    第十步:Gateway-Core 更新本地 Ribbon 服务列表。
    #四、补偿机制

    Eureka 提供了一种安全保护机制。Eureka Client 从 Eureka Server 更新服务列表前,会校验相关Hash值是否改变(Client 服务列表被修改,hash值会改变),如果改变,更新方式会从增量更新变成全量更新,(由《Eureka 缓存机制》可知这30s内 readOnlyCacheMap 和 readWriteCacheMap 的数据可能存在差异),如果Client端缓存列表被readOnlyCacheMap 覆盖,最终会导致 Ribbon 端服务列表与 readWriteCacheMap 数据不一致。
    4.jpg

    针对 Eureka 这种机制,引入监听器 EurekaEventListener 作为补偿机制,它会监听 Eureka Client 全量拉取事件,对于缓存中未超过30s的服务,将其状态重新设置成 OUT_OF_SERVICE 。
    #五、API安全设计

    考虑到系统的安全性问题,如果被人恶意访问,可能会使生产者在Eureka Server中无故下线,导致消费者无法通过 Eureka Server 来发现生产者。

    使用黑白名单做安全过滤,基本流程如下:

    * 对 Gateway-Synchspeed 中设置白名单网段(IP网段)。
    * 在 Gateway-Synchspeed 加入过滤器,对下线请求方进行IP校验,如果请求端IP在网段中,则放行;反之,过滤。

    #六、日志回溯

    由于 Gateway-SynchSpeed 和 Gateway-Core 是部署在 Docker 容器中,如果容器重启,会导致日志文件全部丢失。所以需要将 Gateway-SynchSpeed 和 Gateway-Core 中相关日志写入到 Elasticsearch ,最终由 Kibana 负责查询 Elasticsearch 的数据并以可视化的方式展现。
    #七、代码片段展示

    Gateway-SynchSpeed 做状态同步。
    5.jpg

    EurekaEventListener 处理缓存数据。
    6.jpg

    #八、 补充说明

    目前网关实现对服务下线的实时感知中,使用的 Zuul 和 Eureka 版本为 Spring Cloud Zuul 1.3.6.RELEASE、Spring Cloud Eureka 1.4.4.RELEASE。

    目前网关实现的是对网关下游服务的实时感知,而且需满足以下条件:

    * 生产者需部署在 kubernetes 容器管理平台 。
    * 生产者做正常的下线、升级或者缩容操作。如果是由于容器资源不足,导致服务异常宕机等非正常下线,不支持。

    网关服务下线实时感知是网关对业务方提供的一种可选的解决方案,在 spider 平台中默认是没有开启此功能,是否开启此功能由业务方根据本身系统要求决定,具体如何配置可参考 API网关接入指南 中 《网关实时感知在spider上配置文档说明》。

    原文链接:http://college.creditease.cn/detail/256

    详解Eureka缓存机制

    阿娇 发表了文章 • 0 个评论 • 279 次浏览 • 2019-06-04 14:45 • 来自相关话题

    【编者的话】Eureka是Netflix开源的、用于实现服务注册和发现的服务。Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便。但是由于Eureka本身存在较多缓存,服务状态更新滞后,最常见的状况是:服务下 ...查看全部
    【编者的话】Eureka是Netflix开源的、用于实现服务注册和发现的服务。Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便。但是由于Eureka本身存在较多缓存,服务状态更新滞后,最常见的状况是:服务下线后状态没有及时更新,服务消费者调用到已下线的服务导致请求失败。本文基于Spring Cloud Eureka 1.4.4.RELEASE,在默认region和zone的前提下,介绍Eureka的缓存机制。
    #一、AP特性

    从CAP理论看,Eureka是一个AP系统,优先保证可用性(A)和分区容错性(P),不保证强一致性(C),只保证最终一致性,因此在架构中设计了较多缓存。
    1.jpg

    Eureka高可用架构
    #二、服务状态

    Eureka服务状态enum类:com.netflix.appinfo.InstanceInfo.InstanceStatus
    2.png

    #三、Eureka Server

    在Eureka高可用架构中,Eureka Server也可以作为Client向其他server注册,多节点相互注册组成Eureka集群,集群间相互视为peer。Eureka Client向Server注册、续约、更新状态时,接受节点更新自己的服务注册信息后,逐个同步至其他peer节点。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

    【注意】如果server-A向server-B节点单向注册,则server-A视server-B为peer节点,server-A接受的数据会同步给server-B,但server-B接受的数据不会同步给server-A。
    ##缓存机制

    Eureka Server存在三个变量:(registry、readWriteCacheMap、readOnlyCacheMap)保存服务注册信息,默认情况下定时任务每30s将readWriteCacheMap同步至readOnlyCacheMap,每60s清理超过90s未续约的节点,Eureka Client每30s从readOnlyCacheMap更新服务注册信息,而UI则从registry更新服务注册信息。
    3.jpg

    三级缓存:
    4.jpg

    缓存相关配置:
    5.jpg

    关键类:
    6.jpg

    #四、Eureka Client

    Eureka Client存在两种角色:服务提供者和服务消费者,作为服务消费者一般配合Ribbon或Feign(Feign内部使用Ribbon)使用。Eureka Client启动后,作为服务提供者立即向Server注册,默认情况下每30s续约(renew);作为服务消费者立即向Server全量更新服务注册信息,默认情况下每30s增量更新服务注册信息;Ribbon延时1s向Client获取使用的服务注册信息,默认每30s更新使用的服务注册信息,只保存状态为UP的服务。

    二级缓存:
    7.jpg

    缓存相关配置:
    8.jpg

    关键类:
    9.jpg

    #五、默认配置下服务消费者最长感知时间

    10.jpg

    考虑如下情况:

    * 0s时服务未通知Eureka Client直接下线;
    * 29s时第一次过期检查evict未超过90s;
    * 89s时第二次过期检查evict未超过90s;
    * 149s时第三次过期检查evict未续约时间超过了90s,故将该服务实例从registry和readWriteCacheMap中删除;
    * 179s时定时任务从readWriteCacheMap更新至readOnlyCacheMap;
    * 209s时Eureka Client从Eureka Server的readOnlyCacheMap更新;
    * 239s时Ribbon从Eureka Client更新。

    因此,极限情况下服务消费者最长感知时间将无限趋近240s。
    11.jpg

    #六、应对措施

    服务注册中心在选择使用Eureka时说明已经接受了其优先保证可用性(A)和分区容错性(P)、不保证强一致性(C)的特点。如果需要优先保证强一致性(C),则应该考虑使用ZooKeeper等CP系统作为服务注册中心。分布式系统中一般配置多节点,单个节点服务上线的状态更新滞后并没有什么影响,这里主要考虑服务下线后状态更新滞后的应对措施。
    ##Eureka Server

    1、缩短readOnlyCacheMap更新周期。缩短该定时任务周期可减少滞后时间。
    eureka.server.responsecCacheUpdateIntervalMs: 10000  # Eureka Server readOnlyCacheMap更新周期

    2、关闭readOnlyCacheMap。中小型系统可以考虑该方案,Eureka Client直接从readWriteCacheMap更新服务注册信息。
    eureka.server.useReadOnlyResponseCache: false        # 是否使用readOnlyCacheMap

    ##Eureka Client

    1、服务消费者使用容错机制。如Spring Cloud Retry和Hystrix,Ribbon、Feign、Zuul都可以配置Retry,服务消费者访问某个已下线节点时一般报ConnectTimeout,这时可以通过Retry机制重试下一个节点。

    2、服务消费者缩短更新周期。Eureka Client和Ribbon二级缓存影响状态更新,缩短这两个定时任务周期可减少滞后时间,例如配置:
    eureka.client.registryFetchIntervalSeconds: 5        # Eureka Client更新周期
    ribbon.ServerListRefreshInterval: 2000 # Ribbon更新周期

    3、服务提供者保证服务正常下线。服务下线时使用kill或kill -15命令,避免使用kill -9命令,kill或kill -15命令杀死进程时将触发Eureka Client的shutdown()方法,主动删除Server的registry和readWriteCacheMap中的注册信息,不必依赖Server的evict清除。

    4、服务提供者延迟下线。服务下线之前先调用接口使Eureka Server中保存的服务状态为DOWN或OUT_OF_SERVICE后再下线,二者时间差根据缓存机制和配置决定,比如默认情况下调用接口后延迟90s再下线服务即可保证服务消费者不会调用已下线服务实例。
    #七、网关实现服务下线实时感知

    在软件工程中,没有一个问题是中间层解决不了的,而网关是服务提供者和服务消费者的中间层。以Spring Cloud Zuul网关为例,网关作为Eureka Client保存了服务注册信息,服务消费者通过网关将请求转发给服务提供者,只需要做到服务提供者下线时通知网关在自己保存的服务列表中使该服务失效。为了保持网关的独立性,可实现一个独立服务接收下线通知并协调网关集群。下篇文章将详细介绍网关如何实现服务下线实时感知,敬请期待!

    原文链接:https://mp.weixin.qq.com/s/zwoIDzX8WouYVrBfMJMpJQ

    Spring Cloud微服务如何设计异常处理机制?

    齐达内 发表了文章 • 0 个评论 • 163 次浏览 • 2019-06-03 21:36 • 来自相关话题

    【编者的话】今天和大家聊一下在采用Spring Cloud进行微服务架构设计时,微服务之间调用时异常处理机制应该如何设计的问题。我们知道在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向内部和外部提供相应的功能服务接口。面向外部提供的服务接口,会通 ...查看全部
    【编者的话】今天和大家聊一下在采用Spring Cloud进行微服务架构设计时,微服务之间调用时异常处理机制应该如何设计的问题。我们知道在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向内部和外部提供相应的功能服务接口。面向外部提供的服务接口,会通过服务网关(如使用Zuul提供的apiGateway)面向公网提供服务,如给App客户端提供的用户登陆、注册等服务接口。

    而面向内部的服务接口,则是在进行微服务拆分后由于各个微服务系统的边界划定问题所导致的功能逻辑分散,而需要微服务之间彼此提供内部调用接口,从而实现一个完整的功能逻辑,它是之前单体应用中本地代码接口调用的服务化升级拆分。例如,需要在团购系统中,从下单到完成一次支付,需要交易系统在调用订单系统完成下单后再调用支付系统,从而完成一次团购下单流程,这个时候由于交易系统、订单系统及支付系统是三个不同的微服务,所以为了完成这次用户订单,需要App调用交易系统提供的外部下单接口后,由交易系统以内部服务调用的方式再调用订单系统和支付系统,以完成整个交易流程。如下图所示:
    1.png

    这里需要说明的是,在基于Spring Cloud的微服务架构中,所有服务都是通过如Consul或Eureka这样的服务中间件来实现的服务注册与发现后来进行服务调用的,只是面向外部的服务接口会通过网关服务进行暴露,面向内部的服务接口则在服务网关进行屏蔽,避免直接暴露给公网。而内部微服务间的调用还是可以直接通过Consul或Eureka进行服务发现调用,这二者并不冲突,只是外部客户端是通过调用服务网关,服务网关通过Consul再具体路由到对应的微服务接口,而内部微服务则是直接通过Consul或者Eureka发现服务后直接进行调用。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
    #异常处理的差异

    面向外部的服务接口,我们一般会将接口的报文形式以JSON的方式进行响应,除了正常的数据报文外,我们一般会在报文格式中冗余一个响应码和响应信息的字段,如正常的接口成功返回:
     {
    "code" : "0",
    "msg" : "success",
    "data" : {
    "userId" : "zhangsan",
    "balance" : 5000
    }
    }

    而如果出现异常或者错误,则会相应地返回错误码和错误信息,如:
     {
    "code" : "-1",
    "msg" : "请求参数错误",
    "data" : null
    }

    在编写面向外部的服务接口时,服务端所有的异常处理我们都要进行相应地捕获,并在controller层映射成相应地错误码和错误信息,因为面向外部的是直接暴露给用户的,是需要进行比较友好的展示和提示的,即便系统出现了异常也要坚决向用户进行友好输出,千万不能输出代码级别的异常信息,否则用户会一头雾水。对于客户端而言,只需要按照约定的报文格式进行报文解析及逻辑处理即可,一般我们在开发中调用的第三方开放服务接口也都会进行类似的设计,错误码及错误信息分类得也是非常清晰!

    而微服务间彼此的调用在异常处理方面,我们则是希望更直截了当一些,就像调用本地接口一样方便,在基于Spring Cloud的微服务体系中,微服务提供方会提供相应的客户端SDK代码,而客户端SDK代码则是通过FeignClient的方式进行服务调用,如:而微服务间彼此的调用在异常处理方面,我们则是希望更直截了当一些,就像调用本地接口一样方便,在基于Spring Cloud的微服务体系中,微服务提供方会提供相应的客户端SDK代码,而客户端SDK代码则是通过FeignClient的方式进行服务调用,如:
    @FeignClient( value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class )
    public interface OrderClient {
    /[i] 订单(内) [/i]/
    @RequestMapping( value = "/order/createOrder", method = RequestMethod.POST )
    OrderCostDetailVo orderCost( @RequestParam(value = "orderId") String orderId,
    @RequestParam(value = "userId") long userId,
    @RequestParam(value = "orderType") String orderType,
    @RequestParam(value = "orderCost") int orderCost,
    @RequestParam(value = "currency") String currency,
    @RequestParam(value = "tradeTime") String tradeTime )
    }

    而服务的调用方在拿到这样的SDK后就可以忽略具体的调用细节,实现像本地接口一样调用其他微服务的内部接口了,当然这个是FeignClient框架提供的功能,它内部会集成像Ribbon和Hystrix这样的框架来实现客户端服务调用的负载均衡和服务熔断功能(注解上会指定熔断触发后的处理代码类),由于本文的主题是讨论异常处理,这里暂时就不作展开了。

    现在的问题是,虽然FeignClient向服务调用方提供了类似于本地代码调用的服务对接体验,但服务调用方却是不希望调用时发生错误的,即便发生错误,如何进行错误处理也是服务调用方希望知道的事情。另一方面,我们在设计内部接口时,又不希望将报文形式搞得类似于外部接口那样复杂,因为大多数场景下,我们是希望服务的调用方可以直截了的获取到数据,从而直接利用FeignClient客户端的封装,将其转化为本地对象使用。
    @Data
    @Builder
    public class OrderCostDetailVo implements Serializable {
    private String orderId;
    private String userId;
    private int status; /[i] 1:欠费状态;2:扣费成功 [/i]/
    private int orderCost;
    private String currency;
    private int payCost;
    private int oweCost;
    public OrderCostDetailVo( String orderId, String userId, int status, int orderCost, String currency, int payCost,
    int oweCost )
    {
    this.orderId = orderId;
    this.userId = userId;
    this.status = status;
    this.orderCost = orderCost;
    this.currency = currency;
    this.payCost = payCost;
    this.oweCost = oweCost;
    }
    }

    如我们在把返回数据就是设计成了一个正常的VO/BO对象的这种形式,而不是向外部接口那么样额外设计错误码或者错误信息之类的字段,当然,也并不是说那样的设计方式不可以,只是感觉会让内部正常的逻辑调用,变得比较啰嗦和冗余,毕竟对于内部微服务调用来说,要么对,要么错,错了就Fallback逻辑就好了。

    不过,话虽说如此,可毕竟服务是不可避免的会有异常情况的。如果内部服务在调用时发生了错误,调用方还是应该知道具体的错误信息的,只是这种错误信息的提示需要以异常的方式被集成了FeignClient的服务调用方捕获,并且不影响正常逻辑下的返回对象设计,也就是说我不想额外在每个对象中都增加两个冗余的错误信息字段,因为这样看起来不是那么优雅!

    既然如此,那么应该如何设计呢?
    #最佳实践设计

    首先,无论是内部还是外部的微服务,在服务端我们都应该设计一个全局异常处理类,用来统一封装系统在抛出异常时面向调用方的返回信息。而实现这样一个机制,我们可以利用Spring提供的注解@ControllerAdvice来实现异常的全局拦截和统一处理功能。如:
    @Slf4j
    @RestController
    @ControllerAdvice
    public class GlobalExceptionHandler {
    @Resource
    MessageSource messageSource;
    @ExceptionHandler( { org.springframework.web.bind.MissingServletRequestParameterException.class } )
    @ResponseBody
    public APIResponse processRequestParameterException( HttpServletRequest request,
    HttpServletResponse response,
    MissingServletRequestParameterException e )
    {
    response.setStatus( HttpStatus.FORBIDDEN.value() );
    response.setContentType( "application/json;charset=UTF-8" );
    APIResponse result = new APIResponse();
    result.setCode( ApiResultStatus.BAD_REQUEST.getApiResultStatus() );
    result.setMessage(
    messageSource.getMessage( ApiResultStatus.BAD_REQUEST.getMessageResourceName(),
    null, LocaleContextHolder.getLocale() ) + e.getParameterName() );
    return(result);
    }

    @ExceptionHandler( Exception.class )
    @ResponseBody
    public APIResponse processDefaultException( HttpServletResponse response,
    Exception e )
    {
    /[i] log.error("Server exception", e); [/i]/
    response.setStatus( HttpStatus.INTERNAL_SERVER_ERROR.value() );
    response.setContentType( "application/json;charset=UTF-8" );
    APIResponse result = new APIResponse();
    result.setCode( ApiResultStatus.INTERNAL_SERVER_ERROR.getApiResultStatus() );
    result.setMessage( messageSource.getMessage( ApiResultStatus.INTERNAL_SERVER_ERROR.getMessageResourceName(), null,
    LocaleContextHolder.getLocale() ) );
    return(result);
    }

    @ExceptionHandler( ApiException.class )
    @ResponseBody
    public APIResponse processApiException( HttpServletResponse response,
    ApiException e )
    {
    APIResponse result = new APIResponse();
    response.setStatus( e.getApiResultStatus().getHttpStatus() );
    response.setContentType( "application/json;charset=UTF-8" );
    result.setCode( e.getApiResultStatus().getApiResultStatus() );
    String message = messageSource.getMessage( e.getApiResultStatus().getMessageResourceName(),
    null, LocaleContextHolder.getLocale() );
    result.setMessage( message );
    /[i] log.error("Knowned exception", e.getMessage(), e); [/i]/
    return(result);
    }

    /**
    * 内部微服务异常统一处理方法
    */
    @ExceptionHandler( InternalApiException.class )
    @ResponseBody
    public APIResponse processMicroServiceException( HttpServletResponse response,
    InternalApiException e )
    {
    response.setStatus( HttpStatus.OK.value() );
    response.setContentType( "application/json;charset=UTF-8" );
    APIResponse result = new APIResponse();
    result.setCode( e.getCode() );
    result.setMessage( e.getMessage() );
    return(result);
    }
    }

    如上述代码,我们在全局异常中针对内部统一异常及外部统一异常分别作了全局处理,这样只要服务接口抛出了这样的异常就会被全局处理类进行拦截并统一处理错误的返回信息。

    理论上我们可以在这个全局异常处理类中,捕获处理服务接口业务层抛出的所有异常并统一响应,只是那样会让全局异常处理类变得非常臃肿,所以从最佳实践上考虑,我们一般会为内部和外部接口分别设计一个统一面向调用方的异常对象,如外部统一接口异常我们叫ApiException,而内部统一接口异常叫InternalApiException。这样,我们就需要在面向外部的服务接口controller层中,将所有的业务异常转换为ApiException;而在面向内部服务的controller层中将所有的业务异常转化为InternalApiException。如:
    @RequestMapping( value = "/creatOrder", method = RequestMethod.POST )
    public OrderCostDetailVo orderCost(
    @RequestParam(value = "orderId") String orderId,
    @RequestParam(value = "userId") long userId,
    @RequestParam(value = "orderType") String orderType,
    @RequestParam(value = "orderCost") int orderCost,
    @RequestParam(value = "currency") String currency,
    @RequestParam(value = "tradeTime") String tradeTime ) throws InternalApiException
    {
    OrderCostVo costVo = OrderCostVo.builder().orderId( orderId ).userId( userId ).busiId( busiId ).orderType( orderType )
    .duration( duration ).bikeType( bikeType ).bikeNo( bikeNo ).cityId( cityId ).orderCost( orderCost )
    .currency( currency ).strategyId( strategyId ).tradeTime( tradeTime ).countryName( countryName )
    .build();
    OrderCostDetailVo orderCostDetailVo;
    try {
    orderCostDetailVo = orderCostServiceImpl.orderCost( costVo );
    return(orderCostDetailVo);
    } catch ( VerifyDataException e ) {
    log.error( e.toString() );
    throw new InternalApiException( e.getCode(), e.getMessage() );
    } catch ( RepeatDeductException e ) {
    log.error( e.toString() );
    throw new InternalApiException( e.getCode(), e.getMessage() );
    }
    }

    如上面的内部服务接口的controller层中将所有的业务异常类型都统一转换成了内部服务统一异常对象InternalApiException了。这样全局异常处理类,就可以针对这个异常进行统一响应处理了。

    对于外部服务调用方的处理就不多说了。而对于内部服务调用方而言,为了能够更加优雅和方便地实现异常处理,我们也需要在基于FeignClient的SDK代码中抛出统一内部服务异常对象,如:
    @FeignClient( value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class )
    public interface OrderClient {
    /[i] 订单(内) [/i]/
    @RequestMapping( value = "/order/createOrder", method = RequestMethod.POST )
    OrderCostDetailVo orderCost( @RequestParam(value = "orderId") String orderId,
    @RequestParam(value = "userId") long userId,
    @RequestParam(value = "orderType") String orderType,
    @RequestParam(value = "orderCost") int orderCost,
    @RequestParam(value = "currency") String currency,
    @RequestParam(value = "tradeTime") String tradeTime ) throws InternalApiException
    };

    这样在调用方进行调用时,就会强制要求调用方捕获这个异常,在正常情况下调用方不需要理会这个异常,像本地调用一样处理返回对象数据就可以了。在异常情况下,则会捕获到这个异常的信息,而这个异常信息则一般在服务端全局处理类中会被设计成一个带有错误码和错误信息的json数据,为了避免客户端额外编写这样的解析代码,FeignClient为我们提供了异常解码机制。如:
    @Slf4j
    @Configuration
    public class FeignClientErrorDecoder implements feign.codec.ErrorDecoder {
    private static final Gson gson = new Gson();
    @Override
    public Exception decode( String methodKey, Response response )
    {
    if ( response.status() != HttpStatus.OK.value() )
    {
    if ( response.status() == HttpStatus.SERVICE_UNAVAILABLE.value() )
    {
    String errorContent;
    try {
    errorContent = Util.toString( response.body().asReader() );
    InternalApiException internalApiException = gson.fromJson( errorContent, InternalApiException.class );
    return(internalApiException);
    } catch ( IOException e ) {
    log.error( "handle error exception" );
    return(new InternalApiException( 500, "unknown error" ) );
    }
    }
    }
    return(new InternalApiException( 500, "unknown error" ) );
    }
    }

    我们只需要在服务调用方增加这样一个FeignClient解码器,就可以在解码器中完成错误消息的转换。这样,我们在通过FeignClient调用微服务时就可以直接捕获到异常对象,从而实现向本地一样处理远程服务返回的异常对象了。

    以上就是在利用Spring Cloud进行微服务拆分后关于异常处理机制的一点分享了,如有更好的方式,也欢迎大家给我留言!

    作者:若丨寒
    链接:https://www.jianshu.com/p/9fb7684bbeca

    使用Spring Cloud和Docker构建微服务架构

    老马 发表了文章 • 0 个评论 • 242 次浏览 • 2019-05-28 22:27 • 来自相关话题

    【编者的话】如何使用Spring Boot、Spring Cloud、Docker和Netflix的一些开源工具来构建一个微服务架构。本文通过使用Spring Boot、Spring Cloud和Docker构建的概念型应用示例,提供了了解常见的微服务架构模式 ...查看全部
    【编者的话】如何使用Spring Boot、Spring Cloud、Docker和Netflix的一些开源工具来构建一个微服务架构。本文通过使用Spring Boot、Spring Cloud和Docker构建的概念型应用示例,提供了了解常见的微服务架构模式的起点。

    该代码可以在GitHub上获得,并且在Docker Hub上提供了镜像。您只需要一个命令即可启动整个系统。

    我选择了一个老项目作为这个系统的基础,它的后端以前是单一应用。此应用提供了处理个人财务、整理收入开销、管理储蓄、分析统计和创建简单预测等功能。如果你想和更多Spring Cloud技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
    #功能服务
    整个应用分解为三个核心微服务。它们都是可以独立部署的应用,围绕着某些业务功能进行组织。
    1.png


    账户服务

    包含一般用户输入逻辑和验证:收入/开销记录、储蓄和账户设置。
    2.png


    统计服务

    计算主要的统计参数,并捕获每一个账户的时间序列。数据点包含基于货币和时间段正常化后的值。该数据可用于跟踪账户生命周期中的现金流量动态。
    3.png


    通知服务

    存储用户的联系信息和通知设置(如提醒和备份频率)。安排工作人员从其它服务收集所需的信息并向订阅的客户发送电子邮件。
    4.png


    注意

    * 每一个微服务拥有自己的数据库,因此没有办法绕过API直接访问持久数据。
    * 在这个项目中,我使用MongoDB作为每一个服务的主数据库。拥有一个多种类持久化架构(polyglot persistence architecture)也是很有意义的。
    * 服务间(Service-to-service)通信是非常简单的:微服务仅使用同步的REST API进行通信。现实中的系统的常见做法是使用互动风格的组合。例如,执行同步的GET请求检索数据,并通过消息代理(broker)使用异步方法执行创建/更新操作,以便解除服务和缓冲消息之间的耦合。然而,这带给我们是最终的一致性

    #基础设施服务
    分布式系统中常见的模式,可以帮助我们描述核心服务是怎样工作的。Spring Cloud提供了强大的工具,可以增强Spring Boot应用的行为来实现这些模式。我会简要介绍一下:
    5.png

    ##配置服务
    Spring Cloud Config是分布式系统的水平扩展集中式配置服务。它使用了当前支持的本地存储、Git和Subversion等可拔插存储库层(repository layer)。

    在此项目中,我使用了native profile,它简单地从本地classpath下加载配置文件。您可以在配置服务资源中查看shared目录。现在,当通知服务请求它的配置时,配置服务将响应回shared/notification-service.yml和shared/application.yml(所有客户端应用之间共享)。

    客户端使用

    只需要使用sprng-cloud-starter-config依赖构建Spring Boot应用,自动配置将会完成其它工作。

    现在您的应用中不需要任何嵌入的properties,只需要提供有应用名称和配置服务url的bootstrap.yml即可:
    spring:
    application:
    name: notification-service
    cloud:
    config:
    uri: http://config:8888
    fail-fast: true

    使用Spring Cloud Config,您可以动态更改应用配置

    比如,EmailService bean使用了@RefreshScope注解。这意味着您可以更改电子邮件的内容和主题,而无需重新构建和重启通知服务应用。

    首先,在配置服务器中更改必要的属性。然后,对通知服务执行刷新请求:curl -H "Authorization: Bearer #token#" -XPOST http://127.0.0.1:8000/notifications/refresh。

    您也可以使用webhook来自动执行此过程

    注意

    * 动态刷新存在一些限制。@RefreshScope不能和@Configuraion类一同工作,并且不会作用于@Scheduled方法。
    * fail-fast属性意味着如果Spring Boot应用无法连接到配置服务,将会立即启动失败。当您一起启动所有应用时,这非常有用。
    * 下面有重要的安全提示

    ##授权服务
    负责授权的部分被完全提取到单独的服务器,它为后端资源服务提供OAuth2令牌。授权服务器用于用户授权以及在周边内进行安全的机器间通信。

    在此项目中,我使用密码凭据作为用户授权的授权类型(因为它仅由本地应用UI使用)和客户端凭据作为微服务授权的授权类型。

    Spring Cloud Security提供了方便的注解和自动配置,使其在服务器端或者客户端都可以很容易地实现。您可以在文档中了解到更多信息,并在授权服务器代码中检查配置明细。

    从客户端来看,一切都与传统的基于会话的授权完全相同。您可以从请求中检索Principal对象、检查用户角色和其它基于表达式访问控制和@PreAuthorize注解的内容。

    PiggyMetrics(帐户服务、统计服务、通知服务和浏览器)中的每一个客户端都有一个范围:用于后台服务的服务器、用于浏览器展示的UI。所以我们也可以保护控制器避免受到外部访问,例如:
    @PreAuthorize("#oauth2.hasScope('server')")
    @RequestMapping(value = "accounts/{name}", method = RequestMethod.GET)
    public List getStatisticsByAccountName(@PathVariable String name) {
    return statisticsService.findByAccountName(name);
    }

    ##API网关
    您可以看到,有三个核心服务。它们向客户端暴露外部API。在现实系统中,这个数量可以非常快速地增长,同时整个系统将变得非常复杂。实际上,一个复杂页面的渲染可能涉及到数百个服务。

    理论上,客户端可以直接向每个微服务直接发送请求。但是这种方式是存在挑战和限制的,如果需要知道所有端点的地址,分别对每一段信息执行http请求,将结果合并到客户端。另一个问题是,这不是web友好协议,可能只在后端使用。

    通常一个更好的方法是使用API网关。它是系统的单个入口点,用于通过将请求路由到适当的后端服务或者通过调用多个后端服务并聚合结果来处理请求。此外,它还可以用于认证、insights、压力测试、金丝雀测试(canary testing)、服务迁移、静态响应处理和主动变换管理。

    Netflix开源这样的边缘服务,现在用Spring Cloud,我们可以用一个@EnabledZuulProxy注解来启用它。在这个项目中,我使用Zuul存储静态内容(UI应用),并将请求路由到适当的微服务。以下是一个简单的基于前缀(prefix-based)路由的通知服务配置:
    zuul:
    routes:
    notification-service:
    path: /notifications/**
    serviceId: notification-service
    stripPrefix: false

    这意味着所有以/notification开头的请求将被路由到通知服务。您可以看到,里面没有硬编码的地址。Zuul使用服务发现机制来定位通知服务实例以及断路器和负载均衡器,如下所述。
    ##服务发现
    另一种常见的架构模式是服务发现。它允许自动检测服务实例的网络位置,由于自动扩展、故障和升级,它可能会动态分配地址。

    服务发现的关键部分是注册。我使用Netflix Eureka进行这个项目,当客户端需要负责确定可以用的服务实例(使用注册服务器)的位置和跨平台的负载均衡请求时,Eureka就是客户端发现模式的一个很好的例子。

    使用Spring Boot,您可以使用spring-cloud-starter-eureka-server依赖、@EnabledEurekaServer注解和简单的配置属性轻松构建Eureka注册中心(Eureka Registry)。

    使用@EnabledDiscoveryClient注解和带有应用名称的bootstrap.yml来启用客户端支持:
    spring:
    application:
    name: notification-service

    现在,在应用启动时,它将向Eureka服务器注册并提供元数据,如主机和端口、健康指示器URL、主页等。Eureka接收来自从属于某服务的每个实例的心跳消息。如果心跳失败超过配置的时间表,该实例将从注册表中删除。

    此外,Eureka还提供了一个简单的界面,您可以通过它来跟踪运行中的服务和可用实例的数量:http://localhost:8761
    6.png

    ##负载均衡器、断路器和Http客户端
    Netflix OSS提供了另一套很棒的工具。

    Ribbon

    Ribbon是一个客户端负载均衡器,可以很好地控制HTTP和TCP客户端的行为。与传统的负载均衡器相比,每次线上调用都不需要额外的跳跃——您可以直接联系所需的服务。

    它与Spring Cloud和服务发现是集成在一起的,可开箱即用。Eureka客户端提供了可用服务器的动态列表,因此Ribbon可以在它们之间进行平衡。

    Hystrix

    Hystrix是断路器模式的一种实现,它可以通过网络访问依赖来控制延迟和故障。中心思想是在具有大量微服务的分布式环境中停止级联故障。这有助于快速失败并尽快恢复——自我修复在容错系统中是非常重要的。

    除了断路器控制,在使用Hystrix,您可以添加一个备用方法,在主命令失败的情况下,该方法将被调用以获取默认值。

    此外,Hystrix生成每个命令的执行结果和延迟的度量,我们可以用它来监视系统的行为

    Feign

    Feign是一个声明式HTTP客户端,能与Ribbon和Hystrix无缝集成。实际上,通过一个spring-cloud-starter-feign依赖和@EnabledFeignClients注解,您可以使用一整套负载均衡器、断路器和HTTP客户端,并附带一个合理的的默认配置。

    以下是账户服务的示例:
    @FeignClient(name = "statistics-service")
    public interface StatisticsServiceClient {
    @RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    void updateStatistics(@PathVariable("accountName") String accountName, Account account);
    }


    * 您需要的只是一个接口
    * 您可以在Spring MVC控制器和Feign方法之间共享@RequestMapping部分
    * 以上示例仅指定所需要的服务ID——statistics-service,这得益于Eureka的自动发现(但显然您可以使用特定的URL访问任何资源)。

    ##监控仪表盘
    在这个项目配置中,Hystrix的每一个微服务都通过Spring Cloud Bus(通过AMQP broker)将指标推送到Turbine。监控项目只是一个使用了TurbineHystrix仪表盘的小型Spring Boot应用。

    让我们看看系统行为在负载下:账户服务调用统计服务和它在一个变化的模拟延迟下的响应。响应超时阈值设置为1秒。
    7.png

    ##日志分析
    集中式日志记录在尝试查找分布式环境中的问题时非常有用。Elasticsearch、Logstash和Kibana技术栈可让您轻松搜索和分析您的日志、利用率和网络活动数据。在我的另一个项目中已经有现成的Docker配置。
    ##安全

    高级安全配置已经超过了此概念性项目的范围。为了更真实地模拟真实系统,请考虑使用https和JCE密钥库来加密微服务密码和配置服务器的properties内容(有关详细信息,请参阅文档)。
    #基础设施自动化

    部署微服务比部署单一的应用的流程要复杂得多,因为它们相互依赖。拥有完全基础设置自动化是非常重要的。我们可以通过持续交付的方式获得以下好处:

    * 随时发布软件的能力。
    * 任何构建都可能最终成为一个发行版本。
    * 构建工件(artifact)一次,根据需要进行部署。

    这是一个简单的持续交付工作流程,在这个项目的实现:

    在此配置中,Travis CI为每一个成功的Git推送创建了标记镜像。因此,每一个微服务在Docker Hub上的都会有一个latest镜像,而较旧的镜像则使用Git提交的哈希进行标记。如果有需要,可以轻松部署任何一个,并快速回滚。
    8.png

    #如何运行全部?
    这真的很简单,我建议您尝试一下。请记住,您将要启动8个Spring Boot应用、4个MongoDB实例和RabbitMq。确保您的机器上有4GB的内存。您可以随时通过网关、注册中心、配置、认证服务和账户中心运行重要的服务。

    运行之前

    * 安装Docker和Docker Compose。
    * 配置环境变量:CONFIG_SERVICE_PASSWORD, NOTIFICATION_SERVICE_PASSWORD, STATISTICS_SERVICE_PASSWORD, ACCOUNT_SERVICE_PASSWORD, MONGODB_PASSWORD

    生产模式

    在这种模式下,所有最新的镜像都将从Docker Hub上拉取。只需要复制docker-compose.yml并执行docker-compose up -d即可。

    开发模式

    如果您想自己构建镜像(例如,在代码中进行一些修改),您需要克隆所有仓库(repository)并使用Mavne构建工件(artifact)。然后,运行docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

    docker-compose.dev.yml继承了docker-compose.yml,附带额外配置,可在本地构建镜像,并暴露所有容器端口以方便开发。

    重要的端点(Endpoint)

    * localhost:80 —— 网关
    * localhost:8761 —— Eureka仪表盘
    * localhost:9000 —— Hystrix仪表盘
    * localhost:8989 —— Turbine stream(Hystrix仪表盘来源)
    * localhost:15672 —— RabbitMq管理

    注意

    所有Spring Boot应用都需要运行配置服务器才能启动。得益于Spring Boot的fail-fast属性和docker-compsoe的restart:always选项,我们可以同时启动所有容器。这意味着所有依赖的容器将尝试重新启动,直到配置服务器启动运行为止。

    此外,服务发现机制在所有应用启动后需要一段时间。在实例、Eureka服务器和客户端在其本地缓存中都具有相同的元数据之前,任何服务都不可用于客户端发现,因此可能需要3次心跳。默认的心跳周期为30秒。

    原文链接:Microservice Architectures With Spring Cloud and Docker(翻译:Oopsguy

    微服务网关实战——Spring Cloud Gateway

    博云BoCloud 发表了文章 • 0 个评论 • 247 次浏览 • 2019-05-24 17:29 • 来自相关话题

    作为Netflix Zuul的替代者,Spring Cloud Gateway是一款非常实用的微服务网关,在Spring Cloud微服务架构体系中发挥非常大的作用。本文对Spring Cloud Gateway常见使用场景进行了梳理,希望对微服务开发人员提供 ...查看全部
    作为Netflix Zuul的替代者,Spring Cloud Gateway是一款非常实用的微服务网关,在Spring Cloud微服务架构体系中发挥非常大的作用。本文对Spring Cloud Gateway常见使用场景进行了梳理,希望对微服务开发人员提供一些帮助。




    微服务网关SpringCloudGateway



    1.概述

    Spring cloud gateway是spring官方基于Spring 5.0、Spring Boot2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等。



    2.核心概念

    网关提供API全托管服务,丰富的API管理功能,辅助企业管理大规模的API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等贡呢。一般来说网关对外暴露的URL或者接口信息,我们统称为路由信息。如果研发过网关中间件或者使用过Zuul的人,会知道网关的核心是Filter以及Filter Chain(Filter责任链)。Sprig Cloud Gateway也具有路由和Filter的概念。下面介绍一下Spring Cloud Gateway中几个重要的概念。



    路由。路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配

    断言。Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于http request中的任何信息,比如请求头和参数等。

    过滤器。一个标准的Spring webFilter。Spring cloud gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理




    图片1.png





    如上图所示,Spring cloudGateway发出请求。然后再由Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway web handler。Handler再通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。



    快速入门


    以Spring Boot框架开发为例,启动一个Gateway服务模块(以Consul作为注册中心),一个后端服务模块。client端请求经gateway服务把请求路由到后端服务。



    前提条件:

    • Consul:版本1.5.0。
    • Spring bot:版本2.1.5。
    • Spring cloud:版本Greenwich.SR1。
    • Redis:版本5.0.5。
    1.微服务开发这里以使用Spring Boot框架开发微服务为例,启动一个服务并注册到Consul。引入依赖:
        org.springframework.cloud    spring-cloud-starter-consul-discovery
    注册服务到Consul,配置文件配置如下:
    spring:  application:    name: service-consumer  cloud:    consul:      host: 127.0.0.1      port: 8500      discovery:        service-name: service-consumer
    如下定义RestController,发布HTTP接口。
    @RestController@RequestMapping("/user")public class UserController {    @Resource    private UserService userService;    @GetMapping(value = "/info")    public User info() {        return userService.info();    }
    }

    注:此为服务端配置,经Gateway把请求路由转发到该服务上。

    2.网关配置创建一个Gateway服务,引入以下依赖:
        org.springframework.cloud    spring-cloud-starter-gateway    org.springframework.cloud    spring-cloud-starter-consul-discovery
    启动类配置如下:
    @SpringBootApplication@EnableDiscoveryClientpublic class GatewayApplication {    public static void main(String[] args) {        SpringApplication.run(GatewayApplication.class, args);    }
    }Spring Cloud Gateway对client端请求起到路由功能,主要配置如下:
    server:  port: 8098spring:  application:    name: service-gateway  cloud:    gateway:      discovery:        locator:          enabled: true             lower-case-service-id: true      consul:      host: 127.0.0.1 #注册gateway网关到consul      port: 8500      discovery:        service-name: service-gateway
    此时使用http://localhost:8089/service-consumer/user/info访问服务,网关即可对服务进行路由转发,把请求转发到具体后端服务上。此时,url中使用的url前缀service-consumer,是后端服务在Consul注册的服务名称转为小写字母以后的字符串。 最佳实践 01Gateway网关配置本文第二部分开发规范中定义了网关进行路由转发的配置,除了上述配置方式还可以使用下面的方式进行配置:
    gateway:      discovery:        locator:          enabled: true          lower-case-service-id: true      routes:      - id: service_consumer        uri: lb://service-consumer        predicates:        - Path= /consumer/**        filters:        - StripPrefix=1
    在上面的配置中,配置了一个Path的predicat,将以/consumer/**开头的请求都会转发到uri为lb://service-consumer的地址上,lb://service-consumer(注册中心中服务的名称)即service-consumer服务的负载均衡地址,并用StripPrefix的filter 在转发之前将/consumer去掉。同时将spring.cloud.gateway.discovery.locator.enabled改为false,如果不改的话,之前的http://localhost:8081/service-consumer/user/info这样的请求地址也能正常访问,因为这时为每个服务创建了2个router。本文第二部分和本节一共讲述了两种配置方式,两种配置都可以实现请求路由转发的功能。参数spring.cloud.gateway.discovery.locator.enabled为true,表明Gateway开启服务注册和发现的功能,并且Spring Cloud Gateway自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务。spring.cloud.gateway.discovery.locator.lowerCaseServiceId是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了)。
    gateway:      discovery:        locator:          enabled: true          lower-case-service-id: true
    02Gateway跨域访问Spring Cloud Gateway还针对跨域访问做了设计,可以使用以下配置解决跨域访问问题:
    spring:  cloud:    gateway:      globalcors:        corsConfigurations:          '[/**]':            allowedOrigins: "https://docs.spring.io"            allowedMethods:            - GET            allowHeaders:            - Content-Type
    在上面的示例中,允许来自https://docs.spring.io的get请求进行访问,并且表明服务器允许请求头中携带字段Content-Type。03Gateway 过滤器Spring Cloud Gateway的filter生命周期不像Zuul那么丰富,它只有两个:“pre”和“post”:pre:这种过滤器在请求被路由之前调用。可以利用这个过滤器实现身份验证、在集群中选择请求的微服务、记录调试的信息。post:这种过滤器在路由到服务器之后执行。这种过滤器可用来为响应添加HTTP Header、统计信息和指标、响应从微服务发送给客户端等。Spring Cloud gateway的filter分为两种:GatewayFilter和Globalfilter。GlobalFilter会应用到所有的路由上,而Gatewayfilter将应用到单个路由或者一个分组的路由上。利用Gatewayfilter可以修改请求的http的请求或者是响应,或者根据请求或者响应做一些特殊的限制。更多时候可以利用Gatewayfilter做一些具体的路由配置。下面的配置是AddRequestParameter Gatewayfilter的相关配置。
    spring:  application:    name: service-gateway  cloud:    gateway:     discovery:        locator:         enabled: true     routes:     - id: parameter_route      uri: http://localhost:8504/user/info      filters:      - AddRequestParameter=foo, bar      predicates:      - Method=GET
    上述配置中指定了转发的地址,设置所有的GET方法都会自动添加foo=bar,当请求符合上述路由条件时,即可在后端服务上接收到Gateway网关添加的参数。另外再介绍一种比较常用的filter,即StripPrefix gateway filter。配置如下:
    spring:  cloud:    gateway:      routes:      - id: stripprefixfilter        uri: lb://service-consumer        predicates:        - Path=/consumer/**        filters:        - StripPrefix=1
    当client端使用http://localhost:8098/consumer/user/info路径进行请求时,如果根据上述进行配置Gateway会将请求转换为http://localhost:8098/service-consumer/user/info。以此作为前端请求的最终目的地。04Gateway请求匹配Gateway网关可以根据不同的方式进行匹配进而把请求分发到不同的后端服务上。通过header进行匹配,把请求分发到不同的服务上,配置如下:
    spring:  cloud:    gateway:      routes:      - id: header_route        uri: http://baidu.com        predicates:        - Header=X-Request-Id, \d+
    通过curl测试:curl http://localhost:8080 -H "X-Request-Id:666666",返回页面代码证明匹配成功。如果是以Host进行匹配,配置如下:
    spring:  cloud:    gateway:      routes:      - id: host_route        uri: http://baidu.com        predicates:        - Host=**.baidu.com
    通过curl http://localhost:8098 -H "Host: www.baidu.com"进行测试,返回页面代码即转发成功。可以通过POST、GET、PUT、DELTE等不同的方式进行路由:
    spring:  cloud:    gateway:      routes:      - id: method_route        uri: http://baidu.com        predicates:        - Method=GET
    通过 curl http://localhost:8098 进行测试,返回页面代码即表示成功。上述是单个匹配进行路由,如果把多个匹配合在一起进行路由,必须满足所有的路有条件才会进行路由转发。05Gateway熔断Spring Cloud Gateway也可以利用Hystrix的熔断特性,在流量过大时进行服务降级,同时项目中必须加上Hystrix的依赖。
      org.springframework.cloud  spring-cloud-starter-netflix-hystrix    
    配置后,Gateway将使用fallbackcmd作为名称生成HystrixCommand对象进行熔断处理。如果想添加熔断后的回调内容,需要添加以下配置:
    spring:  cloud:    gateway:      routes:      - id: hystrix_route        uri: lb://consumer-service        predicates:        - Path=/consumer/**        filters:        - name: Hystrix          args:            name: fallbackcmd            fallbackUri: forward:/fallback        - StripPrefix=1hystrix:    command:    fallbackcmd:      execution:        isolation:          thread:            timeoutInMilliseconds: 5000 #超时时间,若不设置超时时间则有可能无法触发熔断
    上述配置中给出了熔断之后返回路径,因此,在Gateway服务模块添加/fallback路径,以作为服务熔断时的返回路径。
    @RestControllerpublic class GatewayController {    @RequestMapping(value = "/fallback")    public String fallback(){        return "fallback nothing";    }
    }fallbackUri: forward:/fallback配置了 fallback 时要会调的路径,当调用 Hystrix 的 fallback 被调用时,请求将转发到/fallback这个 URI,并以此路径的返回值作为返回结果。06Gateway重试路由器通过简单的配置,Spring Cloud Gateway就可以支持请求重试功能。
    spring:  cloud:    gateway:      routes:      - id: header_route        uri: http://localhost:8504/user/info        predicates:        - Path=/user/**        filters:        - name: Retry          args:            retries: 3            status: 503        - StripPrefix=1
    Retry GatewayFilter通过四个参数来控制重试机制,参数说明如下:
    • retries:重试次数,默认值是 3 次。
    • statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus。
    • methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod。
    • series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5个值。
    使用上述配置进行测试,当后台服务不可用时,会在控制台看到请求三次的日志,证明此配置有效。07Gateway 限流操作Spring Cloud Gateway本身集成了限流操作,Gateway限流需要使用Redis,pom文件中添加Redis依赖:
        org.springframework.boot    spring-boot-starter-data-redis-reactive
    配置文件中配置如下:
    spring:  cloud:    gateway:      routes:      - id: rate_limit_route        uri: lb://service-consumer        predicates:        - Path=/user/**        filters:        - name: RequestRateLimiter          args:            key-resolver: "#{@hostAddrKeyResolver}"            redis-rate-limiter.replenishRate: 1            redis-rate-limiter.burstCapacity: 3        - StripPrefix=1    consul:      host: 127.0.0.1      port: 8500      discovery:        service-name: service-gateway        instance-id: service-gateway-233  redis:    host: localhost    port: 6379
    在上面的配置问价中,配置了Redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
    • BurstCapacity:令牌桶的总容量。
    • replenishRate:令牌通每秒填充平均速率。
    • Key-resolver:用于限流的解析器的Bean对象的名字。它使用SpEL表达式#{@beanName}从Spring容器中获取bean对象。

    注意:filter下的name必须是RequestRateLimiter。





    Key-resolver参数后面的bean需要自己实现,然后注入到Spring容器中。KeyResolver需要实现resolve方法,比如根据ip进行限流,则需要用hostAddress去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。还可以根据uri限流,同hostname限流是一样的。例如以ip限流为例,在gateway模块中添加以下实现:



    public class HostAddrKeyResolver implements KeyResolver {

    @Override
    public Mono resolve(ServerWebExchange exchange) {
    return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }

    public HostAddrKeyResolver hostAddrKeyResolver() {
    return new HostAddrKeyResolver();
    }
    }


    把该类注入到spring容器中:



    @SpringBootApplication
    @EnableDiscoveryClient
    public class GatewayApplication {

    public static void main(String[] args) {
    SpringApplication.run(GatewayApplication.class, args);
    }

    @Bean
    public HostAddrKeyResolver hostAddrKeyResolver(){
    return new HostAddrKeyResolver();
    }
    }



    基于上述配置,可以对请求基于ip的访问进行限流。



    08

    自定义Gatewayfilter



    Spring Cloud Gateway内置了过滤器,能够满足很多场景的需求。当然,也可以自定义过滤器。在Spring Cloud Gateway自定义过滤器,过滤器需要实现GatewayFilter和Ordered这两个接口。

    下面的例子实现了Gatewayfilter,它可以以log日志的形式记录每次请求耗费的时间,具体实现如下:



    public class RequestTimeFilter implements GatewayFilter, Ordered {
    private static final Log log = LogFactory.getLog(GatewayFilter.class);
    private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
    return chain.filter(exchange).then(
    Mono.fromRunnable(() -> {
    Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
    if (startTime != null) {
    log.info("请求路径:"+exchange.getRequest().getURI().getRawPath() + "消耗时间: " + (System.currentTimeMillis() - startTime) + "ms");
    }
    })
    );
    }
    @Override
    public int getOrder() {
    return 0;
    }
    }



    上述代码中定义了自己实现的过滤器。Ordered的int getOrder()方法是来给过滤器定优先级的,值越大优先级越低。还有一个filter(ServerWebExchange exchange, GatewayFilterChain chain)方法,在该方法中,先记录了请求的开始时间,并保存在ServerWebExchange中,此处是一个“pre”类型的过滤器。然后再chain.filter()的内部类中的run()方法中相当于"post"过滤器,在此处打印了请求所消耗的时间。



    接下来将该过滤器注册到router中,代码如下。



     @Bean
    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
    .route(r -> r.path("/user/**")
    .filters(f -> f.filter(new RequestTimeFilter())
    .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
    .uri("http://localhost:8504/user/info")
    .order(0)
    .id("customer_filter_router")
    )
    .build();
    }



    除了上述代码的方式配置我们自定义的过滤器的方式之外,也可以在application.yml文件中直接配置,这里不再赘述。



    启动程序,通过curl http://localhost:8098/user/info控制台会打印出请求消耗时间,日志如下:



    ....
    2019-05-22 15:13:31.221 INFO 19780 --- [ctor-http-nio-4] o.s.cloud.gateway.filter.GatewayFilter : 请求路径:/user/info消耗时间: 54ms
    ...
    2019-05-22 16:46:23.785 INFO 29928 --- [ctor-http-nio-1] o.s.cloud.gateway.filter.GatewayFilter : 请求路径:/user/info3消耗时间: 5ms
    ....




    09

    自定义GlobalFilter


    Spring Cloud Gateway根据作用范围分为GatewayFilter和GlobalFilter,二者区别如下:

    GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上。

    GlobalFilter:全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。





    在上一小节中定义的是Gatewayfilter,下面实现的是Globalfilter:



    public class TokenFilter implements GlobalFilter, Ordered {
    Logger logger= LoggerFactory.getLogger( TokenFilter.class );
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    String token = exchange.getRequest().getQueryParams().getFirst("token");
    if (token == null || token.isEmpty()) {
    logger.info( "token 为空,无法进行访问." );
    exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
    return exchange.getResponse().setComplete();
    }
    return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
    return 0;
    }
    }



    上述代码实现了Globalfilter,具体逻辑是判断请求中是否含参数token,如果没有,则校验不通过,对所有请求都有效。如果含有token则转发到具体后端服务上,如果没有则校验不通过。



    通过curl http://localhost:8098/user/info进行访问,因为路径中不含有参数token,则无法通过校验,打印日志如下:



    2019-05-22 15:27:11.078  INFO 5956 --- [ctor-http-nio-1] com.song.gateway.TokenFilter             : token 为空,无法进行访问.
    ...




    通过curl http://localhost:8098/user/info?token=123进行访问时,则可以获取到后端服务返回结果。



    服务迁移之路 | Spring Cloud向Service Mesh转变

    博云BoCloud 发表了文章 • 0 个评论 • 298 次浏览 • 2019-05-20 17:39 • 来自相关话题

    导读 Spring Cloud基于Spring Boot开发,提供一套完整的微服务解决方案,具体包括服务注册与发现,配置中心,全链路监控,API网关,熔断器,远程调用框架,工具客户端等选项中立的开源组件,并且可以根据需求对部分组件进行 ...查看全部
    导读

    Spring Cloud基于Spring Boot开发,提供一套完整的微服务解决方案,具体包括服务注册与发现,配置中心,全链路监控,API网关,熔断器,远程调用框架,工具客户端等选项中立的开源组件,并且可以根据需求对部分组件进行扩展和替换。

    Service Mesh,这里以Istio(目前Service Mesh具体落地实现的一种,且呼声最高)为例简要说明其功能。 Istio 有助于降低这些部署的复杂性,并减轻开发团队的压力。它是一个完全开源的服务网格,可以透明地分层到现有的分布式应用程序上。它也是一个平台,包括允许它集成到任何日志记录平台、遥测或策略系统的 API。Istio的多样化功能集使你能够成功高效地运行分布式微服务架构,并提供保护、连接和监控微服务的统一方法。如果你想和更多 Service Mesh 技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

    从上面的简单介绍中,我们可以看出为什么会存在要把Spring Cloud体系的应用迁移到Service Mesh这样的需求,总结下来,有四方面的原因:

    1. 功能重叠

    来简单看一下他们的功能对比:



    微信截图_20190520171338.png




    从上面表格中可以看到,如果从功能层面考虑,Spring Cloud与Service Mesh在服务治理场景下,有相当大量的重叠功能,从这个层面而言,为Spring Cloud向Service Mesh迁移提供了一种潜在的可能性。



    2. 服务容器化

    在行业当前环境下,还有一个趋势,或者说是现状。越来越多的应用走在了通往应用容器化的道路上,或者在未来,容器化会成为应用部署的标准形态。而且无论哪种容器化运行环境,都天然支撑服务注册发现这一基本要求,这就导致Spring Cloud体系应用上容器的过程中,存在一定的功能重叠,有可能为后期的应用运维带来一定的影响,而Service Mesh恰恰需要依赖容器运行环境,同时弥补了容器环境所欠缺的内容(后续会具体分析)。



    3. 术业有专攻

    从软件设计角度出发,我们一直在追求松耦合的架构,也希望做到领域专攻。例如业务开发人员希望我只要关心业务逻辑即可,不需要关心链路跟踪,熔断,服务注册发现等支撑工具的服务;而平台支撑开发人员,则希望我的代码中不要包含任何业务相关的内容。而Service Mesh的出现,让这种情况成为可能。



    4. 语言壁垒

    目前而言Spring Cloud虽然提供了对众多协议的支持,但是受限于Java技术体系。这就要求应用需要在同一种语言下进行开发(这不一定是坏事儿),在某种情况下,不一定适用于一些工作场景。而从微服务设计考虑,不应该受限于某种语言,各个服务应该能够相互独立,大家需要的是遵循通信规范即可。而Service Mesh恰好可以消除服务间的语言壁垒,同时实现服务治理的能力。


    基于以上四点原因,当下环境,除了部分大多已经提前走在了Service Mesh实践的道路上互联网大厂以外(例如蚂蚁金服的SOFASTACK),也有大部分企业已经开始接触Service Mesh,并且尝试把Spring Cloud构建的应用,迁移到Service Mesh中。



    #Spring Cloud向Service Mesh的迁移方案



    Spring Cloud向Service Mesh迁移,从我们考虑而言大体分为七个步骤,如图所示:



    图片1.png




    1. Spring Cloud架构解析

    Spring Cloud架构解析的目的在于确定需要从当前的服务中去除与Service Mesh重叠的功能,为后续服务替换做准备。我们来看一个典型的Spring Cloud架构体系,如图所示:



    图片2.png






    从图中我们可以简要的分析出,一个基于Spring Cloud的微服务架构,主要包括四部分内容:服务网关,应用服务,外围支撑组件,服务管理控制台。



    • 服务网关
    服务网关涵盖的功能包括路由,鉴权,限流,熔断,降级等对入站请求的统一拦截处理。具体可以进一步划分为外部网关(面向互联网)和内部网关(面向服务内部管理)。
    • 应用服务
    应用服务是企业业务核心。应用服务内部由三部分内容构成:业务逻辑实现,外部组件交互SDK集成,服务内部运行监控集成。
    • 外围支撑组件
    外围支撑组件,涵盖了应用服务依赖的工具,包括注册中心,配置中心,消息中心,安全中心,日志中心等。
    • 服务管理控制台
    服务管理控制台面向服务运维或者运营人员,实现对应用服务运行状态的实时监控,以及根据情况需要能够动态玩成在线服务的管理和配置。



    这里面哪些内容是我们可以拿掉或者说基于Service Mesh(以Istio为例)能力去做的?分析下来,可以替换的组件包括网关(gateway或者Zuul,由Ingress gateway或者egress替换),熔断器(hystrix,由SideCar替换),注册中心(Eureka及Eureka client,由Polit,SideCar替换),负责均衡(Ribbon,由SideCar替换),链路跟踪及其客户端(Pinpoint及Pinpoint client,由SideCar及Mixer替换)。这是我们在Spring Cloud解析中需要完成的目标:即确定需要删除或者替换的支撑模块。



    2. 服务改造

    服务单元改造的目的在于基于第一步的解析结果,完成依赖去除或者依赖替换。根据第一步的分析结果服务单元改造分为三步:

    · 删除组件,包括网关,熔断器,注册中心,负载均衡,链路跟踪组件,同时删除对应client的SDK;
    · 替换组件,采用httpClient的SDK支持http协议的远程调用(原来在Ribbon中),由原来基于注册中心的调用,转变成http直接调用;
    · 配置信息变更,修改与删除组件管理的配置信息以及必要的组件交互代码(根据实际应用情况操作);

    当然服务单元改造过程中,还会涉及到很多的细节问题,都需要根据应用特点进行处理,这里不做深入分析。



    3. 服务容器化

    服务容器化是目前应用部署的趋势所在。服务容器化本身有很多不同的方式,例如基于Jenkins的pipeline实现,基于docker-maven-plugin + dockerfile实现,当然还有很多不同的方式。这里以Spring Cloud一个demo服务通过docker-maven-plugin+dockerfile实现说明为例:



    简易的一个服务的Dockerfile如下所示:


    ROM openjdk:8-jre-alpine
    ENV TZ=Asia/Shanghai \
    SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
    JAVA_OPTS="" \
    JHIPSTER_SLEEP=0
    RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
    CMD echo "The application will start in ${JHIPSTER_SLEEP}s..." && \
    sleep ${JHIPSTER_SLEEP} && \
    java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /app.jar
    # java ${JAVA_OPTS} -Djava.security.egd=environment:/dev/./urandom -jar /app.@project.packaging@

    EXPOSE 8080
    ADD microservice-demo.jar /app.jar



    文件中定义了服务端口以及运行命令。


    Maven-docker-plugin的插件配置如下所示:



    microservice-demo

    ......

    com.spotify
    docker-maven-plugin
    1.2.0


    build-image
    package

    build



    tag-image
    package

    tag


    ${project.build.finalName}:${project.version}
    ${docker.registry.name}/${project.build.finalName}:${project.version}





    ${project.basedir}/src/main/docker
    ${project.build.finalName}:${project.version}


    /
    ${project.build.directory}
    ${project.build.finalName}.${project.packaging}








    通过增加docker-maven-plugin,在执行mvn package的时候可以加载Dockerfile,自动构建服务的容器镜像(需要说明的前提是本地安装docker运行环境,或者通过环境变量在开发工具中配置Docker的远程连接环境),从而完成服务容器化改造。



    4. 容器环境构建

    容器环境决定这Service Mesh的部署形态,这里不详细描述容器环境的部署过程。感兴趣的朋友,可以参考https://github.com/easzlab/kubeasz 开源项目,提供了Kubernetes基于ansible的自动化部署脚本。我们也建议选择Kubernetes来构建容器环境。这里说明容器环境构建的考虑因素:


    · 集群部署方案
    集群部署方案主要考虑多集群,跨数据中心,存储选择,网络方案,集群内部主机标签划分,集群内部网络地址规划等多方面因素。

    · 集群规模
    集群规模主要考虑etcd集群大小,集群内运行实例规模(用来配置ip范围段),集群高可用节点规模等因素。


    基于以上两点来考虑容器化环境的部署方案,关键是合理规划,避免资源浪费。



    5. Service Mesh环境构建

    Service Mesh环境构建依赖于容器环境构建,主要考虑两个方面,以Isito为例:


    · 部署插件
    Istio部署插件需要根据需要的场景,考虑采用的插件完整性,例如prometheus,kiali,是否开启TLS等,具体安装选项可以参考https://preliminary.istio.io/zh/docs/reference/config/installation-options/。

    · 跨集群部署
    依据容器环境考虑是否需要支持Isito的跨集群部署方案.



    6. 服务注入

    服务注入用于将容器化的服务接入到Service Mesh的平台中,目前主要有两种方式。以Isito为例说明,主要包括自动注入和手动入住。选择手动注入的目的在于可以根据企业内部上线流程,对服务接入进行人为控制。而自动注入则能够更加快捷,方便。到此实际上已经完成服务迁移工作。



    7. 服务管理控制台

    由于Service Mesh目前而言,多是基于声明式的配置文件,达到服务治理的效果,因此无法实时传递执行结果。基于这种原因,需要一个独立的Service Mesh的管理控制台,一方面能够查看各个服务的运行状态以及策略执行情况,另外一方面能够支持服务运行过程中策略的动态配置管理。目前而言,可以在Isito安装过程中选择kiali作为一个控制台实现,当然未来也会有大量的企业提供专门的服务。


    通过以上七个步骤,能够在一定程度上帮助企业应用,从Spring Cloud迁移到Service Mesh上,但迁移过程中必然存在不断踩坑的过程,需要根据应用特点,事前做好评估规划。



    #迁移优缺点分析

    Spring Cloud迁移到Service Mesh是不是百利而无一害呢?

    首先,从容器化的环境出发,后续Knative,Kubernetes,Service Mesh必然会构建出一套相对完整的容器化PaaS解决方案,从而完成容器化PaaS支撑平台的构建。Service Mesh将为容器运行态提供保驾护航的作用。

    其次,就目前Service Mesh的落地实现而言,对于一些特定需求的监测粒度有所欠缺,例如调用线程栈的监测(当然,从网络层考虑,或者不在Service Mesh的考虑范围之内),但是恰恰在很多服务治理场景的要求范围之中。我们也需要针对这种情况,考虑实现方案。

    最后,大家一直诟病的性能和安全问题。目前已经有所加强,但是依然被吐槽。

    整体而言,Spring Cloud是微服务实现服务治理平台的现状,而Service Mesh却是未来,当然也不能完全取而代之,毕竟设计思路和侧重点不同,是否迁移需要根据业务场景而定。


    本文由博云研究院原创发表,转载请注明出处。

    一张图了解Spring Cloud微服务架构

    老马 发表了文章 • 0 个评论 • 600 次浏览 • 2019-04-26 20:48 • 来自相关话题

    Spring Cloud作为当下主流的微服务框架,可以让我们更简单快捷地实现微服务架构。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置 ...查看全部
    Spring Cloud作为当下主流的微服务框架,可以让我们更简单快捷地实现微服务架构。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。Spring Cloud中各个组件在微服务架构中扮演的角色如下图所示,黑线表示注释说明,蓝线由A指向B,表示B从A处获取服务。
    6295401-8076ec880947ba79.png

    Spring Cloud组成的微服务架构图

    由上图所示微服务架构大致由上图的逻辑结构组成,其包括各种微服务、注册发现、服务网关、熔断器、统一配置、跟踪服务等。下面说说Spring Cloud中的组件分别充当其中的什么角色。

    Fegin(接口调用):微服务之间通过Rest接口通讯,Spring Cloud提供Feign框架来支持Rest的调用,Feign使得不同进程的Rest接口调用得以用优雅的方式进行,这种优雅表现得就像同一个进程调用一样。

    Netflix eureka(注册发现):微服务模式下,一个大的Web应用通常都被拆分为很多比较小的Web应用(服务),这个时候就需要有一个地方保存这些服务的相关信息,才能让各个小的应用彼此知道对方,这个时候就需要在注册中心进行注册。每个应用启动时向配置的注册中心注册自己的信息(IP地址,端口号, 服务名称等信息),注册中心将他们保存起来,服务间相互调用的时候,通过服务名称就可以到注册中心找到对应的服务信息,从而进行通讯。注册与发现服务为微服务之间的调用带来了方便,解决了硬编码的问题。服务间只通过对方的服务ID,而无需知道其IP和端口即可以获取对方方服务。

    Ribbon(负载均衡):Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon,配置服务提供者的地址列表后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机等。当然,我们也可为Ribbon实现自定义的负载均衡算法。在Spring Cloud中,当Ribbon与Eureka配合使用时,Ribbon可自动从EurekaServer获取服务提供者的地址列表,并基于负载均衡算法,请求其中一个服务提供者的实例(为了服务的可靠性,一个微服务可能部署多个实例)。

    Hystrix(熔断器):当服务提供者响应非常缓慢,那么消费者对提供者的请求就会被强制等待,直到提供者响应或超时。在高负载场景下,如果不做任何处理,此类问题可能会导致服务消费者的资源耗竭甚至整个系统的崩溃(雪崩效应)。Hystrix正是为了防止此类问题发生。Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。Hystrix主要通过以下几点实现延迟和容错。

    * 包裹请求:使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用了设计模式中的“命令模式”。
    * 跳闸机制:当某服务的错误率超过一定阈值时,Hystrix可以自动或者手动跳闸,停止请求该服务一段时间。
    * 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。
    * 监控:Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时和被拒绝的请求等。
    * 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑可由开发人员指定。

    Zuul(微服务网关):不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求。例如一个电影购票的手机APP,可能调用多个微服务的接口才能完成一次购票的业务流程,如果让客户端直接与各个微服务通信,会有以下的问题:

    * 客户端会多次请求不同的微服务,增加了客户端的复杂性。
    * 存在跨域请求,在一定场景下处理相对复杂。
    * 认证复杂,每个服务都需要独立认证。
    * 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将很难实施。
    * 某些微服务可能使用了对防火墙/浏览器不友好的协议,直接访问时会有一定的困难。

    以上问题可借助微服务网关解决。微服务网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过微服务网关。使用微服务网关后,微服务网关将封装应用程序的内部结构,客户端只用跟网关交互,而无须直接调用特定微服务的接口。这样,开发就可以得到简化。不仅如此,使用微服务网关还有以下优点:

    * 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
    * 易于认证。可在微服务网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
    * 减少了客户端与各个微服务之间的交互次数。

    Spring Cloud Bus( 统一配置服务):对于传统的单体应用,常使用配置文件管理所有配置。例如一个SpringBoot开发的单体应用,可将配置内容放在application.yml文件中。如果需要切换环境,可设置多个Profile,并在启动应用时指定spring.profiles.active={profile}。然而,在微服务架构中,微服务的配置管理一般有以下需求:

    * 集中管理配置。一个使用微服务架构的应用系统可能会包含成百上千个微服务,因此集中管理配置是非常有必要的。
    * 不同环境,不同配置。例如,数据源配置在不同的环境(开发、测试、预发布、生产等)中是不同的。
    * 运行期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小或熔断阈值,并且在调整配置时不停止微服务。
    * 配置修改后可自动更新。如配置内容发生变化,微服务能够自动更新配置。综上所述,对于微服务架构而言,一个通用的配置管理机制是必不可少的,常见做法是使用配置服务器管理配置。Spring Cloud Bus利用Git或SVN等管理配置、采用Kafka或者RabbitMQ等消息总线通知所有应用,从而实现配置的自动更新并且刷新所有微服务实例的配置。

    Sleuth+ZipKin(跟踪服务):Sleuth和Zipkin结合使用可以通过图形化的界面查看微服务请求的延迟情况以及各个微服务的依赖情况。需要注意的是Spring Boot 2及以上不在支持Zipkin的自定义,需要到官方网站下载ZipKin相关的jar包。另外需要提一点的是Spring Boot Actuator,提供了很多监控端点如/actuator/info、/actuator/health、/acutator/refresh等,可以查看微服务的信息、健康状况、刷新配置等。

    原文链接:一张图了解Spring Cloud微服务架构(作者:SimpleEasy)

    从 Spring Cloud 看一个微服务框架的「五脏六腑」

    齐达内 发表了文章 • 0 个评论 • 562 次浏览 • 2019-03-31 11:53 • 来自相关话题

    Spring Cloud 是一个基于 Spring Boot 实现的微服务框架,它包含了实现微服务架构所需的各种组件。 注:Spring Boot 简单理解就是简化 Spring 项目的搭建、配置、组合的框架。因为与构建微服务本身没有 ...查看全部
    Spring Cloud 是一个基于 Spring Boot 实现的微服务框架,它包含了实现微服务架构所需的各种组件。

    注:Spring Boot 简单理解就是简化 Spring 项目的搭建、配置、组合的框架。因为与构建微服务本身没有直接关系,所以本文不对 Spring Boot 进行展开。另外本文有一些例子涉及到 Spring 和 Spring Boot,建议先了解一下 Spring 和 Spring Boot 再阅读本文。



    本文的阅读对象主要是没有接触过服务架构,想对其有一个宏观的了解的同学。

    本文将从 Spring Cloud 出发,分两小节讲述微服务框架的「五脏六腑」:

    * 第一小节「服务架构」旨在说明的包括两点,一服务架构是什么及其必要性;二是服务架构的基本组成。为什么第一节写服务架构而不是微服务架构呢?原因主要是微服务架构本身与服务架构有着千丝万缕的关系,服务架构是微服务架构的根基。
    * 第二小节「五脏六腑」则将结合 Spring Cloud 这个特例来介绍一个完整的微服务框架的组成。

    #服务架构
    为了方便理解,我先讲一个小故事(改编自一知乎答主):

    Martin(微服务提出者也叫 Martin)刚来到公司时是一个基层员工,它上面有经理、老板,那个时候所有人都听老板的指挥。

    但是过了两年,公司的人越来越多,原来的模式下整个公司的运作效率太低,管理也很混乱。

    于是已经踏上中层岗位的 Martin 建议老板进行部门划分(服务化),专门的部门只做专门的事情(单一职责)。例如研发部门只做研发,人事部门只做招聘。

    老板听取了 Martin 的意见,对公司的组织架构进行了调整。

    有一天,Martin 发现公司的部门越来越多,各个部门并不能完全知道对方所做的事情,这对跨部门协作(服务调用)带来了困难。

    行政部门会(注册中心)来记录所有的部门,每当有新的部门行政都会记录下来(服务注册),然后公布出来让所有部门知道(服务发现)。

    在新的组织架构下,公司的效率逐步提高。老板也给 Martin 发了大量奖金作为奖励,Martin 从此赢取白富美走向了人生巅峰。

    这是一个公司组织架构演变的故事,主要讲的是随着公司规模的扩大,组织从集中化管理到分布化管理的过程。

    映射到我们的信息系统里来也是一样的,随着我们的系统越来越复杂,变得难以管理,也有人想到去拆分然后治理。在解决复杂问题上,分治可以说是一个屡试不爽的办法。

    服务化即是拆解的一种手段。而上面圆括号里面的内容其实就对应了一个服务化架构的最小组成元素,分别是服务、服务调用、注册中心、服务注册、服务发现。有了这些基本的组成要素,就可以实现一个最简单的服务架构。
    ##面向服务的架构和微服务架构
    面向服务的架构(SOA)和微服务架构是目前两种主流的服务化架构,都符合上面的例子,也有上面提到的所有组件。这两种服务架构有很多可以讲的,但是与本文的相关性不大,本文不做会过多展开,只简单介绍一下两者的区别。

    准确地说微服务是去 ESB(企业服务总线)的 SOA。ESB 借鉴了计算机组成原理中的通信模型 —— 总线,所有需要和外部系统通信的系统,通过 ESB 进行标准化地转换从而消除协议、异构系统之间的差异,这样就可以利用现有的系统构建一个全新的松耦合的异构的分布式系统。微服务架构去掉 ESB,本质上是一种去中心化的思想。
    #五脏六腑
    ##「心脏」
    顺着上一节的思路,从最简单、最核心的问题出发,假设服务 A 要调用服务 B,会有什么问题?

    * 服务在哪?(服务治理问题)
    * 怎么调用?(服务调用问题)

    这两个是最核心的问题,也是任何微服务框架首要解决的两个问题。

    为了解决第一个问题 Spring Cloud 提供了 Eureka、ZooKeeper、Cloud Foundry、Consul 等服务治理框架的集成。它们的工作模式是将所有的微服务注册到一个 Server 上,然后通过心跳进行服务健康监测。这样服务 A 调用 B 时可以从注册中心拿到可用的服务 B 的地址、端口进行调用。

    第二个服务调用有人可能认为就是一个简单的 HTTP 或者 RPC 调用,不是什么问题。但是在分布式的场景下,服务调用需要考虑的因素会更多。比如一个服务有多个实例,此时请求进来了交给谁处理,请求的负载怎么平衡到各个实例,都是比较棘手的问题。Spring Cloud 提供了两种服务调用的方式:一种是 Ribbon + restTemplate,另一种是 Feign。

    其中 Ribbon 是基于 HTTP 和 TCP 客户端的负载均衡器,restTemplate 是 Spring 提供的 Restful 远程调用的模板,两者结合就可以达到远程调用的负载均衡。

    而 Feign 是一个更加声明式的 HTTP 客户端,开发者可以像调用本地方法一样调用它,完全感觉不到是远程调用,结合 Ribbon 也可以做负载均衡。

    既然两个问题都得到了解决,我们就用一个例子来进一步说明一下,例子包含了微服务中最基本的三个角色(注册中心、服务提供者、服务消费者):

    注册中心

    注解 @EnableEurekaServer 表示该 Spring Boot 应用是一个注册中心。
    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaserverApplication {
    public static void main(String[] args) {
    SpringApplication.run(EurekaserverApplication.class, args);
    }
    }

    eureka.client.registerWithEureka: false 和 fetchRegistry: false 来表明自己是一个 eureka server。
    server:
    port: 8080

    eureka:
    instance:
    hostname: localhost
    client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
    defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/


    service-hello 服务

    注解 @EnableEurekaClient 表示他是一个 Eureka 客户端,它会在注册中心注册自己。

    注解 @RestController 表示这是一个控制器,@RequestMapping("/hello") 表示匹配到请求 '/hello' 时会调用该方法进行响应。
    @SpringBootApplication
    @EnableEurekaClient
    @RestController
    public class ServiceHelloApplication {

    public static void main(String[] args) {
    SpringApplication.run(ServiceHelloApplication.class, args);
    }

    @Value("${server.port}")
    String port;
    @RequestMapping("/hello")
    public String home(@RequestParam String name) {
    return "hello "+name+",i am from port:" +port;
    }

    }

    注册中心的地址为 http://localhost:8080/eureka/,也就是上面我们定义的。服务名为 service-hello,将会被调用者使用。
    eureka:
    client:
    serviceUrl:
    defaultZone: http://localhost:8080/eureka/
    server:
    port: 8081
    spring:
    application:
    name: service-hello


    服务消费者 service-ribbon

    假设 service-ribbon 端口为 8082,当我们访问 http://localhost:8080/hello 时,HelloControler 接收到请求,并调用 HelloService 中的 helloService 方法,HelloService 中通过定义的 restTemplate 去调用 http://service-hello/hello。此处要注意的是 @LoadBalanced 注解,它表示启用负载均衡。
    @SpringBootApplication
    @EnableDiscoveryClient
    public class ServiceRibbonApplication {

    public static void main(String[] args) {
    SpringApplication.run(ServiceRibbonApplication.class, args);
    }

    @Bean
    @LoadBalanced
    RestTemplate restTemplate() {
    return new RestTemplate();
    }

    }

    @Service
    public class HelloService {

    @Autowired
    RestTemplate restTemplate;

    public String helloService(String name) {
    return restTemplate.getForObject("http://service-hello/hello?name="+name,String.class);
    }

    }

    @RestController
    public class HelloControler {

    @Autowired
    HelloService helloService;

    @RequestMapping(value = "/hello")
    public String hello(@RequestParam String name){
    return helloService.helloService(name);
    }

    }

    至此其实一个微服务应用的雏形已经搭建出来了,服务治理、服务调用可以说是「五脏六腑」中的「心脏」。
    ##「心脏」的依托
    接下来我们要进一步思考的是「五脏六腑」中其余的部分,因为少了它们人也是活不久的。下面通过一个问题或需求对应一个组件的方式进行介绍。

    服务“雪崩”与断路器

    由于网络等原因,服务并不能保证 100% 可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet 容器的线程资源会被消耗殆尽,导致服务瘫痪。

    由于服务与服务之间存在依赖,故障会在调用链路上传播,导致整个微服务系统崩溃,这就是服务故障的“雪崩”效应。

    为了解决这个问题,Spring Cloud 提供了对 Hystrix 断路器的集成,当服务调用失败的频次达到一定阈值,断路器将被开启,降级的策略可以开发者制定,一般是返回一个固定值。这样就能够避免连锁故障。

    此外 Spring Cloud 还提供 Hystrix Dashboard 和 Hystrix Turbine,帮助我们进行监控和聚合监控。

    服务暴露与路由网关

    微服务中的服务很多,直接暴露给用户一是不安全,二是对用户不友好。因此在微服务和面向服务的架构中,通常会有一个路由网关的角色,来负责路由转发和过滤。对应到 Spring Cloud 中有 Zuul 和 Gateway 两个组件可用。

    路由网关接收了所有的用户请求,有着很高的负载,因此它通常是一个集群。用户的请求会先经过一层负载均衡被发到路由网关。

    服务配置与配置中心

    在微服务应用中,服务数量巨多,而每个服务不同环境都有着不同的配置,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。需要注意的是此处的配置与注册中心注册的配置信息是两个概念,此处的配置是服务本身的一些配置信息,如下图:
    1.png

    Spring Cloud 提供了 Spring Cloud Config 组件,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程 Git 仓库中,帮助我们管理服务的配置信息。

    信息同步与消息总线

    前一个问题讲到了每个服务都有一些配置信息,那么配置信息更新了我们该怎么办,手动一个个去更新?当然不是,Spring Cloud 提供了 Spring Cloud Bus 组件,它通过轻量消息代理连接各个分布的节点。当配置信息更新的时候,我们只要更新一个节点的配置,这个更新就会被广播到这个分布式系统中。

    问题定位与链路追踪

    在微服务系统中,服务之间可以相互调用,因此我们一个请求可能会一条调用链,而整个系统会存在一张调用网,其中任意一个服务调用失败或网络超时都可能导致整个请求失败。因为调用关系的复杂,这给问题的定位造成了极大的困难,这也是必须提供服务链路追踪的原因。

    Spring Cloud 为我们提供了 Spring Cloud Sleuth 组件,它能够跟进一个请求到底有哪些服务参与,参与的顺序是怎样的,从而达到每个请求的步骤清晰可见。借助服务链路追踪,我们可以快速定位问题。

    至此,Spring Cloud 的所有基础组件都介绍完了。但是目前所有的组件介绍都是分散的,它们组合起来,完整的样子是什么样的?如下图:
    2.jpg

    偷懒偷了张图,图中漏掉了 Config Server 和链路追踪组件。但是结合上文的介绍,我们大致可以脑补出这两个东西在图中的位置。Config Server 是一个与所有服务相连的服务集群,链路追踪组件则集成在每个服务中。
    #小结
    服务治理为心脏,路由网关、消息中心、断路器、链路追踪、配置中心等为依托,构造了整个微服务框架的「五脏六腑」。当然,一个微服务系统远比本文所写的复杂得多,尤其是在不同的业务场景之下,因此想要更深入地了解它就需要我们不断地去实践。而作为前端,我了解这些内容一是为了更好地了解整个请求的流程,二是为了后续在 SOA 中接入 Node 子服务积累相关知识。

    最后分享一句有趣的调侃 Spring 的话:在 Spring 中没有什么是一个注解解决不了的,如果有,那么就用两个注解。

    原文链接:从 Spring Cloud 看一个微服务框架的「五脏六腑」

    从技术演变的角度看互联网后台架构

    Andy_Lee 发表了文章 • 0 个评论 • 1258 次浏览 • 2019-03-27 09:30 • 来自相关话题

    本文是腾讯IEG内部做的一个面向后台开发新同学的课程,因为其他BG一些同学要求分享,所以发一下。 其实内容都是些常见开源组件的high level描述,比如Flask,Express框架,中间件的演化,Microservices的 ...查看全部
    本文是腾讯IEG内部做的一个面向后台开发新同学的课程,因为其他BG一些同学要求分享,所以发一下。

    其实内容都是些常见开源组件的high level描述,比如Flask,Express框架,中间件的演化,Microservices的概念,一些对NoSQL/Column based DB的概念介绍,Docker的一些简单概念等等。从单个概念来说,这只是一些科普。

    但是为什么当时要开这门课呢?重点是我发现很多新入职的后台开发同学并不太清楚自己做的东西在现代互联网整体架构中处于一个什么样的角色,而在IEG内部则因为游戏开发和互联网开发的一些历史性差异,有些概念并不清晰。

    拿中间件来说,很多Web application不用啥中间件一样可以跑很好,那么是不是都要上Redis?到底解决什么问题?中间件又存在什么问题?中台和中间件又是个什么关系?如果开个MQ就是中间件,微服务又是要做啥?

    如果能从这十多年来互联网应用的整个Tech stack变化去看待backend architecture的一些改变,应该是一件有趣也有意思的事情。这是当时写这个PPT开课的初衷。

    我不敢说我在这个PPT里面的一些私货概念就是对的,但是也算是个人这么多年的一些认知理解,抛砖引玉吧。

    强调一点,这个PPT的初衷是希望从近十多年来不同时代不同热点下技术栈的变化来看看我们是如何从最早的PHP/ASP/JSP<=>MySQL这样的两层架构,一个阶段一个阶段演变到现在繁复的大数据、机器学习、消息驱动、微服务架构这样的体系,然后在针对其中比较重要的几个方面来给新入门后台开发的同学起个“提纲目录”的作用。如果要对每个方面都深入去谈,那肯定不是一两页PPT就能做到的事情。

    下面我们开始。首先看第一页如下图:什么是System Design?什么是架构设计?为什么要谈架构设计?
    1.jpg

    之所以抛出这个问题,是因为平时常常听到两个互相矛盾的说法:一方面很多人爱说“架构师都是不干活夸夸其谈”,另一方面又有很多人苦恼限于日常业务需求开发,无法或者没有机会去从整体架构思考,不知道怎么成长为架构师。

    上面PPT中很有趣的是第一句英文,翻译过来恰好可以反映了论坛上经常有人问的“如何学习架构”的问题:很多leader一来就是扔几本书(书名)给新同学,期望他们读完书就马上升级……这种一般都只会带来失望。

    何为架构师?不写代码只画PPT?

    不是的,架构师的基本职责是要在项目早期就能设计好基本的框架,这个框架能够确保团队成员顺利coding满足近期内业务需求的变化,又能为进一步的发展留出空间(所谓scalability),这即是所谓技术选型。如何确保选型正确?对于简单的应用,或者没有新意完全是实践过多次的相同方案,确实靠几页PPT足矣。但是对于新的领域新的复杂需求,这个需求未必都是业务需求,也包括根据团队自身特点(人员太多、太少、某些环节成员不熟悉需要剥离开)来进行新的设计,对现有技术重新分解组合,这时候就需要架构师自己编码实现原型并验证思路正确性。

    要达到这样的目标难不难?难!但是现在不是2000年了,是2019年了,大量的框架(framework)、开源工具和各种best practice,其实都是在帮我们解决这件事情。而这些框架并不是凭空而来,而是在这十多年互联网的演化中因为要解决各种具体业务难点而一点一点积累进化而来。无论是从MySQL到MongoDB到Cassandra到Time Series DB,或者从Memcached到Redis,从Lucene到Solr到Elasticsearch,从离线批处理到Hadoop到Storm到Spark到Flink,技术不是突然出现的,总是站在前人的肩膀上不断演变的。而要能在浩如烟海的现代互联网技术栈中选择合适的来组装自己的方案,则需要对技术的来源和历史有一定的了解。否则就会出现一些新人张口ELK,闭口TensorFlow,然后一个简单的异步消息处理就会让他们张口结舌的现象。

    20多年前的经典著作DesignPatterns中讲过学习设计模式的意义,放在这里非常经典:学习设计模式并不是要你学习一种新的技术或者编程语言,而是建立一种交流的共同语言和词汇,在方案设计时方便沟通,同时也帮助人们从更抽象的层次去分析问题本质,而不被一些实现的细枝末节所困扰。同时,当我们能把很多问题抽象出来之后,也能帮我们更深入更好地去了解现有系统-------这些意义,对于今天的后端系统设计来说,也仍然是正确的。

    下图是我们要谈的几个主要方面。
    2.jpg

    上面的几个主题中,第一个后台架构的演化是自己从业十多年来,体会到的互联网技术架构的整体变迁。然后分成后台前端应用框架、Middleware和存储三大块谈一下,最后两节微服务和Docker则是给刚进入后台开发的同学做一些概念普及。其中个人觉得最有趣的,是第一部分后台架构的演化和第三部分的中间件,因为这两者是很好地反映了过去十多年互联网发展期间技术栈的变化,从LAMP到MEAN Stack,从各种繁复的中间层到渐渐统一的消息驱动+流处理,每个阶段的业界热点都相当有代表性。

    当然,不是说Web框架、数据存储就不是热点了,姑且不说这几年Web前端的复杂化,光后端应用框架,Node的Express,Python的Django/Flask,Go在国内的盛行,都是相当有趣的。在数据存储领域,列存储和时序数据随着物联网的发展也是备受重视。但是篇幅所限,在这个课程中这些话题也就只能一带而过,因为这些与其说是技术的演变过程,不如说是不同的技术选型和方向了,比如说MySQL适合OLTP(Online Transaction Processing),而Cassandra/HBase等则适合OLAP(Online Analyical Processing),并不能说后者就优于前者。

    下面我们先来看后台架构的演化。
    3.jpg

    严格说这是个很大的标题,从2000年到现在的故事太多了,我这里只能尽力而为从个人体验来分析。

    首先是2008年以前,我把它称为网站时代。为什么这么说?因为那时候的后台开发就是写网站,而且通常是页面代码和后台数据逻辑一起写。你只要能写JSP/PHP/ASP来读写MySQL或者SQL Server,基本就能保证一份不错的工作了。
    4.jpg

    要强调一下,这种简单的两层结构并不能说就是落后。在现在各个企业、公司以及小团队的大量Web应用包括移动App的后端服务中,采用这种架构的不在少数,尤其是很多公司、学校、企业的内部服务,用这种架构已经足够了。

    注意一个时间节点:2008。

    当然,这个节点是我YY的。这个节点可以是2007,或者2006。这个时间段发生了两个影响到现在的事情:Google上市,Facebook开始推开。

    我个人相信前者上市加上它发表的那三篇大数据paper影响了后来业界的技术方向,后者的火热则造成了社交成为业务热点。偏偏社交网站对大数据处理有着天然的需求,技术的积累和业务的需求就这么阴差阳错完美结合了起来,直接影响了大海那边后面的科技发展。

    同时在中国,那个时候却是网络游戏MMO的黄金年代,对单机单服高并发实时交互的需求,远远压过了对海量数据Data mining的需要,在这个时间点,中美两边的互联网科技树发生了比较大的分叉。这倒是并没有优劣之说,只是业务场景的重要性导致了技能树的侧重。直到今天,单机(包括简单的多服务器方案)高并发、高QPS仍然也是国内业界所追求的目标,而在美国那边,这只是一个业务指标而已,更看重的是如何进行水平扩展(horizontal scaling)和分散压力。

    国内和美国的科技树回到一条线上,大数据的业务需求和相关技术发展紧密结合起来,可能要到2014年左右,随着互联网创业的盛行,O2O业务对大数据实时处理、机器学习推荐提出了真正的需求时,才是国内业界首次出现技术驱动业务,算法驱动产品的现象,重新和美国湾区那边站在了一条线上,而这则是后话了。
    5.jpg

    到了2010年前后,Facebook在全球已经是现象级产品,当时微软直接放弃了Windows Live,就是为了避免在社交领域硬怼Facebook。八卦一下当时在美国湾区那边聚餐的时候,如果谁说他是Facebook的,那基本就是全场羡慕的焦点。

    Facebook的崛起也带动了其他大量的社交网站开始出现,社交网站最大的特点就是频繁的用户搜索、推荐,当用户上亿的时候,这就是前面传统的两层架构无法处理的问题了。因此这就带动了中间件的发展。实际上在国外很少有人用中间件或者Middelware这个词,更多是探讨如何把各种Service集成在一起,像国内这样强行分成Frontend/Middleware/Storage的概念是没听人这么谈过的,后面中间件再说这问题。当时的一个惯例是用PHP做所谓的胶水语言(glue language),然后通过Hessian这些协议工具来把其他Java服务连接到一起。与此同时,为了提高访问速度,降低后端查询压力,Memcached/Redis也开始大量使用。基于Lucene的搜索(2010左右很多是自行开发)或者Solr也被用在用户搜索、推荐以及Typeahead这些场景中。

    我记忆中在2012年之前消息队列的使用还不是太频繁,不像后来这么重要。当时常见的应该就是Beanstalkd/RabbitMQ,ZeroMQ其实我在湾区那边很少听人用,倒是后来回国后看到国内用的人还不少。Kafka在2011年已经出现了,有少部分公司开始用,不过还不是主流。
    6.jpg

    2013年之后就是大数据+云的时代了,如果大家回想一下,基本上国内也是差不多在2014年左右开始叫出了云+大数据的口号(2013年国内还在手游狂潮中...)。不谈国外,在中国那段时间就是互联网创业的时代,从千团大战到手游爆发到15年开始的O2O,业务的发展也带动了技术栈的飞速进步。左上角大致上也写了这个时代互联网业界的主要技术热点,实际上这也就是现在的热点。无论国内国外,绝大部分公司还并没有离开云+大数据这个时代。无论是大数据的实时处理、数据挖掘、推荐系统、Docker化,包括A/B测试,这些都是很多企业还正在努力全面解决的问题。

    但是在少数站在业界技术顶端或者没有历史技术包袱的新兴公司,从某个角度上来说,他们已经开始在往下一个时代前进:机器学习AI驱动的时代
    7.jpg

    2018年开始,实际上可能是2017年中开始,AI驱动成了各大公司口号。上图是Facebook和Uber的机器学习平台使用情况,基本上已经全部进入业务核心。当然并不是说所有公司企业都要AI驱动,显然最近发生的波音737事件就说明该用传统的就该传统,别啥都往并不成熟的AI上堆。但另一方面,很多新兴公司的业务本身就是基于大数据或者算法的,因此他们在这个领域也往往走得比较激进。由于这个AI驱动还并没有一个很明确的定义和概念,还处于一种早期萌芽的阶段,在这里也就不多YY了。

    互联网后台架构发展的简单过程就在这里讲得差不多了,然后我们快速谈一下Web开发框架。
    8.png

    首先在前面我提到,在后端架构中其实也有所谓的Frontend(前台)开发存在,一般来说这是指响应用户请求,实现具体业务逻辑的业务逻辑层。当然这么定义略微粗糙了些,很多中间存储、消息服务也会封装一些业务相关逻辑。总之Web开发框架往往就是为了更方便地实现这些业务逻辑而存在的。

    前文提到在一段较长时间内,国内的技术热点是单机高并发高QPS,因此很多那个时代走过来的人会本能地质疑Web框架的性能,而更偏好TCP长链接甚至UDP协议。然而这往往是自寻烦恼,因为除开特别的强实时系统,无论是休闲手游、视频点播还是信息流,都已经是基于HTTP的了。
    9.jpg

    上图所提到的两个问题中,我想强调的是第一点:所有的业务,在能满足需求的情况下,首选HTTP协议进行数据交互。准确点说,首选JSON,使用Web API。

    Why?这就是上图第一个问题所回答的:无状态、易调试易修改、一般没有80端口限制。

    最为诟病的无非是性能,然而实际上对非实时应用,晚个半秒一秒不应该是大问题,要考虑的是水平扩展scalability,不是实时响应(因为前提就是非实时应用);其次实在不行你还有WebSocket可以用。
    10.jpg

    这一部分是简单列举了一下不同框架的使用,可以看出不同框架的概念其实差不多。重点是要注意到Middleware这个说法在Web Framework和后端架构中的意义不同。在Web Framework中是指具体处理GET/POST这些请求之前的一个通用处理(往往是链式调用),比如可以把鉴权、一些日志处理和请求记录放在这里。但在后端架构设计中的Middleware则是指类似消息队列、缓存这些在最终数据库之前的中间服务组件。
    11.jpg

    最后这里是想说Web Framework并不是包治百病,实际上那只是提供了基础功能的一个library,作为开发者则更多需要考虑如何定义配置文件,一些敏感参数如token、密码怎么传进来,开发环境和生产环境的配置如何自动切换,单元测试怎么搞,代码目录怎么组织。有时候我们可以用一些比如Yeoman之类的Scaffold工具来自动生成项目代码框架,或者类似Django这种也可能自动生成基本目录结构。

    下面进入Middleware环节。Again,强调一下这里只是根据个人经验和感受谈谈演化过程。
    12.png

    13.jpg

    这一页只是大致讲一下怎么定义中间件Middleware。说句题外话,在美国湾区那边提这个概念的很少,而阿里又特别喜欢说中间件,两者相互的交流非常头痛。湾区那边不少Google、Facebook还有Pinterest/Uber这些的朋友好几次都在群里问说啥叫中间件。

    中间件这个概念很含糊,应该是阿里提出来的,对应于Middleware(不过似乎也不是完全对应),可能是因为早期Java的EJB那些概念里面比较强调Middleware这一点吧(个人猜的)。大致上,如果我们把Web后端分为直接处理用户请求的Frontend,最后对数据进行持久存储(persistant storage)这两块,那么中间对数据的所有处理环节都可以视为Middleware。
    14.jpg

    和中间件对应的另一个阿里发明的概念是中台。近一年多阿里的中台概念都相当引人注意,这里对中台不做太多描述。总体来说中台更多是偏向业务和组织架构划分,不能说是一个技术概念,也不是面向开发人员的。而中间件Middleware是标准的技术组件服务。

    那么我们自然会有一个问题:为什么要用中间件?
    15.jpg

    谈到为什么要用Middlware,这里用推荐系统举例。

    推荐系统,对数据少用户少的情况下,简单的MySQL即可,比如早期论坛的什么top 10热门话题啊,最多回复的话题啊,都可以视为简单的推荐,数据量又不大的情况下,直接select就可以了。

    如果是用户推荐的话,用户量不大的情况下,也可以如法炮制,选择同一区域(城市)年龄相当的异性,最后随机挑几个给你,相信世纪佳缘之类的交友网站早期实现也就是类似的模式。
    16.jpg

    那么,如果用户量多了呢?每次都去搜数据库,同时在线用户又多,那对数据库的压力就巨大了。这时候就是引入缓存,Memcached、Redis就出现了。

    简单的做法就是把搜索条件作为key,把结果作为value存入缓存。打个比方你可以把key存为 20:40:beijing:male(20到40岁之间北京的男性),然后把第一次搜索的结果全部打乱shuffle后,存前1000个,10分钟过期,再有人用类似条件搜索,就直接把缓存数据随机挑几个返回。放心,一般来说不会有人10分钟就把1000个用户的资料都看完了,中间偶有重复也没人在意(用世纪佳缘、百合网啥的时候看到过重复的吧)。

    不过话又说回来,现代数据库,尤其是类似MongoDB/ES这些大量占用内存的NoSQL,已经对经常查询的数据做了缓存,在这之上再加cache,未必真的很有效,这需要case by case去分析了,总之盲目加cache也并不推荐。

    加缓存是为了解决访问速度,减轻数据库压力,但是并不提高推荐精准度。如果我们要提高推荐效果呢?在2015年之前机器学习还没那么普及成熟的时候,我们怎么搞呢?
    17.jpg

    提高推荐效果,在机器学习之前有两种做法:

    - 引入基于Lucene的搜索引擎,在搜索的同时通过定制方案实现scoring,比如我可以利用Lucene对用户的年龄、性别、地址等进行indexing,但是再返回结果时我再根据用户和查询者两人的具体信息进行关联,自定义返回的score(可以视为推荐相关系数)
    - 采用离线批处理。固然可以用Hadoop,但是就太杀鸡用牛刀了。常见的是定时批处理任务,按某种规则划分用户群体,对每个群体再做全量计算后把推荐结果写入缓存。这种可以做很繁复准确的计算,虽然慢,但效果往往不错。这种做法也常用在手机游戏的PvP对战列表里面。

    这些处理方法对社交网络/手游这类型的其实已经足够了,但是新的业务是不断出现的。随着Uber/滴滴/饿了么/美团这些需要实时处理数据的App崛起,作为一个司机,并不想你上线后过几分钟才有客人来吧,你希望你开到一个热点区域,一开机就马上接单。
    18.jpg

    所以这种对数据进行实时(近实时)处理的需求也带动了后端体系的大发展,Kafka/Spark等等流处理大行其道。这时候的后端体系就渐渐引入了消息驱动的模式,所谓消息驱动,就是对新的生产数据会有多个消费者,有的是满足实时计算的需求(比如司机信息需要立刻能够被快速检索到,又不能每次都做全量indexing,就需要用到Spark),有的只是为了数据分析,写入类似Cassandra这些数据库里,还有的可能是为了生成定时报表,写入到MySQL。

    大数据的处理一直是业界热点领域。记得2015年硅谷一个朋友就是从一家小公司做PHP跳去另一家物联网公司做Spark相关的工作,之前还很担心玩不转,搞了两年就俨然业界大佬被Oracle挖去负责云平台。

    Anyway,这时候对后端体系的要求是一方面能快速满足实时需求,另一方面又能满足各种耗时长的数据分析、Data lake存储等等,以及当时渐渐普及的机器学习模型(当时2015年初和几个朋友搞Startup,其中一个是Walmart Lab的机器学习专家,上来就一堆模型,啥数据和用户都还没有就把模型摆上来了,后来搞得非常头痛。当时没有Keras/PyTorch/tf这些,那堆模型是真心搞不太懂,但是又不敢扔,要靠那东西去包装拿投资的。)

    但是我们再看上面的图,是不是感觉比较乱呢?各种系统的数据写来写去,是不是有点messy?当公司团队增多,系统复杂度越来越高的时候,我们该怎么梳理?
    19.jpg

    到了2017之后,前面千奇百怪的后端体系基本上都趋同了。Kafka的实时消息队列,Spark的流处理(当然现在也可以换成Flink,不过大部分应该还是Spark),然后后端的存储,基于Hive的数据分析查询,然后根据业务的模型训练平台。各个公司反正都差不多这一套,在具体细节上根据业务有所差异,或者有些实力强大的公司会把中间一些环节替换成自己的实现,不过不管怎么千变万化,整体思路基本都一致了。

    这里可以看到机器学习和AI模型的引入。个人认为,Machine Learning的很大一个好处,是简化业务逻辑,简化后台流程,不然一套业务一套实现,各种数据和业务规则很难用一个整体的技术平台来完成。相比前面一页的后台架构,这一页要清晰许多,而且是一个DAG有向无环图的形式,数据流向很明确。我们在下面再来说这个机器学习对业务数据流程的简化。
    20.jpg

    在传统后端系统中,业务逻辑其实和数据是客观分离的,逻辑规则和数据之间并不存在客观联系,而是人为主观加入,并没形成闭环,如上图左上所示。而基于机器学习的平台,这个闭环就形成了,从业务数据->AI模型->业务逻辑->影响用户行为->新的业务数据这个流程是自给自足的。这在很多推荐系统中表现得很明显,通过用户行为数据训练模型,模型对页面信息流进行调整,从而影响用户行为,然后用新的用户行为数据再次调整模型。而在机器学习之前,这些观察工作是交给运营人员去手工猜测调整。

    上图右边谈的是机器学习相关后台架构和传统Web后台的一些差别,重点是耗时太长,必须异步处理。因此消息驱动机制对机器学习后台是一个必须的设计。
    21.jpg

    这页是一些个人的感受,现代的后端数据处理越来越偏向于DAG的形态,Spark不说了,DAG是最大特色;神经网络本身也可以看作是一个DAG(RNN其实也可以看作无数个单向DNN的组合);TensorFlow也是强调其Graph是DAG,另外编程模式上,Reactive编程也很受追捧。

    其实DAG的形态重点强调的就是数据本身是immutable(不可修改),只能transform后成为新的数据进入下一环。这个思维其实可以贯穿到现代后台系统设计的每个环节,比如Trakcing、Analytics、数据表设计、Microservice等等,但具体实施还是要case by case了。

    无论如何,数据,数据的跟踪Tracking,数据的流向,是现代后台系统的核心问题,只有Dataflow和Data Pipeline清晰了,整个后台架构才会清楚。

    数据库是个非常复杂的领域,在下面对几个基本常用的概念做一些介绍。注意一点是Graph database在这里没有提到,因为日常使用较少,相对来说Facebook提出的GraphQL倒是个有趣的概念,但也只是在传统DB上的一个概念封装。
    22.png

    23.jpg

    上图是2018年12月初热门数据库的排名,我们可以看到关系数据库RDBMS和NOSQL数据库基本上平分秋色。而NoSQL中实际上又可以分为key-value storage(包括文档型)及Column based DB。
    24.jpg

    MySQL这个没啥好讲,大概提一下就是。有趣的是曾经看到一篇文章是AWS CTO谈的一些内容,其中印象深刻是:如果你的用户还不到100万,就别折腾了,无脑使用MySQL吧。

    在2015年之前的一个趋势是不少公司使用MySQL作为数据存储,但是把indexing放在外部去做。这个思路最早似乎是Friendster提出的,后来Uber也模仿这种做法设计了自己的数据库Schemaless。然而随着PostgreSQL的普及(PostgreSQL支持对json的索引),这种做法是否还有意义就值得商榷了。
    25.jpg

    NoSQL最早的使用就是key-value的查找,典型的就是Redis。实际上后来的像MongoDB这些Documentbased DB也是类似的key value,只是它对Document中的内容又做了一次index(b-tree),用空间换时间来提供查找数据,这也是CS不变的思维。

    MongoDB/Elasticsearch收到热捧主要是因为它们的Schemaless属性,也就是不需要提前定义数据格式,只要是json就存,还都能根据每个field搜索,这非常方便程序员快速出demo。但是实际上数据量大之后还是要规范数据结构,定义需要indexing的field的。
    26.jpg

    这里提一个比较好玩的开源Project NodeBB,这是个Node.js开发的论坛系统。在我前几年看到这个的时候它其实只支持Redis,然后当时因为一个项目把它改造了让他支持MySQL。去年再看的时候发现它同时支持了Redis/Postres/MongoDB,如果对比一下同样的功能他如何在这三种DB实现的,相信会很有帮助。

    稍微谈谈列存储。常见MySQL你在select的时候其实往往会把整行都读出来,再在其中挑那么一两个你需要的属性,非常浪费。而MongoDB这些文件型DB,又不支持常见SQL。而列存储DB的好处就是快,不用把一行所有信息读出来,只是按列读取你需要的,对现在的大数据分析特别是OLAP(Online Analytical Processing)来说特别重要。然而据另外的说法,实际上像Casssandra/HBase这些并不是真正的列存储,而只是借用了一些概念。这个我也没深入去了解,有兴趣的同学可以自己研究研究。
    27.jpg

    28.jpg

    列存储的一个重要领域是时序数据库,物联网用得多。其特色是大量写入,只增不改(不修改数据),但是读的次数相对于很少(想想物联网的特点,随时有数据写入,但是你不会随时都在看你家小米电器的状态。)

    注意说Write/Read是正交的。这意思是每次写入是一次一行,而读是按列,加上又不会修改数据,因此各自都能保持极快的速度。

    下面简单谈一下微服务,大部分直接看PPT就可以了,有几页略微谈一下个人思考。
    29.jpg

    30.jpg

    31.jpg

    32.jpg

    33.jpg

    上面这页说说,其实微服务所谓的服务发现/name service不要被忽悠觉得是多神奇的东西。最简单的Nginx/Apache这些都能做(域名转向,Proxy),或者你要写个name : address的对应关系到DB里面也完全可以,再配一个定时HealthCheck的服务,最简单的服务发现也就行了。

    高级点用到ZooKeeper/etcd等等,或者Spring Cloud全家桶,那只是简化配置,原理都一样。从开发角度来看,微服务的开发并不是难点,难点是微服务的配置和部署。最近一段时间微服务部署也是业界热点,除了全家桶形态的Spring Cloud,也可以看看Istio这些开源工具。
    34.jpg

    上图主要大致对比一下,看看从早期的Spring到现在Spring Cloud的变化。想来用过Java Tomcat的朋友都能体会Java这一套Config based development的繁琐,开发的精力很多不是在业务代码上,往往会化不少精力去折腾配置文件。当然,Spring Cloud在这方面简化了不少,不过个人还是不太喜欢Java,搞很多复杂的设计模式,封装了又封装。
    35.jpg

    这里要说并不是微服务解决一切,热门的Python Django尽管有REST Framework,但是它实际上是一个典型的Monolithic体系。对很多核心业务,其实未必要拆开成微服务。

    这两者是互补关系,不是替代关系。

    下面的Docker我就不仔细谈了,PPT基本表达了我想表述的概念,主要意思是:

    • Docker能够简化部署,简化开发,能够在某种程度上让开发环境和产品环境尽量接近。
    • 不要担心Docker的性能,它不是虚拟机,可以看作在Server上运行的一个Process。

    36.png

    37.jpg

    上图是描述Docker之前开发人员的常见开发环境,首先在自己机器上装一大堆服务,像MySQL,Redis,Tomcat啥的。也有直接在远程服务器安装环境后,多人共同登录远端开发,各自使用一个端口避免冲突……实际上这种土法炼钢的形态,在2019年的今天仍然在国内非常普及。

    这种形态的后果就是在最后发布到生产环境时,不同开发人员会经历长时间的“联调”,各种端口、权限、脚本、环境设置在生产环境再来一遍…这也是过去运维人员的主要工作。
    38.jpg

    上一页提到的问题,并不是一定要Docker来解决。在这之前,虚拟机VM的出现,以及Vagrant这样的工具,都让开发环境的搭建多少轻松了一些。不过思路仍然是把VM作为一个独立服务器使用,只是因为快照、镜像和辅助工具,让环境的配置、统一和迁移更加简单快捷。
    39.jpg

    上图是对比程序运行在物理服务器、VM及Docker时的资源共享情况,可以看到运行在Docker的应用,并没有比并发运行在物理服务器上占用更多资源。

    下图是简单的Docker使用,不做赘述。
    40.jpg

    41.jpg

    这一页主要是强调Docker并不等同于虚拟机。虚拟机所占资源是独享的,比如你启动一个VM,分配2G内存,那么这个VM里不管是否运行程序都会占用2G内存。然而如果你启动一个Docker,里面运行一个简单Web服务,在不强制指定内存占用情况下,如果没有请求进入,没有额外占用内存,那么这个Docker服务对整机的内存占用几乎为0(当然仍然存在一些开销,但主要是根据该程序自身的运行状况而定)。
    42.jpg

    最后是Kubernetes,这里大概说说Host-Pod-Container的关系,一个Host可以是物理机或者VM,Pod不是一个Docker,而是可以看作有一个IP的……(不知道怎么形容),总之一个Pod可以包括多个Container(Docker),Pod之中的Container可以共享该Pod的资源(IP,storage等)。不过现实中似乎大多是一个Pod对一个Container。

    对互联网一些热门概念和演变过程的一个很简略的描述就到这里。

    原文链接:从技术演变的角度看互联网后台架构
    Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。