Datadog SLO 101 を読んだ

Datadog のブログに、SLOに関するシリーズがある。今日は一つ目のブログを読んだ。

www.datadoghq.com

以下、大雑把な意訳と感想。

GoogleのSRE本で紹介されたSLO(service level objectives)は、それを利用することでプロダクト開発と運用タスクのバランスをとって、最終的にはプロダクトのユーザ体験を高めることができる、というもの。

このブログでは、SLI, SLO, SLA,エラーバジェットの説明、SLOは誰にとって重要なのか、SLO設定に当たってとりあげるべきSLIについて説明があった。

用語の説明

Service Level Indicators (SLIs)

ユーザに提供しているサービスのレベルを測るメトリクス。例えば可用性、レイテンシースループットなど。

Service Level Objectives (SLOs)

目標サービスレベル。SLIによって計測される。

Service Level Agreements (SLAs)

提供されるサービスのレベルに関して、サービスの提供者とサービスの利用者の間で交わされる契約、合意。 これを下回った場合は料金の減額などが行われる。

Error bugets

直訳するとエラー予算。SLOを守るにあたって許容されるエラーの累積値の上限。 エラーバジェットが残っている間は新規開発などが許されるが、それを下回った場合は不具合の修正や設計の見直しが求められる。

SLOは誰にとって重要なのか?

エンドユーザ

サービスの品質に対するエンドユーザの期待値は高い。ただ100%の信頼性は無理。

SLOを設定することで、製品開発(ユーザに新しい価値を提供するが、何かを壊す可能性もある)と、信頼性(ユーザの幸福度をキープする)のバランスをとることができる。

製品開発エンジニア(dev)、運用エンジニア(ops)

歴史的に製品開発エンジニアと運用エンジニアが分かれていて、それぞれの目標が利益相反していた。 大雑把にいうと、製品開発エンジニアは新しい機能を追加することに責任をもつが、運用エンジニアは出来上がったものの信頼性に責任を持つ。

SLOとエラーバジェットを設定することで信用性に関して同じ責任感を持つことができるようになる。

エラーバジェットが残っているうちは、製品開発者エンジニアは新しい機能をリリースできるし、運用エンジニアは長期的な改善プロジェクトを進めることができる。 エラーバジェットが残り少なくなったら、製品開発者エンジニアは手を止めて信頼性の向上のため、運用エンジニアと共に直近の改善活動をする必要がある。

簡単にいうとエラーバジェットは、製品開発エンジニアと運用エンジニアの仕事を調整するための数値化手法ですよ、という話。

SLIからSLOへ

良いSLIを選ぼう

すべてのSLIが良いSLIではない。 ユーザ体験に直接影響する指標に絞りましょう。 例えば買い物サイトにおいて、サーバのCPU使用率が高くても、ユーザの買い物がスムースにできていれば問題ない。この例ではCPU使用率は良くないSLIと言える。

ユーザに近い位置で計測することを勧める。

SLIの選択には、GoogleのSRE本が参考になる。 https://sre.google/workbook/implementing-slos/#slis-for-different-types-of-services

SLIをSLOにする

SLOは例えば、30日間で99%のリクエストのレイテンシーが250ms以下である、といったものになる。

SLOを設定するにあたっては以下の注意事項がある。

  • 現実的になろう。100%のSLOは無理。
  • 最初から正解のSLOは出せないので運用しながらよくしていきましょう。
  • 複雑にしすぎないように。ユーザ体験に直結するものに絞りましょう。

SLO設定にあたってのチェックリストをDatadogが用意している。 https://www.datadoghq.com/pdf/SLOChecklist_200619.pdf

感想

SLOやエラーバジェットを設定するのはすぐにできそうだが、それをうまく運用するにはチームメンバーとの協力が必要。 設定値に納得感がないといけないし、下回った場合にどうするか事前に決めておく必要がある。

Transactional Outbox パターンをSpring Bootで実装する

Transactional Outboxという、Databaseへの保存とメッセージの送信をアトミックにすることができる実装パターンがあります。

参照

例えば決済処理において、決済完了したことをDBに保存する処理と、決済完了メッセージを送信する処理がアトミックではない場合、下のように困ったことが起きてしまいます。

  • 保存はできたがメッセージの送信に失敗した場合、後続の処理が進まない

  • 保存に失敗したのに、決済完了メッセージだけ送信してしまった場合、後続の処理が誤って進んでしまう

特に、サーバサイドのアプリケーションがマイクロサービス化されていて、サービス間のメッセージ送受信の確実性がシステムの信頼性に直結する場合、 Transactional Outboxパターンはよく使われるパターンかと思います。

実装

このパターンをSpring Boot で実装する方法を紹介します。

言語は Kotlin を使っています。

受注完了時に送信するメッセージを OrderEvent とします。

class OrderEvent(
   val orderId: String,
   ....
)

OrderEventのpublish

受注完了時にOrderEventorg.springframework.context.ApplicationEventPublisher を使って publishします。

@Service
class OrderService(
  private val publisher: ApplicationEventPublisher
) {
  
   ....

  @Transactional
  fun order() {
    ....
    
    val event: OrderEvent = ...

    publisher.publish(event)
  }
}

OrderEventの永続化

publishしたOrderEventorg.springframework.context.event.EventListener アノテーションをつけたメソッドで受けてDBへ永続化します。このときのDBトランザクションは、上記の OrderService.order メソッドで開始しているDBトランザクションと同じものになります。

@Component
class OrderEventPersistentSubscriber {

    @EventListener
    fun processSubscriptionEvent(event: OrderEvent) {
       ... // 永続化処理
    }
}

OrderEventの送信

受注処理が完了してDBトランザクションがCOMMITされた後、OrderEvent を外部のメッセージブローカーに渡します。このときorg.springframework.scheduling.annotation.Async をつけて非同期に送信するとOrderService.order メソッドの処理をブロックしないので効率的です。

@Component
class OrderEventRelay {

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    fun relayOrderEventEvent(event: OrderEvent) {
        .... // OrderEventを外部のメッセージブローカーへ送信する処理
    }
}

OrderEventの再送

メッセージブローカーがダウンしているなどの理由で上記の送信処理が失敗することがあります。 それに備えて、送信されていない OrderEvent を定期的に見つけて再送する必要があります。

例えば、org.springframework.scheduling.annotation.Scheduled を使って、永続化した OrderEvent のうち送信完了していないものを定期的に選択して再送します。

@Component
class  OrderEventRecoveryTask {

    @Scheduled(...)
    fun recovery() {
      .... // 再送処理
    }
}

参考

上記で利用したSpringのクラスの設定方法や利用方法は、下記を参考にして下さい。

ApplicationEventPublisher, @EventListener

Spring bootを使っていればデフォルトで利用可能だったと思います。使い方は下記を参照してください。

Spring Framework - ApplicationEventPublisher Examples

@Async

@EnableAsync で有効にしつつ、Executorの設定が必要です。下記を参照してください。

@Async アノテーションで非同期メソッドの作成

@Scheduled

@EnableScheduling で有効にする必要があります。下記を参照してください。

Scheduling Tasks

Gradleのscanオプションについて

仕事ではgradleを使ったJavaアプリケーションの開発をしている。

その割には gradle についてちゃんと勉強したことがなかったので、Gradle公式のユーザガイド Building Kotlin Applications Sample を読んでいた。

この中で scan オプションが紹介されていた。

 % ./gradlew build --scan

BUILD SUCCESSFUL in 8s
8 actionable tasks: 3 executed, 5 up-to-date

Publishing a build scan to scans.gradle.com requires accepting the Gradle Terms of Service defined at https://gradle.com/terms-of-service.
Do you accept these terms? [yes, no] yes

Gradle Terms of Service accepted.

Publishing build scan...
https://gradle.com/s/wrk56****

途中で Do you accept these terms と聞かれるので同意すると、scanの結果がgradleのwebサーバへアップロードされる。

最後に表示されたURL https://gradle.com/s/wrk56**** を開くと、メールアドレス認証があり、それを通過すると、アップロードしたscan結果を綺麗なレイアウトで見るとこができた。

f:id:nshmuras:20210418123339p:plain

Enterprise版にすればもっと便利な機能もあるらしい。 f:id:nshmuras:20210418124432p:plain

セキュリティ的な懸念は少しあるけどビルド速度の改善には使えるかもしれない。

調べてみると2017年あたりにはあった機能なのね。