1 điểm bởi GN⁺ 2024-07-28 | 1 bình luận | Chia sẻ qua WhatsApp
  • One Million Checkboxes, ra mắt ngày 26/6/2024, là một trang web nơi mọi người cùng thao tác trên cùng 1 triệu checkbox theo thời gian thực; trước khi kết thúc sau 2 tuần, trang đã xử lý hơn 650 triệu lượt đánh dấu
  • Bản thân trạng thái chỉ là 1 triệu bit, tức vỏn vẹn 125KB, nhưng kiến trúc ban đầu dùng nginx, Flask/gunicorn và Redis pubsub nhanh chóng chạm giới hạn trước lượng truy cập ngoài dự kiến
  • Khi hàng chục nghìn người đổ vào từ Hacker News, Reddit, Mastodon và Twitter, hàng loạt vấn đề lộ ra: cạn kết nối Redis, băng thông tăng vọt, thiếu kiểm tra dữ liệu đầu vào, và áp dụng các bản cập nhật cũ
  • Các biện pháp ứng phó tập trung vào những thay đổi có thể triển khai nhanh như mở rộng server và Redis, xử lý cập nhật theo lô, thu gọn định dạng truyền tải, đặt giới hạn băng thông 250Mbit/s bằng Linux tc, và script khởi động lại process
  • Sau đó backend được chuyển sang Go để ổn định hơn; cuối cùng logic đóng băng checkbox được xử lý nguyên tử bằng Redis Lua script, và trang kết thúc lúc 4:35 PM giờ miền Đông ngày 11/7/2024

Trang web và thiết kế ban đầu

  • One Million Checkboxes(OMCB) là một website ra mắt ngày 26/6/2024, cung cấp 1 triệu checkbox toàn cục
    • Khi một người dùng bật hoặc tắt một checkbox, thay đổi được phản ánh ngay lập tức trên màn hình của mọi người dùng
    • Việc xây dựng mất 2 ngày, và số người dùng dự kiến tối đa chỉ ở mức vài trăm
  • Phản ứng thực tế lớn hơn dự kiến rất nhiều
  • Một phần log ban đầu của ngày đầu tiên không còn
    • Vì ban đầu chỉ lưu 1 triệu log mới nhất trong ngày
    • Từ ngày thứ hai, hệ thống bắt đầu ổn định, và trong ngày đó có hơn 50 triệu lượt đánh dấu
    • Trước khi trang kết thúc, tổng số lượt đánh dấu tích lũy đã vượt 650 triệu

Kiến trúc gốc xoay quanh Redis

  • Trạng thái checkbox được biểu diễn bằng 1 triệu bit
    • Ô được đánh dấu là 1, ô chưa được đánh dấu là 0
    • Kích thước toàn bộ trạng thái là 125KB
    • Client lưu bitset và tham chiếu đến nó khi render
  • Client được cấu hình để tránh tải nặng lên DOM
    • Không đưa toàn bộ 1 triệu phần tử vào DOM
    • Dùng react-window để chỉ render các checkbox đang thấy trên màn hình cùng một buffer nhỏ
  • Cấu hình server là dạng đơn giản, có tính đến mở rộng ngang
    • nginx phục vụ nội dung tĩnh và chuyển tiếp các request API cùng kết nối websocket tới server Flask
    • Server Flask gồm hai instance chạy bằng gunicorn
    • Redis đảm nhiệm việc lưu trạng thái checkbox và vai trò hàng đợi thông điệp
  • Cách dùng Redis cũng trực tiếp
    • Thay đổi trạng thái từng checkbox bằng các primitive thao tác bit của Redis
    • Khi client gửi sự kiện check, Flask lật bit trong Redis và ghi sự kiện vào pubsub
    • Hai server Flask đọc pubsub và thông báo thay đổi cho các client đang kết nối với chúng
  • Snapshot toàn bộ trạng thái là cơ chế để bù cho các cập nhật bị bỏ lỡ
    • Dùng để đồng bộ các client bị lỡ cập nhật vì tab ở nền
    • Triển khai ban đầu gửi toàn bộ trạng thái mỗi 30 giây

Nguyên tắc mở rộng

  • Chi phí phải có thể tính được giới hạn trên
    • Tránh kiểu tự động mở rộng không giới hạn khiến chi phí tăng vọt
    • Chọn để hệ thống có thể vỡ khi tải vượt kỳ vọng
  • Giả định thời gian được yêu thích sẽ ngắn
    • Ưu tiên các biện pháp có thể làm trong vài giờ hơn là giải pháp hoàn thiện mất vài ngày hoặc vài tuần
    • Chấp nhận nợ kỹ thuật phát sinh trong quá trình đó
  • Lựa chọn công nghệ ưu tiên sự đơn giản và khả năng tự vận hành trực tiếp
    • Chọn cấu hình cho phép đăng nhập vào server, chạy lệnh và debug trực tiếp
    • Chủ yếu dùng các phụ thuộc có thể tự vận hành và debug
  • Trải nghiệm cốt lõi của trang là đồng bộ toàn cục
    • Dù di chuyển đến đâu, người dùng cũng phải thấy các thay đổi tức thì
    • Không mở rộng theo cách chỉ gửi các checkbox mà người dùng đang nhìn thấy

Ngày đầu tiên: tăng server và nút thắt Redis

  • Trong vòng 30 phút sau khi ra mắt, tải tăng vọt; trang vẫn sống nhưng khó trụ được lâu
    • Cách cải thiện rõ ràng nhất là tăng thêm server
    • nginx có thể dễ dàng reverse proxy sang các instance Flask trên VM khác, và trạng thái vốn đã nằm trong Redis
  • Server thứ hai được thêm vào khoảng 12:30 PM và nhanh chóng đạt tải 100%
    • Ban đầu tưởng rằng thêm một hoặc hai server là đủ
    • Thực tế là càng mở rộng thì lưu lượng cũng càng tăng
    • Trang lên vị trí số 1 trên Hacker News, và hoạt động trên Twitter cũng tăng mạnh
  • Kết nối giữa server Flask và Redis trở thành nút thắt
    • Không có Redis connection pool, và Redis gần như rơi vào tình trạng thiếu kết nối
    • Chuyển sang cách gộp cập nhật thành lô để gửi
    • Không tính đến khả năng tương thích với client cũ, vì dự kiến người dùng sẽ refresh
  • Redis connection pool cũng được thêm vào nhưng không hoạt động gọn gàng với tổ hợp gunicorn và Flask
    • Dù vậy có vẻ vẫn giúp giảm số kết nối Redis
    • Sau đó vấn đề này không được đào sâu thêm mà chuyển sang viết lại bằng Go
  • Rate limit tạo session bị gỡ bỏ
    • Trạng thái rate limit được lưu trong Redis, trong khi kết nối Redis đang cạn kiệt
    • Vấn đề không nằm ở việc session mới tăng đột biến mà ở việc một session đơn lẻ gửi nhiều dữ liệu
    • Đây là biện pháp rủi ro trong ngắn hạn nhưng được xem là chấp nhận được
  • Instance Redis cũng được nâng lên cấu hình lớn hơn
    • Đang dùng Digital Ocean managed Redis
    • Nâng từ instance nhỏ 1 shared CPU, 2GB RAM lên instance 4 dedicated CPU, 32GB RAM
    • Việc resize mất khoảng 30 phút

Vấn đề băng thông và giảm lượng truyền tải

  • Ban đầu chi phí băng thông chưa được cân nhắc đầy đủ
    • Digital Ocean tính $0.01 mỗi GB khi vượt băng thông miễn phí
    • Từ công việc trước đó còn 1TB băng thông miễn phí, và cho rằng OMCB sẽ không ảnh hưởng lớn
  • Snapshot toàn bộ trạng thái có thể tiêu tốn băng thông rất nhanh
    • 1 triệu bit là 1Mbit
    • Nếu gửi cho 1.000 người mỗi 30 giây, sẽ vào khoảng 2GB mỗi phút, tức 120GB mỗi giờ
    • Con số này chưa tính các incremental update
  • Việc kiểm tra băng thông và đặt giới hạn chi phí được xử lý trên máy nginx
    • Dùng ip -s link show dev eth0 để kiểm tra số byte đã gửi
    • Vì dùng một reverse proxy nginx duy nhất nên dễ suy luận nguồn băng thông
  • Giảm lượng truyền tải được thực hiện theo hai hướng
    • Giảm tần suất snapshot toàn bộ trạng thái
    • Thu gọn định dạng incremental update
  • Định dạng cập nhật theo lô được nén đáng kể
    • Định dạng cũ là danh sách dict như { "index": 123, "value": true }
    • Định dạng cuối cùng là cặp mảng index true và mảng index false, dạng [[123, 125], [124]]
    • Cách này ngắn hơn 5 lần so với triển khai ban đầu
  • Đặt hard cap bằng Linux tc để ngăn chi phí bùng nổ
    • Giới hạn lưu lượng trên public interface eth0250Mbit/s
    • Mức này tương đương khoảng 2GB mỗi phút, hơi dưới 3TB mỗi ngày
    • Với giá $0.01 mỗi GB, có thể tránh tình huống chi phí tăng ngoài kiểm soát qua đêm

Ngày thứ hai: thiếu kiểm tra đầu vào và Redis replica

  • Sáng hôm sau, trang đã down; nguyên nhân là thiếu kiểm tra dữ liệu đầu vào
    • Không chặn các checkbox có index vượt quá 1 triệu
    • Ai đó đã thao tác các checkbox có index lên tới hàng trăm triệu
    • Điều này khiến hệ thống trông như số ô đã được đánh dấu đạt 1 triệu và kết luận rằng trang đã kết thúc
  • Dữ liệu Redis cũng phình to không cần thiết
    • Hàng trăm triệu bit 0 được thêm vào giữa bit thứ 1 triệu và bit thứ 100 triệu
    • Dữ liệu gửi tới client lớn hơn 100 lần
  • Việc khôi phục diễn ra nhanh chóng
    • Dừng nginx
    • Chỉ copy 1 triệu bit đầu tiên của bitset cũ sang bitset mới
    • Giữ lại bitset cũ để debug
    • Sửa code để tham chiếu bitset mới và thêm kiểm tra đầu vào
  • Tải trang ban đầu cũng trở nên chậm
    • Redis chịu tải lớn, và do bug connection pool, quá nhiều kết nối cũng đang được tạo
    • Thay vì debug vấn đề connection pool, thêm Redis replica để phân tán tải và kết nối khỏi primary
  • Phải tự tìm private IP của replica
    • Theo hướng dẫn của Digital Ocean, prefix replica- hoạt động với public DNS nhưng không hoạt động với private DNS
    • Dùng public IP được cho là sẽ có rủi ro đi qua public internet và bị tính phí băng thông
    • Thử kết nối tới các địa chỉ gần private IP của primary và các server khác, đến lần thứ ba hoặc thứ tư thì tìm được private IP của replica
    • Sau đó hardcode IP đó

Khởi động lại process và sửa stale update

  • Các process Flask tiếp tục crash, nguyên nhân có vẻ là thiếu kết nối Redis
    • Thay vì debug chi tiết, tạo một bash script kiểm tra số process Flask đang chạy
    • Nếu số process đang chạy dưới 3 thì khởi động lại systemd unit
    • Script được đưa vào crontab
  • Cấu hình nginx cũng được điều chỉnh cùng lúc
    • Server bị down được loại tạm thời khỏi rotation
    • Sau thay đổi này, trang ổn định hơn
  • Đồng bộ trạng thái client có bug stale update
    • Client nhận cả incremental update lẫn full-state snapshot
    • Vì hai loại update này không có timestamp, client có thể áp dụng incremental update cũ sau khi đã nhận snapshot mới
    • Kết quả là có thể thấy trạng thái hoàn toàn sai cho đến full-state snapshot tiếp theo
  • Thêm biện pháp giảm thiểu dựa trên timestamp
    • Gắn timestamp vào full-state snapshot
    • Gắn timestamp vào từng update ghi vào Redis pubsub
    • Batch gửi tới client chứa timestamp lớn nhất trong số các incremental update được bao gồm
    • Client được đổi để bỏ các batch cũ hơn full-state snapshot gần nhất
  • Giải pháp này không hoàn hảo
    • Nếu trong batch có dù chỉ một update mới, batch vẫn có thể được áp dụng dù phần lớn update trong đó là cũ
    • Dù vậy nó cải thiện đáng kể so với trước

Viết lại bằng Go và ổn định hóa

  • Sáng hôm sau, trang vẫn sống, và sau đó tập trung vào việc viết lại backend
    • Khi đó cũng đã nhận được email từ Washington Post
    • Đồng thời cân nhắc kế hoạch kết thúc trang
  • Kế hoạch kết thúc là nếu các ô đã được đánh dấu không được bỏ đánh dấu nhanh thì chúng sẽ bị đóng băng
    • Thay đổi này có thể gây tăng hoạt động và kéo theo thêm việc về server
    • Không chắc kiến trúc dựa trên Flask hiện tại có chịu nổi hay không
  • Cùng với bạn là Eliot, backend được viết lại bằng Go
    • Từ 2 PM Chủ nhật đến 2 AM, cùng thảo luận triển khai và port toàn bộ backend
    • Chuyển sang Go mà không thay đổi lớn cấu trúc
    • Một số phần gây vướng, như tìm thư viện Go socketio hỗ trợ protocol mới nhất
  • Hiệu năng cải thiện rất lớn
    • Hệ thống mở rộng tốt đến mức bot có thể đẩy vào lượng traffic quá mức
    • Cần rate limit tốt hơn
  • Tối Chủ nhật cũng có DDoS
    • Ứng phó bằng cách đặt trang sau Cloudflare và chỉnh nhẹ cấu hình nginx

Logic kết thúc trang

  • Sau khi viết lại bằng Go, trang hoạt động ổn định
    • Trong tuần tiếp theo, xử lý các cuộc phỏng vấn và sự quan tâm
    • Sau đó bắt đầu công việc kết thúc trang
  • Cách kết thúc là đóng băng checkbox
    • Nếu một ô đã được đánh dấu không được bỏ đánh dấu nhanh, nó sẽ chuyển sang trạng thái frozen
    • Theo thời gian, toàn bộ trang sẽ hoàn toàn frozen
  • Redis có thêm trạng thái
    • Thêm một hashtable lưu thời điểm mỗi checkbox được đánh dấu lần cuối
    • Trạng thái này quá lớn để chuyển cho client, nhưng lưu trong Redis thì ổn
    • Cũng lưu giá trị time_to_freeze
  • Kiểm tra trạng thái đóng băng tại thời điểm uncheck
    • Nếu now - last_checked > time_to_freeze thì không uncheck
    • Thay vào đó cập nhật frozen_bitset để đánh dấu checkbox đó ở trạng thái frozen
    • frozen_bitset được phân phối tới client theo cùng cách với trạng thái checked
    • Client vô hiệu hóa checkbox có bit frozen bật
  • Thêm tác vụ riêng để các ô vẫn bị đóng băng ngay cả khi không ai uncheck
    • Định kỳ tìm các bit đáng lẽ phải freeze nhưng chưa được hiển thị, rồi chuyển chúng sang trạng thái frozen
    • Đưa logic liên quan vào Redis Lua script để chạy nguyên tử
    • Việc tránh race condition trở nên dễ hơn
  • Thay đổi kết thúc được áp dụng sau 2 tuần 1 ngày kể từ khi ra mắt
    • Vào 4:35 PM giờ miền Đông ngày 11/7/2024, box 491915 được đánh dấu và trang khép lại

Chi phí và bài học

  • Chi phí vận hành trang khoảng $850
    • Donation khá sát với khoản chi phí này
    • Kết luận rằng đây không phải khoản lỗ lớn
  • Hài lòng với lựa chọn Redis và nginx
    • Đánh giá Redis và nginx là các công nghệ rất hữu ích
    • Việc tự vận hành giúp debug và sửa đổi dễ dàng
    • Tuy nhiên, việc không thể kiểm soát hoàn toàn instance managed Redis hơi bất tiện
  • Việc không thiết kế mở rộng quy mô lớn từ đầu trong thời gian dài được đánh giá tích cực
    • Cho rằng rất khó dự đoán điều gì sẽ thành công trên internet
    • Nếu ngay từ đầu đã dành vài tuần suy nghĩ về scale, có thể trang đã không được ra mắt
    • Việc có nhiều người dùng truy cập giúp tạo động lực bảo trì và đặt ưu tiên
  • Cũng xác nhận nhu cầu đối với các tương tác ẩn danh có giới hạn
    • Mọi người quan tâm đến các trang cho phép tương tác với người lạ theo cách có giới hạn
    • Điều này làm tăng sự tự tin để tiếp tục xây dựng các loại trang như vậy

1 bình luận

 
GN⁺ 2024-07-28
Các ý kiến trên Hacker News
  • Đây là một bài viết có nhiều điều để học, cùng với kiến thức lịch sử về hệ thống phân tán
    Ngoại trừ lưu trữ, có vẻ như họ đã gặp gần như mọi loại điểm gián đoạn và lỗi, và thật hay khi được thấy quá trình giải quyết
    Tôi không biết Redis hỗ trợ Lua, nhưng đọc xong thấy muốn thử dùng nó như một kho lưu trạng thái thay thế
    Băng thông là một trong những điều khó chịu nhất ở các dịch vụ đám mây, vì không có giới hạn cứng để ngăn chi phí vượt mức

    • Lưu trữ thì cũng có gặp, theo cách khá nhàm chán. Cấu hình logrotate không đúng nên đĩa gần đầy, và khi gửi log box-check vào Redis, chúng tôi phải tạo một cơ chế đẩy log cũ xuống đĩa để Redis không nổ tung
      Tuy vậy cả hai đều không phải vấn đề lớn, và việc lưu trữ không phải là một vấn đề đáng kể trong dự án này khá thú vị. Với cá nhân tôi thì đó là trải nghiệm mới
      Băng thông thì thật sự đau đầu. Suốt khoảng hai ngày tôi cứ căng thẳng kiểm tra số byte gửi đi của NIC và tính toán lại, và việc không có hard cap thật đáng sợ. Ngay cả khi Digital Ocean có mức giá khá hợp lý thì vẫn vậy
      Tôi chưa dùng các dịch vụ serverless phổ biến, nhưng theo tôi hiểu thì ở đó chi phí băng thông sẽ khá nặng
      Lua bên trong Redis thật sự rất mạnh; nếu có thể chấp nhận một chút hao hụt hiệu năng, nó giúp bỏ qua rất nhiều vấn đề khó và nhiều race condition, làm việc với nó rất thú vị
  • Bài viết tuyệt vời, và website cũng rất đáng chúc mừng
    Nhưng cá nhân tôi nghĩ bài viết này mới là phần đáng tự hào nhất

    • Tôi đã dành nhiều thời gian viết bài hơn hẳn so với thời gian làm site trước khi ra mắt, và điều đó thấy khá buồn cười
  • Tôi nghĩ điểm cốt lõi là đoạn “làm site trong hai ngày mà hầu như không bận tâm đến khả năng mở rộng là một lựa chọn đúng”
    Đây là điều các kỹ sư, đặc biệt ở giai đoạn đầu sự nghiệp, nên học. Khả năng mở rộng không phải là vấn đề cho đến khi nó thật sự trở thành vấn đề
    Khi nó trở thành vấn đề thì đó lại là một vấn đề tốt, và thường cũng không khó sửa như bạn tưởng

    • Điều đó chỉ đúng khi đi kèm với ý “vì vậy hãy giữ hệ thống đơn giản và cơ bản
      Tôi đã thấy nhiều hệ thống nơi microservice trở thành “lựa chọn hiển nhiên” không phải để mở rộng hay tách nhóm, mà chỉ vì các lập trình viên muốn làm vậy
      Mở rộng những hệ thống như thế thật sự là cực hình
  • Bài liên quan gần đây: One Million Checkboxes - https://news.ycombinator.com/item?id=40800869 - tháng 6/2024, 305 bình luận

  • Những dự án kiểu này rất thú vị
    Khoảng 6 năm trước tôi đã ra mắt Pixmap trên Android, một ứng dụng chỉnh sửa pixel cộng tác nhỏ hỗ trợ các lưới lớn hơn như 1024x1024
    Tôi có một hàng đợi áp dụng từng sự kiện vào ảnh PNG; khi client kết nối, nó tải PNG ban đầu, rồi với mỗi sự kiện vẽ pixel thì chỉ nhận một object nhỏ
    Cách này tận dụng được nén ảnh ở lần tải ban đầu, còn tập thay đổi sau đó thì rất nhỏ. Ngoài ra vì mọi sự kiện đều được lưu trong log nên cũng có thể “tua ngược” ảnh [0]
    [0] 22mb: https://blog.winricklabs.com/images/pixmap-rewind-demo.gif

    • Hay đấy. Tôi từng xem xét một ý tưởng tương tự là gửi cập nhật theo từng pixel đến nhiều web client, nhưng với cách tôi định làm thì có vẻ sẽ tốn quá nhiều băng thông và lưu trữ
      Vì vậy hiện tôi đang mày mò một canvas có thể định địa chỉ bằng API call
      https://x.com/RussTheMagic/status/1816749136487588311
  • Bài hay. Tôi tò mò cuối cùng chi phí là bao nhiêu

    • Lẽ ra tôi nên đưa phần này vào
      Tổng chi phí khoảng 850 USD, và gần như được bù bằng tiền quyên góp
      Sau khi chuyển sang Go, tôi đã mắc lỗi không hạ tầng đúng cách, và cũng có thể bỏ bản sao Redis thứ hai được dựng thêm. Nếu tập trung vào chi phí, có lẽ tôi đã có thể giảm một nửa
      Nhưng vì tiền quyên góp gần như bù được chi phí và có quá nhiều việc khác, tôi không tập trung nhiều vào đó
      Ngay cả sau khi đóng site, tôi vẫn giữ hạ tầng thêm một thời gian để chuẩn bị biểu đồ, v.v., nên tốn thêm một ít tiền; hiện tại hơi lỗ một chút nhưng không đáng kể
  • Với tư cách người mới học backend, tôi tự hỏi liệu có kiến trúc thay thế đơn giản hơn cho dự án này không
    Sẽ thật tốt nếu có cách dễ hơn để host trạng thái 1 triệu bit và đồng bộ với client. Một số giải pháp trong bài khá khó hiểu
    Các dự án của tác giả đều rất tuyệt

    • Xin lỗi nếu một số phần trong bài khó hiểu
      Tôi muốn giải thích dài hơn về các công nghệ đã dùng, nhưng bài vốn đã quá dài nên thấy khó nhét thêm
      Nếu có câu hỏi tôi sẵn lòng trả lời
      Thành thật mà nói tôi không chắc có cách nào đơn giản hóa kiến trúc nhiều hơn nữa. Có thể có các dịch vụ dùng được cho mục đích này, nhưng tôi xem đó gần như là chuyển độ phức tạp cho người khác
      Rốt cuộc những gì cần có là: một database để theo dõi các ô đã được tick, cách đưa dữ liệu vào database, cách báo trạng thái hiện tại cho client, cách để client báo cho server khi tick một ô và cập nhật trạng thái, cách thông báo cho client khi ô được tick hoặc bỏ tick, và cách không render 1 triệu phần tử DOM mọi lúc
      Ở đây tôi dùng Redis để lưu trạng thái tick, lưu thẳng 1 triệu bit vì đơn giản, và gửi toàn bộ 1 triệu bit cho client. May là dữ liệu không quá lớn
      Flask và WebSocket xử lý sự kiện tick và cập nhật; tôi gửi cả cập nhật từng ô riêng lẻ lẫn cập nhật toàn bộ 1 triệu ô, và dùng react-window để tránh vấn đề render
      Phần còn lại như nội dung tĩnh nginx và reverse proxy chủ yếu là cơ chế để dễ mở rộng, nên vẫn có thể triển khai và site vẫn chạy mà không cần các chi tiết đó. Chỉ là sẽ không chịu được cùng mức tải
    • Cũng có thể nhét mọi thứ trong bài vào một tiến trình duy nhất
      Thay vì database, lưu bitset vào file rồi mmap là được. Thay vì reverse proxy, ứng dụng cũng có thể tự xử lý trực tiếp các HTTP request và kết nối WebSocket
    • Thành thật mà nói, như vậy gần như đã là đơn giản nhất rồi
      Chỉ là vài web server phía sau có cache và hàng đợi publish/subscribe
      Có thể làm tất cả trong bộ nhớ trên một host lớn, nhưng nếu không đáp ứng nổi nhu cầu hoặc thất bại vì lý do nào đó thì sẽ bế tắc hoàn toàn
    • Thành thật tôi nghĩ khó mà đơn giản hơn nhiều so với thế
      Trừ kiểu không thể mở rộng như đặt một danh sách global gồm 1 triệu boolean trong cùng tiến trình với API backend
    • Có thể dùng quadtree để tóm tắt toàn bộ các khối checkbox có cùng trạng thái thành tuple (checked, start_x, start_y, end_x, end_y). Chẳng phải đó là cách quá hiển nhiên sao
  • Hay thật
    Tôi tự hỏi bài tiếp theo có phải sẽ là phân tích thống kê xem checkbox nào được tick ít nhất hoặc nhiều nhất không
    Tôi vẫn nhớ hơi buồn vì một checkbox tôi cuộn xuống khá sâu để chọn đã bị bỏ tick gần như ngay lập tức

    • Tôi sẽ sớm chia sẻ dữ liệu thô
      Trước đó còn một câu chuyện nữa cần kể về site
  • Tôi tự hỏi trò chơi còn sống không
    Khi vào https://onemillioncheckboxes.com/, không có ô nào được tick, và trong JS console chỉ thấy cái này
    {"total":0,"totalGold":0,"totalRed":0,"totalGreen":0,"totalPurple":0,"totalOrange":0,"recentlyChecked":false}

    • Theo bài gốc thì “trước khi đóng site sau 2 tuần, nó đã vượt 650 triệu
  • Như một ví dụ hoàn toàn trái ngược với triển khai có khả năng mở rộng, có bản triển khai 1 triệu checkbox dưới 1000 ký tự. Đây là phiên bản Deno
    https://gist.github.com/jeff-hykin/4cdebafd8698298d021f103e2...