本次分享的技术大纲如下:
- 传统应用开发面临的挑战
- 服务化实践
- 服务化不是银弹
- 服务化架构的演进方向
一 、传统应用开发面临的挑战
挑战1-- 研发成本高
主要体现在如下几个方面:
- 代码重复率高
在实际项目分工时,开发都是各自负责几个功能,即便开发之间存在功能重叠,往往也会选择自己实现,而不是类库共享,主要原因如下:
- 从技术架构角度看,传统垂直架构的特点是本地API接口调用,不存在业务的拆分和互相调用,使用到什么功能就本地开发,非常方便,不需要过度依赖于其它功能模块;
- 从考核角度来看,共享很难推行。开发只需要对自己开发的模块交付质量负责,没有义务为他人提供并维护公共类库,这个非常耗费成本;
- 时间依赖很难把控:对于公共类库的使用者而言,依赖别人提供此功能,但是功能提供者可能有更重要的事情在做,提供时间无法满足使用者。与其坐等别人提供,还不如自己开发效率高;
跨地域、跨开发小组协调很困难,业务团队可能跨地域研发,内部通常也会分成多个开发小组,各开发小组之间的协调和沟通成本非常高。
- 需求变更困难
代码重复率变高之后,已有功能变更或者新需求加入都会非常困难,以充值缴费功能为例,不同的充值渠道开发了相同的限额保护功能,当限额保护功能发生变更之后,所有重复开发的限额保护功能都需要重新修改和测试,很容易出现修改不一致或者被遗漏,导致部分渠道充值功能正常,部分存在Bug的问题,示例如下:
- 无法满足新业务快速创新和敏捷交付
挑战2-- 运维效率低
在传统的MVC架构中,业务流程是由一长串本地接口或者方法调用串联起来的,臃肿而冗长,而且往往由一个人负责开发和维护。随着业务的发展和需求变化,本地代码在不断的迭代和变更,最后形成了一个个垂直的功能孤岛,只有原来的开发者才理解接口调用关系和功能需求,一旦原有的开发者离职或者调到其他项目组,这些功能模块的运维就会变得非常困难:
当垂直应用越来越多时,连架构师都无法描述应用间的架构关系,随着业务的发展和功能膨胀,这种架构很容易发生腐化。
- 测试、部署成本高:业务运行在一个进程中,因此系统中任何程序的改变,都需要对整个系统重新测试并部署
- 可伸缩性差:水平扩展只能基于整个系统进行扩展,无法针对某一个功能模块按需扩展
- 可靠性差:某个应用BUG,例如死循环、OOM等,会导致整个进程宕机,影响其它合设的应用
如何解决传统单体架构面临的挑战?
解决对策:1、拆分 2、解耦 3、透明 4、独立 5、分层。
- 拆分:对应用进行水平和垂直拆分,例如商品中心、计费中心、订单中心等。
- 解耦:通过服务化和订阅、发布机制对应用调用关系解耦,支持服务的自动注册和发现
- 透明:通过服务注册中心管理服务的发布和消费、调用关系
- 独立:服务可以独立打包、发布、部署、启停、扩容和升级,核心服务独立集群部署
- 分层:梳理和抽取核心应用、公共应用,作为独立的服务下沉到核心和公共能力层,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求
二、服务化实践
服务的订阅发布机制
它的核心理念是实现服务消费者和服务提供者的解耦,让服务消费者能够像使用本地接口一样消费远端的服务提供者,而不需要关心服务提供者的位置信息,实现透明化调用。
关键技术点:服务的订阅、发布机制、服务的健康状态检测和高HA。
常用的服务注册中心有Zookeeper、ETCD,以及基于数据库的配置中心。
大家在技术选型的时候,需要根据自己的业务实际情况进行选择。例如超大规模集群,服务实例数超过10W,Zookeeper就会存在性能问题。
现在开源的分布式配置服务很多,如无特殊需求,建议选择开源方案。
服务化实践-零侵入
实际上,完全的零侵入很难做到,即使是声明式配置,配置本身也是代码的一部分,只不过相比于代码类库依赖,它不是编译器依赖。
一种好的做法是,服务的发布和消费通过声明式或者注解的方式,而不是直接调用服务框架的接口,例如Thrift。客户端需要调用Thrift提供的类库访问服务端,这就是代码API级的依赖,对业务代码侵入比较大。
一种比较成熟的实践是 利用Spring的扩展机制,通过XML的方式实现服务的发布和消费。
服务化实践-容错和路由
单体应用服务化之后,通常采用分布式集群的部署模式。
这会带来两个问题:
- 服务如何路由;
- 远端服务访问失败之后,如果进行容错。
大部分的容错和路由策略可以抽象到分布式服务框架中,通过策略配置的方式提供给用户使用,降低用户的开发成本。
从业务扩展性角度看,服务框架通常会提供扩展点,供业务做路由和容错定制。例如,业务希望根据手机号码和地市进行路由:
服务化实践-本地短路策略
在电信行业中,小机还是很普遍,应用通常会合设,例如服务提供者和消费者部署到同一台主机上。
为了提升性能,降低时延,往往会提供本地短路策略,具体策略如下:
服务化实践-多样化调用方式
服务的调用方式,主要有三种:同步服务调用、异步服务调用、并行服务调用。最常用、简单的就是同步服务调用。
异步服务调用的工作原理如下:
详细步骤如下:
- 消费者调用服务端发布的接口,接口调用由分布式服务框架包装成动态代理,发起远程服务调用;
- 通信框架异步发送请求消息,如果没有发生I/O异常,返回;
- 请求消息发送成功后,I/O线程构造Future对象,设置到RPC上下文中;
- 用户线程通过RPC上下文获取Future对象;
- 构造Listener对象,将其添加到Future中,用于服务端应答异步回调通知;
- 用户线程返回,不阻塞等待应答;
- 服务端返回应答消息,通信框架负责反序列化等;
- I/O线程将应答设置到Future对象的操作结果中;
- Future对象扫描注册的监听器列表,循环调用监听器的operationComplete方法,将结果通知给监听器,监听器获取到结果之后,继续后续业务逻辑的执行,异步服务调用结束。
并行服务调用,目的是为了提升服务调用的并行度,降低E2E时延。
服务化实践-高性能、低时延
服务框架的性能,主要强调三个要素:1、I/O通信;2、序列化框架;3、线程调用模型。
如果使用Java语言,I/O框架推荐 Netty。
序列化框架推荐:Thrift、Avro序列化框架、PB等。线程调度模型建议参考Reactor。
一种线程模型的参考实现方式:Netty的线程模型
服务化实践-故障隔离
故障隔离非常重要,由于经常会采用同步服务调用模式,核心服务和非核心服务共用同一个线程池和消息队列,非核心服务处理慢往往会阻塞核心服务,导致雪崩现象。
故障隔离的核心技术点如下:
1. 支持服务部署到不同线程/线程池中
2. 核心服务和非核心服务隔离部署
服务化实践-服务治理
随着业务规模的不断扩大,小服务资源浪费等问题逐渐显现,需要能够基于服务调用的性能KPI数据进行容量管理,合理分配各个服务的资源占用,提高机器的利用率。
线上业务发生故障时,需要对故障业务做服务降级、流量控制、流量迁移等,快速恢复业务。
随着开发团队的不断扩大,服务的上线越来越随意,甚至发生功能相同、服务名不同的服务同时上线。上线容易下线难,为了规范服务的上线和下线,在服务发布前,需要走服务预发布流程,由架构师或者项目经理对需要上线的服务做发布审核,审核通过的才能够上线。
为了满足服务线下管控、保障线上高效运行,需要有一个统一的服务治理框架对服务进行统一、有效管控,保障服务的高效、健康运行。
服务治理是分布式服务框架的一个可选特性,尽管从服务开发和运行角度看它不是必须的,但是如果没有服务治理功能,分布式服务框架的服务SLA很难得到保障,服务化也很难真正实施成功。
从架构上看,分布式服务框架的服务治理分为三层:
第1层为服务治理展示层,它主要由服务治理Portal组成,提供可视化的界面,方便服务运维人员进行治理操作。
第2层为服务治理SDK层,它主要由如下几部分组成:
- 服务治理元数据:服务治理元数据主要包括服务治理实体对象,包括服务模型、应用模型、治理组织模型、用户权限模型、数据展示模型等。元数据模型通过Data Mapper和模型扩展,向上层界面屏蔽底层服务框架的数据模型,实现展示层和服务框架的解耦,元数据也可以用于展示界面的定制扩展;
- 服务治理接口:服务治理Portal调用服务治理接口,实现服务治理。例如服务降级接口、服务流控接口、服务路由权重调整接口、服务迁移接口等。服务接口与具体的协议无关,它通常基于分布式服务框架自身实现,可以是Restful接口,也可以是内部的私有协议;
- 服务治理客户端类库:由于服务治理服务本身通常也是基于分布式服务框架开发,因此服务治理Portal需要集成分布式服务框架的客户端类库,实现服务的自动发现和调用;
- 调用示例:客户端SDK需要提供服务治理接口的参数说明、注意事项以及给出常用的调用示例,方便前端开发人员使用;
- 集成开发指南:服务治理SDK需要提供集成开发指南,指导使用者如何在开发环境中搭建、集成和使用服务治理SDK。
第3层为后台服务治理服务层:它通常由一组服务治理服务组成,可以单独部署,也可以与应用合设。考虑到健壮性,通常选择独立集群部署。治理服务的可靠性由分布式服务框架自身来保证,治理服务宕机或者异常,不影响业务的正常使用。服务治理服务通常并不随服务框架发布,治理服务是可选的插件,单独随服务治理框架交付。
服务化实践-高可靠性
关键技术点设计如下:
- 服务无状态设计
- 服务注册中心集群,宕机不影响业务运行
- 服务提供者集群,集群容错屏蔽服务提供者故障
- 服务健康状态检测,基于时延等性能KPI指标
- 服务治理中心集群,宕机不影响业务运行
- 服务级故障隔离
- 核心服务独立部署和集群
- 跨机房路由和异地容灾
三、服务化不是银弹
服务化会带来很多收益,但是它却不是银弹。
服务化不是银弹-时延问题
在服务化之前,业务通常都是本地API调用,本地方法调用性能损耗较小。服务化之后,服务提供者和消费者之间采用远程网络通信,增加了额外的性能损耗。
服务化不是银弹-问题定位
在分布式环境下,如何高效的进行问题定界定位和日志检索
服务化不是银弹-事务一致性
服务化、分布式部署之后,有逻辑关联关系的多个数据库操作被打散到集群中各个独立的服务实例中,引入分布式环境下的事务一致性问题。
服务化不是银弹-前后台直接通信问题
前后台直接通信问题如下:
存在的问题如下:
- 客户端需求和每个微服务暴露的细粒度API不匹配
- 微服务使用的RPC私有协议,不是浏览器友好或防火墙友好的
- 微服务难以重构。随着时间推移,我们可能想要更改系统划分成服务的方式。如果客户端与微服务直接通信,那么执行这类重构就非常困难了
服务化不是银弹-团队协作问题
- 共享服务注册中心问题:为了方便开发测试,经常会在线下共用一个所有服务共享的服务注册中心,这时,一个正在开发中的服务发布到服务注册中心,可能会导致一些消费者不可用。
- 多团队进度协同问题:服务提供者和消费者相互依赖问题,开发依赖、测试依赖等。
- 接口前向兼容性问题:由于线上的Bug修复、内部重构和需求变更,服务提供者会经常修改内部实现,包括但不限于:接口参数变化、参数字段变化、业务逻辑变化和数据表结构变化。在实际项目中经常会发生服务提供者修改了接口或者数据结构,但是并没有及时知会到所有消费者,导致服务调用失败
四、未来演进方向-微服务架构
微服务的划分原则是难点,根据华为的经验:微服务划分不是一步到位,而是不断的迭代和演进,最终找到适合自己团队和业务的微服务划分原则。
未来演进方向-基于Docker部署微服务
使用Docker部署微服务的优点总结:
- 一致的环境:线上线下环境一致
- 避免对特定云基础设施提供商的依赖
- 降低运维团队负担
- 高性能:接近裸机的性能
- 多租户
未来演进方向-云端微服务
利用云平台的弹性资源调度,动态性等,可以实现微服务的Dev&Ops
最后我们一起回顾下服务化的演进历程:
Q&A
Q1:上面提到服务化缺点的第三条接口变更问题,请问微服务是如何解决这个问题的呢?或者说微服务相比之下什么优势会避免这个问题?
A1:根据我们团队的经验,主要从如下几个方面降低影响:1、微服务的接口就是契约,制定 接口兼容性规范;涉及到技术和管理两个层面;2、微服务鼓励只做一件事情,因此它更加稳定;3、基于消费者契约测试,快速发现兼容性问题。
Q2:微服务架构里,分布式事务如何做的,对数据一致性要求较高的系统是否适合拆分成微服务,或者说微服务的粒度如何把握?
A2:分布式事务是难点,策略如下:1)如果业务上能够承受非强一致性,建议通过事务补偿的方式做最终一致性,可以基于MQ等中间件来实现;2)如果是转账、实时计费、充值等对实时性要求高的,往往选择强一致性事务,就需要引入TCC等分布式事务框架。无论如何,只要做分布式,事务一致性就会成为问题,跟是否是微服务没必然关系。
Q3:生产环境中的服务注册中心必然是共享的,那如何去做灰度发布或者A/B Test呢?
A3:一种比较好的服务灰度策略是:1)服务框架提供灰度规则框架,包括后台引擎和前台Portal,由业务配置灰度规则;2)分布式服务框架支持灰度规则推送和业务自定义路由;3)前端SLB ,例如Ngix做灰度插件,接收灰度规则。消息从前端门户接入到后端服务路由,都支持基于规则的路由分发策略,实现灰度发布。
Q4:Netty的无锁化串行会比有锁的并行性能更高吗?有案例吗?华为现在都是用Docker部署应用吗?
A4:Netty的无锁化串行性能问题:1)在实际项目中,线程池争用模式和串行模式我们都使用过,Netty的无锁化串行模式性能更高。Docker部署应用:华为的公有云和私有云都支持基于Docker部署应用,由客户根据需要自主选择。
Q5:IO通信是怎么保证每次连接成功的呢?
A5:NIO通信本身并不保证每次连接都成功,它的连接是异步的,你可以根据如下两种策略获得异步链接的结果:1)发起连接之后主动调用同步方法等待结果返回,阻塞式;2)获取异步连接Future,添加Listener监听器监听连接结果,这种模式是异步回调,不会阻塞当前线程。
Q6:使用zk作为服务注册中心,对与某个服务当客户端连接数很多时候节点变化会引起羊群效应,怎么处理这种问题呢?或者说如何避免这种问题呢?
A6:这个问题真是好!通常而言,大家会使用服务注册中心做服务可用性检测,如果发现某个服务节点不可用,就会将其从注册中心中删除。但是,有一种场景是ZK检测的结果跟客户端和服务端实际的连接状态不一致。从ZK看,服务提供者可以使用。但是由于服务消费者跟提供者之间的链路已经中断,跟ZK的链路却是正常,这种情况下就会出现状态不一致问题。所以,只依靠ZK做状态检测还不够,需要服务提供者和消费者的链路层做双向心跳检测。
Q7:我现在做的系统是zk做注册中心服务把地址注册上去(临时节点),客户端拿地址请求,http的,现在发现如果是公网调用的话,对公网资源要求还挺多的,zk公网, 应用公网;为了减少对公网需求,中间加一层nginx,把nx地址注册上去,不过又得加个http探测监控程序,异常还得删掉注册数据,不知道这种做法是否妥当?
A7:Ng监听ZK注册的服务提供者URL即可,问题不大。
Q8:用Netty做同通信框架,监控上报应该怎么设计更完善?
A8:建议的方式如下:Netty自身不用告警,监听Netty的异常事件,然后通过MQ吐出去,监控系统订阅通信框架的事件主题,实现通信框架和监控系统解耦。
Q9:SOA和微服务架构的区别和联系是?看起来好像啊!
A9:1) 服务拆分粒度:SOA首先要解决的是异构应用的服务化;微服务强调的是服务拆分尽可能小,最好是独立的原子服务;
2) 服务依赖:传统的SOA服务,由于需要重用已有的资产,存在大量的服务间依赖;微服务的设计理念是服务自治、功能单一独立,避免依赖其它服务产生耦合,耦合会带来更高的复杂度;
3) 服务规模:传统SOA服务粒度比较大,多数会采用将多个服务合并打成war包的方案,因此服务实例数比较有限;微服务强调尽可能拆分,同时很多服务会独立部署,这将导致服务规模急剧膨胀,对服务治理和运维带来新的挑战;
4) 架构差异:微服务化之后,服务数量的激增会引起架构质量属性的变化,例如企业集成总线ESB(实总线)逐渐被P2P的虚拟总线替换;为了保证高性能、低时延,需要高性能的分布式服务框架保证微服务架构的实施;
5) 服务治理:传统基于SOA Governance的静态治理转型为服务运行态微治理、实时生效;
6) 敏捷交付:服务由小研发团队负责微服务设计、开发、测试、部署、线上治理、灰度发布和下线,运维整个生命周期支撑,实现真正的DevOps。
总结:量变引起质变,这就是微服务架构和SOA 服务化架构的最大差异。
Q10:如果要将现有单机服务重构到微服务,应该考虑哪些问题?数据迁移的安全问题怎么解决?有什么实践方案吗?
A10:需要考虑的问题如下:1)当前单机应用是否能够满足业务发展需要,有没有必要做服务化改造和分布式部署;2)评估迁移的工作量,以及人员技能培训等。3)自研服务框架还是使用开源的方案。
数据迁移安全问题:如果内网,通常不会涉及到复杂的安全控制问题;如果跨公网,建议加入API Gateway统一做安全管控。
实践方案:公开的资料,可以参考淘宝的服务化实践、京东的服务化实践等。其实华为也有,不过遗憾的是目前政策不允许公开出来。
Q11:麻烦李老师介绍下你们华为内部基于netty做socke通信的协议设计的最佳实践。
A11:这个问题很大,简单介绍下思路。在11年和13年的时候我分别主持设计了华为基于Mina和Netty的统一NIO通信框架。设计要点如下:1)要熟悉Netty的线程调度模型、常用的类库等,能够熟练使用Netty;2)NIO通信框架的分层原则,哪些该做、哪些不该做,需要识别出来;3)扩展点,预留足够的扩展点给上层应用协议栈做扩展;4)可以内置配置化的安全策略、握手认证、心跳检测等机制;5)可服务性设计,包括日志、性能KPI指标等。