I rätt bruk faller Bloom filter som magi. Det är ett djärvt uttalande, men i den här handledningen utforskar vi den nyfikna datastrukturen, hur bäst du kan använda den och några praktiska exempel med Redis och Node.js.
Blomfiltret är en probabilistisk, enkelriktad datastruktur. Ordet "filter" kan vara förvirrande i detta sammanhang; filter innebär att det är en aktiv sak, ett verb, men det kan vara lättare att tänka på det som lagring, ett substantiv. Med ett enkelt Bloom filter kan du göra två saker:
Det här är viktiga begränsningar att förstå - du kan inte ta bort ett objekt eller kan du lista objekten i ett Bloom-filter. Du kan också inte med säkerhet veta om ett föremål har lagts till filtret tidigare. Det här är det probabilistiska som ett Bloom-filter kommer in-falska positiva är möjliga, men falska negativ är inte. Om filtret är korrekt inställt kan falska positioner vara extremt sällsynta.
Varianter av Bloom-filter finns, och de lägger till andra förmågor, som borttagning eller skalning, men de lägger också till komplexitet och begränsningar. Det är viktigt att först förstå enkla Bloom-filter innan du går vidare till varianterna. Den här artikeln täcker bara de enkla blomfiltret.
Med dessa begränsningar har du ett antal fördelar: fast storlek, hashbaserad kryptering och snabba uppslag.
När du ställer in ett Bloom-filter, ger du det en storlek. Denna storlek är fast, så om du har ett objekt eller en miljard objekt i filtret, kommer det aldrig att växa bortom den angivna storleken. När du lägger till fler objekt i ditt filter ökar risken för falsk positiv. Om du angav ett mindre filter kommer denna falska positiva hastighet att öka snabbare än om du har en större storlek.
Blomfiltren är byggda på konceptet med enkelriktad hashing. Liksom att lagra lösenord korrekt lagrar Bloom-filter en hash-algoritm för att bestämma en unik identifierare för de föremål som skickas in i den. Hashes, av naturen, kan inte vändas och representeras av en till synes slumpmässig sträng av tecken. Så, om någon får tillgång till ett Bloom-filter, kommer det inte direkt att avslöja något av innehållet.
Slutligen är Bloom-filter snabbt. Operationen involverar mycket färre jämförelser än andra metoder, och det kan enkelt lagras i minnet, vilket förhindrar prestanda-beröva databas träffar.
Nu när du känner till gränserna och fördelarna med Bloom-filter, låt oss ta en titt på vissa situationer där du kan använda dem.
Vi använder Redis och Node.js för att illustrera Bloom-filter. Redis är ett lagringsmedium för ditt Bloom-filter; det är snabbt, i minnet, och har några specifika kommandon (getbit
, SETBIT
) som gör genomförandet effektivt. Jag antar att du har Node.js, npm och Redis installerad på ditt system. Din Redis-server ska vara igång lokal värd
vid standardporten för våra exempel att arbeta.
I denna handledning kommer vi inte att implementera ett filter från grunden. istället fokuserar vi på praktiska användningsområden med en förbyggd modul i npm: bloom-redis. bloom-redis har en mycket kortfattad uppsättning metoder: Lägg till
, innehåller
och klar
.
Som tidigare nämnts behöver Bloom-filter en hashingalgoritm för att generera unika identifierare för ett objekt. bloom-redis använder den välkända MD5-algoritmen, som, även om den kanske inte är den perfekta passformen för ett Bloom-filter (lite långsamt, överkill på bitar), fungerar bra.
Användarnamn, särskilt de som identifierar en användare i en URL, måste vara unika. Om du bygger en applikation som tillåter användare att ändra användarnamnet, så kommer du sannolikt att ha ett användarnamn som har aldrig har använts för att undvika förvirring och sniping av användarnamn.
Utan ett blomfilter måste du referera till ett bord som har varje användarnamn någonsin använt, och i skala kan det vara mycket dyrt. Blomstfilter tillåter dig att lägga till ett objekt varje gång en användare antar ett nytt namn. När en användare kontrollerar om ett användarnamn tas är allt du behöver göra att kolla Bloom-filtret. Det kommer att kunna säga med absolut säkerhet om det begärda användarnamnet tidigare har lagts till. Det är möjligt att filtret felaktigt kommer att returnera att ett användarnamn har använts när det inte har det, men det är fel på försiktighetssidan och kan inte orsaka någon verklig skada (förutom att en användare kanske inte kan hävda "k3w1d00d47").
För att illustrera detta, låt oss bygga en snabb REST-server med Express. Skapa först din package.json
fil och kör sedan följande terminalkommandon.
npm installera blom-redis -save
npm installera express - save
npm installera redis - save
Standardalternativen för blom-redis har storleken inställd på två megabyte. Detta beror på försiktighetssidan, men det är ganska stort. Ställ in storleken på Bloom-filtret är kritiskt: För stort och du slösar bort minne, är för litet och din falska positiva takt blir för hög. Matematiken som är inblandad i att bestämma storleken är ganska inblandad och bortom omfattningen av denna handledning, men tack och lov finns det en Bloom-kalkylator för att få jobbet gjort utan att spricka en lärobok.
Skapa nu din app.js
som följer:
"javascript var Bloom = kräver ('bloom-redis'), express = kräver ('express'), redis = kräver ('redis'),
app, klient, filter;
// setup vår Express server app = express ();
// skapa anslutningen till Redis-klienten = redis.createClient ();
filter = ny Bloom.BloomFilter (klient: klient, // se till att Bloom-modulen använder vår nyskapade anslutning till Redis-tangenten: 'användarnamn-blomfilter', // Redis-tangenten
// beräknad storlek på Bloom-filtret. // Det här är din storlek / sannolikhet avvägning görs //http://hur.st/bloomfilter?n=100000&p=1.0E-6 storlek: 2875518, // ~ 350kb numHashes: 20);
app.get ('/ check', funktion (req, res, nästa) // kolla för att se till att fråge strängen har "användarnamn" om (typ av req.query.username === 'undefined') // hoppa över denna rutt, gå till nästa - kommer att resultera i en 404 / ej hittad nästa ('rutt'); annars filter.contains (req.query.username, // användarnamnet från fråge-strängfunktionen (fel, resultat ) if (err) next (err); // om ett fel uppstår, skicka det till klienten annat res.send (användarnamn: req.query.username, // om resultatet är felaktigt Vi vet att objektet har inte använts // om resultatet är sant kan vi anta att objektet har använts status: resultat? "använt": "fri"); ); );
app.get ('/ save', funktion (req, res, nästa) if (typof req.query.username === 'undefined') next ('route'); annat // först behöver vi för att försäkra att det inte finns i filtret filter.contains (req.query.username, function (err, result) om (err) next (err); annat om (resultat) // true result means det finns redan, så berätta för användaren res.send (användarnamn: req.query.username, status: 'not-created'; annars // lägger vi till användarnamnet som skickats i frågesträngen till filtret filter.add (req.query.username, function (err) // Återuppringningsargumenten till Lägg till
ger ingen användbar information, så vi kontrollerar bara för att se till att inget fel passerade om (err) next (err); annat res.send (användarnamn: req.query.username, status: 'created'); ); ); );
app.listen (8010);"
För att köra den här servern: nod app.js
. Gå till din webbläsare och peka på den till: https: // localhost: 8010 / check användarnamn = Kyle
. Svaret ska vara: "Username": "Kyle", "status": "gratis"
.
Låt oss nu spara det användarnamnet genom att peka på din webbläsare på http: // localhost: 8010 / spara användarnamn = Kyle
. Svaret kommer att vara: "Username": "Kyle", "status": "skapade"
. Om du går tillbaka till adressen http: // localhost: 8010 / check användarnamn = Kyle
, svaret kommer att vara "Username": "Kyle", "status": "används"
. På samma sätt går tillbaka till http: // localhost: 8010 / spara användarnamn = Kyle
kommer att resultera i "Username": "Kyle", "status": "inte skapat"
.
Från terminalen kan du se filterets storlek: redis-cli strlen användarnamn-blom-filter
.
Just nu, med ett objekt, ska det visa 338.622
.
Nu, fortsätt och försök lägga till fler användarnamn med /spara
rutt. Prova så många som du vill.
Om du sedan kontrollerar storleken igen kan du märka att din storlek har gått upp lite, men inte för varje tillägg. Nyfiken, eller hur? Internt ställer ett Bloom-filter enskilda bitar (1/0) i olika positioner i strängen som sparas vid användarnamn-blom. Men dessa är inte sammanhängande, så om du sätter lite på index 0 och sedan en på index 10 000, kommer allt mellan att vara 0. För praktiska användningsområden är det inte initialt viktigt att förstå den exakta mekaniken i varje operation - vet bara att detta är normalt och att din lagring i Redis aldrig kommer att överstiga det värde du angav.
Färskt innehåll på en webbplats håller en användare tillbaka, så hur visar du en användare någonting nytt varje gång? Med ett traditionellt databas-tillvägagångssätt kan du lägga till en ny rad i en tabell med användaridentifieraren och identifieraren av historien, och då skulle du fråga den där tabellen när du bestämmer dig för att visa en bit innehåll. Som du kan tänka dig kommer din databas att växa extremt snabbt, särskilt med tillväxt av både användare och innehåll.
I det här fallet har en falsk negativ (t ex inte visat en osynlig del av innehållet) mycket liten följd, vilket gör Bloom filters ett lönsamt alternativ. Vid första rodnad kanske du tror att du behöver ett blomfilter för varje användare, men vi använder en enkel sammanfogning av användaridentifieraren och innehållsidentifieraren och sätter sedan in den här strängen i vårt filter. På så sätt kan vi använda ett enda filter för alla användare.
I det här exemplet låt oss bygga en annan grundläggande Express-server som visar innehåll. Varje gång du besöker rutten / Show-innehåll / någon-username
(med någon-username är ett webbadress säkert värde) visas en ny del innehåll tills webbplatsen är slut på innehållet. I exemplet är innehållet den första raden av de tio bästa böckerna på Project Gutenberg.
Vi måste installera en ny npm-modul. Från terminalen kör: npm installera async -save
Din nya app.js-fil:
"javascript var async = kräver ('async'), Bloom = kräver ('bloom-redis'), express = kräver ('express'), redis = kräver ('redis'),
app, klient, filter,
// Från Projekt Gutenberg - öppningslinjer i de 10 bästa offentliga böckerna // https://www.gutenberg.org/browse/scores/top openingLines = 'pride and prejudice': 'Det är en sanning som är universellt erkänd , att en enda man som är i besittning av en lycka, måste vara i behov av en fru. "," Alices-Adventures-in Wonderland ":" Alice började bli väldigt trött på att sitta av sin syster på banken och av att inte ha något att göra: en eller två gånger hade hon tittat på boken som hennes syster läste, men det hade inga bilder eller konversationer i det, "och vad är användningen av en bok, tänkte Alice utan bild eller samtal?" , "a-christmas-carol": "Marley var död: till att börja med.", "metamorphosis": "En morgon, när Gregor Samsa vaknade från oroliga drömmar, fann han sig förvandlad i sängen till en hemsk skadedjur. "frankenstein": "Du kommer att glädjas åt att höra att ingen katastrof har följt ett företag som du har ansett med sådana onda förskott.", "äventyr es-of-huckleberry-finn ":" Du vet inte om mig utan att du har läst en bok med namnet The Adventures of Tom Sawyer; men det spelar ingen roll. "," äventyr-of-sherlock-holmes ":" Till Sherlock Holmes är hon alltid kvinnan. "," Narrative-of-Life-of-Frederick-Douglass ":" Jag föddes i Tuckahoe, nära Hillsborough och ungefär tolv miles från Easton, i Talbot County, Maryland. "," The Prince ":" Alla stater, alla krafter som har hållit och håller regel över män har varit och är antingen republiker eller stormakter. "," Äventyr-av-tom-sawyer ":" TOM! " ;
app = express (); klient = redis.createClient ();
filter = ny Bloom.BloomFilter (klient: klient, nyckel: '3content-bloom-filter', // Redis-tangentstorlek: 2875518, // ~ 350kb // storlek: 1024, numHashes: 20);
app.get ('/ show-content /: user', funktionen (req, res, next) // vi kommer att slinga igenom contentIds, kontrollera om de är i filtret. // Eftersom det här spenderar tid på varje innehåll. Det skulle inte vara tillrådligt att göra över ett stort antal contentIds // Men i så fall är antalet contentIds liten / fast och vårt filter. innehåller funktionen är snabb, det är okej. var // skapar en uppsättning av nycklarna som definieras i openingLines contentIds = Object.keys (openingLines), // att få del av sökvägen från URI-användaren = req.params.user, checkContentId, found = false, done = false;
// eftersom filter.contains är asynkron använder vi async-biblioteket för att göra vår looping async.while (// check-funktion, där vår asynkrona slinga slutar funktionen () return (! Found &&! done);, funktion (cb) // hämta det första objektet från arrayen av contentIds checkContentId = contentIds.shift ();
// false betyder att vi är säkra på att det inte finns i filtret om (! checkContentId) done = true; // detta kommer att fångas av kontrollfunktionen ovanför cb (); annan // sammanlänka användaren (från webbadressen) med innehållet i innehållet filter.contains (user + checkContentId, funktion (err, resultat) om (err) cb (err); else found =! resultat; cb ();); , funktion (err) om (err) next (err); annars if (openingLines [checkContentId]) // innan vi skickar det fria innehållet, lägger vi till det i filtret för att förhindra att det visas igen filter.add (användare + checkContentId, funktion (err) if (err) nästa (err); else // skicka det färskt citatet res.send (openingLines [checkContentId]);); else res.send ('inget nytt innehåll!'); ); );
app.listen (8011);"
Om du noggrant uppmärksammar rundtur i Dev Tools märker du att ju mer du begär en enda sökväg med ett användarnamn, desto längre tid tar det. Vid kontroll av filtret tar en viss tid, i det här exemplet söker vi efter att fler objekt finns. Blomfiltret är begränsat i vad de kan berätta för dig, så du testar för närvaron av varje föremål. Naturligtvis är det i vårt exempel ganska enkelt, men testning för hundratals objekt skulle vara ineffektivt.
I det här exemplet bygger vi en liten Express-server som gör två saker: acceptera nya data via POST och visa aktuella data (med en GET-förfrågan). När den nya data är POST'd till servern kommer programmet att kontrollera att det finns närvaro i filtret. Om den inte är närvarande lägger vi till den i en uppsättning i Redis, annars kommer vi tillbaka null. GET-förfrågan hämtar den från Redis och skickar den till klienten.
Det här är annorlunda än de föregående två situationerna, eftersom falska positiva inte skulle vara okej. Vi använder Bloom-filtret som en första försvarskans. Med tanke på egenskaperna hos Bloom-filter vet vi bara säkert att något inte finns i filtret, så i det här fallet kan vi fortsätta och släppa in data. Om Bloom-filtret återvänder som förmodligen finns i filtret, vi Jag gör en check jämfört med den faktiska datakällan.
Så, vad får vi? Vi får hastigheten att inte behöva kontrollera mot den faktiska källan varje gång. I situationer där datakällan är långsam (externa API: er, pokey databaser, mitten av en platt fil), är hastighetsökningen verkligen nödvändig. För att visa hastigheten, låt oss lägga till en realistisk fördröjning på 150 ms i vårt exempel. Vi använder också console.time
/ console.timeEnd
för att logga på skillnaderna mellan en blomstfilterkontroll och en icke-blomfilterkontroll.
I det här exemplet använder vi också ett extremt begränsat antal bitar: bara 1024. Det fyller snabbt upp. När det fylls kommer det att visa fler och fler falska positiva resultat - du kommer se svarstiden ökar när den falska positiva räntan fyller upp.
Den här servern använder samma moduler som tidigare, så ställ in app.js
fil till:
"javascript var async = kräver ('async'), Bloom = kräver ('bloom-redis'), bodyParser = kräver (" body-parser "), express = kräver (" express "), redis = ),
app, klient, filter,
currentDataKey = 'aktuell data', usedDataKey = 'used-data';
app = express (); klient = redis.createClient ();
filter = ny Bloom.BloomFilter (klient: klient, nyckel: 'stal-bloom-filter', // för illustrationsändamål, detta är ett supert litet filter. Det ska fyllas på cirka 500 saker, så för en produktionsbelastning, du behöver något mycket större! storlek: 1024, numHashes: 20);
app.post ('/', bodyParser.text (), funktion (req, res, nästa) var använt;
console.log ('POST -', req.body); // logga in den aktuella data som publiceras console.time ('post'); // Börja mäta den tid det tar för att slutföra vårt filter och villkorlig verifieringsprocess //async.series används för att hantera flera asynkrontfunktionssamtal. async.series ([function (cb) filter.contains (req.body, function (err, filterStatus) om (err) cb (err); else used = filterStatus; cb (err);) ;, funktion (cb) if (used === false) // Bloomfilter har inga falska negativa, så vi behöver ingen ytterligare verifikation cb (null); else // it * may * vara i filter, så vi behöver göra en uppföljningskontroll. // För övningens syften lägger vi till en 150mars fördröjning här eftersom Redis kan vara snabb nog för att göra det svårt att mäta och förseningen kommer att simulera en långsam databas eller API-samtalet setTimeout (funktion () console.log ("eventuellt falskt positivt"); client.sismember (usedDataKey, req.body, funktion (err, medlemskap) if (err) cb (err); else / / sismember returnerar 0 om en medlem inte ingår i uppsättningen och 1 om den är. // Detta omvandlar resultaten till booleaner för konsekvent logik jämförelse används = medlemskap === 0? false: true; cb (err); );, 150);, funktion (cb) if (used === false) console.log ('Lägg till i filter'); filter.a dd (req.body, cb); else console.log ('Skiped filter addition, [false] positive'); cb (null); , funktion (cb) if (used === false) client.multi () .set (currentDataKey, req.body) // oanvänd data är inställd för enkel åtkomst till 'nuvarande data'-tangenten .sadd (usedDataKey, req.body) // och läggs till i en uppsättning för enkel verifiering senare .exec (cb); annat cb (null); ], funktion (err, cb) om (err) next (err); annat console.timeEnd ('post'); // loggar hur länge tiden har gått sedan console.time-samtalet ovan res.send (saved:! used); // returnerar om objektet sparades, sant för färsk data, felaktigt för gammal data. ); );
app.get ('/', funktion (req, res, nästa) // returnera bara färskdataklienten.get (currentDataKey, funktion (felaktig data) om (err) next (err); else res.send (data);););
app.listen (8012);"
Eftersom POSTing till en server kan vara svårt med en webbläsare, låt oss använda curl för att testa.
curl - data "dina data går här" --header "Content-Type: text / plain" http: // localhost: 8012 /
Ett snabbt bash script kan användas för att visa hur fyllningen av hela filteret ser ut:
bash #! / bin / bash för jag i 'Seq 1 500'; gör curl - data "data $ i" --header "Content-Type: text / plain" http: // localhost: 8012 / done
Titta på en fyllning eller ett fullständigt filter är intressant. Eftersom den här är liten kan du enkelt se den med redis-cli
. Genom att springa redis-cli få inaktuellt filter
från terminalen mellan att lägga till objekt ser du de enskilda bitarna ökar. Ett fullständigt filter kommer att vara \ xff
för varje byte. Vid denna tidpunkt kommer filtret alltid att returnera positivt.
Blomfilter är inte en panacea-lösning, men i rätt situation kan ett blomfilter ge ett snabbt och effektivt komplement till andra datastrukturer.