🦕

Deno入門~Nodeから次の時代へ~

2021/03/01に公開

はじめに

  • こんにちは ! KDDIアジャイル開発センターの小板橋です。
  • この記事は、KDDI Engineer Advent Calendar 2020の9日目の記事となります。
  • 2020/5/14にVersion1.0.0がリリースされたDeno。このDenoについて概要から実際の動作についてまとめていきます。

対象者

  • Denoについて知りたい人
  • そもそもなんでNodeやDeno使うのがいいのってなっている人
  • 実際にローカルで動かしてみたい人
  • Nodeとの違いについて知りたい人
  • 英語のドキュメント読むのしんどい人
  • etc...

Denoってなんだ

  • Denoとは、V8エンジンを使用した、JavaScriptおよびTypeScript用のシンプルでモダンなランタイムです。
  • 作者は、Nodeを作ったRyan Dahlです。天才です。ちなみに彼のGitHubのアカウントはこちら
  • 設計としては、シングルプロセス・シングルスレッド・非同期I/Oに基づいている為、Nodeと同じです。
  • そして。キャラクタが可愛いです。

Denoのキャラクタ

シングルプロセス・シングルスレッド・非同期I/Oのメリットって何??

NodeやDenoを使うメリットについて、ちゃんと書いていきます。
メリット

  • ブラウザの外でJavaScriptが使える点
  • C10K問題の解決するための手段になる点

ブラウザの外でJavaScriptが使える点については、JavaScriptの実行環境だからその通りで、気になるのはC10K問題の解決するための手段になる点だと思います。

C10K問題って何?

結論から答えると、サーバーサイドのハードウェア性能に問題はないのだが、クライアント側の同時接続数が多くなることでサービス応答が遅くなってしまうという問題です。

原因は何??

大きく分けて2つあると思っています。

1.プロセス数の上限
→ Apacheを例に考えると、一般的にApacheではリクエストを捌くときは、1つのリクエストに対して1プロセスを割り当て処理をする方式が主流でした。
このOS内で動作するプロセスには、上限が決まったプロセスIDが割り当てられています。
その為、上限を超えるとプロセス数の上限以上のリクエストを同時に処理することができなくなるのです。

2.ファイルディスクリプターの上限

→ データベースを扱う場合、リクエストごとにDBサーバーに接続するとそのリクエストの数だけ、OSが読み書きしているファイルへの参照を抽象化したキー(ファイルディスクリプター)を消費します。
また、ファイルディスクリプターは一度に使用できる上限がOS/プロセスごとに設定されています。
その為、設定されている値以上の同時接続はできないので、パフォーマンスを低下させる原因となります。

解決策

このC10K問題の解決策となるのが、シングルプロセス・シングルスレッド・非同期I/Oです。

まあ、単純に処理するサーバのスケールアップという方法もあります。しかし、実際に運用するとなると無駄なマシンスペックになるのでこの方法は外します。

シングルスレッドでの動作

スレッドとは、スレッドはCPU利用の単位です。
また、CPU利用の単位として、プロセススレッドがありますが、実行中のプログラムをプロセスと呼び、プロセスは一つ以上のスレッドを持ちます。

なので、複数のスレッドで動作するというのは、複数リクエストが来たとき、利用するプロセスやスレッドを増やし、それぞれのリクエストの処理を同時に行います。

ここからが、Node, Denoを使う上で重要なポイントです。
Node、Denoはシングルスレッドで動作します。
ただ、ここで重要なことは、ただ単純にシングルスレッドで動かすのではなく非同期I/Oを使用している点です。
ただ、シングルスレッドを採用するだけだと、処理の同時実行になるので非効率な実行方法になるのがオチです。

非同期I/Oとは?

従来の同期的なI/Oは、サーバ側で何かしらのデータを取得(例えばDBサーバから取得)する場合、データが返却されるまでクライアント側の処理も止めてしまいます。

それに対して非同期I/Oは、CPU以外のリソースを使う場合に処理を止めずに、後続の処理を行いI/O完了後に本当にやりたかった処理を行います。
つまり、NodeやDenoはこの非同期I/Oを駆使し、時間軸で処理を分散することができるのです。

Denoの大きな特徴

Nodeでは、JavaScriptしか実行できませんでしたが、Denoでは、TypeScriptのコードもそのまま実行することができます。
→ TypeScriptバージョン4.1(2020/12/08時点)では、Ecmaの最新をほぼ網羅しているうえに、TypeScriptコンパイラを内蔵しているしているのでBabelなどを変換せず実行できます。

NodeとDenoの差異

では、NodeとDenoの違いについては新しいV8の機能を使える点とTypeScriptが使える点が違うだけなのか??というとそうではありません。
その差異を知るヒントがRyan DahlのNodeでの設計ミスというjsconf2018の発表を読み解くことできます。
発表資料
Ryan Dahlによるjsconfでの発表

Nodeでの設計ミス

Promise

これは、2009~2010年の騒動がきっかけです。
Promiseは、2009年に一度Nodeのcoreに記述されていたのですが、2010年にpromiseをすべて消すということを決定しました。
しかし、その後主流になったasync/awaitの抽象化を行うためにはPromiseは最初から入れておくべきという判断がなされました。

では、なぜ当初消す判断になったのかというと、当時はcallbackのほうが分かりやすく性能面の利点が大きかった為Promiseにするのはcoreの実装としては不要という判断になったそうです。

そこでDenoではすべての非同期関数はコールバックではなく、promiseを返します。(DenoはNodeとは異なるAPIを提供)

Security

V8エンジンは、JavaScriptのランタイムでありファイルシステムへのアクセスやネットワークアクセスをする為の機能は提供していません。
それに対してNodeは、fsやhttpといったファイルやネットワークリソースへのアクセスを提供します。

しかし、現在のNodeは、Lintやbuildツールとして実行しているときにネットワーク等のリソースへアクセスする権限や、ファイルの書き込みの権限は不要でだと思います。

そこでDenoでは、ファイルアクセス、ネットワーク通信、およびシステム環境へのアクセスに明示的な権限が必要になります。
例: ファイルへの書き込みが必要な場合は実行時に

--allow-write

ネットワークアクセスが必要な場合は

--allow-net

を実行時オプションとして指定する必要があります。

package.json


moduleは、package.json以下にあるディレクトリを指すのではなく、コアが提供している単一ファイルで十分であり、import時に下記のようにurlで指定させる方法や

import foo from "https://example.com/hoge/v1/sample.js" 

下記のようにファイルで指定できるようにするだけでいいのではないかと考えているそうです。

import foo from "./hoge/v1/sample.js"

そこでDenoでは、モジュール解決アルゴリズムでpackage.jsonを使用しません。また、npmを使用せず、URLまたはファイルパスとして参照されるモジュールを使用します。

node_modules

moduleの解決アルゴリズムは、node_modules内の依存関係をたどって依存解決をします。そのため、解決アルゴリズムが相当複雑になってしまっています。
それを比喩した図が下記ですね。。笑

解決アルゴリズムの複雑さ

上記で述べた解決アルゴリズムの複雑さとはどういうことなのかというと、検索対象となるものを考えると分かります。
下記をimportした場合を考えます。

import "hoge"

そうすると、Nodeは、およそ次のものを検索します。

1.標準ライブラリのhoge
2.npm installでインストールしたhoge
3.NODE_PATH内にあるhoge.js
4.NODE_PATH内にあるhoge/index.js

この煩雑さこそが問題だということがわかります。

そこでDenoでは、require()はサポートせず、かわりにES Modulesを使いサードパーティのモジュールは下記のようにURLを介してインポートし、

import * as log from "https://deno.land/std/log/mod.ts";

指定可能なモジュールのパスについては、絶対パスもしくは基となるファイルからの相対パスのみとし、拡張子やindex.jsは省略不可としました。

Denoを実行

環境

  • MacBook Pro: 10.15.4
  • deno: 1.5.3 (リリースversionのLatestを使用しました)

インストール方法

  • macならbrewでいけます。
brew install deno
  • versionの確認
❯ deno --version
deno 1.5.3 (UNKNOWN, release, x86_64-apple-darwin)
v8 8.7.220.3
typescript 4.0.5
  • インストールができたので実行していきます。
  • 下記のコマンドをそのまま実行してみます。
deno run https://deno.land/std/examples/welcome.ts
  • 実行結果
  • うまくいきました。
❯ deno run https://deno.land/std/examples/welcome.ts
Download https://deno.land/std/examples/welcome.ts
Warning Implicitly using latest version (0.80.0) for https://deno.land/std/examples/welcome.ts
Download https://deno.land/std@0.80.0/examples/welcome.ts
Check https://deno.land/std/examples/welcome.ts
Welcome to Deno 🦕

  • 次は世界を開きましょう笑
    ローカルに名前は何でもいいのですが、tsファイルを作成しましょう。
    僕は、deno-example.tsとしました。
    そして以下のファイルをそのままコピペしていただければいいと思います。
import { serve } from "https://deno.land/std/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

  • 何も考えず実行してみる。

そうすると以下のようにエラーが発生します。
これは、先ほどのDenoの特徴でもある、プログラム実行時必要な権限を指定する必要があるためです。
このプログラムの場合ですと、ネットワークアクセス権限が必要です。

~/Desktop 2m 54s
❯ deno run deno-example.ts

error: Uncaught PermissionDenied: network access to "0.0.0.0:8000", run again with the --allow-net flag
    at processResponse (core.js:224:13)
    at Object.jsonOpSync (core.js:248:12)
    at opListen (deno:cli/rt/30_net.js:32:17)
    at Object.listen (deno:cli/rt/30_net.js:207:17)
    at serve (server.ts:301:25)
    at deno-example.ts:2:11
  • 気を取り直して、もう一度実行してみます。

下記が実行結果です。上手く行きましたね!

~/Desktop
❯ deno run --allow-net deno-example.ts

Download https://deno.land/std/http/server.ts
Warning Implicitly using latest version (0.80.0) for https://deno.land/std/http/server.ts
Download https://deno.land/std@0.80.0/http/server.ts
Download https://deno.land/std@0.80.0/http/_io.ts
Download https://deno.land/std@0.80.0/io/bufio.ts
Download https://deno.land/std@0.80.0/async/mod.ts
Download https://deno.land/std@0.80.0/encoding/utf8.ts
Download https://deno.land/std@0.80.0/_util/assert.ts
Download https://deno.land/std@0.80.0/textproto/mod.ts
Download https://deno.land/std@0.80.0/http/http_status.ts
Download https://deno.land/std@0.80.0/async/delay.ts
Download https://deno.land/std@0.80.0/async/pool.ts
Download https://deno.land/std@0.80.0/async/mux_async_iterator.ts
Download https://deno.land/std@0.80.0/async/deferred.ts
Download https://deno.land/std@0.80.0/bytes/mod.ts
Check file:///Users/yoshitaka.koitabashi/Desktop/deno-example.ts
http://localhost:8000/

実際にブラウザで叩いて見ると、見事にhello worldを表示できました。

おわりに

やはりRyan Dahlすごいっすね。笑
それに尽きます。

参考文献

Deno: github
Deno公式ドキュメント
Denoについての記事

Discussion