Thiết kế hệ thống tốt
(seangoedecke.com)- Thiết kế hệ thống tốt là kiểu thiết kế không trông quá phức tạp và không phát sinh vấn đề đáng kể trong thời gian dài
- Việc xử lý trạng thái (state) là phần khó nhất trong thiết kế hệ thống, và điều quan trọng là giảm số lượng thành phần lưu trữ trạng thái xuống mức thấp nhất có thể
- Cơ sở dữ liệu chủ yếu là nơi lưu trữ trạng thái, nên cần một cách tiếp cận tập trung vào thiết kế schema và indexing, cũng như giải quyết điểm nghẽn
- Caching, xử lý sự kiện, tác vụ nền nên được đưa vào một cách thận trọng để phục vụ hiệu năng và bảo trì, và tốt hơn là tránh lạm dụng
- Thay vì thiết kế phức tạp, việc sử dụng hợp lý các thành phần và phương pháp luận đơn giản đã được kiểm chứng là chìa khóa để xây dựng hệ thống bền vững và ổn định
Định nghĩa thiết kế hệ thống và cách tiếp cận tổng thể
- Nếu thiết kế phần mềm là việc lắp ráp mã nguồn, thì thiết kế hệ thống là quá trình kết hợp nhiều dịch vụ khác nhau
- Các thành phần chính của thiết kế hệ thống gồm app server, cơ sở dữ liệu, cache, queue, event bus, proxy
- Một thiết kế tốt tạo ra những phản ứng như “không có vấn đề gì đặc biệt”, “kết thúc dễ hơn tưởng tượng”, “không cần phải bận tâm đến phần này”
- Ngược lại, một thiết kế phức tạp và quá nổi bật có thể che giấu vấn đề gốc rễ hoặc cho thấy hệ thống đã bị thiết kế quá mức
- Thay vì đưa vào một hệ thống phức tạp ngay từ đầu, hướng đi có lợi hơn là bắt đầu từ cấu trúc đơn giản tối thiểu có thể vận hành rồi dần phát triển
Phân biệt trạng thái (state) và phi trạng thái (stateless)
- Phần khó nhất trong thiết kế phần mềm chính là quản lý trạng thái
- Những dịch vụ không lưu trữ thông tin mà trả kết quả ngay lập tức (như render PDF của GitHub) là phi trạng thái
- Ngược lại, những dịch vụ thực hiện ghi vào cơ sở dữ liệu là đang quản lý trạng thái
- Nên giảm tối đa số thành phần lưu trữ trạng thái trong hệ thống. Điều này giúp hạ thấp độ phức tạp và khả năng xảy ra sự cố
- Kiến trúc được khuyến nghị là chỉ để một dịch vụ đảm nhận việc quản lý trạng thái, còn các dịch vụ khác tập trung vào vai trò phi trạng thái như gọi API hoặc phát sinh sự kiện
Thiết kế cơ sở dữ liệu và điểm nghẽn
Thiết kế schema và index
- Để lưu trữ dữ liệu, cần thiết kế schema ở dạng con người dễ đọc
- Schema quá linh hoạt (ví dụ: lưu toàn bộ vào cột JSON) có thể tạo gánh nặng cho mã ứng dụng và hiệu năng
- Cần thiết lập index phù hợp dựa trên các cột sẽ được truy vấn thường xuyên. Đánh index cho mọi thứ ngược lại còn tạo thêm overhead không cần thiết
Cách giải quyết điểm nghẽn
- Truy cập cơ sở dữ liệu thường là một điểm nghẽn nặng
- Về hiệu năng, có lợi hơn nếu xử lý dữ liệu phức tạp ngay trong cơ sở dữ liệu bằng JOIN thay vì làm ở tầng ứng dụng
- Khi dùng ORM, cần cẩn thận với lỗi phát sinh query bên trong vòng lặp
- Khi cần, cũng có thể chia nhỏ query để điều chỉnh tải cho cơ sở dữ liệu hoặc độ phức tạp của query
- Chiến lược phân tán các truy vấn đọc sang read-replica rất hiệu quả để giảm tải cho node chính (write)
- Khi lượng lớn query đổ dồn vào, transaction và thao tác ghi có thể dễ dàng làm quá tải cơ sở dữ liệu, vì vậy nên cân nhắc throttling (giới hạn) query
Tách biệt tác vụ chậm và tác vụ nhanh
- Những thao tác người dùng tương tác cần phản hồi trong vòng vài trăm mili giây
- Với các tác vụ mất nhiều thời gian (ví dụ: chuyển đổi PDF dung lượng lớn), mô hình hiệu quả là chỉ cung cấp ngay phần việc tối thiểu ở phía trước, còn lại chuyển sang nền
- Tác vụ nền thường hoạt động dưới dạng kết hợp giữa queue (ví dụ: Redis) và job runner
- Với các tác vụ được lên lịch xa trong tương lai, cách thực tế hơn là quản lý bằng một bảng DB riêng thay vì Redis, rồi dùng scheduler để thực thi
Caching
- Caching giúp tiết kiệm chi phí và cải thiện hiệu năng khi cùng một phép tính hoặc phép tính đắt đỏ được lặp lại
- Thông thường, junior engineer mới học cache sẽ muốn cache mọi thứ, còn engineer nhiều kinh nghiệm thì thận trọng hơn trong việc đưa cache vào
- Cache đưa thêm một trạng thái mới vào hệ thống, nên tồn tại rủi ro về đồng bộ hóa/lỗi/stale data
- Tốt hơn là trước tiên thử cải thiện hiệu năng như thêm index cho query rồi mới áp dụng cache
- Với cache dung lượng lớn, ngoài Redis/Memcached còn có thể dùng cách lưu định kỳ vào kho lưu trữ tài liệu như S3/Azure Blob Storage
Xử lý sự kiện
- Phần lớn doanh nghiệp đều có event hub (ví dụ: Kafka), và nhiều dịch vụ được phân tán xử lý theo hướng event-driven
- Thay vì lạm dụng event, thiết kế API request–response đơn giản lại hữu ích hơn về mặt logging và xử lý sự cố
- Xử lý dựa trên event phù hợp khi bên phát không cần quan tâm đến hành vi của bên nhận, hoặc trong các kịch bản dung lượng lớn và chấp nhận độ trễ
Cách truyền dữ liệu: push và pull
- Có hai cách truyền dữ liệu là Pull (yêu cầu rồi nhận phản hồi) và Push (tự động gửi khi có thay đổi)
- Cách Pull thì đơn giản nhưng có thể phát sinh vấn đề lặp lại request và quá tải
- Cách Push cho phép máy chủ gửi ngay dữ liệu tới client khi có thay đổi, nên hiệu quả hơn và thuận lợi cho việc duy trì dữ liệu mới nhất
- Khi xử lý lượng lớn client, cần mở rộng hạ tầng phù hợp với từng cách tiếp cận (queue sự kiện, nhiều cache server, v.v.)
Tập trung vào hot path
- Hot path là đường đi quan trọng nhất trong hệ thống, nơi dữ liệu lưu chuyển nhiều nhất
- Hot path có ít lựa chọn hơn, và nếu thiết kế thất bại có thể gây ra vấn đề nghiêm trọng cho toàn bộ dịch vụ, nên bắt buộc phải thiết kế cẩn thận
- So với các tính năng phụ có nhiều lựa chọn, việc tập trung nguồn lực vào thiết kế và kiểm thử hot path sẽ hiệu quả hơn
Logging, metric, tracing
- Khi xảy ra sự cố, để chẩn đoán nguyên nhân thì cần tích cực ghi lại log chi tiết cho các đường đi bất thường (unhappy path)
- Cần thu thập các chỉ số quan sát được cơ bản như tài nguyên hệ thống (CPU/bộ nhớ), kích thước queue, thời gian request/tác vụ
- Thay vì chỉ nhìn giá trị trung bình, nhất định phải quan sát các chỉ số phân phối như độ trễ p95, p99. Một số ít request chậm nhất có thể là vấn đề của nhóm người dùng quan trọng
Kill switch, retry, khôi phục sự cố
- Việc sử dụng chiến lược kill switch (tạm chặn hệ thống) và retry một cách hợp lý là rất quan trọng
- Retry một cách vô điều kiện chỉ làm tăng gánh nặng cho dịch vụ khác; chỉ hiệu quả khi có cơ chế kiểm soát request trước đó như circuit breaker
- Việc đưa vào Idempotency Key giúp tránh xử lý trùng lặp khi cùng một request được gửi lại
- Trong một số tình huống sự cố, cần chọn giữa fail open hoặc fail closed. Ví dụ, Rate Limiting nên theo hướng fail open (cho phép) để giảm tác động đến người dùng. Ngược lại, xác thực thì bắt buộc phải fail closed
Kết luận
- Dù một số chủ đề như tách dịch vụ, container, VM, tracing đã được lược bỏ, việc dùng các thành phần đã được kiểm chứng đúng chỗ vẫn là con đường ổn định nhất để xây dựng hệ thống về lâu dài
- Những thiết kế “đặc biệt” về mặt kỹ thuật thực ra rất hiếm; trái lại, thiết kế đơn giản đến mức nhàm chán mới là thứ được dùng thường xuyên nhất trong thực tế
- Về bản chất, thiết kế hệ thống tốt là quá trình kết hợp an toàn các phương pháp đã được chứng minh đầy đủ mà không quá phô trương
1 bình luận
Ý kiến Hacker News
Tôi thường cảm thấy mình khá cô độc ở điểm này. Kỹ sư nhìn vào các hệ thống phức tạp thì thấy nhiều yếu tố thú vị nên nghĩ rằng “đây mới đúng là nơi diễn ra thiết kế hệ thống thực thụ!”, nhưng thực ra hệ thống phức tạp nhiều khi lại là kết quả của việc thiếu một thiết kế tốt. Nếu đang tìm việc, bạn nên quên sạch điều này trong lúc phỏng vấn. Tôi cũng từng mắc sai lầm khi nói thẳng suy nghĩ đó trong một buổi phỏng vấn thiết kế hệ thống. Trong một buổi phỏng vấn ứng dụng startup giả định, tôi đã trả lời kiểu như “ở mức QPS này thì có thể không cần quan tâm đến backpressure”, “không cần dùng queue thay cho cron job, dù tất nhiên vẫn có trade-off”, “SQL hay NoSQL? Dùng thứ mà team hiểu rõ nhất là được”. Nhưng người phỏng vấn không muốn nghe những câu trả lời như vậy. Bạn phải lấp đầy cả bảng trắng và đưa ra một thiết kế phức tạp tới mức Kubernetes quản lý cả Kubernetes thì mới phát ra tín hiệu mà họ muốn
Tôi nói điều này với tư cách người đã trải qua hàng trăm buổi phỏng vấn thiết kế hệ thống và đã huấn luyện nhiều người. Những câu trả lời bạn nêu ra truyền tín hiệu khá yếu, ngoại trừ phần nói về queue. Điều người phỏng vấn thực sự muốn biết là bạn đưa ra quyết định đó vì sao, đã cân nhắc những yếu tố nào, và muốn nghe quá trình tư duy của bạn. Nếu bạn không giải thích chi tiết câu trả lời, từ góc nhìn của người phỏng vấn rất dễ đi đến kết luận “không thu được bao nhiêu thông tin”. Vì vậy ứng viên cần chủ động truyền tải thông tin mà người phỏng vấn đang tìm kiếm. Ngoài ra, ngay cả một người phỏng vấn giỏi, nếu vẫn phải cố moi từng ý từ bạn, họ cũng sẽ ghi chú kiểu “lý lẽ thì hợp lý nhưng giao tiếp kém hiệu quả”. Kỹ năng giao tiếp cũng là một phần được đánh giá. Cuối cùng, tôi không đồng ý với câu trả lời SQL/NoSQL. Kinh nghiệm của team là quan trọng, nhưng khác biệt giữa các công nghệ là rất rõ, và tùy tình huống mà chênh lệch hiệu năng cũng lớn. Câu trả lời đó dễ tạo cảm giác rằng bạn chưa có nhiều trải nghiệm trong các bối cảnh đa dạng
Như câu “phỏng vấn là hai chiều”, tôi thấy các câu trả lời của bạn rất hợp lý. Nếu tôi là người phỏng vấn, tôi còn chấm điểm cao hơn. Ngược lại, nếu một công ty đánh trượt bạn vì những câu trả lời như vậy, khả năng cao chính công ty đó mới là nơi không ổn. Nhưng thực tế thì nhiều khi mình cần nhanh chóng ổn định chỗ đứng, nên cũng cần biết cân bằng và điều chỉnh câu trả lời theo thứ đối phương muốn nghe
Lời khuyên này không tốt. Một thiết kế đơn giản mà thanh lịch không bắt đầu từ việc phớt lờ các vấn đề tiềm ẩn. Các câu hỏi truy vấn sâu hơn không phải là lúc để tuôn ra trivia kỹ thuật, mà là tín hiệu rằng hai bên nên cùng thảo luận. Những câu trả lời của bạn không cho thấy sự sáng suốt mà lại tạo ấn tượng còn non tay. Đây không phải lỗi của người phỏng vấn
Tôi đồng cảm với ý “phỏng vấn là hai chiều” mà bình luận bên cạnh đã chỉ ra, nhưng một người phỏng vấn tốt sẽ thẳng thắn nói “câu trả lời này cũng ổn, nhưng hiện tại tôi đang kiểm tra kiến thức của bạn về chủ đề này”. Nếu bản thân họ cứ tiếp tục nói những chuyện lạc đề thì đó mới là tín hiệu đáng lo
Tôi nghĩ đây là ví dụ cho thấy chính xác vì sao LinkedIn-driven development tồn tại. Ngoài đời, việc liệt kê cả đống công nghệ trong CV trông ngầu hơn rất nhiều so với việc giải thích rằng bạn chỉ dùng tốt một Postgres và một modular monolith
Tôi thấy đây là một bài rất hay. Nhưng tôi cũng muốn nhắc đến giới hạn của các best practice kiểu này. Ví dụ, có lời khuyên rằng “đừng để 5 service khác nhau cùng ghi vào một bảng, hãy để 4 service gọi API hoặc bắn event, và chỉ 1 service được ghi vào bảng”. Thực tế không phải lúc nào cũng chia tách gọn gàng như vậy. Nếu cả năm cùng truy cập DB thì về cơ bản bạn đã đang xây một hệ phân tán, nhưng DB vốn đã hỗ trợ quyền hạn, transaction và custom query nên đôi khi lại không cần phải thiết kế thêm một interface riêng. Ngược lại, nếu tạo một interface mức cao bằng một service, thì giờ bạn lại phải tự xây thêm xác thực, transaction và xử lý ngoại lệ. Thành ra sẽ nảy sinh câu hỏi: như vậy có phải là đang thêm nhiều failure mode hơn và cả “thuế vận hành” của microservice phức tạp hơn không? Mặt khác, việc nhiều service cùng truy cập một DB tự nó cũng có thể là một code smell. Có lẽ DB này là dấu vết của nhiều DB bị gộp lại, và thật ra số service cũng có thể giảm xuống còn hai hoặc ba
Với câu hỏi “được gì”, API có khả năng thích nghi với thay đổi cao hơn rất nhiều so với việc dùng chung schema DB. Sau khi làm việc với nhiều hệ thống, tôi sẽ không thiết kế lại theo kiểu nhiều service chia sẻ một DB nữa. Với các công ty nhỏ đầu những năm 2000 thì có thể ổn, nhưng từ đó về sau tôi gần như chỉ thấy thất bại, ngoại trừ trường hợp chỉ tách riêng đường đọc/ghi trong cùng một service
Tôi không đồng ý với lập luận rằng DB là interface nên không cần thiết kế riêng. Khi nhiều client dùng cùng một DB, pattern truy cập sẽ khác nhau và vấn đề migration cũng lớn hơn. Cuối cùng vẫn phải thêm các thiết kế bổ sung như view, quản lý quyền hạn, v.v., và gánh nặng bảo trì cũng tăng lên. Trong tình huống lý tưởng, API sạch sẽ hơn nhiều. Ngoài đời, vì áp lực ra tính năng nhanh mà người ta cho phép truy cập trực tiếp DB như một đường tắt, nhưng gốc rễ là vì nhiều người ngại tái thiết kế toàn bộ để phù hợp với yêu cầu hay thiết kế mới
Khi cần thay đổi, mục tiêu là giảm thiểu phạm vi phối hợp liên quan. Nếu cần thay đổi cấu trúc của datastore, bạn phải kiểm soát mọi nơi truy cập vào datastore đó, nên càng ít đường truy cập thì thay đổi càng dễ. Ví dụ, ở nơi tôi làm việc, chỉ riêng việc tách DB ra đã khiến hơn 40 team phải sửa code. Nếu một thay đổi như vậy đã xảy ra chỉ vì “yêu cầu tính năng” thì còn đỡ. Nếu nó xảy ra vì vấn đề “khả năng mở rộng”, có thể chính sản phẩm sẽ sụp đổ
Có người gọi việc nhiều service cùng gắn vào một DB là “code smell”, nhưng nhìn ngược lại, nếu mỗi service đều phải có DB vật lý riêng thì tính sẵn sàng có thể tăng từ N lên N lũy thừa M và trên thực tế lại kém ổn định hơn, ít nhất nếu đang nói ở cấp độ cụm DB
Khi cần truy vấn cơ sở dữ liệu, thường thì cách hiệu quả nhất đúng là truy vấn trực tiếp DB. Nếu cần dữ liệu từ nhiều bảng, bạn nên dùng join thay vì truy vấn riêng từng bảng trong ứng dụng rồi tự ghép lại. Và tôi cũng rất khuyến nghị dùng view, thậm chí cả stored procedure. View là một lớp trừu tượng dữ liệu nên rất hữu ích cho thiết kế, còn SQL nếu viết tốt thì cũng dễ hiểu và dễ bảo trì
Đây chính là lý do ORMs gây ra rất nhiều vấn đề. Trong môi trường SSR, cách làm hiệu quả và thanh lịch để vận hành các dịch vụ web lớn là ở mỗi MVC view dùng trực tiếp SQL view hoặc custom query. Hãy giao phần việc nặng cho RDBMS, còn web server chỉ việc đưa kết quả SQL thẳng vào bảng. Các RDBMS legacy như MSSQL hay Oracle có rất nhiều tối ưu hóa tích hợp sẵn. Còn ORMs thì ép buộc một object model duy nhất nên gần như không có độ linh hoạt
Stored procedure có vẻ hữu ích, nhưng trên thực tế do giới hạn của ngôn ngữ như T-SQL nên cả team khó có thể thống nhất phát triển bằng một ngôn ngữ hiện đại mà ai cũng quen. Tôi đang phải bảo trì một codebase T-SQL lớn, và từ version control đến công cụ chẩn đoán đều không mấy tốt. Code của nhân viên mới ít ra còn đọc được, còn T-SQL thì là ác mộng
Tôi không đồng ý. Trong các kiến trúc hiện đại ưu tiên khả năng mở rộng, tốt hơn là thực hiện join ở backend phía trước DB. Hãy để DB làm các truy vấn chỉ mục đơn giản, còn join thì để backend xử lý; như vậy khả năng mở rộng của DB tốt hơn và tốc độ cũng nhanh hơn. Vì việc tăng thêm instance server dễ hơn nhiều so với mở rộng DB. Chỉ khi join bắt buộc phải diễn ra ở DB do dữ liệu quá lớn thì lúc đó mới cần thay đổi kiến trúc. Nếu thậm chí có thể đẩy cả phần join ra frontend thì còn có lợi cho cache kết quả nữa
Có thật vậy không? Ví dụ với 10.000 khách hàng và 1.000.000 đơn hàng, nếu join rồi truyền toàn bộ một bảng có 20 trường khách hàng + 5 trường đơn hàng thì bạn sẽ phải truyền 25.000.000 trường. Nếu lấy độc lập bằng hai query rồi mới join, bạn chỉ truyền 5.000.000 trường đơn hàng + 200.000 trường khách hàng. Xét về băng thông và hiệu năng thì cách này tốt hơn nhiều
Quy tắc này là một điểm khởi đầu ổn, nhưng bạn phải hiểu rõ khi nào nên ngoại lệ. Có một ứng dụng tôi từng phụ trách mà join làm số record phình lên theo cấp số nhân. Vì thế khi tách query ra, lợi ích từ việc xử lý/lọc kết quả lại lớn hơn chi phí network overhead, nên nhanh hơn hẳn. Về sau chúng tôi còn chuyển sang lưu toàn bộ dữ liệu bằng JSONB và kết quả lại còn tốt hơn nữa
Tôi thấy hơi tiếc là nói về thiết kế hệ thống tốt nhưng lại không hề nhắc đến problem domain. Trong thiết kế hệ thống, phần cốt lõi nhất và cũng khó nhất là interface mà hệ thống cung cấp cho người dùng. Xét cho cùng, một hệ thống phần mềm là một sự đánh đổi của bài toán: “Tôi sẽ cung cấp cho bạn chức năng này, đổi lại bạn phải hiểu cấu trúc/mô hình như thế này”. Sai lầm trong thiết kế interface là thứ tốn kém nhất, và nếu phần lớn thời gian của bạn không dành để thảo luận về interface thì bạn đang bỏ lỡ điều quan trọng nhất. Những yếu tố hệ thống khác sau này hoàn toàn có thể sửa mà không ảnh hưởng đến người dùng
Tôi rất đồng cảm với câu “thiết kế tốt không phô bày chính nó, còn thiết kế tệ lại thường trông có vẻ ấn tượng hơn”. Có vẻ cách đánh giá kỹ sư theo tiêu chí “độ phức tạp” đã vô tình khuyến khích overengineering. Nguyên lý KISS đã không được nhìn nhận đúng mức trong một thời gian dài
Thỉnh thoảng tôi nhìn lại những phần trong codebase mà mình đã lướt qua không cần nghĩ ngợi gì, và chính những chỗ đó lại là dấu hiệu cho thấy đã có một thiết kế tốt
Điều này thật không may nhưng là sự thật. Phần lớn mọi người bị hấp dẫn hơn bởi các giải pháp phức tạp, và nếu đưa ra một câu trả lời đơn giản thì lại dễ bị xem là kém năng lực. Nhưng trên thực tế, cấu trúc đơn giản, dễ quản lý mới đóng góp lớn hơn cho thành công chung của dự án. Tất nhiên cũng có những bài toán vốn dĩ phức tạp, nhưng phần lớn chỉ là web app bình thường
Điều quan trọng nhất trong thiết kế schema là tính linh hoạt. Khi dữ liệu đã tích lũy nhiều thì thay đổi schema trở nên cực kỳ khó. Nhưng nếu thiết kế quá linh hoạt, kiểu nhét mọi thứ vào JSON hay dùng cấu trúc EAV, thì code ứng dụng sẽ phức tạp vô hạn và kéo theo đủ thứ vấn đề hiệu năng kỳ quặc. Vì vậy tôi thường thích những schema mà chỉ cần nhìn vào cấu trúc bảng là đã hiểu ngay chúng dùng để làm gì, tức là dễ đọc với con người. Cứ phải nhìn EAV hay cột/bảng JSON thường xuyên là thật sự muốn bỏ nghề. Rõ ràng EAV vẫn có những trường hợp hữu ích, nhưng đa số trường hợp nó chỉ mang hỗn loạn vào thực tế triển khai. N+1 problem, tạo query động, lưu dữ liệu audit trong cùng DB để rồi nó bị hút vào business logic, môi trường Oracle phức tạp, hay việc phân chia sai cái gì nên nằm trong DB và cái gì nên nằm ở app — từng biến số như vậy đều bào mòn nghiêm trọng chất lượng cuộc sống của lập trình viên
Liên quan đến điểm này, cuốn “SQL Antipatterns” của Bill Karwin giới thiệu rất rõ các rủi ro và giới hạn của pattern EAV. Dù vậy, đôi khi khi schema khó vẽ ra ngay, nó vẫn có thể được dùng như giải pháp tạm, chẳng hạn cột JSONB trong Postgres, nhưng không thể trở thành quy tắc mẫu mực. Nếu có thể chuẩn hóa thì luôn nên chọn chuẩn hóa
Chỗ nói rằng “nếu lưu dữ liệu audit trong cùng một DB thì cuối cùng nó sẽ trở thành một phần của business logic và gây rắc rối”, vậy “cách chuẩn” ở đây là gì? Một DB riêng? Hay một storage hoàn toàn độc lập?
Với lời khuyên “tránh để 5 service cùng ghi vào một bảng, hãy để 4 service chỉ gọi API hoặc bắn event, và chỉ 1 service ghi trực tiếp vào DB”, thì phương án tốt nhất là ngay từ đầu thiết kế sao cho không có chuyện 5 service phải cùng ghi vào một bảng. Nếu chuyện đó vẫn xảy ra, có thể logic giữa các service thực tế đang chồng lấn lên nhau rất nhiều. Khi đó cần tự hỏi liệu cả 5 service này có thật sự cần tách biệt không, hay có thể gộp lại thành một. Trong thực tế, cấp cho chúng các bảng dữ liệu riêng hoặc refactor thường mới là cách giải quyết vấn đề
Việc phân biệt stateful/stateless là cốt lõi để chia trách nhiệm giữa hạ tầng và phát triển. Khi chạy dạng stateless bằng container thì có rất ít thứ có thể đi sai; nếu lỗi thì chỉ cần deploy lại là xong. Miễn là tránh các sai lầm DB đến mức phá hỏng dataset, phần lớn tình huống đều có thể khôi phục nhanh. Với mức này, những người có kinh nghiệm nghề nghiệp, thời gian làm việc và độ cẩn trọng khác nhau vẫn có thể xử lý ổn. Nhưng các vùng có state như database, file storage thì hoàn toàn khác. Chỉ một sai lầm thôi cũng có thể khiến toàn bộ doanh nghiệp gặp nguy hiểm, nên phải do người chuyên trách có nhiều kinh nghiệm thực chiến phụ trách. DB dù đang chạy ổn mà không có backup thì đã là một rủi ro lớn rồi. Đây thực sự là loại vấn đề mà không thể giải quyết chỉ bằng vài phút deploy
Về lời khuyên “dùng timestamp thay cho bool để biểu diễn”, tôi thấy đây có vẻ là một chỉ dẫn quá bao quát. Ví dụ
is_on→ true,on_at→ 1023030 thì rõ ràng, nhưngis_a_bear→ true,a_bear_at→ 12312231231 thì lại rất kỳ cục. Phần lớn con gấu đâu có một thời điểm “trở thành gấu” nào đâu… Đây là ý chỉ phù hợp trong một số hoàn cảnh nhất địnhTôi nghĩ gần như trong mọi trường hợp dùng timestamp hoặc integer thay cho boolean đều tốt hơn. Đặc biệt các trường chỉ có hai trạng thái thường hay phát triển thành “phân loại kiểu”. Ví dụ kể cả hiện giờ chỉ có gấu, bạn vẫn nên phòng trước khả năng mở rộng sang enum; còn các trường trạng thái cũng thường mở rộng vượt ra ngoài chỉ active/inactive, thành nhiều trạng thái như dừng, xóa, tạm ngưng, v.v., nên khi boolean cứ tăng lên thì hệ thống lại càng phức tạp hơn. Dùng integer tốt hơn
Nếu hiểu đúng theo mặt chữ, điều này có nghĩa là bản thân việc dùng boolean trong DB đã là một mùi code, và tôi đồng ý với điều đó. Tuy nhiên cách tiếp cận này, tức chuyển bool → timestamp, nhiều khi chỉ là một mẹo tiện lợi trong join chứ không phải cái gọi là “giải pháp hoàn chỉnh”. Nếu thay đổi theo thời gian thực là quan trọng thì ngay từ đầu dùng bảng audit mới đúng. Soft delete cũng vậy, tôi thấy nó là một giải pháp nửa vời. Ý định thật sự là để ngăn xóa nhầm, nhưng thực ra backup và khôi phục còn bảo vệ hiệu quả hơn
Kiểu dữ liệu boolean có kích thước nhỏ hơn nên với một số workload, chẳng hạn dữ liệu phân tích khối lượng lớn, nó lại hiệu quả hơn. Đôi khi việc lưu một giá trị logic bằng boolean là hoàn toàn hợp lý. Ví dụ kết quả của một process, chỉ cần đánh dấu thành công/thất bại, thì boolean rất thực dụng
Tôi không rõ vì sao lại chỉ boolean mới đáng được thay bằng timestamp.
isDarkTheme,paginationItemscũng có thể là những thứ mà người ta muốn biết thời điểm thay đổi. Cảm giác như đây thực chất là một poor-man changelogNếu là trường hợp như Bear thì dùng một giá trị enum sẽ tốt hơn
Nếu muốn tìm một cuốn sách giúp học về thiết kế hệ thống tốt từ góc nhìn trừu tượng hơn, tôi cực kỳ khuyến nghị Systemantics của John Gall. Tôi cảm thấy đây là cuốn mà kỹ sư nào cũng đáng đọc