本次分享主要介绍了爱油科技基于Docker和Spring Cloud将总体业务微服务化的一些实践经验,主要包括:node
对于单体应用来讲,优势不少,例如:git
然而随着业务复杂性的上升,业务规模的扩大,缺点也显现出来,例如:github
所以微服务技术做为一项对分布式服务治理的架构模式,逐渐被你们认识了。算法
实施微服务,首先对咱们的架构进行了拆分:按行分层,按列分业务spring
在咱们的微服务体系中,全部的服务被划分为了三个层次:docker
实践中咱们主要关注业务服务层和接入层,对于没有足够运维力量的咱们,基础设施使用云服务是省事省力的选择。数据库
业务服务层咱们给他起名叫做Epic,接入层咱们起名Rune,创建之初便订立了以下原则:json
业务逻辑层咱们主要使用使用Java,接入层咱们主要使用PHP或Node。后来随着团队的成长,逐步将接入层所有迁移至Node。后端
爱油科技做为一家成品油行业的初创型公司,须要面对很是复杂的业务场景,并且随着业务的发展,变化的可能性很是高。因此在微服务架构设计之初,咱们就指望咱们的微服务体系能:api
目前常见的微服务相关框架:
这些常见的框架中,Dubbo几乎是惟一能被称做全栈微服务框架的“框架”,它包含了微服务所需的几乎全部内容,而DubboX做为它的加强,增长了REST支持。
它优势不少,例如:
不过遗憾的是:
Motan是微博平台微服务框架,承载了微博平台千亿次调用业务。
优势是:
不过:
Apache Thrift、gRPC等虽然优秀,并不能算做微服务框架,自身并不包括服务发现等必要特性。
若是说微服务少不了Java,那么必定少不了Spring,若是说少不了Spring,那么微服务“官配”Spring Cloud固然是值得斟酌的选择。
Spring Cloud优势:
固然它有不少不足之处,例如:
根据咱们的目标,咱们最终选择了Spring Cloud做为咱们的微服务框架,缘由有4点:
Apache Thrift
和gRPC
自研,投入产出比不好;Motan
尚未发布。所以Spring Cloud成为了理性的选择。
Spring Cloud是一个集成框架,将开源社区中的框架集成到Spring体系下,几个重要的家族项目:
spring-boot
,一改Java应用程序运行难、部署难,甚至无需Web容器,只依赖JRE便可spring-cloud-netflix
,集成Netflix优秀的组件Eureka、Hystrix、Ribbon、Zuul,提供服务发现、限流、客户端负载均衡和API网关等特性支持spring-cloud-config
,微服务配置管理spring-cloud-consul
,集成Consul支持固然,SpringCloud下子项目很是多,这里就不一一列出介绍了。
Spring Cloud Netflix提供了Eureka服务注册的集成支持,不过没选它是由于:
Docker做为支撑平台的重要技术之一,Consul几乎也是咱们的必选服务。所以咱们以为一事不烦二主,理所应当的Consul成为咱们的服务注册中心。
Consul的优点:
也就是说,Consul能够一次性解决咱们对服务注册发现、配置管理的需求,并且长期来看也更适合跟不一样平台的系统,包括和Docker调度系统进行整合。
最初打算本身开发一个Consul和Spring Cloud整合的组件,不过幸运的是,咱们作出这个决定的时候,spring-cloud-consul
刚刚发布了,咱们能够拿来即用,这节约了不少的工做量。
所以借助Consul和spring-cloud-consul
,咱们实现了
srping-cloud-consul
的项目能够自动注册服务,也能够经过HTTP接口手动注册,Docker容器也能够自动注册固然也踩到了一些坑:
spring-cloud-consul
服务注册时不能正确选判本地ip地址。对于咱们的环境来讲,不管是在服务器上,仍是Docker容器里,都有多个网络接口同时存在,而spring-cloud-consul
在注册服务时,须要先选判本地服务的IP地址,判断逻辑是以第一个非本地地址为准,经常错判。所以在容器中咱们利用entrypoint脚本获取再经过环境变量强制指定。
#!/usr/bin/env bash set -e # If service runs as Rancher service, auto set advertise ip address # from Rancher metadata service. if [ -n "$RUN_IN_RANCHER" ]; then echo "Waiting for ip address..." # Waiting for ip address sleep 5 RANCHER_MS_BASE=http://rancher-metadata/2015-12-19 PRIMARY_IP=`curl -sSL $RANCHER_MS_BASE/self/container/primary_ip` SERVICE_INDEX=`curl -sSL $RANCHER_MS_BASE/self/container/service_index` if [ -n "$PRIMARY_IP" ]; then export SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME=$PRIMARY_IP fi echo "Starting service #${SERVICE_INDEX-1} at $PRIMARY_IP." fi exec "$@"
咱们的容器运行在Rancher中,因此能够利用Rancher的metadata服务来获取容器的IP地址,再经过SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME
环境变量来设置服务发现的注册地址。基于其余容器调度平台也会很类似。
为了方便开发人员使用,微服务框架应当简单容易使用。对于不少微服务框架和RPC框架来讲,都提供了很好的机制。在Spring Cloud中经过OpenFeign
实现微服务之间的快速集成:
服务方声明一个Restful的服务接口,和普通的Spring MVC控制器几乎别无二致:
@RestController @RequestMapping("/users") public class UserResource { @RequestMapping(value = "{id}", method = RequestMethod.GET, produces = "application/json") public UserRepresentation findOne(@PathVariable("id") String id) { User user = this.userRepository.findByUserId(new UserId(id)); if (user == null || user.getDeleted()) { throw new NotFoundException("指定ID的用户不存在或者已被删除。"); } return new UserRepresentation(user); } }
客户方使用一个微服务接口,只须要定义一个接口:
@FeignClient("epic-member-microservice") public interface UserClient { @Override @RequestMapping(value = "/users/{id}", method = RequestMethod.GET, produces = "application/json") User findOne(@PathVariable("id") String id); }
在须要使用UserClient
的Bean中,直接注入UserClient
类型便可。事实上,UserClient
和相关VO类,能够直接做为公共接口封装在公共项目中,供任意须要使用的微服务引用,服务方Restful Controller直接实现这一接口便可。
OpenFeign
提供了这种简单的方式来使用Restful服务,这大大下降了进行接口调用的复杂程度。
对于错误的处理,咱们使用HTTP状态码做为错误标识,并作了以下规定:
对于服务器端,只须要在一个异常类上添加注解,便可指定该异常的HTTP响应状态码,例如:
@ResponseStatus(HttpStatus.NOT_FOUND) public class NotFoundException extends RuntimeException { public NotFoundException() { super("查找的资源不存在或者已被删除。"); } public NotFoundException(String message) { super(message); } public NotFoundException(String message, Throwable cause) { super(message, cause); } }
对于客户端咱们实现了本身的FeignClientExceptionErrorDecoder
来将请求异常转换为对于的异常类,示例以下:
@Component public class FeignClientExceptionErrorDecoder implements ErrorDecoder { private final ErrorDecoder delegate = new ErrorDecoder.Default(); @Override public Exception decode(String methodKey, Response response) { // Only decode 4xx errors. if (response.status() >= 500) { return delegate.decode(methodKey, response); } // Response content type must be json if (response.headers().getOrDefault("Content-Type", Lists.newArrayList()).stream() .filter(s -> s.toLowerCase().contains("json")).count() > 0) { try { String body = Util.toString(response.body().asReader()); // 转换并返回异常对象 ... } catch (IOException ex) { throw new RuntimeException("Failed to process response body.", ex); } } return delegate.decode(methodKey, response); } }
须要注意的是,decode
方法返回的4xx状态码异常应当是HystrixBadRequestException
的子类对象,缘由在于,咱们把4xx异常视做业务异常,而不是因为故障致使的异常,因此不该当被Hystrix计算为失败请求,并引起断路器动做,这一点很是重要。
在UserClient.findOne
方法的调用代码中,便可直接捕获相应的异常了:
try { User user = this.userClient.findOne(new UserId(id)); } catch(NotFoundException ex) { ... }
经过OpenFeign
,咱们大大下降了Restful接口进行服务集成的难度,几乎作到了无额外工做量的服务集成。
微服务架构下,因为调用须要跨系统进行远程操做,各微服务独立运维,因此在设计架构时还必须考虑伸缩性和容错性,具体地说主要包括如下几点要求:
spring-cloud-netflix
和相关组件为咱们提供了很好的解决方案:
下面主要介绍一下,各个组件在进行服务质量保证中是如何发挥做用的。
Consul中注册了一致性的可用的服务列表,并经过健康检查保证这些实例都是存活的,服务注册和检查的过程以下:
spring-cloud-consul
经过Consul接口发起服务注册,将服务的/health
做为健康检查端点;/health
,检查当前微服务是否为UP
状态;/health
将会收集微服务内各个仪表收集上来的状态数据,主要包括数据库、消息队列是否连通等;这样可以保证Consul中列出的全部微服务状态都是健康可用的,各个微服务会监视微服务实例列表,自动同步更新他们。
Hystrix提供了断路器模式的实现,主要在三个方面能够说明:
图片来自Hystrix项目文档
首先Hystrix提供了降级方法,断路器开启时,操做请求会快速失败再也不向后投递,直接调用fallback方法来返回操做;当操做失败、被拒或者超时后,也会直接调用fallback方法返回操做。这能够保证在系统过载时,能有后备方案来返回一个操做,或者优雅的提示错误信息。断路器的存在能让故障业务被隔离,防止过载的流量涌入打死后端数据库等。
而后是基于请求数据统计的断路开关,在Hystrix中维护一个请求统计了列表(默认最多10条),列表中的每一项是一个桶。每一个桶记录了在这个桶的时间范围内(默认是1秒),请求的成功数、失败数、超时数、被拒数。其中当失败请求的比例高于某一值时,将会触发断路器工做。
最后是不一样的请求命令(HystrixCommand
)可使用彼此隔离的资源池,不会发生相互的挤占。在Hystrix中提供了两种隔离机制,包括线程池和信号量。线程池模式下,经过线程池的大小来限制同时占用资源的请求命令数目;信号量模式下经过控制进入临界区的操做数目来达到限流的目的。
这里包括了Hystrix的一些重要参数的配置项:
参数 | 说明 |
---|---|
circuitBreaker.requestVolumeThreshold | 至少在一个统计窗口内有多少个请求后,才执行断路器的开关,默认20 |
circuitBreaker.sleepWindowInMilliseconds | 断路器触发后多久后才进行下一次断定,默认5000毫秒 |
circuitBreaker.errorThresholdPercentage | 一个统计窗口内百分之多少的请求失败才触发熔断,默认是50% |
execution.isolation.strategy | 运行隔离策略,支持Thread ,Semaphore ,前者经过线程池来控制同时运行的命令,后者经过信号来控制,默认是Thread |
execution.isolation.thread.interruptOnTimeout | 命令执行的超时时间,默认1000毫秒 |
coreSize | 线程池大小,默认10 |
keepAliveTimeMinutes | 线程存活时间,默认为1分钟 |
maxQueueSize | 最大队列长度,-1使用SynchronousQueue,默认-1。 |
queueSizeRejectionThreshold | 容许队列堆积的最大数量 |
Ribbon使用Consul提供的服务实例列表,能够经过服务名选取一个后端服务实例链接,并保证后端流量均匀分布。spring-cloud-netflix
整合了OpenFeign、Hystrix和Ribbon的负载均衡器,整个调用过程以下(返回值路径已经省略):
在这个过程当中,各个组件扮演的角色以下:
Feign负责提供客户端接口收调用,把发起请求操做(包括编码、解码和请求数据)封装成一个Hystrix命令,这个命令包裹的请求对象,会被Ribbon的负载均衡器处理,按照负载均衡策略选择一个主机,而后交给请求对象绑定的HTTP客户端对象发请求,响应成功或者不成功的结果,返回给Hystrix。
spring-cloud-netflix
中默认使用了Ribbon的ZoneAwareLoadBalancer
负载均衡器,它的负载均衡策略的核心指标是平均活跃请求数(Average Active Requests)。ZoneAwareLoadBalancer
会拉取全部当前可用的服务器列表,而后将目前因为种种缘由(好比网络异常)响应过慢的实例暂时从可用服务实例列表中移除,这样的机制能够保证故障实例被隔离,以避免继续向其发送流量致使集群状态进一步恶化。不过因为目前spring-cloud-consul
还不支持经过consul来指定服务实例的所在区,咱们正在努力将这一功能完善。除了选区策略外,Ribbon中还提供了其余的负载均衡器,也能够自定义合适的负载均衡器。
关于区域的支持,我提交的PR已经Merge到spring-cloud-consul项目中,预计下个版本将会包含这项特性。
总的来看,spring-cloud-netflix
和Ribbon中提供了基本的负载均衡策略,对于咱们来讲已经足够用了。但实践中,若是须要进行灰度发布或者须要进行流量压测,目前来看还很难直接实现。而这些特性在Dubbo则开箱即用。
Zuul为使用Java语言的接入层服务提供API网关服务,既能够根据配置反向代理指定的接口,也能够根据服务发现自动配置。Zuul提供了相似于iptables的处理机制,来帮助咱们实现验证权鉴、日志等,请求工做流以下所示:
图片来自Zuul官方文档。
使用Zuul进行反向代理时,一样会走与OpenFeign相似的请求过程,确保API的调用过程也能经过Hystrix、Ribbon提供的降级、控流机制。
Hystrix会统计每一个请求操做的状况来帮助控制断路器,这些数据是能够暴露出来供监控系统热点。Hystrix Dashboard能够将当前接口调用的状况以图形形式展现出来:
图片来自Hystrix Dashboard官方示例
Hystrix Dashboard既能够集成在其余项目中,也能够独立运行。咱们直接使用Docker启动一个Hystrix Dashboard服务便可:
docker run --rm -ti -p 7979:7979 kennedyoliveira/hystrix-dashboard
为了实现能对整个微服务集群的接口调用状况汇总,可使用spring-cloud-netflix-turbine
来将整个集群的调用状况聚集起来,供Hystrix Dashboard展现。
微服务的日志直接输出到标准输出/标准错误中,再由Docker经过syslog日志驱动将日志写入至节点机器机的rsyslog中。rsyslog在本地暂存并转发至日志中心节点的Logstash中,既归档存储,又经过ElasticSearch进行索引,日志能够经过Kibana展现报表。
在rsyslog的日志收集时,须要将容器信息和镜像信息加入到tag中,经过Docker启动参数来进行配置:
--log-driver syslog --log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}"
不过rsyslog默认只容许tag不超过32个字符,这显然是不够用的,因此咱们自定义了日志模板:
template (name="LongTagForwardFormat" type="string" string="<%PRI%>%TIMESTAMP:::date-rfc3339% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg%")
在实际的使用过程当中发现,当主机内存负载比较高时,rsyslog会发生日志没法收集的状况,报日志数据文件损坏。后来在Redhat官方找到了相关的问题,确认是rsyslog中的一个Bug致使的,当开启日志压缩时会出现这个问题,咱们选择暂时把它禁用掉。
领域驱动设计可以很大程度上帮助咱们享用微服务带来的优点,因此咱们使用领域驱动设计(DDD)的方法来构建微服务,由于微服务架构和DDD有一种自然的契合。把全部业务划分红若干个子领域,有强内在关联关系的领域(界限上下文)应当被放在一块儿做为一个微服务。最后造成了界限上下文-工做团队-微服务一一对应的关系:
在设计单个微服务(Epic层的微服务)时,咱们这样作:
这给咱们带来了显著的好处:
从单体应用迁移到微服务架构时,不得不面临的问题之一就是事务。在单体应用时代,全部业务共享同一个数据库,一次请求操做可放置在同一个数据库事务中;在微服务架构下,这件事变得很是困难。然而事务问题不可避免,很是关键。
解决事务问题时,最早想到的解决方法一般是分布式事务。分布式事务在传统系统中应用的比较普遍,主要基于两阶段提交的方式实现。然而分布式事务在微服务架构中可行性并不高,主要基于这些考虑:
根据CAP理论,分布式系统不可兼得一致性、可用性、分区容错性(可靠性)三者,对于微服务架构来说,咱们一般会保证可用性、容错性,牺牲一部分一致性,追求最终一致性。因此对于微服务架构来讲,使用分布式事务来解决事务问题不管是从成本仍是收益上来看,都不划算。
对微服务系统来讲解决事务问题,CQRS+Event Sourcing是更好的选择。
CQRS是命令和查询职责分离的缩写。CQRS的核心观点是,把操做分为修改状态的命令(Command),和返回数据的查询(Query),前者对应于“写”的操做,不能返回数据,后者对应于“读”的操做,不形成任何影响,由此领域模型被一分为二,分而治之。
Event Sourcing一般被翻译成事件溯源,简单的来讲就是某一对象的当前状态,是由一系列的事件叠加后产生的,存储这些事件便可经过重放得到对象在任一时间节点上的状态。
经过CQRS+Event Sourcing,咱们很容易得到最终一致性,例如对于一个跨系统的交易过程而言:
PlaceOrderEvent
,订单状态PENDING
;PaidEvent
;PaidEvent
,将订单标记为CREATED
;InsufficientEvent
,交易微服务消费将订单标记为CANCELED
。咱们只要保证领域事件能被持久化,那么即便出现网络延迟或部分系统失效,咱们也能保证最终一致性。
实践上,咱们利用Spring从4.2版本开始支持的自定义应用事件机制将本地事务和事件投递结合起来进行:
到目前为止咱们已经有数十个微服务运行于线上了,微服务数目甚至多过了团队人数。若是没有DevOps支持,运维这些微服务将是一场灾难。
咱们使用Docker镜像做为微服务交付的标准件:
因为时间所限,这里就不展开赘述了。
基于spring-cloud-consul
的配置管理仍然须要完善,对于大规模应用的环境中,配置的版本控制、灰度、回滚等很是重要。SpringCloud提供了一个核,可是具体的使用还要结合场景、需求和环境等,再作一些工做。
对于非JVM语言的微服务和基于SpringCloud的微服务如何协同治理,这一问题仍然值得探索。包括像与Docker编排平台,特别是与Mesos协同进行伸缩的服务治理,还须要更多的实践来支持。
Q:大家是部署在公有云,仍是托管机房。
A:咱们部署在阿里云上,使用了不少阿里云服务做为基础设施,这一点是为了节约运维成本。
Q:怎么解决服务过多依赖问题,开发也会有麻烦,由于要开发一个功能,为了把服务跑起来,可能要跑不少服务。
A:在咱们的实际开发过程当中,也遇到了这个问题。主要的是经过部署几个不一样的仿真环境,一组开发者能够共用这组环境。本地开发也很简单,只须要把consul指向到这个集群的consul上便可。
Q:大家微服务业务调用最深有几层?restful接口调用链的效率如何?比单体结构慢多少?
A:通常不超过3层,这是领域驱动设计给咱们带来的优点,单个服务几乎本身就能完成职责范围内的任务,没有出现RPC灾难,一个业务咱们也不倾向于拆分红若干个远程操做进行。
Q:你好 咱们单位从6月份 开始实施 微服务化(O2O业务),使用的是dubbo,使用事务型消息来作最终一致性,请问CQRS+Event Sourcing相对于事务型消息队列来处理一致性问题 有什么优点么。
A:其实CQRS+Event Sourcing是一种观念的转变,落地仍是须要靠存储和消息队列,优点在于能够消除系统中的锁点,性能会更好。
Q:有没有考虑过用kubernetes来实现微服务治理?
A:考虑过,可是咱们团队规模有限,很难快速落地。
Q:其余语言有没有接入spring cloud config?例如PHP和node? 开发人员对微服务进行开发时,是用dockercompose吧服务都起起来,仍是要接入公共的rancher?
A:咱们没有使用spring-cloud-config,目前有接入层的node服务在使用consul下发配置。开发时本机跑本身的服务,连Rancher环境的Consul。
Q:关于领域事件,若是本地事务提交后,下游的服务报错,是否只能在业务层面再发起一个补偿的事件,让本地事务达到最终一致性呢?
A:若是下游服务报错,那么事件不会被消费。会以退避重试的方式重发事件。
Q:分享很棒,请问大家的docker的部署是基于原生的docker和swarm,仍是kubernetes来作的?
A:谢谢,咱们使用Rancher来管理集群。没选Kubernetes的缘由是由于团队资源有限,Swarm最初试过,调度不够完善。后来Docker 1.12之后的Swarmkit应该是更好的选择。
Q:微服务开发测试用例相比于单体应用是否是更复杂一些?大家是怎样保证测试覆盖率的?
A:事实上对于单元测试来说,反而更容易进行了。由于系统拆分以后,把原来很难测试的一些节点给疏通了。
Q:你好请教一下,当微服务之间的依赖关系比较多,且层次比较深时,服务的启动,中止,以及升级之间的关系如何处理?
A:目前还几乎没出现过须要完全冷启动的状况。并且启动服务时并不须要依赖服务也启动,只须要发生业务时,依赖服务启动便可。
Q:有个问题, zuul作api网关时如何配置consul呢? 文章中貌似没交待清楚
A:在网关服务中经过@EnableZuulProxy来启用Zuul反向代理consul中已注册的服务。也能够同时经过配置来自定义,详见http://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/1.2.4.RELEASE/文档中的Embedded Zuul Reverse Proxy章节。