)
ScyllaDB Nodetool getendpoints 详解查询分区键所属节点含复合分区键与 --key-components 用法【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladbnodetool getendpoints是 ScyllaDB 运维中定位数据归属的核心命令给定 keyspace、表名与分区键partition key即可打印出负责存储该分区键的所有节点 IP 或主机名。本文以官方文档 docs/operating-scylla/nodetool-commands/getendpoints.rst 为主体结合仓库中 nodetool 客户端、REST API 与 storage_service 的源码实现完整讲解命令语法、单列与复合分区键的两种传参方式、--key-components的使用场景以及命令背后的 token 计算与副本定位原理帮助你准确判断数据存储在集群中的哪些节点。getendpoints 命令概述getendpoints用于回答一个经典问题某个分区键的数据到底存在哪台节点上在排查热点、验证数据分布、规划拓扑或确认副本位置时这一信息非常关键。命令的基本语法有两种形式nodetool getendpoints keyspace table key nodetool getendpoints keyspace table key-components其中第一种形式使用单个字符串指定分区键第二种形式通过--key-components选项逐个传入分区键的各个组成部分是复合分区键composite partition key的另一种指定方式。两者的作用完全一致打印负责该分区键的节点端点IP 或名称列表。文档中给出的典型用例如下nodetool getendpoints nba player_name Russell该命令查询nbakeyspace 中player_name表里分区键为Russell的数据由哪些节点持有。参数说明命令参数定义如下表内容继承自原文档并补充了各参数的实际约束参数说明keyspacekeyspace 名称table表名称key分区键key-components分区键的组成部分以独立组件形式指定复合分区键的替代方式关于参数的组合规则文档明确给出了三条硬性约束复合分区键使用冒号:分隔各列分区键的所有列都必须提供也可以使用--key-components选项分别指定每个组件而不必将它们拼成一个冒号分隔的字符串——当键值本身包含冒号时这种方式可以避免解析歧义所有分区键列都必须指定并且必须按照表 schema 中定义的顺序提供--key与--key-components必须二选一不能同时使用。这条二选一的约束在 nodetool 客户端源码中有直接体现。在 tools/scylla-nodetool.cc 的getendpoints_operation函数中void getendpoints_operation(scylla_rest_client client, const bpo::variables_map vm) { bool contains_key vm.contains(key); bool contains_key_components vm.contains(key-components); if (!vm.contains(keyspace) || !vm.contains(table) || !(contains_key || contains_key_components)) { throw std::invalid_argument(getendpoint requires keyspace, table and partition key arguments); } if (contains_key contains_key_components) { throw std::invalid_argument(Provide either --key or --key-components, not both); } ... }可以看到keyspace、table 与分区键三者缺一不可否则抛出getendpoint requires keyspace, table and partition key arguments同时传入--key和--key-components则抛出Provide either --key or --key-components, not both。命令底层工作原理解析理解getendpoints的实现有助于解释文档中各种参数约束的由来。整个调用链分为四层第 1 层nodetool 客户端tools/scylla-nodetool.cc在 getendpoints_operation 中客户端根据传参方式选择不同的 REST 接口sstring endpoint; http::request::query_parameters_type params {{cf, {vm[table].assstring()}} }; if (contains_key) { params[key] {vm[key].assstring()}; endpoint seastar::format(/storage_service/natural_endpoints/{}, vm[keyspace].assstring()); } else { params[key_component] vm[key-components].asstd::vectorsstring(); endpoint seastar::format(/storage_service/natural_endpoints/v2/{}, vm[keyspace].assstring()); } auto res client.get(endpoint, params); for (auto inet_address : res.GetArray()) { fmt::print({}\n, rjson::to_string_view(inet_address)); }使用--key时调用/storage_service/natural_endpoints/{keyspace}携带cf表名和key两个查询参数使用--key-components时调用/storage_service/natural_endpoints/v2/{keyspace}携带cf与多个key_component参数注意参数名从单数key变成了复数语义的key_component响应是一个 JSON 数组客户端逐行打印每个端点地址。命令注册处的选项定义tools/scylla-nodetool.cc也能印证参数设计{ getendpoints, Print the end points that owns the key, ... typed_optionsstring(keyspace, The keyspace to query, 1), typed_optionsstring(table, The table to query, 1), typed_optionsstring(key, The partition key for which we need to find the endpoint, 1), typed_optionstd::vectorsstring(key-components, List of components of the key for which we need to find the endpoint, -1), }, { getendpoints_operation }其中key-components的计数值为-1表示该选项可以重复出现多次即允许传入任意数量的组件。第 2 层REST API 层api/storage_service.cc两个 REST 端点分别由 rest_get_natural_endpoints 与 rest_get_natural_endpoints_v2 处理它们都转调storage_service的重载方法rest_get_natural_endpoints(...) { auto res ss.local().get_natural_endpoints(keyspace, req.get_query_param(cf), req.get_query_param(key)); } rest_get_natural_endpoints_v2(...) { auto res ss.local().get_natural_endpoints(keyspace, req.get_query_param(cf), req.get_query_param_array(key_component)); }第 3 层storage_service 核心逻辑service/storage_service.ccget_natural_endpoints 的三个重载是命令的核心inet_address_vector_replica_set storage_service::get_natural_endpoints(const sstring keyspace, const sstring cf, const sstring key) const { auto table _db.local().find_column_family(keyspace, cf); const auto schema table.schema(); auto pk partition_key::from_nodetool_style_string(schema, key); return get_natural_endpoints(keyspace, schema, table, pk); } inet_address_vector_replica_set storage_service::get_natural_endpoints(const sstring keyspace, const sstring cf, const std::vectorsstring key_components) const { auto table _db.local().find_column_family(keyspace, cf); const auto schema table.schema(); auto pk partition_key::from_string_components(schema, key_components); return get_natural_endpoints(keyspace, schema, table, pk); } inet_address_vector_replica_set storage_service::get_natural_endpoints(const sstring keyspace, const schema_ptr schema, const replica::column_family cf, const partition_key pk) const { dht::token token schema-get_partitioner().get_token(*schema, pk.view()); const auto ks _db.local().find_keyspace(keyspace); host_id_vector_replica_set replicas; if (ks.uses_tablets()) { replicas cf.get_effective_replication_map()-get_natural_replicas(token); } else { replicas ks.get_static_effective_replication_map()-get_natural_replicas(token); } return replicas | std::views::transform([] (locator::host_id id) { return _address_map.get(id); }) | std::ranges::toinet_address_vector_replica_set(); }这段代码揭示了getendpoints的完整算法解析分区键把用户输入的字符串按规则解析成partition_key计算 token通过schema-get_partitioner().get_token(*schema, pk.view())用 murmur3 等分区器把分区键哈希成 token定位自然副本根据 keyspace 的复制策略与复制因子查表得到该 token 对应的自然副本节点get_natural_replicas(token)。值得注意的是源码同时兼容两种数据分布模式——ks.uses_tablets()为真时走 tablet 模式的副本查询否则走传统的 vnode静态有效复制映射模式host_id 到地址映射副本集合中的元素是locator::host_id最终通过_address_map.get(id)映射为可读的 IP 地址后返回。第 4 层分区键解析keys/keys.cc分区键的字符串解析逻辑位于 keys/keys.cc它解释了文档中单列分区键不分割、复合分区键按冒号分割的行为差异partition_key partition_key::from_nodetool_style_string(const schema_ptr s, const sstring key) { std::vectorsstring vec; if (s-partition_key_type()-types().size() 1) { // For a single column partition key. Dont try to split the key // See #16596 vec.push_back(key); } else { boost::split(vec, key, boost::is_any_of(:)); } return from_string_components(s, vec); } partition_key partition_key::from_string_components(const schema_ptr s, const std::vectorsstring components) { if (components.size() ! s-partition_key_type()-types().size()) { throw std::invalid_argument(fmt::format(partition key {} has mismatch number of components: expected {}, got {}, components, s-partition_key_type()-types().size(), components.size())); } auto it std::begin(components); std::vectorbytes r; r.reserve(components.size()); for (auto t : s-partition_key_type()-types()) { r.emplace_back(to_bytes(t-from_string(*it))); } return partition_key::from_range(std::move(r)); }关键实现要点单列分区键完全不分割源码注释引用 issue #16596即使键值本身包含冒号也会被整体当作一个组件不存在歧义复合分区键按:分割这正是文档要求键值含冒号时改用--key-components的根源——此时boost::split会把键值内部的冒号误判为组件分隔符组件数量严格校验from_string_components要求组件数量必须与 schema 中分区键列数完全一致否则抛出partition key ... has mismatch number of components: expected N, got M这对应文档中所有分区键列都必须提供的约束按 schema 顺序转换类型每个组件依次用对应列类型的from_string解析为字节序列这对应文档中必须按照表 schema 定义的顺序提供的约束。示例 1单列分区键创建单列分区键的表CREATE TABLE superheroes ( firstname text, lastname text, age int, PRIMARY KEY (firstname) );查询分区键peter的归属节点nodetool getendpoints superheroes peter该命令返回负责superheroes表中分区键peter的节点列表。从源码可知由于superheroes表的PRIMARY KEY只有一列from_nodetool_style_string会走单列不分割分支把peter整体作为分区键解析。需要说明的是原文档中该示例省略了 keyspace 参数前缀实际完整命令应为nodetool getendpoints keyspace superheroes peter其中keyspace需替换为superheroes表所在的真实 keyspace 名称——源码中的参数校验要求 keyspace 是必填项。示例 2复合分区键冒号分隔字符串形式创建复合分区键的表CREATE TABLE superheroes ( firstname text, lastname text, age int, PRIMARY KEY ((firstname, lastname)) );PRIMARY KEY ((firstname, lastname))声明了一个由firstname和lastname两列组成的复合分区键。使用冒号分隔组件nodetool getendpoints mykeyspace superheroes peter:parker使用复合分区键时各组件必须按照 schema 中定义的顺序用冒号:分隔。此处peter对应firstnameparker对应lastname。底层实现中from_nodetool_style_string检测到分区键类型列表长度为 2于是按冒号将字符串拆成两个组件再交给from_string_components做数量与顺序校验。示例 3复合分区键--key-components 逐组件形式针对与示例 2 相同的表结构也可以改用--key-components分别传入每个组件nodetool getendpoints mykeyspace superheroes --key-components peter --key-components parker每个--key-components参数对应一个分区键列顺序与 schema 定义一致先是firstname再是lastname。这一形式在源码层面走的是另一条路径不再经过from_nodetool_style_string的冒号分割而是直接进入from_string_components把组件列表原样交给类型转换因此不存在冒号歧义问题。同时由于key-components选项注册为可重复的多值选项计数值-1组件数量不受命令行长度限制。示例 4键值本身含冒号的复合分区键如果某个分区键组件自身包含冒号字符串形式就会产生解析冲突。以相同的复合分区键表为例nodetool getendpoints mykeyspace superheroes --key-components peter:the-great --key-components parker这里第一个组件peter:the-great本身包含冒号。如果使用冒号分隔的字符串形式如peter:the-great:parkerfrom_nodetool_style_string会把它拆成三个片段peter、the-great、parker而 schema 只期望两个组件直接触发mismatch number of components: expected 2, got 3异常即使恰好凑成两个片段也会得到错误的分区键。使用--key-components后每个组件被独立传递冒号仅作为组件内部的普通字符因此能确保被正确解析。这正是文档强调的该选项的核心价值当键值可能包含冒号时用它来消除歧义。使用限制Frozen UDT 分区键不受支持文档明确指出ScyllaDB 不支持对包含 frozen UDT冻结用户自定义类型的分区键执行 getendpoints。原因可以从源码链路推断from_string_components在转换每个组件时调用t-from_string(*it)而 frozen UDT 这类复杂类型通常没有定义与字符串组件一一对应的转换路径无法可靠地把一个文本组件还原成结构化的 UDT 值。因此涉及 frozen UDT 分区键的场景请改用其他方式如查询系统表或直接读取数据来定位节点。使用建议与常见问题排查结合文档与源码给出如下实战建议明确区分单列与复合分区键单列分区键即使包含冒号也不必转义源码对单列键不分割复合分区键则要警惕冒号歧义。组件顺序必须与 schema 一致复合分区键组件按PRIMARY KEY ((c1, c2, ...))中括号内的声明顺序排列。组件数量必须完整所有分区键列都要提供缺失或多余都会触发mismatch number of components错误。--key与--key-components不可混用同时使用会得到Provide either --key or --key-components, not both的错误提示。理解返回值的含义返回的是一组节点地址副本集合而不是单个节点——它直接反映了该分区键数据在当前复制策略下的全部存储位置。集群拓扑变化如扩缩容、数据流迁移后同一分区键的归属可能发生变化建议在定位问题时结合当时的集群状态判断。复合分区键优先推荐--key-components即使当前键值不含冒号逐组件形式也更直观、更不易出错且两种形式在结果上完全等价。延伸阅读完整的 nodetool 命令索引见 docs/operating-scylla/nodetool-commands/nodetool-index.rstgetendpoints 客户端实现tools/scylla-nodetool.cc含命令注册 L4479-L4497REST API 处理函数api/storage_service.cc核心副本定位逻辑service/storage_service.cc分区键解析与校验keys/keys.cc【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考