恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Mac M1 上 HBase 安装避坑与 LeetCode 865 最小子树解析
首页
资讯中心
/
Mac M1 上 HBase 安装避坑与 LeetCode 865 最小子树解析
Mac M1 上 HBase 安装避坑与 LeetCode 865 最小子树解析
发布时间:2026/9/23 4:35:49
上周公司团建部门挑了一家韩式烤肉店。烤盘刚冒烟几个实习生就开始抱着电脑抢带宽说白天有个任务“就差最后一步”。等五花肉滋滋卷边的时候邻座那位刚换了 MacBook Pro M1 的同事拍桌“我真的会谢HBase 装了一下午每次都快好了又给我报错。”另一边的老哥把拯救者往桌上一放锤了两下键盘嘴里嘟囔着“我这就是解压即用哪来这么多幺蛾子”。这话当然有点夸张但那天晚上回家我还真把 HBase 在 M1 上从安装到放弃、再在拯救者上一次成功的全过程复盘了一遍顺带把白天刷到一半的 LeetCode 865 题“具有所有最深节点的最小子树”也啃完了。这篇不装高深就讲两件事为什么 Mac M1 上 HBase 这么难伺候以及 LeetCode 865 到底该怎么想。1. 烤肉桌旁立下的 flag为什么非要在 M1 上装 HBase1.1 团建那天的经典对话那天真正的名场面是这样的烤盘上的牛舌刚熟负责数据平台那位已经开始在 Mac 上敲命令了。他先是brew install hbase装完还挺开心结果hbase shell一敲屏幕直接卡在连接 ZooKeeper 上改了几次配置又卡在 master initialization好不容易看到日志往前走了又报了个 WAL 路径相关的异常。他一边翻文档一边叹气说“别人都是五分钟装完我怎么跟渡劫一样”。同桌有人冒了一句“你用拯救者试试”他半信半疑打开 Windows 笔记本下载官方二进制包、解压、改了两行配置start-hbase.cmd一跑jps里 HMaster 和 HRegionServer 齐刷刷出现在进程列表里。那一刻整个烤盘桌都安静了。这场景其实特别典型。不是 HBase 歧视 Mac更不是 Apple Silicon 芯片跑不了 Java——HBase 本质是个 Java 应用跨平台能力很强。真正的问题往往出在“依赖环境”和“教程适用对象”上网上 99% 的 HBase 安装教程都是基于 x86_64 Linux 或者老版本 macOS 写的你拿着 M1 去硬套每一步都可能踩到别人没踩过的坑。1.2 这项目到底想干什么在展开踩坑之前简单说清楚 HBase 是个什么玩意儿。你可以把它想成一家大型餐厅WAL 预写日志是门口的点单流水单厨师每做一个菜之前先记一笔防止做到一半停电忘了要做什么MemStore 是后厨的暂存台菜先放在那儿凑一批攒够了再统一送进冷库HFileStoreFile就是冷库货架数据最终按有序结构存在这里。HMaster 负责管全局调度RegionServer 负责真正端菜而 ZooKeeper 是老板的秘书谁在干活、谁掉线了都由它来登记。所以当你执行start-hbase.sh的时候其实是在拉起一整套相互依赖的服务。这也解释了为什么“安装 HBase”看起来是个解压缩动作真正花时间的却是各种关联服务之间的协作。理解了这层架构后面看到 master initialization 卡住、WAL 路径异常这类报错就不会觉得那是玄学而是某个“零件”没对上的必然结果。2. Mac M1 上的完整翻车链路从 brew 到 master initialization2.1 第一关brew install hbase 看起来顺利实际上问题从这就埋下了很多 M1 用户的第一步都是brew install hbase。问题在于Homebrew 在 Apple Silicon 上的安装路径是/opt/homebrew和 Intel 时代的/usr/local不一样如果你之前装过 x86 版工具链或者终端里残留了 Rosetta 转译环境下编译的依赖那么hbase命令表面能执行实际调用的动态库、脚本路径可能全是乱的。更麻烦的是brew 仓库里的 hbase 版本往往不是最新的甚至可能是已经比较旧的稳定版。你拿它去配新版 macOS、配合适的 JDK版本矩阵对不上日志就会在某个莫名其妙的ClassNotFoundException或UnsupportedClassVersionError上停下来。这一步给我的教训是在 M1 上跑大数据组件尽量不要依赖 brew 的“省事”直接去 Apache 官网下载官方二进制包解压反而能少一半麻烦。2.2 第二关JDK 不是“装了就行”ARM 架构下版本选择很关键HBase 2.x 官方支持 JDK 8 和 JDK 11很多教程默认让你配JAVA_HOME指向 JDK 8。但 M1 上是 ARM 架构你得装ARM 版 JDK同理如果你的 Mac 之前为了跑某些旧软件装了 x86 版 JDK那么 HBase 启动时也好、编译时也好很容易出现架构不匹配的诡异问题。我当时试过两种组合JDK 8 HBase 2.2.xJDK 11 HBase 2.4.x。前者在启动时直接给出一堆与 Hadoop native 库相关的UnsatisfiedLinkError后者能跑但中途依然会被配置问题卡住。后来我总结出一个相对稳的组合Azul Zulu 的 aarch64 JDK 11 HBase 2.4.x 官方包 不额外配置 HDFS。这套组合在纯本地 standalone 模式下至少能保证基础进程拉起来不会一开局就死在 JVM 层面。2.3 第三关卡死在 master initialization端口和 hosts 一起背锅如果你已经过了 JDK 这关大概率会遇到一个更经典的劝退点日志里反复出现master initializationHMaster 进程起了又停、停了又起网页管理端死活打不开。这时候先别怀疑芯片架构按下面顺序排查。第一看看/etc/hosts里有没有当前主机名的正确解析。公司电脑经常改过主机名甚至绑了域控结果 HBase 启动时反解主机名失败ZooKeeper 里注册不了节点master 就一直停留在 initialization 状态。解决办法是在 hosts 文件里把127.0.0.1 你的主机名显式加一行然后重启。这个坑在 Windows 上几乎不会遇到因为默认 localhost 映射太完善了但在 macOS 上特别常见。第二确认端口没被占用。HBase 2.x 常用端口有这几位服务默认端口作用ZooKeeper2181协调服务HMaster/RegionServer 注册HMaster RPC16000HMaster 对外 RPC 端口HMaster Web UI16010管理界面入口RegionServer RPC16020RegionServer 对外 RPC 端口RegionServer Web UI16030查看 Region 状态注意网上很多老教程写的是 60010、60030那是 0.98 时代的端口号到了 2.x 早就改掉了。如果你照着老资料配防火墙或者看日志会越看越懵。另外如果本机 2181 端口已经被其他 ZooKeeper 实例占用HBase 内置的 ZooKeeper 会直接起不来表现同样是 master initialization 卡住。2.4 第四关WAL 预写日志相关的异常以及本地文件系统那点事跨过端口和 hosts 之后我遇到的下一个坎就是热词里那个“hbase wal预写日志异常”。具体报错我记得很清楚大概是在创建 WAL 目录的时候失败或者提示/WALs路径不可写。查了一圈发现WAL 目录默认是跟着hbase.rootdir走的也就是说 rootdir 配哪WALs 就建在哪。问题就出在 rootdir 上。很多教程会教你在hbase-site.xml里写property namehbase.rootdir/name valuehdfs://localhost:9000/hbase/value /property这个配置在“已经装好 Hadoop HDFS”的环境里没问题。但大多数本地快速体验场景根本不需要 HDFS你也没启动 HadoopHBase 却傻乎乎地尝试去连 9000 端口连不上WAL 目录建不出来于是报错。真正常见的正确做法是去掉 HDFS 相关配置让 HBase 以 standalone 模式运行rootdir 使用本地文件系统比如property namehbase.rootdir/name valuefile:///Users/你的用户名/hbase_data/value /property property namehbase.zookeeper.property.dataDir/name value/Users/你的用户名/zookeeper_data/value /property2.5 如果你也要在 Apple Silicon 上装 HBase按这个顺序来把上面的坑填平之后我给身边的人总结了一份 M1 环境下的稳妥步骤照着走基本能避开大部分雷下载 Azul Zulu aarch64 JDK 11设置好JAVA_HOME并确认java -version输出里没有 x86_64 字样。去 Apache 官网下载hbase-2.4.x-bin.tar.gz不要用 brew 的旧版本。解压后编辑conf/hbase-site.xmlrootdir 和 ZooKeeper dataDir 都配成本地file://路径。编辑conf/regionservers把里面的默认主机名改成localhost。检查/etc/hosts确保本机主机名能解析到 127.0.0.1。执行bin/start-hbase.sh再用jps看进程确认 HMaster 和 HRegionServer 都在。浏览器打开http://localhost:16010能看到 HBase 管理界面就算成功。这套流程我后来在另外两台 M1 上也验证过结论是一致的MAC M1 完全可以跑 HBase只是不能完全照搬 x86 教程。3. 换成拯救者“一下就好”本质不是系统好坏而是版本矩阵3.1 那台拯救者上到底发生了什么认真复盘之后我发现同事的“一下就装好”并不是因为拯救者比 MacBook 强而是因为那台机器的环境在不知不觉中把最容易出问题的变量全部规避掉了。我后来专门问了他执行了哪些步骤答案非常朴素先装好了 JDK 8然后解压 HBase 2.4.x 二进制包改了 rootdir 为file:///D:/hbase_data运行bin/start-hbase.cmd结束。这里有一个容易被忽略的细节Windows 原生环境下start-hbase.cmd脚本对中文路径、带空格路径很敏感很多人在 Windows 上装不上其实是路径问题。他之所以顺利是因为解压目录是纯英文且无空格。而 M1 那边失败的根因其实是 ARM 版 JDK、brew 旧版 HBase、hosts 解析、rootdir 指向 HDFS 这几件事叠在一起。也就是说不是“Windows 行 Mac 不行”而是“他那台机器的版本矩阵恰好全对上了”。3.2 M1 与拯救者的关键差异对比用表格对比一下两边的差异会更直观维度Mac M1Apple Silicon拯救者x86_64 Windows/WSL处理器架构arm64需要 ARM 版 JDKx86_64JDK 选择无歧义常见教程适配度多数教程面向 Linux/x86需自行翻译与主流教程环境基本一致包管理器差异Homebrew 包版本可能滞后手动下载官方 tar 包更可控hosts/主机名公司域/改主机名易导致 master 起不来默认 localhost 映射靠谱rootdir 配置容易被老教程误导指向 HDFS本地文件系统路径默认直通不过要说明一点如果你在拯救者上用 WSL 跑 Linux 版 HBase那它本质上是 x86_64 Linux 环境比 Windows 原生更接近教程场景。很多人说“Windows 一下就装好”其实是因为他们开了 WSL 或者在虚拟机里干净环境操作而不是真的在 CMD 里硬扛。3.3 把“运气”变成可复现的步骤把两边经验合并之后我得到一个非常重要的认知安装大数据组件的本质是管理好“JDK 版本 组件版本 配置文件 系统网络环境”四元组。任何一个变量偏离教程假设都会表现为奇奇怪怪的运行时错误。所以后来我无论是给客户部署还是自己搭环境都会先列一个环境清单逐项确认JDK 版本与架构是否匹配HBase 二进制包版本是否与 JDK 兼容rootdir 和 WAL 路径是否指向当前可访问的文件系统/etc/hosts或 Windows 主机名解析是否正常2181、16000、16010、16020、16030 端口是否被占用。如果在 M1 上报错先跑一下jps看进程再翻logs/目录下 HMaster 的日志找到第一个 ERROR基本就能定位。这个排查习惯比任何“安装神器”都管用。4. 烤肉消化后LeetCode 865“具有所有最深节点的最小子树”到底在考什么4.1 用烤肉桌的比喻先读懂题意烤肉吃到后半场有人提议刷两道题醒醒酒于是打开了 LeetCode 865。题目其实很短给定一棵二叉树的根节点root返回所有最深节点的最小子树。什么叫“所有最深节点”就是整棵树里深度最大的那些叶子节点可能不止一个。什么叫“最小子树”就是能把这些最深节点全部包含进去的最小子树的根节点。换到烤肉桌的场景想象一下楼下一排观众站起来欢呼你要找的是主舞台上的哪一盏聚光灯能刚好同时罩住这排观众又不能太高——太高会罩住太多无关的人。比如一棵满二叉树最底层有四个叶子它们的最近公共祖先就是根节点那答案就是根节点如果只是一个单边链下去的树最深的节点只有唯一一个那答案就是它自己。注意一个容易混淆的点题目说的“最小子树”不是“包含节点数量最少”而是“高度最低/最靠近叶子的那个公共祖先”准确说就是所有最深叶子节点的最近公共祖先LCALowest Common Ancestor。4.2 关键观察答案等价于最深叶子节点的最近公共祖先如果你对 LCA 有印象这道题就成功了一大半。但这里不能直接套用“两个节点的 LCA”模板因为最深节点可能有很多个而且我们事先不知道它们具体是谁。于是问题变成怎么在不知道最深叶子集合的情况下找到它们的 LCA一个天然的想法是从根节点出发谁的子树更深就往哪边走。因为最深的那批叶子一定集中在某个子树方向上如果左右两棵子树的最大深度相等说明左右两边都有最深叶子那当前节点就是它们的最近公共祖先。这个观察非常关键它让我们不需要显式求出所有最深叶子只靠比较左右子树深度就能一路锁定答案。4.3 最稳妥的实现一次递归同时返回节点和深度这类题的通用技巧是递归函数不要只返回一个值而是返回一个结构体或 pair把“子树内最深节点的 LCA 候选节点”和“当前子树的最大深度”一起带回来。C 写法如下class Solution { public: pairTreeNode*, int dfs(TreeNode* root) { if (!root) return {nullptr, 0}; auto left dfs(root-left); auto right dfs(root-right); if (left.second right.second) { // 左边更深最深的节点都在左子树答案也在左子树 return {left.first, left.second 1}; } if (left.second right.second) { // 右边更深答案在右子树 return {right.first, right.second 1}; } // 左右一样深当前节点就是所有最深节点的 LCA return {root, left.second 1}; } TreeNode* subtreeWithAllDeepest(TreeNode* root) { return dfs(root).first; } };理解这段代码的关键在递归的合并逻辑。每次递归返回两个东西一个节点指针表示“当前子树里包含所有最深节点的最小子树根节点”另一个数字表示“当前子树的最大深度”。左右子树的结果返回后比较它们的深度左子树更深说明全局最深节点只在左边所以左边的那个候选节点就是整个子树的答案右子树更深同理左右一样深说明左右两边都有最深节点那当前节点就是它们共同的最小公共祖先直接返回当前节点。这里你可能会问为什么左右一样深的时候不继续往下找更深点因为左右深度已经相等且都是当前子树的最大深度继续往下走就只能覆盖其中一边覆盖不全另一边所以当前节点就是最小子树根。复杂度是 O(N)每个节点只访问一次空间复杂度 O(H)H 是树高。4.4 练手时先写“二次遍历”版本能帮你理清递归返回值如果你第一次接触这种思路觉得绕建议先写一个更容易理解的“二次遍历”版本第一次递归求出每个节点的深度或者整棵树的最大深度第二次从根往下走优先前往更深的那一侧直到左右子树最大深度相等时返回当前节点。class Solution { public: int dfsHeight(TreeNode* root) { if (!root) return 0; return 1 max(dfsHeight(root-left), dfsHeight(root-right)); } TreeNode* dfs(TreeNode* root, int maxDepth) { if (!root) return nullptr; int leftDepth dfsHeight(root-left); int rightDepth dfsHeight(root-right); if (leftDepth maxDepth - 1 rightDepth maxDepth - 1) { return root; } if (leftDepth maxDepth - 1) { return dfs(root-left, maxDepth); } return dfs(root-right, maxDepth); } TreeNode* subtreeWithAllDeepest(TreeNode* root) { int maxDepth dfsHeight(root); return dfs(root, maxDepth); } };这里maxDepth表示当前节点所处的位置对应的全局最大深度其实可以简化成“当左右子树深度相等时返回当前节点”。这个版本思路更直观但最坏情况下每次都要重新计算深度比如链式满二叉树时复杂度会退化到 O(N^2)。好在 LeetCode 的数据范围下一般也能过作为理解工具很合适。等你想通了再改成一次 DFS 的 pair 版本。4.5 为什么写二叉树程序总是报运行时错误最常见的五个原因热词里有一句“写二叉树程序时为什么总是报运行时错误”我烤着肉想了一下这问题太真实了。结合 865 题最常见的无非五个原因第一递归入口没有判空。subtreeWithAllDeepest(nullptr)直接传给递归函数第一行就访问root-left空指针异常立刻抛出。第二递归没有基线条件或者基线条件写错导致无限递归爆栈。第三返回值类型混用今天写成节点指针明天写成 bool后天写成深度一递归就乱了套。第四把“子树最大深度”和“子树内最深节点的深度”搞混判断条件写反答案跑到错误分支。第五在结构体里用TreeNode*作 key 却不做空指针检查导致哈希表插入空指针运行期报错。针对这五个问题我的建议是写任何二叉树递归前先在脑内跑一遍空树和单节点两个边界把所有能返回“空”的分支全部显式写出来递归函数尽量统一返回类型如果确实需要多个数据用 pair 或结构体不要用全局变量绕来绕去。4.6 举一反三从二叉树深度到更广的树型 DFS吃饱了烤肉再想这题会发现 865 其实是一个“深度控制”类问题的代表LeetCode 104 求最大深度是单纯求值LeetCode 112 路径总和是求是否存在某条路径LeetCode 994 腐烂的橘子则是用 BFS 按层扩散和“深度”本质上也是同一套思想。这些题的核心都是在遍历过程中维护“当前层/当前深度”的信息。对于“找最深节点 LCA”这类题记住一句话单次递归里同时维护节点和深度是通吃“最深”类二叉树题目的大杀器。你甚至可以逐渐形成自己的模板要么返回 pairroot, depth要么额外开一个哈希表记录深度然后从最深节点反向爬父节点。前者简洁后者适合需要同时处理多个最深节点具体路径的场景。这个思路在面试里遇到变形题也能顺势迁移。写在最后吃完烤肉把环境理顺那天烤肉的最后一盘是同事请的理由是他终于搞明白了自己 M1 之前为什么装不上。我后来发现环境问题看着像玄学本质是版本矩阵的排列组合算法题看着像智商测试本质是见过足够多范式之后的条件反射。M1 装不上 HBase别急着说“换 Windows 吧”或者“M1 不行”先看日志把端口、JDK、rootdir 挨个过一遍多半能找到真正的原因LeetCode 刷题卡住也别急着看题解先想清楚这题是“求值”还是“求路径”是“求节点”还是“求深度”把所有返回值列出来答案往往就自己浮出来了。最后分享一个小技巧递归返回结构里同时塞节点和深度是处理“最深”类二叉树题目的万金油。我已经用这个模式秒过好几道题了。肉吃多了容易困但把栈跑顺了心里是真踏实。