Метод дроселювання викликає M запитів за N секунд


137

Мені потрібен компонент / клас, який перешкоджає виконанню якогось методу, щоб максимум M дзвінків за N секунд (або ms чи nanos, не має значення)

Іншими словами, я повинен переконатися, що мій метод виконується не більше M разів у розсувному вікні N секунд.

Якщо ви не знаєте існуючого класу, сміливо публікуйте свої рішення / ідеї, як би ви це реалізували.



3
Є деякі великі відповіді на цю проблему в stackoverflow.com/questions/667508 / ...
skaffman

> Мені потрібно переконатися, що мій метод> виконується не більше M разів у> розсувному вікні N секунд. Нещодавно я написав пост в блозі про те, як це зробити в .NET. Можливо, ви зможете створити щось подібне на Java. Краще обмеження ставок у .NET
Джек Лейч

Оригінальне запитання дуже схоже на проблему, вирішену в цьому дописі в блозі: [Багатоканальний асинхронний дросель Java] ( cordinc.com/blog/2010/04/java-multichannel-asynchronous.html ). Для швидкості M дзвінків за N секунд, дросель, який обговорюється в цьому блозі, гарантує, що будь-який інтервал довжиною N на часовій шкалі не буде містити більше M дзвінків.
Hbf

Відповіді:


81

Я б використовував буфер дзвінка часових позначок із фіксованим розміром M. Кожен раз, коли метод викликається, ви перевіряєте найстаріший запис, і якщо минуло менше N секунд, ви виконуєте та додаєте інший запис, інакше ви спите на різницю в часі.


4
Прекрасна. Тільки те, що мені потрібно. Швидкі спроби показують ~ 10 рядків для реалізації цього та мінімального сліду пам’яті. Просто потрібно подумати про безпеку потоку та чергування вхідних запитів.
втрубников

5
Ось чому ви використовуєте DelayQueue від java.util.concurrent. Це запобігає проблемі декількох потоків, що діють на один запис.
erickson

5
Для випадку з декількома нитками, я вважаю, що підхід до відра токена може бути кращим вибором.
Майкл Боргвардт

1
Чи знаєте ви, як називається цей алгоритм, якщо він взагалі має якусь назву?
Владо Панджич

80

Що для мене вийшло, було Google Guava RateLimiter .

// Allow one request per second
private RateLimiter throttle = RateLimiter.create(1.0);

private void someMethod() {
    throttle.acquire();
    // Do something
}

19
Я б не рекомендував це рішення, оскільки Guava RateLimiter перекриє нитку, і це легко вичерпає пул потоків.
kaviddiss

18
@kaviddiss, якщо ви не хочете блокувати, тоді використовуйтеtryAquire()
slf

7
Проблема з поточною реалізацією RateLimiter (принаймні, для мене) полягає в тому, що він не передбачає періодів часу, більших за 1 секунду, і, отже, швидкості, наприклад, 1 в хвилину.
Джон Б

4
@John B Наскільки я розумію, ви можете домогтися 1 запиту в хвилину за допомогою RateLimiter, використовуючи RateLimiter.create (60.0) + rateLimiter.acquire (60)
divideByZero

2
@radiantRazor Ratelimiter.create (1.0 / 60) і придбання () досягає 1 дзвінка в хвилину.
bizentass

30

Конкретно кажучи, ви повинні мати можливість реалізувати це за допомогою a DelayQueue. Ініціалізуйте чергу з M Delayedекземплярами з їх затримкою, спочатку встановленою на нуль. По мірі надходження запитів до методу takeз'являється маркер, який призводить до блокування методу, поки не буде виконано вимогу до дроселювання. Коли маркер зроблено, addновий маркер до черги із запізненням N.


1
Так, це зробить трюк. Але мені особливо не подобається DelayQueue, тому що він використовує (через PriortyQueue) збалансований бінарний хеш (що означає безліч порівнянь offerта можливий ріст масиву), і все це для мене важке. Я думаю, що для інших це може бути цілком нормально.
втрубников

5
Насправді в цій програмі, оскільки новий елемент, доданий до купи, майже завжди буде максимальним елементом у купі (тобто мати найдовшу затримку), як правило, потрібно одне порівняння на додавання. Також масив ніколи не зростатиме, якщо алгоритм реалізований правильно, оскільки один елемент додається лише після взяття одного елемента.
erickson

3
Я вважаю це корисним також у тих випадках, коли ви не хочете, щоб запити траплялися у великих сплесках, зберігаючи розмір M та затримуючи N порівняно невеликим, на кілька мільйонів. напр. M = 5, N = 20 мс забезпечить пропускну здатність в 250 / сек, а скорочувальний вибух стане розміром 5.
FUD,

Чи буде така шкала на мільйон об / хв і коли одночасні запити дозволені? Мені потрібно додати мільйон затриманих елементів. Також кутові випадки будуть великими щодо затримки - випадок, коли кілька потоків викликає опитування (), і він буде блокуватися кожен раз.
Aditya Joshee

@AdityaJoshee я не оцінював це, але якщо я отримаю деякий час, я спробую зрозуміти, що накладні витрати. Одне, що слід зазначити, це те, що вам не потрібно 1 мільйон жетонів, термін дії яких закінчується за 1 секунду. Ви можете мати 100 жетонів, термін дії яких закінчується за 10 мілісекунд, 10 жетонів, які закінчуються в мілісекундах і т.д. обмеження ставок 1 мільйон об / хв навряд чи звучить як дроселювання. Якщо ви можете пояснити свій випадок використання, я можу мати кращі ідеї.
Еріксон

21

Прочитайте на відро Token алгоритм . В основному у вас є відро з жетонами. Кожен раз, коли ви виконуєте метод, ви приймаєте маркер. Якщо жетонів більше немає, ви блокуєте, поки не отримаєте. Тим часом, існує якийсь зовнішній актор, який поповнює жетони через фіксований інтервал.

Мені невідома бібліотека для цього (або щось подібне). Ви можете записати цю логіку у свій код або використовувати AspectJ, щоб додати поведінку.


3
Дякую за пропозицію, цікаве альго. Але це не зовсім те, що мені потрібно. Наприклад, мені потрібно обмежити виконання 5 дзвінками в секунду. Якщо я використовую відро Token, і 10 запитів надходять одночасно, перші 5 дзвінків забирають усі наявні маркери та виконують миттєво, а решта 5 дзвінків виконуватимуться через фіксований інтервал 1/5 с. У такій ситуації мені потрібно виконати ще 5 дзвінків, які будуть виконані одним циклом лише після проходження 1 секунди.
втрубников

5
Що робити, якщо ви додаєте 5 жетонів у відро щосекунди (або 5 - (залишилось 5) замість 1 кожні 1/5 секунди?
Кевін

@Kevin ні, це все одно не дасть мені ефекту "розсувного вікна"
vtrubnikov

2
@valery так. ( Чи не забудьте обмежити маркери на M , хоча)
NOS

немає потреби в "зовнішньому акторі". Все можна зробити по одному потоку, якщо ви триматимете метадані приблизно про час запиту.
Марселл Уоллес

8

Якщо вам потрібен обмежувач швидкості розсувного вікна на основі Java, який буде працювати в розподіленій системі, ви можете ознайомитися з проектом https://github.com/mokies/ratelimitj .

Конфігурація з підтримкою Redis, щоб обмежити запити IP до 50 в хвилину, виглядатиме так:

import com.lambdaworks.redis.RedisClient;
import es.moki.ratelimitj.core.LimitRule;

RedisClient client = RedisClient.create("redis://localhost");
Set<LimitRule> rules = Collections.singleton(LimitRule.of(1, TimeUnit.MINUTES, 50)); // 50 request per minute, per key
RedisRateLimit requestRateLimiter = new RedisRateLimit(client, rules);

boolean overLimit = requestRateLimiter.overLimit("ip:127.0.0.2");

Див. Https://github.com/mokies/ratelimitj/tree/master/ratelimitj-redis для подальшої інформації про конфігурацію Redis.


5

Це залежить від програми.

Уявіть собі випадок, коли кілька потоків хочуть, щоб маркер здійснив глобальну дію з обмеженою швидкістю без дозволу (тобто ви хочете обмежити 10 дій на 10 секунд, але ви не хочете, щоб 10 дій відбувалися в першій секунді, а потім залишалися 9 секунд зупинився).

У DelayedQueue є недолік: порядок, в якому потоки запитують жетони, може бути не тим порядком, коли вони отримують свій запит. Якщо кілька потоків заблоковано в очікуванні маркера, незрозуміло, який з них буде приймати наступний доступний маркер. На мою точку зору, ви навіть можете чекати теми, які вічно чекають.

Одне рішення - мати мінімальний проміжок часу між двома послідовними діями та здійснювати дії в тому ж порядку, як вони просили.

Ось реалізація:

public class LeakyBucket {
    protected float maxRate;
    protected long minTime;
    //holds time of last action (past or future!)
    protected long lastSchedAction = System.currentTimeMillis();

    public LeakyBucket(float maxRate) throws Exception {
        if(maxRate <= 0.0f) {
            throw new Exception("Invalid rate");
        }
        this.maxRate = maxRate;
        this.minTime = (long)(1000.0f / maxRate);
    }

    public void consume() throws InterruptedException {
        long curTime = System.currentTimeMillis();
        long timeLeft;

        //calculate when can we do the action
        synchronized(this) {
            timeLeft = lastSchedAction + minTime - curTime;
            if(timeLeft > 0) {
                lastSchedAction += minTime;
            }
            else {
                lastSchedAction = curTime;
            }
        }

        //If needed, wait for our time
        if(timeLeft <= 0) {
            return;
        }
        else {
            Thread.sleep(timeLeft);
        }
    }
}

що minTimeтут означає? Що це робить? чи можете ви пояснити це?
спалах

minTime- мінімальна кількість часу, яке потрібно пройти після того, як маркер буде витрачений, перш ніж можна буде використовувати наступний маркер.
Дуарте

3

Хоча це не те, про що ви запитували, ThreadPoolExecutorякий призначений для обмеження одночасних M запитів замість M запитів за N секунд, також може бути корисним.


2

Я реалізував простий алгоритм дроселювання. Спробуйте це посилання, http://krishnaprasadas.blogspot.in/2012/05/throttling-algorithm.html

Короткий огляд про алгоритм,

Цей алгоритм використовує можливості Java Delayed Queue . Створіть об'єкт із затримкою із очікуваною затримкою (тут 1000 / М за мілісекунд TimeUnit ). Помістіть той самий об’єкт у чергу із затримкою, яка стажер надасть нам рухоме вікно. Потім перед кожним викликом методу прийміть об'єкт із черги, take - виклик блокування, який повернеться лише після вказаної затримки, а після виклику методу не забудьте поставити об'єкт у чергу з оновленим часом (тут поточні мілісекунди) .

Тут ми також можемо мати кілька об'єктів із затримкою з різною затримкою. Цей підхід також забезпечить високу пропускну здатність.


6
Ви повинні розмістити резюме свого алгоритму. Якщо ваше посилання відходить, то ваша відповідь стає марною.
jwr

Дякую, я додав коротке.
Кришас

1

Моя реалізація нижче може обробляти довільну точність часу запиту, вона має O (1) часову складність для кожного запиту, не потребує додаткового буфера, наприклад, O (1) складність простору, крім того, він не вимагає фонового потоку для звільнення маркера, замість цього жетони видаються відповідно до часу, минулого з моменту останнього запиту.

class RateLimiter {
    int limit;
    double available;
    long interval;

    long lastTimeStamp;

    RateLimiter(int limit, long interval) {
        this.limit = limit;
        this.interval = interval;

        available = 0;
        lastTimeStamp = System.currentTimeMillis();
    }

    synchronized boolean canAdd() {
        long now = System.currentTimeMillis();
        // more token are released since last request
        available += (now-lastTimeStamp)*1.0/interval*limit; 
        if (available>limit)
            available = limit;

        if (available<1)
            return false;
        else {
            available--;
            lastTimeStamp = now;
            return true;
        }
    }
}

0

Спробуйте використовувати цей простий підхід:

public class SimpleThrottler {

private static final int T = 1; // min
private static final int N = 345;

private Lock lock = new ReentrantLock();
private Condition newFrame = lock.newCondition();
private volatile boolean currentFrame = true;

public SimpleThrottler() {
    handleForGate();
}

/**
 * Payload
 */
private void job() {
    try {
        Thread.sleep(Math.abs(ThreadLocalRandom.current().nextLong(12, 98)));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.err.print(" J. ");
}

public void doJob() throws InterruptedException {
    lock.lock();
    try {

        while (true) {

            int count = 0;

            while (count < N && currentFrame) {
                job();
                count++;
            }

            newFrame.await();
            currentFrame = true;
        }

    } finally {
        lock.unlock();
    }
}

public void handleForGate() {
    Thread handler = new Thread(() -> {
        while (true) {
            try {
                Thread.sleep(1 * 900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                currentFrame = false;

                lock.lock();
                try {
                    newFrame.signal();
                } finally {
                    lock.unlock();
                }
            }
        }
    });
    handler.start();
}

}



0

Це оновлення коду LeakyBucket вище. Це працює на більш ніж 1000 запитів на секунду.

import lombok.SneakyThrows;
import java.util.concurrent.TimeUnit;

class LeakyBucket {
  private long minTimeNano; // sec / billion
  private long sched = System.nanoTime();

  /**
   * Create a rate limiter using the leakybucket alg.
   * @param perSec the number of requests per second
   */
  public LeakyBucket(double perSec) {
    if (perSec <= 0.0) {
      throw new RuntimeException("Invalid rate " + perSec);
    }
    this.minTimeNano = (long) (1_000_000_000.0 / perSec);
  }

  @SneakyThrows public void consume() {
    long curr = System.nanoTime();
    long timeLeft;

    synchronized (this) {
      timeLeft = sched - curr + minTimeNano;
      sched += minTimeNano;
    }
    if (timeLeft <= minTimeNano) {
      return;
    }
    TimeUnit.NANOSECONDS.sleep(timeLeft);
  }
}

і єдиний тест для вище:

import com.google.common.base.Stopwatch;
import org.junit.Ignore;
import org.junit.Test;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class LeakyBucketTest {
  @Test @Ignore public void t() {
    double numberPerSec = 10000;
    LeakyBucket b = new LeakyBucket(numberPerSec);
    Stopwatch w = Stopwatch.createStarted();
    IntStream.range(0, (int) (numberPerSec * 5)).parallel().forEach(
        x -> b.consume());
    System.out.printf("%,d ms%n", w.elapsed(TimeUnit.MILLISECONDS));
  }
}

що тут minTimeNanoозначає? ти можеш пояснити?
спалах

0

Ось трохи вдосконалена версія простого обмежувача швидкості

/**
 * Simple request limiter based on Thread.sleep method.
 * Create limiter instance via {@link #create(float)} and call {@link #consume()} before making any request.
 * If the limit is exceeded cosume method locks and waits for current call rate to fall down below the limit
 */
public class RequestRateLimiter {

    private long minTime;

    private long lastSchedAction;
    private double avgSpent = 0;

    ArrayList<RatePeriod> periods;


    @AllArgsConstructor
    public static class RatePeriod{

        @Getter
        private LocalTime start;

        @Getter
        private LocalTime end;

        @Getter
        private float maxRate;
    }


    /**
     * Create request limiter with maxRate - maximum number of requests per second
     * @param maxRate - maximum number of requests per second
     * @return
     */
    public static RequestRateLimiter create(float maxRate){
        return new RequestRateLimiter(Arrays.asList( new RatePeriod(LocalTime.of(0,0,0),
                LocalTime.of(23,59,59), maxRate)));
    }

    /**
     * Create request limiter with ratePeriods calendar - maximum number of requests per second in every period
     * @param ratePeriods - rate calendar
     * @return
     */
    public static RequestRateLimiter create(List<RatePeriod> ratePeriods){
        return new RequestRateLimiter(ratePeriods);
    }

    private void checkArgs(List<RatePeriod> ratePeriods){

        for (RatePeriod rp: ratePeriods ){
            if ( null == rp || rp.maxRate <= 0.0f || null == rp.start || null == rp.end )
                throw new IllegalArgumentException("list contains null or rate is less then zero or period is zero length");
        }
    }

    private float getCurrentRate(){

        LocalTime now = LocalTime.now();

        for (RatePeriod rp: periods){
            if ( now.isAfter( rp.start ) && now.isBefore( rp.end ) )
                return rp.maxRate;
        }

        return Float.MAX_VALUE;
    }



    private RequestRateLimiter(List<RatePeriod> ratePeriods){

        checkArgs(ratePeriods);
        periods = new ArrayList<>(ratePeriods.size());
        periods.addAll(ratePeriods);

        this.minTime = (long)(1000.0f / getCurrentRate());
        this.lastSchedAction = System.currentTimeMillis() - minTime;
    }

    /**
     * Call this method before making actual request.
     * Method call locks until current rate falls down below the limit
     * @throws InterruptedException
     */
    public void consume() throws InterruptedException {

        long timeLeft;

        synchronized(this) {
            long curTime = System.currentTimeMillis();

            minTime = (long)(1000.0f / getCurrentRate());
            timeLeft = lastSchedAction + minTime - curTime;

            long timeSpent = curTime - lastSchedAction + timeLeft;
            avgSpent = (avgSpent + timeSpent) / 2;

            if(timeLeft <= 0) {
                lastSchedAction = curTime;
                return;
            }

            lastSchedAction = curTime + timeLeft;
        }

        Thread.sleep(timeLeft);
    }

    public synchronized float getCuRate(){
        return (float) ( 1000d / avgSpent);
    }
}

І одиничні тести

import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class RequestRateLimiterTest {


    @Test(expected = IllegalArgumentException.class)
    public void checkSingleThreadZeroRate(){

        // Zero rate
        RequestRateLimiter limiter = RequestRateLimiter.create(0);
        try {
            limiter.consume();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void checkSingleThreadUnlimitedRate(){

        // Unlimited
        RequestRateLimiter limiter = RequestRateLimiter.create(Float.MAX_VALUE);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) < 1000));
    }

    @Test
    public void rcheckSingleThreadRate(){

        // 3 request per minute
        RequestRateLimiter limiter = RequestRateLimiter.create(3f/60f);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 3; i++ ){

            try {
                limiter.consume();
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) >= 60000 ) & ((ended - started) < 61000));
    }



    @Test
    public void checkSingleThreadRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ));
    }

    @Test
    public void checkMultiThreadedRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

        for ( int i = 0; i < 10; i++ ) {

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreaded32RateLimit(){

        // 0,2 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(0.2f);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(8);
        ExecutorService exec = Executors.newFixedThreadPool(8);

        for ( int i = 0; i < 8; i++ ) {

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 2; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreadedRateLimitDynamicRate(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

        for ( int i = 0; i < 10; i++ ) {

            tasks.add( exec.submit(() -> {

                Random r = new Random();
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                        Thread.sleep(r.nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

}

Код досить простий. Ви просто створюєте обмежувач з maxRate або з періодами та швидкістю. А потім просто зателефонуйте, споживайте кожен запит. Щоразу, коли ставка не буде перевищена, обмежувач повертається негайно або чекає деякого часу, перш ніж повернутися до нижчого поточного запиту. Він також має метод поточної швидкості, який повертає ковзаюче середнє значення поточної швидкості.
Леонід Астахов

0

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

public static Runnable throttle (Runnable realRunner, long delay) {
    Runnable throttleRunner = new Runnable() {
        // whether is waiting to run
        private boolean _isWaiting = false;
        // target time to run realRunner
        private long _timeToRun;
        // specified delay time to wait
        private long _delay = delay;
        // Runnable that has the real task to run
        private Runnable _realRunner = realRunner;
        @Override
        public void run() {
            // current time
            long now;
            synchronized (this) {
                // another thread is waiting, skip
                if (_isWaiting) return;
                now = System.currentTimeMillis();
                // update time to run
                // do not update it each time since
                // you do not want to postpone it unlimited
                _timeToRun = now+_delay;
                // set waiting status
                _isWaiting = true;
            }
            try {
                Thread.sleep(_timeToRun-now);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // clear waiting status before run
                _isWaiting = false;
                // do the real task
                _realRunner.run();
            }
        }};
    return throttleRunner;
}

Візьміть від дебютації і натягування ниток JAVA

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.