写点什么

基于 SpringBoot 的微服务架构与 K8S 容器部署实践

用户头像
云流
关注
发布于: 2021 年 02 月 04 日

前不久作为架构师完成了某知名快消企业的一个业务中台建设。系统上线后,经历了双十一活动的流量高峰,整体运行稳定。最近有空,便将此次架构的思路,心得稍作整理在这篇博客中分享一下。不会深入每一个技术细节,而是把用到的技术、框架、工具做一个简单的回顾,作为日后的参考。


业务架构

业务架构方面,该系统作为业务中台,主要负责客户资产管理,包括客户的卡、券以及其他虚拟资产。通过对外暴露标准 restful 接口的方式提供服务。服务的调用方包括自有渠道的 app、小程序,以及合作伙伴渠道,包括招行、阿里等。而系统本身也会通过服务网关去调用公司内部的其他业务系统接口,如通过客户中心接口同步会员信息等。



根据目前的统计,这个业务中台,每日的服务调用量在 700 万次左右,有活动时也会超过 1000 万次。而大部分交易,发生在上班、午休以及下午 3 点左右(下午茶)的时间段内。




由于涉及到客户业务细节,这里对业务架构就不做详细说明了。


技术架构

这个案例中采用了基于 SpringBoot 的微服务架构。结合企业自身的基础架构设施,进行 K8S 容器化部署,并采用 Kong API Gateway 对各业务中台暴露的 API 接口进行统一管理。


Kong API Gateway

随着微服务架构在企业中的流行,原来大而全的系统被拆分为粒度较小的中台,而系统中的大部分功能则被以 restful API 形式提供的服务所取代,这使得 IT 系统能够更加快速地响应业务变化带来的挑战,但同时随着服务的增加,如何有效管理这些服务却成为难题。




在一些中小型项目中,我们一般都会采用 Spring Cloud 的技术栈,并选择 Spring Cloud Gateway 来作服务网关。然而,对于一些大型企业,则需要全局考虑服务的治理,网关性能,以及其他扩展功能。


在这个案例中,企业使用了 Kong 作为 API 网关。中台将需要开放外部使用的 API,通过网关控制台进行注册,添加证书,生成 Auth Key 供关联方使用。


Kong 具有以下一些特性,能够很好地满足大型组织对于服务网关的需求:


  • 开源(本案例中使用的是 Kong 的企业版,提供了原厂服务)

  • 亚毫秒级的响应延迟,得益于基于 Nginx 与 OpenResty 带来的超高性能

  • 单节点 25K TPS

  • 认证、授权、限流、数据转换(此案例中会员 ID 被添加到请求头中)、日志、统计分析




应用架构

整个系统采用 java 开发后端以及 vue 开发前端,应用部分共分为 4 个服务组件,全部进行容器化部署,并通过 Ingress Controller 负载均衡对外暴露服务:


  • 资产服务:提供客户资产相关的服务接口

  • 资产消费者服务:MQ 监听服务,异步处理资产相关请求

  • 控制台服务:资产管理运维类服务接口,供控制台前端使用

  • 控制台前端服务:使用 Vue 开发的控制台前端应用(如下图)




SpringBoot

除控制台前端外,其他三个组件均采用目前主流的 java 微服务框架 SpringBoot 2.3.4 开发(考虑到稳定性,未使用最新的 2.4 版本)。


本案例中,通过开发应用框架,实现了系统中数据表达形式的统一,以及标准的据转换、校验、消息绑定、错误处理等功能。架构师需要对应用框架负责,简明、高效、统一的应用框架,能够提升开发效率,产出标准一致的代码,保证交付质量。


应用框架不在本文的讨论范围内,而以下一些技巧或第三方包,却在我们构建大多数 SpringBoot 应用中得到使用。


####定制 MyBatis 数据层框架采用 MyBatis,在大型应用中 MyBatis 能够帮助程序员更好地控制数据层交互,并进行调优。一般可以在 applicaion.yml 中配置 MyBatis,但当我们需要让 MyBatis 支持更多定制特性(如:多数据库支持)时,可以通过定义 SqlSessionFactory bean 来实现。


    @Bean    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {        SqlSessionFactoryBean sfb = new SqlSessionFactoryBean();        sfb.setDataSource(dataSource);        sfb.setVfs(SpringBootVFS.class);        Properties props = new Properties();        props.setProperty("dialect", dataConfiguration.getDialect());        props.setProperty("reasonable", String.valueOf(dataConfiguration.isPageReasonable()));        PageHelper pagePlugin = new PageHelper();        pagePlugin.setProperties(props);        Interceptor[] plugins = {pagePlugin};        sfb.setPlugins(plugins);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sfb.setMapperLocations(resolver.getResources("classpath*:mappers/"+ dataConfiguration.getDialect()+"/*.xml")); sfb.setTypeAliasesPackage("com.xxx.bl.core.data.model");
SqlSessionFactory factory = sfb.getObject(); factory.getConfiguration().setMapUnderscoreToCamelCase(true);// factory.getConfiguration().addInterceptor(new CoreResultSetHandler()); factory.getConfiguration().setCallSettersOnNulls(dataConfiguration.isCallSettersOnNulls()); return factory; }复制代码
复制代码


使用 logback 日志组件


采用 logback 日志框架,可以在 logback 配置文件中指定针对不同的 Spring profile 在不同的环境中采用不同的日志级别,并采用不同的 appender。同时引入 spring-cloud-starter-sleuth 依赖,通过设置 traceId,使整个请求全链路上的所有日志打印出一致的 traceId,大大方便了各系统间生产问题的协同排查。另外,采用异步方式记录日志,也有利于降低 IO 阻塞。


   <springProfile name="stg">        <root level="error">            <appender-ref ref="STDOUT"/>            <appender-ref ref="SAVE-ERROR-TO-FILE-STG"/>        </root>        <logger name="org.xxx" level="error" additivity="false">            <appender-ref ref="STDOUT"/>            <appender-ref ref="ASYNC-SAVE-TO-FILE-STG"/>        </logger>            </springProfile>    <springProfile name="prod">        <root level="error">            <appender-ref ref="STDOUT"/>            <appender-ref ref="SAVE-ERROR-TO-FILE-PROD"/>        </root>        <logger name="org.xxx" level="error" additivity="false">            <appender-ref ref="ASYNC-SAVE-TO-FILE-PROD"/>        </logger>    </springProfile>复制代码
复制代码


SSL 加密及密码安全


全链路传输加密已成为企业安全中必不可少的措施。通过在 classpath 中引入 CA 颁发(也可以使用自签)的 jks 证书,并在 application 配置文件中进行简单配置,便可实现 SpringBoot 应用的 SSL 加密。


  ssl:    enabled: true    key-store: classpath:xxx.net.jks    key-store-type: JKS    key-store-password: RUIEIoUD    key-password: RUIEIoUD    require-ssl: true复制代码
复制代码


密码以明文形式存放在配置文件中,也是不安全的。你可以 jasypt 加密配置文件中使用到的密码,或者直接使用 Key-Vault 方案,比如本案例中会分别在微软云环境中使用 Azure Key Vault 或本地 IDC 中使用 Cyberark Conjur 方案。


###同步与异步服务 我们并没有使用 Spring Webflux 来支持 reactive 特性,因为,这会增加开发复杂度,并且 Webflux 虽然改善了 Web 容器阻塞机制,但并不能从根本上解决高并发请求到来时的阻塞问题。


在这个案例中,通过搭建了 3 个节点的 RabbitMq 镜像集群,作为消息中间件,并通过应用框架的支持,实现了服务的同步异步切换功能。我们将对外提供的服务注册到数据库中,在应用启动时,读入 redis 缓存。当请求到来时,通过 API code 判断该请求的响应模式:同步或异步。如果是同步请求则直接处理,而如果是异步请求,则发送到 RabbitMq 中,再由经过封装的消费者组件进行异步消费,最终达到削峰的目的。




对于开发人员来说,他们只需要关注服务的业务逻辑开发,由应用框架统一处理服务的同步,异步切换,消息发送或失败时的异常处理,以及死信队列的维护等工作。


Dockerfile


案例中的四个组件需要实现容器化部署,分别为 SpringBoot 应用与 Vue 应用创建 Dockerfile。


典型的 SpringBoot 应用 Dockerfile 如下,一般情况下大型组织会构建私有镜像仓库,通过私有仓库拉取镜像的速度更快,能够节省 CICD 的时间。


FROM openjdk:11-jre#FROM cargo.xxx.net/library/openjdk:11-jreARG JAR_FILE=console-service/build/libs/*.jarCOPY ${JAR_FILE} app.jarEXPOSE 9002EXPOSE 9003ENTRYPOINT [ "java", "-jar", "/app.jar" ]复制代码
复制代码


vue 应用的 Dockerfile 如下,同样添加了 SSL 证书,进行传输加密:


FROM cargo.xxx.net/library/nginx:stable-alpineCOPY /dist /usr/share/nginx/html/consoleCOPY nginx.conf /etc/nginx/nginx.confARG KEY_FILE=stg.xxx.net.keyARG PEM_FILE=stg.xxx.net.pemCOPY ${KEY_FILE} /etc/ssl/certs/cert.keyCOPY ${PEM_FILE} /etc/ssl/certs/cert.pemEXPOSE 80CMD [ "nginx", "-c", "/etc/nginx/nginx.conf", "-g", "daemon off;" ]复制代码
复制代码


编写 dockerfile 时有以下一些注意事项:


  • 基础镜像:尽可能推荐选择官方镜像

  • 选择大小适中的版本:如果选择的基础镜像过大,启动后需要消耗更多的资源,影响系统性能。如果太小,则可能缺失关键功能。

  • 利用缓存:将 dockerfile 中不易变动的内容写在 dockerfile 最前。


##数据库架构 在账户数据上亿,交易数据几百亿的系统,需要采用分库分表方案。本案例中,采用了 MyCat+MySQL 的数据库架构方案。采用 mycat 代理 Master 与 Slave,可灵活进行主从切换。Slave 可作为 Master 热备,也同时可作为读库,实现读写分离。备库除作为准实时的备份外,也可作为运维库或提供大数据平台数据抽取。


同时采用 1 主 2 从 1 备的双机房设计


  • Master 到 Slave 使用半同步方案,保证从库数据一致性。

  • Master 异常时,通过 mycat 切换至 Slave,Slave 转换为新 Master

  • Master 异常恢复后,先将原 Master 设置为 Slave,数据同步完成后,再切换回正式 Master


mycat 高可用

mycat 采用 k8s 容器化运行,使用 k8s service 来实现 mycat 的负载均衡,达到 mycat 的集群的高可用。若 mycat 容器节点异常,应用自动连接到另外的 mycat 节点上。


对数据库的大量操作是读操作,一般占到所有操作 70%以上。所以做读写分离还是很有必要的,如果不做读写分离,那么从库也是一种很大的浪费。 mycat 通过配置很容易做到读写分离,在从库进行读操作,提升资源利用率,在主库进行写操作,减低主库压力。




分库分表

  • 垂直分库:按照功能划分,把数据分别放到不同的数据库和服务器。例如:账户、资产、交易等业务领域不同的数据分别放在不同的库中,分散压力、减少相互影响、降低耦合,独立模块独立发布

  • 水平分库:在垂直分库不能满足要求时,再对模型进行水平的 切分,将同一实体,不同范围的数据分散到不同库中,保持单库数量和压力,提升连接数,达到横向扩展的目的。




冷热数据方案

热数据缓存


  • 对于高频使用的热数据,如经常使用 App 的客户信息等,适当增加数据库 query cache,提升数据库查询性能。

  • 在应用层使用 redis 等内存缓存部分高频使用数据,降低请求响应时间,增加系统流畅度,提升客户体验。

  • 进行读写分离,使用从库提供数据查询的服务,提升从库硬件资源利用率,降低主库读压力,增加主库写性能。提升整体效率。


冷数据归档


  • 对于使用频率很低或基本不使用的冷数据,如历史交易、历史卡券等,进行数据的归档,提升数据库的性能。

  • 也可提供使用频率较低的历史交易查询功能,使用备库提供服务。

  • 对于交易类数据建议按日期进行分库分表,每日交易分为一片或多片,对于历史交易如 1 年前交易进行定期迁移和归档,提升数据库性能。


DEVOPS 与 K8S 容器化部署

####DEVOPS 流水线 本案例中,通过基于 jenkins 的 CICD 平台,将应用代码从 github 代码库获取,使用 gradle 进行构建(前端使用 npm 构建),通过 dockerfile 打成镜像后,部署到 K8S 容器平台。




在进行持续集成的过程中,同时加入了安全检查,合规检查以及单元测试(SpringBoot 应用使用 JUnit,Vue 前端应用使用 Jest 测试框架)的步骤,以保证每一次发布的质量。 ###ConfigMap ConfigMap 用于将应用的配置信息与程序的分离,这种方式不仅可以实现应用程序被的复用,而且还可以通过不同的配置实现更灵活的功能。本案例中,SpringBoot 应用在 K8S 部署时,便将 application.yml 文件以 ConfigMap 文件的形式进行挂载。需要注意,SpringBoot 会优先读取 classpath 下的配置文件,因此需要在打出 springboot 应用 jar 包时,先将配置文件排除,并通过容器启动命令参数来制定挂载的应用配置文件。


-spring.profiles.active=prod-spring.config.location=/config/application.yml复制代码
复制代码


###K8S 容器部署 在 K8S 部署平台,可以为每一个服务指定初始的资源,以及节点数量配置。比如我们为 SpringBoot 应用初始配置,2core 4g 的资源配置,节点数量则为 20 个。




根据需要我们可以采用滚动方式对 pod 数量进行伸缩。而不会引起服务不可用的情况。




另外,我们也可以利用弹性伸缩,基于某些关键指标,如容器的 CPU 使用量作为阈值,来触发容器进行弹性伸缩。在这个案例中,通过弹性伸缩机制,在上班以及中午业务高峰时间段内,将更多 pod 提供给业务服务组件,而在晚上,则会将 pod 从业务组件收回,提供给需要跑批处理以及异步消费的服务组件。




运维与监控

ELK

ELK 是一套解决方案而不是一款软件, 三个字母分别是三个软件产品的缩写。 E 代表 Elasticsearch,负责日志的存储和检索; L 代表 Logstash, 负责日志的收集,过滤和格式化;K 代表 Kibana,负责日志的展示统计和数据可视化。




Dynatrace

Dynatrace 可能是目前最优秀的应用性能管理工具(APM),它既能监控基础设施如服务器,K8S 容器,又能自动发现并监控在容器内运行的动态微服务,了解它们如何执行、相互之间如何通信,还能立即检测出性能不佳的微服务。在我们的案例中,通过定制 dashboard 添加我们所需要关注的监控数据。




Dynatrace 还能自动识别服务,并提供更精细的检测数据,为开发或运维人员定位问题,带来了极大的帮助。




一些思考

  • 数据库分库分表方案带来的代码侵入问题:MyCat+MySQL 虽然在物理上实现了分库分表,但对于开发来说带来了侵入性问题,需要为分片键进行特殊的表结构设计,在进行查询时也需要额外考虑分片键的使用,以提升查询效率。其他的如事务的处理,由于分库的关系,我们不再依赖事务,而是通过数据最终一致性,以及错误补偿等方式进行处理。

  • 未来数据库的选型:MyCat+MySQL 给数据库运维增加了复杂性,而未来针对超大数据量级的应用,在硬件资源允许的情况下,可以考虑转向如:TiDB 这样的 NewSQL 方案进行替代。

  • JVM 优化:应用上线后,在高并发情况下曾偶发 Long GC 问题,通过分析 dump 文件,优化内存使用,进行了解决。另外,对于内存变化较大的应用,也可以考虑使用 jdk13,并开启 ZGC。

  • 缓存优化:案例中通过 redis 缓存服务配置信息,每次服务响应时都需要读取 redis,这给 redis 造成了不小的压力,通过引入 Guava cache,在本地建立缓存副本实现多级缓存,并设定合理的失效时间,能够显著降低对 redis 的压力。

  • 通过应用框架实现低代码:在应用框架上的投资是非常值得的,通过将共性问题集中在应用框架中解决,可以在一定程度上实现低代码平台的特性。开发人员也能更专注于业务逻辑的实现。

  • 开发管理:通过让每位开发人员充分理解应用框架,并形成解决同类问题的统一 Pattern,能够明显提高开发效率,减少低质量代码的产生。


今天先记录到这里,随着实践的深入,相信后面还会有更多新的补充,也欢迎大家一起分享经验。


作者:技匠

链接:https://juejin.cn/post/6925238390161932301


用户头像

云流

关注

还未添加个人签名 2020.09.02 加入

还未添加个人简介

评论

发布
暂无评论
基于SpringBoot的微服务架构与K8S容器部署实践