Креирање додатног кубе-планера са прилагођеним скупом правила планирања

Креирање додатног кубе-планера са прилагођеним скупом правила планирања

Кубе-сцхедулер је интегрална компонента Кубернетес-а, која је одговорна за распоређивање подова између чворова у складу са одређеним смерницама. Често, током рада Кубернетес кластера, не морамо да размишљамо о томе које смернице се користе за планирање подова, пошто је скуп смерница подразумеваног кубе-планера погодан за већину свакодневних задатака. Међутим, постоје ситуације када нам је важно да фино подесимо процес доделе подова, а постоје два начина да се овај задатак оствари:

  1. Направите кубе-планер са прилагођеним скупом правила
  2. Напишите сопствени планер и научите га да ради са захтевима АПИ сервера

У овом чланку ћу описати имплементацију прве тачке за решавање проблема неуједначеног распореда огњишта на једном од наших пројеката.

Кратак увод у то како ради кубе-сцхедулер

Вреди посебно напоменути чињеницу да кубе-сцхедулер није одговоран за директно заказивање подова - он је одговоран само за одређивање чвора на који ће се поставити под. Другим речима, резултат рада кубе-сцхедулер-а је име чвора, које он враћа АПИ серверу за захтев за заказивање, и ту се његов рад завршава.

Прво, кубе-сцхедулер саставља листу чворова на којима се под може распоредити у складу са смерницама предиката. Затим, сваки чвор са ове листе добија одређени број поена у складу са политикама приоритета. Као резултат, изабран је чвор са максималним бројем поена. Ако постоје чворови који имају исти максимални резултат, бира се случајни. Листа и опис политика предиката (филтрирање) и приоритета (бодинг) се могу наћи у документација.

Опис тела проблема

Упркос великом броју различитих Кубернетес кластера који се одржавају у Никис-у, први пут смо се сусрели са проблемом заказивања подова тек недавно, када је један од наших пројеката требао да покрене велики број периодичних задатака (~100 ЦронЈоб ентитета). Да бисмо што више поједноставили опис проблема, узећемо као пример један микросервис, у оквиру којег се црон задатак покреће једном у минуту, стварајући одређено оптерећење на ЦПУ-у. За покретање црон задатка додељена су три чвора са апсолутно идентичним карактеристикама (24 вЦПУ-а на сваком).

Истовремено, немогуће је тачно рећи колико ће времена ЦронЈоб-у бити потребно да се изврши, пошто се обим улазних података стално мења. У просеку, током нормалног рада кубе-сцхедулер-а, сваки чвор покреће 3-4 инстанце посла, што ствара ~20-30% оптерећења на ЦПУ-у сваког чвора:

Креирање додатног кубе-планера са прилагођеним скупом правила планирања

Сам проблем је у томе што су понекад црон задаци престали да буду заказани на једном од три чвора. То јест, у неком тренутку није планиран ни један под за један од чворова, док је на друга два чвора радило 6-8 копија задатка, стварајући ~40-60% оптерећења ЦПУ-а:

Креирање додатног кубе-планера са прилагођеним скупом правила планирања

Проблем се понављао са апсолутно насумичном фреквенцијом и повремено је био у корелацији са тренутком увођења нове верзије кода.

Повећањем нивоа евидентирања кубе-планера на ниво 10 (-в=10), почели смо да бележимо колико је поена сваки чвор добио током процеса евалуације. Током нормалног планирања, следеће информације се могу видети у евиденцији:

resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node03: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1387 millicores 4161694720 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node02: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1347 millicores 4444810240 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node03: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1387 millicores 4161694720 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node01: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1687 millicores 4790840320 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node02: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1347 millicores 4444810240 memory bytes, score 9
resource_allocation.go:78] cronjob-1574828880-mn7m4 -> Node01: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1687 millicores 4790840320 memory bytes, score 9
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node01: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node02: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node03: NodeAffinityPriority, Score: (0)                                                                                       
interpod_affinity.go:237] cronjob-1574828880-mn7m4 -> Node01: InterPodAffinityPriority, Score: (0)                                                                                                        
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node01: TaintTolerationPriority, Score: (10)                                                                                   
interpod_affinity.go:237] cronjob-1574828880-mn7m4 -> Node02: InterPodAffinityPriority, Score: (0)                                                                                                        
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node02: TaintTolerationPriority, Score: (10)                                                                                   
selector_spreading.go:146] cronjob-1574828880-mn7m4 -> Node01: SelectorSpreadPriority, Score: (10)                                                                                                        
interpod_affinity.go:237] cronjob-1574828880-mn7m4 -> Node03: InterPodAffinityPriority, Score: (0)                                                                                                        
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node03: TaintTolerationPriority, Score: (10)                                                                                   
selector_spreading.go:146] cronjob-1574828880-mn7m4 -> Node02: SelectorSpreadPriority, Score: (10)                                                                                                        
selector_spreading.go:146] cronjob-1574828880-mn7m4 -> Node03: SelectorSpreadPriority, Score: (10)                                                                                                        
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node01: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node02: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:726] cronjob-1574828880-mn7m4_project-stage -> Node03: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:781] Host Node01 => Score 100043                                                                                                                                                                        
generic_scheduler.go:781] Host Node02 => Score 100043                                                                                                                                                                        
generic_scheduler.go:781] Host Node03 => Score 100043

Оне. судећи по информацијама добијеним из дневника, сваки од чворова је постигао једнак број коначних поена и за планирање је изабран случајни. У време проблематичног планирања, дневници су изгледали овако:

resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node02: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1587 millicores 4581125120 memory bytes, score 9
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node03: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1087 millicores 3532549120 memory bytes, score 9
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node02: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1587 millicores 4581125120 memory bytes, score 9
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node01: BalancedResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 987 millicores 3322833920 memory bytes, score 9
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node01: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 987 millicores 3322833920 memory bytes, score 9 
resource_allocation.go:78] cronjob-1574211360-bzfkr -> Node03: LeastResourceAllocation, capacity 23900 millicores 67167186944 memory bytes, total request 1087 millicores 3532549120 memory bytes, score 9
interpod_affinity.go:237] cronjob-1574211360-bzfkr -> Node03: InterPodAffinityPriority, Score: (0)                                                                                                        
interpod_affinity.go:237] cronjob-1574211360-bzfkr -> Node02: InterPodAffinityPriority, Score: (0)                                                                                                        
interpod_affinity.go:237] cronjob-1574211360-bzfkr -> Node01: InterPodAffinityPriority, Score: (0)                                                                                                        
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node03: TaintTolerationPriority, Score: (10)                                                                                   
selector_spreading.go:146] cronjob-1574211360-bzfkr -> Node03: SelectorSpreadPriority, Score: (10)                                                                                                        
selector_spreading.go:146] cronjob-1574211360-bzfkr -> Node02: SelectorSpreadPriority, Score: (10)                                                                                                        
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node02: TaintTolerationPriority, Score: (10)                                                                                   
selector_spreading.go:146] cronjob-1574211360-bzfkr -> Node01: SelectorSpreadPriority, Score: (10)                                                                                                        
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node03: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node03: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node02: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node01: TaintTolerationPriority, Score: (10)                                                                                   
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node02: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node01: NodeAffinityPriority, Score: (0)                                                                                       
generic_scheduler.go:726] cronjob-1574211360-bzfkr_project-stage -> Node01: SelectorSpreadPriority, Score: (10)                                                                                    
generic_scheduler.go:781] Host Node03 => Score 100041                                                                                                                                                                        
generic_scheduler.go:781] Host Node02 => Score 100041                                                                                                                                                                        
generic_scheduler.go:781] Host Node01 => Score 100038

Из чега се види да је један од чворова постигао мање коначних поена од осталих, те је стога планирање извршено само за два чвора која су постигла максималан резултат. Тако смо се дефинитивно уверили да је проблем управо у распореду махуна.

Даљи алгоритам за решавање проблема био нам је очигледан - анализирајте евиденције, разумејте по ком приоритету чвор није освојио бодове и, ако је потребно, прилагодите политике подразумеваног кубе-планера. Међутим, овде се суочавамо са две значајне потешкоће:

  1. На максималном нивоу евидентирања (10) рефлектују се бодови добијени само за неке приоритете. У горњем изводу дневника, можете видети да за све приоритете приказане у евиденцији, чворови добијају исти број поена у нормалном и проблемском распореду, али је коначни резултат у случају планирања проблема другачији. Дакле, можемо закључити да се за неке приоритете бодовање дешава „иза кулиса“, а ми немамо начина да разумемо за који приоритет чвор није добио бодове. Овај проблем смо детаљно описали у емисија Кубернетес спремиште на Гитхуб-у. У време писања овог текста, од програмера је примљен одговор да ће подршка за евидентирање бити додата у ажурирањима Кубернетес в1.15,1.16, 1.17 и XNUMX.
  2. Не постоји једноставан начин да се разуме са којим специфичним скупом смерница кубе-сцхедулер тренутно ради. Да, у документација ова листа је наведена, али не садржи информације о томе које специфичне тежине су додељене свакој од приоритетних политика. Можете видети тежине или уредити смернице подразумеваног кубе-планера само у изворни кодови.

Вреди напоменути да смо једном успели да забележимо да чвор није добио бодове у складу са политиком ИмагеЛоцалитиПриорити, која додељује бодове чвору ако већ има слику неопходну за покретање апликације. Односно, у време када је нова верзија апликације представљена, црон задатак је успео да се покрене на два чвора, преузимајући им нову слику из доцкер регистра, и тако су два чвора добила већи коначни резултат у односу на трећи .

Као што сам горе написао, у евиденцијама не видимо информације о процени политике ИмагеЛоцалитиПриорити, тако да смо, да бисмо проверили нашу претпоставку, избацили слику са новом верзијом апликације на трећи чвор, након чега је заказивање функционисало исправно . Управо због политике ИмагеЛоцалитиПриорити проблем заказивања је примећен прилично ретко, чешће је био повезан са нечим другим. Због чињенице да нисмо могли у потпуности да отклонимо грешке у свакој од смерница на листи приоритета подразумеваног кубе-планера, имали смо потребу за флексибилним управљањем политикама планирања под.

Проблем статемент

Желели смо да решење проблема буде што конкретније, односно да главни ентитети Кубернетеса (овде мислимо на подразумевани кубе-планер) треба да остану непромењени. Нисмо желели да решимо проблем на једном месту, а да га створимо на другом. Тако смо дошли до две опције за решавање проблема, које су најављене у уводу чланка - креирање додатног планера или писање сопственог. Главни захтев за планирање црон задатака је равномерна расподела оптерећења на три чвора. Овај захтев се може задовољити постојећим политикама кубе-планера, тако да за решавање нашег проблема нема смисла писати сопствени планер.

Упутства за креирање и примену додатног кубе-планера су описана у документација. Међутим, чинило нам се да ентитет Деплоимент није довољан да обезбеди толеранцију грешака у раду тако критичне услуге као што је кубе-сцхедулер, па смо одлучили да применимо нови кубе-сцхедулер као статички под, који би се директно надгледао би Кубелет. Дакле, имамо следеће захтеве за нови кубе-планер:

  1. Услуга мора бити распоређена као Статиц Под на свим мастерима кластера
  2. Толеранција грешака мора бити обезбеђена у случају да активни под са кубе-планером није доступан
  3. Главни приоритет при планирању треба да буде број доступних ресурса на чвору (ЛеастРекуестедПриорити)

Имплементација решења

Одмах је вредно напоменути да ћемо све радове обављати у Кубернетес в1.14.7, јер Ово је верзија која је коришћена у пројекту. Почнимо са писањем манифеста за наш нови кубе-планер. Узмимо подразумевани манифест (/етц/кубернетес/манифестс/кубе-сцхедулер.иамл) као основу и доведемо га у следећи облик:

kind: Pod
metadata:
  labels:
    component: scheduler
    tier: control-plane
  name: kube-scheduler-cron
  namespace: kube-system
spec:
      containers:
      - command:
        - /usr/local/bin/kube-scheduler
        - --address=0.0.0.0
        - --port=10151
        - --secure-port=10159
        - --config=/etc/kubernetes/scheduler-custom.conf
        - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
        - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
        - --v=2
        image: gcr.io/google-containers/kube-scheduler:v1.14.7
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 8
          httpGet:
            host: 127.0.0.1
            path: /healthz
            port: 10151
            scheme: HTTP
          initialDelaySeconds: 15
          timeoutSeconds: 15
        name: kube-scheduler-cron-container
        resources:
          requests:
            cpu: '0.1'
        volumeMounts:
        - mountPath: /etc/kubernetes/scheduler.conf
          name: kube-config
          readOnly: true
        - mountPath: /etc/localtime
          name: localtime
          readOnly: true
        - mountPath: /etc/kubernetes/scheduler-custom.conf
          name: scheduler-config
          readOnly: true
        - mountPath: /etc/kubernetes/scheduler-custom-policy-config.json
          name: policy-config
          readOnly: true
      hostNetwork: true
      priorityClassName: system-cluster-critical
      volumes:
      - hostPath:
          path: /etc/kubernetes/scheduler.conf
          type: FileOrCreate
        name: kube-config
      - hostPath:
          path: /etc/localtime
        name: localtime
      - hostPath:
          path: /etc/kubernetes/scheduler-custom.conf
          type: FileOrCreate
        name: scheduler-config
      - hostPath:
          path: /etc/kubernetes/scheduler-custom-policy-config.json
          type: FileOrCreate
        name: policy-config

Укратко о главним променама:

  1. Промењено име под и контејнера у кубе-сцхедулер-црон
  2. Наведено коришћење портова 10151 и 10159 као опција дефинисана hostNetwork: true и не можемо да користимо исте портове као подразумевани кубе-планер (10251 и 10259)
  3. Користећи параметар --цонфиг, одредили смо конфигурациону датотеку са којом сервис треба да се покрене
  4. Конфигурисано монтирање конфигурационе датотеке (сцхедулер-цустом.цонф) и датотеке смерница за заказивање (сцхедулер-цустом-полици-цонфиг.јсон) са хоста

Не заборавите да ће нашем кубе-планеру бити потребна права слична подразумеваном. Уредите његову улогу кластера:

kubectl edit clusterrole system:kube-scheduler

...
   resourceNames:
    - kube-scheduler
    - kube-scheduler-cron
...

Хајде сада да разговарамо о томе шта би требало да буде садржано у конфигурационој датотеци и датотеци политике планирања:

  • Конфигурациони фајл (сцхедулер-цустом.цонф)
    Да бисте добили подразумевану конфигурацију кубе-планера, морате користити параметар --write-config-to од документација. Добијену конфигурацију поставићемо у датотеку /етц/кубернетес/сцхедулер-цустом.цонф и свести је на следећи облик:

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
schedulerName: kube-scheduler-cron
bindTimeoutSeconds: 600
clientConnection:
  acceptContentTypes: ""
  burst: 100
  contentType: application/vnd.kubernetes.protobuf
  kubeconfig: /etc/kubernetes/scheduler.conf
  qps: 50
disablePreemption: false
enableContentionProfiling: false
enableProfiling: false
failureDomains: kubernetes.io/hostname,failure-domain.beta.kubernetes.io/zone,failure-domain.beta.kubernetes.io/region
hardPodAffinitySymmetricWeight: 1
healthzBindAddress: 0.0.0.0:10151
leaderElection:
  leaderElect: true
  leaseDuration: 15s
  lockObjectName: kube-scheduler-cron
  lockObjectNamespace: kube-system
  renewDeadline: 10s
  resourceLock: endpoints
  retryPeriod: 2s
metricsBindAddress: 0.0.0.0:10151
percentageOfNodesToScore: 0
algorithmSource:
   policy:
     file:
       path: "/etc/kubernetes/scheduler-custom-policy-config.json"

Укратко о главним променама:

  1. Поставили смо сцхедулерНаме на име нашег кубе-сцхедулер-црон сервиса.
  2. У параметру lockObjectName такође треба да подесите назив наше услуге и уверите се да је параметар leaderElect поставите на тачно (ако имате један главни чвор, можете га поставити на нетачно).
  3. Наведена је путања до датотеке са описом смерница планирања у параметру algorithmSource.

Вреди пажљивије погледати другу тачку, где уређујемо параметре за кључ leaderElection. Да бисмо осигурали толеранцију грешака, омогућили смо (leaderElect) процес избора лидера (мајстора) између подова нашег кубе-планера користећи једну крајњу тачку за њих (resourceLock) под називом кубе-сцхедулер-црон (lockObjectName) у простору имена кубе-система (lockObjectNamespace). Како Кубернетес обезбеђује високу доступност главних компоненти (укључујући кубе-сцхедулер) можете пронаћи у Чланак.

  • Датотека смерница за планирање (сцхедулер-цустом-полици-цонфиг.јсон)
    Као што сам раније написао, можемо сазнати са којим специфичним смерницама подразумевани кубе-планер ради само анализом његовог кода. То јест, не можемо да добијемо датотеку са смерницама планирања за подразумевани кубе-сцхедулер на исти начин као конфигурациону датотеку. Хајде да опишемо смернице планирања које нас занимају у датотеци /етц/кубернетес/сцхедулер-цустом-полици-цонфиг.јсон на следећи начин:

{
  "kind": "Policy",
  "apiVersion": "v1",
  "predicates": [
    {
      "name": "GeneralPredicates"
    }
  ],
  "priorities": [
    {
      "name": "ServiceSpreadingPriority",
      "weight": 1
    },
    {
      "name": "EqualPriority",
      "weight": 1
    },
    {
      "name": "LeastRequestedPriority",
      "weight": 1
    },
    {
      "name": "NodePreferAvoidPodsPriority",
      "weight": 10000
    },
    {
      "name": "NodeAffinityPriority",
      "weight": 1
    }
  ],
  "hardPodAffinitySymmetricWeight" : 10,
  "alwaysCheckAllPredicates" : false
}

Према томе, кубе-сцхедулер прво компајлира листу чворова на које под може бити заказан у складу са политиком ГенералПредицатес (која укључује скуп политика ПодФитсРесоурцес, ПодФитсХостПортс, ХостНаме и МатцхНодеСелецтор). Затим се сваки чвор процењује у складу са скупом политика у низу приоритета. Да бисмо испунили услове нашег задатка, сматрали смо да би такав скуп политика био оптимално решење. Да вас подсетим да је скуп смерница са њиховим детаљним описима доступан у документација. Да бисте извршили свој задатак, можете једноставно променити скуп коришћених политика и доделити им одговарајуће тежине.

Хајде да назовемо манифест новог кубе-сцхедулер-а, који смо креирали на почетку поглавља, кубе-сцхедулер-цустом.иамл и поставимо га на следећу путању /етц/кубернетес/манифестс на три главна чвора. Ако је све урађено исправно, Кубелет ће покренути под на сваком чвору, а у евиденцији нашег новог кубе-планера видећемо информацију да је наша датотека смерница успешно примењена:

Creating scheduler from configuration: {{ } [{GeneralPredicates <nil>}] [{ServiceSpreadingPriority 1 <nil>} {EqualPriority 1 <nil>} {LeastRequestedPriority 1 <nil>} {NodePreferAvoidPodsPriority 10000 <nil>} {NodeAffinityPriority 1 <nil>}] [] 10 false}
Registering predicate: GeneralPredicates
Predicate type GeneralPredicates already registered, reusing.
Registering priority: ServiceSpreadingPriority
Priority type ServiceSpreadingPriority already registered, reusing.
Registering priority: EqualPriority
Priority type EqualPriority already registered, reusing.
Registering priority: LeastRequestedPriority
Priority type LeastRequestedPriority already registered, reusing.
Registering priority: NodePreferAvoidPodsPriority
Priority type NodePreferAvoidPodsPriority already registered, reusing.
Registering priority: NodeAffinityPriority
Priority type NodeAffinityPriority already registered, reusing.
Creating scheduler with fit predicates 'map[GeneralPredicates:{}]' and priority functions 'map[EqualPriority:{} LeastRequestedPriority:{} NodeAffinityPriority:{} NodePreferAvoidPodsPriority:{} ServiceSpreadingPriority:{}]'

Сада остаје само да назначимо у спецификацији нашег ЦронЈоб-а да све захтеве за заказивање његових подова треба да обрађује наш нови кубе-планер:

...
 jobTemplate:
    spec:
      template:
        spec:
          schedulerName: kube-scheduler-cron
...

Закључак

На крају, добили смо додатни кубе-планер са јединственим скупом политика планирања, чији рад директно надгледа кубелет. Поред тога, поставили смо избор новог лидера између подова нашег кубе-планера у случају да стари лидер из неког разлога постане недоступан.

Редовне апликације и услуге настављају да се заказују преко подразумеваног кубе-планера, а сви црон задаци су у потпуности пребачени на нови. Оптерећење које стварају црон задаци сада је равномерно распоређено на све чворове. С обзиром да се већина црон задатака извршава на истим чворовима као и главне апликације пројекта, ово је значајно смањило ризик од померања подова због недостатка ресурса. Након увођења додатног кубе-планера, проблеми са неуједначеним распоредом црон задатака више се нису јављали.

Прочитајте и друге чланке на нашем блогу:

Извор: ввв.хабр.цом

Додај коментар