PostgreSQLにおけるトークンバケットレート制限の楽観的または悲観的なロッキング
26258 ワード
このシリーズの中で、私はAPI呼び出し率制限のためにトークンを得る機能を定義しました.私はそれを実行している
以下はドライバのインストール方法ですnewer releases ):
私は、AとRatLimeRitodクラスを作成します
コンストラクタ
The
PostgreSQL READ COMMITTED different デフォルトでコミットされた分離レベルを使用します 呼び出しでバックエンドpidを連結することによって、異なるユーザーのための呼び出しトークン 私は、スループットが一定であることを確実にするために、これを夜に走らせました:
AWKスクリプトはトークンを獲得し、実際のレートを表示し、成功した呼び出しの割合とリトライ回数を表示します.
毎秒約1000オートコミット更新
私の50スレッドからこれらの単一行の呼び出しを行うと、平均15のデータベースでは、主にWalwriteで待機しています.
数字は、私が表示するものに一致します:1秒あたり100トランザクションコミット、1000タプルフェッチと1000行が更新されます.しかしながら、私はアクセス経路に依存するそれらの統計を表示することの関連性についての若干の疑問を持っています.例えば、私は1000 TuHelpを取得しました.
WALを書くのが主なボトルネックになっているので、競合はありません.PostgreSQLは、単一のカウンタ+ timestamp updateでさえ、多くのWALを生成します.なぜなら、全てのタプルが挿入されているため、以前のバージョンとしてマークされた古いもの、インデックス更新、および完全なページログを持つすべてのものです.これはHAのマルチAZ設定なし.
PostgreSQLシリアル化可能な異なる
PostgreSQL read readと同じです.
エードリアンドーザ
@ adriandozsa
いいえ、右または間違って、ちょうど別のユースケース.テナントでも高い並列性を持つSaaSサービスのレート制限を考えていました.
01 : 05 AM - 2022年1月5日
ここではレートが減少している.
PostgreSQLシリアル化可能です.
リードされた悲観的なロッキングで、50のセッション待っている
ところで、AWKスクリプトは隠しますが、リトライはSQL状態40001として上がります.
覚えておくべきことは、このワークロードでは、既定のread commitまたはserializableは同じ行に競合することなく良いパフォーマンスを持つことです.Retryコマンドが存在しないため、再試行が行われていないため、コミットされた読み込みが簡単になると思います.更新プログラムを挿入するたびに再試行する必要がありますので
\watch 0.01
8セッションのループは、同じユーザーの同時アクセスを表示します.PostgreSQLを使用すると、同じ状態で読み込み、書き込みを行う保証は、悲観的なロックによって強制されます.yugabytedbでは、楽観的なロックはよりスケーラブルですが、競合に失敗することができます.これはアプリケーションで処理する必要があります.このシリーズの中で、私はYugabyteDBのJDBCドライバーを紹介しました.すべてが互換性があるので、PostgreSQLに接続しても使用します.しかし、PostgreSQLが1つのライターエンドポイントしか持っていないので、もちろん、クラスタ認識機能は使用されません.以下はドライバのインストール方法ですnewer releases ):
wget -qc -O jdbc-yugabytedb.jar https://github.com/yugabyte/pgjdbc/releases/download/v1.0.0/jdbc-yugabytedb-42.3.3.jar
export CLASSPATH=.:./jdbc-yugabytedb.jar
表と関数の作成はこのシリーズの中にあり、ここでは再現しない.すべては、このブログシリーズの終わりにGithubリポジトリに行きます.私は、AとRatLimeRitodクラスを作成します
main()
これはデータソース("jdbc:yugabytedb://...
) インargs[1]
を指定します.args[0]
). で定義されているPL/pgSQL関数への呼び出しはレート制限を行います(args[3]
) IDargs[2]
) これはユーザ、テナント、エッジの場所です.これにより、1秒あたりのトークン数が最大になります.各スレッドはデータソースに接続します(ds
) を設定し、トークンを要求するユーザを設定する(id
) 1秒あたりの詰替率でrate
トランザクション競合の場合の再試行回数max_retry
args [ 4 ]で渡される.コンストラクタ
public RateLimitDemo(YBClusterAwareDataSource ds, String id, int rate, int max_retry
デフォルトのトランザクション分離レベルをシリアル化して設定し、手順を呼び出してステートメントを準備します.select文はhost@pidセッションの識別rs.getString(1)
) とトークンの数rs.getInt(2)
). クエリは以下の通りです.select
pg_backend_pid()||'@'||host(inet_server_addr()),
rate_limiting_request(?,?)
また、いくつかのバリエーションを実行しますpg_backend_pid()
にid
競合なしでテストするにはThe
public void run()
トークンリクエスト格納関数を呼び出すループです.トークンが利用可能な場合(rs.getInt(2) >= 0)
) これは、呼び出しカウンタをインクリメントします.そうでなければ1秒待つThread.sleep(1000)
)import java.time.*;
import java.sql.*;
import com.yugabyte.ysql.YBClusterAwareDataSource;
public class RateLimitDemo extends Thread {
private String id; // id on which to get a token
private int rate; // rate for allowed token / second
private int max_retry; // number of retry on tx conflict
private Connection connection; // SQL connection to the database
private PreparedStatement sql; // SQL statement to call to function
public RateLimitDemo(YBClusterAwareDataSource ds, String id, int rate, int max_retry) throws SQLException {
this.max_retry=max_retry;
this.connection=ds.getConnection();
this.sql = connection.prepareStatement(
"select pg_backend_pid()||'@'||host(inet_server_addr()),rate_limiting_request(?,?)"
);
this.sql.setString(1,id); // parameter 1 of sql function is the id requesting a token
this.sql.setInt(2,rate); // parameter 2 of sql function is the rate limit
this.connection.setAutoCommit(true);
this.connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
}
public void run() {
try{
Instant t0=Instant.now(); // initial time to calculate the per-thread throughput
double total_duration; // duration since initial time
int total_tokens=0; // counter for accepted tokens
int retries=0; // retries before failure
for(int i=1;;i++){ // loop to demo with maximum thoughput
try (ResultSet rs = sql.executeQuery()) {
rs.next();
retries=0; // reset retries when sucessful
if (rs.getInt(2) >= 0) {
total_tokens++; // requested token was accepted
} else {
Thread.sleep(1000); // wait when tokens are exhausted
}
total_duration=Duration.between(t0,Instant.now()).toNanos()/1e9;
System.out.printf(
"(pid@host %12s) %6d calls %6d tokens %8.1f /sec %5d remaining\n"
,rs.getString(1),i,total_tokens,(total_tokens/total_duration),rs.getInt(2));
} catch(SQLException e) {
if ( "40001".equals(e.getSQLState()) ) { // transaction conflict
System.out.printf(Instant.now().toString()
+" SQLSTATE %s on retry #%d %s\n",e.getSQLState(),retries,e );
if (retries < max_retry ){
Thread.sleep(50*retries);
retries=retries+1;
} else {
System.out.printf(Instant.now().toString()+" failure after #%d retries %s\n",retries,e );
System.exit(1);
}
} else {
throw e;
}
}
}
} catch(Exception e) {
System.out.printf("Failure" + e );
System.exit(1);
}
}
public static void main(String[] args) throws SQLException {
YBClusterAwareDataSource ds = new YBClusterAwareDataSource();
ds.setUrl( args[1] );
RateLimitDemo thread;
for (int i=0;i<Integer.valueOf( args[0] );i++){
thread=new RateLimitDemo(ds,args[2],
Integer.valueOf( args[3] ),Integer.valueOf( args[4] ));
thread.start();
}
}
} // RateLimitDemo
トランザクション競合はcatch(SQLException e)
and "40001".equals(e.getSQLState())
. エーretries
カウンタをインクリメントし、待ちますThread.sleep(50*retries)
. 最大再試行前(retries < max_retry )
例外を表示し、リトライをインクリメントします.retries
呼び出しが成功するとすぐに0に戻る.アフターmax_retries
, プログラムを止めるSystem.exit(1);
).PostgreSQL READ COMMITTED different
id
まず最初に、PostgreSQL ( AHA RDS DB . M 5 xHaのないHA )で実行しました.前のJavaコードで2つのことを変更しました.setTransactionIsolation
ラインコメントselect pg_backend_pid()||?||host(inet_server_addr()) ,rate_limiting_request(?||pg_backend_pid()::text,?)
javac RateLimitDemo.java && java RateLimitDemo 50 "jdbc:yugabytedb://database-1.cvlvfe1jv6n5.eu-west-1.rds.amazonaws.com/postgres?user=postgres&password=Covid-19" "user2" 1000 20 | awk 'BEGIN{t=systime()}/remaining$/{c=c+1;p=100*$5/$3}NR%100==0{printf "rate: %8.2f/s (last pct: %5.2f) max retry:%3d\n",c/(systime()-t),p,retry}/retry/{sub(/#/,"",$6);if($6>retry)retry=$6}'
これは、1ユーザあたりの上限(1秒あたり1000トークン)で50スレッドを実行します.AWKスクリプトはトークンを獲得し、実際のレートを表示し、成功した呼び出しの割合とリトライ回数を表示します.
rate: 999.29/s (last pct: 100.00) max retry: 0
rate: 1000.00/s (last pct: 100.00) max retry: 0
rate: 1000.71/s (last pct: 100.00) max retry: 0
rate: 1001.42/s (last pct: 100.00) max retry: 0
rate: 1002.13/s (last pct: 100.00) max retry: 0
rate: 995.77/s (last pct: 100.00) max retry: 0
rate: 996.48/s (last pct: 100.00) max retry: 0
rate: 997.18/s (last pct: 100.00) max retry: 0
rate: 997.89/s (last pct: 100.00) max retry: 0
rate: 998.59/s (last pct: 100.00) max retry: 0
rate: 999.30/s (last pct: 100.00) max retry: 0
rate: 1000.00/s (last pct: 100.00) max retry: 0
rate: 1000.70/s (last pct: 100.00) max retry: 0
rate: 1001.41/s (last pct: 100.00) max retry: 0
rate: 1002.11/s (last pct: 100.00) max retry: 0
私はパフォーマンスの洞察を得るためにAWS RDSでこれを実行しました.毎秒約1000オートコミット更新
私の50スレッドからこれらの単一行の呼び出しを行うと、平均15のデータベースでは、主にWalwriteで待機しています.
数字は、私が表示するものに一致します:1秒あたり100トランザクションコミット、1000タプルフェッチと1000行が更新されます.しかしながら、私はアクセス経路に依存するそれらの統計を表示することの関連性についての若干の疑問を持っています.例えば、私は1000 TuHelpを取得しました.
WALを書くのが主なボトルネックになっているので、競合はありません.PostgreSQLは、単一のカウンタ+ timestamp updateでさえ、多くのWALを生成します.なぜなら、全てのタプルが挿入されているため、以前のバージョンとしてマークされた古いもの、インデックス更新、および完全なページログを持つすべてのものです.これはHAのマルチAZ設定なし.
PostgreSQLシリアル化可能な異なる
id
シリアル化可能な分離レベルで同じことを実行しました.rate: 929.99/s (last pct: 100.00) max retry: 4
rate: 930.24/s (last pct: 100.00) max retry: 4
rate: 930.50/s (last pct: 100.00) max retry: 4
rate: 930.75/s (last pct: 100.00) max retry: 4
rate: 931.01/s (last pct: 100.00) max retry: 4
rate: 931.26/s (last pct: 100.00) max retry: 4
rate: 931.51/s (last pct: 100.00) max retry: 4
rate: 931.77/s (last pct: 100.00) max retry: 4
rate: 932.02/s (last pct: 100.00) max retry: 4
rate: 929.91/s (last pct: 100.00) max retry: 4
rate: 930.16/s (last pct: 100.00) max retry: 4
rate: 930.42/s (last pct: 100.00) max retry: 4
rate: 930.67/s (last pct: 100.00) max retry: 4
rate: 930.93/s (last pct: 100.00) max retry: 4
スループットはほぼ同じで、数やリトライは非常に小さかった.このポストはパフォーマンスについてです.私は2つの分離レベルについてのもう一つのポストを書きます、そして、我々がこの「トークンバケット」アルゴリズムでPostgreSQLまたはyugabytedbで得ることができる異常.PostgreSQL read readと同じです.
id
すべてのスレッドが同じIDのトークンを要求する競合状態をテストしたいと思います.エードリアンドーザ
@ adriandozsa
いいえ、右または間違って、ちょうど別のユースケース.テナントでも高い並列性を持つSaaSサービスのレート制限を考えていました.
01 : 05 AM - 2022年1月5日
ここではレートが減少している.
rate: 124.85/s (last pct: 100.00) max retry: 0
rate: 124.78/s (last pct: 100.00) max retry: 0
rate: 124.71/s (last pct: 100.00) max retry: 0
rate: 124.63/s (last pct: 100.00) max retry: 0
rate: 124.56/s (last pct: 100.00) max retry: 0
rate: 124.49/s (last pct: 100.00) max retry: 0
rate: 124.42/s (last pct: 100.00) max retry: 0
rate: 124.35/s (last pct: 100.00) max retry: 0
rate: 124.28/s (last pct: 100.00) max retry: 0
rate: 124.21/s (last pct: 100.00) max retry: 0
rate: 124.14/s (last pct: 100.00) max retry: 0
rate: 124.07/s (last pct: 100.00) max retry: 0
rate: 124.00/s (last pct: 100.00) max retry: 0
rate: 123.93/s (last pct: 100.00) max retry: 0
rate: 123.86/s (last pct: 100.00) max retry: 0
rate: 123.80/s (last pct: 100.00) max retry: 0
rate: 123.73/s (last pct: 100.00) max retry: 0
rate: 123.66/s (last pct: 100.00) max retry: 0
現在、すべてのスレッドが行を更新するために競争するので、ボトルネックはタプルロックですLock:tuple
and Lock:transactionid
行がロックされているとき、ロックを解除するトランザクションを待つ必要があります.平均で待っている45のセッション:すべての私の糸は、わずか5つの呼び出しを待っています:PostgreSQLシリアル化可能です.
id
今、i ' lは上記のプログラムを実行し、シリアルモードで同じ行を更新します.最大再試行数がすぐに達したので、私は50から10まで糸の数を減らしました.とにかく、10スレッドからのスループットは、読まれた50のものより良いです.rate: 218.48/s (last pct: 83.85) max retry: 5
rate: 217.19/s (last pct: 81.35) max retry: 5
rate: 217.39/s (last pct: 85.39) max retry: 5
rate: 217.78/s (last pct: 83.48) max retry: 5
rate: 218.07/s (last pct: 85.09) max retry: 5
rate: 218.37/s (last pct: 84.04) max retry: 5
rate: 218.70/s (last pct: 81.29) max retry: 5
rate: 217.47/s (last pct: 83.84) max retry: 5
rate: 217.82/s (last pct: 85.12) max retry: 5
rate: 218.06/s (last pct: 85.36) max retry: 5
rate: 218.44/s (last pct: 85.13) max retry: 5
rate: 218.65/s (last pct: 83.54) max retry: 5
rate: 217.31/s (last pct: 82.77) max retry: 5
rate: 217.64/s (last pct: 81.35) max retry: 5
rate: 217.82/s (last pct: 83.88) max retry: 5
7 %のリトライで、より多くのスレッドを実行するのは意味がないと思います.私は、これについてパフォーマンス洞察において何も見ませんでした:ボトルネック.この走行は前の直後のことです.リードされた悲観的なロッキングで、50のセッション待っている
Lock
イベントを待つtransactions blocked
統計が、ここでは、私は多くの再試行を知っている、私は何もこれらのメトリックで表示されます.ところで、AWKスクリプトは隠しますが、リトライはSQL状態40001として上がります.
(pid@host [email protected]) 579 calls 471 tokens 22.2 /sec 60000 remaining
(pid@host [email protected]) 544 calls 454 tokens 22.2 /sec 60000 remaining
(pid@host [email protected]) 655 calls 562 tokens 23.1 /sec 60000 remaining
2022-01-05T09:13:28.809161Z SQLSTATE 40001 on retry #0 com.yugabyte.util.PSQLException: ERROR: could not serialize access due to concurrent update
Where: SQL statement "update rate_limiting_token_bucket
set ts=now(), tk=greatest(least(
tk-1+refill_per_sec*extract(epoch from clock_timestamp()-ts)
,window_seconds*refill_per_sec),-1)
where rate_limiting_token_bucket.id=rate_id
returning tk"
PL/pgSQL function rate_limiting_request(text,integer,integer) line 7 at SQL statement
2022-01-05T09:13:28.815876Z SQLSTATE 40001 on retry #1 com.yugabyte.util.PSQLException: ERROR: could not serialize access due to concurrent update
Where: SQL statement "update rate_limiting_token_bucket
set ts=now(), tk=greatest(least(
tk-1+refill_per_sec*extract(epoch from clock_timestamp()-ts)
,window_seconds*refill_per_sec),-1)
where rate_limiting_token_bucket.id=rate_id
returning tk"
PL/pgSQL function rate_limiting_request(text,integer,integer) line 7 at SQL statement
(pid@host [email protected]) 565 calls 471 tokens 23.9 /sec 60000 remaining
(pid@host [email protected]) 739 calls 636 tokens 25.4 /sec 60000 remaining
管理サービスが提供して、モニターするのが簡単であるので、私はこれをアマゾンRDSで走らせました、しかし、これはすべてバニラPostgreSQLと同じです.AWSは、クラウドセキュリティモデルに対処するために、Postgresコードに対してほとんど変更されません.私は、あなたが次のポストで何を走らせるかについて推測することができるように、yugabytedb🤔覚えておくべきことは、このワークロードでは、既定のread commitまたはserializableは同じ行に競合することなく良いパフォーマンスを持つことです.Retryコマンドが存在しないため、再試行が行われていないため、コミットされた読み込みが簡単になると思います.更新プログラムを挿入するたびに再試行する必要がありますので
duplicate key
. 別のポストで詳しく説明します.このトークンバケットアルゴリズムでは、特に競合確率が高いシリアル化可能である.Reference
この問題について(PostgreSQLにおけるトークンバケットレート制限の楽観的または悲観的なロッキング), 我々は、より多くの情報をここで見つけました https://dev.to/aws-heroes/optimistic-or-pessimistic-locking-for-token-buckets-rate-limiting-in-postgresql-4om5テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol