⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/6925238390161932301 「技匠」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

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

业务架构

业务架构方面,该系统作为业务中台,主要负责客户资产管理,包括客户的卡、券以及其他虚拟资产。通过对外暴露标准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-jre
ARG JAR_FILE=console-service/build/libs/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 9002
EXPOSE 9003
ENTRYPOINT [ "java", "-jar", "/app.jar" ]

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

FROM cargo.xxx.net/library/nginx:stable-alpine
COPY /dist /usr/share/nginx/html/console
COPY nginx.conf /etc/nginx/nginx.conf
ARG KEY_FILE=stg.xxx.net.key
ARG PEM_FILE=stg.xxx.net.pem
COPY ${KEY_FILE} /etc/ssl/certs/cert.key
COPY ${PEM_FILE} /etc/ssl/certs/cert.pem
EXPOSE 80
CMD [ "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,能够明显提高开发效率,减少低质量代码的产生。

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

文章目录
  1. 1. 业务架构
  2. 2. 技术架构
  3. 3. Kong API Gateway
  4. 4. 应用架构
    1. 4.0.1. SpringBoot
    2. 4.0.2. 定制MyBatis
    3. 4.0.3. 使用logback日志组件
    4. 4.0.4. SSL加密及密码安全
    5. 4.0.5. 同步与异步服务
  • 5. Dockerfile
  • 6. 数据库架构
    1. 6.0.1. mycat高可用
    2. 6.0.2. 分库分表
  • 7. 冷热数据方案
    1. 7.0.1. 热数据缓存
    2. 7.0.2. 冷数据归档
  • 8. DEVOPS与K8S容器化部署
    1. 8.0.1. DEVOPS流水线
    2. 8.0.2. K8S容器部署
  • 9. 运维与监控
    1. 9.0.1. ELK
    2. 9.0.2. Dynatrace
  • 10. 一些思考