많은 웹 애플리케이션은 MySQL과 같은 관계형 데이터베이스 관리 시스템에 데이터를 저장합니다. 그것에서 그것을 브라우저에 표시합니다. 그러나 데이터의 양이 증가하고 접속이 집중될수록 데이터베이스에 대한 부담 증가, 데이터베이스 응답 저하, 웹 사이트 표시 지연 등의 부작용이 발생할 수 있습니다. 분산 캐싱은 웹사이트 성능을 최적화하는 중요한 수단입니다. 많은 사이트가 확장 가능한 서버 클러스터를 통해 대규모 핫 데이터 캐싱 서비스를 제공하고 있습니다. 데이터 캐싱 라이브러리 쿼리 결과를 저장하고 데이터베이스 액세스 횟수를 줄임으로써 동적 웹 애플리케이션의 속도와 확장성을 크게 향상시킬 수 있습니다. 업계에서 흔히 사용하는 것으로는 redis, memcached 등이 있습니다. 오늘 이야기하고 싶은 것은 python 프로젝트에서 memcached 캐시 서비스를 사용하는 방법입니다. .
memcached는 오픈 소스, 고성능, 분산 메모리 객체캐싱 시스템입니다. 적용 다양한 시나리오에 캐싱이 필요하며, 그 주요 목적은 데이터베이스에 대한 액세스를 줄여 웹 애플리케이션 속도를 높이는 것입니다.
Memcached 자체는 실제로 분산 솔루션을 제공하지 않습니다. 서버 측에서 memcached 클러스터 환경은 실제로 memcached 서버의 축적이며, 환경 구성은 상대적으로 간단합니다. 캐시 배포는 주로 클라이언트에서 구현되며 클라이언트의 를 통해 처리됩니다. 라우팅 분산 솔루션의 목적을 달성합니다. 클라이언트 라우팅의 원리는 매우 간단합니다. 애플리케이션 서버는 특정 키의 값에 액세스할 때마다 라우팅 알고리즘을 통해 해당 키를 특정 memcached 서버 nodeA에 매핑합니다. nodeA에서 수행됩니다. 서버가 여전히 데이터를 캐시하는 한 캐시 적중은 보장됩니다.
나머지를 이용한 단순 라우팅 알고리즘해시: 해시 값을 사용 캐시된 데이터 키를 서버 수로 나눈 나머지는 서버 목록 아래 표의 숫자입니다. 이 알고리즘은 memcached 클러스터 전체에 캐시 데이터를 균등하게 배포할 수 있으며 대부분의 캐시 라우팅 요구 사항도 충족할 수 있습니다.
그러나 memcached 클러스터를 확장해야 하는 경우 문제가 발생합니다. 예를 들어 웹사이트는 3개의 캐시 서버 용량을 4개의 캐시 서버로 확장해야 합니다. 서버 목록을 변경한 후에도 나머지 해시를 계속 사용한다면 요청의 75%가 캐시에 도달하지 않을 것이라고 쉽게 계산할 수 있습니다. 서버 클러스터의 규모가 커질수록 미스율도 높아집니다.
1%3 = 1 1%4 = 1 2%3 = 2 2%4 = 2 3%3 = 0 3%4 = 3 4%4 = 1 4%4 = 0 #以此类推
이러한 확장 작업은 매우 위험하며 데이터베이스에 즉각적인 압력을 가할 수 있으며 심지어 데이터베이스 충돌을 일으킬 수도 있습니다. 이 문제를 해결하는 방법에는 두 가지가 있습니다. 1. 액세스가 적을 때 용량을 확장하고 확장 후 데이터를 준비합니다. 2. 더 나은 라우팅 알고리즘을 사용합니다. 현재 가장 일반적으로 사용되는 알고리즘은 일관된 해시 알고리즘입니다.
그림과 같이 Memcached 클라이언트는 일관적 해시 알고리즘을 라우팅 전략으로 사용할 수 있습니다. 알고리즘 제외 키의 해시 값을 계산하는 것 외에도 각 서버에 해당하는 해시 값도 계산한 다음 이러한 해시 값을 제한된 값 범위(예: 0~2^32)에 매핑합니다. 해시(키)보다 큰 해시 값을 갖는 가장 작은 서버를 찾아 키 데이터를 저장하는 대상 서버로 사용합니다. 찾을 수 없는 경우에는 해시 값이 가장 작은 서버를 바로 대상 서버로 사용합니다. 동시에 단일 노드를 추가하거나 삭제해도 전체 클러스터에 큰 영향을 미치지 않습니다.
가상 레이어
为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached、重启操作系统会导致全部数据消失。另外,缓存的内容容量达到指定值之后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。memcached本身是为缓存而设计的服务,因此并没有过多考虑数据的永久性问题。
memcached仅支持基础的key-value键值对类型数据存储。在memcached内存结构中有两个非常重要的概念:slab和chunk。
slab是一个内存块,它是memcached一次申请内存的最小单位。在启动memcached的时候一般会使用参数-m指定其可用内存,但是并不是在启动的那一刻所有的内存就全部分配出去了,只有在需要的时候才会去申请,而且每次申请一定是一个slab。Slab的大小固定为1M(1048576 Byte),一个slab由若干个大小相等的chunk组成。每个chunk中都保存了一个item结构体、一对key和value。
虽然在同一个slab中chunk的大小相等的,但是在不同的slab中chunk的大小并不一定相等,在memcached中按照chunk的大小不同,可以把slab分为很多种类(class),默认情况下memcached把slab分为40类(class1~class40),在class 1中,chunk的大小为80字节,由于一个slab的大小是固定的1048576字节(1M),因此在class1中最多可以有13107个chunk(也就是这个slab能存最多13107个小于80字节的key-value数据)。
memcached内存管理采取预分配、分组管理的方式,分组管理就是我们上面提到的slab class,按照chunk的大小slab被分为很多种类。内存预分配过程是怎样的呢?向memcached添加一个item时候,memcached首先会根据item的大小,来选择最合适的slab class:例如item的大小为190字节,默认情况下class 4的chunk大小为160字节显然不合适,class 5的chunk大小为200字节,大于190字节,因此该item将放在class 5中(显然这里会有10字节的浪费是不可避免的),计算好所要放入的chunk之后,memcached会去检查该类大小的chunk还有没有空闲的,如果没有,将会申请1M(1个slab)的空间并划分为该种类chunk。例如我们第一次向memcached中放入一个190字节的item时,memcached会产生一个slab class 2(也叫一个page),并会用去一个chunk,剩余5241个chunk供下次有适合大小item时使用,当我们用完这所有的5242个chunk之后,下次再有一个在160~200字节之间的item添加进来时,memcached会再次产生一个class 5的slab(这样就存在了2个pages)。
chunk是在page里面划分的,而page固定为1m,所以chunk最大不能超过1m。
chunk实际占用内存要加48B,因为chunk数据结构本身需要占用48B。
如果用户数据大于1m,则memcached会将其切割,放到多个chunk内。
已分配出去的page不能回收。
-对于key-value信息,最好不要超过1m的大小;同时信息长度最好相对是比较均衡稳定的,这样能够保障最大限度的使用内存;同时,memcached采用的LRU清理策略,合理甚至过期时间,提高命中率。
key-value能满足需求的前提下,使用memcached分布式集群是较好的选择,搭建与操作使用都比较简单;分布式集群在单点故障时,只影响小部分数据异常,目前还可以通过Magent缓存代理模式,做单点备份,提升高可用;整个缓存都是基于内存的,因此响应时间是很快,不需要额外的序列化、反序列化的程序,但同时由于基于内存,数据没有持久化,集群故障重启数据无法恢复。高版本的memcached已经支持CAS模式的原子操作,可以低成本的解决并发控制问题。
$ sudo apt-get install memcached $ memcached -m 32 -p 11211 -d # memcached将会以守护程序的形式启动 memcached(-d),为其分配32M内存(-m 32),并指定监听 localhost的11211端口。
在python中可通过memcache库来操作memcached,这个库使用很简单,声明一个client就可以读写memcached缓存了。
#!/usr/bin/env pythonimport memcache mc = memcache.Client(['127.0.0.1:12000'],debug=0) mc.set("some_key", "Some value") value = mc.get("some_key") mc.set("another_key", 3) mc.delete("another_key") mc.set("key", "1") # note that the key used for incr/decr must be a string. mc.incr("key") mc.decr("key")
然而,python-memcached默认的路由策略没有使用一致性哈希。
def _get_server(self, key): if isinstance(key, tuple): serverhash, key = key else: serverhash = serverHashFunction(key) if not self.buckets: return None, None for i in range(Client._SERVER_RETRIES): server = self.buckets[serverhash % len(self.buckets)] if server.connect(): # print("(using server %s)" % server,) return server, key serverhash = serverHashFunction(str(serverhash) + str(i)) return None, None
从源码中可以看到:server = self.buckets[serverhash % len(self.buckets)]
,只是根据key进行了简单的取模。我们可以通过重写_get_server
方法,让python-memcached支持一致性哈希。
import memcacheimport typesfrom hash_ring import HashRingclass MemcacheRing(memcache.Client): """Extends python-memcache so it uses consistent hashing to distribute the keys. """ def init(self, servers, *k, **kw): self.hash_ring = HashRing(servers) memcache.Client.init(self, servers, *k, **kw) self.server_mapping = {} for server_uri, server_obj in zip(servers, self.servers): self.server_mapping[server_uri] = server_obj def _get_server(self, key): if type(key) == types.TupleType: return memcache.Client._get_server(key) for i in range(self._SERVER_RETRIES): iterator = self.hash_ring.iterate_nodes(key) for server_uri in iterator: server_obj = self.server_mapping[server_uri] if server_obj.connect(): return server_obj, key return None, None
这里采用的策略是:1. 应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。2. 应用程序从cache中取数据,取到后返回。缓存更新是一个很复杂的问题,一般是先把数据存到数据库中,成功后,再让缓存失效。后面会再写文单独讨论memcached缓存更新的问题。
# coding: utf-8import sysimport tornado.ioloopimport tornado.webimport loggingimport memcacheimport jsonimport urllib# 初始化memcache clientmc = memcache.Client(['127.0.0.1:11211'], debug=0) mc_prefix = 'demo'class BaseHandler(tornado.web.RequestHandler): """ 把缓存处理抽象到BaseHandler基类 """ USE_CACHE = False # 控制是否使用缓存 def format_args(self): arg_list = [] for a in self.request.arguments: for value in self.request.arguments[a]: arg_list.append('%s=%s' % (a, urllib.quote(value.replace(' ', '')))) # 根据请求的URL产生key arg_list.sort() key = '%s?%s' % (self.request.path, '&'.join(arg_list)) if arg_list else self.request.path key = '%s_%s' % (mc_prefix, key) # key太长,不进行缓存处理 if len(key) > 250: logging.error('key out of length: %s', key) return None return key def get(self, *args, **kwargs): if self.USE_CACHE: try: # 根据请求获取key self.key = self.format_args() if self.key: data = mc.get(self.key) # 若缓存命中,则直接返回数据 if data: logging.info('get data from memecahce') self.finish(data) return except Exception, e: logging.exception(e) # 若未命中缓存,调用do_get处理请求,获取数据 data = self.do_get() data_str = json.dumps(data) # 把成功获取到的数据,放入memcache缓存 if self.USE_CACHE and data and data.get('result', -1) == 0 and self.key: try: mc.set(self.key, data_str, 60) except Exception, e: logging.exception(e) self.finish(data_str) def do_get(self): return Noneclass DemoHandler(BaseHandler): USE_CACHE = True def do_get(self): a = self.get_argument('a', 'test') b = self.get_argument('b', 'test') # 访问数据库获取数据,此处略去 data = {'result': 0, 'a': a, 'b': b} return datadef make_app(): return tornado.web.Application([ (r"/", DemoHandler), ])if name == "main": logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s %(levelno)s %(message)s', ) app = make_app() app.listen(8888) tornado.ioloop.IOLoop.current().start()
在浏览器访问http://127.0.0.1:8888/?a=1&b=3
,终端打印的log如下:
2017-02-21 22:45:05,987 20 304 GET /?a=1&b=2 (127.0.0.1) 3.11ms 2017-02-21 22:45:07,427 20 get data from memecahce 2017-02-21 22:45:07,427 20 304 GET /?a=1&b=2 (127.0.0.1) 0.71ms 2017-02-21 22:45:10,350 20 200 GET /?a=1&b=3 (127.0.0.1) 0.82ms 2017-02-21 22:45:13,586 20 get data from memecahce
从日志可以看到,缓存命中的情况。
本文介绍了memcached的路由算法、内存管理、使用场景等基本概念,然后举例说明了在python项目中如何使用memcached缓存。缓存更新的问题还需要进一步分析讨论。
위 내용은 Python에서 memcached 동작에 대한 자세한 설명(그림 및 텍스트)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!