Lý do khiến Uber chuyển từ Postgres sang MySQL

uber-logo-696x392

Introduction

Mới đầu, Uber sử dụng ứng dụng backend nguyên khối được viết bằng Python và sử dụng Postgres để nhất quán dữ liệu. Sau một thời gian, cấu trúc của Uber đã có sự thay đổi rõ rệt, sang mô hình microservices và các data platform mới. Đặc biệt, trong nhiều trường hợp dùng đến Postgres trước đây, hiện đã có thể giải quyết bằng Schemaless (database sharding layer mới được xây dựng trên MySQL). Trong bài viết này, ta sẽ khám phá một vài khuyết điểm khi Uber vẫn còn dùng Postgres, và giải thích lý do vì sao phải xây dựng Schemaless và các dịch vụ backend khác trên nền MySQL.

I. Cấu trúc của Postgres

Những hạn chế của Postgres mà Uber gặp phải:

  • Kiến trúc không hiệu quả cho các truy vấn ghi vào (write)
  • Data replication không hiệu quả
  • Các lỗi liên quan tới table corruption
  • Hỗ trợ sao lưu MVCC nghèo nàn
  • Khó cập nhật phiên bản mới

On-Disk Format

Một relational database phải thực hiện được các công việc sau:

  • Cho phép insert/update/delete
  • Cho phép thay đổi schema
  • Triển khai cơ chế multiversion concurrency control (MVCC – đồng kiểm soát đa phiên bản, để các kết nối khác nhau có transactional view của dữ liệu tương ứng)

Việc hình dung cách kết hợp của những tính năng này, là một phần cốt yếu khi phải thiết kế cách database thể hiện data trên ổ cứng.

Immutable row data (dữ liệu bất biến), trong cách nói của Postgres, những hàng data bất biến này được gọi là “tuples”. Những tuples này được xác định độc nhất bởi ctid. Một ctid thế hiện vị trí on-disk của một tuple (như physical disk offset). Nhiều ctids có thể mô tả một hàng duy nhất (như khi tồn tại nhiều phiên bản của hàng, cho mục đích MVCC; hoặc khi phiên bản cũ của hàng vẫn chưa được autovacuum process tiếp quản). Tập hợp tuples có sắp sếp, sẽ hình thành một table. Bản thân tables đều có index, được sắp xếp dưới dạng cấu trúc cây nhị phân (thường là B-trees) giúp map các trường index đến một ctid chứa toàn bộ data tương ứng của nó.

Thông thường, những citds này rất rõ ràng với người dùng. Khi đã biết được cách hoạt động của citd, ta có thể hiểu được cấu trúc on-disk của Postgres tables. Để thấy được ctid hiện có của hàng, bạn có thể thêm ctid vào column list (danh sách cột) trong mệnh đề WHERE:

SELECT ctid, * FROM my_table LIMIT 1;

Để giải thích chi tiết, hãy xem thử ví dụ users table đơn giản sau. Với mỗi user, ta có auto-incrementing user_id primary key (key chính, user_id tự tăng khi insert), tên họ, năm sinh của user. Ta còn xác định compound secondary index (chỉ mục thứ cấp kép) dựa trên họ tên của user, và một secondary index khác dựa trên năm sinh của user. Sau đây là DDL (data definition language) để tạo một table như vậy:

CREATE TABLE users (
id SERIAL,
first TEXT,
last TEXT,
birth_year INTEGER,
PRIMARY KEY (id)
);
CREATE INDEX ix_users_first_last ON users (first, last);
CREATE INDEX ix_users_birth_year ON users (birth_year);

Dữ liệu tương ứng:

id first last birth_year
1 Blaise Pascal 1623
2 Gottfried Leibniz 1646
3 Emmy Noether 1882
4 Muhammad al-Khwārizmī 780
5 Alan Turing 1912
6 Srinivasa Ramanujan 1887
7 Ada Lovelace 1815
8 Henri Poincaré 1854

Như đã được đề cập, mỗi hàng ẩn chứa một ctid độc lập. Bởi vậy, ta có thể nhìn lại bảng trên theo cách sau:

ctid id first last birth_year
A 1 Blaise Pascal 1623
B 2 Gottfried Leibniz 1646
C 3 Emmy Noether 1882
D 4 Muhammad al-Khwārizmī 780
E 5 Alan Turing 1912
F 6 Srinivasa Ramanujan 1887
G 7 Ada Lovelace 1815
H 8 Henri Poincaré 1854

Primary key index giúp map ids đến ctids, được xác định như sau:

id ctid
1 A
2 B
3 C
4 D
5 E
6 F
7 G
8 H

B-tree được xác định trên trường id, và mỗi node trong B-tree chứa giá trị ctid. Để ý, trong trường hợp này, thứ tự sắp xếp của các trường trong B-tree lại tự nhiên giống với thứ tự trong bảng, đó là do ta sử dụng id tự gia tăng.

Hai secondary indexes trông cũng tương tự; điểm khác biệt chính là các trường này được lưu trữ theo thứ tự khác hẳn, vì B-tree phải được sắp xếp theo lexicographical order (một dạng của thứ tự bảng chữ cái Alphabet). Index (first, last) bắt đầu với phần tên (first) ở đầu bảng chữ cái đổ xuống:

first last ctid
Ada Lovelace G
Alan Turing E
Blaise Pascal A
Emmy Noether C
Gottfried Leibniz B
Henri Poincaré H
Muhammad al-Khwārizmī D
Srinivasa Ramanujan F

Tương tự, index birth_year được xếp theo thứ tự tăng dần:

birth_year ctid
780 D
1623 A
1646 B
1815 G
1854 H
1887 F
1882 C
1912 E

Như bạn thấy, trong cả hai trường hợp, trường ctid ở secondary index tương ứng không tăng theo thứ tự Alphabet, khác với trường hợp tự gia tăng ở primary key.

Giả dụ, ta cần cập nhật một record trong table này. Chẳng hạn như cập nhật trường năm sinh cho một năm sinh ước đoán khác của al-Khwārizmī, năm 770 sau công nguyên. Và như ta đã nói, tuples là bất biến. Bởi vậy, để cập nhật record, chúng ta phải thêm tuple mới vào table. Tuple này lại có một ctid ẩn mới, ta sẽ tạm gọi là I. Postgres cần phải phân biệt được tuple I mới và tuple D cũ. Postgres chứa trong mỗi tuple một version field và pointer đến tuple trước đó (nếu có). Như vậy, cấu trúc mới của table như sau:

ctid prev id first last birth_year
A null 1 Blaise Pascal 1623
B null 2 Gottfried Leibniz 1646
C null 3 Emmy Noether 1882
D null 4 Muhammad al-Khwārizmī 780
E null 5 Alan Turing 1912
F null 6 Srinivasa Ramanujan 1887
G null 7 Ada Lovelace 1815
H null 8 Henri Poincaré 1854
I D 4 Muhammad al-Khwārizmī 770

Miễn có hai phiên bản của hàng al-Khwārizmī, thì các index phải chứa entry cho cả hai hàng. Để đảm bảo ngắn gọn, chúng ta sẽ loại bỏ primary key index và chỉ hiển thị secondary index ở đây, như sau:

first last ctid
Ada Lovelace G
Alan Turing E
Blaise Pascal A
Emmy Noether C
Gottfried Leibniz B
Henri Poincaré H
Muhammad al-Khwārizmī D
Muhammad al-Khwārizmī I
Srinivasa Ramanujan F

Theo năm sinh:

birth_year ctid
770 I
780 D
1623 A
1646 B
1815 G
1854 H
1887 F
1882 C
1912 E

Ta đã thể hiện phiên bản cũ màu đỏ và phiên bản mới màu xanh lá cây. Sự thật là, Postgres sử dụng một trường khác để giữ phiên bản của hàng, nhằm xác định tuple nào là mới nhất. Trường thêm vào này giúp database xác định tuple của hàng nào sẽ tham gia vào quá trình giao dịch (có thể không được phép thấy phiên bản của hàng mới nhất).

Postgres_Tuple_Property_-768x365
Với Postgres, primary index và các secondary index tất cả đều trực tiếp chỉ về on-disk tuple offsets. Khi vị trí của một tuple thay đổi, tất cả index phải được cập nhật.

Replication

Để đảm bảo tính phục hồi dữ liệu khi crash, database của Uber đã duy trì một write-ahead log (WAL) và sử dụng WAL thực thi two-phase commit (commit hai giai đoạn). Database phải duy trì WAL này ngay cả khi không mở sao lưu streaming (streaming replication), vì WAL cần thiết cho tính an toàn và độ bền ACID của DB.

Khi nghiên cứu các sự kiện diễn ra khi database crash bất ngờ (như khi mất điện đột ngột), ta có thể hiểu thêm được về WAL. WAL giống như “sổ hộ” ghi chép những thay đổi mà database dự định thực hiện trên on-disk của table và index. Khi service Postgres khời động, process sẽ so sánh dữ liệu trong sổ hộ này với data thực có trên ổ cứng. Nếu sổ hộ chứa dữ liệu không được phản ánh trên ổ cứng, database sẽ sửa lại mọi tuple hoặc index data để phản ánh data do WAL chỉ ra. Sau đó, database sẽ tiếp tục rollback bất cứ data nào xuất hiện trên WAL, nhưng là từ transaction chưa bao giờ được thực hiện.

Postgres thực thi sao lưu streaming bằng cách gửi WAL trên database chính đến các bản sao lưu. Mỗi database sao lưu ngay lập tức phản hồi như thể đang trong trạng thái crash recovery, liên tục áp dụng WAL updates. Điểm khác biệt duy nhất giữa sao lưu streaming và crash recovery thực tế là bản sao ở chế độ “hot standby”, thực hiện read queries khi áp dụng streaming WAL, trong khi ở chế độ crash recovery mode thường từ chối nhận bất cứ queries nào cho đến khi database instance đã hoàn thành quy trình crash recovery.

Vì WAL trong thực tế được thiết kế cho mục đích crash recovery, nó có chứa thông tin bậc thấp (low-level) về on-disk updates. Nội dung của WAL hiện ở mức độ on-disk thực sự của row tuples và disk offsets của chúng (ví dụ như row ctids). Nếu bạn tạm dừng một Postgres chính và bản sao, khi bản sao đã hoàn toàn chạy xong, on-disk content thực tế trên bản sao sẽ khớp chính xác với content trên bản chính đến từng byte một. Bởi vậy, các công cụ như rsync có thể fix bản sao hỏng nếu bản sao bị lỗi thời (out of date) so với bản chính.

Write Amplification

Vấn đề đầu tiên với thiết kế của Postgres còn được gọi là write amplification (ghi khuếch đại). Thông thường, write amplication ám chỉ vấn đề với data được viết vào đĩa SSD: logical update nhỏ (chỉ vài bytes) bỗng nhiên nặng hơn, tốn kém hơn khi được dịch sang physical layer. Khuếch đại ghi trong Postgres cũng không khác gì như thế. Trong ví dụ trước khi ta chỉnh logical update sang năm sinh của al-Khwārizmī, chúng ta phải thực hiện ít nhất bốn physical updates:

  1. Viết tuple cho hàng mới trong tablespace
  2. Cập nhật primary key index để thêm record vào tuple mới
  3. Cập nhật (first, last) index để thêm record vào tuple mới
  4. Cập nhật index birth_year đề thêm record vào tuple mới

Trong thực tế, những cập nhật này chỉ phản ánh các writes trong tablespace chính, mỗi write này cần được phản ánh trên WAL nữa, vậy tổng số write trên ổ cứng thậm chí còn lớn hơn.

Điểm đáng chú ý ở đây là update 2 và 3. Khi ta cập nhật năm sinh cho al-Khwārizmī, chúng ta không thực sự thay đổi primary key của ông, hay thay đổi tên họ. Tuy nhiên, những thông số này vẫn cần phải được cập nhật, với việc tạo tuple hàng mới trong database cho record của hàng. Với những tables có một lượng lớn secondary index, những bước dư thừa này làm tạo áp lực thấy rõ lên hệ thống.

Vấn đề khuếch đại ghi này cũng tự động chuyển vào replication layer (lớp sao lưu), vì quá trình sao lưu xảy ra ở cấp độ on-disk. Thay vì phải sao lưu một logical record nhỏ, như “Đổi năm sinh cho ctid D sang 770” thay vào đó, database sẽ viết WAL entries cho cả 4 writes đã nói trên, và cả 4 WAL entries này sẽ nhân rộng đến cả mang lưới còn lại. Vì vậy, vấn đề khuếch đại ghi còn biến thành vấn đề về khuếch đại bản sao, và dòng dữ liệu sao lưu Postgres sẽ nhanh chóng trở nên cực kỳ khổng lồ, có khả năng chiếm lượng lớn băng thông.

Trong những trường hợp mà sao lưu Postgres chỉ đơn thuần xảy đến với một data center duy nhất, băng thông dành cho sao lưu sẽ không thành vấn đề. Tuy nhiên, khi quá trình sao lưu phải diễn ra giữa nhiều data centers khác nhau, ta sẽ gặp rắc rối lớn. Ví dụ, Uber mới đầu sử dụng physical servers tại một địa điểm thuê mướn ở West Coast. Để thuận lợi cho công tác phục hồi sau thảm họa, Uber thêm nhiều server nữa ở East Coast (cũng là thuê mướn). Theo thiết kế này, Uber có một Postgres instance chính cộng với các bản sao ở data center phía Tây và một loạt bản sao ở phía Đông. Chi phí cho các liên kết đa quốc gia có băng thông cao vô cùng đắt đỏ, và thậm chí khi tiền bạc dư dả, cũng khó có thể kiếm được một liên kết đa quốc gia mạnh mẽ như mạng cục bộ. Vấn đề băng thông còn gây nhiều khó khăn khi lưu trữ WAL.

Data Corruption

Trong một lần nâng cấp định kỳ database chính để tăng dung lượng, Uber chạm phải một lỗi trên Postgres 9.2. Vì lỗi này, một số record đáng lẽ phải được versioning mechanism (cơ chế đánh số phiên bản) đánh dấu là inactive, lại không đúng như vậy.

Query sau mô tả cách bug này ảnh hưởng đến table:

SELECT * FROM users WHERE id = 4;

Query này sẽ trả lại hai records: hàng al-Khwārizmī ban đầu (có năm sinh 780CE), và hàng al-Khwārizmī mới (có năm sinh 770CE). Nếu chúng ta thêm ctid vào danh sách where, ta sẽ thấy các giá trị ctid khác nhau cho hai record được trả về.

Vấn đề này cực kỳ khó chịu vì nhiều lý do. Thứ nhất, chúng ta không thể dễ dàng biết được vấn đề này đã ảnh hưởng bao nhiêu hàng. Các kết quả trùng lặp do database trả về gây ra các lỗi trên application. Vì bug ảnh hưởng đến tất cả server, các hàng bị hỏng sẽ khác với những replica instances, có nghĩa là một bản sao của hàng X có thể vẫn còn tốt nhưng hàng Y lại bị hỏng. Trong thực tế, Uber cũng không chắc chắn về số lượng bản sao có dữ liệu bị hỏng, và liệu vấn đề có ảnh hưởng đến bản chính luôn hay không. Tuy vậy, Uber cho biết, có thể vấn đề chỉ xảy ra với một vài hàng trên database thôi.

Họ cũng rất lo ngại vì quá trình sao lưu diễn ra ở physical level (cấp độ vật lý), nên các index trong database có nguy cơ bị hủy hoàn toàn. B-trees có một đặc tính quan trọng, chúng phải liên tục được rebalanced (tái cân bằng), và những thao tác tái cân bằng này có thể hoàn toàn thay đổi cấu trúc của cả cây vì các sub-trees (cây phụ) được chuyển đến vị trí on-disk mới. Nếu di chuyển không đúng data, phần lớn cây sẽ trở thành “phế liệu”.

Đến cùng, Uber cũng lần ra nguyên do gây bug, và đã xác định database chính vừa nâng cấp không có hàng nào bị hỏng cả. Uber liền sửa được sự cố này ở các bản sao, bằng cách đồng bộ tất cả bản sao từ snapshot của bản chính, một quá trình mệt mỏi; họ chỉ đủ tài nguyên xử lý một vài bản sao một lúc.

Uber chỉ bắt gặp bug này duy nhất nhất ở Postgres 9.2 trước khi được xử lý hoàn toàn ở các phiên bản sau này. Tuy nhiên, họ vẫn khá dè chừng, vì kiểu lỗi này ngốn rất nhiều thời gian và tiền bạc. Nhỡ đâu những phiên bản release tiếp theo lại xuất hiện thì sao, cộng thêm lối làm việc quá phụ thuộc vào bản sao này, sự cố hoàn toàn có thể lan ra tất cả database.

Replica MVCC

Postgres không thực sự hỗ trợ sao lưu MVCC. Khi các bản sao áp dụng WAL updates, chúng sẽ có một bản copy on-disk giống y với bản chính. Uber nhận thấy vấn đề nằm ở đây.

Nếu một streaming có giao dịch mở, updates đến database sẽ bị chặn nếu chúng ảnh hưởng đến các hàng trong giao dịch này. Trong trường hợp này, Pestgres tạm ngưng WAL application thread (dòng đảm nhiệm áp dụng WAL) cho đến khi giao dịch kết thúc. Đây sẽ là vấn đề lớn nếu giao dịch mất quá nhiều thời gian, vì bản sao có thể tụt sau bản chính rất nhiều. Bởi vậy, Postgres áp dụng timeout trong trường hợp này: nếu một giao dịch chặn việc áp dụng WAL trong một khoảng thời gian xác định, Postgres sẽ triệt tiêu giao dịch đó.

Cách thiết kế này đồng nghĩa với việc các bản sao thường có thể đi sau bản chính vài giây, và bởi vậy rất dễ viết code cho ra nhiều giao dịch bị hủy. Vấn đề này có thể không rõ ràng với application developers (những lập trình viên viết code có điểm bắt đầu và kết thức của giao dịch khá mù mịt). Ví dụ như, giả sử một lập trình viên có đoạn code với nhiệm vụ email biên lai cho người dùng. Dựa trên cách viết, đoạn code có thể ngầm chứa giao dịch database được mở cho đến sau khi email gửi xong. Để ‘code giữ giao dịch database’ mở trong khi thực hiện ‘block những I/O không liên quan’ luôn hại nhiều hơn lợi.

Postgres Upgrades

Vì các record sao lưu làm việc ở mức độ vật lý, nên ta không thể sao lưu dữ liệu giữa các phiên bản Postgres khác nhau. Database chính chạy Postgres 9.3 không thể sao lưu được bản sao chạy trên Postgres 9.2, hay ngược lại.

Uber áp dụng cách sau để cập nhật từ phiên bản Postgres GA này sang phiên bản GA khác:

  • Shut down database chính.
  • Chạy lệnh pg_upgrade lên database chính, cập nhật master data này. Với các database lớn, khâu này có thể mất nhiều tiếng đồng hồ và không thể thực hiện xử lý traffic từ database chính.
  • Mở database chính trở lại.
  • Tạo snapshot cho database chính. Bước này hoàn toàn copy tất cả data dừ database chính, nên cũng phải mất nhiều tiếng nữa với database lớn.
  • Xóa sạch mỗi bản sao và phục hồi snapshot mới từ bản chính sang bản sao.
  • Mang mỗi bản sao trở lại hệ thống sao lưu. Đợi bản sao vừa hoàn toàn đáp ứng được với cập nhật từ bản chính, vừa được phục hồi.

Uber bắt đầu với Postgress 9.1 và hoàn thành thành công quá trình cập nhật để chuyển sang Postgres 9.2. Tuy nhiên, quá trình trên mất quá nhiều thời gian, cộng thêm sự phát triển quy mô công ty khi Postgres 9.3 xuất hiện, thời gian chuyển đổi sẽ càng lâu hơn. Vì lý do này, các legacy Postgres instances của Uber vẫn dang chạy 9.2 cho đến nay, ngay cả khi hiện 9.5 Postgres GA đã ra mắt.

Nếu đang chạy Postgress 9.4 hoặc thấp hơn, bạn có thế sử dụng pglogical, giúp bổ sung logical replication layer (lớp sao lưu logic) cho Postgres. Khi dùng pglogical, bạn có thể sao lưu data giữa các phiên bản Postgres khác nhau. Nói cách khác, các bạn có thể nâng từ 9.4 lên 9.5 mà không phải chịu downtime quá lâu. Cách làm này hiển nhiên vẫn tồn đọng khuyêt điểm vì pglogical không được tích hợp vào main-line tree của Postgres, và pglogical không phải là lựa chọn cho những người dùng Postgres ở phiên bản cũ hơn.

II. Cấu trúc MySQL

Không chỉ bàn về hạn chế của Postgres, Uber còn giải thích tầm quan trọng của MySQL cho các project mới của Uber (như Schemaless). Trong nhiều trường hợp, Uber cảm thấy MySQL lại phù hợp hơn. Để thấy được sự khác biệt, Uber nghiên cứu cấu trúc của MySQL và đối chiếu Với Postgres.

InnoDB On-Disk Representation

Giống Postgres, InnoDB hỗ trợ các tính năng nâng cao như MVCC và mutable data. Chúng ta sẽ không bàn về on-disk format, mà về những điểm khác biệt cơ bản với Postgres.

Khác biệt cấu trúc quan trọng nhất là: Postgres trực tiếp map index records đến các vị trí on-disk, còn InnoDB duy trì cấu trúc thứ cấp. Thay vì giữ pointer đến vị trí hàng on-disk (như ctid trong Postgres), InnoDB secondary index records sẽ giữ pointer đến primary key value. Vì thế, secondary index trong MySQL sẽ liên kết index keys với primary keys:

first last id (primary key)
Ada Lovelace 7
Alan Turing 5
Blaise Pascal 1
Emmy Noether 3
Gottfried Leibniz 2
Henri Poincaré 8
Muhammad al-Khwārizmī 4
Srinivasa Ramanujan 6

Để thực hiện index lookup lên (first, last) index, chúng ta cần có hai lookups. Lookup đầu tiên quét trong table và tìm primary key index để tìm vị trí on-disk cho hàng.

Như vậy, InnoDB có hơi kém lợi thế so với Postgres khi thực hiện secondary lookup, vì cả hai index phải được tìm với InnoDB so với chỉ một index như ở Postgres. Tuy nhiên, vì dữ liệu được đơn giản hóa, row updates chỉ cần phải update những index record thực sự được row update thay đổi. Thêm vào đó, InnoDB thường thực hiện row update tại chỗ. Nếu các giao dịch cũ cần phải tham chiếu một hàng để phục vụ MVCC, MySQL sẽ copy hàng cũ vào khu vực đặc biệt gọi là rollback segment.

Hãy xem thử điều gì sẽ xảy ra khi ta update năm sinh của al-Khwārizmī. Nếu có đủ không gian, trường năm sinh ở hàng có id 4 sẽ được update tại chỗ (trong thực tế, cập nhật này luôn diễn ra tại chỗ, vì năm sinh là một số nguyên chiếm giữ lượng không gian cụ thể). Index năm sinh cũng được cập nhật tại chỗ để phản ánh ngày mới. Data của hàng cũ được sao chép đến phân đoạn rollback. Primary key index không nhất thiết phải cập nhật, (first, last) name index cũng vậy. nếu ta có lượng lớn index trên table này, chúng ta chỉ phải update các index thực sự trên trường birth_year. Vậy giả sử ta có các index trên các trường như signup_date,last_login_time,… chúng ta không cần phải update những index này, trong khi Postgres lại cần phải update.

Thiết kế này còn giúp vacuuming và compaction được hiệu quả hơn. Tất cả hàng có đủ điều kiện vacuum sẽ trực tiếp có sẵn trong rollback segment. Theo so sánh, quá trình autovacuum trong Postgres phải thực hiện full table scan để xác định các hàng đã bị xóa bỏ.

MySQL_Index_Property_-768x373
MySQL sử dụng một lớp vô hướng gia cố: secondary index records chỉ đến primary index records, và bản thân primary index nắm giữ vị trí hàng on-disk. Nếu một offset của hàng thay đổi, chỉ cần update primary index thôi.

Replication

MySQL hỗ trợ nhiều chế độ sao lưu khác nhau:

  • Statement-based replication (bản sao dựa trên câu lệnh) ví dụ như, sự sao lưu ngay lập tức của các câu lệnh chữ như: UPDATE users SET birth_year=770 WHERE id = 4
  • Row-based replication (sao lưu theo hàng) sao lưu các records hàng đã thay đổi
  • Mixed replication (sao lưu hỗn hợp) phối hợp hai chế độ trên

Các chế độ sao lưu này đều có lợi và hại khác nhau. Bản sao dựa trên câu lệnh thường gọn nhẹ nhất nhưng yêu cầu các bản sao phải áp dụng nhiều câu lệnh tốn kém để update chỉ lượng nhỏ data. Sao lưu theo hàng, tương tự với sao lưu WAL trong Postgres, thường rườm rà hơn, nhưng mang lại kết quả cập nhật bản sao hiệu quả và dễ dự đoán hơn.

Trong MySQL, chỉ primary index có pointer đến các offset on-disk của hàng. Điều này gây hậu quả khá quan trọng đến bản sao. MySQL replication stream (dòng sao chép MySQL) chỉ cần chứa thông tin về logical update đến hàng. Các update đến bản sao ghi nhận “Đổi timestamp hàng X từ T_1 sang T_2”.

Ngược lại, Postgres replication stream (dòng sao lưu Postgres) lại chứa các thay đổi vật lý, như “tại disk offset 8,382,491 viết bytes XYZ”. Với Postgres, mỗi thay đổi vật lý được thực hiện đến ổ cứng cần phải được gộp vào WAL stream. Các thay đổi logic nhỏ lẻ (như việc update một timestamp) càng tăng thêm độ quan trọng của thay đổi on-disk: Postgres phải nhập tuple mới và update tất cả index để chỉ đến tuple đó. Vì thế, nhiều thay đổi sẽ được đặt vào WAL stream. Điểm thiết kế khác biệt này đồng nghĩa với việc binary log của bản sao MySQL sẽ phần nào gọn nhẹ hơn PostgreSQL WAL stream.

Cách vận hành của mỗi dòng sao lưu cũng có ảnh hưởng nghiêm trọng lên cách MVCC làm việc với các bản sao. Vì dòng sao lưu MySQL có logical updates, các bản sao có thể có ngữ nghĩa MVCC; do đó read queries trên replicas sẽ không block dòng sao lưu. Ngược lại, Postgres WAL stream chứa các thay đổi vật lý on-disk, nên các bản sao Postgres không thể áp dụng lên các update bản sao mâu thuẫn với read queries, vì vậy chúng không thể triển khai MVCC.

Với cấu trúc sao lưu của MySQL, nếu bug gây ra table corruption, vấn đề thường sẽ gây lỗi không quá nghiêm trọng. Quá trình sao lưu thường diễn ra tại logical layer, nên một số thao tác như: cân bằng B-tree sẽ không cách nào làm index bị lỗi. Câu lệnh bị bỏ qua (hoặc áp dụng đến hai lần), là một sự cố sao lưu MySQL điển hình. Việc này có thể khiến data bị mất hoặc vô hiệu lực, nhưng sẽ không làm thất thoát database.

Cuối cùng, cấu trúc sao lưu của MySQL không quá quan trọng việc sao lưu giữa các phiên bản MySQL khác nhau. MySQL chỉ lên phiên bản nếu định dạng sao lưu thay đổi; giữa các phiên bản MySQL, điều này khá bất thường. Định dạng sao lưu logic của MySQL còn chỉ ra rằng, các thay đổi on-disk trong storage engine layer (lớp công cụ lưu trữ) không ảnh hướng đến dịnh dạng sao lưu. Cách thường dùng để nâng cấp MySQL là: áp dụng bản update đến từng bản sao lưu một, và khi đã up hết tất cả sao lưu, bạn hãy thăng cấp một bản sao bất kỳ lên làm bản chính. Cách này vừa giúp nâng cao MySQL thật đơn giản, vừa không mất downtime.

Buffer Pool

Trước hết, ở hai database này, caching làm việc rất khác. Postgres cấp phát một phần bộ nhớ cho internal cache, nhưng những cache này thường rất nhỏ khi so sánh với tổng lượng bộ nhớ của bộ máy. Để tăng hiệu năng, Postgres cho phép kernel tự động cache disk data đã truy cập gần đây, thông qua page cache. Ví dụ như, các bản sao Postgres lớn nhất có 768GB bộ nhớ, nhưng thực sự chỉ có 25GB bộ nhớ đó là RSS memory. Như vậy, ta còn đến hơn 700 GB bộ nhớ dành cho Linux page cache.

Vấn đề với thiết kế này là: truy cập data qua page cache lại tốn kém hơn truy cập bộ nhớ RSS. Để tra cứu data từ ổ cứng, Postgres process sẽ phát các system call lseek(2) và read(2) để cấp phát data. Mỗi system call sẽ phải chịu một context switch, context switch này thường đắt đỏ hơn việc truy cập data từ main memory. Trong thực tế, Postgres cũng không hoàn toàn tối ưu về mặt này: Postgres không tận dụng system call pread(2), kết hợp hai thao tác seek + read thành một system call duy nhất.

Sau khi đối chiếu, InnoDB storage engine (công cụ lưu trữ InnoDB) sẽ tự thực thi LRU của nó trong InnoDB buffer pool. Khá giống với Linux page cache nhưng được thực thi trong userspace. InnoDB buffer pool, tuy phức tạp hơn thiết kế của Postgres, nhưng lại có nhiều thế mạnh lớn:

  • Có thể tích hợp custom LRU design, như: có thể xác định các pathological access patterns (mô hình truy cập bệnh lý) hạn chế thiệt hại xuống thấp nhất.
  • Ít dính đến context switch hơn. Data được truy cập thông qua InnoDB buffer pool không yêu cầu bất cứ user/kernel context switch nào. Trường hợp phản ứng tệ nhất là sự xuất hiện của TLB miss, khá ít và có thể được giảm thiểu với huge pages.

Connection Handling

Thông qua việc spawn thread-per-connection, MySQL có thể thực hiện các concurrent connection (kết nối đồng thời). Cách này ít overhead hơn; mỗi thread sẽ dành một phần memory overhead cho stack place, cộng thêm một số bộ nhớ được cấp phát trên heap cho connection-specific buffers (buffer riêng cho connection). Cũng không quá bất ngờ khi MySQL đạt quy mô đến 10.000 concurrent connection. Bản thân nhiều MySQL instance của Uber cũng đã gần đạt đến con số này.

Tuy nhiên, Postgres lại dùng thiết kế process-per-connection, tốn kém hơn thread-per-connection nhiều. Việc fork một process (mới) chiếm dụng nhiều bộ nhớ hơn là spawn thread mới. Hơn nữa, IPC giữa các process cũng tốn kém hơn giữa thread nhiều. Postgres 9.2 sử dụng các primitive System V IPC cho IPC, thay vì futexes gọn nhẹ với thread. Futexes cũng nhanh hơn System V IPC vì trong trường hợp futex không được thỏa mãn, thường sẽ không cần phải tạo context switch nữa.

Bên cạch các vấn đề về bộ nhớ và IPC overhead, Postgres nhìn chung hỗ trợ rất kém trong việc xử lý lượng lớn kết nối, ngay cả khi vẫn còn đủ bộ nhớ. Uber cũng đã rất chật vật khi phải mở rộng quá trăm kết nối với Postgres. Tài liệu rất khuyến khích sử dụng cơ chế pooling kết nối out-of-process, để xử lý lượng lớn kết nối trong Postgres, nhưng lại không đưa ra lý do tại sao lại làm vậy. Theo đó, Uber cũng đạt được kha khá thành công khi sử dụng pgbouncer để thực hiện connection pooling trong Postgres. Tuy nhiên, họ vẫn thi thoảng gặp phải application bugs trong các dịch vụ backend, khiến các dịch vụ này phải mở thêm active connections (tường là kết nối “idle in transaction”) hơn mức cần thiết; từ đó khiến downtime càng lâu hơn.

Lời kết

Trong giai đoạn đầu của Uber, Postgres đã hoàn thành xuất sắc nhiệm vụ của mình, nhưng lại tỏ ra không phù hợp khi công ty mở rộng quy mô. Hiện nay, tuy Uber vẫn còn một vài legacy Postgres instance, nhưng phần lớn database đã chuyển sang MySQL (kết hợp với Schemaless layer), hoặc các database NoSQL (như Cassandra) trong các trường hợp đặc biệt.

Link bài viết gốc:
https://eng.uber.com/mysql-migration/
https://eng.uber.com/schemaless-part-one/
https://backchannel.org/blog/friendfeed-schemaless-mysql

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s