Kion ni scias pri mikroservoj

Saluton! Mi nomiĝas Vadim Madison, mi gvidas la disvolviĝon de la Avito-Sistema Platformo. Oni diris pli ol unufoje, kiel ni en la kompanio transiras de monolita arkitekturo al mikroservo. Estas tempo konigi kiel ni transformis nian infrastrukturon por profiti la plej grandan parton de mikroservoj kaj malhelpi nin perdiĝi en ili. Kiel PaaS helpas nin ĉi tie, kiel ni simpligis deplojon kaj reduktis la kreadon de mikroservo al unu klako - plu legu. Ne ĉio, pri kio mi skribas sube, estas plene efektivigita en Avito, iuj el ĝi estas kiel ni disvolvas nian platformon.

(Kaj ĉe la fino de ĉi tiu artikolo, mi parolos pri la ŝanco ĉeesti tritagan seminarion de mikroserva arkitektura fakulo Chris Richardson).

Kion ni scias pri mikroservoj

Kiel ni venis al mikroservoj

Avito estas unu el la plej grandaj klasifikitaj retejoj en la mondo; pli ol 15 milionoj da novaj reklamoj estas publikigitaj sur ĝi ĉiutage. Nia backend akceptas pli ol 20 mil petojn sekundo. Nuntempe ni havas plurajn centojn da mikroservoj.

Ni konstruas mikroservan arkitekturon jam de pluraj jaroj. Kiel ĝuste - niaj kolegoj detale rakontis ĉe nia sekcio ĉe RIT++ 2017. Ĉe CodeFest 2017 (vidu. видео), Sergey Orlov kaj Miĥail Prokopĉuk detale klarigis kial ni bezonas la transiron al mikroservoj kaj kian rolon Kubernetes ludis ĉi tie. Nu, nun ni faras ĉion por minimumigi la skalajn kostojn, kiuj estas propraj al tia arkitekturo.

Komence, ni ne kreis ekosistemon, kiu amplekse helpus nin evoluigi kaj lanĉi mikroservojn. Ili simple kolektis prudentajn malfermfontajn solvojn, lanĉis ilin hejme kaj invitis la programiston trakti ilin. Kiel rezulto, li iris al dekduo lokoj (instrumentpaneloj, internaj servoj), post kio li plifortiĝis en sia deziro tranĉi kodon laŭ la malnova maniero, en monolito. La verda koloro en la subaj diagramoj indikas, kion la programisto faras unumaniere aŭ alian per siaj propraj manoj, kaj la flava koloro indikas aŭtomatigon.

Kion ni scias pri mikroservoj

Nun en la ilo PaaS CLI, nova servo estas kreita per unu komando, kaj nova datumbazo estas aldonita kun du pli kaj deplojita al Stage.

Kion ni scias pri mikroservoj

Kiel venki la epokon de "mikroserva fragmentiĝo"

Kun monolita arkitekturo, pro konsistenco de ŝanĝoj en la produkto, programistoj estis devigitaj ekscii, kio okazas kun siaj najbaroj. Laborante pri la nova arkitekturo, servokuntekstoj ne plu dependas unu de la alia.

Krome, por ke mikroserva arkitekturo estu efika, multaj procezoj devas esti establitaj, nome:

• arbohakado;
• peti spuradon (Jaeger);
• erara agregado (Sentry);
• statusoj, mesaĝoj, eventoj de Kubernetes (Event Stream Processing);
• raslimo / interrompilo (vi povas uzi Hystrix);
• kontrolo de servo konektebleco (ni uzas Netramesh);
• monitorado (Grafana);
• asembleo (TeamCity);
• komunikado kaj sciigo (Slack, retpoŝto);
• tasko spurado; (Jira)
• preparado de dokumentado.

Por certigi, ke la sistemo ne perdu sian integrecon kaj restas efika dum ĝi grimpas, ni repensis la organizon de mikroservoj en Avito.

Kiel ni administras mikroservojn

La sekvaj helpas efektivigi unuigitan "partian politikon" inter multaj Avito-mikroservoj:

  • dividante infrastrukturon en tavolojn;
  • Platformo kiel Servo (PaaS) koncepto;
  • monitorante ĉion, kio okazas kun mikroservoj.

Infrastrukturaj abstraktaj tavoloj inkluzivas tri tavolojn. Ni iru de supre al sube.

A. Supre - servo maŝo. Komence ni provis Istio, sed montriĝis, ke ĝi uzas tro multajn rimedojn, kio estas tro multekosta por niaj volumoj. Tial altranga inĝeniero en la arkitektura teamo Aleksandr Lukyanchenko evoluigis sian propran solvon - Netramesh (disponebla en Open Source), kiun ni nuntempe uzas en produktado kaj kiu konsumas plurajn fojojn malpli da rimedoj ol Istio (sed ne faras ĉion pri kio Istio povas fanfaroni).
B. Medium - Kubernetes. Ni deplojas kaj funkciigas mikroservojn sur ĝi.
C. Fundo - nuda metalo. Ni ne uzas nubojn aŭ aferojn kiel OpenStack, sed fidas tute je nuda metalo.

Ĉiuj tavoloj estas kombinitaj de PaaS. Kaj ĉi tiu platformo, siavice, konsistas el tri partoj.

I. Generatoroj, kontrolita per CLI-ilaĵo. Estas ŝi, kiu helpas la programiston krei mikroservon en la ĝusta maniero kaj kun minimuma peno.

II. Plifirmigita kolektanto kun kontrolo de ĉiuj iloj per komuna panelo.

III. Stokado. Ligiĝas kun planistoj, kiuj aŭtomate fiksas ellasilon por gravaj agoj. Dank' al tia sistemo, eĉ unu tasko ne mankas nur ĉar iu forgesis agordi taskon en Jira. Por tio ni uzas internan ilon nomatan Atlaso.

Kion ni scias pri mikroservoj

La efektivigo de mikroservoj en Avito ankaŭ estas efektivigita laŭ ununura skemo, kiu simpligas kontrolon super ili en ĉiu etapo de evoluo kaj liberigo.

Kiel funkcias norma mikroserva disvolva dukto?

Ĝenerale, la mikroserva krea ĉeno aspektas jene:

CLI-puŝo → Daŭra Integriĝo → Baki → Disvolvi → Artefaritaj testoj → Kanariaj testoj → Squeeze Testing → Produktado → Prizorgado.

Ni trarigardu ĝin ĝuste en ĉi tiu ordo.

CLI-puŝo

• Krei mikroservon.
Ni luktis dum longa tempo por instrui ĉiun programiston kiel fari mikroservojn. Tio inkludis verki detalajn instrukciojn en Confluence. Sed la skemoj ŝanĝiĝis kaj estis kompletigitaj. La rezulto estas, ke botelkolo aperis komence de la vojaĝo: necesis multe pli da tempo por lanĉi mikroservojn, kaj ankoraŭ problemoj ofte aperis dum ilia kreado.

Al la fino, ni konstruis simplan CLI-ilaĵon, kiu aŭtomatigas la bazajn paŝojn dum kreado de mikroservo. Fakte, ĝi anstataŭigas la unuan git-puŝon. Jen kion precize ŝi faras.

— Kreas servon laŭ ŝablono — paŝo post paŝo, en reĝimo “sorĉisto”. Ni havas ŝablonojn por la ĉefaj programlingvoj en la backend de Avito: PHP, Golang kaj Python.

- Unu komando samtempe deplojas medion por loka evoluo sur specifa maŝino - Minikube estas lanĉita, Helm-diagramoj estas aŭtomate generitaj kaj lanĉitaj en lokaj kubernetoj.

— Ligas la bezonatan datumbazon. La programisto ne bezonas scii la IP, ensaluton kaj pasvorton por akiri aliron al la datumbazo, kiun li bezonas - ĉu loke, ĉe Stage, aŭ en produktado. Krome, la datumbazo estas deplojita tuj en mistolerema agordo kaj kun ekvilibro.

— Ĝi mem realigas muntadon. Ni diru, ke programisto korektis ion en mikroservo per sia IDE. La ilo vidas ŝanĝojn en la dosiersistemo kaj, surbaze de ili, rekonstruas la aplikaĵon (por Golang) kaj rekomencas. Por PHP, ni simple plusendas la dosierujon ene de la kubo kaj tie live-reŝargi estas akirita "aŭtomate".

— Generas aŭtotestojn. En formo de malplenaj, sed sufiĉe taŭga por uzo.

• Disvolviĝo de mikroservoj.

Deploji mikroservon kutimis esti iom da laboro por ni. La sekvantaroj estis postulataj:

I. Dockerfile.

II. Agordo.
III. Helm-diagramo, kiu mem estas maloportuna kaj inkluzivas:

— la leteroj mem;
— ŝablonoj;
- specifaj valoroj konsiderante malsamajn mediojn.

Ni forigis la doloron de reverkado de manifestoj de Kubernetes, do ili nun estas generitaj aŭtomate. Sed plej grave, ili simpligis la deplojon ĝis la limo. De nun ni havas Dockerfile, kaj la programisto skribas la tutan agordon en unu mallonga app.toml-dosiero.

Kion ni scias pri mikroservoj

Jes, kaj en app.toml mem estas nenio por fari dum minuto. Ni specifas kie kaj kiom da kopioj de la servo por levi (sur la dev-servilo, sur enscenigo, en produktado), kaj indikas ĝiajn dependecojn. Rimarku la liniograndon = "malgranda" en la [motoro] bloko. Ĉi tiu estas la limo, kiu estos asignita al la servo per Kubernetes.

Tiam, surbaze de la agordo, ĉiuj necesaj Helm-diagramoj estas aŭtomate generitaj kaj ligoj al la datumbazoj estas kreitaj.

• Baza validigo. Tiaj kontroloj ankaŭ estas aŭtomatigitaj.
Bezonas spuri:
— ĉu ekzistas Dockerfile;
— ĉu ekzistas app.toml;
— ĉu ekzistas dokumentaro havebla?
— ĉu la dependeco estas en ordo?
— ĉu atentaj reguloj estas starigitaj.
Ĝis la lasta punkto: la posedanto de la servo mem determinas kiajn produktajn metrikojn monitori.

• Preparado de dokumentado.
Ankoraŭ problema areo. Ĝi ŝajnas esti la plej evidenta, sed samtempe ĝi ankaŭ estas rekordo "ofte forgesita", kaj tial vundebla ligo en la ĉeno.
Necesas ke ekzistas dokumentaro por ĉiu mikroservo. Ĝi inkluzivas la sekvajn blokojn.

I. Mallonga priskribo de la servo. Laŭvorte kelkaj frazoj pri tio, kion ĝi faras kaj kial ĝi bezonas.

II. Arkitektura diagramo ligo. Gravas, ke per rapida rigardo al ĝi estas facile kompreni, ekzemple, ĉu vi uzas Redis por kaŝmemoro aŭ kiel la ĉefa datumvendejo en konstanta reĝimo. En Avito nuntempe ĉi tio estas ligilo al Confluence.

III. Runbook. Mallonga gvidilo pri komenci la servon kaj la komplikaĵojn de pritraktado de ĝi.

IV. Oftaj Demandoj, kie estus bone antaŭvidi la problemojn, kiujn viaj kolegoj povas renkonti kiam ili laboras kun la servo.

V. Priskribo de finpunktoj por la API. Se subite vi ne specifis la destinojn, kolegoj, kies mikroservoj rilatas al viaj, preskaŭ certe pagos por ĝi. Nun ni uzas Swagger kaj nian solvon nomitan mallonga por ĉi tio.

VI. Etikedoj. Aŭ signoj kiuj montras al kiu produkto, funkcieco aŭ struktura divido de la firmao apartenas la servo. Ili helpas vin rapide kompreni, ekzemple, ĉu vi tranĉas funkciojn, kiujn viaj kolegoj lanĉis por la sama komerca unuo antaŭ semajno.

VII. Posedanto aŭ posedantoj de la servo. Plejofte, ĝi - aŭ ili - povas esti aŭtomate determinita uzante PaaS, sed por esti sur la sekura flanko, ni postulas, ke la programisto specifu ilin permane.

Fine, estas bona praktiko revizii dokumentadon, simile al koda revizio.

Kontinua Integriĝo

  • Preparado de deponejoj.
  • Kreante dukto en TeamCity.
  • Agordo de rajtoj.
  • Serĉu posedantojn de servo. Estas hibrida skemo ĉi tie - mana markado kaj minimuma aŭtomatigo de PaaS. Plene aŭtomata skemo malsukcesas kiam servoj estas transdonitaj por subteno al alia evolua teamo aŭ, ekzemple, se la serva programisto forlasas.
  • Registrante servon en Atlaso (Vidu supre). Kun ĉiuj ĝiaj posedantoj kaj dependecoj.
  • Kontrolante migradojn. Ni kontrolas ĉu iu el ili estas potenciale danĝera. Ekzemple, en unu el ili aperas ŝanĝtabelo aŭ io alia, kiu povas rompi la kongruon de la datumskemo inter malsamaj versioj de la servo. Tiam la migrado ne estas farita, sed metita en abonon - la PaaS devas signali la servoposedanton kiam estas sekure uzi ĝin.

Baku

La sekva etapo estas pakado de servoj antaŭ deplojo.

  • Konstruante la aplikaĵon. Laŭ la klasikaĵoj - en bildo de Docker.
  • Generacio de Helm-diagramoj por la servo mem kaj rilataj rimedoj. Inkluzive por datumbazoj kaj kaŝmemoro. Ili estas kreitaj aŭtomate laŭ la agordo app.toml, kiu estis generita ĉe la CLI-push-stadio.
  • Kreante biletojn por administrantoj por malfermi havenojn (kiam necesas).
  • Kurante unutestojn kaj kalkulante kodan kovradon. Se la koda kovrado estas sub la specifita sojlo, tiam plej verŝajne la servo ne iros plu - al deplojo. Se ĝi estas akceptebla, tiam la servo estos asignita "pesiman" koeficienton: tiam, se ne estas plibonigo de la indikilo laŭlonge de la tempo, la programisto ricevos sciigon, ke ne estas progreso laŭ provoj ( kaj io devas esti farita pri tio).
  • Kontado pri memoro kaj CPU-limigoj. Ni ĉefe verkas mikroservojn en Golang kaj funkciigas ilin en Kubernetes. Tial unu subtileco asociita kun la propreco de la Golang-lingvo: defaŭlte, kiam komenciĝas, ĉiuj kernoj sur la maŝino estas uzataj, se vi ne eksplicite fiksas la variablon GOMAXPROCS, kaj kiam pluraj tiaj servoj estas lanĉitaj sur la sama maŝino, ili komenciĝas. konkuri por rimedoj, enmiksante unu la alian. La malsupraj grafikaĵoj montras kiel la ekzekuttempo ŝanĝiĝas se vi rulas la aplikaĵon sen disputo kaj en la kuro por rimedoj. (Fontoj de grafikaĵoj estas tie).

Kion ni scias pri mikroservoj

Tempo de ekzekuto, malpli estas pli bona. Maksimumo: 643ms, minimumo: 42ms. La foto estas klakebla.

Kion ni scias pri mikroservoj

Tempo por kirurgio, malpli estas pli bone. Maksimumo: 14091 ns, minimumo: 151 ns. La foto estas klakebla.

En la asemblea prepara stadio, vi povas agordi ĉi tiun variablon eksplicite aŭ vi povas uzi la bibliotekon aŭtomaxprocs de la uloj de Uber.

Deploji

• Kontrolado de konvencioj. Antaŭ ol vi komencas liveri servajn asembleojn al viaj celitaj medioj, vi devas kontroli la jenajn:
- API finpunktoj.
— Konformeco de respondoj de API-finpunktoj kun la skemo.
— Protokolo-formato.
— Agordi kapliniojn por petoj al la servo (nuntempe tio estas farita per netramesh)
— Agordi la posedanto-ĵetonon dum sendado de mesaĝoj al la eventobuso. Ĉi tio estas necesa por spuri la konekteblecon de servoj trans la buso. Vi povas sendi kaj idempotentajn datumojn al la buso, kiu ne pliigas la konekteblecon de servoj (kio estas bona), kaj komercajn datumojn, kiuj plifortigas la konekteblecon de servoj (kio estas tre malbona!). Kaj ĉe la punkto, kiam ĉi tiu konektebleco fariĝas problemo, kompreni kiu skribas kaj legas la buson helpas ĝuste apartigi servojn.

Ankoraŭ ne estas tre multaj kongresoj en Avito, sed ilia naĝejo plivastiĝas. Ju pli tiaj interkonsentoj disponeblas en formo, kiun la teamo komprenas kaj komprenas, des pli facile estas konservi konsistencon inter mikroservoj.

Sintezaj provoj

• Fermita buklo testado. Por tio ni nun uzas malferman fonton Hoverfly.io. Unue, ĝi registras la realan ŝarĝon sur la servo, tiam - nur en fermita buklo - ĝi imitas ĝin.

• Streĉa Testado. Ni provas alporti ĉiujn servojn al optimuma rendimento. Kaj ĉiuj versioj de ĉiu servo devas esti submetitaj al ŝarĝtestado - tiel ni povas kompreni la nunan agadon de la servo kaj la diferencon kun antaŭaj versioj de la sama servo. Se, post serva ĝisdatigo, ĝia rendimento malpliiĝis je unu fojo kaj duono, ĉi tio estas klara signalo por ĝiaj posedantoj: vi devas fosi la kodon kaj korekti la situacion.
Ni uzas la kolektitajn datumojn, ekzemple, por ĝuste efektivigi aŭtomatan skalon kaj, fine, ĝenerale kompreni kiom skalebla estas la servo.

Dum ŝarĝtestado, ni kontrolas ĉu la konsumo de rimedoj renkontas la fiksitajn limojn. Kaj ni koncentriĝas ĉefe pri ekstremaĵoj.

a) Ni rigardas la totalan ŝarĝon.
- Tro malgranda - plej verŝajne io tute ne funkcias se la ŝarĝo subite plurfoje falas.
- Tro granda - optimumigo necesas.

b) Ni rigardas la detranĉon laŭ RPS.
Ĉi tie ni rigardas la diferencon inter la nuna versio kaj la antaŭa kaj la totala kvanto. Ekzemple, se servo produktas 100 rps, tiam ĝi estas aŭ malbone skribita, aŭ ĉi tio estas ĝia specifeco, sed ĉiukaze ĉi tio estas kialo por rigardi la servon tre atente.
Se, male, estas tro da RPS, tiam eble estas ia cimo kaj iuj el la finpunktoj ĉesis ekzekuti la utilan ŝarĝon, sed iu alia simple ekfunkciiĝas. return true;

Kanariaj provoj

Post kiam ni trapasas la sintezajn provojn, ni testas la mikroservon sur malgranda nombro da uzantoj. Ni komencas zorge, kun eta parto de la celita publiko de la servo - malpli ol 0,1%. En ĉi tiu etapo, estas tre grave, ke la ĝustaj teknikaj kaj produktaj metrikoj estu inkluzivitaj en la monitorado, por ke ili montru la problemon en la servo kiel eble plej rapide. La minimuma tempo por kanaria testo estas 5 minutoj, la ĉefa estas 2 horoj. Por kompleksaj servoj, ni agordas la tempon permane.
Ni analizu:
— lingvo-specifaj metrikoj, precipe, php-fpm-laboristoj;
— eraroj en Sentry;
— respondaj statoj;
— responda tempo, preciza kaj averaĝa;
— latenteco;
— esceptoj, prilaboritaj kaj netraktitaj;
- produktaj metrikoj.

Premu Testado

Squeeze Testing ankaŭ estas nomita "prema" testado. La nomo de la tekniko estis enkondukita en Netflix. Ĝia esenco estas, ke unue ni plenigas unu kazon per reala trafiko ĝis la punkto de fiasko kaj tiel fiksas ĝian limon. Tiam ni aldonas alian ekzemplon kaj ŝarĝas ĉi tiun paron - denove al la maksimumo; ni vidas ilian plafonon kaj delton kun la unua "premo". Kaj do ni konektas unu okazon samtempe kaj kalkulas la ŝablonon de ŝanĝoj.
Testaj datumoj per "premado" ankaŭ fluas en komunan metrikan datumbazon, kie ni aŭ riĉigas la artefaritajn ŝarĝrezultojn per ili, aŭ eĉ anstataŭigas "sintezaĵojn" per ili.

Produktado

• Skalado. Kiam ni lanĉas servon al produktado, ni kontrolas kiel ĝi skalas. Laŭ nia sperto, monitorado de nur CPU-indikiloj estas neefika. Aŭtomata skalo kun RPS-benchmarking en ĝia pura formo funkcias, sed nur por certaj servoj, kiel interreta streaming. Do ni unue rigardas aplikaĵ-specifajn produktajn metrikojn.

Kiel rezulto, dum skalo ni analizas:
- CPU kaj RAM-indikiloj,
- la nombro da petoj en la vico,
- tempo de respondo,
— prognozo bazita sur akumulitaj historiaj datumoj.

Dum grimpado de servo, estas ankaŭ grave kontroli ĝiajn dependecojn, por ke ni ne skalu la unuan servon en la ĉeno, kaj tiuj, kiujn ĝi aliras, malsukcesas sub ŝarĝo. Por establi akcepteblan ŝarĝon por la tuta aro de servoj, ni rigardas la historiajn datumojn de la "plej proksima" dependa servo (surbaze de kombinaĵo de CPU kaj RAM-indikiloj, kunigitaj kun aplikaj specifaj metrikoj) kaj komparas ilin kun la historiaj datumoj. de la inicialiga servo, kaj tiel plu tra la "dependeca ĉeno" ", de supre ĝis malsupre.

Servo

Post kiam la mikroservo funkcias, ni povas ligi ellasilon al ĝi.

Jen tipaj situacioj, en kiuj okazas ellasiloj.
— Eble danĝeraj migradoj detektitaj.
— Sekurecaj ĝisdatigoj estis publikigitaj.
— La servo mem ne estas ĝisdatigita dum longa tempo.
— La ŝarĝo sur la servo rimarkeble malpliiĝis aŭ iuj el ĝiaj produktaj mezuroj estas ekster la normala gamo.
— La servo ne plu plenumas la novajn platformpostulojn.

Iuj el la ellasiloj respondecas pri stabileco de funkciado, iuj - kiel funkcio de sistemo prizorgado - ekzemple, iu servo ne estis deplojita dum longa tempo kaj ĝia baza bildo ĉesis pasi sekureckontrolojn.

Panelo

Resume, la panelo estas la kontrolpanelo de nia tuta PaaS.

  • Ununura punkto de informo pri la servo, kun datumoj pri ĝia testkovrado, la nombro da ĝiaj bildoj, la nombro da produktadkopioj, versioj, ktp.
  • Ilo por filtri datumojn laŭ servoj kaj etikedoj (markoj de aparteno al komercaj unuoj, produkta funkcieco, ktp.)
  • Ilo por integriĝo kun infrastrukturaj iloj por spurado, protokolado kaj monitorado.
  • Ununura punkto de serva dokumentaro.
  • Ununura vidpunkto de ĉiuj eventoj trans servoj.

Kion ni scias pri mikroservoj
Kion ni scias pri mikroservoj
Kion ni scias pri mikroservoj
Kion ni scias pri mikroservoj

Tuta

Antaŭ enkonduki PaaS, nova programisto povus pasigi plurajn semajnojn por kompreni ĉiujn ilojn necesajn por lanĉi mikroservon en produktado: Kubernetes, Helm, niaj internaj funkcioj de TeamCity, agordi konektojn al datumbazoj kaj kaŝmemoroj en mistolerema maniero, ktp. prenas kelkajn horojn por legi la rapidan komencon kaj krei la servon mem.

Mi donis raporton pri ĉi tiu temo por HighLoad++ 2018, vi povas spekti ĝin видео и prezento.

Bonustrako por tiuj, kiuj legas ĝis la fino

Ni ĉe Avito organizas internan tritagan trejnadon por programistoj de Chris Richardson, spertulo pri mikroserva arkitekturo. Ni ŝatus doni la ŝancon partopreni en ĝi al unu el la legantoj de ĉi tiu afiŝo. estas La trejna programo estis afiŝita.

La trejnado okazos de la 5-a ĝis la 7-a de aŭgusto en Moskvo. Ĉi tiuj estas labortagoj kiuj estos plene okupitaj. Tagmanĝo kaj trejnado estos en nia oficejo, kaj la elektita partoprenanto pagos mem vojaĝadon kaj loĝadon.

Vi povas peti partoprenon en ĉi tiu gugla formo. De vi - la respondo al la demando kial vi devas ĉeesti la trejnadon kaj informojn pri kiel kontakti vin. Respondu angle, ĉar Chris elektos la partoprenanton, kiu mem ĉeestos la trejnadon.
Ni anoncos la nomon de la trejna partoprenanto en ĝisdatigo de ĉi tiu afiŝo kaj en sociaj retoj Avito por programistoj (AvitoTech en Facebook, Вконтакте, Twitter) ne pli malfrue ol la 19-an de julio.

fonto: www.habr.com

Aldoni komenton