Hateburo: kazeburo hatenablog

Operations Engineer / Site Reliability / 運用系小姑 / Perl Monger

TCP over WebSocket & IAP

Googlehuproxyみたいなもので、任意のprotocolが通しやすく、Google Cloud Load BalancingのIdentity aware proxyに対応したものが欲しかったので、作ってみた。

github.com

github.com

GCPの中のMySQLに対して、service accountで認証して接続するイメージとしては以下のようになる。

f:id:kazeburo:20181022164001p:plain

server側

pathとforward先の設定を行うmapファイルをまずつくる

mysql,10.0.x.x:3306
ssh,127.0.0.1:22

起動

$ wsgate-server --listen 0.0.0.0:8080 --map map-server.txt

これで、

ws://example.com/proxy/mysql にWebSocketで通信を行うと 10.0.x.xの3306 に対してforwardするproxyが起動する

client側

serverと同じようにmapファイルをつくる。今度はportと行き先のurl

127.0.0.1:8306,https://example.com/proxy/mysql
127.0.0.1:8022,https://example.com/proxy/ssh

起動

$ wsgate-client --map map-client.txt

これで各サーバにログインできるようになる。

# mysql
$ mysql -h 127.0.0.1 --port 8306 --user ...

# ssh
ssh -p 8022 user@127.0.0.1

IAPを使う場合

Google Cloud Load Balancing を設定し、IAPを有効にする。 WebSocketを使う場合、timeoutを長くするのがおすすめ

f:id:kazeburo:20181022164955p:plain

クライアント側は、service accountのJSONファイルと、OAuth2のClient IDを指定して起動する

$ wsgate-client --map map-client.txt --iap-credential=/path/to/json --iap-client-id=foo.bar

これで、service accountで認証しつつ、任意のサーバと通信ができるようになる。

この他、wsgate-server, wsgate-clientには簡単な公開鍵認証と、任意のヘッダがつけれる仕組みがあるので、認証しつつ任意のプロトコルで通信することが簡単に実現できる。

TCP over WebSocket、去年の夏にインターンに来てくれた若者がアイディアを出してくれて、それ以降少しずつ使っているところが増えている。便利

普通のサーバでDocker ContainerをHot DeployしたかったのでProxy書いた

2018年も後半だけど、普通のサーバ上でコンテナをHot Deployしたいと思ってproxy書いた

github.com

任意のラベルがついたDocker containerのpublic portをdocker api経由で取得して、private portをlistenして、proxyを開始するものです。 ラベルがついたコンテナが複数個あると、必ず一番新しいものだけにproxyするようになってます。

これで、みんな大好きserver_starterと組み合わせると、Hot Deployができます。

使い方

コンテナを起動

$ KILL_OLD_DELAY=5 start_server -- docker run -P -l app=nginx nginx

public portが32774、private portが80となります

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                   NAMES
20ff30afc6a9        nginx               "nginx -g 'daemon of…"   7 seconds ago       Up 6 seconds        0.0.0.0:32774->80/tcp   practical_blackwell

この状態で motarei さんを起動

$ sudo ./motarei -l app=nginx
2018/10/09 17:28:50 Start listen 0.0.0.0:80

80番だったので、sudoが必要になりましたが、motareiが port 80をlisten開始しています。 この状態で、curl 127.0.0.1 すれば素敵なnginxのページがでます。

コンテナをアップデートしたくなったので server starter に対してHUPを送ります。

$ kill -HUP 33022

コンテナも入れ替わりました。

docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                   NAMES
264bafb478ff        nginx               "nginx -g 'daemon of…"   53 seconds ago      Up 52 seconds       0.0.0.0:32775->80/tcp   agitated_dubinsky

motareiさんは、docker apiに1秒ごとにアクセスし、portの変更を検知して、proxy先を変更します。 古いコンテナ後も、curl 127.0.0.1 は成功するはずです。

もともと、gobetweenというproxyがdocker apiをみてproxy先を切り替える機能をもっていたのですが、新しいコンテナだけにリクエストを行うことができず、hot deployには使えなかったので新しく作った次第です。

github.com

ちなみに、motareiさんはあたま屋さんです

YAPC::Okinawa 2018 ONNASON に行ってきた

YAPC::Hokkaido、YAPC::Fukuoka に続き YAPC::Okinawa 2018 に行ってきました。

発表

発表は2つ。

前夜祭

開催前日に作った資料。logrotateとfluentdとPHPが絡んだわりと複雑な問題に対してLD_PRELOADというやや斜め上の方法をとった話です。

本編スペシャルセッション

PHPとGo言語が中心になっている企業の中での「Perlのお仕事」の紹介です。cronlogにslackへの通知機能を盛り込んだslacklogと、大規模なトラフィックの一部を支えているPerlQ4Mの話を紹介しました。今後Perlがどのような立場になっていくのか個人的な考えも含め述べさせていただきました。

冒頭でmemcachedの問題についてもすこし紹介しています。

他にもバススポンサーをやらせていただいていたので、バスの中でトークなどを行いました。

会場のOIST、すごく綺麗な場所で驚きました。今回は天気が残念でしたがまたチャンスがあれば行きたい。

他の方のtalkではxaicronのPerlでH2の話やmacopyのwebsocketの話などが印象に残りました。PerlでH2の話を聞いて思い出したので、Gazelleに103 Early Hintsの対応を会場で行い、リリースしておきました。

nipotanのLINE NEWSの話がとても懐かしく、楽しく聞かせていただきました。

見てきた

花粉はすごく楽でした。

前日昼過ぎについたので、少し観光

去年の夏にJR最南端は行ったので、今度は日本最南端と最西端の駅。 最西端はゆいレール那覇空港、最南端はその隣の赤嶺駅

そのままゆいレールに乗って首里城まで行ってきた。

首里城は要塞のような印象をうけました。

今回のYAPC用にデザイナさんに作っていただいたメルカリのシール。個人的にかなり気に入りました。

f:id:kazeburo:20180306004718p:plain

さいごに

沖縄で開かれるYAPC最高でした。また沖縄行きたい。 スタッフの皆様、参加者の皆様ありがとうございました!

2泊3日行かせてくれた奥様、息子娘、そして来ていただいた奥様の実家の義母にも感謝。ありがとうー。

さて、次は東京で開催。行くぞ!

2017年 喋ってきたまとめ

振り返り的なエントリです。

3月

さくらインターネット プレゼンツ メルカリ×はてなの夕べ

さくらさん、はてなさんのイベントのセッションに参加しました。専用サーバをどのように使っているかという話をはてな id:wtatsuru さんとしました。

hatenanews.com

SendGrid Night 7

SendGrid Night 7 メルカリUS-UKでの事例とバウンスメール処理/Mercari US-UK and Bounced mail processing // Speaker Deck

メルカリUS/UKで使っているメール配信の話をしました。EventHookを使ってリターンメール処理とmackerelによるモニタリングを実現しています。

sendgrid.kke.co.jp

AWS Dev Day 2017

Cloud connect the world as a Glue - AWS Dev Day 2017 // Speaker Deck

AWSさんのカンファレンスで、メルカリの世界3拠点での開発運用体制、クラウドの利用について喋りました。 DC間、クラウド間の通信を効率的に行う chocon というプロダクトに興味をもっていただいた方が多くいました

www.awssummit.tokyo tech.mercari.com

7月

YAPC::Fukuoka 2017 Hakata

Mercari Timeline Personalization powered by Perl in 5minutes // Speaker Deck

博多で行われたYAPCでのLTです。Personalized Timelineはよりパワーアップしており、その基盤をPerlが支えています。

yapcjapan.org

8月

hbstudy#75 SRE大全:メルカリ編

SRE大全 メルカリ編 前半 #hbstudy 75 / SRE Taizen Mercari 1 hbstudy#75 // Speaker Deck

SRE大全 メルカリ編 後半 #hbstudy 75 / SRE Taizen Mercari 2 hbstudy#75 // Speaker Deck

ハートビーツさんの勉強会にて前後半あわせて2時間の長時間発表をしました。ここまでのSREの取り組みのまとめとなりました。

heartbeats.jp tech.mercari.com

WEB+DB PRESS Vol.100 メルカリ特集

登壇ではありませんが、1号からお世話になっているWEB+DB PRESSの記念すべきVol.100 のメルカリ特集に寄稿しました。

gihyo.jp

tech.mercari.com

9月

Enterprise Development Conference

開発と運用でサービスの信頼性を高める 「SRE」の実践/Mercari SRE in practice Enterprise Development Conference // Speaker Deck

日経BPさん主催のイベントで喋りました。 SREやメルカリSREの取り組みについて紹介しました

ac.nikkeibp.co.jp

10月

Mackerel Day

Driving Mercari with 50+ custom plugins / Mackerel DAY // Speaker Deck

Mackerel 3周年のイベントで、Mackerelの利用方法や監視設計、プラグインをいっぱい作っている話をしました。プラグインはまだまだ増えてます

mackerelio.connpass.com

Fastly Yamagoya Meetup 2017

CDNの使い方 in Mercari/CDN in Mercari // Speaker Deck

メルカリでも導入を行なっているCDN、Faslyさんのイベントで喋りました。

techplay.jp

11月

Monitoring Seminar in mercari

メルカリのシステム・サービス監視について/Monitoring Mercari service and servers // Speaker Deck

はてなさんとの共同イベントで話ました。メルカリのサービス拡大と監視システムとの関わりや、監視内容について紹介しました。

mackerelio.connpass.com

まとめと来年にむけて

各イベントに足を運んでいただいた皆様、主催者の皆様ありがとうございました。参考になる話があれば幸いです。 2018年は2月にデブサミ、3月にMANABIYAにて登壇予定です。YAPC::Okinawaにも参加します。今後ともぜひよろしくお願いします。

ISUCON7 予選通過した!!

ISUCON7に id:sugyanid:gfx と、チーム「スギャブロエックス」で出場して、2日目の2位、全体でも2位で予選通過できました。

icon画像さえ突破できれば、よく練られている問題で楽しめました。サーバもベンチマークも快適に動いて課題に集中出来ました。運営の皆様ありがとうございます。

すでに同じチームの2名がblogを書いているので、そちらも御覧ください

memo.sugyan.com

gfx.hatenablog.com

チームのソースコード、設定もgithubで公開しています。

github.com

最終スコアは 522461、ベストスコアも 522461 です。

言語の選択

最初はGo or Perlという話をしていたのですが、nodejsがサポートされるということが発表された段階で、gfxよりnodejsで行きたいという提案があり、nodejsで行くことにしました。

ISUCON出るなら新しいことを吸収しようという思いがあり、アプリケーションを書いたことはありませんでしたが、読めないことはないだろうということで、nodejsでいくことに。

実際には去年の問題をmatsuuさんのvagrantでローカルに作成し、gfxに少し教わりながらhackすることで勉強しました。最初スコア0だったのを36,000まであげることができました。

その際に

qiita.com

こちらの記事がとても参考になりました。ありがとうございます。

nodejs、正直これまであまりよい印象はありませんでしたが、await/asyncがつかえるようになると、callback地獄にならず普通の言語のように書ける、また高速だというのがわかりました。今後も使う機会増えそうです。

また、今回、サーバが1コアだったこともあり、nodejsという選択はかなり良かったのではないかと思います。

帯域問題

iconの画像配信をどうするか、というのがまずの問題でした。画像データはisucon3でやったのと同じく、webdavで1台に集める方法をとりました。

3台のサーバをisu701、isu702、isu703とすると、

  • isu701 - nginx(reverse proxy), app
  • isu702 - nginx(reverse proxy), app
  • isu703 - nginx(reverse proxy, webdav), mysql

のような構成になりました。703はmysqlが動くことがあり、appは動かさず他の2台にproxyしています。

davに切り替えた時点で、expireヘッダを追加していましたが、どうも304になっている数が少く、100Mbpsにぶつかっているということでスコアが伸びず悩んでいました。この間でgzip_staticやapigzip圧縮などもしていましたが、まったく伸びず。

そこで思いつきで、immutableを追加したらいいじゃねということで expire やめて以下を追加。

add_header Cache-Control "public, max-age=31536000, immutable";

immutableは関係ありませんでしたが、publicが効いて304の数が圧倒的に増えました。近頃Cacheしない方向の知見を溜めてた分、publicはなかなか出てこなかった。

これでスコアが一気に8万点まで上昇。他の上位陣からはだいぶ遅れましたが、やっと他のチューニングができる状態になりました。

その他のチューニング

  • nginx <-> app とnginx <-> nginx 間のkeepalive。効果わからず断念
  • 一番小さいcssだけcache-control追加しない。効果わからず断念
  • 最後に nginxのaccesslog off
  • fail2ban、ufwの停止、sysctlを追加設定

反省・感想など

  • callbackが大量に使われるコードが読めず、もう少しいろんなコードを読んでおくべきだった
  • appのチューニングはもう少しできることがあったなと思わないこともなく。画像に時間がかかり過ぎた
  • チームを組むのが初めてのメンバーで、しかも互いに同僚ではないが、いい感じで作業分担できた
  • 1年間固定してたツイート外した
  • 家で応援してくれた奥さん、息子、娘ありがとうー

GCP HTTP(S) load balancing 配下のnginxでクライアントIPを取得する方法

GCP HTTP(S) load balancing の X-Forwarded-For ヘッダは少し変わっているのでメモ。

X-Forwarded-For とクライアントIP

ELBや他のproxyを使って、その配下のサーバにリクエスト元のIPアドレスを伝える際には、X-Forwarded-For ヘッダが使われます。

X-Forwarded-For: $remote_addr

リクエストにすでにX-F-Fヘッダがあった場合は、後ろに追加します。

X-Forwarded-For: $http_x_forwarded_for, $remote_addr

となります。

X-F-Fを受け取ったサーバでは、アクセス元のIPが信用できるIPアドレスまたはIP帯域の場合に、X-F-Fの最後のIPアドレスを、remote_addrとして利用します。

nginxでは ngx_http_realip_module を使います。

set_real_ip_from  192.168.1.0/24;
real_ip_header    X-Forwarded-For;

アクセス元が 192.168.1.0/24 の場合、X-F-Fの最後のIPを利用するという意味です。

GCP HTTP(S) load balancing の X-F-F

本題のGCP HTTP(S) load balancingの場合、X-F-Fは

X-Forwarded-For: 1.2.3.4, 35.186.x.x

となります。この場合、1.2.3.4 が実際のclientのIPアドレス、35.186.x.xがロードバランサーに割り当てたIPアドレスです。また、アクセス元のIPアドレスは、以下の帯域(2017/2/21現在)となります。

130.211.0.0/22
35.191.0.0/16

この状態で、

まず

set_real_ip_from 130.211.0.0/22;
set_real_ip_from 35.191.0.0/16;
real_ip_header   X-Forwarded-For;

という設定をいれると、nginxはアクセス元を信用してX-F-Fの最後の一つをクライアントIPだとします。この場合は 35.186.x.x になってしまい、クライアントIPではありません。

さらに設定が必要になります。

set_real_ip_from 130.211.0.0/22;
set_real_ip_from 35.191.0.0/16;
set_real_ip_from 35.186.x.x;
real_ip_header   X-Forwarded-For;
real_ip_recursive on;

set_real_ip_from 35.186.x.xreal_ip_recursive を追加しました。real_ip_recursiveを有効にすると信用できるIPである限り再帰的にX-F-Fをたどっていきます。set_real_ip_fromにロードバランサーのIPを追加することで、35.186.x.xを信用し、次のIPアドレスをクライアントのIPとして採用ができるようになります。

この設定でようやく$remote_addrが1.2.3.4となりました。

まとめ

IPアドレスの追加の機会はそれほど回数がないので、IPアドレスをバランサーに追加した際にset_real_ip_from の追加を忘れるということは必ず発生するでしょう(発生した)。

アクセスログで正しくIPアドレスが解決ができているかの監視を行ったり、blogに書くなどしてチームに共有する必要があります。

Googleの皆様におかれましては、X-F-Fだけではなく、Akamai/CloudFlareのように True-Client-IP ヘッダの採用をお願いしたく候。True-Client-IPがあれば、ロードバランサーIPアドレスをnginxのconfに書く必要がなくなり、

set_real_ip_from 130.211.0.0/22;
set_real_ip_from 35.191.0.0/16;
real_ip_header True-Client-IP;

だけとなります。

何卒 :bow:

N秒間だけクエリ実行ログを取りたい

pt-query-digestだったり調査のために、N秒間だけmysqlの全クエリのログを取得したいということはよくありますよね

そんな時はこんなコマンドを使うと簡単に指定の秒数slowlogを切り替えて保存、取得後に元に戻してくれます。

$ slowlog.pl --duration 10 -- --default-extra-file=/hoge/my.cnf -uuser

-- のあとはmysqlコマンドに渡すオプション

ソース

#!/usr/bin/perl

use strict;
use warnings;
use IO::Handle;
use Getopt::Long;
use File::Spec;

sub find_path {
    my $pg = shift;
    my $path;
    for ( split /:/, $ENV{PATH} ) {
        if ( -x "$_/$pg" ) {
            $path = "$_/$pg";
            last;
        }
    }
    $path;
}

my $duration = 10;
Getopt::Long::Configure ("no_ignore_case");
GetOptions(
    "duration=s" => \$duration,
);
my @mysqlopt = @ARGV;
$|=1;

die "duration does not seems numeric" unless $duration =~ m!^\d+$!;
$duration += 0;

my $mysql = find_path('mysql')
    or die "could not find mysql";
my $tmpdir = File::Spec->tmpdir();

my $before = <<'EOF';
SET @cur_long_query_time = @@long_query_time;
SET @cur_slow_query_log_file = @@slow_query_log_file;
SET @cur_slow_query_log = @@slow_query_log;
SET GLOBAL slow_query_log_file = "<TMP_DIR>/slow_query_<DATE>.log";
SET GLOBAL long_query_time = 0;
SET GLOBAL slow_query_log = 1;
EOF

my $after = <<'EOF';
SET GLOBAL long_query_time = @cur_long_query_time;
SET GLOBAL slow_query_log_file = @cur_slow_query_log_file;
SET GLOBAL slow_query_log = @cur_slow_query_log;
EOF

$before =~ s!<TMP_DIR>!$tmpdir!;
my @lt = localtime();
my $date = sprintf('%04d%02d%02d%02d%02d%02d',$lt[5]+1900,$lt[4],$lt[3],$lt[2],$lt[1],$lt[0]);
$before =~ s!<DATE>!$date!;

print STDERR "exec mysql to change long_query_time and slow_query_log_file\n";
print STDERR "save slowlog to $tmpdir/slow_query_$date.log\n";
my $pid = fork;
if ( defined $pid && $pid == 0 ) {
    my $stop = 0;
    local $SIG{INT} = sub {
        $stop++;
    };
    local $SIG{TERM} = sub {
        $stop++;
    };

    open(STDOUT,'>/dev/null');
    open(my $pipe, '|-', $mysql, @mysqlopt, '--sigint-ignore');
    $pipe->autoflush;
    $pipe->print($before);
    for my $i ( 0..$duration ) {
        last if $stop;
        $pipe->print("SELECT 1;\n") if $i % 7 == 0;
        sleep 1;
    }
    $pipe->print($after);
    exit;
}
print STDERR "wait $duration seconds\n";
while (wait == -1) {}
my $exit_code = $?;
if ( $exit_code != 0 ) {
    die sprintf("Error: mysql exited with code: %d", $exit_code >> 8);
}

print STDERR "finished capturing slowlog.\n";

RDSとかAuroraとかよく知りません