Category: Technology
Olen tässä lueskellut The Definitive Guide To MongoDB -nimistä kirjaa (Kindle), joka on hyvä katsaus MongoDB-tietokannan toimintaperiaatteisiin. Huomasin, että MongoDB:ssä on paljon sellaisia ominaisuuksia, joita ei tule välttämättä ajatelleeksi sitä pintapuolisesti kokeillessa.
Vaikka MongoDB käyttää JSONin kaltaisia BSON-tietorakenteita dokumenttien kuvaamiseen, se erottelee tietotyypit paljon tarkemmin kuin JSON. Tyypillinen esimerkki tästä ovat päivämäärät, joita JSON ei määrittee lainkaan erilliseksi tyypikseen. MongoDB tallentaa ne siten, että JavaScript-shellissä käsiteltyinä päivämäärät ovat automaattisesti Date-objekteja.
64-bittisyys haasteena
MongoDB erottelee toisistaan erilaiset numeeriset tyypit, joita JavaScript käsittelee aina liukulukuina. BSONissa mahdollisia tyyppejä ovat tavu (byte), 32-bittinen kokonaisluku (int32), 64-bittinen kokonaisluku (int64) sekä 64-bittinen liukuluku (double). Näiden kanssa on syytä olla tarkkana, sillä JavaScript-shellissä toimiessa kaikki luvut ovat oletuksena liukulukuja.
MongoDB:hen siis tallentuu aina liukuluku, kun JavaScriptistä käsin syötetään dataa. Tämä voi aiheuttaa ongelmia lähinnä suurissa lukuarvoissa, sillä osaa 64-bittisistä kokonaisluvuista ei suoraan vastaa mikään 64-bittinen liukuluku. JavaScript ei pysty käsittelemään esimerkiksi lukua 9223372036854770001, vaan se muuttuu muotoon 9223372036854770000.
Ratkaisuna ongelmaan MongoDB:n JavaScript-shell tarjoaa NumberLong-tyypin, jota voi käyttää 64-bittisten kokonaislukujen tallentamiseen ja käsittelyyn. Esimerkiksi näin:
db.tests.insert({intval:NumberLong('9223372036854770001')}) { "_id" : ObjectId("..."), "intval" : NumberLong("9223372036854770001") }
NumberLongia on käytettävä myös $inc-operaattorin kanssa, jotta lukuarvo säilyy kokonaislukuna:
db.tests.update({$inc:{intval:NumberLong('1')}}) { "_id" : ObjectId("..."), "intval" : NumberLong("9223372036854770002") }
Silloin kun MongoDB:hen tallennettua dataa käsitellään esimerkiksi Python-sovelluksesta käsin pymongo-kirjastolla, datatyypit menevät automaattisesti oikein, sillä Python ymmärtää eron liukulukujen ja kokonaislukujen välillä. Tyypiksi tulee automaattisesti int64 (NumberLong), jos lukuarvo on yli 32-bittinen.
ObjectId
MongoDB-dokumenttien tunnisteena käytettävä ObjectId on oma mielenkiintoinen tietorakenteensa. Useimmissa muissa tietokannoissa ID-arvot ovat kokonaislukuja tai merkkijonoja, mutta MongoDB käsittelee niitä objekteina.
ObjectId koostuu 12 tavusta, jotka esitetään yleensä heksadesimaalissa muodossa ObjectId('4e71009ce0395a172f000001'). Tavuilla on seuraavanlainen merkitys:
0-3: Timestamp (4e71009c) = päivämäärä 2011-09-14 22:29:32
4-6: Machine ID (e0395a) = MD5('MacBookAir.local') kolme ens. tavua
7-8: Process ID (172f) = 5935 ttys002 0:00.28 python
9-11: Counter (000001) = laskuri
Näistä ehkä mielenkiintoisin on aikaleima, josta voidaan siis aina päätellä, milloin jokin MongoDB:hen tallennettu dokumentti on luotu. MongoDB-shellissä leiman voi tarkistaa näin:
ObjectId('4e71009ce0395a172f000001').getTimestamp() ISODate("2011-09-14T19:29:32Z")
Eräs ObjectId-tunnisteiden tärkeimpiä ominaisuuksia on se, että niitä ei generoida palvelimella, vaan tietokanta-ajureissa. Toisin kuin esimerkiksi MySQL:n autoincrement-kentissä, sovellus tietää siis jo etukäteen, millä ID:llä dokumentti tullaan tallentamaan tietokantaan. Tämä tarkoittaa, että mitään LAST_INSERT_ID() -kikkailua ei tarvita relaatioiden luomiseen. Kaikki viittaukset dokumentien välille voidaan määritellä valmiiksi, ja sitten vain tallennetaan kaikki dokumentit kerralla.
MongoMapper is a great MongoDB ORM for Ruby on Rails. It basically allows you to use MongoDB as a drop-in replacement for MySQL/PostgreSQL.
However, it has one particular weakness: It is impossible to filter MongoDB queries by exact array values. MongoMapper converts array queries into $in operations, which means that all documents that contain any of the specified array values will be returned.
For example, assume you have two MongoDB documents:
{ "name": "A", "items": [1, 2, 3] } { "name": "B", "items": [1, 2] }
The following MongoMapper query will return both documents, even if you might have expected only the first one:
MyModel.where(:items => [1, 2, 3])
My $exact fork on GitHub
I've created a fork of plucky, the query engine used by MongoMapper, which adds a new pseudo operator called $exact. My fork can be found here:
kennu/plucky (master, based on v0.4.1)
kennu/plucky/tree/backport_exact_v3 (backported to v0.3.8)
I've also submitted my change set as a pull request to the author of plucky.
You can use the fork in your Rails Gemfile like this (you'll need the v0.3.8 version with current mongo_mapper):
gem 'plucky', :git => 'https://github.com/kennu/plucky.git', :branch => 'backport_exact_v3' gem 'mongo_mapper'
After running bundle install, you can specify queries in the this format:
MyModel.where(:items => { :$exact => [1, 2, 3] })
This query will return only the first document of the previous example above.
Remember, though, that you must keep your arrays in a specific order for these queries to work. An array of [1, 2, 3] won't match if you query it as [2, 3, 1].
Windows 7:llä on ongelmia kytkeytyä Mac OS X Lionin levyjakoihin oletusasetuksilla. Se väittää, että mikään käyttäjätunnus- ja salasanayhdistelmä ei kelpaa, vaikka samat toimisivat Linuxista käsin oikein.
Ongelma korjaantuu avaamalla Windowsista Regedit ja lisäämällä seuraava arvo asetuksiin:
Key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa Value name: LmCompatibilityLevel Value type: DWORD Value: 1
Uudelleenkäynnistyksen jälkeen Windows saa jälleen yhteyden Mac OS X:n levyjakoihin. Jos ongelmia vielä on, kannattaa tarkistaa, että yhteyttä yritetään oikealla domainilla/workgroupilla. Oletuksena Windows 7 käyttää jostain syystä Windows-koneen omaa nimeä eikä palvelimen nimeä.
Otin koemielessä käyttöön Nginx:n HttpLimitReq-moduulin. Sillä voi helposti lisätä webbisaitin haluttuun osaan rajoituksen, joka estää yksittäistä käyttäjää latailemasta sivuja liian nopeasti. Omassani on nyt rajoituksena 1 req/s tietyille sivuille.
Nginxin rajoitus toimii siten, että sivut alkavat latautua hitaammin, kun niitä pommittaa. Kun pommitusta jatkaa riittävästi (rinnakkaisilla yhteyksillä), alkaa saada 503 Service Unavailable -ilmoitusta.
Blogissani on kuitenkin käytössä myös Varnish, joka välimuistittaa useimmat sivut, joten niitä rajoitus ei koske. Ideana on, että ainoastaan Djangolle asti raskaaseen käsittelyyn menevät pyynnöt rajoitetaan.
Amazonin AWS-pilvipalvelussa on tiukka politiikka sähköpostin lähettämisen suhteen. Amazon sulkee portin 25 melko nopeasti, jos joltain virtuaalikoneelta lähetellään vähänkään enemmän sähköpostia. Näin käy helposti, mikäli esimerkiksi cron jobeissa ilmenee virheilmoituksia.
Eräs helppo tapa kiertää Amazonin rajoitukset on pystyttää erillinen välityspalvelin Amazonin ulkopuolelle ja asettaa se kuuntelemaan esimerkiksi porttia 2525. Kaikki postit ohjataan ensin välityspalvelimelle, joka sitten toimittaa ne edelleen oikeille vastaanottajille.
Välityspalvelin
Ubuntun Postfix voidaan määritellä kuuntelemaan porttia 2525 kopioimalla ja muokkaamalla ensimmäinen rivi:
/etc/postfix/master.cf:
smtp inet n - - - - smtpd 2525 inet n - - - - smtpd
Restartin jälkeen Postfixiin voi ottaa yhteyttä molempien porttien kautta. Tämän jälkeen on vielä muokattava yhtä riviä, joka määrittelee, mistä IP-osoitteista voi välittää sähköpostia tämän palvelimen kautta. Rivin loppuun lisätään esimerkiksi oma kiinteä Elastic IP -osoite:
/etc/postfix/main.cf:
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 x.x.x.x/32
Lähettävä palvelin
Amazonin virtuaalikoneen puolelle määritellään lopuksi tämä kyseinen välityspalvelin, jolloin kaikki postit alkavat kulkea sen kautta:
/etc/postfix/main.cf:
relayhost = [my.smtp.server.example.com]:2525
Laajemmassa installaatiossa kannattaa harkita sellaista järjestelyä, jossa Amazonissa pyörii yksi keskitetty sähköpostipalvelin, joka sitten välittää sähköpostit Amazonin ulkopuolella olevalle välityspalvelimelle. Tällöin pääsyä on helppo hallita Security Groupien avulla, ja ainoastaan yksi palvelin tarvitsee kiinteän Elastic IP:n.
There are a lot of guides on the web that instruct you to set up your Nginx FastCGI sites like this for Zend Framework or similar frameworks, where a single index.php script handles all requests:
server {
listen 8080;
root /var/www/public;
index index.php;
try_files $uri /index.php;
location ~ \.php$ {
fastcgi_pass unix:/tmp/php-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
}
The problem here is that the query string is lost. The FastCGI process is passed an empty QUERY_STRING variable.
It's easy to fix this by changing the try_files directive as follows:
try_files $uri /index.php?$query_string;
Now the query string is retained and passed correctly to FastCGI. As a result, Zend Framework will also be able to parse it properly.
PS. If you use this configuration, remember to protect your user uploads, so that nobody can upload and execute an arbitrary PHP script.
Ubuntun käyttämä Debianin APT-pakettijärjestelmä toimii oletuksena interaktiivisesti. Kun uusi paketti asennetaan apt-get install -komennolla, se kyselee usein käyttäjältä erilaisia tietokanta-asetuksia ja muita preferenssejä. Tämä hankaloittaa pakettien asentamista automaattisesti esimeriksi Puppetilla.
Ongelma on ratkaistavissa preseedaamalla tarvittavat asetukset debconfille etukäteen. Puppet tukee preseedausta suoraan package-direktiivisessä.
Ideana on asentaa paketti kerran käsin niin, että tarvittavat asetukset tallentuvat muistiin. Asetukset esimerkiksi phpbb3-paketille saa sitten näkyviin näin:
sudo debconf-get-selections | grep phpbb3
Asetuksia on todennäköisesti aika paljon, joten voi käyttää harkintaa siinä, tarvitaanko välttämättä kaikkia preseedaukseen. Itse kokeillessani nämä kaksi riviä riittivät phpbb3:n tapauksessa:
phpbb3 phpbb3/httpd multiselect apache2 phpbb3 phpbb3/dbconfig-install boolean false
Preseedatut asetukset voi ottaa käyttöön ainakin kahdella eri tavalla. Ne voi laittaa väliaikaistiedostoon ja ottaa käyttöön komennolla:
sudo debconf-set-selections /tmp/phpbb3.preseed
Tai jos käyttää Puppetia, niin package-direktiiviin voi littää responsefile-attribuutin osoittamaan samaan tiedostoon (joka pitää tietysti ensin luoda toisella direktiivillä):
package { 'phpbb3':
ensure => installed,
responsefile => '/tmp/preseed.phpbb3',
require => File['/tmp/preseed.phpbb3'],
}
Tällaisen asennuksen jälkeen täytyy sitten muistaa vielä luoda tarvittavat konfiguraatiotiedostot, jos niiden automaattisen (interaktiivisen) luomisen on ohittanut paketin asennusvaiheessa.
