Skip to content

Instantly share code, notes, and snippets.

@umonaca
Last active March 21, 2021 04:14
Show Gist options
  • Save umonaca/fef1ebe3c2048b822cf678428774cae1 to your computer and use it in GitHub Desktop.
Save umonaca/fef1ebe3c2048b822cf678428774cae1 to your computer and use it in GitHub Desktop.

锁表原因?

我想了一下最可能造成此次锁表的原因。你是程序在线的时候执行的knex-migrate up吗?那样这次是有可能锁死。 如果你cherry-pick了 935681b14f4f41a6af27977d5cc1cae6c2f5817a 这个是为了解决之前打错字的情况,也为了这次你执行knex-migrate up能正确运行而不至于直接挂在前面的错误文件上。

当你执行knex-migrate的时候,它会读取knex_migrations表,对比没有执行过的migration文件,然后执行并把完成的migration记录文件名到这个表里面。

也就是说本次你应该是看到执行了两个migration:
20210223200240_drop_not_null_constraints.js
20210304101412_fix_view_userMetatdata.js

然后前者20210223200240_drop_not_null_constraints.js是个什么东西呢,为了DROP t_work表里面大量多余的NOT NULL constraints,这个是上游写的bug。 然而SQLite非常麻烦的地方在于它不支持绝大多数ALTER TABLE,导致无论用什么ORM或者Query Builder最后都是得自己手写大段重建整个表的语句,具体你可以看那个文件里做了什么。 这个非常繁琐的步骤也是符合SQLite文档的:https://sqlite.org/lang_altertable.html

然而如果重建过程中或者以后导致数据库锁死,那就只有重启了。

不过现在应该不需要做什么,数据库应该是正常的。

数据库迁移的补充解释

关于Kikoeru内部的数据库迁移逻辑,请看这篇文档
如果你看到knex_migrations表里面有两个只差一个字母的文件:

20210223200240_drop_not_null_constrains.js
20210223200240_drop_not_null_constraints.js

并不会造成问题,但是不放心你可以删掉20210223200240_drop_not_null_constrains.js这个记录。具体原因不解释了,其实是你fork unstable上线的时候我还有没测试完的草稿……

关于你提的VIEW问题

VIEW本身不存储数据,它是完全等价于subquery的。 执行

EXPLAIN QUERY PLAN SELECT * FROM userMetadata
	WHERE user_name = 'admin'
	ORDER BY userRating DESC, "release" DESC, id DESC;

EXPLAIN QUERY PLAN SELECT * FROM (SELECT t_work.id,
      t_work.title,
      json_object('id', t_work.circle_id, 'name', t_circle.name) AS circleObj,
      t_work.release,
      t_work.review_count,
      t_work.dl_count,
      t_work.nsfw,
      userrate.userRating,
      userrate.review_text,
      userrate.progress,
      userrate.updated_at,
      json_object('vas', json_group_array(json_object('id', t_va.id, 'name', t_va.name))) AS vaObj,
      userrate.user_name
    FROM t_work
    JOIN t_circle on t_circle.id = t_work.circle_id
    JOIN r_va_work on r_va_work.work_id = t_work.id
    join t_va on t_va.id = r_va_work.va_id
    JOIN (
        SELECT t_review.work_id,
          t_review.rating AS userRating,
          t_review.review_text,
          t_review.progress,
          strftime('%Y-%m-%d %H-%M-%S', t_review.updated_at, 'localtime') AS updated_at,
          t_review.user_name
        FROM t_review
          JOIN t_work on t_work.id = t_review.work_id
        ) AS userrate
    ON userrate.work_id = t_work.id
    GROUP BY t_work.id, userrate.user_name
    ) AS subquery
    WHERE user_name = 'admin'
    ORDER BY userRating DESC, "release" DESC, id DESC;

可以看到QUERY PLAN是完全一致的。
QUERY PLAN 1
QUERY PLAN 2

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

我觉得也许有 cluster 4个实例同时读写的问题在,我不知道 sqlite 有没有在文件层实现锁,而这四个实例也没法互相协调。

我会先把实例数量降低到1来试试

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

就算是因为 transcation 锁住的话,等锁释放以后数据库就应该恢复正常,但他现在的状态是数据库完全锁死。

会不会是在 transcation 中发生了异常导致 transcation 没有正确结束?

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

mysql 的适配我做了一小部分,主要是解决了各个表字段类型不规范的问题

@umonaca
Copy link
Author

umonaca commented Mar 7, 2021

不如试试在向用户返回错误信息以后继续 throw 错误,来达到自杀重启的目的

数据库锁死,除非4个一起关掉重开才有用。

就算是因为 transcation 锁住的话,等锁释放以后数据库就应该恢复正常,但他现在的状态是数据库完全锁死。

会不会是在 transcation 中发生了异常导致 transcation 没有正确结束?

这个问题太底层了,老实说不知道该从哪入手。即使是Knex这么底层的框架出现问题都比数据库锁表容易解决。
我可以先试一下消除那个transaction,不用那个视图、不用json_group_object拆开来多次查询。
然后就是消除N+1查询,消除多余的那个transaction,加上缓存等等……

mysql 的适配我做了一小部分,主要是解决了各个表字段类型不规范的问题

那个json_group_object我会重写的,改用Javascript然后拆开来多跑几次查询。

@umonaca
Copy link
Author

umonaca commented Mar 7, 2021

另外我记得这次不是完全锁死,而是大部分时候锁死。期间我观察了一下浏览器网络选项卡,有些查询是成功了的,不过大部分挂了。另外SQLite默认没有busy handler,这次加上的那个超时1秒或许会有用

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

与其说是大部分时候锁死,不如说是部分接口锁死?

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

主页的works接口似乎百分百能加载出来

@umonaca
Copy link
Author

umonaca commented Mar 7, 2021

另外你可以逐条运行一下以下语句检查一下数据库完整性

PRAGMA integrity_check;
PRAGMA quick_check;
PRAGMA foreign_key_check;

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

我试试

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> PRAGMA integrity_check;
ok
sqlite> PRAGMA quick_check;
ok
sqlite> PRAGMA foreign_key_check;
sqlite> PRAGMA foreign_key_check;

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

最后一个语句没有返回内容, 不过也没有报错

@umonaca
Copy link
Author

umonaca commented Mar 7, 2021

不报错就是正常的。说明数据库本身还是完整的,并且外键都一致。

@umonaca
Copy link
Author

umonaca commented Mar 7, 2021

看来超时1秒还是有用的。现在虽然卡到爆但是没有报错。
路线图:

  • 1秒超时等待
  • 删除不必要的transaction
  • 修复 N + 1查询 (这个很重要)
  • JOIN起来的静态数据(个别字段以目前API的JSON格式)预先存一个单独的表
  • 添加index

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

现在卡到爆是因为我在进行高io操作…

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

我正在上传新的音声档案

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

我在上传档案的时候遇到了一些报错

kikoeru_1 | ! [RJ317746] 在插入元数据过程中出错: COMMIT; - SQLITE_BUSY: database is locked
kikoeru_1 | ! [RJ317581] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317567] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317919] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317278] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317861] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317733] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317787] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318076] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317932] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318040] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317989] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318175] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318367] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318138] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317327] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318153] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318081] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317982] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318191] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318749] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318515] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318345] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318513] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318769] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318516] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318691] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318254] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318580] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ319204] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317634] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318925] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318122] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ319542] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318630] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ317750] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318694] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ319567] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318385] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction
kikoeru_1 | ! [RJ318082] 在插入元数据过程中出错: BEGIN; - SQLITE_ERROR: cannot start a transaction within a transaction

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

重试几次就ok了

@umonaca
Copy link
Author

umonaca commented Mar 7, 2021

我N+1查询重构完了,由于没有单元测试只能手测打算再审核一下。肉眼可见的变快,至少10倍以上。以后用静态表缓存的话数据库速度还可以再快一些,。

Lighthouse 90分

image

我在上传档案的时候遇到了一些报错

底层问题SQLite driver问题,这个在Knex里面是不可能的,因为Knex正常情况下会把nested transaction转换成SAVEPOINT。

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

牛逼

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

你这效率也太高了,我都快跟不上了

@umonaca
Copy link
Author

umonaca commented Mar 7, 2021

后端 cha0sCat/kikoeru-express#2
前端 cha0sCat/kikoeru-quasar#3

前后端需要同时合并,具体见后端操作说明。

@umonaca
Copy link
Author

umonaca commented Mar 7, 2021

有个事情之前知道但我一直没想起来,就是Knex对于SQLite默认max pool size=1,原因是对于SQLite >1不仅没有好处而且容易出问题。
不止我们自己遇到因为这个导致SQLite锁表问题: knex/knex#3176
https://stackoverflow.com/questions/15822778/sqlite-connection-pool-in-java-locked-database
单文件嵌入式数据库就是这样的。
由于你开了4个instance,所以就有概率出问题了。建议改成1个instance,然后把静态资源offload到Nginx之类的上面。我在看这个:
https://stackoverflow.com/questions/4060772/sqlite-concurrent-access
另外也可以直接按MySQL重构。毕竟处理底层问题很烦。如果你合并了上面的更新,Model部分代码会整齐得多,要重构需要改的地方也少。另外你说的不要通过前端生成静态资源地址这个我也做了,需要的话我可以发PR。

@umonaca
Copy link
Author

umonaca commented Mar 7, 2021

如果一定要开4个instance,可以执行

PRAGMA journal_mode=wal;

可能有助于缓解问题。不过这样数据库文件会变成3个。多两个wal和shm结尾的文件。
需要还原的话可以执行

PRAGMA journal_mode=delete;

@cha0sCat
Copy link

cha0sCat commented Mar 7, 2021

我看主页加载的时候回请求两次 works 接口,是重复了吗
Works 的 this.reset() 被触发了一次

不过这个问题不重要,现在我看到works的请求等待时间在3-4秒,静态视图已经创建成功

@cha0sCat
Copy link

cha0sCat commented Mar 8, 2021

需不需要我这边脱敏的生产数据库做测试?

@umonaca
Copy link
Author

umonaca commented Mar 8, 2021

还可以更快,就是把静态视图直接转换成一个同名的大表,不过有个不小的缺陷是你每次添加作品都得自己跑SQL手动更新这个大表。还有就是,你的SQLite数据库大小可能会翻倍,增加的大小约等于t_work表。不过倒是可以不用改后端代码了。

  • 创建:
CREATE TABLE staticMetadata_new (
  id INTEGER,
  title TEXT,
  circle_id INTEGER,
  name TEXT,
  circleObj TEXT,
  nsfw boolean,
  "release" TEXT,
  dl_count INTEGER,
  price INTEGER,
  review_count INTEGER,
  rate_count INTEGER,
  rate_average_2dp REAL,
  rate_count_detail TEXT,
  "rank" TEXT,
  vaObj TEXT,
  tagObj TEXT,
  PRIMARY KEY(id)
);

INSERT INTO staticMetadata_new SELECT * FROM staticMetadata;
DROP VIEW IF EXISTS staticMetadata;
ALTER TABLE staticMetadata_new RENAME TO staticMetadata;
  • 更新
INSERT OR IGNORE INTO staticMetadata
    SELECT baseQueryWithVA.*,
      json_object('tags', json_group_array(json_object('id', t_tag.id, 'name', t_tag.name))) AS tagObj
    FROM (
      SELECT baseQuery.*,
        json_object('vas', json_group_array(json_object('id', t_va.id, 'name', t_va.name))) AS vaObj
      FROM (
        SELECT t_work.id, 
          t_work.title,
          t_work.circle_id,
          t_circle.name,
          json_object('id', t_work.circle_id, 'name', t_circle.name) AS circleObj,
          t_work.nsfw,
          t_work.release,
          t_work.dl_count,
          t_work.price,
          t_work.review_count,
          t_work.rate_count,
          t_work.rate_average_2dp,
          t_work.rate_count_detail,
          t_work.rank
        FROM t_work
        JOIN t_circle ON t_circle.id = t_work.circle_id
      ) AS baseQuery
      JOIN r_va_work ON r_va_work.work_id = baseQuery.id
      JOIN t_va ON t_va.id = r_va_work.va_id
      GROUP BY baseQuery.id
    ) AS baseQueryWithVA
    LEFT JOIN r_tag_work ON r_tag_work.work_id = baseQueryWithVA.id
    LEFT JOIN t_tag ON t_tag.id = r_tag_work.tag_id
    GROUP BY baseQueryWithVA.id
  • 还原成视图
DROP TABLE staticMetadata;

CREATE VIEW staticMetadata AS
    SELECT baseQueryWithVA.*,
      json_object('tags', json_group_array(json_object('id', t_tag.id, 'name', t_tag.name))) AS tagObj
    FROM (
      SELECT baseQuery.*,
        json_object('vas', json_group_array(json_object('id', t_va.id, 'name', t_va.name))) AS vaObj
      FROM (
        SELECT t_work.id, 
          t_work.title,
          t_work.circle_id,
          t_circle.name,
          json_object('id', t_work.circle_id, 'name', t_circle.name) AS circleObj,
          t_work.nsfw,
          t_work.release,
          t_work.dl_count,
          t_work.price,
          t_work.review_count,
          t_work.rate_count,
          t_work.rate_average_2dp,
          t_work.rate_count_detail,
          t_work.rank
        FROM t_work
        JOIN t_circle ON t_circle.id = t_work.circle_id
      ) AS baseQuery
      JOIN r_va_work ON r_va_work.work_id = baseQuery.id
      JOIN t_va ON t_va.id = r_va_work.va_id
      GROUP BY baseQuery.id
    ) AS baseQueryWithVA
    LEFT JOIN r_tag_work ON r_tag_work.work_id = baseQueryWithVA.id
    LEFT JOIN t_tag ON t_tag.id = r_tag_work.tag_id
    GROUP BY baseQueryWithVA.id

@umonaca
Copy link
Author

umonaca commented Mar 8, 2021

我看主页加载的时候回请求两次 works 接口,是重复了吗
Works 的 this.reset() 被触发了一次

要正确地解决主页问题需要换控件重写,不过最近大概是没空了。最主要的问题是:由于你的站点资源非常多,需要的是像Twitter那样的Timeline marker,也就是说点进任何一个作品、返回以后都能回到原来的位置;并且要可以向上向下两个方向无限滚动。现在的问题是无论点进哪个作品,返回,都会直接跳回顶部,这非常不方便。
或者也可以直接重写成传统的分页。我觉得这种比较合理,因为页数信息对用户来说是有意义的。
要改的地方有点多,最近不是很有时间。

需不需要我这边脱敏的生产数据库做测试?

我觉得目前应该不需要。

@cha0sCat
Copy link

cha0sCat commented Mar 8, 2021

现在这种滚动模式确实对用户很不友好,每次想看只能从头往下拉,有的时候想要跳到具体某一页只能靠不停下拉,有一部分作品始终处于中间位置很难被用户翻到

@cha0sCat
Copy link

cha0sCat commented Mar 10, 2021

刚刚一位某+坛友兼b站up主二州牧联系了我,他是一位b站远古音声区up主,也是我的asmr启蒙人。

他希望能够参与kikoeru的开发,我跟他说了一下我fork的项目地址和大佬的某+ id,也许我们可以详聊?

edit: 可能是我理解错了, 二州牧大佬似乎是想找我探讨资源整理问题

@umonaca
Copy link
Author

umonaca commented Mar 18, 2021

我想最需要的大概是按照REST规范重构API。现在像之前写的offload还是补丁套补丁再套补丁。还有很多别的问题:

  • 封面/api/cover应该返回的不是封面本身而是封面地址。
  • 之前提到的无法利用的注入漏洞
  • 路由匹配也非常随意

等过两天打算写一个API草稿。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment