<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title><![CDATA[Programio]]></title>
    <link>http://programio.havrlant.cz/</link>
    <atom:link href="/rss.xml" rel="self" type="application/rss+xml"/>
    <description></description>
    <pubDate>Sun, 20 Nov 2016 15:42:06 GMT</pubDate>
    <generator>http://hexo.io/</generator>
    
    <item>
      <title><![CDATA[Netlify: hosting statických stránek]]></title>
      <link>http://programio.havrlant.cz/netlify/</link>
      <guid>http://programio.havrlant.cz/netlify/</guid>
      <pubDate>Sun, 20 Nov 2016 18:20:48 GMT</pubDate>
      <description>
      <![CDATA[<p>Na <a href="https://www.netlify.com/" target="_blank" rel="external">Netlify.com</a> jsem narazil nedávno, když jsem hledal, kde bych moh]]>
      </description>
      <content:encoded><![CDATA[<p>Na <a href="https://www.netlify.com/" target="_blank" rel="external">Netlify.com</a> jsem narazil nedávno, když jsem hledal, kde bych mohl hostovat své statické stránky, protože mi GitHub Pages přestávaly stačit. V článku vám zkusím popsat, co vám taková služba, která se zaměřuje výhradně jen na statické weby, může nabídnout a proč je to lepší než běžný hosting, <em>na který nahráváme obsah přes ef-té-pé</em>.</p>
<p>Abychom si ujasnili terminologii: statický web je web, pro jehož běh není potřeba žádný serverový jazyk jako třeba PHP. V podstatě se jedná o již vygenerované neměnné HTML stránky, jako se kdysi dělaly třeba ve FrontPage, blahé paměti. Proč byste v dnešní době neměli používat serverový jazyk, když ho nabízí i nejlevnější hosting za deset korun měsíčně? <a href="https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/" target="_blank" rel="external">Důvody si můžete přečíst třeba právě na Netlify</a> a tam si také můžete přečíst seznam <a href="https://www.netlify.com/blog/2016/05/02/top-ten-static-website-generators/" target="_blank" rel="external">deseti nejpoužívanějších nástrojů na vytváření statických webů</a>, o jednom z nich, Hexo, jsem i já napsal <a href="/hexoio/">sérii článků</a>.</p>
<p>Netlify je potom něco jako <a href="https://pages.github.com/" target="_blank" rel="external">GitHub Pages</a> na steroidech. Začátek je stejný:</p>
<ol style="list-style-type: decimal">
<li>Dostaňte kód webu do nějakého repozitáře (to už stejně máte, že?) na GitHubu nebo třeba na BitBucket.</li>
<li>Přihlašte se na <a href="https://www.netlify.com/" target="_blank" rel="external">Netlify</a>, vytvořte nový projekt a slinkujte ho se svým repozitářem na GitHubu/BitBucketu. V kroku “Configure …” nechte výchozí hodnoty.</li>
<li>There is no step three.</li>
</ol>
<p>V tuto chvíli se Netlify stáhne z repozitáře soubory, vytvoří nějakou náhodnou subdoménu <code>r-daneel-olivaw.netlify.com</code> a na této adrese máte dostupný svůj web. Ani to nebolelo, že? V administraci si můžete místo <code>r-daneel-olivaw</code> zvolit libovolné jiné jméno. V tuto chvíli kdykoliv pushnete do masteru nějakou změnu, automaticky se promítne na webu – každý váš push vyvolá nový deploy u Netlify. Ten je navíc atomický. Nestane se vám, jako u nahrávání obsahu přes FTP, že během nahrávání je web v nekonzistentním stavu, protože už se nahrály html soubory, ale obrázky ještě ne, například.</p>
<h2 id="ok-co-dál">Ok, co dál?</h2>
<ul>
<li><strong>Vlastní doména:</strong> toto je samozřejmost. Pokud máte vlastní doménu, můžete projekt u Netlify nastavit tak, aby se zobrazoval na této doméně, např. tento blog je na doméně <code>programio.havrlant.cz</code>, ale je hostován u Netlify. Postup si přečtěte v <a href="https://www.netlify.com/docs/custom-domains/" target="_blank" rel="external">manuálu</a>. Není to úplně triviální, protože musíte hrabat do nastavení DNS, což může skončit katastrofou, ale s tím se nedá nic dělat.</li>
<li><strong>HTTPS:</strong> Váš web by měl <em>samozřejmě</em> běžet na HTTPS, i když jde jen o statický web. Netlify využívá letsencrypt.org, což jednoduše řečeno pro vás znamená, že v administraci jednou kliknete na tlačítko a váš web běží na https, podruhé kliknete a http se automaticky přesměruje na https. Https funguje i na vlastních doménách, což třeba <a href="https://help.github.com/articles/securing-your-github-pages-site-with-https/" target="_blank" rel="external">GitHub pages aktuálně nepodporují</a>.</li>
<li><strong>Historie změn:</strong> Cože? Publikovali jste něco špatného a chcete se rychle vrátit k předchozí verzi? Není problém, v administraci se můžete vrátit se kterémukoliv staršímu deployi, který jste provedli. Nemusíte provádět žádný revert v repozitáři. Opět – jeden klik a máte na webu verzi z minulého měsíce. Dokonce se můžete podívat i na náhled, jak web v předchozí verzi vypadal.</li>
<li><strong><a href="https://www.netlify.com/docs/inject-analytics-snippets/" target="_blank" rel="external">Snippet injection</a>:</strong> Typicky když vyvíjíte, nepotřebujete mít v HTML měřicí skripty jako třeba Google Analytics, zatímco na webu je chcete. Netlify vám proto nabízí, že vám při publikaci “injectne” před <code>&lt;/body&gt;</code> nějaký vlastní kód.</li>
<li><strong>Vlastní build skripty:</strong> statický web musí něco vygenerovat, často se používají softy jako třeba zmíněné Hexo. Máte pak dvě možnosti, jak daný web publikovat: buď u sebe vygenerujete celý web a do repozitáře pushnete už vygenerované HTML, nebo do repozitáře pushnete zdrojové kódy (například v Markdownu) a necháte Netlify, aby web vybuildilo samo. Při zakládaní projektu máte možnost zapsat nějaký skript, který bude spuštěn při každé publikaci webu. Pokud používáte hexo, můžete tam napsat skript <code>hexo generate</code> a při každém pushi se na servrech Netlify spustí Hexo a až tam se vygeneruje finální podoba webu. GitHub pages mají něco podobného, akorát podporují pouze jeden “redakční systém”, <a href="https://help.github.com/categories/customizing-github-pages/" target="_blank" rel="external">Jekyll</a>. U Netlify můžete použít libovolný RS, klidně i vlastní. Můžete si napsat vlastní generátor a jako příkaz pro generování webu můžete napsat <code>npm install &amp;&amp; node app.js</code> a spustí se váš skript napsaný v NodeJS.</li>
</ul>
<p>Toto je výčet těch nejzákladnějších věcí, které asi použije skoro každý a Netlfiy je poskytuje i ve free verzi. Netlify toho umí samozřejmě <a href="https://www.netlify.com/docs/" target="_blank" rel="external">mnohem více</a>, jen stručně: <a href="https://www.netlify.com/docs/redirects/#custom-404" target="_blank" rel="external">vlastní 404 stránky</a>, <a href="https://www.netlify.com/docs/redirects/" target="_blank" rel="external">vlastní rewrity URL</a>, <a href="https://www.netlify.com/docs/password-protection/" target="_blank" rel="external">password protection</a>, <a href="https://www.netlify.com/docs/prerendering/" target="_blank" rel="external">prerendering pro single page aplikace</a>, <a href="https://www.netlify.com/docs/cli/" target="_blank" rel="external">CLI nástroj</a> a mnoho dalšího.</p>
<p>Co se týče trafficu, žádný limit jsem na webu nenašel a na můj dotaz mi odpověděli, že když přesáhnu 1 TB / měsíc, tak už po mně asi budou chtít, ať přejdu z free účtu na nějaký placený účet :-). Chcete-li příklad nějakého webu, který jede na Netlify, koukněte na <a href="https://lodash.com/" target="_blank" rel="external">lodash</a>, dokumentaci asi nejpoužívanější JS/NodeJS knihovny.</p>
<p>Máte-li tak nějaký statický obsah a nevíte-li, kam s ním, zkuste Netlify. Je to jednoduché a funguje to. Co víc chtít?</p>
<p><small>…a hlavně mi netvrďte, že existuje 150 dalších služeb, které tohle všechno nabízí :-P</small></p>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/netlify/#disqus_thread</comments>
    </item>
    
    <item>
      <title><![CDATA[Bloom filter]]></title>
      <link>http://programio.havrlant.cz/bloom-filter/</link>
      <guid>http://programio.havrlant.cz/bloom-filter/</guid>
      <pubDate>Mon, 05 Sep 2016 17:20:48 GMT</pubDate>
      <description>
      <![CDATA[<p>Bloom filter je pravděpodobnostní struktura, která nám umožňuje s jistou mírou pravděpodobnosti říci, zda se nějaký prvek <code>x</code> ]]>
      </description>
      <content:encoded><![CDATA[<p>Bloom filter je pravděpodobnostní struktura, která nám umožňuje s jistou mírou pravděpodobnosti říci, zda se nějaký prvek <code>x</code> nachází v množině <code>M</code>.</p>
<p>V našem systému například potřebujeme měřit, kolikrát uživatelé klikli na reklamní banner. Po každém kliknutí k nám přijde HTTP GET požadavek informující nás o tom, že uživatel XYZ klikl na reklamu 123. Občas se stane, že uživatel omylem myší dvojklikne a nám tak přijdou dva požadavky. My bychom rádi tyto požadavky odfiltrovali a měli je v databázi zaznamenané jako jeden klik.</p>
<p>Potřebujeme proto aplikaci, která bude číst kanál se zprávami o klicích a bude odfiltrovávat duplikace. Jak bychom to udělali? Každá taková zpráva má nějaké ID, nemusíme proto porovnávat celou zprávu, stačí nám zjistit, jestli jsme už někdy nezpracovali zprávu se stejným ID. Jednoduchý kód v JavaScriptu, který by odstraňoval duplikace ze streamu (zde pro jednoduchost z pole) zpráv by mohl vypadat takto:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> messages = [&#123;id: <span class="string">"123"</span>&#125;, &#123;id:<span class="string">"456"</span>&#125;, &#123;id:<span class="string">"123"</span>&#125;, &#123;id: <span class="string">"789"</span>&#125;]</span><br><span class="line"><span class="keyword">var</span> processedIds = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">messages.forEach(<span class="function"><span class="keyword">function</span>(<span class="params">message</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (!processedIds[message.id]) &#123;</span><br><span class="line">        processedIds[message.id] = <span class="literal">true</span>;</span><br><span class="line">        <span class="built_in">console</span>.log(message);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Vytiskne: &#123; id: "123" &#125; &#123; id: "456" &#125; &#123; id: "789" &#125;</span></span><br></pre></td></tr></table></figure>
<p>Toto řešení bude stačit do doby, než nám dojde paměť – a ta jednou dojde, protože množina <code>processedIds</code> bude časem jen a pouze růst. Můžeme samozřejmě doprogramovat nějakou expiraci dat – můžeme vždy odstranit zprávy starší než jedna hodina nebo něco podobného.</p>
<p>Horší je, když ani to nestačí. Co když potřebujeme mít v množině <code>processedIds</code> data za alespoň jeden den a procházíme třeba deset tisíc zpráv za sekundu? V nejhorším případě potřebujeme uložit až <a href="https://goo.gl/iOGIvz" target="_blank" rel="external">864 000 000</a> Idéček. Používáme-li <a href="https://github.com/broofa/node-uuid" target="_blank" rel="external">uuid v4</a>, má každé ID délku 36, což znamená, že za den potřebujeme uložit nejméně <a href="https://goo.gl/Bs6bR4" target="_blank" rel="external">32 GB</a>. To je trochu hodně. Nešla by ta paměťová náročnost snížit?</p>
<h2 id="bloom-filter">Bloom filter</h2>
<p>Místo toho, abychom si ukládali celé ID, můžeme si ukládat jen jeho hash. Můžeme použít klasickou <a href="https://github.com/perezd/node-murmurhash" target="_blank" rel="external">murmurhash</a>, která vrací pro každý string 32bitové celé číslo, navíc má dobré rozdělení. Dále použijeme fígl s bitovým vektorem. To je pole, do kterého budeme ukládat jen nuly a jedničky. Máme-li <code>n</code>-bitovou hashovací funkci, potřebujeme vektor o velikosti 2<sup>n</sup>, abychom byli schopni ve vektoru adresovat všechny možné výstupy hashovací funkce. Idea je, že na začátku obsahuje vektor samé nuly. Pokud nám pak hashovací funkce pro vstup <code>&quot;123&quot;</code> vrátí číslo 47, jednoduše do vektoru na index 47 vložíme jedničku. Tím si označíme, že hash o hodnotě 47 jsme už viděli.</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> n = <span class="number">8</span>;</span><br><span class="line"><span class="keyword">var</span> numberOfValues = <span class="built_in">Math</span>.pow(<span class="number">2</span>, n);</span><br><span class="line"><span class="keyword">var</span> messages = [&#123;id: <span class="string">"123"</span>&#125;, &#123;id:<span class="string">"456"</span>&#125;, &#123;id:<span class="string">"123"</span>&#125;, &#123;id: <span class="string">"789"</span>&#125;];</span><br><span class="line"><span class="keyword">var</span> bitVector = <span class="keyword">new</span> <span class="built_in">Array</span>(numberOfValues);</span><br><span class="line"></span><br><span class="line">messages.forEach(<span class="function"><span class="keyword">function</span>(<span class="params">message</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> hashedId = murmurhash.v2(message.id) % numberOfValues;</span><br><span class="line">    <span class="keyword">if</span> (!bitVector[hashedId]) &#123;</span><br><span class="line">        bitVector[hashedId] = <span class="literal">true</span>;</span><br><span class="line">        <span class="built_in">console</span>.log(message);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Vytiskne: &#123; id: "123" &#125; &#123; id: "456" &#125; &#123; id: "789" &#125;</span></span><br></pre></td></tr></table></figure>
<p>Tím kódem <code>% numberOfValues</code> jsme jen ořezali murmurhash tak, aby z ní byla <code>n</code>-bitová hashovací funkce, nic víc. A toto je Bloom filter. I když taková hodně degenerovaná verze. Problémem tohoto přístupu jsou pochopitelně kolize, které nutně nastanou. Různé vstupní hodnoty mohou mít stejný výstupní hash.</p>
<p>Abychom proto snížili pravděpodobnost, že dojde ke kolizi, nepoužijeme pouze jednu hashovací funkci, ale použijeme jich více. Více hashovacích funkcí můžeme vytvořit například tím, že k vstupní hodnotě přidáme nějakou sůl. Jednoduchá funkce, která by vytvářela různé <code>n</code>-bitové hashovací funkce by mohla vypadat takto:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> murmurhash = <span class="built_in">require</span>(<span class="string">"murmurhash"</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createNBitHashFunction</span>(<span class="params">n, salt</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> upperBound = <span class="built_in">Math</span>.pow(<span class="number">2</span>, n);</span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params">inputString</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> murmurhash.v2(inputString + salt) % upperBound;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> firstHashFunction = createNBitHashFunction(<span class="number">8</span>, <span class="string">"nejakaSul"</span>);</span><br><span class="line"><span class="keyword">var</span> secondHashFunction = createNBitHashFunction(<span class="number">8</span>, <span class="string">"nejakaJinaSul"</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">console</span>.log(firstHashFunction(<span class="string">"123"</span>)); <span class="comment">// 42</span></span><br><span class="line"><span class="built_in">console</span>.log(secondHashFunction(<span class="string">"123"</span>)); <span class="comment">// 174</span></span><br></pre></td></tr></table></figure>
<p>Tyto dvě hashovací funkce bychom využili tak, že bychom vstupní hodnut zahashovali oběma funkcemi a na oba vygenerované indexy bychom uložili jedničku, tj. pro vstup <code>&quot;123&quot;</code> bychom uložili do bitové vektoru jedničky na indexy 42 i na 174.</p>
<p>Při následném dotazu na to, zda jsme už string <code>&quot;123&quot;</code> viděli, ho opět zahashujeme stejnými funkcemi a opět nám vyjdou indexy 42 a 174. Podíváme se na hodnoty v bitovém vektoru a pokud je na jednom z indexů nula, string jsme si do množiny neuložili. Pokud tam budou dvě jedničky, tento string jsme <em>asi</em> do množiny uložili. Samozřejmě může nastat případ, že nějaký úplně jiný string se zahashoval na dvojici čísel 42 a 174, tj. nastala kolize. To se s Bloom filter může stát.</p>
<h3 id="algoritmus-bloom-filteru">Algoritmus Bloom filteru</h3>
<p>Algoritmus Bloom filteru bychom proto mohli napsat takto:</p>
<ul>
<li><strong>Inicializace:</strong>
<ol style="list-style-type: decimal">
<li>Zvolíme počet hashovacích funkcí a jejich velikost</li>
<li>Inicializujeme bitový vektor nulami, a to tak, abychom v něm mohli adresovat všechny hodnoty hashovací funkce.</li>
</ol></li>
<li><strong>Přidání prvku do pole:</strong>
<ol style="list-style-type: decimal">
<li>Vstupní hodnotu zahashujeme všemi hashovacími funkcemi a uložíme si do bitového vektoru na příslušná místa jedničku.</li>
</ol></li>
<li><strong>Kontrola existence prvku v množině:</strong>
<ol style="list-style-type: decimal">
<li>Hledanou hodnotu zahashujeme všemi hashovacími funkcemi</li>
<li>Pokud pro alespoň jeden výsledek hashovací funkce platí, že na daném indexu bitového pole je nula, hledaný prvek v množině není.</li>
<li>V opačném případě tam <em>asi</em> je.</li>
</ol></li>
</ul>
<h3 id="implementace-v-javascriptu">Implementace v JavaScriptu</h3>
<p>Třídu reprezentující Bloom filter můžeme napsat takto:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">BloomFilter</span>(<span class="params">numberOfHashFunctions, numberOfBits</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.bitVector = <span class="keyword">new</span> <span class="built_in">Array</span>(<span class="built_in">Math</span>.pow(<span class="number">2</span>, numberOfBits));</span><br><span class="line">    <span class="keyword">this</span>.hashFunctions = [];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; numberOfHashFunctions; i++) &#123;</span><br><span class="line">        <span class="keyword">this</span>.hashFunctions.push(createNBitHashFunction(numberOfBits, i.toString()));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">BloomFilter.prototype.add = <span class="function"><span class="keyword">function</span>(<span class="params">item</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> checksum;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; <span class="keyword">this</span>.hashFunctions.length; i++) &#123;</span><br><span class="line">        checksum = <span class="keyword">this</span>.hashFunctions[i](item);</span><br><span class="line">        <span class="keyword">this</span>.bitVector[checksum] = <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">BloomFilter.prototype.contains = <span class="function"><span class="keyword">function</span>(<span class="params">item</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">var</span> checksum;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; <span class="keyword">this</span>.hashFunctions.length; i++) &#123;</span><br><span class="line">        checksum = <span class="keyword">this</span>.hashFunctions[i](item);</span><br><span class="line">        <span class="keyword">if</span> (!<span class="keyword">this</span>.bitVector[checksum]) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<ul>
<li>Metoda <code>add</code> přidává prvek do Bloom filteru: spočítá hash všemi hashovacími funkce a na odpovídající index uloží <code>true</code>.</li>
<li>Metoda <code>contains</code> pak zjišťuje, jestli jedaný prvek obsažen v Bloom filteru.</li>
</ul>
<p>Bloom filter můžeme použít snadno:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> bloomFilter = <span class="keyword">new</span> BloomFilter(<span class="number">3</span>, <span class="number">8</span>);</span><br><span class="line"><span class="keyword">var</span> messages = [&#123;id: <span class="string">"123"</span>&#125;, &#123;id:<span class="string">"456"</span>&#125;, &#123;id:<span class="string">"123"</span>&#125;, &#123;id: <span class="string">"789"</span>&#125;];</span><br><span class="line"></span><br><span class="line">messages.forEach(<span class="function"><span class="keyword">function</span>(<span class="params">message</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (!bloomFilter.contains(message.id)) &#123;</span><br><span class="line">        bloomFilter.add(message.id);</span><br><span class="line">        <span class="built_in">console</span>.log(message);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Vytiskne: &#123; id: "123" &#125; &#123; id: "456" &#125; &#123; id: "789" &#125;</span></span><br></pre></td></tr></table></figure>
<p>A to je celá magie základního Bloom Filteru.</p>
<h2 id="vlastnosti-bloom-filteru">Vlastnosti Bloom filteru</h2>
<ul>
<li>Nejzásadnější vlastností je, že paměťová náročnost Bloom filteru je velmi nízká, o několik řádů nižší, než kdybychom ukládali všechna IDéčka. Potřebujeme uložit pouze bitový vektor, nic více.</li>
<li>Pokud metoda <code>contains</code> odpoví, že daný prvek v Bloom Filteru není, znamená, že jsme do něj daný prvek opravdu nikdy nevložili.</li>
<li>Pokud metoda <code>contains</code> odpoví, že daný prvek v Bloom Filteru je přítomen, nemusí to nutně znamenat, že jsme do něj daný prvek opravdu uložili – mohla totiž nastat kolize hashovacích funkcí.</li>
<li>Obecně platí, že chceme-li snížit pravděpodobnost nesprávné odpovědi, musíme zvýšit počet hashovacích funkcí nebo zvýšit jejich velikost (myšleno velikost výstupní hodnoty).</li>
<li>Čím více hashovacích funkcí použijeme, tím pomalejší Bloom Filter bude.</li>
<li>Čím větší hashovací funkce použijeme, tím více místa bude Bloom Filter zabírat.</li>
<li>Pokud jednou nějaký prvek do Bloom Filteru vložíme, už ho není možné odstranit.</li>
<li>Bloom filter nikdy “nenaplníme”, uložíme do něj nekonečně hodnot. Jenom nám bude neustále růst pravděpodobnost kolize.</li>
</ul>
<p>Existují varianty Bloom filteru, které například umožňují odstraňovat jednou přidané prvky. O těch si povíme příště.</p>
<h2 id="příklady-použití">Příklady použití</h2>
<p>Bloom filter je v praxi hojně používaný, viz <a href="https://en.wikipedia.org/wiki/Bloom_filter#Examples" target="_blank" rel="external">příklady na Wiki</a>.</p>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/bloom-filter/#disqus_thread</comments>
    </item>
    
    <item>
      <title><![CDATA[Čtyři nejtrapnější chyby, jaké jsme během vývoje udělali]]></title>
      <link>http://programio.havrlant.cz/chyby/</link>
      <guid>http://programio.havrlant.cz/chyby/</guid>
      <pubDate>Tue, 09 Feb 2016 17:07:19 GMT</pubDate>
      <description>
      <![CDATA[<p>Trvalo nám asi čtvrt roku, než jsme rozběhli celý systém založený na <a href="/kafka">Kafce</a>, <a href="/samza">Samze</a> a <a href="/d]]>
      </description>
      <content:encoded><![CDATA[<p>Trvalo nám asi čtvrt roku, než jsme rozběhli celý systém založený na <a href="/kafka">Kafce</a>, <a href="/samza">Samze</a> a <a href="/druid-io">Druidu</a> několik měsíců potom jsme ještě lovili a řešili další vzniklé bugy. Cestou jsme samozřejmě narazili na hromadu chyb a některé z nich byly tak blbé, že si měl člověk chuť vystřeli mozek z hlavy po jejich vyřešení. Obyčejně to byly chyby, které si chcete nechat pro sebe a rozhodně se s nimi nikde nechcete chlubit, nedej bože někde na veřejnosti. Tak přesně o těchto chybách bude tento článek.</p>
<h2 id="jak-jsme-konfigurovali-druida">Jak jsme konfigurovali Druida</h2>
<p>Nejblbější chyba jakou jsme kdy udělali ― chtěli jsme nastavit, aby <code>druid.announcer.type</code> bylo typu <code>batch</code>. Zní to dost jednoduše, takže jsme editovali soubor a vložili jsme tam příslušný řádek:</p>
<pre>druid.announcer.type=batch&nbsp;</pre>
<p>Restartovali jsme Druida a zdálo se, že to funguje. Po pár hodinách už to ale tak nevypadalo a Druid se choval divně. Googlíme příznaky a většina odpovědí se shoduje, že tyto příznaky jsou obvykle způsoby tím, že config <code>druid.announcer.type</code> není nastavený na <code>batch</code>. Kokuneme do configu a vidíme, že je. Po pár dalších zoufalých hodinách už zkoušíme přes ctrl+c a ctrl+f, jestli je to fakt do písmene stejné a ano, je.</p>
<p>Procházíme logy všech pěti Druidích nodů, koukáme se do ZooKeeper logu, hledáme, jesti chyba není v MySQL, kterou Druid používá. Co HDFS, nenaříká nějak? Ani ne, běží jako Forrest Gump. Jdeme na GitHub a koukáme přímo do zdrojáků Druidu, jestli tam něco nevyčteme. Vyčetli jsme, že by to mělo fungovat, když máme <code>druid.announcer.type</code> nastavený na <code>batch</code>.</p>
<p>Bystřejší, zkušenější a sečtělejší ví. Mezera na konci. Debilní mezera navíc na konci řádku za slovem <code>batch</code>, se kterou si aplikace neporadila a neodstranila ji. Změnili jsme config na</p>
<pre>druid.announcer.type=batch</pre>
<p>a najednou vše fungovalo jak mělo. Kravský <code>.properties</code> formát.</p>
<h2 id="jak-jsme-konfigurovali-cassandru">Jak jsme konfigurovali Cassandru</h2>
<p>Používáme Cassandru a jejího Node.JS klienta. V minulosti jsme narazili na docela dost výkonnostních problémů s touto kombinací, takže jsme vynaložili nemalé úsilí, abychom se dostali na nějaké rozumná čísla. Problémů jsme měli víc, některé klasické, sem tam byly chyby v kódu … a pak tam byla jedna zábavná chyba.</p>
<p>Abyste rozuměli, když vytváříte nového Cassandra klienta, děláte to cca takto:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> client = <span class="keyword">new</span> Client(&#123;</span><br><span class="line">	queryOptions: &#123;</span><br><span class="line">		consistency: types.consistencies.quorum</span><br><span class="line">	&#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>Třídě <code>Client</code> předáte konfigurační objekt, jednoduché, prostě a snadno použitelné. Nakonfigurovali jsme klienta jak jsme potřebovali, klient se rozběhl a fungoval.</p>
<p>Až za čas jsme jaksi zjistili, že některé z těch configů, které jsme tam vrazili, jsou až ve vyšší verzi Cassandra driveru (= měli jsme 2.1.<strong>1</strong> a vyžadovala se 2.1.<strong>2</strong>) a klient se nám neobtěžoval sdělit, že používáme nevalidní config (kluci prostě zmergili <a href="https://github.com/datastax/nodejs-driver/blob/v2.1.1/lib/client-options.js#L60" target="_blank" rel="external">default config a user config</a> a nazdar bazar). Ve výsledku jsme si tak několik týdnů mysleli, že máme driver nějak nakonfigurovaný a driver samotný danou konfiguraci vlastně vůbec nepodporoval. Tak jsme si updatli knihovnu a svět byl zase růžovější.</p>
<p>Takže vás pěkně prosím: validujte configy! (My je samzořejmě také nevalidujeme, lol.)</p>
<h2 id="jak-jsme-neodesílali-zprávy-do-kafky">Jak jsme neodesílali zprávy do Kafky</h2>
<p>To si takhle rozjedete Node.JS appku, která má něco zapisovat do Kafky, nastartujete ji a chystáte se do té Kafky něco odeslat. Jenže ouha, protože je to článek o našich chybách, tak to samozřejmě nejde. Dostáváte jedinou smysluplnou hlášku, něco jako <em>“Could not send a message, no Kafka broker available.”</em></p>
<p>Co teď? Prozkoumáme Kafku, jestli běží, koukneme do logu, všechno se zdá v pohodě. Cvičně se zkusíme připojit z daného stroje na Kafku telnetem, to jde. Stejně tak jsme schopni spustit konzolového producera a poslat do Kafky nějakou zprávu. OK, takže se serverem je všechno v pořádku, žádný firewall nám neutíná spojení, chyba musí být přímo v Node.JS aplikaci.</p>
<p>Střídavě spouštíme appku v debug módu a střídavě přidáváme různé logy do výpisu. Jsme docela rádi, že máme u sebe celý zdrojový kód, včetně všechn knihoven a že jsme schopni ho měnit. A co nevidíme. Nedaří se přeložit doména <code>zookeeper01</code>.</p>
<p>My když jsme totiž zkoušeli toho konzolového producera, tak jsme ho spouštěli cca takovým příkazem:</p>
<pre>bin/kafka-console-producer.sh --broker-list kafka01:9092 --topic test</pre>
<p>Jenomže naše knihovna potřebovala pro připojení Zookeeper, takže se napřed připojovala na Zookeeper, ale to se jí nepovedlo, protože neuměla přeložit doménu <code>zookeeper01</code>, neboť jsme ji zapomněli přidat do <code>/etc/hosts</code>. A ta kravská knihovna nám ani nikde nedala v logu vědět, v čem je vlastně problém.</p>
<p>Tady už jsme se skoro odhodlali prohrábnout se kódem knihovny a napsat pull request s opravou, ale kód byl tak složitý/nesrozumitelný, že jsme se na to vykašlali a na kontrolu domén jsme si napsali skript bokem.</p>
<h2 id="jak-jsme-nastavovali-samzu">Jak jsme nastavovali Samzu</h2>
<p>Měli jsme takový zábavný problém se <a href="/samza">Samzou</a>. Její úkol byl číst zprávy z Kafky, zpracovat je a poslat dále do Kafky. Vše fungovalo správně jak mělo, ale když jsme zkoušeli nějaké performance testy, tak se nám stávalo, že nám Samza při větší záteži ztrácela nějaká data. Po chvíli zkoumání jsme přišli na to, že je Samza neztrácí, že jen zkrátka čte tak polovinu zpráv a druhou polovinu nějak vynechává.</p>
<p>Začali jsme zkoumat, jestli náhodou nečteme jenom některé partitony. V Kafce máme standardně 24 partitionů, kterými tečou data, tak jestli se třeba nestalo, že čteme jen polovinu z nich. Tím to samozřejmě nebylo. Ještě chvíli zkoumáme Samzu a docházíme k závěru, že chyba taky může být v Kafce.</p>
<p>Vedle Samzy si ještě pustíme Konzolového konsumera a zkoušíme číst ta samá data, která čte Samza, a ukládáme je do souboru. V souboru jsou všechny zprávy, přesto Samza všechny zprávy nepřečetla. V Kafce tak chyba být nemůže, tam jsou všechna data, zpět do Samzy.</p>
<p>Přidáváme logy do Samza jobu, s každou přečtenou zprávou už logujeme tak deset řádků. Po pár minutách běhu Samzy mají logy klidně deset gigabajtů. Tady přestává stačit i <code>grep</code>; uvažujeme, že na jejich analýzu nasadíme Excel.</p>
<p>Pak se to někdy stane. Někdo vysloví spásnou myšlenku: <em>“jako by to Kafka Samze mazala přímo pod rukama”</em>. Hehe. Taky že jo - během nastavování Kafky o dva týdny dříve jsme měnili retenci, tj. jak dlouho si má Kafka nechat uložené zprávy na disku. To nastavení vypadalo přibližně takto:</p>
<pre>log.retention.bytes=100000000</pre>
<p>Ano, z nějakého mně neznámého důvodu se retence udává <em>v bajtech</em>. Nemůžete danou hodnotu zadat v nějaké rozumné jednotce, musíte ji zadat v bajtech. No a když si nedáte pozor, tak se může stát, že místo 100 GB nastavíte 100 MB. Co se tedy stalo? Kafka každou chvíli tikla, zjistila, že má na disku třeba 1 GB zpráv, přitom má nastavený limit na 100 MB a 90 % zpráv, které tam měla, smazala. Proto Samza dostávala jen část dat - zbytek už jí Kafka nemohla dát.</p>
<p>Konzolový konzumer měl vždy všechna data čistě proto, že zprávy četl mnohem rychleji než Samza a stihl je tak přečíst ještě předtím, než je Kafka stihla smazat.</p>
<h2 id="závěr">Závěr</h2>
<p>Pokud něco nefungovalo, měli jsme nejčastěji chybu v konfiguraci a často to byla dost trapná chyba. V minimumálním množství případů za to mohl náš kód. Z toho nám vyšlo, že bychom měli nějakým způsobem testovat configy.</p>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/chyby/#disqus_thread</comments>
    </item>
    
    <item>
      <title><![CDATA[Antipatterny Event sourcingu]]></title>
      <link>http://programio.havrlant.cz/event-sourcing-antipatterny/</link>
      <guid>http://programio.havrlant.cz/event-sourcing-antipatterny/</guid>
      <pubDate>Thu, 28 Jan 2016 18:45:19 GMT</pubDate>
      <description>
      <![CDATA[<p>V <a href="/architektura-event-sourcing">minulém článku jsem představil event sourcing</a> a popsal, jak ho u nás v práci používáme. Dnes]]>
      </description>
      <content:encoded><![CDATA[<p>V <a href="/architektura-event-sourcing">minulém článku jsem představil event sourcing</a> a popsal, jak ho u nás v práci používáme. Dnes se podíváme na to, jaké problémy nám přechod na event sourcing přinesl a jak jsme je vyřešili a co jsme si tak nějak označili jako anti-patterny, kterým je lepší se vyhnout.</p>
<h2 id="neidempotence">Neidempotence</h2>
<p>Od Událostí očekáváme, že jejich aplikace bude idempotentní. Idempotence znamená, že</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#8704;x&#8712;D(f): f(x)=f(f(x))</span><br></pre></td></tr></table></figure>
<p>Méně srozumitelně: pokud stejnou Událost aplikujeme na entitu vícekrát, nesmí to změnit stav entity. Nejlépe se to vysvětluje na příkladu: “zvyš mzdu programátora o deset tisíc” není idempotentí Událost, “nastav plat programátora na sto tisíc” je idempotentí Událost. Pokud první Událost aplikujeme pětkrát, zvýšili jsme mzdu programátora o padesát tisíc. U druhého typu Událostí to nehrozí.</p>
<p>Smyslem tohoto pravidla je umožnit zpracovat například posledních sto Událostí znova bez toho, aniž by to rozbilo celý systém a také se to lépe debuguje – zkrátka kouknete na Událost a hned víte, co uživatel nastavil a jaký je aktuální stav systému.</p>
<p>Stejně tak bychom nikdy neměli nic vyvozovat z počtu daných Událostí. Pokud bychom například chtěli uživateli zobrazovat, kolikrát se už pokusil změnit název Kampaně, nemůžeme to udělat tak, že po každé, když se zavolá metoda <code>applyCampaignNameSet</code> inkrementujeme proměnnou, protože se teoreticky mohlo stát, že se metoda <code>applyCampaignNameSet</code> zavolala dvakrát se stejnou Událostí a už bychom to zobrazovali špatně.</p>
<p>Místo toho tak musíme v takovém případě mít informaci o počtu Událostí přímo v datech dané Události. Vyprodukujeme-li třetí Událost <code>CampaignNameSet</code> pro danou Kampaň, uložíme přímo do Události, že je třetí v pořadí. Všechny View Buildery si pak mohou informaci o počtu Událostí daného typu přečíst přímo z Události.</p>
<h2 id="neznámé-události">Neznámé Události</h2>
<p>Občas se nám stalo, že jsme releasli novou verzi aplikace, která vyprodukovala Událost, kterou ostatní aplikace neznaly. Třeba jsme přidali kód, který automaticky hlídal konce kampaně a když tento konec nastal, vypálila se Událost <code>CampaignStateChanged</code>. Kvůli nějaké chybě se stalo, že tenhle kód šel do světa dříve než aktualizace View Builderů, takže když se potom o půlnoci vyprodukovaly první Události <code>CampaignStateChanged</code>, které některým Kampaním nastavily stav na “Finished”, celý zbytek systému byl těmito Událostmi zmaten, protože tuto Události neznal. Nabízí se v zásadě dvě řešení:</p>
<ul>
<li><p>Neznámé Události přeskočit. S tím je ten trabl, že ty překočené Události už se nikdy znova nezpracují. Jako by neexistovaly – pokud ručně neresetujeme View Builder, aby zpracoval všechny Události znova. Do té doby, než bychom resetovaly View Builder, by se tak daná Kampaň jevila jako neukončená celému systému, což jistě není správně.</p></li>
<li><p>Druhou možností je, že když aplikace narazí na Událost, kterou nezná, tak spadne nebo přinejmenším přestane číst další Události. To taky není úplně šťastné řešení. Představme si, že ta nová neznámá Událost je třeba <code>CampaignNameSet</code>. Takovému Jádru, které vybírá, jakou reklamu uživateli zobrazit, je název Kampaně úplně šumák, takže by bylo smutné, kdybychom kvůli této neznámé Události přestali číst další Události. Třeba hned v další Události někdo chtěl snížit cenu za proklik – a my bychom tuto změnu neaplikovali jenom proto, že se uživatel snažil přejmenovat Kampaň a Jádro nevědělo co s tím.</p></li>
</ul>
<p>Celá věc se komplikuje tím, že chceme být obecně schopni releasovat kdykoliv jakoukoliv část systému bez toho, aniž by to ohrozilo ostatní. Zatím je nicméně naše řešení takové, že aplikace spadne nebo se zastaví čtení dalších Událostí, pokud natrefí na Událost, kterou nezná a programátoři musí daný problém rychle vyřešit.</p>
<h2 id="propagace-událostí">Propagace Událostí</h2>
<p>Mějme Stránku, která má pět Sekcí, které zase mají ještě pět Sekcí. To je celkem třicet Sekcí pod jednou hlavní Stránkou. A teď co se má stát, když uživatel nastaví Stránce, že se tam nesmí zobrazovat pornografický obsah? Toto nastavení se typicky dědí i do sekcí. My samozřejmě vypálíme Událost <code>SiteRestrictionSet</code>, otázkou ale je, jestli máme také vypálit třicet Událostí <code>SectionRestrictionSet</code> pro každou Sekci zvlášť.</p>
<ul>
<li><p>Když to neuděláme a necháme jen tu jednu Událost <code>SiteRestrictionSet</code>, tak všechny aplikace, které reagují na tuto Událost, budou muset po svém rešit logiku dědění, tj. budou muset řešit problém “Já sice nemám nastaveou žádnou restrikci, ale můj prapředek ano”. Tuto logiku bychom museli více méně rozkopírovat všude, kde se vytváří nějaký model se Stránkami.</p></li>
<li><p>Když to uděláme a všem Sekcím řekneme pomocí Událost <code>SectionRestrictionSet</code>, že se jim změnilo nastavení, nemusíme už nikde nic zvláštního doprogramovávat, protože příslušné <code>applySectionRestrictionSet</code> metody už všude máme. Na druhou stranu to znamená, že místo jedné Události jich budeme mít třicet. Přitom existují vlastnosti, které se budou měnit daleko častěji a dynamičtěji než zrovna nastavení restrikcí.</p>
<ul>
<li>Předchozí problém s hromadou Událostí by teoreticky šel řešit tak, že dovolíme, aby jednu stejnou Událost mohlo zpracovat více entit. Zatím to máme tak, že jedna Událost patří vždy k jedné entitě. Mohli bychom upravit náš event framework tak, abychom mohli říct, že jedna Událost <code>SectionRestrictionSet</code> může “patřit” několika entitám zároveň.</li>
</ul></li>
</ul>
<p>Zatím nemáme jednotný způsob jak podobné případy řešit, někdy to vyřešíme propagací Událostí, někdy ne. Postupem času se ale asi více přikláníme k první možnosti a Události spíše propagujeme. Dá se to lépe zautomatizovat a množství Událostí nám zatím takový problém nedělá. Ale možná jednou začne.</p>
<p>Máte s Event sourcingem více zkušeností? Jak podobné problémy řešíte vy?</p>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/event-sourcing-antipatterny/#disqus_thread</comments>
    </item>
    
    <item>
      <title><![CDATA[Jak vypadá architektura našeho systému: Event sourcing a CQRS]]></title>
      <link>http://programio.havrlant.cz/architektura-event-sourcing/</link>
      <guid>http://programio.havrlant.cz/architektura-event-sourcing/</guid>
      <pubDate>Mon, 16 Nov 2015 17:07:19 GMT</pubDate>
      <description>
      <![CDATA[<p>Aktuálně vyvíjíme nový video ad server. Plus minus je to systém, pomocí kterého můžete zadávat reklamní kampaně, které se pak budou zobra]]>
      </description>
      <content:encoded><![CDATA[<p>Aktuálně vyvíjíme nový video ad server. Plus minus je to systém, pomocí kterého můžete zadávat reklamní kampaně, které se pak budou zobrazovat ve video přehrávačích. V článku popíši, jaké technologie jsme pro to použili a jak jsme je všechny zkombinovali do jednoho funkčního celku.</p>
<p>Celý příběh začíná ve webovém UI/administraci, což je single-page aplikace napsaná v Angularu (promiň, Dane). Web reaguje s backendem přes AJAX na základě <a href="http://martinfowler.com/bliki/CQRS.html" target="_blank" rel="external">vzoru CQRS</a> a backend samotný je postavený na základech <a href="http://martinfowler.com/eaaDev/EventSourcing.html" target="_blank" rel="external">Event Sourcingu</a>.</p>
<h2 id="commandy-a-queries">Commandy a Queries</h2>
<p>Veškerá interakce mezi webovým UI a backendem probíhá skrze Commandy a Queries:</p>
<ul>
<li><strong>Query</strong> je ajaxový HTTP GET požadavek, který UI vypálí, když potřebuje zjistit nějaké informace z backendu. Query nikdy nesmí změnit stav systému, jen vrátit data. Typická Query může být <em>Vrať mi seznam kampaní daného uživatele</em>.</li>
<li><strong>Command</strong> je HTTP POST (PUT…) požadavek, který se pokouší změnit stav systému. Typický Command je <em>Vytvoř novou kampaň</em> nebo <em>změň název kampaně</em>. Commandy nikdy nevrací žádná data, v odpovědi máme jen jednoduchý indikátor toho, jestli se Command podařilo zpracovat, nebo jestli nastala chyba.</li>
</ul>
<p>Některé důsledky předchozích dvou bodů:</p>
<h3 id="optimistic-ui">Optimistic UI</h3>
<p>Pokud vypálíme nějaký Command, předpokládáme, že uspěje. Přejmenuje-li uživatel kampaň, my vypálíme správný Command, ale v UI už všude ukazujeme nové jméno bez ohledu na to, jestli Command na backendu prošel, nebo neprošel. Říká se tomu <em>Optimistic UI updating</em>. Výsledkem je, že UI reaguje okamžitě na akce uživatele, rychlost odezvy backendu na to nemá vliv.</p>
<p>Pokud ale Command na backendu opravdu selže, musíme nějak zareagovat v UI, nemůžeme se celou dobu tvářit, že se název kampaně změnil, i když se ve skutečnosti na backendu nezměnil. Tohle je obecně těžký problém, protože to selhání může nastat třeba až za deset sekund a během té doby už uživatel mohl přejít na jinou stránku a mohl udělat několik dalších akcí. Nemáme to dobře vyřešené pro všechny případy a ještě s tím asi budeme bojovat.</p>
<p>Protože nemáme úplně ideálně vyřešené undo akce Commandů, snažíme se alespoň provádět co nejvíce validací přímo v UI, abychom na backend nepálili Commandy, které jistě neprojdou byznys logikou na backendu. Má-li být zadaná cena kladná, nedovolíme odeslat Command se zápornou cenou. Samozřejmě to ale nikdy nebude stoprocentní, když už nic, tak backend může spadnout.</p>
<h3 id="performance">Performance</h3>
<p>Protože Commandy nevrací žádná data a předpokládáme, že projdou, není potřeba, aby jejich zpracování bylo extrémně rychlé. To má za následek, že si při jejich zpracování můžeme dovolit provádět více činností a celkově nemusí být celý kód kolem zpracování Commandů optimalizovný na rychlost. Což je v naprostém protikladu se zpracováním Queries, na které naopak uživatel čeká. Query posíláme, když potřebujeme z backendu zjistit nějaké informace, proto se naopak snažíme, aby zpracování Queries bylo co nejrychlejší.</p>
<h3 id="generování-idéček">Generování IDéček</h3>
<p>Zajímavá situace nastane, když v UI vytváříme novou kampaň. Člověk by asi čekal, že když pošlu požadavek na vytvoření nové kampaně, backend mi v odpovědi vrátí ID této kampaně, abych ji mohl dále adresovat. Jenže naše Commandy nevrací žádná data. Vyřešili jsme to tak, že ID nově vzniklé kampaně se už posílá v samotném Commandu. V rámci POST požadavku na vytvořením nové kampaně pošleme i její ID. Používáme <a href="https://github.com/broofa/node-uuid" target="_blank" rel="external">UUID v4</a>, takže máme prakticky garantované, že toto vygenerované ID bude unikátní. Webové UI zkrátka vygeneruje nové UUID a pošle ho backendu, který případně může zkontrolovat, jestli už dané ID není použito.</p>
<h2 id="event-sourcing">Event sourcing</h2>
<p>Další buzzword, který máme na skladě, je <strong>event sourcing</strong>. Základní myšlenkou event sourcingu je, že hlavní databáze obsahuje historii všech změn, které uživatel v systému provedl a jedině od těchto změn se odvíjí stav systému. Historie změn = Single Source of Truth. Historie změn je už z principu neměnná, co se jednou stalo, nemůže se odestát. Přečte-li nějaká aplikace všechny změny, dostane aktuální stav systému. Těmto <em>změnám</em> pak říkáme <strong>Eventy</strong> neboli <strong>Události</strong>.</p>
<p>V praxi to u nás funguje takto: uživatel změní v administraci název kampaně, my vypálíme Command na backend, tam ověříme nějakou byznys logiku (dejme tomu jestli název není moc dlouhý) a pokud je vše v pořádku, vytvoříme Událost <code>CampaignNameSet</code>. Tuto Událost uložíme do <em>Event Store</em>, což máme aktuálně implementované jako kolekci v <a href="https://www.mongodb.org/" target="_blank" rel="external">Mongu</a> (v SQL světě by to byla prostě tabulka). V této kolekci máme uchované všechny Události, které kdy v našem systému nastaly. Událost je přitom jednoduchý JSON objekt, něco takového:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    eventType: "CampaignNameSet",</span><br><span class="line">    entityId: "cf3f128e-5051-47fe-a961-da3e55422258",</span><br><span class="line">    datetime: "2015-09-27T06:26:51.312Z",</span><br><span class="line">    data: &#123;</span><br><span class="line">        name: "Nový název kampaně"</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>Těch dat je tam ve skutečností více, ale to je teď jedno. A proč používáme zrovna Mongo? Potřebovali jsme databázi, která obstojně zvládá replikace a shardování a umí dobře pracovat s JSONem. Mongo to tehdy umělo asi nejlépe.</p>
<h2 id="instancování-entit-při-zpracování-commandů">Instancování entit při zpracování Commandů</h2>
<p>Při zpracování Commandu potřebujeme znát aktuální stav systému, abychom mohli vyhodnotit všechna pravidla. Při zpracování Commandu na přejmenování kampaně bychom mohli kontrolovat dvě pravidla: jestli není název moc dlouhý a jestli se liší od předchozího názvu – nemá smysl přejmenovávat kampaň na stejný název. Kód by mohl vypadat přibližně takhle:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Campaign.prototype.handleSetCampaignNameCommand = <span class="function"><span class="keyword">function</span>(<span class="params">command</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (command.data.name.length &gt; <span class="number">50</span>) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">"Campaign name is too long"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (command.data.name !== <span class="keyword">this</span>.name) &#123;</span><br><span class="line">        produceNewEvent(Events.CampaignNameSet, &#123;name: command.data.name&#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>V <code>command.data.name</code> máme nový název kampaně. Aby tento kód fungoval, je nutné, aby v době zpracování Commandu byl v <code>this.name</code> aktuální název kampaně. Před samotným zpracováním Commandu proto tzv. <em>instancujeme</em> entitu kampaně, na které se Command provádí. To znamená, že z Monga vytáhneme všechny Události, které se týkají dané kampaně a aplikujeme je na danou entitu. Aplikace Události není o nic složitější než předchozí handle metoda:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Campaign.prototype.applyCampaignNameSet = <span class="function"><span class="keyword">function</span>(<span class="params">event</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">this</span>.name = event.data.name;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>Pokud uživatel desetkrát přejmenoval danou kampaň, vyvolá se desetkrát metoda <em>applyCampaignNameSet</em> a desetkrát se přepíše hodnota <code>this.name</code>. Na konci zpracování ale budeme mít entitu v aktuálním stavu (taháme všechny Události, ne jen <code>CampaignNameSet</code>), což je to, co chceme.</p>
<p>V tuto chvíli můžeme začít zpracovávat command samotný, tzn. že v tuto chvíli zavoláme předchozí metodu <code>handleSetCampaignNameCommand</code>, ve které se už můžeme kvalifikovaně rozhodnout, jestli vypálíme Událost <code>CampaignNameSet</code> nebo jestli to nemá smysl.</p>
<p>V případě nutnosti si můžeme instancovat další entity. Toto instancování entit není zrovna nejrychlejší operace, ale můžeme využít faktu, který jsme uvedli výše – zpracování Commandů nemusí být superrychlé. V současnosti platí, že vytvoření aktuální instance entity = dotaz do Monga na Události pro tu danou entitu, ale až to bude moc pomalé, dá se to relativně jednoduše cachovat.</p>
<p>Má-li vzniknout Událost, musí to být na popud nějakého Commandu. Není možné, aby vznikla Událost bez Commandu.</p>
<h2 id="transakční-zpracování">Transakční zpracování</h2>
<p>Jeden Command může vyprodukovat více než jednu Událost. Příkladem může být třeba pausnutí kampaně. Pausne-li uživatel kampaň, vyvolá se pochopitelně Událost <em>CampaignPausedSet</em>, ale spolu s tím se vyvolá Událost <em>CampaignRunnableStateChanged</em>. <em>CampaignRunnableStateChanged</em> je Událost, která nám říká, jestli může kampaň běžet, nebo jestli je “něco špatně”.</p>
<p>Aby kampaň mohla běžet, musí mít nastaveno kdy a kde má běžet a nesmí být pausnutá. Pokud splňuje všechny tři parametry, pak je i <em>Runnable</em>, pokud jeden z těch parametrů změníme, už není <em>Runnable</em>. Tím, že vypalujeme Událost <em>CampaignRunnableStateChanged</em> si ulehčujeme práci, protože všude jinde už nám pak stačí reagovat na tuto Událost a nemusíme nikde jinde vyhodnocovat logiku, jestli má kampaň všechny potřebné vlastnosti.</p>
<p>Aby vše fungovalo jak má, je nutné, aby se buď vyprodukovaly obě Události, nebo ani jedna. Proto všechny Události vyprodukované jedním Commandem ukládáme jako jednu transakci, tj. jako jeden Mongo objekt.</p>
<h2 id="queries-a-view-buildery">Queries a View Buildery</h2>
<p>Zpracování Commandů sice nemusí být rychlé, ale zpracování Queries ano. Jak to řešíme? Držíme aktuální stav entit v Mongu v jiných kolekcích. Tyto kolekce jsou vytvářeny aplikacemi, které nazýváme <em>View Builder</em>. View Buildery čtou všechny Události a na některé z nich nějak po svém reagují; typicky aktualizují záznam v Mongu a vytvářejí specifické <em>View</em>. Důležité je, že nemáme předepsané, jak takové View má vypadat.</p>
<p>Každý View Builder může vytvářet View, které se hodí pro nějaký konkrétní specifický účel a dokonce je možné, abychom ve dvou různých Views měli prakticky stejná data jenom v jiné struktuře. Důležité je, aby View bylo optimalizované pro čtení. Příklad: máme <em>Campaigns</em> View, ve kterém máme uloženy všechny informace o všech kampaních (jeden dokument = jedna kampaň); včetně názvu. V jiném View máme zase uložená ID všech entit (kampaně, publishery, …) a jejich názvy, nic víc. Toto View používáme v reportech, protože tam máme na vstupu seznam IDéček a potřebujeme je rychle přeložit na lidská jména.</p>
<p>Kód View Builderů je opět velmi jednoduchý. Vlastně jen vytváříte <code>apply</code> metody těch Událostí, na které chcete reagovat:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">CampaignViewBuilder.prototype.applyCampaignNameSet = <span class="function"><span class="keyword">function</span>(<span class="params">event</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> mongo.updateDocument(event.entityId, &#123;name: event.data.name&#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">CampaignViewBuilder.prototype.applyCampaignCurrencySet = <span class="function"><span class="keyword">function</span>(<span class="params">event</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> mongo.updateDocument(event.entityId, &#123;name: event.data.currency&#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>A jak se View Builder dostane k novým Událostem? Řekli jsme si, že po zpracování Commandu se Událost pošle do Event Store. Kromě toho se ještě pošle na <em>Event Bus</em> a skrze něj se Událost dostane do všech View Builderů. Event Bus je implementovaný pomocí <a href="http://zeromq.org/" target="_blank" rel="external">ZeroMQ</a> (zkoušeli jsme i <a href="http://nanomsg.org/" target="_blank" rel="external">nanomsg</a>). ZeroMQ není nic extra složitého, je to jen jednoduchý způsob, jak dostat zprávu z jednoho místa na druhé. Kdybychom celý event sourcing implementovali dnes, asi bychom místo ZeroMQ použili <a href="/kafka/">Kafku</a>.</p>
<p>Naše typická Query je proto implementovaná tak, že se jen podívá do předzpracované Mongo kolekce, položí jednoduchý dotaz a vrátí výsledek.</p>
<h2 id="změna-view">Změna View</h2>
<p>Důležitou výhodou je, že View je jen jiný pohled na Události z Event Store. Což znamená, že když se nám současné View nelíbí, můžeme ho změnit. Stačí jen resetovat View Builder a nechat ho znova přečíst všechny Události a naše View můžeme vypadat úplně jinak. Příklad z praxe: zákazník si může v našem webovém UI vytvořit portfolio, což je vlastně web + podsekce. Může to vypadat třeba takto:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">idnes.cz&#10;    | Kultura&#10;    | Technet&#10;        | Web&#10;        | V&#283;da&#10;    | Ekonomika&#10;    | ...</span><br></pre></td></tr></table></figure>
<p>My jsme se tuto strukturu na poprvé snažili ve View uložit tak, jak ji vidíte. Tj. jeden dokument = celý web včetně všech podsekcí:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    name: "idnes.cz",</span><br><span class="line">    nodes: &#123;</span><br><span class="line">        kultura: &#123; name: "Kultura", nodes: &#123; ... &#125; &#125;,</span><br><span class="line">        technet: &#123; name: "Technet", nodes: &#123; web: &#123; ... &#125; ... &#125; &#125;,</span><br><span class="line">        ekonomika: &#123; name: "Ekonomika", nodes: &#123; ... &#125; &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>Jenomže časem se ukázalo, že je to blbost a že by bylo lepší uložit to stylem jeden dokument = jedna sekce s tím, že bychom v každém dokumentu měli uložené IDéčka podsekcí. Tj. takto:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123; name: "idnes.cz", id: "187c48ce", nodes: ["846ed763", "3c33863f", "106ba878"] &#125;</span><br><span class="line">&#123; name: "Kultura", id: "846ed763", nodes: [...] &#125;</span><br><span class="line">&#123; name: "Technet", id: "3c33863f", nodes: [...] &#125;</span><br><span class="line">&#123; name: "Ekonomika", id: "106ba878", nodes: [...] &#125;</span><br><span class="line">...</span><br></pre></td></tr></table></figure>
<p>Přepsali jsme <code>PortfolioViewBuilder</code>, aby jinak reagoval na Události týkající se sekcí, přepsali jsme Query, která vracela strukturu portfolio a to bylo vše. Kompletně změnit strukturu dat, ze kterých taháme informace o portfolio, byla práce na dva dny i s testy…</p>
<h2 id="intermezzo">Intermezzo</h2>
<p>V souvislosti s touto změnou jsme přemýšleli, jak efektivně uložit informaci o portfoliu tak, aby bylo možné jednoduchým dotazem vrátit všechny sekce. V předchozí struktuře totiž platí, že idnes.cz ví jen o svých přímých potomcích, tj. o sekcích “Kultura”, “Technet” a “Ekonomika”. Že existují podsekce “Web” a “Věda” zjistíme až z dokumentu “Technet”. Existuje jeden hezký postup, jak jedním dotazem vrátit všechny své potomky, nehledě na úroveň, viz <a href="http://docs.mongodb.org/manual/tutorial/model-tree-structures-with-nested-sets/" target="_blank" rel="external">Model Tree Structures with Nested Sets</a>. Celý princip je pochopitelný z obrázku, který si vypůjčím z odkazované dokumentace:</p>
<div class="figure">
<img src="/images/data-model-example-nested-set.png" alt="">

</div>
<p>Každý uzel stromu si očíslujeme (projdeme strom do hloubky a očíslujeme jak je vidět z obrázku) a když chceme najít všechny potomky uzlu <em>Programming</em>, nalezneme všechny uzly, které mají levé číslo větší než 2 a pravé číslo menší než 11. To je celé. Dobré, ne?</p>
<p>Ale my jsme to nepoužili, protože nám stačí vždy vrátit celý strom, nepotřebujeme nikdy vracet část podstromu.</p>
<h2 id="eventual-consistency">Eventual consistency</h2>
<p>Command je považován za úspěšně zpracovaný, pokud se podařilo všechny vygenerované Události uložit do Event Store. Ve chvíli, kdy Události jsou v Event Store, nejdou už nijak odstranit, nejdou změnit – zůstanou v systému na věky věků. A naopak – pokud se Událost do Event Store nedostane, jako by se nic nestalo.</p>
<p>Jenomže když se Událost dostane do Event Store, tak to ještě neznamená, že se tato změna projevila ve všech částech systému. Někde vedle existuje View Builder, který čte tyto Události a reaguje na ně. Command ale nemá jak zjistit, jestli už na danou Událost reagovaly všechny View Buildery. Proto se může stát, že uživatel přejmenuje kampaň, Command úspěšně projde, uživatel refreshne webové UI a uvidí starý název kampaně. <code>CampaignViewBuilder</code> zkrátka ještě nestihl přečíst <code>CampaignNameSet</code> Událost a uložit do View aktuální stav.</p>
<p>Obecně proto platí, že Query nemusí vrátit aktuální stav systému, který platil v době, kdy byla Query přijata na backendu. Query vrací stav systému, který je uložený ve View a ten může být zpožděný oproti opravdovému stavu. Pokud bychom <em>nějaký čas</em> neprodukovaly žádné Události, tak by se View <em>nakonec</em> do skutečného aktuálního stavu dostalo, až by všechny View Buildery přečetly a zpracovaly všechny Události. Proto se tomuto principu říká <em>Eventual consistency</em>.</p>
<p>Teoreticky bychom mohli zařídit, aby Query vrátila aktuální stav systému. Museli bychom ale Query implementovat tak, aby se nikdy nedotazovala View, ale aby vždy instancovala entity přímo z Event Store. Jenomže tím bychom se zbavili dvou výhod: bylo by to pomalejší a nemohli bychom si data přeskládat a předzpracovat pro konkrétní Query tak, jak zrovna potřebujeme. Celkově bychom tím řádově více zatěžovali Event Store a stal by se z něj ještě větší single point of failure.</p>
<p>Jedním z pricnipů CQRS je oddělení Read (=Queries) a Write (=Commandy) částí systému, takže by nebylo dobré je míchat.</p>
<h2 id="na-jaké-problémy-jsme-narazili">Na jaké problémy jsme narazili</h2>
<p>…si povíme zase příště v dalším článku.</p>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/architektura-event-sourcing/#disqus_thread</comments>
    </item>
    
    <item>
      <title><![CDATA[Jak vypadá naše vývojové prostředí]]></title>
      <link>http://programio.havrlant.cz/docker-vagrant/</link>
      <guid>http://programio.havrlant.cz/docker-vagrant/</guid>
      <pubDate>Mon, 15 Jun 2015 13:02:55 GMT</pubDate>
      <description>
      <![CDATA[<p>Někdy před rokem jsme se rozhodli – snad finálně! – vyřešit problém, že i když používáme multiplatformní technologie jako je Node.JS, tak]]>
      </description>
      <content:encoded><![CDATA[<p>Někdy před rokem jsme se rozhodli – snad finálně! – vyřešit problém, že i když používáme multiplatformní technologie jako je Node.JS, tak ve výsledku má každý vývojář mírně odlišný systém, což způsobuje, že kód, který je funkční na jednom počítači, není funkční na jiném. Cestou se na naše řešení nabalila i jiná pozitiva a o tom všem si v článku povíme.</p>
<p>Před rokem trpělo naše vývojové prostředí přibližně těmito neduhy:</p>
<ul>
<li><p>Stáhli jste si zdrojáky webového UI, rozjeli jste ho, ale nefungovalo vám, protože jste si zapomněli ručně stáhnout jednu chybějící knihovnu.</p></li>
<li><p>Po stažení se sice UI rozběhlo, ale moc toho nefungovalo, protože jste zapomněli spustit backendový server.</p></li>
<li><p>Spustili jste backend, ale nefungovaly statistiky, protože jste u sebe měli starou verzi backendu.</p></li>
<li><p>Napsali jste nějaký kód, vyzkoušeli jste ho, otestovali, commitli&amp;pushli a po chvíli vám přišel email z Jenkinsu, že neprošli unit testy, protože máte lokálně jinou verzi testovacího frameworku než je na Jenskinsu.</p></li>
</ul>
<p>Podobných chyb si jistě dokážete představit stovky.</p>
<h2 id="docker-to-the-rescue">Docker to the rescue!</h2>
<p><a href="http://www.docker.com/" target="_blank" rel="external">Docker</a> asi znáte, je teď kolem něj docela hype. Je to něco trochu podobného jako virtualizovaný OS. Pomocí Dockeru můžeme vytvořit <em>kontejner</em>, což je jakýsi sandbox, který je co možná nejvíce odstíněný od hostitelského OS. Tento kontejner si můžeme různě přizpůsobit a nainstalovat tam své aplikace. Z pohledu uživatele se pak Docker kontejner jeví jako samostatný operační systém se svým file systémem a se svými procesy, ale fakticky v rámci Dockeru žádný virtualizovaný OS neběží a vše zkrátka vykonává hostitelský OS.</p>
<p>Můžeme proto na jednom systému spustit třeba deset kontejnerů, v pěti můžeme provozovat Node.JS aplikace, v dalších třech Java aplikace a ve zbývajících můžeme mít třeba nějaký monitoring. Každý kontejner bude jinak nakonfigurovaný, budou tam běžet jiné procesy a přitom nemusíme virtualizovat deset OS. Hlavní výhoda je v úspoře prostředků. Představte si, že na jednom počítači skutečně virtualizujete deset OS.</p>
<p>Námi v současnosti vyvíjený produkt se skládá z několika desítek Node.JS projektů. Některé z nich jsou HTTP servery, další jsou ZeroMQ servery a zbytek jsou sdílené knihovny. Nevím, jestli se vlezeme do definice <a href="http://martinfowler.com/articles/microservices.html" target="_blank" rel="external">microservices</a>, nicméně platí, že při běžném vývoji nám běží několik desítek Docker kontejnerů.</p>
<p>Tím, že nám všechny aplikace běží v kontejnerech, získáváme jednotné prostředí pro všechny vývojáře. Už žádné rozdílné verze testovacího frameworku – pro testování používáme framework, který je nainstalovaný v kontejneru. Aplikace se spouští v Nodu, který je opět nainstalovaný v kontejneru. Všechny tyto závislosti se jednou nastaví v konfiguraci daného kontejneru a programátoři už do toho nehrabou.</p>
<p>Veškeré externí závislosti, které se nějak využívají pro běh našich aplikací, máme vždy ve stejných verzích, takže pokud se naše aplikace chová na dvou různých počítačích jinak, obvykle to znamená, že si někdo nesynchronizoval změny v kódu.</p>
<p>No jo, jenže Docker zatím běží pouze na Linuxu a my máme v týmu i Windowsáře a Macaře. Co s tím?</p>
<h2 id="vagrant-to-the-rescue">Vagrant to the rescue!</h2>
<p><a href="https://www.vagrantup.com/" target="_blank" rel="external">Vagrant</a> je jakýsi wrapper nad virtualizačními programy jako je VirtualBox nebo VMware apod. Umožňuje nám pomocí jednoduchého konfiguračního souboru nakonfigurovat běžící virtualizovaný OS. Vagrant jsme proto využili tak, že v něm virtualizujeme CentOS, ve kterém spouštíme naše Docker kontejnery. Ráno zkrátka přijdeme do práce, spustíme Vagrant, v něm se spustí kontejnery a jedeme. Všem vývojářům, nehledě na OS, běží všechny aplikace v identickém prostředí.</p>
<p>Další příjemný bonus je jednoduchost prvotní instalace. Pokud přijde do týmu nový člověk, jako první si nainstaluje samotný Vagrant, pak si stáhne konfigurační soubory z repozitáře definující naše vývojové prostředí a jedním příkazem <code>vagrant install</code> nainstaluje všechny aplikace potřebné pro vývoj současného produktu. Druhým příkazem <code>vagrant up</code> pak prostředí spustí. To je celé.</p>
<p>Pokud se během toho procesu nic nepokazí, má někdy po dvou hodinách funkční a běžící aplikaci. Může si v prohlížeči zobrazit naše UI psané v Angularu, tam si může zkusit založit novou kampaň, což odchytí backend – HTTP server, který přes ZeroMQ dá vědět ostatním částem systému o nové kampani. Ty přijmou zprávu a aktualizují nějaké údaje v Mongu, které nám také běží v jednom z kontejnerů. A tak dále.</p>
<p>Protože máme v Dockeru nainstalované i Mongo, což je naše hlavní databáze, <strong>má každý vývojář na svém počítači úplnou a nezávislou instalaci celého systému</strong>. Pokud například v UI naklikám novou kampaň, data o ní bude mít v Mongu jen ten daný vývojář a nebude tím otravovat ostatní.</p>
<p>Stejně snadno lze psát i integrační testy. Po restartu celého prostředí se všechny kontejnery dostanou do původního stavu, takže můžeme snadno napsat nějaké scénáře a otestovat, zda vše proběhlo jak mělo. Tyto integrační testy si pak může pustit kterýkoliv vývojář, když si například chce ověřit, že novou fíčurou nezničil nějakou starou.</p>
<h2 id="a-co-statistiky">A co statistiky?</h2>
<p>Z <a href="/tags/bigdata/">předchozích článků</a> víte, že pro statistiky používáme hromadu různých služeb, které jsou navíc napsané v Javě, takže jsou to docela žrouty paměti. Nakonec se ukázalo, že to zase tak velký problém nebyl a zprovoznili jsme všechny části našich statistik přes Docker kontejnery, takže je opravdu možné provozovat celý náš systém na svém počítači bez toho, aby jakákoliv součást byla sdílená. Stručně k jednotlivým službám:</p>
<ul>
<li><p><a href="/kafka/">Kafce</a> jsme pouze nastavili, aby nežrala tolik paměti přes <code>-Xmx</code> a <code>-Xms</code> parametry a všechny topicy vytváříme pro jednoduchost pouze s jednou partition a bez replikací.</p></li>
<li><p><a href="/samza/">Samzy</a> jsme se báli nejvíce, protože jsme standardně pouštěli přes Hadoop a že by se nám do Vagrantu chtělo instalovat ještě Hadoop, to se říci nedalo. Nicméně Samza lze pro testovací účely spustit jako obyčejný Java proces, což jsme využili a spouštíme ji právě tak.</p></li>
<li><p><a href="/druid-io">Druid</a> potřebuje k běhu hodně procesů a nějaké ty externí závislosti. Nakonec jsme ale žádnou závislost navíc neinstalovali. Druid je robustní, takže pokud nějaká služba neběží, Druid se snaží stále fungovat dále v omezených podmínkách. To nám pro testovací účely stačí. Segmetny lze buď neukládat, nebo je lze ukládat na lokální disk.</p></li>
</ul>
<p>V kontejnerech nám tedy běží light-weight verze statistik, která se ale chová identicky jako na ostrých serverech, což je přesně to, co potřebujeme.</p>
<h2 id="další-vychytávky">Další vychytávky</h2>
<ul>
<li><p>Všechny Node.JS aplikace se spouští pod <a href="https://github.com/isaacs/node-supervisor" target="_blank" rel="external">supervisorem</a>, který sleduje změny ve zdrojácích a při každé změně restartuje danou službu.</p></li>
<li><p>Jak jsem psal výše, celý náš systém se skládá z několika desítek běžících aplikací či knihoven. Jedním z problémů je, jak zařídit, aby se změna v jedné knihovně automaticky projevila ve všech aplikacích, které tuto knihovnu používají. V kontejnerech máme projekty nainstalované a prolinkované tak, aby se přesně toto dělo.</p></li>
<li><p>Protože díky Vagrantu máme všichni všechny zdrojáky na stejném místě, mohli jsme snadno napsat skripty, které třeba kontrolují, jestli jsme něco nezapomněli commitnout, které umí stáhnout všechny změny z repozitářů nebo které umí spustit unit testy pro danou aplikaci, nehledě na to, jestli je to webová aplikace, nebo serverová. Protože se tyto shell skripty vykonávají uvnitř Vagrantu, automaticky fungují všem, nehledě na OS.</p></li>
<li><p>Díky Dockeru si můžeme snadno zobrazit logy z každé aplikace, tail-efnout se aktuální logy nebo restartovat kontejner, pokud je zrovna nějaký nemocný.</p></li>
</ul>
<h2 id="na-jaké-problémy-jsme-narazili">Na jaké problémy jsme narazili</h2>
<ul>
<li><p>Dlouhodobě jsme měli největší problémy s DNS. Všechny naše projekty běží na nějaké doméně, takže třeba Mongo běží na <code>mongo.adserver.dev</code>, Ad Selector běží na <code>adselector.adserver.dev</code> a podobně. Jenomže aby to fungovalo, potřebujeme něco, co bude dané domény překládat. K tomu jsme používali třeba <a href="https://registry.hub.docker.com/u/skynetservices/skydns/" target="_blank" rel="external">SkyDNS</a>, což sice fungovalo, ale občas se stávalo, že SkyDNS nastartoval později než bylo potřeba a na několik prvních dotazů zkrátka nestihl odpovědět. Částečně to bylo způsobeno i vysokým zatížením systému, viz další bod.</p></li>
<li><p>Občas jsou problémy s výkonem, hlavně kvůli všem těm watcherům, které sledují zdrojáky. To jsme ze začátku dlouho ladili, než se to vyladilo k plné spokojenosti všech. Na Linuxech jsme nakonec skončili u <a href="http://docs.vagrantup.com/v2/synced-folders/nfs.html" target="_blank" rel="external">NFS</a>, se kterým je vše nejrychlejší a nevytěžuje to systém. Při běžném provozu mi celý Vagrant vytěžuje asi 40 % jednoho CPU a přibližně 4 GB RAM.</p></li>
<li><p>Nebylo úplně jednoduché zařídit, aby se ty desítky kontejnerů spustily ve správném pořadí, ve správném čase, až když běží jiná služba apod.</p></li>
<li><p>Přesměrování portů. Pokud nějaká aplikace běží v Dockeru a běží na nějakém portu, musíme zařídit, aby se ostatní mohli na tento port dostat. Chceme-li, ať se vidí jednotlivé kontejnery mezi sebou, stačí to uvést v Dockerfilu. My ale občas chceme poslat ze svého lokálního počítače dotaz na nějaký server běžící v kontejneru. Proto musí ještě nastavit, ať je port viditelný i z venku, ne jen z jiného kontejneru. No jo, jenže „z venku“ znamená ve Vagrantu, protože Docker kontejner běží ve Vagrantu. Proto musíme port přesměrovat ještě jednou, mezi naším lokálním strojem a Vagrantem. Jo, je to přesně takový chaos, jaký z tohoto odstavce máte pocit.</p></li>
<li><p>Co se týče operačních systémů, nejhorší zkušenosti máme s OS X, nejlepší s Linuxem. Windows je tak nějak mezi.</p></li>
</ul>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/docker-vagrant/#disqus_thread</comments>
    </item>
    
    <item>
      <title><![CDATA[Jak Druid.io agreguje data]]></title>
      <link>http://programio.havrlant.cz/druid-io-ingest/</link>
      <guid>http://programio.havrlant.cz/druid-io-ingest/</guid>
      <pubDate>Tue, 02 Jun 2015 18:38:39 GMT</pubDate>
      <description>
      <![CDATA[<p>Z <a href="/druid-io-architektura">minulého dílu</a> víme, že <a href="http://druid.io/" target="_blank" rel="external">Druid</a> typicky]]>
      </description>
      <content:encoded><![CDATA[<p>Z <a href="/druid-io-architektura">minulého dílu</a> víme, že <a href="http://druid.io/" target="_blank" rel="external">Druid</a> typicky čte zprávy z nějakého messaging systému, tvoří segmenty a tyto segmenty poté proplouvají různými částmi systému. Dnes se podíváme na to, jak vypadají data v jednotlivých segmentech.</p>
<h2 id="agregujeme">Agregujeme</h2>
<p>Druid je sloupcová databáze, pro ilustraci si můžeme zpracovaná data zobrazit jako tabulku. Pokud bychom Druidu poslali tyto dvě zprávy</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    "<span class="attribute">timestamp</span>": <span class="value"><span class="string">"2015-03-17T20:40:00Z"</span></span>,</span><br><span class="line">    "<span class="attribute">campaignId</span>": <span class="value"><span class="string">"336"</span></span>,</span><br><span class="line">    "<span class="attribute">web</span>": <span class="value"><span class="string">"csfd.cz"</span></span>,</span><br><span class="line">    "<span class="attribute">browser</span>": <span class="value"><span class="string">"Chrome"</span></span><br><span class="line"></span>&#125;</span><br><span class="line"></span><br><span class="line">&#123;</span><br><span class="line">    "<span class="attribute">timestamp</span>": <span class="value"><span class="string">"2015-03-17T20:40:02Z"</span></span>,</span><br><span class="line">    "<span class="attribute">campaignId</span>": <span class="value"><span class="string">"1344"</span></span>,</span><br><span class="line">    "<span class="attribute">web</span>": <span class="value"><span class="string">"idnes.cz"</span></span>,</span><br><span class="line">    "<span class="attribute">browser</span>": <span class="value"><span class="string">"Chrome"</span></span><br><span class="line"></span>&#125;</span><br></pre></td></tr></table></figure>
<p>v Druidu by se poskládala tato tabulka:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-17T20:40:00Z</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="even">
<td align="left">2015-03-17T20:40:02Z</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">Chrome</td>
</tr>
</tbody>
</table>
<p>Dosud nic světoborného. Zázraky by se začaly dít, až by Druid přečetl například tuto zprávu:</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    "<span class="attribute">timestamp</span>": <span class="value"><span class="string">"2015-03-17T20:40:27Z"</span></span>,</span><br><span class="line">    "<span class="attribute">campaignId</span>": <span class="value"><span class="string">"336"</span></span>,</span><br><span class="line">    "<span class="attribute">web</span>": <span class="value"><span class="string">"csfd.cz"</span></span>,</span><br><span class="line">    "<span class="attribute">browser</span>": <span class="value"><span class="string">"Chrome"</span></span><br><span class="line"></span>&#125;</span><br></pre></td></tr></table></figure>
<p>…protože ta se od úplně první zprávy liší pouze v čase (20:40:<strong>00</strong> vs 20:40:<strong>27</strong>), ostatní vlastnosti jsou stejné. Jsou to dvě stejné události, které pouze nastaly v jiný čas. Druid by samozřejmě mohl tuto zprávu uložit jako třetí řádek do tabulky</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-17T20:40:<strong>00</strong>Z</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="even">
<td align="left">2015-03-17T20:40:02Z</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="odd">
<td align="left">2015-03-17T20:40:<strong>27</strong>Z</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
</tbody>
</table>
<p>…ale efektivnější bude, když provede <em>agregaci</em> a jen si u prvního řádku uloží, že tato událost nastala dvakrát:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">impressions</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-17T20:00:00</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">2</td>
</tr>
<tr class="even">
<td align="left">2015-03-17T20:00:00</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">Chrome</td>
<td align="left">1</td>
</tr>
</tbody>
</table>
<p>Tyto tabulky reprezentují stejná data, u druhé jsme pouze ztratili informaci o tom, kdy která událost přesně nastala. Všimněte si, že ve druhé tabulce už máme v timestampu pouze hodinu, bez přesného času. První řádek tabulky říká, že v čase 20:00:00 až 20:59:59 nastaly pro kampaň 336 na ČSFD dvě imprese.</p>
<p>Druid při agregaci postupuje tak, že porovnává hodnoty ve všech sloupcích a pokud se zprávy liší pouze v čase, spojí tyto zprávy do jedné a za každou zprávu inkrementuje sloupec <em>impressions</em>. Název nově vzniklého sloupce specifikujeme ve specFilu:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">"aggregators": [</span><br><span class="line">    &#123;</span><br><span class="line">        "type": "count",</span><br><span class="line">        "name": "impressions" </span><br><span class="line">    &#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure>
<p>Tímto vytvoříme agregátor <code>count</code>, který se bude ukládat do sloupečku <em>impressions</em>. Druid zná tři druhy sloupců:</p>
<ul>
<li><strong>čas</strong> – reprezentuje, pro kterou hodinu jsou data platná. V příkladu jde o sloupec “timestamp”.</li>
<li><strong>dimenze</strong> – to jsou naše „syrová“ data, jsou to sloupce <code>campaignID</code>, <code>web</code> a <code>browser</code>.</li>
<li><strong>metrika</strong> – vypočtené hodnoty, sloupec <code>impressions</code>.</li>
</ul>
<h2 id="další-agregátory">Další agregátory</h2>
<p>Druid má podporu pro několik dalších <a href="http://druid.io/docs/0.7.1.1/Aggregations.html" target="_blank" rel="external">agregátorů</a>. Mohli bychom například přidat informaci o ceně za impresi; tabulka před agregací by vypadala takto:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">price</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-17T20:40:00Z</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">0.17</td>
</tr>
<tr class="even">
<td align="left">2015-03-17T20:40:02Z</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">Chrome</td>
<td align="left">0.19</td>
</tr>
<tr class="odd">
<td align="left">2015-03-17T20:40:27Z</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">0.43</td>
</tr>
</tbody>
</table>
<p>Podle toho, co dosud víme, by se žádné řádky nespojily v jeden, protože všechny řádky se liší přinejmenším v ceně. To je ale docela nešikovné. Proto můžeme přidat <code>doubleSum</code> agregátor, který zařídí, aby se během agregace hodnoty ve sloupcích sečetly:</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    "<span class="attribute">type</span>": <span class="value"><span class="string">"doubleSum"</span></span>,</span><br><span class="line">    "<span class="attribute">name</span>": <span class="value"><span class="string">"price"</span></span>,</span><br><span class="line">    "<span class="attribute">fieldName</span>": <span class="value"><span class="string">"price"</span></span><br><span class="line"></span>&#125;</span><br></pre></td></tr></table></figure>
<p>Vlastnost <code>fieldName</code> říká, z kterého sloupečku má hodnoty číst a vlastnost <code>name</code> do jakého sloupečku má Druid agregovaná data ukládat. Protože máme oba názvy stejné, budou se data ve sloupečku price přepisovat. Nyní už může dojít k agregaci. Druid ví, že má porovnávat pouze hodnoty ve sloupcích <code>campaignId</code>, <code>web</code> a <code>browser</code>, zatímco sloupec <code>price</code> má vždy jen sečíst. Proto spojí první a třetí řádek do jednoho:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">impressions</th>
<th align="left">price</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-17T20:00:00</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">2</td>
<td align="left">0.6</td>
</tr>
<tr class="even">
<td align="left">2015-03-17T20:00:00</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">Chrome</td>
<td align="left">1</td>
<td align="left">0.19</td>
</tr>
</tbody>
</table>
<p>Nyní už ve sloupci <code>price</code> nemáme cenu jedné imprese, ale součet cen všech impresí v daném časovém úseku, pod danou kampaní, na daném webu a s daným prohlížečem. Z takové agregované tabulky už bychom samozřejmě nikdy nezjistili, jaká byla cena za jednu impresi.</p>
<h2 id="unikátní-uživatelé">Unikátní uživatelé</h2>
<p>Co když do zprávy přidáme i informaci o tom, kterému uživateli se banner zobrazil? Data před agregací:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">userId</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-17T20:40:00Z</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">40BBF8</td>
</tr>
<tr class="even">
<td align="left">2015-03-17T20:40:27Z</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">9896AA</td>
</tr>
</tbody>
</table>
<p>Události nastaly ve stejnou hodinu, ale liší se v hodnotě <code>userId</code>. Druid by proto tyto dvě zprávy uložil do dvou řádků. To je ale dost prekérní situace, protože nyní agregace proběhne jen tehdy, týkají-li se dva řádky stejného uživatele, což nemusí nastat moc často.</p>
<p>V tuto chvíli se musíme zamyslet nad tím, na co data ve skutečnosti potřebujeme. Jde-li nám jen o statistické dotazy typu „kolik unikátních uživatelů vidělo reklamu XYZ na webu ZXY“, můžeme si pomoci tím, že použijeme pravděpodobností algoritmy. Ty nám z dostupných dat nespočítají přesný počet unikátních uživatelů, ale odhadnou počet unikátních uživatelů s nějakou chybou (typicky pod dvě procenta). To je většinou přijatelné. Výhodou pravděpodobnostních algoritmů pak je, že mají nižší paměťové nároky a jsou rychlejší.</p>
<p>Druid zná <a href="/hyperloglog">HyperLogLog</a>, což je v současné době nejlepší algoritmus pro odhadování unikátních hodnot v multimnožině. Přidáme do našeho specFilu nový agregátor <code>hyperUnique</code>:</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    "<span class="attribute">type</span>" : <span class="value"><span class="string">"hyperUnique"</span></span>, </span><br><span class="line">    "<span class="attribute">name</span>" : <span class="value"><span class="string">"uniqueUsers"</span></span>, </span><br><span class="line">    "<span class="attribute">fieldName</span>" : <span class="value"><span class="string">"userId"</span> </span><br><span class="line"></span>&#125;</span><br></pre></td></tr></table></figure>
<p>Abyste plně pochopili, co se teď bude dít, je nutné vědět jak funguje <a href="/hyperloglog">HyperLogLog</a> a hlavně jak funguje <a href="/sjednoceni-hyperloglogu">sjednocení hyperloglogu</a>. Stručně řečeno, HyperLogLog je algoritmus, který převádí hodnoty ve sloupci na vektory, například na [0, 0, 3, 0], z nichž lze později odhadnout počet unikátních hodnot v daném sloupci.</p>
<p>Velkou výhodou je, že máme-li dva vektory [0, 3, 0, 0] a [0, 2, 0, 0], které reprezentují dvě různé hodnoty <code>A</code> a <code>B</code>, můžeme reprezentovat množinu <code>{A, B}</code> tak, že vektory „sjednotíme“ (vypočítáme maximum po složkách):</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[0, 3, 0, 0]&#10;[0, 2, 0, 0]&#10;------------&#10;[0, 3, 0, 0]</span><br></pre></td></tr></table></figure>
<p>Druid při agregaci vypočte HLL vektor a ten uloží do tabulky do sloupce <code>uniqueUsers</code> a <strong>hodnotu <code>userId</code> zahodí</strong>. Před finální agregací bude tabulka vypadat takto:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">impressions</th>
<th align="left">uniqueUsers</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-17T20:00:00</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">1</td>
<td align="left">[0, 3, 0, 0]</td>
</tr>
<tr class="even">
<td align="left">2015-03-17T20:00:00</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">1</td>
<td align="left">[0, 2, 0, 0]</td>
</tr>
</tbody>
</table>
<p>Protože HLL vektory umíme <a href="/sjednoceni-hyperloglogu">sjednotit</a>, můžeme tyto dva řádky spojit do jednoho. Výsledná agregovaná tabulka by vypadala takto:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">impressions</th>
<th align="left">uniqueUsers</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-17T20:00:00</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">2</td>
<td align="left">[0, 3, 0, 0]</td>
</tr>
</tbody>
</table>
<p>Z těchto dat umíme vyčíst přesný počet impresí pro danou kampaň, daný web a daný prohlížeč za daný čas. Z daných dat také umíme získat přibližný počet unikátních uživatelů, kterým se tento banner zobrazil. Důležité je, že i když poroste počet zobrazení daného banneru, stále nám pro uložení stačí jeden řádek. Pokud by kampaň vyprodukovala 15 874 impresí u 6 098 unikátních uživatelů, mohl by řádek vypadat nějak takto:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">impressions</th>
<th align="left">uniqueUsers</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-17T20:00:00</td>
<td align="left">336</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">15 874</td>
<td align="left">[15, 17, 10, 14]</td>
</tr>
</tbody>
</table>
<p>Počet impresí máme v tabulce přesně, počet unikátů by nám Druid odhadl z vektoru <code>[15, 17, 10, 14]</code>. Přitom nikde nemáme uložená IDéčka uživatelů. Nepotřebujeme je.</p>
<p>Sloupec <code>uniqueUsers</code> představuje další metriku v naší tabulce.</p>
<h2 id="časová-granularita">Časová granularita</h2>
<p>Dosud jsme stále pracovali s tím, že se data agregují v rámci jedné hodiny. Jako většina věcí, i toto je v Druidu nastavitelné:</p>
<pre><code>&quot;granularitySpec&quot; : {
    &quot;type&quot; : &quot;uniform&quot;,
    &quot;segmentGranularity&quot; : &quot;day&quot;,
    &quot;queryGranularity&quot; : &quot;hour&quot;
}</code></pre>
<p>Teď se budeme zabývat vlastností <code>queryGranularity</code>, která nám udává, za jaký časový úsek se budou agregovat zprávy. Nastavíme-li hodinovou queryGranularity, pak se nám zprávy budou agregovat vždy v rámci jedné hodiny (14:00 až 14:59:59…), tzn., že za jeden den budeme mít pro každou kombinaci dat 24 různých řádků pro 24 hodin. Jeden příklad lepší než tisíc slov (A pro jednoduchost vynecháme sloupec userId, protože Hyperloglogu stejně nikdo nerozumíte):</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-20T18:50:00Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="even">
<td align="left">2015-03-20T18:51:09Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="odd">
<td align="left">2015-03-20T18:51:12Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="even">
<td align="left">2015-03-20T18:51:35Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="odd">
<td align="left">2015-03-20T18:51:35Z</td>
<td align="left">13650</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="even">
<td align="left">2015-03-20T18:51:35Z</td>
<td align="left">13650</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="odd">
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
</tr>
<tr class="even">
<td align="left">2015-03-20T19:14:27Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="odd">
<td align="left">2015-03-20T19:15:38Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="even">
<td align="left">2015-03-20T19:16:01Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
</tbody>
</table>
<p>tuto tabulku by Druid agregoval na tuto výslednou tabulku:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">Impressions</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-20T18:00:00Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">4</td>
</tr>
<tr class="even">
<td align="left">2015-03-20T18:00:00Z</td>
<td align="left">13650</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">2</td>
</tr>
<tr class="odd">
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
</tr>
<tr class="even">
<td align="left">2015-03-20T19:00:00Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">2</td>
</tr>
</tbody>
</table>
<p>Pokud bychom nastavili <code>queryGranularity</code> na <code>day</code>, vypadala by výsledná agregovaná tabulka takto:</p>
<table>
<thead>
<tr class="header">
<th align="left">timestamp</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">Impressions</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-03-20T00:00:00Z</td>
<td align="left">36708</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">6</td>
</tr>
<tr class="even">
<td align="left">2015-03-20T00:00:00Z</td>
<td align="left">13650</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
<td align="left">2</td>
</tr>
<tr class="odd">
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
</tr>
</tbody>
</table>
<p>Hodnota queryGranularity nám určuje největší možnou granularitu. Nastavíme-li queryGranularity na hodinu, nebudeme už z Druidu schopni vytáhnout data po minutách. Ale můžeme vytáhnout data po dnech, protože to je menší granularita.</p>
<p>Vlastnost segmentGranularity udává velikost jednoho <a href="/druid-io-architektura">segmentu</a>, což jsme řešili v předchozím článku. V jednom denním segmentu protože můžeme mít například 24 hodinových bloků dat.</p>
<h2 id="co-druid-ukládá">Co Druid ukládá</h2>
<p>Důležité je, že <strong>Druid ukládá pouze agregovaná data</strong>. Původní zprávy, ze kterých byla data vybudovaná, neukládá a není možné se k nim přes Druida nějak dostat. Pokud jednou uložíme data s hodinovou granularitou, nedokážeme z Druidu žádným způsobem vytáhnout počet impresí za čas 13:15 až 13:30, budeme moci vytáhnout pouze data za 13:00 až 14:00. Nemůžeme ani říci Druidu, aby přepočítal data z minulého týdne, protože jsme tam našli chybu – Druid surová data nemá a proto nemůže vše znova přepočítat.</p>
<p>Proto se často původní surová data nezahazují, ale ukládají se někam jinam, například do <a href="http://en.wikipedia.org/wiki/Apache_Hadoop#HDFS" target="_blank" rel="external">HDFS</a> nebo do <a href="http://aws.amazon.com/s3/" target="_blank" rel="external">Amazon S3</a>. Tím máme zaručeno, že když se něco v Druidu pokazí, že můžeme vzít data z HDFS a poslat je znovu do Druidu. Třeba když zjistíme, že by se nám vlastně hodila minutová granularita. Na přesun dat z Kafky do HDFS používáme <a href="https://github.com/linkedin/camus" target="_blank" rel="external">Camus</a>.</p>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/druid-io-ingest/#disqus_thread</comments>
    </item>
    
    <item>
      <title><![CDATA[Architektura Druid.io]]></title>
      <link>http://programio.havrlant.cz/druid-io-architektura/</link>
      <guid>http://programio.havrlant.cz/druid-io-architektura/</guid>
      <pubDate>Mon, 11 May 2015 16:42:56 GMT</pubDate>
      <description>
      <![CDATA[<p><a href="http://druid.io/" target="_blank" rel="external">Druid.io</a> je distribuovaná databáze napsaná v Javě, která se skládá z několi]]>
      </description>
      <content:encoded><![CDATA[<p><a href="http://druid.io/" target="_blank" rel="external">Druid.io</a> je distribuovaná databáze napsaná v Javě, která se skládá z několika částí. Těmto částem říkáme <em>uzly</em> (<em>nodes</em>) a jsou to separátní Java procesy. Základní instalace Druidu potřebuje alespoň čtyři uzly: Realtime, Historický, Koordinátor a Broker, přičemž každý z nich má jinou zodpovědnost. V článku si všechny čtyři uzly představíme.</p>
<h2 id="dostáváme-data-do-druidu">Dostáváme data do Druidu</h2>
<p>Není typické, aby aplikace, které generují data (například naše Node.JS HTTP servery) posílaly data přímo do Druidu. Místo toho využívají nějakého messaging systému, do kterého zprávy posílají, a odtud se poté dostanou až do Druidu. My používáme <a href="/kafka/">Kafku</a>.</p>
<p>Začnu s popisem starší verze Druidu, kterou už sice nepoužíváme, ale jednodušeji se vysvětluje. Vstupním bodem celého Druid clusteru je <a href="http://druid.io/docs/0.7.0/Realtime.html" target="_blank" rel="external">Realtime uzel</a>.</p>
<h2 id="realtime-uzel">Realtime uzel</h2>
<p>Realtime uzel je služba, která se stará o načítání zpráv z Kafky nebo z jiného messaging systému. Spouští se jako obyčejný Java proces:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java io.druid.cli.Main server realtime</span><br></pre></td></tr></table></figure>
<p>V praxi by tam ale byla ještě hromada dalších parametrů, kterými se teď nebudeme zatěžovat. Realtime uzel potřebuje na vstupu ještě <a href="http://druid.io/docs/0.7.0/Realtime-ingestion.html" target="_blank" rel="external">konfigurační soubor</a>, tzv. “specFile”. My si z něj ukážeme jen pár částí. Začneme tou, kde se definuje, odkud se budou číst data:</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    "<span class="attribute">type</span>": <span class="value"><span class="string">"kafka-0.8"</span></span>,</span><br><span class="line">    "<span class="attribute">feed</span>": <span class="value"><span class="string">"impressions"</span></span>,</span><br><span class="line">    "<span class="attribute">parser</span>": <span class="value">&#123;</span><br><span class="line">    	"<span class="attribute">data</span>": <span class="value">&#123;</span><br><span class="line">            "<span class="attribute">format</span>": <span class="value"><span class="string">"json"</span></span><br><span class="line">        </span>&#125;</span>,</span><br><span class="line">        "<span class="attribute">timestampSpec</span>": <span class="value">&#123;</span><br><span class="line">            "<span class="attribute">column</span>": <span class="value"><span class="string">"timestamp"</span></span>,</span><br><span class="line">            "<span class="attribute">format</span>": <span class="value"><span class="string">"iso"</span></span><br><span class="line">        </span>&#125;        </span><br><span class="line">    </span>&#125;</span><br><span class="line"></span>&#125;</span><br></pre></td></tr></table></figure>
<p>SpecFile říká, ať Realtime uzel čte zprávy z Kafka topicu <em>impressions</em> a ať očekává na vstupu JSON, který má vlastnost <em>timestamp</em>, ve kterém je uložen čas, kdy událost nastala. Toto je opravdu důležité – do Druidu lze dostat jen <em>události</em>, které nastaly v určitý čas a Druid potřebuje vědět kdy. Zprávy bez timestampu Druid zahodí.</p>
<!-- Tím řekneme Realtime uzlu, aby použil ovladače pro Kafka 0.8 a začal číst Kafka topic *impressions*. Vlastnost "data" říká, vstupní data očekáváme v JSON formátu. Vlastnost "timestampSpec" pak říká, že tyto JSONy budou mít v property "timestamp" časové razítko a to bude ve formátu ISO, tj. něco jako "2015-03-17T20:40:00Z". Schéma dat nikde nespecifikujeme, tj. nikde neříkáme, jaké property má JSON mít. 

Jen pozor na to, že když do topicu pošlete zprávu, která není JSON nebo nemá platné časové razítko v dané vlastnosti, tak Druid tuto zprávu **zahodí**. Když jsme Druida zkoušeli úplně poprvé, tak jsme tam prostě valili nějaké JSONy, které jsme měli po ruce, ale do Druidu se žádná data nedostala, protože neměla platné časové razítko. Validní vstup, který v našem případě reprezentuje zobrazení jednoho reklamního banneru, tj. impresi, by mohl vypadat třeba takto:


<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    "<span class="attribute">timestamp</span>": <span class="value"><span class="string">"2015-03-17T20:40:00Z"</span></span>,</span><br><span class="line">    "<span class="attribute">campaignId</span>": <span class="value"><span class="string">"336"</span></span>,</span><br><span class="line">    "<span class="attribute">web</span>": <span class="value"><span class="string">"csfd.cz"</span></span>,</span><br><span class="line">    "<span class="attribute">browser</span>": <span class="value"><span class="string">"Chrome"</span></span><br><span class="line"></span>&#125;</span><br></pre></td></tr></table></figure>
<p>–&gt;</p>
<p>Po spuštění začne uzel číst zprávy z topicu, zpracovávat je a ukládat je. V tuto chvíli jsou data dotazovatelná – mohli bychom poslat přes REST API dotaz na Realtime uzel a získat třeba počet zpracovaných impresí.</p>
<h2 id="segment">Segment</h2>
<p>Během čtení zpráv začne Realtime uzel vytvářet něco, čemu se říká <strong>segment</strong>. Segment je soubor, který obsahuje Druidem zpracovaná, indexovaná a agregovaná data za určitý čas. Zpracovaný segment už je dále neměnný, není ho možné updatovat.</p>
<p>Kolik dat bude v jednom segmentu obsaženo závisí na hodnotě segmentGranularity:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#34;granularitySpec&#34; : &#123;&#10;    &#34;type&#34; : &#34;uniform&#34;,&#10;    &#34;segmentGranularity&#34; : &#34;day&#34;,&#10;    &#34;queryGranularity&#34; : &#34;hour&#34;&#10;&#125;&#10;``` &#10;&#10;Druid by pro ka&#382;d&#253; nov&#253; den za&#269;al vytv&#225;&#345;et nov&#253; segment. Pro jednoduchost m&#367;&#382;eme nyn&#237; p&#345;edpokl&#225;dat, &#382;e m&#225;me-li denn&#237; segmenty, pak se pro ka&#382;d&#253; den vytvo&#345;&#237; jeden soubor, kter&#253; obsahuje v&#353;echna data z tohoto dne. Tento soubor se pak nakop&#237;ruje na n&#283;kter&#253; z Druid&#237;ch server&#367; a tento server zodpov&#237;d&#225; dotazy pro dan&#253; den.&#10;&#10;&#10;## &#381;ivotn&#237; cyklus segmentu&#10;&#10;Dejme tomu, &#382;e nastartujeme Realtime uzel v pond&#283;l&#237; v 11:15. Realtime uzel by za&#269;al okam&#382;it&#283; vytv&#225;&#345;et denn&#237; segment pro pond&#283;l&#237; a tento segment by zpracov&#225;val a&#382; do p&#367;lnoci. Po celou dobu by data za pond&#283;l&#237; byla na Realtime uzlu, ale byla by dotazovateln&#225; p&#345;es REST API. &#10;&#10;Dal&#353;&#237; zaj&#237;mav&#225; v&#283;c se za&#269;ne d&#237;t na konci dne, o p&#367;lnoci. Realtime uzel se za&#269;ne p&#345;ipravovat na dal&#353;&#237; segment. V 00:00 se vytvo&#345;&#237; dal&#353;&#237; segment a zpr&#225;vy z &#250;terka se zpracov&#225;vaj&#237; do tohoto nov&#233;ho &#250;tern&#237;ho segmentu.&#10;&#10;&#60;!---&#10;No jo, ale co kdy&#382; n&#225;m p&#345;ijde n&#283;jak&#225; zpr&#225;va pozd&#283;? Co kdy&#382; se n&#283;jak&#225; zpr&#225;va s &#269;asem 23:59:57 dostane k Druidu a&#382; v &#269;ase 00:01:12 dal&#353;&#237;ho dne? Druid umo&#382;&#328;uje nastavit &#269;as, po kter&#253; bude je&#353;t&#283; &#269;ekat na takto zpo&#382;d&#283;n&#233; zpr&#225;vy:</span><br></pre></td></tr></table></figure>
<p>“windowPeriod”: “PT10m” ```</p>
<p>Tímto bychom nastavili, že ještě deset minut po půlnoci bude čekat na zpožděné zprávy z předchozího dne. Zprávy s timestampem z pondělka by šly do pondělího segmentu, zprávy s timestampem z úterka do úterního. (Nezpomeňte, že každá zpráva si sama s sebou nese informaci o tom, kdy vznikla.)</p>
<p>Pondělní segment je pak považován za uzavřený a Druid už do něj žádnou další zprávu nepustí. Místo toho vytvoří <em>nový pondělní segment</em>. Dosud totiž Druid potřeboval mít pondělní data v takové struktuře, do které mohl snadno přidávat nově příchozí zprávy. Jenomže teď už je úterý a do pondělního segmentu žádná další data nepřijdou – proto Druid přechroustá všechny pondělní zprávy a vytvoří nové indexy. Celkově vytvoří novou strukturu, která bude optimalizována pro vyhledávání. –&gt;</p>
<p>Pondělní segment je v tuto chvíli považován za uzavřený a Druid už do něj žádnou další zprávu nevloží. Díky tomu může Druid pozměnit strukturu pondělního segmentu tak, aby data v něm byla optimalizována pro vyhledávání. Po uzavření pondělního segmentu proto Druid přeskládá všechny zprávy a vytvoří nové indexy, aby se v segmentu dalo rychle vyhledávat.</p>
<p>Tento upravený segment odešle do <em>deep storage</em>. Tím Druiďáci nazývají vzdálené úložiště, ve kterém jsou archivovány všechny segmenty. V současné době je podporováno <a href="http://hortonworks.com/hadoop/hdfs/">HDFS</a> a <a href="http://aws.amazon.com/s3/">Amazon S3</a>. Tohle je podstatná informace – Druid neobsahuje nic jako trvalé úložiště pro zpracované segmenty. Samotná data musíme uložit jinam. Na druhou stranu napojení na ostatní služby není nijak těžké. My používáme HDFS.</p>
<p>Metadata o novém segmentu uloží Druid do MySQL. Základní schéma vypadá takto:</p>
<div class="figure">
<img src="/images/druid/deepstorage.svg" alt="" />

</div>
<!--
## MySQL
 
Nyní se podíváme na to, jak spolu jednotlivé části Druidu komunikují. Protože Realtime uzel odeslal segment do deep storage, musí o tom dát vědět zbytku clusteru. To se děje pomocí dalších dvou externích závislostí: MySQL a ZooKeeper. Po vytvoření segmentu by Realtime uzel uložil metadata o segmentu do MySQL. Část dat z naší MySQL: 


| dataSource  | start                    | end                      | created_date             |
|-------------|--------------------------|--------------------------|--------------------------|
| ssp-auction | 2015-03-13T11:00:00.000Z | 2015-03-13T12:00:00.000Z | 2015-03-13T12:53:25.540Z |
| ssp-auction | 2015-03-13T12:00:00.000Z | 2015-03-13T13:00:00.000Z | 2015-03-13T14:04:42.668Z |
| ssp-auction | 2015-03-13T13:00:00.000Z | 2015-03-13T14:00:00.000Z | 2015-03-13T15:16:01.205Z |
| ssp-auction | 2015-03-13T14:00:00.000Z | 2015-03-13T15:00:00.000Z | 2015-03-13T16:10:22.088Z |
| ssp-auction | 2015-03-13T15:00:00.000Z | 2015-03-13T16:00:00.000Z | 2015-03-13T16:51:05.289Z |


Vidíme, že v databázi jsou uložené informace jako je čas vzniku segmentu a který interval segment obsluhuje. Kromě těch sloupečků je v databázi ještě některé další sloupečky jako `version` nebo `used`. Z databáze si můžeme všimnout, že vytvoření segmentu není úplně nenáročná činnost: V prvním řádku máme segment, který má data pro interval 11:00--12:00, přitom tento segment vznikl až v 12:53. Window period jsme měli na 15 minutách, takže Druidu trvalo nějakých 38 minut, než segment vytvořil, tj. než zkonvertoval původní strukturu do immutable struktury. 

V tuto chvíli je segment na dvou místech: na Realtime uzel a v deep storage. Protože Realtime uzel už začíná zpracovávat další segment, chce se toho předchozího zbavit. Pokud bychom v tuto chvíli totiž položili dotaz, který by dotazoval data z pondělí, stále by na to odpovídal Realtime uzel. 
-->
<h2 id="koordinátor-a-historický-uzel">Koordinátor a Historický uzel</h2>
<p>Nyní přichází na řadu další dva druidí uzly: <a href="http://druid.io/docs/0.7.0/Coordinator.html" target="_blank" rel="external">Koordinátor</a> a <a href="http://druid.io/docs/0.7.0/Historical.html" target="_blank" rel="external">Historický</a>. Oba uzly jsou zase jen další Java procesy spuštěné na serverech. Historický uzel nezpracovává žádná nová data, jen <strong>přebírá segmenty, které vytvořil Realtime uzel.</strong></p>
<p>Koordinátor řídí přiřazování segmentů. Máme-li v clusteru deset Historických uzlů, musíme se rozhodnout, který z nich bude servírovat náš pondělní segment. Koordinátor je ten, kdo má v tomto konečné slovo. Proto Koordinátor sleduje stav clusteru a když zjistí, že přibyl nový segment, nalezne vhodný Historický uzel a přikáže mu, aby si nový segment stáhl z deep storage. Segmenty se nikdy nepřesouvají mezi Druidími uzly přímo, vždy jen přes deep storage.</p>
<p>Pomůžeme si dalším obrázkem:</p>
<div class="figure">
<img src="/images/druid/druidarch.svg" alt="">

</div>
<p>Když Historický uzel stáhne segment, oznámí to clusteru a všechny dotazy na data, která jsou uložena v tomto segmentu, půjdou na tento server. Za normálních okolností platí, že všechny dosud vytvořené segmenty jsou buď na Historických uzlech, nebo na Realtime uzlech.</p>
<p>Časová posloupnost akcí při vytváření segmentu by mohla vypadat takto:</p>
<ul>
<li>úterý 00:00: Realtime uzel začne vytvářet úterní segment. Oznámí clusteru, že všechny dotazy na úterní data mají jít k němu. Dále uzavírá pondělní segment a začíná vytvářet nové indexy pro pondělní segment.</li>
<li>01:30 Kompilace pondělního segmentu dokončena, Realtime uzel začíná nahrávat segment do HDFS.</li>
<li>01:50 Nahrávání dokončeno, Realtime oznamuje clusteru, že vytvořil nový segment.</li>
<li>01:51 Koordinátor přikáže některému Historickému uzlu, aby si k sobě stáhnul pondělní segment.</li>
<li>02:08 Historický uzel dokončil stahování segmentu a informuje cluster o tom, že bude odpovídat na všechny dotazy za pondělek.</li>
<li>02:09 Realtime uzel maže pondělní segment a oznamuje clusteru, že už dále nebude odpovídat na dotazy na pondělní data.</li>
</ul>
<p>Teď ale máme data za pondělí na Historickém uzlu a data za úterý na Realtime uzlu. Co když chceme znát součet impresí za oba dny současně? Musíme se zeptat obou serverů. Naštěstí to ale nemusíme dělat ručně, Druid nám poskytuje čtvrtý typ uzlu, který právě řeší dotazování.</p>
<h2 id="broker-uzel">Broker uzel</h2>
<p>Všechny dotazy posíláme <a href="http://druid.io/docs/0.7.0/Broker.html" target="_blank" rel="external">Brokeru</a>. Broker je další uzel Druidu, který za nás řeší dvě věci:</p>
<ul>
<li>Ptáme-li se na nějaká data, Broker zjistí, které segmenty tato data obsahují a které uzly tyto segmenty servírují.</li>
<li>Broker přepošle dotaz všem uzlům, které servírují potřebné segmenty a čeká na odpovědi. Tyto odpovědi následně spojí do jedné odpovědi (například sečte počty impresí za pondělí a za úterý) a odešle tuto odpověď zpátky klientovi.</li>
</ul>
<p>Broker uzel nám jako klientům zajišťuje transparentnost – nemusí nás zajímat, na kterých serverech jsou uložena naše data, jestli je servíruje Realtime uzel nebo Historický uzel. Broker dále cachuje výsledky z Historických uzlů, čímž se výrazně zlepšuje výkonnost celého clusteru. Historické uzly obsahují immutable segmenty, takže pro dva stejné dotazy vrátí vždy stejné výsledky. Cachovat data z Realtime uzlu si nemůže dovolit, protože tam se data typicky mění každou sekundu.</p>
<div class="figure">
<img src="/images/druid/druidarch2.svg" alt="">

</div>
<h2 id="jak-výpadek-jedné-ze-služeb-ovlivní-chod-clusteru">Jak výpadek jedné ze služeb ovlivní chod clusteru</h2>
<h3 id="výpadky-realtime-uzlu">Výpadky Realtime uzlu</h3>
<p>Na Realtime uzel jsou kladeny největší nároky – musí zpracovávat příchozí zprávy, zároveň je musí poskytovat k dotazování a k tomu všemu musí být nějak odolný vůči výpadkům.</p>
<ul>
<li><p>První přístup byl takový, že si Realtime uzel průběžně ukládal všechna data, která zpracovával, na disk. Po pádu Realtime uzlu a jeho restartu si mohl načíst již zpracovaná data z disku a mohl pokračovat dále tam, kde přestal. Toto ale zdaleka není stoprocentní řešení – mohlo se stát, že se nějaké zprávy zduplikují, ale to zase tak moc nevadí, protože <a href="/lambda-architecture">lambda architektura</a>. Jenomže pokud by shořel celý server, na kterém Realtime uzel běží, tak bychom přišli o všechna data. Navíc ve chvíli, kdy neběží Realtime uzel, třeba i jen minutu, nemáme přístup k datům, které už zpracoval.</p></li>
<li><p>Proto se začaly ručně vytvářet repliky Realtime uzlů. To se bohužel muselo trochu nahackovat přes Kafka consumer group ID – spustily se dva Realtime uzly a každému se nastavilo jiné group ID, viz <a href="/kafka-consumer/">Jak funguje Kafka consumer</a>. Jenomže to způsobilo <a href="https://groups.google.com/forum/#!searchin/druid-development/realtime$20uzel$20kafka$20replication/druid-development/LRFr2Dzw5Po/wAnCtm4FmrAJ" target="_blank" rel="external">další problémy</a> a celkově je to víc hack než řešení. Navíc už není jednoduché jen tak přivézt k životu jednou spadlý Realtime uzel. Máme-li dva Realtime uzly, po pádu jednoho z nich a po jeho znovu nastartování získáme nekonzistentní stav clusteru, protože uzel, který spadl, obsahuje třeba hodinu stará data, zatímco uzel, který celou dobu běžel, má aktuální data.</p></li>
<li><p>To se nakonec vyřešilo tím, že se <a href="https://groups.google.com/forum/#!searchin/druid-development/realtime$20uzel$20kafka$20replica/druid-development/aRMmNHQGdhI/HIB9kRf_6CwJ" target="_blank" rel="external">přešlo od pull-based řešení k push-based řešení</a>. V novějších verzích se proto typicky data do Druidu posílají, místo toho, aby si je Druid sám tahal z Kafky. O tom se ale budeme víc bavit někdy příště. Tento přístup zajistil, že replikace je už snadno konfigurovatelná věc. Můžeme nastavit, že chceme, aby se Realtime uzel třikrát replikoval. Spadne-li jedna replika, automaticky se obnoví až s dalším segmentem – nenastává problém s tím, že bychom získali hodinu stará data.</p></li>
</ul>
<h3 id="výpadky-zbylých-částí-clusteru">Výpadky zbylých částí clusteru</h3>
<ul>
<li><p>Když vypadne jeden <strong>Broker</strong>, můžeme se zeptat druhého.</p></li>
<li><p>Když vypadne <strong>Historický uzel</strong>, převezme jeho práci jiný Historický uzel. V Druidu můžeme nastavit replikaci Historických uzlů, takže pokud ji nastavíme na 2, po výpadku jednoho Historického uzlu se nestane nic – v clusteru bude vždy existovat ještě jeden uzel, který obsahuje dané segmenty. Pokud by vypadly všechny repliky pro daný segment, musel by se segment časem stáhnout na jiný Historický uzel. Do té doby, než by se segment stáhl, by byla data nedotazovatelná, ale zbytek funkčnosti clusteru by to neovlivnilo.</p></li>
<li><p>Když vypadne <strong>Koordinátor</strong> a neexistuje jeho replika, tak se zmrazí stav clusteru. V tuto chvíli není možné, aby se segment z Realtime uzlu dostal na Historický uzel, protože Historickému uzlu nemá kdo říci, aby si stáhl nový segment. Nicméně segment, který zůstal na Realtime uzlu je nadále dotazovatelný. Problém by nastal až ve chvíli, kdy by na Realtime uzlu docházelo místo nebo paměť.</p></li>
<li><p>Když vypadne <strong>MySQL</strong>, tak je to podobné, jako když vypadne Koordinátor.</p></li>
</ul>
<h2 id="horizontální-škálovatelnost">Horizontální škálovatelnost</h2>
<h3 id="realtime-uzel-1">Realtime uzel</h3>
<p>Ve starém přístupu jsme mohli vytvořit tolik <strong>Realtime uzlů</strong>, kolik partitionů obsahoval náš Kafka topic. V novém push-based přístupu už je to zcela nezávislé a můžeme si sami nastavit, kolik Realtime uzlů má zpracovávat vstupní zprávy. Můžeme nastavit, že naše zprávy budou zpracovávány třemi shardy a replikaci můžeme nastavit na dva. Tím dosáhneme toho, že se automaticky vytvoří celkem šest Realtime uzlů, které bude zpracovávat naše data. Vstupní zprávy se rozdělí na třetiny a každá třetina je poslána jiné dvojice uzlů.</p>
<p>Pro příklad předpokládejme, že máme devět zpráv, očíslované od jedné do devíti. Každý ze tří shardů bude zpracovávat “každou třetí zprávu”:</p>
<div class="figure">
<img src="/images/druid/DruidReplikace.svg" alt="">

</div>
<p>Potřebujeme-li se dotázat na aktuální data, musíme se vždy zeptat tří Realtime uzlů. Vypadne-li Realtime #3, zastoupí ho #4. Vypadne-li i ten, jsou zprávy ze Shardu #2 nedostupné. Realtime uzly #3 a #4 by se automaticky znova nastartovaly s novým segmentem (tj. v našem příkladě se začátkem nového dne).</p>
<h3 id="ostatní-uzly">Ostatní uzly</h3>
<ul>
<li><p><strong>Koordinátor</strong> horizontálně škálovatelný není. Máme-li více Koordinátorů, zvolí se mezi nimi leader a ten koordinuje cluster. Koordinátor nicméně vykonává nenáročnou činnost, jeden aktivní uzel by měl bohatě stačit.</p></li>
<li><p><strong>Historické uzly</strong> jsou sobě zcela nezávislé a můžeme jich mít kolik chceme. Každý Historický uzel zkrátka servíruje určitý počet segmentů, které jsou navíc immutable, a Broker ví o tom, který segment je uložený na kterém Historickém serveru. Můžeme mít klidně pro každý segment, tj. pro každá denní data, vlastní Historický uzel.</p></li>
<li><p><strong>Broker</strong> je opět nezávislý uzel, který nepotřebuje vědět o ostatních Brokerech. Broker jen přijímá dotazy a rozesílá je na Historické a Realtime uzly. Můžeme jich mít kolik chceme a v aplikaci pak střídavě rozesílat dotazy na různé Brokery.</p></li>
</ul>
<p>Celkově vzato je Druid velmi horizontálně i vertikálně škálovatelný. Můžeme proto postavit větší cluster, než jaký jsme viděli na posledním obrázku. Pokud bychom nechali počet Realtime uzlů a jen zvětšili počet Historických uzlů, mohl by dotaz „vrať mi součet impresí za uplynulý týden“ položený v neděli dopadnout takto:</p>
<div class="figure">
<img src="/images/druid/DruidCluster.svg" alt="">

</div>
<p>Broker se za nás zeptá Realtime uzlu z každého shardu a zároveň se zeptá všech Historických uzlů, které zrovna obsahují data, která potřebujeme. Počká na všechny odpovědi, ty spojí a vrátí uživateli.</p>
<p>A jaký je <a href="https://groups.google.com/forum/#!topic/druid-user/xPdSmowsQD4" target="_blank" rel="external">dosud největší Druid cluster</a>?</p>
<ul>
<li>50–100 PB surových dat, z kterých Druid po všech kompresích vytvořil 500 TB dat.</li>
<li>20 bilionů událostí (~ řádků).</li>
<li>400 serverů.</li>
</ul>
<h2 id="konzistence-dat">Konzistence dat</h2>
<p><strong>Data nejsou konzistentní napříč Realtime uzly.</strong> Může se totiž stát, že Realtime uzel #1 zpracovává zprávy s offsetem 10 000, zatímco Realtime uzel #2, jeho replika, je trochu pozadu a zpracovává zprávy teprve s offsetem 9 750, tedy je o 250 zpráv pozadu. Tomu se nedá vyhnout, ať už používáme starý přístup, nebo nový přístup. Broker se přitom může jednou zeptat Realtime uzlu #1 a potom uzlu #2. Potom by se mohlo stát, že by v prvním dotazu zjistil od uzlu #1, že už proběhlo 10 000 impresí, ale o sekundu později by z uzlu #2 zjistil, že proběhlo 9 750 impresí, což je zjevně nesmysl, aby o sekundu později nastalo <em>méně impresí</em>.</p>
<p>Nicméně pokud oba Realtime uzly stíhají, tak by tato nekonzistence měla být nepostřehnutelná.</p>
<p>Data napříč <strong>Historickými uzly jsou plně konzistentní</strong>, protože pokud dva Historické uzly servírují segment pro daný časový úsek, tak tyto dva segmenty jsou zcela identické.</p>
<!--
## Více segmentů pro jeden den

Prozatím jsme uvažovali, že když máme segmentGranularity nastavenou na jeden den, že za den vznikne jeden segment = jeden soubor. Ve skutečnosti těch souborů vznikne více. Když máme v běhu dva Realtime uzly, které zpracovávají vždy polovinu dat, tak každý Realtime uzel si vytváří svůj vlastní segment. Tedy každý den by každý Realtime uzel vyprodukoval svůj vlastní segment a tyto segmenty už by se nikde nespojovaly do jednoho velkého segmentu. Je proto možné mít pro jeden den více segmentů.
-->
<h2 id="zookeeper">ZooKeeper</h2>
<p>V předchozích obrázcích jsem například nakreslil šipku z Koordinátoru přímo do Historického uzlu. To nebylo úplně přesné, protože ve skutečnosti Koordinátor s historickým uzlem nekomunikuj přímo, ale pomocí <a href="https://zookeeper.apache.org/" target="_blank" rel="external">ZooKeeperu</a>.</p>
<p>ZooKeeper je hojně používaná služba v distribuovaných systémech, protože umožňuje takové věci jako zvolit leadera v distribuované síti, udržuje synchronizovaný log napříč svými servery a často se používá na ukládání konfigurací či jiných metadat. Kromě toho poskytuje klienty, které jsou schopny reagovat na změnu dat v Zookeeperu.</p>
<p>Potřebuje-li Koordinátor něco říct Historickému uzlu, uloží na předem domluvené místo v ZooKeeperu danou zprávu a Historický uzel si potom sám všimne, že se v ZooKeeperu na dané adrese něco změnilo, zjistí co a provede příslušnou akci.</p>
<p>Ve skutečnosti tak obrázek s komunikací vypadá takhle: (vypůjčeno přímo z <a href="http://druid.io/docs/0.7.0/Design.html" target="_blank" rel="external">dokumentace Druidu</a>)</p>
<div class="figure">
<img src="/images/druid/druid-manage-1.png" alt="">

</div>
<p>Všechny šipky jdou vždy z druidího uzlu do ZooKeeperu, nikdy ne do dalšího druidího uzlu. Na principu se nic nezměnilo. Druiďáci ale mají v plánu <a href="https://groups.google.com/forum/#!topic/druid-development/5s0Uh2NJ15c" target="_blank" rel="external">použití Zookeeperu omezit</a>. Což není úplně neobvyklé, nová Kafka už také daleko méně závisí na ZooKeeperu než starší verze.</p>
<p>Přes ZooKeeper také Realtime a Historické uzly sdělují celému clusteru jaké segmenty jsou u nich aktuálně uložené. Na tyto informace pak reaguje Broker, který musí v každé chvíli vědět, koho se má zrovna zeptat.</p>
<p>Příště si ukážeme, jak Druid agreguje a zpracovává všechny vstupní zprávy, ze kterých vytváří segmenty.</p>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/druid-io-architektura/#disqus_thread</comments>
    </item>
    
    <item>
      <title><![CDATA[Druid.io: distribuovaná immutable databáze pro analytické výpočty]]></title>
      <link>http://programio.havrlant.cz/druid-io/</link>
      <guid>http://programio.havrlant.cz/druid-io/</guid>
      <pubDate>Mon, 27 Apr 2015 16:42:56 GMT</pubDate>
      <description>
      <![CDATA[<p><a href="http://druid.io/" target="_blank" rel="external">Druid.io</a> používáme pro ukládání všech statistických a analytických dat. Je ]]>
      </description>
      <content:encoded><![CDATA[<p><a href="http://druid.io/" target="_blank" rel="external">Druid.io</a> používáme pro ukládání všech statistických a analytických dat. Je to vysoce škálovatelná distribuovaná immutable databáze, která obsahuje přímou podporu pro agregaci dat a pro aproximační algoritmy jako je <a href="/hyperloglog/">hyperloglog</a> nebo histogram.</p>
<h2 id="dlouhá-cesta-k-druidu">Dlouhá cesta k Druidu</h2>
<p>Než se dostaneme k Druidu, podíváme se na to, jak řešíme ukládání statistických dat teď, co jsme plánovali zkusit a proč jsme nakonec zvolili Druidíka. Vše si ukážeme na nejjednodušším příkladu ukládání impresí (tj. zobrazení reklamního banneru). Naše vstupní data vypadají nějak takto:</p>
<table>
<thead>
<tr class="header">
<th align="left">datetime</th>
<th align="left">campaignID</th>
<th align="left">userId</th>
<th align="left">web</th>
<th align="left">browser</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-15-03 14:10:54</td>
<td align="left">336</td>
<td align="left">40BBF8</td>
<td align="left">csfd.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 14:10:55</td>
<td align="left">1344</td>
<td align="left">9896AA</td>
<td align="left">idnes.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="odd">
<td align="left">2015-15-03 14:11:01</td>
<td align="left">1344</td>
<td align="left">40BBF8</td>
<td align="left">idnes.cz</td>
<td align="left">Firefox</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 14:11:05</td>
<td align="left">3738</td>
<td align="left">43D03E</td>
<td align="left">csfd.cz</td>
<td align="left">Safari</td>
</tr>
<tr class="odd">
<td align="left">2015-15-03 14:11:18</td>
<td align="left">1344</td>
<td align="left">9896AA</td>
<td align="left">idnes.cz</td>
<td align="left">Chrome</td>
</tr>
<tr class="even">
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
</tr>
</tbody>
</table>
<p>Každý řádek reprezentuje jednu impresi. Z těchto surových dat chceme získávat tyto informace:</p>
<ul>
<li>Kolik impresí dosud udělala kampaň XYZ?</li>
<li>Kolik unikátních zobrazení udělala kampaň XYZ za uplynulý týden?</li>
<li>Kolik unikátních zobrazení udělaly všechny kampaně klienta ABC?</li>
</ul>
<p>…a mnoho dalších, toto jsou ty nejzákladnější dotazy. Hlavním problémem samozřejmě je, že těchto dat máme <em>hodně</em>, řekněme zhruba deset tisíc impresí za sekundu, což dělá necelou miliardu denně.</p>
<h3 id="cesta-sql">Cesta SQL</h3>
<p>V současné době máme data v SQL databázi, konkrétně <a href="https://www.infobright.com/" target="_blank" rel="external">InfoBright</a>. Běžné dotazy zvládá odpovídat rychle, nicméně pokud klient požádá o nějaký složitější report, ve kterém se musí hodně joinovat, může si na odpověď počkat i několik dlouhých desítek minut.</p>
<p>Celkově vzato se SQL databáze blbě replikuje a blbě sharduje. Shardováním myslím to, že databáze běží na dvou serverech, v každé databázi je polovina dat a když vy položíte do databáze dotaz, automaticky se provede na obou serverech, obě databáze tento dotaz vyhodnotí a pak nějaký driver tyto dva dílčí výsledky spojí do jednoho. Zkrátka možnost databázi horizontálně škálovat. Proto zatím škálujeme jenom vertikálně.</p>
<p>Jednou z hlavních nevýhod Infobright je navíc to, že je dost drahá.</p>
<h3 id="cesta-nosql-a-předpočítávání-výsledků">Cesta NoSQL a předpočítávání výsledků</h3>
<p>Když jsme tento problém začali řešit, rozhodli jsme se jako první zkusit data předpočítávat. Chtěli jsme vzít nějakou NoSQL Key-value databázi a ukládat do ní data typu:</p>
<ul>
<li>“14. 3. vidělo reklamu XYZ na ČSFD 35 952 lidí, z toho 9744 v Chrome, 4788 ve Firefoxu, 0 v Opeře, …”</li>
<li>“15. 3. vidělo reklamu XYZ na ČSFD 28 434 lidí, z toho 4410 v Chrome, 2772 ve Firefoxu, 0 v Opeře, …”</li>
<li>atd.</li>
</ul>
<p>Vytáhnout počet impresí pro daný den pro daný prohlížeč je pak otázkou vytáhnutí jednoho řádku z databáze. Jenomže tento způsob ukládání trpí několika problémy:</p>
<ul>
<li><p>Ve skutečnosti se <strong>nikdy nebude tahat pouze jeden řádek</strong>. Když budeme chtít znát statistiky za jeden týden, musíme vytáhnout sedm řádků a výsledky sečíst. Navíc chceme podporovat různá časová pásma, což znamená, že bychom předpočítaná data neměli ukládat pod denní granularitou, ale pod hodinovou granularitou. Ale to není velký problém.</p></li>
<li><p>Imprese sice lze sčítat, ale <strong>počet unikátních uživatelů už se sčítat nedá</strong>. Pokud bychom v sobotu měli 1000 unikátů a v neděli 1500 unikátů, neznamená to, že za oba dny tam máme 2500 unikátů. Museli bychom proto předpočítávat počty unikátů pro každý možný časový interval (od 17. 3. do 23. 3 tam bylo 1500 unikátních uživatelů, od 17. 3. do 24. 3. 1750 unikátů atp.) nebo přinejmenším alespoň pro běžné intervaly jako je minulý týden, celý běh kampaně, posledních sedm dní apod. Předpočítávat pro všechny intervaly bude velice náročné, předpočítávat jen některé časy se chvíli zdálo jako docela přijatelné řešení.</p></li>
<li><p><strong>S každou novou dimenzí exponenciálně roste počet klíčů, pro které musíme počítat výsledky</strong>. Pokud bychom zobrazovali reklamu na tisíci webech a ukládali si informaci o deseti prohlížečích, museli bychom si ukládat až 1000 · 10 = 10 000 záznamů za každou hodinu. Pokud bychom dále ukládali i tři hlavní operační systémy, dostaneme se až na 10 000 · 3 = 30 000 záznamů každou hodinu. A tak dále s každou novou dimenzí, podle které bychom chtěli filtrovat. To není dlouhodobě zvládnutelné.</p></li>
</ul>
<h2 id="dude-i-can-store-that-in-memory">Dude, I can store that in memory</h2>
<p>Zajímavé je, že přesně touto cestou si prošli i <a href="http://druid.io/blog/2011/04/30/introducing-druid.html" target="_blank" rel="external">Metamarkeťáci</a>, autoři Druidu, a stejně jako my přišli na to, že ani SQL databáze, ani předpočítávání není ta správná cesta. Tehdy to dopadlo tak, že se nějaký Metamarkeťák podíval na data, která potřebovali prohledávat a pronesl památnou větu: “Dude, I can store that in memory”. Toho se chytli a vznikla první verze Druidu, distribuované databáze, která všechna data, nad kterými se volaly dotazy, držela v paměti. Byl to dobrý nápad? Ano, i ne.</p>
<ul>
<li>Protože jsou všechna data v paměti, jsou dotazy bleskurychlé.</li>
<li>Protože je Druid distribuovaná databáze, nejste omezení velikostí RAMky na jednom počítači. “Přidat” paměť můžete tak, že přidáte nový server do clusteru. Potřebujete-li deset tera paměti, můžete vytvořit cluster ze sto počítačů, každý po sto GB paměti. Druid si s tím poradí.</li>
</ul>
<p>Otázkou zůstává, jestli se to vyplatí. Nebude to moc drahé? Naše současné denní statistiky mají v surové podobě přibližně 50 GB. Za rok by to dalo přibližně 18 TB. Pokud bychom toto množství paměti nakoupili jako <a href="http://aws.amazon.com/ec2/pricing/" target="_blank" rel="external">cloudovou službu u Amazonu</a>, stálo by nás to $210. Za hodinu. Za měsíc by to byly skoro čtyři miliony Kč. (75 instancí r3.8xlarge) No, není to úplně málo.</p>
<h3 id="snižujeme-velikost-dat">Snižujeme velikost dat</h3>
<p>Zkusíme snížit velikost dat.</p>
<ol style="list-style-type: decimal">
<li><p>V těch 50 GB jsou i data, která ve skutečnosti pro statistiky nepotřebujeme – odstraníme je.</p></li>
<li><p>Můžeme nějak rozumně komprimovat data. Místo toho, abychom do databáze ukládali string “chrome”, tak tam uložíme nějakou číselnou konstantu, která tento prohlížeč reprezentuje.</p></li>
<li><p>Blbá jsou IDéčka uživatelů. Pokud chceme spočítat počet unikátních uživatelů, kteří viděli nějakou reklamu, musíme u každé imprese uložit i to, jaký uživatel ji viděl, tj. nějaké jeho unikátní ID. Unikátní ID má ale tu blbou vlastnost, že se velmi špatně komprimuje, protože … no zkrátka protože jsou to příliš unikátní hodnoty. Jak se řeší problém si řekneme v příštím článku, pro teď předpokládejme, že sloupec s IDečky odstraníme.</p></li>
</ol>
<h3 id="agregujeme">Agregujeme</h3>
<p>V tuto chvíli jsme docela slušně zredukovali velikost dat, ale stále ještě nejsme u konce. Můžeme totiž data dále agregovat. Stále máme pro každou impresi jeden řádek, ale to my ani tak moc nepotřebujeme. Potřebujeme vědět, kolik impresí se událo za danou hodinu. Můžeme vzít takovouto tabulku…</p>
<table>
<thead>
<tr class="header">
<th align="left">datetime</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-15-03 10:52:54</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">safari</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 10:52:58</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">safari</td>
</tr>
<tr class="odd">
<td align="left">2015-15-03 10:53:14</td>
<td align="left">3738</td>
<td align="left">csfd.cz</td>
<td align="left">chrome</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 10:53:15</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">safari</td>
</tr>
<tr class="odd">
<td align="left">2015-15-03 10:53:24</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">chrome</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 10:53:25</td>
<td align="left">1344</td>
<td align="left">csfd.cz</td>
<td align="left">chrome</td>
</tr>
<tr class="odd">
<td align="left">2015-15-03 10:53:29</td>
<td align="left">3738</td>
<td align="left">csfd.cz</td>
<td align="left">chrome</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 10:53:32</td>
<td align="left">3738</td>
<td align="left">csfd.cz</td>
<td align="left">chrome</td>
</tr>
<tr class="odd">
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 11:02:38</td>
<td align="left">1344</td>
<td align="left">csfd.cz</td>
<td align="left">safari</td>
</tr>
<tr class="odd">
<td align="left">2015-15-03 11:02:43</td>
<td align="left">1344</td>
<td align="left">csfd.cz</td>
<td align="left">safari</td>
</tr>
</tbody>
</table>
<p>…a můžeme ji agregovat na takovouto tabulku:</p>
<table>
<thead>
<tr class="header">
<th align="left">datetime</th>
<th align="left">campaignID</th>
<th align="left">web</th>
<th align="left">browser</th>
<th align="left">impressions</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">2015-15-03 10:00:00</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">safari</td>
<td align="left">3</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 10:00:00</td>
<td align="left">1344</td>
<td align="left">idnes.cz</td>
<td align="left">chrome</td>
<td align="left">1</td>
</tr>
<tr class="odd">
<td align="left">2015-15-03 10:00:00</td>
<td align="left">1344</td>
<td align="left">csfd.cz</td>
<td align="left">chrome</td>
<td align="left">1</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 10:00:00</td>
<td align="left">3738</td>
<td align="left">csfd.cz</td>
<td align="left">chrome</td>
<td align="left">3</td>
</tr>
<tr class="odd">
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
<td align="left">…</td>
</tr>
<tr class="even">
<td align="left">2015-15-03 11:00:00</td>
<td align="left">1344</td>
<td align="left">csfd.cz</td>
<td align="left">safari</td>
<td align="left">2</td>
</tr>
</tbody>
</table>
<p>V první tabulce máme co jedno zobrazení reklamy, to jeden řádek. Ve druhé tabulce už jsme si spočítali, že na iDnes.cz byly v Safari v dané hodině a pro danou kampaň tři imprese. Druhá tabulka obsahuje stejně informací jako ta první, s výjimkou časového razítka. Z druhé tabulky už nevyčteme, kolik impresí bylo mezi 10:52:00 a 10:53:00. Ale to nás už moc nezajímá, hodinová granularita nám stačí.</p>
<p>Jak moc se dále data agregují záleží na počtu dimenzí, na jejich kardinalitě, tj. kolik různých hodnot v sloupci máme a na časové granularity – agregace bude větší, když se bude agregovat po dnech než po hodinách. Tato agregace může snížit velikost dat klidně <em>na</em> deset procent, ale také může snížit velikost jen <em>o</em> deset procent; záleží na datech.</p>
<p>A jak jsme si pomohli? Se všemi kompresemi a agregacemi bychom se mohli dostat na řádově nižší velikost dat. Pokud bychom předpokládali, že bychom dokázali data takto zredukovat z 50 GB na 2,5 GB, stál by nás cluster na Amazonu dvacetkrát méně, což znamená 200 000 Kč za měsíc. Ne že by to bylo málo, ale už se o tom dá uvažovat.</p>
<h2 id="dámy-a-pánové-druid.io">Dámy a pánové, Druid.io</h2>
<p>Dobře, už jsme si spočítali, že bychom možná dokázali všechna data vměstnat do paměti a že by to nemuselo být úplně drahé. Co nám tedy <a href="http://druid.io/" target="_blank" rel="external">Druid</a> nabízí, že jsme nakonec skončili u něj?</p>
<p>Druid je distribuovaná databáze napsaná v Javě vhodná pro statistická a analytická data. Není například vhodná pro ukládání textů, takže redakční systém iDnes.cz bych na něm nestavěl. Jaké jsou klíčové vlastnosti?</p>
<ul>
<li><p>Zvládá běžné databázové operace jako jsou filtry nebo grupování; nezvládá join – proto jsou typicky data uložená denormalizovaně. Nad sloupcem umí spočítat počet unikátních hodnot, průměr, medián, jiný percentil, rozložení hodnot. Obecně dokáže nad sloupcem udržovat aproximaci histogramu a z něj spočítat hromadu zajímavých údajů. <strong>Všechno z toho dělá Druid distribuovaně na několika různých serverech.</strong></p></li>
<li><p><strong>Je škálovatelný.</strong> Každá složka Druidu je škálovatelná, pokud už současná velikost clusteru nezvládá zpracovávat dotazy nad daty, snadno lze přidat další stroje, které si k sobě stáhnou část dat a přeberou tak i část dotazů. Každý dotaz se typicky vyhodnocuje na několika Druidích serverech současně. Můžete si to představit tak, že máme například 12 Druidích serverů, na každém jsou data za jeden měsíc roku a dotaz na statistická data za uplynulý půl rok by se paralelně vykonával na šesti serverech.</p></li>
<li><p><strong>Každou část lze replikovat.</strong> Stejně jako třeba u <a href="/kafka">Kafky</a> lze i Druidu jednoduše nastavit replikační faktor. Nastavíme-li ho na dva, budou všechna data uložena na dvou odlišných strojích. Pokud jeden ze serverů vypadne, dotazy se přesměrují na druhý stroj.</p></li>
<li><p><strong>Je realtime.</strong> Druid zvládá indexovat desetitisíce nově příchozích zpráv za sekundu a tyto události dává okamžitě k dispozici pro dotazování.</p></li>
<li><p><strong>Všechna data jsou immutable.</strong> Druid v podstatě zvládá pouze <code>append</code> dat. Neumí žádný <code>update</code>. Každá zpráva, kterou appendujeme, musí mít časové razítko, aby šlo určit, do kterého časového intervalu patří. Do Druidu proto patří <em>události</em>, tj. data, která nastala v nějaký okamžik a tato data jsou na vždy platná. Ve chvíli, kdy Druid zpracuje <em>včerejší</em> data, se už tato data nemůžou nijak změnit. Potřebujete-li změnit stará data, není jiné cesty než stará data smazat a vše znova přepočítat (což se běžně dělá).</p></li>
</ul>
<p>To jsou asi věci, které jsme všichni čekali, když se náš seriál zabývá budováním statistik se zaměřením na škálovatelnost, odolnost vůči výpadkům a na nízkou latenci. Co dále Druid umí? Všecho, o čem jsme se bavili v předchozí kapitole:</p>
<ul>
<li><p><strong>Druid používá různé kompresní mechanismy pro zmenšení velikost dat, která do něj vkládáme.</strong> Například i slovníky, takže můžeme do Druidu valit data jako je “chrome” a Druid se už sám postará o to, aby se daného sloupce uložila nějaká číselná konstanta.</p></li>
<li><p><strong>Druid zvládá agregovat data postupem, který jsme si ukázali výše.</strong> Stačí mu nastavit, které sloupce má jak agregovat a nastavit časovou granularitu. Pokud Druid najde v dané hodině dva stejné řádky, spojí je do jednoho a uloží k němu informaci o tom, že tento řádek reprezentuje dvě imprese. O agregaci budeme více mluvit příště.</p></li>
<li><p><strong>Druid má přímou podporu pro <a href="/hyperloglog/">HyperLogLog</a></strong>, díky čemu se můžeme efektivně zbavit sloupce s IDečkami uživatelů. Díky Druidovi jsme schopni velmi přesně odhadnout počet unikátních uživatelů, kterým se zobrazila nějaká reklama bez toho, aniž bychom fakticky měli uloženo, komu se jaká reklama zobrazila. Díky <a href="/sjednoceni-hyperloglogu/">sjednocení HyperLogLogu</a> bude fungovat i agregace.</p></li>
<li><p><strong>Druid je schopný udržovat již zmiňovanou <a href="http://en.wikipedia.org/wiki/Histogram" target="_blank" rel="external">aproximaci histogramu</a> pro daný sloupec</strong>.</p></li>
</ul>
<h2 id="dobře-je-to-všechno-hezké-ale-fakt-musí-být-vše-v-paměti">Dobře, je to všechno hezké, ale … fakt musí být vše v paměti?</h2>
<p>Nemusí! I kluci z Metamarkets si asi časem uvědomili tři věci:</p>
<ul>
<li>Když už spotřebovaná paměť leze do jednotek TB, tak už tak moc levná není.</li>
<li>SSD vlastně nejsou tak úplně pomalé.</li>
<li>Opravdu je nutné držet v paměti tři roky stará data jenom proto, abychom je byli schopni dotázat jednou za uherský rok?</li>
</ul>
<p><strong>Současná verze Druidu proto umožňuje dotazovat i ta data, která jsou uložená na disku a nejsou přímo načtená v paměti.</strong> Je to samozřejmě pomalejší, ale pořád je to dostatečně rychlé. Zároveň umožňuje definovat pravidla, kam se mají jaká data ukládat. Takže můžeme vytvořit pravidla, že data za poslední tři měsíce budou na nejrychlejších strojích s hromadou paměti, zatímco starší data budou na horších strojích s méně paměti. Idea je, že v 90 % případů stejně taháme <em>nová</em> data, ne data několika let stará.</p>
<p>Není to nijak zvlášť nový přístup, takový <a href="https://blog.twitter.com/2014/building-a-complete-tweet-index" target="_blank" rel="external">Twitter používá něco velmi podobného</a>. Aktuální tweety udržuje v paměti, zatímco staré tweety má uložené na SSD (a ještě k tomu trošku vyladili kernel, ale kdo z nás to občas nedělá, že?).</p>
<h2 id="příště">Příště…</h2>
<p>Příště se podíváme na architekturu Druidu.</p>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/druid-io/#disqus_thread</comments>
    </item>
    
    <item>
      <title><![CDATA[Uložení lokálního stavu v Samze]]></title>
      <link>http://programio.havrlant.cz/samza-local-state/</link>
      <guid>http://programio.havrlant.cz/samza-local-state/</guid>
      <pubDate>Mon, 20 Apr 2015 16:54:07 GMT</pubDate>
      <description>
      <![CDATA[<p>V minulé části série jsme si představili problém: pokud restartujeme Samza job, nevyhnutelně přijdeme o lokální stav aplikace, přijdeme o]]>
      </description>
      <content:encoded><![CDATA[<p>V minulé části série jsme si představili problém: pokud restartujeme Samza job, nevyhnutelně přijdeme o lokální stav aplikace, přijdeme o všechna data, které jsme měli uloženy pouze v paměti počítače. Jak nám Samza framework pomůže tento problém vyřešit?</p>
<p>Ukážeme si to na jiném Samza jobu. Jednou z dalších věcí, které měříme, jsou pochopitelně kliky na reklamní bannery. Zpráva o kliku také nakonec skončí v <a href="/kafka">Kafce</a> v topicu <em>clicks</em> a ten se dále zpracovává v Samze. Představme si nyní, že nějaký uživatel přijde na ČSFD, koukne na reklamu a … no to není možné! Vyhrál iPhone! Rychle kliká, stránka se ale neotevírá. Stále jako zběsilý kliká na reklamní banner, až se nakonec dostane na cílovou stránku a začne skákat radostí.</p>
<p>Jaká je největší zrada celého příběhu? Pokud uživatel desetkrát za sebou kliknul na reklamní banner, my to započítáme jako deset různých kliků, přitom logicky by to měl být spíše jeden klik. Abychom to byli schopni takto spočítat, máme v zásadě dvě možnosti: zařídit, aby se po druhém kliku na banner už neodeslal požadavek na naše servery, nebo musíme všechny požadavky ještě dále filtrovat a odstraňovat duplicity.</p>
<p>Z různých důvodů nemusí být úplně jednoduché zařídit, aby se už pdoruhé neodeslal požadavek na naše servery, takže zvolíme druhou možnost. Potřebujeme napsat Samza job, který bude číst topic <em>clicks</em> a bude odstraňovat duplikované zprávy, které bude odesílat do topicu <em>unique_clicks</em>.</p>
<h2 id="naivní-verze">Naivní verze</h2>
<p>Každá naše zpráva má unikátní ID. Pokud budou v topicu dvě shodné zprávy, budou mít také shodné ID. Nemusíme si uchovávat obsah každé zprávy, stačí nám si pamatovat IDéčka těch zpráv, které jsme už viděli.</p>
<p>Napíšeme Samza job tak, že bude číst zprávy z topicu <em>clicks</em> a IDéčko každé dosud nepřečtené zprávy si uloží do nějaké množiny. Pokud z topicu přečteme znova stejnou zprávu, budeme mít ID této zprávy uložené v množině, takže tuto zprávu zahodíme; nebo ji pošleme do topicu <em>duplicated_clicks</em>, abychom o tuto informaci nepřišli.</p>
<p>Spustíme Samza job.</p>
<p>Za tři hodiny spadne na nedostatku paměti.</p>
<h2 id="přidáme-windowing">Přidáme windowing</h2>
<p>Problém byl v tom, že jsme do množiny neustále přidávali nová a nová IDéčka, ale neodstraňovali jsme stará. Nevyhnutelně muselo dojít k tomu, že dojde paměť. Tento problém lze vyřešit například <a href="/samza-windowing">windowingem</a>, tj. budeme z paměti odstraňovat IDéčka, která jsme uložili před více než hodinou a budeme doufat, že to bude stačit.</p>
<p>Máme ale větší problém. Po restartu aplikace přijdeme a celý lokální stav aplikace a tím pádem i o seznam již přečtených zpráv. Po nastartování Samza jobu nutně začneme propouštět i ty zprávy, které jsme přečetli v minulém běhu programu.</p>
<div class="figure">
<img src="/images/samza/samzalocalstate1.svg" alt="">

</div>
<p>Zatímco druhou zprávu s ID 46 vyfiltrujeme, protože ji máme uloženou v množině, druhá zpráva s ID 47 nám projde, protože to bude první zpráva, kterou přečteme po startu Samza jobu a naše množina dosud přečtených zpráv je aktuálně prázdná.</p>
<p>Potřebovali bychom zkrátka zařídit, aby data, která máme v paměti, přežila pád aplikace. Podobný problém jsme už řešili, když jsme se bavili o <a href="/kafka-consumer">ukládání offsetů v Kafce</a>. Hlavním rozdílem oproti offsetům je ten, že lokální stav aplikace může být mnohem větší. Potřebujeme nějaký prostor na uložení potenciálně velkého množství dat, který přežije pád našeho Samza jobu. Jaké máme možnosti?</p>
<ul>
<li>Můžeme data ukládat do souboru. No. Tam by se asi blbě prohledávala, že?</li>
<li>Dobře, můžeme data držet v paměti, kde je budeme prohledávat, a do souboru je budeme pouze zálohovat. Problémem nastane, když velikost dat přeroste dostupnou paměť.</li>
<li>Můžeme data ukládat do nějaké lokální databáze. Jenomže po restartu může Samza job běžet na úplně jiném počítači a k lokálně uloženým datům vůbec nebude mít přístup.</li>
<li>Můžeme data ukládat do nějaké vzdálené databáze na jiném stroji. To už je lepší cesta, ale asi tím slušně snížíme rychlost celého Samza jobu.</li>
</ul>
<h2 id="keyvalue-store">KeyValue store</h2>
<p>Samza nám nabízí něco ještě jiného. Ideálně bychom chtěli mít všechny data v paměti, protože to je nejrychlejší. Druhý nejlepší způsob je mít data ne v paměti, ale alespoň na stejném počítači.</p>
<p>V Samze můžeme použít vestavěnou KeyValue store, která je uložena lokálně na disku, přičemž každá změna, kterou v KeyValue store provedeme, se automaticky zálohuje v Kafce. Samza si sama vytvoří v Kafce nový topic, který pojmenuje podle jobu, který je zrovna spuštěn a do toho topicu bude ukládat všechny změny, které provedeme v KeyValue store.</p>
<p>Když chceme změnit hodnotu v KeyValue storu, uděláme něco jako</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">store.put(<span class="string">"Star trek"</span>, <span class="number">47</span>);</span><br></pre></td></tr></table></figure>
<p>Samza zařídí, aby se v KeyValue store přepsala hodnota pod klíčem “Star trek” hodnotou 47 a zároveň vypálí do správného kafka topicu zprávu v přibližném tvaru</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;"key": "Star trek", value: 47&#125;</span><br></pre></td></tr></table></figure>
<p>Tím získáme historii všech změn, které byly ve storu provedeny.</p>
<p>Po restartu Samza jobu se jako první vytvoří nová prázdná KeyValue store, přečte se celý obsah záložního topicu a aplikují se na KeyValue store všechny přečtené změny. Tím dostaneme KeyValue store do stavu, v jakém byl, když jsme restartovali Samzu.</p>
<p>Kafka nám přitom přímo poskytuje podporu pro takovéto ukládání zpráv – každé zprávě totiž lze přiřadit nějaký <em>klíč</em>. V Kafce můžeme dále zapnout něco, co se jmenuje <a href="https://cwiki.apache.org/confluence/display/KAFKA/Log+Compaction" target="_blank" rel="external">Log Compaction</a>. Tato feature bude v pravidelných intervalech hledat zprávy v topicu se stejným klíčem. Protože nemá smysl mít v topicu více zpráv se stejným klíčem – zajímá nás vždy jen ta nejnovější hodnota – Kafka automaticky smaže všechny starší zprávy.</p>
<div class="figure">
<img src="/images/samza/logcompaction1.svg" alt="">

</div>
<p>Pro každý klíč zůstala v topicu (resp. partitioně) pouze jedna zpráva a to ta nejnovější. Offsety se nezmění. Z tohoto důvodu můžou v jedné partitioně v Kafce vzniknout “díry”, ve kterých chybí zprávy s určitým offsetem. Bohužel to také znamená, že každá zpráva musí v sobě obsahovat svůj offset, což mírně komplikuje kód celé Kafky, ale to už není náš problém.</p>
<p>Díky Log Compaction nebude záložní topic růst do nekonečna, ale bude vždy přibližně stejně velký jako KeyValue store samotná. Budeme-li měnit jen sto hodnot v KeyValue store, v topicu po promazání bude také jen sto zpráv. Přečteme-li nyní celý záložní topic, získáme stav KeyValue storu.</p>
<p>Náš Samza job, který má deduplikovat topic s kliky bychom mohli napsat tak, že by všechna IDéčka ukládal do KeyValue storu a při kontrole by se zase do KeyValue díval. Po restartu aplikace by se KeyValue obnovil do původního stavu, ať spustíme Samza job na kterémkoliv počítači.</p>
<p>Pro přesné použití se podívejte do dokumentace <a href="http://samza.apache.org/learn/documentation/0.7.0/container/state-management.html" target="_blank" rel="external">State Managmentu</a>. Samza aktuálně jako KeyValue store využívá <a href="https://github.com/google/leveldb" target="_blank" rel="external">levelDB</a>. V budoucích verzích by měla přibýt i <a href="http://mail-archives.apache.org/mod_mbox/samza-commits/201502.mbox/%3CJIRA.12771866.1422917062000.234561.1422917317497@Atlassian.JIRA%3E" target="_blank" rel="external">čistě in-memory KeyValue store</a> – to pokud víte, že potřebujete ukládat relativně málo dat, které se vám jistě vlezou do paměti, ale stále byste je chtěli mít zálohované v Kafce.</p>
]]></content:encoded>
      <comments>http://programio.havrlant.cz/samza-local-state/#disqus_thread</comments>
    </item>
    
  </channel>
</rss>
