遍歷百萬級Redis的鍵值的續集

背景

在完成腳本Redis的key的遍歷腳本之後,原以爲事情就這麼過去了,在同事試用腳本之後,拿了一個線上的集羣做了測試,響應速度非常滿意,覺得不錯但是qps過高擔心影響線上業務。於是我查看了測試環境的qps之後發現遍歷五百萬key的時候,qps會非常高。

redis-cli -p 6379 -r 100 -i 1 info|grep ops
instantaneous_ops_per_sec:104245
instantaneous_ops_per_sec:106227
instantaneous_ops_per_sec:109910
instantaneous_ops_per_sec:105947
instantaneous_ops_per_sec:107111
instantaneous_ops_per_sec:104354
instantaneous_ops_per_sec:103974
instantaneous_ops_per_sec:106596
instantaneous_ops_per_sec:97469
instantaneous_ops_per_sec:99605
instantaneous_ops_per_sec:98182
instantaneous_ops_per_sec:100320
instantaneous_ops_per_sec:99811
instantaneous_ops_per_sec:94457
instantaneous_ops_per_sec:103132
instantaneous_ops_per_sec:110565
instantaneous_ops_per_sec:104259
instantaneous_ops_per_sec:93459
instantaneous_ops_per_sec:95340
instantaneous_ops_per_sec:99658
instantaneous_ops_per_sec:106938
instantaneous_ops_per_sec:109205

從採樣的數據可用看見qps最高大概有十萬左右,在測試環境中大概整個腳本跑完花了八十秒左右,測試環境(還是那個八核,6G的虛擬機),但是此時來查看一下虛擬機的性能指標,

 5556 root      20   0 1527188  41608   2144 R  95.7  0.3   0:31.81 python
 5559 root      20   0 1527180  41556   2144 R  95.3  0.3   0:31.79 python
 5558 root      20   0 1527184  41576   2144 R  95.0  0.3   0:31.73 python
 5561 root      20   0 1527212  41592   2144 R  95.0  0.3   0:31.90 python
 5554 root      20   0 1527112  41580   2144 R  94.7  0.3   0:31.89 python
 4224 root      20   0  778136 501036   1032 R  38.5  3.1 182:47.31 redis-server

運行過程中,redis-server的cpu利用率其實都不算高,內存利用率也不算高,但是qps確非常的高。根據同事的反饋如果線上的qps在三四萬的時候,運行qps的採樣程序也會有些許卡頓並且CPU利用率是飆升,並且使用方就會把redis的訪問時間太長的告警。但是在測試環境測試的時候,採樣程序並無卡頓,cpu佔用率也不算高,這不僅引起了我的思考,這是爲什麼。

追根問底-Redis的qps計數機制

本文就講究看着5.0.4的代碼吧,首先找到了info命令的具體過程;

void infoCommand(client *c) {
    char *section = c->argc == 2 ? c->argv[1]->ptr : "default";

    if (c->argc > 2) {
        addReply(c,shared.syntaxerr);
        return;
    }
    addReplyBulkSds(c, genRedisInfoString(section));  // 直接就是獲取redis的info信息
}

sds genRedisInfoString(char *section) {
    ...

    /* Stats */
    if (allsections || defsections || !strcasecmp(section,"stats")) {
        if (sections++) info = sdscat(info,"\r\n");
        info = sdscatprintf(info,
            "# Stats\r\n"
            "total_connections_received:%lld\r\n"
            "total_commands_processed:%lld\r\n"
            "instantaneous_ops_per_sec:%lld\r\n"
            "total_net_input_bytes:%lld\r\n"
            "total_net_output_bytes:%lld\r\n"
            "instantaneous_input_kbps:%.2f\r\n"
            "instantaneous_output_kbps:%.2f\r\n"
            "rejected_connections:%lld\r\n"
            "sync_full:%lld\r\n"
            "sync_partial_ok:%lld\r\n"
            "sync_partial_err:%lld\r\n"
            "expired_keys:%lld\r\n"
            "expired_stale_perc:%.2f\r\n"
            "expired_time_cap_reached_count:%lld\r\n"
            "evicted_keys:%lld\r\n"
            "keyspace_hits:%lld\r\n"
            "keyspace_misses:%lld\r\n"
            "pubsub_channels:%ld\r\n"
            "pubsub_patterns:%lu\r\n"
            "latest_fork_usec:%lld\r\n"
            "migrate_cached_sockets:%ld\r\n"
            "slave_expires_tracked_keys:%zu\r\n"
            "active_defrag_hits:%lld\r\n"
            "active_defrag_misses:%lld\r\n"
            "active_defrag_key_hits:%lld\r\n"
            "active_defrag_key_misses:%lld\r\n",
            server.stat_numconnections,
            server.stat_numcommands,
            getInstantaneousMetric(STATS_METRIC_COMMAND),  // 獲取qps
            server.stat_net_input_bytes,
            server.stat_net_output_bytes,
            (float)getInstantaneousMetric(STATS_METRIC_NET_INPUT)/1024,
            (float)getInstantaneousMetric(STATS_METRIC_NET_OUTPUT)/1024,
            server.stat_rejected_conn,
            server.stat_sync_full,
            server.stat_sync_partial_ok,
            server.stat_sync_partial_err,
            server.stat_expiredkeys,
            server.stat_expired_stale_perc*100,
            server.stat_expired_time_cap_reached_count,
            server.stat_evictedkeys,
            server.stat_keyspace_hits,
            server.stat_keyspace_misses,
            dictSize(server.pubsub_channels),
            listLength(server.pubsub_patterns),
            server.stat_fork_time,
            dictSize(server.migrate_cached_sockets),
            getSlaveKeyWithExpireCount(),
            server.stat_active_defrag_hits,
            server.stat_active_defrag_misses,
            server.stat_active_defrag_key_hits,
            server.stat_active_defrag_key_misses);
    }

    ...
    return info;
}

/* Return the mean of all the samples. */
long long getInstantaneousMetric(int metric) {
    int j;
    long long sum = 0;

    for (j = 0; j < STATS_METRIC_SAMPLES; j++)
        sum += server.inst_metric[metric].samples[j];  // 統計最近十六次的平均值
    return sum / STATS_METRIC_SAMPLES;
}

現在代碼看到這裏了就大致知道server保存了最近一個十六的數組,然後把所有值取平均就是qps,那這個inst_metric是在哪裏生成的呢?就在redis自帶的定時任務中執行每一百毫秒執行一次;

 // 位於server.c的sererCron函數中
 				run_with_period(100) {                                                         // 通過loop執行的次數來判斷是否執行或者小於一個執行hz的時間,從而擴展到指定的時間長度餓回調
        trackInstantaneousMetric(STATS_METRIC_COMMAND,server.stat_numcommands);    // 每次都運行採樣數據並保存
        trackInstantaneousMetric(STATS_METRIC_NET_INPUT,
                server.stat_net_input_bytes);
        trackInstantaneousMetric(STATS_METRIC_NET_OUTPUT,
                server.stat_net_output_bytes);
    }
    
    /* Add a sample to the operations per second array of samples. */
void trackInstantaneousMetric(int metric, long long current_reading) {
    long long t = mstime() - server.inst_metric[metric].last_sample_time;
    long long ops = current_reading -
                    server.inst_metric[metric].last_sample_count;
    long long ops_sec;

    ops_sec = t > 0 ? (ops*1000/t) : 0;

    server.inst_metric[metric].samples[server.inst_metric[metric].idx] =
        ops_sec;
    server.inst_metric[metric].idx++;
    server.inst_metric[metric].idx %= STATS_METRIC_SAMPLES;
    server.inst_metric[metric].last_sample_time = mstime();
    server.inst_metric[metric].last_sample_count = current_reading;
}

原來就是通過stat_numcommands來計數100ms中執行了多少個命令,然後獲取qps,最後將最近16次的qps再取平均即得到終端輸出的qps的值,現在看到這裏好像一切都是那麼順利成章,通過統計單位時間內的命令執行數來,此時我閉起來雙眼,如果沒擦錯的話,redis把我的5000個的memusge的pipeline命令的請求就計數爲5000次了。

void call(client *c, int flags) {
    ...
    server.stat_numcommands++;
}

int processCommand(client *c) {
    ...

    /* Exec the command */
    if (c->flags & CLIENT_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
    {
        queueMultiCommand(c);
        addReply(c,shared.queued);
    } else {
        call(c,CMD_CALL_FULL);                      // 執行命令
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnKeys();
    }
    return C_OK;
}

void processInputBuffer(client *c) {
    server.current_client = c;                          // 設置當前處理的連接的客戶端

    /* Keep processing while there is something in the input buffer */
    while(c->qb_pos < sdslen(c->querybuf)) {            // 檢查長度是否符合要求, 循環執行 因爲緩衝區可能會接受多個命令
        /* Return if clients are paused. */
        if (!(c->flags & CLIENT_SLAVE) && clientsArePaused()) break;

        /* Immediately abort if the client is in the middle of something. */
        if (c->flags & CLIENT_BLOCKED) break;

        /* Don't process input from the master while there is a busy script
         * condition on the slave. We want just to accumulate the replication
         * stream (instead of replying -BUSY like we do with other clients) and
         * later resume the processing. */
        if (server.lua_timedout && c->flags & CLIENT_MASTER) break;

        /* CLIENT_CLOSE_AFTER_REPLY closes the connection once the reply is
         * written to the client. Make sure to not let the reply grow after
         * this flag has been set (i.e. don't process more commands).
         *
         * The same applies for clients we want to terminate ASAP. */
        if (c->flags & (CLIENT_CLOSE_AFTER_REPLY|CLIENT_CLOSE_ASAP)) break;

        /* Determine request type when unknown. */
        if (!c->reqtype) {                                                      // 通過傳入的第一個數據來檢查是否是多個命令參數
            if (c->querybuf[c->qb_pos] == '*') {
                c->reqtype = PROTO_REQ_MULTIBULK;
            } else {
                c->reqtype = PROTO_REQ_INLINE;
            }
        }

        if (c->reqtype == PROTO_REQ_INLINE) {                                   // 檢查接受數據的參數類型
            if (processInlineBuffer(c) != C_OK) break;                          // 解析一個命令參數
        } else if (c->reqtype == PROTO_REQ_MULTIBULK) {
            if (processMultibulkBuffer(c) != C_OK) break;                       // 解析多個命令參數
        } else {
            serverPanic("Unknown request type");
        }

        /* Multibulk processing could see a <= 0 length. */
        if (c->argc == 0) {                                             
            resetClient(c);                                                     // 重置標誌位和參數
        } else {
            /* Only reset the client when the command was executed. */      // 
            if (processCommand(c) == C_OK) {                                    // 處理參數查找命令
                if (c->flags & CLIENT_MASTER && !(c->flags & CLIENT_MULTI)) {
                    /* Update the applied replication offset of our master. */
                    c->reploff = c->read_reploff - sdslen(c->querybuf) + c->qb_pos;     
                }

                /* Don't reset the client structure for clients blocked in a
                 * module blocking command, so that the reply callback will
                 * still be able to access the client argv and argc field.
                 * The client will be reset in unblockClientFromModule(). */
                if (!(c->flags & CLIENT_BLOCKED) || c->btype != BLOCKED_MODULE)
                    resetClient(c);
            }
            /* freeMemoryIfNeeded may flush slave output buffers. This may
             * result into a slave, that may be the active client, to be
             * freed. */
            if (server.current_client == NULL) break;
        }
    }

    /* Trim to pos */
    if (server.current_client != NULL && c->qb_pos) {
        sdsrange(c->querybuf,c->qb_pos,-1);
        c->qb_pos = 0;
    }

    server.current_client = NULL;                       // 處理完成置爲空
}


void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    client *c = (client*) privdata;
    int nread, readlen;
    size_t qblen;
    UNUSED(el);
    UNUSED(mask);

    readlen = PROTO_IOBUF_LEN;
    /* If this is a multi bulk request, and we are processing a bulk reply
     * that is large enough, try to maximize the probability that the query
     * buffer contains exactly the SDS string representing the object, even
     * at the risk of requiring more read(2) calls. This way the function
     * processMultiBulkBuffer() can avoid copying buffers to create the
     * Redis Object representing the argument. */
    if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
        && c->bulklen >= PROTO_MBULK_BIG_ARG)                                       // 檢查該命令是否是多個命令的請求
    {
        ssize_t remaining = (size_t)(c->bulklen+2)-sdslen(c->querybuf);

        /* Note that the 'remaining' variable may be zero in some edge case,
         * for example once we resume a blocked client after CLIENT PAUSE. */
        if (remaining > 0 && remaining < readlen) readlen = remaining;
    }

    qblen = sdslen(c->querybuf);                                                    // 獲取保存的緩衝區大小
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);                             // 擴充內存來容納剩餘需要讀到的數據
    nread = read(fd, c->querybuf+qblen, readlen);                                   // 從連接中讀取數據
    if (nread == -1) {                                                              // 如果讀取的長度爲空則檢查是否連接出錯
        if (errno == EAGAIN) {
            return;
        } else {
            serverLog(LL_VERBOSE, "Reading from client: %s",strerror(errno));       // 釋放連接並打印錯誤
            freeClient(c);
            return;
        }
    } else if (nread == 0) {                                                        // 如果爲0 則表示連接關閉 釋放客戶端
        serverLog(LL_VERBOSE, "Client closed connection");
        freeClient(c);
        return;
    } else if (c->flags & CLIENT_MASTER) {
        /* Append the query buffer to the pending (not applied) buffer
         * of the master. We'll use this buffer later in order to have a
         * copy of the string applied by the last command executed. */
        c->pending_querybuf = sdscatlen(c->pending_querybuf,
                                        c->querybuf+qblen,nread);
    }

    sdsIncrLen(c->querybuf,nread);                                                  // 提升保存緩存區的大小
    c->lastinteraction = server.unixtime;                                           // 獲取時間
    if (c->flags & CLIENT_MASTER) c->read_reploff += nread;                 
    server.stat_net_input_bytes += nread;                                           // 保存新加入的數據處理長度
    if (sdslen(c->querybuf) > server.client_max_querybuf_len) {                     // 如果超過了緩衝區最大的接受長度則答應錯誤釋放內存關閉客戶端
        sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty(); 

        bytes = sdscatrepr(bytes,c->querybuf,64);
        serverLog(LL_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
        sdsfree(ci);
        sdsfree(bytes);
        freeClient(c);
        return;
    }

    /* Time to process the buffer. If the client is a master we need to
     * compute the difference between the applied offset before and after
     * processing the buffer, to understand how much of the replication stream
     * was actually applied to the master state: this quantity, and its
     * corresponding part of the replication stream, will be propagated to
     * the sub-slaves and to the replication backlog. */
    processInputBufferAndReplicate(c);                                      // 嘗試去解析輸入數據並根據解析的數據執行對應命令
}

代碼看到這裏,一切就清晰了,調用的邏輯如下

readQueryFromClient -> processInputBufferAndReplicate -> processInputBuffer -> processCommand -> call

從processInputBuffer一次接受的buff中依次挨個解析每一個命令,如果buffer一次性接受了5000個memusage命令,則會調用call5000次,即統計計數也會增加,但是爲什麼qps這麼高卻跟線上實際的現象不同呢。其實再想一想,線上的環境會被多個client連接,常用的命令基本上面沒有一次5000的pipeline,並且每個客戶端基本上都響應的是不同的短命令,而測試環境的redis其實只有幾個client,處理的事件響應數比較小,而且每次處理的時候都是處理很多命令組成一起的buffer一起發送,故qps高但是性能指標沒有明顯飆升。

線上環境:
A1 -- get命令 -->     
A2 -- set命令 -->    Redis事件(計數加1)
A3 -- get命令 -->
A4 -- get命令 -->


測試環境
A1 -- get命令/get命令/get命令 Redis事件(計數加3)


這樣一對比相比大家也都能看出區別吧。

重構-輕量簡潔的訪問

此時爲了不讓一下把所有的請求都打入到redis上面,並且在一定程度上能夠監控Redis的qps的值,以免壓力太大而影響線上業務。思來想去,好吧,直接解析協議吧,全部改爲異步IO來實現。

import asyncio
from asyncio import events
import time

import aioredis


redis_ip = "192.168.10.205"


STATUS = ["running", "stop", "waiting"]

RUNNING_STATUS = "running"

REDIS_MAX_QPS = 1000000


def change_status(status):
    global RUNNING_STATUS
    RUNNING_STATUS = status


def encode_command(*args, buf=None):
    if buf is None:
        buf = bytearray()
    buf.extend(b'*%d\r\n' % len(args))

    try:
        for arg in args:
            if isinstance(arg, str):
                arg = arg.encode("utf-8")
            buf.extend(b'$%d\r\n%s\r\n' % (len(arg), arg))
    except KeyError:
        raise TypeError("Argument {!r} expected to be of bytearray, bytes,"
                        " float, int, or str type".format(arg))
    return buf


def memory_usage_muti_t(keys=None):
    buf = bytearray()
    buf.extend(b'*%d\r\n$%d\r\n%s\r\n' % (1, len(b"MULTI"), b"MULTI"))
    for k in keys:
        buf.extend(encode_command(b"MEMORY", *["USAGE", k]))
    buf.extend(b'*%d\r\n$%d\r\n%s\r\n' % (1, len(b"EXEC"), b"EXEC"))
    return buf


def parse_memory_muti_i(buf, length):
    vals = buf.split("\r\n")
    if length > 0:
        length = -1 - length
    for v in vals[length:-1]:
        yield int(v[1:])


async def redis_pipeline(reader, writer, data):
    message = memory_usage_muti_t(data)
    length = len(data)
    writer.write(message)

    recv_buf = ""
    counts = length*2 + 2
    while True:
        try:
            recv = await reader.read(1024)
            if not recv:
                return
        except Exception as e:
            writer.close()
            print(e)
            raise e

        recv_buf += recv.decode()

        if counts == recv_buf.count("\r\n"):
            i = 0
            for v in parse_memory_muti_i(recv_buf, length):
                # print(data[i], v)
                i += 1
            break


def t_redis():
    loop = events.new_event_loop()

    async def monitor_qps():
        redis = await aioredis.create_redis('redis://{0}'.format(redis_ip))

        while RUNNING_STATUS in ["running", "waiting"]:
            await asyncio.sleep(0.1)
            redis_stats = await redis.info("stats")
            redis_qps = int(redis_stats["stats"]["instantaneous_ops_per_sec"])
            if redis_qps >= REDIS_MAX_QPS:
                print("qps  {0}".format(redis_qps))
                # RUNNING_STATUS = "waiting"
                if RUNNING_STATUS == "running":
                    change_status("waiting")
            else:
                if RUNNING_STATUS == "waiting":
                    change_status("running")

    async def go():
        redis = await aioredis.create_redis('redis://{0}'.format(redis_ip))

        reader, writer = await asyncio.open_connection(redis_ip, 6379,
                                                       loop=loop)
        work_count = 0

        async def scan_iter(count=5000):
            nonlocal work_count
            cursor = "0"
            pipe_count = 0
            while cursor != 0:
                cursor, data = await redis.scan(cursor=cursor, count=count)
                if len(data):
                    work_count += len(data)
                    pipe_count += 1
                    await redis_pipeline(reader, writer, data)
                print(work_count)
                # 判斷是否需要進行等待
                while RUNNING_STATUS == "waiting":
                    await asyncio.sleep(1)

        await scan_iter()

        print("total count key {0}".format(work_count))
        redis.close()
        # 關閉檢查qps的協程
        change_status("stop")
        print(RUNNING_STATUS)
        await redis.wait_closed()

    start = time.time()

    events.set_event_loop(loop)
    loop.set_debug(False)
    loop.run_until_complete(asyncio.gather(go(), monitor_qps()))
    end = time.time()
    print("finish use time {0} second".format(end-start))


if __name__ == '__main__':
    t_redis()

如果設置REDIS_MAX_QPS爲1000000(相當於對qps不加限制),此時跑完520萬左右的key大約需要67秒,性能數據如下;

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
11352 root      20   0  191168  14672   4556 R  74.4  0.1   0:20.43 python
 4224 root      20   0  778136 501504   1032 S  35.2  3.1 185:20.51 redis-server

如果此時我修改REDIS_MAX_QPS爲20000的話,當檢測到qps大於20000的時候就會停止訪問,此時的執行時間大概需要300秒,此時的性能數據;

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
10865 root      20   0  190904  14652   4556 S  13.2  0.1   0:44.06 python
 4224 root      20   0  778136 501504   1032 S   7.9  3.1 185:07.18 redis-server

通過兩次對比來看的話,確實限速之後的資源佔用率要少一些,但是對應的響應請求的時間就會長一些。事情到了這個地方,其實qps來進行訪問的限制還是有一定的問題的,如果某個集羣的qps一直大於設置的qps的值,那個這個redis基本上不會被執行一直在那裏檢查是否可以被允許查詢。魚和熊掌不可兼得。

重來-通過主從複製的原理來訪問

因爲redis支持主從複製,假如我僞裝成一個redis的從,然後接受redis節點發送的同步數據,通過同步數據來進行key大小的判斷,這個就變成了一個client端只需要被動的收數據就行不進行主動的訪問。

有了這個想法,就需要思考一下如何才能實現,redis的主從複製的原理查看代碼之後發現其實還是比較簡單的就是連接之後發送一個sync同步命令,同步對應的rdb文件,等同步完成之後就發送最新redis接受的操作命令從而達成主從數據一致性。

在完成主從複製之後,就需要解析rdb文件,當我查看了rdb文件的格式之後發現短期去實現一個解析工具還是有難度,那就使用現成的rdbtool這個工具來吧。

現在就需要組合這兩個想法,於是就有了如下代碼(代碼風格確實不好,而且有些邏輯處理不完善,大家僅供查看一下功能而已)。

import socket
import logging
import time
import threading

from rdbtools import RdbParser, KeyValsOnlyCallback
from rdbtools.encodehelpers import ESCAPE_CHOICES

logger = logging.getLogger(__package__)


start = time.time()

redis_ip = "192.168.10.205"
redis_port = 6379
key_size = 42


def encode_command(*args, buf=None):
    if buf is None:
        buf = bytearray()
    buf.extend(b'*%d\r\n' % len(args))

    try:
        for arg in args:
            if isinstance(arg, str):
                arg = arg.encode("utf-8")
            buf.extend(b'$%d\r\n%s\r\n' % (len(arg), arg))
    except KeyError:
        raise TypeError("Argument {!r} expected to be of bytearray, bytes,"
                        " float, int, or str type".format(arg))
    return buf


class RecvBuff(object):

    def __init__(self):
        self.buff = b""
        self.cond = threading.Condition()
        self.length = 0
        self.total_length = 0
        self.is_done = False

    def add(self, data):
        self.buff += data

    def acquire(self):
        self.cond.acquire()

    def release(self):
        self.cond.release()

    def wait(self, timeout=None):
        self.cond.wait(timeout=timeout)

    def notify(self):
        self.cond.notify_all()

    def consumer_length(self, n):
        if len(self.buff) >= n:
            r = self.buff[:n]
            self.buff = self.buff[n:]
            self.length += n
            if self.length == self.total_length:
                self.is_done = True
                raise
            return r
        else:
            while True:
                self.notify()
                self.wait()
                if len(self.buff) >= n:
                    r = self.buff[:n]
                    self.buff = self.buff[n:]
                    self.length += n
                    if self.length == self.total_length:
                        self.is_done = True
                        raise
                    return r


recv_buff = RecvBuff()


def rdb_work():
    # out_file_obj = os.fdopen(sys.stdout.fileno(), 'wb')
    class Writer(object):

        def write(self, value):
            if b" " in value:
                index = value.index(b" ")
                length = len(value)
                if length - index - 1 >= key_size:
                    print(value, index, length)

    out_file_obj = Writer()
    callback = {
        'justkeyvals': lambda f: KeyValsOnlyCallback(f, string_escape=ESCAPE_CHOICES[0]),
    }["justkeyvals"](out_file_obj)
    parser = RdbParser(callback)

    def parse(self, filename=None):
        class Reader(object):
            def __init__(self, buff):
                self.buff = buff

            def __enter__(self):
                return self

            def __exit__(self, exc_type, exc_val, exc_tb):
                pass

            def read(self, n):
                if n <= 0:
                    return
                res = self.buff.consumer_length(n)
                return res

            def close(self):
                pass
        f = Reader(recv_buff)
        self.parse_fd(f)
    setattr(parser, "parse", parse)

    recv_buff.acquire()
    parser.parse(parser)
    recv_buff.is_done = True
    recv_buff.notify()
    recv_buff.release()


class RedisServer(object):

    def __init__(self, host=None, port=None):
        self.host = host or "127.0.0.1"
        self.port = port or 6379
        self.conn = None
        self.recv_buff = recv_buff

    def init(self):
        try:
            self.conn = socket.socket()
            self.conn.connect((self.host, self.port))
        except Exception as e:
            logger.exception(e)
            self.conn = None
            return
        self.slave_sync()

    def slave_sync(self):
        self.send_sync()
        self.recv_buff.acquire()
        total_read_length = 0
        while True:
            data = self.conn.recv(1024 * 1)
            print("check ", data)
            if b"$" == data[:1]:
                length = len(data)
                for i in range(length-1):
                    if b"\r\n" == data[i:(i + 2)]:
                        break

                self.recv_buff.total_length = int(data[1:(i-2)].decode())
                left_data = data[(i+2):]
                total_read_length += len(left_data)
                if left_data:
                    self.recv_buff.add(left_data)
                    self.recv_buff.notify()
                    self.recv_buff.wait()
                break
            if b"\n" == data:
                continue

        while True:
            try:
                data = self.conn.recv(1024 * 8)
            except Exception as e:
                print("recv error : {0}".format(e))
                return
            if data:
                total_read_length += len(data)
                self.recv_buff.add(data)
                self.recv_buff.notify()
                self.recv_buff.wait()
                if self.recv_buff.is_done:
                    return

    def send_sync(self):
        data = encode_command("SYNC")
        try:
            self.conn.send(data)
        except Exception as e:
            return


if __name__ == '__main__':
    rs = RedisServer(redis_ip, redis_port)
    t = threading.Thread(target=rs.init)
    t.start()
    t1 = threading.Thread(target=rdb_work)
    t1.start()
    t.join()
    t1.join()
    end = time.time()
    print("finish use time {0}  second  ".format(end - start))


運行之後系統的監控如下;

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 1608 root      20   0  334428  10560   4368 S  99.7  0.1   0:30.99 python
 4224 root      20   0  778136 504568   1032 S   0.3  3.1 204:00.90 redis-server

可以看出,腳本的cpu利用率較高,但是redis-server的各項性能指標都比較平穩,cpu利用率也比異步執行的要低一些。並且該方法在分析比較大的redis的key時並不會將rdb文件落盤,從而節省了空間,但是該腳本的執行性能相對一般。

....
b'test_71c275a0-91d4-4745-b8d8-864ca37a63d0 test_71c275a0-91d4-4745-b8d8-864ca37a63d0' 41 83
b'test_d36ab798-2ee2-435a-8157-cf15cb42a5ce test_d36ab798-2ee2-435a-8157-cf15cb42a5ce' 41 83
b'test_29839173-db14-4c2c-8900-5ed139d7b7da test_29839173-db14-4c2c-8900-5ed139d7b7da' 41 83
b'test_c8636449-f06b-41c7-862a-3ae40c98e5f6 test_c8636449-f06b-41c7-862a-3ae40c98e5f6' 41 83
b'test_3f02114f-06f8-4f85-8133-415bd695137c test_3f02114f-06f8-4f85-8133-415bd695137c' 41 83
finish use time 149.25378394126892  second  

五百二十萬左右的key,大約需要149秒訪問成功。不過該方法目前來看對redis集羣影響相對較小。

總結

本文只是對上一次大量redis的key的節點訪問的一個繼續探索,因爲使用過程中發現有qps打滿cpu壓力過大等問題,就繼續做了改進,通過換成全部的異步IO和通過主從複製的原理來進行解析,各有優缺點,不過最後的通過主從複製的原理來使用的話,對redis的節點的影響相對較小,但是該方法需要連接的redis的節點的集羣狀態正確,不能是一個斷了主節點的從節點。由於本人才疏學淺,如有錯誤請批評指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章