masakazu-takewakaのブログ

たまに書きます。

「[24時間365日]サーバー/インフラを支える技術」を読んだ

読んだのでメモ。

1章 冗長化・負荷分散の基本

コールドスタンバイ -> 予備機を使わずに、現用機に障害が発生したら切り変えること(e.g. ルーター) ホットスタンバイ -> 予備機も稼働させておくことで現用機と同じ状態を保つこと(e.g. Webサーバー) フェイルオーバー -> 現用機に障害が発生した時に自動的に処理を予備機に引き継ぐこと

Webサーバーのフェイルオーバーは2段階。

  1. ヘルスチェック
  2. 仮想IPの引き継ぎ

1. ヘルスチェック

IP引き継ぎの機会(障害の発生)を検出する。

  • ICMP監視(レイヤ3)
    • ping
    • マシンに電源が入っていることをチェックするだけなので、webサーバーが落ちているかはわからない。
  • ポート監視(レイヤ4)
    • TCPで接続
    • Webサーバーが稼働しているかチェックするだけなので、サービスのエラーはわからない。
  • サービス監視(レイヤ7)
    • HTTPリクエス
    • サービスのエラーはわかるが、他と比べると負荷が高い。

2. 仮想IPの引き継ぎ

4章 性能向上、チューニング。Linux単一ホストの負荷の見極め

負荷 -> 複数のタスク(プロセス)がリソースを奪い合う結果生じる待ち時間

  • ボトルネック見極めの基本的な流れ

  • 多くの場合Webアプリケーションにおいては 負荷分散 = ディスクI/Oの軽減

    • キャッシュを効かせることが鍵
  • AppサーバーはCPUバウンドであることが多い

    • 基本的にDBサーバーから受け取ったデータを加工してクライアントサイドに送るだけだから
  • DBサーバーはI/Oバウンドであることが多い

    • 多くのデータをディスクからフェッチするから
  • topなどで出る3つのロードアベレージの数字はそれぞれ直近1分、5分、15分の間で待ち状態にあったタスクの単位時間あたりの数

ロードアベレージカーネルコードを見る

  • run queueに存在するTASK_RUNNINGとTASK_UNINTERRUPTIBLEの2つをカウントしている
long calc_load_fold_active(struct rq *this_rq, long adjust)
{
    long nr_active, delta = 0;


    nr_active = this_rq->nr_running - adjust;
    nr_active += (long)this_rq->nr_uninterruptible;


    if (nr_active != this_rq->calc_load_active) {
        delta = nr_active - this_rq->calc_load_active;
        this_rq->calc_load_active = nr_active;
    }


    return delta;
}

https://github.com/torvalds/linux/blob/b719ae070ee2371c37d846616ef7453ec6811990/kernel/sched/loadavg.c#L79-L92

CPU使用率とI/O待ち率

  • マルチCPU(シングルコアを仮想的に複数のcpuに見せることが可能)
    • cpu負荷は分散できるがI/O負荷は分散できない
      • e.g) 2cpuだとして、全体のI/O負荷20%、cpu1のI/O負荷0%、cpu2のI/O負荷40%がありえる
        • CPU使用率、I/O待ち率はCPUごとに見ていく必要がある

CPU使用時間などを記録する構造体。sarが表示する項目が全部入っている:

enum cpu_usage_stat {
    CPUTIME_USER,
    CPUTIME_NICE,
    CPUTIME_SYSTEM,
    CPUTIME_SOFTIRQ,
    CPUTIME_IRQ,
    CPUTIME_IDLE,
    CPUTIME_IOWAIT,
    CPUTIME_STEAL,
    CPUTIME_GUEST,
    CPUTIME_GUEST_NICE,
    NR_STATS,
};

https://github.com/torvalds/linux/blob/d60ddd244215da7c695cba858427094d8e366aa7/include/linux/kernel_stat.h#L20-L32

cpu使用率のカーネルコードを見る

タイマ割り込みの度にaccount_process_tickを呼ぶ

void update_process_times(int user_tick)
{
    struct task_struct *p = current;


    /* Note: this timer irq context must be accounted for as well. */
    account_process_tick(p, user_tick);
    run_local_timers();
    rcu_sched_clock_irq(user_tick);
...

https://github.com/torvalds/linux/blob/da05b5ea12c1e50b2988a63470d6b69434796f8b/kernel/time/timer.c#L1719

user_tickが正であればaccount_user_timeを、そうでなければaccount_system_timeもしくはaccount_idle_timeを呼ぶ

void account_process_tick(struct task_struct *p, int user_tick)
{
    u64 cputime, steal;

    if (vtime_accounting_enabled_this_cpu())
        return;

    if (sched_clock_irqtime) {
        irqtime_account_process_tick(p, user_tick, 1);
        return;
    }

    cputime = TICK_NSEC;
    steal = steal_account_process_time(ULONG_MAX);

    if (steal >= cputime)
        return;

    cputime -= steal;

    if (user_tick)
        account_user_time(p, cputime);
    else if ((p != this_rq()->idle) || (irq_count() != HARDIRQ_OFFSET))
        account_system_time(p, HARDIRQ_OFFSET, cputime);
    else
        account_idle_time(cputime);
}

https://github.com/torvalds/linux/blob/c677124e631d97130e4ff7db6e10acdfb7a82321/kernel/sched/cputime.c#L471

各関数はtask_struct構造体の対応するメンバに時間を足して記録する

void account_user_time(struct task_struct *p, u64 cputime)
{
    int index;


    /* Add user time to process. */
    p->utime += cputime; /* <- ここ
   account_group_user_time(p, cputime);


   index = (task_nice(p) > 0) ? CPUTIME_NICE : CPUTIME_USER;


   /* Add user time to cpustat. */
    task_group_account_field(p, index, cputime);


    /* Account for user time used */
    acct_account_cputime(p);
}

https://github.com/torvalds/linux/blob/c677124e631d97130e4ff7db6e10acdfb7a82321/kernel/sched/cputime.c#L117

void account_system_time(struct task_struct *p, int hardirq_offset, u64 cputime)
{
    int index;


    if ((p->flags & PF_VCPU) && (irq_count() - hardirq_offset == 0)) {
        account_guest_time(p, cputime);
        return;
    }


    if (hardirq_count() - hardirq_offset)
        index = CPUTIME_IRQ;
    else if (in_serving_softirq())
        index = CPUTIME_SOFTIRQ;
    else
        index = CPUTIME_SYSTEM;


    account_system_index_time(p, cputime, index);
}

void account_system_index_time(struct task_struct *p,
                   u64 cputime, enum cpu_usage_stat index)
{
    /* Add system time to process. */
    p->stime += cputime; /* <- ここ
   account_group_system_time(p, cputime);


   /* Add system time to cpustat. */
    task_group_account_field(p, index, cputime);


    /* Account for system time used */
    acct_account_cputime(p);
}

https://github.com/torvalds/linux/blob/c677124e631d97130e4ff7db6e10acdfb7a82321/kernel/sched/cputime.c#L184

void account_idle_time(u64 cputime)
{
    u64 *cpustat = kcpustat_this_cpu->cpustat;
    struct rq *rq = this_rq();


    if (atomic_read(&rq->nr_iowait) > 0)
        cpustat[CPUTIME_IOWAIT] += cputime;
    else
        cpustat[CPUTIME_IDLE] += cputime;
}

https://github.com/torvalds/linux/blob/c677124e631d97130e4ff7db6e10acdfb7a82321/kernel/sched/cputime.c#L218

psとスレッド

  • 別名LWP(Light Weight Process)
  • カーネル内では"タスク"としてプロセスと同じ扱いを受ける
  • ps -Lで見れる
  • ps aux -L | wc -l と数字が一致するので、sar -qのplist-sz(システムに存在するプロセス数)にはスレッドの数も含まれている雰囲気

ps, sar, vmstatの使い方

  • ps
    • TIMEはCPU使用時間のことであり、プロセスが起動してからの時間ではない
  • sar
    • sadcというバックグラウンドで動く、カーネルからレポートを収集して保存するプログラムがある
    • 時間毎のOSの状態が見れるので、プログラムなどを入れ替えた直後に使ったりする
    • /var/log/saに過去の情報がある
  • vmstat
    • 仮想メモリ周りの情報
    • 発生したI/Oの絶対値が見れる
      • bi ... ブロックデバイスから受け取ったブロック(blocks/s)
      • bo ... ブロックデバイスに送られたブロック(blocks/s)

その他

  • Linuxは可能な限り空いているメモリをキャッシュに回そうとする
    • DBサーバー再起動->flushでキャッシュが空になる->データ全体に読み込みをかけることでキャッシュ構築->本番環境に戻す

「まんがでわかるLinux シス管系女子3」を読んだ

読んだので軽くメモ。

https://www.amazon.co.jp/dp/B07CM2YNVD/ref=cm_sw_r_tw_dp_U_x_8b9nEb3ZHTFF5

特定のポートでのみ通信を許可する

ファイアウォール・・・許可された通信以外を遮断する仕組み。今回のこれはパケットフィルター。

Ubuntuにはufwというツールがありこんな風に使う。

$ sudo ufw default deny # まず全て遮断する
$ sudo ufw allow 22        # 通信を許可したいポートだけ個別で許可
$ sudo ufw allow 80
$ sudo ufw enable           # 有効果
$ sudo ufw status            # チェック

踏み台の向こうのサーバーに直接ファイルをコピーする

ポートフォワーディングすれば手元のマシンのポートと踏み台の向こうのサーバーのポートの間で直接通信が行われる。(漫画はコピーしたいファイルが大きすぎて踏み台のディスク容量が不足してたところをポートフォワーディングで直接通信させて解決、という流れ)

# ポートフォワーディングする(手元 - コピー先)
$ ssh -L take@踏み台ip 50022(適当):コピー先ip:22
# 手元のマシンから直接コピー(トンネルの入り口があるlocalhostをコピー先に指定)
$ scp -p 50022 fuga/hoge.txt take@localhost:/tmp

誰がそのポート番号を使っているのか知りたい

lsof... ofはOpen Files。プロセスが開いているファイルや、ファイルを開いているプロセスを調べるのに使われる。

$ lsof -i :3000

COMMAND   PID             USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ruby    16873 masakazutakewaka   12u  IPv4 0x150e6b95224251b3      0t0  TCP localhost:hbci (LISTEN)
ruby    16873 masakazutakewaka   13u  IPv6 0x150e6b951d1cfa73      0t0  TCP localhost:hbci (LISTEN)

逆にプロセスが使っているポート番号を調べる。

$ lsof -p 16873 | grep LISTEN

ruby    16873 masakazutakewaka   12u  IPv4 0x150e6b95224251b3      0t0         TCP localhost:hbci (LISTEN)
ruby    16873 masakazutakewaka   13u  IPv6 0x150e6b951d1cfa73      0t0         TCP localhost:hbci (LISTEN)

誰がネットワークの帯域を使っているのか調べたい

  • どのプロセスがどれくらい帯域を使っているのかが知りたければnethogs
  • 誰との通信で帯域が使われているのかが知りたければiftop

管理者権限で行われた捜査の履歴を辿りたい

auth.logを見る。 Ubuntuだと/var/log/auth.log

管理者権限の操作に必ずsudoが付くように、rootアカウントを無効化する。

# passwdにlockオプションをつけるとそのアカウントにログイン出来なくなる
$ sudo passwd --lock root

sshの総当たり攻撃を締め出したい

漫画ではfailregexに認証失敗を締め出す正規表現を追加していたが、僕の環境(Ubuntu18.04, fail2ban0.10.2)では/etc/fail2ban/jail.confに以下の記述があった。

# To use more aggressive sshd modes set filter parameter "mode" in jail.local:
# normal (default), ddos, extra or aggressive (combines all).
# See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details.
#mode   = normal

いう通りに/etc/fail2ban/jail.localを作成。 実験したいだけなので締め出し期間は1分。

[sshd]
mode   = aggressive
bantime = 1m

デーモンを再起動して、sshで何回か意図的に認証失敗してみたら、きちんと締め出された。

# 検証用に用意した仮想マシンの中
$ sudo systemctl restart fail2ban

# ホストマシンでsshの認証失敗を数回

# 検証用に用意した仮想マシンの中
$ sudo fail2ban-client status sshd

Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed: 6
|  `- File list:    /var/log/auth.log
`- Actions
   |- Currently banned: 0
   |- Total banned: 1
   `- Banned IP list:

なぜデフォルトで締め出しの設定がないのかというと、fail2banが本来弱いパスワード認証に対する締め出しに使われるもので、ssh認証の諦めメッセージConnection closed by...はログを溢れさす以上のことはしないかららしい。(?)

管理者権限が必要な操作を一部許可する

/etc/sudoersを編集する。開くとThis file MUST be edited with the 'visudo' command as root.とあるので、visudoで編集する必要がある。visudoはバリデーションが効く。

シンタックスユーザー名 この設定が有効なIP=(なり変われるユーザー:なり変われるグループ) 実行を許可するコマンド。 ()内は(ALL)の様にグループを省略可能で、「なれるグループなし」という意味になるが、sudo -gはあまり使われないので問題なしとのこと。

自分に対して管理者権限でのpasswdsuを禁止してみる。

# /etc/sudoersに以下の様に書かれているので新たにファイルを作成する
# Please consider adding local content in /etc/sudoers.d/ instead of
# directly modifying this file.
$ sudo visudo -f /etc/sudoers.d/take

# /etc/sudoers.d/takeの中
take ALL=(ALL:ALL) ALL, !/bin/su, !/usr/bin/passwd

# 1番下の設定の優先順位が1番高い
$ sudo -l
Matching Defaults entries for take on take-VirtualBox:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User take may run the following commands on take-VirtualBox:
    (ALL : ALL) ALL
    (ALL) ALL, !/bin/su, !/usr/bin/passwd, !/bin/ls

# suとpasswdがsudoで実行できなくなっていることを確認
$ sudo su
Sorry, user take is not allowed to execute '/bin/su' as root on take-VirtualBox.
$ sudo passwd
Sorry, user take is not allowed to execute '/usr/bin/passwd' as root on take-VirtualBox.

その他

  • トンネルを掘る時は最終地点から初期地点までの道を考える
  • sshはgオプションでトンネルを他のマシンにも開放する
    • e.g.) PCでトンネル掘ってスマホで接続
  • トンネルを自動復旧するautosshというコマンドがある
    • tmuxを使わなくてもいい?
  • ファイルの同一性を確かめるだけならmd5で事足りるが、そのファイルが外から来たものであれば必ずsha256などの安全なハッシュ関数を使う

VirtualBoxのマシン(Ubuntu)に原始的なやり方で固定IPを割り当てる

"漫画でわかるLinux シス管系女子3" の1章で原始的(?)な固定IPの割り当て方法が紹介されていた。具体的にはUbuntu/etc/network/interfacesを編集するやり方。

Amazon CAPTCHA

ネットワークの基本的な知識が確かめられるので、経験しておくのもいいと思いVirtualBoxで試してみた。(漫画に出てくる色んな技術を試すのに、コンテナより仮想マシンの方がいいのかなという割とふわっとした考え)

Ubuntuのバージョンは18.04。

/etc/network/interfacesを開いてみる

まだループバックアドレスしか定義されていない。このファイルに固定IPアドレスの情報を追加していく。

# interfaces(5) file used by ifup(8) and ifdown(8)
auto lo
iface lo inet loopback

インターフェースのIPアドレスを調べる

eth0がなくて、代わりにenp0s3が存在していることに気づく。

調べたところenp0s3というのはsystemdが導入したもので、ハードウェアのコネクター(?)の名前らしい。従来のネーミングだと不都合があるっぽい。

https://askubuntu.com/a/704121

$ ip a

#...

2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:15:b1:d2 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
       valid_lft 86276sec preferred_lft 86276sec
    inet6 fe80::893:8d2b:c542:f28b/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

動的IPアドレスで、値は10.0.2.15サブネットマスク255.255.255.0

ちなみになぜ192.168.1.15のような感じではなく10.0.2.15なのかを調べてみたら、VirtualBox10.0.2.xのプライベートネットワークを作るからだった。(これは昔仕事でVirtualBoxを使っていた時に目にした記憶がある)

virtualbox - Trying to SSH to local VM Ubuntu with Putty - Unix & Linux Stack Exchange

ゲートウェイIPアドレスを調べる

ゲートウェイIPアドレス10.0.2.2

$ ip r
# default via 10.0.2.2 dev enp0s3 onlink
# 10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.20
# 169.254.0.0/16 dev enp0s3 scope link metric 1000

/etc/network/interfacesを編集する

こんな感じで書く。

# interfaces(5) file used by ifup(8) and ifdown(8)
auto lo
iface lo inet loopback

auto enp0s3
iface enp0s3 inet static
address 10.0.2.20
netmask 255.255.255.0
gateway 10.0.2.2

設定したインターフェースを起動する

$ sudo ifup enp0s3

意図通りに値が更新されている。10.0.2.1510.0.2.20

$ ip a

#...

2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:15:b1:d2 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.20/24 brd 10.0.2.255 scope global enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe15:b1d2/64 scope link
       valid_lft forever preferred_lft forever

ホストからsshできるか検証

VirtualBoxがつくるプライベートネットワークにホストからアクセスできないのでまずポートフォワーディングする。

f:id:masakazu-takewaka:20200203161633p:plain

host:~$ ssh -p 3022 take@127.0.0.1
take@take-VirtualBox:~$                  # VirtualBox machine

sshすることができたので、きちんと設定できてた模様。

「理論から学ぶデータベース入門」を読んだ

読んだのでメモ。

www.amazon.co.jp

1章 リレーショナルモデルとSQL

リレーショナルモデルとは、リレーショナルモデルとSQLにおける演算の比較

  • リレーショナルモデルはデータモデルの1つで集合論がベース
    • 集合 -> 要素が重複しない、要素が集合に含まれるか確実に判断可能
      • つまり集合にNULLはない
  • リレーショナルモデルのリレーションはSQLでいうテーブル
  • 次のシンブルなSELECT文は3つのリレーションの演算 SELECT (射影) FROM (直積) WHERE (制限);
  • SQLにあってリレーショナルモデルにないもの

2章 述語論理とリレーショナルモデル

述語論理の簡単な導入、リレーショナルモデルの演算を述語論理で表現

  • リレーショナルモデルは述語論理(論理学)もベースにしている
  • 命題 -> ある物事についての文で、真偽値が問えるもの
  • 命題論理 -> 既知の命題から他の命題の真偽値を導く
  • 量化
    • 集団を対象にした真偽値
      • ある集団のすべての要素がある性質を満たすか
      • ある集団にある性質を満たす要素があるか
  • 命題論理 -> 量化で拡張 -> 述語論理
  • 閉世界説
    • 未知の事実は存在しない(全ての事実はリレーションに含まれる)という考え方
      • 全ての問いがリレーションの演算で解決する

3章 正規化理論(関数従属性)

関数従属性、1NF - BCNFの解説

  • 正規化理論はデータの不整合性の原因である「重複」を取り除く方法
  • 候補キー
    • タプルを一意に特定できる、無駄な要素が含まれない属性の集合(無駄が含まれるとスーパーキー)
    • 1つのリレーションにそのような集合は複数存在しうるので「候補」
  • 関数従属性
    • AがわかればBもわかるとき、BはAに関数従属する
  • 無損失分解
    • リレーションをjoinで再構築可能なように複数のリレーションに分解する
  • 1NF
    • リレーションであるかどうか
      • 行と列に順序がない
      • 行が重複しない
      • 値にNULLがない
      • 値がこれ以上分解できない(意味的に)
  • 2NF, 3NF
    • 無損失分解で関数従属性を取り除いていく
  • BCNF
    • 自明なものを除いて関数従属性が存在しない
    • 多くの場合ここまでくると5NFまで満たしている

4章 正規化理論(結合従属性)

結合従属性、4NF - 6NFの解説

  • 結合従属性 -> リレーションRの見出しの部分集合をjoinしてRが再構築できれば、Rは結合従属性を持つ
    • 無損失分解ができるということ
    • 関数従属性は結合従属性の一部
  • 4NF - 6NF
    • 無損失分解で結合従属性を取り除いていく
    • 5NF -> 暗黙的ではない全ての結合従属性が取り除かれた状態。最後の正規形
    • 6NF -> 全ての結合従属性が取り除かれた状態。無駄な結合が増える

5章 リレーションの直交性

  • 直交 -> 2つ以上のリレーションに同じタプルが含まれない
    • 複数リレーション間の重複を解消するもの
    • 異なるテーブルに意味は違うが値は同じレコードが存在することもあるので、直交化は必須ではない

6章 ドメインの設計戦略

  • ドメイン -> 属性が取り得る値の集合
  • 優れたDB設計がしたいならまず優れたアプリケーション設計スキルを身につけよう
    • アプリケーションへの理解が曖昧であったり、設計に欠陥があると、DB設計も悲惨になる
  • DBは本質的なデータを扱う
    • 表示上の問題をDBに持ち込まない
  • ナチュラルキーの設計をする際は、用いる値がIDとして機能するかきちんと吟味する
    • ISBNは長年運用されているが、書籍の旧版と新版に同一の値を割り振る出版社のせいで、実はIDとしては機能していない
  • パーツに意味があるIDはその値に依存した処理で問題が発生する
    • 冗長で非効率なクエリになりがち
      • 1NFでないから

7章 NULLとの戦い

  • NULL -> unknownな状態
    • 1NFを満たさない
    • 閉世界説を破綻させる -> リレーショナルモデルも破綻
  • オプティマイザへの弊害
    • オプティマイザはクエリの結果が等価であることを数学的に証明できる組み合わせから選択する
      • NULLがあると数学的に証明できる組み合わせが激減する
  • クエリのコスト見積もりに影響
    • インデックス上でNULLはそのインデックスの先頭か最後尾にまとめられる
      • IS NULLでその部分を全スキャンする必要が生じる

9章 履歴データとの戦い

  • 履歴データとRDBは相性が良くない(リレーショナルモデルと噛み合わない)
    • リレーションでないので1NFを満たさない
      • 時間によってクエリの実行結果が変わる
        • リレーションとはある時点における事実のなので、これはリレーションではない <- ??
      • 初期値NULLのカラムが登場しやすい
      • 暗黙の意味を持つ特定のレコードの存在
        • e.g.) ある商品の最新の価格
          • リレーションとは「ある述語に当てはめると真になる要素の集合」なのでこれはリレーションではない

11章 インデックスの設計戦略

B+ Tree

  • 文字列は前方一致である必要あり
    • 文字列の左端からソートされてるから
    • マルチカラムインデックスの場合、左から順にカラムを指定
  • インデックスの更新 -> 削除と挿入 -> 高コスト

パーティショニング

  • 刈り込みによって検索が速くなることがメリット
    • 刈り込みが効かないとそれぞれのパーティションに検索をかけることになりむしろ高コスト
  • パーティショニングが有効なケース(多くのケースはインデックスで十分)
    • キーのカーディナリティが低い(大量にフェッチされる)場合
      • カーディナリティの低いインデックスで大量のデータをフェッチするとインデックスページとデータを格納したページを行き来して高コスト
        • パーティショニングをすれば1つのパーティションをスキャンするだけ済ませることが可能
    • アクセスの局所性が存在する場合
      • 刈り込み & ローカルインデックス(インデックスは各パーティションごとに作成される)

最適なインデックスを探す

  • クエリが決まってから高速化のためにインデックスを設計する
    • 先にインデックスを決め打ちしてそれに囚われてはいけない

where

select * from t1 where c1 > 100 and c2 = 'abc';

インデックスは(c2, c1)の順で貼る。('abc', 100) < (c2, c1) < ('abd', c1の最小値) という範囲がインデックスから検索される。

(c1, c2)だとc1 > 100が開いているため、B+ ツリーだとc2が解決されない。<- ??

join

select * from t1 join t2 on t1.c1= t2.c2
where t1.c3 =100 and t2.c4 = 'abc';

joinでは内部表(結合される方のテーブル)にインデックスが効く。

t2.c2がユニークではない場合(オプティマイザが駆動表と内部表を入れ替えることがある)、(c2, c4)のマルチカラムインデックスがあるとクエリが速くなる可能性がある。

t2をフェッチする時点でt1.c1 = t2.c2t2.c4 = 'abc'の両方が適用されて、テーブルをフェッチした後にwhere句で制限する手間が省かれる。

14章 トランザクションの本質

トランザクションとは

データを正しく保つための手法。DBに限った概念ではない

トランザクションの機能

  • スケジュール
    • 同時アクセスによって生じるデータの不整合を防ぐ
  • クラッシュリカバリ
    • 異常終了したらロールバックする、また再起動によって必要なデータを再構築する

スケジューラー

  • 異常の種類
    • lost update
    • inconsistent read
    • dirty read
      • まだコミットされていないデータを読んでしまうこと
    • non repeatable read
      • 同じデータを複数回読んだ時、トランザクション内でそのデータには書き込みをしてないにも関わらず違う値になっていること
    • phantom read
  • 分離レベル
    • 基本的にはserializableを選ぶ(PostgreSQLのデフォルトはserializable)
    • 性能が欲しい場合はread-commitedかrepeatable-readを選択して必要な時だけ明示的にロックを取ることもできる
    • read-uncommitedはロールバックが機能しないので使われない

クラッシュリカバリ

  • トランザクション理論が定義するDBサーバーのコンポーネント
    • stable DB -> ストレージ上のDB
    • stable log -> ストレージ上の履歴
    • DB cache -> メモリ上のDB
    • log buffer -> メモリ上の履歴
  • 各種更新はDB cacheで行われ、フラッシュすることでstable DBに反映される
  • リカバリは2手順
    • stable logに格納されたログエントリをREDOしてDB cacheを再構築
    • 完了していなかったトランザクションのログエントリを全てUNDO

とうとう負けてしまいました

カナダに来て早4ヶ月、とうとう負けてしまいました。誘惑に耐えきれず、日本のお菓子を買ってしまいました。(全部で日本円にして1500円。。)

f:id:masakazu-takewaka:20191015114526j:plain

こちらのスーパーには多種多様のチョコと甘いグミが陳列されていますが、僕の好みとは少し違います。僕は梅と唐辛子のお菓子をこよなく愛します。この世で最も好きな飲料は唐辛子梅茶で、小腹が空いた時には梅ねりが食べたい人です。

f:id:masakazu-takewaka:20191015121552j:plain

梅ねりといえばかつてカツオ梅ねり、するめねり、こんぶねりといったものがありましたね。どれも佐山聡の如く、時代を先取りしすぎた味をしていた。今なら味のわかる人がもっといるかもしれない

f:id:masakazu-takewaka:20191015120552j:plain

f:id:masakazu-takewaka:20191015120557j:plain

Goでマイクロサービスやってみた

序文

マイクロサービスという設計手法が世に広まって数年、 今では様々なプロジェクトでマイクロサービスが採用されているのを目にします。

興味はあるけどまだよくわかってない、でも実際に手を動かして理解を深めたい、といったモヤモヤを抱えている人もいるのではないでしょうか?

私もその一人でしたが、この度マイクロサービスでサンプルAPIを実装してみたのでその実装方法、実際に手を動かしてみることで得た所感を共有させていただきたいと思います。

簡単なマイクロサービスの実装

今回私はマイクロサービスアーキテクチャを用いたAPIのサンプルをGoで実装しました。

https://github.com/masakazutakewaka/grpc-proto

下図が今回実装したサンプルAPIの概観になります。

f:id:masakazu-takewaka:20181008234957p:plain

APIはユーザーとアイテム、コーディネートという3つのサービスで構成されていて、
それぞれのサービスは別々のコンテナ上に配置されています。
そしてそれらのサービスにhttpでアクセスするためのリバースプロキシを用意するところまでやりました。

ディレクトリ構造はこのようになっていています。

grpc-proto/
├── Gopkg.lock
├── Gopkg.toml
├── README.md
├── coordinate
├── docker-compose.yaml
├── gateway
├── item
├── user
└── vendor

docker-composeはこのようになっています。

docker-compose.yml

version: "3.6"

services:
  item:
    build:
      context: "."
      dockerfile: "./item/app.dockerfile"
    depends_on:
      - "item_db"
    environment:
      DATABASE_URL: "postgres://takewaka:takewaka@item_db/grpcproto?sslmode=disable"

  item_db:
    build:
      context: "./item"
      dockerfile: "./db.dockerfile"
    environment:
      POSTGRES_DB: "grpcproto"
      POSTGRES_USER: "takewaka"
      POSTGRES_PASSWORD: "takewaka"
    restart: "unless-stopped"

  user:
    build:
      context: "."
      dockerfile: "./user/app.dockerfile"
    depends_on:
      - "user_db"
    environment:
      DATABASE_URL: "postgres://takewaka:takewaka@user_db/grpcproto?sslmode=disable"

  user_db:
    build:
      context: "./user"
      dockerfile: "./db.dockerfile"
    environment:
      POSTGRES_DB: "grpcproto"
      POSTGRES_USER: "takewaka"
      POSTGRES_PASSWORD: "takewaka"
    restart: "unless-stopped"

  coordinate:
    build:
      context: "."
      dockerfile: "./coordinate/app.dockerfile"
    depends_on:
      - "coordinate_db"
      - "item"
      - "user"
    environment:
      DATABASE_URL: "postgres://takewaka:takewaka@coordinate_db/grpcproto?sslmode=disable"
      ITEM_URL: "item:8080"
      USER_URL: "user:8080"

  coordinate_db:
    build:
      context: "./coordinate"
      dockerfile: "./db.dockerfile"
    environment:
      POSTGRES_DB: "grpcproto"
      POSTGRES_USER: "takewaka"
      POSTGRES_PASSWORD: "takewaka"
    restart: "unless-stopped"

  gateway:
    build:
      context: "."
      dockerfile: "./gateway/gateway.dockerfile"
    ports:
      - "8000:8080"
    depends_on:
      - "item"
      - "user"
      - "coordinate"
    environment:
      ITEM_URL: "item:8080"
      USER_URL: "user:8080"
      COORDINATE_URL: "coordinate:8080"

一つのサービスに対して一つのDBを用意しています。
他のサービスのデータを要求する手段を、サービスのインターフェース経由に限定することで各サービスは疎結合になっています。

それでは早速各サービスの中身を見ていきましょう。

Item

ディレクトリ構成

item/
├── app.dockerfile
├── client.go
├── create_table.sql
├── db.dockerfile
├── main
├── pb
├── repository.go
└── server.go

DBとの連携を担うrepository層、gprcサーバーを定義するserver層、grpcサーバーと通信するクライアント側の機能を担うclient層から成り立ちます。
今回は簡単なサンプル実装ということでドメインロジックは用意していません。(かなり不自然ですが、そういう理由でドメインロジックを格納する層が存在しないです。)

まずはItemサービスのプロトコル定義から見ていきましょう。

プロトコル定義

item/pb/item.proto

syntax = "proto3";

package pb;

import "google/api/annotations.proto";

service ItemService {
  rpc getItem (getItemRequest) returns (getItemResponse) {
    option (google.api.http) = {
      get: "/item/{id}"
    };
  }
  rpc getItems (getItemsRequest) returns (getItemsResponse) {}
  rpc postItem (postItemRequest) returns (postItemResponse) {
    option (google.api.http) = {
      post: "/item"
      body: "*"
    };
  }
}

message Item {
  int32 id = 1;
  string name = 2;
  int32 price = 3;
}
.
.
.

ここではサービスのインターフェースとオブジェクトの型を定義しています。
また、google/api/annotations.protoを使って、httpで通信するリバースプロキシのエンドポイントの定義もここでしています。
protocol bufferのコンパイラであるprotocを使ってこのファイルをコンパイルすると

  • item.pb.go・・・Itemオブジェクトの型、gprcのAPIが定義されている。
  • item.pb.gw.go・・・リバースプロキシのAPIが定義されている。

が生成されます。
各層を実装していく上で、これらのファイルを適宜インポートしていきます。

※ protocコマンドをそのまま使うとオプションが大変なことになるので protoeasy を使いましょう。

client層

client層には、grpcサーバーと通信するクライアント側の機能を実装します。

item/client.go

.
.
.
type Client struct {
    conn    *grpc.ClientConn
    service pb.ItemServiceClient
}

func NewClient(url string) (*Client, error) {
    conn, err := grpc.Dial(url, grpc.WithInsecure())
    if err != nil {
        return nil, err
    }

    client := pb.NewItemServiceClient(conn)
    return &Client{conn, client}, nil
}

func (client *Client) Close() {
    client.conn.Close()
}

func (client *Client) GetItem(ctx context.Context, id int32) (*pb.Item, error) {
    res, err := client.service.GetItem(ctx, &pb.GetItemRequest{Id: id})
    if err != nil {
        return nil, err
    }
    return &pb.Item{
        Id:    res.Item.Id,
        Name:  res.Item.Name,
        Price: res.Item.Price,
    }, nil
}
.
.
.

Client型の2つのフィールドにはそれぞれ

  • conn : gprcサーバーへのコネクション
  • service : クライアント側が持つメソッドを集約したもの

が格納されています。

ItemServiceClientの定義

type ItemServiceClient interface {
    GetItem(ctx context.Context, in *GetItemRequest, opts ...grpc.CallOption) (*GetItemResponse, error)
    GetItems(ctx context.Context, in *GetItemsRequest, opts ...grpc.CallOption) (*GetItemsResponse, error)
    PostItem(ctx context.Context, in *PostItemRequest, opts ...grpc.CallOption) (*PostItemResponse, error)
}

Itemサービスと通信したい場合はNewClient関数を呼び出すことで、gprcサーバーとのコネクションを貼ることができます。

server層

server層にはgprcサーバー周りの機能を実装していきます。

item/server.go

.
.
.

type itemServer struct {
    r Repository
}

func ListenGRPC(r Repository, port int) error {
    listen, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
    if err != nil {
        return err
    }
    server := grpc.NewServer()
    pb.RegisterItemServiceServer(server, &itemServer{r})
    reflection.Register(server)
    return server.Serve(listen)
}

func (s *itemServer) GetItem(ctx context.Context, r *pb.GetItemRequest) (*pb.GetItemResponse, error) {
    item, err := s.r.GetItemByID(ctx, r.Id)
    if err != nil {
        return nil, err
    }
    return &pb.GetItemResponse{Item: item}, nil
}

.
.
.

肝となるのは

RegisterItemServiceServer(server, &itemServer{r})

の部分で、中で grpc#Server.RegisterService を使ってサービスに関わる実装をgprcサーバーに登録しています。
ここでいうサービスに関わる実装というのはitemServer型のフィールドであるRepositoryのことです。
Repositoryの実装は次のrepository層の説明で詳しく見ていきましょう。

repository層

repository層にはDBと直接やり取りする部分を実装していきます。

item/repository.go

.
.
.

type Repository interface {
    Close()
    GetItemByID(ctx context.Context, id int32) (*pb.Item, error)
    GetItemsByIds(ctx context.Context, ids []int32) ([]*pb.Item, error)
    InsertItem(ctx context.Context, name string, price int32) error
}

type postgresRepository struct {
    db *sql.DB
}

func NewPostgresRepository(url string) (Repository, error) {
    db, err := sql.Open("postgres", url)
    if err != nil {
        return nil, err
    }

    err = db.Ping()
    if err != nil {
        return nil, err
    }

    return &postgresRepository{db}, nil
}

.
.
.

func (r *postgresRepository) GetItemByID(ctx context.Context, id int32) (*pb.Item, error) {
    row := r.db.QueryRowContext(ctx, "SELECT id, name, price FROM items WHERE id = $1", id)
    item := &pb.Item{}
    if err := row.Scan(&item.Id, &item.Name, &item.Price); err != nil {
        return nil, err
    }
    return item, nil
}

.
.
.

Repositoryinterfaceにはserver層で定義されているインターフェースの中で使われる関数が集約されています。
それぞれの関数にはDBから取得したデータをGoの型にマッピングする機能を持たせています。

main.go

ポート8080でgrpcサーバーを立ち上げます。

item/main/main.go

.
.
.

func main() {
    dbURL := os.Getenv("DATABASE_URL")

    repo, err := item.NewPostgresRepository(dbURL)
    if err != nil {
        log.Fatal(err)
    }

    log.Println("listen to port 8080 ...")
    log.Fatal(item.ListenGRPC(repo, 8080))
}

文字通りプログラムのエントリーポイントであり、コンテナ内でこのファイルをコンパイルし実行しています。

item/app.dockerfile

FROM golang:1.10.3-alpine3.8 AS builder
WORKDIR /go/src/github.com/masakazutakewaka/grpc-proto/item
COPY vendor ../vendor
COPY item ./
RUN go build -o /go/bin/app main/main.go

FROM alpine:3.8
WORKDIR /usr/bin
COPY --from=builder /go/bin .
EXPOSE 8080
CMD ["app"]

User

ディレクトリ構成

user/
├── app.dockerfile
├── client.go
├── create_table.sql
├── db.dockerfile
├── main
├── pb
├── repository.go
└── server.go

プロトコル定義

user/user.proto

.
.
.

service UserService {
  rpc getUser (getUserRequest) returns (getUserResponse) {
    option (google.api.http) = {
      get: "/user/{id}"
    };
  }
  rpc getUsers (getUsersRequest) returns (getUsersResponse) {}
  rpc postUser (postUserRequest) returns (postUserResponse) {
    option (google.api.http) = {
      post: "/user"
      body: "*"
    };
  }
}

message User {
  int32 id = 1;
  string name = 2;
}

message getUserRequest {
  int32 id = 1;
}

message getUserResponse {
  User user = 1;
}

.
.
.

client層

Itemサービスのclient層とほとんど同じです。

user/client.go

.
.
.

type Client struct {
    conn    *grpc.ClientConn
    service pb.UserServiceClient
}

func NewClient(url string) (*Client, error) {
    conn, err := grpc.Dial(url, grpc.WithInsecure())
    if err != nil {
        return nil, err
    }

    client := pb.NewUserServiceClient(conn)
    return &Client{conn, client}, nil
}

func (client *Client) Close() {
    client.conn.Close()
}

func (client *Client) GetUser(ctx context.Context, id int32) (*pb.User, error) {
    res, err := client.service.GetUser(ctx, &pb.GetUserRequest{Id: id})
    if err != nil {
        return nil, err
    }
    return &pb.User{
        Id:   res.User.Id,
        Name: res.User.Name,
    }, nil
}

.
.
.

server層

Itemサービスのserver層とほとんど同じです。

user/server.go

.
.
.

type userServer struct {
    r Repository
}

func ListenGRPC(r Repository, port int) error {
    listen, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
    if err != nil {
        return err
    }
    server := grpc.NewServer()
    pb.RegisterUserServiceServer(server, &userServer{r})
    reflection.Register(server)
    return server.Serve(listen)
}

func (s *userServer) GetUser(ctx context.Context, r *pb.GetUserRequest) (*pb.GetUserResponse, error) {
    user, err := s.r.GetUserByID(ctx, r.Id)
    if err != nil {
        return nil, err
    }
    return &pb.GetUserResponse{User: user}, nil
}

.
.
.

repository層

Itemサービスのrepository層とほとんど同じです。

user/repository.go

.
.
.

type Repository interface {
    Close()
    GetUserByID(ctx context.Context, id int32) (*pb.User, error)
    ListUsers(ctx context.Context, skip int32, take int32) ([]*pb.User, error)
    InsertUser(ctx context.Context, name string) error
}

type postgresRepository struct {
    db *sql.DB
}

func NewPostgresRepository(url string) (Repository, error) {
    db, err := sql.Open("postgres", url)
    if err != nil {
        return nil, err
    }

    err = db.Ping()
    if err != nil {
        return nil, err
    }

    return &postgresRepository{db}, nil
}

func (r *postgresRepository) Close() {
    r.db.Close()
}

func (r *postgresRepository) Ping() error {
    return r.db.Ping()
}

func (r *postgresRepository) GetUserByID(ctx context.Context, id int32) (*pb.User, error) {
    row := r.db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", id)
    user := &pb.User{}
    if err := row.Scan(&user.Id, &user.Name); err != nil {
        return nil, err
    }
    return user, nil
}

.
.
.

main.go

Itemサービスのmain.goとほとんど同じです。

user/main/main.go

.
.
.

func main() {
    dbURL := os.Getenv("DATABASE_URL")

    repo, err := user.NewPostgresRepository(dbURL)
    if err != nil {
        log.Fatal(err)
    }

    log.Println("listen to port 8080 ...")
    log.Fatal(user.ListenGRPC(repo, 8080))
}

.
.
.

Coordinate

ディレクトリ構成

coordinate/
├── app.dockerfile
├── client.go
├── create_table.sql
├── db.dockerfile
├── main
├── pb
├── repository.go
└── server.go

プロトコル定義

coordinate/pb/coordinate.proto

.
.
.

service CoordinateService {
  rpc getCoordinatesByUser (getCoordinatesByUserRequest) returns (getCoordinatesByUserResponse) {
    option (google.api.http) = {
      get: "/user/{userId}/coordinates"
    };
  }
  rpc postCoordinate (postCoordinateRequest) returns (postCoordinateResponse) {
    option (google.api.http) = {
      post: "/coordinate"
      body: "*"
    };
  }
}

message Coordinate {
  int32 id = 1;
  int32 userId = 2;
  repeated int32 itemIds = 3;
}

message getCoordinatesByUserRequest {
  int32 userId = 1;
}

message getCoordinatesByUserResponse {
  repeated Coordinate coordinates = 1;
}

.
.
.

Coordinate型にはフィールドにユーザーのIDとアイテムのIDを持たせています。

client層

coordinate/client.go

.
.
.

type Client struct {
    conn    *grpc.ClientConn
    service pb.CoordinateServiceClient
}

func NewClient(url string) (*Client, error) {
    conn, err := grpc.Dial(url, grpc.WithInsecure())
    if err != nil {
        return nil, err
    }

    client := pb.NewCoordinateServiceClient(conn)
    return &Client{conn, client}, nil
}

func (client *Client) Close() {
    client.conn.Close()
}

func (client *Client) GetCoordinatesByUser(ctx context.Context, userId int32) ([]*pb.Coordinate, error) {
    res, err := client.service.GetCoordinatesByUser(ctx, &pb.GetCoordinatesByUserRequest{UserId: userId})
    if err != nil {
        return nil, err
    }
    return res.Coordinates, nil
}

GetCoordinatesByUserはユーザーのIDからコーディネートを取ってきます。

server層

coordinate/server.go

.
.
.

type coordinateServer struct {
    r          Repository
    itemClient *item.Client
    userClient *user.Client
}

func ListenGRPC(r Repository, itemURL string, userURL string, port int) error {
    itemClient, err := item.NewClient(itemURL)
    if err != nil {
        return err
    }

    userClient, err := user.NewClient(userURL)
    if err != nil {
        return err
    }

    listen, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
    if err != nil {
        itemClient.Close()
        userClient.Close()
        return err
    }

    server := grpc.NewServer()
    pb.RegisterCoordinateServiceServer(server, &coordinateServer{r, itemClient, userClient})
    reflection.Register(server)
    return server.Serve(listen)
}

func (s *coordinateServer) GetCoordinatesByUser(ctx context.Context, r *pb.GetCoordinatesByUserRequest) (*pb.GetCoordinatesByUserResponse, error) {
    _, err := s.userClient.GetUser(ctx, r.UserId)
    if err != nil {
        return nil, err
    }

    coordinates, err := s.r.GetCoordinatesByUserId(ctx, r.UserId)
    if err != nil {
        return nil, err
    }
    return &pb.GetCoordinatesByUserResponse{Coordinates: coordinates}, nil
}

.
.
.

CoordinateサービスではItemサービス、Userサービスとの連携があるので、coordinateServerのフィールドにitem.Clientuser.Clientを持たせて、Itemサービス、Userサービスのgrpcサーバーと通信できるようにしています。
GetCoordinatesByUserではコーディネートのデータを取得する前に、特定のIDのユーザーが存在するか確かめています。

repository層

coordinate/repository.go

.
.
.

type Repository interface {
    Close()
    GetCoordinatesByUserId(ctx context.Context, userId int32) ([]*pb.Coordinate, error)
    InsertCoordinate(ctx context.Context, userId int32, itemIds []int32) error
}

type postgresRepository struct {
    db *sql.DB
}

func NewPostgresRepository(url string) (Repository, error) {
    db, err := sql.Open("postgres", url)
    if err != nil {
        return nil, err
    }

    err = db.Ping()
    if err != nil {
        return nil, err
    }

    return &postgresRepository{db}, nil
}

func (r *postgresRepository) Close() {
    r.db.Close()
}

func (r *postgresRepository) Ping() error {
    return r.db.Ping()
}

func (r *postgresRepository) GetCoordinatesByUserId(ctx context.Context, userId int32) ([]*pb.Coordinate, error) {
    rows, err := r.db.QueryContext(ctx, "SELECT id, item_ids FROM coordinates WHERE user_id = $1", userId)
    coordinates := []*pb.Coordinate{}
    if err != nil {
        return nil, err
    }

    var itemIds string

    for rows.Next() {
        coordinate := &pb.Coordinate{}
        if err := rows.Scan(&coordinate.Id, &itemIds); err != nil {
            return nil, err
        }
        coordinate.UserId = userId
        coordinate.ItemIds, err = SliceItemIds(itemIds)
        if err != nil {
            return nil, err
        }
        coordinates = append(coordinates, coordinate)
    }
    if err := rows.Err(); err != nil {
        return nil, err
    }
    return coordinates, nil
}

.
.
.

main.go

coordinate/main/main.go

.
.
.

func main() {
    dbURL := os.Getenv("DATABASE_URL")
    itemURL := os.Getenv("ITEM_URL")
    userURL := os.Getenv("USER_URL")

    repo, err := coordinate.NewPostgresRepository(dbURL)
    if err != nil {
        log.Fatal(err)
    }

    log.Println("listen to port 8080 ...")
    log.Fatal(coordinate.ListenGRPC(repo, itemURL, userURL, 8080))
}

.
.
.

リバースプロキシ

ディレクトリ構成

gateway/
├── gateway.dockerfile
└── main

main.go

gateway/main/main.go

import (
  .
  .
  .
    "github.com/golang/glog"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "google.golang.org/grpc"

    coordinatePb "github.com/masakazutakewaka/grpc-proto/coordinate/pb"
    itemPb "github.com/masakazutakewaka/grpc-proto/item/pb"
    userPb "github.com/masakazutakewaka/grpc-proto/user/pb"
)

func run(itemURL string, userURL string, coordinateURL string) error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}

    err := itemPb.RegisterItemServiceHandlerFromEndpoint(ctx, mux, itemURL, opts)
    if err != nil {
        return err
    }
    err = userPb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, userURL, opts)
    if err != nil {
        return err
    }
    err = coordinatePb.RegisterCoordinateServiceHandlerFromEndpoint(ctx, mux, coordinateURL, opts)
    if err != nil {
        return err
    }

    return http.ListenAndServe(":8080", mux)
}

func main() {
    itemURL := os.Getenv("ITEM_URL")
    userURL := os.Getenv("USER_URL")
    coordinateURL := os.Getenv("COORDINATE_URL")

    defer glog.Flush()

    if err := run(itemURL, userURL, coordinateURL); err != nil {
        glog.Fatal(err)
    }
}

作ってみて感じたこと

時間がかかる

今回作ったサンプルAPIRailsでモノリシックに作ればほんの一瞬できるものですが、マイクロサービスにしたことで結構時間がかかった感じがします。
よく言われることですが、マイクロサービスにすることで発生する初期のオーバーヘッドはかなり大きいと感じました。
中規模以上のアプリケーションをマイクロサービスで作ってこの初期のオーバーヘッドを回収していくんだなぁと実感できました。

各機能が疎結合になる

疎結合になることで、シンプルに考えられたり、実装できたりするのは純粋に利点だなぁと実感しました。
ただ疎結合に関する難しさは、既存のサービスの機能を切り分けることそのものにあるのだろうとも思いました。

デバッグが増える

サービス間の通信の部分でのエラーなど、モノリスだと考えなくてよかったものが出てきて、運用する際に悩みのタネになったりしそうだなぁと思いました。

まとめ

今回、マイクロサービスで簡単なサンプルAPIを実装してみたので

  • 実装方法
  • 実装してみた所感

を書きました。

やはりアプリケーションにあった設計方法というものがあって、その判断を適切にするということが重要なんだと実感することができました。
"銀の弾丸"などないんだなーと。

この記事がどなたかの参考に少しでもなれば幸いです。

参考記事

Using GraphQL with Microservices in Go - Outcrawl