Charon a cesta z pickle pekla

Peter Bábics

Prehľad

  • Pickle
  • Charon
  • Reálne nasadenie

Pickle

Pickle

  • Modul v štandardnej knižnici
  • Rýchly spôsob uloženia dát
  • Binárny formát
  • Priamočiare použitie
In [2]:
@attr.s
class SimpleDataClass:
    number = attr.ib(type = int)
    string = attr.ib(type = str)
    float_ = attr.ib(type = float)
    decimal_ = attr. ib()
    object_ = attr.ib()
    
@attr.s
class Data:
    obj = attr.ib()

Príklad

In [3]:
data = SimpleDataClass(2018, 'PyConSK', 3.1459, decimal.Decimal('1.414'), Data(42))
data
Out[3]:
SimpleDataClass(number=2018, string='PyConSK', float_=3.1459, decimal_=Decimal('1.414'), object_=Data(obj=42))
In [4]:
pickled_object = pickle.dumps(data)
deserialized = pickle.loads(pickled_object)
In [5]:
deserialized
Out[5]:
SimpleDataClass(number=2018, string='PyConSK', float_=3.1459, decimal_=Decimal('1.414'), object_=Data(obj=42))
In [6]:
data == deserialized
Out[6]:
True

Nevýhody

  • Remote code execution
  • Chýbajúca podpora migrácií - zmeny v kóde
  • Ťažko opravitelný pri poškodení

Príklad - Remote Code execution

In [7]:
class HackyHacky:
    def __init__(self) -> None:
        self._unused_value = 42 

    def __setstate__(self, state: Dict[str, Any]) -> None:
        print('Execute order 66')
        for k,v in state.items():
            attribute = getattr
            setattr(self, k, v)
In [8]:
remote_code = pickle.dumps(HackyHacky())
In [9]:
x = pickle.loads(remote_code)
Execute order 66

Príklad - Migrácie

In [10]:
@attr.s
class User:
    name = attr.ib()
In [11]:
user = User('Gejza')
user
Out[11]:
User(name='Gejza')
In [12]:
stored_user = pickle.dumps(user)
In [13]:
restored_user = pickle.loads(stored_user)
restored_user
Out[13]:
User(name='Gejza')

Po čase...

In [14]:
@attr.s
class User:
    name = attr.ib()
    password = attr.ib()
In [15]:
user = pickle.loads(stored_user)
In [16]:
user
Out[16]:
User(name='Gejza', password=NOTHING)
In [17]:
user.password
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-17-9456b1ab438a> in <module>()
----> 1 user.password

AttributeError: 'User' object has no attribute 'password'
In [18]:
new_user = User('Harry', 'nbuSR123')
new_user
Out[18]:
User(name='Harry', password='nbuSR123')
In [19]:
new_user.password
Out[19]:
'nbuSR123'

Charon

Funkcionality

  • Kódovanie objektov do jednoduchých štruktúr a späť
  • Verzovanie
  • Testovacie nástroje

Príklad použitia

In [20]:
@attr.s
class User:
    name: str = attr.ib()
In [21]:
registry = charon.CodecRegistry()

@registry.dumper(User, version = 1)
def dump_user_v1(obj: User) -> str:
    return obj.name

@registry.loader(User, version = 1)
def load_user_v1(data: str) -> User:
    return User(data)

codec = charon.Codec([registry]) 
In [22]:
user = User('Gejza')
user
Out[22]:
User(name='Gejza')
In [23]:
encoded_user = codec.dump(user)
In [24]:
encoded_user
Out[24]:
{'!meta': {'dtype': 'User', 'version': 1}, 'params': 'Gejza'}
In [25]:
restored_object = codec.load(encoded_user)
In [26]:
restored_object == user
Out[26]:
True
In [27]:
restored_object
Out[27]:
User(name='Gejza')
In [28]:
user
Out[28]:
User(name='Gejza')
In [29]:
@attr.s
class User:
    name: str = attr.ib()
    password: str = attr.ib() # <~ 
In [30]:
registry = charon.CodecRegistry()

@registry.dumper(User, version = 2)
def dump_user_v2(obj: User) -> Dict[str, Any]:
    return {
        'name': obj.name,
        'password': obj.password
    }

@registry.loader(User, version = 1)
def load_user_v1_migration(data: str) -> User:
    return User(name = data, password = None)

@registry.loader(User, version = 2)
def load_user_v2(data: Dict[str, Any]) -> User:
    return User(**data)

codec = charon.Codec([registry])
In [31]:
restored_user = codec.load(encoded_user)
restored_user
Out[31]:
User(name='Gejza', password=None)
In [ ]:
user == restored_user
Out[ ]:
False

Výkonnostné porovnanie

List of objects

List not cythonized
List cythonized charon
List cythonized charon with codecs

Nested objects

Nested not cythonized
Nested cythonized charon
Nested cythonized charon with codecs

Testy

  • Serializačnej pipeline
  • Verzií kodekov
  • Testy na existenciu testov

Reálne nasadenie

Naša aplikácia

  • Redis na ukladanie stavu
  • Mapovanie Redis objektových typov do Python
  • Sledovanie stavu objektov a udržiavanie histórie
    • Vnorené objekty (zoznamy a slovníky)
    • História objektov (> 10 záznamov v zozname)
  • Veľký počet sledovaných objektov, rádovo 100 000
  • Viacero inštancií aplikácie ~> rádovo 1_000_000 objektov denne

Migrácia

  • Načítavanie v pickle a aj charon
  • Ukladanie v pickle

  • Načítavanie v pickle a aj charon
  • Ukladanie v charon

  • Načítavanie v charon
  • Ukladanie v charon

Problém - Pomalé načítavanie

  • Hladanie úzkeho hrdla - cprofile
  • Hladanie veľkých objektov
  • Optimalizácia kodekov - použitie __new__
  • Vytvorenie natívnej knižnice z `charon` - cython
  • Vytvorenie natívnej knižnice z kodekov - cython
  • Načítanie zoznamov stále pomalé - lazy-loading kde to bolo možné

Po pár mesiacoch

  • Bez pickle
  • Bez výrazného spomalenia
  • Testy ochraňujú pri zmene štruktúry dát
  • Refactoring dátových tried zjednodušený

Využitie v ďaľších aplikáciách

Rovnaké datové typy v jadre

  • Vytvorenie štandardnej knižnice kodekov
    • decimal.Decimal
    • datetime.{datetime, date, time, timedelta}
    • set
    • frozenset

    Prečo práve takto

    • Jednoduché spracovanie
    • Jednoznačna definícia ako objekt rozložit a znova zložiť
      • Explicit is better than implicit