BLOG::はるかさん

はるかさん のブログです。近況情報や技術的な話題など。

Goにおける画像ストリーム処理について発表しました - JPEG streaming in Go #pixiv_night

ご無沙汰しています、最近リードエンジニアという称号がつきましたはるかさんです。2017/03/14に開催されたpixiv nighit #2でGoにおける画像ストリーム処理の話をしました。そのうちpixiv inside に詳細がのるとおもうので、一旦スライドだけ公開しておきます。

speakerdeck.com

pixivの画像変換はほとんどGoで実装されていますが、実際はcgoの呼び出し箇所が多く、時間的には8割くらいCを書いていることになります。実際cgoの呼び出し箇所では面倒なことも多く、最終的にはすべてがGoの世界になるとうれしいんですが、全ライブラリ書き換えないといけないのでなかなか難しいところです。

ImageFluxはこのあたりのところを頑張ることで、うまいことやっていこうとしているので、ご興味あれば是非声をかけていただければと思います。

recruit.pixiv.net

nginxのtcp_nodelayディレクティブは設定しなくても良い

「nginx実践入門」お買い上げ頂きありがとうございます。レビュー等読ませて頂いていると、気になるところがあったので補足です。

nginx実践入門 (WEB+DB PRESS plus)

nginx実践入門 (WEB+DB PRESS plus)

tcp_nodelayは設定しなくても良い

tcp_nodelayディレクティブはTCP_NODELAYオプションを付与するための設定です。これはデフォルトで有効ですし、競合しそうなTCP_NOPUSHsendfileともうまいこと使えるように実装されています。そのため明示的にoffにすることは殆どないでしょう。このディレクティブがデフォルトで有効になったのはnginx 0.3.61の事なので、これより古いバージョンを使っている環境はあまり想像できません。

tcp_nodelayは何をするのか

さて本題はもう終わったのですが、tcp_nodelayディレクティブは何を設定するディレクティブなのか簡単に説明しておきます。tcp_nodelayディレクティブが有効であるとき、ソケットにTCP_NODELAYオプションが設定されます。TCP_NODELAYオプションはNagleアルゴリズムを無効にするためのオプションです。

NagleアルゴリズムはRFC896の著者であるJohn Nagle氏の名前から名付けられたアルゴリズムであり、このRFC中で説明されている"small-packet problem"を回避するためのアルゴリズムです。Nagleアルゴリズムでは、小さなパケットの送信を防ぐため、データを一旦バッファリングして大きなパケットにしてから送信しようとします。そのため、小さなパケットを送信しようとしたとき、バッファリングがタイムアウトするまで(調べてないけど200ms?)遅延が発生してしまうことになります。TCP_NODELAYを有効にすると、このバッファリングを無効にすることで、遅延を最小化することができます。

前述したように、nginxではキープアライブ接続の場合デフォルトでTCP_NODELAYを設定します。これにより、パケット到着までの遅延を短くすることを実現しています。特にTLSのハンドシェイクなどではかなり効いてくるはずです。

tcp_nopushディレクティブと組み合わせるとどうなるのか

さて、本書ではtcp_nopushディレクティブを有効にすることを推奨しています(詳しくは3.4節をごらん下さい)。このオプションはパケットをまとめることで送信するパケット数を最小化するためのオプションです。つまりTCP_NODELAYと逆の動きをするように見えます。ではtcp_nopushディレクティブとtcp_nodelayディレクティブの両方を有効にしても大丈夫なのでしょうか?

nginxではTCP_NOPUSHが有効になるのはsendfileを利用する場合のみです。ソースコードを見ると、LinuxではTCP_CORKを有効にするまえにTCP_NODELAYが有効であれば無効にするように動作します*1。そのため、両方のディレクティブが有効のままでも問題はありません。

設定しないといけないディレクティブは意外と少ない

nginxには数百ものディレクティブがあり、書籍中で取り上げたのはそのうちの一部です。静的ファイル配信に使うのであれば、設定する必要があるディレクティブはそれほど多くありません。自分の好みとしては設定する必要がないディレクティブは少ない方が嬉しいです。調べる必要もなくなるし、余計な手間を減らすことができます。ということで書籍中でもそのようなディレクティブは説明自体を省いています*2

設定しなければないディレクティブが少ないのはnginxの良いところの1つです。tcp_nodelayディレクティブはデフォルトで有効なので、明示的に記述する必要はないでしょう。sendfiletcp_nopushディレクティブは歴史的な事情と環境依存の問題によってデフォルトoffにされていることが推測できるのですが、通常の使用においては有効にして問題になることは少ないでしょう。ということで書籍で紹介しているようにsendfiletcp_nopushは明示的に有効にすることをおすすめします。

詳しくはnginx Tech Talksで

このあたりの話は2月の勉強会でやろうと思ってるので、是非参加いただければ幸いです。

eventdots.jp

*1:FreeBSDのためには別実装が存在しますが追いかけていません

*2:載せるべき、という意見はごもっともだと思いますし、tcp_nodelayディレクティブ以外に説明したほうが良かったな、と思っているディレクティブも多数あります。

書籍「nginx実践入門」のサンプルコードを公開しています

f:id:harukasan:20160115160510j:plain

サポートページなどには掲載して頂いているのですが、nginx実践入門のサンプルコードをGithubで公開しています。

github.com

本書は紙面の都合上、サンプルコードの説明よりも文章やディレクティブの挙動に重きをおいて説明しているところが多々あり、そのためリストも内容が大きく省略されていたりします。公開しているサンプルコードでは本文中の不足を補うため、基本的にはそのまま動作するコードにすることを心がけて作成しています。実際にコードをそのまま確認したい方はサンプルコードを利用して頂くと手間が省けるかもしれません。

また、本書の設定ファイルが動作するnginxをビルドするためのスクリプトも付属しています。DebianとUbuntuのみの対応となりますが(Travis CIでCIをまわしている関係でデフォルトはUbuntuです)、ビルドする際の参考になるはずです。

本書中ではビルド手順について、特定のディストリビューションに依る書き方を避けています。自分はDebianの場合、以下のようなスクリプトを使い回しています。このスクリプトだと設定ファイルを/etc/nginx/nginx.confにおいたり、/var/log/nginxにログファイルを出力したりします。参考になるかもしれません。

#!/bin/bash -ex

set -o pipefail

nginx_version="1.9.5"
openssl_version="1.0.2e"
pcre_version="8.37"
zlib_version="1.2.8"

sudo apt-get update

sudo apt-get install libgd-dev

mkdir src
pwd

cd src
test -d "nginx-${nginx_version}" || wget http://nginx.org/download/nginx-${nginx_version}.tar.gz && tar xvf nginx-${nginx_version}.tar.gz
test -d "pcre-${pcre_version}" || wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-${pcre_version}.tar.gz && tar xvf pcre-${pcre_version}.tar.gz
test -d "openssl-${openssl_version}" || wget https://www.openssl.org/source/openssl-${openssl_version}.tar.gz && tar xvf openssl-${openssl_version}.tar.gz
test -d "zlib-${zlib_version}" || wget http://zlib.net/zlib-${zlib_version}.tar.gz && tar xvf zlib-${zlib_version}.tar.gz

cd nginx-${nginx_version}

mkdir -p /var/lib/nginx

./configure \
  --with-openssl=../openssl-${openssl_version} \
  --with-zlib=../zlib-${zlib_version} \
  --with-pcre=../pcre-${pcre_version} \
  --sbin-path=/usr/sbin/nginx \
  --conf-path=/etc/nginx/nginx.conf \
  --error-log-path=/var/log/nginx/error.log \
  --pid-path=/var/run/nginx.pid \
  --lock-path=/var/lock/nginx.lock \
  --http-log-path=/var/log/nginx/access.log \
  --http-client-body-temp-path=/var/lib/nginx/client_body_temp \
  --http-proxy-temp-path=/var/lib/nginx/proxy_temp \
  --http-fastcgi-temp-path=/var/lib/nginx/fastcgi_temp \
  --http-uwsgi-temp-path=/var/lib/nginx/uwsgi_temp \
  --http-scgi-temp-path=/var/lib/nginx/scgi_temp \
  --with-debug \
  --with-pcre-jit \
  --with-http_ssl_module \
  --with-http_v2_module \
  --with-http_image_filter_module \
  --with-http_dav_module \
  --with-http_gunzip_module \
  --with-http_gzip_static_module \
  --with-http_stub_status_module

make
sudo make install

なお、サンプルコードの内容については2/8(月)開催のnginx Tech Talksでも触れたいと思っています。

eventdots.jp

nginx実践入門を執筆しました!それから出版記念イベントをやります

1/16(土)発売です。長い間書いていたのでようやくでた、というかんじですが、「nginx実践入門」を元同僚の@cubicdaiyaさんと2人で執筆しました。Amazonにて予約受付中です。電子版(PDF)もあります。

nginx実践入門 (WEB+DB PRESS plus)

nginx実践入門 (WEB+DB PRESS plus)

この本はそもそもWeb+DB Press Vol.72@cubicdaiyaさん、@semindさんと3名で執筆した「詳解nginx」がベースになっていますが、大幅に加筆した結果、殆どすべて書き直したかんじになりました。 nginx自体もアップデートしており、HTTP/2に対応したnginx 1.9.5を基準に紹介しているので、それほど古い内容にはなっていないのかなーと思います。

自分が担当した部分ですと、

  • 第3章 基本設定
  • 第4章 静的なWebサイトの構築
  • 第5章 安全かつ高速なHTTPSサーバの構築
  • 第6章 Webアプリケーションサーバの構築
  • 第7章 大規模コンテンツ配信サーバの構築
  • 第8章 Webサーバの運用とメトリクスモニタリング

となっておりまして、一般的なHTTPサーバとして使用する場合の基本的な機能は抑えられているかな、と思います。とくに追加した部分ですと、HTTPSサーバとしての設定など、これからのWebでは当たり前となる事柄が含まれています。小規模なサービスから大規模サービスまで幅広く使える内容になっているかと思います。

これだけ長い間書いたにも関わらず、紙面から漏れてしまった便利なディレクティブもたくさんあって書き足したい気持ちでいっぱいですが、ひとまず本という形でお届けできるのは嬉しいです。是非手にとって頂けると幸いです。

「nginx実践入門」出版記念!執筆者らが語る nginx Tech Talks

eventdots.jp

ということで、2/8(月) 19:00から渋谷dots.さまで出版記念イベントを開催します。書面に書ききれなかった多くを補えればいいかなーと思っているので、本書を予約していただいた方もそうでないかたも是非とも参加をお待ちしております。

GoのSliceは実際どうなっているのか

この記事はGo Advent Calendarのエントリではありません。

Go言語を勉強する人であれば、誰でもがsliceの扱いに躓くであろう。sliceの扱いがちょっとよくわからなかったので、まだよくわかってないけど、ひとまずメモ。

Goのアセンブラに関するドキュメント

sliceをアセンブリから扱うには

結論から書くと、今のところの理解では、sliceを関数に渡すとき、フレームにはslice構造体のアドレス、lencap、そして戻り値のアドレスが積まれている。つまり、次の様なコードを書けば、sliceのアドレスがSIlenがAX、capがBXに入り、要素の最初の値を返す関数になる。

// func FirstElem(x []uint64) uint64
TEXT ·FirstElem(SB),NOSPLIT,$0
MOVQ $x_base+0(FP), SI
MOVQ $x_len+8(FP), AX
MOVQ $x_cap+16(FP), BX
MOVQ 0(SI), SI
MOVQ 0(SI), CX
MOVQ CX, ret+24(FP)
RET

lencapが何故とれるのかよくわかってないけど、これでだいたいのことはできる。以下本題。

sliceはどうなっているのか

sliceの実装はsrc/runtime/slice.goにある。ということで、こんなブログを読まなくてもコードを読めば良い。

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

arrayには要素の配列のポインタ、lenには使用している長さ、capは確保済みの長さが入っている。このあたりはA Tour of Goをやっていれば理解できているだろうしブログにも書いてある。

図にすると次の様なかんじだ。

f:id:harukasan:20151204122947p:plain

ということで、先ほど示したアセンブリのように、引数をデリファレンスして、もう1回デリファレンスすれば要素の最初の値がとれた。これだけ理解しておけば、アセンブリからsliceを扱うことは比較的容易だ。

growするときにどうしているのか

ここまででだいたい実装はわかったけれど、良く理解できていなかったのでsliceの長さを拡張するときの実装を見てみる。実装はruntime.growsliceにある。エラー処理を省略してコメントを追加しておいた。

// growslice handles slice growth during append.
// It is passed the slice type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
func growslice(t *slicetype, old slice, cap int) slice {
    et := t.elem

    // 新しいcap(newcap)を計算する
    newcap := old.cap
    if newcap+newcap < cap {
        newcap = cap
    } else {
        for {
            if old.len < 1024 {
                newcap += newcap
            } else {
                newcap += newcap / 4
            }
            if newcap >= cap {
                break
            }
        }
    }

    lenmem := uintptr(old.len) * uintptr(et.size)
    capmem := roundupsize(uintptr(newcap) * uintptr(et.size))  
    newcap = int(capmem / uintptr(et.size))

    // メモリを確保して内容をコピーする
    var p unsafe.Pointer
    p = rawmem(capmem)
    memmove(p, old.array, lenmem)
    memclr(add(p, lenmem), capmem-lenmem)

    return slice{p, old.len, newcap}
}

まずはnewcapの計算から見てみよう。newcapは拡張後の新しいcapの値になる。newcapの計算処理は次の様なロジックだ。

  1. 現在のcapの2倍の値が求められているcapよりも小さければ、求められているcapnewcapにする
  2. lenが1024より短い場合は現在のcapを2倍にしていき、求められているcapよりも大きくなったらその値をnewcapにする
  3. lenが1024よりも長ければ1/4ずつ増やしていく

つまり、1024より短い時は今のcapの2倍以上、1024よりも長ければ1.25倍以上にするようになっている。これが良く聞くGoのsliceは2倍ずつ増えていく、の実装である。

拡張後のサイズが計算できたら後はその領域を確保し、値をコピーするだけである。

lenmemcapmemlencapのメモリ上で実際に必要な長さを求めている。これを求めるには単純に要素型のサイズをかければ良い。

領域の確保はruntime.rawmemが使われている。この関数の実装はつぎのようになっている。

// rawmem returns a chunk of pointerless memory.  It is
// not zeroed.
func rawmem(size uintptr) unsafe.Pointer {
    return mallocgc(size, nil, flagNoScan|flagNoZero)
}

mallocgcを呼ぶだけであるが、ひとまずはsize分mallocしてそのアドレスを返す、と理解しておけば今のところ問題ない。あとは確保したメモリに値をコピーし、すればおしまいである。memmovememclrの実装はアセンブリで書かれているが、ひとまず次の様に理解しておけば良いはずだ。

// frmからtoにlength分移動させる
func memmove(to *any, frm *any, length uintptr)

// ptrからlength分クリアする
func memclr(ptr *byte, length uintptr)

これでcapmemのサイズだけメモリをコピーし、lenmemの長さだけ値をコピーして、残りの領域をmemclrでクリアする、という流れがわかった。おしまい。

まとめ

最近のGoはGoで記述されているので非常に読みやすく、内部構造を理解しやすい。ということでGoで躓いたときはまずGoのコードを読めばよい。ところで、関数呼び出しの際にフレームにbase, len, capで積まれているように見えるんだけど、なぜこうなるのか未だに理解できていない。関数の引数がどういう風にフレームに積まれるのか、どこ読んだらわかるんだろ。

License

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

本記事はgolang/goで公開されているGoのソースコードを含んでおり、LICENSEファイルは以下のURLで提供されている。

Gulpでplayのアセットファイルを生成できるプラグインをつくった

最近仕事でplay frameworkを使っているのだけど、playにはアセットファイルをコンパイルしてくれるsbt-webというのがある。これは便利っぽいのだけど、いままでnpmの世界で育った人間にはちょっと困った。だいたい、gulp-なんとかってプラグインではなくsbt-なんとかってプラグインを探したり、試したり、つくったりしないといけないのはつらい。

playはpublicディレクトリの下にアセットファイルをおけば良いのだが、gzipしたファイルとかmd5ファイルとかを用意しておけば、それを良い感じに使ってくれる。このあたりは下の記事が詳しい。

d.hatena.ne.jp

gulpでのやり方も書いてあったんだけど、面倒なのでプラグインにしておいた。

www.npmjs.com

Gulpfile.jsにはこんなかんじでかけばよい。

var gulp = require('gulp');
var playAssets = require('gulp-play-assets');
var gzip = require('gulp-gzip');
 
gulp.task('build', function() {
  gulp.src('assets/**')
    .pipe(playAssets())
    .pipe(gulp.dest('./public')); // output unzipped files 
    .pipe(gzip({ append: true }))
    .pipe(gulp.dest('./public')); // output zipped files 

これでちゃんとgzipしたファイルもできて、playもそれを使ってくれる。あとはsbt起動したときにgulpも起動するようにしておけば便利だと思う(やったことないけど)。

はじめてのGulpプラグインなので皆さんのコントリビュートをお待ちしております。

Web+DB PRESS Vol.88にHHVMの運用について書きました

弊社執筆陣で連載しているPHP大規模開発入門も、もう第9回目となります。今週末8/22(土)に発売されるWeb+DB PRESS Vol.88ではHHVMを取り上げました。HHVMが出てだいぶ経つ気がしますが、PHP 7前のここのタイミングしかもう盛り上がる余地ないかなー、と思ってこのタイミングです。連載が続いてネタがきれてきたとかそういうことではありません。さて、内容ですが基本的にはHHVMの簡単な検証方法とpixivでの運用がメインになります。いまさらHHVMかーと思ってる方も多そうですが、PHP 7以降も継続的に開発されそうなので、HHVMやHackに興味がある方はお手にとってもらっても良い内容かと思います。

WEB+DB PRESS Vol.88

WEB+DB PRESS Vol.88

  • 作者: 佐々木拓郎,高柳怜士,鶴原翔夢,小野侑一,中村俊介,佐藤春旗,長野雅広,佐々木健一,久保達彦,若山史郎,佐藤太一,伊藤直也,道井俊介,佐藤歩,泉水翔吾,坪内佑樹,海野弘成,西尾泰和,中島聡,はまちや2,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2015/08/22
  • メディア: 大型本
  • この商品を含むブログを見る