急にリクエストが1.5倍に増えてクソデカJSONと戦った
こんにちは
scrapboxのプロダクトオーナーです
機能の取捨選択や設計・運用に責任を持つ役割
今日の話
1. Scrapboxとは
2. 最近作った機能をざっと紹介
サーバー負荷が右肩上がり
動画
(ここでデモ。ページを作ってみる)
技術スタック
他のNotaプロダクトとの関係
Gyazoで画像や動画をキャプチャし、Scrapboxに埋め込める
リモート開発
フルリモートワーク、年2,3回
泥酔する為に出社していた
原作者、無茶な使い方開拓者
ScrapboxをScrapboxで開発している
用語・概念に、別ページで定義を好きなだけ書ける
格言・スローガンの様なページを作る
影響しあう機能の設計時に、リンクで説明できる
理解・考察が足りていない事をScrapboxで書く
書くとだんだんわかってくる
クソデカ感情を持っている人が直してくれる
通話なし、ドキュメントで相談する
Scrapboxをゴリゴリ書いていくスタイル
プルリクをぶつけあう
開発者同士で言葉を尽くして説明しあう
動くプロトタイプを触る
こうだね(確認)、を繰り返す
書くとわかってくる
例:
が最初こんな感じで雑に書く
内部仕様に詳しくなくても、とにかく書く
不満に思った時点で書けばいい。わかる相手には伝わる
詳しい人が手直しすれば良い
直してる側も、説明(対話)を通してようやく言語化できる事がよくある
意味が変わらなければ、他人の発言を添削・修正してもぜんぜんok
文中の専門用語を、別ページへのリンクにする
タイトルも変える
人に聞くのはコスト高いけど自分で見られるから良い
何度も説明する手間が省ける
最近の機能をざっと紹介
カラーテーマ追加 by
エディタ背景が黒いテーマもあります
DarkとMinimal、愛してます
ファイルアップロード
1ファイル100MB
1アカウント合計1GBまでアップロードできる
nintendo switchの30秒録画をアップロードするのに便利
削除したページの復活
これはなぜでしょうか?
実装できてないからです
business plan(有料)専用の機能
監査ログ
business plan(有料)専用の機能
直近3ヶ月分のイベントが見れる
誰がどのページにアクセスした、ファイルアップロードした
招待URLが使われた
project memberの追加・削除
admin任命・解任など
IPアドレスも見れる
検索結果を30→100件に増やした
リンク貼っていないやつを探すのに便利です
スクロールすれば勝手に横断検索してくれるのとっても良いUIだ
被リンク数が多いページからの被リンク数が多いページほど、上にソートされる
クソデカJSONとの戦い
急に1.5倍ぐらいリクエストが跳ね上がった
2020年4月頭
新年度、新学期
急激にリクエストが増えると
これまで問題として現れていなかった不具合が表面に出てくる
人が多い時間帯で、急にDBのCPUが90%に張り付いたりする
サービスが30分ほど不調になった事が3日間、合計5回ほどあった
胃が痛くなった
かわいそう
重いところを見つける方法
特定のURLパスが遅いのがわかる
appの計算待ちか?DB待ちか?もわかる
5秒以上レスポンスがかかったリクエストを全てslackに通知
特定のscrapbox projectが遅いのがわかる
めちゃうるさくなる。静かになるまでがんばる
数ヶ月〜年単位のグラフでも見る
1週間単位で見てると今日も横ばいだ、異常ナシ!と油断してたらこれ
年単位で見ると超右肩上がりだったりする
サーバーのmetricsは貯めておく
重くなった時、何をどこまで戻せば元気になるのかわかる
危機が迫っているのもわかる
2020春 急に重くなった主な原因
index効いてないDBアクセス1つ
巨大なJSONの生成コスト4つ
関連ページリストAPI
titles API
WebWorkerとUIスレッド間のpostMessage
Auto project backup
index効いてないDBアクセス
問題
MongoDBで
page.icons = { shokai: 3, nota: 1 }
みたいなkey valueにすると
.icons
に "shokai"
というkeyがあるpageを探す時、indexが効かない
こういうデータ構造はやめよう
解決
素直に page.icons = ['shokai', 'nota']
というArrayにした
サービスを止めずにschemaを変更
まずappを、readはkey valueとArrayどちらでも可能、writeはArrayでやるようにする
これでappを動かしているだけで徐々にデータがmigrateされていく
同時にkey valueをArrayに変えるbatchも走らせる
最後にappをArrayしかread/writeしないように修正
背景
結局、個数は特に使いみちが無く、非効率なデータ構造のまま運用していた
#member 人物、Gyazo、Scrapboxなどの大きなhashtagを表示しないようにした
問題
解決
超巨大な2 hop linkは省略する
Gyazoの先の2 hop linkが多すぎるので省略された
1 hop linkのGyazoから辿れるし必要十分だろう
titles API
project内のページタイトルとリンク(空も含む)が全部返ってくるパワフルなAPI
ユーザーのキー入力する前にデータを持っておきたい
UserScriptでよく使ってるAPIだ
このAPIから自前で逆リンクや2 hop linkを計算できたりする
titles APIのJSONが巨大だった
/api/paegs:projectName/search/titles
が明らかに遅かった
原因
何万ページのprojectがけっこうある
数十MBのJSONがリアルタイムに作られていた
大量のページをscanしてDB待ちが長い
解決
pagingした
1000件ずつ取得、next IDで辿っていく
API fetch完了まで時間がかかるようになった
fetch中はcacheから検索する
このAPIだけ時間かかるの理由がようやくわかった
window.postMessage
に巨大objectを送るとブラウザが死ぬ
背景
送受信中、UIスレッドがフリーズする
V8がたまにout of memoryでクラッシュ
DataCloneError: Failed to execute 'postMessage' on 'Worker': Data cannot be cloned, out of memory.
要素数20万ぐらいの配列で発生するようだ
解決
1000件毎のchunkに分割して送受信させた
{data: { pages: Array }, start: true}
で開始
{data: { pages: Array } }
をどんどん貯めて
{data: { pages: Array }, end: true}
で完了
dataの中身の型がArrayなら末尾push、objectなら Object.assign
でmergeする
これを双方向にやる
実行速度は100〜300msecしか遅くならなかった
1ページずつJSON化してwritable streamに出力した
背景
projectまるごとバックアップする機能
Webサーバーではなくbatch処理で実行されている
巨大なscrapbox projectがわりとある
本文だけで100MB超え
数万ページ
フォーマットが悪い
json{
"name": "nota"
"pages": [
{
"title": "page1title",
"lines": [ "page1title", "line2", "line3" ]
},
{
"title": "page2title",
"lines": [ "page2title" ]
}
]
}
これを普通に作ると
DBからページを読み出し、全部メモリに載せてから
const str = JSON.stringify(object)
1つの巨大な文字列ができる
解決
JSONをパーツ毎に動的に作り、1つ1つstreamに流した
フォーマット変えずに済んだ
まず頭を作ってwrite
json{
"name": "nota"
"pages": [
ページを1つずつwriteしていく
MongoDBのquery cursorというイテレータで1件ずつ取得、JSON化してwrite
json {
"title": "page1title",
"lines": [ "page1title", "line2", "line3"]
},
最後に閉じる
JSONという効率の悪いフォーマットを選んだ事を少し後悔もした
末尾まで受信しないとparseできないフォーマットよ
今後export機能を作る人へ
1行1レコードなフォーマット使った方がいいぞ
ScrapboxのHTML、JS、CSS、メニューのアイコン画像、フォント、WebWorkerの事
Scrapboxはページを開いた時にHTMLやJSをダウンロードしていない
初回アクセス時を除く
HTMLやJSはそのCache Storageから読み出す
assets cacheはバックグラウンドで更新される
つまりネイティブアプリみたいになっている
HTTP API
先にCache Storageから取得してReactに渡して画面描画しつつ、裏でHTTPリクエストして差し替え
一度見たページはオフラインでも読める
デプロイと同時にassets cacheの再取得が殺到していた
assets cache更新フロー
デプロイ
古いサーバーが「更新します」というメッセージをSocket.IOで送信、切断
クライアントは、Socket.IO再接続した時にassets cacheを再取得する
問題
みんな同時に再接続し、一斉に再取得しにくる
解決
ウィンドウがactiveではない場合はランダムに待つようにした
他にもいくつかのAPIが、socket.ioの切断・再接続をトリガーとして同時にリロードしていた
同様にウィンドウがactiveでない場合ランダムディレイを入れた
まとめ
サーバーのmetricsとって、たまに年単位のrangeで眺めよう
右肩上がりに気づける
クソデカJSONは分割しよう
paging、chunking
ちょっとずつ送る
もしくはデカいままstreamで扱う
JSONの配列になってる部分をちょっとずつ作って送信
デプロイ直後にアプリケーションがどうなるかも気をつける
おわり
チャンネル登録と高評価お願いします
リアルタイムでいいね増えてていいぞ
Scrapbox上で見てるけどいいのかな