【2日目】Go言語の学習

構造体

Goには Rubyにおける「クラス」が存在しない。ただクラスと同じような、関連する情報をまとめた「構造体」と呼ばれるものが存在する。

type User struct {
    age int
    name string
}


構造体の初期化。

    u := User{} // 空の User構造体
    u.age = 20
    u.name = "seijin"


一行で。フィールド名は省略可

u := User{age: 20, name: "seijin"} // =>u := User{20, "seijin"}


フィールド名を省略する場合、一部のフィールドのみ値を渡すことはできない。

u := User{20)

# command-line-arguments
./Main.go:20:15: too few values in User literal


フィールド名を明示する場合は、一部のフィールドのみでも可能。

u := User{age: 20)

メソッド

構造体は、構造体内にメソッドを定義することができる。

package main
import "fmt"

type User struct {
    age int
    name string
}

func (u *User) setInfo() {
    u.age = 20
    u.name = "seijin"
}

func main() {
    u := User{}
    u.setInfo()
    fmt.Println(u.age, u.name)
}

// => 20 seijin


Goでは利便性のため、メソッド呼び出し時に、暗黙手にレシーバ変換が行われる。上のポインタレシーバの場合は、以下の通り。

u.setInfo() // (&u).setInfo()


値レシーバにしても同様に変換される。

func (u User) setInfo() { // 値レシーバ
}

func main() {
    u := new(User) // uは *User型
    u.setInfo() // (*u).setInfo()
}

【1日目】Go言語の学習

変数のスコープ

main関数の中で宣言した score変数は、main関数内でのみ使用できる。変数が使える範囲をスコープと呼ぶ。

package main

func main() {
    score := 1
    // スコープ内なので使える
    println(score)
}

func sub() {
    // スコープ外なので使えない
    score += 1
}

■ 実行結果

# command-line-arguments
./Main.go:12:5: undefined: score

値のコピー

score変数を main関数から sub関数に渡し、値を更新しているように見えるが、main関数側では score変数の値は変わらない。

package main

func main() {
    score := 1
    sub(score)
    println(score) //=>1
}

func sub(score int) {
    score += 1
    println(score) //=>2
}

■ 実行結果

2
1


実際には、sub関数にはコピーされた変数が渡されているだけで、main関数の変数とは別のもの。変数のアドレス (Goではポインタ)を出力してみると、値が違うことがわかる。

package main

func main() {
    score := 1
    sub(score)
    println(&score) //=>0xc00002c748
}

func sub(score int) {
    score += 1
    println(&score) //=>0xc00002c740
}

■ 実行結果

0xc00002c740
0xc00002c748

ポインタ渡し

sub関数で socre変数の値を更新するには、ポインタを渡せば良い。

package main

func main() {
    score := 1
    sub(&score)
    println(score) //=>2
}

func sub(score *int) {
    *score += 1
}

■ 実行結果

2


main関数から sub関数に対してポインタを渡す。

sub(&score)


sub関数では、①ポインタ変数を定義してポインタを受け取り、②ポインタを使って値を更新。

func sub(score *int) { -①
    *score += 1 -②
}

補足

関数の引数における値とポインタの使い分けについて、公式ドキュメントの Should I define methods on values or pointers? によると、

  • ここまででも述べたように、引数を更新する場合は「ポインタ」
  • 引数のサイズが大きい場合は「ポインタ」
  • 逆に、引数のサイズが小さいのであれば「値」

Memorystore for Redisのメンテナンス

はじめに

先日、Memorystore for Redisのメンテナンスが実施されました。一時的にアプリケーションから Redisへの接続ができていなかったのですが、事前にメンテナンスの通知が来ていなかったり、そもそも Redis (Memorystore for Redisも含めて) についてあまり詳しくなかったので調べてみました。

Memorystore for Redis

Memorystore for Redisは GCPのフルマネージド型の Redisサービスです。

Memorystore for Redis の概要


Memorystore for Redisの Redisインスタンスには、「基本階層」と「標準階層」の2種類があります。基本階層はレプリケーションなしで、障害やメンテナンス時にデータの保持が担保されません。したがって、本番環境では標準階層を利用しています。標準階層の詳細については以降触れていきます。

Redis の階層の機能

メンテナンスについて

メンテナンス ポリシー によると、

  • 四半期に一度の頻度で行われる
  • 実施可能時期を示す時間枠は指定できない

とのこと。通知については書かれていませんでしたので、後日サポートに問い合わせてみます。

Cloud SQLと違って時間枠が決められないので、せめて事前に通知は欲しいなと思います。

高可用性

Memorystore for Redisの標準階層インスタンスは、レプリケーションと自動フェイルオーバー機能によって高可用性を実現しています。

レプリケーション

  • Redisは自動的にプライマリノードとレプリカノードで構成される
  • プライマリとレプリカは異なるゾーンに配置され、ゾーン障害に対応している
  • 単一のノードで障害が発生しても、もう片方にデータがバックアップされるためデータの損失が起きない

フェイルオーバー

  • プライマリノードで障害が発生したときに、自動でフェイルオーバーがトリガーされる
  • フェイルオーバー中は、新しいプライマリへの接続はレプリカにリダイレクトされ、アプリケーションへの応答を継続する
  • 既存の接続は遮断されるが、再接続時にはアプリケーションが接続情報 (IPなど)を変更することなく自動的に新しいプライマリにリダイレクトされる
  • レプリカをプライマリに昇格させる間は、Redisは一時的に利用できなくなる


一時的にアプリケーションから Redisに接続できなかった理由はこれっぽいですね。

  • レプリカをプライマリに昇格させる間は、Redisは一時的に利用できなくなる

フェイルオーバーの情報を確認する

Cloud Monitoringの Metrics Explorerで稼働時間を確認します。

f:id:yd0:20200723221541p:plain


グラフの見方

  • 下落している箇所は再起動を示している
  • データが欠損している箇所は使用不可であったことを示している

フェイルオーバーの流れ

  • まずレプリカが再起動する
  • レプリカとプライマリの両方が接続不可になる (おそらくレプリカがプライマリに昇格する間)
  • レプリカがプライマリに昇格し、同時にプライマリが再起動する
  • プライマリがレプリカに降格


グラフのデータが欠損している時間帯と、アプリケーションから Redisに接続できていない時間帯とがおおよそ一致していることがわかりました。

infoコマンド

redisの状態や設定を確認するのに infoコマンドを使用しました。

redis-cli info -h <IP> info


このように値が返ってきます (一部抜粋)。詳細は INFO – Redis で確認できます。

# Server
redis_mode:standalone

# Replication
role:master
connected_slaves:1

# Cluster
cluster_enabled:0

考察

Memorystore for Redisについて調べましたが、ポイントとしては 四半期に一度、事前通知なしでダウンタイムが発生するメンテナンスが行われる ことです。

フルマネージド型のサービスを利用しているので、サービス側の都合でセキュリティパッチやアップデートを適用するメンテナンスが行われるのは仕方ないことだと思います。ただ一時的でもアプリケーションに影響が出るので、何かしらの対応はしておきたいです。

例えば、

  • Redisへの接続のタイムアウト値の設定/見直し
  • 接続できなかった場合に自動で再接続

など。まだ Redisについて理解できていないことが多いので、上記が可能かどうかもわかりません。今後の課題です。

あと Cloud Monitoringでの Redisの稼働チェック、アラート設定などです。今回はアプリケーション側で検知できましたが、メンテナンスが行われたことに気づかない可能性もあったので対応しておきたいです。

Redisを運用している他社さんの記事などを参考に、ベストプラクティスを探していければと思います。

AWSを触ってみた

はじめに

AWSで構築されているシステムの運用・保守を任されたのは良いのだけれども、これまで AWSを触ったことがなかったので調べました。以前から AWSを触ってみたかったので、とても良い機会です。

システムの構成図

今回はシステムの一部分である SFTP機能について書きます。構成図は以下の通りです。システムの概要については、ユーザが WinSCPFilezillaなどのツールを用いて S3のリソースにアクセスしにくるので、事前に登録したユーザ情報を基に認証を行います。


f:id:yd0:20200719224014p:plain

各サービスについて

  • Transfer for SFTP

    • SFTPを使用し、S3との間でファイル転送ができるフルマネージド型のサービス
    • フルマネージド型なのでインフラストラクチャの運用、管理は不要
  • API Gateway

    • APIの作成から配布、保守、監視、保護が行えるフルマネージド型のサービス
    • トラフィック管理や APIのバージョン管理など、APIコールの受け入れと処理に伴うすべてのタスクを取り扱う
  • Lamdba

    • サーバなしでプログラムが実行できるサービス (≒ Google Cloud Functions)
  • Secrets Manager

    • アプリケーションやリソースなどの認証情報を、簡単にローテーション、管理、取得できるサービス
  • S3

    • ストレージサービス (≒ Google Cloud Storage)


使用しているサービスには GCPにも同様のサービスが存在したりするので、役割であったり仕組みであったりが直感的に理解できました。

わからなかったこと、疑問に思ったことなど

なぜ Secrets Managerを使うのか?

  • Transfer for SFTPには 2つの認証方法がある

    • サービス内にユーザ IDを作成するサービスマネージド型認証
    • 任意の IDプロバイダーを統合できるカスタム認証
  • 前者であればユーザ認証はサービス内で完結するのに、なぜ後者を選んで Secrets Managerを使用するのか?

  • 理由は以下の 2つ

    • ユーザは外部の IDを持っていたこと
    • SSHキーではなくパスワードでの認証が必要であったこと
  • サービスマネージド型はパスワード認証がサポートされていないので、カスタム認証を採用する必要があった

  • 参考

GCPで同じシステムを作るには?

  • GCPには Transfer for SFTPのようなフルマネージド型のサービスは存在しない (はず)

  • SFTPを使用して Cloud Storageとの間でファイル転送するには?

    • Compute Engine + Cloud Storage FUSE + Cloud Storageの構成
    • Compute Engine VMではデフォルトで SFTP接続が可能
    • Cloud Storage FUSEで、Compute Engine VM上のディレクトリに Cloud Storageをマウント
  • GCPではフルマネージドの AWSとはちがって Compute Engine VMの運用、管理が必要

  • 参考

さいごに

はじめて AWSに触れましたが、普段 GCPを使っていることでクラウドやインフラストラクチャに対する理解や考察が深まっている気がします。また複数のパブリッククラウドサービスを使ってみることで、各サービスの特徴なども見れていいですね。

AWSはドキュメントが豊富かつわかりやすくて助かります。GCPはわかりにくい (気がする)。

次は、システムのネットワーク部分について記事を書こうと思います。

Compute Engineインスタンスの Autoscaling

はじめに

私が関わっているサービスでは、非同期処理をアプリケーションのサーバとは分けて別のサーバで行っています。詳細には、アプリケーションサーバから Cloud Memorystore (Redis)にジョブを登録し、非同期処理サーバの Sidekiqがジョブを受けとって処理する構成です (実際にはアプリケーションサーバ冗長化しています)。


f:id:yd0:20200712190229p:plain


非同期処理サーバの利用状況は以下の通りです。

  • 稼働は 24/7
  • 一日のうち、実際に処理が行われているのは 2~3時間程度
  • 処理中、すべてのインスタンスが使われていないことがある
  • 逆に、すべてのインスタンスが CPU使用率 100%に達していることもある


利用状況からリソースの利用効率が悪く、また無駄な費用が発生していることがわかります。 Cloudの特性を活かして 必要なときに必要な分だけ利用する にはどうすれば良いのか調べました。

結論としては、Managed instance groups (MIGs) でインスタンスの Autoscalingを用います。

MIGsの Autoscalingについて

Autoscaling groups of instances

検証

実際に MIGsの Autoscalingを試してみました。

検証の内容/手順

  • インスタンス テンプレートを作成する
  • MIGs を作成する
  • 負荷をかけてスケーリングさせる

インスタンス テンプレートを作成する

Creating instance templates

今回は GCPのコンソールからぽちぽちしました。gcloudや APIでも可能です。

ナビゲーションメニューから Compute Engine > インスタンス テンプレート > インスタンス テンプレート作成 に移動し、インスタンスの設定を行います。


f:id:yd0:20200712152102p:plain

MIGs を作成する

インスタンス テンプレートを作成すると、インスタンス テンプレートの詳細ページに「インスタンスグループを作成する」があるのでそちらに移動します。

今回のポイントとなる設定は以下の通りです。

  • 自動スケーリングモード:
    • 自動スケーリング
  • 自動スケーリング指標:
    • CPU使用率 60% (デフォルト)
  • クールダウン期間:
    • 60秒
  • インスタンスの最小数と最大数
    • 最小数: 1
    • 最大数: 2


f:id:yd0:20200712152640p:plain


クールダウン期間については、インスタンスの起動にかかる時間ちょうどにするのが良いのですが、今回は検証なのでデフォルトの 60秒にしました。

インスタンス グループの作成が完了すると、インスタンス グループの中に設定した最小値の 1つのインスタンスが立ち上がりました。


f:id:yd0:20200712153535p:plain

負荷をかけてスケーリングさせる

準備ができたので、CPU使用率を上げてスケーリングされるか確認します。

インスタンスグループのインスタンスに入り、stressコマンドで CPU使用率を意図的に上げてみます。

$ stress -c 1 -t 300


インスタンスグループの平均 CPU使用率が、設定した 50%前後になるようにインスタンスが追加されます。ただし、今回はインスタンス数の最大値が2なのでそれ以上は増えません。


f:id:yd0:20200712154135p:plain


300秒経過して stressコマンドが終了すると、CPU使用率が 50%を切るため、追加されたインスタンスが削除されていました。

スケールアウトされるのに少しラグがあったのですが、それは Stabilization period という「直近 10分間のピーク時の負荷を基にスケーリングする」仕組みがあったからのようです。

考察・感想

MIGsの Autoscalingを使えば、Cloudの特性である必要なときに必要な分だけリソースを利用することができそうです。 ただ少し残念な点としては、インスタンスグループにおいて、最低でも 1つのインスタンスは稼働していなければならないことです (Cloud Runならその点は解決できそう)

次のステップとしては、Autoscalingで追加されたインスタンスでどのようにアプリケーションを構築 (設定)するかです。Cloud Monitoringでの監視設定や、ログの出力先など、いろいろと考えることが多そうです。。が、楽しくなってきました。

GCSのバケット費用を十分の一に削減した話

はじめに

先週、費用削減のために GCS (Google Cloud Storage) のバケットの整理を行いました。

これまでは gsutilコマンドで GCSにデータをアップロードしたり、バケット間でデータを移動させたりなど、簡単な操作しかしたことがありませんでした。今回はストレージクラスやバケットのライフサイクル機能など学ぶことが多かったのでまとめます。

技術的なトピックは以下の通りです。

  • gsutilコマンドの (ちょっとした)罠
  • Cloud Monitoringによるバケットサイズの確認
  • ストレージクラス Archiveについて
  • バケットのライフサイクル機能

目次

経緯

毎月の GCP (Google Cloud Platform) の費用がプロジェクトの予算をオーバーしているため、費用削減のために GCSの整理をすることにしました。

その際に、用途不明のバケット (以降、”unknownバケット”と呼びます )が見つかりました。数年前に使用されていたようなのですが、当時の管理者は不在で、用途が不明のまま放置されていたようです。運良く、関係者を見つけて情報を得ることができました。

unknownバケットについて

  • サービスから参照されることはない
  • 過去の作業で使用されたデータのバックアップとして存在している
  • 5TB以上あるかもしれない
  • 今後も残しておきたい

現在は使用されていないものの、今後もバケット内のデータは残しておきたいとのことでした。 したがって、できる限り費用を抑えてバケットを管理していく ことにしました。

今回は以下の流れで対応していきました。

  • unknownバケットのサイズを確認
  • かかっている費用を算出
  • 適切なストレージクラスを調査
  • 料金の比較
  • 適切なストレージクラスに移行

unknownバケットのサイズを確認

gsutilの duコマンドでバケットのサイズを計測しようとするとエラーが発生しました。どうやらフォルダ名にスペースが入っていることが原因のようです。

$ gsutil -m du -sh gs://unknown/space folder
CommandException: "du" command does not support "file://" URLs. Did you mean to use a gs:// URL?


スペースの入ったフォルダ名の部分をダブルクォーテーションで囲えばエラーを回避することができるのですが、バケットの中にそのようなフォルダがたくさんあり、かつ階層的に存在していたためかなり手間暇がかかりそうでした (フォルダ名にスペースを入れるないでほしい...)。

$ gsutil -m du -sh gs://unknown/"space folder"


諦めてスクリプトを書くしかないのかなと悩んでいたときに、Cloud Monitoringでバケットのサイズを見れることを知りました。

GCPのメニューから オペレーション > Monitoring > ダッシュボード > Cloud Storage で GCSのメトリクスを確認することができ、バケットのサイズ (Object Size)を見ることできます。とても便利ですね!

バケットの中のフォルダ単位ではサイズが見れないようなので、そこは gsutilコマンドで確認するしかないんですかね。

unknownバケットのサイズは約 5TBということがわかりました。

かかっている費用を算出

unknownバケットの詳細は以下の通りです。

  • サイズ - 約 5TB
  • ストレージクラス - Standard
  • リージョン - asia-northeast1 (東京)

Cloud Storage の料金 によると、unknownバケットには毎月約 1万2,600円 の費用がかかっていることがわかりました。‬年間にすると15万円以上ですから、サービスから参照されないただのバックアップにしては費用がかかりすぎです。

f:id:yd0:20200628154730p:plain

適切なストレージクラスを調査

unknownバケットの利用状況は以下の通りでしたから、ストレージクラスは Archive が適切だと判断しました。

  • サービスから参照されることはない
  • 過去のとある作業で使用されたデータのバックアップとして存在している


Archiveクラスの特徴としては以下の通りです。

  • 1年に1回未満しかアクセスしないデータに最適
  • 最小保存期間が 365日
  • データアクセスと操作に関する料金が、ストレージクラスの中で最も高い
  • 可用性の SLA (Service Lavel Agreement) がない
  • 全ストレージクラスで最安値

サービスから参照されずバックアップとして存在している unknownバケットなので、アクセス頻度や最小保存期間、アクセス/操作料金の高さ、可用性 SLAが無いことは問題ないでしょう (ちなみに、可用性は Nearlineと Coldlineと同じ 99.9%です)。そして何より、全ストレージクラスの中で最安値ですから、できるだけ費用を抑えたい今回のケースにおいては最も適切なストレージクラスといえます。

料金の比較

Archiveクラスに変更した場合、unknownバケットの毎月の費用は約 1,400円 です。現在の Standardでは約 1万2,600円なので、ほぼ十分の一まで費用を削減することが可能です!

適切なストレージクラスに移行

費用をほぼ十分の一に抑えられることを関係者にアピールし、ストレージクラスの変更許可をいただきました。さっそく実施します。

ストレージクラスを変更する方法は2つあります。

今回はライフサイクル機能を使用することにしました。理由としては、unknownバケットのスペースが入っているフォルダでエラーが起きるかもしれないということと、今後ストレージの管理をしていく際に、自動でストレージクラスを変更してくれるライフサイクルは必須になると思ったので、試してみたかったからです。

ライフサイクル機能を使用する

今回のライフサイクルのルール設定は以下の通りです。

  • トリガーは年齢とし、1日以上経過されたオブジェクトが対象
  • アクションはストレージクラスを Archiveに変更

f:id:yd0:20200628024040p:plain


unknownバケットにおける既存のオブジェクトをすべて Archiveクラスに変更できればいいので、トリガーはてきとうに年齢、設定値は1日としました。今思いましたが、ストレージクラスをトリガーにした方が良かったかもしれませんね。まあ結果としては変わりません。

オペレーション費用について

ちなみに、ストレージクラスを変更するのに料金が発生します。 各クラスへのオペレーションの分類 によると、ライフサイクルでのストレージクラスの変更はクラスAオペレーションに該当します。また ライフサイクルの操作 によれば、Archiveクラスへの変更は、ArchiveクラスのクラスAオペレーションの料金で計算されます。

Archiveクラスへのアクセス/操作については料金が高いので注意しなければいけません。またオペレーションの料金はオブジェクト数で決まるので、サイズと同様に Cloud Monitoringで確認が必要です。

バケットのストレージクラスの変更

ライフサイクル機能はオブジェクトに対しての操作なので、unknownバケットのストレージクラスは Standardのままです。今後、新規にデータが追加されることはないのですが、いちおうバケットのストレージクラスも Archiveに変更しておきました。

f:id:yd0:20200628160547p:plain

以上で、ストレージクラスの移行は完了です。

さいごに

費用削減のため GCSのバケットの整理を行いました。適切にストレージ管理を行うにはストレージの知識だけでなく、扱うデータの用途や特徴を理解するべきですね。

ストレージのデータ量は日々増えています。今のままでは時間の経過とともに費用は高くなっていくので、今回学んだことを活かして上手く管理していければと思います。

クラウドストレージっておもしろい。