839の日記

業務とは関係ない趣味の話を書くブログです。

NewSQLのTiDBをローカルに立ててみる

34. NewSQLとは w/ tzkb | fukabori.fm を聞いてNewSQLをローカルで試してみたいなと思って調べてみた。

紹介されてたのはSpanner以外で3つほどあったけど、よく触る機会があるMySQLに互換があるTiDBを選択。 最初に Quick Start Guide for the TiDB Database Platform | PingCAP Docs を見ながら構築しようとしていたけど、ちょっと試してすぐ消したいのでローカルに色々入れるのは避けたかった。 ローカルのk8sにデプロイして確認し終わったらクラスタリセットするような感じで試せないかと調べてみているとhelm chartが提供されていることを発見。

Deploy TiDB in the Minikube Cluster | PingCAP Docs のあたり*1を見ると、ドキュメントに記載されているのはkind/GKE/minikubeで、手元で試したかったけどkindやminikubeを入れるのが面倒だったのでdocker-desktopクラスタにデプロイを行ってみた。 一部ドキュメントが古そうな雰囲気*2のところだけ手元で微修正しつつデプロイ、実行したコマンドは非常にシンプル。 ちなみにhelmのバージョンはv3.2.4を使った。

kubectl create ns tidb-admin
helm repo add pingcap https://charts.pingcap.org/
kubectl apply -f https://raw.githubusercontent.com/pingcap/tidb-operator/master/manifests/crd.yaml
helm upgrade tidb-operator pingcap/tidb-operator -i --wait -n tidb-admin
helm upgrade tidb-cluster pingcap/tidb-cluster -i --wait -n tidb-admin \
  --set schedulerName=tidb-scheduler,pd.storageClassName=hostpath,tikv.storageClassName=hostpath,pd.replicas=1,tikv.replicas=1,tidb.replicas=1,monitor.create=true,monitor.grafana.create=true

ただ最後にtidb-clusterをデプロイしたときにエラーが出た。

Error: UPGRADE FAILED: template: tidb-cluster/templates/monitor-secret.yaml:1:7: executing "tidb-cluster/templates/monitor-secret.yaml" at <(.Values.monitor.create) and (.Values.monitor.grafana.create)>: can't give argument to non-function .Values.monitor.create

https://github.com/pingcap/tidb-operator のchartファイルを見るとmonitor-secret.yamlでifの書き方が誤っているような気がしたので*3、手元にcloneしてそこを修正。 修正したchartで試すとデプロイができるようになった。

ドキュメントとの差異は

  • tidb-operator chartをデプロイするとtidb-schedulerが入っていたのでschedulerNameを指定
  • docker-desktopを使っているのでstorageClassNameをhostpathに変更

schedulerNameはドキュメントに記載されている通り、default-schedulerにしておいても特に問題はなかった。

デプロイ後に手元で軽く遊んでみた。

# http://localhost:2379/dashboard でDashboardが見れる
kubectl port-forward svc/tidb-cluster-pd 2379
# http://localhost:3000 でGrafanaが見える
kubectl port-forward svc/tidb-cluster-grafana 3000
# mysql -h 127.0.0.1 -P 4000 -u root -D test でTiDBに繋げる
kubectl port-forward -n tidb-admin svc/tidb-cluster-tidb 4000

デプロイ時にmonitorを有効化しているとPrometheus/Grafanaがデプロイされていて、最初からいい感じのダッシュボードを提供してくれている。 それとは別にTiDBが独自に作ったダッシュボードがあった。 MySQL Clientで繋ぐのも何もトラブルがなく普通にSQLが叩けた。

f:id:husq:20200705165024p:plain
Dashboardめっちゃ充実してる

少しchartに修正を入れたとはいえ、大きなプロダクトが環境汚染もなくローカルですっと動くのは感動する。 もともとオンプレのデータセンターを運用している会社とかでWriteがスケールするDBが欲しくなったケースなどにはすごい有効そうなプロダクトだなと感じた。 あとはOSSなので何か問題にあたったときにissueを見て周りに同じ現象の人がいるかすぐ確認できたり、実装を見てPRを投げたりできるのも良さそう。

EKSやGKEでproductionとして使う場合のterraformファイルなども提供されており、Cloudで使い始めるのもかなり楽そうな印象があった。 実際運用を始めるとバージョンアップや高負荷時のよくわからない挙動に悩まされるといったことはきっとあるんだろうけど、すごいクオリティが高いなと感じた。 利用者が増えてくるとバグは叩かれていくので本当にマネージドサービスを使う感覚でTiDBを選択肢に入れられる未来もあるのかもしれない。

この手の巨大プロジェクトは手元にいろいろな依存物を入れながら環境を作り、遊び終わっても手元の依存物が残り続けるみたいなことがありがちだけど、 k8sとhelmがあればサクッと試せてサクッとResetできるような世界観なのも体感がすごい良かった。

*1:This is for testing only. DO NOT USE in production!と書かれているのであくまで軽く試してみる場合の手順

*2:なんとなくhelm2前提感がある

*3:とりあえずissueは立てた

じぶん ReleaseNote v1.3

2020年、4月版です。

  • action-slack v3をリリース
    • v2と比べて柔軟に設定を行えるようにした
    • ドキュメントの配信をnetlifyで行うようにした
  • ミドルウェアの更新
    • go/ruby/gem/node/npm/k8sなどなど...
    • 使ってるGitHub Actionsのバージョンも全体的に最新に更新
  • ゲームのスクショ共有サービスの作成
    • クライアント証明書を挟んだ自分専用サービス
    • gRPC+Goで既存のマイクロサービス基盤にサービスを追加
    • フロントはReactでgrpc-webを使っている
  • Actionsの並列化
    • testやlintは並列化することで全体的に速く終わるように修正
  • golangci-lintの除去
    • 1.14系にしてから確率的にtimeoutが発生するようになったので除去
    • 他に困ってる人いないんだろうか…と思っている
  • SSL証明書の期限が意図せず着れていた問題の修正
    • helmのchartをv3にしたときにingressの記法が古いままになっている箇所があった
    • そのためcert-managerで作った証明書が読み込まれていなかった
    • v3の記法に修正して正しく動くことを確認
  • isucon9の素振り
  • dotfilesの整理
    • MBP2018 15インチからmac miniに環境移行したのでその際にdotfilesの整理を行った

この他に、既存に作っていたRailsのサービスを趣味でGo+gRPCに書き直したりしていました。 書き直してて思うのですが、やはり個人である程度大きなサービスを作ろうと思ったときにGo+gRPCは実装量が多くてしんどいですね…。 サーバのパフォーマンスやすごく大きなサービスになる見込みがないならコスパは圧倒的にRailsがいいなぁと思います。

isucon9予選の本番環境を模してAWSで素振りをした

今年はコロナの影響かisuconの案内は出ていないが、isuconっぽいことがしたい気分だったので素振りをしていた。 最初はローカル環境で構築していたが、どうせなら本番環境と同じ条件で素振りしたいよなーと思い始めてAWS環境に同等の環境を準備することにした。

環境構築

今回利用したインスタンスのタイプは「ecs.sn1ne.large」という vCPU数2個、メモリ4GBというスペックで、ストレージには「Ultra クラウドディスク」を使いました。ecs.sn1ne.largeはCPU非共有型のインスタンスで安定した性能が確保されています。インターネット側の通信は最大 100Mbps、VPC内の通信は1Gbpsまで通信ができる環境となります。ubuntu 18.04 LTSをベースに問題のソースコード、データベース(MySQL)、リバースプロキシ(nginx)、アプリケーションサーバ(Go)が起動するOSイメージを参加者に共有し、インスタンスはこのイメージから起動する形としました。

ISUCON9 予選問題の解説と講評 : ISUCON公式Blog

と記載されていたので、AWS側であっていそうなインスタンスとしてc5.largeを選択することにした。 2コア4GBな点は一緒だけど、帯域幅は全体的にちょっと高めになってしまっている。 あまり帯域がネックになる問題ではなかった気もするのでこの辺の差異は目をつむることにした。 ディスクはgp2 40GBを利用した。

特殊なところに関しては後述するけど、おおまかな手順としては

  1. インスタンス上でUbuntu 18.04イメージを使ったisucon9のアプリ環境の初期設定環境をansibleで構築
  2. インスタンス上でUbuntu 18.04イメージを使ったisucon9のベンチマーカー環境をansibleで構築
  3. SSL証明書を設定(webapp/shipment service/payment serviceの3つ)し、nginxのドメインSSL証明書と同じドメインに変更する
  4. EIPを2つ取得してwebapp/benchmarkerのインスタンスに紐付けた上でDNSの設定を行う
  5. ベンチマーカー環境でisucon9の初期設定状態で正しくベンチが取れるか確認
  6. アプリ環境とベンチマーカー環境のAMI作成

までを初期状態のセットとした。

基本的にはREADMEと公式のローカル環境でISUCON9予選の問題を動かすを参考にして構築した。

環境構築は各インスタンスで必要なデータセットの準備を行ってからansibleでlocalhostに対してprovisioningを行うようにした。 ansibleの操作に対してあまり詳しくなかったので vagrant-isucon/Vagrantfile at master · matsuu/vagrant-isucon · GitHub を参考にしてprovisioningを行った。

ansibleの構築が終わった段階ではSSL証明書が期限切れ状態になっているので、これを正規のものに置き換える手動オペレーションを挟んだ。 自分が持っているドメインで適当に *.isucon9.hoge.com みたいなワイルドカード証明書をLet's EncryptのDNS認証で作成し、それをwebapp/benchmarkerのインスタンスに配置した。 ドメインに対してIPを固定したかったのでLBとなるwebappと外部サービスを動かすbenchmarker用に2つのEIPを取得しておいた。 本来であればshipment/payment/benchmarkerはそれぞれ単独のインスタンスにしたほうが良かったかもしれないけど、面倒だったので1つのインスタンスにまとめた。

  • webapp.isucon9.hoge.com -> EIP1
  • shipment.isucon9.hoge.com -> EIP2
  • payment.isucon9.hoge.com -> EIP2

この段階で上記のDNSのAレコードを設定しておいた。

細かいけど、payment/shipmentサービスをprovisioningするときにserviceが起動するようになっており、systemctl stop shipment とかでサービスを止めておく必要がある。 shipment/paymentサービスはbenchmarkerを立ち上げるときに自動で一緒に立ち上がるようになっていたので今回はそれに乗っかった。*1 ここまで来るとbenchmarkerで https://webapp.isucon9.hoge.com を対象にして実行するとベンチマークが通るようになっているはず。 初期スコアがgoアプリでだいたい2300程度で、isucon9予選の本番環境と同じぐらいのスコアだったことを調べ、だいたい同じぐらいになっていると判断した。

この後はAMIを焼いて、webappを3台にしたりbenchmarkerのインスタンスをc5.xlargeに変えたりすれば予選環境を本番ライクに楽しめる環境が構築できた。

素振り結果

この問題はRubyで過去に解いたことがあったのだけど、この1年間は割とGoを触っていたので今回はGo言語で挑戦してみることにした。 上記の環境構築も含めて大体8時間ぐらいやって最高スコアが17340だったので運が良ければ本戦に出られるのかもしれない*2感触を得られたので良かった。

一応解き直すまではisucon9の記事を読んでトップ層の知見を思い出さないようには気をつけていた。*3 方針としてはかなり愚直な改善だけやっていたので、積み重ねればスコアが出るのだなと体験できたのが良かった。

主にやったなーと覚えているのはこの辺。

  • MySQL/redisサーバなどのミドルウェアを3台のサーバに分散
  • アプリのリクエストを3台のサーバに分散
  • nginxサーバで静的コンテンツのキャッシュ設定
  • N+1の改善(ひたすらwhere in句に直す)
  • 外部リクエストの並列化
  • 無駄な外部リクエストのカット
  • Categoryのメモリ化
  • OR句をUNIONに直してインデックスが効くようにする
  • MySQLのコネクション数調整

やり終わった後に一通り記事を読んでみてリクエストの分散化はちょっとほかとやり方が違うなと感じた。 ログインサーバを分散させる人が多かったように思えるけど、自分の場合はほぼすべてのエンドポイントを3サーバに分散させていた。 ただし、ファイルをローカルに書き出す/読み込むAPIはnginxの完全一致で1サーバにルーティングされるように設定していた。 ファイルのローカル書き出しをredisの中に入れて3サーバで共有化するというのもちょっと考えたけど、8時間の枠に収まらなさそうなのでやめた。 最終的にホストの役割は以下の感じにした。

  • webapp-1
    • LB担当
    • app全般担当
      • uploadを操作する系のパスはここに集約
  • webapp-2
    • appのupload以外担当
    • MySQL担当
  • webapp-3
    • appのupload以外担当
    • redis担当

割と9000点ぐらいをさまよっていたけど、transaction_evidencesテーブルのN+1を直したあたりで15000点ぐらいまでぐわっと伸びてここそんなに効くのかーって思ったのが印象的。 あとは外部リクエストに無駄なものがあるという覚えがあったので(これは去年の記憶を持っていたのでチート)、今回を機に仕様をちゃんと読んでみたら、確かにカットできる箇所があったのでカットしたりしていた。

Campaignに関してはずーっと0でやっていて、1にするとどうしてもログイン部分とかが詰まり始めてしまい、このあたりは結局解消することができなかった。 後で記事を読み返すとbcrypt重すぎ問題に関してはみんな苦戦してるっぽく、自分も分散化する程度でしか解決はできなかった。 あとはどうしても外部サービスが応答しなくなってしまったりして、このあたりでリトライ処理を入れたりしたけどあんまり本質的な改善はなかった。 どういう風に解決するのが正道だったんだろうか…。

今回かなり間抜けなことをしてしまったのがコネクション数の変更で、MySQLのクエリでコネクション数を変更していたのだが、ベンチマーク前に各サービスをrestartさせていた際に初期のコネクション数に戻っていることに気づくまでに時間がかかってしまった…。 confの方で設定しないとrestartで戻るということも知らなかったので、これは勉強になった。 本数を増やしてもすぐにtoo many connectionsが出てしまう問題に苦しめられていて、 mysql -e "show global status like 'max_used_connections';" で何本使ってるんだ…って見てみたら151(初期値)になっていたことでようやく気づいた問題だった。

感想

今まで結構Rubyを書いていたけど、Rubyに詳しいというよりは自分はRailsに詳しいだけなので、Sinatraベースの問題だとあんまりRubyの経験が活かせないなとGoをやって思った。 Goで書くときはisuconのように自分でいっぱいロジックを書いて組み合わせて使うことが多いので、isuconのような問題と直面したときも細かい工夫みたいなのを入れるのに慣れているような感覚がある。 あとGoLandで問題を解いていたので、やっぱり関数ジャンプとか補完の機能が便利すぎるな…とは思った。 コンパイル通ってる時点でtypoみたいなアホなミスはない、というのも心強い。

Goでisuconやるならこういうフローでデプロイしないとなーみたいな練習が本番環境ベースで練習できたのでAWSに環境構築して素振りしたのは色々と知見になった。 今年はなんとなくisuconはなさそうな気がしているけど、来年からまた復活してくれたりするならぜひ出たいと思っている。

*1:service経由で動かしていく方式が正道なのかもしれないけど

*2:今回のスコア的には出れるけど、当然2回目なので1回でこのスコアを出さなければいけないし、本番でちゃんと出し切ることは難易度がかなりが上がるということを理解している

*3:とはいえ読んだことがある記事もまぁまぁある