Hash λ Bye

https://utky.github.io/ に移行したので今後こちらに記事は追加されません

ブログページ移行します

https://utky.github.io/

に移行したので今後こちらに記事は追加されない予定。

移行してみて

よかったこと

  • ブログ以外のものも含めた全体を一貫して管理できる
  • いろいろ自分で道具を磨いて遊べるので楽しい

わるかったこと

  • hatenablog からの流入ユーザがぼちぼちいたのがごっそり減った
  • GitHub Page なのでスマホから直接記事投稿とかが困難になった(技術的にはできそうだけどやってない)

Netlink IPCを使ってLinuxカーネルのネットワーク情報にアクセスする

背景

Linux networkingには下記のような様々な構成要素があります。

  • ネットワークデバイス
  • 経路テーブル
  • フィルタ
  • etc.

これらを操作する必要がある場合、一般的には iproute2 などのコマンドラインツールから操作を行うことが多いかと思います。

しかしOpenStackやKubernetesなどの大規模なクラスタを管理する環境では、複数ホストにまたがってネットワークを動的にかつ一貫した方法で構成することが求められます。 こうしたニーズを実現するために、Linux networkingの構成要素を自作のプログラムから直接制御することが最も効率的な場合があります。 NetlinkというLinuxのサブシステムはLinuxカーネル内のネットワーク関連リソースを制御します。このNetlinkサブシステムを活用することでネットワーク管理を自動化することができます。

概要

NetlinkとはLinuxカーネルのサブシステムの名称です。 このサブシステムとユーザ空間のアプリケーションがやりとりするためのインタフェースとしてソケットベースのIPCが定義されています。 アプリケーションは一般的なソケットプログラミングの作法を通じてLinuxカーネル管理下のネットワーク関連リソースを操作することができます。

この記事ではNetlinkサブシステムとの典型的な通信方法について調査し説明します。 NetlinkサブシステムとIPC経由で通信するための典型的な作法については、 Kernel Korner - Why and How to Use Netlink Socket こちらの記事でコードと共に解説されていますのでそれだけでも充分参考になります。

通信方法を示すための例としてここでは、ネットワークインタフェースの情報を取得するケースについて記述します。

全体のコードは こちら

この記事で使ったコードのリビジョン

  • linux kernel : 4.19 84df9525b0c27f3ebc2ebb1864fa62a97fdedb7d
  • iproute2: 260dc56ae3efbac6aa80e946d18eb0c66b95c5a4

IPCによる操作

ここではNetlinkを使ったLinux networkingの設定を行うことを想定します。 特に簡単な例としてネットワークデバイスの作成・削除を例にとります。

IPCメッセージの送受信の流れは概して下記のようになります。

  • 1) socketの確保
  • 2) アドレスの確保
  • 3) 要求メッセージの作成
  • 4) 要求メッセージの送信
  • 5) 応答メッセージの受信
  • 6) 応答メッセージの解析

以降ではこれらのステップごとにコードの例を示しながら説明をします。

1) socketの確保

ソケットの作成には socket を使います。

SOCKET(3P)

SYNOPSIS
       #include <sys/socket.h>

       int socket(int domain, int type, int protocol);

この関数はソケット作成に必要なプロトコルや通信方式を表す定数を渡します。

ソケット作成が成功するとソケットのファイルディスクリプタが返却されます。 失敗した場合は -1 が返却されます。

以降では指定するべき引数について説明します。

domain

ソケット通信に使うプロトコルファミリーを指定します。

ADDRESS_FAMILIES(7)には指定可能なファミリーの定義が記載されています。 一般的によく使うのは

  • AF_INET : IPv4通信
  • AF_LOCAL : Unix domain socket

type

データ送受信の通信方式を指定します。 TCPのような信頼性が高くストリーム指向の通信をおこなうか、UDPのようなデータグラム転送方式で通信をおこなうか選択できます。

protocol

domainで指定した種別に対応する具体的な利用プロトコルを指定します。 0を指定するとデフォルトの実装が使われます。

Netlinkの場合

Netlinkの操作を行う場合、この関数に渡す引数はほぼ決まっています。

  • domain : AF_NETLINK
  • type : SOCK_DGRAM
  • protocol : NETLINK_ROUTE

protocolにはNetlinkサブシステム上に定義されたいくつかの機能から選びます。 定義はNETLINK(7)で確認することができます。

ネットワークデバイスや経路テーブルの制御には NETLINK_ROUTE を指定します。

NETLINK(7)

       NETLINK_ROUTE
              Receives routing and link updates and may be used to modify
              the routing tables (both IPv4 and IPv6), IP addresses, link
              parameters, neighbor setups, queueing disciplines,  traffic
              classes and packet classifiers (see rtnetlink(7)).

コード例

    int fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
    if (fd < 0) {
        perror("Opening socket failed");
        return errno;
    }

負値が返却された場合はソケット作成に失敗しているので、エラーハンドリングを行います。

2) アドレスの確保

ソケットを通信の端点として利用可能にするためにはソケットに特定のアドレスを与える必要があります。 使う domain によってアドレスを表すデータ構造は異なります。

例えばIPv4用ソケットのアドレスは下記のような定義になります。

include/uapi/linux/in.h

struct sockaddr_in {
  __kernel_sa_family_t  sin_family;     /* Address family               */
  __be16                sin_port;       /* Port number                  */
  struct in_addr        sin_addr;       /* Internet address             */

  /* Pad to size of `struct sockaddr'. */
  unsigned char         __pad[__SOCK_SIZE__ - sizeof(short int) -
                        sizeof(unsigned short int) - sizeof(struct in_addr)];
};

IPv4プロトコルにおけるアドレスとして

を指定する必要があることが分かります。 またここでも sin_family のフィールドに socketdomain と同じプロトコルファミリーを指定する必要があります。

Netlinkの場合は先頭のプロトコルファミリー以外にもいくつか固有のフィールドがあります。

include/uapi/linux/netlink.h

struct sockaddr_nl {
        __kernel_sa_family_t    nl_family;      /* AF_NETLINK   */
        unsigned short  nl_pad;         /* zero         */
        __u32           nl_pid;         /* port ID      */
        __u32           nl_groups;      /* multicast groups mask */
};
  • nl_pad : パディング用の領域
  • nl_pid : ポート番号
  • nl_groups : このソケットで受信を許可するマルチキャストメッセージのグループ

アドレスが作れたらソケットとアドレスを対応付けます。 そのために使うのが関数 bind です。

BIND(3P)

SYNOPSIS
       #include <sys/socket.h>

       int bind(int socket, const struct sockaddr *address,
           socklen_t address_len);

対応付けに成功する場合は 0 を返し、失敗した場合は負値を返します。

  • socket : 対応付けるソケットのファイルディスクリプタ
  • address : プロトコルに対応したアドレス構造体のポインタ
  • address_len : アドレス構造体のデータ長

コード例

    int pid = getpid();
    struct sockaddr_nl sa;
    memset(&sa, 0, sizeof(sa));
    sa.nl_family = AF_NETLINK;
    sa.nl_pid = pid;

    if (bind(fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) {
        close(fd);
        perror("Binding socket to address failed");
        return errno;
    }

pid にはアドレスを利用するプロセスのPIDを渡すこともできます。 用途はあくまでもポート番号であるため、識別性があればPIDである必要はありません。

3) 要求メッセージの作成

ソケット経由で送信するNetlinkメッセージは大きく2つの層に分離することができます。

  1. Netlink固有ヘッダ nlmsghdr
  2. データグラム通信共通ヘッダ msghdr

メッセージを作成する場合はまず nlmsghdr とそのボディを先に作ってから、 msghdrカプセル化した構造体を送信用関数に入力することになります。

nlmsghdr

Netlink固有ヘッダは下記のような構造になっています。

include/uapi/linux/netlink.h

struct nlmsghdr {
    __u32       nlmsg_len;  /* Length of message including header */
    __u16       nlmsg_type; /* Message content */
    __u16       nlmsg_flags;    /* Additional flags */
    __u32       nlmsg_seq;  /* Sequence number */
    __u32       nlmsg_pid;  /* Sending process port ID */
};

このデータ構造はあくまでもヘッダです。 実際の詳細な要求内容などのパラメータはボディとしてこのヘッダに続くデータ構造でエンコードされます。 ヘッダに続くデータ構造は nlmsg_type に応じて変わります。 ネットワークインタフェースを操作するメッセージや経路テーブルを操作するメッセージなど、目的に応じてこのヘッダの後に続けるデータの構造を返る必要があります。

Netlinkの NETLINK_ROUTE プロトコルでインタフェースを操作する場合は典型的にこのヘッダの後に

  • メッセージ種別固有のメッセージボディ ifinfomsg
  • 属性 rtattr 可変長リスト

が続きます。

| nlmsghdr | ifinfomsg | rtattr 1 | rtattr 2 | rtattr 3 | ... | rtattr N |

このため要求メッセージに必要な構造体を合成して一つの構造体とする下記のようなデータを定義する方法があります。

struct iplink_req {
    struct nlmsghdr        n;
    struct ifinfomsg   i;
    char           buf[1024];
}

フィールド nlmsgn_len

このヘッダ + ボディ の長さを指定します。 マクロ NLMSG_LENGTH にボディのデータ長を渡すと正しい値が計算されます。

include/uapi/linux/netlink.h

#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)

フィールド nlmsg_type

送信するメッセージの種類を指定します。 ソケット作成時に protocol に指定したNetlinkの機能ごとにメッセージ種別が定義されています。 今回の例であれば NETLINK_ROUTE に対応するメッセージ種別はRTNETLINK(7)にドキュメンテーションされています。

例えばヘッダファイル rtnetlink.h の定数定義ではリンクの操作として下記のように4種類のメソッドが定義されています。

include/uapi/linux/rtnetlink.h

 RTM_NEWLINK = 16,
#define RTM_NEWLINK    RTM_NEWLINK
    RTM_DELLINK,
#define RTM_DELLINK    RTM_DELLINK
    RTM_GETLINK,
#define RTM_GETLINK    RTM_GETLINK
    RTM_SETLINK,
#define RTM_SETLINK    RTM_SETLINK

ちょうどCRUDのような操作種別が定義されているのが分かると思います。 この記事の想定しているネットワークインタフェース情報の取得であれば RTM_GETLINK を指定することになります。

今回は RTM_GETLINK を指定してインタフェース情報を取得してみようと思います。

フィールド nlmsg_flags

Netlinkに共通するメッセージのオプションをフラグ形式で指定します。

NETLINK(7)

       Standard flag bits in nlmsg_flags
       ──────────────────────────────────────────────────────────────────
       NLM_F_REQUEST   Must be set on all request messages.
       NLM_F_MULTI     The message is part of a multipart message termi‐
                       nated by NLMSG_DONE.
       NLM_F_ACK       Request for an acknowledgment on success.
       NLM_F_ECHO      Echo this request.

       Additional flag bits for GET requests
       ────────────────────────────────────────────────────────────────────
       NLM_F_ROOT     Return the complete table instead of a single entry.
       NLM_F_MATCH    Return  all entries matching criteria passed in mes‐
                      sage content.  Not implemented yet.

       NLM_F_ATOMIC   Return an atomic snapshot of the table.
       NLM_F_DUMP     Convenience macro; equivalent to
                      (NLM_F_ROOT|NLM_F_MATCH).

共通のフラグだけでなくGET系メッセージ固有のフラグも定義されていることが分かります。 今回のネットワークインタフェースの参照リクエストではこのGET系メッセージ用のフラグもつけます。

  • NLM_F_REQUEST : リクエストであることを明示する
  • NLM_F_ACK : メッセージ処理成功時にACKが返ることを要求する
  • NLM_F_DUMP : 条件にマッチする内容をすべて取得する

今回は NLM_F_REQUESTNLM_F_DUMP をフラグとして渡します。 NLM_F_ACK を指定するとACKメッセージの解析も必要になって手間が増えそうなので今回はやめておきます。 Netlinkサブシステムに確実にメッセージが届いたことを確認する必要があれば NLM_F_ACK をフラグを指定してACKメッセージを受け取るようにしてください。

フィールド nlmsg_seq

このシーケンス番号は要求メッセージと応答メッセージの対応を追跡するために使われます。

フィールド nlmsg_pid

アドレスに指定したポート番号と同じ数値を指定します。 名称はPIDとありますがプロセスIDを要求されているわけではなく、あくまでも通信端点の識別に必要な番号です。

インタフェース操作用メッセージ ifinfomsg

ネットワークインタフェースの操作に用いる要求メッセージには ifinfomsg という構造体を用います。 これはあくまでのインタフェース操作用のメッセージであるため、他の用途には使えません。 他にも経路制御用の構造体として rtmsg などがあります。

ifinfomsg はインタフェース作成・削除時などに使用するため、 どのインタフェースを操作するのかを特定するインデックスなどのフィールドを持ちます。

include/uapi/linux/rtnetlink.h

/* struct ifinfomsg
 * passes link level specific information, not dependent
 * on network protocol.
 */

struct ifinfomsg {
    unsigned char ifi_family;
    unsigned char __ifi_pad;
    unsigned short    ifi_type;       /* ARPHRD_* */
    int        ifi_index;      /* Link index  */
    unsigned   ifi_flags;      /* IFF_* flags */
    unsigned   ifi_change;     /* IFF_* change mask */
};

コードだけでは少し説明不足なのでmanも確認します。

RTNETLINK(7)

       RTM_NEWLINK, RTM_DELLINK, RTM_GETLINK
              Create, remove or get information about a specific network interface.  These messages contain an ifinfomsg structure  followed  by  a
              series of rtattr structures.

              struct ifinfomsg {
                  unsigned char  ifi_family; /* AF_UNSPEC */
                  unsigned short ifi_type;   /* Device type */
                  int            ifi_index;  /* Interface index */
                  unsigned int   ifi_flags;  /* Device flags  */
                  unsigned int   ifi_change; /* change mask */
              };
  • ifi_family : AF_UNSPEC を指定します
  • ifi_type : ARPHDR_* で定義されたリンクの種類
  • ifi_index : インタフェースのインデックス
  • ifi_flags : NETDFEVICE(7) にある SIOCGIFFLAGS の節を見よとのこと
  • ifi_change : manによると将来に向けて予約されているフィールドでありひとまず 0xFFFFFFFF を指定しておくとのこと
ifi_typeについて

ソースコードのコメントに ARPHRD_* と書いてあるとおりARP関連ヘッダファイルに利用可能な定数の定義があります。

include/uapi/linux/if_arp.h

/* ARP protocol HARDWARE identifiers. */
#define ARPHRD_NETROM  0      /* from KA9Q: NET/ROM pseudo    */
#define ARPHRD_ETHER   1      /* Ethernet 10Mbps      */
#define    ARPHRD_EETHER   2      /* Experimental Ethernet    */
#define    ARPHRD_AX25 3      /* AX.25 Level 2        */
#define    ARPHRD_PRONET   4      /* PROnet token ring        */
#define    ARPHRD_CHAOS    5      /* Chaosnet         */
#define    ARPHRD_IEEE802  6      /* IEEE 802.2 Ethernet/TR/TB    */
#define    ARPHRD_ARCNET   7      /* ARCnet           */
#define    ARPHRD_APPLETLK 8      /* APPLEtalk            */
#define ARPHRD_DLCI    15     /* Frame Relay DLCI     */
#define ARPHRD_ATM 19     /* ATM              */
#define ARPHRD_METRICOM    23     /* Metricom STRIP (new IANA id) */
#define    ARPHRD_IEEE1394 24     /* IEEE 1394 IPv4 - RFC 2734    */
#define ARPHRD_EUI64   27     /* EUI-64                       */
#define ARPHRD_INFINIBAND 32      /* InfiniBand           */

一般的なEthernetのデバイスに対するクエリであれば ARPHRD_ETHER を指定します。

ifi_indexについて

システム上でインタフェースを一意に特定できるインデックスです。 インタフェース名とペアになっているため下記の関数によって名前からインデックスに解決することができます。

IF_NAMETOINDEX(3)

NAME
       if_nametoindex,  if_indextoname  - mappings between network inter‐
       face names and indexes

SYNOPSIS
       #include <net/if.h>

       unsigned int if_nametoindex(const char *ifname);

       char *if_indextoname(unsigned int ifindex, char *ifname);

msghdr

ソケットで送信するデータを収める構造体である msghdr は下記のようになっています。

include/linux/socket.h

/*
 * As we do 4.4BSD message passing we use a 4.4BSD message passing
 * system, not 4.3. Thus msg_accrights(len) are now missing. They
 * belong in an obscure libc emulation or the bin.
 */
 
struct msghdr {
    void       *msg_name;  /* ptr to socket address structure */
    int        msg_namelen;    /* size of socket address structure */
    struct iov_iter    msg_iter;   /* data */
    void       *msg_control;   /* ancillary data */
    __kernel_size_t msg_controllen; /* ancillary data buffer length */
    unsigned int  msg_flags;  /* flags on received message */
    struct kiocb   *msg_iocb;  /* ptr to iocb for async requests */
};

しかし後方互換のために下記の構造体を渡すことができます。ここでは iovec というバッファを単純に扱えるこの互換構造を使います。

struct compat_msghdr {
    compat_uptr_t   msg_name;   /* void * */
    compat_int_t    msg_namelen;
    compat_uptr_t   msg_iov;    /* struct compat_iovec * */
    compat_size_t   msg_iovlen;
    compat_uptr_t   msg_control;    /* void * */
    compat_size_t   msg_controllen;
    compat_uint_t   msg_flags;
};
  • msg_name : ソケットアドレスへのポインタ
  • msg_namelen : ソケットアドレスのデータ長
  • msg_iov : 送信するリクエストのデータを格納するバッファ

iov_iter

msghdrペイロードを埋め込むためのコンテナとして使います。

include/uapi/linux/uio.h

struct iovec
{
        void __user *iov_base;  /* BSD uses caddr_t (1003.1g requires void>
        __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
  • iov_base : データの領域を指すポインタ
  • iov_len : データ長

The iov_iter interface

4) 要求メッセージを送信

送信するメッセージとオープンしたソケットのファイルディスクリプタを使います。

SENDMSG(3P)

NAME
       sendmsg — send a message on a socket using a message structure

SYNOPSIS
       #include <sys/socket.h>

       ssize_t sendmsg(int socket, const struct msghdr *message, int flags);
  • socket : ソケットのファイルディスクリプタ
  • messge : 送信するメッセージを指すポインタ
  • flags : 送信時の振る舞いを細かく制御するフラグ

返却値が負値の場合は送信に失敗しているため、 errno を参照してエラーハンドリングをします。

コード例

    if (sendmsg(fd, &msg, 0) < 0) {
        close(fd);
        perror("Sending message failed");
        return errno;
    }

5) 応答メッセージの受信

カーネルから応答を受け取る際には要求送信時に利用したソケットのファイルディスクリプタとヘッダ msghdr を使い回すようです。 iproute2のlibnetlink.c __rtnl_talk_iovを参照。

バッファの初期化

送信時に使った msghdr に埋め込まれている iovec を初期化することで受信データをカーネルからユーザ空間にコピーする空き領域を作ります。

#define MAX_RECV_BUF_LEN 32768

    char recv_buf[MAX_RECV_BUF_LEN];
    struct iovec riov = {
        .iov_base = &recv_buf,
        .iov_len = MAX_RECV_BUF_LEN
    };
    msg.msg_iov = &riov;
    msg.msg_iovlen = 1;

msg は送信時に使った msghdr を使い回します。 このヘッダ構造のインスタンスにはソケットのアドレスが紐付けられているため、 使い回すことで引き続き同じ通信アドレスを利用できます。

ただし msgペイロード部分には新しく受信したデータを格納する領域が必要であるため、 iovec を新しく確保して msg にそのポインタを埋め込みます。 iovec 内部のデータ格納用領域にはバイト配列である recv_buf を使います。

メッセージの受信

ソケットから受信したデータを取り出すには recvmsg を使います。

RECVMSG(3P)

NAME
       recvmsg — receive a message from a socket

SYNOPSIS
       #include <sys/socket.h>

       ssize_t recvmsg(int socket, struct msghdr *message, int flags);
  • socket : ソケットのファイルディスクリプタ
  • messge : 受信するメッセージを指すポインタ
  • flags : 受信時の振る舞いを細かく制御するフラグ

sendmsg と同様に負値

コード例

        int recv_len = recvmsg(fd, &msg, 0);
        if (recv_len < 0) {
            close(fd);
            perror("Receiving message failed");
            return errno;
        }

6) 応答メッセージの解析

メッセージの受信に成功するとNetlinkサブシステムから受信したデータを格納したアドレスを指すポインタが手に入ります。 メッセージの解析はこのポインタを起点としてアドレスを移動しながらデータを読み出していきます。

いい感じの解析方法の大枠は man ページに書いてあるのでそれを参考にします。

NETLINK(7)

           for (nh = (struct nlmsghdr *) buf; NLMSG_OK (nh, len);
                nh = NLMSG_NEXT (nh, len)) {
               /* The end of multipart message */
               if (nh->nlmsg_type == NLMSG_DONE)
                   return;

               if (nh->nlmsg_type == NLMSG_ERROR)
                   /* Do some error handling */
               ...

               /* Continue with parsing payload */
               ...
           }

メッセージの解析にはマクロを活用します。

NLMSG_OK

NLMSG_OK は指定された nlmsghdr のアドレスと recvmsg で得られたデータ長から正しい長さのデータが受信できているかを判定します。

NETLINK(3)

       int NLMSG_OK(struct nlmsghdr *nlh, int len);

ソケットから読み込んだメッセージが正しい長さである場合に解析処理を続行するための判定に使われます。

           for (nh = (struct nlmsghdr *) buf; NLMSG_OK (nh, len);
                nh = NLMSG_NEXT (nh, len)) {

NLMSG_NEXT

NLMSG_NEXT は指定された nlmsghdr のアドレスと recvmsg で得られたデータ長を元に次の mlmsghdr を指すアドレスを返します。

NETLINK(3)

       struct nlmsghdr *NLMSG_NEXT(struct nlmsghdr *nlh, int len);

このマクロはソケットから一度に複数のメッセージを読み出せた場合に 2つめ以降のメッセージの先頭のポインタを取得するために使います。

コード例

    struct nlmsghdr *rnh = recv_buf;
    if (!NLMSG_OK(rnh, recved)) {
        perror("Maybe received multi-part message in response");
        goto handle_failure;
    }

解析部分のコード例

下記のコード例では解析したデータをただ印字するだけになっています。

    struct nlmsghdr *rnh;
    for (rnh = (struct nlmsghdr *) recv_buf; NLMSG_OK (rnh, recv_len); rnh = NLMSG_NEXT (rnh, recv_len)) {
        /* The end of multipart message */
        if (rnh->nlmsg_type == NLMSG_DONE) {
            printf("NLMSG_DONE detected\n");
            break;
        }
        if (rnh->nlmsg_type == NLMSG_ERROR) {
            perror("Receiving message failed");
            return errno;
        }

        printf("Received nlmsghdr\n");
        printf("\tnlhdr address: %p\n", rnh);
        printf("\tnlmsg_len: %d\n", rnh->nlmsg_len);
        printf("\tnlmsg_type: %d\n", rnh->nlmsg_type);
        printf("\tnlmsg_flags: %x\n", rnh->nlmsg_flags);
        printf("\tnlmsg_seq: %d\n", rnh->nlmsg_seq);
        printf("\tnlmsg_pid: %d\n", rnh->nlmsg_pid);

        struct ifinfomsg *riim = NLMSG_DATA(rnh);
        printf("Received ifinfomsg\n");
        printf("\tifi_type: %d\n", riim->ifi_type);
        printf("\tifi_index: %d\n", riim->ifi_index);
        printf("\tifi_flags: %x\n", riim->ifi_flags);
        printf("\tifi_change: %x\n", riim->ifi_change);
    }

全体のコードは こちら

参考

Netlinkに関する情報を掴むのには下記が有用でした。

  1. Linux Kernalのソースコード
  2. iproute2ソースコード
  3. man
  4. 下記のリンク

  5. Kernel Korner - Why and How to Use Netlink Socket

  6. Manipulating the Networking Environment Using RTNETLINK
  7. Understanding And Programming With Netlink Sockets
  8. The iov_iter interface
  9. Linux, Netlink, and Go — Part 1: netlink

『リーダブルコード』を読んだ

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック

まあ、なんかみんな読んでそうだし、すぐ読めそうだしと思って読んでみた。 内容の引用はせずに感心したポイントだけ書く。

全体として

コードは理解しやすくなければならない

という基本原理に従ってそれを実現するためのコードの捉え方や書き方を伝えている。 それぞれの基準や考え方はこの本に限らず言われていることなので、この本を読まずとも自然と身につく人もいそう。

自分も過去に書いた自分のコードを見返して分かりにくいな、と感じることはこれまでたくさんあった。 その経験から少しずつ上記の原理のような「読み手に伝える」コードを意識するようになっていった。

なので自分の中にあった心がけみたいなものが、この本で改めて言語化されて思い出されたというのが雑感。 こまごまとしたポイントでは「なるほど」と思えるところもあったので、以降ではそういったポイントを拾っていく。

チームでコードを書くにあたって、本書の内容を一つの基準として持っておきたいと思うし、 チームの人達にどうシェアしていくべきなんだろうな、と考えるきっかけにはなった。(ので読んで良かった)

感心したポイント

4.4 縦の線をまっすぐにする

テストコードを書いていると同じ関数に対して実引数を変えて何度も呼び出すようなパターンに出くわす。

ここで実引数の並びが縦方向で整列するように視覚的調整を行うのは、忘れがちだけれどもやってみると幾分見やすくなったと感じるので良いと思った。 パラメータの組み合わせを一瞥して把握できるようになる。各ケースのパラメータの違いが強調されるので、読者もテストのパターンや入出力の対応に集中することができる。

コードの桁を整列して視覚的ノイズを減らすことで、強調したい部分をうまく伝えている。

GroovyのテストフレームワークであるSpockのData Driven Testではさらにデータの対応を強調した表現に特化した構文がある。 Data Tables

5.2 自分の考えを記録する

TODO, FIXMEなどを使って他のメンバーに向けて課題を共有するみたいなことは今までもあった。 しかしここで言われるように、より素直に設計の失敗や懸念をコードに残しておくことはあまりできていなかったかもしれない。

コードを書いている時に発想として「これはこういう構造を参考にできるな」と気づいた時などは、 参考ページへのリンクや考え方の説明を試みたコメントを残すこともあったものの、 もっとカジュアルに「このコードを読む未来の誰かのためにあれこれコメントで語る」まではやっていなかった。 この節を読んで、そのくらいまで気軽に書いていいのかもな、とも思った。

ただ、後々コードが消されたりするタイミングがあると、そのプログラマが「このコメント消していいのかな」と迷ったりするかもしれない。 他の人にとってのそのコメンタリーの言及スコープが明確でない、ということなのでコメンタリーとしては失敗していると言えそう。

6.5 入出力のコーナーケースに実例を使う

これは自分の知る範囲でもPythondoctestHaskelldoctest などで活用事例があった。 ある関数の振る舞いを直感的に捉える上で例が載っていると、理解がぐっと早くなるのは自分の体感としてもある。 コーナーケースの振る舞いを文章で記すだけでなく例示があれば、読み手も説明文に対する自分の理解が正しいかを確認できる。

自分の場合は規則や振る舞いについて一般的な説明を受けただけだとどうしても消化不良になりがちなので、 具体例を記してあると自分の理解の答え合わせができて安心する。

7.6 悪名高き goto

gotoはJavaでいうfinallyブロックへのジャンプのような使い方をする限りにおいてはそんなに忌避しなくても良いのでは、という話。

linuxkernel/fork.c をチラ見してみたが確かにリソース開放処理を漏れなく行うブロックにラベルを付けている。 なにか処理中断の必要性を検知するとそのラベルへと goto でジャンプしてリソース開放をしているようだった。

ちなみにいくつも goto に行き先があったり呼び出し元側へとジャンプしたりするのはスパゲティの元になるので駄目、と本書では言っている。

それならJavaで非検査例外を投げて2, 3スタック上でキャッチするような行為は普通に処刑になってしまうのではと思った。

8.7 式を簡潔にするもう1つの創造的な方法

マクロを使ってボイラープレートを消すのもいいのではないか、という話。

この例ではマクロの宣言と使用が関数の中に閉じており、フォーカスが明確なのであとで混乱を招くことは無さそうだ。 混乱を招かない限りは創造的と言ってもいいのだが、マクロを使うこと自体が創造的であるかのように勘違いしない方がいいな、という。

確かにマクロ導入前のコードでは色々なシンボルにまぎれてしまい、どのデータに注目しているのか分かりにくくなっている。 それに対してC++のコードでのアプローチがたまたまマクロであっただけで、他のプログラミング言語ならまた別のアプローチ (HaskellだとLens) で解けるのだろう。

9.3 変数は一度だけ書き込む

自分がイミュータブルなデータを使ったプログラミングが好きなのでこの節に反応しただけ。 Effective Javaのように長く愛されている書籍でも確か似たようなこと言及されていた気がする。

変数が長命で書き換わる回数が増えれば増えるほど変数の現在の状態を推測するのが難しくなるみたい。

と言いつつ僕たちはレジスタを長時間に渡り書き換えまくっているのに、それに対してはそんなに拒絶反応を示さないのは、なんでなんだっけ、と思ったりする。

10.4 汎用コードをたくさん作る

Lispの本とか読んでいるとよく言われるボトムアッププログラミングのアプローチの一端がこれなのかなと思っている。

コードの進化の方向がボトムアップなのかトップダウンなのかということが問題でもない。 どちらかというとドメイン固有関数とデータ一般を操作する関数を分けよう、というコンセプトが大事なのではないかと思う。

このビジネスロジックとライブラリ的な分割に基づいてアプリケーションのレイヤを捉える話は以前に プログラミングClojureにおける「データ」とは何か で書いた。

14.3 テストと読みやすさ

独自の「ミニ言語」を実装する

というのはテストに必要な入出力などの定義を独自の記法で表せるようにして、 テストコード内にその解釈系を実装する、というアプローチ。

個人的にはこういうローカルなDSLを各テストの都合に応じて拵えることを許すと各テストでミニ言語が散らばって、 かえって認知負荷が増大するのではないかなと危惧しているのであまり勧めたくない。

できるだけそのプラットフォームやライブラリでサポートされているデータ構造と関数を素直に使ってテストを書いた方が良いと思う。 その上にもう1つ解釈系のレイヤをわざわざテストコードに差し挟むことが繰り返された後の混乱の方が怖い。

コードの中に独自の拡張した構文を入れる、みたいなのLispでもあるけどやはり濫用は避けた方が良いとされるし、 使う前に熟考して欲しい感はある。

と、まあだいたいそんなところ。

『入門 監視』を読んだ

『入門 監視』 という本を読んだ。 普段は低レベルな部分の監視が多くて見失いがちだった、高レベルなポイントの監視について思いを馳せる機会を与えてもらったので読んでよかった。

以下、個人的に心の動いたポイントを記す。 本の内容については大して触れていないので、本書の内容そのものが知りたい人は読むしかない。

p.12 1.3.2 アラートに関しては、OSのメトリクスはあまり意味がない

体感としてもOSメトリクス自体にアラートつけて意味のあるアラート出せたことそんなに無かった。 監視を始めるにあたっていきなりこのOSメトリクスから取り組んでアラートを設定すると疲弊しそう。

メトリクス自体はとってもよい。 しかしトラブルが起きた時の解析材料としてのみ有用だと考えた方がよい。 OSから収集できるロードアベレージやメモリ使用率などが上昇したからといって、 必ずしもサービスに影響を与えるとは限らないことがある。 これに対してアラートを発することは、無駄なアラートの原因にもなる。 そのためメトリクス自体は収集しておいて、後のトラブルシュートで役立てる、というのがベターな使い方になる。

p.25 お願いだから円グラフは使わないで

かつてメモリの使用量を表示するグラフが円グラフで提供されていたことがあった。 実際に役に立たなかったのでそのとおりだと思う。

円グラフはあくまでも表示された瞬間のスナップショットでしかなく、時系列データの表現ではない。 メモリの枯渇を起こすまでの 傾向 がまったく可視化されないので肝心の時に役に立たない。 時系列データに対しては少なくともこの円グラフの表現は適切ではないと言える。

p.28 デザインパターン2: ユーザ視点での監視

経験的にサービス故障を検知する監視を最初に定義していたので、結果的にこのパターンに沿っていたように思う。

監視はいきなりシステムの深部から始めるのではなく、 システムの外側、つまりユーザが観測できるポイントから監視しましょう、ということ。

これまでの仕事柄、サーバサイドやネットワーク周辺の監視の仕事が多かった。 RESTful APIを提供しているサーバだと、特定のシナリオに従ってAPIを呼び出すようなスクリプトを定期的に実行し、その結果を監視するのが一番よさそう。 ネットワークにおいてはエッジからエッジまでユーザデータの転送を流し続けられるといい。JunosだとReal-time Performance Monitoring (RPMと呼ぶ)という機能を設定しておくと、同じ機能を持っているルータ間での実測性能をメトリクスとして蓄積できる。ルータ間でiperf動かしているような感じ。

p.47 3.3 インシデント管理

自分の対応を振り返ってみると、対応が終わったあとの振り返りが徹底できていないかな、という反省もあるので付箋を貼ったところ。

インシデント管理のラフなフローは下記のように

  1. 認識
  2. 記録
  3. 診断、分類、解決、クローズ
  4. 問題発生中のコミュニケーション
  5. 改善策

アラートとチケットについて

アラートをトリガとしてチケットが作成されるしかけができていると、 上記のリストの1, 2あたりは達成されていると思う。

しかしAtlassianの記事にはこんな注意事項もコメントされている。

現在の監視ツールの多くはノイズが非常に大きいことを考えると、アラートに基づいた自動チケット作成は、アラートのノイズ問題をチケット作成のノイズ問題へと拡大することになります。

アラートにノイズが多いのは現場あるあるかなと思う。 こんな現場ではチケット作成の自動化にすぐ飛びつくだけだとアラートのノイズの問題がチケットの滞留の問題に転化されるだけになってしまう。

このままでは運用が機能しなくなるので先にノイズを減らす対策をした方がよさそう。 本書でも「チェックボックス監視」というアンチパターンで指摘されているように不要な監視・アラートが仕込まれていることが原因でこのようにノイズに埋もれがちになる。

問題発生中のコミュニケーション

問題の解析とか対処している時は、他の部署とのコミュニケーションまで手が回らないことがあると思う。 本書ではインシデントの解消に取り組む人とは別に「コミュニケーション調整役」を定義している。

絶対そういうロール分けがあった方がいいと思う。 問題を解決を実行的に担当する人間には部署間調整やユーザへの周知などまで手が回らないのは容易に想像できる。 したがって作業担当と調整担当が最速の状況を把握しつつも、調整担当が関連部署への連絡をすることで、作業担当も集中して取り組めてミスも減らせる。

p.59 4.7 分意数

最小、平均、最大だけだと見逃す性質があるのですごく重要な観点。

システムのメトリクスをとっていると飛び抜けて大きいデータが急に現れたりすることがある。 最小、平均、最大だけ見ていると、最大がこの外れたデータで見えたりして驚く。 実際には70〜95パーセンタイルあたりをみてみると、外れたデータはなくて許容範囲に収まっていることが多い。

このようにGauge型メトリクスの跳躍を正しくとらえるためにも分意数による観測はとても役に立つ。 最小、最大だけでなくその間にあるデータの分布も見ましょう、ということなんだけど。

p.65 5.1 ビジネスKPI

サービスの正しさを監視する目的で実装をしてきたことが多いので、この視野での監視はカバーできていなかった。 本書でおさらいしてみて、こういうハイレベルなメトリクスの収集も面白そうだなと感じた。

こういうデータって経営層にレポートするのだろう。 報告に向けたレポート能力や定量データだけに頼りすぎないように誘導する議論のスキルが必要だったりしそう。 いろいろデータ以外にも配慮するべきところありそうだ。

p.80 6.3.1 フロントエンドパフォーマンスのメトリクス

必要性は認識していながら、サーバ側ばかりやってて実装方法をあまり知らなかったのだけど、本書を読んでなんとなくイメージついた。 ブラウザがAPI持っているらしい。(知らなかった)

バックエンドでどんなにチューニングしてもフロントエンドがボトルネックになってユーザ体験損なうということもあるだろう。 そういう問題を倒すための手法としてフロントエンドでの観察方法も身につけていく機会が持てればいいのだけど。

ユーザに近いことから監視ポイントを作っていくっていう目線で考えると、 ユーザにエラー表示した時点でアラート上がるように仕組みを作るのが第一歩になるのかな。

p.93 7.3 healthエンドポイントパターン

ping/pongくらいな単調なやつかと思ったらけっこうしっかりしたエンドポイントだった。

この例ではデータベース検索やキャッシュの取り出しを一回のリクエストの中で行うことで、 サブシステムとの結合をテストする目的があるらしい。

テスト、と書いたのがまさにこの監視の本質なのではないかと個人的には思っている。 少なくともサービスの正常性を監視をしようとするなら、継続的にテストを行うのが一番良いのはないか。 だから自動テストが常にプロダクション環境でも回るようにしておく。 勿論そのテストシナリオが複雑になりすぎないようにいくらかのトレードオフはあるはず。 しかし、いずれにせよ定期的なテストをサービス提供の裏で続けることでシステムの複合的なヘルスチェックを行う意義は大きいように思う。

このhealthエンドポイントではそうしたテストの中でも比較的小さいスコープ、つまり「コンポーネント同士のネットワーク間連携」を中心にチェックしている。 アプリケーションそれ自体で提供するならそのくらいの機能で十分かもしれない。

p.119 8.5 データベースサーバ

性能劣化に気づく重要な指標としてスロークエリは割と認識していたけど、 ミドルウェアの特性にあった監視戦略は引き出しにあまり無いなと感じた。

本書で紹介されているような 『実践ハイパフォーマンスMySQL』 のような本を読みたくなる日が来そう。 (その前に 『詳解システムパフォーマンス』 が読みたいが)

p.140 9.1.5 インタフェイスのメトリクス

スループットは事前の検証で計測することが多かったので、リアルタイムに監視する発想がなかった。 なのでなるほどと思う。

しかしプロダクション環境で物理帯域ぎりぎりのトラフィックをテスト的に印加するのは他のユーザへの影響があることも考えると気軽にできない。 スループットのテストはやはり定常監視とは別で議論した方がいいのか。

あまりすっきりした結論が出ず、個人的にはもやもやしている。

p.146 9.4 ルーティング

BGPのピアの監視はしていたが、下記のポイントは今までケアできていなかった。

  • TCAMのサイズ
  • ASパスの変更
  • 送受信プレフィクス数

サービス提供するお客様のBGPルータから広報される経路は、事前の設計で取り決めたものがあったので、 当時はあまり気にしていなかった。 しかし、いま考えてみるとお客様の使い方を観測するためにも上記のような監視ポイントを押さえておいても良かったのかもしれない。

という反省。

p.147 9.6.2 ハードウェア

個人的にはネットワーク機器の監視の中でもちょっと面白いと感じている部分だったりする。

  • シャーシの内部にどんな部品が内蔵されており、どんな役割をするのか。
  • 壊れるとしたらどんなアラートが上がるのか。

など、そのシステム(=ルータ)を分解していくような楽しさがあって、ハードウェア監視は好き。

LinuxのベアメタルとかもメーカごとのMIBを読んでどこを監視するのか探るのは面白い。

雑感

本書を読んでも分かるけど監視のスキルといっても幅広い。 うまく監視するためには監視対象をよく知らないといけない。

個人的には監視周りのことを仕事でやって面白いのはそんなふうに監視対象をハックしていく瞬間だったりする。 監視をするためにその対象をよく調べて発見をしていく過程が楽しい。

あと、本書に通底するNagiosへの怒り(「ただしNagios、てめーはだめだ」的な雰囲気)が非常にパーソナルで良かった。

プログラミング言語学習の助走

なにか気になるプログラミング言語を見つけたとする。

 

僕が社会人になりたての頃は形から入っていたのか、とりあえずそのプログラミング言語の本を買ってた。その本を片手にいきなり腰を据えてコードを書き始めるって感じたった。

 

真正面から取り組むのは意外と苦しくて結構挫折しがちだった。

 

ここ数年でやり方が変わってることに気付いた。

週一回くらいの気まぐれで気になっているプログラミング言語の公式サイトのドキュメントをちらっと読む。または買っておいた本を少し読む。

 

でもまだコードは書かない。色んなところにあるドキュメントを拾いながら少しだけ頭でっかちになる。

 

ふと時間のできたタイミングや作りたいものができたタイミングでようやく重い腰をあげてそのプログラミング言語を使う。

 

すると不思議な爽快感がある。憧れていた人に実際に会った時のような幻滅と歓喜の入り交じった妙な感覚。でもひどい落差も覚えないのですんなりとそのプログラミング言語を使い始められる。

 

事前に知識だけを入れておくことで、そのプログラミング言語に対して少し距離をおいた熱意をもつことができるのだろう。

ふーん、まあ、そんな感じだよね、でもそこが好き。みたいな。熟年夫婦の愛情に似ている。

 

HaskellClojureに対してはそんな風にして馴染んでいった。けれど、ずっと淡白かというとそうでもなくて、学び始めてからも新しい発見の度にテンション上がったりするので、ほとんど娯楽の永久機関なのでは、と最近思う。

 

まあ、僕はゆっくり堀の周りをうろついて助走をつけながら本丸に登る方が向いてるんだなーという話。

ということで何度も挫折してる数学はそういうアプローチで再挑戦しよう。

コードを書く時の生産性の低さに気付く

大して複雑なコードでもないのに、スクリプト書くのに3時間もかかった話。

問題

仕事で必要になりそうなスクリプトを書いてた。 公開できないけど。

以降に説明するような処理をするClojureスクリプトを書いていたが、 あらかた満足に動くようになるまで3時間もかかってしまった。

時間かかりすぎ、と思った。 ちゃんと設計や実装の戦略を仕込んでおけば半分の時間でできそうだなと、雑ながら感じている。

概要

ログの解析をおこなう。

JSON形式で出力されるログを走査して、 各種データの処理開始と処理終了のログを見つけたら、 そのログをペアリングして経過時間を計算する。

そのように各種データがそれぞれどのくらい時間かかっているのかをログから集計する。

一般的な処理ステップ

  • ログを読んでJSONデータ構造に復元
  • データの正規化
  • 処理の開始、終了にまとめる
  • 集計処理

シンプルだ

分析

どこに時間がかかっているのか?

体感

ソースにしているログのデータにばらつきがあったので、意外とコーナーケースの発見に時間がかかった。 pringデバッグしながらやっていたのでそれで時間くった気がする。

なのでログの解析(parsing)だけで2時間かかってたかもしれない。

何故時間がかかったのか?

なんでだろう。

  1. ゴミデータ、はずれデータが原因で動かない時のデバッグ
  2. API誤解したまま使って、predicateが逆に動いていたり

ふーん。

あ、あともう一個。

比較的大きめに関数を組んでから実行して、動かなくて部品に分解しなおしてデバッグ

これはかなりやり方よくなかったなと思う。 つい小さなパーツでの確認をとばして、どんどん大きい方に結合してからデバッグしちゃう。

小さいスコープ&合成

ということで、いま明確に認識しているのは、 この 小さな部品に分解したあとの小さなテストをしていなかった ところ。

リクスヘッジしてなかったなー。

(ちなみにすごい人はこういうところのリスクヘッジしなくても流れるように正しいコードを書くので基本スキルが違う気がしている)

どうするべきだったのか

実際のデータを使って入出力しつつ、 最小限の部品を入力と出力の間に挟むようにして動作確認していくべきだった。

Step 1

input ---> [ json parsing ] ---> output

Step 2

input ---> [ json parsing ] > [ normalize ] ---> output

Step 3

input ---> [ json parsing ] > [ normalize ] > [ accumlate ] ---> output

こんな感じでpipelineのpassを増やしていくようにして動きを見ていければよかったなーと。 テスト自体もプログラム本体の成長に従って修正されていくTDD的なやり方だったらよかったのかもしれない。

次のアクション

計測

時間を測ろう。

なんの機能あるいはどのステップの実装にどれくらい時間がかかっているかを知る必要がある。

gitのcommitのタイムスタンプから測れるかな。 でもsquashすると解らなくなるので、割とこまめに計測した方が良さそう。

pomodoro techniqueベースで時間管理することが多いから、 pomodoro timerの終わりでcommitするのが良いか。

練習

もっとたくさんプログラミングの練習をしたいな、と思った。

プログラムの速度よりは、振る舞いを実現できるか、というような機能要件にフォーカスしたい。 プログラムの効率・性能はそれから改善していけるので、まずはナイーブでも短い時間で実装できるスキルを磨きたい。

どんな問題を解くか?

どんな問題がいいかなー。

と考えた結果、手元にあってある程度現実的な問題を含む、『珠玉のプログラミング』でも読んで解いてみようかな。

読書

テスト駆動開発』もういちど読みつつ、 実践交えてやりたいな。

duct-frameworkに定時起動ジョブを仕込む

ただのtips。みんな気付いているけどショボすぎて言わないことだ。

ductをREPLから起動・停止くらいはできる人に向けている。 それも解らないよーという人は、なんというか、なんでこの記事を読もうと思ったのか。 そういう人はこちらを読むと良いと思う。

ClojureのDuctでWeb API開発してみた

結論

chimeをintegrantのライフサイクルに組み込むことでバックグラウンド処理するジョブが定義できるよという話。

構成要素

  • duct (core 0.6.2)
  • integrant
  • chime 0.2.2

やりかた

config.edn

まずは設定を書いておく

(抜粋)

 :example.job/channel {}
 :example.job/hello {:ch #ig/ref :example.job/channel}

:example.job/channel はジョブ起動の通知を送ってくれるchannel (core.asyncのchan) を保持する。

:example.job/hello は channel の通知を受け取って起動されるバックグラウンド処理そのもの。 ジョブの起動には通知をくれる channel が必要なので ig/ref で解決している。

clojure

(ns example.job
  (:require [integrant.core :as ig]
            [clojure.core.async :as a]
            [chime :refer [chime-ch]]
            [clj-time.core :as time]
            [clj-time.periodic :as periodic]))

(defmethod ig/init-key :example.job/channel [_ _]
  (chime-ch (periodic/periodic-seq (time/now) (time/seconds 5))
            {:ch (a/chan (a/dropping-buffer 1))}))

(defmethod ig/halt-key! :example.job/channel [_ ch]
  (a/close! ch))

(defmethod ig/init-key :example.job/hello [_ {:keys [ch]}]
  (a/go-loop []
    (when-let [time (a/<! ch)]
      (prn "Hello world at " time)
      (recur))))

少しかいつまんで解説。

channelの初期化

(defmethod ig/init-key :example.job/channel [_ _]
  (chime-ch (periodic/periodic-seq (time/now) (time/seconds 5))
            {:ch (a/chan (a/dropping-buffer 1))}))

chime-ch 指定された時刻にタイムスタンプデータを送信するchannelを生成する。

clj-time/periodicperiodic/periodic-seq を使ってchannelからメッセージを受け取る時刻の無限ストリームを取得する。 (個人的に時間の経過を無限ストリームで表現する設計大好き)

{:ch (a/chan (a/dropping-buffer 1))} ここは chime-ch で生成される channel のバッファに長さと廃棄ポリシを設定している。

channelの破棄

(defmethod ig/halt-key! :example.job/channel [_ ch]
  (a/close! ch))

channel を閉じるだけ。 channel からメッセージを受け取れなくなった軽量プロセスはparking状態のまま何もしなくなるので、実質的にバックグラウンドジョブは停止している。

channelを使ったジョブ

(defmethod ig/init-key :example.job/hello [_ {:keys [ch]}]
  (a/go-loop []
    (when-let [time (a/<! ch)]
      (prn "Hello world at " time)
      (recur))))

when-let を使って channel からメッセージが届く時のみ印字処理を行って次のメッセージがくるまで待機するようにループさせている。

Result

ductなのでREPLから (go) と入力するとサーバの起動とともにREPLに5秒間隔でHello worldとぬかしてくるジョブが起動する。

:initiated
"Hello world at " #object[org.joda.time.DateTime 0x86ddf48 "2018-04-15T07:49:08.518Z"]
"Hello world at " #object[org.joda.time.DateTime 0x7f4e3d0e "2018-04-15T07:49:23.518Z"]
"Hello world at " #object[org.joda.time.DateTime 0x6aa2035 "2018-04-15T07:49:28.518Z"]
"Hello world at " #object[org.joda.time.DateTime 0x549f285d "2018-04-15T07:49:33.518Z"

Web APIとバッチジョブ

モチベーションの話。

僕がやりたかったのは、Web API経由で操作可能なWebクローラみたいなもの。 クローラが巡回しにいくエンドポイントをCRUDで操作できる的なやつ。

ただ、Web APIとバッチジョブっていう組み合わせは 応用が効く ような気がしている。 ふつーの典型的なバッチジョブを作るにしても、ジョブの実行時間や結果の履歴などの統計情報をWeb API経由で取れるようにしておくと、他のAdmin向けサービスの統合がしやすくなったりする。

運用系APIがあるというのは僕みたいなインフラ屋からするととてもポイント高い。

(脱線) Finagleという実例

バックグラウンドジョブとは少し違うけど、僕がとても感心したのはfinagleというScala向けマイクロサービスフレームワーク

このfinagleはマイクロサービスのエンドポイントを作るたくさんのAPIを提供している。なかでも面白いのがサービスエンドポイントの統計情報もとれる仕組みが標準で提供されていること。

Metrics

ユーザのリクエストで起動するにせよ、定時起動するにせよ、サービスの稼働状況を抽出できるAPIを備えていると、デプロイ後のフィードバックが得やすくなる。 これはサービスの改善点を定量的に探ったり問題を検知したりするのに役立つ。

バッチジョブ作る人へ

こんな風にジョブの統計情報を外部にexposeできるような仕組みを仕込んでおくと、サービスが成長した時にありがたがられること間違いなし。

Web APIとバッチジョブは割といい組み合わせだと再認識した。

小さなバッチジョブを作ろうと思ったらまずは

lein new duct {プロジェクト名} +api +ataraxy

と打ってほしい。