<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>大布丁的博客</title><description>人格的容纳之地</description><link>https://chaggle.github.io/</link><language>zh_CN</language><item><title>DataX 适配 Kerberos Hadoop/Hive 集群：从 hdfsreader 到 hdfswriter 的踩坑实录</title><link>https://chaggle.github.io/posts/2026/08/21/datax-kerberos-hive-adaptation/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/21/datax-kerberos-hive-adaptation/</guid><pubDate>Fri, 21 Aug 2026 10:40:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;上一篇博客讲了 DolphinScheduler 怎么接入 Kerberos 认证的 Hive（数据源配置层面）。这次继续往链路深处走：调度平台里的 DataX 任务要真正读写 Kerberos 集群的 HDFS/Hive，官方开源版 DataX 直接跑不通，本文记录从 hdfsreader 读到 hdfswriter 写、再到 GBase 8c 反向推 Hive 的完整适配过程与排错结论。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、先看结论&lt;/h2&gt;
&lt;p&gt;官方 DataX 默认依赖 Hadoop 2.7.1 + Hive 1.1.1 + fastjson 1.x，而实际集群是 Hadoop 3.3.6 + Hive 3.1.3，且服务器发行版做了 fastjson2 改造。五个硬伤逐层暴露，每层都有对应的修复动作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;根因&lt;/th&gt;
&lt;th&gt;修复&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UnknownHost nameservice1&lt;/code&gt; / 表找不到&lt;/td&gt;
&lt;td&gt;defaultFS 与 hadoopConfig 的 nameservice 前缀不一致&lt;/td&gt;
&lt;td&gt;统一 ns 名，配全 HA 五个配置项&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;DataNode 全部 &lt;code&gt;连接被对方重设&lt;/code&gt;（RST）&lt;/td&gt;
&lt;td&gt;Kerberos 集群 DataNode 数据传输默认 SASL，客户端明文握手被拒&lt;/td&gt;
&lt;td&gt;hadoopConfig 加 &lt;code&gt;dfs.data.transfer.protection: authentication&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ClassCastException: JSONObject cannot be cast to JSONObject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;发行版 datax-common 已 fastjson2 化，插件源码还是 fastjson1&lt;/td&gt;
&lt;td&gt;源码 import 改 fastjson2 + pom 显式加 fastjson2 2.0.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unrecognized Hadoop major version number: 3.3.6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hive-exec 1.1.1 的 shims 不识别 Hadoop 3.x&lt;/td&gt;
&lt;td&gt;hive.version 升 3.1.3，删 hive-service / hive-hcatalog-core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;插件包内新旧版本并存&lt;/td&gt;
&lt;td&gt;maven-assembly 增量输出不清理&lt;/td&gt;
&lt;td&gt;打包前手动删残留 jar（注意别误删 hadoop 3.x 的 shaded 依赖）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;hdfswriter 在 hdfsreader 的全部适配之上还多了两个坑：parquet 依赖要靠 hive-hcatalog-core 传递、删掉后就断了；parquet 1.8 之后包名从 &lt;code&gt;parquet.schema.*&lt;/code&gt; 迁到 &lt;code&gt;org.apache.parquet.schema.*&lt;/code&gt;，源码 import 必须同步改。&lt;/p&gt;
&lt;h2&gt;二、30 秒看懂 DataX 的插件类加载&lt;/h2&gt;
&lt;p&gt;DataX 每个插件（reader/writer）独立目录、独立 classloader（child-first）：插件 &lt;code&gt;libs/&lt;/code&gt; 里的 jar 优先于全局 &lt;code&gt;lib/&lt;/code&gt;。这一条解释了后面几乎所有&quot;版本冲突&quot;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插件 libs 里的 &lt;code&gt;datax-common&lt;/code&gt; 若带旧 fastjson，会反向污染全局——&lt;strong&gt;插件包内禁止打 datax-common&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;服务器发行版的 &lt;code&gt;datax-common&lt;/code&gt; 是 fastjson2 改造版（&lt;code&gt;datax/lib/fastjson2-2.0.23.jar&lt;/code&gt;），所以插件源码必须按 fastjson2 编译。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外 rdbmsreader 有个特殊机制：插件初始化时读 &lt;code&gt;plugin.json&lt;/code&gt; 的 &lt;code&gt;drivers&lt;/code&gt; 数组，逐个 &lt;code&gt;Class.forName&lt;/code&gt; &lt;strong&gt;预加载&lt;/strong&gt;，任何一个类缺失直接抛 &lt;code&gt;数据库驱动加载错误&lt;/code&gt;——即使任务根本不用那个驱动。&lt;/p&gt;
&lt;h2&gt;三、hdfsreader：读 Kerberos HDFS 的五层修复&lt;/h2&gt;
&lt;h3&gt;3.1 nameservice 全套配置&lt;/h3&gt;
&lt;p&gt;defaultFS 的 ns 名必须与 hadoopConfig 的 &lt;code&gt;dfs.nameservices&lt;/code&gt; 前缀完全一致，且 HA 五个配置项缺一不可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;defaultFS&quot;: &quot;hdfs://nameservice1&quot;,
&quot;hadoopConfig&quot;: {
  &quot;dfs.nameservices&quot;: &quot;nameservice1&quot;,
  &quot;dfs.ha.namenodes.nameservice1&quot;: &quot;nn1,nn2&quot;,
  &quot;dfs.namenode.rpc-address.nameservice1.nn1&quot;: &quot;10.0.0.1:8020&quot;,
  &quot;dfs.namenode.rpc-address.nameservice1.nn2&quot;: &quot;10.0.0.2:8020&quot;,
  &quot;dfs.client.failover.proxy.provider.nameservice1&quot;: &quot;org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider&quot;,
  &quot;hadoop.security.authentication&quot;: &quot;kerberos&quot;,
  &quot;dfs.data.transfer.protection&quot;: &quot;authentication&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 dfs.data.transfer.protection 是 RST 的根因&lt;/h3&gt;
&lt;p&gt;现象最有迷惑性：三个 DataNode 全部 &lt;code&gt;Failed to connect to /10.x.x.x:1025 ... 连接被对方重设&lt;/code&gt;，换 2.7.1 客户端也一样。集群内 &lt;code&gt;hdfs dfs -cat&lt;/code&gt; 却能读——先隔离&quot;客户端问题 vs 集群问题&quot;，再猜原因。&lt;/p&gt;
&lt;p&gt;根因：Kerberos 集群 DataNode 数据传输默认 SASL 保护（&lt;code&gt;authentication&lt;/code&gt;），DataX 客户端没声明传输保护，以明文握手直接被 DataNode 拒绝（RST）。补上 &lt;code&gt;dfs.data.transfer.protection: authentication&lt;/code&gt; 后日志立刻出现「是[orc]类型的文件」，block 读取成功。&lt;/p&gt;
&lt;h3&gt;3.3 文件格式必须对齐（ORC 魔数）&lt;/h3&gt;
&lt;p&gt;Hive 默认建表即 ORC，&lt;code&gt;hdfs dfs -cat&lt;/code&gt; 看到文件头魔数 &lt;code&gt;ORC&lt;/code&gt;。fileType 配错（写成 text）会在探测阶段误判。读侧按 &lt;code&gt;fileType: &quot;orc&quot;&lt;/code&gt; 配。&lt;/p&gt;
&lt;h3&gt;3.4 fastjson2 双向冲突&lt;/h3&gt;
&lt;p&gt;发行版 datax-common 已 fastjson2 化，而 hdfsreader / plugin-unstructured-storage-util 源码还是 fastjson1，运行时报 &lt;code&gt;ClassCastException: com.alibaba.fastjson2.JSONObject cannot be cast to com.alibaba.fastjson.JSONObject&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;修复三件套：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;DFSUtil.java&lt;/code&gt;、&lt;code&gt;UnstructuredStorageReaderUtil.java&lt;/code&gt; 的 import 改 &lt;code&gt;com.alibaba.fastjson2.*&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;两个模块 pom 显式加 &lt;code&gt;fastjson2:2.0.23&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;插件包内删除 datax-common（避免 child-first 加载到旧版 common）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.5 hive 版本与打包残留&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;hive-exec 1.1.1 的 shims 不识别 Hadoop 3.x（&lt;code&gt;unrecognized Hadoop major version number&lt;/code&gt;），升 3.1.3 并&lt;strong&gt;去掉 hive-service、hive-hcatalog-core&lt;/strong&gt; 两个重型依赖（会拉入 Hadoop 2.x 旧 jar）；&lt;/li&gt;
&lt;li&gt;maven-assembly 增量输出不会清理 target/datax，版本升级后新旧 jar 并存（曾出现 19 个 1.1.1 残留），打包前必须手动清理；&lt;/li&gt;
&lt;li&gt;清理时注意：&lt;code&gt;hadoop-shaded-guava-1.1.1&lt;/code&gt;、&lt;code&gt;hadoop-shaded-protobuf_3_7-1.1.1&lt;/code&gt; 是 Hadoop 3.3.6 的正常依赖（版本号恰好也是 1.1.1），&lt;strong&gt;别误删&lt;/strong&gt;，只匹配 &lt;code&gt;hive.*1.1.1&lt;/code&gt; 与 &lt;code&gt;hcatalog&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四、hdfswriter：写侧多出的两个坑&lt;/h2&gt;
&lt;h3&gt;4.1 parquet 依赖断裂&lt;/h3&gt;
&lt;p&gt;hdfswriter 源码 import 了 &lt;code&gt;parquet.schema.*&lt;/code&gt;，原依赖靠 hive-hcatalog-core 传递。删除重型依赖后必须显式补：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.parquet&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;parquet-hadoop&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.10.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 parquet 包名迁移&lt;/h3&gt;
&lt;p&gt;光有 jar 不够：parquet 1.8 之后包名从 &lt;code&gt;parquet.schema.*&lt;/code&gt; 迁到 &lt;code&gt;org.apache.parquet.schema.*&lt;/code&gt;。本地 javac 单测即可复现「程序包parquet.schema不存在」，4 处 import（&lt;code&gt;MessageTypeParser&lt;/code&gt; / &lt;code&gt;OriginalType&lt;/code&gt; / &lt;code&gt;PrimitiveType&lt;/code&gt; / &lt;code&gt;Types&lt;/code&gt;）必须改，否则 classpath 里明明有 jar 也编不过。&lt;/p&gt;
&lt;h3&gt;4.3 fieldDelimiter 无条件必填&lt;/h3&gt;
&lt;p&gt;hdfswriter 的 &lt;code&gt;fieldDelimiter&lt;/code&gt; 在 Job 初始化时强制校验（null 直接抛 &lt;code&gt;REQUIRED_VALUE&lt;/code&gt;，且仅支持单字符），&lt;strong&gt;不区分文件格式&lt;/strong&gt;——即使写 ORC 也必须填 &lt;code&gt;&quot;\t&quot;&lt;/code&gt; 或 &lt;code&gt;&quot;,&quot;&lt;/code&gt;（列式存储实际不依赖它，但源码强制）。&lt;/p&gt;
&lt;h2&gt;五、反向场景：GBase 8c 推 Hive&lt;/h2&gt;
&lt;p&gt;目标变为：从 GBase 8c（openGauss/PostgreSQL 内核）读表，写 Kerberos 集群 Hive（ORC）。方向反过来，适配成果直接复用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;reader 换 rdbmsreader&lt;/strong&gt;（GBase 8c 是 PG 系，不能用 8a 的 gbasereader）。驱动选 openGauss 官方 JDBC（&lt;code&gt;opengauss-jdbc&lt;/code&gt;，驱动类 &lt;code&gt;org.postgresql.Driver&lt;/code&gt;），jdbcUrl 写 &lt;code&gt;jdbc:postgresql://host:5432/db&lt;/code&gt;——DataX 的 &lt;code&gt;DataBaseType&lt;/code&gt; 枚举按 URL 前缀匹配驱动，PG 是内置支持，&lt;strong&gt;插件源码与 plugin.json 都不用改&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;writer 用已适配的 hdfswriter&lt;/strong&gt;，JSON 侧与第三章完全相同。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;排错中踩到发行版的隐藏机制：任务一启动就报 &lt;code&gt;ClassNotFoundException: org.apache.hive.jdbc.HiveDriver&lt;/code&gt;——服务器版 &lt;code&gt;rdbmsreader/plugin.json&lt;/code&gt; 的 &lt;code&gt;drivers&lt;/code&gt; 数组&lt;strong&gt;比源码版多了 Hive 驱动&lt;/strong&gt;，&lt;code&gt;Class.forName&lt;/code&gt; 预加载缺类直接失败，与任务是否用 Hive 无关。补齐 &lt;code&gt;hive-jdbc-*-standalone.jar&lt;/code&gt; 后进入下一层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;No suitable driver found for jdbc:postgresql://...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一层说明 &lt;code&gt;org.postgresql.Driver&lt;/code&gt; 根本没进 classpath——检查 &lt;code&gt;rdbmsreader/libs/&lt;/code&gt; 确认 openGauss 驱动 jar 是否真的放到位。逐层排查的顺序本身也是经验：&lt;strong&gt;先让 plugin.json 的 drivers 全部能 Class.forName，再谈具体连接&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;六、一页速记&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;hdfsreader / hdfswriter 适配：
  pom: hadoop 3.3.6 + hive 3.1.3
       删 hive-service / hive-hcatalog-core
       hadoop-aliyun 排除 hadoop 传递
       fastjson2 2.0.23（源码 import 同步改）
       hdfswriter 另加 parquet-hadoop 1.10.1 + import 改 org.apache.parquet.schema.*
  打包: 清 hive.*1.1.1 与 hcatalog 残留（别动 hadoop-shaded-*1.1.1）
        删插件 libs 里的 datax-common
  JSON: hadoopConfig 全套 nameservice + dfs.data.transfer.protection=authentication
        fileType 按文件魔数（ORC）配
        fieldDelimiter 必填单字符（ORC 也要）

GBase 8c → Hive：
  reader = rdbmsreader + opengauss-jdbc（org.postgresql.Driver）
  jdbcUrl 前缀必须 jdbc:postgresql://
  发行版 plugin.json 的 drivers 数组可能含 HiveDriver，
  先保证 drivers 全部可加载，再排查具体连接

排错顺序：
  RST / 连接被重设 → 查 dfs.data.transfer.protection
  ClassCastException fastjson → 查插件 libs 与 datax/lib 版本
  unrecognized Hadoop major version → 查 hive-exec 版本
  驱动加载错误 ClassNotFound → 查 plugin.json drivers 与 libs jar
  No suitable driver → 查驱动 jar 是否真在 libs
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alibaba/DataX&quot;&gt;DataX 官方仓库（GitHub）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nehcuh/DataX/blob/master/docs/rdbmsreader.md&quot;&gt;DataX rdbmsreader 文档（GitHub）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alibaba/DataX/tree/master/hdfswriter&quot;&gt;hdfswriter 源码与 assembly 打包结构（GitHub）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dellaxing/DataX/commit/d763fa33c6ab6b289ad288aaa0f650a01c6ac6a9&quot;&gt;修复 rdbmsreader 插件 plugin.json 驱动加载问题（GitHub commit）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs-archive.cloudera.com/documentation/enterprise/5-7-x/topics/cdh_sg_hiveserver2_security.html&quot;&gt;HiveServer2 Security Configuration（Cloudera）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/parquet-java&quot;&gt;Apache Parquet 版本与包名迁移说明（GitHub）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://repo1.maven.org/maven2/org/opengauss/opengauss-jdbc/&quot;&gt;openGauss JDBC 驱动（Maven Central）&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>DolphinScheduler 接入 Kerberos 认证 Hive：从 KDC 到数据源测试连接</title><link>https://chaggle.github.io/posts/2026/08/21/hive-kerberos-dolphinscheduler/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/21/hive-kerberos-dolphinscheduler/</guid><pubDate>Thu, 20 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;一篇讲透 DolphinScheduler 接 Kerberos Hive 的全链路：底座怎么开 Kerberos、凭据怎么分发到调度节点、数据源表单里 principal / keytab / other 怎么填、常见报错怎么定位。文章写作前对标了开源社区已有博客与官方文档，把&quot;底座开启&quot;和&quot;调度平台接入&quot;两张拼图接上了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、先看结论&lt;/h2&gt;
&lt;p&gt;社区里&quot;开启 Hive Kerberos&quot;和&quot;DolphinScheduler 接 Hive&quot;的资料都不缺，但普遍是两段分离的：要么只讲 KDC、keytab、beeline 验证，要么只讲调度平台表单怎么填，中间&quot;凭据怎么到调度节点、进程怎么登录、JDBC URL 怎么拼&quot;这一截经常被一笔带过。&lt;/p&gt;
&lt;p&gt;本文把链路串成五步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;底座核对/开启 Kerberos
  → 调度节点准备凭据与进程登录配置
  → 数据源表单填值
  → 测试连接与验证
  → 按报错特征定位
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考的主要社区资料（详见文末）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://apache.googlesource.com/dolphinscheduler-website/+show/refs/heads/history-docs/docs/2.0.9/docs/zh/guide/datasource/hive.md&quot;&gt;Apache DolphinScheduler 官方 Hive 数据源文档&lt;/a&gt;（现行版见 &lt;a href=&quot;https://github.com/apache/dolphinscheduler/blob/136a1830187509c465236274b2e9e423ee13075f/docs/docs/en/guide/datasource/hive.md&quot;&gt;GitHub&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;CSDN《&lt;a href=&quot;https://blog.csdn.net/qq_53058639/article/details/139338696&quot;&gt;Kerberos 安全认证-连载10-Hive Kerberos 安全配置及访问&lt;/a&gt;》&lt;/li&gt;
&lt;li&gt;Cloudera《&lt;a href=&quot;https://docs-archive.cloudera.com/documentation/enterprise/5-7-x/topics/cdh_sg_hiveserver2_security.html&quot;&gt;HiveServer2 Security Configuration&lt;/a&gt;》&lt;/li&gt;
&lt;li&gt;DolphinScheduler Issue &lt;a href=&quot;https://github.com/apache/dolphinscheduler/issues/17413&quot;&gt;#17413&lt;/a&gt;、&lt;a href=&quot;https://github.com/apache/dolphinscheduler/issues/7964&quot;&gt;#7964&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、30 秒看懂 Kerberos：三个身份别混&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;本文示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;KDC&lt;/td&gt;
&lt;td&gt;密钥分发中心，存所有 principal 和密钥&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kdc.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;realm&lt;/td&gt;
&lt;td&gt;Kerberos 域，通常大写&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HADOOP.COM&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;principal&lt;/td&gt;
&lt;td&gt;身份名 &lt;code&gt;name/instance@REALM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;见下方三个身份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keytab&lt;/td&gt;
&lt;td&gt;存 principal 密钥的文件，相当于&quot;免密身份证&quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*.keytab&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TGT&lt;/td&gt;
&lt;td&gt;客户端登录 KDC 后拿到的&quot;临时通行证&quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kinit&lt;/code&gt; 后缓存于 &lt;code&gt;/tmp/krb5cc_*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;service ticket&lt;/td&gt;
&lt;td&gt;访问某个服务（HS2）的短期票据&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kvno hive/slave01@HADOOP.COM&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;一次 JDBC 连接会用到的三个身份：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;服务身份（HiveServer2 / Metastore）&lt;/strong&gt;：&lt;code&gt;hive/&amp;lt;服务节点hostname&amp;gt;@HADOOP.COM&lt;/code&gt;
——写在底座 &lt;code&gt;hive-site.xml&lt;/code&gt; 和服务端 keytab 里，也写在 JDBC URL 的 &lt;code&gt;principal=&lt;/code&gt; 参数里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端 / 调度平台身份（headless）&lt;/strong&gt;：&lt;code&gt;ds-hive@HADOOP.COM&lt;/code&gt;（名字可自取）
——写在 DolphinScheduler 的 &lt;code&gt;common.properties&lt;/code&gt; / 环境变量里，进程启动时用 keytab 登录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据源表单里的独立 principal 字段与 other 参数&lt;/strong&gt;：二者经常被混淆。最终进 JDBC URL 的是 &lt;strong&gt;other 里的 &lt;code&gt;principal=...&lt;/code&gt;&lt;/strong&gt;，独立字段只负责存储（见第六章代码事实）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;认证链路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DS 进程 --(1) headless keytab 登录 KDC，拿 TGT--&amp;gt;
DS 进程 --(2) 按 URL 中 principal=hive/slave01@HADOOP.COM 申请服务票据--&amp;gt;
DS 进程 --(3) GSSAPI 携带服务票据，与 HS2 握手--&amp;gt;
HS2      --(4) 用本机 hive.service.keytab 验签并证明身份--&amp;gt;
DS 进程 --(5) 握手成功，进入 SQL 会话
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
&lt;strong&gt;一句话理解&lt;/strong&gt;：Kerberos 把&quot;密码验证&quot;变成&quot;票据验证&quot;。调度进程用 keytab 登录一次拿 TGT，之后每次访问 Hive 都用 TGT 换服务票据；服务端只认 KDC 签发的票据，不接触密码。
:::&lt;/p&gt;
&lt;h2&gt;三、总体架构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────────────────────┐        ┌────────────────────────────────┐
│ Hive 集群（Kerberos 底座）      │        │ DolphinScheduler 集群            │
│  · KDC（realm=HADOOP.COM）     │        │  · api-server / master         │
│  · HiveServer2 (10000)         │        │  · worker × N                  │
│  · Metastore (9083)            │        │  · alert（可选）                │
│  · /etc/krb5.conf              │  scp   │  · &amp;lt;DS_HOME&amp;gt;/conf/kerberos/     │
│  · /etc/security/keytab/*      │ ─────▶ │    krb5.conf                   │
│  · hive-site.xml/hdfs-site.xml │        │    ds-hive.headless.keytab     │
└──────────────────────────────┘        │    hive.service.keytab（按需）   │
                                        └────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部署顺序就是文章顺序：底座（第四章）→ 调度节点（第五章）→ 数据源表单（第六章）→ 验证排错（第七、八章）。&lt;/p&gt;
&lt;p&gt;:::warning
如果你用的是 DataSophon 管理底座，keytab 通常统一在 &lt;code&gt;/etc/security/keytab/&lt;/code&gt;，krb5.conf 在 &lt;code&gt;/etc/krb5.conf&lt;/code&gt;，且 &lt;strong&gt;DataSophon 默认不创建 headless keytab&lt;/strong&gt;，需要单独申请。如果是 CDH/CDP/HDP/手工集群，路径不同但配置项一致，替换路径即可。
:::&lt;/p&gt;
&lt;h2&gt;四、底座侧：开启 / 核对 HiveServer2 与 Metastore 的 Kerberos&lt;/h2&gt;
&lt;p&gt;即使拿到的是&quot;已经开了 Kerberos 的集群&quot;，也建议过一遍本章——调度侧所有填值都应该来自这里的命令输出，而不是猜测。&lt;/p&gt;
&lt;h3&gt;4.1 在 KDC 上创建 principal&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 每个运行 HS2/Metastore 的节点一个服务 principal；-randkey 表示随机密钥，不设口令
kadmin.local -q &quot;addprinc -randkey hive/slave01@HADOOP.COM&quot;

# 调度平台进程身份（headless）：一个平台统一一个即可
kadmin.local -q &quot;addprinc -randkey ds-hive@HADOOP.COM&quot;

# 核对真实存在的 principal（这是后面所有填值的唯一事实来源）
kadmin.local -q &quot;listprincs&quot; | grep -iE &quot;hive|ds-hive&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
&lt;code&gt;slave01&lt;/code&gt; 必须是 &lt;strong&gt;hostname&lt;/strong&gt;（&lt;code&gt;hostname -f&lt;/code&gt; 的结果，FQDN 更稳），不是集群名、不是 IP。社区教程里 90% 的 &lt;code&gt;Server not found in Kerberos database&lt;/code&gt; 都是这里写错。
:::&lt;/p&gt;
&lt;h3&gt;4.2 生成并分发 keytab&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /etc/security/keytab

# 服务 keytab：只放在对应的 Hive 服务节点
kadmin.local -q &quot;ktadd -k /etc/security/keytab/hive.service.keytab hive/slave01@HADOOP.COM&quot;

# headless keytab：之后要复制到 DolphinScheduler 各节点
kadmin.local -q &quot;ktadd -k /etc/security/keytab/ds-hive.headless.keytab ds-hive@HADOOP.COM&quot;

chmod 600 /etc/security/keytab/*.keytab

# 立即核对 keytab 里实际装了哪些 principal
klist -k /etc/security/keytab/hive.service.keytab
klist -k /etc/security/keytab/ds-hive.headless.keytab
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3 服务端配置参数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;core-site.xml&lt;/code&gt;（涉及 HDFS 文件访问时）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property&amp;gt;&amp;lt;name&amp;gt;hadoop.security.authentication&amp;lt;/name&amp;gt;&amp;lt;value&amp;gt;kerberos&amp;lt;/value&amp;gt;&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;&amp;lt;name&amp;gt;hadoop.security.authorization&amp;lt;/name&amp;gt;&amp;lt;value&amp;gt;true&amp;lt;/value&amp;gt;&amp;lt;/property&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;hive-site.xml&lt;/code&gt; 关键参数（★ = Kerberos 必须项）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;推荐值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.server2.authentication&lt;/code&gt; ★&lt;/td&gt;
&lt;td&gt;&lt;code&gt;KERBEROS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HS2 认证开关&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.server2.authentication.kerberos.principal&lt;/code&gt; ★&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hive/_HOST@HADOOP.COM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;服务 principal；&lt;code&gt;_HOST&lt;/code&gt; 自动替换本机 hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.server2.authentication.kerberos.keytab&lt;/code&gt; ★&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/etc/security/keytab/hive.service.keytab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HS2 服务 keytab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.metastore.sasl.enabled&lt;/code&gt; ★&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Metastore Thrift SASL 开关&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.metastore.kerberos.principal&lt;/code&gt; ★&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hive/_HOST@HADOOP.COM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Metastore principal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.metastore.kerberos.keytab.file&lt;/code&gt; ★&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/etc/security/keytab/hive.service.keytab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Metastore keytab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.server2.transport.mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;binary&lt;/code&gt;（默认）&lt;/td&gt;
&lt;td&gt;binary/http 不一致是 &lt;code&gt;invalid status 80&lt;/code&gt; 的根源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.server2.support.dynamic.service.discovery&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;（HA 时）&lt;/td&gt;
&lt;td&gt;ZK 动态发现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.server2.zookeeper.namespace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hiveserver2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ZK 命名空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.zookeeper.quorum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;slave01:2181,slave02:2181,slave03:2181&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HA 场景 ZK 地址&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;4.4 重启并验证（beeline 是社区的&quot;标准裁判&quot;）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 按实际环境重启（systemd / 管理台 / 手工脚本均可）
systemctl restart hive-server2 hive-metastore

# 服务身份能登录
kinit -kt /etc/security/keytab/hive.service.keytab hive/slave01@HADOOP.COM &amp;amp;&amp;amp; klist &amp;amp;&amp;amp; kdestroy

# beeline 直连：这是区分&quot;底座问题 vs 调度平台问题&quot;的黄金标准
/opt/hive/bin/beeline \
  -u &quot;jdbc:hive2://slave01:10000/default;principal=hive/slave01@HADOOP.COM&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
判断口径：beeline 也报错 → 回到底座，看 HS2 日志、KDC 服务、时钟与 DNS；beeline 能通、DolphinScheduler 不通 → 问题在调度侧，重点查 &lt;code&gt;other.principal&lt;/code&gt; 和全局开关。
:::&lt;/p&gt;
&lt;h2&gt;五、调度侧：让每个 DolphinScheduler 节点都&quot;带好证件&quot;&lt;/h2&gt;
&lt;h3&gt;5.1 需要分发哪些文件&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;分发范围&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;krb5.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;KDC/realm/域名映射&lt;/td&gt;
&lt;td&gt;所有 DS 节点（api-server/master、worker、alert）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds-hive.headless.keytab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DS 进程登录身份&lt;/td&gt;
&lt;td&gt;所有 DS 节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.service.keytab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仅元数据（Metastore）链路使用&lt;/td&gt;
&lt;td&gt;按需，只给 api-server/master 节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hdfs-site.xml&lt;/code&gt; / &lt;code&gt;hive-site.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元数据客户端需要&lt;/td&gt;
&lt;td&gt;按需，只给 api-server/master 节点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;目标目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;DS_HOME&amp;gt;/conf/kerberos/
├── krb5.conf
├── ds-hive.headless.keytab
├── hive.service.keytab        # 按需
├── hdfs-site.xml              # 按需
└── hive-site.xml              # 按需
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 全局进程登录配置（两种等价方式）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;PropertyUtils&lt;/code&gt; 的优先级：&lt;strong&gt;环境变量 &amp;gt; JVM 参数 &amp;gt; 外部 common.properties &amp;gt; jar 内 classpath&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式 A：外部 &lt;code&gt;common.properties&lt;/code&gt;（每个服务一份，改完重启）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 总开关：false 时 JDBC URL 会用 ? 拼 other，行为完全不同
hadoop.security.authentication.startup.state=true

# krb5.conf 绝对路径
java.security.krb5.conf.path=&amp;lt;DS_HOME&amp;gt;/conf/kerberos/krb5.conf

# 平台进程身份（headless principal + keytab）
login.user.keytab.username=ds-hive@HADOOP.COM
login.user.keytab.path=&amp;lt;DS_HOME&amp;gt;/conf/kerberos/ds-hive.headless.keytab

# TGT 生命周期（小时），进程每 5 分钟自动 checkTGTAndReloginFromKeytab
kerberos.expire.time=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式 B：环境变量（优先级更高，适合快速兜底或老版本 jar）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export HADOOP_SECURITY_AUTHENTICATION_STARTUP_STATE=true
export JAVA_SECURITY_KRB5_CONF_PATH=&amp;lt;DS_HOME&amp;gt;/conf/kerberos/krb5.conf
export LOGIN_USER_KEYTAB_USERNAME=ds-hive@HADOOP.COM
export LOGIN_USER_KEYTAB_PATH=&amp;lt;DS_HOME&amp;gt;/conf/kerberos/ds-hive.headless.keytab
export KERBEROS_EXPIRE_TIME=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;resource.storage.type=HDFS&lt;/code&gt; 且总开关为 true 时才会走 Kerberos 登录；资源存储不是 HDFS 时先确认这条链路。
:::&lt;/p&gt;
&lt;h3&gt;5.3 JAAS：每次从 keytab 取票，而不是依赖会过期的 ticket cache&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;DS_HOME&amp;gt;/conf/kerberos/jaas.conf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Client {
    com.sun.security.auth.module.Krb5LoginModule required
    useKeyTab=true
    keyTab=&quot;&amp;lt;DS_HOME&amp;gt;/conf/kerberos/ds-hive.headless.keytab&quot;
    principal=&quot;ds-hive@HADOOP.COM&quot;
    storeKey=true
    useTicketCache=false;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让 JVM 生效（上游 DolphinScheduler 写入 &lt;code&gt;&amp;lt;DS_HOME&amp;gt;/conf/dolphinscheduler_env.sh&lt;/code&gt;；同源平台写入各服务 start.sh）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export JAVA_TOOL_OPTIONS=&quot;${JAVA_TOOL_OPTIONS} \
  -Djava.security.auth.login.config=&amp;lt;DS_HOME&amp;gt;/conf/kerberos/jaas.conf \
  -Djava.security.krb5.conf=&amp;lt;DS_HOME&amp;gt;/conf/kerberos/krb5.conf&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
为什么必须 &lt;code&gt;useTicketCache=false&lt;/code&gt;：Hive JDBC 驱动的 GSSAPI 默认去系统 ticket cache 找凭证，手动 &lt;code&gt;kinit&lt;/code&gt; 的 TGT 过期后就会出现&quot;测试连接通过、跑着跑着 &lt;code&gt;TGT is expired&lt;/code&gt;&quot;的偶发故障。JAAS 每次从 keytab 登录才能根治。
:::&lt;/p&gt;
&lt;h3&gt;5.4 安装客户端工具&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# CentOS/RHEL
yum install -y krb5-workstation
# Ubuntu/Debian
apt-get install -y krb5-user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;klist&lt;/code&gt; / &lt;code&gt;kinit&lt;/code&gt; / &lt;code&gt;kvno&lt;/code&gt; 齐全，后面的验证才跑得起来。&lt;/p&gt;
&lt;h2&gt;六、数据源配置：表单字段逐个说清楚&lt;/h2&gt;
&lt;p&gt;本文示例：HS2 节点 &lt;code&gt;slave01&lt;/code&gt;、端口 10000、ZK 三个节点。DolphinScheduler 与其同源数据中台的字段一致。&lt;/p&gt;
&lt;h3&gt;6.1 场景 A：JDBC-Hive 数据源（单 HS2，binary 模式）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;填写值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;类型&lt;/td&gt;
&lt;td&gt;&lt;code&gt;JDBC-Hive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;后端 &lt;code&gt;DbType.HIVE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主机 host&lt;/td&gt;
&lt;td&gt;&lt;code&gt;slave01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HS2 所在节点；支持逗号分隔多 host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;端口 port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HS2 端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据库 database&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户名 userName / 密码 password&lt;/td&gt;
&lt;td&gt;非空即可&lt;/td&gt;
&lt;td&gt;Kerberos 场景不参与认证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;principal（独立字段）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hive/slave01@HADOOP.COM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储用，不自动拼 URL（见下方代码事实）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;krb5.conf 路径&lt;/td&gt;
&lt;td&gt;留空用全局，或填节点绝对路径&lt;/td&gt;
&lt;td&gt;数据源级覆盖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keytab 用户名 / keytab 路径&lt;/td&gt;
&lt;td&gt;留空用全局，或填 headless keytab&lt;/td&gt;
&lt;td&gt;数据源级覆盖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;其他参数 other&lt;/strong&gt; ★&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;principal=hive/slave01@HADOOP.COM&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;必须填；多个用 &lt;code&gt;;&lt;/code&gt; 分隔&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::caution
&lt;strong&gt;代码事实（DolphinScheduler 与同源平台一致）&lt;/strong&gt;：&lt;code&gt;getJdbcUrl()&lt;/code&gt; 最终 URL = &lt;code&gt;jdbc:hive2://host:port/db&lt;/code&gt; + &lt;code&gt;;&lt;/code&gt; + &lt;code&gt;other&lt;/code&gt;。全局开关为 true 时用 &lt;code&gt;;&lt;/code&gt; 拼接，false 时用 &lt;code&gt;?&lt;/code&gt; 拼接。&lt;strong&gt;独立 principal 字段只存储，不参与 URL 拼接&lt;/strong&gt;——&lt;code&gt;other.principal&lt;/code&gt; 漏填，报错就是 &lt;code&gt;Unsupported mechanism type PLAIN&lt;/code&gt;。
:::&lt;/p&gt;
&lt;p&gt;正确效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jdbc:hive2://slave01:10000/default;principal=hive/slave01@HADOOP.COM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
底座是 binary 模式时，other 不要加 &lt;code&gt;transportMode=http;httpPath=cliservice&lt;/code&gt;，否则报 &lt;code&gt;invalid status 80&lt;/code&gt;。只有服务端明确配置了 &lt;code&gt;hive.server2.transport.mode=http&lt;/code&gt; 及 HTTP 端口/路径时才加。
:::&lt;/p&gt;
&lt;h3&gt;6.2 场景 A+：HS2 高可用（ZooKeeper 动态发现）&lt;/h3&gt;
&lt;p&gt;当底座配置了 &lt;code&gt;hive.server2.support.dynamic.service.discovery=true&lt;/code&gt; 时：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;填写值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;主机 host&lt;/td&gt;
&lt;td&gt;&lt;code&gt;slave01,slave02,slave03&lt;/code&gt;（ZK 节点列表）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;端口 port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2181&lt;/code&gt;（每个 host 都会拼该端口）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据库 database&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other&lt;/td&gt;
&lt;td&gt;&lt;code&gt;serviceDiscoveryMode=zooKeeper;zooKeeperNamespace=hiveserver2;principal=hive/_HOST@HADOOP.COM&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最终 URL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jdbc:hive2://slave01:2181,slave02:2181,slave03:2181/default;serviceDiscoveryMode=zooKeeper;zooKeeperNamespace=hiveserver2;principal=hive/_HOST@HADOOP.COM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Hive JDBC 从 ZK 读到实际 HS2 主机名后，会把 &lt;code&gt;principal&lt;/code&gt; 里的 &lt;code&gt;_HOST&lt;/code&gt; 替换为该主机名。不能写死 &lt;code&gt;hive/slave01@...&lt;/code&gt;，否则连接被调度到其他节点时 GSS 失败。前提是每个 HS2 节点 keytab 都含 &lt;code&gt;hive/&amp;lt;本机hostname&amp;gt;@REALM&lt;/code&gt;，且 DS 节点能访问 ZK 2181。
:::&lt;/p&gt;
&lt;h3&gt;6.3 场景 B：元数据（Metastore）链路&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;填写值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;metastoreUri&lt;/td&gt;
&lt;td&gt;&lt;code&gt;thrift://slave01:9083&lt;/code&gt;（HA 逗号分隔多地址）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kerberosPrincipal / kerberosKeytabPath / kerberosKrb5ConfPath&lt;/td&gt;
&lt;td&gt;可填，但同源平台代码会用全局 &lt;code&gt;login.user.keytab.*&lt;/code&gt; 覆盖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hdfsSitePath&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;DS_HOME&amp;gt;/conf/kerberos/hdfs-site.xml&lt;/code&gt;（必填）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hiveSitePath&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;DS_HOME&amp;gt;/conf/kerberos/hive-site.xml&lt;/code&gt;（可选）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
元数据链路实际认证身份 = 全局 headless principal，不是前端那三个字段。所以 headless keytab 必须申请，并且 Metastore 侧 ACL 要放行该 principal。
:::&lt;/p&gt;
&lt;h3&gt;6.4 测试连接&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;点「测试连接」：JDBC-Hive → keytab 登录 + &lt;code&gt;DriverManager.getConnection&lt;/code&gt;；元数据链路 → &lt;code&gt;HiveClient&lt;/code&gt; → &lt;code&gt;UGI.loginUserFromKeytab&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;成功标志：服务日志出现 &lt;code&gt;Kerberos authentication successful&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;失败按第八章速查表定位。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;七、端到端验证三板斧&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 斧 1：服务端日志（HS2 的错误是决定性的）
tail -n 1000 /opt/hive/logs/hiveserver2.log | grep -B5 -A30 &quot;Caused by&quot; | tail -100

# 斧 2：客户端身份与票据（在 DS 任一节点）
kinit -kt &amp;lt;DS_HOME&amp;gt;/conf/kerberos/ds-hive.headless.keytab ds-hive@HADOOP.COM &amp;amp;&amp;amp; klist
kvno hive/slave01@HADOOP.COM
# 拿不到服务票据 = KDC 里 principal 不存在或客户端到 KDC 链路不通

# 斧 3：DS 服务日志（客户端侧 Caused by）
grep -B3 -A15 &quot;GSS\|Caused by&quot; &amp;lt;DS_HOME&amp;gt;/logs/dolphinscheduler-api.log | tail -80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有 beeline 时的替代验证（在 Hive 节点，参考阿里云 EMR 的 Java 连接示例）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -cp &quot;.:$HIVE_HOME/lib/*&quot; HiveJdbcTest \
  &quot;jdbc:hive2://slave01:10000/default;principal=hive/slave01@HADOOP.COM&quot; hive &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;八、排错速查表：按报错特征定位&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;报错特征&lt;/th&gt;
&lt;th&gt;根因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;Unsupported mechanism type PLAIN&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JDBC URL 没有有效 &lt;code&gt;principal=&lt;/code&gt;；或全局开关 false（URL 用 &lt;code&gt;?&lt;/code&gt; 拼接）&lt;/td&gt;
&lt;td&gt;在 other 里补 &lt;code&gt;principal=hive/&amp;lt;hostname&amp;gt;@REALM&lt;/code&gt;；确认总开关 true 且已重启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;invalid status 80&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;客户端 http 模式 vs 服务端 binary 模式&lt;/td&gt;
&lt;td&gt;other 去掉 &lt;code&gt;transportMode=http;httpPath=cliservice&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GSS initiate failed: Server not found in Kerberos database&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL principal 在 KDC 不存在（host 写成集群名/IP）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kadmin.local -q &quot;listprincs&quot;&lt;/code&gt; 查真实 principal，KDC/keytab/hive-site 三处一致&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GSS initiate failed: Clock skew too great / checksum fail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;节点时钟偏差 &amp;gt; 5 分钟&lt;/td&gt;
&lt;td&gt;全节点 NTP 同步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GSS initiate failed: Cannot contact any KDC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;krb5.conf 错误 / KDC 88 端口不通&lt;/td&gt;
&lt;td&gt;查 krb5.conf、测 88 端口、&lt;code&gt;kinit -V&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GSS initiate failed: Failed to find any Kerberos tgt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;headless keytab 登录失败&lt;/td&gt;
&lt;td&gt;核对 &lt;code&gt;login.user.keytab.username/path&lt;/code&gt; 与 &lt;code&gt;klist -k&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cannot locate default realm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;krb5.conf 缺失或 &lt;code&gt;java.security.krb5.conf.path&lt;/code&gt; 配错&lt;/td&gt;
&lt;td&gt;核对路径与文件内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cannot get kdc for realm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;realm→KDC 映射或 DNS/反向解析问题&lt;/td&gt;
&lt;td&gt;配 &lt;code&gt;[domain_realm]&lt;/code&gt;；&lt;code&gt;rdns=false&lt;/code&gt;；核对 &lt;code&gt;hostname -f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keytab contains no suitable keys for ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kinit 用的 principal 与 keytab 内容不一致&lt;/td&gt;
&lt;td&gt;&lt;code&gt;klist -k &amp;lt;keytab&amp;gt;&lt;/code&gt; 用文件内真实 principal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unable to obtain password from user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;keytab 不存在/权限不对&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chmod 600&lt;/code&gt;，确认运行账户可读&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;TGT is expired&lt;/code&gt;（连接通过后偶发）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JDBC 依赖系统 ticket cache，手动 kinit 的 TGT 过期&lt;/td&gt;
&lt;td&gt;应急 &lt;code&gt;kinit -kt ...&lt;/code&gt;；长期用 JAAS &lt;code&gt;useKeyTab=true;useTicketCache=false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kinit 长时间卡住&lt;/td&gt;
&lt;td&gt;KDC 不可达&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kinit -V&lt;/code&gt;、&lt;code&gt;getent hosts&lt;/code&gt;、测 88 端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DS 表单填了 principal 但 URL 没带上&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;部分 DS 版本已知 Bug（Issue &lt;a href=&quot;https://github.com/apache/dolphinscheduler/issues/17413&quot;&gt;#17413&lt;/a&gt; / &lt;a href=&quot;https://github.com/apache/dolphinscheduler/issues/7964&quot;&gt;#7964&lt;/a&gt;）&lt;/td&gt;
&lt;td&gt;升级/打 patch；临时规避：principal 同时写进 other；抓日志确认最终 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;测试连接成功但作业失败&lt;/td&gt;
&lt;td&gt;worker 节点未分发凭据或配置未同步&lt;/td&gt;
&lt;td&gt;重新分发到全部 worker，并重启 worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;未开启 Kerberos 但报认证错误&lt;/td&gt;
&lt;td&gt;总开关误开&lt;/td&gt;
&lt;td&gt;确认 &lt;code&gt;hadoop.security.authentication.startup.state&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;报错演进坐标（真实排错经历，建议背下来）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PLAIN（开关/principal 缺失）
  → invalid status 80（transport 不匹配）
  → GSS initiate failed / Server not found（principal 不在 KDC）
  → 连接成功
  → HA 切换（ZK 动态发现 + principal=_HOST）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每越过一个报错，说明前面的配置已经正确，聚焦当前报错即可，不要推倒重来。&lt;/p&gt;
&lt;h2&gt;九、生产安全清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;keytab = 明文密码&lt;/strong&gt;：所有 keytab &lt;code&gt;chmod 600&lt;/code&gt;，属主为运行账户，禁止提交 Git、禁止发聊天群。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小分发&lt;/strong&gt;：服务 keytab 只在 Hive 节点；DS 节点只分发 headless keytab；&lt;code&gt;hive.service.keytab&lt;/code&gt; 仅在元数据链路需要时给 master 节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小授权&lt;/strong&gt;：headless principal 一个平台一个，不复用管理员 principal；Metastore ACL 只放行该身份。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时钟与 DNS&lt;/strong&gt;：NTP 全集群同步（与 KDC 偏差 &amp;lt; 5 分钟）；hostname/FQDN、正反向解析一致，krb5.conf 建议 &lt;code&gt;rdns=false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;凭据轮换&lt;/strong&gt;：keytab 变更后，重新分发 → 重启服务 → 重新测试连接，避免&quot;改了 KDC 忘了中台&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本核对&lt;/strong&gt;：先确认 DolphinScheduler 版本没有 #17413/#7964 类字段 Bug，升级前在测试环境验证。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;十、一页速记&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;底座：kadmin.local addprinc → ktadd → chmod 600
       hive-site.xml: authentication=KERBEROS + principal=_HOST + keytab
       beeline 验证通过才算底座 OK

调度：每个 DS 节点：krb5.conf + headless.keytab
       common.properties: startup.state=true + krb5 path + login.user.keytab.*
       jaas.conf: useKeyTab=true / useTicketCache=false

数据源：
       单机:  other = principal=hive/&amp;lt;hostname&amp;gt;@REALM
       HA  :  host=ZK列表, port=2181,
              other = serviceDiscoveryMode=zooKeeper;zooKeeperNamespace=hiveserver2;principal=hive/_HOST@REALM
       binary 底座: 不要加 transportMode=http;httpPath=cliservice

排错顺序：
       PLAIN → 补 other.principal / 开总开关
       invalid status 80 → 去掉 http 参数
       GSS/Server not found → kadmin.local 核对 principal
       Clock skew → NTP
       TGT is expired → JAAS useTicketCache=false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://apache.googlesource.com/dolphinscheduler-website/+show/refs/heads/history-docs/docs/2.0.9/docs/zh/guide/datasource/hive.md&quot;&gt;Apache DolphinScheduler 官方 Hive 数据源文档（2.0.9 中文）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/dolphinscheduler/blob/136a1830187509c465236274b2e9e423ee13075f/docs/docs/en/guide/datasource/hive.md&quot;&gt;Apache DolphinScheduler hive datasource doc（GitHub）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_53058639/article/details/139338696&quot;&gt;Kerberos 安全认证-连载10-Hive Kerberos 安全配置及访问（CSDN）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_32020645/article/details/131344549&quot;&gt;同主题另一版本（CSDN）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/golove666/article/details/137371835&quot;&gt;将生成的 keytab 文件分发到运行 Hive 服务的所有节点（CSDN）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.alibabacloud.com/help/zh/emr/emr-on-ecs/user-guide/use-java-to-connect-to-an-emr-cluster-with-kerberos-authentication-enabled&quot;&gt;使用 Java 连接开启 Kerberos 认证的 Hive（阿里云 EMR）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs-archive.cloudera.com/documentation/enterprise/5-7-x/topics/cdh_sg_hiveserver2_security.html&quot;&gt;HiveServer2 Security Configuration（Cloudera）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ezmeral.hpe.com/datafabric-customer-managed/80/Hive/HiveServer2-KerberosAuth.html&quot;&gt;Configure HiveServer 2 to use Kerberos（Ezmeral）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/dolphinscheduler/issues/17413&quot;&gt;DolphinScheduler Issue #17413&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/dolphinscheduler/issues/7964&quot;&gt;DolphinScheduler Issue #7964&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/chhyan-dream/p/13442628.html&quot;&gt;Kerberos 的 keytab 文件生成和登录（博客园）&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>FDE（前向部署工程师）岗的核心学习方法</title><link>https://chaggle.github.io/posts/2026/08/20/fde-learning-guide/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/20/fde-learning-guide/</guid><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文以「FDE（Forward Deployed Engineer，前向部署工程师）」为目标岗位，沿用《&lt;a href=&quot;https://chaggle.github.io/2026/08/13/ai/agent-harness-learning-guide/&quot;&gt;Agent Harness 岗的核心学习方法&lt;/a&gt;》的调研方法，对 Palantir、OpenAI、Anthropic、Google Cloud、Databricks、Scale AI 等公司的公开岗位、bilibili 视频课程与 GitHub 开源项目做了一次系统调研。文章梳理 FDE 的岗位本质与 2026 年爆火的原因，给出 T 型技能树、从基础到进阶的分阶段实操路线与面试准备方法，避免泛泛而谈。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#1-%E5%B2%97%E4%BD%8D%E5%AE%9A%E4%BD%8D&quot;&gt;岗位定位：FDE 是什么、为什么 2026 年突然成为最热岗位&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-%E6%A0%B8%E5%BF%83%E6%8A%80%E8%83%BD%E6%A0%91&quot;&gt;核心技能树：T 型能力模型&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-bilibili-%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%BA%90&quot;&gt;bilibili 学习资源：入门到 Offer 的视频路线&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-github-%E9%A1%B9%E7%9B%AE%E7%B2%BE%E8%AF%BB&quot;&gt;GitHub 项目精读：读落地、学架构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-%E8%AE%AD%E7%BB%83%E8%B7%AF%E7%BA%BF&quot;&gt;训练路线：分阶段实操与可复现实验&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-%E5%AF%B9%E6%A0%87%E6%8B%9B%E8%81%98%E8%A6%81%E6%B1%82&quot;&gt;对标招聘要求：能力映射与面试准备&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;1. 岗位定位&lt;/h1&gt;
&lt;h2&gt;1.1 起源：Palantir 定义了&quot;前线工程师&quot;的原型&lt;/h2&gt;
&lt;p&gt;要理解 FDE，绕不开 Palantir。2009 年前后，Palantir 为了在政府与商业客户现场部署 Foundry 与 Gotham 平台，把嵌入客户现场的软件工程师称为 &lt;strong&gt;FDSE（Forward Deployed Software Engineer）&lt;/strong&gt;，与总部负责数据分析的 Echo 团队搭配作战（驻场工程师内部代号 Delta）。&lt;/p&gt;
&lt;p&gt;Palantir 官方对 FDSE 与传统软件工程师的区分非常精辟：&lt;strong&gt;传统软件工程师&quot;为很多客户开发一个通用能力&quot;，而 FDSE&quot;为一个客户组合很多能力&quot;&lt;/strong&gt;。前者是产品中心主义——做一个标准产品卖给很多客户；后者是客户现场主义——进入一个客户的真实环境，把平台、数据、流程、应用、用户一起拉通，解决一个具体而复杂的问题。Palantir 还把 FDSE 比作&quot;startup CTO&quot;：小团队、高自主性、对项目端到端负责。&lt;/p&gt;
&lt;h2&gt;1.2 一个公式：Model + Harness + FDE = 落地&lt;/h2&gt;
&lt;p&gt;上篇《&lt;a href=&quot;https://chaggle.github.io/2026/08/13/ai/agent-harness-learning-guide/&quot;&gt;Agent Harness 岗的核心学习方法&lt;/a&gt;》讲过 DeepSeek Harness 团队的公式 &lt;strong&gt;Model + Harness = Agent&lt;/strong&gt;：模型提供智能，Harness 提供让 Agent 可靠跑起来的全部工程脚手架。&lt;/p&gt;
&lt;p&gt;FDE 在这个公式上的位置是&quot;最后一公里&quot;：&lt;strong&gt;Harness 解决&quot;Agent 能不能可靠跑起来&quot;，FDE 解决&quot;客户会不会真的用起来&quot;&lt;/strong&gt;。打个比方：模型是发动机，Harness 是整车底盘，FDE 是把车开进客户厂区、打通厂内路况、教会司机、再把故障反馈带回厂里改车的人。&lt;/p&gt;
&lt;h2&gt;1.3 为什么 2026 年突然爆火&lt;/h2&gt;
&lt;p&gt;三个数据点可以说明&quot;AI 落地缺的不是算力，而是脚力&quot;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;试点失败率极高&lt;/strong&gt;：MIT 媒体实验室 Project NANDA 的《The GenAI Divide: State of AI in Business 2025》报告分析了 300 多个企业 AI 部署项目，结论是 &lt;strong&gt;95% 的企业生成式 AI 试点没有产生可量化的损益影响&lt;/strong&gt;，涉及的投入约 300~400 亿美元。瓶颈不在模型，而在部署。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大厂集中下注&lt;/strong&gt;：2026 年 5 月的一周内，OpenAI 宣布成立 Deployment Company（19 家机构注资 40 亿美元）；Anthropic 与 Blackstone、Goldman Sachs 合资 15 亿美元做企业部署；Google Cloud 一次性开出 59 个 FDE 职位。Google Cloud CEO 在 2026 年 4 月的 Next 大会上说：&quot;试点的时代结束了，Agent 的时代来了。&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;职缺与薪资暴涨&lt;/strong&gt;：据 The New Stack 与 Lightcast 的数据，FDE 全球职缺从 2024 到 2025 年增长约 800%；Exponent 对招聘数据的统计显示 2025 年底约有 922 个 FDE 在招岗位，同比增长五倍。薪资方面（2026 年 5 月 Levels.fyi 与 Glassdoor 数据）：Palantir FDSE 中位总包约 21.5 万美元（Staff 级可超 63 万美元），OpenAI 中位约 55.5 万美元，Anthropic 中高级 35~55 万美元，入门级 14~22 万美元；对 1000 条 FDE 岗位的分析显示 &lt;strong&gt;0% 带销售配额&lt;/strong&gt;——它是工程岗，不是销售岗。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1.4 各家公司的 FDE 定义&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;公司&lt;/th&gt;
&lt;th&gt;岗位名称&lt;/th&gt;
&lt;th&gt;一句话定义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Palantir&lt;/td&gt;
&lt;td&gt;FDSE / Forward Deployed AI Engineer&lt;/td&gt;
&lt;td&gt;嵌入客户现场的工程师，负责 GenAI 战略与实施，把一线经验反馈给 AIP 产品套件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Forward Deployed Engineer&lt;/td&gt;
&lt;td&gt;帮助战略客户完成前沿模型端到端生产部署，负责 discovery、technical scoping、system design、build 与 production rollout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Forward Deployed Engineer, Applied AI&lt;/td&gt;
&lt;td&gt;在客户系统内构建生产应用，交付 MCP server、sub-agent、agent skills 等技术资产&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud&lt;/td&gt;
&lt;td&gt;Generative AI FDE（embedded builder）&lt;/td&gt;
&lt;td&gt;嵌入客户环境 code、debug、jointly ship，把原型推向 production-grade agentic workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databricks&lt;/td&gt;
&lt;td&gt;AI Engineer / FDE&lt;/td&gt;
&lt;td&gt;帮助客户构建并生产化 first-of-its-kind AI 应用（RAG、多智能体、Text2SQL）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale AI&lt;/td&gt;
&lt;td&gt;Forward Deployed AI Engineer&lt;/td&gt;
&lt;td&gt;开发生产级 AI agents 与多智能体系统，实现评测框架与 human-in-the-loop 闭环&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harvey&lt;/td&gt;
&lt;td&gt;Legal Engineer / New Verticals&lt;/td&gt;
&lt;td&gt;把法律等垂直行业工作流转化为可复制的产品能力，是领域专家型 FDE 的代表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个值得注意的对照：&lt;strong&gt;DeepSeek 社招板（mokahr，org=high-flyer / site=140576）截至 2026-08-20 全量 36 个岗位里，没有独立的 FDE 岗位&lt;/strong&gt;；最接近的是「AGI 核心业务管培生」「AI 产品运营（体验与服务方向）」「AI 跨界技术人才」，以及上篇分析的 Agent Harness 团队岗位。FDE 的机会目前更多在 Palantir、OpenAI、Anthropic、Google Cloud 这类客户侧组织，以及国内做 Agent 落地的公司。&lt;/p&gt;
&lt;h1&gt;2. 核心技能树&lt;/h1&gt;
&lt;p&gt;FDE 面试评估的是 &lt;strong&gt;T 型画像&lt;/strong&gt;：一个深度方向、多个广度领域，外加一根&quot;客户侧软技能&quot;的竖线。&lt;/p&gt;
&lt;h2&gt;2.1 横杠：广度（所有 FDE 必备）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;生产级代码&lt;/strong&gt;：Python 为主，TypeScript/Go/Java 至少其一。不是脚本，而是带测试、错误处理、可观测性、清晰接口的工程代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQL&lt;/strong&gt;：窗口函数、CTE、查询优化、多亿行表上的 messy join。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现代数据栈&lt;/strong&gt;：Snowflake/BigQuery/Redshift、dbt、Airflow 或同类编排工具。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API 集成&lt;/strong&gt;：REST/GraphQL、流式数据、OAuth/SAML/SCIM 认证流、限流、重试退避、幂等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云平台&lt;/strong&gt;：AWS 为主，VPC、IAM、密钥管理、私有网络。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;真实负载的系统设计&lt;/strong&gt;：不是&quot;设计 Instagram&quot;，而是&quot;为一个受监管客户设计带脏数据、SSO 与严格变更窗口的部署&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 素养&lt;/strong&gt;：prompt engineering、RAG 架构（切块、嵌入选型、重排）、agent 编排、评测、微调取舍、向量数据库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.2 竖杠：深度（四选一）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;深度方向&lt;/th&gt;
&lt;th&gt;典型取向公司&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;分布式数据系统与管道&lt;/td&gt;
&lt;td&gt;Palantir、Databricks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生产级 LLM 系统与评测&lt;/td&gt;
&lt;td&gt;OpenAI、Anthropic、Cohere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;后端平台工程 + 安全合规&lt;/td&gt;
&lt;td&gt;国防、金融类 FDE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;前端 + 全栈 ownership&lt;/td&gt;
&lt;td&gt;小型初创公司&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;2.3 软技能：面试全程被考察&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客户同理心&lt;/strong&gt;：能把复杂系统讲给非技术高管听。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Radical ownership&lt;/strong&gt;：端到端拥有一个问题，包括&quot;不是你的错&quot;的部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comfort with ambiguity&lt;/strong&gt;：面对模糊的大目标，先澄清再拆解，而不是第一分钟就跳到技术方案。上篇强调的是&quot;对模型行为有品味&quot;，这里对应的是&quot;对客户问题有结构&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;产品 sense&lt;/strong&gt;：跨客户看到共性模式，反哺产品团队。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高压沟通&lt;/strong&gt;：客户 VP 在周五下午发火时保持冷静。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.4 与 Harness 岗的技能树关系&lt;/h2&gt;
&lt;p&gt;上篇把 Harness 岗的技能树分成 L1~L7 七层。FDE 不需要 L1~L7 全部达到&quot;系统研发&quot;深度，但 &lt;strong&gt;L2~L6（Agent 机制、上下文工程、工具协议、多智能体、评测体系）都要达到&quot;客户现场能用&quot;的标准&lt;/strong&gt;，并且新增三个 Harness 岗不强调的领域：&lt;strong&gt;数据工程&lt;/strong&gt;（Agent 的底座不是 prompt 而是 data）、&lt;strong&gt;企业环境&lt;/strong&gt;（SSO、权限、合规、私有化部署）、&lt;strong&gt;交付与推动&lt;/strong&gt;（培训客户、推动采纳、沉淀 playbook）。一句话：&lt;strong&gt;Harness 岗学&quot;造系统&quot;，FDE 岗学&quot;用系统替客户解决问题&quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;3. bilibili 学习资源&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;th&gt;资源 / UP主&lt;/th&gt;
&lt;th&gt;BV 号 / 链接&lt;/th&gt;
&lt;th&gt;解决什么问题&lt;/th&gt;
&lt;th&gt;适合阶段&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;码士集团《2026 年 AI 前沿部署工程师（FDE）从入门到 Offer》&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV1r38G6EECB&lt;/code&gt;（27 集）&lt;/td&gt;
&lt;td&gt;唯一直接对标 FDE 的中文系统课：知识树、FDE 与程序员/售前/咨询的区别、三阶段工作法、Echo/Delta 双团队模型、两层蒸馏、岗位怎么搜、国内薪资、面试最常问的 5 个行为问题与 5 个技术场景问题；后半程还有 DeepSeek Harness 专题&lt;/td&gt;
&lt;td&gt;全程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;吴恩达《Agentic AI》&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV1aaxyz8ELY&lt;/code&gt;、&lt;code&gt;BV1DfrdByE2H&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;四大设计模式 + evals 评估 + MCP（上篇已详述，此处不重复）&lt;/td&gt;
&lt;td&gt;入门→进阶&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;微软《AI Agents for Beginners》&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV18TZYY8EuJ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统性覆盖概念、工具、记忆、多智能体&lt;/td&gt;
&lt;td&gt;零基础→入门&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★☆&lt;/td&gt;
&lt;td&gt;李宏毅《AI Agent 系统设计》&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV1o3wvzUEDD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;零基础概念直觉&lt;/td&gt;
&lt;td&gt;零基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★☆&lt;/td&gt;
&lt;td&gt;李沐《动手学 AI Agent》&lt;/td&gt;
&lt;td&gt;B 站搜&quot;李沐 Agent&quot;&lt;/td&gt;
&lt;td&gt;手搓框架、论文精读&lt;/td&gt;
&lt;td&gt;进阶&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★☆&lt;/td&gt;
&lt;td&gt;SQL 进阶课程（窗口函数/查询优化）&lt;/td&gt;
&lt;td&gt;B 站搜&quot;SQL 进阶&quot;&lt;/td&gt;
&lt;td&gt;FDE 面试高频：SQL 是横杠必考&lt;/td&gt;
&lt;td&gt;阶段 1~3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★☆☆&lt;/td&gt;
&lt;td&gt;各类 MCP / Claude Code 实战视频&lt;/td&gt;
&lt;td&gt;如上篇提过的 &lt;code&gt;BV1oecTzKELA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;工具协议与真实 Agent 产品上手&lt;/td&gt;
&lt;td&gt;全程穿插&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选课建议与上篇一致：吴恩达负责&quot;体系&quot;，李宏毅与微软二选一负责&quot;直觉&quot;；&lt;strong&gt;码士集团这门 FDE 课建议完整看完&lt;/strong&gt;，它是目前唯一把&quot;FDE 面试真题&quot;讲成专题的中文资源。框架类视频看完必须配合第 5 节的动手项目，只看不写等于没看。&lt;/p&gt;
&lt;h1&gt;4. GitHub 项目精读&lt;/h1&gt;
&lt;p&gt;FDE 的精读取向与 Harness 不同：Harness 读&quot;评测与框架内核&quot;，FDE 读&quot;企业落地全家桶&quot;。&lt;/p&gt;
&lt;h2&gt;4.1 企业落地三件套（选一个通读架构）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;langgenius/dify&lt;/strong&gt;：开源 LLM 应用开发平台，工作流编排、RAG、Agent、可观测一体。读它的&quot;应用 → 工作流 → 工具&quot;分层，理解企业 Agent 产品的标准形态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;infiniflow/ragflow&lt;/strong&gt;：深度文档理解的 RAG 引擎，把文档解析、切块、检索、重排做成产品，是企业知识库问答的参考实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;labring/FastGPT&lt;/strong&gt;：知识库问答 + 工作流编排，是国内私有化部署最常用的底座之一。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4.2 评测与可观测（FDE 的&quot;判断力&quot;）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;explodinggradients/ragas&lt;/strong&gt;：RAG 评测框架（忠实度、答案相关性、上下文相关性），阶段 4 的必用工具。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EleutherAI/lm-evaluation-harness&lt;/strong&gt;：上篇已精读，FDE 用它回答客户最尖锐的问题——&quot;你的 Agent 到底比之前强了多少&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;langfuse/langfuse&lt;/strong&gt;：LLM 可观测性，trace 每个 agent 调用的输入、输出与成本，是现场排查&quot;模型变笨了&quot;的标准答案。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4.3 Agent 与协议（复用上篇）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;langchain-ai/langgraph&lt;/strong&gt;：上篇已精读。FDE 取向记住一点：&lt;strong&gt;checkpointer + human-in-the-loop 是企业场景刚需&lt;/strong&gt;——客户不会接受一个无法审批、无法断点恢复的 Agent。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;huggingface/smolagents&lt;/strong&gt;：上篇已精读，code-first 趋势的参考实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;modelcontextprotocol/servers&lt;/strong&gt;：Anthropic 的 FDE 岗位明确要求交付 MCP server，这是&quot;工具资产化&quot;的协议底座。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4.4 部署与数据&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vllm-project/vllm&lt;/strong&gt;、&lt;strong&gt;ollama/ollama&lt;/strong&gt;：私有化部署入口。客户对数据出域的顾虑，最终都会落到&quot;模型能不能跑在我自己的机器上&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text2SQL 实践项目&lt;/strong&gt;：Databricks 的 FDE 要求明确列出 Text2SQL；找一个开源项目跑通&quot;自然语言 → SQL → 结果校验&quot;闭环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;精读建议&lt;/strong&gt;：Dify 与 RAGFlow 二选一通读架构（企业落地代表），RAGAS 与 Langfuse 必须亲手跑通，其余读文档加跑 demo。不要五个全家桶都读源码。&lt;/p&gt;
&lt;h1&gt;5. 训练路线&lt;/h1&gt;
&lt;p&gt;从零到对标岗位，按 6 个阶段推进，每阶段给出目标、动手项目、可复现实验与验收标准。总周期建议 3~6 个月，有后端或数据经验可压缩。&lt;/p&gt;
&lt;h2&gt;阶段 0：成为 AI 重度用户 + 补 LLM 基础（1~2 周）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：建立对模型与 Agent 产品行为的品味。与上篇阶段 0 相同，但增加&quot;客户视角&quot;：观察这些产品在真实任务里的失败模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手&lt;/strong&gt;：把 Claude Code / Cursor / Dify 接入日常；用 Dify 拖一个知识库问答应用给同事用，记录被吐槽的点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实验&lt;/strong&gt;：同一份文档，分别用固定长度、递归、语义三种切块策略搭检索，对比命中质量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：能写出 200 字的&quot;Agent 行为观察笔记&quot;，能解释 RAG 为什么&quot;会检索但不可靠&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阶段 1：手搓最小 RAG + ReAct Agent（2~3 周）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：不依赖框架，理解 RAG 与 Agent Loop 的底层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：用 Python 写一个 RAG 管道（加载 → 切块 → 嵌入 → 检索 → 重排 → 生成），再给上篇阶段 1 的 ReAct 循环模板叠加一个检索工具。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可复现实验&lt;/strong&gt;：① 不同切块策略在同一问题集上的检索命中率；② 加入重排前后的答案质量对比。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：核心代码少于 300 行且可读；能画出 RAG 数据流图。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阶段 2：企业级 Agent 落地（3~4 周）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：从&quot;能跑&quot;到&quot;企业敢用&quot;——状态、权限、审批、可观测。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：用 LangGraph 把阶段 1 的 Agent 重写，加入 checkpointer、human-in-the-loop 审批节点与 Langfuse 观测，并用 MCP 暴露一个自定义工具。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可复现实验&lt;/strong&gt;：对比&quot;纯自动&quot;与&quot;关键步骤人工审批&quot;的准确率与成本；统计一次长任务的 token 消耗与失败点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：能画出 Agent 状态图，能说清&quot;客户现场部署的信任边界&quot;——哪些动作必须人工、哪些可以自动。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阶段 3：数据工程与系统集成（3~4 周）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：补上 FDE 区别于普通 Agent 开发者的数据硬功。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：把 CSV、数据库、API 三种不同格式的数据源清洗、统一 schema、灌入向量库供阶段 2 的 Agent 使用；SQL 侧完成 10 道窗口函数与 CTE 练习题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可复现实验&lt;/strong&gt;：对同一查询对比&quot;全表扫描&quot;与&quot;加索引/分区&quot;的耗时，写一份调优笔记。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：能设计&quot;12 个碎片数据源 → 统一 → Agent 可用&quot;的管道方案（面试高频题）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阶段 4：评测闭环 + 客户案例模拟（4~6 周，核心）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：用数据证明 Agent 有效，并练好最难的 decomposition 案例面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：① 用 RAGAS 给阶段 1~3 的 Agent 做评测，输出 error analysis 报告；② 每周 2 次 60 分钟案例演练，题目取&quot;城市 911 调度优化&quot;&quot;物流公司 SAP 加天气数据自动改派&quot;&quot;银行三套并购系统统一反欺诈&quot;这类开放性问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可复现实验&lt;/strong&gt;：对同一 Agent 分别做 outcome 打分与轨迹打分，验证&quot;最终对错&quot;如何掩盖&quot;差一步就对&quot;（与上篇阶段 4 同一方法论）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：一份含失败模式分类的 eval 报告；一次完整的五步案例演练（框架见 6.2）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阶段 5：真实项目 + 开源 + 作品（长期）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：把能力沉淀为可展示的作品。FDE 简历最看重的就是&quot;端到端做过、并直接面对过用户&quot;的经历。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：做一个端到端的企业知识库 Agent（RAG + 审批 + 评测 + trace），找一家真实机构试用；开源并写好 README 与评测结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复现实验&lt;/strong&gt;：提交 PR 给 Dify / RAGFlow / LangGraph / RAGAS 社区，修文档、加测试都算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：1~2 个能放进简历的项目，加若干社区 PR。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;6. 对标招聘要求&lt;/h1&gt;
&lt;h2&gt;6.1 能力映射表&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JD 要求（综合多家公司）&lt;/th&gt;
&lt;th&gt;对应训练阶段&lt;/th&gt;
&lt;th&gt;面试验证方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;能进客户现场，理解业务与关键人&lt;/td&gt;
&lt;td&gt;阶段 0、5&lt;/td&gt;
&lt;td&gt;讲一个你直接面对用户的项目：用户是谁、问题是什么、怎么量化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生产级全栈代码（Python/TS/Go + SQL）&lt;/td&gt;
&lt;td&gt;阶段 1~3&lt;/td&gt;
&lt;td&gt;编码面：限流器、脏 CSV 解析、小 RAG 管道；SQL 窗口函数题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 应用经验：RAG、Agent、MCP、Text2SQL、微调取舍&lt;/td&gt;
&lt;td&gt;阶段 1~4&lt;/td&gt;
&lt;td&gt;深挖&quot;为什么用 RAG 不用微调&quot;&quot;切块策略怎么定&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;企业环境：SSO、权限、合规、私有化部署&lt;/td&gt;
&lt;td&gt;阶段 2~3&lt;/td&gt;
&lt;td&gt;系统设计面：受监管客户的 VPC 私有化 RAG 怎么设计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;评测与可观测&lt;/td&gt;
&lt;td&gt;阶段 4&lt;/td&gt;
&lt;td&gt;&quot;你怎么知道你的 AI 系统真的在工作？&quot;——OpenAI 面试的标志性追问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模糊问题拆解与推动采纳&lt;/td&gt;
&lt;td&gt;阶段 4~5&lt;/td&gt;
&lt;td&gt;decomposition 案例面 + 客户模拟轮&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;把现场经验沉淀为产品能力&lt;/td&gt;
&lt;td&gt;阶段 5&lt;/td&gt;
&lt;td&gt;讲你从客户模式里抽出的 playbook 或组件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;6.2 面试流程拆解&lt;/h2&gt;
&lt;p&gt;FDE 面试通常 5~8 轮、历时 3~6 周，最独特的是 &lt;strong&gt;decomposition（拆解）轮&lt;/strong&gt;——Palantir 发明，多数公司已跟进：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Recruiter screen（30 分钟）&lt;/strong&gt;：&quot;为什么 FDE 而不是普通 SWE&quot;是第一题，答案必须连接个人经历，只回答&quot;consulting 但更技术&quot;会被追问到死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hiring manager（45~60 分钟）&lt;/strong&gt;：深挖简历项目，警惕&quot;我们做了&quot;而不是&quot;我做了&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coding（60 分钟）&lt;/strong&gt;：实用工程题而非 LeetCode hard——限流器（Anthropic 最爱）、脏 CSV 解析、小 RAG 管道（AI 实验室最爱）。&lt;strong&gt;边写边讲，沉默等于卡住。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System design（60 分钟）&lt;/strong&gt;：真实部署题，如&quot;为 HIPAA 约束的医疗客户设计 5000 万文档的 VPC 私有 RAG&quot;。覆盖数据流、信任边界、认证、可观测、失败模式与回滚。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decomposition / open-ended（45~60 分钟，最难）&lt;/strong&gt;：给一个模糊大问题，没有标准答案。&lt;strong&gt;五步框架&lt;/strong&gt;：澄清目标 → 明确干系人与成功指标 → 盘点输入（数据在哪、谁拥有、多新鲜）→ 拆成可解子问题并按风险排序 → 先做最薄的 walking skeleton 再迭代。最常见的淘汰原因就是&quot;没澄清就跳方案&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client simulation（45 分钟）&lt;/strong&gt;：面试官扮演客户——&quot;部署延期三周，客户 CTO 在线上，告诉他&quot;。要点是 ownership 语言、先诊断再给方案、给选项讲清取舍、不承诺做不到的事。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Behavioral（45 分钟）&lt;/strong&gt;：STAR 框架，准备 6~8 个 60~90 秒的故事：端到端 ownership、难搞的干系人、技术决策反转、无授权跨团队推动、失败与复盘、跨客户模式发现、对客户说不。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;部分 AI 实验室还有 take-home（OpenAI 约 5 小时：用官方 API 做一个 RAG / Agent / 评测 harness 并答辩）。&lt;/p&gt;
&lt;h2&gt;6.3 简历项目建议（按含金量排序）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;一个&lt;strong&gt;真实用户在用&lt;/strong&gt;的 Agent 产品，带评测、带 trace、带用户反馈记录——比任何 Demo 都有说服力。&lt;/li&gt;
&lt;li&gt;一份 &lt;strong&gt;eval + error analysis 报告&lt;/strong&gt;，复用上篇阶段 4 的产出，叠加客户指标。&lt;/li&gt;
&lt;li&gt;一个&lt;strong&gt;企业级落地组件&lt;/strong&gt;：MCP server、私有化部署方案、权限审批流。&lt;/li&gt;
&lt;li&gt;开源社区 PR，证明能跟社区协作。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;FDE 的本质是：&lt;strong&gt;把模型能力变成客户生产力的人&lt;/strong&gt;。它站在客户现场，也站在产品前沿；理解业务，也理解代码；做原型，也推转化；解决一个客户的问题，也沉淀一个行业的模式。学习路径与上篇一脉相承但终点不同——&lt;strong&gt;Harness 学的是&quot;让 Agent 可靠跑起来的系统&quot;，FDE 学的是&quot;让客户真的用起来的落地&quot;&lt;/strong&gt;：先成为 AI 重度用户（品味），再手搓 RAG 与 Agent（直觉），然后做企业级工程（硬功），用评测闭环建立判断力（数据），最后用客户案例完成说服力（作品）。真正拉开差距的，是你那份&quot;真实用户在用&quot;的项目，和你那套&quot;先澄清、再拆解、给最薄的骨架&quot;的案例方法论。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;岗位与报告&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Palantir 官方博客：Forward Deployed Software Engineer 的定义与 FDSE 说明&lt;/li&gt;
&lt;li&gt;Exponent《Forward Deployed Engineer Interview: The Definitive 2026 Guide (FDE)》（Harvard FAS Mignone Center for Career Success 转载）&lt;/li&gt;
&lt;li&gt;MIT Project NANDA《The GenAI Divide: State of AI in Business 2025》&lt;/li&gt;
&lt;li&gt;The New Stack / Lightcast FDE 职缺统计（2024→2025）&lt;/li&gt;
&lt;li&gt;DeepSeek 社招板全量扫描：&lt;code&gt;app.mokahr.com/social-recruitment/high-flyer/140576&lt;/code&gt;（2026-08-20 实测 36 岗，无独立 FDE 岗）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;bilibili 视频&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;码士集团《2026 年 AI 前沿部署工程师（FDE）从入门到 Offer》：&lt;code&gt;BV1r38G6EECB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;吴恩达《Agentic AI》：&lt;code&gt;BV1aaxyz8ELY&lt;/code&gt;、&lt;code&gt;BV1DfrdByE2H&lt;/code&gt;（上篇已详述）&lt;/li&gt;
&lt;li&gt;微软《AI Agents for Beginners》：&lt;code&gt;BV18TZYY8EuJ&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;李宏毅《AI Agent 系统设计》：&lt;code&gt;BV1o3wvzUEDD&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;GitHub 项目&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;落地：&lt;code&gt;langgenius/dify&lt;/code&gt;、&lt;code&gt;infiniflow/ragflow&lt;/code&gt;、&lt;code&gt;labring/FastGPT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;评测与可观测：&lt;code&gt;explodinggradients/ragas&lt;/code&gt;、&lt;code&gt;EleutherAI/lm-evaluation-harness&lt;/code&gt;、&lt;code&gt;langfuse/langfuse&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Agent 与协议：&lt;code&gt;langchain-ai/langgraph&lt;/code&gt;、&lt;code&gt;huggingface/smolagents&lt;/code&gt;、&lt;code&gt;modelcontextprotocol/servers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;部署：&lt;code&gt;vllm-project/vllm&lt;/code&gt;、&lt;code&gt;ollama/ollama&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;相关文章&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://chaggle.github.io/2026/08/13/ai/agent-harness-learning-guide/&quot;&gt;Agent Harness 岗的核心学习方法&lt;/a&gt;（上篇，2026-08-13 发布）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tenten.co/learning/the-2026-guide-to-forward-deployed-engineering/&quot;&gt;Forward Deployed Engineer 完全攻略：2026 年 AI 落地時代最熱職位的 35 萬美元起跳地圖&lt;/a&gt;（tenten.co，2026-06）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.cnnetsun.cn/news/3492211.html&quot;&gt;Codex+DeepSeek FDE 落地案例分享：从模型内卷到落地为王&lt;/a&gt;（cnnetsun，2026-08）&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>博客写作与维护规范（项目常规操作）</title><link>https://chaggle.github.io/posts/2026/08/14/blog-writing-standards/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/14/blog-writing-standards/</guid><pubDate>Fri, 14 Aug 2026 15:59:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;2026-08-13 发布的两篇求职分析文章在复盘时暴露了三类问题：&lt;strong&gt;时间先后不一致、内容重复、语句不通顺&lt;/strong&gt;。修订完成后，我把这类问题固化为本博客项目的常规操作要求，作为每篇文章发布前的自检清单。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;涉及文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://chaggle.github.io/2026/08/13/ai/agent-infra-job-analysis/&quot;&gt;DeepSeek「Agent Infra 研发工程师」岗位分析与自我匹配评估&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chaggle.github.io/2026/08/13/ai/deepseek-rd-jobs-scan/&quot;&gt;DeepSeek 研发岗位全量扫描与自我匹配评估&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;规范一：时间与引用一致&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;同一批文章必须明确先后&lt;/strong&gt;：发布时间戳按写作顺序递增，&quot;上一篇/下一篇&quot;的引用必须与实际时间顺序一致。同一时刻发布多篇是禁止的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不得引用不存在的文章&lt;/strong&gt;：只在聊天或草稿里存在的内容，不能写成&quot;上一篇博客的结论&quot;；改为&quot;此前评估的结论&quot;并注明未单独成文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨篇引用优先用固定链接&lt;/strong&gt;：引用具体文章时使用 permalink；只有顺序已确定时，才允许使用&quot;上篇/下篇&quot;。&lt;/li&gt;
&lt;li&gt;文章修订后必须更新 front matter 的 &lt;code&gt;lastMod&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;规范二：内容去重&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;总表与详解分工&lt;/strong&gt;：总表只给结论和一句话判断，细节全部放详解；详解不复述总表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨篇去重&lt;/strong&gt;：同一个事实（如&quot;opencode 会话库 376MB&quot;）全站只在一篇详述，其余文章用交叉引用代替复述。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用计划与岗位专属计划分层&lt;/strong&gt;：岗位篇只写该岗位的动作；汇总篇写全局计划，并注明与岗位篇的覆盖关系，避免两处各写一份。&lt;/li&gt;
&lt;li&gt;被引用的章节名保持准确（如&quot;独特的视角优势&quot;），引用时不得张冠李戴。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;规范三：语句通顺&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;禁止电报式短语（如&quot;0 → 1 有&quot;），每个条目写成完整句子。&lt;/li&gt;
&lt;li&gt;引文内不夹带批注符号（如 ✅/❌ 不得出现在引号内的 JD 原文中），批注放在引文之外。&lt;/li&gt;
&lt;li&gt;名词不直接当动词、口语不与书面语混用（如&quot;打卡是证据&quot;应写作&quot;打卡记录是证据&quot;）。&lt;/li&gt;
&lt;li&gt;数据表述清晰（如&quot;已 4 年+&quot;应写作&quot;已有 4 年多经验&quot;）。&lt;/li&gt;
&lt;li&gt;发布前必须通读一遍全文，逐条对照上述清单。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;规范四：发布流程（附）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;统一使用 &lt;code&gt;npm run publish&lt;/code&gt;（等价于 &lt;code&gt;hexo generate &amp;amp;&amp;amp; hexo deploy --generate=false&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;禁止在 &lt;code&gt;hexo clean&lt;/code&gt; 之后直接 &lt;code&gt;hexo deploy&lt;/code&gt;——曾因此导致首页与归档页漏生成、线上 404。若必须 clean，则 clean 后先单独 &lt;code&gt;generate&lt;/code&gt; 并核对产物，再 &lt;code&gt;deploy --generate=false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;发布后等待 GitHub Pages 构建完成（约 1~2 分钟），并抽查首页、归档页与新增文章页是否 200。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;规范五：分类使用&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;summary&lt;/code&gt; 分类只放个人总结与个人思考，不放技术文档、岗位分析与规范类内容。&lt;/li&gt;
&lt;li&gt;与 AI 相关的内容统一放 &lt;code&gt;AI&lt;/code&gt; 分类（2026-08-14 起执行）。&lt;/li&gt;
&lt;li&gt;新增分类前，先确认没有可复用的既有分类。&lt;/li&gt;
&lt;li&gt;调整分类不影响文章 permalink，但调整后需重新生成并核对分类页与归档页。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;规范六：文章归档默认值&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;默认不归档&lt;/strong&gt;：新文章 front matter &lt;strong&gt;不设置&lt;/strong&gt; &lt;code&gt;archive: true&lt;/code&gt;，除非明确声明需要归档。归档字段的作用是让文章从首页时间线隐藏（仍可通过归档页、分类与标签访问），不是&quot;已发布/未发布&quot;的标记。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存量归档不动&lt;/strong&gt;：2026-08-15 之前已带 &lt;code&gt;archive: true&lt;/code&gt; 的文章保持原样，不因本规范回改。&lt;/li&gt;
&lt;li&gt;归档与否不影响文章 permalink，但调整后需重新生成并核对首页与归档页。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;本次修订记录（2026-08-14）&lt;/h2&gt;
&lt;p&gt;对上述两篇文章应用本规范后的具体修改：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时间与引用一致性&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一篇发布时间改为 2026-08-13 21:30，第二篇保持 23:59，两篇的&quot;下一篇/上一篇&quot;引用与实际顺序一致。&lt;/li&gt;
&lt;li&gt;第一篇引言中&quot;上一篇对「Agent Harness 研发/工程方向」的分析结论&quot;改为&quot;此前做过一轮评估&quot;——该结论未单独成文，不再虚指为一篇博客。&lt;/li&gt;
&lt;li&gt;第二篇中&quot;上篇已分析&quot;的 Agent Harness 表述改为&quot;此前评估的结论，未单独成文&quot;。&lt;/li&gt;
&lt;li&gt;两篇 &lt;code&gt;lastMod&lt;/code&gt; 更新为 2026-08-14。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;内容去重&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第二篇删除&quot;结论先行&quot;里与总排序表重复的三个百分比描述，只保留方向提示。&lt;/li&gt;
&lt;li&gt;第二篇删除&quot;第二梯队简评&quot;中 Agent Infra 条目（与总排序表及上篇重复），改为&quot;上篇已逐条分析，此处不重复&quot;。&lt;/li&gt;
&lt;li&gt;opencode 376MB 的事实只在第一篇详述，第二篇两处引用改为交叉引用。&lt;/li&gt;
&lt;li&gt;补课优先级中的可观测性与 Rust 两条与第一篇&quot;行动计划&quot;合并，第二篇只保留新增的 Python 数据栈条目并注明覆盖关系。&lt;/li&gt;
&lt;li&gt;第一篇行动计划第 5 条补充指向第二篇汇总的引导句。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;语句通顺&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加分项八条由电报式短语改为完整句（如&quot;0 → 1 有&quot;→&quot;0 → 1 的完整经历有&quot;）。&lt;/li&gt;
&lt;li&gt;JD 引文内的 ✅/❌ 批注移出引号（&quot;Redis/Kafka ✅&quot;→&quot;对应 Redis/Kafka ✅&quot;）。&lt;/li&gt;
&lt;li&gt;&quot;91 天 LeetCode 打卡是现成的证据&quot;→&quot;打卡记录是现成的证据&quot;。&lt;/li&gt;
&lt;li&gt;&quot;到 2026 年已 4 年+（Go/Java）&quot;→&quot;已有 4 年多后端经验（Go/Java）&quot;。&lt;/li&gt;
&lt;li&gt;&quot;经历&apos;极端的运维抗压工作&apos;&quot;→&quot;有过长期高强度驻场运维的经历&quot;。&lt;/li&gt;
&lt;li&gt;&quot;投递时必须注明&apos;后端专项、主场 Go&apos;&quot;→&quot;&apos;后端专项（主场：Go）&apos;&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;分类调整&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新建 &lt;code&gt;AI&lt;/code&gt; 分类；两篇岗位分析文章、本文与「AI Agent 知识概念总结」从 &lt;code&gt;summary&lt;/code&gt; 迁至 &lt;code&gt;AI&lt;/code&gt;；置顶的「未来学习计划」由未分类归入 &lt;code&gt;AI&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;依据见规范五：&lt;code&gt;summary&lt;/code&gt; 只保留个人总结与个人思考。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;本次修订记录（2026-08-15）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;新增规范六：文章归档默认值&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;规则来源：WSL2 恢复与 Docker 落地文章修订时，明确&quot;默认不归档、除非显式声明&quot;。&lt;/li&gt;
&lt;li&gt;新增&quot;规范六：文章归档默认值&quot;：新文章默认不设置 &lt;code&gt;archive: true&lt;/code&gt;，存量已归档文章不回改。&lt;/li&gt;
&lt;li&gt;本规范文档自身带 &lt;code&gt;archive: true&lt;/code&gt;，属于存量归档文章，按规范六第 2 条保持原样。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
本规范本身同样适用以上所有条款；后续每篇文章发布前按规范一至四自检。
:::&lt;/p&gt;
</content:encoded></item><item><title>PowerShell 实战踩坑大全：从 GBK 乱码到引号地狱的十八个真实案例</title><link>https://chaggle.github.io/posts/2026/08/14/powershell-pitfalls/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/14/powershell-pitfalls/</guid><pubDate>Fri, 14 Aug 2026 15:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是《&lt;a href=&quot;/2026/08/14/windows/wsl2-restore-on-stripped-windows/&quot;&gt;在精简版 Windows 上恢复 WSL2 与 Docker：一次从检测到落地的全过程实录&lt;/a&gt;》的姊妹篇。那次任务在 PowerShell 上踩的坑，单独值得写一篇。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;文章按主题分五组：&lt;strong&gt;① 语法与解析 → ② 字符串、转义与输出 → ③ 外部命令与进程调用 → ④ Windows 组件与 DISM → ⑤ 数据与迭代&lt;/strong&gt;；每一节都标注了对应的检查规则编号（R1–R17），文末是沉淀成的检查插件与完整映射表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第一部分 · 语法与解析（语言本身的坑）&lt;/h2&gt;
&lt;h2&gt;1-1、&lt;code&gt;$var:&lt;/code&gt; 被解析成&quot;驱动器限定变量&quot;（R2）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&quot;attempt $attempt: downloading...&quot;   # 报错: &apos;:&apos; was not followed by a valid variable name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PowerShell 把 &lt;code&gt;$attempt:&lt;/code&gt; 当成 &lt;code&gt;$env:Path&lt;/code&gt; 那种带作用域/驱动器的语法。解法：&lt;strong&gt;用 ${} 显式界定&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;attempt ${attempt}: downloading...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1-2、PowerShell 5.1 没有三元运算符（R3）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$v = ($a -gt 0) ? &quot;yes&quot; : &quot;no&quot;   # PS 5.1 直接解析错误！
$v = if ($a -gt 0) { &quot;yes&quot; } else { &quot;no&quot; }  # 正确
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1-3、PowerShell 5.1 没有 &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; / &lt;code&gt;||&lt;/code&gt; 链式运算符（R11）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git commit &amp;amp;&amp;amp; git push   # PS 5.1 直接解析报错；这是 PS 7+ 才有的语法
git commit; git push     # 用分号链式执行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;/&lt;code&gt;||&lt;/code&gt; 是 PowerShell 7 才引入的；5.1 里链式执行用分号，或按退出码判断：&lt;code&gt;if ($LASTEXITCODE -eq 0) { git push }&lt;/code&gt;。注意 &lt;code&gt;cmd /c &quot;a &amp;amp;&amp;amp; b&quot;&lt;/code&gt; 这类 cmd/bash 内部字符串不受影响。&lt;/p&gt;
&lt;h2&gt;1-4、if/while 条件里用 &lt;code&gt;=&lt;/code&gt; 当比较（R14）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;if ($x = 5) { ... }   # 赋值！条件恒真，静默逻辑 bug
if ($x -eq 5) { ... } # 正确：PowerShell 比较运算符是 -eq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 常把 C/JS 习惯的 &lt;code&gt;==&lt;/code&gt; 简化成 &lt;code&gt;=&lt;/code&gt; 写进条件；PowerShell 里 &lt;code&gt;=&lt;/code&gt; 是赋值，条件恒真且修起来很难找。&lt;/p&gt;
&lt;h2&gt;1-5、PowerShell 7+ 专属语法混入 5.1（R15）&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;-AsHashtable&lt;/code&gt;、&lt;code&gt;ForEach-Object -Parallel&lt;/code&gt;、&lt;code&gt;-AsByteStream&lt;/code&gt;、&lt;code&gt;??&lt;/code&gt;（空合并）、&lt;code&gt;?.&lt;/code&gt;（可选链）都是 PowerShell 7+ 才有；目标机若是 5.1（Windows 自带版本）直接解析报错。AI 默认按 7 写，先确认运行环境或用 5.1 等价写法（如 &lt;code&gt;-Encoding Byte&lt;/code&gt;）。&lt;/p&gt;
&lt;h2&gt;第二部分 · 字符串、转义与输出（文本层）&lt;/h2&gt;
&lt;h2&gt;2-1、双引号里的 &lt;code&gt;$WINDOWS&lt;/code&gt; 被当成变量展开（R4）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Get-Content &quot;C:\$WINDOWS.~BT\Sources\Panther\setupact.log&quot;  # 路径变成 C:\.~BT\...，$WINDOWS 是未定义变量！
Get-Content &apos;C:\$WINDOWS.~BT\Sources\Panther\setupact.log&apos;  # 单引号才对
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个坑导致排查时一度以为日志文件&quot;神秘消失&quot;，浪费了不少时间。&lt;/p&gt;
&lt;h2&gt;2-2、用 JS 模板字符串生成 PowerShell 脚本时的转义地狱（写作侧规则）&lt;/h2&gt;
&lt;p&gt;在 JS 里写模板字符串生成 .ps1 时，&lt;code&gt;$($var)&lt;/code&gt;、&lt;code&gt;${attempt}&lt;/code&gt; 都会被 JS 抢先插值。规则：&lt;strong&gt;所有想保留到 PS 里的 &lt;code&gt;$&lt;/code&gt; 都要写成 &lt;code&gt;\$&lt;/code&gt;&lt;/strong&gt;，否则报 &lt;code&gt;attempt is not defined&lt;/code&gt; 这类 JS 错误。&lt;/p&gt;
&lt;h2&gt;2-3、中文控制台 GBK 乱码（R1）&lt;/h2&gt;
&lt;p&gt;wsl/dism 输出的中文全是乱码（形如 &lt;code&gt;g?R?l g?S?e?T?^/T?R?&lt;/code&gt;）。原因是控制台代码页是 GBK 而程序输出 UTF-8。每次开头加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chcp 65001 | Out-Null
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2-4、Write-Host 输出不进管道（R17）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Write-Host &quot;done&quot;    # 只写控制台，下游 | 捕获不到
Write-Output &quot;done&quot;  # 结果进管道，可被捕获/拼接
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 喜欢用 Write-Host「打印」结果，但它的输出不进成功流；需要返回/捕获结果用 Write-Output 或直接表达式，Write-Host 只适合进度提示。&lt;/p&gt;
&lt;h2&gt;第三部分 · 外部命令与进程调用（调用层）&lt;/h2&gt;
&lt;h2&gt;3-1、Start-Process -ArgumentList 的引号与沙箱坑（R5）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-ArgumentList&lt;/code&gt; 拼接时&lt;strong&gt;不会自动加引号&lt;/strong&gt;，路径含空格会碎参数。&lt;/li&gt;
&lt;li&gt;后台任务里 &lt;code&gt;Start-Process curl&lt;/code&gt; 下载的文件&lt;strong&gt;内容损坏&lt;/strong&gt;（大小正确、字节全错），直接 &lt;code&gt;&amp;amp; curl.exe&lt;/code&gt; 则正常。能直接调用就别包一层。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3-2、Windows 上 &lt;code&gt;npm&lt;/code&gt; 被 ExecutionPolicy 拦截（R10）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm run build   # 报错: File C:\nvm4w\nodejs\npm.ps1 cannot be loaded because running scripts is disabled on this system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 装 Node 后 PATH 里同时存在 &lt;code&gt;npm.ps1&lt;/code&gt; 与 &lt;code&gt;npm.cmd&lt;/code&gt;，PowerShell 优先解析到 &lt;code&gt;.ps1&lt;/code&gt;，而默认执行策略禁止运行脚本。解法：&lt;strong&gt;显式调用 &lt;code&gt;npm.cmd&lt;/code&gt;&lt;/strong&gt;（&lt;code&gt;npx.cmd&lt;/code&gt;、&lt;code&gt;pnpm.cmd&lt;/code&gt; 同理）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm.cmd run build
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3-3、cmd 风格命令与 &lt;code&gt;%VAR%&lt;/code&gt; 环境变量混入（R16）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;del /f /q file.txt   # cmd 风格：/f /q 会被当成参数，语义与 Remove-Item 不同
%PATH%               # 在 PowerShell 里不会展开！要用 $env:PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 在 Windows 上经常混用 cmd 语法；裸 &lt;code&gt;del/copy/move/mkdir&lt;/code&gt; 虽能命中别名，但参数语义不同，&lt;code&gt;%VAR%&lt;/code&gt; 则完全不会展开。&lt;/p&gt;
&lt;h2&gt;3-4、微软 CDN 下载加速（R9，正向收获）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单连接 1.5MB/s → 实测 Range 请求单连接 15~20MB/s、突发 71MB/s。&lt;/li&gt;
&lt;li&gt;做法：HEAD 拿 &lt;code&gt;Content-Length&lt;/code&gt; → 切成 N 段 → 并行 &lt;code&gt;curl -r start-end&lt;/code&gt; → &lt;code&gt;cmd /c copy /b a+b+c out&lt;/code&gt; 合并 → 校验总大小。&lt;/li&gt;
&lt;li&gt;注意 &lt;code&gt;curl -L&lt;/code&gt; 与 &lt;code&gt;-C -&lt;/code&gt; 组合在部分 CDN 上会拖慢速度，极简参数往往更快。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第四部分 · Windows 组件与 DISM（系统层）&lt;/h2&gt;
&lt;h2&gt;4-1、Get-WindowsOptionalFeature 的 FeatureName 不支持数组（R6）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Get-WindowsOptionalFeature -Online -FeatureName A, B   # 报错 Cannot convert &apos;System.Object[]&apos;
foreach ($n in &quot;A&quot;,&quot;B&quot;) { Get-WindowsOptionalFeature -Online -FeatureName $n }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4-2、DISM 动词：/Dismount-Image 不存在（R7）&lt;/h2&gt;
&lt;p&gt;正确动词是 &lt;strong&gt;&lt;code&gt;/Unmount-Image&lt;/code&gt;&lt;/strong&gt;（&lt;code&gt;/Dismount-Image&lt;/code&gt; 报 &lt;code&gt;错误: 87&lt;/code&gt;），导致 wim 一直挂着占空间。&lt;/p&gt;
&lt;h2&gt;4-3、RestoreHealth 的源版本必须 ≤ 当前系统（R8）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;dism /Online /Cleanup-Image /RestoreHealth /Source:WIM:install.wim:1
# 错误: 0x800f0915 找不到修复内容 —— 因为 wim 是 8037，系统是 7705，源比系统新
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修复源要求&lt;strong&gt;同版本或更旧&lt;/strong&gt;（或走 Windows Update）。&lt;/p&gt;
&lt;h2&gt;第五部分 · 数据与迭代（逻辑层）&lt;/h2&gt;
&lt;h2&gt;5-1、ConvertTo-Json 未指定 -Depth：JSON 被静默截断（R12）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$data | ConvertTo-Json                # 嵌套超过 2 层直接截断，结果静默损坏
$data | ConvertTo-Json -Depth 100     # 显式指定深度
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 生成的序列化代码几乎从不带 &lt;code&gt;-Depth&lt;/code&gt;，而默认深度只有 2——深层对象被静默截断成 &lt;code&gt;&quot;...&quot;&lt;/code&gt;，是「AI 输出 JSON 看起来不对」的头号原因。&lt;/p&gt;
&lt;h2&gt;5-2、foreach 循环里误用 &lt;code&gt;$_&lt;/code&gt;（R13）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;foreach ($f in $files) { Write-Output $_ }   # $_ 不是 $f！是外层管道的当前对象或 $null
foreach ($f in $files) { Write-Output $f }   # 正确：用循环变量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$_&lt;/code&gt; 是管道自动变量，只属于 &lt;code&gt;ForEach-Object { }&lt;/code&gt; / &lt;code&gt;Where-Object { }&lt;/code&gt; 管道场景；&lt;code&gt;foreach&lt;/code&gt; 语法循环里它是外层上下文（或空），AI 经常把两种迭代混着写。&lt;/p&gt;
&lt;h2&gt;第六部分 · 沉淀成检查插件：powershell-check&lt;/h2&gt;
&lt;p&gt;这些坑如果只躺在文章里，下次还是会踩。我把它们提取成了一个&lt;strong&gt;检查插件&lt;/strong&gt;：&lt;code&gt;powershell-check&lt;/code&gt; —— 一个静态检查脚本 + 配套使用规范，位于博客仓库 &lt;code&gt;.dsh/skills/powershell-check/&lt;/code&gt; 下。&lt;strong&gt;规则：一旦涉及 PowerShell 的执行（调用 pwsh、生成 .ps1、把命令嵌进脚本模板），先启动插件进行检查，FAIL 修复后再执行。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;该插件已按 DeepSeek Harness 官方插件范式升级为独立仓库：&lt;a href=&quot;https://github.com/chaggle/dsh-powershell-check&quot;&gt;&lt;strong&gt;dsh-powershell-check&lt;/strong&gt;&lt;/a&gt; —— 原生 Cordis 插件，通过官方 &lt;code&gt;tools/pre-execute&lt;/code&gt; 拦截点对每次 pwsh 调用&lt;strong&gt;自动&lt;/strong&gt;检查并拦截（deny reason 即修复指引），同时以 &lt;code&gt;ctx.skills.registerProvider&lt;/code&gt; 内置同名技能，无需每次手动启动；可配置 &lt;code&gt;analyzer: psscriptanalyzer&lt;/code&gt; 叠加官方 PSScriptAnalyzer 深度检查。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 检查命令串
node .dsh/skills/powershell-check/scripts/check-pwsh.mjs -- &apos;&amp;lt;命令原文&amp;gt;&apos;

# 检查 .ps1 脚本文件
Get-Content fix.ps1 -Raw | node .dsh/skills/powershell-check/scripts/check-pwsh.mjs -

# 插件自检（跑内置正反例）
node .dsh/skills/powershell-check/scripts/check-pwsh.mjs --selftest
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;规则总表（R → 章节）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;规则&lt;/th&gt;
&lt;th&gt;级别&lt;/th&gt;
&lt;th&gt;坑位&lt;/th&gt;
&lt;th&gt;章节&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;R1&lt;/td&gt;
&lt;td&gt;建议&lt;/td&gt;
&lt;td&gt;中文控制台 GBK 乱码&lt;/td&gt;
&lt;td&gt;2-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R2&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$var:&lt;/code&gt; 驱动器限定解析&lt;/td&gt;
&lt;td&gt;1-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R3&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;PS 5.1 三元运算符&lt;/td&gt;
&lt;td&gt;1-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R4&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;双引号 &lt;code&gt;$WINDOWS&lt;/code&gt; 展开&lt;/td&gt;
&lt;td&gt;2-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R5&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;Start-Process 包装外部命令&lt;/td&gt;
&lt;td&gt;3-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R6&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;FeatureName 数组&lt;/td&gt;
&lt;td&gt;4-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R7&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;DISM 动词 /Dismount-Image&lt;/td&gt;
&lt;td&gt;4-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R8&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;RestoreHealth 源版本&lt;/td&gt;
&lt;td&gt;4-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R9&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;curl -L 与 -C - 组合&lt;/td&gt;
&lt;td&gt;3-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R10&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;npm 不带 .cmd 后缀&lt;/td&gt;
&lt;td&gt;3-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R11&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; / `&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R12&lt;/td&gt;
&lt;td&gt;建议&lt;/td&gt;
&lt;td&gt;ConvertTo-Json -Depth&lt;/td&gt;
&lt;td&gt;5-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R13&lt;/td&gt;
&lt;td&gt;建议&lt;/td&gt;
&lt;td&gt;foreach 的 &lt;code&gt;$_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R14&lt;/td&gt;
&lt;td&gt;阻断&lt;/td&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt; 当比较&lt;/td&gt;
&lt;td&gt;1-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R15&lt;/td&gt;
&lt;td&gt;建议&lt;/td&gt;
&lt;td&gt;PS 7+ 专属语法&lt;/td&gt;
&lt;td&gt;1-5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R16&lt;/td&gt;
&lt;td&gt;建议&lt;/td&gt;
&lt;td&gt;cmd 风格 / %VAR%&lt;/td&gt;
&lt;td&gt;3-3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R17&lt;/td&gt;
&lt;td&gt;建议&lt;/td&gt;
&lt;td&gt;Write-Host 不进管道&lt;/td&gt;
&lt;td&gt;2-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写作侧&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;JS 模板转义&lt;/td&gt;
&lt;td&gt;2-2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;维护约定（长期生效）：每次 PowerShell 执行踩到新坑，先给检查器加规则（含 selftest 正反例）并跑通 --selftest，再同步补进本文与 SKILL.md，然后走一轮 commit + push + publish 部署，让知识库与工具保持同步。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>在精简版 Windows 上恢复 WSL2 与 Docker：一次从检测到落地的全过程实录</title><link>https://chaggle.github.io/posts/2026/08/14/wsl2-restore-on-stripped-windows/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/14/wsl2-restore-on-stripped-windows/</guid><pubDate>Fri, 14 Aug 2026 15:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;一台 &quot;Ghost 精简版&quot; Windows 11 专业版机器，想用 WSL2 + Docker，结果发现系统镜像把虚拟化组件全部精简掉了。本文完整记录了两个阶段的战斗：第一阶段是检测与原地升级（2026-08-14 深夜），把系统从 26200.7705 完整升级到 26200.9168；第二阶段是重启后的 WSL 升级灾难修复与 Docker Desktop 落地（2026-08-15 凌晨），最终 &lt;code&gt;docker run hello-world&lt;/code&gt; 点亮引擎。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、任务背景&lt;/h2&gt;
&lt;p&gt;目标机器：Intel i5-14600KF / 32GB / NVMe 2TB，Windows 11 专业版（Build 26200.7705）。需求：&lt;strong&gt;确认系统是否具备 WSL2 与容器化能力&lt;/strong&gt;，并完成 Docker 环境搭建。&lt;/p&gt;
&lt;h2&gt;二、检测阶段：看起来正常，实则千疮百孔&lt;/h2&gt;
&lt;p&gt;第一轮检测结果：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;检查项&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU 虚拟化 (VT-x / SLAT)&lt;/td&gt;
&lt;td&gt;✅ BIOS 已开启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WSL 版本（Store 版 2.7.11）&lt;/td&gt;
&lt;td&gt;✅ 已安装&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wsl --status&lt;/code&gt; / &lt;code&gt;wsl -l -v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ 报错 &lt;code&gt;Wsl/0x8007041d&lt;/code&gt;（服务超时），无任何发行版&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LxssManager / vmcompute / vmms 服务&lt;/td&gt;
&lt;td&gt;❌ 全部不存在&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows 功能清单（102 项）&lt;/td&gt;
&lt;td&gt;❌ 无 WSL、VirtualMachinePlatform、Hyper-V、Containers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;根因确认&lt;/strong&gt;：这是一台被深度精简的 Windows 镜像（典型 Ghost 版），组件仓库（WinSxS/CBS）里根本没有虚拟化相关功能包。连恢复环境（reagentc / WinRE）都被删了。硬件没问题，是软件层&quot;缺零件&quot;。&lt;/p&gt;
&lt;h2&gt;三、第一阶段：修复尝试全记录（失败路线图）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这一路踩的坑比预想的多得多，每一步都值得记录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3.1 直接启用功能 → 功能名未知&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux, VirtualMachinePlatform -All
# 错误: 0x800f080c 功能名称 Microsoft-Windows-Subsystem-Linux 未知
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wsl --install&lt;/code&gt; 也是同样的结局（&lt;code&gt;WSL_E_INSTALL_COMPONENT_FAILED&lt;/code&gt;）。&lt;strong&gt;&quot;功能名未知&quot;意味着功能包彻底不在镜像里，不是&quot;未启用&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;3.2 下载官方 ISO：一条 8.5GB 的下载历险&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Fido 脚本（Rufus 作者维护）生成微软官方直链：&lt;code&gt;software.download.prss.microsoft.com&lt;/code&gt;，带 24 小时时效令牌。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单连接被限速 1.5MB/s&lt;/strong&gt; → 实测发现 CDN 支持 Range 请求 → &lt;strong&gt;分段并行下载&lt;/strong&gt;，聚合速度冲到 &lt;strong&gt;74MB/s&lt;/strong&gt;，8.5GB 几分钟搞定。&lt;/li&gt;
&lt;li&gt;中途 &lt;strong&gt;D: 盘（挂载的虚拟盘）神秘消失&lt;/strong&gt;，所有下载写入失败（curl 退出码 23），才意识到 D: 不是物理分区——&lt;strong&gt;下载目标必须选稳定盘&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 从 ISO 补装组件 → 没有载荷&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dism /Online /Enable-Feature /Source:WIM:...&lt;/code&gt; → 依然 &lt;code&gt;0x800f080c&lt;/code&gt;（在线镜像的清单里没有该功能，给源也没用）。&lt;/li&gt;
&lt;li&gt;挂载 wim 提取功能包 → 发现 wim 的 Packages 目录里&lt;strong&gt;只有 .mum/.cat 清单，没有 .cab 载荷&lt;/strong&gt;；而且 WSL 在 25H2 里是&quot;按需功能&quot;(FOD)，&lt;strong&gt;根本不随安装镜像分发&lt;/strong&gt;。手术式补装路线宣告失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 原地升级修复（setup /auto upgrade）→ 七连败&lt;/h3&gt;
&lt;p&gt;微软官方对&quot;精简系统&quot;的正规修复是&lt;strong&gt;原地升级&lt;/strong&gt;（保留文件和应用）。ISO（26200.8037）比当前系统（7705）新，版本匹配没问题，但每次都在不同步骤暴毙：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;次数&lt;/th&gt;
&lt;th&gt;卡住的步骤&lt;/th&gt;
&lt;th&gt;报错&lt;/th&gt;
&lt;th&gt;解决&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v1&lt;/td&gt;
&lt;td&gt;EULA&lt;/td&gt;
&lt;td&gt;&lt;code&gt;User did not accept EULA&lt;/code&gt; (0xC190010E)&lt;/td&gt;
&lt;td&gt;需要应答文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2&lt;/td&gt;
&lt;td&gt;语言选择&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GetInternationalSettingsFromUnattend 0x8007000D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;应答文件补 &lt;code&gt;Microsoft-Windows-International-Core-WinPE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;产品密钥&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ProductKeyValidate 0x80070490&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统是 KMS 卷激活，加 &lt;code&gt;/pkey W269N-...&lt;/code&gt;（Pro GVLK）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v4&lt;/td&gt;
&lt;td&gt;驱动安装&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DriverInstallation 0x80070490&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;补 WinRE（winre.wim + reagentc.exe 从 wim 提取）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v5&lt;/td&gt;
&lt;td&gt;驱动安装&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;td&gt;加 &lt;code&gt;/migratedrivers none&lt;/code&gt;，无效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v6&lt;/td&gt;
&lt;td&gt;驱动安装&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;td&gt;WinRE 已就位，仍无效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v7&lt;/td&gt;
&lt;td&gt;驱动安装&lt;/td&gt;
&lt;td&gt;同上&lt;/td&gt;
&lt;td&gt;补迁移框架 DLL（migcore.dll 等，从 ISO sources 复制）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v8（交互式）&lt;/td&gt;
&lt;td&gt;迁移收集&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Gather data 0x8007042B&lt;/code&gt;，&lt;code&gt;ServiceModelRegMigPlugin.dll does not exist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;补全 migration 插件目录（33 个 DLL）+ acmigration.dll&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;v9（交互式）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅ 全程通过，升级成功&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键洞察&lt;/strong&gt;：升级程序在 downlevel 阶段要从&lt;strong&gt;当前系统&lt;/strong&gt;加载迁移框架（&lt;code&gt;migcore.dll&lt;/code&gt;）、迁移插件（&lt;code&gt;%SYSTEM32%\migration\&lt;/code&gt;）和 WinRE——精简镜像把这类&quot;运行时才需要&quot;的组件也删了。&lt;strong&gt;把这些组件从官方 wim 补齐后，交互式升级可以成功&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;3.5 转机：交互式升级 + 补齐运行时组件&lt;/h3&gt;
&lt;p&gt;期间也制作了 UEFI 启动 U 盘（FAT32 + &lt;code&gt;dism /Split-Image&lt;/code&gt; 拆分 7GB 的 install.wim），但用户否决了 U 盘/双系统路线，于是回到升级修复。&lt;strong&gt;静默模式（/quiet）对每个 DLP 动作失败即中止，换成图形化交互式升级后同一套管线走得更远&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;v8（交互式第 1 次）&lt;/strong&gt;：镜像应用、驱动迁移全部通过，最终卡在&lt;strong&gt;迁移收集&lt;/strong&gt;（&lt;code&gt;Gather data 0x8007042B&lt;/code&gt;，根因是 &lt;code&gt;ServiceModelRegMigPlugin.dll&lt;/code&gt; 等迁移插件缺失）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;v9（交互式第 2 次）&lt;/strong&gt;：从 wim 补全整个 &lt;code&gt;C:\Windows\System32\migration\&lt;/code&gt; 插件目录（33 个 DLL）+ &lt;code&gt;acmigration.dll&lt;/code&gt; 后，&lt;strong&gt;全程通过&lt;/strong&gt;：迁移收集 → 驱动迁移 → 组件分期 → 迁移应用 → 自动重启。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第一阶段成果&lt;/strong&gt;：系统从 26200.7705 升级到 &lt;strong&gt;26200.9168&lt;/strong&gt;（动态更新自动带上最新累积更新），个人文件与应用设置完整迁移，旧系统完整保留在 &lt;code&gt;Windows.old&lt;/code&gt;（确认无误后可删除）。WSL + VirtualMachinePlatform 两个功能随后启用成功，系统提示重启生效。&lt;/p&gt;
&lt;h2&gt;四、第二阶段：重启后的 WSL 升级灾难&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;重启之后，WSL 并没有如预期那样恢复可用，反而抛出了一个更隐蔽的错误。这一阶段的排查对象从&quot;缺失组件&quot;变成了&quot;残留状态&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4.1 表面恢复 ≠ 真正可用&lt;/h3&gt;
&lt;p&gt;重启后功能已生效，但所有 wsl 命令——&lt;code&gt;wsl --version&lt;/code&gt;、&lt;code&gt;wsl --install&lt;/code&gt;、甚至 &lt;code&gt;wsl --shutdown&lt;/code&gt;——一律输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WSL 正在完成升级...
灾难性故障
错误代码: Wsl/CallMsi/Install/E_UNEXPECTED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意两点：其一，错误发生在&quot;&lt;strong&gt;完成升级&lt;/strong&gt;&quot;这个动作上，说明系统认为有一次升级处于未完成状态；其二，Windows Installer 事件日志显示，&lt;strong&gt;每次调用 wsl.exe 都会触发一次 WSL MSI 的&quot;重配置&quot;事务，且状态码为 0（成功）&lt;/strong&gt;——MSI 本身没坏，坏的是触发它之前的判定逻辑。&lt;/p&gt;
&lt;h3&gt;4.2 逐层排查：五个检查点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;功能与组件层&lt;/strong&gt;：DISM 查询确认 &lt;code&gt;Microsoft-Windows-Subsystem-Linux&lt;/code&gt; 与 &lt;code&gt;VirtualMachinePlatform&lt;/code&gt; 均已启用；Appx 包（2.7.11.0）状态 Ok；&lt;code&gt;C:\Program Files\WSL\&lt;/code&gt; 下 wsl.exe、wslservice.exe 等组件齐全且版本一致。&lt;strong&gt;不是缺组件&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MSI 注册层&lt;/strong&gt;：产品注册信息里 &lt;code&gt;DisplayVersion&lt;/code&gt; 为 2.7.11.0、&lt;code&gt;InstallState&lt;/code&gt; 为 5（已安装），但 &lt;strong&gt;&lt;code&gt;InstallLocation&lt;/code&gt; 为空&lt;/strong&gt;——这与 WSL 官方仓库 issue #12623 中 WSL 团队确认的根因完全吻合：&lt;strong&gt;InstallLocation 注册表值缺失，手动重装可解&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手动安装 MSI&lt;/strong&gt;：从 GitHub Releases 下载同版本 MSI（wsl.2.7.11.0.x64.msi）执行 &lt;code&gt;msiexec /i&lt;/code&gt;，&lt;strong&gt;退出码 1603&lt;/strong&gt;。verbose 日志给出两个关键信号：属性表里 &lt;code&gt;MsiSystemRebootPending = 1&lt;/code&gt;（安装器判定系统存在待重启状态），以及自定义操作 &lt;code&gt;ValidateInstall&lt;/code&gt; 返回 3（失败）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追踪待重启状态来源&lt;/strong&gt;：常见的重启标记（CBS RebootPending、WU RebootRequired、RunOnce）都不存在，真正的来源是 &lt;strong&gt;&lt;code&gt;PendingFileRenameOperations&lt;/code&gt;——里面有 70 条 Norton 卸载残留的删除队列&lt;/strong&gt;（这台封装镜像里 Norton 服务早已停用，但删除队列一直没清）。MSI 正是据此判定&quot;系统待重启&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;镜像封装残留&lt;/strong&gt;：&lt;code&gt;WindowsUpdate\Auto Update&lt;/code&gt; 下的 &lt;strong&gt;&lt;code&gt;IsOOBEInProgress = 1&lt;/code&gt;&lt;/strong&gt;（首次开机流程标记）也是封装遗留，同样会干扰系统级安装。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4.3 三个修复动作&lt;/h3&gt;
&lt;p&gt;按&quot;从轻到重&quot;的顺序执行：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;清除 &lt;code&gt;IsOOBEInProgress&lt;/code&gt; 残留标记&lt;/strong&gt;（置 0）→ 重试，无效，错误依旧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;备份并清空 &lt;code&gt;PendingFileRenameOperations&lt;/code&gt;&lt;/strong&gt;（先 &lt;code&gt;reg export&lt;/code&gt; 备份整个 Session Manager 键到 &lt;code&gt;session_manager_backup.reg&lt;/code&gt;，再删除该值）→ &lt;code&gt;MsiSystemRebootPending&lt;/code&gt; 消失，但 &lt;code&gt;ValidateInstall&lt;/code&gt; 仍然返回 3，1603 依旧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键一步：先卸载、再全新安装&lt;/strong&gt;。既然同版本 MSI 进入的是&quot;维护模式/重配置&quot;路径（日志里 &lt;code&gt;Skipping RemoveExistingProducts: current configuration is maintenance mode or an uninstall&lt;/code&gt;），那就 &lt;code&gt;msiexec /x {497CB23D-8747-4047-A079-DD98E0EDFC18}&lt;/code&gt; 彻底卸载产品，随后 &lt;code&gt;msiexec /i&lt;/code&gt; 全新安装 → &lt;strong&gt;退出码 0，安装成功&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;修复效果立竿见影：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wsl --version
WSL 版本: 2.7.11.0
内核版本: 6.18.33.2-2
WSLg 版本: 1.0.73.2
默认版本: 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.4 Docker Desktop 落地&lt;/h3&gt;
&lt;p&gt;WSL 恢复后，Docker 安装一路顺畅：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装&lt;/strong&gt;：&lt;code&gt;winget install -e --id Docker.DockerDesktop&lt;/code&gt;（4.86.0），安装器自动校验哈希，一次成功。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动&lt;/strong&gt;：启动 Docker Desktop 后引擎数分钟内就绪——Server: Docker Desktop 4.86.0，Engine 29.7.2（linux/amd64），containerd v2.2.5。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：&lt;code&gt;docker run --rm hello-world&lt;/code&gt; 成功拉取镜像并运行，输出 &quot;Hello from Docker!&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PATH&lt;/strong&gt;：安装器已将 &lt;code&gt;C:\Program Files\Docker\Docker\resources\bin&lt;/code&gt; 写入系统 PATH。若在旧终端里遇到 &lt;code&gt;docker-credential-desktop: executable file not found&lt;/code&gt;，是会话 PATH 未刷新的表象，新开终端即正常。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;五、经验总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&quot;功能名未知&quot;（0x800f080c）≠ &quot;功能未启用&quot;&lt;/strong&gt;：前者说明镜像里根本没这个功能，任何 /Source 都救不了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度精简镜像（Ghost 版）的升级修复并非无解，但要补齐运行时组件&lt;/strong&gt;：setup 的 downlevel 阶段强依赖当前系统的迁移框架（migcore.dll）、迁移插件（&lt;code&gt;%SYSTEM32%\migration\&lt;/code&gt;）和 WinRE，精简系统删掉的恰恰是这些。报错 &lt;code&gt;0x80070490&lt;/code&gt;（找不到元素）几乎总是&quot;某个被删的运行时组件&quot;的信号，去 &lt;code&gt;migration&lt;/code&gt; 和 &lt;code&gt;Recovery&lt;/code&gt; 目录与官方 wim 对账补齐即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静默升级失败 ≠ 升级无望&lt;/strong&gt;：&lt;code&gt;/quiet&lt;/code&gt; 模式下任意 DLP 动作失败即中止；改用&lt;strong&gt;交互式升级&lt;/strong&gt;（GUI）后同一套管线能走得更远，错误信息也更明确。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件虚拟化支持 ≠ 系统能用 WSL2&lt;/strong&gt;：WSL2 需要 &lt;code&gt;Microsoft-Windows-Subsystem-Linux&lt;/code&gt; + &lt;code&gt;VirtualMachinePlatform&lt;/code&gt; 两个系统功能，缺一个都是 0x8007041d。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;灾难性故障 E_UNEXPECTED&quot; 的排查顺序&lt;/strong&gt;：先看 MSI verbose 日志（&lt;code&gt;msiexec /i ... /l*v&lt;/code&gt;），定位失败的 custom action 与 &lt;code&gt;MsiSystemRebootPending&lt;/code&gt; 属性；再反向追踪待重启标记的来源（&lt;code&gt;PendingFileRenameOperations&lt;/code&gt;、CBS、WU），清掉残留后再重试。&lt;strong&gt;&quot;升级进行中&quot;的判定卡死，往往不是缺文件，而是残留状态没清&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同版本 MSI 修复失败时，先卸载再装&lt;/strong&gt;：维护模式（重配置）走的是另一条执行路径，&lt;code&gt;ValidateInstall&lt;/code&gt; 等自定义操作的行为与全新安装不同；卸载后全新安装常常直接绕开问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装/克隆镜像的残留标记会以奇怪的方式阻塞系统级安装&lt;/strong&gt;：&lt;code&gt;IsOOBEInProgress&lt;/code&gt;、Norton 删除队列这类与&quot;当前任务&quot;无关的残留，会通过待重启判定间接导致 MSI 安装失败（1603）。排查系统级安装失败时，值得把这类&quot;体检项&quot;纳入检查清单。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动化排查的黄金法则&lt;/strong&gt;：每一步都留日志、验证产物、先小样后大样（先下 10MB 验内容再下 8.5GB）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;六、最终结果与数据&lt;/h2&gt;
&lt;h3&gt;6.1 时间线（2026-08-14 → 08-15）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;时间&lt;/th&gt;
&lt;th&gt;事件&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;21:26&lt;/td&gt;
&lt;td&gt;任务开始：检测 WSL2 / 容器化能力&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21:30-21:45&lt;/td&gt;
&lt;td&gt;检测完成，定位精简镜像根因；并行加速下载官方 ISO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22:00-23:20&lt;/td&gt;
&lt;td&gt;DISM 补装三路失败；静默升级 v1-v7 七连败（EULA→语言→密钥→驱动）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23:28&lt;/td&gt;
&lt;td&gt;交互式升级 v8 → 卡迁移收集（缺迁移插件）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;00:10&lt;/td&gt;
&lt;td&gt;补全 35 个迁移组件（33 插件 + acmigration.dll + ServiceModelRegMigPlugin.dll）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;00:24&lt;/td&gt;
&lt;td&gt;交互式升级 v9 启动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;00:55&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;升级完成，自动重启进入新系统（26200.9168）&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01:18&lt;/td&gt;
&lt;td&gt;WSL + VirtualMachinePlatform 功能启用成功（重启生效）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02:20&lt;/td&gt;
&lt;td&gt;第二阶段开始：wsl 全部命令报 &lt;code&gt;Wsl/CallMsi/Install/E_UNEXPECTED&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02:21-02:33&lt;/td&gt;
&lt;td&gt;逐层排查：功能/组件/注册层均正常；确认 &lt;code&gt;InstallLocation&lt;/code&gt; 缺失、&lt;code&gt;MsiSystemRebootPending=1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02:33-02:42&lt;/td&gt;
&lt;td&gt;清除 &lt;code&gt;IsOOBEInProgress&lt;/code&gt; 与 Norton 删除队列残留；MSI 仍 1603&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02:43&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;卸载后全新安装 WSL MSI 2.7.11.0 → 成功，wsl 命令恢复&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02:44-02:53&lt;/td&gt;
&lt;td&gt;winget 安装 Docker Desktop 4.86.0 → 引擎就绪 → &lt;code&gt;hello-world&lt;/code&gt; 验证通过&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;总耗时&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;约 5.5 小时&lt;/strong&gt;（两阶段合计，含 8.5GB 镜像下载与两次安装尝试）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;6.2 会话消耗（AI 助手侧）&lt;/h3&gt;
&lt;p&gt;第一阶段（精简镜像升级）会话消耗：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;数值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;输入 tokens（累计）&lt;/td&gt;
&lt;td&gt;约 15.4 万&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输出 tokens（累计）&lt;/td&gt;
&lt;td&gt;约 25.6 万（其中推理 tokens 13.5 万）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缓存读取 tokens&lt;/td&gt;
&lt;td&gt;约 5471 万&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM 用量记录（工具步骤）&lt;/td&gt;
&lt;td&gt;259 条&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第二阶段（WSL 修复 + Docker 落地）会话消耗未单独统计，工具调用主要集中在：DISM/注册表查询与修改、MSI 安装与日志分析、GitHub API 取包、winget 安装与引擎验证。&lt;/p&gt;
&lt;h3&gt;6.3 解决的问题（16 项）&lt;/h3&gt;
&lt;p&gt;第一阶段（10 项）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定位 WSL2 不可用根因：&lt;code&gt;0x8007041d&lt;/code&gt; → 精简镜像缺失 WSL/VMP 功能组件&lt;/li&gt;
&lt;li&gt;微软 CDN 单连接限速 1.5MB/s → 分段并行下载提速至 74MB/s&lt;/li&gt;
&lt;li&gt;D 盘虚拟盘中途消失导致下载失败 → 换稳定盘并绕过&lt;/li&gt;
&lt;li&gt;澄清&quot;功能名未知&quot;（0x800f080c）≠ &quot;未启用&quot;，DISM /Source 无法补装&lt;/li&gt;
&lt;li&gt;静默升级 EULA 失败 → autounattend.xml 应答文件&lt;/li&gt;
&lt;li&gt;语言选择失败 → 应答文件补 International-Core-WinPE&lt;/li&gt;
&lt;li&gt;产品密钥校验失败 → /pkey 传入 Pro GVLK&lt;/li&gt;
&lt;li&gt;驱动安装 0x80070490 → 部署 WinRE + 迁移框架 DLL&lt;/li&gt;
&lt;li&gt;迁移收集 0x8007042B → 补全 35 个迁移组件 → &lt;strong&gt;升级成功&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;WSL + VirtualMachinePlatform 功能启用成功&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第二阶段（6 项）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Wsl/CallMsi/Install/E_UNEXPECTED&lt;/code&gt;（升级完成判定卡死）→ 定位为 MSI 判定链问题而非组件缺失&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MsiSystemRebootPending=1&lt;/code&gt; 干扰安装 → 备份并清空 &lt;code&gt;PendingFileRenameOperations&lt;/code&gt; 中 70 条 Norton 删除队列残留&lt;/li&gt;
&lt;li&gt;封装镜像残留 &lt;code&gt;IsOOBEInProgress=1&lt;/code&gt; → 清除&lt;/li&gt;
&lt;li&gt;同版本 MSI 维护模式重配置失败（1603）→ 先卸载再全新安装 → &lt;strong&gt;WSL 2.7.11.0 恢复可用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Docker Desktop 安装与启动 → winget 一次成功，引擎 29.7.2 就绪&lt;/li&gt;
&lt;li&gt;端到端验证 → &lt;code&gt;docker run hello-world&lt;/code&gt; 通过&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.4 当前状态与待办&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ 系统：Windows 11 &lt;strong&gt;26200.9168&lt;/strong&gt;（完整镜像，非精简）&lt;/li&gt;
&lt;li&gt;✅ WSL2：2.7.11.0 正常可用（内核 6.18.33.2-2，默认版本 2）&lt;/li&gt;
&lt;li&gt;✅ Docker Desktop：4.86.0 已安装并启动，&lt;code&gt;hello-world&lt;/code&gt; 验证通过，&lt;code&gt;docker&lt;/code&gt; 命令已入系统 PATH&lt;/li&gt;
&lt;li&gt;✅ 遗留清理：&lt;code&gt;session_manager_backup.reg&lt;/code&gt; 保留在 &lt;code&gt;C:\Users\Administrator\&lt;/code&gt;（PendingFileRenameOperations 备份，确认无误后可删）&lt;/li&gt;
&lt;li&gt;📦 &lt;code&gt;C:\iso\Win11_25H2.iso&lt;/code&gt; 按用户要求保留不删；&lt;code&gt;Windows.old&lt;/code&gt; 确认无误后可删除释放空间&lt;/li&gt;
&lt;li&gt;📝 后续可选：&lt;code&gt;wsl --install -d Ubuntu&lt;/code&gt; 安装发行版、配置 Docker 镜像加速&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Agent Harness 岗的核心学习方法</title><link>https://chaggle.github.io/posts/2026/08/14/agent-harness-learning-guide/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/14/agent-harness-learning-guide/</guid><pubDate>Thu, 13 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文以 DeepSeek Harness 团队「Agent Harness 研发/工程方向」为目标岗位，通过对 bilibili 视频课程与 GitHub 开源项目的调研总结，系统阐述如何训练 Agent 开发与 Harness 工程能力。文章给出从基础到进阶的分阶段实操路线、可复现实验与阶段目标，避免泛泛而谈。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#1-%E5%B2%97%E4%BD%8D%E5%AE%9A%E4%BD%8D&quot;&gt;岗位定位：Harness 是什么、为什么重要&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-%E6%A0%B8%E5%BF%83%E6%8A%80%E8%83%BD%E6%A0%91&quot;&gt;核心技能树：从 LLM 基础到 Harness 工程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-bilibili-%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%BA%90&quot;&gt;bilibili 学习资源：入门到进阶的视频路线&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-github-%E9%A1%B9%E7%9B%AE%E7%B2%BE%E8%AF%BB&quot;&gt;GitHub 项目精读：读源码、学架构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-%E8%AE%AD%E7%BB%83%E8%B7%AF%E7%BA%BF&quot;&gt;训练路线：分阶段实操与可复现实验&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-%E5%AF%B9%E6%A0%87%E6%8B%9B%E8%81%98%E8%A6%81%E6%B1%82&quot;&gt;对标招聘要求：能力映射与面试准备&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;1. 岗位定位&lt;/h1&gt;
&lt;h2&gt;1.1 一个公式：Model + Harness = Agent&lt;/h2&gt;
&lt;p&gt;这是目标团队（DeepSeek Harness 团队）写在招聘里的&lt;strong&gt;团队使命&lt;/strong&gt;。理解这个公式，就理解了整份 JD：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Model（模型）&lt;/strong&gt;：经过预训练/后训练的 LLM，提供&quot;智能&quot;本身——推理、知识、代码生成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent（智能体）&lt;/strong&gt;：能自主完成任务的系统，会拆解目标、调用工具、迭代执行、自我纠错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harness（中间那一层）&lt;/strong&gt;：把模型&quot;装&quot;起来，让它变成 Agent 的&lt;strong&gt;全部工程脚手架&lt;/strong&gt;——Agent Loop、工具调用、上下文管理、记忆、多智能体编排、评测、沙箱、可观测性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打个比方：模型是&quot;发动机&quot;，Harness 是&quot;整车底盘 + 变速箱 + 方向盘 + 仪表盘&quot;。没有 Harness，模型只能回答问题；有了 Harness，模型才能&lt;strong&gt;做事&lt;/strong&gt;。这正是为什么这个岗位叫 &quot;Harness&quot; 而不是 &quot;Agent 应用开发&quot;——它关注的是&lt;strong&gt;让 Agent 可靠跑起来的底层系统&lt;/strong&gt;，而不是用框架搭一个聊天机器人。&lt;/p&gt;
&lt;h2&gt;1.2 岗位职责拆解&lt;/h2&gt;
&lt;p&gt;JD 里明确列出了研究/工程/产品三个方向，核心职责可以归纳为四条：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定义并实现 Harness 前沿能力&lt;/strong&gt;：上下文管理、长期记忆、Subagent 与 Multi-Agent、自进化 Agent、超长程任务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型与 Harness 的深度适配&lt;/strong&gt;：与模型训练团队协作，让 Harness 和模型&quot;共同进化&quot;（而不是 Harness 单向适配模型）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建评测体系&lt;/strong&gt;：提出基准测试与评测方法、构建评测数据、制定标注策略，从 Harness 角度持续优化 Agent 智能水平。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;以真实任务为反馈源&lt;/strong&gt;：把用户反馈、真实世界任务变成数据与实验，持续迭代 Agent 在真实场景的表现。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1.3 核心价值：Harness 是 Agent 的&quot;护城河&quot;&lt;/h2&gt;
&lt;p&gt;为什么专门设一个 Harness 团队，而不是让普通后端工程师顺便做？因为&lt;strong&gt;模型能力在快速平价化，而 Harness 工程是差异化竞争点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同样的模型，Harness 的好坏直接决定 Agent 能不能处理&lt;strong&gt;超长程、多步骤、需记忆、需协作&lt;/strong&gt;的真实任务。&lt;/li&gt;
&lt;li&gt;评测（eval）决定了团队能不能&lt;strong&gt;量化&lt;/strong&gt;&quot;Agent 是否真的变强了&quot;，这是模型与 Harness 共同进化的前提。&lt;/li&gt;
&lt;li&gt;上下文管理与记忆机制，直接决定 Agent 在真实工作流里的&quot;续航能力&quot;和&quot;智商天花板&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;Harness 工程师是&quot;模型之上的系统工程师&quot;&lt;/strong&gt;，既要有算法品味（理解模型行为），又要有工程硬功（状态管理、并发、沙箱、可观测性）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;2. 核心技能树&lt;/h1&gt;
&lt;p&gt;对照 JD 的关键词，可以把 Harness 岗的技能树拆成七层，从下到上：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;技能域&lt;/th&gt;
&lt;th&gt;关键知识点（JD 原词）&lt;/th&gt;
&lt;th&gt;掌握标准&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1 基础&lt;/td&gt;
&lt;td&gt;LLM 机制&lt;/td&gt;
&lt;td&gt;LLM API、&lt;strong&gt;KV Cache&lt;/strong&gt;、token、上下文窗口、推理/思考模型&lt;/td&gt;
&lt;td&gt;能解释 KV Cache 为何是长上下文/长任务的性能瓶颈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2 核心&lt;/td&gt;
&lt;td&gt;Agent 机制&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Agent Loop&lt;/strong&gt;、&lt;strong&gt;Tool Use&lt;/strong&gt;、&lt;strong&gt;Reasoning&lt;/strong&gt;、&lt;strong&gt;Planning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;能手写 ReAct Loop，理解 ReAct/CoT/Reflexion/ToT 的差异&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3 上下文&lt;/td&gt;
&lt;td&gt;上下文工程&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;、&lt;strong&gt;Context Engineering&lt;/strong&gt;、&lt;strong&gt;Memory&lt;/strong&gt;、上下文压缩/卸载&lt;/td&gt;
&lt;td&gt;能设计记忆分层 + 上下文窗口管理策略&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L4 工具&lt;/td&gt;
&lt;td&gt;工具与协议&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Skills&lt;/strong&gt;、&lt;strong&gt;MCP&lt;/strong&gt;、工具注册/沙箱/权限&lt;/td&gt;
&lt;td&gt;能写 MCP Server、封装工具、设计工具 schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L5 编排&lt;/td&gt;
&lt;td&gt;多智能体&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Subagent&lt;/strong&gt;、&lt;strong&gt;Multi-Agent&lt;/strong&gt;、编排模式、通信&lt;/td&gt;
&lt;td&gt;能手写 supervisor / handoff / 图编排三种模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L6 评测&lt;/td&gt;
&lt;td&gt;评测体系&lt;/td&gt;
&lt;td&gt;benchmark、eval harness、trajectory scoring、pass@k&lt;/td&gt;
&lt;td&gt;能搭一套可复现评测 + 做 error analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L7 工程&lt;/td&gt;
&lt;td&gt;工程化&lt;/td&gt;
&lt;td&gt;框架选型、状态持久化、checkpoint、可观测性、沙箱、部署&lt;/td&gt;
&lt;td&gt;能做 durable execution、human-in-the-loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;2.1 三个 Engineering 课题（JD 反复强调）&lt;/h2&gt;
&lt;p&gt;JD 里把三个概念并列提出，这是 Harness 岗的&lt;strong&gt;理论主线&lt;/strong&gt;，值得单独拎出来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prompt Engineering（提示词工程）&lt;/strong&gt;：让单次调用输出高质量结果。入门门槛低，但天花板在于&quot;结构化输出、少样本、指令设计&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context Engineering（上下文工程）&lt;/strong&gt;：&lt;strong&gt;把&quot;给模型什么上下文&quot;当成一个系统问题来设计&lt;/strong&gt;——哪些历史要保留、哪些工具结果要压缩、如何分层记忆、如何做上下文卸载（offload）。这是 Harness 岗与普通 Agent 教程的核心分野。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harness Engineering（Harness 工程）&lt;/strong&gt;：把上面所有东西做成&lt;strong&gt;可靠、可评测、可观测、可复现&lt;/strong&gt;的系统——状态图、checkpoint、并发、沙箱、评测闭环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话记忆：&lt;strong&gt;Prompt 管&quot;一次调用&quot;，Context 管&quot;一个会话&quot;，Harness 管&quot;整个系统&quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;2.2 一个隐藏技能：成为 Agent 产品&quot;重度用户&quot;&lt;/h2&gt;
&lt;p&gt;JD 每个方向都要求&quot;深度使用过代码类及通用类 Agent 产品&quot;（Claude Code、Cursor、Codex、Manus、OpenClaw、Hermes 等），并且&quot;把使用融入工作生活&quot;。这不是客套话——&lt;strong&gt;对模型行为有品味、有判断力，只能靠大量真实使用积累&lt;/strong&gt;。这也是后面训练路线里&quot;阶段 0&quot;要做的第一件事。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;3. bilibili 学习资源&lt;/h1&gt;
&lt;p&gt;调研结论：B 站上的 Agent 课程主要分&lt;strong&gt;四类&lt;/strong&gt;，各自解决不同问题。按学习顺序推荐如下。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;th&gt;资源 / UP主&lt;/th&gt;
&lt;th&gt;BV 号 / 链接&lt;/th&gt;
&lt;th&gt;解决什么问题&lt;/th&gt;
&lt;th&gt;适合阶段&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;吴恩达《Agentic AI》&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV1aaxyz8ELY&lt;/code&gt;、&lt;code&gt;BV1DfrdByE2H&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;四大设计模式（反思/工具/规划/多智能体）、&lt;strong&gt;evals 评估&lt;/strong&gt;、MCP，官方中英字幕&lt;/td&gt;
&lt;td&gt;入门→进阶&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;李宏毅《AI Agent 系统设计》2025/2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV1o3wvzUEDD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;零基础概念入门，&quot;复仇者联盟&quot;类比多智能体，生动易懂&lt;/td&gt;
&lt;td&gt;零基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;微软《AI Agents for Beginners》&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV18TZYY8EuJ&lt;/code&gt;（GitHub 5万+星，12 课时中文）&lt;/td&gt;
&lt;td&gt;系统性：从概念到工具调用、记忆、多智能体全覆盖&lt;/td&gt;
&lt;td&gt;零基础→入门&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★☆&lt;/td&gt;
&lt;td&gt;李沐《动手学 AI Agent》&lt;/td&gt;
&lt;td&gt;B 站搜&quot;李沐 Agent&quot;&lt;/td&gt;
&lt;td&gt;从零手搓多智能体框架，&lt;strong&gt;ReAct/AutoGPT 论文逐句精读&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;进阶&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★☆&lt;/td&gt;
&lt;td&gt;尚硅谷 LangChain / LangGraph 实战&lt;/td&gt;
&lt;td&gt;B 站搜&quot;尚硅谷 LangChain&quot;&lt;/td&gt;
&lt;td&gt;框架 API 实战，工程落地&lt;/td&gt;
&lt;td&gt;入门→进阶&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★☆&lt;/td&gt;
&lt;td&gt;Hugging Face《Agents Course》&lt;/td&gt;
&lt;td&gt;B 站搜&quot;HF Agents Course&quot;（官网 huggingface.co/learn/agents-course）&lt;/td&gt;
&lt;td&gt;生产级框架 smolagents / LangGraph 实战，补&quot;跑得起来&quot;的缺口&lt;/td&gt;
&lt;td&gt;进阶&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★☆☆&lt;/td&gt;
&lt;td&gt;各类 MCP / Claude Code 实战视频&lt;/td&gt;
&lt;td&gt;如 &lt;code&gt;BV1oecTzKELA&lt;/code&gt;（86 集 LLM+MCP 实战）&lt;/td&gt;
&lt;td&gt;工具协议与真实 Agent 产品上手&lt;/td&gt;
&lt;td&gt;全程穿插&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;几点选课建议（避免踩坑）&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先刷吴恩达《Agentic AI》&lt;/strong&gt;：它把&quot;反思、工具使用、规划、多智能体&quot;四个设计模式和 &lt;strong&gt;evals（评估）&lt;/strong&gt; 讲得最清楚，而 evals 恰恰是 Harness 岗最核心、也是绝大多数教程最缺的一块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;李宏毅负责&quot;建立直觉&quot;&lt;/strong&gt;，微软《AI Agents for Beginners》负责&quot;系统覆盖&quot;——两个二选一即可，都看完反而拖节奏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;框架类视频（LangChain/LangGraph）不要只看不写&lt;/strong&gt;。B 站教程普遍&quot;演示多、原理解释少&quot;，看完一定要配合第 5 节的动手项目。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;警惕&quot;全 X 百集、七天成大神&quot;类搬运合集&lt;/strong&gt;：这类视频多为营销搬运，结构松散、无版权标注。优先选 UP 主本人或官方频道（DeepLearning.AI、Hugging Face、Datawhale 等）的内容。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;4. GitHub 项目精读&lt;/h1&gt;
&lt;p&gt;GitHub 项目分三类精读：&lt;strong&gt;评测 Harness&lt;/strong&gt;（对标 &quot;harness&quot; 本义，最重要）、&lt;strong&gt;Agent 框架&lt;/strong&gt;（学架构）、&lt;strong&gt;手搓/教学项目&lt;/strong&gt;（练工程感）。&lt;/p&gt;
&lt;h2&gt;4.1 评测 Harness：理解 &quot;harness&quot; 一词的出处&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;EleutherAI/lm-evaluation-harness&lt;/strong&gt;（11k+ star）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;架构&lt;/strong&gt;：把评测拆成&quot;模型接口&quot;与&quot;任务定义&quot;两半。任何模型只需实现 &lt;code&gt;lm_eval.api.model.LM&lt;/code&gt; 的三个原语——&lt;code&gt;loglikelihood&lt;/code&gt;（多选题对数概率）、&lt;code&gt;loglikelihood_rolling&lt;/code&gt;（困惑度）、&lt;code&gt;generate_until&lt;/code&gt;（自由生成）——即可接入 60+ 标准基准。任务从 v0.4.0 起从 Python 子类迁移到 &lt;strong&gt;YAML 配置&lt;/strong&gt;，降低加新 benchmark 门槛。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可借鉴的 Harness 实践&lt;/strong&gt;：① 统一抽象（模型后端与任务解耦）；② YAML 声明式任务定义；③ 结果缓存保证可复现；④ 任务版本号（task version）确保跨时间对比公平。它是 Hugging Face Open LLM Leaderboard 的评测后端，是&quot;评测 harness&quot;的事实标准。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SWE-bench / SWE-agent&lt;/strong&gt;（Princeton）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;架构&lt;/strong&gt;：SWE-bench 收集 2294 个真实 GitHub issue + 对应 PR，用 &lt;strong&gt;FAIL_TO_PASS / PASS_TO_PASS&lt;/strong&gt; 测试作为评测信号——模型要改代码让原本失败的测试通过。SWE-agent 是第一个基于 Agent 的系统，用 ReAct 循环 + &lt;strong&gt;Agent-Computer Interface（ACI）&lt;/strong&gt; 设计（精简的文件/编辑/搜索工具）刷出 12.47% 的 baseline。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可借鉴的 Harness 实践&lt;/strong&gt;：① &lt;strong&gt;执行即评测&lt;/strong&gt;（execution-graded，跑测试而不是看文本相似度）；② 工具接口设计（ACI）对 Agent 性能影响巨大；③ 轨迹打分（trajectory scoring）能区分&quot;差一步就对&quot;和&quot;完全跑偏&quot;两种失败——这是比&quot;最终对错&quot;更细粒度的评测信号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;All-Hands-AI/OpenHands（原 OpenDevin）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;架构&lt;/strong&gt;：代码 Agent 平台，基于 &lt;strong&gt;事件流（event stream）架构&lt;/strong&gt;，Agent 的每个 action/observation 都是事件，可回放、可注入、可评测。内置 CodeActAgent（统一 action space：把&quot;动作&quot;统一为&quot;执行一段代码&quot;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可借鉴的 Harness 实践&lt;/strong&gt;：事件流让 Agent 执行&lt;strong&gt;完全可审计、可回放、可作为训练数据&lt;/strong&gt;——这正是&quot;以真实任务为反馈源、持续迭代&quot;的落地形态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他值得扫一眼的评测项目：&lt;code&gt;UKGovernmentBEIS/inspect_ai&lt;/code&gt;（自定义 agent eval 的框架，标准化推荐）、&lt;code&gt;ServiceNow/BrowserGym&lt;/code&gt; + &lt;code&gt;AgentLab&lt;/code&gt;（Web 环境评测）、&lt;code&gt;sierra-research/tau-bench&lt;/code&gt;（工具+用户仿真，pass^k 可靠性指标）、&lt;code&gt;GAIA&lt;/code&gt;/&lt;code&gt;WebArena&lt;/code&gt;/&lt;code&gt;AgentBench&lt;/code&gt;/&lt;code&gt;OSWorld&lt;/code&gt;（多环境基准）。&lt;/p&gt;
&lt;h2&gt;4.2 Agent 框架：读架构、学取舍&lt;/h2&gt;
&lt;p&gt;调研结论（2026 年年中，star 数与版本随时变动，以下为方向性结论）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;框架&lt;/th&gt;
&lt;th&gt;心智模型&lt;/th&gt;
&lt;th&gt;核心原语&lt;/th&gt;
&lt;th&gt;何时用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LangGraph&lt;/strong&gt;（langchain-ai）&lt;/td&gt;
&lt;td&gt;状态图&lt;/td&gt;
&lt;td&gt;StateGraph、checkpointer、human-in-the-loop&lt;/td&gt;
&lt;td&gt;生产级多智能体、需持久化/恢复/审批&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AutoGen → Microsoft Agent Framework&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;对话/actor&lt;/td&gt;
&lt;td&gt;GroupChat、异步事件&lt;/td&gt;
&lt;td&gt;开放讨论、红队、辩论（成本偏高）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CrewAI&lt;/strong&gt;（crewAIInc）&lt;/td&gt;
&lt;td&gt;角色分工&lt;/td&gt;
&lt;td&gt;role/goal/backstory + crew + process&lt;/td&gt;
&lt;td&gt;内容流水线、快速原型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenAI Agents SDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;handoff 交接&lt;/td&gt;
&lt;td&gt;Agent + handoffs + guardrails + sessions&lt;/td&gt;
&lt;td&gt;快速交付、OpenAI 生态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;smolagents&lt;/strong&gt;（huggingface）&lt;/td&gt;
&lt;td&gt;code-first&lt;/td&gt;
&lt;td&gt;CodeAgent（让 LLM 写 Python）&lt;/td&gt;
&lt;td&gt;可审计、极简、HF 生态&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键取舍（面试常考）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LangGraph 是生产默认&lt;/strong&gt;：durable execution（crash 后从 checkpoint 恢复）、human-in-the-loop、时间旅行调试。代价是学习曲线陡、对简单 agent 显得重。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AutoGen 的 GroupChat 成本失控&lt;/strong&gt;：同一任务可能多出 5~6 倍 token 调用，因为每轮都拉全量 transcript 进上下文——这是&quot;编排方式决定成本&quot;的典型例证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;smolagents 的&quot;code-first&quot;是趋势&lt;/strong&gt;：让 LLM 直接写代码而非走严格的 function-calling 格式，动作空间更灵活、可审计性更好，值得精读其 ~1000 行核心源码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAI Agents SDK 的 handoff 是&quot;委派即原语&quot;&lt;/strong&gt;：triage agent 判断意图后交接给专家 agent，状态通过对话历史流动，心智模型干净但状态默认短暂。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;读源码建议&lt;/strong&gt;：不要五个都读。&lt;strong&gt;精读 smolagents（最小可读）→ 精读 LangGraph（生产代表）&lt;/strong&gt;，其余读文档 + 跑 demo 即可。&lt;/p&gt;
&lt;h2&gt;4.3 手搓/教学项目：练 &quot;harness 工程感&quot;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;datawhalechina/hello-agents&lt;/strong&gt;：从零手搓一个 Agent 框架，无第三方依赖。&lt;strong&gt;强烈推荐&lt;/strong&gt;——只有自己搭过状态图、持久化、工作流编排，出问题才知道断在哪。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;microsoft/ai-agents-for-beginners&lt;/strong&gt;：12 课时的系统入门，含完整中文，覆盖 Agent 概念、工具、记忆、多智能体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP（modelcontextprotocol）&lt;/strong&gt;：不是&quot;项目&quot;而是&quot;协议&quot;，但 &lt;code&gt;modelcontextprotocol/servers&lt;/code&gt; 是理解 Skills/MCP 工具生态的必读。JD 明确把 MCP、Skills 列为知识点。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;5. 训练路线&lt;/h1&gt;
&lt;p&gt;从零到对标岗位，按 6 个阶段推进。每阶段给出&lt;strong&gt;目标、动手项目、可复现实验、验收标准&lt;/strong&gt;。总周期建议 3~6 个月（有后端/算法基础可压缩）。&lt;/p&gt;
&lt;h2&gt;阶段 0：成为 Agent 重度用户 + 补 LLM 基础（1~2 周）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：建立&quot;对模型行为的品味&quot;，这是 JD 反复要求、面试最难伪装的能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手&lt;/strong&gt;：把 Claude Code / Cursor 接入你的日常——用它们写代码、读代码库、重构。记录：什么时候它&quot;神&quot;，什么时候它&quot;蠢&quot;（死循环、改错文件、忘记目标）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实验&lt;/strong&gt;：同一个任务，分别用 Cursor、Claude Code、Manus 跑，对比工具调用轨迹、失败模式、token 消耗。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：能写出 200 字的&quot;Agent 行为观察笔记&quot;，能解释 KV Cache 为什么是长任务瓶颈。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阶段 1：手搓最小 Agent Loop（2~3 周）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：不依赖任何框架，理解 Agent Loop / Tool Use / ReAct 的底层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：用 Python 写一个 ReAct Agent——LLM 输出 &lt;code&gt;Thought → Action → Observation&lt;/code&gt; 循环，调用 2~3 个真实工具（搜索、计算器、文件读写）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可复现实验&lt;/strong&gt;：① 对比&quot;带 ReAct 循环&quot; vs &quot;单次生成&quot;解决多步任务的正确率；② 加入 max_iteration / 循环检测，观察死循环如何被切断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：核心循环 &amp;lt; 200 行可读代码；能画出 ReAct 的状态机图。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 最小 ReAct 循环（伪代码，用于理解，非生产实现）
def react_loop(task: str, tools: dict, llm, max_iter: int = 10):
    messages = [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: SYS_PROMPT}]  # 包含工具 schema
    messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: task})
    for _ in range(max_iter):
        out = llm(messages)                    # Thought + Action(JSON)
        thought, action = parse(out)           # 解析出动作与参数
        if action.name == &quot;finish&quot;:
            return action.args[&quot;answer&quot;]       # 终止
        obs = tools[action.name](**action.args)  # 执行工具，得到 Observation
        messages.append({&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: out})
        messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: f&quot;Observation: {obs}&quot;})
    raise TimeoutError(&quot;agent loop 超过最大迭代&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;阶段 2：框架实战 + 上下文工程（3~4 周）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：用 LangGraph 落地，重点攻克&lt;strong&gt;记忆与上下文管理&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：把阶段 1 的 Agent 用 LangGraph 重写，加入 ① &lt;code&gt;checkpointer&lt;/code&gt;（持久化，crash 后恢复）；② 短期记忆（滑动窗口）+ 长期记忆（向量库/摘要）；③ human-in-the-loop 审批节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可复现实验&lt;/strong&gt;：对比&quot;无记忆 vs 滑动窗口 vs 摘要压缩&quot;在长对话任务中的 token 消耗与正确率，量化上下文管理收益。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：能解释 StateGraph 的状态流；能实现 checkpoint 恢复与时间旅行调试。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阶段 3：多智能体编排 + MCP 工具生态（3~4 周）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：掌握 Subagent / Multi-Agent 与工具协议。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：实现一个 supervisor 模式的多智能体（研究员 + 写手 + 评审），并写一个自定义 &lt;strong&gt;MCP Server&lt;/strong&gt; 让 Agent 调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可复现实验&lt;/strong&gt;：对比&quot;单 Agent&quot; vs &quot;supervisor 多 Agent&quot; vs &quot;GroupChat 多 Agent&quot;在同一任务上的成功率与成本，验证&quot;编排方式决定成本&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：能手写 handoff / 图编排 / supervisor 三种模式并说清取舍。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阶段 4：评测体系（4~6 周，Harness 岗核心）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：搭一套&lt;strong&gt;可复现的 eval harness&lt;/strong&gt;，这是与普通 Agent 开发者拉开差距的关键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：① 用 &lt;code&gt;lm-evaluation-harness&lt;/code&gt; 的 YAML 自定义一个任务；② 用 SWE-bench Lite 跑一个基线 Agent（可参考 smolagents 或 SWE-agent 的最小实现）；③ 自建 20~50 题的个人 benchmark，对阶段 1~3 的 Agent 做 error analysis。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可复现实验&lt;/strong&gt;：对同一 Agent 分别做 outcome 打分 vs trajectory 打分，验证&quot;最终对错&quot;如何掩盖&quot;差一步就对&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：能写出一份 eval 报告，含失败模式分类 + 下一步改进优先级（这正是吴恩达《Agentic AI》模块 4 讲的&quot;误差分析&quot;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阶段 5：真实项目 + 开源贡献（长期）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;：把能力沉淀为&lt;strong&gt;可展示的开源作品&lt;/strong&gt;（JD 加分项）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动手项目&lt;/strong&gt;：做一个端到端的 Agent 产品（如代码维护 Agent、研究 Agent、数据分析 Agent），开源并写好 README、eval 结果、trace 回放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复现实验&lt;/strong&gt;：提交 PR 给 smolagents / LangGraph / lm-eval / SWE-bench 等社区（哪怕修文档、加 test）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收&lt;/strong&gt;：有 1~2 个能放进简历的开源项目 + 若干社区 PR。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;6. 对标招聘要求&lt;/h1&gt;
&lt;p&gt;把 JD 的任职要求逐条映射成&quot;学习完成后应具备的能力&quot;，并给出面试准备方向。&lt;/p&gt;
&lt;h2&gt;6.1 能力映射表&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JD 要求（研发/工程方向）&lt;/th&gt;
&lt;th&gt;对应训练阶段&lt;/th&gt;
&lt;th&gt;面试验证方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;熟悉 LLM + Agent 机制（LLM API、KV Cache、Agent Loop、Tool Use、Reasoning、Planning、Skills、MCP、Memory、Subagent、Multi-Agent）&lt;/td&gt;
&lt;td&gt;阶段 0~3&lt;/td&gt;
&lt;td&gt;白板画 Agent Loop 状态机；解释 KV Cache；比较编排模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;深入理解 Prompt / Context / Harness Engineering&lt;/td&gt;
&lt;td&gt;阶段 2、4&lt;/td&gt;
&lt;td&gt;现场设计一个记忆分层 + 上下文窗口管理方案&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;熟练使用 AI Agent 工具做软件开发&lt;/td&gt;
&lt;td&gt;阶段 0 贯穿全程&lt;/td&gt;
&lt;td&gt;展示你用 Claude Code/Cursor 做项目的实际经历与效率收益&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是 Agent 产品重度用户，对模型行为有品味&lt;/td&gt;
&lt;td&gt;阶段 0&lt;/td&gt;
&lt;td&gt;谈对某个 Agent 产品&quot;神/蠢&quot;时刻的具体观察&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;技术水平过硬、眼界广阔（架构与选型）&lt;/td&gt;
&lt;td&gt;阶段 2~4&lt;/td&gt;
&lt;td&gt;框架选型题：什么场景选 LangGraph 还是 smolagents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;真实任务作反馈源、持续迭代产品&lt;/td&gt;
&lt;td&gt;阶段 4、5&lt;/td&gt;
&lt;td&gt;讲你的 eval → error analysis → 改进闭环&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;6.2 面试准备清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;三个必答理论题&lt;/strong&gt;：① 什么是 Harness，Model + Harness = Agent 怎么理解；② Prompt / Context / Harness Engineering 三者的边界；③ 评测的 outcome vs trajectory 打分差异。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个必带的项目&lt;/strong&gt;：一个&lt;strong&gt;带评测、带 trace、开源&lt;/strong&gt;的 Agent 项目（阶段 5 的产出），重点讲&quot;我怎么评测它、怎么改进它&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三个可现场手撕的小题&lt;/strong&gt;：手写 ReAct Loop、设计一个 MCP 工具 schema、画一个 supervisor 多智能体的状态图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一页&quot;产品观察&quot;&lt;/strong&gt;：你深度使用过的 2~3 个 Agent 产品，各自的优点、失败模式、你作为 Harness 工程师会怎么改。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;6.3 简历项目建议（按含金量排序）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;一个&lt;strong&gt;自建 eval harness + benchmark + error analysis 报告&lt;/strong&gt;（最稀缺、最对味）。&lt;/li&gt;
&lt;li&gt;一个&lt;strong&gt;多智能体 + 记忆 + 持久化的端到端产品&lt;/strong&gt;（LangGraph 实现）。&lt;/li&gt;
&lt;li&gt;一个&lt;strong&gt;最小可读的 ReAct/CodeAct Agent 框架&lt;/strong&gt;（手搓，展示工程功底）。&lt;/li&gt;
&lt;li&gt;开源社区 PR（哪怕小，证明能跟社区协作）。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;Harness 岗的本质是：&lt;strong&gt;在模型之上做&quot;可靠系统&quot;的人&lt;/strong&gt;。学习路径的底层逻辑只有一条——&lt;strong&gt;先成为 Agent 的重度用户（建立品味），再手搓理解底层（建立直觉），然后上框架做工程（建立硬功），最后用评测闭环迭代（建立判断力）&lt;/strong&gt;。B 站负责补&quot;概念与直觉&quot;，GitHub 源码负责补&quot;架构与工程感&quot;，而真正拉开差距的，永远是你自己搭的那套 eval harness 和那份 error analysis 报告。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;岗位&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DeepSeek Harness 团队招聘（mokahr）：&lt;code&gt;https://app.mokahr.com/social-recruitment/high-flyer/140576#/job/8d40c764-d2b2-49b1-826c-e3f2adb75c01&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;bilibili 视频&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;吴恩达《Agentic AI》：&lt;code&gt;BV1aaxyz8ELY&lt;/code&gt;、&lt;code&gt;BV1DfrdByE2H&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;李宏毅《AI Agent 系统设计》2026：&lt;code&gt;BV1o3wvzUEDD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;微软《AI Agents for Beginners》B 站版：&lt;code&gt;BV18TZYY8EuJ&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;李沐《动手学 AI Agent》：B 站搜&quot;李沐 Agent&quot;&lt;/li&gt;
&lt;li&gt;Hugging Face Agents Course：&lt;code&gt;huggingface.co/learn/agents-course&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;GitHub 项目&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;评测 Harness：&lt;code&gt;EleutherAI/lm-evaluation-harness&lt;/code&gt;、&lt;code&gt;SWE-bench/SWE-bench&lt;/code&gt;、&lt;code&gt;SWE-agent/SWE-agent&lt;/code&gt;、&lt;code&gt;All-Hands-AI/OpenHands&lt;/code&gt;、&lt;code&gt;UKGovernmentBEIS/inspect_ai&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Agent 框架：&lt;code&gt;langchain-ai/langgraph&lt;/code&gt;、&lt;code&gt;microsoft/autogen&lt;/code&gt;（→ Microsoft Agent Framework）、&lt;code&gt;crewAIInc/crewAI&lt;/code&gt;、&lt;code&gt;openai/openai-agents-python&lt;/code&gt;、&lt;code&gt;huggingface/smolagents&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;教学/手搓：&lt;code&gt;datawhalechina/hello-agents&lt;/code&gt;、&lt;code&gt;microsoft/ai-agents-for-beginners&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;协议：&lt;code&gt;modelcontextprotocol/servers&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;经典论文（按需精读）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ReAct（arXiv:2210.03629）、Chain-of-Thought（arXiv:2201.11903）、Reflexion、Tree of Thoughts&lt;/li&gt;
&lt;li&gt;SWE-bench（Jimenez et al., ICLR 2024）、CodeAct（OpenHands）、GAIA、WebArena、τ-bench、AgentBench&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DeepSeek 研发岗位全量扫描与自我匹配评估</title><link>https://chaggle.github.io/posts/2026/08/13/deepseek-rd-jobs-scan/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/13/deepseek-rd-jobs-scan/</guid><pubDate>Thu, 13 Aug 2026 15:59:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文对幻方&amp;amp;DeepSeek 招聘官网全部在招岗位做全量扫描——排除产品与职能部门后，对 21 个研发/技术岗位逐一打分排序，并给出投递策略。单个岗位的逐条分析（Agent Infra）见上一篇：&lt;a href=&quot;https://chaggle.github.io/2026/08/13/ai/agent-infra-job-analysis/&quot;&gt;DeepSeek「Agent Infra 研发工程师」岗位分析与自我匹配评估&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::tip
&lt;strong&gt;结论先行&lt;/strong&gt;：最适合我的岗位集中在三个方向——服务端开发（数据仓库/研究中台）、Code Agent 数据工程（后端专项）、AI 平台运维（运维开发）。完整排序与匹配度见下表。
:::&lt;/p&gt;
&lt;h2&gt;筛选口径&lt;/h2&gt;
&lt;p&gt;2026-08-13 抓取招聘官网全部在招记录，共 &lt;strong&gt;36 条&lt;/strong&gt;。按&quot;排除产品、职能部门&quot;的口径筛选：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;产品类 7 条（排除）&lt;/strong&gt;：Agent Harness 产品经理、通用 Agent 数据产品经理、专业领域数据产品经理、AI 创作数据产品经理、AI 产品经理、AI 产品运营、情感智能数据产品经理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;职能类 6 条（排除）&lt;/strong&gt;：HR 团队、AGI 核心业务管培生、法务团队、财务团队、采购团队、行政团队&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳转指引 2 条（合并）&lt;/strong&gt;：Agent Harness 研究员 / 研发工程师 → 合并入「Agent Harness 团队」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;剩余 &lt;strong&gt;21 个研发/技术岗位&lt;/strong&gt;，以下逐一打分排序。&lt;/p&gt;
&lt;h2&gt;匹配度总排序&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;梯队&lt;/th&gt;
&lt;th&gt;岗位&lt;/th&gt;
&lt;th&gt;匹配度&lt;/th&gt;
&lt;th&gt;一句话判断&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;第一梯队&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;服务端开发工程师&lt;/strong&gt;（大模型研究中台 / 线上核心服务 / 数据仓库）&lt;/td&gt;
&lt;td&gt;★★★★★ ~80%&lt;/td&gt;
&lt;td&gt;数据架构转型 + Go 后端 + 与研究员协作经历，几乎逐条命中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第一梯队&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Code Agent 数据工程师&lt;/strong&gt;（后端专项）&lt;/td&gt;
&lt;td&gt;★★★★★ ~75%&lt;/td&gt;
&lt;td&gt;&quot;AI 编程工具重度使用 + 3 年后端 + 主场生态 + 公开技术输出&quot;，博客就是证据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第一梯队&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AI 平台运维工程师&lt;/strong&gt;（运维开发 / 集群运维）&lt;/td&gt;
&lt;td&gt;★★★★ ~70%&lt;/td&gt;
&lt;td&gt;明确要 Go/Python 开发 + 运维痛点经历；集群运维方向&quot;素质优先、不设经验门槛&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第二梯队&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Agent Infra 研发工程师&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;★★★ ~60%&lt;/td&gt;
&lt;td&gt;详见上篇，主攻方向 5、6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第二梯队&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;预训练数据工程师&lt;/strong&gt;（数据基建方向）&lt;/td&gt;
&lt;td&gt;★★★ ~60%&lt;/td&gt;
&lt;td&gt;Redis/Kafka/HDFS/Spark 经历直接映射，但只限数据基建方向&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第二梯队&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;测试开发工程师&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;★★★ ~55%&lt;/td&gt;
&lt;td&gt;Go 优先 + 质量保障体系，CI/CD 与统筹测试团队的经历可讲&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第三梯队&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Agent Harness 研发/工程&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;★★ ~45%&lt;/td&gt;
&lt;td&gt;学历&quot;知名高校&quot;硬门槛 + 无 Harness 工程作品&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第三梯队&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;大模型训练/推理框架工程师&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;★★ ~40%&lt;/td&gt;
&lt;td&gt;无 PyTorch/训练栈；但 JD 罕见地要求&quot;擅长与 Code Agent 合作&quot;——是我的亮点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第三梯队&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;IT 基础设施团队&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;★★ ~40%&lt;/td&gt;
&lt;td&gt;硬件/网络方向要数通与 RDMA；桌面方向门槛低但天花板低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第四梯队&lt;/td&gt;
&lt;td&gt;AI 搜索算法/架构、超算集群研发、深度学习研发&lt;/td&gt;
&lt;td&gt;★ ~25-30%&lt;/td&gt;
&lt;td&gt;需 ML/DL 或体系结构深度，博客证据为零&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第四梯队&lt;/td&gt;
&lt;td&gt;AI 跨界技术人才、前端/客户端&lt;/td&gt;
&lt;td&gt;★ ~15-20%&lt;/td&gt;
&lt;td&gt;前者要&quot;超乎常人的才能&quot;与良好教育背景；后者无前端深度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第五梯队&lt;/td&gt;
&lt;td&gt;各类研究员（预训练/后训练/多模态/Frontier/Harness 研究）、算子/编译器、分布式存储、IDC 数据中心&lt;/td&gt;
&lt;td&gt;★ ~5-10%&lt;/td&gt;
&lt;td&gt;需论文、竞赛、CUDA、内核级深度或电气暖通背景，不建议投&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;第一梯队详解&lt;/h2&gt;
&lt;h3&gt;1. 服务端开发工程师 —— 匹配度最高的岗位&lt;/h3&gt;
&lt;p&gt;JD 分三个方向，与我履历的对应关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据仓库方向&lt;/strong&gt;：要求&quot;本科及以上，1~5 年数据开发/数仓经验，SQL + 至少一门编程语言&quot;。我 2025 年转向数据架构，大数据底座（HDFS/Spark/Flink/Hive）与数据中台的研发交付正是这段经历的书面化证明——博客里 40+ 篇大数据原理与部署调优笔记就是证据链。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大模型研究中台方向&lt;/strong&gt;：要求&quot;扎实工程基础，深入掌握一门后端语言，系统学习分布式系统、高并发架构设计&quot;，且要与研究员紧密协作。Go 是我的主场语言（GMP/内存模型/GC 均有系统笔记）；2025 年与算法组协作交付数据标注平台的经历，直接对应&quot;与研究员协作&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线上核心服务方向&lt;/strong&gt;：要求数据结构与算法——91 天 LeetCode 打卡记录是现成的证据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个岗位的意义在于：它是把我的&quot;传统后端 + 数据架构&quot;两条职业线&lt;strong&gt;合流进 AI 行业的最顺通道&lt;/strong&gt;，不需要伪造任何新能力。&lt;/p&gt;
&lt;h3&gt;2. Code Agent 数据工程师（后端专项）—— 意外的高匹配&lt;/h3&gt;
&lt;p&gt;JD 开篇就写：&quot;这是一个 AI 时代的新型工程师岗位。你不需要写业务代码、交付产品，而是用独到的经验和品味，去发现问题、定义评测标准、改进训练数据，&lt;strong&gt;教大模型更好地写代码&lt;/strong&gt;。&quot;&lt;/p&gt;
&lt;p&gt;逐条对照后端专项要求：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JD 要求&lt;/th&gt;
&lt;th&gt;我的证据&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI 编程工具（Claude Code、Codex 等）重度使用&lt;/td&gt;
&lt;td&gt;opencode 会话库 3 个月 376MB（使用与备份细节详见上篇&quot;独特的视角优势&quot;一节）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 年以上后端一线开发经验&lt;/td&gt;
&lt;td&gt;2022 年入行，至 2026 年已有 4 年多后端经验（Go/Java）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有&quot;主场&quot;技术生态，能讲清语言运行时（并发调度、GC、内存模型）&lt;/td&gt;
&lt;td&gt;Go：GMP、内存模型、GC、并发模式均有系统笔记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长期负责高并发或数据密集服务（事务边界、缓存一致性、限流降级）&lt;/td&gt;
&lt;td&gt;MySQL/Redis/Kafka 原理 + 调优笔记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;公开可查的开源贡献或技术输出&lt;/td&gt;
&lt;td&gt;本博客 175 篇文章本身就是公开技术输出&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip
这个岗位把我最大的&quot;非传统&quot;优势——&lt;strong&gt;AI 编程工具的深度使用经验&lt;/strong&gt;——从爱好变成了职业资格。而且工作内容（定义评测、构造训练环境、补能力短板）与我正在做的事（用 opencode 批产博客、观察模型行为、写备份工具）高度同构。
:::&lt;/p&gt;
&lt;p&gt;注意点：该岗还有前端/移动端/3D/游戏/安全专项，我只匹配&lt;strong&gt;后端专项&lt;/strong&gt;，投递时必须注明&quot;后端专项（主场：Go）&quot;。&lt;/p&gt;
&lt;h3&gt;3. AI 平台运维工程师 —— 唯一&quot;素质优先、主动放开经验门槛&quot;的岗位&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运维开发方向&lt;/strong&gt;：明确要求&quot;较好的 Python/Golang 开发能力&quot;（Go 是我的主场）；加分项&quot;3 年以上一线运维经历，深知运维痛点&quot;——我的政企驻场运维抗压经历就是痛点本身。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集群运维方向&lt;/strong&gt;：JD 原文写&quot;我们不太在意你过去有没有运维经验，也不太在意你是不是科班出身&quot;，看的是拆解问题的逻辑、系统性的技术视野、动手的冲动、对生产环境的敬畏——并且&lt;strong&gt;面试不考背诵&lt;/strong&gt;，只让讲&quot;一个你真正动手做过的事情，追问为什么这么做&quot;。&lt;/li&gt;
&lt;li&gt;我的可讲素材：政企大数据平台从 0 到 1 部署（Kerberos/Ranger 安全加固、K8s 调优、节点故障应急）、opencode 备份系统（WAL 一致性处理、恢复演练、日志审计——恰好是&quot;对生产环境的敬畏&quot;的微型证明）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二梯队简评&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Agent Infra 研发工程师&lt;/strong&gt;：上篇已逐条分析，此处不重复（结论见总排序表）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预训练数据工程师（数据基建方向）&lt;/strong&gt;：该方向职责为键值数据库、消息队列的开发维护调优（对应 Redis/Kafka ✅）、数据湖/对象存储/分布式文件系统（对应 HDFS ✅）、PB 级分布式数据处理框架（对应 Spark/Flink 部署调优 ✅）——与我的大数据底座经历几乎逐条对应；但该岗另三个方向（数据采集/语言数据/多模态）需要 Python 数据栈与爬虫经验，投递时必须锁定&quot;数据基建方向&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试开发工程师&lt;/strong&gt;：要求&quot;熟悉 Go、Rust 优先&quot;+&quot;对质量保障有系统的理解&quot;+测试工具/平台开发经验。我有 Go 深度、CI/CD 实践、2022 年 GoFrame 单测贡献、2023 年统筹研发测试运维的全局视角；缺&quot;测试平台开发&quot;作品，属于可以快速补的短板。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第三梯队及以下：为什么不投&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Agent Harness 研发/工程&lt;/strong&gt;：学历&quot;知名高校&quot;硬门槛 + 无 Harness 工程作品，性价比低（此前评估的结论，未单独成文）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大模型训练/推理框架工程师&lt;/strong&gt;：JD 出现&quot;擅长与 Code Agent 合作&quot;这个罕见要求（我的强项），但主体要求是 Python/C++/Rust + PyTorch 训练栈，我完全没有，先补课再考虑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IT 基础设施团队&lt;/strong&gt;：硬件&amp;amp;网络方向要 OSPF/BGP/RDMA/压测基线；桌面方向是 IT 支持（macOS/Windows 桌面运维），天花板低，与我的方向不符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 搜索算法/架构&lt;/strong&gt;：算法方向要 ML/DL/信息检索；架构方向要 C++/Rust 数据密集型系统。都不够。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超算集群 / 算子 / 编译器 / 分布式存储&lt;/strong&gt;：体系结构深度 + CUDA/Rust + RDMA + 内核级工程，博客证据为零。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;各类研究员岗&lt;/strong&gt;（预训练/后训练/多模态/Frontier/Agent Harness 研究）：需要硕士、论文、竞赛或顶尖实验室经历，全部不满足。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDC 数据中心团队&lt;/strong&gt;：电气/暖通/机房现场，非软件方向。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 跨界技术人才&lt;/strong&gt;：要求&quot;超乎常人的才能&quot;与&quot;良好的教育背景&quot;，加分项是世界级奖项/著名开源贡献/创业——我的个人技术博客只能算弱加分，整体画像差距太大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最终投递策略&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;主投三个&lt;/strong&gt;（按优先级）：
&lt;ol&gt;
&lt;li&gt;服务端开发工程师（数据仓库方向 / 研究中台方向）&lt;/li&gt;
&lt;li&gt;Code Agent 数据工程师（后端专项，注明主场 Go）&lt;/li&gt;
&lt;li&gt;AI 平台运维工程师（运维开发方向）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;次投三个&lt;/strong&gt;：Agent Infra（方向 5/6）、预训练数据工程师（数据基建方向）、测试开发工程师。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简历三条主线&lt;/strong&gt;：Go 后端深度（GMP/GC/内存模型）+ 大数据底座交付（政企项目 0→1）+ AI Agent 重度使用（证据见上篇&quot;独特的视角优势&quot;一节，此处不重复）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;补课优先级按主投岗位倒排&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Python 数据栈（pandas/Spark 编程）→ 服务端数据仓库方向、预训练数据工程师（本次新增项）&lt;/li&gt;
&lt;li&gt;可观测性实战与 Rust 两条已在上篇的&quot;行动计划&quot;中拆解（Prometheus/Grafana/Loki 压测报告、把备份工具重写为 Rust CLI），同时服务于本表的运维、Agent Infra 与测试开发岗位，此处不重复&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个总的观察&lt;/strong&gt;：三个主投岗位的共同点是——不要求名校学历、不要求论文竞赛，看重&quot;真实工程经历 + AI 工具重度使用 + 公开技术输出&quot;。这三点恰好是我博客已经证明的东西。相比此前评估的 Agent Harness 岗，这些岗位才是&quot;我&quot;这个画像的合理入口。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>DeepSeek「Agent Infra 研发工程师」岗位分析与自我匹配评估</title><link>https://chaggle.github.io/posts/2026/08/13/agent-infra-job-analysis/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/13/agent-infra-job-analysis/</guid><pubDate>Thu, 13 Aug 2026 13:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;岗位链接：&lt;a href=&quot;https://app.mokahr.com/social-recruitment/high-flyer/140576#/job/bae809fb-1978-4401-b209-34067b26569d&quot;&gt;幻方&amp;amp;DeepSeek 社会招聘 - Agent Infra 研发工程师&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此前对「Agent Harness 研发/工程方向」做过一轮评估，结论是：工程底子可用，但 LLM/Agent 机制深度与可展示的 Harness 作品不足，匹配度不高。本文分析的是同一个招聘官网上另一个岗位——&lt;strong&gt;Agent Infra 研发工程师&lt;/strong&gt;。它与前者的性质完全不同：本质是一个&lt;strong&gt;系统/基础设施&lt;/strong&gt;岗位，而非 Agent 应用层岗位。逐条对照之后，我的判断是：&lt;strong&gt;这个岗位与我的交集明显更大，是更现实的切入点，但硬伤也同样清晰。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;岗位基本信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;岗位&lt;/td&gt;
&lt;td&gt;Agent Infra 研发工程师&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;团队&lt;/td&gt;
&lt;td&gt;DSec（为 Agent 量身定制的沙箱云平台）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;部门&lt;/td&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;类别&lt;/td&gt;
&lt;td&gt;实习 / 全职&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;地点&lt;/td&gt;
&lt;td&gt;杭州拱墅区 / 北京海淀区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;发布时间&lt;/td&gt;
&lt;td&gt;2026-06-25（2026-08-11 更新，仍在开放）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;JD 留存&lt;/h2&gt;
&lt;h3&gt;团队使命&lt;/h3&gt;
&lt;p&gt;DSec 是为 Agent 量身定制的云平台，托管着成千上万个沙箱环境，正有序推进着下一代模型的迭代。为了增加可靠性和效率，DSec 需要修改整个系统栈：从操作系统到虚拟机，再到各级网络和存储，一直上到应用层调度和管控面服务。&lt;/p&gt;
&lt;p&gt;在这里需要解决各种规模的系统问题——小到自旋锁，大到跨区域数据一致性——还要加上无监管全自动运行的 Agent 带来的更多安全挑战。这意味着以往对负载模式、数据隔离、资源用量、平台安全的种种假设都可能不复成立，需要另辟蹊径。&lt;/p&gt;
&lt;h3&gt;岗位职责（任一方向即可）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;虚拟化环境&lt;/strong&gt;：托管大规模 VM 集群，支持多样的操作系统配置和硬件方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容器化&lt;/strong&gt;：加固隔离、缩小逃逸面、降低沙箱资源占用量；适配用户多种多样的需求以支持更高效的模型训练。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;临时存储&lt;/strong&gt;：设计专供沙箱的块设备和共享卷方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟化网络&lt;/strong&gt;：部署超大规模虚拟网络，允许 Agent 在不离开物理边界的前提下互相通信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云服务&lt;/strong&gt;：设计混合云架构，开发可观测平台、日志系统、控制面等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可观测性和高可用性&lt;/strong&gt;：配置监控，设计节点自动上下线流程和应急响应机制；负责平台的稳定运行。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;岗位要求&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;基本背景：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算机或其他相关专业，本科及以上学历。&lt;/li&gt;
&lt;li&gt;扎实的系统编程能力（倾向 Rust / C / Python），以及对简洁明了、可维护代码的基本美学素养。&lt;/li&gt;
&lt;li&gt;对底层机制有着深厚的好奇心，同时热衷于搞懂事物的基本运行原理。&lt;/li&gt;
&lt;li&gt;有良好的中文沟通能力和团队协作能力。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;满足下列任意一条即可：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;深入了解计算机体系结构，能够清晰描述一次文件写入从调用开始到落盘的全过程以及潜在瓶颈。&lt;/li&gt;
&lt;li&gt;深入了解大规模分布式系统的组织方式，清楚架构瓶颈产生的原因和与之相关的 trade-off。&lt;/li&gt;
&lt;li&gt;熟练运用压测和可观测性工具分析、定位涉及多个层级的复杂系统问题。&lt;/li&gt;
&lt;li&gt;理解可靠系统的设计，并且有意识地确保前人的事故不会在当下重演。&lt;/li&gt;
&lt;li&gt;有过大型系统的自动化运维、应急响应经验。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;加分项&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在 OSDI、SOSP、EuroSys、NSDI、ATC 等系统顶会发表过 OS、存储、网络或虚拟化方向论文。&lt;/li&gt;
&lt;li&gt;在算法竞赛或者 CTF 等其他计算机竞赛中获得过奖项。&lt;/li&gt;
&lt;li&gt;有过活跃的开源项目贡献，或者有拿得出手的个人系统项目。&lt;/li&gt;
&lt;li&gt;设计或部署过大规模 VPC 网络、存储系统、沙箱集群。&lt;/li&gt;
&lt;li&gt;搭建过或者管理过多种多样的虚拟化环境（比如在 RISC-V 上跑 Arch）。&lt;/li&gt;
&lt;li&gt;曾经把某些操作系统组件的性能压榨到 SOTA，或者干脆直接压到硬件极限。&lt;/li&gt;
&lt;li&gt;从 0 到 1 开发部署过需要水平扩展才能撑住的数据密集型应用。&lt;/li&gt;
&lt;li&gt;或者任何你觉得拿得出手的系统方向成果。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;回报&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;你来决定新时代 Agent 基础设施的形态，以及 Agent 们的生活环境。&lt;/li&gt;
&lt;li&gt;可以和顶尖的系统工程师和模型研究员一起工作。&lt;/li&gt;
&lt;li&gt;你的工作直接决定了 Agent 模型训练的多样性和效率。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;逐条自我对照&lt;/h2&gt;
&lt;h3&gt;基本要求（4 条）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;本科及以上学历&lt;/strong&gt; —— ✅ 满足。2021 年长沙理工大学本科毕业。注意：相比 Agent Harness 岗的&quot;知名高校&quot;表述，本岗位只要求本科及以上，学历门槛明显更宽松，我的学历短板在这里不构成硬伤。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扎实的系统编程能力（倾向 Rust / C / Python）&lt;/strong&gt; —— ⚠️ 这是最大的硬伤。我的主栈是 Go / Java / C++，其中 C++ 只停留在 STL 与并发模型的层面；Rust 完全没有作品；Python 仅停留在脚本层面。系统编程（内存分配、IO 栈、并发原语、网络栈）方向没有可展示的深度项目。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对底层机制有深厚好奇心&lt;/strong&gt; —— ✅ 满足。博客多年坚持输出中间件、大数据、网络框架的底层原理笔记（MySQL 日志与索引、Kafka 存储模型、Go 内存模型与调度器等），符合&quot;热衷搞懂基本原理&quot;的画像。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中文沟通与团队协作&lt;/strong&gt; —— ✅ 满足。2023 年起统筹研发、测试、产品、运维多方协作；2024 年任大型政企项目技术负责人，负责整体技术方案与交付质量，沟通与协作是被工作反复检验过的能力。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&quot;满足任意一条即可&quot;（5 选 1）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;要求&lt;/th&gt;
&lt;th&gt;我的情况&lt;/th&gt;
&lt;th&gt;判断&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;体系结构深度（文件写入全流程）&lt;/td&gt;
&lt;td&gt;无内核级、存储栈级知识证据&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;大规模分布式系统组织方式&lt;/td&gt;
&lt;td&gt;大数据底座（HDFS/Yarn/Spark/Flink）+ 数据中台架构 + 中间件集群有系统性积累，但偏部署与应用层，对一致性、分区、复制等分布式核心问题的深度不足&lt;/td&gt;
&lt;td&gt;⚠️ 部分满足&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;压测与可观测性定位多层问题&lt;/td&gt;
&lt;td&gt;有监控部署与线上问题排查经验（Prometheus/Grafana、日志体系），但缺一次完整的、可讲清的跨层压测定位案例&lt;/td&gt;
&lt;td&gt;⚠️ 部分满足&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;可靠系统设计，事故不复演&lt;/td&gt;
&lt;td&gt;有运维事故处理与复盘意识，但未形成系统性的可靠设计方法论&lt;/td&gt;
&lt;td&gt;⚠️ 部分满足&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;大型系统自动化运维、应急响应&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;最接近满足的一条&lt;/strong&gt;：2022 年从 0 到 1 完成产品研发、发布上线与运维；2024-2025 年驻场交付政企大数据平台，有过长期高强度驻场运维的经历；Jenkins CI/CD、Docker、K8s 部署调优均有实践&lt;/td&gt;
&lt;td&gt;✅ 可讲出真实故事&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;结论：五条中第 5 条可以凭真实经历够到门槛，第 2、3 条属于&quot;有素材但深度待补&quot;。这比 Agent Harness 岗&quot;核心要求几乎不满足&quot;的局面好得多。&lt;/p&gt;
&lt;h3&gt;加分项（8 条）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;系统顶会论文：没有 ❌&lt;/li&gt;
&lt;li&gt;算法竞赛 / CTF 奖项：没有 ❌&lt;/li&gt;
&lt;li&gt;开源贡献或个人系统项目：2022 年给 GoFrame 社区做过 pgsql 驱动单测（小贡献），但没有个人系统级项目 ⚠️&lt;/li&gt;
&lt;li&gt;大规模 VPC / 存储 / 沙箱集群：大数据集群（HDFS、Kafka、Flink）的部署有，VPC 与沙箱集群没有 ⚠️&lt;/li&gt;
&lt;li&gt;多样虚拟化环境（如 RISC-V 上跑 Arch）：没有 ❌&lt;/li&gt;
&lt;li&gt;性能压榨到 SOTA：没有 ❌&lt;/li&gt;
&lt;li&gt;从 0 到 1 开发需水平扩展的数据密集型应用：0 → 1 的完整经历有（教育行业产品全生命周期），但大数据平台属于部署交付而非自研，没有需要水平扩展的自研系统 ⚠️&lt;/li&gt;
&lt;li&gt;任何拿得出手的系统方向成果：政企大数据底座整体交付可以讲，但更偏&quot;工程集成&quot;而非&quot;系统研发&quot; ⚠️&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;六个职责方向的适配度&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方向&lt;/th&gt;
&lt;th&gt;我的证据&lt;/th&gt;
&lt;th&gt;适配度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. 虚拟化环境（VM 集群）&lt;/td&gt;
&lt;td&gt;无 KVM/QEMU/虚拟化底层经验&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. 容器化（隔离加固、逃逸面）&lt;/td&gt;
&lt;td&gt;Docker/K8s 使用与部署层，无容器底层（Namespace/Cgroup 原理级）&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. 临时存储（块设备/共享卷）&lt;/td&gt;
&lt;td&gt;无存储栈深度&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. 虚拟化网络（超大规模虚拟网络）&lt;/td&gt;
&lt;td&gt;有网络框架与协议笔记，但无大规模虚拟网络实践&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. 云服务（混合云、可观测、日志、控制面）&lt;/td&gt;
&lt;td&gt;数据中台/平台工程、Jenkins 流水线、监控日志体系经验&lt;/td&gt;
&lt;td&gt;中高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. 可观测性与高可用性&lt;/td&gt;
&lt;td&gt;K8s 部署运维、应急保障、监控调优实践与笔记&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip
六个方向里，&lt;strong&gt;5（云服务）与 6（可观测性与 HA）与我的履历同向&lt;/strong&gt;：这两个方向吃的是分布式运维、平台工程、应急响应能力——恰好是我 2024-2025 年驻场政企项目的主战场。1-4 则需要内核与虚拟化功底，短期补不出来，不应硬投。
:::&lt;/p&gt;
&lt;h3&gt;一个独特的视角优势&lt;/h3&gt;
&lt;p&gt;DSec 服务的是无监管全自动运行的 Agent，而我恰好是 Agent 工具的高强度用户：opencode 会话库三个月涨到 376MB，为此写了跨平台备份/恢复方案（含运行中一致性处理、恢复演练、日志审计），并每天看它的资源占用与日志。&lt;strong&gt;我对&quot;Agent 沙箱里的用户行为&quot;有第一手的负载直觉&lt;/strong&gt;——这在这个岗位的候选池里未必常见，是值得在简历和面试中主动打出的差异点。&lt;/p&gt;
&lt;h3&gt;与 Agent Harness 岗的对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Agent Harness 研发/工程&lt;/th&gt;
&lt;th&gt;Agent Infra 研发工程师&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;学历门槛&lt;/td&gt;
&lt;td&gt;知名高校本科及以上&lt;/td&gt;
&lt;td&gt;本科及以上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;核心考察&lt;/td&gt;
&lt;td&gt;LLM/Agent 机制深度、模型行为品味、Harness 工程&lt;/td&gt;
&lt;td&gt;系统编程、分布式运维、可观测性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;我的匹配度&lt;/td&gt;
&lt;td&gt;低（约 40%）&lt;/td&gt;
&lt;td&gt;中（约 55% ~ 65%）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;硬伤&lt;/td&gt;
&lt;td&gt;Agent 作品为零、机制停留在概念&lt;/td&gt;
&lt;td&gt;Rust/C 主栈缺失、底层深度不足&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;:::warning
诚实地说：这个岗位是&quot;系统底层工程师&quot;画像，理想候选人是有 Rust 系统作品、懂内核与虚拟化的人。我是&quot;平台部署运维 + 业务架构&quot;画像，整体匹配度约 &lt;strong&gt;55% ~ 65%&lt;/strong&gt;，&lt;strong&gt;够得到门槛但不算高匹配&lt;/strong&gt;——真正的交集在方向 5、6，而不是虚拟化与存储。
:::&lt;/p&gt;
&lt;p&gt;但相比 Agent Harness 岗，本岗位有三个现实优势：学历门槛宽松（学历不构成减分）；不考察 LLM 机制深度与模型行为品味，也不要求英文，考察的是运维与平台工程——正是我被工作检验过的能力；&quot;满足任意一条即可&quot;的门槛设计，让我能凭第 5 条（大型系统自动化运维、应急响应）的真实经历进入面试。&lt;/p&gt;
&lt;h2&gt;如果投递：行动计划&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;补 Rust 并做出可展示作品&lt;/strong&gt;：把 opencode 备份方案升级为 Rust CLI + 定时服务（原方案是 PowerShell/bash 脚本），做成一个开源仓库——同时回应&quot;系统编程能力&quot;与&quot;个人系统项目&quot;两条。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;补内核与虚拟化基础&lt;/strong&gt;：进程/内存/文件系统/IO 栈、KVM、Namespace/Cgroup，每学一块写一篇博客（延续本博客&quot;输入必须输出&quot;的原则）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;做一次完整的可观测性实战&lt;/strong&gt;：把家庭环境（NAS + 开发机）接入 Prometheus + Grafana + Loki，设计一次真实压测，写一份&quot;跨层问题定位&quot;报告——直接对应&quot;满足任意一条&quot;第 3 条。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调研 DSec/Agent 沙箱方向&lt;/strong&gt;：搜集公开资料（沙箱逃逸、gVisor/Firecracker 对比、Agent 负载特征），写 1-2 篇技术调研博客。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简历定位务实&lt;/strong&gt;：以&quot;分布式平台工程 + 大数据底座 + AI Agent 重度用户&quot;切入，主攻方向 5、6，不包装成内核专家。全局投递策略与补课优先级见下一篇的汇总。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::note
岗位仍在开放中。稳妥路线是先完成第 1、3 条再投递；若想赶窗口期，也可以先以运维/平台工程故事直接投方向 5、6，同时并行推进补课。
:::&lt;/p&gt;
&lt;p&gt;:::note
对官网全部研发岗位的横向扫描与整体投递策略，见下一篇：&lt;a href=&quot;https://chaggle.github.io/2026/08/13/ai/deepseek-rd-jobs-scan/&quot;&gt;DeepSeek 研发岗位全量扫描与自我匹配评估&lt;/a&gt;。
:::&lt;/p&gt;
</content:encoded></item><item><title>C++ STL 容器学习笔记</title><link>https://chaggle.github.io/posts/2026/08/11/cpp-stl-containers/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/cpp-stl-containers/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;2020-2021 年算法刷题时期学习的 C++ STL 容器笔记（map、priority_queue/multiset、unordered_map、unordered_set）合并整理。除了 bitset、priority_queue（堆）以及 AVL（平衡树相关）之外，其他的都可以进行短时间的手撕代码实现，但熟悉容器用法依然是刷题效率的基础。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;map（有序映射）&lt;/h2&gt;
&lt;h3&gt;map 简介&lt;/h3&gt;
&lt;p&gt;map 是 STL（Standard Template Library，标准模板库）的一个关联容器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;可以将任何基本类型映射到任何基本类型。如 int array[100] 事实上就是定义了一个 int 型到 int 型的映射&lt;/li&gt;
&lt;li&gt;map 提供一对一的数据处理，key-value 键值对，其类型可以自己定义，第一个称为关键字，第二个为关键字的值&lt;/li&gt;
&lt;li&gt;map 内部是自动排序的&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用 map 前必须引入头文件 &lt;code&gt;#include &amp;lt;map&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;map 的定义&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;map&amp;lt;type1name, type2name&amp;gt; maps;&lt;/code&gt;，第一个是键的类型，第二个是值的类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;map&amp;lt;string, int&amp;gt; maps;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;map 容器内元素的访问&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;通过下标进行访问：&lt;code&gt;maps[&apos;c&apos;] = 5;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过迭代器进行访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;map 可以使用 &lt;code&gt;it-&amp;gt;first&lt;/code&gt; 来访问键，使用 &lt;code&gt;it-&amp;gt;second&lt;/code&gt; 访问值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;map&amp;gt;
#include &amp;lt;iostream&amp;gt;
using namespace std;
int main()
{
   map&amp;lt;char,int&amp;gt; maps;
   maps[&apos;a&apos;] = 10;
   maps[&apos;b&apos;] = 20;
   maps[&apos;c&apos;] = 30;
   for(map&amp;lt;char,int&amp;gt;::iterator it=maps.begin(); it!=maps.end(); it++)
   {
       cout&amp;lt;&amp;lt;it -&amp;gt; first&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;it -&amp;gt; second&amp;lt;&amp;lt;endl;
   }
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;map 的常用用法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;maps.insert() 插入：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个map对象
map&amp;lt;int, string&amp;gt; m;

//用insert函数插入pair
m.insert(pair&amp;lt;int, string&amp;gt;(11, &quot;kk&quot;));

// 用insert函数插入value_type数据
m.insert(map&amp;lt;int, string&amp;gt;::value_type(22, &quot;pp&quot;));

// 用数组方式插入
m[12] = &quot;dd&quot;;
m[34] = &quot;ff&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;maps.find() 查找一个元素&lt;/li&gt;
&lt;li&gt;maps.clear() 清空&lt;/li&gt;
&lt;li&gt;maps.erase() 删除一个元素&lt;/li&gt;
&lt;li&gt;maps.size() 长度&lt;/li&gt;
&lt;li&gt;maps.begin() 返回指向 map 头部的迭代器&lt;/li&gt;
&lt;li&gt;maps.end() 返回指向 map 末尾的迭代器&lt;/li&gt;
&lt;li&gt;maps.rbegin() 返回指向 map 尾部的逆向迭代器&lt;/li&gt;
&lt;li&gt;maps.rend() 返回指向 map 头部的逆向迭代器&lt;/li&gt;
&lt;li&gt;maps.empty() 判断其是否为空&lt;/li&gt;
&lt;li&gt;maps.swap() 交换两个 map&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;map&amp;lt;string, int&amp;gt;::iterator it;

it=maps.find(&quot;123&quot;);

//迭代器删除
it = maps.find(&quot;123&quot;);
maps.erase(it);

//关键字删除
int n = maps.erase(&quot;123&quot;); //如果删除了返回1，否则返回0

//用迭代器范围删除 : 把整个map清空
maps.erase(maps.begin(), maps.end());
//等同于maps.clear()

int len = maps.size(); //获取到map中映射的个数

//迭代
map&amp;lt;string, int&amp;gt;::iterator it;
for(it = maps.begin(); it != maps.end(); it++)
    cout&amp;lt;&amp;lt; it-&amp;gt; first&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;it -&amp;gt; second&amp;lt;&amp;lt;endl;//输出key 和value值

//反向迭代
map&amp;lt;string,int&amp;gt;::reverse_iterator it;
for(it = maps.rbegin(); it != maps.rend(); it++)
    cout&amp;lt;&amp;lt;it -&amp;gt; first&amp;lt;&amp;lt;&apos; &apos;&amp;lt;&amp;lt;it -&amp;gt; second&amp;lt;&amp;lt;endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;priority_queue 与 multiset（堆与平衡树）&lt;/h2&gt;
&lt;h3&gt;priority_queue 堆 / 优先队列&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;定义：

priority_queue&amp;lt;T&amp;gt;
priority_queue&amp;lt;int&amp;gt; 大根堆
priority_queue&amp;lt;int, vector&amp;lt;int&amp;gt;, less&amp;lt;int&amp;gt; &amp;gt; 大根堆
priority_queue&amp;lt;int, vector&amp;lt;int&amp;gt;, greater&amp;lt;int&amp;gt; &amp;gt; 小根堆
priority_queue &amp;lt;struct T&amp;gt;

基本函数：

push(x)：加入一个元素，可以是数 or 结构体
pop()：弹出堆顶
top()：堆顶的元素
size()：堆的大小
empty()：是否为空（空即为 1）

关于结构体的比较：

struct type
{
	int x,y;
	friend bool operator &amp;lt; (type left, type right)
    {
        return left.x &amp;lt; right.x;
    }
};
结构体的赋值可以为{a,b,...}或名称{a,b,...}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;multiset&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;multiset vs set
    multiset 可以有重复元素，故一般情况下，（除解决重复元素的集合类问题）都用 multiset
	multiset 也进行自实现排序。

定义：

multiset&amp;lt;T&amp;gt;
multiset&amp;lt;int&amp;gt; 从小到大
multiset&amp;lt;int, less&amp;lt;int&amp;gt; &amp;gt; less&amp;lt;int&amp;gt;表示数字大的优先级大
multiset&amp;lt;int, greater&amp;lt;int&amp;gt; &amp;gt; greater&amp;lt;int&amp;gt;表示数字小的优先级大
multiset&amp;lt;struct T&amp;gt;

迭代器：

multiset&amp;lt;定义和对应的 set 一致&amp;gt; ::iterator，其作用是遍历 set/特别指向某一个元素

基本函数：

insert(x)：加入一个元素，可以是数/结构体
erase(x)：当x为数或结构体，即为删掉所有的x；当x 为迭代器，那么只会删掉迭代器对应的元素
begin()：返回关键值最小的元素指针，指针x对应的值为 *x，如果是结构体则为（*x）.a
end()：返回关键值最大的元素指针的后一位（最大的是end()--）
size(), empty()：同优先队列
lower_bound(x)：第一个大于等于 x 的元素指针
upper_bound(x)：第一个大于 x 的元素指针

multiset&amp;lt;T&amp;gt; st
st.insert(1);
st.insert(2);
st.insert(3);
st.insert(4);
st.insert(5);
cout&amp;lt;&amp;lt;*st.lower_bound(3)&amp;lt;&amp;lt;&quot; &quot; &amp;lt;&amp;lt;*st.upper_bound(3)&amp;lt;&amp;lt;endl;    //&amp;gt;= &amp;gt;
cout&amp;lt;&amp;lt;*--st.lower_bound(3)&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;*--st.upper_bound(3)&amp;lt;&amp;lt;endl; //&amp;lt; &amp;lt;=
//3 4
//2 3

遍历：
可以通过迭代器的移动来遍历（头为 begin()，尾为--end()，最大能走到 end()）

st.insert(1);
st.insert(2);
st.insert(3);
auto a = st.begin();
while (a != st.end())
{
    cout&amp;lt;&amp;lt; *a &amp;lt;&amp;lt;&quot; &quot;;
    ++a;
	cout&amp;lt;&amp;lt;endl;
}
//1 2 3区别
//multiset可以遍历、前驱、后继、删除；
//而priority_queue的比较机制和set/sort相反
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;完整示例程序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdlib&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;set&amp;gt;

#define fo(a, b, c) for (a = b; a &amp;lt;= c; a++)
#define fd(a, b, c) for (a = b; a &amp;gt;= c; a--)

using namespace std;

struct type
{
	int x,y;
	friend bool operator &amp;lt; (type left, type right)
    {
        return left.x &amp;lt; right.x;//此处不能重载大于号，在数学上来说重载小于号的作用更好
    }
};

multiset&amp;lt;type&amp;gt; a;
priority_queue&amp;lt;type&amp;gt; b;
type c[3];

int main()
{
    a.insert({3,3});
    a.insert({2,2});
    a.insert({1,1});
    b.push({1,1});
    b.push({2,2});
    b.push({3,3});
    c[0] = {3,3};
    c[1] = {2,2};
    c[2] = {1,1};
    sort(c, c + 3);
    cout &amp;lt;&amp;lt; (*a.begin()).x &amp;lt;&amp;lt;&quot; &quot; &amp;lt;&amp;lt; b.top().x &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; c[0].x &amp;lt;&amp;lt; endl;
    //1 3 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，priority_queue 得到的结果和 multiset/sort 刚好相反。&lt;/p&gt;
&lt;p&gt;实际上 multiset 与 sort 的最终状态满足 a1 &amp;lt; a2 &amp;lt; a3 &amp;lt; ... &amp;lt; an（&amp;lt; 可重载）。&lt;/p&gt;
&lt;p&gt;而 priority_queue 应该是当一个元素 x 满足 f(a[x]) &amp;lt; x 时交换，实质上维护的是大根堆。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;优先队列 ⇔ 排序后为先大后小。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;unordered_map（哈希表实现的映射）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;unordered_map 是一个关联容器，内部采用的是 hash 表结构，拥有快速检索的功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;特性&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;关联性：通过 key 去检索 value，而不是通过绝对地址（和顺序容器不同）&lt;/li&gt;
&lt;li&gt;无序性：使用 hash 表存储，内部无序&lt;/li&gt;
&lt;li&gt;Map：每个值对应一个键值&lt;/li&gt;
&lt;li&gt;键唯一性：不存在两个元素的键一样&lt;/li&gt;
&lt;li&gt;动态内存管理：使用内存管理模型来动态管理所需要的内存空间&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;模板&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt; class Key,                                    // unordered_map::key_type
           class T,                                      // unordered_map::mapped_type
           class Hash = hash&amp;lt;Key&amp;gt;,                       // unordered_map::hasher
           class Pred = equal_to&amp;lt;Key&amp;gt;,                   // unordered_map::key_equal
           class Alloc = allocator&amp;lt; pair&amp;lt;const Key,T&amp;gt; &amp;gt;  // unordered_map::allocator_type
           &amp;gt; class unordered_map;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般只使用模板前 2 个参数 &lt;code&gt;&amp;lt;Key, T&amp;gt;&lt;/code&gt;，即 &lt;code&gt;unordered_map&amp;lt;const Key, T&amp;gt; map;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;迭代器&lt;/h3&gt;
&lt;p&gt;unordered_map 的迭代器是一个指针，指向这个元素，通过迭代器来取得它的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unordered_map&amp;lt;Key, T&amp;gt;::iterator it;
(*it).first;             // the key value (of type Key)
(*it).second;            // the mapped value (of type T)
(*it);                   // the &quot;element value&quot; (of type pair&amp;lt;const Key,T&amp;gt;)

it -&amp;gt; first;  key       // 它的键值分别是迭代器的first和second属性
it -&amp;gt; second; T
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;构造函数&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;unordered_map 的构造方式有几种：构造空的容器、复制构造、范围构造、用数组构造&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
using namespace std;

typedef unordered_map&amp;lt;string, string&amp;gt; stringmap;

stringmap merge(stringmap a, stringmap b)
{
    stringmap temp(a);
    temp.insert(b.begin(), b.end());
    return temp;
}

int main()
{
    stringmap first;                                                // 空
    stringmap second({{&quot;apple&quot;, &quot;red&quot;}, {&quot;lemon&quot;, &quot;yellow&quot;}});      // 用数组初始
    stringmap third({{&quot;orange&quot;, &quot;orange&quot;}, {&quot;strawberry&quot;, &quot;red&quot;}}); // 用数组初始
    stringmap fourth(second);                                       // 复制初始化
    stringmap fifth(merge(third, fourth));                          // 移动初始化
    stringmap sixth(fifth.begin(), fifth.end());                    // 范围初始化

    cout &amp;lt;&amp;lt; &quot;sixth contains:&quot;;
    for (auto &amp;amp;x : sixth)
        cout &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; x.first &amp;lt;&amp;lt; &quot;:&quot; &amp;lt;&amp;lt; x.second;
    cout &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sixth contains: apple:red lemon:yellow orange:orange strawberry:red
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用成员函数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;size()：返回 unordered_map 的大小&lt;/li&gt;
&lt;li&gt;empty()：为空返回 true，不为空返回 false，和用 size() == 0 判断一样&lt;/li&gt;
&lt;li&gt;find()：查找 key 所在的元素。找到：返回元素的迭代器，通过迭代器的 second 属性获取值；没找到：返回 unordered_map::end&lt;/li&gt;
&lt;li&gt;insert()：复制插入（复制一个已有的 pair 的内容）、数组插入（直接插入一个初始化数组）、范围插入（复制一个起始迭代器和终止迭代器中间的内容）、数组访问模式插入（和数组的 [] 操作很相似）&lt;/li&gt;
&lt;li&gt;at()：查找 key 所对应的值。如果存在：返回 key 对应的值，可以直接修改，和 [] 操作一样；如果不存在：抛出 out_of_range 异常&lt;/li&gt;
&lt;li&gt;erase()：通过位置（迭代器）、通过 key、通过范围（两个迭代器）&lt;/li&gt;
&lt;li&gt;clear()：清空 unordered_map&lt;/li&gt;
&lt;li&gt;swap()：&lt;code&gt;void swap(unordered_map&amp;amp; ump);&lt;/code&gt; 交换两个 unordered_map（整个交换两个 map 中的所有元素）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
using namespace std;

void display(unordered_map&amp;lt;string, double&amp;gt; myrecipe, string str)
{
    cout &amp;lt;&amp;lt; str &amp;lt;&amp;lt; endl;
    for (auto &amp;amp;x : myrecipe)
        cout &amp;lt;&amp;lt; x.first &amp;lt;&amp;lt; &quot;: &quot; &amp;lt;&amp;lt; x.second &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; endl;
}

int main()
{
    unordered_map&amp;lt;string, double&amp;gt; myrecipe, mypantry = {
        {&quot;milk&quot;, 2.0},
        {&quot;flour&quot;, 1.5}
    };

    pair&amp;lt;string, double&amp;gt; myshopping(&quot;baking powder&quot;, 0.3);

    myrecipe.insert(myshopping);                             // 复制插入
    myrecipe.insert(make_pair&amp;lt;string, double&amp;gt;(&quot;eggs&quot;, 6.0)); // 移动插入
    myrecipe.insert(mypantry.begin(), mypantry.end());       // 范围插入
    myrecipe.insert({{&quot;sugar&quot;, 0.8}, {&quot;salt&quot;, 0.1}});        // 初始化数组插入
    myrecipe[&quot;coffee&quot;] = 10.0;                               // 数组形式插入

    display(myrecipe, &quot;myrecipe contains:&quot;);

    /****************查找*****************/
    unordered_map&amp;lt;string, double&amp;gt;::const_iterator got = myrecipe.find(&quot;coffee&quot;);
    if (got == myrecipe.end())
        cout &amp;lt;&amp;lt; &quot;not found&quot;;
    else
        cout &amp;lt;&amp;lt; &quot;found &quot; &amp;lt;&amp;lt; got-&amp;gt;first &amp;lt;&amp;lt; &quot; is &quot; &amp;lt;&amp;lt; got-&amp;gt;second &amp;lt;&amp;lt; &quot;\n\n&quot;;

    /****************修改*****************/
    myrecipe.at(&quot;coffee&quot;) = 9.0;
    myrecipe[&quot;milk&quot;] = 3.0;
    display(myrecipe, &quot;After modify myrecipe contains:&quot;);

    /****************擦除*****************/
    myrecipe.erase(myrecipe.begin()); //通过位置
    myrecipe.erase(&quot;milk&quot;);           //通过key
    display(myrecipe, &quot;After erase myrecipe contains:&quot;);

    /****************交换*****************/
    myrecipe.swap(mypantry);
    display(myrecipe, &quot;After swap with mypantry, myrecipe contains:&quot;);

    /****************清空*****************/
    myrecipe.clear();
    display(myrecipe, &quot;After clear, myrecipe contains:&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myrecipe contains:
salt: 0.1
milk: 2
flour: 1.5
coffee: 10
eggs: 6
sugar: 0.8
baking powder: 0.3

found coffee is 10

After modify myrecipe contains:
salt: 0.1
milk: 3
flour: 1.5
coffee: 9
eggs: 6
sugar: 0.8
baking powder: 0.3

After erase myrecipe contains:
flour: 1.5
coffee: 9
eggs: 6
sugar: 0.8
baking powder: 0.3

After swap with mypantry, myrecipe contains:
flour: 1.5
milk: 2

After clear, myrecipe contains:
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;begin() / end()&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;begin()：返回开始的迭代器；begin(int n)：返回 n 号 bucket 的第一个迭代器&lt;/p&gt;
&lt;p&gt;end()：返回结束位置的迭代器；end(int n)：返回 n 号 bucket 的最后一个迭代器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;bucket（桶）操作&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;bucket()：返回通过哈希计算 key 所在的 bucket。此处仅使用哈希计算确定 bucket，不保证 key 一定存在 bucket 中&lt;/li&gt;
&lt;li&gt;bucket_count()：返回 bucket 的总数&lt;/li&gt;
&lt;li&gt;bucket_size()：返回第 i 个 bucket 的大小。此位置的桶里的元素数量，但是函数并不会判断 n 是否在 count 范围内&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
using namespace std;

int main()
{
    unordered_map&amp;lt;string, string&amp;gt; mymap =
    {
            {&quot;house&quot;, &quot;maison&quot;},
            {&quot;apple&quot;, &quot;pomme&quot;},
            {&quot;tree&quot;, &quot;arbre&quot;},
            {&quot;book&quot;, &quot;livre&quot;},
            {&quot;door&quot;, &quot;porte&quot;},
            {&quot;grapefruit&quot;, &quot;pamplemousse&quot;}
    };

    /************begin和end迭代器***************/
    cout &amp;lt;&amp;lt; &quot;mymap contains:&quot;;
    for (auto it = mymap.begin(); it != mymap.end(); ++it)
        cout &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; it-&amp;gt;first &amp;lt;&amp;lt; &quot;:&quot; &amp;lt;&amp;lt; it-&amp;gt;second;
    cout &amp;lt;&amp;lt; endl;

    /************bucket操作***************/
    unsigned n = mymap.bucket_count();
    cout &amp;lt;&amp;lt; &quot;mymap has &quot; &amp;lt;&amp;lt; n &amp;lt;&amp;lt; &quot; buckets.\n&quot;;

    for (unsigned i = 0; i &amp;lt; n; ++i)
    {
        cout &amp;lt;&amp;lt; &quot;bucket #&quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &quot;&apos;s size:&quot;
             &amp;lt;&amp;lt; mymap.bucket_size(i) &amp;lt;&amp;lt; &quot; contains: &quot;;
        for (auto it = mymap.begin(i); it != mymap.end(i); ++it)
            cout &amp;lt;&amp;lt; &quot;[&quot; &amp;lt;&amp;lt; it-&amp;gt;first &amp;lt;&amp;lt; &quot;:&quot; &amp;lt;&amp;lt; it-&amp;gt;second &amp;lt;&amp;lt; &quot;] &quot;;
        cout &amp;lt;&amp;lt; &quot;\n&quot;;
    }

    cout &amp;lt;&amp;lt; &quot;\nkey:&apos;apple&apos; is in bucket #&quot; &amp;lt;&amp;lt; mymap.bucket(&quot;apple&quot;) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &quot;\nkey:&apos;computer&apos; is in bucket #&quot; &amp;lt;&amp;lt; mymap.bucket(&quot;computer&quot;) &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mymap contains: door:porte grapefruit:pamplemousse tree:arbre apple:pomme book:livre house:maison
mymap has 7 buckets.
bucket #0&apos;s size:2 contains: [book:livre] [house:maison]
bucket #1&apos;s size:0 contains:
bucket #2&apos;s size:0 contains:
bucket #3&apos;s size:2 contains: [grapefruit:pamplemousse] [tree:arbre]
bucket #4&apos;s size:0 contains:
bucket #5&apos;s size:1 contains: [apple:pomme]
bucket #6&apos;s size:1 contains: [door:porte]

key:&apos;apple&apos; is in bucket #5

key:&apos;computer&apos; is in bucket #6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;unordered_set（哈希表实现的集合）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;unordered_set 是一种关联容器。set 和 map 内部实现是基于红黑树（RedBlackTree），unordered_set 和 unordered_map 是基于哈希表（Hashtable）。红黑树有序，而哈希表无序。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;特性&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;不再以键值对的形式存储数据，而是直接存储数据的值（只有一个值）&lt;/li&gt;
&lt;li&gt;容器内部存储的各个元素的值都互不相等，且不能被修改&lt;/li&gt;
&lt;li&gt;不会对内部存储的数据进行排序&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;模板&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt; class Key,                        // unordered_set::key_type/value_type
           class Hash = hash&amp;lt;Key&amp;gt;,           // unordered_set::hasher
           class Pred = equal_to&amp;lt;Key&amp;gt;,       // unordered_set::key_equal
           class Alloc = allocator&amp;lt;Key&amp;gt;      // unordered_set::allocator_type
           &amp;gt; class unordered_set;

//一般定义使用
unordered_set&amp;lt;T&amp;gt; ans;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;迭代器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//返回头迭代器 begin()
unordered_set&amp;lt;int&amp;gt;::iterator it_begin = ans.begin();

//返回尾迭代器 end()
unordered_set&amp;lt;int&amp;gt;::iterator it_end = ans.end();

//返回const头迭代器 cbegin()
unordered_set&amp;lt;int&amp;gt;::const_iterator const_it_begin = ans.cbegin();

//返回const尾迭代器 cend()
unordered_set&amp;lt;int&amp;gt;::const_iterator const_it_end = ans.cend();

//槽迭代器
unordered_set&amp;lt;int&amp;gt;::local_iterator local_iter_begin = ans.begin(1);
unordered_set&amp;lt;int&amp;gt;::local_iterator local_iter_end = ans.end(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一般操作&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//查找函数 find() 通过给定主键查找元素
unordered_set&amp;lt;int&amp;gt;::iterator find_iter = ans.find(1);

//value出现的次数 count() 返回匹配给定主键的元素的个数
ans.count(1);

//返回元素在哪个区域 equal_range() 返回值匹配给定搜索值的元素组成的范围
pair&amp;lt;unordered_set&amp;lt;int&amp;gt;::iterator, unordered_set&amp;lt;int&amp;gt;::iterator&amp;gt;
    							   pair_equal_range = ans.equal_range(1);

//插入函数 emplace()
ans.emplace(1);

//插入函数 emplace_hint() 使用迭代器
ans.emplace_hint(it_begin, 1);

//插入函数 insert()
ans.insert(1);

//删除 erase()
ans.erase(1);//1.迭代器 value 区域

//清空 clear()
ans.clear();

//交换 swap()，括号内可接另外一个unordered_set
ans.swap();

//判断是否为空
ans.empty();

//获取元素个数 size()
ans.size();

//获取最大存储量 max_size()
ans.max_size();

//篮子操作 篮子个数 bucket_count() 返回槽（Bucket）数
ans.bucket_count();

//篮子最大数量 max_bucket_count() 返回最大槽数
ans.max_bucket_count();

//篮子个数 bucket_size() 返回槽大小
ans.bucket_size(3);

//返回篮子 bucket() 返回元素所在槽的序号
ans.bucket(1);

//load_factor 返回载入因子，即一个元素槽（Bucket）的最大元素数
ans.load_factor();

//max_load_factor 返回或设置最大载入因子
ans.max_load_factor();

//rehash 设置槽数
ans.rehash(1);

//reserve 请求改变容器容量
ans.reserve(1000);

//hash_function() 返回与hash_func相同功能的函数指针
auto hash_func_test = ans.hash_function();

//key_eq() 返回比较key值得函数指针
auto key_eq_test = ans.key_eq();
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;参考：C++ STL 函数库（&lt;a href=&quot;https://www.cplusplus.com/reference/&quot;&gt;https://www.cplusplus.com/reference/&lt;/a&gt;）&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Doris 原理、部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/11/doris-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/doris-deploy-tuning/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Doris 是 MPP（Massively Parallel Processing，大规模并行处理）分析型数据库，定位&quot;在线报表 + 实时分析&quot;，主打高并发点查与多表 Join。本文覆盖底层原理、集群部署（FE/BE）、参数优化与常见问题，作为湖仓时代 OLAP 引擎的第一块拼图。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理速览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MPP 架构&lt;/strong&gt;：一条 SQL 拆成多个子任务在多个 BE 上并行执行，结果汇总返回，吞吐随节点数线性扩展&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FE / BE 分离&lt;/strong&gt;：FE（Frontend）管元数据、查询规划、导入调度；BE（Backend）管存储与执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;列式存储 + 向量化执行&lt;/strong&gt;：按列存储天然适配分析聚合；SIMD 向量化让单核性能成倍提升&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tablet 分片&lt;/strong&gt;：表按分区（Range）+ 分桶（Hash）切成 Tablet，是数据分布、副本与均衡的最小单元&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多副本 + Quorum&lt;/strong&gt;：默认 3 副本，写多数派成功即成功，读可走任意副本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;导入体系&lt;/strong&gt;：Stream Load（HTTP）、Broker Load（从外部系统）、Routine Load（Kafka 持续消费）、Insert Into&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compaction&lt;/strong&gt;：后台把多个版本的小数据文件合并成大文件，控制文件数与读放大&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Doris 的定位一句话：&lt;strong&gt;&quot;能查准的 ClickHouse&quot;&lt;/strong&gt;。它有完善的 SQL/Join/事务语义，不像 Hive 那样重，也不像 ES 那样搜。适合承接&quot;报表、即席分析、实时数仓汇总层&quot;。
:::&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;h3&gt;1. 规划&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;节点&lt;/th&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;数量&lt;/th&gt;
&lt;th&gt;规格参考（测试/生产）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FE&lt;/td&gt;
&lt;td&gt;Leader/Follower/Observer&lt;/td&gt;
&lt;td&gt;1+1+1（生产）&lt;/td&gt;
&lt;td&gt;4C8G / 16C64G（元数据放 SSD）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BE&lt;/td&gt;
&lt;td&gt;数据与计算&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;td&gt;8C16G / 32C256G&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;目录规划：FE 的 &lt;code&gt;meta_dir&lt;/code&gt;、BE 的 &lt;code&gt;storage_root_path&lt;/code&gt; 放独立磁盘（SSD）&lt;/li&gt;
&lt;li&gt;端口：FE 9030（MySQL 协议）/ 8030（Web UI）/ 8040（BE 心跳）；BE 9060/8040&lt;/li&gt;
&lt;li&gt;时间同步、关闭 swap、设置 &lt;code&gt;vm.max_map_count&lt;/code&gt;（参考调优）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. FE 部署（部署 3 台，1 Leader + 2 Follower）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 下载并解压（选择与集群版本一致的稳定版）
wget https://apache-doris-releases.oss-accelerate.aliyuncs.com/apache-doris-2.1.x-bin-x64.tar.gz
tar -zxvf apache-doris-*.tar.gz -C /opt &amp;amp;&amp;amp; ln -s /opt/apache-doris-* /opt/doris
mkdir -p /data/doris/meta &amp;amp;&amp;amp; chown -R doris:doris /opt/doris /data/doris
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# fe/conf/fe.conf 关键项
meta_dir = /data/doris/meta
priority_networks = 10.0.0.0/24        # 必须显式指定，避免多网卡选错
JAVA_OPTS=&quot;-Xmx16g -Xms16g&quot;            # FE 内存按元数据量给
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 第一台：启动并初始化集群
fe/bin/start_fe.sh --daemon
mysql -h127.0.0.1 -P9030 -uroot            # 首次无密码

# 其余两台：加入集群
mysql&amp;gt; ALTER SYSTEM ADD FOLLOWER &quot;node2:9010&quot;;
mysql&amp;gt; ALTER SYSTEM ADD FOLLOWER &quot;node3:9010&quot;;
# 每台执行 start_fe.sh --helper node1:9010 --daemon 完成加入
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. BE 部署（3 台）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# be/conf/be.conf 关键项
storage_root_path = /data/doris/data1,medium:ssd;/data/doris/data2,medium:hdd
priority_networks = 10.0.0.0/24
be_port = 9060
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 启动 BE 并加入集群
be/bin/start_be.sh --daemon
mysql&amp;gt; ALTER SYSTEM ADD BACKEND &quot;node1:9050&quot;;
mysql&amp;gt; ALTER SYSTEM ADD BACKEND &quot;node2:9050&quot;;
mysql&amp;gt; ALTER SYSTEM ADD BACKEND &quot;node3:9050&quot;;
mysql&amp;gt; SHOW BACKENDS;    # 全部 Alive 即集群就绪
mysql&amp;gt; SHOW FRONTENDS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
验证就绪的标准：&lt;code&gt;SHOW BACKENDS&lt;/code&gt; 全 Alive、Web UI（8030）可访问、&lt;code&gt;SHOW TABLET&lt;/code&gt; 无异常。首次建表会触发 Tablet 均衡，耐心等副本补齐。
:::&lt;/p&gt;
&lt;h3&gt;4. 建表与导入验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE dwd_order (
  order_id   BIGINT NOT NULL,
  user_id    BIGINT,
  amount     DECIMAL(20,2),
  dt         DATE
)
DUPLICATE KEY(order_id)
PARTITION BY RANGE(dt) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 16
PROPERTIES (&quot;replication_num&quot; = &quot;3&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Stream Load 快速导入（HTTP）
curl -u root: --location-trusted \
  -H &quot;label:demo_001&quot; -H &quot;column_separator:,&quot; \
  -T /tmp/order.csv \
  http://node1:8030/api/demo/dwd_order/_stream_load
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调优&lt;/h2&gt;
&lt;h3&gt;1. 内存&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BE &lt;code&gt;mem_limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;节点物理内存的 60%-70%&lt;/td&gt;
&lt;td&gt;BE 进程可用上限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BE &lt;code&gt;load_process_max_memory_limit_percent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;导入占用上限，避免挤占查询&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FE &lt;code&gt;JAVA_OPTS -Xmx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按元数据规模 8G-32G&lt;/td&gt;
&lt;td&gt;元数据在 FE 内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询 &lt;code&gt;exec_mem_limit&lt;/code&gt;（会话级）&lt;/td&gt;
&lt;td&gt;按 SQL 复杂度&lt;/td&gt;
&lt;td&gt;单查询内存上限，防止大查询打爆 BE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. Tablet 与 Compaction&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分桶数&lt;/strong&gt;：单个 Tablet 数据量控制在 1-10G，桶数 = 数据量 / 单桶目标（如 2G），改小后新分区生效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compaction&lt;/strong&gt;：&lt;code&gt;compaction_checker_interval_second&lt;/code&gt;（默认 10s）、&lt;code&gt;min_compaction_failure_interval_second&lt;/code&gt;；大表可调大 &lt;code&gt;max_compaction_concurrency&lt;/code&gt;（默认 1 即可）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;均衡&lt;/strong&gt;：&lt;code&gt;SHOW BACKENDS&lt;/code&gt; 观察磁盘水位，&lt;code&gt;ADMIN SET FRONTEND CONFIG (&quot;tablet_scheduler_max_balancing_tablets&quot;=&quot;200&quot;)&lt;/code&gt; 加快均衡&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 查询&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;物化视图&lt;/strong&gt;：高频聚合（如按天 sum）建物化视图，Doris 自动命中，替代手动同步聚合表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分桶键选择&lt;/strong&gt;：高频等值过滤列（如 user_id）作分桶键，Join 时若两边同分桶键可走 Bucket Shuffle 免网络&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Join 优化&lt;/strong&gt;：小表放右侧（默认 broadcast）、大表间用 shuffle join；&lt;code&gt;SET enable_cost_based_join_reorder=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向量化&lt;/strong&gt;：默认开启；&lt;code&gt;SET enable_vectorized_engine=true&lt;/code&gt; 兜底老版本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workload Group&lt;/strong&gt;：不同业务建不同资源组（CPU/内存/并发配额），避免大查询拖垮在线报表&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 导入&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Routine Load&lt;/strong&gt;：Kafka 持续导入是实时数仓标配，&lt;code&gt;max_routine_load_job_num&lt;/code&gt; 按需调大；单任务 &lt;code&gt;max_batch_rows&lt;/code&gt;（默认 20 万）可提升吞吐&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stream Load&lt;/strong&gt;：客户端并发 5-10，超并发反而触发导入排队；&lt;code&gt;strict_mode=true&lt;/code&gt; 拒绝脏数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小文件&lt;/strong&gt;：导入频率高导致版本多，配合 compaction 观察 &lt;code&gt;SHOW TABLET&lt;/code&gt; 的 version 数量&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BE 显示 Offline&lt;/td&gt;
&lt;td&gt;心跳端口不通、磁盘满、进程挂了&lt;/td&gt;
&lt;td&gt;检查 9050/8040 连通、&lt;code&gt;SHOW BACKENDS&lt;/code&gt; 看 LastStartTime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tablet 状态异常&lt;/td&gt;
&lt;td&gt;副本数不足或磁盘坏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ADMIN SHOW REPLICA STATUS&lt;/code&gt; 定位，等均衡自动修复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;导入 Label 已存在&lt;/td&gt;
&lt;td&gt;幂等机制生效&lt;/td&gt;
&lt;td&gt;换 label 或看是否真的失败需清理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询报内存超限&lt;/td&gt;
&lt;td&gt;exec_mem_limit 过小&lt;/td&gt;
&lt;td&gt;会话级调大，或改 Workload Group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;表数据膨胀快&lt;/td&gt;
&lt;td&gt;分区粒度太细 / 桶数太多&lt;/td&gt;
&lt;td&gt;分区按周/月，桶数按 1-10G 目标重排&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Doris = FE 元数据 + BE 存储执行，MPP + 列存 + 向量化，定位在线分析与实时数仓汇总层&lt;/li&gt;
&lt;li&gt;部署三步：FE 集群 → BE 集群 → 建表导入验证；&lt;code&gt;priority_networks&lt;/code&gt; 和磁盘规划是常见坑&lt;/li&gt;
&lt;li&gt;调优主线：内存（mem_limit/exec_mem_limit）→ Tablet 与 Compaction → 分桶键与 Join → Workload Group 隔离&lt;/li&gt;
&lt;li&gt;在底座迭代思路中，Doris 是&quot;湖仓一体&quot;阶段替换 Hive 数仓查询路径的首选引擎&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>企业级大数据底座技术架构与未来迭代思路</title><link>https://chaggle.github.io/posts/2026/08/11/datasophon-architecture-iteration/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/datasophon-architecture-iteration/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;上一篇对比了本地 DataSophon（2.0.0）与开源增强版（3.0-SNAPSHOT）的差异。本文跳出版本之争，回答一个更本质的问题：当前企业级大数据底座的技术架构长什么样？未来 3-5 年产品如何迭代？是继续在 DataSophon 上投入，还是走所谓的技术转型？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、当前企业级大数据底座的技术架构&lt;/h2&gt;
&lt;p&gt;不管上层用什么平台管理，企业级大数据底座的物理构成大同小异，可以按七层来拆：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层次&lt;/th&gt;
&lt;th&gt;承载组件（典型）&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;基础设施层&lt;/td&gt;
&lt;td&gt;裸机 / 虚拟化 / 私有云&lt;/td&gt;
&lt;td&gt;CPU、内存、磁盘、网络，决定底座的上限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;资源与调度层&lt;/td&gt;
&lt;td&gt;YARN / K8s / 统一调度器&lt;/td&gt;
&lt;td&gt;计算资源的分配、隔离、抢占&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储层&lt;/td&gt;
&lt;td&gt;HDFS / 对象存储 / JuiceFS 等&lt;/td&gt;
&lt;td&gt;数据的可靠存储与扩展&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;计算引擎层&lt;/td&gt;
&lt;td&gt;Spark、Flink、MapReduce、Trino、Doris/StarRocks&lt;/td&gt;
&lt;td&gt;批、流、交互式、OLAP 四类计算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元数据与数据管理层&lt;/td&gt;
&lt;td&gt;Hive Metastore、Gravitino、数据质量/血缘工具&lt;/td&gt;
&lt;td&gt;Catalog、血缘、质量、生命周期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全体系&lt;/td&gt;
&lt;td&gt;Kerberos（认证）、Ranger（授权）、TLS&lt;/td&gt;
&lt;td&gt;认证、鉴权、审计、加密&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可观测与平台管理&lt;/td&gt;
&lt;td&gt;Prometheus/Grafana/AlertManager、DataSophon&lt;/td&gt;
&lt;td&gt;监控、日志、告警、部署、配置、扩缩容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
DataSophon 这类平台软件（本地 2.0.0 管理 21 个组件）管的是最后两层之下的事情：把组件装上、配好、启停、巡检、扩缩容。它不改变引擎本身，改变的是&quot;底座的可运维性&quot;——这正是企业自建大数据平台与&quot;手工装 Hadoop&quot;的本质区别。
:::&lt;/p&gt;
&lt;h3&gt;核心链路视角&lt;/h3&gt;
&lt;p&gt;一个企业级底座，运行的核心链路通常是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据接入（Flume/Kafka/DataX）→ 数据湖仓（HDFS/对象存储 + Hive/Iceberg）
→ 离线计算（Spark/MapReduce） + 实时计算（Flink）
→ 服务化（Kyuubi/Trino/Doris）→ 下游应用与 BI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安全体系（Kerberos + Ranger）贯穿全链路，可观测（Prometheus + Grafana）旁挂全链路，DataSophon 负责链路上每个组件的生命周期。&lt;/p&gt;
&lt;h2&gt;二、传统底座的能力地图与短板&lt;/h2&gt;
&lt;p&gt;以本地 2.0.0 这套体系（HDFS+YARN+Hive+Spark3+Flink+Kafka+Kerberos+Ranger+监控）为例：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;成熟点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;组件全：21 个组件覆盖存储、计算、调度、元数据、安全、监控全栈&lt;/li&gt;
&lt;li&gt;安全体系完整：Kerberos 认证 + Ranger 授权，政企合规场景的硬通货&lt;/li&gt;
&lt;li&gt;稳定性经过长期生产验证，问题定位经验积累充分&lt;/li&gt;
&lt;li&gt;元数据驱动管理（service_ddl.json），组件扩展有章可循&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;短板（决定未来迭代方向）：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;短板&lt;/th&gt;
&lt;th&gt;表现&lt;/th&gt;
&lt;th&gt;行业对应解法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;弹性差&lt;/td&gt;
&lt;td&gt;扩缩容要改配置、跑脚本，分钟级起步&lt;/td&gt;
&lt;td&gt;云原生：K8s 调度 + 自动扩缩容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存算耦合&lt;/td&gt;
&lt;td&gt;计算和存储绑在同一批节点，扩计算要加存储&lt;/td&gt;
&lt;td&gt;存算分离：对象存储 + 湖格式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;版本老化&lt;/td&gt;
&lt;td&gt;HDFS 3.3.6、Spark 3.4.3，新特性滞后&lt;/td&gt;
&lt;td&gt;组件版本升级与替换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数仓与湖割裂&lt;/td&gt;
&lt;td&gt;Hive 数仓与数据湖各自为政&lt;/td&gt;
&lt;td&gt;湖仓一体：Iceberg/Paimon + Doris 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 能力缺失&lt;/td&gt;
&lt;td&gt;无模型服务、无特征平台&lt;/td&gt;
&lt;td&gt;Data+AI 融合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;运维人效&lt;/td&gt;
&lt;td&gt;巡检靠人，故障响应靠经验&lt;/td&gt;
&lt;td&gt;平台自动化 + AIOps + CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;三、行业演进方向&lt;/h2&gt;
&lt;p&gt;未来 3-5 年，企业级大数据底座有四个确定性方向：&lt;/p&gt;
&lt;h3&gt;1、存算分离&lt;/h3&gt;
&lt;p&gt;计算层与存储层解耦：数据放对象存储（或 JuiceFS 这类分布式缓存），计算集群按需伸缩，扩缩容不再受数据搬迁拖累。HDFS 从&quot;事实标准&quot;退为&quot;兼容层&quot;。&lt;/p&gt;
&lt;h3&gt;2、湖仓一体&lt;/h3&gt;
&lt;p&gt;以 Iceberg / Paimon / Hudi 为核心的开源湖格式 + 高性能分析引擎（Doris/StarRocks）取代&quot;Hive 数仓 + 手动同步&quot;的割裂架构。元数据服务（如 Gravitino）统一管理多 Catalog。&lt;/p&gt;
&lt;h3&gt;3、云原生&lt;/h3&gt;
&lt;p&gt;组件容器化 + K8s 调度（YARN on K8s、Spark on K8s、Flink on K8s），资源池化、弹性伸缩、多集群统一管理。平台软件本身也 K8s 化（3.0 增强版已支持 Helm + k8s-agent）。&lt;/p&gt;
&lt;h3&gt;4、Data + AI&lt;/h3&gt;
&lt;p&gt;数据平台从&quot;支撑 BI&quot;走向&quot;支撑模型&quot;：特征工程平台、向量检索、模型服务、大模型微调与推理的数据管道。元数据管理升级为数据资产的智能化（血缘自动分析、质量自动稽核）。&lt;/p&gt;
&lt;p&gt;:::warning
这四个方向不是选择题，是时间差。存算分离和湖仓一体是&quot;现在就该动&quot;的，云原生是&quot;中期必做&quot;的，Data+AI 是&quot;要提前布局&quot;的。底座架构的迭代思路，本质就是按这个节奏排期。
:::&lt;/p&gt;
&lt;h2&gt;四、决策分析：继续 DataSophon 迭代 vs 技术转型&lt;/h2&gt;
&lt;h3&gt;继续迭代的场景&lt;/h3&gt;
&lt;p&gt;以下情况，留在 DataSophon 体系内继续投入是合理选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;团队已熟练&lt;/strong&gt;：对 2.0.0 的元数据模型、worker 策略类、DDP 包结构吃得很透，扩展一个组件就是&quot;写一个 service_ddl + 一个 strategy&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件生态正好匹配&lt;/strong&gt;：安全体系（Kerberos/Ranger）是业务硬要求，而这恰恰是开源增强版 3.0 缺失的部分&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无云原生诉求&lt;/strong&gt;：机器都在机房里，短期内没有容器化和弹性扩缩容的压力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预算与人手有限&lt;/strong&gt;：技术转型的隐性成本（学习、迁移、双跑）高于升级成本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;继续迭代的路径有三条，按投入递增：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;路径&lt;/th&gt;
&lt;th&gt;做法&lt;/th&gt;
&lt;th&gt;适用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;组件扩展&lt;/td&gt;
&lt;td&gt;在 2.0.0 上加新组件（如 Nacos、JuiceFS 的 service_ddl 移植）&lt;/td&gt;
&lt;td&gt;只缺组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;平台升级&lt;/td&gt;
&lt;td&gt;评估迁移到开源增强版 3.0（Java 21/gRPC/K8s 化，需补 Kerberos/Ranger）&lt;/td&gt;
&lt;td&gt;想要现代架构&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自研增强&lt;/td&gt;
&lt;td&gt;在现有平台上叠加 CI/CD、巡检自动化、参数基线库&lt;/td&gt;
&lt;td&gt;平台稳定，要提人效&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;技术转型的场景&lt;/h3&gt;
&lt;p&gt;出现以下信号，就要认真考虑转型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;业务要求云原生&lt;/strong&gt;：客户或上层的诉求是&quot;大数据要能上云、能弹性&quot;——DataSophon 体系的裸机心智模型天然吃亏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据规模与形态变化&lt;/strong&gt;：湖仓一体、多模态数据成为主流诉求，Hive 数仓的心智模型老化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;招人难&lt;/strong&gt;：Hadoop 体系人才供给收缩，招聘以 Lakehouse / K8s / 云厂商体系为主&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平台演进停滞&lt;/strong&gt;：上游社区对原生 DataSophon 的维护力度有限，长期依赖内部定制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;转型方向有三个主流选项：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方向&lt;/th&gt;
&lt;th&gt;代表&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;湖仓原生体系&lt;/td&gt;
&lt;td&gt;开源：Doris/StarRocks + Iceberg/Paimon + Gravitino&lt;/td&gt;
&lt;td&gt;轻、快、云原生友好，自主可控&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;商业发行版&lt;/td&gt;
&lt;td&gt;CDP、云厂商大数据平台（E-MapReduce/EMR、DataWorks）&lt;/td&gt;
&lt;td&gt;组件全、有支持，成本高、绑定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;云原生数据栈&lt;/td&gt;
&lt;td&gt;Spark/Flink on K8s + 对象存储 + 统一调度（Volcano）&lt;/td&gt;
&lt;td&gt;弹性最好，工程复杂度最高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;决策矩阵&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;继续迭代（DataSophon）&lt;/th&gt;
&lt;th&gt;技术转型（湖仓/云原生）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;稳定性&lt;/td&gt;
&lt;td&gt;高（已验证）&lt;/td&gt;
&lt;td&gt;中（需重新验证）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;组件生态&lt;/td&gt;
&lt;td&gt;全但偏老&lt;/td&gt;
&lt;td&gt;新但不全（安全体系要补）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;云原生能力&lt;/td&gt;
&lt;td&gt;弱（需改造）&lt;/td&gt;
&lt;td&gt;强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 支撑&lt;/td&gt;
&lt;td&gt;弱&lt;/td&gt;
&lt;td&gt;中强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;迁移成本&lt;/td&gt;
&lt;td&gt;低-中&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;人才可得性&lt;/td&gt;
&lt;td&gt;中（圈子收窄）&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全合规&lt;/td&gt;
&lt;td&gt;强（Kerberos/Ranger 成熟）&lt;/td&gt;
&lt;td&gt;中（方案需自建）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip
结论先行：&lt;strong&gt;转型不是二选一，而是分步走。&lt;/strong&gt; 平台管理软件（DataSophon）继续用、继续迭代，解决&quot;底座怎么管&quot;；组件体系逐步引入湖仓与云原生组件，解决&quot;底座是什么&quot;。平台是腿，组件是车，换车不必换腿。
:::&lt;/p&gt;
&lt;h2&gt;五、推荐的 3-5 年路线图&lt;/h2&gt;
&lt;h3&gt;阶段一：平台现代化（0-1 年）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;平台本体：评估 DataSophon 升级（或继续自研增强），建立参数基线库与巡检自动化&lt;/li&gt;
&lt;li&gt;引入 CI/CD：Jenkins（或 GitLab CI）打通&quot;配置变更 → 构建 → 发布&quot;链路，组件部署从手工变为流水线（详见《Jenkins 原理、部署与调优指南》）&lt;/li&gt;
&lt;li&gt;可观测补齐：指标（Prometheus）+ 日志（Loki/ELK）+ 链路（OTel）三件套，故障定位从&quot;翻命令&quot;变&quot;查面板&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;阶段二：存算分离与湖仓改造（1-2 年）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新业务数据直接入对象存储 + Iceberg/Paimon，不再扩 HDFS&lt;/li&gt;
&lt;li&gt;引入 Doris/StarRocks 承载报表与分析，逐步替换 Hive 数仓的查询路径&lt;/li&gt;
&lt;li&gt;用 Gravitino 类元数据服务统一多 Catalog，为 AI 数据管道打底&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;阶段三：云原生与统一调度（2-3 年）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;新组件一律容器化，Spark/Flink on K8s 先行，YARN 集群只保老任务&lt;/li&gt;
&lt;li&gt;平台管理逐步 K8s 化（对标开源增强版 3.0 的 k8s-agent 思路）&lt;/li&gt;
&lt;li&gt;资源池化，实现计算层弹性伸缩&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;阶段四：Data + AI 融合（3-5 年）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;特征平台 + 模型服务化：数据平台为模型提供高质量数据管道&lt;/li&gt;
&lt;li&gt;元数据智能化：血缘自动分析、质量自动稽核、资源智能调优（AIOps）&lt;/li&gt;
&lt;li&gt;平台管理从&quot;部署工具&quot;进化为&quot;数据资产运营平台&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三条原则&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不推倒重来&lt;/strong&gt;：Hive 老数仓按生命周期自然淘汰，新能力用增量方式引入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双轨并行&lt;/strong&gt;：新旧体系并存期，以数据链路打通为准绳，逐步迁移业务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件解耦&lt;/strong&gt;：平台（DataSophon）与组件（引擎）解耦演进，平台升级不影响引擎，引擎替换不依赖平台&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;六、总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;企业级大数据底座 = 七层架构，平台管理软件（DataSophon）解决的是&quot;可运维性&quot;，它决定不了架构方向&lt;/li&gt;
&lt;li&gt;未来四方向：存算分离、湖仓一体、云原生、Data+AI，按时间差排期&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继续迭代 vs 技术转型不是对立&lt;/strong&gt;：平台继续用 DataSophon（解决&quot;怎么管&quot;），组件体系向湖仓/云原生演进（解决&quot;是什么&quot;）&lt;/li&gt;
&lt;li&gt;落地顺序：先平台现代化（含 CI/CD），再存算分离与湖仓，再云原生，最后 Data+AI；过程中始终双轨并行、增量演进&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话：&lt;strong&gt;把腿（DataSophon）练扎实，把车（组件体系）换到湖仓与云原生，把方向盘（AI）提前握好。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Go 并发与内存学习笔记</title><link>https://chaggle.github.io/posts/2026/08/11/go-concurrency-memory/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/go-concurrency-memory/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;2022 年学习 Go 并发编程的五篇笔记（GMP 模型、垃圾回收、channel、sync.Mutex、sync.Pool）合并整理。内容主要参考极客时间 Go 并发编程实战课（鸟窝大佬），如有需要查看其详细内容，请前去购买。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;GMP 模型分析&lt;/h2&gt;
&lt;p&gt;关于 GMP 模型的一些理解，G 为 goroutine，M 为 thread（内核级线程）、P 为 Processor（处理器）。&lt;/p&gt;
&lt;h3&gt;Golang 早期调度器的由来&lt;/h3&gt;
&lt;p&gt;在讲述早期调度器之前，让我们先聊一下早期的操作系统。&lt;/p&gt;
&lt;p&gt;在没有多核 CPU 之前，操作系统以单进程的任务执行，计算机只能一个任务一个任务地完整执行。在此情况下，操作系统不仅存在工作效率低下的问题，而且一旦正在执行的任务被阻塞时，CPU 资源无法释放，会导致其 CPU 资源与时间的浪费，因此操作系统采用了时间片轮询的方式调度进程，而此举动也无法改变单核 CPU 的硬件条件。&lt;/p&gt;
&lt;p&gt;为了解决上述缺陷，随后引入了多进程、多线程的解决方式。虽然多进程与多线程的方式很好地解决了 CPU 调度的效率问题，但是设计多进程、多线程的架构会变得异常复杂。不仅如此，当进程与线程数量越多的时候，多进程多线程系统进行进程线程切换的成本就越大，资源浪费现象也越明显：如（锁、竞争资源冲突等），所以在多进程、多线程模型中也存在相应的壁垒，即高内存占用与高 CPU 调度消耗。&lt;/p&gt;
&lt;p&gt;而 Go 语言为了更好的解决操作系统中线程调度的开销大，引入了比线程更轻量级的协程，其内存占用一般只有 4KB，调度灵活，切换成本低。并且开发出 Go 语言早期的调度器：基本的全局 Go 队列和比较传统的轮询方法，利用多个 M 进行 G 的调度，可以称其为 GM 模型。&lt;/p&gt;
&lt;h3&gt;GM 早期调度器的缺点&lt;/h3&gt;
&lt;p&gt;Go 语言早期的调度器 GM 模型虽然能够利用多核的 CPU，但是其相应的缺点也非常明显：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;1、创建、销毁、调度 G 都需要每个 M 获取锁，形成了激烈的锁竞争&lt;/li&gt;
&lt;li&gt;2、M 调度 G 时，会造成延迟以及额外的系统负载&lt;/li&gt;
&lt;li&gt;3、系统调用（CPU 在 M 之间的切换）导致频繁的线程阻塞和取消操作，增加了系统开销&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以为了解决以上的问题，引入了 GMP 模型。&lt;/p&gt;
&lt;h3&gt;GMP 现代调度器简介&lt;/h3&gt;
&lt;p&gt;GMP 现代调度器，采用了两种队列：全局队列以及本地队列：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/GMP2.png&quot; alt=&quot;GMP&quot; /&gt;&lt;/p&gt;
&lt;p&gt;全局队列很好理解，可视为全局变量，本地队列则能理解为局部变量。本地队列的个数依赖于 P 的个数，即 GOMAXPROCS 的个数，此值由启动时环境变量 &lt;code&gt;$GOMAXPROCS&lt;/code&gt; 或者是由 &lt;code&gt;runtime&lt;/code&gt; 的方法 &lt;code&gt;GOMAXPROCS()&lt;/code&gt; 决定。这意味着在程序执行的任意时刻都只有 &lt;code&gt;$GOMAXPROCS&lt;/code&gt; 个 goroutine 在同时运行。每一个 P 的本地队列中能存放 G 的个数不超过 256 个。新建的 G 一般优先放置在 P 的本地队列中。&lt;/p&gt;
&lt;p&gt;Go 语言本身限定 M 数量的最大量为 10000（忽略），一般操作系统也达不到 10000 个线程。使用 runtime/debug 包中的 SetMaxThreads 函数来设置。有一个 M 阻塞，就会创建一个新的 M，如果有 M 空闲，则会进行回收或者睡眠。&lt;/p&gt;
&lt;h3&gt;GMP 调度策略&lt;/h3&gt;
&lt;p&gt;设计策略有以下四种：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、线程复用：采用两种机制：work stealing 机制与 hand off 机制。&lt;/p&gt;
&lt;p&gt;work stealing 机制：当本线程无可运行的 G 时，尝试从其他的线程绑定的 P 偷取 G，而不是销毁线程，优先级是先从全局队列中获取 G，再从其他的 P 的本地队列中获取 G。&lt;/p&gt;
&lt;p&gt;hand off 机制：当本线程因为 G 进行系统调用阻塞的时候，线程释放绑定的 P，把 P 转移给其他空闲的线程执行。&lt;/p&gt;
&lt;p&gt;2、并行应用：一般来说，GOMAXPROCS 限定的 P 个数为 CPU 的核心数量的一半。&lt;/p&gt;
&lt;p&gt;3、抢占：有多个 G 等待执行时候，每个 G 在 CPU 执行下不超过 10ms，防止其他 G 被饿死现象。&lt;/p&gt;
&lt;p&gt;4、全局 G 队列：基于 work stealing 机制进行的补充。从其他 P 本地队列偷不到 G 时，偷取全局队列的 G。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Golang 垃圾回收机制&lt;/h2&gt;
&lt;h3&gt;Go V1.3 版本之前的标记清除（mark and sweep）&lt;/h3&gt;
&lt;p&gt;此版本的垃圾回收机制，一般分为以下四步：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、暂停程序业务逻辑，找出不可达的对象以及可达的对象&lt;/p&gt;
&lt;p&gt;2、开始标记，程序找出其所有可达的对象，并进行标记&lt;/p&gt;
&lt;p&gt;3、标记完之后，开始清除未标记的对象&lt;/p&gt;
&lt;p&gt;4、继续运行程序。循环以上的过程，直到程序的生命周期终止为止&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是相应的，V1.3 版本中的垃圾回收的问题也较大：首先有个 STW 暂停程序，这会浪费大量的时间去处理垃圾回收，不利于效率的提升，其次每一次的标记需要扫描整个 Heap 堆区，而且清除相关的数据也会产生内存堆的碎片。&lt;/p&gt;
&lt;h3&gt;Go v1.5 版本的三色标记法&lt;/h3&gt;
&lt;p&gt;此时，垃圾回收机制将运行中的程序状态分为：白、黑、灰三种状态：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、每一步默认创建的对象均标记为&quot;白色&quot;&lt;/p&gt;
&lt;p&gt;2、每一次 GC 回收的时候，均会从根节点遍历所有的对象，而且把遍历的对象从白色的集合放入&quot;灰色&quot;的集合中，遍历为非递归形式&lt;/p&gt;
&lt;p&gt;3、遍历灰色的集合，将灰色对象引用的对象从白色集合中放入灰色集合，然后将灰色对象放入黑色集合中&lt;/p&gt;
&lt;p&gt;4、重复第三步，直到灰色集合中无任何对象&lt;/p&gt;
&lt;p&gt;5、回收所有白色集合中的对象，即回收垃圾&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是此处有两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、如果黑色对象之间直接引用指向一个白色的对象，那么在第二步的时候，此白色对象并不能进行相应的染色，所以说此处会导致白色对象被清除&lt;/li&gt;
&lt;li&gt;2、如果之前白色对象被灰色对象引用，而在扫描时系统发生了故障，导致灰色对象引用白色对象的指针丢失，导致白色对象被清除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以这样也引入了相应的优化方法：插入屏障与删除屏障机制。&lt;/p&gt;
&lt;h3&gt;Go 插入写屏障、删除写屏障&lt;/h3&gt;
&lt;p&gt;插入写屏障主要应用为：在强三色不变式下（黑色直接引用白色的对象），那么此时，使用插入写屏障机制，强行改写黑色引用对象白色为灰色。但是插入写屏障机制会有几个缺点：需要重新扫描栈，大约 10 ms ~ 100 ms，也消耗了相应的系统资源。&lt;/p&gt;
&lt;p&gt;删除写屏障主要应用为：在弱三色不变式下（黑色引用白色对象，白色对象其他上游的引用对象有灰色对象进行引用）。其也存在相应的不足：回收精度低、一个对象即使被删除了最后一个指向它的指针也依旧能活过这一轮，在下一轮的 GC 中被清除。&lt;/p&gt;
&lt;h3&gt;Go V1.8 三色标记法与混合写屏障&lt;/h3&gt;
&lt;p&gt;混合标记法其实很容易理解，就是把插入写屏障与删除写屏障混合起来进行使用，其相应的步骤如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、在 Go GC 触发之前，递归扫描分配在栈上的对象，使栈上的对象全部标记为&quot;黑色&quot;（此处因为是标记为全部&quot;黑色&quot;，所以不会使用 STW 机制）&lt;/p&gt;
&lt;p&gt;2、在 Go GC 运行期间，如果往栈上插入对象，将栈上的对象全部标记为&quot;黑色&quot;&lt;/p&gt;
&lt;p&gt;3、同理，在 Go GC 运行，所有往堆上插入或者删除的对象，都标记为&quot;灰色&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Go GC 性能不佳的原因&lt;/h3&gt;
&lt;p&gt;1、建立的 struct 对象过小，所以导致频繁的 GC 检查；&lt;/p&gt;
&lt;p&gt;2、Go 文件中出现内存泄漏的问题。&lt;/p&gt;
&lt;h2&gt;Go 语言中的 channel 学习&lt;/h2&gt;
&lt;p&gt;虽然 Go 的开发者极力推荐使用 channel。但是通过大家的工程化道路上的探索，channel 并不是处理并发问题的普适性的使用方法，有时候使用传统的并发原语更简单，而且不容易出错。&lt;/p&gt;
&lt;p&gt;所以在使用并发原语时候，一般遵循以下几种设置方式：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、共享资源的并发访问使用传统并发原语&lt;/p&gt;
&lt;p&gt;2、复杂的任务编排和消息传递使用 channel&lt;/p&gt;
&lt;p&gt;3、消息通知机制使用 channel，除非只想 signal 一个 goroutine，才使用 Cond&lt;/p&gt;
&lt;p&gt;4、简单等待所有任务的完成用 WaitGroup，也有 channel 的推崇者用 channel，都可以&lt;/p&gt;
&lt;p&gt;5、需要和 select 语句结合，使用 channel&lt;/p&gt;
&lt;p&gt;6、需要和超时配合时，使用 channel 和 context&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;channel 具体使用的方式&lt;/h3&gt;
&lt;p&gt;1、动态处理不定数量的 channel，使用 reflect.Select 函数，将 channel 当成参数传入，具体案例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    var ch1 = make(chan int, 10)
    var ch2 = make(chan int, 10)

    // 创建SelectCase
    var cases = createCases(ch1, ch2)

    // 执行10次select
    for i := 0; i &amp;lt; 10; i++ {
        chosen, recv, ok := reflect.Select(cases)
        if recv.IsValid() { // recv case
            fmt.Println(&quot;recv:&quot;, cases[chosen].Dir, recv, ok)
        } else { // send case
            fmt.Println(&quot;send:&quot;, cases[chosen].Dir, ok)
        }
    }
}

func createCases(chs ...chan int) []reflect.SelectCase {
    var cases []reflect.SelectCase

    // 创建recv case
    for _, ch := range chs {
        cases = append(cases, reflect.SelectCase{
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(ch),
        })
    }

    // 创建send case
    for i, ch := range chs {
        v := reflect.ValueOf(i)
        cases = append(cases, reflect.SelectCase{
            Dir:  reflect.SelectSend,
            Chan: reflect.ValueOf(ch),
            Send: v,
        })
    }

    return cases
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码先使用 createCases 函数分别为每个 channel 生成了 recv case 和 send case，并返回一个 reflect.SelectCase 数组。然后，通过一个循环 10 次的 for 循环执行 reflect.Select 从 cases 中伪随机的选择一个 case 执行。第一次肯定是 send case，因为此时 channel 还没有元素，recv 还不可用。等 channel 中有了数据以后，recv case 就可以被选择了。这就可以处理不定数量的 channel。&lt;/p&gt;
&lt;p&gt;2、经典的消息传递案例：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有 4 个 goroutine，编号为 1、2、3、4。每秒钟会有一个 goroutine 打印出它自己的编号，要求你编写程序，让输出的编号总是按照 1、2、3、4、1、2、3、4……这个顺序打印出来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;type Token struct{}

func newWorker(id int, ch chan Token, nextCh chan Token) {
    for {
        token := &amp;lt;-ch         // 取得令牌
        fmt.Println((id + 1)) // id从1开始
        time.Sleep(time.Second)
        nextCh &amp;lt;- token
    }
}
func main() {
    chs := []chan Token{make(chan Token), make(chan Token), make(chan Token), make(chan Token)}

    // 创建4个worker
    for i := 0; i &amp;lt; 4; i++ {
        go newWorker(i, chs[i], chs[(i+1)%4])
    }

    //首先把令牌交给第一个worker
    chs[0] &amp;lt;- Token{}

    select {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，我们定义一个令牌类型 Token，其结构为空的 struct，一般都是使用空结构体进行消息的通知。接着定义一个创建 worker 的方法，这个方法会从它自己的 chan 中读取令牌。哪个 goroutine 取得了令牌，就可以打印出自己编号。因为需要每秒打印一次数据，所以，我们让它休眠 1 秒后，再把令牌交给它的下家。接着启动每个 worker 的 goroutine，并将令牌先交给第一个 worker。这样，就会保证程序的运行是 1、2、3、4 的顺序输出。&lt;/p&gt;
&lt;h2&gt;Go 语言中的 sync.Mutex 学习&lt;/h2&gt;
&lt;p&gt;Go 语言中的 sync 包的 mutex 的设计，有四个演变阶段。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、初版的 Mutex 采用一个 flag 表示锁是否被持有，实现比较简单&lt;/li&gt;
&lt;li&gt;2、之后为了照顾新来的 Goroutine（下文简称 G），会让新人能够尽可能的优先获取锁，此为第二个阶段&lt;/li&gt;
&lt;li&gt;3、第三个阶段呢，是使被唤醒的 G 与新来的 G 有更多的机会竞争锁，但是这样会引发相应的饥饿问题，所以目前又加入了饥饿的解决方案&lt;/li&gt;
&lt;li&gt;4、第四个即为解决饥饿的阶段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/mutex1.png&quot; alt=&quot;mutex1&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;初版 mutex 的实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 2008 年时候，Russ Cox 提交的第一版的 mutex 如下所示

//CAS 操作，当时并未抽象出 atomic 原子包
func cas(val *int32, old, new int32) bool
func semacquire(*int32)
func semrelease(*int32)

type Mutex struct {
    //锁是否被持有
    key int32

    //信号量专用，用于阻塞/唤醒 G
    sema int32
}

//保证成功在 val 上添加 delta 的值
func xadd(val *int32, delta int32) (new int32) {
    for {
        v := *val
        if cas(val, v, v + delta) {
            return v + delta
        }
    }
    panic(&quot;unreached&quot;)
}

//请求锁
func (m *Mutex) Lock() {
    if xadd(&amp;amp;m.key, 1) == 1 { // 标识加 1，如果为 1，则获取到锁
        return
    }
    semacquire(&amp;amp;m.sema) //否则阻塞等待
}

func (m *Mutex) Unlock() {
    if xadd(&amp;amp;m.key, -1) == 0 {// 标识减 1，如果为 0，则没有其他的等待者
        return
    }
    semrelease(&amp;amp;m.sema) //唤醒其他的 G
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;其中，CAS 为一种指令，即将给定的值与内存地址中的值进行相比较，如果是同一个值，就用新值替换内存地址中的旧值。而且 CAS 操作指令是原子性的指令（即数据库中原子性的概念，修改不了数据，事务回滚到修改之前的数据，数据不改变）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/mutex2.png&quot; alt=&quot;mutex2&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有趣的事情是，Unlock 方法能被任意的 G 调用释放，即使没有持有互斥锁的 G，也能进行相应的操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以在使用 Mutex 的时候，必须保证 G 尽可能不去释放自己未持有的锁，一定遵循&quot;谁申请，谁释放&quot;的原则。一般在使用 Mutex 的时候，Lock 与 Unlock 方法都应该在一个方法内成对出现。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 1.14 版本中 Go 对 defer 做了相应的优化，采取更有效的内联模式，将之前生成的 defer 对象放入 defer chain 中，所以 defer 对程序执行的影响微乎其微了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;缺点：请求锁的时候，G 会排队等待获取互斥锁，虽然看起来挺公平的，但是从性能上来看，并非最优的解法。如果能将锁让给正在用 CPU 时间片的 G 的话，就不需要做上下文的切换，在高并发的情况下，可能会有更好的性能。&lt;/p&gt;
&lt;h3&gt;&quot;给新人机会&quot; 阶段&lt;/h3&gt;
&lt;p&gt;2011 年 6 月 30 日，Go 语言开发者在 commit 中对 Mutex 做了一次大调整，调整后的 Mutex 实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Mutex struct {
    state int32
    sema uint32
}

const (
    mutexLock = 1 &amp;lt;&amp;lt; iota //mutex is locked
    mutexWoken
    mutexWaiterShift = iota
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 Mutex 此版本的设计思想为将第一个 int32 类型的 state 字段，拆分为二进制，按二进制的位数进行区分：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/mutex3.png&quot; alt=&quot;mutex3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样可以以最小的内存来实现互斥锁结构，最低位表示锁是否被占有（1|0 占有|非占有），次低位表示锁是否有被唤醒的 G，其余 30 位表示等待此锁的 G 的数量。与计算机网络的子网划分很相似的设计，一个数值，分为三部分，代表三个意义。&lt;/p&gt;
&lt;p&gt;并且因为 atomic 原子性包的添加，请求锁 Lock 也变复杂了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (m *Mutex) Lock() {
    //Fast path : 幸运 case，能够直接获取到相应的锁
    if atomic.CompareAndSwapInt32(&amp;amp;m.state, 0, mutexLocked) {
        return
    }

    awoke := false
    for {
        old := m.state
        new := old | mutexLocked //新状态加锁
        if old &amp;amp; mutexLocked != 0 {
            new = old + 1 &amp;lt;&amp;lt; mutexWaiterShift //等待者数量加一
        }

        if awoke {
            //G 是被唤醒的
            //新状态清除唤醒标志
            new &amp;amp;^= mutexWoken
        }

        if atomic.CompareAndSwapInt32(&amp;amp;m.state, old, new) {//设置新状态
            if old &amp;amp; mutexLocked == 0 { //锁原状态未加锁
                break
            }
            runtime.Semacquire(&amp;amp;m.sema) //请求信号量
            awoke = true
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设计包含大量的位运算，要联系 Go 语言的位运算优先级进行思考。&lt;/p&gt;
&lt;h3&gt;使用 mutex 的一些注意事项&lt;/h3&gt;
&lt;p&gt;1、能不用 mutex 尽量不用 mutex，使用读写锁更合适&lt;/p&gt;
&lt;p&gt;2、尽量使用 defer 释放锁，防止因为 panic 而导致锁未释放&lt;/p&gt;
&lt;p&gt;3、mutex.Lock() 后是不可重入的，写递归时候，不能调用 mutex&lt;/p&gt;
&lt;p&gt;4、尽量使用读写锁！sync.RLock/RUnlock（读锁）、Lock/Unlock（写锁）&lt;/p&gt;
&lt;h2&gt;Go 语言中的 sync.Pool 学习&lt;/h2&gt;
&lt;p&gt;Go 语言是自带垃圾回收机制的，所以我们不用像 C/C++ 一样，在使用完对象后还需要手动删除/析构对象，防止因为空指针导致的内存泄漏。&lt;/p&gt;
&lt;p&gt;但是 Garbage Collect 机制方便的同时，也带来了一定的性能隐患，比如 STW 机制仍然存在，我们大量在堆上创建的对象，会影响垃圾回收标记的时间。&lt;/p&gt;
&lt;p&gt;所以在 Go 语言中，性能优化的方向一般是采用对象池的方式，把不用的对象回收起来，避免被垃圾回收掉。同样的，类似于数据库、TCP 等长连接，也是保存在对象池中，可以大量减少业务的耗时！对应用程序整体性能也有一个提升。&lt;/p&gt;
&lt;h3&gt;sync.Pool 的概念&lt;/h3&gt;
&lt;p&gt;首先需要我们理解 sync.Pool 的两个概念：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;sync.Pool 数据类型是：独立访问的临时对象，本身是&lt;strong&gt;线程安全&lt;/strong&gt;的，能进行并发读取其中的对象！&lt;/p&gt;
&lt;p&gt;sync.Pool 也是不能进行复制使用的！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;sync.Pool 的使用方法&lt;/h3&gt;
&lt;p&gt;sync.Pool 仅有三种方法：New()、Get()、Put()&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New()&lt;/strong&gt;：sync.Pool 中的 New() 是 func() any 类型，其中 any 在源码中用 interface{} 表示。New 方法的使用场景为：在调用 Pool 的 Get 方法并不能从池子中获取空闲元素后，就会创建新的元素。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get()&lt;/strong&gt;：调用此方法会将一个 Pool 中的一个元素取走，返回值可以为 nil 值，所以使用此方法需要对返回值进行判断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Put()&lt;/strong&gt;：此方法用于将一个元素返回给 Pool，Pool 会将此元素保存在池中，而且可以复用，但是如果值是 nil，则 Pool 会忽略此值。&lt;/p&gt;
&lt;h3&gt;sync.Pool 的应用场景&lt;/h3&gt;
&lt;p&gt;一般来说，很经典的场景即是 buffer 池，如 hugo 中的 bufpool，即可看到以下一段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var buffers = sync.Pool{
    New: func() *bytes.Buffer {
        return new(bytes.Buffer)
    },
}

func GetBuffer() *bytes.Buffer {
    return buffers.Get().(*bytes.Buffer)
}

func PutBuffer(buf *bytes.Buffer) {
    buf.Reset()
    buffers.Put(buf)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而上述代码可能会导致内存泄漏的问题。因为取出 bytes.Buffer 后，在使用时，我们通常会向此 buffer 中增加大量的 byte 数据，此时的 slice 容量可能会扩大到另一个量级。而当我们再将其放入 Pool 中时，在 slice 容量不改变的情况下，由于 Pool 回收的机制，这些大的 buffer 就不会被回收，而是一直留在 Pool 池中，占用着计算机的内存。&lt;/p&gt;
&lt;h3&gt;sync.Pool 的实现&lt;/h3&gt;
&lt;p&gt;Go 1.13 之前的版本实现的 sync.Pool 有两个问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、每次 GC 都会回收其中创建的对象；&lt;/p&gt;
&lt;p&gt;2、底层实现采用了：mutex，而在之前的 mutex 学习中，可以知道，对 mutex 锁进行并发操作，在锁竞争相当激烈的情况下，会导致性能的急剧下降。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以 go 团队在 go 语言的 1.13 版本中，针对上述两个问题，做出了大量的优化（这也是 go 语言不建议我们在大量的并发中使用锁）。所以其中的一种优化方式就是 Pool 中不使用锁。&lt;/p&gt;
&lt;h3&gt;好用的第三方 sync.Pool 库&lt;/h3&gt;
&lt;p&gt;1、&lt;a href=&quot;https://github.com/valyala/bytebufferpool&quot;&gt;bytebufferpool&lt;/a&gt;：fasthttp 作者 valyala 提供的一个 buffer 池，基本功能和 sync.Pool 相同。底层使用 sync.Pool 实现的，并且会检测最大的 buffer，超过最大尺寸的 buffer，就会被丢弃。此官方的库提供了校准（calibrate，用来动态调整创建元素的权重）的机制，可以动态地调整 Pool 的 defaultSize 和 maxSize。&lt;/p&gt;
&lt;p&gt;2、&lt;a href=&quot;https://github.com/oxtoacart/bpool&quot;&gt;oxtoacart/bpool&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;3、&lt;a href=&quot;https://github.com/fatih/pool&quot;&gt;fatih/pool&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;大部分 Work Pool 都是通过 channel 来缓存任务的，因为 channel 能很好的实现并发的保护，防止数据因为并发访问所造成的 data race！&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/pool2.png&quot; alt=&quot;pool2&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;pool 是一个通用性的概念，用于解决对象重用与预先分配的一个常用的优化手段。类似数据库连接、HTTP 的 API 请求中已经封装使用了 Pool 了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果在程序中 GC 耗时特别高，大量相同的类型的临时对象不断进行创建与销毁，可以考虑通过使用 sync.Pool 对其进行优化改良！&lt;/p&gt;
</content:encoded></item><item><title>Go 网络编程与框架学习笔记</title><link>https://chaggle.github.io/posts/2026/08/11/go-network-frameworks/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/go-network-frameworks/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;2022 年学习 Go 网络编程的四篇笔记（HTTP 服务器/客户端、gin 框架、zinx 框架、轻量级 IM 项目）合并整理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Golang 开发一个 HTTP Web 服务器/客户端&lt;/h2&gt;
&lt;p&gt;:::note
之前的 zinx 学习，是基于 TCP/UDP 的 socket 协议进行编写，而本次要实现的是基于 HTTP 协议开发一个 Web 服务器端与客户端！
:::&lt;/p&gt;
&lt;h3&gt;要求&lt;/h3&gt;
&lt;p&gt;服务器端要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、服务端维护一个内存数据结构，所有数据进程重启丢失，不做数据持久化，不考虑内存容量问题&lt;/li&gt;
&lt;li&gt;2、服务端实现一个网络 API 接口，客户端向该 API 发送一个网络请求，请求数据是一个 string 的信息&lt;/li&gt;
&lt;li&gt;3、服务端该 API 收到请求后响应一个 []string 的信息，返回之前发送过的所有 string，当次请求发送的 string 一定在最后，不关心是否重复&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;第一次请求发送：&quot;a&quot;，响应: []string{&quot;a&quot;}&lt;/p&gt;
&lt;p&gt;第二次请求发送：&quot;b&quot;，响应: []string{&quot;a&quot;, &quot;b&quot;}&lt;/p&gt;
&lt;p&gt;第三次请求发送：&quot;a&quot;，响应: []string{&quot;a&quot;, &quot;b&quot;, &quot;a&quot;}&lt;/p&gt;
&lt;p&gt;注意服务端代码部分，任何情况下均不能退出进程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;客户端要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、客户端必须是 Golang 函数（该函数后面简称 BcjClient），调用该服务端的 API 接口&lt;/li&gt;
&lt;li&gt;2、该客户端函数输入参数为：string，输出参数为：[]string 和 error。客户端函数是个 function/函数，不是 method/方法&lt;/li&gt;
&lt;li&gt;3、注意客户端函数业务代码执行过程中，任何情况下均不能退出进程&lt;/li&gt;
&lt;li&gt;4、客户端函数的类型必须严格匹配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;源码要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、代码运行结果应当正确&lt;/li&gt;
&lt;li&gt;2、不应该有 data race（race condition/竞争条件/数据竞争）。多线程客户端调用服务端时，不应当出现任何 2 个客户端得到的同样响应的可能性&lt;/li&gt;
&lt;li&gt;3、需要满足 Database transaction（数据库事务）的 Serializability（可串行性）要求（注意：Golang 的 data race 工具只能找到部分 data race 情况）&lt;/li&gt;
&lt;li&gt;4、代码不允许忽略错误，而且只能调用官方标准库，不能包含其他第三方代码（比如 github.com/xxx，比如 &quot;golang.org/x/sync/xx&quot;）。只有 GOROOT/src 下面的库算官方库，其他都不算官方库&lt;/li&gt;
&lt;li&gt;5、实现应当简洁，代码可读性不能过差&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;设计思路&lt;/h3&gt;
&lt;p&gt;1、http 服务，基于 http 协议规则实现，并非 socket 协议规则进行实现&lt;/p&gt;
&lt;p&gt;2、web 请求，restful 标准下，采用 POST 进行客户端的 web 服务请求&lt;/p&gt;
&lt;p&gt;3、开发第一个版本时服务器维护的内存数据结构采用 sync.Map，采用 client 不设置用户名称，统一设置 token 值为 Client，返回客户端数据为 string 类型，并通过简单的空格字符串 &quot; &quot; 进行分离每一个 []string 类型的元素&lt;/p&gt;
&lt;p&gt;4、（未完成）第二个版本客户端自设名称为 token 值，通过 header 包头的 token 值来对客户端进行区分，这个 token 值的维护放置于客户端，理论上为保证数据安全，应该放置于服务器。（2025 年再来看，只需要服务器端针对于 token 进行校验即可，jwt 的设置即为服务器端不保存 token，由客户端自行维护，当然这里可以采用双 token 的设计方法，在服务器端进行 redis 存储，当然设计也是有安全性风险的，但是一般性企业也不会需要安全性高到此中设计程度）&lt;/p&gt;
&lt;h3&gt;开发的细节&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、Go 语言很适合使用&lt;strong&gt;测试驱动开发&lt;/strong&gt;，在工期缩短的情况下，可能不适用 TDD，而在工期充足的情况下，开发过程中，首先需要进行逻辑的梳理，逻辑梳理好之后再开始代码的撰写，上来就写代码都是想到哪里写到哪里，这种习惯不好，&lt;strong&gt;最合适的方法是先使用注释写好相应的需求逻辑，再写代码，对于逻辑思考很实用&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;2、在做 string 字符串存储成 []string 类型的切片的时候，服务器端程序也需要使用一个 []string 类型进行保存，但是服务器回显给客户端是以 io.writer 实现的 writer 接口返回 []byte 流的数据，&lt;strong&gt;而 []string 直接转为 []byte 流数据形式，之后客户端对于格式的处理较为麻烦&lt;/strong&gt;，所以最好还是 []string 类型，通过拼接一个 string 传输给客户端，我选择 []string 中间分割字符为空格字符。客户端接收到这个 string 类型的字符串后，使用 strings.Split() 函数，以空格字符进行分割。&lt;/p&gt;
&lt;p&gt;3、为了解决客户端先于服务器端启动时间不同，导致的客户端 Dial 服务器端发生端口拒绝错误，采用 for 循环提交 POST 请求，直到请求建立成功后，再 break for 循环。&lt;/p&gt;
&lt;p&gt;4、bug fix: 防止程序 panic 掉，mutex.unlock 并未释放，所以采用 defer m.Unlock 合适&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;优化&lt;/h3&gt;
&lt;p&gt;在服务器端，&lt;strong&gt;使用 sync.Map 数据结构对传输的字符串进行存储，防止进行并发访问与并发存储&lt;/strong&gt;，但是由于自己在并发的能力功底不足，导致虽然设计是 sync.Map 控制，但仍存在 data race 的情况，无奈之下还是对一整段进行了加锁处理，其实也能使用全局的 channel 进行处理。但是这都与最初自己想要一步实现拒绝数据的竞争性访问的设计理念冲突，暂时也没想到很好的解决办法！等日后再对其进行优化！&lt;/p&gt;
&lt;h3&gt;代码附件&lt;/h3&gt;
&lt;p&gt;客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package gohttp

import (
	&quot;bytes&quot;
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;io/ioutil&quot;
	&quot;net/http&quot;
	&quot;strings&quot;
	&quot;time&quot;
)

func BcjClient(writer string) ([]string, error) {
	var r *http.Response
	for {
		client := &amp;amp;http.Client{}
		req, err := http.NewRequest(
			http.MethodPost,
			&quot;http://127.0.0.1:8999/v1&quot;,
			bytes.NewReader([]byte(writer)),
		)

		req.Header.Add(&quot;Token&quot;, &quot;Client&quot;)
		req.Header.Add(&quot;content-Type&quot;, &quot;text/plain&quot;)

		r, err = client.Do(req)
		if err == nil {
			break
		} else {
			fmt.Printf(&quot;err: %s\n&quot;, err)
			time.Sleep(3 * time.Second)
			continue
		}
	}
	defer func(Body io.ReadCloser) {
		_ = Body.Close()
	}(r.Body)

	content, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return nil, err
	}

	//将传入的字符串变成字符串切片，并除去最后的换行格式问题！
	ToVisual := strings.Split(string(content), &quot; &quot;)
	ToVisual = ToVisual[:len(ToVisual)-1]

	return ToVisual, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package gohttp

import (
	&quot;fmt&quot;
	&quot;io/ioutil&quot;
	&quot;net/http&quot;
	&quot;sync&quot;
)

var cache sync.Map
var m sync.Mutex

func ClientHandler(writer http.ResponseWriter, req *http.Request) {

	if req.Method != &quot;POST&quot; {
		_, _ = fmt.Fprintf(writer, &quot;Request is not POST! please send POST request!&quot;)
		return
	}

	token := req.Header.Get(&quot;Token&quot;)
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		_, _ = fmt.Fprintf(writer, &quot;read body err, %v\n&quot;, err)
		return
	}

	m.Lock()
	defer m.Unlock()

	GetFromCache, ok := cache.Load(token)
	if ok {
		GetFromCache = append(GetFromCache.([]string), []string{string(body)}...)
		cache.Store(token, GetFromCache)
	} else if !ok {
		cache.Store(token, []string{string(body)})
		GetFromCache, _ = cache.Load(token)
	}

	change := GetFromCache.([]string)
	var ToClient string
	for _, v := range change {
		ToClient += v
		ToClient += &quot; &quot;
	}
	_, _ = fmt.Fprintf(writer, &quot;%s\n&quot;, ToClient)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Go web 的相关知识复习（gin）&lt;/h2&gt;
&lt;p&gt;开始动手学习一些框架的基本样例，抛开学习底层的设计不谈，主要还是参考文档进行相应的组件开发。&lt;/p&gt;
&lt;h3&gt;Beego 与 gin&lt;/h3&gt;
&lt;p&gt;首先学习框架之前，需要我们理解一个道理，学习框架的目的首先是适应业务场景的需要，其次是分为两种大类：一种是为了开发效率，另一种是为了追求运行的性能。追求运行性能的框架包含的东西挺多，比如 GIN，另一种追求开发效率的框架封装得非常好，即是 Beego 框架。&lt;/p&gt;
&lt;h3&gt;路由分组&lt;/h3&gt;
&lt;p&gt;gin 框架的路由分组为：对 router 创建 Group 即为分组，同一分组拥有同一前缀和同一中间件。其相关的写法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r := gin.Default()

v1 := r.Group(&quot;v1&quot;)
{
    v1.POST(&quot;/login&quot;, login)
    v1.POST(&quot;/submit&quot;, func)
    v1.POST(&quot;/read&quot;, func)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;路由分组的目的是：使路由更加清晰，方便管理路由。&lt;/p&gt;
&lt;h3&gt;中间件&lt;/h3&gt;
&lt;p&gt;在请求到达路由的方法前和后进行的一系列的操作，使用中间件的时候，在路由组上进行 use 操作，后面传入中间件函数即可。中间件有概念叫做&lt;a href=&quot;https://zhuanlan.zhihu.com/p/279391637&quot;&gt;洋葱中间件&lt;/a&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/gin.jpg&quot; alt=&quot;gin&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用 Go 语言写的还有以下的相关中间件：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;日志：一般日志使用其他的工具：如 go-logging、logrus 等开源库。以及配合相应的日志切割工具去使用。&lt;/p&gt;
&lt;p&gt;Gorm：orm 是一种数据库操作辅助工具。Gorm 即是在 go 语言结构体和数据库产生映射，使得数据库关系、表内容可以直观的体现在结构体上。即可使用结构体完成增删改查的操作！至于 Gorm 如何使用，可以参考相应的详细文档，简单的增删改查可以使用 Gorm。设计复杂的结构以及优化操作，建议阅读 MYSQL 性能调优，阅读相应的例子进行优化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;框架的学习&lt;/h3&gt;
&lt;p&gt;快速掌握代码库中的库函数，最适合的学习方法为：先读库函数 -&amp;gt; 再读相应的结构体定义 -&amp;gt; 最后读相应的结构体绑定的方法。其中前两部分只需要执行 go doc 命令即可查看相应的函数与结构体，而第三部分需要仔细阅读相应的源码。记录学习的过程，建议使用思维导图，最后导入 markdown 文件中。&lt;/p&gt;
&lt;h3&gt;Restful 风格&lt;/h3&gt;
&lt;p&gt;其中 restful 主要有四种方式：GET、POST、PUT、DELETE：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GET 用于获取资源&lt;/p&gt;
&lt;p&gt;POST 用于创建资源&lt;/p&gt;
&lt;p&gt;PUT 用于修改资源&lt;/p&gt;
&lt;p&gt;DELETE 用于删除资源&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;参考文档：&lt;a href=&quot;https://www.kancloud.cn/shuangdeyu/gin_book/949415&quot;&gt;https://www.kancloud.cn/shuangdeyu/gin_book/949415&lt;/a&gt;、&lt;a href=&quot;https://gorm.io/zh_CN/docs/&quot;&gt;https://gorm.io/zh_CN/docs/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Zinx 框架的学习&lt;/h2&gt;
&lt;p&gt;开始学习 Go 语言实现的 zinx 框架，项目地址为：&lt;a href=&quot;https://github.com/chaggle/zinx-study&quot;&gt;https://github.com/chaggle/zinx-study&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 go mod 管理，初始化为 go mod init github.com/chaggle/zinx-study，并部署代码到 github.com 以及使用 go get 同步到本地 Gopath 的 github 包下！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.1 基础的 server 模块&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;方法：初始化服务器 NewServer(name string) ziface.IServer、启动服务器 Start()、停止服务器 Stop()、运行服务器 Serve()&lt;/p&gt;
&lt;p&gt;属性：名称 name、IP 版本 IPVersion、监听 IP IP、监听端口 Port&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.2 简单的链接封装和业务绑定&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;方法：启动链接 Start()、停止链接 Stop()、获取当前链接的 conn 对象（套接字）GetTCPConnection() *net.TCPConn、得到链接 ID GetConnID() uint32、得到客户端连接的地址和端口 RemoteAddr() net.TCPAddr、发送数据的方法 Send(data []byte) error&lt;/p&gt;
&lt;p&gt;属性：socket TCP 套接字 Conn *net.TCPConn、链接的 ID ConnID uint32、当前链接状态（是否已经关闭）isClosed bool、与当前链接所绑定的处理业务与方法 handlerAPI ziface.HandleFunc、等待退出的 channel 管道 ExitChan chan bool&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.3 基础的 router 模块&lt;/h3&gt;
&lt;p&gt;Request 请求封装：将链接与数据绑定一起：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;属性：链接的句柄 GetConnection() IConnection、请求数据 GetData() []byte&lt;/p&gt;
&lt;p&gt;方法：得到链接 func (r *Request) GetConnection() ziface.IConnection、得到数据 func (r *Request) GetData() []byte、新建一个 Request 请求&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Router 模块：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;抽象的 IRouter：处理业务之前的方法 PreHandle(request IRequest)、处理业务的主方法 Handle(request IRequest)、处理业务之后的方法 PostHandle(request IRequest)&lt;/p&gt;
&lt;p&gt;具体的 BaseRouter：继承并实现三个接口方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;zinx 集成 Router 模块：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;IServer 增添路由功能 AddRouter(router IRouter)&lt;/p&gt;
&lt;p&gt;Server 类增加 Router 成员（去掉之前的 HandAPI）&lt;/p&gt;
&lt;p&gt;Connection 类绑定一个 Router 成员&lt;/p&gt;
&lt;p&gt;在 Connection 调用已经注册过的 Router 处理业务&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当前版本只有一个路由能使用，如果加入新的路由模块，会使上一个路由模块的方法被重写覆盖。&lt;/p&gt;
&lt;h3&gt;V0.4 全局配置模块&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;路径：服务器项目主地址/conf/zinx.json（用户进行填写）&lt;/p&gt;
&lt;p&gt;创建一个 zinx 的全局配置模块 utils/globalobj.go&lt;/p&gt;
&lt;p&gt;初始化后读取用户配置的 zinx.json，globalobj 对象中对应的 zinx 服务器句柄代码进行参数替换&lt;/p&gt;
&lt;p&gt;提供一个 GlobalObject 对象 var GlobalObject *GlobalObj&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.5 消息封装&lt;/h3&gt;
&lt;p&gt;定义消息的结构 Message：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;属性：消息的 ID、消息的长度、消息的内容&lt;/p&gt;
&lt;p&gt;方法：SetMsgId(uint32)、SetData([]byte)、SetDataLen(uint32)、GetDataLen() uint32、GetMsgId() uint32、GetData() []byte&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;定义解决 TCP 粘包问题的封包拆包的模块：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;针对 Message 进行 TLV 格式的封装 func (dp *DataPack) Pack(msg ziface.IMessage) ([]byte, error)：写 Message 的长度、写 Message 的 ID、写 Message 的内容&lt;/p&gt;
&lt;p&gt;针对 Message 进行 TLV 格式的拆包 func (dp *DataPack) Unpack(binaryData []byte) (ziface.IMessage, error)：先读取固定长度的 head（消息的长度和消息的类型），再根据消息内容的长度，再进行一次读写，从 conn 中读取消息的内容&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;将消息封装机制集成到 Zinx 框架中：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;将 Message 添加到 Request 属性字段&lt;/p&gt;
&lt;p&gt;修改连接读取数据的机制，将之前的单纯读取 byte 改成拆包形式，读取按照 TLV 形式进行读取&lt;/p&gt;
&lt;p&gt;给链接提供一个发包的机制：将发送的消息打包，再发送&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.6 多路由模式&lt;/h3&gt;
&lt;p&gt;消息管理模块（支持多路由 API 调度管理）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;属性：集合 - 消息 ID 与对应 router 的关系 - map Apis map[uint32]ziface.IRouter&lt;/p&gt;
&lt;p&gt;方法：根据 MsgId 来索引调度路由方法 func (mh *MsgHandle) DoMsgHandler(request ziface.IRequest)、添加路由方法到 map 集合中 func (mh *MsgHandle) AddRouter(msgID uint32, router ziface.IRouter)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;消息管理模块集成到 Zinx 框架中：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;将 server 模块里面的 Router 属性变为 MsgHandle 属性&lt;/p&gt;
&lt;p&gt;将 server 模块中的 AddRouter 修改调用 MsgHandler 的 AddRouter&lt;/p&gt;
&lt;p&gt;将 connection 模块中的 Router 属性替换为 MsgHandle 属性&lt;/p&gt;
&lt;p&gt;将 connection 模块中 Router 的业务调度改为 MsgHandle 调度，并修改 StartRead 方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.7 读写协程分离&lt;/h3&gt;
&lt;p&gt;小修改，新增一个 goroutine 即可：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、添加一个 Reader 和 Writer 之间通信的 channel&lt;/p&gt;
&lt;p&gt;2、添加一个 Writer Goroutine&lt;/p&gt;
&lt;p&gt;3、Reader 由之前发送给客户端 改成 发送给 通信 Channel&lt;/p&gt;
&lt;p&gt;4、启动 Reader 和 Writer 一同工作&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.8 消息队列以及多任务&lt;/h3&gt;
&lt;p&gt;消息队列以及 Worker 工作池的实现：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;创建一个消息队列 --- MsgHandler 消息管理模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息队列 TaskQueue []chan ziface.IRequest&lt;/li&gt;
&lt;li&gt;工作池的数量 WorkPoolSize uint32&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建多任务 work 工作池，并且启动：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个 worker 工作池：func (mh *MsgHandle) StartWorkerPool()&lt;/li&gt;
&lt;li&gt;根据 WorkPoolSize 的数量去创建 Worker&lt;/li&gt;
&lt;li&gt;每个 worker 都应该用一个 Go 去承载&lt;/li&gt;
&lt;li&gt;1、阻塞等待当前 worker 的对应的 channel 的消息&lt;/li&gt;
&lt;li&gt;2、一旦有消息到来，worker 应该处理当前消息对应的业务&lt;/li&gt;
&lt;li&gt;3、将之前的发送消息，全部改成将消息发送给消息队列和 worker 工作池处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;定义一个方法，将消息发送给消息队列工作池的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;func (mh *MsgHandle) SendMsgToTaskQueue(request ziface.IRequest)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;将消息队列机制集成到 Zinx 框架中：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、开启并调用消息队列及 worker 工作池，保证 workerPool 只有一个，应该在创建 Server 模块时候开启（在 Server listen 之前添加）&lt;/p&gt;
&lt;p&gt;2、将从客户端处理的消息，发送给当前的 Worker 工作池来处理，在处理完拆包，得到了 request 请求，交给工作池来处理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;小型项目，具体以技术方案跟思考为主，而与需求、产品、测试、运维的沟通可以让步于项目的上线。&lt;/p&gt;
&lt;h2&gt;Go语言实现的轻量级IM项目&lt;/h2&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/chaggle/go-im&quot;&gt;https://github.com/chaggle/go-im&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;V0.1：建立基础的 main.go、server.go&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;main 功能主要为创建服务器以及启动服务器&lt;/p&gt;
&lt;p&gt;server 功能有：&lt;/p&gt;
&lt;p&gt;1、创建 server 对象&lt;/p&gt;
&lt;p&gt;2、启动 Server 服务（TCP socket 套接字）&lt;/p&gt;
&lt;p&gt;3、处理连接的业务&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.2：用户上线功能&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;user 功能新增：&lt;/p&gt;
&lt;p&gt;1、创建 user 对象&lt;/p&gt;
&lt;p&gt;2、监听每个 user 对应的 channel 的消息&lt;/p&gt;
&lt;p&gt;server 新增功能：&lt;/p&gt;
&lt;p&gt;1、新增 OnlineMap 与 Message 属性&lt;/p&gt;
&lt;p&gt;2、在处理客户端上线的 Handler 创建并添加用户（使用到 OS 中的同步 Lock）&lt;/p&gt;
&lt;p&gt;3、新增广播消息方法以及监听广播消息的 channel 方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.3：用户消息广播机制完善&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;server 新增功能：&lt;/p&gt;
&lt;p&gt;1、完善 handle 模块处处理业务的方法，启动一个针对于当前客户端的读 goroutine&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.4：用户业务层封装&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;对于用户层业务的层次化、模块化：&lt;/p&gt;
&lt;p&gt;server 中的 user 业务进行迁移：&lt;/p&gt;
&lt;p&gt;1、server 关联&lt;/p&gt;
&lt;p&gt;2、新增 Online、Offline、Domessage 方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.5：查询用户名以及用户名修改&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;user 新增两个功能：&lt;/p&gt;
&lt;p&gt;1、用户名查询的功能&lt;/p&gt;
&lt;p&gt;2、用户名修改的功能，保证每个用户名唯一&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V0.6：超时强制下线以及私聊功能&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;user 新增两个功能：&lt;/p&gt;
&lt;p&gt;1、设置定时器，超时强制剔除，发消息代表活跃，长时间不发消息代表超时，即强制关闭用户连接&lt;/p&gt;
&lt;p&gt;2、私聊功能，通过获取用户名队列中的用户名，来向用户发起私聊&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V1.0、V1.1：客户端基本功能&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;新增 client 客户端，当然并非 GUI 界面版本，也可以使用带 GUI 界面版本，满足通信协议即可：&lt;/p&gt;
&lt;p&gt;1、连接建立功能、命令行解析功能&lt;/p&gt;
&lt;p&gt;2、客户端菜单功能的预写&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;V1.2：客户端的相应基本请求&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;client 新增：&lt;/p&gt;
&lt;p&gt;1、用户名修改请求，通过 io.copy() 阻塞监听的方式进行回显输出&lt;/p&gt;
&lt;p&gt;2、用户进入公聊模式，进行消息的广播与退出公聊模式&lt;/p&gt;
&lt;p&gt;3、用户进行私聊模式，选择用户功能封装以及单独对其发送消息&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Go 语言基础与工程实践笔记</title><link>https://chaggle.github.io/posts/2026/08/11/go-fundamentals-engineering/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/go-fundamentals-engineering/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;2022 年学习 Go 语言基础与工程实践的多篇笔记（基础知识点、错误处理、TDD、面试复盘、学习方向思考等）合并整理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Go 基础知识点自我总结&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;tips: 总结知识点一定要自己动手，这样知识点才能牢记！比如开源社区有雨痕大佬的 Go 语言笔记，但是光看几个用法，而不进行代码的编写，代码能力提升不明显，能看懂，但是做不到，形成一种眼高手低的习惯。更何况，以后的工作本来就是写需求的代码，所以，各种基础知识点，快速过完！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;保留字与预定义字&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//保留字有 25 个
break  continue  func  interface  select
case  defer   go  map   struct
chan  else   goto  package   fallthrough
const  switch   if  range   type
default  for   import  return    var

//以下为相关的预定义字
true  false   iota  nil

//内建字类型
int  int16  int32 int64
uint  uint16   uint32  uint64
float32  float64   complex64 complex128
bool  byte   rune  string   error

//内建函数
make  len   cap  new  append
copy  close   delete  complex  real
imag  panic   recover
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go 中的一些常用函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; append          -- 用来追加元素到数组、slice中,返回修改后的数组、slice
 close           -- 主要用来关闭 channel
 delete          -- 从map中删除 key 对应的 value
 panic           -- 停止常规的 goroutine (panic和recover：用来做错误处理)
    recover         -- 允许程序定义goroutine的panic动作
 real            -- 返回complex的实部 (complex、real imag用于创建和操作复数)
    imag            -- 返回complex的虚部
    make            -- 用来分配内存，返回 Type 本身(只能应用于 slice, map, channel )
    new             -- 用来分配内存，主要用来分配值类型，如 int、struct 返回指向 Type 的指针
    cap             -- capacity是容量的意思，用于返回某个类型的最大容量（只能用于切片和 map）
    copy            -- 用于复制和连接 slice，返回复制的数目
    len             -- 来求长度，比如 string、array、slice、map、channel，返回长度
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go 中的 Array 数组&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;答：Go 语言中的数组与其他语言的数组不一样：&lt;/p&gt;
&lt;p&gt;1、Go 语言中的数组是值类型，赋值和传参会复制整个数组，而不是指针&lt;/p&gt;
&lt;p&gt;2、Go 语言中的数组长度必须是常量，而且是类型的组成成分，[2]int、[3]int 是不同类型&lt;/p&gt;
&lt;p&gt;3、Go 语言中的数组能支持 &quot;==&quot; &quot;!=&quot; 操作，因为内存被初始化过&lt;/p&gt;
&lt;p&gt;4、指针数组 [n]&lt;em&gt;T //表示由 n 个&lt;/em&gt;T 指针构成&lt;/p&gt;
&lt;p&gt;5、数组指针 *[n]T //表示由一个指针指向这个数组，可以视为一维数组来看&lt;/p&gt;
&lt;p&gt;6、array 数组不建议拷贝，如需要拷贝使用 slice 更好，或者数组指针&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Go 中的 slice&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/*
 slice 为结构体，通过内部指针和相关的结构属性引用数组片段
*/
struct Slice {
 byte* array  //引用类型，但是是结构体，所以采用值拷贝传递
    uintgo len  //slice 数量，读写不超过该限制
    uintgo cap  //slice 容量，不能超出数组限制
 //slice == nil, len = cap = 0
}

//Go 中还有一个关于 slice 的拼接问题，两个 slice 在 append 的时候，
//记住拼接第二个 slice 时，需要将 slice 打散再拼接！
s1 := []int{1, 2, 3}
s2 := []int{4, 5}
s1 = append(s1, s2...) //正确写法
//s1 = append(s1, s2) 编译失败: cannot use s2 (type []int) as type int in append
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go 中的面向对象&lt;/h3&gt;
&lt;p&gt;面向对象的三大特征：多态、继承、封装。Go 语言只支持封装行为，没有继承与多态，也没有 class 关键字，但是能做到 struct 中嵌入 struct 类型。&lt;/p&gt;
&lt;h3&gt;Go 方法&lt;/h3&gt;
&lt;p&gt;方法：方法是绑定对象实例的，隐式将实例作为第一形参。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、只能为当前包内命名类型定义方法&lt;/li&gt;
&lt;li&gt;2、参数 receiver 随意命名，如果未使用此名称，可省略&lt;/li&gt;
&lt;li&gt;3、参数 receiver 类型可以为 T、*T，但是基类 T 不能是指针与接口&lt;/li&gt;
&lt;li&gt;4、不支持方法重载，receiver 为参数签名的组成成分&lt;/li&gt;
&lt;li&gt;5、可用实例 value 或 pointer 调用全部方法，编译器自动转换&lt;/li&gt;
&lt;li&gt;6、通常使用简单工厂模式返回对象实例&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;type Queue struct{
    elements []interface{}
}

func NewQueue() *Queue {
    return &amp;amp;Queue{
        elements: make([]interface{}, 10),
    }
}

func (*Queue) push(e interface{}) error {
    panic(&quot;not implemented&quot;)
}

func (this *Queue) length() int {
    return len(this.elements)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go 接口&lt;/h3&gt;
&lt;p&gt;接口是方法的集合，由于不支持重载，所以每个方法命名唯一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、接口命名以 er 结尾，结构体&lt;/li&gt;
&lt;li&gt;2、接口不实现方法&lt;/li&gt;
&lt;li&gt;3、接口无数据字段&lt;/li&gt;
&lt;li&gt;4、接口内可以嵌入其他接口&lt;/li&gt;
&lt;li&gt;5、类型可实现多接口&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;type Stringer interface {
    String() string
}

type Pointer interface {
    Stringer
    Print()
}

type User struct {
    id int
    name string
}

func (this *User) String() string {
    return fmt.Sprintf(&quot;User %d, %s&quot;, this.id, this.name)
}

func (this *User) Print() {
    return fmt.Println(this.String())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Goroutine&lt;/h3&gt;
&lt;p&gt;Goroutine 为 go 协程，仅仅需要在函数调用语句前添加 go 关键字即可调用 goroutine，并且创建并发执行单元。goroutine 的设计具有轻量级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    go func() {
        fmt.Println(&quot;Hello World&quot;) //此为 goroutine
    }()
    //main 函数的执行其实也是 goroutine
    runtime.Goexit() //立即终止当前 goroutine 的执行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体 CSP 的模式与 GMP 的模型在并发的笔记中继续学习。&lt;/p&gt;
&lt;h3&gt;Channel&lt;/h3&gt;
&lt;p&gt;引用类型 channel 为 CSP 的模式的具体实现，使用 channel 用于多个 goroutine 的通信，确保并发安全，尽量使用 channel 来代替锁进行同步。channel 默认为同步的模式，需要发送和接收配对，否则一直处于被阻塞的状态，直到两边都处于活跃状态然后才被唤醒：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    data := make(chan int) // 数据交换队列
    exit := make(chan bool) // 退出通知

    go func() {
        for d := range data { // 从队列迭代接收数据，直到 close
            fmt.Println(d)
        }

        fmt.Println(&quot;recv over.&quot;)

        exit &amp;lt;- true // 发出退出通知
    }()

    data &amp;lt;- 1 // 发送数据
    data &amp;lt;- 2
    data &amp;lt;- 3

    close(data) // 关闭队列

    fmt.Println(&quot;send over.&quot;)

    &amp;lt;-exit // 等待退出通知
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;异步方式：通过判断缓冲区来决定是否阻塞。如果缓冲区满，发送阻塞；缓冲区为空，接收阻塞。通常情况下，异步的 channel 可以减少排队阻塞，具有更高的效率。但是应该考虑使用指针规避大对象的拷贝，将大对象分治为小对象，如多个元素打包、减小缓冲区大小等：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
    data := make(chan int, 3) // 数据交换队列
    exit := make(chan bool) // 退出通知

    data &amp;lt;- 1 // 在缓冲区未满前，不会阻塞
    data &amp;lt;- 2
    data &amp;lt;- 3

    go func() {
        for d := range data { // 在缓冲区未空前，不会阻塞
            fmt.Println(d)
        }
        exit &amp;lt;- true // 发出退出通知
    }()

    data &amp;lt;- 4
    data &amp;lt;- 5
    close(data)

    &amp;lt;-exit // 等待退出通知
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中缓冲区是内部属性，非类型构成要素，内置函数 len 返回未被读取的缓冲元素数量，cap 返回缓冲区大小。channel 是第一类对象，可传参（内部实现为指针）或者作为结构成员。&lt;/p&gt;
&lt;p&gt;Go 语言还内建了 &lt;code&gt;close()&lt;/code&gt; 函数来关闭一个 channel，但是存在以下几种情况：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、读写 nil Channel 会永远阻塞，关闭 nil Channel 会导致 panic&lt;/p&gt;
&lt;p&gt;2、关闭一个已关闭的 Channel 会导致 panic&lt;/p&gt;
&lt;p&gt;3、向已经关闭的 Channel 发送数据会导致 panic&lt;/p&gt;
&lt;p&gt;4、向已经关闭的 Channel 读取数据不会导致 panic，但读取的值为 Channel 传递的数据类型的零值，可以通过接收语句的第二个返回值来检查 Channel 是否关闭且排空：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;    v, ok := &amp;lt;- ch
    if !ok {
        // 如果是非缓冲 Channel ，说明已经关闭；
        // 如果是带缓冲 Channel ，说明已经关闭，且其内部缓冲区已经排空
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go 中的 select 用法&lt;/h3&gt;
&lt;p&gt;select 是 Go 中的一个控制结构，类似于用于通信的 switch 语句，其用于处理异步的 I/O 操作。其有以下的几个特征：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 1、每个case都必须是一个通信，所有 channel 表达式都会被求值
 2、所有被发送的表达式都会被求值
 3、如果任意某个通信可以进行，它就执行；其他被忽略
 4、如果有多个case都可以运行，select 会随机公平地选出一个执行。其他不会执行
 5、如果有default子句，则执行该语句。如果没有default子句，select将阻塞，
    直到某个通信可以运行；Go不会重新对channel或值进行求值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;select 会监听每个 case 中 channel 的读写操作，但是每次只能运行一个 channel 进行读或写。&lt;/p&gt;
&lt;h3&gt;初始化函数&lt;/h3&gt;
&lt;p&gt;初始化函数 init()：go 语言中 &lt;code&gt;init&lt;/code&gt; 函数用于包（package）的初始化，该函数是 go 语言的一个重要特性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1 init函数是用于程序执行前做包的初始化的函数，比如初始化包里的变量等&lt;/p&gt;
&lt;p&gt;2 每个包可以拥有多个init函数&lt;/p&gt;
&lt;p&gt;3 包的每个源文件也可以拥有多个init函数&lt;/p&gt;
&lt;p&gt;4 同一个包中多个init函数的执行顺序go语言没有明确的定义&lt;/p&gt;
&lt;p&gt;5 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序&lt;/p&gt;
&lt;p&gt;6 init函数不能被其他函数调用，而是在main函数执行之前，自动被调用&lt;/p&gt;
&lt;p&gt;main 对比 init 函数，main函数只能用于main包中，且只能定义一个&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;go mod 包管理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;初始化模块 go mod init &amp;lt;项目模块名称&amp;gt;
依赖关系处理 ,根据go.mod文件 go mod tidy
将依赖包复制到项目下的 vendor目录 go mod vendor
   （如果包被屏蔽(墙)，可以使用这个命令，随后使用go build -mod=vendor编译）
显示依赖关系 go list -m all
显示详细依赖关系 go list -m -json all
下载依赖 go mod download [path@version]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;go debug&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;go run -race 可以查看有无相应的锁竞争问题&lt;/li&gt;
&lt;li&gt;go tool compile -S [filename] 查看相应的汇编代码&lt;/li&gt;
&lt;li&gt;Linux 下使用 top/htop 指令查看 CPU、Memory、VIRT 等信息，其中关于内存泄漏的问题，主要查看的是 VIRT 是否出现不断增长的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;go 内存逃逸&lt;/h3&gt;
&lt;p&gt;内存逃逸的主要概念是：编译时 go 语言编译器会自动决定把一个变量放在栈还是放在堆，编译器会做逃逸分析（escape analysis），当&lt;strong&gt;发现变量的作用域没有跑出函数范围，就可以在栈上，反之则必须分配在堆&lt;/strong&gt;。一般内存逃逸的分析都是编译器进行的，go 语言的设计者不希望我们过多的研究这个方面的内容，所以一般不用管！&lt;/p&gt;
&lt;h2&gt;Go 语言中的错误处理&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;通过模块化组件的方式，学习 Go 语言的微服务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Go 语言中的 error 定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type error interface {
    Error() string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Go 1.13 版本之前，我们通常使用的是 &lt;code&gt;errors.New()&lt;/code&gt; 来进行返回一个 error 对象指针，相应的源码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func New(text string) error {
    return &amp;amp;errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 Go 中的指针与 C 中指针大致相同，都是指向某块内存地址值，可以通过 * 解引用的方式进行使用。至于此处为什么错误类型需要返回一个指针变量，如下解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;主要是防止因为相同的字符串进行匹配，导致错误类型区别不准而导致的问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可能第一次听会难以理解，我们通过代码来解释，来看以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;errors&quot;
    &quot;fmt&quot;
)

type errorString string

func (e errorString) Error() string {
    return string(e)
}

func New(text string) error {
    return errorString(text)
}

var ErrorName = New(&quot;EOF&quot;)
var ErrorStructType = errors.New(&quot;EOF&quot;)

func main() {
    if ErrorName == New(&quot;EOF&quot;) {
        fmt.Println(&quot;Named Type Error&quot;)
    }
    if ErrorStructType == errors.New(&quot;EOF&quot;) {
        fmt.Println(&quot;Struct Type Error&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码输出的是 &lt;code&gt;Named Type Error&lt;/code&gt;，因为 &lt;code&gt;var ErrorStructType = errors.New(&quot;EOF&quot;)&lt;/code&gt; 与 &lt;code&gt;if ErrorStructType == errors.New(&quot;EOF&quot;)&lt;/code&gt; 虽然字符串的值相同，但是因为返回的是一个指针地址，地址不同的指针进行比较，当然也不同。所以这就是为什么要返回指针的原因。&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;panic&lt;/code&gt; 与 &lt;code&gt;error&lt;/code&gt; 的区别，主要是 &lt;code&gt;error&lt;/code&gt; 类型定义仅仅只作为值类型进行处理，其抛出错误，证明业务进行到此处，有错误，但是不影响全局，程序员在后续能进行处理。而 &lt;code&gt;panic&lt;/code&gt; 一般表示不可恢复类的错误，比如栈溢出、索引越界等程序员在后续无法进行处理的问题，必须修改代码才能解决问题。&lt;/p&gt;
&lt;h3&gt;错误类型&lt;/h3&gt;
&lt;p&gt;一般错误类型有以下几种：&lt;/p&gt;
&lt;p&gt;1、&lt;code&gt;Sentinel Error&lt;/code&gt; 预定义的错误，如 &lt;code&gt;io.EOF&lt;/code&gt;、&lt;code&gt;syscall.ENOENT&lt;/code&gt;。使用预定义的错误相当的不方便，所以尽可能避免使用预定义的错误，如果实在要使用，需要联系资深的程序员，进行 code review。&lt;/p&gt;
&lt;p&gt;2、&lt;code&gt;Error types&lt;/code&gt; 使用 &lt;code&gt;error interface&lt;/code&gt; 接口的自定义类型，定义后可以包装底层的错误，提供更多的上下文与堆栈的信息，方便程序员进行调试、定位问题。但是使用这种 interface 接口进行实现的 API 需要将 error 变成 public 字段，会导致和调用者产生强耦合，从而导致 API 变得脆弱。所以还是得尽量避免使用 error types。&lt;/p&gt;
&lt;p&gt;3、&lt;code&gt;Opaque errors&lt;/code&gt; 最为灵活的错误处理，主要是只返回错误，但是错误内部不透明，一般建议 API 的设计，对外只暴露一个 error 类型的返回值即可！&lt;/p&gt;
&lt;h3&gt;go 1.13 版本前的错误处理&lt;/h3&gt;
&lt;p&gt;一般的错误处理模式基本上是 &lt;code&gt;if err != nil {}&lt;/code&gt; 这种常见的模式。所以此处只写建议的事情：&lt;/p&gt;
&lt;p&gt;1、无第三方库协作的时候，简单使用 &lt;code&gt;errors.New()&lt;/code&gt; 或者 &lt;code&gt;errors.Errorf()&lt;/code&gt; 两个函数进行方法的处理。&lt;/p&gt;
&lt;p&gt;2、如果需要配合 github 开源第三方包以及公司中台组件内部的包，使用 &lt;code&gt;errors.Wrap()&lt;/code&gt; 与 &lt;code&gt;errors.Wrapf()&lt;/code&gt; 保存堆栈信息，最终集中化输出所有日志，而不是遇到错误就打印日志。&lt;/p&gt;
&lt;h3&gt;go 1.13 版本后的错误处理&lt;/h3&gt;
&lt;p&gt;go 在 1.13 版本给 &lt;code&gt;errors&lt;/code&gt; 包新增了两个函数方法：errors.Is() 与 errors.As()。并在 &lt;code&gt;fmt&lt;/code&gt; 包中也新增了 &lt;code&gt;fmt.Errorf()&lt;/code&gt; 函数方法，用于向错误中添加错误信息，占位符为 &lt;code&gt;%w&lt;/code&gt;，用于支持以上两个 errors 方法。具体使用案例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;err := fmt.Errorf(&quot;access denied: %w&quot;, ErrPermission)

if errors.Is(err, ErrPermission)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;go 日志错误处理注意事项&lt;/h3&gt;
&lt;p&gt;关于 &lt;code&gt;log.Fatal()&lt;/code&gt; 使用只推荐在 go 项目 init 函数中使用，或者 main 函数内初始化配置失败时候使用！因为 &lt;code&gt;log.Fatal()&lt;/code&gt; 函数会调用 &lt;code&gt;os.Exit(1)&lt;/code&gt;，此种情况下会导致程序直接退出，defer 函数都不会执行，不利于程序排查问题！&lt;/p&gt;
&lt;h3&gt;uber_go_guide 错误处理&lt;/h3&gt;
&lt;p&gt;1、若调用者需要自己处理其中抛出的错误，采用 error.As() 或 error.Is() 函数合适。&lt;/p&gt;
&lt;p&gt;2、错误如果不需要匹配，则如果是静态字符串，则使用 errors.New 进行抛出即可，若是动态字符串信息，则使用 fmt.Errorf 或者自定义的错误。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;错误匹配？&lt;/th&gt;
&lt;th&gt;错误消息&lt;/th&gt;
&lt;th&gt;指导&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;static&lt;/td&gt;
&lt;td&gt;errors.New&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;dynamic&lt;/td&gt;
&lt;td&gt;fmt.Errorf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;static&lt;/td&gt;
&lt;td&gt;top-level var with errors.New&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;dynamic&lt;/td&gt;
&lt;td&gt;custom error type&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://github.com/xxjwxc/uber_go_guide_cn#Errors&quot;&gt;Uber Go 语言编码规范&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;TDD 概念介绍与学习课程&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;在接触 TDD (测试驱动开发) 之后，发现自己的确很能认同测试驱动开发的理念。所以在加上极客时间中有徐昊老师新开的一门课程，所以就开始自己的 TDD 学习之路。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;首先推荐两个课程：&lt;/p&gt;
&lt;p&gt;1、徐昊老师的新课：&lt;a href=&quot;https://time.geekbang.org/column/intro/100109401?tab=catalog&quot;&gt;TDD 项目实战 70 讲&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;2、go 语言的教程：&lt;a href=&quot;https://studygolang.gitbook.io/learn-go-with-tests/&quot;&gt;Learn-go-with-test&lt;/a&gt;，建议能尽量进行英文阅读，就阅读英文原版&lt;/p&gt;
&lt;h3&gt;推荐&lt;/h3&gt;
&lt;p&gt;1、郑烨老师的专栏《软件设计之美》&lt;/p&gt;
&lt;p&gt;2、《测试驱动开发的艺术》&lt;/p&gt;
&lt;p&gt;3、《代码整洁之道》与《修改代码的艺术》&lt;/p&gt;
&lt;h3&gt;上手训练&lt;/h3&gt;
&lt;p&gt;可以查看博主推荐的 github 仓库，查看 TDD 具体的流程是怎么样进行的，以及 TDD 为什么能驱动程序员进行高效的开发：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;github: &lt;a href=&quot;https://github.com/longyue0521/TDD-In-Go/commits/main&quot;&gt;Alexdown 的 github 仓库&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/TDD1.png&quot; alt=&quot;TDD1&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;回看：TDD 的确重要，但是由于自己的工程能力并未到达老师所说的那种高度，所以说自己基础能力存在一定的问题，需要一点前置知识，比如看 uncle Bob 的 clean code、软件工程上的知识，都是对自己的职业发展非常重要的！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;TDD 实践&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;TDD 的项目驱动。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;TDD 具体原则&lt;/h3&gt;
&lt;p&gt;1、当且仅当存在失败的自动化测试，才开始编写生产代码&lt;/p&gt;
&lt;p&gt;2、消除重复（徐昊老师：消除坏味道）&lt;/p&gt;
&lt;p&gt;经典的红/绿/重构（Red/Green/Refactoring）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;红：编写一个失败的小测试，甚至可以是无法编译的测试；&lt;/p&gt;
&lt;p&gt;绿：让这个测试快速通过，甚至不惜犯下任何罪恶；&lt;/p&gt;
&lt;p&gt;重构：消除上一步中产生的所有重复（坏味道）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;TDD 工作流程&lt;/h3&gt;
&lt;p&gt;学习过程中，老师建议使用任务分解法，作为 TDD 的核心要素：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、先构思软件的使用方式，然后把握对外的接口方向&lt;/p&gt;
&lt;p&gt;2、大致的构思方向，划分组件以及组件之间的关系，如果没有头绪，也可以不划分&lt;/p&gt;
&lt;p&gt;3、根据需求的功能描述拆分功能点，功能点要考虑正确路径与边界条件&lt;/p&gt;
&lt;p&gt;4、根据组件与组件的关系，将功能划分到组件中&lt;/p&gt;
&lt;p&gt;5、针对拆分写测试，然后进入红/绿/重构模式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/TDD-2.jpg&quot; alt=&quot;TDD-2&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2022-6-9 日新增&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;1、通过学习 TDD，发现多余的代码会给团队增加编译成本、跑用例的时间成本（这两项影响不大），最重要的是影响理解的成本&lt;/li&gt;
&lt;li&gt;2、好的测试，就应该通过测试用例就能清晰的理解业务逻辑。TDD 的说法是写不出测试代码，就是不能理解整个业务需求。理解业务需求不需要通过一行一行的看代码进行理解&lt;/li&gt;
&lt;li&gt;3、TDD 对程序员来说是一种内功修炼，通过 TDD 会对自己的掌握的知识进行重现，比如语言特性、设计模式、重构手法以及对业务的理解&lt;/li&gt;
&lt;li&gt;4、TDD 是一种对做事方法的极致拆分，一次只做一件事，思考业务逻辑时就不考虑实现和代码坏味道；编写业务代码时，也仅考虑能通过用例的逻辑；而重构时，也是不能改变原来的代码逻辑的。通过一个个极小粒度的操作，实现最终整体的协调&lt;/li&gt;
&lt;li&gt;5、程序员需要对自己的编程的手艺进行匠艺的打磨，比如代码的鲁棒性，就是修改一个模块内部的代码后，需要修改其他的模块多不多。如果不多，证明 code 的鲁棒性良好，反之如果需要修改大量的模块，则证明不好&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;思考&lt;/h3&gt;
&lt;p&gt;发现自己花在 TDD 的时间上少了，所以为了保证自己的代码质量以及学习的方式，决定需要花更多的时间在 TDD 上，进行思考。项目选用 Java 先跟着老师的代码思路走，再针对 Go 语言进行自己的项目实现，如果只是自己从老师的项目出发去模仿老师的 Java 风格去写 Go，只会像一道菜一样&quot;串味&quot;而变得难吃。&lt;/p&gt;
&lt;h2&gt;Go Test 的一个小坑&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 TDD 的模式驱动自己编写测试、驱动开发后，使用 goland 调试过程中出现的一些小问题，做一些记录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;如何解决 undefined function&lt;/h3&gt;
&lt;p&gt;首先在确保自己的测试代码与主干代码在同一个软件包下。最合适的办法是采用命令行调试，但是如果喜欢使用 goland 调试，那么在 go test 模板中需要把目录改成软件包，或者是把目录下文件全部添加上（不建议）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/go-test.png&quot; alt=&quot;go-test&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Go mod init packagename&lt;/h3&gt;
&lt;p&gt;接着上一个问题，goland 进行 go test 测试很难用的原因，是因为自己在 go mod init 的时候，并未使用一个合理的名称，比如使用的 example，而在 function.go 中导入的包是 function name，导致 go mod 模式并未很好的利用，所以 go mod name 建议与文件夹的包名一致，这样不容易出现错误。&lt;/p&gt;
&lt;h2&gt;Go GET 代理&lt;/h2&gt;
&lt;p&gt;Windows 下在安装 github 上的东西时，一般来说建议使用修改 git 配置，使用代理连接 github。即为打开 &lt;code&gt;C:\Users\Lenovo&lt;/code&gt; 下的 &lt;code&gt;.gitconfig&lt;/code&gt; 文件，加上相关的代理配置。如果是 socks5 连接，把 &lt;code&gt;http:&lt;/code&gt; 改为 &lt;code&gt;socks5:&lt;/code&gt; 即可，127.0.0.1 为本地回环地址，7890 是端口号，端口号以你的代理工具使用的端口号为准。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;go get -u -v github.com/gin-gonic/gin&lt;/code&gt; 使用代理的情况下都能下载，唯一一个无法下载的包是 &lt;code&gt;google.golang.org/protobuf&lt;/code&gt;，不仅仅只是网址找不到的原因，更多的是在 &lt;code&gt;https://github.com/golang/protobuf&lt;/code&gt; 下没有这个包。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以那么上面问题又要怎么去解决呢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;搜索一番后发现，这个包使用了另外一个 github 仓库，地址为如下 &lt;code&gt;https://github.com/protocolbuffers/protobuf-go&lt;/code&gt;，我们使用 go get 去下载它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get -v -u github.com/protocolbuffers/protobuf-go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，这个源文件是下载在 gopath 里面的，为了方便使用，我们将下载到 gopath 的这个文件放到 gopath 下的 &lt;code&gt;google.golang.org&lt;/code&gt; 目录下，而且如果解压后文件有版本号时，去掉这个版本号，与上面包名保持一致即可。这样就解决好问题了。&lt;/p&gt;
&lt;p&gt;其中 go 现在采用 go mod 进行包模式的管理，所以说一般出现包管理问题，使用 &lt;code&gt;go mod tidy&lt;/code&gt; 可以解决一些依赖包的问题。&lt;/p&gt;
&lt;h2&gt;Go 学习方向（面试总结）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;通过几次面试，来总结一些现在阶段，对于自己的职业规划的发展与思考！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;经历了三四次面试，收获挺大的。面试不是目的，只是一种过程，要在不断的面试中，找到适合自己发展的领域才是面试的目的！&lt;/p&gt;
&lt;h3&gt;现阶段发展的方向&lt;/h3&gt;
&lt;p&gt;首先，Go 语言的云原生跟云中间件发展是非常迅速的，而且 Go 语言社区中，讨论最多的也是云计算的发展，docker、K8S集群、普罗米修斯，都是云中间件的代表，所以去学习其中设计的思想、源码的思想是非常重要的！&lt;/p&gt;
&lt;h3&gt;Go 面试问题总结&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、数组与链表的区别（数据结构基础知识）&lt;/p&gt;
&lt;p&gt;2、进程、线程、协程有什么区别（操作系统的知识，加 Go 语言特有的协程）&lt;/p&gt;
&lt;p&gt;3、Redis 与 MYSQL 熟悉吗，Redis 有哪几种数据结构，缓存过期（TTL 网络中的生存时间），Redis 一般使用在哪一些场景里面（针对热点数据进行缓存、限时数据缓存、热点权值数据进行缓存）&lt;/p&gt;
&lt;p&gt;4、channel 有几种类型（自己回答是读、写、读写三种，不一定对，也可能 chan int、string、byte）&lt;/p&gt;
&lt;p&gt;5、控制 Goroutine 数量的几种方式（channel 控制，sync.WaitGroup 控制）&lt;/p&gt;
&lt;p&gt;6、Go 语言调度模型：GMP 模型（hand off 机制没回答特别好）&lt;/p&gt;
&lt;p&gt;7、Go 语言的 GC（标记清除法、三色标记法、混合写屏障机制）&lt;/p&gt;
&lt;p&gt;8、Go GC 在什么时候会导致 GC 效率不高（STW 次数多的时候效率不高，毕竟依赖于 STW 机制）&lt;/p&gt;
&lt;p&gt;9、TCP 的 CLOSE_WAIT 状态出现在那一步&lt;/p&gt;
&lt;p&gt;10、slice 与 map 底层源码（slice 底层为指针 Array 类型，Go 中的 map 是 slice + list 数组加链表，而 java 里面是最简单的是数组，规模达到一定程度转换为数组 + 链表或数组 + 红黑树的形式）&lt;/p&gt;
&lt;p&gt;11、Go 语言的内存泄漏、内存逃逸问题（内存逃逸就是栈上开辟空间存放的变量，逃逸到堆上去了，内存泄漏主要是看 OS 的环境，Linux 下使用 top/htop）&lt;/p&gt;
&lt;p&gt;12、几道 Go 语言面试的题目：闭包传参、向空 slice append 数等&lt;/p&gt;
&lt;p&gt;13、Go 语言如何 debug（自己回答是采用单元测试的办法、pprof 等方式方法）&lt;/p&gt;
&lt;p&gt;14、算法题目：三数之和，返回二维数组，二维数组内不能有重复值，即数组内元素相等&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;5 月 21 日温故而知新&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、Go 语言中协程出现了 panic 的情况，有什么样的机制保证协程恢复并继续执行下去？&lt;/p&gt;
&lt;p&gt;2、Go 语言中的调试信息使用什么查看？&lt;/p&gt;
&lt;p&gt;3、Go 语言中的内存模型与内存回收机制&lt;/p&gt;
&lt;p&gt;4、Go 与 Python 语言的协程对比有什么区别&lt;/p&gt;
&lt;p&gt;5、&lt;strong&gt;Mysql 里面执行计划的概念&lt;/strong&gt;（之前复盘都没有听出来这个问题，一个劲在说不知道）&lt;/p&gt;
&lt;p&gt;6、redo-log、undo-log、bin-log 三个 log 文件的作用与区别，如何使用 redo 与 bin 两种日志去保证数据的一致性？&lt;/p&gt;
&lt;p&gt;7、&lt;strong&gt;数据库出现过性能问题嘛，千万级数据量的表如何进行一个排查+处理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;8、gorm 操作数据库是如何操作的？&lt;/p&gt;
&lt;p&gt;9、&lt;strong&gt;redis 中 AOF 与 RDB 两种快照日志主要是为了解决什么样的问题？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;10、gin 框架的源码看过吗，里面路由具体的实现机制是怎样的&lt;/p&gt;
&lt;p&gt;11、平时关注哪些开源技术？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Go 面试复盘&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;先上总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;面经光看没用，只有自己不断的面试，然后总结，自己理解性的进行描述才有意义，否则只是像八股文一样去背记，从而未能理解真正的含义。那么，可能下一次面试的时候，上一次面试的问题依然处于一种遗留的状态，这样就无法在技术的关键节点进行成长&lt;/li&gt;
&lt;li&gt;尤其是算法题，如果一两个月不复盘算法问题，那么算法思维存在，但是代码熟练度会下降，从而导致写算法的时间变长，以及相应的心态焦虑&lt;/li&gt;
&lt;li&gt;以下为自己答的不好与没答出的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;TCP 三次握手&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;答：TCP 的三次握手为：&lt;/p&gt;
&lt;p&gt;1、客户端发送的报文为 SYN 报文，并选择一个初始的 Seq 序号，之后客户端进入监听状态（SYN-SENT）&lt;/p&gt;
&lt;p&gt;2、服务器在接收到客户端第一次发送的 SYN 报文之后，如果同意连接，即向客户端发送连接确认报文，即 SYN + ACK 报文，也附加一个自选的初始 Seq 序号，并且此序号与客户端的序号无关，之后服务器端继续维持监听状态（SYN-RCVD）&lt;/p&gt;
&lt;p&gt;3、客户端在接收到服务器发送回的报文之后，再向服务器端发送确认报文，确认号为服务器初始的 Seq 序号 + 1，序号为自己初始的 Seq 序号 + 1&lt;/p&gt;
&lt;p&gt;4、此后服务器与客户端正式建立连接，开始发送数据，双方状态为 ESTABLISHED&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/tcp3.png&quot; alt=&quot;tcp3&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;TCP 需要三次握手的原因&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;答：采用第三次握手的原因是：&lt;/p&gt;
&lt;p&gt;1、如果第一次客户端的报文中途出现延迟，而客户端开始重发第一次的报文，并且重发报文被服务器正确接收&lt;/p&gt;
&lt;p&gt;2、而此时，第一次客户端发送的报文又到达服务端，服务端接收后，又返回一个报文，相当于服务器同一个客户端建立了两个连接，而客户端只认为自己建立的一个连接，造成了状态不一致，同时服务器的资源也被浪费了&lt;/p&gt;
&lt;p&gt;3、故为了尽可能保证连接的建立及时、有效且资源节约，故采用 TCP 三次握手&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;TCP 四次挥手&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;答：TCP 的四次挥手过程为：&lt;/p&gt;
&lt;p&gt;1、首先由客户端向服务器端发送关闭连接请求报文，即 FIN 报文，此时客户端由 ESTABLISHED 状态转变为 FIN-WAIT-1 状态，此时还能继续接收服务器所发送的数据&lt;/p&gt;
&lt;p&gt;2、服务器在接收到客户端发送的 FIN 包后，向客户端发送确认报文，即 ACK 报文，此时服务器的状态由 ESTABLISHED 状态转变为 CLOSED-WAIT 状态，此时服务器端还能继续把未能发送完的数据继续发送&lt;/p&gt;
&lt;p&gt;3、客户端在接收到服务器端发回的 ACK 确认报文之后由 FIN-WAIT-1 转变为 FIN-WAIT-2 状态，并且能继续接收数据，直到服务器发送终止报文，即 FIN 包为止&lt;/p&gt;
&lt;p&gt;4、服务器向客户端发送 FIN 包，此时服务器端由 CLOSED-WAIT 状态转变为 LAST-ACK 状态，等待客户端返回最后一次确认报文，即 ACK 为止&lt;/p&gt;
&lt;p&gt;5、客户端接收到 FIN 包后，由 FIN-WAIT-2 变为 TIME-WAIT 状态，超过一定的时间后自动转变为 CLOSED 状态&lt;/p&gt;
&lt;p&gt;6、服务器端收到客户端的 ACK 报文后，即由 LAST-ACK 状态转变为 CLOSED 状态，不再发送与接收客户端的数据&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/tcp4.png&quot; alt=&quot;tcp4&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;TCP 需要四次挥手的原因&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;答：采用第四次挥手的原因是：&lt;/p&gt;
&lt;p&gt;1、如果客户端第四次发送 ACK 报文后就直接进入 CLOSED 状态，那么如果第四次发送的 ACK 报文在传输的过程中丢失，服务器由于一直未能接收客户端发送的 ACK 报文，再次向客户端发送相应的 FIN 报文，而此时客户端已经关闭，接收不到服务器发送的 FIN 报文。即造成了服务器的资源浪费&lt;/p&gt;
&lt;p&gt;2、故为了保证通信尽可能的可靠，采用 TCP 四次挥手，但是在考研中，有种特殊的情况，在确保第三次握手能成立的情况下，第四次挥手可以被省略。若将一个往返视为 RTT 的情况下，最短的释放连接所需要的时间为 1.5 个 RTT 即可，不需要 2 个 RTT，所以这也是为了节省资源所考虑的情况，并不一定视为错误答案&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;TCP 粘包问题&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;答：TCP 对比 UDP，前者主要是以字节流的形式传输数据，而 UDP 则以报文的形式传输数据。&lt;/p&gt;
&lt;p&gt;其中报文与字节流的区别主要是，字节流传输是以字节为单位进行数据传输，与每一个数据中的独立的内容无关，而报文传输为传输以报文为单位，保留了报文内容的边界。所以就会导致 TCP 传输字节时候，有可能区分不了数据的边界，导致最后解包数据所造成的解码乱码现象，即 TCP 的粘包的问题。&lt;/p&gt;
&lt;p&gt;解决 TCP 粘包问题的办法有许多种，其中在之前所学的 Zinx 框架中，刘丹冰老师（Aceld 老师）解释了一种 TLV 格式的封包解包办法，即使用 datalen、msgID、data 作为封包解包的字段，首先读取一遍封包的头部长度，即 datalen 与 msgID 字段的大小，此处按照自己设置的来。一般设置是两个 uint32 类型，即为 8 Byte 的。&lt;/p&gt;
&lt;p&gt;其中解析开头的 datalen 为 data 字段的数据长度，msgID 为相应的数据包编号。然后根据开头的 datalen 的具体数值来读取之后的 data 段的数据。至于 TCP 发送数据流中出现的错误，利用好 TCP 的错误重传机制就好。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/TLV.jpeg&quot; alt=&quot;TLV&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;写代码：channel&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//多多练习一下代码能力，要脱离相应的视频项目开发，变为生产实际开发。
//有些东西，一段时间没用就会忘记，比如算法时间复杂度，以及相应的代码熟练度。

//实现两个channel，一个 channel 输出 ping，一个channel 输出 pang，并发执行。

//22-5-13 日再回头看，发现问题很显然了，自己当时写一个经典的并发案例没写出来
//就是属于基础不够好，这个代码也不符合面试官需要达到的要求，只是效果达到了。

package main

import &quot;fmt&quot;

func main() {
	p := make(chan string)
	q := make(chan string)

	go func() {
		p &amp;lt;- &quot;ping&quot;
	}()

	go func() {
		q &amp;lt;- &quot;pang&quot;
	}()

	c := &amp;lt;-q
	a := &amp;lt;-p

	fmt.Printf(&quot;type of c : %T\n&quot;, c)
	fmt.Printf(&quot;type of a : %T\n&quot;, a)

	for {
		fmt.Println(a)
		fmt.Println(c)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Go 中 map 查询的时间复杂度为 O(1)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;答：查看了相应的各种博客，了解到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1、java 中 map 的底层源代码实现为数组 + 链表 + 红黑树，所以在数据量极小的情况下，相应的&lt;strong&gt;查询时间复杂度为 O(1)&lt;/strong&gt;，而在数据量较多的情况，map 的查询时间复杂度应该是大于 O(1)，小于 O(N)，接近 O(logN) 的时间复杂度的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2、所以说关于 map 的查询时间复杂度是一个很老的问题了，一般使用情况默认为 O(1) 的时间复杂度。但是这并不意味着就要否认 map 的查询时间复杂度是 O(logN) 的说法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3、不懂不理解的问题，一定需要事后进行相关资料的查询，以及总结&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;关于 map 查询的时间复杂度，&lt;a href=&quot;https://stackoverflow.com/questions/1055243/is-a-java-hashmap-search-really-o1&quot;&gt;StackOverFlow&lt;/a&gt; 上给出的说法挺多：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/mapsearch.png&quot; alt=&quot;mapsearch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/mapAnswer1.png&quot; alt=&quot;mapAnswer1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/mapAnswer2.png&quot; alt=&quot;mapAnswer2&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;GMP 模型&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;强烈推荐 Aceld 老师的 GMP 模式详细解释，可以明白调度器的调度行为。GMP 模型已经单独作为一篇博客存在，所以此处不再阐述。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Go GC 在什么情况下性能比较低&lt;/h3&gt;
&lt;p&gt;1、内存泄露。&lt;/p&gt;
&lt;p&gt;2、小对象，结构体比指针的好。&lt;/p&gt;
&lt;h2&gt;开源的思考&lt;/h2&gt;
&lt;p&gt;本人曾经的职业规划是，在读研后学习 NLP、CV 等领域的算法论文，而后向着算法工程师的方向进行努力。而近几年的考研难度以及行业的形式导致自己的期待与期望成了泡沫。在二战也未能达到名校的分数线后，自己毅然决定，不调剂，投入社会进行学习与工作。&lt;/p&gt;
&lt;p&gt;但是困扰自己的问题出现了，学习什么样的技术、向什么样的行业发展，成为自己的一大困惑。通过一段时间的自我认知与自我反省，想到了自己曾经大三时候学习的 go 语言，并且使用 beego 框架进行相应的简易博客开发，所以方向是 go 语言工程师，go 语言中的几个方向：云原生、微服务、高性能 API、服务器端开发、游戏开发。自己这几个方向都还是比较喜欢的，更坚定了自己使用 go 语言进行找工作的决心。&lt;/p&gt;
&lt;p&gt;而在今天看到了云原生社区对 APISIX 的联合创始人&amp;amp;CEO 温铭做的采访，其中几个观点，我也是非常认可其中的理念：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、开源比业务代码更有意义&lt;/p&gt;
&lt;p&gt;2、开源的本质是要拿开发者的杠杆&lt;/p&gt;
&lt;p&gt;3、Apache 的理念是社区比代码更重要&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;自己的确非常喜欢开源社区，也十分认同开源的理念，就是能让自己的智慧以及不断发展的能力，给开源社区做出贡献。&lt;/p&gt;
</content:encoded></item><item><title>Hudi 原理、部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/11/hudi-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/hudi-deploy-tuning/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Hudi 是 Uber 开源的增量数据湖框架：给&quot;不可变&quot;的数据湖加上更新与增量读取能力。它与 Iceberg、Paimon 并称湖格式三兄弟，特点是 &lt;strong&gt;Copy-on-Write / Merge-on-Read 双表型&lt;/strong&gt;与&lt;strong&gt;成熟的索引体系&lt;/strong&gt;。本文覆盖原理、集成部署（Spark/Flink）、参数优化与常见问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理速览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;两种表类型&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;COW（Copy-on-Write，写时复制）：更新时重写整个 Parquet 文件，读简单、写放大&lt;/li&gt;
&lt;li&gt;MOR（Merge-on-Read，读时合并）：更新写小 log 文件（Avro），base 文件 + log 文件读时合并，写快读慢，需 Compaction&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timeline（时间线）&lt;/strong&gt;：所有操作（commit/compaction/clustering/rollback）按时间记录在 &lt;code&gt;.hoodie&lt;/code&gt; 目录，是 ACID 与恢复的基础&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;索引（定位旧文件）&lt;/strong&gt;：bloom（默认，布隆过滤器）、HBase、bucket（哈希桶）——upsert 时靠索引定位&quot;这条记录在哪&quot;来决定更新还是插入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增量查询（Incremental）&lt;/strong&gt;：按 commit 时间过滤，读新增/变更数据，配合 CDC 管道做增量同步&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clustering（聚类）&lt;/strong&gt;：把碎片小文件合并成合理大小，控制文件数与查询效率&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 Iceberg/Paimon 对比&lt;/strong&gt;：Hudi 索引体系最成熟、Spark 生态最好、COW/MOR 双模式灵活；Iceberg 元数据规范更简洁、时间旅行更强；Paimon 流式读写最强&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Hudi 的价值一句话：&lt;strong&gt;让&quot;更新&quot;成为湖的一等公民&lt;/strong&gt;。传统 Hive 湖是&quot;覆盖目录&quot;，Hudi 是&quot;按记录更新 + 保留版本&quot;，这对需要 CDC 同步、增量入湖的政企数仓是刚需。
:::&lt;/p&gt;
&lt;h2&gt;部署（集成式，无独立服务）&lt;/h2&gt;
&lt;h3&gt;1. 集成 Spark（Hudi 的主战场）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 使用与 Spark 版本匹配的 hudi-spark bundle
spark-shell \
  --packages org.apache.hudi:hudi-spark3.5-bundle_2.12:0.15.x \
  --conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
  --conf spark.sql.catalog.spark_catalog=org.apache.spark.sql.hudi.catalog.HoodieCatalog \
  --conf spark.sql.extensions=org.apache.spark.sql.hudi.HoodieSparkSessionExtension
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 建 COW 表
CREATE TABLE hudi_orders (
  order_id BIGINT PRIMARY KEY, user_id BIGINT, amount DECIMAL(20,2), ts TIMESTAMP)
USING hudi
TBLPROPERTIES (
  &apos;type&apos; = &apos;cow&apos;,                                -- 或 mor
  &apos;primaryKey&apos; = &apos;order_id&apos;,
  &apos;preCombineField&apos; = &apos;ts&apos;,
  &apos;hoodie.table.partition.fields&apos; = &apos;ts&apos;         -- 分区字段（日期）
) PARTITIONED BY (ts);

-- upsert 写入（存在则更新，不存在则插入）
MERGE INTO hudi_orders t USING src s ON t.order_id = s.order_id
WHEN MATCHED THEN UPDATE SET t.amount = s.amount
WHEN NOT MATCHED THEN INSERT VALUES (s.order_id, s.user_id, s.amount, s.ts);

-- 增量查询（时间点之后的变化数据）
SELECT * FROM hudi_orders
WHERE _hoodie_commit_time &amp;gt;= &apos;20260811000000000&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 集成 Flink&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 拷贝 hudi-flink（带 connector 版本）到 $FLINK_HOME/lib
# Flink SQL 建表（写 Hudi）
CREATE TABLE hudi_orders (
  order_id BIGINT, user_id BIGINT, amount DECIMAL(20,2), ts TIMESTAMP(3),
  PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
  &apos;connector&apos; = &apos;hudi&apos;,
  &apos;path&apos; = &apos;hdfs:///hudi/orders&apos;,
  &apos;table.type&apos; = &apos;MERGE_ON_READ&apos;,      -- COPY_ON_WRITE / MERGE_ON_READ
  &apos;hoodie.datasource.write.recordkey.field&apos; = &apos;order_id&apos;,
  &apos;hoodie.datasource.write.precombine.field&apos; = &apos;ts&apos;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Hive 同步&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 写完后同步 Hive 元数据（Spark 侧）
spark-sql -e &quot;CALL sync_hive_metadata(&apos;hudi_orders&apos;)&quot;
# 或使用 hudi 自带同步工具（配置 hoodie.datasource.hive_sync.* 参数自动同步）

# Hive 侧查询前注册
ADD JAR hudi-hadoop-mr-bundle-*.jar;
CREATE EXTERNAL TABLE hudi_orders_hive (...) STORED AS PARQUET
TBLPROPERTIES (&apos;hoodie.table.name&apos;=&apos;hudi_orders&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Hudi CLI（运维工具）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;hudi-cli
# 连接表：connect --path hdfs:///hudi/orders
# 常用命令：commits show、compaction show、clustering show、index stats
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调优&lt;/h2&gt;
&lt;h3&gt;1. 写入&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hoodie.datasource.write.recordkey.field&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;主键&lt;/td&gt;
&lt;td&gt;索引定位依据，务必选稳定唯一列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hoodie.datasource.write.operation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;upsert / bulk_insert&lt;/td&gt;
&lt;td&gt;全量初装用 bulk_insert，增量用 upsert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hoodie.parquet.small.file.limit&lt;/code&gt;（默认 104857600）&lt;/td&gt;
&lt;td&gt;100M&lt;/td&gt;
&lt;td&gt;小文件合并阈值，写入时自动补齐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hoodie.copyonwrite.record.size.estimate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按实际记录大小&lt;/td&gt;
&lt;td&gt;影响目标文件数估算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写并行度&lt;/td&gt;
&lt;td&gt;与分区数/文件数匹配&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hoodie.datasource.write.operation=bulk_insert&lt;/code&gt; 时并行度=桶/分区数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. Compaction（MOR 专属）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;异步 vs 同步&lt;/strong&gt;：生产用异步（&lt;code&gt;hoodie.compact.async=true&lt;/code&gt; + 独立 compaction 调度），避免阻塞写&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;触发&lt;/strong&gt;：&lt;code&gt;hoodie.compact.inline.max.delta.commits&lt;/code&gt;（如 10）或按时间；延迟合并会让 log 文件膨胀、读变慢&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compaction 并行&lt;/strong&gt;：&lt;code&gt;hoodie.compaction.task.parallelism&lt;/code&gt; 按 CPU 核数的一半&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Clustering&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;触发策略：&lt;code&gt;hoodie.clustering.inline=true&lt;/code&gt; + &lt;code&gt;hoodie.clustering.inline.max.commits&lt;/code&gt;（如 4-6）&lt;/li&gt;
&lt;li&gt;计划与执行分离：&lt;code&gt;clustering plan&lt;/code&gt;（评估）→ &lt;code&gt;clustering run&lt;/code&gt;（执行），避免影响在线写&lt;/li&gt;
&lt;li&gt;目标文件大小：&lt;code&gt;hoodie.clustering.plan.strategy.target.file.max.bytes&lt;/code&gt;（默认 1G 左右）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 索引&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;默认 bloom 索引在数据量百万级以上会有误判开销；记录多且主键有序时评估 &lt;strong&gt;bucket index&lt;/strong&gt;（哈希桶，写入时直接算桶，免布隆过滤）&lt;/li&gt;
&lt;li&gt;HBase 索引适合跨表全局索引，但要养 HBase 集群，非必须不引入&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hoodie.bloom.index.parallelism&lt;/code&gt; 调大可缩短 upsert 定位耗时&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 读取&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;增量查询：&lt;code&gt;_hoodie_commit_time&lt;/code&gt; 过滤要加索引列；Flink/Spark 引擎记得开启 hudi 扩展&lt;/li&gt;
&lt;li&gt;MOR 读慢：优先异步 compaction，读路径 &lt;code&gt;hoodie.mor.compaction.lazy&lt;/code&gt; 评估&lt;/li&gt;
&lt;li&gt;缓存：Spark 侧 &lt;code&gt;spark.sql.hive.convertMetastoreParquet=false&lt;/code&gt; 场景注意兼容&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;upsert 慢&lt;/td&gt;
&lt;td&gt;索引定位开销大&lt;/td&gt;
&lt;td&gt;调 bloom 并行度，或评估 bucket index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MOR 读越来越慢&lt;/td&gt;
&lt;td&gt;log 文件积压未 compact&lt;/td&gt;
&lt;td&gt;调度异步 compaction，监控 delta 数量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;小文件多&lt;/td&gt;
&lt;td&gt;高频写 + 无聚类&lt;/td&gt;
&lt;td&gt;small.file.limit + clustering 定时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;同步 Hive 失败&lt;/td&gt;
&lt;td&gt;hive_sync 参数缺失&lt;/td&gt;
&lt;td&gt;检查 hive_sync.enable/uri 配置与权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;表目录无限膨胀&lt;/td&gt;
&lt;td&gt;timeline 未归档&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hoodie.timeline.layout.version&lt;/code&gt; 与归档策略配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;并发写冲突&lt;/td&gt;
&lt;td&gt;多任务写同一分区&lt;/td&gt;
&lt;td&gt;写任务错峰，或按分区隔离任务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Hudi = 增量数据湖：COW/MOR 双表型 + Timeline + 索引体系 + 增量查询 + Clustering&lt;/li&gt;
&lt;li&gt;部署 = Spark/Flink 集成（bundle jar + 建表属性）+ Hive 同步 + hudi-cli 运维；无独立服务&lt;/li&gt;
&lt;li&gt;调优主线：记录键与操作类型（upsert/bulk_insert）→ 异步 compaction（MOR）→ clustering 定时 → 索引选型 → 增量读&lt;/li&gt;
&lt;li&gt;在底座迭代中，Hudi 适合&quot;存量 Hive 体系平滑升级&quot;：Spark 生态内增量入湖、CDC 同步成本最低；若团队以 Flink 为主且要流读流写，优先 Paimon——两个湖格式按团队引擎画像选一个为主，别双养&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Gravitino 原理、部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/11/gravitino-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/gravitino-deploy-tuning/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Gravitino 是 Apache 孵化的统一元数据管理层：把 Hive、Iceberg、MySQL、Kafka、AI 模型等分散的元数据统一到一个&quot;Metalake&quot;里管理，并提供统一的权限与审计。它是湖仓一体时代&quot;多引擎共享一份元数据&quot;的基础设施。本文覆盖原理（四级模型）、部署（gravitino-server + Catalog 对接）、参数优化与常见问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理速览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;四级模型&lt;/strong&gt;：&lt;code&gt;Metalake&lt;/code&gt;（租户级命名空间，相当于一个&quot;数据域&quot;）→ &lt;code&gt;Catalog&lt;/code&gt;（对接具体系统：hive/iceberg/mysql/kafka/模型）→ &lt;code&gt;Schema&lt;/code&gt; → &lt;code&gt;Table&lt;/code&gt;；统一视图查询，不复制数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不存数据只存映射&lt;/strong&gt;：Gravitino 存&quot;哪个 Metalake 的哪个 Catalog 连到哪个系统的什么位置&quot;，真实元数据实时从后端系统读取&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多租户隔离&lt;/strong&gt;：一个 Gravitino 实例可建多个 Metalake，不同部门/项目互不可见，权限在 Metalake 层面隔离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一安全&lt;/strong&gt;：提供统一访问控制（授权模型），把 Hive 的 Ranger 权限、MySQL 的 GRANT 等差异化权限抽象成统一规则，支持 LDAP/内部用户认证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 数据管理&lt;/strong&gt;：内置 Model Catalog——模型（LLM 等）作为一等实体管理，为 Data+AI 融合提供元数据底座&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Messaging 管理&lt;/strong&gt;：支持 Kafka 类消息系统的元数据统一管理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;REST 协议&lt;/strong&gt;：所有访问走 REST API（Java 客户端/JDBC 驱动），服务端无状态，可水平扩展&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Gravitino 的定位一句话：&lt;strong&gt;元数据领域的&quot;统一门户&quot;&lt;/strong&gt;。底座迭代到湖仓一体阶段，表分散在 Hive/Iceberg/MySQL 多个体系，Gravitino 提供&quot;一张清单查所有 + 一套权限管所有&quot;的入口，也是 Iceberg REST Catalog 的推荐宿主。
:::&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;h3&gt;1. 服务端部署&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 下载并解压（Apache Gravitino，目录名 gravitino-0.8.x）
wget https://archive.apache.org/dist/gravitino/0.8.x/apache-gravitino-0.8.x-bin.tar.gz
tar -zxvf apache-gravitino-*.tar.gz -C /opt &amp;amp;&amp;amp; ln -s /opt/apache-gravitino-* /opt/gravitino
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# conf/gravitino.conf 关键项
# 监听地址与端口（默认 8090，REST 端口）
gravitino.server.webserver.host = 0.0.0.0
gravitino.server.webserver.port = 8090

# JVM 内存（conf/gravitino-env.sh）
GRAVITINO_JVM_MEMOPTS=&quot;-Xmx4g -Xms1g -XX:MaxMetaspaceSize=512m&quot;

# 认证（可选，生产建议开启 LDAP）
gravitino.authenticator = simple    # 或 ldap
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 启动与验证
bin/gravitino.sh start
curl http://node1:8090/api/version
# 创建 Metalake（REST API）
curl -X POST http://node1:8090/api/metalakes \
  -H &apos;Content-Type: application/json&apos; \
  -d &apos;{&quot;name&quot;:&quot;demo&quot;,&quot;comment&quot;:&quot;demo lake&quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
Gravitino 自身不存业务数据，但它依赖数据库（默认使用内嵌 H2 存储授权与 Metalake 配置）。生产要换成 MySQL/PostgreSQL（&lt;code&gt;conf/gravitino.conf&lt;/code&gt; 配置 JDBC 地址），否则重启数据丢失风险高。
:::&lt;/p&gt;
&lt;h3&gt;2. 对接 Hive Catalog（最常见）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建指向现有 HMS 的 Hive Catalog
curl -X POST http://node1:8090/api/metalakes/demo/catalogs \
  -H &apos;Content-Type: application/json&apos; -d &apos;{
    &quot;name&quot;:&quot;hive_catalog&quot;,
    &quot;type&quot;:&quot;hive&quot;,
    &quot;properties&quot;:{
      &quot;metastore.uris&quot;:&quot;thrift://node1:9083&quot;,
      &quot;client.pool.size&quot;:&quot;5&quot;
    }
  }&apos;

# 验证：列出该 catalog 下的库表
curl http://node1:8090/api/metalakes/demo/catalogs/hive_catalog/databases
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 对接 Iceberg（REST Catalog 形态）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建 Iceberg Catalog（Gravitino 内置 Iceberg REST Catalog 服务）
curl -X POST http://node1:8090/api/metalakes/demo/catalogs \
  -H &apos;Content-Type: application/json&apos; -d &apos;{
    &quot;name&quot;:&quot;iceberg_catalog&quot;,
    &quot;type&quot;:&quot;iceberg&quot;,
    &quot;properties&quot;:{
      &quot;uri&quot;:&quot;thrift://node1:9083&quot;,          # 底层复用 HMS 或指向对象存储
      &quot;warehouse&quot;:&quot;hdfs:///iceberg_warehouse&quot;,
      &quot;catalog-backend&quot;:&quot;hive&quot;
    }
  }&apos;

# Spark 通过 Gravitino 访问 Iceberg 表：
# --conf spark.sql.catalog.demo_iceberg=org.apache.gravitino.spark.connector.iceberg.IcebergConnector
# --conf spark.sql.catalog.demo_iceberg.gravitino.uri=http://node1:8090
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 对接 MySQL / Kafka / Model&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# MySQL Catalog（统一管理业务库元数据）
curl -X POST ... -d &apos;{
  &quot;name&quot;:&quot;mysql_catalog&quot;,&quot;type&quot;:&quot;jdbc-mysql&quot;,
  &quot;properties&quot;:{&quot;jdbc-url&quot;:&quot;jdbc:mysql://node1:3306&quot;,&quot;jdbc-user&quot;:&quot;u&quot;,&quot;jdbc-password&quot;:&quot;p&quot;}}&apos;

# Kafka Catalog（消息元数据统一纳管）
curl -X POST ... -d &apos;{
  &quot;name&quot;:&quot;kafka_catalog&quot;,&quot;type&quot;:&quot;kafka&quot;,
  &quot;properties&quot;:{&quot;bootstrap.servers&quot;:&quot;node1:9092&quot;}}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 客户端接入&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Java 客户端&lt;/strong&gt;：&lt;code&gt;mvn&lt;/code&gt; 引入 &lt;code&gt;gravitino-client&lt;/code&gt;，&lt;code&gt;GravitinoClient.builder(&quot;http://node1:8090&quot;).withMetalake(&quot;demo&quot;).build()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JDBC&lt;/strong&gt;：&lt;code&gt;jdbc:gravitino://node1:8090&lt;/code&gt;，供 BI 工具/管理端使用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限管理&lt;/strong&gt;：LDAP 认证开启后，用户在 Metalake 级授予 &lt;code&gt;CREATE/TABLE&lt;/code&gt; 等权限，授权记录统一审计&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优&lt;/h2&gt;
&lt;h3&gt;1. 服务端&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gravitino.server.webserver.maxThreads&lt;/code&gt;（默认 200）&lt;/td&gt;
&lt;td&gt;200-500&lt;/td&gt;
&lt;td&gt;REST 并发线程，客户端多时调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM &lt;code&gt;-Xmx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4G-16G&lt;/td&gt;
&lt;td&gt;元数据操作在服务端线程内完成，缓存/并发大时调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gravitino.entity.store.kv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;生产换 MySQL/PG&lt;/td&gt;
&lt;td&gt;内嵌 H2 只用于单机测试&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. 后端连接&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;连接池&lt;/strong&gt;：Hive Catalog 的 &lt;code&gt;client.pool.size&lt;/code&gt;（默认 5），并发查询多时调到 10-20；MySQL JDBC 池同理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时&lt;/strong&gt;：&lt;code&gt;client.pool.timeout&lt;/code&gt;（默认 300s）与后端系统（HMS 9083）超时匹配，避免客户端挂起&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存&lt;/strong&gt;：&lt;code&gt;gravitino.entity.store&lt;/code&gt; 默认有缓存，频繁的元数据列表查询可观察内存占用调大缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 高可用与容量&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Gravitino 无状态，&lt;strong&gt;多实例部署 + 前端负载均衡&lt;/strong&gt;即可横向扩展（共享同一数据库）&lt;/li&gt;
&lt;li&gt;实例数与并发规模 1:1 起步（如 2 实例服务 2000 次/分钟查询）&lt;/li&gt;
&lt;li&gt;依赖的底层系统（HMS/MySQL）才是瓶颈，监控后端连接与响应时间&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 安全&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产必须开启认证（LDAP/内部用户），Metalake 级授权做隔离&lt;/li&gt;
&lt;li&gt;REST 端点仅内网可达；HTTPS 由反向代理终结&lt;/li&gt;
&lt;li&gt;审计日志（&lt;code&gt;gravitino.audit.log&lt;/code&gt;）按需开启，配合统一审计平台&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;创建 Catalog 失败&lt;/td&gt;
&lt;td&gt;后端连接参数错/网络不通&lt;/td&gt;
&lt;td&gt;检查 metastore.uris、JDBC 连通性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询慢&lt;/td&gt;
&lt;td&gt;后端系统慢 + 连接池小&lt;/td&gt;
&lt;td&gt;调 client.pool.size，检查 HMS 负载&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重启后配置丢失&lt;/td&gt;
&lt;td&gt;用了内嵌 H2&lt;/td&gt;
&lt;td&gt;换 MySQL/PG 存储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;权限不生效&lt;/td&gt;
&lt;td&gt;认证模式未开启&lt;/td&gt;
&lt;td&gt;配置 authenticator + 重启，确认 LDAP 连通&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark 连不上&lt;/td&gt;
&lt;td&gt;catalog 参数名拼错&lt;/td&gt;
&lt;td&gt;核对 spark.sql.catalog.*.gravitino.uri&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;版本兼容&lt;/td&gt;
&lt;td&gt;客户端与服务端版本不一致&lt;/td&gt;
&lt;td&gt;客户端与服务端保持同一大版本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Gravitino = 统一元数据管理：Metalake/Catalog/Schema/Table 四级模型，不存数据只存映射，统一权限与审计，内置 AI Model 与 Kafka 管理&lt;/li&gt;
&lt;li&gt;部署 = gravitino-server（+ MySQL 存储）→ 建 Metalake → 对接各 Catalog（Hive/Iceberg/JDBC/Kafka）→ 客户端接入；服务无状态可水平扩展&lt;/li&gt;
&lt;li&gt;调优主线：服务端线程与内存 → 后端连接池 → 生产数据库 → 认证与审计&lt;/li&gt;
&lt;li&gt;在底座迭代中，Gravitino 是湖仓一体的&quot;元数据中枢&quot;：Iceberg 表、Hive 表、MySQL 业务库统一纳管，为后续 AI 数据管道与统一数据资产运营打底&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Jenkins 原理、部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/11/jenkins-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/jenkins-deploy-tuning/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;大数据底座的迭代思路里提到&quot;引入 CI/CD，把组件部署从手工变为流水线&quot;，落地首选就是 Jenkins。本文按&quot;原理 → 部署 → 调优&quot;三步整理企业级使用指南，延续《Kerberos 部署与调优指南》等系列的结构。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、Jenkins 核心原理&lt;/h2&gt;
&lt;h3&gt;1、Jenkins 是什么&lt;/h3&gt;
&lt;p&gt;Jenkins 是 Java 编写的开源 CI/CD 工具，负责&quot;自动化的构建、测试、发布&quot;。它的核心价值不是某个功能，而是&lt;strong&gt;可编程的流水线&lt;/strong&gt;：把&quot;拉代码 → 编译 → 测试 → 打包 → 发布&quot;整条链路用代码表达、可重复执行。&lt;/p&gt;
&lt;h3&gt;2、整体架构：Master / Agent&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;                   ┌────────────────────────┐
   Git / SVN ──→   │        Master          │   ← 调度中心（控制器）
   Webhook/定时 ──→ │  Queue → Executors 调度│
                   └──────┬─────────┬───────┘
                          │ SSH/协议 │
                   ┌──────┴─────┐ ┌─┴────────────┐
                   │  Agent 1   │ │  Agent N     │   ← 执行节点
                   │ (Linux)    │ │ (K8s/Docker) │
                   └────────────┘ └──────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Master&lt;/strong&gt;：负责任务调度、配置存储、Web 界面、权限。默认自带执行器，生产建议设 0（只调度不干活）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent&lt;/strong&gt;：真正执行构建的节点，一个 master 可以挂大量 agent，按标签（label）分组，比如 &lt;code&gt;bigdata&lt;/code&gt;、&lt;code&gt;java17&lt;/code&gt;、&lt;code&gt;k8s-node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JENKINS_HOME&lt;/strong&gt;：一切状态的家——配置、构建记录、凭据、插件都在这里，是备份与迁移的核心&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、核心对象&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Job / Item&lt;/td&gt;
&lt;td&gt;一个任务（freestyle / pipeline / multibranch）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;一次构建执行，有编号（#1、#2...）和状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workspace&lt;/td&gt;
&lt;td&gt;agent 上的工作目录，代码和构建产物都在里面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executor&lt;/td&gt;
&lt;td&gt;执行器，一个 agent 上可以配多个（并发数）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue&lt;/td&gt;
&lt;td&gt;等待队列，executor 空闲才被调度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node / Label&lt;/td&gt;
&lt;td&gt;节点与标签，用于把任务路由到特定 agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;凭据（SSH 密钥、token、密码），加密存在 JENKINS_HOME/secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCM&lt;/td&gt;
&lt;td&gt;源码管理：Git / SVN 集成，轮询或 webhook 触发&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;4、一次构建的完整流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;触发（Webhook / 定时 / 手动 / API）
  → 进入 Queue 等待空闲 Executor
  → Agent 上创建/复用 Workspace，拉取源码
  → 按 Pipeline 执行阶段：构建（Maven/Gradle/npm）→ 测试 → 归档制品
  → 发布：SSH 推包 / 上传 Nexus / kubectl 滚动更新
  → 结果通知（邮件 / 企业微信 / DingTalk）并记录构建历史
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、Pipeline 原理&lt;/h3&gt;
&lt;p&gt;Pipeline 是 Jenkins 的&quot;一等公民&quot;，用 Groovy 编写，两种写法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Declarative（声明式）&lt;/strong&gt;：结构化，推荐生产使用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pipeline {
    agent { label &apos;bigdata&apos; }          // 指定执行节点
    stages {
        stage(&apos;拉取代码&apos;) { steps { checkout scm } }
        stage(&apos;构建&apos;)    { steps { sh &apos;mvn -q clean package&apos; } }
        stage(&apos;发布&apos;)    { steps { sh &apos;./deploy.sh&apos; } }
    }
    post { success { emailext subject: &apos;构建成功&apos; } }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scripted（脚本式）&lt;/strong&gt;：自由但难维护&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pipeline 的核心机制是 &lt;strong&gt;CPS（Continuation Passing Style）&lt;/strong&gt;：脚本被翻译成可暂停/恢复的状态机，每次构建的执行状态持久化，所以能支持&quot;构建被打断后恢复&quot;、Blue Ocean 可视化、以及跨重启的 stage 视图。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jenkins Shared Library（共享库）&lt;/strong&gt;：把通用逻辑（部署脚本、参数校验、通知封装）抽成公共 Groovy 库，各流水线引用，解决&quot;流水线代码重复&quot;的问题。&lt;/p&gt;
&lt;h3&gt;6、分布式构建的三种 Agent 形态&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;形态&lt;/th&gt;
&lt;th&gt;原理&lt;/th&gt;
&lt;th&gt;适用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSH Agent&lt;/td&gt;
&lt;td&gt;master 通过 SSH 连 agent 节点，按需拉起 slave.jar&lt;/td&gt;
&lt;td&gt;固定物理/虚机节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inbound Agent（JNLP/WebSocket）&lt;/td&gt;
&lt;td&gt;agent 主动连 master（可跨 NAT），Java Web Start 已废弃&lt;/td&gt;
&lt;td&gt;跨网络、容器内节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;云 Agent（Docker / Kubernetes）&lt;/td&gt;
&lt;td&gt;构建时动态起一个容器跑任务，跑完销毁&lt;/td&gt;
&lt;td&gt;弹性场景，资源利用最省&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;K8s Agent 是企业级弹性的关键&lt;/strong&gt;：构建高峰期自动起 20 个 pod 并行，空闲时归零，比固定 agent 池省一半以上资源。配合共享存储（PVC）挂 maven 仓库、npm 缓存，构建速度接近本地。
:::&lt;/p&gt;
&lt;h3&gt;7、高可用与制品管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HA 思路&lt;/strong&gt;：Jenkins 本身无内置集群，企业做法是&quot;NFS 共享 JENKINS_HOME + 双实例 active/standby + 前端负载均衡&quot;，或&quot;单实例 + 定期备份（tar JENKINS_HOME）&quot;降级方案&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;制品&lt;/strong&gt;：构建产物归档到 Nexus/Artifactory（Maven、Docker registry），发布环境从制品库拉取，而不是从构建机拷贝——保证&quot;构建一次，处处部署&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、部署（企业级，Linux）&lt;/h2&gt;
&lt;h3&gt;1、前置准备&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JDK&lt;/td&gt;
&lt;td&gt;Jenkins 当前 LTS 要求 Java 11+，推荐 &lt;strong&gt;Java 17 LTS&lt;/strong&gt;；K8s/Docker agent 场景再加 JDK 21 工具链&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;td&gt;Master 建议 2G+，构建任务多的 4G；JENKINS_HOME 放独立磁盘（SSD）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;时区&lt;/td&gt;
&lt;td&gt;JVM 加 &lt;code&gt;-Duser.timezone=Asia/Shanghai&lt;/code&gt;，否则日志/时间戳差 8 小时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;镜像&lt;/td&gt;
&lt;td&gt;插件更新中心走清华镜像，Maven/npm 走国内源，否则初始化极慢&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2、部署方式对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;th&gt;推荐度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;war + systemd（原生）&lt;/td&gt;
&lt;td&gt;可控、易调 JVM、无额外依赖&lt;/td&gt;
&lt;td&gt;手工步骤多&lt;/td&gt;
&lt;td&gt;★★★ 推荐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker Compose&lt;/td&gt;
&lt;td&gt;环境隔离、迁移方便&lt;/td&gt;
&lt;td&gt;数据卷管理、JVM 调参绕&lt;/td&gt;
&lt;td&gt;★★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tomcat 部署 war&lt;/td&gt;
&lt;td&gt;复用现有 Tomcat&lt;/td&gt;
&lt;td&gt;多一层依赖&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3、war + systemd 部署步骤&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 1) 创建用户与目录
useradd -r -s /bin/bash jenkins
mkdir -p /data/jenkins_home /data/jenkins &amp;amp;&amp;amp; chown -R jenkins:jenkins /data/jenkins*

# 2) 下载 LTS war 包（清华镜像更快）
wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/war-stable/latest/jenkins.war \
  -O /data/jenkins/jenkins.war

# 3) systemd 服务（/etc/systemd/system/jenkins.service）
[Unit]
Description=Jenkins CI
After=network.target

[Service]
User=jenkins
Environment=&quot;JENKINS_HOME=/data/jenkins_home&quot;
Environment=&quot;JAVA_OPTS=-Xms2g -Xmx4g -XX:MaxMetaspaceSize=512m \
  -Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8 \
  -Dhudson.model.WorkspaceCleanupThread.interval=3600&quot;
ExecStart=/usr/bin/java $JAVA_OPTS -jar /data/jenkins/jenkins.war --httpPort=8080
Restart=always

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 4) 启动并初始化
systemctl daemon-reload &amp;amp;&amp;amp; systemctl start jenkins
cat /data/jenkins_home/secrets/initialAdminPassword   # 初始解锁密码

# 5) 配置插件镜像（/data/jenkins_home/hudson.model.UpdateCenter.xml）
#    清华镜像：https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、初始化配置清单&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;解锁后用&lt;strong&gt;管理员账号&lt;/strong&gt;（不要用 admin 默认密码）登录&lt;/li&gt;
&lt;li&gt;必装插件：&lt;code&gt;Chinese&lt;/code&gt;、&lt;code&gt;Git/Subversion&lt;/code&gt;、&lt;code&gt;Pipeline&lt;/code&gt;、&lt;code&gt;Blue Ocean&lt;/code&gt;、&lt;code&gt;Credentials Binding&lt;/code&gt;、&lt;code&gt;SSH Pipeline Steps&lt;/code&gt;、&lt;code&gt;Docker/Kubernetes&lt;/code&gt;、&lt;code&gt;Email Extension&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;全局工具：JDK（17/21）、Maven（配 settings.xml 阿里云镜像）、Node.js&lt;/li&gt;
&lt;li&gt;凭据：Git 账号（token）、SSH 私钥（部署用）、Nexus/Registry 账号，全部用 &lt;strong&gt;Credential 管理&lt;/strong&gt;，不写死在脚本里&lt;/li&gt;
&lt;li&gt;权限：按团队建用户/角色，用&lt;strong&gt;矩阵授权&lt;/strong&gt;（Matrix Authorization Strategy），Master 上禁用匿名访问&lt;/li&gt;
&lt;li&gt;反向代理 HTTPS（nginx）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 443 ssl;
    server_name ci.example.com;
    ssl_certificate     /etc/nginx/cert/server.crt;
    ssl_certificate_key /etc/nginx/cert/server.key;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、Agent 节点接入&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;SSH Agent（固定节点）&lt;/strong&gt;：系统管理 → 节点 → 新建节点 → 填写 SSH 地址与凭据 → 指定标签（如 &lt;code&gt;bigdata&lt;/code&gt;）与执行器数量（按 CPU 核数的一半）。注意 agent 上也要装 JDK 与工具链。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes Agent（弹性）&lt;/strong&gt;：装 &lt;code&gt;Kubernetes&lt;/code&gt; 插件 → 配置集群地址、命名空间、Pod 模板（镜像含 JDK/Maven）→ Pipeline 里 &lt;code&gt;agent { kubernetes { label &apos;ephemeral&apos; } }&lt;/code&gt;，构建结束 Pod 自动销毁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;验证&lt;/strong&gt;：在节点页点&quot;连接&quot;，能看到节点图标为绿色并显示空闲执行器数。&lt;/p&gt;
&lt;h2&gt;三、调优&lt;/h2&gt;
&lt;h3&gt;1、JVM 调优&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-Xms / -Xmx&lt;/td&gt;
&lt;td&gt;2G / 4G（任务多可 4G/8G）&lt;/td&gt;
&lt;td&gt;相等可避免动态伸缩抖动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-XX:MaxMetaspaceSize&lt;/td&gt;
&lt;td&gt;512m-1G&lt;/td&gt;
&lt;td&gt;插件与 Groovy 脚本大量加载类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC&lt;/td&gt;
&lt;td&gt;默认 G1 即可&lt;/td&gt;
&lt;td&gt;不要轻易换 GC 算法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-Duser.timezone&lt;/td&gt;
&lt;td&gt;Asia/Shanghai&lt;/td&gt;
&lt;td&gt;时区问题最常见&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
JENKINS_HOME 磁盘 IO 是最大的性能瓶颈：构建记录、日志、workspace 都在它下面。放 SSD、预留 20% 以上空间、监控 inode，比调 JVM 更见效。
:::&lt;/p&gt;
&lt;h3&gt;2、系统级调优&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Master 执行器设 0&lt;/strong&gt;：调度与执行分离，避免主控被构建任务拖垮&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建记录保留策略&lt;/strong&gt;：按分支/标签保留最近 N 次，开启 SCM 保留策略（有改动才留）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;制品保留&lt;/strong&gt;：归档制品设保留天数，配合 Nexus 做长期存储&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workspace 清理&lt;/strong&gt;：开启自动清理（&lt;code&gt;WorkspaceCleanupThread&lt;/code&gt;），未用 workspace 定时删除&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件瘦身&lt;/strong&gt;：只装用到的插件，禁用（不卸载）低频插件，减少启动与内存开销&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志轮转&lt;/strong&gt;：&lt;code&gt;/var/log/jenkins&lt;/code&gt; 与构建日志启用 logrotate，避免撑爆磁盘&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、Pipeline 性能优化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;触发用 Webhook 替代轮询&lt;/strong&gt;：SCM 轮询浪费 master 资源，Git 仓库配 webhook 即时触发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并行阶段&lt;/strong&gt;：&lt;code&gt;stage(&apos;并行&apos;) { parallel { ... } }&lt;/code&gt;，测试分片、多平台构建并行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建缓存&lt;/strong&gt;：Maven 本地仓库、npm cache、Gradle 缓存挂到共享卷（NFS/PVC），避免每次全量下载依赖&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减小日志&lt;/strong&gt;：生产关闭 DEBUG 日志输出，构建脚本避免无意义的大打印&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享库管理&lt;/strong&gt;：常用逻辑抽到 Shared Library，流水线文件保持精简&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4、规模扩展&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;固定 Agent 池 + 标签分组：不同技术栈（java/node/python）分池，避免互相污染 workspace&lt;/li&gt;
&lt;li&gt;高峰期弹性：Kubernetes Agent 按队列长度动态伸缩，高峰期 20 并发、低峰归零&lt;/li&gt;
&lt;li&gt;多 master 拆分：业务线多、任务量大时按业务拆 master（或 Team/文件夹隔离），避免单点排队&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5、安全加固&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CSRF 保护默认开启；禁用匿名读&lt;/li&gt;
&lt;li&gt;Agent 接入只放行内网网段，SSH 端口最小化暴露&lt;/li&gt;
&lt;li&gt;凭据只用 &lt;strong&gt;Jenkins Credential&lt;/strong&gt; 管理，脚本里禁止明文密码；开启凭据加密（默认 AES）&lt;/li&gt;
&lt;li&gt;共享库走代码评审，流水线里禁止 &lt;code&gt;eval&lt;/code&gt; 等危险操作&lt;/li&gt;
&lt;li&gt;生产环境 HTTPS 必须（反向代理或证书直配）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6、常见问题排查&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;任务一直排队&lt;/td&gt;
&lt;td&gt;执行器满 / agent 离线 / 标签不匹配&lt;/td&gt;
&lt;td&gt;节点页看连接状态与执行器占用；标签写错会永远等不到 agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;构建卡死无输出&lt;/td&gt;
&lt;td&gt;网络、等待锁、脚本死循环&lt;/td&gt;
&lt;td&gt;看线程转储（Thread Dump），检查 SCM 与制品库连通性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存溢出（OOM）&lt;/td&gt;
&lt;td&gt;JVM 太小或插件泄漏&lt;/td&gt;
&lt;td&gt;调 Xmx，禁用可疑插件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;磁盘满&lt;/td&gt;
&lt;td&gt;构建日志、workspace、备份堆积&lt;/td&gt;
&lt;td&gt;清理 + 轮转 + 保留策略&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插件升级后功能异常&lt;/td&gt;
&lt;td&gt;插件兼容性&lt;/td&gt;
&lt;td&gt;升级前备份 JENKINS_HOME，异常时回退版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;时间/日志差 8 小时&lt;/td&gt;
&lt;td&gt;JVM 时区未设置&lt;/td&gt;
&lt;td&gt;加 -Duser.timezone=Asia/Shanghai 重启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent 频繁断连&lt;/td&gt;
&lt;td&gt;网络抖动 / 版本不匹配&lt;/td&gt;
&lt;td&gt;升级 agent.jar 与 master 同版本，检查防火墙&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;四、总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：Master/Agent 架构 + JENKINS_HOME 状态存储 + Pipeline（CPS 状态机）+ 插件生态；HA 靠共享存储双实例或备份&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署&lt;/strong&gt;：war + systemd 最可控；初始化三件套（国内镜像、插件清单、凭据与权限）一次配好&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调优&lt;/strong&gt;：先磁盘与 IO，再 JVM 与保留策略，再 Pipeline 缓存与弹性 Agent；安全加固与备份是长期责任&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;落地建议&lt;/strong&gt;：Jenkins 接回大数据底座迭代思路——组件参数变更、服务构建、发布脚本全部流水线化，让&quot;平台现代化&quot;真正跑起来&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Iceberg 原理、部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/11/iceberg-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/iceberg-deploy-tuning/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Iceberg 是&quot;表格式&quot;（Table Format）不是数据库：它把&quot;一张表&quot;定义为一组元数据与数据文件的规范，让 Spark/Flink/Trino/Doris 都能读写同一张表并保证 ACID。它是湖仓一体的地基。本文覆盖原理（快照与元数据层级）、集成部署（无独立服务）、参数优化与常见问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理速览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;三层元数据&lt;/strong&gt;：&lt;code&gt;Table Metadata&lt;/code&gt;（表元数据，指向当前快照）→ &lt;code&gt;Manifest List&lt;/code&gt;（快照内的清单列表）→ &lt;code&gt;Manifest&lt;/code&gt;（清单，记录数据文件列表）→ &lt;code&gt;Data Files&lt;/code&gt;（实际数据文件，多为 Parquet）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快照（Snapshot）&lt;/strong&gt;：每次提交生成新快照（新 Table Metadata），旧快照保留可查询 → 天然的&lt;strong&gt;时间旅行&lt;/strong&gt;与 &lt;strong&gt;ACID&lt;/strong&gt;（读已提交）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐藏分区（Hidden Partitioning）&lt;/strong&gt;：&lt;code&gt;PARTITIONED BY (day(ts))&lt;/code&gt; 这类表达式分区，查询带 &lt;code&gt;ts&lt;/code&gt; 条件时自动裁剪，分区演化不重写历史&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模式演进&lt;/strong&gt;：加列/删列/改列类型只改元数据，不重写数据文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不依赖服务&lt;/strong&gt;：表状态全在文件系统（HDFS/对象存储），无 HiveServer 2 那样的守护进程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Catalog 是入口&lt;/strong&gt;：Hive Catalog（借用 HMS）、Hadoop/Nessie/REST Catalog（独立元数据服务）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Iceberg 相对 Hive 表的本质升级：&lt;strong&gt;Hive 的元数据是&quot;表→分区目录&quot;，Iceberg 的元数据是&quot;表→快照→清单→文件&quot;，每次写都留下可回滚的版本&lt;/strong&gt;。这让&quot;批流写同一张表&quot;和&quot;增量读取&quot;成为可能。
:::&lt;/p&gt;
&lt;h2&gt;部署（集成式，无独立服务）&lt;/h2&gt;
&lt;h3&gt;1. 集成 Spark（以 Spark 3.x 为例）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 放 jar：iceberg-spark-runtime（与 spark 3.x 匹配）
# 启动时指定 catalog（也可以写进 spark-defaults.conf）
spark-sql \
  --packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.x \
  --conf spark.sql.catalog.iceberg=org.apache.iceberg.spark.SparkCatalog \
  --conf spark.sql.catalog.iceberg.type=hive \
  --conf spark.sql.catalog.iceberg.uri=thrift://node1:9083 \
  --conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 建表（分区表达式 + 隐藏分区）
CREATE TABLE iceberg.db.orders (
  order_id BIGINT, user_id BIGINT, amount DECIMAL(20,2), ts TIMESTAMP)
USING iceberg
PARTITIONED BY (days(ts), bucket(8, user_id));

-- 写入与查询
INSERT INTO iceberg.db.orders VALUES (1, 100, 20.5, current_timestamp());
SELECT * FROM iceberg.db.orders /*+ OPTIONS(&apos;snapshot-id&apos;=&apos;...&apos;) */;

-- 时间旅行
SELECT * FROM iceberg.db.orders VERSION AS OF &amp;lt;snapshot_id&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 集成 Flink（SQL 客户端）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 拷贝 flink-sql-connector-iceberg（带连接器版本）到 lib
# 启动时注册 catalog
CREATE CATALOG iceberg WITH (
  &apos;type&apos; = &apos;iceberg&apos;,
  &apos;catalog-type&apos; = &apos;hive&apos;,
  &apos;uri&apos; = &apos;thrift://node1:9083&apos;,
  &apos;clients&apos; = &apos;5&apos;);

USE CATALOG iceberg;
CREATE DATABASE IF NOT EXISTS dwd;
CREATE TABLE dwd.orders (...) PARTITIONED BY (days(ts))
  WITH (&apos;format-version&apos;=&apos;2&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. REST Catalog（无 HMS 的独立元数据，推荐新体系）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 用 iceberg REST 服务（如 Gravitino 提供的 Iceberg REST，或自建 iceberg-rest 服务）
spark-sql \
  --conf spark.sql.catalog.iceberg=org.apache.iceberg.spark.SparkCatalog \
  --conf spark.sql.catalog.iceberg.type=rest \
  --conf spark.sql.catalog.iceberg.uri=http://meta-server:9001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
Catalog 一旦用上就别随意切换（元数据入口变了，表就&quot;找不到&quot;）。生产规划时先把 Catalog 类型定死：已有 HMS 就用 hive catalog，新建体系优先 REST/Gravitino。
:::&lt;/p&gt;
&lt;h2&gt;调优&lt;/h2&gt;
&lt;h3&gt;1. 写入&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.target-file-size-bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512MiB（默认 512）&lt;/td&gt;
&lt;td&gt;目标文件大小，控制小文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.distribution-mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hash（默认）&lt;/td&gt;
&lt;td&gt;同分区数据尽量写同文件，减少提交文件数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.spark.fanout.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分区数多时 true&lt;/td&gt;
&lt;td&gt;减少分区间打开文件数，提升写吞吐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write.parquet.compression-codec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;zstd&lt;/td&gt;
&lt;td&gt;压缩比与速度均衡&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. 读与查询&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;裁剪&lt;/strong&gt;：分区表达式要与查询条件匹配（&lt;code&gt;ts &amp;gt;= ?&lt;/code&gt; 才能命中 days 分区）；&lt;code&gt;EXPLAIN&lt;/code&gt; 确认分区过滤生效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scan 并行&lt;/strong&gt;：Spark 侧 &lt;code&gt;spark.sql.sources.maxConcurrentWrites&lt;/code&gt;/并行度按文件数定；Flink 侧 &lt;code&gt;scan.planning-parallelism&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元数据缓存&lt;/strong&gt;：REST/Hive catalog 客户端连接池调大（&lt;code&gt;clients=5&lt;/code&gt; 不够就 10-20）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;谓词下推&lt;/strong&gt;：Parquet 自带统计信息裁剪，确保引擎开启 &lt;code&gt;pushdown&lt;/code&gt;（Spark 默认开）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 表维护（必须定期做）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 1) 过期快照与孤儿文件（时间旅行保留窗口之外的老版本）
CALL iceberg.system.expire_snapshots(&apos;db.orders&apos;, TIMESTAMP &apos;2026-07-01 00:00:00&apos;);
-- 2) 合并小文件（RewriteDataFiles，按 binpack 默认 512MiB 目标合并）
CALL iceberg.system.rewrite_data_files(&apos;db.orders&apos;);
-- 3) 删除孤儿文件
CALL iceberg.system.remove_orphan_files(&apos;db.orders&apos;);
-- 4) 清理 Manifest
CALL iceberg.system.rewrite_manifests(&apos;db.orders&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
&lt;strong&gt;快照与 Manifest 无限膨胀是 Iceberg 最大的运维坑&lt;/strong&gt;：不清理的话，元数据文件越积越多，提交和列表查询会越来越慢。expire_snapshots 必须进定时任务（配合 AIOps 周期表），业务查询会用的时间旅行窗口要单独评估。
:::&lt;/p&gt;
&lt;h3&gt;4. 分区设计&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;高频过滤列做分区：时间按 &lt;code&gt;days()&lt;/code&gt;，枚举均匀列用 &lt;code&gt;bucket(N, col)&lt;/code&gt; 避免倾斜&lt;/li&gt;
&lt;li&gt;分区数 = 数据量 /（每分区目标 1-5G），太细产生大量空分区&lt;/li&gt;
&lt;li&gt;分区演化：&lt;code&gt;ALTER TABLE ... ADD PARTITION FIELD days(ts)&lt;/code&gt; 不重写旧数据，但历史分区仍按旧规则&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;查不到新写入数据&lt;/td&gt;
&lt;td&gt;提交但快照未可见/缓存&lt;/td&gt;
&lt;td&gt;检查 catalog 连接、会话快照隔离（SSE 隔离级别）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;小文件爆炸&lt;/td&gt;
&lt;td&gt;高频写 + 未做 rewrite&lt;/td&gt;
&lt;td&gt;调大 target-file-size，周期性 rewrite_data_files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;提交冲突失败&lt;/td&gt;
&lt;td&gt;并发写同一表&lt;/td&gt;
&lt;td&gt;升级表格式 v2 支持行级冲突；写任务错峰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元数据目录膨胀&lt;/td&gt;
&lt;td&gt;未清理快照&lt;/td&gt;
&lt;td&gt;expire_snapshots + remove_orphan_files 定时任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hive 查不到数据&lt;/td&gt;
&lt;td&gt;Hive 引擎未集成 Iceberg jar&lt;/td&gt;
&lt;td&gt;Hive 侧放 iceberg-hive-runtime 并 &lt;code&gt;set hive.input.format&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Iceberg = 表格式：快照 + 三层元数据 + 隐藏分区 + 模式演进，ACID 与时间旅行是卖点&lt;/li&gt;
&lt;li&gt;部署是&quot;集成&quot;不是&quot;安装&quot;：选好 Catalog（Hive/REST）→ 放 jar → 注册 catalog → 建表；无独立守护进程&lt;/li&gt;
&lt;li&gt;调优主线：写入（目标文件大小/分布模式）→ 分区表达式匹配查询 → &lt;strong&gt;元数据维护定时化&lt;/strong&gt; → 并发写入节奏&lt;/li&gt;
&lt;li&gt;在底座迭代中，Iceberg 是湖仓一体的事实标准格式之一，新数据管道建议直接落 Iceberg（配合 Doris/StarRocks 查询），老 Hive 表按业务逐步迁移&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>JuiceFS 原理、部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/11/juicefs-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/juicefs-deploy-tuning/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;JuiceFS 是&quot;元数据 + 数据分离&quot;的分布式文件系统：数据放对象存储，元数据放数据库，本地留一层缓存。它给大数据底座带来的价值是&lt;strong&gt;存算分离的 POSIX 入口&lt;/strong&gt;——Hadoop 生态、AI 训练、容器都挂同一份数据。本文覆盖原理、部署（客户端/Hadoop SDK/CSI）、参数优化与常见问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理速览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;三件套架构&lt;/strong&gt;：对象存储（存数据）+ 元数据引擎（存目录/文件/权限，支持 Redis/MySQL/PostgreSQL/TiKV/FoundationDB）+ 本地缓存（加速读）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;POSIX 兼容&lt;/strong&gt;：通过 FUSE 挂载成本地目录，支持 rename/锁/权限等 POSIX 语义，应用无需改造&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据分块&lt;/strong&gt;：文件按 64MiB chunk 切分，chunk 内按 4MiB slice 存储为对象存储中的对象，碎片通过合并后台整理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入路径&lt;/strong&gt;：写入 → 客户端本地缓存 → 后台异步上传对象存储（writeback 模式）/同步上传（默认）；先写缓存保证延迟，再保证对象存储持久化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性&lt;/strong&gt;：元数据操作强一致（数据库事务）；多个客户端共享同一元数据引擎，文件可见性有秒级延迟（写缓存到上传对象存储之间）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无本地 GC&lt;/strong&gt;：对象存储是&quot;只增不删&quot;，删除文件即删元数据，对象存储侧用生命周期规则清理孤儿对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
JuiceFS 的取舍一句话：&lt;strong&gt;用对象存储的廉价换 HDFS 的协议兼容，用数据库元数据换自建元数据的运维负担&lt;/strong&gt;。它不适合&quot;文件频繁重命名/秒级强一致&quot;的场景，适合&quot;大数据 + AI + K8s 共享存储&quot;的场景。
:::&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;h3&gt;1. 安装客户端&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -sSL https://d.juicefs.com/install | sh -
juicefs --version

# 内核 FUSE 检查（fuse 模块必须加载）
ls /dev/fuse || modprobe fuse
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 创建文件系统（元数据引擎 + 对象存储）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 元数据引擎用 Redis（生产建议 MySQL/TiKV，Redis 需开启 AOF 持久化）
# 对象存储用 S3/OSS/COS/MinIO 均可
juicefs format \
  --storage s3 \
  --bucket https://my-bucket.oss-cn-hangzhou.aliyuncs.com \
  --access-key xxx --secret-key yyy \
  &quot;mysql://user:pass@(10.0.0.10:3306)/juicefs&quot; \
  myfs
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 挂载&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /mnt/jfs
juicefs mount -d \
  --cache-dir /var/jfs/cache --cache-size 100 \
  &quot;mysql://user:pass@(10.0.0.10:3306)/juicefs&quot; /mnt/jfs

# 开机自启（systemd 单元省略，要点：After=network-online.target，User=juicefs）

# 验证
juicefs status &quot;mysql://user:pass@(10.0.0.10:3306)/juicefs&quot;
echo hello &amp;gt; /mnt/jfs/hello.txt &amp;amp;&amp;amp; cat /mnt/jfs/hello.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Hadoop SDK（大数据挂载方式）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 把 jar 放到 HADOOP_CLASSPATH：juicefs-hadoop.jar（与 hadoop 版本匹配）
cp juicefs-hadoop.jar /opt/hadoop/share/hadoop/common/

# core-site.xml 增加：
&amp;lt;property&amp;gt;&amp;lt;name&amp;gt;fs.jfs.impl&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;io.juicefs.JuiceFileSystem&amp;lt;/value&amp;gt;&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;&amp;lt;name&amp;gt;fs.AbstractFileSystem.jfs.impl&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;io.juicefs.JuiceFileSystem&amp;lt;/value&amp;gt;&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;&amp;lt;name&amp;gt;juicefs.meta&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;mysql://user:pass@(10.0.0.10:3306)/juicefs&amp;lt;/value&amp;gt;&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;&amp;lt;name&amp;gt;juicefs.cache-dir&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;/var/jfs/cache&amp;lt;/value&amp;gt;&amp;lt;/property&amp;gt;

# Spark 访问：spark-submit --conf spark.hadoop.juicefs.meta=...
# 路径写法：hdfs:/// 换成 jfs://myfs/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
Hadoop SDK 与挂载是两种独立客户端，&lt;strong&gt;共享同一份对象存储与元数据，但缓存各自独立&lt;/strong&gt;。Spark 任务用 SDK（性能好），运维人员用挂载点（好管理），两者不要同时写同一文件即可。
:::&lt;/p&gt;
&lt;h3&gt;5. Kubernetes CSI（可选）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 安装 JuiceFS CSI Driver（Helm）
helm repo add juicefs https://juicedata.github.io/charts
helm install juicefs-csi-driver juicefs/juicefs-csi-driver -n kube-system

# StorageClass（元数据引擎与对象存储参数在 secret 中）
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: juicefs-sc }
provisioner: csi.juicefs.com
parameters:
  csi.storage.k8s.io/node-publish-secret-name: juicefs-secret
  csi.storage.k8s.io/node-publish-secret-namespace: kube-system
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调优&lt;/h2&gt;
&lt;h3&gt;1. 缓存&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--cache-dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;多块 SSD 用 &lt;code&gt;:&lt;/code&gt; 分隔（如 /cache1:/cache2）&lt;/td&gt;
&lt;td&gt;按目录分摊 IO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--cache-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;节点内存的 20%-50% 或按磁盘空间&lt;/td&gt;
&lt;td&gt;越大命中率越高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--buffer-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认 300MiB，可调 500-1000&lt;/td&gt;
&lt;td&gt;写缓冲，影响写入吞吐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--prefetch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;顺序读场景调到 8-16&lt;/td&gt;
&lt;td&gt;预读块数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--writeback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;网络差/对象存储慢时开启&lt;/td&gt;
&lt;td&gt;写缓存后异步上传，注意断电丢数风险&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. 元数据引擎&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redis&lt;/strong&gt;：必须 AOF 开启 + &lt;code&gt;appendfsync everysec&lt;/code&gt;（生产）；元数据量大时内存吃紧，监控 used_memory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL/TiKV&lt;/strong&gt;：适合万级文件规模与并发写入；MySQL 给足连接池（&lt;code&gt;max_connections&lt;/code&gt;），TiKV 弹性更好&lt;/li&gt;
&lt;li&gt;元数据引擎是全局单点，&lt;strong&gt;一定要做主从/高可用&lt;/strong&gt;，它是整个文件系统的可用性核心&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 大数据场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spark 写小文件&lt;/strong&gt;：&lt;code&gt;juicefs.memory-size&lt;/code&gt;（默认 300MiB）调大，减少小对象上传&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQL 扫描型读&lt;/strong&gt;：&lt;code&gt;--prefetch&lt;/code&gt; 调大 + 多挂载点；SQL 引擎侧开启谓词下推减少读量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象存储限流&lt;/strong&gt;：调大 &lt;code&gt;--max-uploads&lt;/code&gt;（默认 20）/&lt;code&gt;--max-deletes&lt;/code&gt;（默认 10），带宽够时可提升并发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;垃圾回收&lt;/strong&gt;：用生命周期规则按天清理 &lt;code&gt;juicefs.trash/&lt;/code&gt; 与孤儿对象；&lt;code&gt;juicefs gc&lt;/code&gt; 手工触发&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 挂载稳定性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--atime-mode noatime&lt;/code&gt;：减少元数据写放大&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--open-cache&lt;/code&gt;（默认 0）/&lt;code&gt;--attr-cache&lt;/code&gt;：目录列表与元数据缓存，读密集场景提升明显&lt;/li&gt;
&lt;li&gt;磁盘满会导致缓存写失败进而挂载异常：监控 cache-dir 磁盘水位&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;挂载失败 &lt;code&gt;fuse not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;内核无 FUSE&lt;/td&gt;
&lt;td&gt;安装 fuse 包或重编内核，容器内加特权&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写文件慢&lt;/td&gt;
&lt;td&gt;writeback 未开 / buffer 太小&lt;/td&gt;
&lt;td&gt;评估开 writeback，调 buffer-size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;读命中率低&lt;/td&gt;
&lt;td&gt;缓存太小或数据一次性扫描&lt;/td&gt;
&lt;td&gt;看 &lt;code&gt;juicefs stats&lt;/code&gt; 的 cache 命中，调 cache-size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元数据引擎故障全挂&lt;/td&gt;
&lt;td&gt;单点&lt;/td&gt;
&lt;td&gt;主从/高可用 + 监控告警，故障演练&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hadoop 任务报 &lt;code&gt;Meta was not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;core-site 配置缺失&lt;/td&gt;
&lt;td&gt;检查 juicefs.meta 参数与 jar 版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;卸载不掉&lt;/td&gt;
&lt;td&gt;有进程占用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lsof /mnt/jfs&lt;/code&gt; 找占用进程，&lt;code&gt;umount -l&lt;/code&gt; 强卸&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JuiceFS = 对象存储 + 数据库元数据 + 本地缓存，POSIX 兼容，一份数据打通 Hadoop/AI/K8s&lt;/li&gt;
&lt;li&gt;部署四条路：FUSE 挂载（运维）、Hadoop SDK（大数据任务）、CSI（K8s）、客户端直连&lt;/li&gt;
&lt;li&gt;调优主线：缓存（cache-size/buffer/prefetch）→ 元数据引擎高可用 → 写放大控制 → 对象存储并发&lt;/li&gt;
&lt;li&gt;在底座迭代中，JuiceFS 是&lt;strong&gt;存算分离的第一块跳板&lt;/strong&gt;：HDFS 存量不动，新数据与 AI 场景直接上 JuiceFS，逐步降低对 HDFS 的依赖&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Paimon 原理、部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/11/paimon-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/paimon-deploy-tuning/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Paimon（原 Flink Table Store，Apache 孵化项目）是&quot;流式数据湖仓&quot;：在湖格式上叠加 LSM 结构与 Changelog，让 &lt;strong&gt;Flink 能对湖表做流读流写&lt;/strong&gt;，主键表支持 CDC 同步与行级更新。本文覆盖原理（LSM/Bucket/Changelog）、集成部署（Flink/Spark）、参数优化与常见问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理速览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;表类型&lt;/strong&gt;：主键表（Primary Key，支持行级更新/删除）与 Append-Only 表（纯追加，日志型场景）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LSM 结构&lt;/strong&gt;：写入先进内存缓冲（MemTable），落盘成 Sorted Run（有序数据文件），查询时多路归并——与 HBase/RocksDB 同款思路，换来了&lt;strong&gt;随机写变顺序写&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bucket（分桶）&lt;/strong&gt;：主键哈希分桶，桶是写入与并发的最小单元，桶内有序；&lt;strong&gt;桶数建表后不可改&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Changelog&lt;/strong&gt;：主键表每次变更产生增删改记录，落在 changelog 文件里 → 下游可做&lt;strong&gt;增量流读&lt;/strong&gt;（实时数仓&quot;湖上实时&quot;的关键）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compaction&lt;/strong&gt;：后台把多个 Sorted Run 合并（减少读放大）；full-compaction 生成完整 changelog&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lookup&lt;/strong&gt;：主键点查走索引文件，Flink Join 湖表时按需拉取，替代&quot;全表缓存&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 Iceberg 对比&lt;/strong&gt;：Iceberg 是&quot;批写批读 + ACID&quot;，Paimon 是&quot;流写流读 + LSM&quot;，Paimon 更贴 Flink 生态，Iceberg 更贴 Spark 生态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Paimon 一句话定位：&lt;strong&gt;把 Flink 的状态和 Changelog 落成&quot;可读可查的湖表&quot;&lt;/strong&gt;。以前 Flink 实时数仓的明细要写 Kafka + 再同步到 Hive/OLAP，现在直接写 Paimon，下游 Spark/Doris/Trino 都能读，还能继续流读。
:::&lt;/p&gt;
&lt;h2&gt;部署（集成式，强依赖 Flink）&lt;/h2&gt;
&lt;h3&gt;1. 集成 Flink&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 拷贝 paimon-flink（带连接器版本）到 $FLINK_HOME/lib
cp paimon-flink-1.18-0.8.x.jar $FLINK_HOME/lib/
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- Flink SQL 注册 Catalog
CREATE CATALOG paimon WITH (
  &apos;type&apos; = &apos;paimon&apos;,
  &apos;warehouse&apos; = &apos;hdfs:///paimon&apos;,
  &apos;metastore&apos; = &apos;filesystem&apos;          -- 或 hive（借用 HMS）
  -- ,&apos;uri&apos; = &apos;thrift://node1:9083&apos;   -- metastore=hive 时配置
);
USE CATALOG paimon;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;-- 建主键表（流写流读的核心形态）
CREATE TABLE dwd_orders (
  order_id BIGINT PRIMARY KEY NOT ENFORCED,
  user_id  BIGINT,
  amount   DECIMAL(20,2),
  status   STRING,
  ts       TIMESTAMP(3),
  WATERMARK FOR ts AS ts - INTERVAL &apos;5&apos; SECOND
) WITH (
  &apos;bucket&apos; = &apos;32&apos;,
  &apos;changelog-producer&apos; = &apos;input&apos;,
  &apos;merge-engine&apos; = &apos;deduplicate&apos;
);

-- 流式写入（来自 Kafka 的 CDC/实时流）
CREATE TEMPORARY TABLE kafka_orders (...) WITH (&apos;connector&apos;=&apos;kafka&apos;, ...);
INSERT INTO dwd_orders SELECT * FROM kafka_orders;

-- 流式读取（增量读 changelog，下游可做实时数仓）
SELECT * FROM dwd_orders /*+ OPTIONS(&apos;scan.mode&apos;=&apos;latest&apos;) */;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 集成 Spark / 查询端&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Spark 3.x 读取 Paimon 表
spark-sql \
  --packages org.apache.paimon:paimon-spark-3.5:0.8.x \
  --conf spark.sql.catalog.paimon=org.apache.paimon.spark.SparkCatalog \
  --conf spark.sql.catalog.paimon.warehouse=hdfs:///paimon
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;SELECT user_id, SUM(amount) FROM paimon.dwd.dwd_orders
GROUP BY user_id;   -- Spark 离线读同一张流表
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. MySQL CDC 同步（实时数仓标配链路）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- 用 Flink CDC 把 MySQL 实时同步成 Paimon 主键表
CREATE TEMPORARY TABLE mysql_orders (
  order_id BIGINT PRIMARY KEY NOT ENFORCED, ...
) WITH (&apos;connector&apos;=&apos;mysql-cdc&apos;, &apos;hostname&apos;=&apos;...&apos;, &apos;database-name&apos;=&apos;shop&apos;, &apos;table-name&apos;=&apos;orders&apos;);

CREATE TABLE paimon.dwd.orders (...) WITH (&apos;bucket&apos;=&apos;32&apos;);
INSERT INTO paimon.dwd.orders SELECT * FROM mysql_orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调优&lt;/h2&gt;
&lt;h3&gt;1. Bucket 与并发&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bucket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;写入并行度一致（或整数倍）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;建表后不可改&lt;/strong&gt;，前期定错要重建表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sink 并行度&lt;/td&gt;
&lt;td&gt;= bucket 数&lt;/td&gt;
&lt;td&gt;每个写入子任务对应若干桶，避免重分布&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bucket-key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;主键子集&lt;/td&gt;
&lt;td&gt;打散热点，如 user_id%N&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. 写入&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;write-buffer-size&lt;/code&gt;（默认 64MB）：单任务内存缓冲，调大减少小文件但吃内存（注意并行度乘积）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sink.parallelism&lt;/code&gt; 与 bucket 匹配；&lt;code&gt;commit.force-compact&lt;/code&gt; 可开启提交前强制 compact（写读隔离但延迟高，慎用）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write-only=true&lt;/code&gt;：只写不 compaction（导入大任务场景，事后统一 compact）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Compaction&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;num-sorted-runs.compaction-trigger&lt;/code&gt;（默认 5）：达到阈值触发 compaction，调大可减少触发频率&lt;/li&gt;
&lt;li&gt;&lt;code&gt;compaction.max.file-num&lt;/code&gt; / &lt;code&gt;full-compaction.delta-commits&lt;/code&gt;（如 10）：定期 full compaction 生成完整 changelog（供快照读）&lt;/li&gt;
&lt;li&gt;大表建议独立 compaction 作业池（&lt;code&gt;compaction.parallelism&lt;/code&gt;），避免与写入抢资源&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 读取&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;流读：&lt;code&gt;scan.mode=latest&lt;/code&gt;（实时）/ &lt;code&gt;from-timestamp&lt;/code&gt;（追增量）；&lt;code&gt;read.changelog-producer=lookup&lt;/code&gt; 时点查代价高，尽量 input 模式&lt;/li&gt;
&lt;li&gt;批读（Spark）：谓词下推 + 分区裁剪自动生效；大表先 &lt;code&gt;ANALYZE&lt;/code&gt; 相关统计&lt;/li&gt;
&lt;li&gt;点查 Lookup：&lt;code&gt;lookup.cache&lt;/code&gt; 开启（如 10000 条/10 分钟），Flink Join 湖表时命中缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 快照与文件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;snapshot.num-retained.min&lt;/code&gt;（默认 10）/max：控制快照保留，影响流读回溯窗口&lt;/li&gt;
&lt;li&gt;小文件治理：&lt;code&gt;write-buffer-size&lt;/code&gt; 调大 + compaction 周期内聚；&lt;code&gt;paimon gc&lt;/code&gt; 逻辑同 Iceberg（孤儿文件清理）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;桶数想改&lt;/td&gt;
&lt;td&gt;bucket 建表后固定&lt;/td&gt;
&lt;td&gt;评估重建表 + 数据回灌，或早期按峰值并行度定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;流读延迟高&lt;/td&gt;
&lt;td&gt;changelog-producer 配置弱&lt;/td&gt;
&lt;td&gt;用 input/full-compaction，别用 lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;小文件多&lt;/td&gt;
&lt;td&gt;写缓冲太小/并行度与桶不匹配&lt;/td&gt;
&lt;td&gt;调 write-buffer-size，桶数=并行度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDC 同步延迟&lt;/td&gt;
&lt;td&gt;单任务吞吐/检查点间隔&lt;/td&gt;
&lt;td&gt;调大并行度、检查点间隔 60-120s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;读快照数据量大&lt;/td&gt;
&lt;td&gt;保留快照过多&lt;/td&gt;
&lt;td&gt;调 snapshot.num-retained，full compaction 后清理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flink 找不到表&lt;/td&gt;
&lt;td&gt;Catalog/warehouse 配置漂移&lt;/td&gt;
&lt;td&gt;确认 catalog 类型与 warehouse 路径一致&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Paimon = Flink 生态的流式湖仓：LSM + Bucket + Changelog，主键表支持 CDC 与行级更新，流读流写&lt;/li&gt;
&lt;li&gt;部署 = 放 jar + 注册 catalog（filesystem/hive）+ 建主键表；无独立服务，强依赖 Flink&lt;/li&gt;
&lt;li&gt;调优主线：&lt;strong&gt;bucket 数前期定死&lt;/strong&gt;（核心）→ write-buffer-size → compaction 节奏 → changelog-producer 模式 → 快照保留&lt;/li&gt;
&lt;li&gt;在底座迭代中，Paimon 承接&quot;实时数仓湖化&quot;：Flink 实时链路直接落湖，替代&quot;Kafka + 双写 Hive&quot;的复杂架构；与 Iceberg 分工——实时链路 Paimon，离线/多引擎场景 Iceberg，两套并存是常态&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>MySQL 学习笔记：SQL 复习、原理与优化</title><link>https://chaggle.github.io/posts/2026/08/11/mysql-notes/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/mysql-notes/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;2022 年学习 MySQL 的三篇笔记（SQL 语法复习、MySQL 原理、MySQL 优化）合并整理。原理先行，实践在后。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、SQL 语法复习&lt;/h2&gt;
&lt;p&gt;轻量化复盘一下 SQL 操作，以及 MySQL。&lt;/p&gt;
&lt;h3&gt;SELECT 语句&lt;/h3&gt;
&lt;p&gt;数据查询是关系运算理论在 SQL 语言中的主要体现。SQL 的数据查询只有一条 SELECT 语句，其使用最广泛。一个完整的 SELECT 语句包含了六个子句，前两个子句是必备的，其他子句可以省略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT [DISTINCT] 目标列名序列 ------ 查看的列 [distinct] 为去重操作
FROM 表名或视图名 ------------------- 数据来源
[WHERE 条件表达式] ------------------ 查询条件
[GROUP BY + 列名] ------------------ 分组依据
[HAVING + 组条件表达式] ------------- 分组条件
[ORDER BY + 列名 + [ASC|DESC]序列]--- 排序依据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中使用 distinct 时要注意列名要放在 distinct 的后面，而使用聚合函数时，则是 &lt;code&gt;count(DISTINCT 列名)&lt;/code&gt; 语法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	DISTINCT device_id
FROM usr_profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询列后，将列取别名操作，要使用 as，但是也能省略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	device_id as user_infos_example
FROM user_profile

# 等同于
SELECT
	device_id user_infos_example
FROM user_profile
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;LIMIT 语句&lt;/h3&gt;
&lt;p&gt;在大表中，一般很少一次性查出所有数据，这样对数据库的压力太大。如果只是抽查一些数据，就可以使用 LIMIT 关键字来查询。LIMIT 子句用于强制 SELECT 语句返回指定的记录数，其接受一个或两个数字参数，且参数必须为整型常量。&lt;/p&gt;
&lt;p&gt;如果只给定一个参数，则返回指定数目的记录行；如果给定两个参数，第一个参数是行数序号（从 0 开始表示第一行），第二个参数是数量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM table LIMIT 5,5   # 检索记录行 6-10
SELECT * FROM table LIMIT 10,-1

# 也能结合 offset 一起使用：跳过 0 条，从第一条开始取两条
select device_id from user_profile limit 2 offset 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;BETWEEN 语句&lt;/h3&gt;
&lt;p&gt;当需要查询某个范围值的时候，一般使用两种语句：between and，以及 and 语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT device_id, gender, age FROM user_profile
where age between 20 and 23
# 等价于
SELECT device_id, gender, age FROM user_profile
where age &amp;gt;= 20 and age &amp;lt;= 23
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;NOT IN 语句&lt;/h3&gt;
&lt;p&gt;当需要查询集合中指定属性值时，使用 IN，除此属性值之外的所有值使用 NOT IN。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IN：当列中的值与 IN 中的某个常量值相等时，则结果为 True，表明此记录为符合查询条件的记录&lt;/li&gt;
&lt;li&gt;NOT IN：当列中的值与 NOT IN 中的某个常量值相等时，结果为 False，表明此记录为不符合查询条件的记录&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM user_profile
where university NOT IN &apos;复旦大学&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当需要过滤空值时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT device_id, gender, age, university FROM user_profile
WHERE age != &apos;&apos;;
# 或者
SELECT device_id, gender, age, university FROM user_profile
WHERE age is NOT NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AND、OR 语句&lt;/h3&gt;
&lt;p&gt;bool 值运算关系，混合使用时注意 &lt;strong&gt;AND 的优先级大于 OR&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT device_id, gender, age, university, gpa FROM user_profile
WHERE gpa &amp;gt; 3.5 AND gender = &apos;male&apos;

SELECT device_id, gender, age, university, gpa FROM user_profile
WHERE (gpa &amp;gt; 3.5 and university = &apos;山东大学&apos;) OR (gpa &amp;gt; 3.8 and university = &apos;复旦大学&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;模糊匹配&lt;/h3&gt;
&lt;p&gt;数据库中要进行模糊查询，主要使用以下操作：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;_&lt;/code&gt;：下划线，代表匹配任意一个字符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%&lt;/code&gt;：百分号，代表匹配 0 个或多个字符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt;：中括号，代表匹配其中的任意一个字符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[^]&lt;/code&gt;：取反，不匹配其中的任意一个字符&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;like 模糊查询用法：
  &apos;%北京&apos;   北京开头的
  &apos;_北京%&apos;  第二三个字为北京
  &apos;%北京%&apos;  含有北京的

SELECT device_id, age, university FROM user_profile
WHERE university LIKE &apos;%北京%&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
like 模糊查询会引起全表扫描，速度比较慢，应该尽量避免使用 like 关键字进行模糊查询。
:::&lt;/p&gt;
&lt;h3&gt;SQL 中的常用函数&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;max&lt;/strong&gt;：查找某个条件下最大值的时候，一般有两种方法：第一种为使用降序排序，取第一位；第二种为使用 max 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT max(gpa) FROM user_profile WHERE university = &apos;复旦大学&apos;

# 上为第二种，下为第一种方法
SELECT gpa FROM user_profile WHERE university = &apos;复旦大学&apos; ORDER BY gpa DESC limit 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;count、round、avg&lt;/strong&gt;：count 用于统计某个值的数量，round 用于保留几位小数，avg 用于求某一列的平均值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select COUNT(gender) as male_num, round(avg(gpa), 1) as avg_gpa
from user_profile where gender = &apos;male&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;having&lt;/strong&gt;：聚合函数结果作为筛选条件时，不能用 where，而是用 having 语法，配合重命名即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT university, avg(question_cnt) as avg_question_cnt, avg(answer_cnt) as avg_answer_cnt
FROM user_profile
GROUP BY university
HAVING avg_question_cnt &amp;lt; 5 OR avg_answer_cnt &amp;lt; 20
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多表查询&lt;/h3&gt;
&lt;p&gt;若一个查询同时涉及两个或两个以上的表，则称之为连接查询。连接查询是关系数据库中最主要的查询，包括内连接、外连接、交叉连接等，用于连接两个表的条件称为连接条件或者连接谓词。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT … FROM 表名 [INNER] JOIN 被连接表 ON 连接条件

# 以上结果中会包含重复的列
SELECT * FROM 学生表 INNER JOIN 班级表 ON 学生表.班号=班级表.班号

# 去除重复列
SELECT 学号, 姓名, 班级表.班号, 班名 FROM 学生表 JOIN 班级表 ON 学生表.班号=班级表.班号

# 多表连接示例
select u.university, count(q.question_id) / count(DISTINCT (q.device_id)) as avg_answer_cnt
from user_profile as u
right join question_practice_detail as q
on u.device_id = q.device_id
group by u.university
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;right join 联结结果保留右表的全部数据；left join 保留左表的全部数据；inner join 保留两表的公共数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;组合查询&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;select device_id, gender, age, gpa
from user_profile
where university = &apos;山东大学&apos;
union all
select device_id, gender, age, gpa
from user_profile
where gender = &apos;male&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;union 会去重，union all 不会去重；如果直接用 &lt;code&gt;where university = &apos;山东大学&apos; or gender = &apos;male&apos;&lt;/code&gt; 也会去重。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;练习参考：&lt;a href=&quot;https://www.nowcoder.com/exam/oj?tab=SQL%E7%AF%87&amp;amp;topicId=199&quot;&gt;https://www.nowcoder.com/exam/oj?tab=SQL篇&amp;amp;topicId=199&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;二、MySQL 原理&lt;/h2&gt;
&lt;h3&gt;redo-log 与 bin-log 两种日志的区别&lt;/h3&gt;
&lt;p&gt;redo-log 与 bin-log 两种日志是 InnoDB 数据库引擎为了满足事务的持久性与原子性而引入的。其中，redo-log 是 InnoDB 的特性，bin-log 是 MySQL 中 Server 层的日志。&lt;/p&gt;
&lt;h4&gt;redo-log 日志&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;在更新一条记录时，先将其写入 redo-log 中，然后更新内存，此时的记录就算更新完毕。等待数据库引擎空闲时，再将其更新至磁盘中。&lt;/p&gt;
&lt;p&gt;redo-log 大小固定，由 innodb_log_file_size 设置大小和 innodb_log_files_in_group 设置个数，若要修改，则需重启服务。&lt;/p&gt;
&lt;p&gt;一旦更新的数据条数达到 redo-log 日志记录数的上限，数据库引擎则先停止手中的活，将一部分的 redo-log 日志中的数据更新入磁盘，再继续运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本质上来说，redo-log 就是一个循环队列，如下图所示（原图来自极客时间专栏）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/redo_log.png&quot; alt=&quot;redo-log&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中 write_pos 为当前记录日志的地址，checkpoint 为当前要擦除的地址，擦除记录前需要将记录更新到数据库文件。write_pos 和 checkpoint 之间是 redo-log 空闲的地址块，可以用来记录新的操作。如果 write_pos 追上 checkpoint，说明 redo-log 记录操作已满，不能再执行新的更新，需要数据库引擎先擦除一些记录。&lt;/p&gt;
&lt;p&gt;redo-log 可以防止因为数据库异常重启而导致提交记录丢失的问题——这种特性称为 &quot;crash-safe&quot;。&lt;/p&gt;
&lt;h4&gt;bin-log 日志&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;最开始的时候，MySQL 中没有 InnoDB 数据库引擎，其自带的引擎为 MyISAM，而 MyISAM 没有 &quot;crash-safe&quot; 的能力，bin-log 日志只是归档记录的功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;两种日志的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;redo-log 是 &lt;strong&gt;InnoDB 引擎特有&lt;/strong&gt; 的；bin-log 是 MySQL 的 Server 层实现的，所有引擎都可以使用&lt;/li&gt;
&lt;li&gt;redo-log 是 &lt;strong&gt;物理日志&lt;/strong&gt;，记录 &quot;在某个数据页上的修改&quot;；bin-log 是逻辑日志，记录的是这个语句的 &lt;strong&gt;原始逻辑&lt;/strong&gt;，比如 &quot;给 ID=2 这一行的 c 字段加 1&quot;&lt;/li&gt;
&lt;li&gt;redo-log 是 &lt;strong&gt;循环写的，空间固定&lt;/strong&gt;；bin-log 是以 &lt;strong&gt;追加&lt;/strong&gt; 的方式写入的，&quot;追加写&quot; 是 bin-log 文件达到一定大小后，会切换到下一个 bin-log 日志文件，并不会覆盖以前的日志&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以可得到以下结论：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、redo-log 的写盘时间会直接影响系统的吞吐，所以 redo-log 的数据量要尽量少。&lt;/p&gt;
&lt;p&gt;2、由于系统崩溃的不确定性，重启重放 redo-log 文件时，系统不会知道 redo-log 中的那个 page 页已经修改入磁盘，所以 redo-log 的重放必须是可重复的。&lt;/p&gt;
&lt;p&gt;3、一般来说，&lt;strong&gt;建议一个 redo-log 只涉及一个内存 page 页来进行修改&lt;/strong&gt;，这样就可以兼顾逻辑日志与物理日志的优势。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而 redo-log 提交是有两个阶段的：&lt;strong&gt;prepare 和 commit 阶段&lt;/strong&gt;。之所以进行两个阶段的提交，也是为了达到 &quot;crash-safe&quot; 的目的。&lt;/p&gt;
&lt;h4&gt;两阶段提交的情景&lt;/h4&gt;
&lt;p&gt;假设 redo-log 都是一次性提交，不分两个阶段，此时我们对一个字段 c 值进行更新，让 c 值从当前的 0 更新为 1。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情景 1&lt;/strong&gt;：我们先写 redo-log 后写 bin-log。假设在 redo-log 写完、bin-log 并未写完时，MySQL 进程异常重启。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;redo-log 写完之后，虽然系统崩溃也能将 MySQL 异常启动前的数据恢复回来。但是 bin-log 并未写完，此时记录的 bin-log 文件就没有记录之前操作的语句。之后用于备份的 bin-log 中也没有相应的操作语句，而若我们需要恢复临时库，由于 bin-log 记录的语句丢失，临时库恢复出来的这一行 c 的值就是 0，与原库的值不同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;情景 2&lt;/strong&gt;：我们先写 bin-log，后写 redo-log。假设在 bin-log 写完、redo-log 并未写完时，MySQL 进程异常重启。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果在 bin-log 写完之后系统崩溃，由于 bin-log 不具备 &quot;crash-safe&quot; 的功能，而 redo-log 没写，所以崩溃恢复以后这个事务无效。但是 bin-log 里面已经记录了 &quot;c 从 0 改为 1&quot; 的日志，所以在之后用 bin-log 恢复临时库时，就会多出一个事务，恢复出来的 c 的值就是 1，与原库的值不同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以两阶段提交就是让 redo-log 与 bin-log 两个状态保持逻辑上的一致。针对于异常重启，在 MySQL 设置中的 &lt;code&gt;innodb_flush_log_at_trx_commit&lt;/code&gt;、&lt;code&gt;sync_binlog&lt;/code&gt; 均设置为 1，能保证 MySQL 异常重启之后数据不丢失与 bin-log 文件不丢失。&lt;/p&gt;
&lt;h4&gt;一些补充&lt;/h4&gt;
&lt;p&gt;1、MySQL 的 bin-log 完整性：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、statement 格式，最后会有 COMMIT；&lt;/p&gt;
&lt;p&gt;2、row 格式，最后会有 XID event；&lt;/p&gt;
&lt;p&gt;3、MySQL 5.6.5 版本后也引入了 bin-log checksum 用于验证 bin-log 内容的正确性，所以一旦 bin-log 所在的磁盘出现故障，可以通过验证 checksum 来确定准确性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2、关于 bin-log 备份建议：&lt;/p&gt;
&lt;p&gt;一般建议在一周一备份与一日一备份之间进行选择，具体看业务的评估，比如 RTO（恢复目标时间）指标。一周一备份存储成本小，但是 RTO 长；而一日一备份 RTO 时间小，但是存储的成本大。&lt;/p&gt;
&lt;h3&gt;事务中的隔离性&lt;/h3&gt;
&lt;p&gt;事务是数据库在操作数据时，为了保证其逻辑一致性而划分的最小单位。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;事务的特性：&lt;strong&gt;A（Atomicity）、C（Consistency）、I（Isolation）、D（Durability）&lt;/strong&gt;，即原子性、一致性、隔离性、持久性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;事务是保持逻辑一致性、可恢复性的重要方法，而锁是保证事务的完整性与并发性的重要概念。&lt;/p&gt;
&lt;h4&gt;隔离性的概念&lt;/h4&gt;
&lt;p&gt;隔离性的存在主要是为了&lt;strong&gt;区分多个事务并行执行的顺序问题&lt;/strong&gt;，比如多个事务并行执行时，出现脏读、不可重复读、幻读等。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;脏读：读取到其他事务未提交的数据&lt;/p&gt;
&lt;p&gt;不可重复读：前后读取记录内容不一致&lt;/p&gt;
&lt;p&gt;幻读：前后读取记录数量不一致&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以隔离性针对于以上的情况，分成如下几个级别的隔离：&lt;strong&gt;读未提交、读提交、可重复读与串行化&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读未提交：一个事务还未提交，其所做的变更可以被其他事务读取&lt;/li&gt;
&lt;li&gt;读提交：一个事务提交之后，其所做的变更才能被其他事务读取&lt;/li&gt;
&lt;li&gt;可重复读：一个事务执行过程中看到的数据是一致的，未提交时其所做的变更对其余事务不可见&lt;/li&gt;
&lt;li&gt;串行化：对一个记录进行加读写锁，若其发生冲突，后访问的事务需要等前一个事务执行完毕时，才能继续执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面四种情况，并行性逐步降低，但是安全性逐步升高。&lt;/p&gt;
&lt;p&gt;:::note
Oracle 数据库的默认隔离级别是读提交，所以从 Oracle 数据库迁移到 MySQL 数据库中，需要将 MySQL 的启动参数 &lt;code&gt;transaction-isolation&lt;/code&gt; 的值设置成 READ-COMMITTED。MySQL 数据库默认的隔离级别是可重复读，但是可重复读会导致幻读的情况。
:::&lt;/p&gt;
&lt;h4&gt;隔离性的实现&lt;/h4&gt;
&lt;p&gt;在 MySQL 数据库中，为了控制并发执行的语句的顺序，引入了多版本并发控制 MVCC（Multi-Version Concurrency Control）。&lt;/p&gt;
&lt;p&gt;其具体内容可以概括如下：每条记录在更新的时候都会同时记录一条回滚日志：undo-log，同一条记录在系统中可以存在多个版本：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/MySQL-isolation1.png&quot; alt=&quot;isolation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;若我们需要将当前 4 的值恢复到 1 时，需要执行 3 次 undo-log 回滚。而当系统中不存在比回滚日志更早的 read-view 时，undo-log 则会被删除。以上图为例子，当 read-viewA 视图被删除后，将 3 改为 2 之前的 undo-log 即会被删除。所以说一般开发时，MySQL 数据层的事务尽量避免过长。&lt;/p&gt;
&lt;h4&gt;事务的启动方式&lt;/h4&gt;
&lt;p&gt;MySQL 中的事务启动方式一般为两种：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、显式启动：使用 begin 或 start transaction，配套的提交语句为 commit，回滚语句为 rollback&lt;/p&gt;
&lt;p&gt;2、set autocommit = 0：此命令会关闭自动提交，只有手动执行上述配套的语句，才能进行一个事务的提交，或者断开连接时提交&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以建议使用 &lt;strong&gt;set autocommit = 1&lt;/strong&gt;，通过显式语句来启动事务，而 commit 命令一般使用 &lt;strong&gt;commit work and chain&lt;/strong&gt; 命令，即提交并且启动下一个事务。带来的效果是从程序开发的角度可以明确的知道每个语句是否存在于事务中。&lt;/p&gt;
&lt;h2&gt;三、MySQL 优化&lt;/h2&gt;
&lt;h3&gt;索引&lt;/h3&gt;
&lt;p&gt;索引只是一种数据结构而已，具体看 MySQL 的数据库引擎。比如 MyISAM 使用 B 树，InnoDB 使用的是 B+ 树。&lt;/p&gt;
&lt;p&gt;:::note
聚簇索引与非聚簇索引就是 B 树与 B+ 树的两个别名。
:::&lt;/p&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;少量数据不需要索引&lt;/li&gt;
&lt;li&gt;频繁更新的数据不适合作为索引&lt;/li&gt;
&lt;li&gt;很少使用的字段不需要索引&lt;/li&gt;
&lt;li&gt;索引提升查询效率，但会降低增删改的效率&lt;/li&gt;
&lt;li&gt;索引占用空间很大&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询效率高&lt;/li&gt;
&lt;li&gt;CPU 占用少（order by XXX desc 时，B 树、B+ 树不用排序，所以 CPU 计算少）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;索引细节&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;分类&lt;/strong&gt;：索引一般有三种：单值索引、唯一索引、复合索引。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单值索引：单列的索引，一个表可以有很多单值索引&lt;/li&gt;
&lt;li&gt;唯一索引：所有值不能重复，即 distinct&lt;/li&gt;
&lt;li&gt;复合索引：多个列组成的二级索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;创建索引&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 方式一：create
create index 索引名称 on 表(字段名称)                    # 单值
create unique index 索引名称 on 表(字段名称)             # 唯一
create index 索引名称 on 表(字段名称)                    # 复合

# 方式二：alter
alter table 表名 add index 索引名称(字段名称)            # 单值
alter table 表名 add unique index 索引名称(字段名称)     # 唯一
alter table 表名 add index 索引名称(字段名称, 字段名称)   # 复合
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
primary key 设置后自动就是主键索引。主键与唯一索引均不能为 null。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;查询/删除索引&lt;/strong&gt;：&lt;code&gt;show index from 表名&lt;/code&gt; / &lt;code&gt;drop index 索引名 on 表名&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;SQL 优化之执行计划&lt;/h3&gt;
&lt;p&gt;SQL 优化的主要原因即是：性能低、SQL 语句执行时间长、等待时间长、索引失效、服务器参数不合理等。&lt;/p&gt;
&lt;p&gt;:::warning
有索引才能涉及到 SQL 优化，没有索引一般都是 ALL 级别。
:::&lt;/p&gt;
&lt;p&gt;先说 SQL 语句的执行。在 MySQL 数据库引擎进行数据解析的过程中，识别 SQL 语句后，实际的执行过程与编写的语句不一致，一般执行过程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from...on...join...where...group by...
having...select (distinct)...order by... limit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优化时，必须解决的问题就是 SQL 语句的执行计划。SQL 执行计划的关键字：&lt;strong&gt;explain&lt;/strong&gt;，可以模拟 SQL 优化器执行 SQL 语句，从而让开发人员具体知道自己写的 SQL 语句是如何执行的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用方法&lt;/strong&gt;：&lt;code&gt;explain + SQL 语句&lt;/code&gt;，如 &lt;code&gt;explain select * from tables&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/index1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;id 值相同的执行计划&lt;/h4&gt;
&lt;p&gt;id 值相同，即按照 table 字段从上到下的顺序进行执行。而此种情况下的表查询的顺序，会因为表内数据量的多少而改变。原因是计算权重的算法就是笛卡尔积，笛卡尔积是连乘，产生的中间过程的数据量，理应越小越好。所以在执行计划中，在表最终的结果集一致的情况下，表遵循表内数据从小到大的顺序进行执行。&lt;/p&gt;
&lt;h4&gt;id 值不同的执行计划&lt;/h4&gt;
&lt;p&gt;id 值不同时，id 值越大越优先执行。本质即是多表联结查询转变为子查询，子查询是先执行最内层查询，再执行外层查询，所以即是 id 值越大，越优先执行。&lt;/p&gt;
&lt;h4&gt;select_type 查询类型&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;1、primary：主查询方法，为最外层的查询&lt;/p&gt;
&lt;p&gt;2、subquery：主查询的对立方法，即子查询方法，为内层查询&lt;/p&gt;
&lt;p&gt;3、simple：简单查询，即不涉及子查询与 union 查询&lt;/p&gt;
&lt;p&gt;4、derived：衍生查询，会创建一个临时表。（如果在 from 子查询中只涉及一个表，则该表为临时表；如果在 from 子查询中涉及两个表，则左表是临时表）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;type 索引类型&lt;/h4&gt;
&lt;p&gt;一般企业中常用的索引类型如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;system &amp;gt;&amp;gt; const &amp;gt;&amp;gt; eq_ref &amp;gt;&amp;gt; ref &amp;gt;&amp;gt; range &amp;gt;&amp;gt; index &amp;gt;&amp;gt; all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上类型的索引效率越往左越高。其中 system 与 const 只是理想情况下的效果，一般 SQL 优化，在 ref ~ range 之间。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;system（忽略）：只有一条数据的系统表，或衍生表只有一条数据的主查询&lt;/li&gt;
&lt;li&gt;const（忽略）：仅仅能查到一条数据，只能用于 primary key 或 unique index&lt;/li&gt;
&lt;li&gt;eq_ref（尽量满足，但是可遇不可求）：唯一性索引，对于每个索引键的查询，返回匹配的唯一行数据（只能为 1，不能多、也不能是 0），常见于主键索引与唯一索引。即查询的主数据表与临时的表内的数据数量必须一致，才能达到 eq_ref 的效果&lt;/li&gt;
&lt;li&gt;ref：非唯一性索引，对于每一个索引键的查询，返回匹配的所有行（0 或 多）&lt;/li&gt;
&lt;li&gt;range：检索指定范围的行，where 后面为范围查询的情况（between、in、&amp;gt;、&amp;lt; 等），但是 in 有时候会索引失效，转变为 ALL 级别&lt;/li&gt;
&lt;li&gt;index：查询全部索引的数据，索引查询肯定要小于等于全表查询&lt;/li&gt;
&lt;li&gt;all：查询全部表中数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结：system、const 结果只能有一条数据；eq_ref 结果是多条，但是每条数据具有唯一性；ref 结果多条，但是每条数据是 0 或 多条。&lt;/p&gt;
&lt;h4&gt;possible_keys、key、key_len、ref、rows、Extra&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;possible_keys 为预测的索引值，不准确&lt;/li&gt;
&lt;li&gt;key 为实际使用的索引。&lt;strong&gt;注意&lt;/strong&gt;：如果 possible_keys / key 均为 null，则没使用索引&lt;/li&gt;
&lt;li&gt;判断复合索引是否完全被使用。如果复合索引有一个索引允许为空，那么 key_len 长度会默认 +1B，作为空标识符；2B 标识 varchar 的可变长度&lt;/li&gt;
&lt;li&gt;ref 为：指明当前表所参照的字段。&lt;strong&gt;注意&lt;/strong&gt;：与 type 中的 ref 进行区分&lt;/li&gt;
&lt;li&gt;rows 为：被索引优化查询的个数，即量级&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Extra 常见有以下字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;using filesort：性能消耗大，需要进行额外一次排序（查询）。对于单索引，如果排序查找的是同一字段，则不会出现 filesort；如果排序与查找的不是同一字段，则需要使用 using filesort。在复合索引下，避免使用 using filesort，就使用 where 与 order by 按照复合索引的顺序使用，不要跨列就不用使用（最佳左前缀）&lt;/li&gt;
&lt;li&gt;using temporary：出现性能损耗，用到了临时表，一般出现在 group by 语句中&lt;/li&gt;
&lt;li&gt;using index：性能提升，覆盖索引。原因：不读取原文件，只从索引文件读取数据，不需要回表查询。但是使用 using index 时，会对 possible_keys 和 key 造成影响：没有 where 时，索引出现在 key 中；有 where 时，索引出现在 key 和 possible_keys 中&lt;/li&gt;
&lt;li&gt;using where：既查索引，又查原表（即回表查询）&lt;/li&gt;
&lt;li&gt;impossible where：where 子句永远为 false&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;单表优化&lt;/h3&gt;
&lt;p&gt;在进行单表优化时，主要注意以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;where 与 order by 联合使用索引查询，尽量不要跨列进行搜索&lt;/li&gt;
&lt;li&gt;如果复合索引的使用顺序全部一致（且不跨列使用），则复合索引全部使用；如果部分使用（且不跨列使用），则使用部分索引&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子，假设我们有一张 book 表，建表语句如下所示，我们也先添加四个数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create table book
(
	bid int(4) primary key,
	name varchar(20) not null,
	authorid int(4) not null,
	publicid int(4) not null,
	typeid int(4) not null
);

insert into book values(1, &apos;tjava&apos;, 1, 1, 2);
insert into book values(2, &apos;tc&apos;, 2, 1, 2);
insert into book values(3, &apos;wx&apos;, 3, 2, 2);
insert into book values(4, &apos;math&apos;, 4, 3, 2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们想查询 authorid = 1 且 typeid 为 2 或 3 的 bid：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select bid from book where typeid in (2,3) and authorid = 1;
explain select bid from book where typeid in (2,3) and authorid = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/explain1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发现没有索引的情况下，默认发生的是全表查询，即 explain 执行计划 type 字段值为 all，extra 字段为 using where。而后我们为了让优化的效果更加明显一点，再配合 order by desc 来使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select bid from book where typeid in (2,3) and authorid = 1 order by typeid desc;
explain select bid from book where typeid in (2,3) and authorid = 1 order by typeid desc;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/explain2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时我们会发现，extra 字段会出现 using filesort 字段值，即查询全表后还对返回的数据重新排序，这样很浪费性能，所以我们对其添加索引优化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alter table book add index idx_bta (bid, typeid, authorid);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再执行上述的 SQL 语句：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/explain3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可见，增加索引后可以将全表查询变为索引查询，type 等字段发生改变，但是 using where 与 using filesort 还是未能优化掉，所以说此条 SQL 语句还能继续进行相应的优化。&lt;/p&gt;
&lt;p&gt;而 SQL 语句的优化，即是我们之前说的，理解 SQL 语句在 MySQL 中的执行顺序，那么按照执行顺序进行索引的优化即是最好。比如我们这里是先查 bid，再查 typeid 与 authorid。但是 MySQL 中，select 语句执行时在 where 之后，所以优化时，索引应为 (typeid, authorid, bid)。&lt;/p&gt;
&lt;p&gt;**那么索引 bid 是否能去除呢？**这里我选择不去除，因为虽然删除 bid 索引后，根据索引也能回表进行查询，但是如果只查一次索引即可得到数据，还是建议只查索引。虽然这样会导致索引很大，但是牺牲空间换时间也是一种常用的做法。&lt;/p&gt;
&lt;p&gt;继续回到原 SQL 语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select bid from book where typeid in (2,3) and authorid = 1 order by typeid desc;
explain select bid from book where typeid in (2,3) and authorid = 1 order by typeid desc;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这里注意&lt;/strong&gt;：如果是范围查询的 &lt;code&gt;in&lt;/code&gt; 语句，有时候会生效，有时候会失效，所以我们需要在开始使用索引时就走一个必定生效的索引，更改语句如下。同时，我们需要先删除之前添加的索引，防止之前的索引干扰：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;drop index idx_bta on book;

select bid from book where authorid = 1 and typeid in (2,3) order by typeid desc;
explain select bid from book where authorid = 1 and typeid in (2,3) order by typeid desc;

alter table book add index idx_atb (authorid, typeid, bid);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/explain4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过 explain 查看相应的执行计划，可见 type 级别从 index 变为了 range，提升了一个级别。其中有字段 &lt;code&gt;Backward index scan&lt;/code&gt;，这是 MySQL 8 的新特性，叫做降序索引。在 MySQL 5.x 中执行计划显示 type 优化为 ref 级别，而在 MySQL 8 中则是 range 级别，可见在 MySQL 8 中，这样优化达不到 MySQL 5.x 的优化效果。&lt;/p&gt;
&lt;p&gt;using where 与 using index 最大的区别即是需不需要回原表进行查询。而两者同时出现，即是 in 范围查询有时候失效、有时候不失效的情况。所以为了消除这种情况，一般情况用其他有效条件替换 in 即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select bid from book where authorid = 1 and typeid = 3 order by typeid desc;
explain select bid from book where authorid = 1 and typeid = 3 order by typeid desc;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/explain5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其 explain 结果可见 type 字段变为 ref 值，优化上升两个级别，而一般达到 ref 或者 eq_ref 级别，已经是比较好的一种情况了。&lt;/p&gt;
&lt;h3&gt;多表优化&lt;/h3&gt;
&lt;p&gt;创建两个表 teacher2 与 course2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create table teacher2
(
	id int(4) primary key,
	cid int(4) not null
);

create table course2
(
	cid int(4),
	cname varchar(20)
);

insert into teacher2 values (1,2);
insert into teacher2 values (2,1);
insert into teacher2 values (3,3);

insert into course2 values (1,&apos;java&apos;);
insert into course2 values (1,&apos;python&apos;);
insert into course2 values (1,&apos;kotlin&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们将两表通过左连接进行两表连接，查询 cname = &apos;java&apos; 的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from teacher2 as t left outer join course2 as c on t.cid = c.cid where c.cname = &apos;java&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/explain6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可见 type 为 all，即全表查询，而且 extra 出现一个新的字段值：using join buffer。出现此字段就说明 SQL 写得很差，MySQL 底层使用优化器对 SQL 进行了优化，即使用连接缓存。但是由于不涉及索引查询，所以必然还是很慢。&lt;/p&gt;
&lt;p&gt;所以两表的索引应该如何去加呢？直接上结论：&lt;strong&gt;小表驱动大表&lt;/strong&gt;，写法如 &lt;strong&gt;小表.X = 大表.X&lt;/strong&gt;。原理就是根据 CPU 与内存的空间局部性原理，不用频繁进入磁盘访存拿出数据。&lt;/p&gt;
&lt;p&gt;所以，关于两表索引的增加，一般是先看连接，比如左外连接在左表上加索引、右外连接在右表上加索引。小表中使用频繁的字段加索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alter table teacher2 add index index_teacher2_cid(cid);
alter table course2 add index index_course2_cname(cname);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/explain7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此后执行效果直接提升为 ref 级别，而且不再出现 using join buffer，证明 SQL 语句优化得还行。&lt;/p&gt;
&lt;p&gt;而关于三表优化只需要记住两个原则即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小表驱动大表&lt;/li&gt;
&lt;li&gt;索引建立在经常查询的字段上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
注意：SQL 优化是一种概率事件！是否实际使用了我们的优化，需要通过 explain 进行查看。
:::&lt;/p&gt;
&lt;h3&gt;避免索引失效的一些原则&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;复合索引不跨列或者无序使用（最佳左前缀匹配），尽量使用全索引匹配&lt;/li&gt;
&lt;li&gt;不要在索引上进行任何操作（函数计算、类型转换等），否则都会使索引失效：
&lt;ul&gt;
&lt;li&gt;复合索引中，如果对左侧的索引进行操作，那么包括此索引的右侧索引全部失效&lt;/li&gt;
&lt;li&gt;复合索引不能使用不等于（!=、&amp;gt;、&amp;lt;）或 is null（is not null），否则自身以及右侧的索引全部失效&lt;/li&gt;
&lt;li&gt;复合索引使用等于（=、&amp;gt;、&amp;lt;）有部分概率使自身以及右侧的索引全部失效&lt;/li&gt;
&lt;li&gt;SQL 优化由于 SQL 优化器等原因，并非 100% 成立。一般而言，范围查询的（&amp;gt;、&amp;lt;、in）之后的索引失效&lt;/li&gt;
&lt;li&gt;补救方式：尽量使用覆盖索引&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;like 后尽量以 &quot;常量&quot; 开头，不要以 % 开头，否则索引失效&lt;/li&gt;
&lt;li&gt;尽量不要使用类型转换（显式、隐式），否则索引失效&lt;/li&gt;
&lt;li&gt;尽量不要使用 or，否则索引失效，左右的索引都会失效&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;索引优化方法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;exist 与 in 使用情况：主查询数据集大用 in，子查询数据集大用 exist&lt;/li&gt;
&lt;li&gt;order by 优化：
&lt;ul&gt;
&lt;li&gt;using filesort 有两种算法：双路排序与单路排序（根据 I/O 的次数）&lt;/li&gt;
&lt;li&gt;选择使用单路、双路，调整 buffer 的容量大小&lt;/li&gt;
&lt;li&gt;避免使用 select *&lt;/li&gt;
&lt;li&gt;复合索引不跨列&lt;/li&gt;
&lt;li&gt;保证全部的排序字段排序的一致性（都是升序或降序）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SQL 排查之慢查询&lt;/h3&gt;
&lt;p&gt;慢查询日志是 MySQL 提供的一种日志记录，用于记录 MySQL 中响应时间超过阈值的 SQL 语句。配置文件中显示为：long_query_time，默认为 10s。慢查询日志默认是关闭的：开发时建议打开，部署时关闭：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/slow-query.png&quot; alt=&quot;默认关闭&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一般是临时开启，如果需要永久开启，直接在配置文件中追加配置即可。&lt;/p&gt;
&lt;p&gt;Linux 上也能通过使用 mysqldumpslow 工具对慢 SQL 语句进行排查，可以通过设置，对 SQL 语句进行快速筛选。使用命令 &lt;code&gt;mysqldumpslow --help&lt;/code&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;s：排序方式&lt;/li&gt;
&lt;li&gt;r：逆序&lt;/li&gt;
&lt;li&gt;l：锁定时间&lt;/li&gt;
&lt;li&gt;g：正则匹配&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;参考 bash 写法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 获取返回记录最多的 3 个 SQL
mysqldumpslow -s r -t 3 /var/lib/mysql/localhost-slow.log

# 获取访问次数最多的 3 个 SQL
mysqldumpslow -s c -t 3 /var/lib/mysql/localhost-slow.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
一般来说，实际的生产过程中有挺多复杂的场景，但是在业务量没有提升的时候，具体问题可以不进行优化，待业务量与使用量上来后，再进行优化，毕竟做事都是有时间成本的。
:::&lt;/p&gt;
</content:encoded></item><item><title>Redis 学习笔记：概念、持久化与底层</title><link>https://chaggle.github.io/posts/2026/08/11/redis-notes/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/redis-notes/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;2022 年学习 Redis 的两篇笔记（浅谈 Redis 中间件、深入理解 Redis 底层）合并整理。教程来源于 b站狂神说 Java 的 Redis 教程，底层部分配图主要来自极客时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Redis 概念&lt;/h2&gt;
&lt;p&gt;1、Redis 是基于计算机内存的数据库，一般称为缓存数据库，由于没有固定的表结构与关系，也叫 NoSQL（Not Only SQL）数据库。&lt;/p&gt;
&lt;p&gt;2、为什么要使用 Redis：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据的爆发增长&lt;/li&gt;
&lt;li&gt;没有固定关系的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3、最新的版本中，官方不建议直接在 win 上直接使用 Redis，而是使用 WSL2 Linux 子系统进行 Redis 的开发。&lt;/p&gt;
&lt;p&gt;4、Redis 常见使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存缓存&lt;/li&gt;
&lt;li&gt;不想使用 Kafka 的话，可以作为消息队列而存在&lt;/li&gt;
&lt;li&gt;做电商的热点数据保存，因为可以设置 TTL&lt;/li&gt;
&lt;li&gt;朋友圈点赞&lt;/li&gt;
&lt;li&gt;做秒杀的库存&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Redis benchmark 压力测试&lt;/h2&gt;
&lt;p&gt;使用官方自带的 redis-benchmark 工具进行压力测试，其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-h：host，可以是本地，也能是远程进行压力测试&lt;/li&gt;
&lt;li&gt;-p：port，即端口号&lt;/li&gt;
&lt;li&gt;-c：connection，连接的数量，基本上连接都是基于 TCP/socket 的&lt;/li&gt;
&lt;li&gt;-n：跟时间复杂度与空间复杂度差不多，即指定每一个连接数的请求次数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;redis-benchmark -h localhost -p 6379 -c 100 -n 10000

# 如果需要保存在一个日志文件中
redis-benchmark -h localhost -p 6379 -c 100 -n 10000 &amp;gt; redis.log
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Redis 基本知识&lt;/h2&gt;
&lt;p&gt;Redis 默认有 16 个数据库，默认是第 0 个数据库。Redis 的命令大小写不敏感，而其中的 key - value 大小写敏感。常见的命令有：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;select&lt;/td&gt;
&lt;td&gt;切换数据库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbsize&lt;/td&gt;
&lt;td&gt;查看数据库大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flushdb&lt;/td&gt;
&lt;td&gt;清空当前数据库所有 K-V 值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;flushall&lt;/td&gt;
&lt;td&gt;清空 Redis 数据库中所有 K-V 值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Redis 的限制跟 CPU 的性能无关，性能主要受制于硬件的内存与网络带宽。&lt;/p&gt;
&lt;h2&gt;Redis 设计目标与底层数据结构&lt;/h2&gt;
&lt;p&gt;Redis 主要是为了实现&lt;strong&gt;高性能、高可靠、高可扩展性&lt;/strong&gt;三个目标。而全面学习 Redis，主要是当 Redis 出现相应的故障问题时，开发人员能快速对问题进行定位与解决：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/middleware/redis1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Redis 底层数据结构主要采用整数数组和压缩列表，但是在查找时间复杂度方面并没有很大的优势，那为什么 Redis 还会把它们作为底层数据结构呢？这有两个方面的原因：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、从内存利用率出发，数组和压缩列表都是非常紧凑的数据结构，它比链表占用的内存要更少。Redis 是内存数据库，大量数据存到内存中，此时需要做尽可能的优化，提高内存的利用率。&lt;/p&gt;
&lt;p&gt;2、从 CPU 高速缓存出发，Redis 在设计时，集合数据元素较少的情况下，默认采用内存紧凑排列的方式存储，同时利用 CPU 高速缓存不会降低访问速度。当数据元素超过设定阈值后，避免查询时间复杂度太高，转为哈希和跳表数据结构存储，保证查询效率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Redis 五大基本类型&lt;/h2&gt;
&lt;h3&gt;String 类型&lt;/h3&gt;
&lt;p&gt;String 类型中常见的几种命令：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;keys *&lt;/td&gt;
&lt;td&gt;查询所有的 key 值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type key&lt;/td&gt;
&lt;td&gt;查看 key 的类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exist key&lt;/td&gt;
&lt;td&gt;是否存在 key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;append key value&lt;/td&gt;
&lt;td&gt;向 key 类型添加数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;incr key&lt;/td&gt;
&lt;td&gt;key 的 value 原子性 + 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;decr key&lt;/td&gt;
&lt;td&gt;key 的 value 原子性 - 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;incrby key [numbers]&lt;/td&gt;
&lt;td&gt;key 的 value 原子性 + numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;decrby key [numbers]&lt;/td&gt;
&lt;td&gt;key 的 value 原子性 - numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expire key [second]&lt;/td&gt;
&lt;td&gt;设置 key 的过期时间，默认单位是秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getrange key start end&lt;/td&gt;
&lt;td&gt;取 key 的 value 从 start 到 end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;setrange key offset val&lt;/td&gt;
&lt;td&gt;设置 key 的 value 的 offset 位后替换为 val&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ttl key&lt;/td&gt;
&lt;td&gt;查看 key 剩余过期时间，默认单位是秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;setex key expire value&lt;/td&gt;
&lt;td&gt;设置 key value 以及过期时间 expire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;setnx key value&lt;/td&gt;
&lt;td&gt;如果 key 不存在，则设置 value，存在则设置失败（分布式锁使用，乐观锁）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getset key value&lt;/td&gt;
&lt;td&gt;先得到 key 再设置 key value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;getset 命令有点类似于 CompareAndSwap 这种类型的操作。同理，还有 mset、mget 为批量设置（非原子性），msetnx、msetex 等（原子性操作）。&lt;/p&gt;
&lt;p&gt;在 Redis 中，如果要设计一种封装类型的对象，语法可以如下所示：&lt;/p&gt;
&lt;p&gt;1、&lt;code&gt;set user:1 {name:zhangsan, age : 18}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;2、&lt;code&gt;set user:1:name zhangsan user:1:age 18&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;String 常见的使用场景为：计数器、统计多单位的数量、粉丝数、对象缓存存储。&lt;/p&gt;
&lt;h3&gt;List 类型&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;lpush [listname] value&lt;/td&gt;
&lt;td&gt;将 value 插入 [listname] 队头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lrange [listname] start end&lt;/td&gt;
&lt;td&gt;查看 [listname] 从队头的 start 到 end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lpop [listname]&lt;/td&gt;
&lt;td&gt;将 [listname] 队头的元素移除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lindex [listname] index&lt;/td&gt;
&lt;td&gt;从队头开始索引第 index 个值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rpush [listname] value&lt;/td&gt;
&lt;td&gt;将 value 插入 [listname] 队尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rpop [listname]&lt;/td&gt;
&lt;td&gt;将 [listname] 队尾的元素移除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llen [listname]&lt;/td&gt;
&lt;td&gt;查看 [listname] 长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lrem [listname] count value&lt;/td&gt;
&lt;td&gt;移除 [listname] 中 count 个值为 value 的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ltrim [listname] start end&lt;/td&gt;
&lt;td&gt;截断 [listname] 从 start 到 end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;List 底层实现就是一个双向链表，所以优点跟缺点都跟双向链表一样。&lt;/p&gt;
&lt;h3&gt;Set 类型&lt;/h3&gt;
&lt;p&gt;Set 集合类型，里面的值不重复：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sadd [setname] value&lt;/code&gt;：向 [setname] 添加 value&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spop [setname] value&lt;/code&gt;：从 [setname] 移除 value&lt;/li&gt;
&lt;li&gt;&lt;code&gt;smembers [setname]&lt;/code&gt;：查看 [setname] 内所有值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sismember [setname] value&lt;/code&gt;：判断 value 是否存在 [setname] 中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;srem [setname] value&lt;/code&gt;：移除 [setname] 中的 value&lt;/li&gt;
&lt;li&gt;&lt;code&gt;srandmember [setname] count&lt;/code&gt;：随机获取 [setname] 内的一个值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;smove source destination value&lt;/code&gt;：将 value 从 source 移动到 destination&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sdiff [setname1] [setname2]&lt;/code&gt;：[setname1] 与 [setname2] 的差集&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sinter [setname1] [setname2]&lt;/code&gt;：[setname1] 与 [setname2] 的交集&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sunion [setname1] [setname2]&lt;/code&gt;：[setname1] 与 [setname2] 的并集&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Set 为一个无序、不重复集合。&lt;/p&gt;
&lt;h3&gt;Hash 类型&lt;/h3&gt;
&lt;p&gt;其实就是变为 key - map 结构，只是一个 map 集合。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hash 类型的命令大同小异，只是在基本操作前加一个 h，如 hget、hset、hgetall、hlen、hexists、hmset 等。其中独有的特性即是：hkeys 与 hvals 是只获取 k 或 v，以及 hincrby、hdecrby、hsetnx、hsetex 等命令。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;虽然 Hash 与 String 很相似，但是从特点来看，Hash 更适合存储结构体对象，String 更适合存储字符串类型。&lt;/p&gt;
&lt;h3&gt;Zset 类型&lt;/h3&gt;
&lt;p&gt;有序集合，在 Set 基础之上增加了一个排序维度。常见命令有 zadd、zrange、zrangebyscore xxx -inf +inf（涉及正负无穷的问题）、zcard（记个数）、zrem（删除）、zcount 等。其中没有 ( 与 ) 即是闭区间，有 ( 与 ) 是开区间，比大小的时候经常使用这种方式。&lt;/p&gt;
&lt;p&gt;zset 主要存储重要消息、带权重进行判断、排行榜的应用实现 Top N 测试。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多的命令，可以前往 Redis 的官网进行查询。我们使用 Go、Java、Cpp 等语言做开发的时候，Java 用 Jedis、Go 用 go-redis，具体使用看文档即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Redis 三大特殊数据类型&lt;/h2&gt;
&lt;h3&gt;Geospatial&lt;/h3&gt;
&lt;p&gt;Geospatial 在 Redis 中即是 geo，在 Redis 3.x 版本就已经推出了，可以推算地理位置信息，比如两地之间的距离、方圆几里的人。geo 的 api 在现在的官网只有 9 个命令：geoadd、geopos、geodist、georadius。其中 geo 的底层实现还是基于 zset，所以 zset 的命令都能操作 geo 数据结构。如果不做地图地理信息的话，基本上项目内不会使用此数据类型。&lt;/p&gt;
&lt;h3&gt;HyperLogLog&lt;/h3&gt;
&lt;p&gt;HyperLogLog 是基数统计的算法，用于网页的 UV 计数。此数据结构占用内存非常小，只占用 $2^{64}$ B，即 12 KB 大小，所以从内存角度，HyperLogLog 是优先选择。使用即是 pfadd、pfcount、pfmerge。&lt;/p&gt;
&lt;h3&gt;Bitmaps&lt;/h3&gt;
&lt;p&gt;位图，跟操作系统里面的一样，Redis 里面可以统计用户信息：活跃、登陆、打开。操作为：setbit、getbit、bitcount。&lt;/p&gt;
&lt;h2&gt;Redis 事务&lt;/h2&gt;
&lt;p&gt;:::caution
在 Redis 中，单条命令保证原子性，而其事务是不保证原子性的！
:::&lt;/p&gt;
&lt;p&gt;事务的本质即是：一组命令的集合。事务具有一次性、顺序性、排他性。Redis 中的事务是没有隔离性的概念，没有隔离级别。所有的命令在事务中没有被执行，必须发起执行命令的时候，事务才能执行，即：exec 命令。&lt;/p&gt;
&lt;p&gt;Redis 事务执行方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开启事务（multi）&lt;/li&gt;
&lt;li&gt;命令入队，返回值均为 queue&lt;/li&gt;
&lt;li&gt;执行事务（exec）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事务也能放弃，命令即是：discard（即放弃事务队列里的所有命令）。&lt;/p&gt;
&lt;p&gt;以下几点异常需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、编译型异常：代码错误、错误命令，Redis 事务队列不会去执行&lt;/li&gt;
&lt;li&gt;2、运行时异常：如果事务队列中存在语法性错误，那么执行命令的时候，只放弃命令错误的那几条&lt;/li&gt;
&lt;li&gt;3、&lt;strong&gt;注意：错误命令与命令错误这两种说法的区别&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Redis 实现乐观锁&lt;/h2&gt;
&lt;p&gt;乐观锁：只会在更新数据的时候去判断一下，在此期间是否有修改过数据。所以使用 watch 命令对 key 值进行监视即可。修改失败，用 unwatch 解锁，再 watch key，即可。&lt;/p&gt;
&lt;h2&gt;redis.conf 配置文件&lt;/h2&gt;
&lt;p&gt;Redis 大小写不敏感在配置文件中已经说明了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;daemonize  yes # 后台守护进程开启，默认为 no

pidfile /var/run/redis_6379.pid # 后台运行的 pid 值

save 900 1 # 快照 900s 内修改一个，就自动保存一次
save 300 10
save 60 10000

config get / set requirepass  # 设置 redis 的安全密码

maxmemory-policy # 内存到达上限的处理政策，一般以下 6 种处理方法
1、volatile-lru : 只对设置了过期时间的 key 进行 lru
2、allkeys-lru
3、volatile-random 随机删除设置了过期时间的 key
4、allkeys-random
5、volatile-ttl 删除即将过期的
6、noeviction 永不过期，返回错误
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;AOF 与 RDB&lt;/h2&gt;
&lt;p&gt;在大部分情况下，RDB 是默认的持久化配置，大部分的情况下都够用了。&lt;/p&gt;
&lt;p&gt;一般进行数据持久化的保存操作过程一般分为五步：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(1) 客户端向服务端发送写操作（数据在客户端的内存中）&lt;/p&gt;
&lt;p&gt;(2) 数据库服务端接收到写请求的数据（数据在服务端的内存中）&lt;/p&gt;
&lt;p&gt;(3) 服务端调用系统调用函数 write，将数据写入磁盘（数据在系统内存的缓冲区中）&lt;/p&gt;
&lt;p&gt;(4) 操作系统将缓冲区中的数据转移到磁盘控制器上（数据在磁盘缓存中）&lt;/p&gt;
&lt;p&gt;(5) 磁盘控制器将数据写到磁盘的物理介质中（数据真正落到磁盘上）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以上五个步骤为在理想条件下，一个正常的保存流程。但是在大多数情况下，我们的机器等等都会有各种各样的故障，这里划分了两种情况：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、Redis 数据库发生故障，只要在上面的第三步执行完毕，那么就可以持久化保存，剩下的两步由操作系统替我们完成&lt;/p&gt;
&lt;p&gt;2、操作系统发生故障，必须上面 5 步都完成才可保存&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;RDB&lt;/h3&gt;
&lt;p&gt;RDB（Redis DataBase）操作是&lt;strong&gt;在内存中的数据库记录定时 dump 到磁盘上的 RDB 持久化&lt;/strong&gt;。这种方式就是将内存中数据以快照的方式写入到二进制文件中，默认的文件名为 dump.rdb：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/SQL/RDB.jpg&quot; alt=&quot;RDB&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在执行 RDB 快照备份时候，一般会通过父进程 fork 一个子进程，将数据写到一个临时文件，快照写完后，替换原来的快照文件，子进程就退出，临时文件变成正式的 RDB 文件。&lt;/p&gt;
&lt;p&gt;但是 RDB 对数据的完整性不敏感，最后一次持久化之后的数据在宕机后可能丢失。&lt;/p&gt;
&lt;p&gt;RDB 的触发条件如下：&lt;/p&gt;
&lt;p&gt;1、满足默认的 save 条件，会触发 rdb 规则&lt;/p&gt;
&lt;p&gt;2、执行 flushall，也会触发 rdb 规则&lt;/p&gt;
&lt;p&gt;3、退出 redis，也会产生 rdb 文件&lt;/p&gt;
&lt;p&gt;RDB 的恢复条件如下：&lt;/p&gt;
&lt;p&gt;1、将 dump.rdb 文件放在 redis 启动目录即可，redis 会自动检查其中的数据并恢复&lt;/p&gt;
&lt;p&gt;2、查看需要存在的位置：redis-cli 中 config get dir 即可&lt;/p&gt;
&lt;p&gt;rdb 适合大规模的数据恢复，但是需要一定的时间间隔进程操作。&lt;/p&gt;
&lt;h3&gt;AOF&lt;/h3&gt;
&lt;p&gt;AOF（Append Only File），记录服务器执行的&lt;strong&gt;所有写操作&lt;/strong&gt;命令，类似于 MySQL 中的日志。在服务器启动时，通过重新执行 AOF 这些命令来还原数据集。相应的配置文件为 appendonly.aof，默认不开启，需要手动进行启动。&lt;/p&gt;
&lt;p&gt;其他相应的配置可以查看 redis 的相应的 conf 文件，比如 appendfsync、auto-aof-rewrite-percentage、auto-aof-rewrite-min-size 等。aof 文件大小大于 64mb 后就会产生重写的操作。当 aof 文件出现问题时，需要使用 &lt;code&gt;redis-check-aof --fix&lt;/code&gt; 工具对 aof 文件进行修复。&lt;/p&gt;
&lt;p&gt;aof 同步设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每一次修改都会同步，文件的完整性会更好&lt;/li&gt;
&lt;li&gt;每 1s 同步一次，那么可能会丢失 1s 的数据&lt;/li&gt;
&lt;li&gt;不同步效率是最高的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对比 AOF 与 RDB，AOF 是文件读写流操作，所以运行效率比 RDB 低，数据文件也比 RDB 大，修复数据的速度也比 RDB 慢。&lt;/p&gt;
&lt;p&gt;Redis 还可以&lt;strong&gt;同时使用 AOF 持久化和 RDB 持久化&lt;/strong&gt;。在这种情况下，当 Redis 重启时，它会优先使用 AOF 文件来还原数据集，因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。&lt;/p&gt;
&lt;h2&gt;Redis 发布与订阅&lt;/h2&gt;
&lt;p&gt;发布订阅（pub/sub）是一种消息通信的模型，发送者发送消息，接收者接受消息。一般此模型涉及三种对象：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、消息发送者&lt;/p&gt;
&lt;p&gt;2、频道&lt;/p&gt;
&lt;p&gt;3、消息订阅者&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;模型的视图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/middleware/redis-pub-sub.png&quot; alt=&quot;redis-pub-sub&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Redis 的客户端可以订阅任意数量的频道，其相应的操作文档可以参考前文的 Redis 官方文档。&lt;/p&gt;
&lt;h2&gt;Redis 主从复制&lt;/h2&gt;
&lt;p&gt;在企业中，Redis 集群是必用的，其架构通常为主从模式：一个主节点有多个从节点，一个从节点只有一个主节点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主从复制的作用如下所示：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据冗余：主从复制是可以热备份的，是区别于 Redis 持久化的另外一种数据冗余的方式&lt;/li&gt;
&lt;li&gt;故障恢复：当主节点出现问题时，可以由从节点提供服务，实现故障的快速恢复，也能称为服务冗余&lt;/li&gt;
&lt;li&gt;负载均衡：主从架构主要是一种多读少写型的架构模式，通过多个 Redis 服务器分担读的负载，可以大大提升 Redis 服务器的并发量&lt;/li&gt;
&lt;li&gt;高可用的基石：主从复制是哨兵和集群能实施的基础，所以说是基石&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一般来说，单体 Redis 服务器一旦发生单点故障，那么很有可能会丢失至少 1s 以上的数据，并且单体 Redis 需要处理所有的请求负载，压力较大。而且，单体 Redis 由于服务器的内存有限，不可能让服务器内存全部交给 Redis 进行数据存储。单台 Redis 内存占用不应超过 20GB。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;info replication&lt;/code&gt; 表示查看当前的 Redis 服务器的角色信息。&lt;/p&gt;
&lt;p&gt;实现 Redis 集群模式只需要修改三个配置属性即可，然后使用 Redis 启动这三个配置即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、端口&lt;/li&gt;
&lt;li&gt;2、pid&lt;/li&gt;
&lt;li&gt;3、log 文件名&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置从机时候，使用 &lt;code&gt;slaveof + host + port&lt;/code&gt; 即可配置主从形式，但是一旦此 Redis 重启后，配置就会重置，若需要持久化的配置文件，需要手动设置 redis.conf 配置文件。&lt;/p&gt;
&lt;p&gt;主从复制一般两种形式：全量复制与增量复制。其中，但凡从机只要是重新连接主机，都会发生一次全量复制。&lt;/p&gt;
</content:encoded></item><item><title>2022 年周总结合集</title><link>https://chaggle.github.io/posts/2026/08/11/weekly-summaries-2022/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/weekly-summaries-2022/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;2022 年 3 月至 6 月写的 9 篇周总结合并整理。求职、成长、技术学习的记录，按时间顺序保留。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第 11 周总结（2022-03-14）&lt;/h2&gt;
&lt;p&gt;上一周没怎么写博客，第一个是杂事比较多，理论方向的学习偏少；第二是跟大佬交流，发现自己存在找原因性的思维方式，需要转变自己的思维。因此，一段时间写一次总结性的博客是很有必要的。&lt;/p&gt;
&lt;h3&gt;关于求职&lt;/h3&gt;
&lt;p&gt;大佬提醒我，&lt;strong&gt;要建立模板化、模块化的求职思路，如面试之前需要搜集相关的资料，主要关注以下四点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公司的名称&lt;/li&gt;
&lt;li&gt;公司的&lt;strong&gt;核心产品&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;公司产品的主要&lt;strong&gt;受众人群&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;公司招聘的&lt;strong&gt;岗位职责、经营状况、真实业务需求&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在新入职场的初期，可以考虑多往几个方向投简历。我希望&lt;strong&gt;未来 3-5 年从事 Go 语言的工程师岗位&lt;/strong&gt;，而在 Go 语言的发展方向中，以下四种是我最希望发展的：&lt;strong&gt;云中间件、微服务、高性能 API、游戏后端服务器开发&lt;/strong&gt;。而后再从大厂、中小厂、创业公司去搜集岗位信息以及岗位所对应的产品团队等，在以上四种方向中选公司投简历，这样才能了解公司需求，在掌握足够多信息的情况下拿到岗位的 offer。&lt;/p&gt;
&lt;h3&gt;关于自身的成长&lt;/h3&gt;
&lt;p&gt;能进公司，再对公司的业务进行钻研，是最能迅速提升的方式，前提是得先进去。那么进不去公司，业务如何进行迅速的提升呢？一般像我们通过自学提升，流程先是看视频 -&amp;gt; 看书 -&amp;gt; 实践。而看视频只是跟随别人的思路来写项目，虽然能快速浏览项目，但项目的设计、自己对项目的把握能力都不够，包括看书也如此。&lt;/p&gt;
&lt;p&gt;查看岗位的职责与需要掌握的基础技能：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、计算机或相关专业毕业，本科及以上学历，熟悉 TCP/IP、HTTP 等协议，有扎实的网络、存储、安全、计算机体系结构方面的知识，精通 Linux 操作系统，精通 C/C++/Python/Go 任意一门语言，熟悉 shell 脚本&lt;/p&gt;
&lt;p&gt;2、熟练掌握 MySQL 数据库，熟悉 MySQL 使用及基础优化，熟悉主流的非关系型数据库、缓存技术（Redis、Memcached 等）&lt;/p&gt;
&lt;p&gt;3、有监控、CI/CD、DevOps 相关工作经验者优先，有 Hadoop/Spark/Flink/TBase 实际项目使用经验者优先&lt;/p&gt;
&lt;p&gt;4、有责任心，良好的沟通协调能力及团队协作精神，善于学习、分析、思考&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么根据我自己而言，Go、Linux、网络、计算机体系结构相应的知识只需要每日多复习即可，自己的缺陷在数据库（MySQL、MongoDB、Redis）、监控、CI/CD、DevOps 上。可以从数据库开始实践，先熟悉一遍执行流程，再思考业务需要这些来做什么样的事情，达成了什么样的效果，效果能不能继续提升（调整优化等），之后再看看别人对数据库、DevOps、K8S 的思考，也就达到了暂时性的提升效果。至于阶段性的成长，就需要自己对自己所掌握的知识库进行总结与输出，比如写博客、做演讲等。&lt;/p&gt;
&lt;h3&gt;目前的任务&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;3 月 14 日 - 3 月 27 日，接下来两周的计划：先调研云中间件、游戏开发、微服务、高性能 API 公司的产品与经营状况，然后在每个分支方向选大厂、中小厂、创业公司分别投简历；能进入面试后，再根据面试得到的信息，决定具体的发展方向&lt;/li&gt;
&lt;li&gt;建立思维导图的时间轴：了解公司的业务背景 -&amp;gt; 投简历的时间 -&amp;gt; 一面的时间 -&amp;gt; 二面的时间 -&amp;gt; 三面的时间，对时间需要有一定的敏感程度&lt;/li&gt;
&lt;li&gt;首先要对自己定位：自己是缺乏工作经验的往届生，如果能通过简历筛选关，证明公司注重的是自己的个人修养。需要更好地展现自己曾经做过的事，扎实自己作为技术人员的基础&lt;/li&gt;
&lt;li&gt;个人表述要加强，面试的语速需要清晰、有节奏、谈吐有轻重。同时在回答相应专业问题时，需要像自己之前写 TCP/IP 协议一样，理清楚思路，总结成模板，形成自己的模板库&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第 12 - 13 周总结（2022-03-27）&lt;/h2&gt;
&lt;p&gt;上两周没写周总结。写周总结还是挺有效的，至少输入与输出能够理顺，将逻辑顺得十分清晰。&lt;/p&gt;
&lt;h3&gt;12 周&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、APISIX 网关的相关知识学习&lt;/p&gt;
&lt;p&gt;2、Redis 的面试概念学习&lt;/p&gt;
&lt;p&gt;3、sync 包中的 Pool（协程池）学习&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;令牌桶、漏桶算法的学习，包括 TDD 的了解、思考到现在的实践，都挺有收获的。&lt;/p&gt;
&lt;h3&gt;13 周&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、Docker 的复习&lt;/p&gt;
&lt;p&gt;2、极客时间（GeekTime）TDD 实践 + Learn TDD In Go 的学习&lt;/p&gt;
&lt;p&gt;3、算法题目练习以及 gin 框架的深入，学习 web 路由的路由匹配算法，以及学习极客兔兔的 7days 课程，小有收获，但是不成体系，所以暂时不放下这个课程&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;14 周&lt;/h3&gt;
&lt;p&gt;打算继续学习 TDD，进行 TDD 的实践。由于自己的毕业设计是做路面语义分割的图像算法模块的落地，也是复现国外的论文，并通过迁移学习模型的方式进行改进，但是无法解决过拟合的问题。导致当时处于一个比较尴尬的状态：算法学得不怎么样，开发也没学得太好。所以暂时的想法是继续维护更新大三做课程设计的 Go web 项目，把个人博客系统改为社区系统，然后加入新学习的知识：包括 TDD，做一些单元测试，并且想一想如何进行高并发的模拟场景，边学习边改。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;同时，算法刷题也不能落下，要继续高频算法的刷题，刷题的目的是为了维持自己解决问题的思维！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3 月 30 日&lt;/h3&gt;
&lt;p&gt;这几日在思考做项目的一些事情，比如自己还是在学习 go web，但是思来想去，觉得 go-web 项目不应该是现在这个阶段的重点。当时自己的阶段：没有真正的企业级项目实战的经历，也没有使用微服务的框架进行微服务开发的经历，而 web 项目的含金量并不高，而且 web 项目在企业工作中，所应用的场景也不是重点，所以重点现在应该放在云上。&lt;/p&gt;
&lt;p&gt;也是从大佬那，学习到了相应的微服务、云中间件的一些理念。结合自己的现状：项目的主要方向应该放入微服务那一块，所以在了解几个微服务框架后，开始对 go-zero 微服务进行相应的学习！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先从其中的例子开始学习，&lt;a href=&quot;https://github.com/zeromicro/zero-examples&quot;&gt;网址点击此处即可&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;之后研究源码的时候，采用如下的方法：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/go/readSourceCode.png&quot; alt=&quot;readSourceCode&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每日有必要去读读大佬的观点、博客，这样才能站在前辈、巨人的肩膀上去思考问题！&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第 14 周总结（2022-04-03）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;坚持做长期主义者，践行自己的观点，输入与输出都重要！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;1、上周写了一个 HTTP 的服务器接口，重新拾起大学学习的一些 HTTP 服务器的基础知识与一些 Golang 官方包！&lt;/p&gt;
&lt;p&gt;2、学习了 MySQL 的两种日志的具体区别：redo-log 与 bin-log，MySQL 的聚簇索引与非聚簇索引、主键索引与普通索引等，具体也请看 MySQL 的博客。&lt;/p&gt;
&lt;p&gt;3、学习 TDD，开始写第一个 TDD 的具体项目：命令行参数解析，具体请看 TDD 的那一篇博客！&lt;/p&gt;
&lt;h3&gt;下一周计划&lt;/h3&gt;
&lt;p&gt;本周的内容学习并不多，而且大部分时间，忘记自己在干什么事情了，好像干了一些事情，也好像没干，可能日更更能记录自己的日子。但是可以知道的是 Test-Driven-Development 自己的确很认可！&lt;/p&gt;
&lt;p&gt;1、程序员不仅要阅读项目、阅读文档，读书也是非常重要的。类似于&lt;strong&gt;代码整洁之道、架构设计之道、软件架构设计原则&lt;/strong&gt;等高维层次的书也需要阅读！电子书个人感觉方便，但是阅读的质量并未能达到纸质书的效果，而且自己在大学期间阅读的相应技术纸质书籍太少了！所以每年要多花点钱在自己的技术书籍上，多读一些经典的软件设计书籍。软件开发本身也是一个设计的过程，创造力的具体体现还是在经验上。&lt;/p&gt;
&lt;p&gt;2、开发过程中，自己很有可能遇到几个细小的知识点无法搞定。写代码不需要多久，调试 + 格式的整理是时间占比较大的部分！日后需要先进行伪代码与 test 文件的撰写，写完后才开始正式的代码开发。身为程序员，对代码本身也需要敬畏！&lt;/p&gt;
&lt;p&gt;3、Golang 官方的 package 包需要深入研究一下！第三方的包与库文件可以等在工作业务需要的时候去使用与研究。TDD 做好的同时，也需要进行实践驱动开发，具体的案例是先使用，再通过查看源代码的方式理解其中的设计思想与原理！包括架构设计、微服务、设计模式。&lt;/p&gt;
&lt;h2&gt;第 15 周总结（2022-04-10）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;慢慢积累，不能急&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;1、上周成果并不大，go-zero-look-look 部署成功，加上看起了&lt;strong&gt;重构&lt;/strong&gt;这本书，重构的理念与 TDD 的思想，的确都是软件设计的基本功。好好学好软件工程的思想，并应用到实践！&lt;/p&gt;
&lt;p&gt;2、SQL 又复习了一遍。go-zero-look-look 的部署坑点，主要在 MySQL 一块。基于目前的基本情况，不应该使用图形界面，先好好写 SQL 语句进行使用！&lt;/p&gt;
&lt;h3&gt;下一周计划&lt;/h3&gt;
&lt;p&gt;又继续回归到 go-web 本身，go-web 的一些第三方的组件需要进行熟悉！&lt;/p&gt;
&lt;p&gt;1、&lt;a href=&quot;https://github.com/spf13/viper&quot;&gt;viper 配置工具&lt;/a&gt;、&lt;a href=&quot;https://github.com/uber-go/zap&quot;&gt;zap 日志&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;2、go 语言操作 MySQL、Redis 的第三方包：&lt;a href=&quot;https://github.com/jmoiron/sqlx&quot;&gt;sqlx&lt;/a&gt;、&lt;a href=&quot;https://github.com/go-redis&quot;&gt;go-redis&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;3、徐昊老师使用 Java 进行 TDD 思想的传播。自己 Java 不熟悉了，但是想到自己身为程序员，需要快速学习知识，所以 Java 当成工具用，不喜欢它，但是也不能抗拒！&lt;/p&gt;
&lt;p&gt;4、每日一道算法题目，入大厂，还是需要写。&lt;/p&gt;
&lt;h2&gt;第 16 周总结（2022-04-17）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这几周主要学习微服务，顺便继续学习 Go web 使用的组件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;上周主要是学习了 twitter 公司的分布式 ID 生成的雪花算法以及微服务的一些概念，跟架构相关的一些知识：比如单体架构 =&amp;gt; 模块化架构 =&amp;gt; 微服务架构的演进、rpc 框架的技术选型考量、API 网关的划分、面向前端应用的后端服务等。&lt;/p&gt;
&lt;p&gt;举两个学习的小例子：&lt;/p&gt;
&lt;p&gt;1、API 两种设计模式：分为 &lt;code&gt;面向资源内网与面向业务场景&lt;/code&gt; 两种类型。&lt;/p&gt;
&lt;p&gt;2、MicroService 的划分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过业务职能进行划分 =&amp;gt; 分部门：如售前、售中、售后&lt;/li&gt;
&lt;li&gt;通过背景上下文进行拆分 =&amp;gt; DDD 的拆分方式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DDD 是领域驱动设计（Domain-Driven Design）的概念，大家都知道概念，但是实现落地都不是特别理想。&lt;/p&gt;
&lt;h3&gt;下一周计划&lt;/h3&gt;
&lt;p&gt;1、总结微服务的概念，整理成博客文集。微服务化需要很长的时间以及队伍的努力与领导的支持。&lt;/p&gt;
&lt;p&gt;2、继续学习 Go 语言微服务的具体模块，比如异常处理模块、日志模块等，建议可以参考极客时间的 Go 进阶训练营的计划！&lt;/p&gt;
&lt;p&gt;3、Go 的第三方的组件，以及准备学习一些其他的知识。70% 的时间放在学习日后与 Go 语言相关的知识，剩下的时间需要学习其他领域、其他行业的知识。永远需要有 plan B 计划，因为这个行业的确 35 岁被优化是事实，多手准备总是好的。&lt;/p&gt;
&lt;h3&gt;4 月 20 日摘抄&lt;/h3&gt;
&lt;p&gt;1、提升自己的&quot;免疫力&quot;：机体免疫力、心理免疫力、财务免疫力、价值免疫力，战胜困难，需要提升所有免疫力。&lt;/p&gt;
&lt;p&gt;2、明确的短期目标：学习圈层的划分一般为三种：舒适区、学习区、困难区。跳出自己的舒适区，走入学习区，这样行为不会太难，所以路得一步一步走。&lt;/p&gt;
&lt;p&gt;3、番茄时钟周期：使用 25 分钟工作学习、5 分钟休息的时间，这样时间使用起来会更加的高效。而且需要转变自己的学习方式，学多少不重要，知识的管理与输出非常重要。&lt;/p&gt;
&lt;p&gt;4、输出学习方法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、学习到什么&lt;/p&gt;
&lt;p&gt;2、有什么思考&lt;/p&gt;
&lt;p&gt;3、准备怎么做&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以思维导图化的形式。&lt;/p&gt;
&lt;h2&gt;第 17 周总结（2022-04-24）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本周效率有所上升&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;本周尝试使用全栈的方式去实践项目，前端使用 React 中 ant design pro，后端使用 java 三大组件，之后如果有需求，重构会选择 go。&lt;/p&gt;
&lt;p&gt;1、使用 ssm 框架进行开发的基本做法与 go 语言一致，对比两种语言：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 java 进行业务开发，比 go 开发效率快捷很多：包括依赖管理、使用注解运行等，go mod 管理包的方式，还是欠缺一些火候。&lt;/p&gt;
&lt;p&gt;就个人而言，还是喜欢使用 go 语言进行核心业务的开发，比如微服务的组件等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2、go 中的微服务模块大纲、异常处理两模块概念学习完毕，需要进行相应的实现，go-zero 与 kratos 在有了扎实的理论基础之后可以学习起来了！&lt;/p&gt;
&lt;h3&gt;下一周计划&lt;/h3&gt;
&lt;p&gt;1、微服务的概念整理，现在只做了一个大纲的内容，相应细节部分需要进一步的填充。&lt;/p&gt;
&lt;p&gt;2、Go 语言微服务模块：并行编程、go 工程化实践两个模块学习。&lt;/p&gt;
&lt;p&gt;3、设计模式学习理解。&lt;/p&gt;
&lt;h2&gt;第 18 - 19 周总结思考（2022-05-07）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;总结与思考，可能并非是单纯从技术的层面上进行思考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;18 周到 19 周，中间度过了五一，稍微休息了三四天，所以上一周的总结就没有及时的跟上计划。当然，我自己也不是一个完全按照计划执行的人，总会有一些本质上劣根性存在的。&lt;/p&gt;
&lt;h3&gt;18 周&lt;/h3&gt;
&lt;p&gt;在 18 周的时候，由于自己实现不了一个完整的项目，而感受到个人能力的不足。从大学以来，这样的痛苦就是恒常的，总是在追寻自己想做的事情，而有时候却忽略了自己的能力。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有些当时想要去完成的东西，却以自己暂时的能力达不到，又没办法进行良好的行为坚持，而以失败告终。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以自己暂时性的放弃了建立一个完整项目的念想。毕竟没有工作经验、实习经验，所谓的项目，不过是自己照着培训机构去模仿而已。好好打扎实基础、去开源项目上进行小功能的追加、bug 维护，可能更合适一些。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现在自己写博客坚持了大半年了，技术水平的提升也有所长进，这大半年也算小有成就。唯一不足的就是没能在企业中创造价值，属于自己的闭门造车罢了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以自己在后端算是学有余力的情况下，开启了前端的基础学习。自己虽然在大学时候也算了解过前端的一些知识，所以直接上手 vue 框架，这样确实不符合一般性的规律，在此还是&lt;strong&gt;不建议大家上手直接从框架开始，一步一个脚印走慢一些，对于自己的个人成长更好&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;像我的数据结构与算法以及两年的 408 学习，虽然考出的考研成绩不理想，但是也算这方面比较扎实的层面，当然也有一些漏洞，不过大纲还是没问题。这样两年的学习，带给自己专业基础上的提升也是显而易见的。&lt;/p&gt;
&lt;p&gt;总之，基础一定是王道。自己最快速的提升阶段，理应是算法的学习，所以初学者将算法学习扎实，是最重要的一个环节。&lt;/p&gt;
&lt;h3&gt;19 周&lt;/h3&gt;
&lt;p&gt;19 周主要是五月一日劳动节、青年节放假，所以自己休息了三天，期间主要是看 &lt;strong&gt;王德峰教授的&quot;中西方文化差异的源头&quot;，以及他对佛法的讲述&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;闲暇之余才能思考，人越忙的时间，想法就越纯粹：脑中只有把活干好。中国社会如王教授所说：从一个普遍物质不满足的时代，走入一个普遍精神不安宁的时代。中国人如何去理解自己的价值，如何找到自己安身立命的地方。&lt;/p&gt;
&lt;h3&gt;20 周计划&lt;/h3&gt;
&lt;p&gt;未来还是以自己的计划为主，多方面进行学习与提升，不仅只是技术方向上，沟通能力、人文社科、申论 + 行测。全方面的素质提升，对自己带来的效果，更加适合人作为人而存在。&lt;/p&gt;
&lt;h3&gt;关于写博客的反思&lt;/h3&gt;
&lt;p&gt;最近几周，对自己写博客的内容有所质疑。毕竟自己写博客的目的也是为了加深自己对知识的理解，技能的熟练。而看到自己以前的博客，质量与内容，可能初学的时候，还能勉勉强强的接受，到现在一看，排版不好、内容质量也不算高，维护更新就更不用说了。&lt;/p&gt;
&lt;p&gt;除了基础之外，一切东西都是需要即时进行更新的。就像 web 框架一样，一段时间没有使用，就可能 bug 满天飞。如果不及时进行维护，那么一方面用户会快速流失，另一方面也会导致失去对待项目代码质量的感知，技能就不再是技能，而是知识罢了。&lt;/p&gt;
&lt;p&gt;所以，最近几周不会再继续撰写新的博客，把 GMP、内存模型、垃圾回收、go 并发的内容再扩充扩充，对自己理解了但未能实现的功能即时的进行实现。&lt;/p&gt;
&lt;h2&gt;第 20 - 21 周总结思考（2022-05-21）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;两周总结一次，原因是因为周总结有一些不好写，自己又陷入了一个瓶颈期，所以需要将时间的跨度拉大一些，这样才好。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;20 ~ 21 周&lt;/h3&gt;
&lt;p&gt;20~21 周主要是在参与开源社区，帮 GoFrame 社区做 pgsql 驱动的单元测试，期间主要学习了以下内容：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、如何写单测（模仿 + 逻辑改写）&lt;/p&gt;
&lt;p&gt;2、如何参与开源项目以及如何做 code review&lt;/p&gt;
&lt;p&gt;3、pgsql 语法学习，以及做数据库的 sql 测试（就是执行一个创建表、退出 test 协程时候删除表的逻辑）&lt;/p&gt;
&lt;p&gt;4、github workflow ci/cd 流程的学习&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大致如以上几点所示，反思总结是学习与提升很大，尤其是在 code review 上，可以通过学习别人提交的 commit 对比，然后与别人交流设计的思想，这样人的能力能得到一个快速的提升。&lt;/p&gt;
&lt;p&gt;但是缺点也很明显，知识没有得到一个系统性的提升，只是在工作中学习碎片化的知识。应该是使用碎片化的时间进行系统性的学习，比如看书、看视频等。看博客、写博客也只是一种零散性的学习而已。&lt;/p&gt;
&lt;h3&gt;22 ~ 23 计划&lt;/h3&gt;
&lt;p&gt;接下来六个月，需要自己在 mysql、redis、kafka 这样的中间件上，进行相应的学习与提升修养，所以需要进行一个系统化的提升。主要准备看 mysql 与 redis 的书、专栏进行系统化的学习，当然也会进行一个实战的提升。&lt;/p&gt;
&lt;p&gt;还有即是整洁之道三部曲：代码整洁之道、架构整洁之道、匠艺整洁之道三者。&lt;/p&gt;
&lt;h2&gt;第 22 - 23 周总结思考（2022-06-05）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这几周做了身体检查，主要以调整作息为主&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;22 ~ 23 周&lt;/h3&gt;
&lt;p&gt;这两周，给自己的任务安排得非常轻，所以去医院做了一些检查，检查结果良好。但是医生建议为了避免以后的恶化，需要多多更改作息，比如早睡早起、每日半小时以上的跑步、游泳等体育锻炼。&lt;/p&gt;
&lt;p&gt;想一想，考研以后，自己已经有接近两年的时间没有进行游泳的体育锻炼了。小县城的游泳馆已经倒闭了，毕竟有这样的需求的乡村人还是挺少的。&lt;/p&gt;
&lt;p&gt;除开这些之外，主要还是就 Redis 的技术进行研究，毕竟之前也没有系统性学习过，只是通过相应的文档进行开发驱动的使用罢了。&lt;/p&gt;
&lt;h3&gt;24 ~ 25 周&lt;/h3&gt;
&lt;p&gt;主要还是进行 Redis + MySQL 的理解，包括相应的设计与实现。其实并不能说明，学到的东西能发挥作用，学习的知识也是为了未来进行储备的。一直持续到今年年底的 6 个月，对这两门技术进行底层化的探索。&lt;/p&gt;
&lt;p&gt;下个 6 个月的大周期，主要给 kafka、消息队列、devops 的一些组件（监控、K8S）等运维化的工具研究。&lt;/p&gt;
</content:encoded></item><item><title>StarRocks 原理、部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/11/starrocks-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/11/starrocks-deploy-tuning/</guid><pubDate>Mon, 10 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;StarRocks 是从 Doris 分叉出的 MPP 分析引擎，在&quot;高并发点查、实时更新、物化视图、存算分离&quot;上走得更远。本文覆盖底层原理、集群部署（FE/BE）、存算分离形态、参数优化与常见问题。与《Doris 原理、部署与调优指南》对照阅读，差异点会标出来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理速览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FE / BE 架构&lt;/strong&gt;：与 Doris 同源（FE 元数据与查询规划，BE 存储与执行），但查询优化器是独立的 CBO（Cost-Based Optimizer，基于代价），统计信息驱动 Join 顺序选择&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;四种表模型&lt;/strong&gt;：明细（Duplicate）、聚合（Aggregate）、更新（Unique/主键表）、主键（Primary Key）——主键表支持真正的行级实时更新（Delete+Insert）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pipeline 执行引擎&lt;/strong&gt;：算子流水线化，消除执行引擎线程调度开销，多核利用率更高&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步/异步物化视图&lt;/strong&gt;：单表同步 MV 自动透明改写；异步 MV 支持多表 Join/聚合，定时刷新&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存算分离（Share-Nothing → Share-Data）&lt;/strong&gt;：数据放对象存储，BE 只做缓存与计算，集群可独立弹性伸缩&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Short Key Index / ZoneMap&lt;/strong&gt;：建表时可选前 3 列做稀疏索引；每列统计信息在扫描时裁剪数据块&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
StarRocks 与 Doris 的关系：同源于百度 Doris，StarRocks 侧重&lt;strong&gt;性能与实时更新&lt;/strong&gt;（主键表、CBO、Pipeline），Doris 侧重&lt;strong&gt;生态与易用性&lt;/strong&gt;（Routine Load、物化视图、文档生态）。选型看场景：超高并发点查与实时更新选 StarRocks，求稳与生态全选 Doris。
:::&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;h3&gt;1. 规划&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;节点&lt;/th&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;数量&lt;/th&gt;
&lt;th&gt;规格参考（生产）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FE&lt;/td&gt;
&lt;td&gt;Leader + Follower（可加 Observer）&lt;/td&gt;
&lt;td&gt;1+2&lt;/td&gt;
&lt;td&gt;16C64G，SSD 元数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BE&lt;/td&gt;
&lt;td&gt;存储与计算&lt;/td&gt;
&lt;td&gt;3+&lt;/td&gt;
&lt;td&gt;32C256G&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;端口：FE 8030（Web）/ 9030（MySQL）；BE 9060（BE 通信）/ 9050（心跳）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;priority_networks&lt;/code&gt; 必须显式指定；FE 与 BE 时钟同步（ntp）；&lt;code&gt;sysctl vm.max_map_count=262144&lt;/code&gt; 且 &lt;code&gt;sysctl -w vm.swappiness=0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. FE 部署&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wget https://releases.starrocks.io/starrocks/StarRocks-3.2.x-x86_64.tar.gz
tar -zxvf StarRocks-*.tar.gz -C /opt &amp;amp;&amp;amp; ln -s /opt/StarRocks-* /opt/starrocks
mkdir -p /data/starrocks/meta &amp;amp;&amp;amp; chown -R starrocks:starrocks /opt/starrocks /data/starrocks
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# fe/conf/fe.conf
meta_dir = /data/starrocks/meta
priority_networks = 10.0.0.0/24
JAVA_OPTS=&quot;-Xmx16g -Xms16g -Xmn4g&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 第一台初始化
fe/bin/start_fe.sh --daemon
mysql -h127.0.0.1 -P9030 -uroot

# 后续 FE 加入
mysql&amp;gt; ALTER SYSTEM ADD FOLLOWER &quot;node2:9010&quot;;
mysql&amp;gt; ALTER SYSTEM ADD FOLLOWER &quot;node3:9010&quot;;
# 每台：start_fe.sh --helper node1:9010 --daemon
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. BE 部署&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# be/conf/be.conf
storage_root_path = /data/starrocks/data1,medium:ssd;/data/starrocks/data2,medium:hdd
priority_networks = 10.0.0.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;be/bin/start_be.sh --daemon
mysql&amp;gt; ALTER SYSTEM ADD BACKEND &quot;node1:9050&quot;;
mysql&amp;gt; ALTER SYSTEM ADD BACKEND &quot;node2:9050&quot;;
mysql&amp;gt; ALTER SYSTEM ADD BACKEND &quot;node3:9050&quot;;
mysql&amp;gt; SHOW BACKENDS;   -- 全部 Alive 即就绪
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 建表验证（主键表 + 物化视图）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE orders (
  order_id  BIGINT,
  user_id   BIGINT,
  status    VARCHAR(16),
  amount    DECIMAL(20,2),
  modify_ts DATETIME
)
PRIMARY KEY (order_id)                          -- 主键表：实时更新
DISTRIBUTED BY HASH(order_id) BUCKETS 24;

-- 异步物化视图：多表聚合定时刷新
CREATE MATERIALIZED VIEW mv_user_daily
REFRESH ASYNC START(&apos;00:00&apos;) EVERY(INTERVAL 1 DAY)
AS SELECT user_id, DATE(create_time) d, SUM(amount) total
   FROM orders GROUP BY user_id, DATE(create_time);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 存算分离形态（Share-Data，可选）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 部署 CN（Compute Node）替代/并存 BE，数据在对象存储
# fe.conf 增加
enable_cloud_snapshot = true
# 创建存储卷（示例：S3 兼容存储）
CREATE STORAGE VOLUME s3vol TYPE S3
  LOCATIONS (&quot;s3://bucket/prefix&quot;)
  PROPERTIES (&quot;aws.s3.endpoint&quot;=&quot;...&quot;, &quot;aws.s3.access_key&quot;=&quot;...&quot;, &quot;aws.s3.secret_key&quot;=&quot;...&quot;);

# 建表时指定 use_storage_volume
CREATE TABLE t (...) DISTRIBUTED BY HASH(k) BUCKETS 16
  PROPERTIES (&quot;storage_volume&quot;=&quot;s3vol&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
存算分离适合&quot;弹性伸缩、多集群共享一份数据&quot;的场景，但点查延迟依赖本地缓存命中率（热数据命中才快）。评估不了缓存命中率前，不建议把核心在线场景直接迁到 Share-Data。
:::&lt;/p&gt;
&lt;h2&gt;调优&lt;/h2&gt;
&lt;h3&gt;1. 查询优化器&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SET enable_cbo=true&lt;/code&gt;（默认开启）；关键表执行 &lt;code&gt;ANALYZE TABLE xxx&lt;/code&gt; 收集统计信息，CBO 才有依据&lt;/li&gt;
&lt;li&gt;大宽表关掉无关列的统计；Join 用小表驱动（&lt;code&gt;SET join_implementation_mode=&quot;auto&quot;&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;Pipeline：&lt;code&gt;SET pipeline_dop=0&lt;/code&gt;（按机器核数自适应），高并发场景可显式设 4-8，避免线程数爆炸&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 内存&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BE &lt;code&gt;mem_limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60%-70% 物理内存&lt;/td&gt;
&lt;td&gt;单 BE 上限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;会话 &lt;code&gt;query_mem_limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;大查询单独给&lt;/td&gt;
&lt;td&gt;防止 OOM，配合 &lt;code&gt;EXPLAIN&lt;/code&gt; 看预算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query_max_memory_limit_percent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;单查询占 BE 内存上限&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3. 主键表与实时更新&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;主键表写入是&quot;内存主键索引 + 写入时与旧数据合并&quot;，&lt;strong&gt;高频小批次更新&lt;/strong&gt;最合适（如 1 分钟级增量）&lt;/li&gt;
&lt;li&gt;批量导入用 &lt;code&gt;INSERT INTO ... VALUES&lt;/code&gt; 分批，或 Broker Load；更新量大时增大分桶数（主键索引内存占用也增大）&lt;/li&gt;
&lt;li&gt;主键表不适用全表重写型大任务（如每天全量重灌），此时考虑明细表或异步物化视图&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 物化视图&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;同步 MV：只服务单表聚合，命中即透明改写，适合高频点查计数&lt;/li&gt;
&lt;li&gt;异步 MV：刷新策略 &lt;code&gt;REFRESH ASYNC START(&apos;00:00&apos;) EVERY(INTERVAL 1 DAY)&lt;/code&gt;，任务多时调大 &lt;code&gt;mv_task_run_num&lt;/code&gt;（默认 4）&lt;/li&gt;
&lt;li&gt;查询是否能命中 MV：&lt;code&gt;EXPLAIN SELECT ...&lt;/code&gt; 看是否出现 &lt;code&gt;MV_&lt;/code&gt; 前缀节点&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. Colocate Join 与缓存&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Colocate Join&lt;/strong&gt;：多表同分桶键 + &lt;code&gt;colocate_with=&quot;g1&quot;&lt;/code&gt;，Join 时数据本地化，免 shuffle，高并发 Join 收益最大&lt;/li&gt;
&lt;li&gt;BE 块缓存 &lt;code&gt;storage_page_cache_limit&lt;/code&gt;（默认 20% 内存）；热数据表可用 &lt;code&gt;CREATE TABLE ... PROPERTIES (&quot;enable_persistent_index&quot;=&quot;true&quot;)&lt;/code&gt; 提升点查&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;点查延迟高&lt;/td&gt;
&lt;td&gt;未命中块缓存/主键索引未加载&lt;/td&gt;
&lt;td&gt;查 &lt;code&gt;SHOW BE&lt;/code&gt; 缓存命中率，调 storage_page_cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主键表导入慢&lt;/td&gt;
&lt;td&gt;主键索引内存不足换磁盘&lt;/td&gt;
&lt;td&gt;调大 BE mem_limit 或减少桶数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;物化视图不刷新&lt;/td&gt;
&lt;td&gt;mv_task 并发/超时&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SHOW MATERIALIZED VIEWS&lt;/code&gt; 看 State，调 mv_task_run_num&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存算分离查询慢&lt;/td&gt;
&lt;td&gt;缓存冷&lt;/td&gt;
&lt;td&gt;预热热表、调大磁盘缓存 &lt;code&gt;datacache.*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;磁盘水位不均&lt;/td&gt;
&lt;td&gt;新 BE 数据未均衡&lt;/td&gt;
&lt;td&gt;等待或 &lt;code&gt;ALTER SYSTEM ...&lt;/code&gt; 检查 &lt;code&gt;SHOW BACKENDS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;StarRocks = 同源 Doris 的&quot;性能特化&quot;分支：CBO + Pipeline + 主键表 + 同步/异步物化视图&lt;/li&gt;
&lt;li&gt;部署与 Doris 同构（FE 集群 + BE 集群），新增存算分离形态（对象存储 + CN）&lt;/li&gt;
&lt;li&gt;调优主线：统计信息与 CBO → Pipeline 并行度 → 主键表写入模式 → 物化视图 → Colocate Join&lt;/li&gt;
&lt;li&gt;选型结论：高并发点查、实时更新场景优先 StarRocks；Doris/StarRocks 两者是湖仓时代 OLAP 的左右手，底座迭代时二选一即可，不建议同时养两套&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>大数据与中间件组件部署总览</title><link>https://chaggle.github.io/posts/2026/08/10/bigdata-middleware-deploy-overview/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/bigdata-middleware-deploy-overview/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文汇总 HDFS、YARN、Hive、Spark、Flink、Zookeeper、Kafka、MySQL、Redis、Nacos、RocketMQ 共 11 个组件的安装部署文档，统一了目录导航，便于对照查阅。各组件更深入的&lt;strong&gt;生产调优&lt;/strong&gt;（8C16G / 32C256G / 64C512G 三档规格参数、集群规模优化、容灾备份）见对应组件各自的《XXX 生产调优实践》文章；K8S 环境下的部署见《Kubernetes 底层原理》《Kubernetes 常见问题排查》《Kubernetes 部署参数优化》三篇。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;第一部分：大数据组件（HDFS、YARN、Hive、Spark、Flink）&lt;/li&gt;
&lt;li&gt;第二部分：中间件组件（Zookeeper、Kafka、MySQL、Redis、Nacos、RocketMQ）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;第一部分：大数据组件&lt;/h1&gt;
&lt;h2&gt;HDFS（分布式文件系统）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;HDFS 是 Hadoop 的分布式文件系统，安装部署的核心是搞清楚三个角色的分工：NameNode（元数据）、DataNode（数据）、SecondaryNameNode（辅助检查点）。本文以 Hadoop 3.x 为例，记录从下载解压到启动验证的完整过程，单节点起步，扩展为集群只需加节点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;要求&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;操作系统&lt;/td&gt;
&lt;td&gt;Linux（CentOS 7+/Ubuntu 均可用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JDK&lt;/td&gt;
&lt;td&gt;1.8 或 11（Hadoop 3.x 要求）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;免密登录&lt;/td&gt;
&lt;td&gt;主节点到所有 DataNode 节点 SSH 免密&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;时间同步&lt;/td&gt;
&lt;td&gt;集群节点统一时间（NTP/chrony）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hosts 解析&lt;/td&gt;
&lt;td&gt;所有节点 hostname 与 IP 写入 &lt;code&gt;/etc/hosts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;# 免密配置（主节点执行）
ssh-keygen -t rsa
ssh-copy-id user@node1
ssh-copy-id user@node2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
HDFS 单点起步时免密不是必须的，但集群模式（主节点用 ssh 拉起从节点进程）必须配置。
:::&lt;/p&gt;
&lt;h3&gt;下载与解压&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 下载 Hadoop 3.x 二进制包（以 3.3.6 为例，建议从 Apache 镜像站获取）
wget https://dlcdn.apache.org/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz

# 统一解压到 /opt 并建立软链
tar -zxvf hadoop-3.3.6.tar.gz -C /opt
ln -s /opt/hadoop-3.3.6 /opt/hadoop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置环境变量（&lt;code&gt;/etc/profile&lt;/code&gt; 或 &lt;code&gt;~/.bashrc&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export JAVA_HOME=/usr/local/jdk1.8.0_xxx
export HADOOP_HOME=/opt/hadoop
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
source /etc/profile
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;核心配置&lt;/h3&gt;
&lt;p&gt;配置文件都在 &lt;code&gt;$HADOOP_HOME/etc/hadoop/&lt;/code&gt; 下，集群模式需要同步到所有节点。&lt;/p&gt;
&lt;h4&gt;1. &lt;code&gt;core-site.xml&lt;/code&gt;：文件系统入口&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;fs.defaultFS&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;hdfs://node1:8020&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;hadoop.tmp.dir&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;/data/hadoop/tmp&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. &lt;code&gt;hdfs-site.xml&lt;/code&gt;：存储目录与副本&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
  &amp;lt;!-- NameNode 元数据目录（建议配多目录，不同磁盘） --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;dfs.namenode.name.dir&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;/data/hadoop/namenode&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;!-- DataNode 数据目录（多块盘逗号分隔） --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;dfs.datanode.data.dir&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;/data/hadoop/datanode&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;!-- 副本数：3 节点集群配 2 或 3 --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;dfs.replication&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;2&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;strong&gt;生产铁律&lt;/strong&gt;：&lt;code&gt;dfs.namenode.name.dir&lt;/code&gt; 配多个目录并放在不同磁盘（甚至两台机器），元数据是 HDFS 的命根子，目录损坏等于文件系统报废。
:::&lt;/p&gt;
&lt;h4&gt;3. &lt;code&gt;workers&lt;/code&gt; 文件：DataNode 清单&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 每行一个 DataNode 主机名
node1
node2
node3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 环境配置 &lt;code&gt;hadoop-env.sh&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;export JAVA_HOME=/usr/local/jdk1.8.0_xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;格式化与启动&lt;/h3&gt;
&lt;p&gt;:::caution
&lt;strong&gt;NameNode 只能格式化一次&lt;/strong&gt;（单节点首次部署时）。格式化会清空元数据目录，重复格式化会导致集群 DataNode 上报的集群 ID 不一致而启动失败。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 创建数据目录
mkdir -p /data/hadoop/{namenode,datanode,tmp}

# 2. 首次部署时格式化 NameNode
hdfs namenode -format

# 3. 启动 HDFS（主节点执行，自动 ssh 到 workers 拉起 DataNode）
start-dfs.sh

# 4. 检查进程
jps
# 主节点应有：NameNode、SecondaryNameNode
# 从节点应有：DataNode
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看集群状态：Live nodes 数量、块情况
hdfs dfsadmin -report

# 基本读写测试
hdfs dfs -mkdir -p /test
echo &quot;hello hdfs&quot; &amp;gt; /tmp/test.txt
hdfs dfs -put /tmp/test.txt /test/
hdfs dfs -cat /test/test.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Web UI：&lt;code&gt;http://&amp;lt;NameNode节点&amp;gt;:9870&lt;/code&gt;（Hadoop 3.x；2.x 为 50070），可查看节点状态、文件系统浏览、NameNode 日志。&lt;/p&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DataNode 一直处于 Decommissioned/Down&lt;/strong&gt;：检查 &lt;code&gt;dfs.datanode.data.dir&lt;/code&gt; 目录权限（属主应为运行用户）、磁盘空间、节点间 hostname 解析&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集群 ID 不一致&lt;/strong&gt;（&lt;code&gt;java.io.IOException: Incompatible clusterIDs&lt;/code&gt;）：多为重复格式化或从备份恢复元数据导致，需要比对 &lt;code&gt;VERSION&lt;/code&gt; 文件中的 clusterID 或清理 DataNode 数据目录重新启动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;块副本不足&lt;/strong&gt;：&lt;code&gt;dfs.replication&lt;/code&gt; 与存活 DataNode 数不匹配，或部分节点掉线；&lt;code&gt;hdfs fsck / -files -blocks&lt;/code&gt; 排查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全模式（Safe mode）&lt;/strong&gt;：刚启动短暂处于安全模式属正常，长时间不退出检查 &lt;code&gt;hdfs dfsadmin -safemode get&lt;/code&gt; 与数据目录空间&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;扩展为集群&lt;/h3&gt;
&lt;p&gt;单节点验证通过后，把 &lt;code&gt;hdfs-site.xml&lt;/code&gt;、&lt;code&gt;core-site.xml&lt;/code&gt;、&lt;code&gt;workers&lt;/code&gt; 同步到其他节点，在各节点准备相同路径的 &lt;code&gt;dfs.datanode.data.dir&lt;/code&gt; 目录，重启 DataNode 即可加入集群。HA（双 NameNode + JournalNode）属于进阶部署，后续单独写文档。&lt;/p&gt;
&lt;h2&gt;YARN（资源调度）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;YARN 是 Hadoop 的资源调度框架，负责给 MapReduce、Spark、Flink 等作业分配 CPU 与内存。它的安装包和 HDFS 是同一个 Hadoop 发行版，只需额外配置两个文件：&lt;code&gt;mapred-site.xml&lt;/code&gt;（把计算框架指向 YARN）和 &lt;code&gt;yarn-site.xml&lt;/code&gt;（定义 ResourceManager 与资源参数）。本文接 HDFS 安装文档，记录 YARN 的配置、启动与验证。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;角色说明&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;th&gt;部署位置&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ResourceManager（RM）&lt;/td&gt;
&lt;td&gt;全局资源调度，分配 Container&lt;/td&gt;
&lt;td&gt;主节点（可 HA 两台）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NodeManager（NM）&lt;/td&gt;
&lt;td&gt;单机资源管理与任务执行&lt;/td&gt;
&lt;td&gt;每个计算节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ApplicationMaster（AM）&lt;/td&gt;
&lt;td&gt;每个应用一个，向 RM 申请资源&lt;/td&gt;
&lt;td&gt;由 NM 拉起，无需手动部署&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TimelineServer / JobHistoryServer&lt;/td&gt;
&lt;td&gt;作业历史记录&lt;/td&gt;
&lt;td&gt;主节点（可选）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;配置&lt;/h3&gt;
&lt;p&gt;沿用 HDFS 部署时的 Hadoop 安装目录，在 &lt;code&gt;$HADOOP_HOME/etc/hadoop/&lt;/code&gt; 下配置。&lt;/p&gt;
&lt;h4&gt;1. &lt;code&gt;mapred-site.xml&lt;/code&gt;：计算框架对接 YARN&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;mapreduce.framework.name&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;yarn&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;mapreduce.jobhistory.address&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;node1:10020&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. &lt;code&gt;yarn-site.xml&lt;/code&gt;：资源调度与单机资源&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
  &amp;lt;!-- ResourceManager 地址 --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;yarn.resourcemanager.hostname&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;node1&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;!-- 调度器：容量调度器 --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;yarn.resourcemanager.scheduler.class&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;!-- 单节点可分配内存与核数（按机器实际配置调整） --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;yarn.nodemanager.resource.memory-mb&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;8192&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;yarn.nodemanager.resource.cpu-vcores&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;4&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;!-- 日志聚合：作业日志汇总到 HDFS --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;yarn.log-aggregation-enable&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;true&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt; 是 NodeManager 可分配给容器（Container）的内存总量，&lt;strong&gt;必须给操作系统留出余量&lt;/strong&gt;。8GB 内存的机器建议配 6~7GB，配满会导致操作系统内存耗尽被 OOM 杀掉。
:::&lt;/p&gt;
&lt;h4&gt;3. &lt;code&gt;yarn-env.sh&lt;/code&gt; 补充 JDK&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;export JAVA_HOME=/usr/local/jdk1.8.0_xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动与验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 启动 YARN（主节点执行）
start-yarn.sh

# 检查进程
jps
# 主节点：ResourceManager
# 从节点：NodeManager

# 查看节点资源
yarn node -list
# 输出应显示每个 NodeManager 的 IP 与可用资源
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Web UI：&lt;code&gt;http://&amp;lt;ResourceManager节点&amp;gt;:8088&lt;/code&gt;。集群概览页能看到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Active Nodes 数量&lt;/li&gt;
&lt;li&gt;各节点内存/核数（与 &lt;code&gt;yarn.nodemanager.resource.*&lt;/code&gt; 配置对应）&lt;/li&gt;
&lt;li&gt;正在运行与已完成的 Application&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;跑一个 MR 作业验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 官方示例：统计 /test 目录下单词（先确认 HDFS 上已有文件）
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.6.jar \
  wordcount /test /test-output

# 查看结果
hdfs dfs -cat /test-output/part-r-00000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作业提交后在 8088 页面能看到 Application 从 ACCEPTED → RUNNING → SUCCEEDED 的变化过程。&lt;/p&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ResourceManager 无法启动&lt;/strong&gt;：检查 8088 端口占用、&lt;code&gt;yarn.resourcemanager.hostname&lt;/code&gt; 是否被主机名解析、&lt;code&gt;yarn-site.xml&lt;/code&gt; 是否有残留配置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作业一直 ACCEPTED 不运行&lt;/strong&gt;：NodeManager 上报资源为 0（内存配置异常）、或集群总资源被其他作业占满&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Container 启动即失败（Exited with exit code 127/1）&lt;/strong&gt;：节点上 JAVA_HOME 配置错误、或 &lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt; 过小导致容器分配不足&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志看不到&lt;/strong&gt;：&lt;code&gt;yarn.log-aggregation-enable&lt;/code&gt; 未开启时作业日志留在 NodeManager 本地，开启后可通过 8088 页面 &lt;code&gt;Logs&lt;/code&gt; 入口查看&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核数设置大于物理核数&lt;/strong&gt;：会导致超卖，任务互相抢占 CPU，一般按物理核数配置&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;扩展：ResourceManager HA（可选）&lt;/h3&gt;
&lt;p&gt;两个 ResourceManager 节点 + Zookeeper 实现故障自动切换，配置 &lt;code&gt;yarn.resourcemanager.ha.enabled=true&lt;/code&gt;、&lt;code&gt;yarn.resourcemanager.zk-address&lt;/code&gt; 后重启生效。单节点起步阶段可暂缓。&lt;/p&gt;
&lt;h2&gt;Hive（数仓 SQL 引擎）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Hive 本身不存储数据、不执行计算，它把 SQL 翻译成 MapReduce/Tez/Spark 作业丢给 YARN 跑，表结构（元数据）存在数据库里。安装 Hive 的核心就是两件事：&lt;strong&gt;把元数据从默认的 Derby 换成 MySQL&lt;/strong&gt;（生产必须），以及处理它与 Hadoop 的版本兼容。本文以 Hive 3.1.3 + Hadoop 3.x 为例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;安装前必须理解的架构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HiveServer2（提供 JDBC 服务）
   │
   ├── 元数据 → MySQL（Metastore）
   └── 计算引擎 → YARN（MapReduce / Tez / Spark）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
Hive 自带的内嵌 Derby 元数据库只适合本地演示：同一时间只允许一个会话连接，并发一多就锁表。&lt;strong&gt;生产环境必须外置 MySQL 作为 Metastore&lt;/strong&gt;。
:::&lt;/p&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hadoop 集群已部署（HDFS + YARN）&lt;/li&gt;
&lt;li&gt;JDK 1.8&lt;/li&gt;
&lt;li&gt;MySQL 5.7/8.0 可用（提前建好库与账号）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;下载与解压&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wget https://dlcdn.apache.org/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz
tar -zxvf apache-hive-3.1.3-bin.tar.gz -C /opt
ln -s /opt/apache-hive-3.1.3-bin /opt/hive

# 环境变量
export HIVE_HOME=/opt/hive
export PATH=$PATH:$HIVE_HOME/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置 Metastore（MySQL）&lt;/h3&gt;
&lt;h4&gt;1. 准备 MySQL 库&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;CREATE DATABASE IF NOT EXISTS hive DEFAULT CHARACTER SET utf8;
GRANT ALL PRIVILEGES ON hive.* TO &apos;hive&apos;@&apos;%&apos; IDENTIFIED BY &apos;hive123&apos;;
FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 添加 MySQL 驱动&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 将 mysql-connector-java 的 jar（如 mysql-connector-j-8.0.33.jar）放到 lib 目录
cp mysql-connector-j-8.0.33.jar $HIVE_HOME/lib/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 修改 &lt;code&gt;conf/hive-site.xml&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
  &amp;lt;!-- 数据库连接 --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;javax.jdo.option.ConnectionURL&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;jdbc:mysql://localhost:3306/hive?useSSL=false&amp;amp;amp;characterEncoding=UTF-8&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;javax.jdo.option.ConnectionDriverName&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;com.mysql.cj.jdbc.Driver&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;javax.jdo.option.ConnectionUserName&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;hive&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;javax.jdo.option.ConnectionPassword&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;hive123&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;!-- HiveServer2 服务地址（供 beeline/客户端连接） --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;hive.server2.thrift.bind.host&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;node1&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;hive.server2.thrift.port&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;10000&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;!-- 执行引擎：MR / tez / spark 三选一 --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;hive.execution.engine&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;mr&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Hive 元数据会存放文件在 HDFS 的 &lt;code&gt;/user/hive/warehouse&lt;/code&gt;（默认建表目录），启动前确认 HDFS 上有对应目录及写权限，或在配置中指定 &lt;code&gt;hive.metastore.warehouse.dir&lt;/code&gt;。
:::&lt;/p&gt;
&lt;h3&gt;初始化元数据库&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 初始化 schema（首次部署执行一次，向 MySQL 建 60+ 张元数据表）
schematool -initSchema -dbType mysql
# 看到 &quot;schemaTool completed&quot; 即成功
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动与验证&lt;/h3&gt;
&lt;h4&gt;方式一：本地 CLI（快速验证）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;hive
# 执行验证
show databases;
create table t_test(id int, name string);
insert into t_test values (1, &apos;hello&apos;);
select * from t_test;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;方式二：独立 Metastore + HiveServer2（生产模式）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 后台启动元数据服务（共享元数据、允许多客户端）
nohup hive --service metastore &amp;gt; /opt/hive/logs/metastore.log 2&amp;gt;&amp;amp;1 &amp;amp;

# 启动 HiveServer2（JDBC 服务）
nohup hive --service hiveserver2 &amp;gt; /opt/hive/logs/hiveserver2.log 2&amp;gt;&amp;amp;1 &amp;amp;

# beeline 客户端连接验证
beeline -u jdbc:hive2://node1:10000 -n root
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Could not resolve org.apache.hadoop:hadoop-client&lt;/code&gt; / guava 冲突&lt;/strong&gt;：Hive 与 Hadoop 的 guava jar 版本不一致，删除 Hive lib 下的低版本 guava，替换为 Hadoop lib 下的版本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化报 &lt;code&gt;Access denied for user&lt;/code&gt;&lt;/strong&gt;：MySQL 账号权限未授权 &lt;code&gt;hive.*&lt;/code&gt;，或连接串中的 &lt;code&gt;useSSL=false&lt;/code&gt; 未生效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HiveServer2 启动后连不上&lt;/strong&gt;：检查 10000 端口是否监听（&lt;code&gt;ss -lntp | grep 10000&lt;/code&gt;）、&lt;code&gt;hive.server2.thrift.bind.host&lt;/code&gt; 是否被正确解析&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;insert 很慢&lt;/strong&gt;：MR 引擎作业冷启动开销大，可用 &lt;code&gt;set hive.execution.engine=tez;&lt;/code&gt; 切换（需安装 Tez）；表数据量大时优先考虑分区表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;warehouse&lt;/code&gt; 目录权限不足&lt;/strong&gt;：&lt;code&gt;hdfs dfs -chmod -R 755 /user/hive/warehouse&lt;/code&gt; 或直接 &lt;code&gt;hdfs dfs -chown -R hive:hive /user/hive&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产将 Metastore 独立部署（可多实例），HiveServer2 前挂 Kyuubi/LB 供多业务共用&lt;/li&gt;
&lt;li&gt;建表规范：分区表 + 合理的文件格式（ORC/Parquet），避免小文件堆积&lt;/li&gt;
&lt;li&gt;计算引擎：数据量大建议 Tez/Spark，MR 作为兜底&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Spark（内存计算引擎）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Spark 是内存计算引擎，安装方式按资源管理模式分三种：Local（单机调试）、Standalone（Spark 自带集群）、YARN（复用 Hadoop 资源池）。生产环境几乎都是 &lt;strong&gt;on YARN&lt;/strong&gt;，但部署上反而是最简单的——只需要客户端 + 配置，不用起任何常驻进程。本文三种模式都讲清楚，重点落在 YARN 模式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;部署模式对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;部署动作&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;解压即用&lt;/td&gt;
&lt;td&gt;开发调试、学习&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone&lt;/td&gt;
&lt;td&gt;启动 Master + Worker 常驻进程&lt;/td&gt;
&lt;td&gt;无 Hadoop 的小集群&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;on YARN&lt;/td&gt;
&lt;td&gt;只需客户端，作业提交后由 YARN 调度&lt;/td&gt;
&lt;td&gt;生产（与 Hadoop 共用资源）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK 1.8/11&lt;/li&gt;
&lt;li&gt;Hadoop 3.x（on YARN 模式需要，且 &lt;code&gt;YARN_CONF_DIR&lt;/code&gt; 指向有效配置）&lt;/li&gt;
&lt;li&gt;Scala 无需单独安装（发行包自带）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;下载与解压&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 选择与 Hadoop 大版本匹配的预编译包（hadoop3 版）
wget https://dlcdn.apache.org/spark/spark-3.5.1/spark-3.5.1-bin-hadoop3.tgz
tar -zxvf spark-3.5.1-bin-hadoop3.tgz -C /opt
ln -s /opt/spark-3.5.1-bin-hadoop3 /opt/spark

export SPARK_HOME=/opt/spark
export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置&lt;/h3&gt;
&lt;p&gt;配置目录 &lt;code&gt;$SPARK_HOME/conf/&lt;/code&gt;，官方自带模板，复制后修改。&lt;/p&gt;
&lt;h4&gt;1. &lt;code&gt;spark-env.sh&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;export JAVA_HOME=/usr/local/jdk1.8.0_xxx
# on YARN 模式：让 YARN 知道 Hadoop 配置位置
export HADOOP_CONF_DIR=/opt/hadoop/etc/hadoop
# Standalone 模式：本机作为 Master 时指定
export SPARK_MASTER_HOST=node1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. &lt;code&gt;spark-defaults.conf&lt;/code&gt;（on YARN 常用参数）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 默认资源模式：用 YARN
spark.master=yarn
spark.deploy.mode=client
# 单 Executor 资源（内存与核数按数据量调整）
spark.executor.memory=4g
spark.executor.cores=2
spark.driver.memory=2g
# 动态资源分配（可选）
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=1
spark.dynamicAllocation.maxExecutors=10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
注意区分两类内存：&lt;code&gt;spark.driver.memory&lt;/code&gt; 在 client 模式是本地 JVM 内存，在 cluster 模式是 YARN 容器内存；Driver 端用 &lt;code&gt;collect()&lt;/code&gt; 汇总大数据量时容易 OOM。另外 YARN 容器的资源上限受 &lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt; 约束，Executor 数量、内存、核数的乘积不能超过集群总资源，否则作业提交后一直处于 PENDING。
:::&lt;/p&gt;
&lt;h3&gt;三种模式的启动&lt;/h3&gt;
&lt;h4&gt;Local 模式（零部署）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;spark-shell
# 或提交作业
spark-submit --master local[*] your_app.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Standalone 模式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 主节点
start-master.sh      # Web UI: http://node1:8080
# 各从节点（Master 节点执行，会 ssh 拉起）
start-workers.sh
# 提交
spark-submit --master spark://node1:7077 your_app.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;on YARN 模式（生产）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 不需要启动任何 Spark 进程！直接提交即可
spark-submit --master yarn --deploy-mode cluster \
  --class org.apache.spark.examples.SparkPi \
  $SPARK_HOME/examples/jars/spark-examples_2.12-3.5.1.jar 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# YARN 模式：任务运行情况去 YARN 8088 页面看
# 提交后：
yarn application -list          # 看到 SparkPi 的 Application
yarn logs -applicationId &amp;lt;id&amp;gt;   # 查看作业日志

# Standalone 模式：访问 http://node1:8080 查看 Worker 与执行中的应用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
YARN 模式的验证要点：&lt;strong&gt;Spark 有没有真正用上 YARN，看 8088 页面&lt;/strong&gt;。如果作业出现在 YARN 的 Application 列表里，说明资源管理器接管成功；如果提交时报 &lt;code&gt;java.io.IOException: Connecting to ResourceManager failed&lt;/code&gt;，检查 &lt;code&gt;YARN_CONF_DIR&lt;/code&gt; 与 &lt;code&gt;yarn-site.xml&lt;/code&gt;。
:::&lt;/p&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ClassNotFoundException: org.apache.hadoop.fs.FileSystem&lt;/code&gt;&lt;/strong&gt;：Hadoop 依赖未加入 classpath，确认 &lt;code&gt;HADOOP_CONF_DIR&lt;/code&gt; 已导出，或提交时加 &lt;code&gt;--jars&lt;/code&gt; 补齐&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作业一直 PENDING&lt;/strong&gt;：资源不足（Executors 需求超过集群资源）或 YARN 队列容量不足；调小 &lt;code&gt;spark.executor.memory/cores&lt;/code&gt;，或检查 &lt;code&gt;yarn.scheduler.capacity&lt;/code&gt; 队列配置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Driver OOM&lt;/strong&gt;：client 模式下 &lt;code&gt;collect()&lt;/code&gt; 大结果集；改用 &lt;code&gt;saveAsTextFile&lt;/code&gt; 写 HDFS，或加大 driver 内存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executor 丢失（Lost executor）&lt;/strong&gt;：节点内存被打满被系统 OOM Kill，对照 &lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt; 与 Executor 配置排查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spark 版本与 Hadoop 不匹配&lt;/strong&gt;：&lt;code&gt;spark-x.y.z-bin-hadoop2&lt;/code&gt; 与 hadoop3 混用会报 RPC 协议错误，必须选 &lt;code&gt;bin-hadoop3&lt;/code&gt; 包&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产统一走 &lt;strong&gt;on YARN + client 或 cluster 模式&lt;/strong&gt;：cluster 模式 Driver 由 YARN 托管更稳，适合定时作业；交互式开发用 client&lt;/li&gt;
&lt;li&gt;动态资源分配建议开启：波峰波谷明显的作业能省一半资源&lt;/li&gt;
&lt;li&gt;历史记录服务（SparkHistoryServer）配合 &lt;code&gt;spark.eventLog.enabled=true&lt;/code&gt; 开启，便于排查历史作业&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flink（流式计算引擎）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Flink 是流批一体的分布式计算引擎，常与 Kafka 搭配做实时数仓。部署模式比 Spark 简单直观：解压后改一个配置文件，&lt;code&gt;start-cluster.sh&lt;/code&gt; 就能起一个可用集群（Standalone 模式）；生产环境也可以跑在 YARN 上，让资源调度统一归 YARN 管理。本文以 Flink 1.17 为例，覆盖 Standalone 与 on YARN 两种方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK 1.8 或 11（Flink 1.17 要求 JDK 8/11）&lt;/li&gt;
&lt;li&gt;on YARN 模式：Hadoop 3.x 集群 + &lt;code&gt;HADOOP_CLASSPATH&lt;/code&gt; 配置&lt;/li&gt;
&lt;li&gt;建议关闭节点防火墙或放行 Web UI 端口&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;下载与解压&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 选择 with-hadoop 预编译包，省去手动集成 Hadoop 依赖
wget https://dlcdn.apache.org/flink/flink-1.17.2/flink-1.17.2-bin-scala_2.12.tgz
tar -zxvf flink-1.17.2-bin-scala_2.12.tgz -C /opt
ln -s /opt/flink-1.17.2 /opt/flink

export FLINK_HOME=/opt/flink
export PATH=$PATH:$FLINK_HOME/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Flink 1.17 之前的老版本（如 1.13）预编译包不带 hadoop 集成，需要手动把 &lt;code&gt;flink-shaded-hadoop-uber&lt;/code&gt; jar 拷进 &lt;code&gt;lib/&lt;/code&gt; 才能在 YARN 上跑。新版省掉了这一步。
:::&lt;/p&gt;
&lt;h3&gt;Standalone 模式配置&lt;/h3&gt;
&lt;p&gt;核心配置文件 &lt;code&gt;conf/flink-conf.yaml&lt;/code&gt;（重点三个：内存、并行度、Web 端口）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# JobManager 与 TaskManager 内存（按机器实际调整）
jobmanager.memory.process.size: 2048m
taskmanager.memory.process.size: 4096m

# 每个 TaskManager 的并行度（Slot 数）
taskmanager.numberOfTaskSlots: 4

# 默认并行度（建议不超过总 Slot 数）
parallelism.default: 4

# Web UI 端口
rest.port: 8081
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;启动&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 主节点启动集群（JobManager + 本机 TaskManager）
start-cluster.sh

# 检查进程
jps   # 应看到 StandaloneSessionClusterEntrypoint 与 TaskManagerExecutor

# 停止
stop-cluster.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Web UI：&lt;code&gt;http://&amp;lt;主节点&amp;gt;:8081&lt;/code&gt;，能看到 JobManager 状态、TaskManager 列表、运行中的作业。&lt;/p&gt;
&lt;h3&gt;on YARN 模式&lt;/h3&gt;
&lt;p&gt;Flink 跑在 YARN 上有三种会话形态，区别在于作业生命周期：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;形态&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application 模式（推荐）&lt;/td&gt;
&lt;td&gt;每个作业一个专用 Flink 集群，用完即释放&lt;/td&gt;
&lt;td&gt;生产作业、长任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-Job 模式&lt;/td&gt;
&lt;td&gt;旧模式，作业结束集群释放（1.15+ 已弃用）&lt;/td&gt;
&lt;td&gt;不推荐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session 模式&lt;/td&gt;
&lt;td&gt;共享集群，多个作业复用&lt;/td&gt;
&lt;td&gt;短小临时作业多、希望快速提交&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;# 先让 Flink 客户端拿到 Hadoop 依赖
export HADOOP_CLASSPATH=$(hadoop classpath)

# Application 模式提交（作业常驻）
flink run-application -t yarn-application \
  -Djobmanager.memory.process.size=2048m \
  -Dtaskmanager.memory.process.size=4096m \
  -Dtaskmanager.numberOfTaskSlots=4 \
  -Dyarn.application.name=flink-etl-job \
  -c com.example.YourJob your-app.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Application 模式的作业在 YARN 8088 页面能看到独立的 Application。作业失败时整个集群自动销毁，由外部调度（如 DolphinScheduler/定时任务）负责重启，这正是它适合生产的原因。
:::&lt;/p&gt;
&lt;h3&gt;验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Standalone：提交内置示例（流式词频统计）
flink run $FLINK_HOME/examples/streaming/WordCount.jar \
  --input /tmp/input.txt --output /tmp/out

# 运行后在 8081 页面点击作业，能看到算子执行图与吞吐量

# on YARN：同样提交，作业出现在 YARN 8088 页面
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TaskManager 启动失败/OOM&lt;/strong&gt;：&lt;code&gt;taskmanager.memory.process.size&lt;/code&gt; 与机器内存不匹配，或与 &lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt; 冲突；JVM 堆内堆外内存（managed memory）分配不合理也会导致启动即死&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Could not resolve KeeperErrorCode&lt;/code&gt;&lt;/strong&gt;：使用 Kafka connector 时配置错误导致连不上 Kafka；检查 &lt;code&gt;bootstrap.servers&lt;/code&gt; 与 topic 权限&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反压（Backpressure）高&lt;/strong&gt;：下游处理不过来，通过 Web UI 的 BackPressure 标签页定位慢算子，调大并行度或优化算子逻辑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查点（Checkpoint）失败&lt;/strong&gt;：state.backend（RocksDB）存储路径磁盘不足，或 HDFS 目录权限问题；&lt;code&gt;flink run&lt;/code&gt; 加 &lt;code&gt;-Dstate.checkpoints.dir=hdfs://node1:8020/flink-cp&lt;/code&gt; 指定&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;on YARN 提交报 &lt;code&gt;NoSuchMethodError&lt;/code&gt; 等版本冲突&lt;/strong&gt;：&lt;code&gt;HADOOP_CLASSPATH&lt;/code&gt; 未配置或混入了错误版本的 hadoop 依赖&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产用 &lt;strong&gt;Application 模式 + 检查点 + 重启策略&lt;/strong&gt;（&lt;code&gt;restart-strategy: failure-rate&lt;/code&gt;），配合监控报警（Web UI 或接入 Prometheus）&lt;/li&gt;
&lt;li&gt;状态后端选 RocksDB（大状态），checkpoint 目录放 HDFS 保证可恢复&lt;/li&gt;
&lt;li&gt;Standalone 适合测试；正式环境资源统一归 YARN 管理，避免两套资源池互相争抢&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;第二部分：中间件组件&lt;/h1&gt;
&lt;h2&gt;Zookeeper（分布式协调）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Zookeeper 是分布式协调服务，Kafka、HBase、HDFS HA、Dubbo 注册中心等大量中间件都依赖它做选主与元数据存储。它的部署模式很简单：单机（演示）、伪集群、真集群（3/5/7 台奇数）。真集群的关键就两件事：&lt;code&gt;zoo.cfg&lt;/code&gt; 里声明所有节点、每个节点写自己的 &lt;code&gt;myid&lt;/code&gt;。本文以 Zookeeper 3.8 为例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK 1.8+&lt;/li&gt;
&lt;li&gt;集群节点数建议奇数（3、5、7）：选主需要过半机制，奇数台才能容忍一半以下的节点故障&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;下载与解压&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wget https://dlcdn.apache.org/zookeeper/zookeeper-3.8.4/apache-zookeeper-3.8.4-bin.tar.gz
tar -zxvf apache-zookeeper-3.8.4-bin.tar.gz -C /opt
ln -s /opt/apache-zookeeper-3.8.4-bin /opt/zookeeper

export ZOOKEEPER_HOME=/opt/zookeeper
export PATH=$PATH:$ZOOKEEPER_HOME/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置&lt;/h3&gt;
&lt;h4&gt;1. 核心配置 &lt;code&gt;conf/zoo.cfg&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;模板 &lt;code&gt;zoo_sample.cfg&lt;/code&gt; 复制而来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 基础心跳时间单位（毫秒），其他时间参数都是它的倍数
tickTime=2000
# 初始化连接时的最大心跳数：follower 与 leader 初始同步最长 10*tickTime
initLimit=10
# 运行中 leader 与 follower 心跳最大间隔，超过则判死
syncLimit=5
# 数据目录（快照），务必放数据盘；事务日志目录可另配 dataLogDir
dataDir=/data/zookeeper/data
dataLogDir=/data/zookeeper/logs
# 客户端连接端口
clientPort=2181
# 集群节点声明：server.&amp;lt;myid&amp;gt;=&amp;lt;host&amp;gt;:&amp;lt;选举端口&amp;gt;:&amp;lt;数据同步端口&amp;gt;
server.1=node1:2888:3888
server.2=node2:2888:3888
server.3=node3:2888:3888
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
三个端口的区分：2181 是客户端连接，2888 是 leader 与 follower 的数据同步，3888 是选举通信端口。防火墙放行时三个都要开。
:::&lt;/p&gt;
&lt;h4&gt;2. 每个节点写 &lt;code&gt;myid&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 在 dataDir 目录下创建 myid 文件，内容与 zoo.cfg 中的 server 编号一致
mkdir -p /data/zookeeper/data
# node1 上：
echo 1 &amp;gt; /data/zookeeper/data/myid
# node2 上：
echo 2 &amp;gt; /data/zookeeper/data/myid
# node3 上：
echo 3 &amp;gt; /data/zookeeper/data/myid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;strong&gt;myid 与 zoo.cfg 的 server 编号必须一一对应&lt;/strong&gt;，写错会导致节点加入不了集群，日志报 &lt;code&gt;Failed to sync&lt;/code&gt; 或反复切换 Leader。
:::&lt;/p&gt;
&lt;h3&gt;启动与验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 每个节点启动
zkServer.sh start

# 查看状态（会输出该节点是 leader 还是 follower）
zkServer.sh status

# 客户端连接验证
zkCli.sh -server node1:2181
# 连接成功后执行：
ls /                    # 根节点（应存在 /zookeeper 内置节点）
create /test hello      # 创建节点
get /test               # 读取节点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3 台节点启动后，&lt;code&gt;zkServer.sh status&lt;/code&gt; 应显示 1 台 leader、2 台 follower。&lt;/p&gt;
&lt;h3&gt;常用运维命令&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;zkServer.sh status              # 角色状态
zkServer.sh restart             # 重启
echo srvr | nc localhost 2181   # 查看运行统计（4lw 命令，需开启）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;conf/zoo.cfg&lt;/code&gt; 追加 &lt;code&gt;4lw.commands.whitelist=*&lt;/code&gt; 可开启四字命令（生产建议按需白名单）。&lt;/p&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动报 &lt;code&gt;Address already in use&lt;/code&gt;&lt;/strong&gt;：端口被占用，&lt;code&gt;ss -lntp | grep 2181&lt;/code&gt; 排查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集群全部变成 standalone / 一直 leader 选举中&lt;/strong&gt;：节点间 2888/3888 不通（防火墙）或 myid 冲突&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Session expired&lt;/code&gt;&lt;/strong&gt;：客户端与服务器之间心跳超时，tickTime 与 sessionTimeout 配置过小，或网络抖动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘打满&lt;/strong&gt;：&lt;code&gt;dataDir&lt;/code&gt; 下快照不断增长，Zookeeper 3.6+ 默认自动清理（&lt;code&gt;autopurge.snapRetainCount&lt;/code&gt;、&lt;code&gt;autopurge.purgeInterval&lt;/code&gt;），老版本需手动清&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU 飙高&lt;/strong&gt;：连接数过多或 watcher 数量过大，用 4lw 命令 &lt;code&gt;mntr&lt;/code&gt;、&lt;code&gt;wchs&lt;/code&gt; 排查&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产 3 台起步，ZK 与业务节点混部时注意隔离（它吃磁盘 IO 和网络）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dataLogDir&lt;/code&gt; 独立磁盘放事务日志，能显著提升写性能&lt;/li&gt;
&lt;li&gt;快照与事务日志定期检查清理策略，防止磁盘写满导致集群不可用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kafka（消息队列）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Kafka 是分布式消息队列，也是实时数仓的事实标准：上游 Flink 消费它做计算，下游 sink 到 HDFS/Doris。安装的核心配置在 &lt;code&gt;server.properties&lt;/code&gt; 一个文件里：broker 身份、监听地址、数据目录、集群协调方式（Zookeeper 或 3.x 的 KRaft）。本文以 Kafka 3.x 为例，先讲最常用的 Zookeeper 协调模式，再补充 KRaft 模式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;架构与依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Kafka Broker（消息存储与分发）
   ├── 集群协调：Zookeeper（传统模式）/ KRaft（3.x 新架构，去 ZK）
   ├── 客户端：producer / consumer
   └── 管理：kafka-topics.sh、kafka-console-*.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Kafka 3.x 引入 KRaft 模式后可以完全脱离 Zookeeper，但主流生产（尤其是与 Hadoop 生态共存）仍大量使用 Zookeeper 协调。KRaft 部署更轻，适合新建集群。
:::&lt;/p&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK 1.8+（Kafka 3.x 需要 JDK 8/11/17）&lt;/li&gt;
&lt;li&gt;Zookeeper 集群可用（传统模式）&lt;/li&gt;
&lt;li&gt;多 broker 节点建议奇数台&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;下载与解压&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wget https://dlcdn.apache.org/kafka/3.6.2/kafka_2.13-3.6.2.tgz
tar -zxvf kafka_2.13-3.6.2.tgz -C /opt
ln -s /opt/kafka_2.13-3.6.2 /opt/kafka

export KAFKA_HOME=/opt/kafka
export PATH=$PATH:$KAFKA_HOME/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置（Zookeeper 协调模式）&lt;/h3&gt;
&lt;p&gt;编辑 &lt;code&gt;config/server.properties&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 每台 broker 唯一
broker.id=0

# 监听地址：生产必须显式指定，否则注册给客户端的地址是内网主机名，客户端解析失败
listeners=PLAINTEXT://node1:9092

# 数据目录：日志段文件存放位置，多块盘逗号分隔
log.dirs=/data/kafka/logs

# 分区数与副本数的默认值（单分区默认即可，多分区按吞吐调整）
num.partitions=3
default.replication.factor=2

# Zookeeper 连接（传统模式）
zookeeper.connect=node1:2181,node2:2181,node3:2181

# 数据保留时间与大小（生产按业务设置）
log.retention.hours=72
log.retention.bytes=-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;listeners&lt;/code&gt; 是最容易踩的坑：不显式配置时，broker 会把自己注册为 &lt;code&gt;主机名:9092&lt;/code&gt;，客户端在别的机器上无法解析该主机名而连接失败。生产建议同时配 &lt;code&gt;advertised.listeners&lt;/code&gt; 指向客户端可达的地址。
:::&lt;/p&gt;
&lt;h3&gt;启动与验证&lt;/h3&gt;
&lt;h4&gt;1. 启动 Zookeeper（若未部署，单机演示可先用 Kafka 自带的）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 仅单机演示：kafka 自带的单机 ZK
zookeeper-server-start.sh config/zookeeper.properties
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 启动 Broker&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 每台节点执行（后台运行）
nohup kafka-server-start.sh config/server.properties &amp;gt; /opt/kafka/logs/kafka.out 2&amp;gt;&amp;amp;1 &amp;amp;

# 查看集群成员
kafka-broker-api-versions.sh --bootstrap-server node1:9092
# 或 kafka-metadata.sh（旧版用 zookeeper-shell 查 /brokers/ids）
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 消息收发验证&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 创建 topic：3 分区 2 副本
kafka-topics.sh --create --topic test-topic \
  --partitions 3 --replication-factor 2 \
  --bootstrap-server node1:9092

# 查看 topic 详情（确认分区与副本状态）
kafka-topics.sh --describe --topic test-topic --bootstrap-server node1:9092

# 生产消息
kafka-console-producer.sh --topic test-topic --bootstrap-server node1:9092
&amp;gt; hello kafka

# 消费消息（另开终端）
kafka-console-consumer.sh --topic test-topic \
  --from-beginning --bootstrap-server node1:9092
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;KRaft 模式（去 Zookeeper，可选）&lt;/h3&gt;
&lt;p&gt;3.x 新建集群可以直接用 KRaft，少维护一套 ZK：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 生成集群唯一 ID
KAFKA_CLUSTER_ID=&quot;$(kafka-storage.sh random-uuid)&quot;

# 2. 格式化存储目录（每台节点）
kafka-storage.sh format -t $KAFKA_CLUSTER_ID -c config/kraft/server.properties

# 3. 启动（controller 与 broker 合一角色，3 台起步）
kafka-server-start.sh config/kraft/server.properties
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;server.properties&lt;/code&gt; 中相应配置改为 &lt;code&gt;process.roles=broker,controller&lt;/code&gt;、&lt;code&gt;controller.quorum.voters=1@node1:9093,2@node2:9093,3@node3:9093&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客户端报 &lt;code&gt;Failed to construct kafka consumer / Connection refused&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;listeners&lt;/code&gt;/&lt;code&gt;advertised.listeners&lt;/code&gt; 未配或配错；防火墙 9092 未放行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;LEADER_NOT_AVAILABLE&lt;/code&gt;&lt;/strong&gt;：topic 刚创建 leader 选举未完成，稍等重试；持续出现则检查 broker 是否全部加入集群&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘 IO 高/吞吐上不去&lt;/strong&gt;：&lt;code&gt;log.dirs&lt;/code&gt; 只配了一块盘；副本因子高、acks 配置过严（&lt;code&gt;acks=all&lt;/code&gt; + &lt;code&gt;min.insync.replicas&lt;/code&gt; 不匹配）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消费组 lag 持续增长&lt;/strong&gt;：消费者处理速度跟不上生产速度，或分区数少于消费者并行度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Replication factor: 2 larger than available brokers: 1&lt;/code&gt;&lt;/strong&gt;：副本数超过存活 broker 数，单机演示时把副本因子改成 1&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产 3+ broker，&lt;code&gt;default.replication.factor&lt;/code&gt; 与 &lt;code&gt;min.insync.replicas&lt;/code&gt;（建议 2）配合使用，保证消息不丢&lt;/li&gt;
&lt;li&gt;数据盘单独挂载，&lt;code&gt;log.dirs&lt;/code&gt; 多盘分担 IO&lt;/li&gt;
&lt;li&gt;监控：JMX 指标（kafka.server 的 BytesInPerSec、UnderReplicatedPartitions）接入 Prometheus/Grafana&lt;/li&gt;
&lt;li&gt;消息体大小、保留时长按业务定，避免无脑调大导致磁盘膨胀&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MySQL（关系型数据库）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;MySQL 是使用最广的关系型数据库，安装方式很多：yum/apt 源安装、官方二进制包（tar.xz）、源码编译。二进制包方式最可控、与发行版无关，本文以 MySQL 8.0 官方二进制包为例，覆盖从初始化、启动到安全配置的完整流程。服务化使用 systemd 管理，保证重启自愈。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Linux（CentOS 7+/Ubuntu），glibc 2.17+（8.0 二进制包要求）&lt;/li&gt;
&lt;li&gt;依赖库：&lt;code&gt;libaio&lt;/code&gt;、&lt;code&gt;libncurses&lt;/code&gt;（可用 yum/apt 安装）&lt;/li&gt;
&lt;li&gt;数据盘建议独立挂载&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# CentOS 依赖
yum install -y libaio ncurses-libs
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;下载与解压&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 8.0 二进制包（约 250MB，选择 Linux-Generic 版本）
wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.36-linux-glibc2.17-x86_64.tar.xz
tar -xvf mysql-8.0.36-linux-glibc2.17-x86_64.tar.xz -C /opt
ln -s /opt/mysql-8.0.36-linux-glibc2.17-x86_64 /opt/mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建运行用户与目录&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# MySQL 禁止以 root 运行，创建专用用户
useradd -r -s /sbin/nologin mysql
mkdir -p /data/mysql
chown -R mysql:mysql /opt/mysql /data/mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;初始化数据目录&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 8.0 初始化方式（--initialize-insecure：root 初始密码为空）
/opt/mysql/bin/mysqld --initialize-insecure \
  --user=mysql --datadir=/data/mysql

# 如想生成随机初始密码，用 --initialize（密码在错误日志里）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
5.7 时代用 &lt;code&gt;mysql_install_db&lt;/code&gt; 初始化，8.0 统一用 &lt;code&gt;mysqld --initialize&lt;/code&gt;。初始化只需执行一次，重复执行会报 &lt;code&gt;datadir already exists&lt;/code&gt;。
:::&lt;/p&gt;
&lt;h3&gt;配置文件 my.cnf&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
basedir=/opt/mysql
datadir=/data/mysql
port=3306
socket=/data/mysql/mysql.sock
# 服务端字符集
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
# 数据目录环境（8.0 区分大小写必须在初始化前定）
lower_case_table_names=0
# 连接数（按业务调整）
max_connections=500
# 日志
log-error=/data/mysql/error.log
pid-file=/data/mysql/mysql.pid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;lower_case_table_names&lt;/code&gt; 决定表名是否大小写敏感，&lt;strong&gt;必须在初始化前确定&lt;/strong&gt;，8.0 之后运行时修改会直接报错。Linux 生产环境一般保持默认（敏感，值为 0），迁移自 Windows 环境的库注意此坑。
:::&lt;/p&gt;
&lt;h3&gt;启动并注册 systemd&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建 systemd 服务
cat &amp;gt; /etc/systemd/system/mysqld.service &amp;lt;&amp;lt;&apos;EOF&apos;
[Unit]
Description=MySQL Server
After=network.target

[Service]
User=mysql
Group=mysql
ExecStart=/opt/mysql/bin/mysqld --defaults-file=/etc/my.cnf
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now mysqld
systemctl status mysqld
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安全配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 首次连接（初始密码为空）
/opt/mysql/bin/mysql -uroot

# 设置 root 密码并删除匿名用户
ALTER USER &apos;root&apos;@&apos;localhost&apos; IDENTIFIED BY &apos;StrongPass!123&apos;;
DELETE FROM mysql.user WHERE User=&apos;&apos;;
FLUSH PRIVILEGES;

# 允许 root 远程访问（生产不推荐 root 远程，建议专用账号）
CREATE USER &apos;root&apos;@&apos;%&apos; IDENTIFIED BY &apos;StrongPass!123&apos;;
GRANT ALL PRIVILEGES ON *.* TO &apos;root&apos;@&apos;%&apos; WITH GRANT OPTION;
FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mysql -uroot -p
SELECT VERSION();          # 版本
SHOW VARIABLES LIKE &apos;%datadir%&apos;;   # 数据目录
# 建库建表压力测试
CREATE DATABASE testdb DEFAULT CHARACTER SET utf8mb4;
USE testdb; CREATE TABLE t(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50));
INSERT INTO t(name) VALUES (&apos;mysql&apos;);
SELECT * FROM t;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动报 &lt;code&gt;libaio.so.1: cannot open shared object file&lt;/code&gt;&lt;/strong&gt;：未装 libaio 依赖&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Can&apos;t connect to local MySQL server through socket&lt;/code&gt;&lt;/strong&gt;：mysqld 没起来，先看 &lt;code&gt;/data/mysql/error.log&lt;/code&gt;；socket 路径不一致也常见&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;忘记 root 密码&lt;/strong&gt;：&lt;code&gt;--skip-grant-tables&lt;/code&gt; 跳过权限表启动，重置后再正常重启&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Authentication plugin &apos;caching_sha2_password&apos;&lt;/code&gt; 连接失败&lt;/strong&gt;：8.0 默认认证插件，老客户端（5.7 及以下 JDBC 驱动）不支持；给账号指定 &lt;code&gt;IDENTIFIED WITH mysql_native_password BY &apos;...&apos;&lt;/code&gt;，或升级驱动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Too many connections&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;max_connections&lt;/code&gt; 不够，同时排查是否有连接泄漏&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产：主从复制（binlog 开启 &lt;code&gt;log-bin&lt;/code&gt; + &lt;code&gt;server-id&lt;/code&gt;）+ 定时物理备份，从库可读扩展&lt;/li&gt;
&lt;li&gt;数据目录放数据盘，日志（binlog/error log）单独盘位更好&lt;/li&gt;
&lt;li&gt;8.0 默认 &lt;code&gt;caching_sha2_password&lt;/code&gt; 认证，客户端组件版本要匹配&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Redis（缓存）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Redis 是高性能的内存键值数据库，安装过程在中间件里算简单的：源码编译三步走（configure/make/make install），核心工作量全在 &lt;code&gt;redis.conf&lt;/code&gt; 的取舍上——是否开启持久化、保护模式与密码、内存淘汰策略。本文以 Redis 7.x 为例，覆盖单机部署与主从复制的配置要点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;gcc、make（编译工具）&lt;/li&gt;
&lt;li&gt;内存规划：Redis 数据全在内存，数据量 ×2 的内存预算比较稳妥（内存峰值 + 持久化缓冲）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;yum install -y gcc make
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;下载与编译安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wget https://download.redis.io/releases/redis-7.2.4.tar.gz
tar -zxvf redis-7.2.4.tar.gz -C /opt
cd /opt/redis-7.2.4

# 编译并安装（默认安装到 /usr/local/bin：redis-server、redis-cli 等）
make -j4
make install

# 目录规划
mkdir -p /data/redis
cp redis.conf /etc/redis.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;核心配置 redis.conf&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 后台运行
daemonize yes

# 监听地址：生产只绑内网地址，避免暴露公网
bind 0.0.0.0

# 端口
port 6379

# 保护模式：开启时只允许本机回环地址连接
protected-mode yes

# 设置访问密码（生产必须）
requirepass YourPass123

# 持久化：AOF（默认关闭）与 RDB（默认开启）同时使用更稳
appendonly yes
appendfilename &quot;appendonly.aof&quot;

# 内存淘汰策略：到达 maxmemory 后如何清理
maxmemory 4gb
maxmemory-policy allkeys-lru

# 日志与数据目录
dir /data/redis
logfile /data/redis/redis.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
&lt;strong&gt;公网裸奔事故&lt;/strong&gt;：Redis 默认无密码 + 绑定所有网卡，暴露到公网后几分钟内就会被扫描器攻破，被植入挖矿程序。生产必须：&lt;code&gt;requirepass&lt;/code&gt; 设密码 + &lt;code&gt;bind&lt;/code&gt; 内网 IP + &lt;code&gt;protected-mode yes&lt;/code&gt;。
:::&lt;/p&gt;
&lt;h3&gt;启动与验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 启动
redis-server /etc/redis.conf

# 验证连接与读写
redis-cli -a YourPass123 ping
# 输出 PONG

redis-cli -a YourPass123
&amp;gt; set name redis
&amp;gt; get name
&amp;gt; info memory
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;主从复制（可选）&lt;/h3&gt;
&lt;p&gt;从节点在 &lt;code&gt;redis.conf&lt;/code&gt; 中配置一行即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 从节点指向主节点
replicaof 192.168.1.10 6379
# 主节点有密码时从节点也要配
masterauth YourPass123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证：主节点 &lt;code&gt;info replication&lt;/code&gt; 应显示从节点在线；从节点 &lt;code&gt;role:slave&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;(error) NOAUTH Authentication required&lt;/code&gt;&lt;/strong&gt;：未带密码认证，&lt;code&gt;redis-cli -a &amp;lt;密码&amp;gt;&lt;/code&gt; 或连接后 &lt;code&gt;AUTH &amp;lt;密码&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Could not connect to Redis at 127.0.0.1:6379&lt;/code&gt;&lt;/strong&gt;：服务没起来（看 &lt;code&gt;/data/redis/redis.log&lt;/code&gt;）或 bind 地址不对&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MISCONF Redis is configured to save RDB snapshots but is currently not able to persist&lt;/code&gt;&lt;/strong&gt;：磁盘权限或空间不足，RDB 保存失败；修复 &lt;code&gt;dir&lt;/code&gt; 目录权限，必要时 &lt;code&gt;CONFIG SET stop-writes-on-bgsave-error no&lt;/code&gt; 临时顶住&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存打满 OOM&lt;/strong&gt;：&lt;code&gt;maxmemory&lt;/code&gt; 未设置导致 Redis 吃满内存被内核 OOM Kill；设置 maxmemory + 淘汰策略&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主从延迟大&lt;/strong&gt;：网络延迟、从节点磁盘慢或 AOF 同步策略（&lt;code&gt;appendfsync&lt;/code&gt;）过严；&lt;code&gt;info replication&lt;/code&gt; 的 &lt;code&gt;master_repl_offset&lt;/code&gt; 对比&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产：主从 + 哨兵（Sentinel）或集群（Cluster）模式，单机是学习起点&lt;/li&gt;
&lt;li&gt;持久化：AOF + RDB 双开，&lt;code&gt;appendfsync everysec&lt;/code&gt; 平衡性能与安全&lt;/li&gt;
&lt;li&gt;缓存场景务必配 &lt;code&gt;maxmemory&lt;/code&gt; 与淘汰策略，防止缓存打满拖垮服务&lt;/li&gt;
&lt;li&gt;监控：&lt;code&gt;INFO&lt;/code&gt; 命令的 memory/connected_clients/keyspace 指标接入监控系统&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Nacos（注册与配置中心）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Nacos 是阿里开源的服务注册中心与配置中心，微服务架构（Spring Cloud Alibaba、Dubbo）的核心依赖。安装分单机与集群两种模式：单机内置 Derby 数据库开箱即用，集群必须外接 MySQL 并至少 3 节点。部署的关键参数就两个：&lt;strong&gt;启动模式&lt;/strong&gt;（standalone 还是集群）和&lt;strong&gt;数据库配置&lt;/strong&gt;。本文以 Nacos 2.3.x 为例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;架构与端口&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Nacos Server（注册中心 + 配置中心）
   ├── 单机：内嵌 Derby（开箱即用，仅测试）
   └── 集群：外接 MySQL（生产必须）+ 3 节点 + 一致性协议
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;端口&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8848&lt;/td&gt;
&lt;td&gt;主端口（HTTP 控制台与 API）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9848/9849&lt;/td&gt;
&lt;td&gt;gRPC 端口（2.x 客户端长连接，&lt;strong&gt;必须放行&lt;/strong&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
Nacos 2.x 客户端通过 gRPC 通信，只放行 8848 会导致服务注册成功但心跳续约失败。9848（客户端 gRPC）与 9849（集群间 gRPC）都要放行。
:::&lt;/p&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK 1.8+（2.x 需要 JDK 8+）&lt;/li&gt;
&lt;li&gt;集群模式：MySQL 5.7/8.0 + 3 台节点&lt;/li&gt;
&lt;li&gt;建议与业务同内网，8848/9848 不暴露公网&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;下载与解压&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/alibaba/nacos/releases/download/2.3.2/nacos-server-2.3.2.tar.gz
tar -zxvf nacos-server-2.3.2.tar.gz -C /opt
ln -s /opt/nacos-server-2.3.2 /opt/nacos

# 数据目录
mkdir -p /data/nacos
export NACOS_HOME=/opt/nacos
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;单机模式（快速体验）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 直接启动（默认 standalone 模式，使用内嵌 Derby）
sh bin/startup.sh -m standalone

# 访问控制台
# http://&amp;lt;IP&amp;gt;:8848/nacos   默认账号：nacos / nacos

# 查看日志确认启动成功
tail -f logs/start.out
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;集群模式（生产）&lt;/h3&gt;
&lt;h4&gt;1. 初始化 MySQL&lt;/h4&gt;
&lt;p&gt;Nacos 自带建表 SQL，2.x 是 &lt;code&gt;mysql-schema.sql&lt;/code&gt;（与 &lt;code&gt;conf/nacos-mysql.sql&lt;/code&gt; 同源，按版本选择）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -uroot -p &amp;lt; /opt/nacos/conf/mysql-schema.sql
# 执行后生成 nacos_config 库及其表
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 配置 &lt;code&gt;conf/application.properties&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 指定外部 MySQL
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://192.168.1.10:3306/nacos_config?characterEncoding=utf8&amp;amp;connectTimeout=1000&amp;amp;socketTimeout=3000&amp;amp;autoReconnect=true&amp;amp;useUnicode=true&amp;amp;useSSL=false&amp;amp;serverTimezone=Asia/Shanghai
db.user.0=nacos
db.password.0=nacos123
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 配置集群节点 &lt;code&gt;conf/cluster.conf&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 每行一个节点 IP:port（注意与 node.id 对应；2.2+ 版本需配置 node.id）
# node.id=1,192.168.1.10:8848
# node.id=2,192.168.1.11:8848
192.168.1.10:8848
192.168.1.11:8848
192.168.1.12:8848
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 三节点启动&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;sh bin/startup.sh      # 默认集群模式
# 各节点日志确认集群成员：
# logs/naming-server.log 中应看到对等节点列表
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 控制台：http://&amp;lt;IP&amp;gt;:8848/nacos
# 服务列表/配置管理页面正常、各节点数据一致即部署成功

# API 验证（服务注册）
curl -X POST &apos;http://192.168.1.10:8848/nacos/v1/ns/instance?serviceName=test-service&amp;amp;ip=192.168.1.20&amp;amp;port=8080&apos;
# 返回 ok 表示注册成功

# 查看节点状态（集群模式下确认全部在线）
curl &apos;http://192.168.1.10:8848/nacos/v1/ns/operator/servers&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动报 &lt;code&gt;Unable to start web server&lt;/code&gt; / 端口冲突&lt;/strong&gt;：8848 被占用；或 JDK 版本过低&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集群节点互相看不到&lt;/strong&gt;：&lt;code&gt;cluster.conf&lt;/code&gt; 配置错误、9849 gRPC 端口不通、各节点 &lt;code&gt;nacos.core.auth&lt;/code&gt; 或节点 ID 不一致&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置发布失败报 Derby 相关错误&lt;/strong&gt;：运行的是单机内嵌 Derby 但多实例同时连，或本该集群却起了多个单机实例；检查 &lt;code&gt;application.properties&lt;/code&gt; 的 MySQL 配置是否生效（启动日志会打印 &lt;code&gt;Database&lt;/code&gt; 类型）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务注册成功但客户端调用 503&lt;/strong&gt;：服务健康检查（心跳）失败，客户端与 9848 端口不通&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nacos&lt;/code&gt; 默认密码登录后必须改&lt;/strong&gt;：控制台首次登录用 nacos/nacos，生产第一时间修改&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产至少 3 节点集群 + 外接 MySQL（单机 Derby 只用于测试）&lt;/li&gt;
&lt;li&gt;2.x 客户端与服务端版本尽量一致，gRPC 协议跨大版本不兼容&lt;/li&gt;
&lt;li&gt;配置变更通过控制台/API 操作，变更记录存 MySQL，天然可审计&lt;/li&gt;
&lt;li&gt;与 Kubernetes 结合时可用 nacos-k8s 方案，但传统部署仍是主流&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;RocketMQ（消息队列）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;RocketMQ 是阿里开源的消息中间件，特点是高吞吐、低延迟、事务消息与顺序消息支持好，国内互联网与政企项目使用广泛。它分 NameServer（路由注册）与 Broker（消息存储）两个角色，部署模板化：装 JDK、解压、改内存参数、起两个进程。本文以 RocketMQ 5.x 为例，覆盖单机与集群两种形态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;架构角色&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Producer / Consumer（业务）
        │
        ▼
NameServer（路由中心，轻量，可多台）
        ▲
        │ 心跳 + 路由
Broker（消息存储，一主一从或多主多从）
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;th&gt;端口&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NameServer&lt;/td&gt;
&lt;td&gt;管理 Broker 路由，客户端先连它拿地址&lt;/td&gt;
&lt;td&gt;9876&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker&lt;/td&gt;
&lt;td&gt;消息存储与分发，向 NameServer 注册&lt;/td&gt;
&lt;td&gt;10911（主端口）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker VIP&lt;/td&gt;
&lt;td&gt;客户端访问 Broker 的 VIP 端口&lt;/td&gt;
&lt;td&gt;10909（默认开放）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK 1.8+（RocketMQ 官方推荐 JDK 8，5.x 支持到 JDK 11+ 但以 8 最稳）&lt;/li&gt;
&lt;li&gt;内存：官方建议 4GB+（默认启动脚本 Xmx 4g，机器小需调低）&lt;/li&gt;
&lt;li&gt;Linux 建议调大文件句柄与进程数限制&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;* soft nofile 655350&apos; &amp;gt;&amp;gt; /etc/security/limits.conf
echo &apos;* hard nofile 655350&apos; &amp;gt;&amp;gt; /etc/security/limits.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;下载与解压&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wget https://dlcdn.apache.org/rocketmq/5.1.4/rocketmq-all-5.1.4-bin-release.zip
unzip rocketmq-all-5.1.4-bin-release.zip -d /opt
ln -s /opt/rocketmq-all-5.1.4-bin-release /opt/rocketmq

export ROCKETMQ_HOME=/opt/rocketmq
export PATH=$PATH:$ROCKETMQ_HOME/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;单机部署（快速体验）&lt;/h3&gt;
&lt;h4&gt;1. 启动 NameServer&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;nohup sh bin/mqnamesrv &amp;gt; /opt/rocketmq/logs/namesrv.log 2&amp;gt;&amp;amp;1 &amp;amp;
# 看到 &quot;The Name Server boot success&quot; 即成功
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 启动 Broker（使用默认配置）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;nohup sh bin/mqbroker -n localhost:9876 \
  -c conf/broker.conf &amp;gt; /opt/rocketmq/logs/broker.log 2&amp;gt;&amp;amp;1 &amp;amp;
# 看到 &quot;The broker boot success&quot; 即成功
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
内存不足的机器启动失败时，先改启动脚本里的 JVM 参数：&lt;code&gt;bin/runserver.sh&lt;/code&gt;（NameServer 的 Xmx 默认 4g）与 &lt;code&gt;bin/runbroker.sh&lt;/code&gt;（Broker 的 Xmx 默认 8g）调小，例如 &lt;code&gt;-Xms1g -Xmx1g&lt;/code&gt;。
:::&lt;/p&gt;
&lt;h3&gt;生产配置要点（broker.conf）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Broker 集群名与自身名字
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0

# 注册给客户端/NameServer 的地址：生产必须填内网 IP，不能留空
brokerIP1=192.168.1.20

# NameServer 列表
namesrvAddr=192.168.1.10:9876;192.168.1.11:9876

# 消息存储目录（放数据盘）
storePathRootDir=/data/rocketmq/store
storePathCommitLog=/data/rocketmq/store/commitlog

# 删除策略：磁盘使用率阈值与保留时长
deleteWhen=04
fileReservedTime=72

# 自动创建 topic（生产建议关闭，Topic 走后台创建）
autoCreateTopicEnable=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;brokerIP1&lt;/code&gt; 不配置时 Broker 会注册自己的主机名/IP，客户端在其他机器解析不了就连接失败。这是 RocketMQ 最常见的部署坑，同 Kafka 的 &lt;code&gt;listeners&lt;/code&gt; 一个道理。
:::&lt;/p&gt;
&lt;h3&gt;验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看集群与 Broker 状态
mqadmin clusterList -n localhost:9876
# 应显示 broker-a 与对应的地址、版本

# 命令行收发消息测试
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer \
  -n localhost:9876
# 输出 &quot;Send Result: SEND_OK&quot;

sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer \
  -n localhost:9876
# 应消费到刚才生产的消息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产环境建议额外部署 &lt;code&gt;rocketmq-dashboard&lt;/code&gt;（官方 Web 控制台，spring boot 应用）查看 topic、消费组积压与消息详情。&lt;/p&gt;
&lt;h3&gt;集群模式（主从，可扩展）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一主一从&lt;/strong&gt;：两个 Broker 节点 &lt;code&gt;brokerName&lt;/code&gt; 相同、&lt;code&gt;brokerId&lt;/code&gt; 分别为 0 和 1（从节点同配置起 mqbroker 即可），从节点自动同步&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多主多从&lt;/strong&gt;：多组 brokerName 独立的主从对，分担 topic 流量&lt;/li&gt;
&lt;li&gt;NameServer 部署 2~3 台，业务侧 &lt;code&gt;namesrvAddr&lt;/code&gt; 写全&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 从节点只需 broker.conf 中 brokerId=1 并指向同一集群
nohup sh bin/mqbroker -n &amp;lt;namesrv&amp;gt;:9876 -c conf/broker-s.conf &amp;gt; broker-s.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;connect to 127.0.0.1:9876 failed&lt;/code&gt;&lt;/strong&gt;：NameServer 没启动或 &lt;code&gt;namesrvAddr&lt;/code&gt; 写错&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动报内存不足（&lt;code&gt;Could not reserve enough space&lt;/code&gt;）&lt;/strong&gt;：默认 JVM 参数 Xmx 太大，改 &lt;code&gt;runserver.sh&lt;/code&gt;/&lt;code&gt;runbroker.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端连不上 Broker（&lt;code&gt;sendDefaultImpl call timeout&lt;/code&gt;）&lt;/strong&gt;：&lt;code&gt;brokerIP1&lt;/code&gt; 未配置导致注册地址不可达；防火墙 10911/10909 未放行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消费积压持续增加&lt;/strong&gt;：消费端处理慢或消费失败重试死循环，查看消费组 &lt;code&gt;mqadmin consumerProgress -n &amp;lt;ns&amp;gt; -g &amp;lt;group&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息堆积磁盘暴涨&lt;/strong&gt;：&lt;code&gt;fileReservedTime&lt;/code&gt; 保留时间过长、&lt;code&gt;autoCreateTopicEnable=true&lt;/code&gt; 导致无限自动建 topic&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;生产：主从成组 + 2 台以上 NameServer，Topic 划分业务隔离&lt;/li&gt;
&lt;li&gt;存储目录独立数据盘，commitlog 与 consumequeue 分开磁盘效果更好&lt;/li&gt;
&lt;li&gt;事务消息、延迟消息测试先行，确认 5.x 与业务 SDK 版本兼容&lt;/li&gt;
&lt;li&gt;监控：Broker 的 &lt;code&gt;mqadmin&lt;/code&gt; 指标 + 消费积压告警（dashboard 自带）&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DataSophon 部署大数据组件：参数说明与优化实践</title><link>https://chaggle.github.io/posts/2026/08/10/datasophon-deploy-params/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/datasophon-deploy-params/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文基于本地源码工程（&lt;code&gt;datasophon&lt;/code&gt;，2.0.0 分支，对应 &lt;code&gt;datasophon-api/src/main/resources/meta/DDP-2.0.0&lt;/code&gt;）梳理 DataSophon 的组件部署机制：服务如何定义、参数如何组织、平台如何自动注入变量，并逐一给出核心组件的参数说明与优化建议。此前的手工部署与调优系列（HDFS/YARN/Hive/Spark/Flink/ZK/Kafka 等调优实践）可作为参数取舍的理论依据，本文讲&quot;在 DataSophon 里这些参数长什么样、怎么配&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、DataSophon 2.0.0 组件全景&lt;/h2&gt;
&lt;p&gt;源码中 &lt;code&gt;meta/DDP-2.0.0/&lt;/code&gt; 目录定义了当前版本支持的全部服务，每个服务一个 &lt;code&gt;service_ddl.json&lt;/code&gt;。共 21 个组件：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;版本&lt;/th&gt;
&lt;th&gt;依赖&lt;/th&gt;
&lt;th&gt;角色（roleType）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ZOOKEEPER&lt;/td&gt;
&lt;td&gt;3.6.4&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;ZkServer（master）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDFS&lt;/td&gt;
&lt;td&gt;3.3.6&lt;/td&gt;
&lt;td&gt;ZOOKEEPER&lt;/td&gt;
&lt;td&gt;JournalNode、NameNode、ZKFC（master）、DataNode（worker）、HdfsClient（client）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YARN&lt;/td&gt;
&lt;td&gt;3.3.6&lt;/td&gt;
&lt;td&gt;HDFS&lt;/td&gt;
&lt;td&gt;ResourceManager、HistoryServer（master）、NodeManager（worker）、YarnClient（client）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HIVE&lt;/td&gt;
&lt;td&gt;3.1.3&lt;/td&gt;
&lt;td&gt;HDFS&lt;/td&gt;
&lt;td&gt;HiveMetaStore、HiveServer2（master）、HiveClient（client）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPARK3&lt;/td&gt;
&lt;td&gt;3.4.3&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;SparkClient3（client）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FLINK&lt;/td&gt;
&lt;td&gt;1.16.2&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;FlinkClient（client）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KAFKA&lt;/td&gt;
&lt;td&gt;2.8.2&lt;/td&gt;
&lt;td&gt;ZOOKEEPER&lt;/td&gt;
&lt;td&gt;KafkaBroker（master）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TEZ&lt;/td&gt;
&lt;td&gt;0.10.4&lt;/td&gt;
&lt;td&gt;HDFS&lt;/td&gt;
&lt;td&gt;TezServer（master）、TezClient（client）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HBASE&lt;/td&gt;
&lt;td&gt;2.4.16&lt;/td&gt;
&lt;td&gt;HDFS&lt;/td&gt;
&lt;td&gt;HbaseMaster（master）、RegionServer（worker）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DORIS&lt;/td&gt;
&lt;td&gt;1.2.6&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;DorisFE（master）、DorisFEObserver（worker）、DorisBE（worker）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DS（DolphinScheduler）&lt;/td&gt;
&lt;td&gt;3.1.8&lt;/td&gt;
&lt;td&gt;ZOOKEEPER&lt;/td&gt;
&lt;td&gt;ApiServer、MasterServer、AlertServer（master）、WorkerServer（worker）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KYUUBI&lt;/td&gt;
&lt;td&gt;1.7.3&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;KyuubiServer（master）、KyuubiClient（client）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RANGER&lt;/td&gt;
&lt;td&gt;2.1.0&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;RangerAdmin（master）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KERBEROS&lt;/td&gt;
&lt;td&gt;1.15.1&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;Krb5Kdc、KAdmin（master）、Krb5Client（client）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TRINO&lt;/td&gt;
&lt;td&gt;367&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;TrinoCoordinator（master）、TrinoWorker（worker）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ELASTICSEARCH&lt;/td&gt;
&lt;td&gt;7.16.2&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;ElasticSearch（master）、EsExporter（master）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ICEBERG&lt;/td&gt;
&lt;td&gt;1.4.0&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;IcebergClient（client）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STREAMPARK&lt;/td&gt;
&lt;td&gt;2.1.1&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;StreamPark（master）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PROMETHEUS&lt;/td&gt;
&lt;td&gt;2.54.0&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;Prometheus（master）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GRAFANA&lt;/td&gt;
&lt;td&gt;11.2.1&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;Grafana（master）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALERTMANAGER&lt;/td&gt;
&lt;td&gt;0.23.0&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;AlertManager（master）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;依赖关系即安装顺序&lt;/strong&gt;：HDFS 依赖 ZOOKEEPER，YARN/Hive/Tez/HBase 依赖 HDFS，Kafka/DolphinScheduler 依赖 ZOOKEEPER。DataSophon 在安装向导中按依赖拓扑排序，先装依赖服务。
:::&lt;/p&gt;
&lt;p&gt;:::warning
本地工程为&lt;strong&gt;定制版本&lt;/strong&gt;：Hive/Ranger 等服务的元数据库默认指向&lt;strong&gt;达梦数据库&lt;/strong&gt;（&lt;code&gt;javax.jdo.option.ConnectionDriverName=dm.jdbc.driver.DmDriver&lt;/code&gt;），与社区版默认 MySQL 不同。部署前务必按企业环境修改数据库连接参数。
:::&lt;/p&gt;
&lt;h2&gt;二、服务定义模型（service_ddl.json）&lt;/h2&gt;
&lt;p&gt;每个组件的 &lt;code&gt;service_ddl.json&lt;/code&gt; 描述四件事：&lt;strong&gt;服务&lt;/strong&gt;、&lt;strong&gt;角色&lt;/strong&gt;、&lt;strong&gt;启停脚本&lt;/strong&gt;、&lt;strong&gt;参数&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1. 服务与角色&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;HDFS&quot;,
  &quot;version&quot;: &quot;3.3.6&quot;,
  &quot;dependencies&quot;: [&quot;ZOOKEEPER&quot;],
  &quot;packageName&quot;: &quot;hadoop-3.3.6.tar.gz&quot;,
  &quot;roles&quot;: [
    {
      &quot;name&quot;: &quot;NameNode&quot;,
      &quot;roleType&quot;: &quot;master&quot;,
      &quot;runAs&quot;: { &quot;user&quot;: &quot;hdfs&quot;, &quot;group&quot;: &quot;hadoop&quot; },
      &quot;cardinality&quot;: &quot;1+&quot;,
      &quot;logFile&quot;: &quot;${hadoopLogDir}/hadoop-hdfs-namenode-${host}.log&quot;,
      &quot;jmxPort&quot;: 27001,
      &quot;externalLink&quot;: { &quot;name&quot;: &quot;NameNode Ui&quot;, &quot;url&quot;: &quot;http://${host}:9870&quot; }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cardinality&lt;/code&gt;：角色实例数约束（&lt;code&gt;1&lt;/code&gt; 表示固定 1 个，&lt;code&gt;1+&lt;/code&gt; 表示至少 1 个可多个）——HDFS 的 JournalNode/NameNode 是 &lt;code&gt;1+&lt;/code&gt;，配合 HA 参数实现双主&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runAs&lt;/code&gt;：进程运行用户与组（hdfs:hadoop、kafka:kafka 等），平台自动创建用户与组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;startRunner/stopRunner/statusRunner&lt;/code&gt;：调用的控制脚本与参数（如 &lt;code&gt;control_hadoop.sh start namenode&lt;/code&gt;），脚本在服务目录内&lt;/li&gt;
&lt;li&gt;&lt;code&gt;externalLink&lt;/code&gt;：部署完成后 Web UI 直链（NameNode 9870、RM 8088 等）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jmxPort&lt;/code&gt;：JMX 指标端口，Prometheus 抓取用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 参数模型&lt;/h3&gt;
&lt;p&gt;每个参数是一个对象，字段含义：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;参数名（即写入组件配置文件的键）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;label&lt;/code&gt; / &lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;界面展示的中文名与说明&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;控件类型：&lt;code&gt;input&lt;/code&gt;、&lt;code&gt;select&lt;/code&gt;、&lt;code&gt;switch&lt;/code&gt;、&lt;code&gt;slider&lt;/code&gt;、&lt;code&gt;multiple&lt;/code&gt;（数组）、&lt;code&gt;multipleWithKey&lt;/code&gt;（自定义键值对）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;configType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;参数归属：&lt;code&gt;ha&lt;/code&gt;（高可用）、&lt;code&gt;path&lt;/code&gt;（路径）、&lt;code&gt;map&lt;/code&gt;（环境变量）、&lt;code&gt;rack&lt;/code&gt;（机架）、&lt;code&gt;kb&lt;/code&gt;（Kerberos）、&lt;code&gt;permission&lt;/code&gt;（Ranger 权限）、&lt;code&gt;custom&lt;/code&gt;（自定义配置）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;value&lt;/code&gt; / &lt;code&gt;defaultValue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前值 / 默认值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;configurableInWizard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是否在安装向导中可改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hidden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是否在界面隐藏&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;参数渲染机制&lt;/strong&gt;：&lt;code&gt;configType&lt;/code&gt; 决定参数在向导中如何分组展示（HA 组、Kerberos 组、机架组……），最终由 worker 端的 FreeMarker 模板（&lt;code&gt;templates/*.ftl&lt;/code&gt;，如 &lt;code&gt;hdfs-site.xml&lt;/code&gt; 由 &lt;code&gt;xml.ftl&lt;/code&gt;、&lt;code&gt;capacity-scheduler.ftl&lt;/code&gt;、&lt;code&gt;jvm.config.ftl&lt;/code&gt;）渲染成组件真实配置文件。&lt;code&gt;multipleWithKey&lt;/code&gt; 类型的参数允许&quot;自定义配置 key=value 追加到配置文件&quot;——这是不改源码扩展参数的口子。
:::&lt;/p&gt;
&lt;h2&gt;三、变量注入机制&lt;/h2&gt;
&lt;p&gt;参数默认值大量使用 &lt;code&gt;${...}&lt;/code&gt; 占位符，部署时平台自动解析：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变量&lt;/th&gt;
&lt;th&gt;来源&lt;/th&gt;
&lt;th&gt;示例用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${zkUrls}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zookeeper 服务实例&lt;/td&gt;
&lt;td&gt;HDFS 的 &lt;code&gt;ha.zookeeper.quorum&lt;/code&gt;、Kafka 的 &lt;code&gt;zookeeper.connect&lt;/code&gt; 自动填入 ZK 地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${dfs.nameservices}&lt;/code&gt; / &lt;code&gt;${nn1}&lt;/code&gt; / &lt;code&gt;${nn2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HDFS HA 参数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fs.defaultFS=hdfs://${dfs.nameservices}&lt;/code&gt; 自动组装&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${host}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前角色所在主机&lt;/td&gt;
&lt;td&gt;角色专属参数（&lt;code&gt;hive.server2.thrift.bind.host&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${realm}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;KERBEROS 服务&lt;/td&gt;
&lt;td&gt;各组件 Kerberos principal 自动拼接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${apiHost}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;管理端地址&lt;/td&gt;
&lt;td&gt;数据库连接（&lt;code&gt;jdbc:...${apiHost}...&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${HADOOP_HOME}&lt;/code&gt; / &lt;code&gt;${INSTALL_PATH}&lt;/code&gt; 等&lt;/td&gt;
&lt;td&gt;安装路径&lt;/td&gt;
&lt;td&gt;日志、配置目录引用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${metastoreHost}&lt;/code&gt; / &lt;code&gt;${historyserverHost}&lt;/code&gt; / &lt;code&gt;${rm1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对应角色部署主机&lt;/td&gt;
&lt;td&gt;服务间地址互引&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;strong&gt;变量注入是&quot;编排正确性&quot;的核心&lt;/strong&gt;：手工部署最大的痛点（地址写错、主机名不统一）被这套机制消解。但注意：${变量} 引用的服务必须已部署（如 &lt;code&gt;${zkUrls}&lt;/code&gt; 需要先装 Zookeeper），依赖顺序错误会导致渲染出空地址。
:::&lt;/p&gt;
&lt;h2&gt;四、部署流程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;建集群&lt;/strong&gt;：创建集群、选框架版本（对应 DDP-2.0.0）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加主机&lt;/strong&gt;：填 IP/SSH 信息，管理端向节点分发 Worker 并采集主机信息（&lt;code&gt;host-info-collect.sh&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选服务与角色分配&lt;/strong&gt;：勾选组件 → 每个角色指定节点（master/worker/client 三类角色各就各位）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数配置&lt;/strong&gt;：向导中按 configType 分组展示参数，改动 &lt;code&gt;value&lt;/code&gt; 即可；HA/路径/Kerberos 参数集中在此&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一键部署&lt;/strong&gt;：管理端生成 DAG（DAGBuildActor）→ 分发部署包（DispatcherWorkerActor）→ worker 解压 → 渲染配置（ConfigureServiceActor，FreeMarker）→ 执行 &lt;code&gt;control_xxx.sh&lt;/code&gt; 启停（InstallServiceActor/StartServiceActor）→ 状态回传&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;五、核心组件参数说明与优化&lt;/h2&gt;
&lt;p&gt;以下参数均来自 DDP-2.0.0 实际定义（含默认值），优化建议与《XXX 生产调优实践》系列一致。&lt;/p&gt;
&lt;h3&gt;HDFS（默认：blocksize 256M、replication 3、NN 堆 8G）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.blocksize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;268435456（256M）&lt;/td&gt;
&lt;td&gt;已比社区默认 128M 大一档：大块省元数据；文件以小文件为主时改回 134217728&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.replication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;容量与可靠性取舍：有异地备份可降 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;namenodeHeapSize&lt;/code&gt;（slider）&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;NN 堆内存，按&quot;每百万块 2~4G&quot;估算：32C256G 中型集群调 16，64C512G 大规模调 32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;datanodeHeapSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;DN 堆，写密集调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.handler.count&lt;/code&gt;（slider）&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;按 &lt;code&gt;20×log2(节点数)&lt;/code&gt;：百节点集群应调到 130~200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.datanode.handler.count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;写密集调 30~50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.name.dir&lt;/code&gt;（multiple）&lt;/td&gt;
&lt;td&gt;空&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;必填&lt;/strong&gt;：多目录分盘（如 /data1/nn,/data2/nn），元数据命根子&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.datanode.data.dir&lt;/code&gt;（multiple）&lt;/td&gt;
&lt;td&gt;空&lt;/td&gt;
&lt;td&gt;每节点多盘逗号填多目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hadoop.tmp.dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;/data/tmp/hadoop&lt;/td&gt;
&lt;td&gt;运行临时目录，放数据盘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.journalnode.edits.dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;/data/hdfs/jn&lt;/td&gt;
&lt;td&gt;JournalNode 日志目录，独立盘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enableRack&lt;/code&gt; + rack 参数&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;大集群开启机架感知（TableMapping + rack.properties）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.permissions.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;注意默认关闭权限检查；接 Ranger 后由 Ranger 策略管权限&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;YARN（默认：NM 内存 32G、容器上限 128G、公平调度）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32768&lt;/td&gt;
&lt;td&gt;单节点容器内存总量：8C16G 配 12G、32C256G 配 220G、64C512G 配 460G（预留系统）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.resource.cpu-vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;与 &lt;code&gt;pcores-vcores-multiplier&lt;/code&gt;（2.1）配合：&lt;code&gt;pcores × 2.1&lt;/code&gt; 是默认核数算法，可按物理核直接改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.minimum/maximum-allocation-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2048 / 131072&lt;/td&gt;
&lt;td&gt;容器内存上下限：最小 2G、最大 128G，防单容器&quot;一口吃太多&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.local-dirs&lt;/code&gt; / &lt;code&gt;log-dirs&lt;/code&gt;（multiple）&lt;/td&gt;
&lt;td&gt;空&lt;/td&gt;
&lt;td&gt;多盘填多目录，容器临时数据与日志分散 IO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.log-aggregation-enable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;日志聚合到 HDFS，&lt;code&gt;yarn.nodemanager.remote-app-log-dir&lt;/code&gt; 指定位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.aux-services&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;spark_shuffle,mapreduce_shuffle&lt;/td&gt;
&lt;td&gt;已预置 Spark Shuffle Service，配合 Spark 动态分配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.resourcemanager.scheduler.class&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FairScheduler&lt;/td&gt;
&lt;td&gt;生产按队列规划选 Capacity 或保留 Fair：配合 &lt;code&gt;yarn.scheduler.fair.allocation.file&lt;/code&gt; 定义队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.node-labels.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;节点标签调度：大集群按&quot;计算型/内存型&quot;打标签隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Zookeeper（默认：堆 1G、tickTime 2000）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dataDir&lt;/code&gt; / &lt;code&gt;dataLogDir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;/data/zookeeper、/data/log/zookeeper&lt;/td&gt;
&lt;td&gt;快照与事务日志&lt;strong&gt;分目录分盘&lt;/strong&gt;，事务日志盘性能决定写性能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zkHeapSize&lt;/code&gt;（slider）&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;ZK 数据全在内存，2~4G 足够；不随规格盲目加大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxClientCnxns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;连接池大的业务调 500~3000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initLimit&lt;/code&gt; / &lt;code&gt;syncLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10 / 5&lt;/td&gt;
&lt;td&gt;大集群/慢网络调 30/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server.1&lt;/code&gt; ~ &lt;code&gt;server.7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;空&lt;/td&gt;
&lt;td&gt;集群节点声明（ip:2888:3888），几台配几个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;autopurge.snapRetainCount&lt;/code&gt; / &lt;code&gt;purgeInterval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 / 12&lt;/td&gt;
&lt;td&gt;快照自动清理，防磁盘写满&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;custom.zoo.cfg&lt;/code&gt;（multipleWithKey）&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;追加任意 zoo.cfg 配置的扩展口&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Kafka（默认：分区 8、副本 2、IO 线程 12、堆 6G）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num.partitions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;分区数按吞吐规划：目标吞吐 ÷ 单分区吞吐；后期只加不减&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default.replication.factor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;生产 3 副本 + 内置 topic（&lt;code&gt;offsets.topic.replication.factor=3&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num.network.threads&lt;/code&gt; / &lt;code&gt;num.io.threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 / 12&lt;/td&gt;
&lt;td&gt;按核数调：8C 网络 4、IO 8；32C IO 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.dirs&lt;/code&gt;（multiple）&lt;/td&gt;
&lt;td&gt;空&lt;/td&gt;
&lt;td&gt;数据目录多盘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.retention.hours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;与磁盘容量联动，配 &lt;code&gt;log.retention.bytes&lt;/code&gt; 更可控&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kafkaHeapSize&lt;/code&gt;（slider）&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;页缓存优先，堆 4~8G 即可，不随规格涨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message.max.bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10000120&lt;/td&gt;
&lt;td&gt;单条消息上限，大消息业务按需调&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unclean.leader.election.enable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;保持 false，杜绝数据乱序&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Hive（默认：MR 引擎、map/reduce 内存 8G、堆 256M）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.execution.engine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;mr（select）&lt;/td&gt;
&lt;td&gt;生产切 tez（装 TEZ 服务后）或 spark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mapreduce.map/reduce.memory.mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;MR 引擎容器内存；切 Tez 后由 &lt;code&gt;tez.container.size&lt;/code&gt; 接管&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mapreduce.input.fileinputformat.split.maxsize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1073741824（1G）&lt;/td&gt;
&lt;td&gt;输入分片上限：小文件多调小（如 256M）提并行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.merge.mapfiles&lt;/code&gt; / &lt;code&gt;mapredfiles&lt;/code&gt; / &lt;code&gt;merge.size.per.task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true/true/256M&lt;/td&gt;
&lt;td&gt;小文件合并三件套已默认开启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.metastore.warehouse.dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;/user/hive/warehouse&lt;/td&gt;
&lt;td&gt;数仓根目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hiveHeapSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;默认偏小&lt;/strong&gt;：HiveServer2 堆按规格调 4~8G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.server2.support.dynamic.service.discovery&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;HS2 多实例 + ZK 动态发现，HiveServer2 可多台&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;custom.hive.site.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;追加 hive-site.xml 参数扩展口（如 CBO、MapJoin 阈值）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Spark3（默认：executor 8g×2core、driver 8g、动态分配开）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.executor.memory&lt;/code&gt; / &lt;code&gt;spark.executor.cores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8g / 2&lt;/td&gt;
&lt;td&gt;单 Executor 内存 8~32G、核 3~5；8C16G 节点降 4g×2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.driver.memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8g&lt;/td&gt;
&lt;td&gt;client 模式本地 JVM；大规模调 16g&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.dynamicAllocation.enabled&lt;/code&gt; + min/max&lt;/td&gt;
&lt;td&gt;true、1/6&lt;/td&gt;
&lt;td&gt;动态分配已开；maxExecutors 按队列资源定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.shuffle.service.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;与 YARN aux-services 的 spark_shuffle 配套&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.eventLog.enabled&lt;/code&gt; / &lt;code&gt;spark.history.fs.logDirectory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true / hdfs://.../spark-logs&lt;/td&gt;
&lt;td&gt;事件日志与 HistoryServer 已默认开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;custom.spark.defaults.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;追加参数口（AQE、广播阈值、Kryo 等按调优文章补）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Flink（默认：JM 堆 1.6G、TM 1.28G）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobmanager.memory.heap.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1600m&lt;/td&gt;
&lt;td&gt;8C16G 够用；大集群调 4~8G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taskmanager.memory.flink.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1280m&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;偏小&lt;/strong&gt;：按规格调（8C16G→12G、32C256G→24G），配合 &lt;code&gt;custom.flink.conf.yaml&lt;/code&gt; 补托管内存比例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enableJMHA&lt;/code&gt; + zookeeper 参数&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;生产开启：&lt;code&gt;high-availability=zookeeper&lt;/code&gt;、&lt;code&gt;storageDir&lt;/code&gt; 指向 HDFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;custom.flink.conf.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;追加 flink-conf.yaml 参数口（RocksDB、checkpoint、并行度）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Tez（依赖 HDFS）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.lib.uris&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;${fs.defaultFS}/tez/tez.tar.gz&lt;/td&gt;
&lt;td&gt;平台自动把 Tez 库上传到 HDFS 并注入，无需手工 put&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;custom.tez.site.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;追加 tez-site.xml（&lt;code&gt;tez.container.size&lt;/code&gt;、Session 超时等）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Kerberos（1.15.1）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;realm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HADOOP.COM&lt;/td&gt;
&lt;td&gt;域名（全大写惯例）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ticketLifetime&lt;/code&gt; / &lt;code&gt;renewLifetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;24h / 7d&lt;/td&gt;
&lt;td&gt;覆盖长作业：按作业时长调大 renew 并配自动续票&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kdcHost&lt;/code&gt; / &lt;code&gt;kadminHost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;${kdcHost}&lt;/td&gt;
&lt;td&gt;KDC 主机自动回填&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;Kerberos 全局联动&lt;/strong&gt;：各组件参数中的 &lt;code&gt;enableKerberos&lt;/code&gt; 开关 + &lt;code&gt;configType=kb&lt;/code&gt; 组（principal/keytab 等）会统一启用：HDFS 的 &lt;code&gt;nn/_HOST@${realm}&lt;/code&gt;、Kafka 的 &lt;code&gt;SASL_PLAINTEXT&lt;/code&gt;、Hive 的 &lt;code&gt;hive.server2.authentication=kerberos&lt;/code&gt; 全部由平台按 &lt;code&gt;${realm}&lt;/code&gt; 自动组装——这正是&quot;编排平台&quot;相对手工部署的巨大优势。
:::&lt;/p&gt;
&lt;h3&gt;Ranger（2.1.0）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明与优化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auditStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;elasticsearch&lt;/td&gt;
&lt;td&gt;审计存储指向 ES（版本内已集成 7.16.2）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enableHDFSPlugin&lt;/code&gt; / &lt;code&gt;enableHIVEPlugin&lt;/code&gt; / &lt;code&gt;enableHBASEPlugin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;按需开启&lt;/strong&gt;：开插件后对应组件侧自动生成 Ranger 集成（HDFS 的 &lt;code&gt;RangerHdfsAuthorizer&lt;/code&gt;、HBase 的 coprocessor 类）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rangerWebUrl&lt;/code&gt; / &lt;code&gt;rangerUserPass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;${rangerAdminUrl} / 初始密码&lt;/td&gt;
&lt;td&gt;部署后立即修改默认密码&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;六、三档规格参数速查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;8C16G&lt;/th&gt;
&lt;th&gt;32C256G&lt;/th&gt;
&lt;th&gt;64C512G&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HDFS &lt;code&gt;namenodeHeapSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YARN &lt;code&gt;memory-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12G&lt;/td&gt;
&lt;td&gt;220G&lt;/td&gt;
&lt;td&gt;460G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YARN &lt;code&gt;cpu-vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka &lt;code&gt;kafkaHeapSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark executor&lt;/td&gt;
&lt;td&gt;4g×2&lt;/td&gt;
&lt;td&gt;16g×4&lt;/td&gt;
&lt;td&gt;32g×5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flink &lt;code&gt;taskmanager.memory.flink.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12G&lt;/td&gt;
&lt;td&gt;24G&lt;/td&gt;
&lt;td&gt;48G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hive &lt;code&gt;hiveHeapSize&lt;/code&gt;(MB)&lt;/td&gt;
&lt;td&gt;4096&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;16384&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZK &lt;code&gt;zkHeapSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;七、部署优化建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先装依赖链&lt;/strong&gt;：ZOOKEEPER → HDFS → YARN → Hive/Tez/Spark/Kafka；Kerberos 建议最早装（后续组件 principal 自动联动）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径类参数（configType=path）部署前一次配对&lt;/strong&gt;：数据目录、日志目录、HA 地址，后期改动需重启组件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;善用 &lt;code&gt;custom.*&lt;/code&gt; 扩展参数&lt;/strong&gt;：官方参数表没有的（如 Spark AQE、Hive MapJoin 阈值、Tez container 大小），用 &lt;code&gt;multipleWithKey&lt;/code&gt; 追加，不必改源码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HA 参数组在向导中集中核对&lt;/strong&gt;：HDFS 双 NN + JournalNode（3 台）+ ZKFC、YARN 双 RM，&lt;code&gt;cardinality=1+&lt;/code&gt; 保证可分配两台&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kerberos/Ranger 一键联动&lt;/strong&gt;：安全需求高的集群，先装 KERBEROS，各组件开启 &lt;code&gt;enableKerberos&lt;/code&gt;；再装 RANGER 并开启各组件插件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控栈随装随用&lt;/strong&gt;：PROMETHEUS + GRAFANA + ALERTMANAGER 由平台部署并自动抓取各组件 jmxPort&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与手工部署的衔接&lt;/strong&gt;：DataSophon 生成的配置文件在组件安装目录（&lt;code&gt;${INSTALL_PATH}&lt;/code&gt;）下，&lt;code&gt;custom.*&lt;/code&gt; 追加的参数与平台默认参数合并渲染，排障时可直接查看最终配置文件，与调优系列文章的参数互相印证&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;变量渲染出空地址&lt;/strong&gt;（如 &lt;code&gt;zookeeper.connect&lt;/code&gt; 为空）：依赖服务未安装或未启动，检查服务依赖顺序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数修改不生效&lt;/strong&gt;：向导中改参数后需重新分发配置并重启对应角色；&lt;code&gt;custom.*&lt;/code&gt; 参数修改同理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;达梦/数据库连接失败&lt;/strong&gt;：定制版默认达梦驱动，换 MySQL 需同步修改驱动类与连接串&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;角色分配不符校验&lt;/strong&gt;：HA 角色（NameNode/JournalNode/RM）&lt;code&gt;cardinality=1+&lt;/code&gt;，至少分配 1 个；双主需 2 个&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署失败排查入口&lt;/strong&gt;：worker 端组件目录的 &lt;code&gt;control_xxx.sh&lt;/code&gt; 脚本与日志（&lt;code&gt;logFile&lt;/code&gt; 参数定义了路径），单角色可单独重装&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;DataSophon 的价值不在&quot;省去敲命令&quot;，而在&lt;strong&gt;把组件间的依赖、地址、安全联动固化成参数模型&lt;/strong&gt;：变量注入解决了地址编排，configType 分类管理了 HA/Kerberos/Ranger 等横切关注点，&lt;code&gt;custom.*&lt;/code&gt; 保留了手工调优的全部自由度。部署时按&quot;依赖链 → 路径参数 → 规格参数 → 安全联动&quot;的顺序推进，再结合调优系列的三档规格表，即可从&quot;能装&quot;走向&quot;装好&quot;。&lt;/p&gt;
</content:encoded></item><item><title>本地 DataSophon 与开源增强版对比及升级思路</title><link>https://chaggle.github.io/posts/2026/08/10/datasophon-local-vs-remote/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/datasophon-local-vs-remote/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;上一篇《DataSophon 部署大数据组件》基于本地私有工程（2.0.0）。本文将其与 GitHub 公开仓库 &lt;code&gt;88fantasy/datasophon&lt;/code&gt;（3.0-SNAPSHOT，深度定制分支）做全面对比：技术栈、通信架构、组件清单、参数模型，并给出&quot;要不要升、怎么升&quot;的决策与迁移思路。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、两个工程的定位&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工程&lt;/th&gt;
&lt;th&gt;版本&lt;/th&gt;
&lt;th&gt;来源&lt;/th&gt;
&lt;th&gt;定位&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;本地工程（&lt;code&gt;IdeaProjects/datasophon&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;2.0.0（feature-V2.0.0）&lt;/td&gt;
&lt;td&gt;企业内网私有&lt;/td&gt;
&lt;td&gt;生产定制版：达梦数据库集成、21 组件、历经多年生产验证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开源增强版（&lt;code&gt;github.com/88fantasy/datasophon&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;3.0-SNAPSHOT&lt;/td&gt;
&lt;td&gt;公开 GitHub，活跃维护（38+ PR）&lt;/td&gt;
&lt;td&gt;现代化重写：Java 21/gRPC/K8s/OTel/Go CLI/AI Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
网上仓库是&quot;基于上游 Datasophon 的定制分支&quot;，README 明确说明做了 gRPC 替代 Pekko（原 Akka）、CLI Go 重写、Spring Boot 3 升级等改造。它不是原版社区（datasophon-org），而是第三方深度定制的增强分支。
:::&lt;/p&gt;
&lt;h2&gt;二、技术栈对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;本地 2.0.0&lt;/th&gt;
&lt;th&gt;开源增强版 3.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语言&lt;/td&gt;
&lt;td&gt;Java 8&lt;/td&gt;
&lt;td&gt;Java 21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;后端框架&lt;/td&gt;
&lt;td&gt;Spring Boot 2.x&lt;/td&gt;
&lt;td&gt;Spring Boot 3.4.5、MyBatis-Plus 3.5.9、Druid、Flyway 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;进程通信&lt;/td&gt;
&lt;td&gt;Akka 2.4.20（Akka Remote，2551/2552 端口）&lt;/td&gt;
&lt;td&gt;gRPC 1.68（18081/18082，grpc-spring-boot-starter）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;前端&lt;/td&gt;
&lt;td&gt;无独立前端模块（配套 UI 项目）&lt;/td&gt;
&lt;td&gt;React 19 + Ant Design Pro + Monaco + AntV X6/G6（datasophon-ui）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据库&lt;/td&gt;
&lt;td&gt;达梦 DM（定制默认）&lt;/td&gt;
&lt;td&gt;MySQL 8 + Flyway 迁移（1.1.0 → 2.1.0 全链路迁移脚本）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;任务编排&lt;/td&gt;
&lt;td&gt;Akka Actor + DAG 构建&lt;/td&gt;
&lt;td&gt;RepoDAG + @Async + @Scheduled 巡检&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;部署形态&lt;/td&gt;
&lt;td&gt;裸机脚本（datasophon-api.sh）&lt;/td&gt;
&lt;td&gt;裸机 / Docker / Docker Compose / K8s manifest 四选一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;datasophon-cli-go（Go 1.21，节点初始化 33 步、断点续跑、--dry-run）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;K8s&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;datasophon-k8s-agent（RSA 签名鉴权）+ Helm + K8sServiceInstance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可观测&lt;/td&gt;
&lt;td&gt;Prometheus/Grafana/AlertManager + JMX&lt;/td&gt;
&lt;td&gt;上者 + OTel Collector 生态 + Loki/Promtail（otel 改造分支）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 能力&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;datasophon-ai-agent 模块&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;strong&gt;技术栈差异是升级的根本成本&lt;/strong&gt;：本地是 Java 8 + Akka 2.4.20 时代（Spring Boot 2 已停止社区维护），网上是 Java 21 + Spring Boot 3 + gRPC 的现代架构。这不是&quot;换个版本号&quot;，而是两次技术代际的跨越，Worker 与 Master 的通信协议完全不同（Akka 序列化 vs Protobuf）。
:::&lt;/p&gt;
&lt;h2&gt;三、组件能力对比&lt;/h2&gt;
&lt;h3&gt;本地 2.0.0（DDP-2.0.0，21 个）&lt;/h3&gt;
&lt;p&gt;ZK 3.6.4、HDFS 3.3.6、YARN 3.3.6、Hive 3.1.3、Spark3 3.4.3、Flink 1.16.2、Kafka 2.8.2、Tez 0.10.4、HBase 2.4.16、Doris 1.2.6、DS 3.1.8、Kyuubi 1.7.3、Ranger 2.1.0、Kerberos 1.15.1、Trino 367、ES 7.16.2、Iceberg 1.4.0、StreamPark 2.1.1、Prometheus、Grafana 11.2.1、AlertManager&lt;/p&gt;
&lt;h3&gt;开源增强版 3.0（27+ 个）&lt;/h3&gt;
&lt;p&gt;HDFS 3.5.0、YARN、Spark3、Flink、Hive、Kyuubi、Kafka、ZK、Doris、DS、ES、&lt;strong&gt;Gravitino&lt;/strong&gt;、&lt;strong&gt;JuiceFS&lt;/strong&gt;、&lt;strong&gt;Nacos&lt;/strong&gt;、&lt;strong&gt;Nginx&lt;/strong&gt;、&lt;strong&gt;OTelCollector&lt;/strong&gt;、&lt;strong&gt;Valkey&lt;/strong&gt;、&lt;strong&gt;APISIX&lt;/strong&gt;、&lt;strong&gt;Datart&lt;/strong&gt;、&lt;strong&gt;MinIO&lt;/strong&gt;、&lt;strong&gt;ETCD&lt;/strong&gt;、&lt;strong&gt;USCHEDULER&lt;/strong&gt;、&lt;strong&gt;Loki/Promtail&lt;/strong&gt;、&lt;strong&gt;Redis&lt;/strong&gt;、&lt;strong&gt;Amoro&lt;/strong&gt;、&lt;strong&gt;StarRocks&lt;/strong&gt;（Doris 兼容）等&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对比项&lt;/th&gt;
&lt;th&gt;本地独有&lt;/th&gt;
&lt;th&gt;网上独有&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;安全体系&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Kerberos、Ranger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;计算&lt;/td&gt;
&lt;td&gt;Tez、Trino、Iceberg&lt;/td&gt;
&lt;td&gt;Gravitino（元数据）、Amoro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储&lt;/td&gt;
&lt;td&gt;HBase&lt;/td&gt;
&lt;td&gt;JuiceFS、MinIO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;消息/协调&lt;/td&gt;
&lt;td&gt;StreamPark&lt;/td&gt;
&lt;td&gt;Nacos、ETCD、USCHEDULER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网关/缓存&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;APISIX、Nginx、Valkey（Redis 兼容）、Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BI/可观测&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Datart、OTelCollector、Loki/Promtail&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::caution
&lt;strong&gt;组件清单是升级决策的最大变量&lt;/strong&gt;：本地独有的 Kerberos/Ranger（安全体系）与 Tez/Trino/HBase 等组件在 3.0 的 meta 中&lt;strong&gt;没有定义&lt;/strong&gt;。升级到 3.0 意味着这些组件的部署能力需要重新开发移植，而安全体系（Kerberos/Ranger）往往是政企场景的硬要求。
:::&lt;/p&gt;
&lt;h2&gt;四、服务定义与参数模型演进&lt;/h2&gt;
&lt;p&gt;两个版本都采用&quot;service_ddl.json + worker 策略类&quot;的元数据驱动模型，但参数定义字段有演进：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 本地 2.0.0
{
  &quot;name&quot;: &quot;dfs.namenode.handler.count&quot;,
  &quot;label&quot;: &quot;NameNode处理线程池大小&quot;,
  &quot;type&quot;: &quot;slider&quot;,
  &quot;configType&quot;: &quot;&quot;,          // ha/path/map/rack/kb/permission/custom 归属
  &quot;configurableInWizard&quot;: true,
  &quot;defaultValue&quot;: &quot;16&quot;
}

// 开源增强版 3.0
{
  &quot;name&quot;: &quot;dfs.namenode.handler.count&quot;,
  &quot;configName&quot;: &quot;dfs.namenode.handler.count&quot;,
  &quot;required&quot;: true,
  &quot;minValue&quot;: 0,
  &quot;maxValue&quot;: 128,           // 参数校验上下界
  &quot;type&quot;: &quot;slider&quot;,
  &quot;configurableInWizard&quot;: true,
  &quot;defaultValue&quot;: &quot;16&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;演进点&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;参数校验&lt;/td&gt;
&lt;td&gt;3.0 增加 &lt;code&gt;required&lt;/code&gt; / &lt;code&gt;minValue&lt;/code&gt; / &lt;code&gt;maxValue&lt;/code&gt;，前端可做输入校验&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;arch&lt;/code&gt; 字段&lt;/td&gt;
&lt;td&gt;3.0 服务定义支持 &lt;code&gt;arch&lt;/code&gt;（x86/arm），延续混合架构部署能力&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;角色扩展&lt;/td&gt;
&lt;td&gt;HDFS 在 3.0 增加 HttpFs 角色，本地无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元数据位置&lt;/td&gt;
&lt;td&gt;本地 &lt;code&gt;meta/DDP-2.0.0/&lt;/code&gt;，3.0 为 &lt;code&gt;package/raw/meta/datacluster-physical/&lt;/code&gt;（物理集群）并预留 datacluster-k8s 形态&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;好消息&lt;/strong&gt;：两版的 service_ddl.json 结构同源（name/roles/parameters/configurableInWizard），组件定义迁移是&quot;字段级适配&quot;而非重写——主要工作是补 &lt;code&gt;required/minValue/maxValue&lt;/code&gt; 等校验字段，以及把 configType 分组语义映射到 3.0 的表单组织方式。
:::&lt;/p&gt;
&lt;h2&gt;五、升级决策&lt;/h2&gt;
&lt;h3&gt;何时值得升级&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;结论&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;有 K8s 化诉求（大数据上云、多集群、容器化）&lt;/td&gt;
&lt;td&gt;值得：3.0 原生支持 K8s Agent + 多形态集群&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要现代可观测（OTel/Loki 链路追踪）&lt;/td&gt;
&lt;td&gt;值得：3.0 已 OTel 化改造&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要新组件（Gravitino/JuiceFS/Nacos/APISIX/Valkey）&lt;/td&gt;
&lt;td&gt;值得：现成定义直接用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要 AI Agent 运维能力&lt;/td&gt;
&lt;td&gt;值得：3.0 有 datasophon-ai-agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生产已稳定、无上述诉求、强依赖 Kerberos/Ranger 安全体系&lt;/td&gt;
&lt;td&gt;暂缓：迁移成本高、收益低&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;升级成本评估清单&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;通信架构重构&lt;/strong&gt;：Akka（2551/2552）→ gRPC（18081/18082），Worker 侧所有 actor 消息处理需重写为 gRPC 服务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元数据迁移&lt;/strong&gt;：本地达梦库表 → 3.0 MySQL 结构。3.0 自带 Flyway 1.1.0→2.1.0 迁移链，但&lt;strong&gt;不含本地定制库表&lt;/strong&gt;（达梦 schema、定制配置表），需自写迁移 SQL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件迁移&lt;/strong&gt;：21 个 service_ddl.json 逐组件字段适配 + 对应 worker 策略类（&lt;code&gt;*HandlerStrategy&lt;/code&gt;）移植；&lt;strong&gt;Kerberos/Ranger/Tez/Trino/HBase/Iceberg/StreamPark 需从零开发&lt;/strong&gt;（3.0 无定义）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DDP 安装包&lt;/strong&gt;：本地包与 3.0 组件版本不同（HDFS 3.3.6 vs 3.5.0），需重新准备包与校验&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库定制回归&lt;/strong&gt;：达梦 → MySQL 的驱动、方言、大小写规则差异&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI 切换&lt;/strong&gt;：配套 UI → React 19 前端，运维习惯与权限流程需要适配&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;六、升级路径建议&lt;/h2&gt;
&lt;h3&gt;路径 A：完整迁移（适合&quot;新集群、无历史包袱&quot;）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;评估（2 周）→ 元数据迁移（Flyway + 定制 SQL）→ 核心组件迁移
→ 缺失组件开发（Kerberos/Ranger 优先）→ 双集群灰度 → 业务切换
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路径 B：渐进式（推荐，适合&quot;生产稳定 + 逐步演进&quot;）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;阶段一（零风险）&lt;/strong&gt;：保持 2.0.0 生产运行，搭一套 3.0 测试集群&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阶段二（验证）&lt;/strong&gt;：3.0 上复刻核心链路（ZK→HDFS→YARN→Hive/Spark），验证 gRPC 通信、参数渲染、组件兼容&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阶段三（移植）&lt;/strong&gt;：把 3.0 的成熟组件定义（如 Gravitiño/JuiceFS/Nacos）反向移植到 2.0.0 或评估迁移；同步自研 Kerberos/Ranger 的 3.0 版定义&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阶段四（切换）&lt;/strong&gt;：新业务集群直接用 3.0，老集群按生命周期自然淘汰&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;路径 C：不升平台，只升组件&lt;/h3&gt;
&lt;p&gt;在 2.0.0 内用 &lt;code&gt;custom.*&lt;/code&gt; 扩展参数（&lt;code&gt;multipleWithKey&lt;/code&gt; 追加配置）+ 自研 service_ddl.json 补齐组件（Nacos 等），把 3.0 的&quot;组件生态&quot;借过来，平台本体不动。适合&quot;平台稳定、只想加组件&quot;的场景。&lt;/p&gt;
&lt;p&gt;:::note
三条路径的共同前提：&lt;strong&gt;先做组件清单对齐&lt;/strong&gt;。把两边的服务定义逐项对照（组件名、版本、角色、parameters 数量），输出差异矩阵，作为迁移排期的输入——缺失组件（尤其 Kerberos/Ranger 安全体系）的开发工作量决定路径选择，也最难自动化。
:::&lt;/p&gt;
&lt;h2&gt;七、总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;两者不是版本关系，是代际关系&lt;/strong&gt;：本地 2.0.0 是&quot;Java 8/Akka/裸机&quot;的经典形态，网上 3.0 是&quot;Java 21/gRPC/K8s/OTel&quot;的现代形态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件生态各有侧重&lt;/strong&gt;：本地强在安全（Kerberos/Ranger）与 Hadoop 全家桶，3.0 强在新组件（Gravitino/JuiceFS/Nacos/APISIX）与可观测&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;升级的本质是迁移工程&lt;/strong&gt;：通信协议、元数据库、组件定义、DDP 包四件事，按&quot;先对齐组件清单、再迁移元数据、最后切业务&quot;的顺序推进&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产最优解通常是渐进式&lt;/strong&gt;：3.0 测试集群先行验证，缺失组件（尤其安全体系）自研移植后再谈切换；平台本体不必追求一步到位&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Flink 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/flink-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/flink-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Flink 调优的核心矛盾是&lt;strong&gt;内存三分&lt;/strong&gt;（JVM 堆、托管内存、堆外）与&lt;strong&gt;状态后端&lt;/strong&gt;（RocksDB 还是堆内）的选择，其次是并行度与检查点的节奏。调优目标是：作业吞吐稳定、故障秒级恢复、不丢数据。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taskmanager.memory.process.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;TaskManager 总内存（含堆+托管+堆外），按规格表调整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taskmanager.memory.managed.fraction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.4&lt;/td&gt;
&lt;td&gt;托管内存占比（排序/哈希/RocksDB 状态），RocksDB 场景调到 0.6~0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taskmanager.memory.task.off-heap.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128M&lt;/td&gt;
&lt;td&gt;任务堆外内存，网络缓冲不足时调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taskmanager.numberOfTaskSlots&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;每个 TaskManager 的槽位数，一般 = 该机核数（配合内存）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parallelism.default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;默认并行度，= 总 Slot 数（或按吞吐压测）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;state.backend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HashMap（堆内）&lt;/td&gt;
&lt;td&gt;RocksDB（大状态、增量 checkpoint）或堆内（小状态、低延迟）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;state.checkpoints.dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;Checkpoint 存储路径，生产必须指向 HDFS/S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;execution.checkpointing.interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;Checkpoint 间隔：30~120s 常见，越小恢复越快、开销越大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;execution.checkpointing.min-pause&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;两次 checkpoint 最小间隔，防止 checkpoint 风暴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;restart-strategy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;固定延迟&lt;/td&gt;
&lt;td&gt;生产用 failure-rate（失败率限流重启），配合 checkpoint 恢复&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;内存三分法的理解&lt;/strong&gt;：Flink 内存 = 堆（用户代码+状态）+ 托管内存（RocksDB/排序缓冲）+ 堆外（网络/框架）。调优时先算总量，再按状态类型分配比例——堆内状态作业把托管比例调小，RocksDB 作业调大。&lt;strong&gt;调优 90% 的内存问题都是三者比例失衡&lt;/strong&gt;。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数（每节点）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G&lt;/th&gt;
&lt;th&gt;32C256G&lt;/th&gt;
&lt;th&gt;64C512G&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TaskManager 数&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3~4&lt;/td&gt;
&lt;td&gt;6~8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taskmanager.memory.process.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12G&lt;/td&gt;
&lt;td&gt;64G&lt;/td&gt;
&lt;td&gt;64G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taskmanager.numberOfTaskSlots&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单 Slot 内存&lt;/td&gt;
&lt;td&gt;3G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;状态后端（大状态）&lt;/td&gt;
&lt;td&gt;RocksDB&lt;/td&gt;
&lt;td&gt;RocksDB&lt;/td&gt;
&lt;td&gt;RocksDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taskmanager.memory.managed.fraction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobmanager.memory.process.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;execution.checkpointing.interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;td&gt;60s&lt;/td&gt;
&lt;td&gt;60s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint 模式&lt;/td&gt;
&lt;td&gt;exactly-once&lt;/td&gt;
&lt;td&gt;exactly-once&lt;/td&gt;
&lt;td&gt;exactly-once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
TaskManager 数太多（如 64C512G 上 8 个）时注意：Slot 与核数不要求一一对应，但&lt;strong&gt;总 Slot 数不要超过物理核数&lt;/strong&gt;，超了就是超卖，吞吐不升反降。规格表建议先按&quot;单 Slot 内存 6~10G、每节点 Slot ≤ 核数&quot;试跑，再按压力测试微调。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并行度确定法&lt;/strong&gt;：先按&quot;总 Slot 数&quot;设初始并行度，用生产流量的 2 倍压测；吞吐不达标时优先查&lt;strong&gt;反压&lt;/strong&gt;（Web UI BackPressure 页），而不是盲目加并行度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反压处理&lt;/strong&gt;：反压 = 下游瓶颈。定位慢算子（常见：无键聚合、外部 IO、序列化瓶颈），对无状态算子可调大并行度，有状态算子并行度与 Key 分布强相关，需要重分区&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态与 RocksDB&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;增量 checkpoint 开启（&lt;code&gt;state.backend.incremental=true&lt;/code&gt;），大状态作业 checkpoint 时间直线下降&lt;/li&gt;
&lt;li&gt;RocksDB 调参：&lt;code&gt;state.backend.rocksdb.memory.managed=true&lt;/code&gt;（托管内存统一管）、&lt;code&gt;writebuffer.size&lt;/code&gt;、&lt;code&gt;block.cache.size&lt;/code&gt; 由托管比例自动分配&lt;/li&gt;
&lt;li&gt;状态分区设计：按业务维度合理选 Key，避免单一 Key 热点（Sink 侧倾斜常见）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;窗口作业&lt;/strong&gt;：&lt;code&gt;window.allowedLateness&lt;/code&gt; 与水位线（watermark）节奏匹配；乱序大时调大 out-of-orderness，避免窗口频繁晚到重算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sink 背压&lt;/strong&gt;：写入 Kafka/MySQL 慢导致反压时，先优化 Sink（批量、异步、连接池），再考虑并行度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes/资源管理&lt;/strong&gt;：任务独占资源（Application 模式），防止共享集群作业互相挤占&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;状态恢复&lt;/td&gt;
&lt;td&gt;Checkpoint + 增量&lt;/td&gt;
&lt;td&gt;默认机制，故障回放至最近一次成功快照&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主动备份&lt;/td&gt;
&lt;td&gt;Savepoint&lt;/td&gt;
&lt;td&gt;停机/升级前手动保存，可跨集群迁移恢复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元数据&lt;/td&gt;
&lt;td&gt;Checkpoint 目录异地&lt;/td&gt;
&lt;td&gt;&lt;code&gt;state.checkpoints.dir&lt;/code&gt; 放 HDFS/S3，配合快照异地&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;作业管理&lt;/td&gt;
&lt;td&gt;JobManager HA&lt;/td&gt;
&lt;td&gt;standalone 模式下用 ZK/K8s 高可用；on YARN 由 YARN 保证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;双跑&lt;/td&gt;
&lt;td&gt;备集群影子作业&lt;/td&gt;
&lt;td&gt;核心链路可在灾备集群跑影子任务，切换时补数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
Checkpoint 与 Savepoint 的区别：Checkpoint 是引擎自动、周期性、面向故障恢复的&quot;自动档&quot;；Savepoint 是人工、按需、面向版本升级与迁移的&quot;手刹&quot;。日常靠 Checkpoint，发布升级用 Savepoint。
:::&lt;/p&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;反压持续红色&lt;/strong&gt;：先用 BackPressure 页定位算子；Topology 图看哪个算子 input buffer 满，检查其并行度与外部依赖&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checkpoint 失败/超时&lt;/strong&gt;：&lt;code&gt;execution.checkpointing.timeout&lt;/code&gt; 太短、状态过大（增量未生效）、目标存储 IO 差；逐项排查，RocksDB 场景先开增量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TaskManager OOM&lt;/strong&gt;：堆内状态爆掉——RocksDB 作业没把状态挪到托管内存（&lt;code&gt;state.backend.rocksdb.memory.managed&lt;/code&gt; 未开）；或托管比例太小&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作业恢复后乱序严重&lt;/strong&gt;：重启策略与 checkpoint 对齐，&lt;code&gt;restart-strategy.failure-rate&lt;/code&gt; 的窗口期内连续失败会放弃恢复，注意窗口配置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态一直膨胀不收敛&lt;/strong&gt;：无 TTL 的 KeyedState（&lt;code&gt;state.ttl&lt;/code&gt; 未配置）或 Key 选择导致状态无法清理；业务状态上 TTL，避免无限增长&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;内存三分比例与状态后端匹配（RocksDB 则托管比例 0.6+）&lt;/li&gt;
&lt;li&gt;并行度 = 压测结果，不拍脑袋&lt;/li&gt;
&lt;li&gt;Checkpoint 30~120s + 增量 + HDFS 存储&lt;/li&gt;
&lt;li&gt;Savepoint 纳入发布流程&lt;/li&gt;
&lt;li&gt;反压、checkpoint 时长、状态大小纳入监控&lt;/li&gt;
&lt;li&gt;状态 Key 设计合理、TTL 就位&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>HDFS 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/hdfs-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/hdfs-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;HDFS 的调优思路只有一条主线：&lt;strong&gt;NameNode 是瓶颈，DataNode 是 IO&lt;/strong&gt;。NameNode 吃内存（元数据全在堆里），DataNode 吃磁盘与网络。所以调优时先算两笔账：集群的块数决定 NameNode 堆要多大，节点的磁盘吞吐决定副本数与块大小。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.blocksize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128M&lt;/td&gt;
&lt;td&gt;块越小元数据越多，NameNode 内存压力越大；大文件场景用 256M，小文件多则维持 128M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.replication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;副本数与容量成本成反比；3 副本 = 3 倍存储，2 副本在可靠性与成本间折中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.handler.count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;NameNode 处理 RPC 的线程数，官方公式 &lt;code&gt;20 × log2(节点数)&lt;/code&gt;，百节点集群约 130~200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.datanode.handler.count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;DataNode 处理 RPC 线程数，写密集场景调至 30~50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.datanode.max.transfer.threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4096&lt;/td&gt;
&lt;td&gt;数据传输线程上限，块读写并发高时调大（8192+）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.name.dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单目录&lt;/td&gt;
&lt;td&gt;元数据目录必须多目录/多磁盘，元数据是 HDFS 的命根子&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.replication.work.multiplier.per.iteration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;每轮复制块数，大量副本待补时调大加速恢复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fs.trash.interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0（关闭）&lt;/td&gt;
&lt;td&gt;开启回收站（如 1440 分钟），防误删&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;NameNode 堆内存估算&lt;/strong&gt;：每 100 万块（含副本）约占用 2~4GB 堆内存。100 节点的集群通常有数千万块，堆 32GB 起。块大小从 128M 提到 256M 能直接砍半元数据量。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G（3 节点起步）&lt;/th&gt;
&lt;th&gt;32C256G（中规模）&lt;/th&gt;
&lt;th&gt;64C512G（大规模）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NameNode 堆内存（HADOOP_NAMENODE_OPTS）&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;td&gt;32G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.blocksize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128M&lt;/td&gt;
&lt;td&gt;128M~256M&lt;/td&gt;
&lt;td&gt;256M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.replication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2~3&lt;/td&gt;
&lt;td&gt;3（有异地备份可 2）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.handler.count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.datanode.handler.count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.datanode.max.transfer.threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4096&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;16384&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.name.dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 个目录&lt;/td&gt;
&lt;td&gt;2~3 个目录分盘&lt;/td&gt;
&lt;td&gt;3+ 目录分盘 + 独立 JournalNode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM GC&lt;/td&gt;
&lt;td&gt;CMS&lt;/td&gt;
&lt;td&gt;G1&lt;/td&gt;
&lt;td&gt;G1 + 元数据预热&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
NameNode 堆调大后必须同步关注 GC：Full GC 超过 5 秒会导致 RPC 超时连锁反应。堆 ≥16G 时用 G1，&lt;code&gt;-XX:MaxGCPauseMillis=200&lt;/code&gt;，并通过 JVM 监控盯 Old 区增长。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;块数与堆内存联动&lt;/strong&gt;：定期用 &lt;code&gt;hdfs fsck / -blocks -files -locations&lt;/code&gt; 统计块数，堆内存按&quot;每 100 万块 2~4G&quot;滚动扩容；块数逼近上限时优先考虑调整 blocksize 或压缩文件数量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机架感知&lt;/strong&gt;：配置 &lt;code&gt;topology.script.file.name&lt;/code&gt;，让副本分布感知机架，写作业跨机架带宽可控，读取本地机架优先&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NameNode 与 DataNode 物理隔离&lt;/strong&gt;：大集群把 NN/JN/ZK 部署在专用节点（32C256G 起步），不与 DataNode 混部，避免磁盘 IO 互相干扰&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DataNode 多盘多目录&lt;/strong&gt;：每节点 4~8 块盘配 &lt;code&gt;dfs.datanode.data.dir&lt;/code&gt;，HDFS 自动做卷均衡（disk balancer），避免单盘打满&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小文件治理&lt;/strong&gt;：小文件（&amp;lt; blocksize/4）会成倍消耗 NN 内存，用归档（har）、合并（spark/hive 重写）或列式压缩表收敛&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NameNode 高可用（HA）&lt;/strong&gt;：双 NN + JournalNode（3 台）+ ZKFC，故障切换秒级；JournalNode 独立磁盘，日志目录多盘&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;副本均衡&lt;/strong&gt;：&lt;code&gt;hdfs balancer&lt;/code&gt; 定期执行（避开作业高峰），或启用自动 balancer&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;集群内&lt;/td&gt;
&lt;td&gt;HA + JournalNode&lt;/td&gt;
&lt;td&gt;双 NameNode 热备，元数据实时同步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;集群内&lt;/td&gt;
&lt;td&gt;快照&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hdfs dfsadmin -allowSnapshot&lt;/code&gt; + &lt;code&gt;hdfs dfs -createSnapshot&lt;/code&gt;，误删可回滚&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨集群&lt;/td&gt;
&lt;td&gt;FsImage 备份&lt;/td&gt;
&lt;td&gt;每日 &lt;code&gt;hdfs dfsadmin -fetchImage&lt;/code&gt; 拉取元数据镜像，rsync 到异地/对象存储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨集群&lt;/td&gt;
&lt;td&gt;数据级容灾&lt;/td&gt;
&lt;td&gt;核心目录定期 &lt;code&gt;distcp&lt;/code&gt; 到灾备集群；或上异地副本（&lt;code&gt;dfs.replication&lt;/code&gt; 无法跨机房，需 distcp 定时同步）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;回收站&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fs.trash.interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;开启 1440 分钟，防手滑删库删表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::caution
HDFS 的容灾铁律：&lt;strong&gt;元数据备份 = 数据备份&lt;/strong&gt;。只备份 DataNode 数据块而丢了 FsImage，等于数据全丢。FsImage + Edits 的双机、异地备份优先级最高。
:::&lt;/p&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NameNode Full GC 卡顿&lt;/strong&gt;：堆不足或元数据碎片化，先查块数是否符合估算，再考虑 G1 与大块&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入慢但磁盘空闲&lt;/strong&gt;：检查 &lt;code&gt;dfs.datanode.max.transfer.threads&lt;/code&gt; 是否被占满（&lt;code&gt;netstat&lt;/code&gt; 看连接数），客户端侧检查写入 pipeline 是否有慢节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;副本恢复慢&lt;/strong&gt;：&lt;code&gt;dfs.namenode.replication.work.multiplier.per.iteration&lt;/code&gt; 调大，同时确认 DataNode 的带宽参数 &lt;code&gt;dfs.datanode.balance.bandwidthPerSec&lt;/code&gt; 没被限死&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小文件导致 NN 内存告急&lt;/strong&gt;：先治理文件数量，再决定是否扩堆；治标（扩堆）不治本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;堆内存与块数匹配（每百万块 2~4G）&lt;/li&gt;
&lt;li&gt;name.dir 多目录分盘，data.dir 多盘&lt;/li&gt;
&lt;li&gt;handler 线程按公式配置&lt;/li&gt;
&lt;li&gt;blocksize 与文件大小匹配&lt;/li&gt;
&lt;li&gt;回收站开启、快照核心目录、FsImage 每日异地备份&lt;/li&gt;
&lt;li&gt;HA + balancer + fsck 巡检纳入日常&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Hive 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/hive-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/hive-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Hive 调优的抓手分三层：&lt;strong&gt;执行引擎&lt;/strong&gt;（MR/Tez/Spark 的选择）、&lt;strong&gt;单作业参数&lt;/strong&gt;（并行度、MapJoin、Reducer 数）、&lt;strong&gt;数仓建设&lt;/strong&gt;（分区、文件格式、小文件）。前两层是调参数，第三层是调习惯——数仓层的收益远大于参数层。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.execution.engine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;mr&lt;/td&gt;
&lt;td&gt;Tez 比 MR 快 2~3 倍（DAG 复用容器），Spark 更快但资源管理不同，生产主流 Tez&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.parallel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;无依赖 Stage 并行执行，多表 union/多 join 场景收益明显&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.auto.convert.join&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;小表自动转 MapJoin，避免 shuffle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.auto.convert.join.noconditionaltask.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10M&lt;/td&gt;
&lt;td&gt;MapJoin 小表阈值，内存充足时调到 100~512M，大表与小表 join 秒出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.reducers.bytes.per.reducer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;256M&lt;/td&gt;
&lt;td&gt;每个 Reducer 处理的数据量，控制 Reducer 数量 = 输出数据量 ÷ 该值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.tez.container.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512M&lt;/td&gt;
&lt;td&gt;Tez 容器内存，默认值过小，生产至少 2G 起步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.map.aggr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Map 端聚合，减少 shuffle 数据量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.dynamic.partition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;动态分区写入，注意 &lt;code&gt;hive.exec.max.dynamic.partitions&lt;/code&gt; 上限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.vectorized.execution.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;向量化执行，ORC 格式下提升明显&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.cbo.enable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;CBO 代价优化，join 顺序自动调整&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;Reducer 数不要手动写死&lt;/strong&gt;。正确做法是配 &lt;code&gt;hive.exec.reducers.bytes.per.reducer&lt;/code&gt;（256M），让引擎按数据量自动算。手动 &lt;code&gt;set mapred.reduce.tasks&lt;/code&gt; 写死会导致大任务输出大量小文件、小任务白开一堆 Reducer。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G（单机/小集群）&lt;/th&gt;
&lt;th&gt;32C256G（中规模）&lt;/th&gt;
&lt;th&gt;64C512G（大规模）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;执行引擎&lt;/td&gt;
&lt;td&gt;Tez&lt;/td&gt;
&lt;td&gt;Tez&lt;/td&gt;
&lt;td&gt;Tez / Spark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.tez.container.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.tez.java.opts&lt;/code&gt;（容器堆）&lt;/td&gt;
&lt;td&gt;1.5G&lt;/td&gt;
&lt;td&gt;3.5G&lt;/td&gt;
&lt;td&gt;7G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.reducers.bytes.per.reducer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128M&lt;/td&gt;
&lt;td&gt;256M&lt;/td&gt;
&lt;td&gt;256M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.auto.convert.join.noconditionaltask.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100M&lt;/td&gt;
&lt;td&gt;256M&lt;/td&gt;
&lt;td&gt;512M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.parallel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.parallel.thread.number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.mapred.mode&lt;/code&gt;（严格模式）&lt;/td&gt;
&lt;td&gt;strict（生产开）&lt;/td&gt;
&lt;td&gt;strict&lt;/td&gt;
&lt;td&gt;strict&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;code&gt;hive.tez.container.size&lt;/code&gt; 是 Hive 调优里收益最大的单个参数：默认 512M 意味着每个任务只有 512M 内存，SQL 稍复杂就 OOM 或疯狂溢写。但它必须与 YARN 的 &lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt; 匹配，容器申请超过上限会被拒绝。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数仓模型先行&lt;/strong&gt;：分区表（按天/小时分区）、分桶表（join 列分桶做 bucket join）、ORC/Parquet + 列裁剪，这些是百节点集群性能的根本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小文件治理三件套&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;写入侧：&lt;code&gt;hive.merge.mapfiles=true&lt;/code&gt;、&lt;code&gt;hive.merge.size.per.task=256M&lt;/code&gt;（合并 map 输出小文件）&lt;/li&gt;
&lt;li&gt;计算侧：Reducer 数量公式自动控制&lt;/li&gt;
&lt;li&gt;存量侧：定期用 INSERT OVERWRITE 重写合并小文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大表 join 策略&lt;/strong&gt;：小表进 MapJoin（阈值内）；中表用 bucket map join（&lt;code&gt;hive.optimize.bucketmapjoin&lt;/code&gt;）；大表 join 依赖 CBO + 列裁剪&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态分区注意&lt;/strong&gt;：&lt;code&gt;hive.exec.max.dynamic.partitions&lt;/code&gt;（默认 1000）不够时调大，同时 &lt;code&gt;hive.exec.max.created.files&lt;/code&gt; 防文件爆炸&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tez 容器复用&lt;/strong&gt;：Tez 在 DAG 内复用容器，&lt;code&gt;hive.tez.container.size&lt;/code&gt; 与 YARN 资源匹配后，无需手动控制并行度，交给 &lt;code&gt;hive.exec.reducers.bytes.per.reducer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存型参数纪律&lt;/strong&gt;：&lt;code&gt;hive.auto.convert.join.noconditionaltask.size&lt;/code&gt; 调大后要评估各节点 YARN 剩余内存，MapJoin 广播小表到所有 Map 容器，内存不够会 OOM&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metastore 高可用&lt;/strong&gt;：Metastore 多实例（共享 MySQL），实例故障业务无感；MySQL 本身主从 + 每日全量备份&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元数据库备份&lt;/strong&gt;：&lt;code&gt;mysqldump&lt;/code&gt; 每日备份 + binlog 增量，元数据丢了等于数仓目录全丢&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据层容灾&lt;/strong&gt;：warehouse 目录在 HDFS 上，依赖 HDFS 快照与 FsImage 备份（见 HDFS 调优）；核心表可 &lt;code&gt;distcp&lt;/code&gt; 到灾备集群&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作业编排恢复&lt;/strong&gt;：数仓调度（DolphinScheduler/Airflow）配置重跑机制，上游失败自动重试，下游补数窗口留足&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQL 一直卡在 Map 阶段&lt;/strong&gt;：数据倾斜（group by 热点 key），加 &lt;code&gt;hive.map.aggr.hash.percentmemory&lt;/code&gt; 或启用倾斜自动处理 &lt;code&gt;hive.groupby.skewindata&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reducer 全部 OOM&lt;/strong&gt;：&lt;code&gt;hive.exec.reducers.bytes.per.reducer&lt;/code&gt; 过小导致 Reducer 数爆炸，或单个 key 数据量巨大（倾斜）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写表巨慢且小文件多&lt;/strong&gt;：动态分区键粒度过细或 Reducer 数失控，收敛分区键粒度、确认 reducer 公式生效&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tez 容器超 YARN 上限被拒&lt;/strong&gt;：&lt;code&gt;hive.tez.container.size&lt;/code&gt; + 系统开销超过 &lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt;，两者联动调整&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同一条 SQL 快慢差异巨大&lt;/strong&gt;：数据量波动 + 无统计信息，执行 &lt;code&gt;ANALYZE TABLE ... COMPUTE STATISTICS&lt;/code&gt; 让 CBO 有据可依&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;引擎 Tez，&lt;code&gt;hive.tez.container.size&lt;/code&gt; ≥2G 且与 YARN 上限匹配&lt;/li&gt;
&lt;li&gt;数仓模型：分区、分桶、ORC、列裁剪&lt;/li&gt;
&lt;li&gt;Reducer 数交给公式，不手动写死&lt;/li&gt;
&lt;li&gt;MapJoin 阈值按内存余量调&lt;/li&gt;
&lt;li&gt;小文件治理例行化（merge + 定期重写）&lt;/li&gt;
&lt;li&gt;统计信息更新纳入调度，Metastore 与 MySQL 双备份&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Kafka 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/kafka-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/kafka-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Kafka 调优的本质是&lt;strong&gt;吞吐与可靠的权衡&lt;/strong&gt;：Producer 侧（batch、压缩、acks）、Broker 侧（IO 线程、分段、保留策略）、Consumer 侧（拉取、提交、分区并行）。Kafka 最大的性能秘密在&lt;strong&gt;页缓存&lt;/strong&gt;——它不靠 JVM 堆缓存数据，所以堆别给太大，钱要花在磁盘和内存页缓存上。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;h3&gt;Producer 侧&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;acks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;all（3.x 默认）&lt;/td&gt;
&lt;td&gt;1 吞吐高但可能丢；all + &lt;code&gt;min.insync.replicas=2&lt;/code&gt; 是生产标配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;batch.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16384（16K）&lt;/td&gt;
&lt;td&gt;单批消息上限，调大（如 64K）减少网络往返，吞吐提升明显&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;linger.ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;攒批等待时间，调到 5~20ms 让批凑满，延迟换吞吐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compression.type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;开 snappy/lz4，CPU 换网络，大数据量场景收益巨大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;buffer.memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;33554432（32M）&lt;/td&gt;
&lt;td&gt;Producer 缓冲池，突发流量别让缓冲打满报异常&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2147483647&lt;/td&gt;
&lt;td&gt;重试次数，配合 &lt;code&gt;enable.idempotence=true&lt;/code&gt; 幂等不重复&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Broker 侧&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num.network.threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;网络线程，8C 机器 4~8，大集群按核数 1/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num.io.threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;IO 线程（磁盘读写），16 核机器 8~16，写密集调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.segment.bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1073741824（1G）&lt;/td&gt;
&lt;td&gt;日志分段大小，影响索引粒度与清理粒度，一般 1G 不动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.retention.hours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;168（7 天）&lt;/td&gt;
&lt;td&gt;保留时长，与磁盘容量、下游消费节奏匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.retention.bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-1&lt;/td&gt;
&lt;td&gt;按容量保留（如每 topic 500G），防止磁盘写满&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.flush.interval.messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;消息数触发刷盘；追求低丢失可调小，一般交给 OS 刷盘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min.insync.replicas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;ISR 最小副本数=2 时，acks=all 才真正不丢&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;为什么 Kafka 堆不用大&lt;/strong&gt;：数据读写走 OS 页缓存（page cache），JVM 堆只放元数据与索引。堆给 4~8G 足够，把内存留给页缓存，热数据命中在内存，IO 骤降。&lt;strong&gt;堆设 32G 反而 Full GC 拖垮吞吐&lt;/strong&gt;。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G&lt;/th&gt;
&lt;th&gt;32C256G&lt;/th&gt;
&lt;th&gt;64C512G&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Broker 堆（KAFKA_HEAP_OPTS）&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;td&gt;8G（省内存给页缓存）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num.network.threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num.io.threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.segment.bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.retention.hours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;72~168&lt;/td&gt;
&lt;td&gt;按容量（&lt;code&gt;log.retention.bytes&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min.insync.replicas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log.dirs&lt;/code&gt; 盘数&lt;/td&gt;
&lt;td&gt;1~2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer &lt;code&gt;batch.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32K&lt;/td&gt;
&lt;td&gt;64K&lt;/td&gt;
&lt;td&gt;64K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer &lt;code&gt;linger.ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;strong&gt;副本数与磁盘是硬约束&lt;/strong&gt;：&lt;code&gt;default.replication.factor=3&lt;/code&gt; 意味着 3 倍磁盘占用。64C512G 的机器配 8 块盘时，单 broker 磁盘吞吐约等于 8 块盘的 RAID0 带宽，分区副本再翻倍后，每分区实际可用带宽会骤降——规划吞吐时先算盘，再算副本。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分区数规划&lt;/strong&gt;：单分区吞吐约 5~20MB/s（取决于磁盘），目标吞吐 ÷ 单分区吞吐 = 分区数；分区总数除以 broker 数让副本均衡；&lt;strong&gt;分区数宁多勿少的教训&lt;/strong&gt;：后期扩分区只加不减，且扩容时 consumer 重平衡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机架感知&lt;/strong&gt;：&lt;code&gt;broker.rack&lt;/code&gt; 配置，副本跨机架放置，机架级故障不丢数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumer 消费力&lt;/strong&gt;：分区数 ≥ 消费者数（同组内），lag 监控（Kafka Lag Exporter / dashboard）是必装项；重平衡风暴（频繁 rebalance）用 &lt;code&gt;partition.assignment.strategy&lt;/code&gt; 选 CooperativeSticky 减少抖动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔离部署&lt;/strong&gt;：Kafka 与 Hadoop 作业混部会互相抢磁盘 IO；生产独立 broker 节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控三件套&lt;/strong&gt;：UnderReplicatedPartitions（副本落后）、BytesInPerSec（吞吐水位）、消费 lag；异常联动告警&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;集群内&lt;/td&gt;
&lt;td&gt;副本因子 3 + &lt;code&gt;min.insync.replicas=2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单节点故障无感，写入不丢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨机房&lt;/td&gt;
&lt;td&gt;MirrorMaker 2&lt;/td&gt;
&lt;td&gt;主集群 → 灾备集群异步镜像，消费侧可切换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据&lt;/td&gt;
&lt;td&gt;Topic 导出备份&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kafka-dump-log&lt;/code&gt; / Kafka Connect S3/HDFS 归档，满足重放需求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;变更&lt;/td&gt;
&lt;td&gt;配置审计&lt;/td&gt;
&lt;td&gt;topic 的 retention/副本配置纳入版本管理，避免误改&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;吞吐上不去但 CPU 不高&lt;/strong&gt;：网络或页缓存瓶颈——检查 &lt;code&gt;linger.ms&lt;/code&gt;/&lt;code&gt;batch.size&lt;/code&gt; 是否生效、网卡是否跑满；生产端压缩开启&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Not enough replicas&lt;/code&gt; / 写入超时&lt;/strong&gt;：ISR 不足（&lt;code&gt;min.insync.replicas&lt;/code&gt; 未满足）或 &lt;code&gt;acks=all&lt;/code&gt; 下副本落后；查 UnderReplicatedPartitions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消费 lag 持续增长&lt;/strong&gt;：消费者线程数 &amp;lt; 分区数、单条消息处理慢、重平衡频繁；先看 lag 分布定位是整体还是单分区（数据倾斜）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘写满&lt;/strong&gt;：&lt;code&gt;log.retention.bytes&lt;/code&gt; 未配 + 业务无脑设大 retention；按 topic 容量上限管控&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rebalance 风暴&lt;/strong&gt;：心跳超时（&lt;code&gt;session.timeout.ms&lt;/code&gt;）或消费者进出频繁；调大 session.timeout、用 CooperativeSticky 分配策略&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Producer：batch + linger + 压缩 + acks=all + 幂等&lt;/li&gt;
&lt;li&gt;Broker：堆 4~8G（页缓存优先）、IO 线程按核数&lt;/li&gt;
&lt;li&gt;副本因子 3 + ISR=2&lt;/li&gt;
&lt;li&gt;分区数按吞吐规划，lag 监控必装&lt;/li&gt;
&lt;li&gt;跨机房 MirrorMaker2（核心链路）&lt;/li&gt;
&lt;li&gt;磁盘容量与 retention 管控&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Kerberos 部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/10/kerberos-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/kerberos-deploy-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Kerberos 是 Hadoop 生态（HDFS/YARN/Hive/Kafka 等）的标准认证协议：客户端先向 KDC 拿票据，再用票据访问服务，全程不传密码。部署的核心是 &lt;strong&gt;KDC 服务 + 每个组件的 principal/keytab&lt;/strong&gt;；调优的核心是&lt;strong&gt;票据生命周期与时钟同步&lt;/strong&gt;。本文覆盖原理、部署、参数优化与常见问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理&lt;/h2&gt;
&lt;h3&gt;认证流程（两次票据交换）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① AS-REQ    客户端 → KDC（AS 服务）：用密码加密请求
② AS-REP    客户端 ← KDC：TGT（票据授权票据，密码解密）
③ TGS-REQ   客户端 → KDC（TGS 服务）：TGT + 目标服务名
④ TGS-REP   客户端 ← KDC：服务票据 ST（目标服务密钥加密）
⑤ AP-REQ    客户端 → 服务端：ST + 认证信息
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Principal&lt;/strong&gt;：身份标识，格式 &lt;code&gt;user/instance@REALM&lt;/code&gt;，如 &lt;code&gt;hdfs/node1@BLOG.COM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keytab&lt;/strong&gt;：principal 的密钥文件（等价密码），服务进程用它免交互认证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TGT&lt;/strong&gt;：登录凭证，默认有效期 24 小时（&lt;code&gt;ticket_lifetime&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时钟同步&lt;/strong&gt;：Kerberos 票据校验时间戳，客户端与 KDC 时钟偏差超过默认 300 秒直接失败&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
&lt;strong&gt;一句话理解&lt;/strong&gt;：Kerberos 把&quot;密码验证&quot;变成&quot;票据验证&quot;。第一次登录向 KDC 要 TGT（验证密码），之后所有服务访问都用 TGT 换 ST，服务端只认 KDC 签发的票据，不再接触密码。
:::&lt;/p&gt;
&lt;h2&gt;部署（KDC + Hadoop 集成）&lt;/h2&gt;
&lt;h3&gt;1. 安装 KDC（独立节点）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# CentOS/RHEL
yum install -y krb5-server krb5-workstation
# 配置 /etc/krb5.conf（所有客户端同样配置）
cat &amp;gt;&amp;gt; /etc/krb5.conf &amp;lt;&amp;lt;&apos;EOF&apos;
[libdefaults]
  default_realm = BLOG.COM
  dns_lookup_realm = false
  dns_lookup_kdc = false
  ticket_lifetime = 24h
  renew_lifetime = 7d
  forwardable = true
[realms]
  BLOG.COM = {
    kdc = kdc-server
    admin_server = kdc-server
  }
[domain_realm]
  .blog.com = BLOG.COM
  blog.com = BLOG.COM
EOF

# 初始化数据库并启动
kdb5_util create -s -r BLOG.COM        # 输入 KDC 主密码
systemctl start krb5kdc &amp;amp;&amp;amp; systemctl start kadmin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 创建 principal 与 keytab&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kadmin.local
# 服务 principal（每组件每节点）
addprinc -randkey hdfs/node1@BLOG.COM
addprinc -randkey hdfs/node2@BLOG.COM
# 导出 keytab 到节点（分发到对应机器）
xst -k /etc/security/keytabs/hdfs.keytab hdfs/node1@BLOG.COM
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Hadoop 开启认证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- core-site.xml --&amp;gt;
&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;hadoop.security.authentication&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;kerberos&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;hadoop.security.authorization&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;true&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;!-- hdfs-site.xml：DataNode 通信认证 --&amp;gt;
&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;dfs.namenode.keytab.file&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;/etc/security/keytabs/hdfs.keytab&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 客户端认证方式一：kinit（交互/定时续票）
kinit -kt /etc/security/keytabs/hdfs.keytab hdfs/node1@BLOG.COM

# 方式二：作业提交通道
# 普通用户 kinit 后提交作业，或使用 Hadoop 代理用户（proxyuser）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数优化（三档规格）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;中小集群（&amp;lt;50 节点）&lt;/th&gt;
&lt;th&gt;中大型（50~200）&lt;/th&gt;
&lt;th&gt;超大型（200+）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ticket_lifetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;24h&lt;/td&gt;
&lt;td&gt;24h（服务端）+ 客户端续票&lt;/td&gt;
&lt;td&gt;24h + 自动续期脚本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;renew_lifetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7d&lt;/td&gt;
&lt;td&gt;14d&lt;/td&gt;
&lt;td&gt;30d（配合自动 kinit 定时任务）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxrenewlife&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7d&lt;/td&gt;
&lt;td&gt;14d&lt;/td&gt;
&lt;td&gt;30d&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clockskew&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;300s&lt;/td&gt;
&lt;td&gt;300s&lt;/td&gt;
&lt;td&gt;300s（配合 NTP 保准）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KDC 规格&lt;/td&gt;
&lt;td&gt;8C16G&lt;/td&gt;
&lt;td&gt;16C32G&lt;/td&gt;
&lt;td&gt;32C64G 或双 KDC 主备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kdc_max_dgram_reply_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2048&lt;/td&gt;
&lt;td&gt;4096&lt;/td&gt;
&lt;td&gt;8192（大票据/UDP 限制）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;strong&gt;长作业 vs 票据过期&lt;/strong&gt;：Spark/Flink 长任务运行时票据过期（&lt;code&gt;Ticket expired&lt;/code&gt;）是经典故障。方案：客户端 &lt;code&gt;renew_lifetime&lt;/code&gt; 覆盖作业时长 + 定时 &lt;code&gt;kinit -R&lt;/code&gt; 续票；或开启 Hadoop 的令牌（delegation token）自动刷新机制。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化理解&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ticket_lifetime&lt;/code&gt; 与安全是反比：24h 兼顾可用与风险；安全要求高的场景缩到 8~12h，但必须配套自动续票，否则运维事故频发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clockskew&lt;/code&gt; 放宽会弱化重放攻击防护，&lt;strong&gt;优先保 NTP 而不是放宽 clockskew&lt;/strong&gt;——NTP 偏差控制在 1 分钟内是硬性运维指标&lt;/li&gt;
&lt;li&gt;KDC 是&lt;strong&gt;同步瓶颈&lt;/strong&gt;：集群认证风暴（全部节点同时重启）时 KDC 单点 CPU/网络飙高，双 KDC（主备）或独立 VIP 是超大规模标配&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;集群规模优化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时钟同步是地基&lt;/strong&gt;：所有节点 NTP/chrony 统一时钟，偏差 &amp;gt;300s 一切认证失败；KDC 自身也加入时间源&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;keytab 管理&lt;/strong&gt;：keytab 按主机分发、权限 400、root 属主；定期轮换（&lt;code&gt;xst -norandkey&lt;/code&gt; 不换密钥，&lt;code&gt;-randkey&lt;/code&gt; 换密钥需重发）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代理用户（proxyuser）&lt;/strong&gt;：提交作业统一走调度平台账号 + 代理用户，避免人人在节点上 kinit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;令牌（delegation token）&lt;/strong&gt;：HDFS/YARN 长作业用 DT 免 keytab，配置 &lt;code&gt;hadoop.security.token.service.use_ip&lt;/code&gt; 与刷新周期&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka/Ranger 联动&lt;/strong&gt;：Kafka 开 SASL/GSSAPI 复用同一 KDC；Ranger 与 Kerberos 双认证共存时注意票据传递链&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排障工具链&lt;/strong&gt;：&lt;code&gt;klist&lt;/code&gt;（看票据）、&lt;code&gt;kadmin&lt;/code&gt;（查 principal）、KDC 日志（/var/log/krb5kdc.log）定位认证失败&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Clock skew too great (X &amp;gt; 300 seconds)&lt;/code&gt;&lt;/strong&gt;：时钟偏差——先 NTPdate 校准所有节点，别急着放宽 clockskew&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Ticket expired&lt;/code&gt;&lt;/strong&gt;：票据过期——kinit 续票或刷新 delegation token；长作业必须规划续期&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Server not found in Kerberos database&lt;/code&gt;&lt;/strong&gt;：principal 拼写/格式错误，或实例名与节点 hostname 不一致&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Decrypt integrity check failed&lt;/code&gt;&lt;/strong&gt;：keytab 与 KDC 密钥不一致（换过密码/keytab 损坏）——重新 &lt;code&gt;addprinc -randkey&lt;/code&gt; + 导出&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;kinit: Client not found&lt;/code&gt;&lt;/strong&gt;：principal 不存在；确认 realm 大小写（REALM 惯例全大写）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开启 Kerberos 后 HDFS 报权限失败&lt;/strong&gt;：认证通过但授权失败——检查用户映射（&lt;code&gt;hadoop.security.auth_to_local&lt;/code&gt; 规则）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;部署检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;全节点时钟同步（NTP）——Kerberos 的地基&lt;/li&gt;
&lt;li&gt;KDC 双机（超大规模），keytab 权限 400&lt;/li&gt;
&lt;li&gt;ticket_lifetime/renew_lifetime 覆盖作业时长，续票自动化&lt;/li&gt;
&lt;li&gt;delegation token 用于长作业，proxyuser 收敛账号&lt;/li&gt;
&lt;li&gt;klist/kadmin/KDC 日志排障工具齐备&lt;/li&gt;
&lt;li&gt;故障演练：KDC 主备切换、票据过期恢复&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Kubernetes 底层原理</title><link>https://chaggle.github.io/posts/2026/08/10/kubernetes-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/kubernetes-principles/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Kubernetes（K8S）是容器编排的事实标准。理解它的底层原理，记住一条主线就够：&lt;strong&gt;声明式 API + 控制器循环&lt;/strong&gt;——用户声明&quot;我要 3 个副本&quot;，控制器不停地对比&quot;现状&quot;与&quot;期望&quot;并拉齐。本文梳理控制面、数据面、调度、网络、存储五大块。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;整体架构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌─ 控制面（Master）────────────────────────┐
│  kube-apiserver（唯一入口，RESTful API）   │
│  etcd（集群状态存储，共识层）              │
│  kube-scheduler（调度器）                 │
│  controller-manager（控制器们）           │
└──────────────────────────────────────────┘
            │ API
┌───────────▼──────────────────────────────┐
│  数据面（Worker 节点）                    │
│  kubelet（节点代理，管 Pod 生命周期）       │
│  kube-proxy（Service 流量转发）           │
│  容器运行时（containerd / CRI-O）         │
│  网络插件 CNI（Calico / Flannel）         │
└──────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;kube-apiserver&lt;/strong&gt;：所有组件与用户操作的唯一入口，一切请求走 API；etcd 只允许 apiserver 读写&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;etcd&lt;/strong&gt;：集群&quot;真相&quot;存储（期望状态 + 当前状态），选主用 Raft 共识；性能瓶颈在磁盘 IO（fsync）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;controller-manager&lt;/strong&gt;：一堆控制器（Deployment/Node/Namespace...），核心模式是&quot;期望 vs 现状&quot;的调谐循环&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;kubelet&lt;/strong&gt;：每个节点的代理，监听 apiserver 下发的 Pod 定义，驱动容器运行时创建/销毁容器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;kube-proxy&lt;/strong&gt;：实现 Service 的虚拟 IP 转发（iptables 或 IPVS 模式）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
&lt;strong&gt;控制器的哲学&lt;/strong&gt;：所有对象都有一个 &lt;code&gt;spec&lt;/code&gt;（期望）和 &lt;code&gt;status&lt;/code&gt;（现状）。控制器循环执行：读现状 → 对比期望 → 执行动作 → 更新 status。Pod 被删了？控制器发现&quot;现状少一个&quot;，立即补一个——这就是&quot;自愈&quot;的来源。
:::&lt;/p&gt;
&lt;h2&gt;核心对象&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;关键点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pod&lt;/td&gt;
&lt;td&gt;最小调度单位（一个或多个容器）&lt;/td&gt;
&lt;td&gt;同 Pod 共享网络与存储卷&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;无状态应用的副本管理&lt;/td&gt;
&lt;td&gt;滚动更新、回滚、自愈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StatefulSet&lt;/td&gt;
&lt;td&gt;有状态应用（数据库/中间件）&lt;/td&gt;
&lt;td&gt;稳定网络标识（序号）、有序部署&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DaemonSet&lt;/td&gt;
&lt;td&gt;每个节点跑一个（监控/日志）&lt;/td&gt;
&lt;td&gt;节点级守护&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service&lt;/td&gt;
&lt;td&gt;稳定的访问入口（ClusterIP）&lt;/td&gt;
&lt;td&gt;负载均衡到一组 Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingress&lt;/td&gt;
&lt;td&gt;七层 HTTP 路由（域名→Service）&lt;/td&gt;
&lt;td&gt;外部访问入口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ConfigMap/Secret&lt;/td&gt;
&lt;td&gt;配置与敏感信息&lt;/td&gt;
&lt;td&gt;Secret 等保存储（etcd 加密）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PV/PVC&lt;/td&gt;
&lt;td&gt;持久化存储抽象&lt;/td&gt;
&lt;td&gt;存储与使用分离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HPA&lt;/td&gt;
&lt;td&gt;按指标自动扩缩副本&lt;/td&gt;
&lt;td&gt;CPU/自定义指标&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;调度原理&lt;/h2&gt;
&lt;p&gt;kube-scheduler 两步走：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;过滤（Filter）&lt;/strong&gt;：剔除不满足条件的节点（资源不足、污点不匹配、端口冲突）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打分（Score）&lt;/strong&gt;：剩余节点按策略打分（资源余量、亲和性、拓扑分布），选最高分&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;调度结果记录在 Pod 的 &lt;code&gt;nodeName&lt;/code&gt; 上，kubelet 看到后开始创建容器。调度器可扩展（自定义 scheduler extender）。&lt;/p&gt;
&lt;h2&gt;网络模型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pod 网络&lt;/strong&gt;：每个 Pod 一个唯一 IP，Pod 间直接通信（跨节点），由 CNI 插件实现
&lt;ul&gt;
&lt;li&gt;Calico：BGP 路由分发，性能好，大集群主流&lt;/li&gt;
&lt;li&gt;Flannel：VXLAN/overlay 封装，简单&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service 网络&lt;/strong&gt;：ClusterIP 是虚拟 IP，kube-proxy 通过 iptables/IPVS 规则把流量转发给后端 Pod（依赖 EndpointSlice）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS&lt;/strong&gt;：CoreDNS 解析 Service 名（&lt;code&gt;svc.namespace.svc.cluster.local&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
&lt;strong&gt;Pod IP 不稳定的问题&lt;/strong&gt;：Pod 重建 IP 就变，所以业务访问不直连 Pod，而是通过 Service 的稳定虚拟 IP——Service 背后挂一组 Pod，Pod 增减自动更新 Endpoint。
:::&lt;/p&gt;
&lt;h2&gt;存储原理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PV（持久卷）&lt;/strong&gt;：管理员准备的存储（NFS、本地盘、云盘），对应 CSI 插件接入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PVC（持久卷声明）&lt;/strong&gt;：用户申请&quot;我要 10G&quot;，控制器把 PVC 绑定到合适的 PV&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有状态应用&lt;/strong&gt;：StatefulSet 每个副本绑定独立 PVC，删除重建后数据仍在（如 Kafka/ZK/MySQL 的 K8S 化）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见架构误区&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;etcd 与 apiserver 合部&lt;/strong&gt;：小集群可合部，生产分离；etcd 用 SSD、独立部署、3/5 节点奇数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;控制面与业务混部&lt;/strong&gt;：Worker 节点跑业务与控制面组件混在一起，etcd/调度抖动会拖垮业务；生产控制面独立节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不用网络策略&lt;/strong&gt;：K8S 默认网络平面全通，生产必须 NetworkPolicy（Calico 实现）收敛东西向流量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pod 无资源限制&lt;/strong&gt;：不写 requests/limits 的 Pod 可以吃满节点，见《Kubernetes 部署参数优化》&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;学习路径建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;先玩熟 kubectl 与资源清单（YAML 声明式）&lt;/li&gt;
&lt;li&gt;动手验证控制器原理（删 Pod 观察自动重建）&lt;/li&gt;
&lt;li&gt;网络：从 Service 转发到 CNI 再到 NetworkPolicy&lt;/li&gt;
&lt;li&gt;存储：PV/PVC 与 StatefulSet 落地一个中间件&lt;/li&gt;
&lt;li&gt;进阶：调度策略、多集群、Operator 模式&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Kubernetes 常见问题排查</title><link>https://chaggle.github.io/posts/2026/08/10/kubernetes-faq/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/kubernetes-faq/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;K8S 排障有固定套路：&lt;strong&gt;先看事件，再看日志，最后看资源状态&lt;/strong&gt;。绝大多数问题能通过 &lt;code&gt;kubectl describe&lt;/code&gt;、&lt;code&gt;kubectl logs&lt;/code&gt;、&lt;code&gt;kubectl get events&lt;/code&gt; 三步定位。本文按&quot;Pod 起不来 → 网络不通 → 存储异常 → 控制面异常&quot;的顺序整理高频问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、Pod 层问题&lt;/h2&gt;
&lt;h3&gt;1. Pod 一直 Pending&lt;/h3&gt;
&lt;p&gt;排查路径：&lt;code&gt;kubectl describe pod xxx&lt;/code&gt; 看 Events。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;事件&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;解法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0/1 nodes are available&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;节点资源不足（CPU/内存）&lt;/td&gt;
&lt;td&gt;调小 requests 或加节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node(s) had taint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;节点有污点（如专用节点）&lt;/td&gt;
&lt;td&gt;确认 Pod 是否容忍；业务不该跑在控制面节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pod has unbound PersistentVolumeClaims&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PVC 没有可绑定的 PV&lt;/td&gt;
&lt;td&gt;检查存储类与 PV 供给&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed to fit in any node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;端口冲突/亲和性不满足&lt;/td&gt;
&lt;td&gt;看详细拒绝原因&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. CrashLoopBackOff（反复重启）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl logs pod xxx --previous   # 看上次退出日志
kubectl get pod -o yaml           # 看 Last State 与退出码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见原因：启动参数错误、配置文件缺失（ConfigMap 未挂载）、探针（liveness）失败被反复杀掉、OOMKilled（看退出码 137）。&lt;/p&gt;
&lt;p&gt;:::warning
退出码 137 = 被 OOM Kill；128+signal 系列要先查内存。常见误解：只看日志不看退出码，把 OOM 当业务 bug 排查，浪费半天。
:::&lt;/p&gt;
&lt;h3&gt;3. ImagePullBackOff&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;镜像不存在/标签写错：&lt;code&gt;kubectl describe&lt;/code&gt; 有 &lt;code&gt;ErrImagePull&lt;/code&gt; 详情&lt;/li&gt;
&lt;li&gt;私有仓库认证失败：配置 imagePullSecrets&lt;/li&gt;
&lt;li&gt;节点无法访问镜像仓库：内网环境需配置 mirror 或离线导入&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. Running 但不健康（探针问题）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;readiness 失败：流量不转发但 Pod 在跑；查就绪探针路径与端口&lt;/li&gt;
&lt;li&gt;liveness 失败：被杀重启——探针超时设置不合理（如 Java 启动慢，initialDelay 太短）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、网络层问题&lt;/h2&gt;
&lt;h3&gt;1. Service 访问不通&lt;/h3&gt;
&lt;p&gt;排查顺序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get endpoints &amp;lt;svc&amp;gt;      # 1. 后端 Pod 是否有 Endpoint
kubectl get pods -o wide         # 2. Pod 是否 Ready、IP 是否正确
kubectl exec &amp;lt;pod&amp;gt; -- curl &amp;lt;ClusterIP&amp;gt;:&amp;lt;port&amp;gt;   # 3. 集群内直连测试
kubectl describe svc &amp;lt;svc&amp;gt;       # 4. 选择器与标签是否匹配
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
&lt;code&gt;Endpoints 为空&lt;/code&gt;是最常见的 Service 不通原因——Service 的 selector 与 Pod 的 labels 不一致，流量没有后端可转发。
:::&lt;/p&gt;
&lt;h3&gt;2. Pod 间跨节点不通&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CNI 插件状态：&lt;code&gt;kubectl get pods -n kube-system&lt;/code&gt; 看 calico/flannel Pod&lt;/li&gt;
&lt;li&gt;节点防火墙（iptables 规则被清空或塞满）&lt;/li&gt;
&lt;li&gt;大集群 Calico 的 BGP 对端数过多导致路由收敛慢&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. DNS 解析失败&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CoreDNS Pod 是否 Running、&lt;code&gt;kubectl -n kube-system logs -l k8s-app=kube-dns&lt;/code&gt; 看报错&lt;/li&gt;
&lt;li&gt;Pod 的 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 是否指向 CoreDNS&lt;/li&gt;
&lt;li&gt;自定义域名需 ConfigMap 配置 ndots/search 域&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 节点端口访问（NodePort）不通&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;NodePort 端口区间（30000~32767）确认、宿主机防火墙放行&lt;/li&gt;
&lt;li&gt;kube-proxy 是否运行正常（iptables/IPVS 规则生成）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三、存储层问题&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;解法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PVC Pending&lt;/td&gt;
&lt;td&gt;存储类不存在或 PV 供给失败&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl get storageclass&lt;/code&gt;、查 CSI 插件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod 卡 Terminating&lt;/td&gt;
&lt;td&gt;挂载卷卸载不掉（NFS 断连）&lt;/td&gt;
&lt;td&gt;强制删除（&lt;code&gt;--grace-period=0 --force&lt;/code&gt;）后清理节点挂载点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据丢失&lt;/td&gt;
&lt;td&gt;本地盘 PV 节点故障&lt;/td&gt;
&lt;td&gt;本地盘只能单节点，关键数据用网络存储或副本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;挂载慢/超时&lt;/td&gt;
&lt;td&gt;CSI 插件与存储后端 IO 高&lt;/td&gt;
&lt;td&gt;查存储集群与 CSI 日志&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;四、控制面问题&lt;/h2&gt;
&lt;h3&gt;1. apiserver 不可用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;etcd 健康：&lt;code&gt;etcdctl endpoint health&lt;/code&gt;；etcd 磁盘慢/满 → 整个集群&quot;假死&quot;&lt;/li&gt;
&lt;li&gt;证书过期：K8S 1.20 后证书 1 年，&lt;code&gt;kubeadm certs renew all&lt;/code&gt; 或升级&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 节点 NotReady&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl get nodes
kubectl describe node &amp;lt;node&amp;gt;     # 看 Ready 条件与最近心跳
journalctl -u kubelet -f         # kubelet 日志
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见：kubelet 挂掉、磁盘满（&lt;code&gt;--eviction-hard&lt;/code&gt; 驱逐）、docker/containerd 挂掉、节点网络到 apiserver 不通。&lt;/p&gt;
&lt;h3&gt;3. 集群证书问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Unable to connect to the server: x509: certificate has expired&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;kubeadm 集群：&lt;code&gt;kubeadm certs check-expiration&lt;/code&gt; 检查，&lt;code&gt;kubeadm certs renew all&lt;/code&gt; 续期并滚动重启组件&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;五、通用排障方法论&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;看事件&lt;/strong&gt;：&lt;code&gt;kubectl describe&lt;/code&gt; 的 Events 是排障起点，90% 的问题在这里给出原因&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;看日志&lt;/strong&gt;：Pod 内多个容器用 &lt;code&gt;-c 容器名&lt;/code&gt;；崩溃看 &lt;code&gt;--previous&lt;/code&gt;；中间件看节点组件日志（kubelet/CNI）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;看资源&lt;/strong&gt;：&lt;code&gt;kubectl top node/pod&lt;/code&gt;（需 metrics-server）判断资源水位&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复现思维&lt;/strong&gt;：临时起一个测试 Pod（&lt;code&gt;kubectl run -it --image=busybox&lt;/code&gt;）在集群内验证网络与 DNS，把&quot;环境问题&quot;和&quot;业务问题&quot;分开&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记录复盘&lt;/strong&gt;：每次事故沉淀成 runbook，排障手册是团队最值钱的资产&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Kubernetes 部署参数优化</title><link>https://chaggle.github.io/posts/2026/08/10/kubernetes-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/kubernetes-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;K8S 部署优化分两层：&lt;strong&gt;集群层&lt;/strong&gt;（控制面规格、etcd 磁盘、系统参数）与&lt;strong&gt;工作负载层&lt;/strong&gt;（requests/limits、调度策略、扩缩容）。本文给出 8C16G / 32C256G / 64C512G 三种节点规格下的部署建议，以及大数据/中间件组件容器化的关键参数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;集群层优化&lt;/h2&gt;
&lt;h3&gt;节点规格规划&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;角色&lt;/th&gt;
&lt;th&gt;规格&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;控制面（etcd+apiserver+scheduler+cm）&lt;/td&gt;
&lt;td&gt;8C16G 起步 ×3&lt;/td&gt;
&lt;td&gt;etcd 独立 SSD；apiserver 内存随规模涨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker（业务）&lt;/td&gt;
&lt;td&gt;8C16G / 32C256G / 64C512G&lt;/td&gt;
&lt;td&gt;按业务类型混合：内存型与计算型分开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;存储节点&lt;/td&gt;
&lt;td&gt;高磁盘密度&lt;/td&gt;
&lt;td&gt;跑分布式存储（Ceph/Longhorn）或大数据本地盘&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;控制面参数&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-requests-inflight&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;apiserver 并发上限&lt;/td&gt;
&lt;td&gt;默认 400，大集群调 1000+，配合 &lt;code&gt;--max-mutating-requests-inflight&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--request-timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;请求超时&lt;/td&gt;
&lt;td&gt;默认 60s；大对象（LIST 全量）可调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kube-apiserver --enable-aggregator-routing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;聚合器路由&lt;/td&gt;
&lt;td&gt;依部署环境开启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;etcd &lt;code&gt;--quota-backend-bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;后端配额&lt;/td&gt;
&lt;td&gt;默认 2G，大集群 8~16G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubelet &lt;code&gt;--max-pods&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单节点 Pod 上限&lt;/td&gt;
&lt;td&gt;默认 110；大数据节点按实际调低&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;etcd 是集群的性能底座&lt;/strong&gt;：所有状态读写都要过它，磁盘 fsync 延迟直接决定 apiserver 响应。etcd 节点必须 SSD/NVMe，&lt;code&gt;disk.latency&lt;/code&gt; 监控；数据盘与系统盘分离。
:::&lt;/p&gt;
&lt;h3&gt;节点系统参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 内核参数（kubelet 前置）
sysctl -w vm.max_map_count=262144          # 大数据组件（ES/HBase 类）需要
sysctl -w fs.file-max=1048576
sysctl -w net.core.somaxconn=65535

# kubelet 预留资源：防止系统组件被业务 Pod 挤死
kubelet --system-reserved=cpu=500m,memory=1Gi \
        --kube-reserved=cpu=500m,memory=1Gi \
        --eviction-hard=memory.available&amp;lt;500Mi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;工作负载层优化&lt;/h2&gt;
&lt;h3&gt;1. requests / limits 是基本功&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;resources:
  requests:          # 调度依据（分配保证）
    cpu: 500m
    memory: 1Gi
  limits:            # 运行上限（超限被限流/OOM）
    cpu: 1
    memory: 2Gi
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;requests 决定调度&lt;/strong&gt;，写大 = 节点资源被高估 → 调度不足&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;limits 决定生死&lt;/strong&gt;，写小 = 业务 OOM；写大 = 节点超卖失控&lt;/li&gt;
&lt;li&gt;三个 QoS 等级：Guaranteed（requests=limits）最稳，适合中间件；Burstable 常见；BestEffort 禁止用于生产&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
&lt;strong&gt;内存 limits 与 Java 的坑&lt;/strong&gt;：容器 memory limits=2G，但 JVM 默认按宿主机内存算堆，Pod 一启动就被 OOM。解法：&lt;code&gt;-XX:MaxRAMPercentage=75&lt;/code&gt; 或显式 &lt;code&gt;-Xmx&lt;/code&gt;，让 JVM 感知容器限制。
:::&lt;/p&gt;
&lt;h3&gt;2. 调度策略&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;affinity:
  nodeAffinity:            # 节点亲和：大内存任务 → 内存型节点
    requiredDuringScheduling:
      nodeSelectorTerms:
      - matchExpressions:
        - key: node-type
          operator: In
          values: [&quot;memory&quot;]
  podAntiAffinity:         # 反亲和：有状态组件副本分散到不同节点
    preferredDuringScheduling:
      - weight: 100
        podAffinityTerm:
          topologyKey: kubernetes.io/hostname
          labelSelector:
            matchLabels: {app: kafka}
tolerations:               # 容忍：允许调度到带污点的专用节点
  - key: &quot;bigdata&quot;
    operator: Exists
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;控制面节点打污点（&lt;code&gt;node-role.kubernetes.io/master:NoSchedule&lt;/code&gt;）防止业务误调&lt;/li&gt;
&lt;li&gt;大数据节点打标签 + 污点，Spark/Flink 任务用容忍+节点亲和定向调度，避免与在线业务互抢&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 扩缩容&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# HPA：按 CPU 自动扩缩（需 metrics-server）
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: web-hpa}
spec:
  scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: web}
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource: {name: cpu, target: {type: Utilization, averageUtilization: 60}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
HPA 的三个坑：扩容快（几秒）缩容慢（默认 5 分钟冷却）；无状态业务才适合 HPA；有状态组件（数据库）用垂直扩缩（VPA）或人工，别套 HPA。
:::&lt;/p&gt;
&lt;h2&gt;组件容器化要点（大数据/中间件）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;部署形态&lt;/th&gt;
&lt;th&gt;关键参数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zookeeper/Kafka&lt;/td&gt;
&lt;td&gt;StatefulSet&lt;/td&gt;
&lt;td&gt;稳定 ID、本地 PV、&lt;code&gt;preStop&lt;/code&gt; 优雅下线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL/Redis&lt;/td&gt;
&lt;td&gt;StatefulSet + 备份&lt;/td&gt;
&lt;td&gt;数据卷独立 PVC、&lt;code&gt;fsGroup&lt;/code&gt;、快照备份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flink（K8s 原生）&lt;/td&gt;
&lt;td&gt;Application 模式 Job&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jobmanager/taskmanager&lt;/code&gt; 资源分离、检查点 PVC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark on K8s&lt;/td&gt;
&lt;td&gt;Operator 或 spark-submit&lt;/td&gt;
&lt;td&gt;driver/executor requests 精确配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nacos&lt;/td&gt;
&lt;td&gt;StatefulSet × 3&lt;/td&gt;
&lt;td&gt;集群域名解析、9848/9849 端口&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;StatefulSet 通用骨架&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: StatefulSet
metadata: {name: zookeeper}
spec:
  serviceName: zk-headless      # 无头 Service：稳定网络标识
  replicas: 3
  selector: {matchLabels: {app: zookeeper}}
  template:
    metadata: {labels: {app: zookeeper}}
    spec:
      terminationGracePeriodSeconds: 60   # 优雅下线窗口
      containers:
      - name: zk
        image: zookeeper:3.8
        resources:
          requests: {cpu: &quot;1&quot;, memory: 2Gi}
          limits: {cpu: &quot;2&quot;, memory: 4Gi}
  volumeClaimTemplates:          # 每副本独立 PVC
  - metadata: {name: data}
    spec:
      accessModes: [&quot;ReadWriteOnce&quot;]
      resources: {requests: {storage: 50Gi}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三档规格部署建议&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G&lt;/th&gt;
&lt;th&gt;32C256G&lt;/th&gt;
&lt;th&gt;64C512G&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;角色定位&lt;/td&gt;
&lt;td&gt;控制面/轻业务&lt;/td&gt;
&lt;td&gt;通用业务&lt;/td&gt;
&lt;td&gt;大数据/内存型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max-pods&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;system-reserved&lt;/td&gt;
&lt;td&gt;500m/1Gi&lt;/td&gt;
&lt;td&gt;1C/2Gi&lt;/td&gt;
&lt;td&gt;2C/4Gi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kube-reserved&lt;/td&gt;
&lt;td&gt;500m/1Gi&lt;/td&gt;
&lt;td&gt;1C/2Gi&lt;/td&gt;
&lt;td&gt;2C/4Gi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大数据组件实例&lt;/td&gt;
&lt;td&gt;控制面配套&lt;/td&gt;
&lt;td&gt;单实例起步&lt;/td&gt;
&lt;td&gt;多实例/本地盘存储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;容器运行时&lt;/td&gt;
&lt;td&gt;containerd&lt;/td&gt;
&lt;td&gt;containerd&lt;/td&gt;
&lt;td&gt;containerd + 大数据本地盘&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
64C512G 节点给大数据组件（HDFS DataNode、Kafka）时，考虑&lt;strong&gt;本地盘 PV（Local PersistentVolume）+ 节点污点&lt;/strong&gt;组合：数据本地化保吞吐，污点防止其他业务误调度挤占。本地盘 PV 的代价是&quot;数据绑定节点&quot;——节点故障数据不可达，需要配合组件自身副本（HDFS 副本、Kafka ISR）兜底，而不是依赖 K8S 做数据层容灾。
:::&lt;/p&gt;
&lt;h2&gt;优化检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;etcd SSD 独立、控制面独立节点&lt;/li&gt;
&lt;li&gt;所有工作负载有 requests/limits（Guaranteed 给中间件）&lt;/li&gt;
&lt;li&gt;JVM 容器化参数（MaxRAMPercentage）就位&lt;/li&gt;
&lt;li&gt;节点标签/污点/亲和策略落地&lt;/li&gt;
&lt;li&gt;HPA 只给无状态业务&lt;/li&gt;
&lt;li&gt;本地盘 PV 场景用组件副本兜底数据&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>MySQL 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/mysql-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/mysql-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;MySQL 调优的优先级永远是：&lt;strong&gt;慢查询 → 索引 → 参数&lt;/strong&gt;。参数调优是最后一步——一个被慢查询拖垮的库，innodb_buffer_pool_size 调得再大也救不了。本文先给参数理解与规格表，再强调集群规模的读写分离与容灾。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_buffer_pool_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128M&lt;/td&gt;
&lt;td&gt;InnoDB 缓存池，&lt;strong&gt;最重要参数&lt;/strong&gt;：建议物理内存的 60~75%，热点数据命中内存才能快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_buffer_pool_instances&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;缓冲池分片数，大内存下减少锁竞争（每片 ≥1G）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_log_file_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;48M&lt;/td&gt;
&lt;td&gt;Redo 日志大小，&lt;strong&gt;建议 1~4G&lt;/strong&gt;：太小导致频繁刷盘 checkpoint，写入抖动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_flush_log_at_trx_commit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1=每次提交刷盘（最安全）、2=每秒刷盘（性能与安全折中）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_flush_method&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fsync&lt;/td&gt;
&lt;td&gt;Linux 上 &lt;code&gt;O_DIRECT&lt;/code&gt; 绕过页缓存，避免双缓存，数据盘直接读写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_io_capacity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;刷盘 IO 上限，SSD 调 1000~2000，NVMe 可更高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max_connections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;151&lt;/td&gt;
&lt;td&gt;连接上限，同时盯 &lt;code&gt;max_connections&lt;/code&gt; 与 &lt;code&gt;thread_cache_size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sync_binlog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;每次事务刷 binlog，配合 &lt;code&gt;innodb_flush_log_at_trx_commit=1&lt;/code&gt; 双 1 最安全&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slow_query_log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;开启慢查询日志 + &lt;code&gt;long_query_time=1&lt;/code&gt;，调优的起点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;双 1 与性能的权衡&lt;/strong&gt;：&lt;code&gt;sync_binlog=1 + innodb_flush_log_at_trx_commit=1&lt;/code&gt; 保证崩溃不丢任何已提交事务，但每次提交两次刷盘。对事务一致性要求极高的金融/支付场景必须双 1；普通业务可用 &lt;code&gt;innodb_flush_log_at_trx_commit=2&lt;/code&gt;（每秒刷盘）换取数倍写入性能。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G&lt;/th&gt;
&lt;th&gt;32C256G&lt;/th&gt;
&lt;th&gt;64C512G&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_buffer_pool_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10G（62%）&lt;/td&gt;
&lt;td&gt;180G（70%）&lt;/td&gt;
&lt;td&gt;380G（74%）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_buffer_pool_instances&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_log_file_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_log_buffer_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16M&lt;/td&gt;
&lt;td&gt;32M&lt;/td&gt;
&lt;td&gt;64M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_flush_method&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O_DIRECT&lt;/td&gt;
&lt;td&gt;O_DIRECT&lt;/td&gt;
&lt;td&gt;O_DIRECT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;innodb_io_capacity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;4000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max_connections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sort_buffer_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2M&lt;/td&gt;
&lt;td&gt;4M&lt;/td&gt;
&lt;td&gt;4M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;join_buffer_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2M&lt;/td&gt;
&lt;td&gt;4M&lt;/td&gt;
&lt;td&gt;8M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;code&gt;sort_buffer_size&lt;/code&gt; / &lt;code&gt;join_buffer_size&lt;/code&gt; 是&lt;strong&gt;会话级&lt;/strong&gt;内存，按连接数放大计算：1000 连接 × 8M = 8G。这类参数给大了不如不给——连接一多内存直接打爆。大内存优先喂 &lt;code&gt;buffer_pool&lt;/code&gt;，会话缓冲保持小值。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读写分离&lt;/strong&gt;：一主多从（3~5 从），读流量水平扩展；从库 &lt;code&gt;read_only=ON&lt;/code&gt;，业务层/中间件（ProxySQL、ShardingSphere）路由&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主从复制优化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;并行复制：&lt;code&gt;replica_parallel_workers=8&lt;/code&gt;（8.0）减少从库延迟&lt;/li&gt;
&lt;li&gt;&lt;code&gt;binlog_transaction_dependency_tracking=WRITESET&lt;/code&gt; 提升并行度&lt;/li&gt;
&lt;li&gt;大事务拆小（单事务 &amp;lt; 5 万行），否则从库追不上&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分库分表&lt;/strong&gt;：单表过千万/库容量过 T 级时规划分片（ShardingSphere/MyCat）；分片键按业务维度设计，避免跨片查询&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接治理&lt;/strong&gt;：连接池（HikariCP/Druid）设置合理上限，防止雪崩时连接风暴打垮 DB；&lt;code&gt;max_connections&lt;/code&gt; 与 &lt;code&gt;wait_timeout&lt;/code&gt; 联动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;慢查询治理&lt;/strong&gt;：慢日志 → 索引/改写循环；&lt;code&gt;pt-query-digest&lt;/code&gt; 定期分析 TOP SQL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数与版本管理&lt;/strong&gt;：my.cnf 纳入版本库，变更走发布流程，&lt;code&gt;SET GLOBAL&lt;/code&gt; 只做临时变更&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;高可用&lt;/td&gt;
&lt;td&gt;主从 + 自动切换&lt;/td&gt;
&lt;td&gt;MHA / Orchestrator / 云 RDS 机制，秒级切换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据&lt;/td&gt;
&lt;td&gt;全量 + binlog 增量&lt;/td&gt;
&lt;td&gt;xtrabackup 每日全量 + binlog 定期归档（如 10 分钟），可恢复到任意时点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨机房&lt;/td&gt;
&lt;td&gt;异地从库&lt;/td&gt;
&lt;td&gt;异步复制或半同步，灾备切换入口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一致性&lt;/td&gt;
&lt;td&gt;半同步复制&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rpl_semi_sync_master_enabled&lt;/code&gt;（主从间），主库确认至少一个从库收到 binlog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::caution
备份的验证比备份本身重要：&lt;strong&gt;每周做一次&quot;备份恢复演练&quot;&lt;/strong&gt;——用最新备份在测试环境恢复并跑核心查询。生产上&quot;备份了三年、从未恢复过&quot;最后恢复失败的事故并不少见。
:::&lt;/p&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU 100% 但连接不多&lt;/strong&gt;：慢查询 + 全表扫描，看 &lt;code&gt;SHOW PROCESSLIST&lt;/code&gt; 定位，优化索引而非加机器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主从延迟持续增长&lt;/strong&gt;：大事务、从库配置低于主库、并行复制未开；&lt;code&gt;SHOW REPLICA STATUS&lt;/code&gt; 看 &lt;code&gt;Seconds_Behind_Source&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Too many connections&lt;/code&gt;&lt;/strong&gt;：连接池泄漏或突发流量，先查 processlist 的 Sleep 连接，再定 max_connections&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘 IO 100% 但内存空闲&lt;/strong&gt;：&lt;code&gt;buffer_pool&lt;/code&gt; 命中率低（&lt;code&gt;SHOW GLOBAL STATUS LIKE &apos;Innodb_buffer_pool_read%&apos;&lt;/code&gt;）或 redo 太小频繁刷盘&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Deadlock found when trying to get lock&lt;/code&gt;&lt;/strong&gt;：业务侧事务顺序不一致，统一加锁顺序 + 缩短事务&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;buffer_pool 60~75% 内存，redo 1~4G&lt;/li&gt;
&lt;li&gt;双 1 或 2（按一致性要求），O_DIRECT&lt;/li&gt;
&lt;li&gt;慢查询日志开启 + 定期治理&lt;/li&gt;
&lt;li&gt;主从 + 并行复制 + 半同步&lt;/li&gt;
&lt;li&gt;备份全量+增量，周演练恢复&lt;/li&gt;
&lt;li&gt;连接池上限与 max_connections 匹配&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Nacos 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/nacos-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/nacos-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Nacos 的调优重点是&lt;strong&gt;容量与稳定性&lt;/strong&gt;：能承载多少服务实例、多少配置变更、客户端长连接（gRPC）与推送如何并发。单机 Derby 模式只配测试；生产调优的第一动作就是&lt;strong&gt;外接 MySQL 并组成 3 节点集群&lt;/strong&gt;。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JVM 堆（&lt;code&gt;-Xms/-Xmx&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;实例数与配置量决定；几万实例规模 4~8G 足够，&lt;strong&gt;堆大不等于能扛更多连接&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nacos.naming.empty-service.auto-clean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;自动清理空服务，频繁上下线的环境开启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nacos.naming.distro.task.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认&lt;/td&gt;
&lt;td&gt;集群数据同步任务线程，节点多时按默认即可，别乱调&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gRPC 端口&lt;/td&gt;
&lt;td&gt;9848/9849&lt;/td&gt;
&lt;td&gt;客户端与集群间通信，&lt;strong&gt;防火墙漏放是连接事故主因&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;客户端心跳/重连&lt;/td&gt;
&lt;td&gt;gRPC 长连接&lt;/td&gt;
&lt;td&gt;2.x 客户端基于 gRPC 长连接，&lt;strong&gt;客户端版本尽量对齐服务端&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nacos.core.auth.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;生产必须开启&lt;/strong&gt;，未开启的 Nacos 可被任意读写配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;保护阈值（Protect Threshold）&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;服务下健康实例比例低于阈值时，将不健康实例也参与返回，防雪崩&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;2.x 与 1.x 的容量差异&lt;/strong&gt;：1.x 客户端用 HTTP 轮询（30s 长轮询），2.x 用 gRPC 长连接+服务端推送。2.x 单节点可支撑的服务实例数远高于 1.x，但长连接数量受文件句柄与线程限制，&lt;code&gt;ulimit -n&lt;/code&gt; 要放行。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G（中小规模）&lt;/th&gt;
&lt;th&gt;32C256G（大规模）&lt;/th&gt;
&lt;th&gt;64C512G（超大规模）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;集群节点&lt;/td&gt;
&lt;td&gt;3（独立部署）&lt;/td&gt;
&lt;td&gt;3~5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM 堆&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;元数据库&lt;/td&gt;
&lt;td&gt;MySQL 独立实例&lt;/td&gt;
&lt;td&gt;MySQL 主从&lt;/td&gt;
&lt;td&gt;MySQL 主从 + 独立实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最大服务数（规划值）&lt;/td&gt;
&lt;td&gt;1 万内&lt;/td&gt;
&lt;td&gt;5 万内&lt;/td&gt;
&lt;td&gt;10 万+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单机连接上限规划&lt;/td&gt;
&lt;td&gt;5 万内&lt;/td&gt;
&lt;td&gt;10 万内&lt;/td&gt;
&lt;td&gt;20 万内&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nacos.naming.empty-service.auto-clean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
Nacos 是&lt;strong&gt;控制面组件，不承担业务流量&lt;/strong&gt;。32C256G/64C512G 规格的机器跑 Nacos 是浪费——按上表 8C16G/16C32G 独立部署即可，大规格列的差异体现在&lt;strong&gt;集群节点数、MySQL 冗余与连接规划&lt;/strong&gt;上，而不是单机堆料。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;集群节点数：3 起步，5 封顶&lt;/strong&gt;：Nacos 用 Raft（JRaft）选主，节点越多写入越慢；100 节点应用集群的接入方是&quot;几百个应用 + 几千实例&quot;，Nacos 3~5 节点完全够&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命名空间/分组规划&lt;/strong&gt;：生产按环境（dev/test/prod）与业务线划分 namespace，配置与服务的可见范围收敛，避免&quot;一锅烩&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置变更节流&lt;/strong&gt;：配置批量推送会打满 gRPC 推送线程，变更走&quot;分批次发布&quot;，高峰期不批量改配置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务健康检查&lt;/strong&gt;：客户端心跳（gRPC）与服务端主动探测并存；临时实例（ephemeral）数量大时，关注服务端线程与内存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端侧&lt;/strong&gt;：服务发现不阻塞主流程（fail-fast + 本地缓存），注册中心抖动不影响业务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 K8S 结合&lt;/strong&gt;：云原生环境用 DNS 或 K8s 服务发现兜底，Nacos 作为配置中心为主&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;集群内&lt;/td&gt;
&lt;td&gt;3~5 节点 Raft&lt;/td&gt;
&lt;td&gt;半数以上存活即可用，节点故障自动选主&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据&lt;/td&gt;
&lt;td&gt;MySQL 主从&lt;/td&gt;
&lt;td&gt;元数据存 MySQL，主从保证库级可用；&lt;code&gt;mysqldump&lt;/code&gt; 每日全量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置&lt;/td&gt;
&lt;td&gt;配置导出&lt;/td&gt;
&lt;td&gt;控制台批量导出全部配置，版本管理与灾备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;客户端&lt;/td&gt;
&lt;td&gt;本地缓存兜底&lt;/td&gt;
&lt;td&gt;客户端启动拉取并缓存配置，Nacos 全挂服务仍可用旧配置运行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务注册成功但客户端发现不到&lt;/strong&gt;：检查 9848 gRPC 端口连通（&lt;code&gt;telnet &amp;lt;ip&amp;gt; 9848&lt;/code&gt;）、客户端版本与服务端大版本是否一致&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置变更未生效/推送失败&lt;/strong&gt;：客户端 gRPC 断连重连中；看服务端 &lt;code&gt;push&lt;/code&gt; 相关日志，检查 namespace 是否匹配&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;控制台/API 401&lt;/strong&gt;：&lt;code&gt;nacos.core.auth.enabled&lt;/code&gt; 开启后未配 token（&lt;code&gt;nacos.core.auth.plugin.nacos.token.secret.key&lt;/code&gt;），生产必须显式配置密钥&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集群脑裂/节点互相踢&lt;/strong&gt;：节点间 9849 不通、或 clock 偏差；确认集群网络与时钟同步&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动报数据库连接失败&lt;/strong&gt;：MySQL 未就绪或 &lt;code&gt;db.url.0&lt;/code&gt; 配置错误，检查 &lt;code&gt;application.properties&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;生产必做：3 节点集群 + MySQL + 认证开启&lt;/li&gt;
&lt;li&gt;9848/9849 端口放行，客户端版本对齐&lt;/li&gt;
&lt;li&gt;命名空间与分组规范，配置版本管理&lt;/li&gt;
&lt;li&gt;堆 4~8G（按实例数），不盲目堆料&lt;/li&gt;
&lt;li&gt;MySQL 主从 + 每日备份 + 配置导出&lt;/li&gt;
&lt;li&gt;变更节流与监控（控制台节点状态、gRPC 连接数）&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>opencode 数据备份与恢复方案</title><link>https://chaggle.github.io/posts/2026/08/10/opencode-backup-restore/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/opencode-backup-restore/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;用 opencode 写代码、记对话，所有会话历史都存在本地。一旦硬盘损坏或者系统重装，几年的积累就全没了。本文记录我搭建的&quot;NAS 每日备份 + 安全恢复&quot;完整方案，Windows / Linux / macOS 三平台通用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;为什么要做备份&lt;/h2&gt;
&lt;p&gt;opencode 是&lt;strong&gt;本地优先&lt;/strong&gt;的 AI 编程助手：会话记录、对话数据库、附件文件都存在本机 &lt;code&gt;C:\Users\Administrator\.local\share\opencode&lt;/code&gt; 目录下，其中核心是一个 SQLite 数据库 &lt;code&gt;opencode.db&lt;/code&gt;（当前已经增长到 376MB）。&lt;/p&gt;
&lt;p&gt;:::warning
本地优先意味着&quot;数据只存在这一台机器上&quot;。没有备份的情况下，一次磁盘故障、一次误删，所有历史对话就永久消失了。
:::&lt;/p&gt;
&lt;p&gt;备份设计遵循两个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本机快照&lt;/strong&gt;：离数据最近，恢复最快（但本机挂了就一起挂）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NAS 镜像&lt;/strong&gt;：局域网内独立存储，每日自动增量同步&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;备份方案：绿联云 NAS + robocopy 增量镜像&lt;/h2&gt;
&lt;p&gt;家里有一台绿联云 NAS（&lt;code&gt;192.168.137.108&lt;/code&gt;），挂在局域网内。通过 Windows 的 &lt;code&gt;robocopy&lt;/code&gt; 做&lt;strong&gt;增量镜像同步&lt;/strong&gt;，只传输有变化的部分，速度快、支持断点续传。&lt;/p&gt;
&lt;p&gt;脚本 &lt;code&gt;opencode-backup.ps1&lt;/code&gt; 的核心逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源目录：&lt;code&gt;C:\Users\Administrator\.local\share\opencode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;目标目录：&lt;code&gt;\\192.168.137.108\personal_folder\opencode-backup\opencode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排除临时文件：&lt;code&gt;*.tmp&lt;/code&gt;、&lt;code&gt;*.lock&lt;/code&gt;、&lt;code&gt;*.part&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/MIR&lt;/code&gt; 镜像模式：NAS 上多出的文件自动清理，本地删除的文件也会从 NAS 上同步删除&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
&lt;strong&gt;为什么要排除临时文件&lt;/strong&gt;：SQLite 运行时会生成 WAL/锁文件，直接同步会导致备份副本与主库状态不一致。备份镜像中只保留稳定的数据文件。
:::&lt;/p&gt;
&lt;p&gt;几个关键设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;24 小时间隔&lt;/strong&gt;：脚本放入开机自启，每次启动检查&quot;上次成功备份时间&quot;，不足 24 小时直接跳过，一天至多一次&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;凭据安全&lt;/strong&gt;：NAS 登录凭据存在 Windows 凭据管理器（&lt;code&gt;cmdkey&lt;/code&gt;），脚本里不含任何密码明文&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志审计&lt;/strong&gt;：每天一份日志 &lt;code&gt;logs/opencode-backup-YYYYMMDD.log&lt;/code&gt;，保留 30 天，出问题可回溯&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完整脚本 &lt;code&gt;tools/opencode-backup.ps1&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# opencode-backup.ps1
# 每日增量备份 opencode 数据目录到绿联云 NAS (192.168.137.108)
# 触发方式：开机自启（Startup 文件夹），内置 24 小时间隔检查，一天至多备份一次
# 凭据使用 Windows 凭据管理器中的 cmdkey 记录，脚本内不含任何密码
$ErrorActionPreference = &apos;Stop&apos;

$src = &apos;C:\Users\Administrator\.local\share\opencode&apos;
$dst = &apos;\\192.168.137.108\personal_folder\opencode-backup\opencode&apos;
$logDir = Join-Path $PSScriptRoot &apos;logs&apos;
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null }
$logFile = Join-Path $logDir (&quot;opencode-backup-{0}.log&quot; -f (Get-Date -Format &apos;yyyyMMdd&apos;))
$marker = Join-Path $logDir &apos;.last-backup&apos;

function Log($msg) {
  $line = &apos;{0} {1}&apos; -f (Get-Date -Format &apos;yyyy-MM-dd HH:mm:ss&apos;), $msg
  Add-Content -Path $logFile -Value $line -Encoding UTF8
}

# 0. 24 小时间隔检查：距上次成功备份不足 24 小时则跳过
if (Test-Path $marker) {
  $last = (Get-Item $marker).LastWriteTime
  if ($last -gt (Get-Date).AddHours(-24)) {
    Log &apos;SKIP: last backup within 24h, skip this run&apos;
    exit 0
  }
}

Log &apos;==== opencode backup start ====&apos;

# 1. 检查 opencode 是否在运行（运行中复制 WAL 三件套可能不一致，仅告警）
$proc = Get-Process -Name &apos;opencode&apos; -ErrorAction SilentlyContinue
if ($proc) { Log &apos;WARN: opencode is running, sqlite backup may be inconsistent&apos; }

# 2. 确认 NAS 可达
if (-not (Test-Path &apos;\\192.168.137.108\personal_folder&apos;)) {
  Log &apos;ERROR: NAS share unreachable&apos;
  exit 1
}

# 3. robocopy 增量镜像同步（目标端为专用备份目录，/MIR 安全）
# 排除临时/锁文件；保留 30 天日志由计划任务侧清理
$null = robocopy $src $dst /MIR /R:2 /W:3 /XF *.tmp *.lock *.part /NP /LOG+:$logFile /TEE
$code = $LASTEXITCODE

# robocopy 退出码 0-7 均为成功，&amp;gt;=8 为失败
if ($code -lt 8) {
  Log &quot;OK: robocopy exit=$code, backup completed&quot;
  New-Item -ItemType File -Path $marker -Force | Out-Null
} else {
  Log &quot;ERROR: robocopy exit=$code&quot;
}

# 4. 清理 30 天前的日志
Get-ChildItem $logDir -Filter &apos;opencode-backup-*.log&apos; |
  Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
  Remove-Item -Force -ErrorAction SilentlyContinue

Log &apos;==== opencode backup end ====&apos;
exit $code
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux / macOS：rsync + SSH&lt;/h3&gt;
&lt;p&gt;Windows 脚本依赖 &lt;code&gt;robocopy&lt;/code&gt; 和 SMB 共享，Linux/macOS 上换成 &lt;strong&gt;&lt;code&gt;rsync&lt;/code&gt; + SSH&lt;/strong&gt;，一套脚本两个平台通用（两个系统都自带 rsync）。&lt;/p&gt;
&lt;p&gt;完整脚本 &lt;code&gt;tools/opencode-backup.sh&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
# opencode-backup.sh
# 每日增量备份 opencode 数据目录到绿联云 NAS (rsync + SSH)
# 适用于 Linux / macOS（两个平台均自带 rsync）
# 用法:
#   ./opencode-backup.sh                     # 默认备份到 NAS（SSH + rsync）
#   NAS_REMOTE=user@host:/path ./opencode-backup.sh  # 自定义 NAS 地址/路径
# 定时: Linux 用 crontab，macOS 用 launchd 或 crontab
# 免密: Linux: ssh-copy-id $NAS_USER@$NAS_HOST
#       macOS: cat ~/.ssh/id_rsa.pub | ssh $NAS_USER@$NAS_HOST &quot;mkdir -p ~/.ssh &amp;amp;&amp;amp; cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&quot;
set -euo pipefail

# 可配置项（按自己环境修改）
NAS_HOST=&quot;192.168.137.108&quot;
NAS_USER=&quot;${NAS_USER:-admin}&quot;
# 绿联 UGOS 共享目录默认挂载在 /volume1 下，如路径不同请修改
NAS_REMOTE=&quot;${NAS_REMOTE:-$NAS_USER@$NAS_HOST:/volume1/personal_folder/opencode-backup/opencode}&quot;

SRC=&quot;$HOME/.local/share/opencode&quot;
LOG_DIR=&quot;$(cd &quot;$(dirname &quot;$0&quot;)&quot; &amp;amp;&amp;amp; pwd)/logs&quot;
mkdir -p &quot;$LOG_DIR&quot;
LOG_FILE=&quot;$LOG_DIR/opencode-backup-$(date +%Y%m%d).log&quot;
MARKER_FILE=&quot;$LOG_DIR/.last-backup&quot;

log() { echo &quot;$(date &apos;+%Y-%m-%d %H:%M:%S&apos;) $*&quot; | tee -a &quot;$LOG_FILE&quot;; }

# 0. 24 小时间隔检查：距上次成功备份不足 24 小时则跳过
if [ -f &quot;$MARKER_FILE&quot; ] &amp;amp;&amp;amp; [ &quot;$(find &quot;$MARKER_FILE&quot; -mmin -1440 | wc -l)&quot; -eq 1 ]; then
  log &apos;SKIP: last backup within 24h, skip this run&apos;
  exit 0
fi

log &apos;==== opencode backup start ====&apos;

# 1. 检查 opencode 是否在运行（运行中复制 sqlite 可能不一致，仅告警）
if pgrep -x opencode &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  log &apos;WARN: opencode is running, sqlite backup may be inconsistent&apos;
fi

# 2. 检查源目录
if [ ! -d &quot;$SRC&quot; ]; then
  log &quot;ERROR: source dir missing: $SRC&quot;
  exit 1
fi

# 3. rsync 增量镜像同步（排除临时/锁文件）
if ! rsync -avz --delete --timeout=60 \
  --exclude &apos;*.tmp&apos; --exclude &apos;*.lock&apos; --exclude &apos;*.part&apos; \
  -e ssh &quot;$SRC/&quot; &quot;$NAS_REMOTE/&quot;; then
  log &quot;ERROR: rsync failed&quot;
  exit 1
fi

log &apos;OK: backup completed&apos;
touch &quot;$MARKER_FILE&quot;

# 4. 清理 30 天前的日志
find &quot;$LOG_DIR&quot; -name &apos;opencode-backup-*.log&apos; -mtime +30 -delete 2&amp;gt;/dev/null || true

log &apos;==== opencode backup end ====&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
&lt;strong&gt;SSH 免密配置&lt;/strong&gt;：NAS 上需开启 SSH（绿联云：设置 → 终端机 → 开启 SSH）。然后用 &lt;code&gt;ssh-copy-id admin@192.168.137.108&lt;/code&gt;（Linux）或手动追加公钥（macOS）实现免密登录，脚本才能定时自动执行。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定时执行&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux（crontab）：&lt;code&gt;0 3 * * * /path/to/tools/opencode-backup.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;macOS（launchd 或 crontab）：推荐 crontab 与 Linux 一致，简单够用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;恢复方案：安全快照 + 演练模式 + 结果校验&lt;/h2&gt;
&lt;p&gt;备份做得再勤，恢复不出来也是白搭。所以恢复脚本 &lt;code&gt;opencode-restore.ps1&lt;/code&gt; 设计了&lt;strong&gt;三道保险&lt;/strong&gt;：&lt;/p&gt;
&lt;h3&gt;1. 本地安全快照&lt;/h3&gt;
&lt;p&gt;恢复操作会&lt;strong&gt;覆盖本地现有数据&lt;/strong&gt;。万一恢复失败或者从 NAS 拿回来的数据有问题，本地数据也没了——那才是真正的灾难。因此恢复前先把当前本地数据完整镜像到临时目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SAFETY: local data snapshot -&amp;gt; %TEMP%\opencode-safety-20260810-002133
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 演练模式&lt;/h3&gt;
&lt;p&gt;脚本支持 &lt;code&gt;-RestoreTo&lt;/code&gt; 参数，把恢复目标指向任意目录，&lt;strong&gt;不碰本地真实数据&lt;/strong&gt;。建议每次升级方案后先在临时目录演练一遍：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.\opencode-restore.ps1 -RestoreTo C:\temp\opencode-test
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 结果校验&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;robocopy 退出码 0-7 均视为成功（1 表示有文件复制），大于等于 8 才算失败&lt;/li&gt;
&lt;li&gt;恢复完成后验证 &lt;code&gt;opencode.db&lt;/code&gt; 是否存在并检查文件大小，防止&quot;恢复成功但数据库缺失&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完整脚本 &lt;code&gt;tools/opencode-restore.ps1&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# opencode-restore.ps1
# 从绿联云 NAS 恢复 opencode 数据目录到本地
# 用法:
#   .\opencode-restore.ps1              # 标准恢复（含本地安全快照）
#   .\opencode-restore.ps1 -SkipSafetyCopy   # 跳过本地安全快照
#   .\opencode-restore.ps1 -RestoreTo C:\temp\opencode-test  # 恢复到指定目录（演练用，不覆盖本地）
param(
  [switch]$SkipSafetyCopy,
  [string]$RestoreTo = &apos;&apos;
)

$ErrorActionPreference = &apos;Stop&apos;

$srcNas = &apos;\\192.168.137.108\personal_folder\opencode-backup\opencode&apos;
$local = &apos;C:\Users\Administrator\.local\share\opencode&apos;
$logDir = Join-Path $PSScriptRoot &apos;logs&apos;
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null }
$logFile = Join-Path $logDir (&quot;opencode-restore-{0}.log&quot; -f (Get-Date -Format &apos;yyyyMMdd&apos;))

function Log($msg) {
  $line = &apos;{0} {1}&apos; -f (Get-Date -Format &apos;yyyy-MM-dd HH:mm:ss&apos;), $msg
  Add-Content -Path $logFile -Value $line -Encoding UTF8
  Write-Host $line
}

Log &apos;==== opencode restore start ====&apos;

# 1. NAS 可达性检查
if (-not (Test-Path $srcNas)) {
  Log &apos;ERROR: NAS backup dir unreachable, abort&apos;
  exit 1
}
if (-not (Test-Path (Join-Path $srcNas &apos;opencode.db&apos;))) {
  Log &apos;ERROR: opencode.db not found on NAS, abort&apos;
  exit 1
}

# 2. 目标目录确认
if ($RestoreTo) {
  $target = $RestoreTo
  Log &quot;Restore target (test mode): $target&quot;
} else {
  $target = $local
}

# 3. 运行中检测：恢复会覆盖本地数据，opencode 运行中禁止
$proc = Get-Process -Name &apos;opencode&apos; -ErrorAction SilentlyContinue
if ($proc -and -not $RestoreTo) {
  Log &apos;ERROR: opencode is running, please exit opencode first then retry&apos;
  exit 1
}

# 4. 本地安全快照（防止恢复失败导致数据丢失）
if (-not $SkipSafetyCopy -and -not $RestoreTo) {
  $safety = Join-Path $env:TEMP (&quot;opencode-safety-{0}&quot; -f (Get-Date -Format &apos;yyyyMMdd-HHmmss&apos;))
  New-Item -ItemType Directory -Path $safety -Force | Out-Null
  $null = robocopy $local $safety /MIR /R:2 /W:3 /NP /NFL /NDL /NJH /NJS
  Log &quot;SAFETY: local data snapshot -&amp;gt; $safety&quot;
}

# 5. 执行恢复（NAS 镜像回本地）
$null = robocopy $srcNas $target /MIR /R:2 /W:3 /XF *.tmp *.lock *.part /NP /LOG+:$logFile /TEE
$code = $LASTEXITCODE
if ($code -lt 8) {
  Log &quot;OK: restore completed (robocopy exit=$code) -&amp;gt; $target&quot;
} else {
  Log &quot;ERROR: restore failed (robocopy exit=$code)&quot;
  exit $code
}

# 6. 校验
try {
  $db = Join-Path $target &apos;opencode.db&apos;
  if (Test-Path $db) {
    $size = (Get-Item $db).Length
    Log &quot;VERIFY: opencode.db exists, size=$size bytes&quot;
  } else {
    Log &apos;WARN: opencode.db missing after restore&apos;
  }
} catch { }

Log &apos;==== opencode restore end ====&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux / macOS：恢复脚本&lt;/h3&gt;
&lt;p&gt;与备份脚本对应，&lt;code&gt;tools/opencode-restore.sh&lt;/code&gt; 同样基于 rsync + SSH，保留 Windows 版的全部三道保险：安全快照、演练模式、结果校验。&lt;/p&gt;
&lt;p&gt;完整脚本 &lt;code&gt;tools/opencode-restore.sh&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
# opencode-restore.sh
# 从绿联云 NAS 恢复 opencode 数据目录 (rsync + SSH)
# 适用于 Linux / macOS
# 用法:
#   ./opencode-restore.sh                  # 标准恢复（含本地安全快照）
#   ./opencode-restore.sh -s               # 跳过本地安全快照
#   ./opencode-restore.sh -t DIR           # 恢复到指定目录（演练用，不覆盖本地）
# 免密: 同 opencode-backup.sh，需先配置 SSH 公钥
set -euo pipefail

# 可配置项（与备份脚本保持一致）
NAS_HOST=&quot;192.168.137.108&quot;
NAS_USER=&quot;${NAS_USER:-admin}&quot;
NAS_REMOTE=&quot;${NAS_REMOTE:-$NAS_USER@$NAS_HOST:/volume1/personal_folder/opencode-backup/opencode}&quot;

LOCAL=&quot;$HOME/.local/share/opencode&quot;
LOG_DIR=&quot;$(cd &quot;$(dirname &quot;$0&quot;)&quot; &amp;amp;&amp;amp; pwd)/logs&quot;
mkdir -p &quot;$LOG_DIR&quot;
LOG_FILE=&quot;$LOG_DIR/opencode-restore-$(date +%Y%m%d).log&quot;

SKIP_SAFETY=0
TARGET=&quot;&quot;

usage() {
  echo &quot;用法: $0 [-s] [-t DIR]&quot;
  echo &quot;  -s         跳过本地安全快照&quot;
  echo &quot;  -t DIR     恢复到指定目录（演练模式，不覆盖本地）&quot;
  exit 0
}

while getopts &quot;st:h&quot; opt; do
  case $opt in
    s) SKIP_SAFETY=1 ;;
    t) TARGET=&quot;$OPTARG&quot; ;;
    h) usage ;;
    *) usage ;;
  esac
done

log() { echo &quot;$(date &apos;+%Y-%m-%d %H:%M:%S&apos;) $*&quot; | tee -a &quot;$LOG_FILE&quot;; }

log &apos;==== opencode restore start ====&apos;

# 1. NAS 可达性检查
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes &quot;$NAS_USER@$NAS_HOST&quot; \
  &quot;test -d /volume1/personal_folder/opencode-backup/opencode&quot; 2&amp;gt;/dev/null; then
  log &apos;ERROR: NAS backup dir unreachable (SSH), abort&apos;
  exit 1
fi

# 2. 目标目录确认
if [ -n &quot;$TARGET&quot; ]; then
  DEST=&quot;$TARGET&quot;
  log &quot;Restore target (test mode): $DEST&quot;
else
  DEST=&quot;$LOCAL&quot;
fi

# 3. 运行中检测：恢复会覆盖本地数据，opencode 运行中禁止
if pgrep -x opencode &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; [ -z &quot;$TARGET&quot; ]; then
  log &apos;ERROR: opencode is running, please exit opencode first then retry&apos;
  exit 1
fi

# 4. 本地安全快照（防止恢复失败导致数据丢失）
if [ &quot;$SKIP_SAFETY&quot; -eq 0 ] &amp;amp;&amp;amp; [ -z &quot;$TARGET&quot; ] &amp;amp;&amp;amp; [ -d &quot;$LOCAL&quot; ]; then
  SAFETY=&quot;$HOME/opencode-safety-$(date +%Y%m%d-%H%M%S)&quot;
  mkdir -p &quot;$SAFETY&quot;
  rsync -a &quot;$LOCAL/&quot; &quot;$SAFETY/&quot;
  log &quot;SAFETY: local data snapshot -&amp;gt; $SAFETY&quot;
fi

# 5. 执行恢复（NAS 镜像回本地）
if ! rsync -avz --delete --timeout=60 \
  --exclude &apos;*.tmp&apos; --exclude &apos;*.lock&apos; --exclude &apos;*.part&apos; \
  -e ssh &quot;$NAS_REMOTE/&quot; &quot;$DEST/&quot;; then
  log &quot;ERROR: restore failed&quot;
  exit 1
fi
log &quot;OK: restore completed -&amp;gt; $DEST&quot;

# 6. 校验
DB=&quot;$DEST/opencode.db&quot;
if [ -f &quot;$DB&quot; ]; then
  if [ &quot;$(uname)&quot; = &quot;Darwin&quot; ]; then
    SIZE=$(stat -f%z &quot;$DB&quot;)
  else
    SIZE=$(stat -c%s &quot;$DB&quot;)
  fi
  log &quot;VERIFY: opencode.db exists, size=$SIZE bytes&quot;
else
  log &apos;WARN: opencode.db missing after restore&apos;
fi

log &apos;==== opencode restore end ====&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
&lt;strong&gt;恢复演练&lt;/strong&gt;（跨平台通用）：&lt;code&gt;./opencode-restore.sh -t ~/opencode-test&lt;/code&gt; 恢复到临时目录验证，确认无误后再执行标准恢复。
:::&lt;/p&gt;
&lt;h2&gt;日常使用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;手动备份&lt;/strong&gt;（一般不需要，开机自启会自动跑）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;powershell -ExecutionPolicy Bypass -File .\tools\opencode-backup.ps1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;完整恢复&lt;/strong&gt;（先退出 opencode，脚本会自动拒绝在运行中恢复）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;powershell -ExecutionPolicy Bypass -File .\tools\opencode-restore.ps1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;快速恢复&lt;/strong&gt;（确认本地数据不重要，跳过安全快照）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;powershell -ExecutionPolicy Bypass -File .\tools\opencode-restore.ps1 -SkipSafetyCopy
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux / macOS&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;手动备份&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./tools/opencode-backup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;完整恢复&lt;/strong&gt;（先退出 opencode）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./tools/opencode-restore.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;恢复演练&lt;/strong&gt;（不覆盖本地数据）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./tools/opencode-restore.sh -t ~/opencode-test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
恢复是&lt;strong&gt;覆盖式&lt;/strong&gt;操作，会把本地数据替换成 NAS 上的版本。执行前务必确认本地没有比 NAS 更新的对话记录，必要时先手动复制一份。
:::&lt;/p&gt;
&lt;h2&gt;踩过的坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;robocopy 退出码&lt;/strong&gt;：1-7 是&quot;成功但有复制动作&quot;，8+ 才是失败，直接 &lt;code&gt;if ($LASTEXITCODE -eq 0)&lt;/code&gt; 判断会误报失败&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行中备份的一致性&lt;/strong&gt;：opencode 运行时会写数据库，此时备份的副本可能不一致。脚本只告警不阻塞——NAS 副本用于&quot;恢复历史&quot;是够的，但如果追求严格一致，应该在退出 opencode 后备份&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库持续增长&lt;/strong&gt;：opencode.db 三个月涨到 376MB，首次全量同步会比较慢，之后增量就快了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨平台差异&lt;/strong&gt;：Linux/macOS 脚本用 rsync + SSH 直连 NAS（不依赖挂载）；&lt;code&gt;stat&lt;/code&gt; 参数两平台不同（Linux &lt;code&gt;-c%s&lt;/code&gt;，macOS &lt;code&gt;-f%z&lt;/code&gt;），脚本已兼容；NAS 共享目录的卷路径（&lt;code&gt;/volume1&lt;/code&gt;）不同机型可能不同，按实际修改脚本顶部配置即可&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;备份这件事，&lt;strong&gt;贵在自动、重在验证&lt;/strong&gt;。方案的价值不在于脚本写得多漂亮，而在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每天自动跑，不用人记着&lt;/li&gt;
&lt;li&gt;恢复有演练，不是纸面流程&lt;/li&gt;
&lt;li&gt;单点故障不会全灭&lt;/li&gt;
&lt;li&gt;凭据安全，脚本可公开可审计&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下次换电脑或者系统重装，一条命令就能把几年的对话历史完整带回来。&lt;/p&gt;
</content:encoded></item><item><title>Ranger 部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/10/ranger-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/ranger-deploy-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Ranger 是 Hadoop 生态的集中授权与审计系统：管理员在网页上配置&quot;谁能访问哪些资源、什么权限&quot;，各组件（HDFS/Hive/Kafka/YARN）通过内置插件实时拦截执行。与 Kerberos（认证：你是谁）配合，Ranger 管授权（你能做什么），审计（你做了什么）。本文覆盖原理、部署、参数优化与常见问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理&lt;/h2&gt;
&lt;h3&gt;架构三件套&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Ranger Admin（策略管理 + 审计展示）
   ├── 存策略与审计到数据库（MySQL/Postgres）+ Solr（审计索引）
   └── 各组件节点上的 Agent（Plugin）：
       HDFS NameNode、HiveServer2、Kafka Broker、YARN RM ...
       缓存策略（本地缓存 + 定时刷新 30s）
       拦截访问 → 查缓存策略 → allow/deny → 写审计日志
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略模型&lt;/strong&gt;：用户/用户组/角色 × 资源（HDFS 路径、Hive 库表、Kafka topic）× 权限（读/写/执行/Select/Insert...）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件拉取策略&lt;/strong&gt;：Agent 每 30 秒（默认）从 Admin 拉取策略增量，本地缓存兜底——Admin 故障不影响已缓存的鉴权&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计链路&lt;/strong&gt;：插件把访问日志发给 Admin，Admin 批量写 Solr，UI 查询审计记录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ranger 与原生权限叠加&lt;/strong&gt;：HDFS 同时受 HDFS ACL 与 Ranger 策略约束（取交集），开启 Ranger 后建议收敛原生 ACL 到 Ranger 统一管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
&lt;strong&gt;一句话理解&lt;/strong&gt;：Ranger = 集中式&quot;门禁系统&quot;。策略在 Admin 网页配一次，插件在所有组件门口执行；审计是每扇门的通行记录。插件本地缓存保证&quot;门禁中心&quot;挂了门还能开（按旧策略）。
:::&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;h3&gt;1. 前置&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;MySQL/Postgres（策略+审计主存储）、Solr（审计索引，可选但建议）&lt;/li&gt;
&lt;li&gt;JDK 8+；Ranger 与各组件版本兼容表（ranger-2.x 配 Hadoop 3.x 主流）&lt;/li&gt;
&lt;li&gt;已开启 Kerberos 时，Ranger 各组件需要对应 principal&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 安装 Ranger Admin&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 解压 ranger-2.3.0-admin 并配置
tar -zxvf ranger-2.3.0-admin.tar.gz -C /opt
cd /opt/ranger-2.3.0-admin

# 修改 setup 配置（conf/ranger-admin-site.xml）
# ranger.jpa.jdbc.url / jdbc.user / jdbc.password   （数据库连接）
# ranger.audit.solr.urls=localhost:8983/solr/ranger_audits

# 执行初始化（建表 + 初始用户 admin/admin）
./setup.sh
# 启动
ranger-admin start
# 访问 http://&amp;lt;admin&amp;gt;:6080  默认账号 admin/admin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 安装组件插件（以 Hive 为例）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 解压 ranger-2.3.0-hive-plugin
cd /opt/ranger-2.3.0-hive-plugin
./enable-hive-plugin.sh
# 脚本会向 Ranger Admin 注册服务（hive_repo），并把插件 jar 放入 Hive 的 lib
# 重启 HiveServer2 生效
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HDFS 插件（&lt;code&gt;enable-hdfs-plugin.sh&lt;/code&gt;，配置 NameNode）、Kafka 插件（&lt;code&gt;enable-kafka-plugin.sh&lt;/code&gt;）同理。&lt;/p&gt;
&lt;h3&gt;4. 配置策略&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;登录 Admin → HDFS 服务 → 创建策略（路径 &lt;code&gt;/user/test/*&lt;/code&gt; → 组 &lt;code&gt;dev&lt;/code&gt; → Read/Write/Execute）&lt;/li&gt;
&lt;li&gt;Hive 服务 → 建库授权（库 &lt;code&gt;ods&lt;/code&gt; → 组 &lt;code&gt;etl&lt;/code&gt; → Select）&lt;/li&gt;
&lt;li&gt;Kafka 服务 → topic 授权（topic &lt;code&gt;orders&lt;/code&gt; → 用户 &lt;code&gt;flink-user&lt;/code&gt; → Publish/Consume）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参数优化（三档规格）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;中小集群（&amp;lt;50 节点）&lt;/th&gt;
&lt;th&gt;中大型（50~200）&lt;/th&gt;
&lt;th&gt;超大型（200+）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ranger Admin 规格&lt;/td&gt;
&lt;td&gt;8C16G&lt;/td&gt;
&lt;td&gt;16C32G&lt;/td&gt;
&lt;td&gt;32C64G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin JVM 堆&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;策略刷新间隔 &lt;code&gt;ranger.plugin.policy.refresh.interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30000ms&lt;/td&gt;
&lt;td&gt;30000ms&lt;/td&gt;
&lt;td&gt;60000ms（放宽减少拉取风暴）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;审计缓冲 &lt;code&gt;ranger.audit.source.type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;solr&lt;/td&gt;
&lt;td&gt;solr&lt;/td&gt;
&lt;td&gt;solr + 独立索引集群&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solr 规格&lt;/td&gt;
&lt;td&gt;与 Admin 合部&lt;/td&gt;
&lt;td&gt;独立 16C32G&lt;/td&gt;
&lt;td&gt;独立 32C64G × 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插件缓存时间&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;td&gt;30s&lt;/td&gt;
&lt;td&gt;60s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;优化理解&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略刷新间隔 vs 生效速度&lt;/strong&gt;：30s 是默认，策略变更最多 30s 后生效；大集群全部节点同时拉取形成&quot;策略拉取风暴&quot;，放宽到 60s 并错峰&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计是性能黑洞&lt;/strong&gt;：高吞吐组件（Kafka/NameNode）每个请求都写审计，批量与异步缓冲是关键；审计不影响主链路鉴权，失败只降级审计不拦截&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略规模控制&lt;/strong&gt;：策略爆炸（上千条）会拖慢插件匹配，按&quot;目录级/库级&quot;粗粒度为主，细粒度（行级/列级 Mask）慎用，行级过滤对性能影响明显&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;集群规模优化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;插件版本一致性&lt;/strong&gt;：Ranger Admin 与各组件插件版本必须配套，跨版本会导致策略拉取失败（滚动升级插件时先升 Admin）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限模型设计&lt;/strong&gt;：以组（dev/etl/ops）为授权单位，用户加组即获得权限，避免逐用户配策略；定期用&quot;策略报告&quot;清理孤儿策略&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计治理&lt;/strong&gt;：审计量大时按级别过滤（&lt;code&gt;ranger.plugin.audit.filters&lt;/code&gt;），业务敏感操作强制审计、常规读降噪&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多租户&lt;/strong&gt;：按服务（HDFS/Hive 各自 repo）与资源路径分段授权，租户间目录隔离（参考 HDFS 配额 + Ranger 双保险）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 Kerberos 配合&lt;/strong&gt;：认证在 Kerberos（Ranger 用户来源），授权在 Ranger；两者都开启时排障顺序：先 klist 验票据，再查 Ranger 策略&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略改了不生效&lt;/strong&gt;：插件缓存——等刷新间隔或手动触发；检查插件与 Admin 连通（6080 端口、服务注册状态）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件未生效（组件行为无鉴权）&lt;/strong&gt;：插件 jar 未正确放入组件 lib、组件未重启、或 &lt;code&gt;enable-*-plugin.sh&lt;/code&gt; 未向 Admin 注册服务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计查不到记录&lt;/strong&gt;：Solr 索引故障或审计队列积压；&lt;code&gt;ranger_audits&lt;/code&gt; collection 状态、插件侧 &lt;code&gt;ranger.audit.solr.urls&lt;/code&gt; 配置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin 连不上数据库&lt;/strong&gt;：&lt;code&gt;ranger.jpa.jdbc.*&lt;/code&gt; 配置错误或 MySQL 未初始化（setup.sh 失败时先查 DB）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hive 鉴权叠加混乱&lt;/strong&gt;：Hive 原生权限与 Ranger 同时生效，SQL 被误拦——按规范统一收敛到 Ranger，关闭 &lt;code&gt;hive.security.authorization.manager&lt;/code&gt; 原生管理器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略同步失败（stamp mismatch）&lt;/strong&gt;：Admin 重启后插件缓存时间戳不一致，等下次刷新或重启插件&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;部署检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Admin + DB + Solr 独立成链，插件版本配套&lt;/li&gt;
&lt;li&gt;策略按&quot;组 + 目录/库级&quot;粗粒度设计&lt;/li&gt;
&lt;li&gt;刷新间隔与集群规模匹配，错峰拉取&lt;/li&gt;
&lt;li&gt;审计分级降噪，Solr 容量监控&lt;/li&gt;
&lt;li&gt;权限申请/变更走规范流程，定期策略审计&lt;/li&gt;
&lt;li&gt;与 Kerberos 双认证的排障手册就位&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Redis 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/redis-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/redis-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Redis 是单线程模型（6.x 起 IO 线程可选），调优思路与其他组件相反：&lt;strong&gt;不是加资源，而是控制单个操作的成本&lt;/strong&gt;——大 Key、慢命令、全量复制是三大杀手。内存管理（淘汰策略、编码优化）与持久化取舍是日常重点。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxmemory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0（不限）&lt;/td&gt;
&lt;td&gt;内存上限，&lt;strong&gt;生产必须设&lt;/strong&gt;，否则 OOM 被内核杀掉&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxmemory-policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;noeviction&lt;/td&gt;
&lt;td&gt;淘汰策略：allkeys-lru（缓存通用）、volatile-lru（只淘汰带 TTL 的键）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appendonly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;AOF 持久化，生产开启；&lt;code&gt;appendfsync&lt;/code&gt; 选 everysec 平衡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appendfsync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;everysec&lt;/td&gt;
&lt;td&gt;每秒刷盘：最常用；always 最安全最慢；no 交给 OS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;save&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认 RDB 策略&lt;/td&gt;
&lt;td&gt;RDB 快照条件，与 AOF 配合使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxclients&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;最大客户端连接，连接池满报错时查这个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;io-threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4（6.x 起）&lt;/td&gt;
&lt;td&gt;IO 线程数（仅网络读写），CPU 核多时可设 8；&lt;strong&gt;命令执行仍单线程&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash-max-listpack-entries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;小对象编码阈值：小 hash/list 用紧凑编码，省内存但操作快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;空闲连接超时，建议 300 秒防连接堆积&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rename-command&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;禁危险命令：FLUSHALL、FLUSHDB、KEYS 重命名/禁用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;大 Key 是 Redis 的头号事故源&lt;/strong&gt;：单个 string &amp;gt; 10MB 或集合元素 &amp;gt; 万级，会造成阻塞（删除、迁移、复制全卡）。治理手段：拆 Key（按业务维度分片）、&lt;code&gt;UNLINK&lt;/code&gt; 异步删除（4.0+）、&lt;code&gt;SCAN&lt;/code&gt; 替代 &lt;code&gt;KEYS&lt;/code&gt;。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G&lt;/th&gt;
&lt;th&gt;32C256G&lt;/th&gt;
&lt;th&gt;64C512G&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxmemory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10G&lt;/td&gt;
&lt;td&gt;24G&lt;/td&gt;
&lt;td&gt;40G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxmemory-policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;allkeys-lru&lt;/td&gt;
&lt;td&gt;allkeys-lru&lt;/td&gt;
&lt;td&gt;allkeys-lru&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appendonly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appendfsync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;everysec&lt;/td&gt;
&lt;td&gt;everysec&lt;/td&gt;
&lt;td&gt;everysec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;io-threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxclients&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;30000&lt;/td&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash-max-listpack-entries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实例规划&lt;/td&gt;
&lt;td&gt;单实例&lt;/td&gt;
&lt;td&gt;多实例分片（2~4 个）&lt;/td&gt;
&lt;td&gt;多实例分片（4~8 个）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
64C512G 的机器不要跑&lt;strong&gt;一个&lt;/strong&gt; 40G 的 Redis：单实例故障影响面=整机、AOF 重写时 IO 阻塞全实例。生产按业务拆多实例（缓存/会话/排行榜各一个实例），或直接上 Redis Cluster，把单实例的爆炸半径缩小。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redis Cluster 规划&lt;/strong&gt;：16384 个槽按节点均分；3 主 3 从起步，扩容加主节点自动迁移槽；客户端路由（lettuce/jedis cluster 模式）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;哨兵 vs 集群&lt;/strong&gt;：数据量 &amp;lt; 50G 用&quot;主从 + Sentinel（3 台）&quot;即可；数据量大、需水平扩展用 Cluster&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读写分离&lt;/strong&gt;：Cluster 内从节点可读（&lt;code&gt;readonly&lt;/code&gt;），读多场景配读路由；注意从节点读到的数据可能滞后&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存编码利用&lt;/strong&gt;：小对象走紧凑编码（listpack/intset），大批小 value 场景内存省 50%+；&lt;code&gt;MEMORY USAGE key&lt;/code&gt; 验证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过期键清理&lt;/strong&gt;：&lt;code&gt;activedefrag&lt;/code&gt;（碎片整理，6.x）+ 过期键惰性/定期删除机制，&lt;code&gt;maxmemory-policy&lt;/code&gt; 兜底&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接与慢命令治理&lt;/strong&gt;：&lt;code&gt;SLOWLOG GET&lt;/code&gt; 定期查慢命令，大 Key/&lt;code&gt;KEYS&lt;/code&gt;/&lt;code&gt;HGETALL&lt;/code&gt; 大集合列为红线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;故障转移演练&lt;/strong&gt;：Cluster 自动 failover 与手动 &lt;code&gt;CLUSTER FAILOVER&lt;/code&gt; 演练纳入运维计划&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;高可用&lt;/td&gt;
&lt;td&gt;主从 + Sentinel / Cluster&lt;/td&gt;
&lt;td&gt;自动故障转移，秒级切换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据&lt;/td&gt;
&lt;td&gt;RDB/AOF 定期备份&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BGSAVE&lt;/code&gt; 生成 RDB，或直接用持久化文件快照（停写瞬间拷贝）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;异地&lt;/td&gt;
&lt;td&gt;跨机房从节点&lt;/td&gt;
&lt;td&gt;灾备机房挂从节点（或 Cluster 跨机房），切换读流量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;变更&lt;/td&gt;
&lt;td&gt;内存/Key 审计&lt;/td&gt;
&lt;td&gt;定期扫描大 Key 与内存分布（&lt;code&gt;redis-rdb-tools&lt;/code&gt;/&lt;code&gt;MEMORY STATS&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
Redis 备份的误区：把它当唯一数据源。Redis 是缓存与高速通道，&lt;strong&gt;持久化能力是保底不是主用&lt;/strong&gt;——设计上保证&quot;Redis 全丢，业务能重建&quot;（DB 恢复/消息重放），备份只是降低重建成本。生产惯例：缓存场景容忍 AOF 每秒刷盘；数据可重建的业务，Redis 挂了直接重建，不做&quot;数据库式&quot;的严格备份。
:::&lt;/p&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;OOM command not allowed when used memory &amp;gt; &apos;maxmemory&apos;&lt;/code&gt;&lt;/strong&gt;：达到 maxmemory 且淘汰策略不匹配（如 noeviction）；按业务调策略或扩内存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单线程阻塞、延迟毛刺&lt;/strong&gt;：大 Key 删除/迁移、&lt;code&gt;KEYS&lt;/code&gt;、AOF 重写、RDB fork；用 &lt;code&gt;LATENCY&lt;/code&gt; 监控定位，逐个治理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主从复制卡住&lt;/strong&gt;：全量复制时 &lt;code&gt;repl-backlog-size&lt;/code&gt; 太小导致断线重连触发反复全量；调大 backlog（如 128M）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存碎片率高&lt;/strong&gt;：&lt;code&gt;MEMORY FRAGMENTATION&lt;/code&gt; 高于 1.5，开启 &lt;code&gt;activedefrag yes&lt;/code&gt;（7.x 默认）或定期重启实例&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接数打满&lt;/strong&gt;：客户端连接池未回收（&lt;code&gt;maxclients&lt;/code&gt; 报错），查 &lt;code&gt;CLIENT LIST&lt;/code&gt; 定位业务侧泄漏&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;maxmemory + 淘汰策略必设&lt;/li&gt;
&lt;li&gt;大 Key/慢命令红线管控，SLOWLOG 巡检&lt;/li&gt;
&lt;li&gt;AOF everysec + RDB 兜底，备份定期演练&lt;/li&gt;
&lt;li&gt;大机器拆多实例或 Cluster，缩小爆炸半径&lt;/li&gt;
&lt;li&gt;连接池与 maxclients 匹配&lt;/li&gt;
&lt;li&gt;主从/Sentinel/Cluster 故障转移演练&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>RocketMQ 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/rocketmq-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/rocketmq-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;RocketMQ 的调优主线是&lt;strong&gt;刷盘与复制的两对选择&lt;/strong&gt;：异步刷盘还是同步刷盘、异步复制还是同步复制——组合出四种可靠性与性能档位；其次是 Broker 的 JVM 与磁盘 IO。生产默认&quot;异步刷盘 + 异步复制&quot;性能最好，核心链路升到&quot;同步复制&quot;。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;h3&gt;Broker 配置（broker.conf）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flushDiskType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ASYNC_FLUSH&lt;/td&gt;
&lt;td&gt;异步刷盘：写内存立即返回，OS 定期落盘，性能好；SYNC_FLUSH 每次落盘，慢 10 倍+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;brokerRole&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ASYNC_MASTER&lt;/td&gt;
&lt;td&gt;异步复制：主写成功即返回；SYNC_MASTER 等从节点同步完成，双机可用性高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sendMessageThreadPoolNums&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;消息写入线程数，8C 机器调 4~8，写密集核心参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pullMessageThreadPoolNums&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;拉取消息线程数，消费旺盛时调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxMessageSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4M&lt;/td&gt;
&lt;td&gt;单条消息上限，业务大消息按需调整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fileReservedTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;消息文件保留小时数，与磁盘容量联动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deleteWhen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;清理磁盘文件的时刻，业务低峰期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waitTimeMillsInSendQueue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;发送队列满时等待毫秒数，超时直接失败，防队列堆积拖垮&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;操作系统层&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vm.overcommit_memory=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;关闭内存超卖检查，避免大页分配被拒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vm.swappiness=10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;降低 swap 倾向，消息内存优先&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ulimit -n 655350&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文件句柄，Broker 连接与文件数要求高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;可靠性四档组合&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异步刷盘 + 异步复制：性能最好，消息可能丢（宕机窗口）&lt;/li&gt;
&lt;li&gt;同步刷盘 + 异步复制：消息不丢（落盘了），但主宕机同步窗口内从机可能缺数据&lt;/li&gt;
&lt;li&gt;异步刷盘 + 同步复制：性能尚可，主从切换不丢已同步消息&lt;/li&gt;
&lt;li&gt;同步刷盘 + 同步复制：最稳最慢，极少场景使用
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三档规格推荐参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G&lt;/th&gt;
&lt;th&gt;32C256G&lt;/th&gt;
&lt;th&gt;64C512G&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Broker JVM 堆（runbroker.sh）&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;12G&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NameServer 堆（runserver.sh）&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sendMessageThreadPoolNums&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pullMessageThreadPoolNums&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flushDiskType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ASYNC_FLUSH&lt;/td&gt;
&lt;td&gt;ASYNC_FLUSH&lt;/td&gt;
&lt;td&gt;ASYNC_FLUSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;brokerRole&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ASYNC_MASTER&lt;/td&gt;
&lt;td&gt;SYNC_MASTER（核心链路）&lt;/td&gt;
&lt;td&gt;SYNC_MASTER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxMessageSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4M&lt;/td&gt;
&lt;td&gt;4M&lt;/td&gt;
&lt;td&gt;8M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;commitlog 磁盘&lt;/td&gt;
&lt;td&gt;1~2 盘&lt;/td&gt;
&lt;td&gt;4 盘（RAID0/裸盘）&lt;/td&gt;
&lt;td&gt;8+ 盘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fileReservedTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;按容量配&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
Broker JVM 堆&lt;strong&gt;不要给太大&lt;/strong&gt;：RocketMQ 利用 mmap 与页缓存，堆 8~16G 即可，内存留给 OS 页缓存（同 Kafka 的道理）。堆 32G+ 会因 Full GC 造成发送毛刺。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主从成组扩展&lt;/strong&gt;：多组主从（如 3 主 3 从）水平扩展吞吐；topic 建在 master 上，读流量可走 slave（&lt;code&gt;readQueueNums&lt;/code&gt; 与 &lt;code&gt;slaveReadEnable&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broker 分组规划&lt;/strong&gt;：按业务拆分独立 broker 集群（订单集群/日志集群），故障爆炸半径收敛；&lt;code&gt;brokerClusterName&lt;/code&gt; 区分&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Topic 与队列规划&lt;/strong&gt;：写并行度 = master 数 × &lt;code&gt;writeQueueNums&lt;/code&gt;；读并行度 = 消费组消费者数 × &lt;code&gt;readQueueNums&lt;/code&gt;；队列数后期只加不减，规划时按峰值预留&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消费积压治理&lt;/strong&gt;：&lt;code&gt;mqadmin consumerProgress&lt;/code&gt; 巡检；积压持续时先确认消费端能力（线程池、下游 IO），再考虑扩容消费者/加队列&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘 IO 优化&lt;/strong&gt;：commitlog 与 consumequeue 分盘；&lt;code&gt;storePathCommitLog&lt;/code&gt; 放性能最好的盘，&lt;code&gt;storePathRootDir&lt;/code&gt; 分区独立&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NameServer 2~3 台&lt;/strong&gt;：不参与业务流量，轻量部署；客户端 &lt;code&gt;namesrvAddr&lt;/code&gt; 写全&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;高可用&lt;/td&gt;
&lt;td&gt;主从同步复制&lt;/td&gt;
&lt;td&gt;&lt;code&gt;brokerRole=SYNC_MASTER&lt;/code&gt;，主挂从机顶上不丢已同步消息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高可用&lt;/td&gt;
&lt;td&gt;NameServer 多台&lt;/td&gt;
&lt;td&gt;单台故障不影响路由发现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据&lt;/td&gt;
&lt;td&gt;消息重放&lt;/td&gt;
&lt;td&gt;消费端幂等 + 消息可重放设计，业务侧兜底&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据&lt;/td&gt;
&lt;td&gt;文件备份&lt;/td&gt;
&lt;td&gt;commitlog 定期归档到对象存储（离线重放场景）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;监控&lt;/td&gt;
&lt;td&gt;积压/主从同步监控&lt;/td&gt;
&lt;td&gt;dashboard + 告警：积压数、slave 落后数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
RocketMQ 的事务消息（半消息 + 回查）是业务一致性的关键工具：先写半消息，本地事务成功后提交。回查参数 &lt;code&gt;transactionCheckInterval&lt;/code&gt; 与 &lt;code&gt;transactionTimeOut&lt;/code&gt;（默认 6s/回查间隔）按业务事务时长配置，避免高频回查放大 DB 压力；超时未决消息会滞留，需要兜底任务处理。
:::&lt;/p&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;发送超时（&lt;code&gt;sendDefaultImpl call timeout&lt;/code&gt;）&lt;/strong&gt;：Broker 写入线程打满（调 &lt;code&gt;sendMessageThreadPoolNums&lt;/code&gt;）、磁盘慢、或 &lt;code&gt;waitTimeMillsInSendQueue&lt;/code&gt; 太小被提前丢弃&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消费积压持续增长&lt;/strong&gt;：消费线程不足或下游阻塞；先 &lt;code&gt;mqadmin consumerProgress&lt;/code&gt; 看 lag 分布，再调消费者线程池&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动 OOM&lt;/strong&gt;：&lt;code&gt;runbroker.sh&lt;/code&gt;/&lt;code&gt;runserver.sh&lt;/code&gt; 的 Xmx 与机器内存不匹配，按规格表调整&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主从不同步&lt;/strong&gt;：&lt;code&gt;brokerRole=SYNC_MASTER&lt;/code&gt; 时从机故障会导致主写入阻塞，注意 &lt;code&gt;slaveReadEnable&lt;/code&gt; 与健康检查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息重复消费&lt;/strong&gt;：客户端重试 + 主从切换导致；消费端必须幂等（唯一键去重），这是 RocketMQ 语义内的事&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘写满&lt;/strong&gt;：&lt;code&gt;fileReservedTime&lt;/code&gt; 未收敛 + 生产量增长，按容量配 &lt;code&gt;deleteWhen&lt;/code&gt; 与保留时长&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;刷盘/复制组合按可靠性要求定档&lt;/li&gt;
&lt;li&gt;Broker 堆 8~16G，内存留给页缓存&lt;/li&gt;
&lt;li&gt;发送/拉取线程池与核数匹配&lt;/li&gt;
&lt;li&gt;主从成组 + 同步复制（核心链路）&lt;/li&gt;
&lt;li&gt;积压与同步滞后监控告警&lt;/li&gt;
&lt;li&gt;消费端幂等 + 消息重放演练&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Spark 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/spark-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/spark-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Spark 调优围绕四个变量转：&lt;strong&gt;Executor 的数量 × 单 Executor 的内存 × 单 Executor 的核数&lt;/strong&gt;，以及&lt;strong&gt;分区数（并行度）&lt;/strong&gt;。前三个决定资源总量，第四个决定并行度是否与资源匹配——资源给了、并行度不够，照样跑不快。本文给出三档规格的分配方案与常见坑。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.executor.memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;单 Executor 堆内存，结合核数决定 Executor 规模&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.executor.cores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;单 Executor 核数，&lt;strong&gt;建议 3~5&lt;/strong&gt;：太多则单 Executor 内任务并发 IO 互相竞争&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.executor.memoryOverhead&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;堆的 10%&lt;/td&gt;
&lt;td&gt;堆外内存（Metaspace、网络、序列化缓冲），大作业容易在这里 OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.memory.fraction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;堆内执行+存储内存占比，剩余给用户代码；GC 频繁可降到 0.5~0.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.memory.storageFraction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;存储内存占比，缓存多的作业调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Shuffle 分区数，&lt;strong&gt;必须随数据量与集群规模调整&lt;/strong&gt;，公式：总 Executor 核数 × 2~3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.autoBroadcastJoinThreshold&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10M&lt;/td&gt;
&lt;td&gt;小表广播阈值，调到 50~100M 可消除大量 shuffle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.adaptive.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true（3.2+）&lt;/td&gt;
&lt;td&gt;AQE 动态合并/拆分分区、自动处理倾斜，生产必开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.serializer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;换 Kryo：序列化数据量减少 2~5 倍，性能与 GC 双受益&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.local.dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;/tmp&lt;/td&gt;
&lt;td&gt;Shuffle 临时目录，多盘分布（逗号分隔），避免单盘瓶颈&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;Executor 分配的黄金法则&lt;/strong&gt;：单 Executor 核数 3~5，内存 8~32G。每节点 Executor 数 = 可用核 ÷ Executor 核数。宁可 Executor 多而小，不要大而少（单 Executor 太大 → GC 长暂停 + 故障影响面大）。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数（on YARN）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G（单节点）&lt;/th&gt;
&lt;th&gt;32C256G（每节点）&lt;/th&gt;
&lt;th&gt;64C512G（每节点）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;可用核（预留后）&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executor 核数&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executor 内存&lt;/td&gt;
&lt;td&gt;3G&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;td&gt;32G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executor 数&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6~7&lt;/td&gt;
&lt;td&gt;11~12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.executor.memoryOverhead&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.5G&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.driver.memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;2000~4000&lt;/td&gt;
&lt;td&gt;5000~10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;广播阈值&lt;/td&gt;
&lt;td&gt;50M&lt;/td&gt;
&lt;td&gt;100M&lt;/td&gt;
&lt;td&gt;100M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
三档表是&lt;strong&gt;每节点&lt;/strong&gt;的值。集群提交作业时用动态资源分配（&lt;code&gt;spark.dynamicAllocation.enabled=true&lt;/code&gt;），让作业按需申请，避免&quot;一个集群配一套全局参数&quot;的僵化。但 &lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt; 要与作业实际数据量联动，不能只靠资源分配解决。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AQE 三件套全开&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;spark.sql.adaptive.coalescePartitions.enabled=true&lt;/code&gt;（动态合并小分区）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spark.sql.adaptive.skewJoin.enabled=true&lt;/code&gt;（倾斜 join 自动拆分）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spark.sql.adaptive.advisoryPartitionSizeInBytes=64M&lt;/code&gt;（合并后的目标分区大小）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;序列化&lt;/strong&gt;：全部换 Kryo（&lt;code&gt;spark.serializer=org.apache.spark.serializer.KryoSerializer&lt;/code&gt;），注册类减少大对象序列化成本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shuffle 阶段&lt;/strong&gt;：&lt;code&gt;spark.shuffle.compress=true&lt;/code&gt;、&lt;code&gt;spark.shuffle.file.buffer=64k&lt;/code&gt;；&lt;code&gt;spark.local.dir&lt;/code&gt; 每节点 4~8 盘，shuffle 数据 IO 摊开&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存策略&lt;/strong&gt;：频繁复用的中间结果 &lt;code&gt;cache()&lt;/code&gt; 并确认存储级别（&lt;code&gt;storageLevel= MEMORY_AND_DISK&lt;/code&gt;）；用完 &lt;code&gt;unpersist()&lt;/code&gt;，防止内存被缓存挤占执行空间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;join 策略&lt;/strong&gt;：数据量允许的维度表直接广播（阈值 100M 内）；事实表 join 用分桶 + bucket join（&lt;code&gt;spark.sql.bucketing.enabled&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写 HDFS 调小文件&lt;/strong&gt;：&lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt; 与 AQE 合并配合，写出分区数收敛到目标文件数（目标单文件 128~256M）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GC 观察&lt;/strong&gt;：Executor 日志看 GC 时间占比 &amp;gt;10% 时，优先降低 &lt;code&gt;spark.memory.fraction&lt;/code&gt; 给用户代码留空间，或换 G1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;事件日志&lt;/strong&gt;：&lt;code&gt;spark.eventLog.enabled=true&lt;/code&gt; + &lt;code&gt;spark.eventLog.dir=hdfs://...&lt;/code&gt;，配合 HistoryServer 留存全部作业审计与排障依据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流作业 Checkpoint&lt;/strong&gt;：&lt;code&gt;spark.sql.streaming.checkpointLocation&lt;/code&gt; 指向 HDFS，故障恢复不丢进度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Driver 高可用&lt;/strong&gt;：生产统一 &lt;code&gt;--deploy-mode cluster&lt;/code&gt;（Driver 由 YARN 托管，失败自动重试）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作业重跑&lt;/strong&gt;：离线作业幂等设计（目标表先删后写或分区覆盖），失败重跑不产生重复数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Executor 全灭（Lost executor）&lt;/strong&gt;：内存超 YARN 容器上限被 Kill——&lt;code&gt;executor.memory + memoryOverhead&lt;/code&gt; 超过 &lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt;，或节点内存被打满&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GC 时间过长&lt;/strong&gt;：executor 内存与核数不匹配（核多内存少）、&lt;code&gt;spark.memory.fraction&lt;/code&gt; 过高挤占用户代码堆&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据倾斜卡死&lt;/strong&gt;：一个分区拖垮整体——开启 AQE skewJoin，或业务侧对热点 key 加盐（前缀随机数）二次聚合&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小文件成灾&lt;/strong&gt;：&lt;code&gt;shuffle.partitions&lt;/code&gt; 过大 + 输出目标无合并，AQE coalesce 与目标分区数对齐&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广播超限报 OOM&lt;/strong&gt;：&lt;code&gt;autoBroadcastJoinThreshold&lt;/code&gt; 调太大，广播表超过 Executor 内存；阈值按最小 Executor 内存的 1/3 封顶&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Executor = 核 3~5、内存 8~32G，与 YARN 上限匹配&lt;/li&gt;
&lt;li&gt;动态资源分配开启&lt;/li&gt;
&lt;li&gt;shuffle.partitions 与数据量/集群核数联动&lt;/li&gt;
&lt;li&gt;AQE 全开 + Kryo + 多盘 local.dir&lt;/li&gt;
&lt;li&gt;事件日志 + checkpoint + cluster 模式&lt;/li&gt;
&lt;li&gt;定期观察 GC 占比与 Executor 稳定性&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Tez 部署与调优指南</title><link>https://chaggle.github.io/posts/2026/08/10/tez-deploy-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/tez-deploy-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Tez 是 Hive 的 DAG 执行引擎，把多轮 MapReduce 的&quot;写盘-重读&quot;改成一张 DAG 内存流转，SQL 性能提升 2~3 倍是常态。它不独立部署（寄生于 YARN），部署动作是&quot;把 Tez 库放到 HDFS + 让 Hive 引擎指向它&quot;。本文覆盖底层原理（衔接《Tez 底层原理与知识要点》）、部署、参数优化与常见问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;底层原理速览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DAG&lt;/strong&gt;：一次提交整个执行计划图，顶点（Vertex）是逻辑阶段，边（Edge）描述数据流转&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三种 Edge&lt;/strong&gt;：OneToOne（窄依赖）、Broadcast（小表广播）、ScatterGather（shuffle）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session 复用&lt;/strong&gt;：Tez 会话常驻 AM 与容器，多个作业复用，省去 JVM 反复启动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容器复用&lt;/strong&gt;：一个容器内连续跑多个任务，任务启动开销趋近于零&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Tez 的性能来源三句话：&lt;strong&gt;中间结果不落 HDFS、一次调度多阶段、容器 JVM 复用&lt;/strong&gt;。与 MR 对比，复杂 SQL 的磁盘读写从&quot;每阶段一次&quot;降到&quot;只读输入只写输出&quot;。
:::&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;h3&gt;1. 准备 Tez 包并上传 HDFS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 下载与 Hadoop 版本匹配的 tez（如 tez-0.10.2 配 Hadoop 3.3）
wget https://dlcdn.apache.org/tez/0.10.2/apache-tez-0.10.2-bin.tar.gz
tar -zxvf apache-tez-0.10.2-bin.tar.gz -C /opt
ln -s /opt/apache-tez-0.10.2 /opt/tez

# 把 tez 库上传到 HDFS（所有节点通过 tez.lib.uris 引用）
hdfs dfs -mkdir -p /apps/tez
hdfs dfs -put /opt/tez/share/tez.tar.gz /apps/tez/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 配置 tez-site.xml（$TEZ_HOME/conf）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
  &amp;lt;!-- 指向 HDFS 上的 tez 库 --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;tez.lib.uris&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;hdfs://node1:8020/apps/tez/tez.tar.gz&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;!-- AM 与容器内存 --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;tez.am.resource.memory.mb&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;2048&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;tez.container.size&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;4096&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
  &amp;lt;!-- Session 复用：作业间复用 AM，减少启动开销 --&amp;gt;
  &amp;lt;property&amp;gt;
    &amp;lt;name&amp;gt;tez.session.client.timeout&amp;lt;/name&amp;gt;
    &amp;lt;value&amp;gt;300&amp;lt;/value&amp;gt;
  &amp;lt;/property&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Hive 切换引擎&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 环境变量（所有跑 Hive 的节点）
export TEZ_HOME=/opt/tez
export TEZ_JARS=/opt/tez/share/tez.tar.gz
export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:/opt/tez/share/tez.tar.gz

# 会话级或 hive-site.xml 配置
set hive.execution.engine=tez;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数优化（三档规格）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G（每节点）&lt;/th&gt;
&lt;th&gt;32C256G（每节点）&lt;/th&gt;
&lt;th&gt;64C512G（每节点）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.am.resource.memory.mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.am.java.opts&lt;/code&gt;（AM 堆）&lt;/td&gt;
&lt;td&gt;768M&lt;/td&gt;
&lt;td&gt;1.5G&lt;/td&gt;
&lt;td&gt;3G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.container.size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.task.max.memory.mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.5G&lt;/td&gt;
&lt;td&gt;3G&lt;/td&gt;
&lt;td&gt;6G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.task.scale.memory.additional-resource.fraction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.session.client.timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.runtime.sort.threads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
&lt;code&gt;tez.container.size&lt;/code&gt; 必须与 Hive 的 &lt;code&gt;hive.tez.container.size&lt;/code&gt; 一致（Hive 侧是配置入口），且容器总量不能超过 YARN 的 &lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt;。容器申请超过上限会被 YARN 拒绝，作业直接失败。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化理解&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;容器内存 = 数据排序的池子&lt;/strong&gt;：Tez 的 shuffle 在容器内排序，&lt;code&gt;tez.container.size&lt;/code&gt; 越大，单任务能处理的数据量越大，溢写（spill）越少；但容器大 → 并发任务少，需要按数据量平衡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session 超时&lt;/strong&gt;：&lt;code&gt;tez.session.client.timeout&lt;/code&gt; 是会话闲置回收时间；调大让跑批作业间的复用率提高（省 AM 启动），但占用 YARN 资源不释放，按作业密度取舍&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AM 内存&lt;/strong&gt;：&lt;code&gt;tez.am.resource.memory.mb&lt;/code&gt; 只影响 AM 本身，占资源小，2~4G 足够，不用跟着规格涨&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;容器复用是收益主力&lt;/strong&gt;：批作业密集时段让 Session 常驻（timeout 调大），削峰时段回收；配合 YARN 队列隔离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 YARN 资源联动&lt;/strong&gt;：&lt;code&gt;tez.container.size&lt;/code&gt; 别超过节点可用内存 ÷ 期望并发任务数；优先保证单节点 2~4 个 Tez 容器并发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地模式&lt;/strong&gt;：小任务自动降级 &lt;code&gt;tez.local.mode=true&lt;/code&gt;（本地文件系统跑），不占 YARN 资源，适合测试与临时查询&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据倾斜&lt;/strong&gt;：&lt;code&gt;tez.groupby.split.grouping.enabled&lt;/code&gt; + 二阶段聚合兜底；与 Hive 的 &lt;code&gt;hive.groupby.skewindata&lt;/code&gt; 配合&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RPC 与元数据&lt;/strong&gt;：&lt;code&gt;tez.am.am-rm.heartbeat.interval-ms&lt;/code&gt;（默认 1s）在百节点集群可放宽到 5s，减少心跳流量&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ClassNotFoundException: org.apache.tez.dag...&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;tez.lib.uris&lt;/code&gt; 指向的 HDFS 路径不存在或权限不足，或节点没配 &lt;code&gt;HADOOP_CLASSPATH&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作业卡在 INITIALIZING&lt;/strong&gt;：AM 无法获取容器——&lt;code&gt;tez.am.resource.memory.mb&lt;/code&gt; 超 YARN 上限，或 Session 被回收后客户端仍在等旧会话&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Container OOM&lt;/strong&gt;：&lt;code&gt;tez.container.size&lt;/code&gt; 与 &lt;code&gt;hive.tez.container.size&lt;/code&gt; 不一致（Hive 侧默认 512M 覆盖）、或排序数据量超过容器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session 僵死&lt;/strong&gt;：&lt;code&gt;tez.session.client.timeout&lt;/code&gt; 太小导致频繁建 Session，作业排队；调大或确认 AM 被 YARN 回收后客户端重连逻辑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地模式误开&lt;/strong&gt;：&lt;code&gt;tez.local.mode&lt;/code&gt; 开了但本地磁盘不足，任务失败；确认本地目录空间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 Spark 引擎切换后参数残留&lt;/strong&gt;：&lt;code&gt;hive.execution.engine&lt;/code&gt; 切换时，两边引擎参数（tez.&lt;em&gt;/spark.&lt;/em&gt;）互相残留影响，切换后验证首条 SQL&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;部署检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;tez.tar.gz 在 HDFS 且 &lt;code&gt;tez.lib.uris&lt;/code&gt; 正确&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hive.tez.container.size&lt;/code&gt; 与 YARN 上限匹配&lt;/li&gt;
&lt;li&gt;Session 超时与作业密度匹配&lt;/li&gt;
&lt;li&gt;与 08-09 原理文章联动：Edge 类型决定 shuffle 成本，优化 SQL 时先看执行计划（&lt;code&gt;EXPLAIN&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Zookeeper 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/zookeeper-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/zookeeper-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Zookeeper 是&quot;轻量但金贵&quot;的组件：它吃&lt;strong&gt;磁盘 IO（事务日志）&lt;/strong&gt;、吃&lt;strong&gt;网络（同步）&lt;/strong&gt;、吃&lt;strong&gt;GC（内存）&lt;/strong&gt;。调优的核心是保护它的这三个资源——ZK 抖动一次，Kafka/HBase/HDFS HA 全跟着抖。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tickTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2000ms&lt;/td&gt;
&lt;td&gt;基础时间单位，所有超时都是它的倍数；调大 = 放宽心跳容忍，网络差的机房可调到 3000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;follower 启动同步的容忍心跳数，节点多、数据大时调大（如 20~30）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;syncLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;运行中 leader-follower 心跳容忍，抖动检测的灵敏度，一般 5~10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dataDir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;快照目录；与事务日志目录分开（&lt;code&gt;dataLogDir&lt;/code&gt;）是关键优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dataLogDir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;事务日志目录：&lt;strong&gt;必须独立磁盘&lt;/strong&gt;，ZK 写性能看它&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;snapCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;td&gt;每 10 万事务触发一次快照；事务量大时快照频繁会毛刺，可调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;autopurge.snapRetainCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;保留快照数；&lt;code&gt;autopurge.purgeInterval&lt;/code&gt;（小时）配套开启自动清理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxClientCnxns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;单客户端 IP 最大连接数，连接池大的业务要调大（如 1000+）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM 堆&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;ZK 数据全在内存，堆 2~4G 足够；&lt;strong&gt;堆大反而 Full GC 更痛&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;ZK 调优第一原则：堆不用大，但要稳&lt;/strong&gt;。ZK 的快照、数据都吃内存，但 4G 堆能装下几百万节点。堆设 32G 只会让 Full GC 暂停长达数秒，集群直接断连。宁可在数据量规划上控制，不要盲目加堆。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G（3 节点）&lt;/th&gt;
&lt;th&gt;32C256G（中型集群配套）&lt;/th&gt;
&lt;th&gt;64C512G（大型集群配套）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;部署规格&lt;/td&gt;
&lt;td&gt;独立 8C16G 即可&lt;/td&gt;
&lt;td&gt;独立 8C16G（多实例隔离）&lt;/td&gt;
&lt;td&gt;独立 16C32G（多实例隔离）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JVM 堆&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tickTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;syncLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxClientCnxns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;3000+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;snapCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;td&gt;200000&lt;/td&gt;
&lt;td&gt;200000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;autopurge.purgeInterval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
大型集群的坑：&lt;strong&gt;不要把所有业务都塞进同一个 ZK 集群&lt;/strong&gt;。Kafka、HBase、HDFS HA、DolphinScheduler 各自连接，一个 ZK 抖动全链路雪崩。生产按职责拆分 ZK 集群（如&quot;消息集群专用&quot;&quot;HDFS HA 专用&quot;），每个集群独立 3~5 节点。
:::&lt;/p&gt;
&lt;h2&gt;集群规模优化（几十~上百节点）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;节点数保持 3/5/7&lt;/strong&gt;：ZK 是 CP 系统，节点越多写性能越差（过半同步），7 个封顶；上百节点的大集群让 ZK 只服务&quot;元数据协调&quot;而非&quot;每个业务会话&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话与 Watcher 管控&lt;/strong&gt;：客户端连接数和 Watcher 数量是主要内存与 CPU 消耗，压测前先量化（&lt;code&gt;mntr&lt;/code&gt;、&lt;code&gt;wchs&lt;/code&gt; 四字命令）；长连接会话配置心跳合理，避免 SessionTimeout 过大导致故障发现慢&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大节点禁止&lt;/strong&gt;：单个 ZNode 超过 1M 直接让该节点读写卡死（写复制全集群）；配置项、元数据按小键值拆分&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络隔离&lt;/strong&gt;：ZK 节点间走独立内网/独立网卡，避免与大数据作业共享带宽；跨机房部署时 tickTime 相应放大&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快照与日志管理&lt;/strong&gt;：&lt;code&gt;autopurge&lt;/code&gt; 开启 + 定时巡检磁盘；&lt;code&gt;dataDir&lt;/code&gt;/&lt;code&gt;dataLogDir&lt;/code&gt; 分盘后快照与日志互不争 IO&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;手段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;集群内&lt;/td&gt;
&lt;td&gt;3~5 节点奇数集群&lt;/td&gt;
&lt;td&gt;过半机制，容忍少数节点故障&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨机房&lt;/td&gt;
&lt;td&gt;2 机房 + 仲裁节点&lt;/td&gt;
&lt;td&gt;数据双写或就近集群 + 跨机房同步，注意过半要求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dataDir&lt;/code&gt; 定期快照&lt;/td&gt;
&lt;td&gt;停写瞬间 tar 快照目录，异机冷备（ZK 数据本身小，秒级完成）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;变更&lt;/td&gt;
&lt;td&gt;配置变更审计&lt;/td&gt;
&lt;td&gt;业务侧的配置变更日志留存，配合 ZK 快照可回放&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
ZK 是&lt;strong&gt;无数据丢失设计&lt;/strong&gt;：事务日志落盘成功才返回成功。因此它的&quot;备份&quot;主要防&quot;集群全灭&quot;（机房级），日常故障靠集群内副本即可恢复，冷备是最后防线。
:::&lt;/p&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;leader 频繁切换&lt;/strong&gt;：syncLimit 太小（网络抖动即判死）或 GC 卡顿；看日志 &lt;code&gt;FLE&lt;/code&gt; 与 GC 时间线，放宽 syncLimit、检查堆&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Connection refused&lt;/code&gt;/大量断连&lt;/strong&gt;：连接数超 &lt;code&gt;maxClientCnxns&lt;/code&gt;、文件句柄不足（&lt;code&gt;ulimit -n&lt;/code&gt; 调 65535+）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘写满卡死&lt;/strong&gt;：事务日志目录打满，ZK 会停写；&lt;code&gt;dataLogDir&lt;/code&gt; 独立盘 + autopurge + 容量监控&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读放大：getChildren 巨多&lt;/strong&gt;：大目录（上万子节点）操作 O(n) 耗时，业务侧避免&quot;目录即列表&quot;的用法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GC 停顿导致集群抖动&lt;/strong&gt;：先减堆再减对象（节点数），检查是否把 ZK 当缓存用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;事务日志独立磁盘，快照日志分目录&lt;/li&gt;
&lt;li&gt;堆 2~4G，不盲目加&lt;/li&gt;
&lt;li&gt;连接数/文件句柄配额放行&lt;/li&gt;
&lt;li&gt;按职责拆分 ZK 集群（大集群）&lt;/li&gt;
&lt;li&gt;autopurge 开启、磁盘容量监控&lt;/li&gt;
&lt;li&gt;故障演练：杀一台节点观察过半与恢复&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>YARN 生产调优实践</title><link>https://chaggle.github.io/posts/2026/08/10/yarn-tuning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/10/yarn-tuning/</guid><pubDate>Sun, 09 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;YARN 调优的核心是&lt;strong&gt;算好每台机器能拿出多少资源给容器&lt;/strong&gt;，以及&lt;strong&gt;资源怎么分给不同业务&lt;/strong&gt;。前者是内存与核数预算（要留给操作系统和系统进程），后者是队列规划（Capacity Scheduler）。调不好最常见的症状：作业一直 ACCEPTED 不跑、容器 OOM、大作业挤死小作业。部署安装见《大数据与中间件组件部署总览》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心参数与调优理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;调优理解&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;单节点可分配给容器的内存总量，&lt;strong&gt;必须小于物理内存&lt;/strong&gt;，预留系统与内核&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.resource.cpu-vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;单节点可分配核数，一般 = 物理核数（超售有风险）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;单个容器内存上限，限制大作业的&quot;一口吃太多&quot;，也防止死配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.maximum-allocation-vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;单个容器核数上限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.minimum-allocation-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;单个容器内存下限（资源粒度）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.pmem-check-enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;物理内存超限检查，误杀大容器时考虑关闭或调大容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.vmem-check-enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;虚拟内存检查，Spark/Flink 大作业常被它误杀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.resourcemanager.am.max-attempts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;ApplicationMaster 重试次数，作业失败自动拉起&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
&lt;strong&gt;内存预算公式&lt;/strong&gt;：可分配内存 = 物理内存 - 系统预留。经验值：16G 机器预留 3~4G，256G 机器预留 20~30G（给操作系统页缓存、Agent、JVM 元空间），512G 机器预留 40~50G。
:::&lt;/p&gt;
&lt;h2&gt;三档规格推荐参数&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;8C16G&lt;/th&gt;
&lt;th&gt;32C256G&lt;/th&gt;
&lt;th&gt;64C512G&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12G（预留 4G）&lt;/td&gt;
&lt;td&gt;220G（预留 36G）&lt;/td&gt;
&lt;td&gt;460G（预留 52G）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.resource.cpu-vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8G&lt;/td&gt;
&lt;td&gt;64G&lt;/td&gt;
&lt;td&gt;128G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.maximum-allocation-vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.minimum-allocation-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512M&lt;/td&gt;
&lt;td&gt;1G&lt;/td&gt;
&lt;td&gt;2G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.pmem-check-enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;true（大容器作业多时关）&lt;/td&gt;
&lt;td&gt;关&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ResourceManager 堆（YARN_RESOURCEMANAGER_OPTS）&lt;/td&gt;
&lt;td&gt;4G&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;td&gt;32G&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
被 &lt;code&gt;pmem-check-enabled&lt;/code&gt; 误杀是 YARN 最经典的问题：检查逻辑按容器申请值限定&quot;软限制&quot;，容器实际内存超出就被 NodeManager 杀掉。Hive/Spark 大作业集群普遍直接关闭物理内存与虚拟内存检查，靠 &lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt; 总量兜底，换取稳定性。
:::&lt;/p&gt;
&lt;h2&gt;队列规划（几十~上百节点）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;队列&lt;/th&gt;
&lt;th&gt;容量&lt;/th&gt;
&lt;th&gt;适用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;root.default&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;临时作业、测试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;root.offline&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;跑批作业（离线数仓）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;root.realtime&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;实时计算（Flink）、交互查询&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Capacity Scheduler 关键参数（&lt;code&gt;capacity-scheduler.xml&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;yarn.scheduler.capacity.root.queues&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;default,offline,realtime&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;yarn.scheduler.capacity.root.offline.capacity&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;50&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;yarn.scheduler.capacity.root.realtime.maximum-capacity&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;80&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
队列规划的原则：&lt;strong&gt;核心业务队列有保底容量（capacity）+ 弹性上限（maximum-capacity）&lt;/strong&gt;。实时队列弹性上限设 80%，避免离线跑批把资源吃光后 Flink 作业饿死；&lt;code&gt;capacity&lt;/code&gt; 之和必须等于 100。
:::&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;百节点资源分区&lt;/strong&gt;：计算密集节点（高核）与内存密集节点（高内存）用分区（partition）隔离，调度时按分区投放作业&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NodeManager 多本地目录&lt;/strong&gt;：&lt;code&gt;yarn.nodemanager.local-dirs&lt;/code&gt; 多盘，容器临时数据分散 IO；&lt;code&gt;yarn.nodemanager.log-dirs&lt;/code&gt; 同&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘健康检查&lt;/strong&gt;：&lt;code&gt;yarn.nodemanager.disk-health-checker.enabled=true&lt;/code&gt;（默认开），坏盘节点自动变为 unhealthy 不接新容器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源抢占&lt;/strong&gt;：&lt;code&gt;yarn.resourcemanager.scheduler.monitor.enable&lt;/code&gt; + ProportionalCapacityPreemptionPolicy，保障队列容量&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;容灾与备份&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ResourceManager HA&lt;/strong&gt;：双 RM + Zookeeper，秒级切换；切换时运行中作业的 AM 会重试（&lt;code&gt;yarn.resourcemanager.am.max-attempts&lt;/code&gt; 调 3~5）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作业日志聚合&lt;/strong&gt;：&lt;code&gt;yarn.log-aggregation-enable=true&lt;/code&gt;，作业日志汇总到 HDFS，节点销毁后仍可查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ApplicationMaster 重试&lt;/strong&gt;：&lt;code&gt;yarn.resourcemanager.am.max-attempts&lt;/code&gt;，配合调度器避免单点故障导致作业丢失&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;队列配置备份&lt;/strong&gt;：&lt;code&gt;capacity-scheduler.xml&lt;/code&gt; 纳入版本管理，变更前 &lt;code&gt;yarn rmadmin -refreshQueues&lt;/code&gt; 验证&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优常见问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作业一直 ACCEPTED&lt;/strong&gt;：队列容量被占满或 &lt;code&gt;maximum-allocation&lt;/code&gt; 小于作业申请；&lt;code&gt;yarn application -list&lt;/code&gt; 看待运行队列，&lt;code&gt;yarn queue -status&lt;/code&gt; 看容量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容器被 NodeManager 杀&lt;/strong&gt;：看节点日志的 &lt;code&gt;KillContainer&lt;/code&gt; 原因——内存超限（关闭检查或调容器）、磁盘超限（&lt;code&gt;yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单节点资源上不去&lt;/strong&gt;：检查 NM 上报内存是否等于 &lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt;（改了配置要重启 NM），以及是否被 &lt;code&gt;yarn-site.xml&lt;/code&gt; 的别名旧参数覆盖&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大作业独占集群&lt;/strong&gt;：容量调度器默认没有超卖限制，给大作业所在队列设 &lt;code&gt;maximum-capacity&lt;/code&gt; 上限&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;调优检查清单&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;每节点内存/核数预算正确（预留系统）&lt;/li&gt;
&lt;li&gt;队列容量规划并启用弹性上限&lt;/li&gt;
&lt;li&gt;大作业集群关闭 pmem/vmem 检查&lt;/li&gt;
&lt;li&gt;RM HA + 日志聚合 + AM 重试&lt;/li&gt;
&lt;li&gt;NodeManager 多盘 local-dirs，坏盘自动隔离&lt;/li&gt;
&lt;li&gt;集群资源水位、队列使用率纳入监控&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>AI Agent 知识概念总结</title><link>https://chaggle.github.io/posts/2026/08/09/ai-agent-concepts/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/ai-agent-concepts/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;从 LLM 到 Agent：大模型应用落地的最重要形态——AI Agent（智能体）核心知识概念梳理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;什么是 AI Agent&lt;/h2&gt;
&lt;p&gt;AI Agent（人工智能智能体）是指&lt;strong&gt;以大语言模型（LLM）为大脑核心，能够自主感知环境、做出决策并执行行动&lt;/strong&gt;的智能系统。它不只是&quot;问答机器人&quot;，而是能理解目标、拆解任务、调用工具并最终完成复杂任务的&lt;strong&gt;自主执行者&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;:::note
一句话概括：LLM 负责&quot;思考&quot;，Agent 负责&quot;做事&quot;。传统 AI 应用是人提问 AI 回答，Agent 则是人提出目标，AI 自己规划并执行。
:::&lt;/p&gt;
&lt;h2&gt;Agent 的五大核心组成&lt;/h2&gt;
&lt;p&gt;一个完整的 AI Agent 通常由以下五个模块构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LLM 大脑（Brain）&lt;/strong&gt;：负责理解、推理、决策，是 Agent 的智力核心&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规划（Planning）&lt;/strong&gt;：将复杂目标拆解为可执行的子任务序列&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记忆（Memory）&lt;/strong&gt;：存储历史交互与经验，支持上下文与长期知识&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具（Tools）&lt;/strong&gt;：调用外部能力，如搜索、计算器、API、代码执行等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行动（Action）&lt;/strong&gt;：将决策转化为实际输出，如生成文本、操作软件、调用接口&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;感知-思考-行动循环（ReAct 模式）&lt;/h2&gt;
&lt;p&gt;Agent 的核心工作模式是 &lt;strong&gt;ReAct（Reasoning + Acting）&lt;/strong&gt;，即思考与行动交替进行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;感知环境 → 思考推理 → 采取行动 → 观察结果 → 再次思考...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个循环不断迭代，直到完成任务。ReAct 模式让 Agent 摆脱了&quot;一步到位的回答&quot;，而能够像人一样&lt;strong&gt;边做边想、根据反馈修正&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;规划能力&lt;/h2&gt;
&lt;p&gt;规划是 Agent 区别于普通对话模型的标志性能力，主要包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;任务分解&lt;/strong&gt;：把大目标拆成若干子任务（如&quot;写一篇报告&quot;拆为：查资料 → 列提纲 → 逐章撰写 → 校对）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思维链（Chain-of-Thought, CoT）&lt;/strong&gt;：通过逐步推理提升复杂问题的解决准确率&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自我反思（Self-Refine）&lt;/strong&gt;：执行后对结果进行自我检查与修正&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;树状思考（Tree-of-Thought, ToT）&lt;/strong&gt;：探索多条推理路径，择优而行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
规划能力是 Agent 从&quot;能用&quot;到&quot;好用&quot;的分水岭：任务拆得越合理，执行质量越高。
:::&lt;/p&gt;
&lt;h2&gt;记忆机制&lt;/h2&gt;
&lt;p&gt;Agent 的记忆分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;短期记忆（工作记忆）&lt;/strong&gt;：当前任务中的对话上下文，依赖 LLM 的上下文窗口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长期记忆（持久记忆）&lt;/strong&gt;：跨会话的知识沉淀，通常借助向量数据库（如 Milvus、pgvector）以&lt;strong&gt;向量化 + 相似度检索&lt;/strong&gt;的方式存取&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;长期记忆让 Agent 能够&quot;记得用户偏好&quot;&quot;积累领域知识&quot;，也是 RAG（检索增强生成）技术的基础。&lt;/p&gt;
&lt;h2&gt;工具使用与 Function Calling&lt;/h2&gt;
&lt;p&gt;Agent 通过&lt;strong&gt;工具调用&lt;/strong&gt;突破 LLM 自身的局限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Function Calling&lt;/strong&gt;：LLM 根据用户需求输出结构化调用指令，由程序执行真实函数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAG（检索增强生成）&lt;/strong&gt;：先从知识库检索相关内容，再交给 LLM 生成答案，解决幻觉问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP（Model Context Protocol）&lt;/strong&gt;：标准化 Agent 与外部工具/数据源的连接协议，让一个 Agent 对接多个生态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
工具是 Agent 的&quot;手和脚&quot;。没有工具的 LLM 只能回答&quot;怎么做&quot;，有了工具才能真正&quot;做到&quot;。
:::&lt;/p&gt;
&lt;h2&gt;多智能体协作&lt;/h2&gt;
&lt;p&gt;单个 Agent 能力有限，复杂任务可由&lt;strong&gt;多个分工明确的 Agent 协作完成&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分工模式&lt;/strong&gt;：如 Product Manager Agent 拆需求 → Developer Agent 写代码 → Reviewer Agent 审查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对话模式&lt;/strong&gt;：多个 Agent 相互辩论、互补，提升决策质量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;典型案例&lt;/strong&gt;：MetaGPT 模拟软件公司各角色协作产出整个项目&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见 Agent 框架与平台&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LangChain / LangGraph&lt;/strong&gt;：最流行的 Agent 开发框架，支持编排、记忆、工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AutoGPT / MetaGPT&lt;/strong&gt;：早期多智能体自主任务框架&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CrewAI&lt;/strong&gt;：轻量级多角色协作框架&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dify / Coze（扣子）/ FastGPT&lt;/strong&gt;：低代码 Agent 搭建平台，业务人员也可快速上手&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Agent 与传统程序的区别&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;传统程序&lt;/th&gt;
&lt;th&gt;AI Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;逻辑来源&lt;/td&gt;
&lt;td&gt;人工编写的固定规则&lt;/td&gt;
&lt;td&gt;LLM 动态推理生成&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;任务适配&lt;/td&gt;
&lt;td&gt;需求变则代码变&lt;/td&gt;
&lt;td&gt;自然语言描述即可调整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;容错能力&lt;/td&gt;
&lt;td&gt;异常即崩溃&lt;/td&gt;
&lt;td&gt;可自我反思、修正&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;扩展能力&lt;/td&gt;
&lt;td&gt;需重新开发&lt;/td&gt;
&lt;td&gt;挂接工具即扩展&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;应用场景&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开发助手&lt;/strong&gt;：理解需求、生成代码、修复 Bug、执行 CI/CD&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;智能客服&lt;/strong&gt;：自主查询订单、解答问题、引导售后流程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;办公自动化&lt;/strong&gt;：整理文档、汇总邮件、生成报表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;个人助理&lt;/strong&gt;：安排日程、管理信息、跨应用操作&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;局限与挑战&lt;/h2&gt;
&lt;p&gt;:::caution&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;幻觉问题&lt;/strong&gt;：LLM 可能一本正经地输出错误结论，工具与 RAG 只能缓解不能根除&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;稳定性&lt;/strong&gt;：多步执行链路长，任一环节出错都可能让整个任务跑偏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成本与延迟&lt;/strong&gt;：多轮思考 + 工具调用消耗大量 Token，速度与费用仍需权衡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全与权限&lt;/strong&gt;：Agent 自主执行外部操作，需要严格的权限控制与审计机制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评测困难&lt;/strong&gt;：开放任务的执行质量难以自动化衡量
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;AI Agent 的底层逻辑并不神秘：&lt;strong&gt;LLM 做大脑、规划做思路、记忆做沉淀、工具做手脚、行动做结果&lt;/strong&gt;。理解这五个概念，就抓住了 Agent 的骨架。至于它能走多远，取决于工程化能力——毕竟从&quot;能演示&quot;到&quot;能稳定上线&quot;，中间隔着大量工程细节。&lt;/p&gt;
&lt;p&gt;:::tip
技术的演进永远是从&quot;人适应机器&quot;走向&quot;机器适应人&quot;。Agent 正是这条路上关键的一步。
:::&lt;/p&gt;
</content:encoded></item><item><title>Flink 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/flink-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/flink-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是对 Flink 核心知识的系统性总结，覆盖架构、流处理模型、窗口与 Watermark、状态编程、Checkpoint 容错、常用参数以及日常问题排查要点，作为个人技术笔记使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;整体架构：JobManager 与 TaskManager&lt;/h2&gt;
&lt;p&gt;Flink 是真正的流式计算框架（Streaming First），采用主从架构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JobManager（JobMaster）&lt;/strong&gt;：集群的大脑，负责作业调度、Checkpoint 协调、故障恢复。内部包含 ResourceManager、Dispatcher、JobMaster&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TaskManager（TaskExecutor）&lt;/strong&gt;：实际执行任务的工作进程，一个 TM 上有若干 Slot（资源单元），一个 Slot 跑一个线程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slot&lt;/strong&gt;：TaskManager 内存资源的最小分配单位。默认一个 Slot 可以跑多个 Task 的共享模式（slots per TM 控制 Slot 数）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client&lt;/strong&gt;：提交作业的入口，只负责提交（构建 JobGraph 后发出），不参与计算&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Client ──提交 JobGraph──▶ JobManager
                          ├─ 解析成 ExecutionGraph
                          ├─ 调度 Task 到 Slot
                          └─ 协调 Checkpoint
TaskManager  ◀── 部署 Task / SubTask ──▶ TaskManager
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Flink 与 Spark Streaming 的本质区别：Flink 每个数据事件一进来就被处理（真正的流），而 Spark Streaming 是微批次（Micro-Batch）模拟流。因此 Flink 天然延迟更低（毫秒级 vs 秒级）。
:::&lt;/p&gt;
&lt;h3&gt;JobGraph / ExecutionGraph / Task / SubTask&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JobGraph&lt;/strong&gt;：Client 提交的作业逻辑图，包含 Operator 节点和边&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ExecutionGraph&lt;/strong&gt;：JobManager 将 JobGraph 并行化后生成的执行图，每个 Operator 变成多个并行子任务（SubTask）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SubTask&lt;/strong&gt;：算子的一个并行实例，是 Flink 调度的最小单元。一个 TaskManager 的一个线程跑一个 SubTask&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并行度（Parallelism）&lt;/strong&gt;：算子并行实例的个数，可在算子、执行环境、提交参数三层设置，取最近一层的设置&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;部署模式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Session（会话）模式&lt;/strong&gt;：共享一个 Flink 集群跑多个作业，JobManager 已启动，提交快，但作业间资源隔离差、一个作业出问题可能拖垮集群&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-Job 模式&lt;/strong&gt;：每个作业独立启动一个集群，作业结束集群释放，资源隔离好（Flink 1.15+ 已废弃）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application 模式&lt;/strong&gt;：main 方法在集群内执行，每个应用一个 JobManager，适合生产&lt;/li&gt;
&lt;li&gt;资源管理：Standalone（自管）、YARN（yarn-session / yarn-application）、Kubernetes（原生 K8s）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
生产环境推荐 YARN Application 或 K8s 部署：资源按作业隔离，main 方法跑在集群内，本地不需要提交环境。
:::&lt;/p&gt;
&lt;h2&gt;流处理模型与 DataStream&lt;/h2&gt;
&lt;h3&gt;有界流与无界流&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无界流（Unbounded）&lt;/strong&gt;：数据持续产生永不结束，必须用事件时间 + Watermark + 窗口来处理，如 Kafka 实时接入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有界流（Bounded）&lt;/strong&gt;：数据有明确边界，本质上是批处理，Flink 用同一套 DataStream API 处理，如读文件/数据库&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;DataStream 常用算子&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;无状态算子：map、flatMap、filter&lt;/li&gt;
&lt;li&gt;分区算子：keyBy（按 key 分组，产生网络 shuffle）、rebalance（轮询）、broadcast、partitionCustom&lt;/li&gt;
&lt;li&gt;有状态算子：window、process（ProcessFunction 是最灵活的底层 API）&lt;/li&gt;
&lt;li&gt;侧输出流（Side Output）：把脏数据、迟到数据分流，是生产环境标配&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;窗口与 Watermark&lt;/h2&gt;
&lt;h3&gt;三类窗口&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;滚动窗口（Tumbling）&lt;/strong&gt;：固定大小、互不重叠，如每 10 秒统计一次&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;滑动窗口（Sliding）&lt;/strong&gt;：固定大小 + 滑动步长，重叠，如每 5 秒统计过去 10 秒&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话窗口（Session）&lt;/strong&gt;：按不活跃间隔切分，适合用户行为会话类统计&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 滚动窗口示例：每 10s 按 userId 统计订单金额
dataStream
  .keyBy(_.userId)
  .window(TumblingEventTimeWindows.of(Time.seconds(10)))
  .aggregate(new OrderAggFunc)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Watermark：处理乱序数据&lt;/h3&gt;
&lt;p&gt;Watermark 是 Flink 处理乱序的核心机制，语义：&lt;strong&gt;&quot;时间戳 &amp;lt;= Watermark 的事件已经全部到达&quot;&lt;/strong&gt;。当事件时间超过当前窗口结束时间时，窗口才会触发计算。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Watermark = 已见最大事件时间 - 允许乱序的时间（BoundedOutOfOrderness）&lt;/li&gt;
&lt;li&gt;Watermark 必须与事件时间（EventTime）配合，指定 &lt;code&gt;env.setStreamTimeCharacteristic&lt;/code&gt;（1.12+ 默认事件时间）&lt;/li&gt;
&lt;li&gt;迟到事件处理：Watermark 之后的迟到数据，可丢弃、可落到侧输出流&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;val stream = source
  .assignTimestampsAndWatermarks(
    WatermarkStrategy
      .forBoundedOutOfOrderness[Order](Duration.ofSeconds(5))
      .withTimestampAssigner(_.timestamp)
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
Watermark 是在 Task 内部基于分区推进的，多个分区要先经过对齐（取最小），才决定窗口是否触发。单个分区长时间无数据，会拖住整个窗口——常见坑。
:::&lt;/p&gt;
&lt;h2&gt;状态编程&lt;/h2&gt;
&lt;h3&gt;Keyed State&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;状态必须以 keyBy 为前提，按 key 隔离（每个 key 一份）&lt;/li&gt;
&lt;li&gt;常用类型：ValueState、ListState、MapState、ReducingState、AggregatingState&lt;/li&gt;
&lt;li&gt;无 key 的算子可用 Operator State（如 Kafka offset 的存储）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class MyProcess extends KeyedProcessFunction[String, Order, Result] {
  var totalState: ValueState[Double] = _
  override def open(parameters: Configuration): Unit = {
    val desc = new ValueStateDescriptor[Double](&quot;total&quot;, classOf[Double])
    totalState = getRuntimeContext.getState(desc)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;状态后端&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;后端&lt;/th&gt;
&lt;th&gt;存储&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HashMap（堆内存）&lt;/td&gt;
&lt;td&gt;JVM 堆&lt;/td&gt;
&lt;td&gt;快，但受 GC 影响大，大状态 OOM 风险高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RocksDB（默认）&lt;/td&gt;
&lt;td&gt;本地磁盘（嵌入 RocksDB，LMS 结构）&lt;/td&gt;
&lt;td&gt;状态量大时首选，增量 Checkpoint 快，代价是序列化开销&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存文件系统&lt;/td&gt;
&lt;td&gt;实验性&lt;/td&gt;
&lt;td&gt;目前不建议生产使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
RocksDB 适合 100GB 以上的大状态；毫秒级延迟敏感且状态小时可选堆内存后端。增量 Checkpoint 只在 RocksDB 上支持。
:::&lt;/p&gt;
&lt;h2&gt;容错机制：Checkpoint 与 Exactly-Once&lt;/h2&gt;
&lt;h3&gt;Checkpoint：Barrier 对齐&lt;/h3&gt;
&lt;p&gt;Flink 周期性对算子状态做一致性快照，核心是 &lt;strong&gt;Checkpoint Barrier&lt;/strong&gt;（屏障）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Source(offset) ──Barrier N──▶ 上游算子 ──▶ Sink(写临时事务)
        Barrier 随数据流传播，每个算子收到 Barrier 即对状态做快照
        全部算子快照成功 → 本次 Checkpoint 完成（New 变 Completed）
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Barrier 对齐&lt;/strong&gt;：多上游（如 Kafka 多分区）算子必须等所有输入的 Barrier 都到齐再快照，期间缓存数据。这是 Exactly-Once 的关键，代价是期间数据排队（可关闭对齐换性能，退化为 At-Least-Once）&lt;/li&gt;
&lt;li&gt;快照存到外部存储：JobManager 内存（小状态）或 HDFS/OSS（&lt;code&gt;state.checkpoints.dir&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Savepoint&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;手动触发的完整状态快照，用于升级版本、改并行度、暂停恢复（&lt;code&gt;flink savepoint&lt;/code&gt; 命令）&lt;/li&gt;
&lt;li&gt;与 Checkpoint 独立存储，Checkpoint 默认自动过期，Savepoint 需手动删除&lt;/li&gt;
&lt;li&gt;Checkpoint 恢复后&lt;strong&gt;没有 idempotent 保证&lt;/strong&gt;，保存点做迁移是常态&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Exactly-Once：两阶段提交 Sink&lt;/h3&gt;
&lt;p&gt;Kafka Sink 的 Exactly-Once 语义通过两阶段提交实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;预提交&lt;/strong&gt;：Checkpoint Barrier 到达 Sink 时，Kafka Producer 挂起当前事务，写入预提交状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提交&lt;/strong&gt;：JobManager 确认所有算子快照成功后，通知 Sink 真正提交事务（写入不可见 → 可见）&lt;/li&gt;
&lt;li&gt;Checkpoint 失败则回滚事务，数据不落盘&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::caution
Exactly-Once 的前提是 Sink 支持事务（Kafka、MySQL 有，Redis 需要自实现）。且&lt;strong&gt;两阶段提交依赖&quot;下游不存在其他事务写入&quot;&lt;/strong&gt;，如果 Sink 端还有别的程序写同一 topic 事务，可能造成阻塞。
:::&lt;/p&gt;
&lt;h3&gt;Source 重放&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;外部数据源必须支持重放才能实现精确一次：Kafka 靠 offset 消费位点（存在 state 或 checkpoint 中，消费失败可回滚 offset）&lt;/li&gt;
&lt;li&gt;重启恢复时，Flink 从最近一次成功 Checkpoint 恢复状态 + 对应的 Source offset，数据重放一遍&lt;/li&gt;
&lt;li&gt;幂等写入（Redis set、MySQL 主键覆盖）是另一种保证思路，与两阶段提交二选一&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;重要参数介绍&lt;/h2&gt;
&lt;h3&gt;内存与资源&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;taskmanager.memory.process.size&lt;/td&gt;
&lt;td&gt;无（必配）&lt;/td&gt;
&lt;td&gt;TM 总进程内存（含堆+托管内存+网络缓冲+JVM overhead）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;taskmanager.memory.managed.size&lt;/td&gt;
&lt;td&gt;0.4 比例&lt;/td&gt;
&lt;td&gt;托管内存（RocksDB、sort buffer 用），RocksDB 场景建议加大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;taskmanager.memory.framework.heap.size&lt;/td&gt;
&lt;td&gt;128m&lt;/td&gt;
&lt;td&gt;框架堆内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;taskmanager.numberOfTaskSlots&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;每 TM 的 Slot 数，Slot 数 × TM 数 = 最大并行度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;parallelism.default&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;默认并行度，生产按资源评估&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;状态与 Checkpoint&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;state.backend&lt;/td&gt;
&lt;td&gt;RocksDB&lt;/td&gt;
&lt;td&gt;HashMap / RocksDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;state.backend.incremental&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;增量 Checkpoint（仅 RocksDB）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;execution.checkpointing.interval&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;Checkpoint 间隔，如 60s；太短压力大，太长恢复慢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;execution.checkpointing.min-pause&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;两次 Checkpoint 之间最短停顿，避免频繁触发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;execution.checkpointing.timeout&lt;/td&gt;
&lt;td&gt;10min&lt;/td&gt;
&lt;td&gt;单次 Checkpoint 超时时间，超时视为失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;execution.checkpointing.max-concurrent-checkpoints&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;并发 Checkpoint 数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;execution.checkpointing.exactly-once&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;模式开关，Kafka 场景可设 at_least_once 换吞吐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;restart-strategy&lt;/td&gt;
&lt;td&gt;视部署方式&lt;/td&gt;
&lt;td&gt;fixed-delay（默认重启 Integer.MAX_VALUE 次）/ failure-rate / none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;# 常见生产配置示例
taskmanager.memory.process.size: 8g
taskmanager.numberOfTaskSlots: 4
parallelism.default: 8
execution.checkpointing.interval: 60s
execution.checkpointing.min-pause: 30s
execution.checkpointing.timeout: 5min
state.backend: rocksdb
state.checkpoints.dir: hdfs://nameservice/flink/cp
restart-strategy.fixed-delay.attempts: 3
restart-strategy.fixed-delay.delay: 30s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;taskmanager.memory.process.size&lt;/code&gt; 是进程级总内存，堆内存会自动计算，无需再配 taskmanager.memory.heap.size。改托管内存比例时注意总内存不变。
:::&lt;/p&gt;
&lt;h3&gt;Watermark 相关&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数/API&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;forBoundedOutOfOrderness(duration)&lt;/td&gt;
&lt;td&gt;固定乱序容忍度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;withIdleness(timeout)&lt;/td&gt;
&lt;td&gt;分区长时间无数据时推进 Watermark，解决单分区拖拽（关键）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;allowedLateness&lt;/td&gt;
&lt;td&gt;窗口触发后再等的宽容时间，迟到数据再进窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sideOutputLateData&lt;/td&gt;
&lt;td&gt;超宽容期的数据输出到侧流，便于对账&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;常见问题排查&lt;/h2&gt;
&lt;h3&gt;背压（Backpressure）定位&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：Web UI 上游算子显示背压高（High），下游处理不过来&lt;/li&gt;
&lt;li&gt;原理：数据从 Source 一直流到 Sink，中间某个算子处理慢，消息在 TaskManager 缓冲队列（默认 100）堆积，逐级向上传递为背压&lt;/li&gt;
&lt;li&gt;定位：Flink Web UI → 作业 → 算子节点颜色/背压数值，红色高背压的算子就是瓶颈&lt;/li&gt;
&lt;li&gt;常见原因与解法：
&lt;ul&gt;
&lt;li&gt;单算子计算过重：优化算子逻辑、拆并行度&lt;/li&gt;
&lt;li&gt;下游为外部存储（DB/ES）写入慢：削峰（Kafka 天然削峰）、批量写入、限流&lt;/li&gt;
&lt;li&gt;反序列化/状态访问慢：状态后端换 RocksDB 或调优序列化&lt;/li&gt;
&lt;li&gt;窗口聚合算子热点：见下方窗口倾斜&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;反压导致延迟升高&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：端到端延迟飙升，Kafka lag（消费者 lag）持续增长&lt;/li&gt;
&lt;li&gt;排查顺序：看哪个算子背压最重 → 该算子 CPU/GC 是否打满 → 检查外部系统吞吐 → 检查是否有未关闭的日志/序列化开销&lt;/li&gt;
&lt;li&gt;手段：&lt;code&gt;execution.checkpointing.min-pause&lt;/code&gt; 过短会加剧（checkpoint 期间暂停消费），适当调大；并行度不够就扩容 Slot 与并行度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
背压不是 bug，是系统在说&quot;下游消费不过来了&quot;。盲目加并行度若瓶颈在外部存储 IO，只会让外部系统更慢。先定位瓶颈算子再动手。
:::&lt;/p&gt;
&lt;h3&gt;Checkpoint 失败/超时（Barrier 未对齐）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：UI 上 Checkpoint 一直失败，&lt;code&gt;Checkpoint expired before completing&lt;/code&gt;、&lt;code&gt;Barrier 未对齐&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;原因：
&lt;ul&gt;
&lt;li&gt;单分区长时间无数据导致 Watermark 不推进（配 withIdleness）&lt;/li&gt;
&lt;li&gt;算子处理过慢，Barrier 排队，超过 checkpoint.timeout&lt;/li&gt;
&lt;li&gt;状态后端写入慢（HDFS 抖动）&lt;/li&gt;
&lt;li&gt;背压导致 Barrier 无法按时到达&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;解法：调大 timeout / min-pause；排查背压；RocksDB 增量 Checkpoint；把 Checkpoint 间隔与数据吞吐匹配&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;状态过大&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：内存占用高、Checkpoint 越来越大、恢复越来越慢&lt;/li&gt;
&lt;li&gt;解法：
&lt;ul&gt;
&lt;li&gt;用 TTL（StateTtlConfig）清理过期 key（如 7 天未更新的 key 自动过期）&lt;/li&gt;
&lt;li&gt;RocksDB 天然适合大状态，别用堆内存硬扛&lt;/li&gt;
&lt;li&gt;检查 key 是否无限增长（如按用户存明细而不是聚合值）&lt;/li&gt;
&lt;li&gt;增量 Checkpoint + 定期 Savepoint 清理旧 Checkpoint&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;TaskManager 内存溢出&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：TM 进程被杀（OOM）、&lt;code&gt;Container killed by YARN&lt;/code&gt;、Metaspace/堆溢出&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;堆 OOM：并行度 × 状态量超限，调大 process.size 或减小 slots&lt;/li&gt;
&lt;li&gt;RocksDB 撑爆托管内存：&lt;code&gt;taskmanager.memory.managed.size&lt;/code&gt; 调大&lt;/li&gt;
&lt;li&gt;网络缓冲溢出：&lt;code&gt;taskmanager.memory.network.size&lt;/code&gt;（默认 64m × slots）不足会报 &lt;code&gt;NotEnoughAvailableSlotsException&lt;/code&gt; 或反序列化错误&lt;/li&gt;
&lt;li&gt;直接内存泄漏：检查是否用了非 Flink 的第三方 IO 库（JDBC 连接池、Netty 自定义 handler）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Exactly-Once 失效&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：重启后数据重复或丢失&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;Sink 是否实现两阶段提交（Kafka sink 需 1.4+ 且 &lt;code&gt;enable.2pc&lt;/code&gt;，参数 &lt;code&gt;sink.semantic=exactly_once&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;是否混用了外部写（外部还往同一 Kafka topic 写数据，事务互相干扰）&lt;/li&gt;
&lt;li&gt;幂等依赖的存储（Redis/MySQL）是否真有幂等语义（主键/唯一索引）&lt;/li&gt;
&lt;li&gt;Source offset 是否在 state 里（否则重启从头读）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;窗口数据倾斜&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：窗口聚合单个 key 数据量极大，对应 SubTask 长时间卡住&lt;/li&gt;
&lt;li&gt;解法：
&lt;ul&gt;
&lt;li&gt;预聚合后按子 key 二次聚合（两阶段聚合，key 加随机后缀）&lt;/li&gt;
&lt;li&gt;热点 key 单独拆出来做局部聚合（key 数据量已知可控时）&lt;/li&gt;
&lt;li&gt;调整并行度，配合 keyBy 的哈希分布观察各 Slot 数据均衡性&lt;/li&gt;
&lt;li&gt;数据模型层解决：先按维度拆分，避免单 key 无限累积&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;与 Kafka 集成 offset 问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：重启后消费重复/丢数据；&lt;code&gt;offset out of range&lt;/code&gt; 报错&lt;/li&gt;
&lt;li&gt;要点：
&lt;ul&gt;
&lt;li&gt;默认从 Checkpoint 恢复 offset，不配 &lt;code&gt;setStartFromEarliest/Latest&lt;/code&gt;（这些只在无 checkpoint 时生效）&lt;/li&gt;
&lt;li&gt;重复消费：Exactly-Once 开启后是&lt;strong&gt;端到端&lt;/strong&gt;保证，必须 Sink 两阶段提交配合；否则只能 At-Least-Once + 幂等&lt;/li&gt;
&lt;li&gt;&lt;code&gt;offset out of range&lt;/code&gt;：offset 过期被 Kafka 清理，检查 topic 的 retention 是否小于 Checkpoint 恢复间隔&lt;/li&gt;
&lt;li&gt;消费者组在 Flink 中只是&quot;占位&quot;，group.id 不用于 offset 管理，别指望 Flink 之外的程序共用 group 消费&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
排查通用心法：&lt;strong&gt;先看 Web UI 的 Backpressure 与 Checkpoint 状态 → 再看 TaskManager 日志（OOM/序列化）→ 最后结合数据分布（倾斜）与外部系统（Kafka/DB）判断&lt;/strong&gt;，与 Spark 排查思路一脉相承。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Flink 的核心在于三件事：&lt;strong&gt;以事件时间为基准处理乱序流（Window + Watermark）、以状态承载所有中间结果、以 Barrier 快照实现 Exactly-Once&lt;/strong&gt;。生产环境里大部分问题的根因最终都落在背压、状态、Checkpoint 三角上，把这三个维度吃透，Flink 调优就成功了大半。&lt;/p&gt;
</content:encoded></item><item><title>HDFS 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/hdfs-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/hdfs-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;大数据入门的第一块基石就是 HDFS。作为 Hadoop 的分布式文件系统，它藏了很多&quot;为什么&quot;：为什么块是 128M？为什么写数据要走 pipeline？NameNode 挂了数据会不会丢？这篇文章把 HDFS 的架构、原理、参数和排障经验梳理一遍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心架构&lt;/h2&gt;
&lt;p&gt;HDFS 是主从（Master/Slave）架构，核心角色有三个：NameNode、DataNode，以及辅助的 Secondary NameNode / Standby NameNode。&lt;/p&gt;
&lt;h3&gt;NameNode（主节点）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;管理文件系统的命名空间：目录树、文件名、文件与块的映射关系、块与 DataNode 的对应关系&lt;/li&gt;
&lt;li&gt;不存数据本身，只存元数据（Metadata），保存在内存中，由磁盘上的 FsImage + EditsLog 持久化&lt;/li&gt;
&lt;li&gt;响应客户端的读写请求：告诉客户端数据块在哪个 DataNode 上&lt;/li&gt;
&lt;li&gt;是单点（SPOF），依赖 HA 机制或 Secondary NameNode 缓解风险&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
NameNode 一旦丢失元数据（FsImage + EditsLog 全部损坏），整个集群的数据就&quot;找不回来&quot;了，因为块和文件的映射关系没了。所以 name.dir 一定要配置多个目录，最好在不同磁盘甚至不同机器上。
:::&lt;/p&gt;
&lt;h3&gt;DataNode（从节点）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;真正存储数据块的地方，默认块大小 128M，冗余副本默认 3 份&lt;/li&gt;
&lt;li&gt;周期性向 NameNode 发送心跳（默认 3 秒一次）和块报告（BlockReport）&lt;/li&gt;
&lt;li&gt;响应客户端的读写请求，负责数据块的复制、删除、校验&lt;/li&gt;
&lt;li&gt;每个块在磁盘上有两个文件：数据文件（blk_xxx）和校验文件（blk_xxx.meta）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Secondary NameNode（辅助节点）&lt;/h3&gt;
&lt;p&gt;注意它&lt;strong&gt;不是&lt;/strong&gt; NameNode 的热备，不能接管故障：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定期（默认 1 小时）从 NameNode 拉取 EditsLog，与 FsImage 合并（checkpoint），生成新的 FsImage 回传给 NameNode&lt;/li&gt;
&lt;li&gt;作用是把 NameNode 的 EditLog 控制在一个较小范围，加速 NameNode 重启恢复&lt;/li&gt;
&lt;li&gt;在 HA 架构中，它被 Standby NameNode 取代（Standby 会持续同步元数据，随时可以切换接管）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;机架感知（Rack Awareness）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hadoop 默认认为所有节点在同一个机架，通过配置 &lt;code&gt;topology.script.file.name&lt;/code&gt; 或 &lt;code&gt;net.topology.node.switch.mapping.impl&lt;/code&gt; 启用&lt;/li&gt;
&lt;li&gt;副本放置策略依赖机架信息：第一个副本放客户端所在节点，第二个副本放同机架不同节点，第三个副本放不同机架节点&lt;/li&gt;
&lt;li&gt;好处：既满足容灾（跨机架），又减少跨机架带宽消耗（读写大多数副本在本机架内）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;读写流程&lt;/h2&gt;
&lt;h3&gt;写数据流程（Pipeline 流水线复制）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;客户端调用 &lt;code&gt;FileSystem.create()&lt;/code&gt;，向 NameNode 发起创建请求&lt;/li&gt;
&lt;li&gt;NameNode 检查权限、路径合法性，返回可写的 DataNode 列表（按机架感知选 3 个）&lt;/li&gt;
&lt;li&gt;客户端按块（128M）写入：把数据切包（packet，默认 64KB），流式推给第一个 DataNode&lt;/li&gt;
&lt;li&gt;第一个 DataNode 边接收边存盘，同时把 packet 转发给第二个，第二个再转发给第三个，形成 pipeline&lt;/li&gt;
&lt;li&gt;每个 DataNode 写完一个 packet 后逐级 ack 回传，客户端收到确认后才发送下一个 packet&lt;/li&gt;
&lt;li&gt;一个块写完，向 NameNode 汇报块完成信息；全部块写完，关闭文件流&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::note
pipeline 写入的妙处：3 份副本只需要一份网络流量即可完成（顺序转发），相比&quot;并发写三份&quot;带宽节省显著，代价是写入延迟随副本数增加而略微上升。
:::&lt;/p&gt;
&lt;h3&gt;读数据流程（就近读取）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;客户端调用 &lt;code&gt;FileSystem.open()&lt;/code&gt;，拿到文件的块列表（每个块包含多个副本位置）&lt;/li&gt;
&lt;li&gt;客户端对每个块按&quot;网络距离&quot;排序副本节点：本机 &amp;gt; 同机架 &amp;gt; 同数据中心 &amp;gt; 跨数据中心&lt;/li&gt;
&lt;li&gt;从最近的 DataNode 读取，优先读第一个副本，失败自动切换下一个副本&lt;/li&gt;
&lt;li&gt;读完自动校验 CRC 校验码，不一致会重新选取副本读取&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;重要原理机制&lt;/h2&gt;
&lt;h3&gt;块 Block（128M）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;默认 128M（旧版本 64M），通过 &lt;code&gt;dfs.blocksize&lt;/code&gt; 配置&lt;/li&gt;
&lt;li&gt;大块设计的原因：
&lt;ul&gt;
&lt;li&gt;减少元数据量：一个文件块数少，NameNode 内存占用小&lt;/li&gt;
&lt;li&gt;减少寻道次数：块大，一次寻道能读更多连续数据，适合&quot;一次写入、多次读取、大文件流式读&quot;的场景&lt;/li&gt;
&lt;li&gt;便于并行处理：文件被切块后，MapReduce/Spark 可以按块并行处理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
块设置不是越大越好：块太大，MapReduce 任务分片粒度变粗，并行度下降；块太小，元数据膨胀。实际业务中小文件多才是更大的问题。
:::&lt;/p&gt;
&lt;h3&gt;副本策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;默认副本数 3（&lt;code&gt;dfs.replication&lt;/code&gt;），可对单个文件/目录单独设置（&lt;code&gt;hdfs dfs -setrep&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;放置策略：机架感知（见上文）&lt;/li&gt;
&lt;li&gt;副本冗余的目的是容错 + 就近读取加速&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;心跳与块报告&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;DataNode 每 3 秒向 NameNode 发送心跳（&lt;code&gt;dfs.heartbeat.interval&lt;/code&gt;），表示&quot;我还活着&quot;&lt;/li&gt;
&lt;li&gt;心跳还携带容量、剩余空间、正在复制的块数等信息，NameNode 据此做负载均衡决策&lt;/li&gt;
&lt;li&gt;DataNode 启动时会向 NameNode 上报全部块列表（块报告），之后周期性增量上报&lt;/li&gt;
&lt;li&gt;超过 &lt;code&gt;dfs.namenode.heartbeat.recheck-interval&lt;/code&gt; 判定 DataNode 死亡，将其上的块标记为需要复制&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;安全模式（SafeMode）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;NameNode 启动后先进入安全模式，此时只读，不能写&lt;/li&gt;
&lt;li&gt;等待各 DataNode 上报块报告，达到阈值（&lt;code&gt;dfs.namenode.safemode.threshold-pct&lt;/code&gt;，默认 0.999）且副本满足率达标后自动退出&lt;/li&gt;
&lt;li&gt;手动操作：&lt;code&gt;hdfs dfsadmin -safemode enter/leave/get&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;元数据管理：FsImage + EditsLog&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;内存中的元数据定期落盘为 FsImage（文件系统镜像）&lt;/li&gt;
&lt;li&gt;所有增删改操作先追加写 EditsLog（日志），定期 checkpoint 合并&lt;/li&gt;
&lt;li&gt;设计思路借鉴了传统数据库的 WAL：重启时加载 FsImage + 重放 EditsLog 恢复内存状态&lt;/li&gt;
&lt;li&gt;EditsLog 多目录冗余（&lt;code&gt;dfs.namenode.edits.dir&lt;/code&gt;），防止单盘损坏&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;HA 高可用（QJM）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;两个 NameNode（Active/Standby），Active 对外服务，Standby 热备&lt;/li&gt;
&lt;li&gt;共享存储用 QJM（Quorum Journal Manager）：由 3 个 JournalNode 组成，Active 写 EditsLog 到多数派（2/3），Standby 实时读取并在内存中重放&lt;/li&gt;
&lt;li&gt;ZKFC（ZKFailoverController）借助 ZooKeeper 监控 Active 状态，故障时自动切换&lt;/li&gt;
&lt;li&gt;切换时靠 &lt;strong&gt;fencing（隔离）&lt;/strong&gt; 防止脑裂：比如 kill 掉旧 Active 的进程、踢出共享存储，确保同时只有一个 Active&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
脑裂（Split Brain）是最危险的事故：两个 NameNode 同时认为自己是 Active，同时写 EditsLog，元数据会错乱。HA 场景下 fencing 机制必须配置完整，比如 &lt;code&gt;dfs.ha.fencing.methods&lt;/code&gt; 要配置 sshfence 或 shell 脚本。
:::&lt;/p&gt;
&lt;h3&gt;负载均衡&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hdfs balancer&lt;/code&gt; 命令触发，按 &lt;code&gt;dfs.datanode.balance.bandwidthPerSec&lt;/code&gt; 限速&lt;/li&gt;
&lt;li&gt;均衡目标是让各 DataNode 磁盘使用率接近集群平均值（±5%）&lt;/li&gt;
&lt;li&gt;新节点扩容后通常会执行一次 balancer&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;重要参数介绍&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.replication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;默认副本数；带宽有限或测试环境可调低，重要数据可调高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.blocksize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;134217728 (128M)&lt;/td&gt;
&lt;td&gt;块大小，新写入生效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.handler.count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;NameNode 处理 RPC 的线程数；机器核多、集群大时适当调大（如 100~200）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.name.dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;file:///...&lt;/td&gt;
&lt;td&gt;元数据（FsImage+EditsLog）存储目录，配置多个以容灾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.datanode.data.dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;file:///...&lt;/td&gt;
&lt;td&gt;DataNode 数据存储目录，多盘可配置逗号分隔多个目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.replication.max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;单个块最大副本数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.heartbeat.interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;DataNode 心跳间隔（秒）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.namenode.safemode.threshold-pct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.999&lt;/td&gt;
&lt;td&gt;安全模式退出的块报告比例阈值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dfs.trash.interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;垃圾回收站保留时间（分钟），0 表示关闭&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;与写入带宽相关的参数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dfs.client.block.write.replace-datanode-on-failure&lt;/code&gt;：pipeline 中某个 DataNode 故障时是否更换节点重写&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dfs.namenode.replication.work.multiplier.per.iteration&lt;/code&gt;：NameNode 每轮心跳下发的复制任务数，影响副本恢复速度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dfs.datanode.handler.count&lt;/code&gt;：DataNode 处理读写请求的线程数，调大能提升并发吞吐&lt;/li&gt;
&lt;li&gt;客户端侧：&lt;code&gt;io.file.buffer.size&lt;/code&gt;（默认 4KB~128KB）影响读写缓冲区，调大可提升单线程吞吐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
垃圾回收站是保命神器：&lt;code&gt;dfs.trash.interval&lt;/code&gt; 设为 1440（1 天），用户误删文件后可以在 &lt;code&gt;.Trash&lt;/code&gt; 目录里捞回来，HDFS Shell 删文件默认只进回收站，不直接物理删除。
:::&lt;/p&gt;
&lt;h2&gt;常见问题排查&lt;/h2&gt;
&lt;h3&gt;NameNode 启动失败 / 元数据损坏&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：日志报 &lt;code&gt;No such file or directory&lt;/code&gt;、&lt;code&gt;Corrupt image&lt;/code&gt;、&lt;code&gt;NameNode format without confirming&lt;/code&gt; 等&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;先看日志 &lt;code&gt;logs/hadoop-hdfs-namenode-xxx.log&lt;/code&gt;，确认是镜像文件损坏还是磁盘故障&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;hdfs fsck / -files -blocks&lt;/code&gt; 检查文件系统完整性&lt;/li&gt;
&lt;li&gt;单点环境可以用 Secondary NameNode 的最新 FsImage 恢复；HA 环境直接用 Standby 的元数据&lt;/li&gt;
&lt;li&gt;严重时考虑 &lt;code&gt;hdfs namenode -recover&lt;/code&gt;（offline image viewer / edit log viewer 辅助判断）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;预防：多目录冗余 + 定期备份 FsImage + 配置 HA&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;DataNode 下线（Decommission）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;下线流程：&lt;code&gt;hdfs dfsadmin -decommission datanode:50020&lt;/code&gt;，NameNode 会把该节点上的块复制到其他节点，完成后状态变为 Decommissioned&lt;/li&gt;
&lt;li&gt;注意事项：
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;-refresh&lt;/code&gt; 刷新超时时间，防止复制未完成被误判&lt;/li&gt;
&lt;li&gt;下线期间观察 &lt;code&gt;hdfs dfsadmin -report&lt;/code&gt;，确认副本数恢复后再真正关机&lt;/li&gt;
&lt;li&gt;千万别直接 kill DataNode 进程（块会变&quot;复制中&quot;，触发大量网络复制）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;块丢失（Missing Blocks）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;表现：&lt;code&gt;fsck&lt;/code&gt; 报 &lt;code&gt;MISSING&lt;/code&gt;，Spark 读数据报 &lt;code&gt;FileNotFoundException&lt;/code&gt; 或 &lt;code&gt;ChecksumException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hdfs fsck / -files -blocks&lt;/code&gt; 定位丢失块的文件&lt;/li&gt;
&lt;li&gt;查 &lt;code&gt;hdfs dfsadmin -report&lt;/code&gt; 看存活 DataNode 和副本状态&lt;/li&gt;
&lt;li&gt;磁盘坏、节点被误下线、硬件故障是常见原因；副本数 = 1 的文件一旦节点坏必丢&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;处理：无法恢复的块只能接受文件损坏；关键数据务必副本数 ≥ 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;磁盘满（Disk Full）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;表现：写入报 &lt;code&gt;Disk out of space&lt;/code&gt;、&lt;code&gt;No space left on device&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;df -h&lt;/code&gt; 看 DataNode 数据盘使用率，&lt;code&gt;hdfs dfsadmin -report&lt;/code&gt; 看各节点容量&lt;/li&gt;
&lt;li&gt;清理回收站、过期日志、临时文件&lt;/li&gt;
&lt;li&gt;扩容节点后跑 &lt;code&gt;hdfs balancer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注意 NameNode 所在盘满会导致元数据写不进去，比数据盘满更危险&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;小文件过多&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;危害：每个文件、每个块都要占 NameNode 内存（约 150 字节/条目），百万小文件直接打爆 NameNode；MapReduce 每个小文件一个 map，效率极低&lt;/li&gt;
&lt;li&gt;治理：
&lt;ul&gt;
&lt;li&gt;写入侧：合并写入（SequenceFile / ORC / Parquet 大文件）&lt;/li&gt;
&lt;li&gt;存量侧：用 Spark 或 &lt;code&gt;hadoop archive -archiveName&lt;/code&gt;（HAR）合并归档&lt;/li&gt;
&lt;li&gt;配合 &lt;code&gt;dfs.namenode.handler.count&lt;/code&gt; 调大缓解压力&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;脑裂（Split Brain）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;表现：两个 Active 同时出现，元数据双写错乱&lt;/li&gt;
&lt;li&gt;排查：检查 ZKFC 日志、fencing 脚本执行情况、网络分区是否恢复&lt;/li&gt;
&lt;li&gt;处理：第一时间停掉一个 NameNode，用 &lt;code&gt;hdfs haadmin -failover&lt;/code&gt; 强制切换，之后从完好的元数据恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;安全模式卡住&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;SafeMode is ON&lt;/code&gt; 一直不退出，无法写数据&lt;/li&gt;
&lt;li&gt;原因：DataNode 还没全部汇报完、块副本不足、或阈值配置不对&lt;/li&gt;
&lt;li&gt;处理：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hdfs dfsadmin -report&lt;/code&gt; 看副本满足率&lt;/li&gt;
&lt;li&gt;等 DataNode 上线后自动退出；确认无丢失后可 &lt;code&gt;hdfs dfsadmin -safemode leave&lt;/code&gt;（慎用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
排障通用口诀：先看 NameNode/DataNode 日志，再 &lt;code&gt;hdfs dfsadmin -report&lt;/code&gt; 看集群健康，最后 &lt;code&gt;hdfs fsck /&lt;/code&gt; 定位具体问题路径。日志永远是最先要看的。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;HDFS 的核心心智模型就三句话：&lt;strong&gt;元数据与数据分离&lt;/strong&gt;（NameNode 管元数据、DataNode 管数据）、&lt;strong&gt;块 + 副本&lt;/strong&gt;（128M 大块、3 副本、机架感知）、&lt;strong&gt;WAL 思路&lt;/strong&gt;（EditsLog + FsImage 持久化元数据）。把这三点吃透，再看读写流程、HA、参数和排障，都会顺理成章。&lt;/p&gt;
</content:encoded></item><item><title>Hive 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/hive-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/hive-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Hive 是大数据圈&quot;SQL 化&quot;的起点：把 HDFS 上的数据抽象成表，把 SQL 翻译成 MapReduce/Tez/Spark 作业。它不存数据、不跑计算，只做翻译和元数据管理。搞懂 Hive 的架构和优化手段，是数据开发的基本功。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心架构&lt;/h2&gt;
&lt;p&gt;Hive 由三个关键部分组成：MetaStore（元数据）、HiveServer2（服务入口）、Driver（SQL 解析与执行）。&lt;/p&gt;
&lt;h3&gt;MetaStore（元数据库）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;存储表结构、分区信息、字段类型、存储格式、表与数据文件的对应关系（元数据）&lt;/li&gt;
&lt;li&gt;默认用内嵌 Derby 数据库（只支持单连接，仅测试用），生产用 MySQL/PostgreSQL&lt;/li&gt;
&lt;li&gt;是 Hive 的&quot;目录册&quot;：Spark SQL、Impala、Presto 等都可以共用同一个 MetaStore&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
MetaStore 是中心依赖：它挂了所有 SQL 查询都进不来。生产环境要么用独立部署的 metastore 服务（&lt;code&gt;hive --service metastore&lt;/code&gt;），要么直接用 HiveServer2 内置的 metastore，并务必把元数据存到 MySQL 而不是 Derby。
:::&lt;/p&gt;
&lt;h3&gt;HiveServer2（HS2）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;对外提供 JDBC/ODBC 接口的服务，客户端（beeline、DBeaver、代码）都连它&lt;/li&gt;
&lt;li&gt;多用户、多会话支持，带身份认证与授权&lt;/li&gt;
&lt;li&gt;每个会话在 HS2 里完成 SQL 的解析、编译、提交&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Driver（SQL 处理引擎）&lt;/h3&gt;
&lt;p&gt;一个 SQL 的生命周期在 Driver 中完成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;解析（Parse）&lt;/strong&gt;：SQL 文本 → 抽象语法树（AST），做语法检查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义分析（Semantic Analyze）&lt;/strong&gt;：AST → 校验表、字段是否存在（查 MetaStore），类型检查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑计划（Logical Plan）&lt;/strong&gt;：生成逻辑执行计划（操作符树，如 TS、FIL、JOIN、AGG）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理计划（Physical Plan）&lt;/strong&gt;：逻辑计划 → MapReduce/Tez/Spark 物理执行计划，切分阶段（Stage）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行（Execute）&lt;/strong&gt;：提交给 Yarn 执行，监控进度，返回结果&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;与 Hadoop 组件的关系&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;数据存在 &lt;strong&gt;HDFS&lt;/strong&gt; 上，元数据在 &lt;strong&gt;MetaStore&lt;/strong&gt;（MySQL），计算跑在 &lt;strong&gt;Yarn&lt;/strong&gt; 上（MapReduce/Tez/Spark on Yarn）&lt;/li&gt;
&lt;li&gt;Hive 本身不存数据：删表 ≠ 删文件，取决于是不是外部表&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;核心原理&lt;/h2&gt;
&lt;h3&gt;SQL 到执行的完整链路&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;SQL → AST（解析）→ 逻辑计划（语义分析）→ 物理计划（阶段划分）→ MapReduce/Tez/Spark 作业 → Yarn 执行
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;一个 SQL 可能拆成多个 Stage（比如先 join 再 group by 就是两个 stage），stage 之间串行或并行&lt;/li&gt;
&lt;li&gt;Tez 相比 MapReduce 的优势：DAG 化，阶段间不走落盘（shuffle 优化、顶点复用），小作业明显更快&lt;/li&gt;
&lt;li&gt;执行引擎通过 &lt;code&gt;hive.execution.engine&lt;/code&gt; 配置（mr/tez/spark）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;分区（Partition）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;按分区列在 HDFS 上建目录：&lt;code&gt;/warehouse/t_orders/dt=2026-08-09/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;好处：&lt;strong&gt;分区裁剪&lt;/strong&gt;——查询只扫命中的分区目录，数据量小几个数量级&lt;/li&gt;
&lt;li&gt;分区不是越多越好：分区数过多会导致 MetaStore 元数据膨胀、文件碎片化&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;分桶（Bucket）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;按某列哈希（&lt;code&gt;clustered by (id) into 8 buckets&lt;/code&gt;），数据均匀分布到 8 个文件&lt;/li&gt;
&lt;li&gt;好处：采样（tablesample）、map 侧 join 优化（SMB Join 前身）、桶内数据分布均匀&lt;/li&gt;
&lt;li&gt;分桶表桶数基本固定，后续变更麻烦，设计时要规划好&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;内部表与外部表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内部表（Managed Table）&lt;/strong&gt;：&lt;code&gt;drop table&lt;/code&gt; 会同时删 HDFS 数据；建表时数据被移动到 Hive 管理的目录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外部表（External Table）&lt;/strong&gt;：&lt;code&gt;drop table&lt;/code&gt; 只删元数据，HDFS 数据保留；适合指向已有数据（日志、其他系统写入的数据）&lt;/li&gt;
&lt;li&gt;判断标准：数据是谁的。生产上日志/ods 层数据强烈建议外部表，防止误删&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
内部表 &lt;code&gt;truncate&lt;/code&gt; 直接清数据、&lt;code&gt;drop&lt;/code&gt; 直接删数据，且不可恢复（回收站对内部表 drop 不生效是历史行为）。误删事故里内部表占了大头，重要数据一律外部表。
:::&lt;/p&gt;
&lt;h3&gt;存储格式&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;格式&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TextFile&lt;/td&gt;
&lt;td&gt;纯文本，可读，无压缩无列式，最费 IO&lt;/td&gt;
&lt;td&gt;临时表、导入导出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SequenceFile&lt;/td&gt;
&lt;td&gt;二进制、行式、支持块压缩&lt;/td&gt;
&lt;td&gt;老的中间数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ORC&lt;/td&gt;
&lt;td&gt;列式、内置索引、压缩率高（Snappy/Zlib）、支持复杂类型&lt;/td&gt;
&lt;td&gt;大表、分析查询（Hive 亲儿子）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parquet&lt;/td&gt;
&lt;td&gt;列式、跨生态（Spark/Impala/Presto 通用）&lt;/td&gt;
&lt;td&gt;多引擎共用的大表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;列式存储（ORC/Parquet）的核心优势：只读需要的列 + 高压缩比 + 列内编码（字典、游程），全列扫描大表时性能差距是数量级的。&lt;/p&gt;
&lt;h2&gt;优化要点&lt;/h2&gt;
&lt;h3&gt;分区裁剪与谓词下推&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;分区裁剪：&lt;code&gt;where dt=&apos;2026-08-09&apos;&lt;/code&gt; 只扫该分区（SQL 层面自动做，但要写对分区字段过滤）&lt;/li&gt;
&lt;li&gt;谓词下推：&lt;code&gt;where&lt;/code&gt; 条件尽量下推到数据读取阶段（ORC 能跳过不符合的行组），join 前先过滤小数据量&lt;/li&gt;
&lt;li&gt;经验：过滤条件能写在子查询里就在子查询里写，让大数据量先瘦身&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MapJoin 与 SMB Join&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MapJoin&lt;/strong&gt;：小表（默认 &amp;lt; 25M，&lt;code&gt;hive.mapjoin.smalltable.filesize&lt;/code&gt;）加载进内存，Map 阶段直接完成 join，不走 reduce，避免 shuffle&lt;/li&gt;
&lt;li&gt;自动开关：&lt;code&gt;hive.auto.convert.join=true&lt;/code&gt;，小表 join 大表自动转 map join&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SMB Join（Sort Merge Bucket Join）&lt;/strong&gt;：两个分桶表（同字段、同桶数），join 时只匹配对应桶，可以大表 join 大表时减少 shuffle，还能 map 侧直接做&lt;/li&gt;
&lt;li&gt;注意：MapJoin 小表要放 join 左侧（或开启自动转换后无所谓）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据倾斜处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;表现：reduce 大部分秒完，个别 reduce 卡很久（长尾）&lt;/li&gt;
&lt;li&gt;常见场景：join 的 key 集中（如 null、热点商品 id）、group by 的 key 倾斜、distinct 计数&lt;/li&gt;
&lt;li&gt;处理手段：
&lt;ul&gt;
&lt;li&gt;空值：&lt;code&gt;coalesce(uid, rand())&lt;/code&gt; 打散&lt;/li&gt;
&lt;li&gt;热点 key：拆分后 union，或加随机前缀再第二轮聚合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hive.groupby.skewindata=true&lt;/code&gt;：group by 自动两阶段聚合（先随机分桶预聚合）&lt;/li&gt;
&lt;li&gt;倾斜 join：&lt;code&gt;hive.optimize.skewjoin=true&lt;/code&gt;（运行时检测）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;根治思路：先定位倾斜 key（&lt;code&gt;group by key having count(*) &amp;gt; N&lt;/code&gt; 查出来），再对症下药&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;小文件合并&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;危害：文件多 → 元数据多、Map 启动开销大、NameNode 压力大、查询慢&lt;/li&gt;
&lt;li&gt;手段：
&lt;ul&gt;
&lt;li&gt;写入侧：&lt;code&gt;hive.merge.mapfiles=true&lt;/code&gt; / &lt;code&gt;hive.merge.mapredfiles=true&lt;/code&gt; / &lt;code&gt;hive.merge.size.per.task&lt;/code&gt;（合并目标文件大小，默认 256M）&lt;/li&gt;
&lt;li&gt;动态分区小文件多时配合 &lt;code&gt;hive.optimize.sort.dynamic.partition=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;存量侧：&lt;code&gt;insert overwrite&lt;/code&gt; 重写一遍表（用 reduce 数控制输出文件数）&lt;/li&gt;
&lt;li&gt;Spark 侧：&lt;code&gt;coalesce&lt;/code&gt; / &lt;code&gt;repartition&lt;/code&gt; 控制分区数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;动态分区&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;自动按分区列值写入对应分区，&lt;code&gt;hive.exec.dynamic.partition=true&lt;/code&gt;，&lt;code&gt;hive.exec.dynamic.partition.mode=nonstrict&lt;/code&gt;（允许所有分区列都是动态的）&lt;/li&gt;
&lt;li&gt;坑：单次作业动态分区数太多（默认 1000，&lt;code&gt;hive.exec.max.dynamic.partitions&lt;/code&gt;）会报错；数据倾斜到一个分区会引发小文件&lt;/li&gt;
&lt;li&gt;建议：按天/按小时的场景用动态分区，小维度（省份等）用静态分区&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;重要参数介绍&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.execution.engine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;mr&lt;/td&gt;
&lt;td&gt;执行引擎：mr/tez/spark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.parallel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;无依赖的 stage 并行执行，多个 union/多表 join 场景提速明显&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.parallel.thread.number&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;并行 stage 上限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.auto.convert.join&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;自动把小表 join 转 MapJoin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.mapjoin.smalltable.filesize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;25000000 (25M)&lt;/td&gt;
&lt;td&gt;MapJoin 小表大小阈值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.dynamic.partition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;开启动态分区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.dynamic.partition.mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;strict&lt;/td&gt;
&lt;td&gt;strict 要求至少一个静态分区列；大数据量全动态需改 nonstrict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.exec.max.dynamic.partitions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;动态分区上限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.fetch.task.conversion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;more&lt;/td&gt;
&lt;td&gt;简单查询（无聚合/limit 的 select）不启 MapReduce，直接本地拉取，体验差距巨大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.groupby.skewindata&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;group by 两阶段聚合，缓解倾斜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hive.optimize.skewjoin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;join 倾斜检测与优化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;与 MapReduce 资源相关的参数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mapreduce.map.memory.mb&lt;/code&gt; / &lt;code&gt;mapreduce.reduce.memory.mb&lt;/code&gt;：Map/Reduce 容器内存（默认 1024M），大表聚合/排序不够会 OOM&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapreduce.map.java.opts&lt;/code&gt; / &lt;code&gt;mapreduce.reduce.java.opts&lt;/code&gt;：JVM 堆内存（一般 = 容器内存 × 0.8）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapreduce.map.cpu.vcores&lt;/code&gt; / &lt;code&gt;mapreduce.reduce.cpu.vcores&lt;/code&gt;：容器 vcore 数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapreduce.job.reduces&lt;/code&gt;：reduce 数，不设置时由 Hive 按数据量估算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapreduce.input.fileinputformat.split.maxsize&lt;/code&gt;：控制分片大小，间接控制 map 数&lt;/li&gt;
&lt;li&gt;注意：容器申请内存不能超过 &lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt;，否则直接失败&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题排查&lt;/h2&gt;
&lt;h3&gt;数据倾斜的定位&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：作业卡在最后几个 reduce，进度条 99% 不动&lt;/li&gt;
&lt;li&gt;定位：
&lt;ul&gt;
&lt;li&gt;HS2 里 &lt;code&gt;explain&lt;/code&gt; 看 reduce 逻辑；HiveServer2 UI / Yarn UI 看各 task 耗时&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;group by key having count(*) &amp;gt; 1000&lt;/code&gt; 查候选倾斜 key&lt;/li&gt;
&lt;li&gt;看任务日志中 shuffle 数据量：个别 reduce 数据量异常大&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;处理：按上文倾斜手段（打散 null、两阶段聚合、skewjoin）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;内存溢出（OOM）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;Container killed by the ApplicationMaster&lt;/code&gt;、&lt;code&gt;Java heap space&lt;/code&gt;、&lt;code&gt;GC overhead limit exceeded&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排查顺序：
&lt;ul&gt;
&lt;li&gt;是 reducer OOM：调大 &lt;code&gt;mapreduce.reduce.memory.mb&lt;/code&gt; 及对应 &lt;code&gt;java.opts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;是 MapJoin 内存不足：小表调大 &lt;code&gt;hive.mapjoin.smalltable.filesize&lt;/code&gt; 前先确认内存；或把大 key 多的 join 拆小&lt;/li&gt;
&lt;li&gt;聚合类任务：先压缩输入（列式+过滤），再考虑调资源&lt;/li&gt;
&lt;li&gt;注意 vmem 检查：容器内存调大时同步调大 &lt;code&gt;java.opts&lt;/code&gt;，否则堆占不满容器还被误杀&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;小文件过多&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;Number of files: xxx&lt;/code&gt; 巨大、map 数爆炸、NameNode 告警&lt;/li&gt;
&lt;li&gt;处理：合并参数 + &lt;code&gt;insert overwrite&lt;/code&gt; 重写；源头治理（控制动态分区数、reduce 数）&lt;/li&gt;
&lt;li&gt;检查：&lt;code&gt;dfs -count&lt;/code&gt; 看目录文件数，或用 &lt;code&gt;show tblproperties&lt;/code&gt; 看表文件信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MetaStore 连接问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;Unable to connect to metastore&lt;/code&gt;、&lt;code&gt;RetryingMetaStoreClient&lt;/code&gt;、&lt;code&gt;Access denied for user &apos;hive&apos;@&apos;host&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;MySQL 连接是否通：&lt;code&gt;mysql -h &amp;lt;metastore_host&amp;gt; -u hive -p&lt;/code&gt; 手测&lt;/li&gt;
&lt;li&gt;最大连接数是否打满（&lt;code&gt;max_connections&lt;/code&gt;），HS2 并发会话多时常见&lt;/li&gt;
&lt;li&gt;元数据表锁死：&lt;code&gt;HiveMetaStore&lt;/code&gt; 表 DDL 冲突（分区操作并发）&lt;/li&gt;
&lt;li&gt;版本匹配：客户端 hive 版本与服务端 metastore 版本差异过大&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;处理：重启 metastore 服务、清理连接、检查 MySQL 磁盘与慢查询&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;查询慢&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;排查思路（从便宜到贵）：
&lt;ol&gt;
&lt;li&gt;是否走了分区裁剪（explain 看读哪个分区）&lt;/li&gt;
&lt;li&gt;是否触发全表扫描（过滤条件没下推）&lt;/li&gt;
&lt;li&gt;存储格式是否列式（TextFile 换 ORC 收益巨大）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetch.task.conversion&lt;/code&gt; 是否被关闭（简单查询被拖进 MapReduce）&lt;/li&gt;
&lt;li&gt;数据倾斜（见上）&lt;/li&gt;
&lt;li&gt;join 顺序与小表处理是否合理&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;通用手段：&lt;code&gt;explain&lt;/code&gt; / &lt;code&gt;explain extended&lt;/code&gt; 看执行计划，先读计划再优化，别瞎调参数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;动态分区失败&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;Too many dynamic partitions&lt;/code&gt;、&lt;code&gt;Exception while processing to map&lt;/code&gt;、&lt;code&gt;Specified partition already exists&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;处理：
&lt;ul&gt;
&lt;li&gt;分区数超限：调大 &lt;code&gt;hive.exec.max.dynamic.partitions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;strict 模式要求至少一个静态分区：写 &lt;code&gt;partition(dt=&apos;2026-08-09&apos;, xxx)&lt;/code&gt; 或改 nonstrict&lt;/li&gt;
&lt;li&gt;插入数据里存在分区列乱值（null/异常）导致目标目录非法：先过滤脏数据&lt;/li&gt;
&lt;li&gt;同名分区并发写冲突：串行化该作业&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;UDF 报错&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;Class not found&lt;/code&gt;、&lt;code&gt;Method not supported&lt;/code&gt;、&lt;code&gt;Invalid return type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;jar 是否 &lt;code&gt;add jar&lt;/code&gt; 且路径在 HS2 节点可访问&lt;/li&gt;
&lt;li&gt;临时函数 vs 永久函数：&lt;code&gt;create temporary function&lt;/code&gt; 仅当前会话；永久函数要 &lt;code&gt;create function ... using jar &apos;hdfs://...&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;UDF 的类签名（&lt;code&gt;evaluate&lt;/code&gt; 方法的入参类型）与调用参数类型是否匹配，Hive 类型和 Java 类型映射要一致&lt;/li&gt;
&lt;li&gt;看 HS2 日志中的异常栈，一般一眼能看出是反射错误还是业务代码问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
Hive 优化的两条主线：&lt;strong&gt;先少读数据&lt;/strong&gt;（分区裁剪、列式存储、谓词下推、过滤前置），&lt;strong&gt;再少 shuffle&lt;/strong&gt;（MapJoin、SMB、两阶段聚合、合并小文件）。参数只是辅助，数据流设计才是根。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Hive 的心智模型：&lt;strong&gt;元数据在 MetaStore、数据在 HDFS、计算在 Yarn、Driver 负责把 SQL 翻译成执行计划&lt;/strong&gt;。优化的本质是让翻译出来的执行计划&quot;读得更少、shuffle 更少&quot;。遇到问题先 explain、再定位倾斜、最后才调参，这条路走通，Hive 基本就吃透了。&lt;/p&gt;
</content:encoded></item><item><title>Kafka 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/kafka-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/kafka-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是我学习 Kafka 时整理的笔记，涵盖核心架构、底层存储原理、副本与一致性机制、生产消费流程，以及日常排障的经验总结，适合面试复习和实际排查问题参考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心架构&lt;/h2&gt;
&lt;h3&gt;角色划分&lt;/h3&gt;
&lt;p&gt;Kafka 集群主要由以下几类角色组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Producer（生产者）&lt;/strong&gt;：向 Topic 发送消息的客户端，负责选择分区、批量发送、处理发送结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broker（服务端）&lt;/strong&gt;：Kafka 集群中的一台服务器就是一个 Broker，负责消息的存储、接收与副本管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumer（消费者）&lt;/strong&gt;：从 Topic 拉取消息的客户端，多个消费者组成消费组协同消费。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controller（控制器）&lt;/strong&gt;：集群中特殊角色，负责分区 Leader 选举、元数据管理等集群协调工作，本质是一个 Broker。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zookeeper / KRaft&lt;/strong&gt;：旧版本依赖 Zookeeper 存储元数据（Topic、分区、broker 状态等）并完成 Controller 选举；新版本引入 KRaft 模式，用内置的 Raft 协议替代 ZK，简化运维。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
2.8 版本开始引入 KRaft（Kafka Raft Metadata mode），3.3+ 起 KRaft 生产可用，4.0 已完全移除 Zookeeper。KRaft 通过 Controller 节点上内置的 Raft 日志存储元数据，Controller 与 Broker 角色可以合并部署，也可以独立部署。
:::&lt;/p&gt;
&lt;h3&gt;核心概念&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Topic（主题）&lt;/strong&gt;：消息的逻辑分类，一个 Topic 可被多个生产者写入、多个消费组读取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partition（分区）&lt;/strong&gt;：Topic 在物理上的切分单元，每个分区是一个有序的日志文件，分区内消息有序，分区之间无序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offset（偏移量）&lt;/strong&gt;：分区内消息的唯一序号，从 0 开始递增，Consumer 通过维护 offset 记录消费进度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
分区是 Kafka 并行度的来源：生产者按分区并行写、消费者按分区并行读。分区数越多，吞吐上限越高，但也带来更多文件句柄与副本开销，并非越多越好。
:::&lt;/p&gt;
&lt;h2&gt;存储原理&lt;/h2&gt;
&lt;h3&gt;分区与段（Segment）&lt;/h3&gt;
&lt;p&gt;每个 Partition 在磁盘上是一个目录（&lt;code&gt;topic-分区号&lt;/code&gt;），目录内按**段（Segment）**组织数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.log&lt;/code&gt;：消息数据文件，消息按 offset 顺序追加写入。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.index&lt;/code&gt;：稀疏索引文件，记录 offset 到物理位置的映射（消息在文件中的字节偏移）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.timeindex&lt;/code&gt;：时间索引文件，记录时间戳与 offset 的映射，用于按时间查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个 Segment 默认 1GB（&lt;code&gt;log.segment.bytes&lt;/code&gt;）达到上限后滚动创建新 Segment，活动段（active segment）之外的段可以被删除或压缩。&lt;/p&gt;
&lt;h3&gt;稀疏索引&lt;/h3&gt;
&lt;p&gt;.index 文件不是每条消息都建索引，而是&lt;strong&gt;每隔一定字节（&lt;code&gt;log.index.interval.bytes&lt;/code&gt;，默认 4096）写入一条索引项&lt;/strong&gt;，即稀疏索引。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;好处：索引文件体积小，能常驻内存，检索快。&lt;/li&gt;
&lt;li&gt;查找流程：先二分查找最近的索引项定位到起始物理位置，再顺序扫描 .log 文件定位目标消息，单次查找磁盘扫描量很小，性能可观。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;顺序写与页缓存&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顺序写&lt;/strong&gt;：消息追加到日志文件尾部，磁盘顺序写速度远高于随机写（机械盘百倍级差距），这是 Kafka 高吞吐的核心之一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页缓存（Page Cache）&lt;/strong&gt;：Kafka 写入数据只落 Page Cache，由操作系统统一管理刷盘，而非自己维护缓存；读取时优先命中 Page Cache，保证读写两端都很快，也实现了&quot;读写都用 OS 缓存、磁盘只做备份&quot;的效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
Kafka 依赖页缓存 + 顺序写获得高性能，因此建议机器内存尽量充裕，并关闭&quot;脏页回写&quot;相关不必要的限制（如 vm.dirty_ratio），不要在同一台机器上部署多个重 IO 应用抢占缓存。
:::&lt;/p&gt;
&lt;h3&gt;零拷贝（sendfile）&lt;/h3&gt;
&lt;p&gt;Consumer 拉取消息时，服务端将磁盘数据发给网卡，传统做法要经过&quot;磁盘 → 内核缓冲区 → 用户态 → 内核 socket 缓冲区 → 网卡&quot;多次拷贝；Kafka 使用 &lt;code&gt;sendfile&lt;/code&gt;（FileChannel.transferTo）让数据&lt;strong&gt;直接从内核页缓存拷贝到网卡&lt;/strong&gt;，全程不经过用户态，极大降低 CPU 与拷贝开销。&lt;/p&gt;
&lt;h3&gt;批量写与批量读&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Producer 攒批发送（batch + linger.ms），减少网络往返。&lt;/li&gt;
&lt;li&gt;Broker 接收后以批次（record batch）为单位落盘。&lt;/li&gt;
&lt;li&gt;Consumer 拉取时按 &lt;code&gt;fetch.min.bytes&lt;/code&gt; / &lt;code&gt;fetch.max.wait.ms&lt;/code&gt; 攒批返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;副本机制&lt;/h2&gt;
&lt;h3&gt;分区副本&lt;/h3&gt;
&lt;p&gt;每个分区有多个副本（&lt;code&gt;replication.factor&lt;/code&gt;），其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Leader&lt;/strong&gt;：负责所有读写请求，Producer 和 Consumer 只与 Leader 交互。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Follower&lt;/strong&gt;：从 Leader 拉取消息同步数据，Leader 故障时参与选举。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ISR、HW、LEO&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LEO（Log End Offset）&lt;/strong&gt;：副本日志下一条待写入消息的 offset，即日志末尾位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HW（High Watermark）&lt;/strong&gt;：已同步的最小 LEO，Consumer 只能消费到 HW 之前的消息，保证消费到的数据至少在多数副本上存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISR（In-Sync Replicas）&lt;/strong&gt;：与 Leader 保持同步的副本集合，默认 &lt;code&gt;replica.lag.time.max.ms&lt;/code&gt;（10 秒）内未跟上同步的副本会被踢出 ISR。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
HW 机制下消费只能看到&quot;已提交&quot;的消息，可能造成消息&quot;不可见延迟&quot;；且 HW 的推进依赖副本之间的同步与 Leader 转发，配合 fetch 协议存在数据丢失/重复的窗口期（旧版本消息发送语义的固有风险），生产环境应启用 &lt;code&gt;min.insync.replicas&lt;/code&gt; 并配合 acks=all 降低风险。
:::&lt;/p&gt;
&lt;h3&gt;Leader 选举&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;分区 Leader 宕机后由 &lt;strong&gt;Controller&lt;/strong&gt; 在 ISR 中挑选新 Leader（优先 ISR，ISR 为空时可降级选择非 ISR 副本，并可能丢数据）。&lt;/li&gt;
&lt;li&gt;选举范围优先&quot;最近同步过&quot;的副本，避免选出一个数据明显落后的副本导致大量消息&quot;回退&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;acks 三种级别&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;acks&lt;/th&gt;
&lt;th&gt;行为&lt;/th&gt;
&lt;th&gt;语义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;发完就返回，不等待任何确认&lt;/td&gt;
&lt;td&gt;最快，可能丢消息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Leader 写入本地日志后返回&lt;/td&gt;
&lt;td&gt;默认值，Leader 宕机可能丢数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;all（-1）&lt;/td&gt;
&lt;td&gt;等待 ISR 全部副本写入后返回&lt;/td&gt;
&lt;td&gt;最可靠，配合 &lt;code&gt;min.insync.replicas&lt;/code&gt; 使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip
高可靠场景建议 acks=all + min.insync.replicas=2（至少 2 个副本同步才算提交），牺牲部分延迟换取不丢消息。
:::&lt;/p&gt;
&lt;h2&gt;生产者原理&lt;/h2&gt;
&lt;h3&gt;分区策略&lt;/h3&gt;
&lt;p&gt;Producer 发送消息时确定目标分区，规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息指定了 Partition：直接使用指定分区。&lt;/li&gt;
&lt;li&gt;指定了 Key：对 Key 哈希取模（&lt;code&gt;murmur2&lt;/code&gt;）选择分区，相同 Key 的消息进入同一分区，保证同 Key 消息有序。&lt;/li&gt;
&lt;li&gt;两者都没指定：使用 &lt;strong&gt;Sticky Partitioner&lt;/strong&gt;，随机选一个分区并尽量把消息攒到一个批次里发出去，兼顾负载均衡与批处理效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;批量发送与缓冲&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;消息先进入内存缓冲（&lt;code&gt;buffer.memory&lt;/code&gt;，默认 32MB），由发送线程批量取出发送。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;batch.size&lt;/code&gt;（默认 16KB）控制单批次大小，&lt;code&gt;linger.ms&lt;/code&gt; 控制等待时间，两者共同决定&quot;攒批&quot;的程度。&lt;/li&gt;
&lt;li&gt;缓冲区写满时 &lt;code&gt;max.block.ms&lt;/code&gt; 到期后抛异常，需要监控该异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;幂等性（PID + 序列号）&lt;/h3&gt;
&lt;p&gt;开启 &lt;code&gt;enable.idempotence=true&lt;/code&gt; 后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Producer 初始化时分配 &lt;strong&gt;PID（Producer Id）&lt;/strong&gt;，每条消息携带该 PID 和&lt;strong&gt;单调递增的序列号&lt;/strong&gt;（按分区维度）。&lt;/li&gt;
&lt;li&gt;Broker 端校验序列号，重复的序列号直接丢弃，从而解决&quot;重试导致的消息重复&quot;问题。&lt;/li&gt;
&lt;li&gt;注意：PID 只在单会话有效，重启会生成新 PID；且只对单分区有序，跨分区无法保证全局不重复。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;事务&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;enable.idempotence=true&lt;/code&gt; 是事务的前提，事务通过 Transaction Coordinator 管理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持&quot;多分区原子写入&quot;和&quot;读已提交（read_committed）&quot;消费语义。&lt;/li&gt;
&lt;li&gt;通过 LSO（Last Stable Offset）控制，只有事务提交后消息才对 read_committed 消费者可见。&lt;/li&gt;
&lt;li&gt;典型应用：Kafka Streams 的 exactly-once 处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;消费者原理&lt;/h2&gt;
&lt;h3&gt;消费组与分区分配&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;同一消费组内，一个分区同一时刻只能被一个消费者实例消费（一条消息只被组内一个消费者消费）。&lt;/li&gt;
&lt;li&gt;消费者数 &amp;gt; 分区数时，多余的消费者空闲。&lt;/li&gt;
&lt;li&gt;分配策略（&lt;code&gt;partition.assignment.strategy&lt;/code&gt;）：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;策略&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Range&lt;/td&gt;
&lt;td&gt;按 Topic 内分区序号范围连续划分，易造成&quot;前段消费者分多、后端消费者分少&quot;的不均衡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RoundRobin&lt;/td&gt;
&lt;td&gt;全部分区轮询分配，相对均衡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sticky&lt;/td&gt;
&lt;td&gt;在保持上次分配尽量不变的前提下均衡分配，减少 rebalance 后的分区移动&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Rebalance（新旧协议）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;发生条件：消费者加入/退出、分区数变化、消费组订阅变化。&lt;/li&gt;
&lt;li&gt;新协议（增量式合作 rebalance，0.11+）：消费者和 Group Coordinator 之间用&quot;分阶段协商&quot;，每次只迁移需要移动的分区，Sticky/CooperativeSticky 策略下能显著降低全组停止消费的时间。&lt;/li&gt;
&lt;li&gt;旧协议：停止消费 → 全体重新分配 → 重新拉取，期间整个消费组不可用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
Rebalance 期间消费组短暂不可用，频繁 rebalance 通常意味着：消费者心跳超时、处理时间过长导致会话超时、网络抖动，或消费者实例频繁启停，需要重点排查。
:::&lt;/p&gt;
&lt;h3&gt;Offset 提交&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;自动提交（&lt;code&gt;enable.auto.commit=true&lt;/code&gt;，默认）：每 &lt;code&gt;auto.commit.interval.ms&lt;/code&gt;（默认 5 秒）提交当前拉取位置。注意它提交的是&quot;最近一次 poll 返回的 offset&quot;，处理滞后会导致重复消费，因此自动提交需配合幂等消费设计。&lt;/li&gt;
&lt;li&gt;手动提交：&lt;code&gt;commitSync&lt;/code&gt;（同步，重试保证不丢）或 &lt;code&gt;commitAsync&lt;/code&gt;（异步，不阻塞但可能失败）。&lt;/li&gt;
&lt;li&gt;Offset 存放在内部 Topic &lt;code&gt;__consumer_offsets&lt;/code&gt;（默认 50 个分区），消费进度是&quot;committed offset 与 consumed 之间&quot;的关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;高可用与故障恢复&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;副本同步&lt;/strong&gt;：Follower 以 fetch 方式向 Leader 拉取数据，Leader 在 ISR 内推进 HW，Consumer 只能读到 HW 前的数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controller&lt;/strong&gt;：负责分区 Leader 选举、Broker 上下线元数据变更通知（通过 ZK 或 KRaft）；Controller 本身也靠选举保证高可用（旧版基于 ZK，KRaft 基于 Raft）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志压缩（Log Compaction）&lt;/strong&gt;：针对&quot;保留最新 value&quot;的场景（如 Key-Value 型数据、配置同步），压缩后日志中每个 Key 只保留最新版本，可配合 &lt;code&gt;cleanup.policy=compact&lt;/code&gt; 使用，与&quot;基于时间的过期删除&quot;是两种不同的清理策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分区副本重新分配&lt;/strong&gt;：Broker 下线后，Controller 会在存活 Broker 上补足副本（auto.leader.rebalance 与副本迁移任务）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
Kafka 的可用性保障是&quot;多副本 + ISR 选主 + 分区迁移&quot;，数据安全由 acks 级别和 min.insync.replicas 决定，两者组合使用才能兼顾可用与可靠。
:::&lt;/p&gt;
&lt;h2&gt;常规问题排查&lt;/h2&gt;
&lt;h3&gt;消费堆积（Lag）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用命令行查看：&lt;code&gt;kafka-consumer-groups.sh --bootstrap-server xxx --describe --group g1&lt;/code&gt;，关注 LAG 列。&lt;/li&gt;
&lt;li&gt;判断堆积原因：
&lt;ul&gt;
&lt;li&gt;消费能力不足：单条消息处理耗时长、消费者线程数 &amp;lt; 分区数。&lt;/li&gt;
&lt;li&gt;消费卡住：消费线程异常/阻塞/频繁 rebalance 导致重复消费。&lt;/li&gt;
&lt;li&gt;生产过快：生产速率超过消费能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;处理手段：扩容消费者实例/增加分区、优化消费逻辑、必要时直接重置 offset（&lt;code&gt;--reset-offsets&lt;/code&gt;，谨慎使用）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;消息丢失与重复&lt;/h3&gt;
&lt;p&gt;丢失常见原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;acks=0/1&lt;/code&gt; 且 Broker 宕机。&lt;/li&gt;
&lt;li&gt;生产者发送失败未重试（&lt;code&gt;retries=0&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;消费者自动提交 offset 后处理失败。&lt;/li&gt;
&lt;li&gt;分区 Leader 从非 ISR 副本选出（丢未同步消息）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;重复常见原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生产者重试造成重复 → 开启幂等（PID+序列号）。&lt;/li&gt;
&lt;li&gt;消费者处理成功后未提交 offset 就宕机 → 重复消费，业务侧要做幂等。&lt;/li&gt;
&lt;li&gt;事务/跨分区场景的 exactly-once 需依赖事务与 read_committed。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;分区数调整&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;分区只能增多不能减少（&lt;code&gt;kafka-topics.sh --alter --partitions N&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;增加分区后，如果消息有 Key，相同 Key 可能落到不同分区，&lt;strong&gt;同 Key 有序性被破坏&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;分区数调整会触发消费者 rebalance。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
分区数的选择要结合吞吐目标与扩展计划一次规划好（如 3×broker 下 6/9 分区），后期&quot;只增不减&quot;，改分区数是个敏感操作。
:::&lt;/p&gt;
&lt;h3&gt;磁盘占用与清理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;默认按时间保留（&lt;code&gt;log.retention.hours=168&lt;/code&gt;）或按大小（&lt;code&gt;log.retention.bytes&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;磁盘告警排查：&lt;code&gt;du -sh&lt;/code&gt; 查看分区目录大小；确认是否有 Topic 未设置合理的 retention；检查日志压缩是否生效；清理旧 Topic（&lt;code&gt;kafka-topics.sh --delete&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;磁盘空间不足会直接导致 Broker 写失败，务必提前监控。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;吞吐瓶颈定位&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;排查链路：Producer 端（batch.size / linger.ms / compression 压缩）、网络带宽、Broker 磁盘 IO（顺序写是否被打断）、页缓存命中率、Consumer 拉取参数。&lt;/li&gt;
&lt;li&gt;常见手段：开启 &lt;code&gt;compression.type=lz4/zstd&lt;/code&gt;、调大 &lt;code&gt;num.io.threads&lt;/code&gt;、确认分区数足够并行。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;kafka-run-class.sh kafka.tools.JmxTool&lt;/code&gt; 或监控面板看 IO 与网络指标。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Rebalance 频繁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;检查 &lt;code&gt;session.timeout.ms&lt;/code&gt; 与 &lt;code&gt;max.poll.interval.ms&lt;/code&gt;：处理慢导致超时被踢出消费组。&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;heartbeat.interval.ms&lt;/code&gt; 是否过大。&lt;/li&gt;
&lt;li&gt;排查消费者实例是否频繁上下线（如容器 OOM、被调度重启）。&lt;/li&gt;
&lt;li&gt;升级到 CooperativeSticky 策略减少 rebalance 影响面。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;客户端参数调优要点&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;高吞吐&lt;/td&gt;
&lt;td&gt;调大 batch.size、linger.ms，开启压缩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;低延迟&lt;/td&gt;
&lt;td&gt;调小 linger.ms（如 5ms），适当减小 batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高可靠&lt;/td&gt;
&lt;td&gt;acks=all + enable.idempotence=true + min.insync.replicas=2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;消费能力&lt;/td&gt;
&lt;td&gt;max.poll.records 与处理耗时匹配，超时调大 max.poll.interval.ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip
排查 Kafka 问题遵循三板斧：先看 Lag（消费是否跟上）、再看 rebalance（消费组是否稳定）、最后看磁盘/页缓存/网络（Broker 是否健康），配合监控告警能快速定位大部分故障。
:::&lt;/p&gt;
</content:encoded></item><item><title>MapReduce 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/mapreduce-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/mapreduce-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;学大数据绕不开 MapReduce，它是 Hadoop 分布式计算的基石。这篇文章是我对 MapReduce 底层原理的总结，从计算模型讲到执行流程，再到参数调优和常见问题排查，希望能帮你把这块知识串起来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;计算模型：Map / Reduce 思想&lt;/h2&gt;
&lt;p&gt;MapReduce 的核心思想就四个字：&lt;strong&gt;分而治之&lt;/strong&gt;。把一个大规模计算任务拆成无数个小任务，分发给集群中的多台机器并行处理，最后再汇总结果。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Map（映射）&lt;/strong&gt;：把输入数据转换为一组 &lt;code&gt;(key, value)&lt;/code&gt; 键值对，即&quot;分&quot;的过程，天然可并行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shuffle（洗牌）&lt;/strong&gt;：框架自动完成，把 Map 输出的数据按 key 分组、排序、传输到 Reduce 端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce（归约）&lt;/strong&gt;：对相同 key 的数据做聚合运算，即&quot;合&quot;的过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
MapReduce 对外只暴露 Map 和 Reduce 两个接口，中间最复杂的 Shuffle 过程完全由框架封装，这也是它好上手的原因。
:::&lt;/p&gt;
&lt;h3&gt;WordCount 经典流程&lt;/h3&gt;
&lt;p&gt;以最经典的词频统计为例，完整流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;输入&lt;/strong&gt;：文件被切分成多个 split，每行文本作为一条记录输入 Map。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Map 阶段&lt;/strong&gt;：对每一行按空格切词，输出 &lt;code&gt;(word, 1)&lt;/code&gt;，比如 &lt;code&gt;(hello, 1)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shuffle 阶段&lt;/strong&gt;：框架把所有 &lt;code&gt;hello&lt;/code&gt; 的 key 分到同一组，排序后传给同一个 Reduce。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce 阶段&lt;/strong&gt;：对 &lt;code&gt;(hello, [1,1,1,1])&lt;/code&gt; 做求和，输出 &lt;code&gt;(hello, 4)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出&lt;/strong&gt;：写入结果文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;输入: &quot;hello world hello hadoop world&quot;
Map 输出: (hello,1) (world,1) (hello,1) (hadoop,1) (world,1)
Shuffle:  分组排序 -&amp;gt; hello:[1,1] world:[1,1] hadoop:[1]
Reduce:   (hello,2) (world,2) (hadoop,1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;执行流程详解&lt;/h2&gt;
&lt;p&gt;一个 MapReduce Job 的执行可以分成五大阶段：Input Split、Map、Shuffle、Reduce、Output。&lt;/p&gt;
&lt;h3&gt;1. Input Split（输入分片）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;输入文件会被逻辑切分成若干个 &lt;strong&gt;split（分片）&lt;/strong&gt;，每个 split 对应一个 Map Task。&lt;/li&gt;
&lt;li&gt;split 与 HDFS 的 &lt;strong&gt;block（块）&lt;/strong&gt; 有关系但不等同：block 是 HDFS 存储层的物理概念（默认 128MB），split 是计算层的逻辑概念。&lt;/li&gt;
&lt;li&gt;默认情况下 split 大小与 block 对齐（128MB），目的是保证 &lt;strong&gt;数据本地性&lt;/strong&gt;——Map 任务可以在存储该数据的节点上运行，避免网络传输。&lt;/li&gt;
&lt;li&gt;每个 split 会切成多条记录（record），&lt;code&gt;(key, value)&lt;/code&gt; 形式喂给 Map，比如 TextInputFormat 中 key 是行偏移量、value 是一行文本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
split 切分时如果一条记录横跨两个 split，会有一个小的&quot;边界重叠&quot;（默认 split 可以跨 block 边界读取），这导致一个文件最多会多出一个小分片，小文件过多时特别明显。
:::&lt;/p&gt;
&lt;h3&gt;2. Map 阶段&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Map 读入 &lt;code&gt;(key, value)&lt;/code&gt; 调用用户自定义的 &lt;code&gt;map()&lt;/code&gt; 方法，输出新的 &lt;code&gt;(key, value)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Map 的输出&lt;strong&gt;先写入环形缓冲区（内存）&lt;/strong&gt;，不会直接落盘，只有溢写（spill）时才写本地磁盘。&lt;/li&gt;
&lt;li&gt;环形缓冲区默认 100MB（由 &lt;code&gt;mapreduce.task.io.sort.mb&lt;/code&gt; 控制），当使用量达到阈值（默认 80%，即 &lt;code&gt;mapreduce.map.sort.spill.percent&lt;/code&gt;）时，后台线程开始溢写。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Shuffle 阶段（重头戏）&lt;/h3&gt;
&lt;p&gt;Shuffle 是 MapReduce 的精华，也是性能瓶颈最常出现的地方，包括：分区、排序、合并、压缩、溢写、归并六个动作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Map 端 Shuffle：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分区 Partition&lt;/strong&gt;：每个 key 通过 Partitioner 决定进入哪个分区（默认按 key 的 hash 对 Reduce 数量取模），每个分区对应一个 Reduce Task。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排序 Sort&lt;/strong&gt;：溢写前先按 key 排序，默认只按 key 排序，可以配置二次排序（key + value 联合排序）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合并 Combine&lt;/strong&gt;（可选）：Map 端先做一次局部聚合，减少网络传输量，比如 &lt;code&gt;(hello,1)(hello,1)&lt;/code&gt; 可以先合并成 &lt;code&gt;(hello,2)&lt;/code&gt;。注意 Combiner 的输入输出类型必须与 Reduce 一致，且只适用于&lt;strong&gt;满足交换律和结合律&lt;/strong&gt;的聚合（如求和、取 max，但不适合求平均值）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩&lt;/strong&gt;（可选）：通过 &lt;code&gt;mapreduce.map.output.compress&lt;/code&gt; 开启中间结果压缩，比如 Snappy，大幅减少磁盘 IO 和网络传输。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;溢写 Spill&lt;/strong&gt;：缓冲区满后把数据写入本地磁盘，生成溢写文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;归并 Merge&lt;/strong&gt;：多个溢写文件在 Map 结束时合并成一个大文件（同时做分区内排序），并建立索引方便 Reduce 拉取。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Reduce 端 Shuffle：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduce Task 启动后，启动 copy 线程从各 Map 端&lt;strong&gt;拉取属于自己分区的数据&lt;/strong&gt;，边拉边做归并。&lt;/li&gt;
&lt;li&gt;拉取的数据先放内存缓冲区，内存不够时写磁盘，最终把多个 map 的输出归并成一个有序的输入喂给 &lt;code&gt;reduce()&lt;/code&gt; 方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Map 端:  环形缓冲 -&amp;gt; 分区 -&amp;gt; 排序 -&amp;gt; (combine) -&amp;gt; (压缩) -&amp;gt; 溢写 -&amp;gt; 归并
                                                   |
Reduce 端:  &amp;lt;------ 拉取自己分区数据 ------ 内存/磁盘归并 -&amp;gt; 分组有序输入 -&amp;gt; reduce()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Reduce 阶段&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reduce()&lt;/code&gt; 收到 &lt;code&gt;(key, value迭代器)&lt;/code&gt;，把相同 key 的 value 做聚合，输出结果。&lt;/li&gt;
&lt;li&gt;Reduce 完成后把结果写入 HDFS（通过 OutputFormat），每个分区输出一个文件，命名如 &lt;code&gt;part-r-00000&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. Output 输出&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;输出通过 OutputFormat 控制，默认 TextOutputFormat 每行一条记录，&lt;code&gt;key \t value&lt;/code&gt; 格式。&lt;/li&gt;
&lt;li&gt;输出文件写入 HDFS 通常为 3 副本，直接面向用户或下游任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;核心机制&lt;/h2&gt;
&lt;h3&gt;Job / Task 概念&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Job&lt;/strong&gt;：一个完整的 MapReduce 程序（如一次 WordCount），包含多个 Task。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task&lt;/strong&gt;：Job 的最小执行单元，分为 &lt;strong&gt;Map Task&lt;/strong&gt; 和 &lt;strong&gt;Reduce Task&lt;/strong&gt;，Task 内部又划分为 &lt;code&gt;setup -&amp;gt; map/reduce -&amp;gt; cleanup&lt;/code&gt; 三阶段。&lt;/li&gt;
&lt;li&gt;Task 运行在 &lt;strong&gt;YARN 的 Container&lt;/strong&gt; 里，一个 Container 就是一个任务的计算资源（内存 + CPU）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;容错机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Task 失败重试&lt;/strong&gt;：Task 运行失败后由 ApplicationMaster（AM）重新调度执行，默认重试 4 次（&lt;code&gt;mapreduce.map.maxattempts&lt;/code&gt; / &lt;code&gt;mapreduce.reduce.maxattempts&lt;/code&gt;），重试次数耗尽则整个 Job 失败。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推测执行（Speculative Execution）&lt;/strong&gt;：当某个 Task 运行速度明显慢于同 Job 其他 Task（&quot;拖后腿&quot;），AM 会在另一台机器上&lt;strong&gt;启动一个相同的备份任务&lt;/strong&gt;，谁先跑完用谁的结果，后完成者直接 kill。这能有效缓解集群不均衡问题，但会浪费资源，对反复失败的机器反而加重负担。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AM 失败&lt;/strong&gt;：AM 自身失败后由 YARN ResourceManager 重新启动，恢复任务状态（Hadoop 2.x 支持 AM 自动恢复）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据本地性（机架感知调度）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;计算移动比数据移动成本低，所以调度器优先把 Task 调度到&lt;strong&gt;数据所在的节点&lt;/strong&gt;（Node-local），其次同一机架（Rack-local），最后才是跨机架（Off-rack）。&lt;/li&gt;
&lt;li&gt;这依赖 &lt;strong&gt;机架感知（Rack Awareness）&lt;/strong&gt;：HDFS 知道每个节点的机架位置，调度时据此决策。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
数据本地性层级：Node-local（数据在本地磁盘）&amp;gt; Rack-local（数据在同机架其他节点）&amp;gt; Off-rack（跨机架）。层级越低，网络传输越多，Job 越慢。
:::&lt;/p&gt;
&lt;h3&gt;Combiner 与 Partitioner 的区别&lt;/h3&gt;
&lt;p&gt;这是面试高频考点，二者完全不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Combiner&lt;/strong&gt;：Map 端的&lt;strong&gt;本地 reducer&lt;/strong&gt;，对 map 输出做预聚合，发生在 Map 端、Reduce 之前，目的是&lt;strong&gt;减少网络传输量&lt;/strong&gt;。可选组件，逻辑必须可交换可结合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partitioner&lt;/strong&gt;：决定每个 &lt;code&gt;(key, value)&lt;/code&gt; &lt;strong&gt;进入哪个分区&lt;/strong&gt;（哪个 Reduce Task），发生在 Map 端溢写之前。默认 HashPartitioner 按 key hash 取模，可自定义实现数据分发策略（比如让某个 key 单独进一个分区）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
Combiner 的聚合逻辑是&quot;预聚合&quot;，最终结果必须和不用 Combiner 一致，否则会算错。典型的反例是求平均值：两个分片的局部平均值不能直接再平均。
:::&lt;/p&gt;
&lt;h3&gt;Writable 序列化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hadoop 不直接使用 Java 原生的 Serializable，而是自己实现了 &lt;strong&gt;Writable&lt;/strong&gt; 序列化机制。&lt;/li&gt;
&lt;li&gt;原因：Java 序列化会携带大量类信息、对象结构等冗余，序列化结果体积大；Writable 只序列化字段值，&lt;strong&gt;体积小、速度快&lt;/strong&gt;，适合大规模网络传输和磁盘写入。&lt;/li&gt;
&lt;li&gt;常用类型：&lt;code&gt;IntWritable&lt;/code&gt;、&lt;code&gt;LongWritable&lt;/code&gt;、&lt;code&gt;Text&lt;/code&gt;、&lt;code&gt;DoubleWritable&lt;/code&gt; 等，自定义数据类型需要实现 &lt;code&gt;Writable&lt;/code&gt; 接口（序列化/反序列化顺序要一致）。&lt;/li&gt;
&lt;li&gt;若 key 参与排序，还需实现 &lt;code&gt;WritableComparable&lt;/code&gt; 接口（compareTo 方法）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;InputFormat / OutputFormat&lt;/h2&gt;
&lt;p&gt;InputFormat 负责&lt;strong&gt;切分输入数据 + 解析记录&lt;/strong&gt;，OutputFormat 负责&lt;strong&gt;写出结果&lt;/strong&gt;。都是可插拔的，通过 &lt;code&gt;job.setInputFormatClass()&lt;/code&gt; / &lt;code&gt;job.setOutputFormatClass()&lt;/code&gt; 指定。&lt;/p&gt;
&lt;p&gt;常见 InputFormat：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TextInputFormat&lt;/strong&gt;（默认）：按行读取，key 为行偏移量（LongWritable），value 为一行内容（Text）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KeyValueTextInputFormat&lt;/strong&gt;：每行按分隔符（默认 tab）拆成 key-value。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SequenceFileInputFormat&lt;/strong&gt;：读取二进制 SequenceFile，适合 MR 与 MR 之间的中间结果，不解析成文本，效率更高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CombineFileInputFormat&lt;/strong&gt;：把小文件合并成一个 split，专治小文件过多的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见 OutputFormat：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TextOutputFormat&lt;/strong&gt;（默认）：每行一条 &lt;code&gt;key\tvalue&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SequenceFileOutputFormat&lt;/strong&gt;：输出二进制 SequenceFile，供下游 MR 直接读。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MultipleOutputs&lt;/strong&gt;：按条件输出到不同文件/目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;重要参数介绍&lt;/h2&gt;
&lt;p&gt;:::note
下面是我常用的核心参数，调优时优先关注这些，其他参数按需再查。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;资源类：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 单个 Map Task 的内存上限（MB），默认 1024
mapreduce.map.memory.mb=1024

# 单个 Reduce Task 的内存上限（MB），默认 1024
mapreduce.reduce.memory.mb=1024

# 单个 Map Task 申请的虚拟 CPU 核数，默认 1
mapreduce.map.cpu.vcores=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Shuffle / 排序类：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Map 端排序缓冲区大小（MB），默认 100，通常配合调大 memory.mb
mapreduce.task.io.sort.mb=100

# 缓冲区溢写阈值（百分比），默认 0.80，即缓冲区用到 80% 时开始溢写
mapreduce.map.sort.spill.percent=0.80

# Reduce 端 shuffle 拉取数据的内存占比，默认 0.70（mapreduce.reduce.shuffle.parallelcopies 控制并行 copy 线程数，默认 5）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;任务数 / 重试类：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 手动指定 Reduce 数量（默认为 1），生产上常设为节点数或合理估算值
mapreduce.job.reduces=10

# Map Task 最大重试次数，默认 4，超限则 Job 失败
mapreduce.map.maxattempts=4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;推测执行类：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Map / Reduce 推测执行开关，生产上默认开启，一般不建议关
mapreduce.map.speculative=true
mapreduce.reduce.speculative=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Combiner / 压缩类：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 指定 Combiner 类（和 reducer 类相同则 job.setCombinerClass）
# 中间结果压缩（Map 端输出，强烈建议开启）
mapreduce.map.output.compress=true
mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
&lt;code&gt;mapreduce.job.reduces&lt;/code&gt; 不设置时默认只有 1 个 Reduce，大量小数据全部压到一个任务里，既慢又容易 OOM，集群大时千万别忘设置。
:::&lt;/p&gt;
&lt;h2&gt;常见问题排查&lt;/h2&gt;
&lt;h3&gt;1. 数据倾斜&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：某个/某几个 Reduce 跑得特别慢，其他 Reduce 早早就结束了；或单个 Map 处理的数据量异常大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：key 分布不均，比如热词、空 key、业务上某个 key 天然占大头；Partitioner 取模后多对一。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空 key 加随机前缀后再分区，结果再处理。&lt;/li&gt;
&lt;li&gt;热点 key 先做两阶段聚合（局部聚合 + 全局聚合）。&lt;/li&gt;
&lt;li&gt;自定义 Partitioner，让热点 key 均匀分散。&lt;/li&gt;
&lt;li&gt;增大 Reduce 数量只能缓解，不能根治。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. OOM（内存溢出）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Container 被杀，报 &lt;code&gt;Container killed by the ApplicationMaster&lt;/code&gt; 或 GC 异常。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;mapreduce.map.memory.mb&lt;/code&gt; 设置过小，或数据量超过预期。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调大 &lt;code&gt;mapreduce.map.memory.mb&lt;/code&gt; / &lt;code&gt;mapreduce.reduce.memory.mb&lt;/code&gt;，同时调大 YARN 侧 &lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt; 和 &lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;调大 &lt;code&gt;mapreduce.task.io.sort.mb&lt;/code&gt; 需同步调大容器内存，因为缓冲区从容器堆内存中分配。&lt;/li&gt;
&lt;li&gt;检查代码里是否有不必要的对象缓存，尽量流式处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 小文件过多&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Job 启动的 Map Task 数量巨大、元数据撑爆 NameNode、启动开销远超计算时间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：一个文件（split）对应一个 Map，百万个小文件就是百万个 Map Task。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;CombineFileInputFormat&lt;/code&gt; 把小文件合并成少量 split。&lt;/li&gt;
&lt;li&gt;源头治理：上游合并输出、定期归档小文件（HDFS 层面合并）。&lt;/li&gt;
&lt;li&gt;输出端避免产生太多小分区文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. Shuffle 慢&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Job 时间大部分耗在 shuffle，Map 结束后 Reduce 端迟迟不结束。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开启中间结果压缩（Snappy），减少磁盘 IO 和网络传输。&lt;/li&gt;
&lt;li&gt;调大 &lt;code&gt;mapreduce.reduce.shuffle.parallelcopies&lt;/code&gt;，提高并发拉取。&lt;/li&gt;
&lt;li&gt;合理使用 Combiner 减少传输量。&lt;/li&gt;
&lt;li&gt;确认 Map 端溢写次数是否过多（溢写次数 = shuffle 垃圾），调大 sort buffer。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. Reducer 数量设置不当&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;过多&lt;/strong&gt;：每个 Reduce 处理数据量小，任务启动/调度开销占比大，输出文件碎片化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过少&lt;/strong&gt;：单个 Reduce 压力大，慢、OOM、输出文件过大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经验值&lt;/strong&gt;：&lt;code&gt;Reduce 数 ≈ 节点数 × 每节点 1~2 个&lt;/code&gt;，或者按 &lt;code&gt;总数据量 / 单 Reduce 处理量（约 1GB）&lt;/code&gt;估算。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 任务卡住 / 重复执行&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：进度一直卡在 66.6% 等数字不动，或日志显示同一 Task 反复执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码逻辑死循环、等待外部资源（如连接数据库）。&lt;/li&gt;
&lt;li&gt;Task 反复失败但没超重试上限，被 AM 反复调度。&lt;/li&gt;
&lt;li&gt;数据本地性差，大量 Task 等待传输；或节点宕机后任务重新调度。&lt;/li&gt;
&lt;li&gt;Speculative 启动的备份任务互相竞争，资源紧张时拖慢整体。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;排查&lt;/strong&gt;：看 AM/Container 日志（&lt;code&gt;yarn logs -applicationId&lt;/code&gt;），确认是失败重试还是慢任务；检查节点资源水位；必要时关闭推测执行观察。&lt;/p&gt;
&lt;h3&gt;7. 本地化率低&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：大量 Map Task 在非数据所在节点运行，网络传输量大，Job 慢。YARN 的 &lt;code&gt;yarn-clusters&lt;/code&gt; 或 ResourceManager 界面可以看本地化率。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：split 与 block 不对齐、节点宕机/资源不足、调度器未开启延迟调度、数据刚写入尚未就绪。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保证输入文件块大小与 split 对齐（默认 128MB 即可）。&lt;/li&gt;
&lt;li&gt;检查数据是否使用 Erasure Coding 或副本数导致本地化失效。&lt;/li&gt;
&lt;li&gt;调整 YARN 调度器配置（开启 &lt;code&gt;yarn.scheduler.capacity.node-locality-delay&lt;/code&gt; 等延迟调度参数，给本地化一些等待时间）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;:::tip
MapReduce 的脉络其实很清晰：&lt;strong&gt;分（InputSplit + Map）→ 洗（Partition + Sort + Merge + 网络传输）→ 合（Reduce）→ 出（Output）&lt;/strong&gt;。把 Shuffle 六个动作的时序搞清楚，参数就都串起来了；把任务失败、倾斜、OOM 这几个高频问题的特征记牢，生产排查就不慌。
:::&lt;/p&gt;
&lt;p&gt;最后提醒一点：现在大部分场景下 Flink、Spark 已经取代 MapReduce 成为主流，但 MapReduce 的分区、排序、归并、推测执行、本地性这些思想被大量沿用，理解它依然是深入大数据的必修课。&lt;/p&gt;
</content:encoded></item><item><title>MySQL 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/mysql-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/mysql-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是对 MySQL 核心知识的系统性总结，覆盖架构、InnoDB 索引、事务与 MVCC、锁、日志、缓冲池以及日常问题排查要点，作为个人技术笔记使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;整体架构：一条 SQL 是如何执行的&lt;/h2&gt;
&lt;p&gt;MySQL 的架构可以自上而下分为四层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;连接层&lt;/strong&gt;：负责客户端连接管理、认证授权、线程复用（thread pool）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务层&lt;/strong&gt;：SQL 接口、解析器、优化器、执行器、缓存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储引擎层&lt;/strong&gt;：可插拔式，常用 InnoDB、MyISAM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件系统层&lt;/strong&gt;：数据文件（.ibd）、日志文件（redo/binlog）、系统表空间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一条 SQL 的完整执行流程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;客户端 → 连接器(建立连接/权限校验) → 查询缓存(8.0 已移除)
       → 解析器(词法/语法分析，生成语法树)
       → 优化器(选择索引、生成执行计划)
       → 执行器(调用存储引擎 API，返回结果)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
查询缓存 8.0 已被移除，因为任何写操作都会使其大面积失效，维护成本远大于收益。
:::&lt;/p&gt;
&lt;p&gt;各组件职责：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;连接器&lt;/strong&gt;：&lt;code&gt;mysql -uroot -p&lt;/code&gt; 建立连接，验证用户名密码，连接默认 8 小时空闲自动断开&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解析器&lt;/strong&gt;：词法分析拆分关键字，语法分析校验语法，出错报 &lt;code&gt;You have an error in your SQL syntax&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化器&lt;/strong&gt;：决定用哪个索引、join 顺序，可通过 &lt;code&gt;explain&lt;/code&gt; 观察优化结果&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行器&lt;/strong&gt;：判断权限，调用引擎接口逐行读取数据，&lt;code&gt;rows_examined&lt;/code&gt; 统计扫描行数&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;InnoDB 存储引擎与 B+ 树索引&lt;/h2&gt;
&lt;h3&gt;为什么是 B+ 树&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;相比哈希索引，B+ 树支持范围查询与排序&lt;/li&gt;
&lt;li&gt;相比 B 树，B+ 树只有叶子节点存数据，非叶子节点可存放更多索引键，树更矮，IO 次数更少（3~4 层即可支撑千万级数据）&lt;/li&gt;
&lt;li&gt;叶子节点之间通过链表串联，天然支持范围扫描与排序&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;聚簇索引与二级索引&lt;/h3&gt;
&lt;p&gt;InnoDB 表数据本身按主键组织，主键索引即聚簇索引：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;聚簇索引&lt;/strong&gt;：叶子节点直接存放整行数据，主键查找一次 IO 即可拿到数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二级索引&lt;/strong&gt;：叶子节点存放索引列 + 主键值，查询需要先找主键，再回表查数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回表&lt;/strong&gt;：通过二级索引查到主键，再回聚簇索引取整行，多一次 IO&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;索引覆盖&lt;/strong&gt;：select 的列都在索引中，无需回表，&lt;code&gt;explain&lt;/code&gt; 中 &lt;code&gt;Using index&lt;/code&gt; 即表示覆盖&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
主键建议使用自增整数，因为聚簇索引物理有序，随机字符串主键会导致页分裂与碎片。
:::&lt;/p&gt;
&lt;h3&gt;联合索引与最左前缀&lt;/h3&gt;
&lt;p&gt;联合索引 &lt;code&gt;(a, b, c)&lt;/code&gt; 实际上创建了 &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;(a,b)&lt;/code&gt;、&lt;code&gt;(a,b,c)&lt;/code&gt; 三个索引，遵循&lt;strong&gt;最左前缀原则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;where a=1&lt;/code&gt; ✅ 用到索引&lt;/li&gt;
&lt;li&gt;&lt;code&gt;where a=1 and b=2&lt;/code&gt; ✅ 用到索引&lt;/li&gt;
&lt;li&gt;&lt;code&gt;where b=2&lt;/code&gt; ❌ 索引失效，因为跳过了最左列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;where a=1 and c=3&lt;/code&gt; ⚠️ 只用到 a 列，c 列无法用上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;排序字段如果符合最左前缀，还能避免 filesort。&lt;/p&gt;
&lt;h3&gt;页结构与行格式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;InnoDB 最小存储单位是&lt;strong&gt;页&lt;/strong&gt;，默认 16KB，页内由目录（Page Directory）、用户记录区组成&lt;/li&gt;
&lt;li&gt;页之间通过链表连接，树的高度即索引层数&lt;/li&gt;
&lt;li&gt;行格式默认 &lt;code&gt;DYNAMIC&lt;/code&gt;，变长字段（varchar/text/blob）超长时部分存储在溢出页，页内只存 20 字节指针&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 查看行格式
SHOW TABLE STATUS LIKE &apos;t_user&apos;;
-- 查看索引
SHOW INDEX FROM t_user;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;事务：ACID 与隔离级别&lt;/h2&gt;
&lt;p&gt;事务四大特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子性 A&lt;/strong&gt;：要么全部成功要么全部失败，靠 undo log 回滚&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性 C&lt;/strong&gt;：事务前后数据总量不变，由其他三个特性共同保证&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔离性 I&lt;/strong&gt;：并发事务互不干扰，靠锁与 MVCC&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久性 D&lt;/strong&gt;：事务提交后不丢失，靠 redo log&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;隔离级别（由低到高）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;级别&lt;/th&gt;
&lt;th&gt;脏读&lt;/th&gt;
&lt;th&gt;不可重复读&lt;/th&gt;
&lt;th&gt;幻读&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read Uncommitted&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read Committed&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;td&gt;可能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable Read&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;可能（InnoDB 通过 next-key lock 解决）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;td&gt;不会&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
MySQL 默认隔离级别是 Repeatable Read，通过间隙锁完全解决了幻读问题，与 Oracle 默认 RC 不同。
:::&lt;/p&gt;
&lt;h2&gt;MVCC 原理&lt;/h2&gt;
&lt;p&gt;MVCC（多版本并发控制）用于解决读写冲突，实现&quot;读写不阻塞&quot;。&lt;/p&gt;
&lt;h3&gt;核心组成&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隐藏字段&lt;/strong&gt;：每行有隐藏的 &lt;code&gt;trx_id&lt;/code&gt;（最近修改该行的事务 id）、&lt;code&gt;roll_pointer&lt;/code&gt;（指向 undo log 版本链）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;undo log&lt;/strong&gt;：记录事务修改前的数据，多个版本串成一条版本链&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ReadView&lt;/strong&gt;：事务生成快照时记录活跃事务列表，用于判断版本可见性&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ReadView 判断规则&lt;/h3&gt;
&lt;p&gt;ReadView 包含四个字段：&lt;code&gt;m_ids&lt;/code&gt;（活跃事务 id 集合）、&lt;code&gt;min_trx_id&lt;/code&gt;、&lt;code&gt;max_trx_id&lt;/code&gt;、&lt;code&gt;creator_trx_id&lt;/code&gt;。判断某版本可见性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trx_id &amp;lt; min_trx_id&lt;/code&gt;：已提交，可见&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trx_id &amp;gt; max_trx_id&lt;/code&gt;：未开始，不可见&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;m_ids&lt;/code&gt; 中：活跃事务（含未提交），不可见&lt;/li&gt;
&lt;li&gt;等于 &lt;code&gt;creator_trx_id&lt;/code&gt;：自己修改的，可见&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;快照读与当前读&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;快照读&lt;/strong&gt;：普通 &lt;code&gt;select&lt;/code&gt;，读 ReadView 生成的快照，不加锁。RC 每次 select 重新生成 ReadView，RR 只在第一次 select 生成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当前读&lt;/strong&gt;：&lt;code&gt;select ... for update&lt;/code&gt;、&lt;code&gt;update&lt;/code&gt;、&lt;code&gt;delete&lt;/code&gt;、&lt;code&gt;insert&lt;/code&gt;，读最新数据并加锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
RR 下为什么能解决幻读：当前读靠 next-key lock 锁住间隙阻止插入，快照读靠 ReadView 一致性快照保证两次读取结果一致。
:::&lt;/p&gt;
&lt;h2&gt;锁机制&lt;/h2&gt;
&lt;p&gt;按粒度分类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全局锁&lt;/strong&gt;：&lt;code&gt;flush tables with read lock&lt;/code&gt;，锁整个库，用于全库备份&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表级锁&lt;/strong&gt;：&lt;code&gt;lock tables ... read/write&lt;/code&gt;、元数据锁（DDL 时自动加）、意向锁&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行级锁&lt;/strong&gt;：InnoDB 特有，包括记录锁（Record Lock）、间隙锁（Gap Lock）、临键锁（Next-Key Lock）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
行锁是加在索引上的！没有索引的 update/delete 会退化为锁全表，生产事故高发点。
:::&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;记录锁&lt;/strong&gt;：锁定单行记录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;间隙锁&lt;/strong&gt;：锁定一个区间，只阻止其他事务在间隙内插入，用于解决幻读&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;临键锁&lt;/strong&gt;：记录锁 + 间隙锁的组合，左开右闭区间，RR 下默认使用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;死锁&lt;/h3&gt;
&lt;p&gt;两个事务互相持有对方需要的锁时发生，InnoDB 会自动检测并回滚代价较小的事务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;死锁常见解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务上统一加锁顺序（如都按 id 升序操作）&lt;/li&gt;
&lt;li&gt;缩短事务时间，减少锁持有&lt;/li&gt;
&lt;li&gt;在 MySQL 8.0+ 使用 &lt;code&gt;innodb_deadlock_detect&lt;/code&gt; 控制检测开关&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三大日志：redo log / binlog / undo log&lt;/h2&gt;
&lt;h3&gt;redo log（重做日志）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;物理日志，记录&quot;某页某偏移做了什么修改&quot;，用于&lt;strong&gt;崩溃恢复&lt;/strong&gt;，保证持久性&lt;/li&gt;
&lt;li&gt;采用 WAL（Write-Ahead Logging）：先写日志、再写磁盘数据页&lt;/li&gt;
&lt;li&gt;环形写入，&lt;code&gt;innodb_log_file_size&lt;/code&gt; 控制大小，满了触发刷盘 checkpoint&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;binlog（归档日志）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;逻辑日志，记录 SQL 或行变更，用于&lt;strong&gt;主从复制&lt;/strong&gt;与&lt;strong&gt;数据恢复&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;三种格式：&lt;code&gt;STATEMENT&lt;/code&gt;（记录 SQL）、&lt;code&gt;ROW&lt;/code&gt;（记录行变更，默认，最安全）、&lt;code&gt;MIXED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;与 redo log 的区别：redo 是 InnoDB 引擎层的物理日志（环形覆盖），binlog 是 Server 层的逻辑日志（追加写）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;两阶段提交&lt;/h3&gt;
&lt;p&gt;redo log 与 binlog 要保持一致，否则崩溃恢复或主从复制可能出现数据不一致，因此采用两阶段提交：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prepare 阶段：写 redo log 并置为 prepare 状态
commit 阶段：写 binlog，成功后 redo log 置为 commit 状态
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
两阶段提交的恢复原则：崩溃后若 redo 处于 prepare 状态，检查 binlog 是否完整，完整则提交、否则回滚，保证两份日志一致。
:::&lt;/p&gt;
&lt;h3&gt;undo log（回滚日志）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;逻辑日志，记录修改前的旧值，用于事务回滚与 MVCC 版本链&lt;/li&gt;
&lt;li&gt;对应回滚段（rollback segment），purge 线程负责清理不再被任何 ReadView 引用的版本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Buffer Pool 与 change buffer&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Buffer Pool&lt;/strong&gt;：InnoDB 的内存缓冲，缓存数据页与索引页，默认 128MB（建议机器物理内存 50%~70%）。&lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; 可动态调整&lt;/li&gt;
&lt;li&gt;读：优先从缓冲池查，未命中才去磁盘加载&lt;/li&gt;
&lt;li&gt;写：修改先在缓冲池完成（脏页），由后台线程按 checkpoint 刷盘&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;change buffer&lt;/strong&gt;：针对二级索引的变更缓冲，插入/更新二级索引时若目标页不在缓冲池，先记录变更而非直接写磁盘，等页面被读到或后台合并&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
唯一索引的插入无法使用 change buffer（必须先读页判断唯一性），因此&quot;写多读少&quot;的场景优先使用普通索引。
:::&lt;/p&gt;
&lt;h2&gt;常规问题排查&lt;/h2&gt;
&lt;h3&gt;慢查询定位&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;开启慢查询日志：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;  -- 超过 1 秒记录
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;拿到慢 SQL 后执行 &lt;code&gt;explain&lt;/code&gt;，重点看：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;td&gt;all（全表扫描）→ index → range → ref → const，最好到 range 以上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;td&gt;实际使用的索引，为 null 表示没走索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rows&lt;/td&gt;
&lt;td&gt;预估扫描行数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Using filesort&lt;/code&gt;/&lt;code&gt;Using temporary&lt;/code&gt; 需要优化&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;索引失效常见场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;对索引列使用函数或计算：&lt;code&gt;where year(create_time)=2026&lt;/code&gt; ❌&lt;/li&gt;
&lt;li&gt;隐式类型转换：&lt;code&gt;where phone = 13800000000&lt;/code&gt;（phone 是 varchar）❌&lt;/li&gt;
&lt;li&gt;前导模糊查询：&lt;code&gt;like &apos;%abc&apos;&lt;/code&gt; ❌&lt;/li&gt;
&lt;li&gt;联合索引不满足最左前缀&lt;/li&gt;
&lt;li&gt;索引列用 &lt;code&gt;or&lt;/code&gt; 且另一侧无索引&lt;/li&gt;
&lt;li&gt;大量数据 &lt;code&gt;not in&lt;/code&gt; / &lt;code&gt;is not null&lt;/code&gt; 导致优化器放弃索引&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;死锁与锁等待排查&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;SHOW ENGINE INNODB STATUS;  -- 查看死锁/锁等待
SELECT * FROM performance_schema.data_lock_waits;  -- 8.0 查看锁等待
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;连接数打满&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;查看连接：&lt;code&gt;show status like &apos;Threads_connected&apos;;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看超时：&lt;code&gt;show variables like &apos;wait_timeout&apos;;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排查长事务：&lt;code&gt;select * from information_schema.innodb_trx;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;应用侧：连接池合理配置、防止慢 SQL 占用连接、及时释放事务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;主从延迟&lt;/h3&gt;
&lt;p&gt;常见原因与对策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主库大事务/大 DDL：拆分事务，DDL 用 pt-osc 类工具&lt;/li&gt;
&lt;li&gt;从库单线程回放：开启并行复制 &lt;code&gt;slave_parallel_workers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;从库硬件差、有分析类大查询：提升配置或分担只读流量&lt;/li&gt;
&lt;li&gt;查看延迟：&lt;code&gt;show slave status&lt;/code&gt; 的 &lt;code&gt;Seconds_Behind_Master&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;磁盘与内存问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;磁盘写满：优先清理 binlog（&lt;code&gt;purge binary logs&lt;/code&gt;）与慢日志，检查 binlog 保留时间&lt;/li&gt;
&lt;li&gt;磁盘 IO 高：关注刷盘策略 &lt;code&gt;innodb_flush_log_at_trx_commit=1&lt;/code&gt; 时每次提交 fsync，可权衡调整为 2&lt;/li&gt;
&lt;li&gt;内存不足：swap 会拖垮性能，检查 Buffer Pool 是否过大；Buffer Pool 命中率低时增大缓冲池并优化 SQL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
&lt;code&gt;innodb_flush_log_at_trx_commit=1&lt;/code&gt; 保证每次提交都刷盘（最安全），改为 0/2 可能丢最近 1 秒日志，生产环境需谨慎权衡。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;架构上理解一条 SQL 从解析、优化到执行的完整链路&lt;/li&gt;
&lt;li&gt;索引核心是 B+ 树 + 最左前缀 + 覆盖索引，减少回表&lt;/li&gt;
&lt;li&gt;事务靠 MVCC 与锁实现隔离，redo/binlog 两阶段提交保证一致性与持久性&lt;/li&gt;
&lt;li&gt;排查问题的核心抓手：慢查询日志、explain、&lt;code&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt;、&lt;code&gt;performance_schema&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Nacos 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/nacos-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/nacos-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文梳理 Nacos 的架构、服务发现（AP/Distro）、配置中心（长轮询）、集群一致性（Raft + Derby/MySQL）、与主流注册中心的对比，以及常见问题的排查思路。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Nacos（Dynamic Naming and Configuration Service）是阿里巴巴开源的，同时提供&lt;strong&gt;服务发现&lt;/strong&gt;与&lt;strong&gt;配置管理&lt;/strong&gt;两大能力的中间件，是 Spring Cloud Alibaba 生态的核心组件。&lt;/p&gt;
&lt;p&gt;:::tip
Nacos 的核心卖点是&quot;一个组件解决注册中心 + 配置中心两个问题&quot;，且原生支持 AP（可用性优先）与 CP（一致性优先）双模型切换，比 Zookeeper/Eureka 的组合更简单。
:::&lt;/p&gt;
&lt;p&gt;核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命名空间（Namespace）&lt;/strong&gt;：用于多环境/多租户隔离，默认 public&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分组（Group）&lt;/strong&gt;：同一命名空间内的逻辑分组，默认 DEFAULT_GROUP，用于区分不同业务域&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务（Service）&lt;/strong&gt;：逻辑上的服务名，下面挂多个实例（Instance）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实例（Instance）&lt;/strong&gt;：具体提供服务的地址（ip:port + 元数据），分为临时实例与持久实例&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 定位一个配置/服务资源的完整标识
namespace + group + service/dataId
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;架构与一致性模型&lt;/h2&gt;
&lt;h3&gt;Server 端双模型&lt;/h3&gt;
&lt;p&gt;Nacos Server 内部有两种一致性协议，按数据类别分工：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数据&lt;/th&gt;
&lt;th&gt;一致性模型&lt;/th&gt;
&lt;th&gt;底层协议&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;注册中心（临时实例）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distro&lt;/td&gt;
&lt;td&gt;多节点异步复制，可用性优先，允许短暂不一致&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;注册中心（持久实例）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Raft&lt;/td&gt;
&lt;td&gt;强一致，配合健康检查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置中心&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Raft&lt;/td&gt;
&lt;td&gt;配置必须强一致，防止读到旧配置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
Nacos 1.x 的 Raft 是自研简化版（内部依赖轻量级实现），Nacos 2.x 起配置/持久化数据统一采用 &lt;strong&gt;Raft（Jraft）&lt;/strong&gt; 实现，并引入了 &lt;strong&gt;gRPC&lt;/strong&gt; 通信协议，大幅提升性能。
:::&lt;/p&gt;
&lt;h3&gt;架构角色&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Server&lt;/strong&gt;：Nacos 集群节点，通过 Raft 选主；负责数据存储、健康检查、配置推送&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client（SDK）&lt;/strong&gt;：业务侧 Java/Go 等 SDK，注册实例、订阅配置、维护心跳；也提供 &lt;strong&gt;OpenAPI&lt;/strong&gt;（HTTP）方式接入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Console&lt;/strong&gt;：控制台，管理命名空间、服务、配置&lt;/li&gt;
&lt;li&gt;2.x 起客户端与 Server 之间默认走 &lt;strong&gt;gRPC 长连接&lt;/strong&gt;（1.x 是 HTTP 短连接 + UDP 推送），每个服务实例一个长连接，服务端主动推送配置变更&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
Nacos 2.x 的 gRPC 长连接是常见问题根源：客户端与服务端之间的连接被防火墙/网关静默断开（半开连接），会导致注册信息不更新、配置收不到推送。排查时优先看 gRPC 端口（默认 9848、9849）是否放通。
:::&lt;/p&gt;
&lt;h2&gt;服务发现&lt;/h2&gt;
&lt;h3&gt;注册原理&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;客户端启动时调用注册接口（SDK &lt;code&gt;naming.registerInstance&lt;/code&gt; 或 OpenAPI POST &lt;code&gt;/nacos/v1/ns/instance&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;临时实例注册到&lt;strong&gt;当前连接的 Server 节点&lt;/strong&gt;，节点通过 Distro 协议异步同步给其他节点&lt;/li&gt;
&lt;li&gt;注册数据放入本地内存 + 定期同步，心跳续约&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;健康检查（双机制）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;临时实例：客户端主动上报（心跳）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;客户端每 5 秒发一次心跳（&lt;code&gt;beat&lt;/code&gt;），服务端每 15 秒检查一次，超过 30 秒（默认）未收到心跳则标记不健康并剔除&lt;/li&gt;
&lt;li&gt;心跳超时判定、实例剔除都是&lt;strong&gt;每台节点本地执行&lt;/strong&gt;，靠 Distro 同步结果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久实例：服务端主动探测&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;服务端按配置的检查协议（HTTP/TCP/MySQL 等）主动探测实例健康状态&lt;/li&gt;
&lt;li&gt;探测失败按 &lt;code&gt;healthyCheckTimes&lt;/code&gt; 阈值判定不健康&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
临时实例适合&quot;进程级注册&quot;（进程退出即消失），持久实例适合&quot;需要服务端探测&quot;的静态/外部服务（如数据库、跨网络的服务）。默认是临时实例。
:::&lt;/p&gt;
&lt;h3&gt;保护阈值（Protect Threshold）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个服务可设置 &lt;code&gt;protectThreshold&lt;/code&gt;（0~1，默认 0）&lt;/li&gt;
&lt;li&gt;当&lt;strong&gt;健康实例数 / 总实例数 &amp;lt; 保护阈值&lt;/strong&gt;时，触发保护：不再剔除不健康实例，而是把不健康实例也返回给调用方&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
保护阈值的本意是&quot;防止全部实例被误判下线导致流量打空&quot;（比返回空列表好，至少能尝试调用）。但如果不健康实例真不可用，流量会大量失败。排查&quot;突然出现大量失败调用&quot;时要先看是否触发了保护阈值。
:::&lt;/p&gt;
&lt;h3&gt;订阅与变更感知&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;消费者订阅服务，SDK 通过 gRPC 建立订阅，服务端实例变更时&lt;strong&gt;主动推送&lt;/strong&gt;（UDP 广播推送在 1.x；2.x 走 gRPC 推送）&lt;/li&gt;
&lt;li&gt;客户端本地也做兜底：定期拉取全量实例列表（&lt;code&gt;failover&lt;/code&gt; 双缓存）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;配置中心&lt;/h2&gt;
&lt;h3&gt;发布与订阅&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;发布&lt;/strong&gt;：控制台/OpenAPI/SDK 写入配置，Server 通过 Raft 保证一致后持久化，再通知订阅方&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;订阅&lt;/strong&gt;：客户端 &lt;code&gt;configService.addListener&lt;/code&gt; 注册监听器（Listener），配置变更时回调 &lt;code&gt;receiveConfigInfo&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;长轮询机制（Long Polling）&lt;/h3&gt;
&lt;p&gt;这是 Nacos 配置中心的灵魂：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端发起配置拉取请求（带 &lt;code&gt;dataId + group + content-md5&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;如果本地缓存 md5 与服务端一致（无变化），服务端&lt;strong&gt;挂起请求&lt;/strong&gt;（默认 30 秒内不返回）&lt;/li&gt;
&lt;li&gt;期间配置被修改 → 服务端立即返回新配置；30 秒内无变化 → 返回&quot;无变化&quot;&lt;/li&gt;
&lt;li&gt;客户端拿到结果后，循环发起下一次长轮询&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;客户端 ---- 长轮询请求(带md5) ----&amp;gt; 服务端
客户端 &amp;lt;---- 立即返回新配置/超时返回无变化 --- 服务端
客户端(有变化) ----&amp;gt; 拉取完整配置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
长轮询的好处：相比纯推送，服务端不需要维护海量连接状态；相比短轮询，几乎无轮询开销，变更感知延迟能做到秒级甚至毫秒级。
:::&lt;/p&gt;
&lt;h3&gt;监听回调与灰度发布&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Listener&lt;/strong&gt;：&lt;code&gt;addListener&lt;/code&gt; 后，配置变更由内部长轮询线程感知，回调业务代码&lt;/li&gt;
&lt;li&gt;常见坑：回调内做重逻辑会阻塞配置线程；多个 listener 变更顺序不确定，不要依赖回调顺序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灰度发布（Beta 发布）&lt;/strong&gt;：指定部分 IP 灰度下发，通过 &lt;code&gt;publishConfig&lt;/code&gt; 携带灰度规则；灰度发布仅对临时配置有效，正式发布后灰度信息清除&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;配置快照与容灾&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;客户端本地有&lt;strong&gt;配置快照&lt;/strong&gt;（默认 &lt;code&gt;${user.home}/nacos/config&lt;/code&gt;），缓存最近拉取的配置&lt;/li&gt;
&lt;li&gt;Server 全挂时，客户端用本地快照 + &lt;code&gt;failover&lt;/code&gt; 机制继续提供旧配置，业务不中断&lt;/li&gt;
&lt;li&gt;服务端配置持久化：内嵌 &lt;strong&gt;Derby&lt;/strong&gt;（默认单机，集群共享一份）或外置 &lt;strong&gt;MySQL&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;集群部署与持久化&lt;/h2&gt;
&lt;h3&gt;集群一致性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;配置中心 / 持久实例：Raft（CP）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;集群选 Leader，写请求走 Leader，过半确认&lt;/li&gt;
&lt;li&gt;节点故障（&amp;lt; 半数）不影响写入&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;临时实例注册：Distro（AP）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;每个节点独立承接写请求（无 Leader 瓶颈），本地生效后&lt;strong&gt;异步&lt;/strong&gt;复制给其他节点&lt;/li&gt;
&lt;li&gt;节点故障期间的数据差异靠定期全量同步 + 心跳续约收敛&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
Distro 是最终一致：刚注册的实例可能在其他节点上要等一小段时间才能被读到（异步复制）。如果服务刚上线就被消费端请求打过来且报&quot;服务不存在&quot;，多半是 Distro 复制延迟 + 本地缓存未刷新。
:::&lt;/p&gt;
&lt;h3&gt;Raft 选举&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;基于 Raft 协议：Leader 心跳超时 → 触发选举 → 得票过半当选&lt;/li&gt;
&lt;li&gt;选举时比较 &lt;strong&gt;term + log index&lt;/strong&gt;（日志越新越有资格）&lt;/li&gt;
&lt;li&gt;集群建议奇数节点（3 台起步），单节点无法满足 CP 写入半数要求（单节点集群会退化为独立模式）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据持久化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内嵌 Derby&lt;/strong&gt;：默认模式，适合单机/快速体验；多节点共享同一份 Derby 存储时用 &lt;code&gt;jdbcUrl&lt;/code&gt; 指向同一个数据库（1.x 的集群默认方式是共享 Derby，存在锁问题，2.x 默认每节点独立 Derby，配置走 raft 同步）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外置 MySQL&lt;/strong&gt;：生产推荐，&lt;code&gt;application.properties&lt;/code&gt; 配置 &lt;code&gt;spring.datasource.platform=mysql&lt;/code&gt; 并执行初始化 SQL 脚本；MySQL 高可用（主从）需自行保证&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# conf/application.properties 关键配置
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8
db.user.0=root
db.password.0=root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
Derby 锁问题：1.x 集群多个节点共享同一个 Derby 数据库时，可能出现 &lt;code&gt;Table &apos;CONFIG_INFO&apos; in statement is locked&lt;/code&gt; 或写入失败。解决：改用 MySQL，或升级到 2.x 每节点独立 Derby。
:::&lt;/p&gt;
&lt;h2&gt;与 Zookeeper / Eureka / Consul 对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Nacos&lt;/th&gt;
&lt;th&gt;Zookeeper&lt;/th&gt;
&lt;th&gt;Eureka&lt;/th&gt;
&lt;th&gt;Consul&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;能力&lt;/td&gt;
&lt;td&gt;注册中心 + 配置中心&lt;/td&gt;
&lt;td&gt;协调服务（可做注册）&lt;/td&gt;
&lt;td&gt;仅注册中心&lt;/td&gt;
&lt;td&gt;注册 + 配置 + KV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一致性&lt;/td&gt;
&lt;td&gt;AP（临时）/ CP（配置）&lt;/td&gt;
&lt;td&gt;CP（ZAB）&lt;/td&gt;
&lt;td&gt;AP（无主）&lt;/td&gt;
&lt;td&gt;CP（Raft）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;健康检查&lt;/td&gt;
&lt;td&gt;心跳上报 + 服务端探测&lt;/td&gt;
&lt;td&gt;会话心跳（临时节点）&lt;/td&gt;
&lt;td&gt;客户端心跳&lt;/td&gt;
&lt;td&gt;服务端探测&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置中心&lt;/td&gt;
&lt;td&gt;内置，长轮询&lt;/td&gt;
&lt;td&gt;可做（watch）&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;内置（KV watch）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;协议&lt;/td&gt;
&lt;td&gt;gRPC / HTTP&lt;/td&gt;
&lt;td&gt;TCP 长连接 + ZAB&lt;/td&gt;
&lt;td&gt;HTTP 心跳&lt;/td&gt;
&lt;td&gt;HTTP + Raft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用&lt;/td&gt;
&lt;td&gt;Spring Cloud 全家桶&lt;/td&gt;
&lt;td&gt;强一致协调场景&lt;/td&gt;
&lt;td&gt;大规模注册（已停维护）&lt;/td&gt;
&lt;td&gt;多数据中心注册&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
选型建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;追求&quot;注册 + 配置一站式&quot;、Spring Cloud 技术栈 → Nacos&lt;/li&gt;
&lt;li&gt;需要强一致协调能力（锁、选主）→ Zookeeper&lt;/li&gt;
&lt;li&gt;纯注册中心、读写都大 → 考虑 Nacos AP 模式或 Consul&lt;/li&gt;
&lt;li&gt;Eureka 已进入维护期，新项目不建议
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常规问题排查&lt;/h2&gt;
&lt;h3&gt;1. 服务上下线不及时&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：服务停止后很久才从列表消失，或新服务迟迟不可见&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;临时实例靠心跳超时（默认 30 秒），下线延迟正常；调小 &lt;code&gt;healthCheckTimeout&lt;/code&gt; / 心跳间隔可加快，但会增加误判风险&lt;/li&gt;
&lt;li&gt;Distro 异步复制延迟：检查集群各节点时间是否同步（NTP），日志是否有 &lt;code&gt;distro&lt;/code&gt; 同步报错&lt;/li&gt;
&lt;li&gt;客户端是否优雅下线（&lt;code&gt;deregisterInstance&lt;/code&gt;）：kill -9 无法走注销流程，只能等心跳超时&lt;/li&gt;
&lt;li&gt;2.x 的 gRPC 连接是否被断开导致服务端感知不到客户端异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 配置变更不生效 / 未触发监听&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：改了配置，客户端 &lt;code&gt;receiveConfigInfo&lt;/code&gt; 没回调，或拿到的还是旧值&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;dataId、group、namespace 三者是否与服务端完全一致（最常见的&quot;改了别的环境的配置&quot;）&lt;/li&gt;
&lt;li&gt;客户端是否 &lt;code&gt;addListener&lt;/code&gt; 后才修改的配置（先监听再发布才有回调）&lt;/li&gt;
&lt;li&gt;长轮询线程是否被阻塞（服务端 30s 挂起超时被占用）——看服务端日志 &lt;code&gt;ConfigChangeNotifyTask&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;gRPC 半开连接：客户端收到推送但连接已断&lt;/li&gt;
&lt;li&gt;检查本地快照是否被手动修改（快照 md5 与服务端不同会导致一直走&quot;拉取&quot;分支的假象）&lt;/li&gt;
&lt;li&gt;灰度发布只对灰度 IP 生效，普通实例收不到是正常行为&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 心跳超时实例被剔除&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：实例被误剔除，或总是进 &lt;code&gt;不健康&lt;/code&gt; 列表&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;客户端进程 GC 停顿/主线程阻塞导致心跳发送延迟（临时实例 5s 心跳，连续 6 次未收到才剔除，GC 太久会触发）&lt;/li&gt;
&lt;li&gt;服务端与客户端时钟偏差过大&lt;/li&gt;
&lt;li&gt;服务端负载高、健康检查线程池打满，处理心跳慢——看 Nacos 日志 &lt;code&gt;health check&lt;/code&gt; 相关 warning&lt;/li&gt;
&lt;li&gt;网络抖动：Nacos 所在网络与业务网络之间丢包&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 集群数据不一致&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：不同节点查到的服务实例/配置不一样&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;临时实例走 Distro：检查节点间 8848（HTTP）/9848（gRPC）互通，&lt;code&gt;distro&lt;/code&gt; 同步任务是否报错&lt;/li&gt;
&lt;li&gt;配置走 Raft：检查是否选了 Leader（&lt;code&gt;nacos_raft&lt;/code&gt; 相关日志），Leader 挂后是否正常重新选举&lt;/li&gt;
&lt;li&gt;检查各节点配置 &lt;code&gt;cluster.conf&lt;/code&gt; 是否一致、节点间时钟同步&lt;/li&gt;
&lt;li&gt;持久实例走 Raft，如果发现不一致，优先看 Raft 日志是否有 snapshot 恢复失败&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. Derby 锁问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：控制台/服务端日志报 &lt;code&gt;Table &apos;xxx&apos; is locked&lt;/code&gt;、&lt;code&gt;Unable to obtain lock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;原因：1.x 集群多节点共享 Derby&lt;/li&gt;
&lt;li&gt;处理：迁移到 MySQL（执行 &lt;code&gt;conf/mysql-schema.sql&lt;/code&gt;），或升级 2.x&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 保护阈值触发导致无可用实例&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：突然大量调用失败，但服务列表里实例&quot;看起来都在&quot;&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;查看服务详情页的&lt;strong&gt;保护阈值&lt;/strong&gt;设置与健康实例比例&lt;/li&gt;
&lt;li&gt;保护触发后不健康实例也会被返回，调用方需自行做失败重试/熔断&lt;/li&gt;
&lt;li&gt;阈值设置过高（如 0.9）会在正常波动时误触发，建议结合健康实例数合理设置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. 灰度发布失效&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：灰度发布后，目标 IP 没收到灰度配置，或全量实例都收到灰度配置&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;灰度发布基于 IP 列表匹配，确认服务端拿到的客户端 IP 是否正确（经过代理/网关后取到的是代理 IP）&lt;/li&gt;
&lt;li&gt;灰度配置只对临时配置生效，检查是否误把配置改成正式发布&lt;/li&gt;
&lt;li&gt;灰度期间又做了正式发布，灰度信息会被覆盖清除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8. 与 Spring Cloud Alibaba 集成问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;No Feign Client for loadBalancing defined&lt;/code&gt;、&lt;code&gt;Config not found&lt;/code&gt;、&lt;code&gt;NacosException: Client not connected&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;版本兼容：Spring Boot / Spring Cloud / Spring Cloud Alibaba 版本必须按官方对应矩阵（如 Spring Cloud 2021.x 配 Alibaba 2021.0.x）&lt;/li&gt;
&lt;li&gt;连接失败：检查 &lt;code&gt;nacos.config.server-addr&lt;/code&gt;、&lt;code&gt;nacos.discovery.server-addr&lt;/code&gt; 是否正确，2.x 需放通 9848/9849 gRPC 端口&lt;/li&gt;
&lt;li&gt;配置优先级：&lt;code&gt;bootstrap.yml&lt;/code&gt; 中的配置要早于业务配置，&lt;code&gt;spring.cloud.nacos.config&lt;/code&gt; 前缀不要拼错&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NacosException: Client not connected&lt;/code&gt;：多半是 gRPC 长连接被断，检查防火墙/负载均衡对长连接的空闲断开策略&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;架构上记住&quot;命名空间/分组/服务三层隔离，Server AP+CP 双模型，Client 走 gRPC&quot;&lt;/li&gt;
&lt;li&gt;服务发现记住&quot;临时实例心跳上报（AP），持久实例服务端探测（CP），保护阈值兜底&quot;&lt;/li&gt;
&lt;li&gt;配置中心记住&quot;长轮询 + md5 比对 + Listener 回调 + 本地快照容灾&quot;&lt;/li&gt;
&lt;li&gt;集群记住&quot;配置 Raft 强一致，临时实例 Distro 最终一致，生产用 MySQL 持久化&quot;&lt;/li&gt;
&lt;li&gt;排查问题的第一直觉：&lt;strong&gt;先看 gRPC 连接是否正常，再看 dataId/group/namespace 是否对齐，最后看 AP/CP 同步日志&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Redis 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/redis-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/redis-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文总结 Redis 的核心底层原理：数据结构实现、单线程模型、持久化机制、过期与淘汰策略、缓存三大问题、高可用方案以及常见问题排查要点，作为个人技术笔记。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;五种基础数据结构与底层实现&lt;/h2&gt;
&lt;h3&gt;string：SDS&lt;/h3&gt;
&lt;p&gt;string 底层是 &lt;strong&gt;SDS（Simple Dynamic String）&lt;/strong&gt;，而非 C 字符串：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;带长度字段，获取长度 O(1)&lt;/li&gt;
&lt;li&gt;预分配空间 + 惰性释放，减少内存重分配次数&lt;/li&gt;
&lt;li&gt;二进制安全，可存任意二进制数据&lt;/li&gt;
&lt;li&gt;3.2+ 分 &lt;code&gt;sdshdr5/8/16/32/64&lt;/code&gt; 按长度分级，节省内存&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;list：双向链表与 quicklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;3.2 之前：元素少且小时用 &lt;strong&gt;ziplist（压缩列表）&lt;/strong&gt;，否则用双向链表&lt;/li&gt;
&lt;li&gt;3.2+ 统一为 &lt;strong&gt;quicklist&lt;/strong&gt;：以 ziplist 为节点组成的双向链表，兼顾内存紧凑与读写效率&lt;/li&gt;
&lt;li&gt;7.0 之后引入 &lt;strong&gt;listpack&lt;/strong&gt; 逐步替代 ziplist（解决连锁更新问题）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
quicklist 本质上就是&quot;链表 + 压缩块&quot;，通过 &lt;code&gt;list-max-ziplist-size&lt;/code&gt; 控制每个节点的大小。
:::&lt;/p&gt;
&lt;h3&gt;hash：哈希表与 ziplist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;元素少且小时：ziplist&lt;/li&gt;
&lt;li&gt;元素多时：&lt;strong&gt;hashtable&lt;/strong&gt;，rehash 采用&lt;strong&gt;渐进式 rehash&lt;/strong&gt;（分多次搬迁，避免一次性阻塞）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;set：整数集合与哈希表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;全部为整数且数量少时：&lt;strong&gt;intset（整数集合）&lt;/strong&gt;，有序紧凑存储，查找二分&lt;/li&gt;
&lt;li&gt;否则：hashtable（值为 null）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;zset：跳表 + 哈希表&lt;/h3&gt;
&lt;p&gt;zset 底层是&lt;strong&gt;跳表（skiplist） + 哈希表&lt;/strong&gt;的组合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哈希表存 member -&amp;gt; score，O(1) 取分数&lt;/li&gt;
&lt;li&gt;跳表按 score 排序，支持范围查询、排名（zrange/zrank）O(logN)&lt;/li&gt;
&lt;li&gt;元素少时也用 ziplist/listpack 压缩存储&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
跳表相比平衡树实现简单、区间遍历友好，Redis 作者因此选跳表实现有序集合。
:::&lt;/p&gt;
&lt;h2&gt;单线程模型与 IO 多路复用&lt;/h2&gt;
&lt;h3&gt;为什么单线程还快&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;基于内存操作，瓶颈不在 CPU&lt;/li&gt;
&lt;li&gt;核心是 &lt;strong&gt;IO 多路复用（epoll）&lt;/strong&gt;：单线程注册多个 socket 事件，有事件才处理，无事件阻塞，避免线程切换与锁竞争&lt;/li&gt;
&lt;li&gt;数据结构高效，命令执行本身微秒级&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事件循环流程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;epoll_wait 等待事件 → 读取命令 → 执行命令（单线程） → 写回响应
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Redis 6.0 多线程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;6.0 引入&lt;strong&gt;多线程 IO&lt;/strong&gt;：网络读写（accept/read/write）由多个线程并行处理，命令执行仍是单线程&lt;/li&gt;
&lt;li&gt;解决的是大流量下网络 IO 成为瓶颈的问题，而非 CPU 计算瓶颈&lt;/li&gt;
&lt;li&gt;默认关闭，通过 &lt;code&gt;io-threads&lt;/code&gt; 开启（建议 4 核以下不开启）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
Redis 单线程意味着一个慢命令会阻塞所有客户端，生产环境禁止大 key 操作、&lt;code&gt;keys *&lt;/code&gt;、&lt;code&gt;hgetall&lt;/code&gt; 大 hash、&lt;code&gt;lrange&lt;/code&gt; 大 list。
:::&lt;/p&gt;
&lt;h2&gt;持久化&lt;/h2&gt;
&lt;h3&gt;RDB 快照&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;全量快照：&lt;code&gt;save&lt;/code&gt;（同步阻塞）/ &lt;code&gt;bgsave&lt;/code&gt;（fork 子进程后台生成 dump.rdb）&lt;/li&gt;
&lt;li&gt;触发条件：&lt;code&gt;save 900 1 300 10 60 10000&lt;/code&gt; 等配置，或 &lt;code&gt;shutdown&lt;/code&gt;、主从全量同步&lt;/li&gt;
&lt;li&gt;优点：文件紧凑、加载快、适合备份；缺点：两次快照之间数据会丢&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AOF 日志&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;追加写命令，三种写回策略（appendfsync）：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;策略&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;数据安全性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;always&lt;/td&gt;
&lt;td&gt;每次写命令 fsync 磁盘&lt;/td&gt;
&lt;td&gt;最安全，性能最差&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;everysec&lt;/td&gt;
&lt;td&gt;每秒 fsync（默认）&lt;/td&gt;
&lt;td&gt;最多丢 1 秒数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;交给操作系统刷盘&lt;/td&gt;
&lt;td&gt;可能丢较多数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;AOF 文件膨胀后触发 &lt;code&gt;bgrewriteaof&lt;/code&gt; 重写压缩&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;混合持久化（4.0+）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aof-use-rdb-preamble yes&lt;/code&gt;：AOF 重写时先写 RDB 格式的全量数据，再追加增量命令&lt;/li&gt;
&lt;li&gt;优点：加载快（RDB 部分直接加载）+ 丢失数据少&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;fork 与 copy-on-write&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bgsave&lt;/code&gt;/&lt;code&gt;bgrewriteaof&lt;/code&gt; 都依赖 &lt;strong&gt;fork + COW&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fork 创建子进程，子进程共享父进程内存页表&lt;/li&gt;
&lt;li&gt;主进程继续写时触发写时复制，被修改的页复制一份&lt;/li&gt;
&lt;li&gt;内存越大、写越频繁，fork 耗时与 COW 内存开销越大&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
内存过大（如超过物理内存一半）时 bgsave 可能因 COW 触发 OOM；fork 耗时在 1ms 以上需要关注。
:::&lt;/p&gt;
&lt;h2&gt;过期删除与内存淘汰&lt;/h2&gt;
&lt;h3&gt;过期删除策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;惰性删除&lt;/strong&gt;：访问 key 时才检查是否过期，过期则删。节省 CPU，但过期 key 堆积占内存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定期删除&lt;/strong&gt;：每秒采样删除部分过期 key（默认 10 次/秒，每次取 20 个样本）&lt;/li&gt;
&lt;li&gt;两者配合使用，平衡 CPU 与内存&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;内存淘汰策略（8 种）&lt;/h3&gt;
&lt;p&gt;达到 &lt;code&gt;maxmemory&lt;/code&gt; 时触发，8 种策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;noeviction        不淘汰，写报错（默认）
allkeys-lru       所有 key 按 LRU 淘汰
volatile-lru      只淘汰设置了过期时间的 LRU
allkeys-random    所有 key 随机淘汰
volatile-random   过期 key 随机淘汰
volatile-ttl      过期 key 中 TTL 最短的优先淘汰
allkeys-lfu       所有 key 按 LFU 淘汰（4.0+）
volatile-lfu      过期 key 按 LFU 淘汰（4.0+）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
缓存场景建议 &lt;code&gt;allkeys-lru&lt;/code&gt;；需要区分热点与非热点的用 &lt;code&gt;allkeys-lfu&lt;/code&gt;；不能丢数据的业务绝不依赖淘汰兜底。
:::&lt;/p&gt;
&lt;h2&gt;缓存三大问题与解决方案&lt;/h2&gt;
&lt;h3&gt;缓存穿透&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：查询一个不存在的 key，缓存无数据，每次打到 DB。&lt;/p&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存空值（key 不存在也缓存空串 + 短过期时间）&lt;/li&gt;
&lt;li&gt;布隆过滤器（Bloom Filter）：前置拦截不存在的 key&lt;/li&gt;
&lt;li&gt;接口层参数校验（如 id 非法的直接拒绝）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存击穿&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：某个&lt;strong&gt;热点 key 过期&lt;/strong&gt;的瞬间，大量请求同时打到 DB。&lt;/p&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互斥锁（分布式锁）：重建缓存的请求加锁，其余等待&lt;/li&gt;
&lt;li&gt;逻辑过期：key 永不过期，后台线程异步刷新&lt;/li&gt;
&lt;li&gt;热点 key 过期时间加随机值错开&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存雪崩&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：大量 key 同一时间过期，或 Redis 宕机，DB 被打垮。&lt;/p&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;过期时间加随机抖动：&lt;code&gt;setex key random(60~120s)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;多级缓存（本地缓存 Caffeine + Redis）&lt;/li&gt;
&lt;li&gt;Redis 高可用（哨兵/集群）+ 限流降级兜底&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
缓存击穿是&quot;单个热点 key&quot;，雪崩是&quot;大面积 key 同时失效&quot;，定位时要区分清楚。
:::&lt;/p&gt;
&lt;h2&gt;高可用架构&lt;/h2&gt;
&lt;h3&gt;主从复制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;全量同步：从节点发 &lt;code&gt;psync ? -1&lt;/code&gt;，主节点 bgsave 生成 RDB 传给从节点，期间写入记录在 repl backlog 中，同步完成后补发增量&lt;/li&gt;
&lt;li&gt;增量同步：断线重连后基于偏移量从 backlog 增量同步&lt;/li&gt;
&lt;li&gt;从节点默认只读，&lt;code&gt;slaveof&lt;/code&gt; 指定主从关系&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;哨兵 Sentinel&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;作用：监控主从节点、自动故障转移、通知客户端&lt;/li&gt;
&lt;li&gt;判定：主观下线（单个哨兵超时）→ 客观下线（多数哨兵确认）→ 选举新主（从节点优先级、复制偏移量、run_id）&lt;/li&gt;
&lt;li&gt;哨兵自身集群部署，至少 3 个实例&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;集群 Cluster&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;16384 个槽位&lt;/strong&gt;，key 通过 &lt;code&gt;CRC16(key) % 16384&lt;/code&gt; 决定归属节点&lt;/li&gt;
&lt;li&gt;客户端访问错节点时返回 &lt;strong&gt;MOVED 重定向&lt;/strong&gt;（槽位迁移中返回 ASK），客户端需处理重定向&lt;/li&gt;
&lt;li&gt;数据分片 + 主从副本，&lt;code&gt;cluster meet&lt;/code&gt; 组建集群&lt;/li&gt;
&lt;li&gt;槽位迁移、多 key 操作（mget）跨槽不支持&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Redis 分布式锁&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;SET lock_key token NX PX 30000   # 加锁：原子设置，带过期时间
DEL lock_key                     # 解锁：需校验 token（Lua 脚本保证原子）
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;红锁（RedLock）：多节点加锁，过半成功才算成功，用于强一致场景&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redisson 看门狗&lt;/strong&gt;：默认 30s 锁超时，后台定时续期（默认每 10s 续一次），业务未完成时防止锁被自动释放；宕机时看门狗停更，锁自然过期释放&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Redis 分布式锁适合大多数业务，但要清楚它基于 AP 架构；对一致性要求极高的场景考虑 ZooKeeper 锁。
:::&lt;/p&gt;
&lt;h2&gt;常规问题排查&lt;/h2&gt;
&lt;h3&gt;内存暴涨&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redis-cli --bigkeys&lt;/code&gt; 扫描大 key（大 hash/zset/string）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory usage key&lt;/code&gt; 查看单个 key 内存&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info memory&lt;/code&gt; 查看 used_memory、fragmentation 等指标&lt;/li&gt;
&lt;li&gt;常见原因：无过期时间的 key 堆积、大 value、list 无限追加（消息堆积）、内存碎片（&lt;code&gt;activedefrag yes&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;慢查询&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;slowlog get 10&lt;/code&gt; 查看慢命令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;slowlog-log-slower-than&lt;/code&gt; 阈值配置（默认 10000 微秒）&lt;/li&gt;
&lt;li&gt;定位后优化：拆小 key、避免 O(N) 命令（&lt;code&gt;hgetall&lt;/code&gt;/&lt;code&gt;lrange&lt;/code&gt; 大范围）、&lt;code&gt;keys&lt;/code&gt; 换 &lt;code&gt;scan&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CPU 高 / Hot Key&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hotkeys&lt;/code&gt; 参数开启后 &lt;code&gt;redis-cli --hotkeys&lt;/code&gt; 检测（4.0+）&lt;/li&gt;
&lt;li&gt;缓解：本地缓存热 key、读写分离（从节点分担读）、key 打散（&lt;code&gt;key#1&lt;/code&gt;~&lt;code&gt;key#N&lt;/code&gt; 分片到不同节点）&lt;/li&gt;
&lt;li&gt;注意：高频小 key 的 CPU 消耗主要在协议解析与内存分配，可评估 &lt;code&gt;io-threads&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据不一致（双写一致性）&lt;/h3&gt;
&lt;p&gt;常见方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache Aside&lt;/strong&gt;：先更新 DB，再删缓存；删缓存失败用重试/订阅 binlog（Canal）+ MQ 补偿&lt;/li&gt;
&lt;li&gt;保证最终一致：更新 DB 成功 → 延迟双删（先删缓存，延迟 500ms 再删一次）&lt;/li&gt;
&lt;li&gt;强一致场景：读写都走 DB 或分布式事务，缓存只做加速&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
&quot;先删缓存再更新 DB&quot;在并发读下极易产生脏数据，生产应统一&quot;先更 DB 再删缓存&quot;。
:::&lt;/p&gt;
&lt;h3&gt;持久化阻塞与延迟抖动&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;大实例 bgsave fork 阻塞主线程：控制单实例内存（建议 10GB 内），或换用物理机大内存 + &lt;code&gt;fork&lt;/code&gt; 前避免大写入&lt;/li&gt;
&lt;li&gt;AOF &lt;code&gt;always&lt;/code&gt; 策略磁盘慢导致抖动：评估改 &lt;code&gt;everysec&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;触发场景排查：&lt;code&gt;info stats&lt;/code&gt; 看 &lt;code&gt;latest_fork_usec&lt;/code&gt;，检查是否有频繁 bgsave（如主从全量重连）&lt;/li&gt;
&lt;li&gt;内存碎片导致延迟：&lt;code&gt;info memory&lt;/code&gt; 看 &lt;code&gt;mem_fragmentation_ratio &amp;gt; 1.5&lt;/code&gt; 时开启 &lt;code&gt;activedefrag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;网络抖动：客户端连接池 + 合理的超时重试策略&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;底层数据结构都是&quot;小数据压缩、大数据换结构&quot;的策略（ziplist/quicklist/intset → 跳表/哈希表）&lt;/li&gt;
&lt;li&gt;单线程 + epoll 是高吞吐的根基，任何 O(N) 操作都是性能雷区&lt;/li&gt;
&lt;li&gt;持久化、过期淘汰、缓存问题、高可用本质都是对&quot;内存、磁盘、网络&quot;三者的权衡&lt;/li&gt;
&lt;li&gt;排查问题的核心抓手：&lt;code&gt;info&lt;/code&gt;、&lt;code&gt;slowlog&lt;/code&gt;、&lt;code&gt;--bigkeys/--hotkeys&lt;/code&gt;、&lt;code&gt;memory usage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>深度好文：为何穷人完不成资本积累？</title><link>https://chaggle.github.io/posts/2026/08/09/repost-poor-capital-accumulation/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/repost-poor-capital-accumulation/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
&lt;strong&gt;转载声明&lt;/strong&gt;：本文转载自微信公众号「猫视青年」（经世致用，知行合一！），版权归原作者所有，已注明作者与原文链接。
原文链接：https://mp.weixin.qq.com/s/Pw_KqD-TTQ-6IbSbWUzS1w
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;穷人完不成资本积累，是因为穷人不懂投资，还是社会结构陷阱，阻挡自下而上通道。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;（一）穷人思维&lt;/h2&gt;
&lt;p&gt;这是穷人完不成资本积累表象，最典型说法就是一道算术题：穷人月入五千，每月雷打不动存一千，十年高度自律下来也就存 12 万；但富人家里给 200 万本金，什么都不用干，就放银行吃 2% 利息，一年就有 4 万。也就是说穷人拼命十年攒下来的钱，富人三年就躺出来了，这就是投资复利魅力。&lt;/p&gt;
&lt;p&gt;同时穷人喜欢用花呗分期、借呗消费贷和信用卡最低还款，这些东西本质上是复利债务。你欠一万块，年化 15%，三年连本带利滚到一万五；但富人却把同样一万块丢进年化 2% 银行理财里，三年后变成一万零六百。也就是说穷人在付复利，富人在收复利——穷人越借钱越少，富人越存钱越多。&lt;/p&gt;
&lt;p&gt;更可怕的是穷人没钱做体检，小毛病扛成大毛病，进一次医院花掉半年积蓄；没钱买质量好的东西，便宜货用坏了再买，总账算下来反而更贵。穷人每年花 10 块买一双便宜靴子，富人花 50 块买一双昂贵靴子穿十年，十年下来穷人花了 100 块，富人只花了 50 块，这就是经典靴子投资理论。&lt;/p&gt;
&lt;p&gt;再者穷人为了省钱，往往选择住便宜郊区房子，每天通勤三个小时；但富人却选择住在市中心，每天通勤只要半小时。两者一天相差两个半小时，一个月就相差 75 个小时，一年就相差 900 个小时——足够穷人从零开始学一门能变现的技能，但穷人却把这 900 小时浪费在地铁刷视频上。富人喜欢用钱买时间，叫人保洁和跑腿，再用买来的时间想怎么赚更多钱。也就是说穷人时间越用越没钱，富人时间越用越有钱。&lt;/p&gt;
&lt;p&gt;最后穷人身边都是打工的，你能接触到的赚钱方式就是打工。你不知道有人靠做跨境电商一年赚一套房，也不知道有人靠炒房成亿万富翁，更不知道有人靠炒股成百亿富翁。穷人饭局聊的都是超市打折和加班费，富人饭局聊的是行业红利、政策变动和投资机会——穷人越社交越没钱，富人越社交越有钱。&lt;/p&gt;
&lt;p&gt;更残忍的是穷人资本积累难以达到逃逸速度。就像火箭发射速度不够，就飞不出地球引力，永远在大气层打转那样，穷人资本积累总是在某个临界点之下：收入被生活成本吃掉大头，少的可怜积蓄趴在银行跑不过通胀，还经常被意外周期性清零——一场大病、小孩出生、升学教育、结婚成家、买车买房，导致穷人一直很努力，却永远在贫困状态里打转，不存在慢慢积累就能逃离贫困的可能。&lt;/p&gt;
&lt;p&gt;上述这些都是富人给我们灌输的&quot;穷人完不成资本积累&quot;的理由：他们认为穷人没有资本，所以没法实现投资复利收益，甚至借钱支付复利变得更穷；因为没钱喜欢买便宜货却花费更多；因为没钱贱卖时间变得更穷；穷人社交圈子缺少赚大钱机会，以致资本积累难以达到脱离贫困圈的逃逸速度。&lt;/p&gt;
&lt;p&gt;所以富人建议就是改造穷人思维：一方面要求穷人苦行僧禁欲，不消费、不结婚和不生娃，这样才能实现原始资本积累，开启投资复利越来越有钱；另一方面又要求穷人消费升级，买昂贵奢侈品、买车买房和买时间，这样才能向上打入富人社交圈，赚到更多钱达到脱离贫困状态的逃逸速度。&lt;/p&gt;
&lt;p&gt;于是我们穷人就陷入无尽自我改造内耗：明明按照富人要求节俭不婚不育，但还是没有原始资本的穷人；明明按照富人要求消费升级，甚至跪舔式向上社交，却还是没赚到大钱，永远都是富人打趣的土包子。&lt;/p&gt;
&lt;p&gt;因为富人让穷人节俭不婚不育，就是让穷人省下来的那点钱，专门去买富人想卖的昂贵商品；同时富人认为社交都需要利益等价交换，他们鼓励穷人向上社交，满足富人高高在上的情绪价值后，并不会给穷人提供赚钱机会——这就是富人改造穷人思维的真相。&lt;/p&gt;
&lt;h2&gt;（二）社会陷阱&lt;/h2&gt;
&lt;p&gt;这是穷人完不成资本积累的内核。首先来看最正确的资本积累概念，就得引用马克思在 150 年前给出的答案：资本积累以剩余价值为前提，剩余价值以资本主义生产为前提，资本主义生产又以大量资本和劳动力在商品生产者中为前提。直白说就是你有资本才能赚钱，赚钱才能积累资本，但关键是原始资本从哪来。&lt;/p&gt;
&lt;p&gt;马克思给出的答案是：原始资本不是勤俭节约的结果，而是暴力结果。原文写的是&quot;征服、奴役、劫掠、杀戮，用血与火文的文字写在人类编年史中&quot;。如西方国家原始资本既有西班牙和葡萄牙殖民美洲抢夺金银，也有英国通过圈地运动抢夺农民土地；苏联原始资本则是通过集体农庄抽取农业剩余价值。&lt;/p&gt;
&lt;p&gt;假如觉得上升到国家高度不妥，那就下拉到个人层面思考问题：底层个体为何无法实现正向净资产积累、无法跨越技能投资门槛、无法以非掠夺性条件获得信贷？这三种失败其实根源于四个维度的系统性剥夺，并且这四个维度相互耦合成无法打破的社会结构陷阱。&lt;/p&gt;
&lt;p&gt;**首先是空间不稳定，保不住任何东西。**有研究发现穷人租房驱逐产生贫困：一次驱逐导致社区网络断裂，子女被迫转学，通勤成本跳升，押金搬迁费用击穿资金缓冲。空间剥夺的影响超出经济层面——反复经历驱逐的人不再把家理解成稳定、可以投射未来的锚点，而是理解成随时能被收回的临时场所。这种观念改变压缩个人规划视野，使得任何&quot;先投入后回报&quot;的行为在主观上都无法想象。所以穷人不做长期规划，并不是穷人认知缺陷，而是对&quot;保不住任何东西&quot;环境的准确认知。&lt;/p&gt;
&lt;p&gt;**其次是时间碎片化，阻止改善长期处境的技能投资。**人力资本投资需要连续可提前规划的时间块，零散碎片时间无论总量多大都无法胜任。富人显得更善于规划未来，是因为他们生活条件允许他们反复练习&quot;设定目标-投入资源-等待回报-调整策略&quot;这个循环，并且每次成功循环都强化他们对未来的信心和操控感。穷人被剥夺的恰恰是这种机会：无法预测下周时间安排，也就无法设定目标，从而无法投入资源培养技能，自然也就无法改变长期处境。&lt;/p&gt;
&lt;p&gt;**再者是贫困溢价，让穷人支付的单价系统高于富人。**当资金缓冲接近零时，每个人就会失去选择&quot;短期成本更高但长期更便宜&quot;的能力：你知道年付车险更便宜，但账上没有一次付清的钱只能月付；还清信用卡就不用付利息，但账上只有最低还款的钱只能分期。富人谈复利不是在帮穷人，更像是表演一种精致的残忍。同时在资金缓冲为零的极端条件下，拒绝承担风险就是最优策略——穷人不是没有投资认知，而是一次失败就能击穿生存底线，防守只顾眼前生存的行为是穷人容错率为零环境下的最优解。&lt;/p&gt;
&lt;p&gt;**最后是信贷杠杆变成穷人陷阱。**富人经济学家认为穷人无法积累资本，是因为他们资产缺乏正式法律产权，无法作为抵押品进入金融系统。但实际上&quot;能进入信贷市场&quot;和&quot;能从信贷市场获益&quot;是两回事，其中隔着一组关键制度条件：利率上限、信息透明度要求、违约救济程序和反掠夺性贷款。穷人只能借到年化 24% 的高利贷信用贷款，投资不透明 P2P 违约只能拿回打折本金，买到烂尾楼还要还银行贷款……这些都表明信贷杠杆对有资金缓冲的富人是工具，对没有资金缓冲的穷人是陷阱。&lt;/p&gt;
&lt;p&gt;所以穷人：生存空间不稳定消耗资金缓冲，让穷人更依赖即时现金流；而提供即时现金流的底层劳动力市场，尤其是临工经济和服务业，又剥夺穷人时间连续可规划性，阻止穷人任何可能改善长期处境的技能投资；从而让穷人资金缓冲始终处于趋近于零的状态，就需要支付更高商品服务单价，甚至陷入信贷杠杆陷阱——从而越来越穷。&lt;/p&gt;
&lt;h2&gt;（三）走向何方&lt;/h2&gt;
&lt;p&gt;这是穷人需要思考的问题。我们不能被富人预设的&quot;改造穷人思维&quot;左右：单纯以为勤俭节约，甚至不婚不育，就能拿到原始资本；也不能幻想消费升级，就能接触富人圈层从而赚到大钱，并且依靠复利投资就能达到脱离贫困状态的逃逸速度。&lt;/p&gt;
&lt;p&gt;我们更需要思考三个问题：如何实现正向净资产积累？如何跨越技能投资门槛？如何从信贷市场获益？那就是要有一个长期稳定的生存空间，拥有一份长期稳定的工作，打造出充裕的资金缓冲——就能降低支付商品服务单价，跨越技能投资门槛，实现正向净资产积累，更能让信贷杠杆成为造富工具，而不是加剧贫困陷阱。&lt;/p&gt;
&lt;p&gt;我们更要明白：富人传输禁欲积累资本学，不是帮助穷人致富，而是把社会问题转化成穷人苦修问题。穷人不应该以放弃正常人类生活为代价，去换取一个社会结构层面已被堵死的资本积累机会；更不能把太多能量消耗在更拼命打工、更极端压缩消费和更焦虑向上社交等自我改造上。而是要去了解社会运行逻辑，让自己处在社会运行的有利位置，避免踏进社会运行陷阱出不来——远比盲目自我感动式努力重要得多。&lt;/p&gt;
&lt;p&gt;:::note
本文已按 CC BY-NC-SA 协议转载，作者「猫视青年」，原文链接：https://mp.weixin.qq.com/s/Pw_KqD-TTQ-6IbSbWUzS1w
:::&lt;/p&gt;
</content:encoded></item><item><title>RocketMQ 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/rocketmq-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/rocketmq-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是我学习 RocketMQ 时整理的笔记，围绕四大组件、存储与高可用架构、消息模型，以及线上常见的堆积、丢失、顺序与事务问题展开，适合面试复习和日常排查参考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心架构&lt;/h2&gt;
&lt;p&gt;RocketMQ 集群由四大组件构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NameServer&lt;/strong&gt;：轻量级路由中心，维护 Topic 与 Broker 的路由信息，Broker 启动时注册、断开时剔除，不存储消息数据，&lt;strong&gt;无状态&lt;/strong&gt;，可多台部署互为冗余。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broker&lt;/strong&gt;：消息存储与转发节点，负责接收生产者消息、落盘、向消费者推送/拉取消息，每个 Broker 保存全部 Topic 的部分队列数据。多 Broker 组成主从结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Producer&lt;/strong&gt;：消息生产者，从 NameServer 拉取路由信息后选择队列发送，支持失败重试、延迟消息、事务消息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumer&lt;/strong&gt;：消息消费者，分为 Push 与 Pull 两种模式，从 Broker 拉取消息消费。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;路由注册与心跳机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Broker 启动后&lt;strong&gt;每 30 秒&lt;/strong&gt;向所有 NameServer 发送心跳（注册自身信息与 Topic 路由表），NameServer 若 &lt;strong&gt;10 秒&lt;/strong&gt;收不到心跳，则将该 Broker 标记为不可用。&lt;/li&gt;
&lt;li&gt;Producer / Consumer 在发送/消费前先向 NameServer 拉取路由（默认每 30 秒定时更新），因此 &lt;strong&gt;NameServer 与客户端之间的路由不是实时的&lt;/strong&gt;，Broker 宕机后客户端最多需要 30 秒左右感知。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
NameServer 之间互不通信，各自独立保存全量路由，Broker 向所有 NameServer 同时注册，保证单台 NameServer 故障不影响集群路由发现。
:::&lt;/p&gt;
&lt;h2&gt;存储原理&lt;/h2&gt;
&lt;h3&gt;CommitLog&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;所有 Topic 的消息&lt;strong&gt;共享写入一个文件&lt;/strong&gt;：&lt;code&gt;CommitLog&lt;/code&gt;，按顺序追加，文件默认 1GB（&lt;code&gt;mapedFileSizeCommitLog&lt;/code&gt;），写满后新建文件，文件名即起始物理偏移量。&lt;/li&gt;
&lt;li&gt;顺序写 + 内存映射（mmap）是 RocketMQ 高吞吐的基础：所有消息统一进 CommitLog，彻底避免了多 Topic 各自随机写文件带来的性能损耗。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
&quot;所有消息写一个文件&quot;是 RocketMQ 与 Kafka（按分区文件）最大的存储差异：RocketMQ 牺牲了一点查询灵活性，换取了极致的顺序写性能与极低的文件数。
:::&lt;/p&gt;
&lt;h3&gt;ConsumeQueue&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个 Topic 的每个 Queue 对应一组 &lt;code&gt;ConsumeQueue&lt;/code&gt; 文件，记录&lt;strong&gt;消息在 CommitLog 中的偏移量、长度、tag hash&lt;/strong&gt; 等索引信息，按顺序递增。&lt;/li&gt;
&lt;li&gt;消费时先查 ConsumeQueue 拿到物理偏移，再去 CommitLog 读取真实消息，实现&quot;索引与数据分离&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;IndexFile&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;按 key 或统一时间戳查询消息时使用 &lt;code&gt;IndexFile&lt;/code&gt;（哈希索引），支持&quot;按 key 精确查&quot;与&quot;按时间范围查&quot;，主要用于业务排查与运维工具（如 &lt;code&gt;mqadmin queryMsgByKey&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;页缓存与 mmap&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Broker 写入与读取都基于 &lt;code&gt;FileChannel&lt;/code&gt; + &lt;code&gt;mmap&lt;/code&gt;（MappedByteBuffer）读写，数据先进入&lt;strong&gt;页缓存（Page Cache）&lt;/strong&gt;，由操作系统统一管理刷盘，命中缓存时读写极快。&lt;/li&gt;
&lt;li&gt;读取路径：先读 ConsumeQueue 索引（常驻页缓存），再读 CommitLog 数据，两者都尽量命中页缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;消息刷盘策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同步刷盘（SYNC_FLUSH）&lt;/strong&gt;：消息写入 CommitLog 并成功落盘后，才向生产者返回写入成功，可靠性最高，但吞吐下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步刷盘（ASYNC_FLUSH）&lt;/strong&gt;：写入页缓存即返回，由后台线程批量刷盘，吞吐高，机器断电可能丢少量消息（默认配置）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
同步/异步刷盘关注的是&quot;Broker 本地不丢&quot;，与主从复制关注点不同；生产环境单机数据可靠性要求高时用同步刷盘，重吞吐场景用异步刷盘 + 主从同步复制组合。
:::&lt;/p&gt;
&lt;h2&gt;消息模型&lt;/h2&gt;
&lt;h3&gt;Topic 与 Queue&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Topic&lt;/strong&gt;：业务逻辑上的消息类别，一个 Topic 可以分布在多个 Broker 上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queue（队列）&lt;/strong&gt;：Topic 的物理切分单元，每个 Broker 上每个 Topic 默认 4 个队列（&lt;code&gt;defaultTopicQueueNums&lt;/code&gt;），队列是消息顺序的最小保证单位，也是消费并行度的来源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;消费模式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;集群消费（Clustering）&lt;/strong&gt;：同一消费组内各消费者分摊队列，一条消息只被组内一个消费者消费（默认）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广播消费（Broadcasting）&lt;/strong&gt;：组内每个消费者都消费全部消息，适合&quot;每台机器都要收到&quot;的场景，如本地缓存刷新。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;消费组与负载均衡&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;同一消费组共享消费进度（offset 存在 Broker），Rebalance 时按&quot;消费者数 : 队列数&quot;平均分配队列，消费者数超过队列数则部分消费者空闲。&lt;/li&gt;
&lt;li&gt;消费组内消费者故障或新消费者加入时，会触发 Rebalance 重新分配队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Rebalance 造成队列从消费者 A 迁移到 B 时，B 从&quot;存储的 offset&quot;继续消费，因此可能出现重复消费；消费者数量建议与队列总数成倍数关系，避免分配不均。
:::&lt;/p&gt;
&lt;h3&gt;顺序消息&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;局部有序&lt;/strong&gt;：把需要有序的消息通过选择队列算法（如 MessageQueueSelector 按订单 ID 哈希）送入&lt;strong&gt;同一个队列&lt;/strong&gt;，该队列单线程顺序消费，即保证队列内严格有序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局有序&lt;/strong&gt;：一个 Topic 只有一个队列，吞吐受限，非必须不采用。&lt;/li&gt;
&lt;li&gt;消费端需避免并发/重试导致的乱序：用 MessageListenerOrderly（顺序消费会加锁并严格串行处理）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;事务消息（半消息 + 回查）&lt;/h3&gt;
&lt;p&gt;事务消息流程（两阶段）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Producer 发送&lt;strong&gt;半消息（half message）&lt;/strong&gt;，Broker 暂存，消费者不可见。&lt;/li&gt;
&lt;li&gt;执行本地事务，成功则提交事务消息，失败则回滚。&lt;/li&gt;
&lt;li&gt;若本地事务结果未上报（网络异常等），Broker 定时&lt;strong&gt;回查&lt;/strong&gt;（check）Producer 的本地事务状态，Producer 实现 &lt;code&gt;TransactionListener#checkLocalTransaction&lt;/code&gt; 返回提交/回滚。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::caution
事务消息解决的是&quot;本地数据库操作与发消息&quot;的一致性问题，但需要业务实现&lt;strong&gt;本地事务表或日志&lt;/strong&gt;来支撑回查，否则回查时无法判断业务是否成功，可能造成消息最终状态不确定。
:::&lt;/p&gt;
&lt;h3&gt;延迟消息&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;内置 18 个延迟级别（&lt;code&gt;messageDelayLevel&lt;/code&gt;：1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h），&lt;code&gt;setDelayTimeLevel(3)&lt;/code&gt; 即延迟 10 秒。&lt;/li&gt;
&lt;li&gt;实现原理：延迟消息写入&lt;strong&gt;专门的延迟队列（SCHEDULE_TOPIC_XXXX）&lt;/strong&gt;，定时任务扫描到期后重新投递到真实 Topic，到期时间只精确到秒级。&lt;/li&gt;
&lt;li&gt;支持自定义时间（5.x 新增）需配置时间跨度较大的 level，或使用定时消息（基于时间轮实现）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;高可用&lt;/h2&gt;
&lt;h3&gt;Broker 主从结构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个 Broker 分为 Master 与 Slave（可 1 主多从），同一 Broker 组的 Master/Slave 数据同步有&lt;strong&gt;同步复制&lt;/strong&gt;与&lt;strong&gt;异步复制&lt;/strong&gt;两种：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同步复制（SYNC_MASTER）&lt;/strong&gt;：主从都写入成功才返回，可靠性高，可用性略降（从挂掉会导致主不可用），Broker 主从切换后数据不丢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步复制（ASYNC_MASTER）&lt;/strong&gt;：主写入成功即返回，主挂掉时可能丢少量消息，但可用性好。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;主从切换：传统模式下需要运维介入切换（更新 namesrv 路由、重启）；5.x 引入 &lt;strong&gt;Controller 模式&lt;/strong&gt;（基于 DLedger/Raft 的自动切换）可实现自动选主。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
Broker 故障自动恢复优先级：同一 Broker 组内 Slave 可接管读请求（消费端优先读 Slave），Master 宕机后由 Controller（或运维）将 Slave 提升为 Master。
:::&lt;/p&gt;
&lt;h3&gt;NameServer 高可用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;NameServer 完全无状态，多台独立部署即高可用，Broker 向所有 NameServer 注册，客户端随机选一台即可获得全量路由。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
RocketMQ 的&quot;高可用&quot;依赖三层：NameServer 无状态多活（路由层）、Broker 主从复制（数据层）、消息消费的 offset 持久化（进度层），三层各自独立设计，故障面被隔离。
:::&lt;/p&gt;
&lt;h2&gt;常规问题排查&lt;/h2&gt;
&lt;h3&gt;消息堆积&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用控制台或 &lt;code&gt;mqadmin consumerProgress -g group&lt;/code&gt; 查看消费进度与堆积量。&lt;/li&gt;
&lt;li&gt;堆积原因：消费能力不足（并发线程数小、单条处理慢）、消费者宕机、Rebalance 后队列分配不均、Broker 读性能下降。&lt;/li&gt;
&lt;li&gt;处理：提高消费线程数、优化单条处理耗时、扩容消费者（注意队列数限制）、短期可临时增加队列并扩消费者，快速止血。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;消息丢失&lt;/h3&gt;
&lt;p&gt;排查点（按可靠性层级）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Producer 发送失败：&lt;code&gt;retryTimesWhenSendFailed&lt;/code&gt; 默认 2 次，确认是否重试足够。&lt;/li&gt;
&lt;li&gt;Broker 刷盘策略：异步刷盘 + 机器断电会丢数据，关键业务改同步刷盘。&lt;/li&gt;
&lt;li&gt;主从复制：异步复制下主挂会丢，改同步复制（SYNC_MASTER）。&lt;/li&gt;
&lt;li&gt;消费端：消费失败且重试耗尽会进入死信队列，若 DLQ 无人处理等于&quot;逻辑丢失&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;重复消费&lt;/h3&gt;
&lt;p&gt;重复消费不可避免（至少一次语义），常见原因与对策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消费成功但提交 offset 失败（客户端宕机/网络闪断）后重新消费。&lt;/li&gt;
&lt;li&gt;Rebalance 迁移队列导致重复。&lt;/li&gt;
&lt;li&gt;对策：消费逻辑幂等（唯一键去重、数据库主键防重、Redis setnx 去重）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;顺序错乱&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;原因 1：业务未保证同一业务键进同一队列（未用 MessageQueueSelector）。&lt;/li&gt;
&lt;li&gt;原因 2：使用了并发消费（MessageListenerConcurrently）。&lt;/li&gt;
&lt;li&gt;原因 3：消费失败重试时跳过失败消息继续消费后续消息。&lt;/li&gt;
&lt;li&gt;对策：selector 选队列 + MessageListenerOrderly 顺序消费；重试需暂停后续消费或把失败消息转死信由专门消费者处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;事务消息不回查 / 卡在半提交&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;确认 Producer 实现并注册了 &lt;code&gt;TransactionListener&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;回查间隔与次数有限（默认回查 15 次），回查失败会丢弃半消息，需在业务侧查&quot;本地事务表&quot;核对。&lt;/li&gt;
&lt;li&gt;排查：&lt;code&gt;mqadmin queryMsgById&lt;/code&gt; 查看半消息状态（Commit/ROLLBACK），配合监控半消息数量指标。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Broker 内存 / 磁盘告警&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;磁盘：CommitLog 与 ConsumeQueue 按保留时间（&lt;code&gt;fileReservedTime&lt;/code&gt;，默认 72 小时）清理；磁盘写满会导致 broker 停写，关注磁盘使用率告警并合理设置清理策略。&lt;/li&gt;
&lt;li&gt;内存：页缓存占用高是正常现象（RocketMQ 依赖页缓存），主要监控 JVM 堆内存与 GC；大量瞬时消息可能造成内存页缓存压力大，注意 &lt;code&gt;maxMessageSize&lt;/code&gt; 与流量规划。&lt;/li&gt;
&lt;li&gt;CPU 高：关注是否发送了大消息、消费端频繁 GC、刷盘线程打满。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;消费重试与死信队列（DLQ）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;消费失败默认&lt;strong&gt;重试 16 次&lt;/strong&gt;，间隔按 &lt;code&gt;%RETRY%&lt;/code&gt; 队列的延迟等级递增（10s → 5min 逐级增加）。&lt;/li&gt;
&lt;li&gt;重试耗尽后消息转入&lt;strong&gt;死信队列 &lt;code&gt;%DLQ%消费组名&lt;/code&gt;&lt;/strong&gt;，DLQ 默认只读，需要人工（或运维脚本）消费恢复。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
DLQ 是&quot;最后一道防线&quot;，必须配置告警与人工处理流程，否则消息静默丢失；恢复时注意按消息头里的原 Topic 信息重新投递并处理幂等。
:::&lt;/p&gt;
&lt;h3&gt;常用排查命令&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看消费进度与堆积
mqadmin consumerProgress -g &amp;lt;group&amp;gt; -n &amp;lt;namesrvAddr&amp;gt;

# 查看 Topic 路由与队列分布
mqadmin topicRoute -t &amp;lt;topic&amp;gt;

# 按 key 查询消息
mqadmin queryMsgByKey -t &amp;lt;topic&amp;gt; -k &amp;lt;key&amp;gt;

# 查看 broker 运行状态与连接数
mqadmin brokerStatus -b &amp;lt;brokerAddr&amp;gt;

# 查看/修改延迟级别配置
mqadmin updateBrokerConfig -b &amp;lt;brokerAddr&amp;gt; -c messageDelayLevel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
RocketMQ 排障思路总结：先看生产端（发送是否成功）→ 再查存储端（刷盘/复制/磁盘）→ 后看消费端（重试、DLQ、堆积），结合消息轨迹（5.x 支持全链路轨迹）可以快速定位每一跳的问题。
:::&lt;/p&gt;
</content:encoded></item><item><title>Spark 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/spark-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/spark-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文是对 Spark 核心知识的系统性总结，覆盖架构、RDD 与 DAG 调度、Shuffle、缓存与 Checkpoint、运行模式、常用参数以及日常问题排查要点，作为个人技术笔记使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;整体架构：一次任务是怎么跑起来的&lt;/h2&gt;
&lt;p&gt;Spark 是内存计算框架，采用 Master-Slave 架构，核心组件如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Driver&lt;/strong&gt;：任务的&quot;大脑&quot;，运行用户程序 main 函数，负责解析代码、构建 DAG、调度任务、分发 Task，同时持有 SparkContext&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executor&lt;/strong&gt;：实际执行计算的工作进程，运行在 Worker 节点上，负责执行 Task、存储 RDD 缓存数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Master&lt;/strong&gt;：集群的&quot;总管&quot;，负责资源管理，接收 Worker 心跳，将资源分配给 Application&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worker&lt;/strong&gt;：集群中的&quot;包工头&quot;，汇报资源给 Master，负责启动和监控本机的 Executor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cluster Manager&lt;/strong&gt;：外部资源调度器（Standalone / YARN / Mesos / Kubernetes），Spark 本身支持 Standalone 自带的 Master-Worker&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提交一个 Application 后的大致流程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spark-submit → 启动 Driver(SparkContext)
    → Driver 向 Cluster Manager 申请资源
    → Cluster Manager 通知 Worker 启动 Executor
    → Executor 反向注册给 Driver
    → Driver 把代码拆成 Job → Stage → Task 分发执行
    → Task 在 Executor 上执行，结果汇聚给 Driver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Spark 的内存计算优势在于：中间结果优先放内存，避免像 MapReduce 那样每步都落盘。但前提是 Executor 内存要够，否则退化为磁盘计算，性能反而不如 MR。
:::&lt;/p&gt;
&lt;h3&gt;Application / Job / Stage / Task 层次&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application&lt;/strong&gt;：一次 spark-submit 提交的任务，包含一个 Driver 和若干 Executor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Job&lt;/strong&gt;：一个 Action 算子（count、collect、saveAsTextFile 等）触发一个 Job，一个 Application 可以有多个 Job&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stage&lt;/strong&gt;：一个 Job 按宽依赖（Shuffle）划分成多个 Stage，窄依赖之间不切 Stage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task&lt;/strong&gt;：Stage 的最小执行单元，一个分区对应一个 Task，同一 Stage 的 Task 执行相同的代码片段&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Application
 └── Job1 (Action: count)
      ├── Stage0 (读 HDFS / 窄依赖算子)
      └── Stage1 (Shuffle → reduce)
           └── Task × N (每个分区一个 Task)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
记住：&lt;strong&gt;Action 触发 Job，宽依赖划分 Stage，分区数决定 Task 数&lt;/strong&gt;，这是 Spark 调优最核心的三个概念。
:::&lt;/p&gt;
&lt;h2&gt;RDD 与 DAG 调度原理&lt;/h2&gt;
&lt;h3&gt;RDD：弹性分布式数据集&lt;/h3&gt;
&lt;p&gt;RDD（Resilient Distributed Dataset）是只读、可分区的数据集合抽象，核心特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分区（Partition）&lt;/strong&gt;：数据被切成多块分布在不同节点，并行计算的粒度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;血缘（Lineage）&lt;/strong&gt;：记录 RDD 从父 RDD 派生的过程，是容错的基础&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;惰性求值（Lazy）&lt;/strong&gt;：转换算子（map、filter）只是记录血缘，不真正计算，遇到 Action 才触发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存（Cache）&lt;/strong&gt;：可以持久化到内存/磁盘，供多次复用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;宽依赖与窄依赖&lt;/h3&gt;
&lt;p&gt;依赖关系决定了容错成本和 Stage 划分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;窄依赖&lt;/strong&gt;：父 RDD 的每个分区只被一个子分区使用（map、filter、union、coalesce），计算在同一个 Stage 内流水线式执行，父分区丢失只需重算这一个分区&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宽依赖&lt;/strong&gt;：父 RDD 的一个分区被多个子分区使用（groupByKey、reduceByKey、join），必然产生 Shuffle，子分区计算必须等父分区全部算完，父分区丢失需要整条血缘重算&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
宽依赖是 Shuffle 的唯一来源，也是性能瓶颈和节点宕机风险的重灾区。能用 map-side 预聚合的算子（reduceByKey、aggregateByKey）就不要用 groupByKey。
:::&lt;/p&gt;
&lt;h3&gt;DAG 调度与 Stage 划分&lt;/h3&gt;
&lt;p&gt;DAGScheduler 把 Job 的 DAG 反向解析，从 Action 往前回溯：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;遇到宽依赖就切一刀，形成新的 Stage&lt;/li&gt;
&lt;li&gt;先执行前面的 Stage，Shuffle 数据落盘后由后面的 Stage 拉取&lt;/li&gt;
&lt;li&gt;Stage 内窄依赖算子串成流水线，减少调度开销&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;rdd.map(f1).filter(f2).groupByKey(f3).mapValues(f4).collect()
        └── Stage0 ──┘    └──── Stage1（Shuffle 切割）────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;划分好 Stage 后，TaskScheduler 按数据本地性（Data Locality）把 Task 派发给 Executor 执行。&lt;/p&gt;
&lt;h2&gt;Shuffle 机制&lt;/h2&gt;
&lt;h3&gt;演进：Hash Shuffle 到 Sort Shuffle&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hash Shuffle（1.x 早期）&lt;/strong&gt;：每个 Map Task 为每个下游分区写一个文件，M 个 Task × R 个分区 = M×R 个文件，小文件爆炸，磁盘 IO 是灾难&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consolidated Hash Shuffle&lt;/strong&gt;：同 Executor 的 Map Task 复用输出文件组，减少到 Executor 数 × R&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sort Shuffle（2.x 默认）&lt;/strong&gt;：每个 Map Task 只输出一个数据文件 + 一个索引文件，先按分区排序（可选再按键排序），下游按索引拉取，小文件数量从 M×R 降到 M&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Sort Shuffle 之所以胜出，本质是&lt;strong&gt;用排序代价换小文件数&lt;/strong&gt;。spark.shuffle.sort.bypassMergeThreshold 默认为 200，当分区数小于 200 时走 bypass 模式，不排序直接合并，更快。
:::&lt;/p&gt;
&lt;h3&gt;中间数据与合并小文件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Shuffle 中间文件由 Executor 负责清理（不再依赖 Driver 删除，规避了 Driver 挂掉数据残留问题）&lt;/li&gt;
&lt;li&gt;小文件过多的优化手段：
&lt;ul&gt;
&lt;li&gt;调节 &lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt;，分区数不是越大越好，200 起步，按数据量评估&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;coalesce&lt;/code&gt;（减少分区，无 Shuffle）或 &lt;code&gt;repartition&lt;/code&gt;（重分布，有 Shuffle）&lt;/li&gt;
&lt;li&gt;开启 &lt;code&gt;spark.sql.adaptive.coalescePartitions.enabled&lt;/code&gt;（AQE）自动合并小分区&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缓存与 Checkpoint&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache/persist&lt;/strong&gt;：把 RDD 缓存在 Executor 内存或磁盘，多次 Action 复用，存储级别有 MEMORY_ONLY、MEMORY_AND_DISK、DISK_ONLY 等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checkpoint&lt;/strong&gt;：把 RDD 数据物理写入 HDFS，&lt;strong&gt;截断血缘&lt;/strong&gt;。与 cache 不同，checkpoint 是为了容错而不是性能——血缘过长时重算代价高，直接读取检查点恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
使用 Checkpoint 前建议先 cache，否则 checkpoint 会触发一次完整计算；且 checkpoint 要等 Job 执行完才落盘，第一次运行没有实际效果。
:::&lt;/p&gt;
&lt;h3&gt;广播变量与累加器&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;广播变量（Broadcast）&lt;/strong&gt;：把小表或大字典发给所有 Executor 一份只读副本，避免每个 Task 都从 Driver 拉取一份（序列化走网络、内存翻 N 倍）。join 小表用 broadcast join 可避免 shuffle&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;累加器（Accumulator）&lt;/strong&gt;：Driver 定义的只写变量，Executor 只能累加，Driver 最终读取，用于统计（如过滤掉的行数、错误计数）。注意 Task 重试会导致重复累加，需要结合执行次数判断&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;val dict = spark.sparkContext.broadcast(Map(&quot;a&quot; -&amp;gt; 1, &quot;b&quot; -&amp;gt; 2))
val badCnt = spark.sparkContext.longAccumulator(&quot;badCnt&quot;)

rdd.map(x =&amp;gt; {
  if (!dict.value.contains(x)) badCnt.add(1)
  dict.value.getOrElse(x, -1)
})
println(s&quot;bad count: ${badCnt.value}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;运行模式&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;单机多线程，local[2] 表示 2 个线程&lt;/td&gt;
&lt;td&gt;开发调试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone&lt;/td&gt;
&lt;td&gt;Spark 自带 Master/Worker 集群&lt;/td&gt;
&lt;td&gt;无 Hadoop 的小集群&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YARN Client&lt;/td&gt;
&lt;td&gt;Driver 跑在提交机，适合交互式调试&lt;/td&gt;
&lt;td&gt;日志直接打在本机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YARN Cluster&lt;/td&gt;
&lt;td&gt;Driver 跑在集群内 AM 容器中，提交机即退&lt;/td&gt;
&lt;td&gt;生产环境推荐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;td&gt;Executor 以 Pod 方式动态创建&lt;/td&gt;
&lt;td&gt;容器化平台&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip
生产环境几乎都是 YARN Cluster 或 K8s。client 模式的 Driver 跑在提交机上，提交机一断网/宕机任务就挂，且 Web UI 无法统一查看。
:::&lt;/p&gt;
&lt;p&gt;YARN 模式下 Executor 内存配额会按公式被二次削减：&lt;code&gt;executor-memory × (1 - spark.memory.overheadFactor)&lt;/code&gt;，overhead 默认 0.1，这部分是堆外内存，留给 JVM 自身和网络缓冲。申请 10g 实际可用堆约 9g。&lt;/p&gt;
&lt;h2&gt;重要参数介绍&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;spark.executor.memory&lt;/td&gt;
&lt;td&gt;1g&lt;/td&gt;
&lt;td&gt;每个 Executor 的 JVM 堆内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.executor.cores&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;每个 Executor 的 CPU 核数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.executor.instances&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;固定模式下的 Executor 数量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.driver.memory&lt;/td&gt;
&lt;td&gt;1g&lt;/td&gt;
&lt;td&gt;Driver 堆内存，collect 大数据时调大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.sql.shuffle.partitions&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;SQL 聚合/Join 的 Shuffle 分区数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.default.parallelism&lt;/td&gt;
&lt;td&gt;取决于模式&lt;/td&gt;
&lt;td&gt;未指定时的默认并行度（分区数）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.memory.fraction&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;堆内可用统一内存占比（0.6 × 堆），剩 0.4 留给用户代码和元数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.memory.storageFraction&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;统一内存中 storage 的&quot;保底份额&quot;，execution 可抢占超出部分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.shuffle.file.buffer&lt;/td&gt;
&lt;td&gt;32k&lt;/td&gt;
&lt;td&gt;Shuffle write 输出缓冲区大小，调大减少磁盘写次数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spark.reducer.maxSizeInFlight&lt;/td&gt;
&lt;td&gt;48m&lt;/td&gt;
&lt;td&gt;Shuffle read 每次拉取数据上限&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;动态资源分配&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.initialExecutors=2
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.maxExecutors=50
spark.shuffle.service.enabled=true   # 必须配套开启（Standalone 默认开）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理：Driver 根据待处理 Task 数和 Executor 利用率周期性扩容/缩容。&lt;strong&gt;注意 YARN 下必须开启 spark.shuffle.service，否则 Executor 被回收后 Shuffle 中间文件无法被其他 Executor 读取&lt;/strong&gt;，会导致任务失败或性能退化。&lt;/p&gt;
&lt;p&gt;:::warning
executor.cores 建议与 Executor 内存匹配，一个 Executor 核数太多（如 8 核 8G）会导致并发 Task 共享内存互相挤压 OOM；常见配比：1 Executor = 4~5 核 + 8~16G。
:::&lt;/p&gt;
&lt;h2&gt;常见问题排查&lt;/h2&gt;
&lt;h3&gt;Executor OOM&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;ExecutorLostFailure&lt;/code&gt;、&lt;code&gt;Java heap space&lt;/code&gt;、YARN 页面看到容器被杀（Container killed by RM）&lt;/li&gt;
&lt;li&gt;排查思路：
&lt;ul&gt;
&lt;li&gt;看是 Task 内存不足（加大 spark.executor.memory）还是 Executor 太多 Task 并发（减少 spark.executor.cores）&lt;/li&gt;
&lt;li&gt;大聚合/大 join 场景调大 &lt;code&gt;spark.memory.fraction&lt;/code&gt; 或直接加内存&lt;/li&gt;
&lt;li&gt;数据倾斜导致的局部 OOM 优先解决倾斜，而不是盲目加内存&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;spark.executor.extraJavaOptions=-XX:+PrintGCDetails&lt;/code&gt; 打 GC 日志辅助定位&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Driver OOM&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;java.lang.OutOfMemoryError: Java heap space&lt;/code&gt; 出现在 Driver 日志，&lt;code&gt;collect&lt;/code&gt;/&lt;code&gt;take&lt;/code&gt; 拉回的数据量过大&lt;/li&gt;
&lt;li&gt;解决：避免 collect 全量数据；非要拉数据用 &lt;code&gt;take(N)&lt;/code&gt; 或先聚合；调大 spark.driver.memory；广播变量过大也占 Driver 内存&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据倾斜&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：某个 Task 运行时间远超同 Stage 其他 Task，GC 频繁&lt;/li&gt;
&lt;li&gt;定位：Spark Web UI 看 Stage 的 Task 耗时分布，或对 key 做 count 统计&lt;/li&gt;
&lt;li&gt;解法（按优先级）：
&lt;ul&gt;
&lt;li&gt;过滤无效 key（如 null、空字符串，先确认业务上可丢弃）&lt;/li&gt;
&lt;li&gt;两阶段聚合：key 加随机前缀打散 → 局部聚合 → 去前缀 → 全局聚合&lt;/li&gt;
&lt;li&gt;join 倾斜：小表广播（broadcast join）；或把大 key 拆分加盐后与扩容的小表 join&lt;/li&gt;
&lt;li&gt;提升并行度：调大 spark.sql.shuffle.partitions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Shuffle 慢&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：Stage 卡在 Shuffle Read，或大量 fetch failed&lt;/li&gt;
&lt;li&gt;检查：executor 心跳是否超时、网络是否拥塞、&lt;code&gt;spark.reducer.maxSizeInFlight&lt;/code&gt; 是否过大导致瞬时拉取压力&lt;/li&gt;
&lt;li&gt;优化：开启压缩（spark.shuffle.compress）、调大 &lt;code&gt;spark.shuffle.file.buffer&lt;/code&gt;、检查是否有大量小文件需要读取、合并小分区&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;任务 Straggler（掉队 Task）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：大部分 Task 秒级完成，少数 Task 跑很久&lt;/li&gt;
&lt;li&gt;解法：开启 &lt;code&gt;spark.speculation=true&lt;/code&gt;（推测执行），对慢 Task 复制一份在别的 Executor 上执行，先完成的生效；倾斜场景靠 speculation 治标不治本，还是得处理倾斜&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;序列化错误&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;NotSerializableException&lt;/code&gt;、&lt;code&gt;Task not serializable&lt;/code&gt;（最常见是闭包里引用了不可序列化的对象，如 SparkSession、Connection）&lt;/li&gt;
&lt;li&gt;解法：把不可序列化的对象声明为 transient 或改用广播变量；使用 Kryo 序列化（&lt;code&gt;spark.serializer=org.apache.spark.serializer.KryoSerializer&lt;/code&gt;）并注册类，比 Java 默认快 10 倍以上&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;动态分配不生效&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：Executor 数量恒定不变，不扩容&lt;/li&gt;
&lt;li&gt;检查：是否开启了 &lt;code&gt;spark.dynamicAllocation.enabled&lt;/code&gt;；YARN 下是否开了 &lt;code&gt;spark.shuffle.service.enabled&lt;/code&gt; 且 Shuffle Service 实例存活；是否设置了 &lt;code&gt;spark.executor.instances&lt;/code&gt; 固定值（两者冲突，固定值优先）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Executor 丢失&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：&lt;code&gt;Lost executor&lt;/code&gt; / &lt;code&gt;Executor is not registered&lt;/code&gt; / &lt;code&gt;Connection refused&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排查：
&lt;ul&gt;
&lt;li&gt;YARN 看容器是被主动杀死（内存超限）还是机器宕机（节点内存/磁盘满）&lt;/li&gt;
&lt;li&gt;Executor 日志找 &lt;code&gt;Container killed by YARN for exceeding memory limits&lt;/code&gt; 说明内存配小了&lt;/li&gt;
&lt;li&gt;心跳超时（heartbeatInterval 默认 10s，超时 spark.network.timeout 默认 120s）调大网络超时或检查 GC 长停顿&lt;/li&gt;
&lt;li&gt;磁盘空间不足导致 Shuffle 写失败，检查 spark.local.dir 所在盘&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
排查问题的通用顺序：&lt;strong&gt;先看 Spark Web UI 的 Stage/Task 耗时与日志 → 再查 Executor 日志与 YARN 容器状态 → 最后结合参数与数据分布下结论&lt;/strong&gt;，不要上来就盲目调参。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Spark 的核心思想可以浓缩为一句话：&lt;strong&gt;以 RDD 血缘为基础、以宽依赖为界划分 Stage、用内存换速度&lt;/strong&gt;。调优也是围绕这三点展开：减小 Shuffle 数据量、提高并行度、让内存物尽其用。掌握架构与调度原理后，大部分性能问题都能从 Web UI 上一眼定位到根因。&lt;/p&gt;
</content:encoded></item><item><title>Tez 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/tez-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/tez-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Tez 是 Apache 旗下的 DAG 计算引擎，脱胎于 MapReduce，用于替代 MR 执行多阶段复杂作业。它把&quot;多轮 MR 写盘&quot;改成&quot;一个 DAG 内存流转&quot;，性能提升肉眼可见，也是 Hive 默认执行引擎之一。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一、Tez 的定位与背景&lt;/h2&gt;
&lt;p&gt;MapReduce 的缺陷很明确：一次作业只能表达 Map → Reduce 两个阶段，复杂的分析（比如 Hive SQL）会被翻译成一串串 MR 作业串行执行，每个 MR 作业的中间结果都要落 HDFS，一轮一轮地读写磁盘，性能极差，而且整个集群的调度开销也被放大。&lt;/p&gt;
&lt;p&gt;Tez 的解决思路：把多个 MR 阶段融合成&lt;strong&gt;一个有向无环图（DAG）&lt;/strong&gt;，一次提交、一次调度，顶点之间直接传递数据，能省则省。&lt;/p&gt;
&lt;p&gt;:::tip
Tez 不是要取代 HDFS/Yarn，而是站在它们之上，解决&quot;计算编排&quot;层面的效率问题。它不存数据，只算数据。
:::&lt;/p&gt;
&lt;p&gt;核心收益：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中间结果尽量留在内存/本地磁盘，不写 HDFS&lt;/li&gt;
&lt;li&gt;一个 DAG 一次调度，避免多轮作业的 AM 反复启动&lt;/li&gt;
&lt;li&gt;任务/容器复用，减少 JVM 启动开销&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、核心概念&lt;/h2&gt;
&lt;p&gt;Tez 的核心抽象围绕 DAG 展开，先记住这几个词：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DAG&lt;/strong&gt;：整个作业的执行计划图，节点是 Vertex，边是 Edge&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vertex（顶点）&lt;/strong&gt;：一个逻辑阶段，对应 MR 里的 Map 或 Reduce 阶段，也可以对应 Spark 里的 stage 概念&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge（边）&lt;/strong&gt;：连接两个顶点，描述上游到下游的数据流转方式和数据分布方式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task&lt;/strong&gt;：顶点的一个并行实例，一个 Vertex 被拆成多个 Task 并行执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Container&lt;/strong&gt;：Yarn 分配的资源容器，Task 在 Container 里运行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session&lt;/strong&gt;：Tez 会话，复用 AM 与容器资源的长期运行环境&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Edge 的类型&lt;/h3&gt;
&lt;p&gt;Edge 除了描述&quot;谁给谁传数据&quot;，还要声明数据怎么分，常用的有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OneToOne（窄依赖）&lt;/strong&gt;：上游一个 Task 的输出只发给下游一个 Task，如 Map 到 Map&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broadcast&lt;/strong&gt;：上游每个 Task 的输出广播给下游所有 Task，如小表分发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ScatterGather（Shuffle）&lt;/strong&gt;：上游输出按分区算法打散，下游每个 Task 从所有上游 Task 拉数据，如 Map 到 Reduce&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Edge 的分区器（Partitioner）和排序逻辑都可插拔，这也是 Tez 能支撑多种引擎接入的底气。
:::&lt;/p&gt;
&lt;h2&gt;三、执行原理&lt;/h2&gt;
&lt;h3&gt;1. 数据流转：尽量不落盘&lt;/h3&gt;
&lt;p&gt;传统 MR：Map 输出 → Shuffle 写盘 → Reduce 拉取再写盘。&lt;/p&gt;
&lt;p&gt;Tez：同一 DAG 内，上游 Task 的输出可以&lt;strong&gt;直接通过内存/网络传给下游 Task&lt;/strong&gt;，只有当数据量超过内存阈值时才落本地磁盘做 spill。只有跨越多个作业（多个 DAG）时才会写 HDFS。&lt;/p&gt;
&lt;h3&gt;2. Task 复用与容器复用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Task 复用&lt;/strong&gt;：一个 Task 处理完自己的分片后，如果还有空闲容器，可以继续处理其他分片，减少启动开销&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容器复用（Container Reuse）&lt;/strong&gt;：一个 Container 跑完一批 Task 后不销毁，继续加载下一个 Task，避免反复申请资源和启动 JVM&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两招对&quot;Task 数多但单个 Task 执行快&quot;的场景（比如大量小文件、聚合类 SQL）收益特别大。&lt;/p&gt;
&lt;h3&gt;3. 动态资源分配&lt;/h3&gt;
&lt;p&gt;Tez 的 AM 会统计当前 DAG 各顶点的实际执行速度、数据量，动态调整后续顶点的并行度，比如 Shuffle 下游 Reduce 的 Task 数可以根据实际数据量缩放，避免预估不准导致资源浪费或倾斜。&lt;/p&gt;
&lt;h2&gt;四、与 MapReduce 的对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;MapReduce&lt;/th&gt;
&lt;th&gt;Tez&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;作业模型&lt;/td&gt;
&lt;td&gt;单作业两阶段（Map/Reduce）&lt;/td&gt;
&lt;td&gt;多阶段 DAG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中间结果&lt;/td&gt;
&lt;td&gt;每轮写 HDFS&lt;/td&gt;
&lt;td&gt;内存/本地流转，尽量不落盘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调度次数&lt;/td&gt;
&lt;td&gt;每轮作业一次（AM 反复启停）&lt;/td&gt;
&lt;td&gt;整个 DAG 一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;容器复用&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;支持，任务/容器级复用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;资源利用&lt;/td&gt;
&lt;td&gt;轮次间有大量空窗&lt;/td&gt;
&lt;td&gt;连续流水线式执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复杂 SQL&lt;/td&gt;
&lt;td&gt;翻译成 N 个 MR 串行&lt;/td&gt;
&lt;td&gt;翻译成一个 DAG&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
MR 不是一无是处：单阶段、逻辑简单的作业上两者差距不大，而且 MR 的稳定性经过多年打磨。但凡是多阶段 JOIN/聚合类作业，Tez 的收益非常明显。
:::&lt;/p&gt;
&lt;h2&gt;五、在 Hive / Spark 中的应用&lt;/h2&gt;
&lt;h3&gt;1. Hive on Tez&lt;/h3&gt;
&lt;p&gt;Hive 里一条 SQL 的执行计划（比如 JOIN → GROUP BY → ORDER BY）会被翻译成 Tez DAG，每个 MR 风格的阶段变成 DAG 中的 Vertex，Reduce 阶段之间的 Shuffle 变成 Edge，一次提交执行。&lt;/p&gt;
&lt;p&gt;切换引擎（老版本 Hive 1.x 时代常用）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET hive.execution.engine=tez;   -- tez / mr / spark
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Spark 与 Tez&lt;/h3&gt;
&lt;p&gt;Spark 有自己的 DAG 引擎，一般不依赖 Tez。但在部分生态（如老版本 Hive on Spark 之前的过渡期、某些 Impala/Athena 变体）里 Tez 仍扮演执行引擎角色。对大数据从业者来说，掌握 Tez 的 DAG 思想能帮助理解 Spark Stage 的划分，两者是同一个思想脉络。&lt;/p&gt;
&lt;h2&gt;六、重要参数&lt;/h2&gt;
&lt;h3&gt;1. 资源相关&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.am.resource.memory.mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AM 内存，默认 1024，大作业建议调大，否则 AM 频繁 GC 甚至 OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.task.resource.memory.mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单个 Task 内存，默认 1024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tez.task.resource.cpu.vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单个 Task 虚拟核数，默认 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. 复用相关&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tez.am.container.reuse.enabled&lt;/code&gt;：默认 true，开启容器复用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tez.am.container.reuse.rack-fallback.enabled&lt;/code&gt;：容器复用落空时是否允许跨机架复用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 运行时相关&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tez.runtime.io.sort.mb&lt;/code&gt;：排序缓冲区大小，类似 MR 的 sort.mb，大 shuffle 时可调大&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tez.grouping.min-size&lt;/code&gt;：Map 端输入分片的最小分组大小，默认 16MB，影响 Input 合并粒度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tez.session.mode&lt;/code&gt;：会话模式，取值 &lt;code&gt;strict&lt;/code&gt;（独占 AM）/ &lt;code&gt;nonsession&lt;/code&gt;（按作业启停 AM），Hive 通常配合 &lt;code&gt;hive.server2.tez.sessions.per.default.queue&lt;/code&gt; 管理常驻 Session&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. Hive 侧相关&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hive.execution.engine&lt;/code&gt;：切换执行引擎&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hive.tez.container.size&lt;/code&gt;：Hive 传给 Tez 的容器内存，常与 &lt;code&gt;tez.task.resource.memory.mb&lt;/code&gt; 联动&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
改内存参数时注意和 Yarn 的 &lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt; 对齐，Task/AM 申请超过队列上限会一直 pending。
:::&lt;/p&gt;
&lt;h2&gt;七、常见问题排查&lt;/h2&gt;
&lt;h3&gt;1. 任务启动慢&lt;/h3&gt;
&lt;p&gt;DAG 调度阶段耗时过长，通常是 AM 申请慢、会话未复用（每次都新建 AM）、队列资源紧张。检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是否启用了 Tez Session（Hive 场景看 &lt;code&gt;hive.server2.tez.sessions&lt;/code&gt; 配置）&lt;/li&gt;
&lt;li&gt;Yarn 队列是否打满，AM 是否长时间 pending&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. AM 内存不足&lt;/h3&gt;
&lt;p&gt;日志出现 &lt;code&gt;Container killed by the ResourceManager&lt;/code&gt; 或 AM OOM。把 &lt;code&gt;tez.am.resource.memory.mb&lt;/code&gt; 调大，同时注意队列最大资源上限。&lt;/p&gt;
&lt;h3&gt;3. Tez Session 未复用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tez.session.mode&lt;/code&gt; 配成了 &lt;code&gt;nonsession&lt;/code&gt; 会导致每个查询都新建 AM&lt;/li&gt;
&lt;li&gt;Session 空闲超时被回收，检查 &lt;code&gt;tez.session.am.dag.submit.timeout&lt;/code&gt;、&lt;code&gt;hive.server2.tez.sessions.per.default.queue&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 任务失败重试风暴&lt;/h3&gt;
&lt;p&gt;某个 Task 反复失败重试，占满集群资源。优先看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是否数据倾斜导致个别 Task 拉爆内存&lt;/li&gt;
&lt;li&gt;是否有坏节点，Container 反复失败，配合 &lt;code&gt;tez.am.max.task.attempts&lt;/code&gt; 限制重试次数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 数据倾斜&lt;/h3&gt;
&lt;p&gt;Shuffle 下游个别 Task 处理量远大于均值。手段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开启倾斜优化（Hive 侧 &lt;code&gt;hive.groupby.skewindata=true&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;合理设置分区数，避免 Reduce 数量过少&lt;/li&gt;
&lt;li&gt;大小表 JOIN 用 Broadcast Edge，避免全量 Shuffle&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. Yarn 资源配额问题&lt;/h3&gt;
&lt;p&gt;现象：Tez 作业正常，但提交后一直 ACCEPTED 不运行。检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt; 是否小于 Tez 申请的内存&lt;/li&gt;
&lt;li&gt;队列容量/最大容量是否被其他作业占满&lt;/li&gt;
&lt;li&gt;用户权限：提交的队列是否有访问权限&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. 日志定位（Tez UI）&lt;/h3&gt;
&lt;p&gt;Tez 自带 Web UI（AM 页面），能看 DAG 各顶点耗时、每个 Task 的 Attempt 次数、Shuffle 数据量、反压情况。排查性能问题优先看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪个 Vertex 耗时最长（瓶颈顶点）&lt;/li&gt;
&lt;li&gt;哪些 Task 的 Attempt 数异常（失败重试）&lt;/li&gt;
&lt;li&gt;顶点间传输数据量是否异常（倾斜）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;八、小结&lt;/h2&gt;
&lt;p&gt;:::tip
一句话总结 Tez：&lt;strong&gt;把 MapReduce 的多轮写盘改成一张 DAG 的内存流水线，配合容器/任务复用和动态资源，把多阶段计算的速度提上去。&lt;/strong&gt; 它是理解 Hive 执行引擎、以及后来自研 DAG 引擎的重要基石。
:::&lt;/p&gt;
&lt;p&gt;学习建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先跑一个 &lt;code&gt;explain&lt;/code&gt; 看 Hive on Tez 的 DAG 结构，对照本文概念逐个理解&lt;/li&gt;
&lt;li&gt;出问题时先看 Tez UI，再查参数，不要盲目调内存&lt;/li&gt;
&lt;li&gt;和 Spark Stage/Shuffle 对照着学，一通百通&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Zookeeper 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/zookeeper-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/zookeeper-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文从 Zookeeper 的数据模型、ZAB 协议、Leader 选举、会话机制出发，梳理分布式协调服务的底层原理，最后给出常见问题的排查思路。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Zookeeper 是 Apache 开源的分布式协调服务，用于解决分布式系统中的&lt;strong&gt;一致性&lt;/strong&gt;、&lt;strong&gt;数据发布订阅&lt;/strong&gt;、&lt;strong&gt;命名服务&lt;/strong&gt;、&lt;strong&gt;分布式锁&lt;/strong&gt;、&lt;strong&gt;集群管理&lt;/strong&gt;等问题。&lt;/p&gt;
&lt;p&gt;:::note
Zookeeper 的核心定位是&quot;小数据的协调&quot;，它存储的数据量很小（一般建议单节点不超过几 MB 到十几 MB），但要求极高的读性能和一致性。不要把 ZK 当数据库用。
:::&lt;/p&gt;
&lt;p&gt;它的典型使用者：Kafka（broker 元数据、Controller 选举、分区副本选主）、HBase（HMaster 选举、meta 表定位）、Dubbo（注册中心）、Eureka/Consul 之外的分布式锁实现等。&lt;/p&gt;
&lt;h2&gt;数据模型&lt;/h2&gt;
&lt;h3&gt;Znode 树形结构&lt;/h3&gt;
&lt;p&gt;Zookeeper 的数据模型是一棵&lt;strong&gt;树&lt;/strong&gt;，树的每个节点称为 &lt;strong&gt;Znode&lt;/strong&gt;。每个 Znode 有唯一的路径，如 &lt;code&gt;/dubbo/service1/providers&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 Znode 可以存储数据（上限默认 1MB，建议小数据量）和挂载子节点&lt;/li&gt;
&lt;li&gt;路径以 &lt;code&gt;/&lt;/code&gt; 开头，不允许嵌套 &lt;code&gt;/dubbo//x&lt;/code&gt; 这样的写法&lt;/li&gt;
&lt;li&gt;每个 Znode 都有 stat 状态信息：版本号（version）、子节点版本（cversion）、ACL 版本（aversion）、时间戳（ctime/mtime）、数据长度、ephemeralOwner 等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Znode 按生命周期分为四类：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;应用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;持久节点（Persistent）&lt;/td&gt;
&lt;td&gt;创建后永久存在，除非显式删除&lt;/td&gt;
&lt;td&gt;配置数据、固定目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;持久顺序节点（Persistent Sequential）&lt;/td&gt;
&lt;td&gt;创建时自动追加递增序号&lt;/td&gt;
&lt;td&gt;分布式队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;临时节点（Ephemeral）&lt;/td&gt;
&lt;td&gt;会话结束自动删除&lt;/td&gt;
&lt;td&gt;注册中心、分布式锁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;临时顺序节点（Ephemeral Sequential）&lt;/td&gt;
&lt;td&gt;临时 + 顺序&lt;/td&gt;
&lt;td&gt;分布式锁核心&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
临时节点不能有子节点，且它的生命周期与创建它的&lt;strong&gt;会话&lt;/strong&gt;绑定：会话失效（不是连接断开），节点即被删除。这是 ZK 做服务注册发现时&quot;宕机自动下线&quot;的根基。
:::&lt;/p&gt;
&lt;h3&gt;版本号与乐观锁&lt;/h3&gt;
&lt;p&gt;每个 Znode 的 stat 中有 &lt;code&gt;dataVersion&lt;/code&gt;（数据版本号），任何写操作都携带版本号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 携带 version，实现 CAS 式更新
zk.setData(path, data, version);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;传入的 version 与当前版本不一致时，抛出 &lt;code&gt;BadVersionException&lt;/code&gt;，事务提交失败&lt;/li&gt;
&lt;li&gt;这是 ZK 实现分布式锁&quot;防重入、防误删&quot;的重要手段：删除锁节点时携带创建时读到的 version，避免删掉别人重入创建的节点&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Watch 机制&lt;/h3&gt;
&lt;p&gt;Watch（监听）是 ZK 的&lt;strong&gt;发布订阅&lt;/strong&gt;实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;客户端对某个节点注册 watch：&lt;code&gt;getData(path, watch, ...)&lt;/code&gt;、&lt;code&gt;getChildren(path, watch, ...)&lt;/code&gt;、&lt;code&gt;exists(path, watch, ...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;节点数据变化、子节点变化、节点删除时，服务端向注册了 watch 的客户端发送通知&lt;/li&gt;
&lt;li&gt;通知是&lt;strong&gt;一次性&lt;/strong&gt;的：触发后即失效，需要客户端重新注册&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
Watch 通知只能保证&quot;事件发生了&quot;，不保证事件内容完整：&lt;code&gt;getData&lt;/code&gt; 触发的通知里不含新数据，需要客户端再主动 &lt;code&gt;getData&lt;/code&gt; 一次拿最新值。且 watch 在 session 过期时全部失效（不会触发 Watcher），回调线程执行过慢还可能触发 &lt;code&gt;Session moved&lt;/code&gt; 相关异常。
:::&lt;/p&gt;
&lt;p&gt;:::warning
ZK 客户端发起 watch 注册与获得通知之间存在&quot;窗口期&quot;：注册请求没到达服务端之前事件已经发生，就会丢失。这就是常见的&quot;watch 丢失&quot;问题来源，通常通过读锁路径时先 &lt;code&gt;exists&lt;/code&gt; 再 &lt;code&gt;getData&lt;/code&gt; 之类的二次确认来缓解。
:::&lt;/p&gt;
&lt;h2&gt;一致性协议：ZAB&lt;/h2&gt;
&lt;p&gt;ZAB（Zookeeper Atomic Broadcast）是 ZK 专属的一致性协议，全称&quot;原子广播协议&quot;。它只解决一个问题：&lt;strong&gt;保证所有副本上的数据变更以事务形式按序提交&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;ZAB 协议包含两大阶段：&lt;/p&gt;
&lt;h3&gt;阶段一：原子广播（正常运行）&lt;/h3&gt;
&lt;p&gt;写请求统一交给 Leader，Leader 执行两阶段提交的变体：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端将写请求发给 Leader&lt;/li&gt;
&lt;li&gt;Leader 生成全局唯一的 &lt;strong&gt;ZXID&lt;/strong&gt;（事务 id），把提案（Proposal）广播给所有 Follower&lt;/li&gt;
&lt;li&gt;Follower 将提案写入本地事务日志（sync 落盘），返回 ACK&lt;/li&gt;
&lt;li&gt;Leader 收到&lt;strong&gt;过半（法定人数 quorum）&lt;/strong&gt; ACK 后提交该提案，并广播 commit 给 Follower&lt;/li&gt;
&lt;li&gt;Follower 应用 commit，返回客户端结果&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip
过半提交保证了任意两条事务不会在不同机器上以相反顺序被应用：因为任何过半集合必有交集。这就是 ZK 容忍少数节点故障（容忍 2 台挂 1 台）的理论依据。
:::&lt;/p&gt;
&lt;h3&gt;阶段二：崩溃恢复（Leader 故障）&lt;/h3&gt;
&lt;p&gt;Leader 崩溃后，集群进入崩溃恢复阶段，选出新 Leader，并把旧 Leader 已经提交但未同步的事务通过**截断（truncate）&lt;strong&gt;或&lt;/strong&gt;同步（sync）**对齐到新 Leader 的状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新 Leader 必须是&lt;strong&gt;数据最新&lt;/strong&gt;（ZXID 最大）的节点&lt;/li&gt;
&lt;li&gt;对落后节点：把超过新 Leader ZXID 的未提交事务截断丢弃&lt;/li&gt;
&lt;li&gt;对可能已经提交的事务：先同步补齐再对外服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
ZAB 与 Paxos/Raft 的异同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paxos：没有 Leader 概念（Basic Paxos），通过两阶段 Prepare/Accept 达成共识，ZK 的 ZAB 受 Paxos 启发但做了简化：固定 Leader、广播式提交&lt;/li&gt;
&lt;li&gt;Raft：同样基于 Leader + 日志复制 + 过半提交，但 Raft 有完整的&quot;任期（term）&quot;和随机超时选主；ZAB 的 ZXID 包含 epoch（朝代）+ 计数，等价于 term&lt;/li&gt;
&lt;li&gt;关键区别：Raft 的日志是&quot;连续性&quot;的（领导者尝试填平日志空洞），ZAB 直接截断多余日志；ZAB 是顺序提交、无日志空洞
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Leader 选举&lt;/h2&gt;
&lt;h3&gt;选举算法：Fast Leader Election（FLE）&lt;/h3&gt;
&lt;p&gt;ZK 3.4+ 默认使用 Fast Leader Election。选举触发时机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集群启动时&lt;/li&gt;
&lt;li&gt;Leader 崩溃 / 失联（Follower 收不到 Leader 心跳）&lt;/li&gt;
&lt;li&gt;节点进入 LOOKING 状态&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;投票规则（比较规则）&lt;/h3&gt;
&lt;p&gt;每张投票包含 &lt;code&gt;(myid, zxid, epoch)&lt;/code&gt;，比较顺序是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;epoch（朝代）&lt;/strong&gt; 大的优先 —— 新的选举周期&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;zxid（最新事务 id）&lt;/strong&gt; 大的优先 —— 数据新的节点当 Leader&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;myid（节点编号）&lt;/strong&gt; 大的优先 —— 数据相同时编号大的胜出&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip
&quot;epoch 优先、zxid 优先、myid 兜底&quot; 保证选出的 Leader 一定是&lt;strong&gt;数据最全&lt;/strong&gt;的节点，避免数据回退；myid 兜底保证最终一定能收敛出唯一 Leader。
:::&lt;/p&gt;
&lt;p&gt;选举流程（以 3 节点为例，myid=1,2,3）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所有节点进入 LOOKING，先投自己（myid, zxid, epoch）&lt;/li&gt;
&lt;li&gt;把投票广播给其他节点&lt;/li&gt;
&lt;li&gt;收到别人的投票后按规则 PK：如果别人&quot;更大&quot;就改投别人，并广播新票&lt;/li&gt;
&lt;li&gt;某节点发现自己的票被&lt;strong&gt;过半&lt;/strong&gt;节点认可（包括自己），成为 Leader，其余节点成为 Follower&lt;/li&gt;
&lt;li&gt;Leader 与其他节点建立学习（sync）流程，对齐数据后进入正常广播阶段&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::warning
脑裂场景：ZK 集群必须满足&lt;strong&gt;过半存活&lt;/strong&gt;才能选出 Leader。例如 5 台集群，网络分区成 3+2，3 台那边能选出 Leader 继续服务；2 台那边永远无法过半，只能等待恢复。所以 ZK 集群建议奇数台（3、5、7），避免双 Leader。
:::&lt;/p&gt;
&lt;h3&gt;myid / zxid 的组成&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;myid&lt;/strong&gt;：写在 &lt;code&gt;data/myid&lt;/code&gt; 文件里的唯一编号，用于区分节点身份&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;zxid&lt;/strong&gt;：64 位，高 32 位是 &lt;strong&gt;epoch&lt;/strong&gt;（每次选举 +1），低 32 位是当前 epoch 内的事务序号。保证全局单调递增&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;epoch&lt;/strong&gt; 在选举时互相 PK，只有 epoch 更大或相同才接受对方的投票&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 查看节点角色与状态
zkServer.sh status
# 输出：Mode: leader / follower
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;会话机制&lt;/h2&gt;
&lt;h3&gt;Session 与超时&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;客户端连接 ZK 建立会话，服务端分配 sessionId（64 位）与 sessionTimeout&lt;/li&gt;
&lt;li&gt;会话以&lt;strong&gt;心跳&lt;/strong&gt;维持：客户端定期（sessionTimeout/3 左右）发 ping，服务端过期扫描线程按 &lt;code&gt;sessionTimeout/2&lt;/code&gt; 的粒度检查&lt;/li&gt;
&lt;li&gt;服务端判定会话超时后：删除该会话的所有临时节点，并广播事件&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;临时节点生命周期&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;创建临时节点  -&amp;gt;  会话存活  -&amp;gt;  节点存在
              -&amp;gt;  会话超时  -&amp;gt;  节点被删除（与连接断开无关）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
关键区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;连接断开&lt;/strong&gt;：TCP 断了，但会话可能还在（超时时间内重连成功，节点还在）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话超时&lt;/strong&gt;：超过 sessionTimeout 没恢复心跳，节点永久删除
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;重连与恢复&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;客户端断开后，ZK Client 库会按 &lt;code&gt;connectTimeout&lt;/code&gt; 重试，尽量重连到&lt;strong&gt;同一台&lt;/strong&gt;服务端（会话仍有效）&lt;/li&gt;
&lt;li&gt;连接串配多个地址时，重连成功后如果 session 未过期，无需重新初始化，watch 保留；如果 session 已过期，客户端抛出 &lt;code&gt;SessionExpiredException&lt;/code&gt;，应用需自行重建会话、重建 watch、重新注册临时节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::warning
临时节点的&quot;存活依赖&quot;陷阱：如果应用进程还在，但 session 超时导致临时节点被删，ZK 不会自动帮你重建。服务注册场景必须捕获 &lt;code&gt;SessionExpiredException&lt;/code&gt; 并重连重建，否则会出现&quot;服务还活着、注册信息没了&quot;的现象。
:::&lt;/p&gt;
&lt;h2&gt;典型应用&lt;/h2&gt;
&lt;h3&gt;分布式锁（临时顺序节点 + Watch）&lt;/h3&gt;
&lt;p&gt;核心思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加锁：在锁目录下创建&lt;strong&gt;临时顺序节点&lt;/strong&gt;，如 &lt;code&gt;/locks/lock_0000000010&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;判断自己是否是序号最小的节点：是则获得锁&lt;/li&gt;
&lt;li&gt;不是则注册 watch 监听&lt;strong&gt;前一个节点&lt;/strong&gt;的删除事件，被唤醒后重新检查&lt;/li&gt;
&lt;li&gt;解锁：删除自己的节点（携带 version 防误删），会话异常则临时节点自动清理&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 伪代码：加锁流程
String path = zk.create(&quot;/locks/lock-&quot;, data, EPHEMERAL_SEQUENTIAL);
List&amp;lt;String&amp;gt; children = zk.getChildren(&quot;/locks&quot;, false);
String lockId = path.substring(path.lastIndexOf(&quot;/&quot;) + 1);
int mySeq = getSeq(lockId);
String prev = findPrevSmaller(mySeq);   // 找序号比自己小的最近节点
if (prev == null) {
    return 获得锁;
}
zk.exists(prev, new Watcher() {        // watch 前一个节点
    public void process(WatchedEvent e) {
        if (e.getType() == NodeDeleted) {
            // 唤醒抢锁线程，重新走判断流程
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
与 Redis 锁相比，ZK 锁的优点是&quot;天然防死锁&quot;（会话超时自动释放临时节点）、有事件通知不用自旋轮询；缺点是性能差（每次加锁多次 RTT）、依赖 ZK 集群。
:::&lt;/p&gt;
&lt;h3&gt;服务注册发现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;服务提供者启动时在 &lt;code&gt;/services/{service}/providers/{ip:port}&lt;/code&gt; 下创建&lt;strong&gt;临时节点&lt;/strong&gt;，注册 URL 元数据&lt;/li&gt;
&lt;li&gt;消费者 watch 该服务目录的 &lt;code&gt;getChildren&lt;/code&gt;，节点变化（上线/下线）实时感知&lt;/li&gt;
&lt;li&gt;提供者宕机 → 会话超时 → 临时节点删除 → 消费者 watch 触发 → 剔除实例&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;分布式队列&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;FIFO 队列：用持久顺序节点，出队时取序号最小节点（类似分布式锁反向）&lt;/li&gt;
&lt;li&gt;屏障（Barrier）：先注册一个 ready 节点，参与者全部就绪才删除它，实现多节点同步&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;大数据组件的协调器&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kafka&lt;/strong&gt;：broker 注册、Controller 选举（谁先抢到临时节点谁是 Controller）、分区 leader 选举、ISR 元数据存储&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HBase&lt;/strong&gt;：HMaster 抢注临时节点选举、meta 表位置记录、RegionServer 在线状态上报&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HDFS&lt;/strong&gt;（旧版）：NameNode 双机热备的 Active/Standby 选举&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常规问题排查&lt;/h2&gt;
&lt;h3&gt;1. 集群脑裂 / 无法选主&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;检查各节点 &lt;code&gt;zkServer.sh status&lt;/code&gt;，是否处于 LOOKING 状态&lt;/li&gt;
&lt;li&gt;网络分区：确认节点之间 2888/3888 端口互通，防火墙是否误拦&lt;/li&gt;
&lt;li&gt;集群必须&lt;strong&gt;过半&lt;/strong&gt;存活才能选主，&lt;code&gt;server.1=...&lt;/code&gt; 配置中节点总数是否与实际一致&lt;/li&gt;
&lt;li&gt;检查 &lt;code&gt;data/myid&lt;/code&gt; 是否与配置文件对应、事务日志目录是否可写&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
「无法选主」最常见原因：集群配置里写了 5 台但实际只起了 2 台，永远无法过半；或者磁盘满了事务日志写不进去。
:::&lt;/p&gt;
&lt;h3&gt;2. session 过期导致临时节点丢失&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：客户端未重启，但注册中心里的临时节点消失了&lt;/li&gt;
&lt;li&gt;排查：查看日志 &lt;code&gt;Session 0x... expired&lt;/code&gt;；调大客户端 &lt;code&gt;sessionTimeout&lt;/code&gt; 配置（同时服务端需在 &lt;code&gt;maxSessionTimeout&lt;/code&gt; 范围内）&lt;/li&gt;
&lt;li&gt;应用层必须监听 &lt;code&gt;SessionExpiredException&lt;/code&gt; 做重建逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. watch 通知丢失&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;原因：watch 一次性 + 注册与事件之间的窗口期&lt;/li&gt;
&lt;li&gt;排查：检查是否在事件回调里重新注册了 watch；不要在回调里做耗时操作（长时间占用 watcher 线程会触发 &lt;code&gt;Session moved&lt;/code&gt;/&lt;code&gt;Watcher&lt;/code&gt; 抛错）&lt;/li&gt;
&lt;li&gt;缓解：先 &lt;code&gt;exists&lt;/code&gt; 再 &lt;code&gt;getData&lt;/code&gt; 二次确认；业务上以&quot;主动拉取 + watch 兜底&quot;双保险&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 读写性能瓶颈&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ZK 读性能远好于写：写必须过 Leader 且落盘 + 过半 ACK&lt;/li&gt;
&lt;li&gt;客户端连接全部打到 Leader 导致写放大：使用 observer 节点分担读压力（observer 不参与投票）&lt;/li&gt;
&lt;li&gt;单机吞吐提升：将事务日志放在独立磁盘（SSD），与数据快照分离&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 连接数限制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;服务端默认连接数上限 &lt;code&gt;maxClientCnxns&lt;/code&gt;（默认 60），连接池/多客户端场景容易触发 &lt;code&gt;Connection refused&lt;/code&gt; 或 &lt;code&gt;Too many connections&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调大配置并重启，或应用侧复用连接池&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 慢操作：sync 阻塞&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;写操作必须等待事务日志 fsync 落盘，磁盘 IO 慢 → 写延迟飙高&lt;/li&gt;
&lt;li&gt;现象：&lt;code&gt;sync 耗时高&lt;/code&gt;、事务日志增长慢、客户端大量超时&lt;/li&gt;
&lt;li&gt;排查：&lt;code&gt;iostat&lt;/code&gt; 看磁盘；保证 &lt;code&gt;/data/version-2&lt;/code&gt; 下的 &lt;code&gt;log.*&lt;/code&gt; 文件所在盘性能&lt;/li&gt;
&lt;li&gt;配置 &lt;code&gt;fsync.windowsize&lt;/code&gt; 批量刷盘可缓解部分压力（牺牲少量可靠性换取吞吐）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7. 磁盘事务日志管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;事务日志默认无限制增长，老日志可通过 &lt;code&gt;autopurge.snapRetainCount&lt;/code&gt; + &lt;code&gt;autopurge.purgeInterval&lt;/code&gt; 自动清理&lt;/li&gt;
&lt;li&gt;快照（snapshot）与事务日志（log）分别存储；恢复时先加载快照再重放 log&lt;/li&gt;
&lt;li&gt;磁盘写满后 ZK 会停止接受写入，表现为 &quot;Unable to write to transaction log&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;数据模型上记住&quot;树 + Znode 四种类型 + 版本号 + 一次性 Watch&quot;&lt;/li&gt;
&lt;li&gt;一致性上记住&quot;ZAB = 原子广播 + 崩溃恢复，过半提交&quot;&lt;/li&gt;
&lt;li&gt;选举上记住&quot;epoch &amp;gt; zxid &amp;gt; myid&quot;的 PK 规则&lt;/li&gt;
&lt;li&gt;会话上记住&quot;临时节点跟随会话生命周期，超时即删&quot;&lt;/li&gt;
&lt;li&gt;面试/实践最关键的一句话：&lt;strong&gt;ZK 适合做协调和小数据强一致，不适合当数据库和无限扩容的注册中心&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Yarn 底层原理与知识要点</title><link>https://chaggle.github.io/posts/2026/08/09/yarn-principles/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/08/09/yarn-principles/</guid><pubDate>Sat, 08 Aug 2026 16:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Yarn 是 Hadoop 的资源调度层，负责把集群的 CPU 和内存&quot;分蛋糕&quot;。搞懂 Yarn，才能明白为什么任务一直卡在 ACCEPTED、为什么容器总被杀、为什么队列里任务挤成一堆。这篇文章从架构到排障完整梳理一遍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;核心架构&lt;/h2&gt;
&lt;p&gt;Yarn 采用主从 + 两级调度的架构：ResourceManager 管全局资源，NodeManager 管单节点资源，ApplicationMaster 管单个应用。&lt;/p&gt;
&lt;h3&gt;ResourceManager（RM，主节点）&lt;/h3&gt;
&lt;p&gt;全局资源管理器，包含两大组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scheduler（调度器）&lt;/strong&gt;：纯资源分配，不关心应用具体执行。把资源以 Container 的形式分配给各个应用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ApplicationManager（应用管理器）&lt;/strong&gt;：负责接收作业提交、协商启动 ApplicationMaster、监控 AM 状态、失败时重启 AM、处理作业完成与清理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RM 自身是单点，需要配置 HA（Active/Standby，依赖 ZooKeeper 选主）。&lt;/p&gt;
&lt;h3&gt;NodeManager（NM，从节点）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每台计算节点一个，管理本节点的资源（CPU、内存、磁盘）&lt;/li&gt;
&lt;li&gt;负责启动/销毁 Container，上报容器状态、节点健康情况（含磁盘健康检查）给 RM&lt;/li&gt;
&lt;li&gt;不参与计算，计算都在 Container 里跑&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ApplicationMaster（AM）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个应用（作业）一个 AM，是&quot;作业的大脑&quot;&lt;/li&gt;
&lt;li&gt;向 RM 申请资源（Container），与 NM 通信启动任务，监控任务进度，失败重试&lt;/li&gt;
&lt;li&gt;AM 本身就是运行在一个 Container 里的特殊任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Container（容器）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;资源的抽象单位：一段内存 + 若干虚拟 CPU 核（vcore）&lt;/li&gt;
&lt;li&gt;任务的每个执行单元（Map/Reduce Task、Executor）都跑在一个 Container 里&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;队列（Queue）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;调度器按队列组织资源：多个队列共享集群资源，每个队列可配容量、最大资源、ACL 等&lt;/li&gt;
&lt;li&gt;提交作业时指定队列：&lt;code&gt;-D mapreduce.job.queuename=xxx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
两级调度的心智模型：RM 把资源给&quot;应用&quot;（AM），AM 再把资源细分为一个个 Container 给&quot;任务&quot;。RM 不感知单个 task，只感知应用层面的资源需求。
:::&lt;/p&gt;
&lt;h2&gt;调度器对比&lt;/h2&gt;
&lt;h3&gt;FIFO Scheduler&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;先进先出，一个队列，先来先服务&lt;/li&gt;
&lt;li&gt;问题：前面一个大作业占满资源，后面所有小作业都要等（队头阻塞）&lt;/li&gt;
&lt;li&gt;只适合教学/单用户场景，生产环境基本不用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Capacity Scheduler（容量调度器，默认）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;按队列分配固定比例容量（capacity），如 &lt;code&gt;root.default&lt;/code&gt; 60%、&lt;code&gt;root.etl&lt;/code&gt; 40%&lt;/li&gt;
&lt;li&gt;每个队列容量有下限保障，还有弹性：某个队列资源闲置时，可被其他队列&quot;借走&quot;（弹性共享），忙时收回&lt;/li&gt;
&lt;li&gt;支持层级队列（root 下有子队列）、ACL 控制、优先级&lt;/li&gt;
&lt;li&gt;特点：可预测、易于管理，适合多部门多业务线&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fair Scheduler（公平调度器）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;所有运行中的作业按权重&lt;strong&gt;公平分享&lt;/strong&gt;资源，新作业启动后能抢到等待的作业一半资源，收敛更快&lt;/li&gt;
&lt;li&gt;按资源需求动态分配，空闲队列资源利用率高&lt;/li&gt;
&lt;li&gt;配置较灵活（fair-scheduler.xml），适合用户多、作业多且小的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;调度器&lt;/th&gt;
&lt;th&gt;分配依据&lt;/th&gt;
&lt;th&gt;典型场景&lt;/th&gt;
&lt;th&gt;配置项&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FIFO&lt;/td&gt;
&lt;td&gt;提交时间&lt;/td&gt;
&lt;td&gt;单用户测试&lt;/td&gt;
&lt;td&gt;无需配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacity&lt;/td&gt;
&lt;td&gt;队列容量比例&lt;/td&gt;
&lt;td&gt;多业务线、资源隔离要求高&lt;/td&gt;
&lt;td&gt;capacity-scheduler.xml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;公平份额&lt;/td&gt;
&lt;td&gt;多用户共享、作业量多变&lt;/td&gt;
&lt;td&gt;fair-scheduler.xml&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning
CDH/阿里云 EMR 默认是 Capacity Scheduler；如果线上任务忽快忽慢、某个部门的作业老吃不到资源，先看队列容量配置和调度器类型，别急着怪 Spark 参数。
:::&lt;/p&gt;
&lt;h2&gt;应用提交与运行流程&lt;/h2&gt;
&lt;p&gt;以 MapReduce 作业为例，完整生命周期：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;提交&lt;/strong&gt;：客户端调用 &lt;code&gt;Job.waitForCompletion()&lt;/code&gt;，向 RM 提交作业（含 jar、分片信息），RM 返回作业 ID 和提交路径&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度 AM&lt;/strong&gt;：RM 的 Scheduler 为该作业分配第一个 Container，ApplicationManager 指示 NM 启动 ApplicationMaster&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AM 初始化&lt;/strong&gt;：AM 启动后向 RM 注册自己，然后根据输入分片向 RM 申请运行 Map Task 的 Container&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配与启动&lt;/strong&gt;：RM 分配 Container（满足队列、资源、节点位置等约束），AM 与对应 NM 通信，启动任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务执行&lt;/strong&gt;：Map Task 执行完，shuffle 阶段数据传给 Reduce Task；AM 持续监控进度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完成清理&lt;/strong&gt;：作业完成后 AM 注销并向 RM 汇报，RM 清理作业状态&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::note
这里的重点是&quot;AM 向 RM 申请资源&quot;的循环：RM 只是分配器，&lt;strong&gt;不直接启动任务&lt;/strong&gt;。如果 AM 申请不到资源，作业就会一直卡在 ACCEPTED / RUNNING 但无 task 启动。
:::&lt;/p&gt;
&lt;h2&gt;重要参数介绍&lt;/h2&gt;
&lt;h3&gt;节点资源（NM 层面）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.resource.cpu-vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;单节点可供 Yarn 使用的虚拟核数，按物理核配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.resource.memory-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;单节点可供 Yarn 使用的总内存（MB），&lt;strong&gt;必须小于物理内存&lt;/strong&gt;，留出系统余量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.nodemanager.resource.detect-hardware-capabilities&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;是否自动探测硬件并配置上面两项&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;容器资源范围（RM/Scheduler 层面）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.minimum-allocation-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;单个 Container 最小内存，申请会向上取整到它的倍数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;单个 Container 最大内存，超过会被拒绝或降级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.minimum-allocation-vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;单个 Container 最小 vcore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yarn.scheduler.maximum-allocation-vcores&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;单个 Container 最大 vcore&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::caution
&lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt; 默认 8192，很多任务向 Spark 申请 driver 10G 内存直接失败或被杀。改这个参数时要同步评估单节点能同时跑几个大 Container，防止内存超卖。
:::&lt;/p&gt;
&lt;h3&gt;内存与调度行为&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;yarn.nodemanager.pmem-check-enabled&lt;/code&gt; / &lt;code&gt;yarn.nodemanager.vmem-check-enabled&lt;/code&gt;：是否开启物理内存/虚拟内存超限检查，默认 true。任务实际内存超申请值会被 NM 直接 kill&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yarn.nodemanager.pmem-check-enabled=false&lt;/code&gt; 是&quot;容器被杀&quot;的常见临时规避手段，但不建议生产关掉&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yarn.resourcemanager.scheduler.class&lt;/code&gt;：指定调度器类，如 &lt;code&gt;org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yarn.nodemanager.local-dirs&lt;/code&gt; / &lt;code&gt;yarn.nodemanager.log-dirs&lt;/code&gt;：本地临时目录与日志目录，多磁盘逗号分隔&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;队列配置（capacity-scheduler.xml）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;yarn.scheduler.capacity.root.queues&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;default,etl&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;yarn.scheduler.capacity.root.default.capacity&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;60&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;yarn.scheduler.capacity.root.etl.capacity&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;40&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;yarn.scheduler.capacity.root.etl.maximum-capacity&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;60&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&amp;lt;property&amp;gt;
  &amp;lt;name&amp;gt;yarn.scheduler.capacity.root.etl.acl_submit_applications&amp;lt;/name&amp;gt;
  &amp;lt;value&amp;gt;etl_user&amp;lt;/value&amp;gt;
&amp;lt;/property&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;capacity&lt;/code&gt;：队列保证容量（子队列之和 = 100）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maximum-capacity&lt;/code&gt;：弹性上限，防止独占全部资源&lt;/li&gt;
&lt;li&gt;修改后 &lt;code&gt;yarn rmadmin -refreshQueues&lt;/code&gt; 热生效&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;资源抢占（Preemption）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;开启后，当队列使用超过 &lt;code&gt;maximum-capacity&lt;/code&gt; 时，RM 会杀掉抢占队列中过期的容器（&lt;code&gt;yarn.resourcemanager.scheduler.monitor.enable=true&lt;/code&gt; + 配置 ProportionalCapacityPreemptionPolicy）&lt;/li&gt;
&lt;li&gt;参数：&lt;code&gt;yarn.resourcemanager.monitor.capacity.preemption.monitoring-interval&lt;/code&gt;（检查周期）、&lt;code&gt;yarn.resourcemanager.monitor.capacity.preemption.max-wait-before-kill&lt;/code&gt;（宽限期，默认 15s）&lt;/li&gt;
&lt;li&gt;抢占会杀容器，对跑长任务的流作业不友好，生产需谨慎评估&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题排查&lt;/h2&gt;
&lt;h3&gt;任务卡在 ACCEPTED / 调度不分配容器&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：作业提交后长时间 &lt;code&gt;ACCEPTED&lt;/code&gt; 或 &lt;code&gt;RUNNING&lt;/code&gt; 但 task 一个没启动&lt;/li&gt;
&lt;li&gt;排查步骤：
&lt;ol&gt;
&lt;li&gt;ResourceManager UI（8088 端口）看作业所在队列的&lt;strong&gt;资源使用情况&lt;/strong&gt;：Pending 容器多说明申请排队了&lt;/li&gt;
&lt;li&gt;看队列是不是被其他作业占满（尤其大作业或&quot;资源泄露&quot;的僵尸任务）&lt;/li&gt;
&lt;li&gt;确认申请的容器内存是否超过 &lt;code&gt;yarn.scheduler.maximum-allocation-mb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;看 AM 是否启动成功：AM 反复重启会导致作业一直 RUNNING 没进展（见下）&lt;/li&gt;
&lt;li&gt;检查用户是否有队列提交权限（ACL 拒绝会直接失败而不是排队）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;容器被杀 / 内存溢出&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：日志里出现 &lt;code&gt;Container killed by the ApplicationMaster&lt;/code&gt;、&lt;code&gt;Process tree is exceeding the physical memory limit&lt;/code&gt;、&lt;code&gt;OutOfMemoryError&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;原因与处理：
&lt;ul&gt;
&lt;li&gt;任务实际内存超过申请值 → 调大 &lt;code&gt;mapreduce.map.memory.mb&lt;/code&gt; / &lt;code&gt;spark.executor.memory&lt;/code&gt; 等任务内存参数&lt;/li&gt;
&lt;li&gt;虚拟内存超限（vmem）常因 JVM 预留空间大触发，可评估 &lt;code&gt;yarn.nodemanager.vmem-pmem-ratio&lt;/code&gt; 是否调大&lt;/li&gt;
&lt;li&gt;确认是不是单个 Container 申请超过 maximum-allocation（申请被降级或拒绝）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;先看容器日志（&lt;code&gt;logs/userlogs/&lt;/code&gt;）里的 JVM 错误，再决定调哪个参数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;节点资源未释放&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：RM UI 显示节点内存被占满，但实际没有任务在跑&lt;/li&gt;
&lt;li&gt;原因：
&lt;ul&gt;
&lt;li&gt;客户端 kill 作业但 AM 没收到信号，任务进程还活着&lt;/li&gt;
&lt;li&gt;NM 与 RM 心跳中断，NM 状态没及时更新（看节点状态是 Lost 还是 Healthy）&lt;/li&gt;
&lt;li&gt;容器所在进程残留（&lt;code&gt;jps&lt;/code&gt; 看是否有 Java 进程未退出），杀掉后资源自动回收&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;处理：&lt;code&gt;yarn application -kill &amp;lt;appid&amp;gt;&lt;/code&gt; 强杀；再不行重启对应 NM&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;队列配置错误&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：作业提交时报 &lt;code&gt;Queue ... doesn&apos;t exist&lt;/code&gt;、&lt;code&gt;Failed to submit application&lt;/code&gt;、&lt;code&gt;ACL ... is denied&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;排查：检查 &lt;code&gt;yarn.scheduler.capacity.root.queues&lt;/code&gt; 是否包含目标队列名；子队列 capacity 之和是否 100%；ACL 用户是否匹配&lt;/li&gt;
&lt;li&gt;修改配置后务必 &lt;code&gt;yarn rmadmin -refreshQueues&lt;/code&gt; 并在 RM UI 确认生效&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AM 反复重启&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：RM UI 上 &lt;code&gt;AM Container: failed&lt;/code&gt;，作业反复从 0 重跑&lt;/li&gt;
&lt;li&gt;常见原因：
&lt;ul&gt;
&lt;li&gt;AM 申请的内存小于实际需要 → 被 kill&lt;/li&gt;
&lt;li&gt;依赖的 jar/资源在 AM 节点上找不到（本地化失败）&lt;/li&gt;
&lt;li&gt;AM 启动阶段抛异常（代码问题、配置项拼错）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yarn.resourcemanager.am.max-attempts&lt;/code&gt;（默认 2）内没起来就彻底失败&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;排查：看 RM 日志中该 AppAttempt 的失败原因，重点看 AM 容器的 stderr&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;磁盘心跳（Disk Health）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;NM 会对本地目录做磁盘健康检查，&lt;code&gt;yarn.nodemanager.disk-health-checker.enabled&lt;/code&gt; 默认开启&lt;/li&gt;
&lt;li&gt;某个磁盘满了或写失败，该盘会被标记坏盘，NM 上报给 RM；坏盘过多（&lt;code&gt;yarn.nodemanager.max-disk-utilization-per-disk-percentage&lt;/code&gt;）会导致整个节点被标记为 unhealthy，RM 停止向它分配容器&lt;/li&gt;
&lt;li&gt;现象：节点状态 unhealthy、容器一直起不来&lt;/li&gt;
&lt;li&gt;处理：清理磁盘/更换坏盘，NM 会自动恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;RM 单点故障 / HA 切换&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;现象：RM 进程挂了，所有作业停摆；HA 场景下主动切换后作业中断&lt;/li&gt;
&lt;li&gt;注意：&lt;strong&gt;RM 切换不丢作业状态&lt;/strong&gt;（依赖 ZK），但 AM 和 task 会中断重跑&lt;/li&gt;
&lt;li&gt;排查：看 ZK 中 &lt;code&gt;yarn-leader-election&lt;/code&gt; 节点，检查两个 RM 的 Active/Standby 状态（&lt;code&gt;yarn rmadmin -getAllServiceState&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;参数：&lt;code&gt;yarn.resourcemanager.zk-addresses&lt;/code&gt;、&lt;code&gt;yarn.resourcemanager.ha.enabled&lt;/code&gt;，ZK 抖动是 RM 频繁切换的常见原因&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
Yarn 排障三板斧：① RM UI（8088）看队列资源与容器状态；② &lt;code&gt;yarn logs -applicationId &amp;lt;appid&amp;gt;&lt;/code&gt; 看任务日志；③ &lt;code&gt;yarn node -list&lt;/code&gt; / &lt;code&gt;yarn node -status &amp;lt;node&amp;gt;&lt;/code&gt; 看节点健康。UI 永远是第一手信息源。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Yarn 的本质是&lt;strong&gt;资源抽象的通用调度平台&lt;/strong&gt;：把节点资源抽象成 Container，用两级调度（RM→AM→Task）隔离&quot;资源分配&quot;和&quot;任务执行&quot;。理解队列容量与弹性、记住&quot;内存申请超限会被杀&quot;和&quot;资源不释放要先看节点健康&quot;，大部分 Yarn 问题都能快速定位。&lt;/p&gt;
</content:encoded></item><item><title>7.30复盘 - 越涨越安全，越跌越不安全</title><link>https://chaggle.github.io/posts/2026/07/30/review-20260730/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/07/30/review-20260730/</guid><pubDate>Thu, 30 Jul 2026 04:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文链接：&lt;a href=&quot;https://mp.weixin.qq.com/s/aGUOMRns75Y2t6yG_Ht6jQ&quot;&gt;https://mp.weixin.qq.com/s/aGUOMRns75Y2t6yG_Ht6jQ&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自复盘的思考与理解，仅用于记录投资思考，不构成任何投资建议。仅用于记录永恒的方新侠。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;越涨越安全，越跌越不安全&lt;/h2&gt;
&lt;p&gt;很多人亏钱的根源，就是喜欢越跌越补、越跌越扛，误以为低位就是安全，实际上真正的风险，是跌出来的，并且是层层放大、不可逆的。&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;p&gt;第一轮风险临界点，普遍在回撤 &lt;strong&gt;20%&lt;/strong&gt; 左右。一旦跌到这个位置，市场最敏感的两类资金会率先动作：场内融资盘开始主动卸杠杆、规避风险；带有严格止损纪律的短线资金、量化资金会统一触发止损离场；同时前期积累的获利盘也会集中兑现了结。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;这个位置也是唯一的短期分水岭：如果此时有新的主力资金承接抛压、逆势回流，股价能够重新企稳回升，本轮下跌风险就可以顺利化解，趋势还有修复机会。但如果承接无力、资金持续出逃，风险就会彻底扩散，跌幅会快速放大到 &lt;strong&gt;30%-40%&lt;/strong&gt; 区间。&lt;/p&gt;
&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;当回撤来到 &lt;strong&gt;30%-40%&lt;/strong&gt;，多空会彻底质变。所有坚持回撤风控的机构资金、稳健资金会选择彻底清仓离场；融资盘踩踏式卸杠杆的行为进一步加剧；同时持仓亏损扩大引发基民恐慌赎回，基金经理为了应对大规模赎回，只能被动无脑卖出持仓个股，完全不计成本，基本面已经完全失效，涨跌不再可控。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;而一旦跌幅击穿 &lt;strong&gt;50%&lt;/strong&gt; 及以上，个股就会进入不可逆的螺旋下跌结构，彻底失去自我修复能力。原本左侧进场的抄底盘全部被套，随后变成新的止损抛压，源源不断涌出；融资盘濒临爆仓底线，被动强平盘持续砸盘；基民恐慌性赎回形成负反馈闭环，多重抛压叠加，下跌只会越演越烈。&lt;/p&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;这也是为什么&lt;strong&gt;守住趋势至关重要&lt;/strong&gt;，个股在趋势完好时，股价由基本面、逻辑、题材决定；一旦趋势彻底破位，后面的价格和基本面毫无关系，完全由资金情绪、被动抛压、负反馈螺旋主导。&lt;/p&gt;
&lt;p&gt;而螺旋下跌最残忍的地方在于：它最终一定会跌穿所有人的心理底线、成本底线和认知底线，绝大多数人都会在极致恐慌中被动割在最低。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;市场给了两次救命离场的机会&lt;/h2&gt;
&lt;p&gt;整场下跌行情里，市场其实给了所有人两次救命离场的机会：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一次救命机会&lt;/strong&gt;：在盘面风雨欲来、趋势刚刚走弱、风险初现的阶段，主动大幅降低总仓位，提前规避后续的系统性踩踏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二次救命机会&lt;/strong&gt;：在趋势彻底确认无法修复、反弹无力、重心持续下移后，果断二次降仓，出清剩余风险仓位，彻底告别负反馈行情。&lt;/p&gt;
&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;如果这两次关键的自救机会全部错过，后续就再也没有挽回的余地，只能被动深陷深套的恶性循环，完全丧失交易主动权。&lt;/p&gt;
&lt;p&gt;丧失主动权之后，后面新的回血机会即便出现了，也与你无关了，因为你已经无法靠自己脱离这个螺旋漩涡。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;在这两个救命稻草之外，还有一条救命稻草，那就是持之以恒的蚂蚁搬家。我们能够在风雨中保存下大部分实力，靠的就是从 24 年开始不间断的持续的蚂蚁搬家，早早的搬走本金，才能在如此巨大的回撤中保持镇定。&lt;/p&gt;
&lt;h2&gt;市场的问题在哪儿？&lt;/h2&gt;
&lt;p&gt;当下整个&lt;strong&gt;科技AI板块&lt;/strong&gt;的核心症结，本质是产业链出现了严重的&lt;strong&gt;供需错配与节奏断层&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;上游算力、硬件端资本开支疯狂扩张，产能、设备投入节奏极快，但下游 AI 应用的落地速度、商业化变现速度远远跟不上上游扩张节奏，中间形成了巨大的时间差。&lt;/p&gt;
&lt;p&gt;又叠加了棒子一群蠢猪，不懂杠杆的危害，资金集中踩踏，叠加外资杠杆比例过高引发局部股灾，负面情绪快速传导，最终演化成席卷全球科技板块的一轮系统性调整，整个 AI 硬件赛道随之深度回调。&lt;/p&gt;
&lt;h2&gt;两条破局路径&lt;/h2&gt;
&lt;p&gt;站在当下，想要彻底破解当前的产业困局、终结这轮调整行情，市场只有两条可行的破局路径：&lt;/p&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一条&lt;/strong&gt;，诞生全新的技术突破与产业叙事，创造出超预期、超大规模的增量算力需求，重新拉高市场对硬件端的成长预期，重塑整条产业链的估值体系。但从当前产业进度来看，短期暂无颠覆性技术迭代与全新叙事落地，这条路径短期无法兑现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二条&lt;/strong&gt;，下游 AI 应用迎来规模化爆发。只有应用端场景持续落地、商业化需求持续攀升，才能反向反哺上游硬件、消化过剩算力产能，修复产业链供需失衡的问题，彻底打消市场疑虑。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;基于这两个点出发，我们从周一开始寻找破局之道。&lt;/p&gt;
&lt;p&gt;硬件新的叙事和突破暂时没有出现，那么唯一的解就在&lt;strong&gt;应用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;现阶段绝大多数 AI 应用标的经过长期调整，筹码干净、位置极致低位，没有高位套牢盘和潜伏资金，具备绝佳的布局性价比。无论是市场资金、机构资金，还是上游硬件厂商，当下都高度期盼应用端的行情爆发。&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对资金而言，应用是当前唯一有预期差、有反转空间的主线；&lt;/li&gt;
&lt;li&gt;对硬件厂商而言，只有应用落地、需求爆发，才能盘活自身算力产能、修复经营压力。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以超前布局低位 AI 应用，不是单纯的题材炒作，而是整个产业集体自救、抱团修复的最优解。我们周一开始超前布局应用，目前基本上都是正反馈，不但没有亏钱，还能持续贡献利润。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;关于明天&lt;/h2&gt;
&lt;p&gt;确实跌太多了，这里总该有个反弹吧。&lt;/p&gt;
&lt;p&gt;今天美股上涨，如果能持续到收盘不回落，那么明天情绪上支持科技反弹一波。只是如果在反弹的过程中没有解决上面提到的两个长期问题，那么反弹还是卖点。&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;p&gt;所有错过两次救命机会的，要注意这个卖点。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>【主线追踪】 - 三星60万亿韩元物理AI集群：四大受益链条与标的全梳理</title><link>https://chaggle.github.io/posts/2026/07/05/main-invest-samsung-ai-cluster-20260705/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/07/05/main-invest-samsung-ai-cluster-20260705/</guid><pubDate>Sun, 05 Jul 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自主线追踪，仅用于记录永恒的六一中路&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;事件背景&lt;/h2&gt;
&lt;p&gt;2026年7月3日，三星官宣岭南区域60万亿韩元专项投资，打造全球物理 AI（Physical AI）集群，由三星电子、三星SDI、三星电机、三星重工、三星SDS联合落地，核心四大赛道：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;人形机器人 + AI数据工厂（龟尾基地）&lt;/strong&gt;：人形机器人量产、机器人仿真数据中心、边缘算力产线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全固态电池 + 储能（蔚山三星SDI）&lt;/strong&gt;：人形机器人固态电池、AI机房储能、钠电池扩产&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI服务器核心元器件（釜山三星电机）&lt;/strong&gt;：高端MLCC、ABF封装基板、HBM配套材料&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI智能化船舶工业（巨济三星重工）&lt;/strong&gt;：工业机器人、数字孪生制造设备&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;物理AI本质：算力硬件 + 实体智能硬件一体化，先扩产HBM/先进封装支撑云端算力，再落地人形机器人、工业自动化等实体智能终端。整条产业链分为&lt;strong&gt;半导体算力层、元器件层、机器人硬件层、电池能源层&lt;/strong&gt;四大受益链条。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;HBM/先进存储与先进封装（订单确定性最强）&lt;/h2&gt;
&lt;p&gt;三星集群首要配套：AI服务器HBM存储、2.5D/3D先进封装。三星本土扩产HBM产线，国内已进入三星供应链的封测、材料、存储芯片企业直接受益。&lt;/p&gt;
&lt;h3&gt;封测龙头&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;通富微电&lt;/strong&gt;：国内HBM封装龙头，拿下三星HBM约45%外包份额，HBM3大规模量产，2.5D堆叠工艺成熟。三星扩产HBM直接拉动封测订单，同时供货AMD算力芯片，双线受益AI算力。HBM堆叠层数从8层升级16层，封测价值量翻倍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;深科技&lt;/strong&gt;：子公司沛顿是三星国内最大存储封测伙伴，专门预留HBM测试产线，承接三星DRAM/NAND/HBM全系列封测，消息刺激后短期弹性极强。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;长电科技&lt;/strong&gt;：全球第三封测厂，稳定供货三星、SK海力士HBM 2.5D封装，TSV硅通孔技术适配高阶HBM，海外韩厂客户订单持续放量。&lt;/p&gt;
&lt;h3&gt;HBM专用封装材料（壁垒最高）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;华海诚科&lt;/strong&gt;：国内唯一量产HBM级GMC环氧塑封料，已通过三星、SK海力士双认证，适配12-16层高阶HBM堆叠。HBM对低翘曲、高流动性塑封料刚需，产能紧缺。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;德邦科技&lt;/strong&gt;：HBM底部填充胶、导热界面材料龙头，多层堆叠芯片散热/应力缓冲必备，单颗芯片耗材价值远高于普通存储，持续对接三星验证。&lt;/p&gt;
&lt;h3&gt;内存接口芯片&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;澜起科技&lt;/strong&gt;：三星为第一大客户，收入占比超30%。DDR5/MRCD内存接口芯片供给三星HBM配套内存模组，CXL互联芯片进入三星AI服务器方案，三星创投间接持股，资本+业务双重绑定。&lt;/p&gt;
&lt;h2&gt;AI服务器元器件（MLCC/封装基板）&lt;/h2&gt;
&lt;p&gt;三星电机60万亿配套产能全部面向AI服务器、人形机器人端侧硬件。两大核心细分：高端MLCC、ABF载板/高速PCB。&lt;/p&gt;
&lt;h3&gt;高端MLCC&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;风华高科&lt;/strong&gt;：国内MLCC龙头，高频高容车规/AI服务器MLCC实现国产替代。三星电机全球扩产MLCC推升行业涨价周期，AI算力、机器人设备拉高高端电容需求，行业供需收紧。&lt;/p&gt;
&lt;h3&gt;ABF封装基板与高速PCB&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;兴森科技&lt;/strong&gt;：国内少数通过三星FCBGA封装基板认证厂商，HBM、AI GPU底层基板核心耗材，三星釜山基板基地扩产带动上游基材需求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;深南电路&lt;/strong&gt;：AI服务器高速PCB龙头，批量供货海外算力厂商，三星自建AI数据中心配套服务器PCB需求持续释放。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生益科技&lt;/strong&gt;：覆铜板CCL龙头，ABF载板、高速PCB核心原材料。AI服务器CCL用量是传统服务器3-5倍，上游耗材量价齐升。&lt;/p&gt;
&lt;h2&gt;半导体设备与半导体材料&lt;/h2&gt;
&lt;p&gt;三星岭南新建机器人芯片产线、HBM封装厂，建厂70%成本投入设备与材料。&lt;/p&gt;
&lt;h3&gt;设备标的&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;赛腾股份&lt;/strong&gt;：控股日本Optima晶圆检测设备批量交付三星，国内稀缺打入韩厂高端检测供应链。HBM多层堆叠检测工序随层数同步增加，设备需求同步扩张。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;精测电子&lt;/strong&gt;：三星存储量测设备长期供应商，HBM良率检测刚需，三星扩产存储直接拉动检测设备订单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;中微公司/拓荆科技&lt;/strong&gt;：刻蚀、薄膜沉积设备持续导入三星存储产线，国产替代长期逻辑。&lt;/p&gt;
&lt;h3&gt;半导体材料&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;雅克科技&lt;/strong&gt;：ALD前驱体龙头，三星、SK海力士HBM薄膜沉积核心耗材供应商，存储扩产带动前驱体长期增量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;江丰电子&lt;/strong&gt;：高纯钨/钼/硅靶材龙头，供货三星存储晶圆厂，HBM薄膜制程靶材消耗大幅提升。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;沪硅产业（上海新昇）&lt;/strong&gt;：12英寸硅片龙头，向三星送样认证，三星新建存储晶圆厂推高全球12英寸硅片需求，行业价格拐点向上。&lt;/p&gt;
&lt;h2&gt;物理AI人形机器人全产业链&lt;/h2&gt;
&lt;p&gt;三星60万亿重点布局人形机器人量产基地（龟尾），机器人分为感知层、执行层、电池能源三条细分。&lt;/p&gt;
&lt;h3&gt;执行层（减速器/伺服）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;绿的谐波&lt;/strong&gt;：人形机器人谐波减速器龙头，全球机器人厂商通用供应链，三星人形量产带动减速器增量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;双环传动&lt;/strong&gt;：RV减速器国产龙头，工业、人形机器人双路线放量。&lt;/p&gt;
&lt;h3&gt;感知层（3D视觉）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;奥比中光&lt;/strong&gt;：全栈3D深度相机，适配机器人空间感知，英伟达Isaac物理AI仿真平台配套硬件，人形机器人视觉刚需。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;凌云光&lt;/strong&gt;：工业光学动捕、3D视觉，机器人动作标定、物理仿真训练必备。&lt;/p&gt;
&lt;h3&gt;固态电池与储能配套&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;容百科技&lt;/strong&gt;：固态电池正极材料龙头，对接三星SDI全固态电池研发供应链。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;英维克/高澜股份&lt;/strong&gt;：AI数据中心液冷散热，三星新建机器人数据机房、算力集群温控刚需。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;鹏辉能源&lt;/strong&gt;：储能电池配套AI机房备用电源，储能系统配套三星算力集群。&lt;/p&gt;
&lt;h2&gt;标的梯队分级&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一梯队（直接进入三星供应链，短期订单兑现最强）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通富微电、深科技（HBM封测） | 澜起科技（三星第一大客户内存芯片） | 华海诚科（HBM塑封料双韩厂认证） | 赛腾股份、精测电子（三星半导体设备直供）&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二梯队（行业周期受益，三星扩产拉动全球供需涨价）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;风华高科（高端MLCC涨价） | 生益科技、兴森科技（封装基板/CCL耗材） | 雅克科技、江丰电子（半导体耗材） | 沪硅产业（12英寸硅片供需收紧）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三梯队（长期物理AI人形机器人远期弹性标的）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;绿的谐波、双环传动、奥比中光、英维克、容百科技&lt;/p&gt;
&lt;h2&gt;风险提示&lt;/h2&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;三星产能全部本土布局，国内企业仅上游耗材/设备外包供货，整机终端订单有限&lt;/li&gt;
&lt;li&gt;半导体行业周期波动，HBM扩产若引发存储价格回落，压制企业毛利率&lt;/li&gt;
&lt;li&gt;国产设备、材料三星认证进度不及预期，订单落地延迟&lt;/li&gt;
&lt;li&gt;韩美半导体出口管制政策变化，影响国内企业供货三星&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;原文&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;原创文章&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>对近期市场的几点理解和推测（2026年6月29日）</title><link>https://chaggle.github.io/posts/2026/06/30/market-thought-20260630/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/06/30/market-thought-20260630/</guid><pubDate>Tue, 30 Jun 2026 04:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自市场理解，仅用于记录投资思考，不构成任何投资建议。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;科技股分化&lt;/h2&gt;
&lt;p&gt;科技股大面积下跌，分化开始了，而不是普涨。核心个股才是机会，特别是行业龙头，产品市占率高，甚至产品是无可替代的才是核心。&lt;/p&gt;
&lt;h2&gt;医药板块承接力存疑&lt;/h2&gt;
&lt;p&gt;今天科技板块下跌，资金流动到医药等板块，目前来看，医药不具备承接科技成为主流题材的条件，主要还是消息刺激，持续性成疑。多点清醒，不冲动。&lt;/p&gt;
&lt;h2&gt;敢于低吸&lt;/h2&gt;
&lt;p&gt;敢于低吸这波科技深水区的，特别是核心的个股，都会得到不错的回报。&lt;/p&gt;
&lt;h2&gt;韩国三星/SK海力士大扩产&lt;/h2&gt;
&lt;p&gt;韩国三星和SK海力士宣布投资4755万亿韩元（约20万亿元人民币），真是人有多大胆地有多大产，20万亿+的水平，叹为观止。&lt;/p&gt;
&lt;h2&gt;最先受益：半导体设备与材料&lt;/h2&gt;
&lt;p&gt;三星和SK海力士要大扩产，最先受益的是给他们提供半导体设备和半导体材料的厂家。&lt;/p&gt;
&lt;h2&gt;卖方市场来临&lt;/h2&gt;
&lt;p&gt;存储巨头开启大规模扩产，说明半导体设备和半导体材料将进入卖方市场，但是，我们要特别强调的是，&lt;strong&gt;只有行业龙头才有议价权&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;p&gt;核心逻辑：行业龙头 + 议价权 = 确定性机会&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;不要被震下车&lt;/h2&gt;
&lt;p&gt;在今天科技板块反复拉锯的过程中，应该有不少人被震下车了。有些核心个股，一旦下去就不好再上车了。&lt;/p&gt;
&lt;h2&gt;存储板块最淡定&lt;/h2&gt;
&lt;p&gt;今天科技板块调整过程中，存储板块是表现最为淡定的，中军龙头又继续创新高，成为资金抱团对象。&lt;/p&gt;
&lt;h2&gt;玻璃基板刚起步&lt;/h2&gt;
&lt;p&gt;玻璃基板今天调整的比较多。其实放长周期来看，玻璃基板现在才刚刚起步，后续还是有空间的。&lt;/p&gt;
&lt;h2&gt;什么是核心？&lt;/h2&gt;
&lt;p&gt;举个例子：产品市场占有率及国内外排名这么硬刚的公司。&lt;/p&gt;
&lt;h3&gt;MO源（金属有机前驱体）——全球绝对龙头&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;市占率：全球35%–40%（第一），国内 &lt;strong&gt;&amp;gt;60%&lt;/strong&gt;（绝对龙头）&lt;/li&gt;
&lt;li&gt;行业地位：全球最大MO源供应商，全球唯一全系列MO源量产企业&lt;/li&gt;
&lt;li&gt;纯度达6N–7N（99.9999%+），对标杜邦、陶氏&lt;/li&gt;
&lt;li&gt;客户覆盖台积电、三星、三安光电等全球头部厂商&lt;/li&gt;
&lt;li&gt;核心优势：2020年并购杜邦19项核心专利（含6项全球首创），技术跻身国际一线；毛利率长期50%+，是稳定现金牛&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;高纯电子特气——国内龙头、全球第一梯队&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;磷烷（PH₃）&lt;/strong&gt;：全球市占第一，国内市占 &lt;strong&gt;&amp;gt;40%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;砷烷（AsH₃）&lt;/strong&gt;：全球市占第二，国内市占 &lt;strong&gt;&amp;gt;40%&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三氟化氮（NF₃）&lt;/strong&gt;：全球产能第三，国内市占 &lt;strong&gt;≈35%&lt;/strong&gt;（第一梯队）&lt;/li&gt;
&lt;li&gt;行业地位：国内唯一7N级高纯砷烷/磷烷量产企业；氢类特气国内市占 &lt;strong&gt;&amp;gt;60%&lt;/strong&gt;；2025年电子特气收入15.36亿元（占总营收59.39%），为第一大收入来源&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ArF光刻胶（193nm）——国内唯一量产龙头&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;市占率：国内 &lt;strong&gt;≈80%&lt;/strong&gt;（国产份额），整体国内市占≈10%；全球市占&amp;lt;5%（日企垄断90%+）&lt;/li&gt;
&lt;li&gt;行业地位：国内唯一实现28nm及以上先进制程ArF光刻胶量产企业&lt;/li&gt;
&lt;li&gt;产品适配90–28nm逻辑/存储芯片，3款通过头部晶圆厂全流程验证&lt;/li&gt;
&lt;li&gt;宁波500吨/年产线2026年达产，产能扩10倍&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;综合行业地位&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;国内唯一同时覆盖前驱体、电子特气、光刻胶三大核心半导体材料的平台型企业&lt;/li&gt;
&lt;li&gt;国家&quot;专精特新&quot;小巨人、制造业单项冠军，承担36项国家重大科技专项&lt;/li&gt;
&lt;li&gt;进入中芯国际、长江存储、华虹等所有头部晶圆厂供应链，国产替代核心标的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;什么是核心？产品市场占有率及国内外排名靠前、技术不可替代、进入全球头部供应链——这才是真正的核心标的。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;同时供货三大巨头的核心个股&lt;/h2&gt;
&lt;p&gt;三星和SK海力士要大扩产，最先受益的是给他们提供半导体设备和半导体材料的厂家。A股中，能做到同时供货三星和SK海力士的个股并不多，如果能同时供应三星、SK海力士和美光这三大巨头，那就是核心个股。&lt;/p&gt;
&lt;h2&gt;AI创新突破与智算集群&lt;/h2&gt;
&lt;p&gt;要加力推进人工智能创新突破，加快关键技术攻关和超大规模智算集群建设。这不是新口号，市场是否认可值得商榷。&lt;strong&gt;近期还是以半导体设备和半导体材料为主。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;警惕&quot;蹭热度&quot;个股&lt;/h2&gt;
&lt;p&gt;对于&quot;传闻&quot;有某些材料或者设备的个股要多一个心眼，因为近期上面对于&quot;蹭热度&quot;管控力度非常大，一般的公司都怕惹上麻烦，都会主动&quot;澄清&quot;，所以，还是得盯着真正有主营产品市占率高的，或者不可替代的公司。&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;p&gt;紧盯真正有主营产品市占率高、不可替代的公司，远离传闻股。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;韩国投资规模&lt;/h2&gt;
&lt;p&gt;韩国到底要投资多少万亿（核心是三星和SK海力士），好像众说纷纭，每家媒体说的数字不太一样，有的是说今年，有的是说10年远景投资，但是总体上来说，&lt;strong&gt;韩国这次是卯足了劲要搞科技发展，决心非常大。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;稀土管控的连锁反应&lt;/h2&gt;
&lt;p&gt;在思考一个问题：我们对小日子管控稀土和稀散小金属等，所以导致目前小日子的很多半导体材料要么大涨要么停产。韩国很多上游材料也是需要从我们这里采购，他们就不用考虑这个问题吗？&lt;/p&gt;
&lt;h2&gt;中报业绩预告窗口&lt;/h2&gt;
&lt;p&gt;A股中报业绩预告，从6月中下旬开始，到7月15日截至（注意是预告），而7月上半月会最密集，所以，接下来资金对业绩预报的关注度会增加，资金关注的核心是什么？就是 &lt;strong&gt;半导体设备/材料 + 业绩大幅预增&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;主线方向：半导体设备 / 半导体材料 + 中报业绩大幅预增&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>2026年6月30日（周二）每日一股逻辑分析：雅克科技</title><link>https://chaggle.github.io/posts/2026/06/30/daily-stock-yake-tech-20260630/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/06/30/daily-stock-yake-tech-20260630/</guid><pubDate>Tue, 30 Jun 2026 04:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自每日一股逻辑分析，仅用于记录投资思考，不构成任何投资建议。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;今日选股思路&lt;/h2&gt;
&lt;p&gt;今天选择比较难，因为很多科技核心龙头近期涨幅都比较大，盘中低吸为主。&lt;/p&gt;
&lt;p&gt;今天分享的是&lt;strong&gt;雅克科技&lt;/strong&gt;：A股唯一批量供货三星、SK海力士和美光三大存储原厂的HBM前驱体材料企业。&lt;/p&gt;
&lt;p&gt;雅克科技是国内电子材料与LNG（液化天然气）保温绝热板材领域的&quot;双龙头&quot;企业，目前已成功转型为一家平台级的全球半导体材料企业。&lt;/p&gt;
&lt;h2&gt;主营业务板块&lt;/h2&gt;
&lt;p&gt;公司的核心业务主要分为两大板块，并形成了**&quot;前驱体 + 电子特气 + 光刻胶 + LNG板材 + 湿化学品 + 洗净服务&quot;**的全链条产品矩阵。&lt;/p&gt;
&lt;h3&gt;电子材料业务（核心主业）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;半导体前驱体材料&lt;/strong&gt;：深度绑定SK海力士、三星、美光等国际巨头，是HBM（高带宽存储器）芯片的核心配套材料供应商&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光刻胶及配套试剂&lt;/strong&gt;：产品涵盖正性TFT光刻胶、RGB彩色光刻胶等，客户涵盖京东方、TCL华星、LG Display等头部面板厂&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;含氟类特种气体&lt;/strong&gt;：主要产品为四氟化碳和六氟化硫&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他电子材料&lt;/strong&gt;：包括半导体封装粉体填充料、电子粉体材料（如球形硅微粉）、半导体材料输送系统（LDS）以及半导体湿化学品（显影液、蚀刻液等）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;LNG 保温绝热板材业务&lt;/h3&gt;
&lt;p&gt;核心产品为聚氨酯泡沫及增强复合材料，主要用于大型LNG运输船及陆上储罐。公司已打破国外垄断，是国内独家为LNG大型运输船提供关键材料配套的企业，已与沪东中华造船、江南造船等签订大量订单。&lt;/p&gt;
&lt;h2&gt;行业地位与竞争优势&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;业务板块&lt;/th&gt;
&lt;th&gt;国内市占率&lt;/th&gt;
&lt;th&gt;全球市占率&lt;/th&gt;
&lt;th&gt;排名&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;半导体前驱体&lt;/td&gt;
&lt;td&gt;≈35%&lt;/td&gt;
&lt;td&gt;8%–10%&lt;/td&gt;
&lt;td&gt;国内第一，全球第三&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;显示光刻胶&lt;/td&gt;
&lt;td&gt;≈28%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;国内第一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;含氟电子特气&lt;/td&gt;
&lt;td&gt;第一&lt;/td&gt;
&lt;td&gt;≈18%&lt;/td&gt;
&lt;td&gt;国内第一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LNG保温复合材料&lt;/td&gt;
&lt;td&gt;≈70%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;国内第一，全球第四&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;全球仅有的4家通过法国GTT认证的LNG保温材料供应商之一。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;核心竞争壁垒&lt;/h2&gt;
&lt;h3&gt;全链条国产化能力&lt;/h3&gt;
&lt;p&gt;公司具备前驱体金属原料、光刻胶色浆树脂等&lt;strong&gt;自研自产&lt;/strong&gt;能力，成本低于外资15%–20%。&lt;/p&gt;
&lt;h3&gt;极高的客户认证壁垒&lt;/h3&gt;
&lt;p&gt;进入台积电、三星等头部供应链的认证周期通常&lt;strong&gt;超过3年&lt;/strong&gt;，一旦进入难以被替代。&lt;/p&gt;
&lt;h3&gt;A股唯一性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A股唯一&lt;/strong&gt;批量供货三星、SK海力士和美光三大存储原厂的HBM前驱体材料企业——这正是当前韩国大规模扩产背景下最具确定性的受益标的。&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;p&gt;核心逻辑：韩国三星 + SK海力士大扩产 → 前驱体需求暴增 → 雅克科技作为A股唯一同时供货三大巨头的HBM前驱体企业，确定性最强。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>【主线追踪】 - 分阶段分仓位法：一种高效灵活的建仓获利方法</title><link>https://chaggle.github.io/posts/2026/06/19/main-invest-phased-position-building-20260619/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/06/19/main-invest-phased-position-building-20260619/</guid><pubDate>Fri, 19 Jun 2026 11:44:42 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自主线追踪，仅用于记录永恒的六一中路&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;今天开始，我们准备陆陆续续跟大家聊一聊&quot;分阶段分仓位法&quot;的应用。&quot;分阶段分仓位法&quot;是我们团队独创的操作方法。这种方法在行情好的时候可以快速获利，而在行情差的时候却能避免单纯的止损割肉而以时间和灵活仓位来换取获利的空间。这两三年中，我们也是应用此方法而收获颇丰。&lt;/p&gt;
&lt;p&gt;在我们平常操作个股中，大家往往会发现，如果大仓位去买一只票，往往短期内都是亏损的，割肉的概率是比较大的；而那些获利的票，往往都是投入仓位比较小的。这也是在A股中很难有散户能长期获利的原因。如何解决这种症结，&quot;分阶段分仓位法&quot;便是利器。&lt;/p&gt;
&lt;h2&gt;什么是&quot;分阶段分仓位法&quot;&lt;/h2&gt;
&lt;p&gt;所谓&quot;分阶段分仓位法&quot;，是一个两维的思路，我们可以理解为横轴是&quot;阶段&quot;，纵轴是&quot;仓位&quot;。当您在某一个阶段的时候，可以投入某仓位买入一只票，如果个股下跌，那么在符合条件的另一个阶段时候，再次投入一定仓位加仓，这就是建仓的过程。同样的道理，在某阶段已达成预定目标，就可以将某仓位的个股卖出，完成获利的过程。&lt;/p&gt;
&lt;p&gt;&quot;分阶段分仓位法&quot;，首先要结合自己的资金情况、可接受的亏损情况和可接受的持股时间，来确定分&quot;几个阶段&quot;和分&quot;几成&quot;仓位。&lt;/p&gt;
&lt;h2&gt;举例说明&lt;/h2&gt;
&lt;p&gt;也许这样很难理解，那我们就拿算力金属题材的某只个股来举例。&lt;/p&gt;
&lt;p&gt;假设A股票为算力金属题材的核心中军股，我们看好它，想建仓，但是前段时间它已经有了一定的涨幅了，我们担心买进去就碰到它回调。如果不买，又担心它后面会继续大涨。这个时候，我们就可以用&quot;分阶段分仓位法&quot;来完成建仓。我们以资金100万来举例。&lt;/p&gt;
&lt;h3&gt;确定阶段与仓位&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;首先确定阶段。&lt;/strong&gt; 我们自己一般建仓一只个股是分3个阶段，也就是分3次买入。当然您也可以分4次买入或者5次买入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其次确定仓位。&lt;/strong&gt; 分3次买入，可以第一次2成仓，第二次3成仓，第三次5成仓；也可以第一次3成仓，第二次3成仓，第三次4成仓等。我们一般是选择3-3-4，这种分仓法在实际操作中成功率也比较高。&lt;/p&gt;
&lt;h3&gt;第一次建仓&lt;/h3&gt;
&lt;p&gt;假如我们看上了A股票了，准备就搞它了，那我们可以直接开始第一次建仓，那就是先买入30万元，比如单价是43元，则第一次买入6900股左右。&lt;/p&gt;
&lt;p&gt;接下来就是等待，等待止盈或者加仓。&lt;/p&gt;
&lt;p&gt;止盈的条件也因人而异，看您是想获利10%还是获利5%。我们一般选择止盈指数为1.06，也就是说第一次建仓后，如果获利6%了，我们就选择卖出。43 × 1.06 = 45.58，也就是说股价到45.58元，我们就卖掉它。&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;这是最好的情况——第一次建仓就能达到获利条件，获利30万 × 6% = 1.8万。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;第一次加仓&lt;/h3&gt;
&lt;p&gt;那如果第一次建仓后继续跌呢？这时候就有可能触发加仓。加仓的条件也是自由设定，比如您可以选择下跌10%以后加仓，或者15%以后加仓。&lt;/p&gt;
&lt;p&gt;对于我们来说，一般主板个股设定第一次加仓的条件是下跌8%，也就是说43元建仓后，如果下跌到43 × 0.92 = 39.56元，那就触发了加仓条件，这个时候我们就会在39.56元再买入30万元，也就是7600股左右。&lt;/p&gt;
&lt;p&gt;经过这次加仓后，个股的平均成本为41.21元，这时候止盈的条件就是41.21 × 1.06 = 43.68元。也就是说，经过一次加仓后，自己就持有6成仓位，如果股价达到43.68元就止盈卖掉，获利60万 × 6% = 3.6万。&lt;/p&gt;
&lt;h3&gt;第二次加仓&lt;/h3&gt;
&lt;p&gt;同样的道理，如果经过第一次加仓后，股价还继续下跌，这时候我们就不能在下跌8%就加仓，而应该设置个幅度更大的跌幅比，比如我们设置的就是继续下跌12%，也就是在34.81元这个位置完成第二次加仓，而且这次加仓的仓位是4成。&lt;/p&gt;
&lt;p&gt;加仓后平均成本就变为38.39元，一样是获利6%卖出，所以卖出的条件是股价回到40.69元，可以获利100万 × 6% = 6万。&lt;/p&gt;
&lt;h2&gt;后续思考&lt;/h2&gt;
&lt;p&gt;如果看到这里，您能看懂一半，那么我们就觉得您很有天赋了。因为后面还有很多问题要思考，比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果完成3次建仓后，股票还在跌怎么办？要不要设置止损？&lt;/li&gt;
&lt;li&gt;如果是创业板/科创板个股或者北交所个股，第一次加仓和第二次加仓设置的条件还是8%和12%吗？&lt;/li&gt;
&lt;li&gt;如果第一次建仓后，个股迟迟到不了止盈或者加仓的条件，就一直等下去吗？&lt;/li&gt;
&lt;li&gt;我们能不能分享一个只要填写参数而自动计算的方法？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何选择一只合适的个股？&lt;/strong&gt; 这是最最基本，也是最最重要的课题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;p&gt;这些问题，我们会慢慢跟大家分享。现在愿意跟大家免费分享获利方法的号主真的不多了。请大家记得分享和继续关注。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;原文&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;原创文章&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>AI 的底层能力与个人成长思考</title><link>https://chaggle.github.io/posts/2026/06/07/ai-capability-personal-growth-20260607/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/06/07/ai-capability-personal-growth-20260607/</guid><pubDate>Sat, 06 Jun 2026 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;AI 的底层能力&lt;/h2&gt;
&lt;p&gt;今日又刷到了博主推荐的：&lt;strong&gt;AI 的底层能力是英文能力&lt;/strong&gt;。包括整个软件的架构体系，基本上都是由英文单词构成。小到数据结构，大到操作系统，基本上都是由西方所构成。&lt;/p&gt;
&lt;p&gt;当然，中国也有&lt;strong&gt;易语言&lt;/strong&gt;这样完全由底层逻辑由汉字构成、采用汉字直接编译为二进制代码的语言。但是毕竟中国开发者接受的教育大多还是跟西方保持一致的。&lt;/p&gt;
&lt;p&gt;而且技术的发展也离不开商务文化——毕竟有商务的注资才能推动技术的深入发展。&lt;strong&gt;技术与业务相辅相成&lt;/strong&gt;：技术给业务提供支持，业务反哺技术的发展。&lt;/p&gt;
&lt;p&gt;此外，现代软件发展很充足，软件生态也是同样影响传播的事情。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;重新认识英语学习&lt;/h2&gt;
&lt;p&gt;所以我又开始萌生了每天跟着学学英语的思维方式。正好又看到了 &lt;strong&gt;@京城英语课Jenny老师&lt;/strong&gt; 的博文。她的主页上有一篇文章表述：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;高考英语有完形填空4篇，阅读一篇七选五，语法填空，还有两篇作文。
一个学生读一句话，需要反复读3~5遍才能读明白其中的意思——&lt;strong&gt;说明处理繁杂信息能力较差&lt;/strong&gt;。
在规定时间内要读完所有信息并保证正确率，思考的时间必然会被压缩。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;看到这段内容，深有所感。回想自己的高中生活：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;死活英语卡在 120 分以下的水平上不去&lt;/li&gt;
&lt;li&gt;数学也老是 120-140 分左右，偶尔还会掉到 100 分以下&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每次总结自己为什么上不去，基本上都是&quot;不熟悉题型&quot;&quot;知识结构掌握能力不足&quot;这类表象原因。&lt;/p&gt;
&lt;p&gt;:::note
今日看到这一句话，好像冥冥中的思路被打开了：原来当时是自己的&lt;strong&gt;思维能力处理不了繁杂信息&lt;/strong&gt;，对繁杂信息的理解能力达不到我想要的目标水平。豁然开朗。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;工作带来的能力提升&lt;/h2&gt;
&lt;p&gt;包括为什么现在工作三四年后，感觉自己能力有一个实质上的提升？原来也正是因为自己经历了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;繁杂的事务工作&lt;/li&gt;
&lt;li&gt;极端的运维抗压工作&lt;/li&gt;
&lt;li&gt;尖端的技术调研实践工作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对人、对事都有一个全方面的提升。这样才让自己现在对繁杂信息的快速理解能力，能提升到更高的一个水平。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;家庭与成长环境&lt;/h2&gt;
&lt;p&gt;这可能也是一个普普通通穷小子在 20 岁之前最大的问题：&lt;strong&gt;能经历的太少，锻炼的太少了&lt;/strong&gt;。父母对自己的保护，也是一种限制。&lt;/p&gt;
&lt;p&gt;不过也不怨父母——能带给我的，已经都给我了。毕竟父母也没有得到社会多余的资源投入，所以也同样一直处于一种匮乏的状态，在金钱与时间的处理上一直不够松弛。&lt;/p&gt;
&lt;p&gt;:::tip
这恰好是我现在需要&lt;strong&gt;反哺给家庭的能力&lt;/strong&gt;：带着家庭往松弛的方向走。能力的锻炼与松弛的心态，也是我后续需要走的道路。
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;富裕家庭与资源差距&lt;/h2&gt;
&lt;p&gt;为什么说富裕家庭（如果领导家庭的人员足够有眼光与眼见）的子女，思维本身的能力，都应该比相对贫穷的人要强一点呢？&lt;/p&gt;
&lt;p&gt;我是这样理解的：富裕家庭本身有充足的社会资源去经历——有钱、有人际关系。在某一需求的领域，如果没有像样的资源，家庭会想办法找可信渠道让子女去经历，&lt;strong&gt;经历后还会带着子女复盘&lt;/strong&gt;。毕竟成年人的思维、心智水平比未成年人要高一大截。这也同样很考验一个家庭的心智水平。&lt;/p&gt;
&lt;p&gt;由此也能推到公司层面：私人公司能不能做大做强，更多考验的是&lt;strong&gt;老板的个人能力与个人魅力&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;个人能力&lt;/strong&gt;决定起势&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;个人魅力&lt;/strong&gt;决定运势（此处&quot;运势&quot;更多讲的是团结人心的力量，跟风水、年运没有关联）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;人生的复盘与成长&lt;/h2&gt;
&lt;p&gt;人的一辈子都在做各种的决策，公司的有限生命周期内也是。所以人的思想一定需要在一段时间内不断地复盘，也是大量样本带来的顿悟之感。&lt;/p&gt;
&lt;p&gt;可惜这条路需要走几十年，厚积薄发、大器晚成——只是那段黄金的时间已经过去了。好在，人生不是只有一条道路。所以不管什么时候走，能达到更高的水平就更好。&lt;/p&gt;
&lt;p&gt;:::warning
我不推崇社会达尔文主义，但是我推崇：&lt;strong&gt;每个人应该不断让自己的心智水平比过去更高&lt;/strong&gt;。这样社会的意识形态一样会变好。日后人会意识到自己的问题，即使当时不愿意承认，但内心对自己的行为有自知，自己对自己的觉察能力会更高。
:::&lt;/p&gt;
&lt;p&gt;这样，社会不说马上会变得更好，肯定以后会变好。人性不变，但是&lt;strong&gt;控制人性的人，始终是变化的&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>【主线追踪】 - 近期涨价逻辑最清晰、涨幅最显著的核心品类！</title><link>https://chaggle.github.io/posts/2026/05/18/main-invest-price-increase-categories-20260518/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/05/18/main-invest-price-increase-categories-20260518/</guid><pubDate>Mon, 18 May 2026 15:45:12 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自主线追踪，仅用于记录永恒的六一中路&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;近期受AI算力需求井喷、Token消耗量指数级增长以及上游原材料成本飙升的三重驱动，算力硬件配套与半导体材料领域正经历一场剧烈的&quot;通胀潮&quot;。以下是近期涨价逻辑最清晰、涨幅最显著的核心品类：&lt;/p&gt;
&lt;h2&gt;核心半导体材料与芯片&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;存储芯片（DRAM &amp;amp; NAND）&lt;/strong&gt;：本轮涨价的&quot;震中&quot;。受AI服务器对HBM（高带宽内存）产能的疯狂挤兑，传统存储产能大幅压缩。2026年二季度，DRAM合约价预计环比暴涨58%-63%，NAND Flash涨幅更是高达70%-75%。高盛甚至已将2026年DRAM价格涨幅预测上调至250%-280%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;半导体靶材&lt;/strong&gt;：作为芯片制造的&quot;基石&quot;，用于先进制程与AI芯片的特殊小金属靶材（如钼、钽、钨）价格涨幅普遍突破60%-70%，部分高纯度定制产品甚至逼近翻倍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磷化铟（InP）衬底&lt;/strong&gt;：光通信和光模块的核心基材。2英寸光通信级磷化铟衬底从2025年初的800美元/片，飙升至2026年4月的2300-2500美元/片，涨幅接近2倍；6英寸高端衬底涨幅更是超过250%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;金属镓（Ga）&lt;/strong&gt;：砷化镓（GaAs）衬底的核心原料。自2025年初以来，镓价累计上涨约140%，目前国际市场报价已突破2200美元/kg。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;电子特气与湿化学品&lt;/strong&gt;：用于先进封装工艺的六氟化钨价格从2025年初的47万元/吨，暴涨至2026年5月的150万元/吨以上；受中东地缘局势影响，半导体制造与冷却不可或缺的氦气现货价格也飙升超过50%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装用环氧模塑料（EMC）&lt;/strong&gt;：全球龙头住友电木已宣布自2026年6月1日起，将全系列半导体封装用环氧树脂模塑料价格上调10%-20%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功率半导体与MCU&lt;/strong&gt;：AI数据中心对电源管理需求激增。英飞凌、德州仪器、士兰微、中微半导等国内外龙头密集发布涨价通知，涨幅普遍在10%至20%，部分紧缺品类（如MCU）涨幅达15%-50%。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;AI 硬件基础配套（PCB 与光纤）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特种光纤&lt;/strong&gt;：AI算力中心内部高速互联的&quot;血管&quot;。用于AI算力中心的G.657.A2特种光纤价格在一年内飙升650%（从32元/芯公里涨至240元/芯公里），头部厂商订单已排至2028年。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;电子布（尤其是Low Dk特种电子布）&lt;/strong&gt;：PCB的核心原材料。部分上市公司的特种电子布价格在2026年一季度同比涨幅高达116.85%，织布机订单已排到2028年底。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高端覆铜板（CCL）&lt;/strong&gt;：日本三菱瓦斯化学等巨头已集中发布涨价函，对AI服务器等高阶材料调涨10%-40%，这已是半年内的多次提价。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ABF载板&lt;/strong&gt;：高端AI芯片封装的绝对刚需。目前行业缺口超40%，2026年整体价格涨幅预期已上调至30%-35%。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;算力服务与租赁&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU算力租赁&lt;/strong&gt;：英伟达H100 GPU的一年期租赁价从2025年底的1.7美元/小时，飙升至2026年4月的2.35美元/小时，涨幅近40%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云厂商AI服务&lt;/strong&gt;：因底层硬件成本暴涨，腾讯云、阿里云、百度智能云等头部厂商在2026年3月至5月密集上调AI模型及算力服务价格，部分涨幅高达5%-400%不等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结来看，AI算力硬件的涨价已经全面传导到了上游的封装基板、PCB上游（电子布、铜箔）、光通信材料（光纤、磷化铟）、存储颗粒以及各类半导体特种气体等底层材料端。这些材料由于技术壁垒高、扩产周期长，短期内供需错配的局面很难缓解。&lt;/p&gt;
&lt;h2&gt;原文&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/eZVXWSN2wteJJvCqRKBDYw&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>【主线追踪】 - CPU 的角色从“配角”一跃成为“总指挥”，价格暴涨且严重缺货，重点关注这20只CPU概念股！</title><link>https://chaggle.github.io/posts/2026/05/14/main-invest-cpu-concept-stocks-20260514/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/05/14/main-invest-cpu-concept-stocks-20260514/</guid><pubDate>Thu, 14 May 2026 07:19:12 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自主线追踪，仅用于记录永恒的六一中路&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;近期CPU价格暴涨且面临严重缺货，其核心原因并非传统的周期性波动，而是由AI产业的技术变革引发的结构性供需失衡。&lt;/p&gt;
&lt;p&gt;简单来说，随着AI进入&quot;智能体（Agentic AI）&quot;时代，CPU的角色从过去的&quot;配角&quot;一跃成为算力系统的&quot;总指挥&quot;，需求成倍增加，而供给端却面临严重的产能瓶颈。&lt;/p&gt;
&lt;h2&gt;需求端：AI 范式转变，CPU 重回C位&lt;/h2&gt;
&lt;p&gt;在传统的AI大模型训练阶段，算力主要依赖GPU进行大规模并行计算，CPU仅承担数据加载等辅助工作，一台服务器通常只需配备1颗CPU搭配4到8颗GPU（配比为1:4至1:8）。&lt;/p&gt;
&lt;p&gt;但随着AI从&quot;训练&quot;转向&quot;推理&quot;和&quot;智能体（Agentic AI）&quot;应用，情况发生了根本性逆转：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;角色转变&lt;/strong&gt;：智能体需要实时规划任务、调用外部工具、做出决策并执行动作。这些复杂的逻辑控制、任务编排和数据调度工作，完全依赖于CPU的串行处理能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需求暴增&lt;/strong&gt;：如果CPU处理速度跟不上，昂贵的GPU就会被迫闲置等待。为了缓解这一瓶颈，服务器中CPU与GPU的配比正在向1:2甚至1:1转变。这意味着，同等规模的AI算力建设，对CPU的需求量直接翻了3到8倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;供给端：先进产能被挤占，扩产周期漫长&lt;/h2&gt;
&lt;p&gt;面对需求的爆发式增长，CPU的供给端却显得捉襟见肘：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;产能被GPU挤占&lt;/strong&gt;：目前全球最先进的2nm、3nm晶圆代工产能极度紧张。台积电等代工厂将绝大多数产能优先分配给了利润更高、需求更迫切的AI GPU芯片（如英伟达的产品），导致CPU的产能被严重挤压。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩产周期漫长&lt;/strong&gt;：高端CPU的制造涉及复杂的先进制程和供应链（如光刻胶、特种材料等），新建晶圆厂或扩充产线的周期通常长达2到3年，短期内根本无法快速提升产能来填补缺口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;巨头垄断与产能售罄&lt;/strong&gt;：全球通用CPU市场高度集中在英特尔（Intel）、AMD和Arm手中。由于需求远超预期，这两大巨头的2026年服务器CPU产能已基本售罄，甚至出现了客户抢购原本计划报废的低良率产品的现象。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;市场表现：量价齐升，交期大幅延长&lt;/h2&gt;
&lt;p&gt;在极端的供需错配下，CPU市场呈现出以下特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;价格全面暴涨&lt;/strong&gt;：自2026年初以来，消费级CPU价格普遍上涨5%-10%，服务器CPU涨幅达10%-20%，部分高端AI专用CPU涨幅甚至超过25%。英特尔和AMD已多次上调价格，且酝酿新一轮涨价。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交期严重滞后&lt;/strong&gt;：CPU的常规交货周期从过去的1-2周，被拉长至8-12周，部分高端型号甚至需要等待长达6个月。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全产业链传导&lt;/strong&gt;：CPU的缺货和涨价已经直接推高了服务器整机的成本，并进一步传导至笔记本电脑等消费电子终端，导致近期PC市场也迎来了大幅涨价。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上所述，CPU暴涨的本质是AI算力架构向&quot;CPU+GPU协同&quot;演进过程中的必然阵痛。在晶圆产能瓶颈得到缓解之前，这种供不应求的局面预计至少将持续到2027年。&lt;/p&gt;
&lt;p&gt;美股CPU赛道爆发形成极强的全球产业链共振效应，叠加国内信创替代、算力自主可控、AI算力基建加码三重红利，A股相关产业链迎来戴维斯双击机会。整体可分为三大核心赛道：国产通用CPU设计核心、CPU配套核心芯片、产业链制造封测与整机生态。&lt;/p&gt;
&lt;h2&gt;核心主线：国产服务器/嵌入式CPU设计（最纯正受益）&lt;/h2&gt;
&lt;p&gt;本轮全球CPU涨价、产能紧缺背景下，海外大厂供货紧张、交付周期拉长，国内互联网大厂、政企、金融机构加速导入国产CPU方案，叠加信创政策持续落地，国产替代节奏大幅提速，是确定性最高的细分方向。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;海光信息&lt;/strong&gt;：A股最纯正的X86架构服务器CPU龙头，技术生态与英特尔、AMD高度兼容，适配高端数据中心、AI推理场景。2026年一季度订单饱满，订单规模达去年全年营收1.5倍，充分受益于海外CPU缺货涨价、国内高端服务器替代红利，是AI服务器CPU核心受益标的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;龙芯中科&lt;/strong&gt;：国内唯一全自主指令集CPU龙头，完全实现自主可控，无卡脖子风险。旗下3C6000系列服务器芯片性能对标英特尔至强系列产品，适配党政、央企、金融信创场景，2026年一季度订单量突破82亿，业绩兑现能力强劲，是自主可控CPU核心标杆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中国长城&lt;/strong&gt;：搭载飞腾ARM架构CPU，深耕党政信创核心市场，飞腾CPU累计销量突破1300万片，党政市场占有率超50%，具备芯片设计、整机制造全栈交付能力，充分受益于信创CPU规模化替代。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;北京君正&lt;/strong&gt;：嵌入式CPU龙头，拥有自主MIPS架构，聚焦边缘计算、工业控制、车载、消费电子场景，AI边缘智能体落地带动嵌入式CPU需求爆发，2026年一季度业绩同比大幅增长，成长确定性强。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纳思达&lt;/strong&gt;：通用嵌入式CPU+打印芯片龙头，旗下奔图电子CPU广泛应用于打印机、工业嵌入式设备，国产替代持续推进，业绩稳步增长，基本面扎实。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;配套主线：CPU配套核心芯片（算力刚需，高景气）&lt;/h2&gt;
&lt;p&gt;服务器CPU放量、AI算力集群建设，直接带动内存接口、互连芯片、安全算力芯片等配套产品需求爆发，量价齐升逻辑明确。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;澜起科技&lt;/strong&gt;：全球内存接口芯片龙头，深度绑定英特尔、AMD服务器平台，同时拥有津逮国产CPU产品线，适配高端服务器可信计算场景，充分受益于全球服务器CPU出货量增长，业绩稳定性、成长性兼备。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;禾盛新材&lt;/strong&gt;：持股熠知电子布局ARM架构服务器CPU，聚焦低功耗AI边缘服务器场景，贴合AI智能体边缘算力需求，赛道稀缺性突出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;综艺股份&lt;/strong&gt;：持股神州龙芯为第一代龙芯处理器&quot;龙芯1号&quot;诞生地，专注自主可控嵌入式工业级处理器，拥有自主知识产权GSC系列CPU，产品广泛应用于军工、安全、政府办公等领域。此外，子公司南京天悦专注超低功耗助听器芯片的国产化替代，在珠三角市场站稳脚跟，为嵌入式CPU生态提供差异化配套解决方案。控股子公司吉莱微为功率半导体IDM企业，集芯片设计、晶圆制造、封装测试于一体，为嵌入式CPU等提供功率半导体配套解决方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;瑞芯微&lt;/strong&gt;：专注SoC芯片设计，为国产CPU提供配套解决方案，在消费电子与物联网领域应用广泛。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;下游及配套产业链：封测、整机、材料（间接高弹性受益）&lt;/h2&gt;
&lt;h3&gt;先进封测环节&lt;/h3&gt;
&lt;p&gt;CPU高端封装测试产能紧缺，是制约出货量的核心瓶颈，叠加AMD、英特尔订单转移，国内封测企业业绩弹性极大。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;通富微电&lt;/strong&gt;：AMD全球核心封测合作伙伴，承接AMD超八成的CPU、GPU封测订单，深度绑定海外龙头，直接受益于AMD产能扩张、出货量暴涨，2025年业绩已实现高增长，2026年持续兑现红利。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长电科技&lt;/strong&gt;：全球第三大封测厂，为龙芯等国产CPU提供Fanout、SiP等先进封装服务，月产能达10万颗，良率超98%，高端封装收入占比60%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;华天科技&lt;/strong&gt;：国内封测龙头之一，在传统封装领域优势显著，为中低端CPU提供高性价比封测解决方案，同时布局先进封装技术。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;服务器整机环节&lt;/h3&gt;
&lt;p&gt;国产CPU落地最终依托服务器整机出货，AI算力基建、信创服务器替换带来整机增量需求。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;中科曙光&lt;/strong&gt;：国内高端算力服务器龙头，深度搭载海光CPU，聚焦AI算力集群、超级计算场景，是国产AI服务器核心交付厂商，充分受益于CPU国产化+AI算力建设双红利。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;浪潮信息&lt;/strong&gt;：国产服务器龙头，除采用英特尔CPU外，积极布局国产CPU服务器，与海光、龙芯等深度合作，推动国产算力落地。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;半导体材料环节&lt;/h3&gt;
&lt;p&gt;CPU产能扩张、国产芯片量产提速，带动上游晶圆制造材料需求增长，国产化替代空间广阔。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;江丰电子&lt;/strong&gt;：高纯溅射靶材龙头，覆盖CPU晶圆金属化核心环节，绑定国内主流晶圆厂，充分受益于国产CPU量产扩产。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安集科技&lt;/strong&gt;：CMP抛光液国产龙头，用于CPU制程平坦化环节，打破海外垄断，伴随高端CPU制程升级持续放量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;沪硅产业&lt;/strong&gt;：国内12英寸硅片龙头，硅片良率达95%，为国产CPU提供关键基材支撑，打破国外垄断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;雅克科技&lt;/strong&gt;：全球前驱体龙头，为国产CPU先进制程提供关键材料，国内市占率第一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;菲利华&lt;/strong&gt;：高纯石英玻璃核心供应商，用于CPU制造的刻蚀、扩散工序，是国内少数通过国际认证的石英材料企业。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;衢州发展&lt;/strong&gt;：拟收购先导电科（全球ITO靶材市占率30%+），靶材是CPU制造金属化环节关键材料，契合半导体材料国产替代趋势。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，紫光展锐为国内移动CPU龙头，面向手机、平板、物联网等领域，虎贲系列芯片已实现规模化商用，是国内少数具备完整SoC设计能力的企业。彤程新材、兆易创新参股紫光展锐。&lt;/p&gt;
&lt;p&gt;CPU产业链还有很多公司，但是我们觉得目前关注上述20只就足够。&lt;/p&gt;
&lt;h2&gt;原文&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/TJK7OPNRTb2HNt9LsPcgEw&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>【波段中线之美】 - No.3：存储巨头全在创新高！大盘子的No.3有可能成为资金的抱团对象</title><link>https://chaggle.github.io/posts/2026/05/05/middle-invest-no3-storage-giants-20260505/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/05/05/middle-invest-no3-storage-giants-20260505/</guid><pubDate>Tue, 05 May 2026 07:19:12 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自波段中线之美，仅用于记录永恒的六一中路&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;我们说过很多次了，No.3才是我们所有波段中线中最折磨人的。好在近期大幅上涨上来，又接近历史新高。&lt;/p&gt;
&lt;p&gt;A股涉及存储的概念股有179只（同花顺分类），而真正受益于企业级存储涨价的个股并不多，大部分存储概念股是产业链上的中低端。No.3在中国乃至全球存储行业中都占据着举足轻重的地位，被公认为中国存储芯片领域的龙头企业和全球化芯片设计的领军者。在多个核心存储细分领域均跻身全球前十，是全球唯一一家在NOR Flash、SLC NAND Flash、利基型DRAM和MCU四大领域均位列全球前十的中国内地企业。&lt;img src=&quot;/images/invest/middle-invest-20260505.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;牛散葛卫东也是在No.3重仓持有中。&lt;/p&gt;
&lt;p&gt;就是这样一家在国内具有举足轻重的存储巨头，股价表现却远不如其他个股，最重要的原因是因为盘子大。&lt;/p&gt;
&lt;p&gt;现在，真正的存储巨头，不管是美国的四巨头&lt;strong&gt;美光科技、闪迪、希捷科技和西部数据&lt;/strong&gt;，还是韩国的&lt;strong&gt;三星和SK海力士&lt;/strong&gt;，股价都在不断的创历史新高。&lt;/p&gt;
&lt;p&gt;在全世界的存储巨头都在继续大涨，国内资金大概率会寻找存储板块的抱团对象，这时候No.3的盘子大反而会成为优势。&lt;/p&gt;
&lt;p&gt;传导到A股，存储概念将会继续分化，真正受益于&lt;strong&gt;高端存储涨价的个股将会率先突围&lt;/strong&gt;，而作为&lt;strong&gt;国内存储巨头的No.3继续创历史新高只是时间问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，即使近期No.3有回调的可能，也不会改变我们继续持有的决心和耐心。&lt;/p&gt;
&lt;h2&gt;原文&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/KQL6qN3_stSKlsPX5k2TSA&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>【波段中线之美】 - 波段中线如何避免量化的收割？看懂了本文，您就可以理解我们选择No.4的原因了！</title><link>https://chaggle.github.io/posts/2026/03/22/middle-invest-avoid-quant-harvest-20260322/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/03/22/middle-invest-avoid-quant-harvest-20260322/</guid><pubDate>Sun, 22 Mar 2026 07:19:12 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自波段中线之美，仅用于记录永恒的六一中路&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;从2025年9月份我们开通本公众号以来，我们一直秉承着&quot;选择正确的题材，在可控的时间内，获取超预期的收益&quot;的宗旨，更具体的目标是半年获取30%收益。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;量化，现在成为过街老鼠人人喊打，但奈何，即使量化在狂收割散户，也没人管得了。所以，散户能做的就是不要成为量化的对手盘。&lt;/p&gt;
&lt;p&gt;做短线的不用说，基本避不开量化。而即使我们这样做波段中线，也要好好研究如何避免量化的收割。&lt;/p&gt;
&lt;p&gt;当然，以下文章只是理论上的，有利于大家对我们选择No.4的理解以及操作，具体操作建议还是多关注我们的后续文章。&lt;/p&gt;
&lt;h2&gt;波段中线最容易被量化收割的 3 个点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;追高买入：量化在突破位预埋大单，你一追就被砸。&lt;/li&gt;
&lt;li&gt;恐慌止损：量化故意砸破关键均线 / 支撑，逼你割肉再拉回。&lt;/li&gt;
&lt;li&gt;尾盘偷袭：你看尾盘拉升追进去，第二天量化直接低开闷杀。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只要避开这三类行为，量化在波段级别基本收割不到你。&lt;/p&gt;
&lt;h2&gt;波段中线反量化核心策略（最关键）&lt;/h2&gt;
&lt;p&gt;核心原则：不跟量化拼速度，只拼&quot;结构 + 周期 + 仓位&quot;。&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 选股：只做&quot;量化不想 / 不敢重仓搞&quot;的票&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a. 市值：50 亿～2000 亿（备注：No.4流通盘符合）&lt;/li&gt;
&lt;li&gt;b. 日均成交额：≥8 亿（流动性够，量化难控盘）（备注：No.4符合）&lt;/li&gt;
&lt;li&gt;c. 有真实赛道景气 + 业绩预期（量化很难深度建模产业逻辑）（备注：No.4所处的题材是算电协同，符合）&lt;/li&gt;
&lt;li&gt;d. 拒绝：纯题材小票、庄股、流动性差的冷门股（备注：No.4流动性比较好，国企非庄股，符合）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 入场：只做&quot;回踩&quot;，不做&quot;突破追&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;量化最喜欢收割追突破的人。&lt;/p&gt;
&lt;p&gt;你只做两种买点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;回踩 20 日线 / 60 日线企稳&lt;/li&gt;
&lt;li&gt;箱体下沿或黄金分割支撑位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;入场方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用限价单分批买，不跟盘口抢单&lt;/li&gt;
&lt;li&gt;不追涨超过 3% 的票&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 止损：用&quot;结构止损&quot;，不用&quot;情绪化止损&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;量化最怕你有刚性、不对称、不轻易动的止损。&lt;/p&gt;
&lt;p&gt;波段中线标准止损：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单笔最大亏损：-5%～-7% 硬止损（特别强调：因人而异，如果&lt;strong&gt;都持有不动，亏损10%+也不是硬要止损&lt;/strong&gt;，注意查看我们后续发文）&lt;/li&gt;
&lt;li&gt;或：有效跌破 20 日线且收不回离场&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;坚决不做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;盘中一砸就慌，随便改止损&lt;/li&gt;
&lt;li&gt;越跌越补，变成被动长线&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 止盈：不吃鱼尾，量化最容易在鱼尾埋人&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一目标：+10%～+20% 减仓一半（特别强调：这是普适性原则，但是对于我们选中的波段中线，因人而异选择止盈减仓目标）&lt;/li&gt;
&lt;li&gt;剩下仓位用20 日线移动止盈&lt;/li&gt;
&lt;li&gt;出现高位放量长上影、量化盘口特征明显，直接走&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不贪最后一段，就是最好的反收割。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. 仓位：永远不给量化&quot;逼仓&quot;你的机会&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单票仓位：≤25%&lt;/li&gt;
&lt;li&gt;总仓位：&lt;strong&gt;30%～60%&lt;/strong&gt; 滚动&lt;/li&gt;
&lt;li&gt;不满仓、不梭哈、不加仓到重仓被套&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;量化逼仓的前提是你满仓 + 杠杆。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;盘口识别：一眼看出&quot;量化在搞事&quot;，直接避让&lt;/h2&gt;
&lt;p&gt;出现以下任意一种，当天不买、不加仓：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;盘口大量 1 手、2 手刷单&lt;/li&gt;
&lt;li&gt;挂单秒挂秒撤，盘口闪烁&lt;/li&gt;
&lt;li&gt;直线脉冲后瞬间砸回，无承接&lt;/li&gt;
&lt;li&gt;涨跌停反复炸板，量能异常&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对策：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不接力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已有持仓逢高减仓&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;等回踩结构再看&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;交易行为：从根源上切断被量化收割的可能&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;不做日内 T+0（&lt;strong&gt;特别强调：我们不建议做T，因为绝大部分人很容易卖飞，比如No.2就有很多朋友T飞后不敢接回，结果导致后面连续3天暴涨50%+而没吃到，当然有能力的朋友可以根据自己水平操作，别做出低卖高买的反效果就行&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;不频繁撤单（你的行为就是量化的训练数据）&lt;/li&gt;
&lt;li&gt;不盯分时盯日线&lt;/li&gt;
&lt;li&gt;单日最多交易 1 次&lt;/li&gt;
&lt;li&gt;连续错 2 笔，强制空仓 1 天&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;一句话总结（波段中线反量化口诀）&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;选中军，做回踩，设硬止损，&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分批买，移动止盈，不吃鱼尾，&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不盯盘口，不追突破，不满仓。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;你只要严格执行这套，波段中线基本不可能被量化收割，反而能吃量化砸出来的黄金坑。&lt;/p&gt;
&lt;h2&gt;原文&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/DOV_ufTdfczm5q4DWlhERg&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>【主线追踪】 - 存储芯片目前订单最多的十家公司！</title><link>https://chaggle.github.io/posts/2026/03/22/main-invest-storage-chip-orders-20260322/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/03/22/main-invest-storage-chip-orders-20260322/</guid><pubDate>Sun, 22 Mar 2026 07:19:12 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自主线追踪，仅用于记录永恒的六一中路&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;存储芯片（DRAM/NAND）价格在 2025 年底至 2026 年 3 月迎来史诗级暴涨，核心是AI 算力需求爆发 + 供给端刚性收缩 + 寡头控价 + 库存见底的多重共振，形成供需深度失衡。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;需求端：AI 算力引爆，需求几何级增长（核心推手）&lt;/h2&gt;
&lt;h3&gt;AI 服务器需求暴增&lt;/h3&gt;
&lt;p&gt;单台 AI 服务器 DRAM 用量是传统服务器的8–10 倍，NAND 用量是3 倍。&lt;/p&gt;
&lt;p&gt;2026 年 AI 服务器出货量同比 &lt;strong&gt;+180%&lt;/strong&gt;，直接吞噬全球存储产能。&lt;/p&gt;
&lt;p&gt;高带宽内存（HBM）是 AI 算力核心，2026 年产能被英伟达、微软、谷歌、AMD 等100% 提前包圆，订单排至 2027 年后，缺口达50%–60%。&lt;/p&gt;
&lt;h3&gt;消费电子触底复苏 + 存储配置升级&lt;/h3&gt;
&lt;p&gt;PC、手机需求回暖，AI 手机、折叠屏普遍标配12GB+512GB及以上，单台存储用量翻倍。&lt;/p&gt;
&lt;p&gt;2026 年 Q1，8GB+256GB 存储组合合约价较 2025 年同期涨近 200%。&lt;/p&gt;
&lt;h3&gt;车规 / 工业存储需求刚性增长&lt;/h3&gt;
&lt;p&gt;智能汽车、工控、IoT 对车规级 DRAM/NOR/NAND 需求持续上升，进一步分流产能。&lt;/p&gt;
&lt;h2&gt;供给端：产能刚性 + 结构性错配 + 寡头控价（涨价基础）&lt;/h2&gt;
&lt;h3&gt;产能严重不足 + 无新增产能&lt;/h3&gt;
&lt;p&gt;存储芯片是重资产、长周期行业，新建产线需18–24 个月，2026 年几乎无新增产能释放。&lt;/p&gt;
&lt;p&gt;三大厂（三星、SK 海力士、美光）2023–2024 年因价格暴跌大幅砍产能，2025 年底库存见底。&lt;/p&gt;
&lt;h3&gt;产能结构性倾斜（最关键）&lt;/h3&gt;
&lt;p&gt;三大厂将70%+ 先进产能转向HBM/AI 服务器等高利润产品（HBM 利润是普通 DRAM 的3–4 倍）。&lt;/p&gt;
&lt;p&gt;SK 海力士40%消费级 DRAM 产能转产 HBM，三星30%、美光25%。&lt;/p&gt;
&lt;p&gt;生产 1 单位 HBM 消耗晶圆面积是传统 DRAM 的3 倍，进一步挤压通用存储供给。&lt;/p&gt;
&lt;h3&gt;寡头垄断 + 协同控价&lt;/h3&gt;
&lt;p&gt;三星、SK 海力士、美光垄断90%+ DRAM、70%+ NAND市场，具备极强定价权。&lt;/p&gt;
&lt;p&gt;2026 年三大厂无降价计划，主动控产保价。&lt;/p&gt;
&lt;h3&gt;库存历史低位&lt;/h3&gt;
&lt;p&gt;原厂库存仅3–5 周（安全线 8–12 周），渠道库存告急，无货可补。&lt;/p&gt;
&lt;h2&gt;其他关键因素&lt;/h2&gt;
&lt;h3&gt;DDR4 退出历史舞台&lt;/h3&gt;
&lt;p&gt;2025 年底美光、三星、SK 海力士停止 DDR4 产能，消费级 / 工控级成熟产品供应断崖式下滑。&lt;/p&gt;
&lt;h3&gt;晶圆 / 设备瓶颈&lt;/h3&gt;
&lt;p&gt;300mm 硅晶圆供应紧张，先进制程设备（EUV）交付周期拉长，产能扩张受阻。&lt;/p&gt;
&lt;h3&gt;地缘与政策&lt;/h3&gt;
&lt;p&gt;存储芯片战略地位提升，贸易摩擦、出口管制加剧供应不确定性。&lt;/p&gt;
&lt;h2&gt;价格表现（2026 年 Q1）&lt;/h2&gt;
&lt;p&gt;DRAM：合约价环比 &lt;strong&gt;+90%–95%&lt;/strong&gt;，DDR4 8Gb 现货较 2025 年低点 &lt;strong&gt;+369%&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;NAND：合约价环比 &lt;strong&gt;+55%–60%&lt;/strong&gt;，企业级 SSD 涨幅 &lt;strong&gt;&amp;gt;200%&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;HBM：价格涨幅 &lt;strong&gt;&amp;gt;300%&lt;/strong&gt;，一芯难求。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本轮涨价是AI 算力革命引发的结构性供需失衡，叠加寡头控价、产能刚性、库存见底，形成超级景气周期。预计涨价至少持续1–2 年，直到 2027 年后新增产能逐步释放。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基于上述原因，国内存储芯片相关产业链也十分受益，我们就从订单方面入手，整理出10家在手订单最多的公司。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 长江存储（未上市）【存储芯片制造环节】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：3D NAND 闪存（128 层 / 232 层及以上）、UFS、eMMC、企业级 SSD&lt;/li&gt;
&lt;li&gt;核心客户：华为、小米、联想、服务器厂商、国产 PC / 手机品牌、云厂商&lt;/li&gt;
&lt;li&gt;订单特点：国产 3D NAND 唯一龙头，订单超380 亿元，排期至2028 年底，AI 服务器与国产替代双驱动&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 长电科技【存储芯片封测环节】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：DRAM/NAND/HBM 全系列封测、2.5D/3D 堆叠、Chiplet、SIP 先进封装&lt;/li&gt;
&lt;li&gt;核心客户：长江存储、SK 海力士、三星、美光、澜起科技、兆易创新、英伟达（HBM 认证）&lt;/li&gt;
&lt;li&gt;订单特点：全球第三、国内第一封测厂，存储封测占比约25%；HBM 8 层堆叠良率98.5%；订单320 亿元，排至2028 年 Q2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 生益科技【存储材料 / 基板】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：覆铜板（CCL）、封装基板（存储专用）、高速高频材料&lt;/li&gt;
&lt;li&gt;核心客户：深南电路、长电科技、华天科技、全球 PCB / 封测厂、存储模组厂&lt;/li&gt;
&lt;li&gt;订单特点：全球第二大覆铜板供应商，内资最大存储封装基板供应商；订单250 亿元，排至2027 年底&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. 深南电路【存储材料 / 基板】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：高端 PCB、存储封装基板、HBM / 先进封装载板&lt;/li&gt;
&lt;li&gt;核心客户：长江存储、长电科技、澜起科技、英特尔、AMD、英伟达、华为海思&lt;/li&gt;
&lt;li&gt;订单特点：国内存储封装基板龙头，HBM 载板核心供应商；订单220 亿元，排至2028 年 Q1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;5. 澜起科技【存储芯片设计】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：DDR4/DDR5 内存接口芯片（RCD/DB）、HBM 控制器、CXL 互连芯片、时序 / 电源管理芯片&lt;/li&gt;
&lt;li&gt;核心客户：三星、SK 海力士、美光、长鑫存储、服务器厂商（戴尔、浪潮、华为）、英伟达&lt;/li&gt;
&lt;li&gt;订单特点：全球 DDR5 接口芯片龙头（市占40%–50%）；AI 服务器驱动 DDR5 渗透；订单180 亿元，排至2027 年 Q4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;6. 兆易创新【存储芯片设计】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：NOR Flash（全球第二）、SLC NAND、利基型 DRAM、车规级存储、MCU&lt;/li&gt;
&lt;li&gt;核心客户：消费电子（小米、OPPO、vivo）、工业控制、汽车电子（比亚迪、特斯拉）、IoT 设备、长鑫存储&lt;/li&gt;
&lt;li&gt;订单特点：国内唯一覆盖四大存储领域的平台型设计公司；车规 NOR/DRAM 批量出货；订单150 亿元，排至2027 年 Q3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;7. 江波龙【存储模组 / 主控】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：企业级 SSD、内存条、嵌入式存储、工业级存储、Lexar（雷克沙）品牌消费存储&lt;/li&gt;
&lt;li&gt;核心客户：云厂商（阿里云、腾讯云）、服务器厂商、工业自动化、汽车电子、消费电子品牌&lt;/li&gt;
&lt;li&gt;订单特点：第三方存储模组龙头，企业级 SSD 市占12%；AI 服务器适配率70%；订单130 亿元，排至2027 年 Q2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;8. 德明利【存储模组 / 主控】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：SSD 主控芯片、消费级 SSD、嵌入式存储、移动存储、工业级存储模组&lt;/li&gt;
&lt;li&gt;核心客户：PC 厂商、消费电子品牌、电商渠道、工业设备厂商、国内存储品牌&lt;/li&gt;
&lt;li&gt;订单特点：主控 + 模组一体化，打破海外主控垄断；性价比优势显著；订单110 亿元，排至2026 年底&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;9. 佰维存储【存储模组 / 主控】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：嵌入式存储、车规级存储、端侧 AI 存储、工业级 SSD、高端消费存储&lt;/li&gt;
&lt;li&gt;核心客户：头部 PC 厂商、智能硬件、IoT 设备、汽车电子、工业控制、AI 终端厂商&lt;/li&gt;
&lt;li&gt;订单特点：设计 + 封测 + 模组一体化；AI 眼镜嵌入式存储全球市占第一；订单90 亿元，排至2027 年 Q1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;10. 北京君正【存储芯片设计】&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;核心产品：车规级 SRAM（全球市占29%）、车规 DRAM、嵌入式存储、工业级存储&lt;/li&gt;
&lt;li&gt;核心客户：大众、宝马、比亚迪、特斯拉、博世、大陆集团、国内 Tier1、工控厂商&lt;/li&gt;
&lt;li&gt;订单特点：车规存储绝对龙头，AEC-Q100 认证；订单85 亿元，排至2026 年 Q4&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;原文&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/kpjWSlhkkHFGhisTxBLY9Q&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>【波段中线之美】 - 关于No.1到No.4的一点说明！</title><link>https://chaggle.github.io/posts/2026/03/22/middle-invest-no1-no4-notes-20260322/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/03/22/middle-invest-no1-no4-notes-20260322/</guid><pubDate>Sun, 22 Mar 2026 07:19:12 GMT</pubDate><content:encoded>&lt;p&gt;:::caution&lt;/p&gt;
&lt;p&gt;本文源自波段中线之美，仅用于记录永恒的六一中路&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;从2025年9月份我们开通本公众号以来，我们一直秉承着&quot;选择正确的题材，在可控的时间内，获取超预期的收益&quot;的宗旨，更具体的目标是半年获取30%收益。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;本公众号已开通半年左右的时间（实际上第一篇发文是2025年10月份），我们跟大家分享了No.1-No.4共4只波段中线的逻辑。&lt;/p&gt;
&lt;p&gt;No.1和No.2都已经超额完成任务，甚至超额出太多太多。&lt;/p&gt;
&lt;p&gt;No.3还在煎熬中。&lt;/p&gt;
&lt;p&gt;No.4刚分享两天。&lt;/p&gt;
&lt;p&gt;其实我们更喜欢在一段时间内跟踪一只波段中线，但是因为特殊原因，比如No.2刚分享时，很多朋友提出没有创业板权限，所以我们接着分享了同为存储板块题材且为主板的No.3。&lt;/p&gt;
&lt;p&gt;等No.2加速超额完成任务后，很多朋友因为No.3的高单价而没有跟随，提出最好能分享一只低价主板个股，所以我们前期在调研时基本选择都是属于主板且低价的公司。No.4就是在这样的背景下诞生。&lt;/p&gt;
&lt;p&gt;关于No.3，近期还是在不断震荡，我们继续看好，后续也会继续点评，直至加速出清，这个大家不用担心。&lt;/p&gt;
&lt;p&gt;关于No.4，开始过于顺利，我们不喜欢，具体原因我们昨天文章分析了。当然，如果开始震荡筑底的时候，我们会像以前一样来鼓励大家，给大家心理按摩。&lt;/p&gt;
&lt;p&gt;为了保护我们的公众号，也为了保护跟大家沟通交流的平台，我们提醒大家在留言时不要打出No.4的全称，完全可以用&quot;4号&quot;或者&quot;3号&quot;或者拼音等替代，我们也会定期检查留言，有涉及到全称的留言会被我们删除。在此也谢过大家的配合！至于打赏，我们每篇文章都开通着，大家可以随点心意支持我们（调研和研究），也可以完全不理，不用有心理负担。&lt;/p&gt;
&lt;h2&gt;原文&lt;/h2&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;https://mp.weixin.qq.com/s/-Qqeao3hcpWVVwjuX0MXtA&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>又一年</title><link>https://chaggle.github.io/posts/2026/03/15/another-year-20260315/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2026/03/15/another-year-20260315/</guid><pubDate>Sun, 15 Mar 2026 13:38:36 GMT</pubDate><content:encoded>&lt;h2&gt;感思&lt;/h2&gt;
&lt;p&gt;距离 2025 年 3 月 1 日又过去了一年。上一次回忆还是在回忆自己的工作生涯。又过一年，在 2025 年这一年中，自己的日子跟 2022 比，虽然稍微轻松一点了，但是还是没有那么轻松。&lt;/p&gt;
&lt;p&gt;:::tip
2025 年 3 月出差北京研发数据标注平台
:::&lt;/p&gt;
&lt;p&gt;在 2025 年 3 月 1 日写下博客后，就出差去北京了。这辈子没有坐过飞机，也是在公司的业务发展下坐上了飞机，感觉到很新奇。但是对坐飞机的印象就是安检很麻烦，但是绝对安全，好处都是有代价的。&lt;/p&gt;
&lt;p&gt;在北京的西城区住了一个月的汉庭，把北京想去的地方都去了一次：故宫、颐和园、八达岭长城、水立方游泳池、天坛公园、天安门。出差顺便旅游，这种感觉对我来说，莫名还觉得挺舒服的。还有就是住在连锁酒店真省心，比起住在一般的小巷酒店，更符合自己的心意。个人比较喜欢一些标准化的东西，可以不用那么独特，但是功能性必须齐全。&lt;/p&gt;
&lt;p&gt;在北京也见了同学跟网友，感叹大家在北京呆着都不容易：房价高，但是给的工资钱也不是特别多，还不知道能不能留在北京。北漂的苦，作为背井离乡的我来说，最容易引起共鸣。毕竟人最喜欢确定性，但是人这一辈子最多的却是不确定。&lt;/p&gt;
&lt;p&gt;:::tip
2025 年 5 月做保密项目
:::&lt;/p&gt;
&lt;p&gt;在北京呆了一个月后，度过了业务最麻烦的一段时光。在自研产品良好运营后，回长沙在公司混日子过完 4 月，马上又接到一个紧急任务，开始去保密单位做大数据平台跟数据中台。现在回想起来，还是自己能硬抗，一抗就在驻场的项目里扛到现在了。期间不止是技术性的工作，沟通工作、编写方案的工作、熟悉与了解业务的工作，比研究技术上的内容多了很多，也导致自己仿佛丢失了一线代码的工作任务。&lt;/p&gt;
&lt;p&gt;现在想起来，其实人毕竟是多维度的人，不止是技术细节上的工作，可能更多的还需要适应社会，适者生存么。弄明白社会运转的底层规律，其实比研究技术细节、研究代码的组织架构，还费心。还需要理解技术的产生，本身就是为了满足业务需求而存在的，而业务需求，本质上是资本的产物。所以说所有的技术人员，都应该对资本有足够的了解与理解，这样才能方便自己的工作。&lt;/p&gt;
&lt;p&gt;:::caution
2025 年 7 月进入股市
:::&lt;/p&gt;
&lt;p&gt;所以在那边项目的强压下，自己硬着头皮在里面干了两个月。当时焦虑到什么样子：晚上 1 点睡觉的，到凌晨 4 点左右，就醒了，然后就睡不着了。情绪波动也大，一个劲往外边送钱，看到喜欢的东西就打赏，用以购买情绪价值，然后继续空虚。毕竟当时的自己并没有在生活有独属于自己的创造性价值，所以陷入恶性循环。现在看来当时可能都有情绪躯体化的特征了，而后开始思考属于自己的财富管理。这里也就读书读到了硅谷居士，然后开始定投纳指。当然也是由于自己的贪心，以及对股市、对钱的向往，开始进入股市，并且浮浮沉沉到今日。&lt;/p&gt;
&lt;p&gt;不得不说，股市治好了我的焦虑。虽然可能我在股市没有赚钱，还亏掉了本金，但是我从现在来看，我的确适合去做金融。虽然可能随时有可能被巨轮碾压，但是我依旧不后悔。毕竟我觉得我的事业也不止技术代码，也需要另外一条腿，方便在我技术不能兜底的情况下，继续拉着我的残躯继续前进。&lt;/p&gt;
&lt;p&gt;所以就到了 2026 年。今年比之前都过得平淡，而且自己也想过一下不一样的生活：开始自己做饭，不因为麻烦而劝退自己。只是自己已经 27 的虚岁了，应该要自己多做饭，多注意自己的身体，多关心自己的心理情绪了。也就是常人说的，年龄越大，越需要对自己好一点。&lt;/p&gt;
</content:encoded></item><item><title>在 Linux 下使用 Clash 进行全局上网代理及自动订阅代理和规则</title><link>https://chaggle.github.io/posts/2025/03/21/clash-linux-global-proxy/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2025/03/21/clash-linux-global-proxy/</guid><pubDate>Fri, 21 Mar 2025 07:47:55 GMT</pubDate><content:encoded>&lt;p&gt;:::note
本文将详细介绍如何在 Linux 环境下配置 Clash，以实现全局网络代理。
同时使用 proxy-group 与 rule-providers，来实现机场订阅链接和规则的自动订阅。
:::&lt;/p&gt;
&lt;h2&gt;下载 Clash&lt;/h2&gt;
&lt;p&gt;从以下备份库下载 Clash Premium。由于 Clash Premium 的主库已被删除，我们将使用备份库来获取：&lt;a href=&quot;https://github.com/zhongfly/Clash-premium-backup/releases/download/2023-09-05-gdcc8d87/clash-linux-amd64-n2023-09-05-gdcc8d87.gz&quot;&gt;Clash-premium-backup&lt;/a&gt;。如果备份库失效，也可以百度搜索 clash for linux 进行下载。&lt;/p&gt;
&lt;h2&gt;解压 Clash&lt;/h2&gt;
&lt;p&gt;解压下载的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gzip -d clash-linux-amd64-n2023-09-05-gdcc8d87.gz
chmod +x clash-linux-amd64
mv clash-linux-amd64 clash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建配置文件 config.yaml，并编辑配置：&lt;/p&gt;
&lt;p&gt;确保将 &lt;code&gt;&amp;lt;这里替换为你机场的订阅链接&amp;gt;&lt;/code&gt; 替换为你的实际订阅链接。&lt;/p&gt;
&lt;p&gt;这里我们使用了 proxy-groups 和 rule-providers：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以自动订阅节点&lt;/li&gt;
&lt;li&gt;可以自动订阅规则&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然也可以使用原机场提供的配置文件，但是需要自己手动订阅节点和规则。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;port: 7890
socks-port: 7891
redir-port: 7892
mixed-port: 7893
allow-lan: false
mode: rule
log-level: info
ipv6: false
external-controller: 127.0.0.1:9091 # 最好不要在公网打开面板进行使用
clash-for-android:
  append-system-dns: false
profile:
  tracing: true
experimental:
  sniff-tls-sni: true

tun:
  enable: true
  stack: system
  dns-hijack:
    - any:53
  auto-route: true
  auto-detect-interface: true

dns:
  enable: true
  enhanced-mode: fake-ip
  listen: :53
  default-nameserver:
      - 210.5.56.145
  nameserver:
      - 114.114.114.114
  fallback:
      - https://8888.google/dns-query
      - https://1.0.0.1/dns-query
      - https://dns.twnic.tw/dns-query
      - https://doh.opendns.com/dns-query
      - https://dns-nyc.aaflalo.me/dns-query
      - https://dns.aa.net.uk/dns-query
      - https://sg.adhole.org/dns-query
      - https://kaitain.restena.lu/dns-query
      - https://hydra.plan9-ns1.com/dns-query
      - https://jp.tiar.app/dns-query
      - https://doh.asia.dnswarden.com/adblock
  fallback-filter:
    geoip: true
    geoip-code: CN

proxy-providers:
  Exemple:
    type: http
    path: ./example.yaml
    url: &amp;lt;这里替换为你机场的订阅链接&amp;gt;
    interval: 3600
    health-check:
      enable: true
      url: http://www.gstatic.com/generate_204
      interval: 300

proxy-groups:
  - name: PROXY
    type: select
    url: http://www.gstatic.com/generate_204
    interval: 3600
    use:
      - Exemple

rule-providers:
  reject:
    type: http
    behavior: domain
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/reject.txt&quot;
    path: ./ruleset/reject.yaml
    interval: 86400

  icloud:
    type: http
    behavior: domain
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/icloud.txt&quot;
    path: ./ruleset/icloud.yaml
    interval: 86400

  apple:
    type: http
    behavior: domain
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/apple.txt&quot;
    path: ./ruleset/apple.yaml
    interval: 86400

  google:
    type: http
    behavior: domain
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/google.txt&quot;
    path: ./ruleset/google.yaml
    interval: 86400

  proxy:
    type: http
    behavior: domain
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/proxy.txt&quot;
    path: ./ruleset/proxy.yaml
    interval: 86400

  direct:
    type: http
    behavior: domain
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/direct.txt&quot;
    path: ./ruleset/direct.yaml
    interval: 86400

  private:
    type: http
    behavior: domain
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/private.txt&quot;
    path: ./ruleset/private.yaml
    interval: 86400

  gfw:
    type: http
    behavior: domain
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/gfw.txt&quot;
    path: ./ruleset/gfw.yaml
    interval: 86400

  tld-not-cn:
    type: http
    behavior: domain
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/tld-not-cn.txt&quot;
    path: ./ruleset/tld-not-cn.yaml
    interval: 86400

  telegramcidr:
    type: http
    behavior: ipcidr
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/telegramcidr.txt&quot;
    path: ./ruleset/telegramcidr.yaml
    interval: 86400

  cncidr:
    type: http
    behavior: ipcidr
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/cncidr.txt&quot;
    path: ./ruleset/cncidr.yaml
    interval: 86400

  lancidr:
    type: http
    behavior: ipcidr
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/lancidr.txt&quot;
    path: ./ruleset/lancidr.yaml
    interval: 86400

  applications:
    type: http
    behavior: classical
    url: &quot;https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/applications.txt&quot;
    path: ./ruleset/applications.yaml
    interval: 86400


rules:
  - RULE-SET,applications,DIRECT
  - DOMAIN,clash.razord.top,DIRECT
  - DOMAIN,yacd.haishan.me,DIRECT
  - RULE-SET,private,DIRECT
  - RULE-SET,reject,REJECT
  - RULE-SET,tld-not-cn,PROXY
  - RULE-SET,gfw,PROXY
  - RULE-SET,google,PROXY
  - RULE-SET,telegramcidr,PROXY
  - MATCH,DIRECT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行 Clash：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clash -f /path/to/your/config.yaml
# 注意：请替换 /path/to/your/config.yaml 为你的实际配置文件路径。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置 systemd 服务&lt;/h2&gt;
&lt;p&gt;创建一个 systemd 服务文件，例如 /etc/systemd/system/clash.service，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Clash Service
After=network.target

[Service]
Type=simple
User=&amp;lt;你的用户名&amp;gt;
WorkingDirectory=&amp;lt;Clash配置文件目录&amp;gt;
ExecStart=&amp;lt;Clash执行文件路径&amp;gt; -f &amp;lt;Clash配置文件路径&amp;gt;
Restart=on-failure

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
需要替换 &lt;code&gt;&amp;lt;你的用户名&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;Clash 配置文件目录&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;Clash 执行文件路径&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;Clash 配置文件路径&amp;gt;&lt;/code&gt; 为你的实际信息。
:::&lt;/p&gt;
&lt;p&gt;重新加载 systemd 管理器配置，并启用 Clash 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable --now clash.service
systemctl status clash.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动后需要设置代理环境变量，如果 http 请求不需要走代理，就只需要设置 https_proxy 即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export http_proxy=&quot;http://127.0.0.1:7890&quot;
export https_proxy=&quot;http://127.0.0.1:7890&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，Clash 基本安装完毕，但是可能还是无法访问外网，因为 Clash 默认配置是 DIRECT 直连，需要修改配置文件，将 direct 直连修改为 proxy 代理。&lt;/p&gt;
&lt;h3&gt;切换配置&lt;/h3&gt;
&lt;p&gt;安装 jq 工具，用于解析 JSON 字符串：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install jq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用 curl 命令，打印当前配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 此处的端口在 external-controller 已经配置
curl --request GET --url http://127.0.0.1:9091/proxies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置文件信息太长，只看节点选择策略组选中的节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl --request GET --url http://127.0.0.1:9091/proxies | jq &apos;.proxies.&quot;🚀 节点选择&quot;.now&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次输出结果应该为 DIRECT 直连，此状态下无法翻墙。由于 &lt;code&gt;🚀 节点选择&lt;/code&gt; 这个策略组包含 emoji 和中文，所以需要进行转义，可以使用如下在线网站进行转义：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tool.oschina.net/encode?type=4&quot;&gt;转义网站：https://tool.oschina.net/encode?type=4&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;拼接为以下 curl 命令，其中 proxies 后面是你要操作的策略组转义字符串，data 内的 name 对应需要操作的完整节点名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl --request PUT --url &quot;http://127.0.0.1:9091/proxies/%F0%9F%9A%80%20%E8%8A%82%E7%82%B9%E9%80%89%E6%8B%A9&quot; --header &quot;Content-Type: text/plain&quot; --data &quot;{\&quot;name\&quot;: \&quot;&amp;lt;替换为你想要选择的节点&amp;gt;\&quot;}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常操作完不会有返回值，此时我们再次调用命令查看，可看到输出修改的节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl --request GET --url http://127.0.0.1:9091/proxies | jq &apos;.proxies.&quot;🚀 节点选择&quot;.now&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参考链接&lt;/h3&gt;
&lt;p&gt;如有不懂可自行查阅：&lt;a href=&quot;https://telegra.ph/%E8%8B%A5%E5%B9%B2%E8%BD%AF%E4%BB%B6%E8%AE%BE%E7%BD%AE-CordCloud-11-22&quot;&gt;此链接&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>三年之期</title><link>https://chaggle.github.io/posts/2025/03/01/three-year-milestone-20250301/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2025/03/01/three-year-milestone-20250301/</guid><pubDate>Fri, 28 Feb 2025 17:48:36 GMT</pubDate><content:encoded>&lt;h2&gt;历年总结&lt;/h2&gt;
&lt;p&gt;:::note
2022 年，自己完成了身份上的转变，正式开始后端的职业生涯。
:::&lt;/p&gt;
&lt;p&gt;在公司的安排下，进入教育行业，做教育行业的需求。由于体系的问题，一方面是做 TOC 的业务应用，满足客户经理、学校客户的需求；另一方面也是做 TOB 与 TOG 的需求，满足领导、教育局对平台的期望。公司的步子迈得挺大，研发的基础设施并未能跟上公司的节奏，但是好在也坚持下来了。&lt;/p&gt;
&lt;p&gt;:::tip
2023 年于自己来说，属于思维方式上的转变。
:::&lt;/p&gt;
&lt;p&gt;由工作的后端主业，变成逐步统筹研发、测试、产品、运维进行工作内容上的协调与沟通，从零到一搭建团队跟项目，并且出差不同的城市学习不同的业务经营方式、商业模式、研发方法等，思维上开拓了不少。&lt;/p&gt;
&lt;p&gt;:::warning
2024 年，在自己的意识形态上发生转变后，在后端研发的主职过程中，也兼任部分技术经理的工作。
:::&lt;/p&gt;
&lt;p&gt;大大小小的研发流程问题、部门协同的工作问题也踩坑不少。总体来说，对于技术的要求跟需求，都让步于业务的发展跟要求，属于不断试错的一个过程。对于自己的技术能力提升有限，但是团队的互相理解更进一步加深。&lt;/p&gt;
&lt;p&gt;:::caution
2025 年，因为公司的组织架构调整离开了教育行业，前往技术中台研发一卡通平台。
:::&lt;/p&gt;
&lt;p&gt;但由于 deepSeek AI 大模型的技术突破，以及国家政策的支持与导向，开始研发数据标注平台，完成跟算法组同事的配合与协调。从算法组同事身上也学习到不少的算法术语与调研大模型、使用大模型、简单模型的调参能力。至于算法理论的基础，也逐步在恶补，压力比前几年都大。毕竟后端简化工作来说，只需要提供 API 的服务能力即可，对于数理逻辑的要求并没有算法组要求高。&lt;/p&gt;
&lt;h2&gt;未来期望&lt;/h2&gt;
&lt;p&gt;1、个人发展在公司的业务选择上已经到达一个瓶颈。对于自己来说，更希望自己的技术能力有所突破，能够在公司的研发中发挥更大的价值，所以未来可能会再次审视自己职业上的发展。&lt;/p&gt;
&lt;p&gt;2、自己的技术栈大致规划如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3 年的 java 开发经验：熟悉 jvm、gc，对于锁、并发有一定自己的理解，熟悉 spring、springboot、springcloud 等常用框架的使用、配置、搭建，熟悉 Spring AOP 与 IoC。&lt;/li&gt;
&lt;li&gt;熟悉 git、github、gitlab 等代码工具的使用，熟悉代码规范、代码评审、代码合并等流程。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>GPE 监控系统分析与搭建</title><link>https://chaggle.github.io/posts/2022/08/19/gpe-monitoring-system/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2022/08/19/gpe-monitoring-system/</guid><pubDate>Fri, 19 Aug 2022 00:48:36 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Grafana + Prometheus + Exporter 监控体系的搭建&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prometheus 的监控体系&lt;/h2&gt;
&lt;h3&gt;系统层监控（需要监控的数据）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、CPU、Load、Memory、swap、disk i/o、process 等
2、网络监控：网络设备、工作负载、网络延迟、丢包率等&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;中间件及基础设施类监控&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、消息中间件：kafka、redis、RocketMQ 等消息代理/中间件
2、WEB 服务器容器：tomcat、weblogic、apache、php、spring 系列
3、数据库/缓存数据库：MySQL、PostgreSQL、MongoDB、es、redis&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;应用层监控&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;用于衡量应用程序代码状态和性能&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;白盒监控：自省指标，等待被下载&lt;/li&gt;
&lt;li&gt;黑盒监控：基于探针的监控方式，不会主动干预、影响数据&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;业务层监控&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;用于衡量应用程序的价值&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;如电商业务的销售量，QPS、dau 日活、转化率等。&lt;/li&gt;
&lt;li&gt;业务接口：登入数量，注册数、订单量、搜索量和支付量。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Prometheus 监控系统安装流程&lt;/h2&gt;
&lt;p&gt;prometheus + alertmanager + node_exporter 下载来源可以有以下三种：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、官网下载：https://prometheus.io/download/
2、github 下载：https://github.com/prometheus/prometheus
3、github 镜像下载：https://hub.fastgit.xyz/prometheus/prometheus&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下载完毕后按如下 shell 先进行解压操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 本次采用 prometheus-2.37.0.linux 版本进行操作
tar -zxvf prometheus-2.37.0.linux-amd64.tar.gz
mv prometheus-2.37.0.linux-amd64 prometheus-2.37.0

# 本次将 alertmanager 一起都装好，采用 0.24.0.linux-amd64 版本进行操作
tar -zxvf alertmanager-0.24.0.linux-amd64.tar.gz
mv alertmanager-0.24.0.linux-amd64 alertmanager-0.24.0

# 本次 node_exporter 版本为 1.3.1.linux-amd64 版本
tar -zxvf node_exporter-1.3.1.linux-amd64.tar.gz
mv node_exporter-1.3.1.linux-amd64/ node_exporter-1.3.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编辑其中的 yml 配置文件（如下所示），更改成自己需要的环境，重点在 job 那一块。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# my global config
global:
  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global &apos;evaluation_interval&apos;.
rule_files:
  # - &quot;first_rules.yml&quot;
  # - &quot;second_rules.yml&quot;

# A scrape configuration containing exactly one endpoint to scrape:
# Here it&apos;s Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=&amp;lt;job_name&amp;gt;` to any timeseries scraped from this config.
  - job_name: &quot;prometheus&quot;

    # metrics_path defaults to &apos;/metrics&apos;
    # scheme defaults to &apos;http&apos;.

    static_configs:
      - targets: [&quot;localhost:9090&quot;]

  - job_name: &quot;k12_zs_01&quot;

    static_configs:
      - targets: [&quot;localhost:9100&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要监控的内容直接在 job_name 新增字段即可，其中 node_exporter 默认监听端口为 9100 端口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 因为要查看防火墙的状态
systemctl status firewalld

# 开放端口(可批量)
firewall-cmd --zone=public --add-port=9100/tcp --permanent &amp;amp;&amp;amp; firewall-cmd --reload

# 关闭端口(可批量)
firewall-cmd --zone=public --remove-port=9100/tcp --permanent &amp;amp;&amp;amp; firewall-cmd --reload

# 查看开放端口
firewall-cmd --zone=public --list-ports
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后开始装 Grafana，其中 shell 操作如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 2022年8月19日最新版本为 grafana-enterprise-9.1.0.linux-amd64，使用二进制包更合适
wget https://dl.grafana.com/enterprise/release/grafana-enterprise-9.1.0.linux-amd64.tar.gz

# 解压之后不用改名
tar -zxvf grafana-enterprise-9.1.0.linux-amd64.tar.gz
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>2022 年 24 - 31 周总结思考</title><link>https://chaggle.github.io/posts/2022/07/30/weekly-review-2022-w24-31/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2022/07/30/weekly-review-2022-w24-31/</guid><pubDate>Sat, 30 Jul 2022 00:48:36 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;周总结还是需要写！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;24 ~ 31 周&lt;/h2&gt;
&lt;p&gt;原本在 6 月底 ~ 7 月初，也写了一篇博客，但是由于之前电脑丢失，博客数据丢失了，所以博客现在的更新停留在 6 月初的时间点。故以后的博客元数据，也需要及时同步到 github 的私有仓库上！&lt;/p&gt;
&lt;p&gt;这段时间，工作有着落了，工资也拿到手了。提升自己也是第一要义，以后的人生需要学习的东西还有许多许多！&lt;/p&gt;
&lt;p&gt;工作后所接手的第一个项目就是 Java 的 SpringCloud 项目，整合了许多中间件，包括熟悉的 MySQL、Redis、Nginx、RocketMQ，而后还加入了 Java 的微服务生态，比如 SkyWalking、Nacos、ElasticSearch、Logstash、Kibana、SpringCloud Gateway、Prometheus、Grafana 等。这些知识之前都有所了解过，但是自己并未能用到实际的生产环境中去，工作也给了自己去开发 + 部署的机会，对于自己项目工程能力的提升是很大的！&lt;/p&gt;
&lt;h2&gt;32 ~ 34 周&lt;/h2&gt;
&lt;p&gt;未来两周也是持续学习 Java 以及其生态。Java 的生态的确成熟很多，但是也笨重很多，所以好好地学习 Java，也对自己学习与使用 Go 在思想层面上有帮助。至少现在设计模式、重构手法，都能通过 Java 来训练，TDD 的思想，也能进行不断的学习与打磨！&lt;/p&gt;
&lt;p&gt;当然，除了代码水平的提升，还需要有其他的提升，比如一个项目如何使用 gitflow 进行分布式开发的流程，项目的业务场景、标书如何撰写，都是自己可以学习的东西！&lt;/p&gt;
</content:encoded></item><item><title>雪花算法</title><link>https://chaggle.github.io/posts/2022/04/11/snowflake-id-algorithm/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2022/04/11/snowflake-id-algorithm/</guid><pubDate>Mon, 11 Apr 2022 09:14:16 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;主要学习 Twitter 开源的雪花算法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;雪花算法是 Twitter 公司开源的、由 64 bit 整数组成的分布式 ID，并且在单机上递增！&lt;/p&gt;
&lt;p&gt;snowflake 64 bit 的结构如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/middleware/snowflake.png&quot; alt=&quot;snowflake&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如图：&lt;/p&gt;
&lt;p&gt;1、雪花算法默认左起第一位应该是 0，第一位在实际中并无任何作用。&lt;/p&gt;
&lt;p&gt;2、从第一位之后的 41 位都是时间戳，单位为 ms，能容纳 69 年的时间。以某个时间点为基准，基准可以自行设置，偏移量为其中的 41 位的数值。&lt;/p&gt;
&lt;p&gt;3、第 43 - 52 位，十位是工作机器的 ID 号，因为是分布式的算法，所以应该支持多台机器。从左开始的五位表示数据中心的 ID，后五位表示工作节点的 ID，最多容纳 1024 个节点。&lt;/p&gt;
&lt;p&gt;4、剩下 12 位为序列号，记录同毫秒内产生不同的 ID。每个节点每毫秒从 0 开始不断累加，最多累加 4095。&lt;/p&gt;
&lt;p&gt;故 1 ms 内最多产生 1024 x 4096 = $2^{22}$ 个不同的 ID 号。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/bwmarrin/snowflake&quot;&gt;bwmarrin/snowflake&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Go 语言的原生雪花算法实现。&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/sony/sonyflake&quot;&gt;sony/sonyflake&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;sony 公司实现的 sonyflake，在 snowflake 的基础上进行修改，基本的实现原理跟 snowflake 差不多，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/middleware/sonyflake.png&quot; alt=&quot;sonyflake&quot; /&gt;&lt;/p&gt;
&lt;p&gt;sonyflake 主要是修改了时间戳，计量单位变为了 10 ms，所以可记录的时间延长为 174 年。sequence ID 跟之前的定义一致，Machine ID 即是节点的 ID 值。&lt;/p&gt;
</content:encoded></item><item><title>【置顶】未来学习计划</title><link>https://chaggle.github.io/posts/2022/03/05/study-plan/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2022/03/05/study-plan/</guid><pubDate>Sat, 05 Mar 2022 14:10:42 GMT</pubDate><content:encoded>&lt;p&gt;:::note
对于已经掌握的知识，必须要及时进行复盘与总结，并且进行文字化的输出！
:::&lt;/p&gt;
&lt;p&gt;:::caution
2026 年学习方向已重新定位：过去以 Go、Java、数据库、中间件为代表的传统技术栈，大多已是成熟甚至被替代的知识。未来的重心是——&lt;strong&gt;理解 AI 行业、理解大模型算法、理解商业与各行各业，用 AI 赋能行业应用&lt;/strong&gt;，本质是从&quot;技术执行者&quot;转向&quot;技术价值的组织者与赋能者&quot;。
:::&lt;/p&gt;
&lt;h2&gt;学习方向的底层逻辑&lt;/h2&gt;
&lt;p&gt;技术的价值不在于技术本身，而在于&lt;strong&gt;解决业务问题&lt;/strong&gt;。过去十年是信息化、数字化的时代，技术的核心是&quot;把线下流程搬到线上&quot;；未来十年是智能化的时代，核心是&quot;用 AI 重构业务流程与决策方式&quot;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;技术是工具，行业是土壤，商业是目的——三者缺一不可&lt;/li&gt;
&lt;li&gt;大模型是通用能力底座，谁能与具体行业结合，谁就掌握价值&lt;/li&gt;
&lt;li&gt;从&quot;我能写什么代码&quot;转向&quot;这个行业需要什么能力&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
一句话总结：&lt;strong&gt;懂 AI、懂行业、懂商业，用大模型能力为各行各业做数字化、智能化升级。&lt;/strong&gt;
:::&lt;/p&gt;
&lt;h2&gt;一、理解 AI 行业&lt;/h2&gt;
&lt;p&gt;理解 AI 行业不是只关心模型评测榜单，而是理解整个产业的运转方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;产业链结构&lt;/strong&gt;：算力（芯片、服务器、云）→ 模型（基础大模型、开源模型）→ 平台（模型服务、Agent 平台）→ 应用（行业解决方案、AI 原生应用）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;格局与玩家&lt;/strong&gt;：国内外大模型厂商的定位差异、开源与闭源路线之争、云厂商的模型即服务（MaaS）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;商业模式&lt;/strong&gt;：订阅制、Token 计费、项目制交付、行业私有化部署各自的适用场景&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;政策与趋势&lt;/strong&gt;：数据要素、人工智能+行动、行业监管与合规要求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资本市场视角&lt;/strong&gt;：AI 板块的一二级市场动态，哪些环节真赚钱、哪些是概念&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
目标：能够对 AI 行业说出&quot;产业链上每个环节靠什么赚钱、护城河在哪、未来 3-5 年往哪走&quot;。
:::&lt;/p&gt;
&lt;h2&gt;二、理解大模型算法&lt;/h2&gt;
&lt;p&gt;不求亲手训练千亿参数模型，但必须吃透原理，才能做出正确的工程决策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机器学习/深度学习基础&lt;/strong&gt;：损失函数、优化器、正则化、CNN/RNN 到 Transformer 的演进&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transformer 与注意力机制&lt;/strong&gt;：Self-Attention、多头注意力、位置编码、KV Cache&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练范式&lt;/strong&gt;：预训练（Pretrain）→ 指令微调（SFT）→ 人类反馈强化学习（RLHF/DPO）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推理与部署&lt;/strong&gt;：量化、蒸馏、投机采样、vLLM 等推理框架、显存与吞吐优化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用技术栈&lt;/strong&gt;：Prompt 工程、RAG（检索增强生成）、Function Calling、Agent（规划/记忆/工具）、微调（LoRA/QLoRA）、模型评测&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多模态方向&lt;/strong&gt;：文本、图像、音视频统一理解与生成的基本原理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
目标：能读懂模型技术报告的关键指标，能判断某个业务场景该用 RAG、微调还是 Agent，能讲清推理成本的构成。
:::&lt;/p&gt;
&lt;h2&gt;三、理解商业与各个行业&lt;/h2&gt;
&lt;p&gt;技术人的盲区往往在商业与行业认知，这正是 AI 赋能价值最大的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;商业基础&lt;/strong&gt;：商业模式画布、定价逻辑、成本结构、客户生命周期、ROI 计算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行业研究方法&lt;/strong&gt;：宏观（政策、人口、技术周期）→ 中观（产业链、竞争格局）→ 微观（企业、产品、流程）三层分析法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重点行业调研&lt;/strong&gt;：制造、能源、金融、政务、医疗、教育、零售——各自的核心业务、数字化现状、痛点与付费能力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据视角&lt;/strong&gt;：每个行业的&quot;数据金矿&quot;在哪里，数据如何变成资产、变成模型训练语料&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
方法与阅读材料：行业研究报告（券商研报、咨询报告）、上市公司财报与招股书、行业会议与白皮书。每研究一个行业，输出一篇行业分析博客。
:::&lt;/p&gt;
&lt;h2&gt;四、AI 赋能行业应用&lt;/h2&gt;
&lt;p&gt;这是学习的落脚点——把前面的认知转化为真实的落地能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景识别&lt;/strong&gt;：从行业痛点中筛选&quot;高价值、低风险、数据可得&quot;的 AI 应用场景&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案设计&lt;/strong&gt;：大模型 + RAG + Agent + 原有系统的融合架构，规划数据底座&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;落地路径&lt;/strong&gt;：POC 验证 → 小范围试点 → 规模化推广；效果评估与 ROI 核算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数字化/信息化衔接&lt;/strong&gt;：AI 应用依赖数据质量与系统打通，理解数据中台、数据治理、主数据、指标体系&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交付与运营&lt;/strong&gt;：项目制与产品化之间的平衡，模型迭代与运营机制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution
注意：AI 落地最大的瓶颈往往不是模型能力，而是数据质量、组织协同与业务流程改造。理解这一点，比理解算法更重要。
:::&lt;/p&gt;
&lt;h2&gt;五、技术底座（保留但不作为主线）&lt;/h2&gt;
&lt;p&gt;以下能力作为 AI 落地的基础设施继续沉淀，不再作为主要学习方向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大数据底座：HDFS、Yarn、Hive、Spark、Flink、Tez、MapReduce（已有系统总结）&lt;/li&gt;
&lt;li&gt;数据中台：数据治理、数据服务、指标体系、血缘管理&lt;/li&gt;
&lt;li&gt;传统工程能力：Go / Java 保持熟练即可，不投入新学习&lt;/li&gt;
&lt;li&gt;基础设施：Linux、Docker、K8s、MySQL、Redis、Kafka 等按需复习&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;六、行动清单&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;行动&lt;/th&gt;
&lt;th&gt;产出&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;近期&lt;/td&gt;
&lt;td&gt;补全大模型算法知识体系，跑通一个 RAG + Agent 的本地应用&lt;/td&gt;
&lt;td&gt;技术博客 2-3 篇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;近期&lt;/td&gt;
&lt;td&gt;研究一个重点行业（优先政务/制造/能源）的数字化现状&lt;/td&gt;
&lt;td&gt;行业分析博客 1 篇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中期&lt;/td&gt;
&lt;td&gt;参与/主导一个 AI 赋能行业的 POC 项目&lt;/td&gt;
&lt;td&gt;方案文档 + 复盘博客&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中期&lt;/td&gt;
&lt;td&gt;每季度阅读 2 家 AI 相关公司财报或研报&lt;/td&gt;
&lt;td&gt;投资/行业笔记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长期&lt;/td&gt;
&lt;td&gt;建立&quot;AI + 行业&quot;的方法论，形成可复用的方案框架&lt;/td&gt;
&lt;td&gt;体系化博客&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::note
所有学习必须文字化输出到本博客——博客既是知识管理工具，也是未来个人 AI 助手的语料库。输入而不输出，等于没有学习。
:::&lt;/p&gt;
</content:encoded></item><item><title>1791. 找出星型图的中心节点</title><link>https://chaggle.github.io/posts/2022/02/18/1791-find-center-of-star-graph/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2022/02/18/1791-find-center-of-star-graph/</guid><pubDate>Fri, 18 Feb 2022 15:02:49 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/find-center-of-star-graph/&quot;&gt;1791. 找出星型图的中心节点&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;有一个无向的 星型 图，由 n 个编号从 1 到 n 的节点组成。星型图有一个 中心 节点，并且恰有 n - 1 条边将中心节点与其他每个节点连接起来。

给你一个二维整数数组 edges ，其中 edges[i] = [ui, vi] 表示在节点 ui 和 vi 之间存在一条边。请你找出并返回 edges 所表示星型图的中心节点。

示例 1：

输入：edges = [[1,2],[2,3],[4,2]]
输出：2
解释：如上图所示，节点 2 与其他每个节点都相连，所以节点 2 是中心节点。
示例 2：

输入：edges = [[1,2],[5,1],[1,3],[1,4]]
输出：1

提示：

3 &amp;lt;= n &amp;lt;= 105
edges.length == n - 1
edges[i].length == 2
1 &amp;lt;= ui, vi &amp;lt;= n
ui != vi
题目数据给出的 edges 表示一个有效的星型图
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;方法一：使用计数排序的思路，可以用 map 实现，也能用结构体，遍历一遍后取出现次数最多的节点即可。&lt;/p&gt;
&lt;p&gt;方法二：利用星型图的性质——所有其他节点都只与中心节点连接，所以只需要判断前两条边中的两个节点即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func findCenter(edges [][]int) int {
    /*
    方法 1：遍历二维数组，时间复杂度O(n ^ 2)
    n, m := len(edges), len(edges[0])
    hash := map[int]int{}
    for i := 0; i &amp;lt; n; i++ {
        for j := 0; j &amp;lt; m; j++ {
            hash[edges[i][j]]++
        }
    }
    p := hash[0]
    q := 0
    for i, v := range hash {
        if v &amp;gt; p {
            p = v
            q = i
        }
    }
    return q */

    //方法二 ：由于是星型线，所以每个子数组两个值必有一个值相等
    if edges[0][0] == edges[1][0] || edges[0][0] == edges[1][1]{
        return edges[0][0]
    }
    return edges[0][1]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n ^ 2) / O(1)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n) / O(1)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>1984. 学生分数的最小差值</title><link>https://chaggle.github.io/posts/2022/02/11/1984-minimum-difference-between-highest-and-lowest-of-k-scores/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2022/02/11/1984-minimum-difference-between-highest-and-lowest-of-k-scores/</guid><pubDate>Fri, 11 Feb 2022 02:42:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/minimum-difference-between-highest-and-lowest-of-k-scores/&quot;&gt;1984. 学生分数的最小差值&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个 下标从 0 开始 的整数数组 nums ，其中 nums[i] 表示第 i 名学生的分数。另给你一个整数 k 。

从数组中选出任意 k 名学生的分数，使这 k 个分数间 最高分 和 最低分 的 差值 达到 最小化 。

返回可能的 最小差值 。

示例 1：

输入：nums = [90], k = 1
输出：0
解释：选出 1 名学生的分数，仅有 1 种方法：
- [90] 最高分和最低分之间的差值是 90 - 90 = 0
可能的最小差值是 0
示例 2：

输入：nums = [9,4,1,7], k = 2
输出：2
解释：选出 2 名学生的分数，有 6 种方法：
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 4 = 5
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 1 = 8
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 7 = 2
- [9,4,1,7] 最高分和最低分之间的差值是 4 - 1 = 3
- [9,4,1,7] 最高分和最低分之间的差值是 7 - 4 = 3
- [9,4,1,7] 最高分和最低分之间的差值是 7 - 1 = 6
可能的最小差值是 2

提示：

1 &amp;lt;= k &amp;lt;= nums.length &amp;lt;= 1000
0 &amp;lt;= nums[i] &amp;lt;= 10^5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;最简单的方法就是排序之后使用滑动窗口。要找最小差值，两个数需要处于排序之后居中的位置，所以排序后用固定长度为 k 的窗口从左到右滑动一遍，取窗口内最大值与最小值之差的最小值即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func minimumDifference(nums []int, k int) int {
    sort.Ints(nums) //go sort
    ans := math.MaxInt32
    for i, num := range nums[:len(nums) - k + 1] {
        ans = min(ans, nums[i + k - 1] - num)
    }
    return ans
}

func min(a, b int) int {
    if a &amp;gt; b {
        return b
    }
    return a
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>707. 设计链表</title><link>https://chaggle.github.io/posts/2022/02/10/707-design-linked-list/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2022/02/10/707-design-linked-list/</guid><pubDate>Thu, 10 Feb 2022 04:54:06 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/design-linked-list/&quot;&gt;707. 设计链表&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性：val 和 next。val 是当前节点的值，next 是指向下一个节点的指针/引用。如果要使用双向链表，则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能：

get(index)：获取链表中第 index 个节点的值。如果索引无效，则返回-1。
addAtHead(val)：在链表的第一个元素之前添加一个值为 val 的节点。插入后，新节点将成为链表的第一个节点。
addAtTail(val)：将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val)：在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度，则该节点将附加到链表的末尾。如果 index 大于链表长度，则不会插入节点。如果index小于0，则在头部插入节点。
deleteAtIndex(index)：如果索引 index 有效，则删除链表中的第 index 个节点。


MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-&amp;gt; 2-&amp;gt; 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-&amp;gt; 3
linkedList.get(1);            //返回3

所有val值都在 [1, 1000] 之内。
操作次数将在  [1, 1000] 之内。
请不要使用内置的 LinkedList 库。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 Go 语言实现基础的数据结构——单链表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type MyLinkedList struct {
 Val  int
 Next *MyLinkedList
}

func Constructor() MyLinkedList {
    // 该节点为头结点，不会用到
 return MyLinkedList{
        Val: -1,
        Next: nil,
    }
}

func (this *MyLinkedList) Get(index int) int {
 tmp := this.Next
 for i := 0; tmp != nil; i++ {
        if i == index {
            return tmp.Val
        } else {
            tmp = tmp.Next
        }
 }
    return -1
}

func (this *MyLinkedList) AddAtHead(val int) {
 this.Next = &amp;amp;MyLinkedList{
  Val:  val,
  Next: this.Next,
 }
}

func (this *MyLinkedList) AddAtTail(val int) {
 tmp := this
 for tmp.Next != nil {
  tmp = tmp.Next
 }
 tmp.Next = &amp;amp;MyLinkedList{
  Val:  val,
  Next: nil,
 }
}

func (this *MyLinkedList) AddAtIndex(index int, val int) {
 if index &amp;lt;= 0 {
  this.AddAtHead(val)
        return
 }
    tmp := this.Next
    // 遍历到index处
    for i := 1; i &amp;lt; index &amp;amp;&amp;amp; tmp.Next != nil; i++ {
        tmp = tmp.Next
    }
    // index超出链表长度
    if tmp == nil {
        return
    }
    tmp.Next = &amp;amp;MyLinkedList{
        Val:  val,
        Next: tmp.Next,
    }
}

func (this *MyLinkedList) DeleteAtIndex(index int) {
    // 空链表
    if this.Next == nil {
        return
    }
 if index &amp;lt;= 0 {
  this.Next = this.Next.Next
  return
 }
 tmp := this.Next
 for i := 1; i &amp;lt; index &amp;amp;&amp;amp; tmp.Next != nil; i++ {
  tmp = tmp.Next
 }
    // index超出链表长度
 if tmp.Next == nil {
  return
 }
 tmp.Next = tmp.Next.Next
}


/**
 * Your MyLinkedList object will be instantiated and called as such:
 * obj := Constructor();
 * param_1 := obj.Get(index);
 * obj.AddAtHead(val);
 * obj.AddAtTail(val);
 * obj.AddAtIndex(index,val);
 * obj.DeleteAtIndex(index);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 91 1206. 设计跳表</title><link>https://chaggle.github.io/posts/2021/12/09/day-91-1206-design-skiplist/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/12/09/day-91-1206-design-skiplist/</guid><pubDate>Thu, 09 Dec 2021 08:35:22 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/design-skiplist/&quot;&gt;1206. 设计跳表&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;不使用任何库函数，设计一个跳表。

跳表是在 O(log(n)) 时间内完成增加、删除、搜索操作的数据结构。

跳表相比于树堆与红黑树，其功能与性能相当，并且跳表的代码长度相较下更短，其设计思想与链表相似。

例如，一个跳表包含 [30, 40, 50, 60, 70, 90]，然后增加 80、45 到跳表中，以下图的方式操作：

跳表中有很多层，每一层是一个短的链表。

在第一层的作用下，增加、删除和搜索操作的时间复杂度不超过 O(n)。

跳表的每一个操作的平均时间复杂度是 O(log(n))，空间复杂度是 O(n)。

在本题中，你的设计应该要包含这些函数：

bool search(int target) : 返回target是否存在于跳表中。

void add(int num): 插入一个元素到跳表。

bool erase(int num): 在跳表中删除一个值，如果 num 不存在，直接返回 false.

如果存在多个 num ，删除其中任意一个即可。

了解更多 : https://en.wikipedia.org/wiki/Skip_list

注意，跳表中可能存在多个相同的值，你的代码需要处理这种情况。

样例:

Skiplist skiplist = new Skiplist();

skiplist.add(1);
skiplist.add(2);
skiplist.add(3);
skiplist.search(0);   // 返回 false
skiplist.add(4);
skiplist.search(1);   // 返回 true
skiplist.erase(0);    // 返回 false，0 不在跳表中
skiplist.erase(1);    // 返回 true
skiplist.search(1);   // 返回 false，1 已被擦除
约束条件:

0 &amp;lt;= num, target &amp;lt;= 20000
最多调用 50000 次 search, add, 以及 erase操作。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;没什么时间重新写一个容器，现在偷点懒直接用 vector 的库函数，用它存放跳表每一层的后继指针，节点的层数通过随机数生成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;struct Node
{
    int key;
    vector&amp;lt;Node*&amp;gt; nexts;
    Node(int level,int k) : nexts(level + 1),key(k) {};
};

class Skiplist {
private:
    Node* head;
    int allLevel = -1;
public:
    Skiplist() {
        head = new Node(31,-1);
    }

    bool search(int key) {
        Node* cur = head;
        for(int curLevel = allLevel; curLevel &amp;gt;= 0; curLevel--)
        {
            while(cur -&amp;gt; nexts[curLevel] &amp;amp;&amp;amp; cur -&amp;gt; nexts[curLevel] -&amp;gt; key &amp;lt; key) {
                cur = cur -&amp;gt; nexts[curLevel];
            }
            if(cur -&amp;gt; nexts[curLevel] &amp;amp;&amp;amp; cur -&amp;gt; nexts[curLevel] -&amp;gt; key == key)
                return true;
        }
        return false;
    }
    void add(int key) {
        int newLevel = getLevel();
        allLevel = max(allLevel, newLevel);
        Node* cur = head;
        Node* newNode = new Node(newLevel, key);
        for(int curLevel = allLevel; curLevel &amp;gt;= 0; curLevel--)
        {
            while(cur -&amp;gt; nexts[curLevel] &amp;amp;&amp;amp; cur -&amp;gt; nexts[curLevel] -&amp;gt; key &amp;lt; key){
                cur = cur -&amp;gt; nexts[curLevel];
            }
            if(curLevel &amp;lt;= newLevel)
            {
                newNode -&amp;gt; nexts[curLevel] = cur -&amp;gt; nexts[curLevel];
                cur -&amp;gt; nexts[curLevel] = newNode;
            }
        }
    }
    bool erase(int key) {
        Node* cur = head;
        bool flag = false;
        for(int curLevel = allLevel; curLevel &amp;gt;= 0; curLevel--)
        {
            while(cur -&amp;gt; nexts[curLevel] &amp;amp;&amp;amp; cur -&amp;gt; nexts[curLevel] -&amp;gt; key &amp;lt; key) {
                cur = cur -&amp;gt; nexts[curLevel];
            }
            if(cur -&amp;gt; nexts[curLevel] &amp;amp;&amp;amp; cur -&amp;gt; nexts[curLevel] -&amp;gt; key == key) {
                flag = true;
                Node* tmp = cur -&amp;gt; nexts[curLevel] -&amp;gt; nexts[curLevel];
                cur -&amp;gt; nexts[curLevel] -&amp;gt; nexts[curLevel] = nullptr;
                cur -&amp;gt; nexts[curLevel] = tmp;
            }
        }
        if(flag == false) return false;
        return true;
    }
    int getLevel() {
        int ans = 0;
        while(ans &amp;lt; 32 &amp;amp;&amp;amp; rand() &amp;lt; RAND_MAX * 0.25) ans++;
        return ans;
    }
};
/**
 * Your Skiplist object will be instantiated and called as such:
 * Skiplist* obj = new Skiplist();
 * bool param_1 = obj-&amp;gt;search(target);
 * obj-&amp;gt;add(num);
 * bool param_3 = obj-&amp;gt;erase(num);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(nlogn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n ^ 2)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 90 1054. 距离相等的条形码</title><link>https://chaggle.github.io/posts/2021/12/08/day-90-1054-distant-barcodes/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/12/08/day-90-1054-distant-barcodes/</guid><pubDate>Wed, 08 Dec 2021 07:28:10 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/distant-barcodes/&quot;&gt;1054. 距离相等的条形码&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;在一个仓库里，有一排条形码，其中第 i 个条形码为 barcodes[i]。

请你重新排列这些条形码，使其中两个相邻的条形码 不能相等。

你可以返回任何满足该要求的答案，此题保证存在答案。

 

示例 1：

输入：[1,1,1,2,2,2]
输出：[2,1,2,1,2,1]
示例 2：

输入：[1,1,1,1,2,2,3,3]
输出：[1,3,1,3,2,1,2,1]
 

提示：

1 &amp;lt;= barcodes.length &amp;lt;= 10000
1 &amp;lt;= barcodes[i] &amp;lt;= 10000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;统计每个条形码的出现次数并按次数降序排序，然后进行间隔插入：先填满偶数下标，再回填奇数下标，从而保证相邻的条形码不相等。这是最后两日了，明天打卡 91 日后就要考试了，所以停止更新。直到 12 月 30 日开始，从头复盘这 91 天，每日复盘 4 题左右，顺便开始阅读相应的论文，展开研究型的工作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; rearrangeBarcodes(vector&amp;lt;int&amp;gt;&amp;amp; barcodes) {
        int n = barcodes.size();
        unordered_map&amp;lt;int, int&amp;gt; up;
        for(int i : barcodes) up[i]++;
        vector&amp;lt;pair&amp;lt;int,int&amp;gt;&amp;gt; ans;
        for(auto [x, fx] : up) ans.push_back(pair&amp;lt;int, int&amp;gt;{x, fx});

        sort(ans.begin(), ans.end(),
        [&amp;amp;](const pair&amp;lt;int, int&amp;gt; &amp;amp;a, const pair&amp;lt;int, int&amp;gt; &amp;amp;b)
        {
            return a.second &amp;gt; b.second;
        });

        vector&amp;lt;int&amp;gt; res(n);
        int m = 0;
        for(auto [x, fx] : ans)
        {
            for (int i = 0 ; i &amp;lt; fx; i++)
            {
                if (m &amp;gt;= n) m = 1;
                res[m] = x;
                m += 2;
            }
        }
        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(nlogn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 89 378. 有序矩阵中第 K 小的元素</title><link>https://chaggle.github.io/posts/2021/12/07/day-89-378-kth-smallest-element-in-a-sorted-matrix/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/12/07/day-89-378-kth-smallest-element-in-a-sorted-matrix/</guid><pubDate>Tue, 07 Dec 2021 00:38:44 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix/&quot;&gt;378. 有序矩阵中第 K 小的元素&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个 n x n 矩阵 matrix ，其中每行和每列元素均按升序排序，找到矩阵中第 k 小的元素。
请注意，它是 排序后 的第 k 小元素，而不是第 k 个 不同 的元素。

 

示例 1：

输入：matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
输出：13
解释：矩阵中的元素为 [1,5,9,10,11,12,13,13,15]，第 8 小元素是 13
示例 2：

输入：matrix = [[-5]], k = 1
输出：-5
 

提示：

n == matrix.length
n == matrix[i].length
1 &amp;lt;= n &amp;lt;= 300
-109 &amp;lt;= matrix[i][j] &amp;lt;= 109
题目数据 保证 matrix 中的所有行和列都按 非递减顺序 排列
1 &amp;lt;= k &amp;lt;= n^2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;题目中最合适的办法理应是二分的思路：对值域进行二分，check 函数利用矩阵每行每列均有序的特性，从矩阵左下角出发统计小于等于 mid 的元素个数，据此收缩范围，最终得到第 k 小的元素。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool check(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; matrix, int mid, int k, int n) {
        int i = n - 1, j = 0, num = 0;
        while (i &amp;gt;= 0 &amp;amp;&amp;amp; j &amp;lt; n)
        {
            if (matrix[i][j] &amp;lt;= mid)
            {
                num += i + 1;
                j++;
            } else i--;
        }
        return num &amp;gt;= k;
    }

    int kthSmallest(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; matrix, int k) {
        int n = matrix.size();
        int l = matrix[0][0], r = matrix[n - 1][n - 1];
        while(l &amp;lt; r)
        {
            int mid = l + ((r - l) &amp;gt;&amp;gt; 1);
            if (check(matrix, mid, k, n)) r = mid;
            else l = mid + 1;
        }
        return l;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(nlogn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(1)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 88 451. 根据字符出现频率排序</title><link>https://chaggle.github.io/posts/2021/12/06/day-88-451-sort-characters-by-frequency/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/12/06/day-88-451-sort-characters-by-frequency/</guid><pubDate>Mon, 06 Dec 2021 14:06:57 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/sort-characters-by-frequency/&quot;&gt;451. 根据字符出现频率排序&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个字符串，请将字符串里的字符按照出现的频率降序排列。

示例 1:

输入:
&quot;tree&quot;

输出:
&quot;eert&quot;

解释:
&apos;e&apos;出现两次，&apos;r&apos;和&apos;t&apos;都只出现一次。
因此&apos;e&apos;必须出现在&apos;r&apos;和&apos;t&apos;之前。此外，&quot;eetr&quot;也是一个有效的答案。
示例 2:

输入:
&quot;cccaaa&quot;

输出:
&quot;cccaaa&quot;

解释:
&apos;c&apos;和&apos;a&apos;都出现三次。此外，&quot;aaaccc&quot;也是有效的答案。
注意&quot;cacaca&quot;是不正确的，因为相同的字母必须放在一起。
示例 3:

输入:
&quot;Aabb&quot;

输出:
&quot;bbAa&quot;

解释:
此外，&quot;bbaA&quot;也是一个有效的答案，但&quot;Aabb&quot;是不正确的。
注意&apos;A&apos;和&apos;a&apos;被认为是两种不同的字符。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;如题目所说，手写一个 cmp 比较函数，然后按照字符出现的频率进行排序，返回排序后的字符串即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    string frequencySort(string s) {
        unordered_map&amp;lt;char, int&amp;gt; up;
        for(auto i : s) up[i]++;
        sort(s.begin(), s.end(), [&amp;amp;](const char &amp;amp;a, const char &amp;amp;b) {
            return up[a] == up[b] ? a &amp;gt; b : up[a] &amp;gt; up[b];
        });
        return s;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(logn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 87 23. 合并K个升序链表</title><link>https://chaggle.github.io/posts/2021/12/05/day-87-23-merge-k-sorted-lists/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/12/05/day-87-23-merge-k-sorted-lists/</guid><pubDate>Sun, 05 Dec 2021 13:29:57 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/merge-k-sorted-lists/&quot;&gt;23. 合并 K 个升序链表&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个链表数组，每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中，返回合并后的链表。

 

示例 1：

输入：lists = [[1,4,5],[1,3,4],[2,6]]
输出：[1,1,2,3,4,4,5,6]
解释：链表数组如下：
[
  1-&amp;gt;4-&amp;gt;5,
  1-&amp;gt;3-&amp;gt;4,
  2-&amp;gt;6
]
将它们合并到一个有序链表中得到。
1-&amp;gt;1-&amp;gt;2-&amp;gt;3-&amp;gt;4-&amp;gt;4-&amp;gt;5-&amp;gt;6
示例 2：

输入：lists = []
输出：[]
示例 3：

输入：lists = [[]]
输出：[]
 

提示：

k == lists.length
0 &amp;lt;= k &amp;lt;= 10^4
0 &amp;lt;= lists[i].length &amp;lt;= 500
-10^4 &amp;lt;= lists[i][j] &amp;lt;= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本题目在 day 69 时使用分治的思想做过，今日使用小顶堆解决：用优先队列每次取出当前最小的节点接入结果链表，再将其后继节点入队。这两日时间花费有些多，故今日不手写堆。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeKLists(vector&amp;lt;ListNode*&amp;gt;&amp;amp; lists) {
        auto min = [](ListNode *a, ListNode *b)
        {
            return a -&amp;gt; val &amp;gt; b -&amp;gt; val;
            };
        priority_queue&amp;lt;ListNode*, vector&amp;lt;ListNode*&amp;gt;, decltype(min)&amp;gt; ans(min);
        auto *root = new ListNode();
        for(auto p:  lists)
        {
            if(p != nullptr) ans.push(p);
        }
        ListNode *cur = root;
        while(!ans.empty())
        {
            ListNode *tmp = ans.top(); ans.pop();
            if(tmp -&amp;gt; next != nullptr) ans.push(tmp-&amp;gt;next);
            tmp -&amp;gt; next = cur -&amp;gt; next;
            cur -&amp;gt; next = tmp;
            cur = cur -&amp;gt; next;
        }
        return root -&amp;gt; next;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(logn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 86 1046. 最后一块石头的重量</title><link>https://chaggle.github.io/posts/2021/12/04/day-86-1046-last-stone-weight/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/12/04/day-86-1046-last-stone-weight/</guid><pubDate>Sat, 04 Dec 2021 09:42:18 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/last-stone-weight/&quot;&gt;1046. 最后一块石头的重量&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;有一堆石头，每块石头的重量都是正整数。

每一回合，从中选出两块 最重的 石头，然后将它们一起粉碎。

假设石头的重量分别为 x 和 y，且 x &amp;lt;= y。那么粉碎的可能结果如下：

如果 x == y，那么两块石头都会被完全粉碎；

如果 x != y，那么重量为 x 的石头将会完全粉碎，而重量为 y 的石头新重量为 y-x。

最后，最多只会剩下一块石头。

返回此石头的重量。如果没有石头剩下，就返回 0。

 

示例：

输入：[2,7,4,1,8,1]
输出：1
解释：
先选出 7 和 8，得到 1，所以数组转换为 [2,4,1,1,1]，
再选出 2 和 4，得到 2，所以数组转换为 [2,1,1,1]，
接着是 2 和 1，得到 1，所以数组转换为 [1,1,1]，
最后选出 1 和 1，得到 0，最终数组转换为 [1]，这就是最后剩下那块石头的重量。
 

提示：

1 &amp;lt;= stones.length &amp;lt;= 30
1 &amp;lt;= stones[i] &amp;lt;= 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;今日仍使用最大堆的办法，拒绝使用库函数自己手写：每次取出最重的两块石头，相等则全部粉碎，不等则把差值放回堆中，直到只剩一块或没有石头。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    void buildmaxheap(vector&amp;lt;int&amp;gt;&amp;amp; stones, int i, int n) {
        int p = stones[i];
        for (int j = 2 * i + 1; j &amp;lt; n; j = 2 * j + 1)
        {
            if (j &amp;lt; n - 1 &amp;amp;&amp;amp; stones[j] &amp;lt; stones[j + 1]) j++;
            if (p &amp;gt;= stones[j]) break;
            else
            {
                stones[i] = stones[j];
                i = j;
            }
        }
        stones[i] = p;
    }

    int lastStoneWeight(vector&amp;lt;int&amp;gt;&amp;amp; stones) {
        int max1, max2;
        int n = stones.size();
        for (int i = (n - 1) / 2; i &amp;gt;= 0; i--) buildmaxheap(stones, i, n);
        while (n &amp;gt; 1)
        {
            max1 = stones[0];
            stones[0] = stones[--n];
            buildmaxheap(stones, 0, n);
            max2 = stones[0];
            if (max1 == max2)
            {
                if(n &amp;lt; 2) return 0;
                else stones[0] = stones[--n];
            }
            else stones[0] = abs(max1 - max2);
            buildmaxheap(stones, 0, n);
        }
        return stones[0];
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(nlogn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 85 215. 数组中的第K个最大元素</title><link>https://chaggle.github.io/posts/2021/12/03/day-85-215-kth-largest-element-in-an-array/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/12/03/day-85-215-kth-largest-element-in-an-array/</guid><pubDate>Fri, 03 Dec 2021 14:29:54 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/kth-largest-element-in-an-array/&quot;&gt;215. 数组中的第K个最大元素&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定整数数组 nums 和整数 k，请返回数组中第 k 个最大的元素。

请注意，你需要找的是数组排序后的第 k 个最大的元素，而不是第 k 个不同的元素。

 

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
 

提示：

1 &amp;lt;= k &amp;lt;= nums.length &amp;lt;= 104
-104 &amp;lt;= nums[i] &amp;lt;= 10^4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;今日的题目很经典，一个可以复习快排的思想，一个可以复习如何建立堆。这里采用手写大顶堆：先构建堆，再依次把堆顶与末尾元素交换并向下调整，经过 k - 1 次调整后，堆顶即为第 k 大的元素。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution{
public:
    void heap_sort(vector&amp;lt;int&amp;gt;&amp;amp;ans, int i, int n) {
        int l = i * 2 + 1, r = i * 2 + 2, max = i;

        if (l &amp;lt; n &amp;amp;&amp;amp; ans[l] &amp;gt; ans[max]) max = l;
        if (r &amp;lt; n &amp;amp;&amp;amp; ans[r] &amp;gt; ans[max]) max = r;

        if (max != i)
        {
            swap(ans[i], ans[max]);
            heap_sort(ans, max, n);
        }
    }

    void buildHeap(vector&amp;lt;int&amp;gt;&amp;amp; ans, int n) {
        for (int i = n / 2; i &amp;gt;= 0; --i) heap_sort(ans, i, n);
    }

    int findKthLargest(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) {
        int n = nums.size();
        buildHeap(nums, n);
        for (int i = nums.size() - 1; i &amp;gt;= nums.size() - k + 1; i--)
        {
            swap(nums[0], nums[i]);
            --n;
            heap_sort(nums, 0, n);
        }
        return nums[0];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(nlogn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(logn)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 84 28. 实现 strStr()</title><link>https://chaggle.github.io/posts/2021/12/02/day-84-28-implement-strstr/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/12/02/day-84-28-implement-strstr/</guid><pubDate>Thu, 02 Dec 2021 12:17:07 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/implement-strstr/&quot;&gt;28. 实现 strStr()&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;实现 strStr() 函数。

给你两个字符串 haystack 和 needle 在 haystack 字符串中

找出 needle 字符串出现的第一个位置（下标从 0 开始）。

如果不存在，则返回  -1 。

 

说明：

当 needle 是空字符串时我们应当返回什么值呢？

对于本题而言，当 needle 是空字符串时我们应当返回 0 。

这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。

 

示例 1：

输入：haystack = &quot;hello&quot;, needle = &quot;ll&quot;
输出：2
示例 2：

输入：haystack = &quot;aaaaa&quot;, needle = &quot;bba&quot;
输出：-1
示例 3：

输入：haystack = &quot;&quot;, needle = &quot;&quot;
输出：0
 

提示：

0 &amp;lt;= haystack.length, needle.length &amp;lt;= 5 * 104
haystack 和 needle 仅由小写英文字符组成
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;经典题目，一眼看出是 KMP 算法的考察，将 haystack 视为主串 s，needle 视为模式串 p 即可。昨日做的是 BF（暴力匹配）算法，今日做 KMP，我刚好相反。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    //KMP
    int strStr(string s, string p) {
        int n = s.size(), m = p.size();
        if(m == 0) return 0;

        s.insert(s.begin(), &apos; &apos;);
        p.insert(p.begin(), &apos; &apos;);

        vector&amp;lt;int&amp;gt; next(m + 1);

        for(int i = 2, j = 0; i &amp;lt;= m; i++) {
            while(j &amp;amp;&amp;amp; p[i] != p[j + 1]) j = next[j];
            if(p[i] == p[j + 1]) j++;
            next[i] = j;
        }

        for(int i = 1, j = 0; i &amp;lt;= n; i++) {
            while(j &amp;amp;&amp;amp; s[i] != p[j + 1]) j = next[j];
            if(s[i] == p[j + 1]) j++;
            if(j == m) return i - m;
        }
        return -1;
    }

    //BF算法
    int strStr(string s, string t) {
        int m = s.size(), n = t.size();
        if (m &amp;lt; n) return -1;
        if (n == 0) return 0;
        int i = 0, j = 0, l = 0;
        while (i &amp;lt; m &amp;amp;&amp;amp; j &amp;lt; n)
        {
            if(s[i] == t[j]) {
                i++;
                j++;
            } else {
                l++;
                i = l;
                j = 0;
            }
        }
        if (j == n) return l;
        return -1;
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(m + n) // KMP&lt;/li&gt;
&lt;li&gt;空间复杂度：O(m + n) // KMP&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 82 47. 全排列 II</title><link>https://chaggle.github.io/posts/2021/11/30/day-82-47-permutations-ii/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/30/day-82-47-permutations-ii/</guid><pubDate>Tue, 30 Nov 2021 07:17:24 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/permutations-ii/&quot;&gt;47. 全排列 II&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个可包含重复数字的序列 nums ，按任意顺序 返回所有不重复的全排列。

示例 1：

输入：nums = [1,1,2]
输出：
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2：

输入：nums = [1,2,3]
输出：[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
 

提示：

1 &amp;lt;= nums.length &amp;lt;= 8
-10 &amp;lt;= nums[i] &amp;lt;= 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;回溯法，只不过今日的题目较为简单。核心的去重思路：先对数组排序，在同一层递归中，若当前数字与前一个数字相同且前一个数字尚未使用（!vis[j - 1]），则跳过，从而避免生成重复的全排列。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; vis;

    void backtrack(
        vector&amp;lt;int&amp;gt;&amp;amp; nums, vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; ans, int i, vector&amp;lt;int&amp;gt;&amp;amp; res
    ) {
        int n = nums.size();
        if (i == n) {
            ans.emplace_back(res);
            return;
        }
        for (int j = 0; j &amp;lt; n; j++) {
            if (vis[j] || (j &amp;gt; 0 &amp;amp;&amp;amp; nums[j] == nums[j - 1] &amp;amp;&amp;amp; !vis[j - 1])) continue;
            res.emplace_back(nums[j]);
            vis[j] = 1;
            backtrack(nums, ans, i + 1, res);
            vis[j] = 0;
            res.pop_back();
        }
    }

    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; permuteUnique(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;
        vector&amp;lt;int&amp;gt; res;
        vis.resize(nums.size());
        sort(nums.begin(), nums.end());
        backtrack(nums, ans, 0, res);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O($2 ^ n * n$)&lt;/li&gt;
&lt;li&gt;空间复杂度：O($n$)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 81 40. 组合总和 II</title><link>https://chaggle.github.io/posts/2021/11/29/day-81-40-combination-sum-ii/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/29/day-81-40-combination-sum-ii/</guid><pubDate>Mon, 29 Nov 2021 09:31:37 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/combination-sum-ii/&quot;&gt;40. 组合总和 II&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个数组 candidates 和一个目标数 target 

找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

注意：解集不能包含重复的组合。 

 

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
 

提示:

1 &amp;lt;= candidates.length &amp;lt;= 100
1 &amp;lt;= candidates[i] &amp;lt;= 50
1 &amp;lt;= target &amp;lt;= 30
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;与昨日一样的回溯法。官方题解可以使用 pair 存储，但其实与缓存过程中的数据类型 vector&amp;lt;int&amp;gt; 是一样的。由于每个元素只能使用一次且解集不能包含重复组合，先对数组排序，在同一层递归中跳过重复元素即可去重。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; res;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;
    vector&amp;lt;int&amp;gt; tmp;
public:
    void dfs(int start, int target) {
        int n = res.size();
        if(target == 0) {
            ans.push_back(tmp);
            return;
        }

        for(int i = start; i &amp;lt; n &amp;amp;&amp;amp; target - res[i] &amp;gt;= 0; i++) {
            if(i &amp;gt; start &amp;amp;&amp;amp; res[i] == res[i - 1]) continue;
            tmp.push_back(res[i]);
            dfs(i + 1, target - res[i]);
            tmp.pop_back();
        }
    }

    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; combinationSum2(vector&amp;lt;int&amp;gt; &amp;amp;candidates, int target) {
        sort(candidates.begin(), candidates.end());
        this -&amp;gt; res = candidates;
        dfs(0, target);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O($2 ^ n * n$)&lt;/li&gt;
&lt;li&gt;空间复杂度：O($n$)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 80 39. 组合总和</title><link>https://chaggle.github.io/posts/2021/11/29/day-80-39-combination-sum/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/29/day-80-39-combination-sum/</guid><pubDate>Mon, 29 Nov 2021 09:31:34 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/combination-sum/&quot;&gt;39. 组合总和&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个无重复元素的正整数数组 candidates 和一个正整数 target

找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。

candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同，则两种组合是唯一的。 

对于给定的输入，保证和为 target 的唯一组合数少于 150 个。

 

示例 1：

输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]
示例 2：

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3：

输入: candidates = [2], target = 1
输出: []
示例 4：

输入: candidates = [1], target = 1
输出: [[1]]
示例 5：

输入: candidates = [1], target = 2
输出: [[1,1]]
 

提示：

1 &amp;lt;= candidates.length &amp;lt;= 30
1 &amp;lt;= candidates[i] &amp;lt;= 200
candidate 中的每个元素都是独一无二的。
1 &amp;lt;= target &amp;lt;= 500
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;经典递归算法：每个元素都有&quot;不选&quot;与&quot;选&quot;两个分支，选择当前元素后继续从当前下标递归，从而允许同一个元素被无限次选取；当组合和等于 target 时记录答案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    void dfs(
        vector&amp;lt;int&amp;gt;&amp;amp; candidates, int target, vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; ans,
        vector&amp;lt;int&amp;gt;&amp;amp; combine, int idx
    ) {
        if (idx == candidates.size()) return;

        if (target == 0) {
            ans.emplace_back(combine);
            return;
        }

        dfs(candidates, target, ans, combine, idx + 1);

        if (target - candidates[idx] &amp;gt;= 0) {
            combine.emplace_back(candidates[idx]);
            dfs(candidates, target - candidates[idx], ans, combine, idx);
            combine.pop_back();
        }
    }

    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; combinationSum(vector&amp;lt;int&amp;gt;&amp;amp; candidates, int target) {
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;
        vector&amp;lt;int&amp;gt; combine;
        dfs(candidates, target, ans, combine, 0);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(s)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(target)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 79 814. 二叉树剪枝</title><link>https://chaggle.github.io/posts/2021/11/27/day-79-814-binary-tree-pruning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/27/day-79-814-binary-tree-pruning/</guid><pubDate>Fri, 26 Nov 2021 16:10:45 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/binary-tree-pruning/&quot;&gt;814. 二叉树剪枝&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你二叉树的根结点 root ，此外树的每个结点的值要么是 0 ，要么是 1 。

返回移除了所有不包含 1 的子树的原二叉树。

节点 node 的子树为 node 本身加上所有 node 的后代。


示例 1：


输入：root = [1,null,0,0,1]
输出：[1,null,0,null,1]
解释：
只有红色节点满足条件&quot;所有不包含 1 的子树&quot;。 右图为返回的答案。
示例 2：


输入：root = [1,0,1,0,0,0,1]
输出：[1,null,1,null,1]

示例 3：

输入：root = [1,1,0,1,1,0,1,0]
输出：[1,1,0,1,1,null,1]

提示：

树中节点的数目在范围 [1, 200] 内
Node.val 为 0 或 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;二叉树递归的典型想法：dfs 返回以当前节点为根的子树节点值之和，左子树之和为 0 则剪掉左子树，右子树同理；若整棵子树之和为 0，则整棵树被剪掉。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) :
        val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int dfs(TreeNode* root) {
        if(!root) return 0;
        auto l = dfs(root -&amp;gt; left);
        auto r = dfs(root -&amp;gt; right);
        if(!l) root -&amp;gt; left = nullptr;
        if(!r) root -&amp;gt; right = nullptr;
        return root -&amp;gt; val + l + r;
    }
    TreeNode* pruneTree(TreeNode* root) {
        return dfs(root) != 0 ? root : nullptr;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 78 1319. 连通网络的操作次数</title><link>https://chaggle.github.io/posts/2021/11/26/day-78-1319-number-of-operations-to-make-network-connected/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/26/day-78-1319-number-of-operations-to-make-network-connected/</guid><pubDate>Fri, 26 Nov 2021 14:47:27 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/number-of-operations-to-make-network-connected/&quot;&gt;1319. 连通网络的操作次数&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;用以太网线缆将 n 台计算机连接成一个网络，计算机的编号从 0 到 n-1。

线缆用 connections 表示，其中 connections[i] = [a, b] 连接了计算机 a 和 b。

网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。

给你这个计算机网络的初始布线 connections，

你可以拔开任意两台直连计算机之间的线缆，并用它连接一对未直连的计算机。

请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能，则返回 -1 。 

 

示例 1：


输入：n = 4, connections = [[0,1],[0,2],[1,2]]
输出：1
解释：拔下计算机 1 和 2 之间的线缆，并将它插到计算机 1 和 3 上。
示例 2：


输入：n = 6, connections = [[0,1],[0,2],[0,3],[1,2],[1,3]]
输出：2
示例 3：

输入：n = 6, connections = [[0,1],[0,2],[0,3],[1,2]]
输出：-1
解释：线缆数量不足。
示例 4：

输入：n = 5, connections = [[0,1],[0,2],[3,4],[2,3]]
输出：0
 

提示：

1 &amp;lt;= n &amp;lt;= 10^5
1 &amp;lt;= connections.length &amp;lt;= min(n*(n-1)/2, 10^5)
connections[i].length == 2
0 &amp;lt;= connections[i][0], connections[i][1] &amp;lt; n
connections[i][0] != connections[i][1]
没有重复的连接。
两台计算机不会通过多条线缆连接。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;与昨日相同的并查集思路。统计连通分量的个数与多余的连接数，若多余的连接数不足以连通所有分量（小于分量数减一），则返回 -1，否则返回分量数减一。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Djset {
private:
    vector&amp;lt;int&amp;gt; parent;  // 记录节点的根
    vector&amp;lt;int&amp;gt; rank;  // 记录根节点的深度（用于优化）
    int count;         // 记录连通分量的个数
    int rest;          // 记录多余的连接数
public:
    Djset(int n): parent(vector&amp;lt;int&amp;gt;(n)), rank(vector&amp;lt;int&amp;gt;(n)), count(n), rest(0) {
        for (int i = 0; i &amp;lt; n; i++) {
            parent[i] = i;
        }
    }

    int find(int x) {
        // 压缩方式：直接指向根节点
        if (x != parent[x]) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    void merge(int x, int y) {
        int rootx = find(x);
        int rooty = find(y);
        if (rootx != rooty) {
            // 按秩合并
            if (rank[rootx] &amp;lt; rank[rooty]) {
                swap(rootx, rooty);
            }
            parent[rooty] = rootx;
            if (rank[rootx] == rank[rooty]) rank[rootx] += 1;
            count--;
        } else {
            rest++;
        }
    }
    int getCount() {
        return count;
    }
    int getRest() {
        return rest;
    }
};

class Solution {
public:
    int makeConnected(int n, vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; connections) {
        Djset ds(n);
        for (auto&amp;amp; e :connections) {
            ds.merge(e[0], e[1]);
        }
        if (ds.getRest() &amp;lt; ds.getCount() - 1) return -1;
        return ds.getCount() - 1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O($n^2$)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 77 924. 尽量减少恶意软件的传播</title><link>https://chaggle.github.io/posts/2021/11/25/day-77-924-minimize-malware-spread/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/25/day-77-924-minimize-malware-spread/</guid><pubDate>Thu, 25 Nov 2021 13:58:09 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/minimize-malware-spread/&quot;&gt;924. 尽量减少恶意软件的传播&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;在节点网络中，只有当 graph[i][j] = 1 时，每个节点 i 能够直接连接到另一个节点 j。

一些节点 initial 最初被恶意软件感染。

只要两个节点直接连接，且其中至少一个节点受到恶意软件的感染，那么两个节点都将被恶意软件感染。

这种恶意软件的传播将继续，直到没有更多的节点可以被这种方式感染。

假设 M(initial) 是在恶意软件停止传播之后，整个网络中感染恶意软件的最终节点数。

如果从初始列表中移除某一节点能够最小化 M(initial)，

返回该节点。如果有多个节点满足条件，就返回索引最小的节点。

请注意，如果某个节点已从受感染节点的列表 initial 中删除，

它以后可能仍然因恶意软件传播而受到感染。

 

示例 1：

输入：graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1]
输出：0
示例 2：

输入：graph = [[1,0,0],[0,1,0],[0,0,1]], initial = [0,2]
输出：0
示例 3：

输入：graph = [[1,1,1],[1,1,1],[1,1,1]], initial = [1,2]
输出：1
 

提示：

1 &amp;lt; graph.length = graph[0].length &amp;lt;= 300
0 &amp;lt;= graph[i][j] == graph[j][i] &amp;lt;= 1
graph[i][i] == 1
1 &amp;lt;= initial.length &amp;lt; graph.length
0 &amp;lt;= initial[i] &amp;lt; graph.length
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这道题有些小难。首先要计算图中每个连通分量块的大小，然后统计 initial 表中每个节点所在连通分量块的大小。当某个连通分量块只含一个 initial 节点时，删除该节点即可使整个块免于感染，找到满足条件的最大连通块并返回对应节点，块大小相等时返回索引最小的节点；如果不存在这样的连通分量块，则返回 initial 中的最小值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class UnionFind {
public:
    vector&amp;lt;int&amp;gt; data;
    vector&amp;lt;int&amp;gt; cnt; // 每个节点连通块大小

    void init(int n) {
        data.resize(n, 0);
        for(int i = 0; i &amp;lt; n; i++) {
            data[i] = i;
        }
        cnt.resize(n, 1);
    }

    int find(int x) {
        /* int root = x;
        while (data[root] &amp;gt;= 0) root = data[root];

        //路径压缩
        while (x != root) {
            int tmp = data[x]; //tmp 指向 x 的父节点
            data[x] = root;	   //挂到根节点下
            x = tmp;
        }

        return root; //返回根节点的编号。 */
        //上述的优化会超时！
        if(data[x] != x) {
            return find(data[x]);
        } else {
            return x;
        }
    }

    void Union(int x, int y) {
        int p = find(x);
        int q = find(y);

        data[p] = q;
        if (p != q) {
            cnt[q] += cnt[p];
        }
    }

    int GetCnt(int x) {
        return cnt[find(x)];
    }
};

class Solution {
public:
    int minMalwareSpread(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; graph, vector&amp;lt;int&amp;gt;&amp;amp; initial) {
        UnionFind u;
        int n = graph.size(), m = graph[0].size();
        u.init(n);

        sort(initial.begin(), initial.end());

        for(int i = 0; i &amp;lt; n; i++) {
            for (int j = i + 1; j &amp;lt; m; j++) {
                if (graph[i][j] == 1) u.Union(i, j);

            }
        }

        // 计算initial中共连通块元素个数
        map&amp;lt;int, int&amp;gt; count;
        for (auto c : initial) {
            count[u.find(c)]++;
        }

        int ans = -1;
        int res = -1;
        for (auto c : initial) {
            if (count[u.find(c)] == 1) {
                int size = u.GetCnt(c);
                if (size &amp;gt; res) {
                    res = size;
                    ans = c;
                }
            }
        }
        if (ans == -1) return initial[0];
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O($n^2$)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 76 547. 省份数量</title><link>https://chaggle.github.io/posts/2021/11/24/day-76-547-number-of-provinces/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/24/day-76-547-number-of-provinces/</guid><pubDate>Wed, 24 Nov 2021 13:42:41 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/number-of-provinces/&quot;&gt;547. 省份数量&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;有 n 个城市，其中一些彼此相连，另一些没有相连。

如果城市 a 与城市 b 直接相连，且城市 b 与城市 c 直接相连，那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市，组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ，

其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连，

而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

 

示例 1：


输入：isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出：2
示例 2：


输入：isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出：3
 

提示：

1 &amp;lt;= n &amp;lt;= 200
n == isConnected.length
n == isConnected[i].length
isConnected[i][j] 为 1 或 0
isConnected[i][i] == 1
isConnected[i][j] == isConnected[j][i]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;刚好今年考研新增了并查集的学习内容，所以本次正好可以复习一下 UnionFind 类的相应写法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class UnionFind {
private:
    unordered_map&amp;lt;int, int&amp;gt; data; //存储父节点

    int cnt = 0; //记录数目

public:
    int find(int x) {
        int root = x;

        while(data[root] &amp;gt;= 0) root = data[root];

        //路径压缩
        while(x != root) {
            int tmp = data[x]; //tmp 指向 x 的父节点
            data[x] = root;	   //挂到根节点下
            x = tmp;
        }

        return root; //返回根节点的编号。
    }

    bool isconnected(int x, int y) {
        return find(x) == find(y);
    }

    void merge(int x, int y) {
        int p = find(x);
        int q = find(y);

        if(p != q) {
            data[p] = q;
            cnt--;
        }
    }

    void add(int x) {
        if(data.count(x) == 0) {
            data[x] = -1;
            cnt++;
        }
    }

    int getCnt() {
        return cnt;
    }
};

class Solution {
public:
    int findCircleNum(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; isConnected) {
        UnionFind u;
        int n = isConnected.size();
        for (int i = 0; i &amp;lt; n; i++) {
            u.add(i);
            for(int j = 0; j &amp;lt; i; j++) {
                if(isConnected[i][j]) u.merge(i, j);
            }
        }
        return u.getCnt();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O($n^2$ * logn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 75 面试题 17.17. 多次搜索</title><link>https://chaggle.github.io/posts/2021/11/23/day-75-multi-search-lcci/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/23/day-75-multi-search-lcci/</guid><pubDate>Tue, 23 Nov 2021 11:59:12 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/multi-search-lcci/&quot;&gt;面试题 17.17. 多次搜索&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个较长字符串big和一个包含较短字符串的数组smalls，

设计一个方法，根据smalls中的每一个较短字符串，对big进行搜索。

输出smalls中的字符串在big里出现的所有位置positions，

其中positions[i]为smalls[i]出现的所有位置。

示例：

输入：
big = &quot;mississippi&quot;
smalls = [&quot;is&quot;,&quot;ppi&quot;,&quot;hi&quot;,&quot;sis&quot;,&quot;i&quot;,&quot;ssippi&quot;]
输出： [[1,4],[8],[],[3],[1,4,7,10],[5]]
提示：

0 &amp;lt;= len(big) &amp;lt;= 1000
0 &amp;lt;= len(smalls[i]) &amp;lt;= 1000
smalls的总字符数不会超过 100000。
你可以认为smalls中没有重复字符串。
所有出现的字符均为英文小写字母。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Trie 树的经典方法，明年 1 月份再到博客进行一次复盘，讲述一步一步的思路。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;struct Trie
{
    int index = -1;
    Trie* next[26];
};
class Solution {
public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; multiSearch(string big, vector&amp;lt;string&amp;gt;&amp;amp; smalls) {
        Trie* root = new Trie();
        int n = smalls.size(), m = big.size();
        for (int i = 0; i &amp;lt; n; i++)
        {
            Trie* p = root;
            for (auto s : smalls[i]) {
                if (p -&amp;gt; next[s - &apos;a&apos;] == nullptr)
                {
                    Trie* q = new Trie();
                    p -&amp;gt; next[s - &apos;a&apos;] = q;
                }
                p = p -&amp;gt; next[s - &apos;a&apos;];
            }
            p -&amp;gt; index = i;
        }

        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans(n);

        for (int i = 0; i &amp;lt; m; i ++)
        {
            Trie* p = root;
            int j = i;
            while (j &amp;lt; m &amp;amp;&amp;amp; p -&amp;gt; next[big[j] - &apos;a&apos;] != nullptr)
            {
                p = p -&amp;gt; next[big[j] - &apos;a&apos;];
                if (p -&amp;gt; index != -1)
                    ans[p -&amp;gt; index].push_back(i);
                j++;
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n * m * c)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 74 677. 键值映射</title><link>https://chaggle.github.io/posts/2021/11/22/day-74-677-map-sum-pairs/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/22/day-74-677-map-sum-pairs/</guid><pubDate>Mon, 22 Nov 2021 03:45:25 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/map-sum-pairs/&quot;&gt;677. 键值映射&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;实现一个 MapSum 类，支持两个方法，insert 和 sum：

MapSum() 初始化 MapSum 对象

void insert(String key, int val) 插入 key-val 键值对，字符串表示键 key ，整数表示值 val 。

如果键 key 已经存在，那么原来的键值对将被替代成新的键值对。

int sum(string prefix) 返回所有以该前缀 prefix 开头的键 key 的值的总和。
 

示例：

输入：
[&quot;MapSum&quot;, &quot;insert&quot;, &quot;sum&quot;, &quot;insert&quot;, &quot;sum&quot;]
[[], [&quot;apple&quot;, 3], [&quot;ap&quot;], [&quot;app&quot;, 2], [&quot;ap&quot;]]
输出：
[null, null, 3, null, 5]

解释：
MapSum mapSum = new MapSum();
mapSum.insert(&quot;apple&quot;, 3);
mapSum.sum(&quot;ap&quot;);           // return 3 (apple = 3)
mapSum.insert(&quot;app&quot;, 2);
mapSum.sum(&quot;ap&quot;);           // return 5 (apple + app = 3 + 2 = 5)
 

提示：

1 &amp;lt;= key.length, prefix.length &amp;lt;= 50
key 和 prefix 仅由小写英文字母组成
1 &amp;lt;= val &amp;lt;= 1000
最多调用 50 次 insert 和 sum
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;最简单的方法可以直接使用 C++ 的容器组件完成，但由于是学习 Trie 树的用法，这里使用 Trie 来构建：插入时在 key 的末尾节点记录对应的 val，sum 时顺着前缀走到对应节点后，用 dfs 累加该节点子树上的所有值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const int N = 3000;
int ans[N][26];
int cnt[N];
int idx = 0;

class MapSum {
public:
    MapSum() {
        memset(ans, 0, sizeof ans); // 也可以使用动态数组进行i
        memset(cnt, 0, sizeof cnt);
    }

    void insert(string key, int val) {
        int p = 0;
        for (auto &amp;amp;i : key)
        {
            int q = i - &apos;a&apos;;
            if (!ans[p][q]) ans[p][q] = ++idx;
            p = ans[p][q];
        }
        cnt[p] = val;
    }

    int sum(string prefix) {
        int p = 0;
        for (auto &amp;amp;i : prefix)
        {
            int q = i - &apos;a&apos;;
            if (!ans[p][q]) return 0;
            p = ans[p][q];
        }
        return dfs(p);
    }

    int dfs(int p) {
        if (!p) return 0;
        int res = cnt[p];
        for (int i = 0; i &amp;lt; 26; ++i) {
            res += dfs(ans[p][i]);
        }
        return res;
    }
};

/**
 * Your MapSum object will be instantiated and called as such:
 * MapSum* obj = new MapSum();
 * obj-&amp;gt;insert(key,val);
 * int param_2 = obj-&amp;gt;sum(prefix);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n * m * C)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 73 208. 实现 Trie (前缀树)</title><link>https://chaggle.github.io/posts/2021/11/21/day-73-208-implement-trie-prefix-tree/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/21/day-73-208-implement-trie-prefix-tree/</guid><pubDate>Sun, 21 Nov 2021 01:35:06 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/implement-trie-prefix-tree/&quot;&gt;208. 实现 Trie (前缀树)&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Trie（发音类似 &quot;try&quot;）或者说 前缀树 是一种树形数据结构，用于高效地存储和检索字符串数据集中的键。

这一数据结构有相当多的应用情景，例如自动补完和拼写检查。

请你实现 Trie 类：

Trie() 初始化前缀树对象。

void insert(String word) 向前缀树中插入字符串 word 。

boolean search(String word) 如果字符串 word 在前缀树中, 返回 true; 否则，返回 false。

boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一，返回 true
 

示例：

输入
[&quot;Trie&quot;, &quot;insert&quot;, &quot;search&quot;, &quot;search&quot;, &quot;startsWith&quot;, &quot;insert&quot;, &quot;search&quot;]
[[], [&quot;apple&quot;], [&quot;apple&quot;], [&quot;app&quot;], [&quot;app&quot;], [&quot;app&quot;], [&quot;app&quot;]]
输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert(&quot;apple&quot;);
trie.search(&quot;apple&quot;);   // 返回 True
trie.search(&quot;app&quot;);     // 返回 False
trie.startsWith(&quot;app&quot;); // 返回 True
trie.insert(&quot;app&quot;);
trie.search(&quot;app&quot;);     // 返回 True
 

提示：

1 &amp;lt;= word.length, prefix.length &amp;lt;= 2000
word 和 prefix 仅由小写英文字母组成
insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Trie 树（前缀树）的设计模板题目，建议先了解其概念，然后背诵模板实现，多做几道 Trie 树的题目即可熟练掌握。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Trie {
private:
    bool isEnd;
    vector&amp;lt;Trie*&amp;gt; next;
public:
    Trie() : next(26), isEnd(false) {}

    void insert(string word) {
        Trie* T = this;
        for (char c : word)
        {
            if (T -&amp;gt; next[c - &apos;a&apos;] == NULL)
            {
                T -&amp;gt; next[c - &apos;a&apos;] = new Trie();
            }
            T = T -&amp;gt; next[c - &apos;a&apos;];
        }
        T -&amp;gt; isEnd = true;
    }

    bool search(string word) {
        Trie* T = this;
        for (char c : word)
        {
            T = T -&amp;gt; next[c - &apos;a&apos;];
            if (T == NULL) return false;
        }
        return T -&amp;gt; isEnd;
    }

    bool startsWith(string prefix) {
        Trie* T = this;
        for (char c : prefix)
        {
            T = T -&amp;gt; next[c-&apos;a&apos;];
            if (T == NULL) return false;
        }
        return true;
    }
};
/**
 * Your Trie object will be instantiated and called as such:
 * Trie* obj = new Trie();
 * obj-&amp;gt;insert(word);
 * bool param_2 = obj-&amp;gt;search(word);
 * bool param_3 = obj-&amp;gt;startsWith(prefix);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(1 + |S|)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(|T| * sum)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 72 78. 子集</title><link>https://chaggle.github.io/posts/2021/11/20/day-72-78-subsets/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/20/day-72-78-subsets/</guid><pubDate>Sat, 20 Nov 2021 01:08:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/subsets/&quot;&gt;78. 子集&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个整数数组 nums ，数组中的元素 互不相同 。返回该数组所有可能的子集（幂集）。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

 

示例 1：

输入：nums = [1,2,3]
输出：[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2：

输入：nums = [0]
输出：[[],[0]]
 

提示：

1 &amp;lt;= nums.length &amp;lt;= 10
-10 &amp;lt;= nums[i] &amp;lt;= 10
nums 中的所有元素 互不相同
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本题目若只是求子集的个数该多好，直接套公式即可，可惜是要输出每一个对应的子集。利用两个数组，一个存放最终答案，一个作为递归缓存；dfs 先加入当前元素递归到底，再回溯剔除该元素继续递归，从而枚举出全部子集。最后 34 天就要考研了，写得会比较紧张。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; tmp;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;

    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; subsets(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        dfs(0, nums);
        return ans;
    }

    void dfs(int i, vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        if (i == n) {
            ans.push_back(tmp);
            return;
        }
        tmp.push_back(nums[i]);
        dfs(i + 1, nums);
        tmp.pop_back();
        dfs(i + 1, nums);
    }

};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n*2^n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 71 260. 只出现一次的数字 III</title><link>https://chaggle.github.io/posts/2021/11/19/day-71-260-single-number-iii/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/19/day-71-260-single-number-iii/</guid><pubDate>Fri, 19 Nov 2021 07:40:14 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/single-number-iii/&quot;&gt;260. 只出现一次的数字 III&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个整数数组 nums，其中恰好有两个元素只出现一次，其余所有元素均出现两次。

找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

进阶：你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现？

 

示例 1：

输入：nums = [1,2,1,3,2,5]
输出：[3,5]
解释：[5, 3] 也是有效的答案。
示例 2：

输入：nums = [-1,0]
输出：[-1,0]
示例 3：

输入：nums = [0,1]
输出：[1,0]
提示：

2 &amp;lt;= nums.length &amp;lt;= 3 * 104
-231 &amp;lt;= nums[i] &amp;lt;= 2^31 - 1
除两个只出现一次的整数外，nums 中的其他数字都出现两次
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;因为只有两个数字不重复，看到题目便想到位运算的方法：先将所有数字异或，得到两个目标数字的异或值，再取出该值中任意一个为 1 的二进制位，按此位将数组分成两组分别异或，即可得到这两个数字。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; singleNumber(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int sum = 0;
        for (int i : nums) sum ^= i;
        int k = -1;
        for(int i = 31; i &amp;gt;= 0 &amp;amp;&amp;amp; k == -1; i--) {
            if (((sum &amp;gt;&amp;gt; i) &amp;amp; 1) == 1) k = i;
        }
        vector&amp;lt;int&amp;gt; ans(2);

        for (int i : nums) {
            if (((i &amp;gt;&amp;gt; k) &amp;amp; 1) == 1) ans[1] ^= i;
            else ans[0] ^= i;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 70 932. 漂亮数组</title><link>https://chaggle.github.io/posts/2021/11/18/day-70-932-beautiful-array/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/18/day-70-932-beautiful-array/</guid><pubDate>Wed, 17 Nov 2021 16:20:21 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/beautiful-array/&quot;&gt;932. 漂亮数组&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;对于某些固定的 N，如果数组 A 是整数 1, 2, ..., N 组成的排列，使得：

对于每个 i &amp;lt; j，都不存在 k 满足 i &amp;lt; k &amp;lt; j 使得 A[k] * 2 = A[i] + A[j]。

那么数组 A 是漂亮数组。

 

给定 N，返回任意漂亮数组 A（保证存在一个）。

 

示例 1：

输入：4
输出：[2,1,4,3]
示例 2：

输入：5
输出：[3,1,2,5,4]
 

提示：

1 &amp;lt;= N &amp;lt;= 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;分治法的逻辑题目：递归构造左、右两部分，将左半部分映射为奇数（2x - 1），右半部分映射为偶数（2x），奇偶分离后即可保证数组满足漂亮数组的条件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; beautifulArray(int n) {
        if(n == 1) return {1};
        auto l = beautifulArray((n + 1) / 2);
        auto r = beautifulArray(n / 2);
        vector&amp;lt;int&amp;gt; ans;
        for(auto x : l) ans.push_back(2 * x - 1);
        for(auto x : r) ans.push_back(2 * x);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(logn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 69 23. 合并K个升序链表</title><link>https://chaggle.github.io/posts/2021/11/17/day-69-23-merge-k-sorted-lists/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/17/day-69-23-merge-k-sorted-lists/</guid><pubDate>Wed, 17 Nov 2021 09:35:08 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/merge-k-sorted-lists/&quot;&gt;23. 合并K个升序链表&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个链表数组，每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中，返回合并后的链表。

 

示例 1：

输入：lists = [[1,4,5],[1,3,4],[2,6]]
输出：[1,1,2,3,4,4,5,6]
解释：链表数组如下：
[
  1-&amp;gt;4-&amp;gt;5,
  1-&amp;gt;3-&amp;gt;4,
  2-&amp;gt;6
]
将它们合并到一个有序链表中得到。
1-&amp;gt;1-&amp;gt;2-&amp;gt;3-&amp;gt;4-&amp;gt;4-&amp;gt;5-&amp;gt;6
示例 2：

输入：lists = []
输出：[]
示例 3：

输入：lists = [[]]
输出：[]
 

提示：

k == lists.length
0 &amp;lt;= k &amp;lt;= 10^4
0 &amp;lt;= lists[i].length &amp;lt;= 500
-10^4 &amp;lt;= lists[i][j] &amp;lt;= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;看题目很明显，这是 merge_sort 的考察点，使用的大多是分治的思想。虽然是 k 个升序链表，但仍可以分解为两两合并：把区间不断二分，再合并左右两半的结果即可。使用 y 神给的模板进行改写，确实十分好用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* merge_sort(ListNode* a, ListNode* b) {
        if (a == nullptr || b == nullptr) return a ? a : b;
        ListNode head, *tail = &amp;amp;head, *p = a, *q = b;
        while (p &amp;amp;&amp;amp; q)
        {
            if (p -&amp;gt; val &amp;lt; q -&amp;gt; val) {
                tail -&amp;gt; next = p;
                p = p -&amp;gt; next;
            }
            else {
                tail -&amp;gt; next = q;
                q = q -&amp;gt; next;
            }
            tail = tail -&amp;gt; next;
        }
        tail -&amp;gt; next = (p ? p : q);
        return head.next;
    }

    ListNode* merge(vector &amp;lt;ListNode*&amp;gt;&amp;amp; lists, int l, int r) {
        if (l == r) return lists[l];
        if (l &amp;gt; r) return nullptr;
        int mid = (l + r) &amp;gt;&amp;gt; 1;
        return merge_sort(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

    ListNode* mergeKLists(vector&amp;lt;ListNode*&amp;gt;&amp;amp; lists) {
        return merge(lists, 0, lists.size() - 1);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(k * logk)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(logk)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 68 96. 不同的二叉搜索树</title><link>https://chaggle.github.io/posts/2021/11/16/day-68-96-unique-binary-search-trees/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/16/day-68-96-unique-binary-search-trees/</guid><pubDate>Tue, 16 Nov 2021 12:08:51 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/unique-binary-search-trees/&quot;&gt;96. 不同的二叉搜索树&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个整数 n ，求恰由 n 个节点组成且节点值

从 1 到 n 互不相同的 二叉搜索树 有多少种？

返回满足题意的二叉搜索树的种数。


示例 1：


输入：n = 3
输出：5
示例 2：

输入：n = 1
输出：1


提示：

1 &amp;lt;= n &amp;lt;= 19
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本题采用动态规划求解子问题，dp[i] 表示 i 个节点组成的二叉搜索树的种数。枚举根节点 j 时，左子树由 j - 1 个节点构成（对应 dp[j - 1]），右子树由 i - j 个节点构成（对应 dp[i - j]），两者相乘后累加即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numTrees(int n) {
        vector&amp;lt;int&amp;gt; dp(n + 1, 0);
        dp[0] = dp[1] = 1;
        for(int i = 2; i &amp;lt;= n; i++)
        {
            for(int j = 1; j &amp;lt;= i; j++)
            {
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(nlogn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 67 881. 救生艇</title><link>https://chaggle.github.io/posts/2021/11/15/day-67-881-boats-to-save-people/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/15/day-67-881-boats-to-save-people/</guid><pubDate>Mon, 15 Nov 2021 03:45:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/boats-to-save-people/&quot;&gt;881. 救生艇&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;第 i 个人的体重为 people[i]，每艘船可以承载的最大重量为 limit。

每艘船最多可同时载两人，但条件是这些人的重量之和最多为 limit。

返回载到每一个人所需的最小船数。(保证每个人都能被船载)。

 

示例 1：

输入：people = [1,2], limit = 3
输出：1
解释：1 艘船载 (1, 2)
示例 2：

输入：people = [3,2,2,1], limit = 3
输出：3
解释：3 艘船分别载 (1, 2), (2) 和 (3)
示例 3：

输入：people = [3,5,3,4], limit = 5
输出：4
解释：4 艘船分别载 (3), (3), (4), (5)
提示：

1 &amp;lt;= people.length &amp;lt;= 50000
1 &amp;lt;= people[i] &amp;lt;= limit &amp;lt;= 30000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;贪心法加双指针，双指针的思考方式与快排一致：排序后从两端向中间配对，最重的人尽量与最轻的人同船，从而得到所需的最小船数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numRescueBoats(vector&amp;lt;int&amp;gt;&amp;amp; people, int limit) {
        int ans = 0;
        sort(people.begin(), people.end());
        int l= 0, r = people.size() - 1;
        while(l &amp;lt;= r)
        {
            if (people[l] + people[r] &amp;gt; limit) r--;
            else
            {
                l++;r--;
            }
            ans++;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(logn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 66 435. 无重叠区间</title><link>https://chaggle.github.io/posts/2021/11/14/day-66-435-non-overlapping-intervals/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/14/day-66-435-non-overlapping-intervals/</guid><pubDate>Sun, 14 Nov 2021 12:25:06 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/non-overlapping-intervals/&quot;&gt;435. 无重叠区间&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个区间的集合，找到需要移除区间的最小数量，使剩余区间互不重叠。

注意:

可以认为区间的终点总是大于它的起点。

区间 [1,2] 和 [2,3] 的边界相互&quot;接触&quot;，但没有相互重叠。

示例 1:

输入: [ [1,2], [2,3], [3,4], [1,3] ]

输出: 1

解释: 移除 [1,3] 后，剩下的区间没有重叠。
示例 2:

输入: [ [1,2], [1,2], [1,2] ]

输出: 2

解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:

输入: [ [1,2], [2,3] ]

输出: 0

解释: 你不需要移除任何区间，因为它们已经是无重叠的了。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;单刀直入，按照右边界升序排序，右边界值越大越靠后；遍历每一个区间，若当前区间的左端点与上一个保留区间的右边界产生重叠，则移除该区间，否则更新右边界。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int eraseOverlapIntervals(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; intervals) {
        int n = intervals.size();
        if(n == 0) return n;
        sort(intervals.begin(), intervals.end(), [](const auto &amp;amp; a, const auto &amp;amp;b)
        {
            return a[1] &amp;lt; b[1];
        });
        int del = 0;
        int r = intervals[0][1];
        for(int i = 1; i &amp;lt; n; i++)
        {
            if(intervals[i][0] &amp;lt; r) del++;
            else r = intervals[i][1];
        }
        return del;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(nlogn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(logn)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 65 455. 分发饼干</title><link>https://chaggle.github.io/posts/2021/11/13/day-65-455-assign-cookies/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/13/day-65-455-assign-cookies/</guid><pubDate>Sat, 13 Nov 2021 00:54:19 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/assign-cookies/&quot;&gt;455. 分发饼干&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;假设你是一位很棒的家长，想要给你的孩子们一些小饼干。

但是，每个孩子最多只能给一块饼干。

对每个孩子 i，都有一个胃口值 g[i]，

这是能让孩子们满足胃口的饼干的最小尺寸；

并且每块饼干 j，都有一个尺寸 s[j] 。

如果 s[j] &amp;gt;= g[i]，我们可以将这个饼干 j 分配给孩子 i ，这个孩子会得到满足。

你的目标是尽可能满足越多数量的孩子，并输出这个最大数值。

 
示例 1:

输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干，3个孩子的胃口值分别是：1,2,3。
虽然你有两块小饼干，由于他们的尺寸都是1，你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:

输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干，2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
 

提示：

1 &amp;lt;= g.length &amp;lt;= 3 * 104
0 &amp;lt;= s.length &amp;lt;= 3 * 104
1 &amp;lt;= g[i], s[j] &amp;lt;= 2^31 - 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;贪心法，遵循优先满足最小胃口的原则：将两个数组排序后，用双指针依次匹配，优先满足胃口最小的孩子，返回能满足的孩子个数即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findContentChildren(vector&amp;lt;int&amp;gt;&amp;amp; g, vector&amp;lt;int&amp;gt;&amp;amp; s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int n = g.size(), m = s.size();
        int count = 0;
        for (int i = 0, j = 0; i &amp;lt; n &amp;amp;&amp;amp; j &amp;lt; m; i++, j++) {
            while (j &amp;lt; m &amp;amp;&amp;amp; g[i] &amp;gt; s[j]) j++;
            if (j &amp;lt; m) count++;
        }
        return count;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 64 518. 零钱兑换 II</title><link>https://chaggle.github.io/posts/2021/11/12/day-64-518-coin-change-ii/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/12/day-64-518-coin-change-ii/</guid><pubDate>Fri, 12 Nov 2021 08:56:19 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/coin-change-2/&quot;&gt;518. 零钱兑换 II&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个整数数组 coins 表示不同面额的硬币，另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额，返回 0 。

假设每一种面额的硬币有无限个。 

题目数据保证结果符合 32 位带符号整数。

 

示例 1：

输入：amount = 5, coins = [1, 2, 5]
输出：4
解释：有四种方式可以凑成总金额：
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2：

输入：amount = 3, coins = [2]
输出：0
解释：只用面额 2 的硬币不能凑成总金额 3 。
示例 3：

输入：amount = 10, coins = [10]
输出：1
 

提示：

1 &amp;lt;= coins.length &amp;lt;= 300
1 &amp;lt;= coins[i] &amp;lt;= 5000
coins 中的所有值 互不相同
0 &amp;lt;= amount &amp;lt;= 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;与昨天的题目不同的是，本题的硬币数据规模变小了，而且 coins 中的所有值互不相同，因此本次题目用模板解决会更加简单。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int change(int amount, vector&amp;lt;int&amp;gt;&amp;amp; coins) {
        vector&amp;lt;int&amp;gt; dp(amount + 1);
        dp[0] = 1;
        for(auto&amp;amp; n : coins)
        {
            for (int i = n; i &amp;lt;= amount; i++) {
                dp[i] += dp[i - n];
            }
        }
        return dp[amount];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n ^ 2)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-a7dd/&lt;/p&gt;
</content:encoded></item><item><title>Day 63 322. 零钱兑换</title><link>https://chaggle.github.io/posts/2021/11/11/day-63-322-coin-change/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/11/day-63-322-coin-change/</guid><pubDate>Thu, 11 Nov 2021 09:37:46 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/coin-change/&quot;&gt;322. 零钱兑换&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个整数数组 coins ，表示不同面额的硬币；以及一个整数 amount ，表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额，返回 -1 。

你可以认为每种硬币的数量是无限的。

 

示例 1：

输入：coins = [1, 2, 5], amount = 11
输出：3
解释：11 = 5 + 5 + 1
示例 2：

输入：coins = [2], amount = 3
输出：-1
示例 3：

输入：coins = [1], amount = 0
输出：0
示例 4：

输入：coins = [1], amount = 1
输出：1
示例 5：

输入：coins = [1], amount = 2
输出：2
 

提示：

1 &amp;lt;= coins.length &amp;lt;= 12
1 &amp;lt;= coins[i] &amp;lt;= 231 - 1
0 &amp;lt;= amount &amp;lt;= 104
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;与前面的背包题目思路相同，参考以往题目与资料后，对模板稍作改动即可解决问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int INF = 1000000000;
    int coinChange(vector&amp;lt;int&amp;gt;&amp;amp; coins, int amount) {
        vector&amp;lt;int&amp;gt; dp(amount + 1, INF);
        dp[0] = 0;
        for (int i = 0; i &amp;lt; coins.size(); i++)
        {
            for(int j = coins[i]; j &amp;lt;= amount; j++)
            {
                dp[j] = min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        if (dp[amount] == INF) dp[amount] = -1;
        return dp[amount];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n ^ 2)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-a7dd/&lt;/p&gt;
</content:encoded></item><item><title>Day 62 494. 目标和</title><link>https://chaggle.github.io/posts/2021/11/10/day-62-494-target-sum/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/10/day-62-494-target-sum/</guid><pubDate>Wed, 10 Nov 2021 08:57:32 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/target-sum/&quot;&gt;494. 目标和&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个 只包含正整数 的 非空 数组 nums 。

请你判断是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。

 

示例 1：

输入：nums = [1,5,11,5]
输出：true
解释：数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2：

输入：nums = [1,2,3,5]
输出：false
解释：数组不能分割成两个元素和相等的子集。
 

提示：

1 &amp;lt;= nums.length &amp;lt;= 200
1 &amp;lt;= nums[i] &amp;lt;= 100
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;依旧是 0/1 背包问题的模板解法，查看大佬的模板后仅需进行一些改动即可解题，本质上与昨日属于同一类题目。有两种边界情况可以直接排除：数组总和小于目标值，以及数组总和与目标值之差为奇数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findTargetSumWays(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if((sum - target) % 2 == 1 || sum &amp;lt; target) return false;

        int n = (sum - target) / 2;
        vector&amp;lt;int&amp;gt; dp(n + 1, 0);
        dp[0] = 1;
        for(int i : nums)
        {
            for (int j = n; j &amp;gt;= i; j--) {
                dp[j] += dp[j - i];
            }
        }
        return dp[n];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n ^ 2)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-a7dd/&lt;/p&gt;
</content:encoded></item><item><title>Day 61 416. 分割等和子集</title><link>https://chaggle.github.io/posts/2021/11/09/day-61-416-partition-equal-subset-sum/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/09/day-61-416-partition-equal-subset-sum/</guid><pubDate>Tue, 09 Nov 2021 12:51:31 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/partition-equal-subset-sum/&quot;&gt;416. 分割等和子集&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个只包含正整数的非空数组 nums 。

请你判断是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。

 

示例 1：

输入：nums = [1,5,11,5]
输出：true
解释：数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2：

输入：nums = [1,2,3,5]
输出：false
解释：数组不能分割成两个元素和相等的子集。
 

提示：

1 &amp;lt;= nums.length &amp;lt;= 200
1 &amp;lt;= nums[i] &amp;lt;= 100
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;今天进入 0/1 背包的问题，背包的每个物品只有取与不取两种状态，本题是求能否把数组等分为两个元素和相同的子集。首先数组总和一定是偶数才能等分，所以和为奇数的数组直接舍弃；接下来只需判断是否存在子集和为 sum / 2 即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool canPartition(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum % 2 == 1) return false;
        int target = sum / 2;
        vector&amp;lt;int&amp;gt; dp(target + 1, 0);
        dp[0] = 1;

        for(auto i : nums)
        {
            for(int j = target; j &amp;gt;= i; j--)
            {
                dp[j] = dp[j] || dp[j - i];
            }
        }

        return dp[target];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n ^ 2)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-a7dd/&lt;/p&gt;
</content:encoded></item><item><title>Day 60 464. 我能赢吗</title><link>https://chaggle.github.io/posts/2021/11/08/day-60-464-can-i-win/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/08/day-60-464-can-i-win/</guid><pubDate>Mon, 08 Nov 2021 12:40:13 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/can-i-win/&quot;&gt;464. 我能赢吗&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;在 &quot;100 game&quot; 这个游戏中，两名玩家轮流选择从 1 到 10 的任意整数，累计整数和，

先使得累计整数和达到或超过 100 的玩家，即为胜者。

如果我们将游戏规则改为 “玩家不能重复使用整数” 呢？

例如，两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数（不放回），直到累计整数和 &amp;gt;= 100。

给定一个整数 maxChoosableInteger 和另一个整数 desiredTotal，判断先出手的玩家是否能稳赢？

你可以假设 maxChoosableInteger 不会大于 20， desiredTotal 不会大于 300。

示例：

输入：
maxChoosableInteger = 10
desiredTotal = 11

输出：
false

解释：
无论第一个玩家选择哪个整数，他都会失败。
第一个玩家可以选择从 1 到 10 的整数。
如果第一个玩家选择 1，那么第二个玩家只能选择从 2 到 10 的整数。
第二个玩家可以通过选择整数 10（那么累积和为 11 &amp;gt;= desiredTotal），从而取得胜利.
同样地，第一个玩家选择任意其他整数，第二个玩家都会赢。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题只有两种对立的结局：必赢和必输，也就是博弈中判断先手能否稳赢的问题。如果先手 A 选择某个数 X 后，无论后手 B 选什么都能保证 A 获胜，则返回 true；否则 A 选择任意数后 B 都有必胜方法，返回 false。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool canIWin(int stable, int desired) {
        if (stable &amp;gt;= desired) return true;

        if (stable * (stable + 1) / 2 &amp;lt; desired) return false;

        unordered_map&amp;lt;int, bool&amp;gt; up;
        return dfs(stable, desired, 0, up);
    }

    bool dfs(int n, int sum, int k, unordered_map&amp;lt;int, bool&amp;gt;&amp;amp; up) {
        if (up.count(k)) return up[k];

        for (int i = 0; i &amp;lt; n; ++i) {
            int cur = (1 &amp;lt;&amp;lt; i);
            if ((cur &amp;amp; k) == 0)
            {//这个值没有使用过
                if (sum &amp;lt;= i + 1 || !dfs(n, sum - (i + 1), cur | k, up))
                {
                    up[k] = true;
                    return true;
                }
            }
        }
        up[k] = false;
        return false;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n * 2^n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(2^n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 59 688. “马”在棋盘上的概率</title><link>https://chaggle.github.io/posts/2021/11/07/day-59-688-knight-probability-in-chessboard/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/07/day-59-688-knight-probability-in-chessboard/</guid><pubDate>Sun, 07 Nov 2021 12:37:09 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/knight-probability-in-chessboard/&quot;&gt;688. “马”在棋盘上的概率&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;已知一个 NxN 的国际象棋棋盘，棋盘的行号和列号都是从 0 开始。

即最左上角的格子记为 (0, 0)，最右下角的记为 (N-1, N-1)。 

现有一个 “马”（也译作 “骑士”）位于 (r, c) ，并打算进行 K 次移动。 

如下图所示，国际象棋的 “马” 每一步先沿水平或垂直方向移动 2 个格子，

然后向与之相垂直的方向再移动 1 个格子，共有 8 个可选的位置。
 
现在 “马” 每一步都从可选的位置（包括棋盘外部的）中独立随机地选择一个进行移动，

直到移动了 K 次或跳到了棋盘外面。

求移动结束后，“马” 仍留在棋盘上的概率。

示例：

输入: 3, 2, 0, 0
输出: 0.0625
解释:
输入的数据依次为 N, K, r, c
第 1 步时，有且只有 2 种走法令 “马” 可以留在棋盘上（跳到（1,2）或（2,1））。

对于以上的两种情况，各自在第2步均有且只有2种走法令 “马” 仍然留在棋盘上。

所以 “马” 在结束后仍在棋盘上的概率为 0.0625。
 

注意：

N 的取值范围为 [1, 25]
K 的取值范围为 [0, 100]
开始时，“马” 总是位于棋盘上
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;这道题暂时没有看懂，先挖个坑，抄个答案记录一下。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int d[8][2] = {
                    {1, 2}, {2, 1}, {2, - 1}, {1, -2},
                    {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}
         };
    double knightProbability(int N, int K, int r, int c) {
        if (K == 0) return 1.0;
        vector&amp;lt;vector&amp;lt;double&amp;gt; &amp;gt; dp(N, vector&amp;lt;double&amp;gt;(N, 1));
        for (int k = 1; k &amp;lt;= K; ++k) {
            auto ans = dp;
            for (int i = 0; i &amp;lt; N; ++i) {
                for (int j = 0; j &amp;lt; N; ++j) {
                    ans[i][j] = 0;
                    for (int l = 0; l &amp;lt; 8; ++l) {
                        int r = i + d[l][0];
                        int c = j + d[l][1];
                        if (r &amp;gt;= 0 &amp;amp;&amp;amp; r &amp;lt; N &amp;amp;&amp;amp; c &amp;gt;= 0 &amp;amp;&amp;amp; c &amp;lt; N) {
                            ans[i][j] += dp[r][c] / 8;
                        }
                    }
                }
            }
            swap(ans, dp);
        }
        return dp[r][c];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(K * N^2)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(N^2)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 58 62. 不同路径</title><link>https://chaggle.github.io/posts/2021/11/06/day-58-62-unique-paths/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/06/day-58-62-unique-paths/</guid><pubDate>Sat, 06 Nov 2021 14:14:57 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/unique-paths/&quot;&gt;62. 不同路径&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为 “Start” ）。

机器人每次只能向下或者向右移动一步。

机器人试图达到网格的右下角（在下图中标记为 “Finish” ）。

问总共有多少条不同的路径？

 

示例 1：


输入：m = 3, n = 7
输出：28
示例 2：

输入：m = 3, n = 2
输出：3
解释：
从左上角开始，总共有 3 条路径可以到达右下角。
1. 向右 -&amp;gt; 向下 -&amp;gt; 向下
2. 向下 -&amp;gt; 向下 -&amp;gt; 向右
3. 向下 -&amp;gt; 向右 -&amp;gt; 向下
示例 3：

输入：m = 7, n = 3
输出：28
示例 4：

输入：m = 3, n = 3
输出：6
 

提示：

1 &amp;lt;= m, n &amp;lt;= 100
题目数据保证答案小于等于 2 * 10^9
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;典型的二维 DP 简单题。第一行和第一列全部初始化为 1，然后逐格向下累加即可，其实也是一个排列组合的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int uniquePaths(int m, int n) {
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans(m, vector&amp;lt;int&amp;gt;(n));

        for(int i = 0; i &amp;lt; m; i++) ans[i][0] = 1;
        for(int i = 0; i &amp;lt; n; i++) ans[0][i] = 1;

        for(int i = 1; i &amp;lt; m; i++)
        {
            for(int j = 1; j &amp;lt; n; j++)
            {
                ans[i][j] = ans[i - 1][j] + ans[i][j - 1];
            }
        }

        return ans[m - 1][n - 1];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n * m)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n * m)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 57 1143. 最长公共子序列</title><link>https://chaggle.github.io/posts/2021/11/05/day-57-1143-longest-common-subsequence/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/05/day-57-1143-longest-common-subsequence/</guid><pubDate>Fri, 05 Nov 2021 07:47:17 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/longest-common-subsequence/&quot;&gt;1143. 最长公共子序列&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定两个字符串 text1 和 text2，返回这两个字符串的最长 公共子序列 的长度。

如果不存在 公共子序列 ，返回 0 。

一个字符串的 子序列 是指这样一个新的字符串：

它是由原字符串在不改变字符的相对顺序的情况下删除某些字符后组成的新字符串。

例如，&quot;ace&quot; 是 &quot;abcde&quot; 的子序列，但 &quot;aec&quot; 不是 &quot;abcde&quot; 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

 

示例 1：

输入：text1 = &quot;abcde&quot;, text2 = &quot;ace&quot;
输出：3
解释：最长公共子序列是 &quot;ace&quot; ，它的长度为 3 。
示例 2：

输入：text1 = &quot;abc&quot;, text2 = &quot;abc&quot;
输出：3
解释：最长公共子序列是 &quot;abc&quot; ，它的长度为 3 。
示例 3：

输入：text1 = &quot;abc&quot;, text2 = &quot;def&quot;
输出：0
解释：两个字符串没有公共子序列，返回 0 。
 

提示：

1 &amp;lt;= text1.length, text2.length &amp;lt;= 1000
text1 和 text2 仅由小写英文字符组成。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题既可以用两个一维数组求解，也可以用二维数组求解，这里选择使用二维数组。&lt;/li&gt;
&lt;li&gt;优化后与之前做的几道题一致，可以优化空间为一个 2 列、text2.size() 行的滚动数组，由于时间关系暂时作为 TODO。&lt;/li&gt;
&lt;li&gt;状态转移方程为：当 &lt;code&gt;text1[i] == text2[j]&lt;/code&gt; 时，&lt;code&gt;ans[i][j] = ans[i - 1][j - 1] + 1&lt;/code&gt;，即当前字符匹配上时，最长公共子序列的长度加一；当 &lt;code&gt;text1[i] != text2[j]&lt;/code&gt; 时，&lt;code&gt;ans[i][j] = max(ans[i - 1][j], ans[i][j - 1])&lt;/code&gt;，分别代表必然不使用 s1[i]（但可能使用 s2[j]）和必然不使用 s2[j]（但可能使用 s1[i]）时的长度。&lt;/li&gt;
&lt;li&gt;在字符串前面补一个空格，可以更好地处理边界条件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n = text1.size(), m = text2.size();
    	text1 = &quot; &quot; + text1;
        text2 = &quot; &quot; + text2;
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans(n + 1, vector&amp;lt;int&amp;gt;(m + 1, 0));

    	for(int i = 0; i &amp;lt;= n; i++) ans[i][0] = 1;
    	for(int j = 0; j &amp;lt;= m; j++) ans[0][j] = 1;

    	for(int i = 1; i &amp;lt;= n; i++)
        {
    		for(int j = 1; j &amp;lt;= m; j++)
            {
    			if(text1[i] == text2[j])
				{
					ans[i][j] = max(
						ans[i - 1][j - 1] + 1,
						max(ans[i - 1][j], ans[i][j - 1])
					);
				}
    			else ans[i][j] = max(ans[i - 1][j], ans[i][j - 1]);
    		}
    	}
    	return ans[n][m] - 1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n * m)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n * m)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 56 673. 最长递增子序列的个数</title><link>https://chaggle.github.io/posts/2021/11/04/day-56-673-number-of-longest-increasing-subsequence/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/04/day-56-673-number-of-longest-increasing-subsequence/</guid><pubDate>Thu, 04 Nov 2021 08:38:52 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/&quot;&gt;673. 最长递增子序列的个数&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个未排序的整数数组，找到最长递增子序列的个数。

示例 1:

输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列，分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:

输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1，并且存在5个子序列的长度为1，因此输出5。
注意: 给定的数组长度不超过 2000 并且结果一定是32位有符号整数
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题思考有两个难点：一是找出最长的递增子序列，二是统计能够达到该长度的子序列个数。&lt;/li&gt;
&lt;li&gt;还有一种树状数组的做法，留待日后学习，现在一时消化不了，目前使用的方法有些麻烦。&lt;/li&gt;
&lt;li&gt;使用两个数组，一个记录最长子序列的长度，一个记录对应的个数，全部初始化为 1，因为每个元素都可以独立看作一个长度为 1 的子序列。然后对区间 [0, i) 内的所有元素遍历一次，对于每个 nums[j] &amp;lt; nums[i]，说明 nums[i] 可以接在 nums[j] 后面形成上升子序列，据此更新 len[i] 的长度与 mlen[i] 的个数。len[i] 表示以 i 结尾时能取得的最大长度，mlen[i] 表示在该长度下的子序列个数，同时用 maxlen 记录整个数组中的最大长度。&lt;/li&gt;
&lt;li&gt;最后把所有 len[i] 等于 maxlen 的位置对应的 mlen[i] 相加，即可得到答案。整个过程比较复杂，第一条可能难以看明白。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findNumberOfLIS(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        vector&amp;lt;int&amp;gt; len(n, 1), mlen(n, 1);
        int maxlen = 1;
        for(int i = 0; i &amp;lt; n; i++)
        {
            for(int j = 0; j &amp;lt; i; j++)
            {
                if(nums[j] &amp;lt; nums[i])
                {
                    if(len[i] &amp;lt; len[j] + 1)
                    {
                        len[i] = len[j] + 1;
                        mlen[i] = mlen[j];
                    }
                    else if(len[i] == len[j] + 1) mlen[i] += mlen[j];
                }
            }
            maxlen = max(maxlen, len[i]);
        }
        int ans = 0;
        for(int i = 0; i &amp;lt; n; i++)
        {
            if(len[i] == maxlen) ans += mlen[i];
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n ^ 2)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 55 198. 打家劫舍</title><link>https://chaggle.github.io/posts/2021/11/03/day-55-198-house-robber/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/03/day-55-198-house-robber/</guid><pubDate>Wed, 03 Nov 2021 05:26:11 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/house-robber/&quot;&gt;198. 打家劫舍&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;你是一个专业的小偷，计划偷窃沿街的房屋。

每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，

如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组，

计算你 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额。

 

示例 1：

输入：[1,2,3,1]
输出：4
解释：偷窃 1 号房屋 (金额 = 1) ，然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。
示例 2：

输入：[2,7,9,3,1]
输出：12
解释：偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9)，接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
 

提示：

1 &amp;lt;= nums.length &amp;lt;= 100
0 &amp;lt;= nums[i] &amp;lt;= 400
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;DP 原本是我掌握得最熟练的部分，但好久没分析状态转移方程了，居然错了好多次。&lt;/li&gt;
&lt;li&gt;把握好状态转移方程就能解决问题：dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])。&lt;/li&gt;
&lt;li&gt;优化空间后，跟斐波那契数列一样，只需要维护两个状态变量即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int rob(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        if(n == 1) return nums[0];
        if(n == 2) return max(nums[0], nums[1]);
        int dp[2];
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for(int i = 2; i &amp;lt; n; i++)
        {
            int best = max(dp[0] + nums[i], dp[1]);
            dp[0] = dp[1];
            dp[1] = best;
        }

        return dp[1];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 54 746. 使用最小花费爬楼梯</title><link>https://chaggle.github.io/posts/2021/11/02/day-54-746-min-cost-climbing-stairs/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/02/day-54-746-min-cost-climbing-stairs/</guid><pubDate>Tue, 02 Nov 2021 04:23:08 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/min-cost-climbing-stairs/&quot;&gt;746. 使用最小花费爬楼梯&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;数组的每个下标作为一个阶梯，第 i 个阶梯对应着一个非负数的体力花费值 cost[i]（下标从 0 开始）。

每当你爬上一个阶梯你都要花费对应的体力值，一旦支付了相应的体力值，

你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时，你可以选择从下标为 0 或 1 的元素作为初始阶梯。

 

示例 1：

输入：cost = [10, 15, 20]
输出：15
解释：最低花费是从 cost[1] 开始，然后走两步即可到阶梯顶，一共花费 15 。
 示例 2：

输入：cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出：6
解释：最低花费方式是从 cost[0] 开始，逐个经过那些 1 ，跳过 cost[3] ，一共花费 6 。
 

提示：

cost 的长度范围是 [2, 1000]。
cost[i] 将会是一个整型数据，范围为 [0, 999] 。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;与斐波那契数列同类的动态规划题。由于每次有爬一层和爬两层两种选择，所以只需要维护两个状态，最后返回数组末尾两个状态中较小的那一个即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int minCostClimbingStairs(vector&amp;lt;int&amp;gt;&amp;amp; cost) {
        int n = cost.size();
        int dp[2];
        dp[0] = cost[0];
        dp[1] = cost[1];

        for(int i = 2; i &amp;lt; n; i++)
        {
            int mix = min(dp[0], dp[1]) + cost[i];
            dp[0] = dp[1];
            dp[1] = mix;
        }

        return min(dp[0], dp[1]);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 53 Top View of a Tree</title><link>https://chaggle.github.io/posts/2021/11/01/day-53-top-view-of-a-tree/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/11/01/day-53-top-view-of-a-tree/</guid><pubDate>Mon, 01 Nov 2021 09:29:48 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://binarysearch.com/problems/Top-View-of-a-Tree&quot;&gt;Top View of a Tree&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Given a binary tree root,

 return the top view of the tree, sorted left-to-right.

Constraints

n ≤ 100,000 where n is the number of nodes in root

Example 1

Input
root = [1, [2, null, [4, null, [5, null, [6, null,
       [7, null, null]]]]], [3, null, null]]

Output
[2, 1, 3, 6, 7]

Explanation

Note that directly above 4 is 1 and directly above 5 is 3

so these are not part of the top view.

Example 2
Input
root = [3, [1, [0, null, null], [2, null, null]], [4, null, null]]
Output
[0, 1, 3, 4]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目要求返回树的顶视图（Top View），并按从左到右排序。俯视视角下，上层的节点会遮挡下层节点：覆盖自己左孩子节点的右孩子节点，以及自己右孩子节点的左孩子节点都是看不见的。因此只要 y 值（层级）相同，同一层只需加入其中 x 值最大和最小的那两个节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * class Tree {
 *     public:
 *         int val;
 *         Tree *left;
 *         Tree *right;
 * };
 */
void dfs(Tree* root, int x, int y, map&amp;lt;int, pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;amp; up) {
    if(root == nullptr) return;
    if(up.find(x) == up.end() || up[y].first &amp;gt; h) up[x] = {h, root -&amp;gt; val};

    dfs(root -&amp;gt; left, x - 1, h + 1, up);
    dfs(root -&amp;gt; right, x + 1, h + 1, up);
}

vector&amp;lt;int&amp;gt; solve(Tree* root) {
    map&amp;lt;int, pair&amp;lt;int, int&amp;gt;&amp;gt; up;
    dfs(root, 0, 0, up);
    vector&amp;lt;int&amp;gt; ans;
    for (auto [k, v] : up) {
        ans.push_back(v.second);
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(nlogn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 52 Shortest Cycle Containing Target Node</title><link>https://chaggle.github.io/posts/2021/10/31/day-52-shortest-cycle-containing-target-node/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/31/day-52-shortest-cycle-containing-target-node/</guid><pubDate>Sun, 31 Oct 2021 12:26:49 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://binarysearch.com/problems/Shortest-Cycle-Containing-Target-Node&quot;&gt;Shortest Cycle Containing Target Node&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are given a two-dimensional list of integers graph

representing a directed graph as an adjacency list.

You are also given an integer target.

Return the length of a shortest cycle that contains target.

If a solution does not exist, return -1.

Constraints

n, m ≤ 250 where n and m are the number of rows and columns in graph
Example 1
Input
Visualize
graph = [
    [1],
    [2],
    [0]
]
target = 0
Output
3
Explanation
The nodes 0 -&amp;gt; 1 -&amp;gt; 2 -&amp;gt; 0 form a cycle

Example 2
Input
Visualize
graph = [
    [1],
    [2],
    [4],
    [],
    [0]
]
target = 3
Output
-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;BFS 题。查看示例可以发现图的存储方式是邻接表，所以直接套用 BFS 模板即可解决问题。&lt;/li&gt;
&lt;li&gt;从目标节点出发逐层扫描整张图，判断每个节点能否回到目标节点，能找到则直接返回当前层数，遍历完仍找不到则返回 -1。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int solve(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; g, int t) {
    int n = g.size();
    vector&amp;lt;bool&amp;gt; vis(n, 0);
    queue&amp;lt;int&amp;gt; q;
    q.push(t);
    int ans = 0;

    while (!q.empty())
    {
        ans++;
        int n = q.size();
        for (int i = 0; i &amp;lt; n; i++)
        {
            int node = q.front();q.pop();
            for (auto&amp;amp; v : g[node])
            {
                if(v == t) return ans;
                if(!vis[v])  //如果是未访问节点， 等价于 vis[v] == 0
                {
                    q.push(v);
                    vis[v] = 1;
                }
            }
        }
    }
    return -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n + e)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 51 1162. 地图分析</title><link>https://chaggle.github.io/posts/2021/10/30/day-51-1162-as-far-from-land-as-possible/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/30/day-51-1162-as-far-from-land-as-possible/</guid><pubDate>Sat, 30 Oct 2021 12:44:57 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/as-far-from-land-as-possible/&quot;&gt;1162. 地图分析&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;你现在手里有一份大小为 N x N 的 网格 grid，

上面的每个 单元格 都用 0 和 1 标记好了。其中 0 代表海洋，1 代表陆地，

请你找出一个海洋单元格，这个海洋单元格到离它最近的陆地单元格的距离是最大的。

我们这里说的距离是「曼哈顿距离」（ Manhattan Distance）：

(x0, y0) 和 (x1, y1) 这两个单元格之间的距离是 |x0 - x1| + |y0 - y1| 。

如果网格上只有陆地或者海洋，请返回 -1。

 

示例 1：


输入：[[1,0,1],[0,0,0],[1,0,1]]
输出：2
解释：
海洋单元格 (1, 1) 和所有陆地单元格之间的距离都达到最大，最大距离为 2。
示例 2：


输入：[[1,0,0],[0,0,0],[0,0,0]]
输出：4
解释：
海洋单元格 (2, 2) 和所有陆地单元格之间的距离都达到最大，最大距离为 4。
 

提示：

1 &amp;lt;= grid.length == grid[0].length &amp;lt;= 100
grid[i][j] 不是 0 就是 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;BFS 模板题，从所有陆地同时出发进行 BFS，只需要加上上下左右四个方向的判断条件即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; dis = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};
public:
    int maxDistance(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; grid) {
        int m = grid.size(), n = grid[0].size();
        deque&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; q;
        for(int i = 0; i &amp;lt; m; i++)
        {
            for(int j = 0; j &amp;lt; n; j++)
            {
                if(grid[i][j] == 1)
                {
                    q.push_back({i, j});
                }
            }
        }
        //无陆地或者海洋则返回；
        if(q.size() == 0 || q.size() == m * n) return -1;

        int ans = -1;
        while(!q.empty())
        {
            ans++;
            int s = q.size();
            while(s--)
            {
                auto k = q.front(); q.pop_front();
                for(auto&amp;amp; d : dis)
                {
                   int x = k.first + d[0];
                   int y = k.second + d[1];
                    // 如果搜索到的新坐标超出范围/陆地/已经遍历过，则不搜索了
                    if(x &amp;lt; 0 || x &amp;gt;= m || y &amp;lt; 0 || y &amp;gt;= n || grid[x][y] != 0) continue;
                    //搜索后记为-1，表示不能再遇见
                    grid[x][y] = -1;
                    q.push_back({x, y});
                }
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(m * n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 50 695. 岛屿的最大面积</title><link>https://chaggle.github.io/posts/2021/10/29/day-50-695-max-area-of-island/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/29/day-50-695-max-area-of-island/</guid><pubDate>Fri, 29 Oct 2021 14:42:28 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/max-area-of-island/&quot;&gt;695. 岛屿的最大面积&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个大小为 m x n 的二进制矩阵 grid 。

岛屿 是由一些相邻的 1 (代表土地) 构成的组合，

这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。

你可以假设 grid 的四个边缘都被 0（代表水）包围着。

岛屿的面积是岛上值为 1 的单元格的数目。

计算并返回 grid 中最大的岛屿面积。如果没有岛屿，则返回面积为 0 。

 

示例 1：


输入：grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0,
             [0,0,0,0,0,0,0,1,1,1,0,0,0],
             [0,1,1,0,1,0,0,0,0,0,0,0,0],
             [0,1,0,0,1,1,0,0,1,0,1,0,0],
             [0,1,0,0,1,1,0,0,1,1,1,0,0],
             [0,0,0,0,0,0,0,0,0,0,1,0,0],
             [0,0,0,0,0,0,0,1,1,1,0,0,0],
             [0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出：6
解释：答案不应该是 11 ，因为岛屿只能包含水平或垂直这四个方向上的 1 。
示例 2：

输入：grid = [[0,0,0,0,0,0,0,0]]
输出：0
 

提示：

m == grid.length
n == grid[i].length
1 &amp;lt;= m, n &amp;lt;= 50
grid[i][j] 为 0 或 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;常规的 DFS 解法。注意递归过程中局部变量不能重名，重名会导致错误。&lt;/li&gt;
&lt;li&gt;由于本题的岛屿用 0 和 1 表示，访问过的格子直接置 0 即可，可以省去访问数组；一般的 DFS 则需要设置 bool 类型的访问数组来标记是否访问过该格子。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxAreaOfIsland(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; grid) {
        int ans = 0;
        int n = grid.size(), m = grid[0].size();
        for(int i = 0; i &amp;lt; grid.size(); ++i)
        {
            for(int j = 0; j &amp;lt; grid[0].size(); ++j)
            {
                if (grid[i][j] == 1)
                    ans = max(dfs(grid, i, j), ans);
            }
        }
        return ans;
    }

    int dfs(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; grid, int i, int j)
    {
        int ans = 0;
        int l = grid.size(), r = grid[0].size();
        if(i &amp;gt;= l || i &amp;lt; 0 || j &amp;gt;= r || j &amp;lt; 0 || grid[i][j] == 0)
            return 0;
        else
        {
            ans++;
            grid[i][j] = 0;
            ans += dfs(grid, i + 1, j);
            ans += dfs(grid, i, j + 1);
            ans += dfs(grid, i - 1, j);
            ans += dfs(grid, i, j - 1);
        }

        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(m * n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 49 52. N皇后 II</title><link>https://chaggle.github.io/posts/2021/10/28/day-49-52-n-queens-ii/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/28/day-49-52-n-queens-ii/</guid><pubDate>Thu, 28 Oct 2021 07:12:35 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/n-queens-ii/&quot;&gt;52. N 皇后 II&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上，并且使皇后彼此之间不能相互攻击。

给你一个整数 n ，返回 n 皇后问题 不同的解决方案的数量。

示例 1：

输入：n = 4
输出：2
解释：如上图所示，4 皇后问题存在两个不同的解法。

示例 2：

输入：n = 1
输出：1
 

提示：

1 &amp;lt;= n &amp;lt;= 9
皇后彼此不能相互攻击，也就是说：任何两个皇后都不能处于同一条横行、纵行或斜线上。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;数据规模只有 1 到 9，本来可以直接打表、面向答案编程！&lt;/li&gt;
&lt;li&gt;不过这里还是用了回溯法 + DFS：只要横、竖、斜三个方向没有其他皇后即可放置，若均冲突则返回 false。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
private:
    vector&amp;lt;int&amp;gt; res;
    int ans = 0;
public:
    void dfs(vector&amp;lt;int&amp;gt;&amp;amp; res, int col, int row)
    {
        if(col == row) ans++;
        for(int i = 0; i &amp;lt; row; i++)
        {
            if (judge(res, i))
            {
                res.push_back(i);
                dfs(res, col + 1, row);
                res.pop_back();
            }
        }
    }

    bool judge(vector&amp;lt;int&amp;gt; res, int n)
    {
        for(int i = 0; i &amp;lt; res.size(); i++)
        {
            if ((abs(res[i] - n) == res.size() - i) || n == res[i]) return false;
        }
        return true;
    }

    int totalNQueens(int n) {
        /* vector&amp;lt;int&amp;gt; res = {1, 0, 0 ,2, 10, 4, 40, 92, 352};
        return res[n - 1]; */
        dfs(res, 0, n);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n!)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 48 401. 二进制手表</title><link>https://chaggle.github.io/posts/2021/10/27/day-48-401-binary-watch/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/27/day-48-401-binary-watch/</guid><pubDate>Wed, 27 Oct 2021 14:33:25 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/binary-watch/&quot;&gt;401. 二进制手表&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;二进制手表顶部有 4 个 LED 代表 小时（0-11），底部的 6 个 LED 代表 分钟（0-59）。

每个 LED 代表一个 0 或 1，最低位在右侧。

例如，下面的二进制手表读取 &quot;3:25&quot; 。

给你一个整数 turnedOn ，表示当前亮着的 LED 的数量，

返回二进制手表可以表示的所有可能时间。你可以 按任意顺序 返回答案。

小时不会以零开头：

例如，&quot;01:00&quot; 是无效的时间，正确的写法应该是 &quot;1:00&quot; 。
分钟必须由两位数组成，可能会以零开头：

例如，&quot;10:2&quot; 是无效的时间，正确的写法应该是 &quot;10:02&quot; 。
 

示例 1：

输入：turnedOn = 1
输出：[&quot;0:01&quot;,&quot;0:02&quot;,&quot;0:04&quot;,&quot;0:08&quot;,&quot;0:16&quot;,&quot;0:32&quot;,&quot;1:00&quot;,&quot;2:00&quot;,&quot;4:00&quot;,&quot;8:00&quot;]
示例 2：

输入：turnedOn = 9
输出：[]
 

提示：

0 &amp;lt;= turnedOn &amp;lt;= 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;这类题目使用打表（枚举）的方法可能更好，直接枚举小时和分钟的所有组合，判断亮灯数量是否等于 turnedOn 即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int count1(int n)
    {
        int k = 0;
        while(n != 0)
        {
            n = n &amp;amp; (n - 1);
            k++;
        }
        return k;
    }

    vector&amp;lt;string&amp;gt; readBinaryWatch(int turnedOn) {
        vector&amp;lt;string&amp;gt; ans;
        for (int i = 0; i &amp;lt; 12; i++)
        {
            for (int j = 0; j &amp;lt; 60; j++)
            {
                if (count1(i) + count1(j) == turnedOn)
                {
                    ans.push_back(to_string(i)+&quot;:&quot;+
                                  (j &amp;lt; 10 ? &quot;0&quot;+to_string(j) : to_string(j)));
                }
            }
        }
        return ans;
    }

};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 47 Number of Operations to Decrement Target to Zero</title><link>https://chaggle.github.io/posts/2021/10/26/day-47-number-of-operations-to-decrement-target-to-zero/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/26/day-47-number-of-operations-to-decrement-target-to-zero/</guid><pubDate>Tue, 26 Oct 2021 12:04:41 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://binarysearch.com/problems/Number-of-Operations-to-Decrement-Target-to-Zero&quot;&gt;Number of Operations to Decrement Target to Zero&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are given a list of positive integers nums and an integer target.

Consider an operation where we remove a number v from either the front

or the back of nums and decrement target by v.

Return the minimum number of operations required to decrement target to zero.

If it&apos;s not possible, return -1.

Constraints

n ≤ 100,000 where n is the length of nums
Example 1
Input
nums = [3, 1, 1, 2, 5, 1, 1]
target = 7
Output
3
Explanation
We can remove 1, 1 and 5 from the back to decrement target to zero.

Example 2
Input
nums = [2, 4]
target = 7
Output
-1
Explanation
There&apos;s no way to decrement target = 7 to zero.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;滑动窗口题。先遍历数组求总和，若总和恰好等于 target，说明一次操作都不用做。&lt;/li&gt;
&lt;li&gt;若不相等，则用总和减去 target，得到需要从数组中移除的和。使用双指针滑动窗口寻找和为该值的最大连续子数组：窗口内和大于目标值时，左指针向前滑动；相等时对比之前的长度，记录最大的移动区间。最后用数组长度减去这个长度，就是最少需要的操作次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int solve(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
    int n = nums.size();
    int sum;

    for(int i = 0; i &amp;lt; n; i++)
    {
        sum += nums[i];
    }
    if (sum == target) return n;
	sum -= target;

    int l = 0, total = 0, pos = 0;
    for (int r = 0; r &amp;lt; n; r++)
    {
        total += nums[r];
        while(total &amp;gt; sum)
        {
            total -= nums[l++];
        }
        if (total == sum)
        {
            pos = max(pos, r - l + 1);
        }
    }
    return pos &amp;gt; 0 ? n - pos : -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 46 76. 最小覆盖子串</title><link>https://chaggle.github.io/posts/2021/10/25/day-46-76-minimum-window-substring/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/25/day-46-76-minimum-window-substring/</guid><pubDate>Mon, 25 Oct 2021 13:36:54 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/minimum-window-substring/&quot;&gt;76. 最小覆盖子串&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个字符串 s 、一个字符串 t 。

返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串，则返回空字符串 &quot;&quot; 。

 

注意：

对于 t 中重复字符，我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串，我们保证它是唯一的答案。
 

示例 1：

输入：s = &quot;ADOBECODEBANC&quot;, t = &quot;ABC&quot;
输出：&quot;BANC&quot;
示例 2：

输入：s = &quot;a&quot;, t = &quot;a&quot;
输出：&quot;a&quot;
示例 3:

输入: s = &quot;a&quot;, t = &quot;aa&quot;
输出: &quot;&quot;
解释: t 中两个字符 &apos;a&apos; 均应包含在 s 的子串中，
因此没有符合条件的子字符串，返回空字符串。
 

提示：

1 &amp;lt;= s.length, t.length &amp;lt;= 105
s 和 t 由英文字母组成
 

进阶：你能设计一个在 o(n) 时间内解决此问题的算法吗？
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题依旧使用滑动窗口解法。核心在于：只要 s 的窗口内各字符的出现次数不少于 t 中对应字符的出现次数，该窗口就是一个覆盖子串，再在其中求出最短的区间即可。&lt;/li&gt;
&lt;li&gt;与昨天的题目一样，本题使用两个哈希表代替两个数组。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    string minWindow(string s, string t) {
        int n = s.size(), m = t.size();
        unordered_map&amp;lt;char, int&amp;gt; wins, wint;
        for(auto ch: t)
        {
            wint[ch]++;
        }
        string res;
        int count = 0;
        for(int i = 0, j = 0; i &amp;lt; n; i++)
        {
            wins[s[i]] ++ ;
            if(wins[s[i]] &amp;lt;= wint[s[i]]) count++ ;
            while(wins[s[j]] &amp;gt; wint[s[j]]) wins[s[j++]]--;
            if(count == m)
            {
                if(res.empty() || i - j + 1 &amp;lt; res.size())
                {
                    res = s.substr(j, i - j + 1);
                }
            }
        }
        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 45 438. 找到字符串中所有字母异位词</title><link>https://chaggle.github.io/posts/2021/10/24/day-45-438-find-all-anagrams-in-a-string/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/24/day-45-438-find-all-anagrams-in-a-string/</guid><pubDate>Sun, 24 Oct 2021 11:52:21 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/&quot;&gt;438. 找到字符串中所有字母异位词&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定两个字符串 s 和 p，找到 s 中所有 p 的 异位词 的子串，

返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串（包括相同的字符串）。

 

示例 1:

输入: s = &quot;cbaebabacd&quot;, p = &quot;abc&quot;
输出: [0,6]
解释:
起始索引等于 0 的子串是 &quot;cba&quot;, 它是 &quot;abc&quot; 的异位词。
起始索引等于 6 的子串是 &quot;bac&quot;, 它是 &quot;abc&quot; 的异位词。
 示例 2:

输入: s = &quot;abab&quot;, p = &quot;ab&quot;
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 &quot;ab&quot;, 它是 &quot;ab&quot; 的异位词。
起始索引等于 1 的子串是 &quot;ba&quot;, 它是 &quot;ab&quot; 的异位词。
起始索引等于 2 的子串是 &quot;ab&quot;, 它是 &quot;ab&quot; 的异位词。
 

提示:

1 &amp;lt;= s.length, p.length &amp;lt;= 3 * 104
s 和 p 仅包含小写字母
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题依旧使用滑动窗口解法。核心在于：只要 s 中长度为 p 的子串内各个字母的出现次数，与 p 中对应字母的出现次数相同，该子串就是 p 的字母异位词，因此只需要两个计数数组即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; findAnagrams(string s, string p) {
        vector&amp;lt;int&amp;gt; ans;
        int n = s.size(), m = p.size();
        if(n &amp;lt; m) return ans;
        vector&amp;lt;int&amp;gt; sin(26, 0);
        vector&amp;lt;int&amp;gt; pin(26, 0);
        for(int i = 0; i &amp;lt; m; i++)
        {
            pin[p[i] - &apos;a&apos;]++;
        }
        int l = 0;
        for(int r = 0; r &amp;lt; n; r++)
        {
            sin[s[r] - &apos;a&apos;]++;
            if(r &amp;gt;= m)
            {
                sin[s[l] - &apos;a&apos;]--;
                l++;
            }
            if(pin == sin) ans.push_back(l);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 44 837. 新21点</title><link>https://chaggle.github.io/posts/2021/10/23/day-44-837-new-21-game/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/23/day-44-837-new-21-game/</guid><pubDate>Sat, 23 Oct 2021 08:51:49 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/new-21-game/&quot;&gt;837. 新 21 点&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;爱丽丝参与一个大致基于纸牌游戏 “21点” 规则的游戏，描述如下：

爱丽丝以 0 分开始，并在她的得分少于 K 分时抽取数字。

抽取时，她从 [1, W] 的范围中随机获得一个整数作为分数进行累计，其中 W 是整数。

 每次抽取都是独立的，其结果具有相同的概率。

当爱丽丝获得不少于 K 分时，她就停止抽取数字。 爱丽丝的分数不超过 N 的概率是多少？

 

示例 1：

输入：N = 10, K = 1, W = 10
输出：1.00000
说明：爱丽丝得到一张卡，然后停止。
示例 2：

输入：N = 6, K = 1, W = 10
输出：0.60000
说明：爱丽丝得到一张卡，然后停止。
在 W = 10 的 6 种可能下，她的得分不超过 N = 6 分。
示例 3：

输入：N = 21, K = 17, W = 10
输出：0.73278
 

提示：

0 &amp;lt;= K &amp;lt;= N &amp;lt;= 10000
1 &amp;lt;= W &amp;lt;= 10000
如果答案与正确答案的误差不超过 10^-5，则该答案将被视为正确答案通过。
此问题的判断限制时间已经减少。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;本题不仅使用滑动窗口来减小重复计算，还需要用动态规划来建立状态转移方程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;题意是：从 0 分开始，手中分数小于 K 时就随机抽取，每次从 [1, W] 中抽到的概率互相独立，均为 1 / W。令 dp[x] 表示从得分为 x 的情况开始游戏并获胜的概率，目标是求 dp[0] 的值。整个题目给人一种用全概率公式求解条件概率的感觉，前一次抽取的结果会影响后续的抽取和结论，与马尔可夫链如出一辙；查了查书发现确实如此，于是应用马尔可夫链的相关结论，可以得到状态转移方程：&lt;/p&gt;
&lt;p&gt;$$
dp\left[ x \right] =\frac{\sum_{i=1}^w{dp\left[ x,,+,,i \right]}}{w}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本题应该属于较难的题目，思想方法不易理解，需要较强的概率论基础。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    double new21Game(int n, int k, int w) {
        double dp[k + w];
        memset(dp, 0, sizeof(dp));
        double ans = 0.0;
        for(int i = k; i &amp;lt; k + w; i++)
        {
            dp[i] = i &amp;lt;= n ? 1 : 0;
            ans += dp[i];
        }
        for(int i = k-1; i &amp;gt;= 0; i--)
        {
            dp[i] = ans / w;
            ans = ans - dp[i + w] + dp[i];
        }
        return dp[0];
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n + k)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n + k)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 43 1456. 定长子串中元音的最大数目</title><link>https://chaggle.github.io/posts/2021/10/22/day-43-1456-maximum-number-of-vowels-in-a-substring-of-given-length/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/22/day-43-1456-maximum-number-of-vowels-in-a-substring-of-given-length/</guid><pubDate>Fri, 22 Oct 2021 12:22:22 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/maximum-number-of-vowels-in-a-substring-of-given-length/&quot;&gt;1456. 定长子串中元音的最大数目&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你字符串 s 和整数 k 。

请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。

英文中的 元音字母 为（a, e, i, o, u）。

 

示例 1：

输入：s = &quot;abciiidef&quot;, k = 3
输出：3
解释：子字符串 &quot;iii&quot; 包含 3 个元音字母。
示例 2：

输入：s = &quot;aeiou&quot;, k = 2
输出：2
解释：任意长度为 2 的子字符串都包含 2 个元音字母。
示例 3：

输入：s = &quot;leetcode&quot;, k = 3
输出：2
解释：&quot;lee&quot;、&quot;eet&quot; 和 &quot;ode&quot; 都包含 2 个元音字母。
示例 4：

输入：s = &quot;rhythms&quot;, k = 4
输出：0
解释：字符串 s 中不含任何元音字母。
示例 5：

输入：s = &quot;tryhard&quot;, k = 4
输出：1
 

提示：

1 &amp;lt;= s.length &amp;lt;= 10^5
s 由小写英文字母组成
1 &amp;lt;= k &amp;lt;= s.length
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;滑动窗口题。设定窗口大小为 k，从左到右逐步滑动，每次滑动时判断窗口头尾字符的变化，更新窗口内的元音数量即可。还可以分情况简化写法，但时间复杂度基本不变。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool isvaild(char ch) {
        return ch == &apos;a&apos; || ch == &apos;e&apos; || ch == &apos;i&apos; || ch == &apos;o&apos; || ch == &apos;u&apos;;
    }

    int maxVowels(string s, int k) {
        int n = s.size();
        deque&amp;lt;char&amp;gt; ans;
        int count = 0, tmp = 0;
        for(int i = 0; i &amp;lt; k; i++)
        {
            if(isvaild(s[i])) count++; ans.push_back(s[i]);
        }

        tmp = count;

        for(int i = k; i &amp;lt; n; i++)
        {
            if(isvaild(ans.front())) count--; ans.pop_front();
            if(isvaild(s[i])) count++; ans.push_back(s[i]);
            if(tmp &amp;lt; count) tmp = count;
        }
        return tmp;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 42 778. 水位上升的泳池中游泳</title><link>https://chaggle.github.io/posts/2021/10/21/day-42-778-swim-in-rising-water/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/21/day-42-778-swim-in-rising-water/</guid><pubDate>Thu, 21 Oct 2021 08:30:21 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/swim-in-rising-water/&quot;&gt;778. 水位上升的泳池中游泳&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;在一个 N x N 的坐标方格 grid 中，每一个方格的值 grid[i][j] 表示在位置 (i,j) 的平台高度。

现在开始下雨了。当时间为 t 时，此时雨水导致水池中任意位置的水位为 t 。

你可以从一个平台游向四周相邻的任意一个平台，但是前提是此时水位必须同时淹没这两个平台。

假定你可以瞬间移动无限距离，也就是默认在方格内部游动是不耗时的。

当然，在你游泳的时候你必须待在坐标方格里面。

你从坐标方格的左上平台 (0，0) 出发。最少耗时多久你才能到达坐标方格的右下平台 (N-1, N-1)？

 

示例 1:

输入: [[0,2],[1,3]]
输出: 3
解释:
时间为0时，你位于坐标方格的位置为 (0, 0)。
此时你不能游向任意方向，

因为四个相邻方向平台的高度都大于当前时间为 0 时的水位。

等时间到达 3 时，你才可以游向平台 (1, 1). 因为此时的水位是 3，

坐标方格中的平台没有比水位 3 更高的，所以你可以游向坐标方格中的任意位置
示例2:

输入: [[0,1,2,3,4],
      [24,23,22,21,5],
      [12,13,14,15,16],
      [11,17,18,19,20],
      [10,9,8,7,6]]
输出: 16
解释:
 0  1  2  3  4
24 23 22 21  5
12 13 14 15 16
11 17 18 19 20
10  9  8  7  6

最终的路线用加粗进行了标记。
我们必须等到时间为 16，此时才能保证平台 (0, 0) 和 (4, 4) 是连通的
 

提示:

2 &amp;lt;= N &amp;lt;= 50.
grid[i][j] 是 [0, ..., N * N - 1] 的排列。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;其实最想用并查集去做，毕竟它能很方便地判断图的连通情况；但考虑到实现方法是二分，所以还是采用了二分查找 + DFS 遍历图的做法。&lt;/li&gt;
&lt;li&gt;二分的目的是寻找一个合适的水位阈值，DFS 用来尝试能否从左上角走到右下角，边界值应该是 $n * n$；搜索时不能重复走已经走过的格子，所以要设置一个 bool 类型的标记记录。&lt;/li&gt;
&lt;li&gt;DFS 写起来其实比较简单，边界条件想清楚即可：只有一种情况能返回 true，其他情况都返回 false，每次搜索有四个方向可选。&lt;/li&gt;
&lt;li&gt;这道题比前两天的题目要简单一些。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int swimInWater(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; grid) {
        int n = grid.size();
        int l = 0, r = n * n - 1;
        while(l &amp;lt; r)
        {
            int mid = l + (r - l) / 2;
            vector&amp;lt;vector&amp;lt;bool&amp;gt;&amp;gt; vis(n, vector&amp;lt;bool&amp;gt; (n, false));
            if(dfs(grid, vis, n, mid, 0, 0)) r = mid;//可达，偏大
            else l = mid + 1; //不可达，偏小
        }
        return l;
    }

        bool dfs(
            vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp;grid,
            vector&amp;lt;vector&amp;lt;bool&amp;gt;&amp;gt; &amp;amp;vis, int n, int mid, int i, int j)
        {
            if(i &amp;lt; 0 || j &amp;lt; 0 || i &amp;gt;= n || j &amp;gt;= n) return false;
            else if(vis[i][j]) return false;
            vis[i][j] = true;
            if(grid[i][j] &amp;gt; mid) return false;
            else if(i == n - 1 &amp;amp;&amp;amp; j == n - 1) return true; //只有到终点才返回true；
            return dfs(grid, vis, n, mid, i - 1, j) ||
                   dfs(grid, vis, n, mid, i + 1, j) ||
                   dfs(grid, vis, n, mid, i, j - 1) ||
                   dfs(grid, vis, n, mid, i, j + 1);
        }

};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O($n ^ 2 * logn$)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O($n ^ 2$)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 41 822. Kth-Pair-Distance</title><link>https://chaggle.github.io/posts/2021/10/20/day-41-822-kth-pair-distance/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/20/day-41-822-kth-pair-distance/</guid><pubDate>Wed, 20 Oct 2021 12:22:16 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://binarysearch.com/problems/Kth-Pair-Distance&quot;&gt;822. Kth-Pair-Distance&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Given a list of integers nums and an integer k,

return the k-th smallest abs(x - y) for every pair of elements (x, y) in nums.

Note that (x, y) and (y, x) are considered the same pair.

Constraints

n ≤ 100,000 where n is the length of nums
Example 1
Input
nums = [1, 5, 3, 2]
k = 3
Output
2
Explanation
Here are all the pair distances:

abs(1 - 5) = 4
abs(1 - 3) = 2
abs(1 - 2) = 1
abs(5 - 3) = 2
abs(5 - 2) = 3
abs(3 - 2) = 1
Sorted in ascending order we have [1, 1, 2, 2, 3, 4].
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;题目求的是数组中第 k 小的数对距离。先将数组排序，最大的距离就是数组首尾元素之差，最小的距离当然为 0，在这个范围内二分查找一个合理的值即可。&lt;/li&gt;
&lt;li&gt;使用二分法，每次取 mid = l + (r - l) / 2，可以降低查找的时间复杂度；如果直接计数枚举所有数对，时间复杂度会达到 O(n^2)。&lt;/li&gt;
&lt;li&gt;本题也可以从数学的角度考虑：所有距离对一共有 $n(n-1)$ 个，只要大于 mid 的距离对数有 $n(n-1)$ - k 个，即为不合理。（还有些地方没想明白，先放着。）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool isvaild(vector&amp;lt;int&amp;gt;&amp;amp; nums, int dis, int k)
    {
        int n = nums.size();
        long long int count = 0;
        int i = 0, j = 0;
        while (i &amp;lt; n || j &amp;lt; n)
        {
            while (j &amp;lt; n &amp;amp;&amp;amp; nums[j] - nums[i] &amp;lt;= dis) j++;
            count += j - i - 1;
            i++;
        }
        return count &amp;gt;= k;
    };

    int solve(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) {
        int n = nums.size();
        k++;
        sort(nums.begin(), nums.end());
        int l = 0, r = nums[n - 1] - nums[0];
        while(l &amp;lt; r)
        {
            int mid = l + (r - l) / 2;
            int count = 0, left = 0;
            if(isvaild(nums, mid, k)) r = mid;
            else l = mid + 1;
        }
        return l;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(nlogn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 40 796. Minimum Light Radius</title><link>https://chaggle.github.io/posts/2021/10/19/day-40-796-minimum-light-radius/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/19/day-40-796-minimum-light-radius/</guid><pubDate>Tue, 19 Oct 2021 10:03:15 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://binarysearch.com/problems/Minimum-Light-Radius&quot;&gt;796. Minimum Light Radius&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are given a list of integers nums

representing coordinates of houses on a 1-dimensional line.

You have 3 street lights that you can put anywhere on the coordinate line

and a light at coordinate x lights up houses in [x - r, x + r],

inclusive. Return the smallest r required

such that we can place the 3 lights and all the houses are lit up.

Constraints

n ≤ 100,000 where n is the length of nums
Example 1
Input
nums = [3, 4, 5, 6]
Output
0.5
Explanation
If we place the lamps on 3.5, 4.5 and 5.5 then with r = 0.5 we can light up all 4 houses.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;这道题有点数学问题的味道，通过两个二分搜索来找到可能的最小半径。&lt;/li&gt;
&lt;li&gt;其中一个二分用于判断当前的直径能否用三个路灯覆盖整个数组的范围。&lt;/li&gt;
&lt;li&gt;另一个二分则用来调整直径的大小。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool isvaild(double diameter, vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        double l = 0, ans = 0;
        while (l &amp;lt; n)
        {
            l = upper_bound(nums.begin(), nums.end(), nums[l] + diameter) - nums.begin();
            ans++;
        }
        return ans &amp;lt;= 3 ? 1: 0;
    }

    double solve(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        double l = 0;
        double r = nums[n - 1];
        while(l &amp;lt; r)
        {
            int diameter = l + (r - l) / 2;
            if(isvaild(diameter, nums)) r = diameter;
            else l = diameter + 1;
        }
        return l / 2;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(nlogn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 39 762.Number Stream to Intervals</title><link>https://chaggle.github.io/posts/2021/10/18/day-39-762-number-stream-to-intervals/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/18/day-39-762-number-stream-to-intervals/</guid><pubDate>Mon, 18 Oct 2021 08:30:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://binarysearch.com/problems/Triple-Inversion&quot;&gt;762.Number Stream to Intervals&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Given a list of integers nums,

return the number of pairs i &amp;lt; j such that nums[i] &amp;gt; nums[j] * 3.

Constraints

n ≤ 100,000 where n is the length of nums
Example 1
Input
nums = [7, 1, 2]
Output
2
Explanation
We have the pairs (7, 1) and (7, 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;构造逆序对，然后返回逆序对的数量。使用归并排序比二分法更合适，在归并的过程中统计每个子区间里大于当前 nums[q] * 3 的元素个数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    void merge(vector&amp;lt;int&amp;gt; &amp;amp;nums, int i, int mid, int j)
    {

        vector&amp;lt;int&amp;gt; ans(j - i + 1);
        int l = i, r = mid + 1;
        int cnt = 0;

        while(l &amp;lt;= mid &amp;amp;&amp;amp; r &amp;lt;= j)
        {
            if(nums[l] &amp;lt;= nums[r])
            {
                ans[cnt] = nums[l];
                l++;
            }
            else
            {
                ans[cnt] = nums[r];
                r++;
            }
            cnt++;
        }

        while(l &amp;lt;= mid) ans[cnt++] = nums[l++];
        while(r &amp;lt;= j) ans[cnt++] = nums[r++];

        cnt = 0;
        while(i &amp;lt;= j) nums[i++] = ans[cnt++];
    }

    int mergeSort(vector&amp;lt;int&amp;gt; &amp;amp;nums, int l, int r)
    {
        if(r &amp;lt;= l) return 0;
        int ans = 0;
        int mid = (l + r) / 2;
        ans += mergeSort(nums, l, mid);
        ans += mergeSort(nums, mid + 1, r);

        int p = l;
        int q = mid + 1;
        while(p &amp;lt;= mid &amp;amp;&amp;amp; q &amp;lt;= r)
        {
            if(nums[p] &amp;gt; nums[q] * 3)
            {
                ans += (mid - p + 1);
                q++;
            }
            else p++;
        }
        merge(nums, l, mid, r);
        return ans;
    }

    int solve(vector&amp;lt;int&amp;gt; &amp;amp;nums)
    {
        int ans = mergeSort(nums, 0, nums.size() - 1);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(nlogn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 38 278. 第一个错误的版本</title><link>https://chaggle.github.io/posts/2021/10/17/day-38-278-first-bad-version/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/17/day-38-278-first-bad-version/</guid><pubDate>Sun, 17 Oct 2021 11:30:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/first-bad-version/&quot;&gt;278. 第一个错误的版本&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;你是产品经理，目前正在带领一个团队开发新的产品。不幸的是，你的产品的最新版本没有通过质量检测。

由于每个版本都是基于之前的版本开发的，所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, ..., n]，你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。

实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

 
示例 1：

输入：n = 5, bad = 4
输出：4
解释：
调用 isBadVersion(3) -&amp;gt; false
调用 isBadVersion(5) -&amp;gt; true
调用 isBadVersion(4) -&amp;gt; true
所以，4 是第一个错误的版本。
示例 2：

输入：n = 1, bad = 1
输出：1
 

提示：

1 &amp;lt;= bad &amp;lt;= n &amp;lt;= 2^31 - 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;需要注意：当接口返回 true 时，并不一定就找到了目标位置，还需要继续向前查找，直到找到第一个有问题的版本为止。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion(int n) {
      int l = 1, r = n;
      while(l &amp;lt; r)
      {
          int mid = l + (r - l) / 2;
          if(isBadVersion(mid)) r = mid;
          else l = mid + 1;
      }
      return l;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(logn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 37 69. Sqrt(x)</title><link>https://chaggle.github.io/posts/2021/10/16/day-37-69-sqrtx/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/16/day-37-69-sqrtx/</guid><pubDate>Sat, 16 Oct 2021 09:44:11 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/sqrtx/&quot;&gt;69. Sqrt(x)&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个非负整数 x ，计算并返回 x 的 算术平方根 。

由于返回类型是整数，结果只保留 整数部分 ，小数部分将被 舍去 。

注意：不允许使用任何内置指数函数和算符，例如 pow(x, 0.5) 或者 x ** 0.5 。

 

示例 1：

输入：x = 4
输出：2
示例 2：

输入：x = 8
输出：2
解释：8 的算术平方根是 2.82842..., 由于返回类型是整数，小数部分将被舍去。
 

提示：

0 &amp;lt;= x &amp;lt;= 2^31 - 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用二分法求算术平方根。另外还有牛顿迭代法和红蓝法，我还没有研究，后面找时间研究一下。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int mySqrt(int x) {
        long long l = 0, r = x;
        while(l &amp;lt; r)
        {
            long long mid = (l + r + 1) &amp;gt;&amp;gt; 1;
            if(mid &amp;lt;= x /mid) l = mid;
            else r = mid - 1;
        }
        return l;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(logn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 36 912. 排序数组</title><link>https://chaggle.github.io/posts/2021/10/15/day-36-912-sort-an-array/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/15/day-36-912-sort-an-array/</guid><pubDate>Fri, 15 Oct 2021 12:25:16 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/sort-an-array/&quot;&gt;912. 排序数组&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个整数数组 nums，请你将该数组升序排列。

 

示例 1：

输入：nums = [5,2,3,1]
输出：[1,2,3,5]
示例 2：

输入：nums = [5,1,1,2,0,0]
输出：[0,0,1,1,2,5]
 

提示：

1 &amp;lt;= nums.length &amp;lt;= 50000
-50000 &amp;lt;= nums[i] &amp;lt;= 50000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;最简单的写法是直接调用库函数，但这样显然没有任何意义，所以这里把各类排序算法都复现了一遍，顺便练习一下，防止考研过程中遇到相应的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; sortArray(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        //sort(nums.begin(), nums.end());
        //return nums;
        int n = nums.size();

        //冒泡排序 超时
        /* for(int i = 0; i &amp;lt; n; i++)
        {
            bool flag = false;
            for(int j = n - 2; j &amp;gt;= i; j--)
            {
                if(nums[j] &amp;gt; nums[j + 1])
                {
                    swap(nums[j], nums[j + 1]);
                    flag = true;
                }
            }
            if(flag == false)break;
        } */

        //插入排序 超时
        /* for(int i = 0; i &amp;lt; n - 1; i++)
        {
            int min = i;
            for(int j = i + 1; j &amp;lt; n; j++)
            {
                if(nums[min] &amp;gt; nums[j]) min = j;
            }
            if(min != i) swap(nums[i], nums[min]);
        } */

        //快排 失败
        /* dfs(0, n - 1, nums); */

        //堆排序
        /* for(int i = 0; i &amp;lt; n; i++)
        {
            int pos = i;
            int mid = (pos - 1)/2;
            while(nums[pos] &amp;gt; nums[mid])
            {
                swap(nums[pos], nums[mid]);
                pos = mid;
                mid = (pos - 1) / 2;
            }
        }
        for(int j = n - 1; j &amp;gt;= 0; j--)
        {
            swap(nums[0], nums[j]);
            heapsort(nums,0,j);
        } */

        //计数排序
        int minn = *min_element(nums.begin(), nums.end());
        int maxn = *max_element(nums.begin(), nums.end());
        vector&amp;lt;int&amp;gt; ans(maxn - minn + 1);
        for(auto i : nums)
        {
            ans[i - minn]++;
        }
        int j = 0, i = 0;
        while(i &amp;lt; ans.size())
        {
            while(ans[i] &amp;gt; 0)
            {
                nums[j++] = i + minn;
                ans[i]--;
            }
            i++;
        }
        return nums;
    }

    /* void dfs(int start, int end, vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        if(start &amp;gt; end) return;
        int i = start, j = end;
        while(i &amp;lt; j)
        {
            while(nums[j] &amp;gt;= nums[start] &amp;amp;&amp;amp; i &amp;lt; j) j--;
            while(nums[i] &amp;lt;= nums[start] &amp;amp;&amp;amp; i &amp;lt; j) i++;
            if(i &amp;lt; j) swap(nums[i], nums[j]);
        }
        swap(nums[start], nums[i]);
        dfs(start, i - 1, nums);
        dfs(i + 1, end, nums);
    } */

   /*  void heapsort(vector&amp;lt;int&amp;gt;&amp;amp; nums, int pos, int n)
    {
        int l = pos * 2 + 1, r = pos * 2 + 2;
        while(l &amp;lt; n)
        {
            int big = pos;
            if(l &amp;lt; n &amp;amp;&amp;amp; nums[l] &amp;gt; nums[big]) big = l;
            if(r &amp;lt; n &amp;amp;&amp;amp; nums[r] &amp;gt; nums[big]) big = r;
            if(big == pos) break;
            swap(nums[pos], nums[big]);
            pos = big;
            l = pos * 2 + 1, r = pos * 2 + 2;
        }
    } */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(nlogn) 堆排序；O(n) 计数排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1) 堆排序；O(n) 计数排序&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Centos 8.2 中 Git 安装</title><link>https://chaggle.github.io/posts/2021/10/15/git-install-centos/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/15/git-install-centos/</guid><pubDate>Fri, 15 Oct 2021 12:18:42 GMT</pubDate><content:encoded>&lt;h2&gt;安装依赖包&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 系统为 centos8.2 版本，可能会出现系统版本不兼容等问题！
sudo yum -y install make autoconf automake cmake perl-CPAN libcurl-devel libtool gcc gcc-c++ glibc-headers zlib-devel git-lfs telnet ctags lrzsz jq expat-devel openssl-devel
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装新版 Git 包&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd /tmp
wget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.30.2.tar.gz
tar -xvzf git-2.30.2.tar.gz
cd git-2.30.2/
./configure
make
sudo make install
git --version          # 输出 git 版本号，说明安装成功
git version 2.30.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用配置命令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git config --global user.name &quot;chaggle&quot;  用户名改成自己的
git config --global user.email &quot;chaggle@foxmail.com&quot; 邮箱改成自己的
git config --global credential.helper store
git config --global core.longpaths true

# 在 Git 中，我们会把非 ASCII 字符叫做 Unusual 字符。这类字符在 Git 输出到终端的时候默认是用 8 进制转义字符输出的（以防乱码），但现在的终端多数都支持直接显示非 ASCII 字符，所以我们可以关闭掉这个特性
git config --global core.quotepath off

# 访问 github.com 太慢，可以通过国内 GitHub 镜像网站来访问
git config --global url.&quot;https://gitclone.com/&quot;.insteadOf &quot;https://&quot;

# GitHub 限制最大只能克隆 100M 的单个文件，为了能够克隆大于 100M 的文件，我们还需要安装 Git Large File Storage
git lfs install --skip-repo
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GitHub 代理配置&lt;/h2&gt;
&lt;p&gt;这里博主为了稳定性考虑，只推荐 https://gitclone.com 网站，其余的代理都容易失效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#原地址
git clone https://github.com/kubernetes/kubernetes.git
#改为
git clone https://gitclone.com/github.com/kubernetes/kubernetes.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是提交 Git 仓库的时候，需要将代理地址进行还原，否则就会上传到代理地址对应的 Git 仓库！&lt;/p&gt;
&lt;h2&gt;HTTPS 与 SSH clone 的稳定性问题&lt;/h2&gt;
&lt;p&gt;一般建议在网络环境友好的情况下使用 HTTPS 协议，配置简单；而网络环境差的情况下使用 SSH 协议，因为 SSH 建立的传输链接不容易丢失。&lt;/p&gt;
</content:encoded></item><item><title>Day 35 1737. 满足三条件之一需改变的最少字符数</title><link>https://chaggle.github.io/posts/2021/10/14/day-35-1737-change-minimum-characters-to-satisfy-one-of-three-conditions/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/14/day-35-1737-change-minimum-characters-to-satisfy-one-of-three-conditions/</guid><pubDate>Thu, 14 Oct 2021 08:34:23 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/change-minimum-characters-to-satisfy-one-of-three-conditions/&quot;&gt;1737. 满足三条件之一需改变的最少字符数&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你两个字符串 a 和 b ，二者均由小写字母组成。一步操作中，

你可以将 a 或 b 中的 任一字符 改变为 任一小写字母 。

操作的最终目标是满足下列三个条件 之一 ：

a 中的 每个字母 在字母表中 严格小于 b 中的 每个字母 。
b 中的 每个字母 在字母表中 严格小于 a 中的 每个字母 。
a 和 b 都 由 同一个 字母组成。
返回达成目标所需的 最少 操作数。

 

示例 1：

输入：a = &quot;aba&quot;, b = &quot;caa&quot;
输出：2
解释：满足每个条件的最佳方案分别是：
1) 将 b 变为 &quot;ccc&quot;，2 次操作，满足 a 中的每个字母都小于 b 中的每个字母；
2) 将 a 变为 &quot;bbb&quot; 并将 b 变为 &quot;aaa&quot;，3 次操作，满足 b 中的每个字母都小于 a 中的每个字母；
3) 将 a 变为 &quot;aaa&quot; 并将 b 变为 &quot;aaa&quot;，2 次操作，满足 a 和 b 由同一个字母组成。
最佳的方案只需要 2 次操作（满足条件 1 或者条件 3）。
示例 2：

输入：a = &quot;dabadd&quot;, b = &quot;cda&quot;
输出：3
解释：满足条件 1 的最佳方案是将 b 变为 &quot;eee&quot; 。
 

提示：

1 &amp;lt;= a.length, b.length &amp;lt;= 105
a 和 b 只由小写字母组成
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用前缀和以及计数的思想，枚举字母表中的分界点，分别计算三种条件下需要修改的字符数，取其中的最小值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int minCharacters(string a, string b) {
        int n = a.size(), m = b.size();
        int l[26] = {0};
        int r[26] = {0};
        for(auto x : a)
        {
            l[x - &apos;a&apos;]++;
        }
        for(auto x : b)
        {
            r[x - &apos;a&apos;]++;
        }
        int ls = 0;
        int rs = 0;
        int ans = n + m;
        for (int i = 0; i &amp;lt; 25; ++i)
        {
            ls += l[i];
            rs += r[i];
            ans = min(ans, min(min(n + m - l[i] - r[i], n - ls + rs), m - rs + ls));
        }
        ans = min(ans, n + m - l[25] - r[25]);
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 34 1904. 你完成的完整对局数</title><link>https://chaggle.github.io/posts/2021/10/13/day-34-1904-the-number-of-full-rounds-you-have-played/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/13/day-34-1904-the-number-of-full-rounds-you-have-played/</guid><pubDate>Wed, 13 Oct 2021 04:18:50 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/the-number-of-full-rounds-you-have-played/&quot;&gt;1904. 你完成的完整对局数&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;一款新的在线电子游戏在近期发布，

在该电子游戏中，以刻钟为周期规划若干时长为 15 分钟的游戏对局。

这意味着，在 HH:00、HH:15、HH:30 和 HH:45

将会开始一个新的对局，其中 HH 用一个从 00 到 23 的整数表示。

游戏中使用 24 小时制的时钟，所以一天中最早的时间是 00:00 ，最晚的时间是 23:59 。

给你两个字符串 startTime 和 finishTime ，均符合 &quot;HH:MM&quot; 格式，

分别表示你进入和退出游戏的确切时间，请计算在整个游戏会话期间，你完成的完整对局的对局数 。

例如，如果 startTime = &quot;05:20&quot; 且 finishTime = &quot;05:59&quot;，

这意味着你仅仅完成从 05:30 到 05:45 这一个完整对局。

而你没有完成从 05:15 到 05:30 的完整对局，因为你是在对局开始后进入的游戏；

同时，你也没有完成从 05:45 到 06:00 的完整对局，因为你是在对局结束前退出的游戏。

如果 finishTime 早于 startTime ，这表示你玩了个通宵

假设你是从 startTime 进入游戏，并在 finishTime 退出游戏，

请计算并返回你完成的 完整对局的对局数 。

 

示例 1：

输入：startTime = &quot;12:01&quot;, finishTime = &quot;12:44&quot;
输出：1
解释：你完成了从 12:15 到 12:30 的一个完整对局。
你没有完成从 12:00 到 12:15 的完整对局，

因为你是在对局开始后的 12:01 进入的游戏。

你没有完成从 12:30 到 12:45 的完整对局，

因为你是在对局结束前的 12:44 退出的游戏。
示例 2：

输入：startTime = &quot;20:00&quot;, finishTime = &quot;06:00&quot;
输出：40
解释：
你完成了从 20:00 到 00:00 的 16 个完整的对局，
以及从 00:00 到 06:00 的 24 个完整的对局。
16 + 24 = 40
示例 3：

输入：startTime = &quot;00:00&quot;, finishTime = &quot;23:59&quot;
输出：95
解释：除最后一个小时你只完成了 3 个完整对局外，

其余每个小时均完成了 4 场完整对局。
 

提示：

startTime 和 finishTime 的格式为 HH:MM
00 &amp;lt;= HH &amp;lt;= 23
00 &amp;lt;= MM &amp;lt;= 59
startTime 和 finishTime 不相等
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;模拟题，把题目多读几遍就能理解。思路是将起止时间都换算成分钟数，若结束时间早于开始时间则说明跨天，需要再加上 24 小时；然后分别对齐到 15 分钟边界（开始时间向上取整、结束时间向下取整），相减即可得到完整对局数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numberOfRounds(string startTime, string finishTime)
    {
        int h1 = stoi(startTime.substr(0, 2));
        int m1 = stoi(startTime.substr(3, 2));
        int h2 = stoi(finishTime.substr(0, 2));
        int m2 = stoi(finishTime.substr(3, 2));

        int a1 = h1 * 60 + m1;
        int a2 = h2 * 60 + m2;
        if (a1 &amp;gt; a2)
            a2 += 24 * 60;

        int a = ceil(double(a1) / 15);
        int b = floor(double(a2) / 15);

        return max(0, b - a);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 33 1834. 单线程 CPU</title><link>https://chaggle.github.io/posts/2021/10/12/day-33-1834-single-threaded-cpu/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/12/day-33-1834-single-threaded-cpu/</guid><pubDate>Tue, 12 Oct 2021 08:05:18 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/single-threaded-cpu/&quot;&gt;1834. 单线程 CPU&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个二维数组 tasks ，用于表示 n 项从 0 到 n - 1 编号的任务。

其中 tasks[i] = [enqueueTimei, processingTimei]

意味着第 i 项任务将会于 enqueueTimei 时进入任务队列

需要 processingTimei 的时长完成执行。

现有一个单线程 CPU ，同一时间只能执行 最多一项 任务，该 CPU 将会按照下述方式运行：

如果 CPU 空闲，且任务队列中没有需要执行的任务，则 CPU 保持空闲状态。

如果 CPU 空闲，但任务队列中有需要执行的任务，则 CPU 将会选择 执行时间最短 的任务开始执行。

如果多个任务具有同样的最短执行时间，则选择下标最小的任务开始执行。
一旦某项任务开始执行，CPU 在 执行完整个任务 前都不会停止。
CPU 可以在完成一项任务后，立即开始执行一项新任务。
返回 CPU 处理任务的顺序。

 

示例 1：

输入：tasks = [[1,2],[2,4],[3,2],[4,1]]
输出：[0,2,3,1]
解释：事件按下述流程运行：
- time = 1 ，任务 0 进入任务队列，可执行任务项 = {0}
- 同样在 time = 1 ，空闲状态的 CPU 开始执行任务 0 ，可执行任务项 = {}
- time = 2 ，任务 1 进入任务队列，可执行任务项 = {1}
- time = 3 ，任务 2 进入任务队列，可执行任务项 = {1, 2}
- 同样在 time = 3 ，CPU 完成任务 0 并开始执行队列中用时最短的任务 2 ，可执行任务项 = {1}
- time = 4 ，任务 3 进入任务队列，可执行任务项 = {1, 3}
- time = 5 ，CPU 完成任务 2 并开始执行队列中用时最短的任务 3 ，可执行任务项 = {1}
- time = 6 ，CPU 完成任务 3 并开始执行任务 1 ，可执行任务项 = {}
- time = 10 ，CPU 完成任务 1 并进入空闲状态
示例 2：

输入：tasks = [[7,10],[7,12],[7,5],[7,4],[7,2]]
输出：[4,3,2,0,1]
解释：事件按下述流程运行：
- time = 7 ，所有任务同时进入任务队列，可执行任务项  = {0,1,2,3,4}
- 同样在 time = 7 ，空闲状态的 CPU 开始执行任务 4 ，可执行任务项 = {0,1,2,3}
- time = 9 ，CPU 完成任务 4 并开始执行任务 3 ，可执行任务项 = {0,1,2}
- time = 13 ，CPU 完成任务 3 并开始执行任务 2 ，可执行任务项 = {0,1}
- time = 18 ，CPU 完成任务 2 并开始执行任务 0 ，可执行任务项 = {1}
- time = 28 ，CPU 完成任务 0 并开始执行任务 1 ，可执行任务项 = {}
- time = 40 ，CPU 完成任务 1 并进入空闲状态
 

提示：

tasks.length == n
1 &amp;lt;= n &amp;lt;= 105
1 &amp;lt;= enqueueTimei, processingTimei &amp;lt;= 109
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;设计 + 模拟题，本质上就是模拟一个单线程 CPU 的调度过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;//task index序列号 start为进入队列时间 times为完成时间
class task {
public:
    int index;
    int start;
    int times;
    task(int index, int start, int times) : index(index), start(start), times(times) {}
    task() = default;
};

class Solution {
public:
    vector&amp;lt;int&amp;gt; getOrder(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; &amp;amp;tasks)
    {
        vector&amp;lt;int&amp;gt; ans;
        auto cmptimes = [](task a, task b)
        {
            if (a.times == b.times) return a.index &amp;gt; b.index;
            else return a.times &amp;gt; b.times;
        };

        auto cmpstart = [](task a, task b)
        {
            return a.start &amp;gt; b.start;
        };

        priority_queue&amp;lt;task, vector&amp;lt;task&amp;gt;, decltype(cmptimes)&amp;gt; qtimes(cmptimes);
        priority_queue&amp;lt;task, vector&amp;lt;task&amp;gt;, decltype(cmpstart)&amp;gt; qstart(cmpstart);
        int n = tasks.size();

        for(int i = 0; i &amp;lt; n; ++i)
        {
            qstart.push({i, tasks[i][0], tasks[i][1]});
        }

        int now = qstart.top().start;
        while(qstart.size() &amp;gt; 0)
        {

            while (qstart.size() &amp;gt; 0 &amp;amp;&amp;amp; qstart.top().start &amp;lt;= now)
            {
                qtimes.push(qstart.top());
                qstart.pop();
            }
            if (qtimes.size() &amp;gt; 0)
            {
                task x = qtimes.top();
                qtimes.pop();
                ans.push_back(x.index);
                now += x.times;
            }
            else
            {
                now = qstart.top().start;
            }
        }

        while (qtimes.size() &amp;gt; 0)
        {
            task x = qtimes.top();
            qtimes.pop();
            ans.push_back(x.index);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(nlogn)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 32 657. 机器人能否返回原点</title><link>https://chaggle.github.io/posts/2021/10/11/day-32-657-robot-return-to-origin/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/11/day-32-657-robot-return-to-origin/</guid><pubDate>Mon, 11 Oct 2021 00:54:59 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/robot-return-to-origin/&quot;&gt;657. 机器人能否返回原点&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;在二维平面上，有一个机器人从原点 (0, 0) 开始。

给出它的移动顺序，判断这个机器人在完成移动后是否在 (0, 0) 处结束。

移动顺序由字符串表示。字符 move[i] 表示其第 i 次移动。

机器人的有效动作有 R（右），L（左），U（上）和 D（下）。

如果机器人在完成所有动作后返回原点，则返回 true。否则，返回 false。

注意：机器人“面朝”的方向无关紧要。

“R” 将始终使机器人向右移动一次，“L” 将始终向左移动等。此外，假设每次移动机器人的移动幅度相同。

 

示例 1:

输入: &quot;UD&quot;
输出: true
解释：机器人向上移动一次，然后向下移动一次。

所有动作都具有相同的幅度，因此它最终回到它开始的原点。因此，我们返回 true。

示例 2:

输入: &quot;LL&quot;
输出: false
解释：机器人向左移动两次。它最终位于原点的左侧，距原点有两次 “移动” 的距离。

我们返回 false，因为它在移动结束时没有返回原点。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简单的模拟题，按照题意一步步模拟机器人的移动过程，最后判断是否回到原点即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool judgeCircle(string m) {
        if(m.size() % 2 != 0) return false;
        vector&amp;lt;int&amp;gt; s(2, 0);
        for(char ch : m)
        {
            switch (ch)
            {
                case &apos;U&apos; : s[0]++;break;
                case &apos;D&apos; : s[0]--;break;
                case &apos;L&apos; : s[1]++;break;
                case &apos;R&apos; : s[1]--;break;
            }
        }
        return s[0] == 0 &amp;amp;&amp;amp; s[1] == 0;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n)，遍历&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(1)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 31 1203. 项目管理</title><link>https://chaggle.github.io/posts/2021/10/10/day-31-1203-sort-items-by-groups-respecting-dependencies/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/10/day-31-1203-sort-items-by-groups-respecting-dependencies/</guid><pubDate>Sun, 10 Oct 2021 02:30:34 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/sort-items-by-groups-respecting-dependencies/&quot;&gt;1203. 项目管理&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;有 n 个项目，每个项目或者不属于任何小组，或者属于 m 个小组之一。

group[i] 表示第 i 个项目所属的小组，如果第 i 个项目不属于任何小组，则 group[i] 等于 -1。

项目和小组都是从零开始编号的。可能存在小组不负责任何项目，即没有任何项目属于这个小组。

请你帮忙按要求安排这些项目的进度，并返回排序后的项目列表：

同一小组的项目，排序后在列表中彼此相邻。
项目之间存在一定的依赖关系，

我们用一个列表 beforeItems 来表示，

其中 beforeItems[i] 表示在进行第 i 个项目前应该完成的所有项目。

如果存在多个解决方案，只需要返回其中任意一个即可。

如果没有合适的解决方案，就请返回一个 空列表 。

 

示例 1：


输入：

n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1],
beforeItems = [[],[6],[5],[6],[3,6],[],[],[]]

输出：[6,3,4,1,5,2,0,7]
示例 2：

输入：
n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1],
beforeItems = [[],[6],[5],[6],[3],[],[4],[]]

输出：[]
解释：与示例 1 大致相同，但是在排序后的列表中，4 必须放在 6 的前面。
 

提示：

1 &amp;lt;= m &amp;lt;= n &amp;lt;= 3 * 104
group.length == beforeItems.length == n
-1 &amp;lt;= group[i] &amp;lt;= m - 1
0 &amp;lt;= beforeItems[i].length &amp;lt;= n - 1
0 &amp;lt;= beforeItems[i][j] &amp;lt;= n - 1
i != beforeItems[i][j]
beforeItems[i] 不含重复元素
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;拓扑排序类的问题，今天的题目比较难，只做一次拓扑排序可能不够，所以尝试使用双拓扑排序的思路。&lt;/li&gt;
&lt;li&gt;这几天身体不舒服，只能先 CV 一下，过几天再回来补一篇解析。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; sortItems(
        int n, int m,
        vector&amp;lt;int&amp;gt;&amp;amp; group,
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; beforeItems)
        {
        vector&amp;lt;int&amp;gt; ans, blank;
        //如果某个项目不属于当前任何小组，
        //新建一个小组，并且把它分给这个小组，为了避免冲突，新的小组编号从m开始
        for(int i = 0; i &amp;lt; group.size(); i++)
        if(group[i] == -1)
        {
            group[i] = m++;
        }

        //分别保存组间依赖关系、项目之间的依赖关系、各个小组负责的项目
        vector&amp;lt;int&amp;gt; topoGroup[m], topoItem[n], groupItem[m];
        //分别保存小组和项目在图中的入度
        int groupDgr[m], itemDgr[n];
        //避免添加多余的有向边，依赖关系只需要一条有向边就可以表示
        unordered_set&amp;lt;int&amp;gt; vis;
        memset(groupDgr, 0, sizeof groupDgr);
        memset(itemDgr, 0 ,sizeof itemDgr);
        for(int i = 0; i &amp;lt; beforeItems.size(); i++)
        {
            //将项目添加到对应的组内
            groupItem[group[i]].push_back(i);
            for(int j = 0; j &amp;lt; beforeItems[i].size(); j++)
            {
                //hashval用于判断是否有重复的有向边出现
                int u = beforeItems[i][j], hashval = group[u] * m + group[i];
                topoItem[u].push_back(i);
                itemDgr[i]++;
                //自己不能依赖自己
                if(group[i] == group[u] || vis.find(hashval) != vis.end()) continue;
                vis.insert(hashval);
                topoGroup[group[u]].push_back(group[i]);
                groupDgr[group[i]]++;
            }
        }

        //groupOrder保存了组的顺序
        vector&amp;lt;int&amp;gt; groupOrder;
        queue&amp;lt;int&amp;gt; q;
        for(int i = 0; i &amp;lt; m; i++)
        if(groupDgr[i] == 0) q.push(i);

        while(!q.empty())
        {
            int u = q.front();
            q.pop();
            groupOrder.push_back(u);
            for(int i = 0; i &amp;lt; topoGroup[u].size(); i++)
            {
                int v = topoGroup[u][i];
                groupDgr[v]--;
                if(groupDgr[v] == 0) q.push(v);
            }
        }

        //判断组内项目是否满足拓扑排序
        for(int i = 0; i &amp;lt; groupOrder.size(); i++)
        {
            int t = groupOrder[i];
            for(int j = 0; j &amp;lt; groupItem[t].size(); j++)
            {
                int u = groupItem[t][j];
                if(itemDgr[u] == 0) q.push(u);
            }

            int cnt = 0;
            while(!q.empty())
            {
                int u = q.front();
                q.pop();
                cnt++;
                ans.push_back(u);
                for(int j = 0; j &amp;lt; topoItem[u].size(); j++)
                {
                    int v = topoItem[u][j];
                    itemDgr[v]--;
                    if(itemDgr[v] == 0 &amp;amp;&amp;amp; group[v] == t) q.push(v);
                }
            }
            if(cnt != groupItem[t].size()) return blank;
        }

        //如果组间关系不能满足拓扑排序，必定有项目不能加入ans内
        if(ans.size() != n) return blank;

        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：O(n + m)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：O(n + m)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 30 886. 可能的二分法</title><link>https://chaggle.github.io/posts/2021/10/09/day-30-886-possible-bipartition/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/09/day-30-886-possible-bipartition/</guid><pubDate>Sat, 09 Oct 2021 07:36:22 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/possible-bipartition/&quot;&gt;886. 可能的二分法&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一组 N 人（编号为 1, 2, ..., N）， 我们想把每个人分进任意大小的两组。

每个人都可能不喜欢其他人，那么他们不应该属于同一组。

形式上，如果 dislikes[i] = [a, b]，表示不允许将编号为 a 和 b 的人归入同一组。

当可以用这种方法将所有人分进两组时，返回 true；否则返回 false。

 

示例 1：

输入：N = 4, dislikes = [[1,2],[1,3],[2,4]]
输出：true
解释：group1 [1,4], group2 [2,3]
示例 2：

输入：N = 3, dislikes = [[1,2],[1,3],[2,3]]
输出：false
示例 3：

输入：N = 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]]
输出：false
 

提示：

1 &amp;lt;= N &amp;lt;= 2000
0 &amp;lt;= dislikes.length &amp;lt;= 10000
dislikes[i].length == 2
1 &amp;lt;= dislikes[i][j] &amp;lt;= N
dislikes[i][0] &amp;lt; dislikes[i][1]
对于 dislikes[i] == dislikes[j] 不存在 i != j
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;经典并查集题目。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class UnionFound {
public:
    vector&amp;lt;int&amp;gt; F;

    UnionFound(int n)
    {
        F = vector&amp;lt;int&amp;gt;(n, 0);
        for (int i = 0; i &amp;lt; n; i++)
        {
            F[i] = i;
        }
    }

    int Find(int x)
    {
        if (x == F[x]) return x;
        return F[x] = Find(F[x]);
    }

    void Union(int x, int y)
    {
        x = Find(x);
        y = Find(y);

        if (x != y) F[x] = y;
    }
};

class Solution {
public:
    bool possibleBipartition(int n, vector&amp;lt;vector&amp;lt;int&amp;gt; &amp;gt; &amp;amp;dislikes)
    {
        unordered_map&amp;lt;int, vector&amp;lt;int&amp;gt; &amp;gt; mp;

        for (int i = 0; i &amp;lt; dislikes.size(); i++) {
            mp[dislikes[i][0] - 1].push_back(dislikes[i][1] - 1);
            mp[dislikes[i][1] - 1].push_back(dislikes[i][0] - 1);
        }

        UnionFound uf(n);
        for (int i = 0; i &amp;lt; n; i++) {
            auto vec = mp[i];

            for (auto c : vec) {
                if (uf.Find(i) == uf.Find(c)) {
                    return false;
                }
                uf.Union(vec[0], c);
            }
        }
        return true;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>16. 最接近的三数之和</title><link>https://chaggle.github.io/posts/2021/10/08/16-3sum-closest/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/08/16-3sum-closest/</guid><pubDate>Fri, 08 Oct 2021 07:47:55 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/3sum-closest/&quot;&gt;16. 最接近的三数之和&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个长度为 n 的整数数组 nums 和 一个目标值 target。

请你从 nums 中选出三个整数，使它们的和与 target 最接近。

返回这三个数的和。

假定每组输入只存在恰好一个解。

示例 1：

输入：nums = [-1,2,1,-4], target = 1
输出：2
解释：与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
示例 2：

输入：nums = [0,0,0], target = 1
输出：0

提示：

3 &amp;lt;= nums.length &amp;lt;= 1000
-1000 &amp;lt;= nums[i] &amp;lt;= 1000
-104 &amp;lt;= target &amp;lt;= 104
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本题与两数之和、三数之和一样，都是双指针解法。首先对数组进行排序，然后确定第一个点，之后开始用双指针遍历数组前后的每一个值。由于是找最接近的数，所以如果三数之和 sum 等于 target，直接返回该值即可；如果 sum 大于目标值，则让右端点 r--，反之让左端点 l++。&lt;/p&gt;
&lt;p&gt;另外，如果在 i &amp;gt; 0 的情况下 nums[i] 与 nums[i-1] 的值相等，那么之后遍历得到的三数之和情况也相等，所以在只需要返回唯一合适值的情况下，可以直接使用 continue 跳过本轮循环进行优化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func threeSumClosest(nums []int, target int) int {
    n := len(nums)

    if n == 3 {
        return nums[0] + nums[1] + nums[2]
    }

    sort.Ints(nums)
    min := math.MaxInt32

    update := func (sum int) {
        if abs(min - target) &amp;gt; abs(sum - target) {
            min = sum
        }
    }

    for i := 0; i &amp;lt; n - 2; i++ {

        if i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i - 1] {
            continue
        }

        l, r := i + 1, n - 1

        for l &amp;lt; r {
            sum := nums[i] + nums[l] + nums[r]

            update(sum)

            if sum == target {
                return sum
            } else if sum &amp;gt; target {
                r--
            } else if sum &amp;lt; target {
                l++
            }
        }
    }

    return min
}


func abs(x int) int {
    if x &amp;lt; 0 {
        x = -x
    }

    return x
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n^2)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(nlogn)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>187. 重复的 DNA 序列</title><link>https://chaggle.github.io/posts/2021/10/08/187-repeated-dna-sequences/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/08/187-repeated-dna-sequences/</guid><pubDate>Fri, 08 Oct 2021 07:47:55 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/repeated-dna-sequences/&quot;&gt;187. 重复的 DNA 序列&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;所有 DNA 都由一系列缩写为 &apos;A&apos;，&apos;C&apos;，&apos;G&apos; 和 &apos;T&apos; 的核苷酸组成，例如：&quot;ACGAATTCCG&quot;。在研究 DNA 时，识别 DNA 中的重复序列有时会对研究非常有帮助。

编写一个函数来找出所有目标子串，目标子串的长度为 10，且在 DNA 字符串 s 中出现次数超过一次。

示例 1：

输入：s = &quot;AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT&quot;
输出：[&quot;AAAAACCCCC&quot;,&quot;CCCCCAAAAA&quot;]
示例 2：

输入：s = &quot;AAAAAAAAAAAAA&quot;
输出：[&quot;AAAAAAAAAA&quot;]

提示：

0 &amp;lt;= s.length &amp;lt;= 105
s[i] 为 &apos;A&apos;、&apos;C&apos;、&apos;G&apos; 或 &apos;T&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;使用滑动窗口，窗口长度为 10，用哈希表统计每个长度为 10 的子串出现的次数，出现次数超过一次的子串即为答案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;string&amp;gt; findRepeatedDnaSequences(string s) {
        vector&amp;lt;string&amp;gt; ans;
        int n = s.size();
        unordered_map&amp;lt;string, int&amp;gt; st;
        for(int i = 0, j = 9; j &amp;lt; n; j++, i++)
        {
            if(st[s.substr(i,10)] == 1)
            {
                ans.push_back(s.substr(i, 10));
            }
            st[s.substr(i, 10)]++;
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n*C)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 29 997. 找到小镇的法官</title><link>https://chaggle.github.io/posts/2021/10/08/day-29-997-find-the-town-judge/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/08/day-29-997-find-the-town-judge/</guid><pubDate>Fri, 08 Oct 2021 07:47:22 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/find-the-town-judge/&quot;&gt;997. 找到小镇的法官&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;在一个小镇里，按从 1 到 n 为 n 个人进行编号。传言称，这些人中有一个是小镇上的秘密法官。

如果小镇的法官真的存在，那么：

小镇的法官不相信任何人。
每个人（除了小镇法官外）都信任小镇的法官。
只有一个人同时满足条件 1 和条件 2 。
给定数组 trust，该数组由信任对 trust[i] = [a, b] 组成，表示编号为 a 的人信任编号为 b 的人。

如果小镇存在秘密法官并且可以确定他的身份，请返回该法官的编号。否则，返回 -1。

 

示例 1：

输入：n = 2, trust = [[1,2]]
输出：2
示例 2：

输入：n = 3, trust = [[1,3],[2,3]]
输出：3
示例 3：

输入：n = 3, trust = [[1,3],[2,3],[3,1]]
输出：-1
示例 4：

输入：n = 3, trust = [[1,2],[2,3]]
输出：-1
示例 5：

输入：n = 4, trust = [[1,3],[1,4],[2,3],[2,4],[4,3]]
输出：3
 

提示：

1 &amp;lt;= n &amp;lt;= 1000
0 &amp;lt;= trust.length &amp;lt;= 10^4
trust[i].length == 2
trust[i] 互不相同
trust[i][0] != trust[i][1]
1 &amp;lt;= trust[i][0], trust[i][1] &amp;lt;= n
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简单图论类问题：其他人都信任法官，即其他人在图中都指向法官，而法官不指向任何人，所以法官的出度为 0、入度为 n - 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int findJudge(int n, vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; trust) {
        vector&amp;lt;int&amp;gt; in(n + 1);
        vector&amp;lt;int&amp;gt; out(n + 1);
        int len = trust.size();
        for(int i = 0; i &amp;lt; len; i++)
        {
            in[trust[i][1]]++; //入度
            out[trust[i][0]]++; //出度
        }

        for(int i = 1; i &amp;lt;= n; i++)
        {
            if(in[i] == n - 1 &amp;amp;&amp;amp; out[i] == 0) return i;
        }

        return -1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>434. 字符串中的单词数</title><link>https://chaggle.github.io/posts/2021/10/07/434-number-of-segments-in-a-string/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/07/434-number-of-segments-in-a-string/</guid><pubDate>Thu, 07 Oct 2021 08:26:31 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/number-of-segments-in-a-string/&quot;&gt;434. 字符串中的单词数&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;统计字符串中的单词个数，这里的单词指的是连续的不是空格的字符。

请注意，你可以假定字符串里不包括任何不可打印的字符。

示例:

输入: &quot;Hello, my name is John&quot;
输出: 5
解释: 这里的单词是指连续的不是空格的字符，所以 &quot;Hello,&quot; 算作 1 个单词。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;简单题目。字符前不是空格、后为空格，即可视为 1 个单词。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int countSegments(string s) {
        int n = s.size();
        if(n == 0) return 0;
        int ans = 0;
        for(int i = 0; i &amp;lt; s.size(); i++)
        {
            if(s[i] != &apos; &apos; &amp;amp;&amp;amp; s[i + 1] == &apos; &apos;)
            {
                ans++;
            }
        }
        return s[n - 1]==&apos; &apos; ? ans : ans + 1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 28 239. 滑动窗口最大值</title><link>https://chaggle.github.io/posts/2021/10/07/day-28-239-sliding-window-maximum/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/07/day-28-239-sliding-window-maximum/</guid><pubDate>Thu, 07 Oct 2021 07:18:45 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/sliding-window-maximum/&quot;&gt;239. 滑动窗口最大值&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个整数数组 nums，有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。

你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

 

示例 1：

输入：nums = [1,3,-1,-3,5,3,6,7], k = 3
输出：[3,3,5,5,6,7]
解释：
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
示例 2：

输入：nums = [1], k = 1
输出：[1]
示例 3：

输入：nums = [1,-1], k = 1
输出：[1,-1]
示例 4：

输入：nums = [9,11], k = 2
输出：[11]
示例 5：

输入：nums = [4,-2], k = 2
输出：[4]
 

提示：

1 &amp;lt;= nums.length &amp;lt;= 10^5
-10^4 &amp;lt;= nums[i] &amp;lt;= 10^4
1 &amp;lt;= k &amp;lt;= nums.length
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;滑动窗口题目，最多算个中等题。可以用双端队列解决，也可以维护一个 size = k 的最大堆；其中双端队列的时间复杂度为 O(n)，而最大堆为 O(nlogn)；&lt;/li&gt;
&lt;li&gt;双端队列中维护的是数组 nums 中的下标，便于判断元素是否已经滑出窗口。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; maxSlidingWindow(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) {
        vector&amp;lt;int&amp;gt; ans;
        deque&amp;lt;int&amp;gt; q;
        int n = nums.size();
        if(n == 0) return nums;
        for(int i = 0; i &amp;lt; n; i++)
        {
            //如果队头的元素值满足 i - k 的长度时候，即窗口需要向后滑动，将队头的元素踢除！
            if(!q.empty() &amp;amp;&amp;amp; q.front() == i - k) q.pop_front();
            //保证从大到小，如果前面数小则需要依次剔除，直至满足要求
            while(!q.empty() &amp;amp;&amp;amp; nums[i] &amp;gt; nums[q.back()]) q.pop_back();
            q.push_back(i);
            if(i &amp;gt;= k - 1) ans.push_back(nums[q.front()]);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(k)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>GCC 与 GDB 的学习</title><link>https://chaggle.github.io/posts/2021/10/06/gcc-gdb-learning/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/06/gcc-gdb-learning/</guid><pubDate>Wed, 06 Oct 2021 14:56:50 GMT</pubDate><content:encoded>&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;gcc 是 GNU Compiler Collection 的缩写，支持多种语言的编译，比如 C、C++、Java、Pascal 等。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;GCC 的编译过程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;预处理（pre-processing）E：插入头文件，替换宏&lt;/li&gt;
&lt;li&gt;编译（Compiling）S：编译成汇编&lt;/li&gt;
&lt;li&gt;汇编（Assembling）c：编译成目标文件&lt;/li&gt;
&lt;li&gt;链接（Linking）：链接到库中，变成可执行文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;gcc -E hello.c -o hello.i

gcc -S hello.i -o hello.s

gcc -c hello.s -o hello.o

gcc hello.s -o hello 链接，生成可执行文件

./hello 运行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以一次性完成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc hello.c -o hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但一般情况下生成 .o 文件比较好，目标文件可以重定位，方便让别人使用。&lt;/p&gt;
&lt;h2&gt;GCC 常用选项&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-c&lt;/td&gt;
&lt;td&gt;编译或汇编源文件，但不链接，生成目标文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-o&lt;/td&gt;
&lt;td&gt;指定输出文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-E&lt;/td&gt;
&lt;td&gt;只运行 C 预编译器（头文件、宏等展开）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-S&lt;/td&gt;
&lt;td&gt;生成汇编语言文件后停止编译（.s 文件）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-Wall&lt;/td&gt;
&lt;td&gt;打开编译告警（所有）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-g&lt;/td&gt;
&lt;td&gt;嵌入调试信息，方便 gdb 调试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-llib&lt;/td&gt;
&lt;td&gt;链接 lib 库（这里是小写 L），相当于 C++ 的 #pragma comment(lib, &quot;xxx.lib&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-Idir&lt;/td&gt;
&lt;td&gt;增加 include 目录（这里是大写 i），指定头文件路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-LDir&lt;/td&gt;
&lt;td&gt;增加 lib 目录（编译静态库和动态库）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;GDB 调试实例&lt;/h2&gt;
&lt;p&gt;下面以一个带有错误的例子程序来介绍 gdb 的使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*bugging.c*/
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

static char buff [256];
static char* string;
int main ()
{
    printf (&quot;Please input a string: &quot;);
    gets (string);
    printf (&quot;\nYour string is: %s\n&quot;, string);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的程序接受用户的输入，然后将用户的输入打印出来。由于使用了未经过初始化的字符串指针 string，编译并运行之后，将出现 &quot;Segmentation Fault&quot; 错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ gcc -o bugging -g  bugging.c
$ ./bugging

# Please input a string: asdf
# Segmentation fault (core dumped)
# 为了查找该程序中出现的问题，我们利用 gdb，并按如下的步骤进行：

# [1] 运行 &quot;gdb bugging&quot; ，加载 bugging 可执行文件；
$ gdb bugging

# [2] 执行装入的 bugging 命令；
(gdb) run

# [3] 使用 where 命令查看程序出错的地方；
(gdb) where

# [4] 利用 list 命令查看调用 gets 函数附近的代码；
(gdb) list

# [5] 在 gdb 中，我们在第 11 行处设置断点，看看是否是在第 11 行出错；
(gdb) break 11

# [6] 程序重新运行到第 11 行处停止，这时程序正常，然后执行单步命令 next；
(gdb) next

# [7] 程序确实出错，能够导致 gets 函数出错的因素就是变量 string。重新执行测试程序，用 print 命令查看 string 的值；
(gdb) run
(gdb) print string
(gdb) $1=0x0

# [8] 问题在于 string 指向的是一个无效指针。修改程序，在第 10 行和第 11 行之间增加一条语句 &quot;string=buff;&quot;，重新编译程序，然后继续运行，将看到正确的程序运行结果。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 gdb 查看源代码可以用 list 命令，但这样不够灵活。可以使用 &quot;layout src&quot; 命令，或者按 Ctrl-X 再按 A，就会出现一个窗口来查看源代码；也可以使用 -tui 参数，这样进入 gdb 后就能直接打开代码查看窗口。其他代码窗口相关命令：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;info win&lt;/td&gt;
&lt;td&gt;显示窗口的大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;layout next&lt;/td&gt;
&lt;td&gt;切换到下一个布局模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;layout prev&lt;/td&gt;
&lt;td&gt;切换到上一个布局模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;layout src&lt;/td&gt;
&lt;td&gt;只显示源代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;layout asm&lt;/td&gt;
&lt;td&gt;只显示汇编代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;layout split&lt;/td&gt;
&lt;td&gt;显示源代码和汇编代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;layout regs&lt;/td&gt;
&lt;td&gt;增加寄存器内容显示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;focus cmd/src/asm/regs/next/prev&lt;/td&gt;
&lt;td&gt;切换当前窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;refresh&lt;/td&gt;
&lt;td&gt;刷新所有窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tui reg next&lt;/td&gt;
&lt;td&gt;显示下一组寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tui reg system&lt;/td&gt;
&lt;td&gt;显示系统寄存器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;update&lt;/td&gt;
&lt;td&gt;更新源代码窗口和当前执行点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;winheight name +/- line&lt;/td&gt;
&lt;td&gt;调整 name 窗口的高度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tabset nchar&lt;/td&gt;
&lt;td&gt;设置 tab 为 nchar 个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>414. 第三大的数</title><link>https://chaggle.github.io/posts/2021/10/06/414-third-maximum-number/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/06/414-third-maximum-number/</guid><pubDate>Wed, 06 Oct 2021 08:08:55 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/third-maximum-number/&quot;&gt;414. 第三大的数&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个非空数组，返回此数组中 第三大的数 。如果不存在，则返回数组中最大的数。

示例 1：

输入：[3, 2, 1]
输出：1
解释：第三大的数是 1 。
示例 2：

输入：[1, 2]
输出：2
解释：第三大的数不存在, 所以返回最大的数 2 。
示例 3：

输入：[2, 2, 3, 1]
输出：1
解释：注意，要求返回第三大的数，是指在所有不同数字中排第三大的数。
此例中存在两个值为 2 的数，它们都排第二。在所有不同数字中排第三大的数为 1 。

提示：

1 &amp;lt;= nums.length &amp;lt;= 104
-231 &amp;lt;= nums[i] &amp;lt;= 231 - 1

进阶：你能设计一个时间复杂度 O(n) 的解决方案吗？
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;用 set 存储唯一值，然后维护一个最多为三个元素的 set 容器即可。虽然时间会慢一点，但理解起来相当简单；当然也能手写一个最大堆。由于时间关系，等考研结束后（明年 1 月 1 日开搞）再回来更新这部分内容！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int thirdMax(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        set&amp;lt;int&amp;gt; st;
        for(auto i : nums)
        {
            st.insert (i);
            if(st.size() &amp;gt; 3) st.erase(st.begin());
        }
        if(st.size() == 3) return *st.begin();
        return *st.rbegin();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(nlogn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 27 35. 搜索插入位置</title><link>https://chaggle.github.io/posts/2021/10/06/day-27-35-search-insert-position/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/06/day-27-35-search-insert-position/</guid><pubDate>Wed, 06 Oct 2021 07:30:50 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/search-insert-position/&quot;&gt;35. 搜索插入位置&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个排序数组和一个目标值，在数组中找到目标值，并返回其索引。

如果目标值不存在于数组中，返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

 

示例 1:

输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:

输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:

输入: nums = [1,3,5,6], target = 7
输出: 4
示例 4:

输入: nums = [1,3,5,6], target = 0
输出: 0
示例 5:

输入: nums = [1], target = 0
输出: 0
 

提示:

1 &amp;lt;= nums.length &amp;lt;= 10^4
-10^4 &amp;lt;= nums[i] &amp;lt;= 10^4
nums 为无重复元素的升序排列数组
-10^4 &amp;lt;= target &amp;lt;= 10^4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一道需要修改二分法模板的题目。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int searchInsert(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        int n = nums.size();
        int l = 0, r = n - 1;
        int mid = 0;
        while(l &amp;lt;= r)
        {
            mid = l + (r - l) / 2;
            if(nums[mid] == target) return mid;
            else if(nums[mid] &amp;gt; target) r = mid - 1;
            else l = mid + 1;
        }
        return l == mid + 1 ? l : mid;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(logn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(1)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>284. 顶端迭代器</title><link>https://chaggle.github.io/posts/2021/10/05/284-peeking-iterator/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/05/284-peeking-iterator/</guid><pubDate>Tue, 05 Oct 2021 08:23:28 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/peeking-iterator/&quot;&gt;284. 顶端迭代器&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;请你设计一个迭代器，除了支持 hasNext 和 next 操作外，还支持 peek 操作。

实现 PeekingIterator 类：

PeekingIterator(int[] nums) 使用指定整数数组 nums 初始化迭代器。
int next() 返回数组中的下一个元素，并将指针移动到下个元素处。
bool hasNext() 如果数组中存在下一个元素，返回 true ；否则，返回 false 。
int peek() 返回数组中的下一个元素，但 不 移动指针。

示例：

输入：
[&quot;PeekingIterator&quot;, &quot;next&quot;, &quot;peek&quot;, &quot;next&quot;, &quot;next&quot;, &quot;hasNext&quot;]
[[[1, 2, 3]], [], [], [], [], []]
输出：
[null, 1, 2, 2, 3, false]

解释：
PeekingIterator peekingIterator = new PeekingIterator([1, 2, 3]); // [1,2,3]
peekingIterator.next();    // 返回 1 ，指针移动到下一个元素 [1,2,3]
peekingIterator.peek();    // 返回 2 ，指针未发生移动 [1,2,3]
peekingIterator.next();    // 返回 2 ，指针移动到下一个元素 [1,2,3]
peekingIterator.next();    // 返回 3 ，指针移动到下一个元素 [1,2,3]
peekingIterator.hasNext(); // 返回 False

提示：

1 &amp;lt;= nums.length &amp;lt;= 1000
1 &amp;lt;= nums[i] &amp;lt;= 1000
对 next 和 peek 的调用均有效
next、hasNext 和 peek 最多调用  1000 次
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;简单的迭代器设计题，主要考察对迭代器的理解。常规迭代器的「访问」只支持两种操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hasNext() 操作：如果存在下一元素，返回 true，否则返回 false。实现上，就是判断游标是否到达结尾位置；&lt;/li&gt;
&lt;li&gt;next() 操作：返回下一元素（当不存在下一元素时，返回 null）。实现上，就是返回游标指向的元素，并让游标后移。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/*
 * Below is the interface for Iterator, which is already defined for you.
 * **DO NOT** modify the interface for Iterator.
 *
 *  class Iterator {
 *  struct Data;
 *   Data* data;
 *  public:
 *  Iterator(const vector&amp;lt;int&amp;gt;&amp;amp; nums);
 *   Iterator(const Iterator&amp;amp; iter);
 *
 *   // Returns the next element in the iteration.
 *  int next();
 *
 *   // Returns true if the iteration has more elements.
 *  bool hasNext() const;
 * };
 */

class PeekingIterator : public Iterator {
private:
    int _next;
    bool _hasNext;

public:
 PeekingIterator(const vector&amp;lt;int&amp;gt;&amp;amp; nums) : Iterator(nums) {
     // Initialize any member here.
     // **DO NOT** save a copy of nums and manipulate it directly.
     // You should only use the Iterator interface methods.
     _hasNext = Iterator::hasNext();
        if(_hasNext) _next = Iterator::next();
 }

    // Returns the next element in the iteration without advancing the iterator.
 int peek() {
        return _next;
 }

 // hasNext() and next() should behave the same as in the Iterator interface.
 // Override them if needed.
 int next() {
     int dummy = _next;
        _hasNext = Iterator::hasNext();
        if(_hasNext) _next = Iterator::next();
        return dummy;
 }

 bool hasNext() const {
     return _hasNext;
 }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 26 26. 删除有序数组中的重复项</title><link>https://chaggle.github.io/posts/2021/10/05/day-26-26-remove-duplicates-from-sorted-array/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/05/day-26-26-remove-duplicates-from-sorted-array/</guid><pubDate>Tue, 05 Oct 2021 06:49:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/&quot;&gt;26. 删除有序数组中的重复项&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个有序数组 nums ，请你原地删除重复出现的元素，

使每个元素只出现一次，返回删除后数组的新长度。

不要使用额外的数组空间，你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

 

说明:

为什么返回数值是整数，但输出的答案是数组呢?

请注意，输入数组是以「引用」方式传递的，这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以&quot;引用&quot;方式传递的。也就是说，不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i &amp;lt; len; i++) {
    print(nums[i]);
}
 
示例 1：

输入：nums = [1,1,2]
输出：2, nums = [1,2]
解释：函数应该返回新的长度 2,

并且原数组 nums 的前两个元素被修改为 1, 2 。

不需要考虑数组中超出新长度后面的元素。

示例 2：

输入：nums = [0,0,1,1,1,2,2,3,3,4]
输出：5, nums = [0,1,2,3,4]
解释：函数应该返回新的长度 5,

并且原数组 nums 的前五个元素被修改为 0, 1,

2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
 

提示：

0 &amp;lt;= nums.length &amp;lt;= 3 * 10^4
-10^4 &amp;lt;= nums[i] &amp;lt;= 10^4
nums 已按升序排列
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简单题目，只需要使用双指针的解法即可：遇到与 nums[l] 不相等的值，就将 nums[r] 的值赋给 nums[l + 1]。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int removeDuplicates(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        if(n == 0) return 0;
        int l = 0, r = 1;
        while(r &amp;lt; n)
        {
            if(nums[r] != nums[l])
            {
                nums[++l] = nums[r];
                r++;
            }
            else r++;
        }
        return l + 1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(1)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>482. 密钥格式化</title><link>https://chaggle.github.io/posts/2021/10/04/482-license-key-formatting/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/04/482-license-key-formatting/</guid><pubDate>Mon, 04 Oct 2021 08:43:00 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/license-key-formatting/&quot;&gt;482. 密钥格式化&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;有一个密钥字符串 S ，只包含字母，数字以及 &apos;-&apos;（破折号）。其中， N 个 &apos;-&apos; 将字符串分成了 N+1 组。

给你一个数字 K，请你重新格式化字符串，使每个分组恰好包含 K 个字符。特别地，第一个分组包含的字符个数必须小于等于 K，但至少要包含 1 个字符。两个分组之间需要用 &apos;-&apos;（破折号）隔开，并且将所有的小写字母转换为大写字母。

给定非空字符串 S 和数字 K，按照上面描述的规则进行格式化。

示例 1：

输入：S = &quot;5F3Z-2e-9-w&quot;, K = 4
输出：&quot;5F3Z-2E9W&quot;
解释：字符串 S 被分成了两个部分，每部分 4 个字符；
    注意，两个额外的破折号需要删掉。
示例 2：

输入：S = &quot;2-5g-3-J&quot;, K = 2
输出：&quot;2-5G-3J&quot;
解释：字符串 S 被分成了 3 个部分，按照前面的规则描述，第一部分的字符可以少于给定的数量，其余部分皆为 2 个字符。

提示:

S 的长度可能很长，请按需分配大小。K 为正整数。
S 只包含字母数字（a-z，A-Z，0-9）以及破折号&apos;-&apos;
S 非空
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;简单题目，但是细节处理要处理好，也耽误了自己接近半小时的时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    string licenseKeyFormatting(string s, int k) {
        string res = &quot;&quot;;

        for(auto &amp;amp; i : s)
        {
            if(i == &apos;-&apos;) continue;
            else if(i &amp;gt;= &apos;a&apos; &amp;amp;&amp;amp; i &amp;lt;= &apos;z&apos;)  res += i - &apos;a&apos; + &apos;A&apos;;
            else res += i;
        }

        if(res.size() == 0) return &quot;&quot;;

        string ans = &quot;&quot;;
        int n = res.size();
        int per = n / k;    //能分均分几组
        int begin = n % k;  //第一组有多少个

        if(begin == 0)
        {
            for(int i = 0; i &amp;lt; n; i = i + k)
            {
                ans += res.substr(i, k);
                ans += &apos;-&apos;;
            }
            ans.pop_back();
        }
        else
        {
            ans += res.substr(0, begin);
            ans += &apos;-&apos;;
            for(int i = begin; i &amp;lt; n; i = i + k)
            {
                ans += res.substr(i, k);
                ans += &apos;-&apos;;
            }
            ans.pop_back();
        }
        return ans;
    }
       /*  for(auto it = s.begin(); it != s.end(); it++)
        {
            if(*it == &apos;-&apos;)
            {
                s.erase(it);
                it--;
            }
            else if(islower(*it)) //判断是否为小写字母
            {
                *it = toupper(*it);//将小写字母转换为大写字母
            }
        }

        int n = s.size();
        int begin = n % k;
        string ans = s.substr(0, begin);

        for(int i = begin; i &amp;lt; n; i += k)
        {
            if(i != 0) ans += &quot;-&quot;;
            ans += s.substr(i, k);
        }
        return ans;
    } */
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 25 876. 链表的中间结点</title><link>https://chaggle.github.io/posts/2021/10/04/day-25-876-middle-of-the-linked-list/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/04/day-25-876-middle-of-the-linked-list/</guid><pubDate>Mon, 04 Oct 2021 07:29:39 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/middle-of-the-linked-list/&quot;&gt;876. 链表的中间结点&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个头结点为 head 的非空单链表，返回链表的中间结点。

如果有两个中间结点，则返回第二个中间结点。

示例 1：

输入：[1,2,3,4,5]
输出：此列表中的结点 3 (序列化形式：[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意，我们返回了一个 ListNode 类型的对象 ans，这样：
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

示例 2：

输入：[1,2,3,4,5,6]
输出：此列表中的结点 4 (序列化形式：[4,5,6])
由于该列表有两个中间结点，值分别为 3 和 4，我们返回第二个结点。

提示：

给定链表的结点数介于 1 和 100 之间。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简单题目，开始的思路是求出链表长度：奇数折半，偶数折半减一；优化后直接使用快慢指针即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        /* if(head -&amp;gt; next == nullptr) return head;
        int len = 1;
        ListNode* fast = head;
        while(fast)
        {
            fast = fast -&amp;gt; next;
            len++;
        }
        if(len % 2 != 0) len /= 2;
        else len = len / 2 - 1;
        ListNode* slow = head;
        while(len)
        {
            slow = slow -&amp;gt; next;
            len--;
        }
        return slow; */
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast != nullptr &amp;amp;&amp;amp; fast -&amp;gt; next != nullptr)
        {
            slow = slow -&amp;gt; next;
            fast = fast -&amp;gt; next -&amp;gt; next;
        }
        return slow;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(1)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 24 924. 删除子列表以使总和可被 K 整除</title><link>https://chaggle.github.io/posts/2021/10/03/day-24-924-delete-sublist-to-make-sum-divisible-by-k/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/03/day-24-924-delete-sublist-to-make-sum-divisible-by-k/</guid><pubDate>Sun, 03 Oct 2021 08:23:38 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://binarysearch.com/problems/Delete-Sublist-to-Make-Sum-Divisible-By-K&quot;&gt;924. 删除子列表以使总和可被 K 整除&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;You are given a list of positive integers nums

and a positive integer k.

Return the length of the shortest sublist (can be empty sublist )

you can delete such that the resulting list&apos;s sum is divisible by k.

 You cannot delete the entire list.

If it&apos;s not possible, return -1.

Constraints

1 ≤ n ≤ 100,000 where n is the length of nums
Example 1
Input
nums = [1, 8, 6, 4, 5]
k = 7
Output
2
Explanation
We can remove the sublist [6, 4] to get [1, 8, 5]

which sums to 14 and is divisible by 7.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题用到数学中的同余定理以及前缀和的思路。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int solve(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) {
        int sum = 0;
        for(auto i : nums)
        {
            sum += i;
        }
        sum = sum % k;
        map&amp;lt;int, int&amp;gt; up;
        up[0] = -1;
        int pre = 0, n = nums.size();
        int ans = nums.size();
        for (int i = 0; i &amp;lt; n; i++)
        {
            pre += nums[i];
            int re = pre % k;
            up[re] = i;
            int x = pre - sum;
            int m = ((x % k) + k) % k;
            if (up.count(m)) ans = min(ans, i - up[m]);
        }
        return ans == nums.size() ? -1 : ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 23 30. 串联所有单词的子串</title><link>https://chaggle.github.io/posts/2021/10/02/day-23-30-substring-with-concatenation-of-all-words/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/02/day-23-30-substring-with-concatenation-of-all-words/</guid><pubDate>Sat, 02 Oct 2021 05:49:39 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words/&quot;&gt;30. 串联所有单词的子串&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个字符串 s 和一些 长度相同 的单词 words 。

找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。

注意子串要与 words 中的单词完全匹配，

中间不能有其他字符 ，但不需要考虑 words 中单词串联的顺序。

 

示例 1：

输入：s = &quot;barfoothefoobarman&quot;, words = [&quot;foo&quot;,&quot;bar&quot;]
输出：[0,9]
解释：
从索引 0 和 9 开始的子串分别是 &quot;barfoo&quot; 和 &quot;foobar&quot; 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2：

输入：s = &quot;wordgoodgoodgoodbestword&quot;, words = [&quot;word&quot;,&quot;good&quot;,&quot;best&quot;,&quot;word&quot;]
输出：[]
示例 3：

输入：s = &quot;barfoofoobarthefoobarman&quot;, words = [&quot;bar&quot;,&quot;foo&quot;,&quot;the&quot;]
输出：[6,9,12]
 

提示：

1 &amp;lt;= s.length &amp;lt;= 10^4
s 由小写英文字母组成
1 &amp;lt;= words.length &amp;lt;= 5000
1 &amp;lt;= words[i].length &amp;lt;= 30
words[i] 由小写英文字母组成
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;困难题目，开局思考半小时，发现应该可以使用滑动窗口的解法；&lt;/li&gt;
&lt;li&gt;具体思想是使用两个 unordered_map：up 记录 words 中所有单词及对应数量，ump 记录滑动窗口内 words 中出现的单词及对应数量；&lt;/li&gt;
&lt;li&gt;遍历时每次增加一个单词的长度，按照 ws 去移动窗口，对于每一趟遍历，维持窗口 l = r；&lt;/li&gt;
&lt;li&gt;若单词在 up 里不存在，重置窗口并清空 ump；&lt;/li&gt;
&lt;li&gt;若单词在 up 里存在，则检查其出现次数是否超过 up 中的次数，若超过则需要增大左边界（右移 l）来缩小窗口；当 cnt 满足等于 ns 时，将 l 作为当前的答案插入。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; findSubstring(string s, vector&amp;lt;string&amp;gt;&amp;amp; words) {
        vector&amp;lt;int&amp;gt; ans;
        int n = s.size();
        if(n &amp;lt;= 0 || words.empty()) return ans;
        int ws = words[0].size();		//words中的word的长度，为统一长度
        int ns = words.size();			//words中的word数量
        unordered_map&amp;lt;string, int&amp;gt; up; //保存words中的word
        for(auto &amp;amp; w : words)
        {
            up[w]++;	//记录每一个单词出现的次数
        }
        for(int i = 0; i &amp;lt; ws; i++)
        {
            int l = i, r = i; //从左开始
            int cnt = 0;	//下文ump存放的单词总数！
            unordered_map&amp;lt;string, int&amp;gt; ump;
            while(r + ws &amp;lt;= n)	//右边长度加每个单词长度要小于s的全长
            {
                string res = s.substr(r, ws); //在右边界加入一个单词
                r += ws;		//扩展右边界
                if(up.find(res) != up.end()) //查看res在up中的位置
                {
                    ump[res]++;
                    cnt++;
                    while(ump[res] &amp;gt; up[res])// 需要检查数量是否超过，超过则要右移left
                    {						 //来缩小窗口
                        string tmp = s.substr(l, ws);
                        l += ws;
                        cnt--;
                        ump[tmp]--;
                    }
                    if(cnt == ns) ans.push_back(l);
                }
                else //未出现，舍弃此单词。清空
                {
                    l = r;
                    cnt = 0;
                    ump.clear();
                }
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：整体复杂度为 O(ns * ws)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(ns * ws)，可能有一些问题，有空再来思考&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 22 3. 无重复字符的最长子串</title><link>https://chaggle.github.io/posts/2021/10/01/day-22-3-longest-substring-without-repeating-characters/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/01/day-22-3-longest-substring-without-repeating-characters/</guid><pubDate>Fri, 01 Oct 2021 08:26:21 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/&quot;&gt;3. 无重复字符的最长子串&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个字符串 s ，请你找出其中不含有重复字符的 最长子串 的长度。

 

示例 1:

输入: s = &quot;abcabcbb&quot;
输出: 3
解释: 因为无重复字符的最长子串是 &quot;abc&quot;，所以其长度为 3。
示例 2:

输入: s = &quot;bbbbb&quot;
输出: 1
解释: 因为无重复字符的最长子串是 &quot;b&quot;，所以其长度为 1。
示例 3:

输入: s = &quot;pwwkew&quot;
输出: 3
解释: 因为无重复字符的最长子串是 &quot;wke&quot;，所以其长度为 3。
     请注意，你的答案必须是 子串 的长度，&quot;pwke&quot; 是一个子序列，不是子串。
示例 4:

输入: s = &quot;&quot;
输出: 0
 

提示：

0 &amp;lt;= s.length &amp;lt;= 5 * 10^4
s 由英文字母、数字、符号和空格组成
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题是很经典的滑动窗口题目。建立 unordered_set 集合，确保集合中没有重复元素。其中 unordered_set 的 find() 方法返回一个迭代器，指向和参数哈希值匹配的元素；如果没有匹配的元素，会返回容器的结束迭代器；&lt;/li&gt;
&lt;li&gt;本题只需要不断移入字符，当集合不满足题目要求时，移除左边的元素，直到满足要求为止，便可得到答案。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.size();
        unordered_set&amp;lt;char&amp;gt; ans;
        int str = 0;
        int l = 0;
        for(int i = 0; i &amp;lt; n; i++)
        {
            while(ans.find(s[i]) != ans.end())
            {
                ans.erase(s[l]);
                l++;
            }
            str = max(str, i - l + 1);
            ans.insert(s[i]);
        }
        return str;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O($n$)&lt;/li&gt;
&lt;li&gt;空间复杂度：O($n$)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>属于二战的一些个人的思考</title><link>https://chaggle.github.io/posts/2021/10/01/thoughts-on-exam-retake-20211001/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/10/01/thoughts-on-exam-retake-20211001/</guid><pubDate>Fri, 01 Oct 2021 07:35:08 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;今天想写一下二战考研的一些想法。因为自己的大学基础走得并不扎实，好好的个人综合素质被自己用得叫做一塌糊涂，自己本科时候做的项目与竞赛没得一个能拿得出手的，自己大学也就做出了一个重要的决定，就是能在大一的时候转了专业。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;自己可惜在大学的时候没能多想一些，多行动一些，这样自己也能够学得更多，学得更加扎实一些。&lt;/p&gt;
&lt;h2&gt;偶然看到的一段话&lt;/h2&gt;
&lt;p&gt;如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;做任何选择都要做到&quot;退可以守，进可以攻。&quot;&lt;/p&gt;
&lt;p&gt;本科理应先把基础打好，学得扎实一点，这样是最稳妥也最安全的。基础是经过千万人的检验，稳定可靠的东西，变化也最慢，比如说算法和数据结构、操作系统编译原理，&lt;/p&gt;
&lt;p&gt;比如说高等数学和线性代数。&lt;/p&gt;
&lt;p&gt;市面上那些花花闪闪的东西都根植于此，万变不离其宗。学通之后，市面上的很多看起来很高的行业壁垒也会不复存在，要退一步找到不那么前沿但需求旺盛的方向也相对方便。&lt;/p&gt;
&lt;p&gt;如果这个基础上，能够足够牛，先试试水钻研某个 AI 的特定方向，技术文章看得多一点，挖得深一点，走得比别人快一点，知道的就比别人多，选择就比别人宽广，而且以后再做出来的选择也会更贴近自己的情况。现在网上各种资源那么多，好好学习提高，总不会是一件坏事。这和在哪个学校无关，在哪个专业也无关。&lt;/p&gt;
&lt;p&gt;到这时候再回头看当时的计划，可能就会哑然失笑，因为远远有更多更好的选择在那里，只是当时坐井观天，以为世界就那么大。&lt;/p&gt;
&lt;p&gt;作者：田渊栋
链接：https://www.zhihu.com/question/410286656/answer/1368251825
来源：知乎&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/somedays/1.png&quot; alt=&quot;image1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;非常肯定这个大佬的说法。&lt;/p&gt;
&lt;h2&gt;考研带来的改变&lt;/h2&gt;
&lt;p&gt;所以自己在一战二战过程中的收获，也让自己走到了如今这个位置：数据结构与算法编程能力的提升，数学思维的提升，以及对语言的应用能力、英语阅读的能力。&lt;/p&gt;
&lt;p&gt;虽然距离自己理想的状态差距还很大，但是逃离了学校，逃脱了一直陷在泥潭里面的室友，自己沉下心，还是能做很多很多的事情：早睡早起，坚持运动，学习技能，都是考研过程中所带给自己的。&lt;/p&gt;
&lt;p&gt;想起儒家学说中，出于语出《礼记·中庸》的慎独自律精神，不仅只是要求自己的道德标杆，在现在这个社会，更体现在自律的习惯之上。&lt;/p&gt;
</content:encoded></item><item><title>Day 21 447. 回旋镖的数量</title><link>https://chaggle.github.io/posts/2021/09/30/day-21-447-number-of-boomerangs/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/30/day-21-447-number-of-boomerangs/</guid><pubDate>Thu, 30 Sep 2021 06:12:26 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/number-of-boomerangs/&quot;&gt;447. 回旋镖的数量&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定平面上 n 对 互不相同 的点 points ，其中 points[i] = [xi, yi] 。

回旋镖 是由点 (i, j, k) 表示的元组 ，

其中 i 和 j 之间的距离和 i 和 k 之间的欧式距离相等（需要考虑元组的顺序）。

返回平面上所有回旋镖的数量。

示例 1：

输入：points = [[0,0],[1,0],[2,0]]
输出：2
解释：两个回旋镖为 [[1,0],[0,0],[2,0]] 和 [[1,0],[2,0],[0,0]]

示例 2：

输入：points = [[1,1],[2,2],[3,3]]
输出：2

示例 3：

输入：points = [[1,1]]
输出：0
 

提示：

n == points.length
1 &amp;lt;= n &amp;lt;= 500
points[i].length == 2
-10^4 &amp;lt;= xi, yi &amp;lt;= 10^4
所有点都 互不相同
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题翻译成人话就是：求二维平面上所给的点中，能组成等腰三角形两条腰边的三个顶点共有多少组，返回其总数；&lt;/li&gt;
&lt;li&gt;使用 unordered_map 处理：在确定 i 作为三元组第一个点的回旋镖个数之前，先计算 i 与其余点的距离，以 {距离 : 个数} 的形式存储，最后分别对所有的距离进行累加计数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numberOfBoomerangs(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; p) {
        int n = p.size();
        int ans = 0;
        for(int i = 0; i &amp;lt; n; i++)
        {
            unordered_map&amp;lt;int, int&amp;gt; up;
            for(int j = 0; j &amp;lt; n; j++)
            {
                if(i == j) continue;
                int x = p[i][0] - p[j][0];
                int y = p[i][1] - p[j][1];
                int d = x * x + y *y;
                ++up[d];
            }
            for(auto [d, cnt] : up)
            {
                ans += cnt * (cnt - 1);
            }
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O($n^2$)&lt;/li&gt;
&lt;li&gt;空间复杂度：O($n$)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 20 347. 前 K 个高频元素</title><link>https://chaggle.github.io/posts/2021/09/29/day-20-347-top-k-frequent-elements/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/29/day-20-347-top-k-frequent-elements/</guid><pubDate>Wed, 29 Sep 2021 02:22:10 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/top-k-frequent-elements/&quot;&gt;347. 前 K 个高频元素&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个整数数组 nums 和一个整数 k ，

请你返回其中出现频率前 k 高的元素。

你可以按 任意顺序 返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]
 
提示：

1 &amp;lt;= nums.length &amp;lt;= 10^5
k 的取值范围是 [1, 数组中不相同的元素的个数]
题目数据保证答案唯一，换句话说，数组中前 k 个高频元素的集合是唯一的
 

进阶：你所设计算法的时间复杂度 必须 优于 O(n log n) ，其中 n 是数组大小。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;典型的大顶堆问题。可以先用 unordered_map 保存键值对，再建立一个 priority_queue；由于 C++ 中 priority_queue 默认是大顶堆，将 unordered_map 中的键值对 {k, v} 改为 {v, k} 保存进 priority_queue 即可，最后输出前 k 个键值对的 v 值即可；&lt;/li&gt;
&lt;li&gt;进阶版可以手写一个堆来替换 priority_queue，等以后有时间再来补坑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; topKFrequent(vector&amp;lt;int&amp;gt;&amp;amp; nums, int n) {
        unordered_map&amp;lt;int, int&amp;gt; up;
        for(auto i : nums) up[i]++;
        priority_queue&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; p;
        for(auto it = up.begin(); it != up.end(); it++)
        {
            p.push({it -&amp;gt; second, it -&amp;gt; first});
        }
        vector&amp;lt;int&amp;gt; ans;
        while(n--)
        {
            ans.push_back(p.top().second);
            p.pop();
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(nlogn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 19 1. 两数之和</title><link>https://chaggle.github.io/posts/2021/09/28/day-19-1-two-sum/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/28/day-19-1-two-sum/</guid><pubDate>Tue, 28 Sep 2021 07:51:16 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/two-sum/&quot;&gt;1. 两数之和&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个整数数组 nums 和一个整数目标值 target，

请你在该数组中找出 和为目标值 target 的那两个 整数，并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是，数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1：

输入：nums = [2,7,11,15], target = 9
输出：[0,1]
解释：因为 nums[0] + nums[1] == 9 ，返回 [0, 1] 。

示例 2：

输入：nums = [3,2,4], target = 6
输出：[1,2]

示例 3：

输入：nums = [3,3], target = 6
输出：[0,1]
 
提示：

2 &amp;lt;= nums.length &amp;lt;= 10^4
-10^9 &amp;lt;= nums[i] &amp;lt;= 10^9
-10^9 &amp;lt;= target &amp;lt;= 10^9
只会存在一个有效答案
进阶：你可以想出一个时间复杂度小于 O(n^2) 的算法吗？
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;暴力解没什么好说的，主要是回忆一下 unordered_map 的相关使用方法，具体可以看本博客去年的总结。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; twoSum(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        unordered_map&amp;lt;int, int&amp;gt; umap;
        int n = nums.size();
        for(int i = 0; i &amp;lt; n; i++)
        {
            auto it = umap.find(target - nums[i]);
            if(it != umap.end())
            {
                return {it -&amp;gt; second, i};
            }
            umap[nums[i]] = i;
        }
        return {};
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)，仅需一次遍历即可&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 18 987. 二叉树的垂序遍历</title><link>https://chaggle.github.io/posts/2021/09/27/day-18-987-vertical-order-traversal-of-a-binary-tree/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/27/day-18-987-vertical-order-traversal-of-a-binary-tree/</guid><pubDate>Mon, 27 Sep 2021 07:45:43 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/vertical-order-traversal-of-a-binary-tree/&quot;&gt;987. 二叉树的垂序遍历&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你二叉树的根结点 root ，

请你设计算法计算二叉树的 垂序遍历 序列。

对位于 (row, col) 的每个结点而言，

其左右子结点分别位于 (row + 1, col - 1) 和 (row + 1, col + 1) 。

树的根结点位于 (0, 0) 。

二叉树的垂序遍历从最左边的列开始直到最右边的列结束，

按列索引每一列上的所有结点，形成一个按出现位置从上到下排序的有序列表。

如果同行同列上有多个结点，则按结点的值从小到大进行排序。

返回二叉树的 垂序遍历 序列。

示例 1：

输入：root = [3,9,20,null,null,15,7]
输出：[[9],[3,15],[20],[7]]
解释：
列 -1 ：只有结点 9 在此列中。
列  0 ：只有结点 3 和 15 在此列中，按从上到下顺序。
列  1 ：只有结点 20 在此列中。
列  2 ：只有结点 7 在此列中。

示例 2：

输入：root = [1,2,3,4,5,6,7]
输出：[[4],[2],[1,5,6],[3],[7]]
解释：
列 -2 ：只有结点 4 在此列中。
列 -1 ：只有结点 2 在此列中。
列  0 ：结点 1 、5 和 6 都在此列中。
          1 在上面，所以它出现在前面。
          5 和 6 位置都是 (2, 0) ，所以按值从小到大排序，5 在 6 的前面。
列  1 ：只有结点 3 在此列中。
列  2 ：只有结点 7 在此列中。

示例 3：

输入：root = [1,2,3,4,6,5,7]
输出：[[4],[2],[1,5,6],[3],[7]]
解释：
这个示例实际上与示例 2 完全相同，只是结点 5 和 6 在树中的位置发生了交换。
因为 5 和 6 的位置仍然相同，所以答案保持不变，仍然按值从小到大排序。
 
提示：

树中结点数目总数在范围 [1, 1000] 内
0 &amp;lt;= Node.val &amp;lt;= 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本质上还是 DFS 与 BFS 的题目，只是解决问题的方式要麻烦一点，所以花费时间较长；本质应属于中等题，算不上难度大的题目；&lt;/li&gt;
&lt;li&gt;DFS 采用哈希表加优先队列实现。由于 C++ 的 priority_queue 默认实现为大根堆，而此题需要小根堆，所以使用 multiset（小根堆）实现；&lt;/li&gt;
&lt;li&gt;BFS 解法留待考研后再写，现在每天思考题目一个小时，时间上已经太多了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) :
        val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    typedef map&amp;lt;int, multiset&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;gt; maps;
    void dfs(TreeNode* root, int x, int y, maps &amp;amp;mp)
    {
        if(root == nullptr) return;
        mp[y].insert({x, root-&amp;gt;val});
        if(root -&amp;gt; left ) dfs(root -&amp;gt; left, x + 1, y - 1, mp);
        if(root -&amp;gt; right) dfs(root -&amp;gt; right, x + 1, y + 1, mp);
    }
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; verticalTraversal(TreeNode* root)
    {
        maps mp;
        dfs(root, 0, 0, mp);
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ans;
        for(auto &amp;amp; [k, v] : mp)
        {
            vector&amp;lt;int&amp;gt; vs;
            for(auto &amp;amp; p : v) vs.push_back(p.second);
            ans.push_back(vs);
        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n*logn)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 17 297. 二叉树的序列化与反序列化</title><link>https://chaggle.github.io/posts/2021/09/26/day-17-297-serialize-and-deserialize-binary-tree/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/26/day-17-297-serialize-and-deserialize-binary-tree/</guid><pubDate>Sun, 26 Sep 2021 09:00:13 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/&quot;&gt;297. 二叉树的序列化与反序列化&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;序列化是将一个数据结构或者对象转换为连续的比特位的操作，

进而可以将转换后的数据存储在一个文件或者内存中，同时也可以通过网络传输到另一个计算机环境，

采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。

这里不限定你的序列 / 反序列化算法执行逻辑，

你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示: 输入输出格式与 LeetCode 目前使用的方式一致，

详情请参阅 LeetCode 序列化二叉树的格式。

你并非必须采取这种方式，你也可以采用其他的方法解决这个问题。

示例 1：

输入：root = [1,2,3,null,null,4,5]
输出：[1,2,3,null,null,4,5]

示例 2：

输入：root = []
输出：[]

示例 3：

输入：root = [1]
输出：[1]

示例 4：

输入：root = [1,2]
输出：[1,2]
 

提示：

树中结点数在范围 [0, 10^4] 内
-1000 &amp;lt;= Node.val &amp;lt;= 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本质上还是 DFS 与 BFS 的题目，但此题难度较大，所以花费时间较长；&lt;/li&gt;
&lt;li&gt;DFS 有前序、后序两种递归形式，后序遍历因为时间限制暂时没有写；而中序遍历由于无法确定根节点，所以无法形成递归形式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Codec {
public:
    /* void predfs(TreeNode* root, string&amp;amp; data)
    {
        if(root == nullptr) data += &quot;null,&quot;;
        else
        {
            data += to_string(root -&amp;gt; val) + &quot;,&quot;;
            predfs(root -&amp;gt; left, data);
            predfs(root -&amp;gt; right, data);
        }
    }

    string serialize(TreeNode* root)
    {
        string data;
        predfs(root, data);
        return data;
    }

    TreeNode* rdfs(list&amp;lt;string&amp;gt;&amp;amp; str)
    {
        if (str.front() == &quot;null&quot;)
        {
            str.erase(str.begin());
            return nullptr;
        }
        TreeNode* root = new TreeNode(stoi(str.front()));
        str.erase(str.begin());
        root -&amp;gt; left = rdfs(str);
        root -&amp;gt; right = rdfs(str);
        return root;
    }

    TreeNode* deserialize(string data)
    {
        list&amp;lt;string&amp;gt; str;
        string s;
        for (auto&amp;amp; ch : data)
        {
            if (ch == &apos;,&apos;)
            {
                str.push_back(s);
                s.clear();
            }
            else s.push_back(ch);
        }
        if (s.empty() != 0)
        {
            str.push_back(s);
            s.clear();
        }
        return rdfs(str);
    } */
    string serialize(TreeNode* root) {
        string ans;
        queue&amp;lt;TreeNode* &amp;gt; q;
        if(root == nullptr) return ans;
        else q.push(root);
        while(!q.empty())
        {
            int n = q.size();
            for(int i = 0; i &amp;lt; n; ++i){
                auto node = q.front();
                q.pop();
                if(node == nullptr) ans += &quot;null,&quot;;
                else
                {
                    ans += to_string(node-&amp;gt;val) + &quot;,&quot;;
                    q.push(node-&amp;gt;left);
                    q.push(node-&amp;gt;right);
                }
            }
        }
        return ans;
    }
    TreeNode* deserialize(string data) {
        if(data.empty()) return NULL;
        vector&amp;lt;TreeNode* &amp;gt; ans;
        int k = 0;
        int n = data.size();
        while(k &amp;lt; n)
        {
            string tmp;
            while(data[k] != &apos;,&apos;)
            {
                tmp += data[k];
                k++;
            }
            if(tmp == &quot;null&quot;) ans.push_back(NULL);
            else ans.push_back(new TreeNode(stoi(tmp)));
            tmp.clear();
            k++;
        }
        int pos = 1;
        for(int i = 0; i &amp;lt; ans.size(); i++)
        {
            if(ans[i] == NULL) continue;
            ans[i] -&amp;gt; left = ans[pos++];
            ans[i] -&amp;gt; right = ans[pos++];
        }
        return ans[0];
    }
};
// Your Codec object will be instantiated and called as such:
// Codec ser, deser;
// TreeNode* ans = deser.deserialize(ser.serialize(root));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 16 513. 找树左下角的值</title><link>https://chaggle.github.io/posts/2021/09/25/day-16-513-find-bottom-left-tree-value/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/25/day-16-513-find-bottom-left-tree-value/</guid><pubDate>Sat, 25 Sep 2021 07:13:58 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/find-bottom-left-tree-value/&quot;&gt;513. 找树左下角的值&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个二叉树的 根节点 root，请找出该二叉树的 最底层 最左边 节点的值。

假设二叉树中至少有一个节点。

示例 1:

输入: root = [2,1,3]
输出: 1

示例 2:

输入: root = [1,2,3,4,null,5,6,null,null,7]
输出: 7

提示:

二叉树的节点个数的范围是 [1,10^4]
-2^31 &amp;lt;= Node.val &amp;lt;= 2^31 - 1 

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;DFS 与 BFS 题目，DFS 变式多，BFS 单一，更简单。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) :
        val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    /* int findBottomLeftValue(TreeNode* root) {
        queue&amp;lt;TreeNode* &amp;gt; q;
        if(root != NULL) q.push(root);
        int result = 0;
        while (!q.empty())
        {
            int n = q.size();
            for (int i = 0; i &amp;lt; n; i++)
            {
                TreeNode* node = q.front();
                q.pop();
                if(i == 0) result = node-&amp;gt;val;
                if(node -&amp;gt; left != nullptr) q.push(node -&amp;gt; left);
                if(node -&amp;gt; right != nullptr) q.push(node -&amp;gt; right);
            }
        }
        return result; */
        int len = INT_MIN;
        int value;
        void dfs(TreeNode* root, int deep)
        {
            if(root != nullptr)
            {
                if(root -&amp;gt; left == nullptr &amp;amp;&amp;amp; root -&amp;gt; right == nullptr)
                {
                    if(deep &amp;gt; len)
                    {
                        len = deep;
                        value = root -&amp;gt; val;
                    }
                    return;
                }
                dfs(root -&amp;gt; left, deep + 1);
                dfs(root -&amp;gt; right, deep + 1);
            }
            return;
        }
    int findBottomLeftValue(TreeNode* root)
    {
        dfs(root, 0);
        return value;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 15 129. 求根节点到叶节点数字之和</title><link>https://chaggle.github.io/posts/2021/09/24/day-15-129-sum-root-to-leaf-numbers/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/24/day-15-129-sum-root-to-leaf-numbers/</guid><pubDate>Fri, 24 Sep 2021 08:11:15 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/&quot;&gt;129. 求根节点到叶节点数字之和&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个二叉树的根节点 root ，树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字：

例如，从根节点到叶节点的路径 1 -&amp;gt; 2 -&amp;gt; 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。

叶节点 是指没有子节点的节点。

示例 1：

输入：root = [1,2,3]
输出：25
解释：
从根到叶子节点路径 1-&amp;gt;2 代表数字 12
从根到叶子节点路径 1-&amp;gt;3 代表数字 13
因此，数字总和 = 12 + 13 = 25

示例 2：

输入：root = [4,9,0,5,1]
输出：1026
解释：
从根到叶子节点路径 4-&amp;gt;9-&amp;gt;5 代表数字 495
从根到叶子节点路径 4-&amp;gt;9-&amp;gt;1 代表数字 491
从根到叶子节点路径 4-&amp;gt;0 代表数字 40
因此，数字总和 = 495 + 491 + 40 = 1026

提示：

树中节点的数目在范围 [1, 1000] 内
0 &amp;lt;= Node.val &amp;lt;= 9
树的深度不超过 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;DFS 与 BFS 题目，DFS 变式多，BFS 单一，更简单。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) :
        val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int sumNumbers(TreeNode* root) {
        /* return dfs(root, 0);
    }
    int dfs(TreeNode* root, int pre)
    {
        if(root == nullptr) return 0;
        int sum = 10 * pre + root -&amp;gt; val;
        if(root -&amp;gt; left == nullptr &amp;amp;&amp;amp; root -&amp;gt; right == nullptr) return sum;
        else return dfs(root -&amp;gt; left, sum) + dfs(root -&amp;gt; right, sum); */
        if(root == nullptr) return 0;
        int sum = 0;
        queue&amp;lt;TreeNode* &amp;gt;q1;
        queue&amp;lt;int&amp;gt; q2;
        q1.push(root);
        q2.push(root -&amp;gt; val);
        while(q1.empty() == 0)
        {
            TreeNode* n = q1.front();
            int num = q2.front();
            q1.pop();q2.pop();
            TreeNode* l = n -&amp;gt; left;
            TreeNode* r = n -&amp;gt; right;
            if(l == nullptr &amp;amp;&amp;amp; r == nullptr)
                sum += num;
            else
            {
                if(l != nullptr)
                {
                    q1.push(l);
                    q2.push(num * 10 + l -&amp;gt; val);
                }
                if(r != nullptr)
                {
                    q1.push(r);
                    q2.push(num * 10 + r -&amp;gt; val);
                }
            }
        }
        return sum;
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 14 100. 相同的树</title><link>https://chaggle.github.io/posts/2021/09/23/day-14-100-same-tree/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/23/day-14-100-same-tree/</guid><pubDate>Thu, 23 Sep 2021 07:32:27 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/same-tree/&quot;&gt;100. 相同的树&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你两棵二叉树的根节点 p 和 q ，编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同，并且节点具有相同的值，则认为它们是相同的。


示例 1：

输入：p = [1,2,3], q = [1,2,3]
输出：true

示例 2：

输入：p = [1,2], q = [1,null,2]
输出：false

示例 3：

输入：p = [1,2,1], q = [1,1,2]
输出：false
 
提示：

两棵树上的节点数目都在范围 [0, 100] 内
-10^4 &amp;lt;= Node.val &amp;lt;= 10^4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简单题，与昨日的解法类似，可以使用递归解决问题，也可以进行层序遍历，熟悉一下 BFS 的模板。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) :
        val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool isSameTree(TreeNode* p, TreeNode* q) {
        /* if (p == nullptr &amp;amp;&amp;amp; q == nullptr) {
            return true;
        } else if (p == nullptr || q == nullptr) {
            return false;
        } else if (p-&amp;gt;val != q-&amp;gt;val) {
            return false;
        } else {
            return isSameTree(p-&amp;gt;left, q-&amp;gt;left) &amp;amp;&amp;amp; isSameTree(p-&amp;gt;right, q-&amp;gt;right);
        } */
        if (p == nullptr &amp;amp;&amp;amp; q == nullptr) return true;
        else if (p == nullptr || q == nullptr) return false;
        queue &amp;lt;TreeNode*&amp;gt; q1, q2;
        q1.push(p);
        q2.push(q);
        while (q1.empty() == 0 &amp;amp;&amp;amp; q2.empty() == 0) {
            auto n1 = q1.front();
            q1.pop();
            auto n2 = q2.front();
            q2.pop();
            if (n1 -&amp;gt; val != n2 -&amp;gt; val) return false;
            auto l1 = n1 -&amp;gt; left, r1 = n1 -&amp;gt; right, l2 = n2 -&amp;gt; left, r2 = n2 -&amp;gt; right;
            if ((l1 == nullptr) ^ (l2 == nullptr)) return false;
            if ((r1 == nullptr) ^ (r2 == nullptr)) return false;
            if (l1 != nullptr) q1.push(l1);
            if (r1 != nullptr) q1.push(r1);
            if (l2 != nullptr) q2.push(l2);
            if (r2 != nullptr) q2.push(r2);
        }
        return q1.empty() &amp;amp;&amp;amp; q2.empty();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(min(n, m))&lt;/li&gt;
&lt;li&gt;空间复杂度：递归为 O(min(n, m))，调用栈使用的空间即为二叉树的高度；层序遍历为 O(n)，用于存放所有的节点数。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 13 104. 二叉树的最大深度</title><link>https://chaggle.github.io/posts/2021/09/22/day-13-104-maximum-depth-of-binary-tree/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/22/day-13-104-maximum-depth-of-binary-tree/</guid><pubDate>Wed, 22 Sep 2021 08:42:13 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/&quot;&gt;104. 二叉树的最大深度&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个二叉树，找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例：
给定二叉树 [3,9,20,null,null,15,7]，
    3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3 。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简单题，可以使用递归解决问题，也可以进行层序遍历，顺便熟悉一下 BFS 的模板。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) :
        val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int maxDepth(TreeNode* root) {
        /* if (root == nullptr) return 0;
        int l, r;
        l = maxDepth(root -&amp;gt; left);
        r = maxDepth(root -&amp;gt; right);
        return max(l, r) + 1; */
        //以下为BFS
        queue&amp;lt;TreeNode* &amp;gt; q;
        if (root == nullptr) return 0;
        q.push(root);
        int tmp = 0;
        while(q.empty() == 0)
        {
            int n = q.size();
            tmp++;
            for(int i = 0; i &amp;lt; n; i++)
            {
                TreeNode* res = q.front();
                q.pop();
                if(res -&amp;gt; left != nullptr) q.push(res -&amp;gt; left);
                if(res -&amp;gt; right != nullptr) q.push(res -&amp;gt; right);
            }
        }
        return tmp;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)，因为要遍历所有节点，层序遍历时间复杂度也是 O(n)；&lt;/li&gt;
&lt;li&gt;空间复杂度：递归为 O(height)，调用栈使用的空间即为二叉树的高度；层序遍历为 O(n)，用于存放所有的节点数。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 12 146. LRU 缓存机制</title><link>https://chaggle.github.io/posts/2021/09/21/day-12-146-lru-cache/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/21/day-12-146-lru-cache/</guid><pubDate>Tue, 21 Sep 2021 08:02:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/lru-cache/&quot;&gt;146. LRU 缓存机制&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;运用你所掌握的数据结构，设计和实现一个  LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类：

LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存

int get(int key) 如果关键字 key 存在于缓存中，则返回关键字的值，否则返回 -1 。

void put(int key, int value) 如果关键字已经存在，则变更其数据值；

如果关键字不存在，则插入该组「关键字-值」。

当缓存容量达到上限时，它应该在写入新数据之前删除最久未使用的数据值，从而为新的数据值留出空间。

进阶：你是否可以在 O(1) 时间复杂度内完成这两种操作？

示例：

输入
[&quot;LRUCache&quot;, &quot;put&quot;, &quot;put&quot;, &quot;get&quot;, &quot;put&quot;, &quot;get&quot;, &quot;put&quot;, &quot;get&quot;, &quot;get&quot;, &quot;get&quot;]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废，缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废，缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4
 

提示：

1 &amp;lt;= capacity &amp;lt;= 3000
0 &amp;lt;= key &amp;lt;= 10000
0 &amp;lt;= value &amp;lt;= 10^5
最多调用 2 * 10^5 次 get 和 put
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题需要自己实现一个 LRU 缓存机制。结合操作系统中学到的知识以及题目所给条件，实现上应该选用哈希表加双向链表：双向链表需要实现移除、插入、修改等操作，实现起来比较有难度；哈希表可以使用 STL 库中的 unordered_map 实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class LRUCache {
private:
    struct Node
    {
        int key, value;
        Node* left ,*right;
        Node(int _key,int _value) : key(_key), value(_value), left(NULL), right(NULL) {}
    } *l, *r;      //双链表的最左和最右节点，不存贮值。
    int n;
    unordered_map&amp;lt;int, Node*&amp;gt;hash;

public:
    void remove(Node* p)
    {
        p -&amp;gt; right -&amp;gt; left = p -&amp;gt; left;
        p -&amp;gt; left -&amp;gt; right = p -&amp;gt; right;
    }

    void insert(Node *p)
    {
        p -&amp;gt; right = l -&amp;gt; right;
        p -&amp;gt; left = l;
        l -&amp;gt; right -&amp;gt; left = p;
        l -&amp;gt; right = p;
    }

    LRUCache(int capacity)
    {
        n = capacity;
        l = new Node(-1, -1);
        r = new Node(-1, -1);
        l -&amp;gt; right = r;
        r -&amp;gt; left = l;
    }

    int get(int key)
    {
        if(hash.count(key) == 0) return -1;   //不存在关键字key
        auto p = hash[key];
        remove(p);
        insert(p);                              //将当前节点放在双链表的第一位
        return p -&amp;gt; value;
    }

    void put(int key, int value)
    {
        if(hash.count(key))                   //如果key存在，则修改对应的value
        {
            auto p = hash[key];
            p -&amp;gt; value = value;
            remove(p);
            insert(p);
        }
        else
        {
            if(hash.size() == n)              //如果缓存已满，则删除双链表最右侧的节点
            {
                auto  p = r -&amp;gt; left;
                remove(p);
                hash.erase(p -&amp;gt; key);         //更新哈希表
                delete p;                     //释放内存，c++注意内存管理
            }
            auto p = new Node(key, value);    //否则，插入(key, value)
            hash[key] = p;
            insert(p);
        }
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj-&amp;gt;get(key);
 * obj-&amp;gt;put(key,value);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(1)，都是常数级复杂度；&lt;/li&gt;
&lt;li&gt;空间复杂度：O(capacity + 1)。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 11 142. 环形链表 II</title><link>https://chaggle.github.io/posts/2021/09/20/day-11-142-linked-list-cycle-ii/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/20/day-11-142-linked-list-cycle-ii/</guid><pubDate>Mon, 20 Sep 2021 07:20:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/linked-list-cycle-ii/&quot;&gt;142. 环形链表 II&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个链表，返回链表开始入环的第一个节点。 如果链表无环，则返回 null。

为了表示给定链表中的环，我们使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。

如果 pos 是 -1，则在该链表中没有环。

注意，pos 仅仅是用于标识环的情况，并不会作为参数传递到函数中。

说明：不允许修改给定的链表。

进阶：

你是否可以使用 O(1) 空间解决此题？
 
示例 1：

输入：head = [3,2,0,-4], pos = 1
输出：返回索引为 1 的链表节点
解释：链表中有一个环，其尾部连接到第二个节点。

示例 2：

输入：head = [1,2], pos = 0
输出：返回索引为 0 的链表节点
解释：链表中有一个环，其尾部连接到第一个节点。

示例 3：

输入：head = [1], pos = -1
输出：返回 null
解释：链表中没有环。

提示：

链表中节点的数目范围在范围 [0, 10^4] 内
-10^5 &amp;lt;= Node.val &amp;lt;= 10^5
pos 的值为 -1 或者链表中的一个有效索引
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;经典双指针问题，同时与昨日的题目一样，也可以使用哈希表解法，但哈希表的空间复杂度较高，效果不佳；&lt;/li&gt;
&lt;li&gt;双指针解法的完整证明见官方题解，此处不再赘述。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if(head == NULL || head -&amp;gt; next == nullptr) return NULL;
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast != nullptr &amp;amp;&amp;amp; fast -&amp;gt; next != nullptr)
        {
            slow = slow -&amp;gt; next;
            fast = fast -&amp;gt; next -&amp;gt; next;
            if(fast == slow)
            {
                ListNode* tmp = head;
                while(tmp != slow)
                {
                    tmp = tmp -&amp;gt; next;
                    slow = slow -&amp;gt; next;
                }
                return tmp;
            }
        }
        return NULL;
    }
        /* if(head == NULL || head -&amp;gt; next == nullptr) return NULL;
        unordered_set&amp;lt;ListNode* &amp;gt; m;
        ListNode* fast = head;
        while(fast -&amp;gt; next != nullptr)
        {
            if(m.count(fast) != 0) return fast;
            m.insert(fast);
            fast = fast -&amp;gt; next;
        }
        return nullptr; */
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：双指针 O(n)，哈希表为 O(n)；&lt;/li&gt;
&lt;li&gt;空间复杂度：双指针 O(1)，哈希表为 O(n)。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 10 160. 相交链表</title><link>https://chaggle.github.io/posts/2021/09/19/day-10-160-intersection-of-two-linked-lists/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/19/day-10-160-intersection-of-two-linked-lists/</guid><pubDate>Sun, 19 Sep 2021 08:17:31 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/intersection-of-two-linked-lists/&quot;&gt;160. 相交链表&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你两个单链表的头节点 headA 和 headB ，

请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点，返回 null 。

图示两个链表在节点 c1 开始相交：

题目数据 保证 整个链式结构中不存在环。

注意，函数返回结果后，链表必须 保持其原始结构 。

示例 1：


输入：intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出：Intersected at &apos;8&apos;
解释：相交节点的值为 8 （注意，如果两个链表相交则不能为 0）。
从各自的表头开始算起，链表 A 为 [4,1,8,4,5]，链表 B 为 [5,0,1,8,4,5]。
在 A 中，相交节点前有 2 个节点；在 B 中，相交节点前有 3 个节点。
示例 2：


输入：intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出：Intersected at &apos;2&apos;
解释：相交节点的值为 2 （注意，如果两个链表相交则不能为 0）。
从各自的表头开始算起，链表 A 为 [0,9,1,2,4]，链表 B 为 [3,2,4]。
在 A 中，相交节点前有 3 个节点；在 B 中，相交节点前有 1 个节点。
示例 3：


输入：intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出：null
解释：从各自的表头开始算起，链表 A 为 [2,6,4]，链表 B 为 [1,5]。
由于这两个链表不相交，所以 intersectVal 必须为 0，而 skipA 和 skipB 可以是任意值。
这两个链表不相交，因此返回 null 。
 

提示：

listA 中节点数目为 m
listB 中节点数目为 n
0 &amp;lt;= m, n &amp;lt;= 3 * 10^4
1 &amp;lt;= Node.val &amp;lt;= 10^5
0 &amp;lt;= skipA &amp;lt;= m
0 &amp;lt;= skipB &amp;lt;= n
如果 listA 和 listB 没有交点，intersectVal 为 0
如果 listA 和 listB 有交点，intersectVal == listA[skipA + 1] == listB[skipB + 1]
 

进阶：你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案？
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;经典双指针问题，如果 A、B 中有一个为空，则无交点；&lt;/li&gt;
&lt;li&gt;若相交则无需证明其正确性；若不相交，则两个指针相互遍历到最后的结果都为 nullptr，需要经过 m + n 次；&lt;/li&gt;
&lt;li&gt;哈希表解法更好理解一些：当 count 函数查询到 q 时，即可返回相交节点，否则返回 nullptr。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        /* if(headA == nullptr || headB == nullptr) return nullptr;
        ListNode* p = headA;
        ListNode* q = headB;
        while(p != q)
        {
            p = p == nullptr ? headB : p -&amp;gt; next;
            q = q == nullptr ? headA : q -&amp;gt; next;
        }
        return p; */
        if(headA == nullptr || headB == nullptr) return nullptr;
        unordered_set&amp;lt;ListNode* &amp;gt; vis;
        ListNode* p = headA;
        ListNode* q = headB;
        while(p != nullptr)
        {
            vis.insert(p);
            p = p -&amp;gt; next;
        }

        while(q != nullptr)
        {
            if(vis.count(q)) return q;
            q = q -&amp;gt; next;
        }
        return nullptr;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：双指针 O(n)，哈希表也是 O(n)；&lt;/li&gt;
&lt;li&gt;空间复杂度：双指针 O(1)，哈希表为 O(n)。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 9 109. 有序链表转换二叉搜索树</title><link>https://chaggle.github.io/posts/2021/09/18/day-09-109-convert-sorted-list-to-binary-search-tree/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/18/day-09-109-convert-sorted-list-to-binary-search-tree/</guid><pubDate>Sat, 18 Sep 2021 08:17:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/convert-sorted-list-to-binary-search-tree/&quot;&gt;109. 有序链表转换二叉搜索树&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个单链表，其中的元素按升序排序，将其转换为高度平衡的二叉搜索树。

本题中，一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

示例:

给定的有序链表： [-10, -3, 0, 5, 9],

一个可能的答案是：[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树：

      0
     / \
   -3   9
   /   /
 -10  5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本题相当于将链表转化为一棵二叉搜索树（Binary Search Tree，BST）；&lt;/li&gt;
&lt;li&gt;基于中序遍历恢复二叉搜索树，即可从任意节点出发，以节点左边的升序序列作为左子树，右边的升序序列作为右子树。本题要求高度平衡，即左右子树高度差至多为 1，所以从链表的中点开始建树最好；&lt;/li&gt;
&lt;li&gt;本题也可以用 BFS 建树、DFS 填节点值，但这样空间复杂度较高，在时间复杂度与递归链表差不多的情况下，确实不是特别优秀的解法，不过也可以用来熟悉一下 DFS 与 BFS。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) :
        val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* sortedListToBST(ListNode* head) {
        if(head == nullptr) return nullptr;
        if(head -&amp;gt; next == nullptr)
        {
            TreeNode* root = new TreeNode(head -&amp;gt; val);
            return root;
        }
        ListNode* fast = head;
        int len = 1;
        while(fast -&amp;gt; next != nullptr)
        {
            fast = fast -&amp;gt; next;
            len++;
        }
        len = len &amp;gt;&amp;gt; 1;
        ListNode* slow = head;
        ListNode* p = slow;
        while(len--)
        {
            p = slow;
            slow = slow -&amp;gt; next;
        }
        p -&amp;gt; next = nullptr;
        TreeNode* root = new TreeNode(slow -&amp;gt; val);
        root -&amp;gt; left = sortedListToBST(head);
        root -&amp;gt; right = sortedListToBST(slow -&amp;gt; next);
        return root;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(log n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(1)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 8 24. 两两交换链表中的节点</title><link>https://chaggle.github.io/posts/2021/09/17/day-08-24-swap-nodes-in-pairs/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/17/day-08-24-swap-nodes-in-pairs/</guid><pubDate>Fri, 17 Sep 2021 07:12:01 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/swap-nodes-in-pairs/&quot;&gt;24. 两两交换链表中的节点&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个链表，两两交换其中相邻的节点，并返回交换后的链表。

你不能只是单纯的改变节点内部的值，而是需要实际的进行节点交换。

示例 1：

输入：head = [1,2,3,4]
输出：[2,1,4,3]

示例 2：

输入：head = []
输出：[]

示例 3：

输入：head = [1]
输出：[1]
 
提示：

链表中节点的数目在范围 [0, 100] 内
0 &amp;lt;= Node.val &amp;lt;= 100
 

进阶：你能在不修改链表节点值的情况下解决这个问题吗?（也就是说，仅修改节点本身。）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;最简单的思路自然是递归的思路；&lt;/li&gt;
&lt;li&gt;而递归调用的也是栈的实现方法，所以自然也能想到用栈来做的解法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
		if(head == nullptr || head -&amp;gt; next == nullptr) return head;
		stack&amp;lt;ListNode* &amp;gt; stk;
		ListNode* p = new ListNode();
		ListNode* pos = head;
        head = p;
		while(pos != nullptr &amp;amp;&amp;amp; pos -&amp;gt; next != nullptr)
        {
			stk.push(pos);
			stk.push(pos -&amp;gt; next);
			pos = pos -&amp;gt; next -&amp;gt; next;
			p -&amp;gt; next = stk.top();
            stk.pop();
			p = p -&amp;gt; next;
			p -&amp;gt; next = stk.top();
            stk.pop();
			p = p -&amp;gt; next;
		}
		if(pos != nullptr) p -&amp;gt; next = pos;
	    else p -&amp;gt; next = nullptr;
		return head -&amp;gt; next;
		/*
		if (head == nullptr || head-&amp;gt;next == nullptr) return head;
        ListNode* p = head -&amp;gt; next;
        head -&amp;gt; next = swapPairs(p -&amp;gt; next);
        p -&amp;gt; next = head;
        return p;
		*/
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 7 61. 旋转链表</title><link>https://chaggle.github.io/posts/2021/09/16/day-07-61-rotate-list/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/16/day-07-61-rotate-list/</guid><pubDate>Thu, 16 Sep 2021 08:32:11 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/rotate-list/&quot;&gt;61. 旋转链表&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给你一个链表的头节点 head ，旋转链表，将链表每个节点向右移动 k 个位置。

示例 1：

输入：head = [1,2,3,4,5], k = 2
输出：[4,5,1,2,3]

示例 2:

输入：head = [0,1,2], k = 4
输出：[2,0,1]
 
提示：

链表中节点的数目在范围 [0, 500] 内
-100 &amp;lt;= Node.val &amp;lt;= 100
0 &amp;lt;= k &amp;lt;= 2 * 10^9
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;理清 len - 1 - k % len 即为尾节点，就能弄明白这道题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        if(k == 0 || head == NULL || head -&amp;gt; next == nullptr) return head;
        int len = 1;
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast -&amp;gt; next != nullptr)
        {
            fast = fast -&amp;gt; next;
            ++len;
        }
        fast -&amp;gt; next = head;
        k = len - 1 - k % len; //此处注意移动k步后的末尾节点,鄙人在此卡了接近半小时
        if(k == len) return head;//最后想明白了len - 1是链表尾节点，k % len为剩余步数
        while(k)     //len - 1 - k % n为尾节点
        {
            slow = slow -&amp;gt; next;
            k--;
        }
        fast = slow -&amp;gt; next;
        slow -&amp;gt; next = nullptr;
        return fast;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)，遍历两次链表&lt;/li&gt;
&lt;li&gt;空间复杂度：O(1)，常数级&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 6 768. 最多能完成排序的块 II</title><link>https://chaggle.github.io/posts/2021/09/15/day-06-768-max-chunks-to-make-sorted-ii/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/15/day-06-768-max-chunks-to-make-sorted-ii/</guid><pubDate>Wed, 15 Sep 2021 06:22:34 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/max-chunks-to-make-sorted-ii/&quot;&gt;768. 最多能完成排序的块 II&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;这个问题和“最多能完成排序的块”相似，

但给定数组中的元素可以重复，输入数组最大长度为2000，其中的元素最大为10^8。

arr是一个可能包含重复元素的整数数组，我们将这个数组分割成几个“块”，

并将这些块分别进行排序。之后再连接起来，使得连接的结果和按升序排序后的原数组相同。

我们最多能将数组分成多少块？

示例 1:

输入: arr = [5,4,3,2,1]
输出: 1
解释:
将数组分成2块或者更多块，都无法得到所需的结果。
例如，分成 [5, 4], [3, 2, 1] 的结果是 [4, 5, 1, 2, 3]，这不是有序的数组。
示例 2:

输入: arr = [2,1,3,4,4]
输出: 4
解释:
我们可以把它分成两块，例如 [2, 1], [3, 4, 4]。
然而，分成 [2, 1], [3], [4], [4] 可以得到最多的块数。
注意:

arr的长度在[1, 2000]之间。
arr[i]的大小在[0, 10**8]之间。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;分块排序后再连接起来，结果需要与原数组升序排序后的结果相同；&lt;/li&gt;
&lt;li&gt;可以采用动态规划的思想：前一个块的最大值要小于下一个块的最小值，这样分块排序合并之后就会与原数组升序排序的结果一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int maxChunksToSorted(vector&amp;lt;int&amp;gt;&amp;amp; arr) {
        /* 虽然能通过但是时间复杂度太高，两者比较与清空操作要花费较多的时间开销
           解法并不优秀。
        int n = arr.size();
        vector&amp;lt;int&amp;gt; nums(arr);
        sort(nums.begin(), nums.end());
        unordered_map&amp;lt;int, int&amp;gt; h1, h2;
        int ans = 0;
        for (int i = 0; i &amp;lt; n; i++)
        {
            ++h1[arr[i]];
            ++h2[nums[i]];
            if(h1 == h2)
            {
                ans++;
                h1.clear();
                h2.clear();
            }
        }
        return ans;*/
        //此解法就极大缩短了时间，只需要寻找左边最大值小于右边最小值的区域即可
        int n = arr.size();
        vector&amp;lt;int&amp;gt; l(n, 0);
        vector&amp;lt;int&amp;gt; r(n, 10^8);
        l[0] = arr[0];
        r[n - 1] = arr[n - 1];
        for(int i = 1; i &amp;lt; n; i++)
        {
            //动态规划
            l[i] = max(l[i - 1], arr[i]);
            r[n - 1 - i] = min(r[n - i], arr[n - 1 - i]);
        }
        int res = 1;
        for(int i = 0; i &amp;lt; n - 1; i++)
        {
            int ans = l[i] &amp;lt;= r[i + 1] ? 1 : 0;
            res += ans;
        }
        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n)&lt;/li&gt;
&lt;li&gt;空间复杂度：O(2n)，两个数组开辟 2n 的空间。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 5 232. 用栈实现队列</title><link>https://chaggle.github.io/posts/2021/09/14/day-05-232-implement-queue-using-stacks/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/14/day-05-232-implement-queue-using-stacks/</guid><pubDate>Tue, 14 Sep 2021 08:11:21 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/implement-queue-using-stacks/&quot;&gt;232. 用栈实现队列&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;请你仅使用两个栈实现先入先出队列。

队列应当支持一般队列支持的所有操作（push、pop、peek、empty）：

实现 MyQueue 类：

void push(int x) 将元素 x 推到队列的末尾

int pop() 从队列的开头移除并返回元素

int peek() 返回队列开头的元素

boolean empty() 如果队列为空，返回 true ；否则，返回 false
 

说明：

你只能使用标准的栈操作
就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。

你所使用的语言也许不支持栈。

你可以使用 list 或者 deque（双端队列）来模拟一个栈，只要是标准的栈操作即可。
 
进阶：

你能否实现每个操作均摊时间复杂度为 O(1) 的队列？

换句话说，执行 n 个操作的总时间复杂度为 O(n) ，即使其中一个操作可能花费较长时间。
 

示例：

输入：
[&quot;MyQueue&quot;, &quot;push&quot;, &quot;push&quot;, &quot;peek&quot;, &quot;pop&quot;, &quot;empty&quot;]
[[], [1], [2], [], [], []]
输出：
[null, null, null, 1, 1, false]

解释：
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
 

提示：

1 &amp;lt;= x &amp;lt;= 9
最多调用 100 次 push、pop、peek 和 empty
假设所有操作都是有效的 （例如，一个空的队列不会调用 pop 或者 peek 操作）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;如题所给，创建两个栈，其中 s1 为入栈，s2 为出栈；&lt;/li&gt;
&lt;li&gt;由于栈是动态实现的，不存在上溢问题，所以入栈只需要将 x 保存至 s1 中即可；&lt;/li&gt;
&lt;li&gt;在 pop() 与 peek() 函数中，是相同的逻辑，只是一个需要删除，一个不需要删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class MyQueue {
private:
    stack&amp;lt;int&amp;gt; s1, s2;
public:
    /** Initialize your data structure here. */
    MyQueue() {}

    /** Push element x to the back of queue. */
    void push(int x) {
        s1.push(x);
    }

    /** Removes the element from in front of queue and returns that element. */
    int pop()
    {
        if(s2.size() == 0 &amp;amp;&amp;amp; s1.size() == 0) return false;
        else if(s2.size() != 0)
        {
            int temp = s2.top();
            s2.pop();
            return temp;
        }
        else
        {
            while(s1.size() != 0)
            {
                int tmp = s1.top();
                s2.push(s1.top());
                s1.pop();
            }
            int temp = s2.top();
            s2.pop();
            return temp;
        }
    }

    /** Get the front element. */
    int peek() {
        if(s2.size() == 0 &amp;amp;&amp;amp; s1.size() == 0) return false;
        else if(s2.size() != 0) return s2.top();
        else
        {
            while(s1.size() != 0)
            {
                int tmp = s1.top();
                s2.push(s1.top());
                s1.pop();
            }
        }
        return s2.top();
    }

    /** Returns whether the queue is empty. */
    bool empty() {
        return s1.empty() &amp;amp;&amp;amp; s2.empty();
    }
};

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue* obj = new MyQueue();
 * obj-&amp;gt;push(x);
 * int param_2 = obj-&amp;gt;pop();
 * int param_3 = obj-&amp;gt;peek();
 * bool param_4 = obj-&amp;gt;empty();
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：均摊 O(1)。除了调用 pop() 与 peek() 初次需要较长的时间之外，其余都只需要 O(1) 的复杂度（击败 100% 应该就是 O(1) 吧）。&lt;/li&gt;
&lt;li&gt;空间复杂度：O(n)，两个栈一共存放输入 n 个数据规模，故应为 O(n)。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 4 394. 字符串解码</title><link>https://chaggle.github.io/posts/2021/09/13/day-04-394-decode-string/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/13/day-04-394-decode-string/</guid><pubDate>Mon, 13 Sep 2021 03:17:33 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/decode-string/&quot;&gt;394. 字符串解码&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个经过编码的字符串，返回它解码后的字符串。

编码规则为: k[encoded_string]，表示其中方括号内部的 encoded_string 正好重复 k 次。

注意 k 保证为正整数。

你可以认为输入字符串总是有效的；输入字符串中没有额外的空格，且输入的方括号总是符合格式要求的。

此外，你可以认为原始数据不包含数字，所有的数字只表示重复的次数 k 。

例如不会出现像 3a 或 2[4] 的输入。

 

示例 1：

输入：s = &quot;3[a]2[bc]&quot;
输出：&quot;aaabcbc&quot;
示例 2：

输入：s = &quot;3[a2[c]]&quot;
输出：&quot;accaccacc&quot;
示例 3：

输入：s = &quot;2[abc]3[cd]ef&quot;
输出：&quot;abcabccdcdcdef&quot;
示例 4：

输入：s = &quot;abc3[cd]xyz&quot;
输出：&quot;abccdcdcdxyz&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;创建一个 stack&amp;lt;string&amp;gt; 辅助栈，以及最终返回值 string 类型的 res，初始化为空；&lt;/li&gt;
&lt;li&gt;[ 之前的数字可能不止一位，所以要设置一个 sum 表示其之前数字的总和；&lt;/li&gt;
&lt;li&gt;字符串题有大小写的区别，所以要注意大小写；&lt;/li&gt;
&lt;li&gt;可以结合代码中的注释理解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    string decodeString(string s) {
        stack&amp;lt;string&amp;gt; pos;
        stack&amp;lt;int&amp;gt; nums;
        string res = &quot;&quot;;
        int sum = 0;
        for(int i = 0; i &amp;lt; s.size(); i++)
        {
            if(isdigit(s[i]))
            //if(s[i] &amp;gt;= &apos;0&apos; &amp;amp;&amp;amp; s[i] &amp;lt;= &apos;9&apos;)
                sum = sum * 10 + s[i] - &apos;0&apos;;
            else if(isalpha(s[i]))
                //if((s[i] &amp;gt;= &apos;a&apos; &amp;amp;&amp;amp; s[i] &amp;lt;= &apos;z&apos;)||(s[i] &amp;gt;= &apos;A&apos; &amp;amp;&amp;amp; s[i] &amp;lt;= &apos;Z&apos;))
                res += s[i]; //注意大小写，之前第一次提交发现大小写也存在区分
            else if(s[i] == &apos;[&apos;)
            {
                nums.push(sum);
                sum = 0;    //重置 0
                pos.push(res);
                res = &quot;&quot;;   //重置为空字符串
            }
            else if(s[i] == &apos;]&apos;)   //此处举例3[a2[c]];
            {       //出现[后将3放入nums中，sum重置，res仍为&quot;&quot;
                sum = nums.top();  //而后再出现[，此时sum=2;res=&quot;a&quot;,存入pos
                nums.pop();    //第一次出现], 此时sum = nums.top() == 2
                while(sum)    //pos.top = a, res = &apos;c&apos;,输出为res = &quot;acc&quot;
                {      //第二次出现],sum = nums.top() == 3;
                    pos.top() += res;  //pos.top = &quot;&quot; 第一次存入的res, res =&quot;acc&quot;,
                    sum--;    //画图更好理解，sum经历变化后的值最终始终为0
                }
                res = pos.top();
                pos.pop();
            }
        }
        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：一次遍历 O(s.size)，其中的 while 循环应该是常数级；&lt;/li&gt;
&lt;li&gt;空间复杂度：O(s.size)。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 3 1381. 设计一个支持增量操作的栈</title><link>https://chaggle.github.io/posts/2021/09/12/day-03-1381-design-a-stack-with-increment-operation/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/12/day-03-1381-design-a-stack-with-increment-operation/</guid><pubDate>Sun, 12 Sep 2021 09:42:15 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/design-a-stack-with-increment-operation/&quot;&gt;1381. 设计一个支持增量操作的栈&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;请你设计一个支持下述操作的栈。

实现自定义栈类 CustomStack ：

CustomStack(int maxSize)：用 maxSize 初始化对象，maxSize 是栈中最多能容纳的元素数量

栈在增长到 maxSize 之后则不支持 push 操作。

void push(int x)：如果栈还未增长到 maxSize ，就将 x 添加到栈顶。

int pop()：弹出栈顶元素，并返回栈顶的值，或栈为空时返回 -1 。

void increment(int k, int val)：栈底的 k 个元素的值都增加 val

如果栈中元素总数小于 k ，则栈中的所有元素都增加 val 。


示例：

输入：
[&quot;CustomStack&quot;,&quot;push&quot;,&quot;push&quot;,&quot;push&quot;,
&quot;push&quot;,&quot;push&quot;,&quot;increment&quot;,&quot;increment&quot;,
&quot;pop&quot;,&quot;pop&quot;,&quot;pop&quot;,&quot;pop&quot;]
[[3],[1],[2],[2],[3],[4],[5,100],[2,100],[],[],[],[]]
输出：
[null,null,null,null,null,null,null,null,2,103,202,201]
解释：
CustomStack customStack = new CustomStack(3); // 栈是空的 []
customStack.push(1); // 栈变为 [1]
customStack.push(2); // 栈变为 [1, 2]
customStack.pop(); // 返回 2 --&amp;gt; 返回栈顶值 2，栈变为 [1]
customStack.push(2); // 栈变为 [1, 2]
customStack.push(3); // 栈变为 [1, 2, 3]
customStack.push(4); // 栈仍然是 [1, 2, 3]，不能添加其他元素使栈大小变为 4
customStack.increment(5, 100); // 栈变为 [101, 102, 103]
customStack.increment(2, 100); // 栈变为 [201, 202, 103]
customStack.pop(); // 返回 103 --&amp;gt; 返回栈顶值 103，栈变为 [201, 202]
customStack.pop(); // 返回 202 --&amp;gt; 返回栈顶值 202，栈变为 [201]
customStack.pop(); // 返回 201 --&amp;gt; 返回栈顶值 201，栈变为 []
customStack.pop(); // 返回 -1 --&amp;gt; 栈为空，返回 -1


提示：

1 &amp;lt;= maxSize &amp;lt;= 1000
1 &amp;lt;= x &amp;lt;= 1000
1 &amp;lt;= k &amp;lt;= 1000
0 &amp;lt;= val &amp;lt;= 100
每种方法 increment，push 以及 pop 分别最多调用 1000 次
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;创建一个动态数组 data，可以使用 C++ 中的 resize 函数重置数组容量，但反复动态扩容反而增大了时间的消耗；&lt;/li&gt;
&lt;li&gt;push、pop 都是经典的栈写法；&lt;/li&gt;
&lt;li&gt;increment 函数通过一次数组遍历即可完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class CustomStack {
private:
    vector&amp;lt;int&amp;gt; data;
    int top;

public:
    CustomStack(int maxSize)
    {
        data.resize(maxSize);
        top = -1;
    }

    void push(int x)
    {
        if (top != data.size() - 1)
        {
            data[++top] = x;
        }
    }

    int pop()
    {
        if (top == -1) return -1;
        else return data[top--];
    }

    void increment(int k, int val)
    {
        int minx = min(k, top + 1);
        for (int i = 0; i &amp;lt; minx; i++)
        {
            data[i] += val;
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;type CustomStack struct {
    top int
    cap int
    stack []int

}

func Constructor(maxSize int) CustomStack {
    return CustomStack {
        stack : make([]int, 0),
        cap : maxSize,
        top : 0,
    }
}

func (this *CustomStack) Push(x int)  {
    if this.top &amp;lt; this.cap {
        this.stack = append(this.stack, x)
        this.top++
    } 
}

func (this *CustomStack) Pop() int {
    if this.top == 0 {
        return -1
    }
    
    x := this.stack[this.top - 1]
    this.stack = this.stack[:this.top - 1]
    this.top--
    
    return x
}

func (this *CustomStack) Increment(k int, val int)  {
    if k &amp;gt; this.top {
        k = this.top
    }
    for i := 0; i &amp;lt; k; i++ {
        this.stack[i] += val
    }
}

/**
 * Your CustomStack object will be instantiated and called as such:
 * obj := Constructor(maxSize);
 * obj.Push(x);
 * param_2 := obj.Pop();
 * obj.Increment(k,val);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：push、pop 均为 $O(1)$，而 increment 为 $O(minx)$，go 中为 $O(K)$。&lt;/li&gt;
&lt;li&gt;空间复杂度：由创建的动态数组 data 的长度决定，即为 $O(maxSize)$。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 2 821. 字符的最短距离</title><link>https://chaggle.github.io/posts/2021/09/11/day-02-821-shortest-distance-to-a-character/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/11/day-02-821-shortest-distance-to-a-character/</guid><pubDate>Sat, 11 Sep 2021 06:27:35 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/shortest-distance-to-a-character/&quot;&gt;821. 字符的最短距离&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;给定一个字符串 S 和一个字符 C

返回一个代表字符串 S 中每个字符到字符串 S 中的字符 C 的最短距离的数组。

示例 1：

输入：s = &quot;loveleetcode&quot;, c = &quot;e&quot;

输出：[3,2,1,0,1,0,0,1,2,2,1,0]

解释：字符 &apos;e&apos; 出现在下标 3、5、6 和 11 处（下标从 0 开始计数）

距下标 0 最近的 &apos;e&apos; 出现在下标 3 ，所以距离为 abs(0 - 3) = 3

距下标 1 最近的 &apos;e&apos; 出现在下标 3 ，所以距离为 abs(1 - 3) = 2

对于下标 4 来说，出现在下标 3 和下标 5 处的 &apos;e&apos; 都离它最近。

但距离是一样的 abs(4 - 3) == abs(4 - 5) = 1

距下标 8 最近的 &apos;e&apos; 出现在下标 6 ，所以距离为 abs(8 - 6) = 2
示例 2：

输入：s = &quot;aaab&quot;, c = &quot;b&quot;
输出：[3,2,1,0]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;建立一个 vector，存储遍历字符串后等于输入字符 c 的位置下标；&lt;/li&gt;
&lt;li&gt;对于每一个位置，将其减去数组中的值，取绝对值，并返回其中最小的值；&lt;/li&gt;
&lt;li&gt;min(abs(i - pos[j]), abs(i - pos[j + 1])) 中会出现数组长度为一的特殊情况，此处的特殊情况之后会进一步优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int[] shortestToChar(String s, char c){
        // 双重for循环查找
        int n = s.length();
        int[] res = new int[n];
        Arrays.fill(res, n + 1);
        for (int i = 0, j = -1; i &amp;lt; n; i++) {
            if (s.charAt(i) == c) {
                j = i;
            }
            if (j != -1) {
                res[i] = i - j;
            }
        }
        for (int i = n - 1, j = -1; i &amp;gt;= 0; i--) {
            if (s.charAt(i) == c) {
                j = i;
            }
            if (j != -1) {
                res[i] = Math.min(res[i], j - i);
            }
        }
        return res;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; shortestToChar(string s, char c) {
        int n = s.size();
        vector&amp;lt;int&amp;gt; pos;
        vector&amp;lt;int&amp;gt; ans(n, n);

        for(int i = 0; i &amp;lt; n; i++)
        {
            if(s[i] == c) pos.push_back(i);
        }

        for(int i = 0; i &amp;lt; n; i++)
        {
            int tmp = 0;
            if(pos.size() != 1)
            {
                for(int j = 0; j &amp;lt; pos.size() - 1; j++)
                {
                    tmp = min(abs(i - pos[j]), abs(i - pos[j + 1]));
                    if(ans[i] &amp;gt; tmp) ans[i] = tmp;
                }
            }
            else
            {
                ans[i] = abs(i - pos[0]);
            }

        }
        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;func shortestToChar(s string, c byte) []int {
    n := len(s)

    ans := make([]int, n)
    pos := make([]int, 0)

    for i := 0; i &amp;lt; n; i++ {
        if s[i] == c {
            pos = append(pos, i)
        }
    }

    for i := 0; i &amp;lt; n; i++ {
        if s[i] != c {
            ans[i] = min(pos, i)
        } else {
            ans[i] = 0
        }
    }

    return ans
}

func min(num []int, n int) int {
    min := 10000 //因为最长为10000
    
    for i := 0; i &amp;lt; len(num); i++ {
        if min &amp;gt; abs(n - num[i]) {
            min = abs(n - num[i])
        }
    }

    return min
}

func abs(a int) int {
    if a &amp;lt; 0 {
        return -a
    }

    return a
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：O($n*k$)，n 是 s 的长度，k 是字符 c 在字符串中出现的次数，k &amp;lt;= n。&lt;/li&gt;
&lt;li&gt;空间复杂度：O(k)，k 为字符 c 出现的次数，这是记录字符 c 出现下标的辅助数组消耗的空间。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Day 1 989. 数组形式的整数加法</title><link>https://chaggle.github.io/posts/2021/09/10/day-01-989-add-to-array-form-of-integer/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2021/09/10/day-01-989-add-to-array-form-of-integer/</guid><pubDate>Fri, 10 Sep 2021 02:12:05 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;&lt;a href=&quot;https://leetcode-cn.com/problems/add-to-array-form-of-integer/&quot;&gt;989. 数组形式的整数加法&lt;/a&gt;&lt;/strong&gt;&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;对于非负整数 X 而言，X 的数组形式是每位数字按从左到右的顺序形成的数组。

例如，如果 X = 1231，那么其数组形式为 [1,2,3,1]。

给定非负整数 X 的数组形式 A，返回整数 X+K 的数组形式。

示例 1：

输入：A = [1,2,0,0], K = 34
输出：[1,2,3,4]
解释：1200 + 34 = 1234
示例 2：

输入：A = [2,7,4], K = 181
输出：[4,5,5]
解释：274 + 181 = 455
示例 3：

输入：A = [2,1,5], K = 806
输出：[1,0,2,1]
解释：215 + 806 = 1021
示例 4：

输入：A = [9,9,9,9,9,9,9,9,9,9], K = 1
输出：[1,0,0,0,0,0,0,0,0,0,0]
解释：9999999999 + 1 = 10000000000


提示：

1 &amp;lt;= A.length &amp;lt;= 10000
0 &amp;lt;= A[i] &amp;lt;= 9
0 &amp;lt;= K &amp;lt;= 10000
如果 A.length &amp;gt; 1，那么 A[0] != 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;题目思路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;建立一个动态数组 res，以位数的形式存储最后的结果值；&lt;/li&gt;
&lt;li&gt;既可以通过对 k 取 10 的模值进行加减，也可以从后往前与 k 相加，再对相加得到的值对 10 取模；&lt;/li&gt;
&lt;li&gt;如果 k 的位数大于数组所给的位数，那么循环内 k 除以 10 后余留的值必然大于 0；&lt;/li&gt;
&lt;li&gt;此时需要扩展数组，由于是动态数组，直接在循环内改写为类似 k = sum / 10 的形式，即 k /= 10，直到 k 为 0 为止；&lt;/li&gt;
&lt;li&gt;最后将数组逆序输出即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题目代码&lt;/h2&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public List&amp;lt;Integer&amp;gt; addToArrayForm(int[] num, int k) {
        List&amp;lt;Integer&amp;gt; res = new ArrayList&amp;lt;&amp;gt;();
        int n = num.length;
        for(int i = num.length - 1; i &amp;gt;= 0; i--) {
            int sum = num[i] + k % 10;
            k /= 10;
            if(sum &amp;gt;= 10) {
                k++;
                sum -= 10;
            }
            res.add(sum);
        }
        for(; k &amp;gt; 0; k /= 10) {
            res.add(k % 10);
        }
        // Collections.reverse 源码中会判断对存储的数据结构进行判断，如果支持随机存储的数据结构，默认使用的是 swap 交换元素，而如果是链表，那么默认使用的是头插法进行逆序
        Collections.reverse(res);
        return res;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; addToArrayForm(vector&amp;lt;int&amp;gt;&amp;amp; nums, int k) {
        vector&amp;lt;int&amp;gt; res;

        for(int i = nums.size() - 1; i &amp;gt;= 0; i--)
        {
            int sum = nums[i] + k;
            res.push_back(sum % 10);
            k = sum / 10;
        }

        while(k &amp;gt; 0)
        {
            res.push_back(k % 10);
            k /= 10;
        }

        reverse(res.begin(), res.end());

        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;func addToArrayForm(num []int, k int) []int {
    n := len(num)
    ans := make([]int, 0)

    for i := n - 1; i &amp;gt;= 0; i-- {
        v := num[i] + k;
        ans = append(ans, v % 10)
        k = v / 10

    }

    for k &amp;gt; 0 {
        ans = append(ans, k % 10)
        k = k / 10
    }
    
    return reverse(ans)
}

func reverse(num []int) []int {
    for i, j := 0, len(num) - 1; i &amp;lt;= j; {
        num[i], num[j] = num[j], num[i]
        i++
        j--
    }
    
    return num;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;空间复杂度：申请了一个常数级数组，故空间为 O(1)&lt;/li&gt;
&lt;li&gt;时间复杂度：$O(max(n, \log k))$，其中 n 为数组的长度。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>C++ 面试知识点总结</title><link>https://chaggle.github.io/posts/2020/03/24/cpp-interview-notes/</link><guid isPermaLink="true">https://chaggle.github.io/posts/2020/03/24/cpp-interview-notes/</guid><pubDate>Tue, 24 Mar 2020 15:53:11 GMT</pubDate><content:encoded>&lt;p&gt;记录刷题的细节内容，算法与 C++ 特性（等之后有空再优化）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;static 定义静态局部变量特点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在全局数据区分配内存&lt;/li&gt;
&lt;li&gt;首次初始化执行后，之后函数调用不再初始化&lt;/li&gt;
&lt;li&gt;无显式初始化时，自动初始化为 0&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;局部变量、全局变量与动态数据的内存区域&lt;/h2&gt;
&lt;p&gt;局部变量存在于堆栈中，全局变量存在于静态区中，动态申请的数据存在于堆中。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;模板&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename 数据类型参数标识符&amp;gt;(typename 和 class 没区别)
&amp;lt;返回类型&amp;gt;&amp;lt;函数名&amp;gt;(参数表)
{
    函数体
}
模板形参不能为空。一旦声明了模板函数就可以用模板函数的形参名
声明类中的成员变量和成员函数，即可以在该函数中使用内置类型的地方
都可以使用模板形参名
example:
template&amp;lt;class T&amp;gt; struct{
 static void foo(T op1, T op2){
        cout&amp;lt;&amp;lt;op1&amp;lt;&amp;lt;op2;
    }
};sum &amp;lt;int&amp;gt;::foo(1,3)   //一个完整的例子
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;C++ 虚函数概念&lt;/h2&gt;
&lt;p&gt;C++ 中的虚函数的作用主要是实现了多态的机制。基类定义虚函数，子类可以重写该函数。&lt;/p&gt;
&lt;p&gt;当子类重新定义了父类的虚函数后，&lt;strong&gt;当父类的指针指向子类对象的地址时&lt;/strong&gt;，即 &lt;code&gt;B b; A *a = &amp;amp;b;&lt;/code&gt;，父类指针根据赋给它的不同子类指针，动态的调用子类的该函数，而不是父类的函数。而如果使用了 &lt;strong&gt;virtual&lt;/strong&gt; 关键字，程序将根据引用或指针指向的&lt;strong&gt;对象类型&lt;/strong&gt;来选择方法，否则使用&lt;strong&gt;引用类型或指针类型&lt;/strong&gt;来选择方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B b;
A *pa;
pa = &amp;amp;b;
A *pa2 = new A;

pa-&amp;gt;FuncA();  // (3) pa=&amp;amp;b 动态绑定，但是 FuncA 不是虚函数，所以 FuncA called
pa-&amp;gt;FuncB();  // (4) FuncB 是虚函数，所以调用 B 中 FuncB，FuncBB called
pa2-&amp;gt;FuncA(); // (5) pa2 是 A 类指针，不涉及虚函数，调用的都是 A 中函数，所以输出 FuncA called FuncB called
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;vector 的函数 erase()&lt;/h2&gt;
&lt;p&gt;一次只删除一个元素，返回一个迭代器指针，指向下一个元素。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;默认只读模式打开文件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ios::app&lt;/code&gt; 和 &lt;code&gt;ios::app | ios::out&lt;/code&gt; 是一个意思，只能输出不能输入。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;数组的定义区别理解&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int *s[8];
   定义一个指针数组，该数组中每个元素是一个指针，
   每个指针指向哪里就需要程序中后续再定义了。
int (*s)[8];
   定义一个数组指针，该指针指向含8个元素的一维数组（数组中每个元素是int型）。

区分int *p[n]; 和int (*p)[n];
就要看运算符的优先级了。
int *p[n]; 中，运算符[ ]优先级高，先与p结合成为一个数组，
再由int*说明这是一个整型指针数组。
int (*p)[n]; 中( )优先级高，
首先说明p是一个指针，指向一个整型的一维数组。
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;函数模板&lt;/h2&gt;
&lt;p&gt;函数模板必须由编译器根据程序员的调用类型实例化为可执行的函数。&lt;/p&gt;
&lt;p&gt;类模板的成员函数都是函数模板。&lt;/p&gt;
&lt;p&gt;没使用过的成员函数（即函数模板）不会被实例化。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;双目运算符的判断机制&lt;/h2&gt;
&lt;p&gt;&amp;amp;&amp;amp; 和 || 的判断机制：A&amp;amp;&amp;amp;B，若 A 为假，直接返回 false，B 不执行；若 A 为 true，继续执行 B。A||B，若 A 为 true，直接返回 true，B 不执行。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;C++ 中的 switch&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;switch 后面的&quot;表达式&quot;，可以是 int、char 和枚举型中的一种，不能是 float 型变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;case 后面必须是&quot;常量表达式&quot;，表达式中不能包含变量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;switch 后的表达式加了分号；非法&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;unsigned&lt;/h2&gt;
&lt;p&gt;unsigned 为无符号整数，取值范围为 0-255，永远不可能小于零，最好不要做控制条件。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;派生类继承编译&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;派生类继承（public, private）基类成员后，
继承的基类成员在派生类中的相应权限的改变。
 public  继承,基类的各类（public, protected, private）成员
 在派生类中权限与原来在基类时保持一致;
 private 继承,基类的各类成员在派生类中权限均变为private.
    故C类中的print调用A::print()，
    因为A::print()此时在C类中为private等级,
    所以不能被C类任何成员所访问，故不能通过编译
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;引用&lt;/h2&gt;
&lt;p&gt;引用是一个对象的别名，引用类型引用（refer to）另外一种类型。如 &lt;code&gt;int &amp;amp;refVal = val;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引用必须初始化&lt;/li&gt;
&lt;li&gt;引用与其初始值是**绑定（bind）**在一起的，而不是拷贝&lt;/li&gt;
&lt;li&gt;值传递以拷贝实参的方式，传递给形参&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;C++ STL 的 string 库与 C 风格字符串&lt;/h2&gt;
&lt;p&gt;C++ STL（Standard Template Library）string 库中的风格比 C 风格的字符串更加安全、高效。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;while 与 for&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;while：&lt;/strong&gt; 为不确定迭代的次数时使用，例如读取数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;for：&lt;/strong&gt; 确定范围时使用，也可以省略初始值、条件以及表达式&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;异常问题&lt;/h2&gt;
&lt;p&gt;throw、try、异常块；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;throw：&lt;/strong&gt; 异常表达式表示其遇到无法处理的问题，引发（raise）的异常&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;try：&lt;/strong&gt; try 和一个或多个 catch 块配合使用，一般的 try 抛出的某个异常会被某个 catch 捕获处理，故 catch 称为异常处理代码。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;class 与 struct 区别&lt;/h2&gt;
&lt;p&gt;在于访问权限&lt;/p&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt; 第一个访问说明符之前的成员为 &lt;code&gt;private&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;struct&lt;/code&gt; 第一个访问说明符之前的成员为 &lt;code&gt;public&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;使用非成员版本的 swap 是一个好习惯&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;swap(c1, c2)&lt;/code&gt; 与 &lt;code&gt;c1.swap(c2)&lt;/code&gt; 等价，但最好使用前者。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;泛型算法&lt;/h2&gt;
&lt;p&gt;头文件 &lt;code&gt;#include &amp;lt;algorithm&amp;gt;&lt;/code&gt; 和 &lt;code&gt;#include &amp;lt;numeric&amp;gt;&lt;/code&gt; 中，标准库提供了超过 100 个算法，所以说阅读源代码非常重要。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;copy (ilst.begin(), ilst.end(), back_inserter(ivec))
   copy函数

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;关联容器（hash、map、set）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt;：&lt;code&gt;map&amp;lt;string, int&amp;gt; word_count = {{&quot;a&quot;, 1}, {&quot;b&quot;, 2}};&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set&lt;/code&gt;：&lt;code&gt;set&amp;lt;string&amp;gt; exclude = {&quot;the&quot;, &quot;a&quot;};&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;unordered_map（hash 组织 map）&lt;/li&gt;
&lt;li&gt;unordered_set（hash 组织 set）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;字符指针&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;char *str[3] = {&quot;stra&quot;,&quot;strb&quot;,&quot;strc&quot;};&lt;/code&gt; 是一个指针数组；&lt;code&gt;char *p = str[0]&lt;/code&gt;；实际上 p 指向第一个字符串，所以字符串加 1，则指向字符串的下一个字母，而并非是下一个字符串。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;static 与非 static、全局变量与局部变量&lt;/h2&gt;
&lt;p&gt;无论是 static 还是非 static 的全局变量，如果不加限制随意访问的话，容易出现同步问题。&lt;/p&gt;
&lt;p&gt;无论是 static 还是非 static 的局部变量，每个线程都是私有的，其他线程不会对其进行干扰。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;C/C++ 中 main 函数&lt;/h2&gt;
&lt;p&gt;main 函数是 C/C++ 程序的入口函数，为必须存在的函数。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;throw、catch、try&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;throw 是抛出异常的关键字，try 是尝试执行可能有异常代码的关键字，catch 是捕获异常的关键字&lt;/li&gt;
&lt;li&gt;可能抛出异常的代码块都应该放在 try 代码块中&lt;/li&gt;
&lt;li&gt;catch 捕获相应的异常，可以有多级 catch 代码块来捕获不同级别的异常&lt;/li&gt;
&lt;li&gt;被捕获的异常可以再次抛出&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;线程&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;线程使用了 &lt;code&gt;wait&lt;/code&gt; 方法，会强行打断当前操作，进入阻塞（暂停）状态，然后需要 &lt;code&gt;notify&lt;/code&gt; 方法或 &lt;code&gt;notifyAll&lt;/code&gt; 方法才能进入就绪状态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;枚举变量&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;枚举变量是全局变量的情况下，枚举值的缺省值是 0，不是枚举的第一个值。其他情况，其值是不定的，而且不限定于所列出的枚举值。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;全局变量时初始化为 0，局部变量时初始化为随机值。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;unsigned 指针运算&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
移动的1代表的是一个单位量
    unsigned
p1+5=p1+5*1=p1+5*sizeof(unsigned char)
=p1+5*1=0x801000+0x5=0x801005
p2+5=p2+5*1=p2+5*sizeof(unsigned long)
=p2+5*4=0x810000+20=0x810000+0x14=0x810014
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;数组、指针、字符串的长度计算&lt;/h2&gt;
&lt;p&gt;对字符串进行 &lt;code&gt;sizeof&lt;/code&gt; 操作的时候，会把字符串的结束符 &lt;code&gt;\0&lt;/code&gt; 计算进去的。&lt;/p&gt;
&lt;p&gt;进行 &lt;code&gt;strlen&lt;/code&gt; 操作求字符串的长度的时候，不计算 &lt;code&gt;\0&lt;/code&gt; 的。&lt;/p&gt;
&lt;p&gt;数组作为函数参数传递的时候，已经退化为指针了，&lt;code&gt;Func&lt;/code&gt; 函数的参数 &lt;code&gt;str_arg&lt;/code&gt; 只是表示一个指针，那个 &lt;code&gt;str_arg[]&lt;/code&gt; 括号中的数值不起任何作用。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;强制类型转换后指针的类型&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;char str[] = &quot;glad to test something&quot;;
char *p = str;
p++;
int *p1 = reinterpret_cast&amp;lt;int *&amp;gt;(p);
p1++;
p = reinterpret_cast&amp;lt;char *&amp;gt;(p1);
printf(&quot;result is %s\n&quot;, p);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;p 的类型为 &lt;code&gt;char *&lt;/code&gt;，&lt;code&gt;p++&lt;/code&gt; 后 &lt;code&gt;p&lt;/code&gt; 指向 &lt;code&gt;str&lt;/code&gt; 数组的第 2 个元素即字母 &quot;l&quot; 的位置。&lt;/p&gt;
&lt;p&gt;p1 的类型为 &lt;code&gt;int *&lt;/code&gt;，&lt;code&gt;p1++&lt;/code&gt; 后 &lt;code&gt;p1&lt;/code&gt; 指向的位置增加 4 个字节，指向 &lt;code&gt;str&lt;/code&gt; 数组中的第 6 个元素即字母 &lt;code&gt;t&lt;/code&gt; 的位置。&lt;/p&gt;
&lt;p&gt;因此最后 p 的内容为 &lt;code&gt;to test something&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;类&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在类中，如果为空类，则类占用 1 个字节&lt;/li&gt;
&lt;li&gt;一旦类中有其他的占用空间成员，则这 1 个字节就不在计算之内&lt;/li&gt;
&lt;li&gt;一个类只有一 &lt;code&gt;int&lt;/code&gt; 则占用 4 字节而不是 5 字节&lt;/li&gt;
&lt;li&gt;如果只有成员函数，则还是只占用 1 个字节，因为类函数不占用空间&lt;/li&gt;
&lt;li&gt;但是由于虚函数存在一个虚函数表，需要 4 个字节，数据成员对象如果为指针则为 4 字节，注意有字节对齐，如果为 13 字节，则进位到 16 字节空间&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;buffer&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;函数char *myString()中没有使用new或者malloc分配内存，
所有buffer数组的内存区域在栈区
随着char *myString()的结束，栈区内存释放，字符数组也就不存在了，
所以会产生野指针，输出结果未知
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;动态分配在堆区&lt;/h2&gt;
&lt;p&gt;动态分配在堆区，其他的均不在堆区。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;操作系统中用户态切换到内核态的 3 种方式&lt;/h2&gt;
&lt;p&gt;a. 系统调用&lt;/p&gt;
&lt;p&gt;b. 异常&lt;/p&gt;
&lt;p&gt;c. 外围设备的中断&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;函数嵌套&lt;/h2&gt;
&lt;p&gt;若有如下函数定义，这种嵌套定义根本算不出结果，所以函数嵌套定义肯定不行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int f(int a)={return a+f1(a);}
int f1(int b)={return b+f(b);}
无法算出结果故定义嵌套不使用
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;静态变量初始化&lt;/h2&gt;
&lt;p&gt;在 C++ 中，类的静态成员（static member）必须在类内声明，在类外初始化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A
{
private: static int count ; // 类内声明
};
int A::count = 0 ; // 类外初始化，不必再加static关键字
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么？因为静态成员属于整个类，而不属于某个对象，如果在类内初始化，会导致每个对象都包含该静态成员，这是矛盾的。&lt;/p&gt;
&lt;p&gt;能在类中初始化的成员只有一种，那就是静态常量成员。&lt;/p&gt;
&lt;p&gt;这样不行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A
{
  private: static int count = 0;
  静态成员不能在类内初始化
};

class A
{
  private: const int count = 0;
   常量成员也不能在类内初始化
};

class A
{
  private: static const int count = 0;
   静态常量成员可以在类内初始化
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>