Newsletter #117

Mời bạn thưởng thức Newsletter #117.

Should you normalize RGB values by 255 or 256?

Pekka Väänänen mổ xẻ một câu hỏi tưởng nhỏ nhưng rất hay gây tranh cãi trong xử lý ảnh: khi chuyển giá trị màu RGB 8-bit sang số thực, nên chia cho 255 hay cộng thêm nửa bước rồi chia cho 256. Cách quen thuộc img / 255.0 ánh xạ 0 thành 0.0 và 255 thành 1.0, nên đen tuyệt đối và trắng tuyệt đối vẫn giữ đúng ý nghĩa, rất tiện cho mọi đoạn mã xử lý phía sau. Cách thay thế (img + 0.5) / 256.0 đặt mỗi giá trị vào giữa một khoảng lượng tử, đẹp hơn về mặt mô hình lượng tử hóa và cho sai số tái dựng lý thuyết thấp hơn một chút, nhưng chỉ khi ta kiểm soát cả bước ghi lẫn bước đọc dữ liệu.

Kết luận thực dụng của tác giả là với ảnh đến từ bên ngoài, cứ chia cho 255. Những lập luận thường được đưa ra để chống lại 255, như khoảng lượng tử ở hai đầu hẹp hơn, số thực không biểu diễn chính xác tuyệt đối hay cảm giác lãng phí một chút độ chính xác, đều không đáng kể trong thực tế. Ngược lại, chia cho 256 với ảnh vốn được lượng tử hóa theo quy ước 255 có thể đưa thêm sai lệch và khiến mã nguồn phụ thuộc vào dải 8-bit. Lỗi thực sự đáng sợ là trộn bước mã hóa của quy ước này với bước giải mã của quy ước kia. Vì vậy chỉ nên cân nhắc 256 trong một hệ thống khép kín, nơi bạn kiểm soát toàn bộ quy trình lưu và đọc ảnh, không cần 0 ánh xạ đúng về 0.0 và mọi thành phần đều thống nhất một quy ước.

Jira is Turing-Complete

Nicolas Seriot biến một câu đùa quen thuộc về Jira thành một chứng minh nghiêm túc: Jira Automation có thể mô phỏng máy thanh ghi Minsky, một mô hình tính toán đã được chứng minh là Turing-complete. Trong cách dựng của tác giả, mỗi thanh ghi là số lượng issue liên kết thuộc một loại nhất định, chẳng hạn Bug và Task; con trỏ lệnh là trạng thái hiện tại của một Epic; bảng lệnh là tập automation rule; còn mỗi nhịp chạy là một lần chuyển trạng thái do automation kích hoạt. Chỉ với các thao tác tạo, xóa và kiểm tra issue liên kết, Jira đã biểu diễn được INC, DEC và rẽ nhánh có điều kiện.

Ví dụ tối giản là phép cộng: chuyển toàn bộ số Bug ở thanh ghi A sang số Task ở thanh ghi B, dùng các trạng thái như TODO, DEV và PROD. Khi A còn Bug, rule xóa một Bug rồi chuyển sang trạng thái tăng B; khi A bằng 0 thì chuyển sang trạng thái dừng. Tác giả còn dùng thao tác đổi loại issue, thực chất là gộp một lần giảm và một lần tăng, để rút gọn chương trình Fibonacci xuống còn ba trạng thái với ba thanh ghi. Chương trình này chạy cho đến khi chạm giới hạn độ sâu chuỗi automation của Jira Cloud và cần người vận hành kích hoạt lại. Các giới hạn quota như vậy không phủ nhận lập luận, vì mọi máy tính vật lý cũng hữu hạn. Thông điệp không phải là nên lập trình bằng Jira, mà là các automation phức tạp thật sự mang hình dạng của một chương trình, với trạng thái, bộ nhớ, rẽ nhánh và vòng lặp.

USB for Software Developers: An introduction to writing userspace USB drivers

WerWolv viết một bài nhập môn USB cho lập trình viên phần mềm muốn giao tiếp với thiết bị mà không phải đào sâu vào tín hiệu điện hay viết mã trong nhân hệ điều hành. Ví dụ xuyên suốt là một điện thoại Android ở chế độ bootloader, vì giao thức Fastboot đơn giản, có tài liệu rõ ràng và thường không bị hệ điều hành gắn sẵn driver. Bài viết bắt đầu bằng lsusb để đọc VID, PID, lớp thiết bị và driver đang gắn, sau đó chuyển sang thư viện libusb để tìm và mở thiết bị ngay trong không gian người dùng.

Phần giá trị nhất là cách tác giả nối từng khái niệm USB với mã nguồn thật. Trong quá trình liệt kê thiết bị, host dùng control endpoint 0x00 để gửi các request chuẩn như GET_STATUS và GET_DESCRIPTOR, qua đó đọc descriptor và biết thiết bị có những cấu hình, interface và endpoint nào. Endpoint có thể hình dung như các cổng giao tiếp, nhưng USB luôn do host chủ động hỏi, thiết bị không tự gửi dữ liệu. Tác giả giải thích bốn kiểu truyền Control, Bulk, Interrupt, Isochronous, cùng hướng IN và OUT luôn được hiểu từ góc nhìn của host. Cuối cùng, chương trình gửi lệnh Fastboot getvar:version qua Bulk OUT endpoint và nhận phản hồi OKAY0.4 qua Bulk IN endpoint. Bài học rút ra: với thiết bị có giao thức rõ ràng, một driver chạy trong không gian người dùng bằng libusb gần với lập trình socket hơn là một dự án kernel đáng sợ.

Everything you need to know about Sourcemaps

Neciu Dan giải thích sourcemap ở cả hai mặt: công cụ gỡ lỗi không thể thiếu và một rủi ro bảo mật hay bị xem nhẹ. Sau khi đóng gói, trình duyệt chạy mã đã được gộp, nén và đổi tên biến, nên một lỗi kiểu bundle.min.js:1:48211 gần như vô nghĩa. Sourcemap là file JSON ánh xạ từng vị trí trong mã đã nén về file, dòng, cột và tên gốc, giúp stack trace dễ đọc trở lại. Nhưng chính các trường như sources, names, mappings và đặc biệt sourcesContent có thể để lộ cấu trúc thư mục, tên module, comment, endpoint API, thậm chí toàn bộ mã nguồn gốc nếu file .map bị công khai.

Thông điệp cốt lõi là minify không phải cơ chế bảo vệ mã nguồn. Khi bundle production trỏ tới sourcemap qua sourceMappingURL hoặc HTTP header SourceMap, bất kỳ ai có DevTools hay curl đều tải được nếu máy chủ cho phép. Tác giả dẫn các sự cố thực tế, như trang App Store trên web của Apple năm 2025 và gói npm @anthropic-ai/claude-code phiên bản 2.1.88 kèm file cli.js.map gần 60 MB, để cho thấy đây thường chỉ là một lỗi cấu hình đóng gói chứ không phải kỹ thuật tấn công phức tạp. Cách phòng tránh gồm tắt sourcemap ở production khi không cần, dùng hidden sourcemap và tải riêng lên công cụ theo dõi lỗi như Sentry, chặn truy cập .map ở máy chủ, loại file .map khỏi artifact và gói npm, đồng thời thêm bước kiểm tra trong CI/CD hoặc sau npm pack để chặn bản phát hành chứa sourcemap.

Extended Statistics in Postgres

Valerie Parham-Thompson giải thích cách extended statistics giúp query planner của Postgres ước lượng chính xác hơn khi các cột có quan hệ với nhau mà thống kê mặc định không nhận ra. Planner dựa vào các bảng thống kê nội bộ như pg_class và pg_stats để ước lượng số dòng, số giá trị phân biệt, histogram và tần suất, nhưng các thống kê này chủ yếu nhìn từng cột riêng lẻ và ngầm giả định các điều kiện lọc độc lập với nhau. Ví dụ zip='91605' gần như kéo theo state='CA', vậy mà planner vẫn nhân xác suất của hai điều kiện như hai biến độc lập, dẫn đến ước lượng số dòng thấp hơn thực tế rất nhiều và chọn một kế hoạch truy vấn kém.

Bài viết đi qua ba loại extended statistics. Loại dependencies mô tả quan hệ phụ thuộc giữa các cột, như mã ZIP gần như xác định bang; sau khi chạy CREATE STATISTICS ... (dependencies) và ANALYZE, ước lượng số dòng sát thực tế hơn hẳn. Loại mcv lưu các tổ hợp giá trị phổ biến trên nhiều cột, hữu ích cho điều kiện WHERE kết hợp như danh mục và danh mục con. Loại ndistinct giúp planner ước lượng số nhóm khi GROUP BY trên nhiều cột, thay vì nhân số giá trị phân biệt của từng cột. Bài học thực tế: khi biết dữ liệu của mình có các cột tương quan, hãy dạy cho planner bằng extended statistics thay vì chỉ nghĩ đến việc thêm index.

Finding a needle in a 4 GB haystack: from 0.75 GB/s to 49 GB/s in Go

Assel Meher dùng một bài toán cố ý thật đơn giản để mổ xẻ hiệu năng đọc và quét file trong Go: tìm một giá trị int64 khác 0 nằm ở cuối một file 4 GiB gần như toàn số 0. Không có phân tích cú pháp hay thuật toán phức tạp, nên phép đo phơi bày chi phí thật của từng lớp: Go runtime, thư viện chuẩn, syscall, page cache, bộ nhớ, mmap, pread, đa luồng và SIMD. Điểm xuất phát là os.ReadFile rồi duyệt range từng byte, chỉ đạt khoảng 0.75 GB/s vì vừa cấp phát và sao chép 4 GiB vừa xử lý từng byte; bufio.ReadByte cũng không khá hơn vì tốn một lời gọi hàm cho mỗi byte.

Bước nhảy lớn đầu tiên đến từ việc bỏ chi phí theo byte: đọc theo khối 1 MiB và quét theo uint64 đã vượt 13 GB/s. mmap không tự động thắng, vì tuy tránh được sao chép nhưng lại phát sinh hơn một triệu minor page fault khi chạm vào từng trang. Song song hóa giúp cải thiện, và pread song song vượt hẳn mmap song song, vốn chững lại quanh 28 GB/s do tranh chấp page fault, vì sao chép tuần tự theo khối rẻ hơn page fault hàng loạt. SIMD bằng AVX2 hay gói simd/archsimd của Go 1.26 làm vòng quét gần như miễn phí, nhưng khi đã chạm trần băng thông bộ nhớ thì chiến lược đọc và mức song song mới là yếu tố quyết định. Kết quả tốt nhất đạt khoảng 49 GB/s với pread song song kết hợp SIMD thuần Go; muốn nhanh hơn nữa chỉ còn cách đọc ít dữ liệu hơn hoặc đổi phần cứng.

HTTP caching, a refresher

Dan Cătălin Burzo đọc lại RFC 9111 để hệ thống hóa những khái niệm quan trọng của HTTP caching hiện đại. Bài viết nhắc rằng cache không chỉ là cache riêng của trình duyệt mà còn gồm các shared cache như proxy, CDN hay Varnish đứng giữa client và máy chủ gốc. Trọng tâm là header Cache-Control: các directive như max-age, s-maxage, no-cache, no-store, private, public, must-revalidate, stale-while-revalidate và stale-if-error không chỉ quyết định có lưu response hay không, mà còn quyết định khi nào response còn “tươi” dựa trên tuổi so với max-age, s-maxage, Expires hoặc ước lượng từ Last-Modified, khi nào phải xác thực lại, và shared cache có được lưu response của request mang Authorization hay không.

Tác giả cũng làm rõ những chi tiết dễ nhầm. Response hết hạn không nhất thiết bị bỏ đi, vì cache có thể gửi request có điều kiện bằng If-None-Match với ETag hoặc If-Modified-Since với Last-Modified để nhận 304 Not Modified mà không phải tải lại nội dung. Header Vary mở rộng khóa cache theo các header như Accept-Language nhưng dễ làm cache kém hiệu quả nếu giá trị quá đa dạng, còn No-Vary-Search giải quyết chiều ngược lại bằng cách bỏ qua các tham số truy vấn không ảnh hưởng nội dung. Phần cuối so sánh tải lại thường và tải lại cưỡng bức trong trình duyệt, vai trò của immutable, và cảnh báo rằng public, s-maxage hay must-revalidate có thể khiến shared cache lưu response của request đã xác thực nếu dùng sai, nên hãy dùng private khi cần chặn. Đây là bài ôn tập tốt cho cả người làm máy chủ, CDN lẫn client tự viết.

How to Use ss as a Replacement for netstat to View IPv4 Sockets

Nawaz Dhandala giới thiệu ss như công cụ hiện đại thay thế netstat đã lỗi thời để kiểm tra socket trên Linux. Vì truy vấn thông tin socket trực tiếp từ kernel, ss nhanh hơn đáng kể, nhất là trên máy chủ bận rộn với hàng nghìn kết nối. Bài viết tập trung vào IPv4 với cờ -4 và đưa ra các mẫu lệnh hay dùng: xem toàn bộ socket TCP, UDP và raw, xem kết nối TCP, xem các cổng đang lắng nghe, hiển thị tên tiến trình và PID khi có quyền, lọc theo trạng thái như state established, lọc theo cổng bằng biểu thức như dport = :443, lọc theo địa chỉ như src 192.168.1.100, và xem thống kê tổng quan bằng ss -s. Các cờ -t, -u, -l, -n, -p lần lượt tương ứng với TCP, UDP, socket đang lắng nghe, hiển thị dạng số và thông tin tiến trình.

Phần hữu ích nhất là bảng đối chiếu từ netstat sang ss: những lệnh quen tay như netstat -an, netstat -tnp, netstat -tlnp gần như chuyển thẳng thành ss -an, ss -tnp, ss -tlnp. Trong chẩn đoán hằng ngày, ss -4tulnp là lệnh gọn nhất để xem các socket TCP và UDP IPv4 đang lắng nghe kèm tiến trình sở hữu, còn ss -4tn state established phù hợp khi cần xem các kết nối TCP IPv4 đang hoạt động. Đây là một bảng tra cứu ngắn gọn, thực dụng cho công việc vận hành Linux.


Bài viết đã được viết lại bởi Claude Code với Opus 5.5 vào ngày 27/09/2026.

Made by miti99 with ❤️
Built with Hugo
Theme Stack thiết kế bởi Jimmy