Feature toggling: як релізити без релізу

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт, мене звати Роман Мальчишин. Близько 5 років я є Java/SAP Commerce Cloud розробником в EPAM, в основному спеціалізуюся в E-commerce домені. В сучасному світі, де кожен день щось змінюється як і в бізнесі, так і в інших сферах життя, виникає потреба в таких же динамічних змінах в програмному забезпеченні. Тому ми, як розробники, мали б створювати продукти, які будуть достатньо гнучкими щодо внесення різноманітних змін. І саме фіча тогли є одним з інструментів, що дозволяє досягти такого підходу. Про фіча тогли як підхід я дізнався близько 1,5 року тому, відтоді я ними і зацікавився. Адже з допомогою тоглів поведінки системи, над якою працювало більше сотні людей, кардинально змінювалась як з точки зору інтерфейсу користувача так і з точки зору бізнес логіки. В свою чергу простота реалізації і можливості, які дають тогли, дозволили мені подивитись на розробку програмного забезпечення по іншому. Вже не як просто об’ємний код, класи, пакети, а як набір фіч, що взаємодіють між собою і дозволяють як конструктор будувати складні системи. Цей матеріал дозволить зрозуміти суть фіча тоглів, як їх використовувати та імплементувати, а також, де без фіча тоглів важко або навіть неможливо обійтися.

Feature Toggle

Як завжди, визначень є багато, але суть одна — фіча тогл це техніка/принцип в розробці, що дозволяє динамічно змінювати поведінку системи без змін в коді. Найпростішим прикладом тоглу може буди конфігураційний атрибут, який має тільки два значення — true/false, і відповідно, коли значення рівне true — фіча буде включена.

public void doSomething() { 
	final boolean isOn = getProperty("enable_fetature"); 
	if (isOn) { 
    	doWhenOn(); 
	} else { 
    	doWhenOff(); 
	} 
} 

З таким примітивним ‘тоглом’ практично будь-який розробник зустрічався не раз. Проте якщо зробити таку конфігурацію динамічною з красивим інтерфейсом і можливістю змінювати значення в рантаймі, відкривається багато можливостей як і для розробників, так і для бізнесу. Адже це дозволить не боятися вносити ризиковані або не фінальні зміни в систему. З точки зору розробки, тогли є корисними, коли йде робота над великою фічею, яку важко зарелізити в короткий проміжок часу. Як я писав вище, саме під час розробки такої фічі я й зацікавився використанням тоглів. Продукт розроблявся ітеративно, з релізами в кожні 3 тижні. Проте новий функціонал був занадто великим і складним, щоб зробити його в такий проміжок часу. Імплементація тривала більше пів року двома скрам командами. Частини функціоналу релізились кожну ітерацію, проте кінцеві користувачі не бачили тих змін аж до фінального релізу, коли фічу було включено. Окрім того, що фіча тогл дозволив значно спростити процес розробки (адже не потрібно було мати окрему гілку і тестові сервери з системою суто для однією нової фічі), це також дозволило проводити тестування поступово і разом з іншими фічами. Такий підхід дозволив провести реліз безболісно, швидко і з мінімальними дефектами. З точки зору бізнесу, без фіча тоглів важко або й неможливо вносити в систему експериментальні фічі або проводити А/Б тестування. Також без тоглів важко обійтись при використанні trank based development. Про це і А/Б тестування детальніше напишу далі.

Використання

Окрім основного призначення тоглів — включення/виключення функціоналу в живій системі — варіантів використання є значено більше. З основних я б виділив такі:

  • CI/CD
  • A/Б тестування (експериментальні фічі)
  • Trunk based development

CI/CD

У випадку з CI/CD, тогли дозволять швидко і просто змінювати певні конфігурації. Наприклад, коли у вас є декілька середовищ із запущеною системою, DEV (для розробки) /QA (для тестування) /PRE-PROD (для фінальних тестів перед релізом), PROD (продакшн). І є інтеграції з сторонніми платними системами (наприклад, SMS розсилка). Для того, щоб не витрачати зайві кошти, замість реальної системи можна зробити заглушку (mock), яка буде включеною для тільки DEV/QA, для запуску тестів у пайплайні або перформанс тестів. В свою чергу на PRE-PROD/PROD буде використовуватись реальна інтеграція.

А/Б тестування

А/Б тестування є окремою і великою темою для обговорення. Проте завдяки фіча тоглам, використання таких тестів стає досить простим у системах різної складності. Це значно спрощує життя як і розробникам, так і бізнесу. А/Б тести — це метод порівняння двох версії сайту (певної системи), кожна з яких буде доступна певній групі користувачів. Далі на основі взаємодії користувачів з різними версіями, проводиться аналіз того, яка з версій є кращою/більш прибутковою і тд. Найпростіший приклад — це розміщення UI елементів на сторінці з подальшим аналізом поведінки користувачів, де в одній версії основне меню є з лівої сторони, на іншій — з правої. В А/Б тестуванні користувачі діляться на різні групи: географічно чи в довільному порядку. Одній групі надається версія сайту А, іншій — Б. Далі на основі цього по кожному користувачу збирають дані відповідно до їхньої поведінки. Один з найважливіших факторів це те, що заміри мають проводитись одночасно для всіх груп. І вже після проведення всіх тих тестів, наступним і найважливішим кроком є аналіз даних, який напряму залежатиме від якості самих даних і замірів. З використанням фіча тоглів, А/Б тести можна миттєво відміняти, і взагалі їх можуть проводити бізнес користувачі без залучення розробників.

Trunk based development

Тепер хочу поговорити, де ще фіча тогли є необхідними і в даному випадку вже для розробників. Про trunk based development в бренчуванні. В ньому існує тільки одна гілка — транк (мастер) і весь код має литись туди без проміжних (develop/release) гілок. Це для того, щоб деліверити код максимально швидко і без затримок. Важливо, що транк бейсед підхід по замовчуванню заставляє мати повноцінно налаштований CI/CD. Адже весь код в мастері має бути повністю провалідовани/тестований. Це означає, що ваш код має бути завжди готовим до релізу. Як фіча тогли допомагають в такому підході? Завдяки фіча тоглам, є можливість вносити великий/складний функціонал поступово, не ламаючи при тому існуючу систему. В іншому випадку було б необхідно робити окремі паралельні довгоживучі гілки, що взагалі не сумісно з trunk based підходом. Проте, trunk based development має вибиратись на основі того, який життєвий цикл проекту, частота релізів і загальна складність. Наприклад, якщо у вас 3-4 тижневі спринти, в які беруться величезні сторіc, то скоріш за все транк бейсед не буде кращим рішенням. Або ж якщо сам пайплайн включає величезну кількість перевірок і триває години (що означає досить велику вартість кожного білда), то напевно часті мерджі також не вартуватимуть того.

Типи тоглів

Базуючись на варіантах використання тоглів, їх можна розділити на декілька типів:

  • в залежності від довготривалості (скільки часу тогл існує в системі)
  • в залежності від динамічності (наскільки часто стан тогла змінюється).

На тему тоглів та їх типів є чудова стаття від Pete Hodgson.

Тип Динамічність Довговічність Призначення
Реліз (release) — — Для функціоналу, що є не завершений/в розробці, але може бути задеплоєний в продакшн. В trunk based development.
Експериментальні (experimental) + — Для А/Б тестування.
Операційні (operation) — + Для операційної діяльності системи, для фіч, що можуть мати негативний вплив на перформанс.
Дозволу (permissioning) + + Для керування доступом до функціоналу тільки певним групам користувачів.

Базуючись на типі тоглів, можна думати над варіанти їх реалізації. Адже не завжди потрібно робити складну систему, коли стан тоглу змінюється рідко або в залежності від середовища.

Варіанти реалізацій

Варіантів як завжди багато, але я б виділив три основних:

  1. з найпримітивнішого це конфігруація в пропертях, де ви прописуйте для кожного середовища стан фіча тогла. Такий підхід може працювати коли вам треба включити якісь замокані сервіси на локалі/деві або під час девеломпенту якоїсь фічі.
  2. веб сервіс, що дозволить динамічно змінювати стан тоглів. Складність сервісу вже буде залежати від конкретних задач, але базовий функціонал можна досить швидко написати.
  3. ну і найкращим, але не завжди необхідним рішенням, може бути використання SAAS. Проте вони можуть бути не дешевими в користуванні. Щоб показати приклад імплементації, припустимо, що нам потрібно зробити фічу, яка дозволить показувати рейтинг продуктів. Але оскільки ми не знаємо наскільки корисним буде такий функіонал, вирішено використати фіча тогл, щоб в разі необхідності миттєво його виключити. Наступні приклади написано з використанням Java 11/Spring Boot/Angular.

Тогл вимкнений Тогл ввімкнений

Configuration properties (feature-toggle-config-props)

Конфігураційні проперті це найпростійший спосіб реалізації тоглів, проте не зручний у випадку подальших змін стану тогла. Для того, щоб все ж таки мати можливість змінювати стан тоглів без перезапуску програми, використовується ‘commons-configuration’. Отже, для початку нам треба сам тогл:

stars.rating.enabled=false 

Далі оголошуємо конфігурацію, яка буде кожну секунду дивитись на стан тогла:

spring.properties.refreshDelay=1000 
spring.config.location=toggle.properties 
@Bean 
@ConditionalOnProperty(name = "spring.config.location", matchIfMissing = false) 
public PropertiesConfiguration propertiesConfiguration( 
            @Value("${spring.config.location}") final String path, 
            @Value("${spring.properties.refreshDelay}") final long refreshDelay) throws Exception { 
        PropertiesConfiguration configuration = new PropertiesConfiguration(new File(path).getCanonicalPath()); 
        FileChangedReloadingStrategy fileChangedReloadingStrategy = new FileChangedReloadingStrategy(); 
        fileChangedReloadingStrategy.setRefreshDelay(refreshDelay); 
        configuration.setReloadingStrategy(fileChangedReloadingStrategy); 
        return configuration; 
} 

І сервіс, що перевірятиме стан по коду тогла:

@Slf4j 
@Service 
@RequiredArgsConstructor 
public class FeatureToggleServiceImpl implements FeatureToggleService { 
  
    private final Properties properties; 
  
    public boolean isToggleOn(String code) { 
        boolean isOn = Boolean.parseBoolean(properties.getProperty(code)); 
        log.info("Is toggle [{}] on - [{}].", code, isOn); 
        return isOn; 
    } 
} 

Вже на рівні контролера ми тільки передаємо значення в шаблон сторінки і на основі цього вирішуємо чи показувати рейтинг, чи ні:

@GetMapping("/products/{code}") 
public String getProduct(@PathVariable("code") final String code, final Model model) { 
        model.addAttribute("product", productRepository.findByCode(code)); 
        model.addAttribute("ratingEnabled", featureToggleService.isToggleOn(featureToggleProperties.getStarsRating())); 
        return "product"; 
} 
<div class="header"> 
    <h1 th:text="${product.name}"></h1> 
  
    <div th:if="${ratingEnabled}" class="rate" th:data-rate-value="${product.rating}"></div> 
    <h3 th:if="${ratingEnabled}" th:text="${product.rating}"></h3> 
    <p th:text="${product.description}"></p> 
</div> 

Коли буде потрібно включити тогл, ми тільки змінюємо значення проперті. Така імплементація є доволі простою, проте зберігання стану тоглів на рівні пропертей є незручним. Адже не завжди є прямий доступ до файлів на сервері і взагалі відслідковувати хто і коли змінив стан може бути не просто. Хоча для випадку ‘операційних’ тоглів, конфігураційних пропертей може бути більше ніж достатньо.

stars.rating.enabled=true 

Microservice (feature-toggle-service, products-rating)

Мікро-сервіс лежить десь посередині між конфігураційними пропертями і SaaS, і зазвичай для більшості проектів такого рішення буде достатньо. Для імплементації зробимо окремий сервіс, який дозволить додавати нові тогли, змінювати і дивитись на їх стан. Самі тогли зберігатимуться в NoSQL базі даних. Для початку робимо контролер з необхідними ендпоінтами:

@RestController 
@RequestMapping("/feature-toggle/v1") 
@RequiredArgsConstructor 
@CrossOrigin("*") 
public class FeatureToggleController { 
  
    private final FeatureToggleService featureToggleService; 
  
    @GetMapping 
    public List getToggles() { 
        return featureToggleService.getToggles(); 
    } 
  
    @GetMapping("/{code}") 
    public FeatureToggle getToggle(@PathVariable final String code) { 
        return featureToggleService.getToggle(code); 
    } 
  
    @PutMapping("/{code}") 
    public FeatureToggle updateToggle(@PathVariable final String code, @RequestBody final FeatureToggle toggle) { 
        toggle.setCode(code); 
        return featureToggleService.updateToggle(toggle); 
    } 
  
    @PostMapping("/{code}") 
    public void createToggle(@PathVariable final String code, @RequestBody final FeatureToggle toggle) { 
        toggle.setCode(code); 
        featureToggleService.createToggle(toggle); 
    } 
  
    @DeleteMapping("/{code}") 
    public void deleteToggle(@PathVariable final String code) { 
        featureToggleService.deleteToggle(FeatureToggle.builder().code(code).build()); 
    } 
} 

І сервіс, що взаємодіятиме з базою даних:

@Component 
@RequiredArgsConstructor 
public class FeatureToggleServiceImpl implements FeatureToggleService { 
  
    private final FeatureToggleRepository repository; 
  
    @Override 
    public List getToggles() { 
        final List result = new ArrayList<>(); 
        repository.findAll().forEach(result::add); 
        return result; 
    } 
  
    @Override 
    public FeatureToggle getToggle(final String code) { 
        return repository.findByCode(code); 
    } 
  
    @Override 
    public FeatureToggle updateToggle(final FeatureToggle toggle) { 
        final FeatureToggle byCode = repository.findByCode(toggle.getCode()); 
        byCode.setEnabled(toggle.isEnabled()); 
        return repository.save(byCode); 
    } 
  
    @Override 
    public void createToggle(final FeatureToggle toggle) { 
        repository.save(toggle); 
    } 
  
    @Override 
    public void deleteToggle(final FeatureToggle toggle) { 
        repository.delete(toggle); 
    } 
} 

Тогли зберігатиміться в DynamoDB у такому вигляді:

{ 
  "enabled": 1, 
  "code": "stars.rating" 
} 

Щоб отримати стан тогла, викликаємо ‘/feature-toggle/v1/stars.rating’ ендпоінт: Для фронтенд часитини з сторінкою продукту і рейтином використаємо Angular, де буде компонент, що взаємодіятиме з сервісом, який в свою чергу діставатиме стан тогла. Шаблон сторінки з рейтингом, компонент та сервіс:

<div class="header"> 
  <h1>Product A</h1> 
  <div class="rate" data-rate-value="4.7" [hidden]="!ratingEnabled"></div> 
  <h3 *ngIf="ratingEnabled">4.7</h3> 
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse in tristique felis, sed tempus ipsum.</p> 
</div> 
@Component({ 
  selector: 'app-root', 
  templateUrl: './app.component.html', 
  styleUrls: ['./app.component.css'], 
}) 
export class AppComponent implements OnInit { 
  title = 'products-rating'; 
  toggleName = 'stars.rating'; 
  ratingEnabled = false; 
  
  constructor(private http: HttpClient, @Inject('toggleService') private toggleService: ToggleService) { 
  } 
  
  ngOnInit(): void { 
    this.toggleService.isFeatureEnabled(this.toggleName).subscribe(enabled => { 
      this.ratingEnabled = enabled; 
    }); 
  } 
} 
class Toggle { 
  code: string; 
  enabled: boolean; 
} 
  
@Injectable({ 
  providedIn: 'root' 
}) 
export class RestToggleService implements ToggleService { 
  toggleName = 'stars.rating'; 
  
  constructor(private http: HttpClient) { 
  } 
  
  isFeatureEnabled(code: string): Observable { 
    const toggle = this.http.get('http://localhost:8081/feature-toggle/v1/' + this.toggleName); 
    return toggle.pipe(map((tg: Toggle) => tg.enabled)); 
  } 
} 

Як результат, маємо таку ж сторінку з рейтингом продукту, проте вона не залежить від бекенду, і в разі проблем з тогл сервісом, сама сторінка буде в робочому стані. Ну і з переваг — це буде можливість деплоїв і будь-яких змін в коді оновлювати інтерфейс за необхідності.

SAAS (Optimizely)

І про SAAS. На ринку рішень досить багато. З найвідоміших я б виділив Optimizely, Split, Taplytics. Всі ці рішення володіють обширним функціоналом як з точки зору фіча тоглів, так і А/Б тестування і аналітики. Для розробки є sdk як і для java, javascript, .Net, Python, Ruby. З цікавого: taplytics більш зосереджений на а/б тесвання і аналітку плюс має підтримку пуш нотифікацій і дозволяє контролювати запук фіч в конкретний час. Решта сервісів має все таке саме: А/Б тестування і тд. Далі детальніше покажу приклад використання Optimizely, додавши новий сервіс та конфігурацію в існуючу Angular аплікацію. Проте, для початку необхідно зробити тогл з відповідним ключем адмін панелі Optimizely: За замовчуванням, Optimizely робить зразу дві версії тоглу, одну для розробки та іншу для продакшину. Далі необхідно скопіювати sdk ключ для подальшої конфігурації. Після того, робимо нову реалізацію сервісу, де робимо інстанс Optimizely (передавши ключ до skd). Однією з важливих деталей є необхідність перед діставанням стану тогла, викликати метод ‘onReady’, який сигналізує про те, що конфігурація Optimizely готова, в іншому випадку стан тогла не можливо буде дістати.

@Injectable({ 
  providedIn: 'root' 
}) 
export class OptimizelyToggleService implements ToggleService { 
  private testUser = 'user'; 
  private optimizelyClientInstance = optimizelySDK.createInstance({ 
    sdkKey: '<skd_key>' 
  }); 
  
  isFeatureEnabled(code: string): Observable { 
    return new Observable(ob => { 
      this.optimizelyClientInstance.onReady().then(() => { 
        const enabled = this.optimizelyClientInstance.isFeatureEnabled('stars_rating', this.testUser); 
        console.log('Is enabled:' + enabled); 
        ob.next(enabled); 
      }); 
    }); 
  } 
} 

Також окрім тогла, в метод ‘isFeatureEnable’ ще передається ‘userId’. На основі значення цього, атрибути Optimizely визначає, який стан тогла повертати. Це корисно у випадку, коли до тогла додано А/Б експеримент. В моєму випадку 50% користувачів отримуватимуть фічу включеною, решта 50% виключеною. Якщо в ‘user’ передавати одне і те ж значення, то і стан тоглу для такого користувача буде не змінним. Як бачимо, використання SaaS дозволяє максимально спростити систему, але в свою чергу і надає такій системі значно більше можливостей. Також варто додати, що якщо потрібно буде написати певну бізнес логіку на бекенді, що залежатиме від того ж тогла, нам достатньо буде зінтегруватись з SDK, і зробити невеликий сервіс що діставатиме цей тогл. Для Java це виглядатиме так:

@Profile("optimizely") 
@Bean 
public Optimizely optimizely() { 
        final String sdkKey = environment.getProperty("optimizely.sdk.key"); 
        final HttpProjectConfigManager configManager = HttpProjectConfigManager.builder() 
                .withSdkKey(sdkKey) 
                .withPollingInterval(7L, TimeUnit.SECONDS) 
                .withBlockingTimeout(7L, TimeUnit.SECONDS) 
                .build(); 
        return OptimizelyFactory.newDefaultInstance(configManager); 
} 
feature.toggle.stars.rating=stars_rating 
optimizely.sdk.key=<skd_key>
@Profile("optimizely") 
@Service 
public class OptimizelyFeatureToggleServiceImpl implements FeatureToggleService { 
  
    private static final Logger LOGGER = LoggerFactory.getLogger(OptimizelyFeatureToggleServiceImpl.class); 
  
    @Autowired 
    private Optimizely optimizely; 
  
    public boolean isToggleOn(final String code) { 
        final String user = UUID.randomUUID().toString(); 
        final Boolean featureEnabled = optimizely.isFeatureEnabled(code, user); 
        LOGGER.info("Is toggle [{}] on - [{}].", code, featureEnabled); 
        return featureEnabled; 
    } 

Підсумок

Фіча тогли є назвичайно потужним підходом як і в розробці, так і за її межами, незважаючи на свою простоту. Для розробників тогли дозволяють бути ще більш гнучкішими під час розробки та знизити ймовірність поламати систему складним функціоналом. В свою чергу для бізнесу, тогли є хорошим механізмом для реалізації будь-яких ідей та гіпотез в різноманітних маркетингових цілях. Проте слід також врахувати те, що не завжди використання тоглів може бути доцільним. Окрім цього для великих систем кількість тоглів може стрімко збільшуватись до сотень, що значно ускладнює тестування таких систем та керування тими тоглами. З іншої сторони, з використанням SAAS, деякі подібні проблеми можуть бути вирішені. І як показує практика, якщо на проекті стає очевидною необхідність використання тоглів, і їхня кількість буде збільшуватись (до десятків, а то й сотень), краще відразу задуматись про SaaS, щоб не витрачати зайві ресурси на написання кастомного рішення, яке з часом все одно буде замінено.

👍НравитсяПонравилось8
В избранноеВ избранном6
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Спасибо за статью, буду рад ответам на несколько практических вопросов.
1.Например, нужно сделать большую фичу в течении 2х месяцев (4 спринта, 4 релиза). Фича дробится на задачи (кусочки). В течении каждого из 4х релизов выводится по 2-3 задачи с выключенной функциональностью. Затем последний релиз включает все тоглы и вся фича работает целиком. Правильно я понял суть Release toggles? Что делать с тоглами добавленными в 1,2,3 релизе, когда фича полностью выведена на ПРОД в 4м релизе? Оставлять их в коде? Или одно из изменений в 4м релизе — почистить тоглы?
2.И еще, на сколько практикуется поведение одной мини-фичи ставить в завимость от состояния тогла другой? Например, если функция получения баланса активна, то показать кнопку Обновить баланс.

Дякую за статтю!
Ми на проекті юзаємо Launch Darkly — launchdarkly.com
Поки задоволені, все працює без проблем навіть при великій комбінації фіч і юзерів, в яких вони включені/виключені.

Стаття чудова.

В одній компанії, де працювали друзі, також був Feature toggling, але коли ці фічі починають накладатись то це перетворюється в болюче та складнозрозуміле дерево if-ів.

Друга ситуація коли бізнес починає переключати фічі, а потім питає за просідання метрик тому треба зберігати хто та коли переключив.

Щодо зберігання історії змін тоглів, я б назвав це необхідністю, щоб забезпечити себе від ’безвідповідального’ переключання тоглів, оскільки це може привести до проблем з поведінкою системи, і за це хтось мав би відповідати.

Чому ваша компанія мовчить про Дія Сіті у відгуках? (риторичне питання)

Потому что это не его компания, а компания, на которой он гребёт и возможно отгребает. Не будет же он за пана владельца ответ нести. Некоторый вот в ДТЭК архитекты (не бейте за троллинг), и ничё — живут

Подписаться на комментарии