当前位置: 首页 > 科技观察

小团队如何应用大公司正在玩的微服务?

时间:2023-03-16 00:35:10 科技观察

微服务是否适合小团队,见仁见智。回归现象取决于本质。随着业务复杂度的增加,单体应用变得越来越大,就像一个类,代码行越来越多。分而治之,切入多个类应该是更好的解决方案。因此,将一个巨大的单体应用划分为多个小应用,更符合这种分而治之的思想。当然,微服务架构不应该是一个小团队一开始就应该考虑的问题,而是逐渐演进的结果。特别重要的是要小心过度设计。公司背景是提供SaaS服务,也会有针对大客户的定制开发和私有化部署。经过不到2年的时间,技术架构经历了从单体到微服务再到容器化的过程。单体应用时代的早期开发只有两个人,考虑微服务之类的是多余的。但受前公司的影响,初步定下了前后端分离的路线。因为不需要考虑SEO的问题,所以简单的做了一个SPA单页应用。还有一点,前后端分离并不一定意味着服务端渲染不行,比如电商系统或者一些可以匿名访问的系统,加一个薄的View层,不管是PHP或Thymeleaf是一个不错的选择。在部署架构上,我们使用Nginx代理前端HTML资源,接收请求时,根据路径反向代理到Server的8080端口实现业务。接口定义接口按照标准Restful:version定义,统一后跟/api/,比如/api/v2。以资源为中心,使用复数形式,如/api/contacts,也可以嵌套,如/api/groups/1/contacts/100。尽量不要在url中使用动词。在实践中发现,要做到这一点确实很难。各个开发者的思路不一致,名字也千奇百怪,都需要在codereview中覆盖。Action支持,POST/PUT/DELELE/GET,这里有个坑,PUT和PATCH都是更新,但是PUT是全量更新,PATCH是部分更新。如果前者传入的字段为空(不传则视为空)那么也会在数据库中进行更新。虽然我们目前使用PUT但是忽略空字段和未传输字段,本质上是局部更新,也会带来一些问题。比如确实有空白服务需要特殊处理。接口通过swagger生成文档供前端同事使用。持续集成(CI)团队的初始成员都有过在大型团队中一起工作的经验,因此他们对质量控制和过程管理有一些共同的要求。因此在开发之初就引入了集成测试系统,可以直接针对接口开发测试用例,统一执行并计算覆盖率。一般来说,代码的自动执行就是单元测试(UnitTest)。之所以叫集成测试,是因为测试用例是针对API的,包括数据库的读写,MQ的操作等。除了对外部服务的依赖,基本符合实际生产场景,相当于直接在Java层面把Jmeter干掉了。这为我们在开发初期提供了极大的便利。但值得注意的是,由于数据库等资源的引入,在数据准备和数据清洗上会出现更多需要考虑的问题,比如如何控制并行任务之间测试数据的相互影响等。为了自动化这套流程,引入Jenkins是理所当然的事。开发者将代码提交到Gerrit中,Jenkins被触发编译代码并进行集成测试。完成后生成测试报告,测试通过后reviewer进行codereview。在单体应用时代,这样的CI架构已经足够好了。由于集成测试的覆盖,在保持API兼容性的同时进行代码重构将变得更有信心。微服务时代的服务拆分原则从数据层面来说,最简单的方法就是看数据库中表之间的关系是不是比较少。比如最容易分离的一般是用户管理模块。从领域驱动设计(DDD)的角度来看,一个服务其实就是一个或几个关联的领域模型,通过少量的数据冗余来划定服务边界。多个域对象之间的协作是通过单个服务中的域服务完成的。当然DDD更复杂,要求领域对象的设计是充血模型而不是贫血模型。从实用的角度来看,充血模型对于大多数开发者来说是非常困难的。哪些代码属于行为,哪些属于领域服务,往往很考验人员的水平。服务拆分是个大工程,往往需要和几个最熟悉业务和数据的人一起讨论,甚至还要考虑团队架构。最终的效果是服务边界清晰,没有循环依赖,避免了双向依赖。框架选择由于之前的单体服务使用了SpringBoot,框架自然选择了SpringCloud。其实我个人认为微服务框架不应该限制技术和语言,但是在生产实践中发现Dubbo和SpringCloud都存在侵入性。我们在将Node.js应用集成到SpringCloud系统中时,发现了很多问题。或许未来的ServiceMesh是一条更合理的发展路径。本图来自纯笑公众号这是SpringCloud的一个典型用法:Zuul作为一个Gateway,将不同??客户端的请求分发到具体的Service上。Eureka作为注册中心完成服务发现和服务注册。每个Service,包括Gateway,都自带Hystrix提供的限流熔断功能。服务通过Feign和Ribbon互相调用。Feign实际上是阻塞了Service在Eureka上的运行。上述服务集成到异构语言后,服务注册、服务发现、服务调用、熔断、限流都需要自己处理。关于Zuul我想多说几句。SpringCloud提供的Zuul针对Netflix版本进行了裁剪,去除了动态路由功能(Groovy实现)。还有一点就是Zuul的表现一般。由于采用同步编程模型,对于后台处理时间长的IO密集型环节,Servlet线程池很容易被填满。所以如果Zuul和主Service放在同一台物理机上,在大流量的情况下Zuul的资源消耗会非常大。实际测试也发现Zuul和直接调用Service的性能损失在30%左右,在并发压力大的时候更加明显。现在SpringCloudGateway主要由Pivotal主推,支持异步编程模型。后续可能会采用架构优化,或者直接使用Kong这种基于Nginx的网关Kong来提供优质的性能。当然同步模型也有优势,编码更简单。如何使用ThreadLocal建立链接跟踪,后面会提到。架构改造经过半年多的改造和新需求的加入,单体服务不断拆分,最终形成了10多个微服务,并为BI搭建了Spark。两大系统初步形成,微服务架构的在线业务系统(OLTP)+Spark大数据分析系统(OLAP)。数据源从原来只有MySQL增加到ES和Hive。多数据源之间的数据同步也是一个值得聊的话题,但是内容太多,本文不再赘述。对于服务拆分,我们采用直接割接的方式,数据表也是整体迁移。因为几大改造的升级都申请了停服,所以步骤比较简单。如果需要不停的业务升级,先双写再逐步切换,保证业务不受影响。与CI相比,自动化部署实现持续交付(CD)更为复杂。在资源不足的情况下,我们还没有实现CD,只是实现了自动化部署。由于生产环境需要通过跳板机进行操作,所以我们使用Jenkins生成jar包传输到跳板机上,然后通过Ansible部署到集群中。简单粗暴的部署方式对于小规模的团队开发还是够用的,但是部署前需要保证测试(手工测试+自动化测试)到位。LinkTracking开源的全链路跟踪有很多,比如SpringCloudSleuth+Zipkin,国内还有美团的CAT。它的目的是当一个请求经过多个服务时,可以通过一个固定值获取整个请求链路的行为日志。以此为基础可以进行耗时分析,可以推导出一些性能诊断函数。但对我们来说,主要目的是解决问题。如果出现问题,我们需要快速定位到发生异常的服务,整个请求的链接是什么。为了使方案轻量化,我们在日志中打印RequestId和TraceId来标记链接。RequestId表示Gateway生成的请求。TraceId相当于一个二级路径。一开始和RequestId是一样的,但是TraceId在进入线程池或者消息队列后,会加上一个标记,标识一条路径。例如,当请求向MQ发送消息时,消息可能会被多个消费者消费。这时候每个消费线程都会生成一个TraceId来标记消费链接。添加TraceId的目的是为了避免过滤掉太多只有RequestId的日志。实现如图:简单的说,APIRequestContext串口服务中的所有调用都是通过ThreadLocal来存储的。跨服务调用时,将APIRequestContext信息转换成HttpHeader,被调用者拿到HttpHeader重新构建APIRequestContext放入ThreadLocal,如此循环往复,保证RequestId和TraceId不丢失。如果进入MQ,那么APIRequestContext信息可以转化为MessageHeader(基于RabbitMQ实现)。日志聚合到日志系统后,如果出现问题,只需要抓取异常的RequestId或TraceId即可定位问题。经过一年的使用,基本可以满足绝大部分故障排查场景,一般半小时内定位到具体业务。运维监控采用容器化前Telegraf+InfluxDB+Grafana的方案。Telegraf作为探针收集JVM、System、MySQL等资源的信息,写入InfluxDB,最后通过Grafana将数据可视化。SpringBootActuator可以配合Jolokia暴露JVM的Endpoint。整个解决方案零编码,只需花时间配置。容器化时代的架构转型因为在微服务之初就规划了容器化,所以架构没有太大变化,只是每个服务都会创建一个Dockerfile来创建docker镜像。涉及变化的部分包括:在CI中构建docker镜像的步骤增加了。在自动化测试过程中,数据库升级与应用分离,做成docker镜像。在生产中,使用Kubernetes自带的Service代替Eruka。原因如下。SpringCloud&Kubernetes整合我们使用Redhat的OpenShift,可以认为是Kubernetes企业版,本身就有Service的概念。一个Service下有多个Pod,一个Pod是一个可服务的单元。当Services相互调用时,Kubernetes会提供默认的负载均衡控制。调用方只需要写被叫方的ServiceId即可。这和SpringCloudFegin使用Ribbon提供的功能是一模一样的。也就是说,服务治理可以通过Kubernetes来解决,那为什么要换掉呢?其实上面说了,SpringCloud技术栈是支持异构语言的。我们有很多BFF(BackendforFrontend)是使用Node.js实现的。这些服务如果要集成到SpringCloud中,服务注册、负载均衡、心跳检查等都必须自己实现。如果以后加入其他语言架构的服务,这些轮子就得重新造了。基于这些原因综合考虑后,决定将Eruka替换为OpenShift提供的网络能力。由于在本地开发和联调过程中仍然依赖Eruka,因此在生产中仅通过配置参数控制:eureka.client.enabled设置为false,停止各服务的Eureka注册。ribbon.eureka.enabled设置为false,这样Ribbon就不会从Eureka获取服务列表。以servicefoo为例,foo.ribbon.listofservers设置为http://foo:8080,那么当一个服务需要使用servicefoo时,会直接调用http://foo:8080。CI的改造CI的改造主要是增加了一个编译docker镜像并打包到Harbor的过程。部署时会直接从Harbor中拉取镜像。另一个是数据库升级工具。之前我们使用Flyway作为数据库升级工具,在应用启动时自动执行SQL脚本。随着服务实例越来越多,会出现一个服务的多个实例同时升级的情况。Flyway虽然通过数据库锁来实现升级过程,不会出现并发,但是会导致加锁的服务启动时间变长。问题。从实际的升级过程来看,把可能的并发升级变成一个单一的过程可能更靠谱。另外,后期分库分表的结构,也会造成应用启动时自动升级数据库的困难。综合考虑,我们拆分了升级任务,每个服务都有自己的升级项目,都会容器化。使用时,作为runonce工具使用,即dockerrun-rm的方式。并且后来还支持了设置目标版本的功能,对私有化项目的跨版本升级起到了很好的作用。至于自动部署,由于服务之间存在上下游关系,比如Config、Eruka等基础服务都依赖于其他服务,所以部署也是有先后顺序的。基于Jenkins做Pipeline可以很好的解决这个问题。小结以上每一点都可以深入的写成一篇文章。微服务架构的演进涉及开发、测试和运维,需要团队内部多工种的紧密配合。分而治之是软件行业解决大型系统的必由之路。作为一个小团队,我们并没有盲目追求新的创新,而是在开发过程中通过面向服务的方式解决问题。另一方面,我们也意识到微服务对人的要求和对团队的挑战比过去更高更大。未来仍需探索,进化仍在路上。