恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

HBase实践指南:从Rowkey设计到二级索引与协处理器调优

  • 首页
  • 资讯中心
  • /
  • HBase实践指南:从Rowkey设计到二级索引与协处理器调优

相关资讯

5分钟部署OpenClaw智能体:免费托管+免费模型实战全记录 2026/10/16 11:48:04
GCN论文分类实战:用TensorFlow从零实现图神经网络 2026/10/16 11:43:03
TPOT实战:用遗传编程自动优化机器学习流水线 2026/10/16 11:43:03

最新资讯

Claude Code Windows安装报错?nvm与PowerShell环境配置全解析
污水处理设备设计升级:CAXA三维建模与二维出图协同实践
免费做“马跃新岁”封面:从设计流程到素材字体全攻略
AI智能体应用部署实战:Docker、K8s与Serverless如何选型?
合规爬虫技术全解析:从原理到工程实践
蓝桥杯C++模拟题龟兔赛跑预测:状态机与循环逻辑全解析

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

HBase实践指南:从Rowkey设计到二级索引与协处理器调优

发布时间:2026/10/16 11:48:04
HBase实践指南:从Rowkey设计到二级索引与协处理器调优 做HBase这行当有个很直白的经验Rowkey设计、二级索引方案和核心参数调优这三件事基本要在建表之前就想清楚不然表一上线后面每一步都是在为前期的取舍买单。之前我们聊过HBase的基础读写路径和集群部署这一篇直接把焦点放在最容易被问爆的三块内容上——Rowkey怎么设计才能避开热点和长尾二级索引在什么场景下值得做、怎么做协处理器什么时候用、用了之后怎么保证不把RegionServer拖垮以及那堆优化参数到底哪些是真正值得动手调的。这篇文章适合两类人一类是刚进入HBase维护工作的同学每天跟超时、Full GC、Scan慢搏斗另一类是有一定基础、想系统梳理底层决策逻辑的开发者。我不会只丢结论会把每一个决定背后的原因、代价、替代方案都讲透最后附上我实际踩过的坑和排查思路你可以当成自查清单用。1. Rowkey设计原则排序、长度、散列一个都不能少1.1 先理解Rowkey为什么这么重要HBase本质上是一个“按行键排序的分布式有序Map”。它不像关系型数据库那样有独立索引系统HBase的索引就是Rowkey本身。每一个Region负责一段连续的Rowkey范围RegionServer通过Rowkey定位到具体Region。换句话说Rowkey直接决定了数据落在哪个Region、用几次RPC能找到数据、Scan能扫多大的范围。这一点带来的连锁反应是Rowkey设计不好轻则部分Region访问集中、写热点重则全表Scan慢到没法用。比如你用时间戳做前缀同一天的数据全部落在同一个Region上写入并发再高也没用物理瓶颈卡在那里。见过不少团队把“随机散列”写在PPT上上线后Rowkey却只做了简单字符串拼接连预分区都没配结果一周之后某个Region的StoreFile大小是其他Region的十几倍。1.2 行得通的四条设计原则第一长度要短但要够用。Rowkey的每一字节都会占用存储空间同时HBase在内存中维护索引时长Rowkey也会浪费堆内存。业界常说的“8字节以内最好”并不是硬性规定而是让你重视长度带来的成本。比如用Long型代替String型存储订单号用整数ID代替UUID能省下来的是实打实的内存和IO。但别走极端——有人为了短把业务信息全丢光Rowkey变成纯随机数结果业务上想按用户维度查询时只能全表扫。正确的做法是Rowkey中既保留业务可辨识的信息又用散列或盐值前缀打散热点同时控制总长度在二三十字节以内。只要你压缩列名、减少列族数量Rowkey长一点带来的开销完全可控。第二散列优先。典型的Rowkey结构是“散列前缀 业务字段 时间戳”。散列前缀可以是对原始Key做哈希取模得到的值也可以是翻转后的字符串。这么做的核心目的是把原本单调递增的Key均匀分布到各个Region上。举例来说如果原始Rowkey是用户ID加时间戳直接拼接会导致每个用户的数据集中在同一段一旦用户访问量不均衡热点就出现了。在Rowkey前面拼上userId.hashCode() % regionCount同一用户的记录会被分散到不同Region代价是单个用户的查询会多几次RPC。第三唯一性要能直接确定。HBase的Put在相同Rowkey下是覆盖写如果你的Rowkey没有唯一性旧数据就悄悄被覆盖了。很多线上事故都源于此用“用户ID-时间戳”做Rowkey可是业务里同一分钟存在多个事件等发现数据“丢”了才想起来Rowkey冲突了。要么在Rowkey末尾加自增序号或随机后缀要么用不同的列版本机制这点在设计时必须定死。第四排序方向要配合你的主要查询路径。Rowkey按字典序排列如果你经常需要查“某个用户最近一周的操作”在Rowkey结构里就要让时间倒序把时间戳转成Long.MAX_VALUE - timestamp这样最新的记录排在前面Scan直接取Limit就能拿到最新数据。反过来如果你想查“某个用户的第一条记录”时间正序就够了。这种对排序方向的考究能让你少写不少过滤代码。1.3 预分区没有分区的散列等于没有散列我在不少文章里都强调过Rowkey散列必须和预分区配套做。HBase建表默认只有一个Region你在代码里费劲做了哈希前缀如果不提前把表分成多个Region数据还是只落在一个Region上散列的意义直接归零。预分区的思路是按照Rowkey散列前缀的分布区间在建表时指定每一个边界。例如你打算把表分成16个Region就可以选4位十六进制前缀从0000到FFFF划分成16段。HBase Shell里可以这样建create user_action, {NAME cf, COMPRESSION SNAPPY, VERSIONS 1}, {SPLITS [1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, a000, b000, c000, d000, e000, f000]}如果你用的是普通字符前缀也可以直接指定分割点比如create user_action, {NAME cf}, SPLITS [a|, b|, c|, d|, e|, f|, g|, h|, ...]这里有个细节预分区数量不是拍脑袋定的。每个RegionServer能承载的Region数量有限一般经验是每台RegionServer上的Region总数控制在20到200个之间单Region的数据量在10GB到20GB之间比较合适。你可以根据预估的数据总量和集群规模倒推分区数而不是“越多越好”。分区太多会导致Region频繁split元数据管理和RPC开销反而上去了。1.4 盐化的代价散列也不是白拿的盐化Salting是Rowkey散列的一种具体实现指的是在Rowkey前面加一个计算出来的前缀让数据分布均匀。但盐化有两个代价必须有意识第一是查询时你自己得知道盐值否则会变成全Region扫描第二是范围扫描的连续性被打断。举个例子如果你按用户ID做盐化查询某一条具体记录时你还能通过同样的哈希函数算出盐值定位到具体Region这是没问题的。但如果你要Scan“某个时间段内所有用户的数据”因为盐值打乱了时间顺序Scan会落到多个Region上整体效率可能不如不散列。所以在设计阶段我会先列出核心查询模式哪些是点查、哪些是范围查、哪些是批量扫。点查多的场景果断盐化范围查多的场景谨慎盐化或采用“时间倒序不散列”的方案。没有万能的Rowkey模板只有匹配查询模式的Rowkey设计。2. 二级索引非Rowkey查询的三种出路2.1 HBase查询模型的局限在哪里HBase的Get和Scan都是围绕Rowkey设计的。你要么按Rowkey直接拿一条数据要么扫一个Rowkey范围。但如果需求是“根据用户的手机号查用户信息”而用户表的Rowkey是userId这就是标准的不支持查询——除非把手机号放进Rowkey否则只能全表Scan每条记录都做一次比较。全表Scan在数据量小的时候还能接受一旦表里有千万行、Region几十个Scan的代价就是分钟级的。此时二级索引就是必需品。二级索引的本质是另建一张索引表索引表的Rowkey是被索引的列值Value是原表Rowkey。查询时先查索引表拿到原表Rowkey再回原表查数据。2.2 四种可实现方案对比主流的实现方式大致有四类各有适用场景。方案实现方式优点缺点自建索引表应用层在写主表时同步写索引表逻辑透明、可控性强双写一致性需要自己保证协处理器自动维护用Observer拦截Put/Delete自动同步索引表对应用层透明引入协处理器风险排查困难外部索引组件通过同步工具把HBase数据实时同步到外部组件查询能力丰富、扩展性强增加外部依赖链路变长HBase官方生态项目使用支持二级索引的框架功能完整、SQL友好版本兼容性需要维护自建索引表的典型结构是索引表Rowkey 索引列值 原表Rowkey列族里只需要存一个原表Rowkey即可。查询时先Get索引表得到原Rowkey再Get主表。这个方案的好处是逻辑完全在自己手里什么问题都能查清楚坏处是应用层必须记得双写一旦漏掉索引和主表的数据就对不上了。协处理器方案可以解决漏写的问题它在RegionServer侧拦截写请求把索引维护逻辑内聚在一个地方。但代价是把业务逻辑搬到了存储层出了故障影响面更大你需要非常熟悉协处理器的执行模型才敢上。外部索引组件适合对查询模式要求很灵活的场景比如需要全文检索、聚合分析、复杂过滤排序。但HBase实时同步到外部组件链路拉长数据延迟和维护成本都会上升适合查询复杂度远高于单点Get的场景。2.3 自建二级索引的完整小例子我实际的建议是大多数业务场景用自建索引表就够了前提是你能接受双写逻辑。下面是一个较典型的实现思路。假设主表Rowkey是userId列族里有mobile这个列。需要支持按照手机号查用户信息那么索引表结构可以这样设计main_table: rowkey: userId cf: mobile, name, age index_mobile_table: rowkey: hash(mobile) mobile cf: mainRowkey使用盐化前缀是因为mobile号码本身是有规律可寻的直接做Rowkey前缀会导致号码段热点比如某个号段的用户集中注册时写入就集中了。加一个哈希前缀后再拼手机号既能点查又不会产生明显热点。写入主表时业务代码需要同时构造两个PutPut mainPut new Put(Bytes.toBytes(userId)); mainPut.addColumn(cf, mobile, Bytes.toBytes(mobile)); Put indexPut new Put(Bytes.toBytes(hashPrefix(mobile) mobile)); indexPut.addColumn(cf, mainRowkey, Bytes.toBytes(userId));查询时先查索引表Get indexGet new Get(Bytes.toBytes(hashPrefix(mobile) mobile)); Result indexResult indexTable.get(indexGet); byte[] userId indexResult.getValue(cf, mainRowkey); Get mainGet new Get(userId);这个过程看起来简单实际落地时有几个容易遇到问题的点第一索引表的主表Rowkey字段建议带上主表的表名前缀防止将来多表共用一套索引逻辑时混淆第二更新索引时必须先检查旧值——如果用户改了手机号需要先根据旧手机号删除旧索引再写入新索引这一来一回如果不在事务里就会出现短暂的不一致窗口第三索引表本身也面临和主表一样的Region热点问题所以索引表的Rowkey也要做预分区不能直接拿索引列的原始值当前缀了事。必须强调的是自建二级索引只适合索引列是“低频更新”的场景。如果用户手机号频繁变更每次变更都要处理两条索引索引表的写放大很严重此时你该考虑协处理器集中处理或者干脆调整Rowkey设计。3. 协处理器把索引维护和聚合计算下沉到存储引擎3.1 Observer和Endpoint先分清再使用协处理器可以理解为HBase开放给RegionServer内部的钩子分两种Observer和Endpoint。Observer是“观察者模式”在Region的读写操作发生前或发生后插入自定义逻辑。它不能把数据搬到客户端但能修改请求内容或响应内容。常用场景是Put之前自动生成索引、Get返回结果后按需加工、Server端完成计数或求和。Endpoint是“终端模式”相当于在RegionServer端定义了一个可调用远程方法。客户端把一个请求分发给所有相关RegionRegion各自执行一段聚合逻辑再把结果合并返回给客户端。它适合做聚合计算比如求最大值、平均值、分组统计。Endpoint和Observer谁先用谁后加一定要在文档里写清楚否则后面维护的人根本看不懂这些钩子是怎么串起来的。3.2 用Observer自动维护二级索引我们前面说自建索引表需要在应用层双写。如果应用层代码比较分散或者多个团队共用一张HBase表双写逻辑很容易被遗漏这时候Observer就派上用场了。思路是继承BaseRegionObserver重写postPut方法在主表写入成功后自动向索引表写入索引数据。伪代码大致是这样的public class MobileIndexObserver extends BaseRegionObserver { Override public void postPut(ObserverContextRegionCoprocessorEnvironment c, Put put) throws IOException { byte[] row put.getRow(); // 提取需要建索引的列例如mobile列 KeyValue kv put.get(cf, mobile); if (kv null) return; // 构造索引表Put String indexRow hash(kv.getValue()) Bytes.toString(kv.getValue()); Put indexPut new Put(Bytes.toBytes(indexRow)); indexPut.addColumn(cf, mainRowkey, row); // 获取索引表并写入 Table indexTable c.getEnvironment().getTable(indexTableName); indexTable.put(indexPut); } }这段代码要上线还差几步把编译好的jar包打到RegionServer的classpath里或通过配置动态加载然后在表描述符里加入一个名为“索引Observer”的协处理器参数。这样所有指向该表的Put都会经过这段逻辑不需要业务调用方感知。3.3 协处理器的风险边界协处理器不是银弹它把代码从“应用可控”变成了“RegionServer内部执行”这意味着任何未捕获异常、死循环、慢查询、内存分配都会直接伤害同一台RegionServer上的其他Region。我见过一次事故一个Observer在处理Put时调用了一个外部API外部API超时导致连接到该RegionServer的写入线程全部阻塞整个集群的写入不可用长达十几分钟。从那以后我给自己定了几条规矩协处理器里只做纯内存计算和HBase自身Table操作绝不调用外部接口所有流程都用try-finally包裹不能有任何未捕获异常漏出去写Omnio时如果索引表写失败主表的数据已经写入需要靠异步对账任务修复不能指望Observer回滚主表写入。另一个容易忽略的问题是协处理器版本管理。线上集群可能有几百张表每张表都可能加载了不同版本的jar包一旦升级必须一张表一张表地重新加载配置。建议把协处理器逻辑独立成一个专门的项目发布时带上表名清单和版本号至少在ChangeLog里写清楚。Endpoint的使用场景比Observer更窄。开发上它会侵入客户端调用代码维护成本更高而且跨Region聚合的结果需要序列化传输传输性能也要做压测。不是特别复杂的聚合运算优先考虑在客户端并行Scan多个线程各自扫描Region再做内存聚合运维复杂度会低很多。4. 优化参数从默认值到生产环境的调优模板4.1 先打开RegionServer堆内存和GC参数的认知HBase的很多优化参数都跟堆内存相关但堆内存不是越大越好。RegionServer内存同时服务缓存BlockCache和写路径MemStore两者有一个平衡点。具体堆大小要结合Region数量、单Region数据量和应用读写比例来评估。最经典的争议是BlockCache和MemStore的比例。默认情况下BlockCache占堆的40%左右MemStore占堆的40%左右剩余留给其他开销。如果你的场景是写多读少MemStore可以稍微调大读多写少BlockCache可以调大。但MemStore占整体堆的比例不能超过50%否则Flush刷盘频率过高反而埋下写放大隐患。我这里给出一个典型参数组合可以当作起点去压测调整hbase.regionserver.global.memstore.size 0.4 hfile.block.cache.size 0.4 hbase.regionserver.handler.count 30按核数调整 hbase.regionserver.memstore.flush.size 128MB默认值不够大 hbase.hregion.memstore.block.multiplier 4值得注意BlockCache和MemStore这两项加起来的比例必须留出至少20%的堆空间给其他对象和GC否则即使你设计出世界上最完美的RowkeyFull GC也会让你在深夜被报警电话打醒。4.2 客户端参数写缓冲和Scan缓存很多性能问题并不在服务端而是客户端参数没调好。常见的有两个第一hbase.client.write.buffer。默认是2MB对于批量写入场景明显偏小建议调到5MB到10MB这样可以减少RPC次数。但注意这个参数是客户端缓冲如果应用进程不稳定崩溃缓冲里没刷出去的数据会丢掉所以要根据业务对数据安全的要求做权衡。第二Scan的缓存行数。默认Scan每次RPC抓取的记录条数可能只有几十条这在大表扫描时会造成大量额外RPC。把hbase.client.scanner.caching调大到1000或5000配合hbase.client.scanner.max.result.size控制单次RPC返回的数据量能显著降低Scan耗时。但缓存太大也会占满客户端堆内存扫描长Region时容易导致内存溢出这两个参数要一起调。4.3 文件压缩与StoreFile参数HBase支持多种压缩算法常用的有GZIP、Snappy和Zstandard。GZIP压缩率高但CPU开销大Snappy均衡Zstandard在压缩率和速度上都表现不错新版集群里我优先推荐Zstd。对压缩算法有个常见误解以为压缩只在落盘时生效对读操作没用。实际上BlockCache里存的是解压后的数据HFile在磁盘上的大小直接决定了BlockCache能缓存多少数据块。如果表数据量大换一个压缩率更高的算法命中率提升明显。代价是解压耗CPU因此CPU核数紧张时别盲目上Zstd可以先压测再做决定。StoreFile相关的参数里hbase.hregion.memstore.flush.size默认值通常是128MB太小会导致频繁刷盘。可以通过压测调高到256MB以平摊刷盘开销。但memstore太大又会延长Flush时阻塞写入的时间。这里需要看监控在写入尖峰时段观察RegionServer的“Memstore Flush线程占用率”和“写队列长度”如果两者在高位持续说明Flush跟不上写入速度你要么调低写缓冲要么增加RegionServer数量。4.4 Compaction相关的取舍Compaction是HBase老生常谈的痛。Major Compaction能把小文件合并成大文件、清理删除标记但频繁执行会让磁盘IO和CPU持续打满。线上并不需要每天执行Major Compaction很多团队会把自动Major Compaction关掉放到凌晨业务低峰手动执行。参数可以这样设置hbase.hregion.majorcompaction 0禁用自动Major Compaction或者设置一个较长周期比如7天一次。同时把hbase.hstore.compactionThreshold调高一些例如从默认的3改成5减少Minor Compaction的频率。这两个操作能明显降低写放大但前提是你有完善的Compaction监控和对齐任务否则删除的数据可能长时间不清理StoreFile数量持续上涨读性能下降。4.5 连接数和线程池参数RegionServer上hbase.regionserver.handler.count默认是10到30表示同时处理的请求数。这个值不是越大越好它受CPU核心、单请求处理和IO队列深度影响。设太高请求全部排队线程上下文切换开销反而拖垮吞吐。建议按照CPU核数的2到4倍设置然后压测扫描场景观察线程活跃度和响应时间RSI的曲线。还要留意hbase.ipc.server.read.threadpool.size和hbase.ipc.server.callqueue.handler.factor这两个参数决定RPC的读写分离程度。写多读多的混合场景适当调大读线程池能把分析型Scan对写入的影响隔离开。我个人的习惯是在压测时一旦发现“写入P99耗时上去了但线程池活跃度很高”优先检查是不是大Scan把RPC线程占满了而不是无脑加线程。5. 常见问题与排查技巧实录5.1 热点Region怎么定位热点出现时RegionServer监控页面会看到某台机器CPU飙升、请求延迟上涨HBase自带的Region监控里能看到某个Region的读请求数或写请求数是其他Region的好几倍。最直接的定位方式是看RPC请求分布按Region维度查看请求数量确认是哪一段Rowkey范围在打转。拿到Rowkey区间后分析是不是Rowkey前缀的业务规律性太强比如时间戳前缀导致整点写入都打在同一个Region上。解决办法通常是重新设计Rowkey加上盐值前缀或者把表删除重建、按新的前缀做预分区。如果不想重建数据也可以用“分表”的方式绕开按业务拆成多张HBase表每张表对应一套Rowkey规则。这种方式没有从根上解决问题但能把热点打散到不同物理表适合阶段性应急。5.2 RegionServer频繁Full GC排查Full GC是HBase性能下降最常见的元凶。先查几个点BlockCache命中率是否低于90%MemStore占用是否长期超过global memstore.sizeRegion数量是否过多导致RegionServer内存碎片化对象大小是否异常比如Rowkey过长导致每行占用内存过大。排查命令上jstat先看GC频率和耗时再用jmap导出堆快照重点分析BlockCache和MemStore里占用的对象。如果BlockCache命中率正常MemStore也没爆但GC还是频繁很可能是有大对象在客户端Scan时被反复传递比如某个列的值有几MBScan一次就撑爆了堆。这种情况优先要改业务限制单行大小。5.3 Scan越来越慢但数据量没涨这时候优先看Region数量和StoreFile数量。Region数太多Scan就得跨多个Region并发如果客户端配置的线程数不够整体延迟上不去。StoreFile数量多说明Compaction没跟上。先用HBase Shell查看表的Region数量和每个Region的StoreFile数再决定是清理小文件还是调整Compaction参数。还有一个小技巧如果Scan只需要部分列一定要用setFilter或只指定需要的列族和列HBase会把不需要的列直接跳过减少网络传输和反序列化开销。这个优化最常见也容易被忽略。5.4 协处理器拖慢写路径怎么快速止血遇到写性能突然下降且时间点与协处理器上线时间吻合先别急着改业务先在表描述符里确认协处理器加载情况。快速止血的方法是临时移除该表的协处理器配置然后重新加载表让写路径恢复无钩子状态再单独调试协处理器的逻辑。调试时重点考察三件事协处理器内部有没有同步等待外部服务、有没有在postPut里做太重的事务处理、索引表本身有没有热点。协处理器性能问题九成以上出在这三个地方。5.5 参数调优速查表我把上面提到的所有参数整理成一个速查表方便你直接对着排查。参数默认值我的建议适用场景hbase.regionserver.global.memstore.size0.40.35-0.45写比例高于50%hfile.block.cache.size0.40.3-0.4读比例高于50%hbase.client.write.buffer2MB5-10MB批量写入场景hbase.client.scanner.caching100500-1000大范围Scanhbase.hregion.memstore.flush.size128MB128-256MB写入尖峰高hbase.hstore.compactionThreshold33-5降低写放大hbase.hregion.majorcompaction604800000ms0或7天控制Compactionhbase.regionserver.handler.count30CPU核数x2-4混合读写这些参数不是拍出来的每个都需要配合监控去验证。建议改参数时一次只改一个观察至少半天再做下一次调整。别指望一版参数走天下不同表的读写特征不一样最优参数组合也不同。写在最后的一个小建议如果你现在正准备上线一张新HBase表我给你一个实际验证过的流程先列出所有查询场景把点查、范围查、批量扫分开然后基于主要场景设计Rowkey再决定是否需要二级索引如果需要索引先评估自建索引表是不是够用不要一上来就碰协处理器最后建表时直接把预分区和压缩配好不要在数据量上来之后再返工。参数调优放在上线前做一次基准压测上线后根据监控持续调整就好。这套流程我先后用在不同规模的项目上不敢说绝对最优但能让你在最开始就避开大多数HBase性能坑。真到了排查问题的时候记得回到第一性原理HBase是一个有序KV存储所有的性能和一致性问题最终都能追溯到Rowkey、Region和Compaction这三件事上。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号