更多干货看微服务适不适合小团队,见仁见智。回归现象取决于本质。随着业务复杂度的增加,单体应用变得越来越大,就像一个类,代码行越来越多。分而治之,切入多个类应该是更好的解决方案,所以将一个巨大的单体应用拆分成多个小应用更符合这种分而治之的思想。当然,微服务架构不应该是一个小团队一开始就应该考虑的问题,而是逐渐演进的结果。特别重要的是要小心过度设计。公司后台是提供SaaS服务,也会有针对大客户的定制化开发和私有化部署。历时不到2年,技术架构经历了从单体到微服务再到容器化的过程。单体应用时代的早期开发只有两个人,考虑微服务之类的是多余的。但受前公司的影响,初步定下了前后端分离的路线。因为不需要考虑SEO的问题,所以简单的做了一个SPA单页应用。还有一点,前后端分离并不一定意味着服务端渲染不行,比如电商系统或者一些可以匿名访问的系统,加一个薄的View层,不管是PHP或Thymeleaf是一个不错的选择。在部署架构上,我们使用Nginx代理前端HTML资源,在收到请求时,根据路径反向代理到服务器的8080端口实现业务。图片接口定义接口按照标准Restful定义,版本统一后跟/api/,例如/api/v2以资源为中心,使用复数表达,例如/api/contacts,也可以嵌套,比如/api/groups/1/contacts/100url中尽量不要使用动词。在实践中发现,要做到这一点确实很难。各个开发者的思路不一致,名字也千奇百怪,都需要在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都存在侵入性。我们在将nodejs应用集成到springcloud系统中时,发现了很多问题。或许未来的ServiceMesh才是更合理的发展路径。图片这是一个典型的SpringCloud使用zuul作为网关,将不同客户端的请求分发到具体的serviceerueka作为注册中心,完成服务发现和服务注册的方法。包括gateway在内的每一个服务都自带了Hystrix提供的限流熔断功能,服务之间通过feign和ribbon相互调用。Feign实际上是屏蔽了erueka上服务的运行。上述服务一旦被集成到异构语言中,服务注册、服务发现、服务调用、熔断和限流都是必需的。自己处理。关于zuul我想多说几句。SprinCloud提供的zuul针对Netflix版本进行了裁剪,去除了动态路由功能(Groovy实现)。还有一点就是zuul的性能一般。由于同步编程模型,它是IO密集型的。类型等后台处理时间长的环节很容易把servlet线程池填满,所以如果Zuul和主服务放在同一台物理机上,在大流量的情况下Zuul的资源消耗会非常大.实际测试也发现zuul和直接调用服务后的性能损失在30%左右,在并发压力大的时候更加明显。现在springcloudgateway是Pivotal主推的,支持异步编程模型。后续可能会采用架构优化,也可能直接使用Kong等基于nginx的网关来提供性能。当然同步模型也有优势,编码更简单。如何使用ThreadLocal建立链接跟踪,后面会提到。架构改造经过半年多的改造和新需求的加入,单体服务不断拆分,最终形成了10多个微服务,并为BI搭建了Spark。两大系统初步形成,微服务架构的在线业务系统(OLTP)+Spark大数据分析系统(OLAP)。数据源从原来只有Mysql增加到ES和Hive。多数据源之间的数据同步也是一个值得聊的话题,但是内容太多,本文不再赘述。与CI相比,镜像的自动部署实现持续交付(CD)更为复杂。在资源不足的情况下,我们还没有实现CD,只是实现了自动部署。由于生产环境需要通过跳板机进行操作,所以我们使用Jenkins生成jar包传输到跳板机上,然后通过Ansible部署到集群中。图片这种简单粗暴的部署方式对于小规模的团队开发还是够用的,但是部署前需要保证测试(手动测试+自动化测试)到位。LinkTracking开源的全链路跟踪有很多,比如springcloudsleuth+zipkin,美团在国内的CAT等等。它的目的是当一个请求经过多个服务时,可以通过一个固定值获取整个请求链路的行为日志。以此为基础可以进行耗时分析,可以推导出一些性能诊断函数。但对我们来说,主要目的是解决问题。如果出现问题,我们需要快速定位到发生异常的服务,整个请求的链接是什么。为了使方案轻量化,我们在日志中打印RequestId和TraceId来标记链接。RequestId在网关中生成,表示唯一的请求。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的端点。整个解决方案零编码,只需花时间配置。容器化时代的架构转型因为在微服务之初就规划了容器化,所以架构没有太大变化,只是每个服务都会创建一个Dockerfile来创建docker镜像。涉及改动的部分包括:CI中更多的构建在dockerimage的步骤中,在自动化测试过程中,数据库升级与应用分离,做成dockerimage。在制作docker镜像时,使用k8s自带的服务来替代eruka。原因如下。对于SpringCloud和k8s的集成,我们使用的是Redhat的Openshift,可以认为是k8s的企业版,本身就有服务的概念。一个service下有多个pod,一个pod是一个可服务的单元。当服务相互调用时,k8s会提供默认的负载均衡控制。调用方只需要写被叫方的serviceId即可。这和springcloudfegin使用ribbon提供的功能是一模一样的。换句话说,服务治理可以通过k8s来解决,那为什么要换掉呢?其实上面说了,SpringCloud技术栈是支持异构语言的。我们有许多使用nodejs实现的BFF(前端后端)。这些服务要想集成到SpringCloud中,服务注册、负载均衡、心跳检测等都得自己实现。如果以后加入其他语言架构的服务,这些轮子就得重新造了。基于这些原因综合考虑后,决定将eruka替换为Openshift提供的网络能力。由于在本地开发和联调过程中仍然依赖eruka,因此在生产中仅受配置参数控制。将eureka.client.enabled设置为false以停止每个服务的eureka注册。设置ribbon.eureka.enabled为false,这样ribbon就不会从eureka获取服务列表了foo,会直接调用http://foo:8080CICI的改造改造主要是增加一个编译docker镜像打包到Harbor的过程。部署时会直接从Harbor中拉取镜像。另一个是数据库升级工具。之前我们使用flyway作为数据库升级工具,在应用启动时自动执行SQL脚本。随着服务实例数量的增加,一个服务的多个实例同时升级。flyway虽然通过数据库锁实现了升级过程,不会出现并发,但是会导致加锁的服务启动时间变长。问题。从实际的升级过程来看,把可能的并发升级变成一个单一的过程可能更靠谱。另外,后期分库分表的结构,也会造成应用启动时自动升级数据库的困难。综合考虑,我们拆分了升级任务,每个服务都有自己的升级项目,都会容器化。使用时,作为runonce工具使用,即dockerrun-rm的方式。并且后来还支持了设置目标版本的功能,对私有化项目的跨版本升级起到了很好的作用。至于自动部署,由于服务之间存在上下游关系,比如config、eruka等都是基础服务,被其他服务所依赖,所以部署也是有先后顺序的。基于Jenkins做一个pipeline可以很好的解决这个问题。小结其实上面的每一个点都可以写成一篇有深度的文章。微服务的架构演进涉及开发、测试和运维,需要团队内部多工种的紧密配合。分而治之是软件行业解决大型系统的必由之路。作为一个小团队,我们并没有盲目追求新的创新,而是在开发过程中通过面向服务的方式解决问题。另一方面,我们也意识到微服务对人的要求和对团队的挑战比过去更高更大。未来仍需探索,进化仍在路上。(感谢阅读,希望对大家有所帮助)
