Bir Neo4j denemesi: Twitter takipçi grafı

Neo4j ile ilk karşılaşmam 2009 yılında olmuştu. O sıralar -henüz badem bıyıklılarla dolmamış en bir bilimsel ve teknolojik kurumumuzda- çeşitli veritabanlarına ait kullanım istatistiklerini saklamak üzere embed edilebilir bir veritabanı arıyordum. Bu süreçte bir hafta kadar bir süre Neo4j’e şans versem de gerek disk alanı, gerekse performans sorunları yüzünden BerkeleyDB’ye geçiş yapmıştım. Şimdi dönüp baktığımda asıl sorunun bir graf(Türkçe karşılığı çizge olarak geçiyor ama benim kulağıma pek aşina gelmedi. Bilen birileri beni düzeltirse sevinirim.) veritabanının ne amaçla kullanılacağını o zamanlar bilmeyen bende olduğunu düşünüyorum.

Geçen zaman içerisinde Neo4j büyük bir gelişme gösterdi ve yaygınlaştı. Ben de bu popüler veritabanını tekrar kurcalamak istedim. Örnek uygulamamda kendi Twitter takipçilerimden başlayarak bir takipçi grafı oluşturmaya karar verdim. Uygulama benim takipçilerimi çekecek ve ve aramızda FOLLOWS ilişkisi ile Neo4j veritabanına kaydedecek. Daha sonra takipçilerimin takipçilerini çekecek ve aynı işlemi veritabanında olmayan kişi kalmayana dek devam ettirecek.

İşe önce Neo4j’i indirerek başladım. Konsol aracılığıyla sunucuyu herhangi bir sorunla karşılaşmadan başlattım. neo4j-shell ile sunucuya bağlanıp komutları biraz kurcaladım. Daha sonra dokümantasyonda gezerken Neo4j’in bir REST API sağladığını fark ettim. Benzer bir REST API‘yi Twitter da sağladığı için uygulamayı bu API’leri kullanarak JavaScript ile geliştirmeye karar verdim.

Twitter, istenilen kullanıcının takipçilerinin ID listesini döndüren bir API sunuyor.


http://api.twitter.com/1/followers/ids.json?user_id=...

Belki tek sıkıntı saatte 150 sorgu sınırının olması. Yazdığım kod bu API aracılığıyla her bir kullanıcının takipçilerini çekiyor ve createNode fonksiyonu ile Neo4j içinde her bir kullanıcı için bir düğüm oluşturuyor. Kullanıcıların tekrarlanmasını engellemek için Neo4j’in unique indexlerinden faydalandım. İndexlenmek istenen veri ilk kez indexleniyorsa API’nin kendisi otomatik olarak bir düğüm oluşturuyor. Aksi halde yeni düğüm oluşturulmuyor. 7474. portta çalışan bir Neo4j sunucusu olduğunu varsayan kodu aşağıda paylaşıyorum.

Mevcut kod kullanıcıların yalnızca ID’lerini çekiyor. Tüm graf oluştuktan sonra ayrı bir kodun bu ID’lere ait ayrıntıları çekip düğümleri güncellemesini planlıyorum. Böylece aynı anda en çok 100 kullanıcının ayrıntılarını döndürebilen Twitter API’sini de etkin kullanmış olacağım.

Elde bu graf olduktan sonra iki düğüm arasındaki en kısa yolu hesaplamak Neo4j ile oldukça kolay. Ayrıca graf, centrality açısından da kolayca incelenebilecektir diye tahmin ediyorum.

Senkronize olmayan java.util.Set gerçeklemelerinin performans karşılaştırması

Yine bir Pazar günü evde aylaklık edip uzun zamandır birikmiş olan Twitter favorilerimi eritmeye çalışırken hangi durumda hangi java.util.Set gerçeklemesinin kullanılması gerektiği konusunda net bir fikrim olmadığını farkettim. Bu sebeple senkronize olmayan java.util.Set gerçeklemelerini performans açısından karşılaştırarak dokümantasyonlarında yazan bilgileri doğrulamak istedim. Yaptığım testte HashSet, LinkedHashSet ve TreeSet için temel Set işlevlerini(add, remove, contains, size ve iterator) 1,000,000 kez tekrarladım ve geçen süreyi ölçtüm. Elde ettiğim sonuçlar bu üç gerçeklemenin de dokümantasyonlarında yazdığı şekilde çalıştığını gösterdi. Sonuçları aşağıdaki tabloda paylaşıyorum. Süreler µs(1/1000ms) cinsindendir.

add remove contains size iterator
HashSet 2039583 970396 1032553 4176 44500
LinkedHashSet 2158727 987442 1073558 11339 34027
TreeSet 4754283 4287016 4394679 12125 114829

Tabloyu şöyle özetleyebiliriz. HashSet ve LinkedHashSet performansı birbirine oldukça yakın. Fazladan yönetilmesi gereken LinkedList sebebiyle LinkedHashSet add, remove ve contains işlevlerinde HashSet’e göre çok az da olsa kötü performans sergiliyor. Buna karşılık LinkedHashSet’in iterator performansı HashSet’e göre %30 kadar daha iyi. TreeSet ise gerçekten Set elemanlarının sıralanması istenmiyorsa yanına yaklaşılmaması gereken bir gerçekleme olarak kendini gösteriyor.

Bu sonuçlara bakarak ben varsayılan gerçekleme olarak LinkedHashSet ile yoluma devam etmeye karar verdim. Başka bir Pazar günü de senkronize olan gerçeklemelere bulaşmaya çalışacağım.

Bu arada http://implement.asyonturkcedegil.com/

Güncelleme
Ahmet A. Akın ve Mehmet D. Akın’ın önerileriyle test kodunu ve sonuç analizini güncelledim. Testleri art arda 3 kez çalıştırıp sonuçları add, remove ve contains için ayrı tablolarda gösterdim. Süreler ms cinsindendir. Denemek isterseniz Ahmet A. Akın’ın biraz daha geliştirdiği test kodu burada.

add testi Test 0 Test 1 Test 2
HashSet 1021 563 697
LinkedHashSet 978 984 800
TreeSet 3830 3569 3837
remove testi Test 0 Test 1 Test 2
HashSet 489 497 686
LinkedHashSet 749 789 550
TreeSet 3786 3440 3291
contains testi Test 0 Test 1 Test 2
HashSet 264 261 418
LinkedHashSet 359 432 334
TreeSet 3331 3247 3245

 

Ahmet A. Akın:

Eğer ciddi bir bellek ve performans kaygım yoksa,
- Ekleme sırası önemsiz ise HashSet
- Ekleme sırası önemli ise LinkedHashSet
- Nesnelerin doğal sıralanışı önemli ise TreeSet
gönül rahatlığı ile kullanıyorum. Yani bu durumda hızdan çok işe göre yapıyı seçmek daha makul. Özel durumlar için ise özel kütüphaneleri tercih edilebilir. Guava, muhtelif primitive collections vs.

Mikrobenchmarking’i bu devirde doğru yapmak neredeyse mümkün değil (http://www.parleys.com/#id=2103&st=5 http://wiki.jvmlangsummit.com/images/1/1d/PerformanceAnxiety2010.pdf). O nedenle kafayı küçük farklara takmamak iyidir derim. Sonuçta farklı işletim sistemleri, işlemciler ve JVM sürümleri ile testler yapılıyor.

Mehmet D. Akın:

size() metodunu olcmek cok anlamli degil cunku hepsi de sadece nesnenin icindeki size ismindeki bir integerin degerini donduruyor, rakamlardaki farklilik gürültuden ibaret.

Microbenchmarking cok ince bir is, ozellikle Java gibi JVM userinde calisan ve GC iceren bir ortamda islemci, isletim sistemi, JVM versiyonu ve daha baska bir cok etken sonuclari degistirebiliyor.

Herkes RESTful Web API tasarlayabilir

Herkes RESTful Web API tasarlayabilir ama bazı kurallara uymak koşuluyla…

Geçenlerde Twitter’da kaynağını tam olarak bilemediğim fakat çok beğendiğim bir tweet dolaştı.

HTTP response codes for dummies. 50x: we fucked up. 40x: you fucked up. 30x: ask that dude over there. 20x: cool.

Kısmen usturuplu Türkçe çevirisi şöyle:

Zor öğrenenler için HTTP durum kodları. 50x: biz sıçtık. 40x: sen sıçtın. 30x: şu karşıdaki elemana sor. 20x: tamamdır.

HTTP durum kodları bundan daha iyi anlatılamazdı sanırım. Bu tweetten bir zaman sonra Karl Seguin‘in şu tweeti geldi. Kısaca söylediği şuydu: web servisiniz hataları neden 200 durum kodu ile döndürüyor?

Bu iki tweet RESTful Web API geliştiricileri için önemli ipuçları veriyor. Ben de kendi tecrübelerim ışığında bir RESTful Web API’nin nasıl tasarlanması gerektiğinden bahsetmeye çalışacağım. Anlatacaklarımın %100 geçerli ya da doğru olmasını beklemek büyük bir yanılgı olur. Bu sebeple bu yazıyı bir tür başlangıç noktası olarak değerlendirmenizi öneririm.

Hangi durumda hangi HTTP metodu kullanılmalı?

HTTP Metodu Açıklama
HEAD HEAD metodu ile yapılan isteklere verilen yanıt içeriği boştur. Geriye yalnızca HTTP Yanıt Başlıkları ve durum kodu döner. Bu metod ile servis sağlayıcı hakkında bilgi alınabilir ya da bir kaynağın varlığı doğrulanabilir.
GET GET metodunu bir kaynağın ayrıntılarına ulaşmak için kullanabilirsiniz. Buna örnek olarak 1 ID’li kullanıcının bilgilerini verebiliriz.
POST POST metodu ile servis sağlayıcı üzerinde yeni bir kaynak oluşturabilirsiniz. Yine örnek olarak yeni bir kullanıcı oluşturmayı verebiliriz. Kaynağı güncellemek için ben PUT metodunu öneriyorum. Bu sebeple genelde POST metodu ile kaynak ID’sini göndermeye gerek yoktur. ID, servis sağlayıcı tarafından oluşturulmalıdır.
PUT PUT metodu ile servis sağlayıcı üzerindeki bir kaynağı güncelleyebilirsiniz. Hangi kaynağın güncelleneceğini belirtmek için kaynağın ID’si servis sağlayıcıya gönderilmelidir.
DELETE DELETE metodunu bir kaynağı silmek için kullanabilirsiniz. Hangi kaynağın silineceğini belirtmek için kaynağın ID’si servis sağlayıcıya gönderilmelidir. ID belirtilmezse tüm kaynakların silinmesi de sağlanabilir. Ancak bu durum dikkatle gerçeklenmelidir.

HTTP metodlarını kullanırken dikkat edilmesi gereken belki en önemli nokta HEAD ve GET metodlarının salt okunur olması gereğidir. Başka bir deyişle HEAD ve GET metodları ile kaynak bilgilerini güncellememeli ya da yeni kaynak oluşturmamalısınız. Yazma işleri için POST, PUT ve DELETE metodlarını kullanmalısınız. Diğer yandan PUT ve DELETE metodları idempotent metodlardır. Bu sebeple art arda çağrılmaları sorun oluşturmaz/oluşturmamalıdır.

URI’ler nasıl olmalı?

Google’da arama yaparsanız bu konuda pek çok farklı öneriye/gerçeklemeye ulaşabilirsiniz. Ben de kendi önerilerimi sıralayacağım. Wikipedia’da yapılan açıklama oldukça güzel. Benim önerilerim de bu tabloya benzer olacak.

URI’leriniz ID dışında parametre içermemeli.
Parametreleri URI içine yazdığınızda URI’leriniz yönetilemez bir hale gelecektir. Bir örnek vereyim:

GET /api/{TICKET}/{DOMAIN}/users/{ROLE}/

Bu API ile belli bir etki alanında(DOMAIN) belli bir role(ROLE) sahip olan kullanıcıların sorgulandığını varsayalım. TICKET parametresinin ise güvenlik amacılığıyla gönderilen bir anahtar olduğunu kabul edelim. Bu URI’nin istemci tarafında oluşturulması oldukça zordur. Bu sebeple ben ID parametresi dışındaki parametrelerin URI’de bulunmasını pratikte çok uygun bulmuyorum. Buna bir istisna olarak örnekteki TICKET parametresini verebilirim. Eğer TICKET parametresi her URI’de bulunacak ise (bu tür parametrelere statik parametre diyorum) bu URI’lerin istemci tarafında ortak bir kod ile oluşturulması pek zor olmayacaktır. Bunun diğer bir faydası da TICKET içermeyen istekler için doğrudan HTTP 404 döndürülebilmesine imkan sağlamasıdır. Sonuç olarak önerilerim doğrultusunda yukarıdaki URI şu hale gelmelidir.

GET /api/{TICKET}/users?domain=...&role=...

URI’lerde büyük harf kullanılmamalı.
Bu önerimin nesnel bir açıklaması yok ama genel kullanımda /dailySessions yerine /daily_sessions ya da /sessions/daily kullanımı daha düzgün ve kolay görünüyor.

Hangi durumda hangi HTTP durum kodu kullanılmalı?

Yazının başında verdiğim tweet örneği durum kodlarını oldukça iyi açıklıyor. 50x durum kodları servis sağlayıcı tarafında bir hata olduğuna, 40x durum kodları istemci tarafında bir hata olduğuna işaret ediyor. 20x durum kodları ise isteğin başarıyla sonuçladığını ifade ediyor. Bu sebeple hata mesajları asla ve asla 20x durum kodu ile gönderilmemelidir. Hata türüne uygun durum kodu seçilerek hata mesajı bu durum kodu ile istemciye gönderilmelidir. Oldukça fazla sayıda durum kodu olsa da ben 200, 400, 403, 404 ve 500 durum kodlarını yeterli buluyorum. Ama belli durumlarda diğer durum kodlarını kullanma gereği olabileceğini de aklınızdan çıkarmayın derim.

HTTP Durum Kodu Açıklama
200 İşlemin başarıyla gerçekleştiğini belirtir. Yalnızca başarı durumunda kullanılmalıdır.
400 İsteğin geçersiz olduğunu belirtir. Eksik bir parametre ya da parametrenin hatalı olması durumunda kullanmanızı öneririm.
403 İstemcinin bu kaynağa erişiminin yasak olduğunu belirtir. Kendi kendini açıklıyor.
404 İstenen kaynağın bulunamadığı anlamına gelir. Örnek olarak belirtilen ID’ye sahip kullanıcı yok ise bu kodu kullanabilirsiniz.
500 Servis sağlayıcıda beklenmeyen bir hata olduğunda bu kodu kullanmalısınız.

Durum kodu ile birlikte hata mesajı gönderme zorunluluğunuz bulunmasa da hata mesajı vermek API kullanıcılarının işini oldukça kolaylaştıracaktır. Bu sebeple 400, 403, 404 ve 500 durum kodları ile birlikte mutlaka hata mesajı dönmenizi öneririm.

Yanıt hangi formatta olmalı?

Yanıt için yeni bir format keşfetmenize inanın hiç gerek yok. JSON formatı hem güzel, hem parse edilmesi kolay, hem de çok fazla kütüphane desteğine sahiptir. Bu sebeple yanıtlarınızı aksi çok gerekli olmadıkça JSON formatında dönmenizi öneririm.

JSONP ve CORS

Bu iki yöntemin ayrıntılarına burada değinmeyeceğim ama geliştirdiğiniz API Javascript istemciler ile kullanılacaksa iki yönteme de destek vermeniz yerinde olacaktır.

Buraya kadar sabırla okuduğunuz için teşekkür ederim. Umarım faydalı olmuştur.

Akka ya da Java ExecutorService ve Hazelcast ile kullanıcı bildirim kuyruğu

Son zamanlarda Hazelcast’i dağıtık bir bildirim kuyruğu olarak kullanmak üzerine kafa yoruyorum. Amacım, farklı node’ların sistem çapındaki herhangi bir kullanıcı için üreteceği bildirimleri ortak bir kuyrukta tutmak ve bu bildirimleri gerekli zamanlarda işleyerek ilgili kullanıcıya ulaştırmak. Burada gerekli zaman ile bir kullanıcı için bildirim kuyruğu uzunluğunun belli bir sayıyı geçmesi ya da bu kullanıcı için kuyruktaki en eski bildirimin belli bir zamandan önce oluşturulmuş olmasını kastetmekteyim. Bu doğrultuda bir yandan hangi Hazelcast veri yapısını kullanacağımı düşünürken bir yandan da yazma işlemini ne şekilde yapabileceğimi araştırıyorum.

Önceleri bildirimleri saklamak için Queue kullanmayı düşündüm. Fakat yukarıda belirttiğim ihtiyaçları değerlendirince her kullanıcı için ayrı bir kuyruğa sahip olmam gerektiğini fark ettim. Her kullanıcı için ayrı bir Queue oluşturmak ve bunları yönetmek çok maliyetli olacağından ihtiyacıma en uygun veri yapısının MultiMap olduğuna kanaat getirdim. MultiMap içinde kullanacağım anahtarlar kullanıcı adına(username), değerler de bildirimlere(Notification) karşılık gelecek. Bir kullanıcı için birden fazla bildirim olabilecek.

Sonraki adımda bildirim kuyruğundaki bildirim sayısı belli bir değerin üstünde ve en eski bildirimin oluşturulma tarihi belli bir zamandan önce olan kullanıcıları en performanslı şekilde nasıl elde edebileceğimi düşündüm. Hazelcast, Map içindeki değerleri sorgulayabilmemiz için SQL benzeri bir DSL sağlıyor. Yalnız bu özellik MultiMap için geçerli değil. Bu sebeple veriyi denormalize etmeye ve kullanıcı kuyruklarına ait bilgileri(her bir kullanıcı kuyruğu için eleman sayısı ve en eski bildirim zamanı) ayrı bir Map içinde tutmaya karar verdim.

MultiMap içine yazacağım bildirimler için bir Notification sınıfı oluşturdum.

Her bir kullanıcı için kuyruk bilgilerini saklamak amacıyla oluşturduğum QueueMetaData sınıfı ise şöyle:

Daha sonra, bir ExecutorService kullanarak 6 farklı kullanıcı için 16384 adet rastgele bildirim oluşturan bir kod yazdım. Buradaki belki en önemli problem Map ve MultiMap için senkronizasyonu sağlamak idi. Hazelcast’in sağladığı anahtar bazlı lock mekanizması yaptığım testlerde sorunsuz çalıştı. Yazma işleminin ayrıntısı için aşağıdaki Producer koduna bakabilirsiniz.

Her bir Producer opCount kadar Notification oluşturmakta. Serialization için JSON kullanmaya karar verdim. Bu sebeple google-gson kütüphanesinden faydalandım. Farklı sayıda Producer ile testler yaptım. Sonuçları yazının sonunda paylaşacağım. Producer sınıfının çalışabilmesi için gerekli olan LatchWorker ve HazelcastManager sınıflarının kodları aşağıda.



Son olarak yazma işlemini Akka ile de denemeye karar verdim. Bundaki amacım hem Akka’yı öğrenmek, hem de performansını ExecutorService ile karşılaştırmaktı. Producer sınıfına benzer şekilde bir Producer aktörü yazdım.

Producer aktörlerini yönetmek için yazdığım Master aktörü aşağıda. Bu aktörün görevi belirtilen sayıda Producer oluşturarak bunların opCount kadar Notification oluşturmalarını sağlamak.

16384 adet bildirim için farklı thread/actor sayıları ile Akka ve ExecutorService yöntemlerini karşılaştırdım ve işlem sürelerini ölçtüm. Süreler µs(1/1000ms) cinsindendir. Sonuçlar aşağıdaki tabloda yer almakta.

ExecutorService Akka
1 thread/actor 21173751 16442245
2 thread/actor 15759791 12487179
4 thread/actor 13337575 9462002
8 thread/actor 11633355 8432141
16 thread/actor 11394124 8362550

Eğer bariz bir hata yapmıyorsam Akka yöntemi hem yönetim hem de performans açısından ExecutorService yöntemine göre daha önde. Hata bildirimi/öneri için yorumlarınızı beklemekteyim.