Thiết kế tính năng Feature Toggle và Feature Rollout

Feature Toggle (hay Feature Flags) và Feature Rollout là một kĩ thuật phổ biến giúp bạn có thể quản lý được hành vi của phần mềm của mình mà không cần phải đổi code & deploy lại.

Trong bài viết này mình sẽ đi qua 1 vài tính năng từ cơ bản đến nâng cao, đồng thời chia sẽ cách tiếp cận để design và implement một hệ thống Feature Toggle/Feature Rollout nhỏ.

Một vài ứng dụng trong thực tế của chuyện này như:

  • Feature Control: Tắt/bật tính năng cho toàn bộ hệ thống, hoặc một vài khách hàng cụ thể
  • Feature rollout một tính năng từ từ cho tập user của mình, có thể trước mắt cho 1 vài user cụ thể, rồi sau đó user trong một nước/region nào đó.

Bật/tắt toàn cục

Trường hợp cơ bản nhất của Feature Toggle là giúp bạn turn on/off 1 feature mà không cần deploy lại code. Ví dụ lấy tính năng XYZ:

if FeatureToggle.check('xyz')
  # feature xyz is turned on, do what you need here
end
# code minh hoạ bằng Ruby
class FeatureToggle
  def self.check (feature_key) 
    feature = FeatureToggle.get(feature_key)
    return feature&.toggled == true
  end
end

Lúc này, bạn có thể code & deploy tính năng xyz đó trước, sau đó chọn thời điểm thích hợp để bật nó lên cho toàn bộ hệ thống. Hoặc ngược lại, khi bạn phát hiện ra lỗi, hoặc hệ thống quá tải, bạn có thể tắt bớt 1 vài tính năng phụ chỉ với 1 vài click chuột.

Thiết kế đằng sau đơn giản là một database table chứa các tính năng của bạn, cùng với 1 flag on/off:

CREATE TABLE feature_toggles (
  feature_key VARCHAR(255) PRIMARY KEY,
  toggled BOOLEAN DEFAULT FALSE
);

Bật/tắt cho khách hàng cụ thể

Ok great, bây giờ mình thảo luận trường hợp phức tạp hơn, khi mà bạn:

  1. chỉ muốn bật feature cho 1 vài khách hàng thử nghiệm (hoặc là khách hàng cao cấp)
  2. enable cho toàn bộ hệ thống, nhưng disable cho 1 vài khách hàng đặc biệt (khách hàng chưa chịu trả tiền chẳng hạn:P )
if FeatureToggle.check('xyz', current_customer_id)
  # feature xyz is turned on for this customer
end

Có thể nhận ra rằng 2 trường hợp trên giống như 2 mệnh đề đảo ngược nhau, lúc đó, bạn có thể cải thiện hệ thống bằng cách thêm vào 1 cột exception_list để chứa tập khách hàng đặt biệt này:

CREATE TABLE feature_toggles (
  feature_key VARCHAR(255) PRIMARY KEY,
  toggled BOOLEAN DEFAULT FALSE,
  exception_list JSONB DEFAULT '[]'
);

Lúc này thì:

  • toggled = false, exception_list = [1, 2]: tắt toàn bộ, vào bật cho khách hàng ID 1 và 2.
  • toggled = true, exception_list = [1, 2]: enable cho toàn bộ khách hàng, chỉ tắt cho khách hàng ID 1 và 2.

 

alt text

Trong ảnh: UI features toggles của Holistics (dựa trên nền ActiveAdmin).

Toggle cho % tập người dùng (hay là Feature Rollout)

Giả sử bây giờ, bạn vừa deploy 1 feature mới và muốn release thử nghiệm cho 5% tập users của mình thôi, thì mình phải thiết kế như thế nào?

Một cách đơn giản là thêm vào 1 cột rollout_percent, và dùng nó để check:

CREATE TABLE feature_toggles (
  feature_key VARCHAR(255) PRIMARY KEY,
  exception_list JSONB DEFAULT '[]',
  rollout_percent SMALLINT CHECK (rollout_percent >= 0 AND rollout_percent <= 100)
);

Lúc đó, với mỗi user_id, ta sẽ apply 1 hàm hash để đẩy user vào 1 tập từ 1-100, và check với giá trị rollout_percent của mình...

# trong ví dụ này ta dùng hàm hash CRC32 và mod 100
require 'zlib'
def check(feature_key, customer_id)
  ...
  hash_number = Zlib::crc32(customer_id) % 100 + 1
  return hash_number <= rollout_percent
end

Nâng cao: conditional feature rollout

Như trường hợp bạn muốn rollout % cho 1 tập người dùng thoả mãn điều kiện cụ thể, thì design sẽ không giải quyết được trường hợp này, ví dụ như:

  • 0% người dùng ở Mỹ, 10% người dùng ở UK, còn lại thì tắt hết
  • 20% người dùng Android, còn lại thì tắt hết
  • Chỉ rollout % cho người dùng trả phí, còn người dùng free thì không có
  • ...và nhiều trường hợp khác tuỳ yêu cầu của hệ thống

Phần này mình xin phép mở ra cho các bạn độc giải cho ý kiến xem có những cách design như thế nào là hợp lý. Các bạn có thể comment bên dưới về ý kiến của bạn nhé.

Lúc nào thì làm gì?

Nếu để ý kĩ các ví dụ trên, thì không phải lúc nào bạn cũng cần hết những tính năng nói trên:

  • Trường hợp bật/tắt cho khách hàng cụ thể sẽ phù hợp với các ứng dụng B2B (ví dụ: Intercom, Hubspot), khi mà người xài thuộc các company account khác nhau.
  • Trường hợp rollout theo % của user sẽ phù hợp hơn với các ứng dụng B2C (ví dụ Quora, Facebook, Kipalog) khi mỗi người xài là 1 user độc lập.

Nói vậy để thấy là tuy cũng chỉ là cơ chế feature toggle, feature rollout, nhưng mỗi doanh nghiệp khi thiết kế sẽ có nhiều bài toán con khác nhau, ảnh hưởng đến cách bạn thiết kế tính năng này.

Cải thiện tốc độ (Performance Improvement)

Ở trên ta dùng database để lưu các toggles, như vậy mỗi lần call hệ thống phải query vào database. Với những hệ thống có nhiều traffic, chuyện này sẽ làm chậm hệ thống lại rất nhiều và trở nên không thực tế.

Nhận thấy việc dữ liệu toggles ở trên không thường xuyên thay đổi nhiều (chỉ đổi khi admin vào sửa nội dung), nên ta có thể dùng caching (như Redis, hoặc lưu thẳng vào memory) để tránh việc phải có nhiều database calls. Và ta chỉ cần refresh cache khi mà ai đó vào admin để update lại toggle settings.

Một cách triển khai khác là ta hoàn toàn có thể implement trực tiếp bằng Redis mà không cần phải thông qua SQL database. Lúc này ta phải coi như dữ liệu trong Redis của bạn là persistent data và có cơ chế backup thích hợp.

Kết luận

Ở trên mình đã giới thiệu các bạn hướng tiếp cận và giải quyết bài toán Feature Toggle / Feature Rollout. Tuy nhìn đơn giản nhưng khi đi sâu vào nó có những yêu cầu đặc thù khác nhau, cho nên mỗi công ty sẽ có cách tiếp cận/thực thi khác nhau.