Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(backend): DBフォールバック有効時、複数のFTTソースから取得するタイムラインで取得漏れが起きる現象の修正 #13495

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from

Conversation

tai-cha
Copy link
Contributor

@tai-cha tai-cha commented Mar 2, 2024

What

  • FTTにおいて複数のソースからノートを取得する際に複数ソースの中で(そのソース内の)最古のノートが(複数ソースの中で)一番新しいものまでを返すように制限した
  • FTTにおいてDBフォールバックを有効にしている際に、ノートがないソースがある場合はDBにフォールバックするように変更した

Why

(たくさん遡ると起きやすい)
複数のTLソースの中でも保存されている期間に差があり、取得できないノートが発生してしまうため

Fix: #13488
(#13488から再掲)
FTT漏れ

Additional info (optional)

Checklist

  • Read the contribution guide
  • Test working in a local environment
  • (If needed) Add story of storybook
  • (If needed) Update CHANGELOG.md
  • (If possible) Add tests

@github-actions github-actions bot added the packages/backend Server side specific issue/PR label Mar 2, 2024
Copy link

codecov bot commented Mar 2, 2024

Codecov Report

Attention: Patch coverage is 7.69231% with 12 lines in your changes missing coverage. Please review.

Project coverage is 40.18%. Comparing base (32651ab) to head (48232ca).
Report is 8 commits behind head on develop.

Files Patch % Lines
.../backend/src/core/FanoutTimelineEndpointService.ts 8.33% 11 Missing ⚠️
...es/backend/src/server/api/endpoints/users/notes.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #13495      +/-   ##
===========================================
+ Coverage    40.08%   40.18%   +0.10%     
===========================================
  Files         1524     1524              
  Lines       188758   188768      +10     
  Branches      3465     2619     -846     
===========================================
+ Hits         75656    75854     +198     
+ Misses      112528   112372     -156     
+ Partials       574      542      -32     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

github-actions bot commented Mar 2, 2024

このPRによるapi.jsonの差分

差分はこちら

Get diff files from Workflow Page

@tai-cha tai-cha marked this pull request as ready for review March 2, 2024 06:44
@u1-liquid
Copy link
Contributor

HTL/STLはこの実装で大丈夫そうですが、タイムラインの更新が緩やかなサーバー、ユーザー個人のタイムラインの場合FTTを利用できずDBにフォールバックしてしまう割合が高くなりそうという懸念点がありそうです

FTT実装当時しゅいろさんに共有した取得漏れの例ですが

ユーザープロフィールのFTTのlimitを4とした時
あるユーザーが
今日は一般のノートを5件、リプライを0件投稿
昨日は一般のノートを5件、リプライを1件投稿したとすると
今日時点でFTTに残るノートは今日の最新の4件、昨日のリプライ1件で
スクロールして遡ると今日の4件の後すぐ昨日のリプライが見えて、その後ずっと昨日のリプライ以降のノートが見えてくるはず
今日のノート1件が表示されないという結果になってしまう

この実装だとこのユーザーは一度もファイルをアップロードしていないため常にDBにフォールバックされてしまいそうです。
すぐフォールバックさせる代わりに次のページング処理で↓の部分をもう一度照会するとフォールバックなしでも解決できそうですがどうでしょう
image

@tai-cha
Copy link
Contributor Author

tai-cha commented Mar 15, 2024

確かにそうですね!(現在多忙な上に諸事情がありすぐに反映できないかもしれないですがそのほうがパフォーマンス上良さそうなので試してみます)

(ユーザーTLもこの実装を利用しているのをやや忘れていました…)

@tai-cha
Copy link
Contributor Author

tai-cha commented Mar 22, 2024

少し思考が散らかっているのでメモ書きです

(※ここでのDBはPostgreSQLないしRDBを指す)

改めて考えてみているけど一度もファイルをアップロードしていないかどうかはDBを見てみないとわからなくて(FTTのキャッシュがredisの内容が飛ぶなどの理由で不十分な場合もある)、もう一度残った部分のFTTを参照するだけではDBには存在するのにFTTには存在しないノートがあるかどうか(つまり取得漏れがあるかどうか)がわからない

FTTにないだけでDBにはノートがあるケースは割とredisが揮発するものである前提からして(またCacheにMaxが定められているのも含め)起きそうなので悩ましい

全くDBを参照させないというのは仕組み上難しそう(これがそもそも複数のTLのキャッシュが残っている幅の差分の間でフォールバックがきちんと効くようにするPRという趣旨なのもある)

そのFTTソースに対応する条件のノートがDBに本当に存在しないかを確かめるには少なくともCOUNT句か何かでDBを見て比較自体はしないと結局問題が解決しない気がする

もう一度FTTのうちのページングで取得するだけでは取得漏れをきちんと漏らさないことは実現不可能なのでは…?

@tai-cha
Copy link
Contributor Author

tai-cha commented Mar 22, 2024

そもそもFTTに含まれていないノートをDBから持ってくるという趣旨なのでFTTが空な場合にフォールバックするのは自然ではある気がしてきた、ただ一度もファイルを投稿していなければFTTが空で常にフォールバックが起こってしまう(DBへの負荷を減らすためのFTTなのに効果が弱くなる)というのは理解できるのでどうやって落とし所つけようかしら…

@tai-cha
Copy link
Contributor Author

tai-cha commented Mar 22, 2024

タイムラインが穏やかなサーバーでDBへのフォールバック頻度が増えることはあまり大きな問題ではない気がする、それだけアクティブなユーザーも少なくなることが予想されるので(また、DBフォールバックは無効にもできる)

むしろLTLが緩やかなサーバーではより取得漏れが起きやすく深刻

フォローが0人のときもSTLで同様に毎回フォールバックが起きる問題が発生してしまう

フォールバック回数抑えつつ取得漏れをなくすにはよりもっと根本的なところを変える必要がある…?

@tai-cha
Copy link
Contributor Author

tai-cha commented Jun 15, 2024

なんかいけそうな気がしてきたので改良するか
なんでいけそうな気がしてきたか忘れた

@tai-cha
Copy link
Contributor Author

tai-cha commented Jul 25, 2024

タイムラインの更新が緩やかなサーバー、ユーザー個人のタイムラインの場合FTTを利用できずDBにフォールバックしてしまう割合が高くなりそうという懸念点がありそうです

ユーザーTLではDBにフォールバックさせないようにしました(これは取得漏れをさせずDBフォールバックをしない実装するのが難しいため)
そのうえでタイムラインが緩やかなサーバーに関する懸念に関してはそもそもDBへの参照回数もユーザー数が少なければ落ちるのであまり問題ないと考えています

@tai-cha
Copy link
Contributor Author

tai-cha commented Jul 25, 2024

FanoutTimelineNameごとにdbFallbackの手段があれば単純にその区間の行数のcountをしてくればいいのでもう少し楽になりそうではある

Comment on lines +68 to +69
let shouldFallbackToDb = ps.useDbFallback &&
(ps.preventEmptyTimelineDbFallback !== true && redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

デフォルト値がこっちのほうがわかりやすそう

Suggested change
let shouldFallbackToDb = ps.useDbFallback &&
(ps.preventEmptyTimelineDbFallback !== true && redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
ps.preventEmptyTimelineDbFallback ??= false;
let shouldFallbackToDb = ps.useDbFallback &&
(!ps.preventEmptyTimelineDbFallback && redisResult.length > 1 && redisResult.some(ids => ids.length === 0));

あとはtrueのときに無効化するというのがちょっと分かりづらい気がするので

Suggested change
let shouldFallbackToDb = ps.useDbFallback &&
(ps.preventEmptyTimelineDbFallback !== true && redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
ps.fallbackToDbIfHasEmptyTimeline ??= true;
let shouldFallbackToDb = ps.useDbFallback &&
(ps.fallbackToDbIfHasEmptyTimeline && redisResult.length > 1 && redisResult.some(ids => ids.length === 0));

とかのほうがいいのかもしれないと思いました

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このPR作成時に「中身が存在しないTLがある場合はDBを参照しに行く方が取得漏れがなくなるので妥当」という意味でprevent...をtrueにするという命名を行っていたけれどtrueの時に無効化はこの命名でも、紛らわしいか…?(ちょっと上の案、下の案、あるいはやらない、ほかの案などもそれぞれ考えてみます)

@anatawa12
Copy link
Member

現在のmasterにこれを取り込むと #13978 でSTLに追加したlocalTimelineWithReplyTo:${userId}によりSTLの一部の(db fallbackすると落ちるタイプの)テストが落ちることが確認されています (niri-la#217 等が実際に落ちてる事例になります)

これの対策にもなり、またredistimelineがからになってしまってすべてのノートをdbから取得してしまう問題の対策として、各種TL要素のうちuserTimeline${string}:${userId}${timeline}WithReplyTo:${userId}のようにユーザに関わるものについて、ユーザがアカウントに登録した際に一つだけダミーのデータを追加するというのはどうでしょうか。

懸念点としてはノートを一切しない人のためにuserTimeline系をRedisに保存することになることだと思います。

@tai-cha
Copy link
Contributor Author

tai-cha commented Aug 19, 2024

Note:

現在のmasterにこれを取り込むと #13978 でSTLに追加したlocalTimelineWithReplyTo:${userId}によりSTLの一部の(db fallbackすると落ちるタイプの)テストが落ちることが確認されています (niri-la#217 等が実際に落ちてる事例になります)

niri-la@92a55ad のようにテスト環境で挙動を回避すれば問題なさそうなのは確認したし正しそう

これの対策にもなり、またredistimelineがからになってしまってすべてのノートをdbから取得してしまう問題の対策として、各種TL要素のうちuserTimeline${string}:${userId}や${timeline}WithReplyTo:${userId}のようにユーザに関わるものについて、ユーザがアカウントに登録した際に一つだけダミーのデータを追加するというのはどうでしょうか。

これは確かにこれから作成するユーザに関してはユーザー作成時にやれば解決はできるけど、既存ユーザーのうち対象となるノートが一個もない人では引き続き同じことが起きる上にそれを既存ユーザーにまで適用しようと思うとデータを全部作ってあげるのが大変そうな気が…?(純粋に参照されたときになければ作るでいいかもしれないけど)

@anatawa12
Copy link
Member

参照したときに作るか古い人はある程度負荷を許容するか、サーバー規模で必要ならバッジを叩いてもらうかあたりを考えてました

ある程度期間参加してる人はFTTに一切ノートがないってことは少なさそうなのでそんなに発生するのかなという気がしています

@tai-cha
Copy link
Contributor Author

tai-cha commented Aug 19, 2024

Redis自体の内容が飛んだ場合や大規模サーバーで休眠ユーザーが多い場合とかはある程度考えた方がいいかも(実際割合どれぐらいいるのかわからないので有識者が意見してくれる方がありがたい)

@kozakura913
Copy link

DBに1件も無い事を示すマーカーをredisに埋め込む事を思いついた。
マーカーが記録されてるとredisが揮発していない、かつDBにもデータが無い事がDBを叩かず認識できるはず。
DBフォールバックした時に無ければredisにマーカー立てる感じで
マーカーがTTL落ちしても通常のDBフォールバックが起こるはずだから破損しないはず?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
packages/backend Server side specific issue/PR
Projects
None yet
4 participants