Slide 1

Slide 1 text

TypeScript でバックもやるって実際どう? 実運用で困ったこと3選 芹田 悠一郎 @株式会社ケップル KEPPLE CREATORS LAB

Slide 2

Slide 2 text

● 株式会社ケップル KEPPLE CREATORS LAB 所属 ● フルスタック的に Web まわりのエンジニア をやっています ● めちゃくちゃ夜型です。正午の登壇すら 正直言ってちょっとつらい ● とにかくコーヒーが好き 芹田 悠一郎 Yuichiro SERITA

Slide 3

Slide 3 text

前置き

Slide 4

Slide 4 text

TypeScript でバックもやる

Slide 5

Slide 5 text

賛否両論

Slide 6

Slide 6 text

飛び交うポジショントーク

Slide 7

Slide 7 text

何も信じられない……

Slide 8

Slide 8 text

立ち位置 ● 小さいエンジニア組織 ● そのわりに多いプロダクト ● つまり全員フルスタックじゃないといけない ● フロントが TypeScript なので全員 TypeScript を使える (共通項) ● バックエンドも TypeScript でいいじゃん

Slide 9

Slide 9 text

立ち位置 ● TypeScript のバックエンドに NestJS を採用 ● 小規模なもので Hono を使っているものもある ● そんなに特別なことをバックエンドでやっていない DB などから取得したデータを集計して JSON にして返す程度のレベル感

Slide 10

Slide 10 text

弊社の代表的な構成 フロント BFF データ ソース Next.js NestJS NestJS Hono TS 以外もある

Slide 11

Slide 11 text

話すこと ● TypeScript でバックエンドを運用してて遭遇した困りごと ● その困りごとの対処方法

Slide 12

Slide 12 text

持って帰ってほしいこと ● すでに TypeScript でバックエンドを構築している方 → TypeScript でバックエンド構築するとやっぱこうなるんだ、みんな同じだね ● これから TypeScript でバックエンドを構築したい方 → TypeScript でバックエンド構築しても、トラブルってその程度で済むんだー、   へー、なるほどねー

Slide 13

Slide 13 text

本題

Slide 14

Slide 14 text

#1 例外が遅い。date-fns が意外と遅い

Slide 15

Slide 15 text

当時の状況 ● とある API のレスポンスが、件数が多いときにやたら遅い ● 外部 API から取ってきたデータに対し、フィルタしてソートして先頭1000件を返 すだけの処理 ● 数万件くらいのデータがたまにあって、30秒以上かかってタイムアウトする

Slide 16

Slide 16 text

調査してみる ● performance.now() 使って printf デバッグ ● 日付の変換処理が遅かった ● 外部 API から yyyy-MM-dd の形式で string の日付が返ってくる ● Date にして返す

Slide 17

Slide 17 text

原因のコード import { isValid, parse } from 'date-fns'; import { zonedTimeToUtc } from 'date-fns-tz'; export function tryParseDateAsJST( dateString: string | null | undefined, ): Date | undefined { if (dateString /= null) { return undefined; } const formats = ['yyyy/MM/dd', 'yyyy/M/d', 'yyyy-MM-dd', 'MM/dd', 'M/d']; // referenceDate は年をとりたいだけなのでタイムゾーン考慮しない const referenceDate = new Date(); for (const format of formats) { try { const zonedDate = parse(dateString, format, referenceDate); if (!isValid(zonedDate)) continue; return zonedTimeToUtc(zonedDate, JST); } catch { // noop } } return undefined; }

Slide 18

Slide 18 text

原因 ● Node.js の例外は遅い。throw して catch するだけで 1 ミリ秒くらい ● date-fns の parse は実は遅い。 0.1ミリ秒の桁 ● date-fns-tz (v2) の zonedTimeToUtc も実は遅い。 0.1ミリ秒の桁

Slide 19

Slide 19 text

対応 ● parse するところは手書き const [yyyy, MM, dd] = dateString.split('-'); if (yyyy /= null /& MM /= null /& dd /= null) { return new Date( parseInt(yyyy, 10), parseInt(MM, 10) - 1, parseInt(dd, 10), ); } ● zonedTimeToUtc は 1000 件に絞ってからかける → 50倍程度高速化できた

Slide 20

Slide 20 text

#2 CPU bound な処理

Slide 21

Slide 21 text

当時の状況 ● 原因不明のタイムアウト ● バックエンドがまったく応答しなくなる ● 鬼のように飛んでくるアラート ● heartbeat すら応答しない

Slide 22

Slide 22 text

当時の状況 ● とりあえずコンテナの実行環境のスペックを上げてみたりしたが解決しない ● ひたすらログとにらめっこ ● エラー発生時に特定の処理が走っていることに気づく

Slide 23

Slide 23 text

原因 ● どうしてもなくせない CPU bound な処理があった ● データが増えてきて顕在化した ● Node.js は基本的にシングルプロセス・シングルスレッド ● CPU bound な処理をすると他の処理がブロックされてしまう

Slide 24

Slide 24 text

解決策 ● Worker threads を使って別スレッドで処理する ○ 頻繁に叩かれる API ではなかった ● 他にもいろいろ手はある ○ Lambda に逃す ○ 別言語の別バイナリに投げる

Slide 25

Slide 25 text

Worker threads 呼ぶ側 (大枠) import { Worker } from "worker_threads"; new Observable((subscriber) /> { const worker = new Worker(workerFilePath); worker.postMessage(args); worker.on("message", (message) /> { subscriber.next(message); }); worker.on("error", (error) /> subscriber.error(error.message)); worker.on("exit", (code) /> { // exit code を見ていろいろやる処理がありますが省略しています worker.terminate(); subscriber.complete(); }); }).pipe( catchError((err) /> { // worker側のエラーをメインスレッド側でハンドリングしないとメインスレッドが落ちるので注意 return throwError(() /> err); }) );

Slide 26

Slide 26 text

Worker threads 呼ばれる側 (大枠) import { parentPort } from "worker_threads"; parentPort.once("message", async (arg) /> { // いろいろ処理する // Promise で扱うなら postMessage は 1 回だけ呼び出すほうが面倒がない parentPort.postMessage(result); }

Slide 27

Slide 27 text

#3 NestJS の気持ちがたまに分からない

Slide 28

Slide 28 text

#3 NestJS の気持ちがたまに分からない #3.1 前提の話 (なぜ NestJS か)

Slide 29

Slide 29 text

opinionated なフレームワークがほしい ● 強い制約がある代わりに生産性が高いフレームワークを opinionated という ● プロダクトを早く届けるにはレールに乗るほうがよい ● 必要なものが最初から揃っている ● 規約をゼロから作らなくても 「このフレームワークの作法ではこうだから」 で 決められる

Slide 30

Slide 30 text

opinionated なフレームワークのデファクトスタンダードがない ● Node.js の世界では express のような薄いフレームワークが人気 (たぶん) ● Ruby だったら Ruby on Rails, Java だったら Spring, PHP だったら Laravel, のように、他言語だったら opinionated でデファクトスタンダードな フレームワークがある ● Node.js にはデファクトスタンダードがない ● 悩んだ結果、NestJS を選択 (2020年くらいから使ってます)

Slide 31

Slide 31 text

NestJS を実際に使った感想 ● REST API と GraphQL をまとめて扱いやすい ○ 書き味がだいたい同じ、Logger や Interceptor を共通化できる、などなど ● NestJS の作法にしっかり従う必要がある ● Spring Boot や ASP.NET Core あたりに触れたことがあると馴染みやすい ● Angular に触れたことがあると馴染みやすい

Slide 32

Slide 32 text

#3 NestJS の気持ちがたまに分からない #3.2 困ったこと その1 RxJS

Slide 33

Slide 33 text

RxJS とは (ざっくり) ● 非同期処理の枠組みで、 Observable と呼ばれる非同期でデータを出力するス トリームが土台になっている ● Observable に対して宣言的に処理を書いていく 例:1,2,3 が流れるストリームを10倍する of(1, 2, 3).pipe(map(x /> 10 * x)) https://rxjs.dev/api/operators/map より抜粋

Slide 34

Slide 34 text

RxJS が (我々にとって) オーバースペック ● RxJS で簡単にできて Promise では面倒なことはたくさんあるが、 ほぼ使っていない (クライアントがコネクションを切ったら全処理を中断するとか) ● 学習コストが高い ● 記述量がかなり増えてしまう ○ 複数の非同期処理が絡むとき、Promise なら await を書けばいいだけだが forkJoin や concatMap が必要 ○ テストでは毎回 subscribe して done を呼ぶ

Slide 35

Slide 35 text

Promise を使っていいことにしました ● 新たに実装する Resolver, Controller, Service は Promise を使う ● @nestjs/axios の HttpService は引き続き使用する。 firstValueFrom で Observable から Promise に変換する ● リクエストのキャンセルなどが必要になったら別途考える

Slide 36

Slide 36 text

NestJS のリクエストのライフサイクル ここだけ Promise にする Middleware Guard Interceptor Pipe Controller / Resolver Service Interceptor Exception filter

Slide 37

Slide 37 text

RxJS と @nestjs/axios に関する余談 ● @nestjs/axios の HttpService は、 Observable の Teardownlogic でリクエストをキャンセルするように作られている ● しかし、その処理は 2020年5月から2024年10月までずっと壊れていた ○ https://github.com/nestjs/nest/pull/4803 ここで壊れて ○ https://github.com/nestjs/axios/issues/1217 この issue がきっかけで修正された ● Observable の恩恵受けてるユーザーほぼいないのかも……

Slide 38

Slide 38 text

#3 NestJS の気持ちがたまに分からない #3.3 困ったこと その2 モジュールシステム

Slide 39

Slide 39 text

NestJS のモジュールシステム ● Angular とほぼ同じモジュールシステム ● import していないモジュールの Service を DI して使おうとするとエラー https://docs.nestjs.com/modules より抜粋

Slide 40

Slide 40 text

モジュールシステムが (我々にとって) オーバースペック ● モジュールシステムが必要になるほどの規模のバックエンドを作ることがない ● テストを書くときに独特の作法が必要で学習コストが高い ○ 公式ドキュメントに書いてない (読み取れない) ことが多い

Slide 41

Slide 41 text

ドキュメントとコミュニケーションで解決 ● 分からないところは NestJS ソースコードを読む ● ドキュメントを残す ○ PR のコメントに書く、コード中にコメントを書く、程度でもよい ● 知見を共有する ○ レビューやオンボーディングでしっかり共有する

Slide 42

Slide 42 text

#3 NestJS の気持ちがたまに分からない #3.4 困ったこと その3 Logger が独特

Slide 43

Slide 43 text

Logger が独特 ● デフォルトの ConsoleLogger の日時の出力フォーマットが MM/dd/yyyy, h:mm:ss AA で固定 (例: 11/21/2024, 5:48:32 PM) ● 引数が独特すぎて変更できないため、カスタムロガーを実装するにしても苦しい ○ 最後の引数が string だったら context とみなす、stacktrace は引数の数を2 つにして2つ目に string で入れる、stacktrace かどうかの判定は正規表現で やっている、引数の数が2つではない場合は別の判定、Error オブジェクトを受け取 れるようになっていない、などなど……全部実装する必要がある

Slide 44

Slide 44 text

日付のフォーマットの対応 ● Custom Logger を実装して対応 (まだやってない) ○ Custom Logger 自体はあるが、ConsoleLogger に移譲している ● 今すごく困っているかと言われると微妙なので、後回しにしている

Slide 45

Slide 45 text

引数が独特すぎる問題の対応 ● 引数を1つだけ渡すようにし、意図しない挙動を踏み抜くことを防ぐ ○ Node.js の util.inspect を通して string で渡す ● 標準の BaseExceptionFilter など、内部で呼び出しているものに関しては 特になにもしない

Slide 46

Slide 46 text

おわりに

Slide 47

Slide 47 text

困ったことが全然出てこなくて資料作成に困った ● 3選と銘打ったはいいものの、実運用で困ったことが出てこなくて困った ● CPU bound な処理がキツいのなんて最初から分かってることなので、 実際のところ実運用で困ったとは言い難い ● NestJS に対していろいろ言いましたが、何の文句もないフレームワークなんて 存在しないと思う

Slide 48

Slide 48 text

NestJS は実運用に耐えます ● NestJS 嫌いな人がネット上に多い ● でも余裕で実運用できます (我々のようなスタートアップにおいては特に) ○ 20チームで分担して1つの大きなバックエンドを作っていて…… みたいな組織には向かないと思います ● バグ踏んで困った回数で言えば Next.js のほうが全然多い ● 怖がる必要はない

Slide 49

Slide 49 text

TypeScript でバックもやるって実際どう?

Slide 50

Slide 50 text

普通に運用できます!

Slide 51

Slide 51 text

おわり

Slide 52

Slide 52 text

エンジニア絶賛募集中です! ● TypeScript が好き ● スタートアップが好き ● 「プロダクトエンジニア」に興味がある ぜひお話ししましょう! https://lab.kepple.co.jp/