TCP over WebSocket & IAP
Googleのhuproxyみたいなもので、任意のprotocolが通しやすく、Google Cloud Load BalancingのIdentity aware proxyに対応したものが欲しかったので、作ってみた。
GCPの中のMySQLに対して、service accountで認証して接続するイメージとしては以下のようになる。
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を長くするのがおすすめ
クライアント側は、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書いた
任意のラベルがついた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には使えなかったので新しく作った次第です。
ちなみに、motareiさんはあたま屋さんです
YAPC::Okinawa 2018 ONNASON に行ってきた
YAPC::Hokkaido、YAPC::Fukuoka に続き YAPC::Okinawa 2018 に行ってきました。
発表
発表は2つ。
前夜祭
開催前日に作った資料。logrotateとfluentdとPHPが絡んだわりと複雑な問題に対してLD_PRELOADというやや斜め上の方法をとった話です。
本編スペシャルセッション
PHPとGo言語が中心になっている企業の中での「Perlのお仕事」の紹介です。cronlogにslackへの通知機能を盛り込んだslacklogと、大規模なトラフィックの一部を支えているPerlとQ4Mの話を紹介しました。今後Perlがどのような立場になっていくのか個人的な考えも含め述べさせていただきました。
冒頭でmemcachedの問題についてもすこし紹介しています。
他にもバススポンサーをやらせていただいていたので、バスの中でトークなどを行いました。
会場のOIST、すごく綺麗な場所で驚きました。今回は天気が残念でしたがまたチャンスがあれば行きたい。
すごい会場! #yapcjapan pic.twitter.com/b1ltC3Ps7E
— masahiro nagano (@kazeburo) March 3, 2018
ようやく海と空が見えた!!!#yapcjapan pic.twitter.com/yP3iVEOmba
— masahiro nagano (@kazeburo) March 3, 2018
他の方のtalkではxaicronのPerlでH2の話やmacopyのwebsocketの話などが印象に残りました。PerlでH2の話を聞いて思い出したので、Gazelleに103 Early Hintsの対応を会場で行い、リリースしておきました。
nipotanのLINE NEWSの話がとても懐かしく、楽しく聞かせていただきました。
見てきた
沖縄は花粉が飛んでないか確認しに行ってきます。 pic.twitter.com/XYwTTxbwld
— masahiro nagano (@kazeburo) March 2, 2018
花粉はすごく楽でした。
前日昼過ぎについたので、少し観光
最南端と最西端制覇 pic.twitter.com/1f8tG2XkhB
— masahiro nagano (@kazeburo) March 2, 2018
去年の夏にJR最南端は行ったので、今度は日本最南端と最西端の駅。 最西端はゆいレール那覇空港、最南端はその隣の赤嶺駅。
YAPCみにきた pic.twitter.com/OttXanoely
— masahiro nagano (@kazeburo) March 2, 2018
首里城は要塞のような印象をうけました。
残業は国王の許可が必要 pic.twitter.com/M2QnKvc9o1
— masahiro nagano (@kazeburo) March 2, 2018
今回のYAPC用にデザイナさんに作っていただいたメルカリのシール。個人的にかなり気に入りました。
さいごに
沖縄で開かれるYAPC最高でした。また沖縄行きたい。 スタッフの皆様、参加者の皆様ありがとうございました!
2泊3日行かせてくれた奥様、息子娘、そして来ていただいた奥様の実家の義母にも感謝。ありがとうー。
さて、次は東京で開催。行くぞ!
2017年 喋ってきたまとめ
振り返り的なエントリです。
3月
さくらインターネット プレゼンツ メルカリ×はてなの夕べ
さくらさん、はてなさんのイベントのセッションに参加しました。専用サーバをどのように使っているかという話をはてな id:wtatsuru さんとしました。
SendGrid Night 7
SendGrid Night 7 メルカリUS-UKでの事例とバウンスメール処理/Mercari US-UK and Bounced mail processing // Speaker Deck
メルカリUS/UKで使っているメール配信の話をしました。EventHookを使ってリターンメール処理とmackerelによるモニタリングを実現しています。
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が支えています。
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 のメルカリ特集に寄稿しました。
9月
Enterprise Development Conference
日経BPさん主催のイベントで喋りました。 SREやメルカリSREの取り組みについて紹介しました
10月
Mackerel Day
Driving Mercari with 50+ custom plugins / Mackerel DAY // Speaker Deck
Mackerel 3周年のイベントで、Mackerelの利用方法や監視設計、プラグインをいっぱい作っている話をしました。プラグインはまだまだ増えてます
Fastly Yamagoya Meetup 2017
CDNの使い方 in Mercari/CDN in Mercari // Speaker Deck
メルカリでも導入を行なっているCDN、Faslyさんのイベントで喋りました。
11月
Monitoring Seminar in mercari
メルカリのシステム・サービス監視について/Monitoring Mercari service and servers // Speaker Deck
はてなさんとの共同イベントで話ました。メルカリのサービス拡大と監視システムとの関わりや、監視内容について紹介しました。
まとめと来年にむけて
各イベントに足を運んでいただいた皆様、主催者の皆様ありがとうございました。参考になる話があれば幸いです。 2018年は2月にデブサミ、3月にMANABIYAにて登壇予定です。YAPC::Okinawaにも参加します。今後ともぜひよろしくお願いします。
ISUCON7 予選通過した!!
ISUCON7に id:sugyan と id:gfx と、チーム「スギャブロエックス」で出場して、2日目の2位、全体でも2位で予選通過できました。
icon画像さえ突破できれば、よく練られている問題で楽しめました。サーバもベンチマークも快適に動いて課題に集中出来ました。運営の皆様ありがとうございます。
すでに同じチームの2名がblogを書いているので、そちらも御覧ください
最終スコアは 522461、ベストスコアも 522461 です。
言語の選択
最初はGo or Perlという話をしていたのですが、nodejsがサポートされるということが発表された段階で、gfxよりnodejsで行きたいという提案があり、nodejsで行くことにしました。
ISUCON出るなら新しいことを吸収しようという思いがあり、アプリケーションを書いたことはありませんでしたが、読めないことはないだろうということで、nodejsでいくことに。
実際には去年の問題をmatsuuさんのvagrantでローカルに作成し、gfxに少し教わりながらhackすることで勉強しました。最初スコア0だったのを36,000まであげることができました。
その際に
こちらの記事がとても参考になりました。ありがとうございます。
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やapiのgzip圧縮などもしていましたが、まったく伸びず。
そこで思いつきで、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.x
と real_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とかよく知りません