Đổi ngôn ngữ và runtime

Chủ đề Dành cho thành viên
Bài đọc thử: 2/3 · còn 1 bài miễn phí · Xem gói thành viên

Bài toán

Ngôn ngữ ban đầu được chọn để làm nhanh. Khi lớn lên, nó có thể thành nút thắt về hiệu năng, latency, hoặc tổ chức code. Có ba hướng: tối ưu runtime đang dùng, cải tiến dần, hoặc viết lại có phạm vi.

Diễn tiến theo thời gian

  • 2010→2013 · Twitter: Ruby → JVM (Scala/Java). Lý do:

    • mỗi process Rails xử lý một request, chỉ 200–300 request/giây/host;
    • đội đã có kinh nghiệm JVM (search, Flock).

    Sau khi viết lại: 10–20K request/giây/host. Họ viết lại theo từng service chứ không một lần. Twitter 2013

  • 2010→2013 · Go: Playground chạy code không tin cậy trong Native Client (không mạng, không file); runtime riêng có đồng hồ giả nhảy tới timer sớm nhất khi mọi goroutine bị chặn, file system và mạng giả ở tầng syscall, output kèm mốc thời gian để client phát lại đúng nhịp. Go 2013

  • 2011→2014 · WhatsApp (qua HighScalability): bắt đầu từ ejabberd (Erlang), viết lại phần lớn, đổi XMPP sang giao thức riêng và vá chính máy ảo BEAM (khoá, bộ cấp phát, tạm dừng GC khi hàng đợi dài). Theo đội WhatsApp, số server nhỏ là nhờ Erlang; HighScalability lưu ý lượng patch làm nhận định này bớt chắc. Facebook Chat từng dùng Erlang (2009) rồi bỏ vì khó tuyển người. WhatsApp 2014

  • 2012 · WhatsApp: Erlang trên FreeBSD; một máy giữ hơn 2 triệu kết nối. Bài không giải thích lý do chọn. WhatsApp 2012

  • 2012→2024 · Go: math/rand bị khoá bởi cam kết tương thích, kể cả chuỗi số theo seed (Go 1.2 đổi thử và làm hỏng test người dùng). Go 1.20 tự seed bộ sinh toàn cục và thay bằng bộ sinh theo thread, bỏ mutex; phần không sửa tương thích được (bộ sinh mới PCG/ChaCha8, interface, thuật toán Lemire) vào math/rand/v2 (Go 1.22). Go 2024

  • 2012→2020 · LinkedIn: thư viện thí nghiệm Lix Engine viết bằng Clojure (2012): lazy sequence chiếm ít nhất 30% heap service dùng nó, GC pause khoảng 0,5 giây, gần như không ai còn viết được Clojure → viết lại bằng Java (cây đánh giá trong ba mảng phẳng, sinh code thay reflection): nhanh 20 lần, giải phóng hơn 4 TB RAM toàn fleet. LinkedIn 2020

  • ~2014–2015 · Allegro: khi chia thành microservices theo DDD thì chuyển từ PHP sang JVM (chủ yếu Java). Cái giá họ nêu: chi phí học, phải tuyển thêm developer Java. Allegro 2015

  • 2014→2021 · ByteDance: Go được đưa vào cho push service giữ kết nối lâu rồi lan ra nhờ framework chung (Kite 2016, Kitex 2020, hơn 30.000 microservice năm 2021). ByteDance 2022 Tối ưu tiếp (2019–2023): codec sinh code và JIT hạ phần CPU cho codec từ 3,58% còn 0,78%; gctuner nâng GOGC khi bộ nhớ còn dưới ngưỡng (kẹp 50–500) vì GC mặc định chạy mỗi lần heap gấp đôi dù máy còn nhiều RAM; lưu ý cgroup giới hạn thời gian CPU chứ không phải số CPU. ByteDance 2024

  • 2014 · LinkedIn: feed data platform: tune GC bằng prototype chạy lại workload production; ParNew/CMS thắng G1 (G1 tốn CPU và bộ nhớ hơn); AlwaysPreTouch, swappiness=0, không dùng mlock; p99.9 còn 60 ms. LinkedIn 2014

  • 2014 · WhatsApp (qua HighScalability): khoảng 10 người làm Erlang cho 465 triệu user; tiếp tục vá BEAM/OTP: nhiều timer wheel thay một (hàng trăm nghìn lần đặt timer/giây), round-robin file I/O bất đồng bộ, nhiều transaction manager cho Mnesia. Theo Rick Reed, Erlang cho phép ít node vì SMP scale tốt. WhatsApp 2014

  • 2015→2018 · Pinterest: backend chủ yếu Java; các hệ mới cần tail latency thấp, nặng CPU nên viết bằng C++11 + Folly + RocksDB. Đầu tư ban đầu chậm (thư viện nền, build/release) nhưng về sau đáng. Pinterest 2018

  • 2015→2021 · Zerodha: Kite v1 viết bằng Python (2015); ticker viết lại ít nhất 5 lần khi đội giỏi Go hơn; Console (báo cáo trên hàng trăm tỷ dòng) bỏ hẳn bản Python, viết lại bằng Go. Nguyên tắc: interface ổn định để thay được phần bên dưới, và "sửa thứ sắp hỏng". Zerodha 2021

  • 2015→2018 · Dropbox (desktop): Python 2 → 3 cho hơn 1 triệu dòng, hàng trăm triệu bản cài, phát hành hai tuần một lần: bỏ freezer, nhúng runtime sau thư viện libdropbox_bootstrap, rồi ship cả hai runtime cạnh nhau ("Hydra"), chọn bằng feature gate; CI chặn hồi quy Python 3; mở dần nhân viên → Beta → Stable, mọi bug migration sửa xong mới mở rộng; 7 tháng xong. Dropbox 2018

  • 2015 · Jane Street: thay vì pool object, sửa GC của OCaml (aging, incremental hơn, tách major slice khỏi minor GC, cho ứng dụng tự lên lịch GC lúc rảnh): tail latency giảm 3 lần trên một ứng dụng production. Jane Street 2015

  • 2015→2026 · WhatsApp: thư viện media wamedia (C++) được dùng kiểm file trước khi tới thư viện hệ điều hành từ thời Stagefright (2015); năm 2026 viết lại song song bằng Rust (160K dòng C++ → 90K dòng Rust kể cả test), nhanh hơn và ít bộ nhớ hơn, rollout tới hàng tỉ thiết bị. Trở ngại: kích thước binary và hệ build đa nền tảng. Mã mới mặc định dùng ngôn ngữ an toàn bộ nhớ. WhatsApp 2026

  • 2015 · Dropbox: Python 3 cho app desktop hơn 1 triệu dòng: Hack Week 2015 và 2016, CI chạy test + mypy chế độ Python 3, bật dần từng file test, code viết chạy cả 2 và 3; lỗi chủ yếu ở str/bytes; độ phủ mypy 35% → 63%. Dropbox 2019

  • 2016 · Grab: Rails/Node → Go theo từng endpoint, với load test bằng log thật, shadow traffic có so sánh tự động, tăng dần theo RPS. Một hướng đi (proxy gRPC) bị bỏ sau 1 tháng. Lý do chính là tổ chức: một codebase Go chung. Grab 2016

  • 2016 · Slack: giữ PHP vì môi trường (mỗi request trạng thái sạch, concurrency shared-nothing, vòng lặp "sửa, tải lại"); chuyển sang HHVM (latency giảm rõ) và bắt đầu Hack (gradual typing). Slack 2016

  • 2016 · Uber (qua HighScalability): Node.js, Python, Go, Java cùng tồn tại. Theo Matt Ranney, nhiều ngôn ngữ làm văn hoá bị chia thành "phe", khó chia sẻ code, khó chuyển team, khó đo hiệu năng xuyên ngôn ngữ. Uber 2016

  • 2017 · Discord: giữ Elixir/BEAM, viết Manifold (fan-out), FastGlobal (dữ liệu chia sẻ không copy), Semaphore. Theo Discord, nếu làm lại vẫn chọn Elixir. Discord 2017

  • 2017 · Instagram: giữ Python, tắt GC: ~10% capacity. Instagram 2017

  • 2017→2019 · Zerodha: đổi framework mobile hai lần (React Native, rồi Flutter/Dart) vì chi phí giữ hai codebase và hiệu năng trên Android tầm trung với tick giá liên tục; chọn Flutter khi còn alpha sau nhiều tháng thử ở dự án ngoài production và một prototype UI đầy đủ. Đổi lại phải tự viết plugin cho sinh trắc học, 90 Hz, HTTP/2. Zerodha 2022

  • 2017 · V8: builtin từng viết bằng JavaScript tự host (chậm lúc đầu) hoặc assembly viết tay cho từng kiến trúc (hàng chục nghìn dòng mỗi nền tảng); CodeStubAssembler cho viết builtin bằng C++ sinh thẳng IR máy của backend TurboFan, có kiểm kiểu và tự cấp phát thanh ghi, dùng cả cho bytecode handler của Ignition. V8 (Google) 2017

  • 2018→2019 · V8: React gặp "vách hiệu năng": trường thời gian của FiberNode khởi tạo là số nguyên nhỏ rồi nhận số thực, cộng Object.preventExtensions, làm V8 không phát lại được chuỗi shape và tạo một shape mồ côi cho mỗi node (hàng chục nghìn shape). V8 7.4 sửa gốc; React giảm nhẹ bằng cách khởi tạo trường bằng NaN. Tối ưu ngầm của runtime có vách mà chỉ profile ứng dụng thật mới thấy. V8 (Google) 2019

  • 2018 · Tokio: tách reactor (I/O) khỏi executor (chạy task), thêm event loop toàn cục mặc định; phát hành không tăng phiên bản các crate phụ thuộc, tokio thành facade re-export các crate con có mức ổn định ghi rõ. Tokio 2018

  • 2019–2020 · Discord: viết lại đúng một service (Read States) từ Go sang Rust. Lý do: Go ép GC mỗi 2 phút và phải quét toàn bộ LRU cache, gây spike. Bản Rust tốt hơn ở mọi chỉ số và cho phép cache lớn hơn hẳn. Discord tự ghi chú: không nên viết lại mọi thứ bằng Rust. Discord 2020

  • 2019 · Instagram: monolith Python vài triệu dòng khởi động hơn 20 giây vì import chạy code có side effect, không reload tăng dần được, trạng thái toàn cục khả biến gây test flaky; không viết lại được nên thêm strict modules (opt-in, abstract interpretation kiểm code mức module thuần, module bất biến, __slots__ tự động). Instagram 2019

  • 2019 · Discord: Sorted set cho member list hàng trăm nghìn phần tử: dữ liệu bất biến của Elixir không đủ nhanh (skip list tự viết vẫn 640 µs khi chèn cuối 250.000 phần tử); hạ xuống Rust qua NIF (Rustler) còn 0,6–3,7 µs tới 1 triệu phần tử, mọi thao tác dưới ngưỡng scheduler BEAM. Đổi ngôn ngữ đúng một cấu trúc dữ liệu nóng, logic realtime vẫn ở Elixir. Discord 2019

  • 2020 · Zerodha: luật kiểm tra lệnh trong Veto (reverse proxy đứng trước OMS) bỏ DSL biểu thức govaluate (không biến, không rẽ nhánh, chỉ trả boolean, thêm hàm phải sửa engine) sang Go thuần nạp bằng Go plugin, chọn qua benchmark trên một luật production (plugin 503 ns/op, govaluate 907, Yaegi 11.091, Rego 13.796). Theo Zerodha, ngôn ngữ thật dễ học với người vận hành hơn DSL tự chế. Zerodha 2020

  • 2020→2021 · Etsy: chuyển sang TypeScript theo từng team kèm đào tạo; trace hiệu năng của chính compiler tìm ra một kiểu vòng tròn làm kiểm toàn codebase mất gần 10 phút (sửa xong còn khoảng một phần ba). Etsy 2021

  • 2020 · Dropbox: edge proxy Nginx (C, cấu hình Jinja/Python, mở rộng bằng Lua) → Envoy (C++, cấu hình protobuf, control plane xDS); không chọn proxy Go tự viết (Bandaid) vì GC và goroutine mỗi request tốn tài nguyên ở edge không autoscale, TLS không có FIPS, không có cộng đồng ngoài. Dropbox 2020

  • 2020 · Jane Street: chuyển allocator của OCaml từ next-fit sang best-fit (có từ 4.10). Một benchmark chậm đi 34,3 s → 36,1 s, nhưng quét space_overhead 20–320 thì với mỗi điểm next-fit đều có điểm best-fit vừa nhanh hơn vừa nhỏ hơn: trong runtime có GC, hiệu năng là một đường cong không gian–thời gian, và bộ nhớ phải đo bằng RSS chứ không bằng top_heap_size Jane Street 2020. Cùng năm, profile bộ nhớ lấy mẫu của OCaml 4.11 thay Spacetime (ghi mọi lần cấp phát, cần compiler riêng) bằng Memtrace chạy được trong production Jane Street 2020.

  • 2021 · ByteDance: ở lại Go nhưng thay tầng mạng: Hertz bỏ net/http (qua Gin) để dùng Netpoll tự viết, lấy hiệu năng và khả năng mở rộng, đổi lại phải tự làm lại hệ sinh thái. ByteDance 2022

  • 2021→2026 · ScyllaDB: "One Driver To Rule Them All": chỉ phát triển driver Rust, driver các ngôn ngữ khác là lớp binding mỏng (C++ rồi Node.js, rồi C#). Với C# họ tự viết khung FFI async (bindgen sẵn có không hỗ trợ async, napi-rs ở Node.js gây nhiều lỗi ẩn); bẫy: luồng Tokio chạy continuation của C# nên executor có thể bị đói. Theo ScyllaDB, driver C# mới nhanh gần gấp đôi bản fork DataStax khi tải đồng thời cao. ScyllaDB 2026 ScyllaDB 2026

  • 2021 · V8: thêm tầng compiler không tối ưu Sparkplug giữa interpreter và TurboFan: dịch thẳng bytecode sang mã máy trong một lượt, không IR, gọi builtin dùng chung, giữ khung stack giống hệt interpreter nên debugger, profiler, OSR chạy gần như không đổi. Speedometer +5–10%, thời gian V8 trên main thread −5–15%. Hướng "tối ưu runtime đang dùng" ở phía chính runtime. V8 (Google) 2021

  • 2021 · Vitess: nâng lên Go Protobuf APIv2 làm CPU tăng tới 19% (benchmark nightly) vì mất code sinh sẵn của Gogo (fork APIv1 không còn bảo trì); viết vtprotobuf là plugin chạy cạnh trình sinh chính thức thay vì fork, thêm memory pool: CPU unmarshal trên VReplication 7,17 s → 4,15 s → 0,63 s. Vitess 2021

  • 2022–2023 · Discord: Rust thành ngôn ngữ của data service đứng trước DB (request coalescing); migrator viết lại bằng Rust rút thời gian migrate từ 3 tháng xuống 9 ngày. Engine DB cũng đổi từ Java (Cassandra) sang C++ (ScyllaDB) để bỏ GC. Discord 2023

  • 2022 · ByteDance: Go là ngôn ngữ nghiệp vụ chính từ 2014; framework RPC thế hệ hai Kitex thay Kite; codec Thrift JIT (Frugal) để khỏi sinh hàng chục nghìn dòng code codec. ByteDance 2022

  • 2022 · Stripe: type checker Flow (dùng từ 2016) treo laptop vì tốn bộ nhớ, tích hợp editor chậm; chuyển cả Dashboard sang TypeScript trong một PR, dùng project references để giảm bộ nhớ compiler. Stripe 2022

  • 2022→2026 · Yelp: chọn Flow năm 2017 khi chưa có bên thắng rõ; vài năm sau TypeScript có hệ sinh thái lớn hơn hẳn; chuyển xong 2/2026, type coverage từ 83,15% lên 96,44%. Yelp 2026

  • 2023→2025 · HRT: fork CPython với lazy import (từ Cinder của Meta) vì monorepo làm import tốn hàng chục giây tới 10–15 phút; migrate tự nguyện tới khoảng 50%, phần còn lại gộp vào đợt nâng Python 3.12; lazy mặc định toàn công ty Q2/2025. Lỗi chính: side effect lúc import bị trễ, phụ thuộc thứ tự, import bắc cầu ngầm. Hudson River Trading 2025

  • 2023 · Discord: Offload fan-out sang process riêng lại làm guild chậm hơn: erlang.trace cho thấy virtual binary heap liên tục kích GC toàn phần (thu vài trăm KB, chép heap nhiều GB); tăng min_bin_vheap_size mới có lợi ích. Tối ưu "làm ít việc hơn" có thể thua vì tác dụng phụ của runtime. Discord 2023

  • 2024 · HRT: hàng triệu dòng Python, logic nghiệp vụ dùng chung (lịch thị trường) là nguồn sự thật cho mọi CLI/GUI; dùng type annotation + mypy để đổi API dùng chung và để type checker chỉ ra mọi chỗ gọi bị ảnh hưởng; annotate lớp lõi trước, thêm dần. Hudson River Trading 2024

  • 2025 · Grab: viết lại Counter Service (Go) bằng Rust, chọn theo ba tiêu chí: đơn giản, traffic lớn, team sẵn lòng học. Viết lại theo contract (hộp đen), shadow traffic. Kết quả: cùng QPS từ 20 xuống 4,5 core, p99 tương đương. Kết luận của Grab: Rust không nhanh hơn Go nhưng hiệu quả hơn; async là chỗ dễ vấp. Grab 2025

  • 2025 · Dropbox: đường phục vụ feature store của Dash viết lại từ Python (SDK Feast) sang Go: profiling cho thấy parse JSON nặng CPU và GIL là nút thắt ở concurrency cao, chạy nhiều process thì thêm chi phí điều phối; bản Go phục vụ hàng nghìn request/giây, chỉ thêm 5–10 ms, p95 khoảng 25–35 ms. Phần định nghĩa feature vẫn là PySpark. Dropbox 2025

  • 2025 · Man Group: tối ưu pip từ bên trong (PR upstream: install nhanh gấp đôi, download gấp 7) Man Group 2025, rồi xuống tận CPython: Python 3.14 chuyển zlib → zlib-ng (giải nén khoảng 2 lần, nén khoảng 3 lần, CRC 10–100 lần nhờ lệnh phần cứng), sửa mức nén mặc định 9 của gzip/tarfile tồn tại từ 1997. Man Group 2026

  • 2025 · Jane Street: đặt tên OxCaml cho nhánh compiler vốn đã là compiler production: tập cha nghiêm ngặt của OCaml, phần mở rộng (không data race, layout unboxed/SIMD, kiểm soát cấp phát) không hứa ổn định, mục tiêu đưa dần về upstream; thư viện phát hành hai bản Jane Street 2025. Năm 2026 dùng mode local/unique của OxCaml cho cầu nối PyOCaml để object Python (data frame nhiều GB, bộ nhớ GPU) không phải chờ GC của OCaml mới được giải phóng Jane Street 2026.

  • 2026 · ClickHouse: viết lại WAL-G (Go) thành WAL-RUS (Rust) không vì tốc độ mà vì bộ nhớ dự đoán được: đỉnh virtual memory từ gần 2,8 GB xuống dưới 1 GB trong benchmark của họ, throughput tương đương; quan trọng khi Postgres chạy với overcommit tắt. Theo ClickHouse (nhà cung cấp). ClickHouse 2026

  • 2026 · Trigger.dev: broker long-poll Node.js → Bun được 5 lần throughput, nhưng hai bước đầu là bỏ SQLite trong RAM cho tra cứu 4 trường (31% CPU) và profile ra tầng node:http chiếm hơn 50%. Bẫy: Bun giữ trạng thái request tới khi Promise<Response> settle, client ngắt mà không resolve thì leak bộ nhớ. Trigger.dev 2026

  • 2026 · Uber: chỉnh runtime Go qua linkname tới biến riêng (tắt stack khởi đầu thích ứng và co stack), cô lập bằng build tag và kiểm lại ở mỗi phiên bản Go, coi là cách tạm cho tới khi có giải pháp chính thức; song song sửa middleware yarpc tốn 2,6 KB stack mỗi request còn 600 byte. Uber 2026

  • 2026 · Rust: trait solver thế hệ mới thay toàn bộ phần chứng minh trait của rustc (thay đổi lớn nhất từ bản đầu theo đội Rust), sửa hơn 200 issue; từng chậm bậc hai, luỹ thừa ở vài trường hợp, nay gần như ngang bản cũ trên 20.000 crate, datafusion compile nhanh hơn 8 lần. Rust 2026

  • 2026 · Stripe: Rủi ro chính khi viết lại proxy bằng Go là thua Envoy (C++) về latency/CPU; đo bằng load test sớm trước khi cam kết. Theo Stripe, nâng cấp Envoy ngày càng rủi ro theo Hyrum's Law (2024 một thay đổi nghttp2 từ chối nhầm traffic hợp lệ, bắt được ở load test QA). Stripe 2026

Ba hướng

Hướng Ví dụ Khi nào
Tối ưu runtime đang dùng Instagram (GC), Discord (BEAM) Điểm nghẽn đo được, nằm ở runtime, có cách vượt qua
Cải tiến dần Slack PHP → HHVM/Hack Codebase lớn, viết lại quá rủi ro, có đường nâng cấp tương thích
Viết lại có phạm vi Grab từng endpoint; Twitter từng service; Discord một service Có lý do đo được, cắt nhỏ được, kiểm chứng song song được

Quy tắc rút ra

  1. Không ai trong các nguồn viết lại toàn bộ hệ thống một lần.
  2. GC xuất hiện ở cả hai chiều: Instagram tắt GC; Discord bỏ Go và bỏ Cassandra (JVM) vì GC. Hiểu runtime quan trọng hơn chọn ngôn ngữ "đúng".
  3. Ngôn ngữ "không thời thượng" vẫn chạy được hệ thống lớn (Slack PHP, Instagram Python, Stack Overflow .NET).
  4. Đi tới tận nguyên nhân gốc (đọc source runtime, perf) trước khi quyết định viết lại (Discord 2020, Instagram 2017).
  5. Đổi ngôn ngữ thường có lý do tổ chức đi kèm: một codebase chung (Grab), kinh nghiệm sẵn có của đội (Twitter với JVM).
  6. Kỹ thuật migrate an toàn: xem Migration không downtime.
  7. Lợi ích của việc viết lại có thể nằm ở chi phí, không ở latency (Grab 2025: Rust bằng ~1/4 tài nguyên, p99 không tốt hơn), ngược với Discord 2020 (viết lại vì spike latency). Xem Chi phí hạ tầng và build vs buy.
  8. Khi không viết lại được, thêm một tập con có kiểm chứng của chính ngôn ngữ, opt-in theo module (Instagram 2019 strict modules: code mức module thuần, bất biến). Instagram 2019
  9. Chọn công cụ theo hệ sinh thái sẽ có, không chỉ theo hôm nay: Stripe (2016) và Yelp (2017) chọn Flow vì lúc đó tốt hơn, vài năm sau đều phải chuyển sang TypeScript. Stripe 2022 Yelp 2026
  10. Nhiều ngôn ngữ cùng cần một logic khó thì viết lõi một lần, bọc mỏng cho từng ngôn ngữ (ScyllaDB 2021→2026: driver Rust làm lõi cho C++, Node.js, C#); cái giá dồn vào biên FFI: bộ nhớ chéo GC/không GC và hai runtime async có giả định khác nhau về luồng. ScyllaDB 2026 ScyllaDB 2026
  11. DSL cho người không lập trình dễ phình thành một ngôn ngữ dở; Zerodha 2020 cho đội vận hành viết luật bằng Go với khung chặt và test đi kèm. Zerodha 2020
  12. Đặt cược vào nền tảng mới thì thử đủ lâu ngoài production và dựng prototype sát thật trước khi quyết; ít phụ thuộc ngoài làm mỗi lần đổi nền rẻ hơn. Zerodha 2022

Câu hỏi mở

  • Chi phí con người: Grab 2025 đặt điều kiện "team sẵn lòng học, không phụ thuộc một người" và nói phải mất thư viện nội bộ viết cho Go (Grab 2025). Vẫn chưa có số liệu về thời gian đào tạo hay tuyển dụng.