Giới Thiệu
Trong quá trình làm việc trên một dự án, tôi đã gặp phải một thách thức lớn: tốc độ phát triển nhanh hơn nhiều so với quy trình kiểm thử chất lượng (QA). Cảm giác khi bạn liên tục phát hành mã hàng ngày, nhưng kiểm thử và kiểm tra từ phía khách hàng lại diễn ra theo từng đợt là rất khó chịu. Điều này đã dẫn đến việc tôi trì hoãn việc triển khai các thay đổi mà tôi đã thực hiện từ một hoặc hai tuần trước. Vậy điều gì xảy ra khi một cái gì đó bị lỗi sau khi đã kiểm thử, và tôi đã hợp nhất nhiều thay đổi khác? Tôi có nên hoàn tác mọi thứ không? Hay chỉ chọn lựa các thay đổi cần thiết? Đương đầu với các xung đột khi sản phẩm đang gặp sự cố?
Giải pháp tôi tìm thấy cho hầu hết những cơn đau đầu này chính là tính năng flag.
Mặc dù vậy, tôi cũng muốn một số tính năng có thể cấu hình ở cấp độ người dùng. Không chỉ đơn thuần là công tắc bật/tắt toàn cầu, mà là cấu hình riêng cho từng người dùng. Ví dụ, tôi không muốn thay đổi hành vi chính cho tất cả người dùng, chỉ cho một số thử nghiệm cụ thể.
Từ đó, tôi nhận ra rằng tôi cần hai cấp độ: kiểm soát toàn cầu và kiểm soát theo người dùng.
🎯 Tại Sao Điều Này Quan Trọng
Để minh họa cho điều này, hãy cùng xem một số ví dụ thực tế:
Ác Mộng Kiểm Thử Thanh Toán 🤑
Chúng tôi muốn kiểm thử thanh toán trên môi trường sản xuất. Mức giá tối thiểu cho một giờ là 60 đô la. Nhưng tôi không muốn chi trả số tiền đó chỉ để kiểm thử quy trình thanh toán (có thể phải làm nhiều lần). Vì vậy, tôi đã xây dựng cấu hình ở cấp độ người dùng để thiết lập mức giá tối thiểu. Bây giờ tôi có thể đặt mức giá đó là 1 đô la để kiểm thử. Điều này giống như cấu hình động hơn là tính năng flag, nhưng cùng một cơ chế.
Hệ Thống Tín Dụng Mới 💳
Chúng tôi đã giới thiệu tín dụng trong ứng dụng, do đó thanh toán không chỉ còn thông qua Stripe nữa. Vì tôi coi tiền là phần quan trọng nhất của bất kỳ ứng dụng nào, tôi muốn một công tắc tắt tức thì. Tắt nó đi khi có sự cố, bật lại khi chúng tôi tự tin.
Địa Ngục Hoàn Tác 🔥
Bạn luôn có xu hướng mắc lỗi khi đang gấp rút. Phát hành lên sản phẩm, lỗi xảy ra, bạn đã bỏ qua một số cấu hình, và mọi thứ hoạt động khác đi. Bạn cần hoàn tác, nhưng bạn đã hợp nhất nhiều thay đổi. Xung đột khi sản phẩm đang gặp sự cố? Đó là công thức cho nhiều sai lầm hơn.
Với tính năng flag? Chỉ cần bật công tắc. Không cần quy trình phát hành.
🙄 Vấn Đề: Địa Ngục Phát Hành
Vấn đề chính của việc không có tính năng flag không phải là bạn kém linh hoạt hơn. Vấn đề là quy trình phát hành của bạn bị ràng buộc chặt chẽ với quy trình triển khai.
Để thay đổi bất kỳ điều gì, bạn cần phát hành một phiên bản hoàn toàn khác của mã. Điều này có nghĩa là phải trải qua toàn bộ quy trình kiểm thử. Thời gian là tiền bạc, và điều này đã đốt cháy cả hai.
Thực tế hàng ngày của tôi là: Tôi muốn phát hành mã hàng ngày. Tôi đang làm việc trên Tính Năng A - giả sử là tích hợp lịch bên ngoài ảnh hưởng đến khả năng có mặt của người dùng. Vì khả năng có mặt là phần cốt lõi của quy trình kinh doanh, nó rất quan trọng.
Không có tùy chọn an toàn nào để phát hành từng commit trước khi hoàn tất tính năng này. Vì vậy, tôi xếp chồng tất cả các thay đổi, hoàn thành mọi thứ, nhấn nút triển khai, và cầu nguyện rằng nó không phát nổ.
Nếu nó phát nổ thì sao?
Thay đổi mã → Các commit → Kiểm thử → Phát hành (DEV → TEST → PROD) → Quay lại điểm xuất phát
Giải pháp thay thế mà tôi đã xây dựng là:
Giấu tác động phía sau các tính năng flag ngay từ ngày đầu tiên.
def is_available(self, user_id, datetime):
if feature_flag_check('EXTERNAL_CALENDAR_INTEGRATION'):
return check_availability_in_external_calendar(user_id, datetime)
return check_basic_availability(user_id, datetime)
Từ commit đầu tiên, tôi có thể đẩy lên PROD ngay cả khi hàm này trông như sau:
def check_availability_in_external_calendar(user_id, datetime):
return True # TODO: thực hiện logic thực tế
Bởi vì tôi chỉ cần đặt EXTERNAL_CALENDAR_INTEGRATION thành False.
Bước tiếp theo? Chuyển nó thành True cho một người dùng cụ thể, kiểm thử, và nếu nó phát nổ - lập tức chuyển lại thành False. Không cần triển khai, không cần CI, không cần quy trình QA. Chỉ nhanh hơn và an toàn hơn.
💡 Giải Pháp Của Tôi: Giữ Nó Đơn Giản, Giữ Nó Nhanh
Tại thời điểm này, hãy cùng bàn về cách tôi xây dựng điều này. Đây là một cơ chế đơn giản để bắt đầu. Bạn có thể sử dụng các giải pháp bên ngoài, nhưng hãy xây dựng một cái tùy chỉnh thực sự hoạt động.
Những gì bạn cần:
- Lưu trữ cho các tính năng flag
- Bảng điều khiển quản trị cho các thao tác CRUD
- Công cụ để sử dụng chúng trong mã của bạn
Trong ứng dụng của tôi, tôi đang sử dụng bus sự kiện trong bộ nhớ và mẫu ngữ cảnh mà tôi đã mô tả ở đây. Điểm mấu chốt: Một tính năng flag có bật/tắt hay không? Phụ thuộc vào ai đang hỏi.
Tôi lưu trữ các flag trong biến Context toàn cầu theo yêu cầu. Đối với mỗi yêu cầu, khi xây dựng ngữ cảnh, tôi lấy tất cả các flag tính năng đang được kích hoạt toàn cầu và những flag cho người dùng cụ thể.
class FeatureFlagsModel(BaseModel):
__tablename__ = 'feature_flags'
__table_args__ = (
UniqueConstraint('key', 'user_id', name='uq_feature_flag_key_user'),
Index('ix_feature_flag_user_id', 'user_id'),
Index('ix_feature_flag_key', 'key'),
)
key: Mapped[ConfigKey] = mapped_column(String, nullable=False)
enabled: Mapped[bool] = mapped_column(nullable=False, default=False)
value: Mapped[Optional[str]] = mapped_column(nullable=True)
user_id: Mapped[Optional[str]] = mapped_column(
ForeignKey('[users.id](http://users.id)', ondelete='CASCADE'),
nullable=True
)
user: Mapped[Optional['UserModel']] = relationship(back_populates='configuration')
enabled_globally: Mapped[bool] = mapped_column(nullable=False, default=False)
Logic kiểm tra rất đơn giản:
def check_ff(self, key: ConfigKey) -> bool:
if key in self.feature_flags:
flag = self.feature_flags[key]
# Tùy chọn cá nhân của người dùng ghi đè cài đặt toàn cầu
if flag.user_id is not None:
return flag.enabled
# Cờ toàn cầu
return flag.enabled_globally
return False
Đó là tất cả. Các flag cá nhân của người dùng ghi đè lên các flag toàn cầu. Trình tự rõ ràng, hành vi dự đoán được.
🎉 Những Gì Tôi Đã Thực Sự Xây Dựng (Sự Thật Thành Thật)
Hãy để tôi thành thật với bạn - tôi gọi nó là "tính năng flag" nhưng tôi chủ yếu xây dựng một hệ thống cấu hình động.
Dưới đây là 3 cấu hình tôi thực sự sử dụng trong sản xuất:
MINIMUM_TIME_REQUIRED_BEFORE_BOOKING_IN_HOURS- Kiểm soát thời gian cần thiết để đặt chỗ. Chúng tôi đặt thời gian này là 24h nhưng để kiểm thử, nó có thể là 0.CAPTIONER_HOURLY_RATE_MIN- Mức giá tối thiểu cho một giờCAPTIONER_HOURLY_RATE_MAX- Mức giá tối đa cho một giờ
Đây không phải là các flag boolean - chúng là các giá trị cấu hình có kiểu mà tôi có thể thay đổi mà không cần triển khai.
# Trong chính sách đặt chỗ của tôi
min_booking_time = now + timedelta(
hours=Current.context.config.minimum_time_required_before_booking_in_hours
)
# Trong xác thực tỷ lệ
if not (config.captioner_hourly_rate_min <= rate <= config.captioner_hourly_rate_max):
raise ValidationError("Tỷ lệ ngoài phạm vi cho phép")
💭 Bài Học & Những Gì Tiếp Theo
Xây Dựng so với Mua: Bắt đầu tùy chỉnh vì nó nhanh hơn và không quá phức tạp để triển khai.
Vấn Đề Thiếu TTL: Đây là điều mà tôi nên triển khai ngay từ đầu - hết hạn tự động cho các flag. Tính năng flag được thiết kế để tạm thời. Nếu không có TTL, chúng sẽ trở thành nợ kỹ thuật vĩnh viễn. Bạn sẽ kết thúc với hàng chục flag cũ làm lộn xộn mã của bạn và cơ sở dữ liệu.
Trong một hệ thống tính năng flag thực sự, mỗi flag nên có ngày hết hạn. Khi các flag hết hạn, chúng nên:
- Tự động vô hiệu hóa và thông báo cho đội ngũ
- Buộc quyết định dọn dẹp mã
- Tự động xóa bản thân
Điều này ngăn ngừa vấn đề "nghĩa địa flag" nơi bạn có hơn 50 flag và không ai nhớ được một nửa trong số chúng có tác dụng gì.
Những gì tôi sẽ thêm tiếp theo:
- Triển khai TTL với dọn dẹp tự động
- Đổi tên thành "cấu hình động với tính năng bật/tắt"
🤔 Đến Lượt Bạn
Quản lý cấu hình của bạn trông như thế nào? Bạn vẫn đang phải triển khai để thay đổi một giá trị duy nhất? Hay bạn đã xây dựng điều gì tương tự?
Tôi rất tò mò về cách bạn tiếp cận cấu hình theo người dùng và cách bạn xử lý các tác động về hiệu suất.
Hãy để lại một bình luận - tôi rất muốn nghe về những thành công và thất bại trong việc sử dụng tính năng flag của bạn.
Muốn thêm nhiều bài viết như thế này? Tôi viết về kiến trúc phần mềm thực tiễn và những vấn đề thực tế mà chúng tôi giải quyết trong sản xuất. Hãy xem bài viết của tôi về xây dựng hệ thống sự kiện hoặc sửa lỗi trong FastAPI.