我们使用 scylla(version = 2.1.3-0.20180501.8e33e80ad) 并且我们能够在不重新启动整个集群的情况下解决这个问题。
我们的集群最近一直在丢失节点,因为这些节点重新启动并且在启动的 gossip 阶段不允许加入集群。原因是:status=UN(up & normal)的节点出现以下错误,并且在 gossip 阶段不允许那些受影响的节点加入集群。在我们的例子中,错误消息是:
7 月 4 日 01:54:17 host-10.3.7.77 scylla[30263]:[shard 0] gossip - 收到对等 10.3.7.7 的无效 gossip 生成;本地代 = 1526993447,接收代 = 1562158865
现在让我们进入上述错误消息的详细信息和上下文:
- 每个节点都配置了一个种子列表,它会在启动期间尝试向其发送消息并收集集群信息。
- 在启动时,它会创建一个“世代”号(世代号是一个纪元),它在八卦期间与种子主机共享。
gossiper.register (this->shared_from_this());
auto generation_number=db::system_keyspace::increment_and_get_generation().get0();
_gossiper.start_gossiping(generation_number, app_states, gms::bind_messaging_port(bool(do_bind))).get();
- 首次启动时的节点将其世代号发送到种子和种子八卦与其他人一起传递信息。种子将此世代编号存储为参考。这被称为上面错误消息中提到的 local_generation 术语,即联合国节点 10.3.7.77 说对等方 10.3.7.7 正在发送代号 1562158865(即称为接收代),但它已存储为参考 1526993447。您将请注意,1526993447 指的是 2018 年 5 月 22 日的纪元,而 1562158865 指的是 2019 年 7 月 3 日的纪元,即节点 10.3.7.7 于 2018 年 5 月 22 日首次启动并发送其世代号为 1526993447。
- 由于 2 个 epoch 之间的差异大于 1 年,UN 节点将拒绝允许其他节点加入
int64_t MAX_GENERATION_DIFFERENCE = 86400 * 365;
if (local_generation > 2 && remote_generation > local_generation + MAX_GENERATION_DIFFERENCE) { // 假设某个对等点的内存已损坏并且正在广播关于另一个对等点(或它自己)的难以置信的一代
logger.warn("收到一个无效的 gossip 生成对等体.....}
- 现在在 bootup 期间,increment_and_get 的逻辑是:
auto req = format("SELECT gossip_generation FROM system.{} WHERE key='{}'", LOCAL, LOCAL);
return qctx->qp().execute_internal(req).then([] (auto rs) {
int generation;
if (rs->empty() || !rs->one().has("gossip_generation")) {
// seconds-since-epoch isn't a foolproof new generation
// (where foolproof is "guaranteed to be larger than the last one seen at this ip address"),
// but it's as close as sanely possible
generation = service::get_generation_number();
} else {
// Other nodes will ignore gossip messages about a node that have a lower generation than previously seen.
int stored_generation = rs->one().template get_as<int>("gossip_generation") + 1;
int now = service::get_generation_number();
if (stored_generation >= now) {
slogger.warn("Using stored Gossip Generation {} as it is greater than current system time {}."
"See CASSANDRA-3654 if you experience problems", stored_generation, now);
generation = stored_generation;
} else {
generation = now;
}
}
auto req = format("INSERT INTO system.{} (key, gossip_generation) VALUES ('{}', ?)", LOCAL, LOCAL);
- 从上面的逻辑来看,服务器首先从 system.local 表中查找代号。如果值为空,它会生成一个新数字,即当前时间,因为生成代号的逻辑仅取决于当前时间。如果它不为空,它将与当前时间进行比较并使用较大的值,即最近的时间并将其写回 system.local 表
int get_generation_number() { .... auto now = high_resolution_clock::now().time_since_epoch(); int generation_number = duration_cast(now).count(); ....}
因此,在启动时由节点生成并发送到种子的世代号通常总是更接近当前时间,但种子 UN 节点存储为本地参考的世代号不会改变。
为了完全避免集群重启:我们根据上面解释的代码逻辑在生产中采用了这种方法。
-- 根本问题是存储在联合国种子节点中的问题节点的本地生成没有改变。(但每次重新启动时有问题的节点都会发送一个更接近当前时间的新代号)
-- IDEA : 让我们更新存储在 UN 节点中的问题节点的本地代,以便问题节点发送的远程代号将在 1 年内下降。
- 那么我们如何在联合国种子节点中更新这个值呢?我们需要让有问题的节点发送一个代号(epoch),其值落在存储在 UN 种子节点中的本地代号的 1 年窗口内。但由于代码始终将当前时间作为 gen 编号,而当前时间是 2019 年 7 月,我们能做什么?
-- 我们将问题节点上的 TIME 改回 1526993447 的 1 年内的值。在 1 年窗口结束时选择一个纪元值,即将系统时间更改为 2019 年 3 月 31 日的值,即纪元 1554030000 而不是2018 年 10 月 2 日并重新启动节点。节点将重新启动并向种子发送代号 1554030000(当它查找 system.local 表时)或当前时间,即 2019 年 3 月 31 日。
-- 联合国种子节点获取此值并验证有问题的节点发送的远程代号在 2018 年 5 月 22 日的 1 年内,因此,它会继续更新其参考(本地代)。
else if (remote_generation > local_generation) { logger.trace("将心跳状态生成从 {} 更新到 {} for {}", remote_generation, local_generation, ep); // 主要状态更改将通过直接插入远程状态来处理更新 this->handle_major_state_change(ep, remote_state); } ....
-- 我们已成功更新存储在 UN 种子节点中的问题节点的参考(本地生成)。-- 现在我们停止有问题的节点,将有问题的节点上的时间重置为当前时间并重新启动,有问题的节点将发送 2019 年 7 月 4 日的最新纪元,即纪元 1562215230 -- 现在重置并重启时间后,因为 1562215230 (gen sent be problem node using latest time) - 1554030000 (存储在UN种子节点中的本地参考) <1年,问题节点将被允许加入集群。
-- 我们建议您在 1 年窗口结束时选择一个纪元/日期,但在 1 年内,越晚越好,因为新的 1 年窗口从您选择的日期开始,这个问题在很长一段时间内得到缓解 LOL – 是的此问题发生在长时间运行的集群上。这意味着您需要每年进行一次滚动重启以延长 1 年的窗口期。
以下是该过程的步骤:
脚步:
如果有问题的节点是 10.3.7.7 并且在 10.3.7.77(UN 节点)上报告了错误,请确保 10.3.7.7 的种子是 10.3.7.77,这样我们就可以保证它与该节点通信,我们不必搜索找出集群中谁也在说话。如果 7.7 节点的种子与报告错误的节点不同,则查看种子节点打印的错误消息以决定也重置哪个 epoch。在我们的例子中,由于我在 7.77 上看到了错误,我将 7.7 的种子更改为 7.77 节点。
启动有问题的节点。
- 种子节点应该开始打印错误。捕获我们节点的错误消息并记下本地 gen 编号,以便我们也选择重置日期。在我们的例子中,味精如下:
7 月 4 日 01:54:17 host-10.3.7.77 scylla[30263]: [shard 0] gossip – 收到对等 10.3.7.7 的无效 gossip 生成;本地代 = 1526993447,接收代 = 1562158865
cqlsh 到有问题的节点 10.3.7.7 并将生成编号更新到 1526993447 的 1 年内的一个纪元,但是选择一个 1 年窗口结束时的纪元,例如 1554030000(2019 年 3 月 31 日),而不是说 2018 年 7 月/10 月,这样你就有了更长的新 1 年窗口。
在有问题的节点上,运行命令
5.1 '更新system.local set gossip_generation = 1554030000 where key='local';'
5.2 'nodetool 刷新'
停止有问题的节点
编辑配置文件并将 CQL (native_transport_port) 从 9042 更改为 9043,以便客户端无法连接和插入数据 - 在此阶段插入数据将设置时间戳为 2019 年 3 月的记录,这是不正确的,即防止数据损坏。这是一个预防措施
更改系统时间,即“date -s '31 MAR 2019 11:03:25'”</p>
- 通过运行 date 命令验证系统时间是否已更改
- 启动UN种子节点的有问题的节点和尾日志,错误应该消失。
- 等待一段时间(几分钟就足够了)让八卦发生并验证有问题的节点现在是否是联合国。
- 在另一个节点上运行命令“nodetool status”以检查其是否为 UN。
- 您可以跟踪联合国种子节点的日志并检查是否仍然出现错误。如果您再次看到错误 - 从头开始再次重复这些步骤。你错过了一些东西。
一旦节点被宣布为UN:
14.1 关闭节点
14.2 将配置文件中的 CQL (native_transport_port) 从 9043 改回 9042。
14.3 重置盒子上的系统时间
14.4 验证系统时间恢复正常
更改回时间和端口后启动节点。并且节点应该仍然是UN。
自白:
- 是的,我们在生产中做了这个练习。该节点无论如何都被认为是死的,因此风险很小,因为搞砸死节点甚至不会产生任何影响,如果程序失败,我们将仅牺牲 1 个节点,因此只有集群重启的唯一选项。
- 我们扫描了 master 分支的 scylla 代码库,了解集群通信中系统时间的使用情况,发现只有 2 个地方让我们确信更改系统时间会起作用。此外,通过将 CQL 端口更改为 9043,我们消除了客户对现有数据的任何污染。
故事寓意:
- 这发生在 2.1 版本的 scylla 中,截至今天 2019 年 7 月 4 日,scylla 的主分支仍然具有相同的代码逻辑,因此这也可能发生在版本 3 及更高版本中。2 .每隔几个月最好对节点进行滚动重启,以便节点发送一个新的gen号用于八卦,并延长1年的窗口。
- 如果你有一个长时间运行的集群 > 1 年,如果一个节点重新启动,它会受到这个错误的影响,发生的节点重新启动越多,疫情传播得越多。
- 如果代码逻辑相同,这对 cassandra 有效,我认为是这样。
参考:
https://github.com/scylladb/scylla/blob/134b59a425da71f6dfa86332322cc63d47a88cd7/gms/gossiper.cc
https://github.com/scylladb/scylla/blob/94d2194c771dfc2fb260b00f7f525b8089092b41/service/storage_service.cc
https://github.com/scylladb/scylla/blob/077c639e428a643cd4f0ffe8e90874c80b1dc669/db/system_keyspace.cc
您还可以在我的博客https://mash213.wordpress.com/2019/07/05/scylla-received-an-invalid-gossip-generation-for-peer-how-to-resolve上找到上述解释/修复详细信息
/