dersblog

NoSQL

NoSQL tabanları ilişkisel veri yapısı (SQL ile erişilen türden) gerektirmeden veri sorgulamayı, eklemeyi destekleyen tabanlardır. Bir bakıma diske yazılmış Python sözlüğü gibi görülebilirler; tek ihtiyaç duydukları bir anahtar ve ona tekabül eden değerdir, ki değer herhangi bir obje olabilir. Mimarinin çekici tarafı veri ünitesini tek objeye indirdediğimizde dağıtık ortamda çalışmanın kolaylaşması, ölçeklemek için belli objeleri belli makinalara gönderdiğimizde problem çözülüyor, çünkü ilişkisel bağlantıları takip etmek gerekmiyor.

Basit, dağıtık çalışabilen bir NoSQL taban tasarımı şöyle olabilir.

Servis

Basit bir Flask servisi altta,

from flask import Flask, url_for, jsonify, request
import sys, os, sqlite3

app = Flask(__name__)

class OnlyOne(object):
    class __OnlyOne:
        def __init__(self):
            self.conn = None
        def __str__(self):
            return self.val
    instance = None
    def __new__(cls):
        if not OnlyOne.instance:
            OnlyOne.instance = OnlyOne.__OnlyOne()
        return OnlyOne.instance
    def __getattr__(self, name):
        return getattr(self.instance, name)
    def __setattr__(self, name):
        return setattr(self.instance, name)

@app.before_first_request
def start_check_db():
    sno = app.config.get('server_no')
    db = "/opt/Downloads/kvf-%d.db" % int(sno)
    conn = sqlite3.connect(db)
    OnlyOne().conn = conn
    if not os.path.isfile(db): 
        c = OnlyOne().conn.cursor()
        res = c.execute('''CREATE TABLE OBJ (key TEXT PRIMARY KEY, value TEXT); ''')
        c.execute('''INSERT INTO OBJ(key,value) VALUES(?,?)''', ("test1","value1"))
        conn.commit()    
    print ('server', sno)

@app.route('/get', methods=["PUT", "POST"])
def get():    
    data = request.get_json(force=True)   
    key = data['key']
    c = OnlyOne().conn.cursor()
    c.execute("SELECT value FROM OBJ WHERE key = ?", [key])
    res = list(c.fetchall())
    if len(res)==0: 
        return jsonify({'result': "None"})
    return jsonify({'result': res[0][0]})

@app.route('/set', methods=["PUT", "POST"])
def set():    
    data = request.get_json(force=True)   
    key = data['key']
    value = data['value']
    c = OnlyOne().conn.cursor()
    c.execute("insert or replace into OBJ (key,value) values (?,?)", [key,value])
    OnlyOne().conn.commit()
    return jsonify({'result': "OK"})

@app.route('/remove', methods=["PUT", "POST"])
def remove():    
    data = request.get_json(force=True)   
    key = data['key']
    c = OnlyOne().conn.cursor()
    c.execute("delete from OBJ where key = ?", [key])
    OnlyOne().conn.commit()
    return jsonify({'result': "OK"})

if __name__ == "__main__":
    app.config['server_no'] = sys.argv[1]
    app.run(host="localhost", port=8080)   

start_check_db ile taban yoksa başlangıçta yaratılır.

Tabana tek bir bağlantı vardır, o bağlantı OnlyOne tekil obje (singleton) içinde muhafaza ediliyor. Arkadaki sqlite tabanına eşzamanlı erişim olmadığı için birden fazla bağlantıya da gerek yok.

Not: Fakat baz taban daha kuvvetli bir taban seçilirse (bir tablonun tamamını kitlemeden ayrı satırlara paralel erişim desteği sağlayan türden) birden fazla taban bağlantısı idare edilebilir. Bunun için üstteki kodda ufak bir değişim yeterli (tekil obje yerine bir bağlantı havuzu kullanımı mesela)

Metot set içinde insert or replace into komutunu kullandık, bu SQL çağrısı bir satır varsa onu günceller yoksa yeni satır ekler, İngilizce deyimle insert ve update karışımı olan "upsert" işlemini yapar. Bu depolama tarzı da yine hafıza sözlük kullanımını andırıyor, aynen bir Python sözlüğünde d['key'] = 'değer' çağrısının önceden varsa key değerlerini ezecek (yoksa ekleyecek) olduğu gibi, taban bazlı yaklaşım da bu işlevi taklit ediyor.

Not: SQL kullanmayan tabanın arka planda bir SQL tabanı SQLite kullanıyor olması çelişki gibi durabilir fakat aslında değil; bu tabanı sadece basit bir depo olarak kullanıyoruz, ve bunu çok basit düz SQL komutları üzerinden yapıyoruz. Unutmayalım SQL sadece ekleme, silme gibi "CRUD" operasyonlarından ibaret değil, ilişkisel veriler literatürü çok daha kapsamlı. NoSQL dışarıya bu tür ilişkisel bir arayüz vermiyor, arka planda da SQL basit depolama amaçlı olarak kullanılıyor ve onun ilişkisel özelliklerine dokunulmuyor.

Servisi başlatırken üstteki script'e komut satırından 0,1,2.. gibi bir sayı veririz, bu sayı o servisin no'su olur. Bu no taban ismine eklenecektir (böylece aynı makinada bile farklı taban süreçleri yanyana işleyebilir). Bu sayıyı servis tarafında bir makina:port listesi içinden birini seçmesi için de kullanabiliriz. Bağlanan tarafta anahtar üzerinde mod N işletildikten sonra elde edilen sayı aynı listeden bir makina:port seçip ona bağlanırdı.

Şimdi REST arayüzüne curl ile erişelim, mevcut olan test1 verisine erişim,

! curl -H "Content-Type: application/json" -d '{"key":"test1"}'  http://localhost:8080/get
{"result":"value1"}

Olmayan bir anahtara erişim,

! curl -H "Content-Type: application/json" -d '{"key":"asdjflkajsf"}'  http://localhost:8080/get
{"result":"None"}

Bir değeri set edip onun geri almak,

curl -H "Content-Type: application/json" -d '{"key":"3333", "value":"value333"}'  http://localhost:8080/set
curl -H "Content-Type: application/json" -d '{"key":"3333"}'  http://localhost:8080/get

Python İstemci

Üstteki curl tipi ham JSON çağrıları pür Python ile de yapabilirdik,

import requests
response = requests.post('http://localhost:8080/get', json={"key":"test1"})
print("Status code: ", response.status_code)
print(response.json())
Status code:  200
{'result': 'value1'}

Şimdi REST çağrılarını üç ayrı fonksiyon ile sarmalayalım, normal durumda kullanıcıların çağıracağı tek metotlar bunlar olacak.

import requests, pickle, base64

def get(key):
    response = requests.post('http://localhost:8080/get', json={"key":key})
    print("Status code: ", response.status_code)
    res = response.json()
    res = pickle.loads(base64.decodestring(res['result'].encode('utf-8')))
    return res

def set(key,value):
    value = base64.encodestring(pickle.dumps(value)).decode()
    response = requests.post('http://localhost:8080/set', json={"key":key,"value":value})
    print("Status code: ", response.status_code)
    res = response.json()

def remove(key):
    response = requests.post('http://localhost:8080/remove', json={"key":key})
    print("Status code: ", response.status_code)
    res = response.json()


set("2324","33333ddddd3")
o = get("2324")
print (o)
Status code:  200
Status code:  200
33333ddddd3

Üstteki örnekte değer olarak basit string gönderdik ama tasarım öyle ki karmaşık objelere de izin veriyor,

m = np.random.rand(2,2)
set("randomId123",m)
Status code:  200
o = get("randomId123")
print (o)
Status code:  200
[[0.21365823 0.42896852]
 [0.31330469 0.03924181]]

Bir Numpy matrisi gonderdik NoSQL onu bile tabana yazdi.

Taban içeriğine bakarsak objelerin nasıl yazılmış olduğunu görüyoruz,

import sqlite3
c = sqlite3.connect("/opt/Downloads/kvf-0.db" )
cur = c.cursor()
res = cur.execute("SELECT key,value FROM OBJ")
for x in cur.fetchall():
   print (x)
('test1', 'value1')
('2324', 'gANYCwAAADMzMzMzZGRkZGQzcQAu\n')
('randomId123', 'gANjbnVtcHkuY29yZS5tdWx0aWFycmF5Cl9yZWNvbnN0cnVjdApxAGNudW1weQpuZGFycmF5CnEB\nSwCFcQJDAWJxA4dxBFJxBShLAUsCSwKGcQZjbnVtcHkKZHR5cGUKcQdYAgAAAGY4cQiJiIdxCVJx\nCihLA1gBAAAAPHELTk5OSv////9K/////0sAdHEMYolDIPATky0nWcs//JC9Xzh02z+EpHgZLw3U\nP7ATIoOAF6Q/cQ10cQ5iLg==\n')

Karmaşık bazı string değerleri görüyoruz, bunlar Python objelerinin kodlanmış hali.

Birkaç servisli durumdan bahsedelim; daha önce değinildiği gibi birden fazla servise bağlanmak için key üzerinde mod kullanabiliriz, mesela

N = 3 # bu kadar servis var
print (hash("test1"))
print ('servis',hash("test1") % N)
print ('servis',hash("111122") % N)
print ('servis',hash("323") % N)
-663401633428333736
servis 1
servis 2
servis 0

Bu indis değerleri set ve get içinde kullanılırdı, servise bağlantı yapılmadan önce hangi URL kullanılacağını bulmak için. Eğer 0 alındıysa mesela,

http://localhost:8080/get

1 alindiysa

http://localhost:8081/get

bağlantısı yapılıyor olabilirdi, bu karar değiştirilmiş get, set üzerinde verilecektir.

Kaynaklar

[1] [Obje String Olarak Kodlamak](../../2010/10/encoding-objeleri-yazip-okumak-pickle-base64.html)

[2] [MongoDB](../../2014/05/mongodb.html)


Yukarı