Screaming Loud

日々是精進

Goで重い処理をtimeoutさせる ~その2~

前回はGoでtimeoutさせる処理に関して書きました。

yuutookun.hatenablog.com

しかし、前述の記事で書いているのはレスポンスが返らない場合でした。 多くの場合、レスポンスが必要だと思うので、レスポンスを付与するパターンを紹介します。

functionの返り値が値とエラーの二つになっていますが、ここは type result を変更すれば増やすことができます。 ただ、レスポンスをstructにしておけば、このメソッドで事足りると思います。

流れとしては、

  1. 引数の関数の結果格納用のチャネルを作成
  2. goroutineで引数の関数を実行。結果はチャネルに。
  3. for ループのselectでcontextを監視して
  4. チャネルに結果が入れば、結果を返す
  5. context.Doneになれば、空の結果を返す

という感じです。 responseのresultは汎用性のために interface{} で定義しています。

関数終了前にcloseしたりすると、内部でpanicが発生してしまうので、チャネルのcloseに関しては goroutineの終了と一緒にcloseしてます。

以下実際のコードになります

var FailedGetChannel = errors.New("failed to get Channel")

type result struct {
    value interface{}
    err   error
}

func main() {
    f := func() (interface{}, error) {
        time.Sleep(10 * time.Second)
        return []int{1, 2, 3}, nil
    }
    ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
    defer cancel()
    inter, err := ExecWithContext(ctx, f)
    if inter == nil { // interがnullだとcastでpanicになるため
        fmt.Println(err)
        return
    }
    fmt.Println(inter.([]int), err)
}

// ExecWithContext f()の結果をcontextキャンセルされるまで待つ
func ExecWithContext(ctx context.Context, f func() (interface{}, error)) (interface{}, error) {
    resultCh := make(chan result) // f()の実行結果を入れるresult チャネル

    go func() {
        defer close(resultCh)
        resultCh <- func() result {
            i, e := f()
            return result{value: i, err: e}
        }()
    }()
    return waitResult(ctx, resultCh)
}

// contextがtimeoutになるまでchannelの結果が返るのを待つ
func waitResult(ctx context.Context, ch chan result) (interface{}, error) {
    var i result
    for {
        select {
        case <-ctx.Done():
            return i.value, ctx.Err()
        case i, ok := <-ch:
            if !ok {
                return nil, FailedGetChannel
            }
            return i.value, i.err
        default:
        }
        time.Sleep(1 * time.Millisecond)
    }
}

実際のplaygroundは以下です。

Go Playground - The Go Programming Language

追記

ライブラリとして公開しました

github.com

Goで重い処理をtimeoutさせる

Goで重い処理を書いているとタイムアウトさせたいときがあると思います。

大抵のIOが発生するライブラリだとcontextを引数に加えると、context の終了通知が発生して終了してくれます。

例えば、以下のようにhttp requestであれば、contextにタイムアウト設定することでhttp requestがタイムアウトになるとリクエストをキャンセルしてくれます。

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://www.google.com", bytes.NewReader([]byte("")))
resp, _ := r.client.Do(req)

しかし、ライブラリやその他IOによってはちゃんとcontextの終了が実装されていない場合があります。 通常であれば、contextを渡すだけなはずが終了できない場合、自分でその機構を作ってしまうのも手です。

以下のようにキャンセルさせる関数を作ってみました。

func main(){
       // 重い関数
    heavyFunc := func() {
        time.Sleep(10 * time.Second)
    }
    if err := execWithTimeout(context.Background(), heavyFunc, 2*time.Second); err != nil {
        fmt.Println(err)
    }
}

// ctx 親context
// fn 実行させる重い関数
// timeout timeoutさせる時間
// この関数を通して実行すると、timeoutさせることができる。
// 注意するのは、goroutineで投げっぱなしになるので、一時的に重い処理ではなく常時重い処理の場合はどんどんリークしていくので使うべきではない
func execWithTimeout(ctx context.Context, fn func(), timeout time.Duration) error {
    timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
    defer cancel()


    go func() {
        fn()
        fmt.Println("finish heavy")
        cancel()
    }()
    return ticker(timeoutCtx)
}

// 入力されたcontextが終了するかどうかを監視する関数
func ticker(ctx context.Context) error {
    for {
        time.Sleep(10 * time.Millisecond)
        select {
        case <-ctx.Done():
            fmt.Println("Stop ticker")
            if ctx.Err() == context.Canceled {
                return nil
            }
            return ctx.Err()
        default:
        }
    }
}

以下にplay groundを貼っておきます

The Go Playground

既存のEKSクラスタにFargate for EKS でAPI作成する際の導入でハマったところ

既存のEKSクラスタにterraformでFargateのPodを導入する際に、ハマったところを紹介します。

TL;DR

クラスタセキュリティグループ の設定を見直しましょう。

f:id:yuutookun:20210110003437p:plain
クラスタセキュリティグループ

mapRoleへの付与漏れ

Farage導入においては以下の記事がわかりやすいのですが、書いてあるとおりfargate profileを生成すると自動でmapRoleに pod execution roleが入ります。

tech.recruit-mp.co.jp

しかし、terraformで導入していてmapRoleもterraformで管理していると、

  1. terraformでfargate profileを作成
  2. aws側がmapRoleにpod execution roleの権限を追加
  3. terraformに記載されているmapRoleで上書き

となってしまい、mapRoleが消えてしまいます。

この事象が発生しているかは、 $ kubectl get eventUnauthorized 的な文言が出ているとmapRoleの付与漏れです。

fargate Podが起動しない

Podは作成されるけれど、以下のようなログを吐きfargateのノードが立ち上がらない問題が発生しました。 $ kubectl describe pod xxx のEventsで吐かれていました。

 Warning  FailedScheduling  55m   fargate-scheduler  Pod provisioning timed out (will retry) for pod

AWSのサポートに問い合わせてコントロールプレーン側のログを見てもらっても何も出ていませんでした。

結果としては、クラスタセキュリティグループのアウトバウンドがなぜか消えていたからでした。

fargateのノードはクラスタセキュリティグループがノードのセキュリティグループとして評価されるので、これのインバウンドとアウトバウンドはしっかり設定する必要があります。

EKS生成時に作られるクラスタセキュリティグループに対しては最低でも以下を許可する必要があります terraformで記載します

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    self        = true
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

クラスタセキュリティグループ

上の節でも紹介しましたが、fargateはクラスタセキュリティグループがノード(Pod)のセキュリティグループとして評価されます。 なので、ALBなどからつなぐにはクラスタセキュリティグループに対してALBのセキュリティグループを許可する必要があります。

EKSのネットワーク設定にある追加のセキュリティグループに設定されていても、そこはfargateでは評価されないので注意が必要です。

他にもクラスタ上の別のノードと通信するためにはfargateのクラスタセキュリティグループを許可させるために付与する必要があります。

kube-systemなど各ノードに散らばりがちなpodに対して通信するためには、クラスタセキュリティグループは全ノードに付与しておいたほうがおすすめです。

またfargateのpodを運用する場合、色々なセキュリティグループを随時許可する必要が出ると思うので、 terraformで運用する場合はクラスタセキュリティグループを terraform import しておいたほうがいいでしょう。

まとめ

fargateでのハマりどころは、ほぼ クラスタセキュリティグループ と言っていいかもしれません。 そこさえ気をつければ、他のハマりどころは少ないと思います。

could not query provider registry for registry.terraform.io でterraformのproviderがダウンロードできない問題の対処

現象

localのMacでterraform initした際に以下の様なエラーが発生して providerがダウンロードできなくなりました。

Error: Failed to query available provider packages
Could not retrieve the list of available versions for provider hashicorp/aws:
could not query provider registry for registry.terraform.io/hashicorp/aws: the
request failed after 2 attempts, please try again later: Get
"https://registry.terraform.io/v1/providers/hashicorp/aws/versions": net/http:
request canceled while waiting for connection (Client.Timeout exceeded while
awaiting headers)

これ別にaws providerに限った話ではなく、 datadogとかrandomとか全てで発生します

initは通常 .terraform 配下のキャッシュを見ているので required_provider にマッチしていれば、ダウンロード自体が発生しないのでproviderアップデートのときですね

環境は以下です

  • macOS Catalina
  • tfenvによるterraform管理
  • terraform v0.13.2

providerの指定はこんな感じ

 required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 3.0"
    }
    datadog = {
      source  = "datadog/datadog"
      version = "= 2.12.1"
    }
  }

原因

ここに書いてある通りです。

github.com

/etc/resolve.conf に存在しないnameserverを書いている場合、cgoを許可しないとDNS解決がGoでうまくいかないっぽいです。

解決法

以下のどちらかを実施すれば直ります

  • ネットワーク設定から存在しないDNSサーバのIPを消す
  • terraformをソースからビルドする。その際 $ CGO_ENABLED=1 go build -ldflags "-s -w" -o tf でcgoを許可してビルドする

でした。 変なところでハマってしまいました。