快递中心的spring cloud实践

徐键

背景

随着微服务的流行,基于spring生态的spring cloud越来越被关注, Spring Cloud专注于提供良好的开箱即用的典型用例和可扩展性覆盖机制。

特性

  • 分布式/版本化配置
  • 服务注册和发现
  • 路由
  • service - to - service调用
  • 负载均衡
  • 断路器

Spring cloud包含两个基本模块,spring cloud context和spring cloud commons。

spring cloud context即spring cloud应用上下文,包含引导上下文(加载bootstrap配置)作为主应用程序父上下文、配置加密解密(使用java jce加密配置文件,使用EnvironmentDecryptApplicationInitializer类在应用初始化时解密配置)、配置刷新范围(RefreshScope)、控制端点(/env/reset,/refresh,/restart等)功能。

Spring cloud commons 提供服务注册(使用ServiceRegistry抽象),服务发现(使用DiscoveryClient抽象,通过@EnableDiscoveryClient开启),负载均衡(使用LoadBalancerClient抽象,通过@LoadBalanced启用),断路器(通过@EnableCircuitBreaker开启)等模式的一个共用抽象层,为具体现实提供统一抽象。

实际应用过程中,我们使用了

  • spring cloud consul作为服务注册和发现组件
  • spring cloud config作为分布式/版本化配置管理
  • spring cloud bus作为消息总线用于刷新分布式应用配置
  • spring boot admin作为统一的应用监控后台
  • spring cloud Netflix feign 作为rest服务调用client
  • spring cloud Netflix zuul 作为路由、过滤网关
架构图

使用情况

spring boot应用已经普及,以之为基础的spring cloud也是作为微服务框架快速在迭代发展,公司中的快递业务以及商家业务,对接业务都接入了相关服务,相对于dubbo,spring cloud使用更为简单方便,http服务更偏应用层,而服务调用可以使用线程池、异步调用、连接池、io多路复用等技术,在高并发性能上也很强大。

spring cloud consul

Consul 是使用Go开发的开源的服务发现和健康检查的工具.Consul还提供K/V存储.可以很方便的使用Consul集群搭建高可用的微服务基础架构. Consul还包含web ui 可以查看各服务状态.

如下截图:

因为consul内置了服务注册发现功能,所以spring cloud consul代码实现上比较简单,使用ConsulServiceRegistry(实现ServiceRegistry)进行服务注册操作,使用ConsulDiscoveryClient(实现DiscoveryClient)获取所有服务或者up的服务,内部则是使用ConsulClient请求consul集群进行操作。

在使用过程中出现了一个问题,一个服务多个机器注册的时候有时注册的服务会被覆盖,查看源码发现,应用servlet容器初始化事件被监听之后会进行consul应用的自动注册,注册使用ConsulAutoRegistration对象,内部会有一个NewService对象,通过NewService对象的id判断服务的唯一性,而NewService的id默认是ApplicationContext对象的id,也就是应用名称加port加profile,所以同一个应用这个id是相同的所以注册会被覆盖。 找到问题了那么就来解决问题吧,增加配置bean通过覆盖ConsulAutoRegistration对象,设置id为ApplicationContext的id加机器的ip保证应用多台机器的id唯一。

@Bean
@Primary
public ConsulAutoRegistration consulRegistration(ConsulDiscoveryProperties properties,  
    ApplicationContext applicationContext, ServletContext servletContext, HeartbeatProperties heartbeatProperties) {
    properties.setInstanceId(applicationContext.getId()+"-"+ DigestUtils.sha1Hex(properties.getIpAddress()));
    return ConsulAutoRegistration.registration(properties, applicationContext, servletContext, heartbeatProperties);
}

ConsulDiscoveryProperties这个类是consul服务发现的配置类,其中一些配置需要注意,比如preferIpAddress默认为false,即默认是通过host来进行服务的地址进行注册和检查,但是当环境不支持host访问时就会出问题,比如我们线上的阿里云环境,也有机器不支持host访问,所以需要配置preferIpAddress=true。

spring cloud config

Spring Cloud Config提供服务端和客户端在分布式系统中扩展配置。支持不同环境的配置(开发、测试、生产)。使用Git做默认配置后端,可支持配置环境打版本标签。

spring cloud config架构图如下:

Spring cloud config包含4部分

  • git配置仓库 存放应用各环境配置,config server通过对应git地址、用户名、密码配置获取仓库配置文件。
  • config server提供了基于资源的HTTP接口 通过@EnableConfigServer开启配置服务端应用
# git config
spring.cloud.config.server.git.uri=http://60.191.68.43:19090/config/{application}-config.git  
spring.cloud.config.server.git.username=xxx  
spring.cloud.config.server.git.password=xxx  
spring.cloud.config.server.git.force-pull=true (这个配置可以在配置仓库冲突的时候强制拉取,而不会取不到最新的配置)  

使用如上git仓库配置,通过MultipleJGitEnvironmentRepository(继承JGitEnvironmentRepository(实现EnvironmentRepository接口))使用git api获取git仓库配置文件资源。

通过得到的配置资源以ResourceController rest方式提供配置资源,如下所示:

  • config client,通过配置的url或者服务发现拉取config server配置 Config server 通过服务发现注册后,config client可以通过服务发现找到config server,通过config server rest服务拉取配置,通过ConfigServicePropertySourceLocator(实现PropertySourceLocator)获取PropertySource加入Environment中供应用使用。

我们的config client使用consul服务发现来找到config server来拉取配置,默认ConsulDiscoveryClient获取服务的时候是根据服务id获取所有服务节点,即获取了down的服务,造成拉取配置失败,查看consul服务发现的配置类ConsulDiscoveryProperties发现默认queryPassing=false,通过配置改为true即可返回所有健康检查通过的服务。

  • spring cloud bus 分布式应用配置更新通知 Spring cloud bus支持rabbitmq和kafka,因为公司已经部署kafka,所以使用了kafka作为spring cloud bus的消息组件。
    配置如下:
#spring cloud bus kafka
spring.cloud.bus.enabled=true  
spring.cloud.bus.trace.enabled=false  
spring.cloud.stream.kafka.binder.zk-nodes=192.168.11.30:2181,192.168.11.33:2181,192.168.11.35:2181  
spring.cloud.stream.kafka.binder.brokers=192.168.11.30:9092,192.168.11.33:9092,192.168.11.35:9092  

架构如下: 这时Spring Cloud Bus做配置更新步骤如下:

  • 提交代码,利用 git 的 webhook 触发post请求给 bus/refresh
  • server端接收到请求并发送给Spring Cloud Bus
  • Spring Cloud bus接到消息并通知给其它客户端
  • 其它客户端接收到通知,请求Server端获取最新配置
  • 全部客户端均获取到最新的配置

spring boot admin

spring boot admin是spring boot应用的监控管理服务,通过spring boot actuator监控端点监控spring boot应用的各项指标,地址:http://middleware.nidianwo.com/。

分析下重点类

  • AdminServerProperties是服务配置类,以spring.boot.admin为前缀,contextPath可以设置服务的前置url,通过PrefixHandlerMapping类扩展RequestMappingHandlerMapping,在registerHandlerMethod构建RequestMappingInfo的时候给PatternsRequestCondition的patterns增加了配置的contextPath。 该类还定义了监控各个应用的间隔时间、请求超时时间、监控的endpoints列表等属性。
  • AdminServerWebConfiguration配置类定义了静态资源的ResourceHandlers,前面提到的PrefixHandlerMapping,注册了registryController,用于应用注册、获取注册的应用列表及单个应用信息。
  • AdminServerCoreConfiguration配置类定义了应用注册ApplicationRegistry类注册注销应用,StatusUpdateApplicationListener用于监控应用的状态变更,使用ThreadPoolTaskScheduler定时监控应用状态。
  • DiscoveryClientConfiguration配置类使用ApplicationDiscoveryListener监听器通过服务发现DiscoveryClient获取的服务列表使用ApplicationRegistry注册应用,通过内部的部的map存储或者使用Hazelcast分布式存储。
  • RevereseZuulProxyConfiguration配置类定义了zuul相关的配置类,ApplicationRouteLocator扩展zuul的RouteLocator对各个应用的spring boot actuator endpoint进行路由请求。

界面如下: 可以通过界面查看应用的健康状况,内存磁盘使用,java gc情况,配置,日志,thread dump heap dump等各项应用数据。 界面如下:

通过日志的实时监控,可以方便观察应用启动状况,以及实时请求的日志,方便调试。

spring cloud netfix feign

spring cloud netflix包含了很多非常有用的微服务组件,比如服务发现(Eureka),断路器(Hystrix),智能路由(Zuul)和客户端负载均衡(Ribbon),声明式服务调用client(feign)

feign的使用非常简单

*应用main类增加注解@EnableFeignClients(basePackages = "com.dianwoba.open.express.service"),

*添加服务调用接口类:

@FeignClient(name = "heimdall", url="${express.open.url}",
        configuration = RemoteConfiguration.class)
public interface RemoteExpressService {

    //@Headers({"Content-Type: application/x-www-form-urlencoded","Accept: application/x-www-form-urlencoded"})
    @RequestMapping(method = RequestMethod.POST, value = "/express/api/v1/addModifySite")
    @ResponseBody
    ApiResponse addModifySite(SiteDTO siteDTO);
}

Feign内部通过几个组件进行请求的封装、调用和响应的解析。Contract实现上面接口方法的注解的解析以及请求的包装,Encoder实现上面接口方法参数的解析以及请求的组装,Feign.Builder实现核心类feign对象的建造器,Decoder实现响应的解析生成返回对象,Logger实现日志记录方式。这些接口都有默认实现,通过一个配置类配置了默认实现,默认实现为:

  • Decoder feignDecoder: ResponseEntityDecoder(这是对SpringDecoder的封装)
  • Encoder feignEncoder: SpringEncoder
  • Logger feignLogger: Slf4jLogger
  • Contract feignContract: SpringMvcContract
  • Feign.Builder feignBuilder: HystrixFeign.Builder

程序实现过程如下:

  • 首先通过@EnableFeignCleints注解开启FeignCleint
  • 根据Feign的规则实现接口,并加@FeignCleint注解
  • 程序启动后,会进行包扫描,扫描所有的@ FeignCleint的注解的类,并将这些信息注入到ioc容器中。
  • 当接口的方法被调用,FeignClientFactoryBean通过jdk代理生成接口实例,来生成具体的RequesTemplate
  • RequesTemplate在生成Request
  • Request交给Client去处理,其中Client可以是HttpUrlConnection、HttpClient也可以是Okhttp
  • 最后Client被封装到LoadBalanceClient类,这个类结合类Ribbon做到了负载均衡。

spring cloud netfix zuul

zuul作为微服务网关,具有动态路由和过滤器链功能,基于zuul可以实现:

  • 认证&鉴权
  • 数据统计
  • 服务路由
  • 协助单点压测
  • 限流
  • 静态响应

Zuul通过ZuulhanderMapping实现了SrpingMVC的AbstractUrlHandlerMapping,通过RouteLocator获取Route列表,映射对应route的fullPath到ZuulController,ZuulController继承了ServletWrappingController,会把对应请求代理到ZuulServlet,而ZuulServlet是zuul过滤器链的入口,过滤器链分三种类型,分别是pre、route、post分别对应请求执行前,请求路由执行,请求执行返回的操作,可以自定义各种过滤器实现特定需求。
解析下zuul的关键类:

  • ZuulProperties是zuul的配置类,定义了prefix、stripPrefix、retryable等属性,表示请求前缀,忽略前缀、是否重试等,关键配置是routes,通过service的一个字符串对应到service url、serviceId等配置。
  • ZuulConfiguration对应配置类的routes定义,通过SimpleRouteLocator将配置的routes解析使用AbstractUrlHandlerMapping的registerHandlers将对应path映射到ZuulController中处理, 通过@EnableZuulServer启用,这种方式对应静态路由。
  • ZuulProxyConfiguration通过DiscoveryClientRouteLocator实现动态应用的路由加载,通过pre类型的filter PreDecorationFilter利用服务发现解析对应routes,通过route类型filter RibbonRoutingFilter 实现负载均衡的路由请求,通过@EnableZuulProxy启用,这种方式对应动态路由。

使用非常简单

  • Spring boot启动类增加注解@EnableZuulProxy开启zuul代理功能。
  • 配置 zuul.routes.express.path=/express/**
    zuul.routes.express.serviceId=express-open-api
    zuul.routes.express.stripPrefix=false
    请求的不同path会路由到不同的服务,应用中我们使用zuul做了统一的api鉴权,参数处理,服务路由等功能。

一些体会

spring cloud 微服务组件使用感觉比较简单方便,通常是增加starter依赖,开启开关配置,启动类开启注解就能使用,使用过程中可能会遇到一些小问题,不过spring boot以及spring cloud社区非常活跃,版本迭代非常快,问题应该比较好解决,自己也可以看看源码通过解决问题来学习相关设计思想。