PostgreSQL 的 Job Queue、Application Lock 以及 Pub/Sub

Hacker News Daily 上看到一篇講 PostgreSQL 做 Job Queue、Application Lock 以及 Pub/Sub 的方法:「Do You Really Need Redis? How to Get Away with Just PostgreSQL」,對應的討論在「Do you really need Redis? How to get away with just PostgreSQL (atomicobject.com)」這邊可以翻到。

拿 PostgreSQL 跑這些東西的確有點浪費,不過如果是自己的專案,不想要把 infrastructure 搞的太複雜的話,倒是還不錯。

首先是 Job Queue 的部份,從他的範例看起來他是在做 async job queue (不用等回傳值的),這讓我想到很久前寫的 queue service (應該是 2007 年與 2012 年都寫過一次),不過我是用 MySQL 當作後端,要想辦法降低 InnoDB 的 lock 特性。

async job queue 設計起來其實很多奇怪的眉角,主要就是在怎麼處理失敗的狀態。大多數的需求可以放到兩個種類,最常見用的是 at-least-once,保證最少跑一次,大多數從設計上有設計成 idempotence 的都可以往這類丟,像是報表類的 (重複再跑一次昨天的報表是 OK 的),另外每天更新會員狀態也可以放在這邊。

另外少見一點的是 at-most-once 與 exactly-once,最多只跑一次與只跑一次,通常用在不是 idempotence 的操作上,像是扣款之類的,這邊的機制通常都會跟商業邏輯有關,反正不太好處理...

第二個是 Application Lock,跨機器時的 lock 機制,量沒有很大時拿 PostgreSQL 跑還行,再大就要另外想辦法了,馬上想到的是 ZooKeeper,但近年設計的系統應該更偏向用 etcdConsul 了...

最後提到的 Pub/Sub,一樣是在量大的時候拿 PostgreSQL 跑還行,更大的時候就要拿 Kafka 這種專門為了效能而設計出來的軟體出來用...

ALB 支援 Sticky Session

又是一個以為很久前就已經支援,但實際上沒支援的功能...

ALB 支援使用 cookie 實現 sticky session 功能:「Application Load Balancer now supports Application Cookie Stickiness」。

使用者的 session 通常會使用 cookie 記錄,而如果有多台 server 提供服務時,session 裡的資訊就需要找一個 shared session storage 放,以確保使用者在連到不同的 server 時都還是可以讀到對應的 session,比較傳統的方案就是直接把 session 塞進資料庫,後來發展出 memcached 或是 Redis 可以用。

但有些買來的軟體並沒有考慮到這點 (常常都是內部系統),導致前面放 load balancer 時,必須想個辦法記錄使用者使用後端的哪台機器,這樣就可以在後端不支援 shared session storage 的情況下,還是可以讓應用正常運作。

透過 cookie 實做的 sticky session 算是蠻常見的作法,只是以為早就有了...

AWS 上的 Redis 服務支援 Global Replication

AWS 上的 Redis 服務 (Amazon ElastiCache) 推出了 Global Datastore,也就是跨區同步的功能:「Now Available: Amazon ElastiCache Global Datastore for Redis」。

技術上會分成 read endpoint 與 write endpoint (在最後面的操作可以看到 $US_WEST_1_CLUSTER_READWRITE$US_WEST_2_CLUSTER_READONLY),所以算是蠻常見的 primary-secondary 架構。

不過我基本上不太愛 Redis,能用 Memcached 解決的偏好用 Memcached 解。不過如果把 Redis 當作是一種雲端服務的話,倒是還可以接受...

Stripe 將 Redis 單機版轉到 Cluster 版本上降低了錯誤率

在「Scaling a High-traffic Rate Limiting Stack With Redis Cluster」這邊提到了 StripeRedis 單機版轉移到 10 個節點的 cluster 版本,然後錯誤率大幅下降:

Stripe’s rate limiters are built on top of Redis, and until recently, they ran on a single very hot instance of Redis. The server had followers in place for failover, but at any given time, one node was handling every operation.

We eventually solved it by migrating to a 10-node Redis Cluster.

另外也可以看出來,在轉移到 cluster 版本後有不少要注意的,像是因為 sharding 而需要調整平衡性。另外是 cluster 模式下寫入的 confirmation 跟一般預期的不太一樣,不過這對於 rate limit 的應用還好,可以接受某種程度的掉資料...

ElastiCache 支援 r4.* 了

每年 AWS re:Invent 要到的時候就會有很多新的消息出來,Amazon ElastiCache 的團隊應該也是配合著這一波放出消息:「Amazon ElastiCache Now Supports the R4 Node Family」。

包括了 memcachedRedis 都支援了:

Amazon ElastiCache now supports R4 node types. R4 nodes are optimized for latency sensitive and memory intensive workloads. They come in six sizes, providing 12.3GiB to 407GiB of available in-memory capacity. By setting up a 15-shard cluster for Redis, you can scale up to 6.1TiB of in-memory capacity. For Memcached, you can set up a 20-node cluster to support up to 8.14 TiB in-memory workloads. Equipped with the Intel Broadwell processor, and improved networking, R4 node family offers superior performance over the popular R3 node family.

這兩個應用都是看記憶體吃飯的...

Amazon ElastiCache 可以讓你動態改 Redis 大小

Amazon ElastiCacheRedis 可以動態改大小了 (Online Resizing):「Amazon ElastiCache Update – Online Resizing for Redis Clusters」。

不過目前看起來只有 Cluster Mode 的才支援:

設定要加 sharding:

然後就開始跑:

另外也可以 rebalance:

這樣就省了一些功夫...

Reddit 在處理 Page View 的方式

Reddit 說明了他們如何處理 pageview:「View Counting at Reddit」。

以 Reddit 的規模有提到兩個重點,第一個在善用 RedisHyperLogLog 這個資料結構,當量大的時候其實可以允許有微小的誤差:

The amount of memory varies per implementation, but in the case of this implementation, we could count over 1 million IDs using just 12 kilobytes of space, which would be 0.15% of the original space usage!

維基百科上有說明當資料量在 109 這個等級時,用 1.5KB 的記憶體只有 2% 的誤差值:

The HyperLogLog algorithm is able to estimate cardinalities of > 109 with a typical error rate of 2%, using 1.5 kB of memory.

第二個則是寫入允許短時間的誤差 (pageview 不會即時反應),透過批次處理降低對 Cassandra cluster 的負荷:

Writes to Cassandra are batched in 10-second groups per post in order to avoid overloading the cluster.

可以注意到把 Redis 當作 cache 層而非 storage 層。

主要原因應該跟 Redis 定位是 data structure server 而非 data structure storage 有關 (可以從對 Durability 的作法看出來),而使用 Cassandra 存 key-value 非常容易 scale,但讀取很慢。剛好兩個相輔相成。

GitHub 重新定位 Redis 的功能...

GitHub Engineering 說明了他們為什麼改變 Redis 的使用情境:「Moving persistent data out of Redis」。

GitHub 裡面,Redis 有兩種不同的情境,一種叫做 transient Redis,只用做 cache:

We used it as an LRU cache to conveniently store the results of expensive computations over data originally persisted in Git repositories or MySQL. We call this transient Redis.

另外一種則是打開 persistence 功能,叫做 persistent Redis:

We also enabled persistence, which gave us durability guarantees over data that was not stored anywhere else. We used it to store a wide range of values: from sparse data with high read/write ratios, like configuration settings, counters, or quality metrics, to very dynamic information powering core features like spam analysis. We call this persistent Redis.

這邊講的是 persistent Redis 被換成用 MySQL (InnoDB) 儲存:

Recently we made the decision to disable persistence in Redis and stop using it as a source of truth for our data. The main motivations behind this choice were to:

  • Reduce the operational cost of our persistence infrastructure by removing some of its complexity.
  • Take advantage of our expertise operating MySQL.
  • Gain some extra performance, by eliminating the I/O latency during the process of writing big changes on the server state to disk.

For the majority of callsites, we replaced persistent Redis with GitHub::KV, a MySQL key/value store of our own built atop InnoDB, with features like key expiration. We were able to use GitHub::KV almost identically as we used Redis: from trending repositories and users for the explore page, to rate limiting to spammy user detection.

後面講了不少轉換的過程 (還包含了某些功能的改寫),但沒有講的太清楚為什麼不繼續使用 Redis。

目前只能就提到的三點問題來看,persistent 的 i/o 成本可能太高?而且難以再壓榨效能出來?而相反的,InnoDB 已經花了很多力氣在上面,直接拿來用反而可以解決問題?

不過看得出來這個轉換還是花了不少力氣,看得出來有些 application 使用 Redis 的模式不能直接搬到 InnoDB 上,花了時間改寫...

Netflix 開發的 Delayed Queue

原來這個叫做 Delayed Queue,難怪之前用其他關鍵字都找不到什麼資料... (就不講其他關鍵字了 XD)

Netflix 發表了他們自己所開發的 Delayed Queue:「Distributed delay queues based on Dynomite」。

本來的架構是用 Cassandra + Zookeeper 來做:

Traditionally, we have been using a Cassandra based queue recipe along with Zookeeper for distributed locks, since Cassandra is the de facto storage engine at Netflix.

但可以馬上想到不少問題,就如同 Netflix 提到的:

Using Cassandra for queue like data structure is a known anti-pattern, also using a global lock on queue while polling, limits the amount of concurrency on the consumer side as the lock ensures only one consumer can poll from the queue at a time.

所以就改放到 Netflix 另外開發的 Dynamite 上:

Dynomite, inspired by Dynamo whitepaper, is a thin, distributed dynamo layer for different storage engines and protocols. Currently these include Redis and Memcached. Dynomite supports multi-datacenter replication and is designed for high availability.

後端是 RedisMemcached 的系統,可以對抗整個機房從 internet 上消失的狀態。

在設計上則是「保證會跑一次」,也就是有可能會有多次的情況,用 Dyno Queues 系統的人必需要考慮進去:

4. At-least-once delivery semantics

雖然整篇講的頗輕鬆,但實際看起來還是很厚重... 暫時還是不會用吧 :o

對各類 Message Queue 的效能測試

在「Benchmarking Message Queue Latency」這篇看到作者測了一輪 Message Queue 軟體:

RabbitMQ (3.6.0), Kafka (0.8.2.2 and 0.9.0.0), Redis (2.8.4) pub/sub, and NATS (0.7.3)

測試包括了從一個 9 到六個 9 的 latency (i.e. 90%、99%、99.9%、99.99%、99.999%、99.9999%),另外也測了 message 大小帶來的效能差異。

99.9% 表示 1/1000,而 99.99% 表示 1/10000,如果差距跟 90% 很大,表示系統反應時間會很不一致。另外有些 Message Queue 軟體有 disk persistence 的功能,也因為寫入資料,會看到更大的差距。

善用或是避開這些特性去規劃才能減少問題,像是關掉 disk persistence 之類的方法。