PostgreSQLのレプリカでSELECTしたときにクエリがキャンセルされるまでの実験

RDSの設定変更をしたあと以下のエラーが出るようになってしまいました。

過去に何回か出てるのですが、とりあえず時間のかかるクエリが悪いから短くすればいいんだな、くらいの理解でした。
恥ずかしながら、ちゃんと理解できておらず、このくらいの認識になっていたので、いい機会だから調べてみました。

```
ERROR:  canceling statement due to conflict with recovery
DETAIL:  User was holding shared buffer pin for too long.
```

## エラーの原因を推測する

裏側の仕組みは既にまとめてくださっている方がいました。

https://qiita.com/sawada_masahiko/items/16a30b2df52331c86483

記事を追ってみると重要そうに太字で書いてありました。

> スレーブでの参照とスレーブが参照しているタプルを物理削除するWAL(VACUUM等で出力される)の適用がコンフリクトします。

このコンフリクトが起きた後、max_standby_streaming_delay秒後にキャンセルされるようです。

......と、なんとなく想像できるのですが、具体的に何すればコンフリクトするの？というところが分かりませんでした。

このままだと結局分かった気になってるだけ、という感覚です。
ということで、もっと分かった気になるために実験をしてみようと思います(今回の趣旨)

# 事前準備

terraformで一撃実験環境(過去にクラスターの実験で使ってたものを流用しています)

サブネットやセキュリティグループは調整してください。

コンソールから作っても問題ありません。

* クラスター
* マスター用インスタンス
* レプリカ用インスタンス
* サブネットグループ
* クラスターパラメータグループ
* パラメータグループ

```hcl:main.tf
provider "aws" { region = "ap-northeast-1" }

resource "aws_rds_cluster_instance" "db01" {
  engine = "aurora-postgresql"
  engine_version = "9.6.8"
  cluster_identifier = "daijinadb"
  identifier         = "db01"
  instance_class     = "db.r4.large"
  db_subnet_group_name    = "${aws_db_subnet_group.daijinadb_subnet.name}"
  db_parameter_group_name = "${aws_db_parameter_group.daijinadb_parameter.name}"

  publicly_accessible = false
}

resource "aws_rds_cluster_instance" "db02" {
  engine = "aurora-postgresql"
  engine_version = "9.6.8"
  cluster_identifier = "daijinadb"
  identifier         = "db01"
  instance_class     = "db.r4.large"
  db_subnet_group_name    = "${aws_db_subnet_group.daijinadb_subnet.name}"
  db_parameter_group_name = "${aws_db_parameter_group.daijinadb_parameter.name}"
  publicly_accessible = false
}

resource "aws_rds_cluster" "daijinadb" {
  cluster_identifier = "daijinadb"
  engine = "aurora-postgresql"
  engine_version = "9.6.8"
  database_name = "totemodaijinadb"
  master_username = "postgres"
  master_password = "hogehoge"
  skip_final_snapshot = "true"
  backup_retention_period = 1
  port = 5432
  db_cluster_parameter_group_name = "${aws_rds_cluster_parameter_group.daijinadb_parameter.name}"
  vpc_security_group_ids    = "securitygroup-iikanjinihenkoushitene"
  db_subnet_group_name = "${aws_db_subnet_group.daijinadb_subnet.name}"
}

resource "aws_rds_cluster_parameter_group" "daijina_parameter" {
  name        = "daijina_parameter"
  family      = "aurora-postgresql9.6"
  description = "aurora-postgresql9.6"

  parameter {
    name = "timezone"
    value = "JST-9"
  }
}

resource "aws_db_parameter_group" "daijina_parameter" {
  name   = "daijina_parameter"
  family = "aurora-postgresql9.6"
  description = "totemo daiji"

  parameter {
    name = "log_min_duration_statement"
    value = "1000"
  }

  parameter {
    name = "log_statement"
    value = "ddl"
  }
}

resource "aws_db_subnet_group" "daijinadb_subnet" {
  name       = "daijinadb_subnet"
  description = "totemo daijina subnet"
  subnet_ids = [
    "subnet-iikanjinihenkoushitene-a",
    "subnet-iikanjinihenkoushitene-c"
  ]
}
```

## 仮説

実験用のインスタンスができたので実験前にどうすればキャンセルされるのか仮説を立ててみます。
建てた仮説が以下です。

|順番 | マスター  |   レプリカ |
|:---|:--------|:--------|
|1   | UPDATEで不要タプルを作成 | |
|2   | | 処理に時間のかかるSELECT |
|3   | VACUUMで不要タプルの物理削除を実行 | |
|4   | WALを生成 | |
|5   | | WALを適用 |
|6   | | 30秒後にSELECTがキャンセルされる |

UPDATEすると論理削除扱いになって、VACUUMしたタイミングで物理削除されるので、まずUPDATEしています。

次に、この状態で重いSELECTを実行します。これがキャンセルされれば今回の実験は成功です。

今度は論理削除扱いになっている行をVACUUMで物理削除します。

4,5は表面上確認できないので想像ですが、
マスター側でVACUUMをかければ、
レプリカ側で論理削除扱いになっているものも消す必要があるので、
そのためのWALを生成し、それを適用しようとするはずです。

最後にSELECTの結果を待っている状態でmax_standby_streaming_delay秒待ちます。

## 実験

Auroraは読み書きができるエンドポイント、読み込みのみのエンドポイントがそれぞれ用意され、
それぞれマスター用インスタンス、レプリカ用インスタンスにアクセスされます。
以下のSQLの実行インスタンスはそのように読み替えていただければ幸いです。

* [マスター] 下準備(重いクエリを手軽に実行するための準備)

正直、空のインスタンスに大量のデータ入れなくちゃいけなくて実験無理じゃない？と思っていたのですが、
簡単に重いSELECTを実現できる方法が紹介されていたのでこれを利用しました。

https://qiita.com/tt2004d/items/3983234eee51dc036bb0

```
create table test (item1 int);
insert into test values(1);
insert into test values(2);
insert into test values(3);
insert into test values(4);
insert into test values(5);
insert into test values(6);
insert into test values(7);
insert into test values(8);
insert into test values(9);
insert into test values(10);
```

* 1. [マスター] Updateして不要タプルを作成

```
update test set item1 = '1' where item1 = '2';
SELECT relname, n_live_tup, n_dead_tup FROM pg_stat_user_tables; ※vacuumにより物理削除されるタプルがあるか確認
```

* 2. [レプリカ] 処理に時間のかかるSELECT

```
select count(*) from test a , test b , test c , test d , test e, test f, test g, test h, test i, test j, test k;
```

* 3. [マスター] VACUUMで不要タプルの物理削除を実行

```
VACUUM test;
```

* 4.
* 5.

これらはPostgreSQLの仕事

* 6. [レプリカ] 30秒後にSELECTがキャンセルされる

```
ERROR:  canceling statement due to conflict with recovery
DETAIL:  User was holding shared buffer pin for too long.
```

出た！

## まとめ

* 不要タプルがある状態になっている
* 処理に時間のかかるSELECTをかける
* VACUUMをかける

以上の条件で、キャンセルされる可能性があるということが分かりました。

PostgreSQLの仕組みを完全に知らない以上、このエラーが出る他のケースが網羅されているとは限りませんが、一つの原因はこれなんだなと、わかった気になれました。
おそらく網羅されたケースは[公式文書のこれ](https://www.postgresql.jp/document/9.6/html/hot-standby.html#hot-standby-conflict)だと思いますが、文章読んでもいまいちつかみきれないのでいずれ実験してみようかなと思います。