Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

TypeScript でバックもやるって実際どう? 実運用で困ったこと3選

TypeScript でバックもやるって実際どう? 実運用で困ったこと3選

ROSCA株式会社さん主催のイベント フロントからバックエンドまで、TypeScriptでシームレスな開発エクスペリエンスを で発表させていただいた際に使用したスライドです。

Yuichiro SERITA

November 27, 2024
Tweet

More Decks by Yuichiro SERITA

Other Decks in Programming

Transcript

  1. • 株式会社ケップル KEPPLE CREATORS LAB 所属 • フルスタック的に Web まわりのエンジニア

    をやっています • めちゃくちゃ夜型です。正午の登壇すら 正直言ってちょっとつらい • とにかくコーヒーが好き 芹田 悠一郎 Yuichiro SERITA
  2. 立ち位置 • TypeScript のバックエンドに NestJS を採用 • 小規模なもので Hono を使っているものもある

    • そんなに特別なことをバックエンドでやっていない DB などから取得したデータを集計して JSON にして返す程度のレベル感
  3. 持って帰ってほしいこと • すでに TypeScript でバックエンドを構築している方 → TypeScript でバックエンド構築するとやっぱこうなるんだ、みんな同じだね • これから TypeScript

    でバックエンドを構築したい方 → TypeScript でバックエンド構築しても、トラブルってその程度で済むんだー、   へー、なるほどねー
  4. 調査してみる • performance.now() 使って printf デバッグ • 日付の変換処理が遅かった • 外部

    API から yyyy-MM-dd の形式で string の日付が返ってくる • Date にして返す
  5. 原因のコード 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; }
  6. 原因 • Node.js の例外は遅い。throw して catch するだけで 1 ミリ秒くらい •

    date-fns の parse は実は遅い。 0.1ミリ秒の桁 • date-fns-tz (v2) の zonedTimeToUtc も実は遅い。 0.1ミリ秒の桁
  7. 対応 • 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倍程度高速化できた
  8. 原因 • どうしてもなくせない CPU bound な処理があった • データが増えてきて顕在化した • Node.js

    は基本的にシングルプロセス・シングルスレッド • CPU bound な処理をすると他の処理がブロックされてしまう
  9. 解決策 • Worker threads を使って別スレッドで処理する ◦ 頻繁に叩かれる API ではなかった •

    他にもいろいろ手はある ◦ Lambda に逃す ◦ 別言語の別バイナリに投げる
  10. 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); }) );
  11. Worker threads 呼ばれる側 (大枠) import { parentPort } from "worker_threads";

    parentPort.once("message", async (arg) /> { // いろいろ処理する // Promise で扱うなら postMessage は 1 回だけ呼び出すほうが面倒がない parentPort.postMessage(result); }
  12. opinionated なフレームワークのデファクトスタンダードがない • Node.js の世界では express のような薄いフレームワークが人気 (たぶん) • Ruby

    だったら Ruby on Rails, Java だったら Spring, PHP だったら Laravel, のように、他言語だったら opinionated でデファクトスタンダードな フレームワークがある • Node.js にはデファクトスタンダードがない • 悩んだ結果、NestJS を選択 (2020年くらいから使ってます)
  13. NestJS を実際に使った感想 • REST API と GraphQL をまとめて扱いやすい ◦ 書き味がだいたい同じ、Logger

    や Interceptor を共通化できる、などなど • NestJS の作法にしっかり従う必要がある • Spring Boot や ASP.NET Core あたりに触れたことがあると馴染みやすい • Angular に触れたことがあると馴染みやすい
  14. RxJS とは (ざっくり) • 非同期処理の枠組みで、 Observable と呼ばれる非同期でデータを出力するス トリームが土台になっている • Observable

    に対して宣言的に処理を書いていく 例:1,2,3 が流れるストリームを10倍する of(1, 2, 3).pipe(map(x /> 10 * x)) https://rxjs.dev/api/operators/map より抜粋
  15. RxJS が (我々にとって) オーバースペック • RxJS で簡単にできて Promise では面倒なことはたくさんあるが、 ほぼ使っていない

    (クライアントがコネクションを切ったら全処理を中断するとか) • 学習コストが高い • 記述量がかなり増えてしまう ◦ 複数の非同期処理が絡むとき、Promise なら await を書けばいいだけだが forkJoin や concatMap が必要 ◦ テストでは毎回 subscribe して done を呼ぶ
  16. Promise を使っていいことにしました • 新たに実装する Resolver, Controller, Service は Promise を使う

    • @nestjs/axios の HttpService は引き続き使用する。 firstValueFrom で Observable から Promise に変換する • リクエストのキャンセルなどが必要になったら別途考える
  17. 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 の恩恵受けてるユーザーほぼいないのかも……
  18. 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 オブジェクトを受け取 れるようになっていない、などなど……全部実装する必要がある
  19. 日付のフォーマットの対応 • Custom Logger を実装して対応 (まだやってない) ◦ Custom Logger 自体はあるが、ConsoleLogger

    に移譲している • 今すごく困っているかと言われると微妙なので、後回しにしている