lasciva blog

開発して得た知見やwebビジネスのストック

Elastic BeanstalkにRailsアプリケーションをデプロイした

Elastic BeanstalkでRailsアプリケーションを動かしたので、ハマったところ等をまとめました。
用途は少し限定されますが、負荷も大きくないような簡単なアプリケーションを動かすには良い選択肢になるかなと思いました。

特徴/メリット

  • Dockerfileがあるアプリケーションを簡単に構築できる(※ 他の言語もサポートされてる)
  • Nginxなどのwebサーバーの設定がほとんど不要なので、アプリケーションさえあれば動かせる
  • デプロイフローの構築も割と簡単

デメリット

  • 設定等にハマると、普通に構築した方が早いと思った
  • ログがあまり充実してなさそうなので、要確認
  • ガッツリ本番でリクエストを捌くレベルでは、心もとなく感じた

環境

インフラ構成

f:id:hacking15dog:20210124174200p:plain

デプロイ

GitHub Actionsを利用しました。

  1. Github ActionsでDockerのビルド
  2. imageをECRにpush
  3. Elastic Beastalk上で動かすDockerfile等の作成
  4. 作成したDockerfileをElastic Beastalkへデプロイ

※ 事前にAWSでデプロイユーザを作成し、そのクレデンシャルをGitHubのsecretsで設定しています。

name: AWS Elastic Beanstalk Deploy

on:
  push:
    branches:
      - production

env:
  AWS_REGION: ap-northeast-1
  AWS_ECR_REPO_NAME: application-name

jobs:
  delivery:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Checkout the repository
        uses: actions/checkout@v1
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      - name: Build a docker image, and push to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          docker build -t $ECR_REGISTRY/$AWS_ECR_REPO_NAME:${{ github.sha }} .
          docker push $ECR_REGISTRY/$AWS_ECR_REPO_NAME:${{ github.sha }}
      - name: Prepare for Beanstalk deployment
        env:
          DOCKER_IMAGE_URL: ${{ steps.login-ecr.outputs.registry }}/${{ env.AWS_ECR_REPO_NAME }}:${{ github.sha }}
        run: |
          cd elastic_beanstalk &&
            ./build_dockerfile.sh &&
            zip -r ./app-${{ github.sha }}.zip ./
      - name: Deploy web to EB
        uses: einaregilsson/beanstalk-deploy@v14
        with:
          aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          application_name: beanstalk-application
          environment_name: app-env
          wait_for_environment_recovery: 90
          version_label: app-${{ github.sha }}
          region: ${{ env.AWS_REGION }}
          deployment_package: elastic_beanstalk/app-${{ github.sha }}.zip

Prepare for Beanstalk deploymentのステップでは、以下のようにDockerfileのimageのURLを書き込んだりしています。

FROM {{DOCKER_IMAGE_URL}}

ENV RAILS_ENV=production

EXPOSE 3000

ENTRYPOINT [""]
CMD ["/usr/bin/docker_init.sh"]

インフラ構築

ドメイン

こちらで無料で取得できました。

www.freenom.com

Elastic Beanstalk周りでハマったところ

Nginxの設定

AWSの公式ドキュメントや、その他の記事を見ていると .ebextentions配下に設定ファイルを置けば読み込まれると言った情報が見られましたが、以下のように .platform配下に置く必要がありました。 .ebextentionsでも実行自体はされるみたいでしたが、Elastic Beanstalkのライフサイクルの都合で、更に上書きされてしまうようでした。

10MB以上のファイルをアップロードすると、413エラーが返ってくる状態だったため、 client_max_body_size を設定しました。

$ tree -a elastic_beanstalk
elastic_beanstalk
├── .platform
│   └── nginx
│       └── conf.d
│           └── client-max-body-size.conf
└── Dockerfile

$ cat elastic_beanstalk/.platform/nginx/conf.d/client-max-body-size.conf
client_max_body_size 30m;

他にも、デプロイ時に実行するフックなどを設定できます。

docs.aws.amazon.com

Sidekiq

今回の用途では、そこまで高負荷でもなく、Sidekiq用にコンテナの数を増やすほどでもなかったので、少し力技で同一のコンテナ上で動かしました。

docker_init.sh

#!/bin/sh
rm -f tmp/pids/server.pid && \
  rails db:migrate && \
    rails assets:precompile && \
    bundle exec sidekiq & \
    rails server -b 0.0.0.0

「マイクロサービスパターン 実践的システムデザインのためのコード解説」を読んだ

目的、モチベーション

マイクロサービスのデザインパターンを知りたかった。

全体の感想

サーガのパターンを扱っていたのが特に参考になった。全体的には、具体的な仮想のプロジェクトをもとにコードの事例も記載されており、抽象的にも具体的にも説明されていて、バランスが良かった。 マイクロサービスアーキテクチャと重複している箇所も多かったので、読んだことのある人は数章だけ読むので十分だと感じた。

目次

概要

Chapter 1 モノリシック地獄からの脱出

1.4. マイクロサービスアーキテクチャで状況打開

1.4.1. スケールキューブとマイクロサービス

マイクロサービスはArt of Scalability, The: Scalable Web Architecture, Processes, and Organizations for the Modern Enterprise (Pear04)で紹介されているスケールの3次元モデルの軸の一つと言える。

Chapter 2 サービスへの分割

DDDとかに基づいてドメイン分割

Chapter 3 マイクロサービスアーキテクチャで使われるプロセス間通信

3.2 同期的なリモートプロシージャ呼び出しパターンを使った通信

3.3 非同期的メッセージングパターンを使った通信

3.3.4 メッセージブローカー

ブローカーレスメッセージング

  • メリット
    • 直接やり取りするので、ネットワークトラフィックが軽くなり、レイテンシが下がる
    • パフォーマンスのボトルネックや単一障害点になるリスクを抱えたメッセージブローカーがない
    • 運用の複雑度が下がる
  • デメリット
    • サービスディスカバリーが必要
    • センダーとレシーバの両方が利用可能でないといけないので、可用性が下がる
    • 配信保証などのメカニズムの実装が難しくなる

ブローカーベースメッセージング

技術選定の際には、以下のようなことを考慮しなければならない。

  • サポートされているプログラミング言語
  • サポートされているメッセージング標準
  • メッセージの順序
  • 永続記憶
  • 持続性
  • スケーラビリティ
  • レイテンシ
  • 競合するコンシューマ

  • メリット

    • 疎結合
    • メッセージのバッファリング
    • 柔軟性の高い通信
    • 明示的なIPC
  • デメリット
    • パフォーマンスのボトルネックになる危険性
    • 単一障害点になる危険性
    • 運用の複雑度の上昇
3.3.7 トランザクショナルメッセージング

DBの書き込みのトランザクション内で、メッセージをパブリッシュしたいケースがある。パフォーマンスと信頼性を担保するためのパターンがいくつかある。

Transactional outbox

DBにOUTBOXというテーブルを追加して、書き込みのトランザクション内でOUTBOXテーブルにメッセージを挿入する。メッセージリレーがOUTBOXテーブルを読み込み、メッセージブローカーにメッセージをパブリッシュする。
このようにすることで、トランザクションをローカルに留め、アトミック性を保証することができる。

microservices.io

Polling publisher
メッセージリレーが、OUTBOXテーブルをポーリングしてパブリッシュするパターン。

microservices.io

Transactional log tailing
Transaction Log MinerがOUTBOXテーブルのトランザクションログを読み込み、メッセージに変換してパブリッシュする。トランザクションをサポートしていないNoSQLでも使用できる。
以下のようなプロジェクトで採用されている。

microservices.io

Chapter 4 サーガによるトランザクションの管理

4.2 サーガのコーディネート

4.2.1 コレオグラフィベースのサーガ

中央で管理するサービスがなく、それぞれのサービスがメッセージをpublishやconsumeする。問題は大きく2点ある。

  1. DBのトランザクションとイベントのpublishをアトミックに行わなければならない
  2. 相関するデータに対応するために、関連するIDが必要になる

欠点

  1. サーガの定義が分散されているので、わかりにくい
  2. サービスが循環依存してしまうことがある
  3. 関連するイベントをサブスクライブしないといけないので、密結合になる
4.2.2 オーケストレーションベースのサーガ

ビジネスロジックを管理するサービスを中心にして、メッセージのやりとりを行う。参加している他のサービスは共通の1つのチャネルで返信を行うことで、関心を減らすことができる。ビジネスロジックを集約できるが、難しくなりすぎないように複数のサービスにまたがるステートを管理する部分のみに集中させることが重要。

4.3 分離性の欠如への対処方法

サーガを使うと、ACIDのうち分離性が失われてしまう。そのため、更新の消失や、ダーティリード、反復不能読み取りなどが発生してしまう。

4.3.2 分離性の欠如に対処するためのカウンターメジャー
  • Semantic lock: アプリケーションレベルのロック。stateに *_PENDINGのようなものを追加して、その場合にアプリケーション側で適切に処理を行う。
  • Commutative updates: どのように順序でも実行できるように更新処理を設計すること。例えば、決済を単純にキャンセルするのではなく、補償となる逆の処理を行う。
  • Pessimistic view: ビジネスリスクが最小になるようにサーガの順序を並べ直すこと。例えば、ダーティリードが起こりそうなサービスの処理を最後に行う。
  • Reread value: データを読み直して変更されていないことを確認してから上書きするようにしてダーティライトを防ぐこと
  • Version file: レコードに対する更新を記録し、順序を変えられるようにすること
  • By value: 個々のリクエストのビジネスリスクによりダイナミックに変更処理メカニズムを選択すること。例えば、決済額が大きいときは厳密な分散トランザクションを利用して、少額の際は以上で紹介したカウンターメジャーを使うなど。

6つぐらい紹介されてるので、まとめる

Chapter 5 マイクロサービスアーキテクチャにおけるビジネスロジックの設計

スナップショットを作って、効率化する versionを条件で絞って、楽観的なロックを実装 イベント形式は削除の操作が大変

Chapter 7 マイクロサービスアーキテクチャでのクエリーの実装

複数のサービスにまたがったデータを一度に取得するには大きく2種類方法がある。 一つはAPI compositionパターンで、BFFなどで複数のサービスを呼び出してアプリケーションで結合して返す方法。サービス間でデータの独立性が保てなかったり、可用性が下がるなどのデメリットはあるが、シンプルなものはこれで概ね対応できる。
もう一つはCQRS(Command Query Responsibility Segregation)パターンで、この方法では更新CUDの操作と読み込みRの操作を別々のサービスで行う。Commandのサービスがイベントをpublishして、それをQueryのサービスがsubscribeして独自のDBに保存する。Queryに最適なDBを選択できたり、関心を分離でき、複雑なQueryも扱えるようになる。一方で、同時更新やイベントを冪等に処理したり、レプリケーションの遅延を考慮した上で実装を行う必要がある。

Chapter 8 外部APIパターン

8.2 APIゲートウェイの実装

8.2.1 API Gateway パターンの概要

API Gatewayは以下のような役割を担う。

  • リクエストのルーティング
  • API合成: 複数のサービスのリクエストの結果を処理する
  • エッジ機能: 認証、認可、流量制限、キャッシュ、メトリクスの収集、ログ
  • プロトコル変換: 外部との通信に最適なプロトコルと、内部通信に最適なプロトコルを変換する

Chapter 11 本番環境に耐えられるサービスの開発

  • 認証・認可
  • 環境変数などによるconfigの設定
  • Observability
    • Health check
    • Log集約
    • 分散トレーシング
    • アプリケーションのメトリクス
    • Exceptionトラッキング
    • Audit logging
  • サービスメッシュ

次のアクション

「リレーショナルデータベース入門」を読んだ

全体の感想

DBの本格的な入門書としては、今まで読んだ中では一番良かったように感じた。ただ個人的には、出会うタイミングには恵まれなかった。
最初の方は集合論から始まり、データを効率よく操作したり、表現するにはどのようなものでなければならないかについて言及されていた。中盤からはRDBの種類に関係なく一般的なDBMSがどのような仕組みなのか、クエリーを投げてからデータの読み込み、書き込みが行われるまでにどのような処理が行われているのかについて説明されていた。クエリの処理に必要なコストの計算方法なども紹介されており、joinやindexが内部でどのように処理されるのかも説明されていた。
クエリーを投げてから、内部でどのように処理が行われているのか理解できていない人にはオススメ。

目次

概要

第2章 リレーショナルデータモデル

2.1 本章のはじめに

データモデルは次の3要素から成り立っている。

  • 構造記述
  • 意味記述
  • 操作記述

2.9 空

「Interim Report ANSI/X3/SPARC Study Group on Data Base Management Systems」によると14種類の意味がある。

  1. Not valid for this individual
  2. Valid, but does not yet exist for this individual
  3. Exists, but not permitted to be logically stored
  4. Exists, but not knowable for this individual
  5. Exists, but not yet logically stored for this individual
  6. Logically stored, but subsequently logically deleted
  7. Logically stored, but not yet available
  8. Available, but understanding change (may be no longer valid)
  9. Available, but of suspect validity (unreliable)
  10. Available, but invalid
  11. Secured for this class of conceptual data
  12. Secured for this individual object
  13. Secured at this time
  14. Derived from null conceptual data (any of the above)

第3章 リレーショナルデータベースのデータ操作言語

3.3 リレーショナル代数

  • 集合演算
    • 和集合演算
    • 差集合演算
    • 共通集合演算
    • 直積演算
  • リレーショナル代数に特有の演算
    • 射影演算
      • 属性を選択できる
    • 選択演算
      • 比較可能な任意の2つの属性の条件から、行を選択できる
      • 定数との比較は、定数の属性を追加して、それと比較することで実現される
    • 結合演算
      • ざっくり言うと、joinのこと
    • 商演算
      • 例えば、供給元と部品のリレーションから、ある部品の集合を供給する供給元のリレーションを生成するなど

3.5 リレーショナル代数演算の拡張

選択演算を満たすために、比較可能である必要がある。そこで、nullを導入するためには3値論理を導入する必要がある。

3.7 リレーショナル代数とリレーショナル論理の等価性

リレーショナル代数とリレーショナル論理は等価であると知られている。

いずれかを満たす時、リレーショナル完備という。

第6章 データベース管理システムの標準アーキテクチャと機能

6.3 ANSI/X3/SPARCのDBMSの3層スキーマ構造

  • 概念スキーマ: 記号で表現される
  • 内部スキーマ: DB内部の実装方法(ISAMファイル、B+木など)
  • 外部スキーマ: ビューなど、ユーザの様々な要求に応えるために、DB空間を個別に生成する機能

6.4 3層スキーマの意義

物理的データ独立性と論理的データ独立性を担保できる。

6.5 DBMSの3大機能

6.6 リレーショナルDBMSのメタデータ管理

メタデータ自身もテーブルのスキーマとして定義して管理されている。

dev.mysql.com

第8章 ファイル編成とアクセス法

8.4 ファイル編成とアクセス法

8.4.1 ファイル編成
  • ヒープ編成
    • 2次記憶に書き込まれた時刻順にレコードを順番付ける
    • 大量の挿入を高速に行える
    • 線形探索なので、レコードが多いと時間がかかる。また、虫食いブロックが多発して使用効率が悪くなってしまうので、記憶管理が必要になる
  • 順次編成
    • キーの順番で連続するように保存する
    • 高速にアクセスできる
    • レコードの削除、更新などの度にソートし直さなければならない
  • 直接編成
    • hash編成ともいい、キーをハッシュ関数に施して位置を決定する
    • 高速にアクセスできる
    • rangeクエリーが非効率
8.4.2 アクセス法
  • 順次アクセス法
  • インデックス法
  • ハッシュ法

8.6 インデックス法

8.6.1 インデックスとは何か

順序フィールド上にインデックスを張る場合、以下の2種類が定義できる。

  • 密集インデックス: すべての順序フィールド値が出現する
  • 点在インデックス: 一部のみ。アクセスする際は、近い値にアクセスしてから走査してたどり着く。データ量は小さくてすむが、パフォーマンスは良くない。
8.6.2 ISAM

インデックス付順次アクセス法(Indexed Sequential Access Method)

8.7 B+木

多段インデックスで、葉ノードは次のキーのポインタも保持しているので、range queryにも対応できる。平衡を保とうとするので、挿入、削除のコストは低くない。

第9章 リレーショナルDBMSの質問処理とその最適化

9.3 質問処理とそのコスト

9.3.1 コスト式

C = Ci/o + ω x Ccpu

  • C: 質問を処理するコスト
  • Ci/o: 質問を処理するためにフェッチしたページの総数。さらにインデックスとデータに分解できる。
  • Ccpu: 質問を処理するために費やしたCPU使用率
  • ω: CPU使用率をページ数に換算するための重み係数
9.3.3 結合質問処理とそのコスト式
  1. 入れ子型ループ結合法とコスト式
# R: アウタ表, S: インナ表
for each t in R
    for each t` in S
  such that t[B] t`[B]
  compute t * t`
end
  1. ソートマージ結合法とコスト式
    • 結合列で2つのテーブルのレコードをソートして、そこから順次に先頭から一致するものを結合させる。
    • 一般には、入れ子型ループ結合法よりも高速に処理が行える。
      • ソートした結果、インナ表Sが順次編成ファイルとなるので、高速に読み出せるから
  2. ハッシュ結合とコストの計算式
    • 小さい方のテーブルをハッシュに変換して、そのハッシュに大きい方のテーブルで順次に参照する

第11章 トランザクションの同時実行制御

11.5 多版同時実行制御(MVCC)

ある値を複数のバージョンを保持して、トランザクションに応じたtimestampのバージョンを返す

11.6 SQLの隔離性水準

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

第12章 分散型データベース管理システム

12.4 分散型質問処理

12.4.3 準結合演算

異なるサイトにあるテーブルを結合するとき、結合に必要な属性だけ先に渡し、最終的に必要な行の他の属性を再度取得する。こうすることで、データの転送量を減らすことができる。

「JavaScript Primer」を読んだ

jsprimer.net

目的、モチベーション

最近はバックエンドしか触ってなくて、久しぶりにフロントエンドのキャッチアップしておきたかったから。

全体の感想

最近のJavaScriptを知らない人や、雰囲気で触っている人には良いと思った。
本がGitHubで管理されていて差分も把握しやすいため、新しいバージョンが公開されたときにも部分的にキャッチアップしやすくて良いと感じた。
これが無料で読めるのは本当にありがたいなと思った。

目次

メモ

第一部: 基本文法

変数と宣言

varの使い方はletとほとんど同じで、varキーワードには同じ名前の変数を再定義できてしまう問題があります。

データ型とリテラル

BigInt

数値リテラルは倍精度浮動小数(64bit)で数値を扱うのに対して、BigIntでは任意の精度の整数を扱えます

console.log(1n); // => 1n
// 2^53-1より大きな値も扱える
console.log(9007199254740992n); // => 9007199254740992n

// BigIntは整数を扱うデータ型であるため、次のように小数点を含めた場合は構文エラーとなります。
1.2n; // => SyntaxError
文字列(String)

"(ダブルクォート)と ' (シングルクォート)はまったく同じ意味

テンプレートリテラル

`(バッククォート)で囲んだ範囲を文字列とするリテラルです。 テンプレートリテラルでは、複数行の文字列を改行記号のエスケープシーケンス(\n)を使わずにそのまま書くことができます。

`複数行の
文字列を
入れたい`; // => "複数行の\n文字列を\n入れたい"
[コラム] undefinedはリテラルではない

ただのグローバル変数

演算子

比較演算子

厳密等価演算子(===)

// 同じ型で同じ値である場合に、trueを返す
console.log(1 === 1); // => true
console.log(1 === "1"); // => false

// オブジェクトの場合は、同じ参照のときtrueを返す
const objA = {};
const objB = {};
console.log(objA === objB); // => false
console.log(objA === objA); // => true

等価演算子(==)

console.log(1 == 1); // => true

// オブジェクトは参照が一致しているならtrueを返す
const objA = {};
const objB = {};
console.log(objA == objB); // => false
console.log(objA == objA); // => true

// オペランド同士が異なる型の値であった場合に、 同じ型となるように暗黙的な型変換をしてから比較する
// 文字列を数値に変換してから比較
console.log(1 == "1"); // => true
// "01"を数値にすると`1`となる
console.log(1 == "01"); // => true
// 真偽値を数値に変換してから比較
console.log(0 == false); // => true
// nullの比較はfalseを返す
console.log(0 == null); // => false
// nullとundefinedの比較は常にtrueを返す
console.log(null == undefined); // => true

// null または undefined以外は厳密等価演算子を使うべき
const value = undefined; /* または null */
// === では2つの値と比較しないといけない
if (value === null || value === undefined) {
    console.log("valueがnullまたはundefinedである場合の処理");
}
// == では null と比較するだけでよい
if (value == null) {
    console.log("valueがnullまたはundefinedである場合の処理");
}
論理演算子

falsyな値とは次の7種類の値のこと

  • false
  • undefined
  • null
  • 0
  • 0n
  • NaN
  • ""(空文字列)

nulishとは、評価結果がnullまたはundefinedとなる値のこと

暗黙的な型変換

さまざまな暗黙的な型変換
1 + "2"; // => "12"
1 - "2"; // => -1

関数と宣言

可変長引数
// [ES2015] Rest parameters
function fn(...args) {
    // argsは引数の値が順番に入った配列
    console.log(args); // => ["a", "b", "c"]
}
fn("a", "b", "c");

// Spread構文
function fn(x, y, z) {
    console.log(x); // => 1 
    console.log(y); // => 2 
    console.log(z); // => 3 
}
const array = [1, 2, 3];
fn(...array);

// arguments
function fn() {
    // `arguments`はインデックスを指定して各要素にアクセスできる
    // Array ライクなだけで、Arrayのメソッドは使えない
    console.log(arguments[0]); // => "a" 
    console.log(arguments[1]); // => "b" 
    console.log(arguments[2]); // => "c" 
}
fn("a", "b", "c");
[ES2015] 関数の引数と分割代入
// 第1引数のオブジェクトから`id`プロパティを変数`id`として定義する
function printUserId({ id }) {
    console.log(id); // => 42
}
const user = {
    id: 42
};
printUserId(user);
関数式

[ES2015] Arrow Function

// 仮引数の数と定義
const fnA = () => { /* 仮引数がないとき */ };
const fnB = (x) => { /* 仮引数が1つのみのとき */ };
const fnC = x => { /* 仮引数が1つのみのときは()を省略可能 */ };
const fnD = (x, y) => { /* 仮引数が複数のとき */ };
// 値の返し方
// 次の2つの定義は同じ意味となる
const mulA = x => { return x * x; }; // ブロックの中でreturn
const mulB = x => x * x;            // 1行のみの場合はreturnとブロックを省略できる

次のような特徴がある。

  • 名前をつけることができない(常に匿名関数)
  • thisが静的に決定できる(詳細は「関数とスコープ」の章で解説します)
  • functionキーワードに比べて短く書くことができる
  • newできない(コンストラクタ関数ではない)
  • arguments変数を参照できない

条件分岐

switch文

breakが必要で、厳密等価演算子で評価される。

ループと反復処理

for...in文

事故りやすいので、基本的には使わないこと。

オブジェクト

[ES2015] オブジェクトと分割代入
const languages = {
    ja: "日本語",
    en: "英語"
};
const { ja, en } = languages;
console.log(ja); // => "日本語"
console.log(en); // => "英語"
プロパティの削除

deleteを使う。

const obj = {
    key1: "value1",
    key2: "value2"
};
delete obj.key1;
// key1プロパティが削除されている
console.log(obj); // => { "key2": "value2" }
[コラム] constで定義したオブジェクトは変更可能

Objectを変更不可能にするには、 Object.freeze を使う。

"use strict";
const object = Object.freeze({ key: "value" });
object.key = "value"; // => TypeError: "key" is read-only
プロパティの存在を確認する

定義されていない場合は、 undefinedが返るので以下の様に確認する。

const obj = { key: undefined };
if ("key" in obj) {
    console.log("`key`プロパティは存在する");
}
if (obj.hasOwnProperty("key")) {
    console.log("`obj`は`key`プロパティを持っている");
}
[ES2020] Optional chaining演算子(?.)
const title = widget?.window?.title ?? "未定義";
console.log(`ウィジェットのタイトルは${title}です`);
オブジェクトの静的メソッド
const obj = {
    "one": 1,
    "two": 2,
    "three": 3
};
console.log(Object.keys(obj)); // => ["one", "two", "three"]
console.log(Object.values(obj)); // => [1, 2, 3]
console.log(Object.entries(obj)); // => [["one", 1], ["two", 2], ["three", 3]]

複製するときは、 Object.assign({}, obj)で実現できるが、shallowCopyなので注意。

プロトタイプオブジェクト

in演算子とObject#hasOwnPropertyメソッドの違い
const obj = {};
// `obj`というオブジェクト自体に`toString`メソッドが定義されているわけではない
console.log(obj.hasOwnProperty("toString")); // => false
// `in`演算子は指定されたプロパティ名が見つかるまで親をたどるため、`Object.prototype`まで見にいく
console.log("toString" in obj); // => true
オブジェクトの継承元を明示するObject.createメソッド
// const obj = {} と同じ意味
const obj = Object.create(Object.prototype);
// `obj`は`Object.prototype`を継承している
console.log(obj.hasOwnProperty === Object.prototype.hasOwnProperty); // => true
// prototypeを継承しないオブジェクト
const nullObj = Object.create(null);

配列

オブジェクトが配列かどうかを判定する

Array.isArrayを使う。

const obj = {};
const array = [];
console.log(Array.isArray(obj)); // => false
console.log(Array.isArray(array)); // => true
console.log(typeof array); // => "object"
[コラム] undefinedの要素と未定義の要素の違い
const denseArray = [1, undefined, 3];
const sparseArray = [1, , 3];
console.log(denseArray[1]); // => undefined
console.log(sparseArray[1]); // => undefined
console.log(denseArray.hasOwnProperty(1)); // => true
console.log(sparseArray.hasOwnProperty(1)); // => false
配列から要素を削除
const array = ["a", "b", "c"];
// 1番目から1つの要素("b")を削除
array.splice(1, 1);
console.log(array); // => ["a", "c"]
const array = [1, 2, 3];
array.length = 0; // 配列を空にする
console.log(array); // => []
[コラム] Array-likeオブジェクト

Array.fromメソッドでArrayに変換できる。

文字列

正規表現オブジェクト

関数とthis

実行コンテキストとthis
<script>
// 実行コンテキストは"Script"
console.log(this); // => window
</script>
<script type="module">
// 実行コンテキストは"Module"
console.log(this); // => undefined
</script>
// ブラウザでは`window`オブジェクト、Node.jsでは`global`オブジェクトを参照する
console.log(globalThis);

非同期処理:コールバック/Promise/Async Function

非同期処理はメインスレッドで実行される

並列処理ではなく、並行処理。

[ES2015] Promise
  • Promise.allで複数のPromiseをまとめることができる
  • Promise.raceでもまとめることができるが、最初の結果だけが反映される。

[ES2015] Map/Set

WeakMap
const map = new WeakMap();
// キーとなるオブジェクト
let obj = {};
// {} への参照をキーに値をセットする
map.set(obj, "value");
// {} への参照を破棄する
obj = null;
// GCが発生するタイミングでWeakMapから値が破棄される
[コラム] キーの等価性とNaN
const map = new Map();
map.set(NaN, "value");
// NaNは===で比較した場合は常にfalse
console.log(NaN === NaN); // => false
// MapはNaN同士を比較できる
console.log(map.has(NaN)); // => true
console.log(map.get(NaN)); // => "value"

JSON

オブジェクトをJSON文字列に変換する
const obj = { id: 1, name: "js-primer", bio: null };
const replacer1 = (key, value) => {
    if (value === null) {
        return undefined;
    }
    return value;
};
console.log(JSON.stringify(obj, replacer1)); // => '{"id":1,"name":"js-primer"}'

const replacer2 = ["id"];
console.log(JSON.stringify(obj, replacer2)); // => '{"id":1}'

// スペース2個でインデントされたJSON
console.log(JSON.stringify(obj, null, 2)); 
/*
{
  "id": 1,
  "name": "js-primer",
  "bio": null
}
*/
JSONシリアライズできないオブジェクト
シリアライズ前の値 シリアライズ後の値
文字列・数値・真偽値 対応する値
null null
配列 配列
オブジェクト オブジェクト
関数 変換されない(配列のときはnull)
undefined 変換されない(配列のときはnull)
Symbol 変換されない(配列のときはnull)
RegExp {}
Map, Set {}
toJSONメソッドを使ったシリアライズ

オブジェクトがtoJSONメソッドを持っている場合、JSON.stringifyメソッドはtoJSONメソッドの返り値を使う。

const obj = {
    foo: "foo",
    toJSON() {
        return "bar";
    }
};
console.log(JSON.stringify(obj)); // => '"bar"'

「実践 Rustプログラミング入門」を読んだ

全体の感想

他の言語を普段使っていてRustに触れたことがない人にとっては、良い本だと思った。
3部からなり、第1部では基礎的な文法から、Rust特有の考え方(GCの所有権、メモリ安全性、スレッド安全性など)の説明があった。第2部では、実践的なアプリケーションを実装して、コマンドラインツールやWebアプリケーション、GUI、組み込みなど幅広く扱われていた。第3部では、応用的なtipsが紹介されていて、他の言語との連携や、コミュニティ、リリースサイクルなどについて言及されていた。

Rust言語自体は、パフォーマンスがよく、型推論も強くサポートされ、モダンな言語にある機能はほとんど網羅されており、コミュニティも活発で幅広いアプリケーションやツールも作れそうで、いろんな企業が導入し始めてるのも納得できた。特にスレッド安全性などについては、言語の通りに実装すれば未然にバグも防げそうに感じたので、強力だなと思った。
一部パフォーマンスが求められる部分だけRustで書くことも結構簡単にできそうなので、部分的に導入するのも良さそうだと思った。

目次

概要

Part 1 入門

Chapter1 プログラミング言語 Rust

1-2 とにかく実行速度が速い

  • Rust は機械語に直接コンパイルされる
  • ガベージコレクションをもたない
    • 「所有権」「借用」「ライフタイム」という新しい仕組みで、言語側で管理する
  • 「ゼロコスト抽象化」を追求している
    • 抽象化するときにオーバーヘッドとかが生じないようになっている。

1-4 OS から Web アプリケーションまで幅広く実装できる

Chapter3 Rustの基本

3-1 基本的な文法

  • NaNは ==を満たさないので、f32では PartialEqPartialOrdを使う

3-2 Rustを支える言語機能

所有権と借用
  • メモリ管理のために、所有できるのは1つのオブジェクトだけ
  • 参照は複数から行えるので、参照渡しをする
    • 可変の場合は、一度に一つだけ
    • 不変の場合は、制限なし
  • デストラクタとして Dropトレイトが用意されている
スレッド安全性

マルチスレッドの例

use std::thread;

fn main() {
    let mut handlers = Vec::new();
    for x in 0..10 {
        // moveキーワードで所有権をスレッドに移す
        handlers.push(thread::spawn(move || {
            println!("Hello, world!: {}", x);
        }));
    }

    for handle in handles {
        // スレッドの処理が終了するのを待つ
        let _ = handle.join();
    }
}

共有メモリ

use std::rc::{Arc, Mutex};
use std::thread;

fn main() {
  let mut handlers = Vec::new();
  let data = Arc::new(Mutex::new(vec![1; 10]));
  for x in 0..10 {
    // リファレンスカウンターを増やす
    let data_ref = data.clone();
    handlers.push(thread::spawn(move || {
      // 可変参照を得る
      let mut data = data_ref.lock().unwrap();
      data_ref[x] += 1;
    }));
  }

  for handle in handles {
    // スレッドの処理が終了するのを待つ
    let _ = handle.join();
  }

  dbg!(data);
}

メッセージパッシング

use std::sync::mpsc;
use std::thread;

fn main() {
  let mut handles = Vec::new();
  let mut data = vec![1; 10];
  let mut snd_channels = Vec::new();
  let mut rcv_channels = Vec::new();

  for _ in 0..10 {
    let (snd_tx, snd_rx) = mpsc::channel();
    let (rcv_tx, rcv_rx) = mpsc::channel();
    snd_channels.push(snd_tx);
    rcv_channels.push(rcv_tx);

    handlers.push(thread::spawn(move || {
      let mut data = snd_rx.recv().unwrap();
      data += 1;
      let _ = rcv_tx.send(data);
    }));
  }

  for x in 0..10 {
    let _ = snd_channels[x].send(data[x]);
  }

  for x in 0..10 {
    data[x] = rcv_channels[x].recv().unwrap();
  }

  for handle in handlers {
    let _ = handle.join();
  }
  dbg!(data)
}

Part 2 実践

ソースコードはこちら。

github.com

Chapter4 プログラムを作成する

逆ポーランド記法コマンドラインツールを作成した。

Chapter5 Webアプリケーションの開発

The Rust Programming Language

Chapter6 WebAssembly

6-4 サンプルプログラム:ナンバープレースを解く

数独を解く。

JSと比較したベンチマークの結果では、JITコンパイラの最適化の影響のためか、必ずしもRustの方が速いとは限らないみたいだった。

Untitled

Chapter7 GUIアプリケーション

IcedというGUIフレームワークのようなライブラリを使って、タイマーのアプリケーションを作成した。

7-1 RustにおけるGUIの現状

GUIクレートの紹介

Chapter8 組み込みシステム

エミュレータを使って、LEDをチカチカさせるものの実装の仕方が紹介されていた。

Chapter9 開発ツール

9-2 フォーマッタ・リンター

9-3 コードカバレッジ

9-4 ベンチマーク・プロファイラ

Chapter10 プロダクトをリリースする

10-2 ビルドの再現性

  • Cargo.lock: 依存クレートのバージョン
  • rust-toolchain: コンパイラのバージョン

10-3 バイナリサイズの最適化

最適化のオプション
[prifile.release]
// Link Time Optimization
lto = true

// 0: 最適化なし
// 1: 基本的な最適化
// 2: 追加の最適化
// 3: すべての最適化(リリースビルドでのデフォルト)
// "s": バイナリサイズの最適化
// "z": バイナリサイズの最適化(ループのベクトル化を行わない)
opt-level = "z"

// コンパイル時の並列度を下げることで、タスクをまたいだ最適化を行える
codegen-units = 1

// panic時、スタックを巻き戻してバックトレースを生成する必要がないとき
panic = "abort"

// シンボル情報の削除
$ strip ./target/release/hello

10-6 ファジング

cargo-fuzzを使える。

Part 3 Tips

Chapter11 いろいろなRustの発展的Tips

11-2 FFIによる他言語との連携

FFI(Foreign Function Interface)によって、他言語と簡単に連携できる。

11-4 unsafe

Rustはメモリ安全性を保証しているが、実装者の責任によってその安全性を無視して実装することができる。

  • 極力使わないこと
  • 使う場合は、局所的に使うこと
UB (未定義動作)

アプリケーションがライブラリやコンパイラの規約を守らなかったために、どのような動作結果も保証されない状態。

参考資料

11-5 Rust のエディション

Rustでは通常のリリースは6週間ごとに行われている。

大きな変更に関しては、約3年毎にエディションという単位でリリースされる。エディションは互換性があり、クレートごとに指定できる。

11-6 Rust製のOSS

rust-unofficial/awesome-rust

特にすぐ使える有名なもの

11-7 Rust のコミュニティ

最新情報を得るために

次のアクション

もう少し、バックエンドで本番運用できそうか、試してみる。