PostgreSQLにおけるトークンバケットレート制限の楽観的または悲観的なロッキング


このシリーズの中で、私はAPI呼び出し率制限のためにトークンを得る機能を定義しました.私はそれを実行している\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 ラインコメント
  • 呼び出しでバックエンドpidを連結することによって、異なるユーザーのための呼び出しトークン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 . 別のポストで詳しく説明します.このトークンバケットアルゴリズムでは、特に競合確率が高いシリアル化可能である.