ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

M1装HBase屡屡失败?X86环境五分钟跑通,附LeetCode 865最小子树解法

M1装HBase屡屡失败?X86环境五分钟跑通,附LeetCode 865最小子树解法 周五傍晚公司团建烤盘上的五花肉滋滋冒油。我一边夹肉一边刷手机技术群里的消息一条接一条最后同事直接点名“小黑你那个 mac m1 到底装上 hbase 了没”说实话我一时不知道怎么回——我这一天试遍了网上能找到的 M1 安装 HBase 教程全都在最后一两步翻车。隔壁桌同事看我不对劲指了指他那台联想拯救者“别浪费公司烤肉的氛围了吃完回去用我电脑试试。”于是就有了这篇特别的记录在烤肉局结束后我用一台普通的拯救者笔记本五分钟左右装好了 HBase晚上回家顺手把 LeetCode 865 题做出来题目正好是二叉树里“包含所有最深节点的最小子树”。这篇博文就把这段经历拆开讲清楚——M1 装 HBase 到底卡在哪、X86 环境为什么一把过以及 865 题的最小子树到底怎么理解。如果你也在被大数据组件的环境问题折磨或者正在刷树的 LCA 类题目这篇文章应该对你有用。1. 从烤肉桌到终端一次团建引发的环境折腾1.1 烤肉局上的技术债我们公司的团建氛围比较随意没有拉横幅喊口号就是找个烤肉店围成一桌开吃。但吃着吃着话题总会滑向工作谁负责的接口又报警了谁那边数据同步延迟了半个小时。我坐在角落里本想安静吃两口结果被问到了 HBase 的进度。事情是这样的我们最近在做一个用户行为分析的小项目想把点击流数据落到 HBase 里做个本地 demo验证一下列式存储的读写速度。听起来不复杂——HBase 只是个大数据库软件下载、解压、改配置、启动应该半小时搞定。我本来是这么想的直到我掏出自己的 MacBook ProM1 芯片一切就变味了。我大概花了一个下午加半个晚上在 M1 上反复尝试安装 HBase最后状态还是“起不来”。烤肉局上我一边夹肉一边心里还在复盘那些报错信息就像嘴里吃着肉脑子里跑着日志。1.2 为什么非装 HBase 不可先给没接触过的朋友一个背景。HBase 是 Apache 旗下的分布式列式存储系统定位是海量数据下的随机读写。它和 MySQL 这类关系型数据库不一样更适合存那种“几亿行、字段可以动态变化、按行键查询”的数据。我们项目里选它是因为行为日志字段经常调整用关系表不好整列式存储天然宽松。在真正的分布式生产环境里HBase 会跑在一个 HDFS 集群上配合 Zookeeper 做协调。但本地验证根本不需要搭集群HBase 支持 standalone 模式——单机启动数据和 WAL 都写到本地目录进程起来就能用。听起来是不是很简单我也这么认为。偏偏 M1 芯片给了我一个下马威。2. Mac M1 装 HBase一场硬件和软件之间的拉扯2.1 病根ARM 架构和 JNI 原生库先说结论M1 装不上 HBase不是因为你操作不对而是这个组合本身就充满摩擦。HBase 本身是 Java 写的理论上 Java 是跨平台的M1 上装一个 JDK 再跑 HBase 也不是不行。问题出在 HBase 依赖的 Hadoop 生态上。Hadoop 在做本地文件 IO、压缩解压这些操作时会通过 JNI 去加载一堆原生库比如 libhadoop.dylib、snappy、zlib 等。这些原生库是按编译时的 CPU 架构生成的官方发布的二进制包绝大多数是给 x86_64 架构准备的。M1 是 ARM64 架构Java 字节码可以跨架构运行但 JNI 加载的 C/C 动态库不行。你在 M1 上启动 HBase经常看到类似这样的日志Unable to load Apache Hadoop native library java.lang.UnsatisfiedLinkError: no nativehadoop in java.library.path然后 HMaster 进程直接退出或者卡在初始化阶段一动不动。这个场景特别像开车时导航说“请掉头”但你明明在一条禁转弯的路上。JVM 能理解这个 jar 包里的字节码但底层一碰原生库就崩。整个过程让我一度怀疑是不是下载错了版本后来才发现纯粹是架构不兼容。2.2 我实际踩过的四个坑我在 M1 上反复试了很多次把遇到的典型问题整理成下面这个表格如果你也在 M1 上折腾 HBase可以直接对照着排查。现象根因我的处理结果HMaster 启动几秒后退出M1 上用了 arm64 版 JDK加载 x86 原生库失败换 x86 版 JDK 配 Rosetta 后不再直接退出但后续又出新问题日志频繁出现 no nativehadoopHadoop native library 架构不匹配无解装什么都不稳定自带 Zookeeper 一直抢 2181 端口本地服务占用端口内置 ZK 起不来换端口后 HMaster 又连不上问题连环用 Homebrew 安装的 HBase 缺一堆依赖brew 打包的版本和本地环境耦合太多直接放弃 brew改从官网下二进制包结果还是卡在架构上细说第二个坑。网上有些教程会让你从源码编译 Hadoop 原生库或者在命令行加参数跳过 native library 检测。我试了确实能跳过警告但 HBase 执行某些文件操作时会突然异常就像一颗埋在后续步骤里的雷你永远不知道什么时候爆。第三个坑也很有代表性。HBase 自带的 Zookeeper 默认监听 2181我电脑上刚好有个开发服务占着这个端口结果 HMaster 一直卡在initializing状态。你以为它在启动其实它在傻等一个连不上的节点。把端口改成 2182 之后又牵扯出一堆配置联动问题越改越乱。2.3 网上攻略的“妥协方案”为什么没用为了在 M1 上把 HBase 跑起来我几乎翻遍了能找到的教程。网上常见的方案有三种我都试过各有各的坑。第一种是用 Rosetta 2 模拟 x86 环境。思路是让 M1 在底层翻译执行 x86 指令再配一个 x86 版的 JDK 8。这套组合确实让 HBase 启动进度往前推了一点但问题是 HBase 的启动脚本里有大量环境变量和路径判断在 Rosetta 终端和原生终端之间来回切换时PATH、JAVA_HOME 特别容易错乱。而且 Rosetta 本身也是一层性能损耗启动个 HBase 像开虚拟机一样慢。第二种是用 Docker 跑 HBase 镜像。理论上你可以拉一个现成的镜像一条命令启动容器但 M1 上的 Docker Desktop 默认拉的是 arm64 架构镜像很多 HBase 镜像只发布了 amd64 版本。强行跑 amd64 镜像又回到“模拟”的老路而且端口映射、数据卷挂载、容器内网络配置每一个环节都可能出幺蛾子。第三种是装虚拟机比如 Parallels 里面开一个 Ubuntu。但 M1 上的 Parallels 也只能装 ARM 版 UbuntuARM 版 Ubuntu 里跑 HBase 原生库本质上和 M1 直接跑没区别。除非你在虚拟机里再套一层 x86 模拟那性能完全是灾难。有同事甚至建议我直接重装系统我当时沉默了——重装系统解决不了芯片架构的问题。3. 拯救者笔记本X86 环境一次跑通3.1 安装前的环境准备清单烤肉局结束后我拿着同事的拯救者笔记本回到工位。这台电脑装的是 Windows里面开了 WSL2跑着 Ubuntu 22.04对我来说刚好够用。说句实话看到拯救者的那一刻我第一反应是“X86 环境终于能稳定装了”。HBase 的二进制包官方就是面向 X86 的不用绕任何模拟层装起来自然轻松。先确认环境顺序很重要缺一个后面都会报错Java 环境。HBase 2.x 对 JDK 版本有要求JDK 8 和 JDK 11 是主流支持版本。先用java -version确认我这边显示的是 OpenJDK 1.8。解压目录。我习惯把软件统一放到/opt下面后面配环境变量和权限都方便。SSH 免密。这个在真正的集群部署时是必需的本地伪分布式也需要所以一并配好。具体执行命令java -version # 输出 openjdk version 1.8.0_362 这种就对了 cd /opt sudo tar -zxf hbase-2.4.17-bin.tar.gz sudo mv hbase-2.4.17 /opt/hbase下载 HBase 二进制包时建议去官网下载页面选-bin.tar.gz结尾的包不要拿 src 源码包回来手动编译。源码包是为了在特殊架构上二次编译用的X86 环境直接拿编译好的二进制包最省事。3.2 配置文件和启动步骤环境变量是第一个要改的地方。编辑~/.bashrc或者~/.profile追加export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 export HBASE_HOME/opt/hbase export PATH$PATH:$HBASE_HOME/bin注意JAVA_HOME一定要写具体路径不要写成export JAVA_HOME$(dirname $(dirname $(readlink -f $(which java))))这种花活后面 HBase 脚本解析时会很痛苦。然后是 HBase 自己的配置。进到/opt/hbase/conf目录先改hbase-env.shexport JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 export HBASE_MANAGES_ZKtrueHBASE_MANAGES_ZKtrue表示用 HBase 自带的 Zookeeper本地单机测试只要你不打算连外部 ZK就别改成 false。再改hbase-site.xml把核心配置放进configuration标签里configuration property namehbase.rootdir/name valuefile:///opt/hbase/data/value /property property namehbase.zookeeper.property.dataDir/name value/opt/hbase/zookeeper/value /property property namehbase.cluster.distributed/name valuefalse/value /property property namehbase.unsafe.stream.capability.enforce/name valuefalse/value /property /configuration逐个解释一下。hbase.rootdir是数据存放根目录standalone 模式下用file://指向本地路径就行不需要 HDFS。hbase.zookeeper.property.dataDir是 ZK 数据目录。hbase.cluster.distributedfalse表示单机非分布式模式。最后一个hbase.unsafe.stream.capability.enforcefalse是很多新版本在本地文件系统上跑时会遇到“文件流能力检查不通过”的问题直接关掉省心。配置完成后启动start-hbase.sh等几秒钟用jps检查进程。如果看到HMaster和HQuorumPeer说明基本起来了。接着进 HBase Shell 验证hbaseshell在 shell 里输入status liststatus会显示当前集群状态list会列出所有表初始应该是空。看到这两条命令正常返回这个本地 HBase 就算真的装好了。这个过程在拯救者上确实只花了五分钟左右。没有编译没有模拟层没有莫名其妙的 JNI 报错就是下载、解压、改配置、启动一气呵成。3.3 启动问题速查表和 WAL 路径小坑虽然 X86 环境顺利但也不是完全没问题。我在启动过程中遇到过一次 HMaster 起不来的情况日志里显示卡在初始化。我用这个速查表快速定位了一下顺手整理出来给大家参考。现象常见原因解决办法HMaster 一直 initializingrootdir 目录没有写权限或 ZK 端口被占检查/opt/hbase/data权限用 netstat -tlnp访问 16010 打不开 Web UIHBase 绑定地址是 localhost或者防火墙拦截确认访问地址是 http://localhost:16010WSL2 环境注意走 Windows 浏览器时 IP 映射日志提示 Cant find native library缺少 hadoop native lib本地测试可以忽略HBase 会回退到 Java 实现启动后 WAL 路径异常之前非正常关闭残留 WAL 文件损坏备份后清理hbase.rootdir下的WALs目录再重启这里要重点说一下 WAL。HBase 写入数据时会先写一份预写日志Write-Ahead Log保证 RegionServer 宕机后数据能恢复。这份日志默认放在hbase.rootdir/WALs目录下每个 Region 对应一个文件。如果之前的进程没有正常关闭再次启动时可能会因为 WAL 文件状态不一致而报错。本地测试环境最简单粗暴的办法是先备份再清理真实生产环境千万别轻易删 WAL那等于丢数据。这个知识点面试也常考后面第 5 节我再补几句。另外 HBase 的端口清单也值得留个印象HMaster Web UI 默认 16010RegionServer 默认 16020如果你配了额外信息页可能是 16030Zookeeper 端口默认 2181。遇到连不上先对照这三个端口检查比自己瞎猜快得多。4. LeetCode 865二叉树里的“最小包圆”问题4.1 先看懂题目在问什么晚上回到家我终于有空打开 LeetCode。每天刷题的节奏不能断今天轮到的是 865 题具有所有最深节点的最小子树。题目是这样的给你一棵二叉树的根节点root每个节点都有一个深度值根节点深度为 0它的子节点深度为 1以此类推。深度最大的节点叫做“最深节点”。现在让你找到一棵子树要求这棵子树包含这棵树里所有的最深节点并且它的根节点尽量深也就是这棵子树尽可能“小”。理解“最小子树”是关键。它不是指节点数最少而是指子树的根节点深度要尽可能大。更直白地说你要找的是所有最深节点往上汇聚的第一个公共祖先节点以这个节点为根的子树就是答案。如果最深节点只有一个那答案就是这个节点自己。举个例子3 / \ 5 1 / \ / \ 6 2 0 8 / \ 7 4这棵树最大深度是 3深度为 3 的节点有 7 和 4它们是两个“最深节点”。包含 7 和 4 的最小子树的根节点是谁是 2。因为 2 是 7 和 4 的最近公共祖先LCA向上到 5 也能包含这两个节点但 5 太“大”了它的子树里还包括 6而 6 不是最深节点所以 5 不符合“最小”的要求。正确返回的是以 2 为根的子树。4.2 解法一一次递归同时返回子树和深度这道题最优雅的解法是自底向上的后序遍历。对每个节点我都要知道两件事以它为根的子树里所有最深节点的最小子树根节点是什么以及这棵子树的最大深度是多少。递归函数设计成这样返回(节点, 深度)。节点代表当前子树里满足题意的子树根节点深度代表当前子树的整体高度。Python 代码如下# Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right right class Solution: def subtreeWithAllDeepest(self, root: TreeNode) - TreeNode: def dfs(node): # 空节点没有最深节点深度记为 0 if not node: return None, 0 left_node, left_depth dfs(node.left) right_node, right_depth dfs(node.right) # 最深节点全在左子树答案跟着左子树走 if left_depth right_depth: return left_node, left_depth 1 # 最深节点全在右子树答案跟着右子树走 if right_depth left_depth: return right_node, right_depth 1 # 左右子树深度一样说明当前节点就是所有最深节点的交点 return node, left_depth 1 lca, _ dfs(root) return lca递归逻辑其实就三条路第一如果左子树更深说明所有最深节点都在左子树里当前节点的右子树里没有最深节点所以答案不可能是当前节点直接沿用左子树返回的子问题答案深度加 1 再返回给上层。第二右子树更深的情况对称同理。第三如果左右子树的最大深度相等这说明最深节点同时出现在左右两侧此时只有当前节点能“盖住”所有最深节点当前节点就是答案。这个解法时间复杂度 O(N)每个节点只遍历一次空间复杂度 O(H)H 是树的高度递归栈的深度。我刷题时第一次写就是这个思路写完提交一次通过。为什么对因为它本质上是在用深度差来判断“最深节点往哪边聚拢”一旦两边深度相等就说明交汇点到了。4.3 解法二先找所有最深叶子再求它们的最近公共祖先如果你觉得解法一的返回值设计有点绕可以从另一个角度理解这不就是“求所有最深节点的最近公共祖先”嘛。只要我确定了最深节点有哪些然后求它们的 LCA 就行。常规做法是先遍历一遍树算出每个节点深度得到最大深度max_depth。然后从根节点出发根据左右子树中最大深度的分布不断下探如果左子树能达到max_depth而右子树不能说明最深节点都在左子树答案在左子树里。如果右子树能达到max_depth而左子树不能答案在右子树里。如果左右子树都能达到max_depth说明左右两棵子树都有最深节点当前节点就是它们的最近公共祖先。代码实现class Solution: def subtreeWithAllDeepest(self, root: TreeNode) - TreeNode: def depth(node): if not node: return 0 return 1 max(depth(node.left), depth(node.right)) def find(node): if not node: return node left_depth depth(node.left) right_depth depth(node.right) if left_depth right_depth: return node return find(node.left) if left_depth right_depth else find(node.right) return find(root)这个写法更好懂但有明显的性能问题每一层递归都要重新算一次子树深度最坏情况下会退化成 O(N^2)。如果只是解题还能接受如果想追求效率可以对depth做记忆化或者直接改用解法一。面试时如果能先说这个直观思路再优化到单次递归版会显得你思考过两个层次。4.4 边界情况、变形题和刷题延伸边界情况主要在三个地方。空树时题目默认 root 非空但你写代码还是要判空。只有一个节点时左右子树的深度都是 0相等答案就是它自己。整棵树是一条链时最深节点只有一个答案会从根一路下探到最深的叶子。处理这种自底向上的树题有一个通用心法不要只想着“我要返回什么答案”而要想着“我为了让上层知道答案需要返回哪些信息”。865 要的信息是“子树根节点”和“高度”于是递归函数就返回这两个值。以后遇到类似的树题比如 543 二叉树的直径、124 二叉树中的最大路径和思路完全一样。顺便再说一句LeetCode 上很多树题是互相联系的。比如 104 求最大深度236 求两个节点的最近公共祖先1123 最深叶节点的最近公共祖先跟 865 就是同一道题只是名字不同。还有 994 腐烂的橘子是 BFS 层序遍历的经典题073/875 爱吃香蕉的狒狒则是二分答案题。如果你在刷题计划里看到这些题建议放在一起对照着刷比单独刷一道效果好得多。5. 从 HBase 到 LeetCode 的实战心得5.1 环境问题排查三板斧折腾完 M1 和拯救者我最大的感受是环境问题看起来千奇百怪但排查思路其实就三板斧。第一板斧是看日志不要看报错弹窗。HBase 每个组件的日志都写在$HBASE_HOME/logs/目录下比如hbase-hbase-master-xxx.log。报错信息只告诉你“我挂了”日志才告诉你“我为什么挂”。我在 M1 上要是早点打开这个日志早该看到 nativehadoop 加载失败而不是在启动脚本里反复折腾。第二板斧是确认架构和版本是否匹配。Java 版本、芯片架构、二进制包平台这三者必须对齐。M1 上装 x86 的 HBase 包就跟把柴油加进汽油车一样再好的车也跑不起来。换成 X86 环境万事顺手。第三板斧是查端口、目录、权限。master initialing八成是 ZK 端口或数据目录问题Connection refused八成是端口没监听Permission denied就是目录权限。这三类问题占了环境故障的一大半先排查它们能省很多时间。我把这三个建议也写成一句话送给自己当作备忘录先日志后版本再端口目录权限。你按这个顺序走基本不会走偏。5.2 刷题和搞项目怎么互相成就有人可能会问装 HBase 和刷 LeetCode 有什么关系说实话关系不在于“技术栈相同”而在于两件事都在锻炼同一种能力——在复杂系统里找最小解。装 HBase 的时候你要在 M1 的限制、原生库的报错、端口冲突之间找到一条可行路径。刷 865 题的时候你要在左子树、右子树、深度差之间找到最小的包圆子树。两者都是典型的“在约束条件下找答案”。我还有一个具体体会是递归不只是写在题解里的花架子。HBase 启动失败时我习惯从最底层进程一层层往上查每个组件的状态判断就像递归函数里的 if 分支。刷过树这类题之后再排查这种进程树、服务依赖链思路会清晰很多。另外在 HBase 面试题里WAL 几乎是必问项。它解决的问题是“写内存的时候宕机了数据怎么恢复”本质也是日志先行。这次实际操作中我亲手清过 WAL 目录、看过日志恢复流程再被问到 WAL 机制时心里就有底多了。环境踩坑虽然烦但踩完往往比背十遍文档记得牢。现在回想这次团建最大的收获还真不是那盘烤五花肉而是明白了一个道理很多环境问题不是操作问题而是平台匹配问题。mac m1 很好用但 HBase 生态目前对 ARM 的拥抱还不够彻底拯救者听起来不 fancy但 X86 兼容性确实让人省心。如果你也被大数据组件的原生库折磨别硬刚及时换环境是最体面的方案。至于 LeetCode 865这种“一次递归返回两个值”的设计思路写得多了你会发现工程代码里也到处是这种需要“把中间状态一起带回来”的场景。这次从烤肉桌到终端再到题解的经历也算是一种特别的项目复盘了。
返回列表