[] Retry и Circuit Breaker в Kubernetes с помощью Istio и Spring Boot (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Каждому service mesh-фреймворку абсолютно необходимо уметь обрабатывать сбои в межсервисном взаимодействии. К ним также относятся таймауты и HTTP-коды ошибок. Я покажу, как с помощью Istio настроить механизмы retries (повторных попыток) и circuit breaker (автоматического выключения). Мы проанализируем взаимодействие между двумя простыми Spring Boot-сервисами, развёрнутыми в Kubernetes. Но вместо основ рассмотрим более сложные вопросы.
Для демонстрации использования Istio и Spring Boot я создал GitHub-репозиторий с двумя сервисами: callme-service и caller-service.
Архитектура
Архитектура системы очень похожа на ту, что рассматривалась в моей предыдущей статье "Service mesh on Kubernetes with Istio and Spring Boot", но с некоторыми отличиями. Мы добавляем ошибку или задержку не с помощью Istio-компонентов, а прямо в исходном коде сервиса. Почему? Так мы сможем обрабатывать правила для callme-service напрямую, а не на клиенте. Также мы запустим два пода callme-service v2, чтобы проверить, как circuit breaker работает с несколькими подами того же Deployment.
Вот как выглядит архитектура:
Spring Boot-сервисы
Начнём с реализации сервисов. callme-service предоставляет два эндпоинта, возвращающие информацию о версии и ID инстанса. Вызов GET /ping-with-random-error выдаёт ошибку HTTP 504 в ответ на примерно половину запросов. А GET /ping-with-random-delay отвечает со случайной задержкой в диапазоне 0...3 с. Так реализован @RestController на стороне callme-service:
@RestController
@RequestMapping("/callme")
public class CallmeController {
private static final Logger LOGGER = LoggerFactory.getLogger(CallmeController.class);
private static final String INSTANCE_ID = UUID.randomUUID().toString();
private Random random = new Random();
@Autowired
BuildProperties buildProperties;
@Value("${VERSION}")
private String version;
@GetMapping("/ping-with-random-error")
public ResponseEntity<String> pingWithRandomError() {
int r = random.nextInt(100);
if (r % 2 == 0) {
LOGGER.info("Ping with random error: name={}, version={}, random={}, httpCode={}",
buildProperties.getName(), version, r, HttpStatus.GATEWAY_TIMEOUT);
return new ResponseEntity<>("Surprise " + INSTANCE_ID + " " + version, HttpStatus.GATEWAY_TIMEOUT);
} else {
LOGGER.info("Ping with random error: name={}, version={}, random={}, httpCode={}",
buildProperties.getName(), version, r, HttpStatus.OK);
return new ResponseEntity<>("I'm callme-service" + INSTANCE_ID + " " + version, HttpStatus.OK);
}
}
@GetMapping("/ping-with-random-delay")
public String pingWithRandomDelay() throws InterruptedException {
int r = new Random().nextInt(3000);
LOGGER.info("Ping with random delay: name={}, version={}, delay={}", buildProperties.getName(), version, r);
Thread.sleep(r);
return "I'm callme-service " + version;
}
}
Сервис caller-service тоже предоставляет два эндпоинта GET. С помощью RestTemplate он вызывает соответствующий GET callme-service. Сервис также возвращает версию caller-service, у него только один Deployment, он помечен как version=v1.
@RestController
@RequestMapping("/caller")
public class CallerController {
private static final Logger LOGGER = LoggerFactory.getLogger(CallerController.class);
@Autowired
BuildProperties buildProperties;
@Autowired
RestTemplate restTemplate;
@Value("${VERSION}")
private String version;
@GetMapping("/ping-with-random-error")
public ResponseEntity<String> pingWithRandomError() {
LOGGER.info("Ping with random error: name={}, version={}", buildProperties.getName(), version);
ResponseEntity<String> responseEntity =
restTemplate.getForEntity("http://callme-service:8080/callme/ping-with-random-error", String.class);
LOGGER.info("Calling: responseCode={}, response={}", responseEntity.getStatusCode(), responseEntity.getBody());
return new ResponseEntity<>("I'm caller-service " + version + ". Calling... " + responseEntity.getBody(), responseEntity.getStatusCode());
}
@GetMapping("/ping-with-random-delay")
public String pingWithRandomDelay() {
LOGGER.info("Ping with random delay: name={}, version={}", buildProperties.getName(), version);
String response = restTemplate.getForObject("http://callme-service:8080/callme/ping-with-random-delay", String.class);
LOGGER.info("Calling: response={}", response);
return "I'm caller-service " + version + ". Calling... " + response;
}
}
Обработка повторных попыток (retries) в Istio
Определение объекта DestinationRule в Istio такое же, как в моей предыдущей статье. Создано два подмножества для подов, помеченных как version=v1 и version=v2. Retries и timeouts можно настроить в VirtualService. Мы можем задать количество повторных попыток и условия их выполнения (списком enum-строк). В коде ниже также задаётся таймаут 3 с. для всего запроса. Обе эти настройки доступны внутри объекта HTTPRoute. Заодно нам нужно задать длительность таймаута на одну попытку, я задал 1 с. Как это работает на практике? Рассмотрим простой пример:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: callme-service-destination
spec:
host: callme-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: callme-service-route
spec:
hosts:
- callme-service
http:
- route:
- destination:
host: callme-service
subset: v2
weight: 80
- destination:
host: callme-service
subset: v1
weight: 20
retries:
attempts: 3
perTryTimeout: 1s
retryOn: 5xx
timeout: 3s
Перед развёртыванием сервисов нужно поднять уровень логирования. Мы легко можем включить логи обращений в Istio. Тогда Envoy-прокси будут выводить логи для всех входящих запросов и исходящих ответов. Анализ этих записей будет особенно полезен для определения повторных попыток.
$ istioctl manifest apply --set profile=default --set meshConfig.accessLogFile="/dev/stdout"
Давайте выполним тестовый запрос GET /caller/ping-with-random-delay. Он обратится к отвечающему со случайной задержкой GET /callme/ping-with-random-delay сервиса callme-service. Вот запрос и ответ на него:
Вроде бы, всё понятно. Но давайте посмотрим, что происходит под капотом. Я выделил последовательность повторных попыток. Как видите, Istio сделал две попытки, потому что два вызова обрабатывались дольше одной секунды, заданной в perTryTimeout. Два первых вызова завершились по таймауту из-за Istio, что видно в логе обращений. Третья попытка оказалась успешной, потому что обрабатывалась примерно 400 мс.
Повторы из-за таймаута — не единственная функция этого механизма в Istio. Мы можем задавать их при любых кодах 5хх и 4хх. Использовать VirtualService для тестирования одних лишь кодов ошибок гораздо проще, ведь нам не нужно конфигурировать таймауты.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: callme-service-route
spec:
hosts:
- callme-service
http:
- route:
- destination:
host: callme-service
subset: v2
weight: 80
- destination:
host: callme-service
subset: v1
weight: 20
retries:
attempts: 3
retryOn: gateway-error,connect-failure,refused-stream
Вызовем GET /caller/ping-with-random-error, который обратится к GET /callme/ping-with-random-error сервиса callme-service. Она возвращает HTTP 504 в ответ примерно на половину входящих запросов. Вот запрос и успешный ответ с кодом 200 OK.
А вот лог, который показывает, что происходит на стороне callme-service. Было две повторные попытки, потому что на первые два вызова мы получили код ошибки.
Автоматическое выключение (circuit breaker) в Istio
Автоматическое выключение настраивается в объекте DestinationRule. Для этого воспользуемся TrafficPolicy. Не будем задавать retries из предыдущего примера, так что потребуется удалить их из определения VirtualService. Нужно также отключить все настройки повторов в connectionPool внутри TrafficPolicy. А теперь самое важное. Для настройки circuit breaker в Istio мы воспользуемся объектом OutlierDetection. Механизм автоматического выключение реализован на основе последовательных ошибок, возвращаемых конечным сервисом. Количество ошибок можно задать с помощью свойства consecutive5xxErrors или consecutiveGatewayErrors. Они отличаются лишь тем, что могут обрабатывать разные наборы ошибок. consecutiveGatewayErrors обрабатывает только 502, 503 и 504, а consecutive5xxErrors применяется для всех 5хх кодов. Ниже в конфигурации callme-service-destination я задал consecutive5xxErrors значение 3. Это означает, что после трёх ошибок подряд под сервиса на одну минуту убирается из балансировки нагрузки (baseEjectionTime=1m). Поскольку у нас запущено два пода callme-service версии v2, нам также нужно переопределить на 100% заданное для maxEjectionPercent значение по умолчанию, которое равно 10%: это максимальная доля хостов в пуле балансировки нагрузки, которые могут быть исключены.
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: callme-service-destination
spec:
host: callme-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
trafficPolicy:
connectionPool:
http:
http1MaxPendingRequests: 1
maxRequestsPerConnection: 1
maxRetries: 0
outlierDetection:
consecutive5xxErrors: 3
interval: 30s
baseEjectionTime: 1m
maxEjectionPercent: 100
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: callme-service-route
spec:
hosts:
- callme-service
http:
- route:
- destination:
host: callme-service
subset: v2
weight: 80
- destination:
host: callme-service
subset: v1
weight: 20
Оба сервиса быстрее всего можно развернуть с помощью Jib и Skaffold. Сначала идём в директорию callme-service и исполняем команду skaffold dev с опциональным параметром --port-forward.
$ cd callme-service
$ skaffold dev --port-forward
Затем то же самое делаем для caller-service.
$ cd caller-service
$ skaffold dev --port-forward
Прежде чем отправлять тестовые запросы, давайте запустим второй под callme-service версии v2, поскольку Deployment присваивает параметру replicas значение 1. Для этого выполним команду:
$ kubectl scale --replicas=2 deployment/callme-service-v2
Проверим статус деплоймента в Kubernetes. Три деплоймента, две запущенные поды callme-service-v2.
Теперь можно тестировать. Вызовем GET /caller/ping-with-random-error сервиса caller-service, который обращается к эндпоинту GET /callme/ping-with-random-error сервиса callme-service. Напомню, что она возвращает ошибку HTTP 504 в ответ на половину запросов. Я уже настроил для callme-service перенаправление на порт 8080, так что команда вызова сервиса выглядит так:
curl http://localhost:8080/caller/ping-with-random-error
Проанализируем ответ. Я выделил ответы с ошибкой от пода callme-service версии v2 и ID 98c068bb-8d02-4d2a-9999-23951bbed6ad. После трёх ответов с ошибкой подряд от этого пода он немедленно был убран из пула балансировки нагрузки, и в результате все последующие запросы стали отправляться на второй под callme-service v2 с ID 00653617-58e1-4d59-9e36-3f98f9d403b8. Конечно, есть ещё один под callme-service v1, на который идёт 20% всех запросов от caller-service.
Посмотрим, что произойдёт, если единственный под callme-service v1 возвратит три ошибки подряд. Я выделил такие ответы на скриншоте. Поскольку под единственный, перенаправлять входящий трафик больше некуда. Поэтому Istio возвращает HTTP 503 на следующий запрос к callme-service v1. Тот же ответ повторяется в течение следующей минуты, потому что circuit ещё открыт.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Piotr Mińkowski
===========Похожие новости:
- [DevOps, Kubernetes, Серверное администрирование, Системное администрирование] 11 инструментов, делающих Kubernetes лучше (перевод)
- [DevOps, Git, IT-инфраструктура, Open source] Вышел релиз GitLab 13.4 с хранилищем HashiCorp для переменных CI и Kubernetes Agent
- [Kubernetes, Системное администрирование] 14 октября стартует продвинутый курс по Kubernetes: последний в этом году
- [Open source] Проект Kyma: как разрабатывать приложения для SAP с использованием технологии Kubernetes
- [Node.JS, Разработка веб-сайтов] Мониторинг Node.js-приложения
- [Kubernetes, Системное администрирование] Мёртвые оркестраторы оказывается не такие уж мёртвые
- [Разработка веб-сайтов, Клиентская оптимизация, Серверная оптимизация, Браузеры] А ваш CDN умеет так?
- [Java] Ведение журнала в Spring Boot (перевод)
- [DevOps, Kubernetes, Серверное администрирование, Системное администрирование] АйТиБорода: Контейнеризация понятным языком. Интервью с System Engineers из Southbridge
- [Java] Удаленная отладка Spring Boot приложений (IntelliJ + Eclipse) (перевод)
Теги для поиска: #_istio, #_retry, #_circuit_breaker, #_kubernetes, #_spring_boot, #_blog_kompanii_domklik (
Блог компании ДомКлик
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:38
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Каждому service mesh-фреймворку абсолютно необходимо уметь обрабатывать сбои в межсервисном взаимодействии. К ним также относятся таймауты и HTTP-коды ошибок. Я покажу, как с помощью Istio настроить механизмы retries (повторных попыток) и circuit breaker (автоматического выключения). Мы проанализируем взаимодействие между двумя простыми Spring Boot-сервисами, развёрнутыми в Kubernetes. Но вместо основ рассмотрим более сложные вопросы. Для демонстрации использования Istio и Spring Boot я создал GitHub-репозиторий с двумя сервисами: callme-service и caller-service. Архитектура Архитектура системы очень похожа на ту, что рассматривалась в моей предыдущей статье "Service mesh on Kubernetes with Istio and Spring Boot", но с некоторыми отличиями. Мы добавляем ошибку или задержку не с помощью Istio-компонентов, а прямо в исходном коде сервиса. Почему? Так мы сможем обрабатывать правила для callme-service напрямую, а не на клиенте. Также мы запустим два пода callme-service v2, чтобы проверить, как circuit breaker работает с несколькими подами того же Deployment. Вот как выглядит архитектура: Spring Boot-сервисы Начнём с реализации сервисов. callme-service предоставляет два эндпоинта, возвращающие информацию о версии и ID инстанса. Вызов GET /ping-with-random-error выдаёт ошибку HTTP 504 в ответ на примерно половину запросов. А GET /ping-with-random-delay отвечает со случайной задержкой в диапазоне 0...3 с. Так реализован @RestController на стороне callme-service: @RestController
@RequestMapping("/callme") public class CallmeController { private static final Logger LOGGER = LoggerFactory.getLogger(CallmeController.class); private static final String INSTANCE_ID = UUID.randomUUID().toString(); private Random random = new Random(); @Autowired BuildProperties buildProperties; @Value("${VERSION}") private String version; @GetMapping("/ping-with-random-error") public ResponseEntity<String> pingWithRandomError() { int r = random.nextInt(100); if (r % 2 == 0) { LOGGER.info("Ping with random error: name={}, version={}, random={}, httpCode={}", buildProperties.getName(), version, r, HttpStatus.GATEWAY_TIMEOUT); return new ResponseEntity<>("Surprise " + INSTANCE_ID + " " + version, HttpStatus.GATEWAY_TIMEOUT); } else { LOGGER.info("Ping with random error: name={}, version={}, random={}, httpCode={}", buildProperties.getName(), version, r, HttpStatus.OK); return new ResponseEntity<>("I'm callme-service" + INSTANCE_ID + " " + version, HttpStatus.OK); } } @GetMapping("/ping-with-random-delay") public String pingWithRandomDelay() throws InterruptedException { int r = new Random().nextInt(3000); LOGGER.info("Ping with random delay: name={}, version={}, delay={}", buildProperties.getName(), version, r); Thread.sleep(r); return "I'm callme-service " + version; } } Сервис caller-service тоже предоставляет два эндпоинта GET. С помощью RestTemplate он вызывает соответствующий GET callme-service. Сервис также возвращает версию caller-service, у него только один Deployment, он помечен как version=v1. @RestController
@RequestMapping("/caller") public class CallerController { private static final Logger LOGGER = LoggerFactory.getLogger(CallerController.class); @Autowired BuildProperties buildProperties; @Autowired RestTemplate restTemplate; @Value("${VERSION}") private String version; @GetMapping("/ping-with-random-error") public ResponseEntity<String> pingWithRandomError() { LOGGER.info("Ping with random error: name={}, version={}", buildProperties.getName(), version); ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://callme-service:8080/callme/ping-with-random-error", String.class); LOGGER.info("Calling: responseCode={}, response={}", responseEntity.getStatusCode(), responseEntity.getBody()); return new ResponseEntity<>("I'm caller-service " + version + ". Calling... " + responseEntity.getBody(), responseEntity.getStatusCode()); } @GetMapping("/ping-with-random-delay") public String pingWithRandomDelay() { LOGGER.info("Ping with random delay: name={}, version={}", buildProperties.getName(), version); String response = restTemplate.getForObject("http://callme-service:8080/callme/ping-with-random-delay", String.class); LOGGER.info("Calling: response={}", response); return "I'm caller-service " + version + ". Calling... " + response; } } Обработка повторных попыток (retries) в Istio Определение объекта DestinationRule в Istio такое же, как в моей предыдущей статье. Создано два подмножества для подов, помеченных как version=v1 и version=v2. Retries и timeouts можно настроить в VirtualService. Мы можем задать количество повторных попыток и условия их выполнения (списком enum-строк). В коде ниже также задаётся таймаут 3 с. для всего запроса. Обе эти настройки доступны внутри объекта HTTPRoute. Заодно нам нужно задать длительность таймаута на одну попытку, я задал 1 с. Как это работает на практике? Рассмотрим простой пример: apiVersion: networking.istio.io/v1beta1
kind: DestinationRule metadata: name: callme-service-destination spec: host: callme-service subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 --- apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: callme-service-route spec: hosts: - callme-service http: - route: - destination: host: callme-service subset: v2 weight: 80 - destination: host: callme-service subset: v1 weight: 20 retries: attempts: 3 perTryTimeout: 1s retryOn: 5xx timeout: 3s Перед развёртыванием сервисов нужно поднять уровень логирования. Мы легко можем включить логи обращений в Istio. Тогда Envoy-прокси будут выводить логи для всех входящих запросов и исходящих ответов. Анализ этих записей будет особенно полезен для определения повторных попыток. $ istioctl manifest apply --set profile=default --set meshConfig.accessLogFile="/dev/stdout"
Давайте выполним тестовый запрос GET /caller/ping-with-random-delay. Он обратится к отвечающему со случайной задержкой GET /callme/ping-with-random-delay сервиса callme-service. Вот запрос и ответ на него: Вроде бы, всё понятно. Но давайте посмотрим, что происходит под капотом. Я выделил последовательность повторных попыток. Как видите, Istio сделал две попытки, потому что два вызова обрабатывались дольше одной секунды, заданной в perTryTimeout. Два первых вызова завершились по таймауту из-за Istio, что видно в логе обращений. Третья попытка оказалась успешной, потому что обрабатывалась примерно 400 мс. Повторы из-за таймаута — не единственная функция этого механизма в Istio. Мы можем задавать их при любых кодах 5хх и 4хх. Использовать VirtualService для тестирования одних лишь кодов ошибок гораздо проще, ведь нам не нужно конфигурировать таймауты. apiVersion: networking.istio.io/v1beta1
kind: VirtualService metadata: name: callme-service-route spec: hosts: - callme-service http: - route: - destination: host: callme-service subset: v2 weight: 80 - destination: host: callme-service subset: v1 weight: 20 retries: attempts: 3 retryOn: gateway-error,connect-failure,refused-stream Вызовем GET /caller/ping-with-random-error, который обратится к GET /callme/ping-with-random-error сервиса callme-service. Она возвращает HTTP 504 в ответ примерно на половину входящих запросов. Вот запрос и успешный ответ с кодом 200 OK. А вот лог, который показывает, что происходит на стороне callme-service. Было две повторные попытки, потому что на первые два вызова мы получили код ошибки. Автоматическое выключение (circuit breaker) в Istio Автоматическое выключение настраивается в объекте DestinationRule. Для этого воспользуемся TrafficPolicy. Не будем задавать retries из предыдущего примера, так что потребуется удалить их из определения VirtualService. Нужно также отключить все настройки повторов в connectionPool внутри TrafficPolicy. А теперь самое важное. Для настройки circuit breaker в Istio мы воспользуемся объектом OutlierDetection. Механизм автоматического выключение реализован на основе последовательных ошибок, возвращаемых конечным сервисом. Количество ошибок можно задать с помощью свойства consecutive5xxErrors или consecutiveGatewayErrors. Они отличаются лишь тем, что могут обрабатывать разные наборы ошибок. consecutiveGatewayErrors обрабатывает только 502, 503 и 504, а consecutive5xxErrors применяется для всех 5хх кодов. Ниже в конфигурации callme-service-destination я задал consecutive5xxErrors значение 3. Это означает, что после трёх ошибок подряд под сервиса на одну минуту убирается из балансировки нагрузки (baseEjectionTime=1m). Поскольку у нас запущено два пода callme-service версии v2, нам также нужно переопределить на 100% заданное для maxEjectionPercent значение по умолчанию, которое равно 10%: это максимальная доля хостов в пуле балансировки нагрузки, которые могут быть исключены. apiVersion: networking.istio.io/v1beta1
kind: DestinationRule metadata: name: callme-service-destination spec: host: callme-service subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 trafficPolicy: connectionPool: http: http1MaxPendingRequests: 1 maxRequestsPerConnection: 1 maxRetries: 0 outlierDetection: consecutive5xxErrors: 3 interval: 30s baseEjectionTime: 1m maxEjectionPercent: 100 --- apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: callme-service-route spec: hosts: - callme-service http: - route: - destination: host: callme-service subset: v2 weight: 80 - destination: host: callme-service subset: v1 weight: 20 Оба сервиса быстрее всего можно развернуть с помощью Jib и Skaffold. Сначала идём в директорию callme-service и исполняем команду skaffold dev с опциональным параметром --port-forward. $ cd callme-service
$ skaffold dev --port-forward Затем то же самое делаем для caller-service. $ cd caller-service
$ skaffold dev --port-forward Прежде чем отправлять тестовые запросы, давайте запустим второй под callme-service версии v2, поскольку Deployment присваивает параметру replicas значение 1. Для этого выполним команду: $ kubectl scale --replicas=2 deployment/callme-service-v2
Проверим статус деплоймента в Kubernetes. Три деплоймента, две запущенные поды callme-service-v2. Теперь можно тестировать. Вызовем GET /caller/ping-with-random-error сервиса caller-service, который обращается к эндпоинту GET /callme/ping-with-random-error сервиса callme-service. Напомню, что она возвращает ошибку HTTP 504 в ответ на половину запросов. Я уже настроил для callme-service перенаправление на порт 8080, так что команда вызова сервиса выглядит так: curl http://localhost:8080/caller/ping-with-random-error
Проанализируем ответ. Я выделил ответы с ошибкой от пода callme-service версии v2 и ID 98c068bb-8d02-4d2a-9999-23951bbed6ad. После трёх ответов с ошибкой подряд от этого пода он немедленно был убран из пула балансировки нагрузки, и в результате все последующие запросы стали отправляться на второй под callme-service v2 с ID 00653617-58e1-4d59-9e36-3f98f9d403b8. Конечно, есть ещё один под callme-service v1, на который идёт 20% всех запросов от caller-service. Посмотрим, что произойдёт, если единственный под callme-service v1 возвратит три ошибки подряд. Я выделил такие ответы на скриншоте. Поскольку под единственный, перенаправлять входящий трафик больше некуда. Поэтому Istio возвращает HTTP 503 на следующий запрос к callme-service v1. Тот же ответ повторяется в течение следующей минуты, потому что circuit ещё открыт. =========== Источник: habr.com =========== =========== Автор оригинала: Piotr Mińkowski ===========Похожие новости:
Блог компании ДомКлик ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:38
Часовой пояс: UTC + 5