一个可供小团队参考的微服务落地实践

微服务是否适合小团队是个见仁见智的问题。但小团队并不表明出品的必定是小产品,当业务变得愈来愈复杂,如何使用微服务分而治之就成为一个不得不面对的问题。前端


image.png

由于微服务是对整个团队的考验,从开发到交付,每一步都充满了挑战。通过 1 年多的探索和实践,本着将 DevOps 落实到产品中的愿景,一步步建设出适合咱们的微服务平台。git


要不要微服务算法


咱们的产品是 Linkflow,企业运营人员使用的客户数据平台(CDP)。产品的一个重要部分相似企业版的“捷径",让运营人员能够像搭乐高积木同样建立企业的自动化流程,无需编程便可让数据流动起来。docker


从这一点上,咱们的业务特色就是聚少成多,把一个个服务链接起来就成了数据的海洋。数据库


理念上跟微服务一致,一个个独立的小服务最终实现大功能。固然咱们一开始也没有使用微服务,当业务还未成型就开始考虑架构,那么就是“过分设计"。编程


另外一方面须要考虑的因素就是“人",有没有经历过微服务项目的人,团队是否有 DevOps 文化等等,综合考量是否须要微服务化。架构


微服务的好处是什么?负载均衡

  • 相比于单体应用,每一个服务的复杂度会降低,特别是数据层面(数据表关系)更清晰,不会一个应用上百张表,新员工上手快。运维

  • 对于稳定的核心业务能够单独成为一个服务,下降该服务的发布频率,也减小测试人员压力。分布式

  • 能够将不一样密集型的服务搭配着放到物理机上,或者单独对某个服务进行扩容,实现硬件资源的充分利用。

  • 部署灵活,在私有化项目中,若是客户有不须要的业务,那么对应的微服务就不须要部署,节省硬件成本,就像上文提到的乐高积木理念。


微服务有什么挑战?

  • 一旦设计不合理,交叉调用,相互依赖频繁,就会出现牵一发动全身的局面。想象单个应用内 Service 层依赖复杂的场面就明白了。

  • 项目多了,轮子需求也会变多,须要有人专一公共代码的开发。

  • 开发过程的质量须要经过持续集成(CI)严格把控,提升自动化测试的比例,由于每每一个接口改动会涉及多个项目,光靠人工测试很难覆盖全部状况。

  • 发布过程会变得复杂,由于微服务要发挥所有能力须要容器化的加持,容器编排就是最大的挑战。

  • 线上运维,当系统出现问题须要快速定位到某个机器节点或具体服务,监控和链路日志分析都必不可少。


下面详细说说咱们是怎么应对这些挑战的。


开发过程的挑战


持续集成


经过 CI 将开发过程规范化,串联自动化测试和人工 Review。


咱们使用 Gerrit 做为代码&分支管理工具,在流程管理上遵循 GitLab 的工做流模型:

  • 开发人员提交代码至 Gerrit 的 Magic 分支。

  • 代码 Review 人员 Review 代码并给出评分。

  • 对应 Repo 的 Jenkins job 监听分支上的变更,触发 Build job。通过 IT 和 Sonar 的静态代码检查给出评分。

  • Review 和 Verify 皆经过以后,相应 Repo 的负责人将代码 Merge 到真实分支上。

  • 如有一项不经过,代码修改后重复过程。

  • Gerrit 将代码实时同步备份至两个远程仓库中。


集成测试


通常来讲代码自动执行的都是单元测试(Unit Test),即不依赖任何资源(数据库,消息队列)和其余服务,只测试本系统的代码逻辑。


但这种测试须要 Mock 的部分很是多,一是写起来复杂,二是代码重构起来跟着改的测试用例也很是多,显得不够敏捷。并且一旦要求开发团队要达到某个覆盖率,就会出现不少造假的状况。


因此咱们选择主要针对 API 进行测试,即针对 Controller 层的测试。另外对于一些公共组件如分布式锁,Json 序列化模块也会有对应的测试代码覆盖。


测试代码在运行时会采用一个随机端口拉起项目,并经过 HTTP Client 对本地 API 发起请求,测试只会对外部服务作 Mock,数据库的读写,消息队列的消费等都是真实操做,至关于把 Jmeter 的事情在 Java 层面完成一部分。


Spring Boot 项目能够很容易的启动这样一个测试环境,代码以下:


测试过程的 HTTP Client 推荐使用 io.rest-assured:rest-assured 支持 JsonPath,十分好用。


测试时须要注意的一个点是测试数据的构造和清理。构造又分为 Schema 的建立和测试数据的建立:

  • Schema 由 Flyway 处理,在启用测试环境前先删除全部表,再进行表的建立。

  • 测试数据能够经过 @Sql 读取一个 SQL 文件进行建立,在一个用例结束后再清除这些数据。


顺带说一下,基于 Flyway 的 Schema Upgrade 功能咱们封成了独立的项目,每一个微服务都有本身的 Upgrade 项目。


好处:一是支持 command-line 模式,能够细粒度的控制升级版本;二是也能够支持分库分表之后的 Schema 操做。Upgrade项目也会被制做成 Docker image 提交到 Docker hub。


测试在每次提交代码后都会执行,Jenkins 监听 Gerrit 的提交,经过 docker run -rm {upgrade 项目的 image}先执行一次 Schema Upgrade,而后 Gradle test 执行测试。


最终会生成测试报告和覆盖率报告,覆盖率报告采用 JaCoCo 的 Gradle 插件生成,以下图:

image.png

image.png

这里多提一点,除了集成测试,服务之间的接口要保证兼容,实际上还须要一种 consumer-driven testing tool。


就是说接口消费端先写接口测试用例,而后发布到一个公共区域,接口提供方发布接口时也会执行这个公共区域的用例,一旦测试失败,表示接口出现了不兼容的状况。


比较推荐你们使用 Pact 或是 Spring Cloud Contact。咱们目前的契约基于“人的信任”,毕竟服务端开发者还很少,因此没有必要使用这样一套工具。


集成测试的同时还会进行静态代码检查,咱们用的是 Sonar,当全部检查经过后 Jenkins 会 +1 分,再由 Reviewer 进行代码 Review。


自动化测试


单独拿自动化测试出来讲,就是由于它是质量保证的很是重要的一环,上文能在 CI 中执行的测试都是针对单个微服务的。


那么当全部服务(包括前端页面)都在一块儿工做的时候是否会出现问题,就须要一个更接近线上的环境来进行测试了。


在自动化测试环节,咱们结合 Docker 提升必定的工做效率并提升测试运行时环境的一致性以及可移植性。

在准备好基础的 Pyhton 镜像以及 Webdriver(Selenium)以后,咱们的自动化测试工做主要由如下主要步骤组成:

  • 测试人员在本地调试测试代码并提交至 Gerrit。

  • Jenkins 进行测试运行时环境的镜像制做,主要将引用的各类组件和库打包进一个 Python 的基础镜像。

  • 经过 Jenkins 定时或手动触发,调用环境部署的 Job 将专用的自动化测试环境更新,而后拉取自动化测试代码启动一次性的自动化测试运行时环境的 Docker 容器,将代码和测试报告的路径镜像至容器内。

  • 自动化测试过程将在容器内进行。

  • 测试完成以后,没必要手动清理产生的各类多余内容,直接在 Jenkins 上查看发布出来的测试结果与趋势。



关于部分性能测试的执行,咱们一样也将其集成到 Jenkins 中,在能够直观的经过一些结果数值来观察版本性能变化状况的回归测试和基础场景,将会很大程度的提升效率,便捷的观察趋势:

  • 测试人员在本地调试测试代码并提交至 Gerrit。

  • 经过 Jenkins 定时或手动触发,调用环境部署的 Job 将专用的性能测试环境更新以及可能的 Mock Server 更新。

  • 拉取最新的性能测试代码,经过 Jenkins 的性能测试插件来调用测试脚本。

  • 测试完成以后,直接在 Jenkins 上查看经过插件发布出来的测试结果与趋势。


发布过程的挑战


上面提到微服务必定须要结合容器化才能发挥所有优点,容器化就意味着线上有一套容器编排平台。咱们目前采用是 Redhat 的 OpenShift。


因此发布过程较原来只是启动 Jar 包相比要复杂的多,须要结合容器编排平台的特色找到合适的方法。


镜像准备


公司开发基于 GitLab 的工做流程,Git 分支为 Master,Pre-production和 Prodution 三个分支,同时生产版本发布都打上对应的 Tag。


每一个项目代码里面都包含 Dockerfile 与 Jenkinsfile,经过 Jenkins 的多分支 Pipeline 来打包 Docker 镜像并推送到 Harbor 私库上。

image.png

Docker 镜像的命令方式为:项目名/分支名:git_commit_id,如 funnel/production:4ee0b052fd8bd3c4f253b5c2777657424fccfbc9。


Tag 版本的 Docker 镜像命名为:项目名 /release:tag 名,如 funnel/release:18.10.R1。



在 Jenkins 中执行 build docker image job 时会在每次 Pull 代码以后调用 Harbor 的 API 来判断此版本的 Docker image 是否已经存在,若是存在就不执行后续编译打包的 Stage。


在 Jenkins 的发布任务中会调用打包 Job,避免了重复打包镜像,这样就大大的加快了发布速度。


数据库 Schema 升级


数据库的升级用的是 Flyway,打包成 Docker 镜像后,在 OpenShift 中建立 Job 去执行数据库升级。


Job 能够用最简单的命令行的方式去建立:

image.png

脚本升级任务也集成在 Jenkins 中。


容器发布


OpenShift 有个特别概念叫 DeploymentConfig,原生 Kubernetes Deployment 与之类似,但 OpenShift 的 DeploymentConfig 功能更多。


DeploymentConfig 关联了一个叫作 ImageStreamTag 的东西,而这个 ImagesStreamTag 和实际的镜像地址作关联,当 ImageStreamTag 关联的镜像地址发生了变动,就会触发相应的 DeploymentConfig 从新部署。


咱们发布是使用了 Jenkins+OpenShift 插件,只须要将项目对应的 ImageStreamTag 指向到新生成的镜像上,就触发了部署。


若是是服务升级,已经有容器在运行怎么实现平滑替换而不影响业务呢?


配置 Pod 的健康检查,Health Check 只配置了 ReadinessProbe,没有用 LivenessProbe。


由于 LivenessProbe 在健康检查失败以后,会将故障的 Pod 直接干掉,故障现场没有保留,不利于问题的排查定位。而 ReadinessProbe 只会将故障的 Pod 从 Service 中踢除,不接受流量。


使用了 ReadinessProbe 后,能够实现滚动升级不中断业务,只有当 Pod 健康检查成功以后,关联的 Service 才会转发流量请求给新升级的 Pod,并销毁旧的 Pod。


线上运维的挑战


服务间调用


Spring Cloud 使用 Eruka 接受服务注册请求,并在内存中维护服务列表。


当一个服务做为客户端发起跨服务调用时,会先获取服务提供者列表,再经过某种负载均衡算法取得具体的服务提供者地址(IP + Port),即所谓的客户端服务发现。在本地开发环境中咱们使用这种方式。


因为 OpenShift 自然就提供服务端服务发现,即 Service 模块,客户端无需关注服务发现具体细节,只需知道服务的域名就能够发起调用。


因为咱们有 Node.js 应用,在实现 Eureka 的注册和去注册的过程当中都遇到过一些问题,不能达到生产级别。


因此决定直接使用 Service 方式替换掉 Eureka,也为之后采用 Service Mesh 作好铺垫。


具体的作法是,配置环境变量:

EUREKA_CLIENT_ENABLED=false,RIBBON_EUREKA_ENABLED=false


并将服务列表如:

FOO_RIBBON_LISTOFSERVERS: '[http://foo:8080](http://foo:8080/)' 


写进 ConfigMap 中,以 envFrom: configMapRef 方式获取环境变量列表。


若是一个服务须要暴露到外部怎么办,好比暴露前端的 HTML 文件或者服务端的 Gateway。


OpenShift 内置的 HAProxy Router,至关于 Kubernetes 的 Ingress,直接在 OpenShift 的 Web 界面里面就能够很方便的配置。


咱们将前端的资源也做为一个 Pod 并有对应的 Service,当请求进入 HAProxy 符合规则就会转发到 UI 所在的 Service。


Router 支持 A/B test 等功能,惟一的遗憾是还不支持 URL Rewrite。

0a2820a2595cfe58f877d09c463dba92.png

image.png

对于须要 URL Rewrite 的场景怎么办?那么就直接将 Nginx 也做为一个服务,再作一层转发。流程变成 Router → Nginx Pod → 具体提供服务的 Pod。


链路跟踪


开源的全链路跟踪不少,好比 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 便可进行问题定位。


通过一年来的使用,基本能够知足绝大多数 Trouble Shooting 的场景,通常半小时内便可定位到具体业务。


容器监控


容器化前监控用的是 Telegraf 探针,容器化后用的是 Prometheus,直接安装了 OpenShift 自带的 cluster-monitoring-operator。


自带的监控项目已经比较全面,包括 Node,Pod 资源的监控,在新增 Node 后也会自动添加进来。


Java 项目也添加了 Prometheus 的监控端点,只是惋惜 cluster-monitoring-operator 提供的配置是只读的,后期将研究怎么将 Java 的 JVM 监控这些整合进来。


image.png

总结


开源软件是对中小团队的一种福音,不管是 Spring Cloud 仍是 Kubernetes 都大大下降了团队在基础设施建设上的时间成本。


固然其中有更多的话题,好比服务升降级,限流熔断,分布式任务调度,灰度发布,功能开关等等都须要更多时间来探讨。


对于小团队,要根据自身状况选择微服务的技术方案,不可一味追新,适合本身的才是最好的。

相关文章
相关标签/搜索