ISUCON5予選を全体1位で通過しました

ISUCON5 の予選1日目にチーム「fujiwara組」(@fujiwara, @songmu, @sugyan) として参加して、全体通して1位のスコアで通過しました。

isucon.net

今回は ISUCON 1 の時の優勝チームを再結成という形になったわけですが、最初はISUCON 4の時と同じ社内のチームででようかと思ってたんですよね。ところが昨年優勝チームだった「LINE選抜 生ハム原木」が今回参戦できないということで、sugyanがチームどうしよう、と困っていたのでつい…*1

準備

今回はOSは Ubuntu(バージョン非公開)なのが事前にレギュレーションで公開されていたので(前年まではCentOS, Amazon LinuxなどのRedHatディストリビューションでした)、これはきっとWebアプリケーションの起動を systemd でやりたいんだろうなあと予測をして、Ubuntu 14.04LTS, 14.10, 15.04で最低限の初期設定とオペレーションができるように Chef cookbook を整備しておきました。

といっても、使いそうなパッケージをあらかじめ入れておくのと、各自のアカウント設定 (ユーザを作成、sudoできるようにする、githubの公開鍵をauthorized_keysに設置する) ぐらいです。kernelパラメータのチューニングなどは特に何もしていません。また、以下のツールも便利なので Chef でインストールするようにしておきました。

あとは作業用 Slack と github のプライベートリポジトリを用意ですね。

当日

メンバー全員の所属がばらばらなので、検討の結果、はてなさんの表参道オフィスの会議室を借りました。LINEさんのカフェも検討しましたが、おそらく参加者多数になるので落ち着かないのと、窓際のソファー席を確保できない場合椅子に不安が…ということで。

写真に写っているサブディスプレイは On-Lap のもので、現行品だと以下のようなやつになるでしょうか。持ち運びに便利なので ISUCON 本選でも活躍します。おすすめです。

13.3インチモバイル液晶モニター On-Lap 1303H

13.3インチモバイル液晶モニター On-Lap 1303H

やったこと

スコアの推移は以下のようになりました。

timestamp    score   
11:41:25    305 
12:03:33    0   FAIL: 
12:08:14    587 
12:18:27    1333       < indexを2個足した (fujiwara)
12:24:22    0   FAIL: 
12:27:22    1888       < my.cnf調整(fujiwara)
12:31:27    1672    
12:52:11    2008       < Gazelleに入れ替え (fujiwara)
13:47:38    2832       < entriesからtitleカラムを分離(sugyan, fujiwara)
13:49:24    2867    
13:52:20    5206       < entriesから1000件取ってるのをなくす(sugyan)
13:58:35    5500    
14:03:53    9353   < relationsから or を削った(songmu)
14:37:46    9593    
15:05:11    20  FAIL: 
15:08:00    10009   
15:15:17    10092   
15:25:29    10137   
15:30:49    0   FAIL: 
15:36:53    10138   
16:27:23    11247   < comments_for_meをredisのlistに(sugyan)
16:43:50    17  FAIL: 
16:48:45    17  FAIL: 
16:58:10    17  FAIL: 
17:04:20    17  FAIL: 
17:39:28    12389   < footprintをredisのsorted setに(songmu)
17:47:06    0   FAIL: 
18:01:09    17039   < userをmysqlではなくredisから読む(sugyan)、3回相手の属性調べてたのを一発に(songmu)
18:11:22    16402   
18:16:07    18193   < / でコメント10件毎回entriesを取得していたのをwhere inに(songmu)
18:23:54    26338   < redisから引いたuserをプロセスのメモリにcache(sugyan)
18:30:44    26153   < initializeでaofからredisを初期化(fujiwara)  
18:40:11    26694
18:42:59    27232   < 再起動試験後の最終提出スコア

基本的な作業の流れとしては

  • 自分がログ (主に 0.01 sec閾値で出力したMySQLのslow query logとalpでの集計結果) をみながら改善ポイントを指摘
  • songmu, sugyanがコードを読んで効率が悪いところを発見
  • 3人で対応方針を検討
  • songmu, sugyan両名がそれぞれ並列でコード修正
    • その間 fujiwara はインフラ周りの細かいこと(あんまりやることなかった)
  • 修正が終わったものをmergeしてベンチ

をひたすら繰り返していく、という感じでした。 途中 footprint の Redis 化の実装が難航して苦しい時間帯もありましたが、基本的には手戻りなしで一直線にスコアを上げていけたのでいい流れでしたね。

entriesからtitleカラムを分離

entries.bodyが巨大でtitleを表示するためだけに取得するのが重かったので、セオリー通りにカラムを分離しようとしました。ところが1.8GBあるentriesテーブルのALTERが、diskのスループットが全然でなくてなかなか終わらず。(1MB/secぐらいしかでてなかった)

結局snapshotからSSDインスタンスを用意してそっちで作業して戻す、ということをして乗り切りました。

alter table entries add title varchar(191) not null default '';
UPDATE entries SET title=SUBSTRING_INDEX(body, '\n', 1);

/initializeでのRedisデータ初期化

一部のデータはRedisに移す実装をしています。

ベンチ開始時に /initialize にアクセスが来て、そこで初期データ以外のデータは削除されるのですが、そのタイミングでRedisのデータも整合性を取って初期化する必要があります。

そこで普通にMySQLを読んでRedisに書き込みをすると30秒の時間制限を超えてしまうので、MySQLの初期データに対応するRedisの初期データセットを作った上で redis-cli config set appendoonly yes にしてaofファイルを吐き出しておき、initalize時にはそれを redis-cli で読み込んで一気にロードしました。2秒で終わります。

$redis->flushall();
system("/usr/bin/redis-cli --pipe < /home/isucon/appendonly.aof")
    if -e "/home/isucon/appendonly.aof";

感想

とにかく問題のボリュームが大きくてやることがいろいろあって、リモートベンチも快適に掛かって、tagomorisさんが「これがISUCONだ、という予選にしたい」といっていたのが実現できていて素晴らしい出題だったと思います。 運営の皆様、本当にありがとうございました。

本選でも3年ぶり3回目の優勝を勝ち取れるように頑張りたいと思いますので、よろしくお願いします!

*1:昨年のチームに不満があったとかでは全くないのであしからず……

YAPC::Asia 2015で発表してきました & ConsulとStretcherについて

YAPC::Asia 2015 でトークを採用していただいたので、発表してきました。

YAPC::Asiaは自分は2006年から10回皆勤で、トークは2009年LT、2010〜2013, 2015は本編で計6回もしてるんですね…YAPC::Asiaにはここまでのエンジニア人生の半分以上を支えてもらっていて、(ひとまず)最後の回でもトークできて感無量です。

1年ぶりにYAPCでしか顔を合わせない人もいた懇親会は、皆さん言うように同窓会のよう、というか本当の同窓会よりも現在の話題を共有している分濃密でしたね。

Consulと自作OSSを活用した100台規模のWebサービス運用

1日目午後一の激戦枠に放り込まれたのでどれぐらい会場が埋まるか心配でしたが、200人以上入る会場でほぼ満員だったようで、聞きに来ていただいた皆様ありがとうございます。

ここ1年程度で本番運用してきたConsulと周辺に関する知見を詰め込んだので、発表はだいぶ駆け足になってしましました。トーク内容についてもうちょっと詳しく知りたいみたいなことがありましたら、どこか勉強会でも飲み会でもお誘いいただいたらほいほい出向くと思います。

ConsulとStretcherについて

「Consulってこんなに一般化してたのか」的な感想をちらほら見かけるのですが、これはおそらく世間的には全然そんなことはないんじゃないですかね…今回の発表で Consul が出てきたのは自分が把握している限り以下のトークだと思うのですが、

  • @hsbt さんの発表(ペパボさんの事例)
  • @aereal さんの発表(はてなさんの事例、検証中)
  • @kenjiskywalker さんの発表
  • カヤックからの発表 × 2 (@fujiwara, @tkuchiki)

たしかにトーク数では 5/90 なのでだいぶ一般化した技術に見えそうですが、おそらくわりと狭い範囲で実験的に使われているのだけどたまたま通ったトークが多かったので目立ってた、というのが実際のところではないかな、という印象です。

ちゃんと使えるようになるとだいぶ便利なのですが、なにかおかしくなったらとりあえず再起動、的な運用をするとあっさり崩壊するので、慣れた人が導入したConsulを慣れてない人に運用を引き継いだりするとツラい目に遭う未来が見えます。老婆心ながら。

Stretcher については、だいぶ興味を持って頂いたかたが多かったようで、大変嬉しいです。ただ、おそらく最初に「Consul と連携する」という部分を(半ば意図的になのですが) 押し出してしまったせいか、StretcherにはConsulが必須のように思われてる節があるので一応説明しておきますと……

StretcherはConsulなしでも使えます!

「Consulと連携」というのは実際には、「consul watch が渡してくる標準入力からのJSONを当てにしている」ぐらいが正確なところで、標準入力から規定のフォーマットのJSONを流してやれば、最初のバージョンからConsulとは無関係にデプロイを行うことができました。

v0.1.0 からは Serf eventで実行されるイベント形式(単に標準入力に値が来る) に対応したので簡単に Serf でも動きますし、昨夜リリースした v0.1.2 ではデフォルトで (consul watch 以下で実行されない場合に) 標準入力から単に manifest URL を読み取るようになったので、sshでもなんでも、デプロイしたいホストで stretcher コマンドを実行さえできればそれで実行可能になっています。

$ echo s3://example.com/manifest.yml | stertcher

要するにこれが実行できればよい、ということですね。

ということで、台数は少ないし増減もないから Consul いれるのはちょっと……現状capでsshは全台にできるんだけど、というような環境でも簡単に stretcher を実行することができますので、是非お気軽にお試しいただければと思います。

Norikraでwebサービスを守る話をしてきた

Norikra meetup #2でLTをしてきました。LTといいつつ時間に余裕があったので15分以上しゃべっていたような…

atnd.org

発表資料はこちらです。

speakerdeck.com

Norikraで不正アクセスの兆候があるアクセスログを検知して、検知次第IPアドレスmemcachedに突っ込んでそれをもとにアクセスをブロックする、というネタでした。

ログの流し込みが詰まった場合に誤爆しないように、結果のtimestampに1分以上の間隔があった場合は max(time) - min(time) で補正するとか、クエリに後処理で使うための定数を埋め込んでおくことでクエリごとに挙動を調整しやすくするとか、そんなかんじの細かい工夫をしています。

あと皆さん気になっていたNorikraの冗長化ですが、active-standby構成であればすぐできる気はします。

ただし現状、落ちたとしてもすぐインスタンスをあげ直すだけでそれほど重大な問題にはならない使い方なので、普段遊んでるstandbyを用意するコストをかけてまでやるかどうかですね。

Amazon SQSを利用してS3からRedshiftにデータ投入するRinというツールを書いた

fluentdで集約したログをRedshiftに投入するのに、これまでは fluent-plugin-redshift を使っていたのですが、諸々の理由でこれを置き換えるツールをGoで書きました。

Rin - Redshift data Importer by SQS messaging.

プロダクション環境に投入して、2週間ほど快調に動作しているので記事を書いておきます。

アーキテクチャと特徴

S3にデータが保存されたタイミングで、Amazon SNS または SQS にメッセージを飛ばすイベント通知機能がありますので、それを利用しています。

  • (何者か) S3 にデータを保存する (fluent-plugin-s3, その他どんな手段でも可)
  • (S3) SQS に S3 の path 等が記述されたメッセージを通知する
  • (Rin) SQS のメッセージを受信し、Redshift へ COPY を発行して取り込みを行う

S3, SQSの設定をした上で以下のような config を用意し、rin -config config.yaml として起動しておくだけで動作します。

1プロセスで、複数の S3 path(bucket) に対応した Redshift の table (schema) への投入を扱えます。

Go 製なので、バイナリをダウンロードするだけで動作可能です。

queue_name: my_queue_name    # SQS queue name

credentials:
  aws_access_key_id: AAA
  aws_secret_access_key: SSS
  aws_region: ap-northeast-1

redshift:
  host: localhost
  port: 5439
  dbname: test
  user: test_user
  password: test_pass
  schema: public
s3:
  bucket: test.bucket.test
  region: ap-northeast-1
sql_option: "JSON 'auto' GZIP"       # COPY SQL option

# define import target mappings
targets:
  - redshift:
      table: foo
    s3:
      key_prefix: test/foo
  - redshift:
      schema: $1      # expand by key_regexp captured value.
      table: $2
    s3:
      key_regexp: test/([a-z]+)/([a-z]+)/

開発動機

fluent-plugin-redshift を利用していた間、以下のような問題がありました。

アップロード時に重い

fluentdのバッファとしてmsgpack形式で保存したものを、S3へのアップロード時に取り込み用のフォーマットに変換するという処理を行うため、fluentd の CPU を相当食います。それなりの流量のデータ(数千msgs/sec程度) を Redshift に投入しようとすると、fluentd は1プロセスでは複数CPUを有効に使えないため、複数プロセスに処理を分割する必要がありました。

Redshift のメンテナンス時に面倒

Redshiftのクラスタにノードを追加、削除する場合、クラスタリサイズ中にはデータ投入ができなくなります(読み取りは可能)。

その状態で fluent-plugin-redshift のデータ投入が走ると、S3へファイルをアップロードするところまでは成功した上、その後の COPY の発行でエラーになるため、fluentdの処理は「S3へのアップロード処理から」リトライされます。

リトライされるので最終的には問題なく取り込まれるのですが、S3には投入できなかったファイルが残ったままになり、投入できたファイルとできなかったファイルには部分的に同一のログが重複して含まれる状態になります。

エラーになって取り込まれなかったファイルをきちんと消しておかないと、後日まとめて再取り込みをしようとしたときに、ログを重複して読み込んでしまうことになります。

時々死ぬ

原因は結局特定できなかったのですが、plugin-redshiftの定義を多数記述すると数日〜数週間に一度程度の頻度で fluentd ごと処理が停止していました。こうなると kill -KILL しないと再起動もできなくなります。fluentdの優秀なバッファ機構のおかげで kill してもデータロストはないようですが、停止を検知 (ログが流れてこなくなる) して、強制再起動する仕組みを作ってだましだまし動かしていました。

Rin でうれしいこと

S3へのアップロードが軽くなる

fluent-plugin-s3 はアップロードする形式で直接バッファに保存し、そのまま(圧縮して)S3に投げるだけのため、バッファからの再構築でのCPU消費がありません。

Redshiftのメンテナンス時のリトライ処理が楽

S3に上げるところまでは Redshift とは無関係のため、S3 へアップロードされたものが部分的に重複することはありません。 Rin が Redshift へ投入できなかった場合には SQS のメッセージは削除せず、不可視期間が過ぎた後に再度実行します。リトライは SQS のメッセージで担保されます。

死ににくい

fluent-plugin-s3が原因でfluentdが刺さった経験は未だありません。

fluentd以外からのデータ投入も可能になる

S3, ELB, CloudFront など、S3 にログが保存されるサービスの Redshift への取り込みも統一的に扱うことができます。 (まだやってないけどできるはず…)

FAQ

Q1 "Redshift data Importer by SQS messaging" だったら Rin じゃなくて Ris なのでは?

A1 最初、SQS ではなく SNS 通知をトリガにして取り込むようにしようと名前を決めてコードをある程度書いた後に、リトライと実行時のレスポンスを考えると SQS のほうが……となった経緯があります。

Q2 Lambda でやったらよいのでは?

A2 本記事執筆時点、Tokyoリージョンには未だに Lambda が来ていません(もうすぐ来そうな予感がひしひしとしていますが)。 また、Lamba のリトライ処理は3分間隔で3回、とのことなので、リサイズ中には比較的長時間失敗し続けることを考えると不安があります。参考: S3、Kinesis/DynamoDB StreamsでのLambdaリトライ処理

Consul KVSをバックエンドにしたリアルタイムダッシュボード #monitoringcasual

最近悩んでいることを解決する小さいアプリケーションを書いたので、monitoring casual talks #7 で発表してきました。

モニカジは毎回全員発表で濃い話がいろいろできて楽しいですね!

Consul KV Dashboard // Speaker Deck

GitHub - fujiwara/consul-kv-dashboard: Consul KVS based dashboard web application.

概要はスライドにありますが、Consul KVS に保存された値をいい感じにまとめて(リアルタイム更新で)見せることのできる、Go + React.js でできた小さな Web application です。

ConsulのREST APIに値を送る(curlで十分)だけで、現在の各ホストで発生した値を画面でリアルタイムに更新しつつ閲覧できます。

f:id:sfujiwara:20150130212541p:plain

Consul自身が持っているブロッキングクエリというlong pollなリクエストの仕組みを使っているので、本体はだいぶシンプルな感じで実装できました。まだUI部分の挙動が微妙だったりパーマリンクがなかったりしますが、Consulを使っている方はお試しいただければ嬉しいです。

GoでZabbixと通信する、もしくはオレオレZabbix Server/AgentをGoで実装する方法

全国一千万Zabbixユーザの皆様こんにちは。

複数のZabbix Agentから取得した値を集約する zabbix-aggregate-agent や zabbix_get コマンドの Go 実装版 go-zabbix-get を書いて遊んでいるうちに、Go で Zabbix と通信するライブラリが育ってきてしまったので一通りまとめておきます。

"github.com/fujiwara/go-zabbix-get/zabbix" を import して使います。

import "github.com/fujiwara/go-zabbix-get/zabbix"

Zabbix Agentから値を取得する

アイテムでいうところの「Zabbixエージェント」型、ServerやProxyからAgentに対してTCP接続をして値を取得するタイプです。

value, err := zabbix.Get("example.com:10050", "keyname", 5*time.Second)

通信相手のAegentのアドレス、取得したいアイテムのkey名、タイムアウト時間を渡すだけです。簡単ですね。value は string です。

Zabbix Server/Proxy に値を送信する

アイテムでいうところの「Zabbixトラッパー」型、ServerやProxyに対して(Agent以外の何者かが)値を送信するタイプです。Fluentdのプラグインfluent-plugin-zabbix もこの形式ですね。

resp, err := zabbix.Send(
	"example.com:10051",
	zabbix.TrapperData{Host: "localhost", Key: "foo", Value: "bar"},
	5 * time.Second,       
)

送信データを zabbix.TrapperData 型として作成して渡してあげるだけです。これも簡単ですね。

これは何が嬉しいのかというと、自作の daemon 類に何か異常があった場合に Zabbix Serever に直接 trap を送りつけるような機能が実装できます。アラートの即応性が上がりますね。

複数データをまとめて送りつける SendBulk() もあります。

res, err := zabbix.SendBulk(
	"example.com:10051",
	zabbix.TrapperRequest{
		Data: []zabbix.TrapperData{
			zabbix.TrapperData{Host: "localhost", Key: "foo", Value: "bar"},
			zabbix.TrapperData{Host: "localhost", Key: "xxx", Value: "yyy"},
		},
	},
	timeout,
)

オレオレZabbix Agentを実装する

ここまでは既存の Agent や Server に対する通信ですが、自分自身で Agent や Server(Trapper) を実装することも可能です。

err := zabbix.RunAgent("0.0.0.0:10050", func(key string) (string, error) {
	switch key {
	case "agent.ping":
		return "1", nil
	// ...
	default:
		return "", fmt.Errorf("not supported")
	}
})

RunAgent() に func(key string) (string, error) な関数を callback として渡してやることで、TCPサーバとなり Server や zabbix_get コマンドに値を返す daemon を実装できます。

自作の daemon が持っている情報を、直接 Zabbix Server から通信して取得するような機能が実装できます。値の取得のために、別途 zabbix-agent から UserParameter で外部コマンドを起動するような必要がなくなります。

オレオレZabbix Server(Trapper)を実装する

zabbix_sender (zabbix.Send()) により送信された値を受信するサーバも簡単に実装可能です。

err := zabbix.RunTrapper("0.0.0.0:10051", func(req zabbix.TrapperRequest) (res zabbix.TrapperResponse, err error) {
	for _, data := range req.Data {
		log.Println(data)
	}
	res.Proceeded = len(req.Data)
	return res, nil
})

これは使いどころがいまいち難しいのですが、fluent-plugin-zabbixのテスト には便利に使っています。

ここまで道具が揃ったら、Go で Zabbix Server 互換実装を作り上げることも夢ではないですね!(やりませんけど…)

全く誰得プロダクトだと思いますが、お楽しみください。

ISUCON4本選で3位に敗れました #isucon

ISUCON4 に「fujiwara組」として参戦しましたが、既報のとおり 3位に敗れてきました。順位こそ3位で賞金10万円は獲得できたものの、スコアが示すとおり内容的には完敗です。

まずは主催のLINE社様、出題を担当していただいたCookpad社様、本番サーバ提供をしていただいたテコラス社様にお礼申し上げます。本当に楽しいイベントをありがとうございました。

うちのチームとしてやったことは #isucon 4の本戦で3位を取ってきました (追記あり) - beatsync.net に大変詳しいので、そちらをご参照ください。

簡単に最終的な構成をまとめると

  • Redisは1号機に(動画以外)集約
  • 動画はアップロードを受けたサーバがローカルファイルとして保存しnginxが返す。保存されたサーバのアドレスをメタデータとしてRedisに保存し、APIへのレスポンスに含まれるURLを構築するのに使用する
    • そのため、動画ファイル自体の自ホスト間転送はない
  • リクエストは3台で問題なく受けられるが、ベンチマーカーのアクセスパターンの癖(?)か、先に指定したアドレスのほうに帯域が1.5倍ぐらい偏るような挙動が見られたので、最終的には1号機をシングル構成で動かす
  • ただしeth0, eth1のアドレス両方をベンチマーカーに指定することで1Gbpsのインターフェースを2個使ってスループットを出す

という構成でした。

ローカルアドレスを指定するのって(そもそもベンチマーカー相手に指定できるのって)どうなの、という話はあるのですが、出題者側の目論見としてなんらかのロードバランサーやCDN的なキャッシュサーバが前段にいるということであれば、それらからのアクセスがローカルアドレスでも受けられる可能性はないではない、ということで……うーん……

敗着

今回の敗着はこれに尽きます。33万点のスコアを「チームフリー素材」が出した時点で何か秘孔がある可能性は思い浮かんだのですが、そこを突き詰めずに忘れようとしたところが、最後まで考え続けた優勝チームの「生ハム原木」に完全に及ばなかったところです。

途中の構成では2号機3号機でそれぞれファイルを保存するものの、自ホストにないファイルがリクエストされた場合は nginx の try_files を使って相手ホストに reverse proxy することでファイルを見つける構成になっていました。

これが確か15時ぐらいでしたが、その際に毎回 proxy で相手から取得するのは無駄だから proxy_cache を入れよう、と思ってサーバ上の nginx.conf に設定をコピペまでしていました。

が、ここでベンチ実行待ちの間に念のため自らのホスト間の通信速度を iperf で測定したところ、(おそらく同一物理マシン内のVMのため) 32Gbps という計測結果が得られてしまい…… これだとメモリの少ないVMで proxy_cache のためにファイルIOを行うよりも、ホスト間で通信する方がコストが少なかろう、と判断して結局 proxy_cache を一度もベンチしないで外してしまったという経緯がありました。

そこで1回でも、ローカルベンチでも走らせていたら流れが変わっていた可能性があり、全く悔やみきれないのですが後の祭りですね。

もしくは自分のホスト間の帯域も1Gbpsに制約されていたら、確実に通信よりはファイルIOを選択していたわけで、そこが現実にはあり得ない(同一筐体にあることを想定して構成するわけにはいかない) VM配置だったのがある意味罠として作用してしまった感があります。

本選問題の感想など

おそらく出題の意図としては、もっと早い時点で Cache-Control が効くことを発見するチームがでて、そこからが本当の闘いになるというような目論見だったのかなあと勝手に思っています。

が、競技時間が1時間延長されてなお最後の30分まで(自覚的に)発見したチームがなかった、というのが想定外だったのかなと。

帯域ネックな勝負ではない、ということになれば当然ベンチマーカーの1Gbpsな帯域によってリモートベンチ結果が不安定になることもないでしょうし、そこでベンチマーカーの並列数を2にする判断が遅くなったということもあるんでしょうね。

参加者は基本的にはブラックボックスであるリモートからのベンチマークのみを頼りにチューニングする必要があるので、そこがもう少し安定していたらな、というのは正直な感想なのですが、そのあたりは今後 tagomoris さんが開催するであろう ISUCON benchmarkers casual talks で存分に(自分も出題経験者として) 話せたらなあと思います。

Cache-Controlヘッダの付与なんて、いつも飽きるほどやっていることなのに、それが ISUCON 当日にできないところが競技の難しさだなあと毎回思います。

最後に

自分も来年40歳になりますし、今回3回目の優勝を成し遂げた上で引退して勝ち逃げする目標を立てていたのですが、そうもいかなくなりました。また来年、リベンジできたらなと思います。

主催と共催の皆様、参戦者の皆様、本当にお疲れ様でした。今年も楽しかったですね!