微服务

微服务

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

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

技术随业务而生,业务载技术而行。 近些年来,伴随数字经济的发展,在众多企业的数字化转型之路上,云原生、DevOps、微服务、服务治理等成为行业内不断被探讨的新话题。人们在理解和接受这些新型概念的同时,也不断地 ...查看全部

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


近些年来,伴随数字经济的发展,在众多企业的数字化转型之路上,云原生、DevOps、微服务、服务治理等成为行业内不断被探讨的新话题。人们在理解和接受这些新型概念的同时,也不断地思考其可能的落地形态。需求是创造发生的原动力,于是一批代表性的开源技术或者框架涌现而出:Kubernetes,Spring Cloud,Service Mesh,Serverless…… 它们炙手可热,大放异彩。然而在具体落地过程中却步履维艰,磕磕绊绊。 


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

  

 

什么是服务治理? 

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

  

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

  

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

  

世界上唯一不变的是变化本身。----By 斯宾塞.约翰逊 

  

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

 

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

  

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

  

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


  

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

     

数字永远不会说谎。 

  

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

  

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

  

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

  

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

  

量化 

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

 

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

  

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

  

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

  

追踪 

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

  

管控 

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

  

规范 

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

  

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

  

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

  

代码集成 

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

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

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

 

Agent探针 

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

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

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

 


流量劫持 

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

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

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


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

  


03 “百家争鸣”,成就未来 

  

竞争成就未来。 

  

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

  

SpringCloud 

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

  

Dubbo 

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

  

gRPC 

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

  

Service Mesh 

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

 

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


微服务化后缓存怎么做

大卫 发表了文章 • 0 个评论 • 212 次浏览 • 2019-06-03 15:41 • 来自相关话题

【编者的话】最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助。 #问题 问题1: 店铺数据的获取,将用户关注的数据放在店铺信息一起返回。 对外提供的接口: ...查看全部
【编者的话】最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助。
#问题

问题1: 店铺数据的获取,将用户关注的数据放在店铺信息一起返回。

对外提供的接口:
List getPageShop(final Query query,final Boolean cache);

返回的店铺信息:
public class Shop {

public static final long DEFAULT_PRIORITY = 10L;

/**
* 唯一标识
*/
private Long id;
//省略了店铺其他信息
/**
* 用户关注
*/
private ShopAttention attention;
}

当调用方设置cache为true时,因为有缓存的存在,获取不到用户是否关注的数据。

问题2: 统计店铺的被关注数导致的慢SQL,导致数据库cpu飙高,影响到了整个应用。

SQL:
SELECT shop_id, count(user_Id) as attentionNumber
FROM shop_attention
WHERE shop_id IN

#{shopId}

GROUP BY shopId

这两种代码的写法都是基于一个基准。

不同的地方的缓存策略不一样,比如我更新的地方,查找数据时不能缓存,页面展示的查找的地方需要缓存。 既然服务提供方不知道该不该缓存,那就不管了,交给调用方去管理。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

这种假设本身没什么问题,但是忽略了另外一个原则,服务的内聚性。不应该被外部知道的就没必要暴露给外部。

无论是面向过程的C,还是面向对象的语言,都强调内聚性,也就是高内聚,低耦合。单体应用中应当遵循这个原则,微服务同样遵循这个原则。但是在实际过程中,我们发现做到高内聚并不简单。我们必须要时时刻刻审视方法/服务的边界,只有确定好职责边界,才能写出高内聚的代码。
#问题分析

第一个问题,从缓存的角度来看,是忽略了数据的更新频繁性以及数据获取的不同场景。

对于店铺这样一个大的聚合根,本身包含的信息很多,有些数据可能会被频繁更改的,有些则会很少更新的。那么不同的修改频率,是否缓存/缓存策略自然不同,使用同一个参数Boolean cache来控制显然不妥

第二个问题,这种统计类的需求使用SQL统计是一种在数据量比较小的情况下的权宜之计,当数据规模增大后,必须要使用离线计算或者流式计算来解决。它本身是一个慢SQL,所以必须要控制号调用量,这种统计的数据量的时效性应该由服务方控制,不需要暴露给调用方。否则就会出现上述的问题,调用方并不清楚其中的逻辑,不走缓存的话就会使得调用次数增加,QPS的增加会导致慢SQL打垮数据库。
#解法

缓存更新本身就是一个难解的问题,在微服务化后,多个服务就更加复杂了。涉及到跨服务的多级缓存一致性的问题。

所以对大部分的业务,我们可以遵循这样的原则来简单有效处理。

对数据的有效性比较敏感的调用都收敛到服务内部(领域内部应该更合适),不要暴露给调用方。

领域内部做数据的缓存失效控制。

缓存预计算(有些页面的地方不希望首次打开慢)的逻辑也应该放在领域内控制,不要暴露给调用方。

在领域内部控制在不同的地方使用不同的缓存策略,比如更新数据的地方需要获取及时的数据。比如商品的价格,和商品的所属类目更新频次不同,需要有不同的过期时间。

跨服务调用为了减少rpc调用,可以再进行一层缓存。因为这些调用可以接受过期的数据,再进行一层缓存没问题,expired time叠加也没多大影响(expire time在这边主要是影响缓存的命中数)

以上述店铺查询问题改造为例
1.png

扩展:如果后续有case在跨服务的调用时,对数据的过期比较敏感,并且在调用方也做了缓存,那就是跨服务的多级缓存一致性的问题。那就需要服务方告知调用方缓存何时失效,使用消息队列or其他方式来实现。

作者:方丈的寺院
原文:https://fangzhang.blog.csdn.net/article/details/89892575

微服务间的调用和应用内调用有什么区别

阿娇 发表了文章 • 0 个评论 • 193 次浏览 • 2019-06-03 11:19 • 来自相关话题

2019企业IT现状和趋势调研报告:70.7%的企业有云原生相关计划

灵雀云 发表了文章 • 0 个评论 • 155 次浏览 • 2019-06-03 11:00 • 来自相关话题

2019年第一季度,灵雀云发起了“企业IT应用现状和云原生技术落地情况”的调研,通过定向邀请,3个月内共收集了400余份有效调研问卷,这些调研问卷80%以上都来自于国内政府、金融、能源、制造、汽车等传统行业的IT从业者。 发起本次调研 ...查看全部
2019年第一季度,灵雀云发起了“企业IT应用现状和云原生技术落地情况”的调研,通过定向邀请,3个月内共收集了400余份有效调研问卷,这些调研问卷80%以上都来自于国内政府、金融、能源、制造、汽车等传统行业的IT从业者。

发起本次调研的初衷,是我们希望了解当前企业,尤其是传统企业目前IT应用开发现状、以及以DevOps、Kubernetes、微服务等为代表的云原生技术在企业的应用情况,从而勾勒出传统行业IT发展趋势,并对于判断国内用户对云原生相关技术的认知度提供一个有价值的参考。
核心要点解读:
1、 约70%的参与调研者所在企业2019年IT预算有上浮;

2、 24.4%的参与调研者表示公司IT系统基本全靠自研,企业开始自建软件研发团队,主导IT应用的研发;

3、 70.7%的参与调研者所在企业表示在2019年有容器、DevOps和微服务方面的规划;

4、 11.4%的参与调研者所在企业已经试点了具有标杆意义的云原生实践,如精英团队的DevOps实践,小范围非核心应用的微服务拆分改造实践等。


pic2.jpg



本次调研的400多位调研对象中,80%以上来自金融、能源、制造、汽车等传统行业,其中17.3%来自基础架构部门, 22.5%来自运维部门,34.1%来自研发部门,还有约10%的被调研对象为企业的CIO/CTO等高级IT管理者。


pic3.jpg



被调研企业中,服务器规模在100-500台的比例为26.8%,500-1000台的企业占比22%,1000台服务器以上规模的14.6%。



IT系统自研还是外包



pic4.jpg




在数字化转型的背景下,传统外包的做法在被逐渐改变。在此次调查中,70.7%的参与调研者表示目前IT系统是自研外包兼而有之,其中核心业务系统以自己开发为主,24.4%的参与调研者表示公司IT系统基本全靠自研,只有4.9%的参与调研者选择了纯外包选项。这表明,企业开始不再将大部分业务系统,尤其是核心业务需求开发外包,开始自建软件研发团队,主导IT应用的研发。只有企业自己主导IT研发,才能够打造IT核心竞争力。

软件能力成为企业的核心竞争力,这恰好是数字化转型的本质之一。何谓成功的数字化转型?灵雀云认为,有三大衡量标志:IT部门由成本中心转为收入中心;企业自己主导IT产品的研发;改进工具、流程、文化来提高交付速度和质量。最终,实现客户满意度的提升、打造差异化竞争优势、加速产品上市。



IT系统更新频率




PIC5.jpg



在IT系统更新频率方面,每月都要更新、升级的比例达到了51.2%的高占比。同时,每3-6个月更新一次的比例达22%。每个传统领域,都受到了来自Fintech金融科技、车联网、物联网、新零售等新技术驱动的创新业务的挑战,传统企业只有借助IT手段才能实现持续发展,在速度和规模上保持竞争力。



IT系统和研发团队TOP 3挑战



pic6.jpg




本次参与调研的企业以中大型企业为主,其中研发团队规模达到100人以上的比例高达44.3%,20-100人规模的占32.4%。

PIC7.jpg




今天,许多企业都经过了大量IT建设,从分散到集中,造成IT系统越来越复杂,信息孤岛林立,架构臃肿等问题突出。调研中企业IT系统支撑所面临的压力位列前三的挑战分别是:系统复杂性越来越高(65.9%);应用交付压力大,交付速度无法满足业务需求(61.4%);运维管理复杂度提升,IT部门很难构建一支全功能团队(53.7%)。


PIC8.jpg



同时,研发团队所面临的挑战前三甲分别是:部署和运维复杂,运维成本高(74.6%);研发、测试、运维等角色之间相互孤立(62.3%);升级和变更流程复杂,IT服务和应用交付周期长(45.7%)。此外,比较突出的挑战还有,工具链无法完整集成,工具使用困难(32.3%),单体应用过于庞大,迭代效率低下(20.4%)。


pic9.jpg



上述结果充分表明,面对高度创新、快速变化和充满不确定性的新型业务需求,传统开发模式和IT架构已经成为掣肘。70.7%的参与调研企业表示2019年有容器、DevOps和微服务方面的规划和实施计划。

只有朝着持续交付、敏捷部署、快速迭代,通过敏捷IT赋予业务足够的敏捷,才能够满足不断变化的业务需求,重塑自身的生产力,形成竞争优势,带来更好的用户体验,这最终落到以Kubernetes/容器、DevOps和微服务为核心的云原生技术的落地上。云原生架构和理念与数字化转型一脉相承,帮助企业更加顺畅地实施数字化转型。



业务上云需求最强烈,开源、数字化转型受追捧



PIC10.jpg




在企业最关注的新兴技术趋势方面,云计算占比82.9%,企业将业务上云,提升IT资源效率作为首要关注对象。大数据和人工智能紧随其后,占比分别为73.2%和46.3%。其中开源解决方案在调研对象中的关注程度达到24.4%。

当前开源技术正在进入快速发展阶段,向着企业应用的方方面面深入。开源及开源社区不断将新的工具、方法和最佳实践用于云原生的实际业务用例,解决云原生用户的关键问题。借助许多开源解决方案,云原生部署的复杂性和难度也在得以降低。

此外,数字化转型的关注度为33.6%。如今每位IT从业者言必称数字化转型,IT能力也直接指向助力数字化转型。CIO和其他IT管理者已将企业的数字化计划置于新的高度,希望通过数字化来改变企业的商业和业务模式,数字化业务将从初步试验走向大规模应用。伴随企业数字化业务的不断成熟,预计未来几年,数字化转型将进入爆发阶段。



传统企业2019年IT预算稳中有升


PIC11.jpg





本次调研中,被调研企业今年IT工作的重点包括业务上云(56.1%),云原生、大数据、人工智能等新技术采用(53.7%),打造数字化团队,引领企业的数字化创新(43.9%),选择传统业务应用的比例不足20%。越来越多的企业将工作负载放在云端,将正在开发的应用或服务托管在云平台上,云市场不断增长。


PIC12.jpg



在IT预算方面,比客观经济形势略显乐观,和2018年IT预算相比,接近70%参与调研企业2019年的IT预算略有上浮,其中增长5%以内的企业占比37.5%,增长5-10%的企业占比21.2%,增长10%以上的企业达到12.7%。

此外,调研结果显示,数字化转型是一项需要通盘考虑的工作,需要项目管理部门、技术管理部门、开发部门、运维部门共同参与,制定统一的数字化转型方案和决策并推进。有些参与调研的企业特别强调2018年已经在全公司范围内试点了具有标杆意义的云原生实践,如精英团队的DevOps实践,小范围非核心应用的微服务拆分改造实践等,并且这些都将在2019年进行大范围推广。

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

大卫 发表了文章 • 0 个评论 • 242 次浏览 • 2019-05-30 12:58 • 来自相关话题

#前言 今天和大家聊一下在采用Spring Cloud进行微服务架构设计时,微服务之间调用时异常处理机制应该如何设计的问题。我们知道在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向内部和外部提供相应的功能服务接口。面向外部提供的服务 ...查看全部
#前言

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

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

这里需要说明的是,在基于SpringCloud的微服务架构中,所有服务都是通过如consul或eureka这样的服务中间件来实现的服务注册与发现后来进行服务调用的,只是面向外部的服务接口会通过网关服务进行暴露,面向内部的服务接口则在服务网关进行屏蔽,避免直接暴露给公网。而内部微服务间的调用还是可以直接通过consul或eureka进行服务发现调用,这二者并不冲突,只是外部客户端是通过调用服务网关,服务网关通过consul再具体路由到对应的微服务接口,而内部微服务则是直接通过consul或者eureka发现服务后直接进行调用。如果你想和更多微服务技术专家交流,可以加我微信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调用微服务时就可以直接捕获到异常对象,从而实现向本地一样处理远程服务返回的异常对象了。

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

gRPC 使用 protobuf 构建微服务

JetLee 发表了文章 • 0 个评论 • 186 次浏览 • 2019-05-30 09:24 • 来自相关话题

#微服务架构 ##单一的代码库 以前使用 Laravel 做 Web 项目时,是根据 MVC 去划分目录结构的,即 Controller 层处理业务逻辑,Model 层处理数据库的 CURD,View 层处理数据渲染与页面交互。以及 ...查看全部
#微服务架构

##单一的代码库

以前使用 Laravel 做 Web 项目时,是根据 MVC 去划分目录结构的,即 Controller 层处理业务逻辑,Model 层处理数据库的 CURD,View 层处理数据渲染与页面交互。以及 MVP、MVVM 都是将整个项目的代码是集中在一个代码库中,进行业务处理。这种单一聚合代码的方式在前期实现业务的速度很快,但在后期会暴露很多问题:

* 开发与维护困难:随着业务复杂度的增加,代码的耦合度往往会变高,多个模块相互耦合后不易横向扩展
* 效率和可靠性低:过大的代码量将降低响应速度,应用潜在的安全问题也会累积

##拆分的代码库

微服务是一种软件架构,它将一个大且聚合的业务项目拆解为多个小且独立的业务模块,模块即服务,各服务间使用高效的协议(protobuf、JSON 等)相互调用即是 RPC。这种拆分代码库的方式有以下特点:

* 每个服务应作为小规模的、独立的业务模块在运行,类似 Unix 的 Do one thing and do it well
* 每个服务应在进行自动化测试和(分布式)部署时,不影响其他服务
每个服务内部进行细致的错误检查和处理,提高了健壮性

##二者对比

本质上,二者只是聚合与拆分代码的方式不同。
1.png

参考:微服务架构的优势与不足
#构建微服务

##UserInfoService 微服务

接下来创建一个处理用户信息的微服务:UserInfoService,客户端通过 name 向服务端查询用户的年龄、职位等详细信息,需先安装 gRPC 与 protoc 编译器:
go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go

目录结构
├── proto
│ ├── user.proto // 定义客户端请求、服务端响应的数据格式
│ └── user.pb.go // protoc 为 gRPC 生成的读写数据的函数
├── server.go // 实现微服务的服务端
└── client.go // 调用微服务的客户端

##调用流程

2.png

##Protobuf 协议

每个微服务有自己独立的代码库,各自之间在通信时需要高效的协议,要遵循一定的数据结构来解析和编码要传输的数据,在微服务中常使用 protobuf 来定义。

Protobuf(protocal buffers)是谷歌推出的一种二进制数据编码格式,相比 XML 和 JSON 的文本数据编码格式更有优势:

* 读写更快、文件体积更小
* 它没有 XML 的标签名或 JSON 的字段名,更为轻量,更多参考

3.png

语言中立

只需定义一份 .proto 文件,即可使用各语言对应的 protobuf 编译器对其编译,生成的文件中有对 message 编码、解码的函数。

对于 JSON

* 在 PHP 中需使用 json_encode() 和 json_decode() 去编解码,在 Golang 中需使用 json 标准库的 Marshal() 和 Unmarshal() … 每次解析和编码比较繁琐
* 优点:可读性好、开发成本低
* 缺点:相比 protobuf 的读写速度更慢、存储空间更多

对于 Protobuf

.proto 可生成 .php 或 .pb.go … 在项目中可直接引用该文件中编译器生成的编码、解码函数
* 优点:高效轻量、一处定义多处使用
* 缺点:可读性差、开发成本高

定义微服务的 user.proto 文件
syntax = "proto3";	// 指定语法格式,注意 proto3 不再支持 proto2 的 required 和 optional
package proto; // 指定生成的 user.pb.go 的包名,防止命名冲突


// service 定义开放调用的服务,即 UserInfoService 微服务
service UserInfoService {
// rpc 定义服务内的 GetUserInfo 远程调用
rpc GetUserInfo (UserRequest) returns (UserResponse) {
}
}


// message 对应生成代码的 struct
// 定义客户端请求的数据格式
message UserRequest {
// [修饰符] 类型 字段名 = 标识符;
string name = 1;
}


// 定义服务端响应的数据格式
message UserResponse {
int32 id = 1;
string name = 2;
int32 age = 3;
repeated string title = 4; // repeated 修饰符表示字段是可变数组,即 slice 类型
}

编译 user.proto 文件
# protoc 编译器的 grpc 插件会处理 service 字段定义的 UserInfoService
# 使 service 能编码、解码 message
$ protoc -I . --go_out=plugins=grpc:. ./user.proto

生成 user.pb.go
package proto

import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)

// 请求结构
type UserRequest struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}

// 为字段自动生成的 Getter
func (m *UserRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}

// 响应结构
type UserResponse struct {
Id int32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
Age int32 `protobuf:"varint,3,opt,name=age" json:"age,omitempty"`
Title []string `protobuf:"bytes,4,rep,name=title" json:"title,omitempty"`
}
// ...

// 客户端需实现的接口
type UserInfoServiceClient interface {
GetUserInfo(ctx context.Context, in [i]UserRequest, opts ...grpc.CallOption) ([/i]UserResponse, error)
}


// 服务端需实现的接口
type UserInfoServiceServer interface {
GetUserInfo(context.Context, [i]UserRequest) ([/i]UserResponse, error)
}

// 将微服务注册到 grpc
func RegisterUserInfoServiceServer(s *grpc.Server, srv UserInfoServiceServer) {
s.RegisterService(&_UserInfoService_serviceDesc, srv)
}
// 处理请求
func _UserInfoService_GetUserInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {...}

##服务端实现微服务

实现流程
4.png

代码参考
package main
import (...)

// 定义服务端实现约定的接口
type UserInfoService struct{}
var u = UserInfoService{}

// 实现 interface
func (s [i]UserInfoService) GetUserInfo(ctx context.Context, req [/i]pb.UserRequest) (resp *pb.UserResponse, err error) {
name := req.Name

// 模拟在数据库中查找用户信息
// ...
if name == "wuYin" {
resp = &pb.UserResponse{
Id: 233,
Name: name,
Age: 20,
Title: []string{"Gopher", "PHPer"}, // repeated 字段是 slice 类型
}
}
err = nil
return
}

func main() {
port := ":2333"
l, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("listen error: %v\n", err)
}
fmt.Printf("listen %s\n", port)
s := grpc.NewServer()

// 将 UserInfoService 注册到 gRPC
// 注意第二个参数 UserInfoServiceServer 是接口类型的变量
// 需要取地址传参
pb.RegisterUserInfoServiceServer(s, &u)
s.Serve(l)
}

运行监听:
5.png

##客户端调用

实现流程
6.png

代码参考
package main
import (...)

func main() {
conn, err := grpc.Dial(":2333", grpc.WithInsecure())
if err != nil {
log.Fatalf("dial error: %v\n", err)
}
defer conn.Close()

// 实例化 UserInfoService 微服务的客户端
client := pb.NewUserInfoServiceClient(conn)

// 调用服务
req := new(pb.UserRequest)
req.Name = "wuYin"
resp, err := client.GetUserInfo(context.Background(), req)
if err != nil {
log.Fatalf("resp error: %v\n", err)
}

fmt.Printf("Recevied: %v\n", resp)
}

运行调用成功:
7.png

#总结

在上边 UserInfoService 微服务的实现过程中,会发现每个微服务都需要自己管理服务端监听端口,客户端连接后调用,当有很多个微服务时端口的管理会比较麻烦,相比 gRPC,go-micro 实现了服务发现(Service Discovery)来方便的管理微服务,下节将随服务的 Docker 化一起学习。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

原文链接:https://wuyin.io/2018/05/02/protobuf-with-grpc-in-golang/

工业微服务实现工业APP高效开发和运行

玻璃樽 发表了文章 • 0 个评论 • 174 次浏览 • 2019-05-30 07:38 • 来自相关话题

【编者的话】工业微服务架构为工业互联网平台的知识转化和复用提供了最佳技术手段,算法、模型、知识等模块化组件能够以“搭积木”的方式被调用和编排,实现低门槛、高效率的工业App开发。 微服务最早由Martin Fowler与James L ...查看全部
【编者的话】工业微服务架构为工业互联网平台的知识转化和复用提供了最佳技术手段,算法、模型、知识等模块化组件能够以“搭积木”的方式被调用和编排,实现低门槛、高效率的工业App开发。

微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
#什么是工业微服务
工业微服务是工业互联网平台的载体,是以单一功能组件为基础,通过模块化组合方式实现“松耦合”应用开发的软件架构。一个微服务就是一个面向单一功能、能够独立部署的小型应用,将多个不同功能、相互隔离的微服务按需组合在一起并通过API集实现相互通信,就构成了一个功能完整的大型应用系统。以产品生产为例,就可将其拆解为供应链管理、设备运行状态可视化、生产排程、产线数据分析、操作记录等多个微服务功能模块。

在工业互联网领域,由于工业知识繁杂、工业应用复杂程度高等问题,业内人士普遍认为,使用微服务架构将成为开发工业APP的主流方式。国外主流的工业互联网平台,如西门子的Mindsphere、施耐德Eco Struxure等,都通过云平台支持工业微服务组件的开发、部署和管理,从而达到简化工业APP开发的目的。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#工业微服务架构和传统开发模式区别
先来看看传统的web开发方式,一般被称为Monolithic(单体式开发)。所有的功能打包在一个 WAR包里,基本没有外部依赖(除了容器),部署在一个JEE容器(Tomcat,JBoss,WebLogic)里,包含了 DO/DAO,Service,UI等所有逻辑。
1.png

单体架构(Monolithic)优缺点:
2.png

微服务架构与单体架构相比较,微服务架构恰恰弥补了单体架构的不足,通过有效的拆分应用,实现敏捷开发和部署:

  1. 由多个独立的微服务共同组成系统
  2. 微服务单独部署,运行在自己的进程中
  3. 每个微服务为独立的业务开发
  4. 分布式管理
  5. 非常强调隔离性

3.png

关于微服务的一个形象表达:
4.jpg


* X轴:运行多个负载均衡器之后的运行实例
* Y轴:将应用进一步分解为微服务(分库)
* Z轴:大数据量时,将服务分区(分表)

#工业微服务架构的特点
5.png

之所以主流的工业互联网平台都将微服务架构作为开发工业APP的主流方式,是因为微服务架构与传统的架构相比,具备两个显著特点:
##工业微服务开发和维护具有高度灵活性
每个微服务可以由不同团队运用不同语言和工具进行开发和维护,任何修改、升级都不会对应用的其他部分功能产生影响;而传统的统一整体式框架下对软件的任何修改都有可能对整个应用产生意料之外的影响。
##工业微服务运行去中心化分布式执行
不同微服务能够分布式并行执行,应用资源占用率相对较小,且微服务间的数据和资源相互物理隔离,单个服务的故障只会导致单个功能的受损而不会造成整个应用的崩溃。
#微服务支撑工业互联网平台颠覆创新
##工业微服务颠覆传统工业软件研发方式
在企业里,CAD、CAE、DCS、MES、ERP、SCM等传统工业应用软件往往是面向基础的流程或服务进行设计和研发,并在部署阶段根据用户实际情况进行调整,整个软件研发的成本投入较大、研发周期较长,且不能灵活地响应用户个性化需求。而在工业互联网平台中,则可采用工业微服务的方式将上述软件拆解成独立的功能模块,实现对原有生产体系的解构,随后在平台中构建起富含各类功能与服务的微服务组件池,并按照实际需求来调用相应的微服务组件,进行高效率和个性化的面向用户的工业App研发,整个软件研发的技术门槛和投入成本大大降低。原来需要专业团队和雄厚资金支持的精英化软件研发开始向大众化研发转变。
##工业微服务打破工业知识封闭传承体系
过去,工业领域中很多经验知识都停留在老师傅、老专家的脑子里,由于个人精力和地域空间的限制,这些经验知识通常只能在很小的范围里发挥作用,而且还存在易出错、易流失、难推广、难传承等问题。如今,当这些老师傅、老专家将自己的经验知识用软件代码的方式固化下来,转化为平台中的工业微服务之后,由于平台所具备的积累沉淀和开放共享特性,这些经验知识就变成了整个企业、整个行业的宝贵财富,能够被更多的人分享学习和使用,创造出更多的价值。同时,新的专业技术人员还能够在充分消化吸收原有知识的基础上实现进一步提升和创新,推动整个工业知识体系的传递延续和迭代更新。
##工业微服务创造全新平台开放价值生态
随着工业互联网平台中微服务组件池的构建和行业经验知识的持续积累,整个平台既能够为广大第三方开发者提供众多低门槛、易操作、高效率的开发支持手段,形成以工业App开发为核心的平台创新生态,也能够为制造业用户提供以工业微服务为基础的定制化、高可靠、可扩展工业App或解决方案,形成以价值挖掘提升为核心的平台应用生态。最终,构建出以工业互联网平台为桥梁、以工业微服务为载体的相互促进、双向迭代生态体系。
#工业微服务在工业互联网平台的作用
工业微服务实现机理模型算法的模块化、软件化,支撑工业互联网平台中的工业App开发运行。在工业互联网平台中,工业微服务正发挥着承上启下的关键作用。
##独立调试、运行和升级,提升易用性和可维护性
基于不同行业、不同领域经验知识所提炼出来的各类原始机理算法模型通常缺少对外调用的接口,也往往难以进行独立的调试、运行和升级,需要用工业微服务的方式将这些机理算法模型集成起来,封装成可独立调试运行的单一功能或服务模块,提升易用性和可维护性。
##满足工业APP快速运维、持续迭代和个性化定制的需要
在工业互联网平台中基于工业微服务模块进行工业App开发,既能够借助工业微服务并行开发、分布运行的特点,有效发挥平台海量开发者接入、资源弹性配置、云化部署运行等优势,又能够利用工业微服务独立隔离、灵活调用的特点,克服工业App所面临的快速运维、持续迭代、个性化定制等问题。
##无需专业知识,平台调用工业微服务开发工业APP
工业互联网平台发展的核心目标是通过行业经验知识的积累沉淀和复用推广来带动产业整体水平的提升,并打造繁荣创新的开放价值生态。而工业微服务能够将专业知识和IT技术融合起来,变成不需要关心实现细节的“黑盒”,开发者甚至不需要任何专业知识,就可通过调用平台中各类工业微服务的方式开发出解决行业问题的工业App。
##工业微服务具有通用化共享能力,便于复制和应用推广
在此基础上,平台将原来处于企业内部的封闭性专业能力转化为面向行业和社会的通用化共享能力,实现在工业微服务能力复制和应用推广,从而成为服务行业、服务区域的发动机和助推器。
#结语
工业微服务本质是经验知识的软件化和工具化,借助专业化的工具打造通用化的平台。工业微服务架构为工业互联网平台的知识转化和复用提供了最佳技术手段,算法、模型、知识等模块化组件能够以“搭积木”的方式被调用和编排,实现低门槛、高效率的工业App开发,驱动了工业软件开发方式的变革,促进了平台创新生态的形成,工业微服务能力构建已经成为当前工业互联网平台发展的首要任务。

原文链接:https://mp.weixin.qq.com/s/9bSAyBBdl_m534p8Um-59w

微服务架构下的分布式事务基础入门

翔宇 发表了文章 • 0 个评论 • 204 次浏览 • 2019-05-29 22:51 • 来自相关话题

众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据 ...查看全部
众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据库的事务需求,这种事务即为“分布式事务”。那么在目前数据库不支持跨库事务的情况下,我们应该如何实现分布式事务呢?本文首先会为大家梳理分布式事务的基本概念和理论基础,然后介绍几种目前常用的分布式事务解决方案。废话不多说,那就开始吧~
#什么是事务?
事务由一组操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作,要么全都正确执行,要么全都不要执行。
#事务的四大特性 ACID
说到事务,就不得不提一下事务著名的四大特性。

* 原子性,原子性要求,事务是一个不可分割的执行单元,事务中的所有操作要么全都执行,要么全都不执行。
* 一致性,一致性要求,事务在开始前和结束后,数据库的完整性约束没有被破坏。
* 隔离性,事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。
* 持久性,持久性要求,一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。

注意:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。
#事务的隔离级别
这里扩展一下,对事务的隔离性做一个详细的解释。

在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间不会受到任何干扰。这确实能够完全保证数据的安全性,但在实际业务系统中,这种方式性能不高。因此,数据库定义了四种隔离级别,隔离级别和数据库的性能是呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
##事务并发执行会出现的问题
我们先来看一下在不同的隔离级别下,数据库可能会出现的问题:

  1. 更新丢失,当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。 当数据库没有加任何锁操作的情况下会发生。
  2. 脏读,一个事务读到另一个尚未提交的事务中的数据。 该数据可能会被回滚从而失效。 如果第一个事务拿着失效的数据去处理那就发生错误了。
  3. 不可重复读,不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:

* 虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录。
* 幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化。

不可重复读与脏读的区别?:

脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。
##数据库的四种隔离级别
数据库一共有如下四种隔离级别:

  1. Read uncommitted 读未提交,在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。 因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。
  2. Read committed 读提交,在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。
  3. Repeatable read 重复读 ,在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。
  4. Serializable 序列化,该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
#什么是分布式事务?
到此为止,所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立的数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现支持跨数据库的事务支持,这也就是大家常说的“分布式事务”。

这里举一个分布式事务的典型例子——用户下单过程。

当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统等。整个下单的过程如下:

  1. 用户通过商品系统浏览商品,他看中了某一项商品,便点击下单
  2. 此时订单系统会生成一条订单
  3. 订单创建成功后,支付系统提供支付功能
  4. 当支付完成后,由积分系统为该用户增加积分

上述步骤2、3、4需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。
#CAP理论
CAP理论说的是:在一个分布式系统中,最多只能满足C、A、P中的两个需求。

CAP的含义:

C:Consistency 一致性
同一数据的多个副本是否实时相同。

A:Availability 可用性
可用性:一定时间内 & 系统返回一个明确的结果 则称为该系统可用。

P:Partition tolerance 分区容错性
将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。

CAP理论告诉我们,在分布式系统中,C、A、P三个条件中我们最多只能选择两个。那么问题来了,究竟选择哪两个条件较为合适呢?

对于一个业务系统来说,可用性和分区容错性是必须要满足的两个条件,并且这两者是相辅相成的。业务系统之所以使用分布式系统,主要原因有两个:

* 提升整体性能,当业务量猛增,单个服务器已经无法满足我们的业务需求的时候,就需要使用分布式系统,使用多个节点提供相同的功能,从而整体上提升系统的性能,这就是使用分布式系统的第一个原因。
* 实现分区容错性,单一节点 或 多个节点处于相同的网络环境下,那么会存在一定的风险,万一该机房断电、该地区发生自然灾害,那么业务系统就全面瘫痪了。为了防止这一问题,采用分布式系统,将多个子系统分布在不同的地域、不同的机房中,从而保证系统高可用性。

这说明分区容错性是分布式系统的根本,如果分区容错性不能满足,那使用分布式系统将失去意义。

此外,可用性对业务系统也尤为重要。在大谈用户体验的今天,如果业务系统时常出现“系统异常”、响应时间过长等情况,这使得用户对系统的好感度大打折扣,在互联网行业竞争激烈的今天,相同领域的竞争者不甚枚举,系统的间歇性不可用会立马导致用户流向竞争对手。因此,我们只能通过牺牲一致性来换取系统的可用性和分区容错性。这也就是下面要介绍的BASE理论。
#BASE理论
CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性。下面来介绍下BASE理论。

* BA:Basic Available 基本可用

* “一定时间”可以适当延长,当举行大促时,响应时间可以适当延长
* 给部分用户返回一个降级页面,给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。
* 整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:

* S:Soft State:柔性状态,同一数据的不同副本的状态,可以不需要实时一致。
* E:Eventual Consisstency:最终一致性,同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。

#酸碱平衡
ACID能够保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可。但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。因此要根据具体业务场景,在ACID和BASE之间寻求平衡。
#分布式事务协议
下面介绍几种实现分布式事务的协议。
##两阶段提交协议 2PC
分布式系统的一个难点是如何保证架构下多个节点在进行事务性操作的时候保持一致性。为实现这个目的,二阶段提交算法的成立基于以下假设:

* 该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。
* 所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失。
* 所有节点不会永久性损坏,即使损坏后仍然可以恢复。

第一阶段(投票阶段):

  1. 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
  2. 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
  3. 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个"同意"消息;如果参与者节点的事务操作实际执行失败,则它返回一个"中止"消息。

第二阶段(提交执行阶段):

当协调者节点从所有参与者节点获得的相应消息都为"同意"时:

  1. 协调者节点向所有参与者节点发出"正式提交(commit)"的请求。
  2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送"完成"消息。
  4. 协调者节点受到所有参与者节点反馈的"完成"消息后,完成事务。

如果任一参与者节点在第一阶段返回的响应消息为"中止",或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

  1. 协调者节点向所有参与者节点发出"回滚操作(rollback)"的请求。
  2. 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送"回滚完成"消息。
  4. 协调者节点受到所有参与者节点反馈的"回滚完成"消息后,取消事务。

不管最后结果如何,第二阶段都会结束当前事务。

二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:

  1. 执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  2. 参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。(没有多少容错机制)
  3. 协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。(这个可以依赖后面要讲的Paxos协议实现HA)
  4. 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

为此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三阶段提交协议(3PC)。
##三阶段提交协议 3PC
与两阶段提交不同的是,三阶段提交有两个改动点。

* 引入超时机制。同时在协调者和参与者中都引入超时机制。
* 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  1. 事务询问,协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  2. 响应反馈,参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

  1. 发送预提交请求,协调者向参与者发送PreCommit请求,并进入Prepared阶段。
  2. 事务预提交,参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
  3. 响应反馈 ,如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

  1. 发送中断请求,协调者向所有参与者发送abort请求。
  2. 中断事务,参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交:

  1. 发送提交请求,协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
  2. 事务提交,参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
  3. 响应反馈,事务提交完之后,向协调者发送Ack响应。
  4. 完成事务,协调者接收到所有参与者的ack响应之后,完成事务。

中断事务:

协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

  1. 发送中断请求,协调者向所有参与者发送abort请求
  2. 事务回滚,参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
  3. 反馈结果,参与者完成事务回滚之后,向协调者发送ACK消息
  4. 中断事务,协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

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

走进微服务的世界

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

#什么是微服务? 我们首先给出微服务的定义,然后再对该定义给出详细的解释。 微服务就是一些可独立运行、可协同工作的小的服务。 从概念中我们可以提取三个关键词:可独立运行、可协同工作、小。这三个词高 ...查看全部
#什么是微服务?
我们首先给出微服务的定义,然后再对该定义给出详细的解释。

微服务就是一些可独立运行、可协同工作的小的服务。

从概念中我们可以提取三个关键词:可独立运行、可协同工作、小。这三个词高度概括了微服务的核心特性。下面我们就对这三个词作详细解释。

* 可独立运行,微服务是一个个可以独立开发、独立部署、独立运行的系统或者进程。
* 可协同工作,采用了微服务架构后,整个系统被拆分成多个微服务,这些服务之间往往不是完全独立的,在业务上存在一定的耦合,即一个服务可能需要使用另一个服务所提供的功能。这就是所谓的“可协同工作”。与单服务应用不同的是,多个微服务之间的调用时通过RPC通信来实现,而非单服务的本地调用,所以通信的成本相对要高一些,但带来的好处也是可观的。
* 小而美,微服务的思想是,将一个拥有复杂功能的庞大系统,按照业务功能,拆分成多个相互独立的子系统,这些子系统则被称为“微服务”。每个微服务只承担某一项职责,从而相对于单服务应用来说,微服务的体积是“小”的。小也就意味着每个服务承担的职责变少,根据单一职责原则,我们在系统设计时,要尽量使得每一项服务只承担一项职责,从而实现系统的“高内聚”。

#微服务的优点
##易于扩展
在单服务应用中,如果目前性能到达瓶颈,无法支撑目前的业务量,此时一般采用集群模式,即增加服务器集群的节点,并将这个单服务应用“复制”到所有的节点上,从而提升整体性能。然而这种扩展的粒度是比较粗糙的。如果只是系统中某一小部分存在性能问题,在单服务应用中,也要将整个应用进行扩展,这种方式简单粗暴,无法对症下药。而当我们使用了微服务架构后,如果某一项服务的性能到达瓶颈,那么我们只需要增加该服务的节点数即可,其他服务无需变化。这种扩展更加具有针对性,能够充分利用计算机硬件/软件资源。而且只扩展单个服务影响的范围较小,从而系统出错的概率也就越低。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
##部署简单
对于单服务应用而言,所有代码均在一个项目中,从而导致任何微小的改变都需要将整个项目打包、发布、部署,而这一系列操作的代价是高昂的。长此以往,团队为了降低发布的频率,会使得每次发布都伴随着大量的修改,修改越多也就意味着出错的概率也越大。

当我们采用微服务架构以后,每个服务只承担少数职责,从而每次只需要发布发生修改的系统,其他系统依然能够正常运行,波及范围较小。此外,相对于单服务应用而言,每个微服务系统修改的代码相对较少,从而部署后出现错误的概率也相对较低。
##技术异构性
对于单服务应用而言,一个系统的所有模块均整合在一个项目中,所以这些模块只能选择相同的技术。但有些时候,单一技术没办法满足不同的业务需求。如对于项目的算法团队而言,函数试编程语言可能更适合算法的开发,而对于业务开发团队而言,类似于Java的强类型语言具有更高的稳定性。然而在单服务应用中只能互相权衡,选择同一种语言,而当我们使用微服务结构后,这个问题就能够引刃而解。我们将一个完整的系统拆分成了多个独立的服务,从而每个服务都可以根据各自不同的特点,选择最为合适的技术体系。

当然,并不是所有的微服务系统都具备技术异构性,要实现技术异构性,必须保证所有服务都提供通用接口。我们知道,在微服务系统中,服务之间采用RPC接口通信,而实现RPC通信的方式有很多。有一些RPC通信方式与语言强耦合,如Java的RMI技术,它就要求通信的双方都必须采用Java语言开发。当然,也有一些RPC通信方式与语言无关,如基于HTTP协议的REST。这种通信方式对通信双方所采用的语言没有做任何限制,只要通信过程中传输的数据遵循REST规范即可。当然,与语言无关也就意味着通信双方没有类型检查,从而会提高出错的概率。所以,究竟选择与语言无关的RPC通信方式,还是选择与语言强耦合的RPC通信方式,需要我们根据实际的业务场景合理地分析。

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

微服务中集成分布式配置中心 Apollo

大卫 发表了文章 • 0 个评论 • 234 次浏览 • 2019-05-29 12:56 • 来自相关话题

#背景 随着业务的发展、微服务架构的升级,服务的数量、程序的配置日益增多(各种微服务、各种服务器地址、各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、 ...查看全部
#背景
随着业务的发展、微服务架构的升级,服务的数量、程序的配置日益增多(各种微服务、各种服务器地址、各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制。分布式环境下,这些配置更加复杂。

因此,我们需要配置中心来统一管理配置!把业务开发者从复杂以及繁琐的配置中解脱出来,只需专注于业务代码本身,从而能够显著提升开发以及运维效率。同时将配置和发布包解藕也进一步提升发布的成功率,并为运维的细力度管控、应急处理等提供强有力的支持。

在之前的文章中,我们介绍过 Spring Cloud 中的分布式配置中心组件:Spring Cloud Config。本文将会介绍功能更为强大的 Apollo。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#分布式配置中心
在一个分布式环境中,同类型的服务往往会部署很多实例。这些实例使用了一些配置,为了更好地维护这些配置就产生了配置管理服务。通过这个服务可以轻松地管理成千上百个服务实例的配置问题。配置中心的特点:

* 配置的增删改查;
* 不同环境配置隔离(开发、测试、预发布、灰度/线上);
* 高性能、高可用性;
* 请求量多、高并发;
* 读多写少;

现有的配置中心组件有:Spring Cloud Config、Apollo、Disconf、Diamond 等等,这些组件在功能上有或多或少的差异,但是都具有基本的配置中心的功能。
#Apollo 简介
Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。目前的有超过 14k 的 star,使用广泛。Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo。
1.jpg

首先用户在配置中心对配置进行修改并发布;配置中心通知Apollo客户端有配置更新;Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用。

Apollo 支持4个维度管理 Key-Value 格式的配置:

* application(应用):实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置;每个应用都需要有唯一的身份标识 – appId,应用身份是跟着代码走的,所以需要在代码中配置。
* environment(环境):配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置。
* cluster(集群):一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。对不同的cluster,同一个配置可以有不一样的值,如ZooKeeper地址。
* namespace(命名空间):一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等;应用可以直接读取到公共组件的配置namespace,如DAL,RPC等;应用也可以通过继承公共组件的配置namespace来对公共组件的配置做调整,如DAL的初始数据库连接数。

我们在集成 Apollo 时,可以根据需要创建相应的维度。
#快速入门
下面我们搭建一个基于 Spring Boot 的微服务,集成 Apollo。
##启动服务端
Apollo配置中心包括:Config Service、Admin Service 和 Portal。

* Config Service:提供配置获取接口、配置推送接口,服务于Apollo客户端;
* Admin Service:提供配置管理接口、配置修改发布接口,服务于管理界面Portal;
* Portal:配置管理界面,通过MetaServer获取AdminService的服务列表,并使用客户端软负载SLB方式调用AdminService。

官网准备好了一个Quick Start安装包,大家只需要下载到本地,就可以直接使用,免去了编译、打包过程。也可以自行编译,较为繁琐。

Apollo服务端共需要两个数据库:ApolloPortalDB和ApolloConfigDB。创建的语句见安装包,创建好之后需要配置启动的脚本,即 demo.sh 脚本:
#apollo config db info
apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8
apollo_config_db_username=用户名
apollo_config_db_password=密码(如果没有密码,留空即可)

# apollo portal db info
apollo_portal_db_url=jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8
apollo_portal_db_username=用户名
apollo_portal_db_password=密码(如果没有密码,留空即可)

脚本会在本地启动3个服务,分别使用8070, 8080, 8090端口,请确保这3个端口当前没有被使用。执行:
./demo.sh start

看到输出如下的日志信息:
==== starting service ====
Service logging file is ./service/apollo-service.log
Started [10768]
Waiting for config service startup.......
Config service started. You may visit http://localhost:8080 for service status now!
Waiting for admin service startup....
Admin service started
==== starting portal ====
Portal logging file is ./portal/apollo-portal.log
Started [10846]
Waiting for portal startup......
Portal started. You can visit http://localhost:8070 now!

Apollo 服务端启动成功。
##客户端应用
搭建好 Apollo 服务器之后,接下来将我们的应用接入 Apollo。

引入依赖

com.ctrip.framework.apollo
apollo-client
1.1.0

在依赖中只需要增加 apollo-client 的引用。

入口程序
@SpringBootApplication
@EnableApolloConfig("TEST1.product")
public class ApolloApplication {

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

我们通过 @EnableApolloConfig("TEST1.product") 注解开启注册到 Apollo 服务端,并指定了 namespace 为 TEST1.product。

配置文件
app.id: spring-boot-logger
# set apollo meta server address, adjust to actual address if necessary
apollo.meta: http://localhost:8080
server:
port: 0

配置文件中指定了appid 和 Apollo 服务器的地址。

测试应用

我们通过动态设置输出的日志等级来测试接入的配置中心。
@Service
public class LoggerConfiguration {
private static final Logger logger = LoggerFactory.getLogger(LoggerConfiguration.class);
private static final String LOGGER_TAG = "logging.level.";

@Autowired
private LoggingSystem loggingSystem;

@ApolloConfig
private Config config;

@ApolloConfigChangeListener
// 监听 Apollo 配置中心的刷新事件
private void onChange(ConfigChangeEvent changeEvent) {
refreshLoggingLevels();
}

@PostConstruct
// 设置刷新之后的日志级别
private void refreshLoggingLevels() {
Set keyNames = config.getPropertyNames();
for (String key : keyNames) {
if (containsIgnoreCase(key, LOGGER_TAG)) {
String strLevel = config.getProperty(key, "info");
LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
logger.info("{}:{}", key, strLevel);
}
}
}

private static boolean containsIgnoreCase(String str, String searchStr) {
if (str == null || searchStr == null) {
return false;
}
int len = searchStr.length();
int max = str.length() - len;
for (int i = 0; i <= max; i++) {
if (str.regionMatches(true, i, searchStr, 0, len)) {
return true;
}
}
return false;
}
}

如上的配置类用于根据 Apollo 配置中心的日志等级配置,设置本地服务的日志等级,并监听刷新事件,将刷新后的配置及时应用到本地服务,其中 @PostConstruct 注解用于在完成依赖项注入以执行任何初始化之后需要执行的方法。
@Service
public class PrintLogger {
private static Logger logger = LoggerFactory.getLogger(PrintLogger.class);

@ApolloJsonValue("${kk.v}")
private String v;

@PostConstruct
public void printLogger() throws Exception {
Executors.newSingleThreadExecutor().submit(() -> {
while (true) {
logger.error("=========" + v);
logger.info("我是info级别日志");
logger.error("我是error级别日志");
logger.warn("我是warn级别日志");
logger.debug("我是debug级别日志");
TimeUnit.SECONDS.sleep(1);
}
});
}
}

起一个线程,输出不同级别的日志。根据配置的日志等级,过滤后再打印。我们在如上的程序中,还自定义了一个字段,同样用以测试随机打印最新的值。
##测试
我们在 Apollo 的配置界面中,增加如下的配置:
2.jpg

并将配置发布,启动我们本地的 SpringBoot 服务:
2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : =========log-is-error-level.
2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : 我是error级别日志

我们将调整日志的级别为warn,只需要在界面上编辑。
3.jpg

将编辑好的配置发布,应用服务将会收到刷新事件。
4.jpg

可以看到,服务刷新了日志的级别,打印 warn 的日志信息。
2019-05-28 20:35:56.819  WARN 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : 我是warn级别日志
2019-05-28 20:36:06.823 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : =========log-is-warn-level.

#原理细究
在体验了 Apollo 作为配置中心之后,我们将了解下 Apollo 的总体设计和实现的原理。
##Apollo 整体架构
5.png

上图简要描述了 Apollo 的总体设计,从下往上看:

* Config Service 提供配置的读取、推送等功能,服务对象是Apollo客户端
* Admin Service 提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)
* Config Service 和 Admin Service 都是多实例、无状态部署,所以需要将自己注册到 Eureka 中并保持心跳
* 在 Eureka 之上我们架了一层 Meta Server 用于封装 Eureka 的服务发现接口
* Client 通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port 访问服务,同时在 Client 侧会做 load balance、错误重试
* Portal 通过域名访问 Meta Server 获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试
* 为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中

ConfigService、AdminService、Portal 属于 Apollo 服务端的模块,其中提到的 Eureka 是为了保证高可用,Config和Admin都是无状态以集群方式部署的,Client 怎么找到 Config?Portal 怎么找到 Admin?为了解决这个问题,Apollo在其架构中引入了Eureka服务注册中心组件,实现微服务间的服务注册和发现用于服务发现和注册,Config和Admin Service注册实例并定期报心跳, Eureka与ConfigService一起部署。

MetaServer 其实是一个Eureka的Proxy,将Eureka的服务发现接口以更简单明确的HTTP接口的形式暴露出来,方便 Client/Protal 通过简单的 HTTPClient 就可以查询到 Config/Admin 的地址列表。获取到服务实例地址列表之后,再以简单的客户端软负载(Client SLB)策略路由定位到目标实例,并发起调用。
##客户端实现
在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的。
6.png

上图简要描述了配置发布的大致过程:用户在Portal操作配置发布;Portal调用Admin Service的接口操作发布;Admin Service发布配置后,发送ReleaseMessage给各个Config Service;Config Service收到ReleaseMessage后,通知对应的客户端。

如何通知客户端呢?我们看到 Apollo 的实现步骤如下:

  1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
  2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。

* 这是一个fallback机制,为了防止推送机制失效导致配置不更新
* 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
* 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。

  1. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
  2. 客户端会把从服务端获取到的配置在本地文件系统缓存一份,在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置。
  3. 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知

#小结
本文首先介绍分布式配置中心的概念和 Apollo 接入的实践,然后深入介绍了 Apollo 的总体架构和实现的一些细节。总得来说, Apollo 是现有配置中心组件中,功能最全的一个。能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

本文对应的代码地址: https://github.com/keets2012/Spring-Boot-Samples/tree/master/apollo-demo

原文链接:http://blueskykong.com/2019/05/27/apollo-spring-boot/
条新动态, 点击查看
yongfeng

yongfeng 回答了问题 • 2015-07-23 23:25 • 9 个回复 不感兴趣

大家觉得现在的微服务架构最需要什么?

赞同来自:

发表几点自己的看法: &amp;amp;lt;ol&amp;amp;gt;&amp;amp;lt;li&amp;amp;gt;从行业现状来看,在整个中国的IT生态中,集中式架构还是主流。分布式架构目前主要是互联网行业用的比较多。... 显示全部 »
发表几点自己的看法: &amp;amp;lt;ol&amp;amp;gt;&amp;amp;lt;li&amp;amp;gt;从行业现状来看,在整个中国的IT生态中,集中式架构还是主流。分布式架构目前主要是互联网行业用的比较多。相对总的来说,占比还是比较小。而微服务,基本上也就是搞分布式架构的一小拨人开始去尝试的新鲜技术。从这个现状来看,新的思维方式,架构人才永远是痛点。很多人连互联网行业常用的分布式架构尚未理解,让它设计或者在生产实践中采用微服务架构。就勉为其难了。即使架构师具备了这种能力,但是负责具体实施的工程师团队是否有这种思维和类似的设计能力,就是一个很大的疑问。因此必然出现了目前只是在一些大的公司,小范围的实践。&amp;amp;lt;/li&amp;amp;gt;&amp;amp;lt;li&amp;amp;gt;从微服务的运行载体来看,微服务架构的普及,依赖Docker等基于容器的技术在生产领域的大规模普及。微服务的思想,其实在之前的SOA的架构就萌发了,但是那事面向服务的思想,而少了一个“微”字。服务内部,依然是一个大杂烩。微服务,有点像原子,没有办法再进行切割。其核心的思想是每一个微服务的实体就是一个小的自治系统。这个实体不依赖其他的实体而独立存在,运行。它能快速的创建,也能快速的销毁。能够相互组合,也能快速拆散。容器技术,为这种“原子”提供了一个很好的执行载体。而Docker技术,就是他们相互组合的粘合剂。但是,从目前的现状来看,Docker在生产领域还尚未有大规模应用。这也是微服务技术无法大规模普及的原因之一。&amp;amp;lt;/li&amp;amp;gt;&amp;amp;lt;li&amp;amp;gt;再者,微服务架构,和一个公司的研发团队的组织架构也有关系。如果一个公司,其团队按照服务进行划分,彼此松散耦合,就很容易践行这种微服务架构。如果按照职能进行划分,所谓的按照设计,前端,后台,测试,运维等划分,这种微服务架构,就很难玩的转。&amp;amp;lt;/li&amp;amp;gt;&amp;amp;lt;/ol&amp;amp;gt;

微服务化后缓存怎么做

大卫 发表了文章 • 0 个评论 • 212 次浏览 • 2019-06-03 15:41 • 来自相关话题

【编者的话】最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助。 #问题 问题1: 店铺数据的获取,将用户关注的数据放在店铺信息一起返回。 对外提供的接口: ...查看全部
【编者的话】最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助。
#问题

问题1: 店铺数据的获取,将用户关注的数据放在店铺信息一起返回。

对外提供的接口:
List getPageShop(final Query query,final Boolean cache);

返回的店铺信息:
public class Shop {

public static final long DEFAULT_PRIORITY = 10L;

/**
* 唯一标识
*/
private Long id;
//省略了店铺其他信息
/**
* 用户关注
*/
private ShopAttention attention;
}

当调用方设置cache为true时,因为有缓存的存在,获取不到用户是否关注的数据。

问题2: 统计店铺的被关注数导致的慢SQL,导致数据库cpu飙高,影响到了整个应用。

SQL:
SELECT shop_id, count(user_Id) as attentionNumber
FROM shop_attention
WHERE shop_id IN

#{shopId}

GROUP BY shopId

这两种代码的写法都是基于一个基准。

不同的地方的缓存策略不一样,比如我更新的地方,查找数据时不能缓存,页面展示的查找的地方需要缓存。 既然服务提供方不知道该不该缓存,那就不管了,交给调用方去管理。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

这种假设本身没什么问题,但是忽略了另外一个原则,服务的内聚性。不应该被外部知道的就没必要暴露给外部。

无论是面向过程的C,还是面向对象的语言,都强调内聚性,也就是高内聚,低耦合。单体应用中应当遵循这个原则,微服务同样遵循这个原则。但是在实际过程中,我们发现做到高内聚并不简单。我们必须要时时刻刻审视方法/服务的边界,只有确定好职责边界,才能写出高内聚的代码。
#问题分析

第一个问题,从缓存的角度来看,是忽略了数据的更新频繁性以及数据获取的不同场景。

对于店铺这样一个大的聚合根,本身包含的信息很多,有些数据可能会被频繁更改的,有些则会很少更新的。那么不同的修改频率,是否缓存/缓存策略自然不同,使用同一个参数Boolean cache来控制显然不妥

第二个问题,这种统计类的需求使用SQL统计是一种在数据量比较小的情况下的权宜之计,当数据规模增大后,必须要使用离线计算或者流式计算来解决。它本身是一个慢SQL,所以必须要控制号调用量,这种统计的数据量的时效性应该由服务方控制,不需要暴露给调用方。否则就会出现上述的问题,调用方并不清楚其中的逻辑,不走缓存的话就会使得调用次数增加,QPS的增加会导致慢SQL打垮数据库。
#解法

缓存更新本身就是一个难解的问题,在微服务化后,多个服务就更加复杂了。涉及到跨服务的多级缓存一致性的问题。

所以对大部分的业务,我们可以遵循这样的原则来简单有效处理。

对数据的有效性比较敏感的调用都收敛到服务内部(领域内部应该更合适),不要暴露给调用方。

领域内部做数据的缓存失效控制。

缓存预计算(有些页面的地方不希望首次打开慢)的逻辑也应该放在领域内控制,不要暴露给调用方。

在领域内部控制在不同的地方使用不同的缓存策略,比如更新数据的地方需要获取及时的数据。比如商品的价格,和商品的所属类目更新频次不同,需要有不同的过期时间。

跨服务调用为了减少rpc调用,可以再进行一层缓存。因为这些调用可以接受过期的数据,再进行一层缓存没问题,expired time叠加也没多大影响(expire time在这边主要是影响缓存的命中数)

以上述店铺查询问题改造为例
1.png

扩展:如果后续有case在跨服务的调用时,对数据的过期比较敏感,并且在调用方也做了缓存,那就是跨服务的多级缓存一致性的问题。那就需要服务方告知调用方缓存何时失效,使用消息队列or其他方式来实现。

作者:方丈的寺院
原文:https://fangzhang.blog.csdn.net/article/details/89892575

微服务间的调用和应用内调用有什么区别

阿娇 发表了文章 • 0 个评论 • 193 次浏览 • 2019-06-03 11:19 • 来自相关话题

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

大卫 发表了文章 • 0 个评论 • 242 次浏览 • 2019-05-30 12:58 • 来自相关话题

#前言 今天和大家聊一下在采用Spring Cloud进行微服务架构设计时,微服务之间调用时异常处理机制应该如何设计的问题。我们知道在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向内部和外部提供相应的功能服务接口。面向外部提供的服务 ...查看全部
#前言

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

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

这里需要说明的是,在基于SpringCloud的微服务架构中,所有服务都是通过如consul或eureka这样的服务中间件来实现的服务注册与发现后来进行服务调用的,只是面向外部的服务接口会通过网关服务进行暴露,面向内部的服务接口则在服务网关进行屏蔽,避免直接暴露给公网。而内部微服务间的调用还是可以直接通过consul或eureka进行服务发现调用,这二者并不冲突,只是外部客户端是通过调用服务网关,服务网关通过consul再具体路由到对应的微服务接口,而内部微服务则是直接通过consul或者eureka发现服务后直接进行调用。如果你想和更多微服务技术专家交流,可以加我微信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调用微服务时就可以直接捕获到异常对象,从而实现向本地一样处理远程服务返回的异常对象了。

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

工业微服务实现工业APP高效开发和运行

玻璃樽 发表了文章 • 0 个评论 • 174 次浏览 • 2019-05-30 07:38 • 来自相关话题

【编者的话】工业微服务架构为工业互联网平台的知识转化和复用提供了最佳技术手段,算法、模型、知识等模块化组件能够以“搭积木”的方式被调用和编排,实现低门槛、高效率的工业App开发。 微服务最早由Martin Fowler与James L ...查看全部
【编者的话】工业微服务架构为工业互联网平台的知识转化和复用提供了最佳技术手段,算法、模型、知识等模块化组件能够以“搭积木”的方式被调用和编排,实现低门槛、高效率的工业App开发。

微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
#什么是工业微服务
工业微服务是工业互联网平台的载体,是以单一功能组件为基础,通过模块化组合方式实现“松耦合”应用开发的软件架构。一个微服务就是一个面向单一功能、能够独立部署的小型应用,将多个不同功能、相互隔离的微服务按需组合在一起并通过API集实现相互通信,就构成了一个功能完整的大型应用系统。以产品生产为例,就可将其拆解为供应链管理、设备运行状态可视化、生产排程、产线数据分析、操作记录等多个微服务功能模块。

在工业互联网领域,由于工业知识繁杂、工业应用复杂程度高等问题,业内人士普遍认为,使用微服务架构将成为开发工业APP的主流方式。国外主流的工业互联网平台,如西门子的Mindsphere、施耐德Eco Struxure等,都通过云平台支持工业微服务组件的开发、部署和管理,从而达到简化工业APP开发的目的。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#工业微服务架构和传统开发模式区别
先来看看传统的web开发方式,一般被称为Monolithic(单体式开发)。所有的功能打包在一个 WAR包里,基本没有外部依赖(除了容器),部署在一个JEE容器(Tomcat,JBoss,WebLogic)里,包含了 DO/DAO,Service,UI等所有逻辑。
1.png

单体架构(Monolithic)优缺点:
2.png

微服务架构与单体架构相比较,微服务架构恰恰弥补了单体架构的不足,通过有效的拆分应用,实现敏捷开发和部署:

  1. 由多个独立的微服务共同组成系统
  2. 微服务单独部署,运行在自己的进程中
  3. 每个微服务为独立的业务开发
  4. 分布式管理
  5. 非常强调隔离性

3.png

关于微服务的一个形象表达:
4.jpg


* X轴:运行多个负载均衡器之后的运行实例
* Y轴:将应用进一步分解为微服务(分库)
* Z轴:大数据量时,将服务分区(分表)

#工业微服务架构的特点
5.png

之所以主流的工业互联网平台都将微服务架构作为开发工业APP的主流方式,是因为微服务架构与传统的架构相比,具备两个显著特点:
##工业微服务开发和维护具有高度灵活性
每个微服务可以由不同团队运用不同语言和工具进行开发和维护,任何修改、升级都不会对应用的其他部分功能产生影响;而传统的统一整体式框架下对软件的任何修改都有可能对整个应用产生意料之外的影响。
##工业微服务运行去中心化分布式执行
不同微服务能够分布式并行执行,应用资源占用率相对较小,且微服务间的数据和资源相互物理隔离,单个服务的故障只会导致单个功能的受损而不会造成整个应用的崩溃。
#微服务支撑工业互联网平台颠覆创新
##工业微服务颠覆传统工业软件研发方式
在企业里,CAD、CAE、DCS、MES、ERP、SCM等传统工业应用软件往往是面向基础的流程或服务进行设计和研发,并在部署阶段根据用户实际情况进行调整,整个软件研发的成本投入较大、研发周期较长,且不能灵活地响应用户个性化需求。而在工业互联网平台中,则可采用工业微服务的方式将上述软件拆解成独立的功能模块,实现对原有生产体系的解构,随后在平台中构建起富含各类功能与服务的微服务组件池,并按照实际需求来调用相应的微服务组件,进行高效率和个性化的面向用户的工业App研发,整个软件研发的技术门槛和投入成本大大降低。原来需要专业团队和雄厚资金支持的精英化软件研发开始向大众化研发转变。
##工业微服务打破工业知识封闭传承体系
过去,工业领域中很多经验知识都停留在老师傅、老专家的脑子里,由于个人精力和地域空间的限制,这些经验知识通常只能在很小的范围里发挥作用,而且还存在易出错、易流失、难推广、难传承等问题。如今,当这些老师傅、老专家将自己的经验知识用软件代码的方式固化下来,转化为平台中的工业微服务之后,由于平台所具备的积累沉淀和开放共享特性,这些经验知识就变成了整个企业、整个行业的宝贵财富,能够被更多的人分享学习和使用,创造出更多的价值。同时,新的专业技术人员还能够在充分消化吸收原有知识的基础上实现进一步提升和创新,推动整个工业知识体系的传递延续和迭代更新。
##工业微服务创造全新平台开放价值生态
随着工业互联网平台中微服务组件池的构建和行业经验知识的持续积累,整个平台既能够为广大第三方开发者提供众多低门槛、易操作、高效率的开发支持手段,形成以工业App开发为核心的平台创新生态,也能够为制造业用户提供以工业微服务为基础的定制化、高可靠、可扩展工业App或解决方案,形成以价值挖掘提升为核心的平台应用生态。最终,构建出以工业互联网平台为桥梁、以工业微服务为载体的相互促进、双向迭代生态体系。
#工业微服务在工业互联网平台的作用
工业微服务实现机理模型算法的模块化、软件化,支撑工业互联网平台中的工业App开发运行。在工业互联网平台中,工业微服务正发挥着承上启下的关键作用。
##独立调试、运行和升级,提升易用性和可维护性
基于不同行业、不同领域经验知识所提炼出来的各类原始机理算法模型通常缺少对外调用的接口,也往往难以进行独立的调试、运行和升级,需要用工业微服务的方式将这些机理算法模型集成起来,封装成可独立调试运行的单一功能或服务模块,提升易用性和可维护性。
##满足工业APP快速运维、持续迭代和个性化定制的需要
在工业互联网平台中基于工业微服务模块进行工业App开发,既能够借助工业微服务并行开发、分布运行的特点,有效发挥平台海量开发者接入、资源弹性配置、云化部署运行等优势,又能够利用工业微服务独立隔离、灵活调用的特点,克服工业App所面临的快速运维、持续迭代、个性化定制等问题。
##无需专业知识,平台调用工业微服务开发工业APP
工业互联网平台发展的核心目标是通过行业经验知识的积累沉淀和复用推广来带动产业整体水平的提升,并打造繁荣创新的开放价值生态。而工业微服务能够将专业知识和IT技术融合起来,变成不需要关心实现细节的“黑盒”,开发者甚至不需要任何专业知识,就可通过调用平台中各类工业微服务的方式开发出解决行业问题的工业App。
##工业微服务具有通用化共享能力,便于复制和应用推广
在此基础上,平台将原来处于企业内部的封闭性专业能力转化为面向行业和社会的通用化共享能力,实现在工业微服务能力复制和应用推广,从而成为服务行业、服务区域的发动机和助推器。
#结语
工业微服务本质是经验知识的软件化和工具化,借助专业化的工具打造通用化的平台。工业微服务架构为工业互联网平台的知识转化和复用提供了最佳技术手段,算法、模型、知识等模块化组件能够以“搭积木”的方式被调用和编排,实现低门槛、高效率的工业App开发,驱动了工业软件开发方式的变革,促进了平台创新生态的形成,工业微服务能力构建已经成为当前工业互联网平台发展的首要任务。

原文链接:https://mp.weixin.qq.com/s/9bSAyBBdl_m534p8Um-59w

微服务架构下的分布式事务基础入门

翔宇 发表了文章 • 0 个评论 • 204 次浏览 • 2019-05-29 22:51 • 来自相关话题

众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据 ...查看全部
众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据库的事务需求,这种事务即为“分布式事务”。那么在目前数据库不支持跨库事务的情况下,我们应该如何实现分布式事务呢?本文首先会为大家梳理分布式事务的基本概念和理论基础,然后介绍几种目前常用的分布式事务解决方案。废话不多说,那就开始吧~
#什么是事务?
事务由一组操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作,要么全都正确执行,要么全都不要执行。
#事务的四大特性 ACID
说到事务,就不得不提一下事务著名的四大特性。

* 原子性,原子性要求,事务是一个不可分割的执行单元,事务中的所有操作要么全都执行,要么全都不执行。
* 一致性,一致性要求,事务在开始前和结束后,数据库的完整性约束没有被破坏。
* 隔离性,事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。
* 持久性,持久性要求,一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。

注意:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。
#事务的隔离级别
这里扩展一下,对事务的隔离性做一个详细的解释。

在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间不会受到任何干扰。这确实能够完全保证数据的安全性,但在实际业务系统中,这种方式性能不高。因此,数据库定义了四种隔离级别,隔离级别和数据库的性能是呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
##事务并发执行会出现的问题
我们先来看一下在不同的隔离级别下,数据库可能会出现的问题:

  1. 更新丢失,当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。 当数据库没有加任何锁操作的情况下会发生。
  2. 脏读,一个事务读到另一个尚未提交的事务中的数据。 该数据可能会被回滚从而失效。 如果第一个事务拿着失效的数据去处理那就发生错误了。
  3. 不可重复读,不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:

* 虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录。
* 幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化。

不可重复读与脏读的区别?:

脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。
##数据库的四种隔离级别
数据库一共有如下四种隔离级别:

  1. Read uncommitted 读未提交,在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。 因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。
  2. Read committed 读提交,在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。
  3. Repeatable read 重复读 ,在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。
  4. Serializable 序列化,该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
#什么是分布式事务?
到此为止,所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立的数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现支持跨数据库的事务支持,这也就是大家常说的“分布式事务”。

这里举一个分布式事务的典型例子——用户下单过程。

当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统等。整个下单的过程如下:

  1. 用户通过商品系统浏览商品,他看中了某一项商品,便点击下单
  2. 此时订单系统会生成一条订单
  3. 订单创建成功后,支付系统提供支付功能
  4. 当支付完成后,由积分系统为该用户增加积分

上述步骤2、3、4需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。
#CAP理论
CAP理论说的是:在一个分布式系统中,最多只能满足C、A、P中的两个需求。

CAP的含义:

C:Consistency 一致性
同一数据的多个副本是否实时相同。

A:Availability 可用性
可用性:一定时间内 & 系统返回一个明确的结果 则称为该系统可用。

P:Partition tolerance 分区容错性
将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。

CAP理论告诉我们,在分布式系统中,C、A、P三个条件中我们最多只能选择两个。那么问题来了,究竟选择哪两个条件较为合适呢?

对于一个业务系统来说,可用性和分区容错性是必须要满足的两个条件,并且这两者是相辅相成的。业务系统之所以使用分布式系统,主要原因有两个:

* 提升整体性能,当业务量猛增,单个服务器已经无法满足我们的业务需求的时候,就需要使用分布式系统,使用多个节点提供相同的功能,从而整体上提升系统的性能,这就是使用分布式系统的第一个原因。
* 实现分区容错性,单一节点 或 多个节点处于相同的网络环境下,那么会存在一定的风险,万一该机房断电、该地区发生自然灾害,那么业务系统就全面瘫痪了。为了防止这一问题,采用分布式系统,将多个子系统分布在不同的地域、不同的机房中,从而保证系统高可用性。

这说明分区容错性是分布式系统的根本,如果分区容错性不能满足,那使用分布式系统将失去意义。

此外,可用性对业务系统也尤为重要。在大谈用户体验的今天,如果业务系统时常出现“系统异常”、响应时间过长等情况,这使得用户对系统的好感度大打折扣,在互联网行业竞争激烈的今天,相同领域的竞争者不甚枚举,系统的间歇性不可用会立马导致用户流向竞争对手。因此,我们只能通过牺牲一致性来换取系统的可用性和分区容错性。这也就是下面要介绍的BASE理论。
#BASE理论
CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性。下面来介绍下BASE理论。

* BA:Basic Available 基本可用

* “一定时间”可以适当延长,当举行大促时,响应时间可以适当延长
* 给部分用户返回一个降级页面,给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。
* 整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:

* S:Soft State:柔性状态,同一数据的不同副本的状态,可以不需要实时一致。
* E:Eventual Consisstency:最终一致性,同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。

#酸碱平衡
ACID能够保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可。但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。因此要根据具体业务场景,在ACID和BASE之间寻求平衡。
#分布式事务协议
下面介绍几种实现分布式事务的协议。
##两阶段提交协议 2PC
分布式系统的一个难点是如何保证架构下多个节点在进行事务性操作的时候保持一致性。为实现这个目的,二阶段提交算法的成立基于以下假设:

* 该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。
* 所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失。
* 所有节点不会永久性损坏,即使损坏后仍然可以恢复。

第一阶段(投票阶段):

  1. 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
  2. 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
  3. 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个"同意"消息;如果参与者节点的事务操作实际执行失败,则它返回一个"中止"消息。

第二阶段(提交执行阶段):

当协调者节点从所有参与者节点获得的相应消息都为"同意"时:

  1. 协调者节点向所有参与者节点发出"正式提交(commit)"的请求。
  2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送"完成"消息。
  4. 协调者节点受到所有参与者节点反馈的"完成"消息后,完成事务。

如果任一参与者节点在第一阶段返回的响应消息为"中止",或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

  1. 协调者节点向所有参与者节点发出"回滚操作(rollback)"的请求。
  2. 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送"回滚完成"消息。
  4. 协调者节点受到所有参与者节点反馈的"回滚完成"消息后,取消事务。

不管最后结果如何,第二阶段都会结束当前事务。

二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:

  1. 执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  2. 参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。(没有多少容错机制)
  3. 协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。(这个可以依赖后面要讲的Paxos协议实现HA)
  4. 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

为此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三阶段提交协议(3PC)。
##三阶段提交协议 3PC
与两阶段提交不同的是,三阶段提交有两个改动点。

* 引入超时机制。同时在协调者和参与者中都引入超时机制。
* 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  1. 事务询问,协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  2. 响应反馈,参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

  1. 发送预提交请求,协调者向参与者发送PreCommit请求,并进入Prepared阶段。
  2. 事务预提交,参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
  3. 响应反馈 ,如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

  1. 发送中断请求,协调者向所有参与者发送abort请求。
  2. 中断事务,参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交:

  1. 发送提交请求,协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
  2. 事务提交,参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
  3. 响应反馈,事务提交完之后,向协调者发送Ack响应。
  4. 完成事务,协调者接收到所有参与者的ack响应之后,完成事务。

中断事务:

协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

  1. 发送中断请求,协调者向所有参与者发送abort请求
  2. 事务回滚,参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
  3. 反馈结果,参与者完成事务回滚之后,向协调者发送ACK消息
  4. 中断事务,协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

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

微服务中集成分布式配置中心 Apollo

大卫 发表了文章 • 0 个评论 • 234 次浏览 • 2019-05-29 12:56 • 来自相关话题

#背景 随着业务的发展、微服务架构的升级,服务的数量、程序的配置日益增多(各种微服务、各种服务器地址、各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、 ...查看全部
#背景
随着业务的发展、微服务架构的升级,服务的数量、程序的配置日益增多(各种微服务、各种服务器地址、各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制。分布式环境下,这些配置更加复杂。

因此,我们需要配置中心来统一管理配置!把业务开发者从复杂以及繁琐的配置中解脱出来,只需专注于业务代码本身,从而能够显著提升开发以及运维效率。同时将配置和发布包解藕也进一步提升发布的成功率,并为运维的细力度管控、应急处理等提供强有力的支持。

在之前的文章中,我们介绍过 Spring Cloud 中的分布式配置中心组件:Spring Cloud Config。本文将会介绍功能更为强大的 Apollo。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#分布式配置中心
在一个分布式环境中,同类型的服务往往会部署很多实例。这些实例使用了一些配置,为了更好地维护这些配置就产生了配置管理服务。通过这个服务可以轻松地管理成千上百个服务实例的配置问题。配置中心的特点:

* 配置的增删改查;
* 不同环境配置隔离(开发、测试、预发布、灰度/线上);
* 高性能、高可用性;
* 请求量多、高并发;
* 读多写少;

现有的配置中心组件有:Spring Cloud Config、Apollo、Disconf、Diamond 等等,这些组件在功能上有或多或少的差异,但是都具有基本的配置中心的功能。
#Apollo 简介
Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。目前的有超过 14k 的 star,使用广泛。Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo。
1.jpg

首先用户在配置中心对配置进行修改并发布;配置中心通知Apollo客户端有配置更新;Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用。

Apollo 支持4个维度管理 Key-Value 格式的配置:

* application(应用):实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置;每个应用都需要有唯一的身份标识 – appId,应用身份是跟着代码走的,所以需要在代码中配置。
* environment(环境):配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置。
* cluster(集群):一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。对不同的cluster,同一个配置可以有不一样的值,如ZooKeeper地址。
* namespace(命名空间):一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等;应用可以直接读取到公共组件的配置namespace,如DAL,RPC等;应用也可以通过继承公共组件的配置namespace来对公共组件的配置做调整,如DAL的初始数据库连接数。

我们在集成 Apollo 时,可以根据需要创建相应的维度。
#快速入门
下面我们搭建一个基于 Spring Boot 的微服务,集成 Apollo。
##启动服务端
Apollo配置中心包括:Config Service、Admin Service 和 Portal。

* Config Service:提供配置获取接口、配置推送接口,服务于Apollo客户端;
* Admin Service:提供配置管理接口、配置修改发布接口,服务于管理界面Portal;
* Portal:配置管理界面,通过MetaServer获取AdminService的服务列表,并使用客户端软负载SLB方式调用AdminService。

官网准备好了一个Quick Start安装包,大家只需要下载到本地,就可以直接使用,免去了编译、打包过程。也可以自行编译,较为繁琐。

Apollo服务端共需要两个数据库:ApolloPortalDB和ApolloConfigDB。创建的语句见安装包,创建好之后需要配置启动的脚本,即 demo.sh 脚本:
#apollo config db info
apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8
apollo_config_db_username=用户名
apollo_config_db_password=密码(如果没有密码,留空即可)

# apollo portal db info
apollo_portal_db_url=jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8
apollo_portal_db_username=用户名
apollo_portal_db_password=密码(如果没有密码,留空即可)

脚本会在本地启动3个服务,分别使用8070, 8080, 8090端口,请确保这3个端口当前没有被使用。执行:
./demo.sh start

看到输出如下的日志信息:
==== starting service ====
Service logging file is ./service/apollo-service.log
Started [10768]
Waiting for config service startup.......
Config service started. You may visit http://localhost:8080 for service status now!
Waiting for admin service startup....
Admin service started
==== starting portal ====
Portal logging file is ./portal/apollo-portal.log
Started [10846]
Waiting for portal startup......
Portal started. You can visit http://localhost:8070 now!

Apollo 服务端启动成功。
##客户端应用
搭建好 Apollo 服务器之后,接下来将我们的应用接入 Apollo。

引入依赖

com.ctrip.framework.apollo
apollo-client
1.1.0

在依赖中只需要增加 apollo-client 的引用。

入口程序
@SpringBootApplication
@EnableApolloConfig("TEST1.product")
public class ApolloApplication {

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

我们通过 @EnableApolloConfig("TEST1.product") 注解开启注册到 Apollo 服务端,并指定了 namespace 为 TEST1.product。

配置文件
app.id: spring-boot-logger
# set apollo meta server address, adjust to actual address if necessary
apollo.meta: http://localhost:8080
server:
port: 0

配置文件中指定了appid 和 Apollo 服务器的地址。

测试应用

我们通过动态设置输出的日志等级来测试接入的配置中心。
@Service
public class LoggerConfiguration {
private static final Logger logger = LoggerFactory.getLogger(LoggerConfiguration.class);
private static final String LOGGER_TAG = "logging.level.";

@Autowired
private LoggingSystem loggingSystem;

@ApolloConfig
private Config config;

@ApolloConfigChangeListener
// 监听 Apollo 配置中心的刷新事件
private void onChange(ConfigChangeEvent changeEvent) {
refreshLoggingLevels();
}

@PostConstruct
// 设置刷新之后的日志级别
private void refreshLoggingLevels() {
Set keyNames = config.getPropertyNames();
for (String key : keyNames) {
if (containsIgnoreCase(key, LOGGER_TAG)) {
String strLevel = config.getProperty(key, "info");
LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
logger.info("{}:{}", key, strLevel);
}
}
}

private static boolean containsIgnoreCase(String str, String searchStr) {
if (str == null || searchStr == null) {
return false;
}
int len = searchStr.length();
int max = str.length() - len;
for (int i = 0; i <= max; i++) {
if (str.regionMatches(true, i, searchStr, 0, len)) {
return true;
}
}
return false;
}
}

如上的配置类用于根据 Apollo 配置中心的日志等级配置,设置本地服务的日志等级,并监听刷新事件,将刷新后的配置及时应用到本地服务,其中 @PostConstruct 注解用于在完成依赖项注入以执行任何初始化之后需要执行的方法。
@Service
public class PrintLogger {
private static Logger logger = LoggerFactory.getLogger(PrintLogger.class);

@ApolloJsonValue("${kk.v}")
private String v;

@PostConstruct
public void printLogger() throws Exception {
Executors.newSingleThreadExecutor().submit(() -> {
while (true) {
logger.error("=========" + v);
logger.info("我是info级别日志");
logger.error("我是error级别日志");
logger.warn("我是warn级别日志");
logger.debug("我是debug级别日志");
TimeUnit.SECONDS.sleep(1);
}
});
}
}

起一个线程,输出不同级别的日志。根据配置的日志等级,过滤后再打印。我们在如上的程序中,还自定义了一个字段,同样用以测试随机打印最新的值。
##测试
我们在 Apollo 的配置界面中,增加如下的配置:
2.jpg

并将配置发布,启动我们本地的 SpringBoot 服务:
2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : =========log-is-error-level.
2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : 我是error级别日志

我们将调整日志的级别为warn,只需要在界面上编辑。
3.jpg

将编辑好的配置发布,应用服务将会收到刷新事件。
4.jpg

可以看到,服务刷新了日志的级别,打印 warn 的日志信息。
2019-05-28 20:35:56.819  WARN 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : 我是warn级别日志
2019-05-28 20:36:06.823 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : =========log-is-warn-level.

#原理细究
在体验了 Apollo 作为配置中心之后,我们将了解下 Apollo 的总体设计和实现的原理。
##Apollo 整体架构
5.png

上图简要描述了 Apollo 的总体设计,从下往上看:

* Config Service 提供配置的读取、推送等功能,服务对象是Apollo客户端
* Admin Service 提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)
* Config Service 和 Admin Service 都是多实例、无状态部署,所以需要将自己注册到 Eureka 中并保持心跳
* 在 Eureka 之上我们架了一层 Meta Server 用于封装 Eureka 的服务发现接口
* Client 通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port 访问服务,同时在 Client 侧会做 load balance、错误重试
* Portal 通过域名访问 Meta Server 获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试
* 为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中

ConfigService、AdminService、Portal 属于 Apollo 服务端的模块,其中提到的 Eureka 是为了保证高可用,Config和Admin都是无状态以集群方式部署的,Client 怎么找到 Config?Portal 怎么找到 Admin?为了解决这个问题,Apollo在其架构中引入了Eureka服务注册中心组件,实现微服务间的服务注册和发现用于服务发现和注册,Config和Admin Service注册实例并定期报心跳, Eureka与ConfigService一起部署。

MetaServer 其实是一个Eureka的Proxy,将Eureka的服务发现接口以更简单明确的HTTP接口的形式暴露出来,方便 Client/Protal 通过简单的 HTTPClient 就可以查询到 Config/Admin 的地址列表。获取到服务实例地址列表之后,再以简单的客户端软负载(Client SLB)策略路由定位到目标实例,并发起调用。
##客户端实现
在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的。
6.png

上图简要描述了配置发布的大致过程:用户在Portal操作配置发布;Portal调用Admin Service的接口操作发布;Admin Service发布配置后,发送ReleaseMessage给各个Config Service;Config Service收到ReleaseMessage后,通知对应的客户端。

如何通知客户端呢?我们看到 Apollo 的实现步骤如下:

  1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
  2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。

* 这是一个fallback机制,为了防止推送机制失效导致配置不更新
* 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
* 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。

  1. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
  2. 客户端会把从服务端获取到的配置在本地文件系统缓存一份,在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置。
  3. 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知

#小结
本文首先介绍分布式配置中心的概念和 Apollo 接入的实践,然后深入介绍了 Apollo 的总体架构和实现的一些细节。总得来说, Apollo 是现有配置中心组件中,功能最全的一个。能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

本文对应的代码地址: https://github.com/keets2012/Spring-Boot-Samples/tree/master/apollo-demo

原文链接:http://blueskykong.com/2019/05/27/apollo-spring-boot/

微服务架构中的GraphQL

buxie 发表了文章 • 0 个评论 • 229 次浏览 • 2019-05-27 09:45 • 来自相关话题

#背景 在最近一篇文章《 Exploring Tokens with Stellar》中我们提到了如何利用GraphQL拼接来打造解决方案。为了更好的理解,我们使用了一种简单优雅的方式进行了介绍,首先分享下我们解决方案的整体拓扑。 ...查看全部
#背景
在最近一篇文章《 Exploring Tokens with Stellar》中我们提到了如何利用GraphQL拼接来打造解决方案。为了更好的理解,我们使用了一种简单优雅的方式进行了介绍,首先分享下我们解决方案的整体拓扑。
1.png

#GraphQL环境下微服务日益增长的麻烦
在利用GraphQL构建编程模型时,我们发现在每一项功能当中引入离散服务能够使我们的团队快速工作并实现编程模型迭代,同时实现基于微服务与应用十二要素的一系列典型优势。另一方面,分离这些服务使得团队无法充分利用更多的诸如服务器批处理和缓存等GraphQL高级功能。同时,我们在并行状态下也试图简化客户端编程模型,以及Apollo如何在定义完善的端点上来初始化客户端,这为开发团队带来了其它的痛点。
#GraphQL拼接如何解决我们的问题
为了解决我们的痛点,团队成员决定去看看其他人是如何使用GraphQL来处理微服务架构,然后很快就发现了模式拼接的方法。在我们的微服务架构里,每个微服务都有其独一无二的服务端点以及为入口控制器所配置的路由规则。这种方法要求前端开发团队定义多个Apollo客户机来支持各种服务端点,或者在入口处简单路由规则之外来配置复杂的路由规则以支持到各种微服务端点的路由。通过使用模式拼接,我们能够合并来自各种支持GraphQL接口的复合视图微服务的模式,从而可以简化我们的客户机编程模型。其结果将是一个可编程的单个GraphQL端点。另外,这使得我们的客户端编程模型能够真正使用跨越多个服务的GraphQL和批处理查询,而这在以前的体系结构中是不可能做到的。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#GraphQL拼接的架构
GraphQL拼接是一种部署在现有拓扑上的附加运行服务(参见上面的拓扑)。我们的解决方案中在入口控制器和GraphQL端点之间部署了一个GraphQL代理进行路由流量。其核心功能由一个名为GraphQL - tools的社区Node.js模块提供,该模块由一组定义良好且需要实现的API接口组成。拼接的作用是从一组已经定义好的GraphQL端点中来导入和合并模式。此运行时服务负责将入站请求负载映射到对应合适的端点上,同时处理错误和保证安全性。
#我们服务所使用的GraphQL拼接方法
在下面的例子中,包含了用于合并模式的整个资源,同时这些资源也使用各种GraphQL修改查询来初始化Apollo服务。请注意,拼接此时扮演了实际服务代理的角色,并且在模式合并完成后是静态不变的。因此,如果你正在管理的拼接的GraphQL模式是不稳定的,那么你将需要定期监控这些端点的变化。在我们的例子中,利用了Kubernetes的就绪性和活跃性探测来跟踪更改,并在发生更改时来触发服务的重新加载。
const {
makeRemoteExecutableSchema,
introspectSchema,
mergeSchemas
} = require('graphql-tools');
const { createHttpLink } = require('apollo-link-http');
const { setContext } = require('apollo-link-context');
const { ApolloServer } = require('apollo-server-express');
const fetch = require('node-fetch');
const log4js = require('log4js');
const logger = log4js.getLogger('server.js');
logger.level = process.env.LOG_LEVEL || 'debug';
const express = require('express');

const app = express();
const _ = require('lodash');

const ACCOUNT_URI =
process.env.BASE_URI || 'http://token-factory-account-service:4001'; // override for not in K8
const REGISTRATION_URI =
process.env.BASE_URI || 'http://token-factory-registration-service:4000'; // override for not in K8
const STELLAR_NODE_URI = 'https://core-test.gly.sh/graphql'
let schema = false;

// graphql API metadata
const graphqlApis = [
{
uri: STELLAR_NODE_URI //Token Factory APIs will override this default set
},
{
uri: ACCOUNT_URI + '/account'
},
{
uri: REGISTRATION_URI + '/registration'
}
];

// authenticate for schema usage
const context = ({ req }) => {
return { req };
};

// create executable schemas from remote GraphQL APIs
const createRemoteExecutableSchemas = async () => {
let schemas = [];
for (const api of graphqlApis) {
const http = new createHttpLink({ uri: api.uri, fetch });

const link = setContext((request, previousContext) => {
return {
headers: {
authorization: previousContext.graphqlContext
? previousContext.graphqlContext.req.headers.authorization
: ''
}
};
}).concat(http);

const remoteSchema = await introspectSchema(link);
const remoteExecutableSchema = makeRemoteExecutableSchema({
schema: remoteSchema,
link
});
schemas.push(remoteExecutableSchema);
}
return schemas;
};

const createNewSchema = async () => {
const schemas = await createRemoteExecutableSchemas();
if (!schemas) {
return false;
} else {
return mergeSchemas({
schemas
});
}
};

const port = 3001;
const path = '/token-factory';
app.path = path;

//Kub8 health check
app.get('/readiness', async function(req, res) {
try {
schema = await createNewSchema();
if (schema) {
const server = await new ApolloServer({ schema });
server.applyMiddleware({ app, path });
res.status(200).json({
message: 'Graphql service is ready. All services are connected'
});
} else {
res
.status(500)
.json({ err: 'Graphql service not ready. Waiting on services' });
}
} catch (error) {
console.log('error', error);
res.status(500).json({ err: 'Graphql service is unreachable' });
}
});

app.get('/liveness', async function(req, res) {
try {
const tmpSchema = await createNewSchema();
if (tmpSchema && _.differenceBy(tmpSchema, schema).length === 0) {
res.status(200).json({
message:
'Graphql service is alive and no changes to schema have occurred'
});
} else {
res.status(500).json({ err: 'Graphql schema has changed.' });
}
} catch (error) {
console.log('error', error);
res.status(500).json({ err: 'Graphql service is unreachable' });
}
});

const startServer = async () => {
app.listen({ port: port }, () => {
logger.info(`App listening on :${port}${app.path}!`);
});

try {
schema = await createNewSchema();
if (schema) {
const server = new ApolloServer({
schema,
context: context
});
logger.info('schema merged', schema);
server.applyMiddleware({ app, path });
}
} catch (error) {
logger.info(
'Failed to create schema during startup. Defer to K8 probes',
error
);
}
};

startServer();

#远程第三方模式介绍
在上述程序脚本的22行可以看到这样的代码:
const STELLAR_NODE_URI = 'https://core-test.gly.sh/graphql'

GraphQL拼接最强大的功能之一是能够使用第三方的API接口并把它们与已有的GraphQL API接口合并在一起。Stellar开发人员社区十分活跃,并为开发人员提供了许多有用的帮助。我们可以依赖的是一个位于Stellar testnet的Postgres数据库之上支持GraphQL的API层,关于这点以及关于开发者生态系统的强大功能的案例可以在《Building Stellar Apps》这篇文章中找到。这个端点增加了我们的API,并为每个Stellar核心数据库公开了一组丰富的GraphQL查询集。这真是太棒了。
#结论
站在以RESTAPI为中心的角度来看,GraphQL能够执行适合多个前端应用程序的细粒度查询,改变了游戏规则。通过使用GraphQL拼接,我们构建了健壮的API接口策略,既可用来消费使用,又具备了在未来添加额外微服务的扩展能力。

原文链接:GraphQL in a Micro Services Architecture(翻译:韦峻峰)

容器、微服务与服务网格

cleverlzc 发表了文章 • 0 个评论 • 287 次浏览 • 2019-05-26 11:03 • 来自相关话题

【编者的话】本文结合dotCloud的发展为例,阐述了一个基本的服务网格应该具备什么样的能力,对诸如Istio、Linkerd、Consul Connect等现代服务网格系统的基本模式进行了阐述,最后对于“自建还是购买(或者使用开源)”这个老生常谈的话题作者给 ...查看全部
【编者的话】本文结合dotCloud的发展为例,阐述了一个基本的服务网格应该具备什么样的能力,对诸如Istio、Linkerd、Consul Connect等现代服务网格系统的基本模式进行了阐述,最后对于“自建还是购买(或者使用开源)”这个老生常谈的话题作者给出了自己的见解。

如你所知,已经有很多关于服务网格的资料(1234),但这是另外一篇。是的!但是为什么会有这篇文章呢?因为我想给你们一些不同的视角,他们希望服务网格在10年前就已经存在,远早于Docker和Kubernetes这样的容器平台的兴起。我并不是说这个视角比其他视角更好或更差,但是由于服务网格是相当复杂的“野兽”,所以我相信多种视角有助于更好地理解它们。

我将讨论dotCloud平台,这是一个建立在100多个微服务之上的平台,支持数千个运行在容器中的生产应用程序;我将解释在构建和运行它时所面临的挑战;以及服务网格会(或不会)提供帮助。
# dotCloud的历史
我已经写过关于dotCloud平台的历史和它的一些设计选择,但是我没有过多地讨论它的网络层。如果你不想深入了解我之前关于dotCloud的博客,你需要知道的是它是一个PaaS,允许客户运行各种应用程序(Java、PHP、Python等),支持广泛的数据服务(MongoDB、MySQL、Redis等)以及类似于Heroku的工作流程:你可以将代码推送到平台,平台将构建容器映像,并部署这些容器映像。

我将告诉你流量是如何在dotCloud平台上路由的;不是因为它是特别棒或其他什么(我认为现在是比较合适的时间),但主要是因为,如果一个普通的团队需要一种在一个微服务群或一个应用程序群之间路由流量的方法,那么这种设计可以在短时间内用现在已有的工具轻松实现。因此,它将为我们提供一个很好的比较点,“如果我们破解它,我们会得到什么”和“如果我们使用现有的服务网格,我们会得到什么”,也就是老生常谈的“构建与购买”的困境。
# 托管应用的流量路由
部署在dotCloud上的应用程序会暴露HTTP和TCP端点。

HTTP端点被动态地添加到Hipache负载平衡器集群的配置中。这与我们今天使用Kubernetes Ingress资源和Traefik这样的负载平衡器可以实现的功能类似。如果你想和更多Kubernetes技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

只要域名指向dotCloud的负载平衡器,客户端就可以使用它们的关联域名连接到HTTP端点。这里没有什么特别的。

TCP端点与端口号相关联,然后端口号通过环境变量与该堆栈上的所有容器通信。

客户端可以使用指定的主机名(类似于gateway-X.dotcloud.com)和端口号连接到TCP端点。

该主机名将解析为一个“nats”服务器集群(与NATS没有任何关系),该集群将把传入的TCP连接路由到正确的容器(或者,在负载平衡服务的情况下,路由到正确的容器)。

如果你熟悉Kubernetes,这可能会让你想起NodePort服务。

dotCloud平台没有集群IP服务的等价物:为了简单起见,从内部和外部访问服务的方式是相同的。

这非常简单,最初的HTTP和TCP路由网格的实现可能都是几百行Python代码,使用相当简单(我敢说,很天真)的算法,但是随着时间的推移,它们不断发展,以处理平台的增长和额外的需求。

它不需要对现有应用程序代码进行大量重构。十二因素应用程序尤其可以直接使用通过环境变量提供的地址信息。
# 它与现代服务网络有何不同?
可观察性有限。对于TCP路由网格根本没有度量标准。至于HTTP路由网格,后来的版本提供了详细的HTTP度量,显示错误状态码和响应时间;但是现代服务网格的功能远远不止于此,它还提供了与度量收集系统(例如Prometheus)的集成。

可观察性非常重要,不仅从操作角度(帮助我们解决问题),还可以提供安全的蓝/绿部署金丝雀部署等功能。

路由效率也受到限制。在dotCloud路由网格中,所有流量都必须经过一组专用路由节点。这意味着可能跨越几个AZ(可用性区域)边界,并显著增加延迟。我记得对一些代码进行故障排除,这些代码发出100多个SQL请求来显示给定的页面,并为每个请求打开了到SQL服务器的新连接。在本地运行时,页面会立即加载,但在dotCloud上运行时,需要几秒钟,因为每个TCP连接(以及随后的SQL请求)都需要几十毫秒才能完成。在这种特定的情况下,使用持久连接起了作用。

现代服务网络做得更好。首先,通过确保连接在源位置路由。逻辑流仍然是客户端-->网格-->服务,但是现在网格在本地运行,而不是在远程节点上运行,因此客户端-->网格连接是本地连接,因此速度非常快(微秒而不是毫秒)。

现代服务网格还实现了更智能的负载平衡算法。通过监控后端的运行及健康状况,它们可以在更快的后端上发送更多的流量,从而提高整体性能。

随着现代服务网络的出现,安全性也越来越强。dotCloud路由网格完全在EC2 Classic上运行,并且没有加密流量(假设如果有人设法嗅探EC2上的网络流量,那么无论如何都会遇到更大的问题)。现代服务网格可以透明地保护我们所有的通信,例如通过相互的TLS身份验证和随后的加密。
# 平台服务的流量路由
OK,我们已经讨论了应用程序是如何通信的,但是dotCloud平台本身呢?

平台本身由大约100个微服务组成,负责各种功能。其中一些服务接受来自其他服务的请求,而其中一些服务是后台工作应用,它们将连接到其他服务,但不能自己接收连接。无论哪种方式,每个服务都需要知道它需要连接到的地址的端点。

许多高级服务都可以使用上面描述的路由网格。事实上,dotCloud平台的100多个微服务中有很大一部分是作为常规应用程序部署在dotCloud平台上的。但是少数低级服务(特别是那些实现路由网格的服务)需要一些更简单的东西,需要更少的依赖关系(因为它们不能依靠自己来运行;这是一个老生常谈的“先有鸡还是先有蛋”的问题)。

通过直接在几个关键节点上启动容器,而不是依赖于平台的构建器、调度程序和运行器服务,部署了这些底层的基本平台服务。如果你想要与现代容器平台进行比较,这就像直接在节点上运行Docker来启动我们的控制平面,而不是让Kubernetes为我们做这件事。这与kubeadmbootkube在引导自托管集群时使用的静态Pod的概念非常相似。

这些服务以一种非常简单和粗糙的方式被公开:有一个YAML文件列出了这些服务,将它们的名称映射到它们的地址;作为其部署的一部分,这些服务的每个使用者都需要一份该YAML文件的副本。

一方面,这是非常强大的,因为它不涉及像ZooKeeper那样维护外部键值存储(记住,etcd或Consul在那个时候不存在)。另一方面,这使得服务难以移动。每次移动服务时,它的所有消费者都需要接收更新的YAML文件(并且可能会重新启动)。不太方便!

我们开始实现的解决方案是让每个消费者都连接到一个本地代理。使用者不需要知道服务的完整地址+端口,只需要知道它的端口号,并通过localhost进行连接。本地代理将处理该连接,并将其路由到实际后端。现在,当一个后端需要移动到另一台机器上,或按比例放大或缩小,而不是更新它的所有消费者,我们只需要更新所有这些本地代理;我们不再需要重新启动消费者。

(还计划将流量封装在TLS连接中,并在接收端使用另一个代理来打开TLS并验证证书,而不涉及接收服务,该服务将被设置为仅在本地主机上接受连接。稍后会详细介绍。)

这与AirBNB的SmartStack非常相似;与SmartStack实现并部署到生产环境的显著区别是,当dotCloud转向Docker时,它的新的内部路由网格被搁置了。

我个人认为SmartStack是诸如Istio、Linkerd、Consul Connect等系统的先驱之一,因为所有这些系统都遵循这种模式:

  • 在每个节点上运行代理
  • 消费者连接到代理
  • 后端改变时,控制平面更新代理的配置
# 今天实现一个服务网格如果我们今天必须实现类似的网格,我们可以使用类似的原则。例如,我们可以设置一个内部域名系统区域,将服务名映射到127.0.0.0/8空间中的地址。然后在集群的每个节点上运行HAProxy,接受每个服务地址(在127.0.0.0/8子网中)上的连接,并将它们转发/负载平衡到适当的后端。HAProxy配置可以由confd管理,允许在etcd或Consul中存储后端信息,并在需要时自动将更新的配置推送到HAProxy。这就是Istio的工作原理!但是有一些不同之处:
  • 它使用Envoy Proxy而不是HAProxy
  • 它使用Kubernetes API而不是etcd或Consul来存储后端配置
  • 服务在内部子网中分配地址(Kubernetes集群IP地址),而不是127.0.0.0/8
  • 它有一个额外的组件(Citadel),用于在客户机和服务器之间添加相互的TLS身份验证
  • 它增加了对诸如断路、分布式跟踪、金丝雀部署等新特性的支持

让我们快速回顾一下这些差异。
## Envoy Proxy
Envoy Proxy由Lyft撰写。它与其他代理(如HAProxy、NGINX、Traefik)有许多相似之处,但Lyft编写它是因为它们需要当时这些代理中不存在的功能,而且构建一个新的代理比扩展现有代理更有意义。

Envoy可以单独使用。如果有一组给定的服务需要连接到其他服务,可以把它连接到Envoy,然后动态地配置和重新配置其他服务的Envoy的位置,而得到很多漂亮的额外的功能,比如域的可观测性。这里,没有使用定制的客户端库,也没有在代码中添加跟踪调用,而是将流量定向到Envoy,让它为我收集指标。

但Envoy也可以用作服务网格的数据平面。这意味着现在将由该服务网格的控制平面配置Envoy。
## 控制平面
说到控制平面,Istio依赖于Kubernetes API。这与使用confd没有太大的不同。confd依赖etcd或Consul来监视数据存储中的一组密钥。Istio依赖Kubernetes API来监视一组Kubernetes资源。

Aparte:我个人认为阅读Kubernetes API描述非常有帮助。

Kubernetes API服务器是一个“哑服务器”,它提供API资源上的存储、版本控制、验证、更新和监视语义。



Istio是为与Kubernetes合作而设计的;如果你想在Kubernetes之外使用它,则需要运行Kubernetes API服务器的实例(以及支持的etcd服务)。
## 服务地址
Istio依赖Kubernetes分配的集群IP地址,因此Istio得到一个内部地址(不在127.0.0.1/8范围)。

在没有Istio的Kubernetes集群上,前往给定服务的ClusterIP地址的流量被kube-proxy拦截,并发送到该代理的后端。更具体地说,如果你想确定技术细节:kube-proxy设置iptables规则(或IPVS负载平衡器,取决于它是如何设置的)来重写连接到集群IP地址的目标IP地址。

一旦Istio安装在Kubernetes集群上,就不会发生任何变化,直到通过将sidecar容器注入到使用者Pod中,显式地为给定的使用者甚至整个名称空间启用Istio。sidecar将运行一个Envoy实例,并设置一些iptables规则来拦截到其他服务的流量,并将这些流量重定向到Envoy。

结合Kubernetes DNS集成,这意味着我们的代码可以连接到一个服务名,一切都可以正常工作。换句话说,比如我们的代码向`http://api/v1/users/4242`发起一个请求,`api`将解析到10.97.105.48,一条iptables规则将解释连接到10.97.105.48并重定向到本地Envoy代理,本地代理将这个请求路由到实际的API后端。
## 额外的铃声和哨声
Istio还可以通过名为Citadel的组件通过mTLS(双向TLS)提供端到端加密和身份验证。

它还包括混合器,Envoy组件可以查询每一个请求,对请求进行一个临时的决定取决于各种因素,例如请求头、后端负载(别担心,有丰富的规定以确保混合高度可用,即使它休息,Envoy可以继续代理流量)。

当然,我提到了可观察性。Envoy在提供分布式跟踪的同时收集大量的度量指标。微服务架构,如果单个API请求必须经过微服务A、B、C和D,分布式跟踪将添加一个惟一的标识符请求进入系统,并保留标识符在子请求中,所有这些微服务允许收集所有相关的调用、延迟等。
# 自建还是购买
Istio以复杂著称。相比之下,使用我们今天拥有的工具,构建像我在本文开头描述的那样的路由网格相对比较简单。那么,构建我们自己的服务网格是否有意义呢?

如果我们有适度的需求(如果我们不需要可观察性,断路器,和其他细节),我们可能想建立自己的。但是如果我们正在使用Kubernetes,我们甚至可能不需要这样做,因为Kubernetes已经提供了基本的服务发现和负载平衡。

现在,如果我们有高级的需求,购买服务网格可能是一个更好的选择。(由于Istio是开源的,所以它并不总是真正的购买,但是我们仍然需要投入工程时间来理解它是如何工作、部署和运行的。)
#如何选择Istio、Linkerd和Consul Connect
到目前为止,我们只讨论了Istio,但它并不是唯一的服务网格。Linkerd是另一个流行的选择,还有Consul Connect

我们应该选哪一个呢?

实际上在这一点上我也不好说,我不认为我有足够的了解能够帮助任何人做决策。不过,已经有一些有趣的文章比较它们(12),甚至基准测试

一种值得一提并且很有潜力的方法是使用像SuperGloo这样的工具。SuperGloo提供了一个抽象层来简化和统一服务网格公开的API。我们可以使用SuperGloo提供的更简单的构造,并无缝地从一个服务网格切换到另一个服务网格,而不是学习各种服务网格的特定API(在我看来,相对复杂)。有点像我们有一个描述HTTP前端和后端的中间配置格式,能够为NGINX、HAProxy、Traefik、Apache生成实际配置

我已经使用SuperGloo稍微涉足Istio,在未来的博客文章中,我想说明如何使用SuperGloo将Isio或Linkerd添加到现有的集群中,以及后者是否能实现它的承诺,即允许我在不重写配置的情况下从一个路由网格切换到另一个。

如果你喜欢这篇文章,并且想让我尝试一些具体的场景,我很乐意听到你的消息!

原文链接:Containers, microservices, and service meshes

译者:Mr.lzc,软件研发工程师、DevOpsDays深圳组织者&志愿者,目前供职于华为,从事云存储工作,以Cloud Native方式构建云文件系统服务,专注于K8s、微服务领域。

⼤型微服务框架设计实践

大卫 发表了文章 • 0 个评论 • 351 次浏览 • 2019-05-24 18:55 • 来自相关话题

大家好,我是杜欢,很荣幸能代表滴滴来做分享。我来滴滴的第一件事情就是帮助公司统一技术栈,在服务端我们要把以前拿 PHP 和 Java 做的服务统一起来,经过很多思考和选择之后我们决定用 Go 来重构大部分业务服务。现在,滴滴内部已经有非常多的用 Go 实现的服 ...查看全部
大家好,我是杜欢,很荣幸能代表滴滴来做分享。我来滴滴的第一件事情就是帮助公司统一技术栈,在服务端我们要把以前拿 PHP 和 Java 做的服务统一起来,经过很多思考和选择之后我们决定用 Go 来重构大部分业务服务。现在,滴滴内部已经有非常多的用 Go 实现的服务和大量 Go 开发者。

《⼤型微服务框架设计实践》是一个很大的话题,这个题目其实分为三个方面,“微服务框架”、“大型”和“设计实践”。我们日常看到的各种开源微服务框架,在我看来都不算“大型”,解决的问题比较单纯。大型微服务框架究竟是什么,又应该怎么去一步步落地实践,我会从问题出发,分别从以下几个方面来探讨这个话题。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#发现问题:服务开发过程中的痛点

##复杂业务开发过程中的痛点

我们在进行复杂业务开发的过程中,有以下几个常见的痛点:

* 时间紧、任务多、团队⼤、业务增⻓快,如何还能保证架构稳定可靠?
* 研发⽔平参差不⻬、项⽬压⼒⾃顾不暇,如何保证质量基线不被突破?
* 公司有各种⼯具平台、SDK、最佳实践,如何尽可能的在业务中使⽤?

互联网业务研发的特点是“快”、“糙”、“猛”:开发节奏快、质量较粗糙、增长迅猛。我们能否做到“快”、“猛”而“不糙”呢?这就需要有一些技术架构来守住质量基线,在业务快速堆砌代码的时候也能保持技术架构的健康。

在大型项目中,我们也经常会短时间聚集一批人参与开发,很显然我们没有办法保证这些人的能力和风格是完全拉齐的,我们需要尽可能减少“人”在项目质量中的影响。

公司内有大量优秀的技术平台和工具,业务中肯定是希望尽可能都用上的,但又不想付出太多的使用成本,必定需要有一些技术手段让业务与公司基础设施无缝集成起来。

很自然我们会想到,有没有一种“框架”可以解决这个问题,带着这个问题我们探索了所有的可能性并找到一些答案。
#以史鉴今:从服务框架的演进历程中找到规律

##服务框架进化史

1.jpg

服务框架的历史可以追溯到 1995 年,PHP 在那一年诞生。PHP 是一个服务框架,这个语言首先是一个模板,其次才是一种语言,默认情况下所有的 PHP 文件内容都被直接发送到客户端,只有使用了 `` 标签的部分才是代码。在这段时间里,我们也称作 Web 1.0 时代里,浏览器功能还不算强,很多的设计理念来源于 C/S 架构的想法。这时候的服务框架的巅峰是 2002 年推出的 ASP.net,当年真的是非常惊艳,我们可以在 Visual Studio 里面通过拖动界面、双击按钮写代码来完成一个网页的开发,非常具有颠覆性。当然,由于当时技术所限,这样做出来的网页体验并不行,最终没有成为主流。

接着,Web 2.0 时代来临了,大家越来越觉得传统软件中经常使用的 MVC 模式特别适合于服务端开发。Django 发布于 2003 年,这是一款非常经典的 MVC 框架,包含了所有 MVC 框架必有的设计要素。MVC 框架的巅峰当属 Ruby on Rails,它给我们带来了非常多先进的设计理念,例如“约定大于配置”、Active Record、非常好用的工具链等。

2005 年后,各种 MVC 架构的服务框架开始井喷式出现,这里我就不做一一介绍。
##标志性服务框架

2.jpg

随着互联网业务越来越复杂,前端逻辑越来越重,我们发现业务服务开始慢慢分化:页面渲染的工作回到了前端;Model 层逐步下沉成独立服务,并且催生了 RPC 协议的流行;业务接入层只需要提供 API。于是,MVC 中的 V 和 M 逐步消失,演变成了路由框架和 RPC 框架两种形态,分别满足不同的需求。2007 年,Sinatra 发布了,它是一个非常极致的纯路由框架,大量使用 middleware 设计来扩展框架能力,业务代码可以实现的非常简洁优雅。这个框架相对小众(GitHub Stars 10k,实际也算很有名了),其设计思想影响了很多后续框架,包括 Express.js、Go martini 等。同年,Thrift 开源,这是 Facebook 内部使用 RPC 框架,现在被广泛用于各种微服务之中。Google 其实更早就在内部使用 Protobuf,不过直到 2008 年才首次开源。

再往后,我们的基础设施开始发生重大变革,微服务概念兴起,虚拟化、Docker 开始越来越流行,服务框架与业务越发解耦,甚至可以做到业务几乎无感知。2018 年刚开源的 Istio 就是其中的典型,它专注于解决网络触达问题,包括服务治理、负载均衡、动态扩缩容等。
##服务框架的演进趋势

通过回顾服务框架的发展史,我们发现服务框架变得越来越像一种新的“操作系统”,越来越多的框架让我们忘记了 Web 开发有多么复杂,让我们能专注于业务本身。就像操作系统一样,我们在业务代码中以为直接操作了内存,但其实并不然,操作系统为我们屏蔽了总线寻址、虚地址空间、缺页中断等一系列细节,这样我们才能将注意力放在怎么使用内存上,而不是这些跟业务无关的细节。
3.jpg

随着框架对底层的抽象越来越高,框架的入门门槛在变低,以前我们需要逐步学习框架的各种概念之后才能开始写业务代码,到现在,很多框架都提供了非常简洁好用的工具链,使用者很快就能专注输出业务代码。不过这也使得使用者更难以懂得框架背后发生的事情,想要做一些更深层次定制和优化时变得相对困难很多,这使得框架的学习曲线越发趋近于“阶跃式”。

随着技术进步,框架也从代码框架变成一种运行环境,框架代码与业务代码也不断解耦。这时候就体现出 Go 的一些优越性了,在容器生态里面,Go 占据着先发优势,同时 Go 的 interface 也非常适合于实现 duck-typing 模式,避免业务代码显式的与框架耦合,同时 Go 的语法相对简单,也比较容易用一些编译器技巧来透明的增强业务代码。
#⼤道⾄简:⼤型微服务框架的设计要点

##站在全局视角观察微服务架构

服务框架的演进过程是有历史必然性的。

传统 Web 网站最开始只是在简单的呈现内容和完成一些单纯的业务流程,传统的“三层结构”(网站、中间件、存储)就可以非常好的满足需求。

Web 2.0 时代,随着网络带宽和浏览器技术升级,更多的网站开始使用前端渲染,服务端则更多的退化成 API Gateway,前后端有了明显的分层。同时,由于互联网业务越来越复杂,存储变得越来越多,不同业务模块之间的存储隔离势在必行,这种场景催生了微服务架构,并且让微服务框架、服务发现、全链路跟踪、容器化等技术日渐兴盛,成为现在讨论的热点话题,并且也出现了大量成熟可用的技术方案。

再往后呢?我们在滴滴的实践中发现,当一个公司的组织结构成长为多事业群架构,每个事业群里面又有很多事业部,下面还有各种独立的部门,在这种场景下,微服务之间也需要进行隔离和分层,一个部门往往会需要提供一个 API 或 broker 服务来屏蔽公司内其他服务对这个部门服务的调用,在逻辑上就形成了由多个独立微服务构成的“大型微服务”。
4.jpg

在大型微服务架构中,技术挑战会发生什么变化?

据我所知,国内某一线互联网公司的一个事业群里部署了超过 10,000 个微服务。大家可以思考一下,假如一个项目里面有 10,000 个 class 并且互相会有各种调用关系,要设计好这样的项目并且让它容易扩展和维护是不是很困难?这是一定的。如果我们把一个微服务类比成一个 class,为了能够让这么复杂的体系可以正常运转,我们必须给 class 进行更进一步的分类,形成各种 class 之上的设计模式,比如 MVC。以我们开发软件的经验来看,当开发单个 class 不再成为一件难事的时候,如何架构这些 class 会变成我们设计的焦点。

我们看到前面是框架,更多解决是日常基础的东西,但是对于人与人之间如何高效合作、非常复杂的软件架构如何设计与维护,这些方面并没有解决太好。

大型微服务的挑战恰好就在于此。当我们解决了最基本的微服务框架所面临的挑战之后,如何进一步方便架构师像操作 class 一样来重构微服务架构,这成了大型微服务框架应该解决的问题。这对于互联网公司来说是一个问题,比如我所负责的业务整个代码量几百万行,看起来听多了,但跟传统软件比就没那么吓人。以前 Windows 7 操作系统,整体代码量一亿行,其中最大的单体应用是 IE 有几百万行代码,里面的 class 也有上万个了。对于这样规模的软件要注意什么呢?是各种重构工具,要能一键生成或合并或拆分 class,要让软件的组织形式足够灵活。这里面的解决方法可以借鉴传统软件的开发思路。
##大型微服务框架的设计目标

5.jpg

结合上面这些分析,我们意识到大型微服务框架实际上是开发人员的“效率产品”,我们不但要让一线研发专注于业务开发,也要让大家几乎无感知的使用公司各种基础设计,还要让架构师能够非常轻易的调整微服务整体架构,方便像重构代码一样重构微服务整体架构,从而提升架构的可维护性。

公司现有架构就是业务软件的操作系统,不管公司现有架构是什么,所有业务架构必须基于公司现有基础进行构建,没有哪个部门会在做业务的时候分精力去做运维系统。现在所有的开源微服务框架都不知道大家底层实际在用什么,只解决一些通用性问题,要想真的落地使用还需要做很多改造以适应公司现有架构,典型的例子就是 dubbo 和阿里内部的 HSF。为什么内部不直接使用 dubbo?因为 HSF 做了很多跟内部系统绑定的事情,这样可以让开发人员用的更爽,但也就跟开源的系统渐行渐远了。

大型微服务框架是微服务框架之上的东西,它是在一个或多个微服务框架之上,进一步解决效率问题的框架。提升效率的核心是让所有业务方真正专注于业务本身,而不是想很多很重复的问题。如果 10,000 个服务花 5,000 人维护,每个人都思考怎么接公司系统和怎么做好稳定性,就算每次开发过程中花 10% 的时间思考这些,也浪费了 5,000 人的 10% 时间,想想都很多,省下来可以做很多业务。
##Rule of least power

要想设计好大型微服务框,我们必须遵循“Rule of least power”(够用就好)的原则。

这个原则是由 WWW 发明者 Tim Berners-Lee 提出的,它被广泛用于指导各种 W3C 标准制定。Tim BL 说,最好的设计不是解决所有问题,而是恰好解决当下问题。就是因为我们面对的需求实际上是多变的,我们也不确定别人会怎么用,所以我们要尽可能只设计最本质的东西,减少复杂性,这样做反而让框架具有更多可能性。

Rule of least power 其实跟我们通常的设计思想相左,一般在设计框架的时候,架构师会比较倾向于“大而全”,由于我们一般都很难预测框架的使用者会如何使用,于是自然而然的会提供想象中“可能会被用到”的各种功能,导致设计越来越可扩展的同时也越来越复杂。各种软件框架的演进历史告诉我们,“大而全”的框架最终都会被使用者抛弃,而且抛弃它的理由往往都是“太重了”,非常具有讽刺意味。

框架要想设计的“好”,就需要抓住需求的本质,只有真正不变的东西才能进入框架,还没想清楚的部分不要轻易纳入框架,这种思想就是 Rule of least power 的一种应用方式。
##大型微服务框架的设计要点

结合 Rule of least power 设计思想,我们在这里列举了大型微服务框架的设计要点。
6.jpg

最基本的,我们需要实现各种微服务框架必有的功能,例如服务治理、水平扩容等。需要注意的是,在这里我们并不会再次重复造轮子,而是大量使用公司内外已有的技术积累,框架所做的事情是统一并抽象相关接口,让业务代码与具体实现解耦。

从工具链层面来说,我们让业务无需操心开发调试之外的事情,这也要求与公司各种进行无缝集成,降低使用难度。

从设计风格上来说,我们提供非常有限度的扩展度,仅在必要的地方提供 interceptor 模式的扩展接口,所有框架组件都是以“组合”(composite)而不是“继承”(inherit)方式提供给开发者。框架会提供依赖注入的能力,但这种依赖注入与传统意义上 IoC 有一点区别,我们并不追求框架所有东西都可以 IoC,只在我们觉得必要的地方有限度的开放这种能力,用来方便框架兼容一些开源的框架或者库,而不是让业务代码轻易的改变框架行为。

大型微服务框架最有特色的部分是提供了非常多的“可靠性”设计。我们刻意让 RPC 调用的使用体验跟普通的函数调用保持一致,使用者只用关系返回值,永远不需要思考崩溃处理、重试、服务异常处理等细节。访问基础服务时,开发者可以像访问本地文件一样的访问分布式存储,也是不需要关心任何可用性问题,正常的处理各种返回值即可。在服务拆分和合并过程中,我们的框架可以让拆分变得非常简单,真的就跟类重构类似,只需要将一个普通的 struct methods 进行拆分即可,剩下的所有事情自然而然会由框架做好。
#精雕细琢:框架关键实现细节

##业务实践

接下来,我们聊聊这个框架在具体项目中的表现,以及我们在打磨细节的过程中积累的一些经验。

我们落地的场景是一个非常大型的业务系统,2017 年底开始设计并开发。这个业务已经出现了五年,各个巨头已经投入上千名研发持续开发,非常复杂,我们不可能在上线之初就完善所有功能,要这么做起码得几百人做一年,我们等不起。实际落地过程中,我们投入上百人从一个最小系统慢慢迭代出来,最初版本只开发了四个多月。

最开始做技术选型时,我们也在思考应该用什么技术,甚至什么语言。由于滴滴从 2015 年以来已经积累了 1,500+ Go 代码模块、上线了 2,000+ 服务、储备了 1000+ Go 开发者,这使得我们非常自然的就选择 Go 作为最核心的开发语言。

在这个业务中我们实现了非常多的核心能力,基本实现了前面所说大型微服务框架的各种核心功能,并达成预期目标。
7.jpg

同时,也因为滴滴拥有相对完善的基础设施,我们在开发框架的时候也并没有花费太多时间重复造一些业务无关的轮子,这让我们在开发框架的时候也能专注于实现最具有特色的部分,客观上帮助我们快速落地了整体架构思想。

上图只是简单列了一些我们业务中常用的基础设施,其实还有大量基础设施也在公司中被广泛使用,没有提及。
##整体架构

8.jpg

上图是我们框架的整体架构。绿色部分是业务代码,黄色部分是我们的框架,其他部分是各种基础设施和第三方框架。

可以看到,绿色的业务代码被框架整个包起来,屏蔽了业务代码与底层的所有联系。其实我们的框架只做了一点微小的工作:将业务与所有的 I/O 隔离。未来底层发生任何变化,即使换了下面的服务,我们能够通过黄色的兼容层解决掉,业务一行代码不用,底层 driver 做了任何升级业务也完全不受影响。

结合微服务开发的经验,我们发现微服务开发与传统软件开发唯一的区别就是在于 I/O 的可靠程度不同,以前我们花费了大量的时间在各种不同的业务中处理“稳定性”问题,其实归根结底都是类似的问题,本质上就是 I/O 不够可靠。我们并不是要真的让 I/O 变得跟读取本地文件一样可靠,而是由框架统一所有的 I/O 操作并针对各种不可靠场景进行各种兜底,包括重试、节点摘除、链路超时控制等,让业务得到一个确定的返回值——要么成功,要么就彻底失败,无需再挣扎。

实际业务中,我们使用 I/O 的种类其实很少,也就不过十几种,我们这个框架封装了所有可能用到的 I/O 接口,把它们全部变成 Go interface 提供给业务。
##实现要点

前面说了很多思路和概念,接下来我来聊聊具体的细节。

我们的框架跟很多框架都不一样,为了实现框架与业务正交,这个框架干脆连最基本的框架特征都没有,MVC、middleware、AOP 等各种耳熟能详的框架要素在这里都不存在,我们只是设计了一个执行环境,业务只需要提供一个入口 type,它实现了所有业务需要对外暴露的公开方法,框架就会自动让业务运转起来。

我们同时使用两种技术来实现这一点。一方面,我们提供了工具链,对于 IDL-based 的服务框架,我们可以直接分析 IDL 和生成的 Go interface 代码的 AST,根据这些信息透明的生成框架代码,在每个接口调用前后插入必要的 stub 方便框架扩展各种能力。另一方面,我们在程序启动的时候,通过反射拿到业务 type 的信息,动态生成业务路由。

做到了这些事情之后业务开发就完全无需关注框架细节了,甚至我们可以做到业务像调试本地程序一样调试微服务。同时,我们用这种方式避免业务思考“版本”这个问题,我们看到,很多服务框架都因为版本分裂造成了很大的维护成本,当我们这个框架成为一个开发环境之后,框架升级就变得完全透明,实际中我们会要求业务始终使用最新的框架代码,从来不会使用 semver 标记版本号或者兼容性,这样让框架的维护成本也大大降低。“更大的权力意味着更大的责任”,我们也为框架写了大量的单元测试用例保证框架质量,并且规定框架无限向前兼容,这种责任让我们非常谨慎的开发上线功能,非常收敛的提供接口,从而保持业务对框架的信任。
9.jpg

大家也许听说过,Go 官方的 database/sql 的 Stmt 很好用但是有可能会出现连接泄漏的问题,当这个问题刚被发现的时候,公司很多业务线都不得不修改了代码,在业务中避免使用 Stmt,而我们的业务代码完全不需要做任何修改,框架用很巧妙的方法直接修复了这个问题。

下图是框架的启动逻辑,可以看到,这个逻辑非常简单:首先创建一个 Server 实例 s,传入必要的配置参数;然后新建一个业务类型实例 handler,这个业务类型只是个简单的 type,并没有任何约束;最后将接口 IDL interface 和 handler 传入 s,启动服务即可。

我们在 handler 和 IDL interface 之间加一个夹层并做了很多事情,这相当于在业务代码的执行开始和结束前后插入了代码,做了参数预处理、日志、崩溃恢复和清理工作。
10.jpg

我们还需要设计一个接口层来隔绝业务和底层之间的联系。接口层本身没什么特别技术含量,只是需要认真思考如何保证底层接口非常非常稳定,并且如何避免穿透接口直接调用底层能力,要做好这一点需要非常多的心力。

这个接口层的收益是比较容易理解的,可以很好的帮助业务减少无谓的代码修改。开源框架就不能保证这一点,说不定什么时候作者心情好了改了一个框架细节,无法向前兼容,那么业务就必须跟着做修改。公司内部框架则一般不太敢改接口,生怕造成不兼容被业务投诉,但有些接口一开始设计的并不好,只好不断打补丁,让框架越来越乱。

要是真能做到接口层设计出来就不再变更,那就太好了。
11.jpg

那我们真的能做到么?是的,我们做到了,其中的诀窍就是始终思考最本质最不变的东西是什么,只抽象这些不变的部分。
12.jpg

上图就是一个经典案例,展示一下我们是怎么设计 Redis 接口的。

左边是 github.com/go-redis/redis 代码(简称 go-redis),这是一个非常著名的 Redis driver;右边是我们的 Redis 接口设计。

Go-redis 非常优秀,设计了一些很不错的机制,比如 Cmder,巧妙的解决了 Pipeline 读取结果的问题,每个接口的返回值都是一个 Cmder 实例。但这种设计并不本质,包括函数的参数与返回值类型都出现多次修改,包括我自己都曾经提过 Pull Request 修正它的一个参数错误问题,这种修改对于业务来说是非常头疼的。

而我们的接口设计相比 go-redis 则更加贴近本质,我阅读了 Redis 官方所有命令的协议设计和相关设计思路文档,Redis 里面最本质不变的东西是什么呢?当然是 Redis 协议本身。Redis 在设计各种命令时非常严谨,做到了极为严格的向前兼容,无论 Redis 从 1.0 到 3.x 如何变化,各个命令字的协议从未发生过不兼容的变化。因此,我严格参照 Redis 命令字协议设计了我们的 Redis 接口,连接口的参数名都尽量与 Redis 官方保持一致,并严格规定各种参数的类型。

我们小心的进行接口封装之后,还有一些其他收获。

还是以 Redis 为例,最开始我们底层的 Redis driver 使用的是公司广泛采用的 github.com/gomodule/redigo,但后来发现不能很好的适配公司自研的 Redis 集群一些功能,所以考虑切换成 go-redis。由于我们有这样一层 Redis 接口封装,这使得切换完全透明。
13.jpg

我们为了能够让业务研发不要关心很多的传输方面细节,我们实现了协议劫持。HTTP 很好劫持,这里不再赘述,我主要说一下如何劫持 thrift。

劫持协议的目的是控制业务参数收到或发送的协议细节,可以方便我们根据传输内容输出必要的日志或打点,还可以自动处理各种输入或输出参数,把必要参数带上,免得业务忘记。

劫持思路非常简单,我们做了一个有限状态机(FSM),在旁路监听协议的 read/write 过程并还原整个数据结构全貌。比如 Thrift Protocol,我们利用 Thrift 内置的责任链设计,自己实现了一个 protocol factory 来包装底层的 protocol,在实际 protocol 之上做了一个 proxy 层拦截所有的 ReadXXX/WriteXXX 方法,就像是在外部的观察者,记录现在 read/write 到哪一个层级、读写了什么结构。当我们发现现在正在 read/write 我们感兴趣的内容,则开始劫持过程:对于 read,如果要“欺骗”应用层提供一些额外的框架数据或者屏蔽框架才关心的数据,我们就会篡改各种 ReadXXX 返回值来让应用层误以为读到了真实数据;对于 write,如果要偷偷注入框架才关心的内容,我们会在调用 WriteXXX 时主动调用底层 protocol 的相关 write 函数来提前写入内容。

协议可以劫持之后,很多东西的处理就很简单了。比如 context,我们只要求业务在各个接口里带上 context,RPC 过程中则无需关心这个细节,框架会自动将 context 通过协议传递到下游。
14.jpg

我们实现了协议劫持之后,要想实现跨服务边界的 context 就变得很简单了。

我们根据 context interface 和设计规范实现了自己的 context 类型,用来做一些序列化与反序列化的事情,当上下游调用发生时,我们会从 context 里提取框架关心的内容并注入到协议里面,在下游再透明解析出来重新放入 context。

使用 context 时候还有个小坑:context.WithDeadline 或者 context.WithTimeout 很容易被不小心忽略返回的 cancel 函数,导致 timer 资源泄露。我们为了避免出现这种情况设计了一个低精度 timer 来尽可能避免创建真正的 time.Time 实例。
15.jpg

我们发现,业务中根本不需要那么高精度的 timer,我们说的各种超时一般精度都只到 ms,于是一个精度达 0.5ms 的 timer 就能满足所有业务需求。同时,在业务中也不是特别需要使用 Context interface 的 Done() 方法,更多的只是判断一下是否已经超时即可。为了避免大量创建 timer 和 channel,也为了避免让业务使用 cancel 函数,我们实现了一个低精度 timer pool。这是一个 timer 的循环数组,将 1s 分割成若干个时间间隔,设置 timer 的时候其实就是在这个数组上找到对应的时刻。默认情况下,done channel 都不需要初始化,直到真正有业务方需要 done channel 的时候才会 make 出来。在框架里我们非常注意的避免使用任何 done channel,从而避免消耗资源且极大的提高了性能。

业务压力大的时候,我们比较容易在代码层面上犯错,不小心就放大单点故障造成雪崩,我们借用前面所有的技术,让调用超时约束从上游传递到下游,如果单点崩溃了,框架会自动摘除故障节点并自动 fail-fast 避免压力进一步上升,从而实现防雪崩。
16.jpg

防雪崩的具体实现原理很简单:上游调用时会设置一个超时时间,这个时间通过跨边界 context 传递到下游,每个下游节点在收到请求时开始记录自己消耗的时间,如果自己耗时已经超出上游规定的超时时间就会主动停止一切 I/O 调用,快速返回错误。
17.jpg

比如上游 A 调用下游 B 前设置 500ms 超时,B 收到请求后就知道只有 500ms 可用,从收到请求那一刻开始计时,每次在调用其他下游服务前,比如访问 B 的下游 C 本身需要 200ms,但当前 B 已经消耗了 400ms,只剩 100ms 了,那么框架会自动将 C 的超时收敛到 100ms,这样 C 就知道给自己的时间不多了,一旦 C 没能在 100ms 内返回就会主动 fail-fast,避免无谓的消耗系统资源,帮助 C 和 B 快速向上游报告错误。
##业务收益

我们实现的这个框架切实的给业务带来了显著的收益。
18.jpg

我们总共用超过 100 名 Go 语言开发者,在非常大的压力下开发了好几个月便完成一个完整可运营的系统,实现了大量功能,开发效率相当的高。我们后来代码量和服务数量也不断增加,并且由于业务发展我们还支持了国际化,实现了多机房部署,这个过程是比较顺畅的。

我觉得非常自豪的是,我们刚上线一个月就做了全链路压测,框架层稍作修改就搞定了,显著提升了整体系统稳定性和抗压能力,而这个过程对业务是完全透明的,对业务未来的迭代也是完全透明的。我们在线上也没有出现过任何单点故障造成的雪崩,各种监控和关键日志也是自动的透明的做好,服务注册发现、底层 driver 升级、一些框架 bug 修复等对业务都十分透明,业务只用每次升级到最新版就好了,十分省心。
##版本管理

最后提一个细节:管理框架的各个库版本。

我相信很多开发者都有一种烦恼,就是管理各种分裂的代码版本。一方面由于框架会不断升级,需要不断用 semver 规则升级版本,另一方面业务方又没有动力及时升级到最新版,导致框架各个库的版本事实上出现了分裂。这个事情其实是不应该发生的,就像我们用操作系统,比如大家开发业务需要跑在线上 linux 服务器上,我们会关心 linux kernel 版本么?或者用 Go 开发,我们会总是关心用什么 Go 版本么?一般都不会关心的,这跟开发业务没什么关系。我们关心的是系统提供了哪些跟业务开发相关的接口,只要接口不变且稳定,业务代码就能正常的工作。

这是为什么我们在设计框架的时候会花费很多心力保证接口稳定的原因,我们就是希望框架即操作系统,只有做到这一点,业务才能放心大胆的用框架做业务,真正把业务做到快而不糙。也正因为这一点,我们甚至于不会给框架的各个库打 tag,每次上线都必须全部将框架升级到最新版,彻底的解决了版本分裂的问题。
##未来方向

未来我们还是有很多工作值得去做,比如完善工具链、接入更多的一些公司基础设施等。

我们不确定是否能够开源,大概率是不会开源,因为这个框架并不重要,它与滴滴各种基础设施绑定,服务于滴滴研发,重要的是设计理念和思路,大家可以用类似方法因地制宜的在自己的公司里实践这种设计思想。

今天这个活动就是一个很好的场所,我希望通过这个机会跟大家分享这样的想法,如果大家有兴趣也欢迎跟我交流,我可以帮助大家在公司里实现类似的设计。

原文链接:https://mp.weixin.qq.com/s/vOYhlpRaN-uF0DIrZ-sM-w

轻松构建微服务之服务注册和发现

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

【编者的话】为什么需要服务注册中心? 随着服务数量的扩张,我们需要服务调用方能够自动感知到服务提供方的地址,当我们对服务提供方进行横向扩展的时候,服务调用方能够自动感知到,这就需要服务提供方能够在启动或者关闭的时候自动向注册中心注册,而服务调用方直接询问注册 ...查看全部
【编者的话】为什么需要服务注册中心? 随着服务数量的扩张,我们需要服务调用方能够自动感知到服务提供方的地址,当我们对服务提供方进行横向扩展的时候,服务调用方能够自动感知到,这就需要服务提供方能够在启动或者关闭的时候自动向注册中心注册,而服务调用方直接询问注册中心就可以知道具体的服务提供方的地址列表,服务调用方可以自己根据特定的路由算法做负载均衡, 或者服务调用方根本不需要知道服务提供方的具体地址,统一发给一个代理系统,由这个代理系统转发给对应的服务调用方。所以为了支持服务的弹性扩展,我们需要一个独立系统做服务发现和负载均衡,目前做服务发现有三种代理模式。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#集中代理模式

集中代理模式一般是在服务调用方和服务提供方之间部署一套独立的代理系统来接收调用方的请求,然后配合域名系统和特定的负载均衡策略以及路由算法,转发到对应的服务提供方,这个代理系统一般由硬件F5设备加7层软件负载nginx配合域名系统来使用,这种模式,有集中治理,运维简单,而且和语言栈无关的优点,但是目前这个代理系统存在单点问题,而且配置比较麻烦,由于服务调用者没有之间调用服务提供方,所以网络上存在多一跳的开销,目前国内主要有携程等公司在使用。
1.jpg

#客户端嵌入式代理

目前很多公司用springcloud或者dubbo作为rpc框架的公司多选择这种模式,在客户端嵌入一个程序做服务发现和软件负载均衡,调用方会缓存服务提供方的地址列表,并且自己根据路由算法和负载均衡策略选择服务提供者进行调用,这种模式需要一个第三方的注册中心配合,服务提供者启动的时候将服务注册到注册中心,服务调用方去注册中心获取服务提供者信息,这种模式有无单点问题,性能好的优点,但是由于客户端需要关心负载均衡和维护提供者列表,导致客户端臃肿,目前国内主要有阿里的Dubbo和新浪的Motan。
2.jpg

#主机独立进程代理

这种模式是前面两种模式的一个折中,将这个代理放到主机的一个独立程序上,所有这个主机上的应用共享这个代理,应用一般部署在docker容器中,主机可以是一个物理机也可以是虚拟机。在这个代理上进行路由和负载均衡,这个模式也需要一个模式二中独立的注册中心来辅助代理程序做服务发现,这种模式兼具上面两种模式的优点,但是运维较复杂,目前国内只有唯品会有这种模式的探索
3.jpg

#Service Mesh介绍

边车模式,sidecar,将一个单独的进程用来处理、日志、健康、限流、服务发现等功能,和应用层完全分离,目前如果我们大多lib等软件包的形式和应用集成来实现限流和监控,这样增加了应用依赖,每次lIB包升级都需要将应用重新编译发布,而边车模式下我们可以将逻辑层和控制层分开部署。

边车模式进化后,将这个独立程序集群化,就成了Service Mesh,也就是CNCF所推荐的新一代微服务架构,将这个代理程序下沉为一个基础服务,作为平台开放给应用程序。
4.png

Service Mesh的定义:一作为个轻量级的网络代理专职处理服务和服务间的通讯,对应用程序透明,用来分离或者解耦和分布式系统架构中控制层的上的东西。

类似于网络七层模型中的TCP协议,将底层那些难控制的网络通讯方面的东西(拥塞控制、丢包重传、流量控制)都处理了,而上层的http协议就只用关心应用层的html格式。

演化路径:

1.一开始最原始的两台主机之间进程直接通讯
2.然后分离出网络层来,服务间的远程通信通过底层的网络模型
3.由于两边服务由于接收速度不一致,需要在应用层做流控
4.然后流控模块交给网络层去处理,最后TCP演化成为世界上最成功的网络协议
5.类比分布式架构中,我们在应用层加入了限流、熔断、监控、服务发现等功能
6.然后我们发现这些控制层的功能都可以标准化,我们将这些功能分别做成LIB嵌入到应用中,这样谁需要这个功能只要加入这个LIB就好了
7.最后我们发现这些LIB不能跨编程语言,然后有什么改动就需要重新编译发布服务,不太方便,应该有一个专门的层来干这个,就是sidecar
8.然后sidecar集群就成了Service Mesh,成为了一个基础设施

5.png

目前开源的Service Mesh实现有istio。
#为什么ZooKeeper不适合做服务发现

##首先我们分析下服务发现是满足cap里面的ap还是cp


注册中心提供了两个服务,一个是让服务提供方注册,就是写数据,另一个是让服务调用方查询,就是读数据,当注册中心集群部署后,每个节点都可以对外提供读写服务,每一次写请求都会同步到其他节点,这样才能让其他节点提供的读服务正确,如果节点之间的数据复制出现不一致,那么将导致服务调用方获取到的服务提供者列表中要么出现已经下线的节点,要么少提供了一个正常的节点。提供了下线的节点,服务调用者可以通过重试机制调用其他节点,出现少提供一个正常节点则导致服务提供方的流量不均匀,这些都是可以接受的,何况各个节点最终都会同步成功,也就是数据最终一致性,所以我们希望注册中心是高可用的,最好能满足最终一致性,而ZooKeeper是典型的CP设计,在网络分区情况下不可用,当节点超过半数挂掉不可用,违背了注册中心不能因为自身任何原因破坏服务本身的可连通性。
##另外我们分析下出现网络分区的情况


我们看下下图ZooKeeper三机房5节点部署的情况下,如果机房3和机房1机房2网络出现分区成为孤岛,ZooKeeper和机房3里的其他应用不能和外部通信,ZooKeeper由于联系不是leader将不可用,虽然ZooKeeper整个集群中其他4个节点依然可以对外提供服务,但是ZooKeeper为了保证在脑裂的情况下数据一致性,机房3的节点5将不能进行读写服务,这个时候机房3内的应用A虽然不能调用其他机房的服务B但是可以调用机房3内的服务B,但是由于ZooKeeper不能读写,所以机房内也不能读写,这对应用来说是完全不能接受的,我们有时候为了性能考虑还好主动修改路由策略让应用尽量同机房调用,试想一下如果三个机房都出现网络分区相互成为孤岛,那么整个应用将不可用。

[图片上传失败…(image-269105-1558422945065)]
##ZooKeeper的写服务并不支持水平扩展

ZooKeeper需要和所有的服务保持长连接,而随着服务的频繁发布,以及服务的健康检查,会导致ZooKeeper压力越来越大,而ZooKeeper并不能通过横向扩展节点解决,可以提供的方案是按照业务进行拆分到不同的ZooKeeper集群。
##健康检查

服务提供方是否可用,不能仅仅通过ZooKeeper的session是否存活判断,TCP活性并不能反映服务的真实健康状态,而需要完整的健康体系、CPU、内存、服务是否可用、数据库是否能联通等。

几个注册中心的对比:
B.png


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

数据基础架构公司Segment宣布放弃微服务

回复

hokingyang 发起了问题 • 1 人关注 • 0 个回复 • 1090 次浏览 • 2019-03-13 11:29 • 来自相关话题

用户直接运行代码库源文件( Python 单体应用)会有什么弊端?

回复

mowangmm 发起了问题 • 1 人关注 • 0 个回复 • 1389 次浏览 • 2018-03-14 17:04 • 来自相关话题

SSH框架微服务改进实战

回复

slixf 发起了问题 • 1 人关注 • 0 个回复 • 4284 次浏览 • 2016-11-30 23:50 • 来自相关话题

【小数乱弹】十多年了,有人终于迎来了春天

回复

Dataman数人科技 发起了问题 • 1 人关注 • 0 个回复 • 2598 次浏览 • 2016-07-08 15:55 • 来自相关话题

大家觉得现在的微服务架构最需要什么?

回复

难易 回复了问题 • 17 人关注 • 9 个回复 • 11696 次浏览 • 2015-07-24 10:07 • 来自相关话题

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

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

技术随业务而生,业务载技术而行。 近些年来,伴随数字经济的发展,在众多企业的数字化转型之路上,云原生、DevOps、微服务、服务治理等成为行业内不断被探讨的新话题。人们在理解和接受这些新型概念的同时,也不断地 ...查看全部

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


近些年来,伴随数字经济的发展,在众多企业的数字化转型之路上,云原生、DevOps、微服务、服务治理等成为行业内不断被探讨的新话题。人们在理解和接受这些新型概念的同时,也不断地思考其可能的落地形态。需求是创造发生的原动力,于是一批代表性的开源技术或者框架涌现而出:Kubernetes,Spring Cloud,Service Mesh,Serverless…… 它们炙手可热,大放异彩。然而在具体落地过程中却步履维艰,磕磕绊绊。 


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

  

 

什么是服务治理? 

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

  

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

  

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

  

世界上唯一不变的是变化本身。----By 斯宾塞.约翰逊 

  

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

 

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

  

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

  

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


  

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

     

数字永远不会说谎。 

  

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

  

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

  

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

  

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

  

量化 

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

 

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

  

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

  

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

  

追踪 

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

  

管控 

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

  

规范 

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

  

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

  

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

  

代码集成 

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

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

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

 

Agent探针 

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

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

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

 


流量劫持 

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

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

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


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

  


03 “百家争鸣”,成就未来 

  

竞争成就未来。 

  

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

  

SpringCloud 

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

  

Dubbo 

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

  

gRPC 

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

  

Service Mesh 

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

 

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


微服务化后缓存怎么做

大卫 发表了文章 • 0 个评论 • 212 次浏览 • 2019-06-03 15:41 • 来自相关话题

【编者的话】最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助。 #问题 问题1: 店铺数据的获取,将用户关注的数据放在店铺信息一起返回。 对外提供的接口: ...查看全部
【编者的话】最近接手的代码中遇到几个缓存的问题,存在一些设计原则的问题,这里总结一下,希望可以对你有帮助。
#问题

问题1: 店铺数据的获取,将用户关注的数据放在店铺信息一起返回。

对外提供的接口:
List getPageShop(final Query query,final Boolean cache);

返回的店铺信息:
public class Shop {

public static final long DEFAULT_PRIORITY = 10L;

/**
* 唯一标识
*/
private Long id;
//省略了店铺其他信息
/**
* 用户关注
*/
private ShopAttention attention;
}

当调用方设置cache为true时,因为有缓存的存在,获取不到用户是否关注的数据。

问题2: 统计店铺的被关注数导致的慢SQL,导致数据库cpu飙高,影响到了整个应用。

SQL:
SELECT shop_id, count(user_Id) as attentionNumber
FROM shop_attention
WHERE shop_id IN

#{shopId}

GROUP BY shopId

这两种代码的写法都是基于一个基准。

不同的地方的缓存策略不一样,比如我更新的地方,查找数据时不能缓存,页面展示的查找的地方需要缓存。 既然服务提供方不知道该不该缓存,那就不管了,交给调用方去管理。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

这种假设本身没什么问题,但是忽略了另外一个原则,服务的内聚性。不应该被外部知道的就没必要暴露给外部。

无论是面向过程的C,还是面向对象的语言,都强调内聚性,也就是高内聚,低耦合。单体应用中应当遵循这个原则,微服务同样遵循这个原则。但是在实际过程中,我们发现做到高内聚并不简单。我们必须要时时刻刻审视方法/服务的边界,只有确定好职责边界,才能写出高内聚的代码。
#问题分析

第一个问题,从缓存的角度来看,是忽略了数据的更新频繁性以及数据获取的不同场景。

对于店铺这样一个大的聚合根,本身包含的信息很多,有些数据可能会被频繁更改的,有些则会很少更新的。那么不同的修改频率,是否缓存/缓存策略自然不同,使用同一个参数Boolean cache来控制显然不妥

第二个问题,这种统计类的需求使用SQL统计是一种在数据量比较小的情况下的权宜之计,当数据规模增大后,必须要使用离线计算或者流式计算来解决。它本身是一个慢SQL,所以必须要控制号调用量,这种统计的数据量的时效性应该由服务方控制,不需要暴露给调用方。否则就会出现上述的问题,调用方并不清楚其中的逻辑,不走缓存的话就会使得调用次数增加,QPS的增加会导致慢SQL打垮数据库。
#解法

缓存更新本身就是一个难解的问题,在微服务化后,多个服务就更加复杂了。涉及到跨服务的多级缓存一致性的问题。

所以对大部分的业务,我们可以遵循这样的原则来简单有效处理。

对数据的有效性比较敏感的调用都收敛到服务内部(领域内部应该更合适),不要暴露给调用方。

领域内部做数据的缓存失效控制。

缓存预计算(有些页面的地方不希望首次打开慢)的逻辑也应该放在领域内控制,不要暴露给调用方。

在领域内部控制在不同的地方使用不同的缓存策略,比如更新数据的地方需要获取及时的数据。比如商品的价格,和商品的所属类目更新频次不同,需要有不同的过期时间。

跨服务调用为了减少rpc调用,可以再进行一层缓存。因为这些调用可以接受过期的数据,再进行一层缓存没问题,expired time叠加也没多大影响(expire time在这边主要是影响缓存的命中数)

以上述店铺查询问题改造为例
1.png

扩展:如果后续有case在跨服务的调用时,对数据的过期比较敏感,并且在调用方也做了缓存,那就是跨服务的多级缓存一致性的问题。那就需要服务方告知调用方缓存何时失效,使用消息队列or其他方式来实现。

作者:方丈的寺院
原文:https://fangzhang.blog.csdn.net/article/details/89892575

微服务间的调用和应用内调用有什么区别

阿娇 发表了文章 • 0 个评论 • 193 次浏览 • 2019-06-03 11:19 • 来自相关话题

2019企业IT现状和趋势调研报告:70.7%的企业有云原生相关计划

灵雀云 发表了文章 • 0 个评论 • 155 次浏览 • 2019-06-03 11:00 • 来自相关话题

2019年第一季度,灵雀云发起了“企业IT应用现状和云原生技术落地情况”的调研,通过定向邀请,3个月内共收集了400余份有效调研问卷,这些调研问卷80%以上都来自于国内政府、金融、能源、制造、汽车等传统行业的IT从业者。 发起本次调研 ...查看全部
2019年第一季度,灵雀云发起了“企业IT应用现状和云原生技术落地情况”的调研,通过定向邀请,3个月内共收集了400余份有效调研问卷,这些调研问卷80%以上都来自于国内政府、金融、能源、制造、汽车等传统行业的IT从业者。

发起本次调研的初衷,是我们希望了解当前企业,尤其是传统企业目前IT应用开发现状、以及以DevOps、Kubernetes、微服务等为代表的云原生技术在企业的应用情况,从而勾勒出传统行业IT发展趋势,并对于判断国内用户对云原生相关技术的认知度提供一个有价值的参考。
核心要点解读:
1、 约70%的参与调研者所在企业2019年IT预算有上浮;

2、 24.4%的参与调研者表示公司IT系统基本全靠自研,企业开始自建软件研发团队,主导IT应用的研发;

3、 70.7%的参与调研者所在企业表示在2019年有容器、DevOps和微服务方面的规划;

4、 11.4%的参与调研者所在企业已经试点了具有标杆意义的云原生实践,如精英团队的DevOps实践,小范围非核心应用的微服务拆分改造实践等。


pic2.jpg



本次调研的400多位调研对象中,80%以上来自金融、能源、制造、汽车等传统行业,其中17.3%来自基础架构部门, 22.5%来自运维部门,34.1%来自研发部门,还有约10%的被调研对象为企业的CIO/CTO等高级IT管理者。


pic3.jpg



被调研企业中,服务器规模在100-500台的比例为26.8%,500-1000台的企业占比22%,1000台服务器以上规模的14.6%。



IT系统自研还是外包



pic4.jpg




在数字化转型的背景下,传统外包的做法在被逐渐改变。在此次调查中,70.7%的参与调研者表示目前IT系统是自研外包兼而有之,其中核心业务系统以自己开发为主,24.4%的参与调研者表示公司IT系统基本全靠自研,只有4.9%的参与调研者选择了纯外包选项。这表明,企业开始不再将大部分业务系统,尤其是核心业务需求开发外包,开始自建软件研发团队,主导IT应用的研发。只有企业自己主导IT研发,才能够打造IT核心竞争力。

软件能力成为企业的核心竞争力,这恰好是数字化转型的本质之一。何谓成功的数字化转型?灵雀云认为,有三大衡量标志:IT部门由成本中心转为收入中心;企业自己主导IT产品的研发;改进工具、流程、文化来提高交付速度和质量。最终,实现客户满意度的提升、打造差异化竞争优势、加速产品上市。



IT系统更新频率




PIC5.jpg



在IT系统更新频率方面,每月都要更新、升级的比例达到了51.2%的高占比。同时,每3-6个月更新一次的比例达22%。每个传统领域,都受到了来自Fintech金融科技、车联网、物联网、新零售等新技术驱动的创新业务的挑战,传统企业只有借助IT手段才能实现持续发展,在速度和规模上保持竞争力。



IT系统和研发团队TOP 3挑战



pic6.jpg




本次参与调研的企业以中大型企业为主,其中研发团队规模达到100人以上的比例高达44.3%,20-100人规模的占32.4%。

PIC7.jpg




今天,许多企业都经过了大量IT建设,从分散到集中,造成IT系统越来越复杂,信息孤岛林立,架构臃肿等问题突出。调研中企业IT系统支撑所面临的压力位列前三的挑战分别是:系统复杂性越来越高(65.9%);应用交付压力大,交付速度无法满足业务需求(61.4%);运维管理复杂度提升,IT部门很难构建一支全功能团队(53.7%)。


PIC8.jpg



同时,研发团队所面临的挑战前三甲分别是:部署和运维复杂,运维成本高(74.6%);研发、测试、运维等角色之间相互孤立(62.3%);升级和变更流程复杂,IT服务和应用交付周期长(45.7%)。此外,比较突出的挑战还有,工具链无法完整集成,工具使用困难(32.3%),单体应用过于庞大,迭代效率低下(20.4%)。


pic9.jpg



上述结果充分表明,面对高度创新、快速变化和充满不确定性的新型业务需求,传统开发模式和IT架构已经成为掣肘。70.7%的参与调研企业表示2019年有容器、DevOps和微服务方面的规划和实施计划。

只有朝着持续交付、敏捷部署、快速迭代,通过敏捷IT赋予业务足够的敏捷,才能够满足不断变化的业务需求,重塑自身的生产力,形成竞争优势,带来更好的用户体验,这最终落到以Kubernetes/容器、DevOps和微服务为核心的云原生技术的落地上。云原生架构和理念与数字化转型一脉相承,帮助企业更加顺畅地实施数字化转型。



业务上云需求最强烈,开源、数字化转型受追捧



PIC10.jpg




在企业最关注的新兴技术趋势方面,云计算占比82.9%,企业将业务上云,提升IT资源效率作为首要关注对象。大数据和人工智能紧随其后,占比分别为73.2%和46.3%。其中开源解决方案在调研对象中的关注程度达到24.4%。

当前开源技术正在进入快速发展阶段,向着企业应用的方方面面深入。开源及开源社区不断将新的工具、方法和最佳实践用于云原生的实际业务用例,解决云原生用户的关键问题。借助许多开源解决方案,云原生部署的复杂性和难度也在得以降低。

此外,数字化转型的关注度为33.6%。如今每位IT从业者言必称数字化转型,IT能力也直接指向助力数字化转型。CIO和其他IT管理者已将企业的数字化计划置于新的高度,希望通过数字化来改变企业的商业和业务模式,数字化业务将从初步试验走向大规模应用。伴随企业数字化业务的不断成熟,预计未来几年,数字化转型将进入爆发阶段。



传统企业2019年IT预算稳中有升


PIC11.jpg





本次调研中,被调研企业今年IT工作的重点包括业务上云(56.1%),云原生、大数据、人工智能等新技术采用(53.7%),打造数字化团队,引领企业的数字化创新(43.9%),选择传统业务应用的比例不足20%。越来越多的企业将工作负载放在云端,将正在开发的应用或服务托管在云平台上,云市场不断增长。


PIC12.jpg



在IT预算方面,比客观经济形势略显乐观,和2018年IT预算相比,接近70%参与调研企业2019年的IT预算略有上浮,其中增长5%以内的企业占比37.5%,增长5-10%的企业占比21.2%,增长10%以上的企业达到12.7%。

此外,调研结果显示,数字化转型是一项需要通盘考虑的工作,需要项目管理部门、技术管理部门、开发部门、运维部门共同参与,制定统一的数字化转型方案和决策并推进。有些参与调研的企业特别强调2018年已经在全公司范围内试点了具有标杆意义的云原生实践,如精英团队的DevOps实践,小范围非核心应用的微服务拆分改造实践等,并且这些都将在2019年进行大范围推广。

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

大卫 发表了文章 • 0 个评论 • 242 次浏览 • 2019-05-30 12:58 • 来自相关话题

#前言 今天和大家聊一下在采用Spring Cloud进行微服务架构设计时,微服务之间调用时异常处理机制应该如何设计的问题。我们知道在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向内部和外部提供相应的功能服务接口。面向外部提供的服务 ...查看全部
#前言

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

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

这里需要说明的是,在基于SpringCloud的微服务架构中,所有服务都是通过如consul或eureka这样的服务中间件来实现的服务注册与发现后来进行服务调用的,只是面向外部的服务接口会通过网关服务进行暴露,面向内部的服务接口则在服务网关进行屏蔽,避免直接暴露给公网。而内部微服务间的调用还是可以直接通过consul或eureka进行服务发现调用,这二者并不冲突,只是外部客户端是通过调用服务网关,服务网关通过consul再具体路由到对应的微服务接口,而内部微服务则是直接通过consul或者eureka发现服务后直接进行调用。如果你想和更多微服务技术专家交流,可以加我微信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调用微服务时就可以直接捕获到异常对象,从而实现向本地一样处理远程服务返回的异常对象了。

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

gRPC 使用 protobuf 构建微服务

JetLee 发表了文章 • 0 个评论 • 186 次浏览 • 2019-05-30 09:24 • 来自相关话题

#微服务架构 ##单一的代码库 以前使用 Laravel 做 Web 项目时,是根据 MVC 去划分目录结构的,即 Controller 层处理业务逻辑,Model 层处理数据库的 CURD,View 层处理数据渲染与页面交互。以及 ...查看全部
#微服务架构

##单一的代码库

以前使用 Laravel 做 Web 项目时,是根据 MVC 去划分目录结构的,即 Controller 层处理业务逻辑,Model 层处理数据库的 CURD,View 层处理数据渲染与页面交互。以及 MVP、MVVM 都是将整个项目的代码是集中在一个代码库中,进行业务处理。这种单一聚合代码的方式在前期实现业务的速度很快,但在后期会暴露很多问题:

* 开发与维护困难:随着业务复杂度的增加,代码的耦合度往往会变高,多个模块相互耦合后不易横向扩展
* 效率和可靠性低:过大的代码量将降低响应速度,应用潜在的安全问题也会累积

##拆分的代码库

微服务是一种软件架构,它将一个大且聚合的业务项目拆解为多个小且独立的业务模块,模块即服务,各服务间使用高效的协议(protobuf、JSON 等)相互调用即是 RPC。这种拆分代码库的方式有以下特点:

* 每个服务应作为小规模的、独立的业务模块在运行,类似 Unix 的 Do one thing and do it well
* 每个服务应在进行自动化测试和(分布式)部署时,不影响其他服务
每个服务内部进行细致的错误检查和处理,提高了健壮性

##二者对比

本质上,二者只是聚合与拆分代码的方式不同。
1.png

参考:微服务架构的优势与不足
#构建微服务

##UserInfoService 微服务

接下来创建一个处理用户信息的微服务:UserInfoService,客户端通过 name 向服务端查询用户的年龄、职位等详细信息,需先安装 gRPC 与 protoc 编译器:
go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go

目录结构
├── proto
│ ├── user.proto // 定义客户端请求、服务端响应的数据格式
│ └── user.pb.go // protoc 为 gRPC 生成的读写数据的函数
├── server.go // 实现微服务的服务端
└── client.go // 调用微服务的客户端

##调用流程

2.png

##Protobuf 协议

每个微服务有自己独立的代码库,各自之间在通信时需要高效的协议,要遵循一定的数据结构来解析和编码要传输的数据,在微服务中常使用 protobuf 来定义。

Protobuf(protocal buffers)是谷歌推出的一种二进制数据编码格式,相比 XML 和 JSON 的文本数据编码格式更有优势:

* 读写更快、文件体积更小
* 它没有 XML 的标签名或 JSON 的字段名,更为轻量,更多参考

3.png

语言中立

只需定义一份 .proto 文件,即可使用各语言对应的 protobuf 编译器对其编译,生成的文件中有对 message 编码、解码的函数。

对于 JSON

* 在 PHP 中需使用 json_encode() 和 json_decode() 去编解码,在 Golang 中需使用 json 标准库的 Marshal() 和 Unmarshal() … 每次解析和编码比较繁琐
* 优点:可读性好、开发成本低
* 缺点:相比 protobuf 的读写速度更慢、存储空间更多

对于 Protobuf

.proto 可生成 .php 或 .pb.go … 在项目中可直接引用该文件中编译器生成的编码、解码函数
* 优点:高效轻量、一处定义多处使用
* 缺点:可读性差、开发成本高

定义微服务的 user.proto 文件
syntax = "proto3";	// 指定语法格式,注意 proto3 不再支持 proto2 的 required 和 optional
package proto; // 指定生成的 user.pb.go 的包名,防止命名冲突


// service 定义开放调用的服务,即 UserInfoService 微服务
service UserInfoService {
// rpc 定义服务内的 GetUserInfo 远程调用
rpc GetUserInfo (UserRequest) returns (UserResponse) {
}
}


// message 对应生成代码的 struct
// 定义客户端请求的数据格式
message UserRequest {
// [修饰符] 类型 字段名 = 标识符;
string name = 1;
}


// 定义服务端响应的数据格式
message UserResponse {
int32 id = 1;
string name = 2;
int32 age = 3;
repeated string title = 4; // repeated 修饰符表示字段是可变数组,即 slice 类型
}

编译 user.proto 文件
# protoc 编译器的 grpc 插件会处理 service 字段定义的 UserInfoService
# 使 service 能编码、解码 message
$ protoc -I . --go_out=plugins=grpc:. ./user.proto

生成 user.pb.go
package proto

import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)

// 请求结构
type UserRequest struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}

// 为字段自动生成的 Getter
func (m *UserRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}

// 响应结构
type UserResponse struct {
Id int32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
Age int32 `protobuf:"varint,3,opt,name=age" json:"age,omitempty"`
Title []string `protobuf:"bytes,4,rep,name=title" json:"title,omitempty"`
}
// ...

// 客户端需实现的接口
type UserInfoServiceClient interface {
GetUserInfo(ctx context.Context, in [i]UserRequest, opts ...grpc.CallOption) ([/i]UserResponse, error)
}


// 服务端需实现的接口
type UserInfoServiceServer interface {
GetUserInfo(context.Context, [i]UserRequest) ([/i]UserResponse, error)
}

// 将微服务注册到 grpc
func RegisterUserInfoServiceServer(s *grpc.Server, srv UserInfoServiceServer) {
s.RegisterService(&_UserInfoService_serviceDesc, srv)
}
// 处理请求
func _UserInfoService_GetUserInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {...}

##服务端实现微服务

实现流程
4.png

代码参考
package main
import (...)

// 定义服务端实现约定的接口
type UserInfoService struct{}
var u = UserInfoService{}

// 实现 interface
func (s [i]UserInfoService) GetUserInfo(ctx context.Context, req [/i]pb.UserRequest) (resp *pb.UserResponse, err error) {
name := req.Name

// 模拟在数据库中查找用户信息
// ...
if name == "wuYin" {
resp = &pb.UserResponse{
Id: 233,
Name: name,
Age: 20,
Title: []string{"Gopher", "PHPer"}, // repeated 字段是 slice 类型
}
}
err = nil
return
}

func main() {
port := ":2333"
l, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("listen error: %v\n", err)
}
fmt.Printf("listen %s\n", port)
s := grpc.NewServer()

// 将 UserInfoService 注册到 gRPC
// 注意第二个参数 UserInfoServiceServer 是接口类型的变量
// 需要取地址传参
pb.RegisterUserInfoServiceServer(s, &u)
s.Serve(l)
}

运行监听:
5.png

##客户端调用

实现流程
6.png

代码参考
package main
import (...)

func main() {
conn, err := grpc.Dial(":2333", grpc.WithInsecure())
if err != nil {
log.Fatalf("dial error: %v\n", err)
}
defer conn.Close()

// 实例化 UserInfoService 微服务的客户端
client := pb.NewUserInfoServiceClient(conn)

// 调用服务
req := new(pb.UserRequest)
req.Name = "wuYin"
resp, err := client.GetUserInfo(context.Background(), req)
if err != nil {
log.Fatalf("resp error: %v\n", err)
}

fmt.Printf("Recevied: %v\n", resp)
}

运行调用成功:
7.png

#总结

在上边 UserInfoService 微服务的实现过程中,会发现每个微服务都需要自己管理服务端监听端口,客户端连接后调用,当有很多个微服务时端口的管理会比较麻烦,相比 gRPC,go-micro 实现了服务发现(Service Discovery)来方便的管理微服务,下节将随服务的 Docker 化一起学习。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

原文链接:https://wuyin.io/2018/05/02/protobuf-with-grpc-in-golang/

工业微服务实现工业APP高效开发和运行

玻璃樽 发表了文章 • 0 个评论 • 174 次浏览 • 2019-05-30 07:38 • 来自相关话题

【编者的话】工业微服务架构为工业互联网平台的知识转化和复用提供了最佳技术手段,算法、模型、知识等模块化组件能够以“搭积木”的方式被调用和编排,实现低门槛、高效率的工业App开发。 微服务最早由Martin Fowler与James L ...查看全部
【编者的话】工业微服务架构为工业互联网平台的知识转化和复用提供了最佳技术手段,算法、模型、知识等模块化组件能够以“搭积木”的方式被调用和编排,实现低门槛、高效率的工业App开发。

微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
#什么是工业微服务
工业微服务是工业互联网平台的载体,是以单一功能组件为基础,通过模块化组合方式实现“松耦合”应用开发的软件架构。一个微服务就是一个面向单一功能、能够独立部署的小型应用,将多个不同功能、相互隔离的微服务按需组合在一起并通过API集实现相互通信,就构成了一个功能完整的大型应用系统。以产品生产为例,就可将其拆解为供应链管理、设备运行状态可视化、生产排程、产线数据分析、操作记录等多个微服务功能模块。

在工业互联网领域,由于工业知识繁杂、工业应用复杂程度高等问题,业内人士普遍认为,使用微服务架构将成为开发工业APP的主流方式。国外主流的工业互联网平台,如西门子的Mindsphere、施耐德Eco Struxure等,都通过云平台支持工业微服务组件的开发、部署和管理,从而达到简化工业APP开发的目的。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#工业微服务架构和传统开发模式区别
先来看看传统的web开发方式,一般被称为Monolithic(单体式开发)。所有的功能打包在一个 WAR包里,基本没有外部依赖(除了容器),部署在一个JEE容器(Tomcat,JBoss,WebLogic)里,包含了 DO/DAO,Service,UI等所有逻辑。
1.png

单体架构(Monolithic)优缺点:
2.png

微服务架构与单体架构相比较,微服务架构恰恰弥补了单体架构的不足,通过有效的拆分应用,实现敏捷开发和部署:

  1. 由多个独立的微服务共同组成系统
  2. 微服务单独部署,运行在自己的进程中
  3. 每个微服务为独立的业务开发
  4. 分布式管理
  5. 非常强调隔离性

3.png

关于微服务的一个形象表达:
4.jpg


* X轴:运行多个负载均衡器之后的运行实例
* Y轴:将应用进一步分解为微服务(分库)
* Z轴:大数据量时,将服务分区(分表)

#工业微服务架构的特点
5.png

之所以主流的工业互联网平台都将微服务架构作为开发工业APP的主流方式,是因为微服务架构与传统的架构相比,具备两个显著特点:
##工业微服务开发和维护具有高度灵活性
每个微服务可以由不同团队运用不同语言和工具进行开发和维护,任何修改、升级都不会对应用的其他部分功能产生影响;而传统的统一整体式框架下对软件的任何修改都有可能对整个应用产生意料之外的影响。
##工业微服务运行去中心化分布式执行
不同微服务能够分布式并行执行,应用资源占用率相对较小,且微服务间的数据和资源相互物理隔离,单个服务的故障只会导致单个功能的受损而不会造成整个应用的崩溃。
#微服务支撑工业互联网平台颠覆创新
##工业微服务颠覆传统工业软件研发方式
在企业里,CAD、CAE、DCS、MES、ERP、SCM等传统工业应用软件往往是面向基础的流程或服务进行设计和研发,并在部署阶段根据用户实际情况进行调整,整个软件研发的成本投入较大、研发周期较长,且不能灵活地响应用户个性化需求。而在工业互联网平台中,则可采用工业微服务的方式将上述软件拆解成独立的功能模块,实现对原有生产体系的解构,随后在平台中构建起富含各类功能与服务的微服务组件池,并按照实际需求来调用相应的微服务组件,进行高效率和个性化的面向用户的工业App研发,整个软件研发的技术门槛和投入成本大大降低。原来需要专业团队和雄厚资金支持的精英化软件研发开始向大众化研发转变。
##工业微服务打破工业知识封闭传承体系
过去,工业领域中很多经验知识都停留在老师傅、老专家的脑子里,由于个人精力和地域空间的限制,这些经验知识通常只能在很小的范围里发挥作用,而且还存在易出错、易流失、难推广、难传承等问题。如今,当这些老师傅、老专家将自己的经验知识用软件代码的方式固化下来,转化为平台中的工业微服务之后,由于平台所具备的积累沉淀和开放共享特性,这些经验知识就变成了整个企业、整个行业的宝贵财富,能够被更多的人分享学习和使用,创造出更多的价值。同时,新的专业技术人员还能够在充分消化吸收原有知识的基础上实现进一步提升和创新,推动整个工业知识体系的传递延续和迭代更新。
##工业微服务创造全新平台开放价值生态
随着工业互联网平台中微服务组件池的构建和行业经验知识的持续积累,整个平台既能够为广大第三方开发者提供众多低门槛、易操作、高效率的开发支持手段,形成以工业App开发为核心的平台创新生态,也能够为制造业用户提供以工业微服务为基础的定制化、高可靠、可扩展工业App或解决方案,形成以价值挖掘提升为核心的平台应用生态。最终,构建出以工业互联网平台为桥梁、以工业微服务为载体的相互促进、双向迭代生态体系。
#工业微服务在工业互联网平台的作用
工业微服务实现机理模型算法的模块化、软件化,支撑工业互联网平台中的工业App开发运行。在工业互联网平台中,工业微服务正发挥着承上启下的关键作用。
##独立调试、运行和升级,提升易用性和可维护性
基于不同行业、不同领域经验知识所提炼出来的各类原始机理算法模型通常缺少对外调用的接口,也往往难以进行独立的调试、运行和升级,需要用工业微服务的方式将这些机理算法模型集成起来,封装成可独立调试运行的单一功能或服务模块,提升易用性和可维护性。
##满足工业APP快速运维、持续迭代和个性化定制的需要
在工业互联网平台中基于工业微服务模块进行工业App开发,既能够借助工业微服务并行开发、分布运行的特点,有效发挥平台海量开发者接入、资源弹性配置、云化部署运行等优势,又能够利用工业微服务独立隔离、灵活调用的特点,克服工业App所面临的快速运维、持续迭代、个性化定制等问题。
##无需专业知识,平台调用工业微服务开发工业APP
工业互联网平台发展的核心目标是通过行业经验知识的积累沉淀和复用推广来带动产业整体水平的提升,并打造繁荣创新的开放价值生态。而工业微服务能够将专业知识和IT技术融合起来,变成不需要关心实现细节的“黑盒”,开发者甚至不需要任何专业知识,就可通过调用平台中各类工业微服务的方式开发出解决行业问题的工业App。
##工业微服务具有通用化共享能力,便于复制和应用推广
在此基础上,平台将原来处于企业内部的封闭性专业能力转化为面向行业和社会的通用化共享能力,实现在工业微服务能力复制和应用推广,从而成为服务行业、服务区域的发动机和助推器。
#结语
工业微服务本质是经验知识的软件化和工具化,借助专业化的工具打造通用化的平台。工业微服务架构为工业互联网平台的知识转化和复用提供了最佳技术手段,算法、模型、知识等模块化组件能够以“搭积木”的方式被调用和编排,实现低门槛、高效率的工业App开发,驱动了工业软件开发方式的变革,促进了平台创新生态的形成,工业微服务能力构建已经成为当前工业互联网平台发展的首要任务。

原文链接:https://mp.weixin.qq.com/s/9bSAyBBdl_m534p8Um-59w

微服务架构下的分布式事务基础入门

翔宇 发表了文章 • 0 个评论 • 204 次浏览 • 2019-05-29 22:51 • 来自相关话题

众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据 ...查看全部
众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据库的事务需求,这种事务即为“分布式事务”。那么在目前数据库不支持跨库事务的情况下,我们应该如何实现分布式事务呢?本文首先会为大家梳理分布式事务的基本概念和理论基础,然后介绍几种目前常用的分布式事务解决方案。废话不多说,那就开始吧~
#什么是事务?
事务由一组操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作,要么全都正确执行,要么全都不要执行。
#事务的四大特性 ACID
说到事务,就不得不提一下事务著名的四大特性。

* 原子性,原子性要求,事务是一个不可分割的执行单元,事务中的所有操作要么全都执行,要么全都不执行。
* 一致性,一致性要求,事务在开始前和结束后,数据库的完整性约束没有被破坏。
* 隔离性,事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。
* 持久性,持久性要求,一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。

注意:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。
#事务的隔离级别
这里扩展一下,对事务的隔离性做一个详细的解释。

在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间不会受到任何干扰。这确实能够完全保证数据的安全性,但在实际业务系统中,这种方式性能不高。因此,数据库定义了四种隔离级别,隔离级别和数据库的性能是呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
##事务并发执行会出现的问题
我们先来看一下在不同的隔离级别下,数据库可能会出现的问题:

  1. 更新丢失,当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。 当数据库没有加任何锁操作的情况下会发生。
  2. 脏读,一个事务读到另一个尚未提交的事务中的数据。 该数据可能会被回滚从而失效。 如果第一个事务拿着失效的数据去处理那就发生错误了。
  3. 不可重复读,不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:

* 虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录。
* 幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化。

不可重复读与脏读的区别?:

脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。
##数据库的四种隔离级别
数据库一共有如下四种隔离级别:

  1. Read uncommitted 读未提交,在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。 因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。
  2. Read committed 读提交,在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。
  3. Repeatable read 重复读 ,在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。
  4. Serializable 序列化,该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
#什么是分布式事务?
到此为止,所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立的数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现支持跨数据库的事务支持,这也就是大家常说的“分布式事务”。

这里举一个分布式事务的典型例子——用户下单过程。

当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统等。整个下单的过程如下:

  1. 用户通过商品系统浏览商品,他看中了某一项商品,便点击下单
  2. 此时订单系统会生成一条订单
  3. 订单创建成功后,支付系统提供支付功能
  4. 当支付完成后,由积分系统为该用户增加积分

上述步骤2、3、4需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。
#CAP理论
CAP理论说的是:在一个分布式系统中,最多只能满足C、A、P中的两个需求。

CAP的含义:

C:Consistency 一致性
同一数据的多个副本是否实时相同。

A:Availability 可用性
可用性:一定时间内 & 系统返回一个明确的结果 则称为该系统可用。

P:Partition tolerance 分区容错性
将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。

CAP理论告诉我们,在分布式系统中,C、A、P三个条件中我们最多只能选择两个。那么问题来了,究竟选择哪两个条件较为合适呢?

对于一个业务系统来说,可用性和分区容错性是必须要满足的两个条件,并且这两者是相辅相成的。业务系统之所以使用分布式系统,主要原因有两个:

* 提升整体性能,当业务量猛增,单个服务器已经无法满足我们的业务需求的时候,就需要使用分布式系统,使用多个节点提供相同的功能,从而整体上提升系统的性能,这就是使用分布式系统的第一个原因。
* 实现分区容错性,单一节点 或 多个节点处于相同的网络环境下,那么会存在一定的风险,万一该机房断电、该地区发生自然灾害,那么业务系统就全面瘫痪了。为了防止这一问题,采用分布式系统,将多个子系统分布在不同的地域、不同的机房中,从而保证系统高可用性。

这说明分区容错性是分布式系统的根本,如果分区容错性不能满足,那使用分布式系统将失去意义。

此外,可用性对业务系统也尤为重要。在大谈用户体验的今天,如果业务系统时常出现“系统异常”、响应时间过长等情况,这使得用户对系统的好感度大打折扣,在互联网行业竞争激烈的今天,相同领域的竞争者不甚枚举,系统的间歇性不可用会立马导致用户流向竞争对手。因此,我们只能通过牺牲一致性来换取系统的可用性和分区容错性。这也就是下面要介绍的BASE理论。
#BASE理论
CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性。下面来介绍下BASE理论。

* BA:Basic Available 基本可用

* “一定时间”可以适当延长,当举行大促时,响应时间可以适当延长
* 给部分用户返回一个降级页面,给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。
* 整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:

* S:Soft State:柔性状态,同一数据的不同副本的状态,可以不需要实时一致。
* E:Eventual Consisstency:最终一致性,同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。

#酸碱平衡
ACID能够保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可。但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。因此要根据具体业务场景,在ACID和BASE之间寻求平衡。
#分布式事务协议
下面介绍几种实现分布式事务的协议。
##两阶段提交协议 2PC
分布式系统的一个难点是如何保证架构下多个节点在进行事务性操作的时候保持一致性。为实现这个目的,二阶段提交算法的成立基于以下假设:

* 该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。
* 所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失。
* 所有节点不会永久性损坏,即使损坏后仍然可以恢复。

第一阶段(投票阶段):

  1. 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
  2. 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
  3. 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个"同意"消息;如果参与者节点的事务操作实际执行失败,则它返回一个"中止"消息。

第二阶段(提交执行阶段):

当协调者节点从所有参与者节点获得的相应消息都为"同意"时:

  1. 协调者节点向所有参与者节点发出"正式提交(commit)"的请求。
  2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送"完成"消息。
  4. 协调者节点受到所有参与者节点反馈的"完成"消息后,完成事务。

如果任一参与者节点在第一阶段返回的响应消息为"中止",或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

  1. 协调者节点向所有参与者节点发出"回滚操作(rollback)"的请求。
  2. 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送"回滚完成"消息。
  4. 协调者节点受到所有参与者节点反馈的"回滚完成"消息后,取消事务。

不管最后结果如何,第二阶段都会结束当前事务。

二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:

  1. 执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  2. 参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。(没有多少容错机制)
  3. 协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。(这个可以依赖后面要讲的Paxos协议实现HA)
  4. 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

为此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三阶段提交协议(3PC)。
##三阶段提交协议 3PC
与两阶段提交不同的是,三阶段提交有两个改动点。

* 引入超时机制。同时在协调者和参与者中都引入超时机制。
* 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  1. 事务询问,协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  2. 响应反馈,参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

  1. 发送预提交请求,协调者向参与者发送PreCommit请求,并进入Prepared阶段。
  2. 事务预提交,参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
  3. 响应反馈 ,如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

  1. 发送中断请求,协调者向所有参与者发送abort请求。
  2. 中断事务,参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交:

  1. 发送提交请求,协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
  2. 事务提交,参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
  3. 响应反馈,事务提交完之后,向协调者发送Ack响应。
  4. 完成事务,协调者接收到所有参与者的ack响应之后,完成事务。

中断事务:

协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

  1. 发送中断请求,协调者向所有参与者发送abort请求
  2. 事务回滚,参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
  3. 反馈结果,参与者完成事务回滚之后,向协调者发送ACK消息
  4. 中断事务,协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

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

走进微服务的世界

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

#什么是微服务? 我们首先给出微服务的定义,然后再对该定义给出详细的解释。 微服务就是一些可独立运行、可协同工作的小的服务。 从概念中我们可以提取三个关键词:可独立运行、可协同工作、小。这三个词高 ...查看全部
#什么是微服务?
我们首先给出微服务的定义,然后再对该定义给出详细的解释。

微服务就是一些可独立运行、可协同工作的小的服务。

从概念中我们可以提取三个关键词:可独立运行、可协同工作、小。这三个词高度概括了微服务的核心特性。下面我们就对这三个词作详细解释。

* 可独立运行,微服务是一个个可以独立开发、独立部署、独立运行的系统或者进程。
* 可协同工作,采用了微服务架构后,整个系统被拆分成多个微服务,这些服务之间往往不是完全独立的,在业务上存在一定的耦合,即一个服务可能需要使用另一个服务所提供的功能。这就是所谓的“可协同工作”。与单服务应用不同的是,多个微服务之间的调用时通过RPC通信来实现,而非单服务的本地调用,所以通信的成本相对要高一些,但带来的好处也是可观的。
* 小而美,微服务的思想是,将一个拥有复杂功能的庞大系统,按照业务功能,拆分成多个相互独立的子系统,这些子系统则被称为“微服务”。每个微服务只承担某一项职责,从而相对于单服务应用来说,微服务的体积是“小”的。小也就意味着每个服务承担的职责变少,根据单一职责原则,我们在系统设计时,要尽量使得每一项服务只承担一项职责,从而实现系统的“高内聚”。

#微服务的优点
##易于扩展
在单服务应用中,如果目前性能到达瓶颈,无法支撑目前的业务量,此时一般采用集群模式,即增加服务器集群的节点,并将这个单服务应用“复制”到所有的节点上,从而提升整体性能。然而这种扩展的粒度是比较粗糙的。如果只是系统中某一小部分存在性能问题,在单服务应用中,也要将整个应用进行扩展,这种方式简单粗暴,无法对症下药。而当我们使用了微服务架构后,如果某一项服务的性能到达瓶颈,那么我们只需要增加该服务的节点数即可,其他服务无需变化。这种扩展更加具有针对性,能够充分利用计算机硬件/软件资源。而且只扩展单个服务影响的范围较小,从而系统出错的概率也就越低。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
##部署简单
对于单服务应用而言,所有代码均在一个项目中,从而导致任何微小的改变都需要将整个项目打包、发布、部署,而这一系列操作的代价是高昂的。长此以往,团队为了降低发布的频率,会使得每次发布都伴随着大量的修改,修改越多也就意味着出错的概率也越大。

当我们采用微服务架构以后,每个服务只承担少数职责,从而每次只需要发布发生修改的系统,其他系统依然能够正常运行,波及范围较小。此外,相对于单服务应用而言,每个微服务系统修改的代码相对较少,从而部署后出现错误的概率也相对较低。
##技术异构性
对于单服务应用而言,一个系统的所有模块均整合在一个项目中,所以这些模块只能选择相同的技术。但有些时候,单一技术没办法满足不同的业务需求。如对于项目的算法团队而言,函数试编程语言可能更适合算法的开发,而对于业务开发团队而言,类似于Java的强类型语言具有更高的稳定性。然而在单服务应用中只能互相权衡,选择同一种语言,而当我们使用微服务结构后,这个问题就能够引刃而解。我们将一个完整的系统拆分成了多个独立的服务,从而每个服务都可以根据各自不同的特点,选择最为合适的技术体系。

当然,并不是所有的微服务系统都具备技术异构性,要实现技术异构性,必须保证所有服务都提供通用接口。我们知道,在微服务系统中,服务之间采用RPC接口通信,而实现RPC通信的方式有很多。有一些RPC通信方式与语言强耦合,如Java的RMI技术,它就要求通信的双方都必须采用Java语言开发。当然,也有一些RPC通信方式与语言无关,如基于HTTP协议的REST。这种通信方式对通信双方所采用的语言没有做任何限制,只要通信过程中传输的数据遵循REST规范即可。当然,与语言无关也就意味着通信双方没有类型检查,从而会提高出错的概率。所以,究竟选择与语言无关的RPC通信方式,还是选择与语言强耦合的RPC通信方式,需要我们根据实际的业务场景合理地分析。

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

微服务中集成分布式配置中心 Apollo

大卫 发表了文章 • 0 个评论 • 234 次浏览 • 2019-05-29 12:56 • 来自相关话题

#背景 随着业务的发展、微服务架构的升级,服务的数量、程序的配置日益增多(各种微服务、各种服务器地址、各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、 ...查看全部
#背景
随着业务的发展、微服务架构的升级,服务的数量、程序的配置日益增多(各种微服务、各种服务器地址、各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制。分布式环境下,这些配置更加复杂。

因此,我们需要配置中心来统一管理配置!把业务开发者从复杂以及繁琐的配置中解脱出来,只需专注于业务代码本身,从而能够显著提升开发以及运维效率。同时将配置和发布包解藕也进一步提升发布的成功率,并为运维的细力度管控、应急处理等提供强有力的支持。

在之前的文章中,我们介绍过 Spring Cloud 中的分布式配置中心组件:Spring Cloud Config。本文将会介绍功能更为强大的 Apollo。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态
#分布式配置中心
在一个分布式环境中,同类型的服务往往会部署很多实例。这些实例使用了一些配置,为了更好地维护这些配置就产生了配置管理服务。通过这个服务可以轻松地管理成千上百个服务实例的配置问题。配置中心的特点:

* 配置的增删改查;
* 不同环境配置隔离(开发、测试、预发布、灰度/线上);
* 高性能、高可用性;
* 请求量多、高并发;
* 读多写少;

现有的配置中心组件有:Spring Cloud Config、Apollo、Disconf、Diamond 等等,这些组件在功能上有或多或少的差异,但是都具有基本的配置中心的功能。
#Apollo 简介
Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。目前的有超过 14k 的 star,使用广泛。Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo。
1.jpg

首先用户在配置中心对配置进行修改并发布;配置中心通知Apollo客户端有配置更新;Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用。

Apollo 支持4个维度管理 Key-Value 格式的配置:

* application(应用):实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置;每个应用都需要有唯一的身份标识 – appId,应用身份是跟着代码走的,所以需要在代码中配置。
* environment(环境):配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置。
* cluster(集群):一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。对不同的cluster,同一个配置可以有不一样的值,如ZooKeeper地址。
* namespace(命名空间):一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等;应用可以直接读取到公共组件的配置namespace,如DAL,RPC等;应用也可以通过继承公共组件的配置namespace来对公共组件的配置做调整,如DAL的初始数据库连接数。

我们在集成 Apollo 时,可以根据需要创建相应的维度。
#快速入门
下面我们搭建一个基于 Spring Boot 的微服务,集成 Apollo。
##启动服务端
Apollo配置中心包括:Config Service、Admin Service 和 Portal。

* Config Service:提供配置获取接口、配置推送接口,服务于Apollo客户端;
* Admin Service:提供配置管理接口、配置修改发布接口,服务于管理界面Portal;
* Portal:配置管理界面,通过MetaServer获取AdminService的服务列表,并使用客户端软负载SLB方式调用AdminService。

官网准备好了一个Quick Start安装包,大家只需要下载到本地,就可以直接使用,免去了编译、打包过程。也可以自行编译,较为繁琐。

Apollo服务端共需要两个数据库:ApolloPortalDB和ApolloConfigDB。创建的语句见安装包,创建好之后需要配置启动的脚本,即 demo.sh 脚本:
#apollo config db info
apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8
apollo_config_db_username=用户名
apollo_config_db_password=密码(如果没有密码,留空即可)

# apollo portal db info
apollo_portal_db_url=jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8
apollo_portal_db_username=用户名
apollo_portal_db_password=密码(如果没有密码,留空即可)

脚本会在本地启动3个服务,分别使用8070, 8080, 8090端口,请确保这3个端口当前没有被使用。执行:
./demo.sh start

看到输出如下的日志信息:
==== starting service ====
Service logging file is ./service/apollo-service.log
Started [10768]
Waiting for config service startup.......
Config service started. You may visit http://localhost:8080 for service status now!
Waiting for admin service startup....
Admin service started
==== starting portal ====
Portal logging file is ./portal/apollo-portal.log
Started [10846]
Waiting for portal startup......
Portal started. You can visit http://localhost:8070 now!

Apollo 服务端启动成功。
##客户端应用
搭建好 Apollo 服务器之后,接下来将我们的应用接入 Apollo。

引入依赖

com.ctrip.framework.apollo
apollo-client
1.1.0

在依赖中只需要增加 apollo-client 的引用。

入口程序
@SpringBootApplication
@EnableApolloConfig("TEST1.product")
public class ApolloApplication {

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

我们通过 @EnableApolloConfig("TEST1.product") 注解开启注册到 Apollo 服务端,并指定了 namespace 为 TEST1.product。

配置文件
app.id: spring-boot-logger
# set apollo meta server address, adjust to actual address if necessary
apollo.meta: http://localhost:8080
server:
port: 0

配置文件中指定了appid 和 Apollo 服务器的地址。

测试应用

我们通过动态设置输出的日志等级来测试接入的配置中心。
@Service
public class LoggerConfiguration {
private static final Logger logger = LoggerFactory.getLogger(LoggerConfiguration.class);
private static final String LOGGER_TAG = "logging.level.";

@Autowired
private LoggingSystem loggingSystem;

@ApolloConfig
private Config config;

@ApolloConfigChangeListener
// 监听 Apollo 配置中心的刷新事件
private void onChange(ConfigChangeEvent changeEvent) {
refreshLoggingLevels();
}

@PostConstruct
// 设置刷新之后的日志级别
private void refreshLoggingLevels() {
Set keyNames = config.getPropertyNames();
for (String key : keyNames) {
if (containsIgnoreCase(key, LOGGER_TAG)) {
String strLevel = config.getProperty(key, "info");
LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
logger.info("{}:{}", key, strLevel);
}
}
}

private static boolean containsIgnoreCase(String str, String searchStr) {
if (str == null || searchStr == null) {
return false;
}
int len = searchStr.length();
int max = str.length() - len;
for (int i = 0; i <= max; i++) {
if (str.regionMatches(true, i, searchStr, 0, len)) {
return true;
}
}
return false;
}
}

如上的配置类用于根据 Apollo 配置中心的日志等级配置,设置本地服务的日志等级,并监听刷新事件,将刷新后的配置及时应用到本地服务,其中 @PostConstruct 注解用于在完成依赖项注入以执行任何初始化之后需要执行的方法。
@Service
public class PrintLogger {
private static Logger logger = LoggerFactory.getLogger(PrintLogger.class);

@ApolloJsonValue("${kk.v}")
private String v;

@PostConstruct
public void printLogger() throws Exception {
Executors.newSingleThreadExecutor().submit(() -> {
while (true) {
logger.error("=========" + v);
logger.info("我是info级别日志");
logger.error("我是error级别日志");
logger.warn("我是warn级别日志");
logger.debug("我是debug级别日志");
TimeUnit.SECONDS.sleep(1);
}
});
}
}

起一个线程,输出不同级别的日志。根据配置的日志等级,过滤后再打印。我们在如上的程序中,还自定义了一个字段,同样用以测试随机打印最新的值。
##测试
我们在 Apollo 的配置界面中,增加如下的配置:
2.jpg

并将配置发布,启动我们本地的 SpringBoot 服务:
2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : =========log-is-error-level.
2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : 我是error级别日志

我们将调整日志的级别为warn,只需要在界面上编辑。
3.jpg

将编辑好的配置发布,应用服务将会收到刷新事件。
4.jpg

可以看到,服务刷新了日志的级别,打印 warn 的日志信息。
2019-05-28 20:35:56.819  WARN 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : 我是warn级别日志
2019-05-28 20:36:06.823 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : =========log-is-warn-level.

#原理细究
在体验了 Apollo 作为配置中心之后,我们将了解下 Apollo 的总体设计和实现的原理。
##Apollo 整体架构
5.png

上图简要描述了 Apollo 的总体设计,从下往上看:

* Config Service 提供配置的读取、推送等功能,服务对象是Apollo客户端
* Admin Service 提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)
* Config Service 和 Admin Service 都是多实例、无状态部署,所以需要将自己注册到 Eureka 中并保持心跳
* 在 Eureka 之上我们架了一层 Meta Server 用于封装 Eureka 的服务发现接口
* Client 通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port 访问服务,同时在 Client 侧会做 load balance、错误重试
* Portal 通过域名访问 Meta Server 获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试
* 为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中

ConfigService、AdminService、Portal 属于 Apollo 服务端的模块,其中提到的 Eureka 是为了保证高可用,Config和Admin都是无状态以集群方式部署的,Client 怎么找到 Config?Portal 怎么找到 Admin?为了解决这个问题,Apollo在其架构中引入了Eureka服务注册中心组件,实现微服务间的服务注册和发现用于服务发现和注册,Config和Admin Service注册实例并定期报心跳, Eureka与ConfigService一起部署。

MetaServer 其实是一个Eureka的Proxy,将Eureka的服务发现接口以更简单明确的HTTP接口的形式暴露出来,方便 Client/Protal 通过简单的 HTTPClient 就可以查询到 Config/Admin 的地址列表。获取到服务实例地址列表之后,再以简单的客户端软负载(Client SLB)策略路由定位到目标实例,并发起调用。
##客户端实现
在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的。
6.png

上图简要描述了配置发布的大致过程:用户在Portal操作配置发布;Portal调用Admin Service的接口操作发布;Admin Service发布配置后,发送ReleaseMessage给各个Config Service;Config Service收到ReleaseMessage后,通知对应的客户端。

如何通知客户端呢?我们看到 Apollo 的实现步骤如下:

  1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
  2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。

* 这是一个fallback机制,为了防止推送机制失效导致配置不更新
* 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
* 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。

  1. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
  2. 客户端会把从服务端获取到的配置在本地文件系统缓存一份,在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置。
  3. 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知

#小结
本文首先介绍分布式配置中心的概念和 Apollo 接入的实践,然后深入介绍了 Apollo 的总体架构和实现的一些细节。总得来说, Apollo 是现有配置中心组件中,功能最全的一个。能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

本文对应的代码地址: https://github.com/keets2012/Spring-Boot-Samples/tree/master/apollo-demo

原文链接:http://blueskykong.com/2019/05/27/apollo-spring-boot/
微服务架构是近一段时间在软件体系架构领域里出现的一个新名词。它通过将功能分解到多个独立的服务,以实现对解决方案或者复杂系统的解耦。