[{"content":"Zuul 的核心概念 （1）Zuul 是什么？ Zuul 是 Netflix 开源的 API 网关，用于微服务架构中的路由、过滤和负载均衡 主要功能： 路由：将客户端请求转发到后端服务 过滤：在请求和响应的生命周期中执行自定义逻辑 负载均衡：与 Ribbon 集成，实现客户端负载均衡 （2）Zuul 的工作流程 路由：根据配置请求转发到对应的微服务 过滤：在请求到达目标服务之前或之后执行过滤逻辑 负载均衡：通过 Ribbon 集成，实现客户端负载均衡 Zuul 的配置 （1）依赖配置 在 pom.xml 添加 Zuul 依赖： \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-zuul\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; （2）启用 Zuul 在启动类中添加 @EnableZuulProxy 注解： @SpringBootApplication @EnableZuulProxy public class ZuulApplication { public static void main(String[] args) { SpringAppliction.run(ZuulApplication.class, args); } } （3）路由配置 在 application.yml 中配置路由规则： zuul: routes: user-service: # 路径名称 path: /user/** # 路径配置 serviceId: user-service # 目标服务名称 order-service: path: /order/** serviceId: order-service 解释： user-service 和 order-service 是路由的名称，可以自定义 path 是匹配的 URL 路径 serviceId 是 Eureka 中注册的服务名称 自定义路由 如果不想使用服务名称作为路由前缀，可以自定义路由： zuul: routes: custom-user-service: # 路由策略 path: /api/user/** # 匹配路径 url: http://localhost:8081 # 目标服务地址 负载均衡配置 （1）集成 Ribbon Zuul 默认集成了 Ribbon，可以通过服务名称实现负载均衡 在 application.yml 中配置 Ribbon： zuul: routes: custom-user-service: path: /api/user/** serviceId: custom-user-service # 使用服务名称 custom-user-service: ribbon: listOfServers: http://localhost:8081,http://localhost:8082 # 目标服务实例列表 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机策略 实例：完整的 Zuul 配置 （1）application.yml user-service: ribbon: listOfServers: http://localhost:8081,http://localhost:8082 # 目标服务实例列表 （2）启动类 @SpringBootApplication @EnableZuulProxy public class ZuulApplication { public static void main(String[] args) { SpringAppliction.run(ZuulApplication.class, args); } } Zuul 与 Spring Cloud Gateway对比 对比项 Spring Cloud Gateway Spring Cloud Zuul 1.x 架构模型 异步非阻塞 (基于 WebFlux 和 Reactor) 同步阻塞 (基于 Servlet) 过滤器链 轻量高效 相对较重 路由匹配 高效的路由匹配算法 相对较慢 内存消耗 较低 (基于 Netty) 较高 (基于 Servlet 容器) 编程模型 响应式编程 传统同步模型 负载均衡 集成 Spring Cloud LoadBalancer 使用 Ribbon 线程切换 较少 较多 连接管理 使用 Netty 连接池 相对简单 协议支持 原生支持 HTTP/2 主要支持 HTTP/1.1 上下文切换 较少 较多 性能 更高 相对较低 ","date":"2025-05-07T23:04:30+08:00","permalink":"/blog/zuul/","title":"Zuul"},{"content":"Zipkin 是 Twitter 开源的分布式链路追踪系统，用于收集和查看微服务架构中的请求链路数据。它与 Spring Cloud Sleuth（或 Micrometer Tracing）配合使用——Sleuth 负责生成和传播 Trace 数据，Zipkin 负责存储和展示。\n架构组成 +-----------+ +-----------+ +-----------+ | Collector | \u0026lt;-- | Reporter | \u0026lt;-- | Instrument| +-----------+ +-----------+ +-----------+ | | v v +-----------+ +-----------+ | Storage | | UI | +-----------+ +-----------+ Instrument：应用中嵌入的追踪客户端（Brave / Sleuth），生成 Span 数据 Reporter：将 Span 数据异步发送到 Zipkin Server。支持 HTTP、Kafka、RabbitMQ Collector：接收、验证、存储 Span 数据 Storage：持久化存储（内存/MySQL/ElasticSearch/Cassandra） UI：Web 界面查询和可视化链路 数据模型 Span 数据结构 public final class Span implements Serializable { public static final class Builder { private String traceId; // 全局唯一链路 ID private String parentId; // 父 Span ID（根 Span 为 null） private String id; // 当前 Span ID private String name; // Span 名称（如 \u0026#34;get /user/{id}\u0026#34;） private long timestamp; // 开始时间戳（微秒） private long duration; // 持续时间（微秒） private List\u0026lt;Annotation\u0026gt; annotations; // 关键事件标记 private Map\u0026lt;String, String\u0026gt; tags; // 自定义标签 private Boolean shared; // 是否被服务端和客户端共享 } } 关键 Annotation 标记 含义 用途 cs (Client Sent) 客户端发送请求 计算请求开始时间 cr (Client Received) 客户端收到响应 计算总耗时 = cr - cs sr (Server Received) 服务端收到请求 计算网络延迟 = sr - cs ss (Server Sent) 服务端发送响应 计算服务处理时间 = ss - sr error 请求出错 标记异常链路 数据流 1. 应用通过 Reporter 发送 Span 数据 2. Collector 接收并校验数据完整性 3. 数据存储到 Storage（可选择 ES/Cassandra/MySQL） 4. UI 从 Storage 查询并展示 Reporter 发送方式对比 方式 优点 缺点 场景 HTTP（Web） 部署简单，无需额外组件 请求阻塞风险，吞吐量低 开发和低流量 Kafka 高吞吐，削峰填谷 需要维护 Kafka 集群 生产环境高流量 RabbitMQ 类似 Kafka，与 Spring 生态更适配 需要维护 RabbitMQ 已有 RabbitMQ 的基础设施 Scribe 日志收集 维护成本高 历史遗留系统 使用 Kafka 推荐配置：\nspring: zipkin: sender: type: kafka kafka: bootstrap-servers: localhost:9092 Kafka 的优势：即使 Zipkin Server 短暂不可用，数据也不会丢失（Kafka 持久化缓冲）。\n存储后端选择 存储 优势 劣势 推荐场景 内存 零配置，快速体验 重启后数据丢失，容量有限 开发测试 MySQL 运维成熟 查询性能差，数据量大时慢 低流量/小团队 ElasticSearch 查询快，适合全文搜索 运维复杂，资源消耗高 生产环境（推荐） Cassandra 写入性能极高，天然支持水平扩展 查询功能有限，运维复杂 超大规模集群 生产环境选择建议：ElasticSearch 是最常用的选择，兼顾查询灵活性和写入性能。\n数据采样策略 采样重要性 全量采样（probability: 1.0）在高并发下会产生大量数据——1000 QPS 的服务每天产生近亿条 Span。采样是必须的。\n采样方式 固定比率采样：默认，按百分比采样 spring: sleuth: sampler: probability: 0.01 # 采样 1% 限速采样：保证每秒最多采样条数 spring: sleuth: sampler: rate: 10 # 每秒最多采样 10 条 自定义采样规则：对某些路径全量采样（如支付、订单相关接口），其他路径降低采样率。需实现 Sampler 接口。 集成示例 spring: zipkin: base-url: http://zipkin-server:9411 sender: type: web enabled: true sleuth: sampler: probability: 0.1 # 启动 Zipkin Server（Docker） docker run -d -p 9411:9411 openzipkin/zipkin:latest 排查思路 Zipkin UI 看不到数据\n检查应用能否访问 Zipkin Server：telnet zipkin-server 9411 检查采样率是否为 0 检查应用日志中是否出现 ZipkinReporter 相关的异常 链路不完整\n确认所有参与链路的服务都配置了 Zipkin 检查异步调用是否丢失 TraceContext——异步线程池中需要手动传播上下文 确认 Http Header 未被自定义过滤器清除（X-B3-TraceId 等） Zipkin 存储满了\nES：设置 ILM（Index Lifecycle Management）自动删除过期索引 Cassandra：配置 TTL（Time To Live） MySQL：定期清理，或设置较短的数据保留期 性能问题\nZipkin Collector 成为瓶颈：增加 Collector 实例数 ES 写入过慢：降低采样率，或使用 Kafka 缓冲 使用 sender.type: kafka 将数据发送链路上的压力从应用线程转移到 Kafka 高可用部署 多个 Zipkin Server 实例 ↑ (Nginx 负载均衡) Kafka 集群（缓冲区） ↑ 应用实例（Reporter 写入 Kafka） 部署多个 Collector 实例，使用 Nginx/ALB 负载均衡 使用 Kafka 作为 Reporter 和 Collector 之间的缓冲层 存储后端 ES 集群健康状态决定数据可靠性——使用 3 节点 + 副本策略 ES 使用 ILM 按天生成索引，设置保留天数自动清理 ","date":"2025-03-03T22:19:24+08:00","permalink":"/blog/zipkin/","title":"Zipkin"},{"content":"三级缓存 在 Spring 框架中，三级缓存机制用于解决单例 Bean 的循环依赖问题。当两个或多个单例 Bean 相互依赖时，Spring 通过三级缓存来确保它们能够相互引用，避免了循环依赖倒置的错误。\n三级缓存的组成： Spring的DefaultSingletonBeanRegistry类三级缓存对象\nprivate final Map\u0026lt;String, Object\u0026gt; singletonObjects = new ConcurrentHashMap\u0026lt;\u0026gt;(256); private final Map\u0026lt;String, Object\u0026gt; earlySingletonObjects = new ConcurrentHashMap\u0026lt;\u0026gt;(16); private final Map\u0026lt;String, ObjectFactory\u0026lt;?\u0026gt;\u0026gt; singletonFactories = new HashMap\u0026lt;\u0026gt;(16); 一级缓存（singletonObjects）：存放完全初始化的单例 Bean 实例。当请求一个 Bean 时，Spring 首先从一级缓存中查找，如果存在，则直接返回实例。 二级缓存（earlySingletonObjects）：存放尚未完全初始化的单例 Bean 实例，通常是指构造函数已执行完毕，但属性尚未填充的 Bean。当 Bean 的构造函数执行完毕后，但属性尚未填充的 Bean。当 Bean 的构造函数执行完毕后，Spring 会将其放入二级缓存中，以便在依赖注入过程中，其它 Bean 可以获取该 Bean 的早期引用，从而解决循环依赖。 三级缓存（singletonFactories）：存放用于创建单例 Bean 的工厂对象（ObjectFactory）。当 Bean 正在创建过程中，且需要提前暴露其引用时，Spring 会将用于创建该 Bean 的工厂对象放入三级缓存中。其它 Bean 在依赖注入时，可以通过三级缓存获取该工厂对象，从而获取正在创建中的 Bean 的引用。 三级缓存解决下循环依赖的流程 实例化 Bean：当 Spring 需要创建一个 Bean 时，首先会检查一级缓存中是否已有该 Bean 的实例。如果没有，则开始实例化该 Bean。 放入三级缓存：在实例化过程中，Spring 会将用于创建该 Bean 的工厂对象放入三级缓存中。这样，其它 Bean 在依赖注入时，可以通过三级缓存获取该工厂对象，从而获取到正在创建中的 Bean 的引用。 放入二级缓存：当 Bean 的构造函数执行完毕后，Spring 会将其放入二级缓存中。此时，其它 Bean 可以从二级缓存中获取到该 Bean 的早期引用。 放入一级缓存：当 Bean 的属性填充和初始化方法执行完毕后，Spring 会将其放入一级缓存，表示该 Bean 已完全初始化。此时，其它 Bean 可以从一级缓存中获取到该 Bean 的实例。 为什么需要三级缓存？ 虽然二级缓存可以解决大部分循环依赖问题，但在涉及 AOP（面向切面编程）时，三级缓存显得尤为重要。在 AOP 代理的情况下，Bean 的创建过程需要通过代理工厂来生成代理对象。三级缓存缓存中的工厂对象正是用于创建这些代理对象的。如果没有三级缓存，Spring 将无法在 Bean 创建过程中提前暴露代理对象，从而导致循环依赖无法解决。\n通过引用三级缓存，Spring 能够在 Bean 创建过程中提前暴露代理对象，确保循环依赖能够正确解决。这使得 Spring 在处理复杂的依赖关系时，能够保持高效和灵活。\n总之，Spring 的三级缓存机制通过合理的缓存策略，确保了单例 Bean 在存在循环依赖和 AOP 代理的情况下，能够正确地相互引用，避免了循环依赖导致的错误。\n","date":"2025-01-30T22:39:17+08:00","permalink":"/blog/springthridlevelcache/","title":"Spring 三级缓存"},{"content":"\n","date":"2025-01-22T13:16:53+08:00","permalink":"/blog/synchronized/","title":"Synchronized"},{"content":" 注意：Spring Cloud Sleuth 已在 Spring Boot 3.x / Spring Cloud 2022.0.x 中被 Micrometer Tracing 取代。新项目直接使用 Micrometer Tracing，旧项目建议迁移。本文基于 Sleuth 2.x 版本。\n分布式追踪核心概念 为什么需要链路追踪？ 微服务架构中，一个请求可能需要经过多个服务。当请求变慢或报错时，没有链路追踪很难定位问题出在哪个服务、哪个环节。链路追踪通过 Trace ID 将一次请求跨服务的调用串联起来。\n核心术语 Trace：一次完整的请求链路，用唯一的 Trace ID 标识 Span：一个工作单元（如一次 RPC 调用、一次数据库查询），包含开始和结束时间 Annotation：关键事件标记，记录请求在不同阶段的时间戳 Trace (traceId=abc) ├── Span 1 (id=1, parentId=null) —— 入口服务 │ ├── cs (Client Sent): 时间戳 T1 │ ├── sr (Server Received): 时间戳 T2 —— 下游服务收到 │ ├── ss (Server Sent): 时间戳 T3 —— 下游服务返回 │ └── cr (Client Received): 时间戳 T4 └── Span 2 (id=2, parentId=1) —— 下游服务调数据库 ├── cs: 时间戳 T5 └── cr: 时间戳 T6 通过这 4 个时间点可以计算出：\n网络延迟：sr - cs（请求发送耗时）+ cr - ss（响应传输耗时） 服务处理时间：ss - sr（下游服务实际处理耗时） 总耗时：cr - cs Sleuth 核心功能 自动生成并传播 Trace ID 和 Span ID（通过 HTTP Header 或消息头） 集成 Zipkin 等追踪后端 支持多种通信协议（HTTP、消息队列、gRPC） 日志自动关联——日志中自动添加 [appname, traceId, spanId, exportable] 2024-08-01 10:00:00.123 [http-nio-8080-exec-1] [myapp, 2485ec27856c56f4, 2485ec27856c56f4, true] INFO - 请求开始 通过这个格式，可以按 traceId 筛选出某个请求在所有服务中的完整日志。\n源码分析 Tracer 接口 public interface Tracer { Span nextSpan(); Span nextSpan(Span parent); Span newTrace(); void continueSpan(Span span); void close(Span span); Span currentSpan(); } Tracer 负责 Span 的创建和管理。newTrace() 创建全新的 Trace（入口请求），nextSpan() 基于当前 Trace 创建子 Span。\nTraceContext 传播 public interface Propagator { \u0026lt;C\u0026gt; void inject(TraceContext context, C carrier, Setter\u0026lt;C\u0026gt; setter); \u0026lt;C\u0026gt; Span.Builder extract(C carrier, Getter\u0026lt;C\u0026gt; getter); } inject 将 TraceContext 写入 HTTP Header（如 X-B3-TraceId），extract 从请求头中提取并恢复上下文。这是跨服务传播的核心接口。\n自动配置 @Configuration @ConditionalOnProperty(value = \u0026#34;spring.sleuth.enabled\u0026#34;, matchIfMissing = true) public class TraceAutoConfiguration { @Bean public Tracer tracer() { return new DefaultTracer(); } @Bean public CurrentTraceContext currentTraceContext() { return ThreadLocalCurrentTraceContext.create(); } } 重要知识点 1. Sleuth 如何实现跨服务上下文传播？ Sleuth 通过 Brave（Zipkin 的 Java 客户端库）实现上下文传播。HTTP 请求经过 RestTemplate / Feign / WebClient 时，Sleuth 自动注入以下 Header：\nX-B3-TraceId：全局唯一的 Trace ID X-B3-SpanId：当前 Span ID X-B3-ParentSpanId：父 Span ID X-B3-Sampled：是否采样 X-B3-Flags：调试标记 下游服务收到请求后，从 Header 中提取 X-B3-TraceId，创建子 Span 并将自己作为当前 Trace 的一部分。\n2. Trace 和 Span 的关系？ 一个 Trace 由多个 Span 组成，形成树状结构 根 Span（第一个 Span）没有 parentId 子 Span 通过 parentId 引用父 Span 所有 Span 共享同一个 traceId 3. 如何自定义 Span？ @Autowired private Tracer tracer; public void customSpan() { Span span = tracer.nextSpan().name(\u0026#34;custom-operation\u0026#34;).start(); try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) { // 业务逻辑 span.tag(\u0026#34;custom-key\u0026#34;, \u0026#34;custom-value\u0026#34;); span.annotate(\u0026#34;custom-event\u0026#34;); } finally { span.finish(); } } 注意：start() 只调用一次，finish() 会在 finally 中确保执行。使用 withSpanInScope 将当前 Span 绑定到线程上下文。\n4. Sleuth 与 Zipkin 集成？ spring: zipkin: base-url: http://localhost:9411 sender: type: web # 发送方式: web/kafka/rabbitmq sleuth: sampler: probability: 1.0 # 采样率: 0~1，生产环境建议 0.1 以下 采样率说明：\n1.0：100% 采样，开发和调试时使用 0.1：10% 采样，生产环境常用——追踪数据量大时存储和网络压力不可忽视 0.01：1% 采样，高流量系统的保守选择 5. Sleuth 对各种通信方式的支持？ 通信方式 支持情况 说明 RestTemplate 自动 通过 ClientHttpRequestInterceptor Feign 自动 通过 Feign 的 RequestInterceptor WebClient 自动 通过 ExchangeFilterFunction RabbitMQ 自动 通过消息头传播 Kafka 自动 通过消息头传播 gRPC 依赖扩展 需额外配置 线程池 需要手动 使用 Tracer.withSpanInScope 或 LazyTraceExecutor 6. Sleuth 的性能影响？ Sleuth 本身非常轻量，主要开销在： Trace ID 的生成和传播（极低） 数据发送到 Zipkin（异步，不阻塞请求线程） 采样率高时，Zipkin 存储可能成为瓶颈 生产环境建议：probability: 0.01~0.1 + 使用 Kafka 作为 Reporter（避免 HTTP 发送阻塞） 7. Sleuth 如何支持异步操作？ // 方法一：手动绑定上下文 Mono.just(\u0026#34;value\u0026#34;) .doOnNext(value -\u0026gt; { Span span = tracer.nextSpan().name(\u0026#34;async-operation\u0026#34;).start(); try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) { // 异步逻辑 } finally { span.finish(); } }); // 方法二：使用 Sleuth 提供的 LazyTraceExecutor @Bean public Executor asyncExecutor() { return new LazyTraceExecutor(tracer, Executors.newFixedThreadPool(10)); } 排查思路 Trace ID 未在日志中出现\n确认 spring.sleuth.enabled 未被设置为 false 确认日志格式正确（Sleuth 通过 MDC 注入 traceId） 检查是否使用了自定义 RestTemplate 或 Feign，可能覆盖了默认拦截器 调用链不完整\n确认所有参与链路的服务都配置了 Sleuth 确认服务间的调用方式（HTTP/消息队列）被 Sleuth 支持 检查异步操作中是否丢失了上下文——使用 Tracer.withSpanInScope 或 LazyTraceExecutor Zipkin 看不到数据\n检查 spring.zipkin.base-url 是否正确 检查网络连通性（从应用程序到 Zipkin Server） 检查采样率设置：probability: 0 会导致不发送任何数据 迁移到 Micrometer Tracing 的注意点\n配置从 spring.sleuth.* 变为 management.tracing.* Trace ID 格式从 64-bit（Sleuth 默认）变为 128-bit（Micrometer 默认） 依赖从 spring-cloud-starter-sleuth 变为 io.micrometer:micrometer-tracing-bridge-brave ","date":"2024-08-01T13:54:50+08:00","permalink":"/blog/springcloudsleuth/","title":"Spring Cloud Sleuth"},{"content":"Spring Cloud Gateway 核心原理 1. 为什么选择 Gateway 而非 Zuul？ Spring Cloud Gateway 基于 Spring WebFlux 和 Project Reactor，采用反应式编程模型（非阻塞 I/O）。与 Zuul 1.x（基于 Servlet API，同步阻塞）相比：\n特性 Gateway Zuul 1.x Zuul 2.x 底层模型 Netty + WebFlux Tomcat + Servlet Netty I/O 模型 异步非阻塞 同步阻塞 异步非阻塞 长连接 原生支持 不支持 支持 性能（吞吐量） 高 低（每个请求占用一个线程） 中 学习成本 较高（需了解 Reactor） 低 较高 Gateway 的异步模型意味着：一个请求从进入到返回不占用独立线程，处理过程中线程可以服务其他请求，因此在高并发场景下线程资源占用远少于 Zuul 1.x。\n2. 架构组成 Client -\u0026gt; Gateway -\u0026gt; Route Predicate -\u0026gt; Gateway Filter -\u0026gt; Target Service 3. 核心概念 Route：路由，包含目标 URI、Predicate 集合和 Filter 集合 Predicate：断言，用于匹配 HTTP 请求（满足条件才走该路由） Filter：过滤器，用于修改请求和响应（分为 GatewayFilter 和 GlobalFilter） 4. 工作流程 1. 客户端发起请求 2. Gateway Handler Mapping 查找匹配的路由（遍历所有 Route 的 Predicate 集合） 3. 执行 Gateway Filter 链（pre filters） 4. 代理请求到目标服务 5. 执行 Gateway Filter 链（post filters） 6. 返回响应给客户端 5. 关键组件 RouteLocator：路由定位器，定义路由规则 GatewayFilter：网关过滤器（按路由配置） GlobalFilter：全局过滤器（所有路由生效） RoutePredicateHandlerMapping：路由断言处理器映射 WebHandler：Web 请求处理器（底层由 WebFlux 的 DispatcherHandler 执行） 重要知识点 1. Gateway 和 Zuul 的区别？ 见上方对比表。核心差异在于线程模型：Gateway 用少量线程处理大量请求（事件驱动），Zuul 1.x 每个请求对应一个线程（同步阻塞）。\n2. Gateway 的核心组件？ Route、Predicate、Filter（GatewayFilter + GlobalFilter）、RouteLocator、GatewayFilterFactory\n3. 如何自定义过滤器？ 自定义 GatewayFilter 实现 GatewayFilter + Ordered 接口：\n@Component public class CustomFilter implements GatewayFilter, Ordered { @Override public Mono\u0026lt;Void\u0026gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 前置处理 String requestPath = exchange.getRequest().getURI().getPath(); exchange.getAttributes().put(\u0026#34;startTime\u0026#34;, System.currentTimeMillis()); return chain.filter(exchange).then(Mono.fromRunnable(() -\u0026gt; { // 后置处理 Long startTime = exchange.getAttribute(\u0026#34;startTime\u0026#34;); if (startTime != null) { long cost = System.currentTimeMillis() - startTime; log.info(\u0026#34;{} 耗时 {}ms\u0026#34;, requestPath, cost); } })); } @Override public int getOrder() { return -1; // 越小越先执行 } } 注册方式：在 Route 定义中通过 .filters(f -\u0026gt; f.filter(new CustomFilter())) 注入。\n4. Gateway 如何实现负载均衡？ 集成 Spring Cloud LoadBalancer 使用 lb://service-id 格式的 URI（例如 lb://user-service） Gateway 自动从注册中心（Nacos / Eureka）获取服务实例列表 默认负载均衡算法为轮询（支持自定义 ReactiveLoadBalancer） 5. 如何实现限流？ 使用 RequestRateLimiter GatewayFilter，基于令牌桶算法：\nspring: cloud: gateway: routes: - id: limit_route uri: http://example.org filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 # 每秒填充的令牌数 redis-rate-limiter.burstCapacity: 20 # 令牌桶容量 注意：GateWay 的限流需要集成 Redis（基于 Redis 的 lua 脚本实现令牌桶），因此必须引入 spring-boot-starter-data-redis-reactive。\n6. Gateway 如何实现熔断？ spring: cloud: gateway: routes: - id: hystrix_route uri: lb://backing-service:8088 filters: - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/fallback 注意：Hystrix 已进入维护状态，新项目推荐使用 Spring Cloud CircuitBreaker（支持 Resilience4J）替代 Hystrix：\nfilters: - name: CircuitBreaker args: name: myCircuitBreaker fallbackUri: forward:/fallback 7. 如何实现动态路由？ 实现 RouteDefinitionRepository 接口，从数据库或配置中心加载路由：\n@Component public class DynamicRouteService implements ApplicationEventPublisherAware { @Autowired private RouteDefinitionWriter routeDefinitionWriter; public void addRoute(RouteDefinition definition) { routeDefinitionWriter.save(Mono.just(definition)).subscribe(); publisher.publishEvent(new RefreshRoutesEvent(this)); // 刷新路由 } } 更新的关键：保存路由定义后必须发布 RefreshRoutesEvent，否则新路由不会生效。\n8. Gateway 的安全机制？ 集成 Spring Security + OAuth2 / JWT 支持 HTTPS 支持 IP 白名单（通过自定义 GlobalFilter 实现） 常见安全风险：Gateway 默认不校验重复路由、不限制请求体大小——生产环境需额外配置 9. Gateway 的监控？ 集成 Actuator，management.endpoints.web.exposure.include=gateway /actuator/gateway/routes：查看已加载的路由 /actuator/gateway/globalfilters：查看已注册的全局过滤器 结合 Micrometer + Prometheus + Grafana 采集请求延迟和错误率 10. Gateway 如何处理跨域？ spring: cloud: gateway: globalcors: cors-configurations: \u0026#39;[/**]\u0026#39;: allowedOrigins: \u0026#34;*\u0026#34; allowedMethods: - GET - POST - PUT - DELETE allowedHeaders: \u0026#34;*\u0026#34; 注意：Gateway 的位置特殊——跨域配置在网关层处理即可，后端服务无需重复配置 CORS。\n排查思路 路由不匹配 检查 Predicate 顺序（多个 Predicate 为 AND 关系） 查看日志：RoutePredicateHandlerMapping 会打印 \u0026ldquo;Mapping found\u0026rdquo; 或 \u0026ldquo;No route found\u0026rdquo; 转发失败 确认目标服务健康（直接访问目标服务接口验证） 检查负载均衡 URI 格式是否以 lb:// 开头 查看 Gateway 日志：ReadTimeoutException 表示后端响应过慢 过滤器不生效 GlobalFilter 默认所有路由生效，GatewayFilter 须在路由配置中明确指定 检查 Ordered 排序（低 order 值的过滤器先执行前置后执行后置） WebFlux 与旧版 Servlet 冲突 项目中不能同时存在 spring-boot-starter-web 和 spring-boot-starter-webflux，否则 Gateway 无法正常运行（Spring Boot 默认优先使用 Servlet 容器）。排查是否有其他模块引入了 web 依赖。 ","date":"2024-07-18T23:13:45+08:00","permalink":"/blog/springcloudgateway/","title":"Spring Cloud Gateway"},{"content":"核心概念 （1）什么是 Spring Cloud Config？ Spring Cloud Config 是分布式配置管理工具，用于集中管理微服务的配置文件。它解决的核心问题：微服务数量增加后，配置文件散落在各个服务中，修改一个配置需要逐个修改、重启。\n（2）组成部分 Config Server：配置中心服务器，从后端存储加载配置并通过 HTTP API 暴露 Config Client：配置客户端，启动时从 Config Server 拉取配置 （3）核心特性 集中化管理：所有微服务配置统一存储在 Git/SVN/本地等后端 动态刷新：通过 /actuator/refresh 或 Spring Cloud Bus 实现不重启更新配置 多环境支持：通过 spring.profiles.active 加载对应环境的配置（如 application-dev.yml） 安全性：支持配置内容的加密和解密（对称 AES 或非对称 RSA） 版本控制：Git 后端天然具备版本历史、回滚、分支管理能力 权限控制：通过 Git 仓库权限 + Config Server 的 Spring Security 双重控制 高可用：Config Server 可集群部署，通过注册中心实现负载均衡 工作原理 Config Server 配置解析路径 Config Server 根据客户端请求的 {application}/{profile}/{label} 路径解析配置。例如客户端请求 /my-app/dev/master，Config Server 会在 Git 仓库中查找：\nmy-app-dev.yml（应用名+环境） my-app.yml（应用名级配置） application-dev.yml（全局环境配置） application.yml（全局默认配置） 优先级：1 \u0026gt; 2 \u0026gt; 3 \u0026gt; 4（后面覆盖前面）。\nBootstrap 上下文 Spring Cloud Config Client 使用 bootstrap context（由 bootstrap.yml 或 bootstrap.properties 配置），它在主应用上下文创建之前启动，负责：\n从 Config Server 拉取配置 将拉取的配置设置为 Environment 属性源 然后才创建主应用上下文 这就是为什么 Config Server 地址必须在 bootstrap.yml 而非 application.yml 中配置——主上下文启动时必须已经有远程配置可用。\n配置加载顺序 Config Client 的配置优先级：\nbootstrap.yml（配置 Config Server 地址和应用名） Config Server 返回的远程配置 本地的 application.yml（作用被远程配置覆盖） 配置详解 Config Server 配置 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-config-server\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; server: port: 8888 spring: cloud: config: server: git: uri: https://github.com/your-repo/config-repo.git search-paths: config-files default-label: master # 默认分支 timeout: 10 # Git 连接超时（秒） force-pull: true # 拉取失败时强制覆盖本地 @SpringBootApplication @EnableConfigServer public class ConfigServerApplication { public static void main(String[] args) { SpringApplication.run(ConfigServerApplication.class, args); } } Config Client 配置 spring: application: name: my-app cloud: config: uri: http://localhost:8888 profile: dev label: master # 失败快速响应（启动时连不上 Config Server 则直接启动失败） fail-fast: true retry: initial-interval: 1000 multiplier: 1.5 max-attempts: 5 加密配置 Config Server 支持对配置内容加密，有两种方式：\n对称加密：设置 encrypt.key=your-secret-key，然后 POST 到 /encrypt 端点获取密文 非对称加密：配置 RSA 密钥对，将公钥给 Config Server，私钥用于解密 加密后的配置以 {cipher} 前缀存储在 Git 中：\nspring: datasource: password: \u0026#39;{cipher}AQB...encrypted-string...\u0026#39; 动态刷新机制 手动刷新（逐台刷新） curl -X POST http://localhost:8080/actuator/refresh 只刷新当前实例，其他实例需要逐一调用。适用于：\n实例数量少 分批蓝绿部署 全局刷新（通过 Bus） curl -X POST http://localhost:8888/actuator/bus-refresh 通过消息中间件广播刷新事件，所有订阅的客户端同时刷新。\ngraph TD; A[Config Server 接收到刷新请求] --\u003e B[通过 RabbitMQ 发送刷新事件] B --\u003e C[Config Client 接收到刷新事件] C --\u003e D[RefreshScope.refreshAll] D --\u003e E[销毁 @RefreshScope 标记的 Bean] E --\u003e F[重新初始化 Bean] F --\u003e G[配置更新完成]@RefreshScope 的局限性 只对标注了 @RefreshScope 的 Bean 有效，引用了该 Bean 的其他 Bean 不会自动重建 @ConfigurationProperties 类必须同时标注 @RefreshScope 才能刷新 静态变量不会随刷新更新 排查思路 客户端连不上 Config Server\n检查 bootstrap.yml 中的 spring.cloud.config.uri 是否正确 检查 Config Server 是否启动、网络是否可达 配置 fail-fast: true 和 retry 可以在连接失败时快速感知 配置未更新\nConfig Client 未添加 @RefreshScope 调用 refresh 接口时请求体为空（应为 POST 而非 GET） 配置在 Git 中已修改但 Config Server 缓存未刷新——设置 spring.cloud.config.server.git.refresh-rate=0 环境加载错误\n检查 Git 仓库中的文件名格式：必须为 {application}-{profile}.yml default-label 分支名需要与 Git 仓库匹配 加密配置无法解密\n检查 encrypt.key 是否在 Config Server 端正确配置 加密的文本首尾是否有 {cipher} 前缀 非对称加密时，确保公钥已在环境变量或文件配置中正确设置 Config Server 性能问题\nGit 仓库过大会拖慢首次加载——使用 spring.cloud.config.server.git.clone-on-start=false（首次访问时才克隆） 每次请求都访问 Git 仓库有性能损耗——使用本地缓存或 basedir 指向预克隆仓库 高并发场景考虑配置强制 skip-ssl-validation 或使用 SSH 而非 HTTPS 避免证书验证开销 ","date":"2024-06-11T15:40:53+08:00","permalink":"/blog/springcloudconfig/","title":"Spring Cloud Config"},{"content":"Spring Cloud Bus 的核心概念 （1）什么是 Spring Cloud Bus？ Spring Cloud Bus 是一个分布式事件总线，用于在微服务之间传播状态变化（如配置更新） 它基于消息中间件（RabbitMQ、Kafka）实现，解决 Config Server 逐台通知的痛点——没有 Bus 时，配置变更后需要逐一重启或手动调用每个客户端端点的 refresh 与 Spring Cloud Config 配合使用是主流场景，但 Bus 本身不依赖 Config——任何需要广播的事件都可以通过 Bus 传播 （2）Spring Cloud Bus 的组成 消息中间件：RabbitMQ 或 Kafka，负责传递事件 事件生产者：Config Server 或其他微服务，发送事件 事件消费者：Config Client 或其他微服务，接收事件并执行相应操作 （3）Spring Cloud Bus 的特点 全局配置刷新：通过 /actuator/bus-refresh 端点触发全局配置刷新 服务状态同步：通过事件传播实现服务状态同步 多消息中间件支持：支持 RabbitMQ 和 Kafka 定点通知：支持指定某个或某几个服务实例刷新（/actuator/bus-refresh/{destination}），避免全量广播 Spring Cloud Bus 的工作原理 （1）事件传播流程 事件生产者（如 Config Server）发送事件到消息中间件 消息中间件将事件广播给所有订阅该队列的消费者 事件消费者接收到事件并执行相应操作 关键：每个 Bus 客户端启动时会在消息中间件中创建自己的唯一队列（基于 spring.cloud.bus.id），Bus 通过这个队列实现定点通知——/bus-refresh/user-service:** 只会发送给 service ID 为 user-service 的实例。\n（2）全局配置刷新流程 管理员调用 POST /actuator/bus-refresh（向任意一个 Bus 客户端请求即可——它会通过消息中间件广播给所有实例） 该客户端发布 RefreshRemoteApplicationEvent 消息中间件广播该事件 所有 Config Client 监听到事件，调用 ContextRefresher.refresh() → RefreshScope.refreshAll() 标记了 @RefreshScope 的 Bean 被销毁并重新创建，加载新配置 Spring Cloud Bus 的配置 （1）RabbitMQ 配置 spring: rabbitmq: host: localhost port: 5672 username: guest password: guest （2）Kafka 配置 spring: kafka: bootstrap-servers: localhost:9092 consumer: group-id: my-group （3）添加依赖 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-bus-amqp\u0026lt;/artifactId\u0026gt; \u0026lt;!-- RabbitMQ --\u0026gt; \u0026lt;!-- 或 --\u0026gt; \u0026lt;!-- \u0026lt;artifactId\u0026gt;spring-cloud-starter-bus-kafka\u0026lt;/artifactId\u0026gt; --\u0026gt; \u0026lt;!-- Kafka --\u0026gt; \u0026lt;/dependency\u0026gt; （4）暴露 Actuator 端点 management: endpoints: web: exposure: include: bus-refresh Spring Cloud Bus 的使用场景 （1）全局配置刷新 # 全量刷新 curl -X POST http://localhost:8888/actuator/bus-refresh # 定点刷新（只刷新 user-service） curl -X POST http://localhost:8888/actuator/bus-refresh/user-service:** （2）自定义事件 // 发送自定义事件 @Autowired private ApplicationEventPublisher publisher; public void sendCustomEvent() { publisher.publishEvent(new MyCustomEvent(this, \u0026#34;custom-data\u0026#34;)); } // 接收自定义事件 @EventListener public void handleCustomEvent(MyCustomEvent event) { // 处理事件 } 注意：自定义事件必须继承 RemoteApplicationEvent 才能通过 Bus 传播到其他服务节点，否则只会在本地发布。\n（3）指定服务刷新的场景 当集群中不同服务配置不同时，全量刷新可能造成混乱。定点刷新适用于：\n灰度配置发布：只刷新新版本的服务实例 分批部署：一组一组刷新，观察无问题后再继续 Spring Cloud Bus 源码概览 （1）BusRefreshEndpoint @Endpoint(id = \u0026#34;bus-refresh\u0026#34;) public class BusRefreshEndpoint { private final ApplicationEventPublisher publisher; public BusRefreshEndpoint(ApplicationEventPublisher publisher) { this.publisher = publisher; } @WriteOperation public void refresh() { publisher.publishEvent(new RefreshRemoteApplicationEvent(this, null, null)); } } （2）RefreshRemoteApplicationEvent public class RefreshRemoteApplicationEvent extends RemoteApplicationEvent { public RefreshRemoteApplicationEvent(Object source, String originService, String destinationService) { super(source, originService, destinationService); } } destinationService 参数用于定点通知：null 表示广播给所有服务，指定值（如 user-service:**）则只发送给匹配的实例。\n（3）RefreshListener public class RefreshListener implements ApplicationListener\u0026lt;RefreshRemoteApplicationEvent\u0026gt; { private final RefreshScope refreshScope; public RefreshListener(RefreshScope refreshScope) { this.refreshScope = refreshScope; } @Override public void onApplicationEvent(RefreshRemoteApplicationEvent event) { refreshScope.refreshAll(); } } 常见问题排查 Bugs 刷新后配置未生效\n检查 @RefreshScope 是否标注在使用配置的 Bean 上（注意：@RefreshScope 只对加了该注解的 Bean 有效，其依赖的其他 Bean 不会重建） @ConfigurationProperties 的类只需标注 @RefreshScope 即可刷新，但已经在使用的 Bean 不会自动重新注入——需要确保引用方也标注 @RefreshScope 检查 Control Bus 端口（spring.cloud.bus.id）是否在多个服务间唯一 事件未传播到其他服务\n检查所有服务是否连接到同一个消息中间件实例（不同的 RabbitMQ vhost 或 Kafka topic 会导致隔离） 检查 spring.cloud.bus.destination 配置是否一致（默认 springCloudBus 即可） Bus refresh 导致启动过慢\nBus 客户端启动时会连接到消息中间件，如果中间件不可用，启动会卡住。可通过 spring.cloud.bus.enabled=false 禁用非必要实例的 Bus 功能 建议配置 spring.cloud.bus.env.enabled=true（默认开启），允许 Bus 端点只在特定 profile 下启用 RabbitMQ 配置正确但连接失败\n检查防火墙和端口（5672 是 AMQP 端口，15672 是管理控制台——两者不要混淆） 验证 RabbitMQ 的 vhost 和用户权限 ","date":"2024-01-05T13:42:22+08:00","permalink":"/blog/springcloudbus/","title":"Spring Cloud Bus"},{"content":"Spring Boot 的启动入口是 SpringApplication.run()，其核心流程可概括为三个阶段：准备阶段 → 刷新上下文 → 启动完成后的回调。\n一、整体启动流程 1. SpringApplication.run() 入口 2. 准备阶段： ├─ 推断应用类型（Reactive / Servlet） ├─ 加载所有 SpringApplicationRunListener ├─ 构建并准备 Environment（解析 application.yml/properties） └─ 打印 Banner 3. 创建并准备 ApplicationContext： ├─ 创建上下文实例（AnnotationConfigServletWebServerApplicationContext） ├─ 执行 ApplicationContextInitializer ├─ 注册启动参数中的主配置类（@SpringBootApplication 标注的类） └─ 调用 refresh() 方法 → 进入 Spring 容器核心流程 4. 刷新后回调： ├─ 执行 CommandLineRunner / ApplicationRunner └─ 启动内嵌 Web 容器（Tomcat / Jetty / Undertow） refresh() 方法是 Spring 容器初始化的核心，由 AbstractApplicationContext.refresh() 定义，其中 13 个步骤依次执行。\n二、refresh() 核心步骤 refresh() 中的关键步骤（按顺序）：\nprepareRefresh()：设置容器状态，初始化属性源和早期事件发布器 obtainFreshBeanFactory()：刷新 BeanFactory，解析 Bean 定义（从配置类、XML、@ComponentScan 扫描结果） prepareBeanFactory()：配置 BeanFactory 的类加载器、SpEL 解析器、属性编辑器，注册 Aware 接口回调需要的 Bean postProcessBeanFactory()：子类扩展点（Web 容器在此注册 ServletContextAware 处理器） invokeBeanFactoryPostProcessors()：调用所有 BeanFactoryPostProcessor。最关键的是 ConfigurationClassPostProcessor，它解析 @Configuration、@ComponentScan、@Import、@Bean 并注册所有 Bean 定义 registerBeanPostProcessors()：注册 BeanPostProcessor。此时只注册不执行——真正执行时机在 bean 实例化阶段触发 initMessageSource()：初始化国际化组件 initApplicationEventMulticaster()：初始化事件广播器 onRefresh()：创建并启动内嵌 Web 容器（Tomcat 在此启动） registerListeners()：注册所有的 ApplicationListener finishBeanFactoryInitialization()：实例化所有非懒加载的单例 Bean——这是启动最耗时的阶段 finishRefresh()：发布 ContextRefreshedEvent 事件 三、核心 BeanPostProcessor 详解 BeanPostProcessor 在 bean 实例化过程的两个时机触发：初始化前（BeforeInitialization） 和 初始化后（AfterInitialization）。下面列出 Spring Boot 自动装配中最常用的几个：\n依赖注入相关 AutowiredAnnotationBeanPostProcessor\n处理 @Autowired、@Value、@Inject postProcessBeforeInitialization：解析注入点并注入依赖 源码：org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor CommonAnnotationBeanPostProcessor\n处理 JSR-250 注解：@Resource、@PostConstruct、@PreDestroy @Resource 按名称优先注入，与 @Autowired 的 Type 优先策略不同——混用时注意 源码：org.springframework.context.annotation.CommonAnnotationBeanPostProcessor 生命周期回调相关 InitDestroyAnnotationBeanPostProcessor 处理 @PostConstruct 和 @PreDestroy 与 CommonAnnotationBeanPostProcessor 的职责有重叠——InitDestroyAnnotationBeanPostProcessor 更底层，CommonAnnotationBeanPostProcessor 扩展了对 @Resource 的支持 Aware 接口注入 ApplicationContextAwareProcessor 处理 ApplicationContextAware、EnvironmentAware、ResourceLoaderAware 等 postProcessBeforeInitialization 阶段注入，早于 @Autowired 占位符解析 EmbeddedValueResolverAwareBeanPostProcessor 处理 EmbeddedValueResolverAware 接口，注入 StringValueResolver 用于解析 ${property} 占位符和 #{spEL} 表达式 AOP 代理 InfrastructureAdvisorAutoProxyCreator 为符合条件的 Bean 生成 AOP 代理 postProcessAfterInitialization 阶段触发：先有 Bean 实例，再通过代理增加切面逻辑 源码：org.springframework.aop.framework.autoproxy. InfrastructureAdvisorAutoProxyCreator 异常转换 PersistenceExceptionTranslationPostProcessor 将持久化层异常（JPA、Hibernate）转换为 Spring DataAccessException 通常提前注册 BeanPostProcessor 以处理所有持久化 Bean 事件与注解驱动 EventListenerMethodProcessor\n处理 @EventListener，注册事件监听方法 源码：org.springframework.context.event.EventListenerMethodProcessor ScheduledAnnotationBeanPostProcessor\n处理 @Scheduled，注册定时任务 与 @Async 由不同处理器管理：@Async 由 AsyncAnnotationBeanPostProcessor 处理 Import 扩展 ImportAwareBeanPostProcessor 处理 @Import 导入的类中实现 ImportAware 接口的 Bean postProcessBeforeInitialization 阶段注入导入元数据 四、BeanFactoryPostProcessor ConfigurationClassPostProcessor 最重要的 BeanFactoryPostProcessor，负责解析 @Configuration 类 处理 @ComponentScan（包扫描）、@Import（导入配置类）、@Bean（方法级别 Bean 定义） 在 invokeBeanFactoryPostProcessors() 阶段执行——比任何 BeanPostProcessor 都早 没有它，所有注解配置的 Bean 都不会被注册 五、常见问题排查 启动慢怎么办？ 排查点：finishBeanFactoryInitialization 阶段耗时最长，检查是否有大量懒加载泄漏、@ComponentScan 覆盖过多包路径 启用 lazy-initialization（spring.main.lazy-initialization=true）可加速启动，但注意首次请求变慢 Bean 初始化异常如何排查？ 开启 debug=true 查看 ConditionEvaluationReport（自动配置条件评估报告） 关键日志关键词：UnsatisfiedDependencyException、BeanCreationException、NoSuchBeanDefinitionException AOP 代理不生效？ 检查类是否被 final 修饰（CGLIB 无法代理 final 类） 检查同一个类内部方法调用 AOP 代理问题（自调用不走代理） @PostConstruct 与 afterPropertiesSet 的执行顺序？ InitializingBean.afterPropertiesSet() → @PostConstruct → 自定义 init-method 顺序：InitializingBean → BeanPostProcessor.postProcessBeforeInitialization → @PostConstruct → afterPropertiesSet → init-method → BeanPostProcessor.postProcessAfterInitialization ConfigurationClassPostProcessor 的常见错误 @ComponentScan 未生效：检查启动类的位置是否在根包 @Bean 方法被 final 修饰：CGLIB 无法重写 final 方法 @Import 导入的配置类重复：Spring 5.2+ 支持 @Import 去重，但注意 @Import 和 @ComponentScan 同时扫描到同一个类会导致重复注册 ","date":"2023-05-09T23:36:22+08:00","permalink":"/blog/springbootstartflow/","title":"SpringBoot 启动流程"},{"content":" 注意：Ribbon 已进入维护状态（Netflix 不再积极开发）。从 Spring Cloud 2020.0.x（Ilford）开始，官方推荐使用 Spring Cloud LoadBalancer 作为替代。新项目直接使用 LoadBalancer，旧项目建议逐步迁移。本文内容基于 Ribbon 2.x 版本。\n核心概念 Ribbon 是 Netflix 开源的客户端负载均衡器，核心功能：\n负载均衡：从多个服务实例中选择一个处理请求 故障转移：选择的实例不可用时自动重试其他实例 重试机制：支持配置重试次数和超时 关键设计点：Ribbon 是客户端负载均衡，与 Nginx 等服务端负载均衡不同：\n特性 客户端负载均衡（Ribbon） 服务端负载均衡（Nginx） 位置 在应用内部运行 独立部署 实例感知 从注册中心获取列表 配置 upstream 故障转移 自动重试其他实例 需在 upstream 配置 部署复杂度 无需额外部署 需要独立部署和运维 工作原理 服务发现：Ribbon 从注册中心（如 Eureka）获取服务实例列表，并动态更新 负载均衡：根据配置的策略从列表中选择一个实例 故障转移：不可用时自动重试其他实例 请求分发：将请求分发到选定实例，处理响应 完整请求流程 Client Request → RestTemplate/Feign (URL: http://service-name/endpoint) → RibbonLoadBalancerClient.choose(\u0026#34;service-name\u0026#34;) → DynamicServerListLoadBalancer.getServerList() → EurekaClient.getInstancesByVipAddress() → IRule.choose(serverList) → 选择策略: 轮询/随机/加权... → 发送 HTTP 请求到选定实例 → 失败时: RetryHandler 重试其他实例 → 返回响应 负载均衡策略详解 Ribbon 提供 7 种内置策略：\n策略类 说明 适用场景 RoundRobinRule 轮询 默认策略，各实例配置均衡 RandomRule 随机 无状态服务 WeightedResponseTimeRule 加权响应时间 实例性能不均 ZoneAvoidanceRule 区域感知 多数据中心部署 BestAvailableRule 最小并发 长连接场景 RetryRule 带重试的轮询 需要自动重试 AvailabilityFilteringRule 可用性过滤 剔除熔断实例 配置方式 全局配置 ribbon: ConnectTimeout: 1000 ReadTimeout: 3000 MaxAutoRetries: 0 # 同一实例重试次数 MaxAutoRetriesNextServer: 1 # 切换实例重试次数 OkToRetryOnAllOperations: false # 是否所有操作都重试 服务级配置 service-name: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule MaxAutoRetries: 3 MaxAutoRetriesNextServer: 1 ConnectTimeout: 1000 ReadTimeout: 3000 代码配置 @RibbonClient(name = \u0026#34;service-name\u0026#34;, configuration = MyRibbonConfig.class) public class MyRibbonConfig { @Bean public IRule ribbonRule() { return new RandomRule(); } @Bean public RetryHandler retryHandler() { // 参数: maxRetriesOnSameServer, maxRetriesOnNextServer, okToRetryOnAllOperations return new DefaultLoadBalancerRetryHandler(3, 1, true); } } 注意：使用 @RibbonClient 时，配置类不能被 @ComponentScan 扫描到（否则成为全局配置），建议放在启动类所在包之外。\n使用场景 与 RestTemplate 集成 @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } // 调用时直接用服务名，Ribbon 自动解析为实例 IP:PORT String response = restTemplate.getForObject(\u0026#34;http://service-name/endpoint\u0026#34;, String.class); 与 Feign 集成 Feign 默认集成 Ribbon，无需额外配置：\n@FeignClient(name = \u0026#34;service-name\u0026#34;) public interface MyFeignClient { @GetMapping(\u0026#34;/endpoint\u0026#34;) String getResponse(); } 底层原理 ILoadBalancer 接口 Ribbon 的核心接口是 ILoadBalancer，负责管理服务实例列表：\npublic interface ILoadBalancer { void addServers(List\u0026lt;Server\u0026gt; newServers); Server chooseServer(Object key); // 选择一个实例 void markServerDown(Server server); List\u0026lt;Server\u0026gt; getServerList(boolean availableOnly); void markServerDown(Server server); } 默认实现 DynamicServerListLoadBalancer 通过 ServerListUpdater 定时从注册中心拉取最新实例：\npublic class DynamicServerListLoadBalancer\u0026lt;T extends Server\u0026gt; extends BaseLoadBalancer { private final ServerList\u0026lt;T\u0026gt; serverList; private final ServerListUpdater serverListUpdater; protected void updateListOfServers() { List\u0026lt;T\u0026gt; servers = serverList.getUpdatedListOfServers(); setServersList(servers); } } IRule 负载均衡策略接口 public interface IRule { Server choose(Object key); void setLoadBalancer(ILoadBalancer lb); ILoadBalancer getLoadBalancer(); } 从 Eureka 获取实例 public class DiscoveryEnabledNIWSServerList extends AbstractServerList\u0026lt;DiscoveryEnabledServer\u0026gt; { private final EurekaClient eurekaClient; private final String serviceId; private List\u0026lt;DiscoveryEnabledServer\u0026gt; obtainServersViaDiscovery() { List\u0026lt;InstanceInfo\u0026gt; listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(serviceId, false); for (InstanceInfo instanceInfo : listOfInstanceInfo) { if (instanceInfo.getStatus() == InstanceInfo.InstanceStatus.UP) { serverList.add(new DiscoveryEnabledServer(instanceInfo, false, true)); } } return serverList; } } 迁移到 Spring Cloud LoadBalancer 从 Spring Cloud 2020.0.x 开始，Ribbon 被 LoadBalancer 取代。迁移要点：\n移除 Ribbon 依赖：删除 spring-cloud-starter-netflix-ribbon 添加 LoadBalancer 依赖：spring-cloud-starter-loadbalancer 接口变化： ILoadBalancer → ReactiveLoadBalancer / BlockingLoadBalancerClient IRule → ReactiveLoadBalancer 实现（如 RoundRobinLoadBalancer） @RibbonClient → 无直接等价，通过 LoadBalancerClient 配置 配置变化：service-name.ribbon.* → spring.cloud.loadbalancer.* # LoadBalancer 配置示例 spring: cloud: loadbalancer: retry: enabled: true health-check: interval: 30s configurations: health-check # 启用健康检查 排查思路 负载均衡不生效（总是访问同一个实例）\n确认 @LoadBalanced 注解已在 RestTemplate Bean 上 检查服务名解析：http://service-name/ 中的 service-name 是否与注册中心的服务名一致 Ribbon 默认使用 ZoneAvoidanceRule，如果所有实例在同一 zone，行为类似轮询 实例列表未更新\n检查 ServerListRefreshInterval（默认 30 秒） 检查 Eureka Client 的 eureka.client.registryFetchIntervalSeconds（默认 30 秒） 重试不生效\nRibbon 本身不触发重试——需要配合 Spring Retry（需引入 spring-retry 依赖） 只有 @LoadBalanced 的 RestTemplate 或 Feign 客户端才会触发 Ribbon 重试 确保 MaxAutoRetries 和 MaxAutoRetriesNextServer 配置了正值 引入 LoadBalancer 后 Ribbon 失效\nSpring Cloud 2020.0.x 默认禁用了 Ribbon，如果仍想使用需要设置 spring.cloud.loadbalancer.ribbon.enabled=true（但已不推荐） ","date":"2023-04-19T21:09:25+08:00","permalink":"/blog/ribbon/","title":"Ribbon"},{"content":"RabbitMQ 知识架构 1. AMQP 协议基础 RabbitMQ 是 AMQP 0-9-1 协议的实现，其核心模型基于信道（Channel）、**交换机（Exchange）和队列（Queue）**的协作。\n为什么需要 Channel？ AMQP 的设计用意：每次请求都创建 TCP 连接代价太高（三次握手+慢启动），Channel 是 TCP 内的虚拟链路，多个 Channel 复用同一个 TCP 连接。每个 Channel 独立处理消息路由，支持多线程并发。\n相关概念：\n虚拟主机（VHost）：逻辑隔离环境，每个 VHost 拥有独立的交换机、队列和权限。一个 RabbitMQ 实例可以创建多个 VHost，用于多租户隔离。 帧（Frame）：AMQP 数据传输的最小单位，包含帧头、帧体和帧尾，用于封装消息和协议指令。 2. 消息流转原理 生产者发送消息 生产者 → TCP 连接 → Channel → basic.publish(exchange, routingKey, props, body) → Exchange 关键参数：\nexchange：目标交换机 routing_key：路由键（交换机据此路由） mandatory：true 时消息无法路由则返回错误，false 时消息丢失 delivery_mode：2 表示持久化消息 交换机类型 类型 路由规则 性能 场景 Direct 精确匹配 routing_key = binding_key 最高 点对点、单播 Fanout 广播到所有绑定队列，忽略 routing_key 高 发布订阅 Topic 通配符匹配 *（一个单词）#（多个单词） 中 按主题分类 Headers 根据消息 header 键值对匹配 低 复杂条件路由 消息确认机制 模式 行为 风险 场景 自动 ACK 消息发送后立即删除 消费者处理失败时消息丢失 可以容忍丢失 手动 ACK（basic.ack） 消费者处理完成后发送确认消息 无（除非消费者忘记 ACK） 生产环境必选 手动 ACK 的注意事项：\n不 ACK 也不拒绝，消息会一直留在队列中（unacked） 消费者断开连接后，unacked 消息重新入队（requeue），支持重试 拒绝时 requeue=true 会重新入队（可能导致死循环），requeue=false 进入死信队列 3. 持久化与可靠性 消息持久化三要素 队列 durable = true + 消息 delivery_mode = 2 + Publisher Confirm 三者缺一不可：\n队列不持久化 → 队列本身在重启后消失 消息不持久化 → 即使队列持久化，消息也只存在内存中 没有 Confirm → 不知道消息是否到达 Broker Publisher Confirm 模式 // Spring AMQP 示例: 启用 Confirm @Bean public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(connectionFactory); template.setConfirmCallback((correlationData, ack, cause) -\u0026gt; { if (ack) { // 消息成功到达 Exchange } else { // 消息到达 Exchange 失败，记录日志/重试 } }); template.setMandatory(true); template.setReturnsCallback(returned -\u0026gt; { // 消息无法路由到 Queue（mandatory=true 时触发） log.warn(\u0026#34;消息不可路由: {}\u0026#34;, returned.getMessage()); }); return template; } Confirm 和 Return 的区别：\nConfirm：确认消息是否到达 Exchange Return：确认消息从 Exchange 是否路由到 Queue（mandatory=true） 4. 死信队列（DLX） 触发条件 消息被消费者拒绝（basic.reject / basic.nack）+ requeue=false 消息 TTL 过期（expiration 属性） 队列达到最大长度（x-max-length / x-max-length-bytes） 消息超过队列的 TTL（x-message-ttl） 应用场景 延迟队列：消息设置 TTL → 过期后进入死信队列 → 消费者从死信队列消费。不需要额外插件。 使用示例 // Spring Boot 配置延迟队列（通过死信实现） @Bean public Queue normalQueue() { Map\u0026lt;String, Object\u0026gt; args = new HashMap\u0026lt;\u0026gt;(); args.put(\u0026#34;x-dead-letter-exchange\u0026#34;, \u0026#34;dlx.exchange\u0026#34;); args.put(\u0026#34;x-dead-letter-routing-key\u0026#34;, \u0026#34;dlx.routing.key\u0026#34;); args.put(\u0026#34;x-message-ttl\u0026#34;, 30000); // 30秒后过期 return new Queue(\u0026#34;normal.queue\u0026#34;, true, false, false, args); } @Bean public Queue dlq() { return new Queue(\u0026#34;dlq.queue\u0026#34;, true); } 5. 集群与高可用 普通集群 所有节点共享交换机、队列元数据 队列数据仅存储在创建节点 其他节点访问该队列时内部转发（性能损耗） 问题：队列所在节点宕机 → 队列数据丢失 镜像队列（Quorum Queue 替代） 从 RabbitMQ 3.8 起，推荐使用 Quorum Queue 替代传统镜像队列：\n特性 镜像队列 Quorum Queue 一致性协议 GM（Guaranteed Multicast） Raft 数据模型 Master-Slave Leader-Follower 网络分区处理 手动策略 自动（Raft 自动选主） 适用场景 高可用 高可用 + 强一致（推荐） 生产环境推荐：新项目直接使用 Quorum Queue。\n6. 性能优化 QoS 预取 // Spring Boot: 每次只拉取 1 条消息，处理完再拉取下一条 @Bean public SimpleRabbitListenerContainerFactory myFactory( ConnectionFactory connectionFactory) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setPrefetchCount(1); // 关键参数 return factory; } prefetchCount 的选择：\n= 1：公平分发，适合处理时间不均匀的任务，但吞吐量低 1：批量拉取，吞吐量高，但可能出现\u0026quot;慢消费者囤积任务\u0026quot;\n批量操作 批量发布：合并多个消息到单次网络帧 批量 Confirm：multiple=true 一次性确认多条消息 批量 ACK：消费者处理一批后统一 ACK 7. 常见问题排查 消息丢失\n检查队列 durable=true 检查消息 delivery_mode=2（Spring Boot 通过 spring.rabbitmq.template.mandatory=true + MessageDeliveryMode.PERSISTENT） 检查是否开启了 Confirm 模式 消息堆积\n消费者处理速度跟不上 → 增加消费者数量（concurrency 参数） 消费者处理异常一直失败 → 检查 maxLength + 死信队列兜底 消息重复消费\n消费者 ACK 超时后消息重新入队 根本解决方案：生产者幂等（每条消息 correlationId 唯一）+ 消费者侧去重（数据库唯一键 / Redis 原子操作） Linux 文件句柄限制\nRabbitMQ 打开大量文件（队列、连接），超过系统限制会崩溃 检查 ulimit -n，调整 /etc/security/limits.conf 性能瓶颈定位\n使用 rabbitmqctl status 查看内存、磁盘、连接数 管理界面监控 Queued Messages / Message Rates Spring Boot 连接 RabbitMQ 失败\n检查 spring.rabbitmq.host、port（5672 不是 15672） 检查 Virtual Host 是否存在（spring.rabbitmq.virtual-host 默认 /） 检查防火墙和认证凭据 ","date":"2023-03-03T20:54:29+08:00","permalink":"/blog/rabbitmq/","title":"RabbitMQ 深入浅出"},{"content":"一、Netty 线程模型与 Reactor 模式 主从 Reactor 多线程模型 Netty 采用主从 Reactor 模型，核心组件为EventLoopGroup ：\nBossGroup(主 Reactor)：负责监听客户端连接，通过Selector 将新连接注册到 WorkerGroup 中的某个EventLoop 。 WorkerGroup(从 Reactor)：处理已建立连接的 I/O 事件（入读写）及业务逻辑，每个EventLoop 绑定一个线程，通过轮询Selector 监听多个Channel 事件，避免线程切换开销。 主从 Reactor 配置示例\nEventLoopGroup bossGroup = new NioEventLoopGroup(1); // 主Reactor，一般只需1个线程处理连接 EventLoopGroup workerGroup = new NioEventLoopGroup(); // 从Reactor，默认线程数=CPU核心*2 ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer\u0026lt;SocketChannel\u0026gt;() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new MyBusinessHandler()); } }); 调优点： * bossGroup 线程数通常为1（除非需要多端口监听） * workerGroup 线程数根据业务类型调整： * CPU 密集型：线程数 ≈ CPU 核心数 * I/O 密集型：线程数 ≈ CPU 核心数 * 2 * 使用 Epoll 模式（Linux 环境）：\nEventLoopGroup group = new EpollEventLoopGroup(); 无锁化串行设计 Netty 通过 ChannelPipeline 将事件处理任务分配给固定的EventLoop 线程执行，保证同一连接的所有操作在同一个线程内完成，避免多线程竞争，减少锁的使用。\n二、零拷贝(Zero-Copy)的深度实现 操作系统级零拷贝 使用FileRegion 和transferTo() 方法，文件数据直接从内核缓冲区通过 DMA 传输到 Socket 缓冲区，无需用户态与内核态的数据拷贝。\nNetty 层的优化 CompositeByteBuf：合并多个ByteBuf 为逻辑视图，避免物理内存拷贝。 直接内存(Direct Buffer)：分配堆外内存，避免 JVM 堆与 Socket 缓冲区间的拷贝，但需要手动管理内存释放。 示例： FileRegion 实现文件传输(零拷贝) File file = new File(\u0026#34;largefile.zip\u0026#34;); FileInputStream fis = new FileInputStream(file); FileRegion region = new DefaultFileRegion(fis.getChannel(), 0, file.length()); ctx.channel().writeAndFlush(region).addListener(future -\u0026gt; { fis.close(); if (!future.isSuccess()) { future.cause().printStackTrace(); } }); CompositeByteBuf 合并缓冲区 ByteBuf header = Unpooled.copiedBuffer(\u0026#34;Header\u0026#34;, CharsetUtil.UTF_8); ByteBuf body = Unpooled.copiedBuffer(\u0026#34;Body\u0026#34;, CharsetUtil.UTF_8); CompositeByteBuf compositeBuf = Unpooled.compositeBuffer(); compositeBuf.addComponents(true, header, body); // 逻辑合并，无内存拷贝 调优点：\n堆外内存（Direct Buffer）分配： ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(1024); 监控 Direct Buffer 使用量（避免 OOM）： -Dio.netty.maxDirectMemory=256M # JVM参数限制最大堆外内存 三、内存管理与性能优化 内存池(PooledByteBufAllocator) Netty 通过对象池复用ByteBuf ，减少频繁的内存分配与 GC 压力。直接内存池采用 Buddy 算法管理内存块，提升分配效率。\n内存泄漏检测机制 通过ReferenceCounted 接口实现引用计数，结合ResourceLeakDetector 监控未释放的ByteBuf ，防止内存泄漏。\n内存泄漏检测配置 // 设置泄漏检测级别（生产环境建议SIMPLE） ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); // 引用计数示例 ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(); buf.retain(); // 增加引用计数 buf.release(); // 减少引用计数，当计数=0时释放内存 内存池配置 // 服务端启用内存池 ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); 调优参数 -Dio.netty.allocator.type=pooled 强制启用内存池 -Dio.netty.allocator.maxOrder=5 调整内存块大小（默认11，对应8MB块） -Dio.netty.threadLocalDirectBufferSize=0 禁用 ThreadLoad 缓存（大内存场景） 四、TCP 粘包/拆包解决方案 协议设计 定长协议：FixedLengthFrameDecoder 处理固定长度消息。 分隔符协议：DelimiterBasedFrameDecoder 按自定义分隔符（如\\n ）拆分数据。 头部长度字段协议：LengthFieldBasedFrameDecoder 解析消息头中的长度字段，动态拆分数据。 自定义编解码器 结合MessageToByteEncoder 和ByteToMessageDecoder 实现私有协议，例如 Dubbo 的协议头设计。\n// 协议格式：4字节长度 + 数据 public class CustomDecoder extends LengthFieldBasedFrameDecoder { public CustomDecoder() { super(1024 * 1024, 0, 4, 0, 4); // 最大长度1MB，长度字段偏移0 } @Override protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { ByteBuf frame = (ByteBuf) super.decode(ctx, in); if (frame == null) return null; int dataLength = frame.readInt(); byte[] data = new byte[dataLength]; frame.readBytes(data); return new String(data, StandardCharsets.UTF_8); } } // 添加解码器到Pipeline pipeline.addLast(new CustomDecoder()); pipeline.addLast(new MyBusinessHandler()); 五、心跳机制与长连接管理 IdleStateHandler 监控读写空闲时间，触发IdleStateEvent 事件，结合业务实现心跳包发送或断连处理。例如：\npipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS)); pipeline.addLast(new HeartbeatHandler()); // 自定义处理空闲事件 TCP Keep-Alive 与应用层心跳 TCP 层 Keep-Alive 间隔较长（默认2小时），应用层心跳（如每30秒一次）更灵活，可快速检测网络异常。\n六、高性能背后的设计哲学 异步事件驱动模型 基于NIO的非阻塞 I/O，通过ChannelFuture 实现异步回调，避免线程阻塞。\n责任链模式(ChannelPipeline) 将编解码、业务逻辑等ChannelHanlder 串联成链，事件按顺序传播，支持动态扩展。\n线程局部变量(FastThreadLocal) Netty 优化了 JDK 的ThreadLocal，减少哈希冲突，提升访问速度。\n七、高性能调优参数 Linux 系统参数调优 # 最大文件描述符数 ulimit -n 1000000 # TCP缓冲区设置 sysctl -w net.core.rmem_max=16777216 sysctl -w net.core.wmem_max=16777216 sysctl -w net.ipv4.tcp_rmem=\u0026#34;4096 87380 16777216\u0026#34; sysctl -w net.ipv4.tcp_wmem=\u0026#34;4096 65536 16777216\u0026#34; Netty 关键参数 // 服务端配置 bootstrap.option(ChannelOption.SO_BACKLOG, 1024) // 等待连接队列大小 .option(ChannelOption.SO_REUSEADDR, true) // 端口复用 .childOption(ChannelOption.TCP_NODELAY, true) // 禁用Nagle算法 .childOption(ChannelOption.SO_KEEPALIVE, true); // 启用TCP Keep-Alive JVM 参数调优 # 堆外内存限制 -XX:MaxDirectMemorySize=512m # GC优化（G1为例） -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=30 八、高并发场景实战 百万连接优化： 调整 Epoll 事件触发模式 EpollEventLoopGroup group = new EpollEventLoopGroup(); bootstrap.channel(EpollServerSocketChannel.class); // 使用Epoll边缘触发 减少上下文切换： workerGroup.setIoRatio(70); // I/O任务占比70%，业务任务30% 连接数监控 ChannelGroup allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); allChannels.add(channel); // 维护所有活跃Channel 异步处理耗时操作 // 业务Handler中提交异步任务 public void channelRead(ChannelHandlerContext ctx, Object msg) { executorService.execute(() -\u0026gt; { Object result = heavyCompute(msg); // 耗时操作 ctx.writeAndFlush(result); }); } 九、重要知识点 如何避免 Epoll 空轮询 Bug？ Netty 通过重建 Selector 解决 JDK NIO 的空轮询问题：当 Selector 空轮询次数超过阈值，重新注册 Channel 到新 Selector。\nChannel.write() 与 ChannelHandlerContext.write() 的却别？ Channel.write() 从 Pipeline 尾部开始传播，ChannelHandlerContext.write() 从当前 Handler 的下一个节点开始，影响事件处理路径。\n如何实现百万级长连接？ 调整系统参数：最大文件描述符数、TCP 缓冲区大小。 是用 Epoll 边缘触发模式（EpollEventLoopGroup ） 优化内存管理，避免频繁 GC。 内存泄漏排查 使用-Dio.netty.leakDetection.level=paranoid 开启详细日志 通过jmap -histo:live \u0026lt;pid\u0026gt; 查看 Direct Buffer 数量 CPU 100%排查 top -Hp \u0026lt;pid\u0026gt; 找到高 CPU 线程 jstack \u0026lt;pid\u0026gt; 查看线程堆栈，定位 EventLoop 阻塞点 网络拥塞分析 使用netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]} 监控 TCP 状态 通过 Wireshark 抓包分析粘包/拆包 ","date":"2022-06-22T13:16:53+08:00","permalink":"/blog/netty/","title":"Netty知识"},{"content":"一、Nacos 核心概念 是什么？ Nacos（Dynamic Naming and Configuration Server）是阿里开源的服务发现+配置管理+服务治理平台，核心功能：\n服务发现与健康监测：服务注册、心跳机制、DNS 查询 动态配置管理：配置发布、监听、版本管理 动态 DNS：基于权重的路由、地域就近访问 服务元数据管理：支持应用/版本/环境等元数据 关键特性 双模式：支持 CP（强一致性、如配置管理）和 AP（高可用，如服务发现） 配置实时推送：基于长轮询（减少无效请求） 生态兼容：无缝集成 Spring Cloud、Dubbo、Kubernetes 二、核心架构与原理 服务注册与发现流程 注册：服务启动时向 Nacos Server 发送注册请求（HTTP/gRPC） 心跳：客户端定期发送心跳（默认5秒），超时（15秒）标记为不健康，30秒后剔除 发现：消费者通过订阅机制获取服务列表，本地缓存+增量更新 配置管理机制 发布配置：通过控制台/API发布配置，存储于 Derby 或 MySQL 监听配置：客户端长轮询（默认30秒），服务端 Hold 住请求，配置变更立即返回 历史版本：支持配置回滚和版本对比 一致性协议 CP 模式：使用 Raft 协议（配置管理场景） AP 模式：自研 Distro 协议（服务发现场景，最终一致性） 三、重要知识点 Nacos vs Eureka vs Zookeeper？ Eureka：AP 设计，适合服务发现，但功能单一 Zookeeper：CP 设计，强一致性，但写性能低 Nacos：AP/CP 可切换，集成服务发现+配置管理，生态更全面 长轮询如何实现配置实时推送？ 客户端发起查询请求，若配置无变化，服务端 Hold 请求至超时（默认30秒）或期间配置变更立即返回 对比传统轮询：减少无效请求，提升实时性 对比 WebSocket：长轮询服务端实现更简单，但存在30秒最大延迟（非实时配置场景选长轮询即可，实时性要求极高场景考虑 gRPC 流式推送） Nacos 集群如何保证数据一致性？ Distro 模式（AP 模式）：每个节点负责部分数据，通过异步复制保证最终一致性。写入时先写本地，再异步复制到其他节点；读取时可能读到旧数据，但通常可接受（服务发现场景对一致性要求不高）。 Raft 协议（CP 模式）：选举 Leader，多数节点写成功才返回。适用于配置管理——配置错了影响面大，宁可不更新也不能不一致。 Nacos 2.0 升级了哪些内容？ 通信层优化：gRPC 替代 HTTP，减少连接数（1.x 每个客户端与 server 维持多个 HTTP 长连接，2.0 全复用为一条 gRPC 连接） 长连接增强：支持服务端主动推送，降低延迟 性能提升：支撑百万级服务实例 如何实现配置灰度发布？ 通过 Data ID 或 Group 分组，结合 Spring Cloud 的 @RefreshScope 或监听 ConfigChangeEvent 实现部分实例更新 更优雅的方式：使用 Nacos 的 Beta 发布功能——指定目标 IP 列表，只有命中 IP 的客户端拉取到灰度配置。验证无误后再全量发布。 四、实战场景与排查思路 服务注册失败常见原因 网络不通：检查 Nacos Server 地址和端口（telnet 或 nc 测试连通性） 版本冲突：Spring Cloud Alibaba 与 Nacos 版本不兼容 → 参考官方版本对应表 命名空间/Group 配置错误：客户端 namespace 和 group 须与服务端一致，命名空间为空时默认使用 public 防火墙/安全组拦截：gRPC 端口（9848/9849）需额外开放 配置不生效排查步骤 检查控制台配置是否发布成功（Data ID、Group、命名空间与客户端代码匹配） 检查 @RefreshScope 是否加在正确的位置——标注在 @Configuration 类上可能导致整个类重新创建，注意副作用 查看日志：com.alibaba.nacos.client.config.listener 包日志是否有监听异常 长轮询超时确认：查看请求日志中 HTTP 状态码是否为 200（非 304 表示配置变更） Nacos 集群部署注意事项 至少 3 个节点形成 Raft 选举（节点数建议奇数，避免脑裂） 使用 MySQL 作为外部存储（Derby 仅适合单机测试——集群模式下不使用 MySQL 会导致各节点数据不一致） 所有节点配置文件中的 cluster.conf 必须包含所有节点地址 通过 Nginx 做负载均衡时，gRPC 长连接需要配置 upstream 为 ip_hash 或使用 nginx-stream 模块 五、高级话题 Nacos-Sync 应用 跨注册中心同步（如 Kubernetes Service 到 Nacos、Eureka 迁移到 Nacos） 本质是事件监听+双向同步，注意循环同步问题（A→B→A 死循环），Nacos-Sync 通过记录同步来源避免 安全控制 开启鉴权：nacos.core.auth.enabled=true，配置自定义用户角色 重要：Nacos 2.2.3 之前默认鉴权不开启，且密钥为硬编码，务必修改 nacos.core.auth.plugin.nacos.token.secret.key 双活部署 多数据中心通过 Nacos-Sync 同步数据，结合 DNS 实现流量切换 注意：跨地域同步存在网络延迟，配置变更可能在数秒后才能传播到异地——不要依赖 Nacos 跨地域强一致 ","date":"2022-05-03T20:54:29+08:00","permalink":"/blog/nacos/","title":"Nacos"},{"content":"Java Memory Model（JMM）是 Java 虚拟机规范中定义的一组规则，用于屏蔽不同硬件和操作系统的内存访问差异，保证 Java 程序在多线程环境下的原子性、可见性、有序性。\n一、为什么需要 JMM？ 多线程环境下，每个 CPU 核心都有自己的缓存（L1/L2/L3），主内存中的数据被多个核心同时读写时会出现三个问题：\n可见性：一个线程修改变量，另一个线程可能看不到 原子性：多个操作组合在一起可能被其他线程干扰 有序性：编译器和处理器可能对指令重排序，导致执行顺序与代码顺序不一致 JMM 定义了主内存和工作内存的交互规范，以及 happens-before 规则来约束重排序。\n二、JMM 的内存操作 JMM 定义了 8 种内存操作来完成变量在主内存和工作内存之间的同步：\nlock：作用于主内存，将变量锁定为当前线程独享 unlock：作用于主内存，释放锁定 read：作用于主内存，将变量值从主内存传输到工作内存 load：作用于工作内存，将 read 操作传输来的值放入工作内存副本 use：作用于工作内存，将工作内存中的变量值传递给执行引擎 assign：作用于工作内存，将执行引擎计算的值赋给工作内存副本 store：作用于工作内存，将工作内存中的变量值传送到主内存 write：作用于主内存，将 store 操作传来的值写入主内存 这 8 个操作必须满足以下规则：\nread 和 load、store 和 write 必须成对出现 assign 不能单独发生（必须有 use 先获取值才能 assign） 变量在工作内存中改变后必须同步回主内存 单线程下的基本流程 sequenceDiagram participant 主内存 participant 工作内存 participant 线程 Note over 主内存,工作内存: 读取变量（主内存 → 工作内存） 线程 -\u003e\u003e 主内存: read(var) 主内存 --\u003e\u003e 工作内存: 传输变量值 线程 -\u003e\u003e 工作内存: load(var) Note over 工作内存: 修改变量（工作内存内操作） 线程 -\u003e\u003e 工作内存: assign(var, newValue) Note over 工作内存,主内存: 写回变量（工作内存 → 主内存） 线程 -\u003e\u003e 工作内存: store(var) 工作内存 --\u003e\u003e 主内存: 传输变量值 线程 -\u003e\u003e 主内存: write(var)三、三大特性详解 原子性问题 多个独立操作组合在一起不是原子的。典型场景：count++ 实际上是 read→load→use→assign→store→write 六个步骤，线程 A 和线程 B 可能同时执行到中间的 use 步骤。\nsequenceDiagram participant 主内存 participant 工作内存A participant 线程A participant 工作内存B participant 线程B rect rgba(255, 0, 0, 0.1) Note left of 线程A: 线程A操作 线程A -\u003e\u003e 主内存: read(var) 主内存 --\u003e\u003e 工作内存A: var=0 线程A -\u003e\u003e 工作内存A: load(var) 线程A -\u003e\u003e 工作内存A: assign(var, 1) 线程A -\u003e\u003e 工作内存A: store(var) 工作内存A --\u003e\u003e 主内存: 准备写入var=1 end rect rgba(0, 255, 0, 0.1) Note right of 线程B: 线程B操作（插入A的store→write之间） 线程B -\u003e\u003e 主内存: read(var)（主内存仍为0） 主内存 --\u003e\u003e 工作内存B: var=0 线程B -\u003e\u003e 工作内存B: load(var) 线程B -\u003e\u003e 工作内存B: assign(var, 1) 线程B -\u003e\u003e 工作内存B: store(var) 工作内存B --\u003e\u003e 主内存: 写入var=1 主内存 --\u003e\u003e 线程B: write(var)完成 end rect rgba(255, 0, 0, 0.1) Note left of 线程A: 线程A完成写入（被覆盖） 主内存 --\u003e\u003e 线程A: write(var)（最终var=1，预期为2） end解决方式：使用 synchronized 或 Lock 保证原子性，或使用 AtomicInteger 等 CAS 操作。\n可见性问题 线程 B 修改了变量，线程 A 可能仍然使用自己工作内存中的旧副本。原因：A 没有及时从主内存重新 read→load。\nvolatile 解决可见性：对 volatile 变量的写操作会立即执行 store→write 刷新到主内存，读操作会强制从主内存 read→load，而不是使用工作内存缓存的旧值。\nsequenceDiagram participant 主内存 participant 工作内存A participant 线程A participant 工作内存B participant 线程B rect rgba(0, 255, 0, 0.1) Note left of 线程A: 线程A操作（volatile变量） 线程A -\u003e\u003e 主内存: read(var) 主内存 --\u003e\u003e 工作内存A: var=0 线程A -\u003e\u003e 工作内存A: load(var) 线程A -\u003e\u003e 工作内存A: assign(var, 1) 线程A -\u003e\u003e 工作内存A: store(var) 工作内存A --\u003e\u003e 主内存: 立即写入var=1 主内存 --\u003e\u003e 线程A: write(var)完成 end rect rgba(0, 0, 255, 0.1) Note right of 线程B: 线程B操作（强制读取最新值） 线程B -\u003e\u003e 主内存: read(var)（必须读取var=1） 主内存 --\u003e\u003e 工作内存B: var=1 线程B -\u003e\u003e 工作内存B: load(var) end有序性问题 编译器和处理器为了优化性能会对指令重排序，但在多线程下可能导致意外的执行顺序。例如经典的双重检查锁单例：\n// 未正确使用 volatile 的版本 private static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (this) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 可能被重排序 } } } return instance; } instance = new Singleton() 实际为三步操作：\n分配内存空间 初始化对象 将引用指向内存地址 步骤 2 和 3 可能被重排序，导致另一个线程获得一个未初始化完成的对象。volatile 通过禁止指令重排序解决了这个问题。\n四、happens-before 规则 happens-before 是 JMM 的核心规则，用于判断两个操作之间是否存在数据竞争：\n程序顺序规则：一个线程中的每个操作 happens-before 该线程的后续操作 volatile 变量规则：对 volatile 变量的写 happens-before 后续对该变量的读 锁规则：解锁 happens-before 后续对同一把锁的加锁 传递性：A happens-before B 且 B happens-before C → A happens-before C 线程启动规则：Thread.start() happens-before 被启动线程中的任何操作 线程 join 规则：线程中所有操作 happens-before 其他线程对该线程的 Thread.join() 成功返回 五、volatile vs synchronized 对比 特性 volatile synchronized 可见性 即时可见 通过解锁刷新到主内存实现 原子性 不保证 保证 有序性 禁止指令重排序 通过加解锁保证有序 性能 无锁，开销小 有锁，涉及线程挂起和唤醒 使用场景 状态标记、单次写入 复合操作、需要原子性的场景 常见误区 volatile 不能替代 synchronized：volatile 仅保证可见性和有序性，不保证原子性（如 count++ 仍然有并发问题） final 也有可见性保证：JMM 保证构造函数结束后，final 字段对其他线程可见（除非发生 this 引用逸出） happens-before 规则不是时间顺序：它不要求物理时间上 A 先于 B 执行，只要求在语义上 A 的结果对 B 可见 ","date":"2021-07-09T23:36:22+08:00","permalink":"/blog/javamemorymodel/","title":"JMM"},{"content":"可正常运行系统： 通过jmap来查看JVM中各个区域的使用情况 通过jstack来查看线程的运行情况，比如哪些线程阻塞、是否出现死锁 通过jstat来查看垃圾回收情况，特别是fullgc，如果发现fullgc比较频繁，那么就得进行调优 通过各个命令的结果或者jvisualvm等工具来进行分析 首先：初步猜测频发发送fullgc的原因，如果频繁发生fullgc，但是又一直没有出现内存溢出，那么表示fullgc实际上回收了很多对象，所以这些对象最好能在younggc过程中直接回收掉，避免这些对象进入到老年代，对于这种情况，就要考虑这些存活时间不长的对象是不是比较大，导致年轻代放不下，直接进入到老年代，尝试加大年轻代的大小，如果改完之后，fullgc减少，则证明修改有效 同时，还可以找到占用CPU最多的线程，定位到具体的方法，优化这个方法的执行，看是否能避免某些对象的创建，从而节省内存 运行中发生OOM的系统： 一般生产系统中都会设置当前系统发生了OOM时，生成当时的dump文件（-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base） 我们可以利用jvisualvm等工具来分析dump文件 根据dump文件找到异常的实例对象和异常的线程（占用CPU高），定位到具体代码 然后再进行详细的分析和调试 ","date":"2020-07-12T13:16:53+08:00","permalink":"/blog/jvmissuesolution/","title":"JVM 问题排查"},{"content":"Kubernetes 核心概念 为什么需要 Kubernetes？ 容器化解决了环境一致性问题，但生产环境需要管理成百上千个容器——自动部署、扩缩容、负载均衡、服务发现、滚动更新。Kubernetes 提供了这些能力的声明式编排框架。\n架构总览 ┌─────────────────────────────────────────┐ │ Control Plane │ │ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │ │ API Svr │ │Scheduler │ │Ctrl Mgr │ │ │ └────┬─────┘ └────┬─────┘ └────┬────┘ │ │ │ │ │ │ │ ┌────▼──────────────────────────▼────┐ │ │ │ etcd │ │ │ └───────────────────────────────────┘ │ └──────────────────┬──────────────────────┘ │ ┌──────────────────▼──────────────────────┐ │ Node │ │ ┌──────────┐ ┌──────────┐ ┌────────┐ │ │ │ Kubelet │ │Kube-Proxy│ │Runtime │ │ │ └────┬─────┘ └────┬─────┘ └───┬────┘ │ │ │ │ │ │ │ ┌────▼─────────────▼───────────▼────┐ │ │ │ Pods │ │ │ └──────────────────────────────────┘ │ └────────────────────────────────────────┘ 核心组件设计原理 组件 作用 为什么这样设计 API Server 提供 RESTful API，是集群入口 所有组件只与 API Server 通信，避免组件间直接耦合——这是 K8s 的核心设计哲学：声明式 + 控制循环 etcd 分布式 KV 存储，保存集群全部状态 基于 Raft 共识算法保证一致性。为什么选 etcd？强一致、watch 机制（支持控制器监听变更）、CNCF 项目 Scheduler 将 Pod 分配到最优 Node 筛选（Predicates）→ 打分（Priorities）两阶段算法，而非简单轮询，因为需要考虑资源、亲和性、污点等多维约束 Controller Manager 运行各种控制器（Deployment、Node、Endpoints 等） 控制循环模式（Reconciliation Loop）：期望状态 vs 实际状态，不断调谐直到一致 Kubelet 管理 Pod 生命周期 节点上的 Agent，负责创建/删除容器、健康检查、上报节点状态 Kube-Proxy 网络代理和负载均衡 维护 iptables/IPVS 规则，将 Service IP 转发到后端 Pod 关键设计：声明式 API + 控制循环 K8s 的设计核心不是命令式（\u0026ldquo;启动 3 个 nginx\u0026rdquo;），而是声明式（\u0026ldquo;确保有 3 个 nginx 在运行\u0026rdquo;）。用户提交期望状态（通过 YAML），控制器监听 API Server 的变化，不断调谐直到实际状态与期望状态一致。这种模式的好处：\n自动恢复：Pod 挂了，ReplicaSet 控制器自动补一个 幂等：同样的 YAML 应用多次结果一致 可审计：所有变更记录在 etcd 中 核心资源对象详解 Pod Pod 是最小调度单元。为什么不是容器？因为有些场景多个容器需要共享网络栈（如 Sidecar 模式），K8s 将它们打包为 Pod，共享 IP、端口、存储卷。\nPod 生命周期：Pending → Running → Succeeded/Failed → Unknown\n关键阶段说明： - Pending：API Server 已接受，但 Pod 尚未调度到 Node（或镜像拉取中） - Running：至少有一个容器在运行 - Unknown：Node 失联（kubelet 停止上报心跳） Deployment Deployment 管理 ReplicaSet，ReplicaSet 管理 Pod。为什么中间加一层 ReplicaSet？为了实现滚动更新与回滚——每发布一次，创建一个新 RS，逐步扩容新 RS 并缩容旧 RS，回滚时直接指回旧 RS。\napiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: replicas: 3 revisionHistoryLimit: 10 # 保留最近的 10 个版本用于回滚 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # 滚动期间最多允许超出 1 个副本 maxUnavailable: 0 # 滚动期间不允许有不可用副本 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi 滚动更新流程：\n创建新 ReplicaSet（副本数为 0） 新 RS +1，旧 RS -1（maxSurge=1, maxUnavailable=0 意味着始终保持 3 个可用 Pod） 重复直到新 RS 达到 3，旧 RS 缩到 0 Service Pod 的 IP 不固定（重启、扩缩容后会变），Service 提供稳定的访问入口。实现方式：kube-proxy 在节点上维护 iptables/IPVS 规则。\nService 类型选择：\n类型 访问范围 实现方式 场景 ClusterIP 集群内 iptables/IPVS 转发 内部服务调用 NodePort 集群外（节点 IP:端口） 每个节点监听端口 → iptables 开发调试、无 LB LoadBalancer 公网 云厂商 LB + NodePort 生产对外服务 ExternalName DNS 映射 CoreDNS 返回 CNAME 访问外部服务 apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: nginx ports: - protocol: TCP port: 80 targetPort: 80 type: ClusterIP ConfigMap 和 Secret 为什么需要 ConfigMap？将配置与容器镜像解耦，镜像不变、配置可变。\napiVersion: v1 kind: ConfigMap metadata: name: app-config data: application.yml: | server: port: 8080 database: url: jdbc:mysql://db:3306/app Secret 存储敏感信息，数据以 Base64 编码（非加密——需要配合 RBAC 和加密存储）。\napiVersion: v1 kind: Secret metadata: name: db-secret type: Opaque data: username: YWRtaW4= password: MWYyZDFlMmU2N2Rm 注意：ConfigMap/Secret 更新后，Pod 中的环境变量不会自动更新，只有通过 Volume 挂载的方式才会自动同步（默认约 60 秒）。\n网络模型 K8s 网络模型的核心约定——每个 Pod 都有独立 IP，Pod 之间可以直接通信（无需 NAT）。\nCNI 插件原理 K8s 不自己实现网络，而是通过 CNI（Container Network Interface）插件接入。工作流程：\nPod 创建 → kubelet 调用 CNI 插件 → 插件创建 veth pair → 一端 Pod 内，一端连到网桥/路由 常见 CNI 对比：\nCNI 数据平面 特点 Flannel VXLAN/UDP 简单，性能一般，适合小集群 Calico BGP + iptables 纯三层，性能好，支持 NetworkPolicy Cilium eBPF 性能极高，支持 L7 策略，但要求内核 5.8+ Weave VXLAN + fastdp 自动组网，跨主机通信简单 Service 网络：ClusterIP 通过 kube-proxy 的 iptables/IPVS 规则将虚拟 IP 映射到后端 Pod。IPVS 比 iptables 更适合大规模集群（iptables 的规则复杂度为 O(n)，IPVS 为 O(1)）。\nDNS：CoreDNS 为 Service 解析域名，格式为 \u0026lt;service\u0026gt;.\u0026lt;namespace\u0026gt;.svc.cluster.local。\n存储管理 Volumes 类型与选择 类型 生命周期 场景 emptyDir Pod 生命周期 临时缓存、Sidecar 数据交换 hostPath Node 生命周期 需要读取宿主机数据 PersistentVolumeClaim 独立于 Pod 数据库等需持久化存储 PV/PVC 设计原理 为什么设计 PV 和 PVC 两层抽象？将存储的提供和存储的使用分离：\nPV：由集群管理员预先创建或通过 StorageClass 动态供应 PVC：用户声明存储需求（大小、访问模式），与 PV 匹配后绑定 # PV apiVersion: v1 kind: PersistentVolume metadata: name: pv001 spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce hostPath: path: /data --- # PVC apiVersion: v1 kind: PersistentVolumeClaim metadata: name: myclaim spec: accessModes: - ReadWriteOnce resources: requests: storage: 3Gi PV 回收策略：Retain（管理员手动回收）→ 生产环境推荐 / Delete（自动删除）→ 动态供应场景 / Recycle（已废弃，不建议使用）\n调度原理 两阶段调度算法 Scheduler 对每个待调度 Pod 执行两步：\nPredicates（筛选）：过滤不满足条件的 Node。包括：\nPodFitsResources：节点资源足够 MatchNodeSelector：匹配 nodeSelector 标签 PodToleratesNodeTaints：Pod 能容忍 Node 的污点 CheckNodePortConflict：端口不冲突 CheckNodeAffinity：满足节点亲和性 Priorities（打分）：对候选 Node 按策略评分：\nLeastRequestedPriority：资源利用率低的 Node 得分高（分散 Pod） BalancedResourceAllocation：CPU 和内存均衡使用 ImageLocalityPriority：Pod 镜像已有的 Node 得分高 NodeAffinity、TaintToleration 等优先级 节点选择器与亲和性 # nodeSelector：简单的标签匹配 spec: nodeSelector: disktype: ssd # 节点亲和性：更灵活的表达 spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: # 硬约束 nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: [\u0026#34;ssd\u0026#34;] preferredDuringSchedulingIgnoredDuringExecution: # 软约束 - weight: 80 preference: matchExpressions: - key: zone operator: In values: [\u0026#34;us-east-1\u0026#34;] Pod 亲和性与反亲和性 spec: affinity: podAntiAffinity: # 确保同一服务的 Pod 分散在不同节点 requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: nginx topologyKey: kubernetes.io/hostname topologyKey 指定了\u0026quot;分散的范围\u0026quot;——这里是 hostname，即不同宿主机。\n污点与容忍 污点（Taint）阻止 Pod 调度到特定 Node，容忍（Toleration）允许 Pod\u0026quot;忍受\u0026quot;污点。\n# 给节点打污点 # kubectl taint nodes node1 key=value:NoSchedule # Pod 容忍 spec: tolerations: - key: \u0026#34;key1\u0026#34; operator: \u0026#34;Equal\u0026#34; value: \u0026#34;value1\u0026#34; effect: \u0026#34;NoSchedule\u0026#34; 污点效果：NoSchedule（不调度新 Pod）/ PreferNoSchedule（尽量不调度）/ NoExecute（驱逐已有 Pod）\n安全机制 RBAC K8s 的授权体系基于 RBAC（最小权限原则）。\napiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: default name: pod-reader rules: - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;pods\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;list\u0026#34;] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: read-pods namespace: default subjects: - kind: User name: developer apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io ClusterRole vs Role：Role 作用于特定 Namespace，ClusterRole 作用于集群范围。\n高可用设计 Master 高可用 Load Balancer → API Server × 3 → etcd cluster × 3（奇数节点） etcd 基于 Raft 协议，3 节点允许 1 个故障，5 节点允许 2 个故障。Scheduler 和 Controller Manager 通过 Leader Election 选主。\nPod 高可用 Deployment 多副本 + 反亲和性确保跨节点分布 PDB（PodDisruptionBudget）确保更新时最少存活副本数 HPA 根据指标自动伸缩 节点故障处理 Node 失联后：\n默认 40 秒后 Node 变为 NotReady（node-monitor-period 5 秒 × node-monitor-grace-period 40 秒 × 8 次失败） Controller Manager 的 NodeLifecycleController 驱逐 Pod（pod-eviction-timeout 默认 5 分钟） 被驱逐的 Pod 在其他 Node 上重建 常见问题排查 Pod 一直 Pending\nkubectl describe pod \u0026lt;name\u0026gt; 查看 Events 常见原因：节点资源不足、PVC 未绑定、镜像拉取失败、端口冲突 检查 kubectl get nodes 是否有 Ready 节点 Pod 不断 CrashLoopBackOff\nkubectl logs \u0026lt;pod\u0026gt; 查看日志 kubectl logs \u0026lt;pod\u0026gt; --previous 查看上次启动的日志 常见原因：启动参数错误、依赖服务未就绪、配置错误 Node 一直 NotReady\n排查 kubelet 是否运行：systemctl status kubelet 检查 kubelet 日志：journalctl -u kubelet -f docker info（或 crictl info）确认容器运行时正常 检查磁盘空间：df -h（磁盘满会导致 Node NotReady） Service 不通\n检查 Endpoint：kubectl get endpoints \u0026lt;service\u0026gt;——为空说明 Pod 未匹配到 Service selector 检查 CoreDNS：kubectl run -it test --image=busybox -- wget \u0026lt;service\u0026gt;——DNS 解析是否正常 检查 kube-proxy 能否正常工作：iptables-save | grep \u0026lt;service-name\u0026gt; 存储异常\n检查 PV/PVC 状态：kubectl get pv,pvc——Pending 说明未绑定 检查 StorageClass 是否正确创建——动态供应时需要默认 StorageClass 检查宿主机目录权限——hostPath 场景需确认目录存在且权限正确 etcd 性能问题\netcdctl check perf 检查读写延迟 避免单 etcd 成员存储过大（建议 8GB 限制） 避免频繁大量写入（如频繁创建/删除 Pod） ","date":"2020-03-24T13:16:53+08:00","permalink":"/blog/kubernetes/","title":"Kubernetes 深入浅出"},{"content":"功能 服务熔断 服务降级 资源隔离 请求缓存 请求合并 核心思想 防止服务雪崩 当一个服务发生故障时，避免故障扩散到整个系统 快速失败 通过熔断机制快速失败。避免资源耗尽。 优雅降级 在服务不可用的时候，提供备用方案（Fallback） 熔断器（Circuit Breaker） 原理 当服务的容错率超过阈值时，熔断器会被打开，后续请求会直接失败，不再调用目标服务 熔断器打开一段时间后，会进入半打开状态，尝试恢复服务 关键参数 circuitBreaker.requestVolumeThreshold：触发熔断器需要最小请求数 circuitBreaker.errorThresholdPercentage：错误率阈值（默认为50%） circuitBreaker.sleepWindowInMilliseconds：熔断器打开后的休眠时间（默认为5秒） 资源隔离 线程池隔离 每个服务调用使用独立的线程池，避免资源竞争 通过 HystrixThreadPoolKey 配置线程池 信号量隔离 使用信号量限制并发数，适用于轻量级调用（执行时间短的任务） 服务降级 原理 当服务调用失败或者熔断器打开时，执行降级逻辑 降级逻辑可以是返回默认值、调用备用服务等 实现 通过 HystrixCommand(fallbackMethod = \u0026ldquo;fallbackMethod\u0026rdquo;) 指定降级方法 请求缓存 原理 对相同请求参数，Hystrix 可以缓存结果，减少重复调用 实现 使用 @CacheResult 和 @CacheKey 注解 请求合并 原理 将多个请求合并为一个批量请求，减少网络开销 实现 使用 @HystrixCollapser 注解 工作流程 请求进入： 请求被 Hystrix 拦截 检查熔断器状态 如果熔断器打开，直接执行降级逻辑 资源隔离 根据配置选择线程池隔离或信号量隔离 执行目标服务 调用目标服务，记录成功或者失败 更新熔断器状态 根据调用结果更新熔断器状态 执行降级逻辑 如果调用失败或者熔断器打开，执行降级逻辑 返回结果 返回调用结果或者降级结果 sequenceDiagram participant Client as 客户端 participant Hystrix as Hystrix participant TargetService as 目标服务 participant Fallback as 降级逻辑 Client-\u003e\u003eHystrix: 1. 发起请求 Hystrix-\u003e\u003eHystrix: 2. 检查熔断器状态 alt 熔断器关闭 Hystrix-\u003e\u003eHystrix: 3. 资源隔离（线程池/信号量） Hystrix-\u003e\u003eTargetService: 4. 调用目标服务 TargetService--\u003e\u003eHystrix: 5. 返回结果 Hystrix-\u003e\u003eHystrix: 6. 更新熔断器状态 alt 调用成功 Hystrix--\u003e\u003eClient: 7. 返回结果 else 调用失败 Hystrix-\u003e\u003eFallback: 8. 执行降级逻辑 Fallback--\u003e\u003eHystrix: 9. 返回降级结果 Hystrix--\u003e\u003eClient: 10. 返回降级结果 end else 熔断器打开 Hystrix-\u003e\u003eFallback: 11. 执行降级逻辑 Fallback--\u003e\u003eHystrix: 12. 返回降级结果 Hystrix--\u003e\u003eClient: 13. 返回降级结果 end底层原理 熔断器实现 状态机 熔断器有三种状态：CLOSED （关闭）、OPEN（打开）、HALF_OPEN（半开） 状态转换基于容错率和请求量 滑动窗口 使用滑动窗口统计最近一段时间的请求成功率和错误率 线程池隔离 线程池管理 每个服务调用使用独立的线程池，避免资源竞争 线程池大小通过 **coreSize **和 **maxSize **配置。 队列管理 使用有界队列（BlockingQueue）控制请求排队 信号量隔离 信号量机制 使用 **Semaphore **控制并发请求书 适用于轻量级调用，减少线程切换开销 请求缓存 缓存实现 使用 ConcurrentHashMap 存储缓存结果 通过 @CacheKey 指定缓存键 请求合并 合并实现 使用 BatchCommand 将多个请求合并为一个批量请求 通过 @HystrixCollapser 配置合并规则 ","date":"2019-05-22T15:49:23+08:00","permalink":"/blog/hystrix/","title":"Hystrix"},{"content":"为什么需要 HBase？ 传统关系型数据库在数据量达到 TB 级、写入吞吐量达到每秒百万行时会出现瓶颈：\nMySQL 分库分表后跨节点 Join/聚合困难 写入吞吐受限于单机 B+Tree 的随机写性能 HBase 的解决思路：LSM-Tree + 列式存储 + 水平扩展。它将随机写转为顺序写（先写内存再刷盘），通过 Region 自动分裂实现水平扩展。\n1. HBase 核心组件 1.1 HMaster 源码路径：org.apache.hadoop.hbase.master.HMaster\n核心职责：\n元数据管理：处理 DDL（创建/删除/修改表），维护 hbase:meta 表 负载均衡：监控 RegionServer 负载，分配/迁移 Region 故障恢复：RegionServer 宕机时重新分配其 Region 启动流程：\n连接 ZooKeeper，尝试创建 HMaster 临时节点（选主） 加载 hbase:meta 表，获取所有 Region 分布信息 启动后台线程：LoadBalancer（定期均衡 Region）、CatalogJanitor（清理 meta 中的无效条目） 高可用机制：通过 ZooKeeper 选举，同一时间只有一个活跃 HMaster。Standby HMaster 监听活跃节点，发现 znode 消失后立刻尝试成为主。\n面试常见问题：HMaster 宕机后 HBase 还能工作吗？\n可以。读写操作由 RegionServer 直接处理，HMaster 只负责 DDL 和负载均衡。已有表的读写不受影响，但无法创建新表或修改表结构。 1.2 RegionServer 源码路径：org.apache.hadoop.hbase.regionserver.HRegionServer\n核心职责与架构：\n┌─────────────────── RegionServer ───────────────────┐ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ Region1 │ │ Region2 │ │ Region3 │ ← 每个 Region 负责一段 RowKey 范围 │ └────┬───┘ └────┬───┘ └────┬───┘ │ │ │ │ │ │ │ ┌────▼───────────▼───────────▼─────┐ │ │ │ Store(ColumnFamily: info) │ │ │ │ ┌────────────┐ ┌─────────────┐ │ │ │ │ │ MemStore │ │ StoreFile │ │ │ │ │ │(SkipList) │ │ (HFile × N) │ │ │ │ │ └────────────┘ └─────────────┘ │ │ │ ├──────────────────────────────────┤ │ │ │ BlockCache (LruBlockCache) │ │ │ │ WAL (Write Ahead Log → HDFS) │ │ │ └──────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ 写流程详解 Client → RegionServer 1️⃣ 写入 WAL（HDFS，保证不丢数据） 2️⃣ 写入 MemStore（ConcurrentSkipListMap，有序） ─── 返回成功给客户端 ─── 3️⃣ 后台 Flush 线程：当 MemStore 达到阈值（128MB）→ 生成 HFile 4️⃣ 后台 Compaction：合并小 HFile，删除已删除/过期的数据 写入为什么先写 WAL 再写 MemStore？\nWAL 在 HDFS 上（多副本），RegionServer 宕机后可以通过 WAL 恢复 MemStore 中未刷盘的数据 WAL 使用顺序写（HDFS Append），性能远高于随机写 为什么 MemStore 用 ConcurrentSkipListMap？\n数据按 RowKey 排序存储——HFile 要求数据有序，这样可以顺序写入 ConcurrentSkipListMap 支持 O(log N) 的插入和查找，同时线程安全 读流程详解 Client → RegionServer 1️⃣ 查询 BlockCache（LruBlockCache，缓存热点 HFile Block） 2️⃣ 未命中 → 查询 MemStore（未刷盘的最新数据） 3️⃣ 未命中 → 查询 HFile（Bloom Filter 先过滤，跳过不含该 RowKey 的文件） 4️⃣ 合并所有结果（MemStore + 多个 HFile），按时间戳取最新版本 BlockCache vs MemStore：MemStore 存的是还没写入 HFile 的数据（写缓存），BlockCache 缓存已读过的 HFile Block（读缓存）。两者分离，避免写操作影响读缓存。\n1.3 Region 源码路径：org.apache.hadoop.hbase.regionserver.HRegion\nRegion 按 RowKey 范围水平切分表数据。每个 Region 包含多个 Store（每列族一个）。\nRegion 分裂流程：\n1. RegionServer 检测 Region 大小超过阈值 (hbase.hregion.max.filesize, 默认 10GB) 2. 将 Region 下线（短暂不可用） 3. 找到 Region 的中点 RowKey，拆分为两个子 Region 4. 在 hbase:meta 中更新 Region 信息 5. 两个子 Region 上线，客户端刷新 meta 缓存后路由到新 Region 分裂对读写的影响：分裂期间该 Region 短暂不可用（毫秒级），通过 WAL 和 Meta 更新保证一致性。\n1.4 WAL（Write Ahead Log） 源码路径：org.apache.hadoop.hbase.wal.WAL\n每次写入流程： 1. RegionServer 将写入操作封装为 WALEdit 2. 通过 FSHLog 写入 HDFS 上的 WAL 文件（顺序追加，高性能） 3. 写入成功后，再写入 MemStore 4. 当 MemStore 刷盘成功 → 该部分 WAL 可截断 崩溃恢复流程： 1. RegionServer 宕机 → HMaster 感知（通过 ZooKeeper Session 超时） 2. HMaster 将该 RS 上的 Region 重新分配给其他 RS 3. 新 RS 加载 WAL，回放数据到 MemStore 4. MemStore 刷盘后，数据恢复完成 WAL 性能优化：默认是同步写入（每条写入都 fsync），可通过 hbase.wal.hsync=false 改为异步（风险：RegionServer 进程级崩溃可能丢失少量数据）。\n2. HBase 读写流程深入 2.1 完整写入流程（含客户端侧） 1. 客户端从 hbase:meta 获取 RowKey 对应的 RegionServer 2. 客户端直连该 RegionServer（无中间代理） 3. RegionServer 接收请求 → 写入 WAL（多副本到 HDFS） 4. 写入 MemStore（ConcurrentSkipListMap） 5. 返回成功给客户端 6. 异步 Flush 线程在 MemStore 达到 128MB（`hbase.hregion.memstore.flush.size`）时： ├─ 在窗口中保持最新写入（不阻塞） ├─ 将当前 MemStore 快照转换为不可变数据 └─ 写入 HFile（按照 RowKey 顺序） 数据一致性保证：WAL（HDFS 3 副本）+ MemStore 的 Snapshot + Flush 机制确保即使 RS 宕机也不会丢数据。\n2.2 完整读取流程 1. 客户端从 hbase:meta 获取 RowKey 对应的 RegionServer 2. 客户端直连该 RegionServer 3. RegionServer 创建 scanner： ├─ 构建读取路径：MemStore + BlockCache + 所有相关 HFile └─ 合并结果（按 RowKey + TimeStamp + Type 排序） 4. 返回查询结果（逐行或批量） 读性能优化：\nBloom Filter：跳过不含目标 RowKey 的 HFile，减少 I/O。布隆过滤器会占用内存（每个 HFile 约 1% 额外空间），但可大幅提升随机读性能 BlockCache：缓存热点 HFile Block（Block 默认 64KB），顺序读取时命中率高 预分区（Pre-splitting）：建表时指定 Region 数量和 split 点，避免单 Region 热点 压缩：Snappy 压缩 HFile（CPU 开销小，压缩比约 2:1），减少磁盘 I/O 3. 高级特性 3.1 协处理器（Coprocessor） 类似数据库的触发器（Observer）和存储过程（Endpoint）。\nObserver：拦截 HBase 操作，在执行前后注入自定义逻辑。\n应用场景：\n二级索引：Put 操作时写入索引表（Observer 拦截 postPut） 权限校验：Get 操作前检查用户权限（Observer 拦截 preGet） 审计日志：记录所有 DML 操作 Endpoint：在 RegionServer 上执行聚合计算，避免客户端拉回大量数据。\n场景示例：统计某个表的总行数——不通过 Endpoint 的话需要 Scan 全表拉回客户端，通过 Endpoint 各 Region 并行统计后返回汇总结果。\n3.2 压缩与编码 压缩算法 压缩比 CPU 开销 场景 GZIP 最高 最高 归档数据，读少 Snappy 中（~2:1） 低 平衡型（热数据推荐） LZO 中 中 兼容性差（需安装 native lib） ZSTD 高 中 JDK 11+，Facebook 开发 设置方式：\n# 建表时指定 create \u0026#39;mytable\u0026#39;, { NAME =\u0026gt; \u0026#39;cf\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;SNAPPY\u0026#39; } # 已有表修改 alter \u0026#39;mytable\u0026#39;, { NAME =\u0026gt; \u0026#39;cf\u0026#39;, COMPRESSION =\u0026gt; \u0026#39;GZ\u0026#39; } 4. 实战排查 RegionServer 内存溢出（OOM）\n症状：RS 进程消失，HMaster 重新分配 Region 检查 MemStore 大小：hbase.hregion.memstore.flush.size × Region 数量 = RS 写缓冲区总量 通常原因为 Region 过多导致 MemStore 总和超过 RS 堆内存——降低 hbase.regionserver.global.memstore.size（默认 0.4，即 40% 堆内存） 读延迟高\nBlockCache 命中率低：增大 hfile.block.cache.size（默认 0.4） HFile 数量过多：触发 Major Compaction 合并（但 Major Compaction 期间 I/O 压力大） Bloom Filter 未开启：建表时 BLOOMFILTER =\u0026gt; 'ROW' Region 热点\n原因：RowKey 设计不合理，顺序递增导致所有写请求打在一个 Region 上 解决方案：RowKey 加盐（前缀 hash）——如 MD5(id).substring(0,4) + id 使数据均匀分布 Major Compaction 影响读写\nMajor Compaction 合并所有 HFile，I/O 消耗大 建议在低峰期触发：hbase.hregion.majorcompaction 设置为 0（关闭自动），通过 crontab 手动触发 ZooKeeper Session 超时导致 RegionServer 被误判宕机\n检查 GC 日志——长时间的 Full GC 可能让 ZK 认为 RS 失联 调大 zookeeper.session.timeout（默认 90 秒），或缩短 GC 暂停时间 ","date":"2018-08-15T15:43:54+08:00","permalink":"/blog/hbase/","title":"HBase 深入浅出"},{"content":"一、JVM 垃圾回收器概述 常见的垃圾回收器：\nSerial GC Parallel GC（Throughput GC） CMS GC（Concurrent Mark Sweep） G1 GC（Garbage First） ZGC（Z Garbage Collector） Shenandoah GC 二、垃圾回收器的特点与回收流程 衡量 GC 优劣的三个核心指标 吞吐量（Throughput）：用户代码运行时间 /（用户代码运行时间 + GC 时间）。越高越好。 暂停时间（Pause Time / Latency）：GC 时应用线程暂停的时间。越短越好。 内存占用（Footprint）：GC 额外消耗的内存（如 G1 的 RSet、ZGC 的染色指针）。越少越好。 这三者不可能同时优化——任何 GC 都在三者间做取舍。\n1. Serial GC 特点：单线程，回收时全部 STW 为什么存在：单核 CPU 或小型应用场景，多线程反而有额外开销 回收流程： 新生代：复制算法（Copying） 老年代：标记-整理算法（Mark-Compact） 适用场景：客户端应用、小型服务端、单核环境 参数：-XX:+UseSerialGC 何时选它：堆小于 100MB、单核 CPU。其他情况不推荐。 2. Parallel GC（Throughput GC） 特点：多线程，目标是最大化吞吐量，但 STW 时间较长 设计目标：\u0026ldquo;只要暂停时间在可接受范围内，吞吐量越高越好\u0026rdquo; 回收流程： 新生代：复制算法（多线程） 老年代：标记-整理算法（多线程） 适用场景：多核 CPU、对延迟要求不高的批处理/数据分析任务 重要参数： -XX:+UseParallelGC：启用 -XX:ParallelGCThreads：并行 GC 线程数（默认 = CPU 核心数） -XX:MaxGCPauseMillis：期望最大暂停时间（不是硬保证，默认无） -XX:GCTimeRatio：吞吐量目标（默认 99，即允许 1% 时间花在 GC 上） 3. CMS GC（Concurrent Mark Sweep） 特点：并发收集老年代，尽量减少 STW 时间 为什么选 CMS：对于 Web 服务等延迟敏感应用，每次 Full GC 暂停几秒钟不可接受。CMS 让大部分 GC 工作与应用线程并发。 回收流程： 初始标记（STW）：标记 GC Roots 直接关联的对象——快速 并发标记：从 GC Roots 开始遍历所有引用——与应用并发 重新标记（STW）：修正并发标记期间引用变化导致的漏标——较快 并发清除：清除不可达对象——与应用并发 核心问题： 内存碎片：使用标记-清除算法，老年代越来越碎片化 → 最终触发 Full GC 且无法避免长时间 STW CPU 敏感：并发阶段抢占 CPU，应用吞吐量下降 浮动垃圾：并发标记期间新产生的垃圾本次无法回收 Promotion Failed：新生代对象晋升老年代时无连续空间 → Full GC 重要参数： -XX:+UseConcMarkSweepGC：启用（JDK 9 后已废弃，JDK 14 移除） -XX:CMSInitiatingOccupancyFraction：触发 CMS 的老年代使用率阈值（默认 68%） -XX:+UseCMSCompactAtFullCollection：Full GC 时整理内存 -XX:CMSFullGCsBeforeCompaction：多少次 Full GC 后进行一次整理 为什么被废弃：G1 在延迟和吞吐量之间的平衡表现更好，且 CMS 无法解决碎片化和浮动垃圾问题。 4. G1 GC（Garbage First） 特点：面向大堆、多核的服务端 GC。将堆划分为多个 Region，优先回收垃圾最多的 Region（\u0026ldquo;Garbage First\u0026rdquo; 名字的由来） 设计思路：不要求一次回收整个堆，而是分批次回收每个 Region，通过可预测的 pause time 模型控制停顿 回收流程： 初始标记(STW) → 并发标记 → 最终标记(STW) → 筛选回收(STW) 初始标记：标记 GC Roots + 修改 TAMS（Next Top at Mark Start）指针\n并发标记：从 GC Roots 遍历所有对象，记录引用变更到 SATB（Snapshot At The Beginning）队列\n最终标记：处理 SATB 队列中的漏标对象\n筛选回收：计算每个 Region 的回收价值（回收能释放多少空间 × 回收耗时），优先回收\u0026quot;垃圾最多、回收最快\u0026quot;的 Region\n关键内部机制：\n机制 作用 Region 堆划分为 1MB~32MB 的 Region，每个 Region 可扮演 Eden/Survivor/Old/Humongous RSet（Remembered Set） 记录其他 Region 对本 Region 的引用——避免全堆扫描 SATB（Snapshot At The Beginning） 并发标记开始时拍堆快照，所有在标记期间变更的引用都记录到 SATB 队列 写屏障 对象引用变更时触发，更新 RSet + 记录 SATB 适用场景：堆大小 4GB+，多核 CPU，延迟要求 100-300ms\n重要参数：\n-XX:+UseG1GC：启用（JDK 9+ 默认） -XX:MaxGCPauseMillis：期望暂停时间目标（默认 200ms，G1 会努力但无法硬保证） -XX:G1HeapRegionSize：Region 大小（默认堆/2048，1~32MB） -XX:InitiatingHeapOccupancyPercent：触发并发标记的堆使用率（默认 45%） -XX:G1NewSizePercent：新生代最小占比（默认 5%） -XX:G1MaxNewSizePercent：新生代最大占比（默认 60%） 何时选 G1：JDK 11+、堆 4GB+、延迟要求几百毫秒，G1 是默认选择。\n5. ZGC（Z Garbage Collector） 特点：极低延迟（STW \u0026lt; 10ms），与堆大小基本无关（即使是 TB 级堆） 为什么能这么低延迟：几乎所有阶段都并发，只在极少数点 STW（且 STW 阶段不随堆大小增长） 关键技术： 技术 作用 染色指针（Colored Pointers） 在 64 位指针的高位（42 位之后）存储 GC 状态信息（Finalizable/Remapped/Mark1/Mark0），无需额外内存 读屏障（Load Barrier） 每次从堆读取引用时检查指针颜色，如果对象被移动，读屏障在返回前修正引用 并发迁移（Relocation） 存活对象在应用运行时被迁移到新区域，旧区域清空后回收 回收流程：\n并发标记（Concurrent Mark）：遍历对象图，标记存活对象。与 G1 不同，ZGC 的标记完全并发（初始标记的 STW 极短）。 并发迁移预备（Concurrent Prepare for Relocation）：确定需要清理的 Region。 并发迁移（Concurrent Relocate）：将存活对象复制到新 Region，更新引用。读屏障确保应用线程始终访问到正确地址。 并发重映射（Concurrent Remap）：修正所有指向旧位置的引用。 适用场景：TB 级堆、延迟要求 \u0026lt; 10ms 的金融交易/实时系统\n重要参数：\n-XX:+UseZGC：启用（JDK 11 实验性，JDK 15 正式，JDK 18+ 默认） -XX:ZAllocationSpikeTolerance：分配尖峰容忍度（默认 2.0） -XX:ZCollectionInterval：最大回收间隔（秒） 注意事项：ZGC 的吞吐量略低于 G1（因为读屏障开销），内存占用也更高。选 ZGC 时确认你能接受这些 Trade-off。\n6. Shenandoah GC 特点：与 ZGC 类似，目标是极低延迟。与 ZGC 的区别在于实现方式： ZGC 使用染色指针（需要 64 位系统 + 特定 CPU 特性） Shenandoah 使用 Brooks 指针（对象头中加一个转发指针），不依赖硬件 回收流程：并发标记 → 并发转移 → 并发更新引用（比 ZGC 多一个并发更新引用阶段） 适用场景：与 ZGC 类似，但不要求特定硬件 重要参数： -XX:+UseShenandoahGC：启用（JDK 12+，OpenJDK 发行版不默认包含——需要 Build 时开启） -XX:ShenandoahGCHeuristics：启发式策略（adaptive/static/compact/aggressive） 三、GC 选择决策树 应用需要什么？ ├─ 批处理/数据分析 → Parallel（要吞吐量） ├─ Web 服务/微服务 │ ├─ 堆 \u0026lt; 4GB → G1 或 Parallel │ ├─ 堆 4~16GB → G1（JDK 11+ 默认） │ ├─ 堆 \u0026gt; 16GB + 延迟敏感 → G1（延迟要求 \u0026lt; 100ms？） │ │ └─ 是 → ZGC（JDK 17+） │ └─ 堆 \u0026gt; 100GB + 极致低延迟 → ZGC └─ 客户端/小型应用 → Serial 四、GC 优化实践 基础三步 确定目标：是提高吞吐量还是降低延迟？ 收集数据：PrintGCDetails + GCViewer/GCEasy 分析 GC 日志 调整参数：一次只改一个参数，观察效果 常用 JVM 参数组合 # 打印 GC 日志（JDK 9+） -Xlog:gc* -Xlog:gc:gc.log # 打印 GC 日志（JDK 8） -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log # G1 生产环境常用参数 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails # 大堆 ZGC 常用参数 -XX:+UseZGC -Xms64G -Xmx64G -XX:ZAllocationSpikeTolerance=2.0 内存泄漏排查思路 Heap Dump + MAT（Memory Analyzer Tool）分析大对象 GC 日志中 Full GC 频率异常 → 确认是否为晋升失败或 MetaSpace 泄漏 Native Memory Tracking（NMT）检查 off-heap 内存 五、跨代引用与 RSet 详解 G1 的 RSet（Remembered Set） 每个 Region 维护一个 RSet，记录其他 Region 对它的引用。为什么需要 RSet？Young GC 时只扫描新生代 Region，但老年代可能有对象引用新生代对象——有了 RSet 就不必全堆扫描。\nRSet 的结构：哈希表，key 是来源 Region 地址，value 是来源 Region 中的卡页索引（Card Index）。卡页默认 512 字节。\n写屏障维护 RSet：\nvoid writeBarrier(Field field, Object newObj) { Object oldObj = field.get(); if (oldObj != null \u0026amp;\u0026amp; isCrossRegionRef(oldObj, newObj)) { addToRSet(oldObj.region(), newObj.region(), getCardIndex(oldObj)); } field.set(newObj); } G1 vs CMS 跨代引用处理对比 机制 G1 CMS 跨代引用处理 每个 Region 维护 RSet 全局卡表（Card Table） 精度 精确到 Region + 卡页 粗粒度（卡表页） 内存开销 较高（每个 Region 一个 RSet） 较低（全局卡表） 扫描效率 仅扫描相关 Region 需扫描整个老年代脏卡页 ","date":"2018-07-18T15:43:54+08:00","permalink":"/blog/garbagecollector/","title":"垃圾回收器"},{"content":"什么是Feign Feign 是 Netflix 开源的声明式 HTTP 客户端，用于简化 RESTful 服务的调用 通过定义接口和注解，Feign 可以自动生成HTTP请求的实现 特点 声明式 API ：通过接口注解定义 HTTP 请求，无需手动编写 HTTP 客户端代码 集成 Ribbon ：默认集成 Ribbon ，支持客户端负载均衡 集成 Hystrix ：支持熔断和降级机制 可扩展性：支持自定义编码器、解码器和拦截器 工作原理 （1）接口定义 使用 @FeignClient 注解定义 Feign 客户端接口 通过方法注解（如 @GetMapping、@PostMapping）定义 HTTP 请求 （2）动态代理 Feign 通过动态代理技术生成接口的实现类 在运行时，Feign 会将方法调用转换为 Http 请求 （3）请求处理 Feign 根据接口定义生成 HTTP 请求，并通过 Ribbon 进行负载均衡 请求结果通过解码器为 Java 对象 （4）集成 Ribbon 和 Hystrix Feign 默认集成了 Ribbon，支持客户端负载均衡 通过配置可以启用 Hystrix，实现熔断和降级 Feign 的配置 （1）基本配置 使用 @FeignClient 注解定义 Feign 客户端： @FeignClient(name = \u0026#34;service-name\u0026#34;, url = \u0026#34;http://localhost:8080\u0026#34;) public interface MyFeignClient { @GetMapping(\u0026#34;/endpoint\u0026#34;) String getResponse(); } （2）负载均衡 * Feign 默认集成了 Ribbon，无需额外配置 * 可以通过配置文件自定义 Ribbon 的负载均衡策略： service-name: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule （3）熔断和降级 通过 @FeignClient 的** Fallback** 属性指定降级类： @FeignClient(name = \u0026#34;service-name\u0026#34;, fallback = MyFeignClientFallback.class) public interface MyFeignClient { @GetMapping(\u0026#34;/endpoint\u0026#34;) String getResponse(); } @Component public class MyFeignClientFallback implements MyFeignClient { @Override public String getResponse() { return \u0026#34;Fallback response\u0026#34;; } } （4）自定义配置 通过 **@Configuration **类自定义 Feign 的编码器、解码器和拦截器： @Configuration public class FeignConfig { @Bean public Encoder feignEncoder() { return new JacksonEncoder(); } @Bean public Decoder feignDecoder() { return new JacksonDecoder(); } @Bean public RequestInterceptor feignRequestInterceptor() { return template -\u0026gt; template.header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer token\u0026#34;); } } Feign 的使用场景 （1）微服务调用 在微服务架构中，Feign 常用于服务之间的调用 自定义接口和注解，简化 HTTP 请求的编写 （2）负载均衡 Feign 默认集成了 Ribbon，支持客户端负载均衡 可以根据配置选择不同的负载均衡策略 （3）熔断和降级 通过集成 Hystrix，Feign 支持熔断和降级机制 当服务不可用时，可以返回降级结果，避免系统崩溃 （4）自定义 HTTP 客户端 Feign 支持自定义编码器、解码器和拦截器，满足复杂的业务需求 Feign 的底层原理 （1）动态代理 Feign 通过 JDK 动态代理或者 CGLIB 生成接口的实现类 在运行时，Feign 会将方法调用转换为 HTTP 请求 （2）请求模版 Feign 使用 RequestTemplate 类表示 HTTP 请求模版 通过解析接口注解和方法参数，生成具体的 HTTP 请求 （3）编码器和解码器 Feign 使用编码器将 Java 对象转换为 HTTP 请求体 使用编码器将 HTTP 响应体转为 Java 对象 （4）拦截器 Feign 支持请求拦截器，可以在发送请求前修改请求头或者请求体 graph TD A[客户端调用 Feign 接口方法] --\u003e B[ReflectiveFeign.newInstance 创建动态代理] B --\u003e C[InvocationHandler.invoke 拦截方法调用] C --\u003e D[SynchronousMethodHandler.invoke 处理请求] D --\u003e E[RequestTemplate.Factory.create 生成请求模板] E --\u003e F[RequestInterceptor.apply 应用拦截器] F --\u003e G[Encoder.encode 编码请求体] G --\u003e H[LoadBalancerFeignClient.execute 选择服务实例] H --\u003e I[Client.execute 发送 HTTP 请求] I --\u003e J[Decoder.decode 解析响应] J --\u003e K[返回结果给客户端]","date":"2018-06-15T15:43:54+08:00","permalink":"/blog/feign/","title":"Feign"},{"content":"Eureka 配置中心核心功能：\n集中化管理 版本管理 动态更新 权限控制 环境隔离 基本功能：\n服务发现 eureka客户端可以向eureka服务端获取服务实例列表\n服务注册 eureka客户端启动的时候\n三级缓存\n读缓存 ReadOnlyCache 写缓存 WriteCache 注册表缓存 自我保护机制 在网络通讯产生问题时，避免大量正常客服端实例被清除\neureka 自我保护机制服务端相关影响配置 eureka: server: enable-self-preservation: true # 默认true，开启自我保护 renewal-percent-threshold: 0.85 # 续约百分比阈值，默认0.85 renewal-threshold-update-interval-ms: 900000 # 更新阈值的时间间隔，默认15分钟 eureka 自我保护机制客户端相关影响配置 eureka: instance: lease-renewal-interval-in-seconds: 30 # 客户端续约间隔，默认30秒 lease-expiration-duration-in-seconds: 90 # 服务过期时间，默认90秒 客户端服务注册流程 每当有客服端服务启动时，客户端会根据配置eureka服务端的地址，向服务端发起注册动作，\b服务端收到注册请求，会更新注册表缓冲信息，增加新的客户端信息到注册表缓冲，然后将注册表信息更新到写缓存。\n客户端读取数据流程 客户端查看本地缓存是否存在 存在：直接返回本地缓存中的数据 不存在：请求服务端获取数据 获取读缓存，存在直接返回；不存在直接进入下一步 获取写缓存，存在直接返回，并且将数据更新到读缓存；不存在直接进入下一步 获取注册表缓存，存在直接返回，并且将数据更新到读缓存与写缓存 缓存更新场景 注册表更新场景：\n服务实例注册 服务实例注销 服务实例续约（心跳） 服务实例状态变更 写缓存更新场景：\n每当注册表发生变化时，写缓存会被同时更新到最新的注册表信息 读缓存更新场景：\n每隔30s会同步写缓存数据 Eureka 中服务实例的状态主要包括以下几种： UP：服务实例正常运行，可接收请求。 当eureka服务实例发起服务注册时 当eureka服务实例发起心跳，进行续期成功后，保持UP状态 当eureka服务实例启动成功后，将状态从STARTING更改为UP DOWN：服务实例不可用。 当eureka服务实例主动注销 当eureka实例在规定的时间（默认90s）内没有发送心跳消息 STARTING：服务实例正在启动。 当服务器启动时 OUT_OF_SERVICE：服务实例被手动下线，不可用。 通过Eureka的管理接口或者UI，可以手动将状态从UP更改为OUT_OF_SERVICE UNKNOWN：服务实例状态未知。 当网络分区时（网络问题导致服务之间无法通讯），Eureka服务端无法确认服务实例的状态时，就会将状态改为 UNKNOWN Eureka各端缓存应用交互 sequenceDiagram participant Client as 客户端 participant LocalCache as 本地缓存 participant EurekaServer as Eureka 服务器 participant ServiceInstance as 服务实例 Client-\u003e\u003eLocalCache: 1. 检查本地缓存 alt 缓存有效 LocalCache--\u003e\u003eClient: 返回缓存的服务实例 else 缓存无效或过期 Client-\u003e\u003eEurekaServer: 2. 请求服务实例信息 EurekaServer-\u003e\u003eEurekaServer: 3. 检查读缓存 alt 读缓存有效 EurekaServer--\u003e\u003eClient: 返回读缓存的服务实例 else 读缓存过期 EurekaServer-\u003e\u003eEurekaServer: 4. 从写缓存加载最新数据 EurekaServer--\u003e\u003eClient: 返回最新的服务实例 end Client-\u003e\u003eLocalCache: 5. 更新本地缓存 end Client-\u003e\u003eServiceInstance: 6. 调用目标服务实例 Client-\u003e\u003eEurekaServer: 7. 定时拉取最新服务实例 EurekaServer--\u003e\u003eClient: 返回最新的服务实例 Client-\u003e\u003eLocalCache: 8. 更新本地缓存","date":"2018-05-01T13:16:53+08:00","permalink":"/blog/eureka/","title":"Eureka"},{"content":"1. Elasticsearch 概念与架构 集群(Cluster)：Elasticsearch 中的多个节点(Node)组成集群。一个集群中有一个主节点(Master Node)，负责集群管理，其它节点可能是数据节点(Data Node)、协调节点(Coordinating Node)、以及 Ingest 节点等。 节点(Node)：每个 Elasticsearch 实例都被称为节点，节点存储数据并处理请求。节点可以有不同角色，如master 、data 、ingest 等。 索引(Index)：Elasticsearch 中数据的存储单位，每个索引包含多个分片(Shards)，可以进行高效的存储与查询。 分片(Shard)：每个索引会被分割成多个分片，分片是数据存储的基本单位，主分片和副本分片提供了数据的分布与冗余 2. 核心原理 1.1 倒排索引(Inverted Index) 原理：通过词项(Term)映射到文档表，实现快速全文检索 构建过程：文档分词→生成词项→记录词项所在文档及位置 优势：高效查询，支持复杂搜索(如布尔逻辑、短语匹配) 与正排索引对比：正排索引通过文档 ID 查找内容，倒排索引通过词项找文档 1.2 分布式架构 分片(Shard)： 主分片(Primary)*：*处理写操作，数量创建索引时固定 副本分片(Replica)：提供读高可用，数量可调整 节点角色： Master 节点：管理集群状态(如索引创建、节点上下线) Data 节点：存储分片，处理数据 CURD Coordinating 节点：路由请求，聚合结果(默认所有节点均可担任) Ingest 节点： 数据预处理 功能：通过定义 Pipeline(处理管道)，对原始数据进行加工，例如： 解析结构：如将日志中的非结构化文本解析为结构化字段(使用Grok 处理器)。 字段转换：如转换时间格式、大小写转、类型转换(字符串转数字)。 数据增强：如添加地理位置信息、基于 IP 生成 GeoIP 字段。 字段过滤：删除无用字段、重命名字段。 条件处理：根据字段内容动态决定是否执行某些操作。 减轻数据节点负载 职责分离：将计算密集型的预处理任务从 Data 节点剥离，避免影响索引和查询功能。 资源优化：专用 Ingest 节点可配置更高 CPU 资源，专注于数据处理。 简化架构 替代部分 Logstash 功能：在不需要复杂 ETL 的场景中，可直接通过 Elasticsearch 完成数据处理，减少外部组件依赖。 1.3 近实时(NRT，Near Real-Time) Refresh 机制：内存缓冲区数据每隔一秒(默认)生成新 Segment(文件系统缓存)，实现可搜索 Translog 持久化：写入操作记录到事务日志，定期 Flush 到磁盘保证数据安全 1.4 查询流程 客户端发起请求 请求类型：根据需求，客户端可能发起 Get 请求(通过 ID 获取文档)或 Search 请求(复杂查询) 目标节点：客户端将请求发送到集群中的任意节点，该节点默认担任协调节点(Coordinating Node) 请求路由与分片定位 Get by ID 协调节点通过路由算法(如hash(_id) % number_of_shards )确定文档所在分片 直接向该分片的主分片或副本分片发送请求(副本分片负载均衡) Search 请求 若查询未指定路由，协调节点将请求广播到索引的所有分片(主分片或副本分片) 若指定路由参数，仅查询关联的分片 分片本地查询(Query Phase) 分片处理： 每个分片在本地执行查询，使用倒排索引快速匹配词项 对于搜索请求，分片返回匹配文档的 ID 和排序值(如相关性评分) 分片可能使用缓存(如分片请求缓存)加速查询 结果返回： 各分片返回本地排序后的 Top N 结果(N 有size 参数决定) 全局结果合并(Coordinating Node) 排序与筛选： 协调节点收集所有分片的中间结果，按全局排序规则(如相关性评分)合并 筛选最终** Top N 文档的 ID 列表 **(例如用户请求的 size=10 ) 分页处理： 深度分页优化：若使用from + size ，协调节点需要合并所有分片的from + size 条数据，内存开销大。推荐使用 Search After 或 Scroll API 替代 获取文档详情(Fetch Phase) 二次请求： 协调节点根据筛选后的文档 ID，向对应分片发送 Multi-Get 请求获取完整文档内容 分片返回文档的原始数据(如_source 字段) 结果聚合： 协调节点将最终结果组装，返回给客户端 实时性处理 新写入文档可见性： 搜索请求：依赖 Refresh 机制(默认1秒生成新 Segment)，未刷新前不可被搜索 Get 请求：通过实施读取 Translog 可立即获取最新文档，无需等待 Refresh 容错与负载均衡 副本分片利用： 读请求优先分发到副本分片，分担主分片压力，提升吞吐量 故障恢复： 若某分片不可用，协调节点自动重试其它副本分片 缓存机制优化 分片请求缓存： 缓存聚合结果或频繁查询的响应(仅对不含now 或动态参数的查询生效) 字段数据缓存： 用于排序和聚合的字段数据缓存在内存，加速计算 1.5 写入流程 客户端发起写入请求 请求类型：可以是单文档写入(PUT /index/_doc/1 )、批量写入(Bulk API )或更新/删除操作 协调节点选择：客户端将请求发送到任意节点(默认作为协调节点) 路由计算与主分片定位 路由规则： 若指定文档 ID，使用哈希算法：hash(_id) % number_of_shards 确定目标分片 若未指定 ID(自动生成 ID)，Elasticsearch 会随机选择主分片 分片定位：协调节点根据路由结果，将请求转发到该主分片所在的** Data 节点** 主分片写入处理 3.1 写入内存缓冲去\n内存缓冲区(Index Buffer)： 文档首先被写入主分片的内存缓冲区 此时数据不可被搜索（尚未生成 Segment） 分词与索引构建： 对文档字段进行分词（根据 mapping 定义的分词器） 生成倒排序索引的临时数据结构 3.2 写入 Translog (事务日志)\nTranslog 作用：确保数据未持久化到磁盘前不会丢失 所有写入操作会同步追加到 Translog Translog 默认每次请求后刷新(index.translog.durability: request )，保证可靠性 可配置为异步刷新(async)以提高性能，但可能丢失部分数据 3.3 副本分片同步(Replication)\n同步模式： 主分片将写入操作并行发送到所有副本分片 副本分片执行相同的写入流程(写入内存缓冲区 + Translog) 写入确认条件： 默认要求多数分片成功(wait_for_active_shards: quorum) 若副本分片写入失败，主分片会重试，最终可能标记副本分片为失效 返回客户端响应 成功条件： 主分片和副本分片均为完成内存和 Translog 写入后，协调节点向客户端返回acknowledged: true 失败处理： 若副本分片不可用，主分片仍会写入，但集群状态变为yellow (副本未分配) 客户端可能收到ShardFailureException (需重试或处理异常) 近实时搜索实现(Refresh) Refresh机制： 默认每个1秒(index.refresh_interval )，内存缓冲区的数据会生成新的 Lucene Segment Segment 写入文件系统缓存(非磁盘)，此时数据可被搜索 Refresh 操作开销较大，高频写入场景可适当调大间隔(如30秒) 数据持久化(Flush) Flush 触发条件： Translog 大小达到阈值(默认512MB，index.translog.flush_threshold_size ) 时间间隔(默认30分钟，index.translog.sync_interval ) 手动调用_flush API 持久化过程： 清空内存缓冲区，将所有 Segment 持久化到磁盘 清空当前 Translog，生成新的 Translog 文件 Segment 只读：写入磁盘后不再修改，删除操作通过.del 文件标记 Segment 合并(Merge) 后台优化： 多个 Segment 会被合并为更大的 Segment，提升查询性能 合并过程自动触发，删除过期文档(如标记为删除的文档) 合并期间可能占用较多I/O和 CPU 资源 容错与恢复机制 节点宕机恢复： 重启后，通过重放 Translog 恢复未持久化的数据 若主分片宕机，集群选举新的主分片(优先选择最新数据的副本) 数据一致性： 使用_seq_no 和_primary_term 保证写入顺序和冲突解决 写入时可通过version 参数实现乐观锁控制 3. 倒排索引的底层实现 3.1 核心数据结构\n词典(Term Dictionary)： 使用** FST(有限状态转换机)** 压缩存储词项，减少内存占用(Lucene 6.0 后默认)。 支持快速前缀查询(如通配符*)，但需权衡内存与查询性能。 倒排列表(Postings List)： DocID 列表：使用差值编码(Delta Encoding)和 **Frame Of Reference(FOR)**压缩。 原始ID：100, 200, 300 → 差值：100, 100, 100 → 压缩为单值100 + 重复次数。 词频(TF)与位置(Position)：使用**位压缩(Bit Packing)和游程编码(RLE)**。 3.2 倒排索引的局限性\n更新代价高：文档修改需要重建索引，适合读多写少场景。 内存压力：字段数据缓存(如排序字段)可能引发堆内存溢出(需结合doc_values 优化)。 4. 分布式设计的深度剖析 4.1 分片策略的权衡\n分片数选择： 过少：单分片数据量大，扩容困难，写入易成瓶颈。 过多：元数据管理开销大(每个分片是独立 Lucence 索引)，查询合并成本高。 黄金法则：单个分片大小建议在** 10-50GB **，结合数据增长速率设计。 路由优化： 自定义路由：指定routing 参数将相关文档存入同一分片，提升查询效率。 PUT /logs/_doc/1?routing=node-1 { \u0026#34;message\u0026#34;: \u0026#34;error in node-1\u0026#34; } 弊端：可能导致数据倾斜(如某个路由值数据量过大) 4.2 分布式一致性模型\n写入一致性： 参数：wait_for_active_shards (默认为quorum ，即(主分片数+副本数)/ 2 + 1) 。 场景：若副本分片故障，主分片写入成功后仍返回成功，但集群状态为yellow 。 读一致性： 默认最终一致性：主分片写入成功后，副本分片可能短暂落后。 强制读主分片：设置preference=_primary ，但牺牲性能。 4.3 脑裂问题与防范\n原因：网络分区导致多个 Master 节点被选举。 解决方案： discovery.zen.minimun_master_nodes (ES 7.x前)：设置为(master 节点数 / 2)+ 1 。 5. 查询流程深度优化 5.1 Query 与 Filter 的执行差异\nQuery 上下文 计算相关性评分(score)，无法缓存，适合全文搜索 代价：频繁访问到排序索引，CPU 消耗高。 Filter 上下文 仅判断是否匹配，结果可缓存(使用bitset)，适合精准过滤。 优化技巧：将范围查询、term 查询至于filter 中。 5.2 聚合(Aggregation)的底层实现\n全局排序限制: terms 聚合默认返回每个分片的 Top 结果再合并，可能导致精度损失。 解决方案：设置size: 10000 + shard_size: 50000 (增加分片级计算量)。 内存控制： 聚合结果构建在堆内存中，需监控fielddata 使用(避免CircuitBreaker 触发)。 5.3 查询性能优化\n索引设计： 使用keyword 类型替代text ，避免分词开销。 禁用_all 字段(ES 6.0+默认禁用，替换为copy_to )。 查询重写 布尔查询顺序：高选择性条件(如term )放在前面，减少后续计算量。 分页替代方案： Search After：利用上一页的排序值(如@timestamp )避免深分页。 Scroll API：创建快照上下文，适合离线导出(但消耗资源) 6. 高级调优参数 参数 作用 推荐值 indices.memory.index_buffer_size 控制索引缓冲区大小(堆内存百分比) 10%(默认) index.refresh_interval 刷新间隔，影响搜索实时性 30s(写入优化场景) index.translog.flush_threshold_size Translog刷盘阈值 1gb(高吞吐场景) index.merge.scheduler.max_thread_count 合并线程数(需 SSD 支持) 4(默认) search.max_buckets 聚合返回的最大桶数 100000(大数据聚合场景) 6. 架构运行相关流程图 drawio\n快速问答 Translog 如何保证数据安全？ Translog 使用追加写入(Append-Only)模式，避免随机写磁盘的性能问题 通过 fsync 系统调度强制刷盘(依赖index.translog.durability 配置) 崩溃恢复：节点重启时，通过 Translog 重放未持久化到 Lucene Segment 的操作 源码关联：org.elasticsearch.index.translog.Translog 类处理日志的写入与恢复 与 Lucene Commit 的关系：Flush 操作会将内存数据生成 Segment 并刷盘，同时清除已提交的 Translog 使用 Bulk API、调大 Refresh 间隔 实战案例：在日志系统中，将refresh_interval 设为30s ，写入吞吐量提升 3 倍，但需接受搜索延迟 参数调优：调整indices.memory.index_buffer_size (默认 10% 堆内存)增加索引缓冲区 硬件影响：使用 SSD 减少 Flush 耗时，避免写入瓶颈 监控工具：通过 Kibana Monitoring 观察indexing_rate和merge_time ，针对性优化 Elasticsearch 和关系型数据库的写入区别 数据模型：ES 时 Schema-less 的文档模型，支持动态字段；关系数据库需预定义 Schema 事务性：ES 不支持 ACID 事务，依赖版本号(_version )实现乐观锁 写入延迟：ES 的 NRT(近实时)vs 数据库的实时可见 Refresh 机制对写入性能的影响 Refresh 操作将内存缓冲区中的数据生成新的 Lucene Segment，写入文件系统缓存(非磁盘)，使数据可被搜索) 频繁 Refresh 会导致产生大量小 Segment，增加 Merge 开销(I/O 和 CPU 竞争) 日志数据：设置refresh_interval: 30s 甚至-1 (关闭自动 Refresh)，通过手动 Refresh 控制搜索可见性 高查询压力：避免长时间不 Refresh，否则新数据无法被搜索 监控指标：通过_nodes/states/indices/ API 观察refresh_tatol 和refresh_time_in_millis ，评估 Refresh 开销 主分片与副本分片的写入同步 主分片写入成功后，会并行向所有副本分片发送写入请求 副本分片写入成功后，主分片才向客户端返回确认 wait_for_active_shards: quorm (默认)：多数分片(主 + 副本)可用时写入成功 wati_for_active_shards: 1 ：仅主分片成功即可(风险：副本可能落后) 若副本分片写入失败，主分片会向 Master 节点报告，触发分片重分配 集群状态变为Yellow ，直到副本恢复 设计一个支持亿级日志写入的系统，你会如何规划 ES 集群？ 分片设计：按时间滚动索引(如logs-2023-10 )，单个分片大小控制在 50GB 以内 写入优化：使用 Bulk API，客户端侧实现批量压缩与重试机制 硬件规划：独立 Ingest 节点处理数据预处理，Data 节点使用 NVMe SSD 可靠性：设置index.translog.durability: async ，副本数设为 1 监控：通过 ILM(索引生命周期管理)自动归档旧索引到冷存储 为什么 Elasticsearch 查询比数据库快？ 倒排序索引：快速定位文档，避免全表扫描。 分布式并行：查询拆分到多个分片并行执行。 文件系统缓存：Segment 文件缓存在 OS Cache，减少磁盘 IO。 列式存储(doc_values )：优化排序与聚合。 如何处理数据一致性问题？ 写入一致性：通过wait_for_active_shards 控制最小成功分片数。 版本控制： 使用_version 实现乐观锁，避免并发覆盖。 外部版本号(version_type=external )兼容数据库同步。 读一致性： preference=_primary ：强制读主分片(强一致性，性能低)。 replication=async ：异步副本更新(最终一致性，性能高)。 如何设计一个高可用 ES 集群？ 节点角色分离： 专用 Master 节点（3台，避免脑裂）。 独立 Data 节点、Ingest 节点、Coordination 节点。 分片策略： 副本数≥1，跨机架分布（awareness 参数）。 使用ILM（索引生命周期管理）自动滚动索引。 容灾备份： 快照（Snapshot）到 S3/NFS，支持跨集群恢复。 多集群同步（CCR，跨集群复制）。 ","date":"2018-04-22T13:16:53+08:00","permalink":"/blog/elasticsearch/","title":"ElasticSearch 知识"},{"content":"ZooKeeper 核心原理 数据模型： ZNode：树形结构中的节点，存储数据（\u0026lt;=1MB） 节点类型 持久节点：手动删除 临时节点：会话结束自动删除 顺序节点：名称追加单调递增序号 版本号：每个 ZNode 有版本号， 用于 CAS操作 架构与角色： Leader：处理写请求，发起提案 Follower：参与选举，处理读请求 Observer：拓展读性能，不参与选举 ZAB 协议（ZooKeeper Atomic Broadcast） 恢复模式：选举 Leader （基于 zxid 和 myid，半数以上投票） 广播模式：Leader 提出事物，半数以上 Follower 确认后提交 一致性保护： 顺序一致性（客户端操作按顺序执行） 原子性（事物要么全成功要么全失败） 最终一致性（读请求可能返回旧值，但最终同步） Watch 机制： 客户端监听 ZNode 变化（如节点删除，数据更新） 一次性触发，需要重新注册 异步通知，可能存在延迟或丢失 典型应用场景 分布式锁：通过临时顺序节点实现，避免惊群效应 配置管理：集中存储配置，客户端监听变更 服务注册与发现：临时节点表示服务实例在线状态 选主（Leader Election）：最小序号节点成为 Master 分布式队列：顺序节点实现生产消费模型 重要知识点 ZooKeeper 如何保证数据一致性？ 答：通过 ZAB 协议，写操作需要半数以上节点确认，保证顺序一致性和原子性。读请求可能来自任意节点（可能读到旧数据），但可通过 sync 操作强制读取最新值。 临时节点的作用时什么？ 答：临时节点在会话结束后自动删除，常用于服务注册（服务下线自动清除）和分布式锁（客户端崩溃自动释放锁）。 如何处理脑裂问题？ 答：ZAB 协议要求写操作必须由 Leader 发起，且需要半数以上节点确认。网络分区时，仅拥有多数节点的分区能选举 Leader，另一个分区无法写入，避免数据不一致。 Watcher 机制有哪些主要事项？ 答：一次性触发、通知可能延迟、需要处理事件丢失（如 Watch 注册期间发生变更）。需在回调中重新注册 Watcher。 Leader 选举过程是怎样的？ 答：节点启动时进入选举状态，投票优先给 zxid 最大的节点，zxid 相同则选 myid 更大的。获得半数以上投票的节点成为 Leader。 如何实现分布式锁？ 答：客户端创建临时顺序节点，检查是否为最小序号节点。若是则获得锁；否则监听前一个节点的删除事件，使用 exists() +Watcher 避免轮询。 性能与优化 写性能：依赖半数以上节点确认，建议集群节点数为奇数（如3、5）。 读性能：可直接从本地节点读取，高吞吐。 快照与日志：定期生成快照（snapshot）和事务日志（txn log），加速恢复。 配置与运维 会话超时：tickTime 为基本时间单位，sessionTimeout 建议2-20倍tickTime 。 数据持久化：事务日志和快照存储于dataDir 和dataLogDir 。 ACL 控制：支持IP、Digest等多种认证方式，精细化权限管理。 ","date":"2018-03-22T22:21:04+08:00","permalink":"/blog/zookeeper/","title":"ZooKeeper"}]