俯瞰 Java 服务端开发
Java 服务端开发是一个非常宽广的领域,要概括其全貌,即使是几本书也讲不完,该文将会提到许多的技术及工具,但不会深入去讲解,旨在以一个俯瞰的视角去探寻这片领域。
目录
框架
Spring Boot
Spring 框架已经成为 Java 服务端开发领域里的标配,无数的服务基于其开发,它整合了服务端开发所需的绝大多数组件,Spring Boot 在其基础上又做了一层轻封装并简化了依赖管理,使得它用起来更加的便捷。
Vert.x
Spring 框架早已成为主流,但是我们也不能忽略了其他优秀框架的存在。
Vert.x (opens new window) 是在 JVM 基础上构建响应式应用的一套工具集,支持多种语言,它不仅是一套工具集,也可视作是一套框架,其中包含使用 Netty 编写的 Web 框架、gprc、redis 客户端等众多组件,囊括了大部分开发网络应用时所需用到的组件,它最重要的核心概念是使用了事件驱动的非阻塞模型,因此具备高度的可伸缩性。它使用了响应式的编程模型,这个话题在下文中会再提到。
网络
五层协议
学习计算机网络时一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,即物理层、数据链路层、网络层、运输层、应用层,每一层都有其各自的术语,比如:吞吐量、子网掩码、VIP、DNS等等,这在平时工作的沟通过程中也是至关重要。要做好服务端编程,我们必须对网络的一些基本概念有一个清晰的认识,推荐阅读《计算机网络:自顶向下方法》。
推荐阅读:
HTTP 协议
对于服务端编程而言,在网络这个部分最重要的还是 HTTP 协议,从 TCP、DNS ,最后到浏览器响应,我们必须清楚整个过程是如何运转的,中间再加入 CDN、反向代理、流量控制等服务时,其会更加复杂,但也正因为网络的分层模型,使得我们可以在这个中间过程中对服务端的响应性能做出优化。
具体到 HTTP 协议,其承载于 TCP 协议之上,中间再加上 TLS 或 SSL,就成了 HTTPS ,协议头如何解析,响应体如何发送,搞清楚了这些,可以很容易地开发一个简单的 HTTP 服务。HTTP 协议也在不断改进,目前已经到了 2.0 版本,在传输性能上有大幅的提升。
HTTP 使用明文传输,因此很容易受到中间人攻击,可以在路由器、代理等多个层面截获传输信息,因此 HTTP 终将退出历史舞台,HTTPS 必然成为主流,但是 HTTPS 也并非绝对安全,由于证书签发机构存在安全漏洞,曾导致许多网站使用了不安全的 SSL 证书,因此很多应用会采用自定义的加密方式来加强信息传输的安全性。
TCP 拥塞控制
TCP 使用多种拥塞控制策略来避免发送方至接收方之间的链路变得拥塞,其有许多具体的实现算法,具体的实现细节隐藏在操作系统的内核当中,通过使用不同的算法,可以在不同的场景下获得最佳的性能,例如 Google 设计并发布的 BBR(Bottleneck Bandwidth and Round-trip propagation time)拥塞算法,它能更有效地利用网络环境,尤其在超远距离的网络传输中能获得更大的性能提升,目前已经移植到 linux 内核4.9版本。
由于许多网络层相关的算法都隐藏在操作系统内核当中,普通计算机用户一般无需理解这些概念,但是对于服务端开发者来说,若对其有一定的了解,则能够从这一层面寻找解决方案来提升系统的吞吐量。
网络 I/O 模型
常见 I/O 模型主要有 BIO(阻塞I/O),NIO(非阻塞I/O),I/O复用、事件(信号)驱动I/O、AIO(异步I/O)。以读取数据为例,传统的 BIO 里面调用 socket 的 read 方法,函数在收到数据前会一直阻塞,对于 NIO,如果有数据则返回,反之返回 0,不会发生阻塞,而 AIO 则更进一步,不光等待数据就绪是非阻塞,连数据从网卡到内存的过程也是异步的。
结合使用 NIO、AIO、I/O复用,可以解决线程瓶颈并处理海量连接,比如 nginx 使用了 AIO 模型,因此性能比 apache http server 性能更好。在 Java 领域,Netty 基于 Reactor 模式实现了一个异步事件驱动的 NIO 框架,其已经运用在互联网的许多领域,大到大数据、通信行业、游戏行业,小到 redis 客户端、Web 框架等开源组件都有其身影。
数据库
关系型数据库
MySQL 是最流行的开源数据库,PostgreSQL 是最高级的开源数据库,SQL Server 是微软开发的企业级数据库,还有在大型公司用的较多的 Oracle 数据库。在服务端开发方面,MySQL 的市场占用率是最高的,但也推荐学习一下 PostgreSQL 和所谓的「企业级数据库」,毕竟 MySQL 在这些数据库面前有时确实显得功能简单、实用性不足。
在真实的工作中,数据库的设计是一个非常需要平衡取舍的过程,有时为了优化查询性能不得不做一些数据冗余,而在数据量极大的情况下,又必须谨慎选择每一列的存储类型、避免冗余。
数据量非常大的情况下,大多数时候还要进行分库分表的设计。
- ShardingSphere
- 目前 Java 中主流的分库分表中间件,支持客户端架构、代理架构,Sidecar 架构目前还在开发中。
- Vitess
- Vitess 是 Youtube 开源的 MySQL 数据库集群系统,采用的是中心化的数据库代理架构,这套数据集群承载了 Youtube 数以亿计的数据量和访问请求。
存储引擎
MySQL 中主流使用的是 InnoDB 存储引擎,内部采用了 B+ 树的索引结构,Percona XtraDB 是InnoDB 存储引擎的增强版,Percona 兼容 MySQL,号称拥有更好的性能,也具有一定的市场占有率。
除了 InnoDB 及其衍生引擎,RocksDB 也是一个可选项,这是一个 LSM 存储引擎,不同于传统的基于 B+ 树的存储引擎,基于 LSM 存储引擎的数据库尤其适合写多读少的场景,由于最初是设计用来做持久化的键值数据存储,因此在 KV 存储上具有非常高的性能,可惜的是 MySQL 无法选择 RocksDB 作为存储引擎,目前支持的数据库有 MariaDB 和 Percona。
NewSQL
NewSQL 这一新兴领域也大量使用了 RocksDB 作为存储引擎,TiDB 作为流行度较高的 NewSQL 产品,就是用其实现的数据持久化。
NoSQL 数据库
- MongoDB
- MongoDB 介于关系数据库和非关系数据库之间,不要求数据存储具有固定的模式,且能用于存储超大规模的数据集。
时序数据库
随着互联网的深入,应用场景越来越丰富,诸如系统运行状态、系统指标采集等场景产生大量的数据,这类基于时间的一系列数据,以写多读少、数据量极大为特点,传统的数据库已经不适合存储这类数据,时序数据库由此诞生。
主流的时序数据库有:
- influxdb
- Prometheus
- graphite
列式数据库
传统的关系型数据库采用行式存储,大数据领域多采用列式存储,列式存储的主要优势在于可以按需所取,在并行处理和数据压缩上更有优势。关系型数据库适合 OLTP, 列式数据库更适合 OLAP,为了使列式数据库能更好地支持 OLTP,目前出现了像 kudu 和 Druid 这类优秀的开源产品,它们结合了列式存储的优势,并在 OLTP 方面也做了特别的优化。
主流的列式数据库有:
- HBase
- Cassandra
- kudu
- Druid
嵌入式数据库
传统的关系型数据库能够支持企业级的应用,但在许多场景下,我们可能只需要一个小型应用,这个时候使用嵌入式数据库是一个方便的选择,除此之外,嵌入式数据库非常适合用于做单元测试。
Java 中流行的嵌入式数据库有:
- h2base
- moby
中间件
Web Server
- Nginx
- Nginx 使用 AIO 的模型实现高并发,Apache 每个请求独占一个线程。
- AIO 模型适合于 IO 密集型服务,多进程或线程适合于 CPU 密集型服务,由于大多数 Web 服务都属于 IO 密集型,nginx 的市场占有率逐渐超过了 Apache。由于这一特点,Nginx 也非常适合做反向代理,通过这种机制做负载均衡也是非常主流的一种方案。
- tomcat、jetty、weblogic 等传统 Java Web 服务器
- 随着容器化技术的流行,这类服务器日渐式微,市场占有率逐渐下降,进行容器化部署时tomcat一般内置在程序中,这种进步使得开发者可以更关注业务代码本身,而无需关注此类服务器的种种细节,可谓是对开发人员的减负。
- OpenResty
- 优秀的开源产品经常出现许多优秀的衍生产品,比如 Percona 之于 MySQL,OpenResty 之于 Nginx,Kong 之于 OpenResty。
- Nginx 市场占有率之高,但许多场景下是用其做反向代理,OpenResty 的设计目标则是让 Web 服务直接跑在 Nginx 服务内部。
- OpenResty 同时也是基于 LuaJIT 的 Web 平台,开发者可以很方便地使用 Lua 调用 Nignx 模块,具有强大的可扩展性,比如可将典型的 Nginx + Tomcat + MySQL 架构更换为 Nginx + Lua + Redis + Tomcat + MySQL 的架构。
- Kong 从技术上讲也属于 Web Server,但一般用来做 API 网关,下文中再详述。
分布式缓存
- Redis
- Redis 作为一个高性能的内存数据库,目前已被广泛使用,其支持多种数据结构,根据不同场景使用不同的数据结构,才能最有效地使用它。
KV 存储
- Pika
- Redis 的性能非常高,但在将其做数据库使用时存在数据持久化的问题,Pika 就是为了解决这一问题而出现,它底层基于 RocksDB,修改了其部分源代码,在 KV 数据持久化上有非常高的性能,与基于内存的 redis 相比仅有较小的性能下降,同时它还兼容大部分的 redis 协议,与 Redis 的使用几乎没有差异,上手简单。
- Tair
- Tair 与 Pika 类似,底层支持多种存储引擎,包括 mdb、rdb、ldb,其中 ldb 基于 leveldb(google开源,rocksdb 在其基础上优化),它可将内存存储和持久化相结合,具有高可用的分布式架构,目前开源版本已经不再维护,阿里云上则提供了企业级的 Tair 存储服务。
- SSDB
- SSDB 也是兼容 Redis 的一款 KV 数据库,目前更新频率较低,相比而言 Pika 目前还在更新中,且有企业进行背书。
消息队列
消息队列在请求削峰、跨系统间通信解耦、发布订阅等许多场景下都会使用到,不光能解决这些问题,采用消息驱动的架构可以增强系统的扩展性,比如新增一个订阅方,即可以实现新的功能,并且对当前的系统没有任何的侵入性。
常用的消息队列产品有 kafka、rabbitmq 等,它们各有优缺点,在大数据领域 kafka 占有绝对优势,总体的市场占有率也较高,而 rabbitmq 由于产品成熟,也被广泛使用。
在使用消息队列的过程中,需要处理一系列的细节,比如:定义消息处理者、如何发送消息、如何发布事件、消息如何序列化、如何记录消息记录、设计消息路由、消息处理失败的重试机制、消息 id 等等,在具体的编码过程中不能完全专注于业务代码开发,因此市面上有一些 ESB 产品在内部处理好了这些细节,并从更高的抽象层级提供更加简洁的 API,在开发过程中则能更加聚焦在业务逻辑本身,当我们的系统面临这些问题的时候,不妨选择一个 ESB 产品来提升研发效率。
定时调度
简单的定时任务可以采用 linux cron 进行配置,复杂的场景也可以使用分布式任务调度框架,可选的实现方式非常多,这里简单的列举几种。
- Quartz
- 老牌任务调度系统,许多分布式任务调度框架基于它而扩展。
- Spring Scheduler
- 用它来做简单的任务调度非常方便,但要注意由于现在的系统大多采用分布式部署,因此当使用它来做任务调度时最好做到单独的服务中,避免与其他系统耦合。
- 国产分布式任务调度系统
- 目前较为流行的有 Elastic-Job、XXL-JOB,Elastic-Job 采用去中心化的架构,依赖 zookeeper 存储任务调度数据,XXL-JOB 采用中心化调度的架构,调度采用 RPC 方式。
- PowerJob 是新兴的一个开源任务调度系统,在功能上更为强大,支持 MapReduce 分片,值得关注。
RPC
提到 RPC 不得不提到日暮西山的 Web Service,其采用 XML 作为消息格式,并以 SOAP 协议进行封装,由于过于复杂且性能开销较大,其逐渐被采用 JSON 格式的 REST 服务所取代,相比之下,REST 简单且采用更高效的序列化方式,所以目前许多系统广泛采用 HTTP 的方式进行远程过程调用。
在对于性能要求特别高的场景,或从整体架构上考虑,人们才会选用专门的 RPC 产品,这类系统一般拥有更高效的通讯协议和数据传输格式,典型的有 dubbo、grpc、thrift,其中 grpc 具有最优秀的性能。
RPC 框架的原理其实与 HTTP 调用类似,只是采用了更精简的协议头和数据序列化方式,此外在服务注册发现及负载均衡上也做了专门的封装。在 Spring Cloud 中,使用 OpenFeign 进行服务间调用是非常方便的一个选择,其使用 HTTP 方式,当性能无法满足时,可考虑替换序列化方式,或选用 grpc 进行通信。
数据库中间件
数据库本身就是一个庞大的产品,除了前面提到的 ShardingSphere、Vitess 这类中间件,还有一类专门做数据处理的中间件。
- otter
- 分布式数据库同步系统,支持 MySQL、Oracle。
- canal
- 基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。
- DataX-Web
- 分布式数据同步工具,可用来简化 ETL 工作。
- gh-ost
- 对数据表结构进行架构变更时,可能导致表被锁住,如果数据量特别大,这种问题对于线上发布的影响是比较大的,可以采用建新表并迁移数据再修改表名的方式手工处理,这种方式容易出错且耗时,Github 开源的 MySQL 在线架构迁移工具则是程序化完成这一类操作的很好的选择。
日志系统
- ELK
- 日志系统一般采用 ELK 技术栈,这其中包含三个子系统,因此要扩展一个新功能,可以有多种方式切入,比如做监控报警,可以使用 logstash 将 metrics 写入到 Prometheus,也可以使用 kibana 上的 sentinl 插件或者 ElastAlert 插件。
- logstash 支持从许多管道收集数据,其中包括 kafka,在日志量特别大的情况下,可以将日志先发送至 kafka。
- Sentry
- 日志在很大一部分场景下都是用于排查错误的,除了 ELK 外还有专注于应用程序错误报告的系统,比如 Sentry。
配置中心
由于越来越多的系统基于 docker 部署,配置中心不仅可以简化系统的配置管理,也可简化系统的发布流程,目前较为流行的开源配置中心是 Apollo。另外也可以通过 zookeeper、Consul 等工具来实现统一配置管理。
Nacos 是阿里开源的一款集配置中心和注册中心于一体的系统,使用它来做配置中心也较为方便,服务端部署相比 Apollo 简化了许多。
微服务
由于单体应用牵一发而动全身的特点,许多大型应用在开发时都会自觉拆分为多个子系统,这是在微服务概念提出前就被广泛采用的方式,而微服务概念的提出则更进一步,提出了一种全新的系统开发方式,使系统可以方便地拆分到更小的粒度,即微型服务,那么在服务数量越来越多的情况下,服务治理、熔断降级、链路追踪等问题也浮出水面,于是解决这些问题的 Spring Cloud 框架冉冉升起。
服务注册与发现
主流的服务注册与发现组件有:Eureka、Consul、Nacos 等等,它们采用不同的 CAP 分布式一致性规则或多种都支持,但不管使用哪一种,其实还是存在服务失联的问题,比如在滚动更新的过程中,注册中心未能及时剔除掉服务,导致调用方仍在调用停止的服务,首先我们可以通过调整配置减少更新周期,必要时需要修改其源代码,使用长连接,只要连接中断即从注册中心剔除服务,具体的细节需要专门写一篇文章来讲解。
在可能的情况下,尽量使用消息机制来进行服务间通信,这是一个更好的选择,除了更好地进行解耦,在滚动更新这个部分也能更好地保持系统不间断运转。
熔断与降级
服务间的调用过多,一定程度上增加了系统的耦合度,当其他微服务出问题或响应较慢时,整个系统都受影响,在必要时需要对出问题的服务进行熔断或降级。
- Hystrix
- Spring Cloud 框架默认集成的熔断组件。
- Sentinel
- Spring Cloud Alibaba 中集成的熔断组件,提供了一个外部控制台,可以实时调整系统的熔断降级配置,在这个部分强于 Hystrix 。
链路追踪 / APM
服务间互相调用,使得调试变得比单体应用复杂不少,这个时候使用链路追踪工具能够简化调试,同时也能够对应用程序的性能有更直观的监控。
主流的链路追踪组件有:
- zipkin
- pinpoint
- SkyWalking
- jaeger
API 网关
Spring Cloud 体系中常用的网关前有 Zuul,后有 Gateway,这一类跟 Spring Cloud 结合紧密,使用方便,但由于它们都是 Java 写成,在许多场景下还是比不上一些专门的网关产品。
- Kong
- Kong 是 OpenResty 的衍生开源网关产品,拥有优秀的性能和丰富的插件,可满足许多的扩展性需求。
- Traefik
- Traefik 是用 Go 语言编写的网关,定位是云原生的边界路由网关产品,它拥有丰富的特性、易用的控制面板,与云原生场景深度结合,提供了实时的流量指标可对接到 Prometheus 中。其企业版包含限流、高可用等特性,开源版在这一部分有所缺失。
服务网格
从单体应用到微服务的演进,我们会发现服务治理、熔断、Tracing 这些几乎是必不可少的部分,即使是使用 Spring Cloud 框架,我们也需要关注大量的微服务技术细节,为了分离这一关注点并使这些技术成为基础设施一般的存在,服务网格应运而生。
什么是Service Mesh(服务网格)? (opens new window)
服务网格好比微服务间的 TCP/IP,负责服务之间的网络调用、限流、熔断和监控。对于编写应用程序来说一般无须关心 TCP/IP 这一层(比如通过 HTTP 协议的 RESTful 应用),同样使用 Service Mesh 也就无须关心服务之间的那些原本通过服务框架实现的事情,比如 Spring Cloud、Netflix OSS 和其他中间件,现在只要交给 Service Mesh 就可以了。
目前主流的服务网格有:
- Istio
- Linkerd
常用开源组件
上文有提及的,这里不再累述。
数据访问
- MyBatis Plus
- Mapper
- jOOQ
- JPA
- dynamic-datasource-spring-boot-starter
- sharding-jdbc
工具组件
- guava
- commons-lang3
- hutool
缓存
- redission
- jetcache
- caffeine
字节码修改
- asm
- javassist
- cglib
http客户端
- okhttp
- Aache HttpClient
- retrofit
- openfeign
响应式编程
- RxJava
- reactor-core
序列化
- protobuf
- protostuff
- hessian
分布式事务
- seata
事件驱动框架
- AxonFramework
规则引擎
- drools
测试
- junit
- mockito
- Spock
编程思想
编程思想是一个抽象的概念,要将其具象化我们必须透过现象看其本质,优秀的编程思想是对各种优秀想法的组织,这些想法可以精炼成许多原则,原则是构成编程思想的一个重要部分,也是所有编程方式都可以遵守的通用准则。在原则的基础上,在编码过程中反复解决的一些问题又被归纳为模式,这两者是思想的主要构成,另外也有不同的编程范式及方法论,我在这里简单的讲一下设计原则。
原则
很多原则不仅适用于编程领域,也适用于其他领域,我想这也是为什么乔布斯提倡人人都应该学习编程,因为它能让你拥有更好的思考方式。
- 保持简单
- Keep It Simple, Stupid (KISS)
- 最重要的原则之一,可靠来源于简单,只有不断保持系统的简单、代码的简单,才能更好地创造优秀的软件。
- You Ain’t Gonna Need It (YAGNI)
- 如无必要,勿增复杂性,避免过度设计。
- Separation of Concerns (SoC) – 关注点分离
- 将目标相关联的部分封装在一起,标识为关注点。这是降低复杂性的一个重要原则,MVC 或 MVP 模式都是该原则的应用,将模型、视图和控制器作为不同的关注点,使得每一个关注点可以更有效地理解及重用。
- 在编码过程中,也可以应用这一思想,比如我们首先关注应用程序是否可用,当其运行正确后再关心运行效率,这比同时进行这两项工作要简单的多。
- Keep It Simple, Stupid (KISS)
- 不要重复
- Don’t Repeat Yourself (DRY)
- 最简单也最容易理解的原则,每个程序员都应该以随意复制粘贴代码而感到羞愧。
- Convention over Configuration(CoC)- 惯例优于配置原则
- 将约定的配置方式和信息作为缺省的规则来使用,可以减少开发人员做决定的数量,减少编码量,获得简单的好处,又不会丢失灵活性。
- Spring Boot 框架解决的问题之一就是简化项目的配置,其大量应用了 CoC 原则。
- Don’t Repeat Yourself (DRY)
- S.O.L.I.D 原则
- Single Responsibility Principle (SRP) - 单一职责原则
- 一个类,只做一件事,并把这件事做好,其只有一个引起它变化的原因。
- 很简单的原则,但是很多程序员在工作时经常违反这一原则,比如一个 service 类中引入许多 dao 对象,提供多种不相关服务。
- Open/Closed Principle (OCP) – 开闭原则
- 模块是可扩展的,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。
- 设计模式中的代理、策略和观察者模式比较好地实现了这一原则。
- 当我们定义的一个API可接受函数作为参数时,实际上也是一种策略模式的变体,同样也体现了这一原则。
- Liskov substitution principle (LSP) – 里氏代换原则
- 子类必须能够替换成它们的基类。
- 这个原则可作为我们设计类继承关系的基准。
- Interface Segregation Principle (ISP) – 接口隔离原则
- 对接口进行拆分,使用多个专门的接口比使用单一的总接口要好。
- 接口可以多继承,那为何要因为懒惰而将其随便定义在一个总接口里呢?
- Dependency Inversion Principle (DIP) – 依赖倒置原则
- 高层模块不应该依赖于低层模块的实现,而是依赖于高层抽象。
- IoC 是 DIP 的一个具体实现,其已经深入到编程语言当中,Spring 框架最初就只是作为一个 IoC 容器,而后才不断扩展出许多实用功能并最终成为一个开发框架。
- 相关原则:Hollywood Principle – 好莱坞原则(所有的组件都是被动的,所有的组件初始化和调用都由容器负责)。
- Single Responsibility Principle (SRP) - 单一职责原则
- 高内聚、低耦合
- Law of Demeter – 迪米特法则
- 又称“最少知识原则”(Principle of Least Knowledge),一个类对于其他类知道的越少越好,知道的越多其耦合程度就越高。
- 门面模式和中介模式都是迪米特法则应用的例子。
- 这一原则强调低耦合。
- Common Closure Principle(CCP)– 共同封闭原则
- 如果必须修改应用程序里的代码,我们希望所有的修改都发生在一个包里(修改关闭),而不是遍布在很多包里。
- 在微服务架构中,若修改一个功能时,经常需要修改多个服务,那么其很有可能违反了 CCP 原则不恰当地进行了服务拆分。
- 这一原则强调高内聚。
- Common Reuse Principle (CRP) – 共同重用原则
- 包的所有类被一起重用,没有被一起重用的类不应该被组合在一起。依赖一个包就是依赖这个包所包含的一切。
- CCP则让系统的维护者受益,CCP让包尽可能大(CCP原则加入功能相关的类),CRP则让包尽可能小(CRP原则剔除不使用的类)。它们的出发点不一样,但不相互冲突。
- 这一原则同样强调高内聚。
- Law of Demeter – 迪米特法则
结语
正如开头所言,服务端开发领域极其庞大,本文还有许多枝节尚未提及,比如安全、DevOps等等。技术的演进使这个领域加速扩大,未来还会有许多的变化,也许 Service Mesh 将会成为主流,也许 NewSql 将成为开发标配,任何一门技术的演进历史变长,它的总体学习时间相应也会增加,但这并不意味着对其的应用也会变得复杂,我们不能忽视云计算这一因素,云服务的提供为开发者隐藏了许多的细节,在这个时代不需要知晓每一项技术的原理,也能够开发出服务千万用户的产品。
未来会演进成什么样,可以去期待,但不要只是去期待,因为未来已经到来,只是还没有平均分布。