更多干货文章浏览

微服务是否适宜小团队是个见仁见智的问题。

回归景象看实质,随着业务复杂度的进步,单体利用越来越宏大,就如同一个类的代码行越来越多,分而治之,切成多个类应该是更好的解决办法,所以一个宏大的单体利用分出多个小利用也更合乎这种分治的思维。

当然微服务架构不应该是一个小团队一开始就该思考的问题,而是缓缓演变的后果,审慎适度设计尤为重要。

公司的背景是提供SaaS服务,对于大客户也会有定制开发以及私有化部署。通过2年不到的工夫,技术架构经验了从单体到微服务再到容器化的过程。

单体利用时代

晚期开发只有两个人,思考微服务之类的都是多余。不过因为受前公司影响,最后就决定了前后端拆散的路线,因为不须要思考SEO的问题,索性就做成了SPA单页利用。

多说一句,前后端拆散也不肯定就不能服务端渲染,例如电商零碎或者一些匿名即可拜访的零碎,加一层薄薄的View层,无论是php还是用Thymeleaf都是不错的抉择。

部署架构上,咱们应用Nginx代理前端HTML资源,在接管申请时依据门路反向代理到server的8080端口实现业务。

图片

接口定义

接口依照规范的Restful来定义,

  • 版本,对立跟在 /api/前面,例如 /api/v2
  • 以资源为核心,应用复数表述,例如/api/contacts,也能够嵌套,如/api/groups/1/contacts/100
  • url中尽量不应用动词,实际中发现做到这一点真的比拟难,每个研发人员的思路不统一,起的名字也千奇百怪,都须要在代码Review中笼罩。
  • 动作反对,POST / PUT / DELELE / GET ,这里有一个坑,PUT和PATCH都是更新,然而PUT是全量更新而PATCH是局部更新,前者如果传入的字段是空(未传也视为空)那么也会被更新到数据库中。目前咱们尽管是应用PUT然而疏忽空字段和未传字段,实质上是一种局部更新,这也带来了一些问题,比方确有置空的业务须要非凡解决。
  • 接口通过swagger生成文档供前端共事应用。

继续集成(CI)

团队初始成员之前都有在大团队共事的经验,所以对于品质管控和流程治理都有一些独特的要求。因而在开发之初就引入了集成测试的体系,能够间接开发针对接口的测试用例,对立执行并计算覆盖率。

一般来说代码主动执行的都是单元测试(Unit Test),咱们之所以叫集成测试是因为测试用例是针对API的,并且蕴含了数据库的读写,MQ的操作等等,除了内部服务的依赖根本都是合乎实在生产场景,相当于把Jmeter的事件间接在Java层面做掉了。

这在开发初期为咱们提供了十分大的便利性。但值得注意的是,因为数据库以及其余资源的引入,数据筹备以及数据清理时要思考的问题就会更多,例如如何管制并行任务之间的测试数据互不影响等等。

为了让这一套流程能够自动化的运作起来, 引入Jenkins也是天经地义的事件了。

图片

开发人员提交代码进入gerrit中,Jenkins被触发开始编译代码并执行集成测试,实现后生成测试报告,测试通过再由reviewer进行代码review。在单体利用时代这样的CI架构曾经足够好用,因为有集成测试的笼罩,在放弃API兼容性的前提下进行代码重构都会变得更有信念。

微服务时代

服务拆分准则

从数据层面看,最简略的形式就是看数据库的表之间是否有比拟少的关联。例如最容易拆散的一般来说都是用户治理模块。如果从畛域驱动设计(DDD)看,其实一个服务就是一个或几个相关联的畛域模型,通过大量数据冗余划清服务边界。

单个服务内通过畛域服务实现多个畛域对象合作。当然DDD比较复杂,要求畛域对象设计上是充血模型而非贫血模型。

从实际角度讲,充血模型对于大部分开发人员来说难度十分高,什么代码应该属于行为,什么属于畛域服务,很多时候十分考验人员程度。

服务拆分是一个大工程,往往须要几个对业务以及数据最相熟的人一起探讨,甚至要思考到团队构造,最终的成果是服务边界清晰, 没有环形依赖和防止双向依赖。

框架抉择

因为之前的单体服务应用的是spring boot,所以框架天然而的抉择了spring cloud。其实集体认为微服务框架不应该限度技术与语言,但生产实践中发现无论dubbo还是spring cloud都具备侵入性,咱们在将nodejs利用融入spring cloud体系时就发现了许多问题。兴许将来的service mesh才是更正当的倒退路线。

图片

这是典型的Spring Cloud的应用办法

  • zuul作为gateway,散发不同客户端的申请到具体service
  • erueka作为注册核心,实现了服务发现和服务注册
  • 每个service包含gateway都自带了Hystrix提供的限流和熔断性能
  • service之间通过feign和ribbon相互调用,feign实际上是屏蔽了service对erueka的操作

上文说的一旦要融入异构语言的service,那么服务注册,服务发现,服务调用,熔断和限流都须要本人解决。

再有对于zuul要多说几句,Sprin Cloud提供的zuul对Netflix版本的做了裁剪,去掉了动静路由性能(Groovy实现),另外一点就是zuul的性能个别,因为采纳同步编程模型,对于IO密集型等后盾解决工夫长的链路非常容易将servlet的线程池占满,所以如果将zuul与次要service搁置在同一台物理机上,在流量大的状况下,zuul的资源耗费十分大。

理论测试也发现通过zuul与间接调用service的性能损失在30%左右,并发压力大时更为显著。当初spring cloud gateway是pivotal的主推了,反对异步编程模型,后续架构优化兴许会采纳,或是间接应用Kong这种基于nginx的网关来提供性能。当然同步模型也有长处,编码更简略,后文将会提到应用ThreadLocal如何建设链路跟踪。

架构革新

通过大半年的革新以及新需要的退出,单体服务被一直拆分,最终造成了10余个微服务,并且搭建了Spark用于BI。初步造成两大体系,微服务架构的在线业务零碎(OLTP) + Spark大数据分析系统(OLAP)。数据源从只有Mysql减少到了ES和Hive。多数据源之间的数据同步也是值得一说的话题,但内容太多不在此文赘述。

图片

自动化部署

与CI比起来,继续交付(CD)实现更为简单,在资源有余的状况咱们尚未实现CD,只是实现执行了自动化部署。

因为生产环境须要通过跳板机操作,所以咱们通过Jenkins生成jar包传输到跳板机,之后再通过Ansible部署到集群。

图片

简略粗犷的部署形式在小规模团队开发时还是够用的,只是须要在部署前保障测试(人工测试 + 自动化测试)到位。

链路跟踪

开源的全链路跟踪很多,比方spring cloud sleuth + zipkin,国内有美团的CAT等等。其目标就是当一个申请通过多个服务时,能够通过一个固定值获取整条申请链路的行为日志,基于此能够再进行耗时剖析等,衍生出一些性能诊断的性能。不过对于咱们而言,首要目标就是trouble shooting,出了问题须要疾速定位异样呈现在什么服务,整个申请的链路是怎么的。

为了让解决方案轻量,咱们在日志中打印RequestId以及TraceId来标记链路。RequestId在gateway生成示意惟一一次申请,TraceId相当于二级门路,一开始与RequestId一样,但进入线程池或者音讯队列后,TraceId会减少标记来标识惟一条门路。

举个例子,当一次申请会向MQ发送一个音讯,那么这个音讯可能会被多个消费者生产,此时每个生产线程都会本人生成一个TraceId来标记生产链路。退出TraceId的目标就是为了防止只用RequestId过滤出太多日志。实现如图所示,

图片

简略的说,通过ThreadLocal寄存APIRequestContext串联单服务内的所有调用,当跨服务调用时,将APIRequestContext信息转化为Http Header,被调用方获取到Http Header后再次构建APIRequestContext放入ThreadLocal,反复循环保障RequestId和TraceId不失落即可。如果进入MQ,那么APIRequestContext信息转化为Message Header即可(基于Rabbitmq实现)。

当日志汇总到日志零碎后,如果呈现问题,只须要捕捉产生异样的RequestId或是TraceId即可进行问题定位

图片

运维监控

在容器化之前,采纳telegraf + influxdb + grafana的计划。telegraf作为探针收集jvm,system,mysql等资源的信息,写入influxdb,最终通过grafana做数据可视化。spring boot actuator能够配合jolokia裸露jvm的endpoint。整个计划零编码,只须要花工夫配置。

容器化时代

架构革新

因为在做微服务之初就打算了容器化,所以架构并未大动,只是每个服务都会建设一个Dockerfile用于创立docker image

图片

波及变动的局部包含:

  1. CI中多了构建docker image的步骤
  2. 自动化测试过程中将数据库降级从利用中剥离独自做成docker image
  3. 生产中用k8s自带的service代替了eruka

理由下文一一道来。

Spring Cloud与k8s的交融

咱们应用的是Redhat的Openshift,能够认为是k8s企业版,其自身就有service的概念。一个service下有多个pod,pod内即是一个可服务单元。service之间相互调用时k8s会提供默认的负载平衡管制,发动调用方只须要写被调用方的serviceId即可。这一点和spring cloud fegin应用ribbon提供的性能一模一样。

也就是说服务治理能够通过k8s来解决,那么为什么要替换呢?其实上文提到了,Spring Cloud技术栈对于异构语言的反对问题,咱们有许多BFF(Backend for Frontend)是应用nodejs实现的,这些服务要想交融到Spring Cloud中,服务注册,负载平衡,心跳查看等等都要本人实现。

如果当前还有其余语言架构的服务退出进来,这些轮子又要重造。基于此类起因综合考量后,决定采纳Openshift所提供的网络能力替换eruka。

因为本地开发和联调过程中仍然依赖eruka,所以只在生产上通过配置参数来管制,

eureka.client.enabled 设置为 false,进行各服务的eureka注册
ribbon.eureka.enabled 设置为 false,让ribbon不从eureka获取服务列表
以服务foo为例,foo.ribbon.listofservers 设置为 http://foo:8080,那么当一个服务须要应用服务foo的时候,就会间接调用到http://foo:8080

CI的革新

CI的革新次要是多了一部编译docker image并打包到Harbor的过程,部署时会间接从Harbor拉取镜像。另一个就是数据库的降级工具。之前咱们应用flyway作为数据库降级工具,当利用启动时主动执行SQL脚本。

随着服务实例越来越多,一个服务的多个实例同时降级的状况也时有发生,尽管flyway是通过数据库锁实现了降级过程不会有并发,但会导致被锁服务启动工夫变长的问题。

从理论降级过程来看,将可能产生的并发降级变为繁多过程可能更靠谱。此外前期分库分表的架构也会使随利用启动主动降级数据库变的艰难。综合考量,咱们将降级工作做了拆分,每个服务都有本人的降级我的项目并会做容器化。

在应用时,作为run once的工具来应用,即docker run -rm的形式。并且后续也反对了设定指标版本的性能,在私有化我的项目的跨版本升级中起到了十分好的成果。

至于主动部署,因为服务之间存在上下游关系,例如config,eruka等属于根本服务被其余服务依赖,部署也产生了先后顺序。基于Jenkins做pipeline能够很好的解决这个问题。

小结

其实以上的每一点都能够深刻的写成一篇文章,微服务的架构演进波及到开发,测试和运维,要求团队内多工种严密单干。

分治是软件行业解决大零碎的不二法门,作为小团队咱们并没有自觉追新,而是在倒退的过程通过服务化的形式解决问题。

从另一方面咱们也领会到了微服务对于人的要求,以及对于团队的挑战都比过来要高要大。将来仍需摸索,演进仍在路上。

(感激浏览,心愿对你所有帮忙)