Na počesť aprílových bláznov otvoril Reddit stránku pre používateľov, aby sa vyjadrili, ale projekt vytvorený ako vtip sa stal akousi stenou pre kolektívne graffiti od účastníkov z celého sveta, čo ukazuje, akú stopu chcú zanechať v histórii. .
1. apríla Reddit spustil projekt Place, stránku s prázdnym plátnom, na ktorú si každý používateľ fóra mohol nakresliť ľubovoľný obrázok. Umelci mali obmedzenia: mohli nakresliť iba jeden pixel jednej zo 16 farieb každých päť minút a veľkosti plátna boli tiež obmedzené. Na nakreslené môžu byť nakreslené ďalšie pixely (a potom môže prvý autor opäť nakresliť svoj vlastný pixel na pixel súpera), čo je dôvod, prečo sú autori obrázkov a priori v konflikte. Ako je uvedené v popise plátna, projekt je navrhnutý pre spoločnú kreativitu - „Každý z vás môže vytvoriť niečo individuálne. Spolu môžete vytvoriť niečo väčšie."
V momente spustenia projektu všetci doslova zaútočili na plátno – každý pixel na ňom bol zaplnený. Najprv používatelia jednoducho napichali farby do voľných pixelov, ale potom sa začali vytvárať tímy a začali kresliť jeden z najjednoduchších možných nápadov, ktorý našiel rovnako zmýšľajúcich ľudí – štátne vlajky. Čím viac používateľov na plátne pracovalo, tým zaujímavejšie a komplexné myšlienky prišlo im na um. A "The Place" sa z prvoaprílového projektu rozrástlo na miesto, kde sa používatelia z celého sveta spájajú do skutočných komunít, aby svetu niečo ukázali a podporili ich tvorbu od skupín nájazdníkov, ktorí si chcú naozaj len nakresliť nejaký svoj vlastný kreslenie.
Miesto počas prvých otváracích hodín
„Miesto“ sa stalo online nástenkou, ale každá je starostlivo vytvorená. Napríklad, ak chcete nakresliť a chrániť pred squattermi logo Linuxu s rozmermi 48 x 68 pixelov, muselo by to urobiť 3 264 ľudí súčasne.
![](https://i2.wp.com/medialeaks.ru/wp-content/uploads/2017/04/Bezyimyannyiy-4.jpg)
Používatelia sa stretávajú, aby implementovali nápady, ktoré sú menšie a jednoduchšie, ale stále veľmi tímovo orientované, ako napríklad tento rad sŕdc s vlajkami rozdielne krajiny(a nielen): každý je zodpovedný za „svoje“ srdce, no všetci spolu ľudia nechtiac tvoria jeden tím.
![](https://i0.wp.com/medialeaks.ru/wp-content/uploads/2017/04/Bezyimyannyiy-5.jpg)
A iní píšu celé plátna textu, napríklad ako táto skupina fanúšikov “ Hviezdne vojny“, ktorý napísal a podporil slávny monológ najvyššieho kancelára Palpatina o Sithovi Darthovi Plagueisovi z tretej epizódy vesmírnej ságy.
![](https://i0.wp.com/medialeaks.ru/wp-content/uploads/2017/04/Bezyimyannyiy-6.jpg)
Niektorí používatelia sa však sťažovali, že účastníci projektu sa nahradili robotmi, ktorí každých päť minút automaticky aktualizujú pixel obsadený autorom. Napriek tomu, že pri kreslení každého pixelu musí používateľ zopakovať súbor akcií (napríklad výber konkrétnej farby), niektorým sa podarilo obísť bezpečnostný mechanizmus a vytvoriť roboty, ktoré im obrázky kreslia.
Sú však aj takí, ktorí stále vytvárajú špeciálne vlákna, snažiac sa povzbudiť rovnako zmýšľajúcich ľudí, ktorí pomôžu nakresliť a zanechať pre budúce generácie niečo ako... zvracanie Ricka Sancheza z karikatúry „Rick and Morty“. vážne? Si si istý, že to nie sú roboti?
![](https://i2.wp.com/medialeaks.ru/wp-content/uploads/2017/04/Bezyimyannyiy-7.jpg)
Po 72 hodinách práce bol projekt uzavretý. Správa zdrojov poďakovala všetkým za ich účasť a za to, že sa ľudia spojili, „aby vytvorili niečo viac“.
Reddit sa často stáva miestom pre rôzne spoločenské aktivity. Napríklad nedávno smutní používatelia zdroja sa rozhodli opýtať, aké to bolo vstávať každý deň s úsmevom. A predtým sa čitatelia Redditu medzi sebou podelili o príbehy o dievčatách. Nie sú to len anonymní ľudia, ktorí využívajú populárny zdroj: herec nedávno strávil niekoľko hodín odpovedaním na otázky používateľov o filme „Trainspotting“.
Na začiatok bolo mimoriadne dôležité určiť požiadavky na prvoaprílový projekt, pretože musel byť spustený bez „pretaktovania“, aby k nemu mali okamžite prístup všetci používatelia Redditu. Ak by nefungoval dokonale od samého začiatku, sotva by upútal pozornosť mnohých ľudí.
"Doska" by mala mať veľkosť dlaždíc 1000x1000, aby vyzerala veľmi veľká.
Všetci klienti by mali byť synchronizovaní a mali by zobrazovať rovnaký stav dosky. Koniec koncov, ak majú rôzni používatelia rôzne verzie, bude pre nich ťažké komunikovať.
Musíte podporovať aspoň 100 000 súbežných používateľov.
Používatelia môžu umiestniť jednu dlaždicu každých päť minút. Preto je potrebné udržiavať priemernú rýchlosť aktualizácie 100 000 dlaždíc za päť minút (333 aktualizácií za sekundu).
Projekt by nemal negatívne ovplyvniť prevádzku ostatných častí a funkcií lokality (aj keď je na r/Place vysoká návštevnosť).
- Flexibilná konfigurácia musí byť poskytnutá v prípade neočakávaných prekážok alebo porúch. To znamená, že musíte byť schopní prispôsobiť veľkosť dosky a povolenú frekvenciu kreslenia za behu, ak je množstvo údajov príliš veľké alebo frekvencia aktualizácie je príliš vysoká.
Backend
Implementačné riešenia
Hlavným problémom pri vytváraní backendu bola synchronizácia zobrazenia stavu dosky pre všetkých klientov. Rozhodlo sa, že klienti budú počúvať udalosti umiestňovania dlaždíc v reálnom čase a okamžite sa budú pýtať na stav celej dosky. Mierne zastaraný úplný stav je prijateľný, ak ste sa prihlásili na odber aktualizácií pred vygenerovaním úplného stavu. Keď klient dostane úplný stav, zobrazí všetky dlaždice, ktoré dostal počas čakania; všetky nasledujúce dlaždice sa musia objaviť na hracej ploche ihneď po ich prijatí.
Aby táto schéma fungovala, musí byť žiadosť o úplný stav dosky dokončená čo najrýchlejšie. Najprv sme chceli uložiť celú dosku do jedného radu v Cassandre a nechať každú požiadavku jednoducho prečítať tento riadok. Formát každého stĺpca v tomto riadku bol:
(x, y): ('timestamp': epochms, 'author': user_name, 'color': color)
Ale keďže doska obsahuje milión dlaždíc, potrebovali sme prečítať milión stĺpcov. Na našom produkčnom klastri to trvalo až 30 sekúnd, čo bolo neprijateľné a mohlo by to viesť k nadmernému zaťaženiu Cassandry.
Potom sme sa rozhodli uložiť celú dosku v Redis. Zobrali sme bitové pole milióna štvorbitových čísel, z ktorých každé mohlo zakódovať štvorbitovú farbu, a súradnice x a y boli určené posunom (offset = x + 1000y) v bitovom poli. Na získanie úplného stavu dosky bolo potrebné prečítať celé bitové pole.
Dlaždice bolo možné aktualizovať aktualizáciou hodnôt pri konkrétnych posunoch (nie je potrebné blokovať alebo vykonávať celú procedúru čítania/aktualizácie/zápisu). Všetky detaily však stále musia byť uložené v Cassandre, aby používatelia mohli zistiť, kto a kedy umiestnil jednotlivé dlaždice. Tiež sme plánovali použiť Cassandru na obnovenie dosky, keď Redis havaroval. Prečítanie celej dosky z nej trvalo necelých 100 ms, čo bolo celkom rýchle.
Takto sme uložili farby v Redis pomocou príkladu dosky 2x2:
Obávali sme sa, že by sme mohli naraziť na priepustnosť čítania v Redis. Ak by sa pripojilo alebo aktualizovalo veľa klientov súčasne, všetci by súčasne posielali požiadavky na úplný stav dosky. Keďže doska predstavovala zdieľaný globálny stav, samozrejmým riešením bolo použitie vyrovnávacej pamäte. Rozhodli sme sa ukladať do vyrovnávacej pamäte na úrovni CDN (Fastly), pretože to bolo jednoduchšie na implementáciu a vyrovnávacia pamäť bola získaná najbližšie ku klientom, čo skrátilo čas na prijatie odpovede.
Požiadavky na stav plnej penzie uložil Fastly do vyrovnávacej pamäte s časovým limitom jednej sekundy. Zabrániť veľké množstvožiadostí, keď vypršal časový limit, použili sme hlavičku stale-while-revalidate. Rýchlo podporuje približne 33 POP, ktoré sa navzájom nezávisle vyrovnávajú, takže sme očakávali, že za sekundu dostaneme až 33 žiadostí o stav celej dosky.
Na zverejňovanie aktualizácií pre všetkých klientov sme použili našu službu websocket. Predtým sme ho úspešne používali na napájanie Reddit.Live s viac ako 100 000 súbežnými používateľmi pre živé upozornenia na súkromné správy a ďalšie funkcie. Služba bola tiež základným kameňom naše minulé prvoaprílové projekty – The Button a Robin. V prípade r/Place klienti podporovali pripojenia websocket, aby dostávali aktualizácie umiestnení dlaždíc v reálnom čase.
API
Získanie stavu plnej penzie
![](https://i1.wp.com/habrastorage.org/files/c6b/c1b/5e7/c6bc1b5e76f443d5867d93cd0fb120c0.png)
Najprv žiadosti smerovali do Fastly. Ak by mal platnú kópiu dosky, okamžite by ju vrátil bez toho, aby kontaktoval aplikačné servery Reddit. Ak nie, alebo je kópia príliš stará, potom aplikácia Reddit prečíta celú tabuľu od Redis a vráti ju do Fastly, aby sa uložila do vyrovnávacej pamäte a vrátila klientovi.
![](https://i0.wp.com/habrastorage.org/files/16e/ee3/8bc/16eee38bcb1c43d99700a9d1425e2dcd.png)
Všimnite si, že rýchlosť žiadostí nikdy nedosiahla 33 za sekundu, čo znamená, že ukladanie do vyrovnávacej pamäte s Fastly bolo veľmi účinnými prostriedkami Ochrana aplikácie Reddit pred väčšinou žiadostí.
![](https://i1.wp.com/habrastorage.org/files/245/f4c/700/245f4c7007914e29a61c9c8cef1f0fdb.png)
A keď sa žiadosti dostali do aplikácie, Redis reagoval veľmi rýchlo.
Kreslenie dlaždice
![](https://i1.wp.com/habrastorage.org/files/7ce/d3f/c1e/7ced3fc1e0e7426592a3d1afe77543d5.png)
Fázy kreslenia dlaždice:
- Časová pečiatka posledného umiestnenia dlaždice používateľom sa číta z Cassandry. Ak to bolo pred menej ako piatimi minútami, nerobíme nič a používateľovi sa vráti chyba.
- Podrobnosti dlaždíc sú napísané Redisovi a Cassandre.
- Aktuálny čas je zaznamenaný v Cassandre ako posledný čas, kedy bola dlaždica umiestnená používateľom.
- Služba websocket odošle správu o novej dlaždici všetkým pripojeným klientom.
Aby sa zachovala prísna konzistencia, všetky zápisy a čítania v Cassandre sa vykonávali pomocou vrstvy konzistencie QUORUM.
V skutočnosti sme tu mali preteky, kde používatelia mohli umiestniť viacero dlaždíc naraz. Vo fázach 1–3 nedošlo k žiadnemu blokovaniu, takže súčasné pokusy o ťahanie dlaždíc mohli prejsť kontrolou v prvej fáze a mohli byť ťahané v druhej fáze. Zdá sa, že niektorí používatelia objavili túto chybu (alebo použili roboty, ktoré ignorovali limit frekvencie žiadostí) – a v dôsledku toho bolo pomocou nej umiestnených asi 15 000 dlaždíc (~0,09 % z celkového počtu).
Sadzby žiadostí a časy odozvy merané aplikáciou Reddit:
![](https://i1.wp.com/habrastorage.org/files/360/16d/850/36016d850d0349d88b12e971c372a723.png)
Maximálna rýchlosť umiestnenia dlaždíc bola takmer 200 za sekundu. To je pod naším odhadovaným limitom 333 dlaždíc/s (priemer za predpokladu, že 100 000 používateľov umiestni dlaždice každých päť minút).
![](https://i1.wp.com/habrastorage.org/files/ecd/ca9/7a7/ecdca97a7ee64bf5ad6c982938365598.png)
Získanie podrobností pre konkrétnu dlaždicu
![](https://i0.wp.com/habrastorage.org/files/12f/b92/563/12fb925631094e738354c4ad3b186320.png)
Pri vyžiadaní konkrétnych dlaždíc sa údaje čítali priamo z Cassandry.
Sadzby žiadostí a časy odozvy merané aplikáciou Reddit:
![](https://i0.wp.com/habrastorage.org/files/49b/157/286/49b157286eed4041962eae41c0337de8.png)
Táto žiadosť sa ukázala byť veľmi populárna. Okrem bežných požiadaviek klientov ľudia napísali skripty na získanie celej dosky po jednej dlaždici. Keďže táto požiadavka nebola uložená do vyrovnávacej pamäte v CDN, všetky požiadavky obsluhovala aplikácia Reddit.
![](https://i1.wp.com/habrastorage.org/files/730/443/62b/73044362b2b44ab0805a5fad19639cdd.png)
Čas odozvy na tieto požiadavky bol pomerne krátky a zostal na rovnakej úrovni počas celého trvania projektu.
Websockety
Nemáme jednotlivé metriky ukazujúce, ako r/Place ovplyvnilo službu websocket. Hodnoty však môžeme odhadnúť porovnaním údajov pred začiatkom projektu a po jeho ukončení.
Celkový počet pripojení k službe websocket:
![](https://i1.wp.com/habrastorage.org/files/7f0/a07/fdb/7f0a07fdbf164baba18d5f3749fad963.png)
Základné zaťaženie pred spustením r/Place bolo asi 20 000 spojení, vrchol 100 000 spojení. Takže na vrchole sme pravdepodobne mali asi 80 000 súbežných používateľov pripojených k r/Place.
Priepustnosť služby Websocket:
![](https://i2.wp.com/habrastorage.org/files/b72/cfb/6d4/b72cfb6d4b054a538711d760008d1448.png)
Na vrchole zaťaženia na r/Place preniesla služba websocket viac ako 4 Gbps (150 Mbps na inštanciu, celkovo 24 inštancií).
Frontend: weboví a mobilní klienti
V procese vytvárania frontendu pre Place sme museli vyriešiť mnoho zložitých problémov súvisiacich s multiplatformovým vývojom. Chceli sme, aby projekt fungoval rovnako na všetkých hlavných platformách vrátane stolných počítačov a mobilných zariadení so systémom iOS a Android.
Používateľské rozhranie muselo vykonávať tri dôležité funkcie:
- Zobrazenie stavu dosky v reálnom čase.
- Umožnite používateľom interakciu s tabuľou.
- Pracujte na všetkých platformách vrátane mobilných aplikácií.
Hlavným objektom rozhrania bolo plátno a API Canvas bolo preň ideálne. Použili sme prvok
Kreslenie plátna
Plátno muselo odrážať stav dosky v reálnom čase. Po načítaní stránky bolo potrebné nakresliť celú nástenku a dokončiť kreslenie aktualizácií, ktoré prichádzajú cez websockety. Prvok plátna, ktorý používa rozhranie CanvasRenderingContext2D, možno aktualizovať tromi spôsobmi:
- Nakreslite existujúci obrázok na plátno pomocou funkcie drawImage() .
- Nakreslite tvary pomocou rôzne metódy kresliace formy. Napríklad fillRect() vyplní obdĺžnik nejakou farbou.
- Zostavte objekt ImageData a nakreslite ho na plátno pomocou putImageData() .
Prvá možnosť nám nevyhovovala, pretože sme nemali dosku vo forme hotového obrazu. Zostali teda možnosti 2 a 3. Najjednoduchším spôsobom bolo aktualizovať jednotlivé dlaždice pomocou fillRect() : keď aktualizácia príde cez websocket, jednoducho nakreslíme obdĺžnik 1x1 na pozíciu (x, y). Vo všeobecnosti metóda fungovala, ale nebola príliš vhodná na vykresľovanie počiatočný stav dosky. Metóda putImageData() bola oveľa lepšia: mohli sme určiť farbu každého pixelu v jednom objekte ImageData a nakresliť celé plátno naraz.
Kreslenie počiatočného stavu dosky
Použitie putImageData() vyžaduje definovanie stavu dosky ako Uint8ClampedArray , kde každá hodnota je osembitové číslo bez znamienka v rozsahu 0 až 255. Každá hodnota predstavuje farebný kanál (červený, zelený, modrý, alfa) a pixel vyžaduje štyri prvky v poli. Plátno 2x2 vyžaduje 16-bajtové pole, v ktorom prvé štyri bajty predstavujú ľavý horný pixel plátna a posledné štyri predstavujú pixel vpravo dole.
Tu je návod, ako sú pixely plátna spojené s ich reprezentáciami Uint8CampedArray:
![](https://i1.wp.com/habrastorage.org/files/5d9/738/4f0/5d97384f09ef4b648f3b5a97338b5199.png)
Pre plátno nášho projektu sme potrebovali pole štyroch miliónov bajtov – 4 MB.
V backende je stav dosky uložený ako štvorbitové bitové pole. Každá farba je reprezentovaná číslom od 0 do 15, čo nám umožnilo zabaliť dva pixely do každého bajtu. Ak to chcete použiť na klientskom zariadení, musíte urobiť tri veci:
- Preneste binárne dáta z nášho API klientovi.
- Rozbaľte dáta.
- Preveďte štvorbitové farby na 32-bitové.
Na prenos binárnych údajov sme použili rozhranie Fetch API v tých prehliadačoch, ktoré ho podporujú. A v tých, ktoré nepodporujú, sme použili XMLHttpRequest s responseType nastaveným na „arraybuffer“ .
Binárne dáta prijaté z API obsahujú dva pixely v každom byte. Najmenší konštruktor TypedArray, ktorý sme mali, vám umožňuje pracovať s binárnymi údajmi vo forme jednobajtových jednotiek. Na klientskych zariadeniach sa ale ťažko používajú, preto sme dáta rozbalili, aby sa s nimi ľahšie pracovalo. Proces je jednoduchý: iterovali sme zbalené dáta, vytiahli bity vyššieho a nižšieho rádu a potom ich skopírovali do jednotlivých bajtov do iného poľa.
Nakoniec bolo potrebné previesť štvorbitové farby na 32-bitové.
![](https://i1.wp.com/habrastorage.org/files/890/500/cb8/890500cb8f6b4dfd82ea1f708753935b.png)
Štruktúra ImageData, ktorú sme potrebovali na použitie putImageData(), to vyžaduje konečný výsledok bol vo forme Uint8ClampedArray s bajtami kódujúcimi farebné kanály v poradí RGBA. To znamená, že sme museli urobiť ďalšiu dekompresiu, rozdeliť každú farbu na bajty komponentného kanála a umiestniť ich do správneho indexu. Nie je veľmi vhodné vykonávať štyri zápisy na pixel. Ale našťastie tu bola aj iná možnosť.
Objekty TypedArray sú v podstate reprezentácie poľa ArrayBuffer. Je tu jedno upozornenie: viaceré inštancie TypedArray môžu čítať a zapisovať do rovnakej inštancie ArrayBuffer. Namiesto nahrávania štyri hodnoty v osembitovom poli môžeme zapísať jednu hodnotu do 32-bitového! Použitím Uint32Array na písanie sme boli schopní jednoducho aktualizovať farby dlaždíc jednoduchou aktualizáciou jedného indexu poľa. Museli sme však uložiť našu farebnú paletu v poradí veľkých bajtov (ABGR), aby sa bajty automaticky dostali na správne miesta pri čítaní pomocou Uint8ClampedArray .
![](https://i1.wp.com/habrastorage.org/files/f10/6bd/3d0/f106bd3d01b84a699a11d40a5904c6bd.png)
Spracovanie aktualizácií prijatých cez websocket
Metóda drawRect() bola dobrá na vykresľovanie aktualizácií jednotlivých pixelov tak, ako boli prijaté, ale mala jednu slabinu: veľké dávky aktualizácií prichádzajúce naraz by mohli viesť k spomaleniu prehliadačov. A pochopili sme, že aktualizácie stavu dosky môžu prichádzať veľmi často, takže problém bolo potrebné nejako vyriešiť.
Namiesto okamžitého prekresľovania plátna pri každom prijatí aktualizácie cez websocket sme sa rozhodli urobiť to tak, aby aktualizácie websocket, ktoré prichádzajú v rovnakom čase, mohli byť dávkované a vykresľované hromadne. Na dosiahnutie tohto cieľa boli vykonané dve zmeny:
- Prestať používať drawRect() - našli sme pohodlný spôsob aktualizovať veľa pixelov naraz pomocou putImageData() .
- Prenos vykresľovania plátna do cyklu requestAnimationFrame.
Presunutím vykresľovania do animačnej slučky sme boli schopní okamžite zapísať aktualizácie websocketu do ArrayBuffer a zároveň odložiť skutočné vykresľovanie. Všetky aktualizácie webovej zásuvky prichádzajúce medzi snímkami (približne 16 ms) boli dávkované a vykreslené súčasne. Vďaka použitiu requestAnimationFrame , ak by vykresľovanie trvalo príliš dlho (dlhšie ako 16 ms), malo by to vplyv iba na obnovovaciu frekvenciu plátna (a nie na zníženie výkonu celého prehliadača).
Interakcia s plátnom
Je dôležité poznamenať, že plátno bolo potrebné na uľahčenie interakcie používateľov so systémom. Hlavným scenárom interakcie je umiestnenie dlaždíc na plátno.
Urobiť presné vykreslenie každého pixelu v mierke 1:1 by však bolo mimoriadne náročné a nevyhli by sme sa chybám. Potrebovali sme teda (veľký!) zoom. Používatelia navyše potrebovali mať možnosť jednoducho sa pohybovať po plátne, pretože bolo príliš veľké pre väčšinu obrazoviek (najmä pri použití zoomu).
Zoom
Keďže používatelia mohli umiestňovať dlaždice raz za päť minút, chyby pri umiestňovaní by boli pre nich obzvlášť frustrujúce. Bolo potrebné implementovať zoom takého faktora, aby bola dlaždica dostatočne veľká a dala sa do nej ľahko umiestniť Správne miesto. Toto bolo obzvlášť dôležité na zariadeniach s dotykovou obrazovkou.
Implementovali sme 40x priblíženie, to znamená, že každá dlaždica mala veľkosť 40x40. Zabalili sme prvok