ひらめの日常

日常のメモをつらつらと

Java/Scala: ExecutorServiceとExecutionContextについてまとめる

はじめに

ScalaExecutionContext について調べていたのですが、JVMのスレッドプール周りの知識が必要だと思ったので、一通り調べた内容をまとめます。 全体として『Javaパフォーマンス』を参考にしつつ、適宜参考資料を添付します。

ExecutionContext とスレッドプール周りの関係は簡単に以下のように表せます。

classDiagram class Executor { +execute(command: Runnable): void } class ExecutorService { +submit(task: Callable): Future +shutdown(): void } class ThreadPoolExecutor { } class ForkJoinPool { } class ExecutionContext { +execute(runnable: Runnable): void +reportFailure(t: Throwable): void } Executor <|-- ExecutorService ExecutorService <|-- ThreadPoolExecutor ExecutorService <|-- ForkJoinPool ExecutionContext --> Executor: use

ExecutionContext はインターフェースとしてExecutor を利用していますが、実際には Executor を拡張した ExecutorService を利用することが多いため、ExecutorService から調べていこうと思います。

ExecutorService

まず、JVMでスレッドやタスクの管理を担っている ExecutorService について知る必要があります。

ExecutorServiceとは、スレッドを利用してタスクを実行するために抽象化されたインターフェースです。利用者は ExecutorService を通して操作するため、スレッド・タスク・キューを直接操作する必要がありません。ExecutorService は下図のような仕組みで並列実行を管理してくれます。

  • スレッドで実行される単位をタスクと呼ぶ(JavaだとRunnableインタフェースを継承しているやつ)
  • グローバルなタスクキューがあり、タスクが追加されていく
  • 各スレッドはタスクキューからタスクを追加し、実行する
  • 実行中はそのスレッドに対して新しいタスクは追加されない
  • あるスレッドが実行中のタスクが終了した場合、タスクキューから新しいタスクが追加される

ExecutorService がどのようにしてスレッドとタスクを管理しているのかについては、次の動画を見るのがおすすめです。

www.youtube.com

ExecutorService の実装には大きく分けて二つの種類があります。 ThreadPoolExecutorForkJoinPool です。

ThreadPoolExecutor

ThreadPoolExecutor (Java Platform SE 8 )

ThreadPoolExecutor とはスレッドプールを管理する仕組みで、ExecutorServiceを実装しているクラスです。スレッドプールを用いることで、パフォーマンス向上とリソースの最適化を図ります。以下公式ドキュメントより引用

スレッド・プールでは、2つの問題に対処します。まず、タスク当たりの呼出しオーバーヘッドが減少するため、通常は大量の非同期タスクの実行時にパフォーマンスが向上します。また、タスクのコレクションを実行するときに消費されるリソース(スレッドを含む)の境界設定および管理のための方法を提供します。各ThreadPoolExecutorも基本的な統計情報(完了したタスクの数など)を保持します

タスクを保持するキューには種類があり、それぞれスレッドを追加する基準が異なります。ThreadPoolExecutor を利用する際に明示的にキューを直接指定することは少ないですが、キューの種類に応じてスレッドプールの挙動が異なります。Executors.newCachedThreadPool(), Executors.newFixedThreadPool(int), Executors.newSingleThreadExecutor() のように適切なスレッドプールの挙動を指定して ThreadPoolExecutor を初期化する必要があります。

キューとスレッドプールの挙動の種類については、次の動画を見るのがおすすめです。

www.youtube.com

ForkJoinPool

ForkJoinPool (Java Platform SE 8 )

ForkJoinPool とはJava7で追加された、スレッドプールを管理する仕組みで、ExecutorServiceを実装しているクラスです。主に ForkJoinTask を実行することを目的としています。ForkJoinTask とは、分割統治のアルゴリズムに基づくタスクのことです。タスクが再帰的に分割が可能で、大量にサブタスクが生成される点が特徴です。

例えばフィボナッチ数の計算をする関数が例として挙げられます

def fibonacci(n: Int): Int = {
  if (n <= 1) n
  else fibonacci(n - 1) + fibonacci(n - 2)
}

ForkJoinTask を実装した FibonacciTask を作成すると、こんな感じに書けます。サブタスクが再帰的に生成されているのがわかります。

// FibonacciTask(n: Int)#solve() の実装
if (n <= 1) {
  n
} else {
  val f1 = FibonacciTask(n - 1) // サブタスク1 (fork)
  val f2 = FibonacciTask(n - 2) // サブタスク2 (fork)
  f1.solve()
  f2.folve()
  f1.result + f2.result // サブタスクの結果を結合 (join)
}

ここで注意の注意点は、FibonacciTask(n) が処理を進めるためには、 FibonacciTask(n - 1)FibonacciTask(n - 2) の処理が終了するまで待つ必要があるという点です。

ThreadPoolExecutor を用いる場合、次の図のようになります。

  1. FibonacciTask(n) をスレッド1が実行する。サブタスクとして FibonacciTask(n - 1)FibonacciTask(n - 2)をグローバルにあるタスクキューに追加する
  2. スレッド2が FibonacciTask(n - 1) を実行する
  3. スレッド3が FibonacciTask(n - 2) を実行する

この状態では、スレッド1は FibonacciTask(n - 1)FibonacciTask(n - 2) が完了して結果を受け取るまで処理を進めることができず、新しいタスクを受け取ることもできません。

このような処理を効率的に行うために、ForkJoinPool を使用します。ForkJoinPool では、あるスレッドが新しいタスクを開始した後、そのタスクについて一時停止することができます。そして、そのタスクを一時停止している間、スレッドは他のタスクを実行することができます。

よって、タスクが再帰的に分割できる際は、数個の少ないスレッドによって大量のタスクを処理できます。これが ForkJoinPool の利点です。

ForkJoinPool では、グローバルに存在するタスクキューとは別に、スレッドごとにローカルのタスクキューを持っています。これにより、スレッドが生成したサブタスクをスレッド単位で管理することが可能になります。

ここまで ForkJoinPool の役割とそれに適したタスクについて説明しました。もう一点、ForkJoinPool の大きな特徴として、他のスレッドのタスクを奪うことができることが挙げられます。これを work stealing と呼んだりします。

スレッドごとにローカルのタスクキューを持っていますが、これが空になった時、他のスレッドのキューを参照して残っているタスクを実行することができます。以下公式ドキュメントより引用

ForkJoinPoolは、主にwork-stealingを使用する点で、他の種類のExecutorServiceとは異なります。プール内のすべてのスレッドが、プールに送信されたタスク、他のアクティブなタスクによって作成されたタスク、あるいはその両方を見つけて実行しようとします(1つも存在しない場合は、最終的に作業の待機がブロックされます)。これにより、(ほとんどのForkJoinTaskと同様に)ほとんどのタスクが他のサブタスクを生成する場合や、外部のクライアントからプールに小さいタスクが数多く送信される場合に、効率的な処理が可能になります。特に、コンストラクタでasyncModeをtrueに設定した場合、ForkJoinPoolは結合されないイベント形式のタスクでの使用にも適している可能性があります。

これにより、あるスレッドがタスクの一つに長時間かかっている状態でも、別のスレッドが残りのサブタスクを実行することができます。

ForkJoinPool の挙動については、次の動画を見るのがおすすめです。

www.youtube.com

ThreadPoolExecutor vs ForkJoinPool

さて、二種類の ExecutorServiceを紹介しましたが、これはどのように使い分けるのが良いのでしょうか。ForkJoinPool に適している状況を考え、その反対を ThreadPoolExecutor に当てると考えると理解しやすいと思います。

  • ForkJoinPool
    • 再帰的にタスクがサブタスクへと分解でき、ForkJoinTask として実装できるタスク
    • 各タスクのバランスがアンバランスであり、work stealing によって一連の処理を効率化できるタスク
  • ThreadPoolExecutor
    • IOブロックのような、スレッドを止めてしまうようなタスク
      • これは ForkJoinPool の利点をあまり受けられない、という理由が大きい。スレッドが止まっているので、work stealing もできないし、他のサブタスクを実行することもできない
      • 例えば、Slick がIOブロックを管理している AsyncExecutor 内部でも ThreadPoolExecutor が利用されている。Slick 3.4.0 - slick.util.AsyncExecutor:slick.util.AsyncExecutor)
    • 各タスクのバランスが均等で、どのタスクも当程度の時間がかかると見通されるタスク

この辺の使い分けはあまり言及がないのですが、この辺を参考にしました。

ExecutionContext

Scala で非同期実行の際に必要となる ExecutionContext は、これまで説明してきた ExecutorService と同じような働きを持ちます。ExecutionContext を自作する際は、どの ExecutorService の実装を利用するか指定する必要があります。

では、Future をとりあえず動かしたいときに使うExecutionContext.global はどのような ExecutorService を利用しているのでしょうか?ExecutionContext について少しドキュメントを読んでみましょう。

Scala Standard Library 2.13.3 - scala.concurrent.ExecutionContext

The default ExecutionContext implementation is backed by a work-stealing thread pool. It can be configured via the following scala.sys.SystemProperties:

scala.concurrent.context.minThreads = defaults to "1" scala.concurrent.context.numThreads = defaults to "x1" (i.e. the current number of available processors * 1)

scala.concurrent.context.maxThreads = defaults to "x1" (i.e. the current number of available processors * 1)

scala.concurrent.context.maxExtraThreads = defaults to "256"

The pool size of threads is then numThreads bounded by minThreads on the lower end and maxThreads on the high end.

書いてある通りですが簡単にまとめると、こんな感じです。

  • 内部では ForkJoinPool( = work-stealing thread pool) を使用している
  • 並列数は max(256, CPUコア数) に設定される

全ての処理がCPU負荷のみでスレッドがIO待ちなどでブロックされない場合は、並列数=コア数でもそこまで問題にはならないですが、スレッドがブロックされる処理がある場合、ブロック中に他のスレッドの処理を実行したほうが効率が良いです。その場合はCPUコア数以上を設定する必要があります(他にも ExecutionContext.global をあらゆるところで使うと良くないのですが、ここでは一旦省略...参考記事を読んでいただけると)。

Scalaにおける ExecutionContext 周りはこの辺が参考になりました。

これから知りたいこと

  • なぜ ExecutionContext のデフォルトは ForkJoinPool を使用しているのか
    • とりあえずChatGPTに聞いてみた。

In Scala 2.13, the default implementation of ExecutionContext is ForkJoinPool. There are several reasons for this choice:

  1. ForkJoinPool is optimized for parallelism and work-stealing, which makes it a good fit for Future and Promise operations that require parallel execution.
  2. ForkJoinPool is a shared resource, which means that multiple ExecutionContexts can use the same ForkJoinPool instance. This reduces resource consumption and helps prevent contention for system resources.
  3. ForkJoinPool provides a good balance between concurrency and overhead. It is designed to efficiently manage large numbers of threads while minimizing context switching and other overhead.
  4. ForkJoinPool is a standard part of the Java concurrency API, which makes it a familiar and widely-used technology for Java and Scala developers.

Overall, the choice of ForkJoinPool as the default ExecutionContext implementation in Scala 2.13 reflects a balance between efficiency, flexibility, and ease of use

  • Akka の dispatcher はデフォルトで ForkJoinPool を利用しているが、それはなぜか。アクターモデルForkJoinPool の相性の良さとはどの辺なのか

『データ指向アプリケーションデザイン』を読んだ ー 第II部 分散データ 7章

こちらの続きです hiramekun.hatenablog.com

本はこちら

7章は個人的に重要なことが多いと感じたので、内容多めにまとめます。

第7章 トランザクション

ACID

トランザクションが提供する安全性の保証は、ACID で示される。

  • 原子性(Atomicity)
  • 一貫性(Consistency)
    • データについて何らかの不変性があるということ。
    • 一貫性を保つようにトランザクションを適切に定義することはアプリケーションの責任であり、これはデータベースの責務ではない。
  • 分離性(Isolation)
    • 並行して実行されたトランザクションがお互いから分離されているという意味。
    • 直列化可能性(serializability)とも呼ばれる
      • 複数のトランザクションが並行に実行されていても、コミットされた後の結果はそれが直列に実行されたことと同じようになる。
    • 実際にはスナップショット分離が実装されることもある。
  • 永続性(Durability)
    • トランザクションのコミットが成功したら、ハードウェアの障害やクラッシュがあったとしても、トランザクションで書き込まれた全てのデータは失われないことを保証する。
    • とはいえ、完全な永続性は存在しない。

トランザクションのエラーと中断の処理

トランザクションの重要な機能は、エラーが生じた際に中断でき、安全にリトライできること。

リーダーレスレプリケーションを行うようなシステムなど、ベストエフォートで動作するシステムもある。つまり、データベースはできる限り処理を進め、エラーが起きた場合でもそれまでに終わった処理は取り消さない。エラーからのリカバリはアプリケーション側で担当する。

このようなエラーからのリカバリ、リトライでは特に以下の点に注意が必要

  • トランザクションが成功していても、ネットワークエラーにより失敗したとみなされる可能性がある。アプリケーション側でトランザクションの重複排除の仕組みが必要。
  • 過負荷によるエラーが発生した場合、トランザクションをリトライすると状況をむしろ悪化させる。リトライの回数を制限し、指数的バックオフを行うなどが必要。
  • リトライするべきなのは一時的なエラー(デッドロック・一時的なネットワーク障害など)の場合のみであり、恒久的なエラーの場合はリトライしない。
  • トランザクションがデータベース外に副作用を持っている場合、トランザクションが中断されても副作用は生じているかもしれない。
  • クライアントのプロセスがリトライ中に落ちた場合、そのプロセスがデータベースに書き込もうとしたデータは全て失われる。

弱い分離レベル

並行性の問題(レース条件)が問題となるのは、

実際に利用されている弱い分離レベルにおいて、どのようなレース条件が生じるのかをみていく。

Read Committed

トランザクションの分離性において最も基本的なレベルであり、2つのことを保証する。

  • ダーティリードは生じない
    • データベースから読み取りを行なった際に見えるデータは、コミットされたもののみ
  • ダーティライトは生じない
    • データベースへの書き込みを行う場合、上書きするのはコミットされたものに対してのみ

トランザクションがあるオブジェクトを更新したい場合、トランザクションはまずはそのオブジェクトのロックを獲得する。そしてトランザクションが終わるか中断するまで保持する。

トランザクション進行中でまだコミットされていないオブジェクトを読み取ろうとした場合、データベースは以前にコミットされている古い値を返す。

スナップショット分離とリピータブルリード

例としてあげられていたのは以下のようなもの。

  1. 下図のように、Aliceが口座1と口座2にそれぞれ残高が500円あるとする。
  2. Aliceがそれぞれの口座に対して読み取りのトランザクションを開始する
  3. Aliceが読み取りのトランザクションをしている最中に、別の送金トランザクションが100円を口座1から口座2に移し、コミットする。
  4. Aliceが読み取りのトランザクションをコミットする。
  5. するとAliceは「送金前の口座1の値」と「送金後の口座2の値」の両方を参照してしまい、500円 + 400円 となり、不整合が生じる。
sequenceDiagram autonumber actor Alice participant 口座1 participant 口座2 Note over 口座1, 口座2: それぞれ残高500 actor 送金 Alice ->> 口座1: select balance from accounts where id = 1 口座1 -->> Alice: 500 送金 ->> 口座1: update accounts set balance = balance + 100 where id = 1 口座1 -->> 送金: success 送金 ->> 口座2: updaate accounts set balance = balance - 100 where id = 2 口座2 -->> 送金: success Note left of 送金: コミット Alice ->> 口座2: select balance from accounts where id = 2 口座2 -->> Alice: 400 Note right of Alice: コミット

このような現象を nonrepeatable read もしくは読み取りスキューと呼ぶ。この読み取りスキューは、read committed 分離レベルの下では許容されるものとみなされている。

スナップショット分離 を用いると、読み取りスキューを解決できる。スナップショット分離の考え方は、トランザクションが読み取るデータは、すべてそのトランザクション開始時点でデータベースにコミットされたものだけ、というもの。PostgreSQLMySQLなどでサポートされている(これらでは リピータブルリード と呼ばれている)。特徴としては、

  • 読み取りが書き込みをブロックしない
  • 書き込みが読み取りをブロックしない

スナップショット分離の実装

データベースは、あるオブジェクトについてコミットされた状態を保持する必要がある。これは、マルチバージョン並行性制御, MVCC と呼ばれる。

昔のバージョンを参照するため、長時間実行されるトランザクションはずっと昔のバージョンを参照し続けている可能性がある。

更新のロストの回避

更新のロストの問題は、二つ以上の並行するトランザクションが、アプリケーションが何らかの値をデータベースから読み取り、その値を変更して書き戻す場合に生じることがある。2つ目の変更は1つ目の変更を踏まえていないため、2つ目の変更が書き込まれる際は1つ目の変更が失われることになる。

アトミックな書き込み操作

多くのデータベースではアトミックな更新処理が提供されている。アトミックな操作は、次のようにして実装される。

  1. オブジェクトからの読み取りの際に排他ロックを取る
  2. 更新の適用が終わるまで、他のトランザクションがそのオブジェクトを読み込めないようにする

ORマッパーを使用する際、このアトミックな操作を実行しない時があるので注意すること。
https://nateware.com/2010/02/18/an-atomic-rant/

明示的なロック

アプリケーション側で明示的にロックをかけるのも方法の一つ。 FOR UPDATE によってデータベースが、すべての行に対するロックを取得するようにする MySQL :: MySQL 5.6 リファレンスマニュアル :: 14.2.5 ロック読み取り (SELECT ... FOR UPDATE および SELECT ... LOCK IN SHARE MODE)

衝突の解決とレプリケーション

レプリケーションされているデータベースでは、データが別々のノード上で並行して変更される可能性がある。この場合、並行する書き込みによって1つの値について衝突する複数バージョンの生成を許し、その後アプリケーションのコードなどによってそれらのバージョンの衝突解決やマージを行うのが一般的なアプローチ。

書き込みスキューとファントム

これまでは同じオブジェクトに対する書き込みや読み込みによる競合を扱ったが、それ以外の場合も発生する。それが 書き込みスキュー で、次の状態で発生しうる。

  1. 2つのトランザクションが同じオブジェクト群から読み取りを行い、
  2. それらのいくつかを更新する。トランザクションごとに更新するオブジェクトが異なっていても発生しうる。

より具体的には、

  1. SELECT クエリによって特定の条件を満たす行を検索し、要求が満たされているかをアプリケーション側で確認する
  2. このクエリの結果に応じてアプリケーションは振る舞いを変える
  3. 処理を続ける場合、データベースに書き込みを行い、トランザクションをコミットする

このように、あるトランザクションでの書き込みが他のトランザクション中の検索クエリの結果をへkなさせてしまう効果は ファントム と呼ばれる。

何かデータにおける制約がある場合は、データベースのユニーク制約や外部キー制約などで解決できる場合がある。

直列化可能性

直列化可能分離レベルは、最も強い分離レベルとされている。これは、トランザクションが並行して実行されていても、最終的にはそれぞれが1つずつ順番に実行された場合と同じになることを保証する

それを実現するための方法をいくつか紹介する。

完全な順次実行

シングルスレッドで順次実行している方法は、Redisなどで実装されている。ただし、書き込みスキューを解決するためには、ネットワーク越しにクエリを複数回実行し、アプリケーション側で条件分岐をする必要があり、ここにコストがかかっている。そこで、このようなシステムでは外部とのやりとりで複数文を含むトランザクションを禁止し、ストア°プロシージャ を利用する。

ツーフェーズロック(2PL)

2PLでは、

  • writer がブロックするのは他の writer だけではなく、他の reader もブロックする
  • reader も write をブロックする

2PLは、MySQLSQLServer の直列化可能分離レベルなどで利用されている。Reader, Writer のブロックは、データベース内の各オブジェクトにロックを持たせることによって実装される。このロックは二つのモードがあり、shared モードexclusive モード である。

  • 読み取りを行う場合、トランザクションはまず shared モードのロックを取得する。他のトランザクションが exclusive モードのロックを取得していた場合、その解放を待つ必要がある。
  • 書き込みを行う場合、トランザクションはまず exclusive モードのロックを取得する。他のトランザクションが同時に shared, exclusive モードともにロックを取得することは許可されず、どちらかのロックがすでに保持されている場合、その解放を待つ必要がある。

トランザクション開始時にロックを取得し、終了時にロックを解放することから、ツーフェーズロックと呼ばれている。

このツーフェーズロックは、ロックの取得が頻繁に発生するため、パフォーマンス面が大きな欠点となっている。

インデックス範囲ロック

2PLを持つほとんどのデータベースが実装しているのが、インデックス範囲ロック、別名 next-key ロック である。

インデックス範囲ロックでは、 SELECT 文を実行する際に検索条件となっているインデックスのデータに共有ロックを持たせるというもの。これは条件が範囲であればその範囲のデータをロックするし、単独の値であればその値をロックする。

このロックを与えるのに適したインデックスがない場合、データベースはテーブル全体に対する共有ロックを取得する。 その際はこのテーブルに書き込もうとしているすべてのトランザクションを止めるため、パフォーマンスが低下する。

直列化可能なスナップショット分離(SSI)

これは、完全な直列化可能性を提供しながら、パフォーマンス面での低下を抑えられる方法として期待されている。

2PLは、悲観的な並行性制御の仕組みで、状況が安全になるのを待ってから処理を進める方法。

その一方で直列化可能スナップショット分離は楽観的な仕組み。何か競合が起きた場合、そのままトランザクションを実行して問題がなかったことを期待する。もし問題があれば、トランザクションを一度中止し、リトライを行う。

『記憶の脳科学』メモ

記憶の脳科学

こちらの内容を自分なりにまとめたメモ www.gaya.jp

脳の可塑性

  • 覚えていない状態と覚えている状態で、脳は何かが異なっている。このことを 脳の可塑性 と呼ぶ。
  • あるきっかけにより何らかの変化を起こし、そのきっかけがなくなっても変化したまま。その時期  C は人によって異なるであろう。
  • 脳内で変化するのは 神経回路 である。
  • シナプスの伝達効率が上昇する、シナプス可塑性
    • 記憶・学習の基礎をなすメカニズムの一つ

ヘブの法則

記憶の基礎であるシナプス可塑性が持つ性質について

  • シナプス可塑性は、起こるべきシナプスに限定して生成する、入力特異性
  • 一定以上の強い信号が来た時にのみシナプス可塑性が生じる、協力性
    • 記憶するための閾値を超えなければならない。
  • Aからの刺激だけでは閾値を超えないが、Bからの刺激と連合することによって覚えやすくなる。これを 連合性 という。

LTP

ヘブの法則を満たすシナプス可塑性

  • 高頻度の刺激を与えると、神経の反応が瞬時に大きくなり、数時間から数日持続する。これは 長期増強(LTP) と呼ばれる
  • 海馬で観測されるため、記憶の基礎メカニズムである可能性を強く指示する。
  • 学習能力はLTPの大きさと正の相関がある。

LTPのメカニズム

シナプスは前細胞が神経伝達物質を放出し、それを後細胞が反応するという仕組みになっている。

Mac の移行作業メモ

移行アシスタントは不要なものも引き継いでしまいそうなので、まっさらなところから環境構築を行う。 今回シェルをfishからzshに移行したが、そのメモはZennのスクラップに残してある

dotfilesの実行

Apple Store から最低限必要なアプリをダウンロードしてくる。

  • 回線状況を確認するために bandwidth+ をダウンロードして起動する
  • Xcodeは非常に重たいので、可能なら有線環境でダウンロードする

自分の中で必須なのは上二つくらいで、その他は Line や Mathpix Snipping Tool 等必要であればダウンロードする

Xcodeが入れば git が使えるようになるので、自分の環境構築用リポジトリである dotfiles をダウンロードする

github.com

引っ張ってきたら実行

cd dotfiles
sh up

この中では次のようなことをしている

  • ansible を使用し、環境構築手順を自動化
  • homebrew を使用し、パッケージとアプリケーションをダウンロード
  • 各種設定ファイルをダウンロードしてシンボリックリンクを貼る

基本的に必要なパッケージやアプリケーションが追加された場合は dotfeils を編集する。

必須アプリケーションの起動と手動設定

ダウンロードしてきた中で、常に起動しておきたいものがあるので、起動する。 全てのアプリケーションで、「PCの立ち上げ時に自動で起動する」オプションがあれば設定する。

  • Karabiner-Elements
  • Alfred - Productivity App for macOS
    • リッチなSpotlight検索のようなイメージ
    • 起動するホットキーの設定を ctrl + enter に変更
  • HyperSwitch
    • アプリケーション単位ではなく、開いているウインドウ単位で切り替えができるようになる
    • ウインドウ単位での切り替えを cmd + tab に割り当てる
  • BetterTouchTool
    • ウインドウサイズのリサイズや、アプリケーションの起動にショートカットキーを当てることができる
    • dotfiles リポジトリから設定ファイルをダウンロードしてあるので、BetterTouchTool の設定にimportする
    • 昔のメールから有料ライセンスを見つけて、有効化する
  • ShowyEdge
    • 入力言語に応じて、スクリーン上部の色を変えることができる。英語入力したいのに日本語入力だった、みたいな凡ミスが少なくなる
    • スクリーン上部のどの辺まで色を変えるか設定変更。メニューバーと同等の高さにしたいので、use custom frame にして width 100%, height 22pt。
    • メニューバーの内容が見えなくなると困るので、opacityを減らして、色も好みに変更
  • Typora — a markdown editor, markdown reader.
    • 最強のマークダウンエディタ
    • 昔のメールから有料ライセンスを見つけて、有効化する

Karabiner-Elements, BetterTouchTool, HyperSwtich あたりの嬉しさについては昔Qiitaに書いていたので、そちらも参考

qiita.com

macOS の設定

  • Dockを自動で隠す
  • デフォルトブラウザをChormeに変更
  • 数字をデフォルトで半角にする
  • コントロールセンター周り
    • バッテリーの%を常に表示
    • Bluetoothをメニューバーに表示
    • サウンドをメニューバーに表示
    • Spotlight をメニューバーで非表示

JetBrains周り

JetBrainsToolbox を使って必要なアプリケーションをダウンロードする。 自分の場合、IntelliJ, CLion あたりは必須

それぞれのアプリケーション共通のプラグインとして、次のものを導入する

IdeaVimのキーリピートがデフォルトだと効かないのに少しハマったので、有効化を忘れないようにする

IntelliJ key repeating idea.vim - Stack Overflow

defaults write com.jetbrains.intellij ApplePressAndHoldEnabled -bool false
defaults write com.jetbrains.CLion ApplePressAndHoldEnabled -bool false

TODO

自動化可能だが未実装なもの

  • macOSの各種設定
  • JetBrains周りのキーリピート設定

自動化したいが可能かどうかわかっていないもの

  • BetterTouchTool の設定ファイルを、環境構築時に自動でシンボリックリンクを貼りたい
  • 各種有料ライセンスをメールではなくクラウドストレージにアップロードしておき、それをダウンロードして有効化する仕組み

MacBook を守るおすすめのアクセサリー

MacBook を久しぶりに買い替えたので、おすすめのアクセサリーを紹介します(アフィリンクではありません)。

特に自分は手汗をかく方なので、同じような悩みがある方はぜひ参考にしてみてください。

他にもおすすめがあれば教えてくださいー!

ディスプレイフィルム

特にこだわりはないのですが、張りやすい・指紋がつきずらい・反射しないと良いところづくしなのでおすすめです。

キーボードカバー

手汗をかく方なのでキーボードが汚れやすく、キーボードの上にシートを貼って保護しています。また、万が一飲み物を上にこぼした際も被害を最小限にとどめることができます。

他のプラスチック製のものも試したのですが、元々の打感が一番失われないのがこのビニール製シートでした。見た目はそこまでスタイリッシュではないですが、おすすめです。

スキンシール

手汗をかく方なので表面に汚れがつきやすく、それを目出せないために使用しています。キーボードを打っている時の手の平〜手首に相当する部分が汗をかいてしまうのですが、その部分にもシールが貼れるので重宝しています。

デザインも色々あって、自分好みの見た目にできるのでお気に入りです。

プラスチック製のシェルカバーを試したこともありましたが、厚みが増す・重くなる・熱がこもりやすい、といった理由からシール型のものに移行しました。

『データ指向アプリケーションデザイン』を読んだ ー 第II部 分散データ 5~6章

こちらの続きです hiramekun.hatenablog.com

本はこちら

5章 レプリケーション

レプリケーションとは、ネットワークで接続された複数のマシンに同じデータのコピーを保存しておくことを指す。レプリケーションを行う理由は以下の通り。

  • レイテンシを下げる:データを物理的にユーザーの近くで保持するため
  • 可用性を高める:一部に障害があってもシステムが動作し続けるようにするため
  • スループットを高める:読み取りのクエリを処理するマシン数をエスケースアウトせせるため

この章では、パーティショニングは考えずにレプリケーションについてまとめる

リーダーとフォロワー

データベースのコピーを保存する各ノードはレプリカと呼ばれる。全ての書き込みが、全てのレプリカに行き渡るようにするには、リーダーベースレプリケーションがよく使われる(別名 アクティブ/パッシブ、マスター・スレーブレプリケーションMySQL ではマスター・スレーブと呼ばれることが多い。MySQL :: MySQL 5.6 リファレンスマニュアル :: 17 レプリケーション)。動作原理は以下の通り。

  1. レプリカのうち、一つはリーダーに指定される。データベースに書き込む場合、クライアントはリクエストをリーダーに送信する。
  2. 他のレプリカはフォロワーと呼ばれる。リーダーはデータを自身のストレージに書き込んだ後、その変更をレプリケーションログもしくは変更ストリームの一部として全フォロワーに送信する
  3. 各フォロワーはこのログをリーダーから受け取った後、その内容に従ってリーダー上で処理されたのと同じ順序で全ての書き込みを自身のストレージに行う。
  4. クライアントがデータベースから読み取りを行う場合には、リーダー・フォロワー関係なく全てのレプリカにリクエストを送ることができる。

同期的か非同期的か

レプリケーションを行うシステムで重要なのは、レプリケーションが同期的に行われるのか、非同期的に行われるのかどうか。もし非同期である場合、書き込みが即時反映されないレプリカが存在する事になる。その一方で、同期型レプリケーションにも利点と欠点が存在する。

  • 利点:フォロワーが持っているデータが最新であり、リーダーとの一貫性が保証される。
  • 欠点:同期型のフォロワーが反応を返さなかった場合、リーダーは全ての書き込みをブロックするため書き込み処理ができなくなってしまう。

よって可用性を考えるのであれば、全てのフォロワーを同期型にするのは現実的ではない。実際に同期型レプリケーションを有効にするのであれば、複数のフォロワーのうち一つを同期型にして、残りは非同期型にするのが良い。もし同期型のフォロワーが利用できなくなった場合は、非同期型のフォロワーを一つ同期型に変更すれば良い。

また、全てのフォロワーを非同期にすることも頻繁にある。

  • 利点:全てのフォロワーに遅延が生じていても、リーダーが書き込み処理を継続できる。
    • 特にフォロワーが多い場合や、フォロワーが地理的に分散している場合は大きな利点となる。
  • 欠点:リーダーに障害が発生した場合、フォロワーにレプリケーションされていない書き込みは全て失われる。

障害への対処

  • フォロワーの障害:キャッチアップリカバリ

    • フォロワーは障害発生前に最後に処理したトランザクションをログから判断できるので、フォロワーはリーダーに接続し、接続が切れた後に生じた全てのデータ変更を要求できる
  • リーダーの障害:フェイルオーバー

    • フェイルオーバー時の手順は複雑で、簡潔に以下の手順を踏む

      • フォロワーのいずれかをリーダーに昇格させる
      • クライアントは新しい書き込み先を新しいリーダーに設定し直す
      • 他のフォロワーはデータ変更を新しいリーダーから受信する
    • フェイルオーバーが発生した場合、問題となることは多く存在する

      • 非同期のレプリケーションを使用する場合、新しいリーダーは以前のリーダーから全ての書き込みを受信していない可能性がある。その場合、新しいリーダーは受信してない書き込みを破棄することが多い

      • 受信していない書き込みを破棄する場合、障害の発生要因となるので注意が必要。

        同期が遅れていたMySQLのフォロワーがリーダーに昇格。その結果 auto increment によりリーダーで割り当て済みのprimary keyを発行。Redisにもprimary key は保存されていたので、MySQLとRedisでデータ不整合が発生 GitHub availability this week | The GitHub Blog

    • 2つのノードが共に自身をリーダーだと信じてしまうスプリットブレインが起こりうる

レプリケーションログの実装

レプリケーションラグにまつわる問題

書き込むが少なく、読み取りのクエリが多いアプリケーションでは、多くのフォロワーを作り、リクエストを分散させる選択肢がよく取られる。その際、レプリケーションは非同期的に行う必要がある。同期的にレプリケーションする場合、フォロワーの一つがダウンするとシステム全体が書き込み不能になってしまうため。

非同期的にレプリケーションをする場合、リーダーで書き込まれた内容がフォロワーに反映されるまでラグがある。最終的にリーダーとフォロワーが同じ状態になることを、結果整合性 と呼ぶ。また、リーダーとフォロワーの間に異なる状態が存在する時間差を レプリケーションラグ と呼ぶ。

レプリーケーションラグによって、いくつか問題が生じる

  • クライアントが自分で書き込んだ内容をフォロワーから読み取る際に、内容が反映されていない可能性がある。
    • 書き込んだ内容を読み込んだ際にその内容が反映されている一貫性のことを、read-after-write一貫性 と呼ぶ
    • 解決策としては、(1) ユーザーが変更した可能性のあるものを読み取る場合はリーダーから読み取りを行う (2) 最後の更新からある一定の時間内に行う読取は、リーダーから行う などがある
  • 同じクライアントが異なるフォロワーにアクセスし、古い状態のデータが見えることがある。
    • モノトニックな読み取り では、これが生じないことを保証する
    • 連続で同じクライアントがデータを読み取る場合に、古い状態のデータが見えないことを保証する
    • 各クライアントが常に読み取りを同じレプリカから行うようにすれば、モノトニックな読み取りを実現できる
  • 一貫性のあるプレフィックス読み取り を行い、書き込みの順序通りに読み取りが行われるようにする

6章 パーティショニング

レプリケーションでは同じデータを複数のノードで保持することだった。パーティションはそれとは異なり、データを分割して配置することを表す。これはシャーディングとも呼ばれる。

パーティショニングする主な理由は、スケーラビリティ。大規模なデータセットを数多くのディスクに分散して配置できるため、クエリの負荷を分散させることができる。

パーティショニングしたデータをレプリケーションして複数のノードに保存することが多い。

キー・バリューデータのパーティショニング

パーティショニングの目的は、データとクエリの負荷をノードで均等に分散させること。パーティショニングが均等になっておらず、一部のパーティションが他と比べて多くのデータやクエリを担当している状態を スキュー と呼ぶ。また、この状態で高負荷が集中しているパーティションホットスポット と呼ぶ。

キー・バリューデータに対して、データとクエリの負荷をノードで均等に分散させるための手法はいくつか存在している。

  • キーの範囲に基づくパーティショニング
  • キーのハッシュに基づくパーティショニング
    • 多くの分散データストアではこちらを採用
    • ハッシュ関数が決まれば、それぞれのパーティションハッシュ値の範囲を割り当てて分割することで、パーティション間で均等にキーを分散することができる
    • ハッシュ値を使ってパーティショニングすることで、キーのソート順序は失われてしまうため、検索時の効率性は下がるのが欠点

パーティショニングとセカンダリインデックス

レコードを特定するためではなく効率的な検索処理に用いられるセカンダリインデックスを持つ場合は、パーティショニングではユニークなキーによって分割されるため、追加でセカンダリインデックスを利用するための仕組みが必要になる。

  • ドキュメントによるセカンダリインデックスでのパーティショニング
  • 語によるセカンダリインデックスでのパーティショニング
    • 全てのパーティションをカバーするグローバルインデックスを構築する方法。
    • セカンダリインデックスの値に応じてパーティショニングすることで実現する。
    • 検索時にはドキュメントによるセカンダリインデックスよりも効率的。その一方で、書き込み時には多くのパーティションに書き込むこともり、パフォーマンス低下の可能性がある。

パーティションのリバランシング

負荷をクラスタ内のあるノードから別のノードへと移行するプロセスを リバランシング と呼ぶ。

キーをノードで割った剰余でパーティションを行なっている場合、ノード数が変化するとほとんどのキーはノード間を移動する必要があり、必要以上にデータを移動させる必要がある。そのため、単に剰余を用いるのはパーティショニング時に得策ではない。

ノード数よりも多くのパーティションを用意し、一つのノードに複数のパーティションを割り当てる方式であれば、負荷に応じてパーティション内のデータは変えずにパーティションをノード間で移動することでリバランシングが可能になる。この方式では、パーティション数は最初から大きめの値を固定で設定する。

一方で、動的にパーティショニングを行う方式もある。データ量に応じて動的にパーティションする場合は、データの量が増えるとパーティションを分割し、データの量が減った場合はパーディションを結合する。パーティション数をノード数に比例させる方法もある。これは、ノードあたりのパーティションを固定するというものになる。

自動のリバランスと手動のリバランス

リバランシングはリクエストを再ルーティンし、大量のデータをノード間で移動させる負荷の大きい処理。障害検知も自動で行なっている場合、自動でリバランシングさせると、データを移動する際に一時的にノードが過負荷に陥り、クラスタの再配置が実行される可能性がある。これが行われると、さらにパフォーマンスが悪化する。なので、リバランシングにはどこかで人を介在させると良い。

リクエストのルーティング

データをパーティショニングできたとして、クライアントはどのようにして自身のデータが属するノードと通信するかが問題となる。これには大きく分けて3つのアプローチがある

  • クライアントが任意のノードに接続する。その後、ノードが適切なノードにリクエストを転送する
  • クライアントからのリクエストは全てルーティング層に送られ、ルーティングそうが適切なノードにリクエストを転送する
  • クライアントがパーティショニングと、ノードの割り当てを認識し、直接適切なノードにリクエストを送信する

続き

hiramekun.hatenablog.com

MySQL における primary key についてメモ

MySQL における primary key について

MySQL における primary key (プライマリキー、主キーとも呼ばれる)は、テーブルの中のカラムに対して指定できるもので、以下のような特徴を持ちます

  • テーブルで一つのみ指定できる
  • NULLは許容しない
  • 一意の(ユニークな) ID を付与する
  • インデックスが自動で構築される

インデックス構造の基礎知識についてはこちらの記事が非常にわかりやかったのでぜひ参考にしてみてください

MySQL with InnoDB のインデックスの基礎知識とありがちな間違い - クックパッド開発者ブログ

primary key として何を利用するか?

ここで議論となるのは、primary key として何を利用するかという点です。

AUTO_INCREMENT

よくあるのは、DB側の機能として AUTO_INCREMENT を利用し、DB側で採番するという方法です。

メリットとしては以下のようなものが挙げられます。

  1. アプリケーション側の実装が不要
  2. 連番なので必ずキーが被ることはない
  3. 連番なのでインデックス構築の効率が良い

上記の利点からよく採用される AUTO_INCREMENT ですが、以下のようなデメリットもあります。

  1. DBに書き込まれるまで primary key が定まらず、アプリケーション側では一度書き込んだ後の値を使用する必要がある
  2. 複数DBで採番すると ID が重複するため、DBの台数を増やすことができない

採番テーブル

次によくあるのは、採番テーブルを用意し、そこで連番を管理する方法だと思います。こちらではDBにデータを書き込む前に primary key を取得するため、 AUTO_INCREMENT におけるデメリットの1を解決できます。

一方で、DBの台数を増やすことができないデメリットは残しますし、primary key を取得する際に毎回採番テーブルへのIOが発生する点は新しいデメリットです。

UUID

今までのように DB側で primary key を使うのではなく、アプリケーション側で一意な primary key を生成する方法もあります。

その場合はこれまでのように primary key を生成する処理をDBに依存する必要がないのが利点です。

一意な ID と聞くと、UUID が候補として挙げられます。ここではその中でも UUID version1 を取り上げます。UUID を使う場合はパフォーマンス面での懸念があります。次の記事で非常に丁寧に解説されているので読んでみてください。

MySQLでプライマリキーをUUIDにする前に知っておいて欲しいこと | Raccoon Tech Blog

自分の言葉でまとめると、こんな感じになります。

  • インサート時、読み込み時ともに、UUID のうちランダムな部分が原因となり、インデックス構築時にクラスタインデックス(B-treeの発展版なようなものという理解)の全リーフのインデックスを読み込む可能性がある。
    • 前提として、UUID を生成する際にタイムスタンプが参照されるが、タイムスタンプを表すビットの中で、上位ビットと下位ビットを入れ替えたりするため、時系列でシーケンシャルな値になっていない。
    • もしシーケンシャルであれば、近い値のインデックスは場所的にも近いので、一部分だけ読み込めば良い。キャッシュにもヒットする可能性が高い。

UUID を使う場合でも、MySQLuuid_to_bin という関数を使えば、パフォーマンス問題が解決するようです。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 12.24 その他の関数

まず、UUID を16進数表現からバイナリに変換することで 32byte から 16byte に変換することができます。これにより、インデックスの保持に必要な容量が減りパフォーマンス若干向上します。

文字列 UUID をバイナリ UUID に変換し、結果を返します。 (IS_UUID() 関数の説明には、許可されている文字列 UUID 形式がリストされます。) 戻りバイナリ UUID は VARBINARY(16) 値です。 UUID 引数が NULL の場合、戻り値は NULL です。 無効な引数がある場合は、エラーが発生します。

また、インデックスとして使用している場合は次の引数が重要です。 swap_flag を 1 にすることで、UUID 生成の際にスワップされてしまったタイムスタンプ部分の上位ビットと下位ビットを再度交換し、ほぼシーケンシャルな ID として利用することができるようになります。

    • swap_flag が 1 の場合、戻り値の形式は異なります: time-low 部分と time-high 部分 (それぞれ 16 進数の最初と 3 番目のグループ) がスワップされます。 これにより、より迅速に変化する部分が右側に移動し、結果がインデックス付けされたカラムに格納されている場合はインデックス付けの効率を向上させることができます。

ULID

ULID は UUID の問題点であったタイムスタンプがそのまま保持されていない点を解消し、タイムスタンプを上位ビットにそのまま保持し、下位ビットにランダム列が入ります。

ulid/spec: The canonical spec for ulid

そのためデフォルトでほぼシーケンシャルとなり、インデックス周りのパフォーマンス低下を抑えることができます。

他にも、

  • 128bit (=16byte)
  • UUID は文字列で36文字必要なところ、26文字で抑えられる
  • 1msあたり、1.21e+24 のユニークな ULID が生成される

あたりの特徴があります。くわしくはリンク先の仕様を見てください。

UUIDv1, UUIDv4, v7, ULID の比較はこんなところでしょう。

フォーマット ソート可能性 単調増加性 ランダムさ程度
UUIDv1 基本なし(binary変換すればあり) なし (調べたが不明。MACアドレスをもとに生成されるため推測される)
UUIDv4 なし なし 122 bits
UUIDv7 あり あり 62 bits
ULID あり あり 80 bits

ULIDs and Primary Keys | Dave Allie の表に UUIDv1 を追加して記載しました。

別に考える必要のあること

その他参考文献