一、概述 1.1、概述 LangChain4J 即 LangChain For Java。 随着人工智能（AI）技术的迅猛发展，越来越多的开发者开始将目光投向AI应用的开发。然而，目前市场上大多数AI框架和工具如LangChain、PyTorch等主要支持Python，而Java开发者常常面临工具缺乏和学习门槛较高的问题。 LangChain4J 官方文档 1.2、作用 LLM 大模型应用技术架构： 大模型开发分类：超级个体+智能体、行业垂直大模型、基础通用大模型。 产品定位： 二、起步 2.1、前置约定 2.1.1、langchain4j 执行的各种大语言模型(LLMS) Comparison Table of all supported Language Models 2.1.2、模型约定 2.1.3、配置及关键点 所有调用均基于 OpenAI 协议标准或者 SrpingBoot 官方推荐整合规则，实现一致的接口设计与规范，确保多模型切换的便利性，提供高度可扩展的开发支持。 2.2、入门 2.2.1、接入按理百炼平台的通义模型 https://bailian.console.aliyun.com 2.2.2、大模型调用 1、获得 API-key 2、获得模型名 3、获得 baseUrl 开发地址
一、Skywalking skywalking 是一个优秀的国产开源框架，2015 年由个人吴晟（华为开发者）开源 ， 2017 年加入 Apache 孵化器。 skywalking 支持 dubbo，SpringCloud，SpringBoot 集成，代码无侵入，通信方式采用 GRPC，性能较好，实现方式是 java 探针，支持告警，支持 JVM 监控，支持全局调用统计等等，功能较完善。 二、Skywalking & Spring Cloud Sleuth+ZipKin Skywalking 相比于 zipkin 还是有很大的优势的，如下： 1、Skywalking 采用字节码增强的技术实现代码无侵入，zipKin 代码侵入性比较高。 2、Skywalking 功能比较丰富，报表统计，UI 界面更加人性化。 三、Skywalking 架构 skywalking 和 zipkin 一样，也分为服务端和客户端，服务端负责收集日志数据并且展示，架构如下： 上述架构图中主要分为四个部分，如下： 上面的 Agent：负责收集日志数据，并且传递给中间的 OAP 服务器 中间的 OAP：负责接收 Agent 发送的 Tracing 和 Metric 的数据信息，然后进行分析 (Analysis Core) ，存储到外部存储器 ( Storage )，最终提供查询 ( Query ) 功能。 左面的 UI：负责提供 web 控制台，查看链路，查看各种指标，性能等等。 右面 Storage：负责数据的存储，支持多种存储类型。 Agent 负责收集日志传输数据，通过 GRPC 的方式传递给 OAP 进行分析并且存储到数据库中，最终通过 UI 界面将分析的统计报表、服务依赖、拓扑关系图展示出来。 三、服务端搭建 skywalking 同样是通过 jar 包方式启动，需要下载 jar 包，地址： https://skywalking.apache.org/downloads/ https://archive.apache.org/dist/skywalking 3.1、下载安装包 选择 V8.7.0 或其他版本，如下图： 重要的目录结构分析如下： agent：客户端需要指定的目录，其中有一个 jar，就是负责和客户端整合收集日志 bin：服务端启动的脚本 config：一些配置文件的目录 logs：oap 服务的日志目录 oap-libs：oap 所需的依赖目录 webapp：UI 服务的目录 3.2、配置修改 启动之前需要对配置文件做一些修改，修改如下： 3.2.1、/config/application.yml 这个是 oap 服务的配置文件，需要修改注册中心为 nacos，如下图： 配置 1：修改默认注册中心选择 nacos，这样就不用在启动参数中指定了。 配置 2：修改 nacos 的相关配置，由于陈某是本地的，则不用改动，根据自己情况修改。 3.2.2、webapp/webapp.yml 这个是 UI 服务的配置文件，其中有一个 server.port 配置，是 UI 服务的端口，默认 8080，可将其改成 8081，避免端口冲突。 3.3、启动服务 启动命令在 /bin 目录下，这里需要启动两个服务，如下： oap 服务：对应的启动脚本 oapService.bat，Linux 下对应的后缀是 sh。 UI 服务：对应的启动脚本 webappService.bat，Linux 下对应的后缀是 sh。 当然还有一个 startup.bat 启动文件，可以直接启动上述两个服务，我们可以直接使用这个脚本，直接双击，将会弹出两个窗口则表示启动成功。 此时直接访问：http://localhost:8888/，直接进入 UI 端。 四、客户端搭建 客户端也就是单个微服务，由于 Skywalking 采用字节码增强技术，因此对于微服务无代码侵入，只要是普通的微服务即可，不需要引入什么依赖。 想要传输数据必须借助 skywalking 提供的 agent，只需要在启动参数指定即可，命令如下： -javaagent:E:\SOFTWARE\skywalking\apache-skywalking-apm-bin\agent\skywalking-agent.jar -Dskywalking.agent.service_name=jeecg-cloud-gateway -Dskywalking.collector.backend_service=localhost:11800 上述命令解析如下： -javaagent：指定 skywalking 中的 agent 中的 skywalking-agent.jar 的路径。 -Dskywalking.agent.service_name：指定在 skywalking 中的服务名称，一般是微服务的 spring.application.name。 -Dskywalking.collector.backend_service：指定 oap 服务绑定的地址，由于这里是本地，并且 oap 服务默认的端口是 11800，因此只需要配置为 127.0.0.1:11800。 五、数据持久化 服务端重启之后，这些链路追踪数据将会丢失了，因为 skywalking 默认持久化的方式是存储在内存中。 当然这里也是可以通过插拔方式的替换掉存储中间件，企业中往往是使用 ES 存储，此处介绍一下 MySQL 的方式存储。 5.1、修改配置文件 修改 config/application.yml 文件中的存储方式，总共需要修改两处地方。 修改默认的存储方式为 mysql，如下： storage: selector: ${SW_STORAGE:h2} 修改 Mysql 相关的信息，比如用户名、密码等，如下： mysql: properties: jdbcUrl: ${SW_JDBC_URL:"jdbc:mysql://localhost:306/skywalking?ueUnicode-truescharacterEncoding=Unp-8 serverimezone=Asia/shanghai"} dataSource.user: ${SW_DATA_SOURCE_USER:root} dataSource.password: ${SW_DATA_SOURCE_PASSWORD:root@1234} dataSource.cachePrepStmts: ${SW_DATA_SOURCE_CACHE_PREP_STMTS:true} dataSource.prepStmtCacheSize: ${SW_DATA_SOURCE_PREP_STMT_CACHE_SQL_SIZE:250} dataSource.prepStmtCacheSqlLimit: ${SW_DATA_SOURCE_PREP_STMT_CACHE_SQL_LIMIT:2048} dataSource.useServerPrepStmts: ${SW_DATA_SOURCE_USE_SERVER_PREP_STMTS:true} metadataQueryMaxSize: ${SW_STORAGE_MYSQL_QUERY_MAX_SIZE:5000} maxSizeOfArrayColumn: ${SW_STORAGE_MAX_SIZE_OF_ARRAY_COLUMN:20} numOfSearchableValuesPerTag: ${SW_STORAGE_NUM_OF_SEARCHABLE_VALUES_PER_TAG:2} 5.2、添加 MySQL 的 jdbc 依赖 默认的 oap 中是没有 jdbc 驱动依赖，因此需要我们手动添加一下，只需要将驱动的 jar 放在 oap-libs 文件夹中：mysql-connector-java-8.0.15.jar 已经配置完成，启动服务端，在 skywalking 这个数据库中将会自动创建表。 六、日志监控 在 skywalking 的 UI 端有一个日志的模块，用于收集客户端的日志，默认是没有数据的，那么需要如何将日志数据传输到 skywalking 中呢？ 日志框架的种类很多，比较出名的有 log4j，logback，log4j2，此处以 logback 为例子介绍一下如何配置。 6.1、添加依赖 根据官方文档，需要先添加依赖，如下： <dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-logback-1.x</artifactId> <version>${project.release.version}</version> </dependency> 6.2、添加配置文件 新建一个 logback-spring.xml 放在 resource 目录下，配置如下图： <configuration scan="true" scanPeriod=" 5 seconds"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout"> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %boldMagenta([%tid]) [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n</pattern> </layout> </encoder> </appender> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <discardingThreshold>0</discardingThreshold> <queueSize>1024</queueSize> <neverBlock>true</neverBlock> <appender-ref ref="STDOUT"/> </appender> <root level="INFO"> <appender-ref ref="ASYNC"/> </root> </configuration> 当你使用 -javaagent 激活 skywalking tracer 后，logback 将会输出 traceId（如果存在的话）。如果 tracer 未激活，输出将是 TID: N/A。 注意： 如果 agent 和 oap 服务不在同一台服务器上，需要在 /agent/config/agent.config 配置文件末尾添加如下配置： plugin.toolkit.log.grpc.reporter.server_host=${SW_GRPC_LOG_SERVER_HOST:10.10.10.1} plugin.toolkit.log.grpc.reporter.server_port=${SW_GRPC_LOG_SERVER_PORT:11800} plugin.toolkit.log.grpc.reporter.max_message_size=${SW_GRPC_LOG_MAX_MESSAGE_SIZE:10485760} plugin.toolkit.log.grpc.reporter.upstream_timeout=${SW_GRPC_LOG_GRPC_UPSTREAM_TIMEOUT:30} 七、性能剖析 skywalking 在性能剖析方面真的是非常强大，提供到基于堆栈的分析结果，能够让运维人员一眼定位到问题。 新建一个 /order/list 接口，代码中休眠了 2 秒，看看如何在 skywalking 中定位这个问题。 在性能剖析模块 -> 新建任务 -> 选择服务、填写端点、监控时间，操作如下图： 选择这个任务将会出现监控到的数据，如下图： 上图中可以看到 {GET}/order/list 这个接口上耗费了2秒以上，因此选择这个接口点击分析，可以看到详细的堆栈信息，如下图： 如何定位到睡眠 2 秒钟的那一行代码，直接往下翻，如下图： 参考： SkyWalking 8 官方文档 Skywalking增加登录认证和日志收集
一、介绍 1.1、简介 什么是redis集群： 由于数据量过大，单个Master复制集难以承担，因此需要对多个复制集进行集群，形成水平扩展每个复制集只负责存储整个数据集的一部分，这就是Redis的集群，其作用是提供在多个Redis节点间共享数据的程序集。 功能： redis集群支持多个master，每个master又可以挂在多个slave。具有读写分离、数据高可用的效果。 由于集群自带了哨兵的故障转移机制，内置了高可用的支持，所以使用集群无需再去使用哨兵功能。 客户端与redis的节点连接，不再需要连接集群中所有的节点，只需要任意连接集群中的一个可用节点即可。 槽位slot负责分配到各个物理服务节点，由对应的集群来负责维护节点，插槽和数据之间的关系。 Redis 集群的数据分片： Redis 集群没有使用一致性hash，而是引入哈希槽的概念。Redis 集群有16384个哈希槽，每个key通过CRC16校验后对16384取模来决定放置哪个槽，集群的每个节点负责部分hash槽，举个例子，比如当前集群有3个节点，那么: 分片是什么： 使用Redis集群时我们会将存储的数据分散到多台redis机器上，这称为分片。简言之，集群中的每个Redis实例都被认为是整个数据的一个分片。 如何找到给定key的分片： 为了找到给定key的分片，我们对key进行CRC16(key)算法处理并通过对总分片数量取模。然后，使用确定性哈希函数，这意味着 给定的key将多次始终映射到同一个分片，我们可以推断将来读取特定key的位置。 1.2、分片与槽位slot的优势 最大优势，方便扩缩容和数据分派查找： 这种结构很容易添加或者删除节点,比如如果我想新添加个节点D，我需要从节点 A,B,C中得部分槽到D上，如果我想移除节点A，需要将A中的槽移到B和C节点上，然后将没有任何槽的A节点从集群中移除即可，由于从一个节点将哈希槽移动到另一个节点并不会停止服务，所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。 1.3、为什redis集群的最大槽数时16384个 正常的心跳数据包带有节点的完整配置，可以用幂等方式用旧的节点替换旧节点，以便更新旧的配置。 这意味着它们包含原始节点的插槽配置，该节点使用2k的空间和16k的插槽，但是会使用8k的空间（使用65k的插槽）。 同时，由于其他设计折衷，Redis集群不太可能扩展到1000个以上的主节点。 因此16k处于正确的范围内，以确保每个主机具有足够的插槽，最多可容纳1000个矩阵，但数量足够少，可以轻松地将插槽配置作为原始位图传播。请注意，在小型群集中，位图将难以压缩，因为当N较小时，位图将设置的slot / N位占设置位的很大百分比。 (1)如果槽位为65536，发送心跳信息的消息头达8k，发送的心跳包过于庞大。 在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时，这块的大小是: 65536÷8÷1024=8kb 在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时，这块的大小是: 16384÷8÷1024=2kb 因为每秒钟，redis节点需要发送一定数量的ping消息作为心跳包，如果槽位为65536，这个ping消息的消息头太大了，浪费带宽。 (2)redis的集群主节点数量基本不可能超过1000个。 集群节点越多，心跳包的消息体内携带的数据越多。如果节点过1000个，也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么，对于节点数在1000以内的redis cluster集群，16384个槽位够用了。没有必要拓展到65536个。 (3)槽位越小，节点少的情况下，压缩比高，容易传输 Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的，在传输过程中会对bitmap进行压缩，但是如果bitmap的填充率slots / N很高的话(N表示节点数)，bitmap的压缩率就很低。 如果节点数很少，而哈希槽数量很多的话，bitmap的压缩率就很低。 1.4、槽位映射的发展 第一阶段：哈希取余分区 2亿条记录就是2亿个Kv，我们单机不行必须要分布式多机，假设有3台机器构成一个集群，用户每次读写操作都是根据公式hash(key)% N个机器台数，计算出哈希值，用来决定数据映射到哪一个节点上。 优点: 简单粗暴，直接有效，只需要预估好数据规划好节点，例如3台、8台、10台，就能保证一段时间的数据支撑。使用Hash算法让固定的-到同一台服务器上，这样每台服务器固定处理一部分请求(并维护这些请求的信息)，起到负载均衡+分而治之的作用。 缺点: 原来规划好的节点，进行扩容或者缩容就比较麻烦了额，不管扩缩，每次数据变动导致节点有变动，映射关系需要重新进行计算，在用定不变时没有问题，如果需要弹性扩容或故障停机的情况下，原来的取模公式就会发生变化:Hash(key)/3会变成Hash(key)/?。此时地址算的结果将发生很大变化，根据公式获取的服务器也会变得不可控。某个redis机器宕机了，由于台数数量变化，会导致hash取余全部数据重新洗牌。 第二阶段：一致性哈希算法分区 为了在节点数目发生改变时尽可能少的迁移数据 将所有的存储节点排列在收尾相接的Hash环上，每个key在计算Hash后会顺时针找到临近的存储节点存放。 而当有节点加入或退出时仅影响该节点在Hash环上顺时针相邻的后续节点。 优点： 加入和删除节点只影响哈希环中顺时针方向的相邻的节点，对其他节点无影响 缺点： 数据的分布和节点的位置有关，因为这些节点不是均匀的分布在哈希环上的，所以数据在进行存储时达不到均匀分布的效果。 第三阶段：哈希槽分区 1、为什么出现 致性哈希算法的数据倾斜问题 哈希槽实质就是一个数组，数组[0,2^14-1]形成hash slot空间。 2、能干什么 解决均匀分配的问题，在数据和节点之间又加入了一层，把这层称为哈希槽(slot) ，用于管理数据和节点之间的关系，现在就相当于节点上放的是槽，槽里放的是数据。 槽解决的是粒度问题，相当于把粒度变大了，这样便于数据移动。哈希解决的是映射问题，使用key的哈希值来计算所在的槽，便于数据分配 3、多少个hash槽 一个集群只能有16384个槽，编号0-16383(0-2^14-1)。这些槽会分配给集群中的所有主节点，分配策略没有要求。集群会记录节点和槽的对应关系，解决了节点和槽的关系后，接下来就需要对key求哈希值，然后对16384取模，余数是几kev就落入对应的槽里。HASH SLOT=CRC16(key) mod 16384。以槽为单位移动数据，因为槽的数目是固定的，处理起来比较容易，这样数据移动问题就解决了。 Redis集群中内置了16384个哈希槽，redis会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在 Redis集群中放置一个key-value时，redis先对key使用crc16算法算出一个结果然后用结果对16384求余数[CRC16(key)% 16384]，这样每个key都会对应一个编号在 0-16383之间的哈希槽，也就是映射到某个节点上。如下代码，key之A、B在Node2，key之C落在Node3上。 二、配置集群 前置条件： 三台虚拟机，各自新建：mkdir-p /myredis/cluster 配置6个独立redis服务，配置文件展示： bind 0.0.0.0 daemonize yes protected-mode no port 6381 logfile "/myredis/cluster/cluster6381.log" pidfile /myredis/cluster6381.pid dir /myredis/cluster dbfilename dump6381.rdb appendonly yes appendfilename "appendonly6381.aof" requirepass 111111 masterauth 111111 cluster-enabled yes cluster-config-file nodes-6381.conf cluster-node-timeout 5000 配置如上，记得修改端口。 遇到的报错： [root@192 cluster]# redis-server /myredis/cluster/redis-6381.conf *** FATAL CONFIG FILE ERROR (Redis 7.0.0) *** replicaof directive not allowed in cluster mode 哨兵在配置文件中追加了replicaof相关的内容，将哨兵追加的内容删除后重新启动。 三、构建集群 [root@192 cluster]# redis-cli -a 111111 --cluster create --cluster-replicas 1 192.168.1.11:6381 192.168.1.11:6382 192.168.1.12:6383 192.168.1.12:6384 192.168.1.13:6385 192.168.1.13:6386 # 1输入命令 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. >>> Performing hash slots allocation on 6 nodes... Master[0] -> Slots 0 - 5460 Master[1] -> Slots 5461 - 10922 Master[2] -> Slots 10923 - 16383 Adding replica 192.168.1.12:6384 to 192.168.1.11:6381 Adding replica 192.168.1.13:6386 to 192.168.1.12:6383 Adding replica 192.168.1.11:6382 to 192.168.1.13:6385 M: 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381 slots:[0-5460] (5461 slots) master S: cc9a9f6bad23314263e2b89e4beede4fc59c3ef3 192.168.1.11:6382 replicates 95d8800931e38739a85d6face5296d719a8d5bfa M: 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383 slots:[5461-10922] (5462 slots) master S: 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 192.168.1.12:6384 replicates 491bb2df7fa39bafd917f7aaa0a409e19e41b07f M: 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385 slots:[10923-16383] (5461 slots) master S: 458cf2edf6cc3005cee4fc3aa93278efa86637ec 192.168.1.13:6386 replicates 3075d51754554aa421ee23ccbd8e263c0d5c4f0e Can I set the above configuration? (type 'yes' to accept): yes #2输入yes确认 >>> Nodes configuration updated >>> Assign a different config epoch to each node >>> Sending CLUSTER MEET messages to join the cluster Waiting for the cluster to join >>> Performing Cluster Check (using node 192.168.1.11:6381) M: 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381 slots:[0-5460] (5461 slots) master 1 additional replica(s) S: 458cf2edf6cc3005cee4fc3aa93278efa86637ec 192.168.1.13:6386 slots: (0 slots) slave replicates 3075d51754554aa421ee23ccbd8e263c0d5c4f0e S: cc9a9f6bad23314263e2b89e4beede4fc59c3ef3 192.168.1.11:6382 slots: (0 slots) slave replicates 95d8800931e38739a85d6face5296d719a8d5bfa M: 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385 slots:[10923-16383] (5461 slots) master 1 additional replica(s) S: 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 192.168.1.12:6384 slots: (0 slots) slave replicates 491bb2df7fa39bafd917f7aaa0a409e19e41b07f M: 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383 slots:[5461-10922] (5462 slots) master 1 additional replica(s) [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. ##3到这里构建成功。三主三从 [root@192 cluster]# 1638个槽位分别对应哪几个主机 四、场景演示 127.0.0.1:6381> cluster keyslot k1 # 查看“k1”这个key对应的槽位 (integer) 12706 # 12706对应的是6385这台主机 在6381上set k1 v1 127.0.0.1:6381> set k1 v1 (error) MOVED 12706 192.168.1.13:6385 ###报错 127.0.0.1:6381> 报错原因：需要路由到位 如何解决：连接redis时增加-c参数，优化路由 [root@192 cluster]# redis-cli -a 111111 -p 6381 -c ###连接时添加-c优化路由 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. 127.0.0.1:6381> set k1 v1 # set成功 -> Redirected to slot [12706] located at 192.168.1.13:6385 OK 4.1、容错测试：6381这台master挂了，对应的slave是否会变成master 1、查看关系 127.0.0.1:6381> info replication # 查看关系 # Replication role:master connected_slaves:1 slave0:ip=192.168.1.12,port=6384,state=online,offset=17286,lag=0 #6384为6381的slave master_failover_state:no-failover master_replid:4d3c3673de89c3c84a199d59c70ae6cf245742fc master_replid2:0000000000000000000000000000000000000000 master_repl_offset:17286 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:17286 2、shutdown 6381 127.0.0.1:6381> shutdown not connected> quit 3、登录集群内任意主机，执行cluster nodes命令查看集群关系：6381已经fail，6384成为新的master 4.2、6381重新登录之后，是成为master还是slave？--slave身份 [root@192 cluster]# redis-server redis-6381.conf # 启动6381 [root@192 cluster]# redis-cli -a 111111 -p 6381 -c @登录 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. 127.0.0.1:6381> cluster nodes # 查看集群关系 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383@16383 master - 0 1706383916749 3 connected 5461-10922 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 192.168.1.12:6384@16384 master - 0 1706383917558 7 connected 0-5460 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385@16385 master - 0 1706383917558 5 connected 10923-16383 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381@16381 myself,slave 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 0 1706383917000 7 connected # 6381还是slave 458cf2edf6cc3005cee4fc3aa93278efa86637ec 192.168.1.13:6386@16386 slave 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 0 1706383916000 3 connected cc9a9f6bad23314263e2b89e4beede4fc59c3ef3 192.168.1.11:6382@16382 slave 95d8800931e38739a85d6face5296d719a8d5bfa 0 1706383917759 5 connected 4.3、有主机宕机后，集群的关系与当初设计时不一致了。关系被打乱。如何恢复？ 以当前为例。6381的master被6384顶替了。需要登录6381之后命令：cluster failover 127.0.0.1:6381> cluster failover # 将当前主机置为master OK 127.0.0.1:6381> cluster nodes 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383@16383 master - 0 1706384388767 3 connected 5461-10922 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 192.168.1.12:6384@16384 slave 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 0 1706384389000 8 connected 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385@16385 master - 0 1706384389573 5 connected 10923-16383 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381@16381 myself,master - 0 1706384389000 8 connected 0-5460 458cf2edf6cc3005cee4fc3aa93278efa86637ec 192.168.1.13:6386@16386 slave 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 0 1706384388057 3 connected cc9a9f6bad23314263e2b89e4beede4fc59c3ef3 192.168.1.11:6382@16382 slave 95d8800931e38739a85d6face5296d719a8d5bfa 0 1706384389775 5 connected 4.4、扩容与缩容测试---扩容 1、复制两份配置文件，分别将端口号替换为6387和6388 2、启动两个新节点实例，此时他们都是master [root@192 cluster]# redis-server redis-6387.conf # 启动两个实例 [root@192 cluster]# redis-server redis-6388.conf [root@192 cluster]# redis-cli -a 111111 -p 6387 # 登录一个做演示 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. 127.0.0.1:6387> info replication # 查看关系 # Replication role:master ###master connected_slaves:0 master_failover_state:no-failover master_replid:ccf0e3ef0a7ea4c965d4f1298518686b5e748a7c master_replid2:0000000000000000000000000000000000000000 master_repl_offset:0 second_repl_offset:-1 repl_backlog_active:0 repl_backlog_size:1048576 repl_backlog_first_byte_offset:0repl_backlog_histlen:0 3、将新增的6387（空槽号）作为master节点加入原来的集群：命令：redis-cli -a 密码 --cluster add-node 要加入的节点ip:端口 已经在集群中的节点ip:端口（相当于介绍人） [root@192 cluster]# redis-cli -a 111111 --cluster add-node 192.168.1.13:6387 192.168.1.11:6381 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. >>> Adding node 192.168.1.13:6387 to cluster 192.168.1.11:6381 >>> Performing Cluster Check (using node 192.168.1.11:6381) M: 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381 slots:[0-5460] (5461 slots) master 1 additional replica(s) M: 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383 slots:[5461-10922] (5462 slots) master 1 additional replica(s) S: 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 192.168.1.12:6384 slots: (0 slots) slave replicates 491bb2df7fa39bafd917f7aaa0a409e19e41b07f M: 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385 slots:[10923-16383] (5461 slots) master 1 additional replica(s) S: 458cf2edf6cc3005cee4fc3aa93278efa86637ec 192.168.1.13:6386 slots: (0 slots) slave replicates 3075d51754554aa421ee23ccbd8e263c0d5c4f0e S: cc9a9f6bad23314263e2b89e4beede4fc59c3ef3 192.168.1.11:6382 slots: (0 slots) slave replicates 95d8800931e38739a85d6face5296d719a8d5bfa [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. >>> Getting functions from cluster >>> Send FUNCTION LIST to 192.168.1.13:6387 to verify there is no functions in it >>> Send FUNCTION RESTORE to 192.168.1.13:6387 >>> Send CLUSTER MEET to node 192.168.1.13:6387 to make it join the cluster. [OK] New node added correctly. [root@192 cluster]# 4、检查集群的情况 5、给新的节点匀一些槽号。命令：redis-cli -a 密码 --cluster reshard IP地址:端口号 [root@192 cluster]# redis-cli -a 111111 --cluster reshard 192.168.1.11:6381 # ip端口为集群内的任意实例 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. >>> Performing Cluster Check (using node 192.168.1.11:6381) M: 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381 slots:[0-5460] (5461 slots) master 1 additional replica(s) M: 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383 slots:[5461-10922] (5462 slots) master 1 additional replica(s) M: 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 192.168.1.13:6387 slots: (0 slots) master S: 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 192.168.1.12:6384 slots: (0 slots) slave replicates 491bb2df7fa39bafd917f7aaa0a409e19e41b07f M: 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385 slots:[10923-16383] (5461 slots) master 1 additional replica(s) S: 458cf2edf6cc3005cee4fc3aa93278efa86637ec 192.168.1.13:6386 slots: (0 slots) slave replicates 3075d51754554aa421ee23ccbd8e263c0d5c4f0e S: cc9a9f6bad23314263e2b89e4beede4fc59c3ef3 192.168.1.11:6382 slots: (0 slots) slave replicates 95d8800931e38739a85d6face5296d719a8d5bfa [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. How many slots do you want to move (from 1 to 16384)? 4096 ###匀多少槽号，案例匀了4096个 What is the receiving node ID? 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 ###匀给谁，这里需要填目标节点ID（6387的ID） Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1: all ###这里填all Ready to move 4096 slots. Source nodes: M: 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381 slots:[0-5460] (5461 slots) master 1 additional replica(s) M: 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383 slots:[5461-10922] (5462 slots) master 1 additional replica(s) M: 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385 slots:[10923-16383] (5461 slots) master 1 additional replica(s) Destination node: M: 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 192.168.1.13:6387 slots: (0 slots) master Resharding plan: Moving slot 5461 from 3075d51754554aa421ee23ccbd8e263c0d5c4f0e Moving slot 5462 from 3075d51754554aa421ee23ccbd8e263c0d5c4f0e Moving slot 5463 from 3075d51754554aa421ee23ccbd8e263c0d5c4f0e Moving slot 5464 from 3075d51754554aa421ee23ccbd8e263c0d5c4f0e .......等 6、再次查看集群情况 问题：为什么6387是3个新的区间，以前的还是连续？ 重新分配成本太高，所以前3家各自匀出来一部分，从6381/6383/6385三个旧节点分别匀出1364个坑位给新节点6387 7、为6387分配从节点6388。命令：redis-cli -a 密码 --cluster add-node ip:新slave端口 ip:新master端口 --cluster-slave --cluster-master-id 新master主机节点ID [root@192 cluster]# redis-cli -a 111111 --cluster add-node 192.168.1.13:6388 192.168.1.13:6387 --cluster-slave --cluster-master-id 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. >>> Adding node 192.168.1.13:6388 to cluster 192.168.1.13:6387 >>> Performing Cluster Check (using node 192.168.1.13:6387) M: 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 192.168.1.13:6387 slots:[0-1364],[5461-6826],[10923-12287] (4096 slots) master M: 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381 slots:[1365-5460] (4096 slots) master 1 additional replica(s) M: 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385 slots:[12288-16383] (4096 slots) master 1 additional replica(s) S: cc9a9f6bad23314263e2b89e4beede4fc59c3ef3 192.168.1.11:6382 slots: (0 slots) slave replicates 95d8800931e38739a85d6face5296d719a8d5bfa M: 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383 slots:[6827-10922] (4096 slots) master 1 additional replica(s) S: 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 192.168.1.12:6384 slots: (0 slots) slave replicates 491bb2df7fa39bafd917f7aaa0a409e19e41b07f S: 458cf2edf6cc3005cee4fc3aa93278efa86637ec 192.168.1.13:6386 slots: (0 slots) slave replicates 3075d51754554aa421ee23ccbd8e263c0d5c4f0e [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. >>> Send CLUSTER MEET to node 192.168.1.13:6388 to make it join the cluster. Waiting for the cluster to join >>> Configure node as replica of 192.168.1.13:6387. [OK] New node added correctly. [root@192 cluster]# 8、第三次检查集群情况 4.5、扩容与缩容测试---缩容 1、先查看集群的情况，获得6388的ID [root@192 cluster]# redis-cli -a 111111 --cluster check 192.168.1.13:6387 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. 192.168.1.13:6387 (2f1ed0f3...) -> 1 keys | 4096 slots | 1 slaves. 192.168.1.11:6381 (491bb2df...) -> 0 keys | 4096 slots | 1 slaves. 192.168.1.13:6385 (95d88009...) -> 1 keys | 4096 slots | 1 slaves. 192.168.1.12:6383 (3075d517...) -> 0 keys | 4096 slots | 1 slaves. [OK] 2 keys in 4 masters. 0.00 keys per slot on average. >>> Performing Cluster Check (using node 192.168.1.13:6387) M: 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 192.168.1.13:6387 slots:[0-1364],[5461-6826],[10923-12287] (4096 slots) master 1 additional replica(s) S: cc9a9f6bad23314263e2b89e4beede4fc59c3ef3 192.168.1.11:6382 slots: (0 slots) slave replicates 95d8800931e38739a85d6face5296d719a8d5bfa M: 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381 slots:[1365-5460] (4096 slots) master 1 additional replica(s) M: 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385 slots:[12288-16383] (4096 slots) master 1 additional replica(s) S: 458cf2edf6cc3005cee4fc3aa93278efa86637ec 192.168.1.13:6386 slots: (0 slots) slave replicates 3075d51754554aa421ee23ccbd8e263c0d5c4f0e S: b01ff575c1e793617332d8786c61bb2a3d1bd290 192.168.1.13:6388 slots: (0 slots) slave replicates 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 S: 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 192.168.1.12:6384 slots: (0 slots) slave replicates 491bb2df7fa39bafd917f7aaa0a409e19e41b07f M: 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383 slots:[6827-10922] (4096 slots) master 1 additional replica(s) [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. [root@192 cluster]# 2、删除6388节点。命令：redis-cli -a 密码 --cluster del-node 被删ip:端口 被删节点ID [root@192 cluster]# redis-cli -a 111111 --cluster del-node 192.168.1.13:6388 b01ff575c1e793617332d8786c61bb2a3d1bd290 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. >>> Removing node b01ff575c1e793617332d8786c61bb2a3d1bd290 from cluster 192.168.1.13:6388 >>> Sending CLUSTER FORGET messages to the cluster... >>> Sending CLUSTER RESET SOFT to the deleted node. [root@192 cluster]# 3、清空6387的槽号，重新分配。当前案例将槽号都分配给6381 [root@192 cluster]# redis-cli -a 111111 --cluster reshard 192.168.1.11:6381 ### Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. >>> Performing Cluster Check (using node 192.168.1.11:6381) M: 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381 slots:[1365-5460] (4096 slots) master 1 additional replica(s) M: 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383 slots:[6827-10922] (4096 slots) master 1 additional replica(s) M: 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 192.168.1.13:6387 slots:[0-1364],[5461-6826],[10923-12287] (4096 slots) master S: 4a60743d7b067dfd03c6a0d4403c88ce173abb1c 192.168.1.12:6384 slots: (0 slots) slave replicates 491bb2df7fa39bafd917f7aaa0a409e19e41b07f M: 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385 slots:[12288-16383] (4096 slots) master 1 additional replica(s) S: 458cf2edf6cc3005cee4fc3aa93278efa86637ec 192.168.1.13:6386 slots: (0 slots) slave replicates 3075d51754554aa421ee23ccbd8e263c0d5c4f0e S: cc9a9f6bad23314263e2b89e4beede4fc59c3ef3 192.168.1.11:6382 slots: (0 slots) slave replicates 95d8800931e38739a85d6face5296d719a8d5bfa [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. How many slots do you want to move (from 1 to 16384)? 4096### What is the receiving node ID? 491bb2df7fa39bafd917f7aaa0a409e19e41b07f ###6381的ID Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1: 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 # 6387的ID Source node #2: done ###这里填done不是all Ready to move 4096 slots. Source nodes: M: 3075d51754554aa421ee23ccbd8e263c0d5c4f0e 192.168.1.12:6383 slots:[6827-10922] (4096 slots) master 1 additional replica(s) M: 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 192.168.1.13:6387 slots:[0-1364],[5461-6826],[10923-12287] (4096 slots) master M: 95d8800931e38739a85d6face5296d719a8d5bfa 192.168.1.13:6385 slots:[12288-16383] (4096 slots) master 1 additional replica(s) Destination node: M: 491bb2df7fa39bafd917f7aaa0a409e19e41b07f 192.168.1.11:6381 slots:[1365-5460] (4096 slots) master 1 additional replica(s) Resharding plan: Moving slot 6827 from 3075d51754554aa421ee23ccbd8e263c0d5c4f0e Moving slot 6828 from 3075d51754554aa421ee23ccbd8e263c0d5c4f0e ........ 4、删除6387master.命令：redis-cli -a 密码 --cluster del-node ip:端口 6387节点ID [root@192 cluster]# redis-cli -a 111111 --cluster del-node 192.168.1.13:6387 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. >>> Removing node 2f1ed0f391fa1e0a5b3fac8e5d3c669c811ac283 from cluster 192.168.1.13:6387 >>> Sending CLUSTER FORGET messages to the cluster... >>> Sending CLUSTER RESET SOFT to the deleted node. [root@192 cluster]# 注意点 集群也不能保证数据一致性，一定会存在数据丢失的情况。 五、相关命令 info replication：返回关于 Redis 主从复制的详细信息 cluster info：获取 Redis 集群的状态和统计信息 cluster nodes：获取关于集群中所有节点的详细信息。 cluster countkeysinslot <槽位数字编号>：该槽位是否被占用。1占用2未占用 cluster keyslot <keyname> 该key应该存放在哪个槽位上 cluster failover ： 将当前redis的身份从slave变成master，原来的master变成slave 不在同一个slot槽位下的多键操作支持不好，通识占位符登场： 不在同一个slot槽位下的键值无法使用mset、mget等多键操作。 可以通过{}来定义同一个组的概念，使key中{内相同内容的键值对放到一个stot槽位去，对照下图类似k1k2k3都映射为x，自然槽位一样。 设置集群是否完整才能提供服务： 默认YES，现在集群架构是3主3从的redis cluster由3个master平分16384个slot，每个master的小集群负责1/3的slot，对应一部分数据。cluster-require-ful-coverage:默认值yes,即需要集群完整性，方可对外提供服务 通常情况，如果这3个小集群中，任何一个(1主1从)挂了，你这个集群对外可提供的数据只有2/3了，整个集群是不完整的，redis 默认在这种情况下，是不会对外提供服务的。 如果你的诉求是，集群不完整的话也需要对外提供服务，需要将该参数设置为no，这样的话你挂了的那个小集群是不行了，但是其他的小集群仍然可以对外提供服务。
一、调优命令 Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo 。 1.1、jps JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。 1.2、jstat JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令，它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 1、基本语法： # 基本格式 jstat - <vmid> [interval] [count] # 常用示例 jstat -gc 1234 1000 10 # 每秒输出进程号1234的GC信息，共输出10次 2、查看类加载统计： # 查看类加载信息 jstat -class <pid> # 输出示例： Loaded Bytes Unloaded Bytes Time 7035 14506 0 0 3.67 # 字段说明： Loaded : 加载class的数量 Bytes : 加载class的字节数 Unloaded : 未加载class的数量 Bytes : 未加载class的字节数 Time : 加载时间 3、查看GC统计： # 查看GC统计信息 jstat -gc <pid> # 输出示例： S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 1024.0 1024.0 0.0 0.0 8192.0 8171.9 20480.0 20480.0 4864.0 3720.0 512.0 324.8 2 0.002 1 0.003 0.005 # 字段说明： S0C: Survivor0区容量 S1C: Survivor1区容量 S0U: Survivor0区已使用 S1U: Survivor1区已使用 EC : Eden区容量 EU : Eden区已使用 OC : 老年代容量 OU : 老年代已使用 MC : 方法区容量 MU : 方法区已使用 YGC: 年轻代GC次数 YGCT: 年轻代GC时间 FGC: Full GC次数 FGCT: Full GC时间 GCT: 总GC时间 4、查看GC详细信息： # 查看新生代GC统计 jstat -gcnew <pid> # 输出示例： S0C S1C S0U S1U TT MTT DSS EC EU YGC YGCT 1024.0 1024.0 0.0 1024.0 1 15 1024.0 8192.0 7623.5 2 0.002 # 字段说明： TT : Tenuring threshold（晋升阈值） MTT : Maximum tenuring threshold（最大晋升阈值） DSS : Desired survivor size（期望的survivor大小） 实际使用示例： # 监控内存使用情况 while true; do # 每5秒输出一次GC信息 jstat -gc $(jps | grep MyApp | awk '{print $1}') 5000 done # 输出到文件进行分析 jstat -gc $(jps | grep MyApp | awk '{print $1}') 1000 > gc.log 5、性能分析脚本： #!/bin/bash # gc_monitor.sh PID=$1 INTERVAL=${2:-1000} # 默认1000ms COUNT=${3:-""} # 默认持续输出 if [ -z "$PID" ]; then echo "Usage: $0 <pid> [interval] [count]" exit 1 fi # 输出表头 echo "Time YGC YGCT FGC FGCT GCT" # 监控GC情况 jstat -gc $PID $INTERVAL $COUNT | awk ' NR>1 { printf "%s %d %.3f %d %.3f %.3f\n", strftime("%H:%M:%S"), $15, # YGC $16, # YGCT $17, # FGC $18, # FGCT $19 # GCT }' 6、内存分析脚本： #!/bin/bash # memory_monitor.sh PID=$1 INTERVAL=${2:-1000} COUNT=${3:-""} if [ -z "$PID" ]; then echo "Usage: $0 <pid> [interval] [count]" exit 1 fi # 输出表头 echo "Time Eden% Old% Meta%" # 监控内存使用率 jstat -gc $PID $INTERVAL $COUNT | awk ' NR>1 { eden_used=$6 eden_capacity=$5 old_used=$8 old_capacity=$7 meta_used=$10 meta_capacity=$9 printf "%s %.1f%% %.1f%% %.1f%%\n", strftime("%H:%M:%S"), eden_used/eden_capacity*100, old_used/old_capacity*100, meta_used/meta_capacity*100 }' 7、常见用法： # 1. 查看类加载情况 jstat -class $(jps | grep MyApp | awk '{print $1}') # 2. 查看GC情况 jstat -gc $(jps | grep MyApp | awk '{print $1}') # 3. 查看JIT编译情况 jstat -compiler $(jps | grep MyApp | awk '{print $1}') # 4. 持续监控 jstat -gcutil $(jps | grep MyApp | awk '{print $1}') 1000 # 5. 输出到文件 jstat -gc $(jps | grep MyApp | awk '{print $1}') 1000 > gc.log 1.3、jmap 1、基本语法： # 基本格式 jmap [options] <pid> # 常用选项 -heap # 显示堆内存信息 -histo # 显示对象统计信息 -dump # 生成堆转储文件 查看堆内存信息： # 查看堆内存概况 jmap -heap <pid> # 输出示例： Attaching to process ID 1234, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.271-b09 using thread-local object allocation. Parallel GC with 4 thread(s) Heap Configuration: MinHeapFreeRatio = 0 MaxHeapFreeRatio = 100 MaxHeapSize = 2147483648 (2048.0MB) NewSize = 715653120 (682.5MB) MaxNewSize = 715653120 (682.5MB) OldSize = 1431830528 (1365.5MB) NewRatio = 2 SurvivorRatio = 8 MetaspaceSize = 21807104 (20.8MB) CompressedClassSpaceSize = 1073741824 (1024.0MB) MaxMetaspaceSize = 17592186044415 MB G1HeapRegionSize = 0 (0.0MB) Heap Usage: PS Young Generation Eden Space: capacity = 679477248 (648.0MB) used = 424089480 (404.44MB) free = 255387768 (243.56MB) 62.41% used From Space: capacity = 18087936 (17.25MB) used = 0 (0.0MB) free = 18087936 (17.25MB) 0.0% used To Space: capacity = 18087936 (17.25MB) used = 0 (0.0MB) free = 18087936 (17.25MB) 0.0% used PS Old Generation capacity = 1431830528 (1365.5MB) used = 474547616 (452.56MB) free = 957282912 (912.94MB) 33.14% used 2、生成堆转储文件： # 生成堆转储文件 jmap -dump:format=b,file=heap.hprof <pid> # 在发生OOM时自动生成堆转储文件 jmap -dump:live,format=b,file=heap.hprof <pid> # 使用示例： public class HeapDumpDemo { public static void main(String[] args) { List<byte[]> list = new ArrayList<>(); while (true) { list.add(new byte[1024 * 1024]); // 1MB Thread.sleep(100); } } } 3、查看对象统计信息： # 显示堆中对象统计信息 jmap -histo <pid> # 只显示存活对象 jmap -histo:live <pid> # 输出示例： num #instances #bytes class name ---------------------------------------------- 1: 500000 20000000 java.lang.String 2: 30000 15000000 byte[] 3: 1000 800000 java.lang.Class 4、实际应用脚本： #!/bin/bash # heap_monitor.sh PID=$1 DUMP_PATH=${2:-"./dumps"} INTERVAL=${3:-3600} # 默认每小时 if [ -z "$PID" ]; then echo "Usage: $0 <pid> [dump_path] [interval]" exit 1 fi mkdir -p $DUMP_PATH while true; do DATE=$(date +%Y%m%d_%H%M%S) DUMP_FILE="$DUMP_PATH/heap_$DATE.hprof" # 生成堆转储文件 jmap -dump:live,format=b,file=$DUMP_FILE $PID # 生成对象统计信息 jmap -histo:live $PID > "$DUMP_PATH/histo_$DATE.txt" sleep $INTERVAL done 5、内存泄漏分析： #!/bin/bash # leak_analyzer.sh PID=$1 THRESHOLD=${2:-90} # 默认内存使用率阈值 if [ -z "$PID" ]; then echo "Usage: $0 <pid> [threshold]" exit 1 fi while true; do # 获取堆使用率 HEAP_USAGE=$(jmap -heap $PID | grep "used" | awk '{sum+=$3} END {print sum/1024/1024}') if [ $(echo "$HEAP_USAGE > $THRESHOLD" | bc) -eq 1 ]; then echo "Memory usage above threshold: $HEAP_USAGE MB" DUMP_FILE="heap_$(date +%Y%m%d_%H%M%S).hprof" jmap -dump:live,format=b,file=$DUMP_FILE $PID fi sleep 60 done 6、常见问题分析： # 1. 查看最大的对象 jmap -histo:live <pid> | sort -k 3 -n -r | head -n 20 # 2. 检查字符串常量池 jmap -clstats <pid> # 3. 分析内存泄漏 jmap -dump:live,format=b,file=leak.hprof <pid> # 4. 查看类加载情况 jmap -clstats <pid> 7、性能优化建议： # 1. 定期监控 0 * * * * /path/to/heap_monitor.sh <pid> # 2. 设置告警 if [ $(jmap -heap <pid> | grep "used" | awk '{print $3}') -gt 1000000000 ]; then echo "High memory usage alert!" fi # 3. 自动清理旧转储文件 find /path/to/dumps -name "heap_*.hprof" -mtime +7 -delete 1.4、jhat jhat 是一个用于分析 Java 堆转储文件的工具。它可以帮助开发者查看和分析 Java 应用程序的内存使用情况。 1、基本语法： jhat [options] <heap_dump_file> 2、生成堆转储文件： 在使用 jhat 之前，首先需要生成一个堆转储文件。可以使用 jmap 或 jcmd 命令来生成堆转储文件。 # 使用 jmap 生成堆转储文件 jmap -dump:live,format=b,file=heap_dump.hprof <pid> # 或者使用 jcmd jcmd <pid> GC.heap_dump filename=heap_dump.hprof 3、使用 jhat 分析堆转储文件： # 启动 jhat 分析堆转储文件 jhat heap_dump.hprof 4、jhat 启动后的输出： 当你运行 jhat 后，它会启动一个 HTTP 服务器，通常在 http://localhost:7000 上。你可以在浏览器中访问这个地址来查看分析结果。 5、jhat 的常用选项： -J<flag>: 传递参数给 JVM。例如，-J-Xmx512m 可以设置最大堆大小。 -port <port>: 指定 jhat 服务器的端口，默认是 7000。 -h 或 --help: 显示帮助信息。 6、示例： # 生成堆转储文件 jmap -dump:live,format=b,file=heap_dump.hprof <pid> # 使用 jhat 分析堆转储文件 jhat heap_dump.hprof # 访问分析结果 # 打开浏览器，访问 http://localhost:7000 7、分析结果： 在浏览器中，你将看到以下信息： Classes: 显示堆中所有类的实例数量和占用内存。 Instances: 显示每个类的实例详细信息。 Paths: 显示从根对象到特定对象的路径，帮助分析内存泄漏。 8、使用 jhat 的注意事项： 性能: jhat 在处理大型堆转储文件时可能会消耗大量内存和时间。 过时: jhat 在某些情况下可能不再是最佳选择，考虑使用更现代的工具，如 Eclipse Memory Analyzer (MAT) 或 VisualVM。 9、现代替代方案： Eclipse Memory Analyzer (MAT): 提供更强大的分析功能和图形界面。 VisualVM: 也可以分析堆转储文件，并提供实时监控功能。 1.5、jstack jstack 是一个用于生成 Java 线程堆栈跟踪的工具，通常用于分析 Java 应用程序的线程状态和死锁情况。 1、基本语法： jstack [options] <pid> 2、查看线程堆栈： # 查看指定进程的线程堆栈 jstack <pid> 3、示例： 假设你有一个 Java 应用程序正在运行，进程 ID 为 12345，你可以使用以下命令查看线程堆栈： jstack 12345 4、输出示例： jstack 的输出将显示所有线程的状态，包括线程名称、状态、锁信息等。例如： "main" #1 prio=5 os_prio=0 tid=0x00007f8c4c001800 nid=0x1b03 waiting for monitor entry [0x00007f8c4c7f2000] java.lang.Thread.State: BLOCKED (on object monitor) at java.lang.Object.wait(Native Method) - waiting to lock <0x00000000f8c4c8b0> (a java.lang.Object) at java.lang.Object.wait(Object.java:502) at java.lang.Thread.join(Thread.java:1252) at com.example.MyClass.main(MyClass.java:10) 5、生成线程转储文件 如果想将线程堆栈输出保存到文件中，可以使用重定向： jstack 12345 > thread_dump.txt 6、检查死锁 jstack 还可以帮助你检查死锁情况。你可以在输出中查找 Found one Java-level deadlock 的信息。 # 检查死锁 jstack -l <pid> 1.6、jinfo jinfo 是一个用于获取和修改 Java 进程的配置信息的工具。它可以帮助开发者查看 JVM 参数、系统属性以及其他运行时信息。 1、基本语法： # 基本格式 jinfo [option] <pid> # 常用选项 -flag <name> # 打印指定名称的参数 -flag [+|-]<name> # 启用/禁用指定名称的参数 -flag <name>=<value> # 设置指定参数的值 -flags # 打印所有参数 -sysprops # 打印系统属性 2、查看 JVM 参数： # 查看所有 JVM 参数 jinfo -flags <pid> # 查看特定参数 jinfo -flag MaxHeapSize <pid> jinfo -flag UseG1GC <pid> # 输出示例： -XX:MaxHeapSize=2147483648 -XX:+UseG1GC 3、查看系统属性： # 查看所有系统属性 jinfo -sysprops <pid> # 输出示例： java.runtime.name=Java(TM) SE Runtime Environment java.vm.version=25.271-b09 java.home=/path/to/java user.dir=/path/to/working/directory 4、修改运行时参数： 你可以使用 -flag 选项来修改 JVM 参数。例如，修改最大堆大小： # 启用/禁用布尔类型参数 jinfo -flag +PrintGC <pid> # 启用GC日志打印 jinfo -flag -PrintGC <pid> # 禁用GC日志打印 # 设置数值类型参数 jinfo -flag MaxHeapSize=2147483648 <pid> 5、实际应用示例： public class JInfoDemo { public static void main(String[] args) throws Exception { // 打印进程ID String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; System.out.println("Process ID: " + pid); // 保持程序运行 Thread.sleep(Integer.MAX_VALUE); } } 6、监控脚本： #!/bin/bash # jvm_monitor.sh PID=$1 INTERVAL=${2:-60} # 默认60秒 if [ -z "$PID" ]; then echo "Usage: $0 <pid> [interval]" exit 1 fi while true; do echo "=== $(date) ===" # 打印堆内存使用情况 jinfo -flag MaxHeapSize $PID jinfo -flag HeapSize $PID # 打印GC相关参数 jinfo -flag UseG1GC $PID jinfo -flag UseParallelGC $PID sleep $INTERVAL done 7、参数检查脚本： #!/bin/bash # check_params.sh PID=$1 PARAM_FILE="jvm_params.txt" if [ -z "$PID" ]; then echo "Usage: $0 <pid>" exit 1 fi # 保存当前参数 jinfo -flags $PID > $PARAM_FILE # 检查关键参数 echo "Checking critical parameters..." # 检查堆大小 MAX_HEAP=$(grep "MaxHeapSize" $PARAM_FILE | awk '{print $2}') if [ $MAX_HEAP -lt 2147483648 ]; then # 2GB echo "Warning: MaxHeapSize is less than 2GB" fi # 检查GC参数 if ! grep -q "UseG1GC" $PARAM_FILE; then echo "Warning: G1GC is not enabled" fi 8、常见用法： # 1. 查看基本信息 jinfo <pid> # 2. 查看所有标志 jinfo -flags <pid> # 3. 查看特定标志 jinfo -flag UseG1GC <pid> # 4. 修改标志 jinfo -flag +PrintGC <pid> # 5. 查看系统属性 jinfo -sysprops <pid> 二、调优工具 常用调优工具分为两类，jdk 自带监控工具：jconsole 和 jvisualvm，第三方有：MAT (Memory Analyzer Tool)、GChisto。 1、jconsole，Java Monitoring and Management Console 是从 java5 开始，在 JDK 中自带的 java 监控和管理控制台，用于对 JVM 中内存，线程和类等的监控 2、jvisualvm，jdk 自带全能工具，可以分析内存快照、线程快照；监控内存变化、GC 变化等。 3、MAT，Memory Analyzer Tool，一个基于 Eclipse 的内存分析工具，是一个快速、功能丰富的 Javaheap 分析工具，它可以帮助我们查找内存泄漏和减少内存消耗。 4、GChisto，一款专业分析 gc 日志的工具
一、CAS 无锁算法 public class ReentrantSpinLock { private AtomicReference<Thread> cas = new AtomicReference<Thread>(); private int count; public void lock() { Thread current = Thread.currentThread(); if (current == cas.get()) { // 如果当前线程已经获取到了锁，线程数增加一，然后返回 count++; return; } // 如果没获取到锁，则通过CAS自旋 while (!cas.compareAndSet(null, current)) { // DO nothing } } public void unlock() { Thread cur = Thread.currentThread(); if (cur == cas.get()) { if (count > 0) {// 如果大于0，表示当前线程多次获取了该锁，释放锁通过count减一来模拟 count--; } else {// 如果count==0，可以将锁释放，这样就能保证获取锁的次数与释放锁的次数是一致的了。 cas.compareAndSet(cur, null); } } } } TicketLock 解决公平性的问题。 public class TicketLockV2 { /** * 服务号 */ private AtomicInteger serviceNum = new AtomicInteger(); /** * 排队号 */ private AtomicInteger ticketNum = new AtomicInteger(); /** * 新增一个ThreadLocal，用于存储每个线程的排队号 */ private ThreadLocal<Integer> ticketNumHolder = new ThreadLocal<Integer>(); public void lock() { int currentTicketNum = ticketNum.getAndIncrement(); ticketNumHolder.set(currentTicketNum); System.out.println("LOCK " + Thread.currentThread().getName() + " currentTicketNum：" + currentTicketNum + " serviceNum：" + serviceNum.get()); while (currentTicketNum != serviceNum.get()) { Thread.onSpinWait(); // 添加自旋优化 } } public void unlock() { Integer currentTickNum = ticketNumHolder.get(); if (currentTickNum == null) { throw new IllegalMonitorStateException("应该先获取锁再释放锁"); } if (!serviceNum.compareAndSet(currentTickNum, currentTickNum + 1)) { throw new IllegalMonitorStateException("当前线程并未持有锁"); } System.out.println("UNLOCK " + Thread.currentThread().getName() + "- currentTicketNum：" + currentTickNum + "- serviceNum：" + serviceNum.get()); ticketNumHolder.remove(); // 清理ThreadLocal，防止内存泄漏 } } public class TicketLockV2Example { private static int counter = 0; private static final TicketLockV2 lock = new TicketLockV2(); public static void main(String[] args) throws InterruptedException { // 创建多个线程进行测试 int threadCount = 5; Thread[] threads = new Thread[threadCount]; for (int i = 0; i < threadCount; i++) { threads[i] = new Thread(() -> { lock.lock(); // 获取锁 counter++; // 临界区操作 lock.unlock(); // 确保在finally中释放锁 }, "Thread-" + i); } // 启动所有线程 for (Thread thread : threads) { thread.start(); } // 等待所有线程完成 for (Thread thread : threads) { thread.join(); } System.out.println("最终计数器的值: " + counter); } } 二、独占式锁 & 共享式锁 1、独占式锁（Exclusive Lock） public class ExclusiveLockExample { private final ReentrantLock lock = new ReentrantLock(); // 独占式锁 private int count = 0; public void write() { lock.lock(); // 只能被一个线程获取 try { count++; // 模拟写操作 Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + " 写入完成"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } 2、共享式锁（Shared Lock） public class SharedLockExample { private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); // 共享锁 private final Lock writeLock = rwLock.writeLock(); // 独占锁 private int data = 0; // 读操作：可以多个线程同时读 public void read() { readLock.lock(); // 获取共享锁 try { System.out.println(Thread.currentThread().getName() + " 读取数据: " + data); Thread.sleep(1000); // 模拟读取操作 } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock.unlock(); } } // 写操作：只能一个线程写 public void write(int newData) { writeLock.lock(); // 获取独占锁 try { data = newData; System.out.println(Thread.currentThread().getName() + " 写入数据: " + newData); Thread.sleep(1000); // 模拟写入操作 } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock.unlock(); } } } 使用示例： public class LockDemo { public static void main(String[] args) { SharedLockExample sharedLock = new SharedLockExample(); // 创建多个读线程 for (int i = 0; i < 3; i++) { new Thread(() -> { while (true) { sharedLock.read(); } }, "Reader-" + i).start(); } // 创建写线程 new Thread(() -> { int value = 0; while (true) { sharedLock.write(value++); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "Writer").start(); } } 独占式锁： 同一时刻只能有一个线程获取锁 适用于写操作频繁的场景 典型实现：ReentrantLock, synchronized 特点： 互斥访问 可重入 高一致性 共享式锁： 同一时刻可以有多个线程获取锁 适用于读多写少的场景 典型实现：ReentrantReadWriteLock 的读锁 特点： 并发读 读写互斥 写写互斥 三、AQS AQS (AbstractQueuedSynchronizer) 的核心概念。 3.1、AQS 的基本结构 public abstract class AbstractQueuedSynchronizer { // 同步状态 private volatile int state; // 等待队列的头节点 private transient volatile Node head; // 等待队列的尾节点 private transient volatile Node tail; // 内部类 Node，表示等待队列中的节点 static final class Node { volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 volatile Thread thread; // 当前线程 volatile int waitStatus; // 等待状态 } } 3.2、基于 AQS 实现一个简单的互斥锁 public class SimpleLock extends AbstractQueuedSynchronizer { // 尝试获取锁 protected boolean tryAcquire(int arg) { // 期望状态为0，表示锁未被占用 if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 释放锁 protected boolean tryRelease(int arg) { if (getState() == 0) { throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); return true; } // 对外提供的加锁方法 public void lock() { acquire(1); } // 对外提供的解锁方法 public void unlock() { release(1); } } 3.3、AQS 的工作流程示例： public class AQSExample { private static final SimpleLock lock = new SimpleLock(); private static int count = 0; public static void main(String[] args) { // 创建多个线程竞争锁 for (int i = 0; i < 3; i++) { new Thread(() -> { lock.lock(); try { // 临界区操作 count++; System.out.println(Thread.currentThread().getName() + " 获得锁"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "Thread-" + i).start(); } } } 3.4、AQS 的核心机制（源码） // AQS 的主要操作流程 public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); } } // 入队操作 private Node addWaiter(Node mode) { // 创建新节点 Node node = new Node(Thread.currentThread(), mode); Node pred = tail; if (pred != null) { node.prev = pred; // CAS设置尾节点 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 初始化队列 enq(node); return node; } 3.5、AQS 的主要特点 状态管理： // 获取当前同步状态 protected final int getState() { return state; } // 设置同步状态 protected final void setState(int newState) { state = newState; } // CAS 修改同步状态 protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } 队列管理： // 等待队列示意 head -> Node1 -> Node2 -> Node3 -> tail // 每个节点包含： // - 线程引用 // - 等待状态 // - 前驱和后继指针 3.6、AQS 的实际应用 // ReentrantLock 的实现示例 public class ReentrantLock { private final Sync sync; // 公平锁 public static class FairSync extends Sync { protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 检查队列中是否有前驱节点 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 处理重入 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; setState(nextc); return true; } return false; } } } AQS 的核心概念： 同步状态（State）： 使用 volatile 修饰的整数 表示当前同步状态 通过 CAS 操作进行修改 等待队列： 双向链表结构 FIFO 顺序 支持独占和共享两种模式 实现方式： 独占模式：ReentrantLock 共享模式：Semaphore、CountDownLatch 组合模式：ReentrantReadWriteLock 主要优点： 提供了标准化的同步器实现框架 简化了锁的实现 支持公平和非公平两种方式 使用 AQS 的最佳实践： 继承 AQS 实现自定义同步器时： 重写 tryAcquire/tryRelease（独占模式） 重写 tryAcquireShared/tryReleaseShared（共享模式） 根据需要实现 isHeldExclusively 注意事项： 正确维护同步状态 考虑公平性需求 处理中断和超时 四、 ConcurrentHashMap & HashMap 4.1、基本结构 // HashMap public class HashMap<K,V> { // 数组 + 链表/红黑树 Node<K,V>[] table; static class Node<K,V> { final int hash; final K key; V value; Node<K,V> next; } } // ConcurrentHashMap public class ConcurrentHashMap<K,V> { // 数组 + 链表/红黑树 + 分段锁 private transient volatile Node<K,V>[] table; static class Node<K,V> { final int hash; final K key; volatile V value; volatile Node<K,V> next; } } 4.2、并发操作对比 // HashMap 线程不安全的示例 public class HashMapThreadUnsafe { public static void main(String[] args) throws InterruptedException { Map<Integer, Integer> map = new HashMap<>(); // 多线程并发写入 Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { map.put(i, i); } }); Thread t2 = new Thread(() -> { for (int i = 1000; i < 2000; i++) { map.put(i, i); } }); t1.start(); t2.start(); t1.join(); t2.join(); // 结果可能小于2000 System.out.println("Size: " + map.size()); } } // ConcurrentHashMap 线程安全的示例 public class ConcurrentHashMapThreadSafe { public static void main(String[] args) throws InterruptedException { ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>(); // 多线程并发写入 Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { map.put(i, i); } }); Thread t2 = new Thread(() -> { for (int i = 1000; i < 2000; i++) { map.put(i, i); } }); t1.start(); t2.start(); t1.join(); t2.join(); // 结果一定是2000 System.out.println("Size: " + map.size()); } } 4.3、常见操作的实现差异 // 1. 插入操作 public class MapOperations { private HashMap<String, String> hashMap = new HashMap<>(); private ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>(); // HashMap 插入 public void putInHashMap(String key, String value) { hashMap.put(key, value); // 非线程安全 } // ConcurrentHashMap 插入 public void putInConcurrentHashMap(String key, String value) { concurrentHashMap.put(key, value); // 线程安全 } // ConcurrentHashMap 原子操作 public void atomicOperation() { // 原子性的 put-if-absent concurrentHashMap.putIfAbsent("key", "value"); // 原子性的 replace concurrentHashMap.replace("key", "oldValue", "newValue"); // 原子性的计算 concurrentHashMap.compute("key", (k, v) -> v == null ? "default" : v + "updated"); } } 4.4、实际应用场景 // 场景1：缓存实现 public class CacheImplementation { private ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>(); public Object getFromCache(String key, Supplier<Object> valueLoader) { return cache.computeIfAbsent(key, k -> valueLoader.get()); } } // 场景2：计数器 public class ConcurrentCounter { private ConcurrentHashMap<String, AtomicInteger> counters = new ConcurrentHashMap<>(); public void increment(String key) { counters.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet(); } } // 场景3：会话管理 public class SessionManager { private ConcurrentHashMap<String, UserSession> sessions = new ConcurrentHashMap<>(); public void addSession(String sessionId, UserSession session) { sessions.put(sessionId, session); } public UserSession getSession(String sessionId) { return sessions.get(sessionId); } } 4.5、性能优化场景 public class PerformanceOptimization { private ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); // 批量操作 public void batchOperation() { // 使用 forEach 进行并行处理 map.forEach((k, v) -> { // 处理每个元素 processEntry(k, v); }); // 使用 search 查找元素 String result = map.search(1, (k, v) -> { if (someCondition(k, v)) { return v; } return null; }); } } 4.6、总结 主要区别： 线程安全性： HashMap：非线程安全 ConcurrentHashMap：线程安全 实现机制： HashMap：数组 + 链表/红黑树 ConcurrentHashMap：数组 + 链表/红黑树 + CAS + synchronized 性能特点： HashMap：单线程性能好 ConcurrentHashMap：多线程性能好 使用场景： HashMap 适用于： 单线程环境 读多写少 对性能要求高 数据量较小 ConcurrentHashMap 适用于： 多线程环境 并发读写 需要线程安全 缓存实现 计数器 会话管理 注意事项： 容量规划： 初始容量设置 负载因子选择 并发级别考虑 性能优化： 避免过度同步 合理使用批量操作 注意内存使用 功能选择： 根据实际需求选择 考虑是否需要线程安全 评估性能要求
同城灾备、同城双活、两地三中心、异地多活
一、系统可用性 从架构设计的原则说起。 好的软件架构应该遵循以下 3 个原则：高性能、高可用、易扩展。 高可用通常用 2 个指标来衡量： 平均故障间隔 MTBF（Mean Time Between Failure）：表示两次故障的间隔时间，也就是系统「正常运行」的平均时间，这个时间越长，说明系统稳定性越高 故障恢复时间 MTTR（Mean Time To Repair）：表示系统发生故障后「恢复的时间」，这个值越小，故障对用户的影响越小 可用性与这两者的关系：可用性（Availability）= MTBF / (MTBF + MTTR) * 100%。 这个公式得出的结果是一个「比例」，通常我们会用「N 个 9」来描述一个系统的可用性。 系统发生故障其实是不可避免的，尤其是规模越大的系统，发生问题的概率也越大。这些故障一般体现在 3 个方面： 硬件故障：CPU、内存、磁盘、网卡、交换机、路由器 软件问题：代码 Bug、版本迭代 不可抗力：地震、水灾、火灾、战争 这些风险随时都有可能发生。所以，在面对故障时，我们的系统能否以「最快」的速度恢复，就成为了可用性的关键。 下面，从一个最简单的系统出发，一步步演化出一个支持「异地多活」的系统架构。过程中，会看到一个系统会遇到哪些可用性问题，以及为什么架构要这样演进，从而理解异地多活架构的意义。 二、单机架构 业务处于起步阶段，体量非常小，架构如下： 这个架构模型非常简单，客户端请求进来，业务应用读写数据库，返回结果，非常好理解。 但需要注意的是，这里的数据库是「单机」部署的，所以它有一个致命的缺点：一旦遭遇意外，例如磁盘损坏、操作系统异常、误删数据，那这意味着所有数据就全部「丢失」了，这个损失是巨大的。 可以对数据做备份，把数据库文件「定期」cp 到另一台机器上，这样，即使原机器丢失数据，你依旧可以通过备份把数据「恢复」回来，以此保证数据安全。 这个方案实施起来虽然比较简单，但存在 2 个问题： 恢复需要时间：业务需先停机，再恢复数据，停机时间取决于恢复的速度，恢复期间服务「不可用」 数据不完整：因为是定期备份，数据肯定不是「最新」的，数据完整程度取决于备份的周期 很明显，数据库越大，意味故障恢复时间越久。那按照前面提到的「高可用」标准，这个方案可能连 1 个 9 都达不到，远远无法满足我们对可用性的要求。 那有什么更好的方案，既可以快速恢复业务？还能尽可能保证数据完整性呢？ 这时可以采用这个方案：主从副本。 三、主从副本 可以在另一台机器上，再部署一个数据库实例，让这个新实例成为原实例的「副本」，让两者保持「实时同步」，就像这样： 一般把原实例叫作主库（master），新实例叫作从库（slave）。这个方案的优点在于： 数据完整性高：主从副本实时同步，数据「差异」很小。 抗故障能力提升：主库有任何异常，从库可随时「切换」为主库，继续提供服务。 读性能提升：业务应用可直接读从库，分担主库「压力」读压力。 这个方案不错，不仅大大提高了数据库的可用性，还提升了系统的读性能。 同样的思路，「业务应用」也可以在其它机器部署一份，避免单点。因为业务应用通常是「无状态」的（不像数据库那样存储数据），所以直接部署即可，非常简单。 因为业务应用部署了多个，所以现在还需要部署一个「接入层」，来做请求的「负载均衡」（一般会使用 nginx 或 LVS），这样当一台机器宕机后，另一台机器也可以「接管」所有流量，持续提供服务。 从这个方案可以看出，提升可用性的关键思路就是：冗余。 没错，担心一个实例故障，那就部署多个实例，担心一个机器宕机，那就部署多台机器。 到这里，架构基本已演变成主流方案了，之后开发新的业务应用，都可以按照这种模式去部署。 四、风险不可控 按照前面的分析，为了避免单点故障，应用虽然部署了多台机器，但这些机器的分布情况，我们并没有去深究。 而一个机房有很多服务器，这些服务器通常会分布在一个个「机柜」上，如果你使用的这些机器，刚好在一个机柜，还是存在风险。 如果恰好连接这个机柜的交换机 / 路由器发生故障，那么你的应用依旧有「不可用」的风险。 虽然交换机 / 路由器也做了路线冗余，但不能保证一定不出问题。 部署在一个机柜有风险，那把这些机器打散，分散到不同机柜上，是不是就没问题了？ 这样确实会大大降低出问题的概率。但我们依旧不能掉以轻心，因为无论怎么分散，它们总归还是在一个相同的环境下：机房。 即使机房级别的防护已经做得足够好，但只要有「概率」出问题，那现实情况就有可能发生。虽然概率很小，但一旦真的发生，影响之大可见一斑。 看到这里你可能会想，机房出现问题的概率也太小了吧，工作了这么多年，也没让我碰上一次，有必要考虑得这么复杂吗？ 但你有没有思考这样一个问题：不同体量的系统，它们各自关注的重点是什么？ 体量很小的系统，它会重点关注「用户」规模、增长，这个阶段获取用户是一切。等用户体量上来了，这个阶段会重点关注「性能」，优化接口响应时间、页面打开速度等等，这个阶段更多是关注用户体验。 等体量再大到一定规模后你会发现，「可用性」就变得尤为重要。像微信、支付宝这种全民级的应用，如果机房发生一次故障，那整个影响范围可以说是非常巨大的。 所以，再小概率的风险，我们在提高系统可用性时，也不能忽视。 分析了风险，再说回我们的架构。那到底该怎么应对机房级别的故障呢？ 没错，还是冗余。 五、同城灾备 想要抵御「机房」级别的风险，那应对方案就不能局限在一个机房内了。 现在，需要做机房级别的冗余方案，也就是说，需要再搭建一个机房，来部署服务。 简单起见，可以在「同一个城市」再搭建一个机房，原机房我们叫作 A 机房，新机房叫 B 机房，这两个机房的网络用一条「专线」连通。 有了新机房，怎么把它用起来呢？这里还是要优先考虑「数据」风险。 为了避免 A 机房故障导致数据丢失，所以我们需要把数据在 B 机房也存一份。最简单的方案还是和前面提到的一样：备份。 A 机房的数据，定时在 B 机房做备份（拷贝数据文件），这样即使整个 A 机房遭到严重的损坏，B 机房的数据不会丢，通过备份可以把数据「恢复」回来，重启服务。 这种方案，我们称之为「冷备」。为什么叫冷备呢？因为 B 机房只做备份，不提供实时服务，它是冷的，只会在 A 机房故障时才会启用。 但备份的问题依旧和之前描述的一样：数据不完整、恢复数据期间业务不可用，整个系统的可用性还是无法得到保证。 所以，我们还是需要用「主从副本」的方式，在 B 机房部署 A 机房的数据副本，架构就变成了这样： 这样，就算整个 A 机房挂掉，我们在 B 机房也有比较「完整」的数据。 数据是保住了，但这时你需要考虑另外一个问题：如果 A 机房真挂掉了，要想保证服务不中断，你还需要在 B 机房「紧急」做这些事情： B 机房所有从库提升为主库 在 B 机房部署应用，启动服务 部署接入层，配置转发规则 DNS 指向 B 机房接入层，接入流量，业务恢复 看到了么？A 机房故障后，B 机房需要做这么多工作，你的业务才能完全「恢复」过来。 你看，整个过程需要人为介入，且需花费大量时间来操作，恢复之前整个服务还是不可用的，这个方案还是不太爽，如果能做到故障后立即「切换」，那就好了。 因此，要想缩短业务恢复的时间，你必须把这些工作在 B 机房「提前」做好，也就是说，你需要在 B 机房提前部署好接入层、业务应用，等待随时切换。架构就变成了这样： 这样的话，A 机房整个挂掉，我们只需要做 2 件事即可： B 机房所有从库提升为主库 DNS 指向 B 机房接入层，接入流量，业务恢复 这样一来，恢复速度快了很多。 到这里你会发现，B 机房从最开始的「空空如也」，演变到现在，几乎是「镜像」了一份 A 机房的所有东西，从最上层的接入层，到中间的业务应用，到最下层的存储。两个机房唯一的区别是，A 机房的存储都是主库，而 B 机房都是从库。 这种方案，我们把它叫做「热备」。 热的意思是指，B 机房处于「待命」状态，A 故障后 B 可以随时「接管」流量，继续提供服务。热备相比于冷备最大的优点是：随时可切换。 无论是冷备还是热备，因为它们都处于「备用」状态，所以我们把这两个方案统称为：同城灾备。 同城灾备的最大优势在于，我们再也不用担心「机房」级别的故障了，一个机房发生风险，我们只需把流量切换到另一个机房即可，可用性再次提高。 六、同城双活 虽然我们有了应对机房故障的解决方案，但这里有个问题是我们不能忽视的：A 机房挂掉，全部流量切到 B 机房，B 机房能否真的如我们所愿，正常提供服务？ 这是个值得思考的问题。 这就好比有两支军队 A 和 B，A 军队历经沙场，作战经验丰富，而 B 军队只是后备军，除了有军人的基本素养之外，并没有实战经验，战斗经验基本为 0。 如果 A 军队丧失战斗能力，需要 B 军队立即顶上时，作为指挥官的你，肯定也会担心 B 军队能否真的担此重任吧？ 我们的架构也是如此，此时的 B 机房虽然是随时「待命」状态，但 A 机房真的发生故障，我们要把全部流量切到 B 机房，其实是不敢百分百保证它可以「如期」工作的。 你想，我们在一个机房内部署服务，还总是发生各种各样的问题，例如：发布应用的版本不一致、系统资源不足、操作系统参数不一样等等。现在多部署一个机房，这些问题只会增多，不会减少。 另外，从「成本」的角度来看，我们新部署一个机房，需要购买服务器、内存、硬盘、带宽资源，花费成本也是非常高昂的，只让它当一个后备军，未免也太「大材小用」了！ 因此，我们需要让 B 机房也接入流量，实时提供服务，这样做的好处，一是可以实时训练这支后备军，让它达到与 A 机房相同的作战水平，随时可切换，二是 B 机房接入流量后，可以分担 A 机房的流量压力。这才是把 B 机房资源优势，发挥最大化的最好方案！ 那怎么让 B 机房也接入流量呢？很简单，就是把 B 机房的接入层 IP 地址，加入到 DNS 中，这样，B 机房从上层就可以有流量进来了。 但这里有一个问题：别忘了，B 机房的存储，现在可都是 A 机房的「从库」，从库默认可都是「不可写」的，B 机房的写请求打到本机房存储上，肯定会报错，这还是不符合我们预期。怎么办？ 这时，你就需要在「业务应用」层做改造了。 你的业务应用在操作数据库时，需要区分「读写分离」（一般用中间件实现），即两个机房的「读」流量，可以读任意机房的存储，但「写」流量，只允许写 A 机房，因为主库在 A 机房。 这会涉及到你用的所有存储，例如项目中用到了 MySQL、Redis、MongoDB 等等，操作这些数据库，都需要区分读写请求，所以这块需要一定的业务「改造」成本。 因为 A 机房的存储都是主库，所以我们把 A 机房叫做「主机房」，B 机房叫「从机房」。 两个机房部署在「同城」，物理距离比较近，而且两个机房用「专线」网络连接，虽然跨机房访问的延迟，比单个机房内要大一些，但整体的延迟还是可以接受的。 业务改造完成后，B 机房可以慢慢接入流量，从 10%、30%、50% 逐渐覆盖到 100%，你可以持续观察 B 机房的业务是否存在问题，有问题及时修复，逐渐让 B 机房的工作能力，达到和 A 机房相同水平。 现在，因为 B 机房实时接入了流量，此时如果 A 机房挂了，那我们就可以「大胆」地把 A 的流量，全部切换到 B 机房，完成快速切换！ 到这里你可以看到，我们部署的 B 机房，在物理上虽然与 A 有一定距离，但整个系统从「逻辑」上来看，我们是把这两个机房看做一个「整体」来规划的，也就是说，相当于把 2 个机房当作 1 个机房来用。 这种架构方案，比前面的同城灾备更「进了一步」，B 机房实时接入了流量，还能应对随时的故障切换，这种方案我们把它叫做「同城双活」。 因为两个机房都能处理业务请求，这对我们系统的内部维护、改造、升级提供了更多的可实施空间（流量随时切换），现在，整个系统的弹性也变大了。 那这种架构有什么问题呢？ 七、两地三中心 还是回到风险上来说。 虽然我们把 2 个机房当做一个整体来规划，但这 2 个机房在物理层面上，还是处于「一个城市」内，如果是整个城市发生自然灾害，例如地震、水灾（河南水灾刚过去不久），那 2 个机房依旧存在「全局覆没」的风险。 真是防不胜防啊？怎么办？没办法，继续冗余。 但这次冗余机房，就不能部署在同一个城市了，你需要把它放到距离更远的地方，部署在「异地」。 通常建议两个机房的距离要在 1000 公里以上，这样才能应对城市级别的灾难。 假设之前的 A、B 机房在北京，那这次新部署的 C 机房可以放在上海。 按照前面的思路，把 C 机房用起来，最简单粗暴的方案还就是做「冷备」，即定时把 A、B 机房的数据，在 C 机房做备份，防止数据丢失。 这种方案，就是我们经常听到的「两地三中心」。 两地是指 2 个城市，三中心是指有 3 个机房，其中 2 个机房在同一个城市，并且同时提供服务，第 3 个机房部署在异地，只做数据灾备。 这种架构方案，通常用在银行、金融、政企相关的项目中。它的问题还是前面所说的，启用灾备机房需要时间，而且启用后的服务，不确定能否如期工作。 所以，要想真正的抵御城市级别的故障，越来越多的互联网公司，开始实施「异地双活」。 八、伪异地双活 这里，我们还是分析 2 个机房的架构情况。我们不再把 A、B 机房部署在同一个城市，而是分开部署，例如 A 机房放在北京，B 机房放在上海。 前面我们讲了同城双活，那异地双活是不是直接「照搬」同城双活的模式去部署就可以了呢？ 如果还是按照同城双活的架构来部署，那异地双活的架构就是这样的： 注意看，两个机房的网络是通过「跨城专线」连通的。 此时两个机房都接入流量，那上海机房的请求，可能要去读写北京机房的存储，这里存在一个很大的问题：网络延迟。 因为两个机房距离较远，受到物理距离的限制，现在，两地之间的网络延迟就变成了「不可忽视」的因素了。 北京到上海的距离大约 1300 公里，即使架设一条高速的「网络专线」，光纤以光速传输，一个来回也需要近 10ms 的延迟。 况且，网络线路之间还会经历各种路由器、交换机等网络设备，实际延迟可能会达到 30ms ~ 100ms，如果网络发生抖动，延迟甚至会达到 1 秒。 不止是延迟，远距离的网络专线质量，是远远达不到机房内网络质量的，专线网络经常会发生延迟、丢包、甚至中断的情况。总之，不能过度信任和依赖「跨城专线」。 你可能会问，这点延迟对业务影响很大吗？影响非常大！ 试想，一个客户端请求打到上海机房，上海机房要去读写北京机房的存储，一次跨机房访问延迟就达到了 30ms，这大致是机房内网网络（0.5 ms）访问速度的 60 倍（30ms / 0.5ms），一次请求慢 60 倍，来回往返就要慢 100 倍以上。 而我们在 App 打开一个页面，可能会访问后端几十个 API，每次都跨机房访问，整个页面的响应延迟有可能就达到了秒级，这个性能简直惨不忍睹，难以接受。 看到了么，虽然我们只是简单的把机房部署在了「异地」，但「同城双活」的架构模型，在这里就不适用了，还是按照这种方式部署，这是「伪异地双活」！ 那如何做到真正的异地双活呢？ 九、真正的异地双活 既然「跨机房」调用延迟是不容忽视的因素，那我们只能尽量避免跨机房「调用」，规避这个延迟问题。 也就是说，上海机房的应用，不能再「跨机房」去读写北京机房的存储，只允许读写上海本地的存储，实现「就近访问」，这样才能避免延迟问题。 还是之前提到的问题：上海机房存储都是从库，不允许写入啊，除非我们只允许上海机房接入「读流量」，不接收「写流量」，否则无法满足不再跨机房的要求。 很显然，只让上海机房接收读流量的方案不现实，因为很少有项目是只有读流量，没有写流量的。所以这种方案还是不行，这怎么办？ 此时，你就必须在「存储层」做改造了。 要想上海机房读写本机房的存储，那上海机房的存储不能再是北京机房的从库，而是也要变为「主库」。 你没看错，两个机房的存储必须都是「主库」，而且两个机房的数据还要「互相同步」数据，即客户端无论写哪一个机房，都能把这条数据同步到另一个机房。 因为只有两个机房都拥有「全量数据」，才能支持任意切换机房，持续提供服务。 怎么实现这种「双主」架构呢？它们之间如何互相同步数据？ 如果你对 MySQL 有所了解，MySQL 本身就提供了双主架构，它支持双向复制数据，但平时用的并不多。而且 Redis、MongoDB 等数据库并没有提供这个功能，所以，你必须开发对应的「数据同步中间件」来实现双向同步的功能。 此外，除了数据库这种有状态的软件之外，你的项目通常还会使用到消息队列，例如 RabbitMQ、Kafka，这些也是有状态的服务，所以它们也需要开发双向同步的中间件，支持任意机房写入数据，同步至另一个机房。 看到了么，这一下子复杂度就上来了，单单针对每个数据库、队列开发同步中间件，就需要投入很大精力了。 业界也开源出了很多数据同步中间件，例如阿里的 Canal、RedisShake、MongoShake，可分别在两个机房同步 MySQL、Redis、MongoDB 数据。 很多有能力的公司，也会采用自研同步中间件的方式来做，例如饿了么、携程、美团都开发了自己的同步中间件。 我也有幸参与设计开发了 MySQL、Redis/Codis、MongoDB 的同步中间件，有时间写一篇文章详细聊聊实现细节，欢迎持续关注。:) 现在，整个架构就变成了这样： 注意看，两个机房的存储层都互相同步数据的。有了数据同步中间件，就可以达到这样的效果： 北京机房写入 X = 1 上海机房写入 Y = 2 数据通过中间件双向同步 北京、上海机房都有 X = 1、Y = 2 的数据 这里我们用中间件双向同步数据，就不用再担心专线问题，专线出问题，我们的中间件可以自动重试，直到成功，达到数据最终一致。 但这里还会遇到一个问题，两个机房都可以写，操作的不是同一条数据那还好，如果修改的是同一条的数据，发生冲突怎么办？ 用户短时间内发了 2 个修改请求，都是修改同一条数据 一个请求落在北京机房，修改 X = 1（还未同步到上海机房） 另一个请求落在上海机房，修改 X = 2（还未同步到北京机房） 两个机房以哪个为准？ 也就是说，在很短的时间内，同一个用户修改同一条数据，两个机房无法确认谁先谁后，数据发生「冲突」。 这是一个很严重的问题，系统发生故障并不可怕，可怕的是数据发生「错误」，因为修正数据的成本太高了。我们一定要避免这种情况的发生。解决这个问题，有 2 个方案。 第一个方案，数据同步中间件要有自动「合并」数据、解决「冲突」的能力。 这个方案实现起来比较复杂，要想合并数据，就必须要区分出「先后」顺序。我们很容易想到的方案，就是以「时间」为标尺，以「后到达」的请求为准。 但这种方案需要两个机房的「时钟」严格保持一致才行，否则很容易出现问题。例如： 第 1 个请求落到北京机房，北京机房时钟是 10:01，修改 X = 1 第 2 个请求落到上海机房，上海机房时钟是 10:00，修改 X = 2 因为北京机房的时间「更晚」，那最终结果就会是 X = 1。但这里其实应该以第 2 个请求为准，X = 2 才对。 可见，完全「依赖」时钟的冲突解决方案，不太严谨。 所以，通常会采用第二种方案，从「源头」就避免数据冲突的发生。 十、如何实施异地双活 从源头避免数据冲突的思路是：在最上层接入流量时，就不要让冲突的情况发生。 具体来讲就是，要在最上层就把用户「区分」开，部分用户请求固定打到北京机房，其它用户请求固定打到上海 机房，进入某个机房的用户请求，之后的所有业务操作，都在这一个机房内完成，从根源上避免「跨机房」。 所以这时，你需要在接入层之上，再部署一个「路由层」（通常部署在云服务器上），自己可以配置路由规则，把用户「分流」到不同的机房内。 但这个路由规则，具体怎么定呢？有很多种实现方式，最常见的 3 类： 按业务类型分片 直接哈希分片 按地理位置分片 10.1、按业务类型分片 这种方案是指，按应用的「业务类型」来划分。 举例：假设我们一共有 4 个应用，北京和上海机房都部署这些应用。但应用 1、2 只在北京机房接入流量，在上海机房只是热备。应用 3、4 只在上海机房接入流量，在北京机房是热备。 这样一来，应用 1、2 的所有业务请求，只读写北京机房存储，应用 3、4 的所有请求，只会读写上海机房存储。 这样按业务类型分片，也可以避免同一个用户修改同一条数据。 这里按业务类型在不同机房接入流量，还需要考虑多个应用之间的依赖关系，要尽可能的把完成「相关」业务的应用部署在同一个机房，避免跨机房调用。 例如，订单、支付服务有依赖关系，会产生互相调用，那这 2 个服务在 A 机房接入流量。社区、发帖服务有依赖关系，那这 2 个服务在 B 机房接入流量。 10.2、直接哈希分片 这种方案就是，最上层的路由层，会根据用户 ID 计算「哈希」取模，然后从路由表中找到对应的机房，之后把请求转发到指定机房内。 举例：一共 200 个用户，根据用户 ID 计算哈希值，然后根据路由规则，把用户 1 - 100 路由到北京机房，101 - 200 用户路由到上海机房，这样，就避免了同一个用户修改同一条数据的情况发生。 10.3、按地理位置分片 这种方案，非常适合与地理位置密切相关的业务，例如打车、外卖服务就非常适合这种方案。 拿外卖服务举例，你要点外卖肯定是「就近」点餐，整个业务范围相关的有商家、用户、骑手，它们都是在相同的地理位置内的。 针对这种特征，就可以在最上层，按用户的「地理位置」来做分片，分散到不同的机房。 举例：北京、河北地区的用户点餐，请求只会打到北京机房，而上海、浙江地区的用户，请求则只会打到上海机房。这样的分片规则，也能避免数据冲突。 提醒：这 3 种常见的分片规则，第一次看不太好理解，建议配合图多理解几遍。搞懂这 3 个分片规则，你才能真正明白怎么做异地多活。 总之，分片的核心思路在于，让同一个用户的相关请求，只在一个机房内完成所有业务「闭环」，不再出现「跨机房」访问。 阿里在实施这种方案时，给它起了个名字，叫做「单元化」。 当然，最上层的路由层把用户分片后，理论来说同一个用户只会落在同一个机房内，但不排除程序 Bug 导致用户会在两个机房「漂移」。 安全起见，每个机房在写存储时，还需要有一套机制，能够检测「数据归属」，应用层操作存储时，需要通过中间件来做「兜底」，避免不该写本机房的情况发生。（篇幅限制，这里不展开讲，理解思路即可） 现在，两个机房就可以都接收「读写」流量（做好分片的请求），底层存储保持「双向」同步，两个机房都拥有全量数据，当任意机房故障时，另一个机房就可以「接管」全部流量，实现快速切换。 不仅如此，因为机房部署在异地，我们还可以更细化地「优化」路由规则，让用户访问就近的机房，这样整个系统的性能也会大大提升。 这里还有一种情况，是无法做数据分片的：全局数据。例如系统配置、商品库存这类需要强一致的数据，这类服务依旧只能采用写主机房，读从机房的方案，不做双活。 双活的重点，是要优先保证「核心」业务先实现双活，并不是「全部」业务实现双活。 至此，我们才算实现了真正的「异地双活」！ 到这里可以看出，完成这样一套架构，需要投入的成本是巨大的。 路由规则、路由转发、数据同步中间件、数据校验兜底策略，不仅需要开发强大的中间件，同时还要业务配合改造（业务边界划分、依赖拆分）等一些列工作，没有足够的人力物力，这套架构很难实施。 十一、异地多活 理解了异地双活，那「异地多活」顾名思义，就是在异地双活的基础上，部署多个机房即可。架构变成了这样： 这些服务按照「单元化」的部署方式，可以让每个机房部署在任意地区，随时扩展新机房，你只需要在最上层定义好分片规则就好了。 但这里还有一个小问题，随着扩展的机房越来越多，当一个机房写入数据后，需要同步的机房也越来越多，这个实现复杂度会比较高。 所以业界又把这一架构又做了进一步优化，把「网状」架构升级为「星状」： 这种方案必须设立一个「中心机房」，任意机房写入数据后，都只同步到中心机房，再由中心机房同步至其它机房。 这样做的好处是，一个机房写入数据，只需要同步数据到中心机房即可，不需要再关心一共部署了多少个机房，实现复杂度大大「简化」。 但与此同时，这个中心机房的「稳定性」要求会比较高。不过也还好，即使中心机房发生故障，我们也可以把任意一个机房，提升为中心机房，继续按照之前的架构提供服务。 至此，我们的系统彻底实现了「异地多活」！ 多活的优势在于，可以任意扩展机房「就近」部署。任意机房发生故障，可以完成快速「切换」，大大提高了系统的可用性。 同时，我们也再也不用担心系统规模的增长，因为这套架构具有极强的「扩展能力」。 十二、总结 1、一个好的软件架构，应该遵循高性能、高可用、易扩展 3 大原则，其中「高可用」在系统规模变得越来越大时，变得尤为重要 2、系统发生故障并不可怕，能以「最快」的速度恢复，才是高可用追求的目标，异地多活是实现高可用的有效手段 3、提升高可用的核心是「冗余」，备份、主从副本、同城灾备、同城双活、两地三中心、异地双活，异地多活都是在做冗余 4、同城灾备分为「冷备」和「热备」，冷备只备份数据，不提供服务，热备实时同步数据，并做好随时切换的准备 5、同城双活比灾备的优势在于，两个机房都可以接入「读写」流量，提高可用性的同时，还提升了系统性能。虽然物理上是两个机房，但「逻辑」上还是当做一个机房来用 6、两地三中心是在同城双活的基础上，额外部署一个异地机房做「灾备」，用来抵御「城市」级别的灾害，但启用灾备机房需要时间 7、异地双活才是抵御「城市」级别灾害的更好方案，两个机房同时提供服务，故障随时可切换，可用性高。但实现也最复杂，理解了异地双活，才能彻底理解异地多活 8、异地多活是在异地双活的基础上，任意扩展多个机房，不仅又提高了可用性，还能应对更大规模的流量的压力，扩展性最强，是实现高可用的最终方案 参考： http://kaito-kidd.com/2021/10/15/what-is-the-multi-site-high-availability-design/)[http://kaito-kidd.com/2021/10/15/what-is-the-multi-site-high-availability-design/
一、ClickHouse 入门 ClickHouse 是俄罗斯的 Yandex 于 2016 年开源的列式存储数据库（DBMS），使用 C++语言编写，主要用于在线分析处理查询（OLAP），能够使用 SQL 查询实时生成分析数据报告。 1.1、ClickHouse 的特点 1.1.1、列式存储 以下面的表为例： Id Name Age 1 张三 18 2 李四 22 3 王五 34 1）采用行式存储时，数据在磁盘上的组织结构为： 好处是想查某个人所有的属性时，可以通过一次磁盘查找加顺序读取就可以。但是当想查所有人的年龄时，需要不停的查找，或者全表扫描才行，遍历的很多数据都是不需要的。 2）采用列式存储时，数据在磁盘上的组织结构为： 3）列式储存的好处： 对于列的聚合，计数，求和等统计操作原因优于行式存储。 由于某一列的数据类型都是相同的，针对于数据存储更容易进行数据压缩，每一列选择更优的数据压缩算法，大大提高了数据的压缩比重。 由于数据压缩比更好，一方面节省了磁盘空间，另一方面对于 cache 也有了更大的发挥空间。 1.1.2、DBMS 的功能 几乎覆盖了标准 SQL 的大部分语法，包括 DDL 和 DML，以及配套的各种函数，用户管理及权限管理，数据的备份与恢复。 1.1.3、多样化引擎 ClickHouse 和 MySQL 类似，把表级的存储引擎插件化，根据表的不同需求可以设定不同的存储引擎。目前包括合并树、日志、接口和其他四大类 20 多种引擎。 1.1.4、高吞吐写入能力 ClickHouse 采用类 LSM Tree的结构，数据写入后定期在后台 Compaction。通过类 LSM tree的结构，ClickHouse 在数据导入时全部是顺序 append 写，写入后数据段不可更改，在后台compaction 时也是多个段 merge sort 后顺序写回磁盘。顺序写的特性，充分利用了磁盘的吞吐能力，即便在 HDD 上也有着优异的写入性能。 官方公开 benchmark 测试显示能够达到 50MB-200MB/s 的写入吞吐能力，按照每行100Byte 估算，大约相当于 50W-200W 条/s 的写入速度。 1.1.5、数据分区与线程级并行 ClickHouse 将数据划分为多个 partition，每个 partition 再进一步划分为多个 index granularity(索引粒度)，然后通过多个 CPU核心分别处理其中的一部分来实现并行数据处理。在这种设计下，单条 Query 就能利用整机所有 CPU。极致的并行处理能力，极大的降低了查询延时。 所以，ClickHouse 即使对于大量数据的查询也能够化整为零平行处理。但是有一个弊端就是对于单条查询使用多 cpu，就不利于同时并发多条查询。所以对于高 qps 的查询业务，ClickHouse 并不是强项。 1.1.6、性能对比 ClickHouse 像很多 OLAP 数据库一样，单表查询速度优于关联查询，而且 ClickHouse 的两者差距更为明显。 二、ClickHouse 的安装 2.1、准备工作 2.1.1、确定防火墙处于关闭状态 2.1.2、CentOS 取消打开文件数限制 （1）在 hadoop102 的 /etc/security/limits.conf 文件的末尾加入以下内容 [atguigu@hadoop102 ~]$ sudo vim /etc/security/limits.conf * soft nofile 65536 * hard nofile 65536 * soft nproc 131072 * hard nproc 131072 （2）在 hadoop102 的/etc/security/limits.d/20-nproc.conf 文件的末尾加入以下内容 [atguigu@hadoop102 ~]$ sudo vim /etc/security/limits.d/20-nproc.conf * soft nofile 65536 * hard nofile 65536 * soft nproc 131072 * hard nproc 131072 2.1.3、安装依赖 sudo yum install -y libtool sudo yum install -y *unixODBC* 2.1.4、CentOS 取消 SELINUX （1）修改/etc/selinux/config 中的 SELINUX=disabled [atguigu@hadoop102 ~]$ sudo vim /etc/selinux/config SELINUX=disabled 2.2、单机安装 官网：https://clickhouse.tech/ 下载地址：http://repo.red-soft.biz/repos/clickhouse/stable/el7/ 2.2.1、在 hadoop102 的/opt/software 下创建 clickhouse 目录 2.2.2、上传 2.2.3、将安装文件同步到 hadoop103、hadoop104 [atguigu@hadoop102 software]$ xsync clickhouse 2.2.4、分别在三台机子上安装这 4 个 rpm 文件 [atguigu@hadoop102 clickhouse]$ sudo rpm -ivh *.rpm 2.2.5、修改配置文件 [atguigu@hadoop102 clickhouse]$ sudo vim /etc/clickhouse-server/config.xml （1）把 <listen_host>::</listen_host> 的注释打开，这样的话才能让 ClickHouse 被除本机以外的服务器访问。 （2）分发配置文件sudo /home/atguigu/bin/xsync /etc/clickhouse-server/config.xml 在这个文件中，有 ClickHouse 的一些默认路径配置，比较重要的 数据文件路径：/var/lib/clickhouse/ 日志文件路径：/var/log/clickhouse-server/clickhouse-server.log 2.2.6、启动 Server [atguigu@hadoop102 clickhouse]$ sudo systemctl start clickhouse-server 2.2.7、三台机器上关闭开机自启 [atguigu@hadoop102 clickhouse]$sudo systemctl disable clickhouse-server 2.2.8、使用 client 连接 server [atguigu@hadoop102 clickhouse]$ clickhouse-client -m 四、表引擎 4.1、表引擎的使用 表引擎是 ClickHouse 的一大特色。可以说， 表引擎决定了如何存储表的数据。包括： 数据的存储方式和位置，写到哪里以及从哪里读取数据。 支持哪些查询以及如何支持。 并发数据访问。 索引的使用（如果存在）。 是否可以执行多线程请求。 数据复制参数。 表引擎的使用方式就是必须显式在创建表时定义该表使用的引擎，以及引擎使用的相关参数。 特别注意：引擎的名称大小写敏感。 4.2、TinyLog 以列文件的形式保存在磁盘上，不支持索引，没有并发控制。一般保存少量数据的小表，生产环境上作用有限。可以用于平时练习测试用。 如： create table t_tinylog ( id String, name String) engine=TinyLog; 4.3、Memory 内存引擎，数据以未压缩的原始形式直接保存在内存当中，服务器重启数据就会消失。读写操作不会相互阻塞，不支持索引。简单查询下有非常非常高的性能表现（超过 10G/s）。 一般用到它的地方不多，除了用来测试，就是在需要非常高的性能，同时数据量又不太大（上限大概 1 亿行）的场景。 4.4、MergeTree ClickHouse 中最强大的表引擎当属 MergeTree（合并树）引擎及该系列（*MergeTree）中的其他引擎，支持索引和分区，地位可以相当于 innodb 之于 Mysql。而且基于 MergeTree，还衍生除了很多小弟，也是非常有特色的引擎。 1）建表语句 create table t_order_mt( id UInt32, sku_id String, total_amount Decimal(16,2), create_time Datetime ) engine =MergeTree partition by toYYYYMMDD(create_time) primary key (id) order by (id,sku_id); 2）插入数据 insert into t_order_mt values (101,'sku_001',1000.00,'2020-06-01 12:00:00') , (102,'sku_002',2000.00,'2020-06-01 11:00:00'), (102,'sku_004',2500.00,'2020-06-01 12:00:00'), (102,'sku_002',2000.00,'2020-06-01 13:00:00'), (102,'sku_002',12000.00,'2020-06-01 13:00:00'), (102,'sku_002',600.00,'2020-06-02 12:00:00'); MergeTree 其实还有很多参数(绝大多数用默认值即可)，但是三个参数是更加重要的，也涉及了关于 MergeTree 的很多概念。 4.4.1、partition by 分区(可选) 1）作用 分区的目的主要是降低扫描的范围，优化查询速度。 2）如果不填 只会使用一个分区。 3）分区目录 MergeTree 是以列文件+索引文件+表定义文件组成的，但是如果设定了分区那么这些文件就会保存到不同的分区目录中。 4）并行 分区后，面对涉及跨分区的查询统计，ClickHouse 会以分区为单位并行处理。 5）数据写入与分区合并 任何一个批次的数据写入都会产生一个临时分区，不会纳入任何一个已有的分区。写入后的某个时刻（大概 10-15 分钟后），ClickHouse 会自动执行合并操作（等不及也可以手动通过 optimize 执行），把临时分区的数据，合并到已有分区中。 optimize table xxxx final; 6）例如 再次执行上面的插入操作 insert into t_order_mt values (101,'sku_001',1000.00,'2020-06-01 12:00:00') , (102,'sku_002',2000.00,'2020-06-01 11:00:00'), (102,'sku_004',2500.00,'2020-06-01 12:00:00'), (102,'sku_002',2000.00,'2020-06-01 13:00:00'), (102,'sku_002',12000.00,'2020-06-01 13:00:00'), (102,'sku_002',600.00,'2020-06-02 12:00:00'); 查看数据并没有纳入任何分区 手动 optimize 之后 hadoop102 :) optimize table t_order_mt final; 再次查询 4.4.2、primary key 主键(可选) ClickHouse 中的主键，和其他数据库不太一样，它只提供了数据的一级索引，但是却不是唯一约束。这就意味着是可以存在相同 primary key 的数据的。 主键的设定主要依据是查询语句中的 where 条件。 根据条件通过对主键进行某种形式的二分查找，能够定位到对应的 index granularity,避免了全表扫描。 index granularity： 直接翻译的话就是索引粒度，指在稀疏索引中两个相邻索引对应数据的间隔。ClickHouse 中的MergeTree 默认是 8192。官方不建议修改这个值，除非该列存在大量重复值，比如在一个分区中几万行才有一个不同数据。 稀疏索引： 稀疏索引的好处就是可以用很少的索引数据，定位更多的数据，代价就是只能定位到索引粒度的第一行，然后再进行进行一点扫描。 4.4.3、order by（必选） order by 设定了分区内的数据按照哪些字段顺序进行有序保存。 order by 是 MergeTree 中唯一一个必填项，甚至比 primary key 还重要，因为当用户不设置主键的情况，很多处理会依照 order by 的字段进行处理（比如后面会讲的去重和汇总）。 要求：主键必须是 order by 字段的前缀字段。 比如 order by 字段是 (id,sku_id) 那么主键必须是 id 或者(id,sku_id) 4.4.4、二级索引 目前在 ClickHouse 的官网上二级索引的功能在 v20.1.2.4 之前是被标注为实验性的，在这个版本之后默认是开启的。 1）老版本使用二级索引前需要增加设置 是否允许使用实验性的二级索引（v20.1.2.4 开始，这个参数已被删除，默认开启） set allow_experimental_data_skipping_indices=1; 2）创建测试表 create table t_order_mt2( id UInt32, sku_id String, total_amount Decimal(16,2), create_time Datetime, INDEX a total_amount TYPE minmax GRANULARITY 5 ) engine =MergeTree partition by toYYYYMMDD(create_time) primary key (id) order by (id, sku_id); 其中 GRANULARITY N 是设定二级索引对于一级索引粒度的粒度。 3）插入数据 insert into t_order_mt2 values (101,'sku_001',1000.00,'2020-06-01 12:00:00') , (102,'sku_002',2000.00,'2020-06-01 11:00:00'), (102,'sku_004',2500.00,'2020-06-01 12:00:00'), (102,'sku_002',2000.00,'2020-06-01 13:00:00'), (102,'sku_002',12000.00,'2020-06-01 13:00:00'), (102,'sku_002',600.00,'2020-06-02 12:00:00'); 4）对比效果 那么在使用下面语句进行测试，可以看出二级索引能够为非主键字段的查询发挥作用。 [atguigu@hadoop102 lib]$ clickhouse-client --send_logs_level=trace <<< 'select * from t_order_mt2 where total_amount > toDecimal32(900., 2)'; 4.4.5、数据 TTL TTL 即 Time To Live，MergeTree 提供了可以管理数据表或者列的生命周期的功能。 1）列级别 TTL （1）创建测试表 create table t_order_mt3( id UInt32, sku_id String, total_amount Decimal(16,2) TTL create_time+interval 10 SECOND, create_time Datetime ) engine =MergeTree partition by toYYYYMMDD(create_time) primary key (id) order by (id, sku_id); （2）插入数据（注意：根据实际时间改变） insert into t_order_mt3 values (106,'sku_001',1000.00,'2020-06-12 22:52:30'), (107,'sku_002',2000.00,'2020-06-12 22:52:30'), (110,'sku_003',600.00,'2020-06-13 12:00:00'); （3）手动合并，查看效果 到期后，指定的字段数据归 2）表级 TTL 下面的这条语句是数据会在 create_time 之后 10 秒丢失 alter table t_order_mt3 MODIFY TTL create_time + INTERVAL 10 SECOND; 涉及判断的字段必须是 Date 或者 Datetime 类型，推荐使用分区的日期字段。 能够使用的时间周期： - SECOND - MINUTE - HOUR - DAY - WEEK - MONTH - QUARTER - YEAR 4.5、ReplacingMergeTree ReplacingMergeTree 是 MergeTree 的一个变种，它存储特性完全继承 MergeTree，只是多了一个去重的功能。 尽管 MergeTree 可以设置主键，但是 primary key 其实没有唯一约束的功能。如果你想处理掉重复的数据，可以借助这个ReplacingMergeTree。 1）去重时机 数据的去重只会在合并的过程中出现。合并会在未知的时间在后台进行，所以你无法预先作出计划。有一些数据可能仍未被处理。 2）去重范围 如果表经过了分区，去重只会在分区内部进行去重，不能执行跨分区的去重。 所以 ReplacingMergeTree 能力有限， ReplacingMergeTree 适用于在后台清除重复的数据以节省空间，但是它不保证没有重复的数据出现。 3）案例演示 （1）创建表 create table t_order_rmt( id UInt32, sku_id String, total_amount Decimal(16,2) , create_time Datetime ) engine =ReplacingMergeTree(create_time) partition by toYYYYMMDD(create_time) primary key (id) order by (id, sku_id); ReplacingMergeTree() 填入的参数为版本字段，重复数据保留版本字段值最大的。如果不填版本字段，默认按照插入顺序保留最后一条。 （2）向表中插入数据 insert into t_order_rmt values (101,'sku_001',1000.00,'2020-06-01 12:00:00') , (102,'sku_002',2000.00,'2020-06-01 11:00:00'), (102,'sku_004',2500.00,'2020-06-01 12:00:00'), (102,'sku_002',2000.00,'2020-06-01 13:00:00'), (102,'sku_002',12000.00,'2020-06-01 13:00:00'), (102,'sku_002',600.00,'2020-06-02 12:00:00'); （3）执行第一次查询 hadoop102 :) select * from t_order_rmt; （4）手动合并 OPTIMIZE TABLE t_order_rmt FINAL; （5）再执行一次查询 hadoop102 :) select * from t_order_rmt; 4）通过测试得到结论 实际上是使用 order by 字段作为唯一键 去重不能跨分区 只有同一批插入（新版本）或合并分区时才会进行去重 认定重复的数据保留，版本字段值最大的 如果版本字段相同则按插入顺序保留最后一笔 4.6、SummingMergeTree 对于不查询明细，只关心以维度进行汇总聚合结果的场景。如果只使用普通的MergeTree的话，无论是存储空间的开销，还是查询时临时聚合的开销都比较大。 ClickHouse 为了这种场景，提供了一种能够“预聚合”的引擎 SummingMergeTree 1）案例演示 （1）创建表 create table t_order_smt( id UInt32, sku_id String, total_amount Decimal(16,2) , create_time Datetime ) engine =SummingMergeTree(total_amount) partition by toYYYYMMDD(create_time) primary key (id) order by (id,sku_id ); （2）插入数据 insert into t_order_smt values (101,'sku_001',1000.00,'2020-06-01 12:00:00'), (102,'sku_002',2000.00,'2020-06-01 11:00:00'), (102,'sku_004',2500.00,'2020-06-01 12:00:00'), (102,'sku_002',2000.00,'2020-06-01 13:00:00'), (102,'sku_002',12000.00,'2020-06-01 13:00:00'), (102,'sku_002',600.00,'2020-06-02 12:00:00'); （3）执行第一次查询 hadoop102 :) select * from t_order_smt; （4）手动合并 OPTIMIZE TABLE t_order_smt FINAL; （5）再执行一次查询 hadoop102 :) select * from t_order_smt; 2）通过结果可以得到以下结论 以 SummingMergeTree（）中指定的列作为汇总数据列 可以填写多列必须数字列，如果不填，以所有非维度列且为数字列的字段为汇总数据列 以 order by 的列为准，作为维度列 其他的列按插入顺序保留第一行 不在一个分区的数据不会被聚合 只有在同一批次插入(新版本)或分片合并时才会进行聚合 3）开发建议 设计聚合表的话，唯一键值、流水号可以去掉，所有字段全部是维度、度量或者时间戳。 4）问题 能不能直接执行以下 SQL 得到汇总值 select total_amount from XXX where province_name=’’ and create_date=’xxx’ 不行，可能会包含一些还没来得及聚合的临时明细 如果要是获取汇总值，还是需要使用 sum 进行聚合，这样效率会有一定的提高，但本身 ClickHouse 是列式存储的，效率提升有限，不会特别明显。 select sum(total_amount) from province_name=’’ and create_date=‘xxx’ 五、SQL 操作 基本上来说传统关系型数据库（以 MySQL 为例）的 SQL 语句，ClickHouse 基本都支持，这里不会从头讲解 SQL 语法只介绍 ClickHouse 与标准 SQL（MySQL）不一致的地方。 5.1、Insert 基本与标准 SQL（MySQL）基本一致 （1）标准 insert into [table_name] values(…),(….) （2）从表到表的插入 insert into [table_name] select a,b,c from [table_name_2] 5.2、Update 和 Delete ClickHouse 提供了 Delete 和 Update 的能力，这类操作被称为 Mutation 查询，它可以看做 Alter 的一种。 虽然可以实现修改和删除，但是和一般的 OLTP 数据库不一样，Mutation 语句是一种很“重”的操作，而且不支持事务。 “重”的原因主要是每次修改或者删除都会导致放弃目标数据的原有分区，重建新分区。所以尽量做批量的变更，不要进行频繁小数据的操作。 （1）删除操作 alter table t_order_smt delete where sku_id ='sku_001'; （2）修改操作 alter table t_order_smt update total_amount=toDecimal32(2000.00,2) where id =102; 由于操作比较“重”，所以 Mutation 语句分两步执行，同步执行的部分其实只是进行新增数据新增分区和并把旧分区打上逻辑上的失效标记。直到触发分区合并的时候，才会删除旧数据释放磁盘空间，一般不会开放这样的功能给用户，由管理员完成。 5.3、查询操作 ClickHouse 基本上与标准 SQL 差别不大 支持子查询 支持 CTE(Common Table Expression 公用表表达式 with 子句) 支持各种 JOIN，但是 JOIN 操作无法使用缓存，所以即使是两次相同的 JOIN 语句，ClickHouse 也会视为两条新 SQL 窗口函数(官方正在测试中...) 不支持自定义函数 GROUP BY 操作增加了 with rollup\with cube\with total 用来计算小计和总计。 （1）插入数据 hadoop102 :) alter table t_order_mt delete where 1=1; insert into t_order_mt values (101,'sku_001',1000.00,'2020-06-01 12:00:00'), (101,'sku_002',2000.00,'2020-06-01 12:00:00'), (103,'sku_004',2500.00,'2020-06-01 12:00:00'), (104,'sku_002',2000.00,'2020-06-01 12:00:00'), (105,'sku_003',600.00,'2020-06-02 12:00:00'), (106,'sku_001',1000.00,'2020-06-04 12:00:00'), (107,'sku_002',2000.00,'2020-06-04 12:00:00'), (108,'sku_004',2500.00,'2020-06-04 12:00:00'), (109,'sku_002',2000.00,'2020-06-04 12:00:00'), (110,'sku_003',600.00,'2020-06-01 12:00:00'); （2）with rollup：从右至左去掉维度进行小计 hadoop102 :) select id , sku_id,sum(total_amount) from t_order_mt group by id,sku_id with rollup; （3）with cube : 从右至左去掉维度进行小计，再从左至右去掉维度进行小计 hadoop102 :) select id , sku_id,sum(total_amount) from t_order_mt group by id,sku_id with cube; （4）with totals: 只计算合计 hadoop102 :) select id , sku_id,sum(total_amount) from t_order_mt group by id,sku_id with totals; 5.4、alter 操作 同 MySQL 的修改字段基本一致 1）新增字段 alter table tableName add column newcolname String after col1; 2）修改字段类型 alter table tableName modify column newcolname String; 3）删除字段 alter table tableName drop column newcolname; 5.5、导出数据 clickhouse-client --query "select * from t_order_mt where create_time='2020-06-01 12:00:00'" --format CSVWithNames> /opt/module/data/rs1.csv 六、副本 副本的目的主要是保障数据的高可用性，即使一台 ClickHouse 节点宕机，那么也可以从其他服务器获得相同的数据。 https://clickhouse.tech/docs/en/engines/table-engines/mergetree-family/replication/ 6.1、副本写入流程 6.2、配置步骤 （1）启动 zookeeper 集群 （2）在 hadoop102 的/etc/clickhouse-server/config.d 目录下创建一个名为 metrika.xml的配置文件,内容如下： 注：也可以不创建外部文件，直接在 config.xml 中指定 <?xml version="1.0"?> <yandex> <zookeeper-servers> <node index="1"> <host>hadoop102</host> <port>2181</port> </node> <node index="2"> <host>hadoop103</host> <port>2181</port> </node> <node index="3"> <host>hadoop104</host> <port>2181</port> </node> </zookeeper-servers> </yandex> （3）同步到 hadoop103 和 hadoop104 上 sudo /home/atguigu/bin/xsync /etc/clickhouse-server/config.d/metrika.xml （4）在 hadoop102 的/etc/clickhouse-server/config.xml 中增加 <zookeeper incl="zookeeper-servers" optional="true" /> <include_from>/etc/clickhouse-server/config.d/metrika.xml</include_from> （5）同步到 hadoop103 和 hadoop104 上 sudo /home/atguigu/bin/xsync /etc/clickhouse-server/config.xml 分别在 hadoop102 和 hadoop103 上启动 ClickHouse 服务 注意：因为修改了配置文件，如果以前启动了服务需要重启 [atguigu@hadoop102|3 ~]$ sudo clickhouse restart 注意：我们演示副本操作只需要在 hadoop102 和 hadoop103 两台服务器即可，上面的操作，我们 hadoop104 可以你不用同步，我们这里为了保证集群中资源的一致性，做了同步。 （6）在 hadoop102 和 hadoop103 上分别建表 副本只能同步数据，不能同步表结构，所以我们需要在每台机器上自己手动建表 1、hadoop102 create table t_order_rep2 ( id UInt32, sku_id String, total_amount Decimal(16,2), create_time Datetime ) engine =ReplicatedMergeTree('/clickhouse/table/01/t_order_rep','rep_102') partition by toYYYYMMDD(create_time) primary key (id) order by (id,sku_id); 2、hadoop103 create table t_order_rep2 ( id UInt32, sku_id String, total_amount Decimal(16,2), create_time Datetime ) engine =ReplicatedMergeTree('/clickhouse/table/01/t_order_rep','rep_103') partition by toYYYYMMDD(create_time) primary key (id) order by (id,sku_id); 3、参数解释 ReplicatedMergeTree 中， 第一个参数是分片的 zk_path 一般按照：/clickhouse/table/{shard}/{table_name} 的格式写，如果只有一个分片就写 01 即可。 第二个参数是副本名称，相同的分片副本名称不能相同。 （7）在 hadoop102 上执行 insert 语句 insert into t_order_rep2 values (101,'sku_001',1000.00,'2020-06-01 12:00:00'), (102,'sku_002',2000.00,'2020-06-01 12:00:00'), (103,'sku_004',2500.00,'2020-06-01 12:00:00'), (104,'sku_002',2000.00,'2020-06-01 12:00:00'), (105,'sku_003',600.00,'2020-06-02 12:00:00'); （8）在 hadoop103 上执行 select，可以查询出结果，说明副本配置正确
一、创建SpringApplication对象 initialize(sources);//创建对象 private void initialize(Object[] sources) { //【保存主配置类】 if (sources != null && sources.length > 0) { this.sources.addAll(Arrays.asList(sources)); } //【判断当前是否一个web应用】 this.webEnvironment = deduceWebEnvironment(); //从类路径下找到META-INF/spring.factories配置的所有ApplicationContextInitializer；然后保存起来 setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)); //从类路径下找到ETA-INF/spring.factories配置的所有ApplicationListener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); //从多个配置类中找到有main方法的主配置类 this.mainApplicationClass = deduceMainApplicationClass(); } 拿到初始化器： 保存初始化器和监听器： 二、运行run方法 public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; FailureAnalyzers analyzers = null; configureHeadlessProperty(); //获取SpringApplicationRunListeners；从类路径下META-INF/spring.factories SpringApplicationRunListeners listeners = getRunListeners(args); //回调所有的获取SpringApplicationRunListener.starting()方法 listeners.starting(); try { //封装命令行参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); //准备环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); //创建环境完成后回调SpringApplicationRunListener.environmentPrepared()；表示环境准备完成 Banner printedBanner = printBanner(environment); //创建ApplicationContext；决定创建web的ioc还是普通的ioc context = createApplicationContext(); analyzers = new FailureAnalyzers(context); //准备上下文环境;将environment保存到ioc中；而且applyInitializers()； //applyInitializers()：回调之前保存的所有的ApplicationContextInitializer的initialize方法 //回调所有的SpringApplicationRunListener的contextPrepared()； // prepareContext(context, environment, listeners, applicationArguments, printedBanner); //prepareContext运行完成以后回调所有的SpringApplicationRunListener的contextLoaded（）； //s刷新容器；ioc容器初始化（如果是web应用还会创建嵌入式的Tomcat）；Spring注解版 //扫描，创建，加载所有组件的地方；（配置类，组件，自动配置） refreshContext(context); //从ioc容器中获取所有的ApplicationRunner和CommandLineRunner进行回调 //ApplicationRunner先回调，CommandLineRunner再回调 afterRefresh(context, applicationArguments); //所有的SpringApplicationRunListener回调finished方法 listeners.finished(context, null); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } //整个SpringBoot应用启动完成以后返回启动的ioc容器； return context; } catch (Throwable ex) { handleRunFailure(context, listeners, analyzers, ex); throw new IllegalStateException(ex); } } 三、事件监听机制 配置在 META-INF/spring.factories ApplicationContextInitializer public class HelloApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { System.out.println("ApplicationContextInitializer...initialize..."+applicationContext); } } SpringApplicationRunListener public class HelloSpringApplicationRunListener implements SpringApplicationRunListener { //必须有的构造器 public HelloSpringApplicationRunListener(SpringApplication application, String[] args){ } @Override public void starting() { System.out.println("SpringApplicationRunListener...starting..."); } @Override public void environmentPrepared(ConfigurableEnvironment environment) { Object o = environment.getSystemProperties().get("os.name"); System.out.println("SpringApplicationRunListener...environmentPrepared.."+o); } @Override public void contextPrepared(ConfigurableApplicationContext context) { System.out.println("SpringApplicationRunListener...contextPrepared..."); } @Override public void contextLoaded(ConfigurableApplicationContext context) { System.out.println("SpringApplicationRunListener...contextLoaded..."); } @Override public void finished(ConfigurableApplicationContext context, Throwable exception) { System.out.println("SpringApplicationRunListener...finished..."); } } 配置（META-INF/spring.factories） org.springframework.context.ApplicationContextInitializer=\ com.atguigu.springboot.listener.HelloApplicationContextInitializer org.springframework.boot.SpringApplicationRunListener=\ com.atguigu.springboot.listener.HelloSpringApplicationRunListener 只需要放在IOC容器中 ApplicationRunner @Component public class HelloApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println("ApplicationRunner...run...."); } } CommandLineRunner @Component public class HelloCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("CommandLineRunner...run..."+ Arrays.asList(args)); } } 四、总结 4.1、SpringApplication.run(主程序类) new SpringApplication(主程序类) 判断是否web应用 加载并保存所有ApplicationContextInitializer（META-INF/spring.factories）， 加载并保存所有ApplicationListener 获取到主程序类 run() 回调所有的SpringApplicationRunListener（META-INF/spring.factories）的starting 获取ApplicationArguments 准备环境&回调所有监听器（ SpringApplicationRunListener ）的environmentPrepared 打印banner信息 创建ioc容器对象（ AnnotationConfigEmbeddedWebApplicationContext（web环境容器） AnnotationConfigApplicationContext（普通环境容器） 准备环境 执行ApplicationContextInitializer. initialize() 监听器SpringApplicationRunListener回调contextPrepared 加载主配置类定义信息 监听器SpringApplicationRunListener回调contextLoaded 刷新启动IOC容器； 扫描加载所有容器中的组件 包括从META-INF/spring.factories中获取的所有EnableAutoConfiguration组件 回调容器中所有的ApplicationRunner、CommandLineRunner的run方法 监听器SpringApplicationRunListener回调finishe 4.2、自动配置 Spring Boot启动扫描所有jar包的META-INF/spring.factories中配置的EnableAutoConfiguration组件 spring-boot-autoconfigure.jar\META-INF\spring.factories有启动时需要加载的EnableAutoConfiguration组件配置 配置文件中使用debug=true可以观看到当前启用的自动配置的信息 自动配置会为容器中添加大量组件 Spring Boot在做任何功能都需要从容器中获取这个功能的组件 Spring Boot 总是遵循一个标准；容器中有我们自己配置的组件就用我们配置的，没有就用自动配置默认注册进来的组件；
Btree索引 Btree索引(或Balanced Tree)，是一种很普遍的数据库索引结构，oracle默认的索引类型（本文也主要依据oracle来讲）。其特点是定位高效、利用率高、自我平衡，特别适用于高基数字段，定位单条或小范围数据非常高效。理论上，使用Btree在亿条数据与100条数据中定位记录的花销相同。 数据结构利用率高、定位高效。 Btree索引的数据结构如下： 结构看起来Btree索引与Binary Tree相似，但在细节上有所不同，上图中用不同颜色的标示出了Btree索引的几个主要特点： 树形结构：由根节(root)、分支(branches)、叶(leaves)三级节点组成，其中分支节点可以有多层。 多分支结构：与binary tree不相同的是，btree索引中单root/branch可以有多个子节点(超过2个)。 双向链表：整个叶子节点部分是一个双向链表(后面会描述这个设计的作用)。 单个数据块中包括多条索引记录。 这里先把几个特点罗列出来，后面会说到各自的作用。 结构上Btree与Binary Tree的区别，在于binary中每节点代表一个数值，而balanced中root和Btree节点中记录了多条”值范围”条目(如:[60-70][70-80])，这些”值范围”条目分别指向在其范围内的叶子节点。既root与branch可以有多个分支，而不一定是两个，对数据块的利用率更高。 在Leaf节点中，同样也是存放了多条索引记录，这些记录就是具体的索引列值，和与其对应的rowid。另外，在叶节点层上，所有的节点在组成了一个双向链表。 了解基本结构后，下图展示定位数值82的过程： 演算如下： 读取root节点，判断82大于在0-120之间，走左边分支。 读取左边branch节点，判断82大于80且小于等于120，走右边分支。 读取右边leaf节点，在该节点中找到数据82及对应的rowid。 使用rowid去物理表中读取记录数据块(如果是count或者只select rowid，则最后一次读取不需要)。 在整个索引定位过程中，数据块的读取只有3次。既三次I/O后定位到rowid。 而由于Btree索引对结构的利用率很高，定位高效。当1千万条数据时，Btree索引也是三层结构(依稀记得亿级数据才是3层与4层的分水岭)。定位记录仍只需要三次I/O，这便是开头所说的，100条数据和1千万条数据的定位，在btree索引中的花销是一样的。 平衡扩张 除了利用率高、定位高效外，Btree的另一个特点是能够永远保持平衡，这与它的扩张方式有关。(unbalanced和hotspot是两类问题)，先描述下Btree索引的扩张方式： 新建一个索引，索引上只会有一个leaf节点，取名为Node A，不断的向这个leaf节点中插入数据后，直到这个节点满，这个过程如下图（绿色表示新建/空闲状态，红色表示节点没有空余空间）： 当Node A满之后，我们再向表中插入一条记录，此时索引就需要做拆分处理：会新分配两个数据块NodeB & C，如果新插入的值，大于当前最大值，则将Node A中的值全部插入Node B中，将新插入的值放到Node C中；否则按照5-5比例，将已有数据分别插入到NodeB与C中。 无论采用哪种分割方式，之前的leaf节点A，将变成一个root节点，保存两个范围条目，指向B与C，结构如下图（按第一种拆分形式）： 当Node C满之后，此时 Node A仍有空余空间存放条目，所以不需要再拆分，而只是新分配一个数据块Node D，将在Node A中创建指定到Node D的条目： 如果当根节点Node A也满了，则需要进一步拆分：新建Node E&F&G，将Node A中范围条目拆分到E&F两个节点中，并建立E&F到BCD节点的关联，向Node G插入索引值。此时E&F为branch节点，G为leaf节点，A为Root节点： 在整个扩张过程中，Btree自身总能保持平衡，Leaf节点的深度能一直保持一致。 实际应用中的一些问题 前面说完了Btree索引的结构与扩张逻辑，接下来讲一些Btree索引在应用中的一些问题： 单一方向扩展引起的索引竞争(Index Contention) 若索引列使用sequence或者timestamp这类只增不减的数据类型。这种情况下Btree索引的增长方向总是不变的，不断的向右边扩展，因为新插入的值永远是最大的。 当一个最大值插入到leaf block中后，leaf block要向上传播，通知上层节点更新所对应的“值范围”条目中的最大值，因此所有靠右边的block(从leaf 到branch甚至root)都需要做更新操作，并且可能因为块写满后执行块拆分。 如果并发插入多个最大值，则最右边索引数据块的的更新与拆分都会存在争抢，影响效率。在AWR报告中可以通过检测enq: TX – index contention事件的时间来评估争抢的影响。解决此类问题可以使用Reverse Index解决，不过会带来新的问题。 ndex Browning 索引枯萎(不知道该怎么翻译这个名词，就是指leaves节点”死”了，树枯萎了) 其实oracle针对这个问题有优化机制，但优化的不彻底，所以还是要拿出来的说。 我们知道当表中的数据删除后，索引上对应的索引值是不会删除的，特别是在一性次删除大批量数据后，会造成大量的dead leaf挂到索引树上。考虑以下示例，如果表100以上的数据会部被删除了，但这些记录仍在索引中存在，此时若对该列取max()： 通过与之前相同演算，找到了索引树上最大的数据块，按照记录最大的值应该在这里，但发现这数据块里的数据已经被清空了，与是利用Btree索引的另一个特点：leaves节点是一个双向列表，若数据没有找到就去临近的一个数据块中看看，在这个数据块中发现了最大值99。 在计算最大值的过程中，这次的定位多加载了一个数据块，再极端的情况下，大批量的数据被删除，就会造成大量访问这些dead leaves。 针对这个问题的一般解决办法是重建索引，但记住! 重建索引并不是最优方案，详细原因可以看看这。使用coalesce语句来整理这些dead leaves到freelist中，就可以避免这些问题。理论上oracle中这步操作是可以自动完成的，但在实际中一次性大量删除数据后，oracle在短时间内是反应不过来的。
一、索引的分类 1、从存储结构上来划分：BTree索引（B-Tree或B+Tree索引），Hash索引，full-index全文索引，R-Tree索引。这里所描述的是索引存储时保存的形式。 2、从应用层次来分：普通索引，唯一索引，复合索引。 3、根据中数据的物理顺序与键值的逻辑（索引）顺序关系：聚集索引，非聚集索引。 1中所描述的是索引存储时保存的形式，2是索引使用过程中进行的分类，两者是不同层次上的划分。不过平时讲的索引类型一般是指在应用层次的划分。 普通索引：即一个索引只包含单个列，一个表可以有多个单列索引。 唯一索引：索引列的值必须唯一，但允许有空值。 复合索引：即一个索引包含多个列。 聚簇索引(聚集索引)：并不是一种单独的索引类型，而是一种数据存储方式。具体细节取决于不同的实现，InnoDB的聚簇索引其实就是在同一个结构中保存了B-Tree索引(技术上来说是B+Tree)和数据行。 非聚簇索引：不是聚簇索引，就是非聚簇索引。 二、索引的底层实现 MySQL 默认存储引擎 innodb 只显式支持 B-Tree( 从技术上来说是B+Tree)索引，对于频繁访问的表，innodb 会透明建立自适应 hash 索引，即在 B 树索引基础上建立 hash 索引，可以显著提高查找效率，对于客户端是透明的，不可控制的，隐式的。 2.1、Hash索引 基于哈希表实现，只有精确匹配索引所有列的查询才有效，对于每一行数据，存储引擎都会对所有的索引列计算一个哈希码（hash code），并且Hash索引将所有的哈希码存储在索引中，同时在索引表中保存指向每个数据行的指针。 2.2、B-Tree索引（MySQL使用B+Tree） B-Tree能加快数据的访问速度，因为存储引擎不再需要进行全表扫描来获取数据，数据分布在各个节点之中。 2.3、B+Tree索引 是B-Tree的改进版本，同时也是数据库索引索引所采用的存储结构。数据都在叶子节点上，并且增加了顺序访问指针，每个叶子节点都指向相邻的叶子节点的地址。相比 B-Tree 来说，进行范围查找时只需要查找两个节点，进行遍历即可。而 B-Tree 需要获取所有节点，相比之下 B+Tree 效率更高。 结合存储引擎来讨论（一般默认使用 B+Tree） 案例：假设有一张学生表，id为主键 id name birthday 1 Tom 1996-01-01 2 Jann 1996-01-04 3 Ray 1996-01-08 4 Michael 1996-01-10 5 Jack 1996-01-13 6 Steven 1996-01-23 7 Lily 1996-01-25 在MyISAM引擎中的实现（二级索引也是这样实现的） 在InnoDB中的实现 三、问题 问：为什么索引结构默认使用 B-Tree，而不是 hash，二叉树，红黑树？ hash：虽然可以快速定位，但是没有顺序，IO 复杂度高。 二叉树：树的高度不均匀，不能自平衡，查找效率跟数据有关（树的高度），并且IO代价高。 红黑树：树的高度随着数据量增加而增加，IO 代价高。 问：为什么官方建议使用自增长主键作为索引。 结合 B+Tree 的特点，自增主键是连续的，在插入过程中尽量减少页分裂，即使要进行页分裂，也只会分裂很少一部分。并且能减少数据的移动，每次插入都是插入到最后。总之就是减少分裂和移动的频率。