一 概述
关于微服务的介绍目前已经有很多文章做了介绍,本文不再对微服务的概念再做进一步阐述,重点将介绍微服务架构具体开发运维方面的经验总结,侧重于落地实践。
目前业界比较热门的微服务开发框架是SpringCloud和dubbo,由于前期一些项目已经使用了SpringBoot进行快速开发,自然就平滑地升级到SpringCloud进行微服务实践。另外,按照微服务不断演进的思路,我们首先对非核心业务和新业务进行了重构,不断积累微服务开发运维经验,待后续时机成熟,再对核心业务进行微服务重构治理。
二 业务梳理
2.1 业务梳理原则
和大部分企业实施微服务流程一样,首先就是确定微服务重构的原则和目标。由于证券行业的自身特点,确定了如下几个原则:
1. 安全放在首位。安全是第一位的非功能性需求,在设计之初,就应该考虑到,涉及服务器、容器、应用、接口、数据等层面。
2. 核心业务与普通业务分离。交易和行情服务是券商交易APP的核心业务,应该和其他业务进行有效隔离,避免其他应用对核心业务产生任何影响。例如一个运营活动的高峰流量,就不应该对核心交易行情服务产生任何影响。
3. 按照业务和主数据进行合理拆分。可以按照各类业务模块来进行服务拆分,例如资讯、理财、增值服务、业务办理、账户账单等,每个业务模块自然拆分成一个微服务组件。另外,按照主数据属性来分类也是一个不错的维度,例如在CRM中的各类数据维度,可以按照主数据属性来进行划分,例如资产类、账户类、交易流水类等。
2.2 业务梳理流程
按照上述拆分原则进行划分,原有的单体应用已经拆分成多个微服务业务集群。如下图1所示。除了按照业务划分的业务组件之外,还有一些通用的功能性组件和非功能性组件,例如统一权限校验、日志统一处理服务、报警服务等,将在后续详细说明。
图一 微服务业务梳理组件分布图
三 技术架构实现
**
SpringCloud的总体组件架构图如下图2所示。
图2 SpringCloud微服务技术架构组件图
开箱即用的特性使得SpringCloud比较容易上手,需要哪个功能,就通过maven引入响应子系统组件,符合各个层次的使用者,也符合各类不同应用场景特点。微服务改造是一个渐变的过程,不必一开始就使用所有功能。按照目前自身的技术条件与保障能力,同时参照应用自身特点,我们使用了如下几类组件,下面分别进行详细说明。
3.1 服务注册与发现:Eureka集群
目前SpringCloud支持的服务注册有Consul和Eureka。Consul需要独立部署,脱离Spring框架;而Eureka天然集成在SpringCloud中,本身就是一个SpringBoot项目。这2者特点可以通过如下表1展示。
表1 Consul和Eureka对比表
可以看出,Consul在整体功能上比Eureka更加丰富和完备,但同时也增加了一定的复杂性;Eureka相对来说更加轻量,天然溶于SpringCloud体系,虽然缺失一部分功能,但是对于一般性的微服务集群来说已经足够。因此,我们采用了Eureka来作为服务发现与注册组件。下图为目前已经接入Eureka组件的所有拆分的微服务实例。目前来看,数量还不算太多,后期随着业务的发展,会不断增加微服务实例。如下图3所示:
图3 Eureka管理视图
实践经验
从业务量上来说,一个数据中心站点上,一般挂载2个实例做负载均衡即可满足一般业务需求,但在一些请求量较大的服务上,我们会增加微服务的实例数量。例如对于我们的一个行情增值微服务组件,由于早高峰tps接近6000,就挂载了6个实例。因此通过实际业务量的请求数据监控,我们可以动态调配微服务组件的挂载实例,从而满足业务需求,保障服务的HA,同时提高了系统资源的使用效率。
对于Eureka服务本身来说,也是挂载了2个实例,通过客户端的Failover协议同时配置2个实例,来实现服务注册的高可用。需要在每个注册到Eureka集群中微服务组件均增加此配置即可:eureka.client.serviceUrl.defaultZone=http://ip1:20001/eureka/,http://ip2:20001/eureka/。
如果超过2个Eureka实例,则通过继续以“,”分隔连上。这点来说,和ActiveMQ的连接使用异曲同工。
从我们实际运行来看,目前Eureka组件应用逻辑简单、稳定。当一台Eureka Server实例down机后,所有client实际均不受影响。当然从Eureka自身来说,也存在弱一致性的问题,特别是对于微服务节点在重启时候,可能会存在某服务在一个心跳周期不可用的情况,待心跳超时隔离以后,就恢复正常了。其实这也是为了最大程度保证可用性,可见确实只满足了AP。官方目前Eureka2.X版本将不再开源,基于此考虑,后续我们也会持续关注替代方案Consul,或者其他服务注册框架。
3.2 统一接入网关:Zuul
所谓“一夫当关,万夫莫开”,统一接入网关组件作为服务的统一入口,地位足够重要。Zuul网关主要实现了请求服务路由、负载均衡、统一校验等功能。在服务路由和负载均衡方面,和nginx相当类似。Zuul通过自身注册到Eureka,通过url匹配的serviceID配置,即可实现服务路由。
目前我们在Zuul网关中实现了校验token功能,通过ZuulFilter,可以轻松做到所有外部请求的token统一校验。目前还没有把其他校验类逻辑放到Zuul中,一些请求流量统计的功能,我们在nginx层面做了拦截实现。
3.2.1 Zuul1 or Zuul2,同步or异步?
Zuul2版本已经与今年5月份发布,相比于Zuul1,其强大的事件驱动和异步非阻塞调用特性,将支持超高并发接入并转发。其核心是引入了高性能netty框架,从请求接入到调用外部组件,全部采用netty事件驱动模式来实现。而Zuul1则采用传统servlet同步请求的方式进行处理,每个连接分配一个线程,所有的线程均从线程池中获取,一旦外部连接并发量很大的话,线程数将急剧上升,一旦处理线程阻塞,则最终将耗尽容器中的线程池内的线程,造成容器无法继续接受新的请求,因此才有了Hystrix熔断组件来解决服务耗尽资源的解决方案。
虽然Zuul1的同步阻塞带来了吞吐量的瓶颈,但是整个处理模块比较简单,从请求接入到处理,再到响应,整个流程都在一个线程中处理,方便了开发和调试跟踪。而Zuul2则将接入请求全部放入netty的事件队列中,并分别由不同的线程进行接入、处理、响应,从维护和跟踪上面来看,更加复杂和难维护。
其实对于一般的应用场景来说,并发量不会太大,其实用Zuul1就能够满足要求,而且开发维护更简单;但是如果面对高并发的应用场景,例如QPS>1000时,则建议采用Zuul2,其异步非阻塞特性,将轻松接入高并发连接,并配合其EventLoop和pipeline机制,可以保证所有的连接都能被接收和处理,基本不存在连接被拒绝访问的情况。
从Netflix给出的性能结论来看,Zuul2的性能比Zuul1大致提升了20%,可见从吞吐量上面来说,提升能力有限;但是比较明确的是,Zuul2的连接数管理方面要明显好于Zuul1,可以支持超大规模的并发连接处理。从这点上面来说,其性能应该接近于Nginx。可参考如下Zuul演进图4.
图4 从Zuul1到Zuul2演进图
3.2.2 实践经验
目前实际项目中,生产系统还是以Zuul1来作为API网关使用。因为目前我们只是对于内部系统API、外部一些核心低频API调用走Zuul,并发量不太大,还能扛住;对于外部应用的一些高并发API调用,例如行情相关的接口QPS>6000,还是从Nginx接入,直接转发到SpringBoot微服务组件。
从未来趋势来看,为了实现微服务的全覆盖和完整性,后期将把nginx的接入请求全部转到Zuul上来,到时将必须升级到Zuul2上来,没的说。
3.3 服务RPC调用:Feign
**
Feign是一种http方式的声明式调用,目前SpringCloud默认使用java原生的HttpURLConnection进行http调用,同时通过jar包引入还可以支持Apache的httpclient、OKhttp等。Feign底层依靠Ribbon实现了调用各个实例的负载均衡,从效率上面来说,不占太大优势,但是从简单易用上面来看绝对是一个重要的亮点;而且和自动熔断降级组件Hystrix天热融合,优势明显。使用Feign非常简单,只需要创建一个调用服务的接口方法,标记@FeignClient即可像本地方法一样调用。
3.3.1 自动降级熔断:Hystrix
在微服务集群中通常会有很多个服务之间的调用,例如通过Feign调用。各个服务之间的调用最终形成了网状模型。如果某一些基础服务不可用,而上游服务持续不断调用此基础服务,可能导致整个系统故障,也就是“雪崩效应”。下图5展示了常见的场景。
图5 Hystrix服务降级示例图
3.3.2 断路器机制
**
断路器很好理解,当Hystrix Command请求后端服务失败数量超过一定比例(默认50%),断路器会切换到开路状态(OPEN)。这时所有请求会直接失败而不会发送到后端服务。断路器保持在开路状态一段时间后(默认5秒),自动切换到半开路状态(HALF-OPEN)。这时会判断下一次请求的返回情况,如果请求成功, 断路器切回闭路状态(CLOSED),否则重新切换到开路状态(OPEN)。Hystrix的断路器就像我们家庭电路中的保险丝,一旦后端服务不可用,断路器会直接切断请求链,避免发送大量无效请求影响系统吞吐量,并且断路器有自我检测并恢复的能力。
3.3.3 Fallback
Fallback相当于是降级操作。对于查询操作,我们可以实现一个fallback方法,当请求后端服务出现异常的时候,可以使用fallback方法返回的值。 fallback方法的返回值一般是设置的默认值或者来自缓存。例如在一个热门推荐股票榜单功能中,如果Feign调用失败,则自动在Fallback中返回本地缓存中的默认榜单即可,成功实现了推荐榜单服务的降级。
3.3.4 资源隔离
**
在Hystrix中,主要通过线程池来实现资源隔离。通常在使用的时候我们会根据调用的远程服务划分出多个线程池。例如调用产品服务的Command放入A线程池,调用账户服务的Command放入B线程池。这样做的主要优点是运行环境被隔离开了。就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响。但是带来的代价就是维护多个线程池会对系统带来额外的性能开销。如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话,可以使用Hystrix的信号量(Semaphores)来隔离资源。
例如,在我们实际生产系统中,如果服务调用不太频繁时,减少线程数量,节约系统资源,可以在配置文件中显示设置隔离线程池的线程规模=2:hystrix.threadpool.default.coreSize=2。
3.3.5 实践经验
**
由于Feign组件集成了Ribbon和Hystrix,其服务降级和熔断的参数配置较为复杂,如果不看完源码,则只能靠系统测试来一一验证了。例如服务调用超时这类场景,其默认的服务超时设置为1秒,这对一些外部较复杂的服务调用来说是不太合适的,因此需要在配置文件中显式声明hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000,将其扩大到5秒超时。另外对于服务的重试次数,也需要进行谨慎设置,一旦被调用方服务没有做好幂等性防护,可能会带来意想不到的破坏。
3.4 接口调用链分析监控:Zipkin
Zipkin 是一个开放源代码分布式的跟踪系统,由Twitter公司开源,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。每个服务向zipkin报告计时数据,zipkin会根据调用关系通过Zipkin UI生成依赖关系图,显示了多少跟踪请求通过每个服务,该系统让开发者可通过一个 Web 前端轻松的收集和分析数据,例如用户每次请求服务的处理时间等,可方便的监测系统中存在的瓶颈。
Zipkin提供了可插拔数据存储方式:In-Memory、MySql、Cassandra以及Elasticsearch。下图6为我们实际场景中的一个接口的调用链分析。
上方的图形展示了3个微服务之间的调用关系;下方的甘特图则展示了调用链的各个服务耗时分布。对于分析调用链层次多的复杂接口具有很好的分析作用。
实践经验**
在我们实际系统中,对于接口的采样率有个配置:spring.sleuth.sampler.percentage=0.2,表示只采样20%的接口入库存储可展示,对于接口请求量巨大的应用来说,避免接口量太大消耗太多内存。
3.5 应用服务监控SpringBootAdmin
**
SpringBootAdmin提供了开箱即用的监控工具箱,可以看各个注册在Eureka下个微服务实例的运行全貌,如下图7所示。首先在面板上面展示了所有能监控到的应用实例,具体到IP和端口。Status包括UP和DOWN,分别表示正常启动和未启动。
图7 SpringBootAdmin Dashboard
点击detail可以进入应用的详细监控页面。在详情页面概览中包含了应用的全貌信息,包括JVM占用的内存情况、类加载、GC、DataSource数据库连接占用情况快照,如下图8所示。还提供有实时刷新功能,每隔一秒动态展示系统的快照,比JavaVisualVM和jConsole更加方便和直观。我们用的较多的是DataSource连接数占用情况分析,如果Active连接数长期占用率较高,就必须考虑应用对数据库的优化了。
图8 SpringBootAdmin 应用内全貌图
在监控详情页面中,还可以看到Metrics、Environment、Logging、JMX、Threads、Trace、Heapdump几个栏目。
3.5.1 Metrics
Metrics主要展示了接口调用次数和耗时统计(Counter和Gauges),便于查看微服务内部接口的运行情况。如下图9所示。
图9 SpringBootAdmin 应用内Metrics图
3.5.2 Environment
主要提供该服务的所有配置项。包括系统层级和应用的配置列表。
3.5.3 Logging
提供动态改变应用log打印级别的功能。在系统出现异常而没有查到有效日志的情况下,可以动态调整logging的日志级别,例如调整为debug级别,可以让应用立即以debug级别进行日志打印,方便查看系统详细异常情况。其实底层还是使用了JMX实现,需要在logback配置中增加jmx支持配置,如下图10所示。
图10 SpringBootAdmin 应用内Logging图
3.5.4 JMX
JMX栏目将显示应用所有JMX对象,如果有一些自定义的JMX MBean,可以通过此处操作动态执行操作,实现应用配置的动态热变更,如下图11所示。
图11 SpringBootAdmin 应用内JMX图
3.5.5 Threads
主要提供当前线程堆栈的快照信息。通过查看线程全貌快照,可以看到当前所有线程的运行状态,如果发现某类线程组全部都属于running,可能需要扩容线程池。如果发现大部分线程高峰期一直都是waiting状态,可能需要缩减线程池规模。如果发现大部分线程都在等待某一类资源对象锁,则表示该类资源存在瓶颈,需要进行优化调整。合理的调整各类线程池规模,可以让系统运行的更高效,提高系统资源利用率,如下图12所示。
图12 SpringBootAdmin 应用内Thread快照图
3.5.6 Trace
主要提供对外接口的实时运行快照。在此不做详细说明。
3.5.7 Heapdump
**
主要提供了实时获取内存堆栈快照的下载功能。如果发现应用占用内存一直很高,GC释放效率低,则可以通过查看内存堆栈快照,作进一步分析,哪些大对象没有及时释放。
四 敏捷开发运维保障
4.1 运维保障
微服务的正常运转离不开配套的监控体系和敏捷DevOps,大量的微服务实例需要自动化的监控和运维的支持。如下图13为最终微服务系统的部署图。
图13 微服务部署架构图
对于部署的微服务来说,实现了中间件和数据库的完全独立,大部分微服务组件都拥有独立的数据库和缓存资源。当然有些缓存可能存在公用的情况,这个具体场景具体分析。目前部署的环境还是VM,后续为了提高部署速度和资源动态升缩能力,会尝试用docker来进行替代。
目前对于服务器资源的监控时zabbix,对于微服务应用的监控是SpringBootAdmin,另外对于接口API的监控我们还自研了一套接口可用性监控系统,基本上覆盖了监控的大部分需求。从架构演进角度来看,后续监控会引入Prometheus+Grafana结合使用。
4.2 持续集成CI & 持续部署CD
**
微服务的快速部署和迭代,需要持续集成CI与持续部署CD的支持。目前在测试环境已经实现了CI与CD的全流程自动化运行,由于公司网络的监管要求,目前生产系统网段无法直接连通代码仓库,因此目前CI与CD其实是分离的,当前仍然需要人工去做部署包的上传,如下图14所示。
图14 CI&CD现状图
目前已经在jekins上实现了大部分的微服务实例的持续部署,大大提升了部署的效率和准确性。而且通过Jenkins上面的部署历史,也实现了投产的审计和可追溯,提升投产管理水平,如下图15所示。
图15 Jenkins自动部署图
五 总结与展望
**
从目前微服务的搭建和治理来看,整体还处于一个入门级水准,但考虑到我们实际的技术水平和保障能力,也无法一步做到很完善的规模。微服务治理是不断演进的一个过程,正如任何系统架构一样,永远没有最完美的,只有最合适的。
目前整套系统只是使用了SpringCloud中的很小一部分组件,后续可能会增加统一配置中心ConfigServer,但其前提是生产系统网络需要开通到Git库访问,仍需要和机房部门沟通。考虑到目前我们的服务器还是实体机+VM组合,后续仍然需要引入docker容器技术,为以后迁移到云平台做好准备。
本文主要从实际开发运维的角度阐述了基于SpringCloud的微服务体系,希望可以为各位同行快速搭建微服务系统提供一些参考和帮助。