想成为一名顶尖Java开发工程师?这些优化手段一定要掌握!

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
数据传输服务 DTS,数据迁移 small 3个月
推荐场景:
MySQL数据库上云
简介: JVM调优、Redis调优、MySQL调优、消息中间件调优、全文搜索ES调优、系统架构调优。

@[TOC](文章目录)


![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/6abff44e185a4ce1b4a1a3962b03b769.png)



# 服务器配置

## 文件系统参数、TCP网络层参数等系统参数


在CentOS 7上,可以使用以下命令来配置参数:


1. 打开配置文件/etc/sysctl.conf:


```java

sudo vi /etc/sysctl.conf

```


2. 添加或编辑以下参数:

```java

# 设置虚拟内存超配值为 1,可以允许分配比实际物理内存更多的内存空间,从而提高应用程序性能,但可能导致 OOM 错误。为 0 时表示内存空间不足时直接拒绝申请

vm.overcommit_memory = 1

# 设置系统脏页(未写入磁盘的页面)达到多少字节时可以开始写入磁盘

vm.dirty_background_bytes = 8388608

# 设置系统脏页达到多少字节时必须写入磁盘

vm.dirty_bytes = 25165824

# 设置可以在后台写入磁盘的脏页占总脏页的比例(即总脏页数的2%)

vm.dirty_background_ratio = 2

# 设置当系统脏页占总内存的比例超过5%时,系统必须开始写入磁盘

vm.dirty_ratio = 5

# 设置系统判定一个脏页需要写入磁盘的时间,单位为centisecond,即2000分之一秒

vm.dirty_expire_centisecs = 2000

# 设置最小内存分配单位(单位为KB)

vm.min_free_kbytes = 8192

# 将虚拟内存的 overcommit 比率设置为80%

vm.overcommit_ratio = 80

# 设置堆内存溢出处理方式(0表示不紧急内存压缩,1表示紧急内存压缩,2表示杀死进程)

vm.panic_on_oom = 2

# 设置发送方socket buffer大小的最大值为16MB

net.core.wmem_max = 16777216

# 设置接收方socket buffer大小的最大值为16MB

net.core.rmem_max = 16777216

# 当TCP流量控制窗口溢出时,中止连接

net.ipv4.tcp_abort_on_overflow = 1

# 对于高延迟、高带宽的网络,开启窗口缩放

net.ipv4.tcp_adv_win_scale = 1

# 允许使用的TCP拥塞控制算法,可以使用cubic和reno算法

net.ipv4.tcp_allowed_congestion_control = cubic reno

# 应用程序socket buffer的大小,单位为Kbyte

net.ipv4.tcp_app_win = 31

# TCP发送数据时,自动开启corking模式

net.ipv4.tcp_autocorking = 1

# 允许使用的TCP拥塞控制算法,可以使用cubic和reno算法

net.ipv4.tcp_available_congestion_control = cubic reno

# 设置TCP数据包的最小大小,单位为byte

net.ipv4.tcp_base_mss = 512

# 发送方最多允许发送多少个SYN报文段作为challenge ack防范syn flood攻击

net.ipv4.tcp_challenge_ack_limit = 1000

# TCP使用的拥塞控制算法,可以使用cubic算法

net.ipv4.tcp_congestion_control = cubic

# 开启对方乱序数据的确认,以降低网络延迟

net.ipv4.tcp_dsack = 1

# 当检测到丢包时,提前触发重传

net.ipv4.tcp_early_retrans = 3

# 开启ECN(Explicit Congestion Notification)拥塞控制算法

net.ipv4.tcp_ecn = 2

# 使用FACK(Forward Acknowledgment)作为拥塞控制算法的一部分

net.ipv4.tcp_fack = 1

# 开启TCP Fast Open,以加快连接速度

net.ipv4.tcp_fastopen = 3

# 设置TCP Fast Open使用的密钥,可以使用随机数生成器生成

net.ipv4.tcp_fastopen_key = 6d0c41a3-123fdf85-a7f901e8-59fea180

# TCP连接关闭的超时时间,单位为秒

net.ipv4.tcp_fin_timeout = 10

# 开启TCP Fast Recovery防止网络拥塞

net.ipv4.tcp_frto = 2

# 设置TCP连接每秒允许的最大无效数据包数,超过该值则降低发送速度

net.ipv4.tcp_invalid_ratelimit = 500

# TCP保持连接的时间间隔,单位为秒

net.ipv4.tcp_keepalive_intvl = 15

# 发送TCP保持连接探测报文的次数

net.ipv4.tcp_keepalive_probes = 3

# TCP保持连接的时间,单位为秒

net.ipv4.tcp_keepalive_time = 600

# 限制发送缓存的最大空间,单位为byte

net.ipv4.tcp_limit_output_bytes = 262144

# 开启TCP低延迟模式

net.ipv4.tcp_low_latency = 0

# 操作系统允许的最大TCP半连接数

net.ipv4.tcp_max_orphans = 16384

# TCP拥塞窗口增长算法的阈值,一般设为0不使用该功能

net.ipv4.tcp_max_ssthresh = 0

# 等待建立连接请求的最大个数

net.ipv4.tcp_max_syn_backlog = 262144

# 每秒最多处理的TCP连接数,越高则占用CPU时间越多

net.ipv4.tcp_max_tw_buckets = 5000

# 设置TCP Mem,包括min、default、max三个参数,单位为page数量

net.ipv4.tcp_mem = 88053 117407 176106

# 设置发送方socket buffer大小的最小值,单位为byte

net.ipv4.tcp_min_snd_mss = 48

# 设置TCP使用的最小TSO分段数目(只有在开启TSO时生效)

net.ipv4.tcp_min_tso_segs = 2

# 开启TCP自适应窗口大小控制

net.ipv4.tcp_moderate_rcvbuf = 1

# 开启TCP MTU探测,以避免网络分片

net.ipv4.tcp_mtu_probing = 1

# 禁止保存TCP延迟测量得到的数据

net.ipv4.tcp_no_metrics_save = 1

# 无需等待发送缓存为空,就可以发送数据

net.ipv4.tcp_notsent_lowat = -1

# TCP重传数据包的最大次数

net.ipv4.tcp_orphan_retries = 0

# TCP重传数据包后允许接收的最大乱序数据包个数

net.ipv4.tcp_reordering = 3

# 启用TCP Fast Retransmit和Fast Recovery算法

net.ipv4.tcp_retrans_collapse = 1

# 第一次重传TCP数据包的次数

net.ipv4.tcp_retries1 = 3

# 第二次重传TCP数据包的次数

net.ipv4.tcp_retries2 = 15

# 拒绝与RFC1337不兼容的数据包

net.ipv4.tcp_rfc1337 = 1

# 设置TCP接收缓存大小,包括min、default、max三个参数,单位为byte

net.ipv4.tcp_rmem = 4096 87380 33554432

# 开启TCP SACK(Selective Acknowledgments)支持

net.ipv4.tcp_sack = 1

# 关闭TCP连接空闲一段时间后再次发送数据包

net.ipv4.tcp_slow_start_after_idle = 0

# 禁用TCP Socket Urgent功能

net.ipv4.tcp_stdurg = 0

# TCP SYN请求重试的最大次数

net.ipv4.tcp_syn_retries = 1

# TCP SYN/ACK请求重试的最大次数

net.ipv4.tcp_synack_retries = 1

# 开启TCP SYN Cookie防止syn flood攻击

net.ipv4.tcp_syncookies = 1

# 关闭TCP Thin Dupack

net.ipv4.tcp_thin_dupack = 0

# 关闭TCP Thin Linear Timeouts

net.ipv4.tcp_thin_linear_timeouts = 0

# 开启TCP时间戳

net.ipv4.tcp_timestamps = 1

# 设置TCP TSO窗口大小的除数,只有在开启TSO时生效

net.ipv4.tcp_tso_win_divisor = 3

# 开启TCP TIME_WAIT Socket重用机制

net.ipv4.tcp_tw_recycle = 1

# 允许将TIME_WAIT Socket重用于新的TCP连接

net.ipv4.tcp_tw_reuse = 1

# 开启TCP窗口缩放

net.ipv4.tcp_window_scaling = 1

# 设置发送方socket buffer大小,包括min、default、max三个参数,单位为byte

net.ipv4.tcp_wmem = 4096 16384 33554432

# 关闭TCP workaround signed windows(https://tools.ietf.org/html/rfc7323

net.ipv4.tcp_workaround_signed_windows = 0

# 当使用conntrack跟踪TCP连接时,设置是否采用liberal模式

net.netfilter.nf_conntrack_tcp_be_liberal = 0

# 当使用conntrack跟踪TCP连接时,设置是否采用loose模式

net.netfilter.nf_conntrack_tcp_loose = 1

# TCP连接最大重传次数

net.netfilter.nf_conntrack_tcp_max_retrans = 3

# TCP连接关闭后,等待fin结束的时间,单位为秒

net.netfilter.nf_conntrack_tcp_timeout_close = 10

# TCP连接关闭后,进入CLOSE_WAIT状态的时间,单位为秒

net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60

# TCP连接已经建立时,如果长期没有数据传输,连接最长保持时间,单位为秒

net.netfilter.nf_conntrack_tcp_timeout_established = 432000

# 当关闭TCP连接时,TCP_FIN等待ACK的超时时间,单位为秒

net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 120

# 当关闭TCP连接时,ACK等待FIN的超时时间,单位为秒

net.netfilter.nf_conntrack_tcp_timeout_last_ack = 30

# TCP连接最大重传次数,以及TCP RTO

net.netfilter.nf_conntrack_tcp_timeout_max_retrans = 300

# 设置 TCP SYN_RECV 状态的超时时间为 60 秒

net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60

# 设置 TCP SYN_SENT 状态的超时时间为 120 秒

net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 120

# 设置 TCP TIME_WAIT 状态的超时时间为 120 秒

net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120

# 设置 TCP 未确认连接的超时时间为 300 秒

net.netfilter.nf_conntrack_tcp_timeout_unacknowledged = 300

# 设置 sunrpc 协议的 FIN 超时时间为 15 秒

sunrpc.tcp_fin_timeout = 15

# 设置 sunrpc 协议的最大槽位表项数为 65536

sunrpc.tcp_max_slot_table_entries = 65536

# 设置 sunrpc 协议的槽位表项数为 2

sunrpc.tcp_slot_table_entries = 2

# 设置 sunrpc 协议的传输层为 TCP,缓存区大小为 1048576 字节

sunrpc.transports = tcp 1048576

# 设置系统最大连接数为 65535

net.core.somaxconn = 65535

# 设置网络设备缓存队列最大值为 65535

net.core.netdev_max_backlog = 65535

# 设置系统的最大文件句柄数为 65535

fs.file-max = 65535

# 增加文件描述符限制

fs.nr_open = 1000000

# 设置同时为当前用户打开的 inotify 实例的最大数目为 1024

fs.inotify.max_user_instances = 1024

# 设置当前用户为每个 inotify 实例可同时监视的文件和目录数目上限为 65536

fs.inotify.max_user_watches = 65536

# 设置 inotify 实例中等待处理的事件队列的最大(未处理)长度为 16384

fs.inotify.max_queued_events = 16384

# 调整文件系统缓存参数

vfs_cache_pressure = 50

# 设置进程ID的最大值为131072

kernel.pid_max = 131072

# 设置系统支持的最大进程ID值为131072

kernel.max_pid = 131072

# 设置系统的信号量的参数,分别是512个信号量集、每个信号量集的最大值为65535、每个进程最多可以持有的信号量数量为1024、最大的信号量值为2048

kernel.sem = 512 65535 1024 2048

```


3. 保存文件并退出。


4. 使用以下命令使新配置生效:


```java

sudo sysctl -p

```

5. 使用命令行工具查看TCP网络参数的值,例如使用命令:


```java

sysctl -a | grep tcp

```


可以查看到当前TCP网络参数的值,确认修改是否生效。

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/c0c3d64efdcf498487f6337273a375bf.png)

## 修改文件描述符大小


1. 打开命令行终端,输入以下命令:


```java

sudo vi /etc/security/limits.conf

```


2. 在打开的配置文件中添加以下代码:


```java

*    soft    nofile    65535

*    hard    nofile    65535

```


3. 保存文件并关闭。


4. 输入以下命令,使配置修改生效:


```java

ulimit -n 65535

```


注意:此配置只对当前用户有效,如果要对所有用户生效,需要重启系统。


以上配置仅供参考,具体的参数设置应根据实际情况进行调整。在更改任何系统参数之前,请确保了解所需的配置和可能的影响。


# SpringBoot的配置


Tomcat是Spring Boot默认的Web容器,它的配置也需要进行优化,以提高性能。


## 1. 配置HTTP协议下的数据压缩



Spring Boot使用内嵌的Tomcat作为其默认的Web容器,支持HTTP协议下的数据压缩。


要启用数据压缩,需要在application.properties文件中添加以下配置:


```java

server.compression.enabled=true

server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain

server.compression.min-response-size=2048

```


解释一下每一个配置项的含义:


- server.compression.enabled:启用数据压缩,默认为false。

- server.compression.mime-types:需要压缩的数据类型列表,支持多个值,用逗号分隔。

- server.compression.min-response-size:响应数据的最小大小(字节),只有响应数据大小超过该值才会进行压缩。


配置完成后,当客户端请求的Accept-Encoding头中包含“gzip”或“deflate”时,Tomcat会自动压缩响应数据并返回。


需要注意的是,如果使用了反向代理服务器(如Nginx),则需要确保代理服务器不会重复压缩响应数据,否则可能会导致网页无法正确加载。可以通过设置代理服务器的“proxy_set_header Accept-Encoding ”和“proxy_set_header TE ”选项来解决该问题。


## 2. 配置静态资源缓存



Spring Boot的默认配置会自动将静态资源缓存一段时间,并指定缓存路径。默认情况下,静态资源的缓存时间是1小时(3600秒),缓存路径为“/static/**”和“/public/**” 。


如果需要自定义静态资源的缓存配置,可以在application.properties中添加如下配置:


```java

# 配置静态资源缓存时间为10分钟

spring.resources.cache.cachecontrol.max-age=600


# 配置缓存路径

spring.resources.static-locations=classpath:/static/,classpath:/public/

```


在上述配置中,通过`spring.resources.cache.cachecontrol.max-age`可以配置缓存时间,单位为秒;通过`spring.resources.static-locations`可以配置缓存路径。在配置路径时,需要指定静态资源存放的位置,多个位置可以使用逗号分隔。


需要注意的是,如果静态资源名带有版本号或者时间戳等动态变化的参数,那么缓存路径需要指定到该参数前面的部分,否则可能会导致缓存无效。


## 3.Tomcat线程池配置


在高并发场景下,线程池的配置也是非常重要的,可以大大提高系统的并发处理能力。


Spring Boot默认使用Tomcat线程池,它提供了以下参数可以进行调整:


(1)最大工作线程数(maxThreads):表示Tomcat可以处理的并发请求数量,超过最大线程数的请求将会被拒绝。可以根据预期并发请求量进行调整,建议设置为CPU核心数量的2~4倍。


(2)最小工作线程数(minSpareThreads):表示Tomcat最少保持的空闲工作线程。可以根据预期并发请求量进行调整,建议设置为CPU核心数量。


(3)最大连接数(maxConnections):表示Tomcat可以处理的最大请求数量,超过最大连接数的请求将会被放入等待队列。可以根据预期并发请求量进行调整,建议设置为maxThreads的2~4倍。


(4)等待队列长度(acceptCount):表示Tomcat等待队列的长度,超过等待队列长度的请求将会被拒绝。可以根据预期并发请求量进行调整,建议设置为maxConnections的2~4倍。


假设某场景2万用户30秒内处理每个用户5次请求,那么QPS为20000*5/30=3333.3,那么对应的配置如下:

```xml

# server配置

server:

 compression:

   # 启用数据压缩,默认为false。配置完成后,当客户端请求的Accept-Encoding头中包含“gzip”或“deflate”时,Tomcat会自动压缩响应数据并返回。

   enabled: true

   # 需要压缩的数据类型列表,支持多个值,用逗号分隔。

   mime-types: application/json,application/xml,text/html,text/xml,text/plain

   # 响应数据的最小大小(字节),只有响应数据大小超过该值才会进行压缩。

   min-response-size: 2048

 # 服务端口

 port: 8097

 tomcat:

   # 1核2g内存为200,线程数经验值200左右;4核8g内存,线程数经验值800左右,16核32G内存,线程数经验值3200左右,以此类推,对应的服务器配置也需要跟着调整。这里有二种方式:第一种直接上对应的高配服务器,第二种搭建集群,通过弹性伸缩(需要配置报警任务触发扩容)+负载均衡(需要监听服务端口)+云服务器。

   threads:

     # 最多的工作线程数,默认大小是200。该参数相当于临时工,如果并发请求的数量在10到200之间,就会使用这些临时工线程进行处理。建议设置为 2 倍到 4 倍的 QPS

     max: 6667

     # 最少的工作线程数,默认大小是10。该参数相当于长期工,如果并发请求的数量达不到10,就会依次使用这几个线程去处理请求。如果min-spare设置得太低,那么当应用程序接收到高并发请求时,线程池将无法满足服务要求而导致请求失败。较高的min-spare值可能会导致系统响应时间变慢,因为它会创建大量线程来处理请求,这可能会占用过多的CPU和内存资源。如果将min-spare值设置得太低,则线程池可能无法及时响应请求。当系统负载较高时,有些请求可能会被暂时挂起,等待线程变得可用。如果没有足够的空闲线程,则请求将会等待更长时间。设置较高的min-spare值会占用更多的内存资源。如果线程池中的线程数超出了系统的实际需求,则会浪费内存资源。因此,将min-spare值设置为10至20是一种平衡内存和线程利用率的方式。

     min-spare: 20

     # 最大连接数,默认大小是8192。表示Tomcat可以处理的最大请求数量,超过8192的请求就会被放入到等待队列。如果设置为-1,则禁用maxconnections功能,表示不限制tomcat容器的连接数。建议设置为 2 倍到 4 倍的 QPS。如果设置的值太低,将会限制服务器处理客户端请求的能力,从而可能导致应用程序出现性能问题。如果设置的值太高,则会浪费服务器资源,因为服务器的处理能力可能不足以处理所有的连接。通过经验和测试,2到4倍的QPS值通常会在服务器处理客户端请求时提供最佳性能和稳定性。这个范围也会提供一定的缓冲以应对突发流量,从而在服务器资源短缺时避免过载。

   max-connections: 6667

   # 等待队列的长度,默认大小是100。建议设置为 2 至 5 倍的 max-connections。将accept-count设置为2至5倍的max-connections可以确保Tomcat能够处理足够的连接请求,同时避免因过多排队连接导致的性能问题。但是,设置过高的accept-count会增加系统负担和内存压力,同时也可能会引起其他问题,如拒绝服务攻击等。至于为什么建议不超过5倍,是因为实际上超过这个范围的设置已经很少能带来明显的性能提升,反而会增加系统负担。同时,设置过高的accept-count还可能会导致频繁的连接请求失败和性能下降,甚至可能会导致Tomcat崩溃。

   accept-count: 13334

```

# JVM调优

## 目的和原则

JVM调优的主要目的是减少GC的频率和Full GC的次数,并降低STW的停顿时间和次数。


首先,尽可能让对象都在新生代里分配和回收。由于新生代的垃圾回收速度比老年代要快得多,因此将对象尽量分配到新生代中可以减少老年代的负担,降低GC的频率和Full GC的次数。为避免大量对象进入老年代,可以设置适当的新生代大小和比例,以确保不会频繁地进行老年代的垃圾回收。


其次,给系统充足的内存大小。为避免频繁的垃圾回收和Full GC,可以适当增加系统的内存大小。此外,还可以设置合理的堆空间大小,使得堆空间不会快速被占满。这样可以减少GC的频率和Full GC的次数,降低STW的停顿时间和次数,提高系统的稳定性和性能。


最后,避免频繁进行老年代的垃圾回收。老年代的垃圾回收通常是比较耗时的,因此应该尽量避免频繁进行老年代的垃圾回收。可以通过合理设置新生代大小、年龄等参数以及使用CMS等垃圾回收器来减少对老年代的垃圾回收,从而降低GC的频率和Full GC的次数,提高系统的性能和稳定性。


## 可能导致Full GC

以下是可能引起内存泄漏并导致Full GC的一些情况:

(1)对象的长期存活:如果某些对象在JVM中存活了很长时间,可能会导致内存泄漏,并在堆积积累的过程中触发Full GC。

(2)大对象:如果程序中创建了大的对象,但这些对象无法被回收,可能会导致内存泄漏,并触发Full GC。

(3)永久代内存溢出:当应用程序使用大量字符串或其他可序列化的类时,可能会导致永久代内存耗尽,并触发Full GC。

(4)字符串:如果在程序中频繁创建字符串,并且它们不被清除,可能会导致内存泄漏,并触发Full GC。

(5)无用的类和对象:如果程序中存在许多无用的类和对象,可能会导致内存泄漏,并触发Full GC。

(6)ThreadLocal:如果程序中使用了ThreadLocal,但没有正确地清除线程本地存储,可能会导致内存泄漏,并触发Full GC。

(7)频繁创建对象:如果程序中频繁地创建对象,但没有正确地清除这些对象,可能会导致内存泄漏,并触发Full GC。


首先,在使用System.gc()方法时,尽量避免使用该方法,因为调用该方法会建议JVM进行Full GC,而可能会增加Full GC的频率,从而增加间歇性停顿的次数。为了减少该方法的使用,可以禁止RMI调用System.gc(),通过-XX:+DisableExplicitGC参数来实现。


其次,如果Survivor区域的对象满足晋升到老年代的条件,但是晋升进入老年代的对象大小大于老年代的可用内存时,就会触发Full GC。为了避免这种情况,可以通过调整JVM参数或者设计应用程序的算法,来减少对象在老年代的数量。


JDK8开始,Metaspace区取代了永久代(PermGen),Metaspace使用的是本地内存。通过调整JVM参数限制Metaspace的大小,当Metaspace区内存达到阈值时,也会引发Full GC。可以通过调整JVM参数或者设计应用程序的算法,来减少Metaspace区内存的使用。

在Survivor区域的对象满足晋升到老年代的条件时,也可能会引起Full GC。可以通过调整JVM参数来控制对象的晋升行为,从而减少Full GC的发生。


此外,如果堆中产生的大对象超过阈值,也会引发Full GC。可以通过调整JVM参数或者优化应用程序算法,来减少大对象的产生。


最后,老年代连续空间不足或者CMS GC时出现promotion failed和concurrent mode failure所致的Full GC,也可以通过调整JVM参数或者设计应用程序算法来减少其发生。


## 场景调优

对于场景问题,可以从如下几个大方向进行设计:


1. 大访问压力下,MGC 频繁一些是正常的,只要MGC 延迟不导致停顿时间过长或者引发FGC ,那可以适当的增大Eden 空间大小,降低频繁程度,同时要保证,空间增大对垃圾回收产生的停顿时间增长是可以接受的。


2. 如果MinorGC 频繁,且容易引发 Full GC。需要从如下几个角度进行分析。

- 每次MGC存活的对象的大小,是否能够全部移动到 S1区,如果S1 区大小 < MGC 存活的对象大小,这批对象会直接进入老年代。注意 了,这批对象的年龄才1岁,很有可能再多等1次MGC 就能被回收了,可是却进入了老年代,只能等到Full GC 进行回收,很可怕。这种情况下,应该在系统压测的情况下,实时监控MGC存活的对象大小,并合理调整eden和s 区的大小以及比例。

- 还有一种情况会导致对象在未达到15岁之前,直接进入老年代,就是S1区的对象,相同年龄的对象所占总空间大小>s1区空间大小的一半,所以为了应对这种情况,对于S区的大小的调整就要考虑:尽量保证峰值状态下,S1区的对象所占空间能够在MGC的过程中,相同对象年龄所占空间不大于S1区空间的一半, 因此对于S1空间大小的调整,也是十分重要的。


3. 由于大对象创建频繁,导致Full GC 频繁。对于大对象,JVM专门有参数进行控制,**-XX: PretenureSizeThreshold**。超过这个参数值的对象,会直接进入老年代,只能等到full GC 进行回收,所以在系统压测过程中,要重点监测大对象的产生。如果能够优化对象大小,则进行代码层面的优化,优化如:根据业务需求看是否可以将该大对象设置为单例模式下的对象,或者该大对象是否可以进行拆分使用,或者如果大对象确定使用完成后,将该对象赋值为null,方便垃圾回收。

如果代码层面无法优化,则需要考虑:

- 调高-XX: PretenureSizeThreshold参数的大小,使对象有机会在eden区创建,有机会经历MGC以被回收。但是这个参数的调整要结合MGC过程中Eden区的大小是否能够承载,包括S1区的大小承载问题。

- 这是最不希望发生的情况, 如果必须要进入老年代,也要尽量保证,该对象确实是长时间使用的对象,放入老年代的总对象创建量不会造成老年代的内存空间迅速长满发生Full GC,在这种情况下,可以通过定时脚本,在业务系统不繁忙情况下,主动触发full gc。


4. MGC 与 FGC 停顿时间长导致影响用户体验。其实对于停顿时间长的问题无非就两种情况:

- gc 真实回收过程时间长,即real time时间长。这种时间长大部分是因为内存过大导致,从标记到清理的过程中需要对很大的空间进行操作,导致停顿时间长。这种情况,要考虑减少堆内存大 小,包括新生代和老年代,比如之前使用16G的堆内存,可以考虑将16G 内存拆分为4个4G的内存区域,可以单台机器部署JVM逻辑集群,也可以为了降低GC回收时间,进行4节点的分布式部署,这里的分布式部署是为了降低 GC垃圾回收时间。

- gc真实回收时间 real time 并不长,但是user time(用户态执行时间) 和 sys time(核心态执行时间)时间长,导致从客户角度来看,停顿时间过长。这种情况,要考虑线程是否及时达到了安全点,通过`-XX:+PrintSafepointStatistics`和`-XX: PrintSafepointStatisticsCount=1`去查看安全点日志,如果有长时间未达到安全点的线程,再通过参数`-XX: +SafepointTimeout`和`-XX:SafepointTimeoutDelay=2000`两个参数来找到大于2000ms到达安全点的线程,这里 的2000ms可以根据情况自己设置,然后对代码进行针对的调整。除了安全点问题,也有可能是操作系统本身负载比较高,导致处理速度过慢,线程达到安全点时间长,因此需要同时检测操作系统自身的运行情况。


5. 内存泄漏导致的MGC和FGC频繁,最终引发oom。纯代码级别导致的MGC和FGC频繁。如果是这种情况,那就只能对代码进行大范围的调整,这种情况就非常多了,而且会很糟糕。如大循环体中的new 对象,未使用合理容器进行对象托管导致对象创建频繁,不合理的数据结构使用等等。 总之,JVM的调优无非就一个目的,在系统可接受的情况下达到一个合理的MGC和FGC的频率以及可接受的回收时间。


## JVM调优工具

Jstack是用于获取Java线程转储的工具,适用于在程序出现死锁、线程挂起等问题时,通过获取线程转储,更好地诊断和解决问题。在找出占用CPU最高的线程堆栈信息时,可以按以下步骤操作:


(1)打开命令行窗口,并进入Java应用程序所在的目录。


(2)在命令行中输入以下命令,查询Java应用程序的进程id(PID),代码如下:


```java

ps -ef | grep java

```


该命令将返回所有正在运行的Java应用程序进程的详细信息,需要查找要分析的Java应用程序进程的PID。


(3)输入以下命令,使用Jstack导出Java应用程序的线程堆栈信息(将PID替换为前面查询到的Java应用程序进程的PID),代码如下:


```java

jstack PID > thread_dump.txt

```


该命令将会导出当前时间点Java应用程序的线程堆栈信息到thread_dump.txt文件。


(4)打开thread_dump.txt文件,查找占用CPU最高的线程。在文件中,可以查找到每个线程的ID,状态和堆栈信息。找到CPU使用率最高的线程,查看其堆栈信息,尝试从中找到问题所在。可以使用线程ID在文件中搜索线程的堆栈信息,代码如下:


```java

grep "nid=0x1234" thread_dump.txt

```


(5)根据线程堆栈信息,定位并解决问题。根据线程堆栈信息,可以判断线程是否处于阻塞状态,是否存在死锁等问题,从而定位并解决问题。


# MySQL调优

## 表结构设计

在进行数据库设计时,开发者需要关注表的规划。


首先,开发者要了解MySQL数据库的页大小。当表中的单行数据达到16KB时,这意味着表中只能存储一条数据,这对于数据库来说是不合理的。MySQL数据库将数据从磁盘读取到内存,它使用磁盘块作为基本单位进行读取。如果一个数据块中的数据一次性被读取,那么查询效率将会提高。


以InnoDB存储引擎为例,它使用页作为数据读取单位。页是磁盘管理的最小单位,默认大小为16KB。由于系统的磁盘块存储空间通常没有这么大,InnoDB在申请磁盘空间时会使用多个地址连续的磁盘块来达到页的大小16KB。


查询数据时,一个页中的每条数据都能帮助定位到数据记录的位置,从而减少磁盘I/O操作,提高查询效率。InnoDB存储引擎在设计时会将根节点常驻内存,尽力使树的深度不超过3。这意味着在查询过程中,I/O操作不超过3次。树形结构的数据可以让系统高效地找到数据所在的磁盘块。


在这里讨论一下B树和B+树的区别。B树的结构是每个节点既包含key值也包含value值,而每个页的存储空间是16KB。如果数据较大,将会导致一个页能存储数据量的数量很小。相比之下,B+树的结构是将所有数据记录节点按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息。这样可以大大加大每个节点存储的key值数量,降低B+树的高度。


通过了解MySQL数据库底层存储的原理和数据结构,开发者在设计表时应该尽量减少单行数据的大小,将字段宽度设置得尽可能小。


在设计表时,开发者要注意以下几点以提高查询速度和存储空间利用率:


(1)避免使用text、Blob、Clob等大数据类型,它们占用的存储空间更大,读取速度较慢。


(2)尽量使用数字型字段,如性别字段用0/1的方式表示,而不是男女。这样可以控制数据量,增加同一高度下B+树容纳的数据量,提高检索速度。


(3)使用varchar/nvarchar代替char/nchar。变长字段存储空间较小,可以节省存储空间。


(4)不在数据库中存储图片、文件等大数据,可以通过第三方云存储服务存储,并提供图片或文件地址。


(5)金额字段使用decimal类型,注意长度和精度。如果存储的数据范围超过decimal的范围,建议将数据拆成整数和小数分开存储。


(6)避免给数据库留null值。尤其是时间、整数等类型,可以在建表时就设置非空约束。NULL列会使用更多的存储空间,在MySQL中处理NULL值也更复杂。为NULL的列可能导致固定大小的索引变成可变大小的索引,例如只有整数列的索引。

## 建索引

在建立索引时,需要权衡数据的维护速度和查询性能。以下是一些关于如何确定是否为表中字段建立索引的示例:


(1)对于经常修改的数据,建立索引会降低数据维护速度,因此不适合对这些字段建立索引,例如状态字段。


(2)对于性别字段,通常用0和1表示,但由于其区分度不高(100万用户中90万为男性,10万为女性),因此一般不需要建立索引。然而,如果性别字段的区分度非常高(例如90万男性和10万女性),而且该字段不经常更改,则可以考虑为该字段建立索引。


(3)可以在where及order by涉及的列上建立索引。


(4)对于需要查询排序、分组和联合操作的字段,适合建立索引,以提高查询性能。


(5)索引并非越多越好,一个表的索引数最好不要超过6个。当为多个字段创建索引时,表的更新速度会减慢,因此应选择具有较高区分度且不经常更改的字段创建索引。


(6)尽量让字段顺序与索引顺序一致,复合索引中的第一个字段作为条件时才会使用该索引。


(7)遵循最左前缀原则:尽量确保查询中的索引列按照最左侧的列进行匹配。


## SQL优化

为了优化SQL语句,需要了解数据库的架构、索引、查询优化器以及各种SQL执行引擎的机制等技术知识。

### SQL编写

在编写SQL语句时,开发者需要注意一些关键点以提高查询性能。以下是一些建议:


(1)避免在WHERE子句中对查询的列执行范围查询(如NULL值判断、!=、<>、or作为连接条件、IN、NOT IN、LIKE模糊查询、BETWEEN)和使用“=”操作符左侧进行函数操作、算术运算或表达式运算,因为这可能导致索引失效,从而导致全表扫描。


(2)对于JOIN操作,如果数据量较大,先分页再JOIN可以避免大量逻辑读,从而提高性能。


(3)使用COUNT(*)可能导致全表扫描,如有WHERE条件的SQL,WHERE条件字段未创建索引会进行全表扫描。COUNT(*)只统计总行数,聚簇索引的叶子节点存储整行记录,非聚簇索引的叶子节点存储行记录主键值。非聚簇索引比聚簇索引小,选择最小的非聚簇索引扫表更高效。


(4)当数据量较大时,查询只返回必要的列和行,LIMIT 分页限制返回的数据,减少请求的数据量,插入建议分批次批量插入,以提高性能。


(5)对于大连接的查询SQL,由于数据量较多、又是多表,容易出现整个事务日志较大,消耗大量资源,从而导致一些小查询阻塞,所以优化方向是将它拆分成单表查询,在应用程序中关联结果,这样更利于高性能可伸缩,同时由于是单表减少了锁竞争效率上也有一定提升。


(6)尽量明确只查询所需列,避免使用SELECT *。SELECT *会导致全表扫描,降低性能。若必须使用SELECT *,可以考虑使用MySQL 5.6及以上版本,因为这些版本提供了离散读优化(Discretized Read Optimization),将离散度高的列放在联合索引的前面,以提高性能。


索引下推(ICP,Index Condition Pushdown)优化:ICP优化将部分WHERE条件的过滤操作下推到存储引擎层,减少上层SQL层对记录的索取,从而提高性能。在某些查询场景下,ICP优化可以大大减少上层SQL层与存储引擎的交互,提高查询速度。


多范围读取(MRR,Multi-Range Read)优化:MRR优化将磁盘随机访问转化为顺序访问,提高查询性能。当查询辅助索引时,首先根据结果将查询得到的索引键值存放于缓存中。然后,根据主键对缓存中的数据进行排序,并按照排序顺序进行书签查找。

这种顺序查找减少了对缓冲池中页的离散加载次数,可以提高批量处理对键值查询操作的性能。


在编写SQL时,使用EXPLAIN语句观察索引是否失效是个好习惯。索引失效的原因有以下几点:


(1)如果查询条件中包含OR,即使其中部分条件带有索引,也无法使用。


(2)对于复合索引,如果不使用前列,后续列也无法使用。


(3)如果查询条件中的列类型是字符串,则在条件中将数据使用引号引用起来非常重要,否则索引可能失效。


(4)如果在查询条件中使用运算符(如+、-、*、/等)或函数(如substring、concat等),索引将无法使用。


(5)如果MySQL认为全表扫描比使用索引更快,则可能不使用索引。在数据较少的情况下尤其如此。

### SQL优化工具

常用的SQL优化方法包括:业务层逻辑优化、SQL性能优化、索引优化。


业务层逻辑优化:开发者需要重新梳理业务逻辑,将大的业务逻辑拆分成小的逻辑块,并行处理。这样可以提高处理效率,降低数据库的访问压力。


SQL性能优化:除了编写优化的SQL语句、创建合适的索引之外,还可以使用缓存、批量操作减少数据库的访问次数,以提高查询效率。


索引优化:对于复杂的SQL语句,人工直接介入调节可能会增加工作量,且效果不一定好。开发者的索引优化经验参差不齐,因此需要使用索引优化工具,将优化过程工具化、标准化。最好是在提供SQL语句的同时,给出索引优化建议。

### 慢SQL优化

影响程度一般的慢查询通常在中小型企业因为项目赶进度等问题常被忽略,对于大厂基本由数据库管理员通过实时分析慢查询日志,对比历史慢查询,给出优化建议。


影响程度较大的慢查询通常会导致数据库负载过高,人工故障诊断,识别具体的慢查询SQL,及时调整,降低故障处理时长。

当前未被定义为慢查询的SQL可能随时间演化为慢查询,对于核心业务,可能引发故障,需分类接入:


(1)未上线准慢查询:需要通过发布前集成测试流水线,通常都是经验加上explain关键字识别慢查询,待解决缺陷后才能发布上线。


(2)已上线准慢查询:表数据量增加演变为慢查询,比较常见,通常会变成全表扫描,开发者可以增加慢查询配置参数log_queries_not_using_indexes记录至慢日志,实时跟进治理。

## 数据分区

在面对大量数据时,分区可以帮助提高查询性能。分区主要分为两类:表分区和分区表。

### 表分区

表分区是在创建表时定义的,需要在表建立的时候创建规则。如果要修改已有的有规则的表分区,只能新增,不能随意删除。表分区的局限性在于单个MySQL服务器支持1024个分区。

### 分区表

当表分区达到上限时,可以考虑垂直拆分和水平拆分。垂直拆分将单表变为多表,以增加每个分区承载的数据量。水平拆分则是将数据按照某种策略拆分为多个表。


垂直分区的优点是可以减少单个分区的数据量,从而提高查询性能。但缺点是需要考虑数据的关联性,并在SQL查询时进行反复测试以确保性能。


对于包含大文本和BLOB列的表,如果这些列不经常被访问,可以将它们划分到另一个分区,以保证数据相关性的同时提高查询速度。

### 水平分区

随着数据量的持续增长,需要考虑水平分区。水平分区有多种模式,例如:

(1)范围(Range)模式:允许DBA将数据划分为不同的范围。例如DBA可以将一个表按年份划分为三个分区,80年代的数据、90年代的数据以及2000年以后的数据。


(2)哈希(Hash)模式:允许DBA通过对表的一个或多个列的Hash Key进行计算,最后通过这个Hash码不同数值对应的数据区域进行分区。例如DBA可以建立一个根据主键进行分区的表。


(3)列表(List)模式:允许系统通过DBA定义列表的值所对应行数据进行分割。例如DBA建立了一个横跨三个分区的表,分别根据2021年、2022年和2023年的值对应数据。


(4)复合模式(Composite):允许将多个模式组合使用,如在初始化已经进行了Range范围分区的表上,可以对其中一个分区再进行Hash哈希分区。

## 灾备处理

在MySQL中,冷热备份可以帮助 开发者在不影响性能的情况下确保数据的安全性。

### 冷备份

当某些数据不再需要或不常访问时,可以考虑进行冷备份。冷备份是在数据库关闭时进行的数据备份,速度更快,安全性也相对更高。例如您可以将一个不再需要的月度报告数据备份到外部存储设备,以确保在需要时可以轻松访问这些数据。

### 热备份

对于需要实时更新的数据,可以考虑热备份。热备份是在应用程序运行时进行的数据备份,备份的是数据库中的SQL操作语句。例如您可以将用户的购物记录备份到一个在线存储服务中,以便在需要时可以查看这些数据。

### 冷备份与热备份的权衡

(1)冷备份速度更快,因为它不涉及应用程序的运行,但可能需要外部存储设备。


(2)热备份速度较慢,因为它涉及应用程序的运行和数据库操作的记录。


(3)冷备份更安全,因为它在数据库关闭时进行,不受应用程序影响。


(4)热备份安全性稍低,因为它在应用程序运行时进行,需要保持设备和网络环境的稳定性。

### 备份注意事项

(1)备份过程中要保持设备和网络环境稳定,避免因中断导致数据丢失。


(2)备份时需要仔细小心,确保备份数据的正确性,以防止恢复过程中出现问题。


(3)热备份操作要特别仔细,备份SQL操作语句时不能出错。


总之,通过对冷热数据进行备份,可以在不影响应用程序性能的情况下确保数据的安全性。在实际应用中,应根据数据的需求和业务场景选择合适的备份策略。

## 高可用

在生产环境中,MySQL的高可用性变得越来越重要,因为它是一个核心的数据存储和管理系统,任何错误或中断都可能导致严重的数据丢失和系统瘫痪。因此,建立高可用的MySQL环境是至关重要的。

### MMM

用于监控和故障转移MySQL集群。它使用虚拟IP(VIP)机制实现集群的高可用。集群中,主节点通过一个虚拟IP地址提供数据读写服务,当出现故障时,VIP会从原主节点漂移到其他节点,由这些节点继续提供服务。双主故障切换(MMM)的主要缺点是故障转移过程过于简单粗暴,容易丢失事务,因此建议采用半同步复制以降低失败概率。

### MHA

它是一种用于故障切换的工具,能在30秒内完成故障切换,并在切换过程中最大程度地保证数据一致性。高可用性与可伸缩性(MHA)主要监控主节点的状态,当检测到主节点故障时,它会提升具有最新数据的从节点成为新的主节点,并通过其他从节点获取额外信息来避免数据一致性方面的问题。MHA可以单独部署,分为Manager节点和Node节点,分别部署在单独的机器上和每台MySQL机器上。Node节点负责解析MySQL日志,而Manager节点负责探测Node节点并判断各节点的运行状况。当检测到主节点故障时,Manager节点会直接提升一个从节点为新主节点,并让其他从节点挂载到新主节点上,实现完全透明。为了降低数据丢失的风险,建议使用MHA架构。

### MGR

它是MySQL官方在5.7.17版本中正式推出的一种组复制机制,主要用于解决异步复制和半同步复制中可能产生的数据不一致问题。组复制(MGR)由若干个节点组成一个复制组,事务提交后,必须经过超过半数节点的决议并通过后才能提交。引入组复制主要是为了解决传统异步复制和半同步复制可能出现的数据不一致问题。


组复制的主要优点是基本无延迟,延迟较异步复制小很多,且具有数据强一致性,可以保证事务不丢失。然而,它也存在一些局限性:


(1)仅支持InnoDB存储引擎。


(2)表必须具有主键。


(3)仅支持GTID模式,日志格式为row格式。

## 异常发现处理

在使用MySQL时,可能会遇到各种异常情况,例如连接错误、查询错误、数据删除错误等等。在处理这些异常情况时,开发人员需要了解异常的原因和处理方法,以便及时排除问题,保障系统的稳定性和可靠性。

### 数据库监控

及时将数据库异常通过短信、邮件、微信等形式通知给管理员,并且可以将数据库运行的实时指标统计分析图表显示出来,便于更好地对数据库进行规划和评估,目前市面上比较主流的数据库监控工具有Prometheus + Grafana + mysqld_exporter(比较受欢迎)、SolarWinds SQL Sentry、Database Performance Analyzer、OpenFalcon。

### 数据库日志

在MySQL中,有一些关键的日志可以用作异常发现并通过这些日志给出解决方案:


(1)重做日志(redo log):记录物理级别的页修改操作,例如页号123、偏移量456写入了“789”数据。可以通过“show global variables like 'innodb_log%';”命令查看。主要用于事务提交时保证事务的持久性和回滚。


(2)回滚日志(undo log):记录逻辑操作日志,例如添加一条记录时会记录一条相反的删除操作。可以通过“show variables like 'innodb_undo%';”命令查看。主要用于保证事务的原子性,在需要时回滚事务。


(3)变更日志/二进制日志(bin log):记录数据库执行的数据定义语句(DDL)和数据操作语句(DML)等操作。例如数据库意外挂机时,可以通过二进制日志文件查看用户执行的命令,并根据这些操作指令恢复数据库或将数据复制到其他数据库中。可以通过“show variables like '%log_bin%';”命令查看。主要用于性能优化和复制数据。


(4)慢查询日志:记录响应时间超过指定阈值的SQL语句。主要用于性能优化。可以通过“show variables like '%slow_query_log%';”命令查看。


(5)错误日志:记录MySQL服务启动、运行、停止时的诊断信息、错误信息和警告提示。主要用于排查MySQL服务出现异常的原因。可以通过“SHOW VARIABLES LIKE 'log_err%';”命令查看。


(6)通用查询日志:记录用户的所有操作,无论是所有的SQL语句还是调整MySQL参数或者启动和关闭MySQL都会记录。可以还原操作的场景。通过SHOW VARIABLES LIKE '%general%';命令查看。


(7)中继日志(relay log):只存在主从数据库的从数据库上,用于主从同步,可以在xx-relaybin.index索引文件和-relaybin.0000x数据文件查看。


(8)数据定义语句日志(ddl.log):记录数据定义的SQL,比如ALTER TABLE。


(9)processlist日志:查看正在执行的sql语句。


(10) innodb status日志:查看事务、锁、缓冲池和日志文件,主要用于诊断数据库性能。

### 数据库巡检

巡检工作保障系统平稳有效运行,比如飞机起飞巡检保证起飞后能够正常工作。巡检工作主要由数据库管理员和后端开发工程师负责。


数据库管理员主要负责处理数据库基础功能/高可用/备份/中间件/报警组件、集群拓扑、核心参数等集群层面的隐患、服务器硬件层面隐患,对于磁盘可用空间预测等范围。


后端开发工程师主要负责库表设计缺陷、数据库使用不规范等引起的业务故障或性能问题的隐患,定期采集整型字段值有没有超过最大值,因为整型类型的字段保存的数值有上限。对于读写情况需要定期观察表大小,找出有问题的大表进行优化调整。

### 资源评估

测试人员进行压测,观察极限环境下数据库各项指标是否正常工作,运维工程师或者数据库管理员对数据容量进行评估,服务器资源需要提前规划,同时设置预警通知,超过阈值安排相关人员进行扩容,从而保证数据库稳定运行。

## 数据服务

数据服务的主要目的是帮助用户规划和迁移数据,备份和恢复数据库以及进行数据校验等功能,以确保用户的数据始终处于安全可靠的状态。

### 子表结构生成

一个表进行拆分,会根据业务实际情况进行拆解,例如用户表可以根据地区拆分tb_user可拆分成上海地区的用户表(tb_user_sh)、广州地区的用户表(tb_user_gz),那么全国有很多个城市,每个地方都需要创建一张子表并且维护它会比较费时费力,通常情况下,会开发3个接口做表结构同步:根据主表创建子表、主表字段同步到子表、主表索引同步子表。


下面对这3个接口提供思路以及关键代码。


主表创建子表,代码如下:


```java

/**

* {

*     "tableName": "tb_user",

*     "labCodes": [

*         "sh",//上海

*         "gz"//广州

*     ]

* }    

*/

public Boolean createTable(ConfigReq reqObject) {

if (CollectionUtils.isEmpty(reqObject.getLabCodes())) {

 return false;

}

List<String> labCodes = reqObject.getLabCodes();

for (String labCode: labCodes){

 //主表表名

 String tableName = reqObject.getTableName();

 //子表后表名

 String newTable = String.format("%s_%s", tableName, labCode);

 //校验子表是否存在

 Integer checkMatrix = configExtMapper.checkTable(newTable);

 if(checkMatrix == null || checkMatrix.intValue() < 0){

 //创建子表结构

 configExtMapper.createConfigTable(tableName, newTable);

 }

 }

return true;

}

```


主表字段同步到子表,代码如下:


```java

/**

* 主表字段同步到子表

* @param masterTable 主表

* @return

*/

private Boolean syncAlterTableColumn(String masterTable) {

String table = masterTable + "%";

//获取子表名

List<String> tables = configExtMapper.getTableInfoList(table);

if(CollectionUtils.isEmpty(tables)){

 return false;

}

//获取主表结构列信息

List<ColumnInfo> masterColumns = configExtMapper.getColumnInfoList(masterTable);

if (masterColumns.isEmpty()){

 return false;

}

String alterName = null;

for (ColumnInfo column: masterColumns) {

 column.setAlterName(alterName);

 alterName = column.getColumnName();

}

for(String tableName : tables){

 if(StringUtils.equalsIgnoreCase(tableName, masterTable)){

  continue;

 }

 //获取子表结构列信息

 List<ColumnInfo> columns = configExtMapper.getColumnInfoList(tableName);

 if(CollectionUtils.isEmpty(columns)){

  continue;

 }

 for (ColumnInfo masterColumn : masterColumns) {

  ColumnInfo column = columns.stream().filter(c -> StringUtils.equalsIgnoreCase(c.getColumnName(),

  masterColumn.getColumnName())).findFirst().orElse(null);

  if (column == null){

   column = new ColumnInfo();

   column.setColumnName(masterColumn.getColumnName());//列名

   column.setAddColumn(true);//是否修改

  }

  if (column.hashCode() == masterColumn.hashCode()){

   continue;

  }

  column.setTableName(tableName);//表名

  column.setColumnDef(masterColumn.getColumnDef());//是否默认值

  column.setIsNull(masterColumn.getIsNull());//是否允许为空(NO:不能为空、YES:允许为空)

  column.setColumnType(masterColumn.getColumnType());//字段类型(如:varchar(512)、text、bigint(20)、datetime)

  column.setComment(masterColumn.getComment());//字段备注(如:备注)

  column.setAlterName(masterColumn.getAlterName());//修改的列名

  //创建子表字段

  configExtMapper.alterTableColumn(column);

 }

}

return true;

}

```


主表索引同步子表,代码如下:


```java

/**

* 主表索引同步子表

* @param masterTableName 主表名

* @return

*/

private Boolean syncAlterConfigIndex(String masterTableName) {

String table = masterTableName + "%";

//获取子表名

List<String> tableInfoList = configExtMapper.getTableInfoList(table);

if (tableInfoList.isEmpty()){

 return false;

}

// 获取所有索引

List<String> allIndexFromTableName = configExtMapper.getAllIndexNameFromTableName(masterTableName);

if (CollectionUtils.isEmpty(allIndexFromTableName)) {

 return false;

}

for (String indexName : allIndexFromTableName) {

 //获取拥有索引的列名

 List<String> indexFromIndexName = configExtMapper.getAllIndexFromTableName(masterTableName, indexName);

 for (String tableName : tableInfoList) {

  if (!tableName.startsWith(masterTableName)) {

   continue;

  }

  //获取索引名称

  List<String> addIndex = configExtMapper.findIndexFromTableName(tableName, indexName);

  if (CollectionUtils.isEmpty(addIndex)) {

   //创建子表索引

   configExtMapper.commonCreatIndex(tableName, indexName, indexFromIndexName);

  }

 }

}

return true;

}

```

子表结构生成的SQL,代码如下:


```java

<!--校验子表是否存在 这里db_user写死了数据库名称,后面可以根据实际情况调整-->

<select id="checkTable" resultType="java.lang.Integer" >

SELECT 1 FROM INFORMATION_SCHEMA.`TABLES` WHERE TABLE_SCHEMA = 'db_user' AND TABLE_NAME = #{tableName};

</select>

<!--创建子表结构-->

<update id="createConfigTable" >

CREATE TABLE `${newTableName}` LIKE `${sourceName}`;

</update>

<!--获取子表名-->

<select id="getTableInfoList" resultType="java.lang.String">

SELECT `TABLE_NAME`

FROM INFORMATION_SCHEMA.`TABLES`

WHERE `TABLE_NAME` LIKE #{tableName};

</select>

<!--获取主/子表结构列信息 这里db_user写死了数据库名称,后面可以根据实际情况调整-->

<select id="getColumnInfoList" resultType="com.yunxi.datascript.config.ColumnInfo">

SELECT `COLUMN_NAME` AS columnName

,COLUMN_DEFAULT AS columnDef   -- 是否默认值

,IS_NULLABLE AS isNull   -- 是否允许为空

,COLUMN_TYPE AS columnType  -- 字段类型

,COLUMN_COMMENT AS comment     -- 字段备注

FROM INFORMATION_SCHEMA.`COLUMNS`

WHERE TABLE_SCHEMA = 'db_user'

AND `TABLE_NAME` = #{tableName}

ORDER BY ORDINAL_POSITION ASC;

</select>

<!--创建子表字段-->

<update id="alterTableColumn" parameterType="com.yunxi.datascript.config.ColumnInfo">

ALTER TABLE `${tableName}`

<choose>

 <when test="addColumn">

  ADD COLUMN

 </when >

 <otherwise>

  MODIFY COLUMN

 </otherwise>

</choose>

${columnName}

${columnType}

<choose>

 <when test="isNull != null and isNull == 'NO'">

  NOT NULL

 </when >

 <otherwise>

  NULL

 </otherwise>

</choose>

<if test="columnDef != null and columnDef != ''">

 DEFAULT #{columnDef}

</if>

<if test="comment != null and comment != ''">

 COMMENT #{comment}

</if>

<if test="alterName != null and alterName != ''">

 AFTER ${alterName}

</if>

</update>

<!--获取所有索引-->

<select id="getAllIndexNameFromTableName" resultType="java.lang.String">

SELECT DISTINCT index_name FROM information_schema.statistics WHERE table_name = #{tableName} AND index_name != 'PRIMARY'

</select>

<!--获取拥有索引的列名-->

<select id="getAllIndexFromTableName" resultType="java.lang.String">

SELECT COLUMN_NAME FROM information_schema.statistics WHERE table_name = #{tableName} AND index_name = #{idxName} AND index_name != 'PRIMARY'

</select>

<!--获取索引名称-->

<select id="findIndexFromTableName" resultType="java.lang.String">

SELECT index_name FROM information_schema.statistics WHERE table_name = #{tableName} AND index_name = #{idxName}

</select>

<!--创建子表索引-->

<update id="commonCreatIndex">

CREATE INDEX ${idxName} ON `${tableName}`

<foreach collection="list" item="item" open="(" close=")" separator=",">

 `${item}`

</foreach>;

</update>

```


根据以上关键代码以及实现思路结合实际情况开发出3个接口足以满足日常分表需求了。

### 数据迁移

数据迁移通常有两种情况:


第一种是开发人员编码,将数据从一个数据库读取出来,再将数据异步的分批次批量插入另一个库中。


第二种是通过数据库迁移工具,通常使用Navicat for MySQL就可以实现数据迁移。


数据迁移需要注意的是不同数据库语法和实现不同,数据库版本不同,分库分表时数据库的自增主键ID容易出现重复键的问题,通常情况下会在最初需要自增时考虑分布式主键生成策略。

### 数据校验

数据校验有对前端传入的参数进行数据校验、有程序插入数据库中的数据进行校验,比如非空校验、长度校验、类型校验、值的范围校验等、有对数据迁移的源数据库和目标数据库的表数据进行对比、这些都是保证数据的完整性。

## 读写分离

MySQL读写分离是数据库优化的一种手段,通过将读和写操作分离到不同的数据库服务器上,可以提高数据库的读写性能和负载能力。

### 主从数据同步

业务应用发起写请求,将数据写到主库,主库将数据进行同步,同步地复制数据到从库,当主从同步完成后才返回,这个过程需要等待,所以写请求会导致延迟,降低吞吐量,业务应用的数据读从库,这样主从同步完成就能读到最新数据。

### 中间件路由

业务应用发起写请求,中间件将数据发往主库,同时记录写请求的key(例如操作表加主键)。当业务应用有读请求过来时,如果key存在,暂时路由到主库,从主库读取数据,在一定时间过后,中间件认为主从同步完成,就会删除这个key,后续读将会读从库。  

### 缓存路由

缓存路由和中间件路由类似,业务应用发起写请求,数据发往主库,同时缓存记录操作的key,设置缓存的失效时间为主从复制完成的延时时间。如果key存在,暂时路由到主库。如果key不存在,近期没发生写操作,暂时路由到从库。


# Redis调优

## 绑定CPU内核

现代计算机的CPU都是多核心多线程,例如i9-12900k有16个内核、24个逻辑处理器、L1缓存1.4MB、L2缓存14MB、L3缓存30MB,一个内核下的逻辑处理器共用L1和L2缓存。


Redis的主线程处理客户端请求、子进程进行数据持久化、子线程处理RDB/AOF rewrite、后台线程处理异步lazy-free和异步释放fd等。这些线程在多个逻辑处理器之间切换,所以为了降低Redis服务端在多个CPU内核上下文切换带来的性能损耗,Redis6.0版本提供了进程绑定CPU 的方式提高性能。


在Redis6.0版本的redis.conf文件配置即可:

- server_cpulist:RedisServer和IO线程绑定到CPU内核

- bio_cpulist:后台子线程绑定到CPU内核

- aof_rewrite_cpulist:后台AOF rewrite进程绑定到CPU内核

- bgsave_cpulist:后台RDB进程绑定到CPU内核

## 使用复杂度过高的命令

Redis有些命令复制度很高,复杂度过高的命令如下:


```java

MSET、MSETNX、MGET、LPUSH、RPUSH、LRANGE、LINDEX、LSET、LINSERT、HDEL、HGETALL、HKEYS/HVALS、SMEMBERS、

SUNION/SUNIONSTORE、SINTER/SINTERSTORE、SDIFF/SDIFFSTORE、ZRANGE/ZREVRANGE、ZRANGEBYSCORE/ZREVRANGEBYSCORE、

ZREMRANGEBYRANK/ZREMRANGEBYSCORE、DEL、KEYS

```


具体原因有以下:

- 在内存操作数据的时间复杂度太高,消耗的CPU资源较多。

- 一些范围命令一次返回给客户端的数据太多,在数据协议的组装和网络传输的过程就要变长,容易延时。

- Redis虽然使用了多路复用技术,但是复用的还是同一个线程,这一个线程同一时间只能处理一个IO事件,像一个开关一样,当开关拨到哪个IO事件这个电路上,就处理哪个IO事件,所以它单线程处理客户端请求的,如果前面某个命令耗时比较长,后面的请求就会排队,对于客户端来说,响应延迟也会变长。


解决方案:

分批次,每次获取尽量少的数据,数据的聚合在客户端做,减少服务端的压力。

## 大key的存储和删除

当存储一个很大的键值对的时候,由于值非常大,所以Redis分配内存的时候就会很耗时,此外删除这个key也是一样耗时,这种key就是大key。开发者可以通过设置慢日志记录有哪些命令比较耗时,命令如下:


命令执行耗时超过10毫秒,记录慢日志


```java

CONFIG SET slowlog-log-slower-than 10000

```


只保留最近1000条慢日志


```java

CONFIG SET slowlog-max-len 1000

```


后面再通过`SLOWLOG get [n]`查看。


对于大key可以通过以下命令直接以类型展示出来,它只显示元素最多的key,但不代表占用内存最多,命令如下:


```java

#-h:redis主机ip

#-p: redis端口号

#-i:隔几秒扫描

redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01

```


对于这种大key的优化,开发者事先在业务实现层就需要避免存储大key,可以在存储的时候将key简化,变成二进制位进行存储,节约redis空间,例如存储上海市静安区,可以对城市和区域进行编码,上海市标记为0,静安区标记为1,组合起来就是01,将01最为key存储起来比上海市静安区作为key存储起来内存占比更小。


可以将大key拆分成多个小key,整个大key通过程序控制多个小key,例如初始阶段,业务方只需查询某乡公务员姓名。然而,后续需求拓展至县、市、省。开发者未预见此增长,将数据存储于单个键中,导致键变成大键,影响系统性能。现可将大键拆分成多个小键,如省、市、县、乡,使得每级行政区域的公务员姓名均对应一个键。


根据Redis版本不同处理方式也不同,4.0以上版本可以用unlink代替del,这样可以把key释放内存的工作交给后台线程去执行。6.0以上版本开启lazy-free后,执行del命令会自动地在后台线程释放内存。


使用List集合时通过控制列表保存元素个数,每个元素长度触发压缩列表(ziplist)编码,压缩列表是有顺序并且连续的内存块组成的一种专门节约内存的存储结构,通过在redis.conf(linux系统)或者redis.windows.conf(windows系统)文件里面修改以下配置实现:


```java

list-max-ziplist-entries 512

list-max-ziplist-value 64

```


## 数据集中过期

在某个时段,大量关键词(key)会在短时间内过期。当这些关键词过期时,访问Redis的速度会变慢,因为过期数据被惰性删除(被动)和定期删除(主动)策略共同管理。惰性删除是在获取关键词时检查其是否过期,一旦过期就删除。


这意味着大量过期关键词在使用之前并未删除,从而持续占用内存。主动删除则是在主线程执行,每隔一段时间删除一批过期关键词。若出现大量需要删除的过期关键词,客户端访问Redis时必须等待删除完成才能继续访问,导致客户端访问速度变慢。


这种延迟在慢日志中无法查看,经验不足的开发者可能无法定位问题,因为慢日志记录的是操作内存数据所需时间,而主动删除过期关键词发生在命令执行之前,慢日志并未记录时间消耗。


因此,当开发者感知某个关键词访问变慢时,实际上并非该关键词导致,而是Redis在删除大量过期关键词所花费的时间。

(1)开发者检查代码,找到导致集中过期key的逻辑,并设置一个自定义的随机过期时间分散它们,从而避免在短时间内集中删除key。


(2)在Redis 4.0及以上版本中,引入了Lazy Free机制,使得删除键的操作可以在后台线程中执行,不会阻塞主线程。


(3)使用Redis的Info命令查看Redis运行的各种指标,重点关注expired_keys指标。这个指标在短时间内激增时,可以设置报警,通过短信、邮件、微信等方式通知运维人员。它的作用是累计删除过期key的数量。当指标突增时,通常表示大量过期key在同一时间被删除。

## 内存淘汰策略

当Redis的内存达到最大容量限制时,新的数据将先从内存中筛选一部分旧数据以腾出空间,从而导致写操作的延迟。这是由内存淘汰策略所决定的。


常见的两种策略为淘汰最少访问的键(LFU)和淘汰最长时间未访问的键(LRU)。

- LRU策略可能导致最近一段时间的访问数据未被访问而突然成为热点数据。

- LFU策略可能导致前一段时间访问次数很多,但最近一段时间未被访问,导致冷数据无法被淘汰。


尽管LFU策略的性能优于LRU策略,但具体选择哪种策略需要根据实际业务进行调整。对于商品搜索和热门推荐等场景,通常只有少量数据被访问,大部分数据很少被访问,可以使用LFU策略。对于用户最近访问的页面数据可能会被二次访问的场景,则适合使用LRU策略。


除了选择淘汰策略外,还可以通过拆分多个实例或横向扩展来分散淘汰过期键的压力。


如果效果仍不理想,开发者可以编写淘汰过期键功能,设置定时任务,在凌晨不繁忙时段主动触发淘汰,删除过期键。

## 碎片整理

Redis存储在内存中必然会出现频繁修改的情况,而频繁的修改Redis数据会导致Redis出现内存碎片,从而导致Redis的内存使用率减低。


通常情况下在4.0以下版本的Redis只能通过重启解决内存碎片,而4.0及以上版本可以开启碎片自动整理解决,只不过碎片整理是在


主线程中完成的,通用先对延时范围和时间进行评估,然后在机器负载不高同时业务不繁忙时开启内存碎片整理,避免影响客户端请求。


开启内存自动碎片整理配置如下:


```java

# 已启用活动碎片整理

activedefrag yes

# 启动活动碎片整理所需的最小碎片浪费量

active-defrag-ignore-bytes 100mb

# 启动活动碎片整理的最小碎片百分比

active-defrag-threshold-lower 10

# 使用最大努力的最大碎片百分比

active-defrag-threshold-upper 100

# 以CPU百分比表示的碎片整理工作量最小

active-defrag-cycle-min 5

# 以CPU百分比表示的碎片整理最大工作量

active-defrag-cycle-max 75

# 将从主字典扫描中处理的集合/哈希/zset/列表字段的最大数目

active-defrag-max-scan-fields 1000

```


## 内存大页

自Linux内核2.6.38版本起,Redis可申请以2MB为单位的内存,从而降低内存分配次数,提高效率。然而,由于每次分配的内存单位增大,处理时间也相应增加。在进行RDB和AOF持久化时,Redis主进程先创建子进程,子进程将内存快照写入磁盘,而主进程继续处理写请求。数据变动时,主进程将新数据复制到一块新内存,并修改该内存块。


读写分离设计允许并发写入,无需加锁,但在主进程上进行内存复制和申请新内存会增加处理时间,影响性能。大key可能导致申请更大的内存和更长的处理时间。根据项目实际情况,关闭Redis部署机器上的内存大页机制以提高性能是一种不错的选择。

## 数据持久化与AOF刷盘

Redis提供三种持久化方式:RDB快照、AOF日志和混合持久化。默认使用RDB快照。


(1)RDB快照:周期性生成dump.rdb文件,主线程fork子线程,子线程处理磁盘IO,处理RDB快照,主线程fork线程的过程可能会阻塞主线程,主线程内存越大阻塞越久,可能导致服务暂停数秒。


(2)AOF日志:每条写入命令追加,回放日志重建数据。文件过大时,会去除没用的指令,定期根据内存最新数据重新生成aof文件。默认1秒执行一次fsync操作,最多丢失1秒数据。在AOF刷盘时,如果磁盘IO负载过高,fsync可能会阻塞主线程,主线程继续接收写请求,把数据写到文件内存里面,写操作需要等fsync执行完才可以继续执行。


(3)混合持久化:RDB快照模式恢复速度快,但可能丢失部分数据。AOF日志文件通常比RDB数据快照文件大,支持的写QPS较低。将两种持久化模式混合使用,AOF保证数据不丢失,RDB快速数据恢复,混合持久化重写时,将内存数据转换为RESP命令写入AOF文件,结合RDB快照和增量AOF修改。新文件一开始不叫appendonly.aof,重写完成后改名,覆盖原有AOF文件。先加载RDB,再重放AOF。


三种持久化方式都存在问题:fork操作可能阻塞主线程;磁盘IO负载过大时,fork阻塞影响AOF写入文件内存。


原因:fork创建的子进程复制父进程的空间内存页表,fork耗时跟进程总内存量有关,OPS越高耗时越明显。


解决方案:


(1)可以通过info stats命令查看latest_fork_usec指标观察最近一次fork操作耗时进行问题辅助定位。


(2)减少fork频率,根据实际情况适当地调整AOF触发条件


(3)Linux内存分配策略默认配置是vm.overcommit_memory=0,表示内存不足时,不会分配,导致fork阻塞。改成1,允许过量使用,直到内存用完为止。


(4)评估Redis最大可用内存,让机器至少有20%的闲置内存。


(5)定位占用磁盘IO较大的应用程序,将该应用程序移到其他机器上去,减少对Redis影响。


(6)资金充足情况下,更换高性能的SSD磁盘,从硬件层面提高磁盘IO处理能力。


(7)配置no-appendfsync-on-rewrite none表示AOF写入文件内存时,不触发fsync,不执行刷盘。这种调整有一定风险,如果Redis在AOF写入文件内存时刚好挂了,存在数据丢失情况。

## 丢包/中断/CPU亲和性

网络因素有以下问题:


(1)网络宽带和流量是否瓶颈、数据传输延迟和丢包情况、是否频繁短连接(如TCP创建和断开)


(2)数据丢包情况:数据丢包通常发生在网卡设备驱动层面,网卡收到数据包,将数据包从网卡硬件缓存转移到服务器内存中,通知内核处理,经过TCP/IP协议校验、解析、发送给上层协议,应用程序通过read系统调用从socket buffer将新数据从内核区拷贝到用户区读取数据。


TCP能动态调整接收窗口大小,不会出现由于socket buffer接收队列空间不足而丢包的情况。


然而在高负载压力下,网络设备的处理性能达到硬件瓶颈,网络设备和内核资源出现竞争和冲突,网络协议栈无法有效地处理和转发数据包,传输速度受限,而Linux使用缓冲区来缓存接收到的数据包,大量数据包涌入内核缓冲区,可能导致缓冲区溢出,进而影响数据包的处理和传输,内核无法处理所有收到的数据包,处理速度跟不上收包速度,导致数据包丢失。


(3)Redis的数据通常存储在内存中,通过网络和客户端进行交互。在这个过程中,Redis可能会受到中断的影响,因为中断可能会打断Redis的正常执行流程。当CPU正在处理Redis的调用时,如果发生了中断,CPU必须停止当前的工作转而处理中断请求。在处理中断的过程中,Redis无法继续运行,必须等待中断处理完毕后才能继续运行。这会导致Redis的响应速度受到影响,因为在等待中断处理的过程中,Redis无法响应其他请求。


(4)在NUMA架构中,每个CPU内核对应一个NUMA节点。中断处理和网络数据包处理涉及多个CPU内核和NUMA节点。Linux内核使用softnet_data数据结构跟踪网络数据包的处理状态,以实现更高效的数据处理和调度。在处理网络数据包时,内核首先在softnet_data中查找相关信息,然后根据这些信息执行相应操作,如发送数据包、重新排序数据包等。


网络驱动程序使用内核分配的缓冲区(sk_buffer)存储和处理网络数据包,当网络设备收到数据包时,会向驱动程序发送中断信号,通知其处理新数据包。驱动程序从设备获取数据包,并将其添加到sk_buffer缓冲区。内核会继续处理sk_buffer中的数据包,如根据协议类型进行分拣、转发或丢弃等。


softnet_data和sk_buffer缓冲区都可能跨越NUMA节点,在数据接收过程中,数据从NUMA节点的一个节点传递到另一个节点时,由于数据跨越了不同的节点,不仅无法利用L2和L3缓存还需要在节点之间进行数据拷贝,导致数据在传输过程中的额外开销,进而增加了传输时间和响应时间,性能下降。


(5)Linux的CPU亲和性特性也会影响进程的调度。当一个进程唤醒另一个的时候,被唤醒的进程可能会被放到相同的CPU core或者相同的NUMA节点上。当多个NUMA node处理中断时,可能导致Redis进程在CPU core之间频繁迁移,造成性能损失。

解决方案:


(1)升级网络设备或增加网络设备的数量,以提高网络处理能力和带宽。


(2)适当调整Linux内核缓冲区的大小,以平衡网络处理能力和数据包丢失之间的关系。


(3)将中断都分配到同一NUMA Node中,中断处理函数和Redis利用同NUMA下的L2、L3缓存、同节点下的内存,降低延迟。


(4)结合Linux的CPU亲和性特性,将任务或进程固定到同一CPU内核上运行,提高系统性能和效率,保证系统稳定性和可靠性。


注意:在Linux系统中NUMA亲和性可以指定在哪个NUMA节点上运行,Redis在默认情况下并不会自动将NUMA亲和性配置应用于实例部署,通常情况下通过使用Kubernetes等容器编排工具,调整节点亲和性策略或使用pod亲和性和节点亲和性规则来控制Redis实例在特定NUMA节点上运行。或者在手动部署Redis实例时,使用Linux系统中的numactl命令来查看和配置NUMA节点信息,将Redis实例部署在某个NUMA节点上。如果是在虚拟化环境中,使用NUMA aware虚拟机来部署Redis实例,让它在指定的NUMA节点上运行。


(5)添加网络流量阈值预警,超限时通知运维人员,及时扩容。


(6)编写监控脚本,正确配置和使用监控组件,使用长连接收集Redis状态信息,避免短连接。


(7)为Redis机器分配专用资源,避免其他程序占用。

## 操作系统Swap与主从同步

Redis突然变得很慢,需要考虑Redis是否使用操作系统的Swap以缓解内存不足的影响,它允许把部分内存数据存储到磁盘上,而访问磁盘速度比访问内存慢很多,所以操作系统的Swap对Redis的延时是无法接受的。


解决方案:


(1)适当增加Redis服务器的内存


(2)对Redis的内存碎片进行整理


(3)同时当Redis内存不足或者使用了Swap时,通过邮件、短信、微信等渠道通知运维人员及时处理


(4)主从架构的Redis在释放Swap前先将主节点切换至新主节点,旧主节点释放Swap后重启,待从库数据完全同步后再行主从切换,以避免影响应用程序正常运行。


在主从架构数据同步过程中,可能因网络中断或IO异常导致连接中断。建议使用支持数据断点续传的2.8及以上版本,以避免对整份数据进行复制,降低性能浪费。

## 监控

在Redis的监控中,有两种推荐的体系:ELK和Fluent + Prometheus + Grafana。


ELK体系通常使用metricbeat作为指标采集,logstash作为收集管道,并通过可视化工具kibana来呈现数据。ElasticSearch用于存储监控数据。


Fluent + Prometheus + Grafana体系则使用redis-eport作为指标采集,fluentd作为采集管道,并通过可视化工具Grafana来展示数据。Prometheus用于存储监控数据。


这两种监控体系都可以获取Redis的各项指标,并对数据进行持续化存储和对比。可视化工具使得开发者和运维人员能够更清晰地观察Redis集群的运行状况,如内存消耗、集群信息、请求键命中率、客户端连接数、网络指标、内存监控等。此外,它们都支持预警机制,例如设置慢查询日志阈值来监控慢日志个数和最长耗时,超出阈值则通过短信、微信、邮件等方式进行报警通知。这样,有了监控系统后,就可以快速发现问题、定位故障,并协助运维人员进行资源规划、性能观察等操作。

## 高可用

上述提到的主从同步和哨兵机制可以保证Redis服务的高可用,还有多级缓存、冷热分离可以保证高可用。


商品详情页在电商平台的秒杀场景中,涉及商品信息的动态展示和高并发访问,需要通过一系列手段保证系统的高并发和高可用,通过采用Nginx+Lua架构、CDN缓存、本地应用缓存和分布式缓存等多种技术手段,实现了商品详情页的动态化和缓存优化,提高用户访问商品详情页的速度和体验。同时,通过开关前置化和缓存过期机制,确保了缓存数据的有效性,降低了对后端数据库的访问压力。

### 主从同步和哨兵机制

主从复制通常采用异步方式,可能导致主节点数据尚未完全复制至从节点,主节点便已故障,导致数据丢失。因此,需要控制复制数据的时长和ACK延迟,降低数据丢失风险。


主从切换过程通常使用哨兵机制。但在主节点正常运行时,可能因与某从节点连接中断,哨兵误判主节点已故障。在此情况下,哨兵可能启动选举,将某从节点升级为主节点,导致集群出现两个主节点,发生脑裂。旧主节点恢复网络后,将被升级为从节点并挂载至新主节点,导致自身数据丢失,并需从新主节点复制数据。而新主节点并未包含后续客户端写入的数据,导致这些数据丢失。为降低数据丢失风险,可设置连接主节点最少的从节点数量和从节点连接主节点最大的延迟时间,若主节点与从节点断开连接,且从节点超过阈值时间未收到ACK消息,则拒绝客户端的写请求,将数据丢失控制在可控范围。

### 多级缓存

Java多级缓存是一种常见的优化策略,可以有效地提高系统的性能和响应速度。

#### 1.浏览器缓存

在页面间跳转时,从本地缓存获取数据;或在打开新页面时,根据Last-Modified头来CDN验证数据是否过期,减少数据传输量。

CDN缓存当用户点击商品图片或链接时,从最近的CDN节点获取数据,而非回源到北京机房,提升访问性能。

#### 2.服务端应用本地缓存

采用Nginx+Lua架构,通过HttpLuaModule模块的shared dict或内存级Proxy Cache来减少带宽。

#### 3.一致性哈希

在电商场景中,使用商品编号/分类作为哈希键,提高URL命中率。

#### 4.mget优化

根据商品的其他维度数据(如分类、面包屑、商家等),先从本地缓存读取,如不命中则从远程缓存获取。这个优化减少了一半以上的远程缓存流量。

#### 5.服务端缓存

(1)将缓存存储在内存、SSD和JIMDB中,实现读写性能和持久化的平衡。


(2)对热门商品和访问量较大的页面进行缓存,降低数据库压力。  


(3)使用Nignx缓存:存储数据量少但访问量高的热点数据,例如双11或者618活动。


(4)使用JVM本地缓存:存储数据量适中访问量高的热点数据,例如网站首页数据。


(5)使用Redis缓存:存储数据量很大,访问量较高的普通数据,例如商品信息。  

#### 6.商品详情页数据获取

(1)用户打开商品详情页时,先从本地缓存获取基本数据,如商品ID、商品名称和价格等。


(2)根据用户浏览历史和搜索记录,动态加载其他维度数据,如分类、商家信息和评论等。

#### 7.Nginx+Lua架构

(1)使用Nginx作为反向代理和负载均衡器,将请求转发给后端应用。


(2)使用Lua脚本实现动态页面渲染,并对商品详情页数据进行缓存。


(3)重启应用秒级化,重启速度快,且不会丢失共享字典缓存数据。


(4)需求上线速度化,可以快速上线和重启应用,减少抖动。


(5)在Nginx上做开关,设置缓存过期时间,当缓存数据过期时,强制从后端应用获取最新数据,并更新缓存。

### 冷热分离

冷热分离的具体步骤:


(1)分析现有系统的数据类型和访问模式,了解各类数据的冷热程度。


(2)确定合适的冷热分离策略和方案,以优化数据存储和管理。


(3)设计冷热分离架构,为热数据和冷数据选择合适的存储介质、存储策略以及数据同步机制。


(4)将冷数据从热存储介质迁移到冷存储介质,可以采用全量迁移和增量迁移的方式。


(5)对热数据进行有效管理,包括访问控制、数据安全、性能监控等,以确保数据的安全性和可用性。


(6)对冷数据进行持久化、备份、归档等操作,以防止数据丢失并确保数据的可恢复性。


(7)设计合适的故障转移和恢复策略,如主从复制、多副本存储、故障检测与恢复等,以确保系统在故障或恢复时的稳定运行。


(8)在冷热分离后对系统性能进行优化,包括优化热存储介质的性能监控、调整存储结构、调整缓存策略等。


(9)持续监控数据同步、性能指标、故障排查与修复,确保系统的稳定运行。


以实际案例进行说明:


案例1:在线购物网站的商品库存管理系统


(1)热数据:用户频繁访问的商品信息,如商品名称、价格、库存量等,需要快速响应和低延迟。


(2)冷数据:用户访问较少的商品信息,对响应速度要求较低,但对数据安全和完整性要求较高。如商品的详细描述、评价、历史价格等。


案例2:在线音乐平台的曲库管理系统


(1)热数据:用户经常访问的热门歌曲,如排行榜前10名、新上架的歌曲等,存储在高速且高可靠性的SSD硬盘Redis缓存中,以确保快速的数据访问和响应速度。


(2)冷数据:用户较少访问的歌曲,如过时的经典歌曲、小众音乐等,存储在低成本且大容量的存储介质(HDFS、Ceph、S3)中,以节省成本并存储大量历史数据。


案例3:在线求职招聘网站的职位信息管理系统


(1)热数据:用户经常访问的热门职位信息,如招聘需求高的职位、高薪职位、职位信息的基本描述、薪资范围、投递人数等。


(2)冷数据:用户较少访问的职位信息,如停招职位的详细描述、过期职位、历史招聘情况等。


小结:


在冷数据(如历史数据、归档数据等)存储场景中,使用RocksDB作为Key-Value分布式存储引擎,存储大量数据,进行数据备份和恢复,以确保在故障或系统恢复时能够快速恢复数据,节省成本并提高存储空间利用率。


在热数据(如实时更新的数据、用户操作日志等)存储场景中,使用Redis缓存支持各种高并发场景,提升响应速度。

通过以上步骤,可以有效地对冷热数据进行分离,从而实现更高效、更安全的数据存储和管理。

### 缓存雪崩、穿透、击穿、热点缓存重构、缓存失效

从前,有一个叫做小明的程序员,他的网站被越来越多的用户访问,于是他决定使用Redis缓存来提高网站性能。

一天,大雪纷飞,小明的服务器突然停机了。当服务器重新启动后,所有的缓存都失效了。这就是Redis缓存雪崩的场景。


为了避免Redis缓存雪崩,小明决定使用多级缓存和缓存预热等技术手段。他设置了多个Redis实例,同时监听同一个缓存集群。当一个实例出现问题时,其他实例可以顶替它的功能。并且,他在低访问时间段主动向缓存中写入数据,以提前预热缓存。

然而,小明并没有想到缓存穿透的问题。有些用户在请求缓存中不存在的数据时,会频繁地向数据库查询,从而拖慢服务器响应时间。这就是Redis缓存穿透的场景。


为了避免Redis缓存穿透,小明决定使用布隆过滤器等技术手段。布隆过滤器可以高效地过滤掉不存在的数据,从而减少数据库查询次数。


不久之后,小明又遇到了缓存击穿的问题。某一个热门商品被多个用户同时请求,导致缓存无法承受压力,最终请求直接打到了数据库。这就是Redis缓存击穿的场景。


为了避免Redis缓存击穿,小明决定使用分布式锁等技术手段。分布式锁可以保证同一时间只有一个用户请求数据库,避免了缓存被高并发压垮的情况。


最后,小明遇到了缓存热点重构的问题。某一个商品的热度突然升高,导致缓存集中在这个商品上,其他商品的缓存无法承受压力。这就是缓存热点重构的场景。


为了避免缓存热点重构,小明决定使用数据预热等技术手段。他在缓存中设置过期时间,同时在低访问时间段主动重构热点商品的缓存,以避免缓存集中在某一个商品上。


技术解决方案和手段:


(1)多级缓存和缓存预热:适用于缓存雪崩场景,可以提前将数据存储到缓存中,避免缓存雪崩。


(2)布隆过滤器:适用于缓存穿透场景,可以高效地过滤不存在的数据,减少数据库查询次数。


(3)分布式锁:适用于缓存击穿场景,可以保证同一时间只有一个用户请求数据库,避免了缓存被高并发压垮的情况。


(4)数据预热:适用于缓存热点重构场景,可以在低访问时间段主动重构热点商品的缓存,避免缓存集中在某一个商品上。


优缺点对比:


(1)多级缓存和缓存预热:优点是能够提前将数据存储到缓存中,避免缓存雪崩;缺点是需要占用更多的内存空间,同时预热时间过长可能会拖慢服务器响应速度。


(2)布隆过滤器:优点是可以高效地过滤不存在的数据,减少数据库查询次数;缺点是无法完全避免缓存穿透,同时需要占用一定的内存空间。


(3)分布式锁:优点是可以保证同一时间只有一个用户请求数据库,避免了缓存被高并发压垮的情况;缺点是会增加系统的复杂度,可能引入单点故障等问题。


(4)数据预热:优点是可以避免缓存热点重构的问题;缺点是需要占用更多的内存空间,同时需要在低访问时间段主动重构缓存。


总之,不同的技术解决方案和手段都有其优缺点。程序员需要根据实际情况选择适合自己的方案,并且不断地优化和改进,以提高系统的性能和稳定性。


# 消息中间件调优

## 三种mq对比


使用消息队列有解耦,扩展性,削峰,异步等功能,市面上主流的几款mq,rabbitmq,rocketmq,kafka有各自的应用场景。kafka,有出色的吞吐量,比较强悍的性能,而且集群可以实现高可用,就是会丢数据,所以一般被用于日志分析和大数据采集。rabbitmq,消息可靠性比较高,支持六种工作模式,功能比较全面,但是由于吞吐量比较低,消息累积还会影响性能,加上erlang语言不好定制,所以一般使用于小规模的场景,大多数是中小企业用的比较多。rocketmq,高可用,高性能,高吞吐量,支持多种消息类型,比如同步,异步,顺序,广播,延迟,批量,过滤,事务等等消息,功能比较全面,只不过开源版本比不上商业版本的,加上开发这个中间件的大佬写的文档不多,文档不太全,这也是它的一个缺点,不过这个中间件可以作用于几乎全场景。


## 消息丢失

消息丢失,生产者往消息队列发送消息,消息队列往消费者发送消息,会有丢消息的可能,消息队列也有可能丢消息,通常MQ存盘时都会先写入操作系统的缓存页中,然后再由操作系统异步的将消息写入硬盘,这个中间有个时间差,就可能会造成消息丢失,如果服务挂了,缓存中还没有来得及写入硬盘的消息就会发生消息丢失。


不同的消息中间件对于消息丢失也有不同的解决方案,先说说最容易丢失消息的kafka吧。生产者发消息给Kafka Broker:消息写入Leader后,Follower是主动与Leader进行同步,然后发ack告诉生产者收到消息了,这个过程kafka提供了一个参数,request.required.acks属性来确认消息的生产。


0表示不进行消息接收是否成功的确认,发生网络抖动消息丢了,生产者不校验ACK自然就不知道丢了。


1表示当Leader接收成功时确认,只要Leader存活就可以保证不丢失,保证了吞吐量,但是如果leader挂了,恰好选了一个没有ACK的follower,那也丢了。


-1或者all表示Leader和Follower都接收成功时确认,可以最大限度保证消息不丢失,但是吞吐量低,降低了kafka的性能。一般在不涉及金额的情况下,均衡考虑可以使用1,保证消息的发送和性能的一个平衡。


Kafka Broker 消息同步和持久化:Kafka通过多分区多副本机制,可以最大限度保证数据不会丢失,如果数据已经写入系统缓存中,但是还没来得及刷入磁盘,这个时候机器宕机,或者没电了,那就丢消息了,当然这种情况很极端。Kafka Broker 将消息传递给消费者:如果消费这边配置的是自动提交,万一消费到数据还没处理完,就自动提交offset了,但是此时消费者直接宕机了,未处理完的数据丢失了,下次也消费不到了。所以为了避免这种情况,需要将配置改为,先消费处理数据,然后手动提交,这样消息处理失败,也不会提交成功,没有丢消息。


rabbitmq整个消息投递的路径是producer—>rabbitmq broker—>exchange—>queue—>consumer。


生产者将消息投递到Broker时产生confirm状态,会出现二种情况,ack:表示已经被Broker签收。nack:表示表示已经被Broker拒收,原因可能有队列满了,限流,IO异常等。生产者将消息投递到Broker,被Broker签收,但是没有对应的队列进行投递,将消息回退给生产者会产生return状态。


这二种状态是rabbitmq提供的消息可靠投递机制,生产者开启确认模式和退回模式。使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。


使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。消费者在rabbit:listener-container标签中设置acknowledge属性,设置ack方式 none:自动确认,manual:手动确认。


none自动确认模式很危险,当生产者发送多条消息,消费者接收到一条信息时,会自动认为当前发送的消息已经签收了,这个时候消费者进行业务处理时出现了异常情况,也会认为消息已经正常签收处理了,而队列里面显示都被消费掉了。所以真实开发都会改为手动签收,可以防止消息丢失。


消费者如果在消费端没有出现异常,则调用channel.basicAck方法确认签收消息。消费者如果出现异常,则在catch中调用 basicNack或 basicReject,拒绝消息,让MQ重新发送消息。通过一系列的操作,可以保证消息的可靠投递以及防止消息丢失的情况。

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/e76c95d1729c443dab7b8a55cbecdc35.png)


然后说一下rocketmq,生产者使用事务消息机制保证消息零丢失,第一步就是确保Producer发送消息到了Broker这个过程不会丢消息。发送half消息给rocketmq,这个half消息是在生产者操作前发送的,对下游服务的消费者是不可见的。这个消息主要是确认RocketMQ的服务是否正常,通知RocketMQ,马上要发一个消息了,做好准备。


half消息如果写入失败就认为MQ的服务是有问题的,这个时候就不能通知下游服务了,给生产者的操作加上一个状态标记,然后等待MQ服务正常后再进行补偿操作,等MQ服务正常后重新下单通知下游服务。


然后执行本地事务,比如说下了个订单,把下单数据写入到mysql,返回本地事务状态给rocketmq,在这个过程中,如果写入数据库失败,可能是数据库崩了,需要等一段时间才能恢复,这个时候把订单一直标记为"新下单"的状态,订单的消息先缓存起来,比如Redis、文本或者其他方式,然后给RocketMQ返回一个未知状态,未知状态的事务状态回查是由RocketMQ的Broker主动发起的,RocketMQ过一段时间来回查事务状态,在回查事务状态的时候,再尝试把数据写入数据库,如果数据库这时候已经恢复了,继续后面的业务。


而且即便这个时候half消息写入成功后RocketMQ挂了,只要存储的消息没有丢失,等RocketMQ恢复后,RocketMQ就会再次继续状态回查的流程。


第二步就是确保Broker接收到的消息不会丢失,因为RocketMQ为了减少磁盘的IO,会先将消息写入到os缓存中,不是直接写入到磁盘里面,消费者从os缓存中获取消息,类似于从内存中获取消息,速度更快,过一段时间会由os线程异步的将消息刷入磁盘中,此时才算真正完成了消息的持久化。在这个过程中,如果消息还没有完成异步刷盘,RocketMQ中的Broker宕机的话,就会导致消息丢失。


所以第二步,消息支持持久化到Commitlog里面,即使宕机后重启,未消费的消息也是可以加载出来的。把RocketMQ的刷盘方式 flushDiskType配置成同步刷盘,一旦同步刷盘返回成功,可以保证接收到的消息一定存储在本地的内存中。采用主从机构,集群部署,Leader中的数据在多个Follower中都存有备份,防止单点故障,同步复制可以保证即使Master 磁盘崩溃,消息仍然不会丢失。但是这里还会有一个问题,主从结构是只做数据备份,没有容灾功能的。也就是说当一个master节点挂了后,slave节点是无法切换成master节点继续提供服务的。


所以在RocketMQ4.5以后的版本支持Dledge,DLedger是基于Raft协议选举Leader Broker的,当master节点挂了后,Dledger会接管Broker的CommitLog消息存储 ,在Raft协议中进行多台机器的Leader选举,发起一轮一轮的投票,通过多台机器互相投票选出来一个Leader,完成master节点往slave节点的消息同步。


数据同步会通过两个阶段,一个是uncommitted阶段,一个是commited阶段。Leader Broker上的Dledger收到一条数据后,会标记为uncommitted状态,然后他通过自己的DledgerServer组件把这个uncommitted数据发给Follower Broker的DledgerServer组件。


接着Follower Broker的DledgerServer收到uncommitted消息之后,必须返回一个ack给Leader Broker的Dledger。然后如果Leader Broker收到超过半数的Follower Broker返回的ack之后,就会把消息标记为committed状态。再接下来, Leader Broker上的DledgerServer就会发送committed消息给Follower Broker上的DledgerServer,让他们把消息也标记为committed状态。这样,就基于Raft协议完成了两阶段的数据同步。


第三步,Cunmser确保拉取到的消息被成功消费,就需要消费者不要使用异步消费,有可能造成消息状态返回后消费者本地业务逻辑处理失败造成消息丢失的可能。用同步消费方式,消费者端先处理本地事务,然后再给MQ一个ACK响应,这时MQ就会修改Offset,将消息标记为已消费,不再往其他消费者推送消息,在Broker的这种重新推送机制下,消息是不会在传输过程中丢失的。



## 消息重复消费

消息重复消费的问题


第一种情况是发送时消息重复,当一条消息已被成功发送到服务端并完成持久化,此时出现了网络抖动或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。


第二种情况是投递时消息重复,消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,tMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。


第三种情况是负载均衡时消息重复,比如网络抖动、Broker 重启以及订阅方应用重启,当MQ的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到重复消息。


那么怎么解决消息重复消费的问题呢?就是对消息进行幂等性处理。


在MQ中,是无法保证每个消息只被投递一次的,因为网络抖动或者客户端宕机等其他因素,基本都会配置重试机制,所以要在消费者端的业务上做消费幂等处理,MQ的每条消息都有一个唯一的MessageId,这个参数在多次投递的过程中是不会改变的,业务上可以用这个MessageId加上业务的唯一标识来作为判断幂等的关键依据,例如订单ID。而这个业务标识可以使用Message的Key来进行传递。消费者获取到消息后先根据id去查询redis/db是否存在该消息,如果不存在,则正常消费,消费完后写入redis/db。如果存在,则证明消息被消费过,直接丢弃。


## 消息顺序

消息顺序的问题,如果发送端配置了重试机制,mq不会等之前那条消息完全发送成功,才去发送下一条消息,这样可能会出现发送了1,2,3条消息,但是第1条超时了,后面两条发送成功,再重试发送第1条消息,这时消息在broker端的顺序就是2,3,1了。RocketMQ消息有序要保证最终消费到的消息是有序的,需要从Producer、Broker、Consumer三个步骤都保证消息有序才行。


在发送者端:在默认情况下,消息发送者会采取Round Robin轮询方式把消息发送到不同的分区队列,而消费者消费的时候也从多个MessageQueue上拉取消息,这种情况下消息是不能保证顺序的。而只有当一组有序的消息发送到同一个MessageQueue上时,才能利用MessageQueue先进先出的特性保证这一组消息有序。


而Broker中一个队列内的消息是可以保证有序的。在消费者端:消费者会从多个消息队列上去拿消息。这时虽然每个消息队列上的消息是有序的,但是多个队列之间的消息仍然是乱序的。消费者端要保证消息有序,就需要按队列一个一个来取消息,即取完一个队列的消息后,再去取下一个队列的消息。而给consumer注入的MessageListenerOrderly对象,在RocketMQ内部就会通过锁队列的方式保证消息是一个一个队列来取的。


MessageListenerConcurrently这个消息监听器则不会锁队列,每次都是从多个Message中取一批数据,默认不超过32条。因此也无法保证消息有序。


RocketMQ 在默认情况下不保证顺序,要保证全局顺序,需要把 Topic 的读写队列数设置为 1,然后生产者和消费者的并发设置也是 1,不能使用多线程。所以这样的话高并发,高吞吐量的功能完全用不上。全局有序就是无论发的是不是同一个分区,我都可以按照你生产的顺序来消费。分区有序就只针对发到同一个分区的消息可以顺序消费。


kafka保证全链路消息顺序消费,需要从发送端开始,将所有有序消息发送到同一个分区,然后用一个消费者去消费,但是这种性能比较低,可以在消费者端接收到消息后将需要保证顺序消费的几条消费发到内存队列(可以搞多个),一个内存队列开启一个线程顺序处理消息。


RabbitMq没有属性设置消息的顺序性,不过我们可以通过拆分为多个queue,每个queue由一个consumer消费。或者一个queue对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理,保证消息的顺序性。


## 消息积压

线上有时因为发送方发送消息速度过快,或者消费方处理消息过慢,可能会导致broker积压大量未消费消息。消息数据格式变动或消费者程序有bug,导致消费者一直消费不成功,也可能导致broker积压大量未消费消息。解决方案可以修改消费端程序,让其将收到的消息快速转发到其他主题,可以设置很多分区,然后再启动多个消费者同时消费新主题的不同分区。可以将这些消费不成功的消息转发到其它队列里去,类似死信队列,后面再慢慢分析死信队列里的消息处理问题。


在RocketMQ官网中,还分析了一个特殊情况,如果RocketMQ原本是采用的普通方式搭建主从架构,而现在想要中途改为使用Dledger高可用集群,这时候如果不想历史消息丢失,就需要先将消息进行对齐,也就是要消费者把所有的消息都消费完,再来切换主从架构。因为Dledger集群会接管RocketMQ原有的CommitLog日志,所以切换主从架构时,如果有消息没有消费完,这些消息是存在旧的CommitLog中的,就无法再进行消费了。这个场景下也是需要尽快的处理掉积压的消息。


## 延迟队列

消息被发送以后,并不想让消费者立刻获取,而是等待特定的时间后,消费者才能获取这个消息进行消费。例如10分钟,内完成订单支付,支付完成后才会通知下游服务进行进一步的营销补偿。往MQ发一个延迟1分钟的消息,消费到这个消息后去检查订单的支付状态,如果订单已经支付,就往下游发送下单的通知。而如果没有支付,就再发一个延迟1分钟的消息。最终在第10个消息时把订单回收,就不用对全部的订单表进行扫描,而只需要每次处理一个单独的订单消息。这个就是延迟对列的应用场景。


rabbittmq,rocketmq都可以通过设置ttl来设置延迟时间,kafka则是可以在发送延时消息的时候,先把消息按照不同的延迟时间段发送到指定的队列中,比如topic_1s,topic_5s,topic_10s,topic_2h,然后通过定时器进行轮训消费这些topic,查看消息是否到期,如果到期就把这个消息发送到具体业务处理的topic中,队列中消息越靠前的到期时间越早,具体来说就是定时器在一次消费过程中,对消息的发送时间做判断,看下是否延迟到对应时间了,如果到了就转发,如果还没到这一次定时任务就可以提前结束了。

mq设置过期时间,就会有消息失效的情况,如果消息在队列里积压超过指定的过期时间,就会被mq给清理掉,这个时候数据就没了。解决方案也有手动写程序,将丢失的那批数据,一点点地查出来,然后重新插入到 mq 里面去。


## 消息队列高可用

对于RocketMQ来说可以使用Dledger主从架构来保证消息队列的高可用,这个在上面也有提到过。


然后在说说rabbitmq,它提供了一种叫镜像集群模式,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。


RabbitMQ 有很好的管理控制台,可以在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。只不过消息需要同步到所有机器上,导致网络带宽压力和消耗很重。


最后再说说kafka,它是天然的分布式消息队列,在Kafka 0.8 以后,提供了副本机制,一个 topic要求指定partition数量,每个 partition的数据都会同步到其它机器上,形成自己的多个 replica 副本,所有 replica 会选举一个 leader 出来,其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去。如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的,如果这上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来。



# ElasticSearch调优

## CPU优化

对于提升服务能力来说,升级硬件设备配置是最快速有效的方法之一。在配置Elasticsearch服务器时,考虑CPU型号对性能的影响非常重要。因此,建议选用具有高性能CPU的服务器,例如IntelXeon系列或AMDOpteron系列。此外,为了充分利用多核处理器的优势,可以将Elasticsearch节点放置在不同的物理CPU上,以增加性能。


大多数Elasticsearch部署对CPU的要求不高,常见的集群使用2到8个核的机器。如果需要选择更快的CPU或更多的核数,则选择更多的核数更加优越。因为多个内核可以提供更多的并发,这比略微更快的时钟频率更加重要。


注意:CPU的时钟频率是指CPU每秒钟能够执行的时钟周期次数。它通常以赫兹(Hz)为单位表示,如1GHz(1000兆赫)或2.4GHz(2.4千兆赫)。它影响CPU的处理能力和速度,因为更高的时钟频率意味着CPU能够执行更多的指令,并在更短的时间内完成任务。但是,时钟频率并不是唯一决定CPU性能的因素,其他因素如架构、缓存等也会对其性能产生影响。


## 内存优化

为了让Elasticsearch具有良好的性能,需要为其分配足够的内存。对于确定所需内存大小,需要考虑预期的数据量和查询负载进行估算。一般情况下,建议将内存分配给JVM堆,以确保Elasticsearch可以尽可能多地利用内存执行操作。但是,对于内存大小的设置,需要遵循以下规则:


当机器内存小于64G时,应将JVM堆大小设置为物理内存的50%左右,其中一半留给Lucene,另一半留给Elasticsearch。


当机器内存大于64G时,若主要使用场景是全文检索,建议给Elasticsearch Heap分配`4~32G`的内存,其余内存留给操作系统,以供Lucene使用。


若主要使用场景是聚合或排序,并且大多数是数值、日期、地理点和非分析类型的字符数据,建议给Elasticsearch Heap分配4~32G的内存,其余内存留给操作系统,以提供更快的查询性能。


若使用场景是聚合或排序,并且都是基于分析类型的字符数据,需要更多的Heap大小,建议机器上运行多个Elasticsearch实例,每个实例保持不超过50%的Elasticsearch Heap设置(但不超过32G),50%以上留给Lucene。


此外,禁止使用swap,否则会导致严重的性能问题。为了保证Elasticsearch的性能,可以在elasticsearch.yml中设置bootstrap.memory_lock:true,以保持JVM锁定内存。


值得注意的是,由于Elasticsearch构建基于Lucene,Lucene的索引文件segments是存储在单个文件中的,对于操作系统来说,将索引文件保持在缓存中以提高访问速度是非常友好的。


## 网络优化

网络带宽是Elasticsearch性能的瓶颈之一,因为基于网络通信的查询和索引操作需要充分利用带宽。若带宽不足,则可能导致操作变慢或超时。在需要传输大量数据时,带宽限制也可能成为性能瓶颈,影响集群响应时间和高并发请求的处理。


除了网络带宽,网络延迟也是Elasticsearch性能瓶颈的重要因素之一。网络延迟可能导致请求和响应之间的延迟或超时,从而影响集群的响应能力。由于Elasticsearch是分布式的,需要在不同节点之间传输数据,因此网络延迟高会降低其查询和索引性能。


此外,网络故障也可能导致Elasticsearch节点之间通信中断,影响集群的可用性和数据一致性。


网络拓扑结构也会影响集群的性能,例如,如果两个节点之间的网络距离很远,则同步数据的时间可能会增加,并且可能会增加网络故障的风险。


安全设置(例如加密和身份验证)可能会增加网络负载并影响Elasticsearch性能。因此,可以通过优化网络安全设置来减少性能损失。


为了提高Elasticsearch集群网络的性能和稳定性,需要对以下几个方面进行优化:


(1)带宽限制:当Elasticsearch集群的数据量较大,节点之间的数据交换量较大,可能会出现带宽限制的情况。解决这个问题的方法是增加带宽,可以升级网络硬件设备或购买更高带宽的网络服务。同时,可以使用分片和副本来减少节点之间的数据交换量,从而减少带宽负载。


(2)网络延迟:网络延迟是指在节点之间传输数据时所需要的时间,如果网络延迟过高,会影响Elasticsearch集群的性能。优化网络设置可以降低网络延迟,可以使用更快的网络硬件设备,采用更优化的网络协议,优化Elasticsearch的配置参数等方式来降低网络延迟。使用高速网络设备和协议:如Infiniband或RDMA,可以提高网络传输速度,降低网络延迟。


(3)网络故障:网络故障可能会导致Elasticsearch集群无法正常工作,因此需要采取相应的措施来解决网络故障问题。其中一种方法是采用冗余节点或备份节点来解决网络故障问题。当一个节点无法正常工作时,备份节点可以快速接管工作。同时,可以使用网络监控工具来及时发现并解决网络故障问题。如:Wireshark用于网络故障排除和网络安全分析、Nagios检查主机、服务和网络设备的状态来进行网络监控、Zabbix监控网络设备的状态、性能、流量和带宽使用情况等。


(4)网络拓扑结构:优化网络拓扑结构可以提高Elasticsearch集群的性能。可以采用更合理的网络拓扑结构,例如将Elasticsearch节点放置在相同的数据中心或物理机架上,这可以减少数据同步时间和网络故障的风险。在同一物理机架内或同一数据中心内部,可以使用多个节点来提高集群的性能和容错能力。


(5)网络安全:网络安全是Elasticsearch运行过程中必须关注的问题。可以针对网络安全问题进行优化,采用更快的加密算法,对于不同的数据流采用不同的加密等级。同时,可以使用更快的身份认证算法,例如使用公钥认证等方式来提高Elasticsearch性能。采用分层的网络架构可以提高集群的安全性和性能。例如,在内部网络中使用防火墙和安全网关来保护Elasticsearch集群,并将公共接口放置在外部网络中,以提供对外服务。


(6)部署负载均衡器:通过在Elasticsearch节点之间部署负载均衡器,可以平衡查询负载,避免单个节点负载过重导致性能下降。同时,负载均衡器还可以提高Elasticsearch集群的可用性,当有节点故障时,负载均衡器可以自动将查询请求发送到其他节点,保证服务的连续性。


## 磁盘优化

Elasticsearch的性能会受到磁盘延迟的影响,因此为了优化磁盘性能,建议使用高速存储设备如SSD或NVMe,并选择合适的RAID级别。尽可能选择固态硬盘(SSD),因为它比任何旋转介质机械硬盘或磁带写入数据时都会有较大的IO提升,特别是在随机写和顺序写方面。同时,应确保系统I/O调度程序的正确配置,以优化写入数据发送到硬盘的时间,这可以提高写入速度。默认*nix发行版下的调度程序cfq是为机械硬盘优化的,而deadline和noop是为SSD优化的,使用它们可以提高写入性能。


如果使用机械硬盘,可以尝试获取15kRPM驱动器或高性能服务器硬盘来提高磁盘速度。此外,使用RAID0也是提高硬盘速度的有效途径。不需要使用镜像或其他RAID变体,因为Elasticsearch本身提供了副本功能进行备份。如果在硬盘上使用备份功能,对写入速度有较大的影响。最后,避免使用网络附加存储(NAS),因为NAS通常很慢、延时大且是单点故障,而且不如本地驱动器更可靠。因此,建议将数据和索引分开存储,以充分利用不同类型的存储设备。


## 计算机系统优化

为了优化Elasticsearch的性能,在以下方面可以采取措施:


(1)禁用不必要的服务和进程,避免占用系统资源,确保Elasticsearch能够充分利用系统资源。


(2)将Elasticsearch节点配置为静态IP,避免动态IP更改导致网络连接问题,从而保证Elasticsearch节点的稳定性。


(3)关闭防火墙或者调整其设置,以避免防火墙对网络流量造成延迟和影响搜索性能。

操作系统对于Elasticsearch的性能有着重要影响,因此为了优化操作系统设置,可以采取以下措施:


首先,可以通过调整内核参数来提高网络性能和文件系统性能,这可以通过修改/sys文件系统下的控制参数来实现。例如,可以通过调整tcp_tw_reuse和tcp_tw_recycle参数来提高TCP连接的处理效率。同时,也可以通过调整文件系统的读写缓存参数来提高文件系统的性能。


其次,可以将文件描述符限制设置为较高的值,以允许Elasticsearch使用更多的文件句柄。可以通过修改/etc/security/limits.conf文件来设置文件描述符限制。这样可以避免因文件描述符数量不足而导致Elasticsearch性能下降的情况。


另外,还可以启用TransparentHugepages和NUMA支持,以提高内存访问效率。TransparentHugepages是一种Linux内核特性,它可以将大页内存自动映射到进程的虚拟内存空间中,从而减少内存管理时的开销。而NUMA是一种多处理器系统结构,它可以将内存和处理器的访问紧密地绑定在一起,从而提高内存访问效率。


综上所述,通过对操作系统进行优化设置,可以有效提升Elasticsearch的性能和稳定性。


## Elasticsearch本身配置参数

为了优化Elasticsearch性能,可以调整以下配置参数:


(1)分配更大的JVM堆内存,以允许Elasticsearch使用更多的内存来缓存索引和搜索结果。


(2)调整索引分片数量,以平衡查询负载和数据分布。


(3)调整索引缓存设置,以缓存查询结果并减少IO操作。


(4)调整搜索线程池大小,以避免搜索请求积压造成性能下降。


## GC调优

根据Elasticsearch官方发布的文档,JDK8附带的HotSpotJVM的早期版本存在可能导致索引损坏的问题,特别是在启用G1GC收集器时。这个问题影响JDK8u40附带的HotSpot版本之前的版本。


如果使用较高版本的JDK8或JDK9+,建议使用G1GC,因为它对Heap大对象的优化效果比较明显,目前的项目也是使用G1GC并且运行效果良好。为了启用G1GC,需要修改jvm.options文件中的配置。将原本的如下配置,代码如下:


```java

# 开启使用CMS垃圾回收器

-XX:+UseConcMarkSweepGC

# 初始化CMS垃圾回收器的初始占用阈值为75%

-XX:CMSInitiatingOccupancyFraction=75

# 只使用初始化占用阈值作为CMS垃圾回收器的出发条件

-XX:+UseCMSInitiatingOccupancyOnly

改为:

# 开启 G1 垃圾回收器

-XX:+UseG1GC

# 设置最大垃圾回收时间为 50 毫秒

-XX:MaxGCPauseMillis=50

```


其中,-XX:MaxGCPauseMillis用于控制预期的最高GC时长,默认值为200ms。如果线上业务对GC停顿敏感,可以适当设置低一些,但是如果设置过小,可能会带来比较高的CPU消耗。需要注意的是,如果集群因为GC导致卡死,仅仅换成G1GC可能无法根本上解决问题,通常需要优化数据模型或者Query。总之,使用G1GC需要慎重,需要根据具体情况进行评估和调整。


## 索引优化设置

索引优化是Elasticsearch中的一个重要方面,它主要是通过优化插入过程来提升Elasticsearch的性能。虽然Elasticsearch的索引速度本身已经相当快了,但是具体数据仍可以参考官方的benchmark测试结果来了解。根据测试结果总结,不同的硬件和软件组合会影响Elasticsearch在不同场景下的性能表现。具体如下:


(1)索引速度:单节点下,Elasticsearch可以达到每秒2,000个文档的索引速度;而在分布式集群中,其索引速度可以达到每秒3,000到8,000个文档。使用具有高性能硬件配置的服务器可获得更快的索引速度。而索引速度取决于机器的CPU、内存、网络带宽和磁盘性能等因素。


(2)搜索速度:在单节点上,Elasticsearch的搜索速度可以达到每秒50万到70万次查询;在分布式集群上,随着节点和硬件的增加,其搜索速度可以线性扩展,最高可以达到数百万次查询。使用SSD硬盘的服务器和更高版本的Elasticsearch(如5.x和6.x)也可以提高搜索速度。


(3)响应时间:在所有硬件和软件组合下,Elasticsearch的响应时间都可以控制在1秒以内。使用高性能硬件配置的服务器可以获得更快的响应时间。


(4)内存使用:Elasticsearch使用内存来缓存数据,提高读取和写入速度。虽然内存使用与集群规模和硬件配置有关,但在所有情况下,Elasticsearch的内存使用都可以整体控制在较低的水平。使用高性能硬件配置的服务器可以获得更低的内存使用率。


总之,硬件配置越高,性能指标就越好。但是需要注意的是,硬件配置越高,成本也会越高。因此,在实际使用中,需要根据自己的应用场景和需求,选择合适的硬件配置,以达到最优性能和成本的平衡。


## 批量提交

建议在提交大量数据时,采用批量提交(Bulk操作)的方式来提高效率。使用bulk请求时,每个请求的大小不要超过几十兆字节,因为太大会导致内存使用过大。在ELK过程中,比如Logstashindexer向Elasticsearch中提交数据,可以通过调整batchsize来优化性能。


需要根据文档大小和服务器性能来设置合适的size大小。如果Logstash中提交文档大小超过20MB,Logstash会将一个批量请求切分为多个批量请求。如果在提交过程中,遇到EsRejectedExecutionException异常,则说明集群的索引性能已经达到极限。此时,可以考虑提高服务器集群的资源,或者减少数据收集速度。例如,只收集Warn、Error级别以上的日志,以降低数据量。需要根据业务规则来决定如何进行优化。


## 增加Refresh时间间隔

为了提高Elasticsearch的索引性能,在写入数据的过程中,采用了延迟写入的策略。换言之,数据先写入内存中,当超过默认的1秒(即index.refresh_interval)后,会进行一次写入操作,将内存中的segment数据刷新到磁盘中。只有当数据刷新到磁盘中后,才能对数据进行搜索操作,因此Elasticsearch提供的是近实时搜索功能,而不是实时搜索功能。


如果系统对数据的延迟要求不高,可以通过延长refresh时间间隔来减少segment合并压力,从而提高索引速度。例如在进行全链路跟踪时,可以将index.refresh_interval设置为30秒,从而减少refresh次数。在进行全量索引时,可以将refresh次数临时关闭,将index.refresh_interval设置为-1,数据导入成功后再打开到正常模式,例如设置为30秒。在加载大量数据时,也可以暂时不使用refresh和replicas功能,将index.refresh_interval设置为-1,将index.number_of_replicas设置为0。


综上所述,延迟写入的策略可以提高Elasticsearch的索引性能,适当地调整refresh时间间隔和关闭refresh和replicas功能可以更进一步地提高性能。


## 修改index_buffer_size的设置

索引缓冲是一个重要的性能优化工具,通过调整索引缓冲的设置,可以控制内存的分配情况,从而优化节点的索引进程的性能。在Elasticsearch中,索引缓冲的设置是一个全局配置,它会应用于一个节点上所有不同的分片上。可以通过在Elasticsearch的配置文件中设置indices.memory.index_buffer_size参数来控制索引缓冲的大小。这个参数可以接受一个百分比或者一个表示字节大小的值。


默认是10%,意味着分配给节点的总内存的10%用来做索引缓冲的大小。如果设置的是百分比,那么这个百分比会被分到不同的分片上。同时,也可以通过设置indices.memory.min_index_buffer_size参数来指定最小的索引缓冲大小,这个参数的默认值是48mb。另外,还可以通过设置indices.memory.max_index_buffer_size参数来控制索引缓冲的最大值。


需要注意的是,如果为索引缓冲设置了一个过大的值,可能会导致节点的性能下降。因此,在进行索引缓冲的设置时,需要根据具体的应用场景和硬件配置来进行权衡和调整。


## 修改translog相关的设置

为了减少硬盘的IO请求,可以采取一系列操作来优化系统的性能表现。其中一个方法是通过控制数据从内存到硬盘的操作频率来实现。可以通过增加sync_interval时间的设置,来延迟数据写入到硬盘的操作。这样可以降低硬盘的负载,减少硬盘的IO请求。


sync_interval的默认时间为5秒,可以通过命令index.translog.sync_interval:5s来设置。


另一个方法是控制translog数据块的大小,以减少flush到lucene索引文件的次数。这样可以进一步减少对硬盘的IO请求,提高系统性能表现。translog数据块的默认大小为512m,可以通过命令index.translog.flush_threshold_size:512mb来设置。需要根据具体的实际情况进行调整。


综上所述,以上两种方法都可以减少硬盘的IO请求,提高系统的性能表现。需要根据系统的实际情况进行优化调整,以达到最优化的性能表现。


## _id字段、_all字段、_source字段、index属性

在使用Elasticsearch中,应该注意一些最佳实践的建议。


首先,_id字段不应该自定义,因为这可能会导致版本管理方面的问题。建议使用Elasticsearch提供的默认ID生成策略或使用数字类型ID做为主键。


其次,需要注意使用_all字段和_source字段的场景和实际需求。_all字段包含了所有的索引字段,可以方便做全文检索,但如果不需要进行全文检索,就可以禁用该字段。_source字段存储了原始的document内容,如果没有获取原始文档数据的需求,可以通过设置includes和excludes属性来定义需要存储在_source中的字段,避免不必要的存储开销。因此,在使用_all字段和_source字段时,需要根据实际需求来进行权衡和选择。


此外,合理的配置使用index属性,analyzed和not_analyzed,根据业务需求来控制字段是否分词或不分词。只有groupby需求的字段,应该配置成not_analyzed,以提高查询或聚类的效率。综上所述,对于Elasticsearch的使用,需要根据实际情况来进行配置和选择,以达到最佳实践的效果。


## 减少副本数量

Elasticsearch默认拷贝数量为3个,这样的配置虽然能够提高Cluster的可用性,增加查找次数,但是对写入索引的效率也会造成影响。在索引过程中,需要将更新的文档发送到副本节点上,等待副本节点生效后才能返回结果。在实际应用中,建议根据实际需求来调整副本数目。


对于业务搜索等关键应用场景,建议仍然保留副本数为3个,以确保数据的可靠性和高可用性;而对于内部ELK日志系统、分布式跟踪系统等应用场景,则完全可以将副本数设置为1个来提高写入效率。因此,在进行Elasticsearch集群的配置时,需要综合考虑集群的可用性、性能和数据可靠性等因素,以实现最优的性价比。


## 查询方面优化

Elasticsearch作为业务搜索的近实时查询时,查询效率的优化显得尤为重要。


### 路由优化

Elasticsearch作为业务搜索的查询效率的优化显得尤为重要。在查询文档时,Elasticsearch使用公式shard=hash(routing)%number_of_primary_shards来计算文档应该存放到哪个分片中。routing默认为文档的ID,也可以使用用户ID等自定义值。通过对路由进行优化,可以提高查询效率和搜索速度。


### routing查询

查询数据时,如果不带routing参数,则查询过程需要经过分发和聚合两个步骤。


首先,请求会被发送到协调节点,协调节点将查询请求分发到每个分片上;随后,协调节点搜集每个分片上的查询结果,进行排序,最后将结果返回给用户。这种方式存在的问题是由于不知道需要查询的数据具体在哪个分片上,因此需要搜索所有分片,增加了查询的时间和资源消耗。


相比较而言,带routing查询可以直接根据routing信息定位到某个分片进行查询,而不需要查询所有的分片,经过协调节点进行排序。以用户查询为例,如果将routing设置为userid,则可以直接查询出数据,大大提高了查询效率。


总之,带routing查询可以加速查询过程,减少了不必要的查询操作,提高了查询效率。


### Filter VS Query

Filter与Query的使用方法是Elasticsearch中最常用的两种查询上下文,但是它们的使用方式是有所不同的。在实际使用中,应该尽可能使用过滤器上下文(Filter)进行查询,而不是使用查询上下文(Query)。


Query上下文主要是用来评估文档与查询语句之间的匹配程度,并为匹配的文档打分。相比之下,过滤器上下文主要是用来检查文档是否与查询语句匹配,它所做的仅仅是返回结果为是或否的答案,无需进行打分等计算过程,从而提高查询的效率和性能。


此外,过滤器上下文的结果还可以进行缓存,即使在多次查询中使用同样的查询条件,也可以直接返回缓存中已经计算得到的结果,避免了重复计算,提高了查询效率。因此,在实际使用中,尽可能使用过滤器上下文进行查询是非常有必要和推荐的。


### 深度翻页

在使用Elasticsearch过程中,应注意尽量避免大翻页的出现,因为正常翻页查询都是从from开始size条数据,需要在每个分片中查询打分排名在前面的from+size条数据。这样,协同节点将会收集每个分配的前from+size条数据,一共会受到N*(from+size)条数据,然后进行排序,最终返回其中from到from+size条数据。如果from或size很大,参加排序的数量也会同步扩大很多,导致CPU资源消耗增大。


为了解决这一问题,可以使用Elasticsearch中的scroll和scroll-scan高效滚动的方式,以减小CPU资源消耗。


除此之外,还可以结合实际业务特点,根据文档id大小和文档创建时间的一致有序性,以文档id作为分页的偏移量,并将其作为分页查询的一个条件。这样可以优化查询性能,减少排序参与的数据量,提高查询效率。同时,这也需要根据具体业务情况进行实践验证。


### 脚本合理使用

在开发中,脚本(script)的合理使用至关重要。目前脚本使用主要有三种形式:内联动态编译方式、_script索引库中存储和文件脚本存储的形式。其中,内联动态编译方式适合较小的脚本,文件脚本存储形式适合大型脚本,而_script索引库中存储形式则是最常见的使用方式之一。


一般来说,在实际应用中,应尽量采用第二种方式,先将脚本存储在_script索引库中,从而能够提前编译脚本。通过引用脚本id并结合params参数,可以实现模型(逻辑)和数据的分离,同时也更便于脚本模块的扩展和维护。


在使用脚本时,一定要注意场景的选择。对于较小的脚本,可以使用内联动态编译方式;对于大型脚本,则应采用文件脚本存储形式。而对于一些常见的使用场景,最好使用_script索引库中存储形式,以达到更好的编译效果和更高的代码可读性。


总之,脚本的合理使用能够提高开发效率和代码质量,是开发过程中非常重要的一环。


### Cache的设置及使用

在Elasticsearch中,查询性能的优化是非常重要的。其中,QueryCache是Elasticsearch查询的关键性能优化之一,因为它可以减少查询的响应时间。当使用filter查询时,ES会自动使用QueryCache。如果业务场景中的过滤查询比较多,建议将querycache设置大一些,以提高查询速度。可以通过indices.queries.cache.size参数来设置QueryCache的大小,它的默认值为10%。用户可以将其设置为百分比或具体值,如256mb。此外,用户还可以通过设置index.queries.cache.enabled参数来禁用QueryCache。


除了QueryCache之外,Elasticsearch还提供了另一个缓存机制,即FieldDataCache。在聚类或排序场景下,Elasticsearch会频繁使用FieldDataCache。因此,为了提高查询性能,建议用户在这些场景下设置FieldDataCache的大小。


indices.fielddata.cache.size参数可用于设置FieldDataCache的大小,用户可以将其设置为30%或具体值10GB。但是,如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的。


在查询请求发起后,每个分片会将结果返回给协调节点。为了提升查询性能,Elasticsearch提供了一个ShardRequestCache。用户可以通过index.requests.cache.enable参数来开启ShardRequestCache。但是,需要注意的是,shardrequestcache只缓存hits.total、aggregations和suggestions类型的数据,并不会缓存hits的内容。用户也可以通过设置indices.requests.cache.size参数来控制缓存空间大小。默认情况下,其值为1%。


### 更多查询优化经验

(1)针对query_string或multi_match的查询,可以采用将多个字段的值索引到一个新字段的方法。在mapping阶段设置copy_to属性,将多个字段的值索引到新字段,这样在进行multi_match查询时可以直接使用新字段进行查询,从而提高查询速度。


(2)对于日期字段的查询,特别是使用now进行查询时,由于不存在缓存,建议从业务需求出发,考虑是否必须使用now进行查询。事实上,利用querycache可以大大提高查询效率,因此也需要对查询缓存进行充分利用。


(3)在设置查询结果集大小时,应根据实际情况进行设置,不能设置过大的值,如将query.setSize设置为Integer.MAX_VALUE。因为Elasticsearch内部需要建立一个数据结构来存放指定大小的结果集数据,设置过大的值会耗费大量的内存资源。


(4)对于聚合查询,需要避免层级过深的aggregation,因为这会导致内存和CPU资源消耗较大。建议在服务层通过程序来组装业务,或者采用pipeline的方式来优化查询。


(5)对于预先聚合数据的方式,可以采用复用预索引数据的技巧来提高聚合性能。例如,如果要根据年龄进行分组,可以预先在索引阶段设置一个age_group字段,将数据进行分类,而不是通过rangeaggregations来按年龄分组。这样可以通过age_group字段来进行groupby操作,从而避免层级过深的aggregation查询,提高聚合性能。


(6)在编写代码时,有时需要在数据集中进行匹配查询。对于大型数据集,经常需要考虑性能问题。为了优化查询性能,一种常见的优化方式是使用filter代替match查询。filter的优点在于它可以缓存,因此可以提高查询的速度。


此外,由于match查询通常涉及模糊匹配和转换的过程(fuzzy_transpositions),使用filter可以避免这种无意义的查询操作,进一步提高查询效率。然而,需要注意的是,使用filter来代替match查询的优化是有限的。对于一些复杂的查询,仍需要使用match查询来实现。因此,在优化查询性能时,需要综合考虑使用不同的查询方式,以达到最优化的效果。


(7)对于数据类型的选择需要理解Elasticsearch底层数据结构,在Elasticsearch2.x时代,所有数字都是按照keyword类型进行处理,这意味着每个数字都会建立一个倒排索引。虽然这种处理方式可以提高查询速度,但是在执行范围查询时,例如type>1 and type<5,需要将查询转换为type in (1,2,3,4,5),这显著增加了范围查询的难度和耗时。随后,Elasticsearch进行了优化,在处理integer类型数据时采用了一种类似于B-tree的数据结构,即Block k-d tree,以加速范围查询。Block k-d tree被设计用于多维数值字段,并可用于高效过滤地理位置等数据。


此外,它还可用于单维度的数值类型。对于单维度的数据,Block k-d tree的实现与传统的B-tree有所不同。它对所有值进行排序,并反复从中心进行切分,生成具有类似B-tree结构的索引。该结构的叶子节点存储的不是单个值,而是一个值的集合,也就是所谓的一个Block。每个Block最多包含512至1024个值,以确保值在Block之间均匀分布。


这种数据结构大大提高了范围查询的性能,因为在传统的索引结构中,满足查询条件的文档集合并不是按照文档ID顺序存储的,而是需要构造一个巨大的bitset来表示。而使用Block k-d tree索引结构,则可以直接定位满足查询条件的叶子节点块在磁盘上的位置,然后顺序读取,显著提高了范围查询的效率。


(8)在使用Block k-d tree的数据结构进行范围查询时,磁盘读取是顺序读取,因此对范围查询有很大的优势。然而在某些场景下使用PointRangeQuery会非常慢,因为它需要将满足查询条件的docid集合拿出来单独处理。这个处理过程在org.apache.lucene.search.PointRangeQuery#createWeight方法中可以读取到,主要逻辑是在创建scorer对象的时候,顺带先将满足查询条件的docid都选出来,然后构造成一个代表docid集合的bitset。


在执行advance操作时,就会在这个bitset上完成。由于这个构建bitset的过程类似于构造Query cache的过程,所有的耗时都在build_scorer上。因此,使用PointRangeQuery在该场景下会非常慢。另外,对于term查询,如果数值型字段被转换为PointRangeQuery,也会遇到同样的问题。在这种情况下,无法像Postlings list那样按照docid顺序存放满足查询条件的docid集合,因此无法实现postings list上借助跳表做蛙跳的操作。


(9)对于像isDeleted这样只有两个可能取值(是/否)的字段,Elasticsearch会自动根据倒排索引的文档数和Term的文档频率来判断是否使用倒排索引进行查询。如果该Term的文档频率太高,超过了一定的阈值,Elasticsearch会认为使用倒排索引查询的效率不如使用全表扫描,因此会放弃使用倒排索引,转而使用全表扫描。这个阈值可以通过设置index.max_terms_count来调整。如果该字段在查询时频繁被使用,可以考虑将其映射为一个不分词、不使用倒排索引的字段,这样可以避免在查询时产生额外的开销。


(10)当进行多个term查询并列的时候,在Elasticsearch中执行顺序不是由查询时写入的顺序决定的。实际上,Elasticsearch会根据每个filter条件的区分度来评估执行顺序,将高区分度的filter条件先执行,以此可以加速后续的filter循环速度,从而提高查询效率。举例来说,如果一个查询条件的结果集很小,那么Elasticsearch就会优先执行这个条件。为了实现这一点,当使用term进行查询时,每个term都会记录一个词频,即这个term在整个文档中出现的次数。这样Elasticsearch就能判断每个term的区分度高低,从而决定执行顺序。综上所述,当使用多个term查询时,Elasticsearch会根据每个filter条件的区分度来决定执行顺序,以此提高查询效率。


(11)为了快速查找索引 Term 的位置,可以采用哈希表作为索引表来提高查找效率。同时,为了减少倒排链的查询和读取时间,可以采用 RoaringBitmap 数据结构来存储倒排链,并且结合 RLE Container 实现倒排链的压缩存储。这样,可以将倒排链的合并问题转化为排序问题,从而实现批量合并,大大降低了合并的性能消耗。另外,根据数据的分布,自动选择 bitmap/array/RLE 容器,可以进一步提高 RLE 倒排索引的性能。


(12)在增量索引场景下,如果增量索引的变更量非常大,会导致频繁更新内存 RLE 倒排索引,进而带来内存和性能的消耗。为了解决这个问题,可以将 RLE 倒排索引的结构固化到文件中,在写索引时就可以完成对倒排链的编码,避免了频繁更新内存索引的问题。这种做法可以提升索引的写入性能,同时保证了查询的高效性和稳定性。

通过开启慢查询配置定位慢查询


一般而言,当 Elasticsearch 查询所花费的时间超过一定阈值时,系统会记录该查询的相关信息并将其记录在慢查询日志中以供查看。在实际的应用场景中,可以通过对慢查询日志的分析来确定哪些查询较为耗时,从而帮助进行性能优化。通过开启慢查询配置可以快速定位Elasticsearch查询速度缓慢的问题,并进行相应的性能调优,提高系统的查询效率和用户体验。对于 ElasticSearch这类搜索引擎而言,通过设置开启慢查询日志可以快速定位查询速度较慢的原因。


## 数据结构优化

针对 Elasticsearch 的使用场景,需要根据实际情况进行文档数据结构的设计,以便更好地发挥 Elasticsearch 的搜索和分析能力。在设计文档结构时,需要将使用场景作为主要考虑因素,去掉不必要的数据。这有助于减少索引的大小、提高搜索和分析的效率。


在实际使用 Elasticsearch 进行数据存储和检索时,应根据具体场景灵活使用索引和文档类型,合理划分数据和定义字段。这有助于提高搜索和分析的精度和效率,并能够满足不同场景的需求。


因此,在使用 Elasticsearch 时,必须深入了解应用场景,进行合理的文档数据结构设计,去掉不必要的数据,提高搜索和分析的效率和精度,最终实现更好的业务效果和用户体验。


### 减少不需要的字段

如果Elasticsearch作为业务搜索服务的一部分,应该避免将一些不需要用于搜索的字段存储到Elasticsearch中。这种做法能够节省空间,同时在相同的数据量下,也能提高搜索性能。此外,应该避免使用动态值作为字段,因为动态递增的mapping可能会导致集群崩溃。同样,需要控制字段的数量,业务中不使用的字段应该不要索引。控制索引的字段数量、mapping深度、索引字段的类型,这是优化Elasticsearch性能的关键之一。


Elasticsearch在默认情况下设置了一些关于字段数、mapping深度的限制,即index.mapping.nested_objects.limit、index.mapping.total_fields.limit和index.mapping.depth.limit。其中,index.mapping.nested_objects.limit限制了Elasticsearch中嵌套对象的数量,它的默认值为10000。index.mapping.total_fields.limit限制了Elasticsearch中字段的数量,它的默认值为1000。index.mapping.depth.limit限制了Elasticsearch中mapping的嵌套深度,它的默认值为20。在实际使用中,根据业务需求可以适当调整这些限制值以获得更好的性能。


### Nested Object vs Parent/Child

建议在mapping设计阶段尽量避免使用nested或parent/child的字段,因为这些查询性能较差,能不用就不用。如果必须使用nestedfields,要保证nestedfields字段不能过多,因为针对1个document,每一个nestedfield,都会生成一个独立的document,这将使doc数量剧增,影响查询效率,尤其是JOIN的效率。


默认Elasticsearch的限制是每个索引最多有50个nestedfields,如果需要增加或减少nestedfields的数量,可以修改配置文件中的index.mapping.nested_fields.limit参数。对于常规的文档存储和查询,使用NestedObject可以保证文档存储在一起,因此读取性能高;相反,对于需要独立更新父文档或子文档的情况下,可以使用Parent/Child结构,这样可以保证父子文档可以独立更新,互不影响;但是为了维护join关系,需要占用部分内存,读取性能较差。因此,在选择使用NestedObject还是Parent/Child结构时,需要根据具体的场景进行选择,子文档偶尔更新且查询频繁时可以选择NestedObject,而子文档更新频繁时可以选择Parent/Child结构。


### 静态映射

为确保集群的稳定性,在使用Elasticsearch的过程中,推荐选择静态映射方式。静态映射不仅能够保证数据类型的一致性,还能够提高查询效率。相反,如果使用动态映射,可能会导致集群崩溃,并带来不可控制的数据类型,从而影响业务的正常运行。此外,在Elasticsearch中,数据的存储类型分为匹配字段和特征字段两种。匹配字段用于建立倒排索引以进行query匹配,而特征字段如ctr、点击数、评论数等则用于粗排。因此,在设计索引时需要根据不同的功能选择不同类型的字段进行建立倒排索引,以满足业务的需求和提高查询效率。综上所述,静态映射是建立Elasticsearch稳定、高效的必要条件,而动态映射的使用应在必需时加以限制。


### document 模型设计

MySQL经常需要进行一些复杂的关联查询,但是在Elasticsearch中并不推荐使用复杂的关联查询,因为一旦使用会影响性能。因此,最好的做法是在Java系统中先完成关联,将关联好的数据直接写入Elasticsearch中。这样,在搜索时,就不需要使用Elasticsearch的搜索语法来完成关联搜索。


在设计document模型时,需要非常重视,因为在搜索时执行复杂的操作会影响性能。Elasticsearch支持的操作有限,因此需要避免考虑使用Elasticsearch进行一些难以操作的事情。如果确实需要使用某些操作,最好是在document模型设计时就完成。此外,需要尽量避免使用复杂操作,例如join/nested/parent-child搜索,因为它们的性能都很差。


## 集群架构设计

为了提高Elasticsearch服务的整体可用性,需要合理的部署集群架构。Elasticsearch集群采用主节点、数据节点和协调节点分离的架构,即将主节点和数据节点分开布置,同时引入协调节点,以实现负载均衡。在5.x版本以后,数据节点还可进一步细分为Hot-Warm的架构模式。


### 主节点和数据节点

在Elasticsearch配置文件中,有两个非常重要的参数,分别是node.master和node.data。这两个参数配合使用,可以提高服务器的性能。主节点配置node.master:true和node.data:false,表示该节点只作为一个主节点,不存储任何索引数据。推荐每个集群运行3个专用的Master节点,以提高集群的弹性。在使用时,还需设置discovery.zen.minimum_master_nodes参数为2,以避免脑裂的情况。因为三个主节点仅负责集群的管理,不包含数据、不进行搜索和索引操作,因此它们的CPU、内存和磁盘配置可以比数据节点少很多。


数据节点配置node.master:false和node.data:true,只作为一个数据节点,专门用于存储索引数据,实现功能单一,降低资源消耗率。Hot-Warm架构是将数据节点分成热节点和暖节点,热节点只保存最新的数据,暖节点则保存旧的数据,以实现不同数据的不同存储需求。


引入协调节点可以实现负载均衡,减少节点间的通信压力,提升服务的整体性能。在协调节点上可以运行诸如Kibana、Logstash、Beats等工具,以进行数据可视化、数据采集等操作。在配置协调节点时,还需设置discovery.zen.minimum_master_nodes参数为2,避免脑裂的情况。


总之,合理的部署Elasticsearch集群架构,可以提高服务的整体可用性,减少节点间的通信负担,降低资源消耗率,优化服务的整体性能。


### hot节点和warm节点

hot节点主要是用于存储索引数据并保存最近频繁被查询的索引。由于索引是一项CPU和IO密集型操作,因此建议使用SSD磁盘类型来保持高性能的写入操作。同时,为了保证高可用性,建议至少部署3个最小化的hot节点。如果需要增加性能,可以增加服务器数量。要将节点设置为hot类型,elasticsearch.yml应该包含以下配置:node.attr.box_type:hot。对于针对指定索引操作,可以通过设置index.routing.allocation.require.box_type:hot使其将索引写入hot节点。


warm节点主要是处理大量不经常访问的只读索引的设计。由于这些索引是只读的,因此warm节点倾向于挂载大量普通磁盘来替代SSD。内存和CPU的配置应该与hot节点保持一致,节点数量一般也应该大于或等于3。要将节点设置为warm类型,elasticsearch.yml应该包含以下配置:node.attr.box_type:warm。同时,也可以在elasticsearch.yml中设置index.codec:best_compression以保证warm节点的压缩配置。


当索引不再频繁查询时,可以使用index.routing.allocation.require.box_type:warm将索引标记为warm,从而确保索引不写入hot节点,以便将SSD磁盘资源用于处理更为关键的操作。一旦设置了该属性,Elasticsearch会自动将索引合并到warm节点。


### 协调节点

协调(coordinating)节点是分布式系统中的一个节点,用于协调多个分片或节点返回的数据,进行整合后返回给客户端。该节点不会被选作主节点,也不会存储任何索引数据。在Elasticsearch集群中,所有的节点都有可能成为协调节点,但可以通过设置node.master、node.data、node.ingest都为false来专门设置协调节点。


协调节点需要具备较好的CPU和较高的内存。在查询时,通常涉及从多个node服务器上查询数据,并将请求分发到多个指定的node服务器,对各个node服务器返回的结果进行汇总处理,最终返回给客户端。因此,协调节点在查询负载均衡方面发挥了重要的作用。除此之外,可以通过设置node.master和node.data的值来特别指定节点的功能类型,如:node.master:false和node.data:true,该节点仅用于数据存储和查询,node.master:true和node.data:false,该节点仅用于协调请求等。设置节点的功能类型可以使其功能更加单一,从而降低其资源消耗率,提高集群的性能。


### 关闭data节点服务器中的http功能

在Elasticsearch集群中,关闭data节点服务器中的http功能是一种有效的保障数据安全和提升服务性能的方法。具体实现方式是对所有的数据节点进行http.enabled:false的配置参数设置,同时不安装head、bigdesk、marvel等监控插件,这样data节点服务器只需处理索引数据的创建、更新、删除和查询等操作。而http服务可以在非数据节点服务器上开启,相关监控插件也可以安装到这些服务器上,用于监控Elasticsearch集群的状态和数据信息。通过这种方法,可以在保证数据安全的前提下,提升Elasticsearch集群的服务性能。


### 一台服务器上只部署一个node

在一台物理服务器上,可以通过设置不同的启动端口来启动多个node服务器节点。然而,由于服务器的CPU、内存、硬盘等资源是有限的,当在同一台服务器上启动多个node节点时,会导致资源的竞争和争夺,从而影响服务器性能。因此,建议在进行服务器节点部署时,将不同的node节点部署在不同的物理服务器上,从而实现资源的充分利用,提高服务器性能和可靠性。同时,为了确保多个node节点之间的通信和协调,可以使用负载均衡器等技术手段来实现节点间的负载均衡和故障转移,从而保障应用程序的稳定运行。


### 集群分片设置

在Elasticsearch中,一旦创建好索引后,就不能再调整分片的设置。由于一个分片对应于一个Lucene索引,而Lucene索引的读写会占用大量的系统资源,因此分片数不能设置过大。因此,在创建索引时,合理配置分片数是非常重要的。一般来说,应当遵循以下原则:


(1)控制每个分片占用的硬盘容量不超过Elasticsearch的最大JVM堆空间设置,通常不应超过32GB。因此,如果索引的总容量在500GB左右,分片大小应在16个左右。当然,最好同时考虑原则2。


(2)考虑节点数量。通常情况下,每个节点对应一台物理机。如果分片数过多,即大大超过了节点数,很可能会导致在某个节点上存在多个分片。一旦该节点发生故障,即使保持了一个以上的副本,仍然有可能导致数据丢失,从而无法恢复整个集群。因此,一般都设置分片数不超过节点数的三倍。


## 可用性优化

在可用性方面,Elasticsearch原生版本存在三个问题:


(1)系统健壮性不足:系统在面对意外情况时无法保持正常运行,具体表现为系统容易导致集群雪崩和节点OOM。这种情况在大流量的情况下尤为明显,此时系统的负载会变得非常高,容易导致节点内存耗尽,甚至导致集群崩溃。造成这种情况的主要原因是内存资源不足和负载不均。具体来说,内存资源不足可能是由于系统中存在内存泄漏或者内存管理不当等引起的。而负载不均则可能是由于集群中的节点在处理任务时,存在一些节点负载过高,而另一些节点负载过轻的情况。为了解决这个问题,需要采取一些优化措施,以提升系统的健壮性和稳定性。具体来说,可以优化服务限流和节点均衡策略。限流策略的作用是控制系统的访问量,防止系统因为大量请求而导致崩溃或者响应变得异常缓慢。节点均衡策略则是通过对任务进行分配,以使得每个节点的负载均衡,避免一些节点负载过高,而另一些节点负载过轻的情况。这些优化措施可以有效增强系统的健壮性,使得系统更加稳定可靠。


(2)容灾方案欠缺:尽管Elasticsearch自身提供了副本机制,以确保数据的安全性,但是对于涉及多个可用区的容灾策略,需要云平台额外实现。此外,即使在存在副本机制和跨集群复制的情况下,仍然需要提供低成本的备份回滚能力,以应对可能存在的误操作和数据删除的风险。针对这些问题,建议采取以下措施:首先,对于多可用区容灾方案的实现,可以考虑采用云平台提供的跨可用区副本和快照备份功能,以确保数据可靠性和可用性。其次,可以部署多个集群,实现数据的跨集群复制,以进一步提高数据的安全保障。同时,需要在数据备份和回滚方面做好充分的准备,确保在发生误操作或数据删除时能够迅速恢复数据。


(3)内核Bug:Elasticsearch是一种开源搜索引擎,其内核存在一些Bug,可能会影响其可用性。为了解决这些问题,Elasticsearch修复了一系列与内核可用性相关的问题,包括Master任务堵塞、分布式死锁、滚动重启速度慢等问题。此外,为了确保用户能够及时获得修复后的版本,Elasticsearch及时提供了新版本给用户升级。这些措施充分展示了Elasticsearch对用户可用性的关注,并且以负责任的方式解决问题。这些改进将有助于提升Elasticsearch的性能和可靠性,进一步满足用户在搜索领域的需求。


接下来,将针对用户在可用性层面常遇到的两类问题展开分析:


(1)当高并发请求过多时,会导致集群崩溃的问题。为了解决这个问题,可以采用一些方法来提升集群的吞吐能力。其中,可以优化集群的配置,例如增加硬件资源、提升网络带宽、调整线程池大小等。另外,可以采用异步I/O方式来提高请求的处理效率,从而缓解集群压力。此外,负载均衡技术也是一个值得推荐的方法,通过将请求分配到不同的节点上,可以避免某些节点过载而导致集群崩溃的情况发生。总之,需要采取多种方法综合应对高并发请求的问题,从而提升集群的稳定性和吞吐能力。


(2)当进行单个大查询时,很容易出现节点因负载过大而崩溃的情况。为了解决这个问题,可以采用一些优化措施。首先是数据分片和副本,这可以将数据分散到多个节点上,减少单个节点的负载,同时保证数据的可靠性和高可用性。其次是搜索建议,它可以根据用户输入的关键词提供相关的搜索建议,减少用户不必要的查询请求。最后是聚合结果优化,它可以对查询结果进行聚合,减少不必要的数据传输和计算,提高查询效率和稳定性。通过这些优化措施,可以有效地减轻单个查询对节点的负载,提升系统的查询效率和稳定性,达到更好的用户体验和服务质量。


## 高并发请求压垮集群

高并发请求是一种常见的场景,可能会导致集群崩溃。例如,早期内部的一个日志集群,其中写入量在一天内突然增加了5倍,导致集群中的多个节点的Old GC卡住而脱离集群,集群变成了RED状态,写入操作停止了。这个场景可能会对集群造成很大的损失。对于挂掉的节点,进行内存分析后发现,大部分内存都被反序列化前后的写入请求所占用。这些写入请求是堆积在集群的接入层位置上的。接入层是指用户的写入请求先到达其中一个数据节点,称之为数据节点。然后由该协调节点将请求转发给主分片所在节点进行写入,主分片写入完毕再由主分片转发给从分片写入,最后返回给客户端写入结果。从内存分析结果看,这些堆积的位置导致了节点的崩溃,因此根本原因是协调节点的接入层内存被打爆。


经过问题原因的分析,制定了针对高并发场景下的优化方案。这个方案包括两个关键点:加强对接入层的内存管理和实现服务限流。为了避免集群崩溃,需要确保接入层内存不会被输入请求打爆,因此需要加强内存管理。在实现服务限流的方面,需要一个能够控制并发请求数量,并且能够精准地控制内存资源的方案。这个方案还要具有通用性,能够作用于各个层级实现全链限流。


## 服务限流

一般情况下,数据库的限流策略是从业务端或者独立的代理层配置相关的业务规则,进行资源预估等方式进行限流。但是这种方式适应能力较弱、运维成本高、业务端很难准确地预估资源消耗。原生版本本身也有限流策略,但是单纯地基于请求数的限流不能控制资源使用量,而且只作用于分片级子请求的传输层,对于接入层无法起到有效的保护作用。


因此,优化方案是基于内存资源的漏桶策略。将节点JVM内存作为漏桶的资源,当内存资源足够的时候,请求可以正常处理,当内存使用量到达一定阈值的时候分区间阶梯式平滑限流,处理中的请求和merge操作都可以得到保证,从而保证节点内存的安全性。这个方案不仅可以控制并发数,还可以控制资源使用量并且具有通用性,可以应用于各个层级实现全链限流。


在限流方案中,一个重要的挑战是如何实现平滑限流。采用单一的阈值限流很容易出现请求抖动的情况,例如请求一上来就会立即触发限流,因为内存资源不足,而稍微放开一点请求量又会迅速涌入,使内存资源再次极度紧张。因此,通过设置高低限流阈值区间、基于余弦变换实现请求数和内存资源之间的平滑限流方案。在该区间中,当内存资源足够时,请求通过率达到100%;当内存到达限流区间时,请求通过率逐步下降。而当内存使用量下降时,请求通过率也会逐步上升,而不是一下子放开。经过实际测试,平滑的区间限流能够在高压力下保持稳定的写入性能。平滑限流方案是对原生版本基于请求数漏桶策略的有效补充,作用范围更广泛,能覆盖协调节点、数据节点的接入层和传输层。但需要说明的是,该方案并不会替代原生的限流方案,而是对其进行有效的补充。


## 单个大查询打挂节点

在某些分析场景中,需要进行多层嵌套聚合,这可能导致返回的结果集非常大,因此可能导致某个请求将节点打挂。在这种聚合查询流程中,请求首先到达协调节点,然后被拆分为分片级子查询请求,然后发送给目标分片所在的数据节点进行子聚合。最后,协调节点将所有分片结果收集并进行归并、聚合、排序等操作。然而,这个过程中存在两个主要问题点。


第一个问题是,当协调节点大量汇聚结果并反序列化之后,可能会导致内存膨胀。这可能是由于结果集太大,或者节点内存不足等原因造成的。


第二个问题是,二次聚合可能会产生新的结果集,这可能导致内存爆炸。

为了解决上述单个大查询的问题,可以采用以下五个优化方案。


首先,针对内存开销问题,可以通过增加节点内存大小或将查询结果进行分批处理来优化。这样能够在降低内存使用率的同时,提高查询效率。


接着,针对内存浪费严重的写入场景,优化方案主要是实现弹性的内存buffer,并对于读写异常的请求及时进行内存回收。要注意,这里所提到的内存回收策略并不是指GC策略。JVMGC债务管理主要评估JVMOldGC时长和正常工作时长的比例来衡量JVM的健康情况,特殊情况下会重启JVM以防止长时间hang死,这与内存回收策略是两个不同的方面。通过内存利用率优化,整个公有云的Elasticsearch集群的可用性得到了提升,达到了4个9。内存利用率提升了30%,在高压力场景下节点稳定性更强,基本能保证节点不会OOM,集群也不会雪崩。总的来说,内存利用率,内存回收策略以及JVMGC债务管理都是优化内存利用率的重要方面,它们能够提高系统吞吐量,减少节点OOM的发生,保障系统的稳定性和可用性。通过对这些方面的优化,能够有效地提高系统的性能表现,使系统更加健康和稳定。


其次,在进行聚合操作时,需要特别注意减少中间结果的存储和传输。对于大规模数据集的查询,优先考虑使用分布式计算框架,如Apache Spark等。


然后,在数据库管理中,单个查询内存限制是一个非常有用的功能。当一个查询过于庞大时,其会占用大量的内存资源,从而影响其他所有请求的响应时间。通过设置单个查询内存限制,可以有效地控制查询的内存使用量,从而保证整个数据库系统的正常运行。除此之外,滚动重启速度优化也是一个非常实用的功能。尤其是在大规模集群环境下,单个节点的重启时间往往较长,如果要重启整个集群,可能会导致整个系统长时间处于不可用状态。通过优化滚动重启速度,可以将单个节点的重启时间从10分钟降至1分钟以内,大幅缩短了重启时间,从而提高了系统的可用性。值得一提的是,这个优化已经在7.5版本中被合并了,因此用户不需要再自己手动进行配置。如果遇到大集群滚动重启效率问题,可以关注此功能,以提高数据库系统的可靠性和稳定性。


最后,第三个优化方案的重点是内存膨胀预估加流式检查。该方案主要分为两个阶段:第一阶段在协调节点接收数据节点返回的响应结果反序列化之前做内存膨胀预估,并在内存使用量超过阈值时直接熔断请求;第二阶段在协调节点reduce过程中,流式检查桶数,每增加固定数量的桶检查一次内存,如果超限则直接熔断。这样用户不再需要关心最大桶数,只要内存足够就能最大化地满足业务需求。不足之处是大请求还是被拒掉了,但是可以通过官方已有的batch reduce的方式缓解,即每收到部分子结果就先做一次聚合,这样能降低单次聚合的内存开销。该方案已经提交给官方并合并了,将在最近的7.7.0版本中发布。


## 性能优化

性能优化的场景可以分为写入和查询两个部分。在写入方面,主要包括海量时序数据场景,如日志和监控,通常能够实现千万级别的吞吐。带有id的写入会导致性能衰减一倍,因为需要首先查询记录是否存在。在查询方面,主要包括搜索场景和分析场景。搜索服务需要高并发并且具有低延迟;而聚合分析主要涉及大型查询,需要大量的内存和CPU开销。


从性能影响面的角度来看,硬件资源和系统调优通常是直接可控的,例如资源不足时可以进行扩容,调整参数深度来进行调优等。然而,存储模型和执行计划通常涉及内核优化,因此普通用户难以直接进行调整。接下来,将重点介绍存储模型和执行计划的优化。


存储模型的优化是一个关键问题。Elasticsearch底层Lucene基于LSM Tree的数据文件。原生默认的合并策略是按文件大小相似性合并,一次性固定合并10个文件,采用近似分层合并。这种合并方式最大的优点是效率高,可以快速降低文件数。但是,文件不连续会导致查询时的文件裁剪能力较弱,例如查询最近1小时的数据,有可能会将1小时的文件拆分到几天前的文件中,进而增加了必须检索的文件数量。业界通常采用解决数据连续性的合并策略,例如基于时间窗口的合并策略,如以Cassandra、HBase为代表的策略。其优点在于数据按时间序合并,查询效率高,还可以支持表内TTL。缺点是仅适用于时序场景,并且文件大小可能不一致,从而影响合并效率。另一类策略由LevelDB、RocksDB为代表的分层合并策略构成,一层一组有序,每次抽取部分数据向下层合并,优点在于查询高效。但如果相同的数据被合并多次,这将影响写入吞吐。


最后是优化合并策略,其目标是提高数据连续性、收敛文件数量,提升文件裁剪的能力以提高查询性能。实现策略是按时间序分层合并,每层文件按创建时间排序。除了第一层外,所有层次都按照时间序和目标大小进行合并,而不是固定每次合并文件数量,保证了合并效率。对于少量未合并的文件和冷分片文件,采用持续合并策略,将超过默认5分钟不再写入的分片进行持续合并,并控制合并并发和范围,以降低合并成本。


## 执行引擎的优化

在Elasticsearch中,有一种聚合叫做Composite聚合,它支持多字段的嵌套聚合,类似于MySQL的group by多个字段,同时也支持流式聚合,即以翻页的形式分批聚合结果。使用Composite聚合时,只需要在查询时聚合操作下面指定composite关键字,并指定一次翻页的长度和group by的字段列表,每次拿到的聚合结果会伴随着一个after key返回,下一次查询可以拿着这个after key查询下一页的结果。


Composite聚合的实现原理是利用一个固定size的大顶堆,size就是翻页的长度,全量遍历一把所有文档迭代构建这个基于大顶堆的聚合结果,最后返回这个大顶堆并将堆顶作为after key。第二次聚合时,同样的全量遍历一把文档,但会加上过滤条件排除不符合after key的文档。然而,这种实现方式存在性能问题,因为每次拉取结果都需要全量遍历一遍所有文档,并未实现真正的翻页。

为了解决这个问题,提出了一种优化方案,即利用index sorting实现after key跳转以及提前结束(earlytermination)。通过index sorting,可以实现数据的有序性,从而实现真正的流式聚合,大顶堆仍然保留,只需要按照文档的顺序提取指定size的文档数即可快速返回。下一次聚合时,可以直接根据请求携带的afterkey做跳转,直接跳转到指定位置继续向后遍历指定size的文档数即可返回。这种优化方案可以避免每次翻页全量遍历,大幅提升查询性能。


在Elasticsearch7.6版本中,已经实现了覆盖数据顺序和请求顺序不一致的优化场景。该版本在性能层面进行了全面优化,从底层的存储模型、执行引擎、优化器到上层的缓存策略都有相应的提升。具体来说,该版本在存储模型方面采用了更加高效的数据结构和算法,以减少磁盘I/O、内存消耗等问题,提高读写性能。在执行引擎方面,优化了查询和聚合操作的执行过程,使其能快速响应请求并返回数据。而在优化器方面,针对不同的查询场景进行了优化,以减少计算量,提高查询效率。最后,在缓存策略方面,采用了更加智能的缓存机制,以加速常用请求的响应速度。以上这些优化措施的整合,实现了对覆盖数据顺序和请求顺序不一致的场景的优化,并带来了更高效、更可靠的Elasticsearch体验。


## 成本优化

在大规模数据场景下,优化成本是一个非常重要的问题。在此过程中,需要重点关注集群的 CPU、内存和磁盘三个方面。根据实际情况,这三个方面的成本占比一般为1比4比8。也就是说,磁盘和内存成本占比相对较高,需要着重考虑。举个例子,一般的16核64GB,2-5TB磁盘节点的成本占比也大致如此。因此,在成本优化过程中,主要的瓶颈就在于磁盘和内存的使用。在实际操作中,可通过对磁盘和内存的使用进行优化,以降低成本,提高效率。

成本优化的主要目标是存储成本和内存成本。


### 存储成本优化

Elasticsearch单个集群能够处理千万级别的写入操作,但实现千万每秒的写入量需要考虑多方面因素,如硬件配置、索引设计、数据量和查询复杂度等,而业务需要保留至少半年的数据供查询。

假设单集群平均写入速度为1000万OPS,意味着在半年的时间内,共有`60 * 60 * 24 * 180 = 15552000`秒。每个文档大小为50Byte,且基于高可用需要2个副本,因此计算公式为:`1000万(OPS) * 86400(秒) * 180(天) * 50Byte(平均文档大小) * 2(副本)`等于14PB。即此集群需要14PB的存储空间。假设每台物理机的内存和硬盘都能够完全用于Elasticsearch存储,那么:1PB = 1024TB,14PB = 14 * 1024 = 14336TB。然而,一个物理机可以存储多少数据,取决于该数据的复杂度、索引方式、查询频率等因素。假设平均每台物理机可以存储800GB数据,则此集群需要的物理机数量为:14336TB ÷ 0.8TB/台 = 17920台。这么多的物理机数量的成本远远超出了业务成本预算。因此,需要采用其他方式,在不牺牲性能的情况下减少存储需求,以适应业务预算。


为了提高对Elasticsearch系统的效率和成本效益,可以采取多种优化措施。


首先,可以通过调研业务数据访问频率,将历史数据进行冷热分离,将冷数据放入HDD中来降低存储成本。同时,索引生命周期管理可用于数据搬迁,将冷数据盘利用多盘策略提高吞吐和数据容灾能力。此外,超冷数据可以通过冷备到腾讯云对象存储COS中来降低成本。


其次,通过分析数据访问特征,可以采用Rollup方案降低历史数据的精度并降低存储成本。Rollup方案利用预计算来释放原始细粒度数据,例如将秒级数据聚合成小时级和天级,以方便展示跨度较长的跨度报表。Rollup方案还显著降低存储成本和提高查询性能。Rollup优化方案主要基于流式聚合加查询剪枝结合分片级并发来实现高效性。分片级并发可以通过添加routing来实现,让相同的对象落到相同的分片内,并能实现分片级并发。


此外,通过对Rollup任务资源预估,并感知集群的负载压力来自动控制并发度,从而避免对集群整体的影响。综上所述,通过冷热分离、索引生命周期管理、多盘策略、对象存储和Rollup方案等手段,可以从架构层对Elasticsearch进行优化,实现同时满足业务需求和成本效益。


### 内存成本优化

目前,很多情况下堆内存使用率过高,而磁盘使用率相对较低。FST是一种倒排索引,它通常常驻内存且占用较大的内存比例。为了节省内存空间并保持快速访问,FST使用了自适应前缀编码技术,但这也导致了在查询时需要解压缩FST,从而占用大量的堆内存空间。因此,将FST移至堆外(off-heap)并按需加载FST可以显著提高堆内内存利用率并降低垃圾回收开销,从而提高单个节点对磁盘的管理能力。


具体来说,在每10TB的磁盘中,FST需要10GB到15GB的内存来存储索引。为了减小内存占用,可以将FST从堆内存中移至堆外,这种方式可以单独管理并减轻对JVM垃圾回收的影响。除此之外,这种优化方案还可以显著降低堆内内存中FST的占用比例,提高堆内内存利用率,并降低GC开销。同时,使用off-heap内存还可以降低GC的次数和持续时间,从而提高整个系统的性能和稳定性。


因此,需要在使用FST索引时考虑内存占用的问题,并将FST移至堆外并按需加载FST,以提高系统的性能和稳定性。这个优化方案可以显著提高堆内内存利用率,降低GC开销,并提升单个节点管理磁盘的能力。

原生版本实现 off-heap 的方式,将 FST 对象放到 MMAP 中管理。虽然这种方式实现简单,但是有可能会被系统回收掉,导致读盘操作,进而带来性能的损耗。HBase 2.0 版本中 off-heap 的实现方式,则是在堆外建立了 cache,但是索引仍然在堆内,而且淘汰策略完全依赖于 LRU 策略,冷数据不能及时清理。而在堆外建立 cache,可以保证 FST 的空间不受系统影响,实现更精准的淘汰策略,提高内存使用率,同时采用多级 cache 的管理模式来提升性能。虽然这种方式实现起来比较复杂,但是收益很明显。因此,读者可以根据实际需求选择合适的 off-heap 方案。


为了优化访问FST的效率,可以考虑采用一种综合方案,即LRUcache+零拷贝+两级cache。在这种方案中,LRUcache被建立在堆外,并且当堆内需要访问FST时,会从磁盘加载到LRUcache中。由于Lucene默认的访问FST的方式使用一个堆内的buffer,直接从堆外拷贝到堆内的buffer会占用大量的时间和资源。因此,对Lucene访问FST的方式进行了改造,将buffer不直接存放FST,而是存放指向堆外对象的指针,这样就实现了堆内和堆外之间的零拷贝。


需要注意的是,这里的零拷贝和操作系统中的用户态和内核态的零拷贝是两个不同的概念。但是,根据key去查找堆外对象的过程也会损耗一部分性能,例如计算hash、数据校验等。为了进一步优化性能,可以利用Java的弱引用建立第二层轻量级缓存。弱引用指向堆外的地址,只要有请求使用,这个key就不会被回收,可以重复利用而无需重新获取。一旦不再使用,这个key就会被GC回收掉,并回收掉堆外对象指针。但是,堆外对象指针回收之后需要清理堆外内存,不能浪费一部分内存空间。为了解决这个问题,最好的办法是在堆内对象地址回收的时候直接回收堆外对象。然而,Java没有析构的概念,可以利用弱引用的ReferenceQueue,在对象要被GC回收时将对象指向的堆外内存清理掉,这样就可以完美解决堆外内存析构的问题,并提高内存利用率。

为了帮助读者更好地理解上述内容,本节将采用以下故事的形式对上述内容进行再次解析:


曾经有一个叫做小明的程序员,他在开发一个搜索引擎的项目中遇到了一个问题:搜索引擎的堆内存占用率过高,而磁盘的使用率却相对较低。他研究了一番后发现,这是由于搜索引擎中使用的倒排索引(FST)需要很大的内存比例才能常驻内存,而在查询时需要解压缩FST,占用了大量的堆内存空间。


小明急于解决这个问题,因此他开始了一段冒险之旅。他发现,将FST移至堆外并按需加载FST可以显著提高堆内内存利用率并降低垃圾回收开销,从而提高单个节点对磁盘的管理能力。他做了一些实验后,发现将FST放到MMAP中管理的方式实现简单,但是有可能会被系统回收掉,导致读盘操作,进而带来性能的损耗。而在堆外建立cache,可以保证FST的空间不受系统影响,实现更精准的淘汰策略,提高内存使用率,同时采用多级cache的管理模式来提升性能。


在这个过程中,小明还研究了一种综合方案:LRUcache+零拷贝+两级cache。他把LRUcache建立在堆外,并且当堆内需要访问FST时,会从磁盘加载到LRUcache中。同时,他利用Java的弱引用建立第二层轻量级缓存,指向堆外的地址,只要有请求使用,这个key就不会被回收,可以重复利用而无需重新获取。一旦不再使用,这个key就会被GC回收掉,并回收掉堆外对象指针。最后,小明成功地实现了从堆内移动FST到堆外的优化方案,并大幅提高了系统的性能和稳定性。他心满意足地把这个方案分享给了同事们,为搜索引擎的开发作出了重大贡献。


## 扩展性优化

Elasticsearch中的元数据管理模型是由Master节点来管理元数据,并同步给其他节点。以建索引流程为例,首先Master节点会分配分片,并产生差异的元数据,这些元数据会发送到其他节点上。当大多数Master节点返回元数据后,Master节点会发送元数据应用请求,其他节点开始应用元数据,并根据新旧元数据推导出各自节点的分片创建任务。在这个过程中有一些瓶颈点,主要有以下几点:


首先,Mater节点在分配分片时需要进行正反向转换。由于路由信息是由分片到节点的映射,而在做分片分配时需要节点到分片的映射,因此需要知道每个节点上的分片分布。因此,分片分配完毕后还需要将节点到分片的映射转换回来。这个转换过程涉及多次的全量遍历,这在大规模分片的情况下会存在性能瓶颈。其次,在每次索引创建的过程中,会涉及多次元数据同步。在大规模的节点数场景下,会出现同步瓶颈,由于节点数量过多,可能会出现某些节点一些网络抖动或Old GC等问题,导致同步失败。为了解决以上问题,可以从三个方面进行优化:首先,采用任务下发的方式,定向下发分片创建任务,避免了多次全节点元数据同步,从而优化分片创建导致的元数据同步瓶颈。其次,针对分配分片过程中多次正反向遍历的问题,采用增量化的数据结构维护的方式,避免了全量的遍历,从而优化分配分片的性能问题。最后,为了优化统计接口的性能,采用缓存策略避免多次重复的统计计算,大幅降低资源开销。


为了帮助读者更好地理解上述内容,本节将采用以下故事的形式对上述内容进行再次解析:


在一个遥远的星系中,有一个由许多机器人组成的世界。这些机器人可以相互通信,进行各种任务。其中,有一个机器人被选为主节点,负责管理所有机器人的信息。这个主节点的职责很重要,因为它需要协调所有机器人的工作,尤其是在建索引流程中。每当有任务需要建立索引时,主节点会分配分片,并产生差异的元数据,这些元数据会发送到其他机器人上。但是,这个过程并不总是顺利的。主节点需要在分配分片时进行正反向转换,这样每个机器人才能知道自己需要建立哪些索引。这个转换过程需要多次的全量遍历,如果分片数量很多,这个过程会非常耗时。另外,每次索引创建的过程中,也会涉及多次元数据同步。而在大规模的机器人场景下,可能会出现同步瓶颈,导致同步失败。


为了解决这些问题,机器人们开始探索各种优化方案。首先尝试了任务下发的方式,定向下发分片创建任务,避免了多次全节点元数据同步,从而优化了分片创建导致的元数据同步瓶颈。接着,采用增量化的数据结构维护的方式,避免了全量的遍历,从而优化了分配分片的性能问题。最后,采用了缓存策略来优化统计接口的性能,避免多次重复的统计计算,大幅降低了资源开销。这些优化措施让机器人们的工作更加顺利,他们可以更快地完成任务,并且不再遇到瓶颈问题。同时,这些措施也为未来的工作提供了一个优化的思路,让机器人们可以不断地改进和进步。


## 分析性能问题

分析性能问题的路径可以遵循以下步骤:首先,明确性能问题后,进行流量录制,以获取一个用于后续基准压测的测试集合。随后,使用相关的性能分析工具,首先明确是否存在CPU热点或IO问题。对于Java技术栈,可以使用Scaple、JProfiler、Java Flight Recorder、Async Profiler、Arthas、perf等工具进行性能分析。


利用火焰图进行分析,配合源代码进行数据分析和验证。此外,在Elasticsearch中,也可以使用Kibana的Search Profiler协助定位问题。接下来,进行录制大量的流量,抽样分析后,以场景为例,通过Profiler分析,发现TermInSetQuery占用了一半以上的耗时。明确问题后,从索引和检索链路两侧进行分析,评估问题并设计多种解决方案,并利用Java Microbenchmark Harness(JMH)代码基准测试工具验证解决方案的有效性。最后,集成验证解决方案的最终效果。


# 系统架构调优

## 代码层面


Java系统调优是一个非常复杂的过程,它涵盖了很多方面,包括代码层面、系统层面、运行环境层面等等。在这篇文章中,我们将主要关注Java代码层面的调优方法。

### 内存管理

内存管理是 Java 系统架构调优中关键的一个方面。Java 中的垃圾收集器是自动管理内存的,但是如果我们不合理地使用对象,也会导致内存占用过高。例如,如果一个对象不再使用但是没有被及时清理,就会导致内存泄露。


理解 Java 内存模型以及不同的 GC 算法对于优化内存管理非常重要。我们可以使用代码分析工具来分析内存占用情况,及时发现内存泄漏和内存占用高的问题,并针对性地进行优化。

#### 控制对象创建


Java的垃圾回收机制负责在程序运行时自动回收不再使用的对象,但是对象的创建和销毁过程也会对系统的性能产生影响。因此,当我们编写Java程序时应该尽可能地减少对象的创建。


具体的方式包括:


- 使用字符串连接代替字符串拼接


字符串拼接是一种常见的处理方式,但是它每次都会创建新的字符串对象,并且可能导致整个程序变慢。可以使用StringBuilder或StringBuffer类来替换字符串连接操作,从而避免对象的重复创建。


- 使用静态工厂方法


在创建对象时可以使用静态工厂方法来避免对象的重复创建,例如:


```java

public static Integer valueOf(int i) {

   if (i >= IntegerCache.low && i <= IntegerCache.high)

       return IntegerCache.cache[i + (-IntegerCache.low)];

   return new Integer(i);

}

```


- 使用享元模式


享元模式是一种将对象重用的技术,它可以避免对象的重复创建。享元模式可以应用于需要频繁创建的对象,例如字符串和数字类型。


#### 优化循环


循环是Java程序中常见的操作,因此优化循环可以帮助提高程序的性能。以下是一些循环优化的方法:


- 循环条件的优化


循环条件决定了循环的执行次数,优化循环条件可以减少循环的次数,从而提高程序的性能。例如,可以使用逆序循环来遍历数组。


- 循环体的优化


循环体是循环中实际执行的代码块,它的优化可以提高程序的性能。可以使用位运算代替乘法和除法,使用增量运算代替赋值操作等。


- 循环类型的优化


Java中有三种类型的循环:for、while和do-while,可以根据实际情况选择最合适的循环类型。一般而言,for循环在循环次数固定的情况下效率最高,while循环适用于循环次数不固定的情况,do-while循环适用于循环体至少要执行一次的情况。


#### 使用正则表达式


正则表达式是一种强大的文本匹配工具,但是它的匹配过程也会影响程序的性能。因此,我们应该尽可能地优化正则表达式。


具体的方式包括:


- 编译正则表达式


可以使用Pattern.compile()方法来预编译正则表达式,从而减少正则表达式的匹配时间。


- 简化正则表达式


可以使用简化的正则表达式来优化匹配速度。例如,可以使用[^a]代替[a-zA-Z0-9],从而避免使用多个字符的匹配。


- 使用非贪婪模式


贪婪模式是正则表达式默认的匹配模式,它会尽可能地匹配最长的字符串。但是,非贪婪模式会匹配最短的字符串,从而更加高效。


#### 优化异常处理


异常处理是Java程序中常见的操作,但是异常处理也会对程序的性能产生影响。因此,我们应该尽可能地优化异常处理。


异常处理也是代码层面进行架构调优的一个方向。异常处理需要遵守 SOLID 原则中的单一职责原则,即每个方法只需要处理自己的异常,不应该处理其他方法或外界的异常。这样可以降低耦合性,提高代码的可读性和可维护性。


此外,我们还可以针对不同的异常类型进行不同的处理。例如,对于业务异常,可以使用自定义异常来进行处理,避免出现大量的 if-else 判断。对于系统异常,需要及时记录日志并尽可能地将异常信息传递给用户。



具体的方式包括:


- 避免异常的出现


可以通过检查变量的值或使用条件语句来避免异常的出现。


- 细化异常类型


可以使用具体的异常类型来代替通用的Exception类型,从而提高程序的可读性和效率。


- 避免异常处理嵌套


如果在异常处理中嵌套其他异常处理,程序会变得复杂并且会对性能产生影响。因此,应该尽可能地避免异常处理的嵌套。


#### 优化递归


递归是Java程序中常见的操作,但是递归也会对程序的性能产生影响。因此,我们应该尽可能地优化递归。


具体的方式包括:


- 尾递归优化


尾递归是指递归调用出现在函数的最后一步,可以使用尾递归优化来避免递归栈的溢出,从而提高程序的性能。


- 循环优化


可以使用循环代替递归,从而避免递归栈的溢出,提高程序的性能。


- 其他优化


可以使用缓存或动态规划等方法来优化递归,从而减少重复的计算过程。


Java 系统架构调优是一个非常广泛的领域,包括代码层面和系统层面等多方面。本篇文章主要关注在代码层面进行架构调优的几个方向。目前最佳的架构方法是将各个模块分开,遵守 SOLID 原则(单一职责原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则),并且依据具体情况进行调整,以提高系统性能和可维护性。


### 优化算法和数据结构


优化算法和数据结构是代码层面进行架构调优的重要方向之一。在 Java 中,我们可以利用各种容器如 ArrayList、LinkedList、HashMap 等,使用不同的算法和数据结构来避免性能瓶颈。例如,对于需要高效访问元素的情况可以使用 ArrayList,对于需要频繁添加和删除元素的情况可以使用 LinkedList;对于需要快速查找键值对的情况可以使用 HashMap,对于有序集合可以使用 TreeMap 等。


另外,算法的优化也是很重要的。例如,我们可以使用双指针法、二分法等优化查找操作。在代码实现时,需要考虑使用最少的时间和空间复杂度。


### 多线程


多线程是 Java 系统架构中的重要话题,它可以极大地提高系统性能。但是多线程也会带来一系列问题,如线程安全、死锁等。因此,对多线程的优化需要注意以下几点:


- 避免使用过多的锁,使用锁的粒度需要控制在最小范围内,以避免锁竞争导致的性能下降。

- 使用线程安全的容器,例如 ConcurrentHashMap、CopyOnWriteArrayList 等。

- 避免使用过多的线程,线程数量需要根据实际情况进行调整。

- 合理使用线程池,通过线程池来控制线程的生命周期和数量。

- 使用 volatile、synchronized 等关键字来保证线程安全。


### 接口设计


接口设计也是进行架构调优的一个重要方向。好的接口设计可以提高代码的可读性和可维护性,同时检查接口的合理性并及时修正不合理的结构也可以避免一些难以调试的问题。


在接口设计时,可以考虑以下几点:


- 接口设计应遵守 SOLID 原则中的接口隔离原则,即一个接口应该只承担一个用例单元的职责。

- 接口应该是稳定的,一旦发布,就应该保持稳定性。对于接口的修改需要仔细考虑,确保不会对现有系统造成影响。

- 接口应该具有可扩展性。在接口设计时,需要考虑到未来的需求,保证接口能够灵活扩展。


## 设计模式

Java系统架构调优是一项复杂的工作,包含了多个层面,其中设计模式层面是其中之一。设计模式是解决特定问题的通用方案,能够有效提高系统的可复用性、可维护性和可扩展性。下面将介绍几种常见的Java系统架构调优的设计模式。


### 1. 工厂模式


工厂模式是常用的创建型设计模式,它将对象的创建过程封装在工厂类中,客户端只需要向工厂类请求需要的对象即可。在系统架构中,可以使用工厂模式创建需要大量实例化的对象,降低对象的创建和销毁成本,提高系统性能。


### 2. 过滤器模式


过滤器模式是一种结构型设计模式,它可以过滤一组对象,并返回一个过滤后的对象列表。在Java系统架构调优中,过滤器模式可以用于过滤大量的数据,提高查询性能。


### 3. 代理模式


代理模式是一种结构型设计模式,它为对象提供一个代理,以控制对象的访问,可以在不修改原有代码的情况下,增强对象的功能。在Java系统架构调优中,代理模式可以用于实现缓存、延迟加载等功能,提高系统性能。


### 4. 观察者模式


观察者模式是一种行为型设计模式,它定义了对象之间的一种一对多的依赖关系,当一个对象发生改变时,所有依赖它的对象都会收到通知并自动更新。在Java系统架构调优中,观察者模式可以用于实现组件之间的解耦,提高系统的可维护性和可扩展性。


### 5. 单例模式


单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点。在Java系统架构调优中,单例模式可以用于控制系统中实例对象的数量,降低系统的内存使用和开销。


### 6. 享元模式


享元模式是一种结构型设计模式,它通过共享对象来减少内存使用和提高性能。在Java系统架构调优中,享元模式可以用于缓存系统中重复使用的对象,减少系统的内存使用和开销。


### 7. 模板方法模式


模板方法模式是一种行为型设计模式,它定义了一个算法的骨架,并允许子类为一个或多个步骤提供实现。在Java系统架构调优中,模板方法模式可以用于提供一个通用的算法框架,便于不同的业务逻辑实现。


### 8. 策略模式


策略模式是一种行为型设计模式,它定义了一组算法,并将其各自封装起来,使得它们可以相互替换。在Java系统架构调优中,策略模式可以用于提供不同的算法实现,以应对不同的业务场景。


### 9. 职责链模式


职责链模式是一种行为型设计模式,它将请求的发送者和接收者解耦,将多个对象连接起来形成一条链,并沿着这条链传递请求,直到有一个对象处理该请求。在Java系统架构调优中,职责链模式可以用于处理复杂的业务逻辑,减少代码的耦合度和复杂度。


## 前端优化、后端优化、数据库优化、代码优化、架构优化和性能监控

Java系统架构调优涉及的方面非常广泛,包括MVC架构和DDD架构等多个方面。下面将从以下方面进行探讨:


### 1. 前端优化


前端优化主要包括前端代码的优化、页面的加载速度优化以及前端缓存的优化等方面。具体的优化手段包括:


(1)压缩和合并前端资源文件,减少HTTP请求数量,提升页面加载速度;


(2)使用CDN(内容分发网络)来提高前端资源的加载速度;


(3)使用浏览器缓存和本地缓存来减少请求次数,减轻服务器负担;


(4)减少前端代码的冗余和重复,提高前端代码的性能和可读性;


(5)使用响应式设计,使网页能够适配不同的设备。


### 2. 后端优化


后端优化主要是针对服务器的性能进行优化,包括数据库的优化、代码性能优化、服务器资源优化等方面。具体的优化手段包括:


(1)使用缓存技术,减少数据库访问的次数,提升服务器响应速度;


(2)使用连接池技术,减少数据库连接的创建和销毁开销;


(3)优化数据库结构,减少数据冗余和冗余索引,提高数据库查询速度;


(4)使用多线程技术,提升服务器的并发处理能力;


(5)使用负载均衡技术,将请求分配到不同的服务器上,提高服务器的负载均衡能力;


(6)尽量减少网络IO的次数和数据传输的大小,减少服务器的IO开销。


### 3. 数据库优化


数据库优化主要是针对数据库性能进行优化,包括数据库结构的优化、SQL语句的优化、数据库服务器的优化等方面。具体的优化手段包括:


(1)使用索引技术,提高数据库查询速度;


(2)尽量避免全表扫描,优化SQL语句的执行计划;


(3)使用分区表技术,提高数据库的并发处理能力;


(4)尽量避免使用子查询,使用JOIN语句来优化查询性能;


(5)使用数据库缓存技术,减少数据库访问的次数,提升数据库响应速度。


### 4. 代码优化


代码优化主要是针对代码的性能进行优化,包括代码逻辑的优化、算法的优化等方面。具体的优化手段包括:


(1)使用高效的数据结构和算法,提高代码性能和可读性;


(2)尽量避免使用循环,使用迭代和递归来优化代码性能;


(3)使用缓存技术,减少代码计算的次数,提高代码响应速度;


(4)避免使用过多的条件判断语句,使用多态来优化代码性能;


(5)尽量避免使用全局变量和共享对象,使用局部变量和线程安全对象来优化代码性能。


### 5. 架构优化


架构优化主要是针对整个系统架构进行优化,包括模块划分、数据传输的优化、系统安全的优化等方面。具体的优化手段包括:


(1)使用分层架构和模块化设计,提高系统的可扩展性和可维护性;


(2)使用RESTful API设计,提高数据传输的效率和可读性;


(3)使用最小权限原则,提高系统的安全性和可靠性;


(4)使用异步处理技术,提高系统的响应速度和并发能力;


(5)使用微服务架构,提高系统的可伸缩性和可重用性。


### 6. 性能监控


性能监控主要是通过监控系统的各种指标来发现系统的性能瓶颈,从而进行性能优化。具体的监控指标包括:


(1)CPU、内存、网络和磁盘等系统资源的使用情况;


(2)请求和响应的时间、吞吐量和错误率等指标;


(3)数据库的连接数、请求次数和响应时间等指标;


(4)服务器的负载和负载均衡情况等指标。


通过不断的性能监控,可以及时发现系统的性能瓶颈,并及时进行优化,从而提高系统的性能和可靠性。


总之,Java系统架构调优需要从前端优化、后端优化、数据库优化、代码优化、架构优化和性能监控等方面进行优化,不断优化系统性能和可靠性,从而提高用户的满意度和企业的竞争力。


## 传统的MVC和DDD领域驱动

Java系统架构调优是一个复杂的过程,需要在多个层面进行优化,从基础设施、硬件优化到软件架构和代码优化。本篇文章将着重探讨传统的MVC和DDD领域驱动层面的调优方法,为读者提供大致的方向。


### MVC架构调优


MVC(Model-View-Controller)是一种广泛应用的软件架构模式,将应用程序分为三个互相关联的部分:模型(Model)、视图(View)和控制器(Controller)。MVC模式的优点在于它可以使代码更加模块化和易于维护,但同时也会增加系统的复杂度和消耗部分性能。


下面是一些MVC架构调优的建议:


#### 将数据尽可能缓存


由于MVC架构的模型和控制器部分都需要访问数据库,因此数据缓存是非常关键的。将频繁查询的数据进行缓存可以大大提高系统的性能,减轻数据库的负担。


#### 优化数据库查询和操作


除了数据缓存,我们还可以通过优化数据库查询和操作来提高系统的性能。其中一些常见的优化方法包括:使用索引、避免过多的连接、使用批处理等。


#### 实时监控系统性能


MVC架构的性能优化需要实时监控系统性能,对系统进行优化。可以使用Spring Boot Actuator或类似工具对系统的性能进行实时监控,从而了解系统的性能瓶颈所在,进一步进行优化。


### DDD领域驱动层面调优


DDD(Domain-Driven Design)是一种软件架构设计和开发的方法论,其重点在于提取业务逻辑,使其清晰、简单,并且易于维护。DDD注重业务领域的核心模型和业务规则的提取和应用。


下面是一些DDD领域驱动层面的调优建议:


#### 定义清晰的领域边界


在进行DDD架构设计时,需要清晰地定义领域边界。这有助于将系统拆分为更小的模块,提高系统的灵活性和可维护性。


#### 优化聚合根的设计


聚合根是DDD中最基本的概念,负责管理一组相关的业务对象。优化聚合根的设计可以提高系统的性能,其中一些常见的优化方法包括:将聚合根拆分为小的子树、使用非阻塞I/O等。


#### 采用CQRS架构


CQRS(Command Query Responsibility Segregation)是一种将查询操作和修改操作分离的架构模式。采用CQRS可以进一步提高系统的性能和可维护性,使系统更容易扩展。


#### 基于事件驱动的架构


使用基于事件驱动的架构可以减少系统的耦合度,提高系统的可伸缩性和可容错性。事件驱动架构通常涉及到事件发布和订阅机制,消息队列等。


## 架构服务


Java 系统架构调优是一项非常重要的任务,它涉及到架构服务、架构安全、可扩展性、架构迁移等多个方面。下面将从这几个方面介绍 Java 系统架构调优。


### 服务优化


Java服务优化是一项重要的任务,它涉及到服务的性能、可靠性和用户体验等多个方面。在进行Java服务优化时,需要考虑以下几个方面。


#### 系统性能优化


系统性能是Java服务优化的核心,它涉及到服务的响应速度、吞吐量和资源利用率等多个方面。为了优化系统性能,可以从以下几个角度入手:


- 内存管理:Java应用程序使用的内存需要进行垃圾回收,如果垃圾回收不及时或者不充分,会导致内存泄漏和性能下降。因此,在进行Java服务优化时,需要对内存进行合理的管理和配置,包括调整堆大小、设置垃圾回收器类型等。

- 线程管理:线程是Java服务中的重要资源,线程的数量和状态会影响到服务的响应速度和吞吐量。为了优化线程管理,可以使用线程池和异步处理等技术来管理线程,避免因线程数过多或者过少导致性能问题。

- 数据库访问优化:数据库是Java服务中的重要组成部分,数据库的性能直接影响到服务的响应速度和稳定性。为了优化数据库访问,需要使用合理的查询语句、索引和连接池等技术,避免频繁查询和连接导致的性能问题。


#### 用户体验优化


用户体验是Java服务优化的另一个重要方面,它涉及到服务的界面设计、交互方式和反馈机制等多个方面。为了优化用户体验,可以从以下几个角度入手:


- 界面设计:Java服务的界面设计需要符合用户的使用习惯和喜好,同时还需要满足用户的需求和反馈。为了优化界面设计,需要使用合适的控件和样式,避免界面过于复杂和冗余。

- 交互方式:Java服务的交互方式需要简单明了、易于理解和操作。为了优化交互方式,可以使用语音识别、手势识别等新技术,提升用户体验和操作效率。

- 反馈机制:Java服务的反馈机制需要及时、准确和友好,包括提示信息、错误处理和日志记录等。为了优化反馈机制,可以使用合适的提示语和图标,同时还需要记录重要的错误和操作信息,以便后续的分析和优化。


#### 系统稳定性优化


系统稳定性是Java服务优化的关键,它涉及到服务的安全性、可靠性和容错性等多个方面。为了优化系统稳定性,可以从以下几个角度入手:


- 安全性:Java服务的安全性需要保证用户隐私和数据安全,包括身份认证、数据加密和授权管理等。为了优化安全性,需要使用合适的加密和认证方式,同时还需要定期更新和维护安全策略。

- 可靠性:Java服务的可靠性需要保证服务的稳定和可用性,包括故障恢复、备份和监控等。为了优化可靠性,需要使用合适的监控工具和报警机制,定期备份和维护服务,以便在故障时能够快速恢复服务。

- 容错性:Java服务的容错性需要保证服务能够在不同情况下正常运行,包括网络故障、系统崩溃和数据丢失等。为了优化容错性,需要使用合适的容错机制,如负载均衡、数据复制和容器化等,以提高服务的可用性和稳定性。




### 服务治理

服务治理是Java应用架构中非常重要的一部分。它主要涉及对服务的可靠性、可扩展性、可维护性等问题进行管理和控制。下面将详细介绍服务治理的几个方面技术:


#### 配置管理


配置管理是服务治理中非常重要的一环。在服务治理中,很多服务都需要一些配置。例如,数据库连接、接口地址、请求参数等等。如果每个服务都有自己的配置文件,那么对于维护和修改都会非常麻烦。因此,需要采用统一的配置管理平台,将配置统一管理,并且可以支持动态修改配置。这样,当需要修改配置的时候,只需要修改配置中心即可,而不需要修改每个服务的配置文件。


#### 负载均衡


负载均衡可以将流量分布到多台服务器上,避免某一台服务器过载出现性能问题。可以采用硬件负载均衡、软件负载均衡等技术。硬件负载均衡一般是指将流量分发到多台物理服务器上,例如F5、Netscaler等。而软件负载均衡一般是指将流量分发到多个服务提供者上,例如Dubbo、Spring Cloud等。


#### 服务监控


服务监控是服务治理中非常重要的一环。可以采用指标监测、日志监测等技术,对服务进行实时监控,以发现异常和性能瓶颈。指标监测可以监测服务的CPU使用率、内存使用率、网络延迟等指标。而日志监测则可以监测服务的调用日志、错误日志等。通过实时监控,可以及时发现问题并进行处理。


#### 安全管理


安全管理包括用户认证、访问控制、数据加密等方面。可以采用OAuth、SSL等技术来保证系统的安全。OAuth可以实现用户授权认证,防止未授权的请求。而SSL则可以对数据进行加密传输,防止数据被窃取或篡改。此外,还可以采用访问控制技术,对用户的权限进行控制,以确保系统的安全。




### 服务架构


#### 微服务


微服务架构是一种分布式架构,其基本思想是将一个大型的应用系统分解为多个独立的服务,每个服务都有自己独立的业务逻辑和数据存储。每个服务都可以独立地进行开发、测试、部署和运维,服务之间通过API或消息队列进行通信。


微服务架构有以下优点:


- 可扩展性:每个服务都可以独立地进行扩展,不会影响其他服务。

- 可维护性:每个服务都有自己的代码、数据库和日志,易于维护和升级。

- 高可用性:每个服务都可以独立地进行部署和运行,当某个服务出现故障时,不会影响其他服务的正常运行。


#### 云计算


云计算是一种通过互联网进行计算资源的共享和交付的方式。云计算可以将应用部署在云端,通过弹性计算和自动化管理,实现资源的高效利用和成本的降低。


云计算有以下优点:


- 弹性扩展:云计算平台可以根据应用的负载情况自动调整资源,实现弹性扩展。

- 自动化管理:云计算平台可以自动化地管理应用的部署、监控、备份、恢复等工作,减少人工操作,提高效率。

- 成本降低:云计算平台可以根据实际使用情况计费,不需要预先购买昂贵的硬件设备,降低成本。


#### 容器化


容器化是一种虚拟化技术,可以将应用程序打包到一个容器中,实现快速部署和运行。容器和虚拟机不同,容器运行时不需要模拟整个操作系统,只需要虚拟一部分操作系统和相关的进程。


容器化有以下优点:


- 快速部署:容器可以快速地部署和启动,减少了部署和启动的时间。

- 高效利用资源:多个容器可以共享同一个操作系统内核,减少资源的浪费。

- 环境一致性:容器中运行的应用程序环境与开发环境一致,可以避免因环境不同导致的问题。


以上三个技术都是服务架构中非常重要的技术,可以帮助开发人员实现应用的高可用性、高可维护性和高可扩展性。



### 服务容器


服务容器是一种轻量级的虚拟化技术,通过将应用程序及其依赖打包到容器中,从而实现快速、高效、便捷地部署和管理应用程序的目的。


服务容器可以采用以下几个方面的技术:


#### 容器引擎


容器引擎是服务容器的核心部分,它提供了容器的创建、启动、停止、删除等操作。Docker 是目前最流行的容器引擎,它支持将应用程序及其依赖打包到容器中,并提供了使用简单的命令行工具来管理容器。


另外,还有 LXC 和 LXD,它们提供了容器的创建、启动、停止、删除等操作,同时也支持使用简单的命令行工具来管理容器。


#### 容器编排


容器编排可以自动化管理容器,包括容器的部署、扩展、负载均衡、服务发现等。容器编排可以极大地简化容器的管理工作,提高容器的可靠性和可扩展性。


目前最流行的容器编排工具是 Kubernetes,它提供了完整的容器编排及管理解决方案,支持自动化部署、伸缩、升级和容器的自动修复等功能。除此之外,还有 Mesos、Swarm 等容器编排工具。


####  容器注册


容器注册是指将容器信息注册到服务中心,可以方便地进行容器管理和服务发现。容器注册可以让应用程序轻松地访问和使用服务,同时也提供了容器的管理和监控功能。


目前最流行的容器注册中心是 Etcd 和 Consul,它们提供了高可用、分布式的键值存储服务,支持快速注册和发现服务、容器的健康检查等功能。


总的来说,服务容器技术已经成为现代应用架构的重要组成部分,能够提供高效、可靠、便捷的应用程序部署和管理方案,让应用程序可以更快速地响应业务需求。



## 架构安全



代码安全是保障应用程序的源代码不受到恶意攻击或者非法篡改的安全措施。源代码是应用程序的核心,如果有漏洞或者不安全的代码,那么攻击者可以利用这些漏洞来入侵并操纵应用程序,从而造成重大安全风险。因此,代码安全是保障应用程序的合法性、完整性和可信度的必要手段,可以通过代码审计、代码规范约束等方式来实现。


数据安全是保障应用程序中的数据不被非法访问、泄露、篡改或者破坏的安全措施。数据是应用程序的重要组成部分,包括用户信息、交易记录、订单信息等等,如果数据不具有安全性,那么攻击者可以通过各种手段获取有价值的数据甚至篡改数据,从而造成重大的安全风险。数据安全可以通过加密、备份、灾备等方式来保障数据完整性、可用性和可信度,提高应用程序的安全性。


用户安全是保障应用程序中的用户不受到恶意攻击或者隐私泄露的安全措施。用户是应用程序的使用者,用户的安全性和个人隐私是应用程序的重要组成部分。如果用户的信息泄露或者密码被盗用,那么攻击者可以通过伪造身份来获取有价值的信息或者进行欺诈等行为。因此,用户安全包括身份认证、授权和访问控制等措施,可以减少攻击者获取用户信息的难度,从而提高应用程序的安全性。


网络安全是保障应用程序在网络环境中不受到恶意攻击或者破坏的安全措施。网络是应用程序和用户之间的桥梁,也是攻击者攻击应用程序的主要手段之一。网络安全需要通过网络安全设备、协议和策略等多种手段来保障应用程序的安全性,例如防火墙、入侵检测系统、反垃圾邮件系统等。


综上所述,代码安全、数据安全、用户安全和网络安全是保障应用程序安全性的重要方面,需要应用程序开发者和管理员共同来保障。只有通过多项安全措施的整合和协同,才能确保应用程序的安全性。


## 可扩展


### 垂直扩展


垂直扩展,也称为“增加单机资源”,是指通过增加一台服务器的硬件资源(CPU、内存、硬盘等)来提高系统的性能。这种扩展方式可以使单台服务器处理更大的流量和负载,从而提升系统的性能。


垂直扩展的优点在于:


- 硬件升级成本相对较低。为了实现垂直扩展,只需要升级单个服务器上的硬件,不需要增加额外的服务器。

- 系统维护相对容易。由于系统只有一台服务器,因此维护和管理的难度相对较低。


然而,垂直扩展的局限性非常明显。一方面,单台服务器的性能是有限的,如果增加的负载超出了服务器的处理能力,就会导致系统崩溃。另一方面,垂直扩展也存在单点故障的风险,如果服务器出现问题,整个系统就会瘫痪。


###  水平扩展


水平扩展,也称为“增加服务器数量”,是通过增加服务器的数量来提高系统的性能。与垂直扩展不同,水平扩展不是通过增加单个服务器的硬件资源,而是通过增加服务器数量来分担负载,从而提高系统的性能。


水平扩展的优点在于:


- 可以无限制地增加服务器数量,从而满足大规模应用的需求。

- 可以避免单点故障的风险,由于数据和负载被分散到多个服务器上处理,如果一个服务器出现故障,整个系统仍然可以正常运行。


水平扩展的缺点在于:


- 增加服务器的成本相对较高。为了实现水平扩展,需要增加多个服务器和网络设备,从而增加了存储和维护的成本。

- 系统架构相对复杂。由于系统由多个服务器组成,因此需要考虑服务之间的相互协作和负载均衡,这需要复杂的系统架构和算法支持。


###  弹性伸缩


弹性伸缩是指根据应用的负载情况自动调整服务器数量,实现自动扩展和收缩,从而保证应用的高可用性和性能稳定性。弹性伸缩是一种自动化的水平扩展,可以根据负载情况自动增加或减少服务器的数量,从而保证系统的稳定性和可用性。


弹性伸缩的优点在于:


- 可以根据负载情况自动调整服务器数量,从而避免了手动干预的风险和误差。

- 可以根据需求自动扩展和收缩服务器,从而节省了服务器资源和成本。


弹性伸缩的缺点在于:


- 系统架构相对复杂。为了实现弹性伸缩,需要复杂的系统架构和算法支持,这对于系统开发和运维人员的技术要求较高。

- 需要对服务器资源进行有效管理。弹性伸缩需要对服务器资源进行有效管理,包括分配、释放、监控等,这需要投入更多的时间和精力。


## 架构迁移


架构迁移是一项复杂的任务,需要考虑各种因素,包括迁移目标、迁移策略、问题解决和测试验证等方面。下面将对这些方面进行详细的阐述。


### 迁移目标


在确定应用架构迁移的目标和范围时,需要明确以下几点:


1. 迁移的目的:确定应用迁移的主要原因,例如降低成本、提高效率、改善用户体验等。


2. 迁移的范围:确定应用迁移的内容和范围,例如迁移应用的哪些部分、迁移到哪种架构等。


3. 迁移的时间:确定应用迁移的时间,例如在业务低峰期进行迁移,避免对业务造成影响。


###  迁移策略


在确定应用架构迁移的方式和步骤时,需要考虑以下几个方面:


1. 逐步迁移:逐步将应用从原有架构迁移至新的架构,每次只迁移一部分,逐步完成整个迁移过程。


2. 全面迁移:一次性将应用从原有架构迁移至新的架构,整个迁移过程较为复杂,需要充分的准备和协作。


3. 分批迁移:将应用按照不同的模块或业务逐一迁移,避免一次迁移过程中对整个业务造成较大的影响。


4. 非停机迁移:在应用迁移的过程中,保持应用的运行状态,避免对业务造成影响。


###  问题解决


在架构迁移的过程中,可能会遇到各种问题,需要通过技术手段和团队协作来解决。例如:


1. 兼容性问题:在迁移过程中,可能会遇到兼容性问题,需要进行相应的调整和优化。


2. 接口调整问题:在迁移过程中,可能需要对接口进行调整,需要与相关团队进行协作,确保接口在迁移后能够正常运行。


3. 安全问题:在迁移过程中,可能会存在安全问题,需要进行相应的安全调整和优化。


###  测试和验证


在应用架构迁移后,需要进行测试和验证,以确保应用的功能和性能与之前基本一致,避免因迁移带来的新问题和风险。测试和验证的主要内容包括:


1. 功能测试:测试应用的各项功能是否正常。


2. 性能测试:测试应用在新的架构下的性能是否有所提升或下降。


3. 安全测试:测试应用在新的架构下的安全性是否有所提升或下降。


4. 兼容性测试:测试应用在新的架构下是否与其他系统或服务正常协作。


综上所述,应用架构迁移是一项复杂的任务,需要在迁移目标、迁移策略、问题解决和测试验证等方面做好充分的准备和协作。

# 高并发

要调优超高并发系统架构,需要从多个方面入手,包括硬件、软件、网络、数据库等方面,下面是一些调优建议:


1. 硬件调优:采用高性能的服务器和网络设备,包括高性能CPU、大容量内存、高速网络接口卡等。


2. 软件调优:使用高性能的操作系统和Web服务器,调整系统参数和Web服务器配置文件,以充分利用硬件资源,提高系统性能。


3. 网络调优:通过网络优化技术,如负载均衡、内容分发网络(CDN)、安全加速等,增强网络带宽和稳定性。


4. 数据库调优:通过数据库性能优化技术,如索引优化、分区等,提高数据库读写性能和响应速度,从而增强系统并发处理能力。


5. 分布式架构调优:采用分布式架构,将系统拆分成多个服务,通过负载均衡和分布式缓存等技术,实现高效的负载均衡和数据共享,提高系统可伸缩性和稳定性。


6. 静态化和缓存调优:静态化和缓存是提高系统性能的有效技术。静态化采用CDN技术,将静态资源部署到CDN服务器上,缓解Web服务器的负载压力;缓存采用分布式缓存技术,如Redis,将热点数据存储在内存中,缓解数据库的压力。


7. 异常监控和预警:及时发现系统异常和瓶颈,采用监控预警技术,如Zabbix等,实时监控系统性能,及时发现故障。


通常情况下,前面所有列举的优化手段足以支撑过亿级别的并发,剩下的需要依靠硬件层面去拓展,分而治之,平摊到每台机器上其实并发也不会很高。这里可以查看我前面写过的文章红包雨压测2万用户30秒内每个用户5次请求,QPS=3333.3,完全可以抗住一定的并发量。



![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/1c909ff76258433db9e3a966a7013713.png)

# 给读者的话

尊敬的读者朋友们,本篇文章由我撰写。该文章是《Java项目实战—深入理解大型互联网企业通用技术》书的进阶篇的内容,其中包括大量的资料。我认为,对于大部分的开发者而言,这些内容都具有一定的价值。阅读本篇文章后,您可以更好地了解和优化整个系统架构层面,包括JVM、Redis缓存、MySQL数据库、消息队列、全文搜索ES、系统架构等方面。


需要特别强调的是,本篇文章已经通过出版社三审校验,预计明年正式出版。因此,本文不得被抄袭。如被发现,清华大学出版社将追究法律责任,我这里和出版社沟通之后也仅仅只能发部分内容出来,提供给大家学习使用。同时,我诚挚地希望您能给我点赞、收藏,支持我的创作,谢谢。


当然读者朋友们能关注一波就更好啦,哈哈哈。


主页链接:[https://blog.csdn.net/java_wxid](https://blog.csdn.net/java_wxid)

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/3a18759d3f8440dead9ed1ff028e867b.png)

开源项目:[https://gitee.com/java_wxid/java_wxid](https://gitee.com/java_wxid/java_wxid)

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/2b252d3a62ff48209a8f6e007d5cf4ee.png)




相关文章
|
6天前
|
存储 安全 算法
【JAVA】HashMap扩容性能影响及优化策略
【JAVA】HashMap扩容性能影响及优化策略
|
1天前
|
存储 缓存 前端开发
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
11 3
|
4天前
|
Java 索引
Java String应用与开发
Java String应用与开发
12 0
|
4天前
|
IDE Java 开发工具
基于Java程序设计的实验教学方法优化与实践
基于Java程序设计的实验教学方法优化与实践
15 1
|
4天前
|
安全 Java 开发者
构建高效微服务架构:后端开发的新范式Java中的多线程并发编程实践
【4月更文挑战第29天】在数字化转型的浪潮中,微服务架构已成为软件开发的一大趋势。它通过解耦复杂系统、提升可伸缩性和促进敏捷开发来满足现代企业不断变化的业务需求。本文将深入探讨微服务的核心概念、设计原则以及如何利用最新的后端技术栈构建和部署高效的微服务架构。我们将分析微服务带来的挑战,包括服务治理、数据一致性和网络延迟问题,并讨论相应的解决方案。通过实际案例分析和最佳实践的分享,旨在为后端开发者提供一套实施微服务的全面指导。 【4月更文挑战第29天】在现代软件开发中,多线程技术是提高程序性能和响应能力的重要手段。本文通过介绍Java语言的多线程机制,探讨了如何有效地实现线程同步和通信,以及如
|
5天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第28天】在Java并发编程中,锁是一种常用的同步机制,用于保护共享资源的访问。然而,不当的使用锁可能导致性能问题和死锁。本文将探讨Java中锁的优化策略,包括锁粗化、锁消除、锁降级以及读写锁等技术,以提高并发程序的性能和可靠性。
|
6天前
|
存储 SQL 缓存
Java 性能优化:理论与实践
【4月更文挑战第27天】在 Java 应用开发中,性能优化是一个关键领域,它关乎应用的响应速度和资源效率。优化可以从代码级别到架构设计层面进行。本篇博客将深入探讨 Java 性能优化的策略,并通过具体的示例演示如何实现这些优化。
16 3
|
7天前
|
安全 算法 Java
写给Java开发的Go语言协程实践
写给Java开发的Go语言协程实践
12 0
|
8天前
|
消息中间件 缓存 NoSQL
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
|
9天前
|
开发框架 前端开发 安全
Java从入门到精通:2.2.2学习使用Spring框架进行Web应用开发
Java从入门到精通:2.2.2学习使用Spring框架进行Web应用开发