Doqquz Kubernetes Performans Məsləhətləri

Doqquz Kubernetes Performans Məsləhətləri

Hamıya salam! Mənim adım Oleq Sidorenkov, mən DomClick-də infrastruktur komandasının rəhbəri kimi işləyirəm. Biz üç ildən artıqdır ki, Cube-dan satış üçün istifadə edirik və bu müddət ərzində onunla çox müxtəlif maraqlı anlar yaşadıq. Bu gün sizə düzgün yanaşma ilə klasteriniz üçün vanil Kubernetesdən daha çox performansı necə sıxa biləcəyinizi söyləyəcəyəm. Davamlı getməyə hazır olun!

Siz hamınız yaxşı bilirsiniz ki, Kubernetes konteyner orkestrasiyası üçün genişləndirilə bilən açıq mənbə sistemidir; yaxşı, və ya bir server mühitində mikroservislərinizin həyat dövrünü idarə edərək sehr edən 5 binar. Bundan əlavə, bu, müxtəlif tapşırıqlar üçün maksimum fərdiləşdirmə üçün Lego konstruktoru kimi yığıla bilən kifayət qədər çevik bir vasitədir.

Və hər şey yaxşı görünür: odun qutusuna odun kimi klasterə atın və kədərlənməyin. Ancaq ətraf mühitin tərəfdarısınızsa, o zaman fikirləşəcəksiniz: “Mən necə sobada od saxlayım və meşəyə peşman ola bilərəm?”. Başqa sözlə, infrastrukturun yaxşılaşdırılması və xərclərin azaldılması yollarını necə tapmaq olar.

1. Komanda və proqram resurslarını izləyin

Doqquz Kubernetes Performans Məsləhətləri

Ən bayağı, lakin təsirli üsullardan biri tələblərin/limitlərin tətbiqidir. Tətbiqləri ad boşluqları və ad boşluqlarını inkişaf qrupları ilə ayırın. Prosessor vaxtı, yaddaş, efemer yaddaş istehlakı üçün dəyərlər yerləşdirməzdən əvvəl tətbiqi təyin edin.

resources:
   requests:
     memory: 2Gi
     cpu: 250m
   limits:
     memory: 4Gi
     cpu: 500m

Təcrübə ilə belə bir nəticəyə gəldik: məhdudiyyətlərdən gələn sorğuları iki dəfədən çox şişirtməyə dəyməz. Klaster ölçüsü sorğular əsasında hesablanır və tətbiqi resursların fərqinə, məsələn, 5-10 dəfə təyin etsəniz, o zaman qovşaqlarınızla doldurulduqda və birdən bir yük aldıqda onunla nə olacağını təsəvvür edin. Yaxşı heç nə yoxdur. Ən azı, tənzimləmə və maksimum olaraq, işçi ilə vidalaşın və podlar hərəkət etməyə başladıqdan sonra qalan qovşaqlarda tsiklik yük alın.

Bundan əlavə, köməyi ilə limitranges başlanğıcda konteyner üçün resurs dəyərlərini təyin edə bilərsiniz - minimum, maksimum və standart:

➜  ~ kubectl describe limitranges --namespace ops
Name:       limit-range
Namespace:  ops
Type        Resource           Min   Max   Default Request  Default Limit  Max Limit/Request Ratio
----        --------           ---   ---   ---------------  -------------  -----------------------
Container   cpu                50m   10    100m             100m           2
Container   ephemeral-storage  12Mi  8Gi   128Mi            4Gi            -
Container   memory             64Mi  40Gi  128Mi            128Mi          2

Bir komanda klasterin bütün resurslarını götürə bilməməsi üçün ad sahəsi resurslarını məhdudlaşdırmağı unutmayın:

➜  ~ kubectl describe resourcequotas --namespace ops
Name:                   resource-quota
Namespace:              ops
Resource                Used          Hard
--------                ----          ----
limits.cpu              77250m        80
limits.memory           124814367488  150Gi
pods                    31            45
requests.cpu            53850m        80
requests.memory         75613234944   150Gi
services                26            50
services.loadbalancers  0             0
services.nodeports      0             0

Təsvirdən göründüyü kimi resourcequotas, əgər ops əmri daha 10 cpu istehlak edəcək podları yerləşdirmək istəsə, planlaşdırıcı bunu etməyə icazə verməyəcək və xəta verəcək:

Error creating: pods "nginx-proxy-9967d8d78-nh4fs" is forbidden: exceeded quota: resource-quota, requested: limits.cpu=5,requests.cpu=5, used: limits.cpu=77250m,requests.cpu=53850m, limited: limits.cpu=10,requests.cpu=10

Bənzər bir problemi həll etmək üçün bir alət yaza bilərsiniz, məsələn, kimi bu, komanda resurslarının vəziyyətini saxlaya və qəbul edə bilər.

2. Ən yaxşı fayl saxlama yerini seçin

Doqquz Kubernetes Performans Məsləhətləri

Burada davamlı həcmlər və Kubernetes işçi qovşaqlarının disk alt sistemi mövzusuna toxunmaq istərdim. Ümid edirəm ki, istehsalda heç kim HDD-də "Cube" istifadə etmir, lakin bəzən hətta adi bir SSD artıq kifayət etmir. Belə bir problemlə qarşılaşdıq ki, loglar giriş/çıxış əməliyyatları ilə diski öldürürdü və burada həll yolları çox deyil:

  • Yüksək performanslı SSD-lərdən istifadə edin və ya NVMe-yə keçin (əgər siz öz aparatınızı idarə edirsinizsə).

  • Giriş səviyyəsini azaldın.

  • Diski zorlayan podların "ağıllı" balansını aparın (podAntiAffinity).

Yuxarıdakı ekran görüntüsü, access_logs girişi aktiv olduqda (~12k log/san) disklə nginx-ingress-controller altında baş verənləri göstərir. Belə bir vəziyyət, əlbəttə ki, bu qovşaqdakı bütün tətbiqlərin deqradasiyasına səbəb ola bilər.

PV-yə gəlincə, təəssüf ki, mən hər şeyi sınamamışam. növlər Davamlı həcmlər. Sizə uyğun olan ən yaxşı variantdan istifadə edin. Ölkəmizdə tarixən baş verib ki, xidmətlərin kiçik bir hissəsi RWX həcmlərinə ehtiyac duyur və çoxdan bu iş üçün NFS yaddaşından istifadə etməyə başladılar. Ucuz və ... kifayət qədər. Əlbəttə, biz onunla bok yedik - sağlam ol, amma onu necə kökləməyi öyrəndik və başı artıq ağrımır. Mümkünsə, S3 obyekt yaddaşına keçin.

3. Optimallaşdırılmış Şəkillər Yaradın

Doqquz Kubernetes Performans Məsləhətləri

Kubernetes onları daha sürətli əldə edə və daha səmərəli icra edə bilməsi üçün konteyner üçün optimallaşdırılmış şəkillərdən istifadə etmək yaxşıdır. 

Optimallaşdırma o deməkdir ki, şəkillər:

  • yalnız bir proqram ehtiva edir və ya yalnız bir funksiyanı yerinə yetirir;

  • kiçik ölçülü, çünki böyük şəkillər şəbəkə üzərindən daha pis ötürülür;

  • Kubernetesin dayanma vaxtı vəziyyətində tədbir görmək üçün istifadə edə biləcəyi sağlamlıq və hazırlıq son nöqtələrinə sahib olun;

  • konfiqurasiya xətalarına daha davamlı olan konteyner dostu əməliyyat sistemlərindən (məsələn, Alpine və ya CoreOS) istifadə edin;

  • çoxmərhələli quruluşlardan istifadə edin ki, siz müşayiət olunan mənbələri deyil, yalnız tərtib edilmiş proqramları yerləşdirə biləsiniz.

Şəkilləri tez bir zamanda yoxlamağa və optimallaşdırmağa imkan verən bir çox alət və xidmətlər var. Onları həmişə yeni və təhlükəsiz saxlamaq vacibdir. Nəticədə əldə edirsiniz:

  1. Bütün klasterdə şəbəkə yükünün azaldılması.

  2. Konteynerin işə salınma vaxtı azaldı.

  3. Bütün Docker reyestrinizin daha kiçik ölçüsü.

4. DNS keşindən istifadə edin

Doqquz Kubernetes Performans Məsləhətləri

Yüksək yüklərdən danışırıqsa, klasterin DNS sistemini tənzimləmədən həyat olduqca pisdir. Bir vaxtlar Kubernetes tərtibatçıları kube-dns həllini dəstəkləyirdilər. O, ölkəmizdə də tətbiq olundu, lakin bu proqram təminatı xüsusilə uyğunlaşmadı və tələb olunan performansı vermədi, baxmayaraq ki, vəzifə sadədir. Sonra keçdiyimiz və kədərini bilmədiyimiz korednlər meydana çıxdı, sonradan K8-lərdə standart DNS xidməti oldu. Bir anda DNS sisteminə 40 min rps-ə qədər böyüdük və bu həll də kifayət etmədi. Ancaq şanslı bir şansla Nodelocaldns çıxdı, aka node local cache, aka NodeLocal DNSCache.

Niyə istifadə edirik? Linux nüvəsində bir səhv var ki, UDP üzərindən conntrack NAT vasitəsilə çoxsaylı girişlər konntrack cədvəllərinə yazmaq üçün yarış vəziyyətinə gətirib çıxarır və NAT vasitəsilə trafikin bir hissəsi itirilir (Xidmət vasitəsilə hər səfər NAT-dır). Nodelocaldns bu problemi NAT-dan qurtularaq və yuxarı axın DNS-ə TCP bağlantısını təkmilləşdirməklə, həmçinin yuxarı axın DNS sorğularını yerli olaraq keşləməklə həll edir (qısa 5 saniyəlik mənfi keş də daxil olmaqla).

5. Podları üfüqi və şaquli olaraq avtomatik ölçün

Doqquz Kubernetes Performans Məsləhətləri

Əminliklə deyə bilərsiniz ki, bütün mikroservisləriniz yükün iki-üç dəfə artmasına hazırdır? Tətbiqlərinizə resursları necə düzgün bölüşdürmək olar? Bir neçə podun iş yükündən artıq işləməsini saxlamaq lazımsız ola bilər və onların arxa-arxaya saxlanması xidmətə gedən trafikin qəfil artması ilə əlaqədar iş vaxtı riskini artırır. Qızıl orta kimi xidmətlər vurma sehrinə nail olmağa kömək edir Horizontal Pod Autoscaler и Şaquli Pod Autoscaler.

VPA faktiki istifadəyə əsaslanaraq podda konteynerlərinizin tələblərini/limitlərini avtomatik olaraq artırmağa imkan verir. Necə faydalı ola bilər? Əgər nədənsə üfüqi olaraq genişləndirilə bilməyən Podlarınız varsa (bu, tamamilə etibarlı deyil), onda onun resurslarını dəyişdirmək üçün VPA-ya etibar etməyə cəhd edə bilərsiniz. Onun xüsusiyyəti metrik serverdən gələn tarixi və cari məlumatlara əsaslanan tövsiyə sistemidir, ona görə də sorğuları/məhdudiyyətləri avtomatik olaraq dəyişmək istəmirsinizsə, sadəcə konteynerləriniz üçün tövsiyə olunan resurslara nəzarət edə və CPU və yaddaşa qənaət etmək üçün parametrləri optimallaşdıra bilərsiniz. klasterdə.

Doqquz Kubernetes Performans MəsləhətləriŞəkil https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231 saytından götürülüb

Kubernetesdəki planlaşdırıcı həmişə sorğulara əsaslanır. Oraya hansı dəyəri qoysanız, planlaşdırıcı ona əsaslanaraq uyğun bir node axtaracaq. Limit dəyəri kublet tərəfindən podun nə vaxt boğulacağını və ya öldürüləcəyini bilmək üçün lazımdır. Yeganə vacib parametr sorğuların dəyəri olduğundan, VPA onunla işləyəcək. Tətbiqinizi şaquli olaraq miqyaslandırdığınız zaman, hansı sorğuların olması lazım olduğunu müəyyənləşdirirsiniz. Bəs o zaman limitlərlə nə baş verəcək? Bu parametr də mütənasib şəkildə ölçüləcək.

Məsələn, tipik pod parametrləri bunlardır:

resources:
   requests:
     memory: 250Mi
     cpu: 200m
   limits:
     memory: 500Mi
     cpu: 350m

Tövsiyə mühərriki proqramınızın düzgün işləməsi üçün 300 m CPU və 500 Mi tələb etdiyini müəyyən edir. Bu parametrləri əldə edəcəksiniz:

resources:
   requests:
     memory: 500Mi
     cpu: 300m
   limits:
     memory: 1000Mi
     cpu: 525m

Yuxarıda qeyd edildiyi kimi, bu, manifestdəki sorğular/limitlər nisbətinə əsaslanan mütənasib miqyasdır:

  • CPU: 200m → 300m: nisbət 1:1.75;

  • Yaddaş: 250Mi → 500Mi: 1:2 nisbəti.

Gəlincə HPA, onda iş mexanizmi daha şəffaf olur. Prosessor və yaddaş kimi ölçülər üçün həddlər təyin edilir və bütün replikaların orta qiyməti həddi aşırsa, dəyər həddən aşağı düşənə qədər və ya replikaların maksimum sayına çatana qədər proqram +1 pod ilə miqyaslanır.

Doqquz Kubernetes Performans MəsləhətləriŞəkil https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231 saytından götürülüb

CPU və Yaddaş kimi adi ölçülərə əlavə olaraq, siz xüsusi Prometheus metriklərinizdə hədlər təyin edə və onlarla işləyə bilərsiniz, əgər bunun tətbiqinizi nə vaxt miqyaslandıracağını müəyyənləşdirməyin ən doğru yolu olduğunu düşünürsünüzsə. Tətbiq müəyyən edilmiş metrik hədddən aşağı sabitləşdikdən sonra HPA, Podları minimum replika sayına qədər və ya yük həddi qarşılayana qədər kiçildməyə başlayacaq.

6. Node Affinity və Pod Affinity Haqqında Unutmayın

Doqquz Kubernetes Performans Məsləhətləri

Bütün qovşaqlar eyni aparatda işləmir və bütün podların hesablama tələb edən proqramları işə salması lazım deyil. Kubernetes istifadə edərək qovşaqların və podların ixtisasını təyin etməyə imkan verir Düyün yaxınlığı и Pod Yaxınlığı.

Hesablama intensivliyi olan əməliyyatlar üçün uyğun qovşaqlarınız varsa, maksimum səmərəlilik üçün tətbiqləri müvafiq qovşaqlara bağlamaq daha yaxşıdır. Bunu etmək üçün istifadə edin nodeSelector node etiketi ilə.

Tutaq ki, sizin iki qovşaqınız var: biri ilə CPUType=HIGHFREQ və çoxlu sayda sürətli nüvələr, digəri ilə MemoryType=HIGHMEMORY daha çox yaddaş və daha sürətli performans. Ən asan yol bir qovşaq yerləşdirməsini təyin etməkdir HIGHFREQbölməsinə əlavə etməklə spec belə bir seçici:

…
nodeSelector:
	CPUType: HIGHFREQ

Bunu etmək üçün daha bahalı və xüsusi bir yol istifadə etməkdir nodeAffinity sahədə affinity bölmə spec. İki seçim var:

  • requiredDuringSchedulingIgnoredDuringExecution: sərt parametr (planlayıcı yalnız xüsusi qovşaqlarda podları yerləşdirəcək (və başqa heç bir yerdə));

  • preferredDuringSchedulingIgnoredDuringExecution: yumşaq parametr (planlayıcı xüsusi qovşaqlara yerləşdirməyə çalışacaq və uğursuz olarsa, növbəti mövcud qovşaqda yerləşdirməyə çalışacaq).

Siz node etiketlərini idarə etmək üçün xüsusi sintaksisi təyin edə bilərsiniz, məsələn, In, NotIn, Exists, DoesNotExist, Gt və ya Lt. Bununla belə, yadda saxlayın ki, etiketlərin uzun siyahılarındakı mürəkkəb üsullar kritik vəziyyətlərdə qərar qəbul etməyi ləngidir. Başqa sözlə, həddindən artıq mürəkkəbləşdirməyin.

Yuxarıda qeyd edildiyi kimi, Kubernetes sizə cari podların bağlanmasını təyin etməyə imkan verir. Yəni, müəyyən podların eyni əlçatanlıq zonasında (buludlar üçün uyğun) və ya qovşaqlarda digər podlarla birlikdə işləməsini təmin edə bilərsiniz.

В podAffinity margins affinity bölmə spec vəziyyətində olduğu kimi eyni sahələr mövcuddur nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution и preferredDuringSchedulingIgnoredDuringExecution. Yeganə fərq ondadır matchExpressions podları artıq həmin etiketlə pod işlədən node ilə bağlayacaq.

Daha çox Kubernetes bir sahə təklif edir podAntiAffinity, bu, əksinə, bir podu xüsusi podlarla bir düyünlə bağlamır.

İfadələr haqqında nodeAffinity eyni məsləhət verilə bilər: qaydaları sadə və məntiqli saxlamağa çalışın, pod spesifikasiyasını mürəkkəb qaydalar dəsti ilə həddən artıq yükləməyə çalışmayın. Klasterin şərtlərinə uyğun gəlməyən, planlaşdırıcıya əlavə yük verən və ümumi performansı aşağılayan bir qayda yaratmaq çox asandır.

7. Ləkələr və Tolerantlıqlar

Planlayıcını idarə etməyin başqa bir yolu var. Əgər yüzlərlə qovşaq və minlərlə mikroxidmətdən ibarət böyük klasteriniz varsa, müəyyən qovşaqların müəyyən qovşaqlar tərəfindən yerləşdirilməsinin qarşısını almaq çox çətindir.

Ləkə mexanizmi - qadağan qaydaları - buna kömək edir. Məsələn, müəyyən ssenarilərdə müəyyən qovşaqların podların işləməsinin qarşısını ala bilərsiniz. Müəyyən bir node üçün ləkə tətbiq etmək üçün seçimdən istifadə edin taint kubectl-də. Açar və dəyəri göstərin və sonra kimi ləkələyin NoSchedule və ya NoExecute:

$ kubectl taint nodes node10 node-role.kubernetes.io/ingress=true:NoSchedule

Ləkə mexanizminin üç əsas təsiri dəstəklədiyini də qeyd etmək lazımdır: NoSchedule, NoExecute и PreferNoSchedule.

  • NoSchedule pod spesifikasiyasında müvafiq giriş olana qədər o deməkdir ki tolerations, o, qovşaqda yerləşdirilə bilməz (bu nümunədə node10).

  • PreferNoSchedule - sadələşdirilmiş versiya NoSchedule. Bu halda, planlaşdırıcı uyğun girişi olmayan podları ayırmamağa çalışacaq. tolerations node başına, lakin bu çətin hədd deyil. Klasterdə heç bir resurs yoxdursa, podlar bu node üzərində yerləşdirilməyə başlayacaq.

  • NoExecute - bu təsir uyğun girişi olmayan podların dərhal boşaldılmasına səbəb olur tolerations.

Maraqlıdır ki, bu davranış dözümlülük mexanizmi ilə ləğv edilə bilər. Bu, "qadağan olunmuş" bir qovşaq olduqda rahatdır və ona yalnız infrastruktur xidmətləri yerləşdirmək lazımdır. Bunu necə etmək olar? Yalnız uyğun tolerantlıq olan podlara icazə verin.

Pod spesifikasiyasının necə görünəcəyi budur:

spec:
   tolerations:
     - key: "node-role.kubernetes.io/ingress"
        operator: "Equal"
        value: "true"
        effect: "NoSchedule"

Bu o demək deyil ki, növbəti təkrar yerləşdirmə zamanı pod məhz bu qovşağı vuracaq, bu Node Affinity mexanizmi deyil və nodeSelector. Ancaq bir neçə xüsusiyyəti birləşdirərək, çox çevik planlaşdırıcı quraşdırmasına nail ola bilərsiniz.

8. Pod Yerləşdirmə Prioritetini təyin edin

Yalnız pod-to-node bağlamalarını konfiqurasiya etdiyinizə görə, bütün podlar eyni prioritetlə işlənməlidir. Məsələn, bəzi Podları digərlərindən əvvəl yerləşdirmək istəyə bilərsiniz.

Kubernetes Pod Priority və Preemption təyin etmək üçün müxtəlif yollar təklif edir. Parametr bir neçə hissədən ibarətdir: obyekt PriorityClass və sahə təsvirləri priorityClassName pod spesifikasiyasında. Məsələni nəzərdən keçirək:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 99999
globalDefault: false
description: "This priority class should be used for very important pods only"

Biz yaradırıq PriorityClass, ona ad, təsvir və dəyər verin. Daha yüksək value, prioritet nə qədər yüksəkdir. Dəyər 32-dan kiçik və ya ona bərabər olan hər hansı 1 bitlik tam ədəd ola bilər. Daha yüksək qiymətlər kritik kritik sistem podları üçün qorunur, adətən onları qabaqcadan seçmək mümkün deyil. Çıxarma yalnız yüksək prioritet podun dönmək üçün yeri olmadığı halda baş verəcək, o zaman müəyyən bir qovşaqdan bəzi podlar evakuasiya ediləcək. Bu mexanizm sizin üçün çox sərtdirsə, o zaman seçimi əlavə edə bilərsiniz preemptionPolicy: Never, və sonra heç bir üstünlük olmayacaq, pod növbədə birinci olacaq və planlaşdırıcının bunun üçün pulsuz resurslar tapmasını gözləyin.

Sonra, adı təyin etdiyimiz bir pod yaradırıq priorityClassName:

apiVersion: v1
kind: Pod
metadata:
  name: static-web
  labels:
    role: myrole
 spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP
  priorityClassName: high-priority
          

İstədiyiniz qədər prioritet siniflər yarada bilərsiniz, baxmayaraq ki, bununla məşğul olmamaq tövsiyə olunur (məsələn, özünüzü aşağı, orta və yüksək prioritetlərlə məhdudlaşdırın).

Beləliklə, lazım gələrsə, nginx-ingress-controller, coredns və s. kimi kritik xidmətlərin tətbiqinin səmərəliliyini artıra bilərsiniz.

9. ETCD klasterinizi optimallaşdırın

Doqquz Kubernetes Performans Məsləhətləri

ETCD-ni bütün klasterin beyni adlandırmaq olar. Bu verilənlər bazasının işini yüksək səviyyədə saxlamaq çox vacibdir, çünki "Kub" da əməliyyatların sürəti ondan asılıdır. Kifayət qədər standart və eyni zamanda yaxşı bir həll kube-apiserver üçün minimum gecikmə üçün master qovşaqlarda ETCD klasterini saxlamaq olardı. Bu mümkün deyilsə, ETCD-ni iştirakçılar arasında yaxşı bant genişliyi ilə mümkün qədər yaxın yerləşdirin. ETCD-dən neçə qovşağın çoxluğa zərər vermədən düşə biləcəyinə də diqqət yetirin.

Doqquz Kubernetes Performans Məsləhətləri

Nəzərə alın ki, klasterdə iştirakçıların sayında həddindən artıq artım performans hesabına qüsurlara dözümlülüyünü artıra bilər, hər şey insafda olmalıdır.

Xidmətin qurulması haqqında danışırıqsa, bir neçə tövsiyə var:

  1. Klasterin ölçüsünə əsaslanaraq yaxşı aparatlara sahib olun (oxuya bilərsiniz burada).

  2. Bir cüt DC və ya şəbəkəniz arasında klaster yaymısınızsa və disklər arzuolunan çox şey buraxırsa, bir neçə parametri dəyişdirin (oxuya bilərsiniz). burada).

Nəticə

Bu məqalə komandamızın riayət etməyə çalışdığı məqamları təsvir edir. Bu, addım-addım hərəkətlərin təsviri deyil, klasterin yükünü optimallaşdırmaq üçün faydalı ola biləcək seçimlərdir. Aydındır ki, hər bir klaster özünəməxsus şəkildə unikaldır və tənzimləmə həlləri çox fərqli ola bilər, ona görə də sizdən rəy almaq maraqlı olardı: Kubernetes klasterinizi necə izləyirsiniz, onun işini necə yaxşılaşdırırsınız. Təcrübənizi şərhlərdə paylaşın, bunu bilmək maraqlı olacaq.

Mənbə: www.habr.com