# 从大型Web应用架构的变化,带你看Java后端开发都需要学习什么

# 前言

刚刚入门Java后端开发的同学可能对需要学习什么技术还不够了解。现在我们以一个网站从几乎没有并发到几千万并发的架构变化,来简单介绍一下Java后端都需要学习什么。

一言以蔽之:从单机部署到分布式部署,更多是因为性能上的需求。从单体架构到分布式架构,更多是因为开发和运维方面的需求。

# 单体架构、单机部署

# 阶段一:单体应用与数据库部署在一台服务器上

# 描述

一个网站刚刚出现时,用户量和访问量基本都比较小,往往把Web应用与数据库部署在同一台服务器上。此时的Web应用为“比较混乱”的单体应用,可能没有明显的模块划分和职责划分(例如把绝大多数业务逻辑写在JSP里导致一个JSP文件有几千行代码……),也可能还没有应用Maven等依赖管理技术。

# 图示

阶段一图示

# 需要学习

  • Java语言基础(Java SE基础)
  • 面向对象程序设计基础(封装、继承、多态、设计模式(Design Patterns, DP)等)
  • Java Web基础(ServletJDBC(Java Database Connectivity)JSP(Java Server Pages)Java Bean规范等)
  • 关系型数据库(MySQLOracle等)、非关系型数据库(MongoDB等,可选)
  • 服务器软件(Tomcat
  • 计算机网络基础(主要是HTTP协议相关知识点)
  • 开发工具(Intellij IDEAEclipse等,推荐Intellij IDEA
  • Linux操作系统

# 局限

随着业务逻辑渐渐变得复杂,没有明显的模块划分与职责划分的代码混乱不堪,开发和维护效率低下,不足以支撑快速变化的需求。

# 阶段二:单体应用改用MVC模式或三层架构

# 描述

Web应用被重构,采用MVC模式三层架构(或两者兼有)。其中MVC模式的ViewController对应三层架构的UI层,Model对应三层架构的BLLDAL两层。

MVC模式可以借助Spring MVCStruts等框架来实现,也可以使用Java原生的JSPServletJava Bean实现。不过在达到一定规模时,一般都会使用框架。在数据访问层,除了可以使用Java原生的JDBC,也可以使用MyBatisHibernate/Spring Data JPAORM(Object Relational Mapping)框架来简化开发。大家比较熟悉的SSM(Spring + Spring MVC + MyBatis)SSH(Spring + Spring MVC/Struts + Hibernate),还有一站式解决方案Spring Boot就是用来开发这种Web应用的。

有人可能认为Spring Boot只是用来开发微服务的,其实不然。现在有很多单体架构的Web应用从一开始就是使用Spring Boot来开发的,毕竟它实在是太方便了。而且在我看来,Spring Boot绝对是未来的趋势。

因为引入了框架,所以还需要借助MavenGradle等依赖管理工具来管理框架的依赖。

# 图示

阶段二图示

# 需要学习

  • Spring Framework(IoC/DIAOP,以及事务机制)
  • 依赖管理工具(MavenGradle等)
  • MVC框架(Spring MVCStruts(不推荐,早就没人用这玩意了))
  • Spring Boot(现在不会这个基本都不能就业了。。。)
  • ORM框架(MyBatisHibernate(可选)、Spring Data JPA(可选))
  • 单元测试、Mock测试(JUnitMockito等)
  • 模板引擎(Thymeleaf等)

# 局限

随着用户数的增加,部署在一台服务器上的数据库与Web应用相互竞争资源,单机部署的方式无法支持不断增加的用户量和并发量。

# 阶段三:数据库单独部署

# 描述

将数据库单独部署到另一台服务器上。此时Web应用与数据库分别拥有自己的服务器资源,两者的性能均被显著提高。

# 图示

阶段三图示

# 需要学习

  • Java高级知识(多线程、并发编程、IO等)
  • 线程同步机制、锁
  • JVM调优、线上问题定位(JVM参数、GC、、问题定位工具Arthas/jstack/jmap等)
  • GitFlow工作流

# 局限

随着用户数量的增加,读写速度较慢的数据库成为系统的性能瓶颈。

目前,有相当一部分Web应用都是处于阶段二或者阶段三的。

# 阶段四:引入缓存机制解决数据库速度过慢的问题

# 描述

使用本地缓存机制和RedisMemcached等分布式缓存机制并预载部分热点数据,可以将绝大多数请求在到达数据库之前拦截掉,大大降低了数据库的读写压力。

引入缓存虽然解决了数据库成为性能瓶颈的问题,但是也带来了新的麻烦,例如:缓存击穿,缓存雪崩,数据一致性(缓存一致性),Redis持久化,缓存失效时间,缓存高可用等。这些问题也必须得到有效的处理。

# 图示

阶段四图示

# 需要学习

  • Key-Value型数据库Redis
  • Redis相关知识点(Redis持久化、Redis集群、Redis Sentinel、数据同步等)
  • Web框架与Redis的结合,使用框架操作Redis(如Spring BootRedisTemplate等)
  • 缓存相关知识点(缓存击穿、缓存雪崩、缓存一致性、缓存数据失效时间设置等)

# 局限

虽然缓存扛住了绝大部分并发请求,但是随着用户数量的增加,单机的Web应用性能不足,成为了系统的性能瓶颈。

# 单体架构、分布式部署

# 阶段五:部署多个Web应用,并引入Nginx负载均衡

# 描述

因为性能瓶颈在Web应用,所以可以把Web应用部署多份,并且使用Nginx来实现多个Web应用间的负载均衡(LB, Load Balance)——把大量的请求平均分配到每个Web应用中去。Nginx是工作在应用层的反向代理软件,它十分轻量,并且单机的Nginx就可以支持上万的并发,非常适合作为负载均衡器。

理论上Nginx可以扛住50000个并发,如果单机的Spring Boot应用能扛500个并发,那么部署100个Spring Boot应用,整个系统就能扛住50000个并发(假设分布式缓存和数据库也可以扛住的话)。

但是,Web应用分布式部署之后,session的共享、文件的上传与下载也会成为问题。session的共享可以借助Redis或者使用JWT解决,而文件同步则可以采用文件与Nginx放在一台服务器上或者使用分布式文件系统或独立的文件服务器等策略来解决。

# 图示

阶段五图示

# 需要学习

  • Nginx相关知识(安装、启动、配置、负载均衡配置、负载均衡算法、惊群问题等)
  • session同步机制
  • JWT(JSON Web Token)

# 局限

整个系统里除了Nginx只有数据库是单机的了,用大腿想想也应该知道下个瓶颈是数据库了吧

更多请求的到来意味着对数据库更多的读写,虽然有缓存可以挡掉一大部分请求,但是数据库的读写压力依然会增加,数据库出现故障而成为单点故障的情况也有更高的几率发生,数据库再一次成为性能瓶颈。

# 阶段六:数据库主从备份+读写分离

# 描述

把数据库部署多份,并使用数据库原生的主从备份和同步机制把数据库划分为masterslave,实现数据库的高可用。此外,还可以借助MyCat等工具在主从备份的基础上实现数据库的读写分离,进一步降低数据库的读写压力——例如在MySQL中读库可以使用MyISAM存储引擎,这样查询更快;写库可以使用InnoDB存储引擎,提高并发性能。这还会涉及不同数据库之间的数据一致性的问题。

# 图示

(以MyCat为例)

阶段六图示

# 需要学习

  • 数据库主从备份机制、数据同步机制(一般由数据库本身实现)
  • 读写分离中间件(MyCatSharding JDBC等)
  • 数据一致性解决方案

# 局限

随着时间推移,数据库中的数据越来越多,查询性能低下。当数据量达到千万级时,就算增加索引也很难提高多少性能。同时,不同业务之间相互竞争数据库资源,进一步降低了数据库的性能。

# 阶段七:数据库分库分表

# 描述

可以把数据库按照业务拆分,解决不同业务竞争数据库资源的问题。把存有大量数据的数据表进行拆分(例如按照时间拆分,或者将历史数据存入历史表等),解决查询过慢的问题。当然,分库时也可以通过把一张过大的表存入不同数据库来提升查询性能。

数据库在分库分表之后,一般情况下就可以支持水平扩展了。通过水平扩展可以进一步提升数据库的性能。

常见的分库分表方式有两种:客户端模式和服务器模式。Sharding JDBC是客户端模式,而MyCat则是服务器模式。一般来说MyCat这种服务器模式的分库分表方案更好一些,因为这种方式对业务代码的侵入比较小。

# 图示

阶段七图示

# 需要学习

  • MyCat等分库分表方案

# 局限

在数据库、分布式缓存和Web应用都可以水平扩展的情况下,整个系统可以支撑上万的并发量。但是,系统中还有一个东西是单机的,那就是负载均衡器Nginx。单机的Nginx作为外部的唯一访问入口,没办法直接以集群的形式对外提供服务,不能水平扩展,最终会成为系统的瓶颈。

# 阶段八:使用LVS或者F5来解决Nginx不能水平扩展的问题

# 描述

LVS(Linux Virtual Server, Linux虚拟服务器)是一种工作在网络层的负载均衡软件(而且还是中国最早的自由软件之一),并且在Linux 2.6中它已经成为了内核的一部分,而F5则是一种硬件负载均衡器。可以粗略地认为LVS和F5都可以抗住几十万的并发量。

可以使用LVS或者F5来实现多个Nginx之间的负载均衡,但是由于LVS和F5都是单机的,所以必须要部署成主从或者双主模式来保证高可用并防止单点故障。

# 图示

阶段八图示

# 需要学习

  • LVS的使用及负载均衡算法

# 局限

LVS或F5也是单机的,在并发量达到几十万时最终也会达到性能瓶颈。同时,虽然系统是高可用的,但是机房断电断网等情况也有可能发生而影响整个系统的可用性。

# 阶段九:通过增加多条DNS记录实现不同机房间的负载均衡与异地多活

# 描述

配置域名解析时,一条域名可以配置多个ip地址,可以利用这种方式实现机房间的负载均衡、异地多活和机房级别的水平扩展。异地多活也是一个比较复杂的问题,比如跨机房数据备份等,其本质上是满足了CAP定理中的AP(可用性和分区容错性)。

在实现了机房级别的水平扩展后,系统将不存在性能瓶颈,接下来变化的原因更多是出于开发和运维方面的需求。

# 图示

阶段九图示

# 需要学习

  • 异地多活设计方案(不过这个大概只有架构师才用得到吧……)

# 局限

随着业务的增加,访问量不同的业务之间相互竞争资源,单体架构的Web应用越来越不堪重负,水平扩展十分浪费资源。由于代码行数巨多,开发和运维效率逐渐变得低下,难以进行灰度发布和局部更新,并且每一次更新都需要重新部署所有应用。

# 分布式架构、分布式部署

# 阶段十:单体应用拆分为小服务

# 描述

将过大的单体应用按照业务逐步拆分为比较小的“小服务”,服务之间使用轻量级的通信机制(RESTful APIRPC等)相互通信,每个服务由比较小的团队开发,服务之间可以做到独立升级和迭代。这还会涉及单点登录、分布式事务、服务发现、配置等一系列问题,可以使用Zookeeper等中间件解决。

服务拆分后日志散落在各个服务之中,需要使用ELK等分布式日志系统进行日志的收集与分析。

此外,较大的数据量也带来了搜索的需求,可以使用ElasticSearchSolr等搜索引擎来解决。

# 图示

阶段十图示

# 需要学习

  • 服务的拆分原则
  • RPC框架(gRPCDubboThrift等)
  • HTTP远程调用框架(Feign等,可选)
  • 分布式日志系统(ELK等)
  • 搜索引擎(ElasticSearchSolr等)
  • 分布式事务解决方案(两段式提交、Seata等)
  • 单点登录

# 局限

服务拆分后,不同服务之间的访问方式不同,服务可以被外界调用,服务之间也可能互相调用,调用链变得混乱。

# 阶段十一:使用ESB解决服务间接口差异的问题

# 描述

引入ESB(Enterprise Service Bus, 企业服务总线),使用ESB统一进行访问协议转换,外界通过ESB来访问服务,服务与服务之间也通过ESB来相互调用,以此降低系统的耦合度。

这就是我们熟知的SOA(Service Oriented Architecture), 面向服务的架构。SOA的核心思想在于“整合”,即通过ESB将不同的服务协调配合成一个统一的整体。

# 图示

阶段十一图示

# 需要学习

  • ESB(企业服务总线)

不过我怎么感觉现在都没有人用SOA了?好像现在的Web应用到这个规模之后都直接改微服务了。但是SOA毕竟也是存在过的,还是有单独说一说的必要的。

# 局限

服务拆分的粒度不够细,相同的代码可能存在多份,这些代码升级的时候所有的服务都必须跟着升级。此外,作为整个系统的中心,笨重的ESB逐渐成为了系统的瓶颈。SOA仍然不是一种去中心化的解决方案。

# 阶段十二:小服务进一步拆分为微服务

# 描述

微服务架构风格是一种将一个单一应用程序拆分为一组小型服务(微服务)的、去中心化的架构风格。每个微服务都可以由2-5人的小团队开发、独立测试、独立发布、独立部署、使用不同技术栈,服务之间使用轻量级的通信机制相互调用。微服务架构的服务拆分的粒度比SOA更细,核心思想与SOA相反——不是把服务整合起来,而是“化整为零”,一点点地解决掉一个大问题。

在系统改造为微服务架构之前,需要先部署好微服务基础设施——服务注册与发现中心(如Zookeeper(不推荐)、EurekaNacos DiscoveryConsul等)、微服务配置中心(如ApolloNacos ConfigSpring Cloud Config等)、调用追踪与调用链分析(如ZipkinSkyWalking等)、敏捷基础设施(CI/CD工具)等。同时,因为对外的RESTful API分散在各个服务之中,必须使用API网关(如ZuulSpring Cloud GatewayKong,还有KubernetesIngress等)将其整合并统一暴露给客户端进行调用。

目前最火的微服务框架非Spring Cloud莫属,要想拿到高薪,Spring Cloud是必须要会的。

因为整个微服务架构的复杂度非常高,所以必须“为失败而设计”,充分考虑到各种可能产生失败的情况,整个系统不允许出现单点故障。

# 图示

注:这里空间不够了,没画出LVS和F5,并不是没有那一层了。

阶段十二图示

# 需要学习

  • Spring Cloud Netflix(EurekaRibbonFeignZuulHystrix等)
  • Spring Cloud Alibaba(NacosDubboSeataRocketMQ等)
  • Spring Cloud原生组件(Spring Cloud GatewaySpring Cloud Config等)
  • 调用链追踪工具(ZipkinSkywalikng(推荐)、Cat等)
  • DevOps思想及其实践
  • 敏捷开发相关知识点
  • CI/CD

# 局限

微服务改造后,应用和服务的部署变得复杂——因为微服务架构对环境需求较高,如果同一台服务器上部署多个服务还要解决运行环境冲突等问题。对于秒杀这类需要动态扩缩容、水平扩展的场景,就需要在新增的服务上准备运行环境,部署服务等,运维难度非常大。

# 阶段十三:使用容器化技术解决微服务运维困难的问题

# 描述

Docker是目前最流行的容器化技术,而Kubernetes是目前在容器编排领域的无冕之王,可以很方便地使用Docker将微服务制作成镜像之后使用Kubernetes部署到生产环境,大大降低运维成本。

并且容器化对弹性化扩缩容也是非常有好处的——在遇到诸如秒杀这样需要大量资源的业务场景时,可以在更多服务器上使用Kubernetes启动更多的Docker镜像来增强系统的处理能力,流量洪峰过去之后就可以关闭这些镜像,实现动态扩缩容。

# 图示

图中IngressKubernetes的网关。空间不够了,没画出LVS和F5,并不是没有那一层了。

阶段十三图示

# 需要学习

  • Docker(DockerFiledocker-compose等)
  • Kubernetes(yml文件部署服务、RBACIngressEtcdhelm等)

# 局限

虽然容器化可以很方便地实现动态扩缩容,但是服务器仍然需要企业自行管理,遇到秒杀这种场景时必须申请大量资源,结束后这些资源不能得到有效的再利用,造成资源浪费。

# 阶段十四:系统部署到云平台

# 描述

经过容器化改造的系统可以部署到公有云、私有云或者混合云(目前混合云是主流)上,利用云的海量硬件资源,解决弹性扩缩容困难、运维困难的问题,做到真正的按需付费。

# 图示

与上一个阶段相同,但是运行环境挪到了云上。

# 需要学习

  • 云计算、云原生相关技术

# 局限

微服务、云计算和容器化虽然解决了绝大多数的问题,但是也带来了服务治理逻辑(由Spring Cloud等框架实现,以SDK的形式嵌入业务代码)与业务代码耦合的缺点。当SDK升级且不向下兼容时,业务逻辑就算没有发生任何变化也必须跟着升级。并且,使用Spring Cloud意味着系统绑定了Java语言,不符合微服务定义中“可以使用不同语言开发”的特点。

此外,SDK也没有提供服务调度、资源分配等微服务所必需的功能。这些功能必须借助Kubernetes来实现,但是Kubernetes与Spring Cloud功能上有冲突,就算是为整合Kubernetes而设计的Spring Cloud Kubernetes也很难与Kubernetes完美配合,两者在整合中经常会出现各种各样的问题。

# 阶段十五:使用服务网格实现零侵入的服务治理

# 描述

Linkerd世界上最好的框架Istio为代表的服务网格(Service Mesh)彻底把治理逻辑从业务代码中剥离出来,成为了独立的进程(Sidecar)。部署时两者部署在一起,业务代码完全感知不到Sidecar的存在。这就实现了治理逻辑对业务代码的零侵入——实际上不仅是代码没有侵入,在运行时两者也没有任何的耦合。这使得在整个微服务架构体系中不同的微服务完全可以使用不同语言、不同技术栈来开发,也不用在业务代码中解决服务治理问题。

服务网格也可以很方便地与Kubernetes进行整合,进而实现方便的部署和动态扩缩容。

# 图示

以Kubernetes + Istio为例(所以图上写的是Pod、数据平面、控制平面)。图中只画出了Service Mesh本身。

这绝对是全篇里我画的最用心的一张图

阶段十五图示

# 需要学习

  • Service Mesh框架(IstioLinkerd等)

# 局限

目前还没有看到Service Mesh的局限——或许性能不佳是其最大的局限吧——据测试,原来能承载30-35KQPS(每秒千次查询数)的微服务系统被改造成基于Linkerd的Service Mesh后就只能承载大约10-12KQPS,如果是基于Istio则更惨,仅仅有3.2-3.9KQPS——当然,在Istio变成了单体架构、取消了笨重的Mixer之后,这个成绩大概会有所改善吧。但是,我一直认为Service Mesh就是大型Web应用最终也是完美的形态。

那么,下一次演进会是Serverless吗?还会有可以击败Service Mesh的更完美的形态出现吗?

让我们拭目以待吧。

# 结语

本文是按照后端技术从诞生到现在每一次的变化为主线来构思的。其实,现在的Web应用已经几乎不会按照这个方式演进了——几乎所有的Java Web应用都会从MVC模式开始,很大一部分单体架构的Web应用在第一次部署就被装进了容器、放入了云平台,因为微服务的流行导致很多应用被拆分时直接就变成了微服务架构而不会经过SOA这一步,甚至有的单体应用过大后直接就变成了Service Mesh,等等。

为什么那么多人前赴后继地跳进后端这个看不到天花板的“无底洞”里呢?我想,后端与其他开发领域(前端、底层、嵌入式、移动端、客户端等)比起来,更像是一个“世界”,那些人大概也是被后端这种大气、严谨、协调的美吸引了,也想要亲手创造或者参与建设这样一个世界吧。毕竟,任何一个能联网的应用都是需要这样一个“世界”来支撑的。