PG12 中索引损坏,如何修复?

我只是在猜测,但这是我的“锡纸帽”理论:

我同时运行了两次 Reindex(即先运行 ccnew,再运行 ccnew1),
两次都出现了错误。

也许当它失败时,并不会回滚,而是留下这些垃圾数据然后退出。

这听起来非常非常非常有可能。

我也来补充一点个人看法,

Toast 索引无法通过这种方式删除,
我不得不这样做:

su postgres
psql
\connect discourse
drop index pg_toast.pg_toast_309322_index_ccnew;
drop index pg_toast.pg_toast_309322_index_ccnew1;

上述操作仅适用于 pg_toast,因为 discourse 用户没有访问该索引的权限。
PG::InsufficientPrivilege: ERROR: permission denied for schema pg_toast

删除 toast 索引非常危险。不过我想你在这里别无选择。是的,你需要通过 psql 来执行这个操作。

现在执行愉快的重新索引:

discourse=# REINDEX SCHEMA CONCURRENTLY public;
REINDEX

:partying_face:

发现得很及时!我通过以下查询找到了所有名称中包含 “ccnew” 的索引。

psql
\connect discourse
select tablename,indexname,indexdef from pg_indexes where indexname LIKE '%ccnew%';

incoming_referer.csv (7.2 KB)

结果发现我竟然有整整 30 个这样的索引,全部位于 incoming_referers 表上。因此,我通过上述查询中的 indexdef 列验证了所有 ccnew 索引确实都是重复的。

select indexname,indexdef from pg_indexes where tablename = 'incoming_referers';

ccnew.csv (6.6 KB)

随后,我成功删除了所有这些索引。

DROP INDEX incoming_referers_pkey_ccnew;
DROP INDEX incoming_referers_pkey_ccnew_ccnew;
DROP INDEX incoming_referers_pkey_ccnew1;
...以此类推,删除全部 30 个

在那之后,我重新对整个模式(schema)进行了索引重建,但系统再次无法重建两个相同的 incoming_referers ccnew 索引,同时还发现了三个 pg_toast 索引。我删除了这些索引,然后再次对整个模式进行索引重建,结果又出现了更多错误,并在 discourse 模式中发现了更多 ccnew 索引。于是我又进行了第三次索引重建……

我无法在不报错的情况下完成完整的索引重建。系统每次都会创建新的 ccnew 索引,然后重建失败。在进行了四次完整的重建后,我删除了这些 ccnew 索引并放弃了继续尝试。我或许可以尝试非并发重建,但这会导致大量停机时间。

无论如何,我推测大多数从 PG10 升级到 PG12 并在之后尝试重新索引的用户都会遇到这些额外的 ccnew 索引,它们都应该被删除。这些索引只会占用空间,并毫无益处地增加磁盘写入 I/O。

我在 Data-Explorer 中运行了它的等价版本,管理起来稍微容易一些。

我的做法是:将所有 ccnew、ccnew1、ccnew2……ccnewn 索引全部删除,然后重新索引。这为我解决了问题。

c cnew 是 PostgreSQL 用来标记索引的一种机制。我认为,如果索引过程因某种原因失败,处理流程中存在某种低效问题,会导致这些标记被遗留下来。

我建议你先解决根本原因。我之前有重复的标签,在删除索引之前必须先将其删除。如果还剩下哪怕一个重复标签,索引就无法正确执行,并会再次创建另一个 ccnew'n' 索引并失败。

你说得对,我仍然有重复数据——无法非并发地重建非 ccnew 索引。我需要删除这些重复的行。

discourse=# reindex index index_incoming_referers_on_path_and_incoming_domain_id;
ERROR:  could not create unique index "index_incoming_referers_on_path_and_incoming_domain_id"
DETAIL:  Key (path, incoming_domain_id)=(/search/, 3433) is duplicated.
discourse=# reindex index index_incoming_referers_on_path_and_incoming_domain_id;
ERROR:  could not create unique index "index_incoming_referers_on_path_and_incoming_domain_id"
DETAIL:  Key (path, incoming_domain_id)=(/search/, 1861) is duplicated.

很奇怪的是,我在 incoming_referer 表中每个 incoming_domain_id 值只看到一行。那为什么它们会被视为重复呢?

discourse=# select * from incoming_referers where path='/search/' AND incoming_domain_id IN (1861,3433);
  id   |   path   | incoming_domain_id 
-------+----------+--------------------
 42845 | /search/ |               1861
 40763 | /search/ |               3433
(2 rows)

@sam@riking,我是否应该按以下方式删除这两行:

DELETE FROM incoming_referers WHERE path='/search/' AND incoming_domain_id IN (1861,3433);

……我想我终究还是在学 PostgreSQL 了,哈哈。

不,那样会删除两个记录——你需要使用 WHERE 来查找两个 id,然后只选择其中一个 id 进行删除。

你的查询使用了损坏的索引,这就是为什么你每个只看到一个行。试试这个:

... where path LIKE '%/search/' ...

好的,这返回了 43 行。

discourse=# select * from incoming_referers where path LIKE '%/search/' ORDER BY incoming_domain_id;
  id   |    path     | incoming_domain_id 
-------+-------------+--------------------
   878 | /search/    |                 63
 33457 | /do/search/ |                567
  1580 | /search/    |                602
  1888 | /search/    |                663
 42983 | /search/    |               1259
  4896 | /search/    |               1788
 42845 | /search/    |               1861
  5162 | /search/    |               1861
  5176 | /search/    |               1866
 43350 | /search/    |               1905
 17238 | /search/    |               1905
 20689 | /search/    |               1982
  5781 | /hg/search/ |               1987
  8031 | /search/    |               2665
 10325 | /search/    |               3192
 11289 | /search/    |               3414
 40763 | /search/    |               3433
 42849 | /search/    |               3433
 13087 | /search/    |               3895
 13159 | /search/    |               3949
 13802 | /do/search/ |               4051
 14407 | /search/    |               4209
 14507 | /search/    |               4211
 15394 | /search/    |               4230
 15533 | /search/    |               4258
 45274 | /search/    |               5303
 20923 | /search/    |               5400
 21317 | /search/    |               5534
 22928 | /search/    |               5918
 22956 | /search/    |               5926
 37448 | /search/    |               6393
 25094 | /search/    |               6412
 25594 | /search/    |               6547
 39655 | /search/    |               6596
 27371 | /search/    |               6986
 27452 | /a/search/  |               7003
 27623 | /search/    |               7041
 31041 | /search/    |               7767
 36943 | /search/    |               8622
 37381 | /search/    |               8711
 37411 | /search/    |               8716
 40424 | /search/    |               9124
 44451 | /search/    |               9525
(43 rows)

所以我应该运行以下命令吗?

DELETE FROM incoming_referers WHERE path LIKE '%/search/' AND id IN (42845,43350,42849);

没错,这个 DELETE 看起来是正确的,应该能让你进行干净的备份。

信不信由你,又出现了更多失败。第一组问题中,我发现 /m/search 下还有一个重复键未被 %/search/ 通配符捕获(原因不明),于是将其删除。再次重建索引(每次几乎耗时一小时!),结果又出现失败,提示 users(username_lower) 上存在重复键。

discourse=# reindex index index_users_on_username_lower;              
ERROR:  could not create unique index "index_users_on_username_lower"
DETAIL:  Key (username_lower)=(john_smith) is duplicated.

但问题是,username_lower=john_smith 实际上只有一行记录!是时候戴上侦探帽了。

查看论坛管理界面,发现有两个独立用户,用户名分别为 “john_smith” 和 “John_Smith”——注意大小写不同。于是我直接在论坛层面删除了全小写的那个用户,因为他已有四年未活跃,之后该索引重建成功。

我又运行了一次完整重建,每次几乎耗时一小时,终于,我认为问题解决了——仅剩一个错误,但并非重复键,而是 pg_toast 中的 ccnew 索引。我将其删除了。

discourse=# REINDEX SCHEMA CONCURRENTLY public;               
WARNING:  cannot reindex invalid index "pg_toast.pg_toast_2783329_index_ccnew" concurrently, skipping
REINDEX

这真是一段曲折的旅程。

未来的 Discourse 升级能否/是否会自动化这些数据库修复?

如果 Peter 能够复现出一致的测试用例,PostgreSQL 索引损坏问题很可能在后续的 PostgreSQL 版本升级中得到解决。

如果并发 REINDEX 因表包含无效数据而失败,您需要执行以下操作:

  1. 修复无效数据,方法可参考 此处

  2. 使用以下语句列出无效索引:SELECT pg_class.relname FROM pg_class, pg_index WHERE pg_index.indisvalid = false AND pg_index.indexrelid = pg_class.oid;

  3. 使用 DROP INDEX <indexname>; 删除上述列出的每个无效索引。

  4. 再次尝试执行 REINDEX

在执行升级到 2.5.0beta5 并按照更新后指南重新索引数据库后,我遇到了以下错误:

discourse=# REINDEX SCHEMA CONCURRENTLY public;
ERROR:  could not create unique index "index_plugin_store_rows_on_plugin_name_and_key_ccnew"
DETAIL:  Key (plugin_name, key)=(discourse-data-explorer, q:-10) is duplicated.

我不太想在这个问题上随意尝试……那么,如何安全地删除重复项呢?

不,升级并不会损坏你的索引,它只是指出了你的索引已经损坏。你可以通过尝试将备份恢复到另一台服务器(即使是运行 pg10 的服务器)来检查这一点。或者,尝试在现有安装上重建索引。目前尚不清楚是什么导致了索引损坏,但希望 pg12 出现这种情况的可能性会降低。

升级确实有一些性能优势,但暂时不升级也不是一个坏主意。

现在已为时过晚,所以我仍在寻求关于下一步的指导。

类似这样的语句:

select id, plugin_name, key from plugin_store_rows where plugin_name like '%discourse-data-explorer%'

应该能返回相关行。我认为删除它们是安全的。

我认为需要 ilike